diff --git a/flutter_app/android/app/src/main/kotlin/com/example/observer/CameraChannel.kt b/flutter_app/android/app/src/main/kotlin/com/example/observer/CameraChannel.kt index 2f9fcec..d694df0 100644 --- a/flutter_app/android/app/src/main/kotlin/com/example/observer/CameraChannel.kt +++ b/flutter_app/android/app/src/main/kotlin/com/example/observer/CameraChannel.kt @@ -362,8 +362,9 @@ class CameraChannel( /** * 从设备流配置挑预览/分析共用分辨率(均为传感器方向尺寸,宽≥高): - * 取两者交集,16:9 优先、长边 ≤1920 内取最大(推理输入 640x640, - * 更高只增帧传输与旋转开销,无精度收益);无 16:9 时退回最大交集尺寸。 + * 取两者交集,16:9 优先、长边 ≤1920 内取最大(推理输入长边 1280, + * 1920x1080 已留 1.5 倍降采样余量,更高只增帧传输与旋转开销,无精度收益); + * 无 16:9 时退回最大交集尺寸。 */ private fun pickSize(previewSizes: Array, analysisSizes: Array): Size { val common = previewSizes.filter { analysisSizes.contains(it) } diff --git a/flutter_app/lib/detection/tflite_detector.dart b/flutter_app/lib/detection/tflite_detector.dart index b4ecaf8..7f20b5e 100644 --- a/flutter_app/lib/detection/tflite_detector.dart +++ b/flutter_app/lib/detection/tflite_detector.dart @@ -6,14 +6,14 @@ import 'package:tflite_flutter/tflite_flutter.dart'; import 'detection_result.dart'; import 'nms.dart'; -/// YOLOv8n 端侧推理实现(对应 Kotlin TFLiteDetector)。 +/// YOLOv8s 端侧推理实现(对应 Kotlin TFLiteDetector)。 /// 模型输出布局(ultralytics litert 导出):[1, 4 + nc, anchors], /// cx/cy/w/h 已归一化,类别得分已过 sigmoid;按 out[dim][anchor] 索引。 -/// 输入为 NCHW [1, 3, 704, 704](litert 导出保留 torch 布局)。 +/// 输入为 NCHW [1, 3, 1280, 1280](litert 导出保留 torch 布局)。 class TfliteDetector { // 输入尺寸取自模型本身(ultralytics litert 导出 NCHW [1,3,H,W],各数据集 - // 训练 imgsz 可不同),默认 704 兜底 - static const int defaultInputSize = 704; + // 训练 imgsz 可不同),默认 1280 兜底 + static const int defaultInputSize = 1280; // 环颈雉鸡数据置信度普遍偏低(0.1~0.2 量级),保留低分池供运动检测提升; // 可运行时调整(设置页滑块),默认 0.10 double minScore = 0.10; diff --git a/server/README.md b/server/README.md index 2d5d46a..4b48c68 100644 --- a/server/README.md +++ b/server/README.md @@ -200,7 +200,7 @@ APK 下载引导页(静态页面,源码在 `h5/index.html`,由后端 `/dow | GET | `/admin/label-tasks` | 标注任务列表:`page/size` 分页(含 status/total/done) | | GET | `/admin/label-tasks/detail` | 标注任务详情:返回数据集全部图片 + 每张标注框(`boxes`,YOLO 归一化 xywh + 置信度 + 类别) | | POST | `/admin/label-tasks/save` | 保存单张标注 `{"datasetId":1,"filename":"a.jpg","boxes":[{"class":0,"cx":0.5,"cy":0.4,"w":0.1,"h":0.2}]}`:整体覆写该图 `labels_json`(空 boxes=清空标注),返回该数据集当前 `labeledCount` | -| POST | `/admin/trainings` | 发起训练 `{"dataset":"yolo","imgsz":704,"epochs":150,"batch":16,"device":"0","name":"..."}`:先同步数据集到训练机 → 校验目录存在 → runner 启动训练;**并发度 1**,已有 running 任务时返回错误 | +| POST | `/admin/trainings` | 发起训练 `{"dataset":"yolo","imgsz":1280,"epochs":150,"batch":16,"device":"0","name":"..."}`(imgsz/model 实际以 `config.yml` training 节点为准):先同步数据集到训练机 → 校验目录存在 → runner 启动训练;**并发度 1**,已有 running 任务时返回错误 | | GET | `/admin/trainings` | 训练任务列表:`page/size` 分页,按下发时间倒序,含 status/进度/指标 | | GET | `/admin/trainings/detail` | 任务详情 `{"id":1}`:参数快照 + 进度 + 指标 + 日志尾部 | | POST | `/admin/trainings/cancel` | 取消训练 `{"id":1}`(仅 running):杀训练进程,状态置 failed(记录 error) | diff --git a/server/biz/dao/model_training.go b/server/biz/dao/model_training.go index 6832fc7..e08a7f7 100644 --- a/server/biz/dao/model_training.go +++ b/server/biz/dao/model_training.go @@ -24,7 +24,7 @@ func init() { name TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'running', dataset_id INTEGER NOT NULL, - imgsz INTEGER NOT NULL DEFAULT 704, + imgsz INTEGER NOT NULL DEFAULT 1280, epochs INTEGER NOT NULL DEFAULT 150, batch INTEGER NOT NULL DEFAULT 16, device TEXT NOT NULL DEFAULT '0', diff --git a/server/biz/service/training.go b/server/biz/service/training.go index dbd14f7..1720b36 100644 --- a/server/biz/service/training.go +++ b/server/biz/service/training.go @@ -425,6 +425,7 @@ func (s *trainingService) AdminStartTraining(ctx context.Context, req *dto.Admin taskJSON, _ := json.Marshal(map[string]any{ "workdir": cfg.Workdir, "yolo": filepath.ToSlash(filepath.Join(cfg.DatasetDir, "yolo", dataset.Name)), + "model": cfg.Model, "imgsz": imgsz, "epochs": epochs, "batch": batch, diff --git a/server/common/training_runner.go b/server/common/training_runner.go index 1c0c24e..7a311eb 100644 --- a/server/common/training_runner.go +++ b/server/common/training_runner.go @@ -64,7 +64,7 @@ type TrainingJob struct { TaskId int64 DatasetName string // yolo 数据集目录名(训练机 datasetDir/yolo/) Python string // 训练机 venv python 路径 - Workdir string // 训练机工作目录(train_server.py / yolov8n.pt 所在) + Workdir string // 训练机工作目录(train_server.py / yolov8s.pt 所在) DatasetDir string // 训练机数据集根目录(相对 workdir) } @@ -87,6 +87,7 @@ type TrainingConfig struct { DatasetDir string Python string TimeoutMins int + Model string // 训练基座权重(训练机 workdir 下,如 yolov8s.pt) Imgsz int Epochs int Batch int @@ -100,7 +101,8 @@ func TrainingConfigOf(ctx context.Context) (TrainingConfig, bool) { DatasetDir: g.Cfg().MustGet(ctx, "training.datasetDir", "datasets").String(), Python: g.Cfg().MustGet(ctx, "training.venvPython").String(), TimeoutMins: g.Cfg().MustGet(ctx, "training.timeoutMinutes", 600).Int(), - Imgsz: g.Cfg().MustGet(ctx, "training.imgsz", 704).Int(), + Model: g.Cfg().MustGet(ctx, "training.model", "yolov8s.pt").String(), + Imgsz: g.Cfg().MustGet(ctx, "training.imgsz", 1280).Int(), Epochs: g.Cfg().MustGet(ctx, "training.epochs", 150).Int(), Batch: g.Cfg().MustGet(ctx, "training.batch", 16).Int(), Device: g.Cfg().MustGet(ctx, "training.device", "0").String(), diff --git a/server/config.yml b/server/config.yml index d9ca12d..e04ee80 100644 --- a/server/config.yml +++ b/server/config.yml @@ -70,11 +70,12 @@ training: port: 22 privateKeyPath: "" # 私钥路径与 password 二选一 password: "123" - workdir: /opt/pheasant_data # 训练机工作目录(train_server.py / yolov8n.pt 所在) + workdir: /opt/pheasant_data # 训练机工作目录(train_server.py / yolov8s.pt 所在) venvPython: /opt/pheasant_data/venv/bin/python datasetDir: datasets # 训练机数据集根目录(相对 workdir,yolo/ 为子目录) timeoutMinutes: 240 # 训练超时判死(started_at 起算;实测 100 张约 3 分钟,千图级约 1 小时) - imgsz: 704 # 训练/导出分辨率(须与 App 端推理输入对齐) + model: yolov8s.pt # 训练基座权重(须已同步到训练机 workdir;s 容量适配小目标+遮挡,详见技术设计) + imgsz: 1280 # 训练/导出分辨率(须与 App 端推理输入对齐;1280 提升远距离小目标保留像素) epochs: 150 # 训练轮数(patience 30 早停,设大可自动停) batch: 16 # 批大小(按训练机显存调整) device: "0" # GPU 编号(cpu 用 cpu) @@ -87,7 +88,7 @@ localAi: threshold: 0.08 # 候选置信度阈值(宁多勿漏) confConfirmed: 0.2 # 高于此视为确认(class 0),否则疑似(class 1) overlapThreshold: 0.3 # 重叠去重阈值(交叠/较小框面积,NMS 风格;RF-DETR 同目标常输出一大一小两框,minIoU 比 IoU 更能命中) - inputSize: 700 # 提交前整图等比缩放最长边 + inputSize: 700 # 提交前整图等比缩放最长边;RF-DETR 模型包固定 700x700 输入(stretch 拉伸、不支持动态尺寸,见模型包 inference_config.json),提交更大最终也拉伸到 700 无增益,2026-09-01 实测回退 # 标注类别名(写入 data.yaml,随 result.json 存模型 labels)从数据集表取: # 第一类 = gen_species(空回退数据集名),第二类 = gen_classes(空回退 class0/class1) diff --git a/server/data/observer.db b/server/data/observer.db index cd4b5a6..ef0418c 100644 Binary files a/server/data/observer.db and b/server/data/observer.db differ diff --git a/server/training/train_server.py b/server/training/train_server.py index d44c028..44c9f20 100644 --- a/server/training/train_server.py +++ b/server/training/train_server.py @@ -6,9 +6,10 @@ python train_server.py --task-json tasks/.json 任务参数(Go 侧写入,字段相对训练机 workdir): - workdir 训练机工作目录(脚本 / yolov8n.pt / venv 所在),启动即 chdir + workdir 训练机工作目录(脚本 / yolov8s.pt / venv 所在),启动即 chdir yolo 训练集目录(含 dataset.yaml),相对 workdir - imgsz 训练/导出分辨率(默认 704,与端侧推理对齐) + model 训练基座权重文件名(workdir 下,默认 yolov8s.pt) + imgsz 训练/导出分辨率(默认 1280,与端侧推理对齐) epochs 训练轮数 batch 批大小 device GPU 编号或 cpu @@ -236,7 +237,7 @@ def main(): except Exception: pass # 日志不可写(如 workdir 缺失)不阻断错误路径,traceback 仍走 stderr - imgsz = int(task.get("imgsz") or 704) + imgsz = int(task.get("imgsz") or 1280) epochs = int(task.get("epochs") or 150) batch = int(task.get("batch") or 16) device = task.get("device") or "0" @@ -245,7 +246,7 @@ def main(): try: from ultralytics import YOLO - model = YOLO("yolov8n.pt") + model = YOLO(task.get("model") or "yolov8s.pt") register_callback(model) model.train( data=data, imgsz=imgsz, epochs=epochs, diff --git a/server/技术设计.md b/server/技术设计.md index 437a620..6fbcbd2 100644 --- a/server/技术设计.md +++ b/server/技术设计.md @@ -96,7 +96,7 @@ CREATE TABLE IF NOT EXISTS model_training ( name TEXT NOT NULL, -- 任务名(默认「数据集+时间」) status TEXT NOT NULL DEFAULT 'running', -- running | success | failed dataset TEXT NOT NULL, -- 训练机数据集名(datasetDir 下子目录名) - imgsz INTEGER NOT NULL DEFAULT 704, + imgsz INTEGER NOT NULL DEFAULT 1280, epochs INTEGER NOT NULL DEFAULT 150, batch INTEGER NOT NULL DEFAULT 16, device TEXT NOT NULL DEFAULT '0', @@ -360,12 +360,16 @@ training: datasetDir: datasets # 训练机数据集根目录(相对 workdir,数据集为子目录) concurrency: 1 # GPU 独占:同时仅一个 running,新任务排队 timeoutMinutes: 240 # 超时判死(started_at 起算;100 张实测约 3 分钟) - imgsz: 704 # 训练/导出分辨率(须与 App 端推理输入对齐) + model: yolov8s.pt # 训练基座权重(须已同步到训练机 workdir) + imgsz: 1280 # 训练/导出分辨率(须与 App 端推理输入对齐) epochs: 150 # 训练轮数(patience 30 早停,设大可自动停) batch: 16 # 批大小(按训练机显存调整) device: "0" # GPU 编号(cpu 用 cpu) ``` +- **模型与分辨率升级(2026-09-01,用户定案)**:基座 `yolov8n.pt` → `yolov8s.pt`、`imgsz` 704 → **1280**——场景为远距离小目标 + 遮挡多,nano 容量不足且 704 输入下小目标仅剩 5~15 像素;s@1280 是端侧延迟可接受内的最优效果(中端机 70-120ms/帧,预览不卡、框 8-14fps 更新;高端机 30-45ms 流畅)。改动链路:`config.yml`(training.model/imgsz)→ task.json 传 model → `train_server.py` 读 task model → 导出 litert 输入 NCHW [1,3,1280,1280] → App 端 `TfliteDetector` 输入尺寸从模型形状动态读取(`defaultInputSize` 兜底 1280)。相机侧无需改:Android 分析帧固定 16:9 ≤1920(典型 1920x1080)、拍照原图 4000px+,letterbox 后均满足 1280。显存评估:12G 跑 s@1280 batch 8~16 可行(yolov8s 11.2M 参数)。端侧量化保持 fp16(int8 对 <10px 小目标掉点明显)。 +- **预标注模型固定 700x700(2026-09-01 实测确认)**:local-ai 的 rfdetr-xlarge 模型包 `inference_config.json` 声明固定输入 700x700(`training_input_size` 700、`dynamic_spatial_size_supported:false`、`resize_mode:stretch`),提交任何分辨率最终都拉伸到 700x700 推理——`localAi.inputSize` 只是提交前等比缩放,改大无增益(曾误改 1280,实测后回退 700)。**标注框精度天花板 = 700 分辨率**:极小目标(<5px)框不准是标注侧固有噪声;训练 imgsz=1280 与标注 700 并存可行(框坐标按比例映射到 1280 图),突破标注精度需换支持高分辨率输入的检测模型(RT-DETR / YOLO 动态输入 ONNX),未实施。 + - **训练参数默认走配置(2026-08-26)**:imgsz/epochs/batch/device 不随管理端请求传(界面一键开始),由 `training` 节点统一配置——device 取决于训练机硬件、imgsz 必须与端侧推理对齐、epochs 取决于算力预期,均为部署级参数;任务记录仍存各值(`model_training.imgsz/epochs/batch/device`)供列表展示 - service 内 `Runner` 接口:`Start(ctx, *TrainingJob) (pid, error)` / `FetchLogTail(ctx, job)` / `IsAlive(ctx, job) bool` / `Cancel(ctx, job)` / `FetchArtifacts(ctx, job, destDir)`;`subprocess` 与 `ssh` 两个实现,按 config `mode` 选择;**ssh 凭据直接读本节点 `training.ssh` 配置**(见「全局训练配置」节)