Skip to content

Fix CAMM GPS epoch follow-ups from #828 - #842

Open
caglarpir wants to merge 4 commits into
mapillary:mainfrom
caglarpir:fix-camm-epoch-followups
Open

caglarpir wants to merge 4 commits into
mapillary:mainfrom
caglarpir:fix-camm-epoch-followups

Conversation

@caglarpir

@caglarpir caglarpir commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Follow-up to #828. Review of what merged found five problems. The first one ships wrong data.

# Problem on main Fix
1 Our own output reads back as 2034. camm_builder writes the timestamp it holds into the CAMM GPS time field, which since #828 is Unix time for every make, and copies the source make (Labpano) into the track. main's reader converts every Labpano track from GPS time, so our upload of a 2024 Labpano video parses back ten years late. The reader decides the epoch by the mvhd creation time when it can: GPS time sits one GPS epoch before it, Unix time sits close to it, and the two readings are ten years apart. Our output copies the source creation time, so it reads back as Unix time. Without a conclusive creation time the make decides, but a timestamp that GPS time would put in the future is read as Unix time, which covers our upload of a Labpano source that has no creation time. Writes are unchanged from main: Unix time for every make. Released versions wrote something else for a Labpano source: they passed the camera's GPS time through. For the first sample of a PanoX V2 capture from 2024-01-11, v0.14.7 writes 1389017092.966, and main and this branch write 1704981874.966.
2 Exact make match. make == "labpano" misses variants like Labpano Technology Co.,Ltd. Because the elst now widens to 64 bits instead of overflowing, those files come out ten years off silently. The make matches as a substring, and only decides when the creation time is missing or inconclusive. The decision is made on the median timestamp of the track, so one stray sample cannot flip the epoch of the whole track. Samples more than 30 days from the median are dropped with a warning, so a stray cannot end up ten years off either.
3 Silent GPX mis-sync. A GPX file from another day, or an epoch mix-up, only logs a warning, and a GPX a few hours off, such as naive timestamps read in the wrong time zone, syncs silently. Separately, the GPX offset was computed to the first video GPS point instead of to video time 0, so when a video's GPS started late, the GPX track landed early by that much. Once synced, the GPX track is checked against the whole video, from video time 0 to the mvhd duration. A track that misses the video has no position for any moment of it, so it raises MapillaryOutsideGPXTrackError whatever the gap: an epoch mix-up, a GPX file from another day, or naive GPX timestamps read in the wrong time zone. The message names both files and gives the gap in the unit that fits, with a hint to check that the GPX file belongs to the video, then the camera clock and the time zone of the GPX timestamps. A track that covers only part of the video warns with how much of it is left uncovered, allowing 2 s for a once-a-second logger started and stopped with the camera. The check also runs when the video's GPS has no timestamps, as on the Ricoh Theta X: the GPX is then aligned to video time 0, so only an uncovered end can show. If the duration cannot be read, the video is known only up to its last GPS sample, so a gap warns up to 24 h and raises beyond. The offset now projects to video time 0.
4 Mixed tracks. When a track has both type 5 and type 6 samples, the native extractor drops the type 5 points. Both types are merged in time order. The GPX offset anchors on the first point that carries a timestamp. No camera is known to interleave the two types. If one does, interpolating between a type 5 and a type 6 point yields a point without an absolute timestamp, so frames sampled there fall back to the container start time. This is documented in the extractor. On all 14 captures below the output is unchanged.
5 ExifTool 18 s behind native. ExifTool's camm6 rule adds 315964800 without the leap seconds. The ExifTool readers subtract the leap seconds for camera-original CAMM tracks from makes that record GPS time. A camera's track is under a meta handler, which ExifTool reports as MetaFormat=camm, matched case-insensitively. The tracks mapillary_tools writes are under a camm handler (OtherFormat=camm) and hold Unix time, so they are read as is. The correction is logged at debug level.

Behaviour change

  • Exit code 0 → 6 for a GPX that misses the video. --video_geotag_source gpx with a GPX file that does not overlap the video at all now fails that file with MapillaryOutsideGPXTrackError, and process exits with 6 unless --skip_process_errors is set. Before, it exited with 0 and synced the video to a track that has no position for any moment of it. This includes a GPX in local time instead of UTC, and a camera clock off by more than the length of the recording. Other files in the same run are not affected.
  • Partial cover warns but still syncs, for example a GPX logger started a few seconds after the camera. This includes videos whose GPS has no timestamps, which were not checked at all before. Without a readable duration, a gap of up to 24 h warns but still syncs.
  • The error is now a geotagging error (MapillaryOutsideGPXTrackError subclasses MapillaryGeoTaggingError), so the next geotag source gets its turn. A GPX track that misses the capture time says nothing about the file itself. This also applies to images: with --geotag_source gpx --geotag_source exif, an image outside the GPX track now gets its EXIF location instead of failing. With GPX as the only or last source, the error is final, as before. When every source fails, the error reported is the last source's, as with other geotagging errors.
  • Stray CAMM timestamps are dropped. A CAMM GPS point more than 30 days from the median of its track is dropped with a warning. None of the 25 captures below has one.
  • The two warnings Fix GPS epoch vs Unix epoch mix-up when geotagging CAMM videos #828 added are removed. The GPX one is replaced by the check above, and the creation-time check now decides instead of warning.
  • The default geotag sources (native, exiftool_runtime) are unchanged.

Two integration tests paired the 2019 GoPro sample video with a 2025 GPX file and asserted success, which is exactly this mis-sync. Both now shift the GPX to the video's time. A new test asserts that the unrelated pairing fails.

Verification on real files

14 Labpano captures: PanoX V2 ×4, Pilot One ×9, Pilot Era ×1.

  • Default process: main and this branch produce identical output for all 14 files, with no errors and no warnings.
  • Future check and stray drop: none of 25 CAMM captures (the 14 Labpano ones, an Insta360 Pro2, a GoPro MAX video processed by mapillary_tools, and 9 Ricoh Theta X) lacks a creation time or has a stray timestamp. Default process output is the same on all 25 before and after each of the last two commits. The only error, a stationary Ricoh Theta X video, happens on both.
  • Labpano source without a creation time: a PanoX V2 capture from 2024-01-11 with its mvhd creation time cleared. Both the previous commit and this branch read the source correctly. After a dry-run process_and_upload, the previous commit reads the upload back as 2034-01-15, and this branch as 2024-01-11, the same as the source.
  • process with a GPX geotag source on the 12 s GoPro sample video. A GPX covering the video exits 0 without warnings. A GPX starting 6 s into the video warns and exits 0. A GPX in a time zone 2 h off exits 6 with MapillaryOutsideGPXTrackError; main exits 0 without a warning. A GPX starting 0.089 s after the video ends exits 6, and the error says it misses the video by 0.089 seconds, where the previous commit said 0 seconds (0.0 days).
  • 26 GPX and video pairs, run through the GPX extractor before and after the last commit: 24 are unchanged. Both Ricoh Theta X pairs, which were not checked before, now warn, and both warnings are right. In one, the GPX is ExifTool's export of the video's own GPS, which has no timestamps and starts 63 s into the video: aligned to video time 0, the track ends 1.1 minutes before the video does. In the other, the GPX's first point is 1.9 h later than the rest, so the sync puts every other point before the video, leaving 5.0 minutes of it uncovered.
  • process on a sample image with a GPX from another year: with --geotag_source gpx --geotag_source exif, it gets its EXIF location. With GPX alone, it exits 6.
  • Epoch decision: the median gives the same timestamps as deciding by the first sample on all 14 files.
  • Dry-run process_and_upload on a PanoX V2 capture from 2024: parsed back natively, the upload matches the source to the millisecond (2024-01-18 10:40:43.6 to 10:43:56.6). With this branch, ExifTool reads the same times from the source (MetaFormat=camm, leap corrected) and from the upload (OtherFormat=camm, read as is).

main and this branch write the same CAMM bytes, so the only question is who reads them:

Upload read by main's reader this branch's reader v0.14.7 reader (no conversion)
Labpano, 2024 capture 2034 ❌ 2024 2024
Labpano, 2024 capture without a creation time 2034 ❌ 2024 2024

#828 is not in any release yet, so no released version misreads these uploads.

Native vs ExifTool timing. The table shows the last GPS sample minus the mvhd creation time:

14 captures native (leap corrected) ExifTool reading (no leap correction)
last GPS sample − creation time −2.92 s to +0.23 s +15.1 s to +18.2 s

The native reading puts the last GPS sample within 3 s of the creation time on every capture. ExifTool's reading would put it 15–18 s after the file was created.

