Converting YUV Video Frames to RGB Format with OpenCV

Objective

Process uncompressed video files stored in YUV format and convert them to RGB for visualization.

Development Environment

  • Visual Studio 2022
  • OpenCV 3.4.16
  • C++17
  • FFMPEG 6.1.1

Implementation

File paths must use double backslashes (C:\\...) or forward slashes (C:/), as single backslashes are interpreted as escape characters in C++ strings.

#define _CRT_SECURE_NO_WARNINGS

#include <iostream>
#include <opencv2/opencv.hpp>

constexpr const char* INPUT_YUV_PATH = "path/to/video.yuv";
constexpr int VIDEO_HEIGHT = 480;
constexpr int VIDEO_WIDTH = 832;
constexpr int FRAME_RATE = 30;

using namespace cv;

void convertYuv420ToRgb(const uint8_t* yuvBuffer, uint8_t* rgbOutput, int frameWidth, int frameHeight)
{
    const uint8_t* yPlane = yuvBuffer;
    const uint8_t* uPlane = yuvBuffer + (frameHeight * frameWidth);
    const uint8_t* vPlane = uPlane + (frameHeight * frameWidth / 4);

    for (int row = 0; row < frameHeight; ++row)
    {
        for (int col = 0; col < frameWidth; ++col)
        {
            const int yIdx = row * frameWidth + col;
            const int uvIdx = (row / 2) * (frameWidth / 2) + (col / 2);

            const int yValue = yPlane[yIdx] - 16;
            const int uValue = uPlane[uvIdx] - 128;
            const int vValue = vPlane[uvIdx] - 128;

            const int r = (298 * yValue + 409 * vValue + 128) >> 8;
            const int g = (298 * yValue - 100 * uValue - 208 * vValue + 128) >> 8;
            const int b = (298 * yValue + 516 * uValue + 128) >> 8;

            const int pixelOffset = (row * frameWidth + col) * 3;
            rgbOutput[pixelOffset + 0] = saturate_cast<uint8_t>(b);
            rgbOutput[pixelOffset + 1] = saturate_cast<uint8_t>(g);
            rgbOutput[pixelOffset + 2] = saturate_cast<uint8_t>(r);
        }
    }
}

int main()
{
    FILE* videoFile = fopen(INPUT_YUV_PATH, "rb");
    if (!videoFile)
    {
        std::cerr << "Failed to open input file: " << INPUT_YUV_PATH << std::endl;
        return -1;
    }

    Mat yuvFrame(VIDEO_HEIGHT + VIDEO_HEIGHT / 2, VIDEO_WIDTH, CV_8UC1);
    Mat rgbFrame(VIDEO_HEIGHT, VIDEO_WIDTH, CV_8UC3);

    bool continuePlayback = true;

    while (continuePlayback)
    {
        const size_t expectedBytes = yuvFrame.total() * yuvFrame.elemSize();
        const size_t bytesRead = fread(yuvFrame.data, 1, expectedBytes, videoFile);

        if (bytesRead != expectedBytes)
        {
            std::cerr << "End of file reached or read error occurred." << std::endl;
            break;
        }

        convertYuv420ToRgb(yuvFrame.data, rgbFrame.data, VIDEO_WIDTH, VIDEO_HEIGHT);

        imshow("Original YUV", yuvFrame);
        imshow("Converted RGB", rgbFrame);

        if (waitKey(1000 / FRAME_RATE) >= 0)
        {
            continuePlayback = false;
        }
    }

    fclose(videoFile);
    destroyAllWindows();
    return 0;
}

Generating Test YUV Files with FFMPEG

Convert an MKV video to YUV420p format at 832x480 resolution:

ffmpeg -i input.mkv -s 832x480 -pix_fmt yuv420p output.yuv

Inspect the YUV file properties:

ffprobe -show_format -video_size 832x480 output.yuv

Play the YUV file for verification:

ffplay -video_size 832x480 -i output.yuv

Technical Background

YUV420 I420 Memory Layout

In the I420 (YUV420 planar) format, the Y plane comes first, followed by U and V planes. The Y component has the same number of samples as pixels in the frame. The U and V planes each contain one quarter of the pixel count since theey are subsampled horizontally and vertically by a factor of 2.

The memory arrangement follows this pattern:

  • Y plane: width × height bytes
  • U plane: (width/2) × (height/2) bytes
  • V plane: (width/2) × (height/2) bytes

Each 2×2 block of Y pixels shares a single U and V sample, which is why this forrmat achieves 12 bits per pixel compared to 24 bits for RGB.

Color Space Conversion Mathematics

The conversion from YUV to RGB employs the following formulas:

Y' = Y - 16
U' = U - 128
V' = V - 128

R = (298 × Y' + 409 × V' + 128) / 256
G = (298 × Y' - 100 × U' - 208 × V' + 128) / 256
B = (298 × Y' + 516 × U' + 128) / 256

The subtraction of 16 from Y and 128 from U,V normalizes the ranges. Y ranges from 16-235 while U and V range from 16-240. The offset brings them to a symmetric range around zero for accurate multiplication. The final division by 256 (implemented as right shift by 8) scales the results back to the valid 0-255 RGB range. The saturate_cast function ensures values exceeding 0-255 are clamped appropriately.

Tags: YUV RGB OpenCV Video Processing C++

Posted on Sat, 16 May 2026 08:40:09 +0000 by ibechane