双档位训练体系(s高识别/n高性能 串行排队、按档发布)与 TFLite 硬件加速补丁

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-09-03 09:20:46 +08:00
co-authored by Claude Opus 4.7
parent bb75c69d91
commit 6f8c54c31d
23 changed files with 651 additions and 269 deletions
+42 -14
View File
@@ -38,30 +38,58 @@ xcrun devicectl device process launch --console --terminate-existing \
"In iOS 14+, debug mode Flutter apps can only be launched from Flutter tooling"。
debug 调试必须用 `flutter run -d <设备ID>` 或 Xcode IDE 启动(`flutter devices` 查设备ID);
从图标启动只对 release 构建有效。
- **`flutter run` 真机 debug 附接失败(errno 49,多次复现)**:Xcode 构建、安装、
启动都成功,attach 阶段报 `OS Error: Can't assign requested address, errno = 49`
工具即退出——与本机 VPN(utun 隧道)环境相关,断 VPN 后可恢复。
需要真机验证时一律用上方 release + devicectl 流程(不依赖 attach);只有需要
热重载/看 debugPrint 才用 `flutter run`,遇 errno 49 先断 VPN 重试。
## 端侧推理加速(GPU / CoreML
识别慢的根因是 yolov8s@1280 推理量大(CPU 4 线程约每秒不到 1 帧),
`TfliteDetector.fromBuffer` 加载模型时按平台挂加速 delegate,均为**浮点计算不降精度**
(区别于 int8 量化掉点):
| 平台 | delegate | 说明 |
|---|---|---|
| Android | `GpuDelegateV2` | TFLite GPU delegate;依赖 `libtensorflowlite_gpu_jni.so`,已 vendor 到 `android/app/src/main/jniLibs/arm64-v8a/`AAR 因 AGP 9 namespace 冲突保持排除,升级 tflite_flutter 时需同步换 .so,版本对齐 base 2.11.0 |
| iOS | `CoreMlDelegate` | Core ML(苹果 ANE/GPU,插件 pod 自带 TensorFlowLiteSwift/CoreML,无需额外依赖) |
delegate 初始化失败(老设备/驱动/符号缺失)**自动回退纯 CPU 4 线程**,最后才返回 null
(仅预览不识别)。生效与否看日志:加载模型时输出
`[TfliteDetector] 加速生效 model=xxx (CoreML|GPU)`,回退输出 `回退 CPU` 及原因。
GPU delegate 默认允许 FP16 计算(YOLO 类精度损失可忽略);如需全精度改为传
`GpuDelegateOptionsV2(isPrecisionLossAllowed: false)`
## 模型热更新(多数据集模型)
模型与 APK 更新走**独立通道**:启动时拉取 `GET /api/v1/app/update` 随附的
`models` 目录(公开接口,无需登录),与 `UpdateChecker` 的 APK 检查并行。
- **目录条目**`{datasetId, datasetName, version, labels[], sizeBytes, sha256,
downloadUrl, coverUrl}`;服务器未发布模型时不返回 `models` 字段,App 无模型可用,
- **目录条目**`{datasetId, datasetName, variant, version, labels[], sizeBytes, sha256,
downloadUrl, coverUrl}`——**双档位(2026-09-03)**:每数据集至多 2 条 = 高识别 s
(@1280 精度优先,默认)+ 高性能 n(@704 速度优先)各自的当前版本,条目带 `variant`
(s/n);服务器未发布模型时不返回 `models` 字段,App 无模型可用,
相机页仅预览不识别。
- **下载入口**:相机页设置弹层「模型清单」按需下载/使用(封面缩略图 2 列网格,
未下载点击「使用」显示进度,完成自动激活;已激活再次点击取消;下载中可取消)。
- **存储**:应用私有目录 `models/<datasetId>/`,含 `model.tflite`、
`labels.json`、`meta.json`meta 记录 `{version, sha256}`)。版本与摘要都
未变化时跳过下载;变化则下载到 `.part` 临时文件、sha256 校验通过后
子 rename 替换,失败重试一次并保留旧模型,下次启动再试。
- **清理**:服务器下线的数据集下次同步时删除本地对应目录。
- **并行推理合并**:识别时加载全部已激活模型(`DetectorWorker` isolate 内
逐模型加载,单个失败不影响其他),同帧各模型独立推理后按类别分组做
每数据集两档各占一格并带档位角标;未下载点击「使用」显示进度,完成自动激活;
已激活再次点击取消;下载中可取消)。**档位切换**:弹层顶部「识别模式」分段控件
(s 高识别 / n 高性能,默认 s)持久化本地,切换即热加载新档位已激活模型。
- **存储**:应用私有目录 `models/<datasetId>/<variant>/`(双档位 2026-09-03
无 variant 目录与存量 s 档一致——s 档复用 `models/<datasetId>/` 同级读取,
目录键 = 档位标识符),含 `model.tflite`、`labels.json`、`meta.json`
meta 记录 `{version, sha256}`)。版本与摘要都未变化时跳过下载;变化则下载到
`.part` 临时文件、sha256 校验通过后原子 rename 替换,失败重试一次并保留旧模型,
下次启动再试——检查记账按 `(datasetId, variant)` 独立。
- **清理**:服务器下线的数据集下次同步时删除本地对应目录(两档都无条目时才删)。
- **并行推理合并**:识别时加载**当前识别档位**下全部已激活模型(`DetectorWorker`
isolate 内逐模型加载,单个失败不影响其他),同帧各模型独立推理后按类别分组做
**跨模型 NMS**(同类别不同模型检出同一目标取高分去重,不同类别互不压制),
结果叠加 `modelName` 标注来源。
结果叠加 `modelName`(数据集名+档位)标注来源。
实现:`lib/models/model_manager.dart`(下载/校验/持久化,`ModelManager`
单例 + ChangeNotifier)、`lib/detection/detector_worker.dart`(多模型并行
推理与 `mergeAcrossModels`)、`lib/camera/camera_screen.dart`(启动同步 +
诊断行展示模型列表)。
单例 + ChangeNotifier,条目身份含档位)、`lib/detection/detector_worker.dart`
(多模型并行推理与 `mergeAcrossModels`)、`lib/camera/camera_screen.dart`
(启动同步 + 设置弹层「识别模式」切换 + 诊断行展示模型列表)。
- **模型输入是 NHWC**:训练导出的模型需做字节级手术(开头 TRANSPOSE→RESHAPE
输入 [1,320,320,3])再发布给 App,否则 iOS 报
+4 -2
View File
@@ -45,8 +45,10 @@ kotlin {
// 不依赖任何相机三方库(含 CameraX)。
// tflite_flutter 依赖的 tensorflow-lite / tensorflow-lite-gpu / tensorflow-lite-api 三个 AAR
// 声明了相同 namespace(org.tensorflow.lite),新 AGP 视作冲突直接报错;
// 本项目仅用 CPU 推理,GPU delegate 未使用,排除 gpu 及其传递依赖的 api 即可。
// 声明了相同 namespace(org.tensorflow.lite),新 AGP 视作冲突直接报错,故仍整体排除;
// GPU delegate 所需 libtensorflowlite_gpu_jni.so 已手工抽取 vendor 到
// src/main/jniLibs/arm64-v8a/(tflite_flutter 经 FFI 直调 .so,不用 AAR 内 Java 类),
// 与 base tensorflow-lite 同为 2.11.0 版本;升级 tflite_flutter 时需同步更新 .so。
configurations.all {
exclude(group = "org.tensorflow", module = "tensorflow-lite-gpu")
exclude(group = "org.tensorflow", module = "tensorflow-lite-api")
@@ -1,3 +1,4 @@
import 'dart:io' show Platform;
import 'dart:typed_data';
import 'package:flutter/foundation.dart' show debugPrint;
@@ -44,12 +45,28 @@ class TfliteDetector {
/// 模型缺失或加载失败返回 null(App 降级为仅预览)。
/// 在后台 isolate 内调用(模型字节由主 isolate 读取后传入)。
/// 加速:Android 挂 TFLite GPU delegate、iOS 挂 CoreML delegateANE/GPU),
/// 均为浮点计算不降精度(对比 int8 量化);delegate 初始化失败自动回退纯 CPU 4 线程。
static Future<TfliteDetector?> fromBuffer(
Uint8List bytes,
List<String> labels, {
int modelId = -1,
String modelName = '',
}) async {
final delegate = _createAccelDelegate();
if (delegate != null) {
try {
final options = InterpreterOptions()..threads = 4;
options.addDelegate(delegate);
final interpreter = Interpreter.fromBuffer(bytes, options: options);
debugPrint('[TfliteDetector] 加速生效 model=$modelName '
'(${Platform.isIOS ? 'CoreML' : 'GPU'})');
return TfliteDetector._fromModel(
interpreter, labels, modelId, modelName);
} catch (e) {
debugPrint('[TfliteDetector] 加速 delegate 初始化失败,回退 CPU: $e');
}
}
try {
final interpreter = Interpreter.fromBuffer(
bytes,
@@ -62,6 +79,26 @@ class TfliteDetector {
}
}
/// 平台加速 delegateAndroid=TFLite GPUlibtensorflowlite_gpu_jni.so 已 vendor 到
/// android/app/src/main/jniLibsAAR 因 AGP namespace 冲突保持排除)、iOS=CoreML。
/// 老设备/驱动/符号缺失时创建失败返回 null,走 CPU。
static Delegate? _createAccelDelegate() {
if (Platform.isIOS) {
try {
return CoreMlDelegate();
} catch (e) {
debugPrint('[TfliteDetector] CoreML delegate 创建失败: $e');
}
} else if (Platform.isAndroid) {
try {
return GpuDelegateV2();
} catch (e) {
debugPrint('[TfliteDetector] GPU delegate 创建失败: $e');
}
}
return null;
}
/// 输出布局 [1, 4+nc, anchors] 取自模型本身,类别数不与 labels 文件长度耦合。
factory TfliteDetector._fromModel(Interpreter interpreter,
List<String> labels, int modelId, String modelName) {