[object Object]

← back to Wallco Ai

audit-room-seam: tighter masks + pattern-vs-seam disambiguation

467c1ecd826df4b42e975e1c0bdd1d065c05f877 · 2026-05-25 09:06:12 -0700 · Steve Abrams

Reusable room-render seam scanner at scripts/audit-room-seam.py. Differs
from edges-agent (tile PNG scanner): masks ceiling top 10% + furniture
bottom 55% + desk-lamp keep-out before computing column/row edge-energy
discontinuity scores on the wallpaper region only.

Pattern-vs-seam disambiguation — if detected discontinuity peaks are
evenly spaced (gap stdev <25% of mean), they're the pattern's natural
row/column structure, not a tile seam. Real seams are localized one-offs.
This kills false-positive WARN on patterns with discrete row structure
(lips, damask, hex grids, anything with strong horizontal banding).

CLI: python3 scripts/audit-room-seam.py <path> [--json] [--out-dir DIR]

Files touched

Diff

commit 467c1ecd826df4b42e975e1c0bdd1d065c05f877
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon May 25 09:06:12 2026 -0700

    audit-room-seam: tighter masks + pattern-vs-seam disambiguation
    
    Reusable room-render seam scanner at scripts/audit-room-seam.py. Differs
    from edges-agent (tile PNG scanner): masks ceiling top 10% + furniture
    bottom 55% + desk-lamp keep-out before computing column/row edge-energy
    discontinuity scores on the wallpaper region only.
    
    Pattern-vs-seam disambiguation — if detected discontinuity peaks are
    evenly spaced (gap stdev <25% of mean), they're the pattern's natural
    row/column structure, not a tile seam. Real seams are localized one-offs.
    This kills false-positive WARN on patterns with discrete row structure
    (lips, damask, hex grids, anything with strong horizontal banding).
    
    CLI: python3 scripts/audit-room-seam.py <path> [--json] [--out-dir DIR]
---
 scripts/audit-room-seam.py | 190 +++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 190 insertions(+)

