OpenCV中的RGB与YUV转换

news2024/7/7 3:40:09

1 基本概念

        YUV 颜色空间从模拟电视时代开始就被广泛应用于彩色图像的转换与处理。其基于一个 3x3 的矩阵,通过线性变换将 RGB 像素转换为一个亮度(Luma)分量 Y 以及两个色度(Chroma)分量 U 和 V。由于模拟电视存在着多种制式,如 NTSC 与 PAL 等等,考虑到具体硬件与技术上的差异,它们通常会采用不同的转换矩阵系数。即便到了如今的数字电视时代,业界依旧会保留这些差异以保证兼容性,但同时又会根据需求发展出更多新的转换系数。这就导致了 YUV 颜色空间其实是一个非常混乱的概念,甚至于 YUV 本身也只是一个约定俗成的统称,其实际可能为 YCbCr, Y’CbCr, Y’UV, YPbPr, YCC 等等标准叫法中的一种。因此,如果不清楚具体的转换系数,在 RGB 转 YUV 再转回 RGB 的过程中就可能存在转换矩阵不互逆而产生颜色偏差的问题。但是,要完整地阐明 YUV 的各种转换系数是比较困难的,具体可网上查阅 Wiki 等相关资料。本文主要从实用性出发,对 OpenCV 中的 YUV 颜色空间转换进行一些梳理,毕竟 OpenCV 在图像预处理中是非常常用的。

        首先要说明的是,尽管 RGB 与 YUV 的转换可通过一个 3x3 的矩阵变换来实现,但实际上这个 3x3 矩阵中的 9 个数字并不是完全独立选取的。YUV 另一个比较通用的叫法是 YCbCr,即 U 对应 Cb,V 对应 Cr。YCbCr 中的亮度 Y 由 R / G / B 加权求和所得,对应的是单通道的灰度图;而 Cb 反映了蓝色 B 分量与亮度 Y 的差异,Cr 反映了红色 R 分量与亮度 Y 的差异,Cb 与 Cr 统称为色差或色度。具体的转换公式如下:

Y  = R2Y * R + G2Y * G + B2Y * B
Cb = (B - Y) * YCB + delta
Cr = (R - Y) * YCR + delta

其中 [ R2Y, G2Y, B2Y, YCB, YCR ] 决定了不同的转换矩阵,它们都是非负的。由于 Cb 与 Cr 反映的是色差,所以它们的数值分布应该关于原点 0 对称,因此需要加上一个 delta 偏移使其尽量落在与 Y 一致的区间,常用的是 delta = 128。通常来说,RGB 与 YUV 的转换不是正交变换,因此对于 256 x 256 x 256 正立方体分布的 RGB 值,转换所得的 YUV 分布并不同样满足正立方体的分布,而通常是一个截断的锥体。如果在 YUV 转换回 RGB 时使用了锥体外的 YUV 值,所得 RGB 值在现实中则是不存在的,因而也无法显示。

        对于 RGB 转 YUV 或者 YUV 转 RGB,OpenCV 可通过调用 dst = cvtColor(src, code) 来实现,code 决定了所使用的转换系数以及 srcdst 的存储格式。注意,OpenCV 通常使用 BGR 而非 RGB 颜色顺序,但其依旧保留了对 RGB 顺序输入的支持,它们除了内存访问顺序不一样外并没有什么区别,因此在后续讨论 code 时,文章不会对 BGR 与 RGB 进行区分。通过查阅 OpenCV 源码中的 imgproc.hpp,可得 code 主要有:

// YCbCr 4:4:4  <-->  BGR
COLOR_BGR2YCrCb    = 36,
COLOR_YCrCb2BGR    = 38,
// YUV 4:4:4  <-->  BGR
COLOR_BGR2YUV      = 82, 
COLOR_YUV2BGR      = 84,
// YUV_FOCC 4:2:0  -->  BGR
COLOR_YUV2BGR_NV12  = 91,
COLOR_YUV2BGR_NV21  = 93,
COLOR_YUV420sp2BGR  = COLOR_YUV2BGR_NV21,
COLOR_YUV2BGR_YV12  = 99,
COLOR_YUV2BGR_IYUV  = 101,
COLOR_YUV2BGR_I420  = COLOR_YUV2BGR_IYUV,
COLOR_YUV420p2BGR   = COLOR_YUV2BGR_YV12,
// YUV_FOCC 4:2:2  -->  BGR
COLOR_YUV2BGR_UYVY = 108,
COLOR_YUV2BGR_Y422 = COLOR_YUV2BGR_UYVY,
COLOR_YUV2BGR_UYNV = COLOR_YUV2BGR_UYVY,
COLOR_YUV2BGR_YUY2 = 116,
COLOR_YUV2BGR_YVYU = 118,
COLOR_YUV2BGR_YUYV = COLOR_YUV2BGR_YUY2,
COLOR_YUV2BGR_YUNV = COLOR_YUV2BGR_YUY2,
// BGR  <-->  YUV_FOCC 4:2:0
COLOR_BGR2YUV_I420  = 128,
COLOR_BGR2YUV_IYUV  = COLOR_BGR2YUV_I420,
COLOR_BGR2YUV_YV12  = 132,

