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,197 +1,165 @@
1
- import io
2
- import logging
1
+ from __future__ import annotations
2
+
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, geo, types, utils
6
+ from .. import blackvue_parser, exceptions, geo, telemetry, types, utils
10
7
  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
8
+ from ..gpmf import gpmf_gps_filter, gpmf_parser
9
+ from ..types import FileType
10
+ from .geotag_from_generic import GenericVideoExtractor, GeotagVideosFromGeneric
15
11
 
16
- LOG = logging.getLogger(__name__)
17
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)
18
17
 
19
- class GeotagVideosFromVideo(GeotagVideosFromGeneric):
20
- def __init__(
21
- self,
22
- video_paths: T.Sequence[Path],
23
- filetypes: T.Optional[T.Set[types.FileType]] = None,
24
- num_processes: T.Optional[int] = None,
25
- ):
26
- self.video_paths = video_paths
27
- 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
- )
18
+ if gopro_info is None:
19
+ raise exceptions.MapillaryVideoGPSNotFoundError(
20
+ "No GPS data found from the video"
55
21
  )
56
22
 
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
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
+
84
+
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"
151
93
  )
152
94
 
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
- )
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")
176
99
  )
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
100
  return types.describe_error_metadata(
192
- ex,
193
- video_path,
194
- filetype=filetype,
101
+ ex, self.video_path, filetype=FileType.BLACKVUE
195
102
  )
196
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
+
197
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
+ )
149
+
150
+
151
+ class GeotagVideosFromVideo(GeotagVideosFromGeneric):
152
+ def __init__(
153
+ self,
154
+ video_paths: T.Sequence[Path],
155
+ filetypes: set[FileType] | None = None,
156
+ num_processes: int | None = None,
157
+ ):
158
+ super().__init__(video_paths, num_processes=num_processes)
159
+ self.filetypes = filetypes
160
+
161
+ def _generate_video_extractors(self) -> T.Sequence[GenericVideoExtractor]:
162
+ return [
163
+ NativeVideoExtractor(path, filetypes=self.filetypes)
164
+ for path in self.video_paths
165
+ ]
@@ -0,0 +1,159 @@
1
+ from __future__ import annotations
2
+
3
+ import dataclasses
4
+ import enum
5
+ import json
6
+ import typing as T
7
+ from pathlib import Path
8
+
9
+ import jsonschema
10
+
11
+ from .. import types
12
+
13
+
14
+ class SourceType(enum.Enum):
15
+ NATIVE = "native"
16
+ GPX = "gpx"
17
+ NMEA = "nmea"
18
+ EXIFTOOL_XML = "exiftool_xml"
19
+ EXIFTOOL_RUNTIME = "exiftool_runtime"
20
+
21
+ # Legacy source types for images
22
+ GOPRO = "gopro"
23
+ BLACKVUE = "blackvue"
24
+ CAMM = "camm"
25
+ EXIF = "exif"
26
+
27
+
28
+ SOURCE_TYPE_ALIAS: dict[str, SourceType] = {
29
+ "blackvue_videos": SourceType.BLACKVUE,
30
+ "gopro_videos": SourceType.GOPRO,
31
+ }
32
+
33
+
34
+ @dataclasses.dataclass
35
+ class SourceOption:
36
+ # Type of the source
37
+ source: SourceType
38
+
39
+ # Filter by these filetypes
40
+ filetypes: set[types.FileType] | None = None
41
+
42
+ num_processes: int | None = None
43
+
44
+ source_path: SourcePathOption | None = None
45
+
46
+ interpolation: InterpolationOption | None = None
47
+
48
+ @classmethod
49
+ def from_dict(cls, data: dict[str, T.Any]) -> SourceOption:
50
+ validate_option(data)
51
+
52
+ kwargs: dict[str, T.Any] = {}
53
+ for k, v in data.items():
54
+ # None values are considered as absent and should be ignored
55
+ if v is None:
56
+ continue
57
+ if k == "source":
58
+ kwargs[k] = SourceType(SOURCE_TYPE_ALIAS.get(v, v))
59
+ elif k == "filetypes":
60
+ kwargs[k] = {types.FileType(t) for t in v}
61
+ elif k == "source_path":
62
+ kwargs.setdefault("source_path", SourcePathOption()).source_path = v
63
+ elif k == "pattern":
64
+ kwargs.setdefault("source_path", SourcePathOption()).pattern = v
65
+ elif k == "interpolation_offset_time":
66
+ kwargs.setdefault(
67
+ "interpolation", InterpolationOption()
68
+ ).offset_time = v
69
+ elif k == "interpolation_use_gpx_start_time":
70
+ kwargs.setdefault(
71
+ "interpolation", InterpolationOption()
72
+ ).use_gpx_start_time = v
73
+
74
+ return cls(**kwargs)
75
+
76
+
77
+ @dataclasses.dataclass
78
+ class SourcePathOption:
79
+ pattern: str | None = None
80
+ source_path: Path | None = None
81
+
82
+ def __post_init__(self):
83
+ if self.source_path is None and self.pattern is None:
84
+ raise ValueError("Either pattern or source_path must be provided")
85
+
86
+ def resolve(self, path: Path) -> Path:
87
+ if self.source_path is not None:
88
+ return self.source_path
89
+
90
+ assert self.pattern is not None, (
91
+ "either pattern or source_path must be provided"
92
+ )
93
+
94
+ # %f: the full video filename (foo.mp4)
95
+ # %g: the video filename without extension (foo)
96
+ # %e: the video filename extension (.mp4)
97
+ replaced = Path(
98
+ self.pattern.replace("%f", path.name)
99
+ .replace("%g", path.stem)
100
+ .replace("%e", path.suffix)
101
+ )
102
+
103
+ abs_path = (
104
+ replaced
105
+ if replaced.is_absolute()
106
+ else Path.joinpath(path.parent.resolve(), replaced)
107
+ ).resolve()
108
+
109
+ return abs_path
110
+
111
+
112
+ @dataclasses.dataclass
113
+ class InterpolationOption:
114
+ offset_time: float = 0.0
115
+ use_gpx_start_time: bool = False
116
+
117
+
118
+ SourceOptionSchema = {
119
+ "type": "object",
120
+ "properties": {
121
+ "source": {
122
+ "type": "string",
123
+ "enum": [s.value for s in SourceType] + list(SOURCE_TYPE_ALIAS.keys()),
124
+ },
125
+ "filetypes": {
126
+ "type": "array",
127
+ "items": {
128
+ "type": "string",
129
+ "enum": [t.value for t in types.FileType],
130
+ },
131
+ },
132
+ "source_path": {
133
+ "type": "string",
134
+ },
135
+ "pattern": {
136
+ "type": "string",
137
+ },
138
+ "num_processes": {
139
+ "type": "integer",
140
+ },
141
+ "interpolation_offset_time": {
142
+ "type": "float",
143
+ },
144
+ "interpolation_use_gpx_start_time": {
145
+ "type": "boolean",
146
+ },
147
+ },
148
+ "required": ["source"],
149
+ "additionalProperties": False,
150
+ }
151
+
152
+
153
+ def validate_option(instance):
154
+ jsonschema.validate(instance=instance, schema=SourceOptionSchema)
155
+
156
+
157
+ if __name__ == "__main__":
158
+ # python -m mapillary_tools.geotag.options > schema/geotag_source_option.json
159
+ print(json.dumps(SourceOptionSchema, indent=4))