← back to Trending Dw
scripts/heal-tile.py
140 lines
#!/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
import numpy as np
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]}')
try:
return json.loads(r.stdout)
except json.JSONDecodeError as exc:
raise RuntimeError(f'seam scan bad JSON: {r.stdout[:200]}') from exc
# ---- 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]."""
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
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()