← back to Wallco Ai
Add edge-seam mural gate batch (DTD-B: edge-PASS keeps tile, WARN/FAIL→mural) + the (unreliable, kept for record) autocorr gate
393fbb97f4463b0bf33abb786b47ba3a46d295dc · 2026-06-12 10:51:37 -0700 · Steve Abrams
Files touched
A scripts/mural-gate-batch.pyA scripts/tiling-autocorr-gate.py
Diff
commit 393fbb97f4463b0bf33abb786b47ba3a46d295dc
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Fri Jun 12 10:51:37 2026 -0700
Add edge-seam mural gate batch (DTD-B: edge-PASS keeps tile, WARN/FAIL→mural) + the (unreliable, kept for record) autocorr gate
---
scripts/mural-gate-batch.py | 64 ++++++++++++++++++++++++++++++++++++
scripts/tiling-autocorr-gate.py | 73 +++++++++++++++++++++++++++++++++++++++++
2 files changed, 137 insertions(+)
diff --git a/scripts/mural-gate-batch.py b/scripts/mural-gate-batch.py
new file mode 100644
index 0000000..6c4ee1f
--- /dev/null
+++ b/scripts/mural-gate-batch.py
@@ -0,0 +1,64 @@
+#!/usr/bin/env python3
+"""mural-gate-batch.py — RUN ON KAMATERA. Classify canonical drunk-animals
+seamless_tile rows by the edge-seam gate (DTD verdict B, 2026-06-12):
+ edge verdict PASS -> tiles cleanly -> KEEP seamless_tile
+ WARN / FAIL -> does not tile -> mural_panel
+Dry-run by default (read-only: scans images + a SELECT). --apply does the
+chunked canonical UPDATE (Steve-authorized prod write, via sudo -u postgres)."""
+import subprocess, json, os, sys, argparse
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+from importlib import import_module
+ev = import_module('verify-edge-seamless')
+from PIL import Image
+import numpy as np
+
+ROOT = '/root/public-projects/wallco-ai'
+IMGDIR = os.path.join(ROOT, 'data', 'generated')
+OUT = '/tmp/mural-ids.txt'
+
+def psql(sql, write=False):
+ return subprocess.run(['sudo','-u','postgres','psql','-d','dw_unified','-tA','-F','|','-c',sql],
+ capture_output=True, text=True).stdout
+
+def get_list():
+ out = psql("SELECT id, local_path FROM all_designs WHERE category='drunk-animals' "
+ "AND kind='seamless_tile' AND local_path IS NOT NULL ORDER BY id")
+ rows = []
+ for line in out.splitlines():
+ if '|' in line:
+ i, p = line.split('|', 1)
+ rows.append((int(i), p))
+ return rows
+
+def resolve(p):
+ if p and os.path.exists(p): return p
+ b = os.path.basename(p or '')
+ c = os.path.join(IMGDIR, b)
+ return c if os.path.exists(c) else None
+
+def main():
+ ap = argparse.ArgumentParser(); ap.add_argument('--apply', action='store_true')
+ ap.add_argument('--limit', type=int, default=0); a = ap.parse_args()
+ rows = get_list()
+ if a.limit: rows = rows[:a.limit]
+ keep = []; mural = []; missing = 0
+ for n,(i,p) in enumerate(rows):
+ f = resolve(p)
+ if not f: missing += 1; continue
+ try:
+ arr = np.asarray(Image.open(f).convert('RGB'))[:, :, :3]
+ v = ev.verify(arr)
+ (keep if v['verdict'] == 'PASS' else mural).append(i)
+ except Exception:
+ missing += 1
+ if n % 250 == 0: print(f" scanned {n}/{len(rows)}", file=sys.stderr)
+ with open(OUT, 'w') as fh: fh.write('\n'.join(map(str, mural)))
+ print(json.dumps({'total': len(rows), 'keep_tile': len(keep),
+ 'to_mural': len(mural), 'missing': missing, 'ids_file': OUT}))
+ if a.apply and mural:
+ for k in range(0, len(mural), 500):
+ ids = ','.join(map(str, mural[k:k+500]))
+ psql(f"UPDATE all_designs SET kind='mural_panel' WHERE id IN ({ids}) AND kind='seamless_tile';", write=True)
+ print(f"APPLIED mural_panel to {len(mural)} rows")
+
+if __name__ == '__main__': main()
diff --git a/scripts/tiling-autocorr-gate.py b/scripts/tiling-autocorr-gate.py
new file mode 100644
index 0000000..f81de11
--- /dev/null
+++ b/scripts/tiling-autocorr-gate.py
@@ -0,0 +1,73 @@
+#!/usr/bin/env python3
+"""tiling-autocorr-gate.py — classify a design as a TRUE repeating tile vs a
+single-subject illustration (mural), via internal autocorrelation.
+
+A real all-over tile repeats its motif INSIDE the frame, so the 2D autocorrelation
+has a strong secondary peak away from the center. A single centered subject has no
+internal repeat → off-center autocorrelation collapses to ~0. (This is the test
+that found drunk-animals roots score ~0; Steve: treat those as murals.)
+
+score = max off-center normalized autocorrelation (center lobe masked).
+ >= THRESH (default 0.30) => tiles (keep seamless_tile)
+ < THRESH => single-subject => mural_panel
+
+Usage:
+ tiling-autocorr-gate.py --path a.png [--path b.png ...] # ad hoc
+ tiling-autocorr-gate.py --selftest
+Output: one JSON per line {path|id, score, verdict}.
+"""
+import argparse, json, os, sys
+import numpy as np
+from PIL import Image
+
+THRESH = 0.30
+N = 256 # working size
+CENTER_MASK = 24 # half-width of central lobe to zero out
+
+def tiling_score(arr_or_path):
+ if isinstance(arr_or_path, str):
+ im = Image.open(arr_or_path).convert('L').resize((N, N))
+ a = np.asarray(im, float)
+ else:
+ a = np.asarray(Image.fromarray(arr_or_path).convert('L').resize((N, N)), float)
+ a = a - a.mean()
+ if a.std() < 1e-6:
+ return 0.0
+ f = np.fft.fft2(a)
+ ac = np.fft.ifft2(f * np.conj(f)).real
+ ac = np.fft.fftshift(ac)
+ m = ac.max()
+ if m <= 0:
+ return 0.0
+ ac = ac / m
+ c = N // 2
+ ac[c-CENTER_MASK:c+CENTER_MASK, c-CENTER_MASK:c+CENTER_MASK] = 0.0
+ return float(ac.max())
+
+def verdict(s): return 'tiles' if s >= THRESH else 'mural'
+
+def main():
+ ap = argparse.ArgumentParser()
+ ap.add_argument('--path', action='append', default=[])
+ ap.add_argument('--thresh', type=float, default=THRESH)
+ ap.add_argument('--selftest', action='store_true')
+ a = ap.parse_args()
+ if a.selftest:
+ # positive: a perfect 4x4 tile of a random patch (repeats internally)
+ patch = (np.random.RandomState(1).rand(64,64,3)*255).astype('uint8')
+ tile = np.tile(patch, (4,4,1))
+ print(json.dumps({'case':'synthetic_perfect_tile','score':round(tiling_score(tile),3),'verdict':verdict(tiling_score(tile))}))
+ # negative: a single blob centered on flat ground (single subject)
+ img = np.full((256,256,3), 40, 'uint8');
+ yy,xx=np.mgrid[0:256,0:256]; blob=((xx-128)**2+(yy-128)**2)<60**2
+ img[blob]=220
+ print(json.dumps({'case':'synthetic_single_subject','score':round(tiling_score(img),3),'verdict':verdict(tiling_score(img))}))
+ return
+ for p in a.path:
+ try:
+ s = tiling_score(p)
+ print(json.dumps({'path':p,'score':round(s,3),'verdict':verdict(s) if s>=0 else '?'}))
+ except Exception as e:
+ print(json.dumps({'path':p,'error':str(e)[:120]}))
+
+if __name__=='__main__': main()
← af64e05 TODO: seam joint-heal v2 + four-horsemen verdict + non-tilin
·
back to Wallco Ai
·
colorways: add POST /api/design/:id/recolor-preview — live c 66f6a71 →