pose3d¶
3D pose from the 2D: the [pose3d], [smoothing] and [correction] stages.
triangulation and
pictorial implement the [pose3d] methods,
smoothing is the ensemble Kalman smoother, and
correction holds the anatomical corrections.
Robust multi-view triangulation over a rig: the inverse of Rig.project.
A NaN 2D observation means the view did not see the point, so visibility travels as
NaNs, not a mask. pts2d is view-leading, (V, *pts, 2), and the points come back
as (*pts, 3). triangulate_ransac
runs its hypothesis search inside one jitted jax.lax.scan; the GT-honoring solve at
the end is host-side numpy on one point at a time (ADR-0068).
consensus_support ¶
consensus_support(
inliers: Bool[ndarray, V * pts],
conf: Float[ndarray, V * pts] | None = None,
) -> Float[np.ndarray, "*pts"]
How much a consensus rests on: the summed vote of the views it kept.
Each kept view counts its precision_from_conf, the vote of
triangulate_ransac's weighted_vote (ADR-0129), so six sure views give 6 and
two half-sure ones 0.5; without conf every kept view counts 1. Below
LOW_SUPPORT the point is one to distrust (ADR-0132).
Source code in src/deeperfly/pose3d/triangulation.py
depth_on_ray ¶
depth_on_ray(
rig: Rig,
gt_view: int,
gt_xy,
stab_obs: Float[ndarray, "V 2"],
*,
huber_px: float = _STAB_SIGMA_PX,
max_iter: int = 12,
) -> Float[np.ndarray, 3] | None
The point on the single GT view's ray whose depth the stabilizers agree on.
Only the depth is refit, by Huber IRLS at huber_px over max_iter steps, so the
point reprojects onto the GT pixel (ADR-0068). stab_obs is (V, 2), NaN where
none; None when no stabilizer fixes a depth.
Source code in src/deeperfly/pose3d/triangulation.py
placed_in_depth ¶
placed_in_depth(
rig: Rig,
pts3d: Float[ndarray, "*pts 3"],
used: Bool[ndarray, V * pts],
) -> Bool[np.ndarray, "*pts"]
Whether some two of each point's used views see it MIN_PARALLAX_DEG apart.
And as far from opposite: two views looking along one line fix a point across it but not along it, so a point resting on such views alone has an arbitrary depth (ADR-0119, ADR-0130). False where the point is NaN.
Source code in src/deeperfly/pose3d/triangulation.py
precision_from_conf ¶
precision_from_conf(
conf: Float[ndarray, V * pts],
*,
exponent: float = CONF_PRECISION_EXPONENT,
) -> Float[np.ndarray, V * pts]
Detector confidence as a triangulation weight, conf ** exponent (ADR-0067).
A non-finite confidence becomes 0, which drops the view.
Source code in src/deeperfly/pose3d/triangulation.py
reprojection_error ¶
reprojection_error(
rig: Rig,
pts3d: Float[ndarray, "*pts 3"],
pts2d: Float[ndarray, "V *pts 2"],
) -> Float[np.ndarray, V * pts]
Per-(view, point) reprojection error in pixels, NaN where either side is NaN.
Source code in src/deeperfly/pose3d/triangulation.py
triangulate ¶
triangulate(
rig: Rig,
pts2d: Float[ndarray, "V *pts 2"],
weights: Float[ndarray, V * pts] | None = None,
*,
method: str = "fast",
) -> Float[np.ndarray, "*pts 3"]
Triangulate 3D points from 2D observations by NaN-aware DLT.
Fewer than two views give NaN; triangulate_ransac rejects gross outliers.
weights scale each view's rows by sqrt(weight). method="fast" solves by
inverse iteration and re-solves with eigh the rare points too ill-conditioned for
it, matching "eigh" everywhere the triangulation is well posed (ADR-0043).
Source code in src/deeperfly/pose3d/triangulation.py
triangulate_ransac ¶
triangulate_ransac(
rig: Rig,
pts2d: Float[ndarray, "V *pts 2"],
*,
threshold: float = 15.0,
min_views: int = 2,
weights: Float[ndarray, V * pts] | None = None,
method: str = "fast",
weighted_vote: bool = False,
view_precision: Float[ndarray, " V"] | None = None,
) -> tuple[
Float[np.ndarray, "*pts 3"], Bool[np.ndarray, V * pts]
]
Robustly triangulate 3D points, rejecting gross 2D outliers (RANSAC).
A rig has a handful of views, so every C(V, 2) two-view hypothesis is tried (the
deterministic limit of RANSAC). Each candidate counts the views reprojecting
within threshold px; the largest consensus wins, ties to the smaller inlier
error, and the point is refit from its inliers. Fewer than min_views agreeing
views give NaN. weights enter the candidate fits and the refit (ADR-0067); with
weighted_vote they are the vote too, the consensus being the hypothesis whose
agreeing views weigh the most rather than the one with the most views (ADR-0129).
view_precision, (V,), scales each view's weight in the fits but not in the vote
(view_precision). A pair of views
within MIN_PARALLAX_DEG of parallel or opposite at its point forms no hypothesis,
since it leaves the depth arbitrary (ADR-0130). A consensus can still end on such
views alone, when the hypothesis' other view falls outside the gate, and its refit
then has an arbitrary depth: a caller that needs the depth checks
placed_in_depth on the inliers, as [pose3d] does (ADR-0135). method is the
DLT solver's, as for triangulate.
Returns:
| Type | Description |
|---|---|
Float[ndarray, '*pts 3']
|
A tuple |
Bool[ndarray, V * pts]
|
|
Source code in src/deeperfly/pose3d/triangulation.py
triangulate_stabilized ¶
triangulate_stabilized(
rig: Rig,
x0: Float[ndarray, 3],
gt_obs: Float[ndarray, "V 2"],
stab_obs: Float[ndarray, "V 2"],
*,
gt_sigma_px: float = _GT_SIGMA_PX,
huber_px: float = _STAB_SIGMA_PX,
max_iter: int = 12,
) -> Float[np.ndarray, 3]
Two or more GT views, with the stabilizer supplying only what GT cannot see.
Gauss-Newton, for a fixed max_iter steps, on
sum_{GT} |proj(x) - g|^2 / sigma_gt^2
+ sum_{stab} rho_c(|proj(x) - s|) / huber_px^2
with rho_c Huber at huber_px (ADR-0068). x0 is the GT-only solution, the
start and the anchor, returned unchanged when no stabilizer is usable; gt_obs and
stab_obs are (V, 2) raw pixels, NaN where none.
Source code in src/deeperfly/pose3d/triangulation.py
view_centers ¶
Each view's center in world coordinates, in the rig's order.
view_precision ¶
view_precision(
rig: Rig,
pts2d: Float[ndarray, "V *pts 2"],
weights: Float[ndarray, V * pts] | None = None,
*,
threshold: float = 15.0,
max_points: int = 20000,
) -> Float[np.ndarray, " V"]
Each view's precision relative to the steadiest view, (V,) in (0, 1].
(sigma_min / sigma_v) ** 2, with sigma_v the median residual of view v's
inliers to the RANSAC consensus of pts2d (at most max_points points, spread
evenly, weights as for triangulate_ransac). The noise level is measured, so a
rig whose views differ in quality has its steadier views pull harder without
naming them (ADR-0129). A view with no inlier takes the lowest precision measured;
with nothing measured every view is 1.
Source code in src/deeperfly/pose3d/triangulation.py
Pictorial structures: one 3D per keypoint, chosen among the candidate peaks.
When a view's strongest peak is the wrong one (a crossed leg, a left/right swap), the
right answer is often its second or third, and a selection by multi-view consistency
recovers what a RANSAC veto cannot (Gunel et al., DeepFly3D, 2019; ADR-0038). solve
triangulates every candidate pair of every view pair (700 hypotheses at eight views and
five candidates, 11.4 ms of a frame's 25.8), scores each by its views' Gaussian-weighted
support, prunes and refits them, and picks one per keypoint by exact max-product over
the skeleton's forest with a Huber bone-length prior. elect is the cheaper
leave-one-out rule. A veto keeps a weakly calibrated view out of the geometry while
its 2D is still re-selected.
PictorialResult
dataclass
¶
What the selection committed to, per frame.
Attributes:
| Name | Type | Description |
|---|---|---|
points |
Float[ndarray, 'T K 3']
|
The chosen 3D, |
points2d |
Float[ndarray, 'V T K 2']
|
Per view the candidate that supported it, |
conf2d |
Float[ndarray, 'V T K']
|
That candidate's score, |
inliers |
Bool[ndarray, 'V T K']
|
Which views supported the chosen hypothesis, |
Source code in src/deeperfly/pose3d/pictorial.py
bone_targets ¶
bone_targets(
rig: Rig,
pts2d: Float[ndarray, "V T K 2"],
edges: Int[ndarray, "E 2"],
*,
frames: int = 200,
threshold_px: float = 15.0,
min_views: int = 2,
) -> tuple[Float[np.ndarray, " E"], float]
The prior: each edge's median 3D length over pts2d's frames, and the scale.
Up to frames frames of the decoded 2D, triangulated robustly (threshold_px,
min_views); NaN for an edge never triangulated. The scale is the median of the
medians, the Huber residual's and the suppression radius's unit, or a tenth of the
pose's median extent with no edge measured. Returns (targets, scale).
Source code in src/deeperfly/pose3d/pictorial.py
edge_forest ¶
Root the skeleton's edges: each keypoint's parent (-1 at a root) and an order.
Each component is rooted at its lowest index and walked breadth first, parents before children, which keeps the max-product pass exact.
Raises:
| Type | Description |
|---|---|
ValueError
|
If the edges close a cycle; the exact pass needs a forest. |
Source code in src/deeperfly/pose3d/pictorial.py
elect ¶
elect(
rig: Rig,
cand_xy: Float[ndarray, "V T K C 2"],
cand_score: Float[ndarray, "V T K C"],
*,
fallback_xy: Float[ndarray, "V T K 2"],
fallback_conf: Float[ndarray, "V T K"],
threshold_px: float = 15.0,
min_views: int = 2,
reach: float = ELECTION_REACH,
veto=None,
) -> PictorialResult
Re-elect each view's candidate by the other views' decode: leave-one-out.
For view v the other voting views' decode is triangulated (RANSAC at
threshold_px, min_views inliers) and reprojected into v, and each keypoint
takes its nearest candidate within reach * threshold_px, a bound on the
geometry's error, not on the move (the swaps worth recovering sit 100 to 300 px
from the decode). An incumbent that is nearest stays, a cell the others cannot
place keeps its decode, and the result is triangulated once more. No hypothesis
pool, so flat in the candidate count, but the higher-variance estimator: on the old
repo's held-out animals it held 14 to 69% of the available gain where solve
held 53 to 69%, so it is chosen for cost, never the default. A vetoed view is still
re-elected.
Raises:
| Type | Description |
|---|---|
ValueError
|
On |
Source code in src/deeperfly/pose3d/pictorial.py
solve ¶
solve(
rig: Rig,
cand_xy: Float[ndarray, "V T K C 2"],
cand_score: Float[ndarray, "V T K C"],
edges: Int[ndarray, "E 2"],
*,
fallback_xy: Float[ndarray, "V T K 2"],
fallback_conf: Float[ndarray, "V T K"],
threshold_px: float = 15.0,
min_views: int = 2,
weighted: bool = False,
bone_prior: float = 0.0,
targets: Float[ndarray, " E"] | None = None,
scale: float | None = None,
max_hypotheses: int = DEFAULT_MAX_HYPOTHESES,
veto=None,
) -> PictorialResult
Choose one 3D per keypoint and frame among the candidates' triangulations.
cand_xy (V, T, K, C, 2) and cand_score (V, T, K, C) are the candidates, slot
0 the decode; fallback_xy and fallback_conf are the decoded 2D, what the prior
is fitted from and what an unreached keypoint keeps. A view supports a hypothesis
within threshold_px; a hypothesis needs min_views support; weighted scales a
vote by the candidate's score; bone_prior weighs the Huber bone cost against one
unit of evidence per view, over targets and scale (fitted when omitted); at
most max_hypotheses are kept per keypoint. A veto view (a mask or indices)
seeds, supports and refits nothing but is committed to its nearest candidate.
Raises:
| Type | Description |
|---|---|
ValueError
|
On |
Source code in src/deeperfly/pose3d/pictorial.py
146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 | |
The ensemble Kalman smoother: multi-view 2D in, one smoothed 3D track per keypoint.
The nonlinear, geometric EKS of Lightning Pose 3D (Aharon, Whiteway et al., 2026), reimplemented on project_full_one (ADR-0062). The latent state is a 3D point observed through the rig's projection, and a cross-view variance inflation down-weights the views that disagree.
EksResult
dataclass
¶
What the smoother produced, in deeperfly's array conventions.
Attributes:
| Name | Type | Description |
|---|---|---|
pts3d |
Float[ndarray, 'T P 3']
|
The smoothed 3D; NaN for a keypoint two views never saw at once. |
pts2d |
Float[ndarray, 'V T P 2']
|
|
posterior_var |
Float[ndarray, 'T P 3']
|
The per-axis posterior variance, in world units squared. |
smooth_param |
Float[ndarray, P]
|
The process-noise scale per keypoint; small smooths hard. |
n_inflated |
int
|
Observations the variance inflation down-weighted. |
n_testable |
int
|
Observations the inflation could test at all. |
Source code in src/deeperfly/pose3d/smoothing/smoother.py
ensemble_statistics ¶
ensemble_statistics(
pts2d: Float[ndarray, "M V T P 2"],
conf: Float[ndarray, "M V T P"] | None = None,
*,
avg_mode: str = "median",
var_mode: str = "confidence_weighted_var",
) -> tuple[
Float[np.ndarray, "V T P 2"],
Float[np.ndarray, "V T P 2"],
]
Collapse an ensemble of detectors into a center and a per-axis variance in px^2.
With one member the variance is 1 / confidence, a prior rather than a
measurement (ADR-0062); conf=None treats every observation as equally
confident. avg_mode is "median" (robust to one member blowing up) or
"mean"; var_mode is "var", or "confidence_weighted_var" to divide the
variance by the mean confidence.
Source code in src/deeperfly/pose3d/smoothing/ensemble.py
inflate_variances ¶
inflate_variances(
center: Float[ndarray, "V T P 2"],
var: Float[ndarray, "V T P 2"],
mask: Bool[ndarray, "V T P"],
pts3d: Float[ndarray, "T P 3"],
rig: Rig,
*,
threshold: float = 5.0,
factor: float = 10.0,
n_rounds: int = 8,
) -> tuple[Float[np.ndarray, "V T P 2"], int]
Inflate the variance of each view that disagrees with the cross-view consensus.
pts3d is the linearization point; a cell without a finite one, or with fewer
than two observed views, is not tested. A view over the Mahalanobis threshold
has its variance multiplied by factor, for up to n_rounds rounds (defaults:
ADR-0062). Returns the variances and how many (view, frame, point)
observations were inflated.
Source code in src/deeperfly/pose3d/smoothing/ensemble.py
smooth ¶
smooth(
rig: Rig,
pts2d: Float[ndarray, "V T P 2"]
| Float[ndarray, "M V T P 2"],
conf: Float[ndarray, "V T P"]
| Float[ndarray, "M V T P"]
| None = None,
*,
init3d: Float[ndarray, "T P 3"] | None = None,
smooth_param: float | Float[ndarray, P] | None = None,
avg_mode: str = "median",
var: str = "confidence_weighted_var",
inflate: bool = True,
inflate_threshold: float = 5.0,
inflate_factor: float = 10.0,
fit_frames: int = 2000,
fit_iterations: int = 24,
fill_unobserved: bool = False,
) -> EksResult
Smooth a run's multi-view 2D into one latent 3D trajectory per keypoint.
pts2d is one detector's 2D or an ensemble's, with a leading member axis. The
rig's projection is the observation model, so pass the rig the run uses
(bundle-adjusted when that stage is on). init3d defaults to a DLT triangulation
of the ensemble center. smooth_param=None fits one scale per keypoint on the
first fit_frames frames (0 = all); smaller smooths harder. avg_mode and var
are the modes of ensemble_statistics. fill_unobserved also reports the
reprojection in the cells nothing observed (ADR-0062).
Source code in src/deeperfly/pose3d/smoothing/smoother.py
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 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 | |
Anatomical corrections over a 3D track: hold a point still, mirror a pair.
A tethered fly's thorax-coxa joints and neck do not move in its frame, so their jitter
is noise and one position per track the better estimate; and the fly is
bilaterally symmetric, a left point and its partner reflections through one sagittal
plane. Pure functions over arrays and point indices (ADR-0039); the [correction]
stage alone knows which points a config named.
freeze ¶
freeze(
pts3d: Float[ndarray, "T P 3"],
points: Sequence[int],
*,
method: str = "median",
trim: float = 0.1,
) -> Float[np.ndarray, "T P 3"]
Replace each of points with one position, held over every frame.
Occluded frames are filled in, and a point no frame observed stays NaN; method
is one of STATIC_METHODS, trim the tail fraction of "trimmed_mean". Returns a
copy.
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in src/deeperfly/pose3d/correction.py
sagittal_plane ¶
sagittal_plane(
pts3d: Float[ndarray, "*t P 3"],
pairs: Sequence[tuple[int, int]],
) -> tuple[Float[np.ndarray, " 3"] | None, float | None]
Fit the animal's mirror plane from left/right pairs: (normal, offset).
A pair's midpoint lies on the plane and its difference is normal to it; the normal
is the leading singular vector of the differences (their signs are arbitrary, so a
mean could cancel) and the offset puts the plane through the midpoints' centroid.
(None, None) below two usable pairs, the under-determined case.
Source code in src/deeperfly/pose3d/correction.py
symmetrize ¶
symmetrize(
pts3d: Float[ndarray, "T P 3"],
pairs: Sequence[tuple[int, int]],
midline: Sequence[int] = (),
*,
per_frame: bool = False,
strength: float = 1.0,
) -> tuple[
Float[np.ndarray, "T P 3"],
Float[np.ndarray, " 3"] | None,
float | None,
]
Make pairs mirror-symmetric and put midline on the plane.
Each side moves half-way toward the other's mirror (times strength), which
privileges neither; a midline point moves onto the plane along its normal. The
plane is fitted from the pairs alone, since the midline is about to be moved onto
it. With per_frame=False one plane serves the track, a fixed affine map, so a
point freeze made static stays static. Returns
(pts3d, normal, offset), normal None (and the track unchanged) when no plane
fits.