YUV图像用的比较多,而且YUV图像的格式众多(YUV格式可以参YUV pixel formats),如何用OpenCV的Mat类型来存储YUV图像也是经常遇到的问题。

对于YUV444图像来说,就很简单。YUV的三个分量的采样方法一致,因此YUV三个分量的大小一致,可以用Mat的三个channel分别表示YUV即可。假设src是OpenCV默认的BGR三通道图像,和YUV444的转换如下,图像大小不变。

// If src is CV_8UC3, dest is CV_8UC3
cvtColor(src, dest, COLOR_BGR2YUV);
cvtColor(dest, src, COLOR_YUV2BGR);

YUV422用的不多(其实我没用过),先说YUV420。YUV420图像的U/V分量在水平和垂直方向上downsample,在水平和垂直方向上的数据都只有Y分量的一半。因此总体来说,U/V分量的数据量分别只有Y分量的1/4,不能作为Mat类型的一个channel。所以通常YUV420图像的全部数据存储在Mat的一个channel,比如CV_8UC1,这样对于Mat来说,图像的大小就有变化。对于MxN(rows x cols,M行N列)的BGR图像(CV_8UC3),其对应的YUV420图像大小是(3M/2)xN(CV_8UC1)。前MxN个数据是Y分量,后(M/2)xN个数据是U/V分量,UV数据各占一半。

U/V分量如何存储,和YUV420的格式有关。YUV420有所谓的420p(420planar/420面)和420sp(420 semi-planar/420半面)格式。所谓420面格式,YUV三个分量按顺序存储完一个分量所有图像数据,称为一个面,再存储下一个分量的面,因此有三个面数据。420半面格式下,只有Y分量是作为一个单独的面存储,U/V分量按照像素排列顺序交错存储,算作一个面,因此称为半面。

420p

420sp

YUV顺序

YVU顺序

UVUV交错

VUVU交错

I420/IYUV

YV12

NV12

NV21

420p或者420sp都是先存储Y分量的面,然后根据UV分量的存储顺序,又各分为两种格式。420p按照YUV的顺序存储三个面,是I420格式,或者叫IYUV格式。按照YVU的顺序存储三个面,叫YV12格式。420sp的U/V交错面,如果按照UVUV的顺序交错存储,称为NV12格式。反之,按照VUVU的顺序交错存储,称为NV21格式。

OpenCV现在从BGR到YUV420的颜色空间变化仅支持转换到420p的两种格式,不支持转换到420sp。但可以支持420p或者420sp转换到BGR。假设src是OpenCV默认的BGR三通道图像,和420p的转换如下。

// If src is BGR CV_8UC3 with size 640x960, dest is CV_8UC1 with 960x960
cvtColor(src, dest, COLOR_BGR2YUV_I420);    // dest is I420
cvtColor(dest, src, COLOR_YUV2BGR_I420);

cvtColor(src, dest, COLOR_BGR2YUV_YV12);    // dest is YV12
cvtColor(dest, src, COLOR_YUV2BGR_YV12);

假设src是YUV420的420sp图像数据,到BGR的转换如下。

// If src is NV12 CV_8UC1 with size 960x960, dest is BGR CV_8UC3 with 640x960
cvtColor(src, dest, COLOR_YUV2BGR_NV12);


// If src is NV21 CV_8UC1 with size 960x960, dest is BGR CV8UC3 with 640x960 cvtColor(src, dest, COLOR_YUV2BGR_NV21);

OpenCV还提供了一个cvtColorTwoPlane函数,当前仅支持从420sp转换到BGR,但是Y面和U/V交错面存储在两个Mat结构中。

