diff --git a/server/README.md b/server/README.md index 9a587d2..384ccb6 100644 --- a/server/README.md +++ b/server/README.md @@ -186,7 +186,7 @@ APK 下载引导页(静态页面,源码在 `h5/index.html`,由后端 `/dow | POST | `/admin/datasets` | 创建数据集 `{"name":"pheasant_v2","namePrefix":"pheasant","source":"manual"\|"ai","cover":"<文件名>"}`(name ≤50 字唯一,目录自动建;namePrefix=AI 生成图文件名前缀,生成图按 `<前缀>_<两位序号>.jpg` 顺序命名;物种=数据集名(单物种规则),创建时同步调 VLM(qwen3.6-35b-a3b) 自动生成物种/场景/动作/遮挡/站高/类别名等生成参数池,响应含 `poolsGenerated`/`poolError`——VLM 失败不阻断创建,参数可事后用 gen-pools 补生成;封面优先用 cover 参数(新建对话框预生成封面回传,跳过自动生成),否则参数池成功后自动生成 16:9(1248x704)封面(1 雄 1 雌并排,响应含 `coverGenerated`/`coverError`,失败可在编辑模式重新生成);模型生成图统一转 jpg 落盘) | | GET | `/admin/datasets` | 数据集列表:`page/size` 分页,返回 `{total, list}`(含 imageCount/labeledCount/status/cover/description/**training 聚合状态**:最新训练记录的 status/currentEpoch/totalEpochs);**排除负样本库**(source=negative,仅「负样本」tab 展示) | | POST | `/admin/datasets/negative` | 获取负样本库(2026-09-07):不存在则自动创建(source=negative 固定名 `__negative__`),返回数据集记录;其 id 供既有 `/admin/datasets/upload`、`/admin/datasets/images`、`/admin/datasets/images/delete`、`/admin/datasets/image` 复用(负样本无标注/审核/清洗流程) | -| POST | `/admin/datasets/negative/generate` | 负样本批量生成 `{"count":100}`(异步任务,2026-09-07):config.yml 内置场景池按序循环组装提示词(negativeScenes 空场景/negativeAnimals 混淆动物两池两模板),逐张生成后**自动过 RF-DETR 全图检测,有检出即剔除不入库**;进度复用 GET `/admin/datasets/gen-task`(rejected=剔除数),入口在管理端「负样本」tab | +| POST | `/admin/datasets/negative/generate` | 负样本批量生成 `{"count":100}`(异步任务,2026-09-07):config.yml 内置场景池按序循环组装提示词(negativeScenes 空场景 + negativeHumans 人物/衣物两池两模板;**动物不进统一负样本**——它们将来可能是正式识别目标),空场景图**自动过 RF-DETR 空检、有检出即剔除不入库**(人物图不做空检);进度复用 GET `/admin/datasets/gen-task`(rejected=剔除数),入口在管理端「负样本」tab | | POST | `/admin/datasets/update` | 更新数据集配置 `{"id":1,"name":"新名","namePrefix":"pheasant","description":"...","cover":"a.jpg"}`:名称(改名)/文件名前缀/描述/封面,空值字段不覆盖原值;gen_* 生成参数池不在此维护(仅 VLM 生成,见 gen-pools);改名同步迁移图片目录与模型文件,标注/训练进行中拒绝 | | POST | `/admin/datasets/gen-pools` | 重新生成数据集生成参数池 `{"datasetId":1}`:按数据集名(物种)调 VLM(qwen3.6-35b-a3b) 生成轮廓色/站高/场景/动作/遮挡并写表(覆盖旧值);失败报错保留旧值 | | POST | `/admin/datasets/images/vlm-review` | VLM 藏匿位补检 `{"datasetId":1,"imageId":5}`(两阶段标注第二阶段,**须与图像生成显存互斥**):qwen3.6-35b-a3b(+mmproj) 排除已确认框,按环境/季节/时间/天气/光线/地形/习性综合判读画面推理藏身位,追加 ≤3 个疑似框(class 1)进同一 labels_json | diff --git a/server/biz/service/dataset.go b/server/biz/service/dataset.go index 64478a0..be07231 100644 --- a/server/biz/service/dataset.go +++ b/server/biz/service/dataset.go @@ -625,8 +625,9 @@ func (s *datasetService) ensureNegativeLibrary(ctx context.Context) (*entity.Dat } // AdminNegativeGenerate 负样本批量生成(异步任务):提示词从 config.yml 内置场景池按序循环组装 -// (negativeScenes 空场景 + negativeAnimals 混淆动物,保证每个短语均匀覆盖);逐张生成后自动过 -// RF-DETR 全图检测,**有检出即剔除不入库**(图里有像目标的东西就不能当背景喂给训练)—— +// (negativeScenes 空场景 + negativeHumans 人物/衣物——动物不进统一负样本,它们将来可能是正式识别目标)。 +// 空场景图逐张过 RF-DETR 全图检测,**有检出即剔除不入库**(图里有像目标的东西就不能当背景喂训练); +// 人物图不做空检(检出人是预期,检测模型不区分目标物种)。 // 检测服务不可达时 fail-open 保留该图并记日志(付费资产原则:不因质检通道故障丢弃已生成图)。 // 进度复用 GET /datasets/gen-task 按 datasetId 轮询(rejected 列为剔除计数)。 func (s *datasetService) AdminNegativeGenerate(ctx context.Context, req *dto.AdminNegativeGenerateReq) (*dto.AdminNegativeGenerateRes, error) { @@ -636,8 +637,8 @@ func (s *datasetService) AdminNegativeGenerate(ctx context.Context, req *dto.Adm } tpl := g.Cfg().MustGet(ctx, "imageGen.negativePromptTemplate").String() scenes := g.Cfg().MustGet(ctx, "imageGen.negativeScenes").Strings() - animalTpl := g.Cfg().MustGet(ctx, "imageGen.negativeAnimalPromptTemplate").String() - animals := g.Cfg().MustGet(ctx, "imageGen.negativeAnimals").Strings() + humanTpl := g.Cfg().MustGet(ctx, "imageGen.negativeHumanPromptTemplate").String() + humans := g.Cfg().MustGet(ctx, "imageGen.negativeHumans").Strings() if tpl == "" || len(scenes) == 0 { return nil, gerror.New("未配置负样本生成模板/场景池(imageGen.negativePromptTemplate/negativeScenes)") } @@ -665,30 +666,32 @@ func (s *datasetService) AdminNegativeGenerate(ctx context.Context, req *dto.Adm }); err != nil { return nil, err } - s.runNegativeGenTask(taskId, lib, req.Count, tpl, scenes, animalTpl, animals, provider, now) + s.runNegativeGenTask(taskId, lib, req.Count, tpl, scenes, humanTpl, humans, provider, now) return &dto.AdminNegativeGenerateRes{TaskId: taskId, Total: req.Count}, nil } // runNegativeGenTask 负样本生成执行协程(生命周期任务,同 runGenTask 模式):池内逐张生成 → // 转 jpg → RF-DETR 空检 → 合格才落盘入库。done 计已处理张数(含剔除),rejected 计剔除张数。 -func (s *datasetService) runNegativeGenTask(taskId int64, lib *entity.Dataset, count int, tpl string, scenes []string, animalTpl string, animals []string, provider common.ImageGenProvider, now *gtime.Time) { +func (s *datasetService) runNegativeGenTask(taskId int64, lib *entity.Dataset, count int, tpl string, scenes []string, humanTpl string, humans []string, provider common.ImageGenProvider, now *gtime.Time) { bgCtx := context.Background() go func() { dir := common.DatasetImagesDir(bgCtx, lib.Name) qa := common.LocalAiClient(bgCtx) // 空检通道(可能为 nil:fail-open 全保留并记日志) - combined := make([]string, 0, len(scenes)+len(animals)) + combined := make([]string, 0, len(scenes)+len(humans)) combined = append(combined, scenes...) - animalStart := len(combined) - combined = append(combined, animals...) + humanStart := len(combined) + combined = append(combined, humans...) generated := 0 rejected := 0 processed := 0 for i := 0; i < count; i++ { // 按序循环取池:每个短语均匀覆盖,不漏条目 - entry := combined[i%len(combined)] + idx := i % len(combined) + isHuman := idx >= humanStart // 人物/衣物:主体是画面意图本身,且永远不会是识别目标 + entry := combined[idx] prompt := tpl - if animalTpl != "" && i%len(combined) >= animalStart { - prompt = animalTpl // 混淆动物条目:否定句只排除目标物种 + if isHuman && humanTpl != "" { + prompt = humanTpl // 否定句只排除目标物种 } prompt = strings.Replace(prompt, "{scene}", entry, 1) var data []byte @@ -706,8 +709,9 @@ func (s *datasetService) runNegativeGenTask(taskId int64, lib *entity.Dataset, c _ = dao.GenTask.Finish(bgCtx, taskId, fmt.Sprintf("第 %d 张转 jpg 失败(已入库 %d 张): %v", i+1, generated, jErr)) return } - // 空检质检:RF-DETR 全图检测有候选即剔除(负样本必须全空);检测失败 fail-open 保留 - if qa != nil { + // 空检质检:RF-DETR 全图检测有候选即剔除(负样本必须全空);检测失败 fail-open 保留。 + // 仅对空场景生效——人物图检出人是预期,不做空检 + if qa != nil && !isHuman { w, h := imageSize(data) if w > 0 && h > 0 { dets, dErr := qa.Detect(bgCtx, data, "image/jpeg", w, h) diff --git a/server/config.yml b/server/config.yml index ddc5cfb..7d02853 100644 --- a/server/config.yml +++ b/server/config.yml @@ -78,8 +78,15 @@ imageGen: 林地边缘的树木与草地交界, 积雪覆盖的开阔旷野, 河边大片卵石滩, 收割后的农田田埂, 茂密的芦苇丛, 城市人行道与斑马线, 露天停车场的车辆间距, 公园步道与路边长椅 ] - negativeAnimalPromptTemplate: "写实摄影:手机原相机主摄不变焦,{scene},光线自然,画面中没有雉鸡、野兔和斑鸠,真实照片质感" - negativeAnimals: [趴在院子里的家猫, 蹲坐在路边的家犬, 电线上落着的一排麻雀, 广场上觅食的鸽子群, 树枝上的喜鹊] + negativeHumanPromptTemplate: "写实摄影:手机原相机主摄不变焦,{scene},光线自然,画面中没有雉鸡、野兔和斑鸠,真实照片质感" + # 人物/衣物池(2026-09-07 用户定案):人与服饰永远不会是识别目标,可安全进统一负样本; + # 动物(猫/狗/鸽子等)不进统一负样本——它们将来可能训练成正式识别目标,公共负样本会压杀自家模型。 + # 此类带主体图不参与空检剔除(RF-DETR 检出人是预期,无法按物种甄别),靠提示词保证不含目标物种 + negativeHumans: [ + 远处田间弯腰劳作的农民, 公园小路上散步的行人, 石子路上迎面走来的游客, 蹲在田埂上休息的人, + 骑电动车驶过乡间道路的人, 树丛边缘经过的穿迷彩服的人, 挂在晾衣绳上的外套和花色衣物, + 阳台栏杆上晾晒的被褥和衣物, 坐在长椅上穿深色外套的人的背影, 草地远处玩耍的孩童身影 + ] # 目标尺寸提示(sizeHint):按「体高 × focalPx ÷ 表单距离」算出目标在图中的像素高与占比, # 注入模板 {sizeHint}——把「厘米」换算成「米」再参与投影公式;focalPx = 图高/2 ÷ tan(vfov/2); # 站高取数据集表 gen_heights(创建数据集时 VLM 生成,界面不维护,代码/配置不硬编码) diff --git a/server/data/observer.db b/server/data/observer.db index 4cf6559..d8fbe44 100644 Binary files a/server/data/observer.db and b/server/data/observer.db differ diff --git a/server/技术设计.md b/server/技术设计.md index 703fa97..393966a 100644 --- a/server/技术设计.md +++ b/server/技术设计.md @@ -432,7 +432,7 @@ training: - **管理端「数据训练」页双 tab(2026-09-07 用户定案)**:「数据集」tab = 现有数据集卡片列表(列表接口排除负样本库),「负样本」tab = 负样本库图片网格(复用既有上传/删除接口,按负样本库 datasetId 调用;无标注/审核/清洗/预标/生成/众包流程)。 - **训练打包混入(prepareYoloSet)**:打包主数据集后查负样本库,存在且有图则全部图片(不看 review_status/clean_excluded——负样本库无审核语义)加入训练包:文件名统一加 `neg_` 前缀(防与主数据集图重名覆盖),label txt 写空内容(= 背景图),随主数据集按同一 80/20 逻辑拆 train/val(val 含负样本可观测背景误检率);`data.yaml` 类别不变(负样本不引入新类)。无负样本库/库为空 → 行为与原来完全一致。 - **入口防护**:预标注(AdminStartLabelTask)、众包下发(AdminCreateTask)、发起训练(AdminStartTraining)对 `source='negative'` 数据集一律拒绝(负样本库单独训练无意义、其图片不需要标注)。 -- **负样本批量生成(2026-09-07)**:`POST /admin/datasets/negative/generate`(count 1-1000,异步任务,复用 gen_task 表按 datasetId 轮询进度,新增 `rejected` 剔除计数列)——提示词从 config.yml 内置池按序循环组装(保证每个短语均匀覆盖):`imageGen.negativeScenes`(~50 条,按「三物种易被误认的形状机理」穷举:细长轮廓→雉鸡/圆拱形→野兔/高处停息悬挂→斑鸠 + 室内/户外空镜)走 `negativePromptTemplate`(无任何目标词 + 显式否定「没有任何动物」);`imageGen.negativeAnimals`(混淆动物:猫/狗/麻雀/鸽子/喜鹊)走 `negativeAnimalPromptTemplate`(否定句只排除目标物种)。**逐张生成后自动过 RF-DETR 全图检测,有检出即剔除不入库**(图里有像目标的东西就不能当背景喂训练);检测服务不可达 fail-open 保留该图记日志(付费资产原则)。尺寸与正样本一致 704x1248。 +- **负样本批量生成(2026-09-07)**:`POST /admin/datasets/negative/generate`(count 1-1000,异步任务,复用 gen_task 表按 datasetId 轮询进度,新增 `rejected` 剔除计数列)——提示词从 config.yml 内置池按序循环组装(保证每个短语均匀覆盖):`imageGen.negativeScenes`(~50 条,按「三物种易被误认的形状机理」穷举:细长轮廓→雉鸡/圆拱形→野兔/高处停息悬挂→斑鸠 + 室内/户外空镜)走 `negativePromptTemplate`(无任何目标词 + 显式否定「没有任何动物」);`imageGen.negativeHumans`(人物/衣物:田间劳作/行人/迷彩服/晾晒衣物——端侧「人和衣物被识别成动物」的常见误报源)走 `negativeHumanPromptTemplate`(否定句只排除目标物种)。**动物不进统一负样本(2026-09-07 用户定案)**:猫/狗/鸽子等将来可能训练成正式识别目标,统一负样本会把它们的模型压杀掉——物种级混淆难例属于该物种数据集自己的事。**空检剔除仅对空场景生效**:空场景图过 RF-DETR 全图检测、有检出即剔除(图里有像目标的东西不能当背景喂训练);人物图不做空检(检出人是预期,检测模型无法按物种甄别,靠提示词保证不含目标物种)。检测服务不可达 fail-open 保留该图记日志(付费资产原则)。尺寸与正样本一致 704x1248。 - **删除防护**:AdminDeleteDataset 拒绝删除负样本库(防误删整库);单张图片删除照常可用。 ### 模型热更新与多模型推理(客户端 Flutter) diff --git a/server_admin/src/views/Datasets.vue b/server_admin/src/views/Datasets.vue index b730bca..c0c5238 100644 --- a/server_admin/src/views/Datasets.vue +++ b/server_admin/src/views/Datasets.vue @@ -746,8 +746,9 @@ onBeforeUnmount(() => {

- 内置场景池按序循环生成(细长轮廓 / 圆拱形 / 高处停息悬挂 / 室内户外空镜 / 混淆动物,与正样本同相机风格); - 每张生成后自动过 RF-DETR 空检,有检出的自动剔除不入库。约 40 秒/张,进度见负样本 tab 进度条。 + 内置场景池按序循环生成(细长轮廓 / 圆拱形 / 高处停息悬挂 / 室内户外空镜 / 人与衣物, + 与正样本同相机风格);空场景图自动过 RF-DETR 空检,有检出的自动剔除不入库。 + 约 40 秒/张,进度见负样本 tab 进度条。