如何使用 FFmpeg 在 C++ 中将 AVFrame 保存为图像

How to save AVFrame as image in C++ using FFmpeg

在我的项目中,我想从 Hevc 文件中保存其中一帧。我在源代码中使用 FFmpeg 来解码 Hevc 文件并获取 AVFrame 和 AVCodecContext。 我需要的是将框架保存为图片(全彩)。

我试着把它保存为*.pgm文件,所以图片是灰色的,这不是我真正需要的。

有什么建议吗?谢谢!

void HevcDecoder::Images_Save(char* filename, AVFrame *frame)
{
    FILE* file;
    int i;

    fopen_s(&file, filename, "wb");
    fprintf(file, "P5\n%d %d\n%d\n", frame->width, frame->height, 255);
    for (i = 0; i < frame->height; i++)
        fwrite(frame->data[0] + i * frame->linesize[0], 1, frame->width, file);

    fclose(file);
}

void HevcDecoder::Decode(AVCodecContext* dec_ctx, AVFrame* frame, AVPacket* pkt, const char* filename)
{
    char buf[1024];
    int ret;

    ret = avcodec_send_packet(dec_ctx, pkt);
    if (ret < 0) {
        fprintf(stderr, "Error sending a packet for decoding\n");
        exit(1);
    }

    while (ret >= 0) {
        ret = avcodec_receive_frame(dec_ctx, frame);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
            return;
        else if (ret < 0) {
            fprintf(stderr, "Error during decoding\n");
            exit(1);
        }

        printf("saving frame %3d\n", dec_ctx->frame_number);
        fflush(stdout);

        /* the picture is allocated by the decoder. no need to
           free it */
        snprintf(buf, sizeof(buf), "%s-%d.pgm", filename, dec_ctx->frame_number);
        Images_Save(buf, frame/*, dec_ctx*/);
    }
}

使用 FFmpeg CLI 将原始 HEVC 文件转换为图像序列很简单。

假设 input.265 是输入文件(原始 HEVC 视频流):
转换为 PNG 图像:

ffmpeg -i input.265 %05d.png

正在转换为 PPM 图像:

ffmpeg -i input.265 %05d.ppm

如果输入视频使用 MP4 容器并且您需要 JPEG 图像:

ffmpeg -i input.265 %05d.jpg


使用 FFmpeg C 接口 (Libav):

为了使内容可重现,首先使用 FFmpeg CLI 创建输入视频文件:

ffmpeg -y -f lavfi -i testsrc=size=192x108:rate=1:duration=10 -vcodec libx265 -pix_fmt yuv420p input.265

上述命令创建 HEVC (H.265) 编码流 - 10 帧,分辨率为 192x108,像素格式为 YUV420(合成模式)。
编码流为原始视频流(无容器)。

注:

  • RAW HEVC (H.265) 视频流不是常用的文件格式。
    通常流由容器包装(如 MP4 / MKV / AVI ...)。
    我们将原始视频流用于教育目的 - 用于解码的代码更简单。

将图像保存为彩色图像:

代码示例重用了 this post 中的代码。

  • PGM 是一种灰度格式,对于等效的颜色格式我们可以使用 PPM format
  • 我们可以使用 SWS Scale 将格式从 YUV420 转换为 RGB。

我们可以使用

中的代码示例

这是代码示例:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

extern "C" {
#include <libavcodec/avcodec.h>
#include <libswscale/swscale.h>
}

#define INBUF_SIZE 1024

//static void pgm_save(unsigned char* buf, int wrap, int xsize, int ysize, char* filename)
//{
//    FILE* f;
//    int i;
//
//    f = fopen(filename, "wb");
//    fprintf(f, "P5\n%d %d\n%d\n", xsize, ysize, 255);
//    for (i = 0; i < ysize; i++)
//        fwrite(buf + i * wrap, 1, xsize, f);
//    fclose(f);
//}


//Save RGB image as PPM file format
static void ppm_save(unsigned char* buf, int wrap, int xsize, int ysize, char* filename)
{
    FILE* f;
    int i;

    f = fopen(filename, "wb");
    fprintf(f, "P6\n%d %d\n%d\n", xsize, ysize, 255);

    for (i = 0; i < ysize; i++)
    {
        fwrite(buf + i * wrap, 1, xsize*3, f);
    }

    fclose(f);
}


