[object Object]

← 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

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 →