顯示器圖像顯示概述:
我們知道普通彩色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
RGB轉YUV
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;
}
在整個視頻行業中,定義了很多 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)+0void 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