软件设备
Visual Studio2022
实验内容
1、YUV视频文件显示程序为基础,结合图像融合原理与方法,设计并实现一个给YUV视频添加动态字幕的程序(类似卡拉OK动态字幕)。
2、设计实现一个视频特效程序,能够将两个视频实现淡入淡出。
3、设计实现一个视频特效程序,能够将两个(或多个)视频拼接成一个宽幅视频。
4、试将水波纹模拟程序和YUV视频显示程序结合起来,实现在视频中叠加水波纹(选做)。
内容1:添加动态字幕
-
首先,创建一个WIN32项目,如何创建项目?请参考这篇文章:全新超详细!2024 CSU 多媒体技术与应用实验一(详细版)-CSDN博客
-
导入我们的素材:
-
(附:如果您感兴趣的话,可以做一下视频文件的显示内容,跟实验部分没关系,但是可以显示多种类型的视频格式,具体请参考教材中的代码,效果如下,当然我不是为了放我们gege的视频)
-
回到实验部分,接下来我们要完成的是如何显示一个YUV的视频格式;首先,在framework.h最下面添加一个头文件。
-
然后,回到主函数,在最前面添加如下宏定义:
#define MAX_LOADSTRING 100
#define IMAGE_WIDTH 352
#define IMAGE_HIGHT 288
typedef struct COLOR
{
BYTE b;
BYTE g;
BYTE r;
}RGBCOLOR;
-
设置用于视频加载和背景图片(background.bmp)的全局变量,复制下面的代码块中的即可,图片给的少了融合图像的部分,我们内容1只用到了det_image,内容二开始会用到det_image2,所以您不妨先复制下来再说,待会都用得到的;
//用于加载视频文件 static FILE* ifp; //file pointer const char* filename = "foreman.yuv"; static BYTE mybuf[45621248]; //arry,store the video file static BYTE* pBity, * pBitu, * pBitv; static int y[288][352], u[144][176], v[144][176]; //背景图像 static FILE* ifpback; const char* filenameback = "background.bmp"; static unsigned char mybufback[IMAGE_WIDTH * IMAGE_HIGHT * 3 + 100]; static BITMAPFILEHEADER* pbmfh; static BITMAPINFO* pbmi; static BYTE* pbits; static int cxDib, cyDib; //融合图像 static COLOR det_image[IMAGE_HIGHT][IMAGE_WIDTH];//要显示的目标图像1 static COLOR det_image2[IMAGE_HIGHT][IMAGE_WIDTH];//要显示的目标图像2 static int n = 0;
-
在主函数中添加如下代码,用来打开视频文件和图像文件:
//打开视频文件
fopen_s(&ifp, filename, "r");
fread(mybuf, 45621248, 1, ifp);
pBity = mybuf;
pBitu = mybuf + 352 * 288;
pBitv = mybuf + 352 * 288 + 176 * 144;
//打开图像文件
fopen_s(&ifpback, filenameback, "r");
fread(mybufback, 307200, 1, ifpback);
pbmfh = (BITMAPFILEHEADER*)mybufback;
pbmi = (BITMAPINFO*)(pbmfh + 1);
pbits = (BYTE*)pbmfh + pbmfh->bfOffBits;
cxDib = pbmi->bmiHeader.biWidth;
cyDib = pbmi->bmiHeader.biHeight;
-
添加一个计时器(你懂的,在Resource.h文件中添加);
-
初始化计时器,没错,就是放在InitInstance函数下面,实验二中提到过:
-
主窗口过程函数新增代码(图片没截取全,代码我放在下面了)(我这儿代码跟书上的不太一样,注意别出现图片倒转的情况了)原理解释:其实就是读取每一帧的YUV矩阵,然后转成了RGB,要么从头读要么从尾读,都可以的。注意!!!!HDC的定义处别写错了,以给的代码块为准:
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
int wmId, wmEvent;
PAINTSTRUCT ps;
HDC hdc;//!!!!!!!!!!!!!!!!!!!!!!!
int changeColor;
switch (message)
{
//新添加代码的开始处
case WM_TIMER:
hdc = GetDC(hWnd);
n = n + 1;
if (n > 299) n = 0;
pBity = pBity + (352 * 288 + 2 * (176 * 144)) * n;
pBitu = pBity + 352 * 288;
pBitv = pBitu + 176 * 144;
//read a new frame of the yuv file
for (int i = 0; i < 144; i++)
for (int j = 0; j < 176; j++)
{
u[i][j] = *(pBitu + j + 176 * (i));
v[i][j] = *(pBitv + j + 176 * (i));
}
//read y,and translate yuv int rgb and display the pixel
for (int i = 0; i < 288; i++)
for (int j = 0; j < 352; j++)
{
//read y
y[i][j] = *(pBity + j + (i) * 352);
y2[i][j] = *(pBity2 + j + (i) * 352);
//translate
int r = (298 * (y[i][j] - 16) + 409 * (v[i / 2][j / 2] - 128) + 128) >> 8;
if (r < 0) r = 0;
if (r > 255) r = 255;
int g = (298 * (y[i][j] - 16) - 100 * (u[i / 2][j / 2] - 128) - 208 * (v[i / 2][j / 2] - 128) +
128) >> 8;
if (g < 0) g = 0;
if (g > 255) g = 255;
int b = (298 * (y[i][j] - 16) + 516 * (u[i / 2][j / 2] - 128) + 128) >> 8;
if (b < 0) b = 0;
if (b > 255) b = 255;
//存储到目标图像矩阵
det_image[288 - i - 1][j].r = r;
det_image[288 - i - 1][j].g = g;
det_image[288 - i - 1][j].b = b;
}
// 显示当前帧
SetDIBitsToDevice(hdc,
30,
20,
352,
288,
0,
0,
0,
288,
det_image,
pbmi,
DIB_RGB_COLORS);
// 恢复指向图像数据首字节
pBity = mybuf;
ReleaseDC(hWnd, hdc);
break;
// 新添加代码结束
-
显示效果就是这样子的:
-
接下来我们做字幕的部分,咱不着急,先从普通字幕开始做起,原理是什么呢?其实原理就是图片的下方有一个灰色的背景图,这个背景图只有文字那一部分不是黑的,那么我们在读取背景的时候,如果这个像素是黑的,那么我们就将这个像素的值用视频帧的rgb值来替代,如果这个像素不是黑的,那么就用背景图的像素来替代。那么如何添加你自己的字幕呢?我们可以用windows自带的画图工具去添加文字,然后保存为bmp文件即可,background.bmp是老师给的软件资源里面的,字幕别是黑色就行:
-
然后我们将第10步的代码中添加图像融合的步骤,像下面这样子:
case WM_TIMER:
hdc = GetDC(hWnd);
n = n + 1;
if (n>299) n = 0;
pBity = pBity + (352 * 288 + 2 * (176 * 144))*n;
pBitu = pBity + 352 * 288;
pBitv = pBitu + 176 * 144;
//read a new frame of the yuv file
for (int i = 0; i<144; i++)
for (int j = 0; j<176; j++)
{
u[i][j] = *(pBitu + j + 176 * (i));
v[i][j] = *(pBitv + j + 176 * (i));
}
//read y,and translate yuv int rgb and display the pixel
for (int i = 0; i<288; i++)
for (int j = 0; j<352; j++)
{
//read y
y[i][j] = *(pBity + j + (i)* 352);
//translate
int r = (298 * (y[i][j] - 16) + 409 * (v[i / 2][j / 2] - 128) + 128) >> 8;
if (r<0) r = 0;
if (r>255) r = 255;
int g = (298 * (y[i][j] - 16) - 100 * (u[i / 2][j / 2] - 128) - 208 * (v[i / 2][j / 2] - 128) +
128) >> 8;
if (g<0) g = 0;
if (g>255) g = 255;
int b = (298 * (y[i][j] - 16) + 516 * (u[i / 2][j / 2] - 128) + 128) >> 8;
if (b<0) b = 0;
if (b>255) b = 255;
// 取字幕图标图像的像素值
int rback = *(pbits + 2 + j * 3 + (cyDib - i - 1)*cxDib * 3);
int gback = *(pbits + 1 + j * 3 + (cyDib - i - 1)*cxDib * 3);
int bback = *(pbits + 0 + j * 3 + (cyDib - i - 1)*cxDib * 3);
// 如果当前字幕图标图像像素值是黑色,就传送视频像素值到目标图像
if (rback ==0 && gback ==0 && bback ==0)
{
det_image[288 - i - 1][j].r = r;
det_image[288 - i - 1][j].g = g;
det_image[288 - i - 1][j].b = b;
}else//否则,就传送字幕图标图像的当前像素值到目标图像
{
det_image[288 - i - 1][j].r = rback;
det_image[288 - i - 1][j].g = gback;
det_image[288 - i - 1][j].b = bback;
}
}
SetDIBitsToDevice(hdc,
30,
20,
352,
288,
0,
0,
0,
288,
det_image,
pbmi,
DIB_RGB_COLORS);
pBity = mybuf; // let pBity to point at the first place of the file
ReleaseDC(hWnd, hdc);
break;
14.单字幕效果就是这样子的啦:
-
然后我们来完成动态字幕的效果吧!首先!我们添加一个随机变量changeColor来随时间的变化而变化,让changeColor随着n变化即可。
-
然后再添加这样一个else if就可以啦,意思就是y小于这个值时显示绿色,y大于这个值时显示原来的字幕,(颜色是自己定义的)
-
动态字幕的效果如下:
内容2:淡入淡出
-
添加全局变量:
-
主函数中打开视频2(跟内容1如出一辙的):
-
修改主过程函数:
关键代码如下:
for (int i = 0; i < 288; i++)
for (int j = 0; j < 352; j++)
{
//read y
y[i][j] = *(pBity + j + (i) * 352);
y2[i][j] = *(pBity2 + j + (i) * 352);
//translate
int r = (298 * (y[i][j] - 16) + 409 * (v[i / 2][j / 2] - 128) + 128) >> 8;
if (r < 0) r = 0;
if (r > 255) r = 255;
int g = (298 * (y[i][j] - 16) - 100 * (u[i / 2][j / 2] - 128) - 208 * (v[i / 2][j / 2] - 128) +
128) >> 8;
if (g < 0) g = 0;
if (g > 255) g = 255;
int b = (298 * (y[i][j] - 16) + 516 * (u[i / 2][j / 2] - 128) + 128) >> 8;
if (b < 0) b = 0;
if (b > 255) b = 255;
//处理第2个视频
int r2 = (298 * (y2[i][j] - 16) + 409 * (v2[i / 2][j / 2] - 128) + 128) >> 8;
if (r2 < 0) r2 = 0;
if (r2 > 255) r2 = 255;
int g2 = (298 * (y2[i][j] - 16) - 100 * (u2[i / 2][j / 2] - 128) - 208 * (v2[i / 2][j / 2] - 128) +
128) >> 8;
if (g2 < 0) g2 = 0;
if (g2 > 255) g2 = 255;
int b2 = (298 * (y2[i][j] - 16) + 516 * (u2[i / 2][j / 2] - 128) + 128) >> 8;
if (b2 < 0) b2 = 0;
if (b2 > 255) b2 = 255;
//定义透明度参数
double para = n / 300.0;
两帧图像的当前像素的融合,结果放入目标图像矩阵中
det_image[288 - i - 1][j].r = r * (1 - para) + r2 * para;
det_image[288 - i - 1][j].g = g * (1 - para) + g2 * para;
det_image[288 - i - 1][j].b = b * (1 - para) + b2 * para;
//取字幕图标图像的像素值
int rback = *(pbits + 2 + j * 3 + (cyDib - i - 1) * cxDib * 3);
int gback = *(pbits + 1 + j * 3 + (cyDib - i - 1) * cxDib * 3);
int bback = *(pbits + 0 + j * 3 + (cyDib - i - 1) * cxDib * 3);
//如果当前字幕图标图像像素值是黑色,就传送视频像素值到目标图像
if (rback == 0 && gback == 0 && bback == 0)
{
/*det_image[288 - i - 1][j].r = r;
det_image[288 - i - 1][j].g = g;
det_image[288 - i - 1][j].b = b;*/
det_image[288 - i - 1][j].r = r * (1 - para) + r2 * para;
det_image[288 - i - 1][j].g = g * (1 - para) + g2 * para;
det_image[288 - i - 1][j].b = b * (1 - para) + b2 * para;
}
//实现单字幕注释这个else if
//左半部分的字幕颜色改变部分
else if (j < changeColor) {
det_image[288 - i - 1][j].r = 0;
det_image[288 - i - 1][j].g = gback + 100;
det_image[288 - i - 1][j].b = bback;
}
//右半部分颜色不改变
else{
det_image[288 - i - 1][j].r = rback;
det_image[288 - i - 1][j].g = gback;
det_image[288 - i - 1][j].b = bback;
}
}
-
效果如下:
内容3:宽幅视频
-
这个也比较简单,在上面代码的基础上,直接修改主过程函数如下:
-
for (int i = 0; i < 288; i++) for (int j = 0; j < 352; j++) { //read y y[i][j] = *(pBity + j + (i) * 352); y2[i][j] = *(pBity2 + j + (i) * 352); //translate int r = (298 * (y[i][j] - 16) + 409 * (v[i / 2][j / 2] - 128) + 128) >> 8; if (r < 0) r = 0; if (r > 255) r = 255; int g = (298 * (y[i][j] - 16) - 100 * (u[i / 2][j / 2] - 128) - 208 * (v[i / 2][j / 2] - 128) + 128) >> 8; if (g < 0) g = 0; if (g > 255) g = 255; int b = (298 * (y[i][j] - 16) + 516 * (u[i / 2][j / 2] - 128) + 128) >> 8; if (b < 0) b = 0; if (b > 255) b = 255; //处理第2个视频 int r2 = (298 * (y2[i][j] - 16) + 409 * (v2[i / 2][j / 2] - 128) + 128) >> 8; if (r2 < 0) r2 = 0; if (r2 > 255) r2 = 255; int g2 = (298 * (y2[i][j] - 16) - 100 * (u2[i / 2][j / 2] - 128) - 208 * (v2[i / 2][j / 2] - 128) + 128) >> 8; if (g2 < 0) g2 = 0; if (g2 > 255) g2 = 255; int b2 = (298 * (y2[i][j] - 16) + 516 * (u2[i / 2][j / 2] - 128) + 128) >> 8; if (b2 < 0) b2 = 0; if (b2 > 255) b2 = 255; //定义透明度参数 double para = n / 300.0; 两帧图像的当前像素的融合,结果放入目标图像矩阵中 det_image[288 - i - 1][j].r = r * (1 - para) + r2 * para; det_image[288 - i - 1][j].g = g * (1 - para) + g2 * para; det_image[288 - i - 1][j].b = b * (1 - para) + b2 * para; //宽幅视频 det_image[288 - i - 1][j].r = r; det_image[288 - i - 1][j].g = g; det_image[288 - i - 1][j].b = b; det_image2[288 - i - 1][j].r = r2; det_image2[288 - i - 1][j].g = g2; det_image2[288 - i - 1][j].b = b2; //取字幕图标图像的像素值 int rback = *(pbits + 2 + j * 3 + (cyDib - i - 1) * cxDib * 3); int gback = *(pbits + 1 + j * 3 + (cyDib - i - 1) * cxDib * 3); int bback = *(pbits + 0 + j * 3 + (cyDib - i - 1) * cxDib * 3); //如果当前字幕图标图像像素值是黑色,就传送视频像素值到目标图像 if (rback == 0 && gback == 0 && bback == 0) { /*det_image[288 - i - 1][j].r = r; det_image[288 - i - 1][j].g = g; det_image[288 - i - 1][j].b = b;*/ det_image[288 - i - 1][j].r = r * (1 - para) + r2 * para; det_image[288 - i - 1][j].g = g * (1 - para) + g2 * para; det_image[288 - i - 1][j].b = b * (1 - para) + b2 * para; } //实现单字幕注释这个else if //左半部分的字幕颜色改变部分 else if (j < changeColor) { det_image[288 - i - 1][j].r = 0; det_image[288 - i - 1][j].g = gback + 100; det_image[288 - i - 1][j].b = bback; } //右半部分颜色不改变 else{ det_image[288 - i - 1][j].r = rback; det_image[288 - i - 1][j].g = gback; det_image[288 - i - 1][j].b = bback; } } // 显示当前帧 SetDIBitsToDevice(hdc, 30, 20, 352, 288, 0, 0, 0, 288, det_image, pbmi, DIB_RGB_COLORS); SetDIBitsToDevice(hdc, 382, 20, 352, 288, 0, 0, 0, 288, det_image2, pbmi, DIB_RGB_COLORS);
-
效果如下:
-
到此,实验三完成了。
参考文章:
https://www.codenong.com/cs106759209/#google_vignette
更多内容可以查看:https://cds007.github.io/
有问题可在评论区交流~