使用ffmpeg从QImages创建GIF

时间:2016-06-16 14:14:31

标签: c++ qt ffmpeg gif



我想使用ffmpeg从QImage生成GIF - 所有这些以编程方式(C ++)。我正在使用Qt 5.6和ffmpeg的最后一次构建(构建git-0a9e781(2016-06-10)。

我已经能够在.mp4中转换这些QImage并且它可以工作。我尝试对GIF使用相同的原理,改变格式像素和编解码器。 GIF由两张图片(每张1秒)生成,15 FPS。

## INITIALIZATION
#####################################################################

// Filepath : "C:/Users/.../qt_temp.Jv7868.gif"  
// Allocating an AVFormatContext for an output format...
avformat_alloc_output_context2(formatContext, NULL, NULL, filepath);

...

// Adding the video streams using the default format codecs and initializing the codecs.
stream = avformat_new_stream(formatContext, *codec);

AVCodecContext * codecContext = avcodec_alloc_context3(*codec);

context->codec_id       = codecId;
context->bit_rate       = 400000;
...
context->pix_fmt        = AV_PIX_FMT_BGR8;

...

// Opening the codec...
avcodec_open2(codecContext, codec, NULL);

...

frame = allocPicture(codecContext->width, codecContext->height, codecContext->pix_fmt);
tmpFrame = allocPicture(codecContext->width, codecContext->height, AV_PIX_FMT_RGBA);

...

avformat_write_header(formatContext, NULL);

## ADDING A NEW FRAME
#####################################################################

// Getting in parameter the QImage: newFrame(const QImage & image)
const qint32 width  = image.width();
const qint32 height = image.height();

// Converting QImage into AVFrame
for (qint32 y = 0; y < height; y++) {
    const uint8_t * scanline = image.scanLine(y);

    for (qint32 x = 0; x < width * 4; x++) {
        tmpFrame->data[0][y * tmpFrame->linesize[0] + x] = scanline[x];
    }
}

...

// Scaling...
if (codec->pix_fmt != AV_PIX_FMT_BGRA) {
    if (!swsCtx) {
        swsCtx = sws_getContext(codec->width, codec->height,
                                AV_PIX_FMT_BGRA,
                                codec->width, codec->height,
                                codec->pix_fmt,
                                SWS_BICUBIC, NULL, NULL, NULL);
    }

    sws_scale(swsCtx,
              (const uint8_t * const *)tmpFrame->data,
              tmpFrame->linesize,
              0,
              codec->height,
              frame->data,
              frame->linesize);
}
frame->pts = nextPts++;

...

int gotPacket = 0;
AVPacket packet = {0};

av_init_packet(&packet);
avcodec_encode_video2(codec, &packet, frame, &gotPacket);

if (gotPacket) {
    av_packet_rescale_ts(paket, *codec->time_base, stream->time_base);
    paket->stream_index = stream->index;

    av_interleaved_write_frame(formatContext, paket);
}

但是当我尝试修改视频编解码器和像素格式以符合GIF规范时,我遇到了一些问题。 我尝试了几个编解码器,例如AV_CODEC_ID_GIFAV_CODEC_ID_RAWVIDEO,但它们似乎都不起作用。在初始化阶段,avcodec_open2()始终返回此类错误:

Specified pixel format rgb24 is invalid or not supported
Could not open video codec:  gif

编辑2016年6月17日

再挖一点,avcodec_open2()返回-22:

#define EINVAL          22      /* Invalid argument */

编辑22/06/2016

以下是用于编译ffmpeg的标志:

"FFmpeg/Libav configuration: --disable-static --enable-shared --enable-gpl --enable-version3 --disable-w32threads --enable-nvenc --enable-avisynth --enable-bzlib --enable-fontconfig --enable-frei0r --enable-gnutls --enable-iconv --enable-libass --enable-libbluray --enable-libbs2b --enable-libcaca --enable-libfreetype --enable-libgme --enable-libgsm --enable-libilbc --enable-libmodplug --enable-libmfx --enable-libmp3lame --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenjpeg --enable-libopus --enable-librtmp --enable-libschroedinger --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libtheora --enable-libtwolame --enable-libvidstab --enable-libvo-amrwbenc --enable-libvorbis --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxavs --enable-libxvid --enable-libzimg --enable-lzma --enable-decklink --enable-zlib"

我是否错过了GIF的关键作品?

编辑2016年6月27日

感谢Gwen,我有第一个输出:我将context->pix_fmt设置为AV_PIX_FMT_BGR8。顺便说一下,我仍然面临生成的GIF的一些问题。它没有播放,编码似乎失败了。

使用ffmpeg(左)在命令行中生成GIF。 。 。 以编程方式生成GIF(右)
Generated in command line with ffmpeg enter image description here

看起来某些选项未定义...也可能是QImage和AVFrame之间的错误转换?我更新了上面的代码。它代表了很多代码,所以我试图保持简短。不要犹豫,询问更多细节。

编辑结束

我对ffmpeg并不熟悉,任何形式的帮助都会受到高度赞赏。谢谢。

3 个答案:

答案 0 :(得分:4)

GIF只能支持256色位图(每像素8位)。这可能是您遇到Specified pixel format rgb24 is invalid or not supported错误的原因。

您需要使用的像素格式为AV_PIX_FMT_PAL88 bit with RGB32 palette)。

