【Android Camera2】玩转图像数据
业务场景介绍
最近也是在做Android相机这一块,使用的API是Android Camera2。熟悉Android 相机开发的小伙伴都知道Android Camera开发的过程和NV21打交道是必不可少的。上一篇文章相机NV21数据获取详细介绍了如何从将从相机得到的Image转换成NV21,得到了NV21数据后,当然就是要和它打交道了~
举个例子,由于硬件系统,相机的Api,以及前后置相机的差异,我们得到数据有可能是需要调整的。比如从相机的前置摄像头拿到数据时,我们可能需要对相机进行镜像,又或者拿到的数据本身就是倒着的,我们要将它旋转90°等。
其实网上关于NV21数据的旋转,镜像和转换RGBA的数据大多数都有,但是我们很多情况不能使用拿来主义直接COPY。我们需要根据我们自己的场景,选择并修改代码,才能达到最好的效果。为什么呢?
举例来说:当你只是需要对图像做镜像操作,那么你就应该使用我介绍的中心翻转法而不应该使用逐像素法,因为它的耗时比逐像素遍历法的一半;
当你需要对图像旋转90°再镜像时:你可能需要自己修改代码来达到性能的最佳。当然你也可以从网上找代码,首先对图像进行旋转90°,再去做一个镜像,这样做只是效率大大折扣而已。
如果直接拿网上的代码,并且不同原理,当你的适用场景变了的时候,你可能没法应对。另外更重要的一点,网上的代码不一定适合你的运用场景,直接拿过来用可能导致性能损耗。
本文首先大致讲解各种操作的方法,以及它的性能。然后分析当需要组合操作时,我们应该如何修改代码来降低性能的损耗。最后对代码进行了一个汇总,对于各个场景给出一个应该使用的代码,如果不想看原理的同学可以直接去取(当然还是希望大家能够懂原理,能够自己去修改,这样才能够应对不同的场景)。本文会对关键的算法进行一个耗时的统计,使用的手机是小米12,图像的分辨率大小为1280*720.
NV21数据旋转
NV21数据的旋转,其实就是一个数组的旋转。就是类似于leetcode 48题 旋转图像,我们要将一个二维数据的数据进行旋转。不同的地方有两点:1、NV21数据的数据是nm形式的矩阵,而题中给出的数据是nn型的,因此官方题解中的原地旋转法和用翻转代替旋转法就不可以用了,我们只能使用逐像素遍历法了。2、对于NV21的数据来说,它是先由图像像素点的Y值排列,然后由像素点的UV序列排列而成的。我们旋转的时候需要分开来转,首先对Y序列旋转,然后将UV序列旋转,再将它们组合起来。
对于我这里讲的不太明白的小伙伴可以先去看一下我上一篇文章相机NV21数据获取了解一下图像NV21的数据格式。这样有助于理解代码。
逐像素遍历法
图像顺时针旋转90°。代码比较好理解,首先是对Y序列进行旋转,也就是对应着Rotate the Y luma部分。第二部分是对UV数据进行旋转,也就是对应着Rotate the U and V color components。将两个部分依次排列也就得到了最后旋转的结果。
private byte[] rotateYUVDegree90(byte[] data, int imageWidth, int imageHeight) {
byte[] yuv = new byte[imageWidth * imageHeight * 3 / 2];
// Rotate the Y luma
int i = 0;
for (int x = 0; x < imageWidth; x++) {
for (int y = imageHeight - 1; y >= 0; y--) {
yuv[i] = data[y * imageWidth + x];
i++;
}
}
// Rotate the U and V color components
for (int x = 0; x < imageWidth; x = x + 2) {
for (int y = imageHeight / 2 - 1; y >=0; y--) {
yuv[i] = data[(imageWidth * imageHeight) + (y * imageWidth) + x];
i++;
yuv[i] = data[(imageWidth * imageHeight) + (y * imageWidth) + (x + 1)];
i++;
}
}
return yuv;
}
NV21数据镜像
逐像素遍历法
逐像素遍历法,和前面图像的旋转90°思路一样的,只不过旋转前和旋转后图像对应的位置关系发生了变化。你可以看到,两个代码都是相似,都是先对y通道处理,在对uv通道处理,两次循环,不同的地方不过在于如何处理它们的映射关系罢了。也就是在 yuv[i] 的赋值语句中,data数组内的下标不同而已。此处代码用时1.82ms。
private byte[] MirrorYUV(byte[] data, int imageWidth, int imageHeight) {
byte[] yuv = new byte[imageWidth * imageHeight * 3 / 2];
// Mirror the Y luma
int i = 0;
for (int y = 0; y < imageHeight; y++) {
for (int x = imageWidth-1; x >=0; x--) {
yuv[i] = data[y * imageWidth + x];
i++;
}
}
// Rotate the U and V color components
for (int y = 0; y < imageHeight/2; y++) {
for (int x = imageWidth-1; x >=0; x= x-2) {
yuv[i] = data[(imageWidth * imageHeight) + (y * imageWidth) + x - 1];
i++;
yuv[i] = data[(imageWidth * imageHeight) + (y * imageWidth) + x];
i++;
}
}
return yuv;
}
中心翻转法
由于镜像操作的特殊性,镜像后的图像和数据其实就是图像按照中心线进行了左右的对调。利用这一个特征,我们可以对算法进行简化,也就是只遍历图像的前半部分,然后让前半部分后后半部分进项交换,这样遍历的次数就少了一半。算法用时0.8ms。
private byte[] NV21_mirror_center(byte[] nv21_data, int width, int height)
{
int i;
int left, right;
byte temp;
int startPos = 0;
// mirror Y
for (i = 0; i < height; i++)
{
left = startPos;
right = startPos + width - 1;
while (left < right) {
temp = nv21_data[left];
nv21_data[left] = nv21_data[right];
nv21_data[right] = temp;
left ++;
right--;
}
startPos += width;
}
// mirror U and V
int offset = width * height;
startPos = 0;
for (i = 0; i < height / 2; i++)
{
left = offset + startPos;
right = offset + startPos + width - 2;
while (left < right){
temp = nv21_data[left ];
nv21_data[left ] = nv21_data[right];
nv21_data[right] = temp;
left ++;
right--;
temp = nv21_data[left ];
nv21_data[left ] = nv21_data[right];
nv21_data[right] = temp;
left ++;
right--;
}
startPos += width;
}
return nv21_data;
}
显然我们可以看出,当你只用对图像进行镜像操作时,你肯定不应该选择逐像素遍历法,而要使用中心翻转法,否则会让你的执行效率直接降低一半多。
NV21转RGB/RGBA数据
逐像素遍历法
NV21图像转RGB图像,其实就是需要将每一个图像的像素点的RGB值通过NV21数组中的数据计算出来,然后排列成数组。所以算法要做的是遍历图像每一个像素点,找到像素点对应的n,u,v的值,根据rgb和nuv的转换公式计算出该像素点的rgb的值,然后赋给数组就行。具体图像yuv空间和Rgb空间的转换原理和公式可以看一下YUV 格式与 RGB 格式的相互转换公式及C++ 代码或者其它的文章。我将讲解简单写在代码注释里。
public void NV212BGRA(byte[]input , int width , int height , byte[]output, boolean isRGB)
{
int nvOff = width * height ;
int i, j, yIndex = 0;
int y, u, v;
int r, g, b, nvIndex = 0;
//遍历图像每一个像素点,依次计算r,g,b的值。
for(i = 0; i < height; i++){
for(j = 0; j < width; j ++,++yIndex){
//对于位置为i,j的像素,找到对应的y,u,v的值
nvIndex = (i / 2) * width + j - j % 2;
y = input[yIndex] & 0xff;
u = input[nvOff + nvIndex ] & 0xff;
v = input[nvOff + nvIndex + 1] & 0xff;
//对于位置为i,j的像素,根据其yuv的值计算出r,g,b的值
r = y + ((351 * (v-128))>>8); //r
g = y - ((179 * (v-128) + 86 * (u-128))>>8); //g
b = y + ((443 * (u-128))>>8); //b
//要求的rgb值是byte类型,范围是0--255,消除越界的值。
r = ((r>255) ?255 :(r<0)?0:r);
g = ((g>255) ?255 :(g<0)?0:g);
b = ((b>255) ?255 :(b<0)?0:b);
// 对结果rgb/bgr的值赋值,a通道表示透明度,这里都给255(根据你自己的场景设置)
//由于rgba格式和bgra格式只是r,g,b的顺序不同,因此这里通过bool值判断,既可以得到rgba的格式也可以得到bgra格式的数组。
if(isRGB){
output[yIndex*4 + 0] = (byte) b;
output[yIndex*4 + 1] = (byte) g;
output[yIndex*4 + 2] = (byte) r;
output[yIndex*4 + 3] = (byte) 255;
}else{
output[yIndex*4 + 0] = (byte) r;
output[yIndex*4 + 1] = (byte) g;
output[yIndex*4 + 2] = (byte) b;
output[yIndex*4 + 3] = (byte) 255;
}
}
}
}
NV21组合操作(旋转,镜像,转RGB/RGBA)
重点其实在于组合操作了。当你的图像需要将上述三个步骤结合起来的时候,会怎样呢?举个例子,你需要NV21的数据顺时针旋转90°然后转成RGB,你会怎么做呢?先旋转再转RGB?这样就有点愚笨了。其实你注意到了吗,旋转,镜像,转rgb都是可以逐像素去做的。这三种操作的组合,也是可以逐像素做的。不管是怎样的组合,你只要对像素所有点进行一次遍历即可。重点是找到变换前后像素的映射关系。下面讲两个操作,NV21数组顺时针转90度+转换RGBA数据和NV21数组顺时针转270度+镜像+转换成RGBA数据。这两个操作,一次遍历就可以了,不要分两步实现,并且代码几乎完全一样。
NV21数组顺时针转90度+转换RGBA数据
public void NV21rRotate90Degree2BGRA(byte[]input , int width , int height , byte[]output, boolean isRGB)
{
int nvOff = width * height ;
int i, j, yIndex = 0;
int y, u, v;
int r, g, b, nvIndex = 0;
for(i = 0; i < height; i++){
for(j = 0; j < width; j ++,++yIndex){
nvIndex = (i / 2) * width + j - j % 2;
y = input[yIndex] & 0xff;
u = input[nvOff + nvIndex ] & 0xff;
v = input[nvOff + nvIndex + 1] & 0xff;
// yuv to rgb
r = y + ((351 * (v-128))>>8); //r
g = y - ((179 * (v-128) + 86 * (u-128))>>8); //g
b = y + ((443 * (u-128))>>8); //b
r = ((r>255) ?255 :(r<0)?0:r);
g = ((g>255) ?255 :(g<0)?0:g);
b = ((b>255) ?255 :(b<0)?0:b);
int index = j*height+(height - i)-1;
if(isRGB){
output[index*4 + 0] = (byte) b;
output[index*4 + 1] = (byte) g;
output[index*4 + 2] = (byte) r;
output[index*4 + 3] = (byte) 255;
}else{
output[index*4 + 0] = (byte) r;
output[index*4 + 1] = (byte) g;
output[index*4 + 2] = (byte) b;
output[index*4 + 3] = (byte) 255;
}
}
}
}
NV21数组顺时针转270度+镜像+转换RGBA数据
public void NV21Rotate270Degree2BGRAandMirror(byte[]input , int width , int height , byte[]output, boolean isRGB)
{
int nvOff = width * height ;
int i, j, yIndex = 0;
int y, u, v;
int r, g, b, nvIndex = 0;
for(i = 0; i < height; i++){
for(j = 0; j < width; j ++,++yIndex){
nvIndex = (i / 2) * width + j - j % 2;
y = input[yIndex] & 0xff;
u = input[nvOff + nvIndex ] & 0xff;
v = input[nvOff + nvIndex + 1] & 0xff;
// yuv to rgb
r = y + ((351 * (v-128))>>8); //r
g = y - ((179 * (v-128) + 86 * (u-128))>>8); //g
b = y + ((443 * (u-128))>>8); //b
r = ((r>255) ?255 :(r<0)?0:r);
g = ((g>255) ?255 :(g<0)?0:g);
b = ((b>255) ?255 :(b<0)?0:b);
int index = (width-j-1)*height + (height-i-1);
if(isRGB){
output[index*4 + 0] = (byte) b;
output[index*4 + 1] = (byte) g;
output[index*4 + 2] = (byte) r;
output[index*4 + 3] = (byte) 255;
}else{
output[index*4 + 0] = (byte) r;
output[index*4 + 1] = (byte) g;
output[index*4 + 2] = (byte) b;
output[index*4 + 3] = (byte) 255;
}
}
}
}
什么?你说NV21数组顺时针转90度+转换RGBA数据 和 NV21数组顺时针转270度+镜像+转换RGBA数据 和NV21转RGB/RGBA数据三个算法是一样的?哈哈,的确很像,但仔细看不同的地方在于index的值不同。Index的值这里的含义就是找到变换后的像素点和变换前的像素点的映射关系。如果你对一张图象先转90度再转RGBA需要耗时13.5ms;如果你直接用上面的代码需要耗时11ms。当然你的组合操作越多,这样一步到位省时就越多。
文章总结
本文最重要的核心总结一句话:
1、如果你只要对NV21数据进行镜像的话,使用中心旋转法而不要用逐像素处理法,这样你的处理效率会提高一倍以上。
2、如果你要对图像进行多个操作的话,不要按照你的操作步骤一步一步转换,而是直接找到图像操作前和操作后的像素映射关系,采用逐像素法一步到位。即你要对数组旋转+镜像+转RGBA,不要一次运行我上面的三个代码,直接重写一个一步到位的代码,这样效率会极大提升。
代码获取
图像的操作的组合方式很多,我不可能穷尽,更重要的是思想和思路。如果下面的代码没有你需要的操作,那么你需要自己去重新实现,当然,根据我上面讲的,代码是非常简单的,你几乎只用改一行代码。
NV21数据向右旋转90°
private byte[] rotateYUVDegree90(byte[] data, int imageWidth, int imageHeight) {
byte[] yuv = new byte[imageWidth * imageHeight * 3 / 2];
// Rotate the Y luma
int i = 0;
for (int x = 0; x < imageWidth; x++) {
for (int y = imageHeight - 1; y >= 0; y--) {
yuv[i] = data[y * imageWidth + x];
i++;
}
}
// Rotate the U and V color components
for (int x = 0; x < imageWidth; x = x + 2) {
for (int y = imageHeight / 2 - 1; y >=0; y--) {
yuv[i] = data[(imageWidth * imageHeight) + (y * imageWidth) + x];
i++;
yuv[i] = data[(imageWidth * imageHeight) + (y * imageWidth) + (x + 1)];
i++;
}
}
return yuv;
}
NV21数据向右旋转270°
恭喜你,你需要自己去实现~~~~~~~~~~~~~~~
NV21数据镜像
private byte[] NV21_mirror_center(byte[] nv21_data, int width, int height)
{
int i;
int left, right;
byte temp;
int startPos = 0;
// mirror Y
for (i = 0; i < height; i++)
{
left = startPos;
right = startPos + width - 1;
while (left < right) {
temp = nv21_data[left];
nv21_data[left] = nv21_data[right];
nv21_data[right] = temp;
left ++;
right--;
}
startPos += width;
}
// mirror U and V
int offset = width * height;
startPos = 0;
for (i = 0; i < height / 2; i++)
{
left = offset + startPos;
right = offset + startPos + width - 2;
while (left < right){
temp = nv21_data[left ];
nv21_data[left ] = nv21_data[right];
nv21_data[right] = temp;
left ++;
right--;
temp = nv21_data[left ];
nv21_data[left ] = nv21_data[right];
nv21_data[right] = temp;
left ++;
right--;
}
startPos += width;
}
return nv21_data;
}
NV21数据转RGBA数据
public void NV212BGRA(byte[]input , int width , int height , byte[]output, boolean isRGB)
{
int nvOff = width * height ;
int i, j, yIndex = 0;
int y, u, v;
int r, g, b, nvIndex = 0;
//遍历图像每一个像素点,依次计算r,g,b的值。
for(i = 0; i < height; i++){
for(j = 0; j < width; j ++,++yIndex){
//对于位置为i,j的像素,找到对应的y,u,v的值
nvIndex = (i / 2) * width + j - j % 2;
y = input[yIndex] & 0xff;
u = input[nvOff + nvIndex ] & 0xff;
v = input[nvOff + nvIndex + 1] & 0xff;
//对于位置为i,j的像素,根据其yuv的值计算出r,g,b的值
r = y + ((351 * (v-128))>>8); //r
g = y - ((179 * (v-128) + 86 * (u-128))>>8); //g
b = y + ((443 * (u-128))>>8); //b
//要求的rgb值是byte类型,范围是0--255,消除越界的值。
r = ((r>255) ?255 :(r<0)?0:r);
g = ((g>255) ?255 :(g<0)?0:g);
b = ((b>255) ?255 :(b<0)?0:b);
// 对结果rgb/bgr的值赋值,a通道表示透明度,这里都给255(根据你自己的场景设置)
//由于rgba格式和bgra格式只是r,g,b的顺序不同,因此这里通过bool值判断,既可以得到rgba的格式也可以得到bgra格式的数组。
if(isRGB){
output[yIndex*4 + 0] = (byte) b;
output[yIndex*4 + 1] = (byte) g;
output[yIndex*4 + 2] = (byte) r;
output[yIndex*4 + 3] = (byte) 255;
}else{
output[yIndex*4 + 0] = (byte) r;
output[yIndex*4 + 1] = (byte) g;
output[yIndex*4 + 2] = (byte) b;
output[yIndex*4 + 3] = (byte) 255;
}
}
}
}
NV21向右旋转90+镜像
恭喜你,你需要自己去实现~~~~~~~~~~~~
NV21向右旋转270+镜像
恭喜你,你需要自己去实现~~~~~~~~~~~~
NV21向右旋转90+转RGBA
public void NV21rRotate90Degree2BGRA(byte[]input , int width , int height , byte[]output, boolean isRGB)
{
int nvOff = width * height ;
int i, j, yIndex = 0;
int y, u, v;
int r, g, b, nvIndex = 0;
for(i = 0; i < height; i++){
for(j = 0; j < width; j ++,++yIndex){
nvIndex = (i / 2) * width + j - j % 2;
y = input[yIndex] & 0xff;
u = input[nvOff + nvIndex ] & 0xff;
v = input[nvOff + nvIndex + 1] & 0xff;
// yuv to rgb
r = y + ((351 * (v-128))>>8); //r
g = y - ((179 * (v-128) + 86 * (u-128))>>8); //g
b = y + ((443 * (u-128))>>8); //b
r = ((r>255) ?255 :(r<0)?0:r);
g = ((g>255) ?255 :(g<0)?0:g);
b = ((b>255) ?255 :(b<0)?0:b);
int index = j*height+(height - i)-1;
if(isRGB){
output[index*4 + 0] = (byte) b;
output[index*4 + 1] = (byte) g;
output[index*4 + 2] = (byte) r;
output[index*4 + 3] = (byte) 255;
}else{
output[index*4 + 0] = (byte) r;
output[index*4 + 1] = (byte) g;
output[index*4 + 2] = (byte) b;
output[index*4 + 3] = (byte) 255;
}
}
}
}
NV21数组顺时针转270度+镜像+转换RGBA数据
public void NV21Rotate270Degree2BGRAandMirror(byte[]input , int width , int height , byte[]output, boolean isRGB)
{
int nvOff = width * height ;
int i, j, yIndex = 0;
int y, u, v;
int r, g, b, nvIndex = 0;
for(i = 0; i < height; i++){
for(j = 0; j < width; j ++,++yIndex){
nvIndex = (i / 2) * width + j - j % 2;
y = input[yIndex] & 0xff;
u = input[nvOff + nvIndex ] & 0xff;
v = input[nvOff + nvIndex + 1] & 0xff;
// yuv to rgb
r = y + ((351 * (v-128))>>8); //r
g = y - ((179 * (v-128) + 86 * (u-128))>>8); //g
b = y + ((443 * (u-128))>>8); //b
r = ((r>255) ?255 :(r<0)?0:r);
g = ((g>255) ?255 :(g<0)?0:g);
b = ((b>255) ?255 :(b<0)?0:b);
int index = (width-j-1)*height + (height-i-1);
if(isRGB){
output[index*4 + 0] = (byte) b;
output[index*4 + 1] = (byte) g;
output[index*4 + 2] = (byte) r;
output[index*4 + 3] = (byte) 255;
}else{
output[index*4 + 0] = (byte) r;
output[index*4 + 1] = (byte) g;
output[index*4 + 2] = (byte) b;
output[index*4 + 3] = (byte) 255;
}
}
}
}