Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/tag.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
49 changes: 31 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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}');
}
},
)
Expand All @@ -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 |
| ----------------------------------------------------- | --------------------------------- |
Expand All @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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

Expand Down
45 changes: 28 additions & 17 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -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}');
}
},
)
Expand All @@ -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,插件会先下载到应用存储,再完成加载。
Expand All @@ -112,7 +122,8 @@ final yolo = YOLO(modelPath: 'assets/models/custom.tflite');
| 希望插件自动推断 `task` | 使用带元数据的导出模型 |
| 你的导出模型没有元数据 | 自定义模型并显式传入 `task` |

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

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

Expand All @@ -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
Expand Down Expand Up @@ -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(...)` |

## 📚 文档

Expand Down
27 changes: 4 additions & 23 deletions android/src/main/kotlin/com/ultralytics/yolo/Classifier.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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}")
}
Expand All @@ -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...")
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
Expand All @@ -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) }
Expand Down Expand Up @@ -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,
Expand All @@ -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")
Expand All @@ -293,14 +277,11 @@ class Classifier(
val namesMap = data["names"] as? Map<Int, String>
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) {
Expand Down
13 changes: 3 additions & 10 deletions android/src/main/kotlin/com/ultralytics/yolo/ObbDetector.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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}")
}
Expand Down Expand Up @@ -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...")
Expand All @@ -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]
Expand Down Expand Up @@ -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")
Expand All @@ -476,14 +472,11 @@ class ObbDetector(
val namesMap = data["names"] as? Map<Int, String>
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) {
Expand Down
Loading
Loading