mp4视频文件截图--h264解码成yuv再转存为bmp图片

得到yuv序列

从一个mp4文件中取出一张截图,这个直接用ffmpeg命令行就可以完成,这里想分析下原理,大致的流程原理如标题,将mp4文件解复用得到一个视频流,这里就以h264编码的文件为例,当然实际的视频流不一定是h264,可能是mpeg4 、hevc(h265)、vp8 vp9 av1等等。 和一个音频流,aac mp3等。  现在这里只讨论视频流, 将这个h264视频流解码,解码为yuv, 然后将yuv序列中指定的一帧图片内容转换为rgb,再存储为bmp位图。

前面的步骤,解复用,解码,直接用ffmpeg来完成了:

#ffmpeg -i tfdf.mp4 -ss 00:00:10 -t 10 -pix_fmt yuyv422 672x378_yuyv422.yuv
-i tfdf.mp4  输入文件
-ss 00:00:10 从文件的第00时00分10秒开始
-t 取10s时间的长度
-pix_fmt yuyv422  输出格式yuyv422
672x378_yuyv422.yuv 输出文件名

得到yuyv422文件序列,也可以输出yuv420格式

#ffmpeg -i tfdf.mp4 -ss 00:00:10 -t 10 -pix_fmt yuv420p 672x378_yuv420p.yuv

具体哪些输出格式,ffmpeg 源码 /libavutil/pixdesc.c 中结构体av_pix_fmt_descriptors中可以查看

这里来分析下yuv420, yuv421, yuv422, yuyv422的存储方式:
Y 表示亮度信号,即灰阶值。
UV  色度 和 饱和度。两者一般一起成对表示。也有的叫YCrCb,统一都可叫做 YUV . 
这里是基于模拟信号来表示其含义的,所谓420,即N1:N2:N3 ,
 N1表示Y奇数行加上偶数行Y样本的个数。 为4
 N2 表示奇数行 Cr加上Cb样本的个数(这两个成对,才有意义),为2
 N3 表示偶数行 Cr加上Cb样本的个数 为0.
所以就是YUV420。 每四个像素,共用一个uv分量。而Y分量是一个像素对应一个,所以这个420相比于422来说,利用的是人眼对于亮度的敏感度比对色度的敏感度要高的原理,适当减小色度的信息量,但是人的视觉对色度信息被阉割之后的视频看起来差别不大,这样可以达到节省带宽和存储空间的目的。
那么yuv420 和yuyv422? 名称上也可以看出来,前者是把一帧画面的y分量先连续存储在一起,然后在分别把u分量和v分量存储,一帧图像分成了三个部分存储,如果我们从文件顺序读取显示的话,看到的应该是先从上到下刷出一个黑白图(y), 然后再加上颜色,要把它转换为RGB, 也是应该读完整个图之后在转换。这种存储方式称为 planar 平面存储。 即yuv420p
yuyv422, 就是交叉的方式存储,Y0 Cb Y1 Cr , Y U Y V ,也称为packed打包格式。
这两种存储方式,很明显交叉存储的方式它的容错能力是要强一些的,即使读到半帧数据,也是可以显示半帧的,但是yuv平面存储的方式是要读完整帧,比如下面的 672x378 yuyv422格式的元数据,播放的时候把它的高度故意设置错误,图像也是可以分辨出来的:
 而yuv420p的数据,如果高度设置错误,yuv 分量就都错位了,整个播放的一片浑浊。

取出一个yuv帧来,转为rgb,然后存储为BMP格式图片。
得到yuv帧: 1.0 上述的从视频文件中取yuv序列。
                   2.0 从 bmp/jpg/png 图片中取 

ffmpeg -i jianbian.jpg  -pix_fmt yuv420p jianbian.yuv
ffmpeg -i jianbian.bmp  -pix_fmt yuv420p jianbian.yuv
ffmpeg -i jianbian.bmp  -pix_fmt rgb24 jianbian.rgb

将yuv420p转为rgb:


1.0 利用ffmpeg  (ffmpeg  /libswscale/yuv2rgb.c 源码对应)

ffmpeg -s 3840x2160 -pix_fmt yuv420p -i jianbian.yuv  -pix_fmt rgb24 jianbia_yuv2rgb.rgb

2.0 这里从android7 的源码中,有关于 yuv2rgb的功能,在 https://www.androidos.net.cn/android/7.1.1_r28/xref/frameworks/av/media/libstagefright/yuv/YUVImage.cpp

void YUVImage::yuv2rgb(uint8_t yValue, uint8_t uValue, uint8_t vValue,
        uint8_t *r, uint8_t *g, uint8_t *b) const {
    int rTmp = yValue + (1.370705 * (vValue-128));
    int gTmp = yValue - (0.698001 * (vValue-128)) - (0.337633 * (uValue-128));
    int bTmp = yValue + (1.732446 * (uValue-128));

    *r = clamp(rTmp, 0, 255);
    *g = clamp(gTmp, 0, 255);
    *b = clamp(bTmp, 0, 255);
}

