From 639d83f5cbb27ab35bbb18755a519421ee43d2d0 Mon Sep 17 00:00:00 2001 From: Tao Peng Date: Tue, 25 Mar 2025 11:47:43 -0700 Subject: [PATCH 1/7] git mv mapillary_tools/geotag/blackvue_parser.py mapillary_tools/ --- mapillary_tools/{geotag => }/blackvue_parser.py | 4 ++-- mapillary_tools/geotag/geotag_videos_from_video.py | 3 +-- .../video_data_extraction/extractors/blackvue_parser.py | 3 +-- 3 files changed, 4 insertions(+), 6 deletions(-) rename mapillary_tools/{geotag => }/blackvue_parser.py (97%) diff --git a/mapillary_tools/geotag/blackvue_parser.py b/mapillary_tools/blackvue_parser.py similarity index 97% rename from mapillary_tools/geotag/blackvue_parser.py rename to mapillary_tools/blackvue_parser.py index cddd75e60..0703b8d3f 100644 --- a/mapillary_tools/geotag/blackvue_parser.py +++ b/mapillary_tools/blackvue_parser.py @@ -5,8 +5,8 @@ import pynmea2 -from .. import geo -from ..mp4 import simple_mp4_parser as sparser +from . import geo +from .mp4 import simple_mp4_parser as sparser LOG = logging.getLogger(__name__) diff --git a/mapillary_tools/geotag/geotag_videos_from_video.py b/mapillary_tools/geotag/geotag_videos_from_video.py index ac05ce979..950f1e5b3 100644 --- a/mapillary_tools/geotag/geotag_videos_from_video.py +++ b/mapillary_tools/geotag/geotag_videos_from_video.py @@ -4,11 +4,10 @@ import typing as T from pathlib import Path -from .. import exceptions, geo, telemetry, types, utils +from .. import blackvue_parser, exceptions, geo, telemetry, types, utils from ..camm import camm_parser from ..gpmf import gpmf_gps_filter, gpmf_parser from ..types import FileType -from . import blackvue_parser from .geotag_from_generic import GenericVideoExtractor, GeotagVideosFromGeneric diff --git a/mapillary_tools/video_data_extraction/extractors/blackvue_parser.py b/mapillary_tools/video_data_extraction/extractors/blackvue_parser.py index 9aef060f4..bcb4e97ad 100644 --- a/mapillary_tools/video_data_extraction/extractors/blackvue_parser.py +++ b/mapillary_tools/video_data_extraction/extractors/blackvue_parser.py @@ -1,7 +1,6 @@ import typing as T -from ... import geo -from ...geotag import blackvue_parser +from ... import blackvue_parser, geo from ...mp4 import simple_mp4_parser as sparser from .base_parser import BaseParser From 9725e6dcf9b8d366e521aaf8eab9f188bda67ce1 Mon Sep 17 00:00:00 2001 From: Tao Peng Date: Tue, 25 Mar 2025 11:50:41 -0700 Subject: [PATCH 2/7] tests --- tests/unit/test_blackvue_parser.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/unit/test_blackvue_parser.py b/tests/unit/test_blackvue_parser.py index 0832a739f..c5447a9e8 100644 --- a/tests/unit/test_blackvue_parser.py +++ b/tests/unit/test_blackvue_parser.py @@ -1,8 +1,7 @@ import io import mapillary_tools.geo as geo - -from mapillary_tools.geotag import blackvue_parser +from mapillary_tools import blackvue_parser from mapillary_tools.mp4 import construct_mp4_parser as cparser From 487354f80059dc483a956bb98f2d92f9b36ab1cc Mon Sep 17 00:00:00 2001 From: Tao Peng Date: Tue, 25 Mar 2025 16:27:26 -0700 Subject: [PATCH 3/7] fix --- tests/cli/blackvue_parser.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/cli/blackvue_parser.py b/tests/cli/blackvue_parser.py index e1e1c8380..3e904c40f 100644 --- a/tests/cli/blackvue_parser.py +++ b/tests/cli/blackvue_parser.py @@ -6,8 +6,8 @@ import gpxpy import gpxpy.gpx -from mapillary_tools import geo, utils -from mapillary_tools.geotag import blackvue_parser, utils as geotag_utils +from mapillary_tools import blackvue_parser, geo, utils +from mapillary_tools.geotag import utils as geotag_utils def _parse_gpx(path: pathlib.Path) -> list[geo.Point] | None: From 5219db1a947e9e93572efac06283863c207d1fc7 Mon Sep 17 00:00:00 2001 From: Tao Peng Date: Wed, 26 Mar 2025 00:59:12 -0700 Subject: [PATCH 4/7] move around --- mapillary_tools/blackvue_parser.py | 82 +++++++++++++++--------------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/mapillary_tools/blackvue_parser.py b/mapillary_tools/blackvue_parser.py index 0703b8d3f..2794890cb 100644 --- a/mapillary_tools/blackvue_parser.py +++ b/mapillary_tools/blackvue_parser.py @@ -25,31 +25,22 @@ ) -def _parse_gps_box(gps_data: bytes) -> T.Generator[geo.Point, None, None]: - for line_bytes in gps_data.splitlines(): - match = NMEA_LINE_REGEX.match(line_bytes) - if match is None: - continue - nmea_line_bytes = match.group(2) - if nmea_line_bytes.startswith(b"$GPGGA"): - try: - nmea_line = nmea_line_bytes.decode("utf8") - except UnicodeDecodeError: - continue - try: - nmea = pynmea2.parse(nmea_line) - except pynmea2.nmea.ParseError: - continue - if not nmea.is_valid: - continue - epoch_ms = int(match.group(1)) - yield geo.Point( - time=epoch_ms, - lat=nmea.latitude, - lon=nmea.longitude, - alt=nmea.altitude, - angle=None, - ) +def extract_points(fp: T.BinaryIO) -> T.Optional[T.List[geo.Point]]: + gps_data = sparser.parse_mp4_data_first(fp, [b"free", b"gps "]) + if gps_data is None: + return None + + points = list(_parse_gps_box(gps_data)) + if not points: + return points + + points.sort(key=lambda p: p.time) + + first_point_time = points[0].time + for p in points: + p.time = (p.time - first_point_time) / 1000 + + return points def extract_camera_model(fp: T.BinaryIO) -> str: @@ -89,19 +80,28 @@ def extract_camera_model(fp: T.BinaryIO) -> str: return "" -def extract_points(fp: T.BinaryIO) -> T.Optional[T.List[geo.Point]]: - gps_data = sparser.parse_mp4_data_first(fp, [b"free", b"gps "]) - if gps_data is None: - return None - - points = list(_parse_gps_box(gps_data)) - if not points: - return points - - points.sort(key=lambda p: p.time) - - first_point_time = points[0].time - for p in points: - p.time = (p.time - first_point_time) / 1000 - - return points +def _parse_gps_box(gps_data: bytes) -> T.Generator[geo.Point, None, None]: + for line_bytes in gps_data.splitlines(): + match = NMEA_LINE_REGEX.match(line_bytes) + if match is None: + continue + nmea_line_bytes = match.group(2) + if nmea_line_bytes.startswith(b"$GPGGA"): + try: + nmea_line = nmea_line_bytes.decode("utf8") + except UnicodeDecodeError: + continue + try: + nmea = pynmea2.parse(nmea_line) + except pynmea2.nmea.ParseError: + continue + if not nmea.is_valid: + continue + epoch_ms = int(match.group(1)) + yield geo.Point( + time=epoch_ms, + lat=nmea.latitude, + lon=nmea.longitude, + alt=nmea.altitude, + angle=None, + ) From d561cf86cbadfdaa032ee7790bef7035963e6003 Mon Sep 17 00:00:00 2001 From: Tao Peng Date: Wed, 26 Mar 2025 01:30:33 -0700 Subject: [PATCH 5/7] skip parsing errors --- mapillary_tools/blackvue_parser.py | 6 +++++- tests/cli/blackvue_parser.py | 15 +++++++-------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/mapillary_tools/blackvue_parser.py b/mapillary_tools/blackvue_parser.py index 2794890cb..2695fb8cb 100644 --- a/mapillary_tools/blackvue_parser.py +++ b/mapillary_tools/blackvue_parser.py @@ -26,7 +26,11 @@ def extract_points(fp: T.BinaryIO) -> T.Optional[T.List[geo.Point]]: - gps_data = sparser.parse_mp4_data_first(fp, [b"free", b"gps "]) + try: + gps_data = sparser.parse_mp4_data_first(fp, [b"free", b"gps "]) + except sparser.ParsingError: + gps_data = None + if gps_data is None: return None diff --git a/tests/cli/blackvue_parser.py b/tests/cli/blackvue_parser.py index aa490deb5..e1fdddff4 100644 --- a/tests/cli/blackvue_parser.py +++ b/tests/cli/blackvue_parser.py @@ -27,24 +27,23 @@ def _convert_points_to_gpx_segment( return gpx_segment -def _parse_gpx(path: pathlib.Path) -> list[geo.Point] | None: +def _convert_to_track(path: pathlib.Path): + track = gpxpy.gpx.GPXTrack() + track.name = str(path) + with path.open("rb") as fp: points = blackvue_parser.extract_points(fp) - return points - -def _convert_to_track(path: pathlib.Path): - track = gpxpy.gpx.GPXTrack() - points = _parse_gpx(path) if points is None: - raise RuntimeError(f"Invalid BlackVue video {path}") + track.description = "Invalid BlackVue video" + return track segment = _convert_points_to_gpx_segment(points) track.segments.append(segment) with path.open("rb") as fp: model = blackvue_parser.extract_camera_model(fp) track.description = f"Extracted from {model}" - track.name = path.name + return track From f32d0958b9ad684583c468a7ddab8e31ede4b396 Mon Sep 17 00:00:00 2001 From: Tao Peng Date: Wed, 26 Mar 2025 11:13:16 -0700 Subject: [PATCH 6/7] fix --- mapillary_tools/blackvue_parser.py | 43 +++++++++++++--- .../geotag/geotag_videos_from_video.py | 24 ++++----- .../extractors/blackvue_parser.py | 50 ++++++++++++------- tests/cli/blackvue_parser.py | 8 +-- 4 files changed, 82 insertions(+), 43 deletions(-) diff --git a/mapillary_tools/blackvue_parser.py b/mapillary_tools/blackvue_parser.py index 2695fb8cb..e7f30e79c 100644 --- a/mapillary_tools/blackvue_parser.py +++ b/mapillary_tools/blackvue_parser.py @@ -1,3 +1,7 @@ +from __future__ import annotations + +import dataclasses + import json import logging import re @@ -25,7 +29,16 @@ ) -def extract_points(fp: T.BinaryIO) -> T.Optional[T.List[geo.Point]]: +@dataclasses.dataclass +class BlackVueInfo: + # None and [] are equivalent here. Use None as default because: + # ValueError: mutable default for field gps is not allowed: use default_factory + gps: list[geo.Point] | None = None + make: str = "BlackVue" + model: str = "" + + +def extract_blackvue_info(fp: T.BinaryIO) -> BlackVueInfo | None: try: gps_data = sparser.parse_mp4_data_first(fp, [b"free", b"gps "]) except sparser.ParsingError: @@ -35,16 +48,26 @@ def extract_points(fp: T.BinaryIO) -> T.Optional[T.List[geo.Point]]: return None points = list(_parse_gps_box(gps_data)) - if not points: - return points - points.sort(key=lambda p: p.time) - first_point_time = points[0].time - for p in points: - p.time = (p.time - first_point_time) / 1000 + if points: + first_point_time = points[0].time + for p in points: + p.time = (p.time - first_point_time) / 1000 + + # Camera model + try: + cprt_bytes = sparser.parse_mp4_data_first(fp, [b"free", b"cprt"]) + except sparser.ParsingError: + cprt_bytes = None + model = "" + + if cprt_bytes is None: + model = "" + else: + model = _extract_camera_model_from_cprt(cprt_bytes) - return points + return BlackVueInfo(model=model, gps=points) def extract_camera_model(fp: T.BinaryIO) -> str: @@ -56,6 +79,10 @@ def extract_camera_model(fp: T.BinaryIO) -> str: if cprt_bytes is None: return "" + return _extract_camera_model_from_cprt(cprt_bytes) + + +def _extract_camera_model_from_cprt(cprt_bytes: bytes) -> str: # examples: b' {"model":"DR900X Plus","ver":0.918,"lang":"English","direct":1,"psn":"","temp":34,"GPS":1}\x00' # b' Pittasoft Co., Ltd.;DR900S-1CH;1.008;English;1;D90SS1HAE00661;T69;\x00' cprt_bytes = cprt_bytes.strip().strip(b"\x00") diff --git a/mapillary_tools/geotag/geotag_videos_from_video.py b/mapillary_tools/geotag/geotag_videos_from_video.py index 183c9eb91..57c8229ac 100644 --- a/mapillary_tools/geotag/geotag_videos_from_video.py +++ b/mapillary_tools/geotag/geotag_videos_from_video.py @@ -1,6 +1,5 @@ from __future__ import annotations -import io import typing as T from pathlib import Path @@ -70,26 +69,23 @@ def extract(self) -> types.VideoMetadataOrError: class BlackVueVideoExtractor(GenericVideoExtractor): def extract(self) -> types.VideoMetadataOrError: with self.video_path.open("rb") as fp: - points = blackvue_parser.extract_points(fp) + blackvue_info = blackvue_parser.extract_blackvue_info(fp) - if points is None: - raise exceptions.MapillaryVideoGPSNotFoundError( - "No GPS data found from the video" - ) - - if not points: - raise exceptions.MapillaryGPXEmptyError("Empty GPS data found") + if blackvue_info is None: + raise exceptions.MapillaryVideoGPSNotFoundError( + "No GPS data found from the video" + ) - fp.seek(0, io.SEEK_SET) - make, model = "BlackVue", blackvue_parser.extract_camera_model(fp) + if not blackvue_info.gps: + raise exceptions.MapillaryGPXEmptyError("Empty GPS data found") video_metadata = types.VideoMetadata( filename=self.video_path, filesize=utils.get_file_size(self.video_path), filetype=FileType.BLACKVUE, - points=points, - make=make, - model=model, + points=blackvue_info.gps or [], + make=blackvue_info.make, + model=blackvue_info.model, ) return video_metadata diff --git a/mapillary_tools/video_data_extraction/extractors/blackvue_parser.py b/mapillary_tools/video_data_extraction/extractors/blackvue_parser.py index bcb4e97ad..c1ba1cdb0 100644 --- a/mapillary_tools/video_data_extraction/extractors/blackvue_parser.py +++ b/mapillary_tools/video_data_extraction/extractors/blackvue_parser.py @@ -1,7 +1,10 @@ +from __future__ import annotations + +import functools + import typing as T from ... import blackvue_parser, geo -from ...mp4 import simple_mp4_parser as sparser from .base_parser import BaseParser @@ -12,22 +15,35 @@ class BlackVueParser(BaseParser): pointsFound: bool = False - def extract_points(self) -> T.Sequence[geo.Point]: + @functools.cached_property + def extract_blackvue_info(self) -> blackvue_parser.BlackVueInfo | None: source_path = self.geotag_source_path if not source_path: - return [] + return None + with source_path.open("rb") as fp: - try: - points = blackvue_parser.extract_points(fp) or [] - self.pointsFound = len(points) > 0 - return points - except sparser.ParsingError: - return [] - - def extract_make(self) -> T.Optional[str]: - # If no points were found, assume this is not a BlackVue - return "Blackvue" if self.pointsFound else None - - def extract_model(self) -> T.Optional[str]: - with self.videoPath.open("rb") as fp: - return blackvue_parser.extract_camera_model(fp) or None + return blackvue_parser.extract_blackvue_info(fp) + + def extract_points(self) -> T.Sequence[geo.Point]: + blackvue_info = self.extract_blackvue_info + + if blackvue_info is None: + return [] + + return blackvue_info.gps or [] + + def extract_make(self) -> str | None: + blackvue_info = self.extract_blackvue_info + + if blackvue_info is None: + return None + + return blackvue_info.make + + def extract_model(self) -> str | None: + blackvue_info = self.extract_blackvue_info + + if blackvue_info is None: + return None + + return blackvue_info.model diff --git a/tests/cli/blackvue_parser.py b/tests/cli/blackvue_parser.py index e1fdddff4..1bb01a54a 100644 --- a/tests/cli/blackvue_parser.py +++ b/tests/cli/blackvue_parser.py @@ -8,7 +8,7 @@ import gpxpy import gpxpy.gpx -from mapillary_tools import geo, utils, blackvue_parser +from mapillary_tools import blackvue_parser, geo, utils def _convert_points_to_gpx_segment( @@ -32,13 +32,13 @@ def _convert_to_track(path: pathlib.Path): track.name = str(path) with path.open("rb") as fp: - points = blackvue_parser.extract_points(fp) + blackvue_info = blackvue_parser.extract_blackvue_info(fp) - if points is None: + if blackvue_info is None: track.description = "Invalid BlackVue video" return track - segment = _convert_points_to_gpx_segment(points) + segment = _convert_points_to_gpx_segment(blackvue_info.gps or []) track.segments.append(segment) with path.open("rb") as fp: model = blackvue_parser.extract_camera_model(fp) From 2fe0519a7de1652623f138bcda47eaa93254471c Mon Sep 17 00:00:00 2001 From: Tao Peng Date: Wed, 26 Mar 2025 11:17:25 -0700 Subject: [PATCH 7/7] tests --- tests/unit/test_blackvue_parser.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit/test_blackvue_parser.py b/tests/unit/test_blackvue_parser.py index c5447a9e8..2a430ce9b 100644 --- a/tests/unit/test_blackvue_parser.py +++ b/tests/unit/test_blackvue_parser.py @@ -41,8 +41,8 @@ def test_parse_points(): box = {"type": b"free", "data": [{"type": b"gps ", "data": gps_data}]} data = cparser.Box32ConstructBuilder({b"free": {}}).Box.build(box) - x = blackvue_parser.extract_points(io.BytesIO(data)) - assert x is not None + info = blackvue_parser.extract_blackvue_info(io.BytesIO(data)) + assert info is not None assert [ geo.Point( time=0.0, lat=38.8861575, lon=-76.99239516666667, alt=10.2, angle=None @@ -53,4 +53,4 @@ def test_parse_points(): geo.Point( time=0.968, lat=38.88615816666667, lon=-76.992434, alt=7.7, angle=None ), - ] == list(x) + ] == list(info.gps or [])