#!/usr/bin/env python3 """把人工视觉复核结果应用到 CM95 Anonymous Visual Object Catalog。 本脚本不参与视觉判断,也不读取 CM95 source-layer 来决定 Layer。 它只消费已经完成的 reviewed anchors,并生成独立的 Layer Assignment 派生结果。 Anonymous Catalog 本身保持不变。 """ from __future__ import annotations import argparse import csv import html import json from collections import Counter, defaultdict from pathlib import Path from typing import Any def load_json(path: Path) -> dict[str, Any]: return json.loads(path.read_text(encoding="utf-8")) def anchor_layers(anchor: dict[str, Any] | None) -> list[str]: if not anchor: return [] if anchor.get("standard_layer"): return [str(anchor["standard_layer"])] return [str(x) for x in anchor.get("standard_layers", []) if x] def build_html(result: dict[str, Any]) -> str: summary = result["summary"] layer_rows: list[str] = [] for layer, stats in sorted(result["layer_summary"].items()): layer_rows.append( "" f"{html.escape(layer)}" f"{stats['MATCH']}" f"{stats['PARTIAL']}" f"{stats['occurrences']}" "" ) ambiguous_rows: list[str] = [] for item in result["ambiguous_summary"]: ambiguous_rows.append( "" f"{html.escape(' / '.join(item['candidate_standard_layers']))}" f"{item['resource_count']}" f"{item['occurrence_count']}" "" ) group_rows: list[str] = [] for row in result["group_summary"]: dist = ", ".join( f"{html.escape(k)}={v}" for k, v in sorted(row["assigned_layer_counts"].items()) ) or "—" group_rows.append( "" f"{html.escape(row['group_id'])}" f"{row['named_occurrences']}" f"{row['ambiguous_occurrences']}" f"{row['component_occurrences']}" f"{row['unknown_occurrences']}" f"{dist}" "" ) return f""" CM95 Visual Object Layer Assignment v1

CM95 Anonymous Visual Object → NavSea Standard Layer

本结果只应用经人工视觉复核的共同图标/视觉家族锚点。CM95 source-layer 名称、Provider 字段语义和对象类名称均未参与 Layer 判定。

CM95 Visual Objects {summary['total_visual_objects']}
Sprite-backed {summary['sprite_backed_visual_objects']}
唯一 Layer 已命名 {summary['uniquely_named_visual_objects']}
MATCH {summary['match_visual_objects']}
PARTIAL {summary['partial_visual_objects']}
AMBIGUOUS {summary['ambiguous_visual_objects']}
COMPONENT {summary['component_visual_objects']}
UNKNOWN {summary['unknown_visual_objects']}
Reviewed anchors {summary['reviewed_anchor_count']}
状态定义: MATCH = 实际图形基本一致; PARTIAL = 不是同一完整图形,但视觉家族/组件足以支持唯一标准 Layer; AMBIGUOUS = 图形证据只能得到多个标准 Layer 候选,禁止强行选一个; COMPONENT = 只是完整 Visual Object 的组成部分,不单独分配 Layer; UNKNOWN = 当前视觉证据不足。

唯一命名的标准 Layer

{''.join(layer_rows)}
标准 LayerMATCHPARTIALVO occurrence

视觉歧义

{''.join(ambiguous_rows) if ambiguous_rows else ''}
候选标准 Layer资源数VO occurrence
无

匿名 Group 分布

Group 仅用于审计组合范围,不能由整个 group 直接继承一个 Layer。

{''.join(group_rows)}
Group唯一命名AMBIGUOUSCOMPONENTUNKNOWN唯一 Layer 分布

逐 Visual Object 明细见 cm95-layer-assignment-v1.json; 资源级摘要见 cm95-layer-assignment-v1.csv。 Anonymous Catalog 没有被修改。

