修正GT未知类别导入为待分类

- GT Mask 未知 maskid 选择保留时落到黑色 maskid:0 的待分类类别,而不是绿色未定义类别。

- 前端导入预览中未知 maskid 使用黑色待分类覆盖色,并把按钮文案改为导入为待分类。

- 待分类兜底颜色统一为 #000000,和模板保留类、GT_label/Pro_label 导出规则一致。

- 补充后端回归断言并更新 AGENTS 与文档说明,保留 gt_unknown_class 和原始 gt_label_value 供后续重命名追溯。
This commit is contained in:
2026-05-04 06:06:04 +08:00
parent 5d73eacefe
commit 10fe17476d
11 changed files with 47 additions and 28 deletions

View File

@@ -43,6 +43,14 @@ GT_MASK_EMPTY_DETAIL = "GT Mask 图片中没有非背景 maskid 区域。"
GT_IMPORT_MAX_CONTOUR_POINTS = 2048
GT_IMPORT_CONTOUR_EPSILON_RATIO = 0.00075
GT_IMPORT_MIN_CONTOUR_EPSILON = 0.35
RESERVED_UNCLASSIFIED_CLASS = {
"id": "reserved-unclassified",
"name": "待分类",
"color": "#000000",
"zIndex": 0,
"maskId": 0,
"category": "系统保留",
}
def _shared_project_or_404(project_id: int, db: Session, current_user: User) -> Project:
@@ -107,10 +115,11 @@ def _rgb_tuple_to_hex(rgb: tuple[int, int, int]) -> str:
return f"#{values[0]:02x}{values[1]:02x}{values[2]:02x}"
def _template_class_maps(template: Template | None) -> tuple[dict[int, dict[str, Any]], dict[str, dict[str, Any]]]:
def _template_class_maps(template: Template | None) -> tuple[dict[int, dict[str, Any]], dict[str, dict[str, Any]], dict[str, Any]]:
classes = ((template.mapping_rules or {}).get("classes") if template else None) or []
by_maskid: dict[int, dict[str, Any]] = {}
by_color: dict[str, dict[str, Any]] = {}
unclassified = dict(RESERVED_UNCLASSIFIED_CLASS)
for index, item in enumerate(classes):
if not isinstance(item, dict):
continue
@@ -128,16 +137,13 @@ def _template_class_maps(template: Template | None) -> tuple[dict[int, dict[str,
"maskId": maskid,
**({"category": item.get("category")} if item.get("category") else {}),
}
if maskid == 0 or class_meta["id"] == RESERVED_UNCLASSIFIED_CLASS["id"] or class_meta["name"] == RESERVED_UNCLASSIFIED_CLASS["name"]:
unclassified = dict(RESERVED_UNCLASSIFIED_CLASS)
continue
if maskid > 0:
by_maskid[maskid] = class_meta
by_color[color] = class_meta
return by_maskid, by_color
def _gt_unknown_label(token: int | str) -> str:
if isinstance(token, int):
return f"未定义类别 {token}"
return f"未定义颜色 {token}"
return by_maskid, by_color, unclassified
def _load_frame_image(frame: Frame) -> np.ndarray:
@@ -1048,7 +1054,7 @@ async def import_gt_mask(
if resized_to_frame:
label_image = cv2.resize(label_image, (width, height), interpolation=cv2.INTER_NEAREST)
by_maskid, _by_color = _template_class_maps(template)
by_maskid, _by_color, unclassified_class = _template_class_maps(template)
has_template_classes = bool(by_maskid)
fallback_color = _normalize_hex_color(color) or "#22c55e"
@@ -1065,8 +1071,9 @@ async def import_gt_mask(
annotation_label = class_meta["name"]
annotation_color = class_meta["color"]
elif is_unknown:
annotation_label = _gt_unknown_label(label_value)
annotation_color = fallback_color
annotation_label = unclassified_class["name"]
annotation_color = unclassified_class["color"]
class_meta = unclassified_class
else:
annotation_label = f"{label} {label_value}" if len(label_values) > 1 else label
annotation_color = fallback_color

View File

@@ -1664,8 +1664,17 @@ def test_import_gt_mask_handles_unknown_maskid_policy_and_resizes_to_frame(clien
assert undefined_response.status_code == 201
labels = {item["mask_data"]["label"] for item in undefined_response.json()}
assert labels == {"已定义", "未定义类别 2"}
unknown = next(item for item in undefined_response.json() if item["mask_data"]["label"].startswith("未定义"))
assert labels == {"已定义", "待分类"}
unknown = next(item for item in undefined_response.json() if item["mask_data"]["label"] == "待分类")
assert unknown["mask_data"]["color"] == "#000000"
assert unknown["mask_data"]["class"] == {
"id": "reserved-unclassified",
"name": "待分类",
"color": "#000000",
"zIndex": 0,
"maskId": 0,
"category": "系统保留",
}
assert unknown["mask_data"]["gt_unknown_class"] is True
assert unknown["mask_data"]["gt_label_value"] == 2
assert unknown["mask_data"]["gt_resized_to_frame"] is True