这里借一张图,说明一下 yuv转换公式中,uv的对应值关系,yuv420p中因为 uv的个数是不够的,所以是每四个y公用一组uv,当然不是在一行的连续4个像素的y值,而是如下4x4块状,在实际图中相邻的四个像素公用一组uv

这里有个疑惑,如果是把原本rgb24的转换成yuv420,  原理上讲应该是有丢失信息的,在把它从yuv420转换回来rgb24, 应该是有所差别的吧。个人按照网上找的给类yuv转rgb的公式,也写了一个demo(方便对照原理看的一个代码):

#include<stdio.h>
#include<stdlib.h>
unsigned char clamp(unsigned char v, unsigned char min, unsigned char max)
{
	if(max<=min)
	{
		printf("erro");
		return 0;
	}
	if(v>max) return max;
	else if(v<min) return min;
	else return v;
}
int main(int argc, const char*argv[])
{
#define WIDTH 3840
#define HEIGHT 2160
	if(argc<2)
	{
		printf("usage:yuv420p_file\n");
		return 0;
	}
	FILE *fp_in = fopen(argv[1],"r");
	if(fp_in == NULL)
	{
		printf("fopen erro:%s\n",argv[1]);
		return -1;
	}

	FILE *fp_out = fopen("out.rgb","w+");
	if(fp_out == NULL)
	{
		printf("fopen out.rgb erro");
		fclose(fp_in);
		return -1;
	}

	unsigned char *buffer_in = malloc((WIDTH*HEIGHT*1.5));
	unsigned char *buffer_out = malloc(WIDTH*HEIGHT*3);
	unsigned char y[4]={0};
	unsigned char u=0;
	unsigned char v=0;

	unsigned char r[4]={0};
	unsigned char g[4]={0};
	unsigned char b[4]={0};
	
	fread(buffer_in,1,WIDTH*HEIGHT*1.5,fp_in);
	int u_base_offset = WIDTH*HEIGHT;
	int v_base_offset = u_base_offset+WIDTH*HEIGHT/4;
	int i =0;
	int j =0;

	for(i=0;i<HEIGHT;i+=2)
	{
		for(j=0;j<WIDTH;j+=2)
		{//每次处理 相邻 4x4块 4个像素
			y[0]=buffer_in[i*WIDTH+j];
			y[1]=buffer_in[i*WIDTH+j+1];
			y[2]=buffer_in[(i+1)*WIDTH+j];
			y[3]=buffer_in[(i+1)*WIDTH+j+1];
			u=buffer_in[u_base_offset+j/2+i/2*WIDTH/2];
			v=buffer_in[v_base_offset+j/2+i/2*WIDTH/2];

			int k =0;
			for(k=0;k<4;k++)
			{
				double rd=0,gd=0,bd=0;
			//	rd = 1.164*(y[k]-16)+1.596*(v-128);
			//	gd = 1.164*(y[k]-16)-0.813*(u-128) - 0.392*(v-128);
			//	bd = 1.164*(y[k]-16)+2.017*(u-128);

				rd = y[k] + (1.370705*(v-128));
				gd = y[k] - (0.698001*(v-128)) - (0.337633*(v-128));
				bd = y[k] + (1.732446*(u-128));
				r[k] = clamp((unsigned char)rd,0,255);
				g[k] = clamp((unsigned char)gd,0,255);
				b[k] = clamp((unsigned char)bd,0,255);
			}

			char *dest_pix_ptr[4] = {buffer_out+(i*WIDTH+j)*3, buffer_out+(i*WIDTH+j+1)*3, buffer_out+((i+1)*WIDTH+j)*3,buffer_out+((i+1)*WIDTH+j+1)*3};
			//给4个像素点赋值
			for(k=0;k<sizeof(dest_pix_ptr)/sizeof(dest_pix_ptr[0]);k++)
			{
				*(dest_pix_ptr[k]) = r[k];
				*(dest_pix_ptr[k]+1) = g[k];
				*(dest_pix_ptr[k]+2)= b[k];
			}
		}
	}


	fwrite(buffer_out,1,WIDTH*HEIGHT*3,fp_out);
	free(buffer_in);
	free(buffer_out);
	fclose(fp_out);
	fclose(fp_in);
}

在虚拟机 ubuntu上转换得到的图像某些像素就有问题:左为利用ffmpeg命令将yuv转rgb24的输出,右为上述公式计算得到的输出。?????究竟是浮点计算的误差?还是另有蹊跷?

将rgb24存储为bmp格式


这里对bmp格式不做太多的解析,对于rgb24,即每一个像素点用三个字节 分别存储 r g b三个值,就是24bit,颜色深度为24bit, 简单的存储方式,就是存储一个bmp文件格式的头,然后存储 rgb raw数据就可以了,(对于不是24bit的存储方式还有调色板什么的,这里不去掺和了,简单起见。)
比如 我们在photoshop中创建一个4x4像素的图片,如下:

每一个像素给它一个不同的颜色值,存储为bmp位图的话,bmp文件源数据是这样的:

bmp文件格式,主要四部分组成:
文件头,信息头,调色板,数据。 这里rgb24的话,就不需要调色板了。
借博客一用,bitmap格式讲的很清楚:https://blog.csdn.net/u013066730/article/details/82625158

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章