如何用FFmpeg库在C++中提取隔帧并生成小体积图片及优化效率
解决FFmpeg提取帧体积过大及效率优化问题
一、图片体积超大的原因及解决方法
你的save函数实际写入的是无压缩的PPM格式(通过fprintf(file, "P6\n%d %d\n%d\n", ...)),哪怕扩展名设为bmp,本质还是未压缩的原始像素数据,体积会达到宽×高×3字节(比如1920×1080分辨率就约6MB,更高分辨率自然会超15MB)。而命令行使用的是JPEG有损压缩格式,能大幅削减体积。
核心修改:集成FFmpeg MJPEG编码器
要生成小体积JPG,需将解码后的帧编码为JPEG格式,而非直接保存原始像素:
- 初始化JPEG编码器上下文,配置压缩参数
- 将解码帧转换为JPEG支持的YUVJ420P格式
- 编码YUV帧为JPEG数据包并写入文件
二、程序效率低下的原因及优化方案
当前代码存在多处低效点:
- 每次调用
decode都重新创建SwsContext和AVFrame,重复分配/释放内存 - 默认单线程解码,未利用多核CPU资源
- 先解码所有帧再筛选,浪费了不必要的解码算力(命令行用
select滤镜提前过滤,只解码需要的帧)
核心优化:复用资源+多线程解码
- 将格式转换上下文、编码器上下文、帧资源初始化一次,全程复用
- 启用多线程解码,利用CPU多核加速
- 移除冗余的内存分配操作,减少性能损耗
修改后的完整代码
#include <cstdio> #include <cstdlib> #include <iostream> extern "C" { #include <libavcodec/avcodec.h> #include <libavformat/avformat.h> #include <libavfilter/buffersink.h> #include <libavfilter/buffersrc.h> #include <libavutil/opt.h> #include <libswscale/swscale.h> #include <libavutil/imgutils.h> } static AVFormatContext *fmt_ctx; static AVCodecContext *dec_ctx; static AVCodecContext *enc_ctx; static SwsContext *sws_ctx; static int video_stream_index = -1; static AVFrame *yuv_frame; // 初始化JPEG编码器 static int init_jpeg_encoder(int width, int height) { const AVCodec *encoder = avcodec_find_encoder(AV_CODEC_ID_MJPEG); if (!encoder) { fprintf(stderr, "MJPEG encoder not found\n"); return -1; } enc_ctx = avcodec_alloc_context3(encoder); if (!enc_ctx) { fprintf(stderr, "Failed to allocate encoder context\n"); return -1; } // 配置编码器参数 enc_ctx->pix_fmt = AV_PIX_FMT_YUVJ420P; enc_ctx->width = width; enc_ctx->height = height; enc_ctx->time_base = (AVRational){1, 1}; // JPEG无需时间基,随意设置 enc_ctx->bit_rate = 1000000; // 比特率,可调整平衡质量与体积 // 设置压缩质量(0-31,值越小质量越高、体积越大) av_opt_set(enc_ctx->priv_data, "qscale", "2", 0); if (avcodec_open2(enc_ctx, encoder, NULL) < 0) { fprintf(stderr, "Failed to open MJPEG encoder\n"); return -1; } // 分配YUV帧缓冲区 yuv_frame = av_frame_alloc(); yuv_frame->format = enc_ctx->pix_fmt; yuv_frame->width = enc_ctx->width; yuv_frame->height = enc_ctx->height; if (av_frame_get_buffer(yuv_frame, 0) < 0) { fprintf(stderr, "Failed to allocate YUV frame buffer\n"); return -1; } // 创建格式转换上下文(从视频像素格式转YUVJ420P) sws_ctx = sws_getContext(dec_ctx->width, dec_ctx->height, dec_ctx->pix_fmt, enc_ctx->width, enc_ctx->height, enc_ctx->pix_fmt, SWS_BILINEAR, NULL, NULL, NULL); if (!sws_ctx) { fprintf(stderr, "Failed to create SwsContext\n"); return -1; } return 0; } // 编码YUV帧为JPEG并保存到文件 static int save_jpeg(const char *file_name) { AVPacket pkt = {0}; av_init_packet(&pkt); // 发送帧到编码器 int ret = avcodec_send_frame(enc_ctx, yuv_frame); if (ret < 0) { fprintf(stderr, "Error sending frame to encoder\n"); return ret; } // 获取编码后的数据包 ret = avcodec_receive_packet(enc_ctx, &pkt); if (ret < 0) { fprintf(stderr, "Error receiving packet from encoder\n"); return ret; } // 写入文件 FILE *file = fopen(file_name, "wb"); if (!file) { fprintf(stderr, "Failed to open file %s\n", file_name); av_packet_unref(&pkt); return -1; } fwrite(pkt.data, 1, pkt.size, file); fclose(file); av_packet_unref(&pkt); return 0; } // 打开输入文件并初始化解码器 static int open_input_file(const char *filename) { int ret; const AVCodec *dec; if((ret = avformat_open_input(&fmt_ctx, filename, NULL, NULL)) < 0) { fprintf(stderr, "ERROR: failed to open input file\n"); return ret; } if((ret = avformat_find_stream_info(fmt_ctx, NULL)) < 0) { fprintf(stderr, "ERROR: failed to find stream information\n"); return ret; } ret = av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, &dec, 0); if(ret < 0) { fprintf(stderr, "ERROR: failed to find a video stream in the input file\n"); return ret; } video_stream_index = ret; dec_ctx = avcodec_alloc_context3(dec); if(!dec_ctx) { fprintf(stderr, "ERROR: failed to allocate decoding context\n"); return AVERROR(ENOMEM); } avcodec_parameters_to_context(dec_ctx, fmt_ctx->streams[video_stream_index]->codecpar); // 启用多线程解码,利用所有CPU核心 dec_ctx->thread_count = av_cpu_count(); if((ret = avcodec_open2(dec_ctx, dec, NULL)) < 0) { fprintf(stderr, "ERROR: failed to open video decoder\n"); return ret; } // 初始化JPEG编码器 if (init_jpeg_encoder(dec_ctx->width, dec_ctx->height) < 0) { return -1; } return 0; } // 解码帧并处理 static void decode(AVCodecContext *cxt, AVFrame *frame, AVPacket *pkt, const char *out_file_name, int mod=1) { char buf[1024]; int ret; ret = avcodec_send_packet(cxt, pkt); if(ret < 0) { fprintf(stderr, "ERROR: error sending packet for decoding\n"); exit(1); } while(ret >= 0) { ret = avcodec_receive_frame(cxt, frame); if(ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) { return; } else if(ret < 0) { fprintf(stderr, "ERROR: error during decoding\n"); exit(1); } // 筛选每mod帧保存 if(cxt->frame_number % mod == 0){ printf("saving frame %03d\n", cxt->frame_number); fflush(stdout); // 转换帧格式到YUVJ420P sws_scale(sws_ctx, frame->data, frame->linesize, 0, frame->height, yuv_frame->data, yuv_frame->linesize); snprintf(buf, sizeof(buf), "%s_%03d.jpg", out_file_name, cxt->frame_number); save_jpeg(buf); } } } int main() { char argv0[] = "test"; char argv1[] = "/User/Desktop/frames/test_video.mov"; char *argv[] = {argv0, argv1, nullptr}; int ret; AVPacket *packet; AVFrame *frame; frame = av_frame_alloc(); packet = av_packet_alloc(); if (!frame || !packet) { fprintf(stderr, "Could not allocate frame or packet\n"); exit(1); } if((ret = open_input_file(argv[1])) < 0) { goto end; } while(av_read_frame(fmt_ctx, packet) >= 0) { if (packet->stream_index == video_stream_index) { std::string name = "/User/Desktop/frames/img"; decode(dec_ctx, frame, packet, name.c_str(), 5); } av_packet_unref(packet); } end: // 释放所有资源 avcodec_free_context(&dec_ctx); avcodec_free_context(&enc_ctx); avformat_close_input(&fmt_ctx); av_frame_free(&frame); av_frame_free(&yuv_frame); av_packet_free(&packet); sws_freeContext(sws_ctx); if (ret < 0 && ret != AVERROR_EOF) { fprintf(stderr, "Error occurred: %s\n", av_err2str(ret)); exit(1); } exit(0); }
关键修改说明
体积优化:
- 替换原始PPM保存逻辑,改用MJPEG编码器生成压缩JPG
- 通过
qscale参数控制压缩质量,值设为2-5时可达到和命令行相近的体积(800KB左右) - 转换帧为JPEG标准的YUVJ420P格式,避免RGB转码的额外开销
效率优化:
- 复用格式转换上下文、编码器上下文、YUV帧等资源,避免重复分配内存
- 启用多线程解码,利用所有CPU核心提升处理速度
- 移除每次
decode调用时的冗余资源创建,减少内存操作开销
可选进阶优化:
- 集成FFmpeg滤镜链,使用
select滤镜提前过滤不需要的帧,进一步减少解码算力消耗,完全对齐命令行逻辑。
- 集成FFmpeg滤镜链,使用
内容的提问来源于stack exchange,提问作者dark-walrus
相关产品推荐
相关产品推荐

