CameraX API 的 YUV_420_888 图像转换为NV21数据和Bitmap

11 篇文章 1 订阅

CameraX打开相机预览的功能这里不赘述,大家可以在Android官网找到

CameraX 概览  |  Android 开发者  |  Android Developershttps://developer.android.google.cn/training/camerax直接从获取到图像开始

imageAnalysis.setAnalyzer(ContextCompat.getMainExecutor(activity), imageProxy -> {
    //TODO
    ...
    imageProxy.close();//这里如果不关闭,则不会继续获取图像
});

这里ImageProxy是回调的图像数据,接口为

public interface ImageProxy extends AutoCloseable {
    /**
     * Closes the underlying {@link android.media.Image}.
     *
     * @see android.media.Image#close()
     */
    @Override
    void close();

    /**
     * Returns the crop rectangle.
     *
     * @see android.media.Image#getCropRect()
     */
    @NonNull
    Rect getCropRect();

    /**
     * Sets the crop rectangle.
     *
     * @see android.media.Image#setCropRect(Rect)
     */
    void setCropRect(@Nullable Rect rect);

    /**
     * Returns the image format.
     *
     * @see android.media.Image#getFormat()
     */
    int getFormat();

    /**
     * Returns the image height.
     *
     * @see android.media.Image#getHeight()
     */
    int getHeight();

    /**
     * Returns the image width.
     *
     * @see android.media.Image#getWidth()
     */
    int getWidth();

    /**
     * Returns the array of planes.
     *
     * @see android.media.Image#getPlanes()
     */
    @NonNull
    @SuppressLint("ArrayReturn")
    PlaneProxy[] getPlanes();

    /** A plane proxy which has an analogous interface as {@link android.media.Image.Plane}. */
    interface PlaneProxy {
        /**
         * Returns the row stride.
         *
         * @see android.media.Image.Plane#getRowStride()
         */
        int getRowStride();

        /**
         * Returns the pixel stride.
         *
         * @see android.media.Image.Plane#getPixelStride()
         */
        int getPixelStride();

        /**
         * Returns the pixels buffer.
         *
         * @see android.media.Image.Plane#getBuffer()
         */
        @NonNull
        ByteBuffer getBuffer();
    }

    /** Returns the {@link ImageInfo}. */
    @NonNull
    ImageInfo getImageInfo();

    /**
     * Returns the android {@link Image}.
     *
     * <p>If the ImageProxy is a wrapper for an android {@link Image}, it will return the
     * {@link Image}. It is possible for an ImageProxy to wrap something that isn't an
     * {@link Image}. If that's the case then it will return null.
     *
     * <p>The returned image should not be closed by the application. Instead it should be closed by
     * the ImageProxy, which happens, for example, on return from the {@link ImageAnalysis.Analyzer}
     * function.  Destroying the {@link ImageAnalysis} will close the underlying
     * {@link android.media.ImageReader}.  So an {@link Image} obtained with this method will behave
     * as such.
     *
     * @see android.media.Image#close()
     *
     * @return the android image.
     */
    @Nullable
    @ExperimentalGetImage
    Image getImage();
}

拿到的图像数据分为YUV存在了这里

PlaneProxy[] getPlanes();

这里先介绍一下图像格式YUV_420_888

YUV_420_888 是一种通用格式,可以描述任何 YUV 图像,其中 U 和 V 在两个维度上都以 2 倍的因子进行二次采样。
{@link Image#getPlanes} 返回一个包含 Y、U 和 V 平面的数组
Y 平面保证不会交错,因此我们可以将其值复制到 NV21 数组的第一部分。U 和 V 平面可能已经具有 NV21 格式的表示。
如果平面共享相同的缓冲区,则会发生这种情况,V 缓冲区位于 U 缓冲区之前的一个位置,并且平面的 pixelStride 为 2。

YUV_420_888图像保存数据的格式可以分为两种:

1.YYYYYYYYYYYYYYYYY(...)UUUUU(...)VVVVV(...)

2.YYYYYYYYYYYYYYYYY(...)VUVUVUVUVUVU(...)

接下来介绍一下PlaneProxy[]数组中YUV数据的保存方式。

YYYYYYYYYYYYYYYYY(...)UUUUU(...)VVVV(...)

Y缓冲区 PlaneProxy[0]  YYYYYYYYY

U缓冲区 PlaneProxy[1] UUUUU

V缓冲区 PlaneProxy[2] VVVVV

YYYYYYYYYYYYYYYYY(...)VUVUVUVUVUVU(...)

Y缓冲区 PlaneProxy[0]  YYYYYYYYY(...)

U缓冲区 PlaneProxy[1]  UVUVUVUV(...)U

V缓冲区 PlaneProxy[2]  VUVUVUVU(...)V 

PS:PlaneProxy[1]去掉最后一个U PlaneProxy[2]去掉第一个V,则两个缓冲区完全一样

两种格式转NV21

YYYYYYYYYYYYYYYYY(...)UUUUU(...)VVVV(...)

private static void unpackPlane(Plane plane, int width, int height, byte[] out, in
    ByteBuffer buffer = plane.getBuffer();
    buffer.rewind();
    // 计算当前平面的大小。假设它的纵横比与原始图像相同。
    int numRow = (buffer.limit() + plane.getRowStride() - 1) / plane.getRowStride(
    if (numRow == 0) {
        return;
    }
    int scaleFactor = height / numRow;
    int numCol = width / scaleFactor;
    // 提取输出缓冲区中的数据。
    int outputPos = offset;
    int rowStart = 0;
    for (int row = 0; row < numRow; row++) {
        int inputPos = rowStart;
        for (int col = 0; col < numCol; col++) {
            out[outputPos] = buffer.get(inputPos);
            outputPos += pixelStride;
            inputPos += plane.getPixelStride();
        }
        rowStart += plane.getRowStride();
    }
}

unpackPlane(yuv420888planes[0], width, height, out, 0, 1);
unpackPlane(yuv420888planes[1], width, height, out, imageSize + 1, 2);
unpackPlane(yuv420888planes[2], width, height, out, imageSize, 2);

YYYYYYYYYYYYYYYYY(...)VUVUVUVUVUVU(...)

// 复制 Y 的值
yuv420888planes[0].getBuffer().get(out, 0, imageSize);
// 从 V 缓冲区获取第一个 V 值,因为 U 缓冲区不包含它。
yuv420888planes[2].getBuffer().get(out, imageSize, 1);
// 从 U 缓冲区复制第一个 U 值和剩余的 VU 值。
yuv420888planes[1].getBuffer().get(out, imageSize + 1, 2 * imageSize / 4 - 1);

判断是哪种格式

private static boolean areUVPlanesNV21(Plane[] planes, int width, int height) {
    int imageSize = width * height;
    ByteBuffer uBuffer = planes[1].getBuffer();
    ByteBuffer vBuffer = planes[2].getBuffer();
    // 备份缓冲区属性。
    int vBufferPosition = vBuffer.position();
    int uBufferLimit = uBuffer.limit();
    // 将 V 缓冲区推进 1 个字节,因为 U 缓冲区将不包含第一个 V 值。
    vBuffer.position(vBufferPosition + 1);
    // 切掉 U 缓冲区的最后一个字节,因为 V 缓冲区将不包含最后一个 U 值。
    uBuffer.limit(uBufferLimit - 1);
    // 检查缓冲区是否相等并具有预期的元素数量。
    boolean areNV21 = (vBuffer.remaining() == (2 * imageSize / 4 - 2)) && (vBuffer.compareTo(uBuffer) == 0);
    // 将缓冲区恢复到初始状态。
    vBuffer.position(vBufferPosition);
    uBuffer.limit(uBufferLimit);
    return areNV21;
}

Bitmap旋转和翻转

private static Bitmap rotateBitmap(Bitmap bitmap, int rotationDegrees, boolean flipX, boolean flipY) {
    Matrix matrix = new Matrix();
    // 图像旋转
    matrix.postRotate(rotationDegrees);
    // flipY垂直或者flipX水平镜像翻转
    matrix.postScale(flipX ? -1.0f : 1.0f, flipY ? -1.0f : 1.0f);
    Bitmap rotatedBitmap =
            Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
    // 如果旧bitmap已更改,则回收。
    if (rotatedBitmap != bitmap) {
        bitmap.recycle();
    }
    return rotatedBitmap;
}

全部转换代码

public class BitmapUtils {

    /**
     * 将 NV21 格式字节缓冲区转换为Bitmap。
     */
    @Nullable
    public static Bitmap getBitmap(ByteBuffer data, int width, int height, int rotation) {
        data.rewind();
        byte[] imageInBuffer = new byte[data.limit()];
        data.get(imageInBuffer, 0, imageInBuffer.length);
        try {
            YuvImage image = new YuvImage(imageInBuffer, ImageFormat.NV21, width, height, null);
            ByteArrayOutputStream stream = new ByteArrayOutputStream();
            image.compressToJpeg(new Rect(0, 0, width, height), 80, stream);

            Bitmap bmp = BitmapFactory.decodeByteArray(stream.toByteArray(), 0, stream.size());

            stream.close();
            return rotateBitmap(bmp, rotation, true, false);
        } catch (Exception e) {
            Log.e("VisionProcessorBase", "Error: " + e.getMessage());
        }
        return null;
    }

    /**
     * 将来自 CameraX API 的 YUV_420_888 图像转换为Bitmap。
     */
    @RequiresApi(VERSION_CODES.LOLLIPOP)
    @Nullable
    @ExperimentalGetImage
    public static Bitmap getBitmap(ImageProxy imageProxy) {
        if (imageProxy.getImage() == null)
            return null;
        ByteBuffer nv21Buffer = yuv420ThreePlanesToNV21(imageProxy.getImage().getPlanes(), imageProxy.getWidth(), imageProxy.getHeight());
        return getBitmap(nv21Buffer, imageProxy.getWidth(), imageProxy.getHeight(), imageProxy.getImageInfo().getRotationDegrees());
    }

    /**
     * bitmap旋转或者翻转
     */
    private static Bitmap rotateBitmap(Bitmap bitmap, int rotationDegrees, boolean flipX, boolean flipY) {
        Matrix matrix = new Matrix();

        // 图像旋转
        matrix.postRotate(rotationDegrees);

        // flipY垂直或者flipX水平镜像翻转
        matrix.postScale(flipX ? -1.0f : 1.0f, flipY ? -1.0f : 1.0f);
        Bitmap rotatedBitmap =
                Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);

        // 如果旧bitmap已更改,则回收。
        if (rotatedBitmap != bitmap) {
            bitmap.recycle();
        }
        return rotatedBitmap;
    }
    
    /**
     * YUV_420_888格式转换成NV21.
     *
     * NV21 格式由一个包含 Y、U 和 V 值的单字节数组组成。
     * 对于大小为 S 的图像,数组的前 S 个位置包含所有 Y 值。其余位置包含交错的 V 和 U 值。
     * U 和 V 在两个维度上都进行了 2 倍的二次采样,因此有 S/4 U 值和 S/4 V 值。
     * 总之,NV21 数组将包含 S 个 Y 值,后跟 S/4 + S/4 VU 值: YYYYYYYYYYYYYY(...)YVUVUVUVU(...)VU
     *
     * YUV_420_888 是一种通用格式,可以描述任何 YUV 图像,其中 U 和 V 在两个维度上都以 2 倍的因子进行二次采样。
     * {@link Image#getPlanes} 返回一个包含 Y、U 和 V 平面的数组
     * Y 平面保证不会交错,因此我们可以将其值复制到 NV21 数组的第一部分。U 和 V 平面可能已经具有 NV21 格式的表示。
     * 如果平面共享相同的缓冲区,则会发生这种情况,V 缓冲区位于 U 缓冲区之前的一个位置,并且平面的 pixelStride 为 2。
     * 如果是这种情况,我们可以将它们复制到 NV21 阵列中。
     */
    @RequiresApi(VERSION_CODES.KITKAT)
    private static ByteBuffer yuv420ThreePlanesToNV21(
            Plane[] yuv420888planes, int width, int height) {
        int imageSize = width * height;
        byte[] out = new byte[imageSize + 2 * (imageSize / 4)];

        if (areUVPlanesNV21(yuv420888planes, width, height)) {
            // 复制 Y 的值
            yuv420888planes[0].getBuffer().get(out, 0, imageSize);
            // 从 V 缓冲区获取第一个 V 值,因为 U 缓冲区不包含它。
            yuv420888planes[2].getBuffer().get(out, imageSize, 1);
            // 从 U 缓冲区复制第一个 U 值和剩余的 VU 值。
            yuv420888planes[1].getBuffer().get(out, imageSize + 1, 2 * imageSize / 4 - 1);
        } else {
            // 回退到一个一个地复制 UV 值,这更慢但也有效。
            // 取 Y.
            unpackPlane(yuv420888planes[0], width, height, out, 0, 1);
            // 取 U.
            unpackPlane(yuv420888planes[1], width, height, out, imageSize + 1, 2);
            // 取 V.
            unpackPlane(yuv420888planes[2], width, height, out, imageSize, 2);
        }

        return ByteBuffer.wrap(out);
    }

    /**
     * 检查 YUV_420_888 图像的 UV 平面缓冲区是否为 NV21 格式。
     */
    @RequiresApi(VERSION_CODES.KITKAT)
    private static boolean areUVPlanesNV21(Plane[] planes, int width, int height) {
        int imageSize = width * height;

        ByteBuffer uBuffer = planes[1].getBuffer();
        ByteBuffer vBuffer = planes[2].getBuffer();

        // 备份缓冲区属性。
        int vBufferPosition = vBuffer.position();
        int uBufferLimit = uBuffer.limit();

        // 将 V 缓冲区推进 1 个字节,因为 U 缓冲区将不包含第一个 V 值。
        vBuffer.position(vBufferPosition + 1);
        // 切掉 U 缓冲区的最后一个字节,因为 V 缓冲区将不包含最后一个 U 值。
        uBuffer.limit(uBufferLimit - 1);

        // 检查缓冲区是否相等并具有预期的元素数量。
        boolean areNV21 = (vBuffer.remaining() == (2 * imageSize / 4 - 2)) && (vBuffer.compareTo(uBuffer) == 0);

        // 将缓冲区恢复到初始状态。
        vBuffer.position(vBufferPosition);
        uBuffer.limit(uBufferLimit);

        return areNV21;
    }

    /**
     * 将图像平面解压缩为字节数组。
     *
     * 输入平面数据将被复制到“out”中,从“offset”开始,每个像素将被“pixelStride”隔开。 请注意,输出上没有行填充。
     */
    @TargetApi(VERSION_CODES.KITKAT)
    private static void unpackPlane(Plane plane, int width, int height, byte[] out, int offset, int pixelStride) {
        ByteBuffer buffer = plane.getBuffer();
        buffer.rewind();

        // 计算当前平面的大小。假设它的纵横比与原始图像相同。
        int numRow = (buffer.limit() + plane.getRowStride() - 1) / plane.getRowStride();
        if (numRow == 0) {
            return;
        }
        int scaleFactor = height / numRow;
        int numCol = width / scaleFactor;

        // 提取输出缓冲区中的数据。
        int outputPos = offset;
        int rowStart = 0;
        for (int row = 0; row < numRow; row++) {
            int inputPos = rowStart;
            for (int col = 0; col < numCol; col++) {
                out[outputPos] = buffer.get(inputPos);
                outputPos += pixelStride;
                inputPos += plane.getPixelStride();
            }
            rowStart += plane.getRowStride();
        }
    }
}

这样可以实时的把相机预览数据转换成bitmap,其中NV21数据也可以是编码的原数据。

  • 17
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
好的,针对您的问题,我可以提供以下的参考代码,可以将 Android camera2 api YUV_420_888 格式的数据转换为 RGB 格式的数据: ``` private void convertYUV420ToRGB(Image imgYUV, Bitmap imgRGB) { // Get image width and height int width = imgYUV.getWidth(); int height = imgYUV.getHeight(); // Get Y, U, and V image planes from image Image.Plane[] planes = imgYUV.getPlanes(); ByteBuffer bufferY = planes[0].getBuffer(); ByteBuffer bufferU = planes[1].getBuffer(); ByteBuffer bufferV = planes[2].getBuffer(); // Calculate pixel stride and row stride for Y, U, and V image planes int pixelStrideY = planes[0].getPixelStride(); int rowStrideY = planes[0].getRowStride(); int pixelStrideU = planes[1].getPixelStride(); int rowStrideU = planes[1].getRowStride(); int pixelStrideV = planes[2].getPixelStride(); int rowStrideV = planes[2].getRowStride(); // Create arrays for storing pixel data byte[] dataY = new byte[width * height * pixelStrideY]; byte[] dataU = new byte[width * height * pixelStrideU / 4]; byte[] dataV = new byte[width * height * pixelStrideV / 4]; // Copy pixel data from image planes into arrays bufferY.get(dataY, 0, width * height * pixelStrideY); bufferU.get(dataU, 0, width * height * pixelStrideU / 4); bufferV.get(dataV, 0, width * height * pixelStrideV / 4); // Initialize RGB pixel buffer int[] pixels = new int[width * height]; int offset = 0; // Loop through each row and column of pixels for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { // Get YUV pixel values for current pixel int Y = dataY[offset] & 0xff; int U = dataU[offset / 4] & 0xff; int V = dataV[offset / 4] & 0xff; // Calculate RGB pixel values for current pixel int R = (int)(Y + 1.370705f * (V - 128)); int G = (int)(Y - 0.698001f * (V - 128) - 0.337633f * (U - 128)); int B = (int)(Y + 1.732446f * (U - 128)); // Clip RGB pixel values to range [0, 255] R = Math.max(0, Math.min(255, R)); G = Math.max(0, Math.min(255, G)); B = Math.max(0, Math.min(255, B)); // Combine RGB pixel values into single pixel pixels[offset++] = (0xff << 24) | (R << 16) | (G << 8) | B; } // Move to next row of pixels offset += rowStrideY - width * pixelStrideY; } // Set RGB pixel data into Bitmap imgRGB.setPixels(pixels, 0, width, 0, 0, width, height); } ``` 这个方法将会把 Image 对象 imgYUV 中的 YUV 数据转换为 RGB 数据,并且将其存储到 Bitmap 对象 imgRGB 中。其中,YUV 数据的格式为 YUV_420_888,RGB 数据的格式为 ARGB_8888。 注意,这个方法中的转换公式是基于 YUV420 的格式,如果您的 YUV 数据格式不同,需要根据实际情况对转换公式进行修改。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不会写代码的猴子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值