Files
observer/flutter_app
2026-09-08 22:29:00 +08:00
..
1
2026-09-03 17:50:23 +08:00
1
2026-09-03 17:50:23 +08:00
1
2026-09-08 22:29:00 +08:00
1
2026-09-03 17:50:23 +08:00
1
2026-09-01 15:18:05 +08:00
1
2026-08-31 18:35:58 +08:00
1
2026-09-08 22:29:00 +08:00
1
2026-09-08 22:29:00 +08:00
1
2026-09-03 17:50:23 +08:00

observer

动物实时识别 AppFlutter 版)。Android / iOS 一套代码,后端接口与支付见 docs/PaymentApi.md

Android 打包

./build_apk.sh            # 用当前 pubspec 版本构建,产物 build/app/outputs/flutter-apk/observer-x.y.z.apk
./build_apk.sh --bump     # 自动递增 patch+1、build+1 后构建
./build_apk.sh 1.0.7      # 用指定版本(versionName)构建,build+1

产物命名 observer-x.y.z.apk(管理端上传版本号从文件名识别);构建的 app-release.apk 中间产物由脚本清理,只保留规范命名文件。

iOS 真机部署(iPhone

构建与安装

# 生产包直接构建即可:默认 API_BASE_URL 为线上域名(lib/config/app_config.dart),无需传参
flutter build ios --release
# 仅本地联调(后端跑在 Mac 上、手机连同一 Wi-Fi)时才覆盖为 Mac 局域网 IP:
# flutter build ios --release --dart-define=API_BASE_URL=http://<Mac局域网IP>:8080

# 安装到真机(UDID 可用 `xcrun devicectl list devices` 查询)
xcrun devicectl device install app --device <UDID> build/ios/iphoneos/Runner.app

# 启动并抓控制台日志(--terminate-existing 先杀掉旧实例)
xcrun devicectl device process launch --console --terminate-existing \
  --device <UDID> com.observer.app

注意事项(踩过的坑)

  • debug 构建不能在真机上从桌面图标启动iOS 14+ 会提示 "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, variant, version, labels[], sizeBytes, sha256, downloadUrl, coverUrl}——双档位(2026-09-03:每数据集至多 2 条 = 高精度 s (@1280 精度优先,默认)+ 高性能 n(@704 速度优先)各自的当前版本,条目带 variant (s/n);服务器未发布模型时不返回 models 字段,App 无模型可用, 相机页仅预览不识别。
  • 下载入口:相机页设置弹层「模型清单」按需下载/使用(封面缩略图 2 列网格, 同一物种合并一张卡,卡上双档行标注各自版本与状态(使用中绿色高亮);点 「下载」一次性补下该动物高精度+高性能两个已训练文件——各档独立进度逐条展示、 可取消(已下过的档不重复下),目标档落地即自动启用、伴档备好;目标档已 启用显示「使用中」点击取消使用。弹层高度上限 70% 屏高(防盖满全屏无法关闭)。 档位(2026-09-03 修订):同一物种一次只运行一个档位——激活某档会自动停用 同物种另一档,不同物种可用不同档位并行识别(如雉鸡用高性能、斑鸠用高精度)。 弹层顶部「识别模式」分段控件(高性能 / 高精度,默认高精度,持久化本地; 2026-09-03 高性能移到左位)是目标档偏好:不直接切换运行中的模型, 只决定卡片按钮面向哪个档。卡片主按钮(2026-09-03 简化:不再出现「改用X」、 不再显示当前运行档提示):目标档未下载 →「下载」(落地自动启用、伴档备好; 同一动物另一档在使用时补下目标档后自动切换过去);已下载未启用 →「使用」 点击即用(另一档在使用会被自动停用);启用中 →「使用中」点击取消使用。
  • 存储:应用私有目录 models/<datasetId>/<variant>/(双档位 2026-09-03 原无 variant 目录与存量 s 档一致——s 档复用 models/<datasetId>/ 同级读取, 目录键 = 档位标识符),含 model.tflitelabels.jsonmeta.json meta 记录 {version, sha256})。版本与摘要都未变化时跳过下载;变化则下载到 .part 临时文件、sha256 校验通过后原子 rename 替换,失败重试一次并保留旧模型, 下次启动再试——检查记账按 (datasetId, variant) 独立。
  • 目录缓存(2026-09-03:最近一次成功拉取的模型目录落盘应用私有目录 catalog.json;每次刷新先载入缓存、展示立即可用(弹层离线/弱网也有内容), 网络成功后再以权威目录覆盖缓存。清理与自动更新只在网络拉取成功时执行: 离线降级不清文件、不触发下载——升级后断网首启不会误删已下载模型。
  • 清理:服务器下线的数据集下次同步时删除本地对应目录(两档都无条目时才删)。
  • 识别会话制(2026-09-03 修订):每次进入视野页即开始新识别会话——从 仅预览开始、不自动恢复上次使用的模型(上次崩溃/坏模型不会在下一次打开时 自动复现,用户总能进入模型清单调整;这也是打开即慢的根因消除项:进入页面不再 读模型/重建推理器)。识别需在模型清单手动启用,启用状态只在本会话内生效 (不跨会话持久化);已下载文件常驻本地,「使用」即点即用不重新下载。 服务器新版本仅对已下载文件后台自动补齐,补齐不改变启用状态。
  • 并行推理合并:识别时加载本会话已启用的全部模型(激活集即实际运行集, 会话内每数据集至多一个档位;DetectorWorker isolate 内逐模型加载,单个失败 不影响其他;worker 构建完成后原地挂到已启动的相机流,不重启相机), 同帧各模型独立推理后做跨模型 NMS(所有模型的框统一按 IoU 去重取高分—— 异类别重叠也压制,实测多个模型会对同一目标检出不同类别),结果叠加 modelName (数据集名)标注来源。

实现:lib/models/model_manager.dart(下载/校验/清单/会话制单档激活收敛, ModelManager 单例 + ChangeNotifier,条目身份含档位)、 lib/detection/detector_worker.dart(多模型并行推理与 mergeAcrossModels)、 lib/camera/camera_screen.dart(会话重置从仅预览开始 + 先出预览后台同步 + 设置弹层「识别模式」目标档切换 + 底部状态胶囊:识别延迟)、 lib/camera/model_catalog_section.dart(清单网格:按数据集合并卡片、 双档下载与「使用/使用中/下载」主按钮动作,2026-09-03 简化文案)。

  • 模型输入是 NHWC:训练导出的模型需做字节级手术(开头 TRANSPOSE→RESHAPE 输入 [1,320,320,3])再发布给 App,否则 iOS 报 "Node number 0 (TRANSPOSE) failed to prepare"。
  • 模拟器黑屏:本机 iOS 模拟器 Impeller 渲染黑屏,验证 UI 用 VM service flutter run 输出里的 DevTools 地址),或直接真机验证。