Perform joint intrinsic/extrinsic calibration using position-only constraints. This method uses multiple markers at different locations to simultaneously solve for camera intrinsics and the head-to-camera extrinsic transform. Uses differential evolution (global optimizer)
(
session_dir: Path,
intrinsic_calib: dict,
optimize_intrinsics: bool = True,
max_iterations: int = 100,
use_global_optimizer: bool = True
)
| 778 | |
| 779 | |
| 780 | def calibrate_joint( |
| 781 | session_dir: Path, |
| 782 | intrinsic_calib: dict, |
| 783 | optimize_intrinsics: bool = True, |
| 784 | max_iterations: int = 100, |
| 785 | use_global_optimizer: bool = True |
| 786 | ) -> dict: |
| 787 | """ |
| 788 | Perform joint intrinsic/extrinsic calibration using position-only constraints. |
| 789 | |
| 790 | This method uses multiple markers at different locations to simultaneously |
| 791 | solve for camera intrinsics and the head-to-camera extrinsic transform. |
| 792 | |
| 793 | Uses differential evolution (global optimizer) to avoid local minima. |
| 794 | |
| 795 | The cost function minimizes: |
| 796 | sum_t sum_i || T_b^v(t) @ T_v^c @ solvePnP(corners_i(t), K, D) - p_b^{m_i} ||² |
| 797 | |
| 798 | Where p_b^{m_i} is the ground truth marker position in world frame. |
| 799 | |
| 800 | Args: |
| 801 | session_dir: Path to session directory |
| 802 | intrinsic_calib: Initial intrinsic calibration dictionary (used for image size, not values) |
| 803 | optimize_intrinsics: Whether to also optimize intrinsics or just extrinsics |
| 804 | max_iterations: Maximum iterations for the optimizer |
| 805 | use_global_optimizer: If True, use differential evolution; else use LM |
| 806 | |
| 807 | Returns: |
| 808 | Dictionary with optimized calibration results |
| 809 | """ |
| 810 | from scipy.optimize import differential_evolution |
| 811 | |
| 812 | extrinsic_dir = session_dir / "extrinsic" |
| 813 | if not extrinsic_dir.exists(): |
| 814 | raise ValueError(f"Extrinsic directory not found: {extrinsic_dir}") |
| 815 | |
| 816 | # Load session info for marker size |
| 817 | session_info = load_session_info(session_dir) |
| 818 | marker_size = session_info.get("marker_size_meters", 0.055) |
| 819 | |
| 820 | # Get image size from intrinsic calib |
| 821 | img_width = intrinsic_calib.get("image_width", 640) |
| 822 | img_height = intrinsic_calib.get("image_height", 480) |
| 823 | |
| 824 | # Marker 3D points in marker frame (centered at origin) |
| 825 | marker_3d_points = np.array([ |
| 826 | [-marker_size / 2, marker_size / 2, 0], |
| 827 | [marker_size / 2, marker_size / 2, 0], |
| 828 | [marker_size / 2, -marker_size / 2, 0], |
| 829 | [-marker_size / 2, -marker_size / 2, 0] |
| 830 | ], dtype=np.float32) |
| 831 | |
| 832 | # Collect observations for both cameras |
| 833 | results = {} |
| 834 | |
| 835 | for side in ["left", "right"]: |
| 836 | print(f"\n{'='*60}") |
| 837 | print(f"JOINT CALIBRATION - {side.upper()} CAMERA") |
no test coverage detected