mapillary-tools 0.13.3__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 +106 -7
- 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 +2 -0
- mapillary_tools/commands/authenticate.py +8 -1
- mapillary_tools/commands/process.py +27 -51
- mapillary_tools/commands/process_and_upload.py +18 -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 +311 -261
- mapillary_tools/upload_api_v4.py +55 -95
- 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.3.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.3.dist-info → mapillary_tools-0.14.0a1.dist-info}/WHEEL +1 -1
- mapillary_tools/geotag/utils.py +0 -26
- mapillary_tools-0.13.3.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.3.dist-info → mapillary_tools-0.14.0a1.dist-info}/entry_points.txt +0 -0
- {mapillary_tools-0.13.3.dist-info → mapillary_tools-0.14.0a1.dist-info/licenses}/LICENSE +0 -0
- {mapillary_tools-0.13.3.dist-info → mapillary_tools-0.14.0a1.dist-info}/top_level.txt +0 -0
|
@@ -1,12 +1,13 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import dataclasses
|
|
2
4
|
import logging
|
|
3
5
|
import typing as T
|
|
4
|
-
from multiprocessing import Pool
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
|
|
7
8
|
from .. import exceptions, geo, types
|
|
8
9
|
from .geotag_from_generic import GeotagImagesFromGeneric
|
|
9
|
-
from .geotag_images_from_exif import
|
|
10
|
+
from .geotag_images_from_exif import ImageEXIFExtractor
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
LOG = logging.getLogger(__name__)
|
|
@@ -20,41 +21,19 @@ class GeotagImagesFromGPX(GeotagImagesFromGeneric):
|
|
|
20
21
|
use_gpx_start_time: bool = False,
|
|
21
22
|
use_image_start_time: bool = False,
|
|
22
23
|
offset_time: float = 0.0,
|
|
23
|
-
num_processes:
|
|
24
|
+
num_processes: int | None = None,
|
|
24
25
|
):
|
|
25
|
-
super().__init__()
|
|
26
|
-
self.image_paths = image_paths
|
|
26
|
+
super().__init__(image_paths, num_processes=num_processes)
|
|
27
27
|
self.points = points
|
|
28
28
|
self.use_gpx_start_time = use_gpx_start_time
|
|
29
29
|
self.use_image_start_time = use_image_start_time
|
|
30
30
|
self.offset_time = offset_time
|
|
31
|
-
self.num_processes = num_processes
|
|
32
|
-
|
|
33
|
-
@staticmethod
|
|
34
|
-
def geotag_image(image_path: Path) -> types.ImageMetadataOrError:
|
|
35
|
-
return GeotagImagesFromEXIF.geotag_image(image_path, skip_lonlat_error=True)
|
|
36
|
-
|
|
37
|
-
def geotag_multiple_images(
|
|
38
|
-
self, image_paths: T.Sequence[Path]
|
|
39
|
-
) -> T.List[types.ImageMetadataOrError]:
|
|
40
|
-
if self.num_processes is None:
|
|
41
|
-
num_processes = self.num_processes
|
|
42
|
-
disable_multiprocessing = False
|
|
43
|
-
else:
|
|
44
|
-
num_processes = max(self.num_processes, 1)
|
|
45
|
-
disable_multiprocessing = self.num_processes <= 0
|
|
46
|
-
|
|
47
|
-
if disable_multiprocessing:
|
|
48
|
-
return list(map(GeotagImagesFromGPX.geotag_image, image_paths))
|
|
49
|
-
else:
|
|
50
|
-
with Pool(processes=num_processes) as pool:
|
|
51
|
-
return pool.map(GeotagImagesFromGPX.geotag_image, image_paths)
|
|
52
31
|
|
|
53
32
|
def _interpolate_image_metadata_along(
|
|
54
33
|
self,
|
|
55
34
|
image_metadata: types.ImageMetadata,
|
|
56
35
|
sorted_points: T.Sequence[geo.Point],
|
|
57
|
-
) -> types.
|
|
36
|
+
) -> types.ImageMetadata:
|
|
58
37
|
assert sorted_points, "must have at least one point"
|
|
59
38
|
|
|
60
39
|
if image_metadata.time < sorted_points[0].time:
|
|
@@ -63,15 +42,12 @@ class GeotagImagesFromGPX(GeotagImagesFromGeneric):
|
|
|
63
42
|
gpx_end_time = types.datetime_to_map_capture_time(sorted_points[-1].time)
|
|
64
43
|
# with the tolerance of 1ms
|
|
65
44
|
if 0.001 < delta:
|
|
66
|
-
|
|
45
|
+
raise exceptions.MapillaryOutsideGPXTrackError(
|
|
67
46
|
f"The image date time is {round(delta, 3)} seconds behind the GPX start point",
|
|
68
47
|
image_time=types.datetime_to_map_capture_time(image_metadata.time),
|
|
69
48
|
gpx_start_time=gpx_start_time,
|
|
70
49
|
gpx_end_time=gpx_end_time,
|
|
71
50
|
)
|
|
72
|
-
return types.describe_error_metadata(
|
|
73
|
-
exc, image_metadata.filename, filetype=types.FileType.IMAGE
|
|
74
|
-
)
|
|
75
51
|
|
|
76
52
|
if sorted_points[-1].time < image_metadata.time:
|
|
77
53
|
delta = image_metadata.time - sorted_points[-1].time
|
|
@@ -79,15 +55,12 @@ class GeotagImagesFromGPX(GeotagImagesFromGeneric):
|
|
|
79
55
|
gpx_end_time = types.datetime_to_map_capture_time(sorted_points[-1].time)
|
|
80
56
|
# with the tolerance of 1ms
|
|
81
57
|
if 0.001 < delta:
|
|
82
|
-
|
|
58
|
+
raise exceptions.MapillaryOutsideGPXTrackError(
|
|
83
59
|
f"The image time is {round(delta, 3)} seconds beyond the GPX end point",
|
|
84
60
|
image_time=types.datetime_to_map_capture_time(image_metadata.time),
|
|
85
61
|
gpx_start_time=gpx_start_time,
|
|
86
62
|
gpx_end_time=gpx_end_time,
|
|
87
63
|
)
|
|
88
|
-
return types.describe_error_metadata(
|
|
89
|
-
exc, image_metadata.filename, filetype=types.FileType.IMAGE
|
|
90
|
-
)
|
|
91
64
|
|
|
92
65
|
interpolated = geo.interpolate(sorted_points, image_metadata.time)
|
|
93
66
|
|
|
@@ -100,34 +73,25 @@ class GeotagImagesFromGPX(GeotagImagesFromGeneric):
|
|
|
100
73
|
time=interpolated.time,
|
|
101
74
|
)
|
|
102
75
|
|
|
103
|
-
def
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
metadatas.append(
|
|
112
|
-
types.describe_error_metadata(
|
|
113
|
-
exc, image_path, filetype=types.FileType.IMAGE
|
|
114
|
-
),
|
|
115
|
-
)
|
|
116
|
-
assert len(self.image_paths) == len(metadatas)
|
|
117
|
-
return metadatas
|
|
76
|
+
def _generate_image_extractors(self) -> T.Sequence[ImageEXIFExtractor]:
|
|
77
|
+
return [
|
|
78
|
+
ImageEXIFExtractor(path, skip_lonlat_error=True)
|
|
79
|
+
for path in self.image_paths
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
def to_description(self) -> list[types.ImageMetadataOrError]:
|
|
83
|
+
final_metadatas: list[types.ImageMetadataOrError] = []
|
|
118
84
|
|
|
119
|
-
image_metadata_or_errors =
|
|
85
|
+
image_metadata_or_errors = super().to_description()
|
|
120
86
|
|
|
121
|
-
image_metadatas =
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
else:
|
|
126
|
-
image_metadatas.append(metadata_or_error)
|
|
87
|
+
image_metadatas, error_metadatas = types.separate_errors(
|
|
88
|
+
image_metadata_or_errors
|
|
89
|
+
)
|
|
90
|
+
final_metadatas.extend(error_metadatas)
|
|
127
91
|
|
|
128
92
|
if not image_metadatas:
|
|
129
|
-
assert len(self.image_paths) == len(
|
|
130
|
-
return
|
|
93
|
+
assert len(self.image_paths) == len(final_metadatas)
|
|
94
|
+
return final_metadatas
|
|
131
95
|
|
|
132
96
|
# Do not use point itself for comparison because point.angle or point.alt could be None
|
|
133
97
|
# when you compare nonnull value with None, it will throw
|
|
@@ -162,64 +126,25 @@ class GeotagImagesFromGPX(GeotagImagesFromGeneric):
|
|
|
162
126
|
LOG.debug("GPX start time delta: %s", time_delta)
|
|
163
127
|
image_time_offset += time_delta
|
|
164
128
|
|
|
165
|
-
|
|
129
|
+
if image_time_offset:
|
|
130
|
+
LOG.debug("Final time offset for interpolation: %s", image_time_offset)
|
|
131
|
+
for image_metadata in sorted_image_metadatas:
|
|
132
|
+
# TODO: this time modification seems to affect final capture times
|
|
133
|
+
image_metadata.time += image_time_offset
|
|
166
134
|
|
|
167
135
|
for image_metadata in sorted_image_metadatas:
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
assert len(self.image_paths) == len(metadatas)
|
|
174
|
-
return metadatas
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
class GeotagImagesFromGPXWithProgress(GeotagImagesFromGPX):
|
|
178
|
-
def __init__(
|
|
179
|
-
self,
|
|
180
|
-
image_paths: T.Sequence[Path],
|
|
181
|
-
points: T.Sequence[geo.Point],
|
|
182
|
-
use_gpx_start_time: bool = False,
|
|
183
|
-
use_image_start_time: bool = False,
|
|
184
|
-
offset_time: float = 0.0,
|
|
185
|
-
num_processes: T.Optional[int] = None,
|
|
186
|
-
progress_bar=None,
|
|
187
|
-
) -> None:
|
|
188
|
-
super().__init__(
|
|
189
|
-
image_paths,
|
|
190
|
-
points,
|
|
191
|
-
use_gpx_start_time=use_gpx_start_time,
|
|
192
|
-
use_image_start_time=use_image_start_time,
|
|
193
|
-
offset_time=offset_time,
|
|
194
|
-
num_processes=num_processes,
|
|
195
|
-
)
|
|
196
|
-
self._progress_bar = progress_bar
|
|
197
|
-
|
|
198
|
-
def geotag_multiple_images(
|
|
199
|
-
self, image_paths: T.Sequence[Path]
|
|
200
|
-
) -> T.List[types.ImageMetadataOrError]:
|
|
201
|
-
if self._progress_bar is None:
|
|
202
|
-
return super().geotag_multiple_images(image_paths)
|
|
203
|
-
|
|
204
|
-
if self.num_processes is None:
|
|
205
|
-
num_processes = self.num_processes
|
|
206
|
-
disable_multiprocessing = False
|
|
207
|
-
else:
|
|
208
|
-
num_processes = max(self.num_processes, 1)
|
|
209
|
-
disable_multiprocessing = self.num_processes <= 0
|
|
210
|
-
|
|
211
|
-
output = []
|
|
212
|
-
with Pool(processes=num_processes) as pool:
|
|
213
|
-
image_metadatas_iter: T.Iterator[types.ImageMetadataOrError]
|
|
214
|
-
if disable_multiprocessing:
|
|
215
|
-
image_metadatas_iter = map(
|
|
216
|
-
GeotagImagesFromGPX.geotag_image, image_paths
|
|
136
|
+
try:
|
|
137
|
+
final_metadatas.append(
|
|
138
|
+
self._interpolate_image_metadata_along(
|
|
139
|
+
image_metadata, sorted_points
|
|
140
|
+
)
|
|
217
141
|
)
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
142
|
+
except exceptions.MapillaryOutsideGPXTrackError as ex:
|
|
143
|
+
error_metadata = types.describe_error_metadata(
|
|
144
|
+
ex, image_metadata.filename, filetype=types.FileType.IMAGE
|
|
221
145
|
)
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
146
|
+
final_metadatas.append(error_metadata)
|
|
147
|
+
|
|
148
|
+
assert len(self.image_paths) == len(final_metadatas)
|
|
149
|
+
|
|
150
|
+
return final_metadatas
|
|
@@ -1,30 +1,27 @@
|
|
|
1
|
-
import
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
import logging
|
|
3
4
|
import typing as T
|
|
4
|
-
from multiprocessing import Pool
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
|
|
7
7
|
import gpxpy
|
|
8
|
-
from tqdm import tqdm
|
|
9
8
|
|
|
10
|
-
from .. import
|
|
11
|
-
from .
|
|
12
|
-
from .geotag_images_from_gpx import GeotagImagesFromGPXWithProgress
|
|
9
|
+
from .. import geo
|
|
10
|
+
from .geotag_images_from_gpx import GeotagImagesFromGPX
|
|
13
11
|
|
|
14
12
|
|
|
15
13
|
LOG = logging.getLogger(__name__)
|
|
16
14
|
|
|
17
15
|
|
|
18
|
-
class GeotagImagesFromGPXFile(
|
|
16
|
+
class GeotagImagesFromGPXFile(GeotagImagesFromGPX):
|
|
19
17
|
def __init__(
|
|
20
18
|
self,
|
|
21
19
|
image_paths: T.Sequence[Path],
|
|
22
20
|
source_path: Path,
|
|
23
21
|
use_gpx_start_time: bool = False,
|
|
24
22
|
offset_time: float = 0.0,
|
|
25
|
-
num_processes:
|
|
23
|
+
num_processes: int | None = None,
|
|
26
24
|
):
|
|
27
|
-
super().__init__()
|
|
28
25
|
try:
|
|
29
26
|
tracks = parse_gpx(source_path)
|
|
30
27
|
except Exception as ex:
|
|
@@ -38,102 +35,24 @@ class GeotagImagesFromGPXFile(GeotagImagesFromGeneric):
|
|
|
38
35
|
len(tracks),
|
|
39
36
|
source_path,
|
|
40
37
|
)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
@staticmethod
|
|
49
|
-
def _extract_image_metadata(
|
|
50
|
-
image_metadata: types.ImageMetadata,
|
|
51
|
-
) -> types.ImageMetadataOrError:
|
|
52
|
-
try:
|
|
53
|
-
exif = exif_read.ExifRead(image_metadata.filename)
|
|
54
|
-
orientation = exif.extract_orientation()
|
|
55
|
-
make = exif.extract_make()
|
|
56
|
-
model = exif.extract_model()
|
|
57
|
-
except Exception as ex:
|
|
58
|
-
return types.describe_error_metadata(
|
|
59
|
-
ex, image_metadata.filename, filetype=types.FileType.IMAGE
|
|
60
|
-
)
|
|
61
|
-
|
|
62
|
-
return dataclasses.replace(
|
|
63
|
-
image_metadata,
|
|
64
|
-
MAPOrientation=orientation,
|
|
65
|
-
MAPDeviceMake=make,
|
|
66
|
-
MAPDeviceModel=model,
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
def to_description(self) -> T.List[types.ImageMetadataOrError]:
|
|
70
|
-
with tqdm(
|
|
71
|
-
total=len(self.image_paths),
|
|
72
|
-
desc="Interpolating",
|
|
73
|
-
unit="images",
|
|
74
|
-
disable=LOG.getEffectiveLevel() <= logging.DEBUG,
|
|
75
|
-
) as pbar:
|
|
76
|
-
geotag = GeotagImagesFromGPXWithProgress(
|
|
77
|
-
self.image_paths,
|
|
78
|
-
self.points,
|
|
79
|
-
use_gpx_start_time=self.use_gpx_start_time,
|
|
80
|
-
offset_time=self.offset_time,
|
|
81
|
-
progress_bar=pbar,
|
|
82
|
-
)
|
|
83
|
-
image_metadata_or_errors = geotag.to_description()
|
|
84
|
-
|
|
85
|
-
image_metadatas: T.List[types.ImageMetadata] = []
|
|
86
|
-
error_metadatas: T.List[types.ErrorMetadata] = []
|
|
87
|
-
for metadata in image_metadata_or_errors:
|
|
88
|
-
if isinstance(metadata, types.ErrorMetadata):
|
|
89
|
-
error_metadatas.append(metadata)
|
|
90
|
-
else:
|
|
91
|
-
image_metadatas.append(metadata)
|
|
92
|
-
|
|
93
|
-
if self.num_processes is None:
|
|
94
|
-
num_processes = self.num_processes
|
|
95
|
-
disable_multiprocessing = False
|
|
96
|
-
else:
|
|
97
|
-
num_processes = max(self.num_processes, 1)
|
|
98
|
-
disable_multiprocessing = self.num_processes <= 0
|
|
99
|
-
|
|
100
|
-
with Pool(processes=num_processes) as pool:
|
|
101
|
-
image_metadatas_iter: T.Iterator[types.ImageMetadataOrError]
|
|
102
|
-
if disable_multiprocessing:
|
|
103
|
-
image_metadatas_iter = map(
|
|
104
|
-
GeotagImagesFromGPXFile._extract_image_metadata, image_metadatas
|
|
105
|
-
)
|
|
106
|
-
else:
|
|
107
|
-
# Do not pass error metadatas where the error object can not be pickled for multiprocessing to work
|
|
108
|
-
# Otherwise we get:
|
|
109
|
-
# TypeError: __init__() missing 3 required positional arguments: 'image_time', 'gpx_start_time', and 'gpx_end_time'
|
|
110
|
-
# See https://stackoverflow.com/a/61432070
|
|
111
|
-
image_metadatas_iter = pool.imap(
|
|
112
|
-
GeotagImagesFromGPXFile._extract_image_metadata, image_metadatas
|
|
113
|
-
)
|
|
114
|
-
image_metadata_or_errors = list(
|
|
115
|
-
tqdm(
|
|
116
|
-
image_metadatas_iter,
|
|
117
|
-
desc="Processing",
|
|
118
|
-
unit="images",
|
|
119
|
-
disable=LOG.getEffectiveLevel() <= logging.DEBUG,
|
|
120
|
-
)
|
|
121
|
-
)
|
|
122
|
-
|
|
123
|
-
return (
|
|
124
|
-
T.cast(T.List[types.ImageMetadataOrError], error_metadatas)
|
|
125
|
-
+ image_metadata_or_errors
|
|
38
|
+
points = sum(tracks, [])
|
|
39
|
+
super().__init__(
|
|
40
|
+
image_paths,
|
|
41
|
+
points,
|
|
42
|
+
use_gpx_start_time=use_gpx_start_time,
|
|
43
|
+
offset_time=offset_time,
|
|
44
|
+
num_processes=num_processes,
|
|
126
45
|
)
|
|
127
46
|
|
|
128
47
|
|
|
129
48
|
Track = T.List[geo.Point]
|
|
130
49
|
|
|
131
50
|
|
|
132
|
-
def parse_gpx(gpx_file: Path) ->
|
|
51
|
+
def parse_gpx(gpx_file: Path) -> list[Track]:
|
|
133
52
|
with gpx_file.open("r") as f:
|
|
134
53
|
gpx = gpxpy.parse(f)
|
|
135
54
|
|
|
136
|
-
tracks:
|
|
55
|
+
tracks: list[Track] = []
|
|
137
56
|
|
|
138
57
|
for track in gpx.tracks:
|
|
139
58
|
for segment in track.segments:
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import datetime
|
|
2
4
|
import typing as T
|
|
3
5
|
from pathlib import Path
|
|
@@ -15,7 +17,7 @@ class GeotagImagesFromNMEAFile(GeotagImagesFromGPX):
|
|
|
15
17
|
source_path: Path,
|
|
16
18
|
use_gpx_start_time: bool = False,
|
|
17
19
|
offset_time: float = 0.0,
|
|
18
|
-
num_processes:
|
|
20
|
+
num_processes: int | None = None,
|
|
19
21
|
):
|
|
20
22
|
points = get_lat_lon_time_from_nmea(source_path)
|
|
21
23
|
super().__init__(
|
|
@@ -27,7 +29,7 @@ class GeotagImagesFromNMEAFile(GeotagImagesFromGPX):
|
|
|
27
29
|
)
|
|
28
30
|
|
|
29
31
|
|
|
30
|
-
def get_lat_lon_time_from_nmea(nmea_file: Path) ->
|
|
32
|
+
def get_lat_lon_time_from_nmea(nmea_file: Path) -> list[geo.Point]:
|
|
31
33
|
with nmea_file.open("r") as f:
|
|
32
34
|
lines = f.readlines()
|
|
33
35
|
lines = [line.rstrip("\n\r") for line in lines]
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import logging
|
|
2
4
|
import typing as T
|
|
3
5
|
from pathlib import Path
|
|
4
6
|
|
|
5
|
-
from tqdm import tqdm
|
|
6
|
-
|
|
7
7
|
from .. import types, utils
|
|
8
8
|
|
|
9
9
|
from .geotag_from_generic import GeotagImagesFromGeneric
|
|
10
|
-
from .geotag_images_from_gpx import
|
|
10
|
+
from .geotag_images_from_gpx import GeotagImagesFromGPX
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
LOG = logging.getLogger(__name__)
|
|
@@ -19,69 +19,69 @@ class GeotagImagesFromVideo(GeotagImagesFromGeneric):
|
|
|
19
19
|
image_paths: T.Sequence[Path],
|
|
20
20
|
video_metadatas: T.Sequence[types.VideoMetadataOrError],
|
|
21
21
|
offset_time: float = 0.0,
|
|
22
|
-
num_processes:
|
|
22
|
+
num_processes: int | None = None,
|
|
23
23
|
):
|
|
24
|
-
|
|
24
|
+
super().__init__(image_paths, num_processes=num_processes)
|
|
25
25
|
self.video_metadatas = video_metadatas
|
|
26
26
|
self.offset_time = offset_time
|
|
27
|
-
self.num_processes = num_processes
|
|
28
|
-
super().__init__()
|
|
29
27
|
|
|
30
|
-
def to_description(self) ->
|
|
31
|
-
#
|
|
32
|
-
final_image_metadatas:
|
|
28
|
+
def to_description(self) -> list[types.ImageMetadataOrError]:
|
|
29
|
+
# Will return this list
|
|
30
|
+
final_image_metadatas: list[types.ImageMetadataOrError] = []
|
|
33
31
|
|
|
34
|
-
|
|
32
|
+
video_metadatas, video_error_metadatas = types.separate_errors(
|
|
33
|
+
self.video_metadatas
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
for video_error_metadata in video_error_metadatas:
|
|
37
|
+
video_path = video_error_metadata.filename
|
|
38
|
+
sample_paths = list(
|
|
39
|
+
utils.filter_video_samples(self.image_paths, video_path)
|
|
40
|
+
)
|
|
41
|
+
LOG.debug(
|
|
42
|
+
"Found %d sample images from video %s with error: %s",
|
|
43
|
+
len(sample_paths),
|
|
44
|
+
video_path,
|
|
45
|
+
video_error_metadata.error,
|
|
46
|
+
)
|
|
47
|
+
for sample_path in sample_paths:
|
|
48
|
+
image_error_metadata = types.describe_error_metadata(
|
|
49
|
+
video_error_metadata.error,
|
|
50
|
+
sample_path,
|
|
51
|
+
filetype=types.FileType.IMAGE,
|
|
52
|
+
)
|
|
53
|
+
final_image_metadatas.append(image_error_metadata)
|
|
54
|
+
|
|
55
|
+
for video_metadata in video_metadatas:
|
|
35
56
|
video_path = video_metadata.filename
|
|
36
|
-
LOG.debug("Processing video: %s", video_path)
|
|
37
57
|
|
|
38
|
-
|
|
58
|
+
sample_paths = list(
|
|
39
59
|
utils.filter_video_samples(self.image_paths, video_path)
|
|
40
60
|
)
|
|
41
61
|
LOG.debug(
|
|
42
62
|
"Found %d sample images from video %s",
|
|
43
|
-
len(
|
|
63
|
+
len(sample_paths),
|
|
44
64
|
video_path,
|
|
45
65
|
)
|
|
46
66
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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,
|
|
67
|
+
geotag = GeotagImagesFromGPX(
|
|
68
|
+
sample_paths,
|
|
69
|
+
video_metadata.points,
|
|
70
|
+
use_gpx_start_time=False,
|
|
71
|
+
use_image_start_time=True,
|
|
72
|
+
offset_time=self.offset_time,
|
|
73
|
+
num_processes=self.num_processes,
|
|
79
74
|
)
|
|
75
|
+
|
|
76
|
+
image_metadatas = geotag.to_description()
|
|
77
|
+
|
|
80
78
|
for metadata in image_metadatas:
|
|
81
79
|
if isinstance(metadata, types.ImageMetadata):
|
|
82
80
|
metadata.MAPDeviceMake = video_metadata.make
|
|
83
81
|
metadata.MAPDeviceModel = video_metadata.model
|
|
84
82
|
|
|
83
|
+
final_image_metadatas.extend(image_metadatas)
|
|
84
|
+
|
|
85
85
|
# NOTE: this method only geotags images that have a corresponding video,
|
|
86
86
|
# so the number of image metadata objects returned might be less than
|
|
87
87
|
# the number of the input image_paths
|