十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

YUV颜色空间全解析:从RGB转换到4:2:0采样与范围映射

YUV颜色空间全解析:从RGB转换到4:2:0采样与范围映射 1. 为什么视频世界里一直离不开YUV我们天天盯着屏幕摄像头采进来的是RGB显示器点亮的也是RGB但视频编码、图像传输、甚至你做图像处理课程作业时中间永远横着一个YUV。很多人第一次接触数字图像处理学到颜色空间这一章就开始犯迷糊既然RGB是硬件直接用的为什么非得绕一圈换成YUV先说结论因为人眼对亮度的敏感程度远高于对颜色的敏感程度。这是整个YUV体系存在的最底层逻辑也是视频压缩能省下大量码率的基础。对比一下RGB的三个分量R、G、B的权重几乎一样都参与描述亮度信息又都携带各自的色度信息。这种设计对显示设备很友好但对压缩和传输来说等于把高价值的亮度信息和低价值的色度信息一视同仁浪费带宽。YUV把这两类信息拆开Y单独承载亮度U和V承载色差于是视频编码器就可以对U、V做更狠的压缩而人眼几乎感知不到差异。从历史角度看YUV的出现也带着很强的务实色彩。上世纪五十年代彩色电视要兼容黑白电视信号黑白电视只能接收亮度信息彩色电视要能从同一个信号里还原出彩色。广播电视系统最终统一采用“亮度色差”的方案RGB信号经过矩阵变换后变成YUV黑白电视只取Y分量就能显示正常画面彩色电视再把YUV逆变换回RGB驱动显像管。今天数字视频的编码、存储、传输本质上还是沿用了这套思想只是把模拟时代的术语和系数数字化了。对于学习数字图像处理的人来说YUV不是一门“过时的模拟电视技术”而是理解视频编码器设计思路、图像压缩原理、甚至色彩管理工作流的关键枢纽。我见过太多同学习惯把所有图像处理都用RGB做等到接触H.264、H.265编码器源码或者做嵌入式摄像头采集时才发现YUV的知识缺了一大块。这篇文章把YUV从原理讲到实操从公式讲到存储布局争取一篇讲透。2. YUV从哪里来从RGB到亮色分离的数学推演2.1 人眼的视觉特性决定了YUV的设计方向先做一个简单的实验心理感受在一张彩色照片上把饱和度拉到最低变成灰度图你依然能看清所有物体的轮廓、纹理、边缘细节因为灰度图保留了完整的亮度信息。反过来把这张照片的亮度通道模糊到一塌糊涂只保留原始颜色信息你会立刻觉得画面“糊了”“脏了”细节全部丢失。这个实验直观地说明人眼对空间细节的感知几乎全部来自亮度对颜色的空间分辨率则低得多。所谓“细节”在图像处理里对应高频信息而高频信息主要集中在Y分量上。因此视频编码器会有充分理由把色度分量的分辨率砍半甚至砍掉更多而不影响主观画质。这个特性在工程上也有直接体现。RGB转YUV之后Y分量几乎占据了人眼感知画质权重的80%以上U、V分量的权重相对低。于是我们就可以把U、V的水平、垂直分辨率都减半即4:2:0采样画质损失肉眼几乎看不出来码率却能省下不少。2.2 标准变换公式BT.601与BT.709两套系数RGB到YUV的转换不是拍脑袋定一个矩阵就完事而是由国际标准组织根据显示设备的色度特性具体来说是荧光粉或背光的色坐标制定的一套线性变换系数。数字图像处理和视频领域最常遇到的是两套标清时代的BT.601和高清时代的BT.709。BT.601的转换公式RGB范围0~255Y范围16~235Y 0.299R 0.587G 0.114BU 0.564(B - Y) -0.1687R - 0.3313G 0.5BV 0.713(R - Y) 0.5R - 0.4187G - 0.0813BBT.709则是Y 0.2126R 0.7152G 0.0722BU -0.1146R - 0.3854G 0.5BV 0.5R - 0.4542G - 0.0458B很多初学者看到这两套公式就开始头大但只要抓住两点就能理解第一Y分量的系数和就是1这说明亮度是RGB三通道的加权平均权重对应人眼对红绿蓝三色的敏感度绿色权重最大蓝色最小。第二U、V本质上是“蓝色减去亮度”和“红色减去亮度”的差值所以又被称为“色差信号”。当你拿到一段视频素材时第一件事就是确认它是BT.601还是BT.709因为用错矩阵会出现严重的偏色。判断方法也不难标清SD内容720×576、720×480多为BT.601高清HD内容1280×720及以上通常为BT.709。2.3 为什么要加128个偏移量从模拟信号到数字编码的适配如果你直接套用上面的公式会得到一个关键问题U和V可能是负数。模拟电视时代信号可以有正有负但数字图像处理里像素值通常只能是非负整数比如8位精度下是0~255不能直接存储负数。所以工程上还有一个“上移”操作把U、V的结果加上128让中间值落在128附近范围从原来的-128~127映射到0~255。因此数字领域常见的形式是Y 0.299R 0.587G 0.114BU -0.1687R - 0.3313G 0.5B 128V 0.5R - 0.4187G - 0.0813B 128注意这个128的偏移只加在色差分量上Y不加。这意味着一个纯灰色的像素RGB换算之后U和V都等于128恰好落在整个取值范围的中间位置代表“无色”或“中性色”。这个128就是灰色在色度坐标系里的原点这点在做调试时特别有用你在屏幕上看到一块区域U、V值都接近128那这块区域就是纯灰没有偏色。做逆变换时记得把128减回去这是新手最容易丢的一步。丢了偏移量整个画面都会蒙上一层诡异的色调。2.4 手动实现一次完整转换以Python为例理论说完手上得有点能跑的东西。用Python实现一个RGB转YUV的完整流程并不复杂只涉及几个矩阵运算和数组操作。下面这段代码演示了BT.601标准下如何把一张RGB图像转到YUV再转回RGB并验证误差。import numpy as np from PIL import Image def rgb2yuv(rgb): # rgb: HxWx3, dtypeuint8, 范围0-255 r rgb[:, :, 0].astype(np.float32) g rgb[:, :, 1].astype(np.float32) b rgb[:, :, 2].astype(np.float32) y 0.299 * r 0.587 * g 0.114 * b u -0.1687 * r - 0.3313 * g 0.5 * b 128 v 0.5 * r - 0.4187 * g - 0.0813 * b 128 yuv np.stack([y, u, v], axis-1) return np.clip(yuv, 0, 255).astype(np.uint8) def yuv2rgb(yuv): y yuv[:, :, 0].astype(np.float32) u yuv[:, :, 1].astype(np.float32) v yuv[:, :, 2].astype(np.float32) r y 1.402 * (v - 128) g y - 0.344 * (u - 128) - 0.714 * (v - 128) b y 1.772 * (u - 128) rgb np.stack([r, g, b], axis-1) return np.clip(rgb, 0, 255).astype(np.uint8) img Image.open(test.jpg).convert(RGB) arr np.array(img) yuv_arr rgb2yuv(arr) rgb_back yuv2rgb(yuv_arr) # 检查最大误差 diff np.abs(arr.astype(int) - rgb_back.astype(int)) print(max diff:, diff.max())这段代码应该能跑出一个非常小的误差通常最大误差在1~2以内主要来自BT.601小数系数的近似计算和uint8量化。如果你想精确无损那就得用可逆的整数变换比如JPEG里用的那套后面会提到。注意这里为了演示用了最朴素的循环式函数实际处理大图时请用矩阵运算或OpenCV内置函数性能差距能到几十倍。3. YUV、YCbCr、YCoCg一脉相承还是三兄弟各自为政3.1 术语辨析模拟YUV和数字YCbCr别混着说课上老师讲的是YUVOpenCV里用的是YCrCbFFmpeg转出来的是YUV420PH.264标准里写的是YCbCr有些论文里还会出现YCoCg。这些名词之间什么关系很多人栽在这里。严格来说YUV最初指模拟电视系统里的亮度色差信号它的U和V没有经过任何数字化映射直接以模拟电压方式传输YCbCr则是数字视频标准里的术语Cb对应色差信号B-Y的数字化版本Cr对应R-Y的数字化版本也就是我们在数字文件里看到的那个带128偏移的U、V。今天绝大多数实际工程里讨论的“YUV”其实都是YCbCr只是一般人叫习惯了干脆不区分。如果你看到有人强调“YCbCr才是数字域YUV是模拟域”他说的在理但在日常交流中没必要较真知道两者的关系就行。FFmpeg、OpenCV、各种视频编码器里的“YUV”默认都是指YCbCr数字信号。你在代码里写cvtColor(img, COLOR_BGR2YUV)拿到的就是带128偏移的数字色差信号不是模拟电压。3.2 4:4:4、4:2:2、4:2:0色度下采样的三档尺度数字视频里几乎总是伴随后缀的数字比例这就是色度下采样的标识。前面提到人眼对色度细节不敏感所以编码时可以把U、V分量的采样率降下来。这套标识来自模拟分量视频时代4代表亮度Y的采样率后面的数代表色度的采样率。4:4:4每个像素点都保留完整的Y、U、V三个分量信息无损多用于专业制作、电脑桌面捕捉和高质量图像处理。4:2:2每两个水平像素采样一次U、V即水平分辨率减半垂直不减。常见于广播电视采集和高端视频设备。4:2:0每2×2的像素块共享一组U、V水平和垂直分辨率都减半。这是消费级视频的绝对主流几乎所有H.264/H.265网络视频都是这种格式。初学者容易误会一件事4:2:0是不是表示码率省了一半不是。因为Y分量保持了全分辨率真正砍掉的是U、V的采样点。算一下4:4:4是YUV共3个数据单元4:2:0是Y (1/4)U (1/4)V每个像素平均1.5个数据单元所以理论上比4:4:4省一半存储量比4:2:2省四分之一。这在实际编码里还只是采样层面的节约编码器内部的变换、量化和熵编码还能再压掉很大一部分。3.3 面向编码器的YCoCg可逆变换的进阶选择如果你研究过视频编码或图像压缩相关论文还会碰到一个叫YCoCg的颜色空间。这个不是给显示用的而是给编码器做预处理用的。它与YCbCr最大的区别在于变换系数全是整数且变换矩阵的行列式绝对值为1意味着它可以在整数域精确可逆做到“转出去再转回来一个像素都不差”。RGB转YCoCg公式Co R - Bt B (Co 1)Cg G - tY t (Cg 1)逆变换则是t Y - (Cg 1)G Cg tB t - (Co 1)R B CoYCoCg的优点完全可逆、实现简单、能量更集中尤其适合无失真压缩场景。JPEG XL等现代压缩标准中它就有用武之地。不过看到这里不必纠结太多你只需要知道在YCbCr之外还有一个更面向编码器的选项就好日常图像处理还是以YCbCr为主。实操心得查资料时如果看到YUV、YCbCr、YCoCg三种名称混搭出现先判断论文或代码的语境再决定是否套用同一套公式。拿YCbCr的系数去算YCoCg结果必错无疑。4. 存储布局为什么有的YUV看起来像彩虹有的像打了马赛克4.1 打包格式与平面格式的战争YUV数据在内存里的排布方式直接决定了你看文件时的观感以及代码里取像素的方式。总体可以分为三大流派打包格式PackedY、U、V交错排列比如YUYV序列为Y0 U0 Y1 V0 Y2 U2 Y3 V2。这种格式适合硬件直接输出和低延迟采集但不利于视频编码器做块级处理。平面格式Planar所有Y像素连续存放然后所有U连续存放最后所有V连续存放。I420是典型的平面格式。这种布局对编码器特别友好因为变换、量化都是按块处理的把分量分开存放就不用频繁做内存跳跃。半平面格式Semi-PlanarY单独一个平面U和V交错存放在另一个平面NV12是典型代表。这是现代硬件和编码器最喜欢的折中方案。写代码时如果直接读YUV文件第一件事就是确认它是哪种排布。读错排布方式轻则出现混沌的条纹画面重则越界崩溃。4.2 我们常说的YUV420到底有多少种变体YUV420听起来是一个东西实际上至少有I420、YV12、NV12、NV21等七八种常见变体。它们的数据总量一样但排布方式完全不同。I420的排布先是W×H的Y数据再是(W/2)×(H/2)的U数据最后是(W/2)×(H/2)的V数据。YV12只是把U、V的先后顺序调换了一下先V后U。NV12则是Y平面后紧跟一个交错的UV平面每个U后面跟着一个V。如果你用OpenCV读一个NV12文件却按I420的步长去解析U、V分量就会错位画面整体出现色偏而且这种色偏在不同区域还不一样特别容易让人误以为是白平衡问题其实是布局没搞对。写一个小工具来区分这些格式最直接的办法是解析出Y分量单独显示成灰度图再解析U、V分量各自显示。如果Y分量显示出来是正常的完整灰度图说明Y平面的偏移量对了如果U或V分量显示成类似棋盘格或马赛克的图案说明读取的步长和下采样假设错了。4.3 从文件到画面一幅YUV420图像的解析全过程假设你手上有一张1920×1080的NV12图像文件文件大小应该是多少计算方式如下Y平面1920 × 1080 2073600字节UV平面1920 × 540 1036800字节因为UV平面里每个像素点存了一个U和一个V总尺寸是1920×1080/2总大小3110400字节这个数值对应一个精确的公式宽×高×3/2。任何一个YUV420文件不管分辨率多少文件大小都应该严格等于宽×高×1.5字节。如果你的文件大小跟这个对不上第一反应应该是文件本身有问题或者你拿到的不是YUV420而是某种加了额外头信息的封装格式。读文件并用Python显示灰度图的方式import numpy as np import cv2 w, h 1920, 1080 with open(frame.nv12, rb) as f: data np.fromfile(f, dtypenp.uint8, countw*h*3//2) y_data data[:w*h].reshape(h, w) uv_data data[w*h:].reshape(h//2, w) # 单独显示Y分量 cv2.imwrite(y_plane.png, y_data)如果是YUV420PI420则u_data data[w*h:w*h w*h//4].reshape(h//2, w//2) v_data data[w*h w*h//4:].reshape(h//2, w//2)有经验之后你甚至不需要等软件解码光看一个YUV文件的后缀和大小就能大概猜出它的格式和组织方式。这份手感对做视频开发、嵌入式多媒体、图像算法的人都非常有用。5. 范围问题Limited Range与Full Range的差异和那些“发灰”的视频5.1 16~235还是0~255这是个问题我在实际项目里被YUV的range问题坑过好几次每次都不得不承认这个细节太小但弄错了影响巨大。视频编码标准里的YCbCr分为两种合法范围Full Range0~255和Limited Range16~235也叫Video Range。Limited Range的来历依旧是模拟电视时代为了避免信号超调导致画面异常模拟系统会在最黑的电平之上保留一点余量16在最白的电平之下也保留一点余量235。这个习惯延续到了数字标准里。BT.601和BT.709都默认使用Limited Range而计算机显示器和图像处理软件则习惯于Full Range。Display如果你有一段YUV视频标准规定的Y范围是16~235但你的播放器或转换代码没有做范围映射直接把Y16当成纯黑、Y235当成纯白来显示结果就是画面的黑位发灰白位也压不到头整幅画面看起来像蒙了一层雾俗称“发灰”。反过来如果视频本来就是0~255的Full Range却按Limited Range去拉伸画面会显得对比度过强暗部细节部分丢失。5.2 如何在转换时正确映射范围从YCbCr转到RGB时范围映射其实可以跟着矩阵一起做。BT.601的Limited Range YCbCr转RGB的公式为R 1.164(Y - 16) 1.596(V - 128)G 1.164(Y - 16) - 0.392(U - 128) - 0.813(V - 128)B 1.164(Y - 16) 2.017(U - 128)注意这个公式里1.164就是把16~235的Y范围拉伸到0~255的映射系数。Full Range版本则是R Y 1.402(V - 128)G Y - 0.344(U - 128) - 0.714(V - 128)B Y 1.772(U - 128)两套公式只差在Y的缩放系数和是否需要减16但输出效果差之毫厘、谬之千里。实际调试时如果视频转换结果“整体灰蒙蒙”我有八成的把握是Limited和Full没对齐。5.3 快速判断当前视频range的小技巧怎么快速判断一段视频是Limited还是Full我常用的办法是直接打开它的直方图看像素分布是否触及上下边界。如果画面暗部大量像素集中在16以上亮部集中在235以下几乎不会到达0和255那就是Limited Range。如果像素从0到255都有分布特别是暗场画面中依然存在接近0的像素值那就是Full Range。还有一种办法用播放器渲染时手动切换range设置观察哪个状态下画面反差和饱和度更自然。VLC、MPV等播放器都有类似设置切换对比很直观。工程上FFmpeg转码时则要显式指定ffmpeg -i input.mp4 -vf scaleout_rangefull -pix_fmt yuvj420p output_full.mp4yuvj420p这个像素格式名中的j就代表Full RangeJPEG而yuv420p代表Limited Range。很多FFmpeg的坑都是因为这个小小的j。经验在采集设备或相机SDK里做颜色空间转换时设备驱动通常会告诉你当前输出是哪种range。如果文档没写就直接采集一张白色卡片和一张黑色卡片看输出的Y值。白卡如果接近235而不是255十有八九是Limited。6. 数字图像处理课设和竞赛中的YUV实战套路6.1 课程作业里最常见的三个任务方向在数字图像处理课程里YUV相关的作业和实验通常集中在三个方向颜色空间转换、色度重采样、以及基于Y分量的图像增强和边缘检测。搞明白方向之后针对性地准备分数会好看很多。颜色空间转换作业一般是给你一张RGB图或者反过来给你一个.yuv文件要求写代码完成RGB与YUV之间的互转然后对比转换前后图像质量的变化。重点考察两点转换公式是否正确范围处理是否到位。很多同学的实现结果“看起来差不多”但用像素级对比就露馅了因为系数用错了。色度重采样作业则是给你一张4:4:4的YUV图像要求下采样到4:2:0再上采样回来观察质量损失。重点考察插值算法的选择比如最近邻、双线性、双三次之间的主观差异。这时候你可以多试几种插值策略并计算出PSNR得出的曲线非常能说明问题。基于Y分量的增强就更有意思了。因为Y分量几乎是亮度信息对它做直方图均衡化、伽马校正或对比度拉伸就能改善画面观感而不会像在RGB里那样导致颜色扭曲。我见过不少同学的灰度图像增强作业直接对RGB三通道分别做均衡化结果色彩饱和度和色相都变了这就是对颜色空间的理解不够。6.2 用Y分量做边缘检测比灰度化更合理说到边缘检测很多教材上来就是cvtColor转灰度再Canny。但如果你把灰度化这一步改成取Y分量效果往往会更好一些尤其是对彩色边缘来说。原因在于BT.601/709的Y分量是依据人眼感知加权得到的拿到的是最能代表结构信息的亮度估计。直接cvtColor时OpenCV默认使用的也是类似BT.601的系数但如果你在处理BT.709视频素材手动提取Y分量的系数应该换成BT.709的权重才能保留更多高分辨率内容应有的边缘细节。实际测试中我用一张带有大量彩色纹理的测试图对比RGB通道直接灰度化的结果和Y分量提取后做CannyY分量版的边缘更干净对红蓝边界等颜色差异大的区域不会产生额外毛刺。6.3 YUV调试工具箱文件、FFmpeg和Python三板斧做YUV实验离不开工具我平时最常用的组合是这个FFmpeg做格式转换和码流分析把一个RGB图片转成YUV420P文件、查看视频的pix_fmt和color_range设置一条命令行都能完成。Python做像素级分析读写YUV文件、转换矩阵验证、PSNR计算、直方图观察完全可控。图片浏览器加直方图工具快速判断色调和range比如ImageJ或Python的matplotlib绘制的直方图。FFmpeg转图片到YUV的一个典型用法ffmpeg -i input.png -pix_fmt yuv420p output.yuv这条命令会生成裸的YUV数据文件没有封装头正好适合喂给自研代码验证算法。如果想从YUV文件还原成图记得声明分辨率ffmpeg -f rawvideo -pix_fmt yuv420p -s 1920x1080 -i input.yuv -frames:v 1 output.png注意不写-s的话FFmpeg根本不知道每帧长什么样这是新手最容易出错的步骤。6.4 调试时的经典偏色与条纹问题速查我在教学和带项目时学生问得最多的就是三种画面异常这里列成一个速查表遇到问题直接对照排查现象可能原因检查点画面灰蒙蒙、对比度低视频是Limited Range显示按Full Range渲染检查像素分布是否在16~235之间画面偏绿或偏紫YUV转RGB的矩阵系数用错601/709混用确认素材是标清还是高清画面出现彩色横条纹YUV文件按错误的像素格式解析确认I420、NV12、YUYV等排布黑白画面但边缘有彩色噪点色度重采样时插值方法不当检查U、V分量是否做低通滤波图像整体错位、花屏分辨率声明错误或YUV文件被截断用文件大小反推分辨率是否匹配做调试的时候我的习惯是先分离Y分量单独显示成灰度图。如果Y分量看起来正常问题大概率出在色度处理上如果Y分量本身就花屏那要么分辨率不对要么文件本身有损坏。这种自上而下的排查顺序能节省大量时间。7. 一路实操下来我对YUV的几个核心体会做图像处理这些年YUV大概是我接触过的颜色空间里最容易“一眼懂、处处坑”的一个。公式不难难在标准多、术语乱、实现细节藏得深。最后聊几个我实际踩过坑之后才真正理解的点希望对你有用。第一个体会是任何时候都别省略range判断。我自己在校验一个视频处理管线时因为素材全是Full Range就直接写死了映射结果换了一批Limited Range的素材整个输出都灰了一层。后来我在做颜色转换工具链时统一要求上下游把range作为元数据透传宁可多写几行代码也不让取值范围靠猜。FFmpeg转码时务必留意color_range标签Python脚本处理YUV文件时务必先打印Y分量的最小最大值花不了多少功夫却能避开最恼人的画质问题。第二个体会是YUV格式的形态比系数更容易踩坑。公式这人人都能查但I420和NV12的排布、YV12还是NV21的顺序、UV平面的交错还是分离这些在实际代码里一旦写错画面肉眼可见地花。我的习惯是拿到一份YUV文件先别急着处理业务逻辑先用一小段脚本把Y平面、U平面、V平面分别导出来看一眼确认排布无误再做下一步。这个“先看图再动手”的流程帮我省下了大量瞎猜的时间。第三个体会是学YUV一定要动手转一次文件。只看公式很难真正理解那些偏移量和范围映射但当你自己写完代码把一张照片转成YUV420P再转回RGB看到重建后的图像跟原图几乎看不出区别时整个颜色空间体系算是真正内化了。如果你在准备数字图像处理考试或面试试着亲手推一遍BT.601和BT.709的矩阵系数再解释一下为什么Y分量是RGB的加权平均而UV要加128这套逻辑能讲通YUV这一块基本就过关了。
返回列表