mapillary-tools 0.14.0a1__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 (67) hide show
  1. mapillary_tools/__init__.py +1 -1
  2. mapillary_tools/api_v4.py +4 -4
  3. mapillary_tools/camm/camm_parser.py +5 -5
  4. mapillary_tools/commands/__main__.py +1 -2
  5. mapillary_tools/config.py +7 -5
  6. mapillary_tools/constants.py +1 -2
  7. mapillary_tools/exceptions.py +1 -1
  8. mapillary_tools/exif_read.py +65 -65
  9. mapillary_tools/exif_write.py +7 -7
  10. mapillary_tools/exiftool_read.py +23 -46
  11. mapillary_tools/exiftool_read_video.py +36 -34
  12. mapillary_tools/ffmpeg.py +24 -23
  13. mapillary_tools/geo.py +4 -21
  14. mapillary_tools/geotag/{geotag_from_generic.py → base.py} +32 -48
  15. mapillary_tools/geotag/factory.py +27 -34
  16. mapillary_tools/geotag/geotag_images_from_exif.py +15 -51
  17. mapillary_tools/geotag/geotag_images_from_exiftool.py +107 -59
  18. mapillary_tools/geotag/geotag_images_from_gpx.py +20 -10
  19. mapillary_tools/geotag/geotag_images_from_gpx_file.py +2 -34
  20. mapillary_tools/geotag/geotag_images_from_nmea_file.py +0 -3
  21. mapillary_tools/geotag/geotag_images_from_video.py +16 -14
  22. mapillary_tools/geotag/geotag_videos_from_exiftool.py +97 -0
  23. mapillary_tools/geotag/geotag_videos_from_gpx.py +14 -115
  24. mapillary_tools/geotag/geotag_videos_from_video.py +14 -147
  25. mapillary_tools/geotag/image_extractors/base.py +18 -0
  26. mapillary_tools/geotag/image_extractors/exif.py +60 -0
  27. mapillary_tools/geotag/image_extractors/exiftool.py +18 -0
  28. mapillary_tools/geotag/options.py +1 -0
  29. mapillary_tools/geotag/utils.py +62 -0
  30. mapillary_tools/geotag/video_extractors/base.py +18 -0
  31. mapillary_tools/geotag/video_extractors/exiftool.py +70 -0
  32. mapillary_tools/{video_data_extraction/extractors/gpx_parser.py → geotag/video_extractors/gpx.py} +57 -39
  33. mapillary_tools/geotag/video_extractors/native.py +157 -0
  34. mapillary_tools/gpmf/gpmf_parser.py +16 -16
  35. mapillary_tools/gpmf/gps_filter.py +5 -3
  36. mapillary_tools/history.py +4 -2
  37. mapillary_tools/mp4/construct_mp4_parser.py +9 -8
  38. mapillary_tools/mp4/mp4_sample_parser.py +27 -27
  39. mapillary_tools/mp4/simple_mp4_builder.py +10 -9
  40. mapillary_tools/mp4/simple_mp4_parser.py +13 -12
  41. mapillary_tools/process_geotag_properties.py +5 -7
  42. mapillary_tools/process_sequence_properties.py +40 -38
  43. mapillary_tools/sample_video.py +8 -8
  44. mapillary_tools/telemetry.py +6 -5
  45. mapillary_tools/types.py +33 -38
  46. mapillary_tools/utils.py +16 -18
  47. {mapillary_tools-0.14.0a1.dist-info → mapillary_tools-0.14.0a2.dist-info}/METADATA +1 -1
  48. mapillary_tools-0.14.0a2.dist-info/RECORD +72 -0
  49. mapillary_tools/geotag/__init__.py +0 -1
  50. mapillary_tools/geotag/geotag_images_from_exiftool_both_image_and_video.py +0 -77
  51. mapillary_tools/geotag/geotag_videos_from_exiftool_video.py +0 -151
  52. mapillary_tools/video_data_extraction/cli_options.py +0 -22
  53. mapillary_tools/video_data_extraction/extract_video_data.py +0 -157
  54. mapillary_tools/video_data_extraction/extractors/base_parser.py +0 -75
  55. mapillary_tools/video_data_extraction/extractors/blackvue_parser.py +0 -49
  56. mapillary_tools/video_data_extraction/extractors/camm_parser.py +0 -62
  57. mapillary_tools/video_data_extraction/extractors/exiftool_runtime_parser.py +0 -74
  58. mapillary_tools/video_data_extraction/extractors/exiftool_xml_parser.py +0 -52
  59. mapillary_tools/video_data_extraction/extractors/generic_video_parser.py +0 -52
  60. mapillary_tools/video_data_extraction/extractors/gopro_parser.py +0 -58
  61. mapillary_tools/video_data_extraction/extractors/nmea_parser.py +0 -24
  62. mapillary_tools/video_data_extraction/video_data_parser_factory.py +0 -39
  63. mapillary_tools-0.14.0a1.dist-info/RECORD +0 -78
  64. {mapillary_tools-0.14.0a1.dist-info → mapillary_tools-0.14.0a2.dist-info}/WHEEL +0 -0
  65. {mapillary_tools-0.14.0a1.dist-info → mapillary_tools-0.14.0a2.dist-info}/entry_points.txt +0 -0
  66. {mapillary_tools-0.14.0a1.dist-info → mapillary_tools-0.14.0a2.dist-info}/licenses/LICENSE +0 -0
  67. {mapillary_tools-0.14.0a1.dist-info → mapillary_tools-0.14.0a2.dist-info}/top_level.txt +0 -0
