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
@@ -1,13 +1,18 @@
1
+ from __future__ import annotations
2
+
1
3
  import logging
4
+ import sys
2
5
  import typing as T
3
6
  from pathlib import Path
4
7
 
5
- from tqdm import tqdm
8
+ if sys.version_info >= (3, 12):
9
+ from typing import override
10
+ else:
11
+ from typing_extensions import override
6
12
 
7
13
  from .. import types, utils
8
-
9
- from .geotag_from_generic import GeotagImagesFromGeneric
10
- from .geotag_images_from_gpx import GeotagImagesFromGPXWithProgress
14
+ from .base import GeotagImagesFromGeneric
15
+ from .geotag_images_from_gpx import GeotagImagesFromGPX
11
16
 
12
17
 
13
18
  LOG = logging.getLogger(__name__)
@@ -16,75 +21,72 @@ 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
- num_processes: T.Optional[int] = None,
26
+ num_processes: int | None = None,
23
27
  ):
24
- self.image_paths = image_paths
28
+ super().__init__(num_processes=num_processes)
25
29
  self.video_metadatas = video_metadatas
26
30
  self.offset_time = offset_time
27
- self.num_processes = num_processes
28
- super().__init__()
29
31
 
30
- def to_description(self) -> T.List[types.ImageMetadataOrError]:
31
- # will return this list
32
- final_image_metadatas: T.List[types.ImageMetadataOrError] = []
32
+ @override
33
+ def to_description(
34
+ self, image_paths: T.Sequence[Path]
35
+ ) -> list[types.ImageMetadataOrError]:
36
+ # Will return this list
37
+ final_image_metadatas: list[types.ImageMetadataOrError] = []
33
38
 
34
- for video_metadata in self.video_metadatas:
35
- video_path = video_metadata.filename
36
- LOG.debug("Processing video: %s", video_path)
39
+ video_metadatas, video_error_metadatas = types.separate_errors(
40
+ self.video_metadatas
41
+ )
37
42
 
