fix(viz): mirror-conjugate point rotation
CI build oscope-of / build-check (push) Has been cancelled

Live report: video straight but skeleton rotated wrong. Flip and
90-degree rotation do not commute — the video is flipped THEN rotated
while points are rotated THEN display-flipped. Points now use the
mirror-conjugated mode (cw/ccw swapped under mirror), proven by a
composition test.
This commit is contained in:
L'électron rare
2026-07-02 14:41:57 +02:00
parent ef1d557eff
commit c66c4271fa
2 changed files with 36 additions and 2 deletions
+16 -2
View File
@@ -51,6 +51,19 @@ def rotate_norm_xy(x: float, y: float, mode: str) -> "tuple[float, float]":
return f(x, y) if f else (x, y)
def effective_point_rotate(mode: str, mirror: bool) -> str:
"""Point-rotation mode equivalent to the video pipeline's transform.
The VIDEO is horizontally flipped at decode (mirror) THEN rotated by
multi.py; the POINTS are rotated here THEN flipped at display time by
the renderer. Flip and 90-degree rotations do not commute: under the
mirror the point rotation must be the mirror-conjugate of the video
rotation (cw <-> ccw swapped; 180 commutes)."""
if mirror and mode in ("cw", "ccw"):
return "cw" if mode == "ccw" else "ccw"
return mode
def _make_point(x: float, y: float, z: float, conf: float) -> PoseKp:
"""Wrap raw floats from the wire into a PoseKp landmark.
@@ -150,8 +163,9 @@ class IphoneUSBSource:
self._mirror = mirror and _cfg.concert_mirror
# VIDEO_ROTATE also applies to the iPhone 2D landmarks (skeleton2D +
# hands) so they stay aligned with the rotated video (multi.py only
# rotates the frame).
self._video_rotate = _cfg.video_rotate or "none"
# rotates the frame). Mirror-conjugated: see effective_point_rotate.
self._video_rotate = effective_point_rotate(
_cfg.video_rotate or "none", self._mirror)
self._codec = av.codec.CodecContext.create("hevc", "r") if av is not None else None
self._lock = threading.Lock()
self._frame = None # latest BGR np.ndarray
@@ -301,3 +301,23 @@ def test_rotate_norm_xy_matches_rot90():
assert rotate_norm_xy(0.2, 0.7, "180") == pytest.approx((0.8, 0.3))
assert rotate_norm_xy(0.2, 0.7, "none") == (0.2, 0.7)
assert rotate_norm_xy(0.2, 0.7, "garbage") == (0.2, 0.7)
def test_effective_point_rotate_mirror_conjugation():
"""Video = flip THEN rotate; points = rotate THEN flip (renderer).
Prove the conjugated mode lands points where the video pixel lands:
flip_x(T(x,y)) must equal rot(flip_x(x,y)) for T = conjugate(rot)."""
from data_only_viz.iphone_usb_source import (
effective_point_rotate, rotate_norm_xy,
)
flip = lambda x, y: (1.0 - x, y)
for video_mode in ("ccw", "cw", "180", "none"):
pt_mode = effective_point_rotate(video_mode, mirror=True)
for (x, y) in [(0.2, 0.7), (0.9, 0.1), (0.5, 0.5)]:
video_dest = rotate_norm_xy(*flip(x, y), video_mode)
point_dest = flip(*rotate_norm_xy(x, y, pt_mode))
assert video_dest == pytest.approx(point_dest), (
video_mode, x, y)
# No mirror: modes pass through untouched
assert effective_point_rotate("ccw", mirror=False) == "ccw"
assert effective_point_rotate("none", mirror=True) == "none"