Skip to content

Judge a noisy GPS clock by the video's creation time, not by the GPX - #858

Merged
caglarpir merged 8 commits into
mapillary:mainfrom
caglarpir:noisy-clock-by-creation-time
Oct 22, 2026
Merged

caglarpir merged 8 commits into
mapillary:mainfrom
caglarpir:noisy-clock-by-creation-time

Conversation

@caglarpir

@caglarpir caglarpir commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Problem

When a video's GPS is rejected as noise, GPXVideoExtractor still syncs the GPX by the clock of the unfiltered points (#849). GoPro stamps a placeholder date (2015-10-22 and similar) until it acquires GPS time, so a noisy clock more than a day from the GPX was discarded, and the GPX was placed from the start of the video.

The clock was judged against the GPX, though, so a GPX that does not belong to the video was blamed on the clock. A GoPro HERO8 clip with noisy GPS but a real clock, given a GPX recorded on another day, exited 0 with a warning that the clock was 1.3 days away, and that day's track was placed from frame 0. The same GPX on a video with good GPS is rejected with MapillaryOutsideGPXTrackError (#848).

Change

  • A noisy clock that starts within a day of the video's mvhd creation time is taken as real. It places the GPX as for any video with GPS, so a GPX that misses the video raises MapillaryOutsideGPXTrackError. A day absorbs a creation time written in local time (GoPro) and whether it marks the start or the end of the recording.
  • A clock is still a placeholder only when it is also more than a day from the GPX. Some cameras write a meaningless creation time next to a real GPS clock: a GoPro HERO10 recorded in 2022 reports 2016-01-01. A GPX that agrees with such a clock keeps it, as before.
  • If the creation time can't be read, nothing changes.
  • The creation time is read by the camm_parser function that decides the CAMM GPS epoch (Decide the CAMM GPS epoch by the creation time, then the make #846).

Verification

On sample videos, with process <video> --geotag_source gpx --geotag_source_path <gpx> and with video_process … --video_sample_distance 3:

Video + GPX Before After
GoPro HERO8 (noisy, real clock) + GPX from another day exit 0, GPX placed from frame 0 MapillaryOutsideGPXTrackError (exit 6 / 7)
same + GPX ten years off exit 0, GPX placed from frame 0 MapillaryOutsideGPXTrackError (exit 6 / 7)
same + its own GPX synced by the clock, t = 1.35..12.36 s unchanged
same + GPX ending before the video MapillaryOutsideGPXTrackError unchanged
GoPro MAX (noisy, placeholder clock 2015-10-19, created 2023-11-20) + its GPX synced from the start unchanged
GoPro MAX2 timelapse (noisy) + its GPX synced at the clock rate, GPX at 0.16..1.13 s unchanged
  • With no flags, process and video_process still reject the HERO8 clip with MapillaryGPSNoiseError.
  • A GoPro HERO10 clip with noisy GPS, a 2022 clock and a 2016-01-01 creation time, with a GPX built from its own points that starts 60 s early, is still synced by its clock, both as process and as video_process place it.
  • A GPX written from a video's own track and synced back gives the same result as main on 17 sample videos from GoPro, BlackVue, Labpano and Insta360 cameras.
  • New unit tests:
    • A noisy clock that agrees with the creation time rejects a GPX from another day, both as process and as video_process place it. This test fails on main.
    • A placeholder clock is ignored when the creation time is readable.
    • A real clock with a meaningless creation time is kept.
  • Unit tests (942 passed) and integration tests (70 passed), mypy (Python 3.9), ruff check, ruff format --check and usort.

With #855

#855 anchors the GPS clock at the median of its timestamps. With it merged, #858 still changes only the two rows with a GPX from another day, rejected the same way. The rows it leaves unchanged then read:

  • GoPro HERO8 + its own GPX: t = 0.00..11.00 s.
  • GoPro MAX: 66 of its 87 GPS timestamps carry 2023-11-20 and 21 the 2015-10-19 placeholder, so the median clock is real. It places the GPX at t = 6.84 s rather than from the start.
  • GoPro MAX2 timelapse: GPX at 0.188..1.157 s.
  • GoPro HERO10 with the GPX starting 60 s early: -61.36..4.03 s, still synced by its clock.

No sample video then reaches the placeholder branch. The unit tests cover it.

@meta-cla meta-cla Bot added the cla signed label Oct 20, 2026
Interval sampling stamped each frame with the start time of the video plus
its time in the video. A timelapse packs several seconds of capture into
each second of video, so its frames were stamped that many times too close
together. Geotagged from a GPX by those stamps, they all landed near the
start of the track. Geotagged from the video's own track, they moved so far
per second that the speed check could reject them: a 30x timelapse of a
drive read as over 1,000 km/h.

Interval sampling now reads the rate from the video's GPS clock, as GPX sync
does, and stamps the frames at it. The start time is mapped back from the
first GPS timestamps at the same rate. --video_duration_ratio, which used to
apply to every video, now paces only the videos without a GPS clock, so that
a ratio given for those leaves the other videos in the batch alone. Each
video whose clock overrides a given ratio says so in a warning.

The samples are then geotagged from the video's own track (the gopro, camm
and blackvue sources) by their time since the first sample, while the track
is timed in video seconds. It is now timed in GPS seconds, at the rate of its
clock. That also fixes distance samples of a timelapse, which were already
stamped with their capture times.
The previous commit timed the GPS points of a timelapse at one rate from
its first timestamp. Distance samples are stamped with the GPS time of
their video time instead, and the first timestamp of a GoPro MAX
timelapse can be about 3 s early: in one, the camera moved 6 m at 8 m/s
in the 3.9 s to the next one. On two such timelapses (30x and 15x), the
frames that geotagged were placed a median of 36 m and 5 m from where
the sampler picked them, and the last 2 and 1 frames were stamped past
the end of the track:

    The image time is 1.801 seconds beyond the GPX end point

