This commit is contained in:
2026-09-13 00:25:08 +08:00
parent cc42045734
commit 5a97655646
13 changed files with 200 additions and 34 deletions
+2 -9
View File
@@ -211,15 +211,8 @@ func (d *datasetImageDao) CountByDataset(ctx context.Context, datasetId int64) (
return int64(n), err
}
// UpdateLabels 覆写标注 JSON(整图粒度;空串=清空标注;AI 自动标注与人工保存共用)
func (d *datasetImageDao) UpdateLabels(ctx context.Context, id int64, labelsJson string) error {
_, err := g.DB().Model(consts.TableDatasetImage).Ctx(ctx).Where("id", id).
Data(g.Map{"labels_json": common.NilIfEmpty(labelsJson)}).Update()
return err
}
// UpdateLabelsAndReview 覆写标注 JSON 并同步审核状态(预标/AdminLabelSave/App 提交共用):
// 预标检出/人工保存/App 提交 → 1;人工清空保存 → 0(回到未标注池)
// UpdateLabelsAndReview 覆写标注 JSON 并同步审核状态(预标/VLM 补标/AdminLabelSave/App 提交共用)
// 预标检出/VLM 追加疑似框/App 提交 → 1(含疑似待人工过目);管理端人工保存全部框 class 0→2、仍含疑似保持 1、空框→0(回到未标注池)
func (d *datasetImageDao) UpdateLabelsAndReview(ctx context.Context, id int64, labelsJson string, reviewStatus int) error {
_, err := g.DB().Model(consts.TableDatasetImage).Ctx(ctx).Where("id", id).
Data(g.Map{"labels_json": common.NilIfEmpty(labelsJson), "review_status": reviewStatus}).Update()
+16 -6
View File
@@ -239,7 +239,8 @@ func (s *labelTaskService) runDetection(ctx context.Context, taskId int64, datas
return
}
// 全部成功:Serial 内逐张覆写标注(重跑覆盖该图标注);
// 有检出框 → 待审核(1),空检出 → 未标注(0)(无框可审,等人工画框)
// 有检出框 → 待审核(1),空检出 → 未标注(0)(无框可审,等人工画框)
// 含疑似框(class 1)同此口径——自动产出的疑似框必须人工过目(2026-09-12)
err := common.Serial().Submit(bgCtx, func() error {
for i := range results {
raw, jErr := json.Marshal(results[i])
@@ -478,14 +479,15 @@ func (s *labelTaskService) AdminImageVlmReview(ctx context.Context, req *dto.Adm
res.Note = "VLM 未给出有效的藏匿位坐标"
return res, nil
}
// 追加为疑似框并落库(与 RF-DETR/人工框同层,工作台可编辑)
// 追加为疑似框并落库(与 RF-DETR/人工框同层,工作台可编辑)
// 自动链路写入疑似框(class 1)必须人工过目:整图回待审核(2026-09-12 定案)
appended := append(existing, res.Boxes...)
if err := common.Serial().Submit(ctx, func() error {
rawJson, jErr := json.Marshal(appended)
if jErr != nil {
return jErr
}
return dao.DatasetImage.UpdateLabels(ctx, img.Id, string(rawJson))
return dao.DatasetImage.UpdateLabelsAndReview(ctx, img.Id, string(rawJson), consts.ReviewImagePending)
}); err != nil {
return nil, err
}
@@ -577,8 +579,9 @@ func (s *labelTaskService) AdminLabelWorkbench(ctx context.Context, req *dto.Adm
}, nil
}
// AdminLabelSave 保存单张图标注:校验坐标 → json.Marshal 覆写 labels_json(空框=清空),
// 并刷新数据集 labeled_countlabels_json 非空数组的图片数)。
// AdminLabelSave 保存单张图标注:校验坐标 → json.Marshal 覆写 labels_json(空框=清空回未标注),
// 有框全部 class 0 → 已审核(2),仍含 class 1 疑似框 → 保持待审核(1);
// 并刷新数据集 labeled_countreview_status=2 计数)。
func (s *labelTaskService) AdminLabelSave(ctx context.Context, req *dto.AdminLabelSaveReq) (*dto.AdminLabelSaveRes, error) {
dataset, err := dao.Dataset.GetById(ctx, req.DatasetId)
if err != nil {
@@ -599,7 +602,8 @@ func (s *labelTaskService) AdminLabelSave(ctx context.Context, req *dto.AdminLab
return nil, gerror.New("标注框坐标非法(需 0~1 归一化)")
}
}
// 空框 = 清空标注;人工保存即审核定稿(有框→2 已审核,空框→0 未标注回池)
// 空框 = 清空标注回未标注(0);有框全部 class 0 才算人工定稿(2),
// 仍含 class 1 疑似框 → 保持待审核(1)(2026-09-12 用户定案:疑似须逐框确认或删除)
raw := ""
reviewStatus := consts.ReviewImageNone
if len(req.Boxes) > 0 {
@@ -609,6 +613,12 @@ func (s *labelTaskService) AdminLabelSave(ctx context.Context, req *dto.AdminLab
}
raw = string(b)
reviewStatus = consts.ReviewImageApproved
for _, box := range req.Boxes {
if box.Class != 0 {
reviewStatus = consts.ReviewImagePending
break
}
}
}
var labeled int64
if err := common.Serial().Submit(ctx, func() error {
+3
View File
@@ -17,6 +17,9 @@ database:
user: "259278618@qq.com"
pass: "Tongli686^*^"
name: "animal_spot"
# SQL 控制台日志(2026-09-12):每条执行的 SQL(查询/写入/建表/迁移/回刷)自动打印控制台,
# 含耗时与影响行数;本地与服务器 docker logs 行为一致。见 技术设计.md「技术栈与分层」
debug: true
cache:
# 查询缓存 TTL(秒)
ttl: 30
+2 -1
View File
@@ -10,6 +10,7 @@ Go + GoFrame(分层规范见 `CLAUDE.md`):`controller → service → dao`
- service`biz/service`,订单创建、回调落授权、授权查询
- dao`biz/dao`,单表 CRUD,无业务逻辑
- 并发:回调处理为 IO 任务,可走 `common` 池;SQLite 写一律回主 goroutine 串行(无 WAL
- **SQL 控制台日志(2026-09-12 用户要求「执行 SQL 必须输出控制台日志」)**:`config.yml` `database.default.debug: true`——GoFrame 自动把每条执行的 SQL(查询/写入/建表/索引/迁移/回刷)打印到控制台,格式含耗时、影响行数、完整语句(参数已内联);本地与 Docker(docker logs)行为一致,新增 SQL 执行点无需手工补日志。注意日志会包含参数值(如手机号),生产环境按需知悉
## 套餐配置与数据表 DDL
@@ -408,7 +409,7 @@ training:
- `localAi` 配置节点:键与取值以 config.yml 为准,文档不复制具体数值(曾多次调整阈值致双源漂移,2026-09-02 起删除此类描述)——**部署前提:RF-DETR 服务须从 Go 服务器可达**(现跑在 Mac 上,部署时搬服务器/训练机;**未配置时图片上传/生成接口直接报错**——标注是强语义,不允许产生无标注图片;配置调不通则任务 failed,页顶错误条展示,不自动重试);**端点为配置唯一来源,无运行时覆盖**
- **预标注自动触发(2026-08-26 决策,无手动按钮)**:手动上传/AI 生成图片入库成功后,自动对**本次新增图**发起标注;**`localAi` 未配置 → 上传/生成接口直接报错**(同步检查,生成场景在第一张生成前检查避免付费资产生成后标不了);**已有 running 标注任务(忙)→ 不报错**:上传/生成照常成功,新图由「任务成功完成后自动补标未标注图」机制兜底(每轮任务成功结束时检查该数据集**从未扫描过**的图(labels 为 null/''),有则自动续一轮——失败任务不续,防配置坏时无限重试;**2026-09-02 空检出图不入自动补标池**:检出为空的图写 `labels_json='[]'` 后仍被视作未标注(管理端未标注 tab 照常展示等人工画框),但不入自动池——空检出图若留池,每轮自动续一轮只会扫到别的空图、扫完又轮回来:RNPHE_202(阈值调高后 0 检出,低阈下 12 个候选全为噪声,无一过丢弃线)单张被上一版「排除本轮已扫图」恰好挡住未暴露,2026-09-02 兔子批量 3 张空检出实测 {9}→{10,17}→{9}… 无限交替、每轮白打一次 RF-DETR 方暴露;现自动链条只扫从未扫描的图,每张图至多被自动扫一次,空检出图确需重扫由人工触发(按文件名批量/全量任务,见下));service 逐张调 local-ai 推理(common 池并行,`label_task` 记录 total/done 进度)→ 扫描结果(YOLO 归一化 xywh + 置信度 + 建议类别)**直写 `dataset_image.labels_json`(重跑覆盖该图标注)**;**置信度分档(2026-09-02 起按两档语义调定,具体数值以 config.yml 为准不在此复制)**conf ≥ confConfirmed 落 class 0 真目标,threshold ≤ conf < confConfirmed 落 class 1 疑似待人工复核,低于 threshold 不入库;背景(RNPHE_257 实测):COCO 通用 rfdetr 对类鸟纹理误检置信度可达 0.46~0.5 且与真目标仅差 ~0.1(真目标 0.562、两误检 0.477/0.462 曾全落 class 0),低置信段是纯噪声候选(该图 21 个原始检出中 17 个落此区间);单阈值无法分离真/假目标(类别名也无用——全部输出 "bird");误检落 class 0 会作为雉鸡正样本直接污染训练集,危害大于真目标降级疑似(后者人工一键确认即可);**重叠去重(2026-08-26 修订)**:NMS 风格、按置信度降序依次保留,与已保留框**重叠比(minIoU = 交叠面积/两框较小面积)> `localAi.overlapThreshold`**的框剔除(同目标被重复检出只留置信度最高者,跨 class 去重;人工画框不参与去重)——**用 minIoU 而非 IoU**RF-DETR 对同一目标常输出一大一小两个框(标准 IoU 仅 0.3~0.5 会漏杀,实测红黄重叠即此形态),大框套小框时小框被覆盖比例高,minIoU 命中;相邻目标两框互有外露,minIoU 通常 < 0.3;**全图扫描**(项目既定规则:不套用生成规格的位置裁剪,候选宁多勿漏);**单图框数上限(2026-09-02,定义于 consts.go `consts.MaxPrelabelPerImage`)**:去重后仍超上限按置信度取前 N(曾按生成声明数 animal_count 裁剪致漏目标——声明 1 个不代表图内只有 1 个;手动上传图同限);不设候选确认两阶段(2026-08-26 撤销 candidates_json);`POST /admin/label-tasks` 接口保留(服务端 `filenames` 参数支持指定图批量重标,2026-09-02 前端入口收敛后仍可全量——不传 filenames 即全量扫描)
- **预标注四级漏斗(2026-09-04,树栖藏匿难例)**:树冠藏匿生成图(树栖物种目标仅占画面高 ~2%)在全图扫描下不可检——模型包固定 700x700 stretch 输入(见「预标注模型固定 700x700」,不支持动态分辨率,提高提交分辨率无增益),占图高 2% 的目标等比缩入后仅 ~15px 落在检出盲区。单张处理升级为逐级漏斗,**检测只信 RF-DETR,VLM 永不直接出框(只提议候选区)**:①**全图扫描**(现有 `Detect``localAi.threshold`)有候选即收(绝大多数图到此命中,不触发后级);②空检出 → **切片扫描**`common.TileRegions` 滑窗切块(块长边/重叠比 = `localAi.tileSize`/`tileOverlap`,边缘块贴边收口保证全覆盖)逐块 `DetectRegion` 按低阈值(`localAi.tileThreshold`,小目标置信度天然偏低,量级取 RF-DETR-S 实测经验;低于全图丢弃线的候选仍入库落疑似 class 1,宁多勿漏)检测,块内坐标在 DetectRegion 内映射回原图像素,跨块重复检出由既有 minIoU 去重兜住;③仍空 → **VLM 提议候选区**qwen3-vl-8b 按「宁可指错不可遗漏」提示词输出 ≤`consts.VlmLocateMaxRegions` 个归一化候选区(与手动藏匿位补检不同:此阶段图内无已确认框,无排除集;物种注入同 vlm-review——`gen_species` 空回退数据集名),逐区扩大 `consts.VlmRegionExpand` 倍(VLM 坐标偏粗,扩大给 RF-DETR 足够上下文,钳制图片边界)后 `DetectRegion` 精修,精修命中的框才采纳、未命中丢弃(VLM 误报被 RF-DETR 否掉);**VLM 失败/超时/输出非法一律跳过该级不阻塞任务**(三模型共存 + local-ai 服务端排队串行,无需显存互斥检查,2026-09-02 已放开);④全空 → 写 `'[]'`(语义不变:未标注 tab 展示等人工画框、不入自动补标池防乒乓)。漏斗各级候选同通道处理:conf ≥ confConfirmed 落 class 0、否则 class 1、minIoU 去重、单图上限 `consts.MaxPrelabelPerImage`,全图扫描不套生成规格的项目规则不变。单图超时 120s → `consts.LabelImageTimeoutSec`(切片 ~20 块串行 + VLM 兜底;切片在单图池任务内串行执行,不嵌套协程池)
- **标注审核状态机(2026-09-04**`dataset_image.review_status`EnsureColumn 幂等补列):`0=未标注, 1=待审核, 2=已审核`;存量迁移——labels_json 非空数组→2(既有标注视为已审定稿)、null/''/'[]'→0。流转:预标注完成→1(**空检出保持 0**——无框可审,「确认无动物」应由人工画框/清洗表达,避免空图混入已审核口径)、App 用户提交→1、管理端工作台保存(AdminLabelSave)→2、审核「通过」→2、「拒绝」→ 清 labels_json + 回 0(图重新进任务池);**疑似框语义(2026-09-07 用户定案)**:审核「通过」= 人工背书,**全部框原样保留进已标注**(含 class 1 疑似框——suspect 是合法的第二训练类别),通过不再回退未标注;存量一次性清理(已执行 2026-09-07):仅疑似框且未经人工确认的已审核图清空回未标注(兔子 781 等,混合标注图不动);**训练集打包(prepareYoloSet)只收 review_status=2**(标签质量闸门,原「labels_json 非空即收」作废);管理端图片 tab 由三变四:未标注(0)/待审核(1)/已标注(2)/已清洗,另加「标注任务」「标注记录」两个管理面板 tab(同在详情页,无独立菜单,2026-09-07 用户定案)——**任务面板=任务图集网格**(任务即勾选下发的具体图片,选几张展示几张,每张角标=任务 id+状态;头部仅任务概要条与停用;下发入口在未标注 tab 勾选图片顶栏,2026-09-07 修订),记录面板=本数据集众包记录查询/解冻(两列表接口支持 `datasetId` 过滤;`annotate_record` 领取时冗余 `dataset_id` 列,单表过滤不跨表);labeled_count 与 dataset.status='labeled' 口径同步改为 review_status=2 计数(2 必然非空框,无歧义)
- **标注审核状态机(2026-09-04**`dataset_image.review_status`EnsureColumn 幂等补列):`0=未标注, 1=待审核, 2=已审核`;存量迁移——labels_json 非空数组→2(既有标注视为已审定稿)、null/''/'[]'→0。流转:预标注完成→1(**空检出保持 0**——无框可审,「确认无动物」应由人工画框/清洗表达,避免空图混入已审核口径)、App 用户提交→1、管理端工作台保存(AdminLabelSave)→ 有框全 class 0 置 2、仍含 class 1 疑似保持 1、空框置 0、审核「通过」→2、「拒绝」→ 清 labels_json + 回 0(图重新进任务池);**疑似框语义(2026-09-07 用户定案)**:审核「通过」= 人工背书,**全部框原样保留进已标注**(含 class 1 疑似框——suspect 是合法的第二训练类别),通过不再回退未标注;存量一次性清理(已执行 2026-09-07):仅疑似框且未经人工确认的已审核图清空回未标注(兔子 781 等,混合标注图不动);**训练集打包(prepareYoloSet)只收 review_status=2**(标签质量闸门,原「labels_json 非空即收」作废);管理端图片 tab 由三变四:未标注(0)/待审核(1)/已标注(2)/已清洗,另加「标注任务」「标注记录」两个管理面板 tab(同在详情页,无独立菜单,2026-09-07 用户定案)——**任务面板=任务图集网格**(任务即勾选下发的具体图片,选几张展示几张,每张角标=任务 id+状态;头部仅任务概要条与停用;下发入口在未标注 tab 勾选图片顶栏,2026-09-07 修订),记录面板=本数据集众包记录查询/解冻(两列表接口支持 `datasetId` 过滤;`annotate_record` 领取时冗余 `dataset_id` 列,单表过滤不跨表);labeled_count 与 dataset.status='labeled' 口径同步改为 review_status=2 计数(2 必然非空框,无歧义);**疑似框必须人工过目(2026-09-12 用户定案)**:自动链路产出的标注只要含 class 1 就整图回待审核——预标注(原即有框→1)与 VLM 补标追加疑似框(改为 `UpdateLabelsAndReview` 置 1,原只改标注不改状态);人工动作仍可放行(**工作台保存定稿门槛 2026-09-12 用户定案:有框且全部 class 0 才置 2,仍含 class 1 疑似框保存后保持 1 待审核**——疑似须逐框「确认」成 class 0 或删除后才能定稿;审核「通过」维持 09-07 全框原样保留语义);存量一次性回刷(已执行 2026-09-12):`UPDATE dataset_image SET review_status = 1 WHERE review_status <> 1 AND labels_json @> '[{"class":1}]'::jsonb`(本地库 1400 行:环颈雉 141/兔子 427/斑鸠 465/鹌鹑 327/鸭子 40;**刻意不做启动自动迁移**——人工放行图仍含 class 1,启动刷会把人工背书反复冲掉);**补充回刷(2026-09-13,本次门槛定案后复查)**:再得 5 行全疑似框却已标注的遗留图(RNPHE_2059/2061、quail_1160/1162/1163,旧保存逻辑/通过遗留),同口径置 1,复查全库含 class 1 行已无 status=2**保存后本地审核态同步(2026-09-12 用户实测定位)**:管理端工作台「确定」保存成功后前端同步该图本地 review_status(空框→0;有框全 class 0→2;仍含 class 1 疑似框→保持 1 待审核,与服务端 AdminLabelSave 一致),列表分类与 tab 计数即时更新——原先保存只更新本地框数据不改分类,保存过的图滞留旧 tab、计数虚高,直到下一次 loadAll(如随后的删除操作触发)才批量重分类,表现为「删一条却掉多条」的误判(数据库实际只删选中行,删除请求体外恒只含勾选 id)
- **自动标注退场(2026-09-04 用户定案)**:删除上传入库与生成完成(genTaskAutoLabel)两处 AutoLabel 触发及 autoSupplement 自动补标机制(补标池/空检出乒乓问题随机制一并消亡);**`localAi` 未配置不再阻断上传/生成**(标注改纯管理端手动动作,「不允许产生无标注图」强语义随自动标注退场删除);标注唯一入口 = 管理端勾选图片「预标」(AdminStartLabelTask 保留)
- **App 标注众包与时长激励(2026-09-04;图片粒度下发 2026-09-07 用户定案)**:App 用户处理标注任务换取使用时长,全链路进待审核,服务端记录每用户每张图的处理明细。**下发粒度(2026-09-07 修订)**:任务 = 管理端**勾选的具体图片集合**(不再是数据集级动态池)——`dataset_image.annotate_task_id`EnsureColumn 幂等补列,默认 0=未下发)作占用标记:下发时 Serial 内批量写占用(校验属本数据集/未标注/未占用),**已下发图即从管理端「未标注」tab 消失**tab 只显示 annotate_task_id=0);领取池 = 本任务占用且 review_status=0 且 clean_excluded=0 的图;停用任务时**释放未被领取过的图**(清 annotate_task_id,回到未标注 tab 可再下发),已领取未提交的可继续提交(有 annotate_record 的图不释放)。**表**`annotate_task`(下发任务:dataset_id/name/status published|stopped/created_at);`annotate_record`(用户标注记录:phone_num/task_id/image_id/labels_json 提交快照/status pending|submitted|approved|rejected/created_at/submitted_at/reviewed_at——pending 即领取锁(partial unique index 同图仅一条 pending,超时在领取时惰性释放;**并发防重**:挑图 + 插锁全程在 Serial 临界区内串行执行,并发领取时后读方一定看到先读方刚插入的锁,同一张图不会分给多人,索引为跨实例兜底),UNIQUE(phone_num,image_id) 一人一图仅一次(拒后图仍在本任务池内可被他人再标),拒绝不删记录=惩罚统计依据);`reward_log`(发放流水:phone_num/minutes/task_id/granted_at——日上限按自然日求和);`license.annotate_frozen_until` + `license.annotate_stats_since`EnsureColumn):冻结到期标记(>now 即冻结,时间戳对比天然自动解冻,无定时任务)与统计基线(冻结时重置,实现解冻后重新累计)。**规则**:领取 = 每次 `annotateReward.claimSize` 张(池内排除该用户已有 annotate_record 的图,冻结中/停用不可领);提交 = 框写 labels_json + review_status=1 + 记录置 submitted,每累计 `perImages`(10) 张 submitted 发 `minutes`(30) 分钟(license.expires_at = max(now,expires_at)+30min 分钟级顺延),当日已发 +30 > `dailyCapMinutes`(120) 则本次不发(App 展示今日已达上限);审核 = 管理端批量通过/拒绝,每次审核后重算该用户通过比例(reviewed_at ≥ stats_since 的 approved/(approved+rejected)),已审核样本 ≥ `minReviewed`(5) 且比例 < `freezeRatio`(0.8) → annotate_frozen_until=now+`freezeHours`(24h) 且 stats_since=now(惩罚不追溯扣已到账时长)。**接口**:admin 下发(传 `imageIds` 勾选图)/列表/停用任务、批量审核(通过/拒绝)、用户标注记录分页;App(/api/v1) 任务列表(含我的统计/冻结态)、领取、提交、我的统计——App 契约不变(仍按 taskId 领取),仅池语义变为任务占用集。**配置**:`config.yml annotateReward` 节点(数值以 config.yml 为准不在此复制),阈值类缺失回退 consts 默认
- **手动标注入口收敛(2026-09-02 决策)**:详情页删除「全量标注」按钮与图片卡片「审核」按钮,**标注入口仅两处(勾选图片驱动,顶栏)**:①「预标」= POST `/admin/label-tasks` 传选中图 `filenames`(只扫选中图,覆盖各图已有标注);②「补标」= 对选中图逐张 POST `/datasets/images/vlm-review`(VLM 藏匿位补检,追加疑似框,批量=前端循环单图接口,依赖已确认框作为排除基准)——VLM 补检按钮从标注编辑器迁出(2026-09-02);点卡片缩略图仍进人工编辑器(确认/清理/画框,编辑器内不再有 VLM 按钮),编辑器文案「图片审核」→「图片标注」;服务端全量重标能力保留但不再有 UI 入口