@@ -2,11 +2,17 @@ from __future__ import annotations
2
2
 
3
3
  import dataclasses
4
4
  import logging
5
+ import sys
5
6
  import typing as T
6
7
  from pathlib import Path
7
8
 
9
+ if sys.version_info >= (3, 12):
10
+ from typing import override
11
+ else:
12
+ from typing_extensions import override
13
+
8
14
  from .. import exceptions, geo, types
9
- from .geotag_from_generic import GeotagImagesFromGeneric
15
+ from .base import GeotagImagesFromGeneric
10
16
  from .geotag_images_from_exif import ImageEXIFExtractor
11
17
 
12
18
 
@@ -16,14 +22,13 @@ LOG = logging.getLogger(__name__)
16
22
  class GeotagImagesFromGPX(GeotagImagesFromGeneric):
17
23
  def __init__(
18
24
  self,
19
- image_paths: T.Sequence[Path],
20
25
  points: T.Sequence[geo.Point],
21
26
  use_gpx_start_time: bool = False,
22
27
  use_image_start_time: bool = False,
23
28
  offset_time: float = 0.0,
24
29
  num_processes: int | None = None,
25
30
  ):
26
- super().__init__(image_paths, num_processes=num_processes)
31
+ super().__init__(num_processes=num_processes)
27
32
  self.points = points
28
33
  self.use_gpx_start_time = use_gpx_start_time
29
34
  self.use_image_start_time = use_image_start_time
@@ -73,16 +78,21 @@ class GeotagImagesFromGPX(GeotagImagesFromGeneric):
73
78
  time=interpolated.time,
74
79
  )
75
80
 
76
- def _generate_image_extractors(self) -> T.Sequence[ImageEXIFExtractor]:
81
+ @override
82
+ def _generate_image_extractors(
83
+ self, image_paths: T.Sequence[Path]
84
+ ) -> T.Sequence[ImageEXIFExtractor]:
77
85
  return [
78
- ImageEXIFExtractor(path, skip_lonlat_error=True)
79
- for path in self.image_paths
86
+ ImageEXIFExtractor(path, skip_lonlat_error=True) for path in image_paths
80
87
  ]
81
88
 
82
- def to_description(self) -> list[types.ImageMetadataOrError]:
89
+ @override
90
+ def to_description(
91
+ self, image_paths: T.Sequence[Path]
92
+ ) -> list[types.ImageMetadataOrError]:
83
93
  final_metadatas: list[types.ImageMetadataOrError] = []
84
94
 
85
- image_metadata_or_errors = super().to_description()
95
+ image_metadata_or_errors = super().to_description(image_paths)
86
96
 
