diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 3c52eb1f..c67d1421 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -15,7 +15,7 @@ on: jobs: check: - if: github.repository == 'ultralytics/yolo-flutter-app' && (github.actor == 'glenn-jocher' || github.actor == 'asabri97' || github.actor == 'john-rocky') + if: github.repository == 'ultralytics/yolo-flutter-app' && github.actor == 'glenn-jocher' runs-on: ubuntu-latest permissions: contents: write diff --git a/.github/workflows/tag.yml b/.github/workflows/tag.yml index 99121d06..cf28bc83 100644 --- a/.github/workflows/tag.yml +++ b/.github/workflows/tag.yml @@ -29,7 +29,7 @@ on: jobs: tag-and-release: - if: github.repository == 'ultralytics/yolo-flutter-app' && (github.actor == 'glenn-jocher' || github.actor == 'asabri97' || github.actor == 'john-rocky') + if: github.repository == 'ultralytics/yolo-flutter-app' && github.actor == 'glenn-jocher' name: Tag and Release runs-on: ubuntu-latest steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index 5abddc62..596fe57d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,11 @@ -## Unreleased +## 0.3.0 - **Feature**: Add package-level model resolution for official IDs, remote URLs, local files, and Flutter assets. - **Enhancement**: Resolve `task` from exported metadata when available across `YOLO`, `YOLOView`, and `YOLOViewController.switchModel()`. - **Enhancement**: Replace hardcoded release URL assumptions with an official model catalog and latest-release resolution. +- **Enhancement**: Add `YOLO.defaultOfficialModel()` to make the default official model path explicit for new users. +- **Bug Fix**: Enforce official Ultralytics defaults everywhere with `confidenceThreshold = 0.25` and `iouThreshold = 0.7`. +- **Compatibility**: Preserve deprecated API shims and widget wrapper exports to avoid regressions for existing integrations. - **Cleanup**: Remove example-only model management duplication and update docs around the metadata-first flow. ## 0.2.0 diff --git a/README.md b/README.md index 17055286..19054113 100644 --- a/README.md +++ b/README.md @@ -54,11 +54,13 @@ Start with the default official model: ```dart import 'package:ultralytics_yolo/ultralytics_yolo.dart'; +final modelId = YOLO.defaultOfficialModel() ?? 'yolo26n'; + YOLOView( - modelPath: 'yolo26n', + modelPath: modelId, onResult: (results) { - for (final result in results) { - print('${result.className}: ${result.confidence}'); + for (final r in results) { + debugPrint('${r.className}: ${r.confidence}'); } }, ) @@ -80,29 +82,38 @@ The plugin supports three model flows. ### 1. Official model IDs -Use an official ID such as `yolo26n` and let the plugin handle download and caching: +Use the default official model or a specific official ID and let the plugin +handle download and caching: ```dart -final yolo = YOLO(modelPath: 'yolo26n'); +final yolo = YOLO(modelPath: YOLO.defaultOfficialModel() ?? 'yolo26n'); ``` -Call `YOLO.officialModels()` to see which official IDs are available on the current platform. +Call `YOLO.officialModels()` to see which official IDs are available on the +current platform. ### 2. Your own exported model -Pass a local path or Flutter asset path: +Pass your own exported YOLO model as a local path or Flutter asset path: ```dart -final yolo = YOLO(modelPath: 'assets/models/custom.tflite'); +final yolo = YOLO(modelPath: 'assets/models/my-finetuned-model.tflite'); ``` If the exported model includes metadata, the plugin infers `task` automatically. If metadata is missing, pass `task` explicitly. +```dart +final yolo = YOLO( + modelPath: 'assets/models/my-finetuned-model.tflite', + task: YOLOTask.detect, +); +``` + ### 3. Remote model URL Pass an `http` or `https` URL and the plugin will download it into app storage before loading it. -## 🧭 Official Vs Custom +## 🧭 Official vs. Custom | Use case | Recommended path | | ----------------------------------------------------- | --------------------------------- | @@ -112,7 +123,9 @@ Pass an `http` or `https` URL and the plugin will download it into app storage b | You need the plugin to infer `task` automatically | Any export with metadata | | You have an older or stripped export without metadata | Custom model plus explicit `task` | -For official models, start with `YOLO.officialModels()`. For custom models, start with the exported file you actually plan to ship. +For official models, start with `YOLO.defaultOfficialModel()` or +`YOLO.officialModels()`. For custom models, start with the exported file you +actually plan to ship. ## 📥 Drop Your Own Model Into an App @@ -127,7 +140,7 @@ Then point `modelPath` at that file or asset path. ### iOS export note -Detection models exported to CoreML must use `nms=True`: +Detection models exported to Core ML must use `nms=True`: ```python from ultralytics import YOLO @@ -163,13 +176,13 @@ await controller.switchModel('assets/models/custom.tflite', YOLOTask.detect); ## 🧩 Recommended Patterns -| App type | Model loading pattern | -| ---------------------------------- | ---------------------------------------------------------------------- | -| Live camera app | `YOLOView(modelPath: 'yolo26n')` | -| Photo picker or gallery workflow | `YOLO(modelPath: 'yolo26n')` | -| App with your own bundled model | `YOLO(modelPath: 'assets/models/custom.tflite')` | -| Cross-platform CoreML + TFLite app | Use platform-appropriate exported assets and let metadata drive `task` | -| App that changes models at runtime | `YOLOViewController.switchModel(...)` | +| App type | Model loading pattern | +| ----------------------------------- | ---------------------------------------------------------------------- | +| Live camera app | `YOLOView(modelPath: 'yolo26n')` | +| Photo picker or gallery workflow | `YOLO(modelPath: 'yolo26n')` | +| App with your own bundled model | `YOLO(modelPath: 'assets/models/custom.tflite')` | +| Cross-platform Core ML + TFLite app | Use platform-appropriate exported assets and let metadata drive `task` | +| App that changes models at runtime | `YOLOViewController.switchModel(...)` | ## 📚 Documentation diff --git a/README.zh-CN.md b/README.zh-CN.md index 063a975c..293a7448 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -54,11 +54,13 @@ flutter pub get ```dart import 'package:ultralytics_yolo/ultralytics_yolo.dart'; +final modelId = YOLO.defaultOfficialModel() ?? 'yolo26n'; + YOLOView( - modelPath: 'yolo26n', + modelPath: modelId, onResult: (results) { - for (final result in results) { - print('${result.className}: ${result.confidence}'); + for (final r in results) { + debugPrint('${r.className}: ${r.confidence}'); } }, ) @@ -80,24 +82,32 @@ final results = await yolo.predict(imageBytes); ### 1. 官方模型 ID -直接使用官方 ID,例如 `yolo26n`: +可直接使用默认官方模型,或传入指定官方 ID,例如 `yolo26n`: ```dart -final yolo = YOLO(modelPath: 'yolo26n'); +final yolo = YOLO(modelPath: YOLO.defaultOfficialModel() ?? 'yolo26n'); ``` -插件会自动下载并缓存当前平台对应的官方产物。可通过 `YOLO.officialModels()` 查看当前平台可用的官方 ID。 +插件会自动下载并缓存当前平台对应的官方产物。可通过 +`YOLO.officialModels()` 查看当前平台可用的全部官方 ID。 ### 2. 你自己的导出模型 -传入本地路径或 Flutter 资源路径: +传入你自己的导出 YOLO 模型本地路径或 Flutter 资源路径: ```dart -final yolo = YOLO(modelPath: 'assets/models/custom.tflite'); +final yolo = YOLO(modelPath: 'assets/models/my-finetuned-model.tflite'); ``` 如果模型导出时带有元数据,插件会自动推断 `task`。如果没有,就显式传入 `task`。 +```dart +final yolo = YOLO( + modelPath: 'assets/models/my-finetuned-model.tflite', + task: YOLOTask.detect, +); +``` + ### 3. 远程模型 URL 传入 `http` 或 `https` URL,插件会先下载到应用存储,再完成加载。 @@ -112,7 +122,8 @@ final yolo = YOLO(modelPath: 'assets/models/custom.tflite'); | 希望插件自动推断 `task` | 使用带元数据的导出模型 | | 你的导出模型没有元数据 | 自定义模型并显式传入 `task` | -官方模型先看 `YOLO.officialModels()`;自定义模型则直接从你准备实际交付的导出文件开始。 +官方模型可直接从 `YOLO.defaultOfficialModel()` 或 +`YOLO.officialModels()` 开始;自定义模型则直接从你准备实际交付的导出文件开始。 ## 📥 把你自己的模型放进应用 @@ -127,7 +138,7 @@ final yolo = YOLO(modelPath: 'assets/models/custom.tflite'); ### iOS 导出注意事项 -导出 CoreML 检测模型时必须使用 `nms=True`: +导出 Core ML 检测模型时必须使用 `nms=True`: ```python from ultralytics import YOLO @@ -163,13 +174,13 @@ await controller.switchModel('assets/models/custom.tflite', YOLOTask.detect); ## 🧩 推荐接入模式 -| 应用类型 | 推荐模型加载方式 | -| -------------------------------------- | ------------------------------------------------ | -| 实时相机场景 | `YOLOView(modelPath: 'yolo26n')` | -| 图库或单图推理流程 | `YOLO(modelPath: 'yolo26n')` | -| 应用内置自定义模型 | `YOLO(modelPath: 'assets/models/custom.tflite')` | -| 同时支持 CoreML 与 TFLite 的跨平台应用 | 使用各平台对应导出文件,并让元数据决定 `task` | -| 运行时动态切换模型 | `YOLOViewController.switchModel(...)` | +| 应用类型 | 推荐模型加载方式 | +| --------------------------------------- | ------------------------------------------------ | +| 实时相机场景 | `YOLOView(modelPath: 'yolo26n')` | +| 图库或单图推理流程 | `YOLO(modelPath: 'yolo26n')` | +| 应用内置自定义模型 | `YOLO(modelPath: 'assets/models/custom.tflite')` | +| 同时支持 Core ML 与 TFLite 的跨平台应用 | 使用各平台对应导出文件,并让元数据决定 `task` | +| 运行时动态切换模型 | `YOLOViewController.switchModel(...)` | ## 📚 文档 diff --git a/android/src/main/kotlin/com/ultralytics/yolo/Classifier.kt b/android/src/main/kotlin/com/ultralytics/yolo/Classifier.kt index f68a4521..7d2d2315 100644 --- a/android/src/main/kotlin/com/ultralytics/yolo/Classifier.kt +++ b/android/src/main/kotlin/com/ultralytics/yolo/Classifier.kt @@ -41,7 +41,6 @@ class Classifier( if (useGpu) { try { addDelegate(GpuDelegate()) - Log.d(TAG, "GPU delegate is used.") } catch (e: Exception) { Log.e(TAG, "GPU delegate error: ${e.message}") } @@ -61,11 +60,11 @@ class Classifier( val modelBuffer = YOLOUtils.loadModelFile(context, modelPath) // ===== Load label information (try Appended ZIP → FlatBuffers in order) ===== - var loadedLabels = YOLOFileUtils.loadLabelsFromAppendedZip(context, modelPath) + val loadedLabels = YOLOFileUtils.loadLabelsFromAppendedZip(context, modelPath) var labelsWereLoaded = loadedLabels != null - if (labelsWereLoaded) { - this.labels = loadedLabels!! // Use labels from appended ZIP + if (loadedLabels != null) { + this.labels = loadedLabels // Use labels from appended ZIP Log.i(TAG, "Labels successfully loaded from appended ZIP.") } else { Log.w(TAG, "Could not load labels from appended ZIP, trying FlatBuffers metadata...") @@ -112,25 +111,20 @@ class Classifier( require(inChannels == expectedChannels || (expectedChannels == 1 && inChannels == 1) || (expectedChannels == 3 && inChannels == 3)) { "Unexpected input channels. Expected $expectedChannels channels, but got $inChannels channels. Input shape: ${inputShape.joinToString()}" } - - Log.d(TAG, "Model configuration: ${inChannels}-channel input, grayscale mode: $isGrayscaleModel") inputSize = Size(inWidth, inHeight) modelInputSize = Pair(inWidth, inHeight) - Log.d(TAG, "Model input size = $inWidth x $inHeight") val outputShape = interpreter.getOutputTensor(0).shape() // e.g. outputShape = [1, 1000] for ImageNet, [1, 12] for EMNIST numClass = outputShape[1] - + // Validate expected classes if specified (classifierOptions?.get("expectedClasses") as? Int)?.let { expectedClasses -> if (numClass != expectedClasses) { Log.w(TAG, "Warning: Expected $expectedClasses output classes, but model has $numClass classes") } } - - Log.d(TAG, "Model output shape = [1, $numClass] (${if (isGrayscaleModel) "grayscale" else "RGB"} model)") // Setup ImageProcessors only for RGB models (3-channel) // For grayscale models (1-channel), we'll use custom processing @@ -165,8 +159,6 @@ class Classifier( .add(CastOp(DataType.FLOAT32)) .build() } - - Log.d(TAG, "Classifier initialized.") } override fun predict(bitmap: Bitmap, origWidth: Int, origHeight: Int, rotateForCamera: Boolean, isLandscape: Boolean): YOLOResult { @@ -193,7 +185,6 @@ class Classifier( inputMean = inputMean, inputStd = inputStd ) - Log.d(TAG, "Using grayscale processing for 1-channel model") } else { // Use standard RGB processing for 3-channel models val tensorImage = TensorImage(DataType.FLOAT32) @@ -217,7 +208,6 @@ class Classifier( imageProcessorSingleImage.process(tensorImage) } inputBuffer = processedImage.buffer - Log.d(TAG, "Using RGB processing for 3-channel model") } val outputArray = Array(1) { FloatArray(numClass) } @@ -253,10 +243,6 @@ class Classifier( val fpsVal = if (t4 > 0) 1.0 / t4 else 0.0 - Log.d(TAG, "Classification result: top1Label=${probs.top1Label}, top1Conf=${probs.top1Conf}, top1Index=${probs.top1Index}") - Log.d(TAG, "Labels: ${labels}") - Log.d(TAG, "Prediction completed successfully") - return YOLOResult( origShape = Size(origWidth, origHeight), probs = probs, @@ -281,10 +267,8 @@ class Classifier( val files = extractor.associatedFileNames if (!files.isNullOrEmpty()) { for (fileName in files) { - Log.d(TAG, "Found associated file: $fileName") extractor.getAssociatedFile(fileName)?.use { stream -> val fileString = String(stream.readBytes(), Charsets.UTF_8) - Log.d(TAG, "Associated file contents:\n$fileString") val yaml = Yaml() @Suppress("UNCHECKED_CAST") @@ -293,14 +277,11 @@ class Classifier( val namesMap = data["names"] as? Map if (namesMap != null) { labels = namesMap.values.toList() - Log.d(TAG, "Loaded labels from metadata: $labels") return true } } } } - } else { - Log.d(TAG, "No associated files found in the metadata.") } false } catch (e: Exception) { diff --git a/android/src/main/kotlin/com/ultralytics/yolo/ObbDetector.kt b/android/src/main/kotlin/com/ultralytics/yolo/ObbDetector.kt index 4c0f8f90..ed6fb0f2 100644 --- a/android/src/main/kotlin/com/ultralytics/yolo/ObbDetector.kt +++ b/android/src/main/kotlin/com/ultralytics/yolo/ObbDetector.kt @@ -45,7 +45,6 @@ class ObbDetector( if (useGpu) { try { addDelegate(GpuDelegate()) - Log.d("ObbDetector", "GPU delegate is used.") } catch (e: Exception) { Log.e("ObbDetector", "GPU delegate error: ${e.message}") } @@ -76,11 +75,11 @@ class ObbDetector( val modelBuffer = YOLOUtils.loadModelFile(context, modelPath) // ===== Load label information (try Appended ZIP → FlatBuffers in order) ===== - var loadedLabels = YOLOFileUtils.loadLabelsFromAppendedZip(context, modelPath) + val loadedLabels = YOLOFileUtils.loadLabelsFromAppendedZip(context, modelPath) var labelsWereLoaded = loadedLabels != null - if (labelsWereLoaded) { - this.labels = loadedLabels!! // Use labels from appended ZIP + if (loadedLabels != null) { + this.labels = loadedLabels // Use labels from appended ZIP Log.i("ObbDetector", "Labels successfully loaded from appended ZIP.") } else { Log.w("ObbDetector", "Could not load labels from appended ZIP, trying FlatBuffers metadata...") @@ -101,7 +100,6 @@ class ObbDetector( interpreter = Interpreter(modelBuffer, interpreterOptions) // Call allocateTensors() once during initialization, not in the inference loop interpreter.allocateTensors() - Log.d("ObbDetector", "TFLite model loaded and tensors allocated") val inputShape = interpreter.getInputTensor(0).shape() val inHeight = inputShape[1] @@ -464,10 +462,8 @@ class ObbDetector( val files = extractor.associatedFileNames if (!files.isNullOrEmpty()) { for (fileName in files) { - Log.d("ObbDetector", "Found associated file: $fileName") extractor.getAssociatedFile(fileName)?.use { stream -> val fileString = String(stream.readBytes(), Charsets.UTF_8) - Log.d("ObbDetector", "Associated file contents:\n$fileString") val yaml = Yaml() @Suppress("UNCHECKED_CAST") @@ -476,14 +472,11 @@ class ObbDetector( val namesMap = data["names"] as? Map if (namesMap != null) { labels = namesMap.values.toList() - Log.d("ObbDetector", "Loaded labels from metadata: $labels") return true } } } } - } else { - Log.d("ObbDetector", "No associated files found in the metadata.") } false } catch (e: Exception) { diff --git a/android/src/main/kotlin/com/ultralytics/yolo/ObjectDetector.kt b/android/src/main/kotlin/com/ultralytics/yolo/ObjectDetector.kt index 9e2adaf9..51cadc62 100644 --- a/android/src/main/kotlin/com/ultralytics/yolo/ObjectDetector.kt +++ b/android/src/main/kotlin/com/ultralytics/yolo/ObjectDetector.kt @@ -84,7 +84,6 @@ class ObjectDetector( if (useGpu) { try { addDelegate(GpuDelegate()) - Log.d("ObjectDetector", "GPU delegate is used.") } catch (e: Exception) { Log.e("ObjectDetector", "GPU delegate error: ${e.message}") } @@ -99,11 +98,11 @@ class ObjectDetector( val modelBuffer = YOLOUtils.loadModelFile(context, modelPath) /* --- Get labels from metadata (try Appended ZIP → FlatBuffers in order) --- */ - var loadedLabels = YOLOFileUtils.loadLabelsFromAppendedZip(context, modelPath) + val loadedLabels = YOLOFileUtils.loadLabelsFromAppendedZip(context, modelPath) var labelsWereLoaded = loadedLabels != null - if (labelsWereLoaded) { - this.labels = loadedLabels!! // Use labels from appended ZIP + if (loadedLabels != null) { + this.labels = loadedLabels // Use labels from appended ZIP Log.i(TAG, "Labels successfully loaded from appended ZIP.") } else { Log.w(TAG, "Could not load labels from appended ZIP, trying FlatBuffers metadata...") @@ -128,7 +127,6 @@ class ObjectDetector( interpreter = Interpreter(modelBuffer, interpreterOptions) // Call allocateTensors() once during initialization, not in the inference loop interpreter.allocateTensors() - Log.d("TAG", "TFLite model loaded: $modelPath, tensors allocated") // Check input shape (example: [1, inHeight, inWidth, 3]) val inputShape = interpreter.getInputTensor(0).shape() @@ -141,14 +139,12 @@ class ObjectDetector( } inputSize = Size(inWidth, inHeight) // Set variable in BasePredictor modelInputSize = Pair(inWidth, inHeight) - Log.d("TAG", "Model input size = $inWidth x $inHeight") // Output shape (varies by model, modify as needed) // Example: [1, 84, 2100] = [batch, outHeight, outWidth] val outputShape = interpreter.getOutputTensor(0).shape() out1 = outputShape[1] // 84 out2 = outputShape[2] // 2100 - Log.d("TAG", "Model output shape = [1, $out1, $out2]") // Allocate preprocessing resources initPreprocessingResources(inWidth, inHeight) @@ -188,8 +184,6 @@ class ObjectDetector( .add(NormalizeOp(INPUT_MEAN, INPUT_STANDARD_DEVIATION)) .add(CastOp(INPUT_IMAGE_TYPE)) .build() - - Log.d("TAG", "ObjectDetector initialized.") } /* =================================================================== */ @@ -212,10 +206,8 @@ class ObjectDetector( val files = extractor.associatedFileNames if (!files.isNullOrEmpty()) { for (fileName in files) { - Log.d(TAG, "Found associated file: $fileName") extractor.getAssociatedFile(fileName)?.use { stream -> val fileString = String(stream.readBytes(), Charsets.UTF_8) - Log.d(TAG, "Associated file contents:\n$fileString") val yaml = Yaml() @Suppress("UNCHECKED_CAST") @@ -224,14 +216,11 @@ class ObjectDetector( val namesMap = data["names"] as? Map if (namesMap != null) { labels = namesMap.values.toList() // Same as old code - Log.d(TAG, "Loaded labels from metadata: $labels") return true } } } } - } else { - Log.d(TAG, "No associated files found in the metadata.") } false } catch (e: Exception) { @@ -269,7 +258,6 @@ class ObjectDetector( var stageStartTime = System.nanoTime() // ======== Preprocessing: Convert Bitmap to ByteBuffer via TensorImage ======== - Log.d(TAG, "Predict Start: Preprocessing") // 1. Resize to input size (using createScaledBitmap instead of the original scaledBitmap) // val resizedBitmap = Bitmap.createScaledBitmap(bitmap, inputSize.width, inputSize.height, false) @@ -304,23 +292,18 @@ class ObjectDetector( inputBuffer.rewind() var preprocessTimeMs = (System.nanoTime() - stageStartTime) / 1_000_000.0 - Log.d(TAG, "Predict Stage: Preprocessing done in $preprocessTimeMs ms") stageStartTime = System.nanoTime() // ======== Inference ============ - Log.d(TAG, "Predict Start: Inference") interpreter.run(inputBuffer, rawOutput) var inferenceTimeMs = (System.nanoTime() - stageStartTime) / 1_000_000.0 - Log.d(TAG, "Predict Stage: Inference done in $inferenceTimeMs ms") stageStartTime = System.nanoTime() // ======== Post-processing (same as existing code) ============ - Log.d(TAG, "Predict Start: Postprocessing") // val postStart = System.nanoTime() // This was previously here, now using stageStartTime val outHeight = rawOutput[0].size // out1 val outWidth = rawOutput[0][0].size // out2 val shape = interpreter.getOutputTensor(0).shape() // example: [1, 84, 8400] - Log.d("TFLite", "Output shape: " + shape.contentToString()) // // Transpose output ([1][c][w] → [w][c]) // for (i in 0 until outHeight) { @@ -340,9 +323,6 @@ class ObjectDetector( numItemsThreshold = numItemsThreshold, numClasses = labels.size ) - for ((index, boxArray) in resultBoxes.withIndex()) { - Log.d(TAG, "Postprocess result - Box $index: ${boxArray.joinToString(", ")}") - } // Convert to Box list val boxes = mutableListOf() for (boxArray in resultBoxes) { @@ -377,10 +357,8 @@ class ObjectDetector( // val postEnd = System.nanoTime() // This was previously here, now using stageStartTime for end of postprocess var postprocessTimeMs = (System.nanoTime() - stageStartTime) / 1_000_000.0 - Log.d(TAG, "Predict Stage: Postprocessing done in $postprocessTimeMs ms") val totalMs = (System.nanoTime() - overallStartTime) / 1_000_000.0 - Log.d(TAG, "Predict Total time: $totalMs ms (Pre: $preprocessTimeMs, Inf: $inferenceTimeMs, Post: $postprocessTimeMs)") updateTiming() // This updates t0, t1, t2, t3, t4 based on its own logic @@ -395,7 +373,7 @@ class ObjectDetector( // Thresholds (like setConfidenceThreshold, setIouThreshold in TFLiteDetector) private var confidenceThreshold = 0.25f - private var iouThreshold = 0.4f + private var iouThreshold = 0.7f // private var numItemsThreshold = 30 override fun setConfidenceThreshold(conf: Double) { @@ -443,6 +421,6 @@ class ObjectDetector( private val INPUT_IMAGE_TYPE = DataType.FLOAT32 private val OUTPUT_IMAGE_TYPE = DataType.FLOAT32 private const val CONFIDENCE_THRESHOLD = 0.25F - private const val IOU_THRESHOLD = 0.4F + private const val IOU_THRESHOLD = 0.7F } -} \ No newline at end of file +} diff --git a/android/src/main/kotlin/com/ultralytics/yolo/PoseEstimator.kt b/android/src/main/kotlin/com/ultralytics/yolo/PoseEstimator.kt index 827c5263..f4758d63 100644 --- a/android/src/main/kotlin/com/ultralytics/yolo/PoseEstimator.kt +++ b/android/src/main/kotlin/com/ultralytics/yolo/PoseEstimator.kt @@ -32,7 +32,7 @@ class PoseEstimator( override var labels: List, private val useGpu: Boolean = true, private var confidenceThreshold: Float = 0.25f, // Can be changed as needed - private var iouThreshold: Float = 0.45f, // Can be changed as needed + private var iouThreshold: Float = 0.7f, private var numItemsThreshold: Int = 30, private val customOptions: Interpreter.Options? = null ) : BasePredictor() { @@ -100,7 +100,6 @@ class PoseEstimator( if (useGpu) { try { addDelegate(GpuDelegate()) - Log.d("PoseEstimator", "GPU delegate is used.") } catch (e: Exception) { Log.e("PoseEstimator", "GPU delegate error: ${e.message}") } @@ -127,11 +126,11 @@ class PoseEstimator( val modelBuffer = YOLOUtils.loadModelFile(context, modelPath) // ===== Load label information (try Appended ZIP → FlatBuffers in order) ===== - var loadedLabels = YOLOFileUtils.loadLabelsFromAppendedZip(context, modelPath) + val loadedLabels = YOLOFileUtils.loadLabelsFromAppendedZip(context, modelPath) var labelsWereLoaded = loadedLabels != null - if (labelsWereLoaded) { - this.labels = loadedLabels!! // Use labels from appended ZIP + if (loadedLabels != null) { + this.labels = loadedLabels // Use labels from appended ZIP Log.i("PoseEstimator", "Labels successfully loaded from appended ZIP.") } else { Log.w("PoseEstimator", "Could not load labels from appended ZIP, trying FlatBuffers metadata...") @@ -152,7 +151,6 @@ class PoseEstimator( interpreter = Interpreter(modelBuffer, interpreterOptions) // Call allocateTensors() once during initialization interpreter.allocateTensors() - Log.d("PoseEstimator", "TFLite model loaded and tensors allocated") val inputShape = interpreter.getInputTensor(0).shape() val inHeight = inputShape[1] @@ -189,16 +187,10 @@ class PoseEstimator( } } - Log.d( - "PoseEstimator", - "Pose output shape=${outputShape.contentToString()} layout=$outputLayout anchors=$numAnchors" - ) - val inputBytes = 1 * inHeight * inWidth * 3 * 4 // FLOAT32 is 4 bytes inputBuffer = ByteBuffer.allocateDirect(inputBytes).apply { order(java.nio.ByteOrder.nativeOrder()) } - Log.d("PoseEstimator", "Direct ByteBuffer allocated with native ordering: $inputBytes bytes") // For camera feed in portrait mode (with rotation) imageProcessorCameraPortrait = ImageProcessor.Builder() @@ -520,10 +512,8 @@ class PoseEstimator( val files = extractor.associatedFileNames if (!files.isNullOrEmpty()) { for (fileName in files) { - Log.d("PoseEstimator", "Found associated file: $fileName") extractor.getAssociatedFile(fileName)?.use { stream -> val fileString = String(stream.readBytes(), Charsets.UTF_8) - Log.d("PoseEstimator", "Associated file contents:\n$fileString") val yaml = Yaml() @Suppress("UNCHECKED_CAST") @@ -532,14 +522,11 @@ class PoseEstimator( val namesMap = data["names"] as? Map if (namesMap != null) { labels = namesMap.values.toList() - Log.d("PoseEstimator", "Loaded labels from metadata: $labels") return true } } } } - } else { - Log.d("PoseEstimator", "No associated files found in the metadata.") } false } catch (e: Exception) { diff --git a/android/src/main/kotlin/com/ultralytics/yolo/Predictor.kt b/android/src/main/kotlin/com/ultralytics/yolo/Predictor.kt index 8768fddf..3b36b8fa 100644 --- a/android/src/main/kotlin/com/ultralytics/yolo/Predictor.kt +++ b/android/src/main/kotlin/com/ultralytics/yolo/Predictor.kt @@ -43,7 +43,7 @@ abstract class BasePredictor : Predictor { protected var t4: Double = 0.0 var CONFIDENCE_THRESHOLD:Float = 0.25f - var IOU_THRESHOLD:Float = 0.4f + var IOU_THRESHOLD:Float = 0.7f var transformationMatrix: Matrix? = null var pendingBitmapFrame: Bitmap? = null var isFrontCamera: Boolean = false diff --git a/android/src/main/kotlin/com/ultralytics/yolo/Segmenter.kt b/android/src/main/kotlin/com/ultralytics/yolo/Segmenter.kt index 83de2e28..7bf1a858 100644 --- a/android/src/main/kotlin/com/ultralytics/yolo/Segmenter.kt +++ b/android/src/main/kotlin/com/ultralytics/yolo/Segmenter.kt @@ -54,7 +54,6 @@ class Segmenter( if (useGpu) { try { addDelegate(GpuDelegate()) - Log.d("Segmenter", "GPU delegate is used.") } catch (e: Exception) { Log.e("Segmenter", "GPU delegate error: ${e.message}") } @@ -80,11 +79,11 @@ class Segmenter( val modelBuffer = YOLOUtils.loadModelFile(context, modelPath) // ===== Load label information (try Appended ZIP → FlatBuffers in order) ===== - var loadedLabels = YOLOFileUtils.loadLabelsFromAppendedZip(context, modelPath) + val loadedLabels = YOLOFileUtils.loadLabelsFromAppendedZip(context, modelPath) var labelsWereLoaded = loadedLabels != null - if (labelsWereLoaded) { - this.labels = loadedLabels!! // Use labels from appended ZIP + if (loadedLabels != null) { + this.labels = loadedLabels // Use labels from appended ZIP Log.i("Segmenter", "Labels successfully loaded from appended ZIP.") } else { Log.w("Segmenter", "Could not load labels from appended ZIP, trying FlatBuffers metadata...") @@ -112,7 +111,6 @@ class Segmenter( interpreter = Interpreter(modelBuffer, interpreterOptions) // Call allocateTensors() once during initialization interpreter.allocateTensors() - Log.d("Segmenter", "TFLite model loaded and tensors allocated") // Input tensor shape: [1, height, width, 3] val inputShape = interpreter.getInputTensor(0).shape() @@ -427,10 +425,8 @@ class Segmenter( val files = extractor.associatedFileNames if (!files.isNullOrEmpty()) { for (fileName in files) { - Log.d("Segmenter", "Found associated file: $fileName") extractor.getAssociatedFile(fileName)?.use { stream -> val fileString = String(stream.readBytes(), Charsets.UTF_8) - Log.d("Segmenter", "Associated file contents:\n$fileString") val yaml = Yaml() @@ -440,14 +436,11 @@ class Segmenter( val namesMap = data["names"] as? Map if (namesMap != null) { labels = namesMap.values.toList() - Log.d("Segmenter", "Loaded labels from metadata: $labels") return true } } } } - } else { - Log.d("Segmenter", "No associated files found in the metadata.") } false } catch (e: Exception) { diff --git a/android/src/main/kotlin/com/ultralytics/yolo/Utils.kt b/android/src/main/kotlin/com/ultralytics/yolo/Utils.kt index 7893f183..485bd727 100644 --- a/android/src/main/kotlin/com/ultralytics/yolo/Utils.kt +++ b/android/src/main/kotlin/com/ultralytics/yolo/Utils.kt @@ -62,28 +62,23 @@ object YOLOUtils { */ fun loadModelFile(context: Context, modelPath: String): java.nio.MappedByteBuffer { val finalModelPath = ensureTFLiteExtension(modelPath) - Log.d(TAG, "Loading model from path: $finalModelPath") - + try { // Check if it's an absolute path and the file exists if (isAbsolutePath(finalModelPath) && fileExistsAtPath(finalModelPath)) { - Log.d(TAG, "Loading model from absolute path: $finalModelPath") return loadModelFromFilesystem(finalModelPath) } else { // Try loading from assets - Log.d(TAG, "Loading model from assets: $finalModelPath") return FileUtil.loadMappedFile(context, finalModelPath) } } catch (e: Exception) { Log.e(TAG, "Failed to load model with path: $finalModelPath, error: ${e.message}") - + // If the model with extension can't be found, try the original path as a fallback try { if (isAbsolutePath(modelPath) && fileExistsAtPath(modelPath)) { - Log.d(TAG, "Loading model from absolute path (fallback): $modelPath") return loadModelFromFilesystem(modelPath) } else { - Log.d(TAG, "Loading model from assets (fallback): $modelPath") return FileUtil.loadMappedFile(context, modelPath) } } catch (e2: Exception) { diff --git a/android/src/main/kotlin/com/ultralytics/yolo/YOLO.kt b/android/src/main/kotlin/com/ultralytics/yolo/YOLO.kt index 4f763e5c..4a1f6b68 100644 --- a/android/src/main/kotlin/com/ultralytics/yolo/YOLO.kt +++ b/android/src/main/kotlin/com/ultralytics/yolo/YOLO.kt @@ -45,9 +45,6 @@ class YOLO( // Allow FP16 precision for faster computation setAllowFp16PrecisionForFp32(true) - - // Log configuration - Log.d(TAG, "Interpreter options: threads=${Runtime.getRuntime().availableProcessors()}, FP16 enabled") } } catch (e: Exception) { Log.e(TAG, "Error creating interpreter options: ${e.message}") @@ -93,7 +90,6 @@ class YOLO( // Don't create annotated image for classification tasks to save memory and processing time val annotatedImage = if (task == YOLOTask.CLASSIFY) { - Log.d(TAG, "Skipping annotation for CLASSIFY task") null } else { drawAnnotations(bitmap, result, rotateForCamera) diff --git a/android/src/main/kotlin/com/ultralytics/yolo/YOLOFileUtils.kt b/android/src/main/kotlin/com/ultralytics/yolo/YOLOFileUtils.kt index c2569ab1..7b78a1aa 100644 --- a/android/src/main/kotlin/com/ultralytics/yolo/YOLOFileUtils.kt +++ b/android/src/main/kotlin/com/ultralytics/yolo/YOLOFileUtils.kt @@ -94,10 +94,10 @@ object YOLOFileUtils { Channels.newInputStream(fileChannel).use { channelIs -> BufferedInputStream(channelIs).use { bis -> ZipInputStream(bis).use { zis -> - var entry: ZipEntry? - while (zis.nextEntry.also { entry = it } != null) { - val entryName = entry!!.name - if (entry!!.isDirectory) continue + while (true) { + val entry: ZipEntry = zis.nextEntry ?: break + if (entry.isDirectory) continue + val entryName = entry.name if (entryName != "TFLITE_ULTRALYTICS_METADATA.json" && entryName != "metadata.json") { continue } @@ -160,7 +160,6 @@ object YOLOFileUtils { } private fun closeResources(afd: AssetFileDescriptor?, fis: FileInputStream?, fileChannel: FileChannel?, reason: String) { - Log.d(TAG, "Appended ZIP: Closing resources ($reason).") try { fileChannel?.close() } catch (e: IOException) { Log.e(TAG, "Error closing FileChannel", e) } try { fis?.close() } catch (e: IOException) { Log.e(TAG, "Error closing FileInputStream", e) } try { afd?.close() } catch (e: IOException) { Log.e(TAG, "Error closing AssetFileDescriptor", e) } diff --git a/android/src/main/kotlin/com/ultralytics/yolo/YOLOInstanceManager.kt b/android/src/main/kotlin/com/ultralytics/yolo/YOLOInstanceManager.kt index f5132e53..cc1c8bd5 100644 --- a/android/src/main/kotlin/com/ultralytics/yolo/YOLOInstanceManager.kt +++ b/android/src/main/kotlin/com/ultralytics/yolo/YOLOInstanceManager.kt @@ -35,7 +35,6 @@ object YOLOInstanceManager { fun createInstance(instanceId: String) { // Just register the ID, actual YOLO instance created on load loadingStates[instanceId] = false - Log.d(TAG, "Created instance placeholder: $instanceId") } /** @@ -100,14 +99,12 @@ object YOLOInstanceManager { // Store classifier options if provided classifierOptions?.let { options -> instanceOptions[instanceId] = options - Log.d(TAG, "Stored classifier options for instance $instanceId: $options") } // Create YOLO instance with the specified parameters val yolo = YOLO(context, modelPath, task, emptyList(), useGpu, numItemsThreshold, classifierOptions) instances[instanceId] = yolo loadingStates[instanceId] = false - Log.d(TAG, "Model loaded successfully for instance: $instanceId ${if (classifierOptions != null) "with classifier options" else ""}") callback(Result.success(Unit)) } catch (e: Exception) { loadingStates[instanceId] = false @@ -171,7 +168,6 @@ object YOLOInstanceManager { instances[instanceId]?.let { yolo -> try { // YOLO class doesn't have a close() method, just remove from map - Log.d(TAG, "Disposing instance: $instanceId") } catch (e: Exception) { Log.e(TAG, "Error disposing instance $instanceId: ${e.message}") } @@ -194,7 +190,6 @@ object YOLOInstanceManager { fun disposeAll() { val allIds = instances.keys.toList() allIds.forEach { dispose(it) } - Log.d(TAG, "Disposed all ${allIds.size} instances") } /** diff --git a/android/src/main/kotlin/com/ultralytics/yolo/YOLOPlatformView.kt b/android/src/main/kotlin/com/ultralytics/yolo/YOLOPlatformView.kt index 755465ea..dd325f9c 100644 --- a/android/src/main/kotlin/com/ultralytics/yolo/YOLOPlatformView.kt +++ b/android/src/main/kotlin/com/ultralytics/yolo/YOLOPlatformView.kt @@ -50,20 +50,18 @@ class YOLOPlatformView( private var retryRunnable: Runnable? = null init { - Log.d(TAG, "YOLOPlatformView[$viewId] INIT BLOCK STARTING") val dartViewIdParam = creationParams?.get("viewId") viewUniqueId = dartViewIdParam as? String ?: viewId.toString().also { Log.w(TAG, "YOLOPlatformView[$viewId init]: Using platform int viewId '$it' as fallback") } - Log.d(TAG, "YOLOPlatformView[$viewId init]: Initialized with viewUniqueId: $viewUniqueId") // Parse model path and task from creation params var modelPath = creationParams?.get("modelPath") as? String ?: "yolo26n" val taskString = creationParams?.get("task") as? String ?: "detect" - val confidenceParam = creationParams?.get("confidenceThreshold") as? Double ?: 0.5 - val iouParam = creationParams?.get("iouThreshold") as? Double ?: 0.45 + val confidenceParam = creationParams?.get("confidenceThreshold") as? Double ?: 0.25 + val iouParam = creationParams?.get("iouThreshold") as? Double ?: 0.7 val showOverlaysParam = creationParams?.get("showOverlays") as? Boolean ?: true - + // Parse lensFacing parameter val lensFacingParam = creationParams?.get("lensFacing") as? String ?: "back" val lensFacing = when (lensFacingParam.lowercase()) { @@ -75,40 +73,34 @@ class YOLOPlatformView( methodChannel?.setMethodCallHandler(this) // Set initial thresholds - Log.d(TAG, "Setting initial thresholds: conf=$confidenceParam, iou=$iouParam") yoloView.setConfidenceThreshold(confidenceParam) yoloView.setIouThreshold(iouParam) yoloView.setShowOverlays(showOverlaysParam) - + // Set lens facing before initializing camera - Log.d(TAG, "Setting lens facing: $lensFacingParam") yoloView.setLensFacing(lensFacing) - + // Configure YOLOView streaming functionality setupYOLOViewStreaming(creationParams) // Notify lifecycle FIRST before initializing camera // This ensures lifecycle owner is set before camera tries to start if (context is LifecycleOwner) { - Log.d(TAG, "Initial context is a LifecycleOwner, notifying YOLOView") yoloView.onLifecycleOwnerAvailable(context) // initCamera will be called by onLifecycleOwnerAvailable if permissions are granted // But we also call it here as a fallback - Log.d(TAG, "Attempting camera initialization after lifecycle owner setup") yoloView.initCamera() } else { Log.w(TAG, "Context is not a LifecycleOwner, camera may not start") // Still try to initialize camera - it will request permissions if needed yoloView.initCamera() } - + try { // Resolve model path modelPath = resolveModelPath(context, modelPath) val task = YOLOTask.valueOf(taskString.uppercase()) - - Log.d(TAG, "Initializing with model: $modelPath, task: $task") - + // Set up model loading callback yoloView.setOnModelLoadCallback { success -> if (success) { @@ -183,8 +175,6 @@ class YOLOPlatformView( yoloView.setStreamCallback { streamData -> sendStreamDataWithRetry(streamData) } - - Log.d(TAG, "YOLOView streaming configured with setState resilience") } /** @@ -261,8 +251,6 @@ class YOLOPlatformView( retryRunnable = Runnable { if (isStreaming.get()) { - Log.d(TAG, "Retrying to send stream data") - // Check if sink is available if (streamHandler.sink != null) { // Resend last data if available @@ -271,7 +259,6 @@ class YOLOPlatformView( } } else { // Request Flutter to recreate the event channel - Log.d(TAG, "Requesting Flutter to reconnect event channel") methodChannel?.invokeMethod("reconnectEventChannel", mapOf( "viewId" to viewUniqueId, "reason" to "sink_disconnected" @@ -292,8 +279,6 @@ class YOLOPlatformView( */ private fun startStreaming() { if (isStreaming.compareAndSet(false, true)) { - Log.d(TAG, "Started streaming for view $viewId") - // Send initial test message to verify connection sendStreamData(mapOf( "test" to "Streaming started", @@ -302,13 +287,12 @@ class YOLOPlatformView( )) } } - + /** * Stop streaming */ private fun stopStreaming() { if (isStreaming.compareAndSet(true, false)) { - Log.d(TAG, "Stopped streaming for view $viewId") retryRunnable?.let { retryHandler.removeCallbacks(it) } retryRunnable = null } @@ -379,7 +363,6 @@ class YOLOPlatformView( yoloView.setModel(modelPath, task, useGpu) { success -> if (success) { - Log.d(TAG, "Model switched successfully") result.success(null) } else { Log.e(TAG, "Failed to switch model") @@ -428,7 +411,6 @@ class YOLOPlatformView( skipFrames = (configMap["skipFrames"] as? Number)?.toInt() ) yoloView.setStreamConfig(streamConfig) - Log.d(TAG, "Streaming config updated") result.success(null) } else { result.error("invalid_args", "Invalid streaming config", null) @@ -436,18 +418,15 @@ class YOLOPlatformView( } "stop" -> { yoloView.stop() - Log.d(TAG, "Camera and inference stopped") result.success(null) } "restartCamera" -> { - Log.d(TAG, "Restarting camera (requested from Flutter)") yoloView.startCamera() result.success(null) } "captureFrame" -> { val imageData = yoloView.captureFrame() if (imageData != null) { - Log.d(TAG, "Frame captured: ${imageData.size} bytes") result.success(imageData) } else { result.error("capture_failed", "Failed to capture frame", null) @@ -455,7 +434,6 @@ class YOLOPlatformView( } "reconnectStream" -> { // Handle reconnection request from Flutter - Log.d(TAG, "Received reconnect request from Flutter") startStreaming() result.success(null) } @@ -479,10 +457,8 @@ class YOLOPlatformView( } override fun dispose() { - Log.d(TAG, "Disposing YOLOPlatformView for viewId: $viewId") - stopStreaming() - + try { yoloView.stop() // Clear callbacks by setting them to empty implementations @@ -492,11 +468,9 @@ class YOLOPlatformView( } catch (e: Exception) { Log.e(TAG, "Error during disposal", e) } - + methodChannel?.setMethodCallHandler(null) factory.onPlatformViewDisposed(viewId) - - Log.d(TAG, "YOLOPlatformView disposed successfully") } private fun resolveModelPath(context: Context, modelPath: String): String { diff --git a/android/src/main/kotlin/com/ultralytics/yolo/YOLOPlatformViewFactory.kt b/android/src/main/kotlin/com/ultralytics/yolo/YOLOPlatformViewFactory.kt index c91b4b74..c97bce0a 100644 --- a/android/src/main/kotlin/com/ultralytics/yolo/YOLOPlatformViewFactory.kt +++ b/android/src/main/kotlin/com/ultralytics/yolo/YOLOPlatformViewFactory.kt @@ -20,13 +20,11 @@ class CustomStreamHandler(private val viewId: Int) : EventChannel.StreamHandler @Volatile var sink: EventChannel.EventSink? = null private val TAG = "CustomStreamHandler" - + // Add a timestamp to track when the sink was last set private var sinkSetTime: Long = 0 - + override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { - Log.d(TAG, "Event channel for view $viewId started listening") - // Ensure we're on the main thread for sink operations if (android.os.Looper.myLooper() != android.os.Looper.getMainLooper()) { Log.w(TAG, "onListen not called on main thread! Current thread: ${Thread.currentThread().name}") @@ -38,41 +36,36 @@ class CustomStreamHandler(private val viewId: Int) : EventChannel.StreamHandler handleOnListen(arguments, events) } } - + private fun handleOnListen(arguments: Any?, events: EventChannel.EventSink?) { sink = events sinkSetTime = System.currentTimeMillis() - Log.d(TAG, "Sink set on main thread at ${sinkSetTime}, sink: $sink") - + if (events == null) { Log.w(TAG, "onListen called with null EventSink!") } else { // Test the sink by sending a simple message try { - Log.d(TAG, "Testing event sink with a test message") events.success(mapOf( - "test" to "Event channel active", + "test" to "Event channel active", "viewId" to viewId, "timestamp" to System.currentTimeMillis() )) - Log.d(TAG, "Test message sent successfully") } catch (e: Exception) { Log.e(TAG, "Error sending test message to event sink", e) } - + // Schedule a delayed test message to verify sink stays active val mainHandler = android.os.Handler(android.os.Looper.getMainLooper()) mainHandler.postDelayed({ try { if (sink != null) { - Log.d(TAG, "Sending delayed test message to verify sink") sink?.success(mapOf( - "test" to "Event sink verification", + "test" to "Event sink verification", "viewId" to viewId, "timestamp" to System.currentTimeMillis(), "sinkAge" to (System.currentTimeMillis() - sinkSetTime) )) - Log.d(TAG, "Delayed test message sent successfully") } else { Log.w(TAG, "Sink no longer available for delayed test") } @@ -82,32 +75,28 @@ class CustomStreamHandler(private val viewId: Int) : EventChannel.StreamHandler }, 1000) // 1 second delay } } - + override fun onCancel(arguments: Any?) { - Log.d(TAG, "Event channel for view $viewId canceled after ${System.currentTimeMillis() - sinkSetTime}ms, clearing sink") - // Ensure we're on the main thread for sink operations if (android.os.Looper.myLooper() != android.os.Looper.getMainLooper()) { val mainHandler = android.os.Handler(android.os.Looper.getMainLooper()) mainHandler.post { sink = null - Log.d(TAG, "Sink cleared on main thread") } } else { sink = null - Log.d(TAG, "Sink cleared directly") } } - + // Method to check if sink is valid fun isSinkValid(): Boolean { return sink != null } - + // Method to safely send a message fun safelySend(data: Map): Boolean { if (sink == null) return false - + try { // Always send on main thread if (android.os.Looper.myLooper() != android.os.Looper.getMainLooper()) { @@ -142,48 +131,40 @@ class YOLOPlatformViewFactory( internal val activeViews = mutableMapOf() // Map to store event channel handlers, keyed by viewId private val eventChannelHandlers = mutableMapOf() - + // Store activity reference to pass to the YOLOPlatformView fun setActivity(activity: Activity?) { this.activity = activity - Log.d(TAG, "Activity set: ${activity?.javaClass?.simpleName}") } - + override fun create(context: Context, viewId: Int, args: Any?): PlatformView { val creationParams = args as? Map - + // Use activity if available, otherwise use the provided context val effectiveContext = activity ?: context - Log.d(TAG, "Creating YOLOPlatformView with context: ${effectiveContext.javaClass.simpleName}, platform int viewId: $viewId") - Log.d(TAG, "Raw creationParams from Dart: $args") // Get the unique ID for this view val dartViewIdParam = creationParams?.get("viewId") - Log.d(TAG, "Extracted 'viewId' from creationParams: $dartViewIdParam (type: ${dartViewIdParam?.javaClass?.name})") val viewUniqueId = dartViewIdParam as? String ?: viewId.toString().also { Log.w(TAG, "Using platform int viewId '$it' as fallback for viewUniqueId because Dart 'viewId' was null or not a String.") } - Log.d(TAG, "Resolved viewUniqueId for channel naming: $viewUniqueId") - + // Create event channel for detection results val resultChannelName = "com.ultralytics.yolo/detectionResults_$viewUniqueId" val controlChannelName = "com.ultralytics.yolo/controlChannel_$viewUniqueId" - - Log.d(TAG, "Final channel names - Result: $resultChannelName, Control: $controlChannelName") - + // Event channel for streaming detection results val eventChannel = EventChannel(messenger, resultChannelName) // Method channel for controlling the view val methodChannel = MethodChannel(messenger, controlChannelName) - + // Create stream handler for detection results val eventHandler = CustomStreamHandler(viewId) - Log.d(TAG, "Created CustomStreamHandler for view $viewId") - + // Set event handler and store it eventChannel.setStreamHandler(eventHandler) eventChannelHandlers[viewId] = eventHandler - + // Create the platform view with stream handler, not just the sink val platformView = YOLOPlatformView( effectiveContext, @@ -193,26 +174,23 @@ class YOLOPlatformViewFactory( methodChannel, this // Pass the factory itself for disposal callback ) - + // Set up method channel handler for the control channel methodChannel.setMethodCallHandler(platformView) - + activeViews[viewId] = platformView - Log.d(TAG, "Created and stored YOLOPlatformView for viewId: $viewId") return platformView } - + // Called by YOLOPlatformView when it's disposed internal fun onPlatformViewDisposed(viewId: Int) { activeViews.remove(viewId) eventChannelHandlers.remove(viewId) // Assuming CustomStreamHandler doesn't need explicit cancel on its EventChannel - Log.d(TAG, "YOLOPlatformView for viewId $viewId disposed and removed from factory.") } - + fun dispose() { // Called when the FlutterEngine is detached // Clean up event channels when the plugin is disposed eventChannelHandlers.clear() activeViews.clear() - Log.d(TAG, "YOLOPlatformViewFactory disposed, all views and handlers cleared.") } -} \ No newline at end of file +} diff --git a/android/src/main/kotlin/com/ultralytics/yolo/YOLOPlugin.kt b/android/src/main/kotlin/com/ultralytics/yolo/YOLOPlugin.kt index e4771ea1..c41af3e9 100644 --- a/android/src/main/kotlin/com/ultralytics/yolo/YOLOPlugin.kt +++ b/android/src/main/kotlin/com/ultralytics/yolo/YOLOPlugin.kt @@ -52,20 +52,16 @@ class YOLOPlugin : FlutterPlugin, ActivityAware, MethodChannel.MethodCallHandler "yolo_single_image_channel" ) methodChannel.setMethodCallHandler(this) - - Log.d(TAG, "YOLOPlugin attached to engine") } - + override fun onAttachedToActivity(binding: ActivityPluginBinding) { activity = binding.activity activityBinding = binding // Store the binding viewFactory.setActivity(activity) activityBinding?.addRequestPermissionsResultListener(this) - Log.d(TAG, "YOLOPlugin attached to activity: ${activity?.javaClass?.simpleName}, stored binding, and added RequestPermissionsResultListener") } override fun onDetachedFromActivityForConfigChanges() { - Log.d(TAG, "YOLOPlugin detached from activity for config changes. Listener will be removed in onDetachedFromActivity.") // activity and viewFactory.setActivity(null) will be handled by onDetachedFromActivity // activityBinding will also be cleared in onDetachedFromActivity } @@ -75,21 +71,17 @@ class YOLOPlugin : FlutterPlugin, ActivityAware, MethodChannel.MethodCallHandler activityBinding = binding // Store the new binding viewFactory.setActivity(activity) activityBinding?.addRequestPermissionsResultListener(this) // Add listener with new binding - Log.d(TAG, "YOLOPlugin reattached to activity: ${activity?.javaClass?.simpleName}, stored new binding, and re-added RequestPermissionsResultListener") } override fun onDetachedFromActivity() { - Log.d(TAG, "YOLOPlugin detached from activity") activityBinding?.removeRequestPermissionsResultListener(this) activityBinding = null activity = null viewFactory.setActivity(null) - Log.d(TAG, "Cleared activity, activityBinding, and removed RequestPermissionsResultListener") } override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { methodChannel.setMethodCallHandler(null) - Log.d(TAG, "YoloPlugin detached from engine") // Clean up view factory resources viewFactory.dispose() // YOLO class doesn't need explicit release @@ -169,12 +161,7 @@ class YOLOPlugin : FlutterPlugin, ActivityAware, MethodChannel.MethodCallHandler // Use classifier options map directly (follows existing pattern) val classifierOptions = classifierOptionsMap - - // Log classifier options for debugging - if (classifierOptions != null) { - Log.d(TAG, "Parsed classifier options: $classifierOptions") - } - + // Load labels (in real implementation, you would load from metadata) val labels = loadLabels(modelPath) @@ -188,11 +175,7 @@ class YOLOPlugin : FlutterPlugin, ActivityAware, MethodChannel.MethodCallHandler numItemsThreshold = numItemsThreshold, classifierOptions = classifierOptions ) { loadResult -> - if(task == YOLOTask.CLASSIFY){ - Log.d(TAG,"task CLASSIFY not support numItemsThreshold ignore it.") - } if (loadResult.isSuccess) { - Log.d(TAG, "Model loaded successfully: $modelPath for task: $task, instance: $instanceId, useGpu: $useGpu ${if (classifierOptions != null) "with classifier options" else ""}") result.success(true) } else { Log.e(TAG, "Failed to load model for instance $instanceId", loadResult.exceptionOrNull()) @@ -534,14 +517,11 @@ class YOLOPlugin : FlutterPlugin, ActivityAware, MethodChannel.MethodCallHandler permissions: Array, grantResults: IntArray ): Boolean { - Log.d(TAG, "onRequestPermissionsResult called in YoloPlugin. requestCode: $requestCode, activeViews: ${viewFactory.activeViews.size}") var handled = false // Iterate over a copy of the values to avoid concurrent modification issues. val viewsToNotify = ArrayList(viewFactory.activeViews.values) for (platformView in viewsToNotify) { try { - // Log that we're processing permission results - Log.d(TAG, "Processing permission result for YOLOPlatformView") handled = true // Assuming only one view actively requests permissions at a time. // If multiple views could request, 'handled' logic might need adjustment @@ -554,8 +534,6 @@ class YOLOPlugin : FlutterPlugin, ActivityAware, MethodChannel.MethodCallHandler if (!handled && viewsToNotify.isNotEmpty()) { // This log means we iterated views but none seemed to handle it, or an exception occurred. Log.w(TAG, "onRequestPermissionsResult was iterated but not confirmed handled by any YoloPlatformView, or an error occurred during delegation.") - } else if (viewsToNotify.isEmpty()) { - Log.d(TAG, "onRequestPermissionsResult: No active YoloPlatformViews to notify.") } return handled // Return true if any view instance successfully processed it. } diff --git a/android/src/main/kotlin/com/ultralytics/yolo/YOLOView.kt b/android/src/main/kotlin/com/ultralytics/yolo/YOLOView.kt index fefa2622..afb5ac02 100644 --- a/android/src/main/kotlin/com/ultralytics/yolo/YOLOView.kt +++ b/android/src/main/kotlin/com/ultralytics/yolo/YOLOView.kt @@ -163,18 +163,13 @@ class YOLOView @JvmOverloads constructor( /** Set streaming configuration */ fun setStreamConfig(config: YOLOStreamConfig?) { - Log.d(TAG, "🔄 Setting new streaming config") - Log.d(TAG, "📋 Previous config: $streamConfig") this.streamConfig = config setupThrottlingFromConfig() - Log.d(TAG, "✅ New streaming config set: $config") - Log.d(TAG, "🎯 Key settings - includeMasks: ${config?.includeMasks}, includeProcessingTimeMs: ${config?.includeProcessingTimeMs}, inferenceFrequency: ${config?.inferenceFrequency}") } - + /** Set streaming callback */ fun setStreamCallback(callback: ((Map) -> Unit)?) { this.streamCallback = callback - Log.d(TAG, "Streaming callback set: ${callback != null}") } // Callback to notify model load completion @@ -222,7 +217,7 @@ class YOLOView @JvmOverloads constructor( // detection thresholds (can be changed externally via setters) private var confidenceThreshold = 0.25 // initial value - private var iouThreshold = 0.45 + private var iouThreshold = 0.7 private var numItemsThreshold = 30 private var showOverlays = true private lateinit var zoomLabel: TextView @@ -342,8 +337,6 @@ class YOLOView @JvmOverloads constructor( return true } }) - - Log.d(TAG, "YoloView init: forced TextureView usage for camera preview + overlay on top.") } // region threshold setters @@ -453,13 +446,11 @@ class YOLOView @JvmOverloads constructor( // Try to load labels from model metadata first val loadedLabels = YOLOFileUtils.loadLabelsFromAppendedZip(context, modelPath) if (loadedLabels != null) { - Log.d(TAG, "Labels loaded from model metadata: ${loadedLabels.size} classes") return loadedLabels } - + // Return COCO dataset's 80 classes as a fallback // This is much more complete than the previous 7-class hardcoded list - Log.d(TAG, "Using COCO classes as fallback") return listOf( "person", "bicycle", "car", "motorcycle", "airplane", "bus", "train", "truck", "boat", "traffic light", "fire hydrant", "stop sign", "parking meter", "bench", "bird", "cat", "dog", @@ -531,7 +522,6 @@ class YOLOView @JvmOverloads constructor( cameraProviderFuture.addListener({ try { val cameraProvider = cameraProviderFuture.get() - Log.d(TAG, "Camera provider obtained") previewUseCase = Preview.Builder() .setTargetAspectRatio(AspectRatio.RATIO_4_3) @@ -551,7 +541,6 @@ class YOLOView @JvmOverloads constructor( .requireLensFacing(lensFacing) .build() - Log.d(TAG, "Unbinding all camera use cases") cameraProvider.unbindAll() try { @@ -561,31 +550,26 @@ class YOLOView @JvmOverloads constructor( return@addListener } - Log.d(TAG, "Binding camera use cases to lifecycle") camera = cameraProvider.bindToLifecycle( owner, cameraSelector, previewUseCase, imageAnalysisUseCase // the field, not a local val ) - + // Reset zoom to 1.0x when camera starts currentZoomRatio = 1.0f onZoomChanged?.invoke(currentZoomRatio) - Log.d(TAG, "Setting surface provider to previewView") previewUseCase?.setSurfaceProvider(previewView.surfaceProvider) - + // Initialize zoom camera?.let { cam: Camera -> val cameraInfo = cam.cameraInfo minZoomRatio = cameraInfo.zoomState.value?.minZoomRatio ?: 1.0f maxZoomRatio = cameraInfo.zoomState.value?.maxZoomRatio ?: 1.0f currentZoomRatio = cameraInfo.zoomState.value?.zoomRatio ?: 1.0f - Log.d(TAG, "Zoom initialized - min: $minZoomRatio, max: $maxZoomRatio, current: $currentZoomRatio") } - - Log.d(TAG, "Camera setup completed successfully") } catch (e: Exception) { Log.e(TAG, "Use case binding failed", e) } @@ -619,32 +603,25 @@ class YOLOView @JvmOverloads constructor( // Lifecycle methods from DefaultLifecycleObserver override fun onStart(owner: LifecycleOwner) { - Log.d(TAG, "Lifecycle onStart - restarting camera if stopped") if (allPermissionsGranted()) { // Always restart camera on start if it's stopped or null // This ensures camera resumes when navigating back if (isStopped || camera == null) { - Log.d(TAG, "Camera is stopped or null, restarting on onStart") startCamera() - } else { - Log.d(TAG, "Camera is already running, no restart needed") } } } - + override fun onResume(owner: LifecycleOwner) { - Log.d(TAG, "Lifecycle onResume - ensuring camera is running") if (allPermissionsGranted()) { // Double-check camera is running on resume if (isStopped || camera == null) { - Log.d(TAG, "Camera not running on resume, restarting...") startCamera() } } } override fun onStop(owner: LifecycleOwner) { - Log.d(TAG, "Lifecycle onStop") // Camera will be automatically stopped by CameraX when lifecycle stops } @@ -653,11 +630,10 @@ class YOLOView @JvmOverloads constructor( private fun onFrame(imageProxy: ImageProxy) { // Early return if view is stopped to prevent accessing closed resources if (isStopped) { - Log.d(TAG, "onFrame: View is stopped, skipping frame processing") imageProxy.close() return } - + val w = imageProxy.width val h = imageProxy.height val orientation = context.resources.configuration.orientation @@ -671,7 +647,6 @@ class YOLOView @JvmOverloads constructor( // Check again after bitmap conversion (in case stop() was called during conversion) if (isStopped) { - Log.d(TAG, "onFrame: View stopped during bitmap conversion, skipping inference") imageProxy.close() return } @@ -679,14 +654,12 @@ class YOLOView @JvmOverloads constructor( predictor?.let { p -> // Double-check stopped flag before inference (predictor might be closed) if (isStopped) { - Log.d(TAG, "onFrame: View stopped before inference, skipping") imageProxy.close() return } - + // Check if we should run inference on this frame if (!shouldRunInference()) { - Log.d(TAG, "Skipping inference due to frequency control") imageProxy.close() return } @@ -738,8 +711,6 @@ class YOLOView @JvmOverloads constructor( enhancedStreamData["frameNumber"] = frameNumberCounter++ callback.invoke(enhancedStreamData) - } else { - Log.d(TAG, "Skipping frame output due to throttling") } } @@ -776,8 +747,6 @@ class YOLOView @JvmOverloads constructor( // Make overlay not intercept touch events isClickable = false isFocusable = false - - Log.d(TAG, "OverlayView initialized with enhanced Z-order + hardware acceleration") } override fun onDraw(canvas: Canvas) { @@ -821,16 +790,6 @@ class YOLOView @JvmOverloads constructor( // DETECT // ---------------------------------------- YOLOTask.DETECT -> { - Log.d(TAG, "Drawing DETECT boxes: ${result.boxes.size}") - - // Debug first box coordinates - if (result.boxes.isNotEmpty()) { - val firstBox = result.boxes[0] - Log.d(TAG, "=== First Box Debug ===") - Log.d(TAG, "Box normalized coords: (${firstBox.xywhn.left}, ${firstBox.xywhn.top}, ${firstBox.xywhn.right}, ${firstBox.xywhn.bottom})") - Log.d(TAG, "Box pixel coords: (${firstBox.xywh.left}, ${firstBox.xywh.top}, ${firstBox.xywh.right}, ${firstBox.xywh.bottom})") - } - for (box in result.boxes) { val alpha = (box.conf * 255).toInt().coerceIn(0, 255) val baseColor = ultralyticsColors[box.index % ultralyticsColors.size] @@ -877,8 +836,6 @@ class YOLOView @JvmOverloads constructor( left = flippedLeft right = flippedRight } - - Log.d(TAG, "Drawing box for ${box.cls}: L=$left, T=$top, R=$right, B=$bottom, conf=${box.conf}") paint.color = newColor paint.style = Paint.Style.STROKE @@ -1410,48 +1367,40 @@ class YOLOView @JvmOverloads constructor( config.maxFPS?.let { maxFPS -> if (maxFPS > 0) { targetFrameInterval = (1_000_000_000L / maxFPS) // Convert to nanoseconds - Log.d(TAG, "maxFPS throttling enabled - target FPS: $maxFPS, interval: ${targetFrameInterval!! / 1_000_000}ms") } } ?: run { targetFrameInterval = null - Log.d(TAG, "maxFPS throttling disabled") } - + // Setup throttleInterval (for result output) config.throttleIntervalMs?.let { throttleMs -> if (throttleMs > 0) { throttleInterval = throttleMs * 1_000_000L // Convert ms to nanoseconds - Log.d(TAG, "throttleInterval enabled - interval: ${throttleMs}ms") } } ?: run { throttleInterval = null - Log.d(TAG, "throttleInterval disabled") } - + // Setup inference frequency control config.inferenceFrequency?.let { inferenceFreq -> if (inferenceFreq > 0) { inferenceFrameInterval = (1_000_000_000L / inferenceFreq) // Convert to nanoseconds - Log.d(TAG, "Inference frequency control enabled - target inference FPS: $inferenceFreq, interval: ${inferenceFrameInterval!! / 1_000_000}ms") } } ?: run { inferenceFrameInterval = null - Log.d(TAG, "Inference frequency control disabled") } - + // Setup frame skipping config.skipFrames?.let { skipFrames -> if (skipFrames > 0) { targetSkipFrames = skipFrames frameSkipCount = 0 // Reset counter - Log.d(TAG, "Frame skipping enabled - skip $skipFrames frames between inferences") } } ?: run { targetSkipFrames = 0 frameSkipCount = 0 - Log.d(TAG, "Frame skipping disabled") } - + // Initialize timing lastInferenceTime = System.nanoTime() } @@ -1582,8 +1531,7 @@ class YOLOView @JvmOverloads constructor( val keypointsFlat = flattenKeypoints(keypoints) detection["keypoints"] = keypointsFlat - Log.d(TAG, "Added pose detection with ${keypoints.xy.size} keypoints") - + detections.add(detection) } } @@ -1619,7 +1567,6 @@ class YOLOView @JvmOverloads constructor( row.map { it.toDouble() } } detection["mask"] = maskDataDouble - Log.d(TAG, "✅ Added mask data (${maskData.size}x${maskData.firstOrNull()?.size ?: 0}) for detection $detectionIndex") } // Add pose keypoints (if available and enabled) @@ -1628,7 +1575,6 @@ class YOLOView @JvmOverloads constructor( val keypoints = result.keypointsList[detectionIndex] val keypointsFlat = flattenKeypoints(keypoints) detection["keypoints"] = keypointsFlat - Log.d(TAG, "Added keypoints data (${keypoints.xy.size} points) for detection $detectionIndex") } } @@ -1711,14 +1657,12 @@ class YOLOView @JvmOverloads constructor( ) detection["obb"] = obbDataMap - Log.d(TAG, "✅ Added OBB data: ${obbRes.cls} (${String.format("%.1f", obbRes.box.angle * 180.0 / Math.PI)}° rotation)") } detections.add(detection) } 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) @@ -1774,8 +1718,6 @@ class YOLOView @JvmOverloads constructor( if (config.includeProcessingTimeMs) { val processingTimeMs = result.speed.toDouble() map["processingTimeMs"] = processingTimeMs - } else { - Log.d(TAG, "⚠️ Skipping processingTimeMs (includeProcessingTimeMs=${config.includeProcessingTimeMs})") } if (config.includeFps) { @@ -1789,7 +1731,6 @@ class YOLOView @JvmOverloads constructor( bitmap.compress(Bitmap.CompressFormat.JPEG, 90, outputStream) val imageData = outputStream.toByteArray() map["originalImage"] = imageData - Log.d(TAG, "✅ Added original image data (${imageData.size} bytes)") } } @@ -1819,7 +1760,6 @@ class YOLOView @JvmOverloads constructor( // Method 1: Try to get bitmap from PreviewView directly var cameraFrameCaptured = false previewView.bitmap?.let { cameraBitmap -> - Log.d(TAG, "Got camera bitmap from PreviewView: ${cameraBitmap.width}x${cameraBitmap.height}") // Draw the camera bitmap scaled to fit val matrix = Matrix() val scaleX = width.toFloat() / cameraBitmap.width @@ -1861,8 +1801,7 @@ class YOLOView @JvmOverloads constructor( // Clean up outputStream.close() bitmap.recycle() - - Log.d(TAG, "Frame captured successfully: ${imageData.size} bytes, camera captured: $cameraFrameCaptured") + return imageData } catch (e: Exception) { Log.e(TAG, "Error capturing frame", e) @@ -1874,8 +1813,6 @@ class YOLOView @JvmOverloads constructor( * Stop camera and inference (can be restarted later) */ fun stop() { - Log.d(TAG, "YOLOView.stop() called - tearing down camera") - // Set stopped flag first to prevent new frames from being processed isStopped = true @@ -1884,7 +1821,6 @@ class YOLOView @JvmOverloads constructor( if (::cameraProviderFuture.isInitialized) { try { val cameraProvider = cameraProviderFuture.get(1, TimeUnit.SECONDS) - Log.d(TAG, "Unbinding all camera use cases") cameraProvider.unbindAll() } catch (e: Exception) { Log.e(TAG, "Error getting camera provider for unbind", e) @@ -1897,7 +1833,6 @@ class YOLOView @JvmOverloads constructor( previewUseCase = null cameraExecutor?.let { exec -> - Log.d(TAG, "Shutting down camera executor") exec.shutdown() try { if (!exec.awaitTermination(500, TimeUnit.MILLISECONDS)) { @@ -1927,8 +1862,6 @@ class YOLOView @JvmOverloads constructor( inferenceCallback = null streamCallback = null inferenceResult = null - - Log.d(TAG, "YOLOView stop completed successfully") } catch (e: Exception) { Log.e(TAG, "Error during YOLOView stop", e) } diff --git a/doc/api.md b/doc/api.md index 9b2329c2..072322d9 100644 --- a/doc/api.md +++ b/doc/api.md @@ -93,7 +93,7 @@ Future> predict( | --------------------- | ----------- | -------- | ------- | ------------------------------- | | `imageBytes` | `Uint8List` | ✅ | - | Raw image data | | `confidenceThreshold` | `double?` | ❌ | `0.25` | Confidence threshold (0.0-1.0) | -| `iouThreshold` | `double?` | ❌ | `0.4` | IoU threshold for NMS (0.0-1.0) | +| `iouThreshold` | `double?` | ❌ | `0.7` | IoU threshold for NMS (0.0-1.0) | **Returns**: `Future>` - Prediction results @@ -243,8 +243,8 @@ class YOLOView extends StatefulWidget { this.showNativeUI = false, this.onZoomChanged, this.streamingConfig, - this.confidenceThreshold = 0.5, - this.iouThreshold = 0.45, + this.confidenceThreshold = 0.25, + this.iouThreshold = 0.7, this.useGpu = true, this.showOverlays = true, this.overlayTheme = const YOLOOverlayTheme(), @@ -267,8 +267,8 @@ class YOLOView extends StatefulWidget { | `showNativeUI` | `bool` | ❌ | `false` | Show native camera controls | | `onZoomChanged` | `Function(double)?` | ❌ | `null` | Zoom level change callback | | `streamingConfig` | `YOLOStreamingConfig?` | ❌ | `null` | Streaming configuration | -| `confidenceThreshold` | `double` | ❌ | `0.5` | Initial confidence threshold for YOLOView | -| `iouThreshold` | `double` | ❌ | `0.45` | Initial IoU threshold for YOLOView | +| `confidenceThreshold` | `double` | ❌ | `0.25` | Initial confidence threshold for YOLOView | +| `iouThreshold` | `double` | ❌ | `0.7` | Initial IoU threshold for YOLOView | | `useGpu` | `bool` | ❌ | `true` | Enable GPU acceleration for camera inference | | `showOverlays` | `bool` | ❌ | `true` | Draw Flutter-side detection overlays | | `overlayTheme` | `YOLOOverlayTheme` | ❌ | `const YOLOOverlayTheme()` | Customize Flutter overlay styling | @@ -904,7 +904,7 @@ typedef PoseKeypoints = List>; ```dart // Default thresholds const double DEFAULT_CONFIDENCE_THRESHOLD = 0.25; -const double DEFAULT_IOU_THRESHOLD = 0.4; +const double DEFAULT_IOU_THRESHOLD = 0.7; const int DEFAULT_NUM_ITEMS_THRESHOLD = 30; // Performance thresholds diff --git a/doc/install.md b/doc/install.md index 42fc7908..b46d073f 100644 --- a/doc/install.md +++ b/doc/install.md @@ -138,13 +138,13 @@ class TestYOLO extends StatelessWidget { ); await yolo.loadModel(); - print('✅ YOLO loaded successfully!'); + debugPrint('YOLO loaded successfully'); ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('YOLO plugin working!')), + const SnackBar(content: Text('YOLO plugin working!')), ); } catch (e) { - print('❌ Error: $e'); + debugPrint('Error: $e'); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Error: $e')), ); diff --git a/doc/models.md b/doc/models.md index a87d4246..e2d8e618 100644 --- a/doc/models.md +++ b/doc/models.md @@ -15,10 +15,11 @@ The plugin treats model metadata as the source of truth whenever it is available ## 📦 Official Models -Use an official model ID such as `yolo26n`: +Use the default official model or a specific official model ID such as +`yolo26n`: ```dart -final yolo = YOLO(modelPath: 'yolo26n'); +final yolo = YOLO(modelPath: YOLO.defaultOfficialModel() ?? 'yolo26n'); ``` The plugin will: @@ -37,12 +38,15 @@ print(models); `YOLO.officialModels()` only returns real downloadable artifacts for the running platform. +If you want the simplest “start from the default Ultralytics model” entry +point, prefer `YOLO.defaultOfficialModel()`. + ## 📁 Custom Models -You can also point the plugin at your own exported model: +You can also point the plugin at your own fine-tuned exported model: ```dart -final yolo = YOLO(modelPath: 'assets/models/custom.tflite'); +final yolo = YOLO(modelPath: 'assets/models/my-finetuned-model.tflite'); ``` Supported sources: @@ -56,7 +60,7 @@ If the exported model metadata includes `task`, the plugin resolves it automatic ```dart final yolo = YOLO( - modelPath: 'assets/models/custom.tflite', + modelPath: 'assets/models/my-finetuned-model.tflite', task: YOLOTask.detect, ); ``` @@ -137,7 +141,7 @@ Flutter asset models are copied into app storage automatically before loading. You can use either: - `.mlpackage` or `.mlmodel` files added to `ios/Runner.xcworkspace` -- zipped CoreML packages in Flutter assets, for example `assets/models/custom.mlpackage.zip` +- zipped Core ML packages in Flutter assets, for example `assets/models/custom.mlpackage.zip` For Flutter assets on iOS, use `.mlpackage.zip` so the package can unpack the model into app storage before loading it. @@ -149,7 +153,7 @@ Install Ultralytics: pip install ultralytics ``` -### CoreML Export +### Core ML Export Detection models for iOS must be exported with `nms=True`: diff --git a/example/lib/main.dart b/example/lib/main.dart index 3de85da4..2339529e 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -13,7 +13,7 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return const MaterialApp( - title: 'YOLO useGpu Example', + title: 'Ultralytics YOLO', home: CameraInferenceScreen(), ); } diff --git a/example/lib/presentation/controllers/camera_inference_controller.dart b/example/lib/presentation/controllers/camera_inference_controller.dart index b855ad87..ef929576 100644 --- a/example/lib/presentation/controllers/camera_inference_controller.dart +++ b/example/lib/presentation/controllers/camera_inference_controller.dart @@ -14,8 +14,8 @@ class CameraInferenceController extends ChangeNotifier { int _frameCount = 0; DateTime _lastFpsUpdate = DateTime.now(); - double _confidenceThreshold = 0.5; - double _iouThreshold = 0.45; + double _confidenceThreshold = 0.25; + double _iouThreshold = 0.7; int _numItemsThreshold = 30; SliderType _activeSlider = SliderType.none; @@ -48,11 +48,9 @@ class CameraInferenceController extends ChangeNotifier { YOLOViewController get yoloController => _yoloController; static String _defaultModelForTask(YOLOTask task) { + final defaultModel = YOLO.defaultOfficialModel(task: task); + if (defaultModel != null) return defaultModel; final models = YOLO.officialModels(task: task); - final yolo26Models = models - .where((model) => model.startsWith('yolo26n')) - .toList(growable: false); - if (yolo26Models.isNotEmpty) return yolo26Models.first; return models.isEmpty ? '' : models.first; } diff --git a/example/lib/presentation/screens/single_image_screen.dart b/example/lib/presentation/screens/single_image_screen.dart index 3d352dbc..777d5428 100644 --- a/example/lib/presentation/screens/single_image_screen.dart +++ b/example/lib/presentation/screens/single_image_screen.dart @@ -4,11 +4,9 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; -import 'package:ultralytics_yolo/utils/error_handler.dart'; -import 'package:ultralytics_yolo/utils/map_converter.dart'; -import 'package:ultralytics_yolo/yolo.dart'; +import 'package:ultralytics_yolo/ultralytics_yolo.dart'; -/// A screen that demonstrates YOLO inference on a single image. +/// Demonstrates YOLO inference on a single gallery image. class SingleImageScreen extends StatefulWidget { const SingleImageScreen({super.key}); @@ -20,7 +18,7 @@ class _SingleImageScreenState extends State { final _picker = ImagePicker(); final _yolo = YOLO(modelPath: 'yolo26n'); - List> _detections = []; + List _detections = const []; Uint8List? _imageBytes; Uint8List? _annotatedImage; bool _isModelReady = false; @@ -34,16 +32,9 @@ class _SingleImageScreenState extends State { Future _initializeYOLO() async { try { await _yolo.loadModel(); - if (mounted) { - setState(() => _isModelReady = true); - } + if (mounted) setState(() => _isModelReady = true); } catch (e) { - if (!mounted) return; - final error = YOLOErrorHandler.handleError( - e, - 'Failed to load yolo26n for single-image inference', - ); - _showSnackBar('Error loading model: ${error.message}'); + if (mounted) _showSnackBar('Error loading model: $e'); } } @@ -52,25 +43,23 @@ class _SingleImageScreenState extends State { _showSnackBar('Model is loading, please wait...'); return; } - final file = await _picker.pickImage(source: ImageSource.gallery); if (file == null) return; - final bytes = await file.readAsBytes(); final result = await _yolo.predict(bytes); if (!mounted) return; - + final detections = (result['detections'] as List?) + ?.whereType() + .map(YOLOResult.fromMap) + .toList(growable: false); setState(() { - _detections = result['boxes'] is List - ? MapConverter.convertBoxesList(result['boxes'] as List) - : []; + _detections = detections ?? const []; _annotatedImage = result['annotatedImage'] as Uint8List?; _imageBytes = bytes; }); } void _showSnackBar(String message) { - if (!mounted) return; ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(message))); @@ -85,13 +74,7 @@ class _SingleImageScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - title: const Text('Single Image Inference'), - leading: IconButton( - icon: const Icon(Icons.arrow_back), - onPressed: () => Navigator.of(context).pop(), - ), - ), + appBar: AppBar(title: const Text('Single Image Inference')), body: Column( children: [ const SizedBox(height: 20), @@ -113,20 +96,32 @@ class _SingleImageScreenState extends State { ), ), Expanded( - child: SingleChildScrollView( - child: Column( - children: [ - if (_annotatedImage != null || _imageBytes != null) - SizedBox( - height: 300, - width: double.infinity, - child: Image.memory(_annotatedImage ?? _imageBytes!), + child: ListView( + children: [ + if (_annotatedImage != null || _imageBytes != null) + SizedBox( + height: 300, + width: double.infinity, + child: Image.memory(_annotatedImage ?? _imageBytes!), + ), + const SizedBox(height: 10), + if (_detections.isNotEmpty) + const Padding( + padding: EdgeInsets.symmetric(horizontal: 16), + child: Text( + 'Detections', + style: TextStyle(fontWeight: FontWeight.bold), ), - const SizedBox(height: 10), - const Text('Detections:'), - Text(_detections.toString()), - ], - ), + ), + for (final d in _detections) + ListTile( + dense: true, + title: Text(d.className), + trailing: Text( + '${(d.confidence * 100).toStringAsFixed(1)}%', + ), + ), + ], ), ), ], diff --git a/example/lib/presentation/widgets/camera_controls.dart b/example/lib/presentation/widgets/camera_controls.dart index 42ba8a7a..4ef92bb4 100644 --- a/example/lib/presentation/widgets/camera_controls.dart +++ b/example/lib/presentation/widgets/camera_controls.dart @@ -35,8 +35,8 @@ class CameraControls extends StatelessWidget { child: Column( children: [ if (!isFrontCamera) - ControlButton( - content: '${currentZoomLevel.toStringAsFixed(1)}x', + ControlButton.text( + label: '${currentZoomLevel.toStringAsFixed(1)}x', onPressed: () => onZoomChanged( currentZoomLevel < 0.75 ? 1.0 @@ -46,18 +46,18 @@ class CameraControls extends StatelessWidget { ), ), SizedBox(height: isLandscape ? 8 : 12), - ControlButton( - content: Icons.layers, + ControlButton.icon( + icon: Icons.layers, onPressed: () => onSliderToggled(SliderType.numItems), ), SizedBox(height: isLandscape ? 8 : 12), - ControlButton( - content: Icons.adjust, + ControlButton.icon( + icon: Icons.adjust, onPressed: () => onSliderToggled(SliderType.confidence), ), SizedBox(height: isLandscape ? 8 : 12), - ControlButton( - content: 'assets/iou.png', + ControlButton.asset( + assetPath: 'assets/iou.png', onPressed: () => onSliderToggled(SliderType.iou), ), SizedBox(height: isLandscape ? 16 : 40), diff --git a/example/lib/presentation/widgets/control_button.dart b/example/lib/presentation/widgets/control_button.dart index f2303c90..ba46d5b6 100644 --- a/example/lib/presentation/widgets/control_button.dart +++ b/example/lib/presentation/widgets/control_button.dart @@ -2,15 +2,35 @@ import 'package:flutter/material.dart'; -/// A circular control button that can display an icon, image, or text +/// A circular control button that displays an icon, asset image, or text. class ControlButton extends StatelessWidget { - const ControlButton({ + const ControlButton.icon({ super.key, - required this.content, + required IconData icon, required this.onPressed, - }); + }) : _icon = icon, + _assetPath = null, + _label = null; - final dynamic content; + const ControlButton.asset({ + super.key, + required String assetPath, + required this.onPressed, + }) : _icon = null, + _assetPath = assetPath, + _label = null; + + const ControlButton.text({ + super.key, + required String label, + required this.onPressed, + }) : _icon = null, + _assetPath = null, + _label = label; + + final IconData? _icon; + final String? _assetPath; + final String? _label; final VoidCallback onPressed; @override @@ -23,24 +43,31 @@ class ControlButton extends StatelessWidget { } Widget _buildContent() { - if (content is IconData) { + final icon = _icon; + if (icon != null) { return IconButton( - icon: Icon(content, color: Colors.white), + icon: Icon(icon, color: Colors.white), onPressed: onPressed, ); - } else if (content.toString().contains('assets/')) { + } + final assetPath = _assetPath; + if (assetPath != null) { return IconButton( - icon: Image.asset(content, width: 24, height: 24, color: Colors.white), - onPressed: onPressed, - ); - } else { - return TextButton( - onPressed: onPressed, - child: Text( - content, - style: const TextStyle(color: Colors.white, fontSize: 12), + icon: Image.asset( + assetPath, + width: 24, + height: 24, + color: Colors.white, ), + onPressed: onPressed, ); } + return TextButton( + onPressed: onPressed, + child: Text( + _label ?? '', + style: const TextStyle(color: Colors.white, fontSize: 12), + ), + ); } } diff --git a/example/main.dart b/example/main.dart index 559df683..d98a06fe 100644 --- a/example/main.dart +++ b/example/main.dart @@ -3,62 +3,29 @@ import 'package:flutter/material.dart'; import 'package:ultralytics_yolo/ultralytics_yolo.dart'; -void main() { - runApp(const MyApp()); -} +void main() => runApp(const MyApp()); class MyApp extends StatelessWidget { const MyApp({super.key}); @override - Widget build(BuildContext context) { - return const MaterialApp(title: 'YOLO Example', home: YOLOScreen()); - } + Widget build(BuildContext context) => + const MaterialApp(title: 'YOLO Example', home: YOLOScreen()); } class YOLOScreen extends StatelessWidget { const YOLOScreen({super.key}); @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: const Text('YOLO Detection')), - body: YOLOView( - modelPath: 'yolo26n', - onResult: (results) { - for (final result in results) { - debugPrint('${result.className}: ${result.confidence}'); - } - }, - ), - ); - } -} - -// Single image inference example -class SingleImageExample extends StatefulWidget { - const SingleImageExample({super.key}); - - @override - State createState() => _SingleImageExampleState(); -} - -class _SingleImageExampleState extends State { - YOLO? _yolo; - - @override - void initState() { - super.initState(); - _initializeYOLO(); - } - - Future _initializeYOLO() async { - _yolo = YOLO(modelPath: 'yolo26n'); - await _yolo!.loadModel(); - } - - @override - Widget build(BuildContext context) { - return Container(); // Simplified for brevity - } + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text('YOLO Detection')), + body: YOLOView( + modelPath: 'yolo26n', + onResult: (results) { + for (final r in results) { + debugPrint('${r.className}: ${r.confidence}'); + } + }, + ), + ); } diff --git a/ios/Classes/BasePredictor.swift b/ios/Classes/BasePredictor.swift index 56294025..9147b3f0 100644 --- a/ios/Classes/BasePredictor.swift +++ b/ios/Classes/BasePredictor.swift @@ -6,7 +6,7 @@ // Access the source code: https://github.com/ultralytics/yolo-ios-app // // The BasePredictor class is the foundation for all task-specific predictors in the YOLO framework. -// It manages the loading and initialization of CoreML models, handling common operations such as +// It manages the loading and initialization of Core ML models, handling common operations such as // model loading, class label extraction, and inference timing. The class provides an asynchronous // model loading mechanism that runs on background threads and includes support for configuring // model parameters like confidence thresholds and IoU thresholds. Specific task implementations @@ -21,7 +21,7 @@ import Vision /// Base class for all YOLO model predictors, handling common model loading and inference logic. /// /// The BasePredictor serves as the foundation for all task-specific YOLO model predictors. -/// It manages CoreML model loading, initialization, and common inference operations. +/// It manages Core ML model loading, initialization, and common inference operations. /// Specialized predictors (for detection, segmentation, etc.) inherit from this class /// and override the prediction-specific methods to handle task-specific processing. /// @@ -32,10 +32,10 @@ public class BasePredictor: Predictor, @unchecked Sendable { /// Flag indicating if the model has been successfully loaded and is ready for inference. private(set) var isModelLoaded: Bool = false - /// The Vision CoreML model used for inference operations. + /// The Vision Core ML model used for inference operations. var detector: VNCoreMLModel! - /// The Vision request that processes images using the CoreML model. + /// The Vision request that processes images using the Core ML model. var visionRequest: VNCoreMLRequest? /// The class labels used by the model for categorizing detections. @@ -145,28 +145,24 @@ public class BasePredictor: Predictor, @unchecked Sendable { return [] } - /// Performs cleanup when the predictor is deallocated. - /// - /// Cancels any pending vision requests and releases references to avoid memory leaks. + /// Cancels any pending Vision requests and releases references on deinit. deinit { - visionRequest?.cancel() visionRequest = nil detector = nil currentBuffer = nil currentOnResultsListener = nil currentOnInferenceTimeListener = nil - print("BaseP redictor: deinit completed") } /// Factory method to asynchronously create and initialize a predictor with the specified model. /// - /// This method loads the CoreML model in a background thread and sets up the prediction + /// This method loads the Core ML model in a background thread and sets up the prediction /// infrastructure. The completion handler is called on the main thread with either a /// successfully initialized predictor or an error. /// /// - Parameters: - /// - unwrappedModelURL: The URL of the CoreML model file to load. + /// - unwrappedModelURL: The URL of the Core ML model file to load. /// - isRealTime: Flag indicating if the predictor will be used for real-time processing (camera feed). /// - useGpu: Flag indicating whether to use GPU acceleration /// - completion: Callback that receives the initialized predictor or an error. @@ -213,14 +209,9 @@ public class BasePredictor: Predictor, @unchecked Sendable { mlModel.modelDescription .metadata[MLModelMetadataKey.creatorDefinedKey] as? [String: String] - // Continue even when top-level metadata is missing. Some CoreML pipeline exports + // Continue even when top-level metadata is missing. Some Core ML pipeline exports // only keep labels on nested models, and hard-failing here leaves the predictor nil. predictor.labels = userDefined.map(Self.parseLabels(from:)) ?? [] - if predictor.labels.isEmpty { - print( - "BasePredictor: No top-level creatorDefined labels found. Continuing with fallback class labels." - ) - } // (3) Store model input size predictor.modelInputSize = predictor.getModelInputSize(for: mlModel) @@ -289,35 +280,21 @@ public class BasePredictor: Predictor, @unchecked Sendable { let imageOrientation: CGImagePropertyOrientation = .up // Capture original image data for streaming if needed - var originalImageData: Data? = nil - - if let streamConfig = streamConfig { - - if streamConfig.includeOriginalImage { - - if let imageData = convertPixelBufferToJPEGData(pixelBuffer) { - originalImageData = imageData - - } else { - - } - } else { - - } - } else { - - } + let originalImageData: Data? = + streamConfig?.includeOriginalImage == true + ? convertPixelBufferToJPEGData(pixelBuffer) + : nil // Invoke a VNRequestHandler with that image let handler = VNImageRequestHandler( cvPixelBuffer: pixelBuffer, orientation: imageOrientation, options: [:]) t0 = CACurrentMediaTime() // inference start do { - if visionRequest != nil { - try handler.perform([visionRequest!]) + if let request = visionRequest { + try handler.perform([request]) } } catch { - print(error) + NSLog("YOLO inference error: %@", String(describing: error)) } t1 = CACurrentMediaTime() - t0 // inference dt @@ -340,10 +317,10 @@ public class BasePredictor: Predictor, @unchecked Sendable { confidenceThreshold = confidence } - /// The IoU (Intersection over Union) threshold for non-maximum suppression (default: 0.4). + /// The IoU (Intersection over Union) threshold for non-maximum suppression (default: 0.7). /// /// Used to filter overlapping detections during non-maximum suppression. - var iouThreshold = 0.4 + var iouThreshold = 0.7 /// Sets the IoU threshold for non-maximum suppression. /// @@ -391,34 +368,30 @@ public class BasePredictor: Predictor, @unchecked Sendable { /// Extracts the required input dimensions from the model description. /// - /// This utility method determines the expected input size for the CoreML model + /// This utility method determines the expected input size for the Core ML model /// by examining its input description, which is essential for properly sizing /// and formatting images before inference. /// - /// - Parameter model: The CoreML model to analyze. + /// - Parameter model: The Core ML model to analyze. /// - Returns: A tuple containing the width and height in pixels required by the model. func getModelInputSize(for model: MLModel) -> (width: Int, height: Int) { guard let inputDescription = model.modelDescription.inputDescriptionsByName.first?.value else { - print("can not find input description") return (0, 0) } if let multiArrayConstraint = inputDescription.multiArrayConstraint { let shape = multiArrayConstraint.shape if shape.count >= 2 { - let height = shape[0].intValue - let width = shape[1].intValue - return (width: width, height: height) + return (width: shape[1].intValue, height: shape[0].intValue) } } if let imageConstraint = inputDescription.imageConstraint { - let width = Int(imageConstraint.pixelsWide) - let height = Int(imageConstraint.pixelsHigh) - return (width: width, height: height) + return ( + width: Int(imageConstraint.pixelsWide), height: Int(imageConstraint.pixelsHigh) + ) } - print("an not find input size") return (0, 0) } diff --git a/ios/Classes/BoundingBoxView.swift b/ios/Classes/BoundingBoxView.swift index e6152648..e497c0a8 100644 --- a/ios/Classes/BoundingBoxView.swift +++ b/ios/Classes/BoundingBoxView.swift @@ -2,7 +2,7 @@ // // BoundingBoxView for Ultralytics YOLO App -// This class is designed to visualize bounding boxes and labels for detected objects in the YOLOv8 models within the Ultralytics YOLO app. +// This class is designed to visualize bounding boxes and labels for detected objects in the YOLO models within the Ultralytics YOLO app. // It leverages Core Animation layers to draw the bounding boxes and text labels dynamically on the detection video feed. // Licensed under AGPL-3.0. For commercial use, refer to Ultralytics licensing: https://ultralytics.com/license // Access the source code: https://github.com/ultralytics/yolo-ios-app diff --git a/ios/Classes/Classifier.swift b/ios/Classes/Classifier.swift index 3c78e345..1fec9669 100644 --- a/ios/Classes/Classifier.swift +++ b/ios/Classes/Classifier.swift @@ -198,7 +198,7 @@ class Classifier: BasePredictor { } } catch { - print(error) + NSLog("YOLO Classifier error: %@", String(describing: error)) } var result = YOLOResult( diff --git a/ios/Classes/ObbDetector.swift b/ios/Classes/ObbDetector.swift index 43e26878..9d058a5e 100644 --- a/ios/Classes/ObbDetector.swift +++ b/ios/Classes/ObbDetector.swift @@ -100,8 +100,8 @@ class ObbDetector: BasePredictor, @unchecked Sendable { if let prediction = results.first?.featureValue.multiArrayValue { let nmsResults = postProcessOBB( feature: prediction, // your MLMultiArray - confidenceThreshold: 0.25, - iouThreshold: 0.45 + confidenceThreshold: Float(confidenceThreshold), + iouThreshold: Float(iouThreshold) ) var obbResults: [OBBResult] = [] @@ -121,12 +121,12 @@ class ObbDetector: BasePredictor, @unchecked Sendable { } } } catch { - print(error) + NSLog("YOLO ObbDetector error: %@", String(describing: error)) } return result } - fileprivate let lockQueue = DispatchQueue(label: "com.example.obbLock") + fileprivate let lockQueue = DispatchQueue(label: "com.ultralytics.yolo.obbLock") func postProcessOBB( feature: MLMultiArray, diff --git a/ios/Classes/ObjectDetector.swift b/ios/Classes/ObjectDetector.swift index da204b24..1f91ea0d 100644 --- a/ios/Classes/ObjectDetector.swift +++ b/ios/Classes/ObjectDetector.swift @@ -154,7 +154,7 @@ class ObjectDetector: BasePredictor { } } } catch { - print(error) + NSLog("YOLO ObjectDetector error: %@", String(describing: error)) } let speed = Date().timeIntervalSince(start) var result = YOLOResult(orig_shape: inputSize, boxes: boxes, speed: t1, names: labels) diff --git a/ios/Classes/Plot.swift b/ios/Classes/Plot.swift index 9c06b38f..e0bc82ba 100644 --- a/ios/Classes/Plot.swift +++ b/ios/Classes/Plot.swift @@ -207,7 +207,6 @@ func generateCombinedMaskImage( maskWidth > 0, maskChannels > 0 else { - print("Invalid protos shape!") return nil } @@ -502,10 +501,6 @@ func drawSinglePersonKeypoints( confThreshold: Float, drawSkeleton: Bool ) { - // guard keypoints.count == 17 else { - // print("Keypoints array must have 51 elements.") - // return - // } let lineWidth = radius * 0.4 let scaleXToView = Float(imageViewSize.width / originalImageSize.width) let scaleYToView = Float(imageViewSize.height / originalImageSize.height) @@ -534,7 +529,6 @@ func drawSinglePersonKeypoints( let (startIdx, endIdx) = (bone[0] - 1, bone[1] - 1) guard startIdx < points.count, endIdx < points.count else { - print("Invalid skeleton indices: \(startIdx), \(endIdx)") continue } @@ -831,7 +825,6 @@ func drawOBBsOnCIImage( let context = CIContext(options: nil) let extent = ciImage.extent guard let cgImage = context.createCGImage(ciImage, from: extent) else { - print("Failed to create CGImage from CIImage") return nil } diff --git a/ios/Classes/PoseEstimater.swift b/ios/Classes/PoseEstimater.swift index acd778e3..731f8dca 100644 --- a/ios/Classes/PoseEstimater.swift +++ b/ios/Classes/PoseEstimater.swift @@ -127,7 +127,7 @@ class PoseEstimater: BasePredictor, @unchecked Sendable { } } } catch { - print(error) + NSLog("YOLO PoseEstimator error: %@", String(describing: error)) } return result } diff --git a/ios/Classes/README.md b/ios/Classes/README.md index 7e81cc04..063c222f 100644 --- a/ios/Classes/README.md +++ b/ios/Classes/README.md @@ -34,7 +34,7 @@ The Flutter side can hand the iOS layer: ## Export Reminder -For detection models on iOS, CoreML exports must use `nms=True`: +For detection models on iOS, Core ML exports must use `nms=True`: ```python from ultralytics import YOLO diff --git a/ios/Classes/Segmenter.swift b/ios/Classes/Segmenter.swift index 45a51831..3c0e52ca 100644 --- a/ios/Classes/Segmenter.swift +++ b/ios/Classes/Segmenter.swift @@ -79,7 +79,7 @@ class Segmenter: BasePredictor, @unchecked Sendable { DispatchQueue.global(qos: .userInitiated).async { guard - let procceessedMasks = generateCombinedMaskImage( + let processedMasks = generateCombinedMaskImage( detectedObjects: limitedDetections, protos: masks, inputWidth: self.modelInputSize.width, @@ -91,7 +91,7 @@ class Segmenter: BasePredictor, @unchecked Sendable { DispatchQueue.main.async { self.isUpdating = false } return } - var maskResults = Masks(masks: procceessedMasks.1, combinedMask: procceessedMasks.0) + var maskResults = Masks(masks: processedMasks.1, combinedMask: processedMasks.0) var result = YOLOResult( orig_shape: self.inputSize, boxes: boxes, masks: maskResults, speed: self.t2, fps: 1 / self.t4, names: self.labels) @@ -155,7 +155,8 @@ class Segmenter: BasePredictor, @unchecked Sendable { let a = Date() let detectedObjects = postProcessSegment( - feature: pred, confidenceThreshold: 0.25, iouThreshold: 0.4) + feature: pred, confidenceThreshold: Float(confidenceThreshold), + iouThreshold: Float(iouThreshold)) var boxes: [Box] = [] var colorMasks: [CGImage?] = [] var alhaMasks: [CGImage?] = [] @@ -177,7 +178,7 @@ class Segmenter: BasePredictor, @unchecked Sendable { } guard - let procceessedMasks = generateCombinedMaskImage( + let processedMasks = generateCombinedMaskImage( detectedObjects: detectedObjects, protos: masks, inputWidth: self.modelInputSize.width, @@ -192,8 +193,8 @@ class Segmenter: BasePredictor, @unchecked Sendable { } let cgImage = CIContext().createCGImage(image, from: image.extent)! var annotatedImage = composeImageWithMask( - baseImage: cgImage, maskImage: procceessedMasks.0!) - var maskResults: Masks = Masks(masks: procceessedMasks.1, combinedMask: procceessedMasks.0) + baseImage: cgImage, maskImage: processedMasks.0!) + var maskResults: Masks = Masks(masks: processedMasks.1, combinedMask: processedMasks.0) if self.t1 < 10.0 { // valid dt self.t2 = self.t1 * 0.05 + self.t2 * 0.95 // smoothed inference time } @@ -209,7 +210,7 @@ class Segmenter: BasePredictor, @unchecked Sendable { // } } } catch { - print(error) + NSLog("YOLO Segmenter: %@", String(describing: error)) } return result } diff --git a/ios/Classes/SwiftYOLOPlatformView.swift b/ios/Classes/SwiftYOLOPlatformView.swift index e70dbc51..4f81154f 100644 --- a/ios/Classes/SwiftYOLOPlatformView.swift +++ b/ios/Classes/SwiftYOLOPlatformView.swift @@ -29,8 +29,8 @@ public class SwiftYOLOPlatformView: NSObject, FlutterPlatformView, FlutterStream private var yoloView: YOLOView? // Track current threshold values to maintain state - private var currentConfidenceThreshold: Double = 0.5 - private var currentIouThreshold: Double = 0.45 + private var currentConfidenceThreshold: Double = 0.25 + private var currentIouThreshold: Double = 0.7 private var currentNumItemsThreshold: Int = 30 private var currentShowOverlays: Bool = true @@ -76,8 +76,8 @@ public class SwiftYOLOPlatformView: NSObject, FlutterPlatformView, FlutterStream let task = YOLOTask.fromString(taskRaw) // Get new threshold parameters - let confidenceThreshold = dict["confidenceThreshold"] as? Double ?? 0.5 - let iouThreshold = dict["iouThreshold"] as? Double ?? 0.45 + let confidenceThreshold = dict["confidenceThreshold"] as? Double ?? 0.25 + let iouThreshold = dict["iouThreshold"] as? Double ?? 0.7 let numItemsThreshold = dict["numItemsThreshold"] as? Int ?? 30 let showOverlays = dict["showOverlays"] as? Bool ?? true @@ -93,7 +93,7 @@ public class SwiftYOLOPlatformView: NSObject, FlutterPlatformView, FlutterStream self.currentShowOverlays = showOverlays // Old threshold parameter for backward compatibility - let oldThreshold = dict["threshold"] as? Double ?? 0.5 + let oldThreshold = dict["threshold"] as? Double ?? 0.25 // Determine which thresholds to use (prioritize new parameters) @@ -134,7 +134,7 @@ public class SwiftYOLOPlatformView: NSObject, FlutterPlatformView, FlutterStream // Method for backward compatibility private func setupYOLOView(threshold: Double) { - setupYOLOView(confidenceThreshold: threshold, iouThreshold: 0.45) // Use default IoU value + setupYOLOView(confidenceThreshold: threshold, iouThreshold: 0.7) } // Setup YOLOView and connect callbacks (using new parameters) diff --git a/ios/Classes/ThresholdProvider.swift b/ios/Classes/ThresholdProvider.swift index 239b9958..51fd55e0 100644 --- a/ios/Classes/ThresholdProvider.swift +++ b/ios/Classes/ThresholdProvider.swift @@ -3,7 +3,7 @@ // // Threshold Provider for Ultralytics YOLO App // This class is designed to supply custom Intersection Over Union (IoU) and confidence thresholds -// for the YOLOv8 object detection models within the Ultralytics YOLO app. It conforms to the MLFeatureProvider protocol, +// for the YOLO object detection models within the Ultralytics YOLO app. It conforms to the MLFeatureProvider protocol, // allowing these thresholds to be dynamically adjusted and applied to model predictions. // Licensed under AGPL-3.0. For commercial use, refer to Ultralytics licensing: https://ultralytics.com/license // Access the source code: https://github.com/ultralytics/yolo-ios-app @@ -27,7 +27,7 @@ class ThresholdProvider: MLFeatureProvider { /// - Parameters: /// - iouThreshold: The IoU threshold for determining object overlap. /// - confidenceThreshold: The minimum confidence for considering a detection valid. - init(iouThreshold: Double = 0.45, confidenceThreshold: Double = 0.25) { + init(iouThreshold: Double = 0.7, confidenceThreshold: Double = 0.25) { values = [ "iouThreshold": MLFeatureValue(double: iouThreshold), "confidenceThreshold": MLFeatureValue(double: confidenceThreshold), diff --git a/ios/Classes/VideoCapture.swift b/ios/Classes/VideoCapture.swift index 4b6fcf4d..ae46aa00 100644 --- a/ios/Classes/VideoCapture.swift +++ b/ios/Classes/VideoCapture.swift @@ -25,9 +25,6 @@ protocol VideoCaptureDelegate: AnyObject { } func bestCaptureDevice(position: AVCaptureDevice.Position) -> AVCaptureDevice? { - // print("USE TELEPHOTO: ") - // print(UserDefaults.standard.bool(forKey: "use_telephoto")) - if UserDefaults.standard.bool(forKey: "use_telephoto"), let device = AVCaptureDevice.default(.builtInTelephotoCamera, for: .video, position: position) { @@ -85,12 +82,13 @@ class VideoCapture: NSObject, @unchecked Sendable { let authStatus = AVCaptureDevice.authorizationStatus(for: .video) if authStatus == .denied || authStatus == .restricted { - print("Camera permission denied or restricted. Cannot initialize camera.") + NSLog("YOLO VideoCapture: Camera permission denied or restricted. Cannot initialize camera.") return false } if authStatus == .notDetermined { - print("Camera permission not determined. Please request permission first.") + NSLog( + "YOLO VideoCapture: Camera permission not determined. Please request permission first.") return false } @@ -98,7 +96,9 @@ class VideoCapture: NSObject, @unchecked Sendable { captureSession.sessionPreset = sessionPreset guard let device = bestCaptureDevice(position: position) else { - print("No camera device available for position: \(position)") + NSLog( + "YOLO VideoCapture: No camera device available for position: %@", + String(describing: position)) captureSession.commitConfiguration() return false } @@ -109,7 +109,8 @@ class VideoCapture: NSObject, @unchecked Sendable { do { input = try AVCaptureDeviceInput(device: device) } catch { - print("Failed to create AVCaptureDeviceInput: \(error.localizedDescription)") + NSLog( + "YOLO VideoCapture: Failed to create AVCaptureDeviceInput: %@", error.localizedDescription) captureSession.commitConfiguration() return false } @@ -119,7 +120,7 @@ class VideoCapture: NSObject, @unchecked Sendable { if captureSession.canAddInput(input) { captureSession.addInput(input) } else { - print("Cannot add video input to capture session") + NSLog("YOLO VideoCapture: Cannot add video input to capture session") captureSession.commitConfiguration() return false } @@ -166,7 +167,7 @@ class VideoCapture: NSObject, @unchecked Sendable { // Configure captureDevice guard let device = captureDevice else { - print("captureDevice is nil, cannot configure") + NSLog("YOLO VideoCapture: captureDevice is nil, cannot configure") captureSession.commitConfiguration() return false } @@ -183,7 +184,7 @@ class VideoCapture: NSObject, @unchecked Sendable { device.exposureMode = AVCaptureDevice.ExposureMode.continuousAutoExposure device.unlockForConfiguration() } catch { - print("device configuration not working: \(error.localizedDescription)") + NSLog("YOLO VideoCapture: device configuration failed: %@", error.localizedDescription) captureSession.commitConfiguration() return false } @@ -210,7 +211,7 @@ class VideoCapture: NSObject, @unchecked Sendable { func setZoomRatio(ratio: CGFloat) { guard let device = captureDevice else { - print("Cannot set zoom: captureDevice is nil") + NSLog("YOLO VideoCapture: Cannot set zoom: captureDevice is nil") return } do { @@ -220,13 +221,12 @@ class VideoCapture: NSObject, @unchecked Sendable { } device.videoZoomFactor = ratio } catch { - print("Failed to set zoom ratio: \(error.localizedDescription)") + NSLog("YOLO VideoCapture: Failed to set zoom ratio: %@", error.localizedDescription) } } private func predictOnFrame(sampleBuffer: CMSampleBuffer) { guard let predictor = predictor else { - print("predictor is nil") return } if currentBuffer == nil, let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) { @@ -279,7 +279,6 @@ class VideoCapture: NSObject, @unchecked Sendable { } deinit { - print("VideoCapture: deinit called - ensuring capture session is stopped") if captureSession.isRunning { captureSession.stopRunning() } @@ -296,8 +295,6 @@ class VideoCapture: NSObject, @unchecked Sendable { captureSession.removeOutput(output) } } - - print("VideoCapture: deinit completed") } } diff --git a/ios/Classes/YOLO.swift b/ios/Classes/YOLO.swift index c04a89cd..56e2ba07 100644 --- a/ios/Classes/YOLO.swift +++ b/ios/Classes/YOLO.swift @@ -31,7 +31,7 @@ public class YOLO { } /// IoU threshold for non-maximum suppression (0.0-1.0) - public var iouThreshold: Double = 0.4 { + public var iouThreshold: Double = 0.7 { didSet { // Apply to predictor if it has been loaded if let basePredictor = predictor as? BasePredictor { @@ -45,8 +45,6 @@ public class YOLO { numItemsThreshold: Int = 30, completion: ((Result) -> Void)? = nil ) { - print("YOLO.init: Received modelPath: \(modelPathOrName)") - var modelURL: URL? let lowercasedPath = modelPathOrName.lowercased() @@ -78,14 +76,9 @@ public class YOLO { // If the model URL is still unresolved, check Flutter assets if modelURL == nil { - print("YOLO Debug: Searching for model at path: \(modelPathOrName)") - // For absolute paths, use them directly and allow directories var isDirectory: ObjCBool = false if fileManager.fileExists(atPath: modelPathOrName, isDirectory: &isDirectory) { - print( - "YOLO Debug: Found model at absolute path: \(modelPathOrName) (isDirectory: \(isDirectory.boolValue))" - ) modelURL = URL(fileURLWithPath: modelPathOrName) } @@ -96,13 +89,10 @@ public class YOLO { let directory = components.dropLast().joined(separator: "/") let assetDirectory = "flutter_assets/\(directory)" - print("YOLO Debug: Checking in asset directory: \(assetDirectory) for file: \(fileName)") - // Try resolving with the filename as-is if let assetPath = Bundle.main.path( forResource: fileName, ofType: nil, inDirectory: assetDirectory) { - print("YOLO Debug: Found model in assets directory: \(assetPath)") modelURL = URL(fileURLWithPath: assetPath) } @@ -112,14 +102,9 @@ public class YOLO { let name = fileComponents.dropLast().joined(separator: ".") let ext = fileComponents.last ?? "" - print( - "YOLO Debug: Trying with separated name: \(name) and extension: \(ext) in directory: \(assetDirectory)" - ) - if let assetPath = Bundle.main.path( forResource: name, ofType: ext, inDirectory: assetDirectory) { - print("YOLO Debug: Found model with separated extension: \(assetPath)") modelURL = URL(fileURLWithPath: assetPath) } } @@ -128,10 +113,8 @@ public class YOLO { // Check the asset directory directly if modelURL == nil && modelPathOrName.contains("/") { let assetPath = "flutter_assets/\(modelPathOrName)" - print("YOLO Debug: Checking direct asset path: \(assetPath)") if let directPath = Bundle.main.path(forResource: assetPath, ofType: nil) { - print("YOLO Debug: Found model at direct asset path: \(directPath)") modelURL = URL(fileURLWithPath: directPath) } } @@ -139,13 +122,11 @@ public class YOLO { // If there is no folder structure, search by filename only if modelURL == nil { let fileName = modelPathOrName.components(separatedBy: "/").last ?? modelPathOrName - print("YOLO Debug: Checking filename only: \(fileName) in flutter_assets root") // Check the Flutter assets root if let assetPath = Bundle.main.path( forResource: fileName, ofType: nil, inDirectory: "flutter_assets") { - print("YOLO Debug: Found model in flutter_assets root: \(assetPath)") modelURL = URL(fileURLWithPath: assetPath) } @@ -155,13 +136,9 @@ public class YOLO { let name = fileComponents.dropLast().joined(separator: ".") let ext = fileComponents.last ?? "" - print("YOLO Debug: Trying with separated filename: \(name) and extension: \(ext)") - if let assetPath = Bundle.main.path( forResource: name, ofType: ext, inDirectory: "flutter_assets") { - print( - "YOLO Debug: Found model with separated extension in flutter_assets: \(assetPath)") modelURL = URL(fileURLWithPath: assetPath) } } @@ -171,9 +148,6 @@ public class YOLO { // Check resource bundles (for example, Example/Flutter/App.frameworks/App.framework) if modelURL == nil { for bundle in Bundle.allBundles { - let bundleID = bundle.bundleIdentifier ?? "unknown" - print("YOLO Debug: Checking bundle: \(bundleID)") - // Handle paths that include folder structure if modelPathOrName.contains("/") { let components = modelPathOrName.components(separatedBy: "/") @@ -181,7 +155,6 @@ public class YOLO { // Search using the filename only if let path = bundle.path(forResource: fileName, ofType: nil) { - print("YOLO Debug: Found model in bundle \(bundleID): \(path)") modelURL = URL(fileURLWithPath: path) break } @@ -193,7 +166,6 @@ public class YOLO { let ext = fileComponents.last ?? "" if let path = bundle.path(forResource: name, ofType: ext) { - print("YOLO Debug: Found model with ext in bundle \(bundleID): \(path)") modelURL = URL(fileURLWithPath: path) break } @@ -203,46 +175,7 @@ public class YOLO { } guard let unwrappedModelURL = modelURL else { - print("YOLO Error: Model not found at path: \(modelPathOrName)") - print("YOLO Debug: Original model path: \(modelPathOrName)") - print("YOLO Debug: Lowercased path: \(lowercasedPath)") - - // Check if the path exists as directory - var isDirectory: ObjCBool = false - if fileManager.fileExists(atPath: modelPathOrName, isDirectory: &isDirectory) { - print("YOLO Debug: Path exists. Is directory: \(isDirectory.boolValue)") - - // If it's a directory and ends with .mlpackage, it should have been found - if isDirectory.boolValue && lowercasedPath.hasSuffix(".mlpackage") { - print("YOLO Error: mlpackage directory exists but was not properly recognized") - } - } else { - print("YOLO Debug: Path does not exist") - } - - // Print the list of available bundles and assets - print("YOLO Debug: Available bundles:") - for bundle in Bundle.allBundles { - print(" - \(bundle.bundleIdentifier ?? "unknown"): \(bundle.bundlePath)") - } - print("YOLO Debug: Checking if flutter_assets directory exists:") - let flutterAssetsPath = Bundle.main.bundlePath + "/flutter_assets" - if fileManager.fileExists(atPath: flutterAssetsPath) { - print(" - flutter_assets exists at: \(flutterAssetsPath)") - // List files inside flutter_assets - do { - let items = try fileManager.contentsOfDirectory(atPath: flutterAssetsPath) - print("YOLO Debug: Files in flutter_assets:") - for item in items { - print(" - \(item)") - } - } catch { - print("YOLO Debug: Error listing flutter_assets: \(error)") - } - } else { - print(" - flutter_assets NOT found") - } - + NSLog("YOLO: Model not found at path: %@", modelPathOrName) completion?(.failure(PredictorError.modelFileNotFound)) return } @@ -254,7 +187,7 @@ public class YOLO { // Common failure handling for all tasks func handleFailure(_ error: Error) { - print("Failed to load model with error: \(error)") + NSLog("YOLO: Failed to load model: %@", String(describing: error)) completion?(.failure(error)) } @@ -321,11 +254,7 @@ public class YOLO { public func callAsFunction(_ uiImage: UIImage, returnAnnotatedImage: Bool = true) -> YOLOResult { let ciImage = CIImage(image: uiImage)! - var result = predictor.predictOnImage(image: ciImage) - // if returnAnnotatedImage { - // let annotatedImage = drawYOLODetections(on: ciImage, result: result) - // result.annotatedImage = annotatedImage - // } + let result = predictor.predictOnImage(image: ciImage) return result } diff --git a/ios/Classes/YOLOPlugin.swift b/ios/Classes/YOLOPlugin.swift index 5c6263b8..a9afd905 100644 --- a/ios/Classes/YOLOPlugin.swift +++ b/ios/Classes/YOLOPlugin.swift @@ -348,13 +348,11 @@ public class YOLOPlugin: NSObject, FlutterPlugin { return } - print("YOLOPlugin: disposeInstance called for instanceId: \(instanceId)") YOLOInstanceManager.shared.removeInstance(instanceId: instanceId) // Remove the channel for this instance YOLOPlugin.instanceChannels.removeValue(forKey: instanceId) - print("YOLOPlugin: Instance \(instanceId) disposed successfully") result(nil) case "predictorInstance": diff --git a/ios/Classes/YOLOView.swift b/ios/Classes/YOLOView.swift index d87041ae..7d55e28e 100644 --- a/ios/Classes/YOLOView.swift +++ b/ios/Classes/YOLOView.swift @@ -260,8 +260,6 @@ public class YOLOView: UIView, VideoCaptureDelegate { var isDirectory: ObjCBool = false if fileManager.fileExists(atPath: possibleURL.path, isDirectory: &isDirectory) { modelURL = possibleURL - print( - "YOLOView: Found model at: \(possibleURL.path) (isDirectory: \(isDirectory.boolValue))") } } else { if let compiledURL = Bundle.main.url(forResource: modelPathOrName, withExtension: "mlmodelc") @@ -276,9 +274,8 @@ public class YOLOView: UIView, VideoCaptureDelegate { guard let unwrappedModelURL = modelURL else { // Model not found - allow camera preview without inference - print( - "YOLOView Warning: Model file not found: \(modelPathOrName). Camera will run without inference." - ) + NSLog( + "YOLOView: Model file not found: %@. Camera will run without inference.", modelPathOrName) self.videoCapture.predictor = nil self.activityIndicator.stopAnimating() self.labelName.text = "No Model" @@ -310,7 +307,7 @@ public class YOLOView: UIView, VideoCaptureDelegate { // Common failure handling for all tasks func handleFailure(_ error: Error) { - print("Failed to load model with error: \(error)") + NSLog("YOLOView: Failed to load model: %@", String(describing: error)) self.activityIndicator.stopAnimating() completion?(.failure(error)) } @@ -395,7 +392,8 @@ public class YOLOView: UIView, VideoCaptureDelegate { // Once everything is set up, we can start capturing live video. self.videoCapture.start() } else { - print("Failed to set up camera - permission may be denied or camera unavailable") + NSLog( + "YOLOView: Failed to set up camera - permission may be denied or camera unavailable") } self.busy = false } @@ -616,7 +614,6 @@ public class YOLOView: UIView, VideoCaptureDelegate { width: rect.width, height: rect.height) case .unknown: - print("The device orientation is unknown, the predictions may be affected") fallthrough default: break } @@ -847,7 +844,7 @@ public class YOLOView: UIView, VideoCaptureDelegate { sliderConf.addTarget(self, action: #selector(sliderChanged), for: .valueChanged) self.addSubview(sliderConf) - labelSliderIoU.text = "0.45 IoU Threshold" + labelSliderIoU.text = "0.7 IoU Threshold" labelSliderIoU.textAlignment = .left labelSliderIoU.textColor = .black labelSliderIoU.font = UIFont.preferredFont(forTextStyle: .subheadline) @@ -855,7 +852,7 @@ public class YOLOView: UIView, VideoCaptureDelegate { sliderIoU.minimumValue = 0 sliderIoU.maximumValue = 1 - sliderIoU.value = 0.45 + sliderIoU.value = 0.7 sliderIoU.minimumTrackTintColor = .darkGray sliderIoU.maximumTrackTintColor = .systemGray.withAlphaComponent(0.7) sliderIoU.addTarget(self, action: #selector(sliderChanged), for: .valueChanged) @@ -865,7 +862,7 @@ public class YOLOView: UIView, VideoCaptureDelegate { self.labelSliderNumItems.text = "0 items (max " + String(Int(sliderNumItems.value)) + ")" } self.labelSliderConf.text = "0.25 Confidence Threshold" - self.labelSliderIoU.text = "0.45 IoU Threshold" + self.labelSliderIoU.text = "0.7 IoU Threshold" labelZoom.text = "1.00x" labelZoom.textColor = .black @@ -1163,7 +1160,7 @@ public class YOLOView: UIView, VideoCaptureDelegate { } device.videoZoomFactor = factor } catch { - print("\(error.localizedDescription)") + NSLog("YOLOView: %@", error.localizedDescription) } } @@ -1210,7 +1207,7 @@ public class YOLOView: UIView, VideoCaptureDelegate { // Notify zoom change onZoomChanged?(newZoomFactor) } catch { - print("Failed to set zoom level: \(error.localizedDescription)") + NSLog("YOLOView: Failed to set zoom level: %@", error.localizedDescription) } } @@ -1229,7 +1226,7 @@ public class YOLOView: UIView, VideoCaptureDelegate { device.torchMode = .off } } catch { - print("Failed to set torch mode: \(error.localizedDescription)") + NSLog("YOLOView: Failed to set torch mode: %@", error.localizedDescription) } } @@ -1251,14 +1248,14 @@ public class YOLOView: UIView, VideoCaptureDelegate { let authStatus = AVCaptureDevice.authorizationStatus(for: .video) if authStatus != .authorized { - print("Camera permission not authorized. Cannot switch camera.") + NSLog("YOLOView: Camera permission not authorized. Cannot switch camera.") return } self.videoCapture.captureSession.beginConfiguration() guard let currentInput = self.videoCapture.captureSession.inputs.first as? AVCaptureDeviceInput else { - print("No current camera input to remove") + NSLog("YOLOView: No current camera input to remove") self.videoCapture.captureSession.commitConfiguration() return } @@ -1270,13 +1267,15 @@ public class YOLOView: UIView, VideoCaptureDelegate { let nextCameraPosition: AVCaptureDevice.Position = currentPosition == .back ? .front : .back guard let newCameraDevice = bestCaptureDevice(position: nextCameraPosition) else { - print("No camera device available for position: \(nextCameraPosition)") + NSLog( + "YOLOView: No camera device available for position: %@", + String(describing: nextCameraPosition)) self.videoCapture.captureSession.commitConfiguration() return } guard let videoInput1 = try? AVCaptureDeviceInput(device: newCameraDevice) else { - print("Failed to create AVCaptureDeviceInput for camera switch") + NSLog("YOLOView: Failed to create AVCaptureDeviceInput for camera switch") self.videoCapture.captureSession.commitConfiguration() return } @@ -1338,7 +1337,7 @@ extension YOLOView: AVCapturePhotoCaptureDelegate { _ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error? ) { if let error = error { - print("error occurred : \(error.localizedDescription)") + NSLog("YOLOView: Photo capture error: %@", error.localizedDescription) } if let dataImage = photo.fileDataRepresentation() { let dataProvider = CGDataProvider(data: dataImage as CFData) @@ -1685,7 +1684,6 @@ extension YOLOView: AVCapturePhotoCaptureDelegate { } } detection["keypoints"] = keypointsFlat - print("YOLOView: Added pose detection with \(keypoints.xy.count) keypoints") detections.append(detection) } @@ -1808,9 +1806,6 @@ extension YOLOView: AVCapturePhotoCaptureDelegate { } } detection["keypoints"] = keypointsFlat - print( - "YOLOView: Added keypoints data (\(keypoints.xy.count) points) for detection \(detectionIndex)" - ) } // Add OBB data (if available and enabled) diff --git a/lib/config/channel_config.dart b/lib/config/channel_config.dart index f7904e00..f223df19 100644 --- a/lib/config/channel_config.dart +++ b/lib/config/channel_config.dart @@ -2,66 +2,43 @@ import 'package:flutter/services.dart'; -/// Centralized channel configuration for YOLO operations. -/// -/// This class provides a unified way to handle method channel setup, -/// naming conventions, and communication patterns that were previously -/// duplicated across multiple files in the codebase. +/// Centralized channel naming and construction for YOLO platform interop. class ChannelConfig { - // Channel name constants static const String singleImageChannel = 'yolo_single_image_channel'; static const String controlChannelPrefix = 'com.ultralytics.yolo/controlChannel_'; static const String detectionResultsPrefix = 'com.ultralytics.yolo/detectionResults_'; - /// Creates a method channel with the standard YOLO naming convention. - /// - /// [channelName] The base name for the channel - /// [instanceId] Optional instance ID for multi-instance support - /// Returns a properly configured MethodChannel + /// Creates a method channel, suffixed with [instanceId] unless it is the + /// `default` (single-instance) sentinel. static MethodChannel createChannel(String channelName, {String? instanceId}) { - // Treat 'default' as null for backward compatibility final fullChannelName = instanceId != null && instanceId != 'default' ? '${channelName}_$instanceId' : channelName; return MethodChannel(fullChannelName); } - /// Creates a YOLO single image channel for static operations. - /// - /// [instanceId] Optional instance ID for multi-instance support - /// Returns a MethodChannel configured for single image operations - static MethodChannel createSingleImageChannel({String? instanceId}) { - return createChannel(singleImageChannel, instanceId: instanceId); - } + /// Channel for single-image inference and static plugin operations. + static MethodChannel createSingleImageChannel({String? instanceId}) => + createChannel(singleImageChannel, instanceId: instanceId); - /// Creates a YOLO control channel for platform view operations. - /// - /// [viewId] The unique view ID for the platform view - /// Returns a MethodChannel configured for control operations - static MethodChannel createControlChannel(String viewId) { - return MethodChannel('$controlChannelPrefix$viewId'); - } + /// Control channel for a specific YOLO platform view. + static MethodChannel createControlChannel(String viewId) => + MethodChannel('$controlChannelPrefix$viewId'); - /// Creates a YOLO detection results event channel. - /// - /// [viewId] The unique view ID for the platform view - /// Returns an EventChannel configured for detection results - static EventChannel createDetectionResultsChannel(String viewId) { - return EventChannel('$detectionResultsPrefix$viewId'); - } + /// Event channel that streams detection results for a specific view. + static EventChannel createDetectionResultsChannel(String viewId) => + EventChannel('$detectionResultsPrefix$viewId'); - /// Validates method call arguments for common patterns. - /// - /// [call] The method call to validate - /// [requiredKeys] List of required argument keys - /// Throws ArgumentError if validation fails + @Deprecated( + 'Use typed arguments directly at the call site. This shim will be removed ' + 'in a future release.', + ) static void validateMethodCall(MethodCall call, List requiredKeys) { if (call.arguments is! Map) { throw ArgumentError('Method ${call.method} requires Map arguments'); } - final args = call.arguments as Map; for (final key in requiredKeys) { if (!args.containsKey(key)) { @@ -70,13 +47,10 @@ class ChannelConfig { } } - /// Creates standardized method call arguments for common operations. - /// - /// [viewId] Optional view ID for platform view operations - /// [modelPath] Optional model path for model operations - /// [task] Optional task name for task-specific operations - /// [additionalArgs] Additional arguments to include - /// Returns a Map with standardized argument structure + @Deprecated( + 'Build the argument map inline instead. This shim will be removed in a ' + 'future release.', + ) static Map createStandardArgs({ int? viewId, String? modelPath, @@ -84,15 +58,10 @@ class ChannelConfig { Map? additionalArgs, }) { final args = {}; - if (viewId != null) args['viewId'] = viewId; if (modelPath != null) args['modelPath'] = modelPath; if (task != null) args['task'] = task; - - if (additionalArgs != null) { - args.addAll(additionalArgs); - } - + if (additionalArgs != null) args.addAll(additionalArgs); return args; } } diff --git a/lib/core/yolo_inference.dart b/lib/core/yolo_inference.dart index fa42855a..e8b0499a 100644 --- a/lib/core/yolo_inference.dart +++ b/lib/core/yolo_inference.dart @@ -63,8 +63,6 @@ class YOLOInference { } throw InferenceException('Invalid result format returned from inference'); - } on PlatformException catch (e) { - throw YOLOErrorHandler.handleError(e, 'Error during image prediction'); } catch (e) { throw YOLOErrorHandler.handleError(e, 'Error during image prediction'); } diff --git a/lib/core/yolo_model_manager.dart b/lib/core/yolo_model_manager.dart index 2b9d6788..e95c5e69 100644 --- a/lib/core/yolo_model_manager.dart +++ b/lib/core/yolo_model_manager.dart @@ -92,11 +92,6 @@ class YOLOModelManager { final result = await _channel.invokeMethod('loadModel', arguments); return result == true; - } on PlatformException catch (e) { - throw YOLOErrorHandler.handleError( - e, - 'Failed to load model $_modelPath for task ${_task.name}', - ); } catch (e) { throw YOLOErrorHandler.handleError( e, @@ -123,11 +118,6 @@ class YOLOModelManager { } await _channel.invokeMethod('setModel', arguments); - } on PlatformException catch (e) { - throw YOLOErrorHandler.handleError( - e, - 'Failed to switch to model $newModelPath for task ${newTask.name}', - ); } catch (e) { throw YOLOErrorHandler.handleError( e, diff --git a/lib/core/yolo_model_resolver.dart b/lib/core/yolo_model_resolver.dart index cbc2de82..033e16f7 100644 --- a/lib/core/yolo_model_resolver.dart +++ b/lib/core/yolo_model_resolver.dart @@ -143,6 +143,11 @@ class YOLOModelResolver { .toList(growable: false); } + static String? defaultOfficialModel({YOLOTask task = YOLOTask.detect}) { + final models = officialModels(task: task); + return models.isEmpty ? null : models.first; + } + static bool isOfficialModel(String source) => _officialModelForId(_normalizeOfficialModelId(source)) != null; diff --git a/lib/models/yolo_result.dart b/lib/models/yolo_result.dart index bb408432..971bd3a2 100644 --- a/lib/models/yolo_result.dart +++ b/lib/models/yolo_result.dart @@ -142,18 +142,17 @@ class YOLOResult { final angle = map['angle'] is num ? (map['angle'] as num).toDouble() : null; final polygonRaw = map['polygon'] ?? map['obbPoints']; - final polygon = polygonRaw is List ? polygonRaw : null; - final obbPoints = polygon - ?.whereType() - .map(MapConverter.convertToTypedMap) - .where((pointMap) => pointMap['x'] is num && pointMap['y'] is num) - .map( - (pointMap) => { - 'x': (pointMap['x'] as num).toDouble(), - 'y': (pointMap['y'] as num).toDouble(), - }, - ) - .toList(); + List>? obbPoints; + if (polygonRaw is List) { + obbPoints = >[]; + for (final point in polygonRaw) { + if (point is! Map) continue; + final x = point['x']; + final y = point['y']; + if (x is! num || y is! num) continue; + obbPoints.add({'x': x.toDouble(), 'y': y.toDouble()}); + } + } return YOLOResult( classIndex: classIndex, @@ -219,21 +218,8 @@ class YOLOResult { } } -/// Represents a collection of detection results from YOLO models. -/// -/// This class encapsulates the complete output from a YOLO inference, -/// including all detected objects, an optional annotated image showing -/// the detections, and performance metrics. -/// -/// Example: -/// ```dart -/// final results = await yolo.predict(imageBytes); -/// print('Found ${results.detections.length} objects'); -/// print('Processing took ${results.processingTimeMs}ms'); -/// if (results.annotatedImage != null) { -/// // Display or save the annotated image -/// } -/// ``` +/// Complete output from a YOLO inference: detections, optional annotated +/// image, and processing time in milliseconds. class YOLODetectionResults { /// List of all objects detected in the image. /// @@ -298,13 +284,7 @@ class YOLODetectionResults { } } -/// Represents a point in 2D space. -/// -/// Example: -/// ```dart -/// final point = Point(150.5, 200.0); -/// print('Point at (${point.x}, ${point.y})'); -/// ``` +/// A 2D point in image/pixel space. class Point { final double x; final double y; diff --git a/lib/ultralytics_yolo.dart b/lib/ultralytics_yolo.dart index 542bf575..05f29ab7 100644 --- a/lib/ultralytics_yolo.dart +++ b/lib/ultralytics_yolo.dart @@ -1,15 +1,17 @@ // Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license -export 'yolo.dart'; export 'models/yolo_exceptions.dart'; export 'models/yolo_result.dart'; export 'models/yolo_task.dart'; -export 'yolo_view.dart'; -export 'yolo_streaming_config.dart'; -export 'yolo_performance_metrics.dart'; +export 'yolo.dart'; export 'yolo_instance_manager.dart'; -export 'utils/error_handler.dart'; -export 'utils/map_converter.dart'; +export 'yolo_performance_metrics.dart'; +export 'yolo_streaming_config.dart'; +export 'yolo_view.dart'; export 'config/channel_config.dart'; -export 'platform/yolo_platform_interface.dart'; export 'platform/yolo_platform_impl.dart'; +export 'platform/yolo_platform_interface.dart'; +export 'utils/error_handler.dart'; +export 'utils/map_converter.dart'; +export 'widgets/yolo_controller.dart' show YOLOViewController; +export 'widgets/yolo_overlay.dart' show YOLOOverlay, YOLOOverlayTheme; diff --git a/lib/utils/map_converter.dart b/lib/utils/map_converter.dart index 5b90c053..cd9120e5 100644 --- a/lib/utils/map_converter.dart +++ b/lib/utils/map_converter.dart @@ -4,210 +4,102 @@ import 'dart:typed_data'; import 'dart:ui'; import '../models/yolo_result.dart'; -/// Centralized map conversion utilities for YOLO operations. -/// -/// This class provides a unified way to handle map conversions and data parsing -/// that was previously duplicated across multiple files in the codebase. +/// Helpers for adapting loosely-typed platform-channel data +/// (`Map`, `List`) to strongly-typed Dart values. class MapConverter { - /// Converts a dynamic map to a typed String-dynamic map. - /// - /// This method centralizes the map conversion pattern that was repeated - /// throughout the codebase, particularly in result processing. - /// - /// [map] The dynamic map to convert - /// Returns a properly typed `Map` + /// Converts a dynamic map to `Map`. static Map convertToTypedMap(Map map) { return Map.fromEntries( map.entries.map((e) => MapEntry(e.key.toString(), e.value)), ); } - /// Converts a list of dynamic maps to a list of typed maps. - /// - /// This method handles the common pattern of converting lists of maps - /// that was repeated in detection result processing. - /// - /// [maps] The list of dynamic maps to convert - /// Returns a list of properly typed `Map` + /// Null-safe variant of [convertToTypedMap]. + static Map? convertToTypedMapSafe( + Map? map, + ) => map == null ? null : convertToTypedMap(map); + + /// Converts a list of dynamic maps to `List>`, skipping + /// non-map entries. static List> convertMapsList(List maps) { - return maps.whereType().map((item) { - return convertToTypedMap(item); - }).toList(); + return maps.whereType().map(convertToTypedMap).toList(); } - /// Converts detection boxes from dynamic format to typed format. - /// - /// This method centralizes the box conversion logic that was duplicated - /// across multiple files, particularly in result processing. - /// - /// [boxes] The list of dynamic box data - /// Returns a list of properly typed box maps - static List> convertBoxesList(List boxes) { - return boxes.whereType().map((item) { - return convertToTypedMap(item); - }).toList(); - } + /// Alias of [convertMapsList] for detection-box lists. + static List> convertBoxesList(List boxes) => + convertMapsList(boxes); - /// Safely extracts a double value from a map with fallback. - /// - /// This method centralizes the safe double extraction pattern that was - /// repeated throughout the codebase for coordinate and confidence values. - /// - /// [map] The map to extract from - /// [key] The key to extract - /// [fallback] The fallback value if extraction fails - /// Returns the extracted double value or fallback + /// Reads `map[key]` as double; returns [fallback] if absent or non-numeric. static double safeGetDouble( Map map, String key, { double fallback = 0.0, }) { final value = map[key]; - if (value is num) { - return value.toDouble(); - } - return fallback; + return value is num ? value.toDouble() : fallback; } - /// Safely extracts an int value from a map with fallback. - /// - /// This method centralizes the safe int extraction pattern that was - /// repeated throughout the codebase for index and count values. - /// - /// [map] The map to extract from - /// [key] The key to extract - /// [fallback] The fallback value if extraction fails - /// Returns the extracted int value or fallback + /// Reads `map[key]` as int; returns [fallback] if absent or non-numeric. static int safeGetInt( Map map, String key, { int fallback = 0, }) { final value = map[key]; - if (value is num) { - return value.toInt(); - } - return fallback; + return value is num ? value.toInt() : fallback; } - /// Safely extracts a string value from a map with fallback. - /// - /// This method centralizes the safe string extraction pattern that was - /// repeated throughout the codebase for class names and labels. - /// - /// [map] The map to extract from - /// [key] The key to extract - /// [fallback] The fallback value if extraction fails - /// Returns the extracted string value or fallback + /// Reads `map[key]` as String; returns [fallback] if absent or wrong type. static String safeGetString( Map map, String key, { String fallback = '', }) { final value = map[key]; - if (value is String) { - return value; - } - return fallback; + return value is String ? value : fallback; } - /// Converts a bounding box map to a Rect object. - /// - /// This method centralizes the bounding box conversion logic that was - /// duplicated across multiple files in result processing. - /// - /// [boxMap] The map containing bounding box coordinates - /// Returns a Rect object with the bounding box coordinates - static Rect convertBoundingBox(Map boxMap) { - return Rect.fromLTRB( - safeGetDouble(boxMap, 'left'), - safeGetDouble(boxMap, 'top'), - safeGetDouble(boxMap, 'right'), - safeGetDouble(boxMap, 'bottom'), - ); + /// Reads `map[key]` as [Uint8List] or returns null. + static Uint8List? safeGetUint8List(Map map, String key) { + final value = map[key]; + return value is Uint8List ? value : null; } - /// Converts a list of keypoint data to Point objects. - /// - /// This method centralizes the keypoint conversion logic that was - /// duplicated in pose estimation result processing. - /// - /// [keypointsData] The list of keypoint data (x, y, confidence triplets) - /// Returns a list of Point objects and their confidence values + /// Converts a `{left, top, right, bottom}` map to a [Rect]. + static Rect convertBoundingBox(Map boxMap) => Rect.fromLTRB( + safeGetDouble(boxMap, 'left'), + safeGetDouble(boxMap, 'top'), + safeGetDouble(boxMap, 'right'), + safeGetDouble(boxMap, 'bottom'), + ); + + /// Unpacks a flat `[x0, y0, c0, x1, y1, c1, ...]` keypoint list into + /// parallel lists of points and confidences. static ({List keypoints, List confidences}) convertKeypoints( List keypointsData, ) { + final count = keypointsData.length ~/ 3; final keypoints = []; final confidences = []; - - for (var i = 0; i < keypointsData.length; i += 3) { - if (i + 2 < keypointsData.length) { - final x = keypointsData[i] is num - ? (keypointsData[i] as num).toDouble() - : 0.0; - final y = keypointsData[i + 1] is num - ? (keypointsData[i + 1] as num).toDouble() - : 0.0; - final confidence = keypointsData[i + 2] is num - ? (keypointsData[i + 2] as num).toDouble() - : 0.0; - - keypoints.add(Point(x, y)); - confidences.add(confidence); - } + for (var i = 0; i < count; i++) { + final base = i * 3; + final x = keypointsData[base]; + final y = keypointsData[base + 1]; + final c = keypointsData[base + 2]; + keypoints.add( + Point(x is num ? x.toDouble() : 0.0, y is num ? y.toDouble() : 0.0), + ); + confidences.add(c is num ? c.toDouble() : 0.0); } - return (keypoints: keypoints, confidences: confidences); } - /// Converts mask data from dynamic format to typed format. - /// - /// This method centralizes the mask conversion logic that was - /// duplicated in segmentation result processing. - /// - /// [maskData] The dynamic mask data - /// Returns a properly typed mask as `List>` + /// Converts a list of rows of numbers into a typed `List>` + /// mask. Non-numeric values become `0.0`. static List> convertMaskData(List maskData) { return maskData.map((row) { - if (row is List) { - return row.map((val) { - if (val is num) { - return val.toDouble(); - } - return 0.0; - }).toList(); - } - return []; + if (row is! List) return const []; + return row.map((v) => v is num ? v.toDouble() : 0.0).toList(); }).toList(); } - - /// Safely extracts a Uint8List from a map. - /// - /// This method centralizes the Uint8List extraction pattern that was - /// repeated in image data processing. - /// - /// [map] The map to extract from - /// [key] The key to extract - /// Returns the Uint8List or null if not found - static Uint8List? safeGetUint8List(Map map, String key) { - final value = map[key]; - if (value is Uint8List) { - return value; - } - return null; - } - - /// Converts a map to a typed map with null safety. - /// - /// This method provides a safe way to convert dynamic maps to typed maps - /// with proper null handling that was inconsistent across the codebase. - /// - /// [map] The dynamic map to convert - /// Returns a properly typed map with null safety - static Map? convertToTypedMapSafe( - Map? map, - ) { - if (map == null) return null; - return convertToTypedMap(map); - } } diff --git a/lib/widgets/yolo_controller.dart b/lib/widgets/yolo_controller.dart index 5e81edba..5c8abb41 100644 --- a/lib/widgets/yolo_controller.dart +++ b/lib/widgets/yolo_controller.dart @@ -3,15 +3,15 @@ import 'package:flutter/services.dart'; import 'package:ultralytics_yolo/core/yolo_model_resolver.dart'; import 'package:ultralytics_yolo/models/yolo_task.dart'; -import 'package:ultralytics_yolo/yolo_streaming_config.dart'; import 'package:ultralytics_yolo/utils/logger.dart'; +import 'package:ultralytics_yolo/yolo_streaming_config.dart'; -/// Controller for managing YOLO detection settings and camera controls. +/// Controls a [YOLOView] imperatively: thresholds, camera, zoom, streaming. class YOLOViewController { MethodChannel? _methodChannel; int? _viewId; - double _confidenceThreshold = 0.5; - double _iouThreshold = 0.45; + double _confidenceThreshold = 0.25; + double _iouThreshold = 0.7; int _numItemsThreshold = 30; double get confidenceThreshold => _confidenceThreshold; @@ -24,60 +24,39 @@ class YOLOViewController { void init(MethodChannel methodChannel, int viewId) { _methodChannel = methodChannel; _viewId = viewId; - _applyThresholds(); + _invoke('setThresholds', { + 'confidenceThreshold': _confidenceThreshold, + 'iouThreshold': _iouThreshold, + 'numItemsThreshold': _numItemsThreshold, + }); } - Future _applyThresholds() async { - if (_methodChannel != null) { - try { - await _methodChannel!.invokeMethod('setThresholds', { - 'confidenceThreshold': _confidenceThreshold, - 'iouThreshold': _iouThreshold, - 'numItemsThreshold': _numItemsThreshold, - }); - } catch (e) { - logInfo('Error applying thresholds: $e'); - } + Future _invoke(String method, [Map? args]) async { + final channel = _methodChannel; + if (channel == null) return null; + try { + return await channel.invokeMethod(method, args); + } catch (e) { + logInfo('YOLOViewController.$method failed: $e'); + return null; } } Future setConfidenceThreshold(double threshold) async { _confidenceThreshold = threshold.clamp(0.0, 1.0); - if (_methodChannel != null) { - try { - await _methodChannel!.invokeMethod('setConfidenceThreshold', { - 'threshold': _confidenceThreshold, - }); - } catch (e) { - logInfo('Error setting confidence threshold: $e'); - } - } + await _invoke('setConfidenceThreshold', { + 'threshold': _confidenceThreshold, + }); } Future setIoUThreshold(double threshold) async { _iouThreshold = threshold.clamp(0.0, 1.0); - if (_methodChannel != null) { - try { - await _methodChannel!.invokeMethod('setIoUThreshold', { - 'threshold': _iouThreshold, - }); - } catch (e) { - logInfo('Error setting IoU threshold: $e'); - } - } + await _invoke('setIoUThreshold', {'threshold': _iouThreshold}); } Future setNumItemsThreshold(int numItems) async { _numItemsThreshold = numItems.clamp(1, 100); - if (_methodChannel != null) { - try { - await _methodChannel!.invokeMethod('setNumItemsThreshold', { - 'numItems': _numItemsThreshold, - }); - } catch (e) { - logInfo('Error setting num items threshold: $e'); - } - } + await _invoke('setNumItemsThreshold', {'numItems': _numItemsThreshold}); } Future setThresholds({ @@ -94,160 +73,62 @@ class YOLOViewController { if (numItemsThreshold != null) { _numItemsThreshold = numItemsThreshold.clamp(1, 100); } - - if (_methodChannel != null) { - try { - await _methodChannel!.invokeMethod('setThresholds', { - 'confidenceThreshold': _confidenceThreshold, - 'iouThreshold': _iouThreshold, - 'numItemsThreshold': _numItemsThreshold, - }); - } catch (e) { - logInfo('Error setting thresholds: $e'); - } - } + await _invoke('setThresholds', { + 'confidenceThreshold': _confidenceThreshold, + 'iouThreshold': _iouThreshold, + 'numItemsThreshold': _numItemsThreshold, + }); } - Future switchCamera() async { - if (_methodChannel != null) { - try { - await _methodChannel!.invokeMethod('switchCamera'); - } catch (e) { - logInfo('Error switching camera: $e'); - } - } - } + Future switchCamera() => _invoke('switchCamera'); - Future setTorchMode(bool enabled) async { - if (_methodChannel != null) { - try { - await _methodChannel!.invokeMethod('setTorchMode', { - 'enabled': enabled, - }); - } catch (e) { - logInfo('Error setting torch mode: $e'); - } - } - } + Future setTorchMode(bool enabled) => + _invoke('setTorchMode', {'enabled': enabled}); - Future zoomIn() async { - if (_methodChannel != null) { - try { - await _methodChannel!.invokeMethod('zoomIn'); - } catch (e) { - logInfo('Error zooming in: $e'); - } - } - } + Future zoomIn() => _invoke('zoomIn'); - Future zoomOut() async { - if (_methodChannel != null) { - try { - await _methodChannel!.invokeMethod('zoomOut'); - } catch (e) { - logInfo('Error zooming out: $e'); - } - } - } + Future zoomOut() => _invoke('zoomOut'); - Future setZoomLevel(double zoomLevel) async { - if (_methodChannel != null) { - try { - await _methodChannel!.invokeMethod('setZoomLevel', { - 'zoomLevel': zoomLevel, - }); - } catch (e) { - logInfo('Error setting zoom level: $e'); - } - } - } + Future setZoomLevel(double zoomLevel) => + _invoke('setZoomLevel', {'zoomLevel': zoomLevel}); Future switchModel(String modelPath, [YOLOTask? task]) async { - if (_methodChannel != null && _viewId != null) { - final resolvedModel = await YOLOModelResolver.resolve( - modelPath: modelPath, - task: task, - ); - await _methodChannel!.invokeMethod('setModel', { - 'modelPath': resolvedModel.modelPath, - 'task': resolvedModel.task.name, + if (_methodChannel == null || _viewId == null) return; + final resolvedModel = await YOLOModelResolver.resolve( + modelPath: modelPath, + task: task, + ); + await _invoke('setModel', { + 'modelPath': resolvedModel.modelPath, + 'task': resolvedModel.task.name, + }); + } + + Future setStreamingConfig(YOLOStreamingConfig config) => + _invoke('setStreamingConfig', { + 'includeDetections': config.includeDetections, + 'includeClassifications': config.includeClassifications, + 'includeProcessingTimeMs': config.includeProcessingTimeMs, + 'includeFps': config.includeFps, + 'includeMasks': config.includeMasks, + 'includePoses': config.includePoses, + 'includeOBB': config.includeOBB, + 'includeOriginalImage': config.includeOriginalImage, + 'maxFPS': config.maxFPS, + 'throttleIntervalMs': config.throttleInterval?.inMilliseconds, + 'inferenceFrequency': config.inferenceFrequency, + 'skipFrames': config.skipFrames, }); - } - } - - Future setStreamingConfig(YOLOStreamingConfig config) async { - if (_methodChannel != null) { - try { - await _methodChannel!.invokeMethod('setStreamingConfig', { - 'includeDetections': config.includeDetections, - 'includeClassifications': config.includeClassifications, - 'includeProcessingTimeMs': config.includeProcessingTimeMs, - 'includeFps': config.includeFps, - 'includeMasks': config.includeMasks, - 'includePoses': config.includePoses, - 'includeOBB': config.includeOBB, - 'includeOriginalImage': config.includeOriginalImage, - 'maxFPS': config.maxFPS, - 'throttleIntervalMs': config.throttleInterval?.inMilliseconds, - 'inferenceFrequency': config.inferenceFrequency, - 'skipFrames': config.skipFrames, - }); - } catch (e) { - logInfo('Error setting streaming config: $e'); - } - } - } - Future stop() async { - if (_methodChannel != null) { - try { - await _methodChannel!.invokeMethod('stop'); - } catch (e) { - logInfo('Error stopping: $e'); - } - } - } + Future stop() => _invoke('stop'); - Future restartCamera() async { - if (_methodChannel != null) { - try { - await _methodChannel!.invokeMethod('restartCamera'); - } catch (e) { - logInfo('Error restarting camera: $e'); - } - } - } + Future restartCamera() => _invoke('restartCamera'); - Future setShowUIControls(bool show) async { - if (_methodChannel != null) { - try { - await _methodChannel!.invokeMethod('setShowUIControls', {'show': show}); - } catch (e) { - logInfo('Error setting UI controls: $e'); - } - } - } + Future setShowUIControls(bool show) => + _invoke('setShowUIControls', {'show': show}); - Future setShowOverlays(bool show) async { - if (_methodChannel != null) { - try { - await _methodChannel!.invokeMethod('setShowOverlays', {'show': show}); - } catch (e) { - logInfo('Error setting overlay visibility: $e'); - } - } - } + Future setShowOverlays(bool show) => + _invoke('setShowOverlays', {'show': show}); - Future captureFrame() async { - if (_methodChannel != null) { - try { - final result = await _methodChannel!.invokeMethod('captureFrame'); - return result is Uint8List ? result : null; - } catch (e) { - logInfo('Error capturing frame: $e'); - return null; - } - } - return null; - } + Future captureFrame() => _invoke('captureFrame'); } diff --git a/lib/widgets/yolo_controls.dart b/lib/widgets/yolo_controls.dart index 76b4789e..d1133c45 100644 --- a/lib/widgets/yolo_controls.dart +++ b/lib/widgets/yolo_controls.dart @@ -3,7 +3,11 @@ import 'package:flutter/material.dart'; import 'package:ultralytics_yolo/widgets/yolo_controller.dart'; -/// A widget that provides UI controls for YOLO detection settings. +/// Backward-compatible control widgets retained as deprecated shims. +@Deprecated( + 'Build controls in your app with YOLOViewController directly. This wrapper ' + 'will be removed in a future release.', +) class YOLOControls extends StatelessWidget { final YOLOViewController controller; final bool showAdvanced; @@ -19,9 +23,9 @@ class YOLOControls extends StatelessWidget { @override Widget build(BuildContext context) { return Card( - margin: const EdgeInsets.all(8.0), + margin: const EdgeInsets.all(8), child: Padding( - padding: const EdgeInsets.all(16.0), + padding: const EdgeInsets.all(16), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -31,106 +35,82 @@ class YOLOControls extends StatelessWidget { style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: 16), - _buildConfidenceSlider(), + _ThresholdSlider( + label: + 'Confidence: ${(controller.confidenceThreshold * 100).toStringAsFixed(0)}%', + value: controller.confidenceThreshold, + onChanged: (value) { + controller.setConfidenceThreshold(value); + onControlsChanged?.call(); + }, + ), const SizedBox(height: 16), - _buildIoUSlider(), + _ThresholdSlider( + label: + 'IoU Threshold: ${(controller.iouThreshold * 100).toStringAsFixed(0)}%', + value: controller.iouThreshold, + onChanged: (value) { + controller.setIoUThreshold(value); + onControlsChanged?.call(); + }, + ), if (showAdvanced) ...[ const SizedBox(height: 16), - _buildNumItemsSlider(), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Max Items: ${controller.numItemsThreshold}'), + Slider( + value: controller.numItemsThreshold.toDouble(), + min: 1, + max: 100, + divisions: 99, + onChanged: (value) { + controller.setNumItemsThreshold(value.round()); + onControlsChanged?.call(); + }, + ), + ], + ), ], const SizedBox(height: 16), - _buildCameraControls(), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ElevatedButton.icon( + onPressed: controller.isInitialized + ? controller.switchCamera + : null, + icon: const Icon(Icons.switch_camera), + label: const Text('Switch Camera'), + ), + ElevatedButton.icon( + onPressed: controller.isInitialized + ? controller.zoomIn + : null, + icon: const Icon(Icons.zoom_in), + label: const Text('Zoom In'), + ), + ElevatedButton.icon( + onPressed: controller.isInitialized + ? controller.zoomOut + : null, + icon: const Icon(Icons.zoom_out), + label: const Text('Zoom Out'), + ), + ], + ), ], ), ), ); } - - Widget _buildConfidenceSlider() { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Confidence: ${(controller.confidenceThreshold * 100).toStringAsFixed(0)}%', - ), - Slider( - value: controller.confidenceThreshold, - min: 0.0, - max: 1.0, - divisions: 20, - onChanged: (value) { - controller.setConfidenceThreshold(value); - onControlsChanged?.call(); - }, - ), - ], - ); - } - - Widget _buildIoUSlider() { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'IoU Threshold: ${(controller.iouThreshold * 100).toStringAsFixed(0)}%', - ), - Slider( - value: controller.iouThreshold, - min: 0.0, - max: 1.0, - divisions: 20, - onChanged: (value) { - controller.setIoUThreshold(value); - onControlsChanged?.call(); - }, - ), - ], - ); - } - - Widget _buildNumItemsSlider() { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Max Items: ${controller.numItemsThreshold}'), - Slider( - value: controller.numItemsThreshold.toDouble(), - min: 1.0, - max: 100.0, - divisions: 99, - onChanged: (value) { - controller.setNumItemsThreshold(value.round()); - onControlsChanged?.call(); - }, - ), - ], - ); - } - - Widget _buildCameraControls() { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - ElevatedButton.icon( - onPressed: controller.isInitialized ? controller.switchCamera : null, - icon: const Icon(Icons.switch_camera), - label: const Text('Switch Camera'), - ), - ElevatedButton.icon( - onPressed: controller.isInitialized ? controller.zoomIn : null, - icon: const Icon(Icons.zoom_in), - label: const Text('Zoom In'), - ), - ElevatedButton.icon( - onPressed: controller.isInitialized ? controller.zoomOut : null, - icon: const Icon(Icons.zoom_out), - label: const Text('Zoom Out'), - ), - ], - ); - } } +@Deprecated( + 'Build controls in your app with YOLOViewController directly. This wrapper ' + 'will be removed in a future release.', +) class YOLOControlsCompact extends StatelessWidget { final YOLOViewController controller; @@ -139,10 +119,10 @@ class YOLOControlsCompact extends StatelessWidget { @override Widget build(BuildContext context) { return Container( - padding: const EdgeInsets.all(8.0), + padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.black54, - borderRadius: BorderRadius.circular(8.0), + borderRadius: BorderRadius.circular(8), ), child: Row( mainAxisSize: MainAxisSize.min, @@ -169,3 +149,32 @@ class YOLOControlsCompact extends StatelessWidget { ); } } + +class _ThresholdSlider extends StatelessWidget { + final String label; + final double value; + final ValueChanged onChanged; + + const _ThresholdSlider({ + required this.label, + required this.value, + required this.onChanged, + }); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label), + Slider( + value: value, + min: 0, + max: 1, + divisions: 20, + onChanged: onChanged, + ), + ], + ); + } +} diff --git a/lib/yolo.dart b/lib/yolo.dart index d9aa2c70..2c811af5 100644 --- a/lib/yolo.dart +++ b/lib/yolo.dart @@ -1,17 +1,17 @@ // Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license import 'package:flutter/services.dart'; +import 'package:ultralytics_yolo/config/channel_config.dart'; +import 'package:ultralytics_yolo/core/yolo_inference.dart'; +import 'package:ultralytics_yolo/core/yolo_model_manager.dart'; import 'package:ultralytics_yolo/core/yolo_model_resolver.dart'; -import 'package:ultralytics_yolo/models/yolo_task.dart'; import 'package:ultralytics_yolo/models/yolo_exceptions.dart'; +import 'package:ultralytics_yolo/models/yolo_task.dart'; import 'package:ultralytics_yolo/yolo_instance_manager.dart'; -import 'package:ultralytics_yolo/core/yolo_inference.dart'; -import 'package:ultralytics_yolo/core/yolo_model_manager.dart'; -import 'package:ultralytics_yolo/config/channel_config.dart'; -export 'models/yolo_task.dart'; export 'models/yolo_exceptions.dart'; export 'models/yolo_result.dart'; +export 'models/yolo_task.dart'; export 'yolo_instance_manager.dart'; /// YOLO (You Only Look Once) is a class that provides machine learning inference @@ -111,6 +111,10 @@ class YOLO { static List officialModels({YOLOTask? task}) => YOLOModelResolver.officialModels(task: task); + /// Returns the default official model ID for [task] on the current platform. + static String? defaultOfficialModel({YOLOTask task = YOLOTask.detect}) => + YOLOModelResolver.defaultOfficialModel(task: task); + YOLOTask? get resolvedTask => _resolvedModel?.task ?? task; Future _ensureResolved() async { @@ -180,12 +184,7 @@ class YOLO { /// /// Example: /// ```dart - /// bool success = await yolo.loadModel(); - /// if (success) { - /// print('Model loaded successfully'); - /// } else { - /// print('Failed to load model'); - /// } + /// final ok = await yolo.loadModel(); /// ``` /// /// throws [ModelLoadingException] if the model file cannot be found @@ -222,32 +221,14 @@ class YOLO { /// /// Example: /// ```dart - /// // Basic detection usage - /// final results = await yolo.predict(imageBytes); - /// final boxes = results['boxes'] as List; - /// for (var box in boxes) { - /// print('Class: ${box['class']}, Confidence: ${box['confidence']}'); - /// } - /// - /// // Pose estimation with YOLOResult /// final results = await yolo.predict(imageBytes); - /// final detections = results['detections'] as List; - /// for (var detection in detections) { - /// final result = YOLOResult.fromMap(detection); - /// if (result.keypoints != null) { - /// print('Found ${result.keypoints!.length} keypoints'); - /// for (int i = 0; i < result.keypoints!.length; i++) { - /// final kp = result.keypoints![i]; - /// final conf = result.keypointConfidences![i]; - /// print('Keypoint $i: (${kp.x}, ${kp.y}) confidence: $conf'); - /// } - /// } - /// } + /// final detections = (results['detections'] as List) + /// .map((d) => YOLOResult.fromMap(d as Map)); /// ``` /// /// [imageBytes] The raw image data as a Uint8List /// [confidenceThreshold] Optional confidence threshold (0.0-1.0). Defaults to 0.25 if not specified. - /// [iouThreshold] Optional IoU threshold for NMS (0.0-1.0). Defaults to 0.4 if not specified. + /// [iouThreshold] Optional IoU threshold for NMS (0.0-1.0). Defaults to 0.7 if not specified. /// returns A map containing: /// - 'boxes': List of bounding boxes /// - 'detections': List of YOLOResult-compatible detection maps diff --git a/lib/yolo_performance_metrics.dart b/lib/yolo_performance_metrics.dart index f1e2d3d1..ef8b4304 100644 --- a/lib/yolo_performance_metrics.dart +++ b/lib/yolo_performance_metrics.dart @@ -1,19 +1,8 @@ // Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license -/// Performance metrics from YOLO inference processing. -/// -/// This class provides real-time performance data about YOLO model execution, -/// including frame rate, processing time, and frame tracking information. -/// -/// Example: -/// ```dart -/// YOLOView( -/// onPerformanceMetrics: (YOLOPerformanceMetrics metrics) { -/// print('FPS: ${metrics.fps}'); -/// print('Processing time: ${metrics.processingTimeMs}ms'); -/// }, -/// ) -/// ``` +/// Real-time performance data for a YOLO inference: frame rate, processing +/// time, frame counter, and timestamp. Delivered via +/// [YOLOView.onPerformanceMetrics]. class YOLOPerformanceMetrics { /// Current frames per second. /// diff --git a/lib/yolo_view.dart b/lib/yolo_view.dart index f1b3c338..92eb6403 100644 --- a/lib/yolo_view.dart +++ b/lib/yolo_view.dart @@ -49,8 +49,8 @@ class YOLOView extends StatefulWidget { this.showNativeUI = false, this.onZoomChanged, this.streamingConfig, - this.confidenceThreshold = 0.5, - this.iouThreshold = 0.45, + this.confidenceThreshold = 0.25, + this.iouThreshold = 0.7, this.useGpu = true, this.showOverlays = true, this.overlayTheme = const YOLOOverlayTheme(), @@ -173,22 +173,10 @@ class _YOLOViewState extends State { } List _parseDetectionResults(Map event) { - final List detectionsData = event['detections'] ?? []; + final detectionsData = event['detections'] as List? ?? const []; final results = []; - for (final detection in detectionsData) { if (detection is! Map) continue; - - // Validate required fields - if (!detection.containsKey('classIndex') || - !detection.containsKey('className') || - !detection.containsKey('confidence') || - !detection.containsKey('boundingBox') || - !detection.containsKey('normalizedBox')) { - continue; - } - - // Validate non-null values if (detection['classIndex'] == null || detection['className'] == null || detection['confidence'] == null || @@ -196,15 +184,12 @@ class _YOLOViewState extends State { detection['normalizedBox'] == null) { continue; } - try { - final result = YOLOResult.fromMap(detection); - results.add(result); + results.add(YOLOResult.fromMap(detection)); } catch (e) { logInfo('YOLOView: Error parsing detection: $e'); } } - return results; } diff --git a/pubspec.yaml b/pubspec.yaml index b903bd3e..39d0e558 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,11 +2,10 @@ name: ultralytics_yolo description: Flutter plugin for Ultralytics YOLO computer vision models. -version: 0.2.0 +version: 0.3.0 homepage: https://github.com/ultralytics/yolo-flutter-app repository: https://github.com/ultralytics/yolo-flutter-app issue_tracker: https://github.com/ultralytics/yolo-flutter-app/issues -example: example/example.dart topics: - computer-vision diff --git a/test/utils/test_helpers.dart b/test/utils/test_helpers.dart index 7a82fe66..21b6bfa4 100644 --- a/test/utils/test_helpers.dart +++ b/test/utils/test_helpers.dart @@ -302,8 +302,8 @@ class YOLOTestHelpers { 'task': task.name, 'modelPath': 'assets/models/yolo11n.tflite', 'useGpu': true, - 'confidenceThreshold': 0.5, - 'iouThreshold': 0.45, + 'confidenceThreshold': 0.25, + 'iouThreshold': 0.7, 'numItemsThreshold': 30, }; } diff --git a/test/yolo_controller_test.dart b/test/yolo_controller_test.dart index ad574e81..772b4cf8 100644 --- a/test/yolo_controller_test.dart +++ b/test/yolo_controller_test.dart @@ -29,8 +29,8 @@ void main() { }); test('default values and threshold clamping', () { - expect(controller.confidenceThreshold, 0.5); - expect(controller.iouThreshold, 0.45); + expect(controller.confidenceThreshold, 0.25); + expect(controller.iouThreshold, 0.7); expect(controller.numItemsThreshold, 30); // Test clamping diff --git a/test/yolo_test.dart b/test/yolo_test.dart index a3b00876..3c579ea8 100644 --- a/test/yolo_test.dart +++ b/test/yolo_test.dart @@ -94,6 +94,13 @@ void main() { ); }); + test('default official model returns the first supported ID', () { + expect( + YOLO.defaultOfficialModel(task: YOLOTask.detect), + YOLO.officialModels(task: YOLOTask.detect).first, + ); + }); + test('task can be inferred from model metadata', () async { final yolo = YOLO(modelPath: 'test_model.tflite'); await yolo.loadModel(); diff --git a/test/yolo_view_test.dart b/test/yolo_view_test.dart index 2e578f6f..8799eb6b 100644 --- a/test/yolo_view_test.dart +++ b/test/yolo_view_test.dart @@ -33,8 +33,8 @@ void main() { }); test('default values and threshold clamping', () { - expect(controller.confidenceThreshold, 0.5); - expect(controller.iouThreshold, 0.45); + expect(controller.confidenceThreshold, 0.25); + expect(controller.iouThreshold, 0.7); expect(controller.numItemsThreshold, 30); // Test clamping