Skip to content

Commit e433dbf

Browse files
authored
feat: add a new GPMF (GoPro) parser (#529)
* feat: add a new gpmf (GoPro) parser * use this new parser for video_process * refactor * fix tests * fix types * usort * types
1 parent 8fb98b7 commit e433dbf

16 files changed

+565
-293
lines changed

mapillary_tools/constants.py

+7
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os
2+
import typing as T
23

34
import appdirs
45

@@ -22,3 +23,9 @@
2223
)
2324
MAX_SEQUENCE_LENGTH = int(os.getenv(_ENV_PREFIX + "MAX_SEQUENCE_LENGTH", 500))
2425
USER_DATA_DIR = appdirs.user_data_dir(appname="mapillary_tools", appauthor="Mapillary")
26+
# This is DoP value, the lower the better
27+
# See https://github.com/gopro/gpmf-parser#hero5-black-with-gps-enabled-adds
28+
GOPRO_MAX_GPS_PRECISION = int(os.getenv(_ENV_PREFIX + "GOPRO_MAX_GPS_PRECISION", 1000))
29+
GOPRO_GPS_FIXES: T.Set[int] = set(
30+
int(fix) for fix in os.getenv(_ENV_PREFIX + "GOPRO_GPS_FIXES", "2,3").split(",")
31+
)

mapillary_tools/geo.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ def as_unix_time(dt: T.Union[datetime.datetime, int, float]) -> float:
167167
return aware_dt.timestamp()
168168

169169

170-
def interpolate(points: T.List[Point], t: float) -> Point:
170+
def interpolate(points: T.Sequence[Point], t: float) -> Point:
171171
"""
172172
Interpolate or extrapolate the point at time t along the sequence of points (sorted by time).
173173
"""

mapillary_tools/geotag/geotag_from_exif.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515

1616
class GeotagFromEXIF(GeotagFromGeneric):
17-
def __init__(self, image_dir: str, images: T.List[str]):
17+
def __init__(self, image_dir: str, images: T.Sequence[str]):
1818
self.image_dir = image_dir
1919
self.images = images
2020
super().__init__()
+42-105
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,15 @@
1-
import datetime
21
import logging
32
import os
4-
import sys
5-
import tempfile
63
import typing as T
74
from pathlib import Path
85

96
from tqdm import tqdm
107

11-
from .. import constants, exceptions, ffmpeg as ffmpeglib, geo, types, utils
12-
from ..geo import get_max_distance_from_start, gps_distance, pairwise
13-
from . import utils as geotag_utils
8+
from .. import constants, exceptions, geo, types, utils
9+
from . import gpmf_parser, utils as geotag_utils
1410
from .geotag_from_generic import GeotagFromGeneric
1511

1612
from .geotag_from_gpx import GeotagFromGPXWithProgress
17-
from .gpmf import interpolate_times, parse_bin
1813

1914

2015
LOG = logging.getLogger(__name__)
@@ -25,7 +20,6 @@ def __init__(
2520
self,
2621
image_dir: str,
2722
source_path: str,
28-
use_gpx_start_time: bool = False,
2923
offset_time: float = 0.0,
3024
):
3125
self.image_dir = image_dir
@@ -34,10 +28,43 @@ def __init__(
3428
else:
3529
# it is okay to not suffix with .mp4
3630
self.videos = [source_path]
37-
self.use_gpx_start_time = use_gpx_start_time
3831
self.offset_time = offset_time
3932
super().__init__()
4033

34+
def _filter_noisy_points(
35+
self, points: T.Sequence[gpmf_parser.PointWithFix], video: Path
36+
) -> T.Sequence[gpmf_parser.PointWithFix]:
37+
num_points = len(points)
38+
points = [
39+
p
40+
for p in points
41+
if p.gps_fix is not None and p.gps_fix.value in constants.GOPRO_GPS_FIXES
42+
]
43+
if len(points) < num_points:
44+
LOG.warning(
45+
"Removed %d points with the GPS fix not in %s from %s",
46+
num_points - len(points),
47+
constants.GOPRO_GPS_FIXES,
48+
video,
49+
)
50+
51+
num_points = len(points)
52+
points = [
53+
p
54+
for p in points
55+
if p.gps_precision is not None
56+
and p.gps_precision <= constants.GOPRO_MAX_GPS_PRECISION
57+
]
58+
if len(points) < num_points:
59+
LOG.warning(
60+
"Removed %d points with DoP value higher than %d from %s",
61+
num_points - len(points),
62+
constants.GOPRO_MAX_GPS_PRECISION,
63+
video,
64+
)
65+
66+
return points
67+
4168
def to_description(self) -> T.List[types.ImageDescriptionFileOrError]:
4269
descs: T.List[types.ImageDescriptionFileOrError] = []
4370

@@ -55,11 +82,13 @@ def to_description(self) -> T.List[types.ImageDescriptionFileOrError]:
5582
if not sample_images:
5683
continue
5784

58-
points = get_points_from_gpmf(Path(video))
85+
points = self._filter_noisy_points(
86+
gpmf_parser.parse_gpx(Path(video)), Path(video)
87+
)
5988

6089
# bypass empty points to raise MapillaryGPXEmptyError
6190
if points and geotag_utils.is_video_stationary(
62-
get_max_distance_from_start([(p.lat, p.lon) for p in points])
91+
geo.get_max_distance_from_start([(p.lat, p.lon) for p in points])
6392
):
6493
LOG.warning(
6594
"Fail %d sample images due to stationary video %s",
@@ -85,103 +114,11 @@ def to_description(self) -> T.List[types.ImageDescriptionFileOrError]:
85114
self.image_dir,
86115
sample_images,
87116
points,
88-
use_gpx_start_time=self.use_gpx_start_time,
117+
use_gpx_start_time=False,
118+
use_image_start_time=True,
89119
offset_time=self.offset_time,
90120
progress_bar=pbar,
91121
)
92122
descs.extend(geotag.to_description())
93123

94124
return descs
95-
96-
97-
def extract_and_parse_bin(path: Path) -> T.List:
98-
ffmpeg = ffmpeglib.FFMPEG(constants.FFMPEG_PATH, constants.FFPROBE_PATH)
99-
probe = ffmpeg.probe_format_and_streams(path)
100-
101-
format_name = probe["format"]["format_name"].lower()
102-
if "mp4" not in format_name:
103-
raise IOError("File must be an mp4")
104-
105-
stream_id = None
106-
for stream in probe["streams"]:
107-
if (
108-
"codec_tag_string" in stream
109-
and "gpmd" in stream["codec_tag_string"].lower()
110-
):
111-
stream_id = stream["index"]
112-
113-
if stream_id is None:
114-
raise IOError("No GoPro metadata track found - was GPS turned on?")
115-
116-
# https://github.com/mapillary/mapillary_tools/issues/503
117-
if sys.platform == "win32":
118-
delete = False
119-
else:
120-
delete = True
121-
122-
with tempfile.NamedTemporaryFile(delete=delete) as tmp:
123-
try:
124-
LOG.debug("Extracting GoPro stream %s to %s", stream_id, tmp.name)
125-
ffmpeg.extract_stream(path, Path(tmp.name), stream_id)
126-
LOG.debug("Parsing GoPro GPMF %s", tmp.name)
127-
return parse_bin(tmp.name)
128-
finally:
129-
if not delete:
130-
try:
131-
os.remove(tmp.name)
132-
except FileNotFoundError:
133-
pass
134-
135-
136-
def get_points_from_gpmf(path: Path) -> T.List[geo.Point]:
137-
gpmf_data = extract_and_parse_bin(path)
138-
139-
rows = len(gpmf_data)
140-
141-
points: T.List[geo.Point] = []
142-
for i, frame in enumerate(gpmf_data):
143-
t = frame["time"]
144-
145-
if i < rows - 1:
146-
next_ts = gpmf_data[i + 1]["time"]
147-
else:
148-
next_ts = t + datetime.timedelta(seconds=1)
149-
150-
interpolate_times(frame, next_ts)
151-
152-
for point in frame["gps"]:
153-
points.append(
154-
geo.Point(
155-
time=geo.as_unix_time(point["time"]),
156-
lat=point["lat"],
157-
lon=point["lon"],
158-
alt=point["alt"],
159-
angle=None,
160-
)
161-
)
162-
163-
return points
164-
165-
166-
if __name__ == "__main__":
167-
import sys
168-
169-
points = get_points_from_gpmf(Path(sys.argv[1]))
170-
gpx = geotag_utils.convert_points_to_gpx(points)
171-
print(gpx.to_xml())
172-
173-
LOG.setLevel(logging.INFO)
174-
handler = logging.StreamHandler(sys.stderr)
175-
handler.setLevel(logging.INFO)
176-
LOG.addHandler(handler)
177-
LOG.info(
178-
"Stationary: %s",
179-
geotag_utils.is_video_stationary(
180-
get_max_distance_from_start([(p.lat, p.lon) for p in points])
181-
),
182-
)
183-
distance = sum(
184-
gps_distance((cur.lat, cur.lon), (nex.lat, nex.lon))
185-
for cur, nex in pairwise(points)
186-
)
187-
LOG.info("Total distance: %f", distance)

mapillary_tools/geotag/geotag_from_gpx.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ class GeotagFromGPX(GeotagFromGeneric):
2020
def __init__(
2121
self,
2222
image_dir: str,
23-
images: T.List[str],
24-
points: T.List[geo.Point],
23+
images: T.Sequence[str],
24+
points: T.Sequence[geo.Point],
2525
use_gpx_start_time: bool = False,
2626
use_image_start_time: bool = False,
2727
offset_time: float = 0.0,
@@ -172,8 +172,8 @@ class GeotagFromGPXWithProgress(GeotagFromGPX):
172172
def __init__(
173173
self,
174174
image_dir: str,
175-
images: T.List[str],
176-
points: T.List[geo.Point],
175+
images: T.Sequence[str],
176+
points: T.Sequence[geo.Point],
177177
use_gpx_start_time: bool = False,
178178
use_image_start_time: bool = False,
179179
offset_time: float = 0.0,

mapillary_tools/geotag/geotag_from_nmea_file.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ class GeotagFromNMEAFile(GeotagFromGPX):
1212
def __init__(
1313
self,
1414
image_dir: str,
15-
images: T.List[str],
15+
images: T.Sequence[str],
1616
source_path: str,
1717
use_gpx_start_time: bool = False,
1818
offset_time: float = 0.0,

mapillary_tools/geotag/gpmf.py

-134
This file was deleted.

0 commit comments

Comments
 (0)