视觉媒体通信——视频的编解码(H.264)

0 引言

        现如今的主流视频编解码方式有多种,其中最为常见和广泛应用的是H.264、H.265(也称为HEVC)、AV1等。视频编解码技术对于数字视频的传输、存储和播放起着关键的作用。它们能够压缩视频数据,减少传输带宽和存储空间的需求,并且保持高质量的视频观看体验。

        H.264,也被称为Advanced Video Coding (AVC),是一种广泛应用的视频编解码标准。它是由国际电信联盟(ITU)和国际标准化组织(ISO)共同制定的,并于2003年发布。H.264具有高压缩性能和广泛的应用支持,成为当今主流的视频编解码标准之一。

        本篇将实现利用现有编码器ffmpeg实现视频h.264编解码。本次用到的版本为Visual Studio 2019,ffmpeg6.0

1 实验目标

        编程实现视频的编解码应用:使用现有编解码器(ffmpeg,编码标准H.264),将原始视频进行编码、解码得到重建视频,分析压缩码率与视频质量的关系。

2 实验内容

2.1 实现框图

         H.264视频编码仅支持对YUV数据进行编码,所以我们应当输入yuv视频流进行相应的编解码。

2.2 H.264编码

        H.264编码器的主要算法流程如下图所示:

(1)输入YUV文件。包括输入视频的分辨率帧率。

(2)调用avcodec_find_encoder 根据视频流信息的codec_id找到对应的解码器(H.264)。

(3)编码器初始化。调用avcodec_alloc_contex3分配解码器内存。

(4)编码参数设置。包括编码后的分辨率、帧率、编码速率、输出格式(YUV)、B帧率等。

(5)打开解码器。调用函数avcodec_open2 使用给定的AVCodec初始化AVCodecContext。

(6)分配变量空间。

(7)转换处理。将YUV三通道数据进行转换处理。

(8)调用avcodec_send_packet送一帧到编码器,avcodec_receive_frame尝试获取编码数据。

(9)将编好的数据(.h264)写入文件。

(10)编码完成,释放资源。

2.3 H.264解码

        H.264解码器的主要算法流程如下图所示:

(1)调用avformat_open_input来打开媒体文件,该文件是用编码函数所编码的H.264文件。

(2)调用avformat_find_stream_info 初始化AVFormatContext。

(3)匹配到视频流的索引index

(4)调用avcodec_find_decoder 根据视频流信息的codec_id找到对应的解码器(H.264)。

(5)解码器初始化。调用avcodec_alloc_contex3分配解码器内存。

(6)解码器参数设置。

(7)打开解码器。调用函数avcodec_open2 使用给定的AVCodec初始化AVCodecContext。

(8)初始化输出文件、解码AVPacket和AVFrame结构体,用于管理缓存区。

(9)调用av_read_frame循环从输入中读取一帧压缩帧。

(10)调用avcodec_send_packet送一帧到解码器,avcodec_receive_frame尝试获取解码数据。

(11)调用sws_scale进行格式转换,写入YUV文件。

(12)FLUSH解码器。由于压缩编码数据在解码器中存在缓冲或者延时,一些packet并不由decoder直接解码输出,需要decoding结束时flush,从而获得所有的解码数据。

(13)解码完成,释放资源。

2.4 解码视频播放

        经解码后得到的视频格式为.yuv格式,因此可以用第一次实验中的YUV播放器进行观看。

3 实验结果

3.1 原始视频

        我处理的仍然是第一次用到的yuv视频。

         原始视频分辨率为176×144,帧率为25FPS,共有870帧。

3.2 编码处理结果

         可以看到,总共成功编码870帧(1-870),码率约为1Mbps。接着我继续查看编码后的视频文件与源文件。

编码前(左)后(右)文件大小对比

         可以看到,编码前YUV文件大小为31.5MB,在码率为1M且不改变分辨率和帧率情况下,经过H.264编码后,文件大小变为4.15MB。

