YUV 文件是一种存储 视频原始数据 的格式,它不进行压缩(或仅轻度压缩),主要用于专业视频处理、图像分析、计算机视觉等领域。以下是关于 YUV 文件的详细说明:


1. YUV 是什么?

  • YUV 是一种颜色编码系统,与常见的 RGB(红绿蓝)不同,它将图像数据分成:
    • Y(亮度/Luma):决定图像的明暗信息(黑白部分)。
    • U & V(色度/Chroma):存储颜色信息(色彩饱和度、色调)。
  • 这种分离设计最初是为了兼容黑白与彩色电视信号,现在广泛用于视频压缩(如 H.264、H.265)和数字视频处理。

2. YUV 文件的特点

  • 原始数据:通常未经压缩,文件体积较大(例如 1 分钟 1080 p 视频可能占数 GB)。
  • 多种子格式:根据 UV 分量的采样方式不同,分为多种子格式:
    • YUV420:最常用(如 MP4、H.264 视频的底层存储),色度信息缩减采样以节省空间。
    • YUV 444:高质量无缩减(用于专业影视后期)。
    • 其他变体:YUV 422、NV 12 等。
  • 无标准封装:YUV 文件通常只是纯二进制数据流,需额外说明分辨率、采样格式才能正确解析。

3. YUV 文件的常见用途

  • 视频编解码开发:测试编码器/解码器的性能(如 FFmpeg 测试)。
  • 计算机视觉:人脸识别、运动检测等算法处理原始视频数据。
  • 影视后期:无损编辑或色彩校正时保留最大信息量。
  • 学术研究:图像处理论文中常用 YUV 序列作为测试素材。

4. YUV vs. RGB

特性 YUV RGB
数据分离 亮度与色度分离 红绿蓝三通道混合
压缩效率 更高(适合视频编码) 较低
常见用途 视频存储、传输 图像显示、游戏渲染

代码

#include <SDL2/SDL.h>
#include <stdio.h>
#include <string.h>

#define YUM_HEIGHT 240
#define YUM_WIDTH 320

int s_thread_exit = 0;

#define FF_REFRESH_EVENT (SDL_USEREVENT + 1)
#define FF_QUIT_EVENT (SDL_USEREVENT + 2)

int refresh_video_func(void* val) {

    while(!s_thread_exit) {
        SDL_Event event;
        event.type = FF_REFRESH_EVENT;
        SDL_PushEvent(&event);
        SDL_Delay(40);
    }

    s_thread_exit = 0;

    SDL_Event event;
    event.type = FF_QUIT_EVENT;
    SDL_PushEvent(&event);

    return 0;
}

int main(int argc, char* argv[]) {

    SDL_Init(SDL_INIT_VIDEO);

    SDL_Window* window = NULL;
    SDL_Renderer* render = NULL;
    SDL_Texture* texture = NULL;
    SDL_Rect rect;
    SDL_Event event;
    SDL_Thread* video_thread;

    int win_width = YUM_WIDTH;
    int win_height = YUM_HEIGHT;
    int video_width = YUM_WIDTH;
    int video_height = YUM_HEIGHT;
    int video_buf_len = 0;

    uint32_t y_frame_len = video_height * video_width;
    uint32_t u_frame_len = y_frame_len / 4;
    uint32_t v_frame_len = y_frame_len / 4;
    uint32_t one_frame_len = y_frame_len + u_frame_len + v_frame_len;

    window = SDL_CreateWindow("YUV Video", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, win_width, win_height, SDL_WINDOW_OPENGL|SDL_WINDOW_RESIZABLE);

    render = SDL_CreateRenderer(window, -1, 0);

    texture = SDL_CreateTexture(render, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, video_width, video_height);

    uint8_t* video_buf = (uint8_t*)malloc(one_frame_len);

    const char* video_path = "/Users/lenn/Workspace/sdl-learn/sdl-yuv/yuv420p_320x240.yuv";

    FILE* video_fd = fopen(video_path, "rb");

    if (!video_fd) {
        fprintf(stderr, "Open yuv file failed\n");
        goto FAILED;
    }

    video_thread = SDL_CreateThread(refresh_video_func, NULL, NULL);
    printf("after create thread\n");
    while(1) {
        SDL_WaitEvent(&event);

        if (event.type == FF_REFRESH_EVENT) {
            video_buf_len = fread(video_buf, 1, one_frame_len, video_fd);

            if (video_buf_len <= 0) {
                goto FAILED;
            }

            rect.x = 0;
            rect.y = 0;
            float w_ratio = win_width * 1.0 /video_width;
            float h_ratio = win_height * 1.0 /video_height;
            rect.w = video_width * w_ratio;
            rect.h = video_height * h_ratio;
            // rect.w = win_width;
            // rect.h = win_height;
            SDL_UpdateTexture(texture, NULL, video_buf, video_width);

            SDL_RenderClear(render);

            SDL_RenderCopy(render, texture, NULL, &rect);

            SDL_RenderPresent(render);

        }
        else if (event.type == SDL_WINDOWEVENT) {
            SDL_GetWindowSize(window, &win_width, &win_height);
        }
        else if (event.type == SDL_QUIT) {
            s_thread_exit = 1;
        }
        else if ( event.type == FF_QUIT_EVENT) {
            break;
        }
    }

	FAILED:
	    s_thread_exit = 1;
	    free(video_buf);
	    fclose(video_fd);
	    SDL_DestroyTexture(texture);
	    SDL_DestroyRenderer(render);
	    SDL_DestroyWindow(window);
	    
	    SDL_Quit();
	
	    return 0;
}

解读

计算一帧的大小

uint32_t y_frame_len = video_height * video_width;
uint32_t u_frame_len = y_frame_len / 4;
uint32_t v_frame_len = y_frame_len / 4;
uint32_t one_frame_len = y_frame_len + u_frame_len + v_frame_len;

首先是这一段帧大小计算,如何计算一帧的大小。首先我们的视频格式是 YUV420p,在这个格式中,四个 Y 分量对应一个 u 和 v 分量,也就是说 Y 和 uv 的采样比是 4:1。那么 Y 分量的计算就是画面的 长*宽,而 uv 分量就是他的 1/4。一帧的大小就是三个分量的大小相加。

纹理的创建

texture = SDL_CreateTexture(render, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, video_width, video_height);

image.png

这里其实是 SDL 定义的宏,用来解析 YUVP 的帧数据。

更新纹理

SDL_UpdateTexture(texture, NULL, video_buf, video_width);

这里的传参可以会比较奇怪,传了一个帧数据和宽度,其实这里是有说法的。

image.png

如果第二个参数给 NULL 的话就是更新已经 entire 过的纹理,这个纹理在我们创建的时候就已经初始化过了。然后 pitch 参数就是描述帧数据的宽(row)的。