ds_msp.geometry¶
Shared geometric primitives — resection/PnP, robust pose averaging, and covisibility-graph
utilities, consumed by both single-camera (ds_msp.calib) and multi-camera (ds_msp.rig)
services.
ds_msp.geometry ¶
Neutral geometry primitives — the shared single-/multi-view geometry layer.
One resection/PnP, robust pose averaging, and covisibility-graph utilities, consumed by
both single-camera (calib) and multi-camera (rig) calibration so neither has to
import the other. Pure NumPy + stdlib; depends only on core (and data) — never on
models, detection, IO, or a service layer, and never on OpenCV.
average_rotation ¶
Markley SVD quaternion averaging of rotation matrices (geometrytools.cpp:881).
Source code in ds_msp/geometry/averaging.py
average_transform ¶
Fuse a stack of noisy 4x4 transforms into one (initInterTransform analogue).
Rotation by Markley averaging, translation by component-wise median.
Source code in ds_msp/geometry/averaging.py
average_translation ¶
Component-wise median translation (McCalib.cpp:843 / geometrytools.cpp:697).
bundle_adjust ¶
bundle_adjust(model_cls, params0: ndarray, R0: ndarray, t0: ndarray, X_world_list: Sequence[ndarray], keypoints_list: Sequence[ndarray], visibility_list: Sequence[ndarray], *, kernel: str = 'none', scale: 'float | str' = 1.0, gnc_start: float = 0.0, gnc_iters: int = 0, noise_bound: 'float | None' = None, max_iter: int = 200) -> Tuple[np.ndarray, np.ndarray, np.ndarray, OptResult]
Refine model_cls intrinsics + per-image poses from seeded extrinsics.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model_cls
|
type[CameraModel]
|
The camera-model class (the parameter vector is in |
required |
params0
|
(P,) ndarray
|
Initial intrinsics (clipped into |
required |
R0
|
(n,3,3), (n,3) ndarray
|
Seeded per-image rotation matrices and translations (object->camera). |
required |
t0
|
(n,3,3), (n,3) ndarray
|
Seeded per-image rotation matrices and translations (object->camera). |
required |
kernel
|
robust IRLS kernel name + inlier scale (``"none"`` ⇒ plain L2).
|
|
'none'
|
scale
|
robust IRLS kernel name + inlier scale (``"none"`` ⇒ plain L2).
|
|
'none'
|
gnc_start
|
graduated-non-convexity schedule (0 ⇒ off).
|
|
0.0
|
gnc_iters
|
graduated-non-convexity schedule (0 ⇒ off).
|
|
0.0
|
noise_bound
|
expected per-corner reprojection σ in pixels (e.g. ``~0.3``). When set, run a
|
median-free GNC-TLS solve (:func: |
None
|
Returns:
| Type | Description |
|---|---|
tuple
|
|
Source code in ds_msp/geometry/calibrate_core.py
26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 | |
connected_components ¶
Union-find connected components. Each component is sorted so comp[0] is the
minimum id — MC-Calib's reference choice (min_element).
Source code in ds_msp/geometry/graph.py
covis_weights ¶
pair_counts[(i, j)] = #frames i and j were co-observed -> edge weight 1/N.
Keys are normalized to i < j (undirected). Symmetric duplicates are summed.
Source code in ds_msp/geometry/graph.py
decompose_P ¶
Factor P = K[R|t] into intrinsics K (K[2,2]=1), rotation R (det +1),
and translation t. Returns None if the factorization is degenerate.
Source code in ds_msp/geometry/resection.py
dlt_projection ¶
Normalized DLT estimate of the 3x4 camera matrix P (uv ~ P·[X;1]).
Solves the 2N x 12 homogeneous system A vec(P) = 0 by SVD (smallest right-singular
vector) after Hartley normalization, then de-normalizes. The returned P is only defined
up to scale.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X
|
(N, 3) ndarray
|
World-frame 3D points, genuinely non-coplanar (see :func: |
required |
uv
|
(N, 2) ndarray
|
Corresponding image points (pixels, or normalized coordinates with |
required |
Returns:
| Type | Description |
|---|---|
(3, 4) ndarray
|
Camera matrix |
Notes
Needs at least 6 correspondences for A to be non-degenerate (2N >= 11 free
unknowns), but this is not checked: fewer points still run without error and silently
return a meaningless P. Callers needing that guarantee should go through
:func:ransac_resection, which enforces min_sample before calling this.
Source code in ds_msp/geometry/resection.py
intrinsics_seed ¶
intrinsics_seed(objpts_list: List[ndarray], imgpts_list: List[ndarray], w: int, h: int, *, thresh_px: float = 3.0, seed: int = 0) -> Tuple[np.ndarray, List[Optional[Tuple[np.ndarray, np.ndarray, np.ndarray]]]]
Robust pinhole intrinsic seed K from a genuinely-3D target, no OpenCV.
RANSAC-resects every view, RQ-decomposes each into K_i, R_i, t_i, and returns
the robust median K over the views whose decomposition is plausible plus the
per-view (K_i, R_i, t_i) (None for views that failed). Gross outliers are
rejected inside each view's RANSAC, so the focal seed never sees the blunders.
Source code in ds_msp/geometry/resection.py
mean_transform ¶
Markley rotation averaging but arithmetic-mean translation — the convention
CameraGroupObs::computeObjectsPose uses (CameraGroupObs.cpp:95), distinct from
:func:average_transform's median.
Source code in ds_msp/geometry/averaging.py
ransac_pnp_normalized ¶
ransac_pnp_normalized(X: ndarray, pn: ndarray, *, focal: float = 1.0, thresh_px: float = 3.0, max_iters: int = 300, confidence: float = 0.999, min_sample: int = 6, seed: int = 0) -> Tuple[Optional[np.ndarray], np.ndarray]
RANSAC pose on the normalized plane. thresh_px is interpreted in pixels via
focal (the model focal) so the gate matches the pixel-domain blunders. Returns
(T_cam_obj (4,4) | None, inlier_mask).
Branches on target geometry: a coplanar board uses the IPPE planar solver (the general 3×4 DLT is degenerate for coplanar points — it returns garbage poses, ~1700 px reprojection on a wide-FOV board); a non-coplanar (fused multi-board) object uses the DLT.
Source code in ds_msp/geometry/resection.py
ransac_resection ¶
ransac_resection(X: ndarray, uv: ndarray, *, thresh_px: float = 3.0, max_iters: int = 300, confidence: float = 0.999, min_sample: int = 6, seed: int = 0) -> Tuple[Optional[np.ndarray], np.ndarray]
RANSAC a 3x4 camera matrix robust to gross outliers.
Samples min_sample correspondences, fits a DLT P, scores by reprojection
inliers, and refits P on the full consensus set. Returns (P | None, inlier_mask).
Source code in ds_msp/geometry/resection.py
robust_average_transform ¶
Outlier-robust fusion of noisy relative transforms.
A handful of per-frame relative poses can be grossly wrong (a frame whose object pose
was estimated from an outlier-corrupted view), and plain :func:average_transform
weights every sample equally, so the Markley rotation average is dragged into a wrong
basin — the dominant failure mode of rig-extrinsics init under gross outliers. This
estimator instead selects an inlier consensus: start from the (robust) median
estimate, score each sample by its rotation+translation deviation, keep the samples
within a MAD-based gate, and re-fuse — iterated a few times. Falls back to the plain
average when too few samples survive. The translation scale (metric, from the boards)
sets the gate, so SE(3) — not Sim(3) — is all that is needed.
Source code in ds_msp/geometry/averaging.py
shortest_path ¶
Dijkstra shortest path src -> dst by edge weight. Returns the node list
[src, ..., dst] ([src] if equal; raises if disconnected).