[object Object]

← back to Trending Dw

gapgen: add file-based heal-tile.py (near-miss seam rescue, reuses wallco heal algo, no DB)

52f5a8b8a968cbab2b42fe4b7ed60692fcd27c0b · 2026-07-07 09:49:41 -0700 · Steve

Files touched

Diff

commit 52f5a8b8a968cbab2b42fe4b7ed60692fcd27c0b
Author: Steve <steve@designerwallcoverings.com>
Date:   Tue Jul 7 09:49:41 2026 -0700

    gapgen: add file-based heal-tile.py (near-miss seam rescue, reuses wallco heal algo, no DB)
---
 scripts/heal-tile.py | 137 +++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 137 insertions(+)

diff --git a/scripts/heal-tile.py b/scripts/heal-tile.py
new file mode 100644
index 0000000..29d4de6
--- /dev/null
+++ b/scripts/heal-tile.py
@@ -0,0 +1,137 @@
+#!/usr/bin/env python3
+"""
+heal-tile.py — file-based seam healer for trending-dw gap candidates.
+
+The gapgen pipeline is self-host + file-based (candidates are PNGs in /tmp/gap-cand,
+no DB). wallco-ai's heal-seam-region.py does the same joint-healing but is coupled
+to the all_designs DB table, so it can't be used here. This lifts its pure-numpy
+heal algorithm (heal_band_mid / heal_edge_wrap) and drives it from a PNG PATH:
+
+  1. scan the tile via wallco-ai/scripts/seam-defect-boxes.py --path  (get FAIL/WARN boxes)
+  2. heal each flagged joint in-place  (v_mid/h_mid band-average, *_edge mirror-wrap)
+  3. save <name>_healed.png  (NEVER overwrites the source — round-1 output is sacred)
+  4. re-scan the healed file and print before/after JSON
+
+This is the motif-lane rescue the DTD panel required before motif generation resumes:
+figural motifs (leopard/paisley/deco) tile badly (seam 15-46); healing their joints
+can pull a near-miss under the <=5 gate without re-rolling (which discards the design).
+
+Usage:
+  python3 scripts/heal-tile.py --path /tmp/gap-cand/TR-020__s123.png
+  python3 scripts/heal-tile.py --path <png> --kinds v_mid,h_mid   # heal only these joints
+Exit 0 if the healed tile PASSES seam (overall_max <= 5), else 1.
+"""
+import argparse, json, subprocess, sys
+from pathlib import Path
+
+WALLCO_SEAM = Path('/Users/macstudio3/Projects/wallco-ai/scripts/seam-defect-boxes.py')
+PY = sys.executable  # run under the same interpreter (the .gapvenv python that has numpy+PIL)
+
+BLEND_PX = 6   # px each side of the seam to average
+BAND_PX = 32   # full heal-band width (centered on the seam line)
+
+
+def scan(png: Path) -> dict:
+    r = subprocess.run([PY, str(WALLCO_SEAM), '--path', str(png)], capture_output=True, text=True, timeout=60)
+    if r.returncode != 0:
+        raise RuntimeError(f'seam scan failed: {r.stderr.strip()[:200]}')
+    return json.loads(r.stdout)
+
+
+# ---- heal primitives (ported verbatim from wallco-ai/scripts/heal-seam-region.py) ----
+def heal_band_mid(arr, axis, idx, start, end):
+    """axis='v': vertical seam at x=idx, healing rows [start:end];
+       axis='h': horizontal seam at y=idx, healing cols [start:end]."""
+    import numpy as np
+    H, W = arr.shape[:2]
+    if axis == 'v':
+        x0 = max(0, idx - BAND_PX // 2); x1 = min(W, idx + BAND_PX // 2)
+        left  = arr[start:end, max(0, idx - BLEND_PX - 1):idx, :].mean(axis=1)
+        right = arr[start:end, idx:min(W, idx + BLEND_PX), :].mean(axis=1)
+        avg = ((left + right) / 2).round().astype('uint8')
+        for x in range(x0, x1):
+            w = max(0.0, min(1.0, 1 - abs(x - idx) / (BAND_PX / 2)))
+            arr[start:end, x, :] = (avg * w + arr[start:end, x, :] * (1 - w)).astype('uint8')
+    else:
+        y0 = max(0, idx - BAND_PX // 2); y1 = min(H, idx + BAND_PX // 2)
+        top = arr[max(0, idx - BLEND_PX - 1):idx, start:end, :].mean(axis=0)
+        bot = arr[idx:min(H, idx + BLEND_PX), start:end, :].mean(axis=0)
+        avg = ((top + bot) / 2).round().astype('uint8')
+        for y in range(y0, y1):
+            w = max(0.0, min(1.0, 1 - abs(y - idx) / (BAND_PX / 2)))
+            arr[y, start:end, :] = (avg * w + arr[y, start:end, :] * (1 - w)).astype('uint8')
+
+
+def heal_edge_wrap(arr, kind, start, end):
+    """Average each edge strip with its opposite-edge mirror so the tile-repeat
+    boundary is invisible. kind in {top,bottom,left,right}_edge."""
+    H, W = arr.shape[:2]
+    S = 16
+    if kind in ('top_edge', 'bottom_edge'):
+        x0, x1 = start, end
+        top = arr[0:S, x0:x1, :].astype('float32')
+        bottom = arr[H - S:H, x0:x1, :].astype('float32')
+        blend = ((top + bottom[::-1, :, :]) / 2).astype('uint8')
+        arr[0:S, x0:x1, :] = blend
+        arr[H - S:H, x0:x1, :] = blend[::-1, :, :]
+    elif kind in ('left_edge', 'right_edge'):
+        y0, y1 = start, end
+        left = arr[y0:y1, 0:S, :].astype('float32')
+        right = arr[y0:y1, W - S:W, :].astype('float32')
+        blend = ((left + right[:, ::-1, :]) / 2).astype('uint8')
+        arr[y0:y1, 0:S, :] = blend
+        arr[y0:y1, W - S:W, :] = blend[:, ::-1, :]
+
+
+def main():
+    ap = argparse.ArgumentParser()
+    ap.add_argument('--path', required=True, help='PNG candidate to heal')
+    ap.add_argument('--kinds', help='comma-separated joint kinds to heal (default: all flagged)')
+    args = ap.parse_args()
+
+    src = Path(args.path)
+    if not src.exists():
+        print(json.dumps({'ok': False, 'error': f'not found: {src}'})); sys.exit(2)
+
+    before = scan(src)
+    boxes = before.get('boxes', [])
+    if args.kinds:
+        wanted = set(args.kinds.split(','))
+        boxes = [b for b in boxes if b['kind'] in wanted]
+    if not boxes:
+        print(json.dumps({'ok': True, 'note': 'no joints to heal', 'verdict': before['verdict'],
+                          'scores': before['scores'], 'healed_boxes': 0}))
+        sys.exit(0 if before['verdict'] == 'PASS' else 1)
+
+    from PIL import Image
+    import numpy as np
+    arr = np.asarray(Image.open(src).convert('RGB')).copy()
+    H, W = arr.shape[:2]
+
+    for b in boxes:
+        k = b['kind']; x, y, w, h = b['x'], b['y'], b['w'], b['h']
+        if k == 'v_mid':
+            heal_band_mid(arr, 'v', W // 2, y, y + h)
+        elif k == 'h_mid':
+            heal_band_mid(arr, 'h', H // 2, x, x + w)
+        elif k in ('top_edge', 'bottom_edge'):
+            heal_edge_wrap(arr, k, x, x + w)
+        elif k in ('left_edge', 'right_edge'):
+            heal_edge_wrap(arr, k, y, y + h)
+
+    out = src.with_name(src.stem + '_healed.png')
+    Image.fromarray(arr).save(out, optimize=True)
+    after = scan(out)
+
+    passed = after['scores'].get('overall_max', 99) <= 5.0
+    print(json.dumps({
+        'ok': True, 'src': str(src), 'healed': str(out), 'healed_boxes': len(boxes),
+        'before': {'verdict': before['verdict'], 'overall_max': round(before['scores'].get('overall_max', 0), 2)},
+        'after':  {'verdict': after['verdict'],  'overall_max': round(after['scores'].get('overall_max', 0), 2)},
+        'passes_seam_gate': passed,
+    }))
+    sys.exit(0 if passed else 1)
+
+
+if __name__ == '__main__':
+    main()

← bedb55d gapgen: data-driven style-keyed briefs from live gap items (  ·  back to Trending Dw  ·  auto-save: 2026-07-07T09:59:53 (1 files) — scripts/__pycache 7991ccc →