Spaces:
Sleeping
Sleeping
File size: 4,634 Bytes
802aa1a | 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 | """
verify_rotation.py — scratch check for P1-4 default tile rotation.
Computes the wall-aligned rotation from a bundle (same algorithm as
estimate_default_rotation in app.py), then renders a brick grid with
rotation 0 (left) vs the computed rotation (right) using the exact frontend
rotation math, to confirm the sign and magnitude visually.
Usage:
python verify_rotation.py /tmp/hf_new_bundle.json
"""
import base64
import io
import json
import sys
import numpy as np
from PIL import Image
def estimate_default_rotation(mask, H, vp2=None):
h, w = mask.shape[:2]
def _to_plane(img_pts):
ones = np.ones((len(img_pts), 1), dtype=np.float64)
p = np.hstack([img_pts.astype(np.float64), ones]) @ H.T
zs = p[:, 2]
valid = np.abs(zs) > 1e-9
if valid.sum() < 2:
return None
return p[valid, 0] / zs[valid], p[valid, 1] / zs[valid]
pts = []
for x in range(int(w * 0.2), int(w * 0.8), 4):
col = np.where(mask[:, x] > 0)[0]
if len(col) > 10:
pts.append((float(x), float(col[0])))
if len(pts) >= 12:
plane = _to_plane(np.array(pts))
if plane is not None:
px, py = plane
full_span = px.max() - px.min()
keep = np.ones(len(px), dtype=bool)
angle = None
for i in range(3):
if keep.sum() < 12:
break
sub_x, sub_y = px[keep], py[keep]
span = sub_x.max() - sub_x.min()
if span < max(1e-6, full_span * 0.3):
break
slope, intercept = np.polyfit(sub_x, sub_y, 1)
resid = np.abs(py - (slope * px + intercept))
rms = float(np.sqrt(np.mean(resid[keep] ** 2)))
print(f" pass {i}: n={keep.sum()} span={span:.0f} rms={rms:.1f} "
f"({rms / span:.3f} of span) angle={np.degrees(np.arctan(slope)):.1f}")
if rms < span * 0.04 and keep.sum() >= len(px) * 0.6:
angle = float(np.degrees(np.arctan(slope)))
break
keep &= resid <= np.percentile(resid[keep], 70)
if angle is not None and abs(angle) < 25.0:
return angle
return 0.0
def decode_pixels(bundle):
w, h = bundle["width"], bundle["height"]
raw = base64.b64decode(bundle["pixels"])
if len(raw) == w * h * 4:
return np.frombuffer(raw, np.uint8).reshape(h, w, 4)[:, :, :3].copy()
return np.array(Image.open(io.BytesIO(raw)).convert("RGB"))
def render(bundle, rotation_deg):
w, h = bundle["width"], bundle["height"]
img = decode_pixels(bundle)
seg = bundle["segments"][0]
idx = np.frombuffer(base64.b64decode(seg["mask"]), dtype=np.uint32)
H = np.array(seg["homography"], dtype=np.float64).reshape(3, 3)
p = seg["plane"]
ys, xs = idx // w, idx % w
pw, ph = p["width"], p["height"]
cx, cy = p["x"] + pw / 2, p["y"] + ph / 2
repeat = max(32.0, pw * 0.18)
pts = np.column_stack([xs, ys, np.ones(len(xs))]) @ H.T
fx = pts[:, 0] / pts[:, 2]
fy = pts[:, 1] / pts[:, 2]
# frontend rotation math: rad = rot*pi/180; cos(-rad), sin(-rad)
rad = rotation_deg * np.pi / 180.0
c, s = np.cos(-rad), np.sin(-rad)
dx, dy = fx - cx, fy - cy
rx = dx * c - dy * s
ry = dx * s + dy * c
u = rx / repeat
v = ry / repeat
row = np.floor(v).astype(int)
uu = u + (row % 2) * 0.5
cell = ((np.floor(uu).astype(int) + row) % 2).astype(bool)
fu, fv = uu - np.floor(uu), v - np.floor(v)
grout = (fu < 0.06) | (fv < 0.06)
color = np.where(cell[:, None], [184, 115, 51], [222, 184, 135]).astype(np.uint8)
color[grout] = (60, 60, 60)
out = img.copy()
out[ys, xs] = (0.75 * color + 0.25 * out[ys, xs]).astype(np.uint8)
return out
def main():
bundle_path = sys.argv[1]
with open(bundle_path) as f:
bundle = json.load(f)
w, h = bundle["width"], bundle["height"]
seg = bundle["segments"][0]
idx = np.frombuffer(base64.b64decode(seg["mask"]), dtype=np.uint32)
mask = np.zeros(h * w, np.uint8)
mask[idx] = 1
mask = mask.reshape(h, w)
H = np.array(seg["homography"], dtype=np.float64).reshape(3, 3)
rot = estimate_default_rotation(mask, H)
print(f"defaultRotation = {rot:.2f} deg")
a = render(bundle, 0.0)
b = render(bundle, rot)
Image.fromarray(np.hstack([a, b])).save("verify_out/rotation_compare.png")
print("saved verify_out/rotation_compare.png (left=rotation 0, right=defaultRotation)")
if __name__ == "__main__":
main()
|