0
\$\begingroup\$

Thanks to Rotem and his help with this issue, I can offer an implementation that allows to use cvtColor to convert an AVFrame YUV420P into a BGR cv::Mat. The key is to create a temporary intermediate cv::Mat where all the AVFrame data is copied in the way cvtColor expects it.

#include <cstdint>
#include <vector>
#include <opencv2/opencv.hpp>
#include <opencv2/imgproc.hpp>
extern "C" {
#include <libavutil/frame.h>
}

class AVFrameYuvToBgrMatConverter {
private:
    mutable cv::Mat m_yuv;
    ///We use a support buffer because in this way we can force opencv to use the same stride of AVFrame,
    ///so that the y channel, which is 2 thirds of the data, can be copied into the intermediate matrix in one go
    mutable std::vector<uint8_t> m_yuv_buffer;

public:
    void convert(const AVFrame& frame, cv::Mat& dest) const {
        const auto width = frame.width;
        const auto height = frame.height;
        ///Check the input has an even number of cols, and rows divisible by 4
        assert((width & 0x1) + (height & 0x11) == 0);
        const std::size_t y_stride = frame.linesize[0];
        const auto y_src = frame.data[0];
        {
            const std::size_t new_size = y_stride * 3 / 2 * height;
            if (new_size > m_yuv_buffer.size()) {
                ///We resize the buffer only if it needs to grow
                m_yuv_buffer.resize(new_size);
            }
        }
        ///The internal cv::Mat m_yuv doesn't allocate a buffer, so it is efficient, but yet creating a cv::Mat
        ///has some overhead so we only do that when necessary
        if (m_yuv.cols != width || m_yuv.rows != height || m_yuv.step1() != y_stride ||
            m_yuv.ptr() != m_yuv_buffer.data()) {
            ///Unfortunately cv::Mat::create does not support passing a preallocated buffer
            m_yuv = cv::Mat(height * 3 / 2, width, CV_8UC1, m_yuv_buffer.data(), y_stride);
        }
        ///Copy y channel
        memcpy(m_yuv.ptr(), y_src, y_stride * height);

        ///Copy uv channels
        const auto u_stride = frame.linesize[1];
        const uint8_t* u_src = frame.data[1];
        const auto v_stride = frame.linesize[2];
        const uint8_t* v_src = frame.data[2];
        if (static_cast<int>(y_stride) == width && u_stride == width / 2 && v_stride == width / 2) {
            ///In this case we can copy the u and v channels in one go
            memcpy(m_yuv.ptr(height), u_src, width * height / 4);
            memcpy(m_yuv.ptr(height * 5 / 4), v_src, width * height / 4);
        } else {
            ///In this case we have to proceed line by line
            auto dst = m_yuv.ptr(height);
            for (int i = 0; i < height / 4; i++) {
                memcpy(dst, u_src, width / 2);
                u_src += u_stride;
                memcpy(dst + width / 2, u_src, width / 2);
                u_src += u_stride;
                dst += y_stride;
            }
            for (int i = 0; i < height / 4; i++) {
                memcpy(dst, v_src, width / 2);
                v_src += v_stride;
                memcpy(dst + width / 2, v_src, width / 2);
                v_src += v_stride;
                dst += y_stride;
            }
        }
        ///cvtColor input is finally ready
        cv::cvtColor(m_yuv, dest, cv::COLOR_YUV2BGR_I420, 3);
    }
};

Ideally one could go inside OpenCV code, see how the conversion is done, and adapt it to use directly AVFrame's buffers without having to reshuffle bytes beforehand.

The alternative to perform this conversion is to use ffmpeg, this is spelled out as part of this answer. It would be eventually worth to investigate which one is the most efficient, I'll post the results if I ever get to do that.

\$\endgroup\$

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Browse other questions tagged or ask your own question.