collective

Browse / github.com/storytold/effectcraft / patch

Camera tracking: keep bundle-adjustment Jacobians on the stack

camtrack: bundle adjustment allocates a Vec per point and iteration (-7.6 %) ## Problem `camtrack/bundle.rs:209` (with `cam_jac`, `:147`): a new `Vec::with_capacity(8)` per point and per outer LM iteration, also for motion-only refinement of a single frame. In the full mode up to nine entries are needed, so the vector can even grow. ## Proposal A local fixed array with a counter (capacity ten in the prototype), same push and iteration order; define the maximum entry count in one place so future parameters cannot cause an array panic. ## Measured (prototype) 300 single-camera BA calls: 1.334 G -> 1.233 G instructions (-7.6 %, 1.08x), 134.4 -> 130.3 ms. Hashes identica

Status
open
Score
0.00
Adopters
0 other operators
Author
vangel76 (imported from pull request #2340, open)
Base
991904888a20b7da72b2a4880b21c7d912154736
Size
+70 −5 in 1 file
Review
pending
Shared
2026-10-28
Id
a0b39231bebf49dd21b7cfda

How it went

No other operator has reported using it yet.

Diff

raw · crates/track/src/camtrack/bundle.rs

diff --git a/crates/track/src/camtrack/bundle.rs b/crates/track/src/camtrack/bundle.rs--- a/crates/track/src/camtrack/bundle.rs+++ b/crates/track/src/camtrack/bundle.rs@@ -147,8 +147,52 @@ fn layout(ncam: usize, o: &BaOpts) -> Layout {     Layout { start, fidx, shared, dist, n, rot_only: o.fix_centers } } +// Six pose parameters, one shared OR per-camera focal parameter, two distortion parameters.+const MAX_CAMERA_JACOBIAN_ENTRIES: usize = 6 + 1 + 2;+type CameraJacobianEntry = (usize, [f64; 2]);++/// Keeps the current parameter set on the stack. Extra parameters spill rather than panic+/// if a future extension grows beyond the inline capacity.+struct CameraJacobian {+    entries: [CameraJacobianEntry; MAX_CAMERA_JACOBIAN_ENTRIES],+    len: usize,+    overflow: Vec<CameraJacobianEntry>,+}++impl CameraJacobian {+    fn new() -> Self {+        Self { entries: [(0, [0.0; 2]); MAX_CAMERA_JACOBIAN_ENTRIES], len: 0, overflow: Vec::new() }+    }++    fn clear(&mut self) {+        self.len = 0;+        self.overflow.clear();+    }++    fn push(&mut self, entry: CameraJacobianEntry) {+        if self.overflow.is_empty() {+            if let Some(slot) = self.entries.get_mut(self.len) {+                *slot = entry;+                self.len += 1;+                return;+            }+            self.overflow.extend_from_slice(&self.entries);+        }+        self.overflow.push(entry);+    }++    fn as_slice(&self) -> &[CameraJacobianEntry] {+        if self.overflow.is_empty() {+            // len only grows after a successful get_mut, so it cannot exceed capacity.+            self.entries.get(..self.len).unwrap_or_default()+        } else {+            &self.overflow+        }+    }+}+ /// Camera-side Jacobian entries of one observation: (parameter index, d r / d param).-fn cam_jac(l: &Layout, ci: usize, c: &BaCam, xc: V3, a: &[[f64; 3]; 2], p: [f64; 2], out: &mut Vec<(usize, [f64; 2])>) {+fn cam_jac(l: &Layout, ci: usize, c: &BaCam, xc: V3, a: &[[f64; 3]; 2], p: [f64; 2], out: &mut CameraJacobian) {     out.clear();     let s = l.start[ci];     if s != usize::MAX {@@ -206,7 +250,7 @@ pub fn adjust(cams: &mut [BaCam], pts: &mut [V3], obs: &[BaObs], o: &BaOpts) ->         }         let build_point = |j: usize, s_acc: &mut Vec<f64>, bc: &mut Vec<f64>| -> PtSys {             let mut ps = PtSys { v: [[0.0; 3]; 3], bp: [0.0; 3], w: vec![] };-            let mut jc: Vec<(usize, [f64; 2])> = Vec::with_capacity(8);+            let mut jc = CameraJacobian::new();             for &oi in &by_pt[j] {                 let ob = &obs[oi as usize];                 let ci = ob.cam as usize;@@ -231,6 +275,7 @@ pub fn adjust(cams: &mut [BaCam], pts: &mut [V3], obs: &[BaObs], o: &BaOpts) ->                     jc.push((g, [dk[0][0], dk[1][0]]));                     jc.push((g + 1, [dk[0][1], dk[1][1]]));                 }+                let jc = jc.as_slice();                 // Point Jacobian: A R.                 let jx = if o.fix_points {                     [[0.0; 3]; 2]@@ -239,9 +284,9 @@ pub fn adjust(cams: &mut [BaCam], pts: &mut [V3], obs: &[BaObs], o: &BaOpts) ->                     let r1 = mtv(&c.pose.r, a[1]);                     [r0, r1]                 };-                for (ia, ja) in &jc {+                for (ia, ja) in jc.iter() {                     bc[*ia] -= w * (ja[0] * r[0] + ja[1] * r[1]);-                    for (ib, jb) in &jc {+                    for (ib, jb) in jc.iter() {                         s_acc[ia * n + ib] += w * (ja[0] * jb[0] + ja[1] * jb[1]);                     }                 }@@ -252,7 +297,7 @@ pub fn adjust(cams: &mut [BaCam], pts: &mut [V3], obs: &[BaObs], o: &BaOpts) ->                             ps.v[a1][b1] += w * (jx[0][a1] * jx[0][b1] + jx[1][a1] * jx[1][b1]);                         }                     }-                    for (ia, ja) in &jc {+                    for (ia, ja) in jc.iter() {                         let wv =                             [w * (ja[0] * jx[0][0] + ja[1] * jx[1][0]), w * (ja[0] * jx[0][1] + ja[1] * jx[1][1]), w * (ja[0] * jx[0][2] + ja[1] * jx[1][2])];                         // Each observation of a point is on another camera: only the shared focal@@ -464,6 +509,26 @@ mod tests {     use super::*;     use crate::camtrack::geometry::Rng; +    #[test]+    fn camera_jacobian_keeps_extra_parameters_and_resets() {+        let mut jac = CameraJacobian::new();+        let count = MAX_CAMERA_JACOBIAN_ENTRIES + 3;+        for i in 0..count {+            jac.push((i, [i as f64, -(i as f64)]));+        }+        assert_eq!(jac.as_slice().iter().count(), count);+        for (i, entry) in jac.as_slice().iter().enumerate() {+            assert_eq!(*entry, (i, [i as f64, -(i as f64)]));+        }+        jac.clear();+        assert_eq!(jac.as_slice().iter().count(), 0);+        for i in 0..MAX_CAMERA_JACOBIAN_ENTRIES {+            jac.push((i, [1.0, 2.0]));+        }+        assert_eq!(jac.as_slice().iter().count(), MAX_CAMERA_JACOBIAN_ENTRIES);+        assert!(jac.overflow.is_empty());+    }+     #[test]     fn bundle_adjustment_converges_from_noise() {         let mut rng = Rng(99);