1
0
Fork 0
mirror of https://github.com/imjasonh/sprues synced 2026-07-07 01:32:23 +00:00
No description
Find a file
2026-05-04 16:28:57 -04:00
out Drop GW-logo runner strips via pixel-intensity-std filter 2026-05-04 13:03:02 -04:00
.gitignore Hash-keyed output dirs; drop runner-strip false positive on dense sprues 2026-05-04 12:44:21 -04:00
cluster.py Initial pipeline: photo → bit segmentation → DINOv2 clustering → labelling sheet 2026-05-04 12:44:21 -04:00
contact_sheet.py Initial pipeline: photo → bit segmentation → DINOv2 clustering → labelling sheet 2026-05-04 12:44:21 -04:00
lessons.md Drop GW-logo runner strips via pixel-intensity-std filter 2026-05-04 13:03:02 -04:00
LICENSE Initial commit 2026-05-04 12:43:40 -04:00
Makefile Makefile + remove root-level sprue images; demo images in README 2026-05-04 12:44:21 -04:00
pipeline.py Makefile + remove root-level sprue images; demo images in README 2026-05-04 12:44:21 -04:00
plan.md Initial pipeline: photo → bit segmentation → DINOv2 clustering → labelling sheet 2026-05-04 12:44:21 -04:00
pyproject.toml Initial pipeline: photo → bit segmentation → DINOv2 clustering → labelling sheet 2026-05-04 12:44:21 -04:00
README.md Update README.md 2026-05-04 16:28:57 -04:00
segment.py Drop GW-logo runner strips via pixel-intensity-std filter 2026-05-04 13:03:02 -04:00
term-instructions.pdf Initial pipeline: photo → bit segmentation → DINOv2 clustering → labelling sheet 2026-05-04 12:44:21 -04:00
uv.lock Initial pipeline: photo → bit segmentation → DINOv2 clustering → labelling sheet 2026-05-04 12:44:21 -04:00

sprues

A local pipeline that takes a photo of a plastic sprue and segments it into individual bit crops, then clusters visually-identical bits so a single human label propagates to every member.

The pipeline runs entirely on classical CV (OpenCV) for segmentation and a local DINOv2 image-embedding model for clustering. No cloud APIs, no LM Studio dependency, sub-minute end-to-end on Apple Silicon.

Usage

Add a new sprue:

uv run python pipeline.py /path/to/sprue-photo.jpg out
# → out/<short-hash>/orig.jpg + outputs
open out/<short-hash>/clusters.html
# Edit out/<short-hash>/labels.json to attach names to clusters; reload.

Re-process every sprue already in out/:

make regen   # iterates out/*/orig.* and re-runs the pipeline on each

Each out/<short-hash>/ contains:

  • orig.jpg — copy of the input photo (committed)
  • 00_feeders.png 01_plastic.png 02_runner.png 03_masks.png — debug overlays at each pipeline stage (committed)
  • crops/ masks/ — per-bit images (gitignored, regenerate via pipeline)
  • clusters.html clusters.json segments.json labels.json — contact sheet, manifests, user-editable label scaffold (gitignored)

Example

Termagants sprue, original photo and segmented bits (one colour per bit, runner overlay in red):

Input Segmented

Test sprues currently in tree (click the dir for full debug overlays):

sprue dir bits clusters
Termagants (Tyranid, organic bits) out/70a05231fe/ 14 12
Intercessors v1 (Space Marines, dense grid) out/f174fe6dbb/ 59 36
Intercessors v2 (Space Marines, similar layout) out/1ee24d83cd/ 63 39
Rhino (vehicle, large flat panels) out/404866c10c/ 17 13

How it works

segment.py — turn a sprue photo into per-bit crops + masks:

  1. Otsu-binarise the photo into a plastic-vs-background mask.
  2. Detect ejector-tab/feeder discs by Hough circles. Filter by sprue-symmetry (centred on a vertical or horizontal axis), filled-disc-with-bright-centre pattern, and at least four radial runner spokes. Zero those discs in the plastic mask.
  3. Erode the plastic; identify the runner network as the eroded connected component whose bbox spans the most of the image (not the largest by area — vehicle bits dominate that). Dilate it back, close pinhole gaps, thicken the walls so adjacent grid-cell bits don't bleed together.
  4. bits_mask = plastic AND NOT runner_full. Each connected component is one bit, with sprue gates trimmed.
  5. Filter out residual runner fragments by combined edge-density and thickness checks; verify each bit has at least some background-facing edge (real bits sit in cells, runner fragments are sandwiched in runner).

The script tries several erosion-kernel sizes and picks the trial with the most accepted bits.

cluster.py — DINOv2 image embeddings + agglomerative clustering on cosine similarity (threshold 0.80). Visually-identical bits land in the same cluster.

contact_sheet.py — render clusters.html showing one row per cluster with all member crops side-by-side, plus an editable labels.json scaffold.

pipeline.py — orchestrator: segment → cluster → contact sheet.

What's NOT in the pipeline

Bit names ("fleshborer", "body") aren't recovered automatically. Names come from the human via labels.json; the clustering ensures one label propagates across every copy of that bit on the sprue.

Files

  • segment.py cluster.py contact_sheet.py pipeline.py — pipeline
  • plan.md — the original architectural plan (kept for context; pipeline has since deviated in places, see lessons.md)
  • lessons.md — design lessons captured during development (which approaches worked, which didn't, why)
  • term-*.jpg term-instructions.pdf intercessors-*.jpg rhino-*.jpg — test inputs
  • out/ — generated outputs (segmentation, clusters, contact sheet), gitignored