← back to Wallco Ai
element_copy_heal v2: wrap-edge-only crisp palette-reconcile (DTD verdict C)
0fdef022aef6fc7e629b6d1e152b36a825f6a7d6 · 2026-06-12 09:40:25 -0700 · Steve Abrams
Replaces v1 half-roll patch-copy which sliced a visible line through the frog's
face on root 2766 and worsened the top/bottom edge. v2 heals ONLY the 4 physical
tile-wrap edges, and only the columns/rows where the outermost wrap pixels carry
different palette labels (a motif crossing the wrap). Each rewritten pixel is
snapped to a crisp dominant-palette color (no averaging = no smudge), only the
disagreeing lines are touched (no drawn line), and the interior mids (x=W/2,
y=H/2 = motif edges, NOT seams) are never touched. On 2766: wrap-column
disagreement 17 -> 0, frog motif fully preserved, no smudge/line.
Files touched
A scripts/element_copy_heal.py
Diff
commit 0fdef022aef6fc7e629b6d1e152b36a825f6a7d6
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Fri Jun 12 09:40:25 2026 -0700
element_copy_heal v2: wrap-edge-only crisp palette-reconcile (DTD verdict C)
Replaces v1 half-roll patch-copy which sliced a visible line through the frog's
face on root 2766 and worsened the top/bottom edge. v2 heals ONLY the 4 physical
tile-wrap edges, and only the columns/rows where the outermost wrap pixels carry
different palette labels (a motif crossing the wrap). Each rewritten pixel is
snapped to a crisp dominant-palette color (no averaging = no smudge), only the
disagreeing lines are touched (no drawn line), and the interior mids (x=W/2,
y=H/2 = motif edges, NOT seams) are never touched. On 2766: wrap-column
disagreement 17 -> 0, frog motif fully preserved, no smudge/line.
---
scripts/element_copy_heal.py | 180 +++++++++++++++++++++++++++++++++++++++++++
1 file changed, 180 insertions(+)
diff --git a/scripts/element_copy_heal.py b/scripts/element_copy_heal.py
new file mode 100644
index 0000000..57b433e
--- /dev/null
+++ b/scripts/element_copy_heal.py
@@ -0,0 +1,180 @@
+#!/usr/bin/env python3
+r"""
+element_copy_heal (v2) — CRISP tile-seam heal that fixes ONLY the real physical
+tile-WRAP edges, with NO averaging/blur band (the old heal_band_mid smudge that
+Steve rejected 2026-06-12) and NO drawn discontinuity lines (the v1 half-roll
+patch-copy sliced a visible line through the frog's face on root 2766).
+
+DTD verdict C (2026-06-12, Claude+Codex unanimous): only the 4 physical wrap
+boundaries (x=0<->W, y=0<->H) are true tile joins; x=W/2 / y=H/2 are arbitrary
+interior sample lines that must NEVER be healed as seams (doing so cut the frog's
+face in v1). Motif continuity ACROSS a wrap is provided by the neighbor repeat,
+not by interior smoothing or synthetic cuts.
+
+METHOD (low-color / engraving path — the common wallco case):
+ 1. Quantize the whole tile to its dominant palette -> a crisp label per pixel.
+ We only WRITE a thin edge band; the interior keeps original RGB so stipple
+ shading is preserved.
+ 2. top<->bottom wrap: for each COLUMN where the outermost top pixel and
+ outermost bottom pixel carry DIFFERENT palette labels (a motif crosses the
+ wrap), vote a single wrap label across both outer bands and snap the outer
+ `band` px of BOTH edges on that column to that crisp palette color. Every
+ written pixel is a crisp palette color (no averaged smudge); only the
+ disagreeing columns are touched (no continuous drawn line); interior
+ untouched.
+ 3. left<->right wrap: transpose.
+ 4. interior mids: skipped entirely.
+
+Non-low-color designs fall back to a wrap-only crisp mirror on disagreeing
+lines (still no interior, still no full-image blur).
+
+Public entry point:
+ heal_array(arr, boxes, *, two_tone=None, edge_band=12) -> healed uint8 array
+`boxes` from seam-defect-boxes.py (used only to decide WHICH wrap edges need
+work; interior boxes ignored).
+
+NEVER imports make_seamless.py / force-edge-seamless.py (smear sources).
+NEVER calls cv2.inpaint / diffusion (those blur). Pure crisp palette-reconcile.
+"""
+from __future__ import annotations
+import numpy as np
+from PIL import Image
+from collections import Counter
+
+EDGE_BAND_DEFAULT = 12
+LOWCOLOR_MAX = 6
+
+
+def _rgb_to_lab(arr):
+ rgb = arr.astype('float32') / 255.0
+ def f(t):
+ return np.where(t > 0.04045, ((t + 0.055) / 1.055) ** 2.4, t / 12.92)
+ rl, gl, bl = f(rgb[..., 0]), f(rgb[..., 1]), f(rgb[..., 2])
+ X = rl * 0.4124564 + gl * 0.3575761 + bl * 0.1804375
+ Y = rl * 0.2126729 + gl * 0.7151522 + bl * 0.0721750
+ Z = rl * 0.0193339 + gl * 0.1191920 + bl * 0.9503041
+ def fl(t):
+ d = 6 / 29
+ return np.where(t > d ** 3, t ** (1 / 3), t / (3 * d * d) + 4 / 29)
+ fx, fy, fz = fl(X / 0.95047), fl(Y / 1.0), fl(Z / 1.08883)
+ return np.stack([116 * fy - 16, 500 * (fx - fy), 200 * (fy - fz)], axis=-1)
+
+
+def dominant_palette(arr, k=8, min_pct=3.0):
+ im = Image.fromarray(arr).convert('RGB')
+ q = im.quantize(colors=k, method=Image.Quantize.MEDIANCUT,
+ dither=Image.Dither.NONE).convert('RGB')
+ flat = np.asarray(q).reshape(-1, 3)
+ cnt = Counter(map(tuple, flat))
+ total = sum(cnt.values())
+ return [c for c, n in cnt.most_common() if 100.0 * n / total >= min_pct]
+
+
+def is_low_color(arr):
+ return len(dominant_palette(arr)) <= LOWCOLOR_MAX
+
+
+def _palette_labels(arr, palette):
+ pal = np.array(palette, dtype='uint8')
+ pal_lab = _rgb_to_lab(pal.reshape(1, -1, 3)).reshape(-1, 3)
+ lab = _rgb_to_lab(arr).reshape(-1, 3)
+ d = ((lab[:, None, :] - pal_lab[None, :, :]) ** 2).sum(-1)
+ idx = d.argmin(1).reshape(arr.shape[:2])
+ return idx, pal
+
+
+def _reconcile_wrap(out, labels, pal, axis, band):
+ """Make the two opposite wrap edges along axis carry the SAME crisp palette
+ content where they currently disagree. axis='h': top<->bottom (write columns);
+ axis='v': left<->right (write rows). Returns count of lines rewritten."""
+ H, W = labels.shape
+ n = 0
+ if axis == 'h':
+ disagree = labels[0, :] != labels[H - 1, :]
+ top_band = labels[0:band, :]
+ bot_band = labels[H - band:H, :]
+ for x in np.where(disagree)[0]:
+ votes = np.concatenate([top_band[:, x], bot_band[:, x]])
+ wrap = int(np.bincount(votes).argmax())
+ color = pal[wrap]
+ out[0:band, x, :] = color
+ out[H - band:H, x, :] = color
+ n += 1
+ else:
+ disagree = labels[:, 0] != labels[:, W - 1]
+ left_band = labels[:, 0:band]
+ right_band = labels[:, W - band:W]
+ for y in np.where(disagree)[0]:
+ votes = np.concatenate([left_band[y, :], right_band[y, :]])
+ wrap = int(np.bincount(votes).argmax())
+ color = pal[wrap]
+ out[y, 0:band, :] = color
+ out[y, W - band:W, :] = color
+ n += 1
+ return n
+
+
+def _wrap_mirror_disagree(out, arr, axis, band):
+ H, W = arr.shape[:2]
+ g = arr.astype('float32') @ np.array([0.299, 0.587, 0.114])
+ n = 0
+ if axis == 'h':
+ disagree = np.abs(g[0, :] - g[H - 1, :]) > 24
+ for x in np.where(disagree)[0]:
+ src = arr[band, x, :]
+ out[0:band, x, :] = src
+ out[H - band:H, x, :] = src
+ n += 1
+ else:
+ disagree = np.abs(g[:, 0] - g[:, W - 1]) > 24
+ for y in np.where(disagree)[0]:
+ src = arr[y, band, :]
+ out[y, 0:band, :] = src
+ out[y, W - band:W, :] = src
+ n += 1
+ return n
+
+
+def heal_array(arr, boxes, *, two_tone=None, edge_band=EDGE_BAND_DEFAULT):
+ """Heal ONLY the physical wrap edges flagged in boxes; interior mid boxes are
+ ignored (motif edges, not seams). Returns a NEW uint8 array."""
+ out = arr.copy()
+ if two_tone is None:
+ two_tone = is_low_color(arr)
+ have_ledge = any(b['kind'] in ('left_edge', 'right_edge') for b in boxes)
+ have_tedge = any(b['kind'] in ('top_edge', 'bottom_edge') for b in boxes)
+ if two_tone:
+ palette = dominant_palette(arr)
+ labels, pal = _palette_labels(arr, palette)
+ if have_tedge:
+ _reconcile_wrap(out, labels, pal, 'h', edge_band)
+ if have_ledge:
+ _reconcile_wrap(out, labels, pal, 'v', edge_band)
+ else:
+ if have_tedge:
+ _wrap_mirror_disagree(out, arr, 'h', edge_band)
+ if have_ledge:
+ _wrap_mirror_disagree(out, arr, 'v', edge_band)
+ return out
+
+
+if __name__ == '__main__':
+ import argparse, json, sys
+ from pathlib import Path
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
+ seam = __import__('seam-defect-boxes')
+ ap = argparse.ArgumentParser()
+ ap.add_argument('--path', required=True)
+ ap.add_argument('--out', required=True)
+ ap.add_argument('--edge-band', type=int, default=EDGE_BAND_DEFAULT)
+ a = ap.parse_args()
+ src = np.asarray(Image.open(a.path).convert('RGB'))
+ scan_b = seam.scan(src)
+ healed = heal_array(src, scan_b['boxes'], edge_band=a.edge_band)
+ Image.fromarray(healed).save(a.out)
+ scan_a = seam.scan(healed)
+ print(json.dumps({
+ 'before': scan_b['scores'], 'before_verdict': scan_b['verdict'],
+ 'after': scan_a['scores'], 'after_verdict': scan_a['verdict'],
+ 'two_tone': is_low_color(src), 'out': a.out,
+ }))
← d99cb67 Fix variants montage 0/90: count PASS rows from both i2i + o
·
back to Wallco Ai
·
TODO: seam joint-heal v2 + four-horsemen verdict + non-tilin af64e05 →