您的位置:首页 > 其它

YUV与RGB之间的转换

2009-05-15 16:23 495 查看
通过本文您可以学习到如何把图像转换为电视视频格式,笔者以一张24位BMP图像为例实现RGB与YUV相互转换。如果您对图像转换成电视场制的视频格式有疑惑,相信本文能使您了解更多关于图像与视频格式转换的细节。

目录:
显示器图像显示概述
电视图像显示概述
RGB介绍
YUV介绍
隔行读取BMP
RGB转YUV
YUV转RGB
结束语

显示器图像显示概述:
我们知道普通彩色CRT显示器内部有三支电子枪,电子枪去激活显示器屏幕的荧光粉,三种荧光粉发射出的光生成一个像素位置的颜色点,这就是我们人眼能看到的一个像素。每个像素对应红、绿、蓝(R、G、B)三个强度等级,每个像素占用24位,可以显示近1700 万种颜色,这就是我们所说的真彩色。
普通彩色CRT显示器是基于电视技术的光栅扫描,电子束一次扫描一行,从顶到底依次扫描,整个屏幕扫描一次(我们称它为1帧),电子束扫描完一帧后回到最初位置进行下一次扫描。

电视图像显示概述:
电视显示原理与CRT相似,不过采用的是隔行扫描,我国的广播电视采用的是625行隔行扫描方式。隔行扫描是将一帧图像分两次(场)扫描。第一场先扫出1、3、5、7…等奇数行光栅,第二场扫出2、4、6、8…等偶数行光栅。通常将扫奇数行的场叫奇数场(也称上场),扫偶数行的场叫偶数场(也称下场)。为什么电视会选择隔行扫描,这是因为会使显示运动图像更平滑。下面两图为一帧图像的上场和下场的扫描过程。



(图1 上场扫描)



(图2 下场扫描)

常见的电视的制式有三种:NTSC、PAL、SECAM,我国的广播电视采用PAL制式,我国电视制式的帧频只有50HZ和我们日常使用的电流频率一样,PAL帧频为25fps,在文章后面我会以一张720x576的图像转换为720x 576 PAL隔行扫描的电视场视频格式作详细描述。

RGB介绍:
在记录计算机图像时,最常见的是采用RGB(红、绿,蓝)颜色分量来保存颜色信息,例如非压缩的24位的BMP图像就采用RGB空间来保存图像。一个像素24位,每8位保存一种颜色强度(0-255),例如红色保存为 0xFF0000。

YUV介绍:
YUV是被欧洲电视系统所采用的一种颜色编码方法,我国广播电视也普遍采用这类方法。其中“Y”表示明亮度(Luminance或Luma),也就是灰阶值;而“U”和“V”表示的则是色度(Chrominance或Chroma)。彩色电视采用YUV空间正是为了用亮度信号Y解决彩色电视机与黑白电视机的兼容问题,使黑白电视机也能接收彩色电视信号。

隔行读取BMP:
下面我说明如何隔行读取BMP图像,为什么我以BMP图像来作演示,因为BMP可以说是最简单的一种图像格式,最容易用它说明原理,那公为什么要用BMP来演示隔行读取呢,因为要实现RGB转电视场制图像,首先就要知识如何隔行读取。
BMP图像颜色信息的保存顺序是由左到右,由下往上,您可以执行一下附带程序的 (功能菜单->读取RGB) 看到图像的读取和显示过程。代码首先依次显示奇数行像素,如(1,3,5,7,9….行),完成后再依次显示偶数行像素,代码实现如下:

