mapillary-tools 0.10.6a1__py3-none-any.whl → 0.11.0b2__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 (33) hide show
  1. mapillary_tools/__init__.py +1 -1
  2. mapillary_tools/api_v4.py +2 -9
  3. mapillary_tools/commands/__main__.py +5 -3
  4. mapillary_tools/commands/process.py +9 -1
  5. mapillary_tools/constants.py +1 -0
  6. mapillary_tools/exceptions.py +4 -0
  7. mapillary_tools/exif_read.py +46 -10
  8. mapillary_tools/exiftool_read.py +4 -0
  9. mapillary_tools/ffmpeg.py +3 -8
  10. mapillary_tools/geotag/construct_mp4_parser.py +2 -8
  11. mapillary_tools/geotag/gpmf_parser.py +1 -7
  12. mapillary_tools/process_geotag_properties.py +115 -51
  13. mapillary_tools/process_sequence_properties.py +3 -3
  14. mapillary_tools/types.py +1 -6
  15. mapillary_tools/uploader.py +2 -9
  16. mapillary_tools/video_data_extraction/cli_options.py +22 -0
  17. mapillary_tools/video_data_extraction/extract_video_data.py +190 -0
  18. mapillary_tools/video_data_extraction/extractors/base_parser.py +73 -0
  19. mapillary_tools/video_data_extraction/extractors/blackvue_parser.py +33 -0
  20. mapillary_tools/video_data_extraction/extractors/camm_parser.py +41 -0
  21. mapillary_tools/video_data_extraction/extractors/exiftool_runtime_parser.py +56 -0
  22. mapillary_tools/video_data_extraction/extractors/exiftool_xml_parser.py +55 -0
  23. mapillary_tools/video_data_extraction/extractors/generic_video_parser.py +57 -0
  24. mapillary_tools/video_data_extraction/extractors/gopro_parser.py +36 -0
  25. mapillary_tools/video_data_extraction/extractors/gpx_parser.py +29 -0
  26. mapillary_tools/video_data_extraction/extractors/nmea_parser.py +24 -0
  27. mapillary_tools/video_data_extraction/video_data_parser_factory.py +46 -0
  28. {mapillary_tools-0.10.6a1.dist-info → mapillary_tools-0.11.0b2.dist-info}/METADATA +98 -11
  29. {mapillary_tools-0.10.6a1.dist-info → mapillary_tools-0.11.0b2.dist-info}/RECORD +33 -21
  30. {mapillary_tools-0.10.6a1.dist-info → mapillary_tools-0.11.0b2.dist-info}/WHEEL +1 -1
  31. {mapillary_tools-0.10.6a1.dist-info → mapillary_tools-0.11.0b2.dist-info}/LICENSE +0 -0
  32. {mapillary_tools-0.10.6a1.dist-info → mapillary_tools-0.11.0b2.dist-info}/entry_points.txt +0 -0
  33. {mapillary_tools-0.10.6a1.dist-info → mapillary_tools-0.11.0b2.dist-info}/top_level.txt +0 -0
@@ -1 +1 @@
1
- VERSION = "0.10.6a1"
1
+ VERSION = "0.11.0b2"
mapillary_tools/api_v4.py CHANGED
@@ -1,15 +1,8 @@
1
1
  import os
2
- import sys
3
2
  import typing as T
4
- from typing import Union
5
3
 
6
4
  import requests
7
5
 
8
- if sys.version_info >= (3, 8):
9
- from typing import Literal # pylint: disable=no-name-in-module
10
- else:
11
- from typing_extensions import Literal
12
-
13
6
  MAPILLARY_CLIENT_TOKEN = os.getenv(
14
7
  "MAPILLARY_CLIENT_TOKEN", "MLY|5675152195860640|6b02c72e6e3c801e5603ab0495623282"
15
8
  )
@@ -31,7 +24,7 @@ def get_upload_token(email: str, password: str) -> requests.Response:
31
24
 
32
25
 
33
26
  def fetch_organization(
34
- user_access_token: str, organization_id: Union[int, str]
27
+ user_access_token: str, organization_id: T.Union[int, str]
35
28
  ) -> requests.Response:
