"""N1 — horizontal band seams on periodic (geometric) tile assets. Reproduces the canvas-engine texture path on a steep look-down floor (rooms 4/5 of the June-10 test set) for three texture preparations: raw : tile the source as-is (what "wrap" mode does) committed : makeSeamless v2 masked-shift (current organic path) snapped : proposed period-snap crop — detect the pattern's x/y period by autocorrelation, crop to an integer number of periods, and let it wrap exactly. Falls back to masked-shift when no strong period exists (wood, stone), so organic sources are untouched. Pass criteria: 1. checkered.jpeg classifies "organic" (it is — 2.5 x 1.65 periods). 2. Period-snap finds a period; the cropped tile re-classifies as "wrap". 3. Seam-spike score (max row-to-row jump / median) of the snapped render is at or below the committed render's, and shows no spike rows. 4. rustic-wood.jpg (organic) finds NO period -> falls back unchanged. """ import os import numpy as np from PIL import Image HERE = os.path.dirname(os.path.abspath(__file__)) TILES = os.path.join(HERE, "..", "..", "frontend", "viz2d-demo", "src", "assets", "tiles") OUT = os.path.join(HERE, "verify_out") os.makedirs(OUT, exist_ok=True) # ---------------------------------------------------------------- engine ports def detect_wrap_mode(img): """Exact port of detectWrapMode (canvas-engine.ts).""" h, w, _ = img.shape if w < 4 or h < 4: return "organic", np.inf, np.inf def col_diff(xa, xb): return float(np.mean(np.abs(img[:, xa].astype(np.float64) - img[:, xb].astype(np.float64)))) def row_diff(ya, yb): return float(np.mean(np.abs(img[ya].astype(np.float64) - img[yb].astype(np.float64)))) mid_x, mid_y = w >> 1, h >> 1 internal = max((col_diff(mid_x, mid_x + 1) + row_diff(mid_y, mid_y + 1)) / 2, 1.5) seam_x = col_diff(w - 1, 0) / internal seam_y = row_diff(h - 1, 0) / internal mode = "wrap" if (seam_x < 3 and seam_y < 3) else "organic" return mode, seam_x, seam_y def make_seamless(img): """Exact port of makeSeamless v2 (masked shift).""" h, w, _ = img.shape half_w, half_h = w >> 1, h >> 1 band_x = max(2, round(w * 0.12)) band_y = max(2, round(h * 0.12)) def edge(n, band): i = np.arange(n, dtype=np.float64) t = np.minimum(np.minimum(i, n - 1 - i) / band, 1.0) return t * t * (3 - 2 * t) m = np.outer(edge(h, band_y), edge(w, band_x))[..., None] shifted = np.roll(img, (-half_h, -half_w), axis=(0, 1)).astype(np.float64) return (img.astype(np.float64) * m + shifted * (1 - m)).astype(np.uint8) def find_period(img, axis): """Proposed: dominant pattern period along an axis (x: axis=1, y: axis=0). Mean-abs-diff between the image and itself shifted by each candidate lag; a strongly periodic texture has a deep minimum at the period. Searches a <=320px downsample, then refines at full resolution. Returns the full-res period or None. """ raw = np.mean(img.astype(np.float64), axis=2) h, w = raw.shape # High-pass: subtract a 16px low-pass reconstruction so the photo's # lighting gradient doesn't put a floor under the diff at true alignment. lp = np.asarray(Image.fromarray(raw).resize((16, 16), Image.BILINEAR).resize((w, h), Image.BILINEAR)) gray = raw - lp full = gray.shape[1] if axis == 1 else gray.shape[0] scale = max(1, int(np.ceil(full / 320))) ds = gray[::scale, ::scale] n = ds.shape[1] if axis == 1 else ds.shape[0] lags = np.arange(max(12, int(n * 0.18)), int(n * 0.80)) if len(lags) < 4: return None diffs = [] for lag in lags: if axis == 1: d = np.mean(np.abs(ds[:, lag:] - ds[:, :-lag])) else: d = np.mean(np.abs(ds[lag:] - ds[:-lag])) diffs.append(d) diffs = np.asarray(diffs) med = np.median(diffs) + 1e-6 best = diffs.min() # Generous gate: the post-crop wrap re-classification is the real safety # net; this only needs to exclude clearly aperiodic textures (wood ~0.85+, # stone ~0.96 vs checker ~0.46). if best / med > 0.65: return None # fundamental period: smallest lag within 15% of the global minimum idx = np.nonzero(diffs <= best * 1.15)[0][0] coarse = int(lags[idx]) * scale # refine at full resolution lo = max(8, coarse - scale - 2) hi = min(full - 1, coarse + scale + 2) best_lag, best_d = None, np.inf for lag in range(lo, hi + 1): if axis == 1: d = np.mean(np.abs(gray[:, lag:] - gray[:, :-lag])) else: d = np.mean(np.abs(gray[lag:] - gray[:-lag])) if d < best_d: best_d, best_lag = d, lag return best_lag def best_crop(img, k_periods, period, axis): """Micro-search +/-5px around k*period for the crop length L that tiles best. The crop [0, L) wraps perfectly iff line L (the true continuation in the source) matches line 0 — so that is the diff to minimize.""" n = img.shape[1] if axis == 1 else img.shape[0] target = k_periods * period win = max(4, min(16, period // 4)) # window beats flat-cell degeneracy cands = [] for cand in range(max(8, target - 5), min(n - win, target + 5) + 1): if axis == 1: d = np.mean(np.abs(img[:, cand:cand + win].astype(np.float64) - img[:, 0:win].astype(np.float64))) else: d = np.mean(np.abs(img[cand:cand + win].astype(np.float64) - img[0:win].astype(np.float64))) cands.append((cand, d)) if not cands: return target dmin = min(d for _, d in cands) # among near-ties, prefer the length closest to exactly k periods near = [c for c, d in cands if d <= dmin * 1.15 + 1e-6] return min(near, key=lambda c: abs(c - target)) def seam_vs_period_pair(img, crop_len, k, period, axis): """Wrap-seam diff relative to the texture's own k-period-pair diff. The wrap seam joins content k periods apart, so the fair yardstick is two interior lines k periods apart in the ORIGINAL image: they match in structure (grout geometry) but differ by per-tile surface noise and accumulated photo-perspective drift — the achievable floor for this seam. """ a = img.astype(np.float64) if axis == 0: a = a.transpose(1, 0, 2) # treat rows as columns; jog becomes vertical n = a.shape[1] dist = min(k * period, n - 1) a0 = max(0, (n - 1 - dist) // 2) # internal pair, centered, same distance win = max(2, min(8, period // 8)) jog = max(2, round(period * 0.04)) # photo-perspective drift tolerance def pair_diff(xa, xb): # min over a small perpendicular jog: a slightly skewed lattice still # tiles acceptably (the jog reads as installation tolerance, not a seam) best = np.inf pa = a[:, xa:xa + win] for dy in range(-jog, jog + 1): if dy >= 0: d = np.mean(np.abs(pa[dy:] - a[: a.shape[0] - dy, xb:xb + win])) else: d = np.mean(np.abs(pa[:dy] - a[-dy:, xb:xb + win])) best = min(best, d) return best seam = pair_diff(crop_len, 0) # floor = median over several internal pairs at the same distance — a # single pair can land in unrepresentatively flat or busy content starts = range(0, n - dist - win, max(1, (n - dist - win) // 8 or 1)) floors = [pair_diff(s + dist, s) for s in starts] or [pair_diff(a0 + dist, a0)] floor_ = float(np.median(floors)) return seam / max(floor_, 1e-6) def period_snap(img): """Proposed prepare step: crop to integer periods if strongly periodic and the seam is no worse than the texture's own period-pair noise floor; otherwise fall back to masked shift.""" h, w, _ = img.shape px = find_period(img, axis=1) py = find_period(img, axis=0) if px and py: kx, ky = w // px, h // py # the continuation line (k*period) must exist in the source to verify # the wrap; an exact-multiple image keeps one period fewer while kx > 1 and kx * px >= w: kx -= 1 while ky > 1 and ky * py >= h: ky -= 1 if kx * px < w and ky * py < h \ and kx >= 1 and ky >= 1 and kx * px >= 0.4 * w and ky * py >= 0.4 * h: cw = best_crop(img, kx, px, axis=1) ch = best_crop(img, ky, py, axis=0) rx = seam_vs_period_pair(img, cw, kx, px, axis=1) ry = seam_vs_period_pair(img, ch, ky, py, axis=0) if rx < 1.5 and ry < 1.5: return img[:ch, :cw], ("snap", px, py, rx, ry) return make_seamless(img), ("fallback", px, py, None, None) def flatten_luminance(img): """R2-3b — remove the source photo's baked lighting so the texture behaves like albedo: divide by a heavily-blurred (toroidal) luminance field normalized to the texture mean. The blur wraps, so the correction is itself seamless; only the lighting gradient is equalized, grain/veining survives. Mirror of flattenLuminance (canvas-engine.ts) — keep in lockstep.""" h, w, _ = img.shape lum = ( img[..., 0] * 0.299 + img[..., 1] * 0.587 + img[..., 2] * 0.114 ).astype(np.float64) def box_wrap(a, r, axis): win = 2 * r + 1 out = np.zeros_like(a) for d in range(-r, r + 1): out += np.roll(a, -d, axis=axis) return out / win rx = max(2, w // 4) ry = max(2, h // 4) for _ in range(2): lum = box_wrap(lum, rx, axis=1) lum = box_wrap(lum, ry, axis=0) gain = np.clip(lum.mean() / np.maximum(lum, 1.0), 0.6, 1.6) out = np.clip(np.round(img.astype(np.float64) * gain[..., None]), 0, 255) return out.astype(np.uint8) def build_mips(img): mips = [img.astype(np.float64)] cur = img.astype(np.float64) while cur.shape[0] > 1 or cur.shape[1] > 1: h, w, _ = cur.shape nh, nw = max(1, h >> 1), max(1, w >> 1) y0 = np.minimum(2 * np.arange(nh), h - 1) y1 = np.minimum(2 * np.arange(nh) + 1, h - 1) x0 = np.minimum(2 * np.arange(nw), w - 1) x1 = np.minimum(2 * np.arange(nw) + 1, w - 1) cur = (cur[y0][:, x0] + cur[y0][:, x1] + cur[y1][:, x0] + cur[y1][:, x1]) / 4 mips.append(cur) return mips def sample_bilinear_wrap(level, x, y): h, w, _ = level.shape x0 = np.clip(np.floor(x), 0, w - 1).astype(np.int64) y0 = np.clip(np.floor(y), 0, h - 1).astype(np.int64) x1 = (x0 + 1) % w y1 = (y0 + 1) % h fx = (x - np.floor(x))[..., None] fy = (y - np.floor(y))[..., None] p00, p10 = level[y0, x0], level[y0, x1] p01, p11 = level[y1, x0], level[y1, x1] return (p00 * (1 - fx) * (1 - fy) + p10 * fx * (1 - fy) + p01 * (1 - fx) * fy + p11 * fx * fy) def render_floor(tex, img_w=900, img_h=700, repeat_w=180.0): """Steep look-down floor like rooms 4/5: asymmetric trapezoid -> deep plane.""" th, tw, _ = tex.shape repeat_h = repeat_w * (th / tw) plane_w, plane_h = 900.0, 1600.0 # image trapezoid (slight asymmetry = synthetic-VP shear) -> plane rect src = np.array([[260, 120], [610, 120], [900, 700], [0, 700]], np.float64) dst = np.array([[0, 0], [plane_w, 0], [plane_w, plane_h], [0, plane_h]], np.float64) A = [] for (sx, sy), (dx_, dy_) in zip(src, dst): A.append([sx, sy, 1, 0, 0, 0, -dx_ * sx, -dx_ * sy]) A.append([0, 0, 0, sx, sy, 1, -dy_ * sx, -dy_ * sy]) b = dst.reshape(-1) hvec = np.linalg.solve(np.asarray(A), b) H = np.append(hvec, 1).reshape(3, 3) xs, ys = np.meshgrid(np.arange(img_w, dtype=np.float64), np.arange(img_h, dtype=np.float64)) def to_plane(px, py): zz = H[2, 0] * px + H[2, 1] * py + H[2, 2] return ((H[0, 0] * px + H[0, 1] * py + H[0, 2]) / zz, (H[1, 0] * px + H[1, 1] * py + H[1, 2]) / zz) fx, fy = to_plane(xs, ys) fx1, fy1 = to_plane(xs + 1, ys) fx2, fy2 = to_plane(xs, ys + 1) # floor mask: inside the plane rect mask = (fx >= 0) & (fx < plane_w) & (fy >= 0) & (fy < plane_h) u = np.mod(fx / repeat_w, 1.0) v = np.mod(fy / repeat_h, 1.0) tcx, tcy = (fx / repeat_w) * tw, (fy / repeat_h) * th du = np.hypot((fx1 / repeat_w) * tw - tcx, (fy1 / repeat_h) * th - tcy) dv = np.hypot((fx2 / repeat_w) * tw - tcx, (fy2 / repeat_h) * th - tcy) footprint = np.maximum(np.maximum(du, dv), 1e-3) lod = np.log2(footprint) + 0.5 mips = build_mips(tex) max_l = len(mips) - 1 l0 = np.clip(np.floor(lod), 0, max_l).astype(np.int64) f = np.clip(lod - l0, 0, 1) out = np.zeros((img_h, img_w, 3), np.float64) for lev in range(max_l + 1): sel = mask & (l0 == lev) if not sel.any(): continue a = mips[lev] sa = sample_bilinear_wrap(a, u[sel] * a.shape[1], v[sel] * a.shape[0]) fb = f[sel][..., None] if lev < max_l: bl = mips[lev + 1] sb = sample_bilinear_wrap(bl, u[sel] * bl.shape[1], v[sel] * bl.shape[0]) out[sel] = sa + (sb - sa) * fb else: out[sel] = sa return out.astype(np.uint8), mask def sharpness_profile(render, mask): """Per-row mean |horizontal gradient| inside the floor. Crossfade ghost bands collapse local contrast, so they show up as dips in this profile. Comparing per-row against the raw render cancels the natural LOD falloff.""" g = np.mean(render.astype(np.float64), axis=2) grad = np.abs(np.diff(g, axis=1)) both = mask[:, :-1] & mask[:, 1:] rows = np.nonzero(both.sum(axis=1) > 200)[0] prof = np.array([np.mean(grad[y, both[y]]) for y in rows]) return rows, prof # -------------------------------------------------------------------- run def load(name): return np.asarray(Image.open(os.path.join(TILES, name)).convert("RGB")) def main(): ok = True checker = load("checkered.jpeg") mode, sx, sy = detect_wrap_mode(checker) print(f"checkered.jpeg : {checker.shape[1]}x{checker.shape[0]} mode={mode} " f"seamX={sx:.1f}x seamY={sy:.1f}x (threshold 3x)") if mode != "organic": print(" !! expected organic"); ok = False snapped, info = period_snap(checker) tag, px, py, csx, csy = info print(f"period-snap : {tag} periodX={px} periodY={py} " f"crop={snapped.shape[1]}x{snapped.shape[0]} " f"crop-seamX={csx if csx is None else f'{csx:.1f}x'} " f"crop-seamY={csy if csy is None else f'{csy:.1f}x'}") if tag != "snap": print(" !! period-snap did not engage on the checker"); ok = False healed = make_seamless(checker) # Real-photo renders: saved for visual / golden-image comparison (the # photo's per-tile texture variance defeats simple numeric seam metrics). base_repeat = 180.0 snap_repeat = base_repeat * (snapped.shape[1] / checker.shape[1]) for name, tex, rep in [("raw", checker, base_repeat), ("committed", healed, base_repeat), ("snapped", snapped, snap_repeat)]: render, _ = render_floor(tex, repeat_w=rep) Image.fromarray(render).save(os.path.join(OUT, f"n1_{name}.png")) print(f"photo renders saved to verify_out/n1_*.png (visual check)") # ---- synthetic certification: ground truth is known exactly ---------- # Perfect checker, period 64, sized to a NON-integer period count so it # classifies organic. The ideal result is the exact 5-period crop tiled # raw; the snapped pipeline must reproduce it pixel-for-pixel. per = 64 sw, sh = per * 5 + 37, per * 4 + 21 yy, xx = np.mgrid[0:sh, 0:sw] cells = ((xx // (per // 2)) + (yy // (per // 2))) % 2 rng = np.random.default_rng(7) noise = rng.normal(0, 4, (sh, sw)) synth = np.stack([np.where(cells, 205, 120) + noise, np.where(cells, 200, 90) + noise, np.where(cells, 190, 60) + noise], axis=2).clip(0, 255).astype(np.uint8) smode, ssx, ssy = detect_wrap_mode(synth) print(f"synthetic checker: {sw}x{sh} period={per} mode={smode} " f"seam=({ssx:.1f}x,{ssy:.1f}x)") if smode != "organic": print(" !! synthetic checker should classify organic"); ok = False s_snap, s_info = period_snap(synth) print(f"synthetic snap : {s_info[0]} periodX={s_info[1]} periodY={s_info[2]} " f"crop={s_snap.shape[1]}x{s_snap.shape[0]}") if s_info[0] != "snap": print(" !! period-snap did not engage on synthetic checker"); ok = False else: if s_info[1] % per != 0 or s_info[2] % per != 0: print(f" !! found period not a multiple of {per}"); ok = False ideal = synth[:(sh // per) * per, :(sw // per) * per] rep_snap = 180.0 * (s_snap.shape[1] / sw) rep_ideal = 180.0 * (ideal.shape[1] / sw) r_snap, m1 = render_floor(s_snap, repeat_w=rep_snap) r_ideal, m2 = render_floor(ideal, repeat_w=rep_ideal) r_healed, _ = render_floor(make_seamless(synth), repeat_w=180.0) Image.fromarray(r_snap).save(os.path.join(OUT, "n1_synth_snapped.png")) Image.fromarray(r_ideal).save(os.path.join(OUT, "n1_synth_ideal.png")) Image.fromarray(r_healed).save(os.path.join(OUT, "n1_synth_committed.png")) m = m1 & m2 d_snap = float(np.mean(np.abs(r_snap[m].astype(float) - r_ideal[m].astype(float)))) d_healed = float(np.mean(np.abs(r_healed[m].astype(float) - r_ideal[m].astype(float)))) print(f"synthetic render : snapped-vs-ideal={d_snap:.2f} " f"committed-vs-ideal={d_healed:.2f} (mean abs px)") if d_snap > 3.0: print(" !! snapped render deviates from ideal"); ok = False if d_healed < d_snap: print(" !! masked-shift unexpectedly beats period-snap"); ok = False # no-regression: organic wood must NOT engage period-snap wood = load("rustic-wood.jpg") _, winfo = period_snap(wood) print(f"rustic-wood.jpg : period-snap -> {winfo[0]} " f"(periodX={winfo[1]} periodY={winfo[2]})") if winfo[0] != "fallback": print(" !! wood should fall back to masked-shift"); ok = False # informational: how do the other catalog tiles classify? for name in ["floor-natural-stone.jpg", "basalt-outside-wal.jpg", "mosaic-tile.jpg"]: t = load(name) m, a, b = detect_wrap_mode(t) _, i2 = period_snap(t) print(f"{name:24s}: mode={m:7s} seam=({a:.1f}x,{b:.1f}x) snap={i2[0]} px={i2[1]} py={i2[2]}") print("\n" + ("ALL N1 CHECKS PASSED" if ok else "N1 CHECKS FAILED")) return 0 if ok else 1 if __name__ == "__main__": raise SystemExit(main())