mapillary-tools 0.13.3a1__py3-none-any.whl → 0.14.0a1__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 (64) hide show
  1. mapillary_tools/__init__.py +1 -1
  2. mapillary_tools/api_v4.py +235 -14
  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 +425 -177
  7. mapillary_tools/commands/__main__.py +11 -4
  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 +28 -12
  15. mapillary_tools/constants.py +46 -4
  16. mapillary_tools/exceptions.py +34 -35
  17. mapillary_tools/exif_read.py +158 -53
  18. mapillary_tools/exiftool_read.py +19 -5
  19. mapillary_tools/exiftool_read_video.py +12 -1
  20. mapillary_tools/exiftool_runner.py +77 -0
  21. mapillary_tools/geo.py +148 -107
  22. mapillary_tools/geotag/factory.py +298 -0
  23. mapillary_tools/geotag/geotag_from_generic.py +152 -11
  24. mapillary_tools/geotag/geotag_images_from_exif.py +43 -124
  25. mapillary_tools/geotag/geotag_images_from_exiftool.py +66 -70
  26. mapillary_tools/geotag/geotag_images_from_exiftool_both_image_and_video.py +32 -48
  27. mapillary_tools/geotag/geotag_images_from_gpx.py +41 -116
  28. mapillary_tools/geotag/geotag_images_from_gpx_file.py +15 -96
  29. mapillary_tools/geotag/geotag_images_from_nmea_file.py +4 -2
  30. mapillary_tools/geotag/geotag_images_from_video.py +46 -46
  31. mapillary_tools/geotag/geotag_videos_from_exiftool_video.py +98 -92
  32. mapillary_tools/geotag/geotag_videos_from_gpx.py +140 -0
  33. mapillary_tools/geotag/geotag_videos_from_video.py +149 -181
  34. mapillary_tools/geotag/options.py +159 -0
  35. mapillary_tools/{geotag → gpmf}/gpmf_parser.py +194 -171
  36. mapillary_tools/history.py +3 -11
  37. mapillary_tools/mp4/io_utils.py +0 -1
  38. mapillary_tools/mp4/mp4_sample_parser.py +11 -3
  39. mapillary_tools/mp4/simple_mp4_parser.py +0 -10
  40. mapillary_tools/process_geotag_properties.py +151 -386
  41. mapillary_tools/process_sequence_properties.py +554 -202
  42. mapillary_tools/sample_video.py +8 -15
  43. mapillary_tools/telemetry.py +24 -12
  44. mapillary_tools/types.py +80 -22
  45. mapillary_tools/upload.py +316 -298
  46. mapillary_tools/upload_api_v4.py +55 -122
  47. mapillary_tools/uploader.py +396 -254
  48. mapillary_tools/utils.py +26 -0
  49. mapillary_tools/video_data_extraction/extract_video_data.py +17 -36
  50. mapillary_tools/video_data_extraction/extractors/blackvue_parser.py +34 -19
  51. mapillary_tools/video_data_extraction/extractors/camm_parser.py +41 -17
  52. mapillary_tools/video_data_extraction/extractors/exiftool_runtime_parser.py +4 -1
  53. mapillary_tools/video_data_extraction/extractors/exiftool_xml_parser.py +1 -2
  54. mapillary_tools/video_data_extraction/extractors/gopro_parser.py +37 -22
  55. {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0a1.dist-info}/METADATA +3 -2
  56. mapillary_tools-0.14.0a1.dist-info/RECORD +78 -0
  57. {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0a1.dist-info}/WHEEL +1 -1
  58. mapillary_tools/geotag/utils.py +0 -26
  59. mapillary_tools-0.13.3a1.dist-info/RECORD +0 -75
  60. /mapillary_tools/{geotag → gpmf}/gpmf_gps_filter.py +0 -0
  61. /mapillary_tools/{geotag → gpmf}/gps_filter.py +0 -0
  62. {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0a1.dist-info}/entry_points.txt +0 -0
  63. {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0a1.dist-info/licenses}/LICENSE +0 -0
  64. {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0a1.dist-info}/top_level.txt +0 -0
@@ -1,22 +1,163 @@
1
+ from __future__ import annotations
2
+
1
3
  import abc
4
+ import logging
2
5
  import typing as T
