Android 虹软人脸识别 NV21 和 RGB24 转换问题

当我们使用活体检测检测完毕后,需要上传图片给后台进行人脸识别,在 Android 里面默认的 Camera 获取的图片格式是 NV21.

怎么知道这个默认格式呢?

1
2
Camera.Parameters parameters = camera.getParameters();
parameters.getPreviewFormat();

这个格式的图片本身虹软是支持的,但是有个条件限制:SDK 对图形尺寸做了限制,宽高需要大于 0,宽度为 4 的倍数, YUYV/I420/NV21/NV12 格式的图片高度为 2 的倍数, RGB24 格式的图片高度不限制。如果你的图片是 NV21 格式不符合这个条件就需要进行转换。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
private static final int VALUE_FOR_4_ALIGN = 0b11;
private static final int VALUE_FOR_2_ALIGN = 0b01;

/**
    * 确保传给引擎的NV21数据宽度为4的倍数,高为2的倍数
    *
    * @param bitmap 传入的bitmap
    * @return 调整后的bitmap
    */
public static Bitmap alignBitmapForNv21(Bitmap bitmap) {
    if (bitmap == null || bitmap.getWidth() < 4 || bitmap.getHeight() < 2) {
        return null;
    }
    int width = bitmap.getWidth();
    int height = bitmap.getHeight();

    boolean needAdjust = false;
    //保证宽度是4的倍数
    if ((width & VALUE_FOR_4_ALIGN) != 0) {
        width &= ~VALUE_FOR_4_ALIGN;
        needAdjust = true;
    }

    //保证高度是2的倍数
    if ((height & VALUE_FOR_2_ALIGN) != 0) {
        height--;
        needAdjust = true;
    }

    if (needAdjust) {
        bitmap = imageCrop(bitmap, new Rect(0, 0, width, height));
    }
    return bitmap;
}

如何从 onPreviewFrame(byte[] data, Camera camera) 回调中获取 Bitmap 对象呢?这个时候就需要用到 YuvImage.

什么是 YuvImage

YuvImage 包含了 YUV 数据,并且提供了一个将 YUV 数据压缩成 Jpeg 数据的方法。

什么时候使用

相机 Camera 类的 PreviewCallback 回调中,这个接口回调的是相机的预览图片,是 YUV 格式的数据,这是,利用 YuvImage 对象的 compressToJpeg 方法生成 Jpeg 格式的图片保存在本地。

1
2
3
4
5
6
7
8
Camera.Parameters parameters = camera.getParameters();
Camera.Size size = parameters.getPreviewSize();
YuvImage image = new YuvImage(data, parameters.getPreviewFormat(),
        size.width, size.height, null);
File file = new File(Environment.getExternalStorageDirectory()
        .getPath() + "/check_face.jpg");
FileOutputStream filecon = new FileOutputStream(file);
image.compressToJpeg(new Rect(0, 0, image.getWidth(), image.getHeight()), 90, filecon);

当然也可以使用如下方式转换为 Bitmap 对象:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Camera.Parameters parameters = camera.getParameters();
Camera.Size size = parameters.getPreviewSize();
YuvImage image = new YuvImage(data, parameters.getPreviewFormat(),
        size.width, size.height, null);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
image.compressToJpeg(new Rect(0, 0, image.getWidth(), image.getHeight()), 90, outputStream);
byte[] jpegData = outputStream.toByteArray();

BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 1;
Bitmap nv21Bitmap = BitmapFactory.decodeByteArray(jpegData, 0, jpegData.length);

得到 Bitmap 对象后我们可以先进行旋转等处理,当然上面的 compressToJpeg 中的 Rect 最好是头像的轮廓,而不是整个画布,这样可以极大的缩小图片大小。

1
2
3
4
Matrix matrix = new Matrix();
//matrix.postScale(1, 1);
matrix.postRotate(cameraOri);
Bitmap matrixedBitmap = Bitmap.createBitmap(nv21Bitmap, 0, 0, nv21Bitmap.getHeight(), nv21Bitmap.getWidth(), matrix, true);

最后进行 NV21 的宽高检测,确保传给引擎的 NV21 数据宽度为 4 的倍数,高为 2 的倍数,然后保存为文件上传给服务器。

1
2
3
4
Bitmap checkedBitmap = ImageUtils.alignBitmapForNv21(matrixedBitmap);
File file = saveBitmapToFile(checkedBitmap, Environment.getExternalStorageDirectory()
        .getPath() + "/check_face.jpg");
LogPlus.d("http", "开始上传头像");

最后再给出一些常用的工具方法:

保存Bitmap到文件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
private File saveBitmapToFile(Bitmap bitmap, String filePath) {
    if (bitmap == null || TextUtils.isEmpty(filePath)) {
        return null;
    }
    try {
        File file = new File(filePath);
        if (!file.exists() && file.getParentFile().mkdirs() && !file.createNewFile()) {
            return null;
        }
        OutputStream os = new BufferedOutputStream(new FileOutputStream(file));
        bitmap.compress(Bitmap.CompressFormat.JPEG, 100, os);
        os.close();
        return file;
    } catch (IOException e) {
        e.printStackTrace();
        return null;
    }
}

NV21 转换为 Bitmap 对象

1
2
3
4
5
6
7
8
9
private Bitmap nv21ToBitmap(byte[] data, int width, int height) {
    YuvImage yuvImage = new YuvImage(data, ImageFormat.NV21, width, height, null);
    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    yuvImage.compressToJpeg(new Rect(0, 0, yuvImage.getWidth(), yuvImage.getHeight()), 70, outputStream);
    byte[] jpegData = outputStream.toByteArray();
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inSampleSize = 1;
    return BitmapFactory.decodeByteArray(jpegData, 0, jpegData.length);
}

确保传给引擎的 BGR24 数据宽度为4的倍数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public static Bitmap alignBitmapForBgr24(Bitmap bitmap) {
    if (bitmap == null || bitmap.getWidth() < 4) {
        return null;
    }
    int width = bitmap.getWidth();
    int height = bitmap.getHeight();

    boolean needAdjust = false;

    //保证宽度是4的倍数
    if ((width & VALUE_FOR_4_ALIGN) != 0) {
        width &= ~VALUE_FOR_4_ALIGN;
        needAdjust = true;
    }

    if (needAdjust) {
        bitmap = imageCrop(bitmap, new Rect(0, 0, width, height));
    }
    return bitmap;
}

Bitmap 转化为 RGB 数据(格式为 Bitmap.Config#ARGB_8888)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public static byte[] bitmapToBgr(Bitmap image) {
    if (image == null) {
        return null;
    }
    int bytes = image.getByteCount();
    ByteBuffer buffer = ByteBuffer.allocate(bytes);
    image.copyPixelsToBuffer(buffer);
    byte[] temp = buffer.array();
    byte[] pixels = new byte[(temp.length / 4) * 3];
    for (int i = 0; i < temp.length / 4; i++) {
        pixels[i * 3] = temp[i * 4 + 2];
        pixels[i * 3 + 1] = temp[i * 4 + 1];
        pixels[i * 3 + 2] = temp[i * 4];
    }
    return pixels;
}