s6.app.utils.rim_solve

Pose from a single projected circle on a gimbal-mounted plane (no roll). - Known: intrinsics K, image size, pivot distance (base_Z), stick length l, circle radius R. - Unknown: tilt (phi, theta). Estimate from ellipse fitted to observed ring edges. Theta is the direction at which the assembly tilts.

Solution A:
  1. Fit ellipse once (direct LS) -> conic C_obs

  2. Closed-form-ish init:

    phi0 ≈ arccos(b/a) theta0 ≈ alpha ± 90° (choose better)

  3. Tiny Gauss–Newton refinement (3–4 steps) minimizing algebraic conic residuals of projected circle points against C_obs using the exact gimbal model.

Author: you + your chaotic AI bestie ✨

s6.app.utils.rim_solve.intrinsics_from_hfov(width_px: int, height_px: int, hfov_deg: float) ndarray

Build pinhole intrinsics from horizontal FOV (degrees) and resolution. Square pixels, principal point at center.

s6.app.utils.rim_solve.rot_no_roll(phi_deg: float, theta_deg: float) tuple[ndarray, ndarray]

Rotation that tilts +Z by phi toward azimuth theta WITHOUT roll about the new normal. Returns (R, d) where d = R*[0,0,1] (the new normal).

s6.app.utils.rim_solve.project_circle_points(K: ndarray, base_Z_mm: float, stick_l_mm: float, radius_mm: float, phi_deg: float, theta_deg: float, num_samples: int = 64) ndarray

Project points from the true 3D circle on the tilted/translated plane. Returns (N,2) pixel coords.

s6.app.utils.rim_solve.fit_ellipse_conic(pts: ndarray) ndarray

Direct least-squares fit (Fitzgibbon-style) to conic: Ax^2 + Bxy + Cy^2 + Dx + Ey + F = 0 -> symmetric 3x3 matrix C.

s6.app.utils.rim_solve.ellipse_params_from_conic(C: ndarray) tuple[float, float, float, float, float]

Convert 3x3 conic to (x0, y0, a_axis, b_axis, angle), with a >= b and angle the ellipse rotation (radians).

s6.app.utils.rim_solve.conic_algebraic_residuals(C: ndarray, pts2: ndarray) ndarray

Algebraic residual r = x^T C x for homogeneous x = [u, v, 1]^T.

s6.app.utils.rim_solve.loss_from_pose(C_obs: ndarray, K: ndarray, base_Z_mm: float, stick_l_mm: float, radius_mm: float, phi_deg: float, theta_deg: float, num_samples: int = 16) float
s6.app.utils.rim_solve.refine_pose_gauss_newton(C_obs: ndarray, K: ndarray, base_Z_mm: float, stick_l_mm: float, radius_mm: float, phi_deg_init: float, theta_deg_init: float, iters: int = 4, eps: float = 1e-06) tuple[float, float]

Super tiny Gauss–Newton on (phi, theta) using finite differences on the algebraic conic residual loss. Few iterations are enough from our init.

s6.app.utils.rim_solve.self_test()
s6.app.utils.rim_solve.main()