feat: 完善标注删除、AI 框选与视频传播交互

功能增加:

- 在工作区增加按范围传播和传播全部可达入口,支持选中区域或当前帧全部 mask 作为 seed,并按前后帧范围调用 SAM2 传播后刷新已保存标注。

- 在 AI 智能分割中接入框选提示,支持 box prompt 以及 box + 正/反向点的 interactive prompt 细化流程。

- 在 AI 智能分割中增加提示点删除、最近锚点删除、清空锚点、选中 AI 候选删除和 Delete/Backspace 快捷删除。

- 在项目库删除项目后同步清理当前项目、帧、mask 与选区状态,避免删除后工作区残留旧数据。

- 将时间进度条上的已编辑帧提示改为覆盖在进度条上的琥珀色竖线,并保留已编辑帧计数。

- 将 AI 参数文案调整为局部专注模式(自动裁剪无锚区域)和严格除杂模式(自动清理干涉点),仅改善可读性,不改变内部字段。

Bugfix:

- 修复 AI 框选工具无实际 prompt 输出的问题。

- 修复多次执行 AI 高精度语义分割时旧候选 mask 叠加显示的问题,改为替换本页 AI 候选。

- 修复删除 AI 候选后选区仍引用已删除 mask 的状态残留。

- 修复进度条当前帧提示与已编辑帧提示颜色/语义混淆的问题,当前帧继续由播放进度和缩略图高亮表达。

测试与文档:

- 补充 AI 分割框选、候选替换、提示点删除和快捷删除相关测试。

- 补充工作区传播范围、传播全部可达、编辑区域删除和项目删除状态清理测试。

- 更新 README、AGENTS 和 doc 下需求冻结、设计冻结、接口契约、前端审计、实施计划、测试计划,记录当前真实功能和测试覆盖。
This commit is contained in:
2026-05-02 00:56:13 +08:00
parent 29a1a87e52
commit 4c21de02f8
20 changed files with 929 additions and 132 deletions

View File

