Skip to content

Commit d4fb843

Browse files
authored
improve: performance for blackvue processing (#729)
* git mv mapillary_tools/geotag/blackvue_parser.py mapillary_tools/ * tests * fix * move around * skip parsing errors * fix * tests
1 parent d01574c commit d4fb843

5 files changed

Lines changed: 134 additions & 96 deletions

File tree

Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
1+
from __future__ import annotations
2+
3+
import dataclasses
4+
15
import json
26
import logging
37
import re
48
import typing as T
59

610
import pynmea2
711

8-
from .. import geo
9-
from ..mp4 import simple_mp4_parser as sparser
12+
from . import geo
13+
from .mp4 import simple_mp4_parser as sparser
1014

1115

1216
LOG = logging.getLogger(__name__)
@@ -25,31 +29,45 @@
2529
)
2630

2731

28-
def _parse_gps_box(gps_data: bytes) -> T.Generator[geo.Point, None, None]:
29-
for line_bytes in gps_data.splitlines():
30-
match = NMEA_LINE_REGEX.match(line_bytes)
31-
if match is None:
32-
continue
33-
nmea_line_bytes = match.group(2)
34-
if nmea_line_bytes.startswith(b"$GPGGA"):
35-
try:
36-
nmea_line = nmea_line_bytes.decode("utf8")
37-
except UnicodeDecodeError:
38-
continue
39-
try:
40-
nmea = pynmea2.parse(nmea_line)
41-
except pynmea2.nmea.ParseError:
42-
continue
43-
if not nmea.is_valid:
44-
continue
45-
epoch_ms = int(match.group(1))
46-
yield geo.Point(
47-
time=epoch_ms,
48-
lat=nmea.latitude,
49-
lon=nmea.longitude,
50-
alt=nmea.altitude,
51-
angle=None,
52-
)
32+
@dataclasses.dataclass
33+
class BlackVueInfo:
34+
# None and [] are equivalent here. Use None as default because:
35+
# ValueError: mutable default <class 'list'> for field gps is not allowed: use default_factory
36+
gps: list[geo.Point] | None = None
37+
make: str = "BlackVue"
38+
model: str = ""
39+
40+
41+
def extract_blackvue_info(fp: T.BinaryIO) -> BlackVueInfo | None:
42+
try:
43+
gps_data = sparser.parse_mp4_data_first(fp, [b"free", b"gps "])
44+
except sparser.ParsingError:
45+
gps_data = None
46+
47+
if gps_data is None:
48+
return None
49+
50+
points = list(_parse_gps_box(gps_data))
51+
points.sort(key=lambda p: p.time)
52+
53+
if points:
54+
first_point_time = points[0].time
55+
for p in points:
56+
p.time = (p.time - first_point_time) / 1000
57+
58+
# Camera model
59+
try:
60+
cprt_bytes = sparser.parse_mp4_data_first(fp, [b"free", b"cprt"])
61+
except sparser.ParsingError:
62+
cprt_bytes = None
63+
model = ""
64+
65+
if cprt_bytes is None:
66+
model = ""
67+
else:
68+
model = _extract_camera_model_from_cprt(cprt_bytes)
69+
70+
return BlackVueInfo(model=model, gps=points)
5371

5472

5573
def extract_camera_model(fp: T.BinaryIO) -> str:
@@ -61,6 +79,10 @@ def extract_camera_model(fp: T.BinaryIO) -> str:
6179
if cprt_bytes is None:
6280
return ""
6381

82+
return _extract_camera_model_from_cprt(cprt_bytes)
83+
84+
85+
def _extract_camera_model_from_cprt(cprt_bytes: bytes) -> str:
6486
# examples: b' {"model":"DR900X Plus","ver":0.918,"lang":"English","direct":1,"psn":"","temp":34,"GPS":1}\x00'
6587
# b' Pittasoft Co., Ltd.;DR900S-1CH;1.008;English;1;D90SS1HAE00661;T69;\x00'
6688
cprt_bytes = cprt_bytes.strip().strip(b"\x00")
@@ -89,19 +111,28 @@ def extract_camera_model(fp: T.BinaryIO) -> str:
89111
return ""
90112

91113

