преобразование изображения из YV12 в NV12 с помощью opencv (c++)

У меня есть веб-камера, с которой я читаю кадры в формате NV12. Я конвертирую кадры в RGB, затем в YV12, и моя цель - преобразовать их обратно в NV12 для целей проверки. Я делаю примерно так:

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

Я написал следующую функцию для преобразования из YV12 в NV12(похожую на этот пост - Конвертировать YV12 в NV21 (YUV YCrCb 4: 2: 0) ), которая, похоже, не работает. Я получаю изображение в градациях серого с размытой пурпурной копией, смешанной с верхней половиной, с размытой зеленой копией, смешанной с нижней половиной моего итогового изображения.

В моей функции ниже я предполагаю макет, в котором V-плоскость находится рядом с U-плоскостью в матрице. Не знаю, правильно ли это. Сначала я попробовал следовать макету для YV12, как показано на https://docs.microsoft.com/en-us/windows/win32/medfound/recommended-8-bit-yuv-formats-for-video-rendering, где U / Самолеты V сидят друг под другом, а не рядом, но это привело к аварии.

      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);
                }
        }
}

Любые подсказки приветствуются.

1 ответ

Применение преобразования с использованием индексации сбивает с толку.
Мое предложение рассматривает изображение YV12 как 3 отдельных изображения.

  • Y (ширина x высота) - верхнее изображение.
  • V (ширина / 2 x высота / 2) - ниже Y
  • U (ширина / 2 x высота / 2) - ниже V

Согласно следующей документации:

YV12 в точности похож на I420, но порядок плоскостей U и V обратный.

I420 заказывается как здесь :

Преобразование BGR в I420 поддерживается OpenCV и более документированным форматом по сравнению с YV12, поэтому лучше начать тестирование с I420 и продолжить с YV12 (переключая каналы U и V).


Основная идея состоит в том, чтобы «обернуть» матрицы V и U объектами cv:Mat (установка указателя матрицы путем добавления смещений во входные данные). data указатель).

  • inV идет после Y (половина разрешения по каждой оси и половина шага):
          cv::Mat inV = cv::Mat(cv::Size(width/2, height/2), CV_8UC1, (unsigned char*)input.data + stride*height, stride/2);
  • inU идет после V (половинное разрешение по каждой оси и половина шага):
          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);

Вот функция преобразования:

      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);
        }
    }
}

Внедрение и тестирование:

Создание образца образа NV12 с инструмента командной строки помощьюFFmpeg :

      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

Создание образца изображения YV12 с использованием MATLAB (или 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 ++ (как I420toNV12, так и 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();
}

Тестирование вывода с использованием MATLAB (или OCTAVE):

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

Вход (YV12 как изображение в оттенках серого):

Вход (NV12 как изображение в градациях серого):

Другие вопросы по тегам