根据电视原理的相关知识可知,RGB与的YUV对应关系为:
{Y=0.299 R+0.587 G+0.114 BU=−0.1684 R−0.3316 G+0.5 B=0.564 (B−Y)V=0.5 R−0.4187 G−0.0813 B=0.713 (R−Y)(1)
\begin{cases}
Y= 0.299\ R &+ 0.587\ G &+ 0.114\ B \\
U= -0.1684\ R &- 0.3316\ G &+ 0.5\ B &= 0.564\ (B-Y) \\
V= 0.5\ R &- 0.4187\ G &- 0.0813\ B &= 0.713\ (R-Y) \\
\end{cases}
\tag{1}
⎩⎪⎨⎪⎧Y=0.299 RU=−0.1684 RV=0.5 R+0.587 G−0.3316 G−0.4187 G+0.114 B+0.5 B−0.0813 B=0.564 (B−Y)=0.713 (R−Y)(1)
其中,为了使色差信号的动态范围控制在[-0.5, 0.5],需要进行量化前的归一化处理,需要引入数字色差信号的压缩系数(分别为0.564与0.713)。
参考《现代电视原理》7.4.2节“视频信号量化电平的分配”部分:
在进行8 bit量化时,需要在上下两端留出一定的余量,作为信号超越动态范围的保护带。具体地:
对于亮度信号,在256级的上端留出20级,下端留出16级作为余量,即Y的动态范围为16—235; 对于两个色差信号,在256级的上端留出15级,下端留出16级作为余量,即U、V的动态范围为16—240。根据码电平数字表达式
量化等级=int{量化等级最大值−量化等级最小值模拟电平最大值−模拟电平最小值×对应的数字电平公式+0电平对应得量化等级}(2)
量化等级={\rm{int}}\left\{ \dfrac{量化等级最大值-量化等级最小值}{模拟电平最大值-模拟电平最小值}\times 对应的数字电平公式+0电平对应得量化等级 \right\} \tag{2}
量化等级=int{模拟电平最大值−模拟电平最小值量化等级最大值−量化等级最小值×对应的数字电平公式+0电平对应得量化等级}(2)
可知
{Y′=int{235−160.5−(−0.5)Y+16}U′=int{240−161−0U+128}V′=int{240−161−0V+128}(3)
\begin{cases}
Y' = {\rm int}\left\{\dfrac {235-16}{0.5-(-0.5)}Y+16 \right\}\\
U' = {\rm int}\left\{\dfrac {240-16}{1-0}U+128 \right\}\\
V' = {\rm int}\left\{\dfrac {240-16}{1-0}V+128 \right\}
\end{cases}
\tag{3}
⎩⎪⎪⎪⎪⎪⎪⎨⎪⎪⎪⎪⎪⎪⎧Y′=int{0.5−(−0.5)235−16Y+16}U′=int{1−0240−16U+128}V′=int{1−0240−16V+128}(3)
其中,
由于读取的RGB文件已经进行了8 bit的量化(RGB三个分量范围均为0—255),所以要对公式(2)(2)(2)进行修正,先将YYY映射到-0.5—0.5,UUU、VVV映射到0—1:
{Y′=int{219255Y+16}U′=int{224255U+128}V′=int{224255V+128}(4)
\begin{cases}
Y' = {\rm int}\left\{ \dfrac {219}{255}Y+16 \right\}\\
U' = {\rm int}\left\{ \dfrac {224}{255}U+128 \right\}\\
V' = {\rm int}\left\{ \dfrac {224}{255}V+128 \right\}
\end{cases}
\tag{4}
⎩⎪⎪⎪⎪⎪⎪⎨⎪⎪⎪⎪⎪⎪⎧Y′=int{255219Y+16}U′=int{255224U+128}V′=int{255224V+128}(4)
带入(1)(1)(1)式,得:
{Y=66R+129G+25B255+16U=−38R−74G+112B255+128V=112R−94G−18B255+128(5)
\begin{cases}
Y= \dfrac {66R + 129G + 25B}{255} + 16 \\
U= \dfrac{-38R - 74G + 112B}{255} +128 \\
V= \dfrac{112R - 94G - 18B}{255} + 128
\end{cases}
\tag{5}
⎩⎪⎪⎪⎪⎨⎪⎪⎪⎪⎧Y=25566R+129G+25B+16U=255−38R−74G+112B+128V=255112R−94G−18B+128(5)
为了提高计算机的计算效率且不会造成过大的误差,在程序中使用>> 8
来代替除以255的计算。
将(5)(5)(5)式写为矩阵形式:
[YUV]=1255[6612925−38−74112112−94−18][RGB]+[1616128](6)
\begin{bmatrix}
Y \\ U \\ V
\end{bmatrix}
= \dfrac {1}{255}
\begin{bmatrix}
66 & 129 & 25 \\
-38 & -74 & 112 \\
112 & -94 & -18
\end{bmatrix}
\begin{bmatrix}
R \\ G \\ B
\end{bmatrix}
+
\begin{bmatrix}
16 \\ 16 \\ 128
\end{bmatrix}
\tag{6}
⎣⎡YUV⎦⎤=2551⎣⎡66−38112129−74−9425112−18⎦⎤⎣⎡RGB⎦⎤+⎣⎡1616128⎦⎤(6)
并记A=[6612925−38−74112112−94−18]\boldsymbol A= \begin{bmatrix}
66 & 129 & 25 \\
-38 & -74 & 112 \\
112 & -94 & -18
\end{bmatrix}A=⎣⎡66−38112129−74−9425112−18⎦⎤。
反解,得:
[RGB]=255AT[Y−16U−16V−128](7)
\begin{bmatrix}
R \\ G \\ B
\end{bmatrix}
= 255\boldsymbol A^{\rm T}
\begin{bmatrix}
Y-16 \\ U-16 \\ V-128
\end{bmatrix}
\tag{7}
⎣⎡RGB⎦⎤=255AT⎣⎡Y−16U−16V−128⎦⎤(7)
由于A−1\boldsymbol A^{-1}A−1数量级较小,直接使用会造成较大的计算误差,因而转化为
[RGB]=2552⋅(1255AT)[Y−16U−16V−128](8)
\begin{bmatrix}
R \\ G \\ B
\end{bmatrix}
= 255^2 \cdot \left(\dfrac 1 {255}\boldsymbol A^{\rm T}\right)
\begin{bmatrix}
Y-16 \\ U-16 \\ V-128
\end{bmatrix}
\tag{8}
⎣⎡RGB⎦⎤=2552⋅(2551AT)⎣⎡Y−16U−16V−128⎦⎤(8)
整理得:
R = (298 * Y + 411 * V - 57344) >> 8;
G = (298 * Y - 101 * U - 211 * V + 34739) >> 8;
B = (298 * Y + 519 * U - 71117) >> 8;
main
函数的命令行参数
main
函数实际上具有两个形参,int argc
和char* argv[]
。虽然很多情况下是缺省的,但在例如涉及文件的操作中,使用命令行参数可以为编程提供一定的便利。
设置方法如下:在Visual Studio中,依次点击菜单栏中的项目→项目属性,在项目属性页的配置属性菜单下,点击“调试”。通过浏览文件夹的方式设置工作目录,并在命令参数中输入n
个字符串(以空格分隔)。
这些字符串将会自动传递给argv
,作为其第1
至n
个元素(第0
个元素为"项目名.exe"
),而argc
的值为n+1
。
#pragma once
void rgbLookupTable();
void yuvLookupTable();
void rgb2yuv(FILE*, int, int, int, unsigned char*, unsigned char*, unsigned char*, unsigned char*);
void yuv2rgb(FILE*, int, int, int, unsigned char*, unsigned char*, unsigned char*, unsigned char*);
void errorData(int, unsigned char*, char* []);
rgb2yuv.cpp
#include
#include "declarations.h"
int rgb66[256], rgb129[256], rgb25[256];
int rgb38[256], rgb74[256], rgb112[256];
int rgb94[256], rgb18[256];
void rgbLookupTable()
{
for (int i = 0; i < 256; i++)
{
rgb66[i] = 66 * i;
rgb129[i] = 129 * i;
rgb25[i] = 25 * i;
rgb38[i] = 38 * i;
rgb74[i] = 74 * i;
rgb112[i] = 112 * i;
rgb94[i] = 94 * i;
rgb18[i] = 18 * i;
}
}
void rgb2yuv(FILE* yuvFile, int rgbSize, int w, int h, unsigned char* rgbBuf, unsigned char* yBuf, unsigned char* uBuf, unsigned char* vBuf)
{
unsigned char* uBuf444 = NULL; // 下采样前的U分量缓冲区
unsigned char* vBuf444 = NULL; // 下采样前的V分量缓冲区
uBuf444 = new unsigned char[rgbSize / 3]; // 4:4:4格式
vBuf444 = new unsigned char[rgbSize / 3];
int pxNum = w * h;
// RGB to YUV (4:4:4)
for (int i = 0; i > 8) + 16;
//uBuf444[i] = ((-38 * r - 74 * g + 112 * b) >> 8) + 128;
//vBuf444[i] = ((112 * r - 94 * g - 18 * b) >> 8) + 128;
rgbLookupTable(); // 使用查找表,提高运算效率
yBuf[i] = ((rgb66[r] + rgb129[g] + rgb25[b]) >> 8) + 16;
uBuf444[i] = ((-rgb38[r] - rgb74[g] + rgb112[b]) >> 8) + 128;
vBuf444[i] = ((rgb112[r] - rgb94[g] - rgb18[b]) >> 8) + 128;
}
// 4:4:4 to 4:2:0
for (int i = 0; i < h; i += 2)
{
for (int j = 0; j < w; j += 2)
{
uBuf[i / 2 * w / 2 + j / 2] = uBuf444[i * w + j];
vBuf[i / 2 * w / 2 + j / 2] = vBuf444[i * w + j];
}
}
delete[]uBuf444;
delete[]vBuf444;
fwrite(yBuf, sizeof(unsigned char), rgbSize / 3, yuvFile);
fwrite(uBuf, sizeof(unsigned char), rgbSize / 12, yuvFile);
fwrite(vBuf, sizeof(unsigned char), rgbSize / 12, yuvFile);
}
yuv2rgb.cpp
#include
#include "declarations.h"
int yuv298[256], yuv411[256];
int yuv101[256], yuv211[256];
int yuv519[256];
void yuvLookupTable()
{
for (int i = 0; i < 256; i++)
{
yuv298[i] = 298 * i;
yuv411[i] = 411 * i;
yuv101[i] = 101 * i;
yuv211[i] = 211 * i;
yuv519[i] = 519 * i;
}
}
void yuv2rgb(FILE* rgbFile, int yuvSize, int w, int h, unsigned char* yBuf, unsigned char* uBuf, unsigned char* vBuf, unsigned char* rgbBuf)
{
unsigned char* uBuf444 = new unsigned char[yuvSize * 2 / 3]; // 还原成4:4:4的U分量缓冲区
unsigned char* vBuf444 = new unsigned char[yuvSize * 2 / 3]; // 还原成4:4:4的V分量缓冲区
int pxNum = w * h; // 图像中的总像素数
// 4:2:0 to 4:4:4
for (int i = 0; i < h / 2; i++) // i控制行
{
for (int j = 0; j < w / 2; j++) // j控制列
{
uBuf444[2 * i * w + 2 * j] = uBuf[i * w / 2 + j];
uBuf444[2 * i * w + 2 * j + 1] = uBuf[i * w / 2 + j];
uBuf444[2 * i * w + 2 * j + w] = uBuf[i * w / 2 + j];
uBuf444[2 * i * w + 2 * j + w + 1] = uBuf[i * w / 2 + j];
vBuf444[2 * i * w + 2 * j] = vBuf[i * w / 2 + j];
vBuf444[2 * i * w + 2 * j + 1] = vBuf[i * w / 2 + j];
vBuf444[2 * i * w + 2 * j + w] = vBuf[i * w / 2 + j];
vBuf444[2 * i * w + 2 * j + w + 1] = vBuf[i * w / 2 + j];
}
}
// YUV (4:4:4) to RGB
for (int i = 0; i > 8; // 还原的RGB图像的R分量
r = (yuv298[y] + yuv411[v] - 57344) >> 8; // 还原的RGB图像的R分量
if (r 255)
r = 255;
//g = (298 * y - 101 * u - 211 * v + 34739) >> 8; // 还原的RGB图像的G分量
g = (yuv298[y] - yuv101[u] - yuv211[v] + 34739) >> 8; // 还原的RGB图像的G分量
if (g 255)
g = 255;
//b = (298 * y + 519 * u - 71117) >> 8; // 还原的RGB图像的B分量
b = (yuv298[y] + yuv519[u] - 71117) >> 8; // 还原的RGB图像的B分量
if (b 255)
b = 255;
rgbBuf[3 * i + 2] = (unsigned char)r; // 还原的RGB图像的R分量
rgbBuf[3 * i + 1] = (unsigned char)g; // 还原的RGB图像的G分量
rgbBuf[3 * i] = (unsigned char)b; // 还原的RGB图像的B分量
}
delete[]uBuf444;
delete[]vBuf444;
fwrite(rgbBuf, sizeof(unsigned char), yuvSize * 2, rgbFile);
}
errorData.cpp
#include
#include "declarations.h"
using namespace std;
void errorData(int yuvSize, unsigned char* rgbBuf, char* argv[])
{
FILE* rgbOriFile = NULL; // 原始RGB图像文件指针
FILE* errorFile = NULL; // 误差数据文件指针
const char* rgbOriName = argv[1]; // 原始RGB图像文件名
const char* errorName = argv[4]; // 误差数据文件名
// 打开文件
if (fopen_s(&rgbOriFile, rgbOriName, "rb") == 0)
{
cout << "Successfully opened " << rgbOriName << "." << endl;
}
else
{
cout << "Failed to open " << rgbOriName << "." << endl;
exit(0);
}
if (fopen_s(&errorFile, errorName, "w") == 0)
{
cout << "Successfully opened " << errorName << "." << endl;
}
else
{
cout << "Failed to open " << errorName << "." << endl;
exit(0);
}
unsigned char* rgbOriBuf = new unsigned char[yuvSize * 2];
fread(rgbOriBuf, sizeof(unsigned char), yuvSize * 2, rgbOriFile);
// 将误差数据输出到csv文件
fprintf(errorFile, "Pixel,B Error,G Error,R Error\n");
for (int i = 0; i < yuvSize * 2 / 3; i++)
{
fprintf(errorFile, "%d,%d,%d,%d\n", i, (int)abs(rgbBuf[3 * i] - rgbOriBuf[3 * i]), (int)abs(rgbBuf[3 * i + 1] - rgbOriBuf[3 * i + 1]), (int)abs(rgbBuf[3 * i + 2] - rgbOriBuf[3 * i + 2]));
}
delete[]rgbOriBuf;
fclose(rgbOriFile);
fclose(errorFile);
}
main.cpp
#include
#include "declarations.h"
using namespace std;
int main(int argc, char* argv[])
{
FILE* rgbOriFilePtr = NULL; // 原RGB图像的文件指针
FILE* yuvFilePtr = NULL; // YUV图像的文件指针
FILE* rgbRecFilePtr = NULL; // 复原的RGB文件的文件指针
const char* rgbOriFileName = argv[1]; // 原RGB图像文件名
const char* yuvFileName = argv[2]; // YUV图像文件名
const char* rgbRecFileName = argv[3]; // 复原RGB图像文件名
int width = 256; // 图像宽
int height = 256; // 图像高
int rgbFileSize; // RGB图像总字节数
int yuvFileSize; // YUV图像总字节数
unsigned char* rgbOriBuffer = NULL; // 原RGB图像缓冲区
unsigned char* yBuffer = NULL; // Y分量缓冲区
unsigned char* uBuffer = NULL; // U分量缓冲区
unsigned char* vBuffer = NULL; // V分量缓冲区
unsigned char* rgbRecBuffer = NULL; // 复原RGB图像缓冲区
// 打开文件
if (fopen_s(&rgbOriFilePtr, rgbOriFileName, "rb") == 0)
{
cout << "Successfully opened " << rgbOriFileName << "." << endl;
}
else
{
cout << "Failed to open " << rgbOriFileName << "." << endl;
exit(0);
}
if (fopen_s(&yuvFilePtr, yuvFileName, "wb+") == 0)
{
cout << "Successfully opened " << yuvFileName << "." << endl;
}
else
{
cout << "Failed to open " << yuvFileName << "." << endl;
exit(0);
}
if (fopen_s(&rgbRecFilePtr, rgbRecFileName, "wb") == 0)
{
cout << "Successfully opened " << rgbRecFileName << "." << endl;
}
else
{
cout << "Failed to open " << rgbRecFileName << "." << endl;
exit(0);
}
// 计算原RGB图像总字节数
fseek(rgbOriFilePtr, 0L, SEEK_END);
rgbFileSize = ftell(rgbOriFilePtr);
rewind(rgbOriFilePtr);
cout << "The space that " << rgbOriFileName << " accounts for is " << rgbFileSize << " Bytes = " << rgbFileSize / 1024 << " kB." << endl;
yuvFileSize = rgbFileSize / 2;
// 建立缓冲区
rgbOriBuffer = new unsigned char[rgbFileSize];
yBuffer = new unsigned char[rgbFileSize / 3];
uBuffer = new unsigned char[rgbFileSize / 12]; // 4:2:0格式
vBuffer = new unsigned char[rgbFileSize / 12];
rgbRecBuffer = new unsigned char[rgbFileSize];
fread(rgbOriBuffer, sizeof(unsigned char), rgbFileSize, rgbOriFilePtr); // RGB图像读入缓冲区
rgb2yuv(yuvFilePtr, rgbFileSize, width, height, rgbOriBuffer, yBuffer, uBuffer, vBuffer);
yuv2rgb(rgbRecFilePtr, yuvFileSize, width, height, yBuffer, uBuffer, vBuffer, rgbRecBuffer);
errorData(yuvFileSize, rgbRecBuffer, argv);
delete[]rgbOriBuffer;
delete[]yBuffer;
delete[]uBuffer;
delete[]vBuffer;
delete[]rgbRecBuffer;
fclose(rgbOriFilePtr);
fclose(yuvFilePtr);
fclose(rgbRecFilePtr);
}
实验结果与误差分析
以上三张图分别是原RGB图像、通过RGB转换的YUV图像和通过YUV复原的RGB图像。对比第1、3张图,几乎通过肉眼分辨不出差别。为了量化误差,在程序中,利用errorData
函数计算了两张RGB图像各像素的三个分量的误差,并输出到了csv文件中。
由于在C++中进行数据分析与可视化并不方便,考虑到数据量较大,因而采用R进行分析。
在R中分别作出boxplot和直方图:
errorData <- read.csv("errorData.csv")
b.error <- errorData[, 2]
g.error <- errorData[, 3]
r.error <- errorData[, 4]
boxplot(r.error, g.error, b.error,
horizontal = TRUE,
names = c("R Error", "G Error", "B Error"),
col = c("coral2", "palegreen1", "skyblue1"))
hist(r.error, freq = FALSE,
xlab = "Pixel", ylab = "Frequency of R Error",
col = "coral2")
hist(g.error, freq = FALSE,
xlab = "Pixel", ylab = "Frequency of G Error",
col = "palegreen1")
hist(b.error, freq = FALSE,
xlab = "Pixel", ylab = "Frequency of B Error",
col = "skyblue1")
可以再求出各分量误差的Empirical CDF:
> ecdf.r.error ecdf.g.error ecdf.b.error ecdf.r.error(5)
[1] 0.9351196
> ecdf.g.error(5)
[1] 0.9855804
> ecdf.b.error(5)
[1] 0.8774567
图表显示,该色度空间的转换不能做到100%的准确。误差来源可能有:
由于从4:4:4的RGB图像转换为4:2:0的YUV图像时,舍弃掉了3/4的色度信息,因而在还原为YUV文件时是无法还原出舍弃部分的色度信息的; 在进行色彩空间转换的公式推导时,使用了移位运算代替了除法运算,并且在计算过程中存在多次四舍五入; 在YUV向RGB的转换时,存在部分数据溢出。但R、G、B分量分别有93.5%、98.6%、87.8%的像素误差小于等于5,因而该算法的色彩空间转换的误差并不大,效果是可以接受的;由于人眼对色度的敏感度远高于对亮度的敏感度,误差也在人眼的分辨能力之外。
实验中需要注意的问题在进行RGB和YUV的转换时,要特别留意数组下标,保证不会越界;
在将YUV还原为RGB时,可能会出现数据溢出(如下图),因而三个分量都需要分别判断,若有溢出,要置为0或255;
r = (yuv298[y] + yuv411[v] - 57344) >> 8; // 还原的RGB图像的R分量
if (r 255)
r = 255;
g = (yuv298[y] - yuv101[u] - yuv211[v] + 34739) >> 8; // 还原的RGB图像的G分量
if (g 255)
g = 255;
b = (yuv298[y] + yuv519[u] - 71117) >> 8; // 还原的RGB图像的B分量
if (b 255)
b = 255;
r = (yuv298[y] + yuv411[v] - 57344) >> 8; // 还原的RGB图像的R分量
if (r 255)
r = 255;
g = (yuv298[y] - yuv101[u] - yuv211[v] + 34739) >> 8; // 还原的RGB图像的G分量
if (g 255)
g = 255;
b = (yuv298[y] + yuv519[u] - 71117) >> 8; // 还原的RGB图像的B分量
if (b 255)
b = 255;
在转换过程中,中间变量要使用int
型(4字节)而不能使用unsigned char
型(只有1字节),为数据溢出留出空间。