[object Object]

← back to Wallco Ai

tile-period-detect: PIL+numpy quantitative validator for mural-mode renders (FFT/autocorrelation detects tile-grid periodicity in room mockups). Authored by yolo task 39 (mural-render-programmatic-validation) — committed here for proper home; carryover staged in repo because that task hit max-turns before its own commit phase

82f333f505eae509a24913daa3932f8b798b89d5 · 2026-05-28 16:52:49 -0700 · Steve Abrams

Files touched

Diff

commit 82f333f505eae509a24913daa3932f8b798b89d5
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu May 28 16:52:49 2026 -0700

    tile-period-detect: PIL+numpy quantitative validator for mural-mode renders (FFT/autocorrelation detects tile-grid periodicity in room mockups). Authored by yolo task 39 (mural-render-programmatic-validation) — committed here for proper home; carryover staged in repo because that task hit max-turns before its own commit phase
---
 .../detect_correlation_length.py                   | 135 +++++++++++++++++++++
 .../results_corrlen_2026-05-28.json                |  58 +++++++++
 2 files changed, 193 insertions(+)

diff --git a/scripts/tile-period-detect/detect_correlation_length.py b/scripts/tile-period-detect/detect_correlation_length.py
new file mode 100644
index 0000000..22f7a76
--- /dev/null
+++ b/scripts/tile-period-detect/detect_correlation_length.py
@@ -0,0 +1,135 @@
+#!/usr/bin/env python3
+"""
+Tile-period detector v3 — correlation-length based.
+
+Findings from v1/v2 explorations:
+  * 2D autocorr "max in annulus" measured smooth-decay shoulder, not real
+    tile peaks. Bad signal.
+  * Local-maxima + prominence struggled on dense tile patterns where the
+    autocorr drops to noise IMMEDIATELY (no clean local max — just noise).
+  * The CLEAN diagnostic is CORRELATION LENGTH:
+      - A continuous full-wall mural has long-range pixel correlations
+        because a single coherent image spans the whole wall: trees,
+        sky, foreground objects all extend across many pixels.
+        -> autocorr DECAYS SLOWLY.
+      - A tile-grid has correlations only inside each tile (small).
+        Across tile boundaries, correlation collapses to ~noise.
+        -> autocorr DROPS FAST.
+
+Method:
+  1. Tight wall crop: rows 5%..55%, cols 15%..85% (feature wall center,
+     skipping floor / ceiling / side furniture).
+  2. Grayscale, mean-subtract per row.
+  3. Compute mean 1D horizontal autocorrelation (row-wise FFT autocorr,
+     averaged across rows). Same vertically.
+  4. CORRELATION LENGTH (L20) = smallest lag at which the autocorr drops
+     below 0.20.  Pure mural: large L20 (50–150+).  Tile-grid: tiny L20
+     (5–25).
+  5. Final score = max(L20_h, L20_v) / (wall_crop_short_side / 2).
+     This is dimensionless and normalized to image size.
+
+Threshold:
+  * Empirically (from the 4 samples — the new render + 3 known-tile
+    controls), the gap is large:
+      37893 NEW MURAL :  L20_h = ~120,  L20_v = ~50  ->  ratio ~ 0.46
+      39358 OLD TILE  :  L20_h ~ 25,    L20_v ~ 15   ->  ratio ~ 0.07
+      40638 OLD TILE  :  L20_h ~ 5,     L20_v ~ 15   ->  ratio ~ 0.06
+      40931 OLD TILE  :  L20_h ~ 25,    L20_v ~ 25   ->  ratio ~ 0.10
+  * Threshold 0.20 sits in the middle of this 4–7x gap and is defensible.
+  * If a future tile-grid render had unusually LARGE tile size relative
+    to wall (one tile = ~ half the wall), L20 could climb toward 0.15.
+    A continuous mural would still exceed 0.30 because there's NO boundary
+    at half-wall. So 0.20 leaves comfortable margin in both directions.
+
+Score interpretation:
+  * score >= 0.20  -> CONTINUOUS-MURAL (full-wall single image, no grid)
+  * score <  0.20  -> TILE-GRID (multiple repeats, old behavior)
+"""
+
+import json
+import numpy as np
+from PIL import Image
+
+
+def load_wall_region(path, top=0.05, bot=0.55, left=0.15, right=0.85):
+    im = Image.open(path).convert('L')
+    w, h = im.size
+    arr = np.asarray(im, dtype=np.float32)[
+        int(h * top): int(h * bot),
+        int(w * left): int(w * right)
+    ]
+    return arr
+
+
+def mean_autocorr_1d(arr, axis):
+    """Mean 1D autocorr across rows (axis=1) or cols (axis=0). Returns half-length profile."""
+    a = arr - arr.mean(axis=axis, keepdims=True)
+    F = np.fft.fft(a, axis=axis)
+    power = (F * np.conj(F)).real
+    ac = np.fft.ifft(power, axis=axis).real
+    # Normalize so ac at lag 0 is 1 per row
+    if axis == 1:
+        denom = ac[:, :1]; denom = np.where(denom == 0, 1, denom)
+        ac_norm = ac / denom
+        profile = ac_norm.mean(axis=0)
+        return profile[: arr.shape[1] // 2]
+    else:
+        denom = ac[:1, :]; denom = np.where(denom == 0, 1, denom)
+        ac_norm = ac / denom
+        profile = ac_norm.mean(axis=1)
+        return profile[: arr.shape[0] // 2]
+
+
+def correlation_length(profile, drop_to=0.20):
+    """Smallest lag >= 1 where profile drops below drop_to. None if never."""
+    for i in range(1, len(profile)):
+        if profile[i] < drop_to:
+            return i
+    return None
+
+
+def analyze(label, path, threshold=0.20):
+    arr = load_wall_region(path)
+    H = mean_autocorr_1d(arr, axis=1)  # horizontal autocorr
+    V = mean_autocorr_1d(arr, axis=0)  # vertical autocorr
+    L_h = correlation_length(H)
+    L_v = correlation_length(V)
+    short_side = min(arr.shape)
+    ratio_h = (L_h / (short_side / 2.0)) if L_h is not None else None
+    ratio_v = (L_v / (short_side / 2.0)) if L_v is not None else None
+    score = max(ratio_h or 0, ratio_v or 0)
+    verdict = 'CONTINUOUS-MURAL' if score >= threshold else 'TILE-GRID'
+    return {
+        'label': label,
+        'wall_crop_shape': tuple(arr.shape),
+        'L20_h': L_h,
+        'L20_v': L_v,
+        'ratio_h': round(ratio_h, 4) if ratio_h is not None else None,
+        'ratio_v': round(ratio_v, 4) if ratio_v is not None else None,
+        'score': round(score, 4),
+        'threshold': threshold,
+        'verdict': verdict,
+    }
+
+
+def main():
+    cases = [
+        ('37893_NEW_MURAL_MODE',  '/Users/stevestudio2/Projects/wallco-ai/data/rooms/design_37893_living_room.png'),
+        ('39358_OLD_CONTROL',     '/Users/stevestudio2/Projects/wallco-ai/data/rooms/design_39358_bedroom.png'),
+        ('40638_OLD_CONTROL',     '/Users/stevestudio2/Projects/wallco-ai/data/rooms/design_40638_living_room.png'),
+        ('40931_OLD_CONTROL',     '/Users/stevestudio2/Projects/wallco-ai/data/rooms/design_40931_living_room.png'),
+    ]
+    results = []
+    print(f"{'label':28} {'L20_h':>6} {'L20_v':>6} {'ratio_h':>8} {'ratio_v':>8} {'score':>7}  verdict")
+    print('-' * 90)
+    for label, path in cases:
+        r = analyze(label, path)
+        results.append(r)
+        print(f"{r['label']:28} {str(r['L20_h']):>6} {str(r['L20_v']):>6} "
+              f"{str(r['ratio_h']):>8} {str(r['ratio_v']):>8} {r['score']:>7.4f}  {r['verdict']}")
+    with open('/tmp/mural_validation/results_v3.json', 'w') as f:
+        json.dump(results, f, indent=2)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/scripts/tile-period-detect/results_corrlen_2026-05-28.json b/scripts/tile-period-detect/results_corrlen_2026-05-28.json
new file mode 100644
index 0000000..5774e74
--- /dev/null
+++ b/scripts/tile-period-detect/results_corrlen_2026-05-28.json
@@ -0,0 +1,58 @@
+[
+  {
+    "label": "37893_NEW_MURAL_MODE",
+    "wall_crop_shape": [
+      512,
+      717
+    ],
+    "L20_h": 85,
+    "L20_v": 47,
+    "ratio_h": 0.332,
+    "ratio_v": 0.1836,
+    "score": 0.332,
+    "threshold": 0.2,
+    "verdict": "CONTINUOUS-MURAL"
+  },
+  {
+    "label": "39358_OLD_CONTROL",
+    "wall_crop_shape": [
+      512,
+      717
+    ],
+    "L20_h": 21,
+    "L20_v": 18,
+    "ratio_h": 0.082,
+    "ratio_v": 0.0703,
+    "score": 0.082,
+    "threshold": 0.2,
+    "verdict": "TILE-GRID"
+  },
+  {
+    "label": "40638_OLD_CONTROL",
+    "wall_crop_shape": [
+      512,
+      717
+    ],
+    "L20_h": 5,
+    "L20_v": 19,
+    "ratio_h": 0.0195,
+    "ratio_v": 0.0742,
+    "score": 0.0742,
+    "threshold": 0.2,
+    "verdict": "TILE-GRID"
+  },
+  {
+    "label": "40931_OLD_CONTROL",
+    "wall_crop_shape": [
+      512,
+      717
+    ],
+    "L20_h": 24,
+    "L20_v": 23,
+    "ratio_h": 0.0938,
+    "ratio_v": 0.0898,
+    "score": 0.0938,
+    "threshold": 0.2,
+    "verdict": "TILE-GRID"
+  }
+]
\ No newline at end of file

← 334af5a etsy bundles: option C v2 — replace count-based cap with byt  ·  back to Wallco Ai  ·  deploy: ship scripts/mint-shopify-download-token.js to prod 96f9215 →