← 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
A scripts/audit-room-seam.py
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 →