程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> VC >> 關於VC++ >> RGB與YUV圖像視頻格式的相互轉換

RGB與YUV圖像視頻格式的相互轉換

編輯:關於VC++

顯示器圖像顯示概述:

我們知道普通彩色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的字節順序來分析沒有找到反變換的方法(您找到 了記得告訴我喲: [email protected] ),例如我做了一個簡單的測試:假設有六個 像素的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視頻格式的工具,如果您有需要,可以直接到我個 人網站下載或直接與我聯系。

編程環境:Visual C++6.0 & MFC

關於作者:李英江目前就職於 湖南三辰 卡通集團,是一名普通的程序員,可以通過以下方式與我取得聯系。

下載源代碼:http://www.vckbase.com/code/downcode.asp?id=3076

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved