【问题标题】:Converting ImageProxy to Bitmap将 ImageProxy 转换为位图
【发布时间】:2019-06-26 12:34:35
【问题描述】:

所以,我想探索新的 Google 相机 API - CameraX。 我想要做的是每秒从相机中获取一张图像,然后将其传递给一个接受位图以用于机器学习目的的函数。

我阅读了Camera X Image Analyzer 上的文档:

图像分析用例为您的应用提供 CPU 可访问的 image 执行图像处理、计算机视觉或机器 学习推理。应用程序实现了一个分析器方法 在每一帧上运行。

..这基本上是我需要的。所以,我这样实现了这个图像分析器:

imageAnalysis.setAnalyzer { image: ImageProxy, _: Int ->
    viewModel.onAnalyzeImage(image)
}

我得到的是image: ImageProxy。如何将此ImageProxy 转移到Bitmap

我试着这样解决它:

fun decodeBitmap(image: ImageProxy): Bitmap? {
    val buffer = image.planes[0].buffer
    val bytes = ByteArray(buffer.capacity()).also { buffer.get(it) }
    return BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
}

但它返回 null - 因为 decodeByteArray 没有接收到有效的 (?) 位图字节。有什么想法吗?

【问题讨论】:

    标签: android android-camerax


    【解决方案1】:

    您需要检查image.format 是否为ImageFormat.YUV_420_888。如果是这样,那么您可以使用此扩展程序将图像转换为位图:

    fun Image.toBitmap(): Bitmap {
        val yBuffer = planes[0].buffer // Y
        val vuBuffer = planes[2].buffer // VU
    
        val ySize = yBuffer.remaining()
        val vuSize = vuBuffer.remaining()
    
        val nv21 = ByteArray(ySize + vuSize)
    
        yBuffer.get(nv21, 0, ySize)
        vuBuffer.get(nv21, ySize, vuSize)
    
        val yuvImage = YuvImage(nv21, ImageFormat.NV21, this.width, this.height, null)
        val out = ByteArrayOutputStream()
        yuvImage.compressToJpeg(Rect(0, 0, yuvImage.width, yuvImage.height), 50, out)
        val imageBytes = out.toByteArray()
        return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
    }
    

    这适用于多种相机配置。但是,您可能需要使用考虑像素步幅的更高级的方法。

    【讨论】:

    • 这确实有效。唯一可行的解​​决方案之一,但是在我的图像分析器中实现此功能后,一切都变得缓慢且出现故障,帧在预览中移动缓慢,您是否设法解决了这个问题?
    • @Kashkashio 这是在模拟器上还是在实际设备上?我注意到图像分析用例在模拟器上不起作用,但我在真实设备上没有问题。如果您在真实设备上遇到问题,可以使用您的代码创建一个新的 stackoverflow 问题,我可以看看。
    • 我已经尝试过了,这实际上是我可以创建位图的唯一方法。但是 - 现在它无法识别我图像中的任何文本。在我通过 fromMediaImage 传递检测器之前,它确实识别文本。知道为什么吗?我尝试将生成的位图显示到ImageView,这只是胡言乱语。只是一堆绿线。
    • 我用上面的逻辑得到了乱七八糟的绿色图像。有没有解决问题?
    • 我也得到了乱码图像。有人修过吗?此解决方案根本不适用于小米 MI A2。
    【解决方案2】:

    我需要 Java 中的 codeMike A,所以我转换了它。

    你可以先用 Java 将 ImageProxy 转换为 Image

    Image image = imageProxy.getImage();
    

    然后你可以使用转换为Java的upper函数将Image转换为Bitmap

    private Bitmap toBitmap(Image image) {
        Image.Plane[] planes = image.getPlanes();
        ByteBuffer yBuffer = planes[0].getBuffer();
        ByteBuffer uBuffer = planes[1].getBuffer();
        ByteBuffer vBuffer = planes[2].getBuffer();
    
        int ySize = yBuffer.remaining();
        int uSize = uBuffer.remaining();
        int vSize = vBuffer.remaining();
    
        byte[] nv21 = new byte[ySize + uSize + vSize];
        //U and V are swapped
        yBuffer.get(nv21, 0, ySize);
        vBuffer.get(nv21, ySize, vSize);
        uBuffer.get(nv21, ySize + vSize, uSize);
    
        YuvImage yuvImage = new YuvImage(nv21, ImageFormat.NV21, image.getWidth(), image.getHeight(), null);
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        yuvImage.compressToJpeg(new Rect(0, 0, yuvImage.getWidth(), yuvImage.getHeight()), 75, out);
    
        byte[] imageBytes = out.toByteArray();
        return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length);
    }
    

    此答案的权利保留给Mike A

    【讨论】:

    • 您可能需要修复 compressToJpeg 行以将常量替换为 (0, 0, yuvImage.getWidth(), yuvImage.getHeight())
    • 这行不通。大多数设备的图像失真。 (在继续之前检查位图预览)。
    • 我移到了 yuvToRgbConverter.kt 文件
    【解决方案3】:

    此转换还有另一个implementation。首先YUV_420_888 转换为NV21 然后RenderScript 用于转换为位图(因此预计会更有效)。此外,它考虑了更正确的像素步幅。它也来自官方的 android camera samples repo。

    如果有人不想处理RenderScript 和同步这里是修改后的代码:

    fun ImageProxy.toBitmap(): Bitmap? {
        val nv21 = yuv420888ToNv21(this)
        val yuvImage = YuvImage(nv21, ImageFormat.NV21, width, height, null)
        return yuvImage.toBitmap()
    }
    
    private fun YuvImage.toBitmap(): Bitmap? {
        val out = ByteArrayOutputStream()
        if (!compressToJpeg(Rect(0, 0, width, height), 100, out))
            return null
        val imageBytes: ByteArray = out.toByteArray()
        return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
    }
    
    private fun yuv420888ToNv21(image: ImageProxy): ByteArray {
        val pixelCount = image.cropRect.width() * image.cropRect.height()
        val pixelSizeBits = ImageFormat.getBitsPerPixel(ImageFormat.YUV_420_888)
        val outputBuffer = ByteArray(pixelCount * pixelSizeBits / 8)
        imageToByteBuffer(image, outputBuffer, pixelCount)
        return outputBuffer
    }
    
    private fun imageToByteBuffer(image: ImageProxy, outputBuffer: ByteArray, pixelCount: Int) {
        assert(image.format == ImageFormat.YUV_420_888)
    
        val imageCrop = image.cropRect
        val imagePlanes = image.planes
    
        imagePlanes.forEachIndexed { planeIndex, plane ->
            // How many values are read in input for each output value written
            // Only the Y plane has a value for every pixel, U and V have half the resolution i.e.
            //
            // Y Plane            U Plane    V Plane
            // ===============    =======    =======
            // Y Y Y Y Y Y Y Y    U U U U    V V V V
            // Y Y Y Y Y Y Y Y    U U U U    V V V V
            // Y Y Y Y Y Y Y Y    U U U U    V V V V
            // Y Y Y Y Y Y Y Y    U U U U    V V V V
            // Y Y Y Y Y Y Y Y
            // Y Y Y Y Y Y Y Y
            // Y Y Y Y Y Y Y Y
            val outputStride: Int
    
            // The index in the output buffer the next value will be written at
            // For Y it's zero, for U and V we start at the end of Y and interleave them i.e.
            //
            // First chunk        Second chunk
            // ===============    ===============
            // Y Y Y Y Y Y Y Y    V U V U V U V U
            // Y Y Y Y Y Y Y Y    V U V U V U V U
            // Y Y Y Y Y Y Y Y    V U V U V U V U
            // Y Y Y Y Y Y Y Y    V U V U V U V U
            // Y Y Y Y Y Y Y Y
            // Y Y Y Y Y Y Y Y
            // Y Y Y Y Y Y Y Y
            var outputOffset: Int
    
            when (planeIndex) {
                0 -> {
                    outputStride = 1
                    outputOffset = 0
                }
                1 -> {
                    outputStride = 2
                    // For NV21 format, U is in odd-numbered indices
                    outputOffset = pixelCount + 1
                }
                2 -> {
                    outputStride = 2
                    // For NV21 format, V is in even-numbered indices
                    outputOffset = pixelCount
                }
                else -> {
                    // Image contains more than 3 planes, something strange is going on
                    return@forEachIndexed
                }
            }
    
            val planeBuffer = plane.buffer
            val rowStride = plane.rowStride
            val pixelStride = plane.pixelStride
    
            // We have to divide the width and height by two if it's not the Y plane
            val planeCrop = if (planeIndex == 0) {
                imageCrop
            } else {
                Rect(
                        imageCrop.left / 2,
                        imageCrop.top / 2,
                        imageCrop.right / 2,
                        imageCrop.bottom / 2
                )
            }
    
            val planeWidth = planeCrop.width()
            val planeHeight = planeCrop.height()
    
            // Intermediate buffer used to store the bytes of each row
            val rowBuffer = ByteArray(plane.rowStride)
    
            // Size of each row in bytes
            val rowLength = if (pixelStride == 1 && outputStride == 1) {
                planeWidth
            } else {
                // Take into account that the stride may include data from pixels other than this
                // particular plane and row, and that could be between pixels and not after every
                // pixel:
                //
                // |---- Pixel stride ----|                    Row ends here --> |
                // | Pixel 1 | Other Data | Pixel 2 | Other Data | ... | Pixel N |
                //
                // We need to get (N-1) * (pixel stride bytes) per row + 1 byte for the last pixel
                (planeWidth - 1) * pixelStride + 1
            }
    
            for (row in 0 until planeHeight) {
                // Move buffer position to the beginning of this row
                planeBuffer.position(
                        (row + planeCrop.top) * rowStride + planeCrop.left * pixelStride)
    
                if (pixelStride == 1 && outputStride == 1) {
                    // When there is a single stride value for pixel and output, we can just copy
                    // the entire row in a single step
                    planeBuffer.get(outputBuffer, outputOffset, rowLength)
                    outputOffset += rowLength
                } else {
                    // When either pixel or output have a stride > 1 we must copy pixel by pixel
                    planeBuffer.get(rowBuffer, 0, rowLength)
                    for (col in 0 until planeWidth) {
                        outputBuffer[outputOffset] = rowBuffer[col * pixelStride]
                        outputOffset += outputStride
                    }
                }
            }
        }
    }
    

    注意。 There is OpenCV android SDK 中的类似转换。

    【讨论】:

      【解决方案4】:

      从 image.getPlanes() 访问缓冲区时,我遇到了 ArrayIndexOutOfBoundsException。下面的函数可以毫无例外的将 ImageProxy 转换为 Bitmap。

      Java

      private Bitmap convertImageProxyToBitmap(ImageProxy image) {
              ByteBuffer byteBuffer = image.getPlanes()[0].getBuffer();
              byteBuffer.rewind();
              byte[] bytes = new byte[byteBuffer.capacity()];
              byteBuffer.get(bytes);
              byte[] clonedBytes = bytes.clone();
              return BitmapFactory.decodeByteArray(clonedBytes, 0, clonedBytes.length);
          }
      

      Kotlin 扩展函数

      fun ImageProxy.convertImageProxyToBitmap(): Bitmap {
              val buffer = planes[0].buffer
              buffer.rewind()
              val bytes = ByteArray(buffer.capacity())
              buffer.get(bytes)
              return BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
          }
      

      【讨论】:

      • 对于那些正在考虑为什么有不同的解决方案有效的人:使用imageProxy.getFormat() 检查 ImageProxy 的格式。如果您的格式是 35,您可以使用 @Mike A 解决方案,如果您的格式是 256,您可以使用 @darwin 解决方案。最后,显然,每种格式都需要不同的转换过程。 @Mike A 和 @Ahwar 解决方案适用于 YUV_420_888。图片格式:developer.android.com/reference/android/graphics/…
      • @BCJuan 是对的。我试过了,它对我有用,牢记格式并使用 Mike A、Ahwar 和 darwin 的算法。谢谢大家。
      • 这段代码抛出异常BitmapFactory.decodeByte…ray(bytes, 0, bytes.size) must not be null
      • 为我工作谢谢
      【解决方案5】:

      灵感来自@mike-a 的回答

      private fun ImageProxy.toMat(): Mat {
        val graySourceMatrix = Mat(height, width, CvType.CV_8UC1)
        val yBuffer = planes[0].buffer
        val ySize = yBuffer.remaining()
        val yPlane = ByteArray(ySize)
        yBuffer[yPlane, 0, ySize]
        graySourceMatrix.put(0, 0, yPlane)
        return graySourceMatrix
      }
      

      如果您打算使用 OpenCV,这将直接带您进入灰色矩阵,而颜色对您来说不再重要。

      为了提高性能,如果您在每一帧上都这样做,您可以将 Mat 的初始化移到外面。

      【讨论】:

        【解决方案6】:

        有一个更简单的解决方案。您只需从TextureView 获取Bitmap,无需任何转换。更多信息在documentation

        imageAnalysis.setAnalyzer { image: ImageProxy, _: Int ->
            val bitmap = textureView.bitmap
        }
        

        【讨论】:

        • 当我们这样做时,fps 正在下降。
        • 是的,我怀疑您是否应该这样做,分析用例期望您使用 imageProxy,因为它的分辨率较低且更易于管理,它也期望您关闭 imageProxy一旦您检索到当前图像缓冲区,以便相机可以移动到下一个传入帧。
        【解决方案7】:

        请看一下这个answer。您需要将其应用于您的问题就是从 ImageProxy 中获取 Image

        Image img = imaget.getImage();
        

        【讨论】:

        • 您似乎忘记了链接。另外,请不要发布多个相同的答案。如果问题是重复的,请将它们标记为重复。如果没有,请根据具体问题调整您的答案。
        • 链接已修复。如果答案实际上解决了两个不同的问题怎么办?是否应将不同的问题标记为重复?我是否将问题设置为重复?
        • 如果问题实际上不同,那么不,您不应该将它们标记为重复。但是,在这种情况下,答案通常可以用不同的措辞来表达,因为它们回答的是不同的问题。
        【解决方案8】:

        好吧,你给纹理视图设置了预览,你就可以了

        位图位图 = textureView.getBitmap();

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2022-08-22
          • 1970-01-01
          • 1970-01-01
          • 2013-12-15
          • 1970-01-01
          • 2012-07-23
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多