ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

图像格式与存储原理全解析:从RAW、Bayer到YUV、JPEG的工程实践

图像格式与存储原理全解析:从RAW、Bayer到YUV、JPEG的工程实践 做嵌入式ISP调试的那几年我被图像格式坑过的次数比被硬件坑过的次数还多。最典型的一次是sensor输出的RAW图在PC上直接显示成一片绿蒙蒙的噪点当时的直觉是数据线有问题折腾了一整天最后才发现是Bayer排布理解错了。从那以后我意识到搞图像处理的人如果不懂图像格式和存储原理就像搞音频的不懂采样率和位深一样后面所有的算法调试都是在沙滩上盖楼。这篇文章就把图像格式和存储原理这块地基彻底讲透从RAW到RGB再到YUV从内存排布到压缩编码结合ISP处理流程和实际工程经验帮你少走弯路。1. 图像格式的本质像素数据的语言契约1.1 从sensor出来的第一手数据Bayer阵列与RAW格式很多人学OpenCV或Matlab图像处理第一步就是imread拿到的直接就是RGB三通道的图。但真正的图像处理链路远不是从RGB开始的。在相机或者嵌入式视觉设备里sensor输出的是RAW格式这是所有图像格式里最接近物理世界的一层。CMOS图像传感器的感光原理决定了每个像素点只能记录一种颜色的光强通常通过覆盖在像素表面的滤色片CFAColor Filter Array实现。最常见的滤色片排布就是Bayer阵列也就是所谓的RGGB。每个2x2的像素单元里有两个绿色像素、一个红色像素、一个蓝色像素绿色多一倍是因为人眼对绿色最敏感。这个排布非常关键。传感器输出的RAW数据就是按Bayer排布排列的单通道数据而不是三通道。每个像素点的值直接对应某个颜色通道的光照强度。很多刚接触ISP的同学会问为什么不用RGB三色直接成像很简单如果每个像素点都要采集三个通道的光就得用分光棱镜或者三层堆叠结构成本、体积和感光效率全都不划算。Bayer阵列用一片滤色片加差值demosaic算法就能在成本和画质之间取得平衡。实际调试中Bayer排布的方向常常是调试日志里必须打印的第一条信息。常见的排布有RGGB、BGGR、GRBG、GBRG四种全看sensor厂商怎么贴滤色片。同一个sensor排布配置错了最直接的表现是整张图发绿或者发紫边缘部分会出现严重的伪彩。我自己的习惯是在bring-up阶段把四种排布各跑一遍拍一张白纸照片直接看哪个排布的颜色最接近灰白这个就是正确的。虽然看起来土但在没有标准色卡的情况下这个办法比看寄存器文档快得多。另外还要提醒一点Bayer排布不止水平方向的顺序还有垂直方向。如果只配置了水平方向的RGGB却忽略了垂直方向的交替出来的图像会有隔行色偏这个坑特别隐蔽。1.2 位深与色深为什么8bit、10bit、12bit差别这么大RAW数据的另一个重要属性是位深。常见的sensor输出有8bit、10bit、12bit甚至更高。位深表示每个像素点用多少个bit来描述亮度值。8bit就是0到25510bit是0到102312bit是0到4095。位深越高能记录的亮度层次就越多。但这里有一个工程上必须想清楚的问题高bit位深真的能带来视觉上明显的画质提升吗人眼能分辨的灰度级大概在几百到上千的范围内8bit理论上够用但经过ISP的gamma校正、色彩转换、降噪、锐化等一串处理后8bit的数据精度很快就撑不住了。每次运算都会引入量化误差误差积累到一定程度画面就会出现banding也就是平坦区域里肉眼可见的色带断层。这也是为什么专业的图像处理链路里sensor输出10bit或者12bitISP内部通常用16bit或者20bit的中间精度做处理最后输出到显示端时才压缩回8bit。关于位深还有一个值得展开的点高bit数据在内存里的存储方式。10bit数据存储时天然会遇到对齐问题。如果用16bit存一个10bit像素浪费6bit带宽成本高40%。有些方案会把3个10bit像素打包成32bit刚好填满一个32位总线宽度这样既不浪费空间又能保证数据对齐。当年做FPGA图像处理时为了处理这种打包格式我专门写了解包模块每次跨时钟域还要小心处理字节序问题。这块内容在后面的内存排布部分还会细讲。1.3 内存中的像素排布对齐、行长、交错与平面图像在内存里的存储方式决定了对它做处理的难易程度。同一张图用不同的排布方式处理效率能差出好几倍。最基础的概念是三种通道排布方式交错interleaved、平面planar和半平面semi-planar。交错排布就是最常见的RGB888格式内存里依次是BGR BGR BGR或者RGB RGB RGB每个像素的三个通道连续存放。平面排布则是所有B像素存在一个区域所有G像素存在另一个区域所有R像素再存在一个区域。半平面排布介于两者之间比如NV12格式Y数据单独一个平面UV数据交错存放在另一个平面。为什么会有平面排布因为有些硬件加速器在处理时希望每个通道的数据连续访问这样可以减少cache miss也可以更高效地做DMA传输。比如在做视频编码时YUV420 Planar就是H.264和H.265编码器最喜欢的输入格式。而普通显示设备更喜欢交错排布因为显存读取可以直接按像素连续读。做图像格式转换时本质上就是在这些不同的排布之间倒腾数据。内存对齐是另一个极其容易被忽视的问题。很多图像处理硬件要求每行数据按其总线宽度对齐。比如16字节对齐意味着每行数据的字节数必须是16的倍数。如果图像宽度是1920像素RGB888格式下每行是1920*35760字节能被16整除没问题。但如果宽度换成比如640像素的RGB888每行1920字节1920除以16刚好整除也没问题。真正出问题的是那些奇怪的宽度比如500像素的灰度图每行500字节500除以16等于31.25这时候就要在行的末尾填充padding字节补齐到512字节。如果你在FPGA或者DMA里做过图像采集就会发现如果忽略了行对齐图像就会变成斜的每行往后偏移一点从边缘看是一条斜线。排查这种问题最好的办法就是把原始数据按预期行长强行切分在PC上看切片后的图像是否正常。2. 常见图像格式逐个拆解RAW、RGB、YUV的分工与取舍2.1 RAWISP的底片不是给人看的RAW经常被人误解成没处理过的图像其实它更像摄影里的底片保留了sensor采集的所有原始信息但想要直接看是没法看的。RAW格式的设计目标就是给ISP流水线做输入让后续的去噪、坏点校正DPC、黑电平校正BLC、透镜阴影校正LSC、白平衡AWB、颜色插值Demosaic、颜色校正CCM、gamma校正等一系列处理有足够的原始数据可用。在ISP流水线里RAW数据在什么阶段被处理直接决定了该阶段的格式。比如黑电平校正是在RAW域完成的因为此时像素值还带着sensor基底噪声的偏移demosaic是在RAW域到RGB域的转换点做完这一布数据才变成真正的三通道彩色数据。所以调试ISP的时候每个阶段的dump图像格式都不一样你得清楚当前阶段是RGB还是YUV否则看到颜色不对不知道是前面的白平衡问题还是后面的色彩转换问题。跟RAW配套的还有一个关键元素叫metadata里面存了曝光时间、增益、白平衡参数、帧号等辅助信息。图像算法如果只用RAW数据而不看metadata很多问题是没法解释的。比如一个场景突然变暗你可以从metadata里看到增益拉高了随之噪声也放大了。很多工程团队把metadata和RAW数据打包成一个容器格式方便调试时做时间对齐。2.2 RGB算法处理时的工作台RGB是图像算法处理最顺手的格式。形态学操作、膨胀腐蚀、边缘检测、模板匹配这些算法的教科书实现基本都是基于RGB或者灰度图写的。热搜词里提到的OpenCV形态学处理膨胀与腐蚀就是典型的RGB域处理场景。RGB格式的优点是简单直观每个像素的颜色值就是三个通道的数值不需要任何额外的转换逻辑。缺点是数据量大三个通道带来的带宽和存储压力都不小。在RGB域处理时有一点要格外注意颜色空间的线性与否。sensor出来的RAW经过demosaic之后得到的RGB依然是线性光域的也就是数值和光强成正比。但经过gamma校正之后RGB变成了非线性域这时候做图像增强或者颜色混合效果和线性域完全不同。很多算法在RGB非线性域做出来的结果对比度看起来是高了但颜色会出现偏移因为没有在线性域做正确的亮度加权。这在做自动白平衡和颜色校正算法时尤其重要建议在处理链路上明确标注当前RGB是线性还是非线性这个信息应该写进代码注释不然一个月之后连你自己都会忘。2.3 YUV传输与压缩的性价比之王YUV大概是ISP领域最常打交道的一种格式手机上所有后处理、视频编码、网络传输几乎都离不开YUV。Y代表亮度UV代表色度。之所以把亮度和色度拆开是因为人眼对亮度细节敏感、对色度细节不敏感。基于这个生理特性可以在色度分量上做大幅度的下采样而视觉损失几乎察觉不到。这就是YUV 420、422这些概念的核心来源。以420为例每四个像素共享一组UV值具体来说每2x2的像素块里面四个Y值都保留但只有一个U和一个V值。这样一来相比于每个像素单独存RGB数据量直接从3字节降到1.5字节省了一半带宽。这就是为什么视频编码和ISP输出都喜欢用YUV 420。颜色精度有一定损失但对于显示和压缩场景这点损失完全可接受。在ISP的输出阶段YUV格式的排布选择也很讲究。NV12是Android和硬件视频编码器最常用的半平面格式Y平面加交错UV平面。而I420则是全平面格式Y、U、V三个平面各自独立。如果你在写格式转换代码一定要确认系统里预期的是NV12还是I420这两种格式的UV排布方向完全不同搞错了图像会变成奇怪的红蓝狗啃色。还有一个容易出错的点是UV平面的顺序NV12是先U后VNV21是先V后U顺序反了人脸肤色会明显偏紫偏绿。把U和V搞反在调试界有个老梗叫做阿凡达问题——整张图都是蓝紫色调。遇到这样的问题第一反应不是怀疑摄像头坏了而是检查UV顺序。3. 存储原理压缩、编码和数据布局的内在逻辑3.1 为什么图像需要压缩数据量的大账先算一笔简单的账。一幅1080P的RGB图像分辨率1920x1080每个像素3字节一帧的数据量就是192010803大概6.2MB。帧率30fps一秒钟数据量就是186MB一分钟11GB。如果只是做成像设备不考虑存储和传输这么大的数据量基本没法商用。而换成YUV 420格式同一帧数据量变成3.1MB直接减半。如果再上JPEG或者H.264压缩一帧可以压到几百KB甚至几十KB。这就是存储和传输必须做压缩的根本原因。图像压缩的核心理论依据是视觉冗余、空间冗余和时间冗余。视觉冗余指人眼对高频细节不敏感空间冗余指相邻像素之间存在强相关性自然图像里大部分区域都是平滑变化的时间冗余指视频帧与帧之间变化很小。JPEG主要利用空间冗余和视觉冗余H.264和H.265在视频场景下同时利用三种冗余。3.2 JPEG的压缩路径DCT变换与量化JPEG是图像处理里最经典的压缩格式它的原理值得每一个做图像处理的工程师细读一遍。JPEG压缩的步骤可以概括为颜色空间转换RGB转YCbCr、色度下采样、8x8分块DCT变换、量化、ZigZag扫描、熵编码。DCT变换是JPEG的核心步骤。8x8的像素块经过DCT变换后变成8x8的频率系数矩阵。左上角是直流分量代表整个块的平均亮度右下角是高频分量代表块的细节变化。自然图像的能量大部分集中在低频区域高频系数通常接近零。然后做量化量化表里低频区域的步长小、高频区域的步长大这一步就是真正丢信息的地方。高频系数很多被量化成0而0在后续编码里可以连续跳过所以JPEG能压得很小。如果你在工程实际中遇到了JPEG压缩后文字边缘出现振铃效应的问题根因就是高频量化太狠。所谓振铃效应就是文字边缘附近出现一圈一圈的暗纹像声波一样扩散原因是高频系数被强行置零后反变换时产生了吉布斯现象。解决思路一般是做ROI分块编码给文字区域压更低的量化系数或者改用JPEG-XR、WebP这类支持更细粒度控制的编码器。3.3 常见存储格式的适用场景JPEG、PNG、BMP、WebP存储格式之间没有绝对的优劣只有场景适配。JPEG适合照片类、色彩丰富、对细节要求不苛刻的内容它的核心优势是压缩率高。PNG则完全不同它采用无损压缩可以保留图像的Alpha透明通道适合截图、UI元素、带有硬边和文字的图像。BMP基本上不压缩纯粹是像素数据的裸拷贝好处是零解码成本坏处是体积巨大只适合在嵌入式环境里做帧缓冲备份这类场景。实战里有一条经验值得分享如果你把一个PNG转成JPG之后发现图片体积没变小多少大概率这张图原本就是一张偏平面的图比如电脑截图或者UI图标这类图用JPG压缩率高不到哪里去反而会有压缩伪影。反过来如果把一张照片转成PNG体积可能比JPEG大五倍以上。所以做图像处理项目时输出格式要跟随内容类型而不是跟着习惯走。还有一类特殊格式叫DNG它是RAW的标准化容器。DNG把sensor的RAW数据、metadata、色彩校正矩阵全部打包在一个标准格式里方便后期工具统一处理。如果你的ISP平台需要支持多种sensor建议考虑统一输出DNG格式做调试buffer这样可以最大限度保留调试信息。4. 实战踩坑格式问题如何毁掉整个调试流程4.1 把Bayer当RGB显示绿蒙蒙的图背后是什么这是新手最容易踩的坑。sensor直接输出的RAW数据如果你不做demosaic直接当成灰度图显示可以看但会看到密密麻麻的Bayer马赛克纹理。如果你把RAW数据当成RGB三通道图去解析那就是彻底的灾难——画面暗绿、细节模糊、颜色完全错乱。这个坑的深层原因是数据的语义错误。同样一段内存里的字节只有放在正确的格式上下文里才有正确的含义。我见过不少工程师在调试时做各种假设怀疑模数转换坏了、怀疑数据线短路了最后才发现是把RGGB排布下的RAW数据当成了别的格式在做解析。所以拿到任何一段图像数据第一步永远是确认格式元信息位深、通道数、排布方式、行长对齐。没有这些信息就动手写解析代码后面必定返工。4.2 内存对齐与行补齐FPGA传输里的斜线问题FPGA图像处理场景里行长对齐问题被放得很大。因为FPGA的数据传输通常按AXI总线或者自定义DMA的突发长度来搬运长度不对齐会导致跨时钟域的数据包边界错位。DMA控制器很可能在读到行长末尾时因为剩余字节数不够一个burst长度直接补零或者丢弃最终导致输出图像每行都有一个固定偏移。排查方法其实很简单。先用标准测试图比如棋盘格或者彩条做输入采集一段原始数据存成raw文件在PC上用Python或者Matlab按预期分辨率重新解析。如果图像整体斜着走几乎可以断定是行长padding参数不对。还有一个更隐蔽的连带问题如果你改了行长对齐参数没有同步改DMA的缓冲描述符运气好是图像错位运气差直接内存越界把系统搞崩。在FPGA调试阶段我强烈建议把地址越界检测模块打开这类bug非常难查越界可能几小时后才在另一个模块里暴露出来。4.3 Matlab和OpenCV读取图像时的隐藏差异Matlab和OpenCV是图像处理领域最常用的两个工具但它们在格式处理上有些让人高潮的地方。Matlab的imread默认把RGB图像读成double或者uint8的矩阵矩阵索引顺序是行列通道放在第三维。OpenCV的imread默认读成BGR顺序的Mat对象而不是RGB。如果两边混用颜色通道顺序经常被搞反画出来的图偏蓝。更隐蔽的是RAW数据读取的差异。Matlab对RAW的支持依赖tiff库的解析直接读二进制raw文件需要手动指定分辨率和位深。OpenCV的imread更干脆——它根本不支持直接读取sensor原始raw数据你必须用fopen、fread自己按字节读进来再reshape。很多做算法仿真的同学在这一步容易懵从sensor导出的raw文件明明能看但OpenCV里读出来是一堆乱码数据。原因就是你没告诉库函数这堆字节是什么格式。还有个实际场景里经常遇到的小坑OpenCV的imread读灰度图时返回的对象是单通道8bit矩阵而Matlab会返回二维矩阵。做二值化、膨胀腐蚀这类形态学操作时因为矩阵存储方式不同写出的代码风格会天差地别。我自己做智能车图像处理时赛道边缘检测的算法在Matlab仿真通过移植到C OpenCV环境里却一连错了三天最后发现就是矩阵的行列存储顺序和访问方式不同导致的。这类问题只能说踩过一次就会长记性。5. 带宽与容量计算方案设计时先把这笔钱算清楚5.1 ISP链路各节点的带宽需求估算做图像处理方案设计不把带宽算清楚后面就是无底洞。从sensor到ISP再到编码器、再到存储和显示每个节点都要实测带宽是否够用。计算带宽的公式很简单带宽 分辨率像素数 x 每像素字节数 x 帧率。举几个实际例子节点分辨率格式每像素字节帧率带宽需求Sensor输出1920x1080RAW101.2530fps77.8MB/sISP处理后1920x1080RGB888330fps186.6MB/s编码输入1920x1080YUV4201.530fps93.3MB/s编码输出1920x1080H.2640.12530fps7.8MB/s这张表一眼就能看出问题所在RGB阶段的带宽需求最高。如果你的ISP内存带宽有限那么后处理阶段尽量用YUV就是最直接的优化手段。这也是几乎所有视频处理器内部都有RGB转YUV硬件模块的原因。如果整个处理链路都保持RGB而不是转YUV需要的DDR带宽会翻倍芯片成本跟着涨功耗也跟着上一大截。做芯片方案评估时这些数字是要写进PPT说服老板的。5.2 多分辨率多机位场景下的存储压力实际项目里很少只有一个图像源。多路摄像头、多分辨率同时处理的场景存储压力是成倍增长的。举个例子一个四路摄像头的方案每路都是1080P YUV420帧率30fps每一秒原始数据量就是4x93.3MB约373MB/s。一分钟22GB一块128GB的存储卡两个小时就写满。这种情况下必须引入编码器压缩或选择性存储策略。常用的策略有三种第一种是事件触发存储也就是只在检测到感兴趣事件时开始录制平时只做实时分析不落盘第二种是环形缓冲区预先分配一块固定大小的存储空间持续写入新数据并覆盖旧数据配合事件标记算法在事件发生时自动保存前后若干秒第三种是动态分辨率切换在场景变化不剧烈时降低帧率或分辨率只在关键事件时恢复全分辨率。这三种策略可以结合起来用在智能交通、安防监控这类系统里非常常见。5.3 嵌入式设备上的格式选择策略嵌入式设备做图像处理内存和带宽都有限格式选择不是一句用YUV就行能解决的。首先你要分清主处理单元是什么。如果是专用的ISP芯片它能接收RAW并输出RGB或YUV你需要操心的是后端算法模块吃哪种格式。如果是普通MCU加外部SRAM做简单图像处理那么RAW或灰度图往往是唯一现实选择。做嵌入式图像处理尽量在算法设计阶段就把bit深度对齐到硬件寄存器。比如MCU内部的摄像头接口DVP通常是8bit并行你的sensor输出如果只有8bit模式那就可以直接对接省去一个格式转换芯片。如果sensor是10bit输出硬件却不支持10bit模式很多人会选择砍掉低两位直接将10bit右移2位转成8bit这固然能工作但高光区域的细节可能已经丢了。这时候更稳妥的做法是利用sensor内部的short mode或者polarity配置直接配置成8bit输出模式避免软件层面的强制截断。这类细微的产品选型决策往往比后期写复杂的算法更有价值。我的原则是能用硬件原生支持的格式绝不用软件转。软件转格式至少打八折的带宽表现在嵌入式平台上尤其明显。6. 格式转换的最佳实践什么时候转、在哪一级转效率最高6.1 尽量避免格式转换链的本来可以不做环节很多新手在做图像处理时喜欢随手做格式转换。RGB转灰度、灰度转YUV、YUV再转RGB转来转去每一个转换节点都有信息损失和时间开销。我在评审代码时经常问一句话这级转换真的有必要吗灰度图能做的事为什么要先搞成RGB再做YUV能直接处理的为什么要先转RGB举一个典型场景在OpenCV里做膨胀腐蚀或者边缘检测这些算法本质上是单通道灰度处理。如果你手里拿着的是YUV420数据直接取Y分量做灰度处理就行完全没有必要先YUV转RGB再BGR转灰度白白多做了一次色彩插值、一次通道分离。不少智能车比赛里的图像处理代码跑得慢的原因不是算法复杂度高而是格式转换链太长了。每一帧多出来几十毫秒的开销对这种实时性要求极高的场景是无法接受的。6.2 用查表法和定点化提升转换效率如果转换确实绕不开就要讲究转换效率。最典型的RGB转灰度公式是Y 0.299R 0.587G 0.114B。浮点计算的版本在桌面上无所谓但在嵌入式MCU上每次都做浮点乘加效率极低。工程上通常用定点化来实现把系数放大1024倍变成整数乘加最后右移10位。Y (306R 601G 117B) 10这样一个8bit像素的转换只需要三次整数乘法和两次加法加一次移位在普通MCU上也能跑得飞快。这里有个细节系数相加的总和是3066011171024刚好是2的10次方这是公式能成立的前提。如果你自己调整系数一定要保证系数之和等于2的N次方。另外如果转换只涉及有限种输入值比如8bit灰度图的二值化阈值调整可以预先把所有输入值对应的输出结果算好放到查表里。典型的查表应用就是gamma校正256个输入值对应256个输出值查表的时间复杂度就是O(1)。这个方法放在格式转换上同理RGB888转RGB565可以预计算低字节和高字节的映射不过度占用计算资源。6.3 多格式调试工具的自动化技巧调试ISP和图像算法的时候手头应该有一套能快速看格式的脚本工具。我个人的工作流是这样的用一个小型Python脚本指定分辨率、位深、格式类型直接把二进制文件dump成可视化图像。这样无论是RAW10、YUV422、NV12还是RGB565都能在几秒内出图。如果要用OpenCV处理非标准格式记得先用脚本转成常见的PNG或者BMP再进算法流程。这里还有一个小技巧在批量处理实验数据时把图像格式信息写进文件名或csv元数据比如frame_00001_yuv422_1080p.raw。这样即使几个月之后翻出这批文件也不需要翻代码去回忆当时的处理参数。这类数据卫生习惯在长期的算法迭代中能省下大量时间。在像ISP这种对格式极其敏感的领域格式的信息不只是格式本身还包含整个处理链路的上下文。建立一套标准的调试数据管理流程比写几行好看的算法代码重要得多。我自己现在处理图像问题第一个动作永远是打印格式元信息然后才敢碰像素数据。想反了就是回坑里躺一遍。
返回列表