92-
def extract_points(fp: T.BinaryIO) -> T.Optional[T.List[geo.Point]]:
93-
gps_data = sparser.parse_mp4_data_first(fp, [b"free", b"gps "])
94-
if gps_data is None:
95-
return None
96-
97-
points = list(_parse_gps_box(gps_data))
98-
if not points:
99-
return points
100-
101-
points.sort(key=lambda p: p.time)
102-
103-
first_point_time = points[0].time
104-
for p in points:
105-
p.time = (p.time - first_point_time) / 1000
106-
107-
return points
114+
def _parse_gps_box(gps_data: bytes) -> T.Generator[geo.Point, None, None]:
115+
for line_bytes in gps_data.splitlines():
116+
match = NMEA_LINE_REGEX.match(line_bytes)
117+
if match is None:
118+
continue
119+
nmea_line_bytes = match.group(2)
120+
if nmea_line_bytes.startswith(b"$GPGGA"):
121+
try:
122+
nmea_line = nmea_line_bytes.decode("utf8")
123+
except UnicodeDecodeError:
124+
continue
125+
try:
126+
nmea = pynmea2.parse(nmea_line)
127+
except pynmea2.nmea.ParseError:
128+
continue
129+
if not nmea.is_valid:
130+
continue
131+
epoch_ms = int(match.group(1))
132+
yield geo.Point(
133+
time=epoch_ms,
134+
lat=nmea.latitude,
135+
lon=nmea.longitude,
136+
alt=nmea.altitude,
137+
angle=None,
138+
)

mapillary_tools/geotag/geotag_videos_from_video.py

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
from __future__ import annotations
22

3-
import io
43
import typing as T
54
from pathlib import Path
65

7-
from .. import exceptions, geo, telemetry, types, utils
6+
from .. import blackvue_parser, exceptions, geo, telemetry, types, utils
87
from ..camm import camm_parser
98
from ..gpmf import gpmf_gps_filter, gpmf_parser
109
from ..types import FileType
11-
from . import blackvue_parser
1210
from .geotag_from_generic import GenericVideoExtractor, GeotagVideosFromGeneric
1311

1412

@@ -71,26 +69,23 @@ def extract(self) -> types.VideoMetadataOrError:
7169
class BlackVueVideoExtractor(GenericVideoExtractor):
7270
def extract(self) -> types.VideoMetadataOrError:
7371
with self.video_path.open("rb") as fp:
74-
points = blackvue_parser.extract_points(fp)
72+
blackvue_info = blackvue_parser.extract_blackvue_info(fp)
7573

76-
if points is None:
77-
raise exceptions.MapillaryVideoGPSNotFoundError(
78-
"No GPS data found from the video"
79-
)
80-
81-
if not points:
82-
raise exceptions.MapillaryGPXEmptyError("Empty GPS data found")
74+
if blackvue_info is None:
75+
raise exceptions.MapillaryVideoGPSNotFoundError(
76+
"No GPS data found from the video"
77+
)
8378

84-
fp.seek(0, io.SEEK_SET)
85-
make, model = "BlackVue", blackvue_parser.extract_camera_model(fp)
79+
if not blackvue_info.gps:
80+
raise exceptions.MapillaryGPXEmptyError("Empty GPS data found")
8681

8782
video_metadata = types.VideoMetadata(
8883
filename=self.video_path,
8984
filesize=utils.get_file_size(self.video_path),
9085
filetype=FileType.BLACKVUE,
91-
points=points,
92-
make=make,
93-
model=model,
86+
points=blackvue_info.gps or [],
87+
make=blackvue_info.make,
88+
model=blackvue_info.model,
9489
)
9590

9691
return video_metadata
Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
from __future__ import annotations
2+
3+
import functools
4+
15
import typing as T
26

3-
from ... import geo
4-
from ...geotag import blackvue_parser
5-
from ...mp4 import simple_mp4_parser as sparser
7+
from ... import blackvue_parser, geo
68
from .base_parser import BaseParser
79

810

@@ -13,22 +15,35 @@ class BlackVueParser(BaseParser):
1315

1416
pointsFound: bool = False
1517