Time the track by the timestamps, interpolated between them and
extrapolated beyond them at the clock's rate. Stamp interval samples of
a timelapse the same way, from the GPS time at video time 0, as distance
samples are.

On those two timelapses, distance samples now all geotag where they
were picked. Interval samples (0.5 s) stay where they were with
--geotag_source gopro, and with a GPX of the same track they move from
a median of 0.6 m and 0.04 m (4.4 m and 1.4 m at most) to within 1 cm.
Videos without a timelapse clock are unchanged.
--interpolation_use_gpx_start_time places the GPX from the start of the
video instead of by the video's GPS clock. It skipped reading the clock
altogether, and with it the clock's rate, so a timelapse got its GPX laid
out at 1x: a 5 s, 30x timelapse got 157 s of GPX, and distance sampling
picked 5 frames along its first 12 m instead of 148 along the 1.4 km route.

The clock is now read in that mode too, and placed at the start of the GPX,
so that only its rate is used. A video that is not a timelapse has a rate of
1, and is placed as before. The coverage check still sees such a track as
one that no clock placed: the clock gave it only its pace.
A timelapse packs several seconds of capture into each second of video.
With a GPS clock of its own, a GPX is synced at the clock's rate. Without
one, nothing in the video tells the rate, and the GPX was laid out at 1x,
so the frames got only the start of the track: an Insta360 ONE X timelapse,
1.8 s of video with a 110 s GPX, gave 1 frame.

--video_duration_ratio, the seconds of real time per second of video, used
to apply to interval sampling only, and was marked deprecated. It now paces
the GPX of a video without a GPS clock in distance sampling too, whether the
GPX is placed by --video_start_time, by --interpolation_use_gpx_start_time
or by assuming that it starts with the video. The GPS clock of a video, if
it has one, still wins, and says so in a warning when it overrides a ratio.

