Bảo Huy

- fix android image result is not correct orientation and format

1 package dev.steenbakker.mobile_scanner 1 package dev.steenbakker.mobile_scanner
2 2
3 import android.app.Activity 3 import android.app.Activity
  4 +import android.graphics.Bitmap
  5 +import android.graphics.Matrix
4 import android.graphics.Rect 6 import android.graphics.Rect
5 import android.net.Uri 7 import android.net.Uri
6 import android.os.Handler 8 import android.os.Handler
@@ -16,9 +18,12 @@ import com.google.mlkit.vision.barcode.common.Barcode @@ -16,9 +18,12 @@ import com.google.mlkit.vision.barcode.common.Barcode
16 import com.google.mlkit.vision.common.InputImage 18 import com.google.mlkit.vision.common.InputImage
17 import dev.steenbakker.mobile_scanner.objects.DetectionSpeed 19 import dev.steenbakker.mobile_scanner.objects.DetectionSpeed
18 import dev.steenbakker.mobile_scanner.objects.MobileScannerStartParameters 20 import dev.steenbakker.mobile_scanner.objects.MobileScannerStartParameters
  21 +import dev.steenbakker.mobile_scanner.utils.YuvToRgbConverter
19 import io.flutter.view.TextureRegistry 22 import io.flutter.view.TextureRegistry
  23 +import java.io.ByteArrayOutputStream
20 import kotlin.math.roundToInt 24 import kotlin.math.roundToInt
21 25
  26 +
22 class MobileScanner( 27 class MobileScanner(
23 private val activity: Activity, 28 private val activity: Activity,
24 private val textureRegistry: TextureRegistry, 29 private val textureRegistry: TextureRegistry,
@@ -82,13 +87,40 @@ class MobileScanner( @@ -82,13 +87,40 @@ class MobileScanner(
82 } 87 }
83 } 88 }
84 89
  90 +
85 if (barcodeMap.isNotEmpty()) { 91 if (barcodeMap.isNotEmpty()) {
86 - mobileScannerCallback(  
87 - barcodeMap,  
88 - if (returnImage) mediaImage.toByteArray() else null,  
89 - if (returnImage) mediaImage.width else null,  
90 - if (returnImage) mediaImage.height else null  
91 - ) 92 + if (returnImage) {
  93 +
  94 + val bitmap = Bitmap.createBitmap(mediaImage.width, mediaImage.height, Bitmap.Config.ARGB_8888)
  95 +
  96 + val imageFormat = YuvToRgbConverter(activity.applicationContext)
  97 +
  98 + imageFormat.yuvToRgb(mediaImage, bitmap)
  99 +
  100 + val bmResult = rotateBitmap(bitmap, camera!!.cameraInfo.sensorRotationDegrees.toFloat())
  101 +
  102 + val stream = ByteArrayOutputStream()
  103 + bmResult.compress(Bitmap.CompressFormat.PNG, 100, stream)
  104 + val byteArray = stream.toByteArray()
  105 + bmResult.recycle()
  106 +
  107 +
  108 + mobileScannerCallback(
  109 + barcodeMap,
  110 + byteArray,
  111 + bmResult.width,
  112 + bmResult.height
  113 + )
  114 +
  115 + } else {
  116 +
  117 + mobileScannerCallback(
  118 + barcodeMap,
  119 + null,
  120 + null,
  121 + null
  122 + )
  123 + }
92 } 124 }
93 } 125 }
94 .addOnFailureListener { e -> 126 .addOnFailureListener { e ->
@@ -106,6 +138,13 @@ class MobileScanner( @@ -106,6 +138,13 @@ class MobileScanner(
106 } 138 }
107 } 139 }
108 140
  141 + fun rotateBitmap(bitmap: Bitmap, degrees: Float): Bitmap {
  142 + val matrix = Matrix()
  143 + matrix.postRotate(degrees)
  144 + return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
  145 + }
  146 +
  147 +