可以看到,关于 RGB 与 YUV 转换的命名非常繁杂混乱,但以上的 code 实际所涉及的转换系数可分为三类。其中,第一类为 COLOR_BGR2YUV 及其反变换 COLOR_YUV2BGR;第二类为 COLOR_BGR2YCrCb 及其反变换 COLOR_YCrCb2BGR;第三类为带有 FOCC (Four-Character Code) 后缀的 COLOR_BGR2YUV_FOCC 及其反变换 COLOR_YUV2BGR_FOCC。而通过进一步探究,可知前两类用于非下采样 YUV 的转换,而第三类则用于经过下采样的 YUV 的转换。在这里我们先简单地介绍一下 YUV 的下采样格式,这对于后续的理解十分有帮助。

        YUV 由一个亮度分量 Y 与两个色度分量 U 和 V 组成,而根据色彩科学的研究,人眼对亮度变化更加敏感,而对色度变化的感知则比较弱。因此,为了降低图像数据传输所需的带宽,通常会对色度分量 U 和 V 进行下采样,同时完整地保留 Y,这样可以尽可能地以较低成本维持相同的视觉主观体验。一般来说,U 和 V 的下采样位置是一样的。色度分量的下采样通常以每相邻的 4 个像素作为一个下采样单位,在其中保留 1 或 2 或 4 个像素,然而这相邻 4 个像素以及保留像素的选取方式是多样的,这就产生了非常繁杂的下采样格式,它们通常用 YUV 4 : m : n 来表示。下图是三种比较常用的下采样格式。

YUV 4 : m : n 通常是一种约定俗成的概念,m 与 n 并不一定具有实际意义。YUV 4:4:4 等同于保留所有的色度像素,这种格式通常只在 YUV 与 RGB 转换时使用。YUV 4:2:2 表示色度分量在水平方向上进行 1/2 的下采样,而垂直方向则不进行下采样,即 4 个像素中只保留 2 个,其中水平方向 1/2 下采样通常保留左侧像素而丢弃右侧像素,但这并不是强制的,在具体硬件实现中通常有多种模式选择,包括只保留右侧像素,或者两个像素求平均等等。YUV 4:2:2 通常只用在比较高端的摄像机图像处理芯片中。YUV 4:2:0 表示水平与垂直方向都进行 1/2 的下采样,即 4 个像素只保留 1 个,通常为左上角像素,但正如 YUV 4:2:2,其具体的实现也是可选的。YUV 4:2:0 是绝大多数图像处理芯片所使用的格式。YUV 4:1:1 类似于 YUV 4:2:0 只保留 4 个像素中的 1 个,但其只在水平方向上进行 1/4 的下采样,而垂直方向则完整保留,这种格式的应用相对较少。除了以上的格式以外,我们通常用 YUV 4:0:0 来表示灰度图,即只保留亮度 Y 而完全丢弃色度 U 和 V。当需要从下采样格式恢复到完整的 4:4:4 格式时,通常只需要把下采样时所保留的像素复制到所丢弃的像素位置即可,当然这不可避免会导致失真,但由于人眼对色度变化不敏感,这种失真是可接受的。

2 YUV 4:4:4 转换

        对于非下采样 YUV,或者称为 YUV 4:4:4 格式,OpenCV 提供了两类转换系数。

        对于第一类 COLOR_BGR2YUV 及其反变换 COLOR_YUV2BGR,虽然它的名字非常符合我们的认知直觉,但根据 Wikipedia 其应该属于比较久远的 BT.470 标准,目前已不多用。在 OpenCV 源码的 color_yuv.simd.hpp 中,可以看到

//to YUV
static const float B2YF = 0.114f;
static const float G2YF = 0.587f;
static const float R2YF = 0.299f;
static const float B2UF = 0.492f;
static const float R2VF = 0.877f;

代入前面的转换公式,可得具体的转换系数与公式如下:

[ Y U V ] = [ 0.299 0.587 0.114 − 0.147 − 0.289 0.436 0.615 − 0.515 − 0.100 ] [ R G B ] + [ 0 128 128 ] \left[ {\begin{array}{c} Y \\ U \\ V \end{array}} \right] = \left[ {\begin{array}{c} {0.299}&{0.587}&{0.114} \\ { - 0.147}&{ - 0.289}&{0.436} \\ {0.615}&{ - 0.515}&{ - 0.100} \end{array}} \right]\left[ {\begin{array}{c} R \\ G \\ B \end{array}} \right] + \left[ {\begin{array}{c} 0 \\ {128} \\ {128} \end{array}} \right] YUV = 0.2990.1470.6150.5870.2890.5150.1140.4360.100 RGB + 0128128

[ R G B ] = [ 1 0 1.140 1 − 0.395 − 0.581 1 2.032 0 ] [ Y U − 128 V − 128 ] \left[ {\begin{array}{c} R \\ G \\ B \end{array}} \right] = \left[ {\begin{array}{c} 1&0&{1.140} \\ 1&{ - 0.395}&{ - 0.581} \\ 1&{2.032}&0 \end{array}} \right]\left[ {\begin{array}{c} Y \\ {U - 128} \\ {V - 128} \end{array}} \right] RGB = 11100.3952.0321.1400.5810 YU128V128

要注意到,当 R = G = 0 , B = 255 R=G=0, B=255 R=G=0,B=255 时, U U U 取得最大值 U m a x = 239 U_{max}=239 Umax=239。依此计算,可得 Y ∈ [ 0 , 255 ] Y \in [0, 255] Y[0,255] U ∈ [ 17 , 239 ] U \in [17, 239] U[17,239] V ∈ [ − 29 , 285 ] V \in [-29, 285] V[29,285]。因此,Y / U / V 三者的量程在 BT.470 标准下并不一样,这大概是为了弥补模拟电视对于不同分量的响应差异。在数字信号处理中,因为图像通常使用 uint8 来表示像素值,所以 V 在存储前需要截断,从而丢失部分色彩信息。

        对于第二类 COLOR_BGR2YCrCb 及其反变换 COLOR_YCrCb2BGR,要注意的是 OpenCV 使用的是 YCrCb 而非 YUV 所对应的 YCbCr 顺序,在使用时应该小心,但具体这么做的原因未知。实际上 COLOR_BGR2YCrCb 所使用的转换系数才是我们比较熟知且常用的 BT.601 标准。在 OpenCV 源码的 color_yuv.simd.hpp 中,可以看到

