Files
Pre_Seg_Server/doc/08-current-design-freeze.md
admin f88f9bdbb9 支持中空mask编辑和传播保洞
- 前端按 polygonRingCounts 维护外圈/内洞 ring 分组,中空 mask 在调整多边形时显示内洞顶点和插点手柄。

- 保存与回显标注时将中空结构拆分为 mask_data.polygons 和 mask_data.holes,导入/普通 mask 共享同一编辑体验。

- 自动传播 seed 携带 holes,SAM 2 seed 栅格化时扣除内洞,避免中空 mask 以实心形式传播。

- 传播结果轮廓提取改为保留层级内洞,并在同步传播和 Celery 传播落库时写回 holes 与 hasHoles。

- 传播 seed 签名纳入 holes,并加固保存结果时 holes 与原始 polygon 索引对齐。

- 补充前端保存/回显、Canvas 内洞编辑和后端 SAM 2 hole 处理测试。

- 更新 AGENTS、接口契约、需求冻结、设计冻结和测试计划文档,移除中空结构未实现的旧描述。
2026-05-03 18:28:46 +08:00

46 KiB
Raw Blame History

当前设计冻结文档

冻结日期2026-05-01

本文档描述当前代码结构、数据流、接口契约和测试边界。后续实现如果改变这些设计,应同步更新本文档和测试。

总体架构

当前系统由三层组成:

  • React + TypeScript 前端 SPA。
  • FastAPI 后端 API。
  • PostgreSQL、MinIO、Redis、SAM 2 等外部基础设施。SAM 3 相关源码保留,但当前产品入口禁用。