Known limitation

For a Labpano source without an mvhd creation time, the check that keeps our upload in Unix time compares against the reading machine's clock. It holds for any recording less than ten years old when read, so a 2024 upload of such a source read after 2034 would come back ten years late. All 25 captures above have a creation time.

A video whose GPS has no timestamps cannot be synced to a GPX by time, so the GPX is aligned to video time 0, as on main. If the video's GPS starts later, as on one of the Ricoh Theta X captures above, the track lands early by that much. The check then warns about the uncovered end, but cannot tell a misaligned track from a short one.

Tests

  • tests/unit/test_gps_epoch.py covers:
    • round trips across makes and creation times, with Unix time on the wire for every make
    • reprocessing stability
    • make variants
    • the creation time deciding over the make
    • one stray sample not flipping a track, and strays being dropped
    • Labpano tracks without a creation time, with a pinned clock: GPS time converts, Unix time does not
    • mixed type 5/6 tracks
    • the GPX offset to video time 0 when the video's GPS starts late
    • the GPX coverage policy against the video duration, and against the last GPS sample when the duration cannot be read, including a pickle round trip, extract(), reading the duration, and falling through to the next geotag source
    • a GPX synced to exactly video time 0, including a video whose GPS has no timestamps, and the gap reported in a readable unit
    • images outside a GPX track falling through to the next geotag source, and failing with GPX alone
    • ExifTool leap correction, with the camera (MetaFormat) and mapillary_tools (OtherFormat) tags, plus a real ExifTool run over a file we write. That run is skipped where ExifTool is not installed.
  • Run against main, 65 of the file's 97 tests fail:
    • 40 need code that doesn't exist on main (_check_time_gap ×23, the clock the make fallback reads ×8, _records_gps_time ×6, _video_duration ×3).
    • The other 25 fail on behaviour: ten-year timestamps, missing points, strays kept, GPX tracks synced 5 s and 30 s off, a partial cover not warned about, the next source not getting its turn, DID NOT RAISE, and +18 s.
  • Reverting any one of these fails the new tests: the median, the video-time-0 offset, the future check, dropping strays, the video duration, the error class, checking only a non-zero offset, or the old gap format.
  • Unit: 775 passed, 18 skipped, 1 xfailed. Integration: 57 passed. Doctests pass.
  • ruff check, ruff format --check, usort diff and mypy are clean.

#828's description

#828's description was written for its first commit (25e74ce), and parts of it no longer matched what merged. It has been updated to match 2e00e71: CAMM type 6 is not uniformly GPS time, unix_to_gps_epoch() and get_gps_epoch_time() were removed, nothing converts on write, and it now lists both warnings.

After this PR, CAMM type 6 changes epoch in one place: on read, in camm_parser, decided by the creation time and then the make. Writes are Unix time for every make.

mapillary#828 converts CAMM type 6 timestamps from GPS time to Unix time on read
for Labpano, keyed on an exact make match, and writes Unix time back
out. Review of what shipped found five problems.

1. Double conversion on write. camm_builder copies the source make into
   the CAMM track it writes, but wrote Unix time into the GPS time
   field, so our own upload of a 2024 Labpano video parsed back as 2034.
   denormalize_gps_epochs() now converts back to GPS time for makes
   that record it, which is also what the camera itself wrote and what
   v0.14.7 and earlier wrote.

2. Exact make match. "Labpano Technology Co.,Ltd" and similar variants
   were read ten years off, silently now that the elst widens to 64 bits
   instead of overflowing. The make now matches as a substring, and the
   mvhd creation_time decides ahead of the make when it can: GPS time
   sits one GPS epoch before it, Unix time sits close to it, ten years
   apart. That also reads the Unix-time outputs main has written since
   mapillary#828 correctly, whatever their make.

3. Silent GPX mis-sync. GPXVideoExtractor now raises
   MapillaryOutsideGPXTrackError when the GPX track, once synced, does
   not overlap the video's GPS track at all, like image geotagging
   already does. This replaces the warning mapillary#828 added. The error is made
   picklable, since video geotagging raises it in a worker process.

4. Mixed type 5 and type 6 samples. The native extractor dropped the
   type 5 points when a track had both; it now merges them in time
   order, and the GPX offset anchors on the first point that carries a
   timestamp rather than on the first point.

5. ExifTool 18 s behind native. ExifTool adds the GPS epoch offset
   without the leap seconds. Native is right: on a PanoX V2 file the
   first GPS fix plus the mvhd duration ends 0.02 s from the creation
   time, where ExifTool's reading ends 18 s after the file was created.
   The ExifTool reader now subtracts the leap seconds for CAMM tracks
   from makes that record GPS time.

Two integration tests paired the 2019 GoPro sample video with a 2025
GPX file and asserted success, which is the mis-sync in 3. They now
shift the GPX to the video's time, and a new test asserts that the
unrelated pairing fails.
Addresses review of the CAMM epoch follow-ups:

- Write Unix time whatever the make, as main has since mapillary#828, instead of
  converting back to GPS time for Labpano. This is not what released
  versions wrote: v0.14.7 passed a Labpano camera's GPS time through
  unconverted, 1389017092.966 for the first sample of a PanoX V2 capture
  from 2024-01-11, where this writes the Unix time 1704981874.966, one
  GPS epoch less 18 leap seconds later. Our output copies the source
  creation time, which the reader already uses to tell Unix time from
  GPS time. Removes denormalize_gps_epochs and unix_to_gps_epoch.
- Decide the epoch of a CAMM track by the median of its timestamps, so that
  one stray sample cannot flip the whole track.
- Sync GPX to video time 0 rather than to the first video GPS point, which put
  the track early by that point's time when the video's GPS started late.
- Replace the overlap check with a gap check: silent on overlap, a warning
  naming both files for a gap up to a day, and an error only beyond that,
  where no clock or time zone mistake can explain it. The error is now a
  geotagging error, so the next geotag source gets its turn.
- ExifTool leap correction: match the CAMM format case-insensitively, log when
  it applies, and test it against real ExifTool output of a file we write.
- Document the cost of mixing CAMM type 5 and type 6 samples.
Addresses further review of the CAMM epoch follow-ups:

- Check the GPX track against the whole video, from video time 0 to the
  mvhd duration, rather than to the video's last GPS point. A track that
  misses the video now raises MapillaryOutsideGPXTrackError whatever the
  gap, since it has no position for any moment of the video. This
  catches the most common mistake, naive GPX timestamps read in the
  wrong time zone: main syncs a GPX 2 h off silently, and the previous
  commit only warned. A track that covers only part of the video warns.
  When the duration cannot be read, the previous commit's rule applies:
  a gap warns up to 24 h and raises beyond.
- Without a conclusive creation time, do not read a CAMM timestamp as GPS
  time if that would put it in the future. Unix time read as GPS time
  lands ten years late, so our output of a Labpano source without a
  creation time no longer reads back ten years late, for recordings less
  than ten years old when read.
- Drop CAMM GPS points whose timestamps are more than 30 days from the
  median of the track, and warn. A stray was converted with the track, so
  it ended up years off and corrupted the timestamps interpolated next to
  it.
- Test that images outside a GPX track fall through to the next geotag
  source. The previous commit made MapillaryOutsideGPXTrackError a
  geotagging error, and images raise it too: with --geotag_source gpx
  --geotag_source exif, an image outside the GPX track now gets its EXIF
  location instead of failing. With GPX as the only or last source,
  nothing changes.
Addresses review of the previous commit:

- The GPX time check ran only for a non-zero offset. When the video's GPS
  has no timestamps, as on the Ricoh Theta X, the offset is 0 and nothing
  was checked, and neither was a GPX that syncs to exactly video time 0
  but covers only part of the video. Check whenever there are GPX and
  video GPS points. A GPX synced to 0 starts with the video, so the check
  can only warn that it ends before the video does.
- Report a gap in seconds, minutes, hours or days, whichever fits, with
  significant digits: a GPX starting 0.089 s after the video ended was
  reported as missing it "by 0 seconds (0.0 days)". The hint now starts
  with checking that the GPX file belongs to the video, which fits a gap
  of any size, such as the GPX of the next clip.
- Say what is known about the part of a video outside a GPX track: the
  track has no position for it. The warning and docstrings said that
  positions there are extrapolated. The video sampler drops frames
  outside its track instead, and what happens after upload cannot be
  checked here.
@caglarpir
caglarpir force-pushed the fix-camm-epoch-followups branch from b5f4b94 to d761dac Compare September 23, 2026 14:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant