You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用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格式,而非直接保存原始像素:

  1. 初始化JPEG编码器上下文,配置压缩参数
  2. 将解码帧转换为JPEG支持的YUVJ420P格式
  3. 编码YUV帧为JPEG数据包并写入文件

二、程序效率低下的原因及优化方案

当前代码存在多处低效点:

  • 每次调用decode都重新创建SwsContext和AVFrame,重复分配/释放内存
  • 默认单线程解码,未利用多核CPU资源
  • 先解码所有帧再筛选,浪费了不必要的解码算力(命令行用select滤镜提前过滤,只解码需要的帧)

核心优化:复用资源+多线程解码

  1. 将格式转换上下文、编码器上下文、帧资源初始化一次,全程复用
  2. 启用多线程解码,利用CPU多核加速
  3. 移除冗余的内存分配操作,减少性能损耗

修改后的完整代码

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

关键修改说明

  1. 体积优化:

    • 替换原始PPM保存逻辑,改用MJPEG编码器生成压缩JPG
    • 通过qscale参数控制压缩质量,值设为2-5时可达到和命令行相近的体积(800KB左右)
    • 转换帧为JPEG标准的YUVJ420P格式,避免RGB转码的额外开销
  2. 效率优化:

    • 复用格式转换上下文、编码器上下文、YUV帧等资源,避免重复分配内存
    • 启用多线程解码,利用所有CPU核心提升处理速度
    • 移除每次decode调用时的冗余资源创建,减少内存操作开销
  3. 可选进阶优化:

    • 集成FFmpeg滤镜链,使用select滤镜提前过滤不需要的帧,进一步减少解码算力消耗,完全对齐命令行逻辑。

内容的提问来源于stack exchange,提问作者dark-walrus

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.14 01:20:38