Bootstrap

PotatoPie 4.0 实验教程(19) —— FPGA实现摄像头RGB图像转YCbCr

PotatoPie 4.0开发板教程目录(2024/04/15)

什么是色彩空间?

“色彩空间”一词源于西方的“Color Space”,又称作“色域”,色彩学中,人们建立了多种色彩模型,以一维、二维、三维甚至四维空间坐标来表示某一色彩,这种坐标系统所能定义的色彩范围即色彩空间。我们经常用到的色彩空间主要有RGB色彩空间和YUV色彩空间。

RGB 颜色空间

RGB 颜色系统可能是您更熟悉的一种。 RGB 色彩空间是红色、绿色和蓝色的缩写,是一个在每个屏幕像素中使用这三种颜色来显示照片和视频的系统。

与 YCbCr 色彩空间不同,RGB 同时处理颜色和亮度,因此您的文件将占用更多存储空间。

全 RGB 的颜色系统有 255 个级别 – 0 级表示绝对黑色,而 255 级表示绝对白色。在第 1 级到第 254 级之间,您将获得通过使用红色、绿色和蓝色的不同组合创建的全光谱颜色。

RGB 系统最常用于计算机显示器、电视、摄像、摄影等。它也是视频游戏、网页设计和数字媒体的首选色彩系统。

YCbCr色彩空间

 YCbCr 根据亮度 (luma)(又称整体亮度)定义颜色。 “Y”代表亮度,“Cb”代表蓝色减去亮度,“Cr”代表红色减去亮度。

YCbCr 格式能够分离亮度和色度分量,从而尽可能地从文件中删除冗余。

考虑到人眼对亮度比颜色更敏感,YCbCr优先捕捉亮度细节,消除一些不必要和多余的色度细节。这使您的设备可以更好地压缩 YCbCr 文件并节省一些存储空间,同时尽可能保留视频和图像的详细信息。

考虑到这一点,YCbCr 格式仍然可以产生令人惊讶的广泛颜色范围,使其成为印刷机的绝佳选择。

YCbCr不是一种绝对色彩空间,是YUV压缩和偏移的版本。YCbCr的Y与YUV中的Y含义一致,Cb和Cr与UV同样都指色彩,Cb指蓝色色度,Cr指红色色度。YCbCr 也是视频的本机格式,在应用上很广泛,JPEG、MPEG、DVD、摄影机、数字电视等皆采此一格式。因此一般俗称的YUV大多是指YCbCr。

YCbCr 色彩空间与 YUV 色彩空间在概念上很相似,实际上在很多应用中,它们的术语常常被互换使用。但在技术上有一些微妙的区别:

  1. YCbCr 色彩空间是被国际电信联盟(ITU)标准化的,而 YUV 并非官方标准,通常用于描述模拟视频信号。

  2. YCbCr 色彩空间的转换公式经过了精确的数学推导和标准化,而 YUV 的转换公式可能会有一些变化。

  3. YCbCr 色彩空间更为严格地定义了色度和亮度分量的范围,以便于数字信号的编码和解码。

总的来说,YCbCr 色彩空间是一种常用于数字视频处理的标准化色彩编码系统,它将图像的亮度和色度分开表示,便于对彩色图像进行压缩、传输和处理。

YCbCr格式有:

  • 4∶4∶4
  • 4∶2∶2
  • 4∶1∶1
  • 4∶2∶0

YCbCr 和 RGB 颜色之间的联系

YCbCr 和 RGB 都是决定成像系统可以表示的颜色的主要色彩空间 – 然而,这就是两者之间仅有的联系点。

RGB 将所有颜色表示为不同级别的红、绿和蓝的独特组合。所有三个通道都必须以全分辨率存储,因此您的图像和视频会更重并占用更多空间。

另一方面,YCbCr 将亮度和颜色分开,因此您始终可以以较高分辨率存储亮度以获取更多细节,并以较低分辨率存储颜色,因为无论如何人眼都无法完全看到它们。