static void decode(AVCodecContext* dec_ctx, AVFrame* frame, AVPacket* pkt, const char* filename)
{
    struct SwsContext* sws_ctx = NULL;
    char buf[1024];
    int ret;
    int sts;

    ret = avcodec_send_packet(dec_ctx, pkt);
    if (ret < 0)
    {
        fprintf(stderr, "Error sending a packet for decoding\n");
        exit(1);
    }

    //Create SWS Context for converting from decode pixel format (like YUV420) to RGB
    ////////////////////////////////////////////////////////////////////////////
    sws_ctx = sws_getContext(dec_ctx->width,
                             dec_ctx->height,
                             dec_ctx->pix_fmt,
                             dec_ctx->width,
                             dec_ctx->height,
                             AV_PIX_FMT_RGB24,
                             SWS_BICUBIC,
                             NULL,
                             NULL,
                             NULL);

    if (sws_ctx == nullptr)
    {
        return;  //Error!
    }
    ////////////////////////////////////////////////////////////////////////////


    //Allocate frame for storing image converted to RGB.
    ////////////////////////////////////////////////////////////////////////////
    AVFrame* pRGBFrame = av_frame_alloc();

    pRGBFrame->format = AV_PIX_FMT_RGB24;
    pRGBFrame->width = dec_ctx->width;
    pRGBFrame->height = dec_ctx->height;

    sts = av_frame_get_buffer(pRGBFrame, 0);

    if (sts < 0)
    {
        return;  //Error!
    }
    ////////////////////////////////////////////////////////////////////////////


    while (ret >= 0) 
    {
        ret = avcodec_receive_frame(dec_ctx, frame);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
        {
            return;
        }
        else if (ret < 0) 
        {
            fprintf(stderr, "Error during decoding\n");
            exit(1);
        }

        printf("saving frame %3d\n", dec_ctx->frame_number);
        fflush(stdout);

        /* the picture is allocated by the decoder. no need to
           free it */
        //snprintf(buf, sizeof(buf), "%s_%03d.pgm", filename, dec_ctx->frame_number);
        //pgm_save(frame->data[0], frame->linesize[0],
        //    frame->width, frame->height, buf);
         
        //Convert from input format (e.g YUV420) to RGB and save to PPM:
        ////////////////////////////////////////////////////////////////////////////
        sts = sws_scale(sws_ctx,                //struct SwsContext* c,
                        frame->data,            //const uint8_t* const srcSlice[],
                        frame->linesize,        //const int srcStride[],
                        0,                      //int srcSliceY, 
                        frame->height,          //int srcSliceH,
                        pRGBFrame->data,        //uint8_t* const dst[], 
                        pRGBFrame->linesize);   //const int dstStride[]);

        if (sts != frame->height)
        {
            return;  //Error!
        }

        snprintf(buf, sizeof(buf), "%s_%03d.ppm", filename, dec_ctx->frame_number);
        ppm_save(pRGBFrame->data[0], pRGBFrame->linesize[0], pRGBFrame->width, pRGBFrame->height, buf);
        ////////////////////////////////////////////////////////////////////////////
    }

    //Free
    sws_freeContext(sws_ctx);
    av_frame_free(&pRGBFrame);
}

int main(int argc, char** argv)
{
    const char* filename, * outfilename;
    const AVCodec* codec;
    AVCodecParserContext* parser;
    AVCodecContext* c = NULL;
    FILE* f;
    AVFrame* frame;
    uint8_t inbuf[INBUF_SIZE + AV_INPUT_BUFFER_PADDING_SIZE];
    uint8_t* data;
    size_t   data_size;
    int ret;
    AVPacket* pkt;    

    filename = argv[1];
    outfilename = argv[2];

    pkt = av_packet_alloc();
    if (!pkt)
    {
        exit(1);
    }

    //memset(inbuf + INBUF_SIZE, 0, AV_INPUT_BUFFER_PADDING_SIZE);
    memset(inbuf, 0, sizeof(inbuf));

    codec = avcodec_find_decoder(AV_CODEC_ID_HEVC);
    if (!codec)
    {
        fprintf(stderr, "Codec not found\n");
        exit(1);
    }

    parser = av_parser_init(codec->id);
    if (!parser)
    {
        fprintf(stderr, "parser not found\n");
        exit(1);
    }

    c = avcodec_alloc_context3(codec);
    if (!c)
    {
        fprintf(stderr, "Could not allocate video codec context\n");
        exit(1);
    }

    if (avcodec_open2(c, codec, NULL) < 0)
    {
        fprintf(stderr, "Could not open codec\n");
        exit(1);
    }

    f = fopen(filename, "rb");
    if (!f)
    {
        fprintf(stderr, "Could not open %s\n", filename);
        exit(1);
    }

    frame = av_frame_alloc();
    if (!frame)
    {
        fprintf(stderr, "Could not allocate video frame\n");
        exit(1);
    }

    while (!feof(f))
    {
        /* read raw data from the input file */
        data_size = fread(inbuf, 1, INBUF_SIZE, f);

        if (!data_size)
        {
            break;
        }

        /* use the parser to split the data into frames */
        data = inbuf;
        while (data_size > 0) 
        {
            ret = av_parser_parse2(parser, c, &pkt->data, &pkt->size, data, (int)data_size, AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0);

            if (ret < 0)
            {
                fprintf(stderr, "Error while parsing\n");
                exit(1);
            }

            data += ret;
            data_size -= ret;


            if (pkt->data) 
            {
                printf("NICE\n");
                decode(c, frame, pkt, outfilename);
            }
        }
    }

    /* flush the decoder */
    decode(c, frame, NULL, outfilename);

    fclose(f);

    av_parser_close(parser);
    avcodec_free_context(&c);
    av_frame_free(&frame);
    av_packet_free(&pkt);

    return 0;
}

