Skip to content

Commit cd0bb68

Browse files
Align official YOLO defaults and keep API stable (#471)
Co-authored-by: UltralyticsAssistant <web@ultralytics.com>
1 parent 58e8514 commit cd0bb68

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+651
-1280
lines changed

.github/workflows/publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ on:
1515

1616
jobs:
1717
check:
18-
if: github.repository == 'ultralytics/yolo-flutter-app' && (github.actor == 'glenn-jocher' || github.actor == 'asabri97' || github.actor == 'john-rocky')
18+
if: github.repository == 'ultralytics/yolo-flutter-app' && github.actor == 'glenn-jocher'
1919
runs-on: ubuntu-latest
2020
permissions:
2121
contents: write

.github/workflows/tag.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ on:
2929

3030
jobs:
3131
tag-and-release:
32-
if: github.repository == 'ultralytics/yolo-flutter-app' && (github.actor == 'glenn-jocher' || github.actor == 'asabri97' || github.actor == 'john-rocky')
32+
if: github.repository == 'ultralytics/yolo-flutter-app' && github.actor == 'glenn-jocher'
3333
name: Tag and Release
3434
runs-on: ubuntu-latest
3535
steps:

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1-
## Unreleased
1+
## 0.3.0
22

33
- **Feature**: Add package-level model resolution for official IDs, remote URLs, local files, and Flutter assets.
44
- **Enhancement**: Resolve `task` from exported metadata when available across `YOLO`, `YOLOView`, and `YOLOViewController.switchModel()`.
55
- **Enhancement**: Replace hardcoded release URL assumptions with an official model catalog and latest-release resolution.
6+
- **Enhancement**: Add `YOLO.defaultOfficialModel()` to make the default official model path explicit for new users.
7+
- **Bug Fix**: Enforce official Ultralytics defaults everywhere with `confidenceThreshold = 0.25` and `iouThreshold = 0.7`.
8+
- **Compatibility**: Preserve deprecated API shims and widget wrapper exports to avoid regressions for existing integrations.
69
- **Cleanup**: Remove example-only model management duplication and update docs around the metadata-first flow.
710

811
## 0.2.0

README.md

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,13 @@ Start with the default official model:
5454
```dart
5555
import 'package:ultralytics_yolo/ultralytics_yolo.dart';
5656
57+
final modelId = YOLO.defaultOfficialModel() ?? 'yolo26n';
58+
5759
YOLOView(
58-
modelPath: 'yolo26n',
60+
modelPath: modelId,
5961
onResult: (results) {
60-
for (final result in results) {
61-
print('${result.className}: ${result.confidence}');
62+
for (final r in results) {
63+
debugPrint('${r.className}: ${r.confidence}');
6264
}
6365
},
6466
)
@@ -80,29 +82,38 @@ The plugin supports three model flows.
8082

8183
### 1. Official model IDs
8284

83-
Use an official ID such as `yolo26n` and let the plugin handle download and caching:
85+
Use the default official model or a specific official ID and let the plugin
86+
handle download and caching:
8487

8588
```dart
86-
final yolo = YOLO(modelPath: 'yolo26n');
89+
final yolo = YOLO(modelPath: YOLO.defaultOfficialModel() ?? 'yolo26n');
8790
```
8891

89-
Call `YOLO.officialModels()` to see which official IDs are available on the current platform.
92+
Call `YOLO.officialModels()` to see which official IDs are available on the
93+
current platform.
9094

9195
### 2. Your own exported model
9296

93-
Pass a local path or Flutter asset path:
97+
Pass your own exported YOLO model as a local path or Flutter asset path:
9498

9599
```dart
96-
final yolo = YOLO(modelPath: 'assets/models/custom.tflite');
100+
final yolo = YOLO(modelPath: 'assets/models/my-finetuned-model.tflite');
97101
```
98102

99103
If the exported model includes metadata, the plugin infers `task` automatically. If metadata is missing, pass `task` explicitly.
100104

105+
```dart
106+
final yolo = YOLO(
107+
modelPath: 'assets/models/my-finetuned-model.tflite',
108+
task: YOLOTask.detect,
109+
);
110+
```
111+
101112
### 3. Remote model URL
102113

103114
Pass an `http` or `https` URL and the plugin will download it into app storage before loading it.
104115

105-
## 🧭 Official Vs Custom
116+
## 🧭 Official vs. Custom
106117

107118
| Use case | Recommended path |
108119
| ----------------------------------------------------- | --------------------------------- |
@@ -112,7 +123,9 @@ Pass an `http` or `https` URL and the plugin will download it into app storage b
112123
| You need the plugin to infer `task` automatically | Any export with metadata |
113124
| You have an older or stripped export without metadata | Custom model plus explicit `task` |
114125

115-
For official models, start with `YOLO.officialModels()`. For custom models, start with the exported file you actually plan to ship.
126+
For official models, start with `YOLO.defaultOfficialModel()` or
127+
`YOLO.officialModels()`. For custom models, start with the exported file you
128+
actually plan to ship.
116129

117130
## 📥 Drop Your Own Model Into an App
118131

@@ -127,7 +140,7 @@ Then point `modelPath` at that file or asset path.
127140

128141
### iOS export note
129142

130-
Detection models exported to CoreML must use `nms=True`:
143+
Detection models exported to Core ML must use `nms=True`:
131144

132145
```python
133146
from ultralytics import YOLO
@@ -163,13 +176,13 @@ await controller.switchModel('assets/models/custom.tflite', YOLOTask.detect);
163176

164177
## 🧩 Recommended Patterns
165178

166-
| App type | Model loading pattern |
167-
| ---------------------------------- | ---------------------------------------------------------------------- |
168-
| Live camera app | `YOLOView(modelPath: 'yolo26n')` |
169-
| Photo picker or gallery workflow | `YOLO(modelPath: 'yolo26n')` |
170-
| App with your own bundled model | `YOLO(modelPath: 'assets/models/custom.tflite')` |
171-
| Cross-platform CoreML + TFLite app | Use platform-appropriate exported assets and let metadata drive `task` |
172-
| App that changes models at runtime | `YOLOViewController.switchModel(...)` |
179+
| App type | Model loading pattern |
180+
| ----------------------------------- | ---------------------------------------------------------------------- |
181+
| Live camera app | `YOLOView(modelPath: 'yolo26n')` |
182+
| Photo picker or gallery workflow | `YOLO(modelPath: 'yolo26n')` |
183+
| App with your own bundled model | `YOLO(modelPath: 'assets/models/custom.tflite')` |
184+
| Cross-platform Core ML + TFLite app | Use platform-appropriate exported assets and let metadata drive `task` |
185+
| App that changes models at runtime | `YOLOViewController.switchModel(...)` |
173186

174187
## 📚 Documentation
175188

README.zh-CN.md

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,13 @@ flutter pub get
5454
```dart
5555
import 'package:ultralytics_yolo/ultralytics_yolo.dart';
5656
57+
final modelId = YOLO.defaultOfficialModel() ?? 'yolo26n';
58+
5759
YOLOView(
58-
modelPath: 'yolo26n',
60+
modelPath: modelId,
5961
onResult: (results) {
60-
for (final result in results) {
61-
print('${result.className}: ${result.confidence}');
62+
for (final r in results) {
63+
debugPrint('${r.className}: ${r.confidence}');
6264
}
6365
},
6466
)
@@ -80,24 +82,32 @@ final results = await yolo.predict(imageBytes);
8082

8183
### 1. 官方模型 ID
8284

83-
直接使用官方 ID,例如 `yolo26n`
85+
可直接使用默认官方模型,或传入指定官方 ID,例如 `yolo26n`
8486

8587
```dart
86-
final yolo = YOLO(modelPath: 'yolo26n');
88+
final yolo = YOLO(modelPath: YOLO.defaultOfficialModel() ?? 'yolo26n');
8789
```
8890

89-
插件会自动下载并缓存当前平台对应的官方产物。可通过 `YOLO.officialModels()` 查看当前平台可用的官方 ID。
91+
插件会自动下载并缓存当前平台对应的官方产物。可通过
92+
`YOLO.officialModels()` 查看当前平台可用的全部官方 ID。
9093

9194
### 2. 你自己的导出模型
9295

93-
传入本地路径或 Flutter 资源路径:
96+
传入你自己的导出 YOLO 模型本地路径或 Flutter 资源路径:
9497

9598
```dart
96-
final yolo = YOLO(modelPath: 'assets/models/custom.tflite');
99+
final yolo = YOLO(modelPath: 'assets/models/my-finetuned-model.tflite');
97100
```
98101

99102
如果模型导出时带有元数据,插件会自动推断 `task`。如果没有,就显式传入 `task`
100103

104+
```dart
105+
final yolo = YOLO(
106+
modelPath: 'assets/models/my-finetuned-model.tflite',
107+
task: YOLOTask.detect,
108+
);
109+
```
110+
101111
### 3. 远程模型 URL
102112

103113
传入 `http``https` URL,插件会先下载到应用存储,再完成加载。
@@ -112,7 +122,8 @@ final yolo = YOLO(modelPath: 'assets/models/custom.tflite');
112122
| 希望插件自动推断 `task` | 使用带元数据的导出模型 |
113123
| 你的导出模型没有元数据 | 自定义模型并显式传入 `task` |
114124

115-
官方模型先看 `YOLO.officialModels()`;自定义模型则直接从你准备实际交付的导出文件开始。
125+
官方模型可直接从 `YOLO.defaultOfficialModel()`
126+
`YOLO.officialModels()` 开始;自定义模型则直接从你准备实际交付的导出文件开始。
116127

117128
## 📥 把你自己的模型放进应用
118129

@@ -127,7 +138,7 @@ final yolo = YOLO(modelPath: 'assets/models/custom.tflite');
127138

128139
### iOS 导出注意事项
129140

130-
导出 CoreML 检测模型时必须使用 `nms=True`
141+
导出 Core ML 检测模型时必须使用 `nms=True`
131142

132143
```python
133144
from ultralytics import YOLO
@@ -163,13 +174,13 @@ await controller.switchModel('assets/models/custom.tflite', YOLOTask.detect);
163174

164175
## 🧩 推荐接入模式
165176

166-
| 应用类型 | 推荐模型加载方式 |
167-
| -------------------------------------- | ------------------------------------------------ |
168-
| 实时相机场景 | `YOLOView(modelPath: 'yolo26n')` |
169-
| 图库或单图推理流程 | `YOLO(modelPath: 'yolo26n')` |
170-
| 应用内置自定义模型 | `YOLO(modelPath: 'assets/models/custom.tflite')` |
171-
| 同时支持 CoreML 与 TFLite 的跨平台应用 | 使用各平台对应导出文件,并让元数据决定 `task` |
172-
| 运行时动态切换模型 | `YOLOViewController.switchModel(...)` |
177+
| 应用类型 | 推荐模型加载方式 |
178+
| --------------------------------------- | ------------------------------------------------ |
179+
| 实时相机场景 | `YOLOView(modelPath: 'yolo26n')` |
180+
| 图库或单图推理流程 | `YOLO(modelPath: 'yolo26n')` |
181+
| 应用内置自定义模型 | `YOLO(modelPath: 'assets/models/custom.tflite')` |
182+
| 同时支持 Core ML 与 TFLite 的跨平台应用 | 使用各平台对应导出文件,并让元数据决定 `task` |
183+
| 运行时动态切换模型 | `YOLOViewController.switchModel(...)` |
173184

174185
## 📚 文档
175186

android/src/main/kotlin/com/ultralytics/yolo/Classifier.kt

Lines changed: 4 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ class Classifier(
4141
if (useGpu) {
4242
try {
4343
addDelegate(GpuDelegate())
44-
Log.d(TAG, "GPU delegate is used.")
4544
} catch (e: Exception) {
4645
Log.e(TAG, "GPU delegate error: ${e.message}")
4746
}
@@ -61,11 +60,11 @@ class Classifier(
6160
val modelBuffer = YOLOUtils.loadModelFile(context, modelPath)
6261

6362
// ===== Load label information (try Appended ZIP → FlatBuffers in order) =====
64-
var loadedLabels = YOLOFileUtils.loadLabelsFromAppendedZip(context, modelPath)
63+
val loadedLabels = YOLOFileUtils.loadLabelsFromAppendedZip(context, modelPath)
6564
var labelsWereLoaded = loadedLabels != null
6665

67-
if (labelsWereLoaded) {
68-
this.labels = loadedLabels!! // Use labels from appended ZIP
66+
if (loadedLabels != null) {
67+
this.labels = loadedLabels // Use labels from appended ZIP
6968
Log.i(TAG, "Labels successfully loaded from appended ZIP.")
7069
} else {
7170
Log.w(TAG, "Could not load labels from appended ZIP, trying FlatBuffers metadata...")
@@ -112,25 +111,20 @@ class Classifier(
112111
require(inChannels == expectedChannels || (expectedChannels == 1 && inChannels == 1) || (expectedChannels == 3 && inChannels == 3)) {
113112
"Unexpected input channels. Expected $expectedChannels channels, but got $inChannels channels. Input shape: ${inputShape.joinToString()}"
114113
}
115-
116-
Log.d(TAG, "Model configuration: ${inChannels}-channel input, grayscale mode: $isGrayscaleModel")
117114

118115
inputSize = Size(inWidth, inHeight)
119116
modelInputSize = Pair(inWidth, inHeight)
120-
Log.d(TAG, "Model input size = $inWidth x $inHeight")
121117

122118
val outputShape = interpreter.getOutputTensor(0).shape()
123119
// e.g. outputShape = [1, 1000] for ImageNet, [1, 12] for EMNIST
124120
numClass = outputShape[1]
125-
121+
126122
// Validate expected classes if specified
127123
(classifierOptions?.get("expectedClasses") as? Int)?.let { expectedClasses ->
128124
if (numClass != expectedClasses) {
129125
Log.w(TAG, "Warning: Expected $expectedClasses output classes, but model has $numClass classes")
130126
}
131127
}
132-
133-
Log.d(TAG, "Model output shape = [1, $numClass] (${if (isGrayscaleModel) "grayscale" else "RGB"} model)")
134128

135129
// Setup ImageProcessors only for RGB models (3-channel)
136130
// For grayscale models (1-channel), we'll use custom processing
@@ -165,8 +159,6 @@ class Classifier(
165159
.add(CastOp(DataType.FLOAT32))
166160
.build()
167161
}
168-
169-
Log.d(TAG, "Classifier initialized.")
170162
}
171163

172164
override fun predict(bitmap: Bitmap, origWidth: Int, origHeight: Int, rotateForCamera: Boolean, isLandscape: Boolean): YOLOResult {
@@ -193,7 +185,6 @@ class Classifier(
193185
inputMean = inputMean,
194186
inputStd = inputStd
195187
)
196-
Log.d(TAG, "Using grayscale processing for 1-channel model")
197188
} else {
198189
// Use standard RGB processing for 3-channel models
199190
val tensorImage = TensorImage(DataType.FLOAT32)
@@ -217,7 +208,6 @@ class Classifier(
217208
imageProcessorSingleImage.process(tensorImage)
218209
}
219210
inputBuffer = processedImage.buffer
220-
Log.d(TAG, "Using RGB processing for 3-channel model")
221211
}
222212

223213
val outputArray = Array(1) { FloatArray(numClass) }
@@ -253,10 +243,6 @@ class Classifier(
253243

254244
val fpsVal = if (t4 > 0) 1.0 / t4 else 0.0
255245

256-
Log.d(TAG, "Classification result: top1Label=${probs.top1Label}, top1Conf=${probs.top1Conf}, top1Index=${probs.top1Index}")
257-
Log.d(TAG, "Labels: ${labels}")
258-
Log.d(TAG, "Prediction completed successfully")
259-
260246
return YOLOResult(
261247
origShape = Size(origWidth, origHeight),
262248
probs = probs,
@@ -281,10 +267,8 @@ class Classifier(
281267
val files = extractor.associatedFileNames
282268
if (!files.isNullOrEmpty()) {
283269
for (fileName in files) {
284-
Log.d(TAG, "Found associated file: $fileName")
285270
extractor.getAssociatedFile(fileName)?.use { stream ->
286271
val fileString = String(stream.readBytes(), Charsets.UTF_8)
287-
Log.d(TAG, "Associated file contents:\n$fileString")
288272

289273
val yaml = Yaml()
290274
@Suppress("UNCHECKED_CAST")
@@ -293,14 +277,11 @@ class Classifier(
293277
val namesMap = data["names"] as? Map<Int, String>
294278
if (namesMap != null) {
295279
labels = namesMap.values.toList()
296-
Log.d(TAG, "Loaded labels from metadata: $labels")
297280
return true
298281
}
299282
}
300283
}
301284
}
302-
} else {
303-
Log.d(TAG, "No associated files found in the metadata.")
304285
}
305286
false
306287
} catch (e: Exception) {

android/src/main/kotlin/com/ultralytics/yolo/ObbDetector.kt

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ class ObbDetector(
4545
if (useGpu) {
4646
try {
4747
addDelegate(GpuDelegate())
48-
Log.d("ObbDetector", "GPU delegate is used.")
4948
} catch (e: Exception) {
5049
Log.e("ObbDetector", "GPU delegate error: ${e.message}")
5150
}
@@ -76,11 +75,11 @@ class ObbDetector(
7675
val modelBuffer = YOLOUtils.loadModelFile(context, modelPath)
7776

7877
// ===== Load label information (try Appended ZIP → FlatBuffers in order) =====
79-
var loadedLabels = YOLOFileUtils.loadLabelsFromAppendedZip(context, modelPath)
78+
val loadedLabels = YOLOFileUtils.loadLabelsFromAppendedZip(context, modelPath)
8079
var labelsWereLoaded = loadedLabels != null
8180

82-
if (labelsWereLoaded) {
83-
this.labels = loadedLabels!! // Use labels from appended ZIP
81+
if (loadedLabels != null) {
82+
this.labels = loadedLabels // Use labels from appended ZIP
8483
Log.i("ObbDetector", "Labels successfully loaded from appended ZIP.")
8584
} else {
8685
Log.w("ObbDetector", "Could not load labels from appended ZIP, trying FlatBuffers metadata...")
@@ -101,7 +100,6 @@ class ObbDetector(
101100
interpreter = Interpreter(modelBuffer, interpreterOptions)
102101
// Call allocateTensors() once during initialization, not in the inference loop
103102
interpreter.allocateTensors()
104-
Log.d("ObbDetector", "TFLite model loaded and tensors allocated")
105103

106104
val inputShape = interpreter.getInputTensor(0).shape()
107105
val inHeight = inputShape[1]
@@ -464,10 +462,8 @@ class ObbDetector(
464462
val files = extractor.associatedFileNames
465463
if (!files.isNullOrEmpty()) {
466464
for (fileName in files) {
467-
Log.d("ObbDetector", "Found associated file: $fileName")
468465
extractor.getAssociatedFile(fileName)?.use { stream ->
469466
val fileString = String(stream.readBytes(), Charsets.UTF_8)
470-
Log.d("ObbDetector", "Associated file contents:\n$fileString")
471467

472468
val yaml = Yaml()
473469
@Suppress("UNCHECKED_CAST")
@@ -476,14 +472,11 @@ class ObbDetector(
476472
val namesMap = data["names"] as? Map<Int, String>
477473
if (namesMap != null) {
478474
labels = namesMap.values.toList()
479-
Log.d("ObbDetector", "Loaded labels from metadata: $labels")
480475
return true
481476
}
482477
}
483478
}
484479
}
485-
} else {
486-
Log.d("ObbDetector", "No associated files found in the metadata.")
487480
}
488481
false
489482
} catch (e: Exception) {

0 commit comments

Comments
 (0)