@@ -26,7 +26,7 @@
| 模型状态 | `src/components/ModelStatusBadge.tsx` | 展示 GPU 与当前 SAM 模型真实可用状态 |
| 登录页 | `src/components/Login.tsx` | 调用登录 API写入 store |
| Dashboard | `src/components/Dashboard.tsx` | 展示统计、任务控制、失败详情和 WebSocket 进度消息 |
| 项目库 | `src/components/ProjectLibrary.tsx` | 项目列表、新建、导入视频/DICOM、显式生成帧 |
| 项目库 | `src/components/ProjectLibrary.tsx` | 项目列表、新建、删除、导入视频/DICOM、显式生成帧 |
| 工作区 | `src/components/VideoWorkspace.tsx` | 加载帧和模板组织工具栏、Canvas、本体面板、时间轴 |
| Canvas | `src/components/CanvasArea.tsx` | 显示帧、缩放平移、点/框提示、渲染 mask |
| 工具栏 | `src/components/ToolsPalette.tsx` | 切换工具、跳转 AI 页面、触发 mask 撤销/重做 |
@@ -103,7 +103,7 @@
3. 帧数据映射为 store `Frame[]`,包含 `timestampMs``sourceFrameNumber`,供时间轴和后续视频传播使用。
4. 工作区调用 `GET /api/ai/annotations` 回显已保存标注时,会替换当前项目帧中的已保存 mask但保留没有 `annotationId` 的未保存 draft mask这保证 AI 页推送到工作区的候选 mask 不会被异步回显覆盖,并会在合并完成后恢复仍然存在的已选 mask id。
5. `CanvasArea` 会把全局 `selectedMaskIds` 中仍存在于当前帧的 id 同步回本地选区,避免帧初始化时的临时清空覆盖 AI 页推送过来的选中态。
6. `FrameTimeline` 根据当前项目 `frames` 和全局 `masks` 计算有编辑/标注的帧,在进度条与缩略图导航轴之间渲染可点击标记
6. `FrameTimeline` 根据当前项目 `frames` 和全局 `masks` 计算有编辑/标注的帧,并在顶部时间进度条上覆盖可点击的琥珀色竖线;当前帧不额外渲染竖线,由播放进度条末端、时间提示和缩略图高亮表达
7. 当前帧传入 `CanvasArea`
### AI 点/框推理
@@ -116,24 +116,32 @@
6. 后端加载帧图片并通过 SAM registry 分发到所选 SAM 2.1 变体;`model=sam2` 会兼容归一化为 tiny`model=sam3` 会被拒绝。
7. 前端把 `polygons` 转为 mask交互式细化会替换同一个候选 mask而不是新增多个 mask。
8. 若带反向点的 SAM 2.1 细化返回空结果,前端会删除当前旧候选 mask 并提示反向点已排除该区域。
9. AI 页面只按本页生成的候选 id 渲染 mask不把工作区已有 mask 带入 AI 画布。
9. AI 页面只按本页最新生成的候选 id 渲染 mask不把工作区已有 mask 带入 AI 画布;每次 `runInference()` 都先过滤掉旧 `aiMaskIds` 对应候选,再写入本次最高分候选
10. AI 页面候选 mask 的 Path 点击事件会先判断当前工具;正向/反向选点工具下点击 mask 会继续追加提示点,其他工具下才选中 mask。
11. AI 页面“遮罩清晰度”滑杆只调节候选 mask 的 Konva preview opacity不写入 `Mask.segmentation`、分类元数据或后端 payload
12. Canvas 按当前帧过滤并渲染 mask
13. 新 mask 会带上当前选择的模板分类元数据,包括 `classId``className``classZIndex``metadata.source=ai_segmentation` 和保存状态 `draft`
14. 用户点击“结构化归档保存”后,前端将像素 `segmentation` 转成 normalized `mask_data.polygons`;未保存 mask 调用 `POST /api/ai/annotate`dirty mask 调用 `PATCH /api/ai/annotations/{annotation_id}`;保存成功后本次提交的 draft mask id 会从本地保留列表中排除,并由后端 saved annotation 回显替换
15. 工作区加载项目帧后通过 `GET /api/ai/annotations` 取回已保存标注并转成前端 mask。
16. 工作区“清空遮罩”删除当前帧已保存标注,并清除当前帧本地 mask
11. 工作区 SAM 提示点由 `CanvasArea` 本地 `points` 状态维护;点击已渲染提示点会先 `cancelBubble`,再删除对应点并按剩余提示重新调用 `runInference()`,避免同一次点击继续触发 Stage 加点或 Path 选择
12. AI 页面边界框选由 `promptBox/boxStart/boxCurrent` 维护;拖拽时渲染蓝色虚线框,鼠标释放后固化 `promptBox` 并清空旧提示点,避免旧点误绑定到新框
13. AI 页面执行分割时,如果只有 `promptBox` 则发送 `box` prompt如果 `promptBox``points` 同时存在,`predictMask()` 会发送 interactive prompt
14. AI 页面提示点由本地 `points` 状态维护;点击已渲染提示点会按 index 删除对应点,“删除最近锚点”会删除数组最后一个点,不改动候选 mask 列表
15. AI 页面候选 mask 删除只接受当前 `aiMaskIds` 范围内的已选 id“删除选中候选”和 Delete/Backspace 都复用该范围过滤,避免删除工作区已有 mask。
16. AI 页面参数开关文案只做展示增强:“局部专注模式(自动裁剪无锚区域)”仍控制 `cropMode/crop_to_prompt`,“严格除杂模式(自动清理干涉点)”仍控制 `autoDeleteBg/auto_filter_background/min_score`
17. AI 页面“遮罩清晰度”滑杆只调节候选 mask 的 Konva preview opacity不写入 `Mask.segmentation`、分类元数据或后端 payload。
18. Canvas 按当前帧过滤并渲染 mask。
19. 新 mask 会带上当前选择的模板分类元数据,包括 `classId``className``classZIndex``metadata.source=ai_segmentation` 和保存状态 `draft`
20. 用户点击“结构化归档保存”后,前端将像素 `segmentation` 转成 normalized `mask_data.polygons`;未保存 mask 调用 `POST /api/ai/annotate`dirty mask 调用 `PATCH /api/ai/annotations/{annotation_id}`;保存成功后本次提交的 draft mask id 会从本地保留列表中排除,并由后端 saved annotation 回显替换。
21. 工作区加载项目帧后通过 `GET /api/ai/annotations` 取回已保存标注并转成前端 mask。
22. 工作区“清空遮罩”删除当前帧已保存标注,并清除当前帧本地 mask。
### 视频片段传播
1. 用户在工作区选中一个当前帧 mask;如果未显式选中,前端使用当前帧第一个 mask。
2. `VideoWorkspace``buildAnnotationPayload()` 把 seed mask 转成 normalized polygon、bbox、label、color 和 class 元数据
3. 前端调用 `POST /api/ai/propagate`,默认 `direction=forward``max_frames=30``include_source=false`
4. 后端按项目帧序列截取片段,下载对应帧到临时 `frame_%06d.jpg` 目录,保持当前帧在片段中的相对索引
5. `model` 为任一 SAM 2.1 变体时,`sam2_engine` 使用对应 checkpoint/config 加载 `SAM2VideoPredictor.add_new_mask()` 注入 seed mask再用 `propagate_in_video()` 传播
6. `model=sam3` 当前不支持SAM 3 video tracker 代码保留但没有接入产品路径
7. 后端把传播返回的 normalized polygon 保存为后续帧 `Annotation`,跳过源帧,`mask_data.source` 记录模型传播来源
1. 用户在工作区选择传播对象:`selected` 表示当前帧已选 mask`all` 表示当前帧所有 mask。
2. 用户设置起止帧;“传播全部可达”会把起止帧设为 1 到项目总帧数
3. `VideoWorkspace` 以当前帧为 seed将起止帧拆成 `backward` 和/或 `forward` 两段;只包含当前帧时不传播
4. `VideoWorkspace``buildAnnotationPayload()` 把每个 seed mask 转成 normalized polygon、bbox、label、color 和 class 元数据
5. 前端对每个 seed、每个方向顺序调用 `POST /api/ai/propagate``include_source=false``save_annotations=true`;顺序调用是为了避免多个视频 tracker 并发抢占 GPU
6. 后端按项目帧序列截取片段,下载对应帧到临时 `frame_%06d.jpg` 目录,保持当前帧在片段中的相对索引
7. `model` 为任一 SAM 2.1 变体时,`sam2_engine` 使用对应 checkpoint/config 加载 `SAM2VideoPredictor.add_new_mask()` 注入 seed mask再用 `propagate_in_video()` 传播
8. `model=sam3` 当前不支持SAM 3 video tracker 代码保留但没有接入产品路径。
9. 后端把传播返回的 normalized polygon 保存为后续帧 `Annotation`,跳过源帧,`mask_data.source` 记录模型传播来源。
8. 前端传播完成后重新调用 `GET /api/ai/annotations` 并回显新标注。
### 手工绘制与历史栈