优化工作区传播和清空交互

- 手工多边形、矩形和圆在未选语义分类时默认归入 maskid:0 的待分类类别。

- 后端自动传播按来源 annotation/mask/seed key 区分同类多实例,避免多个同类型 mask 传播时互相清理。

- 左侧工具栏在橡皮擦下方新增彩色 AI 自动传播入口,传播权重和范围控件只在进入传播后显示。

- 移除顶栏重复的清空片段遮罩入口,并取消当前清空/DEL 弹窗中的按帧范围清空路径。

- Canvas 右下角显示当前帧:XX/XXX,并调整布尔操作浮层位置避免重叠。

- 更新前端和后端回归测试,覆盖待分类默认、工具栏自动传播和同类多实例传播。

- 同步 AGENTS 与 doc 文档,说明新的工具栏、清空和传播行为。
This commit is contained in:
2026-05-04 00:26:11 +08:00
parent 061f4ed25b
commit 093ef6c63a
14 changed files with 307 additions and 534 deletions

View File

@@ -742,6 +742,87 @@ def test_propagation_task_runner_replaces_legacy_or_different_weight_results(cli
assert annotations[0].mask_data["polygons"] == [output_polygon]
def test_propagation_task_runner_keeps_same_class_seeds_separate(client, db_session, monkeypatch):
project = client.post("/api/projects", json={"name": "Propagation Multi Instance"}).json()
frames = [
client.post(f"/api/projects/{project['id']}/frames", json={
"project_id": project["id"],
"frame_index": idx,
"image_url": f"frames/{idx}.jpg",
"width": 640,
"height": 360,
}).json()
for idx in range(2)
]
output_by_source = {
7: [[0.10, 0.10], [0.20, 0.10], [0.20, 0.20]],
8: [[0.70, 0.70], [0.80, 0.70], [0.80, 0.80]],
}
task = ProcessingTask(
task_type="propagate_masks",
status="queued",
progress=0,
project_id=project["id"],
payload={
"project_id": project["id"],
"frame_id": frames[0]["id"],
"model": "sam2.1_hiera_tiny",
"include_source": False,
"save_annotations": True,
"steps": [
{
"direction": "forward",
"max_frames": 2,
"seed": {
"polygons": [[[0.05, 0.05], [0.15, 0.05], [0.15, 0.15]]],
"label": "胆囊",
"color": "#ff0000",
"source_annotation_id": 7,
"source_mask_id": "annotation-7",
"class_metadata": {"id": "gallbladder", "name": "胆囊"},
},
},
{
"direction": "forward",
"max_frames": 2,
"seed": {
"polygons": [[[0.65, 0.65], [0.75, 0.65], [0.75, 0.75]]],
"label": "胆囊",
"color": "#ff0000",
"source_annotation_id": 8,
"source_mask_id": "annotation-8",
"class_metadata": {"id": "gallbladder", "name": "胆囊"},
},
},
],
},
)
db_session.add(task)
db_session.commit()
db_session.refresh(task)
monkeypatch.setattr("services.propagation_task_runner.download_file", lambda object_name: b"jpeg")
monkeypatch.setattr("services.propagation_task_runner.publish_task_progress_event", lambda event_task: None)
def fake_propagate_video(model, frame_paths, source_frame_index, seed, direction, max_frames):
output_polygon = output_by_source[seed["source_annotation_id"]]
return [
{"frame_index": 0, "polygons": [seed["polygons"][0]], "scores": [0.9]},
{"frame_index": 1, "polygons": [output_polygon], "scores": [0.8]},
]
monkeypatch.setattr("services.propagation_task_runner.sam_registry.propagate_video", fake_propagate_video)
result = run_propagate_project_task(db_session, task.id)
assert result["created_annotation_count"] == 2
assert result["deleted_annotation_count"] == 0
annotations = db_session.query(Annotation).filter(Annotation.project_id == project["id"]).order_by(Annotation.id).all()
assert [annotation.mask_data["source_annotation_id"] for annotation in annotations] == [7, 8]
assert [annotation.mask_data["polygons"][0] for annotation in annotations] == [output_by_source[7], output_by_source[8]]
def test_propagation_task_runner_replaces_downstream_result_from_middle_frame_manual_seed(client, db_session, monkeypatch):
project = client.post("/api/projects", json={"name": "Propagation Middle Frame Replacement"}).json()
frames = [