高效转换 AVFrame 为 QImage

6

我需要在我的基于Qt的应用程序中从视频中提取帧。使用ffmpeg库,我可以获取AVFrames作为帧,我需要将其转换为QImage以在应用程序的其他部分中使用。这种转换需要高效。到目前为止,似乎sws_scale()是正确的函数,但我不确定要指定什么样的源和目标像素格式。


转换为 QImage 不是非常高效的... http://www.qtcentre.org/threads/9935-QImage-data-via-FFmpeg - vipw
6个回答

5
提出以下两步流程,首先将已解码的AVFame转换为另一个RGB颜色空间中的AVFrame,然后再转换为QImage。这个方法有效且运行速度较快。
src_frame = get_decoded_frame();

AVFrame *pFrameRGB = avcodec_alloc_frame(); // intermediate pframe
if(pFrameRGB==NULL) {
    ;// Handle error
}

int numBytes= avpicture_get_size(PIX_FMT_RGB24,
      is->video_st->codec->width, is->video_st->codec->height);
uint8_t *buffer = (uint8_t*)malloc(numBytes);

avpicture_fill((AVPicture*)pFrameRGB, buffer, PIX_FMT_RGB24,
              is->video_st->codec->width, is->video_st->codec->height);

int dst_fmt = PIX_FMT_RGB24;
int dst_w = is->video_st->codec->width;
int dst_h = is->video_st->codec->height;

// TODO: cache following conversion context for speedup,
//       and recalculate only on dimension changes
SwsContext *img_convert_ctx_temp;
img_convert_ctx_temp = sws_getContext(
is->video_st->codec->width, is->video_st->codec->height,
is->video_st->codec->pix_fmt,
dst_w, dst_h, (PixelFormat)dst_fmt,
SWS_BICUBIC, NULL, NULL, NULL);


QImage *myImage = new QImage(dst_w, dst_h, QImage::Format_RGB32);

sws_scale(img_convert_ctx_temp,
          src_frame->data, src_frame->linesize, 0, is->video_st->codec->height,
          pFrameRGB->data,
          pFrameRGB->linesize);

uint8_t *src = (uint8_t *)(pFrameRGB->data[0]);
for (int y = 0; y < dst_h; y++)
{
    QRgb *scanLine = (QRgb *) myImage->scanLine(y);
    for (int x = 0; x < dst_w; x=x+1)
    {
        scanLine[x] = qRgb(src[3*x], src[3*x+1], src[3*x+2]);
    }
    src += pFrameRGB->linesize[0];
}

如果您发现更高效的方法,请在评论中告诉我。

你可以直接从pFrameRGB复制数据到一个新的QImage(RGB888),并在内部进行转换。 - Andi Krusch

4
我知道现在可能有点晚了,但也许有人会发现这很有用。我从这里得到了同样转换的线索,看起来更简短一些。
因此,我创建了一个QImage,它可以重复使用于每个解码帧:
QImage img( width, height, QImage::Format_RGB888 );

创建了 frameRGB:

frameRGB = av_frame_alloc();    
//Allocate memory for the pixels of a picture and setup the AVPicture fields for it.
avpicture_alloc( ( AVPicture *) frameRGB, AV_PIX_FMT_RGB24, width, height);

在第一帧解码后,我通过以下方式创建转换上下文SwsContext(它将用于所有接下来的帧):

mImgConvertCtx = sws_getContext( codecContext->width, codecContext->height, codecContext->pix_fmt, width, height, AV_PIX_FMT_RGB24, SWS_BICUBIC, NULL, NULL, NULL);

最后,对于每个解码帧都要进行转换:

if( 1 == framesFinished && nullptr != imgConvertCtx )
{
//conversion frame to frameRGB
sws_scale(imgConvertCtx, frame->data, frame->linesize, 0, codecContext->height, frameRGB->data, frameRGB->linesize);
//setting QImage from frameRGB
for( int y = 0; y < height; ++y )
   memcpy( img.scanLine(y), frameRGB->data[0]+y * frameRGB->linesize[0], mWidth * 3 );
}

