diff --git a/open_pose_rotator.py b/open_pose_rotator.py index a386647..c8b60a4 100644 --- a/open_pose_rotator.py +++ b/open_pose_rotator.py @@ -27,8 +27,6 @@ class OpenPoseRotator: "optional": { "pose_keypoint": ("POSE_KEYPOINT",), "debug": ("BOOLEAN", {"default": False}), - "perspective": ("FLOAT", {"default": 0.0, "min": -0.5, "max": 0.5, "step": 0.01}), - "focal_length": ("FLOAT", {"default": 800.0, "min": 100.0, "max": 5000.0, "step": 50.0}), }, } @@ -40,11 +38,11 @@ class OpenPoseRotator: mode: str, pose_keypoint: list | None = None, debug: bool = False, - perspective: float = 0.0, - focal_length: float = 800.0, ) -> tuple[torch.Tensor, list]: """ Rotate OpenPose figure(s) around torso. Processes batch of images. + Advanced mode uses a fixed 35° camera elevation and ~90% figure fill to + automatically derive perspective and focal-length — no manual tuning needed. Returns (image_tensor, rotated_pose_keypoints_list). """ batch_size = image.shape[0] @@ -72,8 +70,6 @@ class OpenPoseRotator: image_index=i, debug=debug, mode=mode, - perspective=perspective, - focal_length=focal_length, ) if not success: diff --git a/pose_utils.py b/pose_utils.py index 5d07208..3f98836 100644 --- a/pose_utils.py +++ b/pose_utils.py @@ -405,11 +405,17 @@ def _compute_adaptive_depth_scale( return 0.4 * (120.0 / shoulder_width) -# Perspective scale_y: (cy - y) is in pixels; this scales the term so perspective has visible effect. -# At 0.3, perspective=0.5 with a point 200px above pivot contributes 30 z-units, shifting x by -# ~5px at 10° and ~21px at 45° — clearly perceptible without being overwhelming. +# Perspective scale_y: bridges the raw (cy - y) pixel value to z-units. +# In advanced mode the coefficient is derived geometrically from ADVANCED_CAMERA_ELEVATION_DEG; +# this constant keeps the formula consistent: z += perspective * (cy - y) * PERSPECTIVE_SCALE_Y. PERSPECTIVE_SCALE_Y = 0.3 +# Advanced mode: fixed physical camera / figure assumptions. +# Camera is elevated 35° above horizontal, looking at a standing figure that fills +# roughly 80-100% (average ~90%) of the image height. +ADVANCED_CAMERA_ELEVATION_DEG = 35.0 +ADVANCED_FIGURE_FILL = 0.90 + # Ankle y-parity threshold: fraction of figure height (neck→lower ankle) within which both ankles # are considered to be at the same elevation (standing upright on flat ground). UPRIGHT_ANKLE_THRESHOLD = 0.08 @@ -455,6 +461,28 @@ def _detect_upright_figure( return True, neck, ankle_mid +def _compute_advanced_params(image_height: int) -> tuple[float, float]: + """ + Derive perspective and focal_length for advanced mode from fixed camera/figure assumptions. + + Camera elevation θ = ADVANCED_CAMERA_ELEVATION_DEG (35°) above horizontal. + Figure fills ADVANCED_FIGURE_FILL (~90%) of image height. + + perspective: maps vertical pixel distance from pivot to depth. + Formula: z += perspective * (cy - y) * PERSPECTIVE_SCALE_Y + For geometric accuracy: perspective * PERSPECTIVE_SCALE_Y = tan(θ) + → perspective = tan(35°) / 0.3 ≈ 2.33 + + focal_length: perspective projection strength, scaled to image resolution. + From pinhole geometry: focal_length = image_height * fill / tan(θ) + At 35° with 90% fill → focal_length ≈ 1.29 × image_height + """ + tan_elev = math.tan(math.radians(ADVANCED_CAMERA_ELEVATION_DEG)) + perspective = tan_elev / PERSPECTIVE_SCALE_Y + focal_length = image_height * ADVANCED_FIGURE_FILL / tan_elev + return perspective, focal_length + + def rotate_keypoints_3d( keypoints: dict[str, list[tuple[float, float, float]]], pivot: tuple[float, float], @@ -834,19 +862,27 @@ def rotate_openpose( image_index: int = 0, debug: bool = False, mode: str = "simple", - perspective: float = 0.0, - focal_length: float = 800.0, ) -> tuple[np.ndarray, bool, dict[str, list[tuple[float, float, float]]] | None]: """ Main pipeline: extract/parse keypoints, detect torso, rotate, render. Returns (output_image, success, rotated_keypoints). On failure, returns (input_image, False, None). rotated_keypoints is internal format: {"body": [(x,y,c),...], "hand_left": [...], ...} image_index: used for console logging when processing batches. - mode: "simple" or "advanced". perspective: 0=eye level, >0=camera above, <0=camera below. - focal_length: (advanced only) perspective strength; higher=flatter. + + mode: "simple" uses orthographic projection at eye level (perspective=0). + "advanced" uses perspective projection with camera parameters derived from + ADVANCED_CAMERA_ELEVATION_DEG (35°) and ADVANCED_FIGURE_FILL (90%), which + gives geometrically correct depth inference and focal length for the assumed pose. """ h, w = image.shape[:2] + # Derive camera parameters from mode and fixed assumptions + if mode == "advanced": + perspective, focal_length = _compute_advanced_params(h) + else: + perspective = 0.0 + focal_length = 800.0 # unused in simple (orthographic) mode + # Get keypoints if pose_keypoint is not None: keypoints = parse_pose_keypoint(pose_keypoint)