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

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

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

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

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

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 格式的图片保存在本地。

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 对象:

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 最好是头像的轮廓,而不是整个画布,这样可以极大的缩小图片大小。

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 的倍数,然后保存为文件上传给服务器。

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

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

保存Bitmap到文件

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 对象

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的倍数

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)

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;
}