← back to Majilite Onboard
scripts/crop_swatches.py
110 lines
#!/usr/bin/env python3
"""
Crop 160 Majilite product swatches out of 8 pre-rendered PDF pages.
Each page (page-02..page-09) is 1875x2487 px, near-black, holding a 4-col x 5-row
grid of 20 square-ish fabric swatches, each with a 2-line white text label centered
below it. We detect the swatch grid via brightness projection:
- Column bands: sum brightness per column -> 4 wide high-mass bands (the columns),
separated by black gutters.
- Row bands: sum brightness per row -> thick bands (swatches, >200px tall) plus
thin bands (label text, <90px). We keep only the thick bands = the 5 swatch rows.
Intersecting the 4 column bands x 5 swatch-row bands yields 20 boxes per page,
row-major (left->right, top->bottom). Page 2 -> DWMJ-000001..020, page 3 -> 021..040,
..., page 9 -> 141..160.
Output: public/images/DWMJ-000001.png .. DWMJ-000160.png
"""
import sys
import numpy as np
from PIL import Image
from pathlib import Path
PROJ = Path("/Users/macstudio3/Projects/majilite-onboard")
SRC = Path("/tmp/majilite")
OUT = PROJ / "public" / "images"
# --- tunable params (one set works for all 8 pages) ---
COL_THR_FRAC = 0.15 # column-projection threshold as fraction of max
ROW_THR_FRAC = 0.15 # row-projection threshold as fraction of max (0.15 keeps dark swatch rows)
MIN_COL_W = 200 # a real column band is wide
MIN_ROW_H = 200 # a real swatch row band is tall (labels are <90px)
TRIM = 6 # px of black border to trim off each crop edge
EXPECT_COLS = 4
EXPECT_ROWS = 5
def bands(proj, thr):
"""Return list of (start,end) inclusive index ranges where proj>thr."""
on = proj > thr
out = []
s = None
for i, v in enumerate(on):
if v and s is None:
s = i
elif not v and s is not None:
out.append((s, i - 1))
s = None
if s is not None:
out.append((s, len(on) - 1))
return out
def detect_grid(gray):
col = gray.sum(axis=0)
row = gray.sum(axis=1)
col_bands = [b for b in bands(col, col.max() * COL_THR_FRAC) if (b[1] - b[0]) >= MIN_COL_W]
row_bands = [b for b in bands(row, row.max() * ROW_THR_FRAC) if (b[1] - b[0]) >= MIN_ROW_H]
return col_bands, row_bands
def crop_page(page_num, first_sku):
path = SRC / f"page-{page_num:02d}.png"
img = Image.open(path).convert("RGB")
gray = np.asarray(img.convert("L"), dtype=np.float64)
col_bands, row_bands = detect_grid(gray)
if len(col_bands) != EXPECT_COLS or len(row_bands) != EXPECT_ROWS:
raise SystemExit(
f"page {page_num}: expected {EXPECT_COLS}x{EXPECT_ROWS}, "
f"got {len(col_bands)} cols / {len(row_bands)} rows\n"
f" cols={col_bands}\n rows={row_bands}"
)
OUT.mkdir(parents=True, exist_ok=True)
sku = first_sku
saved = []
for (r0, r1) in row_bands: # top -> bottom
for (c0, c1) in col_bands: # left -> right (row-major)
x0 = c0 + TRIM
y0 = r0 + TRIM
x1 = c1 - TRIM
y1 = r1 - TRIM
crop = img.crop((x0, y0, x1 + 1, y1 + 1))
name = f"DWMJ-{sku:06d}.png"
crop.save(OUT / name)
saved.append((name, crop.size))
sku += 1
return saved
def main():
pages = range(2, 10) # page-02..page-09
if len(sys.argv) > 1:
pages = [int(p) for p in sys.argv[1:]]
total = 0
for page_num in pages:
first_sku = (page_num - 2) * 20 + 1
saved = crop_page(page_num, first_sku)
total += len(saved)
print(f"page {page_num:02d}: {len(saved)} swatches "
f"-> DWMJ-{first_sku:06d}..DWMJ-{first_sku + len(saved) - 1:06d} "
f"(size ~{saved[0][1]})")
print(f"TOTAL: {total} images -> {OUT}")
if __name__ == "__main__":
main()