mirror of
https://github.com/imjasonh/sprues
synced 2026-07-07 03:02:20 +00:00
OpenCV-based runner subtraction with feeder-disc detection (Hough + symmetry + pin-pattern + spoke-count constraints). Tested on Termagant, two Intercessor variants, and a Rhino vehicle sprue. See README and lessons.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
87 lines
2.7 KiB
Python
87 lines
2.7 KiB
Python
"""Render a contact sheet HTML so the user can label clusters at a glance.
|
|
|
|
Each row shows one cluster and all its member crops side-by-side. The user
|
|
edits `out/labels.json` to assign a human-readable name (e.g. "fleshborer")
|
|
to each cluster_id.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import html
|
|
import json
|
|
from pathlib import Path
|
|
|
|
|
|
def render(out_dir: Path) -> Path:
|
|
clusters = json.loads((out_dir / "clusters.json").read_text())
|
|
by_cluster: dict[str, list[str]] = clusters["by_cluster"]
|
|
labels_path = out_dir / "labels.json"
|
|
labels = json.loads(labels_path.read_text()) if labels_path.exists() else {}
|
|
|
|
rows = []
|
|
for cid_str, members in sorted(by_cluster.items(), key=lambda kv: int(kv[0])):
|
|
cid = int(cid_str)
|
|
key = f"cluster_{cid:03d}"
|
|
label = labels.get(key, "")
|
|
thumbs = "".join(
|
|
f'<img src="crops/{m}.png" title="{m}" alt="{m}">'
|
|
for m in members
|
|
)
|
|
rows.append(f"""
|
|
<tr>
|
|
<td class="cid">{cid:03d}</td>
|
|
<td class="count">{len(members)}</td>
|
|
<td class="label">
|
|
<span class="label-text">{html.escape(label) or "—"}</span>
|
|
<code class="key">{key}</code>
|
|
</td>
|
|
<td class="thumbs">{thumbs}</td>
|
|
</tr>""")
|
|
|
|
html_doc = f"""<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>Sprue clusters</title>
|
|
<style>
|
|
body {{ font-family: -apple-system, system-ui, sans-serif; margin: 24px; background: #fafafa; }}
|
|
h1 {{ margin-top: 0; }}
|
|
p.help {{ color: #666; max-width: 60ch; }}
|
|
table {{ border-collapse: collapse; width: 100%; background: #fff; }}
|
|
th, td {{ border: 1px solid #e0e0e0; padding: 8px; vertical-align: top; }}
|
|
th {{ background: #f0f0f0; text-align: left; }}
|
|
td.cid {{ font-family: ui-monospace, monospace; color: #888; width: 4ch; }}
|
|
td.count {{ width: 4ch; text-align: right; color: #666; }}
|
|
td.label {{ width: 22ch; }}
|
|
td.label .label-text {{ font-weight: 600; display: block; }}
|
|
td.label .key {{ color: #888; font-size: 0.85em; }}
|
|
td.thumbs img {{ height: 96px; margin: 2px; border: 1px solid #ddd; background: #fff; }}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>Sprue clusters</h1>
|
|
<p class="help">
|
|
Each row is a cluster of visually-similar bits. Edit
|
|
<code>out/labels.json</code> to set the <code>"cluster_NNN"</code> key to the
|
|
real bit name (e.g. <code>"fleshborer"</code>). Reload this page to see the
|
|
labels appear here.
|
|
</p>
|
|
<table>
|
|
<thead><tr><th>id</th><th>n</th><th>label</th><th>members</th></tr></thead>
|
|
<tbody>
|
|
{"".join(rows)}
|
|
</tbody>
|
|
</table>
|
|
</body>
|
|
</html>
|
|
"""
|
|
out_path = out_dir / "clusters.html"
|
|
out_path.write_text(html_doc)
|
|
print(f"[contact-sheet] wrote {out_path}")
|
|
return out_path
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import sys
|
|
out_dir = Path(sys.argv[1] if len(sys.argv) > 1 else "out")
|
|
render(out_dir)
|