docs(pbf): archive Full production provenance and engineering history

This commit is contained in:
OpenAI Codex
2026-10-13 14:02:56 +08:00
parent 1fef4c6dd1
commit 07d16c38a8
381 changed files with 107826 additions and 527 deletions

View File

@@ -0,0 +1,11 @@
# Visual Object Matcher
输入:
1. 已冻结的 NavSea Standard Visual Object Catalog;
2. Provider 的 Actual Anonymous Visual Object Catalog。
匹配依据只使用视觉结构:Sprite 图像、组件角色、组件顺序、几何角色、line/fill/pattern/text role 等。
禁止使用 Provider source-layer 名称、Provider 名称、对象类名称或字段名称直接决定标准 Layer。
Provider source-layer 仅允许在匹配完成后用于 blind validation / audit,不能进入 matcher input。

View File

@@ -0,0 +1,358 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
import math
from collections import defaultdict
from pathlib import Path
from typing import Any
import numpy as np
from PIL import Image, ImageFilter
def load_catalog(path: Path) -> dict[str, Any]:
return json.loads(path.read_text(encoding="utf-8"))
def load_atlas(catalog_dir: Path) -> Image.Image:
return Image.open(catalog_dir / "_sprite" / "sprite.png").convert("RGBA")
def visual_feature(atlas: Image.Image, meta: dict[str, Any]) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
crop = atlas.crop((
int(meta["x"]), int(meta["y"]),
int(meta["x"]) + int(meta["width"]),
int(meta["y"]) + int(meta["height"]),
))
alpha = np.asarray(crop.getchannel("A"))
ys, xs = np.where(alpha > 8)
if len(xs):
crop = crop.crop((int(xs.min()), int(ys.min()), int(xs.max()) + 1, int(ys.max()) + 1))
w, h = crop.size
scale = min(54.0 / max(w, 1), 54.0 / max(h, 1))
nw = max(1, round(w * scale))
nh = max(1, round(h * scale))
crop = crop.resize((nw, nh), Image.Resampling.LANCZOS)
canvas = Image.new("RGBA", (64, 64), (255, 255, 255, 0))
canvas.alpha_composite(crop, ((64 - nw) // 2, (64 - nh) // 2))
arr = np.asarray(canvas, dtype=np.float32) / 255.0
a = arr[:, :, 3]
rgb = arr[:, :, :3] * a[:, :, None] + (1.0 - a[:, :, None])
edge = np.asarray(canvas.convert("L").filter(ImageFilter.FIND_EDGES), dtype=np.float32) / 255.0
return rgb, a, edge
def cosine(a: np.ndarray, b: np.ndarray) -> float:
aa = a.ravel()
bb = b.ravel()
denom = float(np.linalg.norm(aa) * np.linalg.norm(bb))
return float(np.dot(aa, bb) / denom) if denom else 0.0
def image_similarity(a: tuple[np.ndarray, np.ndarray, np.ndarray], b: tuple[np.ndarray, np.ndarray, np.ndarray]) -> float:
rgb1, alpha1, edge1 = a
rgb2, alpha2, edge2 = b
rgb_score = 1.0 - float(np.mean((rgb1 - rgb2) ** 2))
alpha_score = cosine(alpha1, alpha2)
edge_score = cosine(edge1, edge2)
return 0.45 * rgb_score + 0.35 * alpha_score + 0.20 * edge_score
def unique_sprite_objects(catalog: dict[str, Any]) -> list[dict[str, Any]]:
seen: set[tuple[str, str, str | None]] = set()
out: list[dict[str, Any]] = []
for obj in catalog.get("visual_objects", []):
if not obj.get("resource_key") or not obj.get("resource_meta"):
continue
key = (
obj.get("component_role") or "",
obj.get("visual_hash") or "",
obj.get("standard_layer_name"),
obj.get("group_id"),
)
if key in seen:
continue
seen.add(key)
out.append(obj)
return out
def build_features(catalog: dict[str, Any], catalog_dir: Path) -> tuple[list[dict[str, Any]], list[tuple[np.ndarray, np.ndarray, np.ndarray]]]:
atlas = load_atlas(catalog_dir)
objects = unique_sprite_objects(catalog)
features = [visual_feature(atlas, obj["resource_meta"]) for obj in objects]
return objects, features
def compatible(cm: dict[str, Any], std: dict[str, Any]) -> bool:
if cm.get("component_role") != std.get("component_role"):
return False
cg = cm.get("geometry_role")
sg = std.get("geometry_role")
if cg and sg and cg != sg:
return False
return True
def classify_object(
cm_obj: dict[str, Any],
cm_feat: tuple[np.ndarray, np.ndarray, np.ndarray],
std_objects: list[dict[str, Any]],
std_features: list[tuple[np.ndarray, np.ndarray, np.ndarray]],
) -> dict[str, Any]:
layer_best: dict[str, tuple[float, str]] = {}
for std, feat in zip(std_objects, std_features):
layer = std.get("standard_layer_name")
if not layer or not compatible(cm_obj, std):
continue
score = image_similarity(cm_feat, feat)
prev = layer_best.get(layer)
if prev is None or score > prev[0]:
layer_best[layer] = (score, std["visual_object_id"])
ranked = sorted(
(
{"layer": layer, "score": score, "standard_visual_object_id": vo_id}
for layer, (score, vo_id) in layer_best.items()
),
key=lambda x: x["score"],
reverse=True,
)
top = ranked[0] if ranked else None
second = ranked[1] if len(ranked) > 1 else None
if not top:
status = "UNKNOWN"
else:
margin = top["score"] - (second["score"] if second else 0.0)
if top["score"] >= 0.90 and margin >= 0.025:
status = "MATCH"
elif top["score"] >= 0.82:
status = "PARTIAL"
else:
status = "UNKNOWN"
return {
"visual_object_id": cm_obj["visual_object_id"],
"group_id": cm_obj["group_id"],
"component_role": cm_obj.get("component_role"),
"geometry_role": cm_obj.get("geometry_role"),
"resource_key": cm_obj.get("resource_key"),
"status": status,
"best": top,
"alternatives": ranked[1:4],
}
def group_scores(object_matches: list[dict[str, Any]]) -> dict[str, dict[str, Any]]:
grouped: dict[str, list[dict[str, Any]]] = defaultdict(list)
for item in object_matches:
grouped[item["group_id"]].append(item)
results: dict[str, dict[str, Any]] = {}
for group_id, items in grouped.items():
evidence: dict[str, list[float]] = defaultdict(list)
for item in items:
for cand in ([item["best"]] if item.get("best") else []) + item.get("alternatives", []):
if cand and cand["score"] >= 0.78:
evidence[cand["layer"]].append(cand["score"])
ranking = []
for layer, scores in evidence.items():
scores = sorted(scores, reverse=True)
strongest = scores[: min(12, len(scores))]
coverage = min(1.0, len(scores) / max(3.0, len(items) * 0.25))
score = (sum(strongest) / len(strongest)) * (0.65 + 0.35 * coverage)
ranking.append({
"layer": layer,
"score": score,
"evidence_count": len(scores),
"best_evidence": scores[:5],
})
ranking.sort(key=lambda x: x["score"], reverse=True)
top = ranking[0] if ranking else None
second = ranking[1] if len(ranking) > 1 else None
if top:
margin = top["score"] - (second["score"] if second else 0.0)
dominance = top["evidence_count"] / max(1, sum(x["evidence_count"] for x in ranking))
if top["score"] >= 0.86 and margin >= 0.035 and dominance >= 0.40:
status = "MATCH"
elif top["score"] >= 0.80:
status = "PARTIAL"
else:
status = "UNKNOWN"
else:
status = "UNKNOWN"
results[group_id] = {
"group_id": group_id,
"status": status,
"best": top,
"alternatives": ranking[1:4],
"object_count": len(items),
}
return results
def apply_group_result(object_matches: list[dict[str, Any]], groups: dict[str, dict[str, Any]]) -> None:
for item in object_matches:
g = groups.get(item["group_id"])
if g and g["status"] == "MATCH":
item["final_layer"] = g["best"]["layer"]
item["final_basis"] = "GROUP_VISUAL_CONSENSUS"
item["final_status"] = "MATCH"
elif item["status"] == "MATCH":
item["final_layer"] = item["best"]["layer"]
item["final_basis"] = "OBJECT_VISUAL_MATCH"
item["final_status"] = "MATCH"
elif item["status"] == "PARTIAL":
item["final_layer"] = item["best"]["layer"] if item.get("best") else None
item["final_basis"] = "OBJECT_VISUAL_PARTIAL"
item["final_status"] = "PARTIAL"
else:
item["final_layer"] = None
item["final_basis"] = "INSUFFICIENT_VISUAL_EVIDENCE"
item["final_status"] = "UNKNOWN"
def blind_validation(
cm_catalog: dict[str, Any],
group_results: dict[str, dict[str, Any]],
) -> dict[str, Any]:
# This is deliberately post-match audit only. Raw source-layer names are read
# after predictions have already been produced.
provenance = {g["group_id"]: g.get("provenance", {}) for g in cm_catalog.get("groups", [])}
rows = []
comparable = correct = 0
for group_id, pred in sorted(group_results.items()):
raw_name = provenance.get(group_id, {}).get("source_layer")
predicted = pred.get("best", {}).get("layer") if pred.get("best") else None
row = {
"group_id": group_id,
"raw_source_layer_for_audit_only": raw_name,
"predicted_standard_layer": predicted,
"status": pred.get("status"),
"same_name_after_blind_match": bool(raw_name and predicted and raw_name == predicted),
}
if raw_name and predicted and not str(raw_name).startswith("s52_"):
comparable += 1
if raw_name == predicted:
correct += 1
rows.append(row)
return {
"note": "Raw CM95 source-layer is used only after matching for blind validation; it is not matcher input.",
"comparable_group_count": comparable,
"same_name_count": correct,
"same_name_rate": (correct / comparable) if comparable else None,
"rows": rows,
}
def html_report(result: dict[str, Any]) -> str:
stats = result["summary"]
val = result["blind_validation"]
rows = []
for g in result["group_matches"]:
best = g.get("best") or {}
alts = ", ".join(f'{x["layer"]} {x["score"]:.3f}' for x in g.get("alternatives", []))
rows.append(
"<tr>"
f"<td><code>{g['group_id']}</code></td>"
f"<td>{g['status']}</td>"
f"<td>{best.get('layer','')}</td>"
f"<td>{best.get('score',0):.3f}</td>"
f"<td>{g['object_count']}</td>"
f"<td>{alts}</td>"
"</tr>"
)
return f"""<!doctype html><meta charset='utf-8'><title>CM95 Visual Object Layer Match</title>
<style>
body{{font-family:Arial,sans-serif;margin:24px;color:#17202a}} table{{border-collapse:collapse;width:100%}}
th,td{{border:1px solid #d8dee6;padding:7px;text-align:left;font-size:13px}} th{{background:#eef2f6}}
code{{font-size:11px}} .box{{display:inline-block;margin:0 12px 12px 0;padding:10px 14px;border:1px solid #d8dee6;border-radius:8px}}
</style>
<h1>CM95 Visual Object → NavSea Standard Layer</h1>
<p>匹配程序不读取 CM95 source-layer 名称;Layer 名仅来自 NavSea 已冻结 Standard Visual Object Catalog。</p>
<div class='box'>CM95 Visual Objects <b>{stats['cm95_sprite_visual_objects']}</b></div>
<div class='box'>MATCH <b>{stats['object_match']}</b></div>
<div class='box'>PARTIAL <b>{stats['object_partial']}</b></div>
<div class='box'>UNKNOWN <b>{stats['object_unknown']}</b></div>
<div class='box'>Blind audit {val['same_name_count']}/{val['comparable_group_count']}</div>
<h2>匿名 Group 的视觉匹配</h2>
<table><thead><tr><th>匿名 Group</th><th>状态</th><th>预测标准 Layer</th><th>分数</th><th>VO 数</th><th>候选</th></tr></thead>
<tbody>{''.join(rows)}</tbody></table>
<p>详细到每个 Visual Object 的匹配、候选和 blind validation 见 <code>match-results.json</code>。</p>
"""
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--standard", required=True)
ap.add_argument("--cm95", required=True)
ap.add_argument("--output", required=True)
args = ap.parse_args()
std_path = Path(args.standard)
cm_path = Path(args.cm95)
output = Path(args.output)
output.mkdir(parents=True, exist_ok=True)
std_catalog = load_catalog(std_path)
cm_catalog = load_catalog(cm_path)
std_objects, std_features = build_features(std_catalog, std_path.parent.parent / "navsea-anonymous")
cm_objects, cm_features = build_features(cm_catalog, cm_path.parent)
# Sanitized match inputs deliberately omit CM95 provenance/source-layer.
sanitized = [
{
"visual_object_id": o["visual_object_id"],
"group_id": o["group_id"],
"component_role": o.get("component_role"),
"geometry_role": o.get("geometry_role"),
"resource_key": o.get("resource_key"),
}
for o in cm_objects
]
(output / "cm95-match-input-sanitized.json").write_text(
json.dumps(sanitized, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
)
object_matches = [
classify_object(obj, feat, std_objects, std_features)
for obj, feat in zip(cm_objects, cm_features)
]
groups = group_scores(object_matches)
apply_group_result(object_matches, groups)
validation = blind_validation(cm_catalog, groups)
result = {
"schema": "style-visual-object-match/v1",
"method": {
"cm95_source_layer_used_for_matching": False,
"provider_name_used_for_matching": False,
"resource_key_text_semantics_used_for_matching": False,
"visual_inputs": ["sprite pixels", "component role", "geometry role", "anonymous group membership"],
"standard_labels_from": "frozen NavSea Standard Visual Object Catalog",
},
"summary": {
"standard_sprite_visual_objects": len(std_objects),
"cm95_sprite_visual_objects": len(cm_objects),
"object_match": sum(x["final_status"] == "MATCH" for x in object_matches),
"object_partial": sum(x["final_status"] == "PARTIAL" for x in object_matches),
"object_unknown": sum(x["final_status"] == "UNKNOWN" for x in object_matches),
"group_match": sum(x["status"] == "MATCH" for x in groups.values()),
"group_partial": sum(x["status"] == "PARTIAL" for x in groups.values()),
"group_unknown": sum(x["status"] == "UNKNOWN" for x in groups.values()),
},
"group_matches": sorted(groups.values(), key=lambda x: x["group_id"]),
"visual_object_matches": object_matches,
"blind_validation": validation,
}
(output / "match-results.json").write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
(output / "report.html").write_text(html_report(result), encoding="utf-8")
print(json.dumps(result["summary"], ensure_ascii=False, indent=2))
print(json.dumps({k: validation[k] for k in ("comparable_group_count", "same_name_count", "same_name_rate")}, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()