- dirty 标注 PATCH 404 时改用 POST 重新创建,保留几何、分类和传播 lineage metadata - 保存后回显替换本地旧 annotationId,避免保存改动和开始传播被陈旧 id 中断 - 增加工作区回归测试,覆盖本地旧 annotationId 重新创建流程 - 更新接口契约、需求冻结、设计冻结、测试计划和 AGENTS 说明
47 KiB
47 KiB
当前设计冻结文档
冻结日期:2026-05-01
本文档描述当前代码结构、数据流、接口契约和测试边界。后续实现如果改变这些设计,应同步更新本文档和测试。
总体架构
当前系统由三层组成:
- React + TypeScript 前端 SPA。
- FastAPI 后端 API。
- PostgreSQL、MinIO、Redis、SAM 2 等外部基础设施。SAM 3 相关源码保留,但当前产品入口禁用。
开发时前端通过 server.ts 启动 Express + Vite middleware;后端通过 backend/main.py 启动 FastAPI。前端业务接口访问 FastAPI,server.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,包含pathData、segmentation、bbox、area。selectedMaskIds:Canvas 当前选中的 mask id 列表,供右侧本体面板对已选区域直接换标签。maskHistory/maskFuture:mask 编辑历史栈,用于撤销和重做。activeModule:当前页面。activeTool:当前工具。aiModel:当前启用的 AI 模型,取值为sam2.1_hiera_tiny、sam2.1_hiera_small、sam2.1_hiera_base_plus或sam2.1_hiera_large,默认sam2.1_hiera_tiny。
关键数据流
登录
Login收集用户名和密码。login()调用POST /api/auth/login。- 后端用
users表中的密码哈希校验用户,成功后返回签名 JWT 和用户资料。 - 前端把 token 写入
localStorage和 Zustand;刷新页面时useStore会从localStorage恢复 token。 App在已登录状态调用/api/auth/me恢复当前用户,再拉取当前用户项目列表。
用户隔离
Project.owner_user_id指向users.id;启动时默认 admin 用户会被创建,历史owner_user_id IS NULL的项目会迁移归属到 admin。- 项目、帧、媒体上传/拆帧、AI 标注、传播任务、任务列表、Dashboard 和导出接口都通过当前 JWT 用户过滤项目资源。
Template.owner_user_id支持用户模板;owner_user_id IS NULL的模板视为系统模板,可作为默认分类体系对用户可见。- 角色分为
admin、annotator、viewer:admin/annotator可调用写入类业务接口,viewer只能调用读接口;/api/admin/*仅允许admin。 UserAdmin.tsx仅在当前用户角色为admin时从Sidebar展示,调用/api/admin/users完成新增、角色修改、停用/启用、密码修改和删除无项目用户,调用/api/admin/audit-logs展示登录和管理操作审计;改密码、删除用户和危险区“恢复演示出厂设置”均使用站内弹窗确认,恢复出厂设置要求输入RESET_DEMO_FACTORY后调用/api/admin/demo-factory-reset。POST /api/admin/demo-factory-reset仅允许admin,会重置默认 admin 密码/角色/启用状态,删除其它用户、项目、帧、标注、mask、任务、用户模板和旧审计,重新创建settings.demo_video_path指向的演示视频项目,以及settings.demo_dicom_dir指向的演示 DICOM 项目;DICOM 会按文件名自然顺序上传和生成帧;系统模板保留以保证重置后仍可标注。- 缺失、过期或伪造的 Bearer token 会在业务路由返回 401,权限不足返回 403,其他用户项目资源对当前用户表现为 404。
项目导入与生成帧
ProjectLibrary创建项目。- 导入视频时上传源视频到
/api/media/upload并关联项目;该步骤不调用/api/media/parse。上传期间项目库显示导入进度条、百分比和已上传字节,完成后短暂显示“视频导入完成”。 - 用户在项目卡片点击“生成帧”,在弹窗中选择目标 FPS。
- 前端调用
/api/media/parse创建异步拆帧任务;可通过parse_fps、max_frames和target_width指定标准帧序列参数。 - Celery worker 执行 FFmpeg/OpenCV/pydicom 拆帧;DICOM 在前端选择、后端上传、worker 下载和 pydicom 读取时都按文件名自然顺序排序;视频/DICOM 解析结果都按
frame_%06d.jpg从frame_000000.jpg连续命名,视频帧按目标宽度缩放。 - worker 写入
frames.timestamp_ms和frames.source_frame_number,并在任务result.frame_sequence中记录 FPS、帧数、时长、尺寸和对象存储前缀。 - worker 持续更新
processing_tasks,并发布 Redisseg:progress。 - 刷新项目列表;项目卡片右上角 FPS 徽标显示生成关键帧序列时选择的
parse_fps,原始视频 FPS 仅作为底部“原 xx fps”辅助信息显示。 - 导入视频、生成帧、上传 DICOM 和失败反馈使用
TransientNotice,不再使用浏览器alert()阻塞操作;提示默认数秒后自动消失。视频和 DICOM 上传阶段额外显示项目库内的导入进度面板,DICOM 面板显示有效文件数量,并在上传完成后切换为解析任务进度,轮询GET /api/tasks/{task_id}直到成功、失败或取消。 - DICOM 和视频帧序列写入同一
frames表并共用工作区、时间轴、AI 传播、标注保存、GT 导入和导出链路,差异只存在于项目库导入入口和后端解析器。
任务控制
- Dashboard 从
GET /api/dashboard/overview读取 queued/running/success/failed/cancelled 任务;queued/running 代表当前进度,success/failed/cancelled 代表最近任务状态。 - 用户取消任务时,前端调用
POST /api/tasks/{task_id}/cancel;后端写入cancelled、设置finished_at,并尝试celery_app.control.revoke(..., terminate=True)。 - worker 在下载、解析、上传、写帧等关键阶段刷新任务状态;如果发现
cancelled,停止后续写入并发布 cancelled 事件。 - 用户重试任务时,前端调用
POST /api/tasks/{task_id}/retry;后端基于原任务payload创建新任务,记录retry_of并重新投递 Celery。 - 用户打开详情时,前端调用
GET /api/tasks/{task_id},弹窗展示 error、payload、result、Celery ID 和时间。 - Dashboard 通过
/ws/progress接收 Redisseg:progress转发事件;前端 WebSocket 客户端在onopen/onclose/onerror主动更新连接状态,并定时发送ping心跳,服务端返回status确认连接仍活跃。
工作区加载
VideoWorkspace根据currentProject.id调用getProjectFrames()。- 若无帧但项目有
video_path,显示“尚未生成帧”的状态提示,不自动触发parseMedia()。 - 帧数据映射为 store
Frame[],包含timestampMs和sourceFrameNumber,供时间轴和后续视频传播使用。 - 工作区调用
GET /api/ai/annotations回显已保存标注时,会替换当前项目帧中的已保存 mask,但保留没有annotationId的未保存 draft mask;这保证 AI 页推送到工作区的候选 mask 不会被异步回显覆盖,并会在合并完成后恢复仍然存在的已选 mask id。 VideoWorkspace加载项目帧时会优先按当前选中 mask 的frameId和当前打开帧 id 恢复currentFrameIndex;只有没有可恢复帧时才回到第一帧,避免 AI 页在非第一帧推送回工作区时视角被重置。CanvasArea会把全局selectedMaskIds中仍存在于当前帧的 id 同步回本地选区,避免帧初始化时的临时清空覆盖 AI 页推送过来的选中态;如果切换到另一帧时原 id 不存在,但目标帧存在同一自动传播链的结果,前端会用source_annotation_id、source_mask_id和propagation_seed_key匹配对应传播 mask 并自动选中。CanvasArea根据容器和帧尺寸按 86% 适配比例计算初始 scale/position,使底图默认居中且尽量大,但保留画布边距;滚轮缩放和拖拽平移仍由用户后续控制。CanvasArea未选中特定 mask 时,会按classZIndex从低到高渲染当前帧 mask;该值来自右侧“语义分类树”的拖拽排序,因此高优先级类别会后渲染并覆盖低优先级类别。有选中 mask 时,编辑态可保留选中区域置顶,方便拖点、换类和布尔操作。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 覆盖层,并额外用洋红色起始线和黄绿色结束线贯穿两条进度条,表达待处理或待导出范围边界,可点击/拖拽设置起止帧。- 当前帧传入
CanvasArea。 - 工作区顶栏短状态文本会在空闲状态下自动消失;保存、导出、导入 GT 和传播任务运行中仍保留进度状态,无帧项目提示也会保留。
- 左侧工具栏和右侧本体/语义分类面板使用
seg-scrollbar定制纵向滚动条;默认滚动条 thumb 低透明度融入深色背景,hover/focus 时增强为青色提示,避免系统默认滚动条在工具区中过于突兀。左侧工具栏额外保留右侧滚动条槽位,按钮列仍按原 48px 布局,避免滚动条和图标抢空间。 - 右侧面板不再显示“本体论与属性分类管理树”固定说明栏,直接展示实际可操作内容。
- 右侧“遮罩透明度”滑杆写入 Zustand
maskPreviewOpacity,CanvasArea和AISegmentation都用该值计算 mask group opacity;选中 mask 在基础透明度上加亮或按基础透明度显示,方便保留选中反馈。 - Canvas 点击 mask 后,全局
selectedMaskIds会同步到OntologyInspector;本体面板按选中 mask 的classId、className/label和颜色匹配模板分类,自动设置 active class,并把分类按钮滚动/聚焦到可见区域。 - 工作区顶栏“清空片段遮罩”和“自动传播”共用时间轴范围选择交互;第一次点击“清空片段遮罩”会进入范围选择模式,按钮变为“确认清空”,用户可在播放进度条或视频处理进度条上点击/拖拽选择起止帧;进入清空模式后顶栏显示“清空全部 / 保留人工/AI”两段式模式选择,默认“清空全部”。“清空全部”会对范围内已保存 mask 调用
DELETE /api/ai/annotations/{id},同时移除范围内本地 draft mask、被清空的选区和与清空范围重叠的本地传播历史条;若范围内存在非自动传播来源的 mask,也就是时间轴红色“人工/AI 标注帧”,执行前会显示站内确认弹窗,取消则不删除任何 mask。“保留人工/AI”只删除范围内自动传播/推理 mask,不删除人工绘制或 AI 智能分割生成的红色标注帧,不弹出人工帧确认;范围外 mask 和传播历史片段保持不变。
AI 点/框推理
- 用户在 Canvas 选择正向点、反向点或框选。
CanvasArea读取当前帧 ID 和宽高。- SAM 2.1 框选会创建一个候选 mask,并记录原始框;后续正向点/反向点会累计到同一候选上。
predictMask()归一化坐标并携带当前model调用/api/ai/predict;同时有框和点时发送interactiveprompt。- SAM 2.1 请求中只要存在反向点,
CanvasArea会额外发送options.auto_filter_background=true和options.min_score=0.05,让后端移除低分结果和包含负向点的 polygon。 - 后端加载帧图片并通过 SAM registry 分发到所选 SAM 2.1 变体;
model=sam2会兼容归一化为 tiny,model=sam3会被拒绝。 - 前端把
polygons转为 mask;交互式细化会替换同一个候选 mask,而不是新增多个 mask。 - 若带反向点的 SAM 2.1 细化返回空结果,前端会删除当前旧候选 mask 并提示反向点已排除该区域。
- AI 页面只按本页最新生成的候选 id 渲染 mask,不把工作区已有 mask 带入 AI 画布;每次
runInference()都先过滤掉旧aiMaskIds对应候选,再写入本次最高分候选。 - AI 页面候选 mask 的 Path 点击事件会先判断当前工具;正向/反向选点工具下点击 mask 会继续追加提示点,其他工具下才选中 mask。
- 工作区 SAM 提示点由
CanvasArea本地points状态维护;点击已渲染提示点会先cancelBubble,再删除对应点并按剩余提示重新调用runInference(),避免同一次点击继续触发 Stage 加点或 Path 选择。 - AI 页面边界框选由
promptBox/boxStart/boxCurrent维护;拖拽时渲染蓝色虚线框,鼠标释放后固化promptBox并清空旧提示点,避免旧点误绑定到新框。 - AI 页面执行分割时,如果只有
promptBox则发送boxprompt;如果promptBox和points同时存在,predictMask()会发送 interactive prompt。 - AI 页面提示点由本地
points状态维护;点击已渲染提示点会按 index 删除对应点,“删除最近锚点”会删除数组最后一个点,不改动候选 mask 列表。 - AI 页面候选 mask 删除只接受当前
aiMaskIds范围内的已选 id;“删除选中候选”和 Delete/Backspace 都复用该范围过滤,避免删除工作区已有 mask。 - AI 页面参数开关文案只做展示增强:“局部专注模式(自动裁剪无锚区域)”仍控制
cropMode/crop_to_prompt,“严格除杂模式(自动清理干涉点)”仍控制autoDeleteBg/auto_filter_background/min_score。 - AI 页面“AI 遮罩透明度”滑杆复用 Zustand
maskPreviewOpacity,和右侧“遮罩透明度”联动,只调节候选 mask 的 Konva preview opacity,不写入Mask.segmentation、分类元数据或后端 payload。 - AI 画布左上角根据正向点、反向点、边界框选和视口控制显示上下文提示,说明点击/拖拽、删除提示点和执行推理的操作方式。
- AI 画布根据容器和当前帧尺寸按 86% 适配比例计算初始 scale/position,使底图默认居中且尽量大,但保留画布边距。
- Canvas 按当前帧过滤并渲染 mask。
- 新 mask 会带上当前选择的模板分类元数据,包括
classId、className、classZIndex、metadata.source=ai_segmentation和保存状态draft。 - 顶栏保存状态按钮按当前项目待保存数量显示为“保存 X 个改动”或“已全部保存”;用户点击保存后,前端将像素
segmentation转成 normalizedmask_data.polygons;未保存 mask 调用POST /api/ai/annotate,dirty mask 调用PATCH /api/ai/annotations/{annotation_id};如果 dirty mask 的后端标注已被其它操作删除导致PATCH返回 404,保存链路会保留同一mask_data、几何、分类和传播 lineage metadata,改用POST /api/ai/annotate重新创建,并在随后回显时排除本地旧 mask id;保存成功后本次提交的 draft mask id 会从本地保留列表中排除,并由后端 saved annotation 回显替换。 - 工作区加载项目帧后通过
GET /api/ai/annotations取回已保存标注并转成前端 mask。 - 工作区“清空遮罩”可从画布右下角或左侧工具栏触发;删除当前帧已保存标注、清除当前帧本地 mask,并通过传播 lineage 同步清空这些 mask 的自动传播结果,不删除其它帧独立 AI 推理或人工 mask。
视频片段传播
- 用户在工作区打开一帧作为参考帧;该帧全部 mask 都会作为传播 seed,不再提供传播对象下拉。
- 用户可以直接修改传播起始帧/结束帧数字框,并可通过工作区顶栏“传播权重”下拉独立选择本次传播使用的 SAM 2.1 tiny/small/base+/large 权重;该入口不提供 SAM2/SAM3 家族切换,默认跟随全局 AI 权重,用户手动选择后不再被 AI 页权重切换覆盖。
VideoWorkspace以当前参考帧为 seed,将起止帧拆成backward和/或forward两段;只包含当前帧时不传播。VideoWorkspace在提交传播前会先调用现有归档保存链路保存当前项目中的 draft/dirty mask,并重新读取 store 中的回显结果;参考帧 seed 因此优先携带稳定的后端source_annotation_id,避免用前端临时 mask id 生成传播结果后,二次传播无法找到旧结果。VideoWorkspace用buildAnnotationPayload()把每个 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_smoothingmetadata,payload 仍可透传给后端兼容处理;当前前端平滑应用会直接改写 polygon 几何并移除该参数。- 前端把传播权重 id、每个 seed、每个方向组装成
steps,一次调用POST /api/ai/propagate/task,include_source=false、save_annotations=true;接口先规范化/校验model字段中的权重 id,再创建processing_tasks.task_type=propagate_masks并投递 Celery,避免长 HTTP 请求阻塞前端等待。 VideoWorkspace记录返回的task_id,轮询GET /api/tasks/{task_id}显示任务 message、步骤进度、已处理帧次和已保存区域数;任务运行期间提供取消传播按钮,调用通用POST /api/tasks/{task_id}/cancel。- Celery worker 逐 step 顺序执行传播,避免多个视频 tracker 并发抢占 GPU;每个 step 开始/完成都会写入
processing_tasks.progress/result/message并发布 Redisseg:progress,Dashboard 可同步显示。每个 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 id,worker 仍会用语义信息识别候选旧传播结果,并在写入目标帧新 polygon 前用目标帧 bbox 重叠做二次确认和清理。写入前这层清理不限制旧结果方向,确保 backward 传播可覆盖早先 forward 传播留下的同物体旧 mask。 - 后端按项目帧序列截取片段,下载对应帧到临时目录,并写成
000000.jpg这类纯数字文件名;这是SAM2VideoPredictor对视频帧排序的要求,和项目库中持久化的frame_%06d.jpg对象名无关。 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 调用内部暂不提供逐帧流式进度。model=sam3当前不支持;SAM 3 video tracker 代码保留但没有接入产品路径。- 后端把传播返回的 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_key、propagation_seed_signature、propagation_direction、source_annotation_id和source_mask_id供后续幂等传播判断;历史geometry_smoothing仅在存在时保留用于兼容判断。 - 前端轮询到已创建区域后刷新
GET /api/ai/annotations并回显新标注;任务结束后如果后端返回 0 个新区域,工作区会明确提示没有生成新的 mask,若是未改变 seed 被跳过则提示未改变 mask 已跳过。处理过帧次大于 0 的成功任务会追加一条本地传播历史片段,用于视频处理进度条显示最近传播范围;annotationToMask()会保留传播来源 metadata,供时间轴视频处理进度条显示蓝色传播区段。
手工绘制与历史栈
- 用户在
ToolsPalette选择多边形、矩形、圆、画笔或橡皮擦工具;创建点和创建线段入口不在工作区左侧工具栏中提供。 CanvasArea将交互坐标转换成像素 polygon。- 多边形工具逐次记录节点,三点后点击首节点或按 Enter 时生成闭合 polygon。
- Canvas 左上角根据当前工具和操作阶段显示上下文短提示;多边形提示会随已放置点数切换,明确 Enter 完成、Esc 取消和点击首节点闭合。提示会在工具或操作状态变化时出现,并在数秒后自动隐藏,避免长期遮挡底图。
- mask path 只在
move、edit_polygon、area_merge和area_remove工具下拦截点击;绘制、画笔、橡皮擦和 AI prompt 工具点击已有 mask 时继续冒泡给 Stage。 - 画笔/橡皮擦尺寸保存在 Zustand 中;拖动期间只保留采样后的圆形笔触预览,鼠标松开后再用
polygon-clipping执行 union/difference,避免拖动中反复重算复杂 polygon。 - 新 mask 写入
pathData、像素segmentation、bbox、area和当前模板分类元数据。 addMask()、setMasks()、updateMask()、clearMasks()会维护maskHistory/maskFuture。- 工作区撤销/重做只保留顶栏按钮和快捷键入口,AI 页保留自己的撤销/重做按钮;工作区由
VideoWorkspace统一处理Ctrl/Cmd+Z、Ctrl/Cmd+Shift+Z和Ctrl/Cmd+Y,并在输入框、下拉框和可编辑文本聚焦时跳过快捷键,避免影响帧范围输入。
Polygon 逐点编辑
- 用户选择“调整多边形”或“拖拽/选择”后点击 Canvas 上的 mask path,
CanvasArea记录selectedMaskId并显示该 mask 第一条 polygon 的顶点控制点和边中点插入手柄。 - 顶点
mousedown/dragstart会立即设置当前顶点选择;拖动过程中通过dragMove实时重算pathData、像素segmentation、bbox、area,不需要先单击顶点再拖动。 - Stage 的
onDragEnd只处理 Stage 自身拖拽;polygon 顶点等子节点拖拽结束事件会被忽略,避免子节点坐标误写入 Canvasposition导致视口跳动。 - 点击边中点手柄会在该边中点插入新顶点;在“调整多边形”工具下双击 polygon path 会在最接近的线段上按双击位置插入新顶点。
- 如果 mask 已有
annotationId,编辑会把saveStatus标成dirty且saved=false。 - 归档保存时复用现有
PATCH /api/ai/annotations/{annotation_id}链路,把更新后的 normalized polygon 写回后端。 - 选中顶点后 Delete/Backspace 可删除顶点;前端保持 polygon 至少三点。
- 未选中具体顶点但选中了 mask 时,Delete/Backspace 从前端 store 删除该 mask;如果包含
annotationId,通过工作区回调调用后端删除接口;删除对象属于传播链或传播 seed 时,删除范围会扩展到同链自动传播 mask,但不移除其他帧独立 AI 推理/人工 mask。 - 普通 mask 和导入 mask 都不显示黄色 seed point,也不提供 seed point 拖动;保存 payload 仍可保留已有
points数据兼容,但画布体验统一为区域选择和 polygon 顶点编辑。
区域合并与去除
- 用户选择
area_merge或area_remove后,点击多个当前帧 mask 组成选择集。 - 合并/去除模式隐藏 polygon 顶点和边中点编辑手柄,并在右下角显示已选数量;少于两个 mask 时操作按钮禁用。
- Canvas 左上角提示布尔选择顺序:第一个选中的是主区域,后续区域参与合并或扣除。
- 布尔选择态按选择顺序区分角色:第一个选中的主区域使用黄色实线轮廓,后续参与合并/扣除的区域使用红色虚线轮廓;所有已选区域填充透明度保持一致,避免被误解为阴影模式异常。
CanvasArea把Mask.segmentation转为polygon-clipping的 MultiPolygon。area_merge使用 union,更新第一个选中的主 mask,并从前端 store 移除后续被合并 mask;如果被移除 mask 已保存,会调用工作区传入的删除回调删除后端标注。执行时会按source_annotation_id、source_mask_id和propagation_seed_key查找其它帧中与当前主区域/参与区域对应的传播 mask,并在每个具备对应关系的帧上执行同一次 union;只删除该帧参与合并的次级 mask,避免把同链但未参与同步的区域整链误删。area_remove使用 difference,从第一个选中的主 mask 中扣除后续选中 mask,扣除对象本身保留;同样会在其它传播帧中找到对应主区域和扣除区域并执行 difference,扣除区域本身继续保留;如果 difference 产生内洞,segmentation保留外圈和 hole ring,metadata.polygonRingCounts记录每个 polygon 的 ring 数,渲染时使用 even-odd fill。- 结果会重算
pathData、segmentation、bbox、area,已保存主 mask 会进入 dirty 状态并复用归档 PATCH 链路;同步到传播帧时保留传播来源和 lineage metadata,避免自动传播帧在时间轴上变成人工/AI 标注帧;带洞结果的面积按外圈减内洞计算;进入调整多边形时,外圈和内洞 ring 都会显示顶点和边中点插入手柄,内洞拖动、插点、保存与回显继续保持中空结构。
GT Mask 导入
- 工作区左侧工具栏“导入 GT Mask”选择图片文件;入口位于“重叠区域去除”之后。
- 前端
importGtMask()以 multipart form-data 调用POST /api/ai/import-gt-mask,携带project_id和frame_id。 - 后端验证项目、帧、模板后使用 OpenCV 读取灰度 mask。
- 后端按非零像素值拆分多类别标签。
- 后端对每个类别的前景做高精度 contour 提取,每个连通域保存为一个
Annotation;轮廓使用未压缩链提取并以较小approxPolyDPepsilon 保留细节,超过点数上限时才逐步增加简化强度或抽样。 points字段可保存距离变换中心 seed point 供数据兼容,mask_data.polygons保存 normalized polygon,mask_data.holes保存与外圈对齐的内洞,mask_data.gt_label_value保存原始像素类别值;导入后的 polygon 与普通 mask 走同一套拓扑锚点统计、边缘平滑、编辑和保存链路。- 前端重新读取项目标注并回显。
annotationToMask()仍可把后端points转成像素坐标保存在 mask 数据中,但 Canvas 不显示 seed point,也不提供拖动;普通 polygon 若没有后端 seed point,保存逻辑可按 polygon 自动计算内部代表点写入,以保持数据兼容。
模板管理
TemplateRegistry从后端读取模板。- 编辑态在组件本地维护分类列表。
- 保存时调用
createTemplate()或updateTemplate()。 - 后端把
classes、rules打包进mapping_rules。 - 返回时再解包给前端。
- 模板详情页和编辑弹窗都支持拖拽调整语义类别层级顺序;拖拽后重算
zIndex,保存到后端模板并刷新当前详情页,maskId保持不变。所有模板都会归一化包含黑色maskId: 0的“待分类”保留类,该类固定在语义分类树最后,不参与删除和拖拽上移。编辑弹窗点击分类后只编辑分类名称,不展示或编辑旧category来源元信息。编辑弹窗中的 JSON 批量导入支持[[colors], [names]]和{colors, names}两种格式,并兼容带前缀、代码块、未加引号 keys、单引号、中文逗号/冒号和尾随逗号的粘贴内容;导入前会先显示分类数量、maskid 分配起点和缺失颜色提示,语法或结构错误以内联错误展示,确认导入后进入编辑态,保存模板时落库。 CanvasArea把当前选中的 mask id 同步到全局selectedMaskIds;切换工具、切换帧或卸载 Canvas 时会清空选择。AISegmentation生成 mask 后会写入全局masks并把生成的 mask id 写入selectedMaskIds;点击 AI 页预览 mask 也会更新selectedMaskIds。- AI 页“推送至工作区编辑”会先检查待推送 AI 候选 mask 是否具备
classId或className;缺少语义分类时清空普通推理反馈,并通过TransientNotice右上角 error toast 提示用户先点右侧语义分类树,不切换模块、不修改工具状态。 AISegmentation卸载时会清理仍缺少classId/className的本页 AI 候选,并同步移除对应selectedMaskIds,避免用户绕过推送按钮从侧栏切到工作区时带入无语义 mask。- AI 页语义校验通过后会切换到工作区并把
activeTool设为edit_polygon;CanvasArea初始读取全局selectedMaskIds,让 AI 页选中的 mask 在工作区继续保持选中。 - 工作区帧/标注异步加载完成后,
hydrateSavedAnnotations()会合并本地未保存 draft mask 和后端已保存 mask,不会用后端回显结果直接覆盖整个masksstore。 OntologyInspector可以选择激活模板和具体分类;项目已有任意 mask 时,用户修改激活模板会先弹出确认框,确认后调用删除标注接口清空当前项目所有已保存标注并清空本地 mask,再切换模板;项目没有任何 mask 时直接切换。具体分类选择结果进入全局 store,供CanvasArea和AISegmentation新建/更新 mask 时使用。- 如果
selectedMaskIds中存在当前 store 的 mask,点击分类时会立即更新这些 mask 的templateId、classId、className、classZIndex、label和color。 - 对属于自动传播链的 mask,分类更新会复用
source_annotation_id、source_mask_id和propagation_seed_key查找同一目标实例在前后帧中的传播结果,并同步更新这些传播 mask 的分类元数据,避免同一物体跨帧语义不一致。 - 同一次点击会把这些已选 mask 移动到前端
masks数组末尾;CanvasArea按数组顺序渲染,后渲染的 Path 显示在最上层,方便用户继续编辑刚换标签的区域。该显示置顶不改变模板zIndex或后端导出语义覆盖规则。 - 已保存 mask 被重新分类后进入
dirty且saved=false,同传播链被同步更新的已保存 mask 也进入dirty,继续复用工作区归档保存的 PATCH 链路。 - 模板保存、删除和 JSON 导入失败使用
TransientNotice非阻塞提示,默认数秒后自动消失。
导出
- 后端根据项目、帧、标注和模板生成 COCO JSON。
- PNG mask 导出会把 normalized polygon 渲染为单标注二值 mask。
- PNG mask 导出还会按
mask_data.class.zIndex或模板z_index从低到高覆盖,生成每帧语义融合 mask。 - ZIP 内写入
semantic_classes.json,记录语义值到类别、颜色和 zIndex 的映射。 - 前端使用“分割结果导出”统一入口替代原 JSON/PNG 两个按钮;点击后在下拉栏选择整体视频、特定范围帧或当前图片,默认选中当前图片,并勾选分开二值 mask、GT_label 黑白图、Pro_label 彩色图和 Mix_label 原图叠加图。选择“特定范围帧”时,导出起止帧输入框和
FrameTimeline的范围拖拽选择共用同一组导出范围状态;选择 Mix_label 时显示透明度滑杆,默认 0.3,并用当前/待导出第一帧做遮罩预览。提交前会保存待归档标注,然后下载统一 ZIP。下载文件名使用{项目库项目名}_seg_T_{起始时间戳}-{结束时间戳}_P_{起始项目帧序号}-{结束项目帧序号}.zip;项目名来自currentProject.name,起止帧按当前导出范围取首尾帧,时间戳格式为0h00m00s000ms,帧号使用项目抽帧后的 1-based 顺序,项目名中的文件系统不安全字符会替换为_。 - 统一导出 ZIP 固定包含
annotations_coco.json、maskid_GT像素值_类别映射.json和原始图片/;原始图片文件名使用视频名称_时间戳_项目帧序号。导出会保留类别真实 maskid,GT_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*支持中文项目名。 - 右侧
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_id、prompt_type、prompt_data、model。propagateMasks()使用POST /api/ai/propagate,请求体为project_id、frame_id、model、seed、direction、max_frames,作为单 seed 同步兼容接口保留。queuePropagationTask()使用POST /api/ai/propagate/task,请求体为project_id、frame_id、model、steps、include_source、save_annotations,返回ProcessingTask。saveAnnotation()使用POST /api/ai/annotate。importGtMask()使用POST /api/ai/import-gt-maskmultipart 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_fps、max_frames、target_width,用于生成标准帧序列。getProjectFrames()返回帧图像 URL、宽高、timestamp_ms和source_frame_number。- 后端
/api/ai/predict当前支持 SAM 2.1 的 point、box、interactive;semantic文本提示禁用并返回 400。 - SAM 2.1 是点/框交互式分割模型,不做文本语义分割;AI 页面已经移除纯文本输入。
- SAM 2.1 点提示和 auto fallback 只返回一个最高分候选,避免同一提示产生多个重叠候选 mask。
- SAM 3 前端入口、后端 registry 入口和状态展示均已禁用;
model=sam3会返回不支持。 - 后端
/api/ai/predict支持可选options:crop_to_prompt会对 point/box/interactive prompt 做局部裁剪推理并回映射 polygon,auto_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 服务。
- SAM:AI 推理测试使用 fake registry。
- 浏览器 Canvas/Konva 图片加载:前端测试 mock
react-konva和use-image。
已知占位设计
以下能力属于当前冻结版本的占位或半可用功能:
- Dashboard 初始快照来自
GET /api/dashboard/overview;任务进度区由processing_tasksqueued/running/success/failed/cancelled 任务生成,处理中统计只计算 queued/running。 - 已保存标注支持通过“应用分类”、polygon 顶点拖动/删除、边中点插入、多 polygon 子区域编辑、中空 mask 内洞 ring 编辑和区域合并/去除进入 dirty 状态并归档更新;区域合并/去除会同步到其它传播帧的对应 mask,并保留传播帧来源 metadata;选中整块 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 聚类和模板自动映射尚未实现。