From 6d7ba3b6ab38bf45460b582505d6cd775d819f66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E6=96=8C?= <259278618@qq.com> Date: Mon, 31 Aug 2026 15:57:58 +0800 Subject: [PATCH] =?UTF-8?q?refactor(app):=20=E6=A8=A1=E5=9E=8B=E4=B8=8D?= =?UTF-8?q?=E5=86=8D=E6=89=93=E5=8C=85=E8=BF=9B=20APK=EF=BC=8C=E7=A7=BB?= =?UTF-8?q?=E9=99=A4=E5=86=85=E7=BD=AE=E8=B5=84=E4=BA=A7=E5=9B=9E=E9=80=80?= =?UTF-8?q?=E9=80=BB=E8=BE=91=EF=BC=88=E7=89=88=E6=9C=AC=201.0.5+6?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除 pubspec 的 model.tflite/labels.txt 资产与 DetectorWorker 内置回退 - 无下载模型时仅预览;modelsLabel 改为未下载 - 未打包 release APK(77.4MB,不含模型) --- flutter_app/README.md | 10 +++--- flutter_app/lib/app.dart | 2 +- flutter_app/lib/camera/camera_screen.dart | 2 +- flutter_app/lib/camera/detection_overlay.dart | 4 +-- .../lib/detection/detection_result.dart | 2 +- .../lib/detection/detector_worker.dart | 33 ++++--------------- .../lib/detection/tflite_detector.dart | 4 +-- flutter_app/lib/models/model_manager.dart | 8 ++--- flutter_app/pubspec.yaml | 4 +-- 9 files changed, 22 insertions(+), 47 deletions(-) diff --git a/flutter_app/README.md b/flutter_app/README.md index 6f62193..1c52380 100644 --- a/flutter_app/README.md +++ b/flutter_app/README.md @@ -33,8 +33,8 @@ xcrun devicectl device process launch --console --terminate-existing \ `models` 目录(公开接口,无需登录),与 `UpdateChecker` 的 APK 检查并行。 - **目录条目**:`{datasetId, datasetName, version, labels[], sizeBytes, sha256, - downloadUrl}`;服务器未发布模型时不返回 `models` 字段,App 回退内置 - `assets/model.tflite`(标签 `内置`)。 + downloadUrl}`;服务器未发布模型时不返回 `models` 字段,App 无模型可用, + 相机页仅预览不识别。 - **存储**:应用私有目录 `models//`,含 `model.tflite`、 `labels.json`、`meta.json`(meta 记录 `{version, sha256}`)。版本与摘要都 未变化时跳过下载;变化则下载到 `.part` 临时文件、sha256 校验通过后 @@ -43,15 +43,15 @@ xcrun devicectl device process launch --console --terminate-existing \ - **并行推理合并**:识别时加载全部已下载模型(`DetectorWorker` isolate 内 逐模型加载,单个失败不影响其他),同帧各模型独立推理后按类别分组做 **跨模型 NMS**(同类别不同模型检出同一目标取高分去重,不同类别互不压制), - 结果叠加 `modelName` 标注来源,内置模型兜底。 + 结果叠加 `modelName` 标注来源。 实现:`lib/models/model_manager.dart`(下载/校验/持久化,`ModelManager` 单例 + ChangeNotifier)、`lib/detection/detector_worker.dart`(多模型并行 推理与 `mergeAcrossModels`)、`lib/camera/camera_screen.dart`(启动同步 + 诊断行展示模型列表)。 -- **模型输入是 NHWC**:`assets/model.tflite` 做过字节级手术(开头 TRANSPOSE→RESHAPE, - 输入 [1,320,320,3]),改动记录见 git 历史,重导模型需同步处理,否则 iOS 报 +- **模型输入是 NHWC**:训练导出的模型需做字节级手术(开头 TRANSPOSE→RESHAPE, + 输入 [1,320,320,3])再发布给 App,否则 iOS 报 "Node number 0 (TRANSPOSE) failed to prepare"。 - **模拟器黑屏**:本机 iOS 模拟器 Impeller 渲染黑屏,验证 UI 用 VM service (`flutter run` 输出里的 DevTools 地址),或直接真机验证。 diff --git a/flutter_app/lib/app.dart b/flutter_app/lib/app.dart index 0a94374..37ee93f 100644 --- a/flutter_app/lib/app.dart +++ b/flutter_app/lib/app.dart @@ -95,7 +95,7 @@ class _StartupGateState extends State { final token = await session.readToken(); if (!mounted) return; // 模型热更新:后台拉取模型目录并下载/更新各数据集模型(不阻塞启动, - // 相机页打开前若未就绪会兜底等待;下载失败回退内置资产模型) + // 相机页打开前若未就绪会兜底等待;未下载模型时仅预览) ModelManager.instance.refresh(); Navigator.of(context) .pushReplacementNamed(token == null ? '/login' : '/home'); diff --git a/flutter_app/lib/camera/camera_screen.dart b/flutter_app/lib/camera/camera_screen.dart index 67761e9..0e4bd89 100644 --- a/flutter_app/lib/camera/camera_screen.dart +++ b/flutter_app/lib/camera/camera_screen.dart @@ -121,7 +121,7 @@ class _CameraScreenState extends State { if (!granted) return; // 模型热更新:优先使用已下载的数据集模型(启动时后台拉取;此处兜底等待, - // 下载慢/失败不阻塞相机启动——无下载模型时 worker 回退内置资产) + // 下载慢/失败不阻塞相机启动——无下载模型时仅预览) if (!ModelManager.instance.ready) { try { await ModelManager.instance diff --git a/flutter_app/lib/camera/detection_overlay.dart b/flutter_app/lib/camera/detection_overlay.dart index c873657..a9afeaf 100644 --- a/flutter_app/lib/camera/detection_overlay.dart +++ b/flutter_app/lib/camera/detection_overlay.dart @@ -87,9 +87,9 @@ class _OverlayPainter extends CustomPainter { _drawDashedRect(canvas, box, paint); } - // 标签:框上方,含距离;多模型时标注来源模型名(内置资产不标) + // 标签:框上方,含距离;多模型时标注来源模型名 final dist = _distanceLabel(r); - final modelTag = r.modelName.isNotEmpty && r.modelName != '内置' + final modelTag = r.modelName.isNotEmpty ? '[${r.modelName}]' : ''; final text = diff --git a/flutter_app/lib/detection/detection_result.dart b/flutter_app/lib/detection/detection_result.dart index bdf649d..ebca1c4 100644 --- a/flutter_app/lib/detection/detection_result.dart +++ b/flutter_app/lib/detection/detection_result.dart @@ -9,7 +9,7 @@ class DetectionResult { /// 轨迹已确认(多帧稳定/高分/活动确认),false = 候选,渲染为虚线 final bool confirmed; - /// 产出该框的模型(数据集 id 与名称;内置资产模型为 -1/空) + /// 产出该框的模型(数据集 id 与名称;无来源为 -1/空) final int modelId; final String modelName; diff --git a/flutter_app/lib/detection/detector_worker.dart b/flutter_app/lib/detection/detector_worker.dart index 05c0463..7c00b6f 100644 --- a/flutter_app/lib/detection/detector_worker.dart +++ b/flutter_app/lib/detection/detector_worker.dart @@ -4,7 +4,6 @@ import 'dart:typed_data'; import 'package:camera/camera.dart'; import 'package:flutter/foundation.dart' show debugPrint; -import 'package:flutter/services.dart' show rootBundle; import '../camera/motion_detector.dart'; import '../models/model_manager.dart'; @@ -19,15 +18,8 @@ import 'visual_prior.dart'; /// /// 多模型并行推理:传入 [models](各数据集下载模型)后,每帧逐模型推理, /// 结果按类别分组跨模型 NMS 合并(同标签重复框取高分,不同标签互不压制); -/// 无下载模型时回退内置资产模型。 +/// 无下载模型时不启动推理(仅预览)。 class DetectorWorker { - static const String modelAsset = 'assets/model.tflite'; - static const String labelsAsset = 'assets/labels.txt'; - - /// 内置资产回退模型的标识 - static const int builtinModelId = -1; - static const String builtinModelName = '内置'; - final Isolate _isolate; final ReceivePort _responses; @@ -64,26 +56,13 @@ class DetectorWorker { }); } - /// 加载模型并启动后台推理 isolate;加载失败返回 null(App 降级为仅预览)。 - /// [models] 为空时回退内置资产模型(模型缺失同样返回 null)。 + /// 加载模型并启动后台推理 isolate;无模型或加载失败返回 null(App 降级为仅预览)。 static Future create({List? models}) async { try { - final payload = >[]; - if (models != null && models.isNotEmpty) { - for (final m in models) { - payload.add( - [m.bytes, m.labels, m.datasetId, m.datasetName]); - } - } else { - final data = await rootBundle.load(modelAsset); - final modelBytes = - data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes); - final labels = (await rootBundle.loadString(labelsAsset)) - .split('\n') - .where((l) => l.trim().isNotEmpty) - .toList(); - payload.add([modelBytes, labels, builtinModelId, builtinModelName]); - } + if (models == null || models.isEmpty) return null; + final payload = >[ + for (final m in models) [m.bytes, m.labels, m.datasetId, m.datasetName], + ]; final responses = ReceivePort(); final isolate = await Isolate.spawn(_workerMain, responses.sendPort); diff --git a/flutter_app/lib/detection/tflite_detector.dart b/flutter_app/lib/detection/tflite_detector.dart index 06fbe1f..3b2767a 100644 --- a/flutter_app/lib/detection/tflite_detector.dart +++ b/flutter_app/lib/detection/tflite_detector.dart @@ -19,8 +19,6 @@ class TfliteDetector { double minScore = 0.10; static const double iouThreshold = 0.45; static const int maxDetections = 20; - static const String modelAsset = 'assets/model.tflite'; - static const String labelsAsset = 'assets/labels.txt'; final Interpreter _interpreter; final List _labels; @@ -28,7 +26,7 @@ class TfliteDetector { final int _numAnchors; final int inputSize; - /// 模型身份(多模型并行推理区分来源;内置资产模型为 -1/空) + /// 模型身份(多模型并行推理区分来源;无来源为 -1/空) final int modelId; final String modelName; diff --git a/flutter_app/lib/models/model_manager.dart b/flutter_app/lib/models/model_manager.dart index 4005d35..cc61a6c 100644 --- a/flutter_app/lib/models/model_manager.dart +++ b/flutter_app/lib/models/model_manager.dart @@ -43,7 +43,7 @@ class ModelCatalogItem { ); } -/// 已就绪模型(字节 + 标签,供推理 worker 加载;含内置资产回退模型) +/// 已就绪模型(字节 + 标签,供推理 worker 加载) class ModelBundle { final int datasetId; final String datasetName; @@ -91,7 +91,7 @@ class ModelManager extends ChangeNotifier { _client = client ?? http.Client(), _rootDirOverride = rootDir; - /// 已就绪模型列表(空 = 无服务器模型,回退内置资产) + /// 已就绪模型列表(空 = 未下载任何模型,相机页仅预览) List get models => _models; /// 是否成功拉取过目录(即使下载失败也为 true,用于区分"从未联网"与"目录为空") @@ -102,9 +102,9 @@ class ModelManager extends ChangeNotifier { bool get refreshing => _refreshing; - /// 模型名摘要(诊断行展示):内置 / 数据集名×n + /// 模型名摘要(诊断行展示):未下载 / 数据集名×n String get modelsLabel { - if (_models.isEmpty) return '内置'; + if (_models.isEmpty) return '未下载'; return _models.map((m) => m.datasetName).join(','); } diff --git a/flutter_app/pubspec.yaml b/flutter_app/pubspec.yaml index 997a360..7ebfd61 100644 --- a/flutter_app/pubspec.yaml +++ b/flutter_app/pubspec.yaml @@ -2,7 +2,7 @@ name: observer description: "视野 - 动物实时识别 (环颈雉鸡/生境), YOLOv8 + 充值付费" publish_to: 'none' -version: 1.0.4+5 +version: 1.0.5+6 environment: sdk: ^3.12.2 @@ -52,6 +52,4 @@ dev_dependencies: flutter: uses-material-design: true assets: - - assets/model.tflite - - assets/labels.txt - assets/beep.wav