File size: 6,518 Bytes
8704d80
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
"""
verify_p2_sim.py — synthetic certification for the P2 edge-quality fixes.

T7: the dilated occluder ring used to leave 2-5px of ORIGINAL floor around
    furniture, and the confidence feather made the tile translucent there.
    Expect: tile reclaimed up to a 1px margin, full opacity at depth edges,
    feather kept only at the floor↔wall boundary.
T8: stair-stepped segmentation boundaries on occluders.
    Expect: rounded edges, thin components (chair legs) never erased.

Runs the REAL functions extracted from app.py against a synthetic room,
asserts the geometry, and renders an old-vs-new composite.

Usage:
    python verify_p2_sim.py
"""

import numpy as np
import cv2
from PIL import Image

# --- extract the real implementations from app.py -------------------------
src = open("app.py").read()
ns = {
    "np": np,
    "cv2": cv2,
    "OCCLUDER_CLASSES": {"occ"},
    "REJECT_SURFACE_CLASSES": {"rej"},
    "class_ids": lambda names: [10] if "occ" in names else [20],
}
for fn in ["clean_floor_mask", "build_floor_surface_mask", "build_confidence_map"]:
    start = src.index(f"def {fn}")
    end = src.index("\ndef ", start + 10)
    exec(compile(src[start:end], "app.py", "exec"), ns)

# --- synthetic room --------------------------------------------------------
H, W = 600, 800
FLOOR_Y = 320
seg = np.zeros((H, W), np.int32)
seg[:FLOOR_Y, :] = 20                       # wall

# sofa with stair-stepped right edge (4px steps every 6 rows)
for y in range(250, 450):
    step = 4 * ((y // 6) % 2)
    seg[y, 100:300 + step] = 10

# thin 3px free-standing chair leg
seg[350:430, 500:503] = 10

# thin 3px leg ATTACHED to the sofa body (blur erases it, body survives —
# must be restored by the removed-chunk guard, not the component guard)
seg[450:480, 150:153] = 10

# curtain with zigzag hem
for x in range(600, 700):
    hem = 380 - 10 * ((x // 8) % 2)
    seg[0:hem, x] = 10

floor_mask = ((seg == 0) & (np.arange(H)[:, None] >= FLOOR_Y)).astype(np.uint8)

surface, occ_zone = ns["build_floor_surface_mask"](floor_mask, seg, None, None)
conf_new = ns["build_confidence_map"](surface, occ_zone)
conf_old = ns["build_confidence_map"](surface, None)
occ_raw = (seg == 10).astype(np.uint8)

# --- assertions ------------------------------------------------------------
def first_surface_right_of(y, x_edge):
    row = surface[y, x_edge + 1:]
    nz = np.flatnonzero(row)
    return (x_edge + 1 + nz[0]) if len(nz) else None

print("== T7: fringe gap + opacity at depth edges ==")
for y in (380, 400, 420):
    edge = 300 + 4 * ((y // 6) % 2) - 1          # sofa's true right edge
    fx = first_surface_right_of(y, edge)
    gap = fx - edge - 1
    print(f"  y={y}: gap={gap}px  conf_new={conf_new[y, fx]}  conf_old={conf_old[y, fx]}")
    assert gap <= 3, "fringe gap should be <=3px"
    assert conf_new[y, fx] == 255, "tile must be opaque at the depth edge"
    assert conf_old[y, fx] < 200, "old feather should have been translucent here"

print("== T7: floor-wall boundary keeps its feather, no gap line ==")
col = 50
ys = np.flatnonzero(surface[:, col])
top = ys[0]
ramp = [int(conf_new[top + d, col]) for d in range(0, 9)]
print(f"  top surface row at x={col}: y={top}, conf ramp: {ramp}")
assert conf_new[top, col] < 200, "outer boundary must still feather"
assert 0 not in ramp, "no untiled gap line inside the wall feather"
assert ramp[-1] == 255, "feather must finish within ~8px"
assert all(b >= a for a, b in zip(ramp, ramp[1:])), "ramp must be monotonic"

print("== T8: free-standing leg survives smoothing, no tile painted on it ==")
leg = surface[355:425, 500:503]
assert leg.sum() == 0, "tile must not cover the leg"
assert occ_zone[390, 501] > 0, "leg must remain in the occluder zone"
fx = first_surface_right_of(390, 502)
print(f"  leg untouched, tile resumes {fx - 503}px right of it")
assert fx - 503 <= 3

print("== T8: ATTACHED leg survives via removed-chunk guard ==")
att = surface[455:478, 150:153]
assert att.sum() == 0, "tile must not cover the attached leg"
assert occ_zone[465, 151] > 0, "attached leg must remain in the occluder zone"
print("  attached leg untouched")

print("== T8: boundary roughness (perimeter ratio, lower = smoother) ==")
def perimeter(m):
    cs, _ = cv2.findContours(m, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
    return sum(cv2.arcLength(c, True) for c in cs)
# re-run just the T8 blur (without the guards) for the perimeter metric
smooth_k = max(13, min(H, W) // 100) | 1
occ_smooth = (cv2.GaussianBlur(occ_raw.astype(np.float32), (smooth_k, smooth_k), 0) >= 0.5).astype(np.uint8)
p_raw, p_smooth = perimeter(occ_raw), perimeter(occ_smooth)
print(f"  k={smooth_k}: perimeter raw={p_raw:.0f}  smoothed={p_smooth:.0f}  ({p_smooth / p_raw:.2f}x)")
assert p_smooth < p_raw * 0.92, "staircase must actually collapse"

print("== T8: tile edge beside the sofa is straighter than the raw stairs ==")
tile_edge = [np.flatnonzero(surface[y, 250:340])[0] for y in range(370, 430)]
amp = max(tile_edge) - min(tile_edge)
print(f"  tile-edge amplitude over 60 rows: {amp}px (raw stair amplitude: 4px)")
assert amp <= 2, "tile edge must be straighter than the raw staircase"

print("== T7: furniture feet sit flush (small gap below sofa bottom) ==")
below = np.flatnonzero(surface[450:, 200])
gap = below[0] if len(below) else 99
print(f"  first tile row below sofa bottom: {gap}px")
assert gap <= 3, "feet must sit within 3px of the tile"

# --- composite render: old vs new ------------------------------------------
room = np.full((H, W, 3), (150, 110, 70), np.uint8)      # warm original floor
room[:FLOOR_Y] = (210, 205, 195)                          # wall
room[occ_raw > 0] = (45, 40, 38)                          # dark furniture

yy, xx = np.mgrid[0:H, 0:W]
checker = (((yy // 24) + (xx // 24)) % 2).astype(bool)
tile = np.where(checker[..., None], (235, 235, 230), (200, 200, 195)).astype(np.uint8)

def composite(conf):
    a = (conf.astype(np.float32) / 255.0)[..., None]
    out = room.astype(np.float32) * (1 - a) + tile.astype(np.float32) * a
    return out.astype(np.uint8)

old, new = composite(conf_old), composite(conf_new)
side = np.hstack([old, new])
Image.fromarray(side).save("verify_out/p2_compare.png")
crop = np.hstack([old[330:470, 250:560], new[330:470, 250:560]])
crop = cv2.resize(crop, None, fx=2.5, fy=2.5, interpolation=cv2.INTER_NEAREST)
Image.fromarray(crop).save("verify_out/p2_compare_crop.png")
print("saved verify_out/p2_compare.png + p2_compare_crop.png (left=old, right=new)")
print("ALL P2 CHECKS PASSED")