//to YCbCr
static const float B2YF = 0.114f;
static const float G2YF = 0.587f;
static const float R2YF = 0.299f;
static const float YCBF = 0.564f; // == 1/2/(1-B2YF)
static const float YCRF = 0.713f; // == 1/2/(1-R2YF)

代入转换公式,可得具体的转换系数与公式如下:

[ Y C b C r ] = [ 0.299 0.587 0.114 − 0.169 − 0.331 0.500 0.500 − 0.419 − 0.081 ] [ R G B ] + [ 0 128 128 ] \left[ {\begin{array}{c} Y \\ {Cb} \\ {Cr} \end{array}} \right] = \left[ {\begin{array}{c} {0.299}&{0.587}&{0.114} \\ { - 0.169}&{ - 0.331}&{0.500} \\ {0.500}&{ - 0.419}&{ - 0.081} \end{array}} \right]\left[ {\begin{array}{c} R \\ G \\ B \end{array}} \right] + \left[ {\begin{array}{c} 0 \\ {128} \\ {128} \end{array}} \right] YCbCr = 0.2990.1690.5000.5870.3310.4190.1140.5000.081 RGB + 0128128

[ R G B ] = [ 1 0 1.402 1 − 0.344 − 0.714 1 1.772 0 ] [ Y C b − 128 C r − 128 ] \left[ {\begin{array}{c} R \\ G \\ B \end{array}} \right] = \left[ {\begin{array}{c} 1&0&{1.402} \\ 1&{ - 0.344}&{ - 0.714} \\ 1&{1.772}&0 \end{array}} \right]\left[ {\begin{array}{c} Y \\ {Cb - 128} \\ {Cr - 128} \end{array}} \right] RGB = 11100.3441.7721.4020.7140 YCb128Cr128

通过对比 COLOR_BGR2YUV 的转换公式,可知它们的亮度分量是相等的,但不同的地方在于 YCbCr 各个分量的量程都为 [ 0 , 255 ] [0, 255] [0,255],从而无需截断即可存储在 uint8 类型内,而这正是为了适应数字信号处理而对色度分量范围的调整。不考虑存储精度的损失,使用 COLOR_BGR2YCrCb 及其反变换 COLOR_YCrCb2BGR 进行颜色空间转换不会造成颜色失真。注意,在旧版本的 OpenCV 中,可能还存在 COLOR_BGR2YCR_CB,它和 COLOR_BGR2YCrCb 其实是一个东西,一般不使用。

