fix(viz): mirror-conjugate point rotation
CI build oscope-of / build-check (push) Has been cancelled
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:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user