6
+ from pathlib import Path
7
+
8
+ from tqdm import tqdm
9
+
10
+ from .. import exceptions, types, utils
11
+
12
+
13
+ LOG = logging.getLogger(__name__)
14
+
15
+
16
+ class GenericImageExtractor(abc.ABC):
17
+ """
18
+ Extracts metadata from an image file.
19
+ """
20
+
21
+ def __init__(self, image_path: Path):
22
+ self.image_path = image_path
23
+
24
+ def extract(self) -> types.ImageMetadataOrError:
25
+ raise NotImplementedError
26
+
27
+
28
+ TImageExtractor = T.TypeVar("TImageExtractor", bound=GenericImageExtractor)
29
+
30
+
31
+ class GeotagImagesFromGeneric(abc.ABC, T.Generic[TImageExtractor]):
32
+ """
33
+ Extracts metadata from a list of image files with multiprocessing.
34
+ """
35
+
36
+ def __init__(
37
+ self, image_paths: T.Sequence[Path], num_processes: int | None = None
38
+ ) -> None:
39
+ self.image_paths = image_paths
40
+ self.num_processes = num_processes
41
+
42
+ def to_description(self) -> list[types.ImageMetadataOrError]:
43
+ extractor_or_errors = self._generate_image_extractors()
44
+
45
+ assert len(extractor_or_errors) == len(self.image_paths)
46
+
47
+ extractors, error_metadatas = types.separate_errors(extractor_or_errors)
3
48
 
4
- from .. import types
49
+ map_results = utils.mp_map_maybe(
50
+ self.run_extraction,
51
+ extractors,
52
+ num_processes=self.num_processes,
53
+ )
5
54
 
55
+ results = list(
56
+ tqdm(
57
+ map_results,
58
+ desc="Extracting images",
59
+ unit="images",
60
+ disable=LOG.getEffectiveLevel() <= logging.DEBUG,
61
+ total=len(extractors),
62
+ )
63
+ )
6
64
 
7
- class GeotagImagesFromGeneric(abc.ABC):
8
- def __init__(self) -> None:
9
- pass
65
+ return results + error_metadatas
10
66
 
11
- @abc.abstractmethod
12
- def to_description(self) -> T.List[types.ImageMetadataOrError]:
67
+ def _generate_image_extractors(
68
+ self,
69
+ ) -> T.Sequence[TImageExtractor | types.ErrorMetadata]:
13
70
  raise NotImplementedError
14
71
 
72
+ # This method is passed to multiprocessing
73
+ # so it has to be classmethod or staticmethod to avoid pickling the instance
74
+ @classmethod
75
+ def run_extraction(cls, extractor: TImageExtractor) -> types.ImageMetadataOrError:
76
+ image_path = extractor.image_path
15
77
 
16
- class GeotagVideosFromGeneric(abc.ABC):
17
- def __init__(self) -> None:
18
- pass
78
+ try:
79
+ return extractor.extract()
80
+ except exceptions.MapillaryDescriptionError as ex:
81
+ return types.describe_error_metadata(
82
+ ex, image_path, filetype=types.FileType.IMAGE
83
+ )
84
+ except Exception as ex:
85
+ LOG.exception("Unexpected error extracting metadata from %s", image_path)
86
+ return types.describe_error_metadata(
87
+ ex, image_path, filetype=types.FileType.IMAGE
88
+ )
19
89
 
20
- @abc.abstractmethod
21
- def to_description(self) -> T.List[types.VideoMetadataOrError]:
90
+
91
+ class GenericVideoExtractor(abc.ABC):
92
+ """
93
+ Extracts metadata from a video file.
94
+ """
95
+
96
+ def __init__(self, video_path: Path):
97
+ self.video_path = video_path
98
+
99
+ def extract(self) -> types.VideoMetadataOrError:
100
+ raise NotImplementedError
101
+
102
+
103
+ TVideoExtractor = T.TypeVar("TVideoExtractor", bound=GenericVideoExtractor)
104
+
105
+
106
+ class GeotagVideosFromGeneric(abc.ABC, T.Generic[TVideoExtractor]):
107
+ """
108
+ Extracts metadata from a list of video files with multiprocessing.
109
+ """
110
+
111
+ def __init__(
112
+ self, video_paths: T.Sequence[Path], num_processes: int | None = None
113
+ ) -> None:
114
+ self.video_paths = video_paths
115
+ self.num_processes = num_processes
116
+
117
+ def to_description(self) -> list[types.VideoMetadataOrError]:
118
+ extractor_or_errors = self._generate_video_extractors()
119
+
120
+ assert len(extractor_or_errors) == len(self.video_paths)
121
+
122
+ extractors, error_metadatas = types.separate_errors(extractor_or_errors)
123
+
124
+ map_results = utils.mp_map_maybe(
125
+ self.run_extraction,
126
+ extractors,
127
+ num_processes=self.num_processes,
128
+ )
129
+
130
+ results = list(
131
+ tqdm(
132
+ map_results,
133
+ desc="Extracting videos",
134
+ unit="videos",
135
+ disable=LOG.getEffectiveLevel() <= logging.DEBUG,
136
+ total=len(extractors),
137
+ )
138
+ )
139
+
140
+ return results + error_metadatas
141
+
142
+ def _generate_video_extractors(
143
+ self,
144
+ ) -> T.Sequence[TVideoExtractor | types.ErrorMetadata]:
22
145
  raise NotImplementedError