87
97
  image_metadatas, error_metadatas = types.separate_errors(
88
98
  image_metadata_or_errors
@@ -90,7 +100,7 @@ class GeotagImagesFromGPX(GeotagImagesFromGeneric):
90
100
  final_metadatas.extend(error_metadatas)
91
101
 
92
102
  if not image_metadatas:
93
- assert len(self.image_paths) == len(final_metadatas)
103
+ assert len(image_paths) == len(final_metadatas)
94
104
  return final_metadatas
95
105
 
96
106
  # Do not use point itself for comparison because point.angle or point.alt could be None
@@ -145,6 +155,6 @@ class GeotagImagesFromGPX(GeotagImagesFromGeneric):
145
155
  )
146
156
  final_metadatas.append(error_metadata)
147
157
 
148
- assert len(self.image_paths) == len(final_metadatas)
158
+ assert len(image_paths) == len(final_metadatas)
149
159
 
150
160
  return final_metadatas
@@ -1,12 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import logging
4
- import typing as T
5
4
  from pathlib import Path
6
5
 
7
- import gpxpy
8
-
9
- from .. import geo
6
+ from . import utils
10
7
  from .geotag_images_from_gpx import GeotagImagesFromGPX
11
8
 
12
9
 
@@ -16,14 +13,13 @@ LOG = logging.getLogger(__name__)
16
13
  class GeotagImagesFromGPXFile(GeotagImagesFromGPX):
17
14
  def __init__(
18
15
  self,
19
- image_paths: T.Sequence[Path],
20
16
  source_path: Path,
21
17
  use_gpx_start_time: bool = False,
22
18
  offset_time: float = 0.0,
23
19
  num_processes: int | None = None,
24
20
  ):
25
21
  try:
26
- tracks = parse_gpx(source_path)
22
+ tracks = utils.parse_gpx(source_path)
27
23
  except Exception as ex:
28
24
  raise RuntimeError(
29
25
  f"Error parsing GPX {source_path}: {ex.__class__.__name__}: {ex}"
@@ -37,36 +33,8 @@ class GeotagImagesFromGPXFile(GeotagImagesFromGPX):
37
33
  )
38
34
  points = sum(tracks, [])
39
35
  super().__init__(
40
- image_paths,
41
36
  points,
42
37
  use_gpx_start_time=use_gpx_start_time,
43
38
  offset_time=offset_time,
44
39
  num_processes=num_processes,
45
40
  )
46
-
47
-
48
- Track = T.List[geo.Point]
49
-
50
-
51
- def parse_gpx(gpx_file: Path) -> list[Track]:
52
- with gpx_file.open("r") as f:
53
- gpx = gpxpy.parse(f)
54
-
55
- tracks: list[Track] = []
56
-
57
- for track in gpx.tracks:
58
- for segment in track.segments:
59
- tracks.append([])
60
- for point in segment.points:
61
- if point.time is not None:
62
- tracks[-1].append(
63
- geo.Point(
64
- time=geo.as_unix_time(point.time),
65
- lat=point.latitude,
66
- lon=point.longitude,
67
- alt=point.elevation,
68
- angle=None,
69
- )
70
- )
71
-
72
- return tracks
@@ -1,7 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import datetime
4
- import typing as T
5
4
  from pathlib import Path
6
5
 
7
6
  import pynmea2
@@ -13,7 +12,6 @@ from .geotag_images_from_gpx import GeotagImagesFromGPX
13
12
  class GeotagImagesFromNMEAFile(GeotagImagesFromGPX):
14
13
  def __init__(
15
14
  self,
16
- image_paths: T.Sequence[Path],
17
15
  source_path: Path,
18
16
  use_gpx_start_time: bool = False,
19
17
  offset_time: float = 0.0,
@@ -21,7 +19,6 @@ class GeotagImagesFromNMEAFile(GeotagImagesFromGPX):
21
19
  ):
22
20
  points = get_lat_lon_time_from_nmea(source_path)