使用 OpenCV 显示图像:

显示图像的最简单方法之一是使用 OpenCV 库。

首次设置同时使用 FFmpeg 和 OpenCV 的项目可能具有挑战性。

  • 我们需要图像为 BGR 格式。
  • 要显示图像,请使用:cv::imshow 后跟 cv::waitKey

代码示例:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

//Use OpenCV for showing the inage
#include <opencv2/opencv.hpp>
#include <opencv2/highgui.hpp>


extern "C" {
#include <libavcodec/avcodec.h>
#include <libswscale/swscale.h>
}

#define INBUF_SIZE 1024

//static void pgm_save(unsigned char* buf, int wrap, int xsize, int ysize, char* filename)
//{
//    FILE* f;
//    int i;
//
//    f = fopen(filename, "wb");
//    fprintf(f, "P5\n%d %d\n%d\n", xsize, ysize, 255);
//    for (i = 0; i < ysize; i++)
//        fwrite(buf + i * wrap, 1, xsize, f);
//    fclose(f);
//}


//Save RGB image as PPM file format
//static void ppm_save(unsigned char* buf, int wrap, int xsize, int ysize, char* filename)
//{
//    FILE* f;
//    int i;
//
//    f = fopen(filename, "wb");
//    fprintf(f, "P6\n%d %d\n%d\n", xsize, ysize, 255);
//
//    for (i = 0; i < ysize; i++)
//    {
//        fwrite(buf + i * wrap, 1, xsize*3, f);
//    }
//
//    fclose(f);
//}


static void decode(AVCodecContext* dec_ctx, AVFrame* frame, AVPacket* pkt, const char* filename)
{
    struct SwsContext* sws_ctx = NULL;
    char filename_buf[1024];
    int ret;
    int sts;

    ret = avcodec_send_packet(dec_ctx, pkt);
    if (ret < 0)
    {
        fprintf(stderr, "Error sending a packet for decoding\n");
        exit(1);
    }

    //Create SWS Context for converting from decode pixel format (like YUV420) to BGR
    ////////////////////////////////////////////////////////////////////////////
    sws_ctx = sws_getContext(dec_ctx->width,
                             dec_ctx->height,
                             dec_ctx->pix_fmt,
                             dec_ctx->width,
                             dec_ctx->height,
                             AV_PIX_FMT_BGR24, //For OpenCV, we want BGR pixel format.
                             SWS_BICUBIC,
                             NULL,
                             NULL,
                             NULL);

    if (sws_ctx == nullptr)
    {
        return;  //Error!
    }
    ////////////////////////////////////////////////////////////////////////////


    //Allocate frame for storing image converted to RGB.
    ////////////////////////////////////////////////////////////////////////////
    AVFrame* pBGRFrame = av_frame_alloc();

    pBGRFrame->format = AV_PIX_FMT_BGR24;
    pBGRFrame->width = dec_ctx->width;
    pBGRFrame->height = dec_ctx->height;

    sts = av_frame_get_buffer(pBGRFrame, 0);

    if (sts < 0)
    {
        return;  //Error!
    }
    ////////////////////////////////////////////////////////////////////////////


    while (ret >= 0) 
    {
        ret = avcodec_receive_frame(dec_ctx, frame);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
        {
            return;
        }
        else if (ret < 0) 
        {
            fprintf(stderr, "Error during decoding\n");
            exit(1);
        }

        printf("saving frame %3d\n", dec_ctx->frame_number);
        fflush(stdout);

        /* the picture is allocated by the decoder. no need to
           free it */
        //snprintf(buf, sizeof(buf), "%s_%03d.pgm", filename, dec_ctx->frame_number);
        //pgm_save(frame->data[0], frame->linesize[0],
        //    frame->width, frame->height, buf);
         
        //Convert from input format (e.g YUV420) to BGR:
        ////////////////////////////////////////////////////////////////////////////
        sts = sws_scale(sws_ctx,                //struct SwsContext* c,
                        frame->data,            //const uint8_t* const srcSlice[],
                        frame->linesize,        //const int srcStride[],
                        0,                      //int srcSliceY, 
                        frame->height,          //int srcSliceH,
                        pBGRFrame->data,        //uint8_t* const dst[], 
                        pBGRFrame->linesize);   //const int dstStride[]);

        if (sts != frame->height)
        {
            return;  //Error!
        }

        snprintf(filename_buf, sizeof(filename_buf), "%s_%03d.jpg", filename, dec_ctx->frame_number);
        //ppm_save(pBGRFrame->data[0], pBGRFrame->linesize[0], pBGRFrame->width, pBGRFrame->height, buf);        
        ////////////////////////////////////////////////////////////////////////////

        //Use OpenCV for showing the image (and save the image in JPEG format):
        ////////////////////////////////////////////////////////////////////////////
        cv::Mat img = cv::Mat(pBGRFrame->height, pBGRFrame->width, CV_8UC3, pBGRFrame->data[0], pBGRFrame->linesize[0]);    //cv::Mat is OpenCV "thin image wrapper".
        cv::imshow("img", img);
        cv::waitKey(100);   //Wait 100msec (relativly long time - for testing).

        //Save the inage in JPEG format using OpenCV
        cv::imwrite(filename_buf, img);
        ////////////////////////////////////////////////////////////////////////////

    }

    //Free
    sws_freeContext(sws_ctx);
    av_frame_free(&pBGRFrame);
}

