YUV系列之BGR2YUV

1,252 阅读6分钟

YUV系列之BGR2YUV

在OpenCV中imgproc模块下的cvtColor API。这个API的主要功能是对图片做色彩空间转换,使用起来很方便,但是背后的转换理论多少有些繁琐,但是也不难。因此今天在这篇文章中对色彩空间转换的理论进行梳理。
OpenCV支持的色彩非常丰富,我们会在以后的系列中逐步介绍,这个系列主要介绍YUV色彩空间与RGB或者BGR空间之间的转换,同时借此了解OpenCV中cvtColor这个函数的代码结构。

cvtColor API


opencv/modules/imgproc/src目录下grep cvtColor可以查找到这个API的函数原型,截取片段,如下:

void cvtColor( InputArray _src, OutputArray _dst, int code, int dcn )
{
    CV_INSTRUMENT_REGION();

    CV_Assert(!_src.empty());

    if(dcn <= 0)
            dcn = dstChannels(code);

    CV_OCL_RUN( _src.dims() <= 2 && _dst.isUMat() &&
                !(CV_MAT_DEPTH(_src.type()) == CV_8U && (code == COLOR_Luv2BGR || code == COLOR_Luv2RGB)),
                ocl_cvtColor(_src, _dst, code, dcn) )

    switch( code )
    {
        case COLOR_BGR2BGRA: case COLOR_RGB2BGRA: case COLOR_BGRA2BGR:
        case COLOR_RGBA2BGR: case COLOR_RGB2BGR:  case COLOR_BGRA2RGBA:
            cvtColorBGR2BGR(_src, _dst, dcn, swapBlue(code));
            break;

        case COLOR_BGR2BGR565:  case COLOR_BGR2BGR555: case COLOR_BGRA2BGR565: case COLOR_BGRA2BGR555:
        case COLOR_RGB2BGR565:  case COLOR_RGB2BGR555: case COLOR_RGBA2BGR565: case COLOR_RGBA2BGR555:
            cvtColorBGR25x5(_src, _dst, swapBlue(code), greenBits(code));
            break;
        
        ...

API参数说明:

  • _src: 输入图像
  • _dst: 输出图像
  • code: 色彩空间转换码
  • dcn: 输入图像的通道数,如果为0,则会自动根据_src和色彩空间转换码推断其值

关于色彩空间转换码,可以在opencv/modules/imgproc/include/opencv2目录的imgproc.hpp文件中查看,截取如下:

/** the color conversion code
@see @ref imgproc_color_conversions
@ingroup imgproc_misc
 */
enum ColorConversionCodes {
    COLOR_BGR2BGRA     = 0, //!< add alpha channel to RGB or BGR image
    COLOR_RGB2RGBA     = COLOR_BGR2BGRA,

    COLOR_BGRA2BGR     = 1, //!< remove alpha channel from RGB or BGR image
    COLOR_RGBA2RGB     = COLOR_BGRA2BGR,

    COLOR_BGR2RGBA     = 2, //!< convert between RGB and BGR color spaces (with or without alpha channel)
    COLOR_RGB2BGRA     = COLOR_BGR2RGBA,

    COLOR_RGBA2BGR     = 3,
    COLOR_BGRA2RGB     = COLOR_RGBA2BGR,
    ...

这里定义了各种色彩空间转换的编码。
继续查看cvtColor函数原型,能够看到本片文章的主角——RGB2NV21:

...

case COLOR_BGR2YCrCb: case COLOR_RGB2YCrCb:
case COLOR_BGR2YUV:   case COLOR_RGB2YUV:
    cvtColorBGR2YUV(_src, _dst, swapBlue(code), code == COLOR_BGR2YCrCb || code == COLOR_RGB2YCrCb);
    break;

...

可以看到RGB转NV21的色彩空间转换码是COLOR_BGR2YUV,使用的函数是:cvtColorBGR2YUV

cvtColorBGR2YUV 函数


这个函数的后两个参数,需要关注一下,最后一个参数用来区分是YCrCb还是YUV,接下来看swapBlue函数:

swapBlue函数

inline bool swapBlue(int code)
{
    switch (code)
    {
    case COLOR_BGR2BGRA: case COLOR_BGRA2BGR:
    case COLOR_BGR2BGR565: case COLOR_BGR2BGR555: case COLOR_BGRA2BGR565: case COLOR_BGRA2BGR555:
    case COLOR_BGR5652BGR: case COLOR_BGR5552BGR: case COLOR_BGR5652BGRA: case COLOR_BGR5552BGRA:
    case COLOR_BGR2GRAY: case COLOR_BGRA2GRAY:
    case COLOR_BGR2YCrCb: case COLOR_BGR2YUV:
    case COLOR_YCrCb2BGR: case COLOR_YUV2BGR:
    case COLOR_BGR2XYZ: case COLOR_XYZ2BGR:
    case COLOR_BGR2HSV: case COLOR_BGR2HLS: case COLOR_BGR2HSV_FULL: case COLOR_BGR2HLS_FULL:
    case COLOR_YUV2BGR_YV12: case COLOR_YUV2BGRA_YV12: case COLOR_YUV2BGR_IYUV: case COLOR_YUV2BGRA_IYUV:
    case COLOR_YUV2BGR_NV21: case COLOR_YUV2BGRA_NV21: case COLOR_YUV2BGR_NV12: case COLOR_YUV2BGRA_NV12:
    case COLOR_Lab2BGR: case COLOR_Luv2BGR: case COLOR_Lab2LBGR: case COLOR_Luv2LBGR:
    case COLOR_BGR2Lab: case COLOR_BGR2Luv: case COLOR_LBGR2Lab: case COLOR_LBGR2Luv:
    case COLOR_HSV2BGR: case COLOR_HLS2BGR: case COLOR_HSV2BGR_FULL: case COLOR_HLS2BGR_FULL:
    case COLOR_YUV2BGR_UYVY: case COLOR_YUV2BGRA_UYVY: case COLOR_YUV2BGR_YUY2:
    case COLOR_YUV2BGRA_YUY2:  case COLOR_YUV2BGR_YVYU: case COLOR_YUV2BGRA_YVYU:
    case COLOR_BGR2YUV_IYUV: case COLOR_BGRA2YUV_IYUV: case COLOR_BGR2YUV_YV12: case COLOR_BGRA2YUV_YV12:
        return false;
    default:
        return true;
    }
}

这个inline函数定义在opencv/modules/imgproc/src目录下的color.h文件中,可以发现所有涉及BGR格式的转换,swapBlue的返回值都是false,其他均是true,也就是或OpenCV的默认色彩排列是BGR的,如果输入图像是RGB相关的,或者输出图像是BGR相关的,就需要进行swapBlue,也就是在后续的处理中需要对颜色的通道顺序进行调整,所以相同大小的图,相同的处理,BGR会比RGB要快,因为BGR不需要调整通道顺序。

cvtColorBGR2YUV

路径:opencv/modules/imgproc/src/color_yuv.cpp +2614

void cvtColorBGR2YUV(InputArray _src, OutputArray _dst, bool swapb, bool crcb)
{
    CvtHelper< Set<3, 4>, Set<3>, Set<CV_8U, CV_16U, CV_32F> > h(_src, _dst, 3);

    hal::cvtBGRtoYUV(h.src.data, h.src.step, h.dst.data, h.dst.step, h.src.cols, h.src.rows,
                     h.depth, h.scn, swapb, crcb);
}

可以看到该函数调用了hal::cvtBGRtoYUV

hal::cvtBGRtoYUV

路径:opencv/modules/imgproc/src/color_yuv.cpp +2266

void cvtBGRtoYUV(const uchar * src_data, size_t src_step,
                 uchar * dst_data, size_t dst_step,
                 int width, int height,
                 int depth, int scn, bool swapBlue, bool isCbCr)
{
    CV_INSTRUMENT_REGION();

    CALL_HAL(cvtBGRtoYUV, cv_hal_cvtBGRtoYUV, src_data, src_step, dst_data, dst_step, width, height, depth, scn, swapBlue, isCbCr);

#if defined(HAVE_IPP)
#if !IPP_DISABLE_RGB_YUV
    CV_IPP_CHECK()
    {
        if (scn == 3 && depth == CV_8U && swapBlue && !isCbCr)
        {
            if (CvtColorIPPLoop(src_data, src_step, dst_data, dst_step, width, height,
                                IPPGeneralFunctor((ippiGeneralFunc)ippiRGBToYUV_8u_C3R)))
                return;
        }
        else if (scn == 3 && depth == CV_8U && !swapBlue && !isCbCr)
        {
            if (CvtColorIPPLoop(src_data, src_step, dst_data, dst_step, width, height,
                                IPPReorderGeneralFunctor(ippiSwapChannelsC3RTab[depth],
                                                         (ippiGeneralFunc)ippiRGBToYUV_8u_C3R, 2, 1, 0, depth)))
                return;
        }
        else if (scn == 4 && depth == CV_8U && swapBlue && !isCbCr)
        {
            if (CvtColorIPPLoop(src_data, src_step, dst_data, dst_step, width, height,
                                IPPReorderGeneralFunctor(ippiSwapChannelsC4C3RTab[depth],
                                                         (ippiGeneralFunc)ippiRGBToYUV_8u_C3R, 0, 1, 2, depth)))
                return;
        }
        else if (scn == 4 && depth == CV_8U && !swapBlue && !isCbCr)
        {
            if (CvtColorIPPLoop(src_data, src_step, dst_data, dst_step, width, height,
                                IPPReorderGeneralFunctor(ippiSwapChannelsC4C3RTab[depth],
                                                         (ippiGeneralFunc)ippiRGBToYUV_8u_C3R, 2, 1, 0, depth)))
                return;
        }
    }
#endif
#endif

    int blueIdx = swapBlue ? 2 : 0;
    if( depth == CV_8U )
        CvtColorLoop(src_data, src_step, dst_data, dst_step, width, height, RGB2YCrCb_i<uchar>(scn, blueIdx, isCbCr));
    else if( depth == CV_16U )
        CvtColorLoop(src_data, src_step, dst_data, dst_step, width, height, RGB2YCrCb_i<ushort>(scn, blueIdx, isCbCr));
    else
        CvtColorLoop(src_data, src_step, dst_data, dst_step, width, height, RGB2YCrCb_f<float>(scn, blueIdx, isCbCr));
}

可以看到函数中的宏HAVE_IPP,我们不使用IPP库加速,所以直接看后面的代码就可以。BGR转YUV,所以blueIdx=0; depth = CV_8U。调用CvtColorLoop函数,cvtColorLoop中调用了RGB2YCrCb_i函数,这个函计算的主体部分。 注意blueIdx的取值是0或者2

RGB2YCrCb_i

路径:opencv/modules/imgproc/src/color_yuv.cpp +251

template<typename _Tp> struct RGB2YCrCb_i
{
    typedef _Tp channel_type;

    RGB2YCrCb_i(int _srccn, int _blueIdx, bool _isCrCb)
        : srccn(_srccn), blueIdx(_blueIdx), isCrCb(_isCrCb)
    {
        //设置系数
        static const int coeffs_crb[] = { R2Y, G2Y, B2Y, YCRI, YCBI };
        static const int coeffs_yuv[] = { R2Y, G2Y, B2Y, R2VI, B2UI };
        //yuv和YCrCb的系数不同
        memcpy(coeffs, isCrCb ? coeffs_crb : coeffs_yuv, 5*sizeof(coeffs[0]));
        //RGB和BGR的区别,需要交换B分量和R分量的位置
        if(blueIdx==0) std::swap(coeffs[0], coeffs[2]);
    }
    void operator()(const _Tp* src, _Tp* dst, int n) const
    {
        int scn = srccn, bidx = blueIdx;
        int yuvOrder = !isCrCb; //1 if YUV, 0 if YCrCb
        int C0 = coeffs[0], C1 = coeffs[1], C2 = coeffs[2], C3 = coeffs[3], C4 = coeffs[4];

        //color.hpp +26 : yuv_shift = 14
        int delta = ColorChannel<_Tp>::half()*(1 << yuv_shift);
        n *= 3;
        for(int i = 0; i < n; i += 3, src += scn)
        {
            int Y = CV_DESCALE(src[0]*C0 + src[1]*C1 + src[2]*C2, yuv_shift);
            int Cr = CV_DESCALE((src[bidx^2] - Y)*C3 + delta, yuv_shift);
            int Cb = CV_DESCALE((src[bidx] - Y)*C4 + delta, yuv_shift);
            dst[i] = saturate_cast<_Tp>(Y);
            dst[i+1+yuvOrder] = saturate_cast<_Tp>(Cr);
            dst[i+2-yuvOrder] = saturate_cast<_Tp>(Cb);
        }
    }
    int srccn, blueIdx;
    bool isCrCb;
    int coeffs[5];
};

程序细节在代码中已经做了注释,这里主要介绍一些BGR转YUV的理论公式,首先系数C0-C4分别为: static const int coeffs_yuv[] = { R2Y, G2Y, B2Y, R2VI, B2UI };
R2Y = 4899, // == R2YF16384
G2Y = 9617, // == G2YF
16384
B2Y = 1868, // == B2YF*16384

B2UI = 8061; // == B2UF16384
R2VI = 14369; // == R2VF
16384

bidx ^ 2 是按位异或(请注意bidx的取值是0或者2) :
当bidx == 0的时候,其值为 000 ^ 010 = 010 = 2
当bidx == 2的时候,其值为 010 ^ 010 = 011 = 0 可以得到YUV的计算公式:
Y = (4899 * R + 9617 * G + 1868 * B) >> 14;//注意代码中的注释blueIdx == 0 B和R的系数交换了,默认的系数排列顺序是先RGB,默认的通道排列顺序是BGR V = ((R - Y) * 14369 + delta) >> 14; U = ((B - Y) * 8061 + delta) >> 14;

以上是整数的计算公式,根据计算精度不同,左移的位数不同,整数系数也会调整;

delta的计算方式: int delta = ColorChannel<_Tp>::half()*(1 << yuv_shift);

ColorChannel<_Tp>::half()在color.hpp中定义的:

template<typename _Tp> struct ColorChannel
{
    typedef float worktype_f;
    static _Tp max() { return std::numeric_limits<_Tp>::max(); }
    static _Tp half() { return (_Tp)(max()/2 + 1); }
};

我们计算的是uchar,所以, delta = (255 / 2 + 1) * (1 << 14)

到此位置完整介绍了RGB转YUV的计算方式,如果是浮点数转换,仅仅是公式不同,可以以相同的方式查看代码,查看计算的系数。这里的YUV就是YUV 444,后续的文章中会介绍YUV422,YUV420等计算方式,同时介绍这几种格式的在OpenCV中的采样方法。

欢迎关注公众号:计算机视觉与高性能计算