146
+
147
+ # This method is passed to multiprocessing
148
+ # so it has to be classmethod or staticmethod to avoid pickling the instance
149
+ @classmethod
150
+ def run_extraction(cls, extractor: TVideoExtractor) -> types.VideoMetadataOrError:
151
+ video_path = extractor.video_path
152
+
153
+ try:
154
+ return extractor.extract()
155
+ except exceptions.MapillaryDescriptionError as ex:
156
+ return types.describe_error_metadata(
157
+ ex, video_path, filetype=types.FileType.VIDEO
158
+ )
159
+ except Exception as ex:
160
+ LOG.exception("Unexpected error extracting metadata from %s", video_path)
161
+ return types.describe_error_metadata(
162
+ ex, video_path, filetype=types.FileType.VIDEO
163
+ )
@@ -1,141 +1,60 @@
1
- import io
1
+ import contextlib
2
2
  import logging
3
3
  import typing as T
4
- from multiprocessing import Pool
5
4
  from pathlib import Path
6
5
 
7
- from tqdm import tqdm
8
-
9
- from .. import exceptions, exif_write, geo, types, utils
6
+ from .. import exceptions, geo, types, utils
10
7
  from ..exif_read import ExifRead, ExifReadABC
11
- from .geotag_from_generic import GeotagImagesFromGeneric
8
+ from .geotag_from_generic import GenericImageExtractor, GeotagImagesFromGeneric
12
9
 
13
10
  LOG = logging.getLogger(__name__)
14
11
 
15
12
 
16
- def verify_image_exif_write(
17
- metadata: types.ImageMetadata,
18
- image_bytes: T.Optional[bytes] = None,
19
- ) -> None:
20
- if image_bytes is None:
21
- edit = exif_write.ExifEdit(metadata.filename)
22
- else:
23
- edit = exif_write.ExifEdit(image_bytes)
24
-
25
- # The cast is to fix the type error in Python3.6:
26
- # Argument 1 to "add_image_description" of "ExifEdit" has incompatible type "ImageDescription"; expected "Dict[str, Any]"
27
- edit.add_image_description(
28
- T.cast(T.Dict, types.desc_file_to_exif(types.as_desc(metadata)))
29
- )
30
-
31
- # Possible errors thrown here:
32
- # - struct.error: 'H' format requires 0 <= number <= 65535
33
- # - piexif.InvalidImageDataError
34
- edit.dump_image_bytes()
35
-
36
-
37
- class GeotagImagesFromEXIF(GeotagImagesFromGeneric):
38
- def __init__(
39
- self, image_paths: T.Sequence[Path], num_processes: T.Optional[int] = None
40
- ):
41
- self.image_paths = image_paths
42
- self.num_processes = num_processes
43
- super().__init__()
44
-
45
- @staticmethod
46
- def build_image_metadata(
47
- image_path: Path, exif: ExifReadABC, skip_lonlat_error: bool = False
48
- ) -> types.ImageMetadata:
49
- lonlat = exif.extract_lon_lat()
50
- if lonlat is None:
51
- if not skip_lonlat_error:
13
+ class ImageEXIFExtractor(GenericImageExtractor):
14
+ def __init__(self, image_path: Path, skip_lonlat_error: bool = False):
15
+ super().__init__(image_path)
16
+ self.skip_lonlat_error = skip_lonlat_error
17
+
18
+ @contextlib.contextmanager
19
+ def _exif_context(self) -> T.Generator[ExifReadABC, None, None]:
20
+ with self.image_path.open("rb") as fp:
21
+ yield ExifRead(fp)
22
+
23
+ def extract(self) -> types.ImageMetadata:
24
+ with self._exif_context() as exif:
25
+ lonlat = exif.extract_lon_lat()
26
+ if lonlat is None:
27
+ if not self.skip_lonlat_error:
28
+ raise exceptions.MapillaryGeoTaggingError(
29
+ "Unable to extract GPS Longitude or GPS Latitude from the image"
30
+ )
31
+ lonlat = (0.0, 0.0)
32
+ lon, lat = lonlat
33
+
34
+ capture_time = exif.extract_capture_time()
35
+ if capture_time is None:
52
36
  raise exceptions.MapillaryGeoTaggingError(
53
- "Unable to extract GPS Longitude or GPS Latitude from the image"
37
+ "Unable to extract timestamp from the image"
54
38
  )