// 隔行显示BMP
void CRGB2YUVView::OnReadBmp()
{
// TODO: Add your command handler code here
CDC *pDC = GetDC();

CRect rect;
CBrush brush(RGB(128,128,128));
GetClientRect(&rect);
pDC->FillRect(&rect, &brush);

BITMAPFILEHEADER bmfh;
BITMAPINFOHEADER bmih;

char strFileName[MAX_PATH]="720bmp.bmp";
CFile* f;
f = new CFile();
f->Open(strFileName, CFile::modeRead);
f->SeekToBegin();
f->Read(&bmfh, sizeof(bmfh));
f->Read(&bmih, sizeof(bmih));

// 分配图片像素内存
RGBTRIPLE *rgb;
rgb = new RGBTRIPLE[bmih.biWidth*bmih.biHeight];

f->SeekToBegin();
f->Seek(54,CFile::begin); // BMP 54个字节之后的是像素数据
f->Read(rgb, bmih.biWidth * bmih.biHeight * 3);	 // 这里只读24位RGB(r,g,b)图像

// 显示上场 (奇数行组成的奇数场)
for (int i = 0; i<bmih.biHeight; i++) {
for (int j = 0; j<bmih.biWidth; j++) {
if(!(i%2))
pDC->SetPixel(j, bmih.biHeight-i,
RGB(rgb[i*bmih.biWidth+j].rgbtRed,
rgb[i*bmih.biWidth+j].rgbtGreen,rgb[i*bmih.biWidth+j].rgbtBlue));
for (int k=0; k<1000; k++) ;  //延时
}
}

Sleep(500);

// 显示下场 (偶数行组成的偶数场)
for (int i_ = 0; i_<bmih.biHeight; i_++) {
for (int j_ = 0; j_<bmih.biWidth; j_++) {
if(i_%2)
pDC->SetPixel(j_, bmih.biHeight-i_,
RGB(rgb[i_*bmih.biWidth+j_].rgbtRed,
rgb[i_*bmih.biWidth+j_].rgbtGreen,
rgb[i_*bmih.biWidth+j_].rgbtBlue));
for (int k=0; k<1000; k++) ;  //延时
}
}

// 显示24位BMP信息
LONG dwWidth = bmih.biWidth;
LONG dwHeight = bmih.biHeight;
WORD wBitCount = bmih.biBitCount;
char buffer[80];
sprintf(buffer,"图像宽为:%ld 高为:%ld 像数位数:%d", dwWidth, dwHeight, wBitCount);
MessageBox(buffer, "每个像素的位数", MB_OK | MB_ICONINFORMATION);

f->Close();
delete f;
delete rgb;
}

RGB转YUV

在整个视频行业中,定义了很多 YUV 格式,我以UYVY格式标准来说明,4:2:2 格式UYVY每像素占16 位,UYVY字节顺序如下图:



(图3 UYVY字节顺序)

其中第一个字节为U0,每二个字节为Y0,依次排列如下:
[U0,Y0,U1,Y1] [U1,Y2,V1,Y3] [U2,Y4,V2,Y5] ……
经过仔细分析,我们要实现RGB转YUV格式的话,一个像素的RGB占用三个节,而UYVY每像素占用两个字节,我演示直接把UYVY字节信息保存到*.pal格式中(这是我自己写来测试用的^_^),*.pal格式中,先保存上场像素,接着保存下场像素,如果是720x576的一张图像转换为YUV格式并保存的话,文件大小应该是829,440字节(720*576*2)。您可以执行本文附带的程序 (功能菜单->转换并写入YUV两场) 查看转换过程。

RGB转UYVY公式如下:
公式:(RGB => YCbCr)
Y = 0.257R′ + 0.504G′ + 0.098B′ + 16
Cb = -0.148R′ - 0.291G′ + 0.439B′ + 128
Cr = 0.439R′ - 0.368G′ - 0.071B′ + 128

代码实现:

// RGB转换为YUV
void CRGB2YUVView::RGB2YUV(byte *pRGB, byte *pYUV)
{
byte r,g,b;
r = *pRGB; pRGB++;
g = *pRGB; pRGB++;
b = *pRGB;

*pYUV = static_cast<byte>(0.257*r + 0.504*g + 0.098*b + 16);    pYUV++;   // y
*pYUV = static_cast<byte>(-0.148*r - 0.291*g + 0.439*b + 128);  pYUV++;   // u
*pYUV = static_cast<byte>(0.439*r - 0.368*g - 0.071*b + 128);             // v
}

像素转换实现:

