diff --git a/server/biz/service/dataset.go b/server/biz/service/dataset.go index 9cdc511..b06c342 100644 --- a/server/biz/service/dataset.go +++ b/server/biz/service/dataset.go @@ -1027,7 +1027,8 @@ func (s *datasetService) buildPromptFromTemplate(ctx context.Context, dataset *e return pool } scene, action, occlusion := "", "", "" - if bundle := pickSceneEntry(parseScenePool(dataset.GenScenes)); bundle != nil { + bundle := pickSceneEntry(parseScenePool(dataset.GenScenes)) + if bundle != nil { scene = bundle.Scene if len(bundle.Actions) > 0 { action = pickCfgList(bundle.Actions) @@ -1053,6 +1054,12 @@ func (s *datasetService) buildPromptFromTemplate(ctx context.Context, dataset *e } // 距离描述从表单固定值生成(25 → "25米外") distWord := fmt.Sprintf("%d米外", dist) + // 位置锚 {position}:默认地平线锚(地面/水面场景,目标在地平线附近语义成立); + // 捆绑组 position 覆盖(树栖场景目标须锚到枝头/树冠,否则「地平线附近有且仅有斑鸠」仍落在地面) + position := "在" + distWord + "的地平线附近" + if bundle != nil && strings.TrimSpace(bundle.Position) != "" { + position = strings.ReplaceAll(bundle.Position, "{distanceWord}", distWord) + } // 尺寸提示:按物理公式把表单距离换算成目标在图中的像素高与画面占比 sizeHint, pctVal, sErr := s.buildSizeHint(ctx, dataset, dist, size) if sErr != nil { @@ -1079,6 +1086,7 @@ func (s *datasetService) buildPromptFromTemplate(ctx context.Context, dataset *e "{scene}": scene, "{species}": sexWord + species, "{count}": countWord, "{action}": action, "{occlusion}": occlusion, "{light}": light, "{distanceWord}": distWord, "{sizeHint}": sizeHint, "{tone}": tone, + "{position}": position, } { out = strings.ReplaceAll(out, k, v) } @@ -1162,13 +1170,16 @@ func pickCfgList(pool []string) string { } // genSceneEntry 场景池条目:字符串条目解析为仅含 Scene 的本结构(weight=1,动作/遮挡走全局池); -// 捆绑组对象 {scene, weight, actions, occlusions} 的动作/遮挡与场景联动(2026-09-04, -// 斑鸠树栖 80%——三池独立随机会拼出「树上场景+地面动作/草丛遮挡」矛盾提示词,见技术设计.md) +// 捆绑组对象 {scene, weight, actions, occlusions, position} 的动作/遮挡与场景联动(2026-09-04, +// 斑鸠树栖 80%——三池独立随机会拼出「树上场景+地面动作/草丛遮挡」矛盾提示词,见技术设计.md); +// position 覆盖默认位置锚「在N米外的地平线附近」(树栖目标锚地平线=语义落地,须锚到枝头, +// 内可用 {distanceWord} 占位) type genSceneEntry struct { Scene string `json:"scene"` Weight int `json:"weight"` Actions []string `json:"actions"` Occlusions []string `json:"occlusions"` + Position string `json:"position"` } // parseScenePool 解析场景池 JSON 数组:条目兼容字符串与捆绑组对象,非法条目跳过 diff --git a/server/config.yml b/server/config.yml index 5871609..94fd3a2 100644 --- a/server/config.yml +++ b/server/config.yml @@ -41,19 +41,21 @@ imageGen: # 通用提示词模板:{species} 取数据集 gen_species(单值,空回退数据集名);{count} 为数量词 # (固定 "1只",每张仅 1 个目标;原表单数量选择已取消);{scene}/{action}/{light} 池内随机 # (scene/action/occlusion 从数据集表 gen_scenes/gen_actions/gen_occlusions 池随机,无 config 兜底; - # gen_scenes 条目兼容捆绑组对象 {scene,weight,actions,occlusions}——选中时动作/遮挡组内联动, + # gen_scenes 条目兼容捆绑组对象 {scene,weight,actions,occlusions,position}——选中时动作/遮挡组内联动, + # position 覆盖默认位置锚(树栖场景须把目标锚到枝头,默认地平线锚只适合地面/水面), # 见技术设计.md「场景捆绑组+权重」,斑鸠 80% 树上场景即此机制); # {tone} 取数据集表 gen_tone(默认深色,白化个体配浅色);{sizeHint} 按数据集表 gen_heights 站高与 # 表单距离算出像素高,按画面占比分级尺寸词(像素数与固定物体类比词均不通用,2026-08-31)+ 占比。物种只写名字 # 不写羽毛细节(细节会把镜头拉近,详见技术设计)。引导语「手机原相机主摄不变焦」(2026-08-31 v4 定案): # 与 13px 像素数学完全自洽(公式即按主摄 vfov 52° 计算);手机拍远处目标天然显小(先验同向); # 顺带对齐推理端域(App 就是手机拍摄)。演进:广角(前景大主体先验)→ 长焦(物理放大)→ 全删 → 手机主摄不变焦。 - # 拍摄角度描述(仰/俯/平)仍不带;位置锚定「在{distanceWord}的地平线附近」(曾因"上部1/3"遵循力弱去掉位置描述,改地平线锚点重试); + # 拍摄角度描述(仰/俯/平)仍不带;位置锚 {position}(默认「在{distanceWord}的地平线附近」,地平线=远景场景锚点, + # 适合地面/水面;捆绑组 position 可覆盖为枝头/树冠锚——2026-09-04,树栖目标锚在地平线上语义落地); # 动作/遮挡在普通模板中位于目标之后(动作=叙事焦点=视觉焦点,降级处理) - promptTemplate: "野外大远景实拍:手机原相机主摄不变焦,{scene},在{distanceWord}的地平线附近有且仅有{count}{species}{sizeHint},{occlusion},{species}在{action},只是很小的{tone}轮廓,隐约可辨,画面绝大部分是{scene},没有任何靠近镜头的动物,{light},写实野外摄影,真实照片质感" + promptTemplate: "野外大远景实拍:手机原相机主摄不变焦,{scene},{position}有且仅有{count}{species}{sizeHint},{occlusion},{species}在{action},只是很小的{tone}轮廓,隐约可辨,画面绝大部分是{scene},没有任何靠近镜头的动物,{light},写实野外摄影,真实照片质感" # 远景小目标模板(代码按尺寸占比 <2% 切换):目标小到动作/遮挡/性别细节无法呈现,保留细节描写会 # 迫使模型把目标画大(2026-08-31 实测两次失败后定案)——只留「远到看不清任何细节的小点」 - promptTemplateTiny: "野外大远景实拍:手机原相机主摄不变焦,{scene},在{distanceWord}的地平线附近有且仅有{count}{species}{sizeHint},远到看不清任何细节,只是隐约可辨的{tone}小点,画面绝大部分是{scene},没有任何靠近镜头的动物,{light},写实野外摄影,真实照片质感" + promptTemplateTiny: "野外大远景实拍:手机原相机主摄不变焦,{scene},{position}有且仅有{count}{species}{sizeHint},远到看不清任何细节,只是隐约可辨的{tone}小点,画面绝大部分是{scene},没有任何靠近镜头的动物,{light},写实野外摄影,真实照片质感" lights: [清晨柔和阳光, 正午明亮天光, 傍晚金色斜阳, 阴天柔和散射光] # 目标尺寸提示(sizeHint):按「体高 × focalPx ÷ 表单距离」算出目标在图中的像素高与占比, # 注入模板 {sizeHint}——把「厘米」换算成「米」再参与投影公式;focalPx = 图高/2 ÷ tan(vfov/2); diff --git a/server/data/observer.db b/server/data/observer.db index 8661a1f..712b2fc 100644 Binary files a/server/data/observer.db and b/server/data/observer.db differ diff --git a/server/workspace/trainings/RNPHE_n.tflite b/server/workspace/trainings/RNPHE_n.tflite index 9a47858..c523585 100644 Binary files a/server/workspace/trainings/RNPHE_n.tflite and b/server/workspace/trainings/RNPHE_n.tflite differ diff --git a/server/技术设计.md b/server/技术设计.md index 3d602db..958fdd1 100644 --- a/server/技术设计.md +++ b/server/技术设计.md @@ -350,7 +350,7 @@ Android 客户端启动 GET /api/v1/app/update(公开,无需 token;iOS 不 - **重新生成接口**:`POST /datasets/gen-pools`(`{datasetId}`,物种取数据集名)——编辑对话框「VLM 重新生成参数」按钮入口,VLM 失败报错(不覆盖旧值,旧值保留) - **读取链(无 config 兜底)**:物种 = 表 `gen_species` 单值 → 空则数据集名本身(**固定,不随机**);场景/动作/遮挡 = 表池随机 → **空则模板组装报错**(提示手填提示词或编辑数据集补参数);`{tone}` = 表 `gen_tone` 单值 → 空默认「深色」;站高 = 表 `gen_heights` 数值(**创建时 VLM 生成,界面不维护,代码/配置不硬编码**)→ ≤0 兜底 35cm(物理默认,非配置回退);标注类别名 = 第一类 `gen_species` + 第二类 `gen_classes`(训练 data.yaml / 模型 labels) - **界面不维护 gen_*(2026-08-28 用户定案)**:`gen_species/gen_tone/gen_heights/gen_scenes/gen_actions/gen_occlusions/gen_classes` 全部仅由模型生成(创建数据集时 VLM 自动、`gen-pools` 重新生成),管理端编辑表单对 7 列只读展示、无输入框——`POST /datasets/update` 不接收任何 gen 字段;config.yml 无任何 per-dataset 生成参数节点(`speciesByDataset/speciesTone/speciesHeights/sceneByDataset/actionByDataset/occlusionByDataset/scenes/actions/occlusions/localAi.classNames` 已删,代码无引用),保留:`promptTemplate`(通用模板)与 `lights`(光线池)、`assumedVfovDeg`;数据源唯一入口为数据集表(本库存量 15 行已直接改单值 + 真实站高,无启动迁移);**雪地场景补齐(2026-08-31,按冬季习性)**:VLM 生成场景池缺冬季场景(全部为夏秋农田),13 数据集 `gen_scenes` 直接 SQL 追加 2~3 条雪地条目(农田类雉鸡/鸽子、兔子荒坡、白马鸡蓝马鸡高山林缘);**鸭子/鹌鹑不加**——北方冬季南迁(水面封冻、候鸟不越冬),雪地场景与习性矛盾(用户拍板);`genPoolsWithVLM` 提示词已加「按冬季习性配场景:本地活跃物种须含积雪场景,南迁物种不配雪地」防新数据集一刀切;**林缘/树栖场景补齐(2026-08-31,按栖息习性)**:雉类+鸽子共享场景池(10 数据集)缺林地交接处与树上落栖场景,SQL 追加 5 条(2 林缘 + 3 树上)+ 2 条树上动作(枝头停栖/树枝上歇息);**石鸡剔除树上条目(用户定案)**——岩石地栖不上树,3 树上场景 + 2 树上动作从石鸡池移除(保留 2 林缘);**总原则(用户定案):场景与遮挡物必须符合对应动物习性**——场景(雪地/林缘/树上/水面)与遮挡物(草丛/树叶/树枝/芦苇)均按物种习性逐条判断,习性与场景矛盾即剔除;**遮挡规则(适用于所有具备树上场景的动物)**:遮挡不一定是草丛,树栖/林缘物种遮挡池须含树叶/树枝类遮挡(雉类池原已含 2 条;白马鸡/蓝马鸡补 2 条;兔子原已有 1 条林缘树叶);`genPoolsWithVLM` 提示词同步加「树栖物种场景须含林缘与树上落栖」「具备树上/林缘场景的物种遮挡池须含树叶/树枝类遮挡」 -- **场景捆绑组+权重(2026-09-04,斑鸠树栖 80% 定案)**:`gen_scenes` 条目在字符串外兼容**捆绑组对象** `{scene, weight, actions[], occlusions[]}`——场景池加权随机(weight 缺省 1,普通字符串条目权重 1),选中捆绑组时动作/遮挡仅从组内池随机(与场景联动,杜绝「树上场景+地面动作/草丛遮挡」矛盾组合——三池独立随机按模板拼装矛盾率高);组内缺省或普通字符串条目回落全局 `gen_actions`/`gen_occlusions` 池。落地(2026-09-04 用户定案「每种动物生境权重不同:野鸡主要在地面、斑鸠在树上、鸭子在水里」): +- **场景捆绑组+权重(2026-09-04,斑鸠树栖 80% 定案)**:`gen_scenes` 条目在字符串外兼容**捆绑组对象** `{scene, weight, actions[], occlusions[], position}`——场景池加权随机(weight 缺省 1,普通字符串条目权重 1),选中捆绑组时动作/遮挡仅从组内池随机(与场景联动,杜绝「树上场景+地面动作/草丛遮挡」矛盾组合——三池独立随机按模板拼装矛盾率高);组内缺省或普通字符串条目回落全局 `gen_actions`/`gen_occlusions` 池。**position 覆盖位置锚(2026-09-04 用户实测指出树栖目标锚地平线=语义落地)**:模板占位符由写死的「在{distanceWord}的地平线附近」抽成 `{position}`(两模板同改),默认值不变——地面/水面场景目标锚地平线成立;捆绑组 position 覆盖为「在N米外地平线附近…大树的枝头上/枝杈间/粗壮横枝上」(内可用 `{distanceWord}` 占位),树栖目标随场景锚到枝头。落地(2026-09-04 用户定案「每种动物生境权重不同:野鸡主要在地面、斑鸠在树上、鸭子在水里」): **斑鸠(id 133)** gen_scenes = 4 个树上捆绑组(各 weight 8)+ 8 条地面字符串(weight 1)→ 树上恰好 80%,gen_actions 收敛为纯地面 4 条(枝头/电线类动作移入捆绑组),树栖遮挡含树叶/树枝类; **雉类 8 数据集(环颈雉/红腹锦鸡/白腹锦鸡/白鹇/黄腹角雉/灰胸竹鸡/蓝鹇/褐马鸡)**:gen_scenes 剔除 4 条树上景观字符串、追加 1 个树上捆绑组 weight 3 → 地面 14/17 ≈ 82%、树上 ≈ 18%(雉类地面觅食为主、树上仅停栖/夜栖),gen_actions 剔除枝头停栖/树枝上歇息(移入捆绑组); **鸭子(108)** 全水面场景+游动/潜水动作+芦苇遮挡本就自洽,**白马鸡/蓝马鸡** 高山地面/林缘自洽,**兔子/鹌鹑/石鸡** 无树上条目——均不动。