视觉媒体通信——无损视频播放器

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;
}

  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值