// 转换RGB
void CRGB2YUVView::OnConvertPAL()
{
CDC *pDC = GetDC();
CRect rect;
CBrush brush(RGB(128,128,128));
GetClientRect(&rect);
pDC->FillRect(&rect, &brush);

// PAL 720x576 : 中国的电视标准为PAL制
int CurrentXRes = 720;
int CurrentYRes = 576;
int size        = CurrentXRes * CurrentYRes;

// 分配内存
byte *Video_Field0 = (byte*)malloc(CurrentXRes*CurrentYRes);
byte *Video_Field1 = (byte*)malloc(CurrentXRes*CurrentYRes);

// 保存内存指针
byte *Video_Field0_ = Video_Field0;
byte *Video_Field1_ = Video_Field1;

byte yuv_y0, yuv_u0, yuv_v0, yuv_v1;  // {y0, u0, v0, v1};
byte bufRGB[3];  // 临时保存{R,G,B}
byte bufYUV[3];  // 临时保存{Y,U,V}

// 初始化数组空间
ZeroMemory(bufRGB, sizeof(byte)*3);
ZeroMemory(bufYUV, sizeof(byte)*3);

// 初始化内存
ZeroMemory(Video_Field0, CurrentXRes*CurrentYRes);
ZeroMemory(Video_Field1, CurrentXRes*CurrentYRes);

// BMP 位图操作
BITMAPFILEHEADER bmfh;
BITMAPINFOHEADER bmih;

char strFileName[MAX_PATH]="720bmp.bmp";
CFile* f;
f = new CFile();
f->Open(strFileName, CFile::modeRead);
f->SeekToBegin();
f->Read(&bmfh, sizeof(bmfh));
f->Read(&bmih, sizeof(bmih));

// 分配图片像素内存
RGBTRIPLE *rgb;
rgb = new RGBTRIPLE[bmih.biWidth*bmih.biHeight];

f->SeekToBegin();
f->Seek(54,CFile::begin);  // BMP 54个字节之后的是位像素数据
f->Read(rgb, bmih.biWidth * bmih.biHeight * 3);	 // 这里只读24位RGB(r,g,b)图像

// 上场  (1,3,5,7...行)
for (int i = bmih.biHeight-1; i>=0; i--) {
for (int j = 0; j<bmih.biWidth; j++) {
if(!(i%2)==0)
{
bufRGB[0] = rgb[i*bmih.biWidth+j].rgbtRed;   //	R
bufRGB[1] = rgb[i*bmih.biWidth+j].rgbtGreen; // G
bufRGB[2] = rgb[i*bmih.biWidth+j].rgbtBlue;  // B

// RGB转换为YUV
RGB2YUV(bufRGB,bufYUV);
yuv_y0 = bufYUV[0];   // y
yuv_u0 = bufYUV[1];   // u
yuv_v0 = bufYUV[2];   // v

for (int k=0; k<1000; k++) ;  //延时
// 视图中显示
pDC->SetPixel(j, (bmih.biHeight-1)-i, RGB(bufRGB[0], bufRGB[1], bufRGB[2]));

// UYVY标准  [U0 Y0 V0 Y1] [U1 Y2 V1 Y3] [U2 Y4 V2 Y5]
// 每像素点两个字节,[内]为四个字节
if ((j%2)==0)
{
*Video_Field0 = yuv_u0;
Video_Field0++;
yuv_v1 = yuv_v0;   // v保存起来供下一字节使用
}
else
{
*Video_Field0 = yuv_v1;
Video_Field0++;
}
*Video_Field0 = yuv_y0;
Video_Field0++;
}// end if i%2
}
}

// 下场 (2,4,6,8...行)
for (int i_ = bmih.biHeight-1; i_>=0; i_--) {
for (int j_ = 0; j_<bmih.biWidth; j_++) {
if((i_%2)==0)
{
bufRGB[0] = rgb[i_*bmih.biWidth+j_].rgbtRed;   //	R
bufRGB[1] = rgb[i_*bmih.biWidth+j_].rgbtGreen; // G
bufRGB[2] = rgb[i_*bmih.biWidth+j_].rgbtBlue;  // B

// RGB转换为YUV
RGB2YUV(bufRGB,bufYUV);
yuv_y0 = bufYUV[0];   // y
yuv_u0 = bufYUV[1];   // u
yuv_v0 = bufYUV[2];   // v

for (int k=0; k<1000; k++) ;  //延时
// 视图中显示
pDC->SetPixel(j_, (bmih.biHeight-1)-i_, RGB(bufRGB[0], bufRGB[1], bufRGB[2]));

// UYVY标准  [U0 Y0 V0 Y1] [U1 Y2 V1 Y3] [U2 Y4 V2 Y5]
// 每像素点两个字节,[内]为四个字节
if ((j_%2)==0)
{
*Video_Field1 = yuv_u0;
Video_Field1++;
yuv_v1 = yuv_v0;   // v保存起来供下一字节使用
}
else
{
*Video_Field1 = yuv_v1;
Video_Field1++;
}
*Video_Field1 = yuv_y0;
Video_Field1++;
}
}
}

// 关闭BMP位图文件
f->Close();
WriteYUV(Video_Field0_, Video_Field1_, size);

// 释放内存
free( Video_Field0_ );
free( Video_Field1_ );
delete f;
delete rgb;
}

YUV转RGB

关于YUV转换为RGB公式,我直接使用一篇文章提供的公式,经过思考,我发觉要想实现准确无误的把YUV转换为原有的RGB图像很难实现,因为我从UYVY的字节顺序来分析没有找到反变换的方法(您找到了记得告诉我哟: liyingjiang@21cn.com ),例如我做了一个简单的测试:假设有六个像素的UYVY格式,要把这12个字节的UYVY要转换回18个字节的RGB,分析如下:

