room-visualizer / verify_r3_rug_sim.py
GitHub Actions
Deploy from GitHub commit 9a7f2e42acc186bb14fe484d0e1019bb59e3e334
c4458f9
Raw
History Blame Contribute Delete
6.26 kB
"""R3-3 β€” rug-replace certification (CI-safe, no models).
Two contracts:
1. Class routing: 'rug' is a REPLACEABLE class (RUG_CLASSES), not an
occluder β€” the benchmark behaviour. Verified against the real class sets
from app.py.
2. Shade neutrality: with the rug replaced, the heuristic shade map must
NOT carry the rug's albedo (a dark persian rug would otherwise transfer
as a giant stain onto the new floor). Inside the rug the shade flattens
to neutral; outside the feather band the shading is untouched.
"""
import cv2
import numpy as np
# --- real implementations / class sets from app.py ---------------------------
src = open("app.py").read()
ns = {"np": np, "cv2": cv2}
sets_start = src.index("PRIMARY_FLOOR_CLASSES")
sets_end = src.index("\n\n", src.index("OCCLUDER_CLASSES"))
exec(compile(src[sets_start:sets_end], "app.py", "exec"), ns)
for fn in [
"_adaptive_shade_range",
"_encode_shade",
"_dominant_period",
"_suppress_periodic_shading",
"build_shade_map",
]:
start = src.index(f"def {fn}")
end = src.index("\ndef ", start + 10)
exec(compile(src[start:end], "app.py", "exec"), ns)
build_shade_map = ns["build_shade_map"]
H, W = 480, 640
def scene():
"""Bright floor (lower half) with a gentle vertical light gradient and a
DARK rug rectangle; the mask covers floor incl. rug (R3-3 union)."""
yy = np.mgrid[0:H, 0:W][0].astype(np.float64)
lum = 150.0 + 40.0 * (yy - H / 2) / (H / 2)
img = np.clip(lum, 0, 255)
mask = np.zeros((H, W), np.uint8)
mask[H // 2 :, :] = 1
rug = np.zeros((H, W), np.uint8)
rug[int(H * 0.65) : int(H * 0.85), int(W * 0.3) : int(W * 0.7)] = 1
img = np.where(rug > 0, 60.0, img)
img3 = np.stack([img] * 3, axis=2).astype(np.uint8)
return img3, mask, rug
def decode(enc, rng):
lo, hi = rng
return lo + enc.astype(np.float64) / 255.0 * (hi - lo)
def main():
ok = True
# 1 β€” class routing
rug_ok = (
"rug" in ns["RUG_CLASSES"]
and "rug" not in ns["OCCLUDER_CLASSES"]
and "rug" not in ns["FLOOR_SURFACE_CLASSES"]
)
print(f" [{'PASS' if rug_ok else 'FAIL'}] class routing: rug replaceable "
f"(RUG_CLASSES), not occluder, not silently merged into floor classes")
ok &= rug_ok
img, mask, rug = scene()
enc_with, rng_with = build_shade_map(img, mask, rug)
enc_wo, rng_wo = build_shade_map(img, mask, None)
rel_with = decode(enc_with.reshape(H, W), rng_with)
rel_wo = decode(enc_wo.reshape(H, W), rng_wo)
rug_interior = cv2.erode(rug, np.ones((31, 31), np.uint8)) > 0
# 2 β€” without neutralisation the rug albedo IS a stain (documents the bug)
stain = float(rel_wo[rug_interior].mean())
good = stain < 0.85
print(f" [{'PASS' if good else 'FAIL'}] un-neutralised rug reads as stain: "
f"mean shade {stain:.3f} < 0.85")
ok &= good
# 3 β€” with the rug mask the region flattens to neutral
flat = float(np.abs(rel_with[rug_interior] - 1.0).mean())
good = flat < 0.05
print(f" [{'PASS' if good else 'FAIL'}] neutralised: mean |shade-1| inside rug "
f"{flat:.3f} < 0.05")
ok &= good
# --- R3-3 v2 β€” rug-region refinement (refine_rug_mask) -------------------
SOFT_ID = 7
rns = {
"np": np,
"cv2": cv2,
"class_ids": lambda names: [SOFT_ID],
"SOFT_FURNISHING_CLASSES": {"cushion"},
}
start = src.index("def refine_rug_mask")
end = src.index("\n# ---", start)
exec(compile(src[start:end], "app.py", "exec"), rns)
refine_rug_mask = rns["refine_rug_mask"]
def striped(shape):
yy, xx = np.indices(shape)
return (90 + 60 * ((xx + yy // 4) % 2)).astype(np.uint8)
img2 = np.full((H, W, 3), 170, np.uint8)
seg2 = np.zeros((H, W), np.int64)
rug2 = np.zeros((H, W), np.uint8)
# main rug: striped pattern, 120x160
rug2[300:420, 200:360] = 1
img2[300:420, 200:360] = striped((120, 160))[..., None]
# stray island: 12x12 far away (mislabelled accent tile)
rug2[100:112, 500:512] = 1
# object on the rug: black 24x24 blob (slippers)
img2[340:364, 260:284] = 15
# look-alike soft fragment adjacent to the rug (same stripes)
seg2[300:420, 362:410] = SOFT_ID
img2[300:420, 362:410] = striped((120, 48))[..., None]
# genuine plain cushion adjacent on the other side (uniform, L matches
# the stripe median -> only the texture-contrast gate can save it)
seg2[300:420, 150:198] = SOFT_ID
img2[300:420, 150:198] = 120
refined, protect = refine_rug_mask(rug2, seg2, img2)
# 5 β€” stray island dropped, main rug kept
good = refined[100:112, 500:512].sum() == 0 and refined[305:315, 205:215].all()
print(f" [{'PASS' if good else 'FAIL'}] v2: stray island dropped, rug kept")
ok &= good
# 6 β€” object on the rug carved out and protected
blob = np.zeros((H, W), bool)
blob[342:362, 262:282] = True
good = (protect[blob].mean() > 0.9) and (refined[blob].sum() == 0)
print(f" [{'PASS' if good else 'FAIL'}] v2: object on rug carved out + protected "
f"(protected {protect[blob].mean():.2f} of blob)")
ok &= good
# 7 β€” look-alike fragment absorbed into the rug
good = refined[310:410, 370:400].mean() > 0.9
print(f" [{'PASS' if good else 'FAIL'}] v2: look-alike fragment absorbed "
f"({refined[310:410, 370:400].mean():.2f} of fragment)")
ok &= good
# 8 β€” plain cushion NOT absorbed (texture-contrast gate abstains)
good = refined[310:410, 160:190].sum() == 0
print(f" [{'PASS' if good else 'FAIL'}] v2: plain cushion stays an occluder")
ok &= good
# 9 β€” shading outside the feather band is untouched
feather_zone = cv2.dilate(rug, np.ones((81, 81), np.uint8)) > 0
outside = (mask > 0) & ~feather_zone
drift = float(np.abs(rel_with[outside] - rel_wo[outside]).mean())
good = drift < 0.02
print(f" [{'PASS' if good else 'FAIL'}] outside feather band untouched: "
f"mean drift {drift:.4f} < 0.02")
ok &= good
print("\n" + ("ALL R3-3 SIM CHECKS PASSED" if ok else "R3-3 SIM CHECKS FAILED"))
return 0 if ok else 1
if __name__ == "__main__":
raise SystemExit(main())