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();
我是每播放一帧就直接算出帧率
//计算FPSFPS = 1000 / (endtime - starttime);printf("FPS = %f\n", FPS);
注意,上面的时间单位是ms,所以是1000除以时间差。
视频播放器的原理就是一帧一帧的图片连续显示而已。所以我只需要一直调用上面的YUV转RGB函数即可,将每一帧进行显示。
//视频的播放,循环播放while (1){//借助变量pos找到每帧图片的开头位置,并读入pBuffseek(f, pos, SEEK_SET);fread(pBuf, 1, FrameSize, f);//yuv转rgb,然后将rgb转换进imageYUV420_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找到每帧图片的开头位置,并读入pBuffseek(f, pos, SEEK_SET);fread(pBuf, 1, FrameSize, f);//yuv转rgb,然后将rgb转换进imageYUV420_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();//计算FPSFPS = 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;
}