38
- sample_image_paths = list(
39
- utils.filter_video_samples(self.image_paths, video_path)
43
+ for video_error_metadata in video_error_metadatas:
44
+ video_path = video_error_metadata.filename
45
+ sample_paths = list(utils.filter_video_samples(image_paths, video_path))
46
+ LOG.debug(
47
+ "Found %d sample images from video %s with error: %s",
48
+ len(sample_paths),
49
+ video_path,
50
+ video_error_metadata.error,
40
51
  )
52
+ for sample_path in sample_paths:
53
+ image_error_metadata = types.describe_error_metadata(
54
+ video_error_metadata.error,
55
+ sample_path,
56
+ filetype=types.FileType.IMAGE,
57
+ )
58
+ final_image_metadatas.append(image_error_metadata)
59
+
60
+ for video_metadata in video_metadatas:
61
+ video_path = video_metadata.filename
62
+
63
+ sample_paths = list(utils.filter_video_samples(image_paths, video_path))
41
64
  LOG.debug(
42
65
  "Found %d sample images from video %s",
43
- len(sample_image_paths),
66
+ len(sample_paths),
44
67
  video_path,
45
68
  )
46
69
 
47
- if isinstance(video_metadata, types.ErrorMetadata):
48
- for sample_image_path in sample_image_paths:
49
- error_metadata = types.describe_error_metadata(
50
- video_metadata.error,
51
- sample_image_path,
52
- filetype=types.FileType.IMAGE,
53
- )
54
- final_image_metadatas.append(error_metadata)
55
- continue
56
-
57
- with tqdm(
58
- total=len(sample_image_paths),
59
- desc=f"Interpolating {video_path.name}",
60
- unit="images",
61
- disable=LOG.getEffectiveLevel() <= logging.DEBUG,
62
- ) as pbar:
63
- image_metadatas = GeotagImagesFromGPXWithProgress(
64
- sample_image_paths,
65
- video_metadata.points,
66
- use_gpx_start_time=False,
67
- use_image_start_time=True,
68
- offset_time=self.offset_time,
69
- num_processes=self.num_processes,
70
- progress_bar=pbar,
71
- ).to_description()
72
- final_image_metadatas.extend(image_metadatas)
73
-
74
- # update make and model
75
- LOG.debug(
76
- 'Found camera make "%s" and model "%s"',
77
- video_metadata.make,
78
- video_metadata.model,
70
+ geotag = GeotagImagesFromGPX(
71
+ video_metadata.points,
72
+ use_gpx_start_time=False,
73
+ use_image_start_time=True,
74
+ offset_time=self.offset_time,
75
+ num_processes=self.num_processes,
79
76
  )
77
+
78
+ image_metadatas = geotag.to_description(image_paths)
79
+
80
80
  for metadata in image_metadatas:
81
81
  if isinstance(metadata, types.ImageMetadata):
82
82
  metadata.MAPDeviceMake = video_metadata.make
83
83
  metadata.MAPDeviceModel = video_metadata.model
84
84
 
85
+ final_image_metadatas.extend(image_metadatas)
86
+
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
+ )
@@ -0,0 +1,39 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import sys
5
+ import typing as T
6
+ from pathlib import Path
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 options
14
+ from .base import GeotagVideosFromGeneric
15
+ from .video_extractors.gpx import GPXVideoExtractor
16
+
17
+
18
+ LOG = logging.getLogger(__name__)
19
+
20
+
21
+ class GeotagVideosFromGPX(GeotagVideosFromGeneric):
22
+ def __init__(
23
+ self,
24
+ option: options.SourcePathOption | None = None,
25
+ num_processes: int | None = None,
26
+ ):
27
+ super().__init__(num_processes=num_processes)
28
+ if option is None:
29
+ option = options.SourcePathOption(pattern="%f.gpx")
30
+ self.option = option
31
+
32
+ @override
33
+ def _generate_video_extractors(
34
+ self, video_paths: T.Sequence[Path]
35
+ ) -> T.Sequence[GPXVideoExtractor]:
36
+ return [
37
+ GPXVideoExtractor(video_path, self.option.resolve(video_path))
38
+ for video_path in video_paths
39
+ ]
@@ -1,197 +1,32 @@
1
- import io
2
- import logging
1
+ from __future__ import annotations
2
+
3
+ import sys
3
4
  import typing as T
4
- from multiprocessing import Pool
5
5
  from pathlib import Path
6
6
 
7
- from tqdm import tqdm
8
-
9
- from .. import exceptions, geo, types, utils
10
- from ..camm import camm_parser
11
- from ..mp4 import simple_mp4_parser as sparser
12
- from ..telemetry import GPSPoint
13
- from . import blackvue_parser, gpmf_gps_filter, gpmf_parser, utils as video_utils
14
- from .geotag_from_generic import GeotagVideosFromGeneric
7
+ if sys.version_info >= (3, 12):
8
+ from typing import override
9
+ else:
10
+ from typing_extensions import override
15
11
 
16
- LOG = logging.getLogger(__name__)
12
+ from ..types import FileType
13
+ from .base import GeotagVideosFromGeneric
14
+ from .video_extractors.native import NativeVideoExtractor
17
15
 
18
16
 
19
17
  class GeotagVideosFromVideo(GeotagVideosFromGeneric):
20
18
  def __init__(
21
19
  self,
22
- video_paths: T.Sequence[Path],
23
- filetypes: T.Optional[T.Set[types.FileType]] = None,
24
- num_processes: T.Optional[int] = None,
20
+ filetypes: set[FileType] | None = None,
21
+ num_processes: int | None = None,
25
22
  ):
26
- self.video_paths = video_paths
23
+ super().__init__(num_processes=num_processes)
27
24
  self.filetypes = filetypes
28
- self.num_processes = num_processes
29
-
30
- def to_description(self) -> T.List[types.VideoMetadataOrError]:
31
- if self.num_processes is None:
32
- num_processes = self.num_processes
33
- disable_multiprocessing = False
34
- else:
35
- num_processes = max(self.num_processes, 1)
36
- disable_multiprocessing = self.num_processes <= 0
37
-
38
- with Pool(processes=num_processes) as pool:
39
- video_metadatas_iter: T.Iterator[types.VideoMetadataOrError]
40
- if disable_multiprocessing:
41
- video_metadatas_iter = map(self._geotag_video, self.video_paths)
42
- else:
43
- video_metadatas_iter = pool.imap(
44
- self._geotag_video,
45
- self.video_paths,
46
- )
47
- return list(
48
- tqdm(
49
- video_metadatas_iter,
50
- desc="Extracting GPS tracks from videos",
51
- unit="videos",
52
- disable=LOG.getEffectiveLevel() <= logging.DEBUG,
53
- total=len(self.video_paths),
54
- )
55
- )
56
-
57
- def _geotag_video(
58
- self,
59
- video_path: Path,
60
- ) -> types.VideoMetadataOrError:
61
- return GeotagVideosFromVideo.geotag_video(video_path, self.filetypes)
62
-
63
- @staticmethod
64
- def _extract_video_metadata(
65
- video_path: Path,
66
- filetypes: T.Optional[T.Set[types.FileType]] = None,
67
- ) -> T.Optional[types.VideoMetadata]:
68
- if (
69
- filetypes is None
70
- or types.FileType.VIDEO in filetypes
71
- or types.FileType.CAMM in filetypes
72
- ):
73
- with video_path.open("rb") as fp:
74
- try:
75
- points = camm_parser.extract_points(fp)
76
- except sparser.ParsingError:
77
- points = None
78
-
79
- if points is not None:
80
- fp.seek(0, io.SEEK_SET)
81
- make, model = camm_parser.extract_camera_make_and_model(fp)
82
- return types.VideoMetadata(
83
- filename=video_path,
84
- md5sum=None,
85
- filesize=utils.get_file_size(video_path),
86
- filetype=types.FileType.CAMM,
87
- points=points,
88
- make=make,
89
- model=model,
90
- )
91
-
92
- if (
93
- filetypes is None
94
- or types.FileType.VIDEO in filetypes
95
- or types.FileType.GOPRO in filetypes
96
- ):
97
- with video_path.open("rb") as fp:
98
- try:
99
- points_with_fix = gpmf_parser.extract_points(fp)
100
- except sparser.ParsingError:
101
- points_with_fix = None
102
-
103
- if points_with_fix is not None:
104
- fp.seek(0, io.SEEK_SET)
105
- make, model = "GoPro", gpmf_parser.extract_camera_model(fp)
106
- return types.VideoMetadata(
107
- filename=video_path,
108
- md5sum=None,
109
- filesize=utils.get_file_size(video_path),
110
- filetype=types.FileType.GOPRO,
111
- points=T.cast(T.List[geo.Point], points_with_fix),
112
- make=make,
113
- model=model,
114
- )
115
-
116
- if (
117
- filetypes is None
118
- or types.FileType.VIDEO in filetypes
119
- or types.FileType.BLACKVUE in filetypes
120
- ):
121
- with video_path.open("rb") as fp:
122
- try:
123
- points = blackvue_parser.extract_points(fp)
124
- except sparser.ParsingError:
125
- points = None
126
-
127
- if points is not None:
128
- fp.seek(0, io.SEEK_SET)
129
- make, model = "BlackVue", blackvue_parser.extract_camera_model(fp)
130
- return types.VideoMetadata(
131
- filename=video_path,
132
- md5sum=None,
133
- filesize=utils.get_file_size(video_path),
134
- filetype=types.FileType.BLACKVUE,
135
- points=points,
136
- make=make,
137
- model=model,
138
- )
139
-
140
- return None
141
-
142
- @staticmethod
143
- def geotag_video(
144
- video_path: Path,
145
- filetypes: T.Optional[T.Set[types.FileType]] = None,
146
- ) -> types.VideoMetadataOrError:
147
- video_metadata = None
148
- try:
149
- video_metadata = GeotagVideosFromVideo._extract_video_metadata(
150
- video_path, filetypes
151
- )
152
-
153
- if video_metadata is None:
154
- raise exceptions.MapillaryVideoError("No GPS data found from the video")
155
-
156
- if not video_metadata.points:
157
- raise exceptions.MapillaryGPXEmptyError("Empty GPS data found")
158
-
159
- video_metadata.points = geo.extend_deduplicate_points(video_metadata.points)
160
- assert video_metadata.points, "must have at least one point"
161
-
162
- if all(isinstance(p, GPSPoint) for p in video_metadata.points):
163
- video_metadata.points = T.cast(
164
- T.List[geo.Point],
165
- gpmf_gps_filter.remove_noisy_points(
166
- T.cast(T.List[GPSPoint], video_metadata.points)
167
- ),
168
- )
169
- if not video_metadata.points:
170
- raise exceptions.MapillaryGPSNoiseError("GPS is too noisy")
171
-
172
- stationary = video_utils.is_video_stationary(
173
- geo.get_max_distance_from_start(
174
- [(p.lat, p.lon) for p in video_metadata.points]
175
- )
176
- )
177
- if stationary:
178
- raise exceptions.MapillaryStationaryVideoError("Stationary video")
179
-
180
- LOG.debug("Calculating MD5 checksum for %s", str(video_metadata.filename))
181
- video_metadata.update_md5sum()
182
- except Exception as ex:
183
- if not isinstance(ex, exceptions.MapillaryDescriptionError):
184
- LOG.warning(
185
- "Failed to geotag video %s: %s",
186
- video_path,
187
- str(ex),
188
- exc_info=LOG.getEffectiveLevel() <= logging.DEBUG,
189
- )
190
- filetype = None if video_metadata is None else video_metadata.filetype
191
- return types.describe_error_metadata(
192
- ex,
193
- video_path,
194
- filetype=filetype,
195
- )
196
25
 
197
- return video_metadata
26
+ @override
27
+ def _generate_video_extractors(
28
+ self, video_paths: T.Sequence[Path]
29
+ ) -> T.Sequence[NativeVideoExtractor]:
30
+ return [
31
+ NativeVideoExtractor(path, filetypes=self.filetypes) for path in video_paths
32
+ ]
@@ -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 BaseImageExtractor(abc.ABC):
10
+ """
11
+ Extracts metadata from an image file.
12
+ """
13
+
14
+ def __init__(self, image_path: Path):
15
+ self.image_path = image_path
16
+
17
+ def extract(self) -> types.ImageMetadataOrError:
18
+ raise NotImplementedError
@@ -0,0 +1,60 @@
1
+ from __future__ import annotations
2
+
3
+ import contextlib
4
+ import sys
5
+ import typing as T
6
+ from pathlib import Path
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, exif_read, geo, types, utils
14
+ from .base import BaseImageExtractor
15
+
16
+
17
+ class ImageEXIFExtractor(BaseImageExtractor):
18
+ def __init__(self, image_path: Path, skip_lonlat_error: bool = False):
19
+ super().__init__(image_path)
20
+ self.skip_lonlat_error = skip_lonlat_error
21
+
22
+ @contextlib.contextmanager
23
+ def _exif_context(self) -> T.Generator[exif_read.ExifReadABC, None, None]:
24
+ with self.image_path.open("rb") as fp:
25
+ yield exif_read.ExifRead(fp)
26
+
27
+ @override
28
+ def extract(self) -> types.ImageMetadata:
29
+ with self._exif_context() as exif:
30
+ lonlat = exif.extract_lon_lat()
31
+ if lonlat is None:
32
+ if not self.skip_lonlat_error:
33
+ raise exceptions.MapillaryGeoTaggingError(
34
+ "Unable to extract GPS Longitude or GPS Latitude from the image"
35
+ )
36
+ lonlat = (0.0, 0.0)
37
+ lon, lat = lonlat
38
+
39
+ capture_time = exif.extract_capture_time()
40
+ if capture_time is None:
41
+ raise exceptions.MapillaryGeoTaggingError(
42
+ "Unable to extract timestamp from the image"
43
+ )
44
+
45
+ image_metadata = types.ImageMetadata(
46
+ filename=self.image_path,
47
+ filesize=utils.get_file_size(self.image_path),
48
+ time=geo.as_unix_time(capture_time),
49
+ lat=lat,
50
+ lon=lon,
51
+ alt=exif.extract_altitude(),
52
+ angle=exif.extract_direction(),
53
+ width=exif.extract_width(),
54
+ height=exif.extract_height(),
55
+ MAPOrientation=exif.extract_orientation(),
56
+ MAPDeviceMake=exif.extract_make(),
57
+ MAPDeviceModel=exif.extract_model(),
58
+ )
59
+
60
+ return image_metadata
@@ -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))