23
21
  super().__init__(
24
- image_paths,
25
22
  points,
26
23
  use_gpx_start_time=use_gpx_start_time,
27
24
  offset_time=offset_time,
@@ -1,12 +1,17 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import logging
4
+ import sys
4
5
  import typing as T
5
6
  from pathlib import Path
6
7
 
7
- from .. import types, utils
8
+ if sys.version_info >= (3, 12):
9
+ from typing import override
10
+ else:
11
+ from typing_extensions import override
8
12
 
9
- from .geotag_from_generic import GeotagImagesFromGeneric
13
+ from .. import types, utils
14
+ from .base import GeotagImagesFromGeneric
10
15
  from .geotag_images_from_gpx import GeotagImagesFromGPX
11
16
 
12
17
 
@@ -16,16 +21,18 @@ LOG = logging.getLogger(__name__)
16
21
  class GeotagImagesFromVideo(GeotagImagesFromGeneric):
17
22
  def __init__(
18
23
  self,
19
- image_paths: T.Sequence[Path],
20
24
  video_metadatas: T.Sequence[types.VideoMetadataOrError],
21
25
  offset_time: float = 0.0,
22
26
  num_processes: int | None = None,
23
27
  ):
24
- super().__init__(image_paths, num_processes=num_processes)
28
+ super().__init__(num_processes=num_processes)
25
29
  self.video_metadatas = video_metadatas
26
30
  self.offset_time = offset_time
27
31
 
28
- def to_description(self) -> list[types.ImageMetadataOrError]:
32
+ @override
33
+ def to_description(
34
+ self, image_paths: T.Sequence[Path]
35
+ ) -> list[types.ImageMetadataOrError]:
29
36
  # Will return this list
30
37
  final_image_metadatas: list[types.ImageMetadataOrError] = []
31
38
 
@@ -35,9 +42,7 @@ class GeotagImagesFromVideo(GeotagImagesFromGeneric):
35
42
 
36
43
  for video_error_metadata in video_error_metadatas:
37
44
  video_path = video_error_metadata.filename
38
- sample_paths = list(
39
- utils.filter_video_samples(self.image_paths, video_path)
40
- )
45
+ sample_paths = list(utils.filter_video_samples(image_paths, video_path))
41
46
  LOG.debug(
42
47
  "Found %d sample images from video %s with error: %s",
43
48
  len(sample_paths),
@@ -55,9 +60,7 @@ class GeotagImagesFromVideo(GeotagImagesFromGeneric):
55
60
  for video_metadata in video_metadatas:
56
61
  video_path = video_metadata.filename
57
62
 
58
- sample_paths = list(
59
- utils.filter_video_samples(self.image_paths, video_path)
60
- )
63
+ sample_paths = list(utils.filter_video_samples(image_paths, video_path))
61
64
  LOG.debug(
62
65
  "Found %d sample images from video %s",
63
66
  len(sample_paths),
@@ -65,7 +68,6 @@ class GeotagImagesFromVideo(GeotagImagesFromGeneric):
65
68
  )
66
69
 
67
70
  geotag = GeotagImagesFromGPX(
68
- sample_paths,
69
71
  video_metadata.points,
70
72
  use_gpx_start_time=False,
71
73
  use_image_start_time=True,
@@ -73,7 +75,7 @@ class GeotagImagesFromVideo(GeotagImagesFromGeneric):
73
75
  num_processes=self.num_processes,
74
76
  )
75
77
 
76
- image_metadatas = geotag.to_description()
78
+ image_metadatas = geotag.to_description(image_paths)
77
79
 
78
80
  for metadata in image_metadatas:
79
81
  if isinstance(metadata, types.ImageMetadata):
@@ -85,6 +87,6 @@ class GeotagImagesFromVideo(GeotagImagesFromGeneric):
85
87
  # NOTE: this method only geotags images that have a corresponding video,
86
88
  # so the number of image metadata objects returned might be less than
87
89
  # the number of the input image_paths
88
- assert len(final_image_metadatas) <= len(self.image_paths)
90
+ assert len(final_image_metadatas) <= len(image_paths)
89
91
 
90
92
  return final_image_metadatas
@@ -0,0 +1,97 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import sys
5
+ import typing as T
6
+ import xml.etree.ElementTree as ET
7
+ from pathlib import Path
8
+
9
+ if sys.version_info >= (3, 12):
10
+ from typing import override
11
+ else:
12
+ from typing_extensions import override
13
+
14
+ from .. import constants, exceptions, exiftool_read, types
15
+ from ..exiftool_runner import ExiftoolRunner
16
+ from .base import GeotagVideosFromGeneric
17
+ from .utils import index_rdf_description_by_path
18
+ from .video_extractors.exiftool import VideoExifToolExtractor
19
+
20
+ LOG = logging.getLogger(__name__)
21
+
22
+
23
+ class GeotagVideosFromExifToolXML(GeotagVideosFromGeneric):
24
+ def __init__(
25
+ self,
26
+ xml_path: Path,
27
+ num_processes: int | None = None,
28
+ ):
29
+ super().__init__(num_processes=num_processes)
30
+ self.xml_path = xml_path
31
+
32
+ @classmethod
33
+ def build_image_extractors(
34
+ cls,
35
+ rdf_by_path: dict[str, ET.Element],
36
+ video_paths: T.Iterable[Path],
37
+ ) -> list[VideoExifToolExtractor | types.ErrorMetadata]:
38
+ results: list[VideoExifToolExtractor | types.ErrorMetadata] = []
39
+
40
+ for path in video_paths:
41
+ rdf = rdf_by_path.get(exiftool_read.canonical_path(path))
42
+ if rdf is None:
43
+ ex = exceptions.MapillaryExifToolXMLNotFoundError(
44
+ "Cannot find the video in the ExifTool XML"
45
+ )
46
+ results.append(
47
+ types.describe_error_metadata(
48
+ ex, path, filetype=types.FileType.VIDEO
49
+ )
50
+ )
51
+ else:
52
+ results.append(VideoExifToolExtractor(path, rdf))
53
+
54
+ return results
55
+
56
+ @override
57
+ def _generate_video_extractors(
58
+ self, video_paths: T.Sequence[Path]
59
+ ) -> T.Sequence[VideoExifToolExtractor | types.ErrorMetadata]:
60
+ rdf_by_path = index_rdf_description_by_path([self.xml_path])
61
+ return self.build_image_extractors(rdf_by_path, video_paths)
62
+
63
+
64
+ class GeotagVideosFromExifToolRunner(GeotagVideosFromGeneric):
65
+ @override
66
+ def _generate_video_extractors(
67
+ self, video_paths: T.Sequence[Path]
68
+ ) -> T.Sequence[VideoExifToolExtractor | types.ErrorMetadata]:
69
+ runner = ExiftoolRunner(constants.EXIFTOOL_PATH)
70
+
71
+ LOG.debug(
72
+ "Extracting XML from %d videos with ExifTool command: %s",
73
+ len(video_paths),
74
+ " ".join(runner._build_args_read_stdin()),
75
+ )
76
+ try:
77
+ xml = runner.extract_xml(video_paths)
78
+ except FileNotFoundError as ex:
79
+ raise exceptions.MapillaryExiftoolNotFoundError(ex) from ex
80
+
81
+ try:
82
+ xml_element = ET.fromstring(xml)
83
+ except ET.ParseError as ex:
84
+ LOG.warning(
85
+ "Failed to parse ExifTool XML: %s",
86
+ str(ex),
87
+ exc_info=LOG.getEffectiveLevel() <= logging.DEBUG,
88
+ )
89
+ rdf_by_path = {}
90
+ else:
91
+ rdf_by_path = exiftool_read.index_rdf_description_by_path_from_xml_element(
92
+ xml_element
93
+ )
94
+
95
+ return GeotagVideosFromExifToolXML.build_image_extractors(
96
+ rdf_by_path, video_paths
97
+ )
@@ -1,140 +1,39 @@
1
1
  from __future__ import annotations
2
2
 
3
- import dataclasses
4
- import datetime
5
3
  import logging
6
-
4
+ import sys
7
5
  import typing as T
8
6
  from pathlib import Path
9
7
 
10
- from .. import geo, telemetry, types
8
+ if sys.version_info >= (3, 12):
9
+ from typing import override
10
+ else:
11
+ from typing_extensions import override
12
+
11
13
  from . import options
12
- from .geotag_from_generic import GenericVideoExtractor, GeotagVideosFromGeneric
13
- from .geotag_images_from_gpx_file import parse_gpx
14
- from .geotag_videos_from_video import NativeVideoExtractor
14
+ from .base import GeotagVideosFromGeneric
15
+ from .video_extractors.gpx import GPXVideoExtractor
15
16
 
16
17
 
17
18
  LOG = logging.getLogger(__name__)
18
19
 
19
20
 
20
- class GPXVideoExtractor(GenericVideoExtractor):
21
- def __init__(self, video_path: Path, gpx_path: Path):
22
- self.video_path = video_path
23
- self.gpx_path = gpx_path
24
-
25
- def extract(self) -> types.VideoMetadataOrError:
26
- try:
27
- gpx_tracks = parse_gpx(self.gpx_path)
28
- except Exception as ex:
29
- raise RuntimeError(
30
- f"Error parsing GPX {self.gpx_path}: {ex.__class__.__name__}: {ex}"
31
- )
32
-
33
- if 1 < len(gpx_tracks):
34
- LOG.warning(
35
- "Found %s tracks in the GPX file %s. Will merge points in all the tracks as a single track for interpolation",
36
- len(gpx_tracks),
37
- self.gpx_path,
38
- )
39
-
40
- gpx_points: T.Sequence[geo.Point] = sum(gpx_tracks, [])
41
-
42
- native_extractor = NativeVideoExtractor(self.video_path)
43
-
44
- video_metadata_or_error = native_extractor.extract()
45
-
46
- if isinstance(video_metadata_or_error, types.ErrorMetadata):
47
- self._rebase_times(gpx_points)
48
- return types.VideoMetadata(
49
- filename=video_metadata_or_error.filename,
50
- filetype=video_metadata_or_error.filetype or types.FileType.VIDEO,
51
- points=gpx_points,
52
- )
53
-
54
- video_metadata = video_metadata_or_error
55
-
56
- offset = self._synx_gpx_by_first_gps_timestamp(
57
- gpx_points, video_metadata.points
58
- )
59
-
60
- self._rebase_times(gpx_points, offset=offset)
61
-
62
- return dataclasses.replace(video_metadata_or_error, points=gpx_points)
63
-
64
- @staticmethod
65
- def _rebase_times(points: T.Sequence[geo.Point], offset: float = 0.0):
66
- """
67
- Make point times start from 0
68
- """
69
- if points:
70
- first_timestamp = points[0].time
71
- for p in points:
72
- p.time = (p.time - first_timestamp) + offset
73
- return points
74
-
75
- def _synx_gpx_by_first_gps_timestamp(
76
- self, gpx_points: T.Sequence[geo.Point], video_gps_points: T.Sequence[geo.Point]
77
- ) -> float:
78
- offset: float = 0.0
79
-
80
- if not gpx_points:
81
- return offset
82
-
83
- first_gpx_dt = datetime.datetime.fromtimestamp(
84
- gpx_points[0].time, tz=datetime.timezone.utc
85
- )
86
- LOG.info("First GPX timestamp: %s", first_gpx_dt)
87
-
88
- if not video_gps_points:
89
- LOG.warning(
90
- "Skip GPX synchronization because no GPS found in video %s",
91
- self.video_path,
92
- )
93
- return offset
94
-
95
- first_gps_point = video_gps_points[0]
96
- if isinstance(first_gps_point, telemetry.GPSPoint):
97
- if first_gps_point.epoch_time is not None:
98
- first_gps_dt = datetime.datetime.fromtimestamp(
99
- first_gps_point.epoch_time, tz=datetime.timezone.utc
100
- )
101
- LOG.info("First GPS timestamp: %s", first_gps_dt)
102
- offset = gpx_points[0].time - first_gps_point.epoch_time
103
- if offset:
104
- LOG.warning(
105
- "Found offset between GPX %s and video GPS timestamps %s: %s seconds",
106
- first_gpx_dt,
107
- first_gps_dt,
108
- offset,
109
- )
110
- else:
111
- LOG.info(
112
- "GPX and GPS are perfectly synchronized (all starts from %s)",
113
- first_gpx_dt,
114
- )
115
- else:
116
- LOG.warning(
117
- "Skip GPX synchronization because no GPS epoch time found in video %s",
118
- self.video_path,
119
- )
120
-
121
- return offset
122
-
123
-
124
21
  class GeotagVideosFromGPX(GeotagVideosFromGeneric):
125
22
  def __init__(
126
23
  self,
127
- video_paths: T.Sequence[Path],
128
24
  option: options.SourcePathOption | None = None,
129
25
  num_processes: int | None = None,
130
26
  ):
131
- super().__init__(video_paths, num_processes=num_processes)
27
+ super().__init__(num_processes=num_processes)
132
28
  if option is None:
133
29
  option = options.SourcePathOption(pattern="%f.gpx")
134
30
  self.option = option
135
31
 
136
- def _generate_image_extractors(self) -> T.Sequence[GPXVideoExtractor]:
32
+ @override
33
+ def _generate_video_extractors(
34
+ self, video_paths: T.Sequence[Path]
35
+ ) -> T.Sequence[GPXVideoExtractor]:
137
36
  return [
138
37
  GPXVideoExtractor(video_path, self.option.resolve(video_path))
139
- for video_path in self.video_paths
38
+ for video_path in video_paths
140
39
  ]
@@ -1,165 +1,32 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import sys
3
4
  import typing as T
4
5
  from pathlib import Path
5
6
 
6
- from .. import blackvue_parser, exceptions, geo, telemetry, types, utils
7
- from ..camm import camm_parser
8
- from ..gpmf import gpmf_gps_filter, gpmf_parser
9
- from ..types import FileType
10
- from .geotag_from_generic import GenericVideoExtractor, GeotagVideosFromGeneric
11
-
12
-
13
- class GoProVideoExtractor(GenericVideoExtractor):
14
- def extract(self) -> types.VideoMetadataOrError:
15
- with self.video_path.open("rb") as fp:
16
- gopro_info = gpmf_parser.extract_gopro_info(fp)
17
-
18
- if gopro_info is None:
19
- raise exceptions.MapillaryVideoGPSNotFoundError(
20
- "No GPS data found from the video"
21
- )
22
-
23
- gps_points = gopro_info.gps
24
- assert gps_points is not None, "must have GPS data extracted"
25
- if not gps_points:
26
- # Instead of raising an exception, return error metadata to tell the file type
27
- ex: exceptions.MapillaryDescriptionError = (
28
- exceptions.MapillaryGPXEmptyError("Empty GPS data found")
29
- )
30
- return types.describe_error_metadata(
31
- ex, self.video_path, filetype=FileType.GOPRO
32
- )
33
-
34
- gps_points = T.cast(
35
- T.List[telemetry.GPSPoint], gpmf_gps_filter.remove_noisy_points(gps_points)
36
- )
37
- if not gps_points:
38
- # Instead of raising an exception, return error metadata to tell the file type
39
- ex = exceptions.MapillaryGPSNoiseError("GPS is too noisy")
40
- return types.describe_error_metadata(
41
- ex, self.video_path, filetype=FileType.GOPRO
42
- )
43
-
44
- video_metadata = types.VideoMetadata(
45
- filename=self.video_path,
46
- filesize=utils.get_file_size(self.video_path),
47
- filetype=FileType.GOPRO,
48
- points=T.cast(T.List[geo.Point], gps_points),
49
- make=gopro_info.make,
50
- model=gopro_info.model,
51
- )
52
-
53
- return video_metadata
54
-
55
-
56
- class CAMMVideoExtractor(GenericVideoExtractor):
57
- def extract(self) -> types.VideoMetadataOrError:
58
- with self.video_path.open("rb") as fp:
59
- camm_info = camm_parser.extract_camm_info(fp)
60
-
61
- if camm_info is None:
62
- raise exceptions.MapillaryVideoGPSNotFoundError(
63
- "No GPS data found from the video"
64
- )
65
-
66
- if not camm_info.gps and not camm_info.mini_gps:
67
- # Instead of raising an exception, return error metadata to tell the file type
68
- ex: exceptions.MapillaryDescriptionError = (
69
- exceptions.MapillaryGPXEmptyError("Empty GPS data found")
70
- )
71
- return types.describe_error_metadata(
72
- ex, self.video_path, filetype=FileType.CAMM
73
- )
74
-
75
- return types.VideoMetadata(
76
- filename=self.video_path,
77
- filesize=utils.get_file_size(self.video_path),
78
- filetype=FileType.CAMM,
79
- points=T.cast(T.List[geo.Point], camm_info.gps or camm_info.mini_gps),
80
- make=camm_info.make,
81
- model=camm_info.model,
82
- )
83
-
7
+ if sys.version_info >= (3, 12):
8
+ from typing import override
9
+ else:
10
+ from typing_extensions import override
84
11
 
85
- class BlackVueVideoExtractor(GenericVideoExtractor):
86
- def extract(self) -> types.VideoMetadataOrError:
87
- with self.video_path.open("rb") as fp:
88
- blackvue_info = blackvue_parser.extract_blackvue_info(fp)
89
-
90
- if blackvue_info is None:
91
- raise exceptions.MapillaryVideoGPSNotFoundError(
92
- "No GPS data found from the video"
93
- )
94
-
95
- if not blackvue_info.gps:
96
- # Instead of raising an exception, return error metadata to tell the file type
97
- ex: exceptions.MapillaryDescriptionError = (
98
- exceptions.MapillaryGPXEmptyError("Empty GPS data found")
99
- )
100
- return types.describe_error_metadata(
101
- ex, self.video_path, filetype=FileType.BLACKVUE
102
- )
103
-
104
- video_metadata = types.VideoMetadata(
105
- filename=self.video_path,
106
- filesize=utils.get_file_size(self.video_path),
107
- filetype=FileType.BLACKVUE,
108
- points=blackvue_info.gps or [],
109
- make=blackvue_info.make,
110
- model=blackvue_info.model,
111
- )
112
-
113
- return video_metadata
114
-
115
-
116
- class NativeVideoExtractor(GenericVideoExtractor):
117
- def __init__(self, video_path: Path, filetypes: set[FileType] | None = None):
118
- super().__init__(video_path)
119
- self.filetypes = filetypes
120
-
121
- def extract(self) -> types.VideoMetadataOrError:
122
- ft = self.filetypes
123
- extractor: GenericVideoExtractor
124
-
125
- if ft is None or FileType.VIDEO in ft or FileType.GOPRO in ft:
126
- extractor = GoProVideoExtractor(self.video_path)
127
- try:
128
- return extractor.extract()
129
- except exceptions.MapillaryVideoGPSNotFoundError:
130
- pass
131
-
132
- if ft is None or FileType.VIDEO in ft or FileType.CAMM in ft:
133
- extractor = CAMMVideoExtractor(self.video_path)
134
- try:
135
- return extractor.extract()
136
- except exceptions.MapillaryVideoGPSNotFoundError:
137
- pass
138
-
139
- if ft is None or FileType.VIDEO in ft or FileType.BLACKVUE in ft:
140
- extractor = BlackVueVideoExtractor(self.video_path)
141
- try:
142
- return extractor.extract()
143
- except exceptions.MapillaryVideoGPSNotFoundError:
144
- pass
145
-
146
- raise exceptions.MapillaryVideoGPSNotFoundError(
147
- "No GPS data found from the video"
148
- )
12
+ from ..types import FileType
13
+ from .base import GeotagVideosFromGeneric
14
+ from .video_extractors.native import NativeVideoExtractor
149
15
 
150
16
 
151
17
  class GeotagVideosFromVideo(GeotagVideosFromGeneric):
152
18
  def __init__(
153
19
  self,
154
- video_paths: T.Sequence[Path],
155
20
  filetypes: set[FileType] | None = None,
156
21
  num_processes: int | None = None,
157
22
  ):
158
- super().__init__(video_paths, num_processes=num_processes)
23
+ super().__init__(num_processes=num_processes)
159
24
  self.filetypes = filetypes
160
25
 
161
- def _generate_video_extractors(self) -> T.Sequence[GenericVideoExtractor]:
26
+ @override
27
+ def _generate_video_extractors(
28
+ self, video_paths: T.Sequence[Path]
29
+ ) -> T.Sequence[NativeVideoExtractor]:
162
30
  return [
163
- NativeVideoExtractor(path, filetypes=self.filetypes)
164
- for path in self.video_paths
31
+ NativeVideoExtractor(path, filetypes=self.filetypes) for path in video_paths
165
32
  ]