转换模块(十一):实现 YUV 转 RGB
转换模块(十一)实现 YUV 转 RGB前置必须理解转换框架结构体、链表、注册本文档将彻底解构 YUV 转 RGB 的代码逻辑即使你对颜色格式一无所知也能从零搞懂文章目录 转换模块(十一)实现 YUV 转 RGB 本节核心学习目标 本节涉及的文件及其职责 前置知识YUV 与 RGB 是什么1️⃣ YUYV 格式精讲必须彻底理解内存排列用代码表示2️⃣ RGB565 格式与位操作必须亲手推算2.1 RGB565 的 16 位分配2.2 为什么绿色多 1 位2.3 将 8 位的 R/G/B 转换为 5/6/5 位2.4 打包成 16 位2.5 小端存储写入内存3️⃣ 查表法性能核心3.1 标准 YUV→RGB 公式了解即可不用背3.2 查表思想4️⃣ Yuv2RgbConvert 完整代码与数据流数据流图关键问题回答5️⃣ 与 09.1 框架的衔接✅ 自测题一问一答覆盖所有要点Q1: YUYV 格式中 4 个字节对应几个像素U 和 V 是每个像素独有还是共享Q2: 为什么一次循环要处理 4 个字节而不是 2 个字节Q3: RGB565 中红、绿、蓝各占多少位为什么绿色多 1 位Q4: 将 8 位的红色分量转换成 5 位时为什么要右移 3 位请用数学说明。Q5: color (r 11) | (g 5) | b; 这行代码做了什么请解释每一部分。Q6: 为什么要用 *dst color 0xFF; 和 *dst (color 8) 0xFF; 而不是直接写 *(unsigned short*)dst color;Q7: 查表法比直接计算浮点公式快多少为什么Q8: Yuv2RgbConvert 中为什么用 if (!pOut-aucPixelDatas) 来判断是否分配内存Q9: 输出缓冲区的内存由谁释放在哪个函数中释放Q10: 画出 YUV 转 RGB 的数据流从摄像头到 LCD 显示之前。 常见疑惑 解决方法 总结09.2 你到底学到了什么 本节核心学习目标学完本节你将能够说出YUYV 格式的内存排列4 字节 → 2 像素理解RGB565 打包的位操作为什么右移为什么或运算明白查表法如何替代浮点运算提高速度掌握Convert 函数的数据流输入从哪来输出放哪去内存谁分配谁释放能够回答 “为什么 YUV 转 RGB 要一次循环处理两个像素” 本节涉及的文件及其职责文件职责convert/convert_manager.h转换器结构体定义T_VideoConvertconvert/convert_manager.c链表管理、注册、查找、初始化入口convert/yuv2rgb.cYUV → RGB 的具体实现本节核心convert/color.c/color.h预计算查表避免浮点main.c主循环调用Convert完成格式转换 前置知识YUV 与 RGB 是什么RGB红、绿、蓝三原色每个像素独立存储 R、G、B 值常见 24 位或 16 位。YUV亮度Y 色度U、V。人眼对亮度敏感对颜色不敏感所以可以让多个像素共享一对U、V从而压缩数据量。摄像头为了节省带宽常输出YUYV格式又称 YUV422 打包格式。1️⃣ YUYV 格式精讲必须彻底理解内存排列每4 个字节描述两个像素text字节0: Y0 (第0个像素的亮度) 字节1: U (第0和第1个像素共享的蓝色色差) 字节2: Y1 (第1个像素的亮度) 字节3: V (第0和第1个像素共享的红色色差)接着下一组Y2, U, Y3, V以此类推。所以像素0的颜色 f(Y0, U, V)像素1的颜色 f(Y1, U, V)用代码表示cunsigned char *src; // 指向 YUYV 数据流 for (i 0; i width / 2; i) { Y0 src[0]; U src[1]; Y1 src[2]; V src[3]; src 4; // 移动至下一组 // 转换像素0 // 转换像素1 }width是图像宽度像素个数width/2就是有多少组因为一组两像素。src[0]、src[1]等就是内存中的原始字节。为什么循环次数是 width/2 而不是 width因为一次循环处理两个像素。2️⃣ RGB565 格式与位操作必须亲手推算2.1 RGB565 的 16 位分配位 15-11位 10-5位 4-0红色 (5 bit)绿色 (6 bit)蓝色 (5 bit)红色范围 0~31绿色范围 0~63蓝色范围 0~312.2 为什么绿色多 1 位人眼对绿色最敏感所以多给一位提高精度。这是历史惯例。2.3 将 8 位的 R/G/B 转换为 5/6/5 位8 位红色0~255 → 5 位红色0~31r5 r8 3右移 3 位8 位绿色0~255 → 6 位绿色0~63g6 g8 2右移 2 位8 位蓝色同理b5 b8 3为什么要右移因为要丢弃低 bit 的数据保留高位。例如 8 位红色取值 0~2555 位红色取值 0~31相当于除以 8取整右移 3 位就是除以 8。2.4 打包成 16 位cunsigned short color; color (r5 11) | (g6 5) | b5;r5 11将 5 位红色移到高 5 位位 15-11g6 5将 6 位绿色移到中间 6 位位 10-5b5蓝色已经在低 5 位位 4-0|按位或拼成一个 16 位数。2.5 小端存储写入内存大多数 ARM包括你的开发板采用小端模式低 8 位存在低地址高 8 位存在高地址。因此需要把color拆成两个字节写入c*dst color 0xFF; // 低字节 *dst (color 8) 0xFF; // 高字节color 0xFF取低 8 位(color 8) 0xFF右移 8 位取低 8 位即原高 8 位如果你觉得小端很绕可以暂时背下来先写低 8 位再写高 8 位。这是嵌入式常用写法。3️⃣ 查表法性能核心3.1 标准 YUV→RGB 公式了解即可不用背textR Y 1.402 * (V - 128) G Y - 0.344 * (U - 128) - 0.714 * (V - 128) B Y 1.773 * (U - 128)里面有浮点数每次转换都要计算非常慢。在嵌入式设备几百 MHz 的 ARM上每秒处理几十万像素就会卡顿。3.2 查表思想Y、U、V 的取值范围都是 0~255总共只有 256×256×256 种组合1600万但很多组合不会同时出现。常用的加速方法是预先计算每对 (V) 对 R 的贡献每对 (U) 对 B 的贡献以及 (U,V) 对 G 的贡献。在color.c中initLut()预先计算了cLutRv[v] (int)(1.402 * (v - 128) 0.5); // 四舍五入取整 LutGu[u] (int)(-0.344 * (u - 128) 0.5); LutGv[v] (int)(-0.714 * (v - 128) 0.5); LutBu[u] (int)( 1.773 * (u - 128) 0.5);然后定义宏c#define R_FROMYV(y,v) CLIP((y) LutRv[(v)]) #define G_FROMYUV(y,u,v) CLIP((y) LutGu[(u)] LutGv[(v)]) #define B_FROMYU(y,u) CLIP((y) LutBu[(u)])CLIP(x)确保结果在 0~255 之间大于255取255小于0取0。为什么快因为LutRv[v]是数组访问一次加法即可而浮点乘除需要几十个时钟周期。查表法将浮点运算变成了整数查表 加法。你不需要记住这些具体系数只需要知道查表法是用预计算的整数数组代替运行时浮点计算。4️⃣Yuv2RgbConvert完整代码与数据流cstatic int Yuv2RgbConvert(PT_VideoBuf ptVideoBufIn, PT_VideoBuf ptVideoBufOut) { PT_PixelDatas pIn ptVideoBufIn-tPixelDatas; PT_PixelDatas pOut ptVideoBufOut-tPixelDatas; // 1. 设置输出参数宽高与输入相同 pOut-iWidth pIn-iWidth; pOut-iHeight pIn-iHeight; pOut-iBpp (ptVideoBufOut-iPixelFormat V4L2_PIX_FMT_RGB565) ? 16 : 32; pOut-iLineBytes pOut-iWidth * pOut-iBpp / 8; pOut-iTotalBytes pOut-iLineBytes * pOut-iHeight; // 2. 分配输出缓冲区只在第一次调用时分配 if (!pOut-aucPixelDatas) { pOut-aucPixelDatas malloc(pOut-iTotalBytes); if (!pOut-aucPixelDatas) return -1; } // 3. 逐行转换具体转换函数省略但逻辑清楚 for (y 0; y pOut-iHeight; y) { // 从 pIn-aucPixelDatas y * pIn-iLineBytes 读取一行 YUYV // 转换成 RGB写入 pOut-aucPixelDatas y * pOut-iLineBytes Yuv422ToRgb565(行输入, 行输出, 宽度); } return 0; }数据流图textmain 主循环 │ ├─ GetFrame() --- tVideoBuf (填入原始 YUYV 数据) │ │ │ ▼ │ tVideoBuf.tPixelDatas.aucPixelDatas ────┐ │ │ └─ ptVideoConvert-Convert(tVideoBuf, tConvertBuf) │ │ │ │ Yuv2RgbConvert 内部 │ │ pIn-aucPixelDatas ──────────────────────┘ (读) │ pOut-aucPixelDatas malloc(...) (写) │ 执行逐行转换 │ ▼ tConvertBuf.tPixelDatas.aucPixelDatas (RGB 数据) ──→ 后续缩放/显示关键问题回答问题答案输入数据从哪来GetFrame从摄像头驱动映射的内存原始 YUYV输出数据放哪去Yuv2RgbConvert内部malloc的内存第一次之后复用谁分配内存Convert函数自己if (!pOut-aucPixelDatas)判断谁释放内存对应的ConvertExit函数由上层main 或框架在退出时主动调用为什么要自己 malloc因为转换后的 RGB 数据量通常不同于输入且调用者main事先不知道需要多大缓冲区为什么只 malloc 一次摄像头分辨率不变输出缓冲区大小固定复用可避免频繁 malloc/free提高性能注意在当前的main.c中没有调用ConvertExit因为程序无限循环。但框架设计是完整的如果以后需要重新初始化转换器就要先ConvertExit释放内存再重新调用Convert。5️⃣ 与 09.1 框架的衔接在main.c中cVideoConvertInit(); // 注册所有转换模块包括 yuv2rgb ptVideoConvert GetVideoConvertForFormats(iPixelFormatOfVideo, iPixelFormatOfDisp);GetVideoConvertForFormats遍历链表依次调用每个模块的isSupport。对于yuv2rgbisSupportYuv2Rgb判断输入格式 V4L2_PIX_FMT_YUYV输出格式 V4L2_PIX_FMT_RGB565或RGB32如果匹配返回该模块的指针供后续调用Convert。这样主程序完全不知道内部使用什么算法只关心输入输出格式。这就是面向接口编程。✅ 自测题一问一答覆盖所有要点Q1: YUYV 格式中 4 个字节对应几个像素U 和 V 是每个像素独有还是共享A1:4 个字节对应两个像素。U 和 V 是这两个像素共享的。Q2: 为什么一次循环要处理 4 个字节而不是 2 个字节A2:因为 YUYV 的原子单位是 4 字节Y0, U, Y1, V每次处理完这 4 字节就能得到两个 RGB 像素。如果一次只处理 2 字节无法同时拿到 U 和 V。Q3: RGB565 中红、绿、蓝各占多少位为什么绿色多 1 位A3:红 5 位绿 6 位蓝 5 位。绿色多 1 位是因为人眼对绿色最敏感提高精度。Q4: 将 8 位的红色分量转换成 5 位时为什么要右移 3 位请用数学说明。A4:8 位红色范围 0~2555 位红色范围 0~31。转换公式为r5 r8 / 8 r8 3。右移 3 位等价于除以 8 并取整。Q5:color (r 11) | (g 5) | b;这行代码做了什么请解释每一部分。A5:r 11将 5 位红色移到高 5 位位 15-11。g 5将 6 位绿色移到中间 6 位位 10-5。b蓝色已在低 5 位位 4-0。|按位或将三部分合并成一个 16 位整数。Q6: 为什么要用*dst color 0xFF;和*dst (color 8) 0xFF;而不是直接写*(unsigned short*)dst color;A6:直接写unsigned short会依赖处理器的字节序大端/小端。使用两个unsigned char赋值可以明确控制字节顺序小端低字节在前保证在不同平台上都能正确显示。这是可移植性写法。Q7: 查表法比直接计算浮点公式快多少为什么A7:快几十倍。因为数组访问 整数加法只需几个时钟周期而浮点乘法除法需要几十甚至上百个周期。在嵌入式设备上查表法是必须的优化手段。Q8:Yuv2RgbConvert中为什么用if (!pOut-aucPixelDatas)来判断是否分配内存A8:pOut-aucPixelDatas初始为NULL。第一次调用时条件成立分配内存。之后该指针非NULL不再分配直接复用。这样可以避免每一帧都分配内存大大提高效率。Q9: 输出缓冲区的内存由谁释放在哪个函数中释放A9:由Yuv2RgbConvertExit函数释放。该函数内部会free(pOut-aucPixelDatas)并将其置NULL。上层main应在程序退出或重新初始化转换器时调用它。Q10: 画出 YUV 转 RGB 的数据流从摄像头到 LCD 显示之前。A10:text摄像头硬件 → V4L2 驱动 → mmap 缓冲区 (YUYV) │ ▼ GetFrame() 填充 tVideoBuf.tPixelDatas.aucPixelDatas │ ▼ Yuv2RgbConvert() 读取上述内存转换后写入 tConvertBuf.tPixelDatas.aucPixelDatas (新分配) │ ▼ 缩放/合并模块 → 最终写入 framebuffer → LCD 常见疑惑 解决方法疑惑解答“我实在记不住 U/V 是什么公式更记不住”不用记。你只需要知道 U/V 是色差供两个像素共享具体计算由查表完成。“位操作看不懂”手算一个例子R200(0xC8), G100(0x64), B50(0x32)右移后 R25, G25, B6拼成 0b1100101100110 6502。多算几次就熟了。“为什么要分两个字节写直接用 16 位写不行吗”直接写 16 位在某些平台上可能因为对齐或字节序问题显示错误。用两个字节写是确定行为。“malloc 后为什么不 free会不会内存泄漏”程序一直运行内存一直使用不算泄漏。但退出时应当 free。框架提供ConvertExit就是为了在恰当的时候释放。“我什么时候需要自己写 YUV 转换”几乎不用。标准格式都有现成代码。你要做的是理解原理以便调试比如颜色发绿可能是 UV 反了。 总结09.2 你到底学到了什么YUYV 内存布局4 字节 → 2 像素U/V 共享。RGB565 打包右移 移位 或运算。小端写内存低字节在前。查表法预计算数组替代浮点运算。Convert 数据流输入从摄像头映射区来输出从 malloc 来内存由 Convert 自己分配由 ConvertExit 释放。框架适配通过isSupport自动匹配主程序无感知。这些知识足够你理解和调试 YUV 转 RGB 的代码并且为后面 MJPEG 转 RGB打下基础。 本文档会不断补充你遇到的任何新疑惑。把这一节读透你会发现自己已经能看懂之前觉得天书一般的yuv2rgb.c。