Skip to content

Dev/calibration#9

Open
romleiaj wants to merge 216 commits into
mainfrom
dev/calibration
Open

Dev/calibration#9
romleiaj wants to merge 216 commits into
mainfrom
dev/calibration

Conversation

@romleiaj

Copy link
Copy Markdown
Collaborator

No description provided.

romleiaj and others added 30 commits January 28, 2026 17:20
…PU. Make SensorProfiles location relative, not hardcoded.
…upport GPU. Make SensorProfiles location relative, not hardcoded."

This reverts commit 043f233. Move phase
one SDK changes to separate branch.
Confirmed functionality on nuvo0, and improved write time from ~0.8s to ~0.2s.
…y are failing to write to disk. Remove hardcoded path, move into header file.
Successful build, no delta seen. Hopefully makes system more robust to stop/starts.
…e location needs modification for startup scripts.
romleiaj and others added 30 commits July 7, 2026 11:07
calibrate_rig.py now runs the entire flight calibration end to end. Per
modality group it resolves a model by, in order: an existing aligned/
model (--reuse-aligned), an existing feature-extracted database, or
building one from images0. After exporting all camera models it writes
registration QC gifs.

- build_colmap_database (rig.py): pycolmap-native SIFT extraction (one
  OPENCV camera per folder, KAMERA-tuned settings) + prior-based spatial
  matching, replacing the docker feature_extractor/matcher scripts and
  cutting matching from O(N^2) to spatial neighbors.
- registration_gifs.py: flip each non-reference camera against its
  colocated reference-modality image warped into its view, driven purely
  by the exported StandardCamera models.

Validated on fl09 end to end: 9 camera models + 6 gifs (uv/ir vs rgb at
each station), all frames 2-up and correctly sized.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
prepare_flight.py reorganizes a raw flight (per-view folders holding all
modalities + meta jsons) into the calibration pipeline's layout:
<flight>/colmap_<group>/images0/<prefix>_<channel>_<modality>/, split by
modality group (rgb+uv vs ir) and symlinked by default. Synchronized
triggers are kept together, as the rig/frame logic requires.

Frame selection (flight_prep.select_by_spacing) pares by 3-D INS spacing,
every-Nth, and/or a max count. It also estimates forward overlap from
the along-track ground footprint and flags when a selection is too
sparse for SfM at the lowest altitude -- the binding constraint.

On TO26Su1 fl004 (3-camera EO calibration, three figure-8 altitude
bands at ~275/435/565 m): the flight is not spatially oversampled -- at
its native ~44 m trigger spacing forward overlap is already ~55%, and
any spatial thinning drops the low band below the SfM floor. So the
right selection there is keep-all; the compute win is spatial matching,
not decimation. Staged all 2333 triggers x 3 cameras.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Alongside the per-camera v3 yamls, calibrate_rig.py now writes one
self-contained <flight>_<date>_<config>_rig.json describing the whole
mount:

- provenance: flight / effort / config / date parsed from the imagery,
  UTC calibration time, method, pycolmap version, git commit;
- reference frame: INS-body platform, ENU world with its lat0/lon0/h0
  origin, and the quaternion convention spelled out;
- per group (rgb, ir): reference camera, model source, boresight
  (ins_from_rig), lever arm, and boresight residual stats;
- per camera: intrinsics (fx/fy/cx/cy + OpenCV distortion), the INS
  mount (camera_quaternion + position), the rig extrinsics
  (sensor_from_rig rotation + translation), median reprojection error,
  and image count.

The reference sensor's sensor_from_rig is identity by construction; the
file is the authoritative mount model, with the yamls as the
per-camera runtime view.