开发时前端通过 server.ts 启动 Express + Vite middleware后端通过 backend/main.py 启动 FastAPI。前端业务接口访问 FastAPIserver.ts 不再保留旧版 /api/* mock。

前端模块

模块 文件 设计职责
应用入口 src/App.tsx 根据登录状态和 activeModule 切换页面
全局状态 src/store/useStore.ts Zustand store保存项目、帧、模板、mask、当前选中 mask ids、工具状态和 mask 撤销/重做历史栈
API 封装 src/lib/api.ts Axios 客户端、字段映射、AI 响应转换
配置 src/lib/config.ts 推导 API 和 WebSocket 地址
WebSocket src/lib/websocket.ts 进度流连接、订阅、连接状态通知、心跳和重连
模型状态 src/components/ModelStatusBadge.tsx 展示 GPU 与当前 SAM 模型真实可用状态;工作区顶栏使用 compact 形态,只显示 GPU/CPU 状态,具体传播权重由旁边下拉负责
登录页 src/components/Login.tsx 调用登录 API写入 store
Dashboard src/components/Dashboard.tsx 展示统计、任务控制、失败详情和 WebSocket 进度消息
项目库 src/components/ProjectLibrary.tsx 项目列表、新建、重命名、删除、导入视频/DICOM、显式生成帧
工作区 src/components/VideoWorkspace.tsx 加载帧和模板组织工具栏、Canvas、本体面板、时间轴
Canvas src/components/CanvasArea.tsx 显示帧、缩放平移、点/框提示、渲染 mask
工具栏 src/components/ToolsPalette.tsx 切换工作区编辑工具、在“重叠区域去除”后触发 GT Mask 导入、跳转 AI 页面AI 跳转入口复用 Bot + Sparkles 组合图标以明确表达 AI 智能分割;不再放置 AI 正/反点和框选工具,也不重复放置撤销/重做;拖拽/选择到创建圆、画笔/橡皮擦/区域合并/重叠区域去除、导入 GT Mask/AI 智能分割三类工具之间用浅灰横线分隔;紧凑垂直布局,高度不足时自身滚动;外层宽 56px按钮列固定 48px滚动条使用右侧外扩空间和低对比 seg-scrollbar
工作区顶栏 src/components/VideoWorkspace.tsx 保存状态按钮(“保存 X 个改动”/“已全部保存”)、导出/传播/按起止帧批量清空遮罩、显式撤销/重做按钮和工作区快捷键
时间轴 src/components/FrameTimeline.tsx 帧导航、播放进度、视频处理进度条、自动传播历史片段、自动传播/清空遮罩/导出范围选择、左右方向键切帧、播放和当前/总时长显示
本体面板 src/components/OntologyInspector.tsx 模板选择、工作区 mask 透明度、分类树、后端自定义分类、mask 后端属性分析;内容过长时自身滚动,滚动条使用低对比 seg-scrollbar
AI 页面 src/components/AISegmentation.tsx 独立 AI 推理视图,使用当前项目帧
模板库 src/components/TemplateRegistry.tsx 模板 CRUD、分类编辑、导入、详情页和编辑弹窗拖拽排序
短提示浮层 src/components/TransientNotice.tsx 项目库和模板库的非阻塞成功/失败提示,自动消失

后端模块

模块 文件 设计职责
应用入口 backend/main.py FastAPI app、CORS、路由注册、健康检查、WebSocket
配置 backend/config.py Pydantic settings
数据库 backend/database.py SQLAlchemy engine、session、Base
模型 backend/models.py User、Project、Frame、Template、Annotation、Mask、AuditLog、ProcessingTask
Schema backend/schemas.py Pydantic 请求/响应模型
Auth backend/routers/auth.py 用户表、密码哈希、JWT 登录和 /api/auth/me
Admin backend/routers/admin.py 管理员用户 CRUD、角色/密码/启停用和审计日志
Projects backend/routers/projects.py 项目与帧 CRUD
Templates backend/routers/templates.py 模板 CRUD 和 mapping_rules 打包/解包
Media backend/routers/media.py 上传媒体和拆帧
AI backend/routers/ai.py 当前启用 SAM 2 推理、视频传播、模型状态和标注保存
传播任务 backend/services/propagation_task_runner.py Celery 中执行自动传播 steps写任务进度并保存传播标注
Export backend/routers/export.py COCO 和 PNG mask 导出
SAM 2 backend/services/sam2_engine.py SAM 2 懒加载、状态检测、点/框/自动推理和视频 mask 传播
SAM 3 backend/services/sam3_engine.py, backend/services/sam3_external_worker.py, backend/setup_sam3_env.sh 历史保留的 SAM 3 桥接源码和脚本;当前未接入 registry
SAM Registry backend/services/sam_registry.py 当前暴露 SAM 2.1 四个变体、GPU 状态和推理分发

状态模型

前端 store 的核心对象:

  • Project项目基本信息、状态、帧数、fps、媒体路径。
  • Frame:帧 ID、项目 ID、索引、图片 URL、宽高、序列时间戳和原视频源帧号。
  • Template / TemplateClass:模板和分类定义。
  • Mask:前端渲染用 mask包含 pathDatasegmentationbboxarea
  • selectedMaskIdsCanvas 当前选中的 mask id 列表,供右侧本体面板对已选区域直接换标签。
  • maskHistory / maskFuturemask 编辑历史栈,用于撤销和重做。
  • activeModule:当前页面。
  • activeTool:当前工具。
  • aiModel:当前启用的 AI 模型,取值为 sam2.1_hiera_tinysam2.1_hiera_smallsam2.1_hiera_base_plussam2.1_hiera_large,默认 sam2.1_hiera_tiny

关键数据流

登录

  1. Login 收集用户名和密码。
  2. login() 调用 POST /api/auth/login
  3. 后端用 users 表中的密码哈希校验用户,成功后返回签名 JWT 和用户资料。
  4. 前端把 token 写入 localStorage 和 Zustand刷新页面时 useStore 会从 localStorage 恢复 token。
  5. App 在已登录状态调用 /api/auth/me 恢复当前用户,再拉取当前用户项目列表。

用户隔离

  1. Project.owner_user_id 指向 users.id;启动时默认 admin 用户会被创建,历史 owner_user_id IS NULL 的项目会迁移归属到 admin。
  2. 项目、帧、媒体上传/拆帧、AI 标注、传播任务、任务列表、Dashboard 和导出接口都通过当前 JWT 用户过滤项目资源。
  3. Template.owner_user_id 支持用户模板;owner_user_id IS NULL 的模板视为系统模板,可作为默认分类体系对用户可见。
  4. 角色分为 adminannotatorvieweradmin/annotator 可调用写入类业务接口,viewer 只能调用读接口;/api/admin/* 仅允许 admin
  5. UserAdmin.tsx 仅在当前用户角色为 admin 时从 Sidebar 展示,调用 /api/admin/users 完成新增、角色修改、停用/启用、密码修改和删除无项目用户,调用 /api/admin/audit-logs 展示登录和管理操作审计;改密码、删除用户和危险区“恢复演示出厂设置”均使用站内弹窗确认,恢复出厂设置要求输入 RESET_DEMO_FACTORY 后调用 /api/admin/demo-factory-reset
  6. POST /api/admin/demo-factory-reset 仅允许 admin,会重置默认 admin 密码/角色/启用状态删除其它用户、项目、帧、标注、mask、任务、用户模板和旧审计重新创建 settings.demo_video_path 指向的演示视频项目,以及 settings.demo_dicom_dir 指向的演示 DICOM 项目DICOM 会按文件名自然顺序上传和生成帧;系统模板保留以保证重置后仍可标注。
  7. 缺失、过期或伪造的 Bearer token 会在业务路由返回 401权限不足返回 403其他用户项目资源对当前用户表现为 404。

项目导入与生成帧

  1. ProjectLibrary 创建项目。
  2. 导入视频时上传源视频到 /api/media/upload 并关联项目;该步骤不调用 /api/media/parse。上传期间项目库显示导入进度条、百分比和已上传字节,完成后短暂显示“视频导入完成”。
  3. 用户在项目卡片点击“生成帧”,在弹窗中选择目标 FPS。
  4. 前端调用 /api/media/parse 创建异步拆帧任务;可通过 parse_fpsmax_framestarget_width 指定标准帧序列参数。
  5. Celery worker 执行 FFmpeg/OpenCV/pydicom 拆帧DICOM 在前端选择、后端上传、worker 下载和 pydicom 读取时都按文件名自然顺序排序;视频/DICOM 解析结果都按 frame_%06d.jpgframe_000000.jpg 连续命名,视频帧按目标宽度缩放。
  6. worker 写入 frames.timestamp_msframes.source_frame_number,并在任务 result.frame_sequence 中记录 FPS、帧数、时长、尺寸和对象存储前缀。
  7. worker 持续更新 processing_tasks,并发布 Redis seg:progress
  8. 刷新项目列表;项目卡片右上角 FPS 徽标显示生成关键帧序列时选择的 parse_fps,原始视频 FPS 仅作为底部“原 xx fps”辅助信息显示。
  9. 导入视频、生成帧、上传 DICOM 和失败反馈使用 TransientNotice,不再使用浏览器 alert() 阻塞操作;提示默认数秒后自动消失。视频和 DICOM 上传阶段额外显示项目库内的导入进度面板DICOM 面板显示有效文件数量,并在上传完成后切换为解析任务进度,轮询 GET /api/tasks/{task_id} 直到成功、失败或取消。
  10. DICOM 和视频帧序列写入同一 frames 表并共用工作区、时间轴、AI 传播、标注保存、GT 导入和导出链路,差异只存在于项目库导入入口和后端解析器。

任务控制

  1. Dashboard 从 GET /api/dashboard/overview 读取 queued/running/success/failed/cancelled 任务queued/running 代表当前进度success/failed/cancelled 代表最近任务状态。
  2. 用户取消任务时,前端调用 POST /api/tasks/{task_id}/cancel;后端写入 cancelled、设置 finished_at,并尝试 celery_app.control.revoke(..., terminate=True)
  3. worker 在下载、解析、上传、写帧等关键阶段刷新任务状态;如果发现 cancelled,停止后续写入并发布 cancelled 事件。
  4. 用户重试任务时,前端调用 POST /api/tasks/{task_id}/retry;后端基于原任务 payload 创建新任务,记录 retry_of 并重新投递 Celery。
  5. 用户打开详情时,前端调用 GET /api/tasks/{task_id},弹窗展示 error、payload、result、Celery ID 和时间。
  6. Dashboard 通过 /ws/progress 接收 Redis seg:progress 转发事件;前端 WebSocket 客户端在 onopen/onclose/onerror 主动更新连接状态,并定时发送 ping 心跳,服务端返回 status 确认连接仍活跃。

工作区加载

  1. VideoWorkspace 根据 currentProject.id 调用 getProjectFrames()
  2. 若无帧但项目有 video_path,显示“尚未生成帧”的状态提示,不自动触发 parseMedia()
  3. 帧数据映射为 store Frame[],包含 timestampMssourceFrameNumber,供时间轴和后续视频传播使用。
  4. 工作区调用 GET /api/ai/annotations 回显已保存标注时,会替换当前项目帧中的已保存 mask但保留没有 annotationId 的未保存 draft mask这保证 AI 页推送到工作区的候选 mask 不会被异步回显覆盖,并会在合并完成后恢复仍然存在的已选 mask id。
  5. VideoWorkspace 加载项目帧时会优先按当前选中 mask 的 frameId 和当前打开帧 id 恢复 currentFrameIndex;只有没有可恢复帧时才回到第一帧,避免 AI 页在非第一帧推送回工作区时视角被重置。
  6. CanvasArea 会把全局 selectedMaskIds 中仍存在于当前帧的 id 同步回本地选区,避免帧初始化时的临时清空覆盖 AI 页推送过来的选中态;如果切换到另一帧时原 id 不存在,但目标帧存在同一自动传播链的结果,前端会用 source_annotation_idsource_mask_idpropagation_seed_key 匹配对应传播 mask 并自动选中。
  7. CanvasArea 根据容器和帧尺寸按 86% 适配比例计算初始 scale/position使底图默认居中且尽量大但保留画布边距滚轮缩放和拖拽平移仍由用户后续控制。
  8. CanvasArea 未选中特定 mask 时,会按 classZIndex 从低到高渲染当前帧 mask该值来自右侧“语义分类树”的拖拽排序因此高优先级类别会后渲染并覆盖低优先级类别。有选中 mask 时,编辑态可保留选中区域置顶,方便拖点、换类和布尔操作。
  9. FrameTimeline 顶部播放进度条显示当前播放位置;其下方视频处理进度条根据 Mask.metadata.source / propagated_from_frame_id 计算自动传播帧并显示蓝色区段,对人工绘制或 AI 智能分割等非传播 mask 帧显示红色竖线。当前帧另用白色竖线贯穿播放进度条和视频处理进度条,和青色播放进度、红色标注、蓝色传播状态区分。普通状态下,视频处理进度条可点击跳转到对应帧,红色人工/AI 标注帧和蓝色自动传播帧标识本身也可点击跳转。处理条未处理背景使用中性灰,和红色/蓝色标记保持明显区分。VideoWorkspace 会记录当前会话最近 8 次成功处理过的自动传播范围,并通过 propagationHistory 传给 FrameTimeline;时间轴会把这些片段叠加为同一蓝色系的纯色条,按距最新传播的时间顺序逐次变暗,且第 5 次及更早统一为阈值旧记录色,不再在单个片段内部使用渐变。传播历史条只显示当前仍有自动传播 mask 的帧,VideoWorkspace 会在 mask 变化时按剩余传播 mask 裁剪本地传播历史;FrameTimeline 渲染时也会按当前传播 mask 再次拆分/过滤,避免单独删除传播 mask 后空帧仍显示红/蓝颜色。底部缩略图导航轴对非当前帧使用红色边框标识人工/AI 标注帧,使用蓝色边框标识自动传播/推理帧;如果同一帧同时存在人工/AI 标注和自动传播结果,红色人工/AI 标注边框优先保留,自动传播状态只作为蓝色内描边。当前帧使用青色外框高亮优先,若当前帧同时是人工/AI 标注帧,则以青色外框加红色内描边同时表达两个状态,外层当前帧框和内层人工/AI 框的顺序固定。工作区进入自动传播、清空片段遮罩或特定范围帧导出选择模式时,播放进度条和视频处理进度条显示 amber 覆盖层,并额外用洋红色起始线和黄绿色结束线贯穿两条进度条,表达待处理或待导出范围边界,可点击/拖拽设置起止帧。
  10. 当前帧传入 CanvasArea
  11. 工作区顶栏短状态文本会在空闲状态下自动消失;保存、导出、导入 GT 和传播任务运行中仍保留进度状态,无帧项目提示也会保留。
  12. 左侧工具栏和右侧本体/语义分类面板使用 seg-scrollbar 定制纵向滚动条;默认滚动条 thumb 低透明度融入深色背景hover/focus 时增强为青色提示,避免系统默认滚动条在工具区中过于突兀。左侧工具栏额外保留右侧滚动条槽位,按钮列仍按原 48px 布局,避免滚动条和图标抢空间。
  13. 右侧面板不再显示“本体论与属性分类管理树”固定说明栏,直接展示实际可操作内容。
  14. 右侧“遮罩透明度”滑杆写入 Zustand maskPreviewOpacityCanvasAreaAISegmentation 都用该值计算 mask group opacity选中 mask 在基础透明度上加亮或按基础透明度显示,方便保留选中反馈。
  15. Canvas 点击 mask 后,全局 selectedMaskIds 会同步到 OntologyInspector;本体面板按选中 mask 的 classIdclassName/label 和颜色匹配模板分类,自动设置 active class并把分类按钮滚动/聚焦到可见区域。
  16. 工作区顶栏“清空片段遮罩”和“自动传播”共用时间轴范围选择交互;第一次点击“清空片段遮罩”会进入范围选择模式,按钮变为“确认清空”,用户可在播放进度条或视频处理进度条上点击/拖拽选择起止帧;进入清空模式后顶栏显示“清空全部 / 保留人工/AI”两段式模式选择默认“清空全部”。“清空全部”会对范围内已保存 mask 调用 DELETE /api/ai/annotations/{id},同时移除范围内本地 draft mask、被清空的选区和与清空范围重叠的本地传播历史条若范围内存在非自动传播来源的 mask也就是时间轴红色“人工/AI 标注帧”,执行前会显示站内确认弹窗,取消则不删除任何 mask。“保留人工/AI”只删除范围内自动传播/推理 mask不删除人工绘制或 AI 智能分割生成的红色标注帧,不弹出人工帧确认;范围外 mask 和传播历史片段保持不变。

AI 点/框推理

  1. 用户在 Canvas 选择正向点、反向点或框选。
  2. CanvasArea 读取当前帧 ID 和宽高。
  3. SAM 2.1 框选会创建一个候选 mask并记录原始框后续正向点/反向点会累计到同一候选上。
  4. predictMask() 归一化坐标并携带当前 model 调用 /api/ai/predict;同时有框和点时发送 interactive prompt。
  5. SAM 2.1 请求中只要存在反向点,CanvasArea 会额外发送 options.auto_filter_background=trueoptions.min_score=0.05,让后端移除低分结果和包含负向点的 polygon。
  6. 后端加载帧图片并通过 SAM registry 分发到所选 SAM 2.1 变体;model=sam2 会兼容归一化为 tinymodel=sam3 会被拒绝。
  7. 前端把 polygons 转为 mask交互式细化会替换同一个候选 mask而不是新增多个 mask。
  8. 若带反向点的 SAM 2.1 细化返回空结果,前端会删除当前旧候选 mask 并提示反向点已排除该区域。
  9. AI 页面只按本页最新生成的候选 id 渲染 mask不把工作区已有 mask 带入 AI 画布;每次 runInference() 都先过滤掉旧 aiMaskIds 对应候选,再写入本次最高分候选。
  10. AI 页面候选 mask 的 Path 点击事件会先判断当前工具;正向/反向选点工具下点击 mask 会继续追加提示点,其他工具下才选中 mask。
  11. 工作区 SAM 提示点由 CanvasArea 本地 points 状态维护;点击已渲染提示点会先 cancelBubble,再删除对应点并按剩余提示重新调用 runInference(),避免同一次点击继续触发 Stage 加点或 Path 选择。
  12. AI 页面边界框选由 promptBox/boxStart/boxCurrent 维护;拖拽时渲染蓝色虚线框,鼠标释放后固化 promptBox 并清空旧提示点,避免旧点误绑定到新框。
  13. AI 页面执行分割时,如果只有 promptBox 则发送 box prompt如果 promptBoxpoints 同时存在,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 页面“AI 遮罩透明度”滑杆复用 Zustand maskPreviewOpacity,和右侧“遮罩透明度”联动,只调节候选 mask 的 Konva preview opacity不写入 Mask.segmentation、分类元数据或后端 payload。
  18. AI 画布左上角根据正向点、反向点、边界框选和视口控制显示上下文提示,说明点击/拖拽、删除提示点和执行推理的操作方式。
  19. AI 画布根据容器和当前帧尺寸按 86% 适配比例计算初始 scale/position使底图默认居中且尽量大但保留画布边距。
  20. Canvas 按当前帧过滤并渲染 mask。
  21. 新 mask 会带上当前选择的模板分类元数据,包括 classIdclassNameclassZIndexmetadata.source=ai_segmentation 和保存状态 draft
  22. 顶栏保存状态按钮按当前项目待保存数量显示为“保存 X 个改动”或“已全部保存”;用户点击保存后,前端将像素 segmentation 转成 normalized mask_data.polygons;未保存 mask 调用 POST /api/ai/annotatedirty mask 调用 PATCH /api/ai/annotations/{annotation_id};保存成功后本次提交的 draft mask id 会从本地保留列表中排除,并由后端 saved annotation 回显替换。
  23. 工作区加载项目帧后通过 GET /api/ai/annotations 取回已保存标注并转成前端 mask。
  24. 工作区“清空遮罩”删除当前帧已保存标注,并清除当前帧本地 mask。

视频片段传播

  1. 用户在工作区打开一帧作为参考帧;该帧全部 mask 都会作为传播 seed不再提供传播对象下拉。
  2. 用户可以直接修改传播起始帧/结束帧数字框,并可通过工作区顶栏“传播权重”下拉独立选择本次传播使用的 SAM 2.1 tiny/small/base+/large 权重;该入口不提供 SAM2/SAM3 家族切换,默认跟随全局 AI 权重,用户手动选择后不再被 AI 页权重切换覆盖。
  3. VideoWorkspace 以当前参考帧为 seed将起止帧拆成 backward 和/或 forward 两段;只包含当前帧时不传播。
  4. VideoWorkspace 在提交传播前会先调用现有归档保存链路保存当前项目中的 draft/dirty mask并重新读取 store 中的回显结果;参考帧 seed 因此优先携带稳定的后端 source_annotation_id,避免用前端临时 mask id 生成传播结果后,二次传播无法找到旧结果。
  5. VideoWorkspacebuildAnnotationPayload() 把每个 seed mask 转成 normalized polygon、bbox、label、color、class 元数据、source_mask_id 和可用时的 source_annotation_id;中空 mask 会按 metadata.polygonRingCounts 将外圈写入 mask_data.polygons,把与外圈对齐的内洞写入 mask_data.holes,传播 seed 同步携带 holes;如果 seed mask 是未编辑的自动传播结果,会沿用其原始 source_annotation_id/source_mask_id/propagation_seed_signature,让后端把它识别为原传播链的同一个 seed如果该传播结果被编辑并保存更新 payload 只保留 lineage不保留旧签名使后端按“已修改”路径清理旧结果并重传。对历史或外部写入的 geometry_smoothing metadatapayload 仍可透传给后端兼容处理;当前前端平滑应用会直接改写 polygon 几何并移除该参数。
  6. 前端把传播权重 id、每个 seed、每个方向组装成 steps,一次调用 POST /api/ai/propagate/taskinclude_source=falsesave_annotations=true;接口先规范化/校验 model 字段中的权重 id再创建 processing_tasks.task_type=propagate_masks 并投递 Celery避免长 HTTP 请求阻塞前端等待。
  7. VideoWorkspace 记录返回的 task_id,轮询 GET /api/tasks/{task_id} 显示任务 message、步骤进度、已处理帧次和已保存区域数任务运行期间提供取消传播按钮调用通用 POST /api/tasks/{task_id}/cancel
  8. Celery worker 逐 step 顺序执行传播,避免多个视频 tracker 并发抢占 GPU每个 step 开始/完成都会写入 processing_tasks.progress/result/message 并发布 Redis seg:progressDashboard 可同步显示。每个 step 开始前worker 会在本次目标帧段内用 seed 来源 id、传播方向和 seed 签名查找旧传播标注:同权重、签名相同且目标帧都已有结果时跳过该 seed签名不同、目标帧只部分覆盖或本次使用了其他 SAM 2.1 权重则先删除本次目标帧段内对应方向的旧自动传播标注,再执行新的 video predictor 传播;若历史 seed 签名中包含 geometry_smoothing,仍按完整签名参与兼容去重。对旧版本只记录前端临时 source_mask_id 的传播标注worker 会按 label/color/class 做兼容匹配,确保可被后续稳定 source_annotation_id 的传播替换;对中间帧人工新增的替代 seed若缺少旧 source idworker 仍会用语义信息识别候选旧传播结果,并在写入目标帧新 polygon 前用目标帧 bbox 重叠做二次确认和清理。写入前这层清理不限制旧结果方向,确保 backward 传播可覆盖早先 forward 传播留下的同物体旧 mask。
  9. 后端按项目帧序列截取片段,下载对应帧到临时目录,并写成 000000.jpg 这类纯数字文件名;这是 SAM2VideoPredictor 对视频帧排序的要求,和项目库中持久化的 frame_%06d.jpg 对象名无关。
  10. model 为任一 SAM 2.1 权重变体时,sam2_engine 使用对应 checkpoint/config 加载 SAM2VideoPredictor.add_new_mask() 注入 seed mask再用 propagate_in_video() 传播;注入 seed 前会把外圈 polygon 栅格化为前景,再按 holes 扣除内洞,避免中空参考 mask 以实心形式传播;model=sam2 会在入队时规范化为 tiny任务 payload/result 会保留规范化后的权重 id单个 SAM2 video predictor 调用内部暂不提供逐帧流式进度。
  11. model=sam3 当前不支持SAM 3 video tracker 代码保留但没有接入产品路径。
  12. 后端把传播返回的 normalized polygon 保存为后续帧 Annotation,跳过源帧;传播 mask 轮廓提取使用层级信息保留内洞,外圈写入 mask_data.polygons,内洞按外圈对齐写入 mask_data.holes,并设置 metadata.hasHoles 供前端按中空 mask 回显和编辑;如果历史或外部 seed 带 geometry_smoothing,保存前仍会用同一平滑参数处理 forward/backward 两个方向的结果:强度先经过缓入曲线映射,低强度使用较小 Chaikin 切角比例和简化阈值,高强度再逐步增加迭代、切角和简化力度;随后按强度对 SAM 密集轮廓做 approxPolyDP 去噪简化,再做 Chaikin 平滑,最后二次简化并以平滑后的 polygon 计算 bbox 后落库。当前工作区“应用边缘平滑”会在前端把同传播链对应 mask 直接改写为新的 polygon 并移除 geometry_smoothing 参数,因此后续传播通常按新几何本身参与 seed 签名。mask_data.source 记录权重传播来源,同时写入 propagation_seed_keypropagation_seed_signaturepropagation_directionsource_annotation_idsource_mask_id 供后续幂等传播判断;历史 geometry_smoothing 仅在存在时保留用于兼容判断。
  13. 前端轮询到已创建区域后刷新 GET /api/ai/annotations 并回显新标注;任务结束后如果后端返回 0 个新区域,工作区会明确提示没有生成新的 mask若是未改变 seed 被跳过则提示未改变 mask 已跳过。处理过帧次大于 0 的成功任务会追加一条本地传播历史片段,用于视频处理进度条显示最近传播范围;annotationToMask() 会保留传播来源 metadata供时间轴视频处理进度条显示蓝色传播区段。

手工绘制与历史栈

  1. 用户在 ToolsPalette 选择多边形、矩形、圆、画笔或橡皮擦工具;创建点和创建线段入口不在工作区左侧工具栏中提供。
  2. CanvasArea 将交互坐标转换成像素 polygon。
  3. 多边形工具逐次记录节点,三点后点击首节点或按 Enter 时生成闭合 polygon。
  4. Canvas 左上角根据当前工具和操作阶段显示上下文短提示;多边形提示会随已放置点数切换,明确 Enter 完成、Esc 取消和点击首节点闭合。提示会在工具或操作状态变化时出现,并在数秒后自动隐藏,避免长期遮挡底图。
  5. mask path 只在 moveedit_polygonarea_mergearea_remove 工具下拦截点击;绘制、画笔、橡皮擦和 AI prompt 工具点击已有 mask 时继续冒泡给 Stage。
  6. 画笔/橡皮擦尺寸保存在 Zustand 中;拖动期间只保留采样后的圆形笔触预览,鼠标松开后再用 polygon-clipping 执行 union/difference避免拖动中反复重算复杂 polygon。
  7. 新 mask 写入 pathData、像素 segmentationbboxarea 和当前模板分类元数据。
  8. addMask()setMasks()updateMask()clearMasks() 会维护 maskHistory/maskFuture
  9. 工作区撤销/重做只保留顶栏按钮和快捷键入口AI 页保留自己的撤销/重做按钮;工作区由 VideoWorkspace 统一处理 Ctrl/Cmd+ZCtrl/Cmd+Shift+ZCtrl/Cmd+Y,并在输入框、下拉框和可编辑文本聚焦时跳过快捷键,避免影响帧范围输入。

Polygon 逐点编辑

  1. 用户选择“调整多边形”或“拖拽/选择”后点击 Canvas 上的 mask pathCanvasArea 记录 selectedMaskId 并显示该 mask 第一条 polygon 的顶点控制点和边中点插入手柄。
  2. 顶点 mousedown/dragstart 会立即设置当前顶点选择;拖动过程中通过 dragMove 实时重算 pathData、像素 segmentationbboxarea,不需要先单击顶点再拖动。
  3. Stage 的 onDragEnd 只处理 Stage 自身拖拽polygon 顶点等子节点拖拽结束事件会被忽略,避免子节点坐标误写入 Canvas position 导致视口跳动。
  4. 点击边中点手柄会在该边中点插入新顶点;在“调整多边形”工具下双击 polygon path 会在最接近的线段上按双击位置插入新顶点。
  5. 如果 mask 已有 annotationId,编辑会把 saveStatus 标成 dirtysaved=false
  6. 归档保存时复用现有 PATCH /api/ai/annotations/{annotation_id} 链路,把更新后的 normalized polygon 写回后端。
  7. 选中顶点后 Delete/Backspace 可删除顶点;前端保持 polygon 至少三点。
  8. 未选中具体顶点但选中了 mask 时Delete/Backspace 从前端 store 删除该 mask如果包含 annotationId,通过工作区回调调用后端删除接口;删除对象属于传播链或传播 seed 时,删除范围会扩展到同链自动传播 mask但不移除其他帧独立 AI 推理/人工 mask。
  9. 普通 mask 和导入 mask 都不显示黄色 seed point也不提供 seed point 拖动;保存 payload 仍可保留已有 points 数据兼容,但画布体验统一为区域选择和 polygon 顶点编辑。

区域合并与去除

  1. 用户选择 area_mergearea_remove 后,点击多个当前帧 mask 组成选择集。
  2. 合并/去除模式隐藏 polygon 顶点和边中点编辑手柄,并在右下角显示已选数量;少于两个 mask 时操作按钮禁用。
  3. Canvas 左上角提示布尔选择顺序:第一个选中的是主区域,后续区域参与合并或扣除。
  4. 布尔选择态按选择顺序区分角色:第一个选中的主区域使用黄色实线轮廓,后续参与合并/扣除的区域使用红色虚线轮廓;所有已选区域填充透明度保持一致,避免被误解为阴影模式异常。
  5. CanvasAreaMask.segmentation 转为 polygon-clipping 的 MultiPolygon。
  6. area_merge 使用 union更新第一个选中的主 mask并从前端 store 移除后续被合并 mask如果被移除 mask 已保存,会调用工作区传入的删除回调删除后端标注;被移除 mask 的同链自动传播结果也会一并删除。
  7. area_remove 使用 difference从第一个选中的主 mask 中扣除后续选中 mask扣除对象本身保留如果 difference 产生内洞,segmentation 保留外圈和 hole ringmetadata.polygonRingCounts 记录每个 polygon 的 ring 数,渲染时使用 even-odd fill。
  8. 结果会重算 pathDatasegmentationbboxarea,已保存主 mask 会进入 dirty 状态并复用归档 PATCH 链路;带洞结果的面积按外圈减内洞计算;进入调整多边形时,外圈和内洞 ring 都会显示顶点和边中点插入手柄,内洞拖动、插点、保存与回显继续保持中空结构。

GT Mask 导入

  1. 工作区左侧工具栏“导入 GT Mask”选择图片文件入口位于“重叠区域去除”之后。
  2. 前端 importGtMask() 以 multipart form-data 调用 POST /api/ai/import-gt-mask,携带 project_idframe_id
  3. 后端验证项目、帧、模板后使用 OpenCV 读取灰度 mask。
  4. 后端按非零像素值拆分多类别标签。
  5. 后端对每个类别的前景做高精度 contour 提取,每个连通域保存为一个 Annotation;轮廓使用未压缩链提取并以较小 approxPolyDP epsilon 保留细节,超过点数上限时才逐步增加简化强度或抽样。
  6. points 字段可保存距离变换中心 seed point 供数据兼容,mask_data.polygons 保存 normalized polygonmask_data.holes 保存与外圈对齐的内洞,mask_data.gt_label_value 保存原始像素类别值;导入后的 polygon 与普通 mask 走同一套拓扑锚点统计、边缘平滑、编辑和保存链路。
  7. 前端重新读取项目标注并回显。
  8. annotationToMask() 仍可把后端 points 转成像素坐标保存在 mask 数据中,但 Canvas 不显示 seed point也不提供拖动普通 polygon 若没有后端 seed point保存逻辑可按 polygon 自动计算内部代表点写入,以保持数据兼容。

模板管理

  1. TemplateRegistry 从后端读取模板。
  2. 编辑态在组件本地维护分类列表。
  3. 保存时调用 createTemplate()updateTemplate()
  4. 后端把 classesrules 打包进 mapping_rules
  5. 返回时再解包给前端。
  6. 模板详情页和编辑弹窗都支持拖拽调整语义类别层级顺序;拖拽后重算 zIndex,保存到后端模板并刷新当前详情页,maskId 保持不变。所有模板都会归一化包含黑色 maskId: 0 的“待分类”保留类,该类固定在语义分类树最后,不参与删除和拖拽上移。编辑弹窗点击分类后只编辑分类名称,不展示或编辑旧 category 来源元信息。编辑弹窗中的 JSON 批量导入支持 [[colors], [names]]{colors, names} 两种格式,并兼容带前缀、代码块、未加引号 keys、单引号、中文逗号/冒号和尾随逗号的粘贴内容导入前会先显示分类数量、maskid 分配起点和缺失颜色提示,语法或结构错误以内联错误展示,确认导入后进入编辑态,保存模板时落库。
  7. CanvasArea 把当前选中的 mask id 同步到全局 selectedMaskIds;切换工具、切换帧或卸载 Canvas 时会清空选择。
  8. AISegmentation 生成 mask 后会写入全局 masks 并把生成的 mask id 写入 selectedMaskIds;点击 AI 页预览 mask 也会更新 selectedMaskIds
  9. AI 页“推送至工作区编辑”会先检查待推送 AI 候选 mask 是否具备 classIdclassName;缺少语义分类时清空普通推理反馈,并通过 TransientNotice 右上角 error toast 提示用户先点右侧语义分类树,不切换模块、不修改工具状态。
  10. AISegmentation 卸载时会清理仍缺少 classId/className 的本页 AI 候选,并同步移除对应 selectedMaskIds,避免用户绕过推送按钮从侧栏切到工作区时带入无语义 mask。
  11. AI 页语义校验通过后会切换到工作区并把 activeTool 设为 edit_polygonCanvasArea 初始读取全局 selectedMaskIds,让 AI 页选中的 mask 在工作区继续保持选中。
  12. 工作区帧/标注异步加载完成后,hydrateSavedAnnotations() 会合并本地未保存 draft mask 和后端已保存 mask不会用后端回显结果直接覆盖整个 masks store。
  13. OntologyInspector 可以选择具体分类;选择结果进入全局 storeCanvasAreaAISegmentation 新建/更新 mask 时使用。
  14. 如果 selectedMaskIds 中存在当前 store 的 mask点击分类时会立即更新这些 mask 的 templateIdclassIdclassNameclassZIndexlabelcolor
  15. 对属于自动传播链的 mask分类更新会复用 source_annotation_idsource_mask_idpropagation_seed_key 查找同一目标实例在前后帧中的传播结果,并同步更新这些传播 mask 的分类元数据,避免同一物体跨帧语义不一致。
  16. 同一次点击会把这些已选 mask 移动到前端 masks 数组末尾;CanvasArea 按数组顺序渲染,后渲染的 Path 显示在最上层,方便用户继续编辑刚换标签的区域。该显示置顶不改变模板 zIndex 或后端导出语义覆盖规则。
  17. 已保存 mask 被重新分类后进入 dirtysaved=false,同传播链被同步更新的已保存 mask 也进入 dirty,继续复用工作区归档保存的 PATCH 链路。
  18. 模板保存、删除和 JSON 导入失败使用 TransientNotice 非阻塞提示,默认数秒后自动消失。

导出

  1. 后端根据项目、帧、标注和模板生成 COCO JSON。
  2. PNG mask 导出会把 normalized polygon 渲染为单标注二值 mask。
  3. PNG mask 导出还会按 mask_data.class.zIndex 或模板 z_index 从低到高覆盖,生成每帧语义融合 mask。
  4. ZIP 内写入 semantic_classes.json,记录语义值到类别、颜色和 zIndex 的映射。
  5. 前端使用“分割结果导出”统一入口替代原 JSON/PNG 两个按钮;点击后在下拉栏选择整体视频、特定范围帧或当前图片,默认选中当前图片,并勾选分开二值 mask、GT_label 黑白图、Pro_label 彩色图和 Mix_label 原图叠加图。选择“特定范围帧”时,导出起止帧输入框和 FrameTimeline 的范围拖拽选择共用同一组导出范围状态;选择 Mix_label 时显示透明度滑杆,默认 0.3,并用当前/待导出第一帧做遮罩预览。提交前会保存待归档标注,然后下载统一 ZIP。下载文件名使用 {项目库项目名}_seg_T_{起始时间戳}-{结束时间戳}_P_{起始项目帧序号}-{结束项目帧序号}.zip;项目名来自 currentProject.name,起止帧按当前导出范围取首尾帧,时间戳格式为 0h00m00s000ms,帧号使用项目抽帧后的 1-based 顺序,项目名中的文件系统不安全字符会替换为 _
  6. 统一导出 ZIP 固定包含 annotations_coco.jsonmaskid_GT像素值_类别映射.json原始图片/;原始图片文件名使用 视频名称_时间戳_项目帧序号。导出会保留类别真实 maskidGT_label 固定为 8-bit uint8 PNG像素值与 maskid 相同并跨图一致;maskId: 0 的“待分类”保持 0和背景同为黑色Pro_label 中也输出为 [0,0,0];缺失 maskid 的旧标注才补下一个可用正整数并写入映射 JSON正整数 maskid 超出 1-255 会拒绝导出。选择分开 mask 时包含 分开Mask分割结果/,每帧建立 {视频名称_时间戳_项目帧序号}_分别导出 子文件夹,并按“同一帧同一类别合并一张图”的方式输出 {视频名称_时间戳_项目帧序号}_{类别名称}_maskid{maskid}.png。选择 GT_label 图时包含 GT_label图/{视频名称_时间戳_项目帧序号}.png;选择 Pro_label 图时包含 Pro_label彩色分割结果/{视频名称_时间戳_项目帧序号}.png;选择 Mix_label 图时包含 Mix_label重叠覆盖彩色分割结果/{视频名称_时间戳_项目帧序号}.png。GT_label、Pro_label 和 Mix_label 的重叠区域按内部拖拽排序从低到高覆盖和未选中状态下的画布显示顺序一致maskid 不参与排序。后端直接下载接口的 Content-Disposition 使用同一 ZIP 命名规则,并用 filename* 支持中文项目名。
  7. 右侧 OntologyInspector 的语义分类树支持拖拽调整内部覆盖顺序;拖拽后保存到模板并同步当前工作区同类 mask 的 classZIndex,但保留类别 maskid 不变。

接口契约

接口详情见 doc/04-api-contracts.md。测试中重点固定以下契约:

  • updateProject() 使用 PATCH /api/projects/{id}
  • exportCoco() 使用 GET /api/export/{projectId}/coco
  • exportMasks() 使用 GET /api/export/{projectId}/masks
  • exportSegmentationResults() 使用 GET /api/export/{projectId}/results,通过 query 参数选择范围和 mask 类型。
  • cancelTask() 使用 POST /api/tasks/{taskId}/cancel
  • retryTask() 使用 POST /api/tasks/{taskId}/retry
  • predictMask() 使用 POST /api/ai/predict,请求体为 image_idprompt_typeprompt_datamodel
  • propagateMasks() 使用 POST /api/ai/propagate,请求体为 project_idframe_idmodelseeddirectionmax_frames,作为单 seed 同步兼容接口保留。
  • queuePropagationTask() 使用 POST /api/ai/propagate/task,请求体为 project_idframe_idmodelstepsinclude_sourcesave_annotations,返回 ProcessingTask
  • saveAnnotation() 使用 POST /api/ai/annotate
  • importGtMask() 使用 POST /api/ai/import-gt-mask multipart form-data并传入 unknown_color_policy=discard|undefined。前端上传前弹出导入结果预览和未知 maskid 策略选择;后端使用 cv2.IMREAD_UNCHANGED 读取后校验 dtype。合法 GT mask 限定为 8-bit 灰度图或 8-bit RGB 三通道完全相同的 [X,X,X] maskid 图0 为背景、X 为 1-255 的 maskid灰度/RGB 等通道图按模板 maskId 匹配类别16-bit/uint16 GT_label、全背景 0 图和普通彩色 RGB 类别图不再按颜色匹配并会返回格式错误全背景图提示为“GT Mask 图片中没有非背景 maskid 区域。”;未知类别按策略舍弃或保存为 gt_unknown_class 未定义类别。若 GT mask 尺寸和当前帧不同,后端用最近邻插值拉伸到当前帧尺寸后再生成高精度 polygon。
  • getProjectAnnotations() 使用 GET /api/ai/annotations
  • updateAnnotation() 使用 PATCH /api/ai/annotations/{annotationId}
  • deleteAnnotation() 使用 DELETE /api/ai/annotations/{annotationId}
  • parseMedia() 使用 POST /api/media/parse?project_id=...,可选 parse_fpsmax_framestarget_width,用于生成标准帧序列。
  • getProjectFrames() 返回帧图像 URL、宽高、timestamp_mssource_frame_number
  • 后端 /api/ai/predict 当前支持 SAM 2.1 的 point、box、interactivesemantic 文本提示禁用并返回 400。
  • SAM 2.1 是点/框交互式分割模型不做文本语义分割AI 页面已经移除纯文本输入。
  • SAM 2.1 点提示和 auto fallback 只返回一个最高分候选,避免同一提示产生多个重叠候选 mask。
  • SAM 3 前端入口、后端 registry 入口和状态展示均已禁用;model=sam3 会返回不支持。
  • 后端 /api/ai/predict 支持可选 optionscrop_to_prompt 会对 point/box/interactive prompt 做局部裁剪推理并回映射 polygonauto_filter_background 会按 min_score 和负向点过滤结果。
  • 后端 /api/ai/propagate/task 当前支持所选 SAM 2.1 mask seed 视频传播后台任务;同步 /api/ai/propagate 仍保留为单 seed 兼容接口。
  • 后端 /api/ai/models/status 返回 GPU 和四个 SAM 2.1 变体的真实运行状态。
  • point prompt 支持旧数组形式和 { points, labels } 对象形式。

外部依赖边界

测试不直接依赖以下真实服务:

  • PostgreSQL后端测试使用内存 SQLite。
  • MinIO上传、下载、预签名 URL 使用 monkeypatch。
  • Redis单测使用 monkeypatch 验证进度事件发布,不依赖真实 Redis 服务。
  • SAMAI 推理测试使用 fake registry。
  • 浏览器 Canvas/Konva 图片加载:前端测试 mock react-konvause-image

已知占位设计

以下能力属于当前冻结版本的占位或半可用功能:

  • Dashboard 初始快照来自 GET /api/dashboard/overview;任务进度区由 processing_tasks queued/running/success/failed/cancelled 任务生成,处理中统计只计算 queued/running。
  • 已保存标注支持通过“应用分类”、polygon 顶点拖动/删除、边中点插入、多 polygon 子区域编辑、中空 mask 内洞 ring 编辑和区域合并/去除进入 dirty 状态并归档更新;选中整块 mask 可用 Delete/Backspace 删除并同步后端,同传播链自动传播结果会随传播 seed/传播结果删除而一并清理,独立 AI 推理/人工 mask 保留。
  • SAM 3 文本语义分割已从当前产品路径中禁用相关源码保留恢复时需要重新接入前端入口、registry、状态接口和测试。
  • 自定义分类通过 PATCH /api/templates/{id} 写入当前激活模板的 mapping_rules.classes
  • 选中 mask 后,本体面板的“特定目标实例属性追踪”标题值来自当前 mask 的 className/label,不使用全局 active class面板不再展示长期为 1 的“当前选中区域”计数;面板调用 POST /api/ai/analyze-mask 自动显示拓扑锚点数量等属性,topology_anchor_count 是真实 polygon 顶点数量,topology_anchors 只保留最多 64 个抽样点用于调试展示;OntologyInspector 会为分析请求维护递增序号,旧请求返回时不再回写状态,并静默忽略 Axios abort/cancel 错误,避免快速切换、平滑预览或组件卸载时把正常中止误报成失败;不再提供“重新提取拓扑锚点”调试按钮;“边缘平滑强度”滑杆会即时更新数值,但 POST /api/ai/smooth-mask 预览请求经过约 220ms 防抖后才发送,返回 polygon 作为临时预览写入当前 mask 显示,预览不改变保存状态;点击“应用边缘平滑”后,前端把平滑 polygon 作为新的实际几何写入当前 mask并按传播 lineage 同步写入传播链前后对应 mask相关 mask 标记为 dirty/draft整次操作通过一次 setMasks() 进入撤销/重做历史;应用后不保留 geometry_smoothing 参数,平滑强度重置为 0。前端不再展示“后端模型置信度”。
  • GT mask 导入已完成多类别像素值拆分、contour 和 distance transform seed point 数据兼容;前端不显示或拖动 seed point导入 mask 与普通 mask 共享拓扑统计、边缘平滑、顶点编辑、分类和保存体验骨架提取、HDBSCAN 聚类和模板自动映射尚未实现。