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.
- mapillary_tools/__init__.py +1 -1
- mapillary_tools/api_v4.py +2 -9
- mapillary_tools/commands/__main__.py +5 -3
- mapillary_tools/commands/process.py +9 -1
- mapillary_tools/constants.py +1 -0
- mapillary_tools/exceptions.py +4 -0
- mapillary_tools/exif_read.py +46 -10
- mapillary_tools/exiftool_read.py +4 -0
- mapillary_tools/ffmpeg.py +3 -8
- mapillary_tools/geotag/construct_mp4_parser.py +2 -8
- mapillary_tools/geotag/gpmf_parser.py +1 -7
- mapillary_tools/process_geotag_properties.py +115 -51
- mapillary_tools/process_sequence_properties.py +3 -3
- mapillary_tools/types.py +1 -6
- mapillary_tools/uploader.py +2 -9
- mapillary_tools/video_data_extraction/cli_options.py +22 -0
- mapillary_tools/video_data_extraction/extract_video_data.py +190 -0
- mapillary_tools/video_data_extraction/extractors/base_parser.py +73 -0
- mapillary_tools/video_data_extraction/extractors/blackvue_parser.py +33 -0
- mapillary_tools/video_data_extraction/extractors/camm_parser.py +41 -0
- mapillary_tools/video_data_extraction/extractors/exiftool_runtime_parser.py +56 -0
- mapillary_tools/video_data_extraction/extractors/exiftool_xml_parser.py +55 -0
- mapillary_tools/video_data_extraction/extractors/generic_video_parser.py +57 -0
- mapillary_tools/video_data_extraction/extractors/gopro_parser.py +36 -0
- mapillary_tools/video_data_extraction/extractors/gpx_parser.py +29 -0
- mapillary_tools/video_data_extraction/extractors/nmea_parser.py +24 -0
- mapillary_tools/video_data_extraction/video_data_parser_factory.py +46 -0
- {mapillary_tools-0.10.6a1.dist-info → mapillary_tools-0.11.0b2.dist-info}/METADATA +98 -11
- {mapillary_tools-0.10.6a1.dist-info → mapillary_tools-0.11.0b2.dist-info}/RECORD +33 -21
- {mapillary_tools-0.10.6a1.dist-info → mapillary_tools-0.11.0b2.dist-info}/WHEEL +1 -1
- {mapillary_tools-0.10.6a1.dist-info → mapillary_tools-0.11.0b2.dist-info}/LICENSE +0 -0
- {mapillary_tools-0.10.6a1.dist-info → mapillary_tools-0.11.0b2.dist-info}/entry_points.txt +0 -0
- {mapillary_tools-0.10.6a1.dist-info → mapillary_tools-0.11.0b2.dist-info}/top_level.txt +0 -0
mapillary_tools/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
VERSION = "0.
|
|
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="
|
|
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="
|
|
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(
|
|
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(
|
mapillary_tools/constants.py
CHANGED
|
@@ -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
|
)
|
mapillary_tools/exceptions.py
CHANGED
|
@@ -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
|
|
mapillary_tools/exif_read.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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]:
|
mapillary_tools/exiftool_read.py
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
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
|
|
mapillary_tools/uploader.py
CHANGED
|
@@ -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(
|
|
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]
|