如何将像素格式为AV_PIX_FMT_CUDA的FFmpeg AVFrame转换为像素格式为AV_PIX_FMT_RGB的新AVFrame?

12

我有一个简单的C++应用程序,使用FFmpeg 3.2接收H264 RTP流。为了节省CPU,我使用h264_cuvid解码器进行解码。我的FFmpeg 3.2编译时启用了硬件加速。事实上,如果我执行以下命令:

ffmpeg -hwaccels

我明白了

cuvid

这意味着我的FFmpeg设置已经可以与我的NVIDIA显卡“通话”了。函数avcodec_decode_video2提供的帧具有像素格式AV_PIX_FMT_CUDA。我需要将这些帧转换为新的帧,其像素格式为AV_PIX_FMT_RGB。不幸的是,我无法使用众所周知的函数sws_getContextsws_scale进行转换,因为像素格式AV_PIX_FMT_CUDA不受支持。如果我尝试使用swscale,会出现错误:

“cuda不支持作为输入像素格式”

您知道如何将FFmpeg AVFrameAV_PIX_FMT_CUDA转换为AV_PIX_FMT_RGB吗?(非常感谢提供代码片段)


如果您只需要使用cuvid进行解码,我想就没有必要处理AV_PIX_FMT_CUDA了。虽然没有关于此的示例,但是qsvdec.chw_decode.c官方示例可能是一个很好的参考。在这里,解码器应该将nv12格式返回到主机内存。 - halfelf
3个回答

7
这是我对最新版本FFMpeg 4.1硬件解码的理解。以下是我在研究源代码后得出的结论。
首先,我建议从hw_decode示例中获取灵感:

https://github.com/FFmpeg/FFmpeg/blob/release/4.1/doc/examples/hw_decode.c

使用新的API时,当您使用avcodec_send_packet()将数据包发送到编码器时,可以使用avcodec_receive_frame()来检索解码帧。
有两种不同类型的AVFrame:一种是软件类型,存储在“CPU”内存(也称为RAM)中,另一种是硬件类型,存储在显卡内存中。

从硬件获取AVFrame

要检索硬件帧并将其转换为可读取、可转换(使用 swscaler)的 AVFrame,需要使用 av_hwframe_transfer_data() 从显卡中检索数据。然后查看检索到的帧的像素格式,当使用 nVidia 解码时,通常为 NV12 格式。
// According to the API, if the format of the AVFrame is set before calling 
// av_hwframe_transfer_data(), the graphic card will try to automatically convert
// to the desired format. (with some limitation, see below)
m_swFrame->format = AV_PIX_FMT_NV12;

// retrieve data from GPU to CPU
err = av_hwframe_transfer_data(
     m_swFrame, // The frame that will contain the usable data.
     m_decodedFrame, // Frame returned by avcodec_receive_frame()
     0);

const char* gpu_pixfmt = av_get_pix_fmt_name((AVPixelFormat)m_decodedFrame->format);
const char* cpu_pixfmt = av_get_pix_fmt_name((AVPixelFormat)m_swFrame->format);

列出支持的“软件”像素格式

这里需要注意,如果您想选择像素格式,不是所有的AVPixelFormat都受支持。AVHWFramesConstraints在这里是您的好帮手:

AVHWDeviceType type = AV_HWDEVICE_TYPE_CUDA;
int err = av_hwdevice_ctx_create(&hwDeviceCtx, type, nullptr, nullptr, 0);
if (err < 0) {
    // Err
}

AVHWFramesConstraints* hw_frames_const = av_hwdevice_get_hwframe_constraints(hwDeviceCtx, nullptr);
if (hw_frames_const == nullptr) {
    // Err
}

// Check if we can convert the pixel format to a readable format.
AVPixelFormat found = AV_PIX_FMT_NONE;
for (AVPixelFormat* p = hw_frames_const->valid_sw_formats; 
    *p != AV_PIX_FMT_NONE; p++)
{
    // Check if we can convert to the desired format.
    if (sws_isSupportedInput(*p))
    {
        // Ok! This format can be used with swscale!
        found = *p;
        break;
    }
}

