摄像头YUV格式极速转码
前言
市面上主流摄像头的图像封装格式一般逃不过这三种:JPEG、MJPG和YUV
。其中YUV编码既可以与灰度图像兼容,又利用了人眼对亮度和色度的定量优化,使其可以直接跟三原色RGB进行直接互换而到广泛青睐。但YUV与RGB的转码涉及大量浮点运算,对于高分辨率高速摄像头而言,转码对CPU的负担很重,本文来看看如何巧妙化解这个难点。
摄像头捕获视频数据
摄像头之所以可以捕获图像,使用了很多感光点来采集光线,感光原理就是通过一个个的感光点来对光进行采样和量化,平常所说的 300 万像素的摄像头,指的就是有大约 300 万个感光点。
摄像头的输出数据格式一般有三种:
1,rawRGB
这种格式就是直接输出摄像头的感光点输出,由于每一个像素点包含 RGB 中的一种颜色,因此被称为 rawRGB 而不是 RGB,从 rawRGB 到 RGB 还需要一个“反马赛克”的算法。最后得到的 RGB 就是图像的原生数据了。
2,MJPG/JPEG
由于 rawRGB 或者 RGB 这样的数据没有经过任何加工和压缩,尺寸很大,因此一般市面上的摄像头都不会直接输出这样的格式,最常见的做法是压缩成 JPEG 或者 MJPG 格式来输出。
3,YUV
这是一种更为流行的格式。根据人类眼睛的视觉特征设计——由于人类的眼睛对亮度的敏感度比颜色要高许多,而且在 RGB 三原色中对绿色有尤为敏感,利用这个原理,可以把色度信息减少一点,人眼也无法查觉这一点。YUV三个字母中,其中"Y"表示明亮度(Lumina nce或Luma),也就是灰阶值,而"U"和"V"表示的则是色度(Chrominance或Chroma),作用是描述影像色彩及饱和度,用于指定像素的颜色。我们可以通过减少图像中的红色和蓝色分量来达到减少图像尺寸的目的。在很多技术文档中YUV还会写成YCbCr,Y指的是绿色和亮度,C是Component的首字母,b和r分别是blue和red,从这个角度出发可以认为YUV是RGB的变种。
YUV与RGB格式转换
YUV格式具有亮度信息和色彩信息分离的特点,但大多数图像处理操作都是基于RGB格式。因此当要对图像进行后期处理显示时,需要把YUV格式转换成RGB格式。
RGB与YUV的变换公式如下:
YUV(256 级别) 可以从8位 RGB 直接计算:
Y = 0.299 R + 0.587 G + 0.114 B
U = - 0.1687 R - 0.3313 G + 0.5 B + 128
V = 0.5 R - 0.4187 G - 0.0813 B + 128
反过来,RGB 也可以直接从YUV (256级别) 计算:
R = Y + 1.402 (Cr-128)
G = Y - 0.34414 (Cb-128) - 0.71414 (Cr-128)
B = Y + 1.772 (Cb-128)
可见,YUV可以看做是RGB的优化变种。并且更进一步,既然U和V对人类的眼睛不敏感,我们可以针对它们做各种变化,来减少整体图像的尺寸。具体情况我们来一个个看。
第一种:YUV422.后面的数字可以理解为代表YUV分量的比例是4:2:2,其原理是在每个像素中删去一个U或者V分量,然后再在还原的时候用相邻的像素的UV分量填充,一图顶千言:
这样做虽然损失了原先的一部分UV信息,但是人眼对此部分信息不敏感,海原就可以得到很好的效果,照这样的思路,可以对YUV进程更激进的裁剪,比如下面这种压缩率大一点的YUV411:
还有这种更没人性的YUV420:
以上各种YUV格式思路大同小异,都是通过裁剪UV信息来达到缩小图像尺寸的目的,因此不再一个个赘述了。我们以最常见的YUV422为例,来看看从摄像头中捕获这种数据之后,怎么极速转化为RGB。
首先,由上面转换公式可知,YUV与RGB存在以下互换公式:
R = Y + 1.042*(V-128);
G = Y - 0.344*(U-128)-0.714*(V-128);
B = Y + 1.772*(U-128);
有了以上公式,我们自然可以写出如下代码,来计算每一个YUV像素点的等价RGB值,比如我们有原始YUV422的两个像素为:
那根据公式我们很容易写出对应的两个 RGB 像素点的数值为:
R0 = *Y0 + 1.042*(*V1-128);
G0 = *Y0 - 0.344*(*U0-128) - 0.714*(*V1-128);
B0 = *Y0 + 1.772*(*U0-128);
R1 = *Y1 + 1.042*(*V1-128);
G1 = *Y1 - 0.344*(*U0-128) - 0.714*(*V1-128);
B1 = *Y1 + 1.772*(*U0-128);
以上算式只是其中两个像素点的计算量,假如摄像头的分辨率是1280×720,那么一帧这样的YUV数据就得进行好几百万次浮点运算,而最普通的摄像头一秒可以产生25-30帧数据,高速摄像头每秒可以产生几百到几千帧数据(激光扑捉器每秒200亿帧了解一下),这还不算转换后的RGB写入显存的时间,这种运算量对于嵌入式系统而言简直就是噩梦,如果CPU不支持浮点运算的话还得转化成整型运算,最终的结果是:
牛逼闪闪的摄像头
烂成渣渣的视频流
这个问题的解决,可以通过优化以上公式来苟延残喘,比如可以将浮点运算变成整数运算:
R0 = *Y0 + 1042*(*V1-128) / 1000;
G0 = *Y0 - (344*(*U0-128) - 714*(*V1-128)) / 1000;
B0 = *Y0 + 1772*(*U0-128) / 1000;
R1 = *Y1 + 1042*(*V1-128) / 1000;
G1 = *Y1 - (344*(*U0-128) - 714*(*V1-128)) / 1000;
B1 = *Y1 + 1772*(*U0-128) / 1000;
当然这不够刺激,可以进一步将除法变成右移:
R0 = *Y0 + 4268*(*V1-128) >> 12;
G0 = *Y0 - (1409*(*U0-128) - 2924*(*V1-128)) >> 12;
B0 = *Y0 + 7258*(*U0-128) >> 12;
R1 = *Y1 + 4268*(*V1-128) >> 12;
G1 = *Y1 - (1409*(*U0-128) - 2924*(*V1-128)) >> 12;
B1 = *Y1 + 7258*(*U0-128) >> 12;
虽然这已经大大提高了运算速度,但公式中依然残留了部分挥之不去的乘法运算,踌躇间,想起一句算法届至圣名言:如果要取得时间,就必须要牺牲空间。时间就是执行效率,空间就是运算内存。这条 IT 公理可简称为算法的时空守恒。
怎么个时空互换?简单讲就是作弊:既然那么难算,我就把答案先算好写纸条里,不仅要写纸条里,为了方便作弊还要画个表格(其实就是数组),一五一十白纸黑字写死,要算某个数的时候直接像查字典那样查就行了。请注意,是要把所有的答案全部算出来,作弊就要做得彻底。 比如这两公式:
R = Y + 1.042*(V-128);
B = Y + 1.772*(U-128);
其中Y、U、V是从摄像头获取的图像数值,分别保存在一个字节中,他们的取值无非就是0-255之间,不可能有别的取值,这样一来R、B的取值就从他们中产生,总共256*256中可能。将所有的这些可能统统暴力计算出来保存在一个叫R、B的数组中:
for(int i=0; i<256; i++)
{
for(int j=0; j<256; j++)
{
R[i][j] = i + 1.042 * (j-128);
R[i][i] = R[i][j] < 0 ? 0 : R[i][j];
R[i][j] = R[i][j] > 255 ? 255 : R[i][j];
B[i][j] = i + 1.772 * (j-128);
B[i][i] = B[i][j] < 0 ? 0 : B[i][j];
B[i][j] = B[i][j] > 255 ? 255 : B[i][j];
}
}
注意以上代码是在摄像头开启之前就做好了,因此丝毫不需要对浮点公式本身做任何优化,红色部分是保证RGB取值正确。使用完全一样的算法,将具有三个变量的G也暴力计算一下:
for(int i=0; i<256; i++)
{
for(int j=0; j<256; j++)
{
for(int k=0; k<256; k++)
{
G[i][j] = i - 0.344*(j-128) - 0.714*(k-128);
G[i][i] = G[i][j] < 0 ? 0 : G[i][j];
G[i][j] = G[i][j] > 255 ? 255 : G[i][j];
}
}
}
有了这杨的金光闪闪的数组 R、G、B,进行 YUV 转码 RGB 的时候就易如反掌、快如闪电了:
R = R[*Y][*V];
G = G[*Y][*U][*V];
B = B[*Y][*U];
简单的例子:比如读取YUV中的原始数据得出:Y0=100,U=200,Y1=30,V=88,代进去数组即可得到R=R[100][88],G=G[100][200][88],B=B[30][200]
代码示例
#include <stdio.h>
//YUV 转 RGB 公式:
/*******************************
R = Y + 1.042*(V-128);
G = Y - 0.344*(U-128)-0.714*(V-128);
B = Y + 1.772*(U-128);
*******************************/
// 准备RGB数组
int R[256][256];
int G[256][256][256];
int B[256][256];
int main(int argc, char **argv)
{
// 将RGB的所有可能的取值,都提前算出来(作弊)
for(int i=0; i<256; i++)
{
for(int j=0; j<256; j++)
{
R[i][j] = i + 1.042*(j-128);
R[i][j] = R[i][j]>255 ? 255 : R[i][j];
R[i][j] = R[i][j]<0 ? 0 : R[i][j];
B[i][j] = i + 1.772*(j-128);
B[i][j] = B[i][j]>255 ? 255 : B[i][j];
B[i][j] = B[i][j]<0 ? 0 : B[i][j];
for(int k=0; k<256; k++)
{
G[i][j][k] = i - 0.344*(j-128)-0.714*(k-128);
G[i][j][k] = G[i][j][k]>255 ? 255 : G[i][j][k];
G[i][j][k] = G[i][j][k]<0 ? 0 : G[i][j][k];
}
}
}
// 给定一帧YUV图像,及其尺寸
char *yuv; // 相当于start[i%nbuf];
int height, width;
// 这是每次转换前的两个YUV像素: [Y0,U], [Y1 V]
uint8_t Y0, U;
uint8_t Y1, V;
// 每次转换后得到的连个RGB像素: [R0, G0, B0], [R1, G1, B1]
int R0, G0, B0;
int R1, G1, B1;
int yuv_offset;
for(int y=0; y<height; y++)
{
for(int x=0; x<width; x+=2) // 每次转换两个像素,四个字节
{
yuv_offset = ( 640*y + x ) * 2;
// 取四个字节
Y0 = *(yuv + yuv_offset + 0);
U = *(yuv + yuv_offset + 1);
Y1 = *(yuv + yuv_offset + 2);
V = *(yuv + yuv_offset + 3);
// 得到六个字节
//得到RGB数据就可以妥善放置在LCD上了
R0 = R[Y0][V];
G0 = G[Y0][U][V];
B0 = B[Y0][U];
R1 = R[Y1][V];
G1 = G[Y1][U][V];
B1 = B[Y1][U];
}
}
return 0;
}
下一篇: LLVM 2.9 发布 GCC
推荐阅读