diff --git a/scripts/audit-room-seam.py b/scripts/audit-room-seam.py
new file mode 100755
index 0000000..ca75bbb
--- /dev/null
+++ b/scripts/audit-room-seam.py
@@ -0,0 +1,190 @@
+#!/usr/bin/env python3
+"""
+Room-render seam auditor — wall-only seam discontinuity scan.
+
+Differs from edges-agent (which scans tile PNGs): this takes a ROOM render
+(photo of a wall + furniture) and isolates ONLY the wallpaper region before
+checking for vertical/horizontal seam discontinuities + pattern repeat period.
+
+Masks:
+  · top 10% (ceiling + crown molding)
+  · bottom 55% (desk + furniture)
+  · center-bottom keep-out (desk-lamp silhouette intruding into the wall)
+
+Verdict thresholds (column or row edge-energy ratio vs median, smoothed):
+  · ≤ 2.2× — PASS
+  · 2.2–3.0× — WARN
+  · > 3.0× — FAIL
+Plus a pattern-repetition guard: <200px period at autocorr>0.5 → WARN
+(pattern repeats too tightly — looks like an obvious tile pattern not a wall).
+"""
+import sys, os, json, argparse
+from PIL import Image, ImageDraw
+import numpy as np
+
+CEILING_FRAC = 0.10   # mask top 10%
+FURNITURE_FRAC = 0.55 # mask bottom 55%
+# Desk-lamp keep-out (typical office room layout from room-setting-generator):
+LAMP_X_FRAC = (0.30, 0.65)   # horizontal slice of the wall-bottom band
+LAMP_Y_FRAC = (0.42, 0.55)   # vertical slice (just above furniture mask)
+
+def audit(path: str, out_dir: str = "/tmp", verbose: bool = True):
+    img = Image.open(path).convert("RGB")
+    W, H = img.size
+    if verbose: print(f"image: {W}x{H}")
+
+    wall_top = int(H * CEILING_FRAC)
+    wall_bot = int(H * (1 - FURNITURE_FRAC + 0.45))  # bottom of wall = top of furniture
+    # = H * 0.45 here; keep clean — same effect
+    wall_bot = int(H * 0.45)
+    wall = img.crop((0, wall_top, W, wall_bot))
+    ww, wh = wall.size
+    if verbose: print(f"wall crop: {ww}x{wh}  (y={wall_top}..{wall_bot})")
+
+    arr = np.array(wall, dtype=np.float32)
+    gray = arr.mean(axis=2)
+
+    # Build a per-pixel KEEP mask (1=wallpaper, 0=furniture/lamp).
+    mask = np.ones_like(gray, dtype=np.float32)
+    lx0 = int(ww * LAMP_X_FRAC[0])
+    lx1 = int(ww * LAMP_X_FRAC[1])
+    ly0_in_wall = int(wh * 0.85)  # bottom 15% of wall crop most-likely lamp top
+    mask[ly0_in_wall:, lx0:lx1] = 0.0
+    masked = gray * mask
+
+    # ── Vertical seam detection (column-wise edge energy, masked) ──
+    diff_x = np.abs(np.diff(masked, axis=1))
+    col_signal = diff_x.mean(axis=0)
+    col_signal = np.convolve(col_signal, np.ones(7)/7, mode='same')
+    med_col = float(np.median(col_signal))
+    col_peaks = []
+    last = -50
+    for i, v in enumerate(col_signal):
+        if v > med_col * 1.8 and i - last > 24:
+            col_peaks.append((i, float(v / med_col)))
+            last = i
+
+    # ── Horizontal seam detection (row-wise) ──
+    diff_y = np.abs(np.diff(masked, axis=0))
+    row_signal = diff_y.mean(axis=1)
+    row_signal = np.convolve(row_signal, np.ones(7)/7, mode='same')
+    med_row = float(np.median(row_signal))
+    row_peaks = []
+    last = -50
+    for i, v in enumerate(row_signal):
+        if v > med_row * 1.8 and i - last > 24:
+            row_peaks.append((i + wall_top, float(v / med_row)))
+            last = i
+
+    # ── Pattern periodicity (autocorrelation on a clean mid-strip) ──
+    strip_y = wh // 2
+    strip = gray[strip_y - 8: strip_y + 8].mean(axis=0)
+    strip -= strip.mean()
+    ac = np.correlate(strip, strip, mode='full')[len(strip)-1:]
+    ac = ac / max(ac[0], 1e-9)
+    period, period_strength = None, 0.0
+    for i in range(30, min(W - 1, 700)):
+        if ac[i] > ac[i-1] and ac[i] > ac[i+1] and ac[i] > 0.3:
+            period = i
+            period_strength = float(ac[i])
+            break
+
+    # ── Pattern-vs-seam disambiguation ──
+    # If detected peaks are EVENLY spaced (within 25% of each other), they're
+    # the pattern's natural row/column structure, not a seam. Real seams are
+    # one-offs: a single localized discontinuity, not a repeating set.
+    def is_pattern_structure(peaks):
+        if len(peaks) < 2: return False
+        positions = sorted(p[0] for p in peaks)
+        gaps = [positions[i+1] - positions[i] for i in range(len(positions) - 1)]
+        if not gaps: return False
+        mean_gap = sum(gaps) / len(gaps)
+        if mean_gap < 30: return False  # too tight to be wallpaper rows
+        return all(abs(g - mean_gap) / mean_gap < 0.25 for g in gaps)
+
+    col_is_pattern = is_pattern_structure(col_peaks)
+    row_is_pattern = is_pattern_structure(row_peaks)
+
+    # ── Verdict ──
+    verdict = "PASS"
+    notes = []
+    worst_col = max([r for _, r in col_peaks], default=0.0)
+    worst_row = max([r for _, r in row_peaks], default=0.0)
+    if worst_col >= 3.0 and not col_is_pattern:
+        verdict = "FAIL"; notes.append(f"vertical seam {worst_col:.2f}× median col-energy")
+    elif worst_col >= 2.2 and not col_is_pattern:
+        if verdict == "PASS": verdict = "WARN"
+        notes.append(f"weak vertical discontinuity {worst_col:.2f}× median")
+    elif col_is_pattern:
+        notes.append(f"vertical peaks evenly spaced — pattern column structure, not a seam")
+    if worst_row >= 3.0 and not row_is_pattern:
+        verdict = "FAIL"; notes.append(f"horizontal seam {worst_row:.2f}× median row-energy")
+    elif worst_row >= 2.2 and not row_is_pattern:
+        if verdict == "PASS": verdict = "WARN"
+        notes.append(f"weak horizontal discontinuity {worst_row:.2f}× median")
+    elif row_is_pattern:
+        notes.append(f"horizontal peaks evenly spaced — pattern row structure, not a seam")
+    if period and period_strength > 0.5 and period < 200:
+        if verdict == "PASS": verdict = "WARN"
+        notes.append(f"tight repetition {period}px (strength {period_strength:.2f}) — pattern repeats {W//period}× across wall")
+
+    # ── Artifacts ──
+    base = os.path.splitext(os.path.basename(path))[0]
+    overlay = img.copy()
+    dr = ImageDraw.Draw(overlay)
+    # shade mask regions
+    shade = Image.new("RGBA", img.size, (0,0,0,0))
+    sdr = ImageDraw.Draw(shade)
+    sdr.rectangle([0, 0, W, wall_top], fill=(48, 100, 200, 90))            # ceiling mask
+    sdr.rectangle([0, wall_bot, W, H], fill=(48, 100, 200, 90))            # furniture mask
+    # lamp keep-out (translated to global y)
+    sdr.rectangle([lx0, wall_top + int(wh * 0.85), lx1, wall_bot], fill=(48, 100, 200, 90))
+    overlay = Image.alpha_composite(overlay.convert("RGBA"), shade).convert("RGB")
+    dr = ImageDraw.Draw(overlay)
+    for cx, r in col_peaks[:8]:
+        c = (220, 28, 28) if r >= 3.0 else (220, 180, 28)
+        dr.line([(cx, wall_top), (cx, wall_bot)], fill=c, width=2)
+    for ry, r in row_peaks[:8]:
+        c = (220, 28, 28) if r >= 3.0 else (220, 180, 28)
+        dr.line([(0, ry), (W, ry)], fill=c, width=2)
+    overlay_path = os.path.join(out_dir, f"{base}.audit-overlay.png")
+    overlay.save(overlay_path)
+
+    result = {
+        "ok": True, "path": path, "image": [W, H],
+        "wall_crop": {"x": [0, W], "y": [wall_top, wall_bot]},
+        "verdict": verdict,
+        "vertical": {
+            "median_col_energy": med_col, "worst_ratio": worst_col,
+            "peaks": [{"x": int(c), "ratio": round(r, 3)} for c, r in col_peaks]
+        },
+        "horizontal": {
+            "median_row_energy": med_row, "worst_ratio": worst_row,
+            "peaks": [{"y": int(y), "ratio": round(r, 3)} for y, r in row_peaks]
+        },
+        "periodicity": {"period_px": period, "strength": round(period_strength, 3)},
+        "notes": notes,
+        "overlay": overlay_path,
+    }
+    return result
+
+if __name__ == "__main__":
+    ap = argparse.ArgumentParser()
+    ap.add_argument("path")
+    ap.add_argument("--json", action="store_true")
+    ap.add_argument("--out-dir", default="/tmp")
+    args = ap.parse_args()
+    r = audit(args.path, args.out_dir, verbose=not args.json)
+    if args.json:
+        print(json.dumps(r, indent=2))
+    else:
+        print(f"\n══ VERDICT: {r['verdict']} ══")
+        print(f"  vertical:   max {r['vertical']['worst_ratio']:.2f}× median  ({len(r['vertical']['peaks'])} peaks)")
+        print(f"  horizontal: max {r['horizontal']['worst_ratio']:.2f}× median  ({len(r['horizontal']['peaks'])} peaks)")
+        pp = r['periodicity']
+        print(f"  period:     {pp['period_px']}px  (strength {pp['strength']:.3f})")
+        if r['notes']:
+            for n in r['notes']: print(f"    · {n}")
+        else:
+            print("  · clean wall region — no significant seams or tight repeat detected")
+        print(f"  overlay:    {r['overlay']}")

← 929d53b T7: batch-quality audit — 99.3% ok, no variant/ground bias  ·  back to Wallco Ai  ·  extend-luxe-curator — backfill C + queue A/E for reverted-bl aceea32 →