16-
def extract_points(self) -> T.Sequence[geo.Point]:
18+
@functools.cached_property
19+
def extract_blackvue_info(self) -> blackvue_parser.BlackVueInfo | None:
1720
source_path = self.geotag_source_path
1821
if not source_path:
19-
return []
22+
return None
23+
2024
with source_path.open("rb") as fp:
21-
try:
22-
points = blackvue_parser.extract_points(fp) or []
23-
self.pointsFound = len(points) > 0
24-
return points
25-
except sparser.ParsingError:
26-
return []
27-
28-
def extract_make(self) -> T.Optional[str]:
29-
# If no points were found, assume this is not a BlackVue
30-
return "Blackvue" if self.pointsFound else None
31-
32-
def extract_model(self) -> T.Optional[str]:
33-
with self.videoPath.open("rb") as fp:
34-
return blackvue_parser.extract_camera_model(fp) or None
25+
return blackvue_parser.extract_blackvue_info(fp)
26+
27+
def extract_points(self) -> T.Sequence[geo.Point]:
28+
blackvue_info = self.extract_blackvue_info
29+
30+
if blackvue_info is None:
31+
return []
32+
33+
return blackvue_info.gps or []
34+
35+
def extract_make(self) -> str | None:
36+
blackvue_info = self.extract_blackvue_info
37+
38+
if blackvue_info is None:
39+
return None
40+
41+
return blackvue_info.make
42+
43+
def extract_model(self) -> str | None:
44+
blackvue_info = self.extract_blackvue_info
45+
46+
if blackvue_info is None:
47+
return None
48+
49+
return blackvue_info.model

tests/cli/blackvue_parser.py

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@
88
import gpxpy
99
import gpxpy.gpx
1010

11-
from mapillary_tools import geo, utils
12-
from mapillary_tools.geotag import blackvue_parser
11+
from mapillary_tools import blackvue_parser, geo, utils
1312

1413

1514
def _convert_points_to_gpx_segment(
@@ -28,24 +27,23 @@ def _convert_points_to_gpx_segment(
2827
return gpx_segment
2928

3029

31-
def _parse_gpx(path: pathlib.Path) -> list[geo.Point] | None:
32-
with path.open("rb") as fp:
33-
points = blackvue_parser.extract_points(fp)
34-
return points
35-
36-
3730
def _convert_to_track(path: pathlib.Path):
3831
track = gpxpy.gpx.GPXTrack()
39-
points = _parse_gpx(path)
40-
if points is None:
41-
raise RuntimeError(f"Invalid BlackVue video {path}")
32+
track.name = str(path)
4233

43-
segment = _convert_points_to_gpx_segment(points)
34+
with path.open("rb") as fp:
35+
blackvue_info = blackvue_parser.extract_blackvue_info(fp)
36+
37+
if blackvue_info is None:
38+
track.description = "Invalid BlackVue video"
39+
return track
40+
41+
segment = _convert_points_to_gpx_segment(blackvue_info.gps or [])
4442
track.segments.append(segment)
4543
with path.open("rb") as fp:
4644
model = blackvue_parser.extract_camera_model(fp)
4745
track.description = f"Extracted from {model}"
48-
track.name = path.name
46+
4947
return track
5048

5149

tests/unit/test_blackvue_parser.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import io
22

33
import mapillary_tools.geo as geo
4-
5-
from mapillary_tools.geotag import blackvue_parser
4+
from mapillary_tools import blackvue_parser
65
from mapillary_tools.mp4 import construct_mp4_parser as cparser
76

87

@@ -42,8 +41,8 @@ def test_parse_points():
4241

4342
box = {"type": b"free", "data": [{"type": b"gps ", "data": gps_data}]}
4443
data = cparser.Box32ConstructBuilder({b"free": {}}).Box.build(box)
45-
x = blackvue_parser.extract_points(io.BytesIO(data))
46-
assert x is not None
44+
info = blackvue_parser.extract_blackvue_info(io.BytesIO(data))
45+
assert info is not None
4746
assert [
4847
geo.Point(
4948
time=0.0, lat=38.8861575, lon=-76.99239516666667, alt=10.2, angle=None
@@ -54,4 +53,4 @@ def test_parse_points():
5453
geo.Point(
5554
time=0.968, lat=38.88615816666667, lon=-76.992434, alt=7.7, angle=None
5655
),
57-
] == list(x)
56+
] == list(info.gps or [])

0 commit comments

Comments
 (0)