1
0
Fork 0
mirror of https://github.com/imjasonh/sprues synced 2026-07-07 03:02:20 +00:00
sprues/contact_sheet.py
Jason Hall c555cd2ea7 Initial pipeline: photo → bit segmentation → DINOv2 clustering → labelling sheet
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>
2026-05-04 12:44:21 -04:00

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)