55
- lonlat = (0.0, 0.0)
56
- lon, lat = lonlat
57
39
 
58
- capture_time = exif.extract_capture_time()
59
- if capture_time is None:
60
- raise exceptions.MapillaryGeoTaggingError(
61
- "Unable to extract timestamp from the image"
40
+ image_metadata = types.ImageMetadata(
41
+ filename=self.image_path,
42
+ filesize=utils.get_file_size(self.image_path),
43
+ time=geo.as_unix_time(capture_time),
44
+ lat=lat,
45
+ lon=lon,
46
+ alt=exif.extract_altitude(),
47
+ angle=exif.extract_direction(),
48
+ width=exif.extract_width(),
49
+ height=exif.extract_height(),
50
+ MAPOrientation=exif.extract_orientation(),
51
+ MAPDeviceMake=exif.extract_make(),
52
+ MAPDeviceModel=exif.extract_model(),
62
53
  )
63
54
 
64
- image_metadata = types.ImageMetadata(
65
- filename=image_path,
66
- md5sum=None,
67
- filesize=utils.get_file_size(image_path),
68
- time=geo.as_unix_time(capture_time),
69
- lat=lat,
70
- lon=lon,
71
- alt=exif.extract_altitude(),
72
- angle=exif.extract_direction(),
73
- width=exif.extract_width(),
74
- height=exif.extract_height(),
75
- MAPOrientation=exif.extract_orientation(),
76
- MAPDeviceMake=exif.extract_make(),
77
- MAPDeviceModel=exif.extract_model(),
78
- )
79
-
80
55
  return image_metadata
81
56
 
82
- @staticmethod
83
- def geotag_image(
84
- image_path: Path, skip_lonlat_error: bool = False
85
- ) -> types.ImageMetadataOrError:
86
- try:
87
- # load the image bytes into memory to avoid reading it multiple times
88
- with image_path.open("rb") as fp:
89
- image_bytesio = io.BytesIO(fp.read())
90
-
91
- image_bytesio.seek(0, io.SEEK_SET)
92
- exif = ExifRead(image_bytesio)
93
57
 
94
- image_metadata = GeotagImagesFromEXIF.build_image_metadata(
95
- image_path, exif, skip_lonlat_error=skip_lonlat_error
96
- )
97
-
98
- image_bytesio.seek(0, io.SEEK_SET)
99
- verify_image_exif_write(
100
- image_metadata,
101
- image_bytes=image_bytesio.read(),
102
- )
103
- except Exception as ex:
104
- return types.describe_error_metadata(
105
- ex, image_path, filetype=types.FileType.IMAGE
106
- )
107
-
108
- image_bytesio.seek(0, io.SEEK_SET)
109
- image_metadata.update_md5sum(image_bytesio)
110
-
111
- return image_metadata
112
-
113
- def to_description(self) -> T.List[types.ImageMetadataOrError]:
114
- if self.num_processes is None:
115
- num_processes = self.num_processes
116
- disable_multiprocessing = False
117
- else:
118
- num_processes = max(self.num_processes, 1)
119
- disable_multiprocessing = self.num_processes <= 0
120
-
121
- with Pool(processes=num_processes) as pool:
122
- image_metadatas_iter: T.Iterator[types.ImageMetadataOrError]
123
- if disable_multiprocessing:
124
- image_metadatas_iter = map(
125
- GeotagImagesFromEXIF.geotag_image,
126
- self.image_paths,
127
- )
128
- else:
129
- image_metadatas_iter = pool.imap(
130
- GeotagImagesFromEXIF.geotag_image,
131
- self.image_paths,
132
- )
133
- return list(
134
- tqdm(
135
- image_metadatas_iter,
136
- desc="Extracting geotags from images",
137
- unit="images",
138
- disable=LOG.getEffectiveLevel() <= logging.DEBUG,
139
- total=len(self.image_paths),
140
- )
141
- )
58
+ class GeotagImagesFromEXIF(GeotagImagesFromGeneric):
59
+ def _generate_image_extractors(self) -> T.Sequence[ImageEXIFExtractor]:
60
+ return [ImageEXIFExtractor(path) for path in self.image_paths]
@@ -1,66 +1,91 @@
1
- import io
1
+ from __future__ import annotations
2
+
3
+ import contextlib
2
4
  import logging
3
5
  import typing as T
4
6
  import xml.etree.ElementTree as ET
5
- from multiprocessing import Pool
6
7
  from pathlib import Path
7
8
 
8
- from tqdm import tqdm
9
-
10
- from .. import exceptions, exiftool_read, types
9
+ from .. import constants, exceptions, exiftool_read, types
10
+ from ..exiftool_runner import ExiftoolRunner
11
11
  from .geotag_from_generic import GeotagImagesFromGeneric
12
- from .geotag_images_from_exif import GeotagImagesFromEXIF, verify_image_exif_write
12
+ from .geotag_images_from_exif import ImageEXIFExtractor
13
13
 
14
14
  LOG = logging.getLogger(__name__)
15
15
 
16
16
 
17
+ class ImageExifToolExtractor(ImageEXIFExtractor):
18
+ def __init__(self, image_path: Path, element: ET.Element):
19
+ super().__init__(image_path)
20
+ self.element = element
21
+
22
+ @contextlib.contextmanager
23
+ def _exif_context(self):
24
+ yield exiftool_read.ExifToolRead(ET.ElementTree(self.element))
25
+
26
+
17
27
  class GeotagImagesFromExifTool(GeotagImagesFromGeneric):
18
28
  def __init__(
19
29
  self,
20
30
  image_paths: T.Sequence[Path],
21
31
  xml_path: Path,
22
- num_processes: T.Optional[int] = None,
32
+ num_processes: int | None = None,
23
33
  ):
24
- self.image_paths = image_paths
25
34
  self.xml_path = xml_path
26
- self.num_processes = num_processes
27
- super().__init__()
35
+ super().__init__(image_paths=image_paths, num_processes=num_processes)
28
36
 
29
- @staticmethod
30
- def geotag_image(element: ET.Element) -> types.ImageMetadataOrError:
31
- image_path = exiftool_read.find_rdf_description_path(element)
32
- assert image_path is not None, "must find the path from the element"
37
+ def _generate_image_extractors(
38
+ self,
39
+ ) -> T.Sequence[ImageExifToolExtractor | types.ErrorMetadata]:
40
+ rdf_description_by_path = exiftool_read.index_rdf_description_by_path(
41
+ [self.xml_path]
42
+ )
33
43
 
34
- try:
35
- exif = exiftool_read.ExifToolRead(ET.ElementTree(element))
36
- image_metadata = GeotagImagesFromEXIF.build_image_metadata(
37
- image_path, exif, skip_lonlat_error=False
38
- )
39
- # load the image bytes into memory to avoid reading it multiple times
40
- with image_path.open("rb") as fp:
41
- image_bytesio = io.BytesIO(fp.read())
42
- image_bytesio.seek(0, io.SEEK_SET)
43
- verify_image_exif_write(
44
- image_metadata,
45
- image_bytes=image_bytesio.read(),
46
- )
47
- except Exception as ex:
48
- return types.describe_error_metadata(
49
- ex, image_path, filetype=types.FileType.IMAGE
44
+ results: list[ImageExifToolExtractor | types.ErrorMetadata] = []
45
+
46
+ for path in self.image_paths:
47
+ rdf_description = rdf_description_by_path.get(
48
+ exiftool_read.canonical_path(path)
50
49
  )
50
+ if rdf_description is None:
51
+ exc = exceptions.MapillaryEXIFNotFoundError(
52
+ f"The {exiftool_read._DESCRIPTION_TAG} XML element for the image not found"
53
+ )
54
+ results.append(
55
+ types.describe_error_metadata(
56
+ exc, path, filetype=types.FileType.IMAGE
57
+ )
58
+ )
59
+ else:
60
+ results.append(ImageExifToolExtractor(path, rdf_description))
51
61
 
52
- image_bytesio.seek(0, io.SEEK_SET)
53
- image_metadata.update_md5sum(image_bytesio)
62
+ return results
54
63
 
55
- return image_metadata
56
64
 
57
- def to_description(self) -> T.List[types.ImageMetadataOrError]:
58
- rdf_description_by_path = exiftool_read.index_rdf_description_by_path(
59
- [self.xml_path]
65
+ class GeotagImagesFromExifToolRunner(GeotagImagesFromGeneric):
66
+ def _generate_image_extractors(
67
+ self,
68
+ ) -> T.Sequence[ImageExifToolExtractor | types.ErrorMetadata]:
69
+ runner = ExiftoolRunner(constants.EXIFTOOL_PATH)
70
+
71
+ LOG.debug(
72
+ "Extracting XML from %d images with exiftool command: %s",
73
+ len(self.image_paths),
74
+ " ".join(runner._build_args_read_stdin()),
60
75
  )
76
+ try:
77
+ xml = runner.extract_xml(self.image_paths)
78
+ except FileNotFoundError as ex:
79
+ raise exceptions.MapillaryExiftoolNotFoundError(ex) from ex
80
+
81
+ rdf_description_by_path = (
82
+ exiftool_read.index_rdf_description_by_path_from_xml_element(
83
+ ET.fromstring(xml)
84
+ )
85
+ )
86
+
87
+ results: list[ImageExifToolExtractor | types.ErrorMetadata] = []
61
88
 
62
- error_metadatas: T.List[types.ErrorMetadata] = []
63
- rdf_descriptions: T.List[ET.Element] = []
64
89
  for path in self.image_paths:
65
90
  rdf_description = rdf_description_by_path.get(
66
91
  exiftool_read.canonical_path(path)
@@ -69,41 +94,12 @@ class GeotagImagesFromExifTool(GeotagImagesFromGeneric):
69
94
  exc = exceptions.MapillaryEXIFNotFoundError(
70
95
  f"The {exiftool_read._DESCRIPTION_TAG} XML element for the image not found"
71
96
  )
72
- error_metadatas.append(
97
+ results.append(
73
98
  types.describe_error_metadata(
74
99
  exc, path, filetype=types.FileType.IMAGE
75
100
  )
76
101
  )
77
102
  else:
78
- rdf_descriptions.append(rdf_description)
79
-
80
- if self.num_processes is None:
81
- num_processes = self.num_processes
82
- disable_multiprocessing = False
83
- else:
84
- num_processes = max(self.num_processes, 1)
85
- disable_multiprocessing = self.num_processes <= 0
86
-
87
- with Pool(processes=num_processes) as pool:
88
- image_metadatas_iter: T.Iterator[types.ImageMetadataOrError]
89
- if disable_multiprocessing:
90
- image_metadatas_iter = map(
91
- GeotagImagesFromExifTool.geotag_image,
92
- rdf_descriptions,
93
- )
94
- else:
95
- image_metadatas_iter = pool.imap(
96
- GeotagImagesFromExifTool.geotag_image,
97
- rdf_descriptions,
98
- )
99
- image_metadata_or_errors = list(
100
- tqdm(
101
- image_metadatas_iter,
102
- desc="Extracting geotags from ExifTool XML",
103
- unit="images",
104
- disable=LOG.getEffectiveLevel() <= logging.DEBUG,
105
- total=len(self.image_paths),
106
- )
107
- )
103
+ results.append(ImageExifToolExtractor(path, rdf_description))
108
104
 
109
- return error_metadatas + image_metadata_or_errors
105
+ return results
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import logging
2
4
  import typing as T
3
5
  from pathlib import Path
@@ -20,28 +22,14 @@ class GeotagImagesFromExifToolBothImageAndVideo(GeotagImagesFromGeneric):
20
22
  image_paths: T.Sequence[Path],
21
23
  xml_path: Path,
22
24
  offset_time: float = 0.0,
23
- num_processes: T.Optional[int] = None,
25
+ num_processes: int | None = None,
24
26
  ):
25
- self.image_paths = image_paths
27
+ super().__init__(image_paths, num_processes=num_processes)
26
28
  self.xml_path = xml_path
27
29
  self.offset_time = offset_time
28
- self.num_processes = num_processes
29
- super().__init__()
30
-
31
- def to_description(self) -> T.List[types.ImageMetadataOrError]:
32
- # will return this list
33
- final_image_metadatas: T.List[types.ImageMetadataOrError] = []
34
30
 
35
- # find the images that can be geotagged from EXIF
36
- image_metadatas_from_exiftool = (
37
- geotag_images_from_exiftool.GeotagImagesFromExifTool(
38
- self.image_paths,
39
- self.xml_path,
40
- num_processes=self.num_processes,
41
- ).to_description()
42
- )
43
-
44
- # find all video paths in self.xml_path
31
+ def geotag_samples(self) -> list[types.ImageMetadataOrError]:
32
+ # Find all video paths in self.xml_path
45
33
  rdf_description_by_path = exiftool_read.index_rdf_description_by_path(
46
34
  [self.xml_path]
47
35
  )
@@ -49,45 +37,41 @@ class GeotagImagesFromExifToolBothImageAndVideo(GeotagImagesFromGeneric):
49
37
  [Path(pathstr) for pathstr in rdf_description_by_path.keys()],
50
38
  skip_subfolders=True,
51
39
  )
40
+ # Find all video paths that have sample images
41
+ samples_by_video = utils.find_all_image_samples(self.image_paths, video_paths)
52
42
 
53
- # will try to geotag these error metadatas from video later
54
- error_metadata_by_image_path = {}
55
- for image_metadata in image_metadatas_from_exiftool:
56
- if isinstance(image_metadata, types.ErrorMetadata):
57
- error_metadata_by_image_path[image_metadata.filename] = image_metadata
58
- else:
59
- final_image_metadatas.append(image_metadata)
60
-
61
- maybe_image_samples = list(error_metadata_by_image_path.keys())
62
-
63
- # find all video paths that have sample images
64
- video_paths_with_image_samples = list(
65
- utils.find_all_image_samples(maybe_image_samples, video_paths).keys()
66
- )
67
-
68
- video_metadatas = (
43
+ video_metadata_or_errors = (
69
44
  geotag_videos_from_exiftool_video.GeotagVideosFromExifToolVideo(
70
- video_paths_with_image_samples,
45
+ list(samples_by_video.keys()),
71
46
  self.xml_path,
72
47
  num_processes=self.num_processes,
73
48
  ).to_description()
74
49
  )
75
-
76
- image_metadatas_from_video = geotag_images_from_video.GeotagImagesFromVideo(
77
- maybe_image_samples,
78
- video_metadatas,
50
+ sample_paths = sum(samples_by_video.values(), [])
51
+ sample_metadata_or_errors = geotag_images_from_video.GeotagImagesFromVideo(
52
+ sample_paths,
53
+ video_metadata_or_errors,
79
54
  offset_time=self.offset_time,
80
55
  num_processes=self.num_processes,
81
56
  ).to_description()
82
- final_image_metadatas.extend(image_metadatas_from_video)
83
57
 
84
- # add back error metadatas that can not be geotagged at all
85
- actual_image_sample_paths = set(
86
- image_metadata.filename for image_metadata in image_metadatas_from_video
58
+ return sample_metadata_or_errors
59
+
60
+ def to_description(self) -> list[types.ImageMetadataOrError]:
61
+ sample_metadata_or_errors = self.geotag_samples()
62
+
63
+ sample_paths = set(metadata.filename for metadata in sample_metadata_or_errors)
64
+
65
+ non_sample_paths = [
66
+ path for path in self.image_paths if path not in sample_paths
67
+ ]
68
+
69
+ non_sample_metadata_or_errors = (
70
+ geotag_images_from_exiftool.GeotagImagesFromExifTool(
71
+ non_sample_paths,
72
+ self.xml_path,
73
+ num_processes=self.num_processes,
74
+ ).to_description()
87
75
  )
88
- for path, error_metadata in error_metadata_by_image_path.items():
89
- if path not in actual_image_sample_paths:
90
- final_image_metadatas.append(error_metadata)
91
76
 
92
- assert len(final_image_metadatas) <= len(self.image_paths)
93
- return final_image_metadatas
77
+ return sample_metadata_or_errors + non_sample_metadata_or_errors