12个字节的UYVY排列方式:
[U0 Y0 V0 Y1] [U1 Y2 V1 Y3] [U2 Y4 V2 Y5]
完全转换为18个字节的RGB所需的UYVY字节排列如下:
[Y0 U0 V0] [Y1 U1 V1] [Y2 U2 V2] [Y3 U3 V3] [Y4 U4 V4] [Y5 U5 V5]
我们可以看到,12个字节的UYVY无法实现,缺少U3 V3 U4 V4。于是我抛开准确无误地把UYVY转换回RGB的想法,直接使用最近的UV来执行转换,结果发觉转换回来的RGB图像用肉眼根本分辩不出原有RGB图像与反变换回来的RGB图像差别,您可以执行本文附带的程序 (功能菜单->读取YUV并显示) 查看效果,下面是反变换公式和代码的实现:

//  反变换公式
R= 1.0Y + 0 +1.402(V-128)
G= 1.0Y - 0.34413 (U-128)-0.71414(V-128)
B= 1.0Y + 1.772 (U-128)+0

代码实现:

void CRGB2YUVView::YUV2RGB(byte *pRGB, byte *pYUV)
{
byte y, u, v;
y = *pYUV; pYUV++;
u = *pYUV; pYUV++;
v = *pYUV;

*pRGB = static_cast<byte>(1.0*y + 8 + 1.402*(v-128));    pRGB++;                 // r
*pRGB = static_cast<byte>(1.0*y - 0.34413*(u-128) - 0.71414*(v-128));  pRGB++;   // g
*pRGB = static_cast<byte>(1.0*y + 1.772*(u-128) + 0);                            // b
}

// 读取PAL文件转换为RGB并显示
void CRGB2YUVView::OnReadPAL()
{
// TODO: Add your command handler code here
CDC *pDC = GetDC();
CRect rect;
CBrush brush(RGB(128,128,128));
GetClientRect(&rect);
pDC->FillRect(&rect, &brush);

// PAL 720x576 : 中国的电视标准为PAL制
int CurrentXRes = 720;
int CurrentYRes = 576;
int size        = CurrentXRes * CurrentYRes;

// 分配内存
byte *Video_Field0 = (byte*)malloc(CurrentXRes*CurrentYRes);
byte *Video_Field1 = (byte*)malloc(CurrentXRes*CurrentYRes);

// 保存内存指针
byte *Video_Field0_ = Video_Field0;
byte *Video_Field1_ = Video_Field1;

// 初始化内存
ZeroMemory(Video_Field0, CurrentXRes*CurrentYRes);
ZeroMemory(Video_Field1, CurrentXRes*CurrentYRes);

byte yuv_y0, yuv_u0, yuv_v0; // yuv_v1;  // {y0, u0, v0, v1};
byte r, g, b;
byte bufRGB[3];  // 临时保存{R,G,B}
byte bufYUV[3];  // 临时保存{Y,U,V}

// 初始化数组空间
memset(bufRGB,0, sizeof(byte)*3);
memset(bufYUV,0, sizeof(byte)*3);

char strFileName[MAX_PATH]="720bmp.pal";

// 分配图片像素内存
RGBTRIPLE *rgb;
rgb = new RGBTRIPLE[CurrentXRes*CurrentYRes];

memset(rgb,0, sizeof(RGBTRIPLE)*CurrentXRes*CurrentYRes); // 初始化内存空间

CFile* f;
f = new CFile();
f->Open(strFileName, CFile::modeRead);
f->SeekToBegin();
f->Read(Video_Field0, CurrentXRes*CurrentYRes);
f->Read(Video_Field1, CurrentXRes*CurrentYRes);

// 上场  (1,3,5,7...行)
for ( int i = CurrentYRes-1; i>=0; i--) {
for ( int j = 0; j<CurrentXRes; j++) {
if(!(i%2)==0)
{
// UYVY标准  [U0 Y0 V0 Y1] [U1 Y2 V1 Y3] [U2 Y4 V2 Y5]
// 每像素点两个字节,[内]为四个字节
if ((j%2)==0)
{
yuv_u0 = *Video_Field0;
Video_Field0++;
}
else
{
yuv_v0 = *Video_Field0;
Video_Field0++;
}
yuv_y0 = *Video_Field0;
Video_Field0++;

bufYUV[0] = yuv_y0;  //	Y
bufYUV[1] = yuv_u0;  // U
bufYUV[2] = yuv_v0;  // V

// RGB转换为YUV
YUV2RGB(bufRGB,bufYUV);
r = bufRGB[0];   // y
g = bufRGB[1];   // u
b = bufRGB[2];   // v
if (r>255) r=255; if (r<0) r=0;
if (g>255) g=255; if (g<0) g=0;
if (b>255) b=255; if (b<0) b=0;

for (int k=0; k<1000; k++) ;  //延时
// 视图中显示
pDC->SetPixel(j, CurrentYRes-1-i, RGB(r, g, b));

}// end if i%2
}
}

// 下场 (2,4,6,8...行)
for ( int i_ = CurrentYRes-1; i_>=0; i_--) {
for ( int j_ = 0; j_<CurrentXRes; j_++) {
if((i_%2)==0)
{
// UYVY标准  [U0 Y0 V0 Y1] [U1 Y2 V1 Y3] [U2 Y4 V2 Y5]
// 每像素点两个字节,[内]为四个字节
if ((j_%2)==0)
{
yuv_u0 = *Video_Field1;
Video_Field1++;
}
else
{
yuv_v0 = *Video_Field1;
Video_Field1++;
}
yuv_y0 = *Video_Field1;
Video_Field1++;

bufYUV[0] = yuv_y0;  //	Y
bufYUV[1] = yuv_u0;  // U
bufYUV[2] = yuv_v0;  // V

// RGB转换为YUV
YUV2RGB(bufRGB,bufYUV);
r = bufRGB[0];   // y
g = bufRGB[1];   // u
b = bufRGB[2];   // v
if (r>255) r=255; if (r<0) r=0;
if (g>255) g=255; if (g<0) g=0;
if (b>255) b=255; if (b<0) b=0;

for (int k=0; k<1000; k++) ;  //延时
// 视图中显示
pDC->SetPixel(j_, CurrentYRes-1-i_, RGB(r, g, b));
}
}
}

// 提示完成
char buffer[80];
sprintf(buffer,"完成读取PAL文件:%s ", strFileName);
MessageBox(buffer, "提示信息", MB_OK | MB_ICONINFORMATION);

// 关闭PAL电视场文件
f->Close();

// 释放内存
free( Video_Field0_ );
free( Video_Field1_ );
delete f;
delete rgb;
}

