24 Commits
Author SHA1 Message Date
Debashish SahuandHarry Chen 2a19c6887f sfm: DJI Avata 360 telemetry, attitude and factory lens calibration (#128)
* telemetry: read the Avata 360 djmd with its own field numbers

read_dji applied the Osmo 360 field numbers to dvtm_AVATA360. That printed
an IMU rate of 2^64 (1.10.1 is an int64 -555 on the Avata), put every
quaternion of a frame on one time (sensor fps was read from 1.11, which is
not it), and found no GPS (the Avata writes it at 3.4.4, in degrees, with no
unit field).

A DjiLayout table now carries the numbers per proto:

- clip rate 1.8, fps 1.9, no focal, no per-frame accelerometer
- GPS at 3.4.4.1.{2,3} in degrees, altitude at 3.4.4.2 (mm)
- relative altitude at 3.4.5.1 (f32 mm, new TelemetryGps::rel_alt)
- per-frame exposure: ISO 3.2.3.1 (f32), shutter 3.2.4.1 (two varints n, d,
  n/d seconds), colour temperature 3.2.6.1 (varint K)

The Osmo keeps its numbers. An unknown proto keeps them too and is still
flagged as unverified. f-number and EV are not read: both SRTs carry one
constant value on every frame, so no field can be told apart. Nothing
downstream consumes the exposure yet.

The clip note no longer prints "focal 0.0 px" when the clip has no focal
field. The report gains an exposure line and a relative altitude range on
the GPS line, each only when present, so Osmo reports are unchanged.
docs/notes/imu-gps-for-sfm.md has the field map.

Tests in sfm_telemetry_test: synthetic Avata frames, an Osmo frame carrying
Avata numbers, and an unknown proto. Two more run on real clips when
SS_TEST_AVATA_OSV, SS_TEST_AVATA_OSV_HOVER and SS_TEST_AVATA_LRF_HOVER name
them, and print SKIP otherwise.

The test helper near() becomes close_to() in sfm_telemetry_test. near is a
Windows macro and check_winmacro flags the file otherwise.

Checked against the SRT of a 139 s flight (8354 frames, packet p is FrameCnt
p+1): lat/lon within 5e-7 deg, altitudes within 1 mm, ISO and colour
temperature exact, shutter within the SRT's 1/3-stop rounding, 0 misses.
Shifted by one frame the same comparison misses 2695 colour temperatures
and 662 altitudes, so the alignment is real. A hover clip's .LRF proxy,
read as an oracle, pairs 317 frames by exact time and matches bit for bit.
The oracle shares the reader, so the SRT comparison is the independent one.

An Osmo 360 clip's report is byte-identical before and after.

Written by Claude (Sonnet 5.5), with review by a second Claude session.

* sfm: attitude sense and vertical without an accelerometer (Avata 360)

The Avata 360 writes a 4 kHz fused attitude and no accelerometer. With no
accelerometer, telemetry_check never measures which way the quaternion maps,
and the timeline conjugated it by default. On a real flight that is the wrong
sense: the hand-eye fit gives sig_rot 5.37 deg against 0.315 deg for the
quaternion itself. The eigen gap is 2.0 against 620, and the old refit also
comes out mirrored (det -1). So every run logged that the gyro and the poses
disagree, and no rotation prior was used.

- SensorTimeline: sign -1 conjugates an attitude whose sense no
  accelerometer settled (attitudeSenseOpen), in orientationAt and in upAt's
  attitude branch.
- ImuExtrinsic: the existing sign-hypothesis loop tries that conjugate
  instead of breaking, and recomputes the up votes per hypothesis. A source
  with an accelerometer keeps break-at-minus-one and its own votes.
- Telemetry::attitude_world_up: a carrier may declare the vertical of the
  attitude world. The DJI reader declares z-down for dvtm_AVATA360 only. With
  no accelerometer the timeline uses it as the up source, so hasUp() is true
  and the gravity refit, the up factors and the sensor gauge's up all run.
  An accelerometer-less attitude with no declaration now answers no up vote
  instead of guessing world +Z.

Tests:

- sfm_sensor_prior_test: no accelerometer, declared and undeclared; an
  accelerometer wins over a declared vertical; attitudeSenseOpen() is open
  exactly when there is no accelerometer.
- sfm_sensor_gauge_test T8 and T8b.
- sfm_telemetry_test: the declaration is set for the Avata and not for the
  Osmo or an unknown proto.
- sfm_attitude_replay_test, gated on SS_TEST_AVATA_MODEL and SS_TEST_AVATA_OSV
  (SKIP when unset): runs a finished sparse model and its clip's telemetry
  through the pair calibration, the factors, the registration check and the
  gauge, so the fix is checked without mapping again. It requires det(X) +1
  on every refit, which is what catches a flipped declared vertical: the fit
  settles X's sign by agreement with the up votes, so a flip leaves the up
  consensus at 0.227 deg and mirrors X instead.

On a 2090-image GPS-levelled model of one flight: sig_rot 0.315 deg, up
0.227 deg from the model's +Z, 2090 of 2090 images get a prior, 10 held.
With the old sense restored the gate rejects the pair set. One flight and
one clip only.

Osmo 360 is unchanged: attitudeSenseOpen() is false, hasUp() is true through
the accelerometer, and the declared vertical is never read.

Written by Claude (Sonnet 5.5), with review by a second Claude session.

* sfm: factory lens calibration from the Avata 360 OSV, through #119's params

The Avata 360 clip header carries each lens's calibration at StreamMeta.5
(PanoDewarpParams). Entry 4 (native_refine_far_master) is video track 0 and
entry 3 (slave) is track 1. The model is Kannala-Brandt with five radial
terms (k5 at field 15), tangential p1 p2 at field 20 on the equidistant
coordinates, in pixels of the 3840x3840 frame, lens_model 8.

- Telemetry: LensCalibration, djmd_lenses, video_lenses.
- LensCalibration.h: refits the five-term curve to the fisheye models' four
  (1.97 px worst on the master lens, 1.22 px on the slave, +0.1 px median on
  real matches), and appends one CameraOverride with params per lens folder
  (camN, under the capture prefix), unless a setting a person gave already
  covers it: a per-folder or bare --focal or --distortion, manifest params,
  or a dataset-wide focal, distortion or params. A model-only entry keeps its
  model.
- Pipeline: applied in `auto` before the match signature. The [run] lines
  say which lens got what, or why not. `--sensor-gauge none` does not turn
  it off.
- 7 messages in 13 languages.

Measured on real frames (docs/notes/imu-gps-for-sfm.md section 2.3). Against
DJI Studio's equirect export of a hover clip, the master lens on track 0 fits
at 0.55 px median and the slave's set at 2.2 px. Lens to lens on a flight,
0.88 and 0.90 px, swapped 6.2 and 8.4 px, without p 2.0 to 2.2 px, without k5
unusable (the four-term polynomial turns over before 90 deg).

Tests: sfm_telemetry_test (decoding, and real clips when SS_TEST_AVATA_OSV and
SS_TEST_AVATA_OSV_HOVER are set), and the new sfm_lens_calib_test (the refit
against an independent five-term projection, the bearing() round trip, which
is shown able to fail, precedence, folder sizing).

No accuracy gain over a hand-set focal was measured on one flight (median
distance to GPS 0.77 m with the factory calibration, 0.66 m with a hand
focal, one run each, so not shown to differ from run-to-run noise). What it gives is no
hand-tuned focal and a faster match stage (4:06 against 6:07).

Written by Claude (Sonnet 5.5), with review by a second Claude session.

* sfm: a person's lens setting at any prefix beats the factory calibration

applyLensPlans read only the longest matching override. A focal, extra or
params given for the capture was ignored as soon as a longer override named
one of its lenses with only a model. With `--focal clip=900` and
`--camera-model clip/cam0=opencv-fisheye`, clip/cam0 got the factory
calibration and the person's focal was overridden. The model had the same
flaw: it came from the longest match only, even when that entry set none.

It now reads every matching override. Any that sets a focal, extra or params
makes the lens a person's own. The model is the longest one that names a
model, and is also written into an existing entry for the lens folder, so the
params are read in the model they were cut for.

Tests, each run against the old code first:

- a capture focal with a model-only entry for one lens: the lens kept by the
  person (fails on the old code), and the other lens, which the old code
  already handled
- a capture calibration with a model-only entry for one lens (fails on the
  old code)
- a capture model with a nearer entry that names none: the params are cut in
  the capture's model, and buildCameras builds that lens in it (both fail on
  the old code)
- a dataset-wide manifest calibration, and a frame size that differs in only
  one of width and height. Both pin behaviour the old code already had, so
  they pass on it. They fail on a build with the dataset-wide params check
  removed, and with either size comparison removed.

Written by Claude (Sonnet 5.5), with review by a second Claude session.

* rebuild telemetry wasm module

---------

Co-authored-by: Harry Chen <harry7557558@gmail.com>
2026-09-30 20:35:03 -04:00
Harry Chen 7281ae4fea fix outliers and missing images in colmap parser 2026-09-27 14:51:36 -04:00
Harry Chen d61301853b rebuild web viewers 2026-09-24 19:56:52 -04:00
Harry Chen 75016904ff batch processing and dataset creation/meshing improvements 2026-09-16 00:34:05 -04:00
Harry Chen 09fdb7f9a5 add dataset centering option 2026-09-10 11:51:51 -04:00
Harry Chen 19f49eee2c support parsing telemetry from 360 camera videos 2026-09-08 21:19:25 -04:00
Harry Chen ed90171961 revise frustum size calculation 2026-09-06 11:30:47 -04:00
Harry Chen 105a4d62c1 HDR: add tonemap options 2026-09-02 18:47:29 -04:00
Harry Chen ee19ad8c0e support loading colmap dataset with mixed .bin and .txt (#38) 2026-08-31 19:02:15 -04:00
Harry Chen 6e12ef894b use relative path for __FILE__ + fix linking error 2026-08-22 18:49:05 -04:00
Harry Chen b815973277 fix depth/normal supervision + enforce comment lengths 2026-08-15 19:33:19 -04:00
Harry Chen adbf8b19b8 rename spirulae-splat to Spirula Studio 2026-08-05 01:11:25 -04:00
Harry Chen 38c4c60623 colmap equirectangular support for webgl viewer 2026-07-28 18:03:51 -04:00
Harry Chen 7908fb54a0 code restructure 2026-07-22 14:11:28 -04:00
Harry Chen 21631dbe2c optimize webgl viewer 2026-07-16 16:53:13 -04:00
Harry Chen 1bfd044b8c fix webgl viewer sorting precision 2026-07-15 21:12:05 -04:00
Harry Chen 018d6fa2bb minor bug fix and tuning 2026-07-14 23:21:02 -04:00
Harry Chen de7677a7cd improve viewer grid rendering 2026-07-12 20:48:46 -04:00
Harry Chen 0f24f870a1 gui iteration: colmap reconstruction, axes/grid 2026-07-12 19:04:34 -04:00
Harry Chen cc04572c10 viewer camera/point ui enhancement 2026-07-12 11:29:34 -04:00
Harry Chen 272e572e43 support 3dgs datasets for viewer 2026-07-11 23:46:26 -04:00
Harry Chen 05c5523c76 async sorting 2026-07-11 20:18:09 -04:00
Harry Chen 1bff034594 support loading PLY files greater than 2GB 2026-07-11 19:40:05 -04:00
Harry Chen a72956e0e3 add webgl viewer 2026-07-11 17:53:01 -04:00