2

I need to get a bitmap correctly from ImageProxy and I tried several methods. I tried the following method for conversion ImageProxy to bitmap:

class CustomBitmapConverter(context: Context) {
private val rs = RenderScript.create(context)
@RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
private val scriptYuvToRgb = ScriptIntrinsicYuvToRGB.create(rs, Element.U8_4(rs))

private var pixelCount: Int = -1
private lateinit var yuvBuffer: ByteBuffer
private lateinit var inputAllocation: Allocation
private lateinit var outputAllocation: Allocation

@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
@Synchronized
fun convertYuvToBitmap(image: Image): Bitmap {

    val conf = Bitmap.Config.ARGB_8888
    val output = Bitmap.createBitmap(image.width, image.height, conf)

    // Ensure that the intermediate output byte buffer is allocated
    if (!::yuvBuffer.isInitialized) {
        pixelCount = image.cropRect.width() * image.cropRect.height()
        yuvBuffer = ByteBuffer.allocateDirect(
                pixelCount * ImageFormat.getBitsPerPixel(ImageFormat.YUV_420_888) / 8)
    }

    // Get the YUV data in byte array form
    imageToByteBuffer(image, yuvBuffer)

    // Ensure that the RenderScript inputs and outputs are allocated
    if (!::inputAllocation.isInitialized) {
        inputAllocation = Allocation.createSized(rs, Element.U8(rs), yuvBuffer.array().size)
    }
    if (!::outputAllocation.isInitialized) {
        outputAllocation = Allocation.createFromBitmap(rs, output)
    }

    // Convert YUV to RGB
    inputAllocation.copyFrom(yuvBuffer.array())
    scriptYuvToRgb.setInput(inputAllocation)
    scriptYuvToRgb.forEach(outputAllocation)
    outputAllocation.copyTo(output)

    return output
}

@RequiresApi(Build.VERSION_CODES.KITKAT)
private fun imageToByteBuffer(image: Image, outputBuffer: ByteBuffer) {
    assert(image.format == ImageFormat.YUV_420_888)

    val imageCrop = image.cropRect
    val imagePlanes = image.planes
    val rowData = ByteArray(imagePlanes.first().rowStride)

    imagePlanes.forEachIndexed { planeIndex, plane ->

        val outputStride: Int
        var outputOffset: Int

        when (planeIndex) {
            0 -> {
                outputStride = 1
                outputOffset = 0
            }
            1 -> {
                outputStride = 2
                outputOffset = pixelCount + 1
            }
            2 -> {
                outputStride = 2
                outputOffset = pixelCount
            }
            else -> {
                return@forEachIndexed
            }
        }

        val buffer = 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()

        buffer.position(rowStride * planeCrop.top + pixelStride * planeCrop.left)
        for (row in 0 until planeHeight) {
            val length: Int
            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
                length = planeWidth
                buffer.get(outputBuffer.array(), outputOffset, length)
                outputOffset += length
            } else {
                // When either pixel or output have a stride > 1 we must copy pixel by pixel
                length = (planeWidth - 1) * pixelStride + 1
                buffer.get(rowData, 0, length)
                for (col in 0 until planeWidth) {
                    outputBuffer.array()[outputOffset] = rowData[col * pixelStride]
                    outputOffset += outputStride
                }
            }

            if (row < planeHeight - 1) {
                buffer.position(buffer.position() + rowStride - length)
            }
        }
    }
}

However this reduces quality significantly and you image losses sharpness. Then I tried the following conversion:

 // Image → JPEG
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
public static byte[] imageToByteArray(Image image) {
    byte[] data = null;
    if (image.getFormat() == ImageFormat.JPEG) {
        Image.Plane[] planes = image.getPlanes();
        ByteBuffer buffer = planes[0].getBuffer();
        data = new byte[buffer.capacity()];
        buffer.get(data);
        return data;
    } else if (image.getFormat() == ImageFormat.YUV_420_888) {
        data = NV21toJPEG(YUV_420_888toNV21(image),
                image.getWidth(), image.getHeight());
    }
    return data;
}

// YUV_420_888 → NV21
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
private static byte[] YUV_420_888toNV21(Image image) {
    byte[] nv21;
    ByteBuffer yBuffer = image.getPlanes()[0].getBuffer();
    ByteBuffer uBuffer = image.getPlanes()[1].getBuffer();
    ByteBuffer vBuffer = image.getPlanes()[2].getBuffer();
    int ySize = yBuffer.remaining();
    int uSize = uBuffer.remaining();
    int vSize = vBuffer.remaining();
    nv21 = new byte[ySize + uSize + vSize];
    yBuffer.get(nv21, 0, ySize);
    vBuffer.get(nv21, ySize, vSize);
    uBuffer.get(nv21, ySize + vSize, uSize);
    return nv21;
}

// NV21 → JPEG
private static byte[] NV21toJPEG(byte[] nv21, int width, int height) {
    ByteArrayOutputStream out = new ByteArrayOutputStream();
    YuvImage yuv = new YuvImage(nv21, ImageFormat.NV21, width, height, null);
    yuv.compressToJpeg(new Rect(0, 0, width, height), 100, out);
    return out.toByteArray();
}

This keeps all ratio, but on device like Xioami Mi A2 image has red/blue colors all over it so you can't even see anything there... There is some bug definitely with ImageProxy.

Then I did something much simpler. I get bitmap directly from PreviewView. I use this code:

@RequiresApi(api = Build.VERSION_CODES.KITKAT)
@SuppressLint("UnsafeExperimentalUsageError")
@Override
public void analyze(@NonNull ImageProxy image) {
      final Bitmap bitmap = previewView.getBitmap();
            if(bitmap==null){
                return;
            }
            int rotation = CameraUtils.INSTANCE.getRotationDegrees(previewView.getDisplay().getRotation());
  
    }

Then I don't even need to handle rotation, because returned bitmap has exactly same rotation as I set it with Preview builder:

 internal fun buildCameraXPreview(screenAspectRatio: Int, cameraRotation: Int): Preview {
    return Preview.Builder()
            .setTargetAspectRatio(screenAspectRatio)
            .setTargetRotation(cameraRotation)
            .build()
}

It works well for all cases. Quality, size are same as preview. No freezes or etc.

Now I need your feedback. Is this a viable solution? Are there any alternatives? Perhaps someone managed to fix conversion from Proxy to bitmap...

EDIT I have looked at "recommended" approach in one of the repos from Google here

However, this does not seem even recommended approach, because they have literally annotated this as experimental and only for demonstration...

Viktor Vostrikov
  • 1,322
  • 3
  • 19
  • 36
  • Curiously, have you tried [this recommended approach](https://github.com/android/camera-samples/blob/3730442b49189f76a1083a98f3acf3f5f09222a3/CameraUtils/lib/src/main/java/com/example/android/camera/utils/YuvToRgbConverter.kt)? – Husayn Hakeem Jun 19 '20 at 21:49
  • Yes I tried. Quality is bad – Viktor Vostrikov Jun 20 '20 at 10:43
  • In your first example, the input allocation is created with element type U8 instead of yuv, which is why ScriptIntrinsicYuvToRGB will not work correctly. In any case there are known bugs when using yuv allocations which are not initialized as input from the camera: https://issuetracker.google.com/issues/37130136 – spectralio Jun 23 '20 at 12:44

0 Answers0