← back to Sister Parish Onboarding
scripts/tileability_check.py
98 lines
#!/usr/bin/env python3
"""
Quick tileability score for each Sister Parish primary_image.
Idea: a perfectly seamless tile has zero edge mismatch when wrapped left↔right
and top↔bottom. We measure the per-pixel difference between opposite edges
(20-pixel strips) and average it.
Scores:
0 - 10 → effectively seamless (probably a true repeat)
10 - 30 → near-seamless, would need slight matchup
30 - 80 → photo with cropped pattern; not seamless
80+ → clearly not a repeat (e.g. styled shot, room scene)
Writes to dw_unified.sisterparish_catalog.tileability JSONB.
"""
import json, os, subprocess
from pathlib import Path
from PIL import Image, ImageOps
import numpy as np
ROOT = Path.home() / 'Projects' / 'sister-parish-onboarding'
def psql_json(sql):
out = subprocess.run(['psql','dw_unified','-At','-q','-c',sql],
capture_output=True, text=True, check=True).stdout.strip()
return json.loads(out) if out else []
def edge_score(path, strip_px=20):
img = Image.open(path).convert('RGB')
img = ImageOps.exif_transpose(img)
# Resize to a fixed working size so different image sizes are comparable
img.thumbnail((800, 800))
a = np.asarray(img, dtype=np.float32)
h, w, _ = a.shape
s = min(strip_px, w // 8, h // 8)
# Horizontal seam: left strip vs right strip
L = a[:, :s, :]
R = a[:, -s:, :]
horiz = float(np.mean(np.abs(L - R)))
# Vertical seam: top vs bottom
T = a[:s, :, :]
B = a[-s:, :, :]
vert = float(np.mean(np.abs(T - B)))
return round((horiz + vert) / 2, 2), round(horiz, 2), round(vert, 2)
# Ensure column exists
subprocess.run(['psql','dw_unified','-q','-c',
'ALTER TABLE sisterparish_catalog ADD COLUMN IF NOT EXISTS tileability JSONB;'], check=True)
# Pull each SP record's primary_image; we don't have local images for SP — only URLs.
# Use /tmp/sp_imgs/ which was populated by extract_dominant_hex.py
sp = psql_json("SELECT COALESCE(json_agg(t),'[]'::json) FROM (SELECT sp_product_id, title, primary_image FROM sisterparish_catalog WHERE primary_image IS NOT NULL) t;")
img_dir = Path('/tmp/sp_imgs')
scored = []
for s in sp:
pid = s['sp_product_id']
# Find image file (could be .jpg / .jpeg / .png)
candidates = list(img_dir.glob(f'sp_{pid}.*'))
if not candidates:
continue
img_path = candidates[0]
try:
score, h, v = edge_score(img_path)
verdict = ('seamless' if score < 10
else 'near' if score < 30
else 'photo' if score < 80
else 'scene')
scored.append((pid, score, h, v, verdict, s['title']))
except Exception as e:
print(f" err {pid}: {e}")
# Write back
with open('/tmp/sp_tileability.sql','w') as f:
f.write("BEGIN;\n")
for pid, score, h, v, verdict, title in scored:
obj = {"score": score, "edge_horiz": h, "edge_vert": v, "verdict": verdict}
obj_str = json.dumps(obj).replace("'", "''")
f.write(f"UPDATE sisterparish_catalog SET tileability='{obj_str}'::jsonb WHERE sp_product_id={pid};\n")
f.write("COMMIT;\n")
subprocess.run(['psql','dw_unified','-q','-f','/tmp/sp_tileability.sql'], check=True)
os.unlink('/tmp/sp_tileability.sql')
print(f"\nScored {len(scored)} SP images")
# Verdict distribution
from collections import Counter
v_count = Counter(s[4] for s in scored)
print("Verdict distribution:", dict(v_count))
print("\nMost-tileable (top 8):")
for s in sorted(scored, key=lambda x: x[1])[:8]:
print(f" score={s[1]:>5.1f} {s[4]:>9} {s[5]}")
print("\nLeast-tileable (top 5):")
for s in sorted(scored, key=lambda x: x[1], reverse=True)[:5]:
print(f" score={s[1]:>5.1f} {s[4]:>9} {s[5]}")