Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 25 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -333,20 +333,33 @@ It is used to locate the images along the GPS tracks.
mapillary_tools process MY_IMAGE_DIR --geotag_source "gpx" --geotag_source_path MY_EXTERNAL_GPS.gpx
```

To geotag videos with a GPX file, video start time is required to locate the sample images along the GPS tracks.
It is read from the video's own GPS track when it has one, and otherwise from the video creation time.
Cameras disagree on whether the creation time marks the start or the end of the recording (most dashcams write the end),
so mapillary_tools uses the camera model or a date and time in the file name to tell which, and assumes the start when neither says.
Use `--video_start_time` to override it, in UTC, when the sample images end up one video duration off along the track.
To geotag videos with a GPX file, the sample images are located along the GPS track by the time they were captured.
A video whose own GPS records the time is synced to the track by that clock. For a video without one, specify its start
time, in UTC, with `--video_start_time`, which also lets one GPX file serve several videos:

```sh
# Geotagging with GPX works with interval-based sampling only,
# the two options --video_sample_distance -1 --video_sample_interval 2 are therefore required
# to switch from the default distance-based sampling to the legacy interval-based sampling
mapillary_tools video_process MY_VIDEO_DIR \
--geotag_source "gpx" \
--geotag_source_path MY_EXTERNAL_GPS.gpx \
--video_sample_distance -1 --video_sample_interval 2
--video_start_time 2025_03_14_07_00_30_000
```

Without it, distance-based sampling (the default) assumes that the track starts with the video, and refuses a track
more than twice as long as the video. Interval-based sampling (`--video_sample_distance -1 --video_sample_interval N`)
reads the start time from the video creation time instead. Cameras disagree on whether the creation time marks the start
or the end of the recording (most dashcams write the end), so mapillary_tools uses the camera model or a date and time
in the file name to tell which, and assumes the start when neither says.

A timelapse video packs several seconds of capture into each second of video. Its GPS clock, if it has one, tells how
many. If it has none, specify how many with `--video_duration_ratio`, for instance 60 for a timelapse that took a frame
every 2 seconds and plays at 30 frames per second:

```sh
mapillary_tools video_process MY_TIMELAPSE_DIR \
--geotag_source "gpx" \
--geotag_source_path MY_EXTERNAL_GPS.gpx \
--video_start_time 2025_03_14_07_00_30_000 \
--video_duration_ratio 60
```

Ideally, the GPS device and the capture device should use the same clock to get the timestamps synchronized.
Expand All @@ -363,15 +376,16 @@ mapillary_tools process MY_IMAGE_DIR \

Another option `--interpolation_use_gpx_start_time` moves your images to align with the beginning of the GPS track.
This is useful when you can confirm that you start GPS recording and capturing at the same time, or with a known delay.
For a video without a GPS clock, a track that lasts more than twice as long as the video spans at its
`--video_duration_ratio` is then refused.

```sh
# Start capturing 2.5 seconds after start GPS recording
mapillary_tools video_process MY_VIDEO_DIR \
--geotag_source "gpx" \
--geotag_source_path MY_EXTERNAL_GPS.gpx \
--interpolation_use_gpx_start_time \
--interpolation_offset_time 2.5 \
--video_sample_distance -1 --video_sample_interval 2
--interpolation_offset_time 2.5
```

## New video geotagging features (experimental)
Expand Down
2 changes: 1 addition & 1 deletion mapillary_tools/commands/sample_video.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def add_basic_arguments(self, parser: argparse.ArgumentParser):
)
group.add_argument(
"--video_duration_ratio",
help="[DEPRECATED since v0.10.0] Real time video duration ratio of the under or oversampled video duration. [default: %(default)s]",
help="Seconds of real time per second of video, for videos without a GPS clock of their own to tell, such as 60 for a timelapse that took a frame every 2 seconds and plays at 30 frames per second. A timelapse needs it to be synced to a GPX track. The GPS clock of a video, if it has one, wins. [default: %(default)s]",
type=float,
default=constants.VIDEO_DURATION_RATIO,
required=False,
Expand Down
5 changes: 3 additions & 2 deletions mapillary_tools/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,9 @@ class MapillaryStationaryVideoError(MapillaryDescriptionError):

class MapillaryGPXVideoStartUnknownError(MapillaryDescriptionError):
"""
Nothing says where along a GPX track a video without GPS starts, and the
track is too long to assume that it starts with the video.
Nothing says how a GPX track lines up with a video without a GPS clock:
the track is too long either to assume that it starts with the video, or,
when it is said to, to end with it at the rate the video is said to run.
"""


Expand Down
21 changes: 19 additions & 2 deletions mapillary_tools/geotag/geotag_images_from_video.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from __future__ import annotations

import dataclasses
import logging
import sys
import typing as T
Expand All @@ -15,10 +16,11 @@
else:
from typing_extensions import override

from .. import types, utils
from .. import geo, types, utils
from .base import GeotagImagesFromGeneric
from .geotag_images_from_gpx import GeotagImagesFromGPX
from .geotag_videos_from_video import GeotagVideosFromVideo
from .video_extractors.gpx import GPXVideoExtractor


LOG = logging.getLogger(__name__)
Expand Down Expand Up @@ -74,7 +76,7 @@ def to_description(
)

geotag = GeotagImagesFromGPX(
video_metadata.points,
self._on_capture_timeline(video_metadata.points),
use_gpx_start_time=False,
use_image_start_time=True,
offset_time=self.offset_time,
Expand All @@ -98,6 +100,21 @@ def to_description(

return final_image_metadatas

@classmethod
def _on_capture_timeline(
cls, points: T.Sequence[geo.Point]
) -> T.Sequence[geo.Point]:
"""
Time the video's GPS points in GPS seconds from the start of the video,
like the samples, which are stamped with their capture times. Each
second of a timelapse spans as many as its GPS clock runs faster.
"""
clock = GPXVideoExtractor._read_video_clock(points)
if clock is None or clock.rate == 1.0:
return points

return [dataclasses.replace(p, time=p.time * clock.rate) for p in points]


class GeotagImageSamplesFromVideo(GeotagImagesFromGeneric):
def __init__(
Expand Down
4 changes: 4 additions & 0 deletions mapillary_tools/geotag/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,10 @@ class VideoSyncOption:
# frame at the start of the GPX
use_gpx_start_time: bool = False

# --video_duration_ratio: GPS seconds per second of a video without a GPS
# clock of its own to tell, such as a timelapse
duration_ratio: float = 1.0


SourceOptionSchema = {
"type": "object",
Expand Down
Loading
Loading