- fix android image result is not correct orientation and format
Showing
3 changed files
with
314 additions
and
4 deletions
| 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,15 +87,42 @@ class MobileScanner( | @@ -82,15 +87,42 @@ class MobileScanner( | ||
| 82 | } | 87 | } |
| 83 | } | 88 | } |
| 84 | 89 | ||
| 90 | + | ||
| 85 | if (barcodeMap.isNotEmpty()) { | 91 | if (barcodeMap.isNotEmpty()) { |
| 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 | + | ||
| 86 | mobileScannerCallback( | 117 | mobileScannerCallback( |
| 87 | barcodeMap, | 118 | barcodeMap, |
| 88 | - if (returnImage) mediaImage.toByteArray() else null, | ||
| 89 | - if (returnImage) mediaImage.width else null, | ||
| 90 | - if (returnImage) mediaImage.height else null | 119 | + null, |
| 120 | + null, | ||
| 121 | + null | ||
| 91 | ) | 122 | ) |
| 92 | } | 123 | } |
| 93 | } | 124 | } |
| 125 | + } | ||
| 94 | .addOnFailureListener { e -> | 126 | .addOnFailureListener { e -> |
| 95 | mobileScannerErrorCallback( | 127 | mobileScannerErrorCallback( |
| 96 | e.localizedMessage ?: e.toString() | 128 | e.localizedMessage ?: e.toString() |
| @@ -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 | +} |
-
Please register or login to post a comment