""" def main() -> None: ap = argparse.ArgumentParser() ap.add_argument("--anonymous", required=True) ap.add_argument("--reviewed-anchors", required=True) ap.add_argument("--output", required=True) args = ap.parse_args() anon_path = Path(args.anonymous) anchor_path = Path(args.reviewed_anchors) out = Path(args.output) out.mkdir(parents=True, exist_ok=True) anon = load_json(anon_path) review = load_json(anchor_path) anchors = { x["cm95_resource_key"]: x for x in review.get("anchors", []) } group_provenance = { g["group_id"]: g.get("provenance", {}) for g in anon.get("groups", []) } assignments: list[dict[str, Any]] = [] layer_counts: dict[str, Counter] = defaultdict(Counter) group_counts: dict[str, Counter] = defaultdict(Counter) resource_status: dict[str, Counter] = defaultdict(Counter) ambiguous_occurrence_counts: Counter = Counter() for obj in anon.get("visual_objects", []): resource_key = obj.get("resource_key") anchor = anchors.get(resource_key) candidate_layers = anchor_layers(anchor) if anchor and anchor.get("status") in {"MATCH", "PARTIAL"} and len(candidate_layers) == 1: status = str(anchor["status"]) layer = candidate_layers[0] basis = "REVIEWED_VISUAL_ANCHOR" std_resource = anchor.get("standard_resource_key") layer_counts[layer][status] += 1 layer_counts[layer]["occurrences"] += 1 group_counts[obj["group_id"]][layer] += 1 group_counts[obj["group_id"]]["__named__"] += 1 resource_status[resource_key][status] += 1 elif anchor and anchor.get("status") == "AMBIGUOUS" and len(candidate_layers) >= 2: status = "AMBIGUOUS" layer = None basis = "REVIEWED_VISUAL_AMBIGUITY" std_resource = anchor.get("standard_resource_key") group_counts[obj["group_id"]]["__ambiguous__"] += 1 resource_status[resource_key]["AMBIGUOUS"] += 1 ambiguous_occurrence_counts[tuple(sorted(candidate_layers))] += 1 elif anchor and anchor.get("status") == "COMPONENT": status = "COMPONENT" layer = None candidate_layers = [] basis = "REVIEWED_COMPONENT_ONLY" std_resource = None group_counts[obj["group_id"]]["__component__"] += 1 resource_status[resource_key]["COMPONENT"] += 1 else: status = "UNKNOWN" layer = None candidate_layers = [] basis = "INSUFFICIENT_VISUAL_EVIDENCE" std_resource = None group_counts[obj["group_id"]]["__unknown__"] += 1 if resource_key: resource_status[resource_key]["UNKNOWN"] += 1 assignments.append({ "visual_object_id": obj["visual_object_id"], "group_id": obj["group_id"], "component_role": obj.get("component_role"), "geometry_role": obj.get("geometry_role"), "cm95_resource_key": resource_key, "assignment_status": status, "assigned_standard_layer": layer, "candidate_standard_layers": candidate_layers, "basis": basis, "reviewed_standard_resource_key": std_resource, "component_kind": anchor.get("component_kind") if anchor and status == "COMPONENT" else None, # provenance 仅用于结果审计;不参与上述判定。 "provenance_for_audit_only": obj.get("provenance"), }) for obj in anon.get("unresolved_visual_objects", []): assignments.append({ "visual_object_id": obj["visual_object_id"], "group_id": obj["group_id"], "component_role": obj.get("component_role"), "geometry_role": obj.get("geometry_role"), "cm95_resource_key": None, "assignment_status": "UNKNOWN", "assigned_standard_layer": None, "candidate_standard_layers": [], "basis": "UNRESOLVED_DYNAMIC_VISUAL", "reviewed_standard_resource_key": None, "component_kind": None, "provenance_for_audit_only": obj.get("provenance"), }) group_counts[obj["group_id"]]["__unknown__"] += 1 group_summary: list[dict[str, Any]] = [] for gid in sorted(group_provenance): c = group_counts[gid] group_summary.append({ "group_id": gid, "named_occurrences": c["__named__"], "ambiguous_occurrences": c["__ambiguous__"], "component_occurrences": c["__component__"], "unknown_occurrences": c["__unknown__"], "assigned_layer_counts": { k: v for k, v in c.items() if not k.startswith("__") }, "provenance_for_audit_only": group_provenance[gid], }) layer_summary: dict[str, Any] = {} for layer, c in layer_counts.items(): layer_summary[layer] = { "MATCH": c["MATCH"], "PARTIAL": c["PARTIAL"], "occurrences": c["occurrences"], } all_resource_keys = sorted({ o.get("resource_key") for o in anon.get("visual_objects", []) if o.get("resource_key") }) resource_summary: list[dict[str, Any]] = [] for key in all_resource_keys: a = anchors.get(key) layers = anchor_layers(a) status = str(a["status"]) if a else "UNKNOWN" unique_layer = layers[0] if status in {"MATCH", "PARTIAL"} and len(layers) == 1 else None c = resource_status[key] resource_summary.append({ "cm95_resource_key": key, "assignment_status": status, "assigned_standard_layer": unique_layer, "candidate_standard_layers": layers, "reviewed_standard_resource_key": a.get("standard_resource_key") if a else None, "component_kind": a.get("component_kind") if a and status == "COMPONENT" else None, "occurrence_count": sum(c.values()), }) ambiguous_resource_counts: Counter = Counter() for key, a in anchors.items(): if a.get("status") == "AMBIGUOUS": ambiguous_resource_counts[tuple(sorted(anchor_layers(a)))] += 1 ambiguous_summary = [ { "candidate_standard_layers": list(layer_set), "resource_count": ambiguous_resource_counts[layer_set], "occurrence_count": ambiguous_occurrence_counts[layer_set], } for layer_set in sorted(ambiguous_resource_counts) ] total = len(assignments) match_count = sum(x["assignment_status"] == "MATCH" for x in assignments) partial_count = sum(x["assignment_status"] == "PARTIAL" for x in assignments) ambiguous_count = sum(x["assignment_status"] == "AMBIGUOUS" for x in assignments) component_count = sum(x["assignment_status"] == "COMPONENT" for x in assignments) unknown_count = sum(x["assignment_status"] == "UNKNOWN" for x in assignments) uniquely_named = match_count + partial_count sprite_backed = sum(bool(x.get("cm95_resource_key")) for x in assignments) unique_layers: set[str] = set() for a in anchors.values(): unique_layers.update(anchor_layers(a)) result = { "schema": "cm95-visual-object-layer-assignment/v1", "source_anonymous_catalog": str(anon_path), "reviewed_anchor_file": str(anchor_path), "policy": { "cm95_source_layer_used_for_assignment": False, "provider_field_semantics_used_for_assignment": False, "standard_layer_is_output_only": True, "unknown_is_allowed": True, "ambiguous_is_allowed": True, "component_is_allowed": True, "whole_group_inheritance_forbidden": True, "anonymous_catalog_modified": False, }, "summary": { "total_visual_objects": total, "sprite_backed_visual_objects": sprite_backed, "reviewed_anchor_count": len(anchors), "reviewed_standard_layer_candidates": sorted(unique_layers), "uniquely_named_visual_objects": uniquely_named, "match_visual_objects": match_count, "partial_visual_objects": partial_count, "ambiguous_visual_objects": ambiguous_count, "component_visual_objects": component_count, "unknown_visual_objects": unknown_count, "uniquely_named_ratio": (uniquely_named / total) if total else 0.0, "sprite_backed_uniquely_named_ratio": (uniquely_named / sprite_backed) if sprite_backed else 0.0, }, "layer_summary": layer_summary, "ambiguous_summary": ambiguous_summary, "resource_summary": resource_summary, "group_summary": group_summary, "assignments": assignments, } (out / "cm95-layer-assignment-v1.json").write_text( json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8" ) (out / "cm95-layer-assignment-v1.html").write_text( build_html(result), encoding="utf-8" ) with (out / "cm95-layer-assignment-v1.csv").open("w", encoding="utf-8-sig", newline="") as f: writer = csv.DictWriter( f, lineterminator="\n", fieldnames=[ "cm95_resource_key", "assignment_status", "assigned_standard_layer", "candidate_standard_layers", "reviewed_standard_resource_key", "component_kind", "occurrence_count", ], ) writer.writeheader() for row in resource_summary: writer.writerow({ **row, "candidate_standard_layers": ";".join(row["candidate_standard_layers"]), }) print(json.dumps(result["summary"], ensure_ascii=False, indent=2)) print(json.dumps(result["layer_summary"], ensure_ascii=False, indent=2)) print(json.dumps(result["ambiguous_summary"], ensure_ascii=False, indent=2)) if __name__ == "__main__": main()