YCbCr 与 RGB 的转换公式

RGB转YCbCr
Y = 0.2568*R + 0.5041*G + 0.0979*B + 16;
Cb = -0.1482*R - 0.2910*G + 0.4392*B + 128;
Cr = 0.4392*R - 0.3678*G - 0.0714*B + 128;

YCbCr转RGB
R = 1.1644*(Y- 16) + 1.5960*(Cr - 128);
G = 1.1644*(Y - 16) - 0.3918*(Cb- 128) -0.8130*(Cr- 128);
B = 1.1644*(Y - 16) + 2.0172*(Cb- 128);

python实现上YCbCr转RGB

import cv2
import numpy as np
# 定义 RGB 到 YCbCr 转换的函数
def rgb_to_ycbcr(rgb):
    # 分离 RGB 通道
    R = rgb[:,:,0]
    G = rgb[:,:,1]
    B = rgb[:,:,2]
    # 计算 Y、Cb、Cr 分量
    Y = 0.2568 * R + 0.5041 * G + 0.0979 * B + 16
    Cb = -0.1482 * R - 0.2910 * G + 0.4392 * B + 128
    Cr = 0.4392 * R - 0.3678 * G - 0.0714 * B + 128
    # 将 Y、Cb、Cr 分量合并成 YCbCr 图像
    return np.stack((Y, Cb, Cr), axis=-1)
import os
# 文件绝对路径
current_file_path = __file__
# 借助dirname()从绝对路径中提取目录
current_file_dir = os.path.dirname(current_file_path)
print(f"current_file_dir: {current_file_dir}")
# 读取图片
img = cv2.imread(current_file_dir+'/Lena.jpg')
# 显示原始图像
cv2.imshow('Original Image', img)
# 调用转换函数,将 RGB 图像转换为 YCbCr 色彩空间
ycbcr = rgb_to_ycbcr(img)
# 提取 Y、Cb、Cr 分量
Y = ycbcr[:,:,0]
Cb = ycbcr[:,:,1]
Cr = ycbcr[:,:,2]
# 显示 Y、Cb、Cr 分量图
cv2.imshow('Y Component', Y.astype(np.uint8))
cv2.imshow('Cb Component', Cb.astype(np.uint8))
cv2.imshow('Cr Component', Cr.astype(np.uint8))
# 等待按下任意键继续并关闭窗口
cv2.waitKey(0)
cv2.destroyAllWindows()

代码读取一张图片并将其转换为 YCbCr 色彩空间。然后提取出 Y、Cb、Cr 分量,并显示每个分量的图像。请将 lena.jpg 替换为您要读取的图片文件的路径。

运行结果如下图

matlab实现上YCbCr转RGB

这个Matlab函数实现了RGB到YCbCr色彩空间的转换,并显示了原始图像以及转换后的Y、Cb、Cr分量图像。

import cv2
import numpy as np

# 定义 RGB 到 YCbCr 转换的函数
def rgb_to_ycbcr(rgb):
    # 分离 RGB 通道
    R = rgb[:,:,0]
    G = rgb[:,:,1]
    B = rgb[:,:,2]

    # 计算 Y、Cb、Cr 分量
    Y = 0.2568 * R + 0.5041 * G + 0.0979 * B + 16
    Cb = -0.1482 * R - 0.2910 * G + 0.4392 * B + 128
    Cr = 0.4392 * R - 0.3678 * G - 0.0714 * B + 128

    # 将 Y、Cb、Cr 分量合并成 YCbCr 图像
    return np.stack((Y, Cb, Cr), axis=-1)

import os

# 文件绝对路径
current_file_path = __file__
# 借助dirname()从绝对路径中提取目录
current_file_dir = os.path.dirname(current_file_path)
print(f"current_file_dir: {current_file_dir}")

# 读取图片
img = cv2.imread(current_file_dir+'/Lena.jpg')

