RGB與YUV色彩空間的相互轉換

RGB與YUV色彩空間相互轉換

原理

RGB與YUV空間的對應關係

根據電視原理的相關知識可知,RGB與的YUV對應關係爲:
{Y=0.299 R+0.587 G+0.114 BU=0.1684 R0.3316 G+0.5 B=0.564 (BY)V=0.5 R0.4187 G0.0813 B=0.713 (RY)(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}
其中,爲了使色差信號的動態範圍控制在[-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}
可知
{Y=int{235160.5(0.5)Y+16}U=int{2401610U+128}V=int{2401610V+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}
其中,

  • int{\rm int}表示向下取整;
  • YY'UU'VV'爲數字量化電平,YYUUVV爲歸一化的模擬電平(Y[0,1]Y\in [0,1]U,V[0.5,0.5]U,V\in [-0.5,0.5]);
  • 考慮到色差信號有負值,需要將原來的0值對應到128,故加上128。

由於讀取的RGB文件已經進行了8 bit的量化(RGB三個分量範圍均爲0—255),所以要對公式(2)(2)進行修正,先將YY映射到-0.5—0.5,UUVV映射到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}
帶入(1)(1)式,得:
{Y=66R+129G+25B255+16U=38R74G+112B255+128V=112R94G18B255+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}
爲了提高計算機的計算效率且不會造成過大的誤差,在程序中使用>> 8來代替除以255的計算。

(5)(5)式寫爲矩陣形式:
[YUV]=1255[661292538741121129418][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}
並記A=[661292538741121129418]\boldsymbol A= \begin{bmatrix} 66 & 129 & 25 \\ -38 & -74 & 112 \\ 112 & -94 & -18 \end{bmatrix}

反解,得:
[RGB]=255AT[Y16U16V128](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}
由於A1\boldsymbol A^{-1}數量級較小,直接使用會造成較大的計算誤差,因而轉化爲
[RGB]=2552(1255AT)[Y16U16V128](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}
整理得:

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 argcchar* argv[]。雖然很多情況下是缺省的,但在例如涉及文件的操作中,使用命令行參數可以爲編程提供一定的便利。

設置方法如下:在Visual Studio中,依次點擊菜單欄中的項目→項目屬性,在項目屬性頁的配置屬性菜單下,點擊“調試”。通過瀏覽文件夾的方式設置工作目錄,並在命令參數中輸入n個字符串(以空格分隔)。


命令行參數的設置

這些字符串將會自動傳遞給argv,作爲其第1n個元素(第0個元素爲"項目名.exe"),而argc的值爲n+1

源代碼

declarations.h

#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 <iostream>
#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 < pxNum; i++)	// i爲圖像像素序號
	{
		unsigned char r = rgbBuf[3 * i + 2];	// RGB圖像第i個像素的R分量
		unsigned char g = rgbBuf[3 * i + 1];	// RGB圖像第i個像素的G分量
		unsigned char b = rgbBuf[3 * i];		// RGB圖像第i個像素的B分量
		//yBuf[i] = ((66 * r + 129 * g + 25 * b) >> 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 <iostream>
#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 < pxNum; i++)
	{
		// 中間變量均使用int型,以保證足夠的精度,防止溢出
		int y = yBuf[i];		// YUV圖像第i個像素的Y分量
		int u = uBuf444[i];	// YUV圖像第i個像素的U分量(4:4:4)
		int v = vBuf444[i];	// YUV圖像第i個像素的V分量(4:4:4)
		int r;
		int g;
		int b;

		yuvLookupTable();
		//r = (298 * y + 411 * v - 57344) >> 8;	// 還原的RGB圖像的R分量
		r = (yuv298[y] + yuv411[v] - 57344) >> 8;	// 還原的RGB圖像的R分量
		if (r < 0)	
			r = 0;	// 修正
		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 < 0)
			g = 0;
		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 < 0)
			b = 0;
		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 <iostream>
#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 <iostream>
#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);
}

實驗結果與誤差分析


down.rgb

down_transformed.yuv

down_recoverd.rgb

以上三張圖分別是原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(r.error)

> ecdf.g.error <- ecdf(g.error)

> ecdf.b.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,因而該算法的色彩空間轉換的誤差並不大,效果是可以接受的;由於人眼對色度的敏感度遠高於對亮度的敏感度,誤差也在人眼的分辨能力之外。

實驗中需要注意的問題

  1. 在進行RGB和YUV的轉換時,要特別留意數組下標,保證不會越界;

  2. 在將YUV還原爲RGB時,可能會出現數據溢出(如下圖),因而三個分量都需要分別判斷,若有溢出,要置爲0或255;


  3. down_recovered.rgb(數據有溢出)
		r = (yuv298[y] + yuv411[v] - 57344) >> 8;	// 還原的RGB圖像的R分量
		if (r < 0)	
			r = 0;	// 修正
		if (r > 255)
			r = 255;

		g = (yuv298[y] - yuv101[u] - yuv211[v] + 34739) >> 8;	// 還原的RGB圖像的G分量
		if (g < 0)
			g = 0;
		if (g > 255)
			g = 255;

		b = (yuv298[y] + yuv519[u] - 71117) >> 8;	// 還原的RGB圖像的B分量
		if (b < 0)
			b = 0;
		if (b > 255)
			b = 255;
		r = (yuv298[y] + yuv411[v] - 57344) >> 8;	// 還原的RGB圖像的R分量
		if (r < 0)	
			r = 0;	// 修正
		if (r > 255)
			r = 255;

		g = (yuv298[y] - yuv101[u] - yuv211[v] + 34739) >> 8;	// 還原的RGB圖像的G分量
		if (g < 0)
			g = 0;
		if (g > 255)
			g = 255;

		b = (yuv298[y] + yuv519[u] - 71117) >> 8;	// 還原的RGB圖像的B分量
		if (b < 0)
			b = 0;
		if (b > 255)
			b = 255;
  1. 在轉換過程中,中間變量要使用int型(4字節)而不能使用unsigned char型(只有1字節),爲數據溢出留出空間。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章