请查看链接以获取详细信息。

看起来更好了... 我只需要用 frameRGB->linesize[0] 替换 mWidth * 3 - Rafael Fontes
这个回答看起来比我之前的回答简洁多了。标记为最佳答案。 - S B

4
我认为更简单的方法是:
void takeSnapshot(AVCodecContext* dec_ctx, AVFrame* frame)
{
    SwsContext* img_convert_ctx;

    img_convert_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);

    AVFrame* frameRGB = av_frame_alloc();
    avpicture_alloc((AVPicture*)frameRGB,
                    AV_PIX_FMT_RGB24,
                    dec_ctx->width,
                    dec_ctx->height);

    sws_scale(img_convert_ctx, 
              frame->data, 
              frame->linesize, 0, 
              dec_ctx->height, 
              frameRGB->data, 
              frameRGB->linesize);

    QImage image(frameRGB->data[0], 
                 dec_ctx->width, 
                 dec_ctx->height, 
                 frameRGB->linesize[0], 
                 QImage::Format_RGB888);

    image.save("capture.png");
}

2
欢迎来到stackoverflow!虽然你提供的代码可能是问题的解决方案,但最好还是简要说明一下你的代码是做什么的。 - morten.c
这似乎也避免了复制,性能与 @mike_wei 的解决方案相同。 - Lucker10

2
今天,我已经测试了将image->bit()直接传递给swscale,最终它可以工作,因此不需要复制到内存。例如:

今天,我已经测试了将image->bit()直接传递给swscale,最终它可以工作,因此不需要复制到内存。例如:

/* 1. Get frame and QImage to show */
struct my_frame *frame = get_frame(source);
QImage *myImage = new QImage(dst_w, dst_h, QImage::Format_RGBA8888);

/* 2. Convert and write into image buffer  */
uint8_t *dst[] = {myImage->bits()};
int linesizes[4];
av_image_fill_linesizes(linesizes, AV_PIX_FMT_RGBA, frame->width);

sws_scale(myswscontext, frame->data, (const int*)frame->linesize,
          0, frame->height, dst, linesizes);

这太棒了,特别是如果你必须转换为RGB的话。对于一个FHD帧来说只需要一半的时间!(8毫秒-> 4毫秒) - Lucker10

0
我刚刚发现scanLine只是在缓冲区中寻找..你只需要使用AV_PIX_FMT_RGB32作为AVFrame的格式和QImage::FORMAT_RGB32作为QImage的格式。
然后解码后只需执行memcpy操作 memcpy(img.scanLine(0), pFrameRGB->data[0], pFrameRGB->linesize[0] * pFrameRGB->height());

0

我在其他提出的解决方案中遇到了问题:

  • 它们没有提及释放AVFrame、SwsContext或分配的缓冲区,这导致了大量的内存泄漏(我需要处理成千上万的帧)。这些问题不能轻易地全部解决,因为QImage依赖于底层数据,而不是复制它。如果直接释放缓冲区,则QImage指向已释放的数据并且会崩溃。可以通过使用QImage的cleanupFunction来释放缓冲区,一旦不再需要图像,但由于其他问题,这也不是一个好的解决方案。
  • 在某些情况下,将QImage.bits直接传递给sws_scale的建议之一将无法工作,因为QImage至少需要32位对齐。因此,对于某些尺寸,它将不匹配sws_scale期望的宽度,并使每行输出略微偏移。
  • 第三个问题是他们使用了已弃用的AVPicture元素。

我在另一个问题Converting an AVFrame to QImage with conversion of pixel format中列出了这些问题,并最终找到了一个解决方案,使用一个临时缓冲区,可以将其复制到QImage中,然后安全地释放。

请查看我的答案,其中包含一个完全可工作、高效且没有过时函数调用的实现:https://stackoverflow.com/a/68212609/7360943


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接