3.3 解码处理结果

         可以看到,总共成功编码870帧(0-869),视频的分辨率为176×144,帧率为25FPS。经过解码后,恢复的YUV文件大小为31.5MB,与源文件一致。

3.4 解码处理后的视频

      将解码处理后的yuv视频进行播放。左图为原视频文件,右图为经过编解码处理后的视频。

        编解码处理后的视频分辨率为176×144,帧率为25FPS,共有870帧。色彩正常,视频清晰度较高。

3.5 改变编码速率

         可以看到,总共成功编码870帧(1-870),编码速率约为64kbps。

编码前(左)后(右)文件大小对比

        可以看到,编码前YUV文件大小为31.5MB,在编码速率为64k且不改变分辨率和帧率情况下,经过H.264编码后,文件大小变为277KB,大大减小。

         可以看到,总共成功编码870帧(0-869),视频的分辨率为176×144,帧率为25FPS。经过解码后,恢复的YUV文件大小为31.5MB,与源文件一致。编解码处理前(左)后(右)如下图所示:

        在码率为64kbps,编解码处理后的视频相比于原始视频色彩正常,视频清晰度略有降低。由于该视频本身分辨率不够高,我再次降低编码速率,以此来比较视频质量。将编码速率调整为32kbps、16kbps。

原始(左上)、码率64kbps(右上)、码率32kbps(左下)、码率16kbps(右下)

         可以看到,在相同的视频编码方式下,随着压缩码率的降低,由此视频的质量越来越差(非线性)。但是与此同时编码后的文件大小也降低。因此,权衡好清晰度和文件大小,来选择最佳的压缩码率。

4 完整代码

4.1 H.264编码部分