36
29
  resp = requests.get(
37
30
  f"{MAPILLARY_GRAPH_API_ENDPOINT}/{organization_id}",
@@ -47,7 +40,7 @@ def fetch_organization(
47
40
  return resp
48
41
 
49
42
 
50
- ActionType = Literal[
43
+ ActionType = T.Literal[
51
44
  "upload_started_upload", "upload_finished_upload", "upload_failed_upload"
52
45
  ]
53
46
 
@@ -63,14 +63,14 @@ def add_general_arguments(parser, command):
63
63
  elif command in ["upload"]:
64
64
  parser.add_argument(
65
65
  "import_path",
66
- help="Path to your images.",
66
+ help="Paths to your images or videos.",
67
67
  nargs="+",
68
68
  type=Path,
69
69
  )
70
70
  elif command in ["process", "process_and_upload"]:
71
71
  parser.add_argument(
72
72
  "import_path",
73
- help="Path to your images.",
73
+ help="Paths to your images or videos.",
74
74
  nargs="+",
75
75
  type=Path,
76
76
  )
@@ -167,7 +167,9 @@ def main():
167
167
  try:
168
168
  args.func(argvars)
169
169
  except exceptions.MapillaryUserError as exc:
170
- LOG.error("%s: %s", exc.__class__.__name__, exc)
170
+ LOG.error(
171
+ "%s: %s", exc.__class__.__name__, exc, exc_info=log_level == logging.DEBUG
172
+ )
171
173
  sys.exit(exc.exit_code)
172
174
 
173
175
 
@@ -171,6 +171,13 @@ class Command:
171
171
  required=False,
172
172
  type=Path,
173
173
  )
174
+ group_geotagging.add_argument(
175
+ "--video_geotag_source",
176
+ help="Name of the video data extractor and optional arguments. Can be specified multiple times. See the documentation for details. [Experimental, subject to change]",
177
+ action="append",
178
+ default=[],
179
+ required=False,
180
+ )
174
181
  group_geotagging.add_argument(
175
182
  "--interpolation_use_gpx_start_time",
176
183
  help=f"If supplied, the first image will use the first GPX point time for interpolation, which means the image location will be interpolated to the first GPX point too. Only works for geotagging from {', '.join(geotag_gpx_based_sources)}.",
@@ -261,13 +268,14 @@ class Command:
261
268
  vars_args["duplicate_angle"] = 360
262
269
 
263
270
  metadatas = process_geotag_properties(
271
+ vars_args=vars_args,
264
272
  **(
265
273
  {
266
274
  k: v
267
275
  for k, v in vars_args.items()
268
276
  if k in inspect.getfullargspec(process_geotag_properties).args
269
277
  }
270
- )
278
+ ),
271
279
  )
272
280
 
273
281
  metadatas = process_import_meta_properties(
@@ -18,6 +18,7 @@ VIDEO_SAMPLE_DISTANCE = float(os.getenv(_ENV_PREFIX + "VIDEO_SAMPLE_DISTANCE", 3
18
18
  VIDEO_DURATION_RATIO = float(os.getenv(_ENV_PREFIX + "VIDEO_DURATION_RATIO", 1))
19
19
  FFPROBE_PATH: str = os.getenv(_ENV_PREFIX + "FFPROBE_PATH", "ffprobe")
20
20
  FFMPEG_PATH: str = os.getenv(_ENV_PREFIX + "FFMPEG_PATH", "ffmpeg")
21
+ EXIFTOOL_PATH: str = os.getenv(_ENV_PREFIX + "EXIFTOOL_PATH", "exiftool")
21
22
  IMAGE_DESCRIPTION_FILENAME = os.getenv(
22
23
  _ENV_PREFIX + "IMAGE_DESCRIPTION_FILENAME", "mapillary_image_description.json"
23
24
  )
@@ -34,6 +34,10 @@ class MapillaryFFmpegNotFoundError(MapillaryUserError):
34
34
  help = "https://github.com/mapillary/mapillary_tools#video-support"
35
35
 
36
36
 
37
+ class MapillaryExiftoolNotFoundError(MapillaryUserError):
38
+ exit_code = 8
39
+
40
+
37
41
  class MapillaryDescriptionError(Exception):
38
42
  pass
39
43
 
@@ -1,8 +1,10 @@
1
1
  import abc
2
2
  import datetime
3
3
  import logging
4
+ import re
4
5
  import typing as T
5
6
  import xml.etree.ElementTree as et
7
+ from fractions import Fraction
6
8
  from pathlib import Path
7
9
 
8
10
  import exifread
@@ -21,6 +23,8 @@ XMP_NAMESPACES = {
21
23
  # https://github.com/ianare/exif-py/issues/167
22
24
  EXIFREAD_LOG = logging.getLogger("exifread")
23
25
  EXIFREAD_LOG.setLevel(logging.ERROR)
26
+ SIGN_BY_DIRECTION = {None: 1, "N": 1, "S": -1, "E": 1, "W": -1}
27
+ ADOBE_FORMAT_REGEX = re.compile(r"(\d+),(\d{1,3}\.?\d*)([NSWE])")
24
28
 
25
29
 
26
30
  def eval_frac(value: Ratio) -> float:
@@ -47,6 +51,38 @@ def gps_to_decimal(values: T.Tuple[Ratio, Ratio, Ratio]) -> T.Optional[float]:
47
51
  return degrees + minutes / 60 + seconds / 3600
48
52
 
49
53
 
54
+ def _parse_coord_numeric(coord: str, ref: T.Optional[str]) -> T.Optional[float]:
55
+ try:
56
+ return float(coord) * SIGN_BY_DIRECTION[ref]
57
+ except (ValueError, KeyError):
58
+ return None
59
+
60
+
61
+ def _parse_coord_adobe(coord: str) -> T.Optional[float]:
62
+ """
63
+ Parse Adobe coordinate format: <degrees,fractionalminutes[NSEW]>
64
+ """
65
+ matches = ADOBE_FORMAT_REGEX.match(coord)
66
+ if matches:
67
+ deg = Ratio(int(matches.group(1)), 1)
68
+ min_frac = Fraction.from_float(float(matches.group(2)))
69
+ min = Ratio(min_frac.numerator, min_frac.denominator)
70
+ sec = Ratio(0, 1)
71
+ converted = gps_to_decimal((deg, min, sec))
72
+ if converted is not None:
73
+ return converted * SIGN_BY_DIRECTION[matches.group(3)]
74
+ return None
75
+
76
+
77
+ def _parse_coord(coord: T.Optional[str], ref: T.Optional[str]) -> T.Optional[float]:
78
+ if coord is None:
79
+ return None
80
+ parsed = _parse_coord_numeric(coord, ref)
81
+ if parsed is None:
82
+ parsed = _parse_coord_adobe(coord)
83
+ return parsed
84
+
85
+
50
86
  def _parse_iso(dtstr: str) -> T.Optional[datetime.datetime]:
51
87
  try:
52
88
  return datetime.datetime.fromisoformat(dtstr)
@@ -378,22 +414,22 @@ class ExifReadFromXMP(ExifReadABC):
378
414
  )
379
415
 
380
416
  def extract_lon_lat(self) -> T.Optional[T.Tuple[float, float]]:
381
- lat = self._extract_alternative_fields(["exif:GPSLatitude"], float)
417
+ lat_ref = self._extract_alternative_fields(["exif:GPSLatitudeRef"], str)
418
+ lat_str: T.Optional[str] = self._extract_alternative_fields(
419
+ ["exif:GPSLatitude"], str
420
+ )
421
+ lat: T.Optional[float] = _parse_coord(lat_str, lat_ref)
382
422
  if lat is None:
383
423
  return None
384
424
 
385
- lon = self._extract_alternative_fields(["exif:GPSLongitude"], float)
425
+ lon_ref = self._extract_alternative_fields(["exif:GPSLongitudeRef"], str)
426
+ lon_str: T.Optional[str] = self._extract_alternative_fields(
427
+ ["exif:GPSLongitude"], str
428
+ )
429
+ lon = _parse_coord(lon_str, lon_ref)
386
430
  if lon is None:
387
431
  return None
388
432
 
389
- ref = self._extract_alternative_fields(["exif:GPSLongitudeRef"], str)
390
- if ref and ref.upper() == "W":
391
- lon = -1 * lon
392
-
393
- ref = self._extract_alternative_fields(["exif:GPSLatitudeRef"], str)
394
- if ref and ref.upper() == "S":
395
- lat = -1 * lat
396
-
397
433
  return lon, lat
398
434
 
399
435
  def extract_make(self) -> T.Optional[str]:
@@ -310,6 +310,10 @@ class ExifToolRead(exif_read.ExifReadABC):
310
310
  if lon_lat is not None:
311
311
  return lon_lat
312
312
 
313
+ lon_lat = self._extract_lon_lat("XMP-exif:GPSLongitude", "XMP-exif:GPSLatitude")
314
+ if lon_lat is not None:
315
+ return lon_lat
316
+
313
317
  return None
314
318
 
315
319
  def _extract_lon_lat(
mapillary_tools/ffmpeg.py CHANGED
@@ -11,22 +11,17 @@ import tempfile
11
11
  import typing as T
12
12
  from pathlib import Path
13
13
 
14
- if sys.version_info >= (3, 8):
15
- from typing import TypedDict # pylint: disable=no-name-in-module
16
- else:
17
- from typing_extensions import TypedDict
18
-
19
14
  LOG = logging.getLogger(__name__)
20
15
  FRAME_EXT = ".jpg"
21
16
  NA_STREAM_IDX = "NA"
22
17
 
23
18
 
24
- class StreamTag(TypedDict):
19
+ class StreamTag(T.TypedDict):
25
20
  creation_time: str
26
21
  language: str
27
22
 
28
23
 
29
- class Stream(TypedDict):
24
+ class Stream(T.TypedDict):
30
25
  codec_name: str
31
26
  codec_tag_string: str
32
27
  codec_type: str
@@ -37,7 +32,7 @@ class Stream(TypedDict):
37
32
  width: int
38
33
 
39
34
 
40
- class ProbeOutput(TypedDict):
35
+ class ProbeOutput(T.TypedDict):
41
36
  streams: T.List[Stream]
42
37
 
43
38
 
@@ -1,17 +1,11 @@
1
1
  # pyre-ignore-all-errors[5, 16, 21, 58]
2
2
 
3
- import sys
4
3
  import typing as T
5
4
 
6
- if sys.version_info >= (3, 8):
7
- from typing import Literal, TypedDict # pylint: disable=no-name-in-module
8
- else:
9
- from typing_extensions import Literal, TypedDict
10
-
11
5
  import construct as C
12
6
 
13
7
 
14
- BoxType = Literal[
8
+ BoxType = T.Literal[
15
9
  b"@mak",
16
10
  b"@mod",
17
11
  b"co64",
@@ -46,7 +40,7 @@ BoxType = Literal[
46
40
  ]
47
41
 
48
42
 
49
- class BoxDict(TypedDict, total=True):
43
+ class BoxDict(T.TypedDict, total=True):
50
44
  type: BoxType
51
45
  data: T.Union[T.Sequence["BoxDict"], T.Dict[str, T.Any], bytes]
52
46
 
@@ -1,13 +1,7 @@
1
1
  import io
2
2
  import pathlib
3
- import sys
4
3
  import typing as T
5
4
 
6
- if sys.version_info >= (3, 8):
7
- from typing import TypedDict # pylint: disable=no-name-in-module
8
- else:
9
- from typing_extensions import TypedDict
10
-
11
5
  import construct as C
12
6
 
13
7
  from .. import geo
@@ -35,7 +29,7 @@ NOTE:
35
29
  """
36
30
 
37
31
 
38
- class KLVDict(TypedDict):
32
+ class KLVDict(T.TypedDict):
39
33
  key: bytes
40
34
  type: bytes
41
35
  structure_size: int
@@ -3,16 +3,10 @@ import datetime
3
3
  import itertools
4
4
  import json
5
5
  import logging
6
- import sys
7
6
  import typing as T
8
7
  from multiprocessing import Pool
9
8
  from pathlib import Path
10
9
 
11
- if sys.version_info >= (3, 8):
12
- from typing import Literal # pylint: disable=no-name-in-module
13
- else:
14
- from typing_extensions import Literal
15
-
16
10
  from tqdm import tqdm
17
11
 
18
12
  from . import constants, exceptions, exif_write, history, types, utils
@@ -26,16 +20,30 @@ from .geotag import (
26
20
  geotag_videos_from_exiftool_video,
27
21
  geotag_videos_from_video,
28
22
  )
29
- from .types import FileType
23
+ from .types import FileType, VideoMetadataOrError
24
+
25
+ from .video_data_extraction.cli_options import CliOptions, CliParserOptions
26
+ from .video_data_extraction.extract_video_data import VideoDataExtractor
30
27
 
31
28
 
32
29
  LOG = logging.getLogger(__name__)
33
30
 
34
31
 
35
- GeotagSource = Literal[
32
+ GeotagSource = T.Literal[
36
33
  "gopro_videos", "blackvue_videos", "camm", "exif", "gpx", "nmea", "exiftool"
37
34
  ]
38
35
 
36
+ VideoGeotagSource = T.Literal[
37
+ "video",
38
+ "camm",
39
+ "gopro",
40
+ "blackvue",
41
+ "gpx",
42
+ "nmea",
43
+ "exiftool_xml",
44
+ "exiftool_runtime",
45
+ ]
46
+
39
47
 
40
48
  def _process_images(
41
49
  image_paths: T.Sequence[Path],
@@ -46,7 +54,7 @@ def _process_images(
46
54
  interpolation_offset_time: float = 0.0,
47
55
  num_processes: T.Optional[int] = None,
48
56
  skip_subfolders=False,
49
- ) -> T.List[types.ImageMetadataOrError]:
57
+ ) -> T.Sequence[types.ImageMetadataOrError]:
50
58
  geotag: geotag_from_generic.GeotagImagesFromGeneric
51
59
 
52
60
  if video_import_path is not None:
@@ -130,6 +138,37 @@ def _process_images(
130
138
  return geotag.to_description()
131
139
 
132
140
 
141
+ def _process_videos(
142
+ geotag_source: str,
143
+ geotag_source_path: T.Optional[Path],
144
+ video_paths: T.Sequence[Path],
145
+ num_processes: T.Optional[int],
146
+ filetypes: T.Optional[T.Set[FileType]],
147
+ ) -> T.Sequence[VideoMetadataOrError]:
148
+ geotag: geotag_from_generic.GeotagVideosFromGeneric
149
+ if geotag_source == "exiftool":
150
+ if geotag_source_path is None:
151
+ raise exceptions.MapillaryFileNotFoundError(
152
+ "Geotag source path (--geotag_source_path) is required"
153
+ )
154
+ if not geotag_source_path.exists():
155
+ raise exceptions.MapillaryFileNotFoundError(
156
+ f"Geotag source file not found: {geotag_source_path}"
157
+ )
158
+ geotag = geotag_videos_from_exiftool_video.GeotagVideosFromExifToolVideo(
159
+ video_paths,
160
+ geotag_source_path,
161
+ num_processes=num_processes,
162
+ )
163
+ else:
164
+ geotag = geotag_videos_from_video.GeotagVideosFromVideo(
165
+ video_paths,
166
+ filetypes=filetypes,
167
+ num_processes=num_processes,
168
+ )
169
+ return geotag.to_description()
170
+
171
+
133
172
  def _normalize_import_paths(
134
173
  import_path: T.Union[Path, T.Sequence[Path]]
135
174
  ) -> T.Sequence[Path]:
@@ -143,6 +182,7 @@ def _normalize_import_paths(
143
182
 
144
183
 
145
184
  def process_geotag_properties(
185
+ vars_args: T.Dict, # Hello, I'm a hack
146
186
  import_path: T.Union[Path, T.Sequence[Path]],
147
187
  filetypes: T.Set[FileType],
148
188
  geotag_source: GeotagSource,
@@ -176,51 +216,43 @@ def process_geotag_properties(
176
216
  skip_subfolders=skip_subfolders,
177
217
  check_file_suffix=check_file_suffix,
178
218
  )
179
- image_metadatas = _process_images(
180
- image_paths,
181
- geotag_source=geotag_source,
182
- geotag_source_path=geotag_source_path,
183
- video_import_path=video_import_path,
184
- interpolation_use_gpx_start_time=interpolation_use_gpx_start_time,
185
- interpolation_offset_time=interpolation_offset_time,
186
- num_processes=num_processes,
187
- skip_subfolders=skip_subfolders,
188
- )
189
- metadatas.extend(image_metadatas)
190
-
191
- if (
192
- FileType.CAMM in filetypes
193
- or FileType.GOPRO in filetypes
194
- or FileType.BLACKVUE in filetypes
195
- or FileType.VIDEO in filetypes
196
- ):
197
- video_paths = utils.find_videos(
198
- import_paths,
199
- skip_subfolders=skip_subfolders,
200
- check_file_suffix=check_file_suffix,
201
- )
202
- geotag: geotag_from_generic.GeotagVideosFromGeneric
203
- if geotag_source == "exiftool":
204
- if geotag_source_path is None:
205
- raise exceptions.MapillaryFileNotFoundError(
206
- "Geotag source path (--geotag_source_path) is required"
207
- )
208
- if not geotag_source_path.exists():
209
- raise exceptions.MapillaryFileNotFoundError(
210
- f"Geotag source file not found: {geotag_source_path}"
211
- )
212
- geotag = geotag_videos_from_exiftool_video.GeotagVideosFromExifToolVideo(
213
- video_paths,
214
- geotag_source_path,
219
+ if image_paths:
220
+ image_metadatas = _process_images(
221
+ image_paths,
222
+ geotag_source=geotag_source,
223
+ geotag_source_path=geotag_source_path,
224
+ video_import_path=video_import_path,
225
+ interpolation_use_gpx_start_time=interpolation_use_gpx_start_time,
226
+ interpolation_offset_time=interpolation_offset_time,
215
227
  num_processes=num_processes,
228
+ skip_subfolders=skip_subfolders,
216
229
  )
217
- else:
218
- geotag = geotag_videos_from_video.GeotagVideosFromVideo(
219
- video_paths,
220
- filetypes=filetypes,
221
- num_processes=num_processes,
230
+ metadatas.extend(image_metadatas)
231
+
232
+ # --video_geotag_source is still experimental, for videos execute it XOR the legacy code
233
+ if vars_args["video_geotag_source"]:
234
+ metadatas.extend(_process_videos_beta(vars_args))
235
+ else:
236
+ if (
237
+ FileType.CAMM in filetypes
238
+ or FileType.GOPRO in filetypes
239
+ or FileType.BLACKVUE in filetypes
240
+ or FileType.VIDEO in filetypes
241
+ ):
242
+ video_paths = utils.find_videos(
243
+ import_paths,
244
+ skip_subfolders=skip_subfolders,
245
+ check_file_suffix=check_file_suffix,
222
246
  )
223
- metadatas.extend(geotag.to_description())
247
+ if video_paths:
248
+ video_metadata = _process_videos(
249
+ geotag_source,
250
+ geotag_source_path,
251
+ video_paths,
252
+ num_processes,
253
+ filetypes,
254
+ )
255
+ metadatas.extend(video_metadata)
224
256
 
225
257
  # filenames should be deduplicated in utils.find_images/utils.find_videos
226
258
  assert len(metadatas) == len(
@@ -230,6 +262,38 @@ def process_geotag_properties(
230
262
  return metadatas
231
263
 
232
264
 
265
+ def _process_videos_beta(vars_args: T.Dict):
266
+ geotag_sources = vars_args["video_geotag_source"]
267
+ geotag_sources_opts: T.List[CliParserOptions] = []
268
+ for source in geotag_sources:
269
+ parsed_opts: CliParserOptions = {}
270
+ try:
271
+ parsed_opts = json.loads(source)
272
+ except ValueError:
273
+ if source not in T.get_args(VideoGeotagSource):
274
+ raise exceptions.MapillaryBadParameterError(
275
+ "Unknown beta source %s or invalid JSON", source
276
+ )
277
+ parsed_opts = {"source": source}
278
+
279
+ if "source" not in parsed_opts:
280
+ raise exceptions.MapillaryBadParameterError("Missing beta source name")
281
+
282
+ geotag_sources_opts.append(parsed_opts)
283
+
284
+ options: CliOptions = {
285
+ "paths": vars_args["import_path"],
286
+ "recursive": vars_args["skip_subfolders"] == False,
287
+ "geotag_sources_options": geotag_sources_opts,
288
+ "geotag_source_path": vars_args["geotag_source_path"],
289
+ "num_processes": vars_args["num_processes"],
290
+ "device_make": vars_args["device_make"],
291
+ "device_model": vars_args["device_model"],
292
+ }
293
+ extractor = VideoDataExtractor(options)
294
+ return extractor.process()
295
+
296
+
233
297
  def _apply_offsets(
234
298
  metadatas: T.Iterable[types.ImageMetadata],
235
299
  offset_time: float = 0.0,
@@ -284,6 +284,9 @@ def process_sequence_properties(
284
284
  for cur, nxt in geo.pairwise(sequence):
285
285
  assert cur.time <= nxt.time, "sequence must be sorted"
286
286
 
287
+ for s in sequences_by_folder:
288
+ _interpolate_subsecs_for_sorting(s)
289
+
287
290
  # cut sequences
288
291
  sequences_after_cut: T.List[PointSequence] = []
289
292
  for sequence in sequences_by_folder:
@@ -320,9 +323,6 @@ def process_sequence_properties(
320
323
  max_sequence_pixels,
321
324
  )
322
325
 
323
- for c in cut:
324
- _interpolate_subsecs_for_sorting(c)
325
-
326
326
  # assign sequence UUIDs
327
327
  for c in cut:
328
328
  for p in c:
mapillary_tools/types.py CHANGED
@@ -4,15 +4,10 @@ import enum
4
4
  import hashlib
5
5
  import json
6
6
  import os
7
- import sys
8
7
  import typing as T
9
8
  import uuid
10
9
  from pathlib import Path
11
-
12
- if sys.version_info >= (3, 8):
13
- from typing import Literal, TypedDict # pylint: disable=no-name-in-module
14
- else:
15
- from typing_extensions import Literal, TypedDict
10
+ from typing import Literal, TypedDict
16
11
 
17
12
  import jsonschema
18
13
 
@@ -2,7 +2,6 @@ import io
2
2
  import json
3
3
  import logging
4
4
  import os
5
- import sys
6
5
  import tempfile
7
6
  import time
8
7
  import typing as T
@@ -12,21 +11,15 @@ from contextlib import contextmanager
12
11
  from pathlib import Path
13
12
 
14
13
  import jsonschema
15
-
16
14
  import requests
17
15
 
18
- if sys.version_info >= (3, 8):
19
- from typing import Literal # pylint: disable=no-name-in-module
20
- else:
21
- from typing_extensions import Literal
22
-
23
16
  from . import constants, exif_write, types, upload_api_v4, utils
24
17
 
25
18
 
26
19
  LOG = logging.getLogger(__name__)
27
20
 
28
21
 
29
- class Progress(types.TypedDict, total=False):
22
+ class Progress(T.TypedDict, total=False):
30
23
  # The size of the chunk, in bytes, that has been uploaded in the last request
31
24
  chunk_size: int
32
25
 
@@ -68,7 +61,7 @@ class UploadCancelled(Exception):
68
61
  pass
69
62
 
70
63
 
71
- EventName = Literal[
64
+ EventName = T.Literal[
72
65
  "upload_start",
73
66
  "upload_fetch_offset",
74
67
  "upload_progress",
@@ -0,0 +1,22 @@
1
+ import typing as T
2
+ from pathlib import Path
3
+
4
+
5
+ known_parser_options = ["source", "pattern", "exiftool_path"]
6
+
7
+
8
+ class CliParserOptions(T.TypedDict, total=False):
9
+ source: str
10
+ pattern: T.Optional[str]
11
+ exiftool_path: T.Optional[Path]
12
+
13
+
14
+ class CliOptions(T.TypedDict, total=False):
15
+ paths: T.Sequence[Path]
16
+ recursive: bool
17
+ geotag_sources_options: T.Sequence[CliParserOptions]
18
+ geotag_source_path: Path
19
+ exiftool_path: Path
20
+ num_processes: int
21
+ device_make: T.Optional[str]
22
+ device_model: T.Optional[str]