3 YUV 下采样格式转换

        除了支持 YUV 4:4:4 格式的颜色空间转换以外,OpenCV 还支持 RGB 直接转换为 YUV 下采样格式,或者 YUV 下采样格式直接转换为 RGB,也就是前面所说的第三类 code。第三类 code 可表示为带有某个 FOCC 后缀的 COLOR_BGR2YUV_FOCC 以及反变换的 COLOR_YUV2BGR_FOCC,其中 FOCC 指由 4 个 ASCII 字符所组成的代码,即 Four-Character Code,有点类似于 C/C++ 中的 enumFOCC 在个人程序中通常可以任意定义,但是也存在一些约定俗成或者标准化的命名,例如编解码标准中的 JPEG, MPG4, H264 等等。在第一节中,我们提到过 YUV 中的色度分量通常要进行下采样以节省系统带宽。进一步地,经过下采样后的 YUV 数据中亮度和色度的像素个数是不一样的,我们不能像 RGB 那样将其表示为一个 [H, W, 3] 的矩阵,所以下采样的 YUV 数据存在多种不同的存储方式,这些就构成了各种繁乱的关于 YUV 格式的 FOCC

        为了正确使用 OpenCV 进行下采样格式的 YUV 数据颜色空间转换,这里简单地对 YUV 的存储方式进行介绍。常用的存储方式分为三种,即 Planar, Semi-Planar 以及 Interleave

  • Planar 即平面式存储。其在内存空间中先完整地顺序存储所有 Y 像素,然后再完整地顺序存储所有经过下采样的 U 数据,最后完整地顺序存储所有经过下采样的 V 数据,例如 Y1 Y2 Y3 … Yend U1 U2 U3 … Uend V1 V2 V3 … Vend。它的优点是可以适应任意的下采样格式,并且在只对单个分量如 Y 分量进行操作时无需对其他分量进行存取,缺点是在内存中 Y / U / V 数据间的跨度太大,当需要同时处理 Y / U / V 数据时不利于数据的连续突发传输,或者需要多个数据并行存取分支。注意,有时候也会先存储 V 再存储 U,这时要通过具体的 FOCC 进行区分。

  • Semi-Planar 即半平面式存储。其在内存空间中也是先完整地顺序存储所有 Y 像素,但由于 U 和 V 的数据量是相等的,且绝大多数算法都需要同时用到 U 和 V,所以 U 和 V 之间并不需要分开存储,而是可以进行交叉,例如 Y1 Y2 Y3 … Yend U1 V1 U2 V2 U3 V3 … Uend Vend。它也具有 Planar 格式的适应性以及亮度与色度分量独立存储的优点,但是能够将 U 和 V 的数据访问操作进行合并,改善了内存突发传输性能,所以 Semi-Planar 格式在硬件中反而是更加常用的。和 Planar 同理,有时候也会先存储 V 再存储 U,这时要通过具体的 FOCC 进行区分。

  • Interleave 即交叉式存储。不同于 Semi-Planar 只将 U 和 V 进行交叉,Interleave 将 Y / U / V 数据都交织在一起,可以最大化 YUV 数据的突发传输性能,缺点是在只需要亮度分量时必须把色度分量也进行传输。它通常用于亮度与色度数据量相等的情况,如 YUV 4:2:2 格式,这时两个 Y 对应着一个 U 和一个 V,根据四者的顺序的不同存在着多种 Interleave 格式,并对应着不同的 FOCC。YUV 4:2:2 常用的交叉式存储顺序有 YUYV, YVYU, UYVY, VYUY 等等,其中的两个 Y 需按像素顺序排列。例如对于 YUYV,在内存中有 Y1 U1 Y2 V1 Y3 U2 Y4 V2 …。实际上,大多数硬件都支持内存隔点并行存取的操作,因此对于 YUV 4:2:2 而言,将亮度与色度分量进行交叉存储并不会造成太大的数据传输负担,所以 YUV 4:2:2 数据的处理通常采用此存储格式。

        在第一节中我们已经列出了 OpenCV 所支持的 FOCC,但要注意 COLOR_BGR2YUV_FOCCCOLOR_YUV2BGR_FOCC 所支持的 FOCC 并不是对称的,例如 COLOR_BGR2YUV_FOCC 并不支持 RGB 到 YUV 4:2:2 的转换,如果有这方面的需求则需要利用第二节的 COLOR_BGR2YCrCb 将 RGB 转换到 YUV 4:4:4 并手动进行下采样。不过要注意的是,COLOR_BGR2YCrCbCOLOR_BGR2YUV_FOCC 的转换系数是有差异的,这个留到后面讨论。另外有些 FOCC 的意义是重复的。这里先对各个 FOCC 所对应的下采样及存储格式进行明确:

  • NV12 :YUV 4:2:0 Semi-Planar格式,其中色度分量先存 U 再存 V。
  • NV21 / 420S :YUV 4:2:0 Semi-Planar格式,其中色度分量先存 V 再存 U。
  • IYUV / I420 :YUV 4:2:0 Planar 格式,其中色度分量先存 U 再存 V。
  • YV12 / 420P :YUV 4:2:0 Planar 格式,其中色度分量先存 V 再存 U。
  • UYVY / Y222 / UYNV :YUV 4:2:2 Interleave 格式,存储顺序为 UYVY。
  • YUY2 / YUYV / YUNV :YUV 4:2:2 Interleave 格式,存储顺序为 YUYV。
  • YVYU :YUV 4:2:2 Interleave 格式,存储顺序为 YVYU。

        在 OpenCV 中,虽然 YUV 经过了下采样,但还是会把 YUV 数据打包为一个矩阵方便处理。对于 YUV 4:2:0,色度分量 U 和 V 的尺寸分别为亮度 Y 的 1/4,为了将它们打包在一起,OpenCV 使用一个 [H*3/2, W] 的二维矩阵进行存储,其中 [:H, :W] 存储的是 Y 数据,而 [H : H+H/2, :W] 则根据 Planar 或 Semi-Planar 格式顺序对 U 和 V 数据进行存储。根据以上的 FOCC 对应的格式,它们在二维矩阵中的分布如下图所示:

而对于 YUV 4:2:2,色度分量 U 和 V 的尺寸分别为亮度 Y 的 1/2,即色度分量的数据量和亮度分量是一样的,所以 OpenCV 使用一个 [H, W, 2] 的三维矩阵进行存储。因为矩阵在内存中的索引顺序通常从最后的维度开始,所以根据 FOCC 顺序对 YUV 4:2:2 数据进行存储时,应该在两个 [H, W] 平面上交替写入。以 UYVY 为例,它们在三维矩阵中的分布如下图所示,其他 FOCC 可自行推理,这里不赘述:

        在了解了 YUV 下采样格式的存储方式及对应的 FOCC 以后,我们就能基于 OpenCV 中的 COLOR_BGR2YUV_FOCCCOLOR_YUV2BGR_FOCC 直接将下采样后的 YUV 数据转换为 RGB,或者直接将 RGB 转换为特定下采样及存储方式的 YUV 数据了。实际上,第三类 COLOR_BGR2YUV_FOCC 可看作是第二类 COLOR_BGR2YCrCb 即 BT.601 的继承,但是又有些不同。在信号处理中,一个很重要的概念是系统响应,包括滤波等操作通常会在时域或空域中引入震荡或过冲,使得像素值在原始值附近上下波动。因为 COLOR_BGR2YCrCb 所得 YUV 数据的取值范围都为 [0, 255],经过系统响应以后,YUV 实际的取值就有可能超出这个范围。在硬件中如果没有专门的截断运算,当数据溢出时通常只会保留当前字节的比特,例如对于 256,uint8 类型保留低字节后所得的结果是 0 而不是 255,因而造成很大的失真。如果在硬件执行截断操作,则会增加一定的复杂度。为了解决这个问题,ITU-R 组织在 BT.601 转换系数的基础上,对 COLOR_BGR2YCrCb 所得结果进行缩放与偏移,从而得到了 OpenCV 第三类 code 所对应的转换系数,其通常配合特定的 YUV 下采样及存储格式使用。通过查阅 OpenCV 源码的 color_yuv.simd.hpp,可以看到

// Coefficients for RGB to YUV420p conversion
static const int ITUR_BT_601_SHIFT = 20;
static const int ITUR_BT_601_CRY =  269484;
static const int ITUR_BT_601_CGY =  528482;
static const int ITUR_BT_601_CBY =  102760;
static const int ITUR_BT_601_CRU = -155188;
static const int ITUR_BT_601_CGU = -305135;
static const int ITUR_BT_601_CBU =  460324;
static const int ITUR_BT_601_CGV = -385875;
static const int ITUR_BT_601_CBV = -74448;
//R = 1.164(Y - 16) + 1.596(V - 128)
//G = 1.164(Y - 16) - 0.813(V - 128) - 0.391(U - 128)
//B = 1.164(Y - 16)                  + 2.018(U - 128)

