2

I have a webcam from which I read frames in NV12 format. I convert the frames to RGB, then to YV12, and my goal is to convert them back to NV12, for verification purposes. I am doing something like this:

cv::cvtColor(InputFrame, InputRGB, cv::COLOR_YUV2RGB_NV12);
cv::cvtColor(InputRGB, OutputYV12, cv::COLOR_RGB2YUV_YV12);

I wrote the following function to convert from YV12 to NV12 (similar to this post - Convert YV12 to NV21 (YUV YCrCb 4:2:0)), which doesn't seem to work. I get a grayscale image with a vague magenta copy mixed over the top half with a vague green copy mixed over the bottom half of my resulting image.

In my function below I'm assuming a layout where the V-plane is sitting next to the U-plane in the matrix. I don't know if that is correct. I first tried following the layout for YV12 as shown at https://learn.microsoft.com/en-us/windows/win32/medfound/recommended-8-bit-yuv-formats-for-video-rendering where the U/V planes sit underneath each other instead of next to each other, but that caused a crash.

void YV12toNV12(const cv::Mat& input, cv::Mat& output, int width, int height) {

        input.copyTo(output);

        for (int row = 0; row < height/2; row++) {
                for (int col = 0; col < width/2; col++) {
                        output.at<uchar>(height + row, 2 * col) = input.at<uchar>(height + row, col);
                        output.at<uchar>(height + row, 2 * col + 1) = input.at<uchar>(height + row, width/2 + col);
                }
        }
}

Any hints appreciated.

Cris Luengo
  • 55,762
  • 10
  • 62
  • 120