Verified on fl09 (EO+IR): fl09_20200830_85mm_25_5deg_rig.json, 2 groups,
9 cameras, valid and round-trippable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Documents the two-step rig calibration end to end using fl004 as the
worked example: prepare_flight.py (stage raw per-view imagery into the
images0 per-camera layout, with frame-selection guidance and the
overlap caveat) then calibrate_rig.py (build database, prior-position
map, boresight, export). Covers the raw folder layout, outputs
(per-camera v3 yamls, the rig JSON, QC gifs), a one-paragraph how-it-
works, and troubleshooting.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mirror the main README's native post-processing setup (conda env create
+ make install on Linux/macOS, pip install -e . on Windows), replacing
the local micromamba-specific note. Keep the README's guidance that
conda auto-selects the CUDA vs CPU pycolmap build and that the GPU only
matters for this calibration step.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fl004 as staged is RGB, but the pipeline handles rgb/uv/ir the same
way, so stop framing the flight as inherently EO-only: raw folders hold
whatever modalities were flown, the images0 tree shows both colmap_rgb
(rgb+uv) and colmap_ir groups, each group runs only if its workspace
exists, and outputs list one yaml per physical camera (nine for a
3-station EO+UV+IR flight). fl004 stays as the concrete example without
implying single-modality is the norm.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
First-time setup should come before the quick-run commands. Dropped the
now-redundant "see Installation below" pointer from the TL;DR.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Cut ~25% of the prose (1609 -> 1185 words) — condensed the install to a
single block, merged duplicated commands, trimmed the flag/output
sections — while keeping every caveat (GPU, INS-from-meta, don't
over-prune, reprojection-is-health-signal). Added four image slots
(reconstruction overview, fl004 trajectory, registration gif, mount
frame diagram) with captions, and an assets/calibration/ manifest
listing what each should show.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pre-existing doc polish on this branch: inline flowcharts for the
staging/calibration pipeline, the calibrate_rig solve fan-out, and the
frame chain (replacing the planned mount_frames.png), plus yaml/gif
naming cleanup in the assets checklist.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
vismatch (image-matching-models) wraps MINIMA-LoFTR and auto-downloads
its weights; it pulls in torch, so it is opt-in via
`uv sync --group fusion`. kornia is pinned <0.8 because MINIMA's
vendored LoFTR imports kornia.utils.grid, removed in 0.8.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
deep_match.py wraps vismatch/MINIMA-LoFTR behind a lazy import (the
pipeline stays importable without torch) with a tested coordinate
contract: matched keypoints come back in the frames of the arrays
passed in.

fusion.py registers each IR image into the EO reconstruction,
visual-localization style:

- eo_partners pairs each IR image with its co-located same-trigger EO
  image by (station, frame key) — never by filename swaps, whose
  extensions differ — with a temporal cap so IR triggers whose EO fell
  out of the model don't pair across banking turns.
- warp_homography/prewarp_eo pre-align the EO image into the IR view
  via a ground-plane homography from the per-group mounts (exactly
  invertible; INTER_AREA pre-resize to avoid aliasing through the ~20x
  scale gap), so the matcher only faces the modality gap. On fl09 this
  yields 1000-3500 matches/pair at 2-6 px residual.
- lift_eo_pixels casts each matched EO pixel from the image's
  reconstructed pose to the interpolated depth of its triangulated
  points — EO images carry only ~250-500 SIFT points, far too sparse
  to snap dense matches onto for PnP.
- Per-image PnP only filters outliers: over near-planar terrain a
  single image's pose has a ~1.5 deg tilt/translation ambiguity.
  align_ir_to_eo solves one Sim3 (irworld <- eoworld) jointly over all
  images' inliers instead, so fused IR poses inherit the IR model's
  own multi-view relative geometry (fl09: sensor_from_rig spread
  1.585 -> ~0.23 deg).
- insert_ir_image attaches PnP-inlier keypoints that land within
  snap_px of a triangulated observation as observations of those EO 3D
  points (multimodal tracks), one per point per image.
- refine_fused offers a fixed-EO bundle adjustment; off by default
  since per-image BA against the sparse cross-modal tracks re-adds the
  tilt wobble the joint alignment defeats.

Tests run without torch (fake matcher over synthetic nadir geometry);
matcher-contract tests skip unless vismatch is installed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The per-group calibration runs unchanged (it supplies the IR
intrinsics and the warp initialization); with --fuse and both rgb+ir
groups present, the IR images are then registered into the EO model
(fusion.py), the boresight/sensor_from_rig/export re-run once on the
fused reconstruction, and the same yamls/rig.json are written from it
— IR extrinsics measured against the EO reference camera directly
instead of relayed through the INS. IR cameras that fuse too few
frames keep their two-boresight calibration.

QC: each IR camera's mount delta vs the two-boresight solution is
logged, a fusion_report.json (per-image match/inlier stats, model
alignment, skip counts) lands next to the models, and the rig JSON
gains a fused rgb+ir group and a fusion provenance block. The fused
reconstruction is written to colmap_rig_fused/ for inspection.

Without --fuse the outputs are byte-identical to before (verified on
fl09 against the previous driver). Validated end-to-end on fl09:
633/835 IR images fused, alignment over 287,841 matches, fused IR
mounts 0.19-0.22 deg from the two-boresight mounts — within the
known INS-noise band, now measured from image evidence.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ub.ensuredir for the fused-model directory, and functools.lru_cache in
place of the hand-managed snap/image caches in fuse_ir_into_eo (the
image cache's clear-at-16 was just a worse LRU).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Four-angle cleanup pass (reuse/simplification/efficiency/altitude) over
the fusion diff:

- Generalize registration_homography's pixel homography with optional
  per-camera times and a ground plane (far-field remains the default),
  and delete fusion's warp_homography, which duplicated its
  grid/unproject/project/fit scaffold.
- Drop dead bookkeeping: IrMatchResult.per_partner and the per-partner
  stats dict (written, never read), the unused num_snapped property,
  and EoPartner's image_id/name fields that shadowed its pycolmap
  Image.
- Merge the lockstep matched/entries lists into one, and release each
  result's full depth-lifted point cloud once its inlier subset is
  copied into the alignment entry (it was held for all ~800 images).
- Move the refine_ir_intrinsics-implies-run_ba rule into
  fuse_ir_into_eo so the knobs are self-consistent for every caller.
- Extract _calibrate_group in the driver: the boresight -> extrinsics
  -> export -> records sequence was duplicated between the per-group
  loop and the fused path.
- Trim the fusion module docstring to the two load-bearing rationales
  (depth lift, joint Sim3 vs per-image PnP).

No behavior change: default-path outputs remain byte-identical on fl09
and the fused smoke run reproduces the pre-refactor results.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Delete the half of colmap_processing that pycolmap replaced and that
nothing outside the package imports: the hand-rolled COLMAP sqlite
wrapper (database.py) and model reader (colmap_interface.py), the old
calibration/SLAM drivers (calibration.py, dp.py, slam.py), the static
ground-camera model (static_camera_model.py), the docker
feature/matcher/mapper wrappers that rig.build_colmap_database
replaced, and the one-off scripts built on all of the above. Every
deleted script still used pre-namespace `colmap_processing.` imports,
so none of it has been runnable since the kamera.* migration; the same
was true of test/test_camera_models.py, which also depended on
colmap_interface.

What stays is the live sensor-model layer (camera_models.py's
StandardCamera -- the exported product format -- with platform_pose,
geo_conversions, rotations, image_renderer), whose future home is the
geocam project, plus the VTK/geotiff rendering capability (vtk_util,
world_models, geotiff, and the two geotiff scripts), kept for now with
their import prefixes fixed to the kamera.* namespace so they import
again.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fold the prepare_flight step into the calibration driver: when a
flight has no colmap_* workspace yet (or --raw-dir forces a restage),
the raw imagery dir is auto-detected via flight_prep.find_raw_dir --
either the flight dir itself or its single subdirectory holding the
view folders (both center_view/ and bare center/ layouts occur in the
wild) -- and staged with the same frame-paring flags prepare_flight.py
exposes (--spacing, --max-frames, --focal-px, --copy). Already-staged
flights are untouched (verified byte-identical outputs on fl09).

prepare_flight.py remains for staging-only runs, e.g. to inspect frame
selection and overlap before committing to a calibration.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Frame density and overlap are flight-planning concerns; the pipeline
now stages every trigger that has a nav time and stops second-guessing
the survey design. Removes select_by_spacing, the forward-overlap
estimator, and the --spacing/--every/--max-frames/--focal-px/
--height-px flags from stage_flight, prepare_flight.py, and
calibrate_rig.py (staging keeps just --raw-dir/--prefix/--copy);
stage_flight no longer needs the nav provider at all.

Also normalize utilities.stretch_constrast to a 1-99 percentile
stretch (was 0.1-99.9) and fix its clamp typo (225 -> 255), matching
deep_match.to_uint8.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pycolmap's default num_threads=-1 spawns one extraction worker per
core, each holding a full-resolution decode of the input image before
the max_image_size downscale -- tens of GB of transient RAM on a
many-core machine with 100 MP imagery (OOM-killed in the field), and
worse if pycolmap silently fell back to its CPU build, where each
worker also carries the 2x-upsampled 11-octave SIFT pyramid. A few
feeder threads already saturate the GPU; cap at 8.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant