初始化 observer 项目:纯代码,不含权重与训练数据

This commit is contained in:
2026-08-20 13:11:57 +08:00
commit b9934c996d
52 changed files with 4737 additions and 0 deletions
+318
View File
@@ -0,0 +1,318 @@
# Observer(野视)· 野生动物实时识别 Android App — 技术方案
| 项目 | 内容 |
| --- | --- |
| 文档版本 | v1.1 |
| 编写日期 | 2026-08-17 |
| 状态 | 初稿(v1.1:确认功能范围为"仅实时识别",移除拍照留存) |
| 适用产品 | 纯 Android 原生 App |
---
## 1. 项目概述
### 1.1 项目背景
野生动物观察爱好者在户外需要一款便携工具:打开手机相机,即可从实时画面中识别野鸡,通过检测框与提醒辅助快速发现。
### 1.2 项目目标
- 端侧实时目标检测,**全程离线可用**,不依赖网络;
- 野鸡实时框选识别,展示类别、置信度与**大致距离**;
- 检测到目标时通过震动 / 声音提醒用户,辅助快速发现;
- 纯 Android 原生 AppKotlin),兼容 Android 7.0+ 主流机型,中端机流畅运行;
- **不做拍照、记录、统计等留存功能**,专注实时识别这一核心体验。
### 1.3 名词术语
| 术语 | 说明 |
| --- | --- |
| ImageAnalysis | CameraX 的帧分析用例,用于逐帧回调图像数据 |
| TFLite | TensorFlow LiteGoogle 端侧推理框架 |
| YOLO | You Only Look Once,单阶段目标检测算法 |
| NMS | Non-Maximum Suppression,非极大值抑制 |
| GPU Delegate | TFLite 的 GPU 加速委托,将算子下发 GPU 执行 |
| mAP | mean Average Precision,目标检测平均精度指标 |
| IoU | Intersection over Union,交并比 |
---
## 2. 需求概述
### 2.1 目标用户
| 用户群 | 典型诉求 |
| --- | --- |
| 野生动物观察爱好者 | 户外实时识别画面中的野鸡,不惊扰、近距离观察 |
| 户外徒步 / 摄影人群 | 快速发现野鸡,辅助取景构图 |
| 自然教育、科普工作者 | 物种识别辅助教学 |
### 2.2 核心场景
| 编号 | 场景 | 描述 |
| --- | --- | --- |
| S1 | 野外实时识别 | 徒步 / 观鸟时打开相机,实时框选画面中的野鸡,显示类别、置信度与大致距离 |
| S2 | 快速扫视寻找 | 移动取景快速扫视,检测到野鸡立即震动 / 声音提醒,无需停留操作 |
### 2.3 功能需求摘要
实时识别(P0)、识别结果叠加展示(P0)、检测提醒(P1)、生境区域预警(P1)、设置(P1)、合规提示(P0)、低光增强(P2)。详见《项目功能文档》。
### 2.4 非功能需求摘要
| 指标 | 目标 |
| --- | --- |
| 识别延迟 | 中端机单帧 ≤ 80ms |
| 预览流畅度 | 连续模式 2~3 帧检测一次,预览不卡顿 |
| 耗电 | 连续使用 1 小时耗电 ≤ 15% |
| 兼容性 | Android 7.0+minSdk 24),覆盖主流国产机与三星 |
| 稳定性 | 崩溃率 ≤ 0.5%,启动成功率 ≥ 99% |
---
## 3. 总体架构
### 3.1 架构图
```mermaid
graph TD
subgraph UI层["UI 层 · Jetpack Compose"]
MainScreen["相机主界面<br/>预览 + 检测叠加层"]
SettingsScreen["设置界面"]
end
subgraph ViewModel层["ViewModel 层"]
CameraViewModel["CameraViewModel"]
SettingsViewModel["SettingsViewModel"]
end
subgraph 领域层["领域层"]
Detector接口["Detector 接口"]
end
subgraph 基础设施层["基础设施层"]
CameraX["CameraX<br/>Preview / ImageAnalysis"]
TFLite["TFLite 推理引擎<br/>YOLOv8n 模型 + GPU Delegate"]
DataStore["DataStore 设置存储"]
end
UI层 --> ViewModel层
ViewModel层 --> 领域层
领域层 --> 基础设施层
```
### 3.2 分层说明
| 层 | 职责 |
| --- | --- |
| UI 层 | Compose 页面渲染、检测叠加层绘制、交互 |
| ViewModel 层 | 页面状态管理、识别结果分发、提醒触发 |
| 领域层 | 检测器抽象接口 |
| 基础设施层 | CameraX、TFLite、DataStore 等能力实现 |
### 3.3 核心数据流(识别链路)
```mermaid
flowchart LR
A["CameraX 相机帧"] --> B["ImageAnalysis 帧分析"]
B --> C["预处理<br/>缩放 320×320 / 归一化 / 旋转校正"]
C --> D["TFLite 推理<br/>YOLOv8n"]
D --> E["后处理<br/>解码 / NMS / 阈值过滤"]
E --> F["叠加层渲染<br/>边框 + 类别 + 置信度"]
F --> G["UI 展示"]
E --> H["结果提醒<br/>震动 / 提示音"]
```
### 3.4 部署形态
- **本地优先**:识别全部离线完成,运行期无网络请求;
- **云端(V2 可选)**:仅用于模型版本更新下发。
---
## 4. 技术选型
| 类别 | 选型 | 理由 |
| --- | --- | --- |
| 开发语言 | Kotlin | Android 官方推荐,协程生态成熟 |
| UI 框架 | Jetpack ComposeMaterial 3 | 声明式 UI,开发效率高,叠加层绘制灵活 |
| 相机 | CameraXPreview / ImageAnalysis | Jetpack 官方库,生命周期安全,机型兼容性最佳 |
| 目标检测模型 | YOLOv8n(自定义 4 类训练) | 精度/速度均衡,端侧部署方案成熟 |
| 推理框架 | TensorFlow Lite 2.16+GPU Delegate | 官方支持 GPU 加速;备选 NCNN / MNN |
| 配置存储 | DataStore Preferences | 阈值、开关等设置 |
| 异步 | Kotlin Coroutines + Flow | 主线程安全,生命周期感知 |
| 构建 | GradleKotlin DSL+ AGP 8.x | 现代构建配置 |
---
## 5. 核心功能技术方案
### 5.1 实时识别管线
- CameraX 组合:`Preview`(取景)+ `ImageAnalysis`(识别);
- `ImageAnalysis` 使用 `STRATEGY_KEEP_ONLY_LATEST` 背压策略,保证不积压帧;
- 输出格式使用 `OUTPUT_IMAGE_FORMAT_RGBA_8888`CameraX 1.3+),免去 YUV→RGB 手动转换;
- **帧节流**:连续模式每 2~3 帧检测一次;标准模式 300ms 一次;省电模式每秒一次(依据设置);
- 推理在独立检测线程执行,单例互斥锁防止重叠推理;分析线程不阻塞主线程。
### 5.2 检测叠加层
- Compose Canvas 绘制检测框(矩形 + 类别标签 + 置信度),颜色按类别区分;
- 检测框内同时标注**大致距离**(如"约 25m"),随检测框实时更新;
- 距离标注与检测框同生共灭,展示逻辑一致(≥ 0.5s 出现、消失 2s 后移除);
- 坐标映射链路:模型归一化坐标 → 旋转校正(sensor rotation)→ 预览视图坐标(含 FIT_CENTER 裁剪偏移修正);
- 目标保持 ≥ 0.5s 才展示,避免单帧误检闪烁。
### 5.3 距离标注(单目估计)
- 采用单目针孔模型:`距离 ≈ 焦距px × 物种参考体型 / 检测框像素高度`
- 焦距 px 由相机内参换算(`LENS_INFO_AVAILABLE_FOCAL_LENGTHS` × `SENSOR_INFO_PHYSICAL_SIZE`),个别机型回退用视场角推算;
- 物种参考体型内置表(见 6.1),每类一个平均体型值;
- 展示为"约 X m"5~50m 范围误差预期 ≤ ±30%;
- 纯算术运算,无额外模型与算力开销;V2 可引入地面平面法(设备俯仰角 + 相机高度)提升精度。
### 5.4 检测提醒
- 检测到目标时触发震动 / 提示音,用户可开关;
- 防重复打扰:10s 内同类目标只提醒一次;
- 目标消失后 2s 内不闪断显示,避免频繁提醒。
### 5.5 低光增强(P2
- 简单直方图均衡 / 亮度增益提升低光帧可见度;
- V2 可引入夜视增强网络思路。
---
## 6. AI 模型方案(核心)
### 6.1 目标类别定义
| 类别 ID | 英文标签 | 中文名 | 涵盖范围 | 参考体型(距离估计用) |
| --- | --- | --- | --- | --- |
| 0 | pheasant | 野鸡 | 环颈雉等雉类 | ≈ 0.45m(身高) |
### 6.2 模型选型对比
| 模型 | 输入尺寸 | 中端机单帧耗时(GPU) | 精度 | 说明 |
| --- | --- | --- | --- | --- |
| **YOLOv8n(推荐)** | 320~640 | 约 30~80ms | 高 | 支持自定义类别训练,精度/速度平衡 |
| EfficientDet-Lite0/2 | 320 | 约 20~50ms | 中 | 仅 COCO 预置类别 |
| YOLOv5s | 640 | 约 80~150ms | 高 | 体积与耗电偏大 |
| SSD MobileNetV2 | 300 | 约 15~30ms | 低 | 小目标与远距离效果差 |
**结论**:选用 **YOLOv8n**,使用预训练权重迁移学习自定义训练,导出 TFLite 端侧部署。
### 6.3 数据集方案
| 项 | 方案 |
| --- | --- |
| 数据量 | 1000~2000 张(首版可 500+ 起步,滚动补充) |
| 多样性 | 覆盖不同季节、晨昏/正午/逆光、远近距离、姿态、遮挡、背景(草丛/农田/林地/雪地) |
| 标注 | Roboflow 或 labelImgYOLO 格式(class, cx, cy, w, h |
| 数据增强 | Mosaic、MixUp、HSV 扰动、随机翻转、随机缩放裁剪 |
| 数据划分 | train 80% / val 10% / test 10% |
| 负样本 | 补充无目标场景图,控制误检 |
### 6.4 训练方案
- 框架:ultralytics YOLOv8,预训练权重 `yolov8n.pt` 迁移学习;
- 超参:imgsz=640(训练)、epochs 100~200(早停)、batch 16~32(视 GPU 而定);
- 评估指标:
- mAP@0.5 ≥ **0.85**(达标线 0.80);
- mAP@0.5:0.95 ≥ **0.55**
- 负样本误检率 ≤ **2%**
- 远距离小目标(高度 ≤ 20px)召回率 ≥ **60%**
### 6.5 模型转换与量化
- 导出命令:`yolo export model=best.pt format=tflite imgsz=320`
- 量化策略:
- 优先 **fp16**(配合 GPU Delegate,精度损失小);
- 低端机 / CPU 场景使用 **int8** 量化(体积更小、CPU 更快);
- 输入尺寸:320×320(连续检测)/ 416×416(远距离模式,V2)。
### 6.6 推理优化
- GPU Delegate 优先,初始化失败自动回退 CPU(NNAPI 可选);
- 推理线程 4 线程,首次推理前执行预热(dummy run);
- 输入输出 ByteBuffer / Bitmap 复用,避免热路径频繁分配。
### 6.7 精度与速度目标
| 指标 | 目标 |
| --- | --- |
| 检测阈值(默认) | confidence ≥ 0.40IoU-NMS = 0.45 |
| 单帧推理延迟 | ≤ 80ms(中端机,320 输入,GPU |
| 模型体积 | fp16 ≤ 8MBint8 ≤ 4MB |
| 识别类别数 | 1 类(野鸡) |
| 距离标注 | "约 X m"5~50m 误差 ≤ ±30%,计算开销可忽略 |
---
## 7. 性能指标与优化
| 指标 | 目标值 | 主要优化手段 |
| --- | --- | --- |
| 启动到相机预览 | ≤ 1.5s | 启动即初始化 CameraProvider,懒加载非核心模块 |
| 单帧检测延迟 | ≤ 80ms | GPU Delegate、320 输入、线程复用 |
| 预览帧率 | ≥ 25fps | 帧节流、KEEP_ONLY_LATEST、避免主线程工作 |
| 内存峰值 | ≤ 200MB | Bitmap/ByteBuffer 复用,无大对象常驻 |
| 连续 1 小时耗电 | ≤ 15% | 检测帧节流、省电模式、后台自动释放相机 |
| App 体积 | ≤ 40MB | ABI 拆分、模型量化、R8 混淆 |
---
## 8. 数据与隐私
- 无账号、无埋点、无广告 SDK,**不采集任何用户数据**;
- 不保存照片、不记录位置,运行期无网络请求;
- 模型与标签随 APK 打包,识别数据仅存在于内存中,随进程结束自动释放。
---
## 9. 安全与合规
- 产品定位为**野生动物观察、识别工具**,不提供任何猎捕、诱捕、伤害野生动物的功能或指导;
- 遵守《中华人民共和国野生动物保护法》《陆生野生动物保护实施条例》等法律法规;
- 野鸡(雉类)属"三有"保护动物,猎捕须依法取得许可,App 不鼓励、不协助非法猎捕;
- App 内置观察伦理提示:保持安全距离、不惊扰动物、遵守保护区管理规定(首次启动展示,功能文档 F07)。
---
## 10. 开发计划(里程碑)
| 阶段 | 周期 | 交付内容 |
| --- | --- | --- |
| M0 准备 | 1 周 | 需求确认、数据集启动采集、Android 工程脚手架、模型基线 |
| M1 MVP | 3~4 周 | 相机预览 + 实时检测 + 叠加层 + 检测提醒 + 设置 + 合规提示 |
| M2 打磨发布 | 1~2 周 | 性能优化、真机矩阵测试、混淆加固、上架准备 |
总计约 **5~7 周**(不含数据采集并行时间)。
---
## 11. 风险与应对
| 风险 | 影响 | 应对 |
| --- | --- | --- |
| 训练数据不足 / 类别相近 | 精度低、误检 | 持续滚动采集数据;增加难例挖掘;灰度发布迭代模型 |
| 小目标(远处动物) | 漏检 | 提高输入分辨率;帧节流换取算力;提示用户靠近/变焦;V2 引入 SAHI/tiling 或专用小目标模型 |
| 低光 / 夜间场景 | 漏检 | 低光增强预处理;V2 夜视模式 |
| 单目距离估计精度有限 | 距离显示不准 | 标注为"约"并明示误差预期;体型表持续校准;V2 地面平面法 + 水下折射修正 |
| 生境区域误报偏多 | 提醒频繁、体验差 | 独立低阈值 + 区分提醒方式 + 防重复机制;生境阈值可调;模型迭代降低误报 |
| 中低端机型性能不足 | 帧率低、发热 | 动态分辨率、降频检测、GPU 回退策略、省电模式 |
| CameraX 个别机型异常 | 黑屏/闪退 | 机型兼容测试矩阵、崩溃监控、失败回退(重试/默认配置) |
| 合规风险(被用于非法猎捕) | 法律风险 | 产品定位为观察工具、内置合规提示、不提供猎捕辅助功能 |
---
## 12. 验收标准
1. 野鸡测试集 mAP@0.5 ≥ 0.85,负样本误检率 ≤ 2%;
2. 中端机(如骁龙 7 系)单帧检测 ≤ 80ms,预览流畅无卡顿;
3. 连续使用 1 小时耗电 ≤ 15%,无异常发热;
4. 主流机型(小米 / 华为 / OPPO / vivo / 三星,各 ≥ 2 台)启动成功率 ≥ 99%,崩溃率 ≤ 0.5%;
5. 飞行模式下实时识别全流程(预览、检测、提醒、设置)可用;
6. 检测框正确显示类别、置信度与距离;5~50m 范围距离误差 ≤ ±30%;
7. 通过应用商店合规审核(隐私政策、权限声明完整)。
+193
View File
@@ -0,0 +1,193 @@
# Observer(野视)· 项目功能文档
| 项目 | 内容 |
| --- | --- |
| 文档版本 | v1.1 |
| 编写日期 | 2026-08-17 |
| 状态 | 初稿(v1.1:确认功能范围为"仅实时识别",移除拍照留存) |
| 配套文档 | 《01-技术方案》《03-技术实现文档》 |
---
## 1. 产品概述
### 1.1 产品定位
一款**纯 Android 原生**的野生动物实时识别 App。用户打开相机,即可从实时画面中识别野鸡,通过彩色检测框(含类别、置信度、距离标注)与震动 / 声音提醒辅助快速发现。**无拍照、无记录、无统计**,专注实时识别这一核心体验,**全程离线可用**。
### 1.2 目标用户
- 野生动物观察爱好者(观鸟 / 观兽);
- 户外徒步、摄影人群;
- 自然教育、科普工作者。
### 1.3 价值主张
- **即时发现**:打开相机即识别,检测到目标立即提醒,免去翻图鉴、猜物种;
- **零门槛**:不依赖网络,不依赖外设,一部手机即可;
- **纯净体验**:无留存功能、无账号、无广告,即开即用。
---
## 2. 功能架构(功能树)
```
Observer
├── 实时识别
│ ├── 相机实时预览(F01)
│ ├── 目标实时检测(F02)
│ ├── 识别结果展示与距离标注(F03)
│ ├── 检测提醒(F04
│ └── 低光增强(F06,P2)
├── 设置
│ ├── 识别参数(F05
│ └── 提醒设置(F04 子项)
└── 合规提示(F07
```
---
## 3. 功能需求清单
优先级定义:**P0**=必须,**P1**=V1.0**P2**=后续版本。
| 编号 | 功能 | 功能描述 | 优先级 |
| --- | --- | --- | --- |
| F01 | 相机实时预览 | 全屏取景,默认后置,支持自动对焦、双指缩放、点击对焦、前后摄切换 | P0 |
| F02 | 目标实时检测 | 对野鸡实时检测框选 | P0 |
| F03 | 识别结果展示与距离标注 | 检测框 + 类别名 + 置信度百分比 + 大致距离(约 X m);四类目标使用不同颜色边框 | P0 |
| F04 | 检测提醒 | 检测到目标时震动 / 提示音;支持开关与 10s 防重复 | P1 |
| F05 | 设置 | 置信度阈值(0.2~0.7)、检测频率(连续 / 标准 / 省电)、提醒开关、低光增强开关 | P1 |
| F06 | 低光增强 | 低光环境下自动亮度 / 对比度增强,提高识别率 | P2 |
| F07 | 合规提示 | 首次启动展示《观察伦理与法律提示》,需用户确认 | P0 |
| F08 | 生境区域预警 | 画面中未检测到目标时,识别草丛 / 灌木 / 水面等疑似生境区域,以黄色虚线框展示(含距离标注)并触发预警(独立阈值与提醒方式) | P1 |
---
## 4. 核心业务流程
### 4.1 首次启动与授权
```
启动 App → 合规提示页(F07,用户确认)
→ 请求相机权限
├─ 授权 → 进入相机主界面
└─ 拒绝 → 引导页说明用途,可重新授权
```
### 4.2 实时识别
```
相机主界面(默认后置)
→ 画面持续检测(连续 / 标准 / 省电频率)
→ 检测到目标:显示彩色检测框 + 类别 + 置信度 + 距离
├─ 触发提醒(震动 / 声音,10s 防重复)
└─ 目标停留 ≥ 0.5s 保持显示,消失后 2s 内不闪断
→ 未检测到目标时:识别疑似生境区域(草丛 / 灌木 / 水面)
├─ 黄色虚线框展示,标注"疑似区域"与距离
└─ 触发预警(提醒方式与动物检测区分)
→ 用户可点击检测框查看识别详情(物种、置信度、时间)
```
---
## 5. 界面设计
### 5.1 界面总览
仅两个界面:**相机主界面**(默认全屏展示)+ **设置页**(右上角齿轮入口)。
### 5.2 相机主界面(核心界面)
```
┌─────────────────────────────────┐
│ [低光] [频率] [前/后摄] │ ← 顶部工具
│ 相机预览画面(全屏) │
│ ┌─────────────────────────────┐ │
│ │ ┌───────┐ │ │
│ │ │ 野鸡 86% │ │ │ ← 检测框(按类别着色)
│ │ │ 约 25m │ │ │
│ │ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ │ │ ← 疑似生境区域(黄色虚线,预警)
│ │ 疑似区域 · 约 20m │ │
│ │ └───────┘ │ │
│ └─────────────────────────────┘ │
│ [提醒开关] [设置] │ ← 底部操作
└─────────────────────────────────┘
```
交互说明:
- 顶部:检测频率切换(连续 / 标准 / 省电)、低光增强开关(P2)、前后摄像头切换;
- 中部:全屏预览,检测框 + 类别 + 置信度实时叠加;
- 底部:提醒开关(震动 / 声音)、设置入口;
- 支持双指缩放、点击对焦;
- 检测到目标时边框高亮 + 震动 / 声音提醒(10s 防重复);
- 检测框内显示类别、置信度与大致距离(约 X m),随目标实时更新;
- 未检测到目标时,疑似生境区域以黄色虚线框展示(含距离标注)并预警(提醒方式与动物检测区分)。
### 5.3 识别结果卡片(点击检测框弹出)
| 元素 | 说明 |
| --- | --- |
| 物种名称 | 中文名 + 英文标签(如:野鸡 pheasant |
| 置信度 | 百分比进度条 |
| 时间 | 检测时刻 |
| 距离 | 约 25m(单目估算,误差 ±30%) |
仅展示信息,无保存操作。
疑似生境区域(黄色虚线框)点击后展示:区域类别、置信度、距离(约 X m,按参考植被高度估算,误差较大)。
### 5.4 设置页
| 分组 | 设置项 | 默认值 |
| --- | --- | --- |
| 识别 | 置信度阈值 | 0.40 |
| 识别 | 检测频率 | 连续 |
| 识别 | 低光增强 | 关(P2) |
| 识别 | 距离标注 | 开 |
| 识别 | 生境区域阈值 | 0.35 |
| 提醒 | 震动提醒 / 提示音 / 防重复时长 | 开 / 开 / 10s |
| 提醒 | 生境区域预警 | 开 |
| 关于 | 版本信息 / 隐私说明 | — |
---
## 6. 权限设计
| 权限 | 用途 | 时机 | 备注 |
| --- | --- | --- | --- |
| CAMERA | 实时预览与识别 | 首次启动 | 必须 |
原则:**最小权限、按需申请**。仅申请相机权限;拒绝后提供说明引导页(可跳转系统设置重新授权)。不申请定位、存储等任何其他权限。
---
## 7. 非功能需求
| 类别 | 要求 |
| --- | --- |
| 兼容性 | Android 7.0+minSdk 24),竖屏为主,支持暗色模式 |
| 性能 | 见《技术方案》第 7 章(延迟 ≤ 80ms、启动 ≤ 1.5s、内存 ≤ 200MB |
| 离线 | 飞行模式下实时识别全流程完整可用 |
| 稳定性 | 崩溃率 ≤ 0.5%,启动成功率 ≥ 99%,异常自动降级(GPU 回退 CPU) |
| 耗电 | 连续使用 1 小时 ≤ 15%;省电模式 ≤ 8% |
| 隐私 | 无广告 SDK、无埋点、不保存任何数据,识别数据仅存内存 |
| 无障碍 | 关键操作支持 TalkBack 描述;检测结果支持语音播报(V2) |
---
## 8. 版本规划
| 版本 | 范围 | 说明 |
| --- | --- | --- |
| MVP / V1.0 | F01~F05、F07、F08 | 相机 + 实时识别 + 生境预警 + 提醒 + 设置,即完整核心体验 |
| V2.0 | +F06 及增强 | 低光增强、更多物种、夜视模式、模型远程更新、语音播报、距离精度增强(地面平面法) |
---
## 9. 合规与安全说明
- App 定位为**野生动物观察、识别工具**,不提供猎捕、诱捕、伤害野生动物的功能或指导;
- 首次启动展示合规提示:遵守《中华人民共和国野生动物保护法》,野鸡属"三有"保护动物,猎捕须依法许可;观察时保持距离、不惊扰动物、遵守保护区规定;
- 上架需提供隐私政策,声明仅使用相机权限、不采集与存储任何用户数据。
+512
View File
@@ -0,0 +1,512 @@
# Observer(野视)· 技术实现文档
| 项目 | 内容 |
| --- | --- |
| 文档版本 | v1.1 |
| 编写日期 | 2026-08-17 |
| 状态 | 初稿(v1.1:确认功能范围为"仅实时识别",移除拍照、记录、相册识别、定位相关实现) |
| 配套文档 | 《01-技术方案》《02-项目功能文档》 |
---
## 1. 项目结构
```
app/
├── src/main/
│ ├── java/com/example/observer/
│ │ ├── MainActivity.kt // 单 Activity 入口
│ │ ├── camera/
│ │ │ ├── CameraController.kt // CameraX 生命周期绑定与用例组合
│ │ │ └── FrameAnalyzer.kt // ImageAnalysis 帧分析器(节流 + 调度)
│ │ ├── detection/
│ │ │ ├── Detector.kt // 检测器抽象接口
│ │ │ ├── TFLiteDetector.kt // TFLite 实现(YOLOv8n
│ │ │ ├── DetectionResult.kt // 检测结果数据类
│ │ │ ├── Nms.kt // NMS 后处理
│ │ │ └── CoordinateMapper.kt // 模型坐标 → 视图坐标映射
│ │ ├── distance/
│ │ │ └── DistanceEstimator.kt // 单目距离估计(针孔模型)
│ │ ├── overlay/
│ │ │ └── DetectionOverlay.kt // Compose 叠加层(Canvas 绘制检测框)
│ │ ├── reminder/
│ │ │ └── Reminder.kt // 震动 / 提示音提醒(含防重复)
│ │ ├── ui/
│ │ │ ├── camera/ CameraScreen.kt / CameraViewModel.kt
│ │ │ └── settings/ SettingsScreen.kt / SettingsViewModel.kt
│ │ └── util/
│ │ └── BitmapUtils.kt // 缩放 / 旋转 / 复用
│ ├── assets/
│ │ ├── model.tflite // YOLOv8n 四类模型
│ │ └── labels.txt // 类别标签(按 ID 顺序)
│ └── res/
└── build.gradle.kts
```
---
## 2. 技术栈与版本
| 组件 | 版本(以最新稳定为准) | 用途 |
| --- | --- | --- |
| Kotlin | 2.x | 开发语言 |
| AGP | 8.x | Android Gradle 插件 |
| Jetpack Compose | BOM 2024.x+Material 3 | UI |
| CameraX | 1.4.x | Preview / ImageAnalysis |
| TensorFlow Lite | 2.16.x | 端侧推理 |
| tensorflow-lite-gpu | 2.16.x | GPU 加速 |
| DataStore | 1.1.x | 设置存储 |
依赖(build.gradle.kts 关键片段):
```kotlin
dependencies {
implementation("androidx.camera:camera-core:1.4.1")
implementation("androidx.camera:camera-camera2:1.4.1")
implementation("androidx.camera:camera-lifecycle:1.4.1")
implementation("androidx.camera:camera-view:1.4.1")
implementation("org.tensorflow:tensorflow-lite:2.16.1")
implementation("org.tensorflow:tensorflow-lite-gpu:2.16.1")
implementation("androidx.datastore:datastore-preferences:1.1.1")
}
```
---
## 3. 模块设计
### 3.1 camera 模块 — CameraController.kt
职责:创建并绑定 CameraX 用例(Preview + ImageAnalysis),统一生命周期。
```kotlin
class CameraController(
private val lifecycleOwner: LifecycleOwner,
private val analysisExecutor: Executor,
) {
private lateinit var cameraProvider: ProcessCameraProvider
private lateinit var imageAnalysis: ImageAnalysis
fun start(previewView: PreviewView, analyzer: ImageAnalysis.Analyzer) {
val future = ProcessCameraProvider.getInstance(previewView.context)
future.addListener({
cameraProvider = future.get()
val preview = Preview.Builder().build().also {
it.surfaceProvider = previewView.surfaceProvider
}
imageAnalysis = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_RGBA_8888)
.build()
imageAnalysis.setAnalyzer(analysisExecutor, analyzer)
cameraProvider.bindToLifecycle(
lifecycleOwner,
CameraSelector.DEFAULT_BACK_CAMERA,
preview, imageAnalysis,
)
}, ContextCompat.getMainExecutor(previewView.context))
}
}
```
要点:
- `OUTPUT_IMAGE_FORMAT_RGBA_8888`CameraX 1.3+)直接得到 RGBA 图像,免 YUV 转换;
- `STRATEGY_KEEP_ONLY_LATEST`:分析器忙时丢弃旧帧,不积压;
- 相机权限检查与请求在进入本模块前完成;
- 前后摄切换:重新以对应 `CameraSelector` 执行 `bindToLifecycle`(解绑旧用例)。
### 3.2 camera 模块 — FrameAnalyzer.kt
职责:帧节流、旋转获取、调度检测、结果回调。
```kotlin
class FrameAnalyzer(
private val detector: Detector,
private val onResult: (List<DetectionResult>, Int) -> Unit, // 结果 + 旋转角
) : ImageAnalysis.Analyzer {
private var lastDetectMs = 0L
override fun analyze(imageProxy: ImageProxy) {
val now = SystemClock.elapsedRealtime()
val interval = frameIntervalMs() // 依据设置:连续/标准/省电
if (now - lastDetectMs < interval) {
imageProxy.close(); return
}
lastDetectMs = now
val bitmap = imageProxy.toBitmap() // RGBA_8888 直接转 Bitmap
val rotation = imageProxy.imageInfo.rotationDegrees
try {
val results = detector.detect(bitmap)
onResult(results, rotation)
} finally {
imageProxy.close() // 必须关闭,否则阻塞流
}
}
}
```
### 3.3 detection 模块 — Detector 接口
```kotlin
interface Detector {
/** 输入 RGBA 位图,输出归一化坐标检测结果(0..1) */
fun detect(bitmap: Bitmap): List<DetectionResult>
}
```
### 3.4 detection 模块 — TFLiteDetector.kt
```kotlin
class TFLiteDetector(
context: Context,
private val inputSize: Int = 320,
private val confThreshold: Float = 0.40f, // 动物类阈值
private val habitatThreshold: Float = 0.35f, // 生境区域阈值
private val iouThreshold: Float = 0.45f,
gpuEnabled: Boolean = true,
) : Detector {
private val labels: List<String> =
context.assets.open("labels.txt").bufferedReader().readLines()
private val interpreter: Interpreter = Interpreter(
loadModelFile(context, "model.tflite"),
Interpreter.Options().apply {
setNumThreads(4)
if (gpuEnabled) {
try { addDelegate(GpuDelegate()) } catch (_: Exception) { /* 回退 CPU */ }
}
},
)
private val inputBuffer: ByteBuffer = ByteBuffer.allocateDirect(
1 * inputSize * inputSize * 3 * 4 // float32
).order(ByteOrder.nativeOrder())
private val outputBuffer: ByteBuffer = ByteBuffer.allocateDirect(
OUTPUT_ELEMENTS * 4
).order(ByteOrder.nativeOrder())
override fun detect(bitmap: Bitmap): List<DetectionResult> {
preprocess(bitmap, inputBuffer) // 缩放 + RGB 归一化 → 输入缓冲
interpreter.run(inputBuffer, outputBuffer) // 单张推理
return postprocess(outputBuffer, bitmap.width, bitmap.height)
}
}
```
输入 / 输出规格(YOLOv8n 四类,320 输入):
- 输入:`[1, 320, 320, 3]`RGBfloat32 归一化 0~1
- 输出:`[1, 9, 8400]`8400 = 各尺度 anchor 数,9 = 4 个框坐标 cx/cy/w/h + 5 类得分),展平为 `9 * 8400` 个 float。
### 3.5 detection 模块 — 后处理(解码 + NMS)
```kotlin
private fun postprocess(raw: FloatArray, imgW: Int, imgH: Int): List<DetectionResult> {
val numAnchor = raw.size / 9
val boxes = mutableListOf<DetectionResult>()
for (a in 0 until numAnchor) {
val cx = raw[a]; val cy = raw[9 + a]
val w = raw[18 + a]; val h = raw[27 + a]
var bestCls = 0; var bestScore = 0f
for (c in 0 until 5) {
val s = raw[36 + c * numAnchor + a] // 类别得分按 anchor 平铺
if (s > bestScore) { bestScore = s; bestCls = c }
}
if (bestScore < confThreshold) continue
boxes += DetectionResult(
label = labels[bestCls],
score = bestScore,
// 归一化坐标(裁剪到 [0,1])
left = (cx - w / 2).coerceIn(0f, 1f),
top = (cy - h / 2).coerceIn(0f, 1f),
right = (cx + w / 2).coerceIn(0f, 1f),
bottom = (cy + h / 2).coerceIn(0f, 1f),
)
}
return nms(boxes, iouThreshold)
}
fun nms(boxes: List<DetectionResult>, iouThreshold: Float): List<DetectionResult> {
val sorted = boxes.sortedByDescending { it.score }
val kept = mutableListOf<DetectionResult>()
for (b in sorted) {
if (kept.none { iou(b, it) > iouThreshold }) kept += b
}
return kept
}
```
注意:以上取数索引为示意,**必须与模型导出时的输出布局(yolov8 tflite 为 9×8400,按列平铺)核对一致**,建议训练导出后用 Python 脚本先对单图做一致性校验再接入 App。
### 3.6 distance 模块 — DistanceEstimator.kt
```kotlin
class DistanceEstimator(context: Context) {
private val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
private var focalPxCache = -1f
// 物种参考体型(米),用于针孔模型估算
private val speciesSizeM = mapOf(
"pheasant" to 0.45f, // 身高
)
/** 距离 = 焦距px × 参考体型 / 框高px(在分析图像分辨率下计算) */
fun estimate(label: String, boxHeightNorm: Float, imageHeightPx: Int): Float? {
val realH = speciesSizeM[label] ?: return null
val boxH = boxHeightNorm * imageHeightPx
if (boxH < 8f) return null // 过小目标不估算
val focalPx = focalPx(imageHeightPx)
if (focalPx <= 0) return null
return round(focalPx * realH / boxH)
}
/** focal_px = focal_mm × (imageHeightPx / sensorHeightMm);视场角作回退 */
private fun focalPx(imageHeightPx: Int): Float {
if (focalPxCache > 0) return focalPxCache
val c = cameraManager.getCameraCharacteristics(cameraManager.cameraIdList.first())
val focalMm = c.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS)?.firstOrNull()
val sensor = c.get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE)
val fov = c.get(CameraCharacteristics.LENS_INFO_HORIZONTAL_VIEW_ANGLE)
focalPxCache = when {
focalMm != null && sensor != null -> focalMm * imageHeightPx / sensor.height
fov != null -> (imageHeightPx / 2f) / tan(fov / 2f)
else -> -1f
}
return focalPxCache
}
}
```
说明:
- 前后摄切换时需按当前相机重新获取焦距(缓存按相机 ID 区分);
- 距离估算仅供参考,实际距离可能因环境因素有所偏差。
### 3.7 detection 模块 — CoordinateMapper.kt
模型输出为归一化坐标(相对分析图像,竖屏方向),需转换到预览视图坐标:
```kotlin
class CoordinateMapper(private val view: PreviewView) {
/** 归一化坐标 → 预览视图像素坐标(考虑传感器旋转与 FIT_CENTER 裁剪) */
fun mapToView(norm: DetectionResult, rotation: Int): RectF {
// 1) 旋转校正:把"图像方向"归一化坐标转到"竖屏视图方向"
val (x0, y0, x1, y1) = rotate(norm, rotation)
// 2) 处理 FIT_CENTER 的裁剪偏移与缩放
val viewW = view.width; val viewH = view.height
val scale = min(viewW / bitmapW, viewH / bitmapH) // bitmap 尺寸
val offsetX = (viewW - bitmapW * scale) / 2f
val offsetY = (viewH - bitmapH * scale) / 2f
return RectF(
x0 * bitmapW * scale + offsetX,
y0 * bitmapH * scale + offsetY,
x1 * bitmapW * scale + offsetX,
y1 * bitmapH * scale + offsetY,
)
}
}
```
### 3.8 overlay 模块 — DetectionOverlay.kt
Compose Canvas 叠加层:
```kotlin
@Composable
fun DetectionOverlay(results: List<DetectionResult>, rotation: Int, modifier: Modifier) {
val mapper = remember { CoordinateMapper(view) }
Canvas(modifier = modifier) {
results.forEach { r ->
val rect = mapper.mapToView(r, rotation)
drawRect(
color = colorOf(r.label),
topLeft = Offset(rect.left, rect.top),
size = Size(rect.width(), rect.height()),
style = Stroke(6.dp.toPx()),
)
// 距离由 ViewModel 计算后写入 DetectionResult.distanceFloat?
val dist = r.distance?.let { " · 约${it}m" } ?: ""
drawText("${r.label} ${(r.score * 100).toInt()}%$dist")
}
}
}
```
防闪烁:ViewModel 中对结果做"目标停留 ≥ 0.5s 才显示、消失 2s 后移除"的平滑处理。
### 3.9 reminder 模块 — Reminder.kt
```kotlin
class Reminder(private val context: Context) {
private val vibrator = context.getSystemService(Vibrator::class.java)
private var lastAlertAt = 0L
private var lastAlertLabel: String? = null
/** 同类目标 10s 内只提醒一次 */
fun onDetected(label: String) {
val now = SystemClock.elapsedRealtime()
if (label == lastAlertLabel && now - lastAlertAt < 10_000) return
lastAlertAt = now
lastAlertLabel = label
vibrator?.vibrate(VibrationEffect.createOneShot(200, VibrationEffect.DEFAULT_AMPLITUDE))
}
}
```
### 3.10 设置存储 — DataStore
使用 DataStore Preferences 保存:`conf_threshold`(默认 0.40)、`detect_mode`(连续/标准/省电)、`vibrate_enabled``sound_enabled``low_light_enhance`P2)、`show_distance`(默认开)。`SettingsViewModel` 以 Flow 暴露,`FrameAnalyzer``TFLiteDetector` 读取最新值。
---
## 4. 关键流程实现
### 4.1 启动流程
```
MainActivity.onCreate
→ 检查/请求 CAMERA 权限
→ 初始化 TFLiteDetector(后台线程,含预热推理)
→ CameraController.start(previewView, analyzer)
→ ViewModel 订阅检测结果 → 叠加层渲染 + 提醒触发
```
预热:加载模型后执行一次空推理(全零输入),避免首帧卡顿。
### 4.2 帧节流与并发控制
- 检测间隔:连续 80~100ms / 标准 300ms / 省电 1000ms(由设置决定);
- 推理在单线程 ExecutorHandlerThread)串行执行,天然互斥;
- 分析线程只做"取帧 → 判断节流 → 提交任务",不做推理,保证预览流畅。
### 4.3 提醒、距离标注与防闪烁
- 检测到目标(置信度 ≥ 阈值,生境区域用独立阈值)→ `Reminder.onDetected(label)`,10s 防重复,动物与生境预警提醒方式区分;
- 叠加层展示逻辑:目标连续出现 ≥ 0.5s 才绘制;消失后 2s 内不清除,避免闪烁;
- 低置信度结果(阈值以下)不展示、不提醒;
- 距离标注:结果到达后调用 `DistanceEstimator` 估算"约 X m",随检测框渲染(FrameAnalyzer 回调携带分析图像高度);生境区域同样估算(按参考植被高度,误差较大);框高过小(< 8px)或无法获取焦距显示"--"。
---
## 5. 模型集成细节
| 项 | 说明 |
| --- | --- |
| 模型文件 | `app/src/main/assets/model.tflite`,随 APK 打包 |
| 标签文件 | `assets/labels.txt`,每行一个类别,顺序与训练一致(pheasant) |
| 量化 | 优先 fp16GPU);int8 用于 CPU/低端机回退 |
| 加载 | `Interpreter(loadAssetFile(...))`,进程内单例 |
| 预热 | 初始化后执行一次 dummy run |
| 校验 | 发布前用 Python 脚本(tflite-runtime)对测试集抽样验证模型输出布局与 App 解析一致 |
---
## 6. 性能优化实践
| 手段 | 说明 |
| --- | --- |
| GPU Delegate | 优先启用;`addDelegate` 抛异常或首帧异常时回退 CPU,并上报降级日志 |
| 输入缓冲复用 | ByteBuffer 复用,避免每次检测重新分配 |
| 帧节流 | 按模式降频,KEEP_ONLY_LATEST 不积压 |
| 位图复用 | ImageProxy → Bitmap 走共享缓冲;叠加层仅绘制检测框,不复制整帧 |
| 线程模型 | 检测线程单线程串行;UI 线程零推理 |
| 低分辨率检测 | 320 输入检测;远距离模式可切 416(V2) |
| 距离标注 | 纯算术运算开销可忽略;焦距 px 按相机缓存复用,避免重复查询 CameraCharacteristics |
| 生命周期 | 后台自动 `unbind` 相机释放资源,避免耗电 |
| 省电模式 | 降到 1 帧/秒 + 低分辨率 + 关闭提醒以外的动画 |
---
## 7. 异常与降级策略
| 异常场景 | 处理 |
| --- | --- |
| GPU 不可用 / 模型加载失败 | 回退 CPU 线程数调整;失败则提示"识别不可用"但不影响相机预览 |
| 相机初始化失败(个别机型) | 重试一次 → 失败提示并引导检查权限/重启 |
| 权限被拒 | 引导页说明用途,提供跳转系统设置 |
| 低内存 | 主动释放缓存位图,降低检测分辨率 |
| 推理超时(> 500ms) | 丢弃该帧结果,恢复下一帧,保证预览流畅 |
| 无法读取焦距 / 视场角(个别机型) | 距离显示"--",识别不受影响 |
---
## 8. 测试方案
### 8.1 单元测试(JUnit
- `NmsTest`:NMS 正确性(重叠/多类别/边界框);
- `CoordinateMapperTest`:四向旋转映射、FIT_CENTER 裁剪偏移;
- `ThresholdTest`:置信度阈值过滤逻辑;
- `ReminderTest`:10s 防重复提醒逻辑、动物与生境提醒方式区分;
- `DistanceEstimatorTest`:焦距换算、距离公式、过小目标不估算、生境区域按植被高度估算。
### 8.2 模型评估(Python 脚本,独立于 App)
- 对测试集计算 mAP@0.5、mAP@0.5:0.95、各类别 AP、负样本误检率;
- 输出混淆矩阵,分析检测精度。
### 8.3 仪器测试(androidTest
- 模拟帧注入:将测试图片经 `ImageProxy` 注入分析器,断言结果(不依赖真机取景);
- 权限拒绝 / 恢复场景;
- 冷启动到预览的时间基准测试。
### 8.4 真机测试矩阵
| 机型档位 | 代表机型 | 验证项 |
| --- | --- | --- |
| 旗舰 | 骁龙 8 系 / 麒麟 9 系 | 全功能、GPU 路径、长时间发热 |
| 中端 | 骁龙 7 系 / 天玑 8 系 | 延迟 ≤ 80ms、耗电 ≤ 15%/h |
| 低端 | 骁龙 4 系 / 天玑 6 系 | CPU 回退路径、省电模式可用性 |
---
## 9. 构建与发布
```kotlin
android {
compileSdk = 35
defaultConfig {
applicationId = "com.example.observer"
minSdk = 24
targetSdk = 35
ndk { abiFilters += listOf("arm64-v8a") } // 主发 arm64;如需兼容 32 位另发包
}
buildTypes {
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
}
```
- R8 规则:保留 TFLite 相关类(`-keep class org.tensorflow.** { *; }`),assets 模型不混淆;
- 签名:正式签名 + Gradle 管理(或环境变量注入),不上传 keystore 到仓库;
- 上架检查:隐私政策(仅相机权限、不采集与存储数据声明)、权限用途说明。
---
## 10. 后续演进(V2 方向)
- 更多物种类别(哺乳类、鸟类细分),支持模型远程更新;
- 小目标优化:SAHI 切图推理 / 专用小目标模型 / 数字变焦辅助;
- 夜视 / 红外增强模式;
- 检测结果语音播报,提升无障碍体验;
- 距离精度增强:基于设备俯仰角与相机高度的地面平面法、镜头畸变校正、水下折射修正;
- 生境类别细分与精度提升(湿地、林缘等),生境预警策略优化;
- 若未来需要留存能力(拍照、记录),可基于现有检测链路平滑扩展。