下面的代码片段把height x width的YUV图像数据顺时针旋转90°存储到Mat,格式是NV12。yPixel, uPixel, vPixel分别是指向YUV数据的指针,yStride,uvStride分别是Y和UV的行stride,uvPixelStride是UV数据像素stride。代码分别把YUV数据存储到一个临时Mat中,然后调用OpenCV的transpose()和flip()函数把图像顺时针旋转90°。较新版本的OpenCV提供了函数rotate()可以做90°,180°和270°的旋转,可以使用。最后分别把旋转后的YUV数据写到Mat中,最后的格式是NV12,注意height和width交换了,UV数据是交错存储的。如果不使用OpenCV的函数,自己写一段代码来做旋转也是可以的。不过我试过了,肯定没有OpenCV的函数快。OpenCV的函数至少要比我们用循环写出来的代码快25%。所以有现成的库函数尽量使用他们。

    // Original image with size height x width
    // int32_t width, height; original image width and height
    // uint8_t *yPixel, *uPixel, *vPixel; pointers to YUV data
    // int32_t yStride, uvStride, uvPixelStride; line stride and uv pixel stride

    cv::Mat yuv_nv12(width * 3 / 2, height, CV_8UC1)
    int i, j;
    int height2 = height / 2, width2 = width / 2;

    cv::Mat y_temp(height, width, CV_8UC1);
    cv::Mat u_temp(height2, width2, CV_8UC1);
    cv::Mat v_temp(height2, width2, CV_8UC1);
    // Get Y data and rotate
    line_src = yPixel;
    for (i = 0; i < height; i++) {
        line_dest = y_temp.ptr(i);
        memcpy(line_dest, line_src, width);
        line_src += yStride;
    }
    cv::transpose(y_temp, y_temp);
    cv::flip(y_temp, y_temp, 1);
    // Get U data and rotate
    line_src = uPixel;
    for (i = 0; i < height2; i++) {
        line_dest = u_temp.ptr(i);
        uchar *ptr = line_src;
        for (j = 0; j < width2; j++) {
            *line_dest++ = *ptr;
            ptr += uvPixelStride;
        }
        line_src += uvStride;
    }
    cv::transpose(u_temp, u_temp);
    cv::flip(u_temp, u_temp, 1);
    // Get V data and rotate
    line_src = vPixel;
    for (i = 0; i < height2; i++) {
        line_dest = v_temp.ptr(i);
        uchar *ptr = line_src;
        for (j = 0; j < width2; j++) {
            *line_dest++ = *ptr;
            ptr += uvPixelStride;
        }
        line_src += uvStride;
    }
    cv::transpose(v_temp, v_temp);
    cv::flip(v_temp, v_temp, 1);
    // Write Y data to yuv_nv12
    for (i = 0; i < width; i++) {
        line_dest = yuv_nv12.ptr(i);
        line_src = y_temp.ptr(i);
        memcpy(line_dest, line_src, height);
    }
    // Write UV data to yuv_nv12
    cv::MatIterator_<uchar> it((cv::Mat_<uchar>*)&yuv_nv12, width);
    cv::MatIterator_<uchar> u_src_it = u_temp.begin<uchar>();
    cv::MatIterator_<uchar> v_src_it = v_temp.begin<uchar>();
    int wh2 = width2 * height2;
    for (i = 0; i < wh2; i++) {
        *it++ = *u_src_it++;
        *it++ = *v_src_it++;
    }

至于YUV422图像,我没有试过。OpenCV不支持从BGR转到YUV422,但是可以从YUV422转会BGR。大概看了下,YUV422图像用Mat类型存储应该也是用一个channel来存储所有YUV数据,而且应该是用所谓的紧凑格式(packed format),而不是前面提到的面格式(planar format)。所谓紧凑格式,就是对每个像素的YUV三个分量按照一定的顺序交错存储,每4个数据组成一个所谓的宏像素。因为YUV422垂直方向没有downsample,只有水平方向有,所以每两个Y对应一个U和一个V,组成一个宏像素。比如UYVY格式(按照UYVY交错存储),YUY2格式(按照YUYV交错存储),YVYU格式等等。它们都有对应的转BGR的code,比如COLOR_YUV2BGR_UYVY,不一一列举了。