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:

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):

Input (NV12 as grayscale image):