109 // scales the scanWindow to the provided inputImage and checks if that scaled 148 // scales the scanWindow to the provided inputImage and checks if that scaled
110 // scanWindow contains the barcode 149 // scanWindow contains the barcode
111 private fun isBarcodeInScanWindow( 150 private fun isBarcodeInScanWindow(
@@ -227,7 +266,6 @@ class MobileScanner( @@ -227,7 +266,6 @@ class MobileScanner(
227 }, executor) 266 }, executor)
228 267
229 } 268 }
230 -  
231 /** 269 /**
232 * Stop barcode scanning. 270 * Stop barcode scanning.
233 */ 271 */
  1 +package dev.steenbakker.mobile_scanner.utils
  2 +
  3 +import android.graphics.ImageFormat
  4 +import android.media.Image
  5 +import androidx.annotation.IntDef
  6 +import java.nio.ByteBuffer
  7 +
  8 +/*
  9 +This file is converted from part of https://github.com/gordinmitya/yuv2buf.
  10 +Follow the link to find demo app, performance benchmarks and unit tests.
  11 +
  12 +Intro to YUV image formats:
  13 +YUV_420_888 - is a generic format that can be represented as I420, YV12, NV21, and NV12.
  14 +420 means that for each 4 luminosity pixels we have 2 chroma pixels: U and V.
  15 +
  16 +* I420 format represents an image as Y plane followed by U then followed by V plane
  17 + without chroma channels interleaving.
  18 + For example:
  19 + Y Y Y Y
  20 + Y Y Y Y
  21 + U U V V
  22 +
  23 +* NV21 format represents an image as Y plane followed by V and U interleaved. First V then U.
  24 + For example:
  25 + Y Y Y Y
  26 + Y Y Y Y
  27 + V U V U
  28 +
  29 +* YV12 and NV12 are the same as previous formats but with swapped order of V and U. (U then V)
  30 +
  31 +Visualization of these 4 formats:
  32 +https://user-images.githubusercontent.com/9286092/89119601-4f6f8100-d4b8-11ea-9a51-2765f7e513c2.jpg
  33 +
  34 +It's guaranteed that image.getPlanes() always returns planes in order Y U V for YUV_420_888.
  35 +https://developer.android.com/reference/android/graphics/ImageFormat#YUV_420_888
  36 +
  37 +Because I420 and NV21 are more widely supported (RenderScript, OpenCV, MNN)
  38 +the conversion is done into these formats.
  39 +
  40 +More about each format: https://www.fourcc.org/yuv.php
  41 +*/
  42 +
  43 +@kotlin.annotation.Retention(AnnotationRetention.SOURCE)
  44 +@IntDef(ImageFormat.NV21, ImageFormat.YUV_420_888)
  45 +annotation class YuvType
  46 +
  47 +class YuvByteBuffer(image: Image, dstBuffer: ByteBuffer? = null) {
  48 + @YuvType
  49 + val type: Int
  50 + val buffer: ByteBuffer
  51 +
  52 + init {
  53 + val wrappedImage = ImageWrapper(image)
  54 +
  55 + type = if (wrappedImage.u.pixelStride == 1) {
  56 + ImageFormat.YUV_420_888
  57 + } else {
  58 + ImageFormat.NV21
  59 + }
  60 + val size = image.width * image.height * 3 / 2
  61 + buffer = if (
  62 + dstBuffer == null || dstBuffer.capacity() < size ||
  63 + dstBuffer.isReadOnly || !dstBuffer.isDirect
  64 + ) {
  65 + ByteBuffer.allocateDirect(size) }
  66 + else {
  67 + dstBuffer
  68 + }
  69 + buffer.rewind()
  70 +
  71 + removePadding(wrappedImage)
  72 + }
  73 +
  74 + // Input buffers are always direct as described in
  75 + // https://developer.android.com/reference/android/media/Image.Plane#getBuffer()
  76 + private fun removePadding(image: ImageWrapper) {
  77 + val sizeLuma = image.y.width * image.y.height
  78 + val sizeChroma = image.u.width * image.u.height
  79 + if (image.y.rowStride > image.y.width) {
  80 + removePaddingCompact(image.y, buffer, 0)
  81 + } else {
  82 + buffer.position(0)
  83 + buffer.put(image.y.buffer)
  84 + }
  85 + if (type == ImageFormat.YUV_420_888) {
  86 + if (image.u.rowStride > image.u.width) {
  87 + removePaddingCompact(image.u, buffer, sizeLuma)
  88 + removePaddingCompact(image.v, buffer, sizeLuma + sizeChroma)
  89 + } else {
  90 + buffer.position(sizeLuma)
  91 + buffer.put(image.u.buffer)
  92 + buffer.position(sizeLuma + sizeChroma)
  93 + buffer.put(image.v.buffer)
  94 + }
  95 + } else {
  96 + if (image.u.rowStride > image.u.width * 2) {
  97 + removePaddingNotCompact(image, buffer, sizeLuma)
  98 + } else {
  99 + buffer.position(sizeLuma)
  100 + var uv = image.v.buffer
  101 + val properUVSize = image.v.height * image.v.rowStride - 1
  102 + if (uv.capacity() > properUVSize) {
  103 + uv = clipBuffer(image.v.buffer, 0, properUVSize)
  104 + }
  105 + buffer.put(uv)
  106 + val lastOne = image.u.buffer[image.u.buffer.capacity() - 1]
  107 + buffer.put(buffer.capacity() - 1, lastOne)
  108 + }
  109 + }
  110 + buffer.rewind()
  111 + }
  112 +
  113 + private fun removePaddingCompact(
  114 + plane: PlaneWrapper,
  115 + dst: ByteBuffer,
  116 + offset: Int
  117 + ) {
  118 + require(plane.pixelStride == 1) {
  119 + "use removePaddingCompact with pixelStride == 1"
  120 + }
  121 +
  122 + val src = plane.buffer
  123 + val rowStride = plane.rowStride
  124 + var row: ByteBuffer
  125 + dst.position(offset)
  126 + for (i in 0 until plane.height) {
  127 + row = clipBuffer(src, i * rowStride, plane.width)
  128 + dst.put(row)
  129 + }
  130 + }
  131 +
  132 + private fun removePaddingNotCompact(
  133 + image: ImageWrapper,
  134 + dst: ByteBuffer,
  135 + offset: Int
  136 + ) {
  137 + require(image.u.pixelStride == 2) {
  138 + "use removePaddingNotCompact pixelStride == 2"
  139 + }
  140 + val width = image.u.width
  141 + val height = image.u.height
  142 + val rowStride = image.u.rowStride
  143 + var row: ByteBuffer
  144 + dst.position(offset)
  145 + for (i in 0 until height - 1) {
  146 + row = clipBuffer(image.v.buffer, i * rowStride, width * 2)
  147 + dst.put(row)
  148 + }
  149 + row = clipBuffer(image.u.buffer, (height - 1) * rowStride - 1, width * 2)
  150 + dst.put(row)
  151 + }
  152 +
  153 + private fun clipBuffer(buffer: ByteBuffer, start: Int, size: Int): ByteBuffer {
  154 + val duplicate = buffer.duplicate()
  155 + duplicate.position(start)
  156 + duplicate.limit(start + size)
  157 + return duplicate.slice()
  158 + }
  159 +
  160 + private class ImageWrapper(image:Image) {
  161 + val width= image.width
  162 + val height = image.height
  163 + val y = PlaneWrapper(width, height, image.planes[0])
  164 + val u = PlaneWrapper(width / 2, height / 2, image.planes[1])
  165 + val v = PlaneWrapper(width / 2, height / 2, image.planes[2])
  166 +
  167 + // Check this is a supported image format
  168 + // https://developer.android.com/reference/android/graphics/ImageFormat#YUV_420_888
  169 + init {
  170 + require(y.pixelStride == 1) {
  171 + "Pixel stride for Y plane must be 1 but got ${y.pixelStride} instead."
  172 + }
  173 + require(u.pixelStride == v.pixelStride && u.rowStride == v.rowStride) {
  174 + "U and V planes must have the same pixel and row strides " +
  175 + "but got pixel=${u.pixelStride} row=${u.rowStride} for U " +
  176 + "and pixel=${v.pixelStride} and row=${v.rowStride} for V"
  177 + }
  178 + require(u.pixelStride == 1 || u.pixelStride == 2) {
  179 + "Supported" + " pixel strides for U and V planes are 1 and 2"
  180 + }
  181 + }
  182 + }
  183 +
  184 + private class PlaneWrapper(width: Int, height: Int, plane: Image.Plane) {
  185 + val width = width
  186 + val height = height
  187 + val buffer: ByteBuffer = plane.buffer
  188 + val rowStride = plane.rowStride
  189 + val pixelStride = plane.pixelStride
  190 + }
  191 +}
  1 +package dev.steenbakker.mobile_scanner.utils
  2 +
  3 +import android.content.Context
  4 +import android.graphics.Bitmap
  5 +import android.graphics.ImageFormat
  6 +import android.media.Image
  7 +import android.os.Build
  8 +import android.renderscript.Allocation
  9 +import android.renderscript.Element
  10 +import android.renderscript.RenderScript
  11 +import android.renderscript.ScriptIntrinsicYuvToRGB
  12 +import android.renderscript.Type
  13 +import androidx.annotation.RequiresApi
  14 +import java.nio.ByteBuffer
  15 +
  16 +
  17 +/**
  18 + * Helper class used to efficiently convert a [Media.Image] object from
  19 + * YUV_420_888 format to an RGB [Bitmap] object.
  20 + *
  21 + * Copied from https://github.com/owahltinez/camerax-tflite/blob/master/app/src/main/java/com/android/example/camerax/tflite/YuvToRgbConverter.kt
  22 + *
  23 + * The [yuvToRgb] method is able to achieve the same FPS as the CameraX image
  24 + * analysis use case at the default analyzer resolution, which is 30 FPS with
  25 + * 640x480 on a Pixel 3 XL device.
  26 + */class YuvToRgbConverter(context: Context) {
  27 + private val rs = RenderScript.create(context)
  28 + private val scriptYuvToRgb =
  29 + ScriptIntrinsicYuvToRGB.create(rs, Element.U8_4(rs))
  30 +
  31 + // Do not add getters/setters functions to these private variables
  32 + // because yuvToRgb() assume they won't be modified elsewhere
  33 + private var yuvBits: ByteBuffer? = null
  34 + private var bytes: ByteArray = ByteArray(0)
  35 + private var inputAllocation: Allocation? = null
  36 + private var outputAllocation: Allocation? = null
  37 +
  38 + @Synchronized
  39 + fun yuvToRgb(image: Image, output: Bitmap) {
  40 + val yuvBuffer = YuvByteBuffer(image, yuvBits)
  41 + yuvBits = yuvBuffer.buffer
  42 +
  43 + if (needCreateAllocations(image, yuvBuffer)) {
  44 + val yuvType = Type.Builder(rs, Element.U8(rs))
  45 + .setX(image.width)
  46 + .setY(image.height)
  47 + .setYuvFormat(yuvBuffer.type)
  48 + inputAllocation = Allocation.createTyped(
  49 + rs,
  50 + yuvType.create(),
  51 + Allocation.USAGE_SCRIPT
  52 + )
  53 + bytes = ByteArray(yuvBuffer.buffer.capacity())
  54 + val rgbaType = Type.Builder(rs, Element.RGBA_8888(rs))
  55 + .setX(image.width)
  56 + .setY(image.height)
  57 + outputAllocation = Allocation.createTyped(
  58 + rs,
  59 + rgbaType.create(),
  60 + Allocation.USAGE_SCRIPT
  61 + )
  62 + }
  63 +
  64 + yuvBuffer.buffer.get(bytes)
  65 + inputAllocation!!.copyFrom(bytes)
  66 +
  67 + // Convert NV21 or YUV_420_888 format to RGB
  68 + inputAllocation!!.copyFrom(bytes)
  69 + scriptYuvToRgb.setInput(inputAllocation)
  70 + scriptYuvToRgb.forEach(outputAllocation)
  71 + outputAllocation!!.copyTo(output)
  72 + }
  73 +
  74 + private fun needCreateAllocations(image: Image, yuvBuffer: YuvByteBuffer): Boolean {
  75 + return (inputAllocation == null || // the very 1st call
  76 + inputAllocation!!.type.x != image.width || // image size changed
  77 + inputAllocation!!.type.y != image.height ||
  78 + inputAllocation!!.type.yuv != yuvBuffer.type || // image format changed
  79 + bytes.size == yuvBuffer.buffer.capacity())
  80 + }
  81 +}