其中未标明的 ITUR_BT_601_CRV 和 ITUR_BT_601_CBU 是一样的。转换为浮点后,可得带有 FOCC 后缀的 RGB 与 YUV 转换系数与如下

[ Y ′ U ′ V ′ ] = [ 0.257 0.504 0.098 − 0.148 − 0.291 0.439 0.439 − 0.368 − 0.071 ] [ R G B ] + [ 16 128 128 ] \left[ {\begin{array}{c} {Y'} \\ {U'} \\ {V'} \end{array}} \right] = \left[ {\begin{array}{c} {0.257}&{0.504}&{0.098} \\ { - 0.148}&{ - 0.291}&{0.439} \\ {0.439}&{ - 0.368}&{ - 0.071} \end{array}} \right]\left[ {\begin{array}{c} R \\ G \\ B \end{array}} \right] + \left[ {\begin{array}{c} {16} \\ {128} \\ {128} \end{array}} \right] YUV = 0.2570.1480.4390.5040.2910.3680.0980.4390.071 RGB + 16128128

[ R G B ] = [ 1.164 0 1.596 1.164 − 0.391 − 0.813 1.164 2.018 0 ] [ Y ′ − 16 U ′ − 128 V ′ − 128 ] \left[ {\begin{array}{c} R \\ G \\ B \end{array}} \right] = \left[ {\begin{array}{c} {1.164}&0&{1.596} \\ {1.164}&{ - 0.391}&{ - 0.813} \\ {1.164}&{2.018}&0 \end{array}} \right]\left[ {\begin{array}{c} {Y' - 16} \\ {U' - 128} \\ {V' - 128} \end{array}} \right] RGB = 1.1641.1641.16400.3912.0181.5960.8130 Y16U128V128

容易算得,此时 Y ∈ [ 16 , 235 ] Y \in [16, 235] Y[16,235],而 U , V ∈ [ 16 , 240 ] U,V \in [16, 240] U,V[16,240]。即 YUV 数据的左右两侧都预留了一定的空间,使得一般的操作都不会导致数据的溢出,从而可以在硬件中减少一些截断操作。当然,其代价是数据的表示范围有所缩减,不能最大化 uint8 类型所能表示的精度,但总体上还是利大于弊的,所以这种非满量程的 YUV 也被 ITU-R 组织作为一种标准进行推广。前面我们提到,COLOR_BGR2YUV_FOCC 是对 COLOR_BGR2YCrCb 进行缩放与偏移得到的结果,事实也是如此,只需对比两者的转换矩阵即可,如下:

[ 0.257 0.504 0.098 − 0.148 − 0.291 0.439 0.439 − 0.368 − 0.071 ] ≈ 1 255 [ 219 224 224 ] [ 0.299 0.587 0.114 − 0.169 − 0.331 0.500 0.500 − 0.419 − 0.081 ] \left[ {\begin{array}{c} {0.257}&{0.504}&{0.098} \\ { - 0.148}&{ - 0.291}&{0.439} \\ {0.439}&{ - 0.368}&{ - 0.071} \end{array}} \right] \approx \frac{1}{{255}}\left[ {\begin{array}{c} {219}&{}&{} \\ {}&{224}&{} \\ {}&{}&{224} \end{array}} \right]\left[ {\begin{array}{c} {0.299}&{0.587}&{0.114} \\ { - 0.169}&{ - 0.331}&{0.500} \\ {0.500}&{ - 0.419}&{ - 0.081} \end{array}} \right] 0.2570.1480.4390.5040.2910.3680.0980.4390.071 2551 219224224 0.2990.1690.5000.5870.3310.4190.1140.5000.081

4 验证

为了验证以上的分析是否正确,这里提供了一个测试脚本,有兴趣的可以自行复制运行。

# -*- coding: utf-8 -*-
import numpy as np
import cv2

rgb2yuv_470_full = np.array([
    [ 0.299,  0.587,  0.114],
    [-0.147, -0.289,  0.436],
    [ 0.615, -0.515, -0.100]
])

rgb2yuv_601_full = np.array([
    [ 0.299,  0.587,  0.114],
    [-0.169, -0.331,  0.500],
    [ 0.500, -0.419, -0.081]
])

rgb2yuv_709_full = np.array([
    [ 0.2126,  0.7152,  0.0722],
    [-0.1146, -0.3854,  0.5000],
    [ 0.5000, -0.4542, -0.0458]
])

yuv2rgb_470_full = np.linalg.inv(rgb2yuv_470_full)
yuv2rgb_601_full = np.linalg.inv(rgb2yuv_601_full)
yuv2rgb_709_full = np.linalg.inv(rgb2yuv_709_full)

'''
print('rgb2yuv_470_full')
print(rgb2yuv_601_full)
print('yuv2rgb_470_full')
print(yuv2rgb_601_full)
print('rgb2yuv_601_full')
print(rgb2yuv_601_full)
print('yuv2rgb_601_full')
print(yuv2rgb_601_full)
print('rgb2yuv_709_full')
print(rgb2yuv_709_full)
print('yuv2rgb_709_full')
print(yuv2rgb_709_full)
'''

rgb2yuv_601_comp = 1./255 * np.diag([219, 224, 224]).dot(rgb2yuv_601_full)
rgb2yuv_709_comp = 1./255 * np.diag([219, 224, 224]).dot(rgb2yuv_709_full)

yuv2rgb_601_comp = np.linalg.inv(rgb2yuv_601_comp)
yuv2rgb_709_comp = np.linalg.inv(rgb2yuv_709_comp)

'''
print('rgb2yuv_601_comp')
print(rgb2yuv_601_comp)
print('yuv2rgb_601_comp')
print(yuv2rgb_601_comp)
print('rgb2yuv_709_comp')
print(rgb2yuv_709_comp)
print('yuv2rgb_709_comp')
print(yuv2rgb_709_comp)
'''

def convert_bgr_to_yuv(bgr):
    h, w = bgr.shape[:2]
    assert h % 4 == 0 and w % 4 == 0
    b, g, r = bgr.transpose(2, 0, 1)
    y =  0.299 * r + 0.587 * g + 0.114 * b
    # COLOR_BGR2YUV
    u = -0.147 * r - 0.289 * g + 0.436 * b + 128
    v =  0.615 * r - 0.515 * g - 0.100 * b + 128
    yuv_470 = np.clip(np.array([y, u, v]).transpose(1, 2, 0), 0, 255).astype('uint8')
    # COLOR_BGR2YCrCb
    u = -0.169 * r - 0.331 * g + 0.500 * b + 128
    v =  0.500 * r - 0.419 * g - 0.081 * b + 128
    yvu_601 = np.clip(np.array([y, v, u]).transpose(1, 2, 0), 0, 255).astype('uint8')
    # COLOR_BGR2YUV_FOCC
    y = y * 219. / 255 + 16
    u = u * 224. / 255 + 16
    v = v * 224. / 255 + 16
    yuv_fcc = np.clip(np.array([y, u, v]).transpose(1, 2, 0), 0, 255).astype('uint8')
    return yuv_470, yvu_601, yuv_fcc

def convert_yuv_to_fcc(yuv, fcc):
    fcc_list_420 = ['NV12', 'NV21', '420S', 'YV12', 'IYUV', 'I420', '420P']
    fcc_list_422 = ['UYVY', 'Y422', 'UYNV', 'YUY2', 'YVYU', 'YUYV', 'YUNV']
    assert fcc in fcc_list_420 + fcc_list_422
    h, w = yuv.shape[:2]
    assert h % 4 == 0 and w % 4 == 0
    y, u, v = yuv.transpose(2, 0, 1)
    if fcc in fcc_list_420:
        yuv420 = np.zeros([3*h//2, w], dtype='uint8')
        yuv420[:h] = y
        if fcc == 'NV12':
            yuv420[h:, 0::2] = u[::2, ::2]
            yuv420[h:, 1::2] = v[::2, ::2]
        elif fcc == 'NV21' or fcc == '420S':
            yuv420[h:, 0::2] = v[::2, ::2]
            yuv420[h:, 1::2] = u[::2, ::2]
        elif fcc == 'YV12' or fcc == '420P':
            yuv420[h: h+h//4] = v[::2, ::2].reshape([-1, w])
            yuv420[-h//4:   ] = u[::2, ::2].reshape([-1, w])
        elif fcc == 'IYUV' or fcc == 'I420':
            yuv420[h: h+h//4] = u[::2, ::2].reshape([-1, w])
            yuv420[-h//4:   ] = v[::2, ::2].reshape([-1, w])
        return yuv420
    elif fcc in fcc_list_422:
        yuv422 = np.zeros([h, w, 2], dtype='uint8')
        if fcc == 'UYVY' or fcc == 'Y422' or fcc == 'UYNV':
            yuv422[:, 0::2, 0] = u[:, ::2]
            yuv422[:, 1::2, 0] = v[:, ::2]
            yuv422[:,    :, 1] = y
        elif fcc == 'YUY2' or fcc == 'YUYV' or fcc == 'YUNV':
            yuv422[:,    :, 0] = y
            yuv422[:, 0::2, 1] = u[:, ::2]
            yuv422[:, 1::2, 1] = v[:, ::2]
        elif fcc == 'YVYU':
            yuv422[:,    :, 0] = y
            yuv422[:, 0::2, 1] = v[:, ::2]
            yuv422[:, 1::2, 1] = u[:, ::2]
        return yuv422
    else:
        return yuv

def convert_yuv470_to_bgr(yuv470):
    rgb = (yuv470 - np.array([0, 128, 128])).dot(yuv2rgb_470_full.T)
    bgr = np.clip(rgb[..., ::-1], 0, 255).astype('uint8')
    return bgr

def convert_yvu601_to_bgr(yvu601):
    rgb = (yvu601[..., [0, 2, 1]] - np.array([0, 128, 128])).dot(yuv2rgb_601_full.T)
    bgr = np.clip(rgb[..., ::-1], 0, 255).astype('uint8')
    return bgr

def test_yuv2bgr_fcc(yuv):
    fcc_dict_420 = {
        'NV12': cv2.COLOR_YUV2BGR_NV12,
        'NV21': cv2.COLOR_YUV2BGR_NV21, '420S': cv2.COLOR_YUV420sp2BGR,
        'YV12': cv2.COLOR_YUV2BGR_YV12, '420P': cv2.COLOR_YUV420p2BGR,
        'IYUV': cv2.COLOR_YUV2BGR_IYUV, 'I420': cv2.COLOR_YUV2BGR_I420
        }
    fcc_dict_422 = {
        'UYVY': cv2.COLOR_YUV2BGR_UYVY, 'Y422': cv2.COLOR_YUV2BGR_Y422, 'UYNV': cv2.COLOR_YUV2BGR_UYNV,
        'YUY2': cv2.COLOR_YUV2BGR_YUY2, 'YUYV': cv2.COLOR_YUV2BGR_YUYV, 'YUNV': cv2.COLOR_YUV2BGR_YUNV,
        'YVYU': cv2.COLOR_YUV2BGR_YVYU
        }
    yuv420up = yuv.copy()
    yuv420up[::2, 1::2, 1:] = yuv420up[::2, ::2, 1:]
    yuv420up[1::2, :, 1:] = yuv420up[::2, :, 1:]
    rgb = (yuv420up - np.array([16, 128, 128])).dot(yuv2rgb_601_comp.T)
    bgr420 = np.clip(rgb[..., ::-1], 0, 255).astype('int')
    yuv422up = yuv.copy()
    yuv422up[:, 1::2, 1:] = yuv422up[:, ::2, 1:]
    rgb = (yuv422up - np.array([16, 128, 128])).dot(yuv2rgb_601_comp.T)
    bgr422 = np.clip(rgb[..., ::-1], 0, 255).astype('int')
    for fcc, code in fcc_dict_420.items():
        yuv_fcc = convert_yuv_to_fcc(yuv, fcc)
        bgr_cv = cv2.cvtColor(yuv_fcc, code)
        print('{} bgr max diff: {}'.format(fcc, np.max(np.abs(bgr420 - bgr_cv))))
    for fcc, code in fcc_dict_422.items():
        yuv_fcc = convert_yuv_to_fcc(yuv, fcc)
        bgr_cv = cv2.cvtColor(yuv_fcc, code)
        print('{} bgr max diff: {}'.format(fcc, np.max(np.abs(bgr422 - bgr_cv))))

if __name__ == '__main__':
    w, h = 128, 128
    bgr = np.random.randint(0, 256, [h, w, 3], dtype='uint8')
    yuv470, yvu601, yuv_fcc = convert_bgr_to_yuv(bgr)

    # test COLOR_BGR2YUV
    y0 = cv2.cvtColor(bgr, cv2.COLOR_BGR2YUV)
    x0 = cv2.cvtColor(y0, cv2.COLOR_YUV2BGR)
    x1 = convert_yuv470_to_bgr(yuv470)
    d0 = np.max(yuv470.astype('int') - y0)
    d1 = np.max(x1.astype('int') - x0)
    print('470 max diff: yuv={}, bgr={}'.format(d0, d1))

    # test COLOR_BGR2YCrCb
    y0 = cv2.cvtColor(bgr, cv2.COLOR_BGR2YCrCb)
    x0 = cv2.cvtColor(y0, cv2.COLOR_YCrCb2BGR)
    x1 = convert_yvu601_to_bgr(yvu601)
    d0 = np.max(yvu601.astype('int') - y0)
    d1 = np.max(x1.astype('int') - x0)
    print('601 max diff: yuv={}, bgr={}'.format(d0, d1))

    # test fcc yuv2bgr
    test_yuv2bgr_fcc(yuv_fcc)

    # test fcc br2yuv
    y0 = cv2.cvtColor(bgr, cv2.COLOR_BGR2YUV_I420)
    y1 = convert_yuv_to_fcc(yuv_fcc, 'I420')
    print('I420 yuv max diff: {}'.format(np.max(np.abs(y1.astype('int') - y0))))

    y0 = cv2.cvtColor(bgr, cv2.COLOR_BGR2YUV_IYUV)
    y1 = convert_yuv_to_fcc(yuv_fcc, 'IYUV')
    print('IYUV yuv max diff: {}'.format(np.max(np.abs(y1.astype('int') - y0))))

    y0 = cv2.cvtColor(bgr, cv2.COLOR_BGR2YUV_YV12)
    y1 = convert_yuv_to_fcc(yuv_fcc, 'YV12')
    print('YV12 yuv max diff: {}'.format(np.max(np.abs(y1.astype('int') - y0))))


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/751486.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【图像分割和识别】活动形状模型 (ASM) 和活动外观模型 (AAM)(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

MySQL常用命令1

文章目录 SQL分类DDL数据库数据库中的表 DMLDQLDCL函数约束多表查询多表关系多表查询概述内连接外连接自连接联合查询子查询标量子查询列子查询行子查询表子查询 多表查询案例 事务事务简介事务操作事务隔离级别 SQL分类 DDL 数据库 查询所有数据库 show databases;查询当前…

Android 四大布局使用详解

和你一起终身学习&#xff0c;这里是程序员Android 经典好文推荐&#xff0c;通过阅读本文&#xff0c;您将收获以下知识点: 一、LinearLayout 线性布局二、RelativeLayout 关系布局三、FrameLayout 帧布局四、TableLayout 表格布局 在Android 中&#xff0c;有界面存在的地方就…

Linux 调试进阶(多场景覆盖)

1. 背景 最近在小米项目中分析较多crash问题&#xff0c;结合之前在《程序员的自我修养——链接&#xff0c;状态与库》书中学到的一点皮毛。感触颇深&#xff0c;总结了一些经验&#xff0c;相信对大家分析相关问题有一定的帮助。因为本文的知识点偏于底层&#xff0c;大部分…

前端面试题-js(三)

31 介绍js有哪些内置对象 Object 是 JavaScript 中所有对象的⽗对象数据封装类对象&#xff1a; Object 、 Array 、 Boolean 、 Number 和 String其他对象&#xff1a; Function 、 Arguments 、 Math 、 Date 、 RegExp 、 Error 32 说⼏条写JavaScript的基本规范 不要在同…

【算法集训之线性表篇】Day 09

文章目录 题目基本设计思想代码实现效果 题目 定义三元组(a,b,c)(a,b,c均为整数)的距离D|a-b||b-c||c-a|。给定3个非空集合S1&#xff0c;S2&#xff0c;S3&#xff0c;按升序分别存储在三个数组中。请设计一个尽可能高效的算法&#xff0c;计算并输出所有有可能的三元组中的最…

TabView, DarkMode 的使用

1. TabView 选项卡视图的使用 1.1 实现 /// 选项卡视图 struct TabViewBootcamp: View {State var selectedTab: Int 0let icons: [String] ["heart.fill", "globe", "house.fill", "person.fill"]var body: some View {// tabView…

青岛大学_王卓老师【数据结构与算法】Week05_11_栈与递归_学习笔记

本文是个人学习笔记&#xff0c;素材来自青岛大学王卓老师的教学视频。 一方面用于学习记录与分享&#xff0c; 另一方面是想让更多的人看到这么好的《数据结构与算法》的学习视频。 如有侵权&#xff0c;请留言作删文处理。 课程视频链接&#xff1a; 数据结构与算法基础…

考虑设备动作损耗的配电网分布式电压无功优化(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

【Nginx】proxy_set_header的变量与X-Forwarded-For伪造客户端IP漏洞

前言 上面突然说&#xff0c;需要检查Nginx反向代理的安全问题并给出了修改方法&#xff0c;小白的我一脸懵逼&#xff0c;明明都是中文&#xff0c;连在一起咋就看不明白了。于是乎&#xff0c;对着修改内容简单学习了一下&#xff0c;在此做个记录&#xff0c;如有问题请大佬…

【Ubuntu】Ubuntu14 安装 open-jdk-1.7(open-jdk-7) 最新 持续更新中

【Ubuntu】Ubuntu14 安装 open-jdk-1.7&#xff08;open-jdk-7&#xff09; 最新 持续更新中 一、概述二、特别说明三、开发环境四、安装 open-jdk-1.7五、配置环境1、配置环境变量路径2、环境变量3、完成安装 一、概述 一个好的文章能够帮助开发者完成更便捷、更快速的开发。…

Hive3.1.2伪分布式安装

Hive3.1.2安装 前言 Hive是何物&#xff0c;自己去百度&#xff0c;在此不多bb&#xff0c;直接开整… 学习一个组件&#xff0c;个人觉得最重要的是先学会安装再说&#xff0c;巧妇难为无米之炊撒… 下载 下载地址&#xff1a;https://downloads.apache.org/hive/hive-3.…

Python探索金融数据进行时间序列分析和预测

大家好&#xff0c;时间序列分析是一种基于历史数据和趋势分析进行预测的统计技术。它在金融和经济领域非常普遍&#xff0c;因为它可以准确预测趋势并做出明智的决策。本文将使用Python来探索经济和金融数据&#xff0c;执行统计分析&#xff0c;并创建时间序列预测。 我们将…

面试题更新之-CSS Hack是什么?ie6,7,8的hack分别是什么?

文章目录 导文CSS Hack的定义广泛应用的CSS Hack技巧ie6,7,8的hack分别是什么&#xff1f; 导文 面试题更新之-CSS Hack是什么?ie6,7,8的hack分别是什么&#xff1f; CSS Hack的定义 CSS Hack指的是在CSS中使用一些特定的代码或技巧&#xff0c;通过利用不同浏览器对CSS实现的…

大学生用一周时间给麦当劳做了个App(安卓版)

背景 有个大学生粉丝最近私信联系我&#xff0c;说基于我之前开源的多语言项目做了个仿麦当劳的项目&#xff0c;虽然只是个样子货&#xff0c;但是收获颇多&#xff0c;希望把自己写的代码开源出来供大家一起学习进度。这个小伙伴确实是非常积极上进&#xff0c;很多大学生&a…

Grid++Report 6.8.6 Crack

GridReport报表开发工具总体介绍 报表开发工具概述 GridReport是一款同时支持C/S软件(桌面程序)与B/S软件(WEB程序)开发的报表工具&#xff0c;功能全面易学易用。C/S开发适用于VB.NET、C#、VB、VC、Delphi、CBuilder、QT与易语言等。B/S开发适用于ASP.NET(C# & VB.NET),…

蒲公英打包环境搭建碰到问题

一&#xff1a;证书那边选择手动&#xff0c;不要自动&#xff0c;——》debug配置dev证书&#xff0c;release配置ad-hoc证书 二&#xff1a;证书有时候不生效&#xff0c;删除重新下载。~/Library/MobileDevice/Provisioning Profiles 三&#xff1a;更新测试手机时&#…

数字化孪生技术在工业上的应用场景和案例分享

编者按&#xff1a;数字化孪生是一个完整的生命周期概念&#xff0c;它从设计和制造开始&#xff0c;贯穿整个生产过程&#xff0c;直到产品退役。数字化孪生将现实世界和虚拟世界相结合&#xff0c;使企业能够更好地理解和优化物理系统的运行情况&#xff0c;从而提高生产效率…

Java多线程下的单例模式参考

Java多线程下的单例模式参考 单例有多种的写法&#xff0c;本例是懒汉式单例的一种写法。在高并发环境下需要注意的是&#xff1a; 1.单例在并发访问并调用其相应的getInstance方法的时候也会造成创建多个实例对象&#xff0c;加锁是必要的。 2.使用synchronized是比较好的解…

基于预测控制模型的自适应巡航控制仿真与机器人实现(Matlab代码实现)

目录 &#x1f4a5;1 概述 &#x1f4da;2 运行结果 &#x1f389;3 参考文献 &#x1f468;‍&#x1f4bb;4 Matlab代码 &#x1f4a5;1 概述 自适应巡航控制技术为目前由于汽车保有量不断增长而带来的行车安全、驾驶舒适性及交通拥堵等问题提供了一条有效的解决途径&am…