The ratio is required where a track runs on long after the video and
nothing else accounts for it. Without a start time, a track more than twice
as long as the video was already refused; it is now measured against the
time the video spans at the ratio, and the error names the ratio. With
--interpolation_use_gpx_start_time, such a track is now refused as well, in
interval sampling too: the video is a timelapse, or the track was left
running after it or covers more than this video. For a track of less than
a day, which may be a GPS logger left running, the error gives the start of
the track to pass as --video_start_time instead, which places the video the
same way. A longer track is a log of several trips, which that would put the
video on the first of: the error asks for the time the video started.
With --video_start_time nothing is required, so that one GPX can serve
many videos, each placed by its start time.

The ratio divides GPS time now, so one that is not positive is rejected.

process is unchanged.
The previous commit checks a GPX that --interpolation_use_gpx_start_time
says started with a video without a GPS clock. Interval sampling reads
the clock from the video's telemetry, which is empty when its GPS is
rejected as noise. GPX sync reads the clock from that GPS all the same,
and syncs the GPX at that clock's pace, as in process and in distance
sampling. So a 1.24 s GoPro MAX 2 clip with noisy GPS and a 12x clock,
which main samples with its 12 s GPX, was refused:

    The GPX track in GS018205.360.gpx lasts 12 seconds and the video only
    1.24 seconds, so it runs on long after the video ends.

Skip the check when GPX sync reads a clock from the video. The clip
samples as on main again.
It said that a GPX works with interval sampling only, which has not been
true since distance sampling honours --geotag_source, and that the start
time of a video is always read from its GPS or its creation time, which
distance sampling does not do. It now says how each kind of video is
placed along a GPX, that --video_start_time applies to every video in a
command, so one GPX serves several videos run one at a time, how
--video_duration_ratio paces a timelapse without a GPS clock, and what to
pass instead of --interpolation_use_gpx_start_time for a GPS logger left
running. The examples sample one video, and the
--interpolation_use_gpx_start_time one drops the interval sampling options
it no longer needs.
Interval sampling mapped every video stream of a video to one output, and
ffmpeg numbers the frames of all of them in one sequence. A GoPro MAX .360
video has two video streams, so its frames came out interleaved, and frame
i, taken at (i - 1) // 2 intervals, was stamped (i - 1) intervals into the
video. Half of them were stamped past its end. On a 5-minute .360 video
sampled every second, the frames that could be geotagged were placed a
median of 73 m from where they were taken, and the other half failed.

Interval sampling now samples the video stream with the highest resolution,
as distance sampling does. A video with a single video stream, which is
nearly every video, is sampled as before and its samples keep their names.
When a video's GPS is rejected as noise, the GPX is still synced by the
clock of its unfiltered points, unless that clock is more than a day from
the GPX: GoPro stamps a placeholder date (2015-10-22 and similar) until it
acquires GPS time. Judged against the GPX, though, the clock took the
blame for a GPX that does not belong to the video. A GoPro HERO8 clip
whose noisy clock is real, given a GPX from another day, had that track
placed from its first frame, and process exited 0, while the same GPX on
a video with good GPS is rejected for missing it.

A noisy clock that starts within a day of the mvhd creation time of the
video is now taken as real, and places the GPX as for any video with GPS,
so a GPX that misses the video raises MapillaryOutsideGPXTrackError. A
day absorbs a creation time written in local time (GoPro) and whether it
marks the start or the end of the recording.

A clock is still a placeholder only when it is more than a day from the
GPX too: some cameras write a creation time that means nothing next to a
real GPS clock (a GoPro HERO10 recorded in 2022 reports 2016-01-01), and
a GPX that agrees with the clock keeps it, as before. Without a readable
creation time, nothing changes.

The creation time is read by the same function that decides the CAMM GPS
epoch.
@caglarpir
caglarpir force-pushed the noisy-clock-by-creation-time branch from 3ac7f34 to 04aee80 Compare October 22, 2026 09:04
@caglarpir
caglarpir merged commit 0c8572a into mapillary:main Oct 22, 2026
16 checks passed
@caglarpir
caglarpir deleted the noisy-clock-by-creation-time branch October 22, 2026 11:25
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