* 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>