Skip to content

Treat a gap of more than an hour in a GPX track as outside it - #874

Merged
caglarpir merged 6 commits into
mapillary:mainfrom
caglarpir:reject-times-in-gpx-gaps
Oct 22, 2026
Merged

caglarpir merged 6 commits into
mapillary:mainfrom
caglarpir:reject-times-in-gpx-gaps

Conversation

@caglarpir

Copy link
Copy Markdown
Contributor

Stacked on #873; review the last commit.

Problem

A GPX file can hold several trips, with the logger off in between. A video or image recorded during such a gap got a position interpolated between the last point before it and the first after it, hours and kilometres apart. The coverage checks compared it only with the ends of the track:

mapillary_tools video_process NO20250209-170040-000944F.MP4 out --geotag_source gpx \
    --geotag_source_path "GPX Babel Converted.gpx" --video_start_time 2025_02_09_17_00_40_000
# exit 0: the 70mai Omni X800 clip falls in a 13.9-hour gap of the six-day track, and is geotagged anyway

Fix

Consecutive points more than an hour apart leave a gap that counts as outside the track, like the time before its start or after its end. ExifTool's geotagger likewise treats a time in a gap of more than GeoMaxIntSecs (30 minutes by default) as outside the track.

  • An image, or a sampled frame geotagged from the GPX, in a gap fails with MapillaryOutsideGPXTrackError, so a later geotag source can take it, as for an image past either end.
  • A video that falls in a gap misses the track, and raises or warns as one that misses it at either end does. One that falls in a gap only in part warns that part of it is outside the track.

Both errors name the gap: its length, and the time and position of the last point before it and the first after it. For a timelapse the gap is measured in GPS time. The video's own track, which the legacy video sources geotag images from, is not checked. The limit can be raised with MAPILLARY_TOOLS_MAX_GPX_GAP_SECONDS (3600 by default), like the other limits in constants.py. The README says so.

Verification

  • The command above now fails with MapillaryOutsideGPXTrackError, naming the gap from 2025-02-09T04:11:11Z to 18:07:31Z. Interval sampling of the same clip fails its 12 frames.
  • 27 other GPX runs give identical descriptions, exit codes and warnings: action camera, Ricoh and image samples, and 70mai clips with multi-trip GPX files whose gaps they do not fall in (process, and video_process with --video_start_time, by distance and interval).
  • New unit tests. Unit and integration tests, doctests, mypy --python-version 3.9, ruff check, ruff format --check and usort diff are clean.

The README lists NMEA among the sources that geotag videos, but no video
was read from an NMEA file: process and video_process gave the video no
description at all, and exited 0, so it was silently left out. Sampling
fell back to the GPS in the video, which a video recorded with an
external logger does not have.

Read the NMEA file into a track and place it on the video exactly as a
GPX track is: synced by the GPS clock of the video, or by
--video_start_time, or assumed to start with the video, with the
interpolation options, the coverage checks and the clipping of process.
GPXVideoExtractor now reads its file through _parse_tracks(), which
NMEAVideoExtractor overrides, and names the format of the file in its
messages. A file that is not valid NMEA is reported as an invalid GPX
file is. Interval sampling checks an NMEA track started with the video
by --interpolation_use_gpx_start_time as it checks a GPX track.

On an action camera video with a GPX and the same track as NMEA, process gives
the same track, and video_process samples the same frames at the same
positions.
Images geotagged from an NMEA file failed with a traceback when the
file had an RMC sentence without a date, such as the last one of a log
cut short (TypeError), or GGA sentences and no RMC sentence at all
(UnboundLocalError), and a video was failed as if the file were not
NMEA. A void RMC sentence, which receivers log before they acquire a
fix, gave the date of their unset clock, and a GGA sentence without a
fix added a point at 0, 0. Only $GP sentences were read, so a log of a
receiver of several systems, which writes $GN sentences, gave no points
at all.

Skip RMC sentences that are void or have no date or time, and GGA
sentences without a time or a fix, and read the sentences of any
talker, keeping the first fix of each time, so that a receiver that logs
a fix from two talkers does not give two points at the same time. GGA
sentences without the date of any RMC sentence give no points, with a
warning. The points read from valid files are the same.
process searches a directory for the images and videos it can process,
and skips the other files. It skipped a file named explicitly the same
way, without a word: `process VID.insv` processed 0 files and exited 0,
with nothing on stderr, so a user could not tell why the video was not
there.

Warn of such a file. It is still skipped, and the run does not fail:
callers that pass the files a user selected rely on that, and find the
skipped files in the process report (--process_report_path). A zip file
is not warned of, as process_and_upload uploads it.
video_process samples each video into a folder of its own in the import
path, and then processes the import path with the same options. With
--skip_subfolders, the processing step skipped those folders: it found
no samples, wrote an empty description file and exited 0, and
video_process_and_upload uploaded nothing.

Process, and upload, the sample folders of the videos as import paths
of their own. --skip_subfolders still keeps the other subfolders of the
import path out, and the description file is written to the import path,
as without the option.
For a video without a GPS clock, process places a GPX track at the start
of the video: it assumes the track starts with it, or
--interpolation_use_gpx_start_time says so. --interpolation_offset_time
then moves the track back, before the start of the video. The upload
drops the points before video time 0 without putting one at 0, so the
first frames had no position: with an offset of 2.5 seconds and a point
every second, the first half second of the video.

Clip such a track at the start of the video, with a point interpolated
at video time 0, as a track that a clock places is clipped at both ends.
Its end is left as it is, as the track of a timelapse without a GPS
clock runs on past the end of the video. Sampling, which reads its track
only within the video, is unchanged.

The warning of a track that runs on for more than twice the video said,
with an offset too, that the video was assumed to start with the track.
With an offset, it now says where along the track the offset places the
start of the video, and lists checking the offset next to trimming the
GPX and video_process --video_start_time. Without one, it is unchanged.
A GPX file can hold several trips, with the GPS logger switched off in
between. A video or an image recorded during such a gap was given a
position interpolated between the last point before it and the first
after it, which can be hours and kilometres apart. A dashcam clip placed
by --video_start_time in a 14-hour gap of a six-day GPX was sampled and
geotagged as if the track covered it: the coverage checks compared the
video or image only with the ends of the track.

Consecutive points more than an hour apart now leave a gap that counts
as outside the track, like the time before its start or after its end.
ExifTool's geotagger likewise treats a time in a gap of more than
GeoMaxIntSecs (30 minutes by default) as outside the track.

- An image (or a sampled frame geotagged from the GPX) in a gap fails
  with MapillaryOutsideGPXTrackError, so a later geotag source can take
  it, as it does an image past either end of the track.
- A video that falls in a gap misses the track, and raises or warns as
  one that misses it before or after does. One that falls in a gap only
  in part warns that part of it is outside the track.

Both errors name the gap: its length, and the time and position of the
last point before it and the first after it. For a timelapse the gap is
measured in GPS time, as the other checks are. The video's own GPS
track, which the legacy video sources geotag images from, is not
checked.

The limit can be raised with MAPILLARY_TOOLS_MAX_GPX_GAP_SECONDS (3600
by default), like the other limits in constants.py.
@caglarpir
caglarpir force-pushed the reject-times-in-gpx-gaps branch from 767b151 to a546356 Compare October 22, 2026 20:18
@caglarpir
caglarpir merged commit e9b70bd into mapillary:main Oct 22, 2026
16 checks passed
caglarpir added a commit that referenced this pull request Oct 24, 2026
process uploads the track it places a GPX on a video by with the video,
and the frames are placed along it. Since #874, a time in a gap of more
than MAPILLARY_TOOLS_MAX_GPX_GAP_SECONDS (an hour) between two fixes is
outside the track, but the track clipped to the video was still
interpolated across such a gap. A video that starts in a gap got a point
at video time 0 interpolated between the last fix before the gap and the
first one after it. On a GoPro MAX video with a GPX whose fixes resume 99
seconds into the video, 2 hours after the previous one 5 km away, that
point was 151 m from where the full track puts the start of the video,
and the first 99 seconds of frames were placed between the two. At
driving speed it is kilometres off.

When video time 0, or the end of the video, falls in such a gap, leave
out the fixes on its far side, and interpolate no point across it: the
track starts (or ends) at the first fix past the gap, as one that starts
after the video does, and process still warns that it covers only part
of the video. A gap inside the video fails it with
MapillaryOutsideGPXTrackError, which names the gap and suggests sampling
the video with video_process and --video_geotag_source gpx (or nmea),
which leaves out the frames in the gap. So does a gap that the video
falls in but for an instant at one end, such as the gap after a lone
first fix that the track is started with the video at. A track assumed
to start with a video without a GPS clock keeps its end, which may be
that of a timelapse, unless the end of the video at 1x falls in a gap.
The limit is in GPS seconds, so it spans less video in a timelapse.
Sampling is unchanged.

Checked process with GPX and NMEA tracks made to have gaps:

- The GoPro MAX video above: its track starts at 99.085 s, with the
  warning; with --interpolation_offset_time 2, at 97.085 s. With the
  track ending 159 s into the video and resuming 2 hours later, it ends
  at 159.085 s. With both, it runs from 99.085 to 159.085 s.
- A gap of 70 s inside the video, with the limit at 60 s, fails it, by a
  GPS clock or with --interpolation_use_gpx_start_time, without a GPS
  clock, from an NMEA file, and in a timelapse, whose limit of 30 s is
  in GPS time. With native as the next source, native geotags it.
  video_process samples the same videos and tracks as before.
- Without a GPS clock, a track that resumes 2 hours after the end of the
  video at 1x ends with the video, and one with a lone first fix 2 hours
  before the rest fails. With the limit raised above the gap, the tracks
  are interpolated across it as before.

process with the GPX, NMEA and ExifTool tracks of sample videos and
images from a dozen cameras (58 runs, among them GPX tracks of several
trips with gaps of days, offsets and --interpolation_use_gpx_start_time),
plain process, and video_process with GPX tracks write the same
descriptions as before.
caglarpir added a commit that referenced this pull request Oct 24, 2026
…ource

Interval sampling with --video_geotag_source gpx or nmea geotagged none
of its samples: each failed with "Unable to extract GPS Longitude or GPS
Latitude from the image", and the command exited 6, on a GoPro MAX
timelapse with its GPX and on a Sony video with the GPX or NMEA log of
its GPS. Such a source geotags videos only, so the geotagging stage
reads the samples' own positions, and interval sampling wrote positions
only from the video's own GPS. Distance sampling with the same options
worked, and so did interval sampling with --geotag_source gpx, which
geotags the samples themselves. The message of #896, which located
interval samples along the video's own GPS, said that they were placed
along a GPX too; they were not.

Locate each interval sample along the GPX or NMEA track as GPX sync
placed it on the video, at the sample's video time, and write its
position into the sample, as distance sampling does. Stamp it with the
time of the track there, as distance sampling stamps the samples of a
track from a file: for a video with a GPS clock, and without
--interpolation_offset_time, that is the time it was stamped with
before. With --interpolation_use_gpx_start_time, the track starts at the
first sample, which ffmpeg cuts at time 0, as the samples of a video
whose GPS was rejected as noise are stamped from it, not at the first
frame, where GPX sync starts it for distance sampling. The samples
outside the track, or in one of its gaps of more than an hour, are left
out with a warning, as other samples outside their track are, unless a
legacy source or exiftool_xml, which geotag samples by the GPS of their
video, may geotag them instead.

Distance sampling with such a source interpolated the track across its
gaps: the frames in a gap of hours got positions between the fixes
either side of it, which say nothing of where the camera was, and the
geotagging stage read them from the frames. Since #874 a time in such a
gap is outside the track, and the samples of a file of --geotag_source
in it fail geotagging. Leave those frames out too, with one warning per
video that names the gaps, so that distance and interval samples agree.
The limit is in GPS seconds, as in the geotagging stage, so it spans
less video in a timelapse. Distance sampling with a file of
--geotag_source, which the geotagging stage geotags the frames by, or
with a legacy source or exiftool_xml, is unchanged.

On the GoPro MAX timelapse sampled every 0.5 s, the Sony video sampled
every 20 s with its GPX and with its NMEA log, and a noisy GoPro MAX 2
timelapse with its GPX started with the video, every sample is
geotagged, within 1.4 cm of the track at its capture time, and the
commands exit 0 (the last sample of the GoPro MAX 2 timelapse, past the
GPX, is left out). --geotag_source gpx and nmea, the video's own GPS and
the legacy sources give the same descriptions as before.

Checked distance sampling with tracks made to have gaps:

- A GoPro MAX video with a GPX missing 70 s of fixes inside it, with
  MAPILLARY_TOOLS_MAX_GPX_GAP_SECONDS at 60: the 2098 frames in the gap
  are left out, and at 5 m 55 samples are written instead of 70, none in
  the gap, the 24 before it as before. The same with
  --interpolation_offset_time 2 and with
  --interpolation_use_gpx_start_time. At the default limit, nothing
  changes.
- The same video with a GPX that resumes 99 s into it, 2 hours after its
  previous fix: the first 99 s are left out (46 samples instead of 59),
  where the track starts after the gap; with the GPX ending a minute
  later too, the 14 samples of that minute are written. Started with the
  video, the GPX covers only its first frame, which alone is written,
  instead of 45 samples placed along the gap.
- A Sony video with a GPX and with an NMEA log missing 2 minutes of
  fixes, at a limit of 60 s: 257 samples at 10 m instead of 316, none in
  the gap, the first 114 as before.
- A GoPro MAX timelapse with 41 s of GPS time missing, 1.4 s of video:
  at a limit of 30 s, its 40 frames are left out; at 60 s, nothing
  changes.
- With these tracks as --geotag_source gpx or nmea, with the legacy
  gopro source next to --video_geotag_source gpx, and with interval
  sampling, the descriptions are the same as before.

With the GPX and NMEA tracks of sample videos from a dozen cameras,
among them GPX logs of several trips with gaps of more than an hour
outside the videos (111 runs of video_process: --video_geotag_source gpx
and nmea, with --interpolation_use_gpx_start_time or an offset, interval
sampling, and --geotag_source gpx and nmea), and plain video_process by
distance and by interval on five cameras, the descriptions are the same
as before.
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