答案 1 :(得分:2)

这是一种使用ffmpeg将QImage转换为GIF格式的方法。我试图尽可能清楚地删除错误。

首先,初始化ffmpeg:

AVOutputFormat  * outputFormat  = Q_NULLPTR;
AVFormatContext * formatContext = Q_NULLPTR;

avformat_alloc_output_context2(&formatContext, NULL, NULL, filePath.data()); // i.e. filePath="C:/Users/.../qt_temp.Jv7868.mp4" 

// Adding the video streams using the default format codecs and initializing the codecs...
outputFormat = formatContext->oformat;
if (outputFormat->video_codec != AV_CODEC_ID_NONE) {
    // Finding a registered encoder with a matching codec ID...
    *codec = avcodec_find_encoder(outputFormat->video_codec);

    // Adding a new stream to a media file...
    stream = avformat_new_stream(formatContext, *codec);
    stream->id = formatContext->nb_streams - 1;


    AVCodecContext * codecContext = avcodec_alloc_context3(*codec);

    switch ((*codec)->type) {
    case AVMEDIA_TYPE_VIDEO:
        codecContext->codec_id  = outputFormat->video_codec; // here, outputFormat->video_codec should be AV_CODEC_ID_GIF
        codecContext->bit_rate  = 400000;

        codecContext->width     = 1240;
        codecContext->height    = 874;

        codecContext->pix_fmt   = AV_PIX_FMT_RGB8;

        ...

        // Timebase: this is the fundamental unit of time (in seconds) in terms of which frame
        // timestamps are represented. For fixed-fps content, timebase should be 1/framerate
        // and timestamp increments should be identical to 1.
        stream->time_base       = (AVRational){1, fps}; // i.e. fps=1
        codecContext->time_base = stream->time_base;

        // Emit 1 intra frame every 12 frames at most
        codecContext->gop_size  = 12;
        codecContext->pix_fmt   = AV_PIX_FMT_YUV420P;

        if (codecContext->codec_id == AV_CODEC_ID_H264) {
            av_opt_set(codecContext->priv_data, "preset", "slow", 0);
        }
        break;
    }

    if (formatContext->oformat->flags & AVFMT_GLOBALHEADER) {
        codecContext->flags |= CODEC_FLAG_GLOBAL_HEADER;
    }
}

avcodec_open2(codecContext, codec, NULL);

// Here we need 3 frames. Basically, the QImage is firstly extracted in AV_PIX_FMT_BGRA.
// We need then to convert it to AV_PIX_FMT_RGB8 which is required by the .gif format.
// If we do that directly, there will be some artefacts and bad effects... to prevent that
// we convert FIRST AV_PIX_FMT_BGRA into AV_PIX_FMT_YUV420P THEN into AV_PIX_FMT_RGB8.
frame = allocPicture(codecContext->width, codecContext->height, codecContext->pix_fmt); // here, codecContext->pix_fmt should be AV_PIX_FMT_RGB8
tmpFrame = allocPicture(codecContext->width, codecContext->height, AV_PIX_FMT_BGRA);
yuvFrame = allocPicture(codecContext->width, codecContext->height, AV_PIX_FMT_YUV420P);

avcodec_parameters_from_context(stream->codecpar, codecContext);

av_dump_format(formatContext, 0, filePath.data(), 1);

if (!(outputFormat->flags & AVFMT_NOFILE)) {
    avio_open(&formatContext->pb, filePath.data(), AVIO_FLAG_WRITE);
}

// Writing the stream header, if any...
avformat_write_header(formatContext, NULL);

然后是主要部分,添加一个QImage(例如从循环接收):

// -> parameter: QImage image
const qint32 width  = image.width();
const qint32 height = image.height();

// When we pass a frame to the encoder, it may keep a reference to it internally;
// make sure we do not overwrite it here!
av_frame_make_writable(tmpFrame);

// Converting QImage to AV_PIX_FMT_BGRA AVFrame ...
for (qint32 y = 0; y < height(); y++) {
    const uint8_t * scanline = image.scanLine(y);

    for (qint32 x = 0; x < width() * 4; x++) {
        tmpFrame->data[0][y * tmpFrame->linesize[0] + x] = scanline[x];
    }
}

// Make sure to clear the frame. It prevents a bug that displays only the
// first captured frame on the GIF export.
if (frame) {
    av_frame_free(&frame);
    frame = Q_NULLPTR;
}
frame = allocPicture(codecContext->width, codecContext->height, codecContext->pix_fmt);

if (yuvFrame) {
    av_frame_free(&yuvFrame);
    yuvFrame = Q_NULLPTR;
}
yuvFrame = allocPicture(codecContext->width, codecContext->height, AV_PIX_FMT_YUV420P);

