Self-contained before/after camera position visualization (#95, #43) - #154
Self-contained before/after camera position visualization (#95, #43)#154bpurinton wants to merge 10 commits into
Conversation
) Add ReadBundleAdjustCameras + PlotBundleAdjustCameras to bundle_adjust.py, reading a bundle_adjust output folder directly (.adjust, .adjusted_state.json, optional camera_offsets.txt) with no need for the pre-BA original cameras. Three views (per #95/#43): position-change map quiver (vertical as RdBu color), per-camera center-displacement bars, and orientation-change quiver. New bundle_adjust_cameras CLI + tests + camera_offsets fixture.
A bare CLI call previously rendered the figure but never wrote or displayed it (save happens only when save_dir is set, and there is no plt.show()). Default save_dir to the bundle_adjust directory so running the command directly always saves a figure and prints its path.
A malformed or truncated .adjust (or corrupt state JSON) previously crashed the whole run with a cryptic unpack error. Treat it like a missing .adjust: warn and skip that one camera, so a single bad file cannot sink the figure. Add a test.
The tool is self-contained on the bundle_adjust folder, so the root-directory + subdirectory split (inherited from ReadBundleAdjustFiles) was needless. Accept one --directory pointing at the BA folder and split it internally for the reader.
Drive camera discovery off the .adjust files rather than *.adjusted_state.json. When a camera has no state file (DigitalGlobe bundle_adjust output), read its absolute pre-adjustment center from the original camera .xml ephemeris (<EPHEMLIST> mean), located in the BA dir + parent or via --original_cameras_directory. CSM/jitter runs still use the state file. Add DG tests (center-from-xml and missing-xml-skips); all 399 pass.
The stored ephemeris (CSM m_positions / DG <EPHEMLIST>) can be padded beyond the image acquisition window, so the mean is a poor 'where the camera imaged the scene' anchor. Interpolate the trajectory at the center image line instead: CSM via getTimeAtLine; DG via FIRSTLINETIME + (NUMROWS/2)/AVGLINERATE. Falls back to the mean if timing is unavailable. CSM and DG placements now agree to <1 m on the same scenes (was ~30 m).
…ging Replace the brittle _normalize_camera_id (which stripped a hard-coded 'run-' prefix + '_corr' suffix -- wrong whenever the ASP -o prefix differs, e.g. ba_mvs_csm-) with positional association: zip camera_offsets.txt with camera_list.txt (both per-input-image, same order) and key by camera basename. No filename-string assumptions. _camera_label now strips only deterministic file extensions for display. Add camera_list.txt fixture; update tests.
…cartoons The map-view position quiver conflated true map distance (km) with exaggerated sub-meter shift arrows, and the orientation quiver drew degree-valued arrows against a camera-index x-axis (direction meaningless). Replace both: - Position: keep only the per-camera horizontal/vertical center-change bars (they carry the same magnitudes); label by camera # in the summary. - Orientation: a per-camera satellite/sensor-frustum cartoon with fixed-length roll/pitch/yaw arrows labeled with the actual degrees changed, so tiny (~1e-4 deg) noise isn't visually magnified. summary_plot stacks bars over the grid.
Replace the three free-floating rotation arrows with an explicit body-frame triad (roll about along-track, pitch about across-track, yaw about nadir, sensor cone along nadir), each axis color-coded (roll=red, pitch=green, yaw=blue) with a small rotation arc and the degree value printed by its axis. Makes the angle changes spatially meaningful. First cartoon spells out roll phi / pitch theta / yaw psi.
…around them Per Ben: keep the liked satellite cartoon (body + solar panels + nadir frustum) and overlay the black body-frame X/Y/Z axis bars (X=along-track, Y=across-track, Z=nadir/boresight). A colored rotation arc encircles each axis to show the sense of roll (about X), pitch (about Y), yaw (about Z); the degrees are printed below. Replaces the all-colored triad from the previous commit.
Bundle adjustment vs. jitter correction (context for this PR)Short answer: your intuition is exactly right. Bundle adjustment applies one rigid position + orientation correction to the whole camera; jitter correction adjusts the camera's position and orientation line-by-line along the trajectory. ASP's own docs put it almost word-for-word the way you did — from
Why a pushbroom camera can be corrected with "one shift"The thing that confused you is the key insight: a linescan/pushbroom camera model already contains the full flight trajectory — a time-sampled sequence of positions (ephemeris) and orientations (attitude quaternions). In the CSM state file these are the
The
(That single What jitter actually is"Jitter" is the high-frequency part BA leaves behind. From
So during the ~1–10 s it takes to scan the scene, micro-vibrations (reaction wheels, thermal snap, structural resonance) make the pointing oscillate faster than the smooth, sparsely-sampled ephemeris/attitude model captures. Over a pushbroom scan those attitude wiggles imprint a wavy, oscillatory distortion into the image and the resulting DEM (the "washboard"/banding artifacts you see in the ASTER and CTX jitter examples). A single rigid ASP even says this directly in the bundle-adjustment docs, next to a post-BA intersection-error figure: "The remaining wavy pattern is due to jitter, that ASP has a solver for." BA does its job (removes the bulk error), and whatever wavy residual is left over is, by definition, the jitter. What
|
| bundle_adjust | jitter_solve | |
|---|---|---|
| Free parameters per camera | 1 position + 1 orientation (rigid R,T) |
a sequence of positions + orientations (per pose/line) |
| Error it fixes | bulk / low-frequency (absolute position & pointing) | high-frequency along-track oscillation (jitter) |
| Output | .adjust (T + quaternion) |
full .adjusted_state.json CSM state |
| Typical order | first | after BA, heavily constrained |
Relevance to this PR
This is actually why the two viz layers in asp_plot look different:
bundle_adjust_cameras(this PR) correctly models each camera's change as one center shift + one roll/pitch/yaw change — a single arrow / single body-axis cartoon per camera — because that's all a BA.adjustis. On a position-dominated BA run the orientation change is ~1e-4°, which is why the cartoon prints the number instead of scaling an arrow.csm_camera.py/csm_camera_summary_plotis the right tool for jitter: it plots the position and roll/pitch/yaw differences as a function of image line along the trajectory, so you can actually see the oscillation thatjitter_solveremoved. So the two tools are complementary: per-camera rigid summary (BA) vs. along-track per-pose profile (jitter).
Sources: ASP docs bundle_adjustment, bundle_adjust (§ Format of .adjust files), jitter_solve (overview, § Camera constraints, § Anchor points, § Resampling the poses).
|
Added the nice writeup above in #154 (comment) as a new docs section in the stereopipeline-quickstart: |


What & why
First cut at issue #95 ("visualize bundle adjustment results"), focused on the part @ we discussed: seeing where a camera moved (before → after), not just the residuals on the ground. Also delivers the quiver ideas from #43.
Key design choice: this is self-contained on a
bundle_adjustoutput folder — it does not require the user to supply the pre-BA original camera files (which aren't co-located in a BA folder). Contrast with the existingcsm_camera_plot, which needs original + optimized camera pairs.How it works
New
ReadBundleAdjustCameras+PlotBundleAdjustCamerasinbundle_adjust.pyread three products ASP writes into the BA folder:*.adjust— the rigid adjustment: ECEF translationT+ rotation quaternion.*.adjusted_state.json— the optimized CSM state, used to locate each camera center.*camera_offsets.txt(optional) — ASP's authoritative horizontal/vertical camera-center change in local NED.Per ASP's documented
.adjustconvention (a world point projects the same in the original camera asR*(P−C)+C+Tin the adjusted, withC= camera center for pixel (0,0)), the translationTis the exact bulk camera-center shift at the anchor pixel. I verified against the example data thatT's vertical component matchescamera_offsets.txtclosely, while horizontal differs by the rotation lever-arm — so the tool usescamera_offsets.txtfor authoritative magnitudes (bars) andTfor the shift direction (quiver). Falls back toTfor magnitudes whencamera_offsets.txtis absent (flagged byoffsets_from_asp).Three views (#95 + #43)
summary_plot()combines:Also a
bundle_adjust_camerasCLI (points at a BA folder; no original cameras needed).Validation
.adjustreconstruction againstcamera_offsets.txtground truth..adjustnaming conventions (<base>.adjustand<base>.adjusted_state.adjust)..adjustparsing, id normalization, the three plot methods, error path) +camera_offsets.txtfixture. Full suite: 396 passed.Notes / open questions for review
report_pipeline.py) as a follow-up, and/or add the ellipsoid-relative / NED-vector refinements from Ideas for 3D quiver plots for camera optimization #43.Closes part of #95; addresses #43.