备注:以下是微软更高效的转换代码

http://msdn.microsoft.com/en-us/library/ms893078.aspx

Converting Between YUV and RGBIt is frequently necessary to convert between YUV pixel formats (used by the JPEG and MPEG compression methods) and RGB format (used by many hardware manufacturers.) The following formulas show how to compute a pixel's value in one format from the pixel value in the other format.YUV format allows for higher compression rates without a proportionately high loss of data, as the U and V portions can be highly compressed and computed from the non- or lowly-compressed Y portion.Computer RGB888, or full-scale RGB, uses 8 bits each for the red, green, and blue channels. Black is represented by R = G = B = 0, and white is represented by R = G = B = 255. The 4:4:4 YUV format uses 8 bits each for the Y, U, and V channels. Converting RGB888 to YUV The following formulas define the conversion from RGB to YUV: Copy CodeY = ( (  66 * R + 129 * G +  25 * B + 128) >> 8) +  16
U = ( ( -38 * R -  74 * G + 112 * B + 128) >> 8) + 128
V = ( ( 112 * R -  94 * G -  18 * B + 128) >> 8) + 128

These formulas produce 8-bit results using coefficients that require no more than 8 bits of (unsigned) precision. Intermediate results require up to 16 bits of precision.
Converting 8-bit YUV to RGB888
The following coefficients are used in conversion process:

C = Y - 16
D = U - 128
E = V - 128

Using the previous coefficients and noting that clip() denotes clipping a value to the range of 0 to 255, the following formulas provide the conversion from YUV to RGB:

R = clip(( 298 * C           + 409 * E + 128) >> 8)
G = clip(( 298 * C - 100 * D - 208 * E + 128) >> 8)
B = clip(( 298 * C + 516 * D           + 128) >> 8)

These formulas use some coefficients that require more than 8 bits of precision to produce each 8-bit result, and intermediate results require more than 16 bits of precision.
Note   All units range from 0 (zero) to 1.0 (one). In DirectDraw, they range from 0 to 255. Overflow and underflow can (and does) occur, and the results must be saturated.
To convert 4:2:0 or 4:2:2 YUV to RGB, convert the YUV data to 4:4:4 YUV, and then convert from 4:4:4 YUV to RGB.
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: