오늘은 YUV 파일에 대해 이야기 해보고, RAW TO YUV, YUV TO RAW 코드를 살펴보겠습니다.

YUV 컬러 공간은 인간의 눈의 특성을 고려하여 설계되었습니다.

  • 인간의 눈은 밝기(Y)에는 민감하지만, 색차 신호(Cb, Cr)에는 상대적으로 둔감합니다.
  • 이러한 특성을 활용하여 RGB 모델을 선형 변환하여 YUV 모델로 표현할 수 있습니다.

이번 글에서는 YUV 컬러 공간의 변환 공식과 저장 형식(YUV 422 및 420)에 대해 설명해보겠습니다.

변환 공식

RGB 모델에서 YUV 모델로의 선형 변환 공식은 다음과 같습니다.

RGB → YUV

\[Y = 0.257R' + 0.504G' + 0.098B' + 16\] \[Cb = -0.148R' - 0.291G' + 0.439B' + 128\] \[Cr = 0.439R' - 0.368G' - 0.071B' + 128\]

YUV → RGB

\[R' = 1.164(Y - 16) + 1.596(Cr - 128)\] \[G' = 1.164(Y - 16) - 0.813(Cr - 128) - 0.392(Cb - 128)\] \[B' = 1.164(Y - 16) + 2.017(Cb - 128)\]
  • Y (밝기, Luminance): 밝기를 나타내는 신호입니다. YUV 컬러 영상에서 Y 성분만 추출하면 흑백 영상을 얻을 수 있습니다.

  • Cb 및 Cr (색차, Chrominance): 각각 파랑(Cb) 및 빨강(Cr)의 색차 신호를 나타냅니다.

YUV 저장 형식: 422 및 420

YUV 422

  • Y, Cb, Cr이 4:2:2 비율로 저장됩니다.

  • Cb와 Cr 값은 가로로 두 픽셀당 하나씩 공유됩니다.

YUV 420

  • Y, Cb, Cr이 4:1:1 비율로 저장됩니다.

  • Cb와 Cr 값은 2x2 블록의 픽셀에서 공유됩니다 (가로로 네 픽셀이 아님에 주의!).

저장 순서

  • 데이터는 Y 성분 → Cb 성분 → Cr 성분 순으로 저장됩니다.

  • RAW나 BMP처럼 픽셀별로 저장하지 않고, Y 성분을 모두 저장한 후, Cb와 Cr 성분을 차례로 저장합니다.

YUV 압축의 장점

Cb와 Cr 픽셀 수를 줄이면서도 사람이 느끼는 품질을 유지할 수 있기 때문에 YUV 포맷은 파일 크기를 크게 줄일 수 있습니다. 뷰어 프로그램의 차이로 인해 약간의 차이가 보일 수 있으나, 인간의 시각적 특성 때문에 결과 영상의 품질은 동일하게 느껴집니다.

YUV 컬러 공간과 YUV 422, 420과 같은 저장 형식은 비디오 압축 및 전송에 널리 사용됩니다. YUV 변환 및 저장 원리를 이해하면, 품질 저하 없이 파일 크기를 효과적으로 줄이는 방법을 더 잘 이해할 수 있습니다. 아래 결과를 보면 압축이 되는 것을 볼 수 있습니다.

아래 C 코드를 공유하면서 오늘 포스팅 마치겠습니다.

RAW TO YUV

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define VER 256     // 이미지 수직 사이즈 상수 VER 정의
#define HOR 256     // 이미지 수평 사이즈 상수 HER 정의

void raw_to_YUV422(unsigned char input[][HOR * 3], unsigned char output[][HOR / 2], FILE* fout); // RGB 이미지를 YUV422형식으로 변환하는 함수
void raw_to_YUV420(unsigned char input[][HOR * 3], unsigned char output[][HOR / 2], FILE* fout); // RGB 이미지를 YUV420형식으로 변환하는 함수

int main() {
    unsigned char(*input)[HOR * 3] = (unsigned char(*)[HOR * 3])malloc(VER * HOR * 3 * sizeof(unsigned char)); // RGB 이미지 데이터를 저장하기 위한 메모리 할당
    unsigned char(*outputUV)[HOR / 2] = (unsigned char(*)[HOR / 2])malloc(VER * HOR / 2 * sizeof(unsigned char)); // YUV 변환을 위한 U, V 컴포넌트를 저장할 메모리 할당

    if (!input || !outputUV) { // 메모리 할당이 제대로 되었는지 확인
        perror("Memory allocation failed\n");
        return 1;
    }

    FILE* fin, * fout422, * fout420; // 파일 포인터 선언
    fin = fopen("Lena_Color.raw", "rb"); // 입력 RGB 파일 열기
    if (!fin) { // 파일 열기에 실패했는지 확인
        perror("Unable to open input file\n");
        return 1;
    }
    fread(input[0], sizeof(unsigned char), VER * HOR * 3, fin); // 입력 파일로부터 RGB 데이터 읽기
    fclose(fin);

    fout422 = fopen("Lena_Color422.yuv", "wb"); // YUV 4:2:2 출력 파일 열기
    if (!fout422) { // 파일 열기에 실패했는지 확인
        perror("Unable to open output file for YUV 4:2:2\n");
        return 1;
    }
    raw_to_YUV422(input, outputUV, fout422); // RGB를 YUV 4:2:2로 변환하여 파일에 쓰기
    fclose(fout422);

    fout420 = fopen("Lena_Color420.yuv", "wb"); // YUV 4:2:0 출력 파일 열기
    if (!fout420) {
        perror("Unable to open output file for YUV 4:2:0\n");
        return 1;
    }
    raw_to_YUV420(input, outputUV, fout420); // RGB를 YUV 4:2:0로 변환하여 파일에 쓰기
    fclose(fout420);

    // 할당된 메모리 해제
    free(input);
    free(outputUV);

    return 0;
}

void raw_to_YUV422(unsigned char input[][HOR * 3], unsigned char outputUV[][HOR / 2], FILE* fout) { // RGB를 YUV 4:2:2 형식으로 변환하는 함수
    unsigned char Y, U, V; // Y, U, V 컴포넌트를 저장할 변수 선언

    // Y 값 쓰기
    for (int j = 0; j < VER; j++) {
        for (int i = 0; i < HOR * 3; i += 3) {
            // RGB를 Y 컴포넌트로 변환하고 파일에 쓰기
            Y = (unsigned char)(0.257 * input[j][i] + 0.504 * input[j][i + 1] + 0.098 * input[j][i + 2] + 16);
            
            fwrite(&Y, sizeof(unsigned char), 1, fout);

        }
        
    }

    // U와 V 값 쓰기 (수평으로 2:1로 서브샘플링)
    for (int j = 0; j < VER; j++) {
        for (int i = 0; i < HOR * 3; i += 6) {
            // RGB를 U 컴포넌트로 변환하고 파일에 쓰기
            U = (unsigned char)(-0.148 * (input[j][i] + input[j][i + 3]) / 2 - 0.291 * (input[j][i + 1] + input[j][i + 4]) / 2 + 0.439 * (input[j][i + 2] + input[j][i + 5]) / 2 + 128);
            
            // RGB를 V 컴포넌트로 변환하고 나중에 쓰기 위해 저장
            V = (unsigned char)(0.439 * (input[j][i] + input[j][i + 3]) / 2 - 0.368 * (input[j][i + 1] + input[j][i + 4]) / 2 - 0.071 * (input[j][i + 2] + input[j][i + 5]) / 2 + 128);
            
            fwrite(&U, sizeof(unsigned char), 1, fout);
            outputUV[j][i / 6] = V;
            
        }
    }
    // V 값 쓰기
    for (int j = 0; j < VER; j++) {
        fwrite(outputUV[j], sizeof(unsigned char), HOR / 2, fout);
    }
    
}

void raw_to_YUV420(unsigned char input[][HOR * 3], unsigned char outputUV[][HOR / 2], FILE* fout) { // RGB를 YUV 4:2:0 형식으로 변환하는 함수
    unsigned char Y, U, V; // Y, U, V 컴포넌트를 저장할 변수 선언
    // Y 값 쓰기
    for (int j = 0; j < VER; j++) {
        for (int i = 0; i < HOR * 3; i += 3) {
            Y = (unsigned char)(0.257 * input[j][i] + 0.504 * input[j][i + 1] + 0.098 * input[j][i + 2] + 16);
            
            fwrite(&Y, sizeof(unsigned char), 1, fout);
            
        }
        
    }

    // U와 V 값 쓰기 (수평 및 수직으로 2:1로 서브샘플링)
    for (int j = 0; j < VER; j += 2) {
        for (int i = 0; i < HOR * 3; i += 6) {
            // RGB를 U 컴포넌트로 변환하고 파일에 쓰기
            U = (unsigned char)(-0.148 * (input[j][i] + input[j][i + 3] + input[j + 1][i] + input[j + 1][i + 3]) / 4 - 0.291 * (input[j][i + 1] + input[j][i + 4] + input[j + 1][i + 1] + input[j + 1][i + 4]) / 4 + 0.439 * (input[j][i + 2] + input[j][i + 5] + input[j + 1][i + 2] + input[j + 1][i + 5]) / 4 + 128);
            
            // RGB를 V 컴포넌트로 변환하고 나중에 쓰기 위해 저장
            V = (unsigned char)(0.439 * (input[j][i] + input[j][i + 3] + input[j + 1][i] + input[j + 1][i + 3]) / 4 - 0.368 * (input[j][i + 1] + input[j][i + 4] + input[j + 1][i + 1] + input[j + 1][i + 4]) / 4 - 0.071 * (input[j][i + 2] + input[j][i + 5] + input[j + 1][i + 2] + input[j + 1][i + 5]) / 4 + 128);
            
            fwrite(&U, sizeof(unsigned char), 1, fout);
            outputUV[j / 2][i / 6] = V; // Save V to write after U
        }
    }
    // V 값 쓰기
    for (int j = 0; j < VER / 2; j++) {
        
        fwrite(outputUV[j], sizeof(unsigned char), HOR / 2, fout);
    }
    
}


YUV TO RAW

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>

#define VER 256     // 이미지 수직 사이즈 상수 VER 정의
#define HOR 256     // 이미지 수평 사이즈 상수 HOR 정의

void YUV_to_RGB(int Y, int U, int V, unsigned char* R, unsigned char* G, unsigned char* B);
void YUV422_to_raw(unsigned char inputY[][HOR], unsigned char inputU[][HOR / 2], unsigned char inputV[][HOR / 2], unsigned char output[][HOR * 3]);
void YUV420_to_raw(unsigned char inputY[][HOR], unsigned char inputU[][HOR / 2], unsigned char inputV[][HOR / 2], unsigned char output[][HOR * 3]);

int main() {
    unsigned char(*inputY)[HOR] = (unsigned char(*)[HOR])malloc(VER * HOR * sizeof(unsigned char)); // Y배열에 대한 메모리 할당
    unsigned char(*inputU)[HOR / 2] = (unsigned char(*)[HOR / 2])malloc(VER / 2 * HOR / 2 * sizeof(unsigned char)); // 420 U 배열에 대한 메모리 할당
    unsigned char(*inputV)[HOR / 2] = (unsigned char(*)[HOR / 2])malloc(VER / 2 * HOR / 2 * sizeof(unsigned char)); // 420 V 배열에 대한 메모리 할당
    unsigned char(*inputU2)[HOR / 2] = (unsigned char(*)[HOR / 2])malloc(VER * HOR / 2 * sizeof(unsigned char)); // 422 U 배열에 대한 메모리 할당
    unsigned char(*inputV2)[HOR / 2] = (unsigned char(*)[HOR / 2])malloc(VER * HOR / 2 * sizeof(unsigned char)); // 422 V 배열에 대한 메모리 할당
    unsigned char(*output)[HOR * 3] = (unsigned char(*)[HOR * 3])malloc(VER * HOR * 3 * sizeof(unsigned char)); // 출력 배열에 대한 메모리 할당

    if (!inputY || !inputU || !inputV || !output) {
        perror("Memory allocation failed\n");
        return 1;
    }

    // YUV422 파일 읽기
    FILE* fin422 = fopen("YUV422_Lena_Color.yuv", "rb"); //YUV422_Lena_Color.yuv 입력 파일 포인터 선언, 읽기 모드
    if (!fin422) {
        perror("Unable to open input YUV422 file\n");
        return 1;
    }
    fread(inputY[0], sizeof(unsigned char), VER * HOR, fin422); // 422 Y값 읽기
    fread(inputU2[0], sizeof(unsigned char), VER * HOR / 2, fin422); // 422 U값 읽기
    fread(inputV2[0], sizeof(unsigned char), VER * HOR / 2, fin422); // 422 V값 읽기
    fclose(fin422);

    FILE* fout = fopen("Lena_Color_From_YUV422.raw", "wb"); // YUV422_Lena_Color.yuv 출력 파일 포인터 선언, 쓰기 모드
    if (!fout) {
        perror("Unable to open output raw file from YUV422\n");
        return 1;
    }
    YUV422_to_raw(inputY, inputU2, inputV2, output); // YUV422 TO RAW 함수
    fwrite(output[0], sizeof(unsigned char), VER * HOR * 3, fout); // 파일 쓰기
    fclose(fout);

    // YUV420 파일 읽기
    FILE* fin420 = fopen("YUV420_Lena_Color.yuv", "rb"); //YUV420_Lena_Color.yuv 입력 파일 포인터 선언, 읽기 모드
    if (!fin420) {
        perror("Unable to open input YUV420 file\n");
        return 1;
    }
    fread(inputY[0], sizeof(unsigned char), VER * HOR, fin420); // 420 Y값 읽기
    fread(inputU[0], sizeof(unsigned char), VER / 2 * HOR / 2, fin420); // 420 U값 읽기
    fread(inputV[0], sizeof(unsigned char), VER / 2 * HOR / 2, fin420); // 420 V값 읽기
    fclose(fin420);

    fout = fopen("Lena_Color_From_YUV420.raw", "wb"); // YUV420_Lena_Color.yuv 출력 파일 포인터 선언, 쓰기 모드
    if (!fout) {
        perror("Unable to open output raw file from YUV420\n");
        return 1;
    }
    YUV420_to_raw(inputY, inputU, inputV, output); //YUV TO RAW 변환 함수
    fwrite(output[0], sizeof(unsigned char), VER * HOR * 3, fout); // 파일 쓰기
    fclose(fout);

    free(inputY);
    free(inputU);
    free(inputV);
    free(output);

    return 0;
}


void YUV_to_RGB(int Y, int U, int V, unsigned char* R, unsigned char* G, unsigned char* B) { // YUV to RGB 공식
    int R_temp = (int)(1.164 * (Y - 16) + 1.596 * (V - 128));
    int G_temp = (int)(1.164 * (Y - 16) - 0.813 * (V - 128) - 0.392 * (U - 128));
    int B_temp = (int)(1.164 * (Y - 16) + 2.017 * (U - 128));

    // 클램핑 적용
    *R = (unsigned char)(R_temp < 0 ? 0 : R_temp > 255 ? 255 : R_temp);
    *G = (unsigned char)(G_temp < 0 ? 0 : G_temp > 255 ? 255 : G_temp);
    *B = (unsigned char)(B_temp < 0 ? 0 : B_temp > 255 ? 255 : B_temp);
}

void YUV422_to_raw(unsigned char inputY[][HOR], unsigned char inputU[][HOR / 2], unsigned char inputV[][HOR / 2], unsigned char output[][HOR * 3]) {
    for (int j = 0; j < VER; j++) {
        for (int i = 0; i < HOR; i += 2) {
            
            int Y1 = inputY[j][i];
            int Y2 = inputY[j][i + 1];
            int U = inputU[j][i / 2];
            int V = inputV[j][i / 2];

            unsigned char R, G, B;
            YUV_to_RGB(Y1, U, V, &R, &G, &B);
            output[j][i * 3] = R;
            output[j][i * 3 + 1] = G;
            output[j][i * 3 + 2] = B;

            YUV_to_RGB(Y2, U, V, &R, &G, &B);
            output[j][(i + 1) * 3] = R;
            output[j][(i + 1) * 3 + 1] = G;
            output[j][(i + 1) * 3 + 2] = B;
        }
    }
}

void YUV420_to_raw(unsigned char inputY[][HOR], unsigned char inputU[][HOR / 2], unsigned char inputV[][HOR / 2], unsigned char output[][HOR * 3]) {
    for (int j = 0; j < VER; j += 2) {
        for (int i = 0; i < HOR; i += 2) {
            int U = inputU[j / 2][i / 2];
            int V = inputV[j / 2][i / 2];

            for (int y = 0; y < 2; ++y) {
                for (int x = 0; x < 2; ++x) {
                    int Y = inputY[j + y][i + x];
                    unsigned char R, G, B;
                    YUV_to_RGB(Y, U, V, &R, &G, &B);
                    output[j + y][(i + x) * 3] = R;
                    output[j + y][(i + x) * 3 + 1] = G;
                    output[j + y][(i + x) * 3 + 2] = B;
                }
            }
        }
    }
}