From 8464848c9d7e910fb328a4590817564a2eb69af5 Mon Sep 17 00:00:00 2001 From: Benson Arafat Date: Fri, 27 Feb 2026 09:02:54 +0100 Subject: [PATCH 01/21] refactor: Enhance image format validation and optimize InputImage conversion --- .../vision_detector_views/camera_view.dart | 46 ++++++++++++++----- .../BarcodeScanner.kt | 6 +-- .../google_mlkit_commons/android/build.gradle | 1 + .../InputImageConverter.kt | 10 +++- 4 files changed, 48 insertions(+), 15 deletions(-) diff --git a/packages/example/lib/vision_detector_views/camera_view.dart b/packages/example/lib/vision_detector_views/camera_view.dart index 051c1e1c..39267bfe 100644 --- a/packages/example/lib/vision_detector_views/camera_view.dart +++ b/packages/example/lib/vision_detector_views/camera_view.dart @@ -357,29 +357,53 @@ class _CameraViewState extends State { // get image format final format = InputImageFormatValue.fromRawValue(image.format.raw); - // validate format depending on platform - // only supported formats: - // * nv21 for Android - // * bgra8888 for iOS - if (format == null || - (Platform.isAndroid && format != InputImageFormat.nv21) || + if (format == null) { + print('could not find format from raw value: ${image.format.raw}'); + return null; + } + // Validate format depending on plaform + final androidSupportedFormats = [ + InputImageFormat.nv21, + InputImageFormat.yv12, + InputImageFormat.yuv_420_888 + ]; + + if ((Platform.isAndroid && !androidSupportedFormats.contains(format)) || (Platform.isIOS && format != InputImageFormat.bgra8888)) { + print('image format is not supported: $format'); return null; } - // since format is constraint to nv21 or bgra8888, both only have one plane - if (image.planes.length != 1) return null; - final plane = image.planes.first; + // Compile a flat list of all image data. For image formats with multiple planes, + // Takes some copying. + final Uint8List bytes = image.planes.length == 1 + ? image.planes.first.bytes + : _concatenatePlanes(image); // compose InputImage using bytes return InputImage.fromBytes( - bytes: plane.bytes, + bytes: bytes, metadata: InputImageMetadata( size: Size(image.width.toDouble(), image.height.toDouble()), rotation: rotation, // used only in Android format: format, // used only in iOS - bytesPerRow: plane.bytesPerRow, // used only in iOS + bytesPerRow: image.planes.first.bytesPerRow, // used only in iOS ), ); } + + Uint8List _concatenatePlanes(CameraImage image) { + int length = 0; + for (final Plane p in image.planes) { + length += p.bytes.length; + } + + final Uint8List bytes = Uint8List(length); + int offset = 0; + for (final Plane p in image.planes) { + bytes.setRange(offset, offset + p.bytes.length, p.bytes); + offset += p.bytes.length; + } + return bytes; + } } diff --git a/packages/google_mlkit_barcode_scanning/android/src/main/kotlin/com/google_mlkit_barcode_scanning/BarcodeScanner.kt b/packages/google_mlkit_barcode_scanning/android/src/main/kotlin/com/google_mlkit_barcode_scanning/BarcodeScanner.kt index 85f72f58..f556a403 100644 --- a/packages/google_mlkit_barcode_scanning/android/src/main/kotlin/com/google_mlkit_barcode_scanning/BarcodeScanner.kt +++ b/packages/google_mlkit_barcode_scanning/android/src/main/kotlin/com/google_mlkit_barcode_scanning/BarcodeScanner.kt @@ -68,8 +68,8 @@ class BarcodeScanner( result.error("BarcodeDetectorError", "imageData is null", null) return } - - val inputImage = InputImageConverter.getInputImageFromData(imageData, context, result) ?: return + val converter = InputImageConverter() + val inputImage = converter.getInputImageFromData(imageData, context, result) ?: return val id = call.argument("id")!! val scanner = instances.getOrPut(id) { initialize(call) } @@ -213,7 +213,7 @@ class BarcodeScanner( result.success(barcodeList) }.addOnFailureListener { e -> result.error("BarcodeDetectorError", e.toString(), null) - } + }.addOnCompleteListener { converter.close() } } private fun getPoints(cornerPoints: Array) = diff --git a/packages/google_mlkit_commons/android/build.gradle b/packages/google_mlkit_commons/android/build.gradle index 845030c1..725816b7 100644 --- a/packages/google_mlkit_commons/android/build.gradle +++ b/packages/google_mlkit_commons/android/build.gradle @@ -48,5 +48,6 @@ android { dependencies { implementation("com.google.mlkit:vision-common:17.3.0") + implementation files('/Users/bensonarafat/development/flutter/bin/cache/artifacts/engine/android-arm64-release/flutter.jar') } } diff --git a/packages/google_mlkit_commons/android/src/main/kotlin/com/google_mlkit_commons/InputImageConverter.kt b/packages/google_mlkit_commons/android/src/main/kotlin/com/google_mlkit_commons/InputImageConverter.kt index f1d63b33..527a1e82 100644 --- a/packages/google_mlkit_commons/android/src/main/kotlin/com/google_mlkit_commons/InputImageConverter.kt +++ b/packages/google_mlkit_commons/android/src/main/kotlin/com/google_mlkit_commons/InputImageConverter.kt @@ -2,6 +2,7 @@ package com.google_mlkit_commons import android.content.Context import android.graphics.ImageFormat +import android.media.ImageWriter import android.net.Uri import android.util.Log import com.google.mlkit.vision.common.InputImage @@ -10,7 +11,10 @@ import java.io.File import java.io.IOException import java.nio.IntBuffer -object InputImageConverter { +object InputImageConverter : AutoCloseable { + + lateinit var imageWriter: ImageWriter + // Returns an [InputImage] from the image data received @JvmStatic fun getInputImageFromData( @@ -177,4 +181,8 @@ object InputImageConverter { result.error("InputImageConverterError", e.toString(), e) null } + + override fun close() { + imageWriter.close() + } } From 7b6cd8aae78e1d33fe30ce938539743dae3948ff Mon Sep 17 00:00:00 2001 From: Benson Arafat Date: Fri, 27 Feb 2026 12:57:06 +0100 Subject: [PATCH 02/21] refactor: Replace direct InputImageConverter calls with instance methods for better resource management --- .../google_mlkit_commons/android/build.gradle | 1 - .../InputImageConverter.kt | 77 +++++++++++++++---- .../FaceDetector.kt | 5 +- .../FaceMeshDetector.kt | 6 +- .../PoseDetector.kt | 6 +- .../SelfieSegmenter.kt | 5 +- .../SubjectSegmenter.kt | 4 +- .../TextRecognizer.kt | 5 +- 8 files changed, 79 insertions(+), 30 deletions(-) diff --git a/packages/google_mlkit_commons/android/build.gradle b/packages/google_mlkit_commons/android/build.gradle index 725816b7..845030c1 100644 --- a/packages/google_mlkit_commons/android/build.gradle +++ b/packages/google_mlkit_commons/android/build.gradle @@ -48,6 +48,5 @@ android { dependencies { implementation("com.google.mlkit:vision-common:17.3.0") - implementation files('/Users/bensonarafat/development/flutter/bin/cache/artifacts/engine/android-arm64-release/flutter.jar') } } diff --git a/packages/google_mlkit_commons/android/src/main/kotlin/com/google_mlkit_commons/InputImageConverter.kt b/packages/google_mlkit_commons/android/src/main/kotlin/com/google_mlkit_commons/InputImageConverter.kt index 527a1e82..05f47d78 100644 --- a/packages/google_mlkit_commons/android/src/main/kotlin/com/google_mlkit_commons/InputImageConverter.kt +++ b/packages/google_mlkit_commons/android/src/main/kotlin/com/google_mlkit_commons/InputImageConverter.kt @@ -2,18 +2,23 @@ package com.google_mlkit_commons import android.content.Context import android.graphics.ImageFormat +import android.graphics.SurfaceTexture +import android.media.Image import android.media.ImageWriter import android.net.Uri +import android.os.Build import android.util.Log +import android.view.Surface +import androidx.annotation.RequiresApi import com.google.mlkit.vision.common.InputImage import io.flutter.plugin.common.MethodChannel import java.io.File import java.io.IOException +import java.nio.ByteBuffer import java.nio.IntBuffer object InputImageConverter : AutoCloseable { - - lateinit var imageWriter: ImageWriter + private var writer: ImageWriter? = null // Returns an [InputImage] from the image data received @JvmStatic @@ -144,6 +149,7 @@ object InputImageConverter : AutoCloseable { return InputImage.fromBitmap(bitmap, rotation) } + @RequiresApi(Build.VERSION_CODES.TIRAMISU) private fun handleBytesImage( imageData: Map, result: MethodChannel.Result, @@ -161,19 +167,57 @@ object InputImageConverter : AutoCloseable { val height = metaData["height"]?.toString()?.toDouble()?.toInt() ?: throw IllegalArgumentException("Height is null") - if ( - imageFormat == ImageFormat.NV21 || - imageFormat == ImageFormat.YUV_420_888 || - imageFormat == ImageFormat.YV12 - ) { - InputImage.fromByteArray(data, width, height, rotationDegrees, imageFormat) - } else { - result.error( - "InputImageConverterError", - "ImageFormat is not supported. Supported: NV21(17), YUV_420_888(35), YV12. Got: $imageFormat", - null, - ) - null + when (imageFormat) { + ImageFormat.NV21, ImageFormat.YV12 -> { + InputImage.fromByteArray(data, width, height, rotationDegrees, imageFormat) + } + + ImageFormat.YUV_420_888 -> { + // fromByteArray does NOT support YUV_420_**; must be fromMediaImage + writer = + ImageWriter + .Builder(Surface(SurfaceTexture(true))) + .setWidthAndHeight(width, height) + .setImageFormat(imageFormat) + .build() + + val image: Image = + writer!!.dequeueInputImage() + ?: run { + result.error( + "InputImageConverterError", + "failed to allocate space for input image", + null, + ) + return null + } + val planes = image.planes + + // Y plan + val yBuffer: ByteBuffer = planes[0].buffer + yBuffer.put(data, 0, width * height) + + // U plan + val uBuffer: ByteBuffer = planes[1].buffer + val uOffset = width * height + uBuffer.put(data, uOffset, (width * height) / 4) + + // V Plan + val vBuffer: ByteBuffer = planes[2].buffer + val vOffset = uOffset + (width * height) / 4 + vBuffer.put(data, vOffset, (width * height) / 4) + + InputImage.fromMediaImage(image, rotationDegrees) + } + + else -> { + result.error( + "InputImageConverterError", + "ImageFormat is not supported.", + null, + ) + null + } } } catch (e: Exception) { Log.e("ImageError", "Getting Image failed") @@ -182,7 +226,8 @@ object InputImageConverter : AutoCloseable { null } + @RequiresApi(Build.VERSION_CODES.M) override fun close() { - imageWriter.close() + writer?.close() } } diff --git a/packages/google_mlkit_face_detection/android/src/main/kotlin/com/google_mlkit_face_detection/FaceDetector.kt b/packages/google_mlkit_face_detection/android/src/main/kotlin/com/google_mlkit_face_detection/FaceDetector.kt index 3dba9480..00c60195 100644 --- a/packages/google_mlkit_face_detection/android/src/main/kotlin/com/google_mlkit_face_detection/FaceDetector.kt +++ b/packages/google_mlkit_face_detection/android/src/main/kotlin/com/google_mlkit_face_detection/FaceDetector.kt @@ -49,7 +49,8 @@ class FaceDetector( result.error("FaceDetectorError", "imageData is null", null) return } - val inputImage = InputImageConverter.getInputImageFromData(imageData, context, result) ?: return + val converter = InputImageConverter() + val inputImage = converter.getInputImageFromData(imageData, context, result) ?: return val id = call.argument("id")!! val detector = @@ -90,7 +91,7 @@ class FaceDetector( result.success(faces) }.addOnFailureListener { e -> result.error("FaceDetectorError", e.toString(), null) - } + }.addOnCompleteListener { converter.close() } } private fun parseOptions(options: Map): FaceDetectorOptions { diff --git a/packages/google_mlkit_face_mesh_detection/android/src/main/kotlin/com/google_mlkit_face_mesh_detection/FaceMeshDetector.kt b/packages/google_mlkit_face_mesh_detection/android/src/main/kotlin/com/google_mlkit_face_mesh_detection/FaceMeshDetector.kt index 9fba504b..5c54c1d9 100644 --- a/packages/google_mlkit_face_mesh_detection/android/src/main/kotlin/com/google_mlkit_face_mesh_detection/FaceMeshDetector.kt +++ b/packages/google_mlkit_face_mesh_detection/android/src/main/kotlin/com/google_mlkit_face_mesh_detection/FaceMeshDetector.kt @@ -48,8 +48,8 @@ class FaceMeshDetector( result.error("FaceMeshDetectorError", "imageData is null", null) return } - - val inputImage = InputImageConverter.getInputImageFromData(imageData, context, result) ?: return + val converter = InputImageConverter() + val inputImage = converter.getInputImageFromData(imageData, context, result) ?: return val id = call.argument("id")!! var detector = instances[id] @@ -128,7 +128,7 @@ class FaceMeshDetector( result.success(faceMeshes) }.addOnFailureListener { e -> result.error("FaceMeshDetectorError", e.toString(), null) - } + }.addOnCompleteListener { converter.close() } } private fun pointsToList(points: List): List> = points.map { pointToMap(it) } diff --git a/packages/google_mlkit_pose_detection/android/src/main/kotlin/com/google_mlkit_pose_detection/PoseDetector.kt b/packages/google_mlkit_pose_detection/android/src/main/kotlin/com/google_mlkit_pose_detection/PoseDetector.kt index 7539bc06..31426613 100644 --- a/packages/google_mlkit_pose_detection/android/src/main/kotlin/com/google_mlkit_pose_detection/PoseDetector.kt +++ b/packages/google_mlkit_pose_detection/android/src/main/kotlin/com/google_mlkit_pose_detection/PoseDetector.kt @@ -48,8 +48,8 @@ class PoseDetector( result.error("PoseDetectorError", "imageData is null", null) return } - - val inputImage = InputImageConverter.getInputImageFromData(imageData, context, result) ?: return + val converter = InputImageConverter() + val inputImage = converter.getInputImageFromData(imageData, context, result) ?: return val id = call.argument("id") ?: return var poseDetector = instances[id] @@ -107,7 +107,7 @@ class PoseDetector( result.success(array) }.addOnFailureListener { e -> result.error("PoseDetectorError", e.toString(), null) - } + }.addOnCompleteListener { converter.close() } } private fun closeDetector(call: MethodCall) { diff --git a/packages/google_mlkit_selfie_segmentation/android/src/main/kotlin/com/google_mlkit_selfie_segmentation/SelfieSegmenter.kt b/packages/google_mlkit_selfie_segmentation/android/src/main/kotlin/com/google_mlkit_selfie_segmentation/SelfieSegmenter.kt index 87275033..e90c8128 100644 --- a/packages/google_mlkit_selfie_segmentation/android/src/main/kotlin/com/google_mlkit_selfie_segmentation/SelfieSegmenter.kt +++ b/packages/google_mlkit_selfie_segmentation/android/src/main/kotlin/com/google_mlkit_selfie_segmentation/SelfieSegmenter.kt @@ -66,7 +66,8 @@ class SelfieSegmenter( result.error("SelfieSegmenterError", "imageData is null", null) return } - val inputImage = InputImageConverter.getInputImageFromData(imageData, context, result) ?: return + val converter = InputImageConverter() + val inputImage = converter.getInputImageFromData(imageData, context, result) ?: return val id = call.argument("id") ?: return val segmenter = instances.getOrPut(id) { initialize(call) } @@ -94,7 +95,7 @@ class SelfieSegmenter( ) }.addOnFailureListener { e -> result.error("Selfie segmentation failed!", e.message, e) - } + }.addOnCompleteListener { converter.close() } } private fun closeDetector(call: MethodCall) { diff --git a/packages/google_mlkit_subject_segmentation/android/src/main/kotlin/com/google_mlkit_subject_segmentation/SubjectSegmenter.kt b/packages/google_mlkit_subject_segmentation/android/src/main/kotlin/com/google_mlkit_subject_segmentation/SubjectSegmenter.kt index 948b7eef..68d0b3fa 100644 --- a/packages/google_mlkit_subject_segmentation/android/src/main/kotlin/com/google_mlkit_subject_segmentation/SubjectSegmenter.kt +++ b/packages/google_mlkit_subject_segmentation/android/src/main/kotlin/com/google_mlkit_subject_segmentation/SubjectSegmenter.kt @@ -74,7 +74,8 @@ class SubjectSegmenter( result.error("SubjectSegmenterError", "imageData is null", null) return } - val inputImage = InputImageConverter.getInputImageFromData(imageData, context, result) ?: return + val converter = InputImageConverter() + val inputImage = converter.getInputImageFromData(imageData, context, result) ?: return val id = call.argument("id") ?: return val segmenter = instances.getOrPut(id) { initialize(call) } @@ -83,6 +84,7 @@ class SubjectSegmenter( .process(inputImage) .addOnSuccessListener { processResult(it, result) } .addOnFailureListener { e -> result.error("Subject segmentation failure!", e.message, e) } + .addOnCompleteListener { converter.close() } } private fun initialize(call: MethodCall): com.google.mlkit.vision.segmentation.subject.SubjectSegmenter { diff --git a/packages/google_mlkit_text_recognition/android/src/main/kotlin/com/google_mlkit_text_recognition/TextRecognizer.kt b/packages/google_mlkit_text_recognition/android/src/main/kotlin/com/google_mlkit_text_recognition/TextRecognizer.kt index 6de10613..423a4ab2 100644 --- a/packages/google_mlkit_text_recognition/android/src/main/kotlin/com/google_mlkit_text_recognition/TextRecognizer.kt +++ b/packages/google_mlkit_text_recognition/android/src/main/kotlin/com/google_mlkit_text_recognition/TextRecognizer.kt @@ -58,7 +58,8 @@ class TextRecognizer( result: MethodChannel.Result, ) { val imageData = call.argument>("imageData") ?: return - val inputImage = InputImageConverter.getInputImageFromData(imageData, context, result) ?: return + val converter = InputImageConverter() + val inputImage = converter.getInputImageFromData(imageData, context, result) ?: return val id = call.argument("id") ?: return val textRecognizer = @@ -133,7 +134,7 @@ class TextRecognizer( result.success(textResult) }.addOnFailureListener { e -> result.error("TextRecognizerError", e.toString(), null) - } + }.addOnCompleteListener { converter.close() } } private fun addData( From 5a07f2c3c8bb203b29a1ea411e1d244944148f93 Mon Sep 17 00:00:00 2001 From: Benson Arafat Date: Fri, 27 Feb 2026 22:34:42 +0100 Subject: [PATCH 03/21] refactor: Simplify InputImageConverter usage in pose, face, text, barcode, selfie segmentation, and subject segmentation classes --- .../vision_detector_views/camera_view.dart | 24 +++---- .../BarcodeScanner.kt | 5 +- .../InputImageConverter.kt | 62 ++++--------------- .../FaceDetector.kt | 5 +- .../FaceMeshDetector.kt | 5 +- .../PoseDetector.kt | 5 +- .../SelfieSegmenter.kt | 5 +- .../SubjectSegmenter.kt | 4 +- .../TextRecognizer.kt | 5 +- 9 files changed, 38 insertions(+), 82 deletions(-) diff --git a/packages/example/lib/vision_detector_views/camera_view.dart b/packages/example/lib/vision_detector_views/camera_view.dart index 39267bfe..76c2ea1e 100644 --- a/packages/example/lib/vision_detector_views/camera_view.dart +++ b/packages/example/lib/vision_detector_views/camera_view.dart @@ -361,7 +361,7 @@ class _CameraViewState extends State { print('could not find format from raw value: ${image.format.raw}'); return null; } - // Validate format depending on plaform + // Validate format depending on platform final androidSupportedFormats = [ InputImageFormat.nv21, InputImageFormat.yv12, @@ -393,17 +393,19 @@ class _CameraViewState extends State { } Uint8List _concatenatePlanes(CameraImage image) { - int length = 0; - for (final Plane p in image.planes) { - length += p.bytes.length; - } + final WriteBuffer buffer = WriteBuffer(); + for (final Plane plane in image.planes) { + final int rowStride = plane.bytesPerRow; + final int pixelStride = plane.bytesPerPixel ?? 1; + final int width = image.width; + final int height = (plane.bytes.length / rowStride).floor(); - final Uint8List bytes = Uint8List(length); - int offset = 0; - for (final Plane p in image.planes) { - bytes.setRange(offset, offset + p.bytes.length, p.bytes); - offset += p.bytes.length; + for (int row = 0; row < height; row++) { + for (int col = 0; col < width; col++) { + buffer.putUint8(plane.bytes[row * rowStride + col * pixelStride]); + } + } } - return bytes; + return buffer.done().buffer.asUint8List(); } } diff --git a/packages/google_mlkit_barcode_scanning/android/src/main/kotlin/com/google_mlkit_barcode_scanning/BarcodeScanner.kt b/packages/google_mlkit_barcode_scanning/android/src/main/kotlin/com/google_mlkit_barcode_scanning/BarcodeScanner.kt index f556a403..9092940d 100644 --- a/packages/google_mlkit_barcode_scanning/android/src/main/kotlin/com/google_mlkit_barcode_scanning/BarcodeScanner.kt +++ b/packages/google_mlkit_barcode_scanning/android/src/main/kotlin/com/google_mlkit_barcode_scanning/BarcodeScanner.kt @@ -68,8 +68,7 @@ class BarcodeScanner( result.error("BarcodeDetectorError", "imageData is null", null) return } - val converter = InputImageConverter() - val inputImage = converter.getInputImageFromData(imageData, context, result) ?: return + val inputImage = InputImageConverter.getInputImageFromData(imageData, context, result) ?: return val id = call.argument("id")!! val scanner = instances.getOrPut(id) { initialize(call) } @@ -213,7 +212,7 @@ class BarcodeScanner( result.success(barcodeList) }.addOnFailureListener { e -> result.error("BarcodeDetectorError", e.toString(), null) - }.addOnCompleteListener { converter.close() } + } } private fun getPoints(cornerPoints: Array) = diff --git a/packages/google_mlkit_commons/android/src/main/kotlin/com/google_mlkit_commons/InputImageConverter.kt b/packages/google_mlkit_commons/android/src/main/kotlin/com/google_mlkit_commons/InputImageConverter.kt index 05f47d78..e57177c8 100644 --- a/packages/google_mlkit_commons/android/src/main/kotlin/com/google_mlkit_commons/InputImageConverter.kt +++ b/packages/google_mlkit_commons/android/src/main/kotlin/com/google_mlkit_commons/InputImageConverter.kt @@ -2,14 +2,10 @@ package com.google_mlkit_commons import android.content.Context import android.graphics.ImageFormat -import android.graphics.SurfaceTexture import android.media.Image -import android.media.ImageWriter import android.net.Uri import android.os.Build import android.util.Log -import android.view.Surface -import androidx.annotation.RequiresApi import com.google.mlkit.vision.common.InputImage import io.flutter.plugin.common.MethodChannel import java.io.File @@ -17,9 +13,7 @@ import java.io.IOException import java.nio.ByteBuffer import java.nio.IntBuffer -object InputImageConverter : AutoCloseable { - private var writer: ImageWriter? = null - +object InputImageConverter { // Returns an [InputImage] from the image data received @JvmStatic fun getInputImageFromData( @@ -149,7 +143,6 @@ object InputImageConverter : AutoCloseable { return InputImage.fromBitmap(bitmap, rotation) } - @RequiresApi(Build.VERSION_CODES.TIRAMISU) private fun handleBytesImage( imageData: Map, result: MethodChannel.Result, @@ -173,47 +166,21 @@ object InputImageConverter : AutoCloseable { } ImageFormat.YUV_420_888 -> { - // fromByteArray does NOT support YUV_420_**; must be fromMediaImage - writer = - ImageWriter - .Builder(Surface(SurfaceTexture(true))) - .setWidthAndHeight(width, height) - .setImageFormat(imageFormat) - .build() - - val image: Image = - writer!!.dequeueInputImage() - ?: run { - result.error( - "InputImageConverterError", - "failed to allocate space for input image", - null, - ) - return null - } - val planes = image.planes - - // Y plan - val yBuffer: ByteBuffer = planes[0].buffer - yBuffer.put(data, 0, width * height) - - // U plan - val uBuffer: ByteBuffer = planes[1].buffer - val uOffset = width * height - uBuffer.put(data, uOffset, (width * height) / 4) - - // V Plan - val vBuffer: ByteBuffer = planes[2].buffer - val vOffset = uOffset + (width * height) / 4 - vBuffer.put(data, vOffset, (width * height) / 4) - - InputImage.fromMediaImage(image, rotationDegrees) + // Convert YUV_420_88 bytes to an InputImage using NV21-compatible format. + InputImage.fromByteArray( + data, + width, + height, + rotationDegrees, + ImageFormat.NV21, + ) } else -> { result.error( - "InputImageConverterError", - "ImageFormat is not supported.", + "InputImageConverterError.", + "ImageFormat $imageFormat is not supported. Supported formats are: " + + "${ImageFormat.NV21}, ${ImageFormat.YV12}, ${ImageFormat.YUV_420_888}.", null, ) null @@ -225,9 +192,4 @@ object InputImageConverter : AutoCloseable { result.error("InputImageConverterError", e.toString(), e) null } - - @RequiresApi(Build.VERSION_CODES.M) - override fun close() { - writer?.close() - } } diff --git a/packages/google_mlkit_face_detection/android/src/main/kotlin/com/google_mlkit_face_detection/FaceDetector.kt b/packages/google_mlkit_face_detection/android/src/main/kotlin/com/google_mlkit_face_detection/FaceDetector.kt index 00c60195..3dba9480 100644 --- a/packages/google_mlkit_face_detection/android/src/main/kotlin/com/google_mlkit_face_detection/FaceDetector.kt +++ b/packages/google_mlkit_face_detection/android/src/main/kotlin/com/google_mlkit_face_detection/FaceDetector.kt @@ -49,8 +49,7 @@ class FaceDetector( result.error("FaceDetectorError", "imageData is null", null) return } - val converter = InputImageConverter() - val inputImage = converter.getInputImageFromData(imageData, context, result) ?: return + val inputImage = InputImageConverter.getInputImageFromData(imageData, context, result) ?: return val id = call.argument("id")!! val detector = @@ -91,7 +90,7 @@ class FaceDetector( result.success(faces) }.addOnFailureListener { e -> result.error("FaceDetectorError", e.toString(), null) - }.addOnCompleteListener { converter.close() } + } } private fun parseOptions(options: Map): FaceDetectorOptions { diff --git a/packages/google_mlkit_face_mesh_detection/android/src/main/kotlin/com/google_mlkit_face_mesh_detection/FaceMeshDetector.kt b/packages/google_mlkit_face_mesh_detection/android/src/main/kotlin/com/google_mlkit_face_mesh_detection/FaceMeshDetector.kt index 5c54c1d9..1ec330ca 100644 --- a/packages/google_mlkit_face_mesh_detection/android/src/main/kotlin/com/google_mlkit_face_mesh_detection/FaceMeshDetector.kt +++ b/packages/google_mlkit_face_mesh_detection/android/src/main/kotlin/com/google_mlkit_face_mesh_detection/FaceMeshDetector.kt @@ -48,8 +48,7 @@ class FaceMeshDetector( result.error("FaceMeshDetectorError", "imageData is null", null) return } - val converter = InputImageConverter() - val inputImage = converter.getInputImageFromData(imageData, context, result) ?: return + val inputImage = InputImageConverter.getInputImageFromData(imageData, context, result) ?: return val id = call.argument("id")!! var detector = instances[id] @@ -128,7 +127,7 @@ class FaceMeshDetector( result.success(faceMeshes) }.addOnFailureListener { e -> result.error("FaceMeshDetectorError", e.toString(), null) - }.addOnCompleteListener { converter.close() } + } } private fun pointsToList(points: List): List> = points.map { pointToMap(it) } diff --git a/packages/google_mlkit_pose_detection/android/src/main/kotlin/com/google_mlkit_pose_detection/PoseDetector.kt b/packages/google_mlkit_pose_detection/android/src/main/kotlin/com/google_mlkit_pose_detection/PoseDetector.kt index 31426613..b5ff9e65 100644 --- a/packages/google_mlkit_pose_detection/android/src/main/kotlin/com/google_mlkit_pose_detection/PoseDetector.kt +++ b/packages/google_mlkit_pose_detection/android/src/main/kotlin/com/google_mlkit_pose_detection/PoseDetector.kt @@ -48,8 +48,7 @@ class PoseDetector( result.error("PoseDetectorError", "imageData is null", null) return } - val converter = InputImageConverter() - val inputImage = converter.getInputImageFromData(imageData, context, result) ?: return + val inputImage = InputImageConverter.getInputImageFromData(imageData, context, result) ?: return val id = call.argument("id") ?: return var poseDetector = instances[id] @@ -107,7 +106,7 @@ class PoseDetector( result.success(array) }.addOnFailureListener { e -> result.error("PoseDetectorError", e.toString(), null) - }.addOnCompleteListener { converter.close() } + } } private fun closeDetector(call: MethodCall) { diff --git a/packages/google_mlkit_selfie_segmentation/android/src/main/kotlin/com/google_mlkit_selfie_segmentation/SelfieSegmenter.kt b/packages/google_mlkit_selfie_segmentation/android/src/main/kotlin/com/google_mlkit_selfie_segmentation/SelfieSegmenter.kt index e90c8128..87275033 100644 --- a/packages/google_mlkit_selfie_segmentation/android/src/main/kotlin/com/google_mlkit_selfie_segmentation/SelfieSegmenter.kt +++ b/packages/google_mlkit_selfie_segmentation/android/src/main/kotlin/com/google_mlkit_selfie_segmentation/SelfieSegmenter.kt @@ -66,8 +66,7 @@ class SelfieSegmenter( result.error("SelfieSegmenterError", "imageData is null", null) return } - val converter = InputImageConverter() - val inputImage = converter.getInputImageFromData(imageData, context, result) ?: return + val inputImage = InputImageConverter.getInputImageFromData(imageData, context, result) ?: return val id = call.argument("id") ?: return val segmenter = instances.getOrPut(id) { initialize(call) } @@ -95,7 +94,7 @@ class SelfieSegmenter( ) }.addOnFailureListener { e -> result.error("Selfie segmentation failed!", e.message, e) - }.addOnCompleteListener { converter.close() } + } } private fun closeDetector(call: MethodCall) { diff --git a/packages/google_mlkit_subject_segmentation/android/src/main/kotlin/com/google_mlkit_subject_segmentation/SubjectSegmenter.kt b/packages/google_mlkit_subject_segmentation/android/src/main/kotlin/com/google_mlkit_subject_segmentation/SubjectSegmenter.kt index 68d0b3fa..948b7eef 100644 --- a/packages/google_mlkit_subject_segmentation/android/src/main/kotlin/com/google_mlkit_subject_segmentation/SubjectSegmenter.kt +++ b/packages/google_mlkit_subject_segmentation/android/src/main/kotlin/com/google_mlkit_subject_segmentation/SubjectSegmenter.kt @@ -74,8 +74,7 @@ class SubjectSegmenter( result.error("SubjectSegmenterError", "imageData is null", null) return } - val converter = InputImageConverter() - val inputImage = converter.getInputImageFromData(imageData, context, result) ?: return + val inputImage = InputImageConverter.getInputImageFromData(imageData, context, result) ?: return val id = call.argument("id") ?: return val segmenter = instances.getOrPut(id) { initialize(call) } @@ -84,7 +83,6 @@ class SubjectSegmenter( .process(inputImage) .addOnSuccessListener { processResult(it, result) } .addOnFailureListener { e -> result.error("Subject segmentation failure!", e.message, e) } - .addOnCompleteListener { converter.close() } } private fun initialize(call: MethodCall): com.google.mlkit.vision.segmentation.subject.SubjectSegmenter { diff --git a/packages/google_mlkit_text_recognition/android/src/main/kotlin/com/google_mlkit_text_recognition/TextRecognizer.kt b/packages/google_mlkit_text_recognition/android/src/main/kotlin/com/google_mlkit_text_recognition/TextRecognizer.kt index 423a4ab2..6de10613 100644 --- a/packages/google_mlkit_text_recognition/android/src/main/kotlin/com/google_mlkit_text_recognition/TextRecognizer.kt +++ b/packages/google_mlkit_text_recognition/android/src/main/kotlin/com/google_mlkit_text_recognition/TextRecognizer.kt @@ -58,8 +58,7 @@ class TextRecognizer( result: MethodChannel.Result, ) { val imageData = call.argument>("imageData") ?: return - val converter = InputImageConverter() - val inputImage = converter.getInputImageFromData(imageData, context, result) ?: return + val inputImage = InputImageConverter.getInputImageFromData(imageData, context, result) ?: return val id = call.argument("id") ?: return val textRecognizer = @@ -134,7 +133,7 @@ class TextRecognizer( result.success(textResult) }.addOnFailureListener { e -> result.error("TextRecognizerError", e.toString(), null) - }.addOnCompleteListener { converter.close() } + } } private fun addData( From eca2ea29556a46f504c3349bb8ee1c028fb07a76 Mon Sep 17 00:00:00 2001 From: Benson Arafat Date: Fri, 27 Feb 2026 23:08:19 +0100 Subject: [PATCH 04/21] Update packages/google_mlkit_commons/android/src/main/kotlin/com/google_mlkit_commons/InputImageConverter.kt Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../main/kotlin/com/google_mlkit_commons/InputImageConverter.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/google_mlkit_commons/android/src/main/kotlin/com/google_mlkit_commons/InputImageConverter.kt b/packages/google_mlkit_commons/android/src/main/kotlin/com/google_mlkit_commons/InputImageConverter.kt index e57177c8..c4c4f43c 100644 --- a/packages/google_mlkit_commons/android/src/main/kotlin/com/google_mlkit_commons/InputImageConverter.kt +++ b/packages/google_mlkit_commons/android/src/main/kotlin/com/google_mlkit_commons/InputImageConverter.kt @@ -2,9 +2,7 @@ package com.google_mlkit_commons import android.content.Context import android.graphics.ImageFormat -import android.media.Image import android.net.Uri -import android.os.Build import android.util.Log import com.google.mlkit.vision.common.InputImage import io.flutter.plugin.common.MethodChannel From 2186da8bbcb1cfde5b6b83848d2a0c97687d8183 Mon Sep 17 00:00:00 2001 From: Benson Arafat Date: Fri, 27 Feb 2026 23:08:41 +0100 Subject: [PATCH 05/21] Update packages/google_mlkit_commons/android/src/main/kotlin/com/google_mlkit_commons/InputImageConverter.kt Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../main/kotlin/com/google_mlkit_commons/InputImageConverter.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/google_mlkit_commons/android/src/main/kotlin/com/google_mlkit_commons/InputImageConverter.kt b/packages/google_mlkit_commons/android/src/main/kotlin/com/google_mlkit_commons/InputImageConverter.kt index c4c4f43c..c9ee0095 100644 --- a/packages/google_mlkit_commons/android/src/main/kotlin/com/google_mlkit_commons/InputImageConverter.kt +++ b/packages/google_mlkit_commons/android/src/main/kotlin/com/google_mlkit_commons/InputImageConverter.kt @@ -9,7 +9,6 @@ import io.flutter.plugin.common.MethodChannel import java.io.File import java.io.IOException import java.nio.ByteBuffer -import java.nio.IntBuffer object InputImageConverter { // Returns an [InputImage] from the image data received From 2d9aee29f230351557fe7b68b2bf049d98e25ff0 Mon Sep 17 00:00:00 2001 From: Benson Arafat Date: Fri, 27 Feb 2026 23:09:01 +0100 Subject: [PATCH 06/21] Update packages/google_mlkit_commons/android/src/main/kotlin/com/google_mlkit_commons/InputImageConverter.kt Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../main/kotlin/com/google_mlkit_commons/InputImageConverter.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/google_mlkit_commons/android/src/main/kotlin/com/google_mlkit_commons/InputImageConverter.kt b/packages/google_mlkit_commons/android/src/main/kotlin/com/google_mlkit_commons/InputImageConverter.kt index c9ee0095..bbf50c8a 100644 --- a/packages/google_mlkit_commons/android/src/main/kotlin/com/google_mlkit_commons/InputImageConverter.kt +++ b/packages/google_mlkit_commons/android/src/main/kotlin/com/google_mlkit_commons/InputImageConverter.kt @@ -175,7 +175,7 @@ object InputImageConverter { else -> { result.error( - "InputImageConverterError.", + "InputImageConverterError", "ImageFormat $imageFormat is not supported. Supported formats are: " + "${ImageFormat.NV21}, ${ImageFormat.YV12}, ${ImageFormat.YUV_420_888}.", null, From ccc150afc2ea42a800bbd69ba62a29536a03982f Mon Sep 17 00:00:00 2001 From: Benson Arafat Date: Fri, 27 Feb 2026 23:19:51 +0100 Subject: [PATCH 07/21] refactor: Optimize image plane concatenation and update YUV_420_888 conversion to use reported format --- .../lib/vision_detector_views/camera_view.dart | 11 +---------- .../com/google_mlkit_commons/InputImageConverter.kt | 4 ++-- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/packages/example/lib/vision_detector_views/camera_view.dart b/packages/example/lib/vision_detector_views/camera_view.dart index 76c2ea1e..47a37767 100644 --- a/packages/example/lib/vision_detector_views/camera_view.dart +++ b/packages/example/lib/vision_detector_views/camera_view.dart @@ -395,16 +395,7 @@ class _CameraViewState extends State { Uint8List _concatenatePlanes(CameraImage image) { final WriteBuffer buffer = WriteBuffer(); for (final Plane plane in image.planes) { - final int rowStride = plane.bytesPerRow; - final int pixelStride = plane.bytesPerPixel ?? 1; - final int width = image.width; - final int height = (plane.bytes.length / rowStride).floor(); - - for (int row = 0; row < height; row++) { - for (int col = 0; col < width; col++) { - buffer.putUint8(plane.bytes[row * rowStride + col * pixelStride]); - } - } + buffer.putUint8List(plane.bytes); } return buffer.done().buffer.asUint8List(); } diff --git a/packages/google_mlkit_commons/android/src/main/kotlin/com/google_mlkit_commons/InputImageConverter.kt b/packages/google_mlkit_commons/android/src/main/kotlin/com/google_mlkit_commons/InputImageConverter.kt index bbf50c8a..329019ed 100644 --- a/packages/google_mlkit_commons/android/src/main/kotlin/com/google_mlkit_commons/InputImageConverter.kt +++ b/packages/google_mlkit_commons/android/src/main/kotlin/com/google_mlkit_commons/InputImageConverter.kt @@ -163,13 +163,13 @@ object InputImageConverter { } ImageFormat.YUV_420_888 -> { - // Convert YUV_420_88 bytes to an InputImage using NV21-compatible format. + // Convert YUV_420_888 bytes to an InputImage using the reported image format. InputImage.fromByteArray( data, width, height, rotationDegrees, - ImageFormat.NV21, + ImageFormat, ) } From 940ae04db6d98b9c2685e02110fea24b55d8a106 Mon Sep 17 00:00:00 2001 From: Benson Arafat Date: Fri, 27 Feb 2026 23:31:44 +0100 Subject: [PATCH 08/21] Update packages/google_mlkit_commons/android/src/main/kotlin/com/google_mlkit_commons/InputImageConverter.kt Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../main/kotlin/com/google_mlkit_commons/InputImageConverter.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/google_mlkit_commons/android/src/main/kotlin/com/google_mlkit_commons/InputImageConverter.kt b/packages/google_mlkit_commons/android/src/main/kotlin/com/google_mlkit_commons/InputImageConverter.kt index 329019ed..561bd1a9 100644 --- a/packages/google_mlkit_commons/android/src/main/kotlin/com/google_mlkit_commons/InputImageConverter.kt +++ b/packages/google_mlkit_commons/android/src/main/kotlin/com/google_mlkit_commons/InputImageConverter.kt @@ -169,7 +169,7 @@ object InputImageConverter { width, height, rotationDegrees, - ImageFormat, + imageFormat, ) } From 7c49386bbc5aa1d32a490358378e36ecb59a7359 Mon Sep 17 00:00:00 2001 From: Benson Arafat Date: Fri, 27 Feb 2026 23:38:21 +0100 Subject: [PATCH 09/21] refactor: Replace ByteBuffer with IntBuffer in InputImageConverter --- .../main/kotlin/com/google_mlkit_commons/InputImageConverter.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/google_mlkit_commons/android/src/main/kotlin/com/google_mlkit_commons/InputImageConverter.kt b/packages/google_mlkit_commons/android/src/main/kotlin/com/google_mlkit_commons/InputImageConverter.kt index 561bd1a9..03cf63a7 100644 --- a/packages/google_mlkit_commons/android/src/main/kotlin/com/google_mlkit_commons/InputImageConverter.kt +++ b/packages/google_mlkit_commons/android/src/main/kotlin/com/google_mlkit_commons/InputImageConverter.kt @@ -8,7 +8,7 @@ import com.google.mlkit.vision.common.InputImage import io.flutter.plugin.common.MethodChannel import java.io.File import java.io.IOException -import java.nio.ByteBuffer +import java.nio.IntBuffer object InputImageConverter { // Returns an [InputImage] from the image data received From 7b1065f23ababb1c594e8cea1c055c6a335df786 Mon Sep 17 00:00:00 2001 From: Benson Arafat Date: Fri, 27 Feb 2026 23:38:44 +0100 Subject: [PATCH 10/21] Update packages/example/lib/vision_detector_views/camera_view.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../vision_detector_views/camera_view.dart | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/packages/example/lib/vision_detector_views/camera_view.dart b/packages/example/lib/vision_detector_views/camera_view.dart index 47a37767..83a11a6a 100644 --- a/packages/example/lib/vision_detector_views/camera_view.dart +++ b/packages/example/lib/vision_detector_views/camera_view.dart @@ -392,11 +392,31 @@ class _CameraViewState extends State { ); } + // Reusable buffer to avoid per-frame allocations when concatenating planes. + Uint8List? _reusablePlaneBuffer; + Uint8List _concatenatePlanes(CameraImage image) { - final WriteBuffer buffer = WriteBuffer(); + // Calculate the total number of bytes across all planes. + final int totalBytes = image.planes.fold( + 0, + (int sum, Plane plane) => sum + plane.bytes.length, + ); + + // Ensure the reusable buffer is allocated and large enough. + var buffer = _reusablePlaneBuffer; + if (buffer == null || buffer.length < totalBytes) { + buffer = Uint8List(totalBytes); + _reusablePlaneBuffer = buffer; + } + + // Copy each plane's bytes into the reusable buffer. + var offset = 0; for (final Plane plane in image.planes) { - buffer.putUint8List(plane.bytes); + final bytes = plane.bytes; + buffer.setRange(offset, offset + bytes.length, bytes); + offset += bytes.length; } - return buffer.done().buffer.asUint8List(); + + return buffer; } } From e74326007e9d53cc96b6557dde4356467f3d9a64 Mon Sep 17 00:00:00 2001 From: Benson Arafat Date: Fri, 27 Feb 2026 23:55:52 +0100 Subject: [PATCH 11/21] Update packages/google_mlkit_commons/android/src/main/kotlin/com/google_mlkit_commons/InputImageConverter.kt Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../main/kotlin/com/google_mlkit_commons/InputImageConverter.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/google_mlkit_commons/android/src/main/kotlin/com/google_mlkit_commons/InputImageConverter.kt b/packages/google_mlkit_commons/android/src/main/kotlin/com/google_mlkit_commons/InputImageConverter.kt index 03cf63a7..15492eec 100644 --- a/packages/google_mlkit_commons/android/src/main/kotlin/com/google_mlkit_commons/InputImageConverter.kt +++ b/packages/google_mlkit_commons/android/src/main/kotlin/com/google_mlkit_commons/InputImageConverter.kt @@ -163,7 +163,7 @@ object InputImageConverter { } ImageFormat.YUV_420_888 -> { - // Convert YUV_420_888 bytes to an InputImage using the reported image format. + // Create an InputImage directly from a YUV_420_888 byte array using the reported image format. InputImage.fromByteArray( data, width, From 79eddc42bfcb81dc756dea5a9324ee3a929b3b74 Mon Sep 17 00:00:00 2001 From: Benson Arafat Date: Fri, 27 Feb 2026 23:58:28 +0100 Subject: [PATCH 12/21] Update packages/example/lib/vision_detector_views/camera_view.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/example/lib/vision_detector_views/camera_view.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/example/lib/vision_detector_views/camera_view.dart b/packages/example/lib/vision_detector_views/camera_view.dart index 83a11a6a..fcee2322 100644 --- a/packages/example/lib/vision_detector_views/camera_view.dart +++ b/packages/example/lib/vision_detector_views/camera_view.dart @@ -417,6 +417,7 @@ class _CameraViewState extends State { offset += bytes.length; } - return buffer; + // Return a view limited to the actual number of bytes written for this frame. + return Uint8List.sublistView(buffer, 0, totalBytes); } } From acaa0f6dc1b884ba72127f6f81c2fc5ad54e30f5 Mon Sep 17 00:00:00 2001 From: Benson Arafat Date: Sat, 28 Feb 2026 00:02:31 +0100 Subject: [PATCH 13/21] fix: Improve error message for unsupported image formats in InputImageConverter --- .../kotlin/com/google_mlkit_commons/InputImageConverter.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/google_mlkit_commons/android/src/main/kotlin/com/google_mlkit_commons/InputImageConverter.kt b/packages/google_mlkit_commons/android/src/main/kotlin/com/google_mlkit_commons/InputImageConverter.kt index 15492eec..cf938d09 100644 --- a/packages/google_mlkit_commons/android/src/main/kotlin/com/google_mlkit_commons/InputImageConverter.kt +++ b/packages/google_mlkit_commons/android/src/main/kotlin/com/google_mlkit_commons/InputImageConverter.kt @@ -177,7 +177,9 @@ object InputImageConverter { result.error( "InputImageConverterError", "ImageFormat $imageFormat is not supported. Supported formats are: " + - "${ImageFormat.NV21}, ${ImageFormat.YV12}, ${ImageFormat.YUV_420_888}.", + "NV21 (${ImageFormat.NV21}), " + + "YV12 (${ImageFormat.YV12}), " + + "YUV_420_888 (${ImageFormat.YUV_420_888}).", null, ) null From c8f789f897396b2d086dde32bf47c6b321db3cce Mon Sep 17 00:00:00 2001 From: Benson Arafat Date: Sat, 28 Feb 2026 00:15:09 +0100 Subject: [PATCH 14/21] Update packages/example/lib/vision_detector_views/camera_view.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/example/lib/vision_detector_views/camera_view.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/example/lib/vision_detector_views/camera_view.dart b/packages/example/lib/vision_detector_views/camera_view.dart index fcee2322..d95640bc 100644 --- a/packages/example/lib/vision_detector_views/camera_view.dart +++ b/packages/example/lib/vision_detector_views/camera_view.dart @@ -374,8 +374,8 @@ class _CameraViewState extends State { return null; } - // Compile a flat list of all image data. For image formats with multiple planes, - // Takes some copying. + // Compile a flat list of all image data. For image formats with multiple planes, + // this involves copying the plane bytes into a single buffer. final Uint8List bytes = image.planes.length == 1 ? image.planes.first.bytes : _concatenatePlanes(image); From 025f7b8286d5d354f76f0c8bfd48bf84e72721ad Mon Sep 17 00:00:00 2001 From: Benson Arafat Date: Sat, 28 Feb 2026 06:42:33 +0100 Subject: [PATCH 15/21] feat: Add support for converting YUV420 image formats to NV21 in CameraView --- .../vision_detector_views/camera_view.dart | 74 ++++++++++++++++--- 1 file changed, 65 insertions(+), 9 deletions(-) diff --git a/packages/example/lib/vision_detector_views/camera_view.dart b/packages/example/lib/vision_detector_views/camera_view.dart index d95640bc..2af71018 100644 --- a/packages/example/lib/vision_detector_views/camera_view.dart +++ b/packages/example/lib/vision_detector_views/camera_view.dart @@ -374,24 +374,80 @@ class _CameraViewState extends State { return null; } - // Compile a flat list of all image data. For image formats with multiple planes, - // this involves copying the plane bytes into a single buffer. - final Uint8List bytes = image.planes.length == 1 - ? image.planes.first.bytes - : _concatenatePlanes(image); + InputImageFormat resolvedFormat = format; + final Uint8List bytes; + + if (image.planes.length == 1) { + bytes = image.planes.first.bytes; + } else if (Platform.isAndroid && + (format == InputImageFormat.yuv_420_888 || + format == InputImageFormat.yv12) && + image.planes.length == 3) { + bytes = _convertYUV420ToNV21(image); + resolvedFormat = InputImageFormat.nv21; + } else { + bytes = _concatenatePlanes(image); + } - // compose InputImage using bytes return InputImage.fromBytes( bytes: bytes, metadata: InputImageMetadata( size: Size(image.width.toDouble(), image.height.toDouble()), - rotation: rotation, // used only in Android - format: format, // used only in iOS - bytesPerRow: image.planes.first.bytesPerRow, // used only in iOS + rotation: rotation, + format: resolvedFormat, + bytesPerRow: image.planes.first.bytesPerRow, ), ); } + Uint8List _convertYUV420ToNV21(CameraImage image) { + final int width = image.width; + final int height = image.height; + final int ySize = width * height; + final int uvSize = ySize ~/ 2; + final Uint8List nv21 = Uint8List(ySize + uvSize); + + // Copy Y (luma) plane, stripping row stride padding. + final Plane yPlane = image.planes[0]; + int destIndex = 0; + for (int row = 0; row < height; row++) { + final int srcRowStart = row * yPlane.bytesPerRow; + nv21.setRange(destIndex, destIndex + width, yPlane.bytes, srcRowStart); + destIndex += width; + } + + // Interleave V and U (chroma) planes into NV21 (VU) order, + // stripping row and pixel stride padding. + final Plane uPlane = image.planes[1]; + final Plane vPlane = image.planes[2]; + final int uvPixelStride = uPlane.bytesPerPixel ?? 1; + final int vPixelStride = vPlane.bytesPerPixel ?? 1; + + assert( + uvPixelStride == 1 || uvPixelStride == 2, + 'Unexpected U plane pixel stride: $uvPixelStride', + ); + assert( + vPixelStride == 1 || vPixelStride == 2, + 'Unexpected V plane pixel stride: $vPixelStride', + ); + + int uvIndex = ySize; + for (int row = 0; row < height ~/ 2; row++) { + final int uRowStart = row * uPlane.bytesPerRow; + final int vRowStart = row * vPlane.bytesPerRow; + for (int col = 0; col < width ~/ 2; col++) { + final int uIndex = uRowStart + col * uvPixelStride; + final int vIndex = vRowStart + col * vPixelStride; + // NV21 interleaves V then U. + nv21[uvIndex++] = vPlane.bytes[vIndex]; + nv21[uvIndex++] = uPlane.bytes[uIndex]; + } + } + + return nv21; + } + // Reusable buffer to avoid per-frame allocations when concatenating planes. Uint8List? _reusablePlaneBuffer; From 3f09e140d0198983bd38b9e6554a2ae8c3dcc358 Mon Sep 17 00:00:00 2001 From: Benson Arafat Date: Mon, 2 Mar 2026 19:00:59 +0100 Subject: [PATCH 16/21] refactor: Update CameraView to use const for supported formats and simplify buffer return --- .../lib/vision_detector_views/camera_view.dart | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/packages/example/lib/vision_detector_views/camera_view.dart b/packages/example/lib/vision_detector_views/camera_view.dart index 2af71018..e2a20a07 100644 --- a/packages/example/lib/vision_detector_views/camera_view.dart +++ b/packages/example/lib/vision_detector_views/camera_view.dart @@ -362,7 +362,7 @@ class _CameraViewState extends State { return null; } // Validate format depending on platform - final androidSupportedFormats = [ + const androidSupportedFormats = [ InputImageFormat.nv21, InputImageFormat.yv12, InputImageFormat.yuv_420_888 @@ -423,15 +423,6 @@ class _CameraViewState extends State { final int uvPixelStride = uPlane.bytesPerPixel ?? 1; final int vPixelStride = vPlane.bytesPerPixel ?? 1; - assert( - uvPixelStride == 1 || uvPixelStride == 2, - 'Unexpected U plane pixel stride: $uvPixelStride', - ); - assert( - vPixelStride == 1 || vPixelStride == 2, - 'Unexpected V plane pixel stride: $vPixelStride', - ); - int uvIndex = ySize; for (int row = 0; row < height ~/ 2; row++) { final int uRowStart = row * uPlane.bytesPerRow; @@ -473,7 +464,6 @@ class _CameraViewState extends State { offset += bytes.length; } - // Return a view limited to the actual number of bytes written for this frame. - return Uint8List.sublistView(buffer, 0, totalBytes); + return buffer.sublist(0, totalBytes); } } From ba129e764589bddedb7ccd9b277ed7ba0c755e13 Mon Sep 17 00:00:00 2001 From: Benson Arafat Date: Mon, 2 Mar 2026 19:03:41 +0100 Subject: [PATCH 17/21] fix: Add validation for U and V plane pixel strides in CameraView --- packages/example/lib/vision_detector_views/camera_view.dart | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/example/lib/vision_detector_views/camera_view.dart b/packages/example/lib/vision_detector_views/camera_view.dart index e2a20a07..d372ecc5 100644 --- a/packages/example/lib/vision_detector_views/camera_view.dart +++ b/packages/example/lib/vision_detector_views/camera_view.dart @@ -423,6 +423,12 @@ class _CameraViewState extends State { final int uvPixelStride = uPlane.bytesPerPixel ?? 1; final int vPixelStride = vPlane.bytesPerPixel ?? 1; + if (uvPixelStride != 1 && uvPixelStride != 2) { + throw StateError('Unexpected U plane pixel stride: $uvPixelStride'); + } + if (vPixelStride != 1 && vPixelStride != 2) { + throw StateError('Unexpected V plane pixel stride: $vPixelStride'); + } int uvIndex = ySize; for (int row = 0; row < height ~/ 2; row++) { final int uRowStart = row * uPlane.bytesPerRow; From 296f1a848d62cc5dabf6b05a31ca31ef5e9e77b8 Mon Sep 17 00:00:00 2001 From: Benson Arafat Date: Mon, 2 Mar 2026 19:26:31 +0100 Subject: [PATCH 18/21] Update packages/example/lib/vision_detector_views/camera_view.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/example/lib/vision_detector_views/camera_view.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/example/lib/vision_detector_views/camera_view.dart b/packages/example/lib/vision_detector_views/camera_view.dart index d372ecc5..84e87763 100644 --- a/packages/example/lib/vision_detector_views/camera_view.dart +++ b/packages/example/lib/vision_detector_views/camera_view.dart @@ -470,6 +470,10 @@ class _CameraViewState extends State { offset += bytes.length; } - return buffer.sublist(0, totalBytes); + // Return the reusable buffer directly when sizes match, or a zero-copy view otherwise. + if (totalBytes == buffer.length) { + return buffer; + } + return Uint8List.sublistView(buffer, 0, totalBytes); } } From c2c6a3320e16c9a85f537bd78b97f87b79f169ac Mon Sep 17 00:00:00 2001 From: Benson Arafat Date: Mon, 2 Mar 2026 19:36:01 +0100 Subject: [PATCH 19/21] feat: Optimize YUV420 to NV21 conversion with reusable buffers --- .../vision_detector_views/camera_view.dart | 89 ++++++++++--------- 1 file changed, 46 insertions(+), 43 deletions(-) diff --git a/packages/example/lib/vision_detector_views/camera_view.dart b/packages/example/lib/vision_detector_views/camera_view.dart index 84e87763..dd44226b 100644 --- a/packages/example/lib/vision_detector_views/camera_view.dart +++ b/packages/example/lib/vision_detector_views/camera_view.dart @@ -400,14 +400,55 @@ class _CameraViewState extends State { ); } + // Reusable buffer to avoid per-frame allocations when concatenating planes. + Uint8List? _reusablePlaneBuffer; + + Uint8List _concatenatePlanes(CameraImage image) { + // Calculate the total number of bytes across all planes. + final int totalBytes = image.planes.fold( + 0, + (int sum, Plane plane) => sum + plane.bytes.length, + ); + + // Ensure the reusable buffer is allocated and large enough. + var buffer = _reusablePlaneBuffer; + if (buffer == null || buffer.length < totalBytes) { + buffer = Uint8List(totalBytes); + _reusablePlaneBuffer = buffer; + } + + // Copy each plane's bytes into the reusable buffer. + var offset = 0; + for (final Plane plane in image.planes) { + final bytes = plane.bytes; + buffer.setRange(offset, offset + bytes.length, bytes); + offset += bytes.length; + } + + // Return the reusable buffer directly when sizes match, or a zero-copy view otherwise. + if (totalBytes == buffer.length) { + return buffer; + } + return Uint8List.sublistView(buffer, 0, totalBytes); + } + + Uint8List? _reusableNv21Buffer; + int _lastNv21Size = 0; Uint8List _convertYUV420ToNV21(CameraImage image) { final int width = image.width; final int height = image.height; final int ySize = width * height; final int uvSize = ySize ~/ 2; - final Uint8List nv21 = Uint8List(ySize + uvSize); + final int requiredSize = ySize + uvSize; - // Copy Y (luma) plane, stripping row stride padding. + if (_reusableNv21Buffer == null || _lastNv21Size != requiredSize) { + _reusableNv21Buffer = Uint8List(requiredSize); + _lastNv21Size = requiredSize; + } + + final Uint8List nv21 = _reusableNv21Buffer!; + + // Copy Y plane (strip row padding) final Plane yPlane = image.planes[0]; int destIndex = 0; for (int row = 0; row < height; row++) { @@ -416,27 +457,21 @@ class _CameraViewState extends State { destIndex += width; } - // Interleave V and U (chroma) planes into NV21 (VU) order, - // stripping row and pixel stride padding. + // Interleave V and U planes into NV21 (VU order) final Plane uPlane = image.planes[1]; final Plane vPlane = image.planes[2]; final int uvPixelStride = uPlane.bytesPerPixel ?? 1; final int vPixelStride = vPlane.bytesPerPixel ?? 1; - if (uvPixelStride != 1 && uvPixelStride != 2) { - throw StateError('Unexpected U plane pixel stride: $uvPixelStride'); - } - if (vPixelStride != 1 && vPixelStride != 2) { - throw StateError('Unexpected V plane pixel stride: $vPixelStride'); - } int uvIndex = ySize; for (int row = 0; row < height ~/ 2; row++) { final int uRowStart = row * uPlane.bytesPerRow; final int vRowStart = row * vPlane.bytesPerRow; + for (int col = 0; col < width ~/ 2; col++) { final int uIndex = uRowStart + col * uvPixelStride; final int vIndex = vRowStart + col * vPixelStride; - // NV21 interleaves V then U. + nv21[uvIndex++] = vPlane.bytes[vIndex]; nv21[uvIndex++] = uPlane.bytes[uIndex]; } @@ -444,36 +479,4 @@ class _CameraViewState extends State { return nv21; } - - // Reusable buffer to avoid per-frame allocations when concatenating planes. - Uint8List? _reusablePlaneBuffer; - - Uint8List _concatenatePlanes(CameraImage image) { - // Calculate the total number of bytes across all planes. - final int totalBytes = image.planes.fold( - 0, - (int sum, Plane plane) => sum + plane.bytes.length, - ); - - // Ensure the reusable buffer is allocated and large enough. - var buffer = _reusablePlaneBuffer; - if (buffer == null || buffer.length < totalBytes) { - buffer = Uint8List(totalBytes); - _reusablePlaneBuffer = buffer; - } - - // Copy each plane's bytes into the reusable buffer. - var offset = 0; - for (final Plane plane in image.planes) { - final bytes = plane.bytes; - buffer.setRange(offset, offset + bytes.length, bytes); - offset += bytes.length; - } - - // Return the reusable buffer directly when sizes match, or a zero-copy view otherwise. - if (totalBytes == buffer.length) { - return buffer; - } - return Uint8List.sublistView(buffer, 0, totalBytes); - } } From 0142ff5f62714a20bfd06b6e6b52cb83190fd97a Mon Sep 17 00:00:00 2001 From: Benson Arafat Date: Mon, 2 Mar 2026 20:06:25 +0100 Subject: [PATCH 20/21] fix: Add newline for improved readability in BarcodeScanner, FaceMeshDetector, and PoseDetector --- .../kotlin/com/google_mlkit_barcode_scanning/BarcodeScanner.kt | 1 + .../com/google_mlkit_face_mesh_detection/FaceMeshDetector.kt | 1 + .../main/kotlin/com/google_mlkit_pose_detection/PoseDetector.kt | 1 + 3 files changed, 3 insertions(+) diff --git a/packages/google_mlkit_barcode_scanning/android/src/main/kotlin/com/google_mlkit_barcode_scanning/BarcodeScanner.kt b/packages/google_mlkit_barcode_scanning/android/src/main/kotlin/com/google_mlkit_barcode_scanning/BarcodeScanner.kt index 9092940d..1179276c 100644 --- a/packages/google_mlkit_barcode_scanning/android/src/main/kotlin/com/google_mlkit_barcode_scanning/BarcodeScanner.kt +++ b/packages/google_mlkit_barcode_scanning/android/src/main/kotlin/com/google_mlkit_barcode_scanning/BarcodeScanner.kt @@ -68,6 +68,7 @@ class BarcodeScanner( result.error("BarcodeDetectorError", "imageData is null", null) return } + val inputImage = InputImageConverter.getInputImageFromData(imageData, context, result) ?: return val id = call.argument("id")!! diff --git a/packages/google_mlkit_face_mesh_detection/android/src/main/kotlin/com/google_mlkit_face_mesh_detection/FaceMeshDetector.kt b/packages/google_mlkit_face_mesh_detection/android/src/main/kotlin/com/google_mlkit_face_mesh_detection/FaceMeshDetector.kt index 1ec330ca..de58ff6e 100644 --- a/packages/google_mlkit_face_mesh_detection/android/src/main/kotlin/com/google_mlkit_face_mesh_detection/FaceMeshDetector.kt +++ b/packages/google_mlkit_face_mesh_detection/android/src/main/kotlin/com/google_mlkit_face_mesh_detection/FaceMeshDetector.kt @@ -48,6 +48,7 @@ class FaceMeshDetector( result.error("FaceMeshDetectorError", "imageData is null", null) return } + val inputImage = InputImageConverter.getInputImageFromData(imageData, context, result) ?: return val id = call.argument("id")!! diff --git a/packages/google_mlkit_pose_detection/android/src/main/kotlin/com/google_mlkit_pose_detection/PoseDetector.kt b/packages/google_mlkit_pose_detection/android/src/main/kotlin/com/google_mlkit_pose_detection/PoseDetector.kt index b5ff9e65..64b944a9 100644 --- a/packages/google_mlkit_pose_detection/android/src/main/kotlin/com/google_mlkit_pose_detection/PoseDetector.kt +++ b/packages/google_mlkit_pose_detection/android/src/main/kotlin/com/google_mlkit_pose_detection/PoseDetector.kt @@ -48,6 +48,7 @@ class PoseDetector( result.error("PoseDetectorError", "imageData is null", null) return } + val inputImage = InputImageConverter.getInputImageFromData(imageData, context, result) ?: return val id = call.argument("id") ?: return From 4d0797ed3b6d73251f66c411812fbdeeebe8a6ba Mon Sep 17 00:00:00 2001 From: Benson Arafat Date: Mon, 2 Mar 2026 20:07:36 +0100 Subject: [PATCH 21/21] fix: Add newlines for improved readability in BarcodeScanner, FaceMeshDetector, and PoseDetector --- .../kotlin/com/google_mlkit_barcode_scanning/BarcodeScanner.kt | 2 +- .../com/google_mlkit_face_mesh_detection/FaceMeshDetector.kt | 2 +- .../main/kotlin/com/google_mlkit_pose_detection/PoseDetector.kt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/google_mlkit_barcode_scanning/android/src/main/kotlin/com/google_mlkit_barcode_scanning/BarcodeScanner.kt b/packages/google_mlkit_barcode_scanning/android/src/main/kotlin/com/google_mlkit_barcode_scanning/BarcodeScanner.kt index 1179276c..85f72f58 100644 --- a/packages/google_mlkit_barcode_scanning/android/src/main/kotlin/com/google_mlkit_barcode_scanning/BarcodeScanner.kt +++ b/packages/google_mlkit_barcode_scanning/android/src/main/kotlin/com/google_mlkit_barcode_scanning/BarcodeScanner.kt @@ -68,7 +68,7 @@ class BarcodeScanner( result.error("BarcodeDetectorError", "imageData is null", null) return } - + val inputImage = InputImageConverter.getInputImageFromData(imageData, context, result) ?: return val id = call.argument("id")!! diff --git a/packages/google_mlkit_face_mesh_detection/android/src/main/kotlin/com/google_mlkit_face_mesh_detection/FaceMeshDetector.kt b/packages/google_mlkit_face_mesh_detection/android/src/main/kotlin/com/google_mlkit_face_mesh_detection/FaceMeshDetector.kt index de58ff6e..9fba504b 100644 --- a/packages/google_mlkit_face_mesh_detection/android/src/main/kotlin/com/google_mlkit_face_mesh_detection/FaceMeshDetector.kt +++ b/packages/google_mlkit_face_mesh_detection/android/src/main/kotlin/com/google_mlkit_face_mesh_detection/FaceMeshDetector.kt @@ -48,7 +48,7 @@ class FaceMeshDetector( result.error("FaceMeshDetectorError", "imageData is null", null) return } - + val inputImage = InputImageConverter.getInputImageFromData(imageData, context, result) ?: return val id = call.argument("id")!! diff --git a/packages/google_mlkit_pose_detection/android/src/main/kotlin/com/google_mlkit_pose_detection/PoseDetector.kt b/packages/google_mlkit_pose_detection/android/src/main/kotlin/com/google_mlkit_pose_detection/PoseDetector.kt index 64b944a9..7539bc06 100644 --- a/packages/google_mlkit_pose_detection/android/src/main/kotlin/com/google_mlkit_pose_detection/PoseDetector.kt +++ b/packages/google_mlkit_pose_detection/android/src/main/kotlin/com/google_mlkit_pose_detection/PoseDetector.kt @@ -48,7 +48,7 @@ class PoseDetector( result.error("PoseDetectorError", "imageData is null", null) return } - + val inputImage = InputImageConverter.getInputImageFromData(imageData, context, result) ?: return val id = call.argument("id") ?: return