一、为什么 UV 可以压缩?
一张 1920×1080 的图片,Y 分量保留了所有边缘和纹理细节。把 UV 分辨率降低到 960×540,人眼几乎看不出区别——因为人眼对颜色的空间分辨率远低于亮度。
原始 RGB:1920×1080 × 3 bytes = 6.2 MB
YUV 4:4:4:1920×1080 + 1920×1080 + 1920×1080 = 也 6.2 MB(没省空间)
YUV 4:2:0:1920×1080(Y) + 960×540(U) + 960×540(V) = 3.1 MB → 省了 50%
二、4:4:4 / 4:2:2 / 4:2:0 图解
符号含义:"J:a:b"
J = 水平参考宽度(通常 4 个像素)
a = 第一行每个 J 像素中有几个色度样本
b = 第二行中多出的色度样本数
4:4:4(无压缩)
像素位置: 0 1 2 3 4 5 6 7
Y: Y0 Y1 Y2 Y3 Y4 Y5 Y6 Y7
U: U0 U1 U2 U3 U4 U5 U6 U7
V: V0 V1 V2 V3 V4 V5 V6 V7
每个像素都有自己的 Y/U/V → 数据量 = 1920×1080×3
4:2:2(水平减半)
像素位置: 0 1 2 3 4 5 6 7
Y: Y0 Y1 Y2 Y3 Y4 Y5 Y6 Y7
U: U0 U2 U4 U6
V: V0 V2 V4 V6
每 2 个像素共享一组 UV → 数据量 = 1920×1080 × 2
常见于专业视频采集、广播设备
4:2:0(水平和垂直都减半)
像素位置: 0 1 2 3
第 0 行 Y: Y00 Y10 Y20 Y30
U: U00 U20
V: V00 V20
第 1 行 Y: Y01 Y11 Y21 Y31
U: (使用第 0 行的 U/V,不重复存储)
V:
每 4 个像素共享一组 UV → 数据量 = 1920×1080 × 1.5
所有视频编码标准(H.264/HEVC/VP9/AV1)的默认格式
三、4:4:4 → 4:2:0 下采样的手写实现
#include <vector>
#include <cstdint>
// 把 YUV 4:4:4 的 UV 平面下采样为 4:2:0
// 输入:UV 平面和 Y 一样大(w × h)
// 输出:UV 平面各缩小为 (w/2) × (h/2)
void subsample_420(const uint8_t* src_u, const uint8_t* src_v,
uint8_t* dst_u, uint8_t* dst_v,
int width, int height) {
int uv_w = width / 2;
int uv_h = height / 2;
for (int y = 0; y < uv_h; y++) {
for (int x = 0; x < uv_w; x++) {
// 4:2:0 的 U/V 取 2×2 块的均值
int sum_u = 0, sum_v = 0;
for (int dy = 0; dy < 2; dy++) {
for (int dx = 0; dx < 2; dx++) {
int src_idx = (y * 2 + dy) * width + (x * 2 + dx);
sum_u += src_u[src_idx];
sum_v += src_v[src_idx];
}
}
int dst_idx = y * uv_w + x;
dst_u[dst_idx] = sum_u / 4;
dst_v[dst_idx] = sum_v / 4;
}
}
}
四、三种采样格式的数据量对比
void print_sizes(int width, int height) {
int y_size = width * height;
int uv_444 = width * height;
int uv_422 = (width / 2) * height;
int uv_420 = (width / 2) * (height / 2);
auto fmt = [](int bytes) {
if (bytes > 1024 * 1024)
return std::to_string(bytes / 1024 / 1024) + "MB";
return std::to_string(bytes / 1024) + "KB";
};
std::cout << width << "x" << height << " 一帧:\n"
<< " RGB24: " << fmt(y_size * 3) << "\n"
<< " YUV 4:4:4: " << fmt(y_size + uv_444 * 2) << "\n"
<< " YUV 4:2:2: " << fmt(y_size + uv_422 * 2) << "\n"
<< " YUV 4:2:0: " << fmt(y_size + uv_420 * 2) << "\n";
}
// print_sizes(1920, 1080);
// 输出:
// RGB24: 6.2 MB
// YUV 4:4:4: 6.2 MB
// YUV 4:2:2: 4.1 MB
// YUV 4:2:0: 3.1 MB