// Converting BGRA -> YUV420P...
if (!swsCtx) {
    swsCtx = sws_getContext(width, height,
                            AV_PIX_FMT_BGRA,
                            width, height,
                            AV_PIX_FMT_YUV420P,
                            swsFlags, NULL, NULL, NULL);
}

// ...then converting YUV420P -> RGB8 (natif GIF format pixel)
if (!swsGIFCtx) {
    swsGIFCtx = sws_getContext(width, height,
                                AV_PIX_FMT_YUV420P,
                                codecContext->width, codecContext->height,
                                codecContext->pix_fmt,
                                this->swsFlags, NULL, NULL, NULL);
}

// This double scaling prevent some artifacts on the GIF and improve
// significantly the display quality
sws_scale(swsCtx,
          (const uint8_t * const *)tmpFrame->data,
          tmpFrame->linesize,
          0,
          codecContext->height,
          yuvFrame->data,
          yuvFrame->linesize);
sws_scale(swsGIFCtx,
          (const uint8_t * const *)yuvFrame->data,
          yuvFrame->linesize,
          0,
          codecContext->height,
          frame->data,
          frame->linesize);

...

AVPacket packet;
int gotPacket = 0;

av_init_packet(&packet);

// Packet data will be allocated by the encoder
packet.data = NULL;
packet.size = 0;

frame->pts = nextPts++; // nextPts starts at 0
avcodec_encode_video2(codecContext, &packet, frame, &gotPacket);

if (gotPacket) {
    // Rescale output packet timestamp values from codec to stream timebase
    av_packet_rescale_ts(packet, *codecContext->time_base, stream->time_base);
    packet->stream_index = stream->index;

    // Write the compressed frame to the media file.
    av_interleaved_write_frame(formatContext, packet);

    av_packet_unref(&this->packet);
}

关闭ffmpeg:

// Retrieving delayed frames if any...
// Note: mainly used for video generation, it might be useless for .gif.
for (int gotOutput = 1; gotOutput;) {
    avcodec_encode_video2(codecContext, &packet, NULL, &gotOutput);

    if (gotOutput) {
        // Rescale output packet timestamp values from codec to stream timebase
        av_packet_rescale_ts(packet, *codecContext->time_base, stream->time_base);
        packet->stream_index = stream->index;

        // Write the compressed frame to the media file.
        av_interleaved_write_frame(formatContext, packet);
        av_packet_unref(&packet);
    }
}

av_write_trailer(formatContext);

avcodec_free_context(&codecContext);
av_frame_free(&frame);
av_frame_free(&tmpFrame);
sws_freeContext(swsCtx);

if (!(outputFormat->flags & AVFMT_NOFILE)) {
    // Closing the output file...
    avio_closep(&formatContext->pb);
}

avformat_free_context(formatContext);

我不认为这是最简单的方法,但至少它对我有用。我让问题公开。请随时评论/改进/回答这个问题。

答案 2 :(得分:0)

我根据@Sierra的代码片段制作了一个更完善的c ++类。它不使用任何不推荐使用的ffmpeg函数,因此目前应该是面向未来的。该类分为三个部分:一个将GIF图像标头写入GIF文件的函数,一个在帧位置插入(可能已裁剪的)图像的函数,以及将帧提交到文件中,刷新和删除分配的ffmpeg结构。

关于该课程的几点注意事项:

  • This answer很好地解释了为什么最大帧速率为100FPS:

GIF98a规范在有关图形控件扩展的部分中进行了说明

vii)延迟时间-如果不为0,则该字段指定在继续处理数据流之前要等待的百分之一(1/100)秒。渲染图形后,时钟立即开始滴答。此字段可以与“用户输入标志”字段一起使用。

这意味着对于100 FPS的图像速率,最小值为1(这将很难在大多数监视器上呈现...),对于50 FPS的图像速率,下一个最小值为2,而下一个为对于33.3 FPS的图像速率,该值为3。因此,完全不可能达到60 FPS。

换句话说,实际上以100 FPS保存了60 FPS GIF,以50 FPS保存了30 FPS,像OP试图将其用作16.6 FPS一样保存了15 FPS。

  • 我已经注意到GIF中尺寸奇异的黄色瑕疵。因此,我的课程剪掉了宽度/长度的额外像素。

  • AV_PIX_FMT_BGR8框架为我工作,这就是我的课堂创造的。

  • 最后一个也是最重要的一点,必须按顺序指定帧号,从1开始。尝试不按顺序添加帧将使类中止编码过程,直到下一个GIFExporter::commitFile()调用为止。之所以这样实现是因为,我是从QOpenGLWidget帧缓冲区中抓取图像的,如果绘制了太多的对象,它们会滞后,从而导致某些帧丢失。

下面的Github要点代码:

<script src="https://gist.github.com/ZenulAbidin/4fb0047e459bd929a7c02784f78daca1.js"></script>