Corrects GPS recordings where track points deviate from the actual route during a given time window. The affected points are replaced by positions evenly distributed along the actual route. The original file is left unchanged; the result is written to a new file. Also, you can change the sample rate (reduce or increase by interpolation) and point density by distance, assign timestamps to points that have none, shift all timestamps by a fixed offset, and compare two tracks to measure deviations over time.
The project was vibe coded with Claude.
The highlighted section of the GPS track is intended to follow the actual route, which can be seen in magenta in the background:
Determine the start and end times of the highlighted section. Here in Garmin BaseCamp:
Create and export a GPS track of the actual route:
The GPS track must correspond to the section that is to serve as a reference for all original positions from the start to the end time. The original GPS points are evenly distributed along this track.
And here is the corrected section:
The points in the highlighted sections will be removed (again in Garmin BaseCamp):
The actual route is reconstructed by adding new points through dragging:
After exporting the modified route as a GPX file, the points are timestamped using retime:
Or, using retime --sample-rate 1, additional points are added at the recording’s sample rate:
Of course, the result is only an estimate, but the more evenly (constant speed) the route was covered, the more accurate it will be. It should not include interruptions such as breaks or red lights. It is equally important that the start and end points of the sections to be corrected are actually located where you were at that time, and not simply the result of an inaccurate measurement. Time-lapse photos or video footage containing a feature that allows their position on the map to be clearly identified can be helpful. In the GPS track, the point with the same timestamp as the photo or frame can then be moved to the correct position in, for example, Garmin BaseCamp, and serve as the start or end point of a section.
Both GPS files must be in GPX 1.1 format (xmlns="http://www.topografix.com/GPX/1/1"). This format is exported by Garmin devices and Garmin BaseCamp, among others.
In the project directory:
python -m venv .venv
source .venv/bin/activate
pip install -e .GPSCleaner runs inside a Python virtual environment (.venv). The environment must be activated once per terminal session before any python -m gpscleaner command will work:
source /path/to/GPSCleaner/.venv/bin/activateYou do not need to change into the project directory first — the full path to activate is enough. After activation, python -m gpscleaner works from any directory as long as you provide full paths to your GPX files.
The activation applies only to the current terminal session. Open a new terminal window and you need to run source … activate again.
Alternatively, without activating the environment:
/path/to/GPSCleaner/.venv/bin/python -m gpscleaner clean --recording ...GPSCleaner provides three subcommands: clean, retime and compare.
python -m gpscleaner clean --recording FILE --start TIME --end TIME --reference FILE [--plot]
| Option | Description |
|---|---|
--recording |
GPX file containing the recording with deviations |
--start |
Time when the recording begins to deviate (UTC, ISO 8601) |
--end |
Time when the recording returns to the actual route (UTC, ISO 8601) |
--reference |
GPX file containing the actual route for the given time window |
--plot |
Also save a PNG visualisation of the tracks (optional) |
Times must be given in UTC. Garmin BaseCamp displays times in local time — please convert accordingly.
python -m gpscleaner clean \
--recording /path/to/recording.gpx \
--start 2026-03-22T15:06:08Z \
--end 2026-03-22T15:30:50Z \
--reference /path/to/reference.gpxFor high sample-rate recordings (e.g. dashcams at 25 fps), timestamps are only displayed to the nearest second in Garmin BaseCamp, which is too imprecise. Use track point indices instead:
python -m gpscleaner clean --recording FILE --start-point N --end-point N --reference FILE [--plot]
| Option | Description |
|---|---|
--recording |
GPX file containing the recording with deviations |
--start-point |
Index of the first deviating track point (1 = first point) |
--end-point |
Index of the last deviating track point (1 = first point) |
--reference |
GPX file containing the actual route for the given time window |
--plot |
Also save a PNG visualisation of the tracks (optional) |
python -m gpscleaner clean \
--recording /path/to/recording.gpx \
--start-point 1250 \
--end-point 3400 \
--reference /path/to/reference.gpx--start-point/--end-point and --start/--end cannot be combined.
GPS coordinates can be used to identify the start and end point directly:
python -m gpscleaner clean --recording FILE --start-coord LAT,LON --end-coord LAT,LON --reference FILE [--plot]
| Option | Description |
|---|---|
--recording |
GPX file containing the recording with deviations |
--start-coord |
Coordinate of the first deviating track point (LAT,LON) |
--end-coord |
Coordinate of the last deviating track point (LAT,LON) |
--reference |
GPX file containing the actual route for the given window |
--plot |
Also save a PNG visualisation of the tracks (optional) |
python -m gpscleaner clean \
--recording /path/to/recording.gpx \
--start-coord 45.924565242603421,9.340607235208154 \
--end-coord 45.923309549689293,9.343135720118880 \
--reference /path/to/reference.gpxCoordinates must match a track point in --recording exactly. In JOSM's "Advanced object info" window coordinates are displayed in the format 45.9245652, 9.3406072 — remove the space after the comma to get 45.9245652,9.3406072. The coordinate must be unique within the track; if it occurs more than once (e.g. a looping route), an error is raised.
--start-coord/--end-coord cannot be combined with --start/--end or --start-point/--end-point.
Use --sample-rate to resample a recording to a target number of positions per second. Both operations are applied in a single pass per track segment:
- Dense sections (consecutive points closer than 1/RATE seconds) — excess points are dropped.
- Sparse sections (gap between consecutive points larger than 1/RATE seconds) — new points are inserted by linear interpolation of position and timestamp.
This correctly handles recordings from navigation devices that use adaptive sampling (dense near turns, sparse on straight roads): each section is treated individually rather than comparing a global average rate against the target.
python -m gpscleaner clean --recording FILE --sample-rate RATE [--upsample-only] [--plot]
| Option | Description |
|---|---|
--recording |
GPX file to be resampled |
--sample-rate |
Target number of positions per second (decimals allowed, e.g. 0.2 for one position every 5 seconds) |
--upsample-only |
Only insert points in sparse gaps; never remove points from dense sections (optional) |
--plot |
Also save a PNG visualisation of the tracks (optional) |
# Reduce a 25 fps dashcam recording to 1 position/second
python -m gpscleaner clean --recording /path/to/recording.gpx --sample-rate 1
# Upsample a sparse hiking track to 1 position/second
python -m gpscleaner clean --recording /path/to/recording.gpx --sample-rate 1
# Fill gaps in an adaptive-sampling track for photo geotagging (every 2 s),
# without removing the dense sections near turns
python -m gpscleaner clean --recording /path/to/track.gpx --sample-rate 0.5 --upsample-onlyThe default mode produces a track where consecutive points are at most 1/RATE seconds apart. With --upsample-only, dense sections are preserved as-is (useful when their higher precision is intentional, e.g. for geotagging photos from a navigation track). The summary message reports how many points were removed and inserted.
--sample-rate cannot be combined with --start, --end, or --reference. --upsample-only requires --sample-rate.
Use --distance to remove points so that consecutive kept points are at least a given distance apart:
python -m gpscleaner clean --recording FILE --distance METRES [--plot]
| Option | Description |
|---|---|
--recording |
GPX file to be reduced |
--distance |
Minimum distance in metres between consecutive points (decimals allowed, e.g. 2.5) |
--plot |
Also save a PNG visualisation of the tracks (optional) |
python -m gpscleaner clean --recording /path/to/recording.gpx --distance 3A point is removed only if the next point (after removal) would still be within the threshold distance of the current anchor, so no gap exceeds the threshold. Points are never added; sections already sparser than the threshold remain unchanged. If no points can be removed, a message is printed and no output file is created.
--distance cannot be combined with --start, --end, --start-point, --end-point, --start-coord, --end-coord, --reference, or --sample-rate.
All clean modes write the result next to --recording:
recording.gpx → recording_cleaned.gpx (track correction)
→ recording_cleaned.png (with --plot)
→ recording_sample-rate=1.0.gpx (--sample-rate 1)
→ recording_distance=3.0.gpx (--distance 3)
retime operates in two independent modes selected by which options are given.
Some GPS tracks contain sections where track points have no timestamp. This happens,
for example, when points are manually deleted in Garmin BaseCamp and replaced with
new points drawn along the actual route. retime assigns timestamps to these
points based on their distance from the surrounding timestamped points, assuming
constant speed throughout each affected section.
python -m gpscleaner retime --recording FILE [--overwrite] [--sample-rate RATE]
| Option | Description |
|---|---|
--recording |
GPX file containing track points without timestamps |
--overwrite |
Overwrite the recording in place instead of creating a new file (optional) |
--sample-rate |
Insert additional points in gap sections so that consecutive points are at most 1/RATE seconds apart (optional) |
python -m gpscleaner retime --recording /path/to/recording.gpx
python -m gpscleaner retime --recording /path/to/recording.gpx --sample-rate 25The first and last track points of the recording must have timestamps; otherwise processing is aborted with an error message. If no points without timestamps are found, a message is printed and no output file is created.
With --sample-rate, additional track points are inserted within gap sections by
linear interpolation of position and evenly spaced timestamps. Existing gap points
are always kept. If the time between two consecutive gap points is already shorter
than 1/RATE seconds, no new points are inserted between them.
Without --overwrite, the result is written next to the recording:
recording.gpx → recording_retimed.gpx
--overwrite is useful when retime needs to be applied to multiple sections of
the same file in succession: run the command once per section, each time overwriting
the result of the previous run.
Use --shift-time to move every timestamp in a recording forward or backward by the
same amount. This is useful when a GPS device had the wrong time set (e.g. wrong
timezone or a clock that was not synchronised), so all timestamps are off by the
same constant delta.
python -m gpscleaner retime --recording FILE --shift-time OFFSET [--overwrite]
| Option | Description |
|---|---|
--recording |
GPX file whose timestamps should be shifted |
--shift-time |
Amount to shift, e.g. +1h, -30m, +2h13s, +1h2m13s |
--overwrite |
Overwrite the recording in place instead of creating a new file (optional) |
The offset format is [+\|-]<value>[h][m][s]. At least one of h, m, s must
be present. A + prefix (or no prefix) shifts forward in time; - shifts backward.
# GPS clock was 2 hours ahead — shift all timestamps back by 2 hours
python -m gpscleaner retime --recording /path/to/recording.gpx --shift-time -2h
# Clock was 1 hour 30 minutes behind — shift forward
python -m gpscleaner retime --recording /path/to/recording.gpx --shift-time +1h30m
# Shift by 2 hours, 13 minutes and 7 seconds, overwriting the original
python -m gpscleaner retime --recording /path/to/recording.gpx --shift-time +2h13m7s --overwriteTrack points without a <time> element are left unchanged. If no timestamped
points are found, a message is printed and no output file is created.
Without --overwrite, the result is written next to the recording:
recording.gpx → recording_shifted.gpx
--shift-time cannot be combined with --sample-rate.
--plot is not supported by retime (coordinates are not changed) and results in an error.
If the input file's <trk> has a <name>, every output file written by clean or retime gets the same suffix appended to <trk><name> as to the output filename (e.g. _cleaned, _distance=3.0, _sample-rate=1.0, _retimed, _shifted). This makes the result easy to tell apart from the original when both are shown by name — e.g. in Garmin BaseCamp, which displays the <name> value rather than the file path.
If the input file has no <name>, none is added. If the file contains more than one <trk>, all names are left unchanged. Files written with --overwrite keep their original name, since no new, distinguishable file is created.
Compares two GPS tracks by matching points with similar timestamps and printing their distances:
python -m gpscleaner compare --recording FILE --reference FILE --max-time-diff SECONDS [--interval SECONDS]
| Option | Description |
|---|---|
--recording |
GPX recording to compare against the reference |
--reference |
Reference GPS track (drives the comparison) |
--max-time-diff |
Maximum time difference in seconds between matched points (interpreted as ±) |
--interval |
Check every this many seconds (optional; all reference points are compared if omitted) |
python -m gpscleaner compare \
--recording /path/to/recording.gpx \
--reference /path/to/reference.gpx \
--max-time-diff 2
python -m gpscleaner compare \
--recording /path/to/recording.gpx \
--reference /path/to/reference.gpx \
--max-time-diff 2 --interval 60Output is a table printed to the terminal (redirectable with >):
Timestamp (Reference) Distance (m) Timestamp (Original)
2026-03-22T15:06:08+00:00 3.41 2026-03-22T15:06:08+00:00
2026-03-22T15:07:08+00:00 12.87 2026-03-22T15:07:09+00:00
2026-03-22T15:08:08+00:00
A row with no match within --max-time-diff has empty distance and timestamp columns.
With --plot, an additional PNG file is created. For track correction it shows the original recording (blue), reference route (green), and cleaned track (red). For sample rate reduction it shows the original (blue) and the reduced track (red).










