0 前言
我想,看这篇文章的朋友十有八九是在大三下选了这门课。那么我首先祝贺你们,即将结束美好的大学时光。以前听说这门课很难,但是这门课最大的诱惑就是没有期末考试~,所以我当初就选了。我也很担心自己不会做,但是还是硬着头皮选下来了。emmm怎么说呢,这门课蛮好的,只要你用心做并不难(个人认为),我还是给一些建议给大家,如果你是考研er,你就直接用代码,略微改改,别花太多时间在这上面,因为你想自己独立做出来很费时间。我之前基本一次作业从头到尾8h+完成(我没有c++基础),如果你是保研er,那么你就可以闲的没事做一做(真的会闲的没事吗0.0),既然chatgpt都出来了,建议大家都用一用。
本次课程你将用到Visual Studio、Opencv以及ffmpeg。
废话不多说了,进入正题,如果你觉得这篇文章有用,记得点赞收藏加关注,如果这篇文章效果不错的话(点赞收藏),下一篇我就继续免费查看噜~
1 实验目的
理解数字视频,实现无损视频的播放器。
2 实验内容
2.1 实现方案
2.1.1 YUV文件的处理
首先打开YUV文件,通过查看获取的YUV文件的分辨率(即长、宽),来确定YUV视频一帧的数据大小。通过总数据量和一帧数据之比来计算视频的总帧数。
//计算有多少帧图像
fseek(f, 0, SEEK_END);//文件位置定位到文件尾
frame_count = (int)((int)ftell(f) / FrameSize);
fseek(f, 0, SEEK_SET);//文件位置定位到文件头
这里一定要注意,你必须知道你的YUV文件的分辨率是多少,才能正确实现播放,不然会不对的。比如我下载的YUV视频分辨率是176*144,那么就如下:
#define nWidth 176
#define nHeight 144
#define FrameSize ( nWidth*nHeight*3/2) //YUV一帧图像大小
2.1.2 YUV转RGB
通过编写函数进行YUV到RGB格式的转换。
与我们熟知的RGB类似,YUV也是一种颜色编码方法,主要用于电视系统以及模拟视频领域,它将亮度信息(Y)与色彩信息(UV)分离,没有UV信息一样可以显示完整的图像,只不过是黑白的,这样的设计很好地解决了彩色电视机与黑白电视的兼容问题。并且,YUV不像RGB那样要求三个独立的视频信号同时传输,所以用YUV方式传送占用极少的频宽。YUV 编码采用了明亮度和色度表示每个像素的颜色。其中Y表示明亮度,也就是灰阶值。U、V 表示色度。YUV不是一种格式,而是有多种细分格式。本实验处理的YUV文件格式为YU12(YUV420)文件格式。YUV 4:2:0采样,每四个Y共用UV分量。采样方式如下图所示
存储时,Y, U, V分别存储,分别对应一个plane,统称为YUV420P格式,YU12在存储完Y分量后先存储U分量,存储完U分量之后在存储V分量。存储结构示意图如下图所示:
YUV420转RGB有很多方法,本次实验中我采用了公式法。YUV到RGB的公式如下:
首先你得声明你的函数,如下所示:
//yuv420转rgb函数
void YUV420_2_RGB(unsigned char* pYUV, unsigned char* pRGB, int width, int height);
函数的输入变量应该显而易见吧,分别是YUV、RGB图片的地址,以及你的分辨率。
函数具体如下所示:
void YUV420_2_RGB(unsigned char* pYUV, unsigned char* pRGB, int width, int height)
{
//找到Y、U、V在内存中的首地址
unsigned char* pY = pYUV;
unsigned char* pU = pYUV + height * width;
unsigned char* pV = pU + (height * width / 4);
unsigned char* pBGR = NULL;
unsigned char R = 0;
unsigned char G = 0;
unsigned char B = 0;
unsigned char Y = 0;
unsigned char U = 0;
unsigned char V = 0;
double temp = 0;
for (int i = 0; i < height; i++)
{
for (int j = 0; j < width; j++)
{
//找到相应的RGB首地址
pBGR = pRGB + i * width * 3 + j * 3; //每个像素得存BGR三个数据
//取Y、U、V的数据值
Y = *(pY + i * width + j); //实际上是按行寻找数据值
U = *pU;
V = *pV;
//yuv转rgb公式
//yuv转rgb公式
temp = Y + ((1.772) * (U - 128));
B = temp < 0 ? 0 : (temp > 255 ? 255 : (unsigned char)temp);
temp = (Y - (0.34413) * (U - 128) - (0.714) * (V - 128));
G = temp < 0 ? 0 : (temp > 255 ? 255 : (unsigned char)temp);
temp = (Y + (1.402) * (V - 128));
R = temp < 0 ? 0 : (temp > 255 ? 255 : (unsigned char)temp);
//将转化后的rgb保存在rgb内存中,注意放入的顺序b是最低位
*pBGR = B;
*(pBGR + 1) = G;
*(pBGR + 2) = R;
if (j % 2 != 0)
{
*pU++;
*pV++;
} // 一行中两个Y共用一个UV,当取完两个后,下一次UV开始+1
}
if (i % 2 == 0)
{
pU = pU - width / 2;
pV = pV - width / 2;
} // 每列中两个Y共用一个UV,当取完两列后,UV步进到下一个UV。如第一行i=0读取完了就把UV的首地址放回头地址。
}
}
注:上面的代码不是我本人写的,是我在网上在某个地方找的(显然已经记不清是哪里了,因为太久远了。。。),但是还是很容易理解的。可以自己想一想。
2.1.3 视频显示与操作
将每一帧YUV图像转为RGB格式,进而转为IplImage形式并进行显示,计时器开始计时。在显示每一帧的同时,进行按键检测,对视频进行相应的操作功能,同时进度条变量记录帧的位置进行反馈。显示完毕后循环播放下一帧,并结束计时。通过播放一帧图片的时间来实时计算FPS,并不断反馈给用户。
其中进度条需要写一个函数来用,同样如上节所述,声明然后编写
//进度条反馈函数
void time_trackbar(int, void*);
void time_trackbar(int, void*)
{
pos = time_slider * FrameSize;
}
其中的pos变量用来定位下一帧。
再给他命个名显示一下吧
//进度条名称定义
char TrackbarName[50];
sprintf(TrackbarName, "进度%d", frame_count);
计算FPS就是用帧数除以所用的时间而已,我计算的并不严谨,如果有其他方法,当然更好啦!
首先我在显示一帧图片前进行计时
//计时开始
starttime = clock();
我计时结束的时间是进度条增加后
//计时结束
endtime = clock();
我是每播放一帧就直接算出帧率
//计算FPS
FPS = 1000 / (endtime - starttime);
printf("FPS = %f\n", FPS);
注意,上面的时间单位是ms,所以是1000除以时间差。
视频播放器的原理就是一帧一帧的图片连续显示而已。所以我只需要一直调用上面的YUV转RGB函数即可,将每一帧进行显示。
//视频的播放,循环播放
while (1)
{
//借助变量pos找到每帧图片的开头位置,并读入pBuf
fseek(f, pos, SEEK_SET);
fread(pBuf, 1, FrameSize, f);
//yuv转rgb,然后将rgb转换进image
YUV420_2_RGB(pBuf, pRGB, nWidth, nHeight);
cvSetData(image, pRGB, nWidth * 3);
//显示图片
cvShowImage("yuv_player", image);
/*...按键
操作...*/
//每播放一帧,time_slider自加
time_slider++;
pos += FrameSize;
/*...数据
处理...*/
//播放完后,重新开始播放
if (time_slider > frame_count - 1)
{
pos = 0;
time_slider = 0;
}
}
//释放内存
cvDestroyWindow("yuv_player");
cvReleaseImage(&image);//释放图片内存
delete[] pBuf;
delete[] pRGB;
fclose(f);
return 0;
}
按键操作,就是检测按键操作,然后用判断语句执行相应操作,比如你可以像我这样
//ESC按键按下,退出程序
if (key == 27)
{
return 0;
}
//D键按下,2倍速播放(Double)
if (key == 'D' || key=='d')
{
t = 16;
}
//N键按下,正常速度播放(Normal)
if (key == 'N' || key == 'n')
{
t = 35;
}
//H键按下,0.5倍速播放(Half)
if (key == 'H' || key == 'h')
{
t = 70;
}
//左键按下,快退,步长为10帧
if (key == 0x250000 && (time_slider > 10))
{
pos -= (10 * FrameSize);
time_slider -= 10;
}
// 右键按下,快进,步长为10帧
if (key == 0x270000 && (time_slider < frame_count - 10))
{
pos += (10 * FrameSize);
time_slider += 10;
}
//如果按下p键则暂停
if (key == 'p' || key == 'P')
{
//因为要有单帧播放,所以进入死循环
while (1)
{
key = cvWaitKey(35);
//如果按下g则继续播放
if (key == 'g' || key == 'G')
{
break;
}
//ESC按键按下,退出程序
if (key == 27)
{
return 0;
}
//按下l按键,显示上一帧
if ((key == 'l' || key == 'L') && (time_slider > 0))
{
pos -= FrameSize;
time_slider--;
}
//按下r按键,显示下一帧
if ((key == 'r' || key == 'R') && (time_slider < frame_count))
{
pos += FrameSize;
time_slider++;
}
// 左键按下,10帧快退
if (key == 0x250000 && (time_slider > 0))
{
pos -= (10 * FrameSize);
time_slider -= 10;
}
// 右键按下,10帧快进
if (key == 0x270000 && (time_slider < frame_count - 10))
{
pos += (10 * FrameSize);
time_slider += 10;
}
//创建进度条
fseek(f, pos, SEEK_SET);
fread(pBuf, 1, FrameSize, f);
YUV420_2_RGB(pBuf, pRGB, nWidth, nHeight);
cvSetData(image, pRGB, nWidth * 3);
cvShowImage("yuv_player", image);
}
}
3 实验结果
接下来上才艺,看成片!
4 结语
最后怕大家看不懂(应该不会),或者不知道怎么写出完整的代码(有可能),那么我就给出完整的代码叭
#include <opencv2/highgui/highgui_c.h>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/core/core.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/opencv.hpp>
#include <iostream>
#include <stdio.h>
#include <conio.h>
#pragma comment(lib,"opencv_world344d.lib")
#define nWidth 176
#define nHeight 144
#define FrameSize ( nWidth*nHeight*3/2) //YUV一帧图像大小
using namespace std;
using namespace cv;
int time_slider; //进度条变量
int pos = 0; //用于寻找下一帧位置
int frame_count; //帧数
double starttime;//计时器开始
double endtime;//计时器结束
double FPS;//帧率
char str[10]; // 用于存放帧率的字符串
int t = 35; //延时,用于调整倍速。
//yuv420转rgb函数
void YUV420_2_RGB(unsigned char* pYUV, unsigned char* pRGB, int width, int height);
//进度条反馈函数
void time_trackbar(int, void*);
int main(int argc, char* argv[])
{
int key;
//打开文件
FILE* f;
f = fopen("E:\\Visual+Studio+2019\\YUVplayer\\grandma_qcif.yuv", "rb");
//查看是否成功打开,未成功打开则结束程序
if (f == NULL)
{
printf("file open error!");
return 0;
}
//计算有多少帧图像
fseek(f, 0, SEEK_END);//文件位置定位到文件尾
frame_count = (int)((int)ftell(f) / FrameSize);
fseek(f, 0, SEEK_SET);//文件位置定位到文件头
//开辟缓冲区存放一帧图片信息
unsigned char* pBuf = new unsigned char[FrameSize];
unsigned char* pRGB = new unsigned char[3 * nWidth * nHeight];
//创建image,大小为nWidth*nHeight,数据类型为IPL_DEPTH_8U,3通道
IplImage* image = cvCreateImageHeader(cvSize(nWidth, nHeight), IPL_DEPTH_8U, 3);
//进度条名称定义
char TrackbarName[50];
sprintf(TrackbarName, "进度%d", frame_count);
//创建窗口并命名
cvNamedWindow("ly_yuv_player");
//设置窗口大小
//cvResizeWindow("ly_yuv_player", 1600, 900);
//视频的播放,循环播放
while (1)
{
//借助变量pos找到每帧图片的开头位置,并读入pBuf
fseek(f, pos, SEEK_SET);
fread(pBuf, 1, FrameSize, f);
//yuv转rgb,然后将rgb转换进image
YUV420_2_RGB(pBuf, pRGB, nWidth, nHeight);
cvSetData(image, pRGB, nWidth * 3);
//显示图片
cvShowImage("ly_yuv_player", image);
//计时开始
starttime = clock();
//创建进度条
createTrackbar(TrackbarName, "yuv_player", &time_slider, frame_count, time_trackbar);
//等待按键事件发生
key = cvWaitKey(t);
//根据按键值做出相应的动作
//ESC按键按下,退出程序
if (key == 27)
{
return 0;
}
//D键按下,2倍速播放(Double)
if (key == 'D' || key=='d')
{
t = 16;
}
//N键按下,正常速度播放(Normal)
if (key == 'N' || key == 'n')
{
t = 35;
}
//H键按下,0.5倍速播放(Half)
if (key == 'H' || key == 'h')
{
t = 70;
}
//左键按下,快退,步长为10帧
if (key == 0x250000 && (time_slider > 10))
{
pos -= (10 * FrameSize);
time_slider -= 10;
}
// 右键按下,快进,步长为10帧
if (key == 0x270000 && (time_slider < frame_count - 10))
{
pos += (10 * FrameSize);
time_slider += 10;
}
//如果按下p键则暂停
if (key == 'p' || key == 'P')
{
//因为要有单帧播放,所以进入死循环
while (1)
{
key = cvWaitKey(35);
//如果按下g则继续播放
if (key == 'g' || key == 'G')
{
break;
}
//ESC按键按下,退出程序
if (key == 27)
{
return 0;
}
//按下l按键,显示上一帧
if ((key == 'l' || key == 'L') && (time_slider > 0))
{
pos -= FrameSize;
time_slider--;
}
//按下r按键,显示下一帧
if ((key == 'r' || key == 'R') && (time_slider < frame_count))
{
pos += FrameSize;
time_slider++;
}
// 左键按下,10帧快退
if (key == 0x250000 && (time_slider > 0))
{
pos -= (10 * FrameSize);
time_slider -= 10;
}
// 右键按下,10帧快进
if (key == 0x270000 && (time_slider < frame_count - 10))
{
pos += (10 * FrameSize);
time_slider += 10;
}
//创建进度条
fseek(f, pos, SEEK_SET);
fread(pBuf, 1, FrameSize, f);
YUV420_2_RGB(pBuf, pRGB, nWidth, nHeight);
cvSetData(image, pRGB, nWidth * 3);
cvShowImage("yuv_player", image);
}
}
//每播放一帧,time_slider自加
time_slider++;
pos += FrameSize;
//计时结束
endtime = clock();
//计算FPS
FPS = 1000 / (endtime - starttime);
printf("FPS = %f\n", FPS);
//播放完后,重新开始播放
if (time_slider > frame_count - 1)
{
pos = 0;
time_slider = 0;
}
}
//释放内存
cvDestroyWindow("yuv_player");
cvReleaseImage(&image);//释放图片内存
delete[] pBuf;
delete[] pRGB;
fclose(f);
return 0;
}
void YUV420_2_RGB(unsigned char* pYUV, unsigned char* pRGB, int width, int height)
{
//找到Y、U、V在内存中的首地址
unsigned char* pY = pYUV;
unsigned char* pU = pYUV + height * width;
unsigned char* pV = pU + (height * width / 4);
unsigned char* pBGR = NULL;
unsigned char R = 0;
unsigned char G = 0;
unsigned char B = 0;
unsigned char Y = 0;
unsigned char U = 0;
unsigned char V = 0;
double temp = 0;
for (int i = 0; i < height; i++)
{
for (int j = 0; j < width; j++)
{
//找到相应的RGB首地址
pBGR = pRGB + i * width * 3 + j * 3; //每个像素得存BGR三个数据
//取Y、U、V的数据值
Y = *(pY + i * width + j); //实际上是按行寻找数据值
U = *pU;
V = *pV;
//yuv转rgb公式
temp = Y + ((1.772) * (U - 128));
B = temp < 0 ? 0 : (temp > 255 ? 255 : (unsigned char)temp);
temp = (Y - (0.34413) * (U - 128) - (0.714) * (V - 128));
G = temp < 0 ? 0 : (temp > 255 ? 255 : (unsigned char)temp);
temp = (Y + (1.402) * (V - 128));
R = temp < 0 ? 0 : (temp > 255 ? 255 : (unsigned char)temp);
//将转化后的rgb保存在rgb内存中,注意放入的顺序b是最低位
*pBGR = B;
*(pBGR + 1) = G;
*(pBGR + 2) = R;
if (j % 2 != 0)
{
*pU++;
*pV++;
} // 一行中两个Y共用一个UV,当取完两个后,下一次UV开始+1
}
if (i % 2 == 0)
{
pU = pU - width / 2;
pV = pV - width / 2;
} // 每列中两个Y共用一个UV,当取完两列后,UV步进到下一个UV。如第一行i=0读取完了就把UV的首地址放回头地址。
}
}
void time_trackbar(int, void*)
{
pos = time_slider * FrameSize;
}