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

 

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
可以使用FFmpeg库来保视频文件,具体实现可以参考以下代码: ```c++ #include <iostream> #include <string> #include <sstream> #include <fstream> #include <chrono> #include <thread> #include <opencv2/opencv.hpp> #include <libavcodec/avcodec.h> #include <libavformat/avformat.h> #include <libavutil/imgutils.h> #include <libswscale/swscale.h> using namespace std; using namespace cv; int main(int argc, char* argv[]) { // 初始化FFmpeg库 av_register_all(); avcodec_register_all(); // 打开视频文件 string filename = "test.mp4"; AVFormatContext* format_ctx = nullptr; if (avformat_open_input(&amp;format_ctx, filename.c_str(), nullptr, nullptr) != 0) { cerr << "Failed to open video file " << filename << endl; return -1; } // 查找视频流 int video_stream_index = -1; for (unsigned int i = 0; i < format_ctx->nb_streams; i++) { if (format_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { video_stream_index = i; break; } } if (video_stream_index == -1) { cerr << "Failed to find video stream in " << filename << endl; avformat_close_input(&amp;format_ctx); return -1; } // 获取视频流的解码器 AVCodec* codec = avcodec_find_decoder(format_ctx->streams[video_stream_index]->codecpar->codec_id); if (codec == nullptr) { cerr << "Failed to find codec for video stream in " << filename << endl; avformat_close_input(&amp;format_ctx); return -1; } // 打开解码器 AVCodecContext* codec_ctx = avcodec_alloc_context3(codec); if (avcodec_parameters_to_context(codec_ctx, format_ctx->streams[video_stream_index]->codecpar) != 0) { cerr << "Failed to copy codec parameters to decoder context in " << filename << endl; avcodec_free_context(&amp;codec_ctx); avformat_close_input(&amp;format_ctx); return -1; } if (avcodec_open2(codec_ctx, codec, nullptr) != 0) { cerr << "Failed to open codec for video stream in " << filename << endl; avcodec_free_context(&amp;codec_ctx); avformat_close_input(&amp;format_ctx); return -1; } // 创建视频文件 string output_filename = "output.mp4"; AVFormatContext* output_format_ctx = nullptr; if (avformat_alloc_output_context2(&amp;output_format_ctx, nullptr, nullptr, output_filename.c_str()) != 0) { cerr << "Failed to create output format context for " << output_filename << endl; avcodec_free_context(&amp;codec_ctx); avformat_close_input(&amp;format_ctx); return -1; } // 添加视频流 AVStream* output_stream = avformat_new_stream(output_format_ctx, nullptr); if (output_stream == nullptr) { cerr << "Failed to create output video stream for " << output_filename << endl; avcodec_free_context(&amp;codec_ctx); avformat_close_input(&amp;format_ctx); avformat_free_context(output_format_ctx); return -1; } output_stream->id = output_format_ctx->nb_streams - 1; if (avcodec_parameters_copy(output_stream->codecpar, format_ctx->streams[video_stream_index]->codecpar) != 0) { cerr << "Failed to copy codec parameters to output video stream in " << output_filename << endl; avcodec_free_context(&amp;codec_ctx); avformat_close_input(&amp;format_ctx); avformat_free_context(output_format_ctx); return -1; } output_stream->codecpar->codec_tag = 0; if (avformat_write_header(output_format_ctx, nullptr) != 0) { cerr << "Failed to write output file header for " << output_filename << endl; avcodec_free_context(&amp;codec_ctx); avformat_close_input(&amp;format_ctx); avformat_free_context(output_format_ctx); return -1; } // 分配解码 AVFrame* frame = av_frame_alloc(); AVPacket packet; av_init_packet(&amp;packet); // 读取视频帧并保 while (av_read_frame(format_ctx, &amp;packet) == 0) { if (packet.stream_index == video_stream_index) { if (avcodec_send_packet(codec_ctx, &amp;packet) != 0) { cerr << "Failed to send packet to decoder in " << filename << endl; break; } while (avcodec_receive_frame(codec_ctx, frame) == 0) { // 将YUV换为BGR像 Mat bgr_image(frame->height, frame->width, CV_8UC3); SwsContext* sws_ctx = sws_getContext(frame->width, frame->height, (AVPixelFormat)frame->format, frame->width, frame->height, AV_PIX_FMT_BGR24, SWS_BILINEAR, nullptr, nullptr, nullptr); uint8_t* data[AV_NUM_DATA_POINTERS] = { 0 }; data[0] = bgr_image.data; int linesize[AV_NUM_DATA_POINTERS] = { 0 }; linesize[0] = bgr_image.step; sws_scale(sws_ctx, frame->data, frame->linesize, 0, frame->height, data, linesize); sws_freeContext(sws_ctx); // 保BGR像 AVPacket output_packet; av_init_packet(&amp;output_packet); output_packet.data = nullptr; output_packet.size = 0; avcodec_send_frame(codec_ctx, frame); while (avcodec_receive_packet(codec_ctx, &amp;output_packet) == 0) { output_packet.stream_index = output_stream->id; av_write_frame(output_format_ctx, &amp;output_packet); av_packet_unref(&amp;output_packet); } // 显示BGR像 imshow("BGR Image", bgr_image); waitKey(1); } } av_packet_unref(&amp;packet); } // 释放资源 av_write_trailer(output_format_ctx); avcodec_free_context(&amp;codec_ctx); avformat_close_input(&amp;format_ctx); avformat_free_context(output_format_ctx); av_frame_free(&amp;frame); return 0; } ``` 这段代码可以读取一个视频文件,将其中的每一帧换为BGR格式的像,并保为另一个视频文件。其中,使用了OpenCV库来显示BGR像,使用了FFmpeg库来读取和保视频文件

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值