Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
216 commits
Select commit Hold shift + click to select a range
39c6202
Add try/catch to dat_to_csv, formatting
romleiaj Jan 28, 2026
a2bb96d
Add function to compress jpegs with nvJPEG, and an optional flag to t…
romleiaj Jan 12, 2026
f8e6fa5
Pull in updated PhaseOne image and camera SDK versions that support G…
romleiaj Jan 12, 2026
da0de11
Revert "Pull in updated PhaseOne image and camera SDK versions that s…
romleiaj Jan 26, 2026
ef10596
Fix namespace
romleiaj Jan 28, 2026
fd5f95c
Merge pull request #10 from Kitware/dev/nvjpeg
romleiaj Jan 28, 2026
81e77d3
Build in one-step now
romleiaj Jan 28, 2026
e5fc089
Add more error handling around phase one debayering queues, since the…
romleiaj Jan 26, 2026
346ca4d
Merge pull request #12 from Kitware/dev/process_q
romleiaj Jan 28, 2026
6933ebe
Recreate the to_process file each time, guaranteeing all files will b…
romleiaj Jan 28, 2026
d310544
Add defaults for use_nvjpeg
romleiaj Apr 30, 2026
e5b3af4
Provisioning updates
romleiaj Apr 30, 2026
25e3b94
Add script to seed redis config, update provisioning to use it.
romleiaj May 1, 2026
9f53c69
Update defaults
romleiaj May 4, 2026
2c74ad8
Have safe default
romleiaj May 4, 2026
eae8cd9
Remove local ROS calls in startup
romleiaj May 4, 2026
2675b42
Update redis default bind
romleiaj May 4, 2026
1849c47
Move order of aptitude install so only one cache update is required
romleiaj May 4, 2026
5cdaa69
Move the KAMERA_DIR env variable into the supervisor conf, so only on…
romleiaj May 6, 2026
cd573ed
Create global variables at the supervisord level, consolidating confi…
romleiaj May 7, 2026
76a6cd9
Store image tags in a .env variable, less name duplication
romleiaj May 7, 2026
b2f020d
Eliminate the tmuxinator dependency, just call tmux directly.
romleiaj May 7, 2026
716ed46
Revert KAMERA DIR setting, hard paths are required for supervisor
romleiaj May 7, 2026
8545796
Rollback supervisord changes as well.
romleiaj May 7, 2026
6f5c269
Source env directly before docker startup
romleiaj May 7, 2026
3d11bb1
Call env.sh only once
romleiaj May 7, 2026
15c195f
Remove 'version' tags from compose files to comply
romleiaj May 7, 2026
64d0604
Merge pull request #14 from Kitware/dev/startup_refactor
romleiaj May 12, 2026
c90e31e
Cleanup roskv, remove benedict dependency, collapse RedisEnvoy into o…
romleiaj May 1, 2026
095c1da
Pass correct arg to redis envoy
romleiaj May 1, 2026
cc64e03
Merge pull request #15 from Kitware/dev/roskv_refactor
romleiaj May 12, 2026
ad099ea
Update naming convention for hostnames
romleiaj Jun 10, 2026
0ad7554
Update nayak with new naming convention as well
romleiaj Jun 10, 2026
0b198b6
Move up hosts to top-level cfg
romleiaj Jun 10, 2026
9cd1b7c
Remove cruft, consolidate redis config
romleiaj Jun 10, 2026
a4d1618
makefile, provisioning updates
romleiaj Jun 10, 2026
0790741
Cleanup docker build chain, add concrete build steps to the makefile
romleiaj Jun 12, 2026
261cff7
Begin port to noetic GUI, moving all images to ubuntu 20.04
romleiaj Jun 15, 2026
8d3cdfe
Port all WX py2 code to WX py3
romleiaj Jun 15, 2026
d726165
Turn off docker compose 'base' so build order follows depends_on order
romleiaj Jun 15, 2026
fc98487
No bake isn't listened to, switch to explicit docker compose builds
romleiaj Jun 15, 2026
19b0364
Move GUI supporting packages from py2 -> py3 syntax
romleiaj Jun 15, 2026
3adfb49
Remove some old scripts
romleiaj Jun 15, 2026
7c0d79e
Remove some unused functions in the GUI, make sure casing matches in …
romleiaj Jun 15, 2026
f8bd281
Remove references to files / dirs that no longer exist
romleiaj Jun 15, 2026
2453ee3
Remove old syntax from gui.dockerfile
romleiaj Jun 15, 2026
2faad74
Modernize gui.dockerfile a little more
romleiaj Jun 15, 2026
de52f03
Remove kamera pip install - the gui doesn't need it
romleiaj Jun 15, 2026
48f9ffa
Escape host config queries to support hyphens in hostnames
romleiaj Jun 15, 2026
755c30e
Remove custom tini install, use docker's built-in init stack
romleiaj Jun 15, 2026
1f15a5b
Remove local ros calls
romleiaj Jun 15, 2026
b413ba4
Remove py2 relative imports, use absolute
romleiaj Jun 15, 2026
e15234a
Load defaults based on system name, not from within gui
romleiaj Jun 15, 2026
b1b418d
Add psutil pip package, generate locales so GUI doesn't flop initially
romleiaj Jun 15, 2026
b1496a6
Move AddSpacer to Add for wx4
romleiaj Jun 15, 2026
7e62cbf
Make config conflict resolution a little more verbose / useful
romleiaj Jun 15, 2026
df7df2f
Numerous classic -> phoenix GUI fixes
romleiaj Jun 15, 2026
32aed5b
Move SSD / NAS checks to side thread so GUI doesn't hang, make hosts …
romleiaj Jun 15, 2026
3193015
Hopefully a resolution to our mysterious X Windows errors
romleiaj Jun 15, 2026
7135f29
Revert last commit, ipc:host seems like the actual solution
romleiaj Jun 15, 2026
51d91ae
Render font size after pane is created, to avoid clipping text
romleiaj Jun 15, 2026
eb4019f
More scaling / unsquishing fixes for buttons and panels
romleiaj Jun 15, 2026
5f825ae
Split INS data into 2 columns for vertical space savings
romleiaj Jun 15, 2026
5ef8456
Increase width for camera dropdown, make about dialog open
romleiaj Jun 15, 2026
b69a924
Squeeze vertical block
romleiaj Jun 15, 2026
f3de197
More spacing adjustments.
romleiaj Jun 15, 2026
0624093
Move observer back to its own row, expand camera settings a bit
romleiaj Jun 15, 2026
15fade0
Darker gray for disabled controls, spacing changes
romleiaj Jun 15, 2026
8f16d6b
Make sure disabled values are shown
romleiaj Jun 15, 2026
57ea42e
Remove race condition on detector guage starting red
romleiaj Jun 15, 2026
35b4b8b
Change volume mounts to match
romleiaj Jun 15, 2026
535ca8f
Use repo dir, not ws dir
romleiaj Jun 15, 2026
a54a2f0
Point to correct logo file
romleiaj Jun 15, 2026
8452d2d
Spacing
romleiaj Jun 15, 2026
d1a258d
Change divider
romleiaj Jun 15, 2026
554b18b
Move completely to noetic - remove kinetic shims
romleiaj Jun 15, 2026
71542e5
Merge pull request #16 from Kitware/dev/gui-noetic
romleiaj Jun 15, 2026
a01af88
Add viame to leader build
romleiaj Jun 15, 2026
29ae0af
Remove WS_DIR variable, make REPO_DIR use consistent
romleiaj Jun 15, 2026
43c7d70
Add defaults for debug in provisioning to avoid crashes in certain nodes
romleiaj Jun 15, 2026
60fef39
Remove hyphens from hostnames - does not play nicely with ROS. Exampl…
romleiaj Jun 15, 2026
7687352
Move host filtering to a general helper function, use across all node…
romleiaj Jun 15, 2026
42ebc80
Update netplans for taiga systems
romleiaj Jun 15, 2026
f86c60e
Add core to leader docker build
romleiaj Jun 15, 2026
c1b00a4
Move location of netplan yaml
romleiaj Jun 15, 2026
26cc5dd
Reconfigure start/stop scripts to use center/left/right convention, r…
romleiaj Jun 16, 2026
c655f87
Update startup script to be more general, new naming scheme
romleiaj Jun 16, 2026
dabbaec
README
romleiaj May 26, 2026
e68fad4
Add script to install desktop shortcuts
romleiaj Jun 16, 2026
e4440bc
Add desktop install to leader provision
romleiaj Jun 16, 2026
8ad74c0
Remove unused runscripts, collapse swap dockerd to one script
romleiaj Jun 16, 2026
0cc76f3
Add local push/pull docker config, automatically start registry conta…
romleiaj Jun 16, 2026
5b82c08
Add kamerad to leader
romleiaj Jun 16, 2026
c7b848f
Add a docker restart to ansible
romleiaj Jun 16, 2026
0b31734
Leave docker configuration for configure step
romleiaj Jun 16, 2026
d14b306
Rollback to per-machine netplans, the specificity is needed
romleiaj Jun 16, 2026
bb0bf1a
Add sys2 netplan
romleiaj Jun 16, 2026
1f3d0e1
Remove eth speed max scripts - obe
romleiaj Jun 16, 2026
9e24a9f
Got a little too carried away, need these scripts
romleiaj Jun 16, 2026
2657320
Make scripts executable, strs be f-strs
romleiaj Jun 16, 2026
7a1949e
Pin NFS version, make nas mount dir beforehand
romleiaj Jun 16, 2026
a4ff4a9
Whoops didn't need that folder creation
romleiaj Jun 16, 2026
6d2dfc9
Rollback hosts to a per-machine config, as the NAS requires a distinc…
romleiaj Jun 16, 2026
b55a128
Numerous gui updates to bring it closer to previous behavior
romleiaj Jun 16, 2026
219052f
Let gain breathe a little
romleiaj Jun 16, 2026
6c87471
Update some necessary defaults
romleiaj Jun 16, 2026
242f7fa
Grey out contrast control, as server uses it
romleiaj Jun 16, 2026
bf78ceb
Fix startup spacing
romleiaj Jun 16, 2026
fecc1b5
Shrink observer box, minimize prints
romleiaj Jun 16, 2026
f82db30
Some minor GUI adjustments
romleiaj Jun 16, 2026
e4bde08
Update some defaults, add warning if no pipefile
romleiaj Jun 16, 2026
2958748
Make pipefile loading a little more robust
romleiaj Jun 16, 2026
66d837e
More robust host filtering for detectors
romleiaj Jun 16, 2026
b9dfeaf
Don't try to copy perms to avoid NAS noise in copy
romleiaj Jun 16, 2026
4c3a2bc
We don't use that image manager, change the data migration script
romleiaj Jun 16, 2026
594a3d2
More gui spacing
romleiaj Jun 16, 2026
e3f3cec
Add ansible for system stop / starts
romleiaj Jun 16, 2026
dd72076
Add task to symlink nas to desktop (for convenience)
romleiaj Jun 17, 2026
ea95dc4
Add power management to kamerad, adding supervisor tasks for startup …
romleiaj Jun 17, 2026
c60035a
Cleanup old power scripts, update desktop to point at new endpoints
romleiaj Jun 17, 2026
0921df4
Cut out threading and redis from power management, just make it a sim…
romleiaj Jun 17, 2026
f2e27d2
Add tailscale to system install
romleiaj Jun 17, 2026
310294e
Define clear boundaries for each configuration file - yaml contains s…
romleiaj Jun 17, 2026
4ebe253
Remove all old license strings, replace GUI license with Apache 2.0.
romleiaj Jun 17, 2026
43dd539
Make the icons a little prettier
romleiaj Jun 17, 2026
86eb3f2
Make them a little less scary
romleiaj Jun 17, 2026
8fc389b
Remove seal icon (it was a sea lion :( )
romleiaj Jun 17, 2026
300b47b
Provisioning updates
romleiaj Jun 17, 2026
6e5dc8b
nayak-specific config, no ir or uv cameras
romleiaj Jun 17, 2026
72dc43c
Add default camera config jsons, change default system state to match
romleiaj Jun 17, 2026
34ab586
Add shutter enum mapping for electronic / leaf shutter on phase one c…
romleiaj Jun 17, 2026
09c39ab
More configuration updates, move pipefile to within camera_cfgs,
romleiaj Jun 17, 2026
6209dde
Numerous GUI changes, the biggest being dropdowns for phase one shutt…
romleiaj Jun 17, 2026
2252b97
Display speed instead of exposure in ms
romleiaj Jun 18, 2026
511206d
If any camera parameter changes, light up the 'set' button so its obv…
romleiaj Jun 18, 2026
e529a73
Make kamerad run on startup, and autorestart. Make kamera_run just la…
romleiaj Jun 18, 2026
c2f1833
Delete gui icon
romleiaj Jun 18, 2026
5985234
Resolve 2 bugs in phase one camera driver. The 'total' counter is now…
romleiaj Jun 18, 2026
7c91a5a
Add more color indicators, properly display debayer frames
romleiaj Jun 18, 2026
fc58cbf
The camera config box was hiding some important elements - stretch it…
romleiaj Jun 18, 2026
94f4cc1
Make sure camera configurations are written to disk on save
romleiaj Jun 18, 2026
918ff64
Remove OBE camera models file, pivot to kamera one in gui
romleiaj Jun 18, 2026
58dc9aa
Properly label icons
romleiaj Jun 18, 2026
080f56e
Need that new line
romleiaj Jun 18, 2026
92108bf
Add 'local stratum 3' to nayak sys config, this should have been upda…
romleiaj Jun 29, 2026
aabfbae
Turn those strings to ints, fixing incorrect yellow INS panel
romleiaj Jun 30, 2026
6148239
Port packaging from poetry to uv with PEP 621 metadata
romleiaj Jul 4, 2026
eb052cd
Install the postproc image with uv sync --frozen
romleiaj Jul 4, 2026
d124e04
Let the py3.8 GUI image install past the new requires-python floor
romleiaj Jul 4, 2026
f828885
Add a conda env to support gdal cross-platform
romleiaj Jul 5, 2026
c5f564b
Unify postproc install around conda + make install, in Docker and nat…
romleiaj Jul 6, 2026
cc21dc9
Add pycolmap>=4.0 to the conda env
romleiaj Jul 6, 2026
5a859b0
Replace setup_postproc scripts with documented conda + make install s…
romleiaj Jul 6, 2026
5c95836
Document pip install for Windows, tighten README and comments
romleiaj Jul 6, 2026
c7fa988
Add simple keypoint matching web page
romleiaj Dec 20, 2024
7d1337f
Update imports
romleiaj Dec 20, 2024
0e87b28
Formatting (ruff)
romleiaj Dec 20, 2024
b138579
Add new calibrations, refactor, use pycolmap instead of custom load s…
romleiaj Dec 20, 2024
6dc75ee
Add simple quaternion viz script
romleiaj Dec 20, 2024
915c421
Add dynamic uncertainty
romleiaj Jan 2, 2025
6a65c17
Fix bug: invert rotation so we get perspective from cam to world. Beg…
romleiaj Feb 5, 2025
7df6104
Add a 'transfer_calibration' function for the UV calibration method, …
romleiaj Mar 12, 2025
af1df24
Add IR calibration and GIF generation
romleiaj Mar 12, 2025
fc828d1
Add manual refinement if give point pairs. Add pretty print
romleiaj Mar 21, 2025
fc20ee9
Update points UI
romleiaj Mar 21, 2025
4de4970
Move UI
romleiaj Mar 21, 2025
133a210
Remove unnecessary files
romleiaj Mar 21, 2025
8df450c
Updates for calibration UI
romleiaj Mar 31, 2025
95ad41c
Update keypoint UI, move camera models more to Rotation
romleiaj Mar 31, 2025
3ed7c32
Ignore egg
romleiaj Mar 31, 2025
2c8429e
Minor bugfixes to the calibration process
romleiaj Jun 20, 2025
17da2c6
Update to latest colmap (3.13), test full pipeline again.
romleiaj Dec 29, 2025
fce8c4d
Add bottle for the keypoint server
romleiaj Jul 6, 2026
e3d9853
Add structured parsing for kamera file/directory naming conventions
romleiaj Jul 6, 2026
cacd8eb
Fix calibration pipeline bugs, adopt structured name parsing
romleiaj Jul 6, 2026
c91e5fd
Move to pycolmap 4.x (COLMAP 3.14 bindings)
romleiaj Jul 6, 2026
c21d6a0
Source pycolmap from conda-forge, not pip
romleiaj Jul 6, 2026
d888a2f
Build .venv on the active conda env's python, any version
romleiaj Jul 6, 2026
7bfd1f8
Fix transfer calibration solving in the wrong frame (wall-clock INS p…
romleiaj Jul 6, 2026
612e2e6
Add rig, frame, and INS pose-prior configuration for COLMAP models
romleiaj Jul 6, 2026
f709b01
Add single-boresight solve and camera export for rig reconstructions
romleiaj Jul 6, 2026
94fbdd7
Fix boresight composition convention (validated by reprojection)
romleiaj Jul 6, 2026
74b5f64
Implement two-pass rig mapping (derive extrinsics, then constrain)
romleiaj Jul 6, 2026
aa32b2e
Add calibrate_rig.py driver (map -> boresight -> export)
romleiaj Jul 6, 2026
d922117
Collapse rig calibration to a single mapping pass
romleiaj Jul 7, 2026
0e4a845
Fold IR into rig calibration via independent per-group boresights
romleiaj Jul 7, 2026
c11b440
Wire full calibration: extract/match, map, boresight, export, gifs
romleiaj Jul 7, 2026
bd7e372
Add flight prep: stage raw imagery into images0 with frame selection
romleiaj Jul 7, 2026
c15bc4c
Emit a complete rig model JSON per flight
romleiaj Jul 7, 2026
beb8116
Add CALIBRATION.md: raw flight to camera models walkthrough
romleiaj Jul 7, 2026
6615943
CALIBRATION.md: add README-consistent install instructions
romleiaj Jul 7, 2026
bd9e85b
CALIBRATION.md: generalize modality language beyond fl004's RGB
romleiaj Jul 7, 2026
380e899
CALIBRATION.md: move Installation above TL;DR
romleiaj Jul 7, 2026
83f3b54
CALIBRATION.md: tighten and add image placeholders
romleiaj Jul 7, 2026
5be643e
Remove v3 refs
romleiaj Jul 7, 2026
fd7ef84
Output viz homographies for DIVE as well
romleiaj Jul 7, 2026
edb6486
CALIBRATION.md: add pipeline mermaid diagrams, drop v3 refs
romleiaj Jul 8, 2026
cce5bcb
Add optional 'fusion' dependency group
romleiaj Jul 8, 2026
538ec5c
Add cross-modal fusion: register IR into the EO model with MINIMA-LoFTR
romleiaj Jul 8, 2026
850faf3
calibrate_rig.py --fuse: export from one fused multimodal model
romleiaj Jul 8, 2026
fe993d0
Reuse ubelt/stdlib helpers instead of hand-rolled equivalents
romleiaj Jul 8, 2026
879e264
Simplify fusion after review: dedup scaffolds, drop dead state
romleiaj Jul 8, 2026
468ea8b
Remove pycolmap-superseded colmap_processing tooling
romleiaj Jul 8, 2026
3414dd7
Remove kp server as well
romleiaj Jul 8, 2026
336c1b8
calibrate_rig.py: stage raw imagery automatically on first run
romleiaj Jul 8, 2026
c3403e7
Drop frame-paring heuristics from staging; fix stretch percentiles
romleiaj Jul 9, 2026
73979d4
Cap feature-extraction threads to bound memory
romleiaj Jul 9, 2026
93cdc9b
Make the calibration process more idempotent
romleiaj Jul 9, 2026
e54abf0
Add an error analysis PDF to the output of calibration
romleiaj Jul 9, 2026
fdea094
Add lever arm to boresight calibration
romleiaj Jul 9, 2026
c6c3650
Add linear stretch to IR mapping
romleiaj Jul 10, 2026
f992e60
Resuse sparse dirs
romleiaj Jul 10, 2026
1962e17
Remove same-trigger matches to avoid false starts on model building
romleiaj Jul 10, 2026
93c6a8b
Write empty priors for colmap
romleiaj Jul 10, 2026
1ea7dcd
Match DIVE format for alignment homographies
romleiaj Jul 10, 2026
a887e02
Increase num neighbors and disable max distance
romleiaj Jul 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
3 changes: 3 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.venv
**/__pycache__
**/*.pyc
30 changes: 30 additions & 0 deletions .github/workflows/gui-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: GUI build

on:
push:
paths:
- ".github/workflows/gui-build.yml"
- "docker/base/gui-deps.dockerfile"
- "docker/gui.dockerfile"
- "docker-compose.yml"
- "Makefile"
pull_request:
paths:
- ".github/workflows/gui-build.yml"
- "docker/base/gui-deps.dockerfile"
- "docker/gui.dockerfile"
- "docker-compose.yml"
- "Makefile"

# NOTE: This is config validation only, not an image build. A real `make gui`
# build requires the published Noetic core-deps image as a base and a display
# for the wx smoke test. Upgrade this to an actual layer build (e.g. build
# gui-deps against a published core-deps) when CI infrastructure allows.
jobs:
validate-compose:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Validate gui compose config
run: docker compose --profile gui config
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
**__pycache__
**.swp
**.pyc
**.DS_Store
artifacts
build
devel
provision/ansible/.password
.catkin_tools
.venv
kamera.egg-info
293 changes: 293 additions & 0 deletions CALIBRATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,293 @@
# Camera Rig Calibration

Calibrate a KAMERA camera rig from a raw calibration flight to
per-camera models. For every camera it recovers:

- **intrinsics** — focal length, principal point, OpenCV distortion
(from COLMAP bundle adjustment); and
- **the mount** — orientation and lever arm relative to the aircraft
INS, from a single *boresight* solve per modality group.

Outputs: a per-camera `*.yaml`, one self-contained
`<flight>_<date>_<config>_rig.json`, and registration QC gifs.
`TO26Su1_RicesWhale_calibration/fl004` is the running example.

```mermaid
flowchart LR
raw["Raw flight<br/>center/left/right_view<br/>+ meta.json"]
img0["images0/<br/>colmap_rgb · colmap_ir"]
out["kamera_models/<br/>*.yaml · rig.json · gifs"]
raw -->|"stage (automatic)"| img0 -->|"calibrate_rig.py"| out
```

<!-- IMAGE: sparse reconstruction / camera frustums over terrain -->
![Reconstruction overview](assets/calibration/reconstruction_overview.jpg)
*The prior-mapped SfM model — camera poses and 3-D points in the INS ENU frame.*

---

## Installation

Same native post-processing setup as the main
[README](README.md#installation): GDAL and pycolmap from conda-forge,
[uv](https://docs.astral.sh/uv/) for the rest. Requires
[conda](https://conda-forge.org/download/); works on Windows and Linux.

```bash
git clone https://github.com/Kitware/kamera.git && cd kamera
conda env create -f environment.yml
conda activate kamera
make install && source .venv/bin/activate # Linux/macOS
# Windows: pip install -e .
```

Afterwards `conda activate kamera` is all you need. Conda picks the CUDA
pycolmap build with NVIDIA driver 575+, else CPU. **A GPU only matters
here** — feature extraction/matching over a full flight is slow on CPU.

> The raw flight must contain the INS `*_meta.json` files (one per
> image); everything reads the INS from those, not the `.dat`.

---

## TL;DR

```bash
conda activate kamera
FLIGHT=/Volumes/extreme2tb/TO26Su1_RicesWhale_calibration/fl004

# stage (first run, automatic), build database, map with INS priors,
# boresight, export
python kamera/postflight/scripts/calibrate_rig.py $FLIGHT
```

Outputs land in `$FLIGHT/kamera_models/`. The raw imagery dir is
auto-detected under the flight dir; pass `--raw-dir` if a flight holds
several candidates.

---

## Input: the raw flight

```
fl004/
├── images_21deg_N56RF/ # one folder per camera view
│ ├── center_view/ # *_C_*_<mod>.jpg + *_C_*_meta.json
│ ├── left_view/ # *_L_*_<mod>.jpg + ...
│ └── right_view/ # *_R_*_<mod>.jpg + ...
├── ins_raw/ins_raw_*.dat
└── ..._fl004_log.txt
```

Each view folder holds every image that camera took, in whatever
modalities were flown (`rgb`, `uv`, `ir`, or a mix), each with a
`_meta.json` carrying the INS state at exposure. All cameras are
hardware-synchronized — at each trigger they share one timestamp, which
is how images group into frames. Names encode provenance:

```
TO26Su1_RicesWhale_calibration_fl004_C_20260704_193924.627932_rgb.jpg
└──────────── effort ────────────┘ └flt┘└ch┘└─ date ─┘└── time ──┘└mod┘
```

---

## Step 1 — staging into `images0` (automatic)

The pipeline reads a per-*camera* layout, not the raw per-*view* one.
On its first run against a flight, `calibrate_rig.py` reorganizes it
(by symlink; nothing is copied) into per-modality groups so SIFT never
matches across the EO/IR gap. The same staging is available standalone
— useful to inspect the frame selection before committing to a
calibration run:

```bash
python kamera/postflight/scripts/prepare_flight.py <raw_imagery_dir> <flight_dir>
```

```
fl004/
├── colmap_rgb/images0/ # EO group: rgb + uv
│ ├── 21deg_N56RF_center_rgb/ # + *_uv folders if flown
│ ├── 21deg_N56RF_left_rgb/
│ └── 21deg_N56RF_right_rgb/
└── colmap_ir/images0/ # IR group, only if ir was flown
```

A group with no imagery simply isn't created (fl004 is RGB, so only
`colmap_rgb`).

Every trigger with a nav time is staged (`--copy` copies instead of
symlinking). Frame density and overlap are set during the flight, not
pared here — SfM needs enough overlap to triangulate, and the binding
constraint is the *lowest-altitude* pass (smallest footprint).

<!-- IMAGE: fl004 trajectory colored by altitude, three figure-8 bands -->
![fl004 trajectory](assets/calibration/fl004_trajectory.png)
*fl004 — three figure-8 blocks at ~565/435/275 m. All calibration, no transit.*

---

## Step 2 — `calibrate_rig.py`: calibrate

```bash
python kamera/postflight/scripts/calibrate_rig.py <flight_dir>
```

Per modality group, the map fans out to two independent solves that
rejoin at export:

```mermaid
flowchart TD
db["Build database<br/>SIFT + spatial match"] --> priors["Write INS<br/>pose priors"]
priors --> map["Prior-position map<br/>→ one ENU model"]
map --> bore["Boresight<br/>rig → INS"]
map --> sfr["Derive<br/>sensor_from_rig"]
bore --> export["Export"]
sfr --> export
export --> yaml["*.yaml"]
export --> rig["rig.json"]
export --> gif["registration gifs"]
```

- **Build the database** (if none) — SIFT extraction, one OPENCV camera
per folder, then **spatial matching** on INS priors (spatial neighbors,
not every pair).
- **Priors + map** — each image's ENU position at exposure seeds a
prior-position reconstruction, directly in the INS ENU frame (no
separate alignment) and resistant to fragmenting.
- **Boresight + `sensor_from_rig`** — a single robust rig-to-INS rotation
and each camera's rig pose, both recovered from that one model.
- **Export** — a yaml per camera, mount = `ins_from_rig ∘ rig_from_sensor`.

Flags:

| flag | effect |
|------|--------|
| `--reuse-aligned` | reuse an existing `aligned/` model instead of re-mapping (fast) |
| `--save-dir DIR` | output dir (default `<flight_dir>/kamera_models`) |
| `--prior-std M` | INS position prior std, meters (default 2.0) |
| `--groups rgb:colmap_rgb ir:colmap_ir` | override which workspaces run |
| `--no-gifs` / `--num-gifs N` | skip / set QC gifs per camera |
| `--fuse` | cross-modal fusion — see the next section |

A group runs only if its workspace exists, so a flight calibrates
whatever modalities it carries with no extra flags.

---

## Optional — `--fuse`: one multimodal model

By default EO and IR mounts agree only *through the INS*: each group's
boresight resolves to the same physical INS frame, so their relative
mount inherits the INS attitude noise (validated to ~0.01–0.27 deg).
`--fuse` replaces that relay with direct image evidence, using
[MINIMA-LoFTR](https://github.com/LSXI7/MINIMA) — a modality-invariant
deep matcher — to register every IR image straight into the EO model:

```bash
uv sync --group fusion # one-time: torch + vismatch (weights
# auto-download on first run)
python kamera/postflight/scripts/calibrate_rig.py $FLIGHT --fuse
```

Per IR image, its co-located same-trigger EO image is warped into the
IR view (a ground-plane homography from the just-computed per-group
mounts, so the matcher only faces the modality gap, not the ~20x scale
gap), matched, and each matched EO pixel is lifted to 3-D at the
interpolated depth of that EO image's triangulated points. Per-image
PnP only *filters* these correspondences — over near-planar terrain a
single image's pose has a strong tilt/translation ambiguity — and all
surviving matches (tens of thousands) jointly solve **one Sim3**
aligning the IR reconstruction to the EO reconstruction. Every fused IR
pose therefore keeps the IR model's own multi-view relative geometry,
globally registered by the cross-modal matches. The boresight,
`sensor_from_rig`, and export then re-run once on the fused model, so
IR extrinsics are measured against the EO reference camera directly.

The per-group solve still runs first — it supplies the IR intrinsics
and the warp initialization — and any IR camera that fuses too few
frames falls back to its two-boresight calibration with a warning.
Outputs are the same yamls/rig.json (the rig JSON gains a fused
`rgb+ir` group and a `fusion` provenance block) plus a
`fusion_report.json` with per-image match/inlier stats, the model
alignment (rotation, translation, scale, reprojection), and each IR
camera's mount delta vs the two-boresight solution — expect that delta
within the INS-noise band above; a large outlier on one camera flags a
bad fusion.

Tuning flags (defaults are sensible): `--fuse-matcher` (any vismatch
name, e.g. `minima-roma`), `--fuse-pairs-per-ir` (add neighbor-trigger
EO partners), `--fuse-max-dt`, `--fuse-snap-px`, `--fuse-ransac-px`,
`--fuse-min-inliers`, `--fuse-max-images` (smoke runs), `--fuse-ba` /
`--fuse-refine-ir-intrinsics` (optional fused bundle adjustment). A GPU
(CUDA or Apple MPS) makes matching ~6x faster; CPU works.

---

## Outputs

Under `<flight_dir>/kamera_models/`:

```
kamera_models/
├── 21deg_N56RF_center_rgb.yaml # one per physical camera
├── ... # (nine for a 3-station EO+UV+IR rig)
├── fl004_20260704_21deg_N56RF_rig.json # complete rig model
└── registration_gifs/*.gif # QC
```

**Per-camera yaml** — the runtime model: image size, `fx/fy/cx/cy`,
distortion, `camera_quaternion` (camera→INS mount), `camera_position`.

**Rig JSON** — the authoritative, self-contained mount description:
provenance (flight, date, pycolmap version, git commit); reference frame
(INS body, ENU origin, quaternion convention); per group the boresight,
lever arm, and residual stats; per camera the intrinsics, INS mount, rig
extrinsics (`sensor_from_rig`), reprojection error, and image count.

**Registration gifs** — each non-reference camera flipped against its
colocated reference image warped into its view. Features hold still when
calibrated; jitter means misregistration. The visual acceptance check.

<!-- IMAGE: registration flip gif, e.g. uv vs rgb -->
![Registration gif](assets/calibration/registration_example.gif)
*UV flipped against RGB warped into its view — ground features stay locked.*

---

## How it works

Each camera's mount composes down a chain of frames:

```mermaid
flowchart LR
cam["Camera<br/>pixels / rays"] -->|"sensor_from_rig"| rig["Rig<br/>= reference camera"]
rig -->|"ins_from_rig<br/>(boresight)"| ins["INS body"]
ins -->|"nav state at t"| enu["ENU world"]
```

The cameras are rigidly mounted and synchronized, so the flight maps
onto COLMAP's rig model. Rather than force the rig onto the mapper
(rig-constrained mapping from scratch fragments badly), we map once with
INS priors into one ENU model, then recover the two rig transforms above
in closed form — `sensor_from_rig` by robust averaging over synchronized
frames, and one rig-to-INS **boresight** per group. Because every
group's boresight resolves to the *same physical INS frame*, EO and IR
come out mutually consistent with no cross-modal matching — which is why
this replaces the old per-camera search and IR transfer/keypoint steps.
`--fuse` tightens that INS-relayed link into a measured one by matching
IR images directly into the EO model (see the fusion section above).

---

## Troubleshooting

- **Check the gifs first** — the fastest read on calibration quality.
- **Reprojection error** (rig JSON) is INS-noise-limited (tens of px at
long EO focals); a health signal, not the accuracy metric. The
boresight residual (fractions of a degree) and the gifs are.
- **Fragmented model / few images** → the flight was too sparse (not
enough overlap, especially on the lowest-altitude pass); a
calibration flight needs dense, overlapping coverage by design.
- **Iterating on boresight/export** without re-mapping → `--reuse-aligned`.
Loading
Loading