基于WIN32开发的YUV视频播放器 基于WIN32开发的YUV视频播放器 | Winnie’s 快乐星球

野生菜鸡技术员一位

桂ICP备2021005834号-1

基于WIN32开发的YUV视频播放器

导言:多媒体实验,一个基于WIN32平台开发的YUV视频播放器

设计要求

设计实现一个基于Win32的应用程序,包括如下功能(满分100分)

  1. 能够通过文件选择对话框选定两个视频文件。如果你使用OpenCV则可以选择常规的mp4或avi格式视频;如果你不使用OpenCV则建议载入yuv视频。我们在第十二周的实验代码压缩包“YUV视频播放的参考代码和YUV视频文件.zip”中包含了两个yuv视频文件,可供你使用。(占10分)
  2. 能通过菜单选择任意一个视频进行播放(占10分),具有播放(占10分)暂停(占10分)停止功能(占10分)
  3. 能通过菜单选择画中画效果,即在播放某一个视频时,同时把另外一个视频缩小至前一个视频画面的右上角一同播放(占30分,其中缩小占10分、右上角占10分、一同播放占10分)。在画中画播放过程中,任意视频如果播放到结尾,该视频就从头重播,周而复始(占20分,其中若两个视频都能分别周而复始播放给20分,若只有一个可以则给10分)
  4. 如果实现了额外的视频效果,则最多可以奖励15分。可以选择的视频效果: 1) 可以用鼠标拖拽的方式改变画中画的小视频的位置(5分) 2) 可以用鼠标在视频画面上绘制线条(5分) 3) 可以将画中画的小视频改为边缘图显示(即显示其画面的边缘图而不是原图)(5分)

设计过程

本项目以老师在第12章的发布的 VideoProcDemo 项目为基础开发。

1. 播放YUV视频(10分)。老师的Demo已经可以进行YUV视频的播放,此项完成。

2. 通过菜单选择任意一个视频进行播放(10分)。第一反应是使用一个资源管理器进行文件的浏览和选择,于是选择了 OPENFILENAME 浏览文件,实现了通过菜单选择任意一个视频进行播放。

//浏览文件后保存选择文件名字
OPENFILENAME ofn;					// 文件浏览框
TCHAR szFile[MAX_PATH];				// 保存文件的名字

定义全局变量

完整的打开视频操作如下,读取 szFile 即可获得路径。

ZeroMemory(&ofn, sizeof(OPENFILENAME));
			ofn.lStructSize = sizeof(OPENFILENAME);
			ofn.hwndOwner = hWnd;
			ofn.lpstrFile = szFile;
			ofn.nMaxFile = sizeof(szFile);
			ofn.lpstrFilter = _T("YUV视频文件(*.yuv)\0*.yuv\0\0");
			ofn.nFilterIndex = 1;
			ofn.lpstrFileTitle = NULL;
			ofn.nMaxFileTitle = 0;
			ofn.lpstrInitialDir = NULL;
			ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST;
			if (GetOpenFileName(&ofn) == TRUE) {
                //TODO
            }

注意:如果按照上述代码的方法,需要在 “项目” —— “项目设置” 中将 “字符集” 由 “使用Unicode字符集” 改为 “使用字节字符集”。

3. 播放、暂停和停止(30分)。本项目中视频播放依靠的是定时器TIMER,在case WM_TIMER分支中对定时器事件进行处理,于是我使用两个变量实现对大小视频的状态记录。

static int vedioFirstStatus = 0;			// 视频1的播放状态 1 播放 2 暂停 3 停止

定义全局变量

当变量为1时,case WM_TIMER 分支就会继续播放视频,当变量为其他值时,则不会进行播放;点击暂停,变量会变为2,但此时不对变量n做出改动,当点击播放时,变量变为1,实现视频的播放效果,视频会继续播放,实现暂停的效果;点击停止,变量会变为3,此时设置变量n为0,当点击播放时,视频会从头播放,实现停止的效果

case ID_PAUSE:
    if (vedioFirstStatus == 1) {
        vedioFirstStatus = 2;
    }
    break;
case ID_STOP:
    if (vedioFirstStatus != 3) {
        n = 0;
        vedioFirstStatus = 3;
    }
    break;

4. 能通过菜单选择画中画效果,即在播放某一个视频时,同时把另外一个视频缩小至前一个视频画面的右上角一同播放(占30分,其中缩小占10分、右上角占10分、一同播放占10分)。在 case WM_TIMER 分支中,首先对大视频进行渲染,在大视频渲染完成后,再对小视频进行渲染,可以实现同时播放的效果。对渲染函数 SetDIBitsToDevice 的传入变量进行修改,可以实现视频位置的改变,实现右上角的效果。缩小效果我采用隔行采样实现,即通过横向和纵向的各行采样,并将采样到的值存入长宽均为原视频一半的变量中,实现图像的缩小,比例为1:4。

5. 在画中画播放过程中,任意视频如果播放到结尾,该视频就从头重播,周而复始(占20分,其中若两个视频都能分别周而复始播放给20分,若只有一个可以则给10分)。在视频的播放过程中,全局变量n对当前播放视频的帧数进行计数,设置n超过一定大小时归零,实现两个视频的循环播放

case WM_TIMER:
    hdc = GetDC(hWnd);
    if (vedioFirstStatus == 1) {
        n = n + 1;
        if (n > 299) n = 0;

6. 可以用鼠标拖拽的方式改变画中画的小视频的位置(5分)。首先定义 currentxPos 和 currentyPos 来记录当前鼠标点击的x和y的坐标,当落点在小视频范围内时,拖拽鼠标,记录下移动的距离,并计算更新获得当前的 currentxPos 和 currentyPos ,下一帧就会在新的位置进行渲染,从而实现拖拽改变小视频位置的效果

bool mouse_L_pressed = false;       // 鼠标左键是否处于被按下的状态
static int lastxPos = NULL;			// 上一个X坐标
static int lastyPos = NULL;			// 上一个Y坐标

//移动
static int currentxPos = 176;
static int currentyPos = -144;

定义全局变量,currentyPos 为什么是-144 会在难点解析中细说

case WM_LBUTTONDOWN:
    mouse_L_pressed = true;
    break;
case WM_LBUTTONUP:
    mouse_L_pressed = false;
    lastxPos = NULL;
    lastyPos = NULL;
    break;

控制鼠标按下的 case 分支

// 拖拽移动
else if (mouse_L_pressed && draw_mode == 0) {
    int xPos = GET_X_LPARAM(lParam);			//当前鼠标所在的X位置
    int yPos = GET_Y_LPARAM(lParam);			//当前鼠标所在的Y位置
    int xMove;
    int yMove;
    if (lastxPos != NULL && lastyPos != NULL) {
        if (xPos > currentxPos && xPos < currentxPos + 176 && yPos > currentyPos && currentyPos < currentyPos + 144) {
            xMove = lastxPos - xPos;
            yMove = lastyPos - yPos;
            if (currentxPos - xMove > 0 && currentxPos - xMove < 176 && currentyPos - yMove > -144 && currentyPos - yMove < 0) {
                currentxPos -= xMove;
                currentyPos -= yMove;
            }
        }
    }
    lastxPos = xPos;
    lastyPos = yPos;
}
break;

上述代码写在 case WM_MOUSEMOVE 中

SetDIBitsToDevice(hdc,
                  currentxPos,
                  currentyPos,
                  176,
                  288,
                  0,
                  0,
                  0,
                  144,
                  det_image2,
                  pbmi2,
                  DIB_RGB_COLORS);

7. 可以用鼠标在视频画面上绘制线条(5分)。定义一个变量 draw_mode,当点击开始绘制时,draw_mode 变为1,通过 MoveToEx 和 LineTo 函数实现起点和终点之间的连线。由于视频播放时会使线条消失,于是我使用数组 startx、starty、endx、endy 记录下所有的起点和终点,在图片渲染时进行重绘,实现在播放的视频上绘制线条

int draw_mode = 0;			// 绘图模式,0代表不绘图,1代表画线

定义全局变量

case ID_START_DRAW_LINE:
    if (szFile[0] != '\0')
        draw_mode = 1;
    break;
case ID_STOP_DRAW_LINE:
    draw_mode = 0;
    break;
// 绘图
if (mouse_L_pressed && draw_mode == 1) {
    HDC hdc = GetDC(hWnd);
    int xPos = GET_X_LPARAM(lParam);
    int yPos = GET_Y_LPARAM(lParam);
    if (lastxPos != NULL && lastyPos != NULL) {
    MoveToEx(hdc, lastxPos, lastyPos, NULL);
    LineTo(hdc, xPos, yPos);
    startx[count] = lastxPos;
    starty[count] = lastyPos;
    endx[count] = xPos;
    endy[count] = yPos;
    count++;
    }
    lastxPos = xPos;
    lastyPos = yPos;
}

上述代码写在 case WM_MOUSEMOVE 中。

注意:由于使用 MoveToEx 需要鼠标上次停留的坐标,而如果不存在上次停留的坐标时,会默认坐标位(0,0),于是我定义初始坐标为NULL,再加以判断,以此忽略第一次划线。

8. 可以将画中画的小视频改为边缘图显示(即显示其画面的边缘图而不是原图)(5分)。使用老师在第九周实验中提供的平滑滤波卷积核,对图像进行平滑滤波处理。

//边缘图
float smooth_kernel[3][3] = { {0.11,0.11,0.11},
							  {0.11,0.11,0.11},
							  {0.11,0.11,0.11} };			//用于图像模糊的滤波核
int smooth = 0;

定义全局变量

将原图与处理过后的模糊图对应像素相减,实现边缘图显示的效果

#define ABS(a) a < 0 ? -a : a
#define EDGE(a) a < 0 ? 0 : a

void FilterBmp(float kernel[3][3])
{
	int i, j, x, y, idx, idy;
	byte r, g, b, r2, g2, b2;
	float sumR, sumG, sumB;
	int finalR, finalG, finalB;
	float weight;

	for (i = 0; i < 288; i++)
	{
		for (j = 0; j < 176; j++)
		{
			//以每个像素为中心进行3x3的加权求和
			sumR = sumG = sumB = 0;
			for (y = i - 1; y <= i + 1; y++)
			{
				for (x = j - 1; x <= j + 1; x++)
				{
					if (y < 0 || y >= 288 || x < 0 || x >= 176)
					{
						//如果待加权位置越界,则其“像素”值视为0,0,0
						r = g = b = 0;
					}
					else {
						//获得待加权位置的像素值
						r = det_image2[y][x].r;
						g = det_image2[y][x].g;
						b = det_image2[y][x].b;
					}

					//与滤波核的对应位置进行加权求和
					idy = y - (i - 1);
					idx = x - (j - 1);
					weight = kernel[idy][idx];
					sumR += weight * r;
					sumG += weight * g;
					sumB += weight * b;
				}
			}

			//将结果输出
			r = ABS(sumR);
			g = ABS(sumG);
			b = ABS(sumB);

			finalR = det_image2[i][j].r - r;
			finalG = det_image2[i][j].g - g;
			finalB = det_image2[i][j].b - b;

			edge_det_image2[i][j].r = EDGE(finalR);
			edge_det_image2[i][j].g = EDGE(finalG);
			edge_det_image2[i][j].b = EDGE(finalB);
		}
	}
}

难点解析

在本次实验中,我遇到最难以处理的问题是视频的缩小问题。按照我原先的想法,在我进行隔行采样后,应该就能得到一个大小比为1:4的小视频,但是在实际的操作过程中,小视频显示出来的图像总是只显示一半,我调整了很多参数,要么显示上半部分,要么显示下半部分,就是无法完全显示

错误图像展示1

在尝试多次无果后,我采用了一种很取巧的解决办法,我将缓存下一帧的变量设置为:

static COLOR det_image2[IMAGE_HIGHT][IMAGE_WIDTH/2];	//要显示的目标图像

即IMAGE_HIGHT不除2,于是我得到了一个上半部分带有黑边的完整视频。

错误图像展示2

最后在 SetDIBitsToDevice 函数中将传入的扫描线数量变量由 IMAGE_HIGHT 设置为 IMAGE_HIGHT/2,由于这个函数渲染图片时从底部进行扫描,于是将扫描线数量设置为一半后,上半部分的黑色部分消失,获得一个理想的缩小视频。

因此当设置位置时,需要计算黑色的上半部分,因此 currentyPos 需要设置为 -144

成果展示

Bilibili视频链接:基于WIN32的画中画视频程序演示

Github源码地址:VideoProcDemo

实验总结

通过这次实验,我对这学期所学的WIN32的程序开发有了更深的了解,同时也熟悉了YUV视频的结构和存储方式和对位图的处理,我非常高兴能在实践中利用所学知识。

在实验中,我也遇到了不少的困难,因为是初次接触WIN32的程序开发,很多地方还不太熟悉,但我还是努力克服了困难,最终完成了程序的开发。

纸上得来终觉浅,绝知此事要躬行。学习不能只拘泥于课本,还是要以实践为主。书本上学到的终究是理论,只有多实践,才能从不断的失败中汲取经验,从而获得进步。