mapillary-tools 0.13.3__py3-none-any.whl → 0.14.0__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 (87) hide show
  1. mapillary_tools/__init__.py +1 -1
  2. mapillary_tools/api_v4.py +198 -55
  3. mapillary_tools/authenticate.py +326 -64
  4. mapillary_tools/blackvue_parser.py +195 -0
  5. mapillary_tools/camm/camm_builder.py +55 -97
  6. mapillary_tools/camm/camm_parser.py +429 -181
  7. mapillary_tools/commands/__main__.py +10 -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 +18 -5
  11. mapillary_tools/commands/sample_video.py +2 -3
  12. mapillary_tools/commands/upload.py +44 -13
  13. mapillary_tools/commands/video_process_and_upload.py +19 -5
  14. mapillary_tools/config.py +65 -26
  15. mapillary_tools/constants.py +141 -18
  16. mapillary_tools/exceptions.py +37 -34
  17. mapillary_tools/exif_read.py +221 -116
  18. mapillary_tools/exif_write.py +10 -8
  19. mapillary_tools/exiftool_read.py +33 -42
  20. mapillary_tools/exiftool_read_video.py +97 -47
  21. mapillary_tools/exiftool_runner.py +57 -0
  22. mapillary_tools/ffmpeg.py +417 -242
  23. mapillary_tools/geo.py +158 -118
  24. mapillary_tools/geotag/__init__.py +0 -1
  25. mapillary_tools/geotag/base.py +147 -0
  26. mapillary_tools/geotag/factory.py +307 -0
  27. mapillary_tools/geotag/geotag_images_from_exif.py +14 -131
  28. mapillary_tools/geotag/geotag_images_from_exiftool.py +136 -85
  29. mapillary_tools/geotag/geotag_images_from_gpx.py +60 -124
  30. mapillary_tools/geotag/geotag_images_from_gpx_file.py +13 -126
  31. mapillary_tools/geotag/geotag_images_from_nmea_file.py +4 -5
  32. mapillary_tools/geotag/geotag_images_from_video.py +88 -51
  33. mapillary_tools/geotag/geotag_videos_from_exiftool.py +123 -0
  34. mapillary_tools/geotag/geotag_videos_from_gpx.py +52 -0
  35. mapillary_tools/geotag/geotag_videos_from_video.py +20 -185
  36. mapillary_tools/geotag/image_extractors/base.py +18 -0
  37. mapillary_tools/geotag/image_extractors/exif.py +60 -0
  38. mapillary_tools/geotag/image_extractors/exiftool.py +18 -0
  39. mapillary_tools/geotag/options.py +182 -0
  40. mapillary_tools/geotag/utils.py +52 -16
  41. mapillary_tools/geotag/video_extractors/base.py +18 -0
  42. mapillary_tools/geotag/video_extractors/exiftool.py +70 -0
  43. mapillary_tools/geotag/video_extractors/gpx.py +116 -0
  44. mapillary_tools/geotag/video_extractors/native.py +160 -0
  45. mapillary_tools/{geotag → gpmf}/gpmf_parser.py +205 -182
  46. mapillary_tools/{geotag → gpmf}/gps_filter.py +5 -3
  47. mapillary_tools/history.py +134 -20
  48. mapillary_tools/mp4/construct_mp4_parser.py +17 -10
  49. mapillary_tools/mp4/io_utils.py +0 -1
  50. mapillary_tools/mp4/mp4_sample_parser.py +36 -28
  51. mapillary_tools/mp4/simple_mp4_builder.py +10 -9
  52. mapillary_tools/mp4/simple_mp4_parser.py +13 -22
  53. mapillary_tools/process_geotag_properties.py +184 -414
  54. mapillary_tools/process_sequence_properties.py +594 -225
  55. mapillary_tools/sample_video.py +20 -26
  56. mapillary_tools/serializer/description.py +587 -0
  57. mapillary_tools/serializer/gpx.py +132 -0
  58. mapillary_tools/telemetry.py +26 -13
  59. mapillary_tools/types.py +98 -611
  60. mapillary_tools/upload.py +411 -387
  61. mapillary_tools/upload_api_v4.py +167 -142
  62. mapillary_tools/uploader.py +804 -284
  63. mapillary_tools/utils.py +49 -18
  64. {mapillary_tools-0.13.3.dist-info → mapillary_tools-0.14.0.dist-info}/METADATA +93 -35
  65. mapillary_tools-0.14.0.dist-info/RECORD +75 -0
  66. {mapillary_tools-0.13.3.dist-info → mapillary_tools-0.14.0.dist-info}/WHEEL +1 -1
  67. mapillary_tools/geotag/blackvue_parser.py +0 -118
  68. mapillary_tools/geotag/geotag_from_generic.py +0 -22
  69. mapillary_tools/geotag/geotag_images_from_exiftool_both_image_and_video.py +0 -93
  70. mapillary_tools/geotag/geotag_videos_from_exiftool_video.py +0 -145
  71. mapillary_tools/video_data_extraction/cli_options.py +0 -22
  72. mapillary_tools/video_data_extraction/extract_video_data.py +0 -176
  73. mapillary_tools/video_data_extraction/extractors/base_parser.py +0 -75
  74. mapillary_tools/video_data_extraction/extractors/blackvue_parser.py +0 -34
  75. mapillary_tools/video_data_extraction/extractors/camm_parser.py +0 -38
  76. mapillary_tools/video_data_extraction/extractors/exiftool_runtime_parser.py +0 -71
  77. mapillary_tools/video_data_extraction/extractors/exiftool_xml_parser.py +0 -53
  78. mapillary_tools/video_data_extraction/extractors/generic_video_parser.py +0 -52
  79. mapillary_tools/video_data_extraction/extractors/gopro_parser.py +0 -43
  80. mapillary_tools/video_data_extraction/extractors/gpx_parser.py +0 -108
  81. mapillary_tools/video_data_extraction/extractors/nmea_parser.py +0 -24
  82. mapillary_tools/video_data_extraction/video_data_parser_factory.py +0 -39
  83. mapillary_tools-0.13.3.dist-info/RECORD +0 -75
  84. /mapillary_tools/{geotag → gpmf}/gpmf_gps_filter.py +0 -0
  85. {mapillary_tools-0.13.3.dist-info → mapillary_tools-0.14.0.dist-info}/entry_points.txt +0 -0
  86. {mapillary_tools-0.13.3.dist-info → mapillary_tools-0.14.0.dist-info/licenses}/LICENSE +0 -0
  87. {mapillary_tools-0.13.3.dist-info → mapillary_tools-0.14.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+ import contextlib
4
+ import xml.etree.ElementTree as ET
5
+ from pathlib import Path
6
+
7
+ from ... import exiftool_read
8
+ from .exif import ImageEXIFExtractor
9
+
10
+
11
+ class ImageExifToolExtractor(ImageEXIFExtractor):
12
+ def __init__(self, image_path: Path, element: ET.Element):
13
+ super().__init__(image_path)
14
+ self.element = element
15
+
16
+ @contextlib.contextmanager
17
+ def _exif_context(self):
18
+ yield exiftool_read.ExifToolRead(ET.ElementTree(self.element))
@@ -0,0 +1,182 @@
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(
64
+ "source_path", SourcePathOption(source_path=Path(v))
65
+ ).sourthe_path = Path(v)
66
+ elif k == "pattern":
67
+ kwargs.setdefault(
68
+ "source_path", SourcePathOption(pattern=v)
69
+ ).pattern = v
70
+ elif k == "interpolation_offset_time":
71
+ kwargs.setdefault(
72
+ "interpolation", InterpolationOption()
73
+ ).offset_time = v
74
+ elif k == "interpolation_use_gpx_start_time":
75
+ kwargs.setdefault(
76
+ "interpolation", InterpolationOption()
77
+ ).use_gpx_start_time = v
78
+
79
+ return cls(**kwargs)
80
+
81
+
82
+ @dataclasses.dataclass
83
+ class SourcePathOption:
84
+ pattern: str | None = None
85
+ source_path: Path | None = None
86
+
87
+ def __post_init__(self):
88
+ if self.source_path is None and self.pattern is None:
89
+ raise ValueError("Either pattern or source_path must be provided")
90
+
91
+ def resolve(self, path: Path) -> Path:
92
+ """
93
+ Resolve the source path or pattern against the given path.
94
+
95
+ Examples:
96
+ >>> from pathlib import Path
97
+ >>> opt = SourcePathOption(source_path=Path("/foo/bar.mp4"))
98
+ >>> opt.resolve(Path("/baz/qux.mp4"))
99
+ PosixPath('/foo/bar.mp4')
100
+
101
+ >>> opt = SourcePathOption(pattern="videos/%g_sub%e")
102
+ >>> opt.resolve(Path("/data/video1.mp4"))
103
+ PosixPath('/data/videos/video1_sub.mp4')
104
+
105
+ >>> opt = SourcePathOption(pattern="/abs/path/%f")
106
+ >>> opt.resolve(Path("/tmp/abc.mov"))
107
+ PosixPath('/abs/path/abc.mov')
108
+ """
109
+
110
+ if self.source_path is not None:
111
+ return self.source_path
112
+
113
+ assert self.pattern is not None, (
114
+ "either pattern or source_path must be provided"
115
+ )
116
+
117
+ # %f: the full video filename (foo.mp4)
118
+ # %g: the video filename without extension (foo)
119
+ # %e: the video filename extension (.mp4)
120
+ replaced = Path(
121
+ self.pattern.replace("%f", path.name)
122
+ .replace("%g", path.stem)
123
+ .replace("%e", path.suffix)
124
+ )
125
+
126
+ abs_path = (
127
+ replaced
128
+ if replaced.is_absolute()
129
+ else Path.joinpath(path.parent.resolve(), replaced)
130
+ ).resolve()
131
+
132
+ return abs_path
133
+
134
+
135
+ @dataclasses.dataclass
136
+ class InterpolationOption:
137
+ offset_time: float = 0.0
138
+ use_gpx_start_time: bool = False
139
+
140
+
141
+ SourceOptionSchema = {
142
+ "type": "object",
143
+ "properties": {
144
+ "source": {
145
+ "type": "string",
146
+ "enum": [s.value for s in SourceType] + list(SOURCE_TYPE_ALIAS.keys()),
147
+ },
148
+ "filetypes": {
149
+ "type": "array",
150
+ "items": {
151
+ "type": "string",
152
+ "enum": [t.value for t in types.FileType],
153
+ },
154
+ },
155
+ "source_path": {
156
+ "type": "string",
157
+ },
158
+ "pattern": {
159
+ "type": "string",
160
+ },
161
+ "num_processes": {
162
+ "type": "integer",
163
+ },
164
+ "interpolation_offset_time": {
165
+ "type": "number",
166
+ },
167
+ "interpolation_use_gpx_start_time": {
168
+ "type": "boolean",
169
+ },
170
+ },
171
+ "required": ["source"],
172
+ "additionalProperties": False,
173
+ }
174
+
175
+
176
+ def validate_option(instance):
177
+ jsonschema.validate(instance=instance, schema=SourceOptionSchema)
178
+
179
+
180
+ if __name__ == "__main__":
181
+ # python -m mapillary_tools.geotag.options > schema/geotag_source_option.json
182
+ 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.VideoMetadata:
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.VideoMetadata:
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
@@ -0,0 +1,116 @@
1
+ from __future__ import annotations
2
+
3
+ import dataclasses
4
+ import enum
5
+ import logging
6
+ import sys
7
+ import typing as T
8
+ from pathlib import Path
9
+
10
+ if sys.version_info >= (3, 12):
11
+ from typing import override
12
+ else:
13
+ from typing_extensions import override
14
+
15
+ from ... import exceptions, geo, telemetry, types
16
+ from ..utils import parse_gpx
17
+ from .base import BaseVideoExtractor
18
+ from .native import NativeVideoExtractor
19
+
20
+
21
+ LOG = logging.getLogger(__name__)
22
+
23
+
24
+ class SyncMode(enum.Enum):
25
+ # Sync by video GPS timestamps if found, otherwise rebase
26
+ SYNC = "sync"
27
+ # Sync by video GPS timestamps, and throw if not found
28
+ STRICT_SYNC = "strict_sync"
29
+ # Rebase all GPX timestamps to start from 0
30
+ REBASE = "rebase"
31
+
32
+
33
+ class GPXVideoExtractor(BaseVideoExtractor):
34
+ def __init__(
35
+ self, video_path: Path, gpx_path: Path, sync_mode: SyncMode = SyncMode.SYNC
36
+ ):
37
+ self.video_path = video_path
38
+ self.gpx_path = gpx_path
39
+ self.sync_mode = sync_mode
40
+
41
+ @override
42
+ def extract(self) -> types.VideoMetadata:
43
+ gpx_tracks = parse_gpx(self.gpx_path)
44
+
45
+ if 1 < len(gpx_tracks):
46
+ LOG.warning(
47
+ f"Found {len(gpx_tracks)} tracks in the GPX file {self.gpx_path}. Will merge points in all the tracks as a single track for interpolation"
48
+ )
49
+
50
+ gpx_points: T.Sequence[geo.Point] = sum(gpx_tracks, [])
51
+
52
+ native_extractor = NativeVideoExtractor(self.video_path)
53
+
54
+ try:
55
+ native_video_metadata = native_extractor.extract()
56
+ except exceptions.MapillaryVideoGPSNotFoundError as ex:
57
+ if self.sync_mode is SyncMode.STRICT_SYNC:
58
+ raise ex
59
+ self._rebase_times(gpx_points)
60
+ return types.VideoMetadata(
61
+ filename=self.video_path,
62
+ filetype=types.FileType.VIDEO,
63
+ points=gpx_points,
64
+ )
65
+
66
+ if self.sync_mode is SyncMode.REBASE:
67
+ self._rebase_times(gpx_points)
68
+ else:
69
+ offset = self._gpx_offset(gpx_points, native_video_metadata.points)
70
+ self._rebase_times(gpx_points, offset=offset)
71
+
72
+ return dataclasses.replace(native_video_metadata, points=gpx_points)
73
+
74
+ @classmethod
75
+ def _rebase_times(cls, points: T.Sequence[geo.Point], offset: float = 0.0) -> None:
76
+ """
77
+ Rebase point times to start from **offset**
78
+ """
79
+ if points:
80
+ first_timestamp = points[0].time
81
+ for p in points:
82
+ p.time = (p.time - first_timestamp) + offset
83
+
84
+ @classmethod
85
+ def _gpx_offset(
86
+ cls, gpx_points: T.Sequence[geo.Point], video_gps_points: T.Sequence[geo.Point]
87
+ ) -> float:
88
+ """
89
+ Calculate the offset that needs to be applied to the GPX points to sync with the video GPS points.
90
+
91
+ >>> gpx_points = [geo.Point(time=5, lat=1, lon=1, alt=None, angle=None)]
92
+ >>> GPXVideoExtractor._gpx_offset(gpx_points, gpx_points)
93
+ 0.0
94
+ >>> GPXVideoExtractor._gpx_offset(gpx_points, [])
95
+ 0.0
96
+ >>> GPXVideoExtractor._gpx_offset([], gpx_points)
97
+ 0.0
98
+ """
99
+ offset: float = 0.0
100
+
101
+ if not gpx_points or not video_gps_points:
102
+ return offset
103
+
104
+ gps_epoch_time: float | None = None
105
+ gps_point = video_gps_points[0]
106
+ if isinstance(gps_point, telemetry.GPSPoint):
107
+ if gps_point.epoch_time is not None:
108
+ gps_epoch_time = gps_point.epoch_time
109
+ elif isinstance(gps_point, telemetry.CAMMGPSPoint):
110
+ if gps_point.time_gps_epoch is not None:
111
+ gps_epoch_time = gps_point.time_gps_epoch
112
+
113
+ if gps_epoch_time is not None:
114
+ offset = gpx_points[0].time - gps_epoch_time
115
+
116
+ return offset
@@ -0,0 +1,160 @@
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 ...mp4 import construct_mp4_parser, simple_mp4_parser
16
+ from .base import BaseVideoExtractor
17
+
18
+
19
+ class GoProVideoExtractor(BaseVideoExtractor):
20
+ @override
21
+ def extract(self) -> types.VideoMetadata:
22
+ with self.video_path.open("rb") as fp:
23
+ gopro_info = gpmf_parser.extract_gopro_info(fp)
24
+
25
+ if gopro_info is None:
26
+ raise exceptions.MapillaryVideoGPSNotFoundError(
27
+ "No GPS data found from the video"
28
+ )
29
+
30
+ gps_points = gopro_info.gps
31
+ assert gps_points is not None, "must have GPS data extracted"
32
+ if not gps_points:
33
+ raise exceptions.MapillaryGPXEmptyError("Empty GPS data found")
34
+
35
+ gps_points = T.cast(
36
+ T.List[telemetry.GPSPoint], gpmf_gps_filter.remove_noisy_points(gps_points)
37
+ )
38
+ if not gps_points:
39
+ raise exceptions.MapillaryGPSNoiseError("GPS is too noisy")
40
+
41
+ video_metadata = types.VideoMetadata(
42
+ filename=self.video_path,
43
+ filesize=utils.get_file_size(self.video_path),
44
+ filetype=types.FileType.GOPRO,
45
+ points=T.cast(T.List[geo.Point], gps_points),
46
+ make=gopro_info.make,
47
+ model=gopro_info.model,
48
+ )
49
+
50
+ return video_metadata
51
+
52
+
53
+ class CAMMVideoExtractor(BaseVideoExtractor):
54
+ @override
55
+ def extract(self) -> types.VideoMetadata:
56
+ with self.video_path.open("rb") as fp:
57
+ camm_info = camm_parser.extract_camm_info(fp)
58
+
59
+ if camm_info is None:
60
+ raise exceptions.MapillaryVideoGPSNotFoundError(
61
+ "No GPS data found from the video"
62
+ )
63
+
64
+ if not camm_info.gps and not camm_info.mini_gps:
65
+ raise exceptions.MapillaryGPXEmptyError("Empty GPS data found")
66
+
67
+ return types.VideoMetadata(
68
+ filename=self.video_path,
69
+ filesize=utils.get_file_size(self.video_path),
70
+ filetype=types.FileType.CAMM,
71
+ points=T.cast(T.List[geo.Point], camm_info.gps or camm_info.mini_gps),
72
+ make=camm_info.make,
73
+ model=camm_info.model,
74
+ )
75
+
76
+
77
+ class BlackVueVideoExtractor(BaseVideoExtractor):
78
+ @override
79
+ def extract(self) -> types.VideoMetadata:
80
+ with self.video_path.open("rb") as fp:
81
+ blackvue_info = blackvue_parser.extract_blackvue_info(fp)
82
+
83
+ if blackvue_info is None:
84
+ raise exceptions.MapillaryVideoGPSNotFoundError(
85
+ "No GPS data found from the video"
86
+ )
87
+
88
+ if not blackvue_info.gps:
89
+ raise exceptions.MapillaryGPXEmptyError("Empty GPS data found")
90
+
91
+ video_metadata = types.VideoMetadata(
92
+ filename=self.video_path,
93
+ filesize=utils.get_file_size(self.video_path),
94
+ filetype=types.FileType.BLACKVUE,
95
+ points=blackvue_info.gps,
96
+ make=blackvue_info.make,
97
+ model=blackvue_info.model,
98
+ )
99
+
100
+ return video_metadata
101
+
102
+
103
+ class NativeVideoExtractor(BaseVideoExtractor):
104
+ def __init__(self, video_path: Path, filetypes: set[types.FileType] | None = None):
105
+ super().__init__(video_path)
106
+ self.filetypes = filetypes
107
+
108
+ @override
109
+ def extract(self) -> types.VideoMetadata:
110
+ ft = self.filetypes
111
+ extractor: BaseVideoExtractor
112
+
113
+ if ft is None or types.FileType.VIDEO in ft or types.FileType.GOPRO in ft:
114
+ extractor = GoProVideoExtractor(self.video_path)
115
+ try:
116
+ return extractor.extract()
117
+ except simple_mp4_parser.BoxNotFoundError as ex:
118
+ raise exceptions.MapillaryInvalidVideoError(
119
+ f"Invalid video: {ex}"
120
+ ) from ex
121
+ except construct_mp4_parser.BoxNotFoundError as ex:
122
+ raise exceptions.MapillaryInvalidVideoError(
123
+ f"Invalid video: {ex}"
124
+ ) from ex
125
+ except exceptions.MapillaryVideoGPSNotFoundError:
126
+ pass
127
+
128
+ if ft is None or types.FileType.VIDEO in ft or types.FileType.CAMM in ft:
129
+ extractor = CAMMVideoExtractor(self.video_path)
130
+ try:
131
+ return extractor.extract()
132
+ except simple_mp4_parser.BoxNotFoundError as ex:
133
+ raise exceptions.MapillaryInvalidVideoError(
134
+ f"Invalid video: {ex}"
135
+ ) from ex
136
+ except construct_mp4_parser.BoxNotFoundError as ex:
137
+ raise exceptions.MapillaryInvalidVideoError(
138
+ f"Invalid video: {ex}"
139
+ ) from ex
140
+ except exceptions.MapillaryVideoGPSNotFoundError:
141
+ pass
142
+
143
+ if ft is None or types.FileType.VIDEO in ft or types.FileType.BLACKVUE in ft:
144
+ extractor = BlackVueVideoExtractor(self.video_path)
145
+ try:
146
+ return extractor.extract()
147
+ except simple_mp4_parser.BoxNotFoundError as ex:
148
+ raise exceptions.MapillaryInvalidVideoError(
149
+ f"Invalid video: {ex}"
150
+ ) from ex
151
+ except construct_mp4_parser.BoxNotFoundError as ex:
152
+ raise exceptions.MapillaryInvalidVideoError(
153
+ f"Invalid video: {ex}"
154
+ ) from ex
155
+ except exceptions.MapillaryVideoGPSNotFoundError:
156
+ pass
157
+
158
+ raise exceptions.MapillaryVideoGPSNotFoundError(
159
+ "No GPS data found from the video"
160
+ )