#include 
#include "libavcodec/avcodec.h"
#include "libavutil/imgutils.h"
#include "libavutil/opt.h"#ifdef __cplusplus  
extern "C" {
#endif  #ifdef __cplusplus  
}
#endif static void encode(AVCodecContext* enc_ctx, const AVFrame* frame, AVPacket* pkt, FILE* outfile)
{int ret;ret = avcodec_send_frame(enc_ctx, frame);if (ret < 0) {av_log(NULL, AV_LOG_ERROR, "Error sending a frame for encoding\n");exit(1);}while (ret >= 0) {ret = avcodec_receive_packet(enc_ctx, pkt);if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {return;}else if (ret < 0) {av_log(NULL, AV_LOG_ERROR, "Error during encoding\n");exit(1);}//write to fileprintf("Write packet %lld (size = %d)\n", pkt->pts, pkt->size);fwrite(pkt->data, 1, pkt->size, outfile);av_packet_unref(pkt);}
}#define INPUT_FORMAT_YUV420Pint main()
{// 输入视频文件信息int in_width = 176;int in_height = 144;int in_fps = 25;#ifdef INPUT_FORMAT_YUV420Pconst char* in_file_name = "D:\\grandma_qcif.yuv";AVPixelFormat in_pix_fmt = AV_PIX_FMT_YUV420P;
#endif// 输出编码流文件信息(和输入相同)int out_width = 176;int out_height = 144;int out_fps = 25;const char* out_file_name = "grandma_qcif.h264";AVCodecID codec_id = AV_CODEC_ID_H264;// 输入输出文件FILE* in_file, * out_file;in_file = fopen(in_file_name, "rb");if (!in_file) {fprintf(stderr, "Can not open file %s\n", in_file_name);return 0;}out_file = fopen(out_file_name, "wb");if (!out_file) {fprintf(stderr, "Can not open file %s\n", out_file_name);return 0;}// video encoderconst AVCodec* encodec = avcodec_find_encoder(codec_id);if (!encodec) {av_log(NULL, AV_LOG_ERROR, "Codec '%s' not found\n", avcodec_get_name(codec_id));return 0;}// video encoder contexAVCodecContext* encoder_ctx = avcodec_alloc_context3(encodec);if (!encoder_ctx) {av_log(NULL, AV_LOG_ERROR, "Could not allocate video codec context\n");return 0;}// encoder parametersencoder_ctx->bit_rate = 1000000; // 1Mbpsencoder_ctx->width = out_width;encoder_ctx->height = out_height;encoder_ctx->framerate = AVRational{ out_fps, 1 };encoder_ctx->time_base = AVRational{ 1, out_fps };encoder_ctx->gop_size = out_fps;encoder_ctx->max_b_frames = 0;   // B帧数encoder_ctx->pix_fmt = AV_PIX_FMT_YUV420P;  // H264不支持RGBif (encoder_ctx->codec_id == AV_CODEC_ID_H264) {av_opt_set(encoder_ctx->priv_data, "preset", "slow", 0);}// open condecint ret = avcodec_open2(encoder_ctx, encodec, NULL);if (ret < 0) {av_log(NULL, AV_LOG_ERROR, "Could not open codec\n");avcodec_free_context(&encoder_ctx);return 0;}// allocate variables// 编码器实际输入(参数和编码器保持一致)AVFrame* frame = av_frame_alloc();// 供解码器使用,需要设定width、height、format,否则会出现警告frame->width = encoder_ctx->width;frame->height = encoder_ctx->height;frame->format = encoder_ctx->pix_fmt;// Y,U,V三个通道内存不连续,1字节对齐av_frame_get_buffer(frame, 1); AVPacket* pkt = av_packet_alloc();// start encoderint64_t frame_cnt = 1;while (!feof(in_file)) {// Y,U,V三个通道内存不连续,单通道内存1字节对齐连续if (fread(frame->data[0], in_width * in_height, 1, in_file) != 1)break;if (fread(frame->data[1], in_width * in_height / 4, 1, in_file) != 1)break;if (fread(frame->data[2], in_width * in_height / 4, 1, in_file) != 1)break;frame->pts = frame_cnt++; //必须,否则会有警告,输出视频码率极低,马赛克严重encode(encoder_ctx, frame, pkt, out_file);}// flushencode(encoder_ctx, NULL, pkt, out_file);fclose(in_file);fclose(out_file);avcodec_free_context(&encoder_ctx);av_frame_free(&frame);av_packet_free(&pkt);
}

4.2 H.264解码

#include extern "C" {
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libswscale/swscale.h"
#include "libavutil/pixdesc.h"
#include "libavutil/frame.h"
#include "libavutil/imgutils.h"
}int main()
{//解码部分// 打开输入const char* input_file = "D:\\grandma_qcif.h264";int ret;AVFormatContext* input_fmt_ctx = NULL; // 必须设置NULLif ((ret = avformat_open_input(&input_fmt_ctx, input_file, NULL, NULL)) < 0) {av_log(NULL, AV_LOG_ERROR, "Cannot open input file\n");return ret;}// 分析流信息if ((ret = avformat_find_stream_info(input_fmt_ctx, NULL)) < 0) {av_log(NULL, AV_LOG_ERROR, "Cannot find stream information\n");return ret;}// 打印信息av_dump_format(input_fmt_ctx, 0, input_file, 0);---------------------- 解码部分 ----------------------// int video_stream_index = -1;const AVCodec* video_codec = avcodec_find_decoder(AV_CODEC_ID_H264);// 查找视频流 if ((ret = av_find_best_stream(input_fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, &video_codec, -1)) < 0) {av_log(NULL, AV_LOG_ERROR, "Cannot find an video stream in the input file\n");avformat_close_input(&input_fmt_ctx);return ret;}video_stream_index = ret;// 解码器初始化AVCodecParameters* codecpar = input_fmt_ctx->streams[video_stream_index]->codecpar;// const AVCodec* video_codec = avcodec_find_decoder(AV_CODEC_ID_H264);if (!video_codec) {av_log(NULL, AV_LOG_ERROR, "Can't find decoder\n");return -1;}AVCodecContext* video_decoder_ctx = avcodec_alloc_context3(video_codec);if (!video_decoder_ctx) {av_log(NULL, AV_LOG_ERROR, "Could not allocate a decoding context\n");avformat_close_input(&input_fmt_ctx);return AVERROR(ENOMEM);}// 解码器参数配置if ((ret = avcodec_parameters_to_context(video_decoder_ctx, codecpar)) < 0) {avformat_close_input(&input_fmt_ctx);avcodec_free_context(&video_decoder_ctx);return ret;}// 打开解码器if ((ret = avcodec_open2(video_decoder_ctx, video_codec, NULL)) < 0) {avformat_close_input(&input_fmt_ctx);avcodec_free_context(&video_decoder_ctx);return ret;}// 解码并保存到文件 uint32_t frameCnt = 0;AVPacket* pkt = av_packet_alloc(); // 分配一个AVPactet对象,用于管理其缓冲区AVFrame* frame = av_frame_alloc(); // 分配一个AVFrame对象,用于管理其缓冲区FILE* fyuv = fopen("out.yuv", "wb");// yuv420p对齐处理 变量AVFrame* frame_yuv = av_frame_alloc();// 分配缓冲区,接收转换后yuv420p的1字节对齐数据,分辨率不改变frame_yuv->width = video_decoder_ctx->width;frame_yuv->height = video_decoder_ctx->height;av_image_alloc(frame_yuv->data, frame_yuv->linesize,frame_yuv->width, frame_yuv->height, AV_PIX_FMT_YUV420P, 1);// SwsContext上下文,用于sws_scale调用SwsContext* sws_ctx =sws_getContext(video_decoder_ctx->width, video_decoder_ctx->height, video_decoder_ctx->pix_fmt, // 输入格式frame_yuv->width, frame_yuv->height, AV_PIX_FMT_YUV420P,                         // 输出格式SWS_BILINEAR, NULL, NULL, NULL);                                                 // 变换处理while (av_read_frame(input_fmt_ctx, pkt) >= 0) { // 循环从输入获取一帧压缩编码数据,分配pkt缓冲区  // 仅处理视频码流if (pkt->stream_index != video_stream_index)continue;ret = avcodec_send_packet(video_decoder_ctx, pkt);  // 送一帧到解码器while (ret >= 0) {ret = avcodec_receive_frame(video_decoder_ctx, frame); // 尝试获取解码数据,分配frame缓冲区if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {break;}else if (ret < 0) {av_log(NULL, AV_LOG_ERROR, "Error while sending a packet to the decoder\n");goto end;}// 解码的视频数据处理sws_scale(sws_ctx,frame->data, frame->linesize, 0, frame->height,frame_yuv->data, frame_yuv->linesize);fwrite(frame_yuv->data[0], 1, frame_yuv->width * frame_yuv->height * 3 / 2, fyuv);printf("\rSucceed to decode frame %d\n", frameCnt++);av_frame_unref(frame);  // 释放frame缓冲区数据}av_packet_unref(pkt); // 释放pkt缓冲区数据}while (1) {ret = avcodec_send_packet(video_decoder_ctx, NULL);if (ret < 0)break;while (ret >= 0) {ret = avcodec_receive_frame(video_decoder_ctx, frame);if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {break;}else if (ret < 0) {av_log(NULL, AV_LOG_ERROR, "Error while sending a packet to the decoder\n");goto end;}// 解码的视频数据处理sws_scale(sws_ctx,frame->data, frame->linesize, 0, frame->height,frame_yuv->data, frame_yuv->linesize);fwrite(frame_yuv->data[0], 1, frame_yuv->width * frame_yuv->height * 3 / 2, fyuv);printf("\rSucceed to decode frame %d\n", frameCnt++);av_frame_unref(frame);  // 释放frame缓冲区数据}}end:// 关闭输入avcodec_free_context(&video_decoder_ctx);avformat_close_input(&input_fmt_ctx);av_packet_free(&pkt);av_frame_free(&frame);fclose(fyuv);return 0;}


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部