用HDL实现YCbCr422到RGB888的转换
1.1 ITU-R BT.656 格式简说
ITU-R BT.601 和 ITU-R BT.656 是 国 际 电 信 联 盟 ( International Telecommunication Union)无线通信部门(ITU-R)制定的标准。严格来说,ITU-RBT.656 应该是隶属 ITU-RBT.601 的一个子协议。 ITU-RBT.601 是演播室数字电视编码参数标准,而 ITU-R BT.656 则是 ITU-R BT.601 附件 A 中的数字接口标准,用于主要数字视频设备(包括芯片)之间采用 27Mhzs 并口或 243Mbs 串行接口的数字传输接口标准。
详见《Image\005_OV5640_DDR3_YCbCr422_RGB888\DOC\ITU-R BT.656 协议.docx》
由于 ITU-R BT.656 视频信号为 YUV 信号,同时,目前 CMOS 摄像头支持RGB565、 YCbCr、 Bayer 这几种模式。只玩过 RGB565,未免不太爽了。研究YUV 格式视频的解码,对于未来 BT.656 视频流的处理,以及相关算法的了解,很有好处。既然决定了做视频图像算法, 那么 YCbCr 转 RGB888 算法, 就必须得搞定。
YUV 信号的提出,是因为国际上出现彩色电视,为了兼容黑白电视的信号而设计的,其由于视频码率,压缩,兼容性的优势,一直被沿用至今。如下是完整的 YUV4:2:2 的视频格式数据流:
图5‑1完整的 YUV4:2:2 的视频格式数据流
为了识别帧头帧尾,在 YUV 标准中,还添加了帧头帧尾基准码,如FF0000XY。 PAL/NTSC 都是通过模拟传输的,在接收端通过解码后,将是以上的序列,因此可以用通过 FF0000XY 这几个序列的软件解码,解码出标准数字视频流的行场信号。 更多关于 BT.656 视频流的帧头、帧尾识别标准,请查看相关文档。
《Image\005_OV5640_DDR3_YCbCr422_RGB888\DOC\图像时序规范》
在本章中,我们只研究针对 OV5640 的 YCbCr 解码算法的实现。
OV5640 在 YCbCr422 格式下输出的视频流格式如下。当然由于 OV5640 同时输出了行场信号,我们可以直接硬件解码,不需要通过 FF0000XY 识别。同行OV5640 输出的 YCbCr422 是阉割版的 BT.656 视频流信号,即视频流逐行输出,不存在标准 BT.656 所谓的奇场,偶场信号。
图5‑2 OV5640 输出的 YCbCr422 是阉割版的 BT.656 视频流信号
由于 YCbCr422 格式下,每行发送 640*4 个数据,与 RGB565 一样。因此,我们只需要根据 cmos_vsync、 cmos_href,完全按照 RGB565 一样的接收模式接受数据,同时经过后续 YUV422 转 RGB888 的算法处理,便可以实现 YCbCr422实现 RGB888 数据格式的转换。
1.2 YUV/YCbCr 视频格式简说
YUV 由 Y、 U、 V 复合而成,其中 Y:亮度(16-235) , U:色彩 V:饱和度。YUV 有很多格式,比如 4:2:2; 4:2:2; 4:2:0 等。使用最多的是 YUV422 格式,如下图所示:
图5‑4 YUV422格式码流
YUV422 模式即水平方向上 UV 的采样速度为 Y 的一半,相当于每两个点采样一个 U、 V,每一个点采样一个 Y。这样被允许的原因是因为,我们的眼睛对亮度的敏感度远大于对色度的敏感度,因此可以通过牺牲色度的采样率来达到图像数据压缩的目的。
当年的黑白电视,只有亮度,即 Y; YUV 格式的出现很好的兼容了不同制式的电视,因为 YUV 既能兼容灰度信号,又能通过 YUV2RGB 可以转换为彩色图像,兼容彩色液晶。不明白的孩子,可以直接让{R,G,B}={Y,Y,Y},看看是不是黑白灰度的图像。
YUV 主要应用在模拟系统中,而 YCbCr 是通过 YUV 信号的发展,通过了校正,主要应用在数字视频中的一种格式,一般意义上 YCbCr 即为 YUV 信号,没有严格的划分。 CbCr 分别为蓝色色差、红色色差,详细的说明请看前面的文章。
1.3 YUV422 格式的配置与拼接捕获
此时我们将注意力转移到 OV5640的寄存器配置中来。 前面我们已经完成了RGB565 格式、 RAW8 格式的视频流输出配置, 我们只需要修改极少的一两个寄存器,便能转换为 YUV422 输出。 关于 0x4300 的 寄存器设置, 仅需修改此处便能配置为 YUV422 格式输出,寄存器如下所示:
图5‑8 YUV422格式输出寄存器设置
详细说明请看数据手册86页。最常见的格式为 Cb、 Y、 Cr、 Y, 即 UYVYUYVY,即如图中设置。
这里还需要设置一下ISP即0x501f这一个寄存器,如上图所示。
不过有一个值得开心的事情是, RGB565 与 YUV 格式输出的视频,每行均有640*2 个 像素 , 及 速 率 完 全 一 样 , 因 此 我 们 可 以 直 接 套 用sensor_decode 的采集框架, 模块输出的 16Bit 的 cmos_frame_data, 即为 UY 或 VY 的拼接信号, 并且按照 UY、 VY、 UY、 VY...的序列输出。
此时我们已经得到了 YCbCr 相邻 2 个数据拼接后的结果,在后续模块中,可以直接通过这个序列,来完成 YUV422 到 RGB888 的转换
1.4 YUV422 转 YUV444 的 HDL实现
首先,第一步,前面得到的 YCbCr422 为 2:1 的分量,为了更直观的实现YCbCr转 RGB 的算法,我们首先将 YCbCr422 转换成 YCbCr444, 即通过 Cb、Cr 的分配,完整的将每个像素均赋予 YCbCr 的格式。 这里 Bingo 通过多级寄存及分量拼接, 从第三个像素的时刻开始,持续输出完整的 YCbCr 的格式, 实现的步骤如下:
(1) 寄存 Cb0、 Y0
(2) 寄存 Cr0、 Y1
(3) 输出 Y0、 Cb0、 Cr0,寄存 Cb1、 Y2
(4) 输出 Y1、 Cb0、 Cr0,寄存 Cr1、 Y3
(5) 输出 Y2、 Cb1、 Cr1,寄存 Cb02、 Y02
(6) 输出 Y3、 Cb1、 Cr1,寄存 Cr02、 Y12
(7) ……
可见,通过(1)与(2)的寄存,从(3) 开始,便可以持续的输出完整的YCbCr 格式。 但是问题(1) 与(2) 消耗了 2 个时钟,因此我们需要人为的生成 2 个时钟, 来补充最后 2 个像素的数据输出。 这可以通过 per_frame_clken 的寄存实现, 如下所示:
这里延时 4 个时钟的原因,是由于cmos_pclk 的时钟频率,为拼接后输出的速度的 2 倍。因此 4 次寄存,刚好延时了 2 个像素。 yuv_process_href 与yuv_process_clken 作为前面(1) ~(7) ……的读取使能与读取时钟信号。这里给出 YCbCr422 恢复 YCbCr444 的实现方式, 如下:
这里给出上述 0~5 的状态机转移图, 如下所示。可见从 0~1 为寄存, 2~5 开始循环输出, 直到一行数据的结束。
5‑9状态机转移图
1.5 YUV444 转 RGB888 的 HDL 实现
上一小节中,我们已经得到了每个像素均完整的 8Bit 的 Y、 Cb、 Cr 信号,在此设计 YCbCr444 转 RGB888 算法, 完全几乎与 RGB888 转 YCbCr444 类似的实现方式。
相关的软件应用手册同样给出了 YCbCr 转 RGB 的算法,如下所示:
用上述的转换方式,图像非常的可以。因此转换公式,如下:
R = 1.164(Y-16) + 1.596(Cr-128)
G = 1.164(Y-16) - 0.391(Cb-128)- 0.813(Cr-128)
B = 1.164(Y-16) + 2.018(Cb-128)
->
R = 1.164Y + 1.596Cr - 222.912
G = 1.164Y - 0.391Cb - 0.813Cr +135.488
B = 1.164Y + 2.018Cb - 276.928
->
R << 9 = 596Y + 817Cr -114131
G << 9 = 596Y - 200Cb -416Cr + 69370
B << 9 = 596Y + 1033Cb –141787
针对上式而言,我们需要提取最后的 R、 G、 B,这里只需要进行三个步骤。 首先,计算每一个步中的分量,如下所示:
代码5‑1
1. reg [19:0] img_Y_r1; //8 + 9 + 1 = 18Bit 2. reg [19:0] img_Cb_r1, img_Cb_r2; 3. reg [19:0] img_Cr_r1, img_Cr_r2; 4. always@(posedge clk or negedge rst_n) 5. begin 6. if(!rst_n) 7. begin 8. img_Y_r1 <= 0; 9. img_Cb_r1 <= 0; img_Cb_r2 <= 0; 10. img_Cr_r1 <= 0; img_Cr_r2 <= 0; 11. end 12. else 13. begin 14. img_Y_r1 <= per_img_Y * 18'd596; 15. img_Cb_r1 <= per_img_Cb * 18'd200; 16. img_Cb_r2 <= per_img_Cb * 18'd1033; 17. img_Cr_r1 <= per_img_Cr * 18'd817; 18. img_Cr_r2 <= per_img_Cr * 18'd416; 19. end 20.end |
---|
第二步,计算 512 倍扩大、 9 次移位后的结果, 如下所示:
代码 5‑2
1. //-------------------------------------------- 2. /********************************************** 3. R << 9 = 596Y + 817Cr - 114131 4. G << 9 = 596Y - 200Cb - 416Cr + 69370 5. B << 9 = 596Y + 1033Cb - 141787 6. **********************************************/ 7. reg [19:0] XOUT; 8. reg [19:0] YOUT; 9. reg [19:0] ZOUT; 10.always@(posedge clk or negedge rst_n) 11.begin 12. if(!rst_n) 13. begin 14. XOUT <= 0; 15. YOUT <= 0; 16. ZOUT <= 0; 17. end 18. else 19. begin 20. XOUT <= (img_Y_r1 + img_Cr_r1 - 20'd114131)>>9; 21. YOUT <= (img_Y_r1 - img_Cb_r1 - img_Cr_r2 + 20'd69370)>>9; 22. ZOUT <= (img_Y_r1 + img_Cb_r2 - 20'd141787)>>9; 23. end 24.end |
---|
第三步,根据上述 XOUT、 YOUT、 ZOUT 的结果, 如果小于 0(Bit[10] ==1) ,则赋 0; 如果大于 255, 则赋 255; 如果在 0~255 之间,则保持原值, 具体的实现如下所示:
代码 5‑3
1. //------------------------------------------ 2. //Divide 512 and get the result 3. //{xx[19:11], xx[10:0]} 4. reg [7:0] R, G, B; 5. always@(posedge clk or negedge rst_n) 6. begin 7. if(!rst_n) 8. begin 9. R <= 0; 10. G <= 0; 11. B <= 0; 12. end 13. else 14. begin 15. R <= XOUT[10] ? 8'd0 : (XOUT[9:0] > 9'd255) ? 8'd255 : XOUT[7:0]; 16. G <= YOUT[10] ? 8'd0 : (YOUT[9:0] > 9'd255) ? 8'd255 : YOUT[7:0]; 17. B <= ZOUT[10] ? 8'd0 : (ZOUT[9:0] > 9'd255) ? 8'd255 : ZOUT[7:0]; 18. end 19.end |
---|
此时我们将得到最终的 RGB888 信号,由于前面经过了 3 个时钟的计算, 因此必须将行场、读取使能信号偏移 3 个时钟。 同时根据行有效信号,使能输出最终的 RGB 信号,如下所示:
代码 5‑4
1. //------------------------------------------ 2. //lag n clocks signal sync 3. reg [2:0] post_frame_vsync_r; 4. reg [2:0] post_frame_href_r; 5. reg [2:0] post_frame_clken_r; 6. always@(posedge clk or negedge rst_n) 7. begin 8. if(!rst_n) 9. begin 10. post_frame_vsync_r <= 0; 11. post_frame_href_r <= 0; 12. post_frame_clken_r <= 0; 13. end 14. else 15. begin 16. post_frame_vsync_r <= {post_frame_vsync_r[1:0], per_frame_vsync}; 17. post_frame_href_r <= {post_frame_href_r[1:0], per_frame_href}; 18. post_frame_clken_r <= {post_frame_clken_r[1:0], per_frame_clken}; 19. end 20.end 21.assign post_frame_vsync = post_frame_vsync_r[2]; 22.assign post_frame_href = post_frame_href_r[2]; 23.assign post_frame_clken = post_frame_clken_r[2]; 24.assign post_img_red = post_frame_href ? R : 8'd0; 25.assign post_img_green = post_frame_href ? G : 8'd0; 26.assign post_img_blue = post_frame_href ? B : 8'd0; |
---|
1.6 YCbCr422 转 RGB888 功能测试
将 CMOS_Capture_RGB565 模 块 捕 获 的 YCbCr 输 出 给Video_Image_Processor, 并且经过相应的算法模块,实现YCbCr422 转 RGB888 功能
详细代码请参考源码。
最后,全编译,下载测试, HDMI显示通过 YCbCr422→YCbCr444→RGB888的彩色图像。
下一篇: RGB565与RGB888颜色互相转换
推荐阅读
-
数的机器码表示:原码、反码、补码、变形补码、移码和浮点数编码-数学定义:例:+111的原码为0111,-101的原码为1101 (2) 纯小数的原码表示 纯小数的原码首位同样为符号位,后面的数值则表示小数的尾数,纯小数的整数位为默认为0无需表示。 例:+0.111的原码为0111,-0.101的原码为1101 可以看到,+111和+0.111的原码同为0111,这是因为约定的小数点位置不同,整数的原码的小数点约定在末尾,纯小数的原码的小数点约定在数值的最前面,这样通过约定小数点的位置来表示数的方法就称为定点数表示法,约定小数点位置实际上就是约定编码中每一位的权重。 二、反码 正数的反码与其原码相同。 负数的反码是其对应原码的符号位不变,数值位按位取反。 数学定义:例: 真值 +111 -101 +0.111 -0.101 原码 0111 1101 0111 1101 反码 0111 1010 0111 1010 三、补码 原码虽然转换很简单,但是在做减法时操作很复杂(减不够还要借位),因此计算机在做加负数操作时会先将负数的原码转换为补码再做加法。 先举个栗子,假设时钟现在是9点钟,我把时针往回拨3个小时是6点钟,或者顺时针往后拨9个小时还是6点钟,也就是说9-3的结果等同于9+9(mod 12),对于模数12,-3的补码为+9,这就引申出了一种将减法转换为加法的思想,把减去一个正数视为加上一个负数(例如9+(-3)),再将负数转换为对应的补码,最后就可以和补码做加法了,若结果超出了模数则丢弃一个模数即可。 如图所示:9减去灰色的部分(-3)就等同于加上蓝色的部分,即-3的补码即为蓝色部分的长度9(mod 12)。即补码=模数+真值(超出模数则舍弃一个模数) (1) 整数的补码表示 对于一个n位的二进制真值x,则取模数为2^(n+1),若x为正数则补码和原码相同(加上一个模数又需舍弃一个模数 故相同),若为负数则补码为模数加上x。相对于原码,补码这里的首位就不仅代表原数真值的符号了,也是补码自己的一个数值位。 取模数为2^(n+1)是因为在需要舍弃模数时只需要舍弃运算结果(二进制数)的最高位即可,这在计算机中很容易实现 数学定义:例:三位二进制数的模数2^4就是10000,故+111的补码为0111(即10000 + 111 = 0111 (舍弃模数位)),-101的补码为1011(即10000 - 101 = 1011) 补码运算示例:那么+111 - 101 = +111 + (-101) = 0111 + 1011 = 10010,运算结果只保留后四位(即舍弃模数位),故计算结果为0010。这样就通过加法实现了减法运算。 补码可表示数据范围:由数学定义可知,n位二进制补码可表示的数据范围为 -2n-1~2n-1-1。以8位的byte类型数为例,可表示的数据范围为 -27~27-1,即-128至+127,最小负数-128(补码:1000 0000),最大负数-1(补码:1111 1111),0(补码:0000 0000),最小正数1(补码:0000 0001),最大正数127(补码:0111 1111)。 由补码求真值:正数的补码即为原码即为真值,负数的真值由计算规则可知 负数真值= - (模数 - 补码),以补码1111 1111为例,其真值 = - (1 0000 0000 - 1111 1111) = - 0000 0001 = -1 (2) 纯小数的补码表示 对于一个纯小数x,则取模数为2^1,正数的补码和原码相同,负数的补码为模数2加上x。同样补码的首位不仅代表原数真值的符号,也是补码的数值位。 数学定义:例:纯小数的模数2就是10,故+0.111的补码为0111,-0.101的补码为1011(小数点约定在符号位后) 计算机中求补码的规则 可以注意到求负数的补码时还是要做减法,这在计算机中就很不方便了,但是通过其数学定义可以看到无论是整数还是纯小数,负数的补码都等于反码的末尾加1,而这又等同于原码数值位从右向左遇到第一个1后,这个1左边的数值位都按位取反,故实际计算机中求补码的规则如下:正数的补码等于原码负数的补码等于原码的数值位从右向左的第一个1左边的所有数值位按位取反(例:byte类型值-6的原码为1000 0110,则其补码为1111 1010) 四、变形补码 两个补码在运算时可能会溢出从而产生错误的结果,比如0111+0101 = 1100,两个正数相加反而得到了一个负数,那么在计算机中要如何判断运算结果是否溢出了呢,这就引申出了变形补码。从直观上看,相对于补码来说变形补码就是用两位来表示符号位,00表示正数,11表示负数。运算结果符号位为01表示正溢出,10表示负溢出。
-
用HDL实现YCbCr422到RGB888的转换