int main(int argc, char** argv)
{
    const char* filename, * outfilename;
    const AVCodec* codec;
    AVCodecParserContext* parser;
    AVCodecContext* c = NULL;
    FILE* f;
    AVFrame* frame;
    uint8_t inbuf[INBUF_SIZE + AV_INPUT_BUFFER_PADDING_SIZE];
    uint8_t* data;
    size_t   data_size;
    int ret;
    AVPacket* pkt;    

    filename = argv[1];
    outfilename = argv[2];

    pkt = av_packet_alloc();
    if (!pkt)
    {
        exit(1);
    }

    //memset(inbuf + INBUF_SIZE, 0, AV_INPUT_BUFFER_PADDING_SIZE);
    memset(inbuf, 0, sizeof(inbuf));

    codec = avcodec_find_decoder(AV_CODEC_ID_HEVC);
    if (!codec)
    {
        fprintf(stderr, "Codec not found\n");
        exit(1);
    }

    parser = av_parser_init(codec->id);
    if (!parser)
    {
        fprintf(stderr, "parser not found\n");
        exit(1);
    }

    c = avcodec_alloc_context3(codec);
    if (!c)
    {
        fprintf(stderr, "Could not allocate video codec context\n");
        exit(1);
    }

    if (avcodec_open2(c, codec, NULL) < 0)
    {
        fprintf(stderr, "Could not open codec\n");
        exit(1);
    }

    f = fopen(filename, "rb");
    if (!f)
    {
        fprintf(stderr, "Could not open %s\n", filename);
        exit(1);
    }

    frame = av_frame_alloc();
    if (!frame)
    {
        fprintf(stderr, "Could not allocate video frame\n");
        exit(1);
    }

    while (!feof(f))
    {
        /* read raw data from the input file */
        data_size = fread(inbuf, 1, INBUF_SIZE, f);

        if (!data_size)
        {
            break;
        }

        /* use the parser to split the data into frames */
        data = inbuf;
        while (data_size > 0) 
        {
            ret = av_parser_parse2(parser, c, &pkt->data, &pkt->size, data, (int)data_size, AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0);

            if (ret < 0)
            {
                fprintf(stderr, "Error while parsing\n");
                exit(1);
            }

            data += ret;
            data_size -= ret;


            if (pkt->data) 
            {
                printf("NICE\n");
                decode(c, frame, pkt, outfilename);
            }
        }
    }

    /* flush the decoder */
    decode(c, frame, NULL, outfilename);

    fclose(f);

    av_parser_close(parser);
    avcodec_free_context(&c);
    av_frame_free(&frame);
    av_packet_free(&pkt);

    return 0;
}

示例输出:

output_001.jpg:

output_002.jpg:

output_003.jpg: