diff --git a/flutter_app/lib/camera/camera_screen.dart b/flutter_app/lib/camera/camera_screen.dart index c748692..67761e9 100644 --- a/flutter_app/lib/camera/camera_screen.dart +++ b/flutter_app/lib/camera/camera_screen.dart @@ -80,7 +80,7 @@ class _CameraScreenState extends State { const SizedBox(height: 8), const Text( '阈值越低识别越灵敏(低分框越多,误报也可能增加);' - '野鸡模型置信度普遍在 10%~20%,场景识别不到时可适当调低。', + '环颈雉鸡模型置信度普遍在 10%~20%,场景识别不到时可适当调低。', style: TextStyle(color: Colors.white54, fontSize: 12), ), ], diff --git a/flutter_app/lib/camera/camera_view_model.dart b/flutter_app/lib/camera/camera_view_model.dart index 94b57f2..7acba72 100644 --- a/flutter_app/lib/camera/camera_view_model.dart +++ b/flutter_app/lib/camera/camera_view_model.dart @@ -39,7 +39,7 @@ class CameraUiState { /// 检测结果置信度分级与轨迹确认。 /// -/// - [highConf](0.35):高于此分直接确认显示;真实野鸡多为 0.1~0.2, +/// - [highConf](0.35):高于此分直接确认显示;真实环颈雉鸡多为 0.1~0.2, /// 高于 0.35 视为强证据。 /// - 低于 0.35 的框:需要多帧稳定([confirmFrames] 帧)或 活动证据 /// (运动区域/背景新出现区域重叠)才确认显示。 @@ -105,7 +105,7 @@ class CameraViewModel extends ChangeNotifier { visible.add(r.copyWith(confirmed: t.confirmed)); } - // 提醒:仅新确认的野鸡轨迹(确认瞬间触发一次,10s 同类冷却在 Reminder 内) + // 提醒:仅新确认的环颈雉鸡轨迹(确认瞬间触发一次,10s 同类冷却在 Reminder 内) for (final t in _tracks.values) { if (t.label != 'pheasant' || !t.confirmed || t.reminded) continue; final age = now - t.firstSeenMs; @@ -152,7 +152,7 @@ class CameraViewModel extends ChangeNotifier { for (final t in _tracks.values) { if (matched.contains(t.id)) continue; final d = _centerDist(t.result, r); - // 同标签宽松匹配;跨标签(野鸡↔疑似 抖动)收紧到 60% + // 同标签宽松匹配;跨标签(环颈雉鸡↔疑似 抖动)收紧到 60% final limit = t.label == r.label ? bestD : associateRadius * 0.6; if (d < limit) { bestD = d; @@ -181,7 +181,7 @@ class CameraViewModel extends ChangeNotifier { /// 显示判定(按类别策略): /// - 疑似(生境预警):设计意图是常驻静态预警,始终显示(渲染侧弱化) - /// - 野鸡:确认轨迹直接显示;未确认的只有在高分或活动证据时才显示 + /// - 环颈雉鸡:确认轨迹直接显示;未确认的只有在高分或活动证据时才显示 bool _shouldDisplay(_Track t, List motionRegions, List noveltyRegions) { if (t.label == 'suspect') return true; @@ -196,7 +196,7 @@ class CameraViewModel extends ChangeNotifier { motionRegions.any((m) => MotionAggregator.centerInRegion(r, m)) || noveltyRegions.any((m) => MotionAggregator.centerInRegion(r, m)); - /// 物理合理性过滤:宽高比与相对尺寸(野鸡 20-100px@720 量级,参照标注脚本) + /// 物理合理性过滤:宽高比与相对尺寸(环颈雉鸡 20-100px@720 量级,参照标注脚本) bool _plausible(DetectionResult r) { final h = r.height; final w = r.width; diff --git a/flutter_app/lib/camera/detection_overlay.dart b/flutter_app/lib/camera/detection_overlay.dart index dec1e7c..c873657 100644 --- a/flutter_app/lib/camera/detection_overlay.dart +++ b/flutter_app/lib/camera/detection_overlay.dart @@ -6,8 +6,8 @@ import '../detection/coordinate_mapper.dart'; import '../detection/detection_result.dart'; /// 检测框绘制分级: -/// - 野鸡 confirmed:红色实线 3px(强证据) -/// - 野鸡 candidate:红色虚线 2px 半透明(待确认,弱提示) +/// - 环颈雉鸡 confirmed:红色实线 3px(强证据) +/// - 环颈雉鸡 candidate:红色虚线 2px 半透明(待确认,弱提示) /// - 疑似(生境预警):黄色虚线 2px 半透明(常驻静态预警,弱化渲染) /// 标签附带距离估计(针孔模型 焦距px×参考体型/框高px)。 class DetectionOverlay extends StatelessWidget { @@ -48,9 +48,9 @@ class _OverlayPainter extends CustomPainter { 'pheasant': Color(0xFFE53935), 'suspect': Color(0xFFFDD835), }; - static const _labels = {'pheasant': '野鸡', 'suspect': '疑似'}; + static const _labels = {'pheasant': '环颈雉鸡', 'suspect': '疑似'}; - /// 参考体型(米):野鸡身高 / 植被高度(参照旧 DistanceEstimator) + /// 参考体型(米):环颈雉鸡身高 / 植被高度(参照旧 DistanceEstimator) static const _refSizeM = {'pheasant': 0.45, 'suspect': 0.50}; /// iPhone 13 主摄在 1280 高预览下的估算焦距 px(5.1mm / 5.30mm 传感器), diff --git a/flutter_app/lib/detection/background_model.dart b/flutter_app/lib/detection/background_model.dart index 1f58648..d3964ae 100644 --- a/flutter_app/lib/detection/background_model.dart +++ b/flutter_app/lib/detection/background_model.dart @@ -8,7 +8,7 @@ import 'motion_aggregator.dart'; /// 分块聚合为新颖区域(novelty)。 /// /// 固定机位下,常驻物体(键盘/石头/文字)永远属于背景、不产生新颖区域; -/// 走进画面的目标(野鸡移动/新出现)才会触发。比相邻帧差分更强的证据: +/// 走进画面的目标(环颈雉鸡移动/新出现)才会触发。比相邻帧差分更强的证据: /// 风吹草动是持续的背景更新,不会长期标记为新颖。 class BackgroundModel { final int maxWidth; diff --git a/flutter_app/lib/detection/detector_worker.dart b/flutter_app/lib/detection/detector_worker.dart index b3612e9..05c0463 100644 --- a/flutter_app/lib/detection/detector_worker.dart +++ b/flutter_app/lib/detection/detector_worker.dart @@ -389,7 +389,7 @@ Future _workerMain(SendPort mainPort) async { results.addAll(dets); } results = mergeAcrossModels(results, TfliteDetector.iouThreshold); - // 低分野鸡框过视觉先验(颜色/位置),减少户外误报 + // 低分环颈雉鸡框过视觉先验(颜色/位置),减少户外误报 results = VisualPrior.filter( results, planes: planes, @@ -480,7 +480,7 @@ Future _workerMain(SendPort mainPort) async { } /// 多模型结果合并:按类别分组,组内 NMS(不同模型检出同一目标时取高分)。 -/// 各模型类别体系独立(如野鸡/疑似 vs 野兔/疑似),不同类别互不压制。 +/// 各模型类别体系独立(如环颈雉鸡/疑似 vs 野兔/疑似),不同类别互不压制。 List mergeAcrossModels( List all, double iouThreshold) { if (all.length <= 1) return all; diff --git a/flutter_app/lib/detection/tflite_detector.dart b/flutter_app/lib/detection/tflite_detector.dart index 12b44e7..06fbe1f 100644 --- a/flutter_app/lib/detection/tflite_detector.dart +++ b/flutter_app/lib/detection/tflite_detector.dart @@ -14,7 +14,7 @@ class TfliteDetector { // 输入尺寸取自模型本身(ultralytics litert 导出 NCHW [1,3,H,W],各数据集 // 训练 imgsz 可不同),默认 704 兜底 static const int defaultInputSize = 704; - // 野鸡数据置信度普遍偏低(0.1~0.2 量级),保留低分池供运动检测提升; + // 环颈雉鸡数据置信度普遍偏低(0.1~0.2 量级),保留低分池供运动检测提升; // 可运行时调整(设置页滑块),默认 0.10 double minScore = 0.10; static const double iouThreshold = 0.45; diff --git a/flutter_app/lib/detection/visual_prior.dart b/flutter_app/lib/detection/visual_prior.dart index ad33516..ee8a3d8 100644 --- a/flutter_app/lib/detection/visual_prior.dart +++ b/flutter_app/lib/detection/visual_prior.dart @@ -3,14 +3,14 @@ import 'dart:typed_data'; import 'detection_result.dart'; -/// 运行时视觉先验:对低置信度野鸡框做多线索过滤,降低户外误报。 +/// 运行时视觉先验:对低置信度环颈雉鸡框做多线索过滤,降低户外误报。 /// /// 仅对 score < [maxScore](0.35)的 pheasant 框生效;高分框与 /// suspect(生境预警)不参与过滤,避免误杀。 /// /// 线索: /// - 颜色:绿色主导(草/叶)、蓝色主导(天空/水)、平坦低饱和(键盘/石头/文字) -/// - 位置:中心在画面上部 15%(天空区)——野鸡是地栖动物,不会出现在天空 +/// - 位置:中心在画面上部 15%(天空区)——环颈雉鸡是地栖动物,不会出现在天空 /// /// 采样在原始 planes 上进行(后台 isolate 内,不依赖 UI 线程)。 class VisualPrior { @@ -85,7 +85,7 @@ class VisualPrior { /// 读取单像素 RGB(0~255)。 /// 8888 单平面按实际字节序取通道:BGRA=[b,g,r,a](iOS 插件)、 /// RGBA=[r,g,b,a](Android 自写原生通道)——字节序写死会让 Android - /// 低分框采样到 R/B 互换的颜色(橙色野鸡身被误判成"蓝色")整批误杀; + /// 低分框采样到 R/B 互换的颜色(橙色环颈雉鸡身被误判成"蓝色")整批误杀; /// YUV:y 平面 + 4:2:0 半分辨率 U/V(NV12 交错或 I420 分离)。 static (double, double, double) _pixel(List planes, List strides, int x, int y, int width, int height, bool isBgra, diff --git a/flutter_app/pubspec.yaml b/flutter_app/pubspec.yaml index 99093ad..997a360 100644 --- a/flutter_app/pubspec.yaml +++ b/flutter_app/pubspec.yaml @@ -1,5 +1,5 @@ name: observer -description: "视野 - 动物实时识别 (野鸡/生境), YOLOv8 + 充值付费" +description: "视野 - 动物实时识别 (环颈雉鸡/生境), YOLOv8 + 充值付费" publish_to: 'none' version: 1.0.4+5 diff --git a/flutter_app/test/model_manager_test.dart b/flutter_app/test/model_manager_test.dart index 71d8a8b..7c2e9c0 100644 --- a/flutter_app/test/model_manager_test.dart +++ b/flutter_app/test/model_manager_test.dart @@ -16,12 +16,12 @@ Map _catalog(List> models) => Map _item({String version = 'v1.0.0', String sha = ''}) => { 'datasetId': 7, - 'datasetName': '野鸡数据集', + 'datasetName': '环颈雉鸡数据集', 'version': version, 'labels': ['pheasant', 'suspect'], 'sizeBytes': _modelBytes.length, 'sha256': sha.isEmpty ? _shaHex(_modelBytes) : sha, - 'downloadUrl': '/download/models/野鸡数据集/latest.tflite', + 'downloadUrl': '/download/models/环颈雉鸡数据集/latest.tflite', }; void main() { @@ -60,7 +60,7 @@ void main() { expect(m.ready, isTrue); expect(m.error, isNull); expect(m.models.length, 1); - expect(m.models.first.datasetName, '野鸡数据集'); + expect(m.models.first.datasetName, '环颈雉鸡数据集'); expect(m.models.first.bytes, _modelBytes); expect(downloadHits, 1); @@ -112,7 +112,7 @@ void main() { expect(m2.models.length, 0, reason: '校验失败的模型不应加载'); expect(m2.error, isNotNull); - expect(m2.error, contains('野鸡数据集')); + expect(m2.error, contains('环颈雉鸡数据集')); }); test('目录下线:清理本地并清空模型', () async { diff --git a/flutter_app/test/model_merge_test.dart b/flutter_app/test/model_merge_test.dart index 1edc3c5..43d9111 100644 --- a/flutter_app/test/model_merge_test.dart +++ b/flutter_app/test/model_merge_test.dart @@ -17,9 +17,9 @@ DetectionResult box(String label, double score, double x, double y, void main() { test('不同模型同标签重复框:NMS 去重取高分', () { - // 野鸡模型与野兔模型都检出了同一只"野鸡"(不同模型对同一目标的重复框) + // 环颈雉鸡模型与野兔模型都检出了同一只"环颈雉鸡"(不同模型对同一目标的重复框) final all = [ - box('pheasant', 0.18, 0.3, 0.3, modelId: 1, modelName: '野鸡模型'), + box('pheasant', 0.18, 0.3, 0.3, modelId: 1, modelName: '环颈雉鸡模型'), box('pheasant', 0.55, 0.31, 0.3, modelId: 2, modelName: '野兔模型'), ]; final merged = mergeAcrossModels(all, 0.45); @@ -39,8 +39,8 @@ void main() { test('同模型内部与跨模型合并一致:远处不重叠保留', () { final all = [ - box('pheasant', 0.2, 0.1, 0.1, modelId: 1, modelName: '野鸡模型'), - box('pheasant', 0.3, 0.8, 0.8, modelId: 1, modelName: '野鸡模型'), + box('pheasant', 0.2, 0.1, 0.1, modelId: 1, modelName: '环颈雉鸡模型'), + box('pheasant', 0.3, 0.8, 0.8, modelId: 1, modelName: '环颈雉鸡模型'), ]; final merged = mergeAcrossModels(all, 0.45); expect(merged.length, 2); diff --git a/server/biz/dao/dataset_image.go b/server/biz/dao/dataset_image.go index 99f83cb..6d743ad 100644 --- a/server/biz/dao/dataset_image.go +++ b/server/biz/dao/dataset_image.go @@ -63,11 +63,14 @@ func (d *datasetImageDao) GetById(ctx context.Context, id int64) (*entity.Datase return one, nil } -// ListByDataset 某数据集全部图片(文件名倒序,详情页逐行展示用) +// ListByDataset 某数据集全部图片(按文件名编号倒序,详情页逐行展示用)。 +// 文件名形如 <前缀>_<编号>.jpg(无前缀为时间戳数字名),须提取下划线后的数字按数值倒序: +// 字符串序会把 pheasant_101.jpg 排在 pheasant_2.jpg 之前('1'<'9'),导致最大编号落到底部 func (d *datasetImageDao) ListByDataset(ctx context.Context, datasetId int64) ([]*entity.DatasetImage, error) { var list []*entity.DatasetImage err := g.DB().Model(consts.TableDatasetImage).Ctx(ctx). - Where("dataset_id", datasetId).OrderDesc("filename").Scan(&list) + Where("dataset_id", datasetId). + Order("CAST(substr(filename, instr(filename, '_') + 1) AS INTEGER) DESC").Scan(&list) if err != nil { if common.IsNoRows(err) { return []*entity.DatasetImage{}, nil diff --git a/server/biz/service/dataset.go b/server/biz/service/dataset.go index e677a45..a379882 100644 --- a/server/biz/service/dataset.go +++ b/server/biz/service/dataset.go @@ -609,6 +609,8 @@ func (s *datasetService) buildPromptFromTemplate(ctx context.Context, datasetNam scene := pickCfgByDataset(ctx, datasetName, "imageGen.sceneByDataset", "imageGen.scenes") action := pickCfgByDataset(ctx, datasetName, "imageGen.actionByDataset", "imageGen.actions") light := pickCfg(ctx, "imageGen.lights") + // 轮廓色词按物种(白化个体如白雉鸡配浅色,默认深色剪影) + tone := g.Cfg().MustGet(ctx, "imageGen.speciesTone."+species, "深色").String() // 距离描述从表单固定值生成(25 → "25米外") distWord := fmt.Sprintf("%d米外", dist) // 尺寸提示:按物理公式把表单距离换算成目标在图中的像素高与画面占比 @@ -622,6 +624,7 @@ func (s *datasetService) buildPromptFromTemplate(ctx context.Context, datasetNam for k, v := range map[string]string{ "{scene}": scene, "{species}": species, "{count}": countWord, "{action}": action, "{light}": light, "{distanceWord}": distWord, "{sizeHint}": sizeHint, + "{tone}": tone, } { out = strings.ReplaceAll(out, k, v) } @@ -722,7 +725,7 @@ func (s *datasetService) AdminGenerateImages(ctx context.Context, req *dto.Admin return provider.Generate(genCtx, prompt, req.Size) } // 命名:数据集属性 name_prefix(创建/编辑表单维护)非空时按 <前缀>_<两位序号>.jpg 顺序编号 - // (延续数据集内已有最大序号,参考野鸡目录 pheasant_NN.jpg 惯例);无前缀按时间戳命名。 + // (延续数据集内已有最大序号,参考既有图片目录的连续编号惯例);无前缀按时间戳命名。 // 注意:gtime.Time.Format 是 PHP 风格格式符,Go 数字布局须用 .Time.Format,否则原样透传 prefix := dataset.NamePrefix nextIdx := 1 diff --git a/server/config.yml b/server/config.yml index fe582a5..0f262ce 100644 --- a/server/config.yml +++ b/server/config.yml @@ -40,16 +40,20 @@ imageGen: timeoutSeconds: 600 # 每张生成超时(默认 120;z-image-turbo 首次加载含模型入显存,稳态秒级~分钟级) # 通用提示词模板(管理端填「动物名称」时启用,每张随机取池组装):{species} 优先用表单填写的名称, # 未填则按数据集名取 speciesByDataset 池(再未配置直接用数据集名);{count} 为数量词(固定 "1只",每张仅 1 个目标;原表单 - # 数量选择已取消);{scene}/{action}/{light} 池内随机;物种只写名字不写羽毛细节 - # (细节会把镜头拉近,详见技术设计)。不带拍摄角度描述(2026-08-28):角度描述会与目标位置耦合, - # 仰拍使贴地目标沉底、固定俯拍构图单一——交由模型平视先验自由构图,目标自然落在中部/上部 - promptTemplate: "野外大远景实拍:广角镜头下{scene},{distanceWord}有且仅有{count}{species}的小小身影{sizeHint}在{action},{species}离镜头很远,只是很小的深色轮廓,隐约可辨,画面绝大部分是{scene},没有任何靠近镜头的动物,{light},写实野外摄影,真实照片质感" + # 数量选择已取消);{scene}/{action}/{light} 池内随机;{tone} 为物种轮廓色词(默认深色,白化个体配浅色); + # 物种只写名字不写羽毛细节(细节会把镜头拉近,详见技术设计)。不带拍摄角度描述(2026-08-28): + # 角度描述会与目标位置耦合,仰拍使贴地目标沉底、固定俯拍构图单一——交由模型平视先验自由构图, + # 目标自然落在中部/上部 + promptTemplate: "野外大远景实拍:广角镜头下{scene},{distanceWord}有且仅有{count}{species}的小小身影{sizeHint}在{action},{species}离镜头很远,只是很小的{tone}轮廓,隐约可辨,画面绝大部分是{scene},没有任何靠近镜头的动物,{light},写实野外摄影,真实照片质感" speciesByDataset: # 数据集名 → 物种池(表单未填动物名称时随机取一个) 鸽子: [家鸽,火斑鸠,虎纹斑鸠,珠颈斑鸠,灰斑鸠] - 雉鸡: [雉鸡] + 雉鸡: [雉鸡,白雉鸡] 兔子: [兔子] 鸭子: [野鸭] 鹌鹑: [鹌鹑] + speciesTone: # 物种轮廓色词(模板 {tone} 占位,未配置默认深色):远景小目标深色剪影易辨, + # 白化个体(白雉鸡)用「深色轮廓」描述与白色羽毛矛盾,须配浅色 + 白雉鸡: 浅色 scenes: # 环境场景池(统一远景原则:每条含开阔/延伸向远方/地平线元素, # 把地平线推开、消除近拍暗示;同一短语在模板中出现两次,须两处都读得通) - "一望无际的玉米地,玉米秆两米多高,田间土路和田埂延伸向地平线" @@ -70,6 +74,7 @@ imageGen: - "田边茂密的草丛和矮灌丛连绵延伸到远方" - "荒草坡和灌木丛连成一片,一直铺向远处地平线" - "收割后的农田边缘杂草丛生,矮灌丛点缀延伸到远方" + - "稀疏灌木点缀的草坡,一直延伸到远处的林地边缘" 兔子: - "枯黄的草坡和荒草地一直延伸到远方" - "弃耕地和田埂长满野草,延伸向远处的林地" @@ -98,7 +103,8 @@ imageGen: 灰斑鸠: 18 火斑鸠: 15 虎纹斑鸠: 18 - 野鸡: 55 + 雉鸡: 55 + 白雉鸡: 55 兔子: 35 野鸭: 35 鹌鹑: 12 diff --git a/server/data/observer.db b/server/data/observer.db index 9959096..23120ff 100644 Binary files a/server/data/observer.db and b/server/data/observer.db differ diff --git a/server/workspace/datasets/雉鸡/pheasant_101.jpg b/server/workspace/datasets/雉鸡/pheasant_101.jpg new file mode 100644 index 0000000..d3d4f10 Binary files /dev/null and b/server/workspace/datasets/雉鸡/pheasant_101.jpg differ diff --git a/server/workspace/datasets/雉鸡/pheasant_102.jpg b/server/workspace/datasets/雉鸡/pheasant_102.jpg new file mode 100644 index 0000000..9cf81b1 Binary files /dev/null and b/server/workspace/datasets/雉鸡/pheasant_102.jpg differ diff --git a/server/workspace/datasets/雉鸡/pheasant_106.jpg b/server/workspace/datasets/雉鸡/pheasant_106.jpg new file mode 100644 index 0000000..6500a81 Binary files /dev/null and b/server/workspace/datasets/雉鸡/pheasant_106.jpg differ diff --git a/server/workspace/datasets/雉鸡/pheasant_107.jpg b/server/workspace/datasets/雉鸡/pheasant_107.jpg new file mode 100644 index 0000000..5deaad2 Binary files /dev/null and b/server/workspace/datasets/雉鸡/pheasant_107.jpg differ diff --git a/server/workspace/datasets/雉鸡/pheasant_108.jpg b/server/workspace/datasets/雉鸡/pheasant_108.jpg new file mode 100644 index 0000000..86b13ed Binary files /dev/null and b/server/workspace/datasets/雉鸡/pheasant_108.jpg differ diff --git a/server/workspace/datasets/雉鸡/pheasant_109.jpg b/server/workspace/datasets/雉鸡/pheasant_109.jpg new file mode 100644 index 0000000..5a1b378 Binary files /dev/null and b/server/workspace/datasets/雉鸡/pheasant_109.jpg differ diff --git a/server/技术设计.md b/server/技术设计.md index df4bc2c..160139c 100644 --- a/server/技术设计.md +++ b/server/技术设计.md @@ -332,7 +332,7 @@ Android 客户端启动 GET /api/v1/app/update(公开,无需 token;iOS 不 - 生成同步执行(count 1..8,每张超时 `imageGen.timeoutSeconds`,默认 120s;localai 建议 600),逐张落盘 + 入库(记录 prompt 便于追溯);中途失败返回错误(已成功的图保留,不删除——付费资产原则) - prompt 手填覆盖;留空走 `imageGen.promptTemplate` 通用模板逐张组装(物种池 speciesByDataset 按数据集名、数量词=animalCount、场景/动作/光线池随机,每张独立组装保持多样性;物种只写名字不写羽毛细节,细节会拉近镜头);**遵守项目提示词规范:不得包含目标位置描述**(位置由模型自行推理,前端模板与校验文案落实此约束) - **生成图距离/数量校验(2026-08-28)**:表单填 `distance`(米,固定值) 与 `animalCount` 后逐张启用;**距离校验已取消(2026-08-28)**:生成即入库,无逐张拒检;参数与公式见 git 历史(物理公式 d=体高×focalPx÷像素高) -- **VLM 藏匿位补检(两阶段第二阶段)**:`/datasets/images/vlm-review` 单图接口;qwen3.8-9b(+mmproj) 以「图片+物种(生成 prompt 与物种池匹配)+场景光线线索+已确认框坐标(排除)」推理藏匿位,输出归一化 bbox JSON(范围校验+与已有框重叠去重),追加 ≤3 个 class=1 疑似框进同一 labels_json;VL 与 z-image 显存互斥(12G 装不下两者),批量补检在生成队列空闲后执行;qwen3.8-9b yaml 自带 mmproj 实为多模态模型(生成校验);自动标注框数按置信度降序裁剪 ≤ animal_count(存 dataset_image.animal_count 列),估算距离 = 物种站高 × focalPx ÷ 最大框高像素(focalPx=`图高/2/tan(vfov/2)`,站高按物种查 `imageGen.speciesHeights`,单位 cm 换算为米,未配置物种代码兜底 35cm;**2026-08-28 语义修正**:speciesHeights 为站高(垂直尺度,鸟=脚到头顶、兽=蹲坐高),勿用体长——与提示词「高度」描述及检测框高自洽;避免跨物种共用一个标定系数)(K=1.8:6%≈30m、4.5%≈40m,K 隐含镜头视场角假设);**镜头固定主摄(2026-08-28 定案)**:vfov 固定 `imageGen.assumedVfovDeg=52°`(手机默认主摄;曾随机 [15,90] 覆盖面广但目标像素波动 7~57px 不可控,曾试超广角 90 亦否决);**场景/动作按数据集(2026-08-28)**:`imageGen.sceneByDataset`/`actionByDataset` 按数据集名配池(鸭→水面、雉鸡→灌丛草丛、兔→草坡荒地、鹌鹑→草丛),未配置回退通用 scenes/actions;**拍摄角度(2026-08-28,已废弃角度描述)**:不带任何拍摄角度描述——演进:固定「上部三分之一」区域描述对文生图模型遵循力弱(生成物落底)→ 加固定俯拍(目标上移成功但构图单一,用户否决)→ 池内随机俯/平/仰(仰拍使贴地目标必然沉底,用户否决)→ 只留俯拍/平视(用户仍嫌啰嗦)→ **完全去掉角度描述**(2026-08-28 定案):交由模型平视先验自由构图,目标自然落在画面中部/上部;模板只靠「{species}离镜头很远,只是很小的深色轮廓」(物种名替代泛称「目标」,不限定鸟类——物种池含兔子等非鸟物种)维持远景感,手填提示词仅追加数量约束;不合格丢弃重生成,单张上限 3 次(`consts.GenValidateMaxAttempts`),连续不合格整体报错(已合格张数保留) +- **VLM 藏匿位补检(两阶段第二阶段)**:`/datasets/images/vlm-review` 单图接口;qwen3.8-9b(+mmproj) 以「图片+物种(生成 prompt 与物种池匹配)+场景光线线索+已确认框坐标(排除)」推理藏匿位,输出归一化 bbox JSON(范围校验+与已有框重叠去重),追加 ≤3 个 class=1 疑似框进同一 labels_json;VL 与 z-image 显存互斥(12G 装不下两者),批量补检在生成队列空闲后执行;qwen3.8-9b yaml 自带 mmproj 实为多模态模型(生成校验);自动标注框数按置信度降序裁剪 ≤ animal_count(存 dataset_image.animal_count 列),估算距离 = 物种站高 × focalPx ÷ 最大框高像素(focalPx=`图高/2/tan(vfov/2)`,站高按物种查 `imageGen.speciesHeights`,单位 cm 换算为米,未配置物种代码兜底 35cm;**2026-08-28 语义修正**:speciesHeights 为站高(垂直尺度,鸟=脚到头顶、兽=蹲坐高),勿用体长——与提示词「高度」描述及检测框高自洽;避免跨物种共用一个标定系数)(K=1.8:6%≈30m、4.5%≈40m,K 隐含镜头视场角假设);**镜头固定主摄(2026-08-28 定案)**:vfov 固定 `imageGen.assumedVfovDeg=52°`(手机默认主摄;曾随机 [15,90] 覆盖面广但目标像素波动 7~57px 不可控,曾试超广角 90 亦否决);**场景/动作按数据集(2026-08-28)**:`imageGen.sceneByDataset`/`actionByDataset` 按数据集名配池(鸭→水面、雉鸡→灌丛草丛、兔→草坡荒地、鹌鹑→草丛),未配置回退通用 scenes/actions;**拍摄角度(2026-08-28,已废弃角度描述)**:不带任何拍摄角度描述——演进:固定「上部三分之一」区域描述对文生图模型遵循力弱(生成物落底)→ 加固定俯拍(目标上移成功但构图单一,用户否决)→ 池内随机俯/平/仰(仰拍使贴地目标必然沉底,用户否决)→ 只留俯拍/平视(用户仍嫌啰嗦)→ **完全去掉角度描述**(2026-08-28 定案):交由模型平视先验自由构图,目标自然落在画面中部/上部;模板只靠「{species}离镜头很远,只是很小的{tone}轮廓」(物种名替代泛称「目标」,不限定鸟类——物种池含兔子等非鸟物种;`{tone}` 轮廓色词按 `imageGen.speciesTone.<物种>` 取,默认深色剪影,**白雉鸡配浅色**——白羽个体用「深色轮廓」描述会与模型渲染矛盾,2026-08-28)维持远景感,手填提示词仅追加数量约束;不合格丢弃重生成,单张上限 3 次(`consts.GenValidateMaxAttempts`),连续不合格整体报错(已合格张数保留) ### 训练通道(runner 抽象,决策:同机/异机不确定 → 可配置)