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.
- mapillary_tools/__init__.py +1 -1
- mapillary_tools/api_v4.py +235 -14
- mapillary_tools/authenticate.py +325 -64
- mapillary_tools/{geotag/blackvue_parser.py → blackvue_parser.py} +74 -54
- mapillary_tools/camm/camm_builder.py +55 -97
- mapillary_tools/camm/camm_parser.py +425 -177
- mapillary_tools/commands/__main__.py +11 -4
- mapillary_tools/commands/authenticate.py +8 -1
- mapillary_tools/commands/process.py +27 -51
- mapillary_tools/commands/process_and_upload.py +19 -5
- mapillary_tools/commands/sample_video.py +2 -3
- mapillary_tools/commands/upload.py +18 -9
- mapillary_tools/commands/video_process_and_upload.py +19 -5
- mapillary_tools/config.py +28 -12
- mapillary_tools/constants.py +46 -4
- mapillary_tools/exceptions.py +34 -35
- mapillary_tools/exif_read.py +158 -53
- mapillary_tools/exiftool_read.py +19 -5
- mapillary_tools/exiftool_read_video.py +12 -1
- mapillary_tools/exiftool_runner.py +77 -0
- mapillary_tools/geo.py +148 -107
- mapillary_tools/geotag/factory.py +298 -0
- mapillary_tools/geotag/geotag_from_generic.py +152 -11
- mapillary_tools/geotag/geotag_images_from_exif.py +43 -124
- mapillary_tools/geotag/geotag_images_from_exiftool.py +66 -70
- mapillary_tools/geotag/geotag_images_from_exiftool_both_image_and_video.py +32 -48
- mapillary_tools/geotag/geotag_images_from_gpx.py +41 -116
- mapillary_tools/geotag/geotag_images_from_gpx_file.py +15 -96
- mapillary_tools/geotag/geotag_images_from_nmea_file.py +4 -2
- mapillary_tools/geotag/geotag_images_from_video.py +46 -46
- mapillary_tools/geotag/geotag_videos_from_exiftool_video.py +98 -92
- mapillary_tools/geotag/geotag_videos_from_gpx.py +140 -0
- mapillary_tools/geotag/geotag_videos_from_video.py +149 -181
- mapillary_tools/geotag/options.py +159 -0
- mapillary_tools/{geotag → gpmf}/gpmf_parser.py +194 -171
- mapillary_tools/history.py +3 -11
- mapillary_tools/mp4/io_utils.py +0 -1
- mapillary_tools/mp4/mp4_sample_parser.py +11 -3
- mapillary_tools/mp4/simple_mp4_parser.py +0 -10
- mapillary_tools/process_geotag_properties.py +151 -386
- mapillary_tools/process_sequence_properties.py +554 -202
- mapillary_tools/sample_video.py +8 -15
- mapillary_tools/telemetry.py +24 -12
- mapillary_tools/types.py +80 -22
- mapillary_tools/upload.py +316 -298
- mapillary_tools/upload_api_v4.py +55 -122
- mapillary_tools/uploader.py +396 -254
- mapillary_tools/utils.py +26 -0
- mapillary_tools/video_data_extraction/extract_video_data.py +17 -36
- mapillary_tools/video_data_extraction/extractors/blackvue_parser.py +34 -19
- mapillary_tools/video_data_extraction/extractors/camm_parser.py +41 -17
- mapillary_tools/video_data_extraction/extractors/exiftool_runtime_parser.py +4 -1
- mapillary_tools/video_data_extraction/extractors/exiftool_xml_parser.py +1 -2
- mapillary_tools/video_data_extraction/extractors/gopro_parser.py +37 -22
- {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0a1.dist-info}/METADATA +3 -2
- mapillary_tools-0.14.0a1.dist-info/RECORD +78 -0
- {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0a1.dist-info}/WHEEL +1 -1
- mapillary_tools/geotag/utils.py +0 -26
- mapillary_tools-0.13.3a1.dist-info/RECORD +0 -75
- /mapillary_tools/{geotag → gpmf}/gpmf_gps_filter.py +0 -0
- /mapillary_tools/{geotag → gpmf}/gps_filter.py +0 -0
- {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0a1.dist-info}/entry_points.txt +0 -0
- {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0a1.dist-info/licenses}/LICENSE +0 -0
- {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
|
|
2
|
-
|
|
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
|
|
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 ..
|
|
12
|
-
from ..
|
|
13
|
-
from . import
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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))
|