S B
S B

Reputation: 8384

Efficient conversion of AVFrame to QImage

I need to extract frames from a video in my Qt based application. Using ffmpeg libraries I am able to fetch frames as AVFrames which I need to convert to QImage to use in other parts of my application. This conversion needs to be efficient. So far it seems sws_scale() is the right function to use but I am not sure what source and destination pixel formats are to be specified.

Upvotes: 6

Views: 7037

Answers (6)

Théophane
Théophane

Reputation: 73

I had problems with the other proposed solutions as :

  • They did not mention freeing either AVFrame, SwsContext or the allocated buffers, which caused massive memory leaks (I had thousands of frames to handle). These problems couldn't all be solved easily as QImage relies on the underlying data, and does not copy it. If freeing the buffer directly, the QImage points to freed data and breaks. This could be solved by using QImage's cleanupFunction to free the buffer once the image is no longer needed, but with other problems it wasn't good anyways.
  • In some cases one of the suggestions, of passing QImage.bits directly to sws_scale, would not work as QImage are minimum 32 bit aligned. Therefore for certain dimensions it would not match the expected width by sws_scale and output each line shifted a little bit.
  • A third problem is that they used deprecated AVPicture elements.

I listed the problem in another question Converting an AVFrame to QImage with conversion of pixel format and in the end found a solution using a temporary buffer, which could be copied to the QImage, and then safely freed.

So see my answer for a fully working, efficient, and with no deprecated function calls, implementation : https://stackoverflow.com/a/68212609/7360943

Upvotes: 0

agrau
agrau

Reputation: 51

A simpler approach, I think:

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

Upvotes: 5

mike-wei
mike-wei

Reputation: 21

Today, I have tested directly pass the image->bit() to swscale and finally it works, so it doesn't need to copy to memory. For example:

/* 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);

Upvotes: 2

Rafael Fontes
Rafael Fontes

Reputation: 1353

I just discovered that scanLine is just seeking thru the buffer.. all you need is use AV_PIX_FMT_RGB32 for the AVFrame and QImage::FORMAT_RGB32 for the QImage.

Then after decoding just do a memcpy

memcpy(img.scanLine(0), pFrameRGB->data[0], pFrameRGB->linesize[0] * pFrameRGB->height());

Upvotes: 0

kopalvich
kopalvich

Reputation: 444

I know, it's too late, but maybe someone will find it useful. From here I got the clue of doing the same conversion, which looks a bit shorter.

So I created QImage which is reused for every decoded frame:

QImage img( width, height, QImage::Format_RGB888 );

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

After the the first frame is decoded I create conversion context SwsContext this way (it will be used for all the next frames):

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

And finally for every decoded frame conversion is performed:

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

See the link for the specifics.

Upvotes: 4

S B
S B

Reputation: 8384

Came up with the following 2-step process that first converts a decoded AVFame to another AVFrame in RGB colorspace and then to QImage. It works and is reasonably fast.

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];
}

If you find a more efficient approach, let me know in the comments

Upvotes: 5

Related Questions