# 显示原始图像
cv2.imshow('Original Image', img)

# 调用转换函数,将 RGB 图像转换为 YCbCr 色彩空间
ycbcr = rgb_to_ycbcr(img)

# 提取 Y、Cb、Cr 分量
Y = ycbcr[:,:,0]
Cb = ycbcr[:,:,1]
Cr = ycbcr[:,:,2]

# 显示 Y、Cb、Cr 分量图
cv2.imshow('Y Component', Y.astype(np.uint8))
cv2.imshow('Cb Component', Cb.astype(np.uint8))
cv2.imshow('Cr Component', Cr.astype(np.uint8))

# 等待按下任意键继续并关闭窗口
cv2.waitKey(0)
cv2.destroyAllWindows()

matlab运行脚本时会出现类似下图的错误,提示rgb2ycbcr.m已在matlab路径中存在,直接点更改文件夹即可。

FPGA工程分析

工程数据流图

层次分析

模块代码分析

与demo18相比,只是多了一个rgb2ycbcr的模块,也就是这一段代码,在从SDRAM读出来之后,经它处理后再输出hdmi_tx模块

 

rgb2ycbcr u_rgb2ycbcr

(

.i_clk(clk_pixel),

.i_rst_n(sys_rst_n),

.i_vs(VGA_VS),

.i_hs(VGA_HS),

.i_de(VGA_DE),

.i_r(VGA_RGB[23:16]),

.i_g(VGA_RGB[15:8]),

.i_b(VGA_RGB[7:0]),

.o_vs(ycbcr_vs),

.o_hs(ycbcr_hs),

.o_de(ycbcr_de),

.o_y(ycbcr_Y),

.o_cb(ycbcr_Cb),

.o_cr(ycbcr_Cr)

);

EG4 FPGA可以实现的转换公式

前面的转换公式存在浮点数,我们需要进行浮点转定点的转换。

方法是将式子的右边先乘以一个256,四舍五入取整,然后再除以一个256,这样等式右边相当于没变(不过有一点误差),而除以256对于逻辑来说就是右移256。当然也不是一定要乘以256,乘以1024或2048也是可以的,只是没有必要。

最终算式变成这样:

Y = (66*image_in_r + 129*image_in_g + 25*image_in_b + 4096 )>>8;
Cb = (-38*image_in_r - 74*image_in_g + 112*image_in_b + 32768)>>8;
Cr = (112*image_in_r - 94*image_in_g - 18*image_in_b + 32768 )>>8;

代码中主要就两段。

第一步先计算乘法:

r_d0 <= (66  * i_r) ;
g_d0 <= (129 * i_g) ;
b_d0 <= (25  * i_b) ;
r_d1 <= (38  * i_r) ;
g_d1 <= (74  * i_g) ;
b_d1 <= (112 * i_b) ;
r_d2 <= (112 * i_r) ;
g_d2 <= (94  * i_g) ;
b_d2 <= (18  * i_b) ;

第二步计算加减法:

y_d0 <= (r_d0 + g_d0 + b_d0 + 4096 ) ;

cb_d0 <= (b_d1 - r_d1 - g_d1 + 32768) ;

cr_d0 <= (r_d2 - g_d2 - b_d2 + 32768) ;

需要注意几点:

  • 上面的代码之所以要将公式拆分成两步的原因是为了优化时序,所以做了一个两级流水。
  • 由于上一步做了流水分隔,所以相应的行场信号要往后延一拍。
  • hs <= {hs[0],i_hs};
    vs <= {vs[0],i_vs};
    de <= {de[0],i_de};
  • 要注意因为计算y_d0,bc_d0, br_d0的时候先乘了256,因此需要位宽要16位,这样带来另一个好处,直接取高位就相当于右移了8位。
  • 实验现象

    实验结果

    只显示Y分量的图像

  • 只显示Cb分量
  • 只显示Cr分量
;