From 627ad2422bf647eee89fc4014a6f6c3df4c8d881 Mon Sep 17 00:00:00 2001 From: dmjtian Date: Sat, 14 Feb 2026 12:51:11 +0800 Subject: [PATCH 01/12] fix: add classification streaming support for YOLOView Add classification result processing in convertResultToStreamData() to enable real-time classification streaming via YOLOView. Previously, classification worked for single image prediction (PR #418) but returned empty results for real-time streaming because convertResultToStreamData() didn't process result.probs. This change adds classification handling that: - Checks config.includeClassifications and result.probs - Creates detection object with classification results - Adds full-image bounding box for consistency - Includes appropriate debug logging Tested with yolo11n-cls.tflite model on Android device. Related to: PR #418 --- .../kotlin/com/ultralytics/yolo/YOLOView.kt | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/android/src/main/kotlin/com/ultralytics/yolo/YOLOView.kt b/android/src/main/kotlin/com/ultralytics/yolo/YOLOView.kt index 447aa386..7db3457e 100644 --- a/android/src/main/kotlin/com/ultralytics/yolo/YOLOView.kt +++ b/android/src/main/kotlin/com/ultralytics/yolo/YOLOView.kt @@ -1712,6 +1712,41 @@ class YOLOView @JvmOverloads constructor( map["detections"] = detections Log.d(TAG, "✅ Total detections in stream: ${detections.size} (boxes: ${result.boxes.size}, obb: ${result.obb.size})") } + + // Add classification results (if available and enabled for CLASSIFY task) + if (config.includeClassifications && result.probs != null) { + val probs = result.probs!! + Log.d(TAG, "🎯 Processing CLASSIFY result: top1Label=${probs.top1Label}, conf=${probs.top1Conf}, index=${probs.top1Index}") + + // Add classification result to detections array (for compatibility with YOLOResult.fromMap) + val detections = map["detections"] as? ArrayList> ?: ArrayList() + + val classificationDetection = HashMap() + classificationDetection["classIndex"] = probs.top1Index + classificationDetection["className"] = probs.top1Label + classificationDetection["confidence"] = probs.top1Conf.toDouble() + + // Full image bounding box for classification + val boundingBox = HashMap() + boundingBox["left"] = 0.0 + boundingBox["top"] = 0.0 + boundingBox["right"] = result.origShape.width.toDouble() + boundingBox["bottom"] = result.origShape.height.toDouble() + classificationDetection["boundingBox"] = boundingBox + + // Normalized bounding box (full image) + val normalizedBox = HashMap() + normalizedBox["left"] = 0.0 + normalizedBox["top"] = 0.0 + normalizedBox["right"] = 1.0 + normalizedBox["bottom"] = 1.0 + classificationDetection["normalizedBox"] = normalizedBox + + detections.add(classificationDetection) + map["detections"] = detections + + Log.d(TAG, "✅ Added classification result: ${probs.top1Label} (conf=${probs.top1Conf}, index=${probs.top1Index})") + } // Add performance metrics (if enabled) if (config.includeProcessingTimeMs) { From 4d5258cffcce0645383fe40ed7924b985b6094ea Mon Sep 17 00:00:00 2001 From: dmjtian Date: Wed, 25 Feb 2026 22:15:27 +0800 Subject: [PATCH 02/12] fix: add classification streaming support for iOS YOLOView Add classification result processing (top5) in convertResultToStreamData() to enable real-time classification streaming via YOLOView on iOS. This complements the Android fix (commit bb98008). --- ios/Classes/YOLOView.swift | 49 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/ios/Classes/YOLOView.swift b/ios/Classes/YOLOView.swift index 4e89a85d..bc54d5bd 100644 --- a/ios/Classes/YOLOView.swift +++ b/ios/Classes/YOLOView.swift @@ -1768,6 +1768,55 @@ extension YOLOView: AVCapturePhotoCaptureDelegate { map["detections"] = detections } + // Add classification results (if available and enabled for CLASSIFY task) + if config.includeClassifications, let probs = result.probs, result.boxes.isEmpty { + print("🎯 YOLOView: Processing CLASSIFY result - returning top 5 predictions") + + // Get or create detections array (for compatibility with YOLOResult deserialization) + var detections = map["detections"] as? [[String: Any]] ?? [] + + // For classification models, create detections from top5 predictions + let top5Labels = probs.top5Labels + let top5Confs = probs.top5Confs + + for i in 0.. Date: Wed, 25 Feb 2026 22:33:16 +0800 Subject: [PATCH 03/12] Update android/src/main/kotlin/com/ultralytics/yolo/YOLOView.kt Co-authored-by: Ultralytics Assistant <135830346+UltralyticsAssistant@users.noreply.github.com> Signed-off-by: dmjtian <31685068+dmjtian@users.noreply.github.com> --- android/src/main/kotlin/com/ultralytics/yolo/YOLOView.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/src/main/kotlin/com/ultralytics/yolo/YOLOView.kt b/android/src/main/kotlin/com/ultralytics/yolo/YOLOView.kt index 7db3457e..9da665c4 100644 --- a/android/src/main/kotlin/com/ultralytics/yolo/YOLOView.kt +++ b/android/src/main/kotlin/com/ultralytics/yolo/YOLOView.kt @@ -1719,7 +1719,7 @@ class YOLOView @JvmOverloads constructor( Log.d(TAG, "🎯 Processing CLASSIFY result: top1Label=${probs.top1Label}, conf=${probs.top1Conf}, index=${probs.top1Index}") // Add classification result to detections array (for compatibility with YOLOResult.fromMap) - val detections = map["detections"] as? ArrayList> ?: ArrayList() + val detections = (map["detections"] as? List>)?.toMutableList() ?: ArrayList() val classificationDetection = HashMap() classificationDetection["classIndex"] = probs.top1Index From 982a8403218cec8df17c438902235b52acb23f96 Mon Sep 17 00:00:00 2001 From: dmjtian Date: Mon, 16 Mar 2026 22:43:55 +0800 Subject: [PATCH 04/12] fix: correct top5 class indices and follow Results.summary() format --- .../kotlin/com/ultralytics/yolo/YOLOPlugin.kt | 21 ++++++++++++------- .../kotlin/com/ultralytics/yolo/YOLOView.kt | 21 ++++++++++++++----- 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/android/src/main/kotlin/com/ultralytics/yolo/YOLOPlugin.kt b/android/src/main/kotlin/com/ultralytics/yolo/YOLOPlugin.kt index 97af753b..b6e9f938 100644 --- a/android/src/main/kotlin/com/ultralytics/yolo/YOLOPlugin.kt +++ b/android/src/main/kotlin/com/ultralytics/yolo/YOLOPlugin.kt @@ -299,12 +299,18 @@ class YOLOPlugin : FlutterPlugin, ActivityAware, MethodChannel.MethodCallHandler yoloResult.probs?.let { probs -> // Build top5 list safely using zip to handle mismatched list lengths // Convert top5Confs to List to ensure Iterable compatibility (may be FloatArray) - val top5List = probs.top5Labels.zip(probs.top5Confs.toList()).map { (name, conf) -> - mapOf( - "name" to name, - "confidence" to conf.toDouble() - ) - } + val top5Indices = probs.top5Indices ?: (0..4).toList() + val top5List = top5Indices + .zip(probs.top5Labels) + .zip(probs.top5Confs.toList()) + .take(5) + .map { ((classIdx, name), conf) -> + mapOf( + "class" to classIdx, + "name" to name, + "confidence" to conf.toDouble() + ) + } // Classification response following Results.summary() format // Reference: https://docs.ultralytics.com/reference/engine/results/ @@ -319,8 +325,7 @@ class YOLOPlugin : FlutterPlugin, ActivityAware, MethodChannel.MethodCallHandler response["boxes"] = listOf( mapOf( "class" to probs.top1Index, - "className" to probs.top1Label, - "classIndex" to probs.top1Index, + "name" to probs.top1Label, "confidence" to probs.top1Conf.toDouble(), "x1" to 0.0, "y1" to 0.0, diff --git a/android/src/main/kotlin/com/ultralytics/yolo/YOLOView.kt b/android/src/main/kotlin/com/ultralytics/yolo/YOLOView.kt index 9da665c4..07bb1f10 100644 --- a/android/src/main/kotlin/com/ultralytics/yolo/YOLOView.kt +++ b/android/src/main/kotlin/com/ultralytics/yolo/YOLOView.kt @@ -1716,15 +1716,28 @@ class YOLOView @JvmOverloads constructor( // Add classification results (if available and enabled for CLASSIFY task) if (config.includeClassifications && result.probs != null) { val probs = result.probs!! - Log.d(TAG, "🎯 Processing CLASSIFY result: top1Label=${probs.top1Label}, conf=${probs.top1Conf}, index=${probs.top1Index}") + + val top5Indices = probs.top5Indices ?: (0..4).toList() + val top5List = top5Indices + .zip(probs.top5Labels) + .zip(probs.top5Confs.toList()) + .take(5) + .map { ((classIdx, name), conf) -> + mapOf( + "class" to classIdx, + "name" to name, + "confidence" to conf.toDouble() + ) + } // Add classification result to detections array (for compatibility with YOLOResult.fromMap) val detections = (map["detections"] as? List>)?.toMutableList() ?: ArrayList() val classificationDetection = HashMap() - classificationDetection["classIndex"] = probs.top1Index - classificationDetection["className"] = probs.top1Label + classificationDetection["class"] = probs.top1Index + classificationDetection["name"] = probs.top1Label classificationDetection["confidence"] = probs.top1Conf.toDouble() + classificationDetection["top5"] = top5List // Full image bounding box for classification val boundingBox = HashMap() @@ -1744,8 +1757,6 @@ class YOLOView @JvmOverloads constructor( detections.add(classificationDetection) map["detections"] = detections - - Log.d(TAG, "✅ Added classification result: ${probs.top1Label} (conf=${probs.top1Conf}, index=${probs.top1Index})") } // Add performance metrics (if enabled) From 3631aebd287203e97042e837fa461b58df2bd9d7 Mon Sep 17 00:00:00 2001 From: dmjtian Date: Mon, 16 Mar 2026 23:23:13 +0800 Subject: [PATCH 05/12] fix: standardize iOS classification field names to match Android (class/name format) --- ios/Classes/YOLOView.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ios/Classes/YOLOView.swift b/ios/Classes/YOLOView.swift index bc54d5bd..b82dc0f7 100644 --- a/ios/Classes/YOLOView.swift +++ b/ios/Classes/YOLOView.swift @@ -1788,8 +1788,8 @@ extension YOLOView: AVCapturePhotoCaptureDelegate { classIndex = index } - detection["classIndex"] = classIndex - detection["className"] = top5Labels[i] + detection["class"] = classIndex + detection["name"] = top5Labels[i] detection["confidence"] = Double(top5Confs[i]) // Classification doesn't have bounding boxes, use full image bounds From 4cd389246c4ed951524e80c540a037f6c65ef6c2 Mon Sep 17 00:00:00 2001 From: dmjtian Date: Mon, 16 Mar 2026 23:36:46 +0800 Subject: [PATCH 06/12] fix: align iOS classification payload structure with Android (single detection with top5 list) --- ios/Classes/YOLOView.swift | 67 ++++++++++++++++++++++---------------- 1 file changed, 39 insertions(+), 28 deletions(-) diff --git a/ios/Classes/YOLOView.swift b/ios/Classes/YOLOView.swift index b82dc0f7..c76c1b5e 100644 --- a/ios/Classes/YOLOView.swift +++ b/ios/Classes/YOLOView.swift @@ -1770,51 +1770,62 @@ extension YOLOView: AVCapturePhotoCaptureDelegate { // Add classification results (if available and enabled for CLASSIFY task) if config.includeClassifications, let probs = result.probs, result.boxes.isEmpty { - print("🎯 YOLOView: Processing CLASSIFY result - returning top 5 predictions") - // Get or create detections array (for compatibility with YOLOResult deserialization) var detections = map["detections"] as? [[String: Any]] ?? [] - // For classification models, create detections from top5 predictions + // Build top5 list with correct class indices + var top5List: [[String: Any]] = [] let top5Labels = probs.top5Labels let top5Confs = probs.top5Confs for i in 0.. Date: Mon, 16 Mar 2026 23:52:24 +0800 Subject: [PATCH 07/12] fix: add backward compatibility fields and remove unsafe class index lookup - Android: Keep classIndex/className aliases for backward compatibility - iOS: Remove unsafe class index lookup from top5 (omit class field to avoid -1 values) --- .../main/kotlin/com/ultralytics/yolo/YOLOPlugin.kt | 2 ++ ios/Classes/YOLOView.swift | 11 +++-------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/android/src/main/kotlin/com/ultralytics/yolo/YOLOPlugin.kt b/android/src/main/kotlin/com/ultralytics/yolo/YOLOPlugin.kt index b6e9f938..5f067198 100644 --- a/android/src/main/kotlin/com/ultralytics/yolo/YOLOPlugin.kt +++ b/android/src/main/kotlin/com/ultralytics/yolo/YOLOPlugin.kt @@ -326,6 +326,8 @@ class YOLOPlugin : FlutterPlugin, ActivityAware, MethodChannel.MethodCallHandler mapOf( "class" to probs.top1Index, "name" to probs.top1Label, + "classIndex" to probs.top1Index, + "className" to probs.top1Label, "confidence" to probs.top1Conf.toDouble(), "x1" to 0.0, "y1" to 0.0, diff --git a/ios/Classes/YOLOView.swift b/ios/Classes/YOLOView.swift index c76c1b5e..94cee2df 100644 --- a/ios/Classes/YOLOView.swift +++ b/ios/Classes/YOLOView.swift @@ -1773,20 +1773,15 @@ extension YOLOView: AVCapturePhotoCaptureDelegate { // Get or create detections array (for compatibility with YOLOResult deserialization) var detections = map["detections"] as? [[String: Any]] ?? [] - // Build top5 list with correct class indices + // Build top5 list with labels and confidence var top5List: [[String: Any]] = [] let top5Labels = probs.top5Labels let top5Confs = probs.top5Confs for i in 0.. Date: Mon, 16 Mar 2026 23:57:14 +0800 Subject: [PATCH 08/12] fix: only include class field in top5 when indices are available - Android: Conditionally include class field only when top5Indices is present - iOS: Omit class field from top5 (native API doesn't provide indices) - Maintains consistency: both platforms omit class when indices unavailable - Prevents synthetic/incorrect class IDs in payload --- .../kotlin/com/ultralytics/yolo/YOLOPlugin.kt | 39 ++++++++++++------- .../kotlin/com/ultralytics/yolo/YOLOView.kt | 36 +++++++++++------ ios/Classes/YOLOView.swift | 4 +- 3 files changed, 51 insertions(+), 28 deletions(-) diff --git a/android/src/main/kotlin/com/ultralytics/yolo/YOLOPlugin.kt b/android/src/main/kotlin/com/ultralytics/yolo/YOLOPlugin.kt index 5f067198..9fcf7f5c 100644 --- a/android/src/main/kotlin/com/ultralytics/yolo/YOLOPlugin.kt +++ b/android/src/main/kotlin/com/ultralytics/yolo/YOLOPlugin.kt @@ -297,20 +297,31 @@ class YOLOPlugin : FlutterPlugin, ActivityAware, MethodChannel.MethodCallHandler } YOLOTask.CLASSIFY -> { yoloResult.probs?.let { probs -> - // Build top5 list safely using zip to handle mismatched list lengths - // Convert top5Confs to List to ensure Iterable compatibility (may be FloatArray) - val top5Indices = probs.top5Indices ?: (0..4).toList() - val top5List = top5Indices - .zip(probs.top5Labels) - .zip(probs.top5Confs.toList()) - .take(5) - .map { ((classIdx, name), conf) -> - mapOf( - "class" to classIdx, - "name" to name, - "confidence" to conf.toDouble() - ) - } + // Build top5 list only with available indices to avoid synthetic values + val top5List = if (probs.top5Indices != null) { + probs.top5Indices!! + .zip(probs.top5Labels) + .zip(probs.top5Confs.toList()) + .take(5) + .map { ((classIdx, name), conf) -> + mapOf( + "class" to classIdx, + "name" to name, + "confidence" to conf.toDouble() + ) + } + } else { + // Omit class field when indices are not available + probs.top5Labels + .zip(probs.top5Confs.toList()) + .take(5) + .map { (name, conf) -> + mapOf( + "name" to name, + "confidence" to conf.toDouble() + ) + } + } // Classification response following Results.summary() format // Reference: https://docs.ultralytics.com/reference/engine/results/ diff --git a/android/src/main/kotlin/com/ultralytics/yolo/YOLOView.kt b/android/src/main/kotlin/com/ultralytics/yolo/YOLOView.kt index 07bb1f10..4d4d9535 100644 --- a/android/src/main/kotlin/com/ultralytics/yolo/YOLOView.kt +++ b/android/src/main/kotlin/com/ultralytics/yolo/YOLOView.kt @@ -1717,18 +1717,30 @@ class YOLOView @JvmOverloads constructor( if (config.includeClassifications && result.probs != null) { val probs = result.probs!! - val top5Indices = probs.top5Indices ?: (0..4).toList() - val top5List = top5Indices - .zip(probs.top5Labels) - .zip(probs.top5Confs.toList()) - .take(5) - .map { ((classIdx, name), conf) -> - mapOf( - "class" to classIdx, - "name" to name, - "confidence" to conf.toDouble() - ) - } + val top5List = if (probs.top5Indices != null) { + probs.top5Indices!! + .zip(probs.top5Labels) + .zip(probs.top5Confs.toList()) + .take(5) + .map { ((classIdx, name), conf) -> + mapOf( + "class" to classIdx, + "name" to name, + "confidence" to conf.toDouble() + ) + } + } else { + // Omit class field when indices are not available + probs.top5Labels + .zip(probs.top5Confs.toList()) + .take(5) + .map { (name, conf) -> + mapOf( + "name" to name, + "confidence" to conf.toDouble() + ) + } + } // Add classification result to detections array (for compatibility with YOLOResult.fromMap) val detections = (map["detections"] as? List>)?.toMutableList() ?: ArrayList() diff --git a/ios/Classes/YOLOView.swift b/ios/Classes/YOLOView.swift index 94cee2df..1d00b214 100644 --- a/ios/Classes/YOLOView.swift +++ b/ios/Classes/YOLOView.swift @@ -1774,13 +1774,13 @@ extension YOLOView: AVCapturePhotoCaptureDelegate { var detections = map["detections"] as? [[String: Any]] ?? [] // Build top5 list with labels and confidence + // Note: iOS native API doesn't provide top5 indices, so we omit class field + // to maintain consistency with Android when indices are unavailable var top5List: [[String: Any]] = [] let top5Labels = probs.top5Labels let top5Confs = probs.top5Confs for i in 0.. Date: Tue, 17 Mar 2026 00:02:03 +0800 Subject: [PATCH 09/12] fix: align top5 payload structure across platforms - omit class field - Both Android and iOS now omit class field from top5 entries - Only top1 includes class field (where it's reliable) - Maintains cross-platform consistency since iOS native API doesn't provide top5 indices - Prevents synthetic/incorrect class IDs in top5 payload --- .../kotlin/com/ultralytics/yolo/YOLOPlugin.kt | 37 ++++++------------- .../kotlin/com/ultralytics/yolo/YOLOView.kt | 33 +++++------------ 2 files changed, 21 insertions(+), 49 deletions(-) diff --git a/android/src/main/kotlin/com/ultralytics/yolo/YOLOPlugin.kt b/android/src/main/kotlin/com/ultralytics/yolo/YOLOPlugin.kt index 9fcf7f5c..37dfee62 100644 --- a/android/src/main/kotlin/com/ultralytics/yolo/YOLOPlugin.kt +++ b/android/src/main/kotlin/com/ultralytics/yolo/YOLOPlugin.kt @@ -297,31 +297,18 @@ class YOLOPlugin : FlutterPlugin, ActivityAware, MethodChannel.MethodCallHandler } YOLOTask.CLASSIFY -> { yoloResult.probs?.let { probs -> - // Build top5 list only with available indices to avoid synthetic values - val top5List = if (probs.top5Indices != null) { - probs.top5Indices!! - .zip(probs.top5Labels) - .zip(probs.top5Confs.toList()) - .take(5) - .map { ((classIdx, name), conf) -> - mapOf( - "class" to classIdx, - "name" to name, - "confidence" to conf.toDouble() - ) - } - } else { - // Omit class field when indices are not available - probs.top5Labels - .zip(probs.top5Confs.toList()) - .take(5) - .map { (name, conf) -> - mapOf( - "name" to name, - "confidence" to conf.toDouble() - ) - } - } + // Build top5 list with labels and confidence only + // Omit class field to maintain consistency across platforms + // (iOS native API doesn't provide top5 indices) + val top5List = probs.top5Labels + .zip(probs.top5Confs.toList()) + .take(5) + .map { (name, conf) -> + mapOf( + "name" to name, + "confidence" to conf.toDouble() + ) + } // Classification response following Results.summary() format // Reference: https://docs.ultralytics.com/reference/engine/results/ diff --git a/android/src/main/kotlin/com/ultralytics/yolo/YOLOView.kt b/android/src/main/kotlin/com/ultralytics/yolo/YOLOView.kt index 4d4d9535..7dc35115 100644 --- a/android/src/main/kotlin/com/ultralytics/yolo/YOLOView.kt +++ b/android/src/main/kotlin/com/ultralytics/yolo/YOLOView.kt @@ -1717,30 +1717,15 @@ class YOLOView @JvmOverloads constructor( if (config.includeClassifications && result.probs != null) { val probs = result.probs!! - val top5List = if (probs.top5Indices != null) { - probs.top5Indices!! - .zip(probs.top5Labels) - .zip(probs.top5Confs.toList()) - .take(5) - .map { ((classIdx, name), conf) -> - mapOf( - "class" to classIdx, - "name" to name, - "confidence" to conf.toDouble() - ) - } - } else { - // Omit class field when indices are not available - probs.top5Labels - .zip(probs.top5Confs.toList()) - .take(5) - .map { (name, conf) -> - mapOf( - "name" to name, - "confidence" to conf.toDouble() - ) - } - } + val top5List = probs.top5Labels + .zip(probs.top5Confs.toList()) + .take(5) + .map { (name, conf) -> + mapOf( + "name" to name, + "confidence" to conf.toDouble() + ) + } // Add classification result to detections array (for compatibility with YOLOResult.fromMap) val detections = (map["detections"] as? List>)?.toMutableList() ?: ArrayList() From 83c9efe631e9d1b4e04a9c3941206accee2c17f5 Mon Sep 17 00:00:00 2001 From: dmjtian Date: Tue, 17 Mar 2026 00:11:27 +0800 Subject: [PATCH 10/12] fix: omit class field from streaming classification payload for cross-platform consistency - iOS: Remove unreliable class index lookup (native API doesn't provide it) - Android streaming: Omit class field to match iOS (maintain consistency) - Android single-frame: Keep class field (has real probs.top1Index) - Prevents sending -1 or incorrect class values in streaming payload --- android/src/main/kotlin/com/ultralytics/yolo/YOLOView.kt | 3 ++- ios/Classes/YOLOView.swift | 9 ++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/android/src/main/kotlin/com/ultralytics/yolo/YOLOView.kt b/android/src/main/kotlin/com/ultralytics/yolo/YOLOView.kt index 7dc35115..7717f72a 100644 --- a/android/src/main/kotlin/com/ultralytics/yolo/YOLOView.kt +++ b/android/src/main/kotlin/com/ultralytics/yolo/YOLOView.kt @@ -1731,7 +1731,8 @@ class YOLOView @JvmOverloads constructor( val detections = (map["detections"] as? List>)?.toMutableList() ?: ArrayList() val classificationDetection = HashMap() - classificationDetection["class"] = probs.top1Index + // Omit class field from streaming to maintain consistency with iOS + // (iOS native API doesn't provide class index) classificationDetection["name"] = probs.top1Label classificationDetection["confidence"] = probs.top1Conf.toDouble() classificationDetection["top5"] = top5List diff --git a/ios/Classes/YOLOView.swift b/ios/Classes/YOLOView.swift index 1d00b214..8c509c1e 100644 --- a/ios/Classes/YOLOView.swift +++ b/ios/Classes/YOLOView.swift @@ -1790,13 +1790,8 @@ extension YOLOView: AVCapturePhotoCaptureDelegate { // Create single detection object with top1 and top5 info var detection: [String: Any] = [:] - // Find top1 class index - var top1ClassIndex = -1 - if let index = result.names.firstIndex(of: probs.top1Label) { - top1ClassIndex = index - } - - detection["class"] = top1ClassIndex + // Note: iOS native API doesn't provide real class index, so we omit it + // to avoid sending potentially incorrect values (e.g., -1 or wrong index) detection["name"] = probs.top1Label detection["confidence"] = Double(probs.top1Conf) detection["top5"] = top5List From 93e8a4e0c3cd217f9cd8d9864bfd556c9f0e0a8d Mon Sep 17 00:00:00 2001 From: dmjtian Date: Tue, 17 Mar 2026 00:14:57 +0800 Subject: [PATCH 11/12] fix: add classification-only guard to Android streaming to match iOS - Android now checks result.boxes.isEmpty() before adding classification detection - Prevents mixed payloads when result contains both detections and classifications - Maintains consistency with iOS which explicitly guards with result.boxes.isEmpty --- android/src/main/kotlin/com/ultralytics/yolo/YOLOView.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/src/main/kotlin/com/ultralytics/yolo/YOLOView.kt b/android/src/main/kotlin/com/ultralytics/yolo/YOLOView.kt index 7717f72a..14de70f1 100644 --- a/android/src/main/kotlin/com/ultralytics/yolo/YOLOView.kt +++ b/android/src/main/kotlin/com/ultralytics/yolo/YOLOView.kt @@ -1714,7 +1714,7 @@ class YOLOView @JvmOverloads constructor( } // Add classification results (if available and enabled for CLASSIFY task) - if (config.includeClassifications && result.probs != null) { + if (config.includeClassifications && result.probs != null && result.boxes.isEmpty()) { val probs = result.probs!! val top5List = probs.top5Labels From 0884acde6db75f58f25520bc818407a614c2f4d0 Mon Sep 17 00:00:00 2001 From: dmjtian Date: Tue, 17 Mar 2026 00:19:22 +0800 Subject: [PATCH 12/12] fix: restore class fields in Android, keep iOS without class for safety - Android: Restore class/classIndex fields in top5 and top1 (has real indices) - Android: Maintain schema consistency between single-frame and streaming paths - iOS: Keep class field omitted (native API doesn't provide indices) - Prevents information loss on Android while avoiding -1 values on iOS --- .../kotlin/com/ultralytics/yolo/YOLOPlugin.kt | 37 +++++++++++++------ .../kotlin/com/ultralytics/yolo/YOLOView.kt | 36 ++++++++++++------ 2 files changed, 50 insertions(+), 23 deletions(-) diff --git a/android/src/main/kotlin/com/ultralytics/yolo/YOLOPlugin.kt b/android/src/main/kotlin/com/ultralytics/yolo/YOLOPlugin.kt index 37dfee62..7344a951 100644 --- a/android/src/main/kotlin/com/ultralytics/yolo/YOLOPlugin.kt +++ b/android/src/main/kotlin/com/ultralytics/yolo/YOLOPlugin.kt @@ -297,18 +297,31 @@ class YOLOPlugin : FlutterPlugin, ActivityAware, MethodChannel.MethodCallHandler } YOLOTask.CLASSIFY -> { yoloResult.probs?.let { probs -> - // Build top5 list with labels and confidence only - // Omit class field to maintain consistency across platforms - // (iOS native API doesn't provide top5 indices) - val top5List = probs.top5Labels - .zip(probs.top5Confs.toList()) - .take(5) - .map { (name, conf) -> - mapOf( - "name" to name, - "confidence" to conf.toDouble() - ) - } + // Build top5 list with class indices (available from native API) + val top5List = if (probs.top5Indices != null) { + probs.top5Indices!! + .zip(probs.top5Labels) + .zip(probs.top5Confs.toList()) + .take(5) + .map { ((classIdx, name), conf) -> + mapOf( + "class" to classIdx, + "name" to name, + "confidence" to conf.toDouble() + ) + } + } else { + // Fallback: omit class when indices unavailable + probs.top5Labels + .zip(probs.top5Confs.toList()) + .take(5) + .map { (name, conf) -> + mapOf( + "name" to name, + "confidence" to conf.toDouble() + ) + } + } // Classification response following Results.summary() format // Reference: https://docs.ultralytics.com/reference/engine/results/ diff --git a/android/src/main/kotlin/com/ultralytics/yolo/YOLOView.kt b/android/src/main/kotlin/com/ultralytics/yolo/YOLOView.kt index 14de70f1..4042a849 100644 --- a/android/src/main/kotlin/com/ultralytics/yolo/YOLOView.kt +++ b/android/src/main/kotlin/com/ultralytics/yolo/YOLOView.kt @@ -1717,22 +1717,36 @@ class YOLOView @JvmOverloads constructor( if (config.includeClassifications && result.probs != null && result.boxes.isEmpty()) { val probs = result.probs!! - val top5List = probs.top5Labels - .zip(probs.top5Confs.toList()) - .take(5) - .map { (name, conf) -> - mapOf( - "name" to name, - "confidence" to conf.toDouble() - ) - } + val top5List = if (probs.top5Indices != null) { + probs.top5Indices!! + .zip(probs.top5Labels) + .zip(probs.top5Confs.toList()) + .take(5) + .map { ((classIdx, name), conf) -> + mapOf( + "class" to classIdx, + "name" to name, + "confidence" to conf.toDouble() + ) + } + } else { + // Fallback: omit class when indices unavailable + probs.top5Labels + .zip(probs.top5Confs.toList()) + .take(5) + .map { (name, conf) -> + mapOf( + "name" to name, + "confidence" to conf.toDouble() + ) + } + } // Add classification result to detections array (for compatibility with YOLOResult.fromMap) val detections = (map["detections"] as? List>)?.toMutableList() ?: ArrayList() val classificationDetection = HashMap() - // Omit class field from streaming to maintain consistency with iOS - // (iOS native API doesn't provide class index) + classificationDetection["class"] = probs.top1Index classificationDetection["name"] = probs.top1Label classificationDetection["confidence"] = probs.top1Conf.toDouble() classificationDetection["top5"] = top5List