mapillary-tools 0.13.3a1__py3-none-any.whl → 0.14.0a2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. mapillary_tools/__init__.py +1 -1
  2. mapillary_tools/api_v4.py +237 -16
  3. mapillary_tools/authenticate.py +325 -64
  4. mapillary_tools/{geotag/blackvue_parser.py → blackvue_parser.py} +74 -54
  5. mapillary_tools/camm/camm_builder.py +55 -97
  6. mapillary_tools/camm/camm_parser.py +429 -181
  7. mapillary_tools/commands/__main__.py +12 -6
  8. mapillary_tools/commands/authenticate.py +8 -1
  9. mapillary_tools/commands/process.py +27 -51
  10. mapillary_tools/commands/process_and_upload.py +19 -5
  11. mapillary_tools/commands/sample_video.py +2 -3
  12. mapillary_tools/commands/upload.py +18 -9
  13. mapillary_tools/commands/video_process_and_upload.py +19 -5
  14. mapillary_tools/config.py +31 -13
  15. mapillary_tools/constants.py +47 -6
  16. mapillary_tools/exceptions.py +34 -35
  17. mapillary_tools/exif_read.py +221 -116
  18. mapillary_tools/exif_write.py +7 -7
  19. mapillary_tools/exiftool_read.py +33 -42
  20. mapillary_tools/exiftool_read_video.py +46 -33
  21. mapillary_tools/exiftool_runner.py +77 -0
  22. mapillary_tools/ffmpeg.py +24 -23
  23. mapillary_tools/geo.py +144 -120
  24. mapillary_tools/geotag/base.py +147 -0
  25. mapillary_tools/geotag/factory.py +291 -0
  26. mapillary_tools/geotag/geotag_images_from_exif.py +14 -131
  27. mapillary_tools/geotag/geotag_images_from_exiftool.py +126 -82
  28. mapillary_tools/geotag/geotag_images_from_gpx.py +53 -118
  29. mapillary_tools/geotag/geotag_images_from_gpx_file.py +13 -126
  30. mapillary_tools/geotag/geotag_images_from_nmea_file.py +4 -5
  31. mapillary_tools/geotag/geotag_images_from_video.py +53 -51
  32. mapillary_tools/geotag/geotag_videos_from_exiftool.py +97 -0
  33. mapillary_tools/geotag/geotag_videos_from_gpx.py +39 -0
  34. mapillary_tools/geotag/geotag_videos_from_video.py +20 -185
  35. mapillary_tools/geotag/image_extractors/base.py +18 -0
  36. mapillary_tools/geotag/image_extractors/exif.py +60 -0
  37. mapillary_tools/geotag/image_extractors/exiftool.py +18 -0
  38. mapillary_tools/geotag/options.py +160 -0
  39. mapillary_tools/geotag/utils.py +52 -16
  40. mapillary_tools/geotag/video_extractors/base.py +18 -0
  41. mapillary_tools/geotag/video_extractors/exiftool.py +70 -0
  42. mapillary_tools/{video_data_extraction/extractors/gpx_parser.py → geotag/video_extractors/gpx.py} +57 -39
  43. mapillary_tools/geotag/video_extractors/native.py +157 -0
  44. mapillary_tools/{geotag → gpmf}/gpmf_parser.py +205 -182
  45. mapillary_tools/{geotag → gpmf}/gps_filter.py +5 -3
  46. mapillary_tools/history.py +7 -13
  47. mapillary_tools/mp4/construct_mp4_parser.py +9 -8
  48. mapillary_tools/mp4/io_utils.py +0 -1
  49. mapillary_tools/mp4/mp4_sample_parser.py +36 -28
  50. mapillary_tools/mp4/simple_mp4_builder.py +10 -9
  51. mapillary_tools/mp4/simple_mp4_parser.py +13 -22
  52. mapillary_tools/process_geotag_properties.py +155 -392
  53. mapillary_tools/process_sequence_properties.py +562 -208
  54. mapillary_tools/sample_video.py +13 -20
  55. mapillary_tools/telemetry.py +26 -13
  56. mapillary_tools/types.py +111 -58
  57. mapillary_tools/upload.py +316 -298
  58. mapillary_tools/upload_api_v4.py +55 -122
  59. mapillary_tools/uploader.py +396 -254
  60. mapillary_tools/utils.py +42 -18
  61. {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0a2.dist-info}/METADATA +3 -2
  62. mapillary_tools-0.14.0a2.dist-info/RECORD +72 -0
  63. {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0a2.dist-info}/WHEEL +1 -1
  64. mapillary_tools/geotag/__init__.py +0 -1
  65. mapillary_tools/geotag/geotag_from_generic.py +0 -22
  66. mapillary_tools/geotag/geotag_images_from_exiftool_both_image_and_video.py +0 -93
  67. mapillary_tools/geotag/geotag_videos_from_exiftool_video.py +0 -145
  68. mapillary_tools/video_data_extraction/cli_options.py +0 -22
  69. mapillary_tools/video_data_extraction/extract_video_data.py +0 -176
  70. mapillary_tools/video_data_extraction/extractors/base_parser.py +0 -75
  71. mapillary_tools/video_data_extraction/extractors/blackvue_parser.py +0 -34
  72. mapillary_tools/video_data_extraction/extractors/camm_parser.py +0 -38
  73. mapillary_tools/video_data_extraction/extractors/exiftool_runtime_parser.py +0 -71
  74. mapillary_tools/video_data_extraction/extractors/exiftool_xml_parser.py +0 -53
  75. mapillary_tools/video_data_extraction/extractors/generic_video_parser.py +0 -52
  76. mapillary_tools/video_data_extraction/extractors/gopro_parser.py +0 -43
  77. mapillary_tools/video_data_extraction/extractors/nmea_parser.py +0 -24
  78. mapillary_tools/video_data_extraction/video_data_parser_factory.py +0 -39
  79. mapillary_tools-0.13.3a1.dist-info/RECORD +0 -75
  80. /mapillary_tools/{geotag → gpmf}/gpmf_gps_filter.py +0 -0
  81. {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0a2.dist-info}/entry_points.txt +0 -0
  82. {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0a2.dist-info/licenses}/LICENSE +0 -0
  83. {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0a2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,160 @@
1
+ from __future__ import annotations
2
+
3
+ import dataclasses
4
+ import enum
5
+ import json
6
+ import typing as T
7
+ from pathlib import Path
8
+
9
+ import jsonschema
10
+
11
+ from .. import types
12
+
13
+
14
+ class SourceType(enum.Enum):
15
+ NATIVE = "native"
16
+ GPX = "gpx"
17
+ NMEA = "nmea"
18
+ EXIFTOOL_XML = "exiftool_xml"
19
+ EXIFTOOL_RUNTIME = "exiftool_runtime"
20
+
21
+ # Legacy source types for images
22
+ GOPRO = "gopro"
23
+ BLACKVUE = "blackvue"
24
+ CAMM = "camm"
25
+ EXIF = "exif"
26
+
27
+
28
+ SOURCE_TYPE_ALIAS: dict[str, SourceType] = {
29
+ "blackvue_videos": SourceType.BLACKVUE,
30
+ "gopro_videos": SourceType.GOPRO,
31
+ "exiftool": SourceType.EXIFTOOL_RUNTIME,
32
+ }
33
+
34
+
35
+ @dataclasses.dataclass
36
+ class SourceOption:
37
+ # Type of the source
38
+ source: SourceType
39
+
40
+ # Filter by these filetypes
41
+ filetypes: set[types.FileType] | None = None
42
+
43
+ num_processes: int | None = None
44
+
45
+ source_path: SourcePathOption | None = None
46
+
47
+ interpolation: InterpolationOption | None = None
48
+
49
+ @classmethod
50
+ def from_dict(cls, data: dict[str, T.Any]) -> SourceOption:
51
+ validate_option(data)
52
+
53
+ kwargs: dict[str, T.Any] = {}
54
+ for k, v in data.items():
55
+ # None values are considered as absent and should be ignored
56
+ if v is None:
57
+ continue
58
+ if k == "source":
59
+ kwargs[k] = SourceType(SOURCE_TYPE_ALIAS.get(v, v))
60
+ elif k == "filetypes":
61
+ kwargs[k] = {types.FileType(t) for t in v}
62
+ elif k == "source_path":
63
+ kwargs.setdefault("source_path", SourcePathOption()).source_path = v
64
+ elif k == "pattern":
65
+ kwargs.setdefault("source_path", SourcePathOption()).pattern = v
66
+ elif k == "interpolation_offset_time":
67
+ kwargs.setdefault(
68
+ "interpolation", InterpolationOption()
69
+ ).offset_time = v
70
+ elif k == "interpolation_use_gpx_start_time":
71
+ kwargs.setdefault(
72
+ "interpolation", InterpolationOption()
73
+ ).use_gpx_start_time = v
74
+
75
+ return cls(**kwargs)
76
+
77
+
78
+ @dataclasses.dataclass
79
+ class SourcePathOption:
80
+ pattern: str | None = None
81
+ source_path: Path | None = None
82
+
83
+ def __post_init__(self):
84
+ if self.source_path is None and self.pattern is None:
85
+ raise ValueError("Either pattern or source_path must be provided")
86
+
87
+ def resolve(self, path: Path) -> Path:
88
+ if self.source_path is not None:
89
+ return self.source_path
90
+
91
+ assert self.pattern is not None, (
92
+ "either pattern or source_path must be provided"
93
+ )
94
+
95
+ # %f: the full video filename (foo.mp4)
96
+ # %g: the video filename without extension (foo)
97
+ # %e: the video filename extension (.mp4)
98
+ replaced = Path(
99
+ self.pattern.replace("%f", path.name)
100
+ .replace("%g", path.stem)
101
+ .replace("%e", path.suffix)
102
+ )
103
+
104
+ abs_path = (
105
+ replaced
106
+ if replaced.is_absolute()
107
+ else Path.joinpath(path.parent.resolve(), replaced)
108
+ ).resolve()
109
+
110
+ return abs_path
111
+
112
+
113
+ @dataclasses.dataclass
114
+ class InterpolationOption:
115
+ offset_time: float = 0.0
116
+ use_gpx_start_time: bool = False
117
+
118
+
119
+ SourceOptionSchema = {
120
+ "type": "object",
121
+ "properties": {
122
+ "source": {
123
+ "type": "string",
124
+ "enum": [s.value for s in SourceType] + list(SOURCE_TYPE_ALIAS.keys()),
125
+ },
126
+ "filetypes": {
127
+ "type": "array",
128
+ "items": {
129
+ "type": "string",
130
+ "enum": [t.value for t in types.FileType],
131
+ },
132
+ },
133
+ "source_path": {
134
+ "type": "string",
135
+ },
136
+ "pattern": {
137
+ "type": "string",
138
+ },
139
+ "num_processes": {
140
+ "type": "integer",
141
+ },
142
+ "interpolation_offset_time": {
143
+ "type": "float",
144
+ },
145
+ "interpolation_use_gpx_start_time": {
146
+ "type": "boolean",
147
+ },
148
+ },
149
+ "required": ["source"],
150
+ "additionalProperties": False,
151
+ }
152
+
153
+
154
+ def validate_option(instance):
155
+ jsonschema.validate(instance=instance, schema=SourceOptionSchema)
156
+
157
+
158
+ if __name__ == "__main__":
159
+ # python -m mapillary_tools.geotag.options > schema/geotag_source_option.json
160
+ print(json.dumps(SourceOptionSchema, indent=4))
@@ -1,26 +1,62 @@
1
- import datetime
1
+ from __future__ import annotations
2
+
3
+ import logging
2
4
  import typing as T
5
+ import xml.etree.ElementTree as ET
6
+ from pathlib import Path
3
7
 
4
8
  import gpxpy
5
- import gpxpy.gpx
6
9
 
7
- from .. import geo
10
+ from .. import exiftool_read, geo, utils
11
+
12
+ Track = T.List[geo.Point]
13
+ LOG = logging.getLogger(__name__)
14
+
15
+
16
+ def parse_gpx(gpx_file: Path) -> list[Track]:
17
+ with gpx_file.open("r") as f:
18
+ gpx = gpxpy.parse(f)
8
19
 
20
+ tracks: list[Track] = []
9
21
 
10
- def is_video_stationary(max_distance_from_start: float) -> bool:
11
- radius_threshold = 10
12
- return max_distance_from_start < radius_threshold
22
+ for track in gpx.tracks:
23
+ for segment in track.segments:
24
+ tracks.append([])
25
+ for point in segment.points:
26
+ if point.time is not None:
27
+ tracks[-1].append(
28
+ geo.Point(
29
+ time=geo.as_unix_time(point.time),
30
+ lat=point.latitude,
31
+ lon=point.longitude,
32
+ alt=point.elevation,
33
+ angle=None,
34
+ )
35
+ )
13
36
 
37
+ return tracks
14
38
 
15
- def convert_points_to_gpx_segment(points: T.Sequence[geo.Point]):
16
- gpx_segment = gpxpy.gpx.GPXTrackSegment()
17
- for point in points:
18
- gpx_segment.points.append(
19
- gpxpy.gpx.GPXTrackPoint(
20
- point.lat,
21
- point.lon,
22
- elevation=point.alt,
23
- time=datetime.datetime.utcfromtimestamp(point.time),
39
+
40
+ def index_rdf_description_by_path(
41
+ xml_paths: T.Sequence[Path],
42
+ ) -> dict[str, ET.Element]:
43
+ rdf_description_by_path: dict[str, ET.Element] = {}
44
+
45
+ for xml_path in utils.find_xml_files(xml_paths):
46
+ try:
47
+ etree = ET.parse(xml_path)
48
+ except ET.ParseError as ex:
49
+ verbose = LOG.getEffectiveLevel() <= logging.DEBUG
50
+ if verbose:
51
+ LOG.warning("Failed to parse %s", xml_path, exc_info=True)
52
+ else:
53
+ LOG.warning("Failed to parse %s: %s", xml_path, ex)
54
+ continue
55
+
56
+ rdf_description_by_path.update(
57
+ exiftool_read.index_rdf_description_by_path_from_xml_element(
58
+ etree.getroot()
24
59
  )
25
60
  )
26
- return gpx_segment
61
+
62
+ return rdf_description_by_path
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+ import abc
4
+ from pathlib import Path
5
+
6
+ from ... import types
7
+
8
+
9
+ class BaseVideoExtractor(abc.ABC):
10
+ """
11
+ Extracts metadata from a video file.
12
+ """
13
+
14
+ def __init__(self, video_path: Path):
15
+ self.video_path = video_path
16
+
17
+ def extract(self) -> types.VideoMetadataOrError:
18
+ raise NotImplementedError
@@ -0,0 +1,70 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ import typing as T
5
+ from pathlib import Path
6
+ from xml.etree import ElementTree as ET
7
+
8
+ if sys.version_info >= (3, 12):
9
+ from typing import override
10
+ else:
11
+ from typing_extensions import override
12
+
13
+ from ... import exceptions, exiftool_read_video, geo, telemetry, types, utils
14
+ from ...gpmf import gpmf_gps_filter
15
+ from .base import BaseVideoExtractor
16
+
17
+
18
+ class VideoExifToolExtractor(BaseVideoExtractor):
19
+ def __init__(self, video_path: Path, element: ET.Element):
20
+ super().__init__(video_path)
21
+ self.element = element
22
+
23
+ @override
24
+ def extract(self) -> types.VideoMetadataOrError:
25
+ exif = exiftool_read_video.ExifToolReadVideo(ET.ElementTree(self.element))
26
+
27
+ make = exif.extract_make()
28
+ model = exif.extract_model()
29
+
30
+ is_gopro = make is not None and make.upper() in ["GOPRO"]
31
+
32
+ points = exif.extract_gps_track()
33
+
34
+ # ExifTool has no idea if GPS is not found or found but empty
35
+ if is_gopro:
36
+ if not points:
37
+ raise exceptions.MapillaryGPXEmptyError("Empty GPS data found")
38
+
39
+ # ExifTool (since 13.04) converts GPSSpeed for GoPro to km/h, so here we convert it back to m/s
40
+ for p in points:
41
+ if isinstance(p, telemetry.GPSPoint) and p.ground_speed is not None:
42
+ p.ground_speed = p.ground_speed / 3.6
43
+
44
+ if isinstance(points[0], telemetry.GPSPoint):
45
+ points = T.cast(
46
+ T.List[geo.Point],
47
+ gpmf_gps_filter.remove_noisy_points(
48
+ T.cast(T.List[telemetry.GPSPoint], points)
49
+ ),
50
+ )
51
+ if not points:
52
+ raise exceptions.MapillaryGPSNoiseError("GPS is too noisy")
53
+
54
+ if not points:
55
+ raise exceptions.MapillaryVideoGPSNotFoundError(
56
+ "No GPS data found from the video"
57
+ )
58
+
59
+ filetype = types.FileType.GOPRO if is_gopro else types.FileType.VIDEO
60
+
61
+ video_metadata = types.VideoMetadata(
62
+ self.video_path,
63
+ filesize=utils.get_file_size(self.video_path),
64
+ filetype=filetype,
65
+ points=points,
66
+ make=make,
67
+ model=model,
68
+ )
69
+
70
+ return video_metadata
@@ -1,51 +1,84 @@
1
+ from __future__ import annotations
2
+
3
+ import dataclasses
1
4
  import datetime
2
5
  import logging
6
+ import sys
3
7
  import typing as T
8
+ from pathlib import Path
4
9
 
5
- from ... import geo, telemetry
6
- from ...geotag import geotag_images_from_gpx_file
7
- from .base_parser import BaseParser
8
- from .generic_video_parser import GenericVideoParser
10
+ if sys.version_info >= (3, 12):
11
+ from typing import override
12
+ else:
13
+ from typing_extensions import override
9
14
 
15
+ from ... import geo, telemetry, types
16
+ from ..utils import parse_gpx
17
+ from .base import BaseVideoExtractor
18
+ from .native import NativeVideoExtractor
10
19
 
11
- LOG = logging.getLogger(__name__)
12
20
 
21
+ LOG = logging.getLogger(__name__)
13
22
 
14
- class GpxParser(BaseParser):
15
- default_source_pattern = "%g.gpx"
16
- parser_label = "gpx"
17
23
 
18
- def extract_points(self) -> T.Sequence[geo.Point]:
19
- path = self.geotag_source_path
20
- if not path:
21
- return []
24
+ class GPXVideoExtractor(BaseVideoExtractor):
25
+ def __init__(self, video_path: Path, gpx_path: Path):
26
+ self.video_path = video_path
27
+ self.gpx_path = gpx_path
22
28
 
29
+ @override
30
+ def extract(self) -> types.VideoMetadataOrError:
23
31
  try:
24
- gpx_tracks = geotag_images_from_gpx_file.parse_gpx(path)
32
+ gpx_tracks = parse_gpx(self.gpx_path)
25
33
  except Exception as ex:
26
34
  raise RuntimeError(
27
- f"Error parsing GPX {path}: {ex.__class__.__name__}: {ex}"
35
+ f"Error parsing GPX {self.gpx_path}: {ex.__class__.__name__}: {ex}"
28
36
  )
29
37
 
30
38
  if 1 < len(gpx_tracks):
31
39
  LOG.warning(
32
40
  "Found %s tracks in the GPX file %s. Will merge points in all the tracks as a single track for interpolation",
33
41
  len(gpx_tracks),
34
- self.videoPath,
42
+ self.gpx_path,
35
43
  )
36
44
 
37
45
  gpx_points: T.Sequence[geo.Point] = sum(gpx_tracks, [])
38
- if not gpx_points:
39
- return gpx_points
40
46
 
41
- offset = self._synx_gpx_by_first_gps_timestamp(gpx_points)
47
+ native_extractor = NativeVideoExtractor(self.video_path)
48
+
49
+ video_metadata_or_error = native_extractor.extract()
50
+
51
+ if isinstance(video_metadata_or_error, types.ErrorMetadata):
52
+ self._rebase_times(gpx_points)
53
+ return types.VideoMetadata(
54
+ filename=video_metadata_or_error.filename,
55
+ filetype=video_metadata_or_error.filetype or types.FileType.VIDEO,
56
+ points=gpx_points,
57
+ )
58
+
59
+ video_metadata = video_metadata_or_error
60
+
61
+ offset = self._synx_gpx_by_first_gps_timestamp(
62
+ gpx_points, video_metadata.points
63
+ )
42
64
 
43
65
  self._rebase_times(gpx_points, offset=offset)
44
66
 
45
- return gpx_points
67
+ return dataclasses.replace(video_metadata_or_error, points=gpx_points)
68
+
69
+ @staticmethod
70
+ def _rebase_times(points: T.Sequence[geo.Point], offset: float = 0.0):
71
+ """
72
+ Make point times start from 0
73
+ """
74
+ if points:
75
+ first_timestamp = points[0].time
76
+ for p in points:
77
+ p.time = (p.time - first_timestamp) + offset
78
+ return points
46
79
 
47
80
  def _synx_gpx_by_first_gps_timestamp(
48
- self, gpx_points: T.Sequence[geo.Point]
81
+ self, gpx_points: T.Sequence[geo.Point], video_gps_points: T.Sequence[geo.Point]
49
82
  ) -> float:
50
83
  offset: float = 0.0
51
84
 
@@ -57,19 +90,14 @@ class GpxParser(BaseParser):
57
90
  )
58
91
  LOG.info("First GPX timestamp: %s", first_gpx_dt)
59
92
 
60
- # Extract first GPS timestamp (if found) for synchronization
61
- # Use an empty dictionary to force video parsers to extract make/model from the video metadata itself
62
- parser = GenericVideoParser(self.videoPath, self.options, {})
63
- gps_points = parser.extract_points()
64
-
65
- if not gps_points:
93
+ if not video_gps_points:
66
94
  LOG.warning(
67
95
  "Skip GPX synchronization because no GPS found in video %s",
68
- self.videoPath,
96
+ self.video_path,
69
97
  )
70
98
  return offset
71
99
 
72
- first_gps_point = gps_points[0]
100
+ first_gps_point = video_gps_points[0]
73
101
  if isinstance(first_gps_point, telemetry.GPSPoint):
74
102
  if first_gps_point.epoch_time is not None:
75
103
  first_gps_dt = datetime.datetime.fromtimestamp(
@@ -92,17 +120,7 @@ class GpxParser(BaseParser):
92
120
  else:
93
121
  LOG.warning(
94
122
  "Skip GPX synchronization because no GPS epoch time found in video %s",
95
- self.videoPath,
123
+ self.video_path,
96
124
  )
97
125
 
98
126
  return offset
99
-
100
- def extract_make(self) -> T.Optional[str]:
101
- # Use an empty dictionary to force video parsers to extract make/model from the video metadata itself
102
- parser = GenericVideoParser(self.videoPath, self.options, {})
103
- return parser.extract_make()
104
-
105
- def extract_model(self) -> T.Optional[str]:
106
- # Use an empty dictionary to force video parsers to extract make/model from the video metadata itself
107
- parser = GenericVideoParser(self.videoPath, self.options, {})
108
- return parser.extract_model()
@@ -0,0 +1,157 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ import typing as T
5
+ from pathlib import Path
6
+
7
+ if sys.version_info >= (3, 12):
8
+ from typing import override
9
+ else:
10
+ from typing_extensions import override
11
+
12
+ from ... import blackvue_parser, exceptions, geo, telemetry, types, utils
13
+ from ...camm import camm_parser
14
+ from ...gpmf import gpmf_gps_filter, gpmf_parser
15
+ from .base import BaseVideoExtractor
16
+
17
+
18
+ class GoProVideoExtractor(BaseVideoExtractor):
19
+ @override
20
+ def extract(self) -> types.VideoMetadataOrError:
21
+ with self.video_path.open("rb") as fp:
22
+ gopro_info = gpmf_parser.extract_gopro_info(fp)
23
+
24
+ if gopro_info is None:
25
+ raise exceptions.MapillaryVideoGPSNotFoundError(
26
+ "No GPS data found from the video"
27
+ )
28
+
29
+ gps_points = gopro_info.gps
30
+ assert gps_points is not None, "must have GPS data extracted"
31
+ if not gps_points:
32
+ # Instead of raising an exception, return error metadata to tell the file type
33
+ ex: exceptions.MapillaryDescriptionError = (
34
+ exceptions.MapillaryGPXEmptyError("Empty GPS data found")
35
+ )
36
+ return types.describe_error_metadata(
37
+ ex, self.video_path, filetype=types.FileType.GOPRO
38
+ )
39
+
40
+ gps_points = T.cast(
41
+ T.List[telemetry.GPSPoint], gpmf_gps_filter.remove_noisy_points(gps_points)
42
+ )
43
+ if not gps_points:
44
+ # Instead of raising an exception, return error metadata to tell the file type
45
+ ex = exceptions.MapillaryGPSNoiseError("GPS is too noisy")
46
+ return types.describe_error_metadata(
47
+ ex, self.video_path, filetype=types.FileType.GOPRO
48
+ )
49
+
50
+ video_metadata = types.VideoMetadata(
51
+ filename=self.video_path,
52
+ filesize=utils.get_file_size(self.video_path),
53
+ filetype=types.FileType.GOPRO,
54
+ points=T.cast(T.List[geo.Point], gps_points),
55
+ make=gopro_info.make,
56
+ model=gopro_info.model,
57
+ )
58
+
59
+ return video_metadata
60
+
61
+
62
+ class CAMMVideoExtractor(BaseVideoExtractor):
63
+ @override
64
+ def extract(self) -> types.VideoMetadataOrError:
65
+ with self.video_path.open("rb") as fp:
66
+ camm_info = camm_parser.extract_camm_info(fp)
67
+
68
+ if camm_info is None:
69
+ raise exceptions.MapillaryVideoGPSNotFoundError(
70
+ "No GPS data found from the video"
71
+ )
72
+
73
+ if not camm_info.gps and not camm_info.mini_gps:
74
+ # Instead of raising an exception, return error metadata to tell the file type
75
+ ex: exceptions.MapillaryDescriptionError = (
76
+ exceptions.MapillaryGPXEmptyError("Empty GPS data found")
77
+ )
78
+ return types.describe_error_metadata(
79
+ ex, self.video_path, filetype=types.FileType.CAMM
80
+ )
81
+
82
+ return types.VideoMetadata(
83
+ filename=self.video_path,
84
+ filesize=utils.get_file_size(self.video_path),
85
+ filetype=types.FileType.CAMM,
86
+ points=T.cast(T.List[geo.Point], camm_info.gps or camm_info.mini_gps),
87
+ make=camm_info.make,
88
+ model=camm_info.model,
89
+ )
90
+
91
+
92
+ class BlackVueVideoExtractor(BaseVideoExtractor):
93
+ @override
94
+ def extract(self) -> types.VideoMetadataOrError:
95
+ with self.video_path.open("rb") as fp:
96
+ blackvue_info = blackvue_parser.extract_blackvue_info(fp)
97
+
98
+ if blackvue_info is None:
99
+ raise exceptions.MapillaryVideoGPSNotFoundError(
100
+ "No GPS data found from the video"
101
+ )
102
+
103
+ if not blackvue_info.gps:
104
+ # Instead of raising an exception, return error metadata to tell the file type
105
+ ex: exceptions.MapillaryDescriptionError = (
106
+ exceptions.MapillaryGPXEmptyError("Empty GPS data found")
107
+ )
108
+ return types.describe_error_metadata(
109
+ ex, self.video_path, filetype=types.FileType.BLACKVUE
110
+ )
111
+
112
+ video_metadata = types.VideoMetadata(
113
+ filename=self.video_path,
114
+ filesize=utils.get_file_size(self.video_path),
115
+ filetype=types.FileType.BLACKVUE,
116
+ points=blackvue_info.gps or [],
117
+ make=blackvue_info.make,
118
+ model=blackvue_info.model,
119
+ )
120
+
121
+ return video_metadata
122
+
123
+
124
+ class NativeVideoExtractor(BaseVideoExtractor):
125
+ def __init__(self, video_path: Path, filetypes: set[types.FileType] | None = None):
126
+ super().__init__(video_path)
127
+ self.filetypes = filetypes
128
+
129
+ @override
130
+ def extract(self) -> types.VideoMetadataOrError:
131
+ ft = self.filetypes
132
+ extractor: BaseVideoExtractor
133
+
134
+ if ft is None or types.FileType.VIDEO in ft or types.FileType.GOPRO in ft:
135
+ extractor = GoProVideoExtractor(self.video_path)
136
+ try:
137
+ return extractor.extract()
138
+ except exceptions.MapillaryVideoGPSNotFoundError:
139
+ pass
140
+
141
+ if ft is None or types.FileType.VIDEO in ft or types.FileType.CAMM in ft:
142
+ extractor = CAMMVideoExtractor(self.video_path)
143
+ try:
144
+ return extractor.extract()
145
+ except exceptions.MapillaryVideoGPSNotFoundError:
146
+ pass
147
+
148
+ if ft is None or types.FileType.VIDEO in ft or types.FileType.BLACKVUE in ft:
149
+ extractor = BlackVueVideoExtractor(self.video_path)
150
+ try:
151
+ return extractor.extract()
152
+ except exceptions.MapillaryVideoGPSNotFoundError:
153
+ pass
154
+
155
+ raise exceptions.MapillaryVideoGPSNotFoundError(
156
+ "No GPS data found from the video"
157
+ )