b20000
  • 995
  • 1
  • 12
  • 30
  • 1
    According to the [following](https://wiki.videolan.org/YUV#YV12) documentation: "YV12 is exactly like I420, but the order of the U and V planes is reversed". I420 is ordered as [here](https://upload.wikimedia.org/wikipedia/commons/7/7d/I420.jpg). – Rotem Jul 28 '21 at 15:40

1 Answers1

4

Applying the conversion using indexing is confusing.
My suggestion is treating the YV12 image as 3 separate images.

  • Y (width x height) - Top Image.
  • V (width/2 x height/2) - Below Y
  • U (width/2 x height/2) - Below V

According to the following documentation:

YV12 is exactly like I420, but the order of the U and V planes is reversed.

I420 is ordered as here:
enter image description here

BGR to I420 conversion is supported by OpenCV, and more documented format compared to YV12, so we better start testing with I420, and continue with YV12 (by switching U and V channels).


The main idea is "wrapping" V and U matrices with cv:Mat objects (setting matrix data pointer by adding offsets to the input data pointer).

  • inV Comes after Y (half resolution in each axis, and half stride):
    cv::Mat inV = cv::Mat(cv::Size(width/2, height/2), CV_8UC1, (unsigned char*)input.data + stride*height, stride/2);
  • inU Comes after V (half resolution in each axis, and half stride):
    cv::Mat inU = cv::Mat(cv::Size(width/2, height/2), CV_8UC1, (unsigned char*)input.data + stride*height + (stride/2)*(height/2), stride/2);

Here is the conversion function:

void YV12toNV12(const cv::Mat& input, cv::Mat& output) {
    int width = input.cols;
    int height = input.rows * 2 / 3;
    int stride = (int)input.step[0];    //Rows bytes stride - in most cases equal to width

    input.copyTo(output);

    //Y Channel
    // YYYYYYYYYYYYYYYY
    // YYYYYYYYYYYYYYYY
    // YYYYYYYYYYYYYYYY
    // YYYYYYYYYYYYYYYY
    // YYYYYYYYYYYYYYYY
    // YYYYYYYYYYYYYYYY

    //V Input channel
    // VVVVVVVV
    // VVVVVVVV
    // VVVVVVVV
    cv::Mat inV = cv::Mat(cv::Size(width / 2, height / 2), CV_8UC1, (unsigned char*)input.data + stride * height, stride / 2);   // Input V color channel (in YV12 V is above U).

    //U Input channel
    // UUUUUUUU
    // UUUUUUUU
    // UUUUUUUU
    cv::Mat inU = cv::Mat(cv::Size(width / 2, height / 2), CV_8UC1, (unsigned char*)input.data + stride * height + (stride / 2)*(height / 2), stride / 2);  //Input V color channel (in YV12 U is below V).

    for (int row = 0; row < height / 2; row++) {
        for (int col = 0; col < width / 2; col++) {
            output.at<uchar>(height + row, 2 * col) = inU.at<uchar>(row, col);
            output.at<uchar>(height + row, 2 * col + 1) = inV.at<uchar>(row, col);
        }
    }
}

Implementation and Testing:

Creating NV12 sample image using FFmpeg command line tool:

ffmpeg -y -f lavfi -i testsrc=size=192x108:rate=1:duration=1 -pix_fmt nv12 -f rawvideo test.nv12
ffmpeg -y -f rawvideo -pixel_format gray -video_size 192x162 -i test.nv12 -pix_fmt gray test_nv12.png

Creating YV12 sample image using MATLAB (or OCTAVE):

NV12 = imread('test_nv12.png');
Y = NV12(1:108, :);
U = NV12(109:end, 1:2:end);
V = NV12(109:end, 2:2:end);

f = fopen('test.yv12', 'w');
fwrite(f, Y', 'uint8');
fwrite(f, V', 'uint8');
fwrite(f, U', 'uint8');
fclose(f);

f = fopen('test.yv12', 'r');
I = fread(f, [192, 108*1.5], '*uint8')';
fclose(f);
imwrite(I, 'test_yv12.png');

C++ implementation (both I420toNV12 and YV12toNV12):

#include "opencv2/opencv.hpp"

void YV12toNV12(const cv::Mat& input, cv::Mat& output) {
    int width = input.cols;
    int height = input.rows * 2 / 3;
    int stride = (int)input.step[0];    //Rows bytes stride - in most cases equal to width

    input.copyTo(output);

    //Y Channel
    // YYYYYYYYYYYYYYYY
    // YYYYYYYYYYYYYYYY
    // YYYYYYYYYYYYYYYY
    // YYYYYYYYYYYYYYYY
    // YYYYYYYYYYYYYYYY
    // YYYYYYYYYYYYYYYY

    //V Input channel
    // VVVVVVVV
    // VVVVVVVV
    // VVVVVVVV
    cv::Mat inV = cv::Mat(cv::Size(width / 2, height / 2), CV_8UC1, (unsigned char*)input.data + stride * height, stride / 2);   // Input V color channel (in YV12 V is above U).

    //U Input channel
    // UUUUUUUU
    // UUUUUUUU
    // UUUUUUUU
    cv::Mat inU = cv::Mat(cv::Size(width / 2, height / 2), CV_8UC1, (unsigned char*)input.data + stride * height + (stride / 2)*(height / 2), stride / 2);  //Input V color channel (in YV12 U is below V).

    for (int row = 0; row < height / 2; row++) {
        for (int col = 0; col < width / 2; col++) {
            output.at<uchar>(height + row, 2 * col) = inU.at<uchar>(row, col);
            output.at<uchar>(height + row, 2 * col + 1) = inV.at<uchar>(row, col);
        }
    }
}


void I420toNV12(const cv::Mat& input, cv::Mat& output) {
    int width = input.cols;
    int height = input.rows * 2 / 3;
    int stride = (int)input.step[0];    //Rows bytes stride - in most cases equal to width
    
    input.copyTo(output);

    //Y Channel
    // YYYYYYYYYYYYYYYY
    // YYYYYYYYYYYYYYYY
    // YYYYYYYYYYYYYYYY
    // YYYYYYYYYYYYYYYY
    // YYYYYYYYYYYYYYYY
    // YYYYYYYYYYYYYYYY
    
    //U Input channel
    // UUUUUUUU
    // UUUUUUUU
    // UUUUUUUU
    cv::Mat inU = cv::Mat(cv::Size(width / 2, height / 2), CV_8UC1, (unsigned char*)input.data + stride * height, stride / 2);   // Input U color channel (in I420 U is above V).

    //V Input channel
    // VVVVVVVV
    // VVVVVVVV
    // VVVVVVVV
    cv::Mat inV = cv::Mat(cv::Size(width/2, height/2), CV_8UC1, (unsigned char*)input.data + stride*height + (stride/2)*(height/2), stride/2);  //Input V color channel (in I420 V is below U).

    for (int row = 0; row < height / 2; row++) {
        for (int col = 0; col < width / 2; col++) {
            output.at<uchar>(height + row, 2 * col) = inU.at<uchar>(row, col);
            output.at<uchar>(height + row, 2 * col + 1) = inV.at<uchar>(row, col);
        }
    }
}


int main()
{   
    //cv::Mat input = cv::imread("test_I420.png", cv::IMREAD_GRAYSCALE);
    //cv::Mat output;
    //I420toNV12(input, output);
    //cv::imwrite("output_NV12.png", output);

    cv::Mat input = cv::imread("test_YV12.png", cv::IMREAD_GRAYSCALE);
    cv::Mat output;

    YV12toNV12(input, output);
    cv::imwrite("output_NV12.png", output);

    cv::imshow("input", input);
    cv::imshow("output", output);
    cv::waitKey(0);
    cv::destroyAllWindows();
}

Testing the output using MATLAB (or OCTAVE):

A = imread('test_nv12.png');
B = imread('output_NV12.png');
display(isequal(A, B))

Input (YV12 as grayscale image):
enter image description here

Input (NV12 as grayscale image):
enter image description here

Rotem
  • 30,366
  • 4
  • 32
  • 65