mapillary-tools 0.13.3a1__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 +287 -22
  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 +17 -8
  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 +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 +408 -416
  61. mapillary_tools/upload_api_v4.py +172 -174
  62. mapillary_tools/uploader.py +804 -284
  63. mapillary_tools/utils.py +49 -18
  64. {mapillary_tools-0.13.3a1.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.3a1.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.3a1.dist-info/RECORD +0 -75
  84. /mapillary_tools/{geotag → gpmf}/gpmf_gps_filter.py +0 -0
  85. {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0.dist-info}/entry_points.txt +0 -0
  86. {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0.dist-info/licenses}/LICENSE +0 -0
  87. {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0.dist-info}/top_level.txt +0 -0
@@ -1,5 +1,6 @@
1
+ from __future__ import annotations
2
+
1
3
  import datetime
2
- import typing as T
3
4
  from pathlib import Path
4
5
 
5
6
  import pynmea2
@@ -11,15 +12,13 @@ from .geotag_images_from_gpx import GeotagImagesFromGPX
11
12
  class GeotagImagesFromNMEAFile(GeotagImagesFromGPX):
12
13
  def __init__(
13
14
  self,
14
- image_paths: T.Sequence[Path],
15
15
  source_path: Path,
16
16
  use_gpx_start_time: bool = False,
17
17
  offset_time: float = 0.0,
18
- num_processes: T.Optional[int] = None,
18
+ num_processes: int | None = None,
19
19
  ):
20
20
  points = get_lat_lon_time_from_nmea(source_path)
21
21
  super().__init__(
22
- image_paths,
23
22
  points,
24
23
  use_gpx_start_time=use_gpx_start_time,
25
24
  offset_time=offset_time,
@@ -27,7 +26,7 @@ class GeotagImagesFromNMEAFile(GeotagImagesFromGPX):
27
26
  )
28
27
 
29
28
 
30
- def get_lat_lon_time_from_nmea(nmea_file: Path) -> T.List[geo.Point]:
29
+ def get_lat_lon_time_from_nmea(nmea_file: Path) -> list[geo.Point]:
31
30
  with nmea_file.open("r") as f:
32
31
  lines = f.readlines()
33
32
  lines = [line.rstrip("\n\r") for line in lines]
@@ -1,13 +1,19 @@
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
16
+ from .geotag_videos_from_video import GeotagVideosFromVideo
11
17
 
12
18
 
13
19
  LOG = logging.getLogger(__name__)
@@ -16,75 +22,106 @@ LOG = logging.getLogger(__name__)
16
22
  class GeotagImagesFromVideo(GeotagImagesFromGeneric):
17
23
  def __init__(
18
24
  self,
19
- image_paths: T.Sequence[Path],
20
25
  video_metadatas: T.Sequence[types.VideoMetadataOrError],
21
26
  offset_time: float = 0.0,
22
- num_processes: T.Optional[int] = None,
27
+ num_processes: int | None = None,
23
28
  ):
24
- self.image_paths = image_paths
29
+ super().__init__(num_processes=num_processes)
25
30
  self.video_metadatas = video_metadatas
26
31
  self.offset_time = offset_time
27
- self.num_processes = num_processes
28
- super().__init__()
29
32
 
30
- def to_description(self) -> T.List[types.ImageMetadataOrError]:
31
- # will return this list
32
- final_image_metadatas: T.List[types.ImageMetadataOrError] = []
33
+ @override
34
+ def to_description(
35
+ self, image_paths: T.Sequence[Path]
36
+ ) -> list[types.ImageMetadataOrError]:
37
+ # Will return this list
38
+ final_image_metadatas: list[types.ImageMetadataOrError] = []
33
39
 
34
- for video_metadata in self.video_metadatas:
35
- video_path = video_metadata.filename
36
- LOG.debug("Processing video: %s", video_path)
40
+ video_metadatas, video_error_metadatas = types.separate_errors(
41
+ self.video_metadatas
42
+ )
37
43
 
38
- sample_image_paths = list(
39
- utils.filter_video_samples(self.image_paths, video_path)
44
+ for video_error_metadata in video_error_metadatas:
45
+ video_path = video_error_metadata.filename
46
+ sample_paths = list(utils.filter_video_samples(image_paths, video_path))
47
+ LOG.debug(
48
+ "Found %d sample images from video %s with error: %s",
49
+ len(sample_paths),
50
+ video_path,
51
+ video_error_metadata.error,
40
52
  )
53
+ for sample_path in sample_paths:
54
+ image_error_metadata = types.describe_error_metadata(
55
+ video_error_metadata.error,
56
+ sample_path,
57
+ filetype=types.FileType.IMAGE,
58
+ )
59
+ final_image_metadatas.append(image_error_metadata)
60
+
61
+ for video_metadata in video_metadatas:
62
+ video_path = video_metadata.filename
63
+
64
+ sample_paths = list(utils.filter_video_samples(image_paths, video_path))
41
65
  LOG.debug(
42
66
  "Found %d sample images from video %s",
43
- len(sample_image_paths),
67
+ len(sample_paths),
44
68
  video_path,
45
69
  )
46
70
 
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,
71
+ geotag = GeotagImagesFromGPX(
72
+ video_metadata.points,
73
+ use_gpx_start_time=False,
74
+ use_image_start_time=True,
75
+ offset_time=self.offset_time,
76
+ num_processes=self.num_processes,
79
77
  )
78
+
79
+ image_metadatas = geotag.to_description(image_paths)
80
+
80
81
  for metadata in image_metadatas:
81
82
  if isinstance(metadata, types.ImageMetadata):
82
83
  metadata.MAPDeviceMake = video_metadata.make
83
84
  metadata.MAPDeviceModel = video_metadata.model
84
85
 
86
+ final_image_metadatas.extend(image_metadatas)
87
+
85
88
  # NOTE: this method only geotags images that have a corresponding video,
86
89
  # so the number of image metadata objects returned might be less than
87
90
  # the number of the input image_paths
88
- assert len(final_image_metadatas) <= len(self.image_paths)
91
+ assert len(final_image_metadatas) <= len(image_paths)
89
92
 
90
93
  return final_image_metadatas
94
+
95
+
96
+ class GeotagImageSamplesFromVideo(GeotagImagesFromGeneric):
97
+ def __init__(
98
+ self,
99
+ source_path: Path,
100
+ filetypes: set[types.FileType] | None = None,
101
+ offset_time: float = 0.0,
102
+ num_processes: int | None = None,
103
+ ):
104
+ super().__init__(num_processes=num_processes)
105
+ self.source_path = source_path
106
+ self.filetypes = filetypes
107
+ self.offset_time = offset_time
108
+
109
+ @override
110
+ def to_description(
111
+ self, image_paths: T.Sequence[Path]
112
+ ) -> list[types.ImageMetadataOrError]:
113
+ video_paths = utils.find_videos([self.source_path])
114
+ image_samples_by_video_path = utils.find_all_image_samples(
115
+ image_paths, video_paths
116
+ )
117
+ video_paths_with_image_samples = list(image_samples_by_video_path.keys())
118
+ video_metadatas = GeotagVideosFromVideo(
119
+ filetypes=self.filetypes,
120
+ num_processes=self.num_processes,
121
+ ).to_description(video_paths_with_image_samples)
122
+ geotag = GeotagImagesFromVideo(
123
+ video_metadatas,
124
+ offset_time=self.offset_time,
125
+ num_processes=self.num_processes,
126
+ )
127
+ return geotag.to_description(image_paths)
@@ -0,0 +1,123 @@
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 . import options
17
+ from .base import GeotagVideosFromGeneric
18
+ from .utils import index_rdf_description_by_path
19
+ from .video_extractors.exiftool import VideoExifToolExtractor
20
+
21
+ LOG = logging.getLogger(__name__)
22
+
23
+
24
+ class GeotagVideosFromExifToolXML(GeotagVideosFromGeneric):
25
+ def __init__(
26
+ self, source_path: options.SourcePathOption, num_processes: int | None = None
27
+ ):
28
+ super().__init__(num_processes=num_processes)
29
+ self.source_path = source_path
30
+
31
+ @classmethod
32
+ def build_video_extractors_from_etree(
33
+ cls, rdf_by_path: dict[str, ET.Element], video_paths: T.Iterable[Path]
34
+ ) -> list[VideoExifToolExtractor | types.ErrorMetadata]:
35
+ results: list[VideoExifToolExtractor | types.ErrorMetadata] = []
36
+
37
+ for path in video_paths:
38
+ rdf = rdf_by_path.get(exiftool_read.canonical_path(path))
39
+ if rdf is None:
40
+ ex = exceptions.MapillaryExifToolXMLNotFoundError(
41
+ "Cannot find the video in the ExifTool XML"
42
+ )
43
+ results.append(
44
+ types.describe_error_metadata(
45
+ ex, path, filetype=types.FileType.VIDEO
46
+ )
47
+ )
48
+ else:
49
+ results.append(VideoExifToolExtractor(path, rdf))
50
+
51
+ return results
52
+
53
+ @override
54
+ def _generate_video_extractors(
55
+ self, video_paths: T.Sequence[Path]
56
+ ) -> T.Sequence[VideoExifToolExtractor | types.ErrorMetadata]:
57
+ rdf_by_path = self.find_rdf_by_path(self.source_path, video_paths)
58
+ return self.build_video_extractors_from_etree(rdf_by_path, video_paths)
59
+
60
+ @classmethod
61
+ def find_rdf_by_path(
62
+ cls, option: options.SourcePathOption, paths: T.Iterable[Path]
63
+ ) -> dict[str, ET.Element]:
64
+ if option.source_path is not None:
65
+ return index_rdf_description_by_path([option.source_path])
66
+
67
+ elif option.pattern is not None:
68
+ rdf_by_path = {}
69
+ for path in paths:
70
+ source_path = option.resolve(path)
71
+ r = index_rdf_description_by_path([source_path])
72
+ rdfs = list(r.values())
73
+ if rdfs:
74
+ rdf_by_path[exiftool_read.canonical_path(path)] = rdfs[0]
75
+ return rdf_by_path
76
+
77
+ else:
78
+ assert False, "Either source_path or pattern must be provided"
79
+
80
+
81
+ class GeotagVideosFromExifToolRunner(GeotagVideosFromGeneric):
82
+ @override
83
+ def _generate_video_extractors(
84
+ self, video_paths: T.Sequence[Path]
85
+ ) -> T.Sequence[VideoExifToolExtractor | types.ErrorMetadata]:
86
+ if constants.EXIFTOOL_PATH is None:
87
+ runner = ExiftoolRunner()
88
+ else:
89
+ runner = ExiftoolRunner(constants.EXIFTOOL_PATH)
90
+
91
+ LOG.debug(
92
+ "Extracting XML from %d videos with ExifTool command: %s",
93
+ len(video_paths),
94
+ " ".join(runner._build_args_read_stdin()),
95
+ )
96
+ try:
97
+ xml = runner.extract_xml(video_paths)
98
+ except FileNotFoundError as ex:
99
+ exiftool_ex = exceptions.MapillaryExiftoolNotFoundError(ex)
100
+ return [
101
+ types.describe_error_metadata(
102
+ exiftool_ex, video_path, filetype=types.FileType.VIDEO
103
+ )
104
+ for video_path in video_paths
105
+ ]
106
+
107
+ try:
108
+ xml_element = ET.fromstring(xml)
109
+ except ET.ParseError as ex:
110
+ LOG.warning(
111
+ "Failed to parse ExifTool XML: %s",
112
+ str(ex),
113
+ exc_info=LOG.getEffectiveLevel() <= logging.DEBUG,
114
+ )
115
+ rdf_by_path = {}
116
+ else:
117
+ rdf_by_path = exiftool_read.index_rdf_description_by_path_from_xml_element(
118
+ xml_element
119
+ )
120
+
121
+ return GeotagVideosFromExifToolXML.build_video_extractors_from_etree(
122
+ rdf_by_path, video_paths
123
+ )
@@ -0,0 +1,52 @@
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 exceptions, types
14
+ from . import options
15
+ from .base import GeotagVideosFromGeneric
16
+ from .video_extractors.gpx import GPXVideoExtractor
17
+
18
+
19
+ LOG = logging.getLogger(__name__)
20
+
21
+
22
+ class GeotagVideosFromGPX(GeotagVideosFromGeneric):
23
+ def __init__(
24
+ self,
25
+ source_path: options.SourcePathOption | None = None,
26
+ num_processes: int | None = None,
27
+ ):
28
+ super().__init__(num_processes=num_processes)
29
+ if source_path is None:
30
+ source_path = options.SourcePathOption(pattern="%g.gpx")
31
+ self.source_path = source_path
32
+
33
+ @override
34
+ def _generate_video_extractors(
35
+ self, video_paths: T.Sequence[Path]
36
+ ) -> T.Sequence[GPXVideoExtractor | types.ErrorMetadata]:
37
+ results: list[GPXVideoExtractor | types.ErrorMetadata] = []
38
+ for video_path in video_paths:
39
+ source_path = self.source_path.resolve(video_path)
40
+ if source_path.is_file():
41
+ results.append(GPXVideoExtractor(video_path, source_path))
42
+ else:
43
+ results.append(
44
+ types.describe_error_metadata(
45
+ exceptions.MapillaryVideoGPSNotFoundError(
46
+ "GPX file not found for video"
47
+ ),
48
+ filename=video_path,
49
+ filetype=types.FileType.VIDEO,
50
+ )
51
+ )
52
+ return results
@@ -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