1
This commit is contained in:
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
@@ -4,8 +4,8 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>视野管理端</title>
|
||||
<script type="module" crossorigin src="/admin/assets/index-S93JCAwW.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/admin/assets/index-66aoa8zi.css">
|
||||
<script type="module" crossorigin src="/admin/assets/index-CHT1qyKR.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/admin/assets/index-aSE0N1Mp.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -52,6 +52,9 @@ const (
|
||||
// 文生图池默认并发度(被 config.yml imageGen.poolSize 覆盖;z-image 显存独占,默认 1)
|
||||
GenPoolDefaultSize = 1
|
||||
|
||||
// 单图 VLM 藏匿位补检疑似框(class=1)上限;每次调用可追加数 = 上限 - 图内已有疑似框数,≤0 跳过调用
|
||||
VlmMaxSuspectPerImage = 3
|
||||
|
||||
// 模型版本号前缀(m1.0.0),同数据集内递增
|
||||
ModelVersionPrefix = "m"
|
||||
)
|
||||
|
||||
@@ -211,6 +211,7 @@ func (s *labelTaskService) runDetection(ctx context.Context, taskId int64, datas
|
||||
failed := ""
|
||||
doneCount := 0
|
||||
for i, img := range images {
|
||||
g.Log().Infof(bgCtx, "预标 %s (%d/%d): %s", dataset.Name, i+1, len(images), img.Filename)
|
||||
// 单张 120s 超时兜底(无超时 + 无取消的独立 ctx 下防检测服务悬挂拖死任务)
|
||||
detCtx, cancel := context.WithTimeout(bgCtx, 120*time.Second)
|
||||
err := common.LabelTaskPoolInstance().Submit(detCtx, func(ctx context.Context) error {
|
||||
@@ -326,7 +327,7 @@ func (s *labelTaskService) AdminImageVlmReview(ctx context.Context, req *dto.Adm
|
||||
return nil, gerror.Wrap(jErr, "解析已有标注失败")
|
||||
}
|
||||
}
|
||||
// 排除集:已确认框坐标(VLM 不得重复输出这些位置)
|
||||
// 排除集:已有框坐标(VLM 不得输出与这些矩形重叠的框)
|
||||
exclude := "无"
|
||||
if len(existing) > 0 {
|
||||
parts := make([]string, 0, len(existing))
|
||||
@@ -335,6 +336,20 @@ func (s *labelTaskService) AdminImageVlmReview(ctx context.Context, req *dto.Adm
|
||||
}
|
||||
exclude = strings.Join(parts, " ")
|
||||
}
|
||||
// 可追加上限:单图疑似框(class=1)总数 ≤ VlmMaxSuspectPerImage,
|
||||
// 本次最多补 maxAdd = 上限 - 已有疑似框数;已达上限则跳过模型调用
|
||||
suspectCount := 0
|
||||
for _, b := range existing {
|
||||
if b.Class == 1 {
|
||||
suspectCount++
|
||||
}
|
||||
}
|
||||
maxAdd := consts.VlmMaxSuspectPerImage - suspectCount
|
||||
if maxAdd <= 0 {
|
||||
return &dto.AdminImageVlmReviewRes{
|
||||
Note: fmt.Sprintf("该图已有 %d 个疑似框(上限 %d),无需补标", suspectCount, consts.VlmMaxSuspectPerImage),
|
||||
}, nil
|
||||
}
|
||||
// 物种:数据集单物种(gen_species)直接注入;提示词不再存储(2026-09-02 清理),
|
||||
// 无法从提示词匹配,交给 VLM 从已确认框自行判断
|
||||
speciesLine := "从已确认目标框自行判断物种"
|
||||
@@ -345,8 +360,9 @@ func (s *labelTaskService) AdminImageVlmReview(ctx context.Context, req *dto.Adm
|
||||
"你是野生动物监测照片分析助手。\n物种:%s\n已确认目标位置(归一化 cx,cy,w,h):%s\n"+
|
||||
"不要出现与已有位置绘制矩形重叠的框。\n"+
|
||||
"请基于环境、天气、光线与该物种习性(觅食路线、隐蔽处、阴影边缘、植被深处),分析除已确认位置外还可能藏匿个体的位置。\n"+
|
||||
"输出归一化 bbox 的 JSON 数组:[{\"cx\":0.5,\"cy\":0.5,\"w\":0.2,\"h\":0.15}],最多 3 个,按可能性从高到低;确无可能则输出 []。只输出 JSON 数组本身,不要其他文字。",
|
||||
speciesLine, exclude)
|
||||
"输出归一化 bbox 的 JSON 数组:[{\"cx\":0.5,\"cy\":0.5,\"w\":0.2,\"h\":0.15}],最多 %d 个,按可能性从高到低;确无可能则输出 []。只输出 JSON 数组本身,不要其他文字。",
|
||||
speciesLine, exclude, maxAdd)
|
||||
g.Log().Infof(ctx, "补标 %s: %s(可追加 %d 个疑似框)", dataset.Name, img.Filename, maxAdd)
|
||||
content, err := common.QwenVL(ctx, data, imageMime(img.Filename), prompt)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "VLM 推理失败")
|
||||
@@ -371,7 +387,7 @@ func (s *labelTaskService) AdminImageVlmReview(ctx context.Context, req *dto.Adm
|
||||
overlap = client.OverlapThreshold
|
||||
}
|
||||
for _, b := range raw {
|
||||
if len(res.Boxes) >= 3 {
|
||||
if len(res.Boxes) >= maxAdd {
|
||||
break
|
||||
}
|
||||
if b.Cx <= 0 || b.Cx >= 1 || b.Cy <= 0 || b.Cy >= 1 || b.W <= 0 || b.W >= 1 || b.H <= 0 || b.H >= 1 {
|
||||
|
||||
Binary file not shown.
+1
-1
@@ -333,7 +333,7 @@ Android 客户端启动 GET /api/v1/app/update(公开,无需 token;iOS 不
|
||||
- **生成异步化(2026-08-28,count 1..1000)**:`/datasets/generate` 仅做校验(provider/数据集/标注服务/Serial 内并发检查已有 running 生成任务)+ 插 `gen_task` 落 running 记录即返回 `{taskId, total}`(毫秒级,前端 10s 超时无忧);后台协程逐张生成(`context.Background()` 生命周期模式,与预标注 `runDetection` 同构;`imageGen.poolSize` 并发池,z-image 显存独占默认 1),**每张不设调用超时(2026-08-29)**——异步任务超时只会造成假失败:超时仅放弃等待、取消不了 local-ai backend,任务置 failed 而模型仍被占用、poolSize=1 的池被占死、后续请求连环超时;失败判定完全由 provider 真实返回决定(dashscope 内部自限 120s 轮询不受影响);极端情况 local-ai 挂死时任务保持 running(状态诚实),由启动恢复兜底;逐张落盘 + 入库(2026-09-02 起不记录 prompt——生成提示词临时使用不落库)+ Serial 更新进度,完成(或部分失败)后对本次新增图触发自动标注,置 done/failed 由前端轮询 `/datasets/gen-task` 呈现;中途失败保留已生成图(不删除——付费资产原则);启动恢复 `recoverGenTasks` 孤儿 running 置 failed;VLM 补检与生成显存互斥(该数据集有 running 生成任务时拒绝)
|
||||
- prompt 手填覆盖;留空走 `imageGen.promptTemplate` 通用模板逐张组装(**物种/场景/动作/遮挡/轮廓色/站高池均从数据集表读取(见下节),每张独立组装保持多样性**;**性别每张随机雄/雌(2026-08-28)**——两性体型大小与外观差异大(雉鸡雄艳雌褐、野鸭雄艳雌素等),随机让训练数据覆盖两性形态,`{species}` 替换为「雄/雌性+物种」;物种只写名字不写羽毛细节,细节会拉近镜头);**位置/镜头描述(2026-08-31 v4 最终定案:手机原相机主摄不变焦)**:2026-08-28 曾定「不得包含目标位置描述」(上部1/3 弱约束遵循力差、角度与位置耦合沉底);2026-08-31 用户确认恢复距离感锚定,镜头词四版演进:广角(构图先验=前景大主体,用户实测野鸡在画面下半部)→ 长焦(**长焦物理放大与 13px 像素数学矛盾**,用户实测野鸡占半图宽)→ 全删 → **「手机原相机主摄不变焦」**(用户提议;与 13px 像素数学完全自洽——公式即按主摄 vfov 52° 计算;手机拍远处目标天然显小,先验同向;顺带对齐推理端域——App 就是手机拍摄);保留「**在{distanceWord}的地平线附近**」位置锚定(地平线是远景场景锚点,不同于上部1/3 弱约束);**远景小目标模板 `promptTemplateTiny`(2026-08-31)**:尺寸占比 <2% 时切换——目标小到动作/遮挡/性别细节无法呈现,保留细节描写(低头啄食/只露出头背)会迫使模型把目标画大(语义矛盾:1% 目标不可见细节),只留「远到看不清任何细节,只是隐约可辨的小点」
|
||||
- **生成图距离/数量校验(2026-08-28)**:表单填 `distance`(米,固定值) 与 `animalCount` 后逐张启用;**距离校验已取消(2026-08-28)**:生成即入库,无逐张拒检;参数与公式见 git 历史(物理公式 d=体高×focalPx÷像素高)
|
||||
- **VLM 藏匿位补检(两阶段第二阶段)**:`/datasets/images/vlm-review` 单图接口;qwen3.6-35b-a3b(+mmproj) 以「图片+物种(数据集单物种 gen_species 注入;2026-09-02 prompt 不再存储,原「prompt 含物种名才注入」匹配逻辑删除)+已确认框坐标(排除)」推理藏匿位,输出归一化 bbox JSON(范围校验+与已有框重叠去重),追加 ≤3 个 class=1 疑似框进同一 labels_json;VL 与 z-image 显存互斥(12G 装不下两者),批量补检在生成队列空闲后执行;qwen3.6-35b-a3b 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)**:模板加 `{occlusion}` 占位(插在「在{action}」之后)——身体完全暴露的个体对识别/标注训练无意义,野外目标必然存在植被等遮挡;`imageGen.occlusionByDataset` 按数据集配遮挡池(雉鸡→草丛灌丛半掩、兔→荒草只露头耳、鸭→芦苇水面半没、鹌鹑→密草藏身),未配置回退通用 `imageGen.occlusions`;池内条目覆盖轻(身体半掩)~重(只露头背)遮挡,每张随机取一条,杜绝完全暴露;遮挡物多样(杂草/树叶/枝条,2026-08-28 补树叶)且须与 sceneByDataset 场景自洽;遮挡后可见部分小于站高,检测框高与距离估算按可见部分仍自洽(真实场景同此);**拍摄角度(2026-08-28,已废弃角度描述)**:不带任何拍摄角度描述——演进:固定「上部三分之一」区域描述对文生图模型遵循力弱(生成物落底)→ 加固定俯拍(目标上移成功但构图单一,用户否决)→ 池内随机俯/平/仰(仰拍使贴地目标必然沉底,用户否决)→ 只留俯拍/平视(用户仍嫌啰嗦)→ **完全去掉角度描述**(2026-08-28 定案):交由模型平视先验自由构图,目标自然落在画面中部/上部;模板只靠「{species}离镜头很远,只是很小的{tone}轮廓」(物种名替代泛称「目标」,不限定鸟类——物种池含兔子等非鸟物种;`{tone}` 轮廓色词按 `imageGen.speciesTone.<物种>` 取,默认深色剪影,**白雉鸡配浅色**——白羽个体用「深色轮廓」描述会与模型渲染矛盾,2026-08-28)维持远景感,手填提示词仅追加数量约束;**2026-08-31 改版(最终定案)**:镜头词全删(广角/长焦实测均失败);「{distanceWord}有且仅有」→「在{distanceWord}的地平线附近有且仅有」;{action} 从句末主体降级至遮挡之后(动作=叙事焦点=视觉焦点,弱化动作降低主体视觉权重);{sizeHint} 由「像素数+占比」改「画面占比分级尺寸词(微小几乎难辨/很小/较小/中等/较大)+占比」(量化像素数与固定物体类比词针尖/米粒/拳头均不跨物种/距离通用,用户否决);**远景小目标模板 `promptTemplateTiny`**:占比 <2% 时切换,动作/遮挡/性别细节全删(保留细节描写迫使模型画大);不合格丢弃重生成,单张上限 3 次(`consts.GenValidateMaxAttempts`),连续不合格整体报错(已合格张数保留)
|
||||
- **VLM 藏匿位补检(两阶段第二阶段)**:`/datasets/images/vlm-review` 单图接口;qwen3.6-35b-a3b(+mmproj) 以「图片+物种(数据集单物种 gen_species 注入;2026-09-02 prompt 不再存储,原「prompt 含物种名才注入」匹配逻辑删除)+已确认框坐标(排除)」推理藏匿位,输出归一化 bbox JSON(范围校验+与已有框重叠去重),追加 class=1 疑似框进同一 labels_json(**2026-09-02 上限动态化**:单图疑似框总数 ≤ `consts.VlmMaxSuspectPerImage`(3),每次可追加数 = 3 − 图内已有疑似框数,≤0 直接跳过模型调用;提示词「最多 N 个」与落库硬截断均按此数);VL 与 z-image 显存互斥(12G 装不下两者),批量补检在生成队列空闲后执行;qwen3.6-35b-a3b 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)**:模板加 `{occlusion}` 占位(插在「在{action}」之后)——身体完全暴露的个体对识别/标注训练无意义,野外目标必然存在植被等遮挡;`imageGen.occlusionByDataset` 按数据集配遮挡池(雉鸡→草丛灌丛半掩、兔→荒草只露头耳、鸭→芦苇水面半没、鹌鹑→密草藏身),未配置回退通用 `imageGen.occlusions`;池内条目覆盖轻(身体半掩)~重(只露头背)遮挡,每张随机取一条,杜绝完全暴露;遮挡物多样(杂草/树叶/枝条,2026-08-28 补树叶)且须与 sceneByDataset 场景自洽;遮挡后可见部分小于站高,检测框高与距离估算按可见部分仍自洽(真实场景同此);**拍摄角度(2026-08-28,已废弃角度描述)**:不带任何拍摄角度描述——演进:固定「上部三分之一」区域描述对文生图模型遵循力弱(生成物落底)→ 加固定俯拍(目标上移成功但构图单一,用户否决)→ 池内随机俯/平/仰(仰拍使贴地目标必然沉底,用户否决)→ 只留俯拍/平视(用户仍嫌啰嗦)→ **完全去掉角度描述**(2026-08-28 定案):交由模型平视先验自由构图,目标自然落在画面中部/上部;模板只靠「{species}离镜头很远,只是很小的{tone}轮廓」(物种名替代泛称「目标」,不限定鸟类——物种池含兔子等非鸟物种;`{tone}` 轮廓色词按 `imageGen.speciesTone.<物种>` 取,默认深色剪影,**白雉鸡配浅色**——白羽个体用「深色轮廓」描述会与模型渲染矛盾,2026-08-28)维持远景感,手填提示词仅追加数量约束;**2026-08-31 改版(最终定案)**:镜头词全删(广角/长焦实测均失败);「{distanceWord}有且仅有」→「在{distanceWord}的地平线附近有且仅有」;{action} 从句末主体降级至遮挡之后(动作=叙事焦点=视觉焦点,弱化动作降低主体视觉权重);{sizeHint} 由「像素数+占比」改「画面占比分级尺寸词(微小几乎难辨/很小/较小/中等/较大)+占比」(量化像素数与固定物体类比词针尖/米粒/拳头均不跨物种/距离通用,用户否决);**远景小目标模板 `promptTemplateTiny`**:占比 <2% 时切换,动作/遮挡/性别细节全删(保留细节描写迫使模型画大);不合格丢弃重生成,单张上限 3 次(`consts.GenValidateMaxAttempts`),连续不合格整体报错(已合格张数保留)
|
||||
|
||||
### 生成参数存储于数据集表(2026-08-28)
|
||||
|
||||
|
||||
@@ -206,26 +206,35 @@ function startLabelTask() {
|
||||
ElMessage.success('已发起预标,进度见页顶进度条')
|
||||
pollLabelTask(data.id)
|
||||
})
|
||||
.catch(() => {})
|
||||
.catch((e) => {
|
||||
const msg = e?.message || e
|
||||
if (msg !== 'cancel' && msg !== 'close') ElMessage.error(`预标发起失败:${msg}`)
|
||||
})
|
||||
}
|
||||
|
||||
// VLM 藏匿位补检(「补标」按钮,勾选驱动批量执行):排除已确认框,按环境/光线/习性
|
||||
// 找可能藏匿处,逐张追加疑似框(黄框,人工确认);依赖已确认框作为排除基准
|
||||
const vlmReviewing = ref(false)
|
||||
const vlmProgress = ref({ done: 0, total: 0 })
|
||||
async function vlmBatchReview() {
|
||||
if (!selected.value.length) return
|
||||
const images = (detail.value.images || []).filter((it) => selected.value.includes(it.filename))
|
||||
if (!images.length) return
|
||||
vlmReviewing.value = true
|
||||
vlmProgress.value = { done: 0, total: images.length }
|
||||
try {
|
||||
let added = 0
|
||||
let note = ''
|
||||
for (const item of images) {
|
||||
const m = meta(item)
|
||||
if (!m.id) continue
|
||||
if (!m.id) {
|
||||
vlmProgress.value.done++
|
||||
continue
|
||||
}
|
||||
const d = await request.post('/datasets/images/vlm-review', { datasetId: datasetId.value, imageId: m.id }, { timeout: 300000 })
|
||||
added += d.added || 0
|
||||
note = d.note || note
|
||||
vlmProgress.value.done++
|
||||
}
|
||||
if (added > 0) {
|
||||
ElMessage.success(`补标新增 ${added} 个疑似框(黄框,请人工确认)`)
|
||||
@@ -235,6 +244,8 @@ async function vlmBatchReview() {
|
||||
await loadAll()
|
||||
// 标注弹窗开着时刷新当前图的标注
|
||||
if (editorVisible.value) nextTick(resetEditor)
|
||||
} catch (e) {
|
||||
ElMessage.error(`补标失败:${e?.message || e}`)
|
||||
} finally {
|
||||
vlmReviewing.value = false
|
||||
}
|
||||
@@ -770,6 +781,13 @@ onBeforeUnmount(() => {
|
||||
<div v-if="labelTask && labelTask.status === 'done' && labelTask.error" class="label-error">
|
||||
自动标注未完成:{{ labelTask.error }}
|
||||
</div>
|
||||
<div v-if="vlmReviewing" class="label-progress">
|
||||
<el-progress
|
||||
:percentage="vlmProgress.total ? Math.min(100, Math.round((vlmProgress.done / vlmProgress.total) * 100)) : 0"
|
||||
:stroke-width="10"
|
||||
/>
|
||||
<span class="label-progress-tip">补标中 {{ vlmProgress.done }}/{{ vlmProgress.total }} 张(VLM 藏匿位补检)…</span>
|
||||
</div>
|
||||
<div v-if="genRunning" class="label-progress">
|
||||
<el-progress
|
||||
:percentage="genTask.total ? Math.min(100, Math.round((genTask.done / genTask.total) * 100)) : 0"
|
||||
|
||||
Reference in New Issue
Block a user