RGB与YUV图像视频格式的相互转换(转)

摘要:
通过本文您可以学习到如何把图像转换为电视视频格式,YUV图像视频主要应用于电视和游戏视频显示。笔者以一张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,V0,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;
}
 
结束语:
通过阅读本文,希望能让您理解一些RGB及YUV转换的细节,其实在一些开发包 里已经提供了一些函数实现转换,本文主要是说明转换原理,没有对代码做优化,也没有对读取和写入格式做一些异常处理,希望这不会妨碍您对本文的理解。 YUV的格式非常多且复杂,本文只以UYVY为例,希望能起到抛砖引玉的作用。写本文之前笔者阅读了不少相关的文章不能一一列出,在此对他们无私的把自己 的知识拿出来共享表示感谢。本文所带源码您可以直接到我的个人网站下载 http://www.cgsir.com 。另外本人专门写了一个AVI转换为YUV视频格式的工具,如果您有需要,可以直接到我个人网站下载或直接与我联系。
 
 
参考文章:
Video Rendering with 8-Bit YUV Formats
关于YUV和RGB图像格式的问题
….
 
编程环境:Visual C++6.0 & MFC
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值