// Don't forget to free the constraint object.
av_hwframe_constraints_free(&hw_frames_const);

// Attach your hw device to your codec context if you want to use hw decoding.
// Check AVCodecContext.hw_device_ctx!

最后,可能更快的方法是使用av_hwframe_transfer_get_formats()函数,但您需要解码至少一个帧。

希望这可以帮到您!


2
您必须使用vf_scale_npp来完成此操作。根据您的需求,您可以使用nppscale_deinterleavenppscale_resize
两者都具有相同的输入参数,其中包括AVFilterContext,应该使用nppscale_init进行初始化,NPPScaleStageContext,它接受您的输入/输出像素格式和两个AVFrame,当然是您的输入和输出帧。
更多信息请参见npplib\nppscale定义,自ffmpeg 3.1以来将执行CUDA加速格式转换和缩放。
无论如何,我建议直接使用NVIDIA视频编解码器SDK来实现这个目的。

你好,Hamed。非常感谢您的回答。我将研究vf_scale_npp。这些函数static int nppscale_deinterleave (AVFilterContext *ctx, NPPScaleStageContext * stage, AVFrame *out, AVFrame *in);static int nppscale_resize (AVFilterContext *ctx, NPPScaleStageContext * stage, AVFrame *out, AVFrame *in);看起来非常有前途。我很快会给出反馈。再次感谢 - costef
嗨,哈梅德。我尝试了,但没有成功。 我从函数“nppscale_init”中获得了成功,但从“nppscale_deinterleave”中获得了失败。从这个错误代码中可以看出:[in @ 0x7fff69b97820] NPP deinterleave error: -8显然问题出在我的“in”AVFrame上。但是什么问题呢?你知道它的含义吗?你还建议直接使用NVIDIA视频编解码器SDK进行此类转换。我愿意使用它。FFmpeg在这里缺乏文档和好的示例。你有一段代码,一个函数,可以接收从cuvid获取的AVFrame并返回一个AV_PIX_FMT_RGB中的新帧吗? - costef
好的,没问题。无论如何还是谢谢你。我进行了更深入的调查。我发现“nppscale_deinterleave”调用了“nppiYCbCr420_8u_P2P3R”函数,并返回错误代码-8。该错误代码为NPP_NULL_POINTER_ERROR = -8我已经检查过我没有向“nppscale_deinterleave”传递任何NULL,但仍然出现错误。我怀疑我需要在我的软件中“引入”更多的vf_scale_npp代码。 - costef

2

我不是一个ffmpeg专家,但是我遇到了类似的问题并成功解决了。我从cuvid(mjpeg_cuvid解码器)中获取了AV_PIX_FMT_NV12,想要使用AV_PIX_FMT_CUDA进行cuda处理。

我发现在解码帧之前设置像素格式可以解决问题。

    pCodecCtx->pix_fmt = AV_PIX_FMT_CUDA; // change format here
    avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished, &packet);
    // do something with pFrame->data[0] (Y) and pFrame->data[1] (UV)

您可以使用pix_fmts检查您的解码器支持哪些像素格式:

    AVCodec *pCodec = avcodec_find_decoder_by_name("mjpeg_cuvid");
    for (int i = 0; pCodec->pix_fmts[i] != AV_PIX_FMT_NONE; i++)
            std::cout << pCodec->pix_fmts[i] << std::endl;

我相信有更好的方法来完成这个任务,但是我使用了这个列表将整数像素格式ID映射为易于理解的像素格式。
如果那不起作用,您可以使用cudaMemcpy将像素从设备传输到主机:
    cudaMemcpy(pLocalBuf pFrame->data[0], size, cudaMemcpyDeviceToHost);

从YUV到RGB/RGBA的转换可以采用多种方法。这个示例使用libavdevice API进行转换。

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