Physical Address:
ChongQing,China.
WebSite:
在Android Camera开发过程中,数据格式转换是经常会遇到的。这里的数据格式转换通常会发生在两个环节:
1.底层驱动与HAL交互时发生的数据格式转换。
2.HAL层与Native Service交互时发生的数据格式转换。
而libyuv是由谷歌开源并维护的一套专门用于处理图像数据格式转换的工具库,使用该库,我们可以方便快捷地在不同的数据格式间进行转换。
在使用libyuv前,我们需要对Camera的数据格式有一个基本的了解。
首先我们需要有一个色彩空间的概念,常见的如RGB色彩空间、YUV色彩空间,这通常也是我们接触最多的两类。
RGB色彩空间想必大家都很了解,在该色彩空间内,我们使用三原色——即红色(Red)、绿色(Green)与Blue(蓝色)来表示所有的颜色组合。我们在使用时,一般还增加了一个透明度(Alpha)的配置,所以通常我们会在代码中看到RGBA这样的字眼,这表示我们图像中的最小单元(我们称其为像素)的色彩信息即通过RGBA的形式来表示。在程序内部进行表达时,我们一般认为每一个分量占一个字节,所以RGBA8888代表每个分量占8个bit,一个像素占用4个字节内存空间。
另一个比较常见的色彩空间即是YUV色彩空间,在YUV色彩空间中,我们将一个像素的色彩信息分为两部分:亮度信息与颜色信息。这其中Y代表亮度信息(luminance),而UV整体代表颜色信息,U表示色度信息,也就是色调,而V表示浓度信息,也就是饱和度。在早期的彩色电视行业,学者们也将YUV称为YCbCr,其中Cr反映的是RGB输入信号红色部分与RGB输入亮度之间的差异,等价于V;而Cb反应的是RGB输入信号中蓝色部分与RGB输入亮度之间的差异,等价于U。
在YUV色彩空间中,有一个采样的概念,目前分为三种采样:YUV444,YUV422,与YUV420,不同的采样会导致每个像素所占用的内存大小不同。
关于这三种采样形式的不同,可以参考如下示意图,以黑色实心圆代表Y分量,白色实心代表UV分量:
简单来讲:
在YUV444采样中,每一个Y分量,对应一组UV分量,排列形式如下:
YYYYYYYY UV UV UV UV UV UV UV UV(每一位代表1bit)
由此可以得出,YUV444采样时每一个像素占用3个字节。
在YUV422采样中,每两个Y分量,对应一组UV分量,其排列形式如下:
YYYYYYYY UV UV UV UV (每一位代表1bit)
由此可以得出,在YUV422采样时,每个像素点占用2个字节。
在YUV420采样中,每四个Y分量,对应一组UV分量,其排列形式如下:
YYYYYYYY UV UV
由此可以得出,在YUV420采样时,每个像素点占用1.5个字节。
在了解YUV的采样模式后,我们需要了解一下YUV色彩空间下的数据格式命名规格,这一点很重要,初学者很容易迷糊(包括我)。简单来讲,这里存在两种命名规格:带Plane模型与不带Plane模型的。不带Plane模型即是普通的YUV命名规则,有点类似于RGBA这种命名规则,比如YUYV,YYUV,UYVY等,此时每个分量依次排列存储(没有分开),从命名就可以很好地理解其内存分布形式。而带Plane模型的则会复杂许多,这里我们简单讲解一个Plane的概念,我们可以将其理解为“通道”的意思。了解这个概念很重要,因为Plane会影响YUV各个分量在我们内存内的分布。
一般而言,YUV的Plane分为两种:Three-Plane与Two-Plane模式。
简单讲一下Three-Plane与Two-plane的差别:Three-Plane其实是将Y,U,V分量分别存储(可以理解为Y,U,V每个分量一个通道),如下所示:
如YUV422采样,Three-Plane模式:
//U在前,V在后,每个分量单独一个Plane
YYYYYYYY
UUUU
VVVV
//V在前,U在后,每个分量单独一个Plane
YYYYYYYY
VVVV
UUUU
我们可以看到,在Three-Plane模式下YUV的每一类分量都是单独做集中存储的,一般我们将Y分量优先存储,其次将U,V分别存储;而U在前与V在前这两种形式,最终在内存的分布都是不一样的。
在Two-Plane模式下:
//U在前,V在后,Y与UV分量单独一个Plane
YYYYYYYY
UVUVUVUV
//V在前,U在后,Y与UV分量单独一个Plane
YYYYYYYY
VUVUVUVU
这里将例举一些我们比较常见的YUV数据格式并标记出对应的采样模式与Plane模型:
名称 | 采样类型 | Plane模型 | 备注 |
NV24 | YUV444 | Two-Plane | U在前,V在后 |
NV42 | YUV444 | Two-Plane | V在前,U在后 |
I444 | YUV444 | Three-Plane | Y,U,V依次分别存储 |
NV16 | YUV422 | Two-Plane | U在前,V在后 |
NV61 | YUV422 | Two-Plane | V在前,U在后 |
I422 | YUV422 | Three-Plane | Y,U,V依次分别存储;Y分量取值为16~240 |
J422 | YUV422 | Three-Plane | Y,U,V依次分别存储 ;Y分量取值为完整的0~255 |
NV12 | YUV420 | Two-Plane | U在前,V在后 |
NV21 | YUV420 | Two-Plane | V在前,U在后 |
I420 | YUV420 | Three-Plane | Y,U,V依次分别存储;Y分量取值为16~240 |
J420 | YUV420 | Three-Plane | Y,U,V依次分别存储 ;Y分量取值为完整的0~255 |
YV12 | YUV420 | Three-Plane | Y,U,V依次分别存储;Y分量取值为16~240 |
在了解上述基本概念后,我们回到本篇文章的核心——格式转换。其实格式转换核心就两点:
1.不同色彩空间的格式转换
2.相同色彩空间下各分量的重新排列
怎么去理解这两点呢,如果参与格式转换的两个数据类型都是同一色彩空间下的不同格式,那么第一步要做的就是色彩空间的转换,这部分有现成的公式可参考,具体细节不在此处展开,直接给出公式:
//From RGB To YUV
Y = 0.299R + 0.587G + 0.114B
U = -0.147R - 0.289G + 0.436B
V = 0.615R - 0.515G - 0.100B
//From YUV To RGB
R = Y + 1.140V
G = Y - 0.395U - 0.581V
B = Y + 2.032U
根据公式我们可以写出相应的格式空间转换格式(YUV To RGB):
static inline float clamp(float v, float min, float max)
{
if (v < min)
return min;
if (v > max)
return max;
return v;
}
static uint32_t yuvToRgbx(const unsigned char Y, const unsigned char Uin, const unsigned char Vin)
{
float U = Uin - 128.0f;
float V = Vin - 128.0f;
float Rf = Y + 1.140f * V;
float Gf = Y - 0.395f * U - 0.581f * V;
float Bf = Y + 2.032f * U;
unsigned char R = (unsigned char)clamp(Rf, 0.0f, 255.0f);
unsigned char G = (unsigned char)clamp(Gf, 0.0f, 255.0f);
unsigned char B = (unsigned char)clamp(Bf, 0.0f, 255.0f);
return (R) | (G << 8) | (B << 16) | 0xFF000000;
}
肯定有朋友注意到了此处在转换时引入了一个常量128.0f,这在公式中并未提及,这是为了避免在格式转换过程中引入误差,关于这部分说明可以参考链接。
在相同色彩空间的基础下,不同的数据格式相对来说就比较简单了,其实就是各分量的重新排列,如NV21与NV12的转换那么就是讲UV分量的排列调整为期待值即可。所以了解各种数据格式的内存排列形式很重要。
在我们使用libyuv库来完成数据格式转换时,我们需要进行相应的一些配置:
1.mk或者bp文件内导入libyuv,以bp为例:
shared_libs: [
...
"libEGL",
"libGLESv2",
"libyuv",
"liblog",
...
],
2.头文件引入#include <libyuv.h>
这里将举例说明,将YUYV(不带Plane模型)的数据格式转换为RGBA格式:
void fillRGBAFromYUYV(
const BufferDesc& tgtBuff,
uint8_t* tgt,
void* imgData,
void* buf,
unsigned imgStride)
{
const auto srcStrideInBytes = imgStride * 2;
const auto dstStrideInBytes = pDesc->stride * 4;
// 色彩空间转换
auto result = libyuv::YUY2ToARGB((const uint8_t*)imgData,
srcStrideInBytes,
(uint8_t*)buf,
dstStrideInBytes,
pDesc->width,
pDesc->height);
if (result)
{
ALOGE("Failed to convert YUYV to BGRA");
return;
}
result = libyuv::ABGRToARGB((uint8_t*)buf, dstStrideInBytes, tgt, dstStrideInBytes,
pDesc->width, pDesc->height);
if (result)
{
ALOGE("Failed to convert BGRA to RGBA");
}
}
在这个示例中,tgtBuffer为我们数据格式的最终去向,取其宽、高与Stride参数,用于计算单行有效数据的字节数。imgData为原始的数据buffer,buf用于存储进行色彩空间变换后的数据,tgt才代表我们最终转换后的数据地址。在这里,我们调用了libyuv的两个函数:
1. libyuv::YUY2ToARG,该函数其实是将我们的数据从YUV色彩空间转换到了RGB空间,在RGB空间内的数据格式为BGRA(这是因为libyuv是小端模式,而Android内的RGBA为大端)
2. libyuv::ABGRToARGB ,转换大小端,调整各分量次序
不过当前我对libyuv的了解仅限于如何使用而已,后面有机会再进行更为深入的了解。