笔者拟做背景虚化的光斑效果,但是RGB(NV21转YUV420)通道做出的效果容易产生色差,而且在过曝光的条件下出现光斑过多等问题;故在YUV444做虚化;需要将NV21(YUV420)转成YUV444。

    image.png
    2、YUV420转YUV444的主要思路:
    nv21格式yuv420文件,其排列方式为:YYYYYYYYVUVU; 即为:Y在前面,VU交错排列;
    nv12格式yuv420文件,其排列方式为: YYYYYYYYUVUV; 即为:Y在前面,UV交错排列;

    YUV420转YUV444,实际就是对色度(V和U)进行上采样,最为简单的实现思路是直接填充。

    代码如下:
    1、将nv21文件读取为yuv420格式Mat类型

    1. Mat nv21_to_yuv420(string nv21_file_name,int height, int width)
    2. {
    3. assert(rgb_img.channels() == 3);
    4. FILE *fp;
    5. fopen_s(&fp, nv21_file_name.c_str(), "r");
    6. void *img_block = malloc(height * width * 1.5 * sizeof(uchar));
    7. fread(img_block, sizeof(uchar), height * width * 1.5, fp);
    8. cv::Mat img_yuv(height*1.5, width, CV_8UC1, img_block);
    9. return img_yuv;
    10. // convert yuv420 to rgb
    11. // cv::Mat img_rgb(height, width, CV_8UC3);
    12. // cv::cvtColor(img_yuv, img_rgb, COLOR_YUV2BGR_NV21);
    13. }
    static void UpSample(Mat &srcImg, Mat &dstImg)
    {
        int rows = dstImg.rows;
        int cols = dstImg.cols;
        for (int row = 0; row < rows; ++row)
        {
            uchar * srcImg_ptr = srcImg.ptr<uchar>(row / 2);
            uchar * dstImg_ptr = dstImg.ptr<uchar>(row);
            for (int col = 0; col < cols; ++col)
            {
                dstImg_ptr[col] = srcImg_ptr[col / 2];
            }
        }
    }
    
    void yuv420_to_yuv444(const Mat & img_yuv420, Mat & img_yuv444)
    {
        assert(img_yuv444.channels() == 3);
        assert(img_yuv420.channels() == 1);
    
        int height = img_yuv420.rows * 2 / 3;
        int width = img_yuv420.cols;
    
        Mat U(height / 2, width / 2, CV_8UC1);
        Mat V(height / 2, width / 2, CV_8UC1);
        uchar * img_vu_ptr = img_yuv420.data + height * width;
        uchar * img_v_ptr = V.data;
        uchar * img_u_ptr = U.data;
        int size = width * height / 2;
        int i = 0;
        while (i < size)
        {
            *img_v_ptr++ = *img_vu_ptr++;
            img_vu_ptr++;
            ++i;
            ++i;
        }
    
        i = 1;
        img_vu_ptr = img_yuv420.data + height * width;
        while (i < size)
        {
            img_vu_ptr++;
            *img_u_ptr++ = *img_vu_ptr++;
            ++i;
            ++i;
        }
    
        Mat upSampled_U(height, width, CV_8UC1);
        Mat upSampled_V(height, width, CV_8UC1);
        UpSample(U, upSampled_U);
        UpSample(V, upSampled_V);
    
        vector<Mat> yuv(3);
        yuv[0] = img_yuv420(Rect(0, 0, width, height));
        yuv[1] = upSampled_U;
        yuv[2] = upSampled_V;
        merge(yuv, img_yuv444);
    }
    

    2、YUV444转YUV420,实际就是对色度(V和U)进行降采样,最为简单的实现思路是直接取值(4个中取一个)。

    static void DownSample(Mat & srcImg, Mat & dstImg)
    {
        int rows = dstImg.rows;
        int cols = dstImg.cols;
        for (int row = 0; row < rows; ++row)
        {
            uchar * srcImg_ptr = srcImg.ptr<uchar>(row << 1);
            uchar * dstImg_ptr = dstImg.ptr<uchar>(row);
            for (int col = 0; col < cols; ++col)
            {
                dstImg_ptr[col] = srcImg_ptr[col << 1];
            }
        }
    }
    
    void yuv444_to_yuv420(const Mat & img_yuv444, Mat & img_yuv420)
    {
        assert(img_yuv444.channels() == 3);
        assert(img_yuv420.channels() == 1);
        vector<Mat> yuv(3);
        split(img_yuv444, yuv);
        int height = yuv[0].rows;
        int width = yuv[0].cols;
        img_yuv420.create(Size(width, height * 1.5), CV_8UC1);
        Mat downSampled_U(height / 2, width / 2, CV_8UC1);
        Mat downSampled_V(height / 2, width / 2, CV_8UC1);
        DownSample(yuv[1], downSampled_U);
        DownSample(yuv[2], downSampled_V);
        yuv[0].copyTo(img_yuv420(Rect(0, 0, width, height)));
    
        Mat vu420(height / 2, width, CV_8UC1);
        uchar * img_vu_ptr = vu420.data;
        uchar * img_v_ptr = downSampled_V.data;
        uchar * img_u_ptr = downSampled_U.data;
        int size = width * height / 2;
        int i = 0;
        while (i < size)
        {
            if (i & 0x1)
                *img_vu_ptr++ = *img_u_ptr++;
            else
                *img_vu_ptr++ = *img_v_ptr++;
            ++i;
        }
        vu420.copyTo(img_yuv444(Rect(0, height, width, height / 2)));
    }
    

    3、保存yuv420为nv21文件

    void saveYUV420_to_nv21(const Mat & yuv420_Img, string nv21_file_name)
    {
        FILE  *fp = fopen(nv21_file_name.c_str(), "wb");   // string.c_str() transform string to char *
    
        if (yuv420_Img.empty())
        {
            std::cout << "Empty! check your image! " << endl;
            return;
        }
        int cols = yuv420_Img.cols;
        int rows = yuv420_Img.rows;
        int size = rows * cols;
        // unsigned char * nv21_buff = new unsigned char[size];
        uchar * yuv420_img_ptr = yuv420_Img.data;
        // size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream))  把ptr所指向的数组中的数据写入到给定流 stream 中
        // ptr --- 指向要被写入的元素数组的指针;
        // size --- 被写入的每个元素大小,字节为单位
        // nmemb --- 元素个数,每个远大的大小为size字节
        // stream --- FILE对象的指针,FILE对象指定了一个输出流
        // 如果成功,返回一个size_t对象,表示元素的总数;如果该数与nmemb不同,则会输出一个错误
        fwrite(yuv420_img_ptr, sizeof(uchar), size, fp);
        // for (int i = 0; i < size; ++i){
        //      fwrite(yuv420_img_ptr++, 1, 1, fp);
        // }
        fclose(fp);
    }