1
This commit is contained in:
+1
-1
@@ -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 落盘) |
|
| 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 展示) |
|
| 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` | 获取负样本库(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/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/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 |
|
| POST | `/admin/datasets/images/vlm-review` | VLM 藏匿位补检 `{"datasetId":1,"imageId":5}`(两阶段标注第二阶段,**须与图像生成显存互斥**):qwen3.6-35b-a3b(+mmproj) 排除已确认框,按环境/季节/时间/天气/光线/地形/习性综合判读画面推理藏身位,追加 ≤3 个疑似框(class 1)进同一 labels_json |
|
||||||
|
|||||||
@@ -625,8 +625,9 @@ func (s *datasetService) ensureNegativeLibrary(ctx context.Context) (*entity.Dat
|
|||||||
}
|
}
|
||||||
|
|
||||||
// AdminNegativeGenerate 负样本批量生成(异步任务):提示词从 config.yml 内置场景池按序循环组装
|
// AdminNegativeGenerate 负样本批量生成(异步任务):提示词从 config.yml 内置场景池按序循环组装
|
||||||
// (negativeScenes 空场景 + negativeAnimals 混淆动物,保证每个短语均匀覆盖);逐张生成后自动过
|
// (negativeScenes 空场景 + negativeHumans 人物/衣物——动物不进统一负样本,它们将来可能是正式识别目标)。
|
||||||
// RF-DETR 全图检测,**有检出即剔除不入库**(图里有像目标的东西就不能当背景喂给训练)——
|
// 空场景图逐张过 RF-DETR 全图检测,**有检出即剔除不入库**(图里有像目标的东西就不能当背景喂训练);
|
||||||
|
// 人物图不做空检(检出人是预期,检测模型不区分目标物种)。
|
||||||
// 检测服务不可达时 fail-open 保留该图并记日志(付费资产原则:不因质检通道故障丢弃已生成图)。
|
// 检测服务不可达时 fail-open 保留该图并记日志(付费资产原则:不因质检通道故障丢弃已生成图)。
|
||||||
// 进度复用 GET /datasets/gen-task 按 datasetId 轮询(rejected 列为剔除计数)。
|
// 进度复用 GET /datasets/gen-task 按 datasetId 轮询(rejected 列为剔除计数)。
|
||||||
func (s *datasetService) AdminNegativeGenerate(ctx context.Context, req *dto.AdminNegativeGenerateReq) (*dto.AdminNegativeGenerateRes, error) {
|
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()
|
tpl := g.Cfg().MustGet(ctx, "imageGen.negativePromptTemplate").String()
|
||||||
scenes := g.Cfg().MustGet(ctx, "imageGen.negativeScenes").Strings()
|
scenes := g.Cfg().MustGet(ctx, "imageGen.negativeScenes").Strings()
|
||||||
animalTpl := g.Cfg().MustGet(ctx, "imageGen.negativeAnimalPromptTemplate").String()
|
humanTpl := g.Cfg().MustGet(ctx, "imageGen.negativeHumanPromptTemplate").String()
|
||||||
animals := g.Cfg().MustGet(ctx, "imageGen.negativeAnimals").Strings()
|
humans := g.Cfg().MustGet(ctx, "imageGen.negativeHumans").Strings()
|
||||||
if tpl == "" || len(scenes) == 0 {
|
if tpl == "" || len(scenes) == 0 {
|
||||||
return nil, gerror.New("未配置负样本生成模板/场景池(imageGen.negativePromptTemplate/negativeScenes)")
|
return nil, gerror.New("未配置负样本生成模板/场景池(imageGen.negativePromptTemplate/negativeScenes)")
|
||||||
}
|
}
|
||||||
@@ -665,30 +666,32 @@ func (s *datasetService) AdminNegativeGenerate(ctx context.Context, req *dto.Adm
|
|||||||
}); err != nil {
|
}); err != nil {
|
||||||
return nil, err
|
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
|
return &dto.AdminNegativeGenerateRes{TaskId: taskId, Total: req.Count}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// runNegativeGenTask 负样本生成执行协程(生命周期任务,同 runGenTask 模式):池内逐张生成 →
|
// runNegativeGenTask 负样本生成执行协程(生命周期任务,同 runGenTask 模式):池内逐张生成 →
|
||||||
// 转 jpg → RF-DETR 空检 → 合格才落盘入库。done 计已处理张数(含剔除),rejected 计剔除张数。
|
// 转 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()
|
bgCtx := context.Background()
|
||||||
go func() {
|
go func() {
|
||||||
dir := common.DatasetImagesDir(bgCtx, lib.Name)
|
dir := common.DatasetImagesDir(bgCtx, lib.Name)
|
||||||
qa := common.LocalAiClient(bgCtx) // 空检通道(可能为 nil:fail-open 全保留并记日志)
|
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...)
|
combined = append(combined, scenes...)
|
||||||
animalStart := len(combined)
|
humanStart := len(combined)
|
||||||
combined = append(combined, animals...)
|
combined = append(combined, humans...)
|
||||||
generated := 0
|
generated := 0
|
||||||
rejected := 0
|
rejected := 0
|
||||||
processed := 0
|
processed := 0
|
||||||
for i := 0; i < count; i++ {
|
for i := 0; i < count; i++ {
|
||||||
// 按序循环取池:每个短语均匀覆盖,不漏条目
|
// 按序循环取池:每个短语均匀覆盖,不漏条目
|
||||||
entry := combined[i%len(combined)]
|
idx := i % len(combined)
|
||||||
|
isHuman := idx >= humanStart // 人物/衣物:主体是画面意图本身,且永远不会是识别目标
|
||||||
|
entry := combined[idx]
|
||||||
prompt := tpl
|
prompt := tpl
|
||||||
if animalTpl != "" && i%len(combined) >= animalStart {
|
if isHuman && humanTpl != "" {
|
||||||
prompt = animalTpl // 混淆动物条目:否定句只排除目标物种
|
prompt = humanTpl // 否定句只排除目标物种
|
||||||
}
|
}
|
||||||
prompt = strings.Replace(prompt, "{scene}", entry, 1)
|
prompt = strings.Replace(prompt, "{scene}", entry, 1)
|
||||||
var data []byte
|
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))
|
_ = dao.GenTask.Finish(bgCtx, taskId, fmt.Sprintf("第 %d 张转 jpg 失败(已入库 %d 张): %v", i+1, generated, jErr))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 空检质检:RF-DETR 全图检测有候选即剔除(负样本必须全空);检测失败 fail-open 保留
|
// 空检质检:RF-DETR 全图检测有候选即剔除(负样本必须全空);检测失败 fail-open 保留。
|
||||||
if qa != nil {
|
// 仅对空场景生效——人物图检出人是预期,不做空检
|
||||||
|
if qa != nil && !isHuman {
|
||||||
w, h := imageSize(data)
|
w, h := imageSize(data)
|
||||||
if w > 0 && h > 0 {
|
if w > 0 && h > 0 {
|
||||||
dets, dErr := qa.Detect(bgCtx, data, "image/jpeg", w, h)
|
dets, dErr := qa.Detect(bgCtx, data, "image/jpeg", w, h)
|
||||||
|
|||||||
+9
-2
@@ -78,8 +78,15 @@ imageGen:
|
|||||||
林地边缘的树木与草地交界, 积雪覆盖的开阔旷野, 河边大片卵石滩, 收割后的农田田埂,
|
林地边缘的树木与草地交界, 积雪覆盖的开阔旷野, 河边大片卵石滩, 收割后的农田田埂,
|
||||||
茂密的芦苇丛, 城市人行道与斑马线, 露天停车场的车辆间距, 公园步道与路边长椅
|
茂密的芦苇丛, 城市人行道与斑马线, 露天停车场的车辆间距, 公园步道与路边长椅
|
||||||
]
|
]
|
||||||
negativeAnimalPromptTemplate: "写实摄影:手机原相机主摄不变焦,{scene},光线自然,画面中没有雉鸡、野兔和斑鸠,真实照片质感"
|
negativeHumanPromptTemplate: "写实摄影:手机原相机主摄不变焦,{scene},光线自然,画面中没有雉鸡、野兔和斑鸠,真实照片质感"
|
||||||
negativeAnimals: [趴在院子里的家猫, 蹲坐在路边的家犬, 电线上落着的一排麻雀, 广场上觅食的鸽子群, 树枝上的喜鹊]
|
# 人物/衣物池(2026-09-07 用户定案):人与服饰永远不会是识别目标,可安全进统一负样本;
|
||||||
|
# 动物(猫/狗/鸽子等)不进统一负样本——它们将来可能训练成正式识别目标,公共负样本会压杀自家模型。
|
||||||
|
# 此类带主体图不参与空检剔除(RF-DETR 检出人是预期,无法按物种甄别),靠提示词保证不含目标物种
|
||||||
|
negativeHumans: [
|
||||||
|
远处田间弯腰劳作的农民, 公园小路上散步的行人, 石子路上迎面走来的游客, 蹲在田埂上休息的人,
|
||||||
|
骑电动车驶过乡间道路的人, 树丛边缘经过的穿迷彩服的人, 挂在晾衣绳上的外套和花色衣物,
|
||||||
|
阳台栏杆上晾晒的被褥和衣物, 坐在长椅上穿深色外套的人的背影, 草地远处玩耍的孩童身影
|
||||||
|
]
|
||||||
# 目标尺寸提示(sizeHint):按「体高 × focalPx ÷ 表单距离」算出目标在图中的像素高与占比,
|
# 目标尺寸提示(sizeHint):按「体高 × focalPx ÷ 表单距离」算出目标在图中的像素高与占比,
|
||||||
# 注入模板 {sizeHint}——把「厘米」换算成「米」再参与投影公式;focalPx = 图高/2 ÷ tan(vfov/2);
|
# 注入模板 {sizeHint}——把「厘米」换算成「米」再参与投影公式;focalPx = 图高/2 ÷ tan(vfov/2);
|
||||||
# 站高取数据集表 gen_heights(创建数据集时 VLM 生成,界面不维护,代码/配置不硬编码)
|
# 站高取数据集表 gen_heights(创建数据集时 VLM 生成,界面不维护,代码/配置不硬编码)
|
||||||
|
|||||||
Binary file not shown.
+1
-1
@@ -432,7 +432,7 @@ training:
|
|||||||
- **管理端「数据训练」页双 tab(2026-09-07 用户定案)**:「数据集」tab = 现有数据集卡片列表(列表接口排除负样本库),「负样本」tab = 负样本库图片网格(复用既有上传/删除接口,按负样本库 datasetId 调用;无标注/审核/清洗/预标/生成/众包流程)。
|
- **管理端「数据训练」页双 tab(2026-09-07 用户定案)**:「数据集」tab = 现有数据集卡片列表(列表接口排除负样本库),「负样本」tab = 负样本库图片网格(复用既有上传/删除接口,按负样本库 datasetId 调用;无标注/审核/清洗/预标/生成/众包流程)。
|
||||||
- **训练打包混入(prepareYoloSet)**:打包主数据集后查负样本库,存在且有图则全部图片(不看 review_status/clean_excluded——负样本库无审核语义)加入训练包:文件名统一加 `neg_` 前缀(防与主数据集图重名覆盖),label txt 写空内容(= 背景图),随主数据集按同一 80/20 逻辑拆 train/val(val 含负样本可观测背景误检率);`data.yaml` 类别不变(负样本不引入新类)。无负样本库/库为空 → 行为与原来完全一致。
|
- **训练打包混入(prepareYoloSet)**:打包主数据集后查负样本库,存在且有图则全部图片(不看 review_status/clean_excluded——负样本库无审核语义)加入训练包:文件名统一加 `neg_` 前缀(防与主数据集图重名覆盖),label txt 写空内容(= 背景图),随主数据集按同一 80/20 逻辑拆 train/val(val 含负样本可观测背景误检率);`data.yaml` 类别不变(负样本不引入新类)。无负样本库/库为空 → 行为与原来完全一致。
|
||||||
- **入口防护**:预标注(AdminStartLabelTask)、众包下发(AdminCreateTask)、发起训练(AdminStartTraining)对 `source='negative'` 数据集一律拒绝(负样本库单独训练无意义、其图片不需要标注)。
|
- **入口防护**:预标注(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 拒绝删除负样本库(防误删整库);单张图片删除照常可用。
|
- **删除防护**:AdminDeleteDataset 拒绝删除负样本库(防误删整库);单张图片删除照常可用。
|
||||||
|
|
||||||
### 模型热更新与多模型推理(客户端 Flutter)
|
### 模型热更新与多模型推理(客户端 Flutter)
|
||||||
|
|||||||
@@ -746,8 +746,9 @@ onBeforeUnmount(() => {
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<p class="start-note">
|
<p class="start-note">
|
||||||
内置场景池按序循环生成(细长轮廓 / 圆拱形 / 高处停息悬挂 / 室内户外空镜 / 混淆动物,与正样本同相机风格);
|
内置场景池按序循环生成(细长轮廓 / 圆拱形 / 高处停息悬挂 / 室内户外空镜 / 人与衣物,
|
||||||
每张生成后自动过 RF-DETR 空检,有检出的自动剔除不入库。约 40 秒/张,进度见负样本 tab 进度条。
|
与正样本同相机风格);空场景图自动过 RF-DETR 空检,有检出的自动剔除不入库。
|
||||||
|
约 40 秒/张,进度见负样本 tab 进度条。
|
||||||
</p>
|
</p>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="negGenVisible = false">取消</el-button>
|
<el-button @click="negGenVisible = false">取消</el-button>
|
||||||
|
|||||||
Reference in New Issue
Block a user