之前接触到一个 YUYV 的摄像头,需要将采集到的 YUYV 数据转换为 YUV420P 数据,在经历了一些弯路的同时,发现了网上流传的一些资料有误,遂写此博文。
基础
YUV,分为三个分量,「Y」表示明亮度 ( Luminance 或 Luma ) ,也就是灰度值;而「U」和「V」 表示的则是色度 ( Chrominance 或 Chroma ) ,作用是描述影像色彩及饱和度,用于指定像素的颜色。
对于每一个 Y, U,V 分量,其长度都是 1 个字节。YUV420P 和 YUYV 本质上的区别就是采样方式的不同。由于作者水平限制,文中可能出现一些纰漏,欢迎指正。
YUYV 格式介绍
yuyv 的存储结构如下表,如果是 uyvy 的话,就是顺序改变一下。
| Y0 | U0 | Y1 | V0 | Y2 | U2 | Y3 | V2 |
|---|---|---|---|---|---|---|---|
| Y4 | U4 | Y5 | V4 | Y6 | U6 | Y7 | V6 |
| Y8 | U8 | Y9 | V8 | Y10 | U10 | Y11 | V10 |
| Y12 | U12 | Y13 | V12 | Y14 | U14 | Y15 | V14 |
看起来这个结构很奇怪,对吧?而且 UV 分量还没有奇数的份。 我的理解是 YUYV 对应着的是两个像素,其中第一个像素由 Y0, U0, V0 组成,第二个像素由 Y1 组成,那么在一张图片上,YUYV 就是下表的样子。
| Y0,U0,V0 | Y1,U0,V0 | Y2,V2,U2 | Y3,V2,U2 |
|---|---|---|---|
| Y4,U4,V4 | Y5,U4,V4 | Y6,U6,V6 | Y7,U6,V6 |
| Y8,U8,V8 | Y9,U8,V8 | Y10,U10,V10 | Y11,U10,V10 |
| Y12,U12,V12 | Y13,U12,V12 | Y14,U14,V14 | Y15,U14,V14 |
这样一来,UV 分量看起来就舒服多了,和 Y 分量完美对应起来。这就是第一个表中没有奇数 UV 分量的原因。
YUV420P 和 YUYV 本质上的区别就是采样方式的不同。YUYV 使用了隔列采样。
从中可以看到, 4 个字节,一组 YUYV 代表了两个像素,因此如果使用 YUYV 存储,那么文件的大小 size = width *height* 2
容易看出,上面两个表描述了一个 4*4 像素大小的图片。其大小为 32 字节。
YUY420P 格式介绍
同样,首先来看一张 4*4 像素大小的 yuv420p 存储结构,如下表。
| Y0 | Y1 | Y2 | Y3 |
|---|---|---|---|
| Y4 | Y5 | Y6 | Y7 |
| Y8 | Y9 | Y10 | Y11 |
| Y12 | Y13 | Y14 | Y15 |
| ——- | ——- | ——- | ——- |
| U0 | U2 | U8 | U10 |
| ——- | ——- | ——- | ——- |
| V0 | V2 | V8 | V10 |
为了方便理解,没有按顺序排列 UV 分量,怎么好理解怎么来,这些数字都不重要! 首先 YUV420P 将 YUV 三个分量分别打包,Y 存放在一起,U 放在一起,V 放在一起。你可能会觉得 UV 分量少了,但是 UV 分量就是这么多,并没有少。我们来看在图片中的 YUV420P :
| Y0,U0,V0 | Y1,U0,V0 | Y2,V2,U2 | Y3,V2,U2 |
|---|---|---|---|
| Y4,U0,V0 | Y5,U0,V0 | Y6,V2,U2 | Y7,V2,U2 |
| Y8,U8,V8 | Y9,U8,V8 | Y10,U10,V10 | Y11,U10,V10 |
| Y12,U8,V8 | Y13,U8,V8 | Y14,U10,V10 | Y15,U10,V10 |
YUV420P 和 YUYV 本质上的区别就是采样方式的不同。YUV420 使用了隔行隔列采样。
可以理解成 YUV420,在 YUV422 的基础上抛弃了偶数行的 UV 分量, ( 或者抛弃奇数行,或者 U 分量抛弃奇数行,V 分量抛弃偶数行 )
也就是说,四个相邻的像素共用一个 UV 分量,U0 和 V0 提供给了 Y0, Y1, Y4, Y5 四个像素。
那么采用了 YUV420 的 4*4 像素图片,Y 分量大小是 width* height;
U 分量大小是 width *height / 4;
V 分量大小是 width* height / 4;
整个图片大小就是:
size = width *height* 3 / 2。
转换
对比 yuyv 和 yuv420p 的存储方式
- yuyv
| Y0 | U0 | Y1 | V0 | Y2 | U2 | Y3 | V2 |
|---|---|---|---|---|---|---|---|
| Y4 | U4 | Y5 | V4 | Y6 | U6 | Y7 | V6 |
| Y8 | U8 | Y9 | V8 | Y10 | U10 | Y11 | V10 |
| Y12 | U12 | Y13 | V12 | Y14 | U14 | Y15 | V14 |
- yuv420p
| Y0 | Y1 | Y2 | Y3 |
|---|---|---|---|
| Y4 | Y5 | Y6 | Y7 |
| Y8 | Y9 | Y10 | Y11 |
| Y12 | Y13 | Y14 | Y15 |
| ——- | ——- | ——- | ——- |
| U0 | U2 | U8 | U10 |
| ——- | ——- | ——- | ——- |
| V0 | V2 | V8 | V10 |
那么就是读入 yuyv 后,把它按照 yuv420 的结构重新排列一下,抛弃偶数行的 UV 分量就可以了。
分析完 YUYV 和 YUV420P 后,我写了一段 yuyv 转 yuv420 的程序,首先请分配好 out 的空间: size = width *height* 3 / 2。
void yuyv_to_yuv420P ( uint8_t *in, uint8_t *out, int width, int height ) {
uint8_t *y, *u, *v;
int i, j, offset = 0, yoffset = 0;
y = out; // yuv420 的 y 放在前面
u = out + ( width * height ) ; // yuv420 的 u 放在 y 后
v = out + ( width * height * 5 / 4 ) ; // yuv420 的 v 放在 u 后
//总共 size = width * height * 3 / 2
for ( j = 0; j < height; j++ ) {
yoffset = 2 * width * j;
for ( i = 0; i < width * 2; i = i + 4 ) {
offset = yoffset + i;
- ( y++ ) = * ( in + offset ) ;
- ( y++ ) = * ( in + offset + 2 ) ;
if ( j % 2 == 1 ) { //抛弃奇数行的 UV 分量
- ( u++ ) = * ( in + offset + 1 ) ;
- ( v++ ) = * ( in + offset + 3 ) ;
}
}
}
}
程序非常正常,完美运行!