【数字图像处理】基于GDI的入门级YUV播放器设计

来源:互联网 发布:snmpwalk用的什么端口 编辑:程序博客网 时间:2024/09/21 09:21

简介

数字图像处理的过程中,YUV文件是比较常见的视频源数据。本文根据的项目的需要,设计了一款入门级的YUV播放器。该播放器可以支持UYVY、I420这两种YUV文件,通过YUV2RGB将YUV文件转换成DIB位图,从而进行播放显示。


YUV采样格式

和RGB颜色空间相比,YUV颜色空间充分利用了人眼的特性,人的眼睛对亮度的敏感度远大于色度。在保证基本画质的前提下,可以对一幅画面的色度分量进行删减。下面三张图片是常见的三种YUV采样方式,YUV4:4:4、YUV4:2:2、YUV4:2:0。其中,YUV4:4:4是一种无压缩的采样方式,每一个Y分量对应一组UV分量;YUV4:2:2的采样方式丢弃了一半的色度分量,每两个Y分量对应一组UV分量;YUV4:2:0的采样方式对齐了四分之三的色度分量,每四个Y分量对应一组UV分量。





YUV文件格式之UYVY和I420

目前的项目中用到了UYVY和I420这两种YUV文件,为了方便自己使用,因而自己写了一个入门级的YUV播放器。下面以一个8x4的图片来说明UYVY和I420的文件存储格式。

<1> UYVY文件

UYVY文件是YUV4:2:2采样格式的一种存储格式,具体的地址对应关系如下图所示,U、Y、V、Y依次按顺序存储在文件中。

<2> I420文件

I420文件是YUV4:2:0采样格式的一种存储格式,具体的地址对应关系如下图所示,和UYVY文件稍有不同。I420文件先存储Y块、然后是U块和V块。每个Y/U/V块块内的的像素依次存储在文件中。


色域转换

YUV文件方便了数字图像的处理,但是不能直接用于显示,在基于GDI的播放器中,我们仍然需要先将YUV转换为RGB,然后给它按上一个DIB的文件头,然后才能显示。

本文采用的转换公式如下:

R =1.164*(Y-16) + 1.596*(V-128)

G =1.164*(Y-16) - 0.813*(V-128) - 0.392*(U-128)

B =1.164*(Y-16) + 2.017*(U-128)

具体实践的时候,我又将这些浮点数扩大1024倍变为定点数,然后再做处理,经过处理后的公式如下:

R =(1192*(Y-16) + 1634*(V-128))/1024

G =(1192*(Y-16) -  833*(V-128) -401*(U-128))/1024

B =(1192*(Y-16) + 2065*(U-128))/1024

程序中所采用的代码如下:

// ++++++++++++++++++++++++++++++++++++++++++++++++++++ //// 色域转换公式(YUV->RGB)// R = 1.164*(Y-16) + 1.596*(V-128)// G = 1.164*(Y-16) - 0.813*(V-128) - 0.392*(U-128)// B = 1.164*(Y-16) + 2.017*(U-128)// ---------------------------------------------------- //// R = (1192*(Y-16) + 1634*(V-128))/1024// G = (1192*(Y-16) -  833*(V-128) - 401*(U-128))/1024// B = (1192*(Y-16) + 2065*(U-128))/1024// ++++++++++++++++++++++++++++++++++++++++++++++++++++ //void yuv2rgb(BYTE y, BYTE u, BYTE v, BYTE *r, BYTE *g, BYTE *b){int y0,u0,v0;int m0,m1,m2,m3,m4;int r0,g0,b0;y0 = y - 16;u0 = u - 128;v0 = v - 128;m0 = 1192*y0;m1 = 1634*v0;m2 = 833 *v0;m3 = 401 *u0;m4 = 2065*u0;r0 = (m0 + m1) >> 10;g0 = (m0 - m2 - m3) >> 10;b0 = (m0 + m4) >> 10;if(r0 > 255)*r = 255;else if(r0 < 0)*r = 0;else*r = r0;if(g0 > 255)*g = 255;else if(g0 < 0)*g = 0;else*g = g0;if(b0 > 255)*b = 255;else if(b0 < 0)*b = 0;else*b = b0;}

渲染函数

程序采用了一个单独的渲染函数来完成绘图的工作,如下面的代码所示,渲染函数依次完成了动态内存分配,YUV图片读取、色域转换、BITMAPINFO添加以及位图显示。这里需要注意的是BITMAPINFO的biHeight需要设置成负数,因为我们的RGB数据是按照从上到下,从左到右的方式存储的。


消息处理函数

本文所采用的消息处理函数主要完成文件的操作、帧率的控制、以及画面的渲染。其中渲染函数在WM_PAINT和WM_TIMER中都被调用。大部分时间的显示由WM_TIMER控制,这样的好处是避免了WM_PAINT调用时的窗口清除,避免了画面的闪烁。


演示效果

下图为本程序的演示效果:



源代码

完整的源代码如下,用户如果想用这段代码,需要做一些前提准备和修改。用户需要准备一个UYVY或者I420文件,并根据文件的属性修改YUV_FILE_NAMEVIDEO_WIDTHVIDEO_HEIGHTYUV_TYPE
// +FHDR-------------------------------------------------------------------------------------// 一个简单的基于GDI的YUV播放器// 作者: Jason Chen// 博客地址: http://blog.csdn.net/jiandan524// 微信公众号: 视频门徒// ------------------------------------------------------------------------------------------// 该播放器通过WINAPI直接调用GDI进行显示,具体完成如下几个功能:// <1> 根据分辨率自动生成相应大小的显示窗口// <2> 支持UYVY、I420两种文件格式的播放// <3> 完成YUV2RGB的色域转换// <4> 利用Timer控制帧率// -FHDR-------------------------------------------------------------------------------------#include <Windows.h>#include <math.h>#include <stdio.h>#include <stdlib.h>const char g_szClassName[] = "myYuvViewClass";// UYVY文件类型#define T_UYVY    0// U0 Y0 V1 Y1 U2 Y2 V3 Y3 U4 Y4 V5 Y5 U6 Y6 V7 Y7// I420文件格式#define T_I420 1// Y0  Y1  Y2  Y3  Y4  Y5  Y6  Y7// Y8  Y9  Y10 Y11 Y12 Y13 Y14 Y15// U0  U1  U2  U3// V0  V1  V2  V3//#define YUV_FILE_NAME "monkeyking-tlr1_h720p_clip.yuv"#define YUV_FILE_NAME "sunflower_720p.uyvy" // YUV文件名称#define VIDEO_WIDTH   1280                  // 视频宽度#define VIDEO_HEIGHT  720                   // 视频高度#define YUV_TYPE      T_UYVY                // YUV文件类型HDC hdc;PAINTSTRUCT ps;FILE  *yuv_file;fpos_t yuv_file_len;fpos_t yuv_file_pos;unsigned int iyuv_type;// ++++++++++++++++++++++++++++++++++++++++++++++++++++ //// 色域转换公式(YUV->RGB)// R = 1.164*(Y-16) + 1.596*(V-128)// G = 1.164*(Y-16) - 0.813*(V-128) - 0.392*(U-128)// B = 1.164*(Y-16) + 2.017*(U-128)// ---------------------------------------------------- //// R = (1192*(Y-16) + 1634*(V-128))/1024// G = (1192*(Y-16) -  833*(V-128) - 401*(U-128))/1024// B = (1192*(Y-16) + 2065*(U-128))/1024// ++++++++++++++++++++++++++++++++++++++++++++++++++++ //void yuv2rgb(BYTE y, BYTE u, BYTE v, BYTE *r, BYTE *g, BYTE *b){int y0,u0,v0;int m0,m1,m2,m3,m4;int r0,g0,b0;y0 = y - 16;u0 = u - 128;v0 = v - 128;m0 = 1192*y0;m1 = 1634*v0;m2 = 833 *v0;m3 = 401 *u0;m4 = 2065*u0;r0 = (m0 + m1) >> 10;g0 = (m0 - m2 - m3) >> 10;b0 = (m0 + m4) >> 10;if(r0 > 255)*r = 255;else if(r0 < 0)*r = 0;else*r = r0;if(g0 > 255)*g = 255;else if(g0 < 0)*g = 0;else*g = g0;if(b0 > 255)*b = 255;else if(b0 < 0)*b = 0;else*b = b0;}/* * 渲染函数 */void Render(HWND hwnd){int i,j;//BYTE *y,*u,*v;BYTE *yuv;BYTE *rgb;BYTE y,u,v;BYTE r,g,b;BITMAPINFO       dibInfo;// RGB动态分配内存rgb = (BYTE *)malloc(sizeof(BYTE)*VIDEO_WIDTH*VIDEO_HEIGHT*3);// YUV动态分配内存,根据文件格式不同而不同if(iyuv_type == T_UYVY){yuv = (BYTE *)malloc(sizeof(BYTE)*VIDEO_WIDTH*VIDEO_HEIGHT*2);fread(yuv,sizeof(BYTE),VIDEO_WIDTH*VIDEO_HEIGHT*2,yuv_file);}if(iyuv_type == T_I420){yuv = (BYTE *)malloc(sizeof(BYTE)*VIDEO_WIDTH*VIDEO_HEIGHT*1.5);fread(yuv,sizeof(BYTE),VIDEO_WIDTH*VIDEO_HEIGHT*1.5,yuv_file);}// 执行色域转换(YUV2RGB)for(i=0; i<VIDEO_HEIGHT; i++)for(j=0; j<VIDEO_WIDTH; j++){if(iyuv_type == T_UYVY){y = yuv[i*VIDEO_WIDTH*2 + j*2 + 1];u = yuv[i*VIDEO_WIDTH*2 + (j/2)*4];v = yuv[i*VIDEO_WIDTH*2 + (j/2)*4 + 2];}if(iyuv_type == T_I420){y = yuv[i*VIDEO_WIDTH + j];u = yuv[VIDEO_WIDTH*VIDEO_HEIGHT + (i>>1)*VIDEO_WIDTH/2 + (j>>1)];v = yuv[VIDEO_WIDTH*VIDEO_HEIGHT*5/4 + (i>>1)*VIDEO_WIDTH/2 + (j>>1)];}yuv2rgb(y,u,v,&r,&g,&b);rgb[i*VIDEO_WIDTH*3+j*3+2] = r;rgb[i*VIDEO_WIDTH*3+j*3+1] = g;rgb[i*VIDEO_WIDTH*3+j*3]   = b;}// 组装位图信息头dibInfo.bmiHeader.biSize= sizeof(BITMAPINFO);dibInfo.bmiHeader.biWidth= VIDEO_WIDTH;dibInfo.bmiHeader.biHeight= -VIDEO_HEIGHT;dibInfo.bmiHeader.biPlanes= 1;dibInfo.bmiHeader.biBitCount= 24;dibInfo.bmiHeader.biCompression= 0;dibInfo.bmiHeader.biSizeImage= VIDEO_WIDTH*VIDEO_HEIGHT*3;dibInfo.bmiHeader.biXPelsPerMeter= 0x0ec4;dibInfo.bmiHeader.biYPelsPerMeter= 0x0ec4;dibInfo.bmiHeader.biClrUsed= 0;dibInfo.bmiHeader.biClrImportant= 0;int nResult = StretchDIBits(hdc,0, 0,VIDEO_WIDTH, VIDEO_HEIGHT,0, 0,VIDEO_WIDTH, VIDEO_HEIGHT,rgb,&dibInfo,DIB_RGB_COLORS,SRCCOPY);// 释放内存free(rgb);free(yuv);}/* * 消息处理函数 */LRESULT CALLBACK MyWindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam){switch(msg){case WM_CREATE:// 创建窗口是打开YUV文件和定时器iyuv_type = YUV_TYPE;if((yuv_file=fopen(YUV_FILE_NAME,"rb")) == NULL){MessageBox(NULL, TEXT("YUV文件打开失败!"),TEXT("错误"),MB_ICONEXCLAMATION | MB_OK);exit(0);}fseek(yuv_file, 0, SEEK_END);fgetpos(yuv_file,&yuv_file_len);fseek(yuv_file, 0, SEEK_SET);if(iyuv_type == T_UYVY){if(yuv_file_len < VIDEO_WIDTH*VIDEO_HEIGHT*2){MessageBox(NULL,TEXT("YUV数据不足一帧!"),TEXT("错误"),MB_ICONEXCLAMATION | MB_OK);exit(0);}}if(iyuv_type == T_I420){if(yuv_file_len < VIDEO_WIDTH*VIDEO_HEIGHT*1.5){MessageBox(NULL,TEXT("YUV数据不足一帧!"),TEXT("错误"),MB_ICONEXCLAMATION | MB_OK);exit(0);}}// 设定定时器,每30ms更新一次显示区域SetTimer(hwnd,0,30,0);break;// 更新显示区域已经循环控制case WM_TIMER://InvalidateRect(hwnd,0,TRUE);hdc = GetDC(hwnd);fgetpos(yuv_file,&yuv_file_pos);if(iyuv_type == T_UYVY){if(yuv_file_len - yuv_file_pos < VIDEO_WIDTH*VIDEO_HEIGHT*2){fseek(yuv_file, 0, SEEK_SET);}}if(iyuv_type == T_I420){if(yuv_file_len - yuv_file_pos < VIDEO_WIDTH*VIDEO_HEIGHT*1.5){fseek(yuv_file, 0, SEEK_SET);}}Render(hwnd);ReleaseDC(hwnd,hdc);break;case WM_PAINT:hdc = BeginPaint(hwnd, &ps);Render(hwnd);EndPaint(hwnd, &ps);break;case WM_CLOSE:fclose(yuv_file);KillTimer(hwnd,0);DestroyWindow(hwnd);break;case WM_DESTROY:PostQuitMessage(0);break;default:return DefWindowProc(hwnd, msg, wParam, lParam);}return 0;}/* * 窗口注册函数 */void RegisterMyWindow(HINSTANCE hInstance){WNDCLASSEX wc;wc.cbSize= sizeof(WNDCLASSEX);wc.style= 0;wc.lpfnWndProc= MyWindowProc;wc.cbClsExtra   = 0;wc.cbWndExtra   = 0;wc.hInstance    = hInstance;wc.hIcon        = LoadIcon(NULL,IDI_APPLICATION);wc.hCursor      = LoadCursor(NULL, IDC_ARROW);wc.hbrBackground= (HBRUSH)(COLOR_WINDOW+1);wc.lpszMenuName = NULL;wc.lpszClassName= g_szClassName;wc.hIconSm      = LoadIcon(NULL, IDI_APPLICATION);if(!RegisterClassEx(&wc)){MessageBox(NULL, TEXT("窗口注册失败!"),TEXT("错误"),MB_ICONEXCLAMATION | MB_OK);exit(0);}}/* * 窗口创建函数 */HWND CreateMyWindow(HINSTANCE hInstance, int nCmdShow){HWND hwnd;// 调整工作区的大小,用于显示完整的图片RECT wr = {0,0,VIDEO_WIDTH,VIDEO_HEIGHT};AdjustWindowRect(&wr, WS_OVERLAPPEDWINDOW, FALSE);hwnd = CreateWindowEx(WS_EX_CLIENTEDGE,g_szClassName,TEXT("My YUV Viewer"),WS_OVERLAPPEDWINDOW,CW_USEDEFAULT, CW_USEDEFAULT, wr.right - wr.left,wr.bottom - wr.top,//VIDEO_WIDTH, VIDEO_HEIGHT,NULL, NULL, hInstance, NULL);if(hwnd == NULL){MessageBox(NULL, TEXT("窗口创建失败!"),TEXT("错误"), MB_ICONEXCLAMATION | MB_OK);exit(0);}ShowWindow(hwnd, nCmdShow);UpdateWindow(hwnd);return hwnd;}/* * 主函数 */int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,LPSTR lpCmdLine, int nCmdShow){HWND hwnd;MSG  Msg;RegisterMyWindow(hInstance);hwnd = CreateMyWindow(hInstance, nCmdShow);while(GetMessage(&Msg, NULL, 0, 0) > 0){TranslateMessage(&Msg);DispatchMessage(&Msg);}return Msg.wParam;}


0 0
原创粉丝点击