mapillary-tools 0.13.3__py3-none-any.whl → 0.14.0__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 +198 -55
- mapillary_tools/authenticate.py +326 -64
- mapillary_tools/blackvue_parser.py +195 -0
- mapillary_tools/camm/camm_builder.py +55 -97
- mapillary_tools/camm/camm_parser.py +429 -181
- mapillary_tools/commands/__main__.py +10 -6
- 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 +44 -13
- mapillary_tools/commands/video_process_and_upload.py +19 -5
- mapillary_tools/config.py +65 -26
- mapillary_tools/constants.py +141 -18
- mapillary_tools/exceptions.py +37 -34
- mapillary_tools/exif_read.py +221 -116
- mapillary_tools/exif_write.py +10 -8
- mapillary_tools/exiftool_read.py +33 -42
- mapillary_tools/exiftool_read_video.py +97 -47
- mapillary_tools/exiftool_runner.py +57 -0
- mapillary_tools/ffmpeg.py +417 -242
- mapillary_tools/geo.py +158 -118
- mapillary_tools/geotag/__init__.py +0 -1
- mapillary_tools/geotag/base.py +147 -0
- mapillary_tools/geotag/factory.py +307 -0
- mapillary_tools/geotag/geotag_images_from_exif.py +14 -131
- mapillary_tools/geotag/geotag_images_from_exiftool.py +136 -85
- mapillary_tools/geotag/geotag_images_from_gpx.py +60 -124
- mapillary_tools/geotag/geotag_images_from_gpx_file.py +13 -126
- mapillary_tools/geotag/geotag_images_from_nmea_file.py +4 -5
- mapillary_tools/geotag/geotag_images_from_video.py +88 -51
- mapillary_tools/geotag/geotag_videos_from_exiftool.py +123 -0
- mapillary_tools/geotag/geotag_videos_from_gpx.py +52 -0
- mapillary_tools/geotag/geotag_videos_from_video.py +20 -185
- mapillary_tools/geotag/image_extractors/base.py +18 -0
- mapillary_tools/geotag/image_extractors/exif.py +60 -0
- mapillary_tools/geotag/image_extractors/exiftool.py +18 -0
- mapillary_tools/geotag/options.py +182 -0
- mapillary_tools/geotag/utils.py +52 -16
- mapillary_tools/geotag/video_extractors/base.py +18 -0
- mapillary_tools/geotag/video_extractors/exiftool.py +70 -0
- mapillary_tools/geotag/video_extractors/gpx.py +116 -0
- mapillary_tools/geotag/video_extractors/native.py +160 -0
- mapillary_tools/{geotag → gpmf}/gpmf_parser.py +205 -182
- mapillary_tools/{geotag → gpmf}/gps_filter.py +5 -3
- mapillary_tools/history.py +134 -20
- mapillary_tools/mp4/construct_mp4_parser.py +17 -10
- mapillary_tools/mp4/io_utils.py +0 -1
- mapillary_tools/mp4/mp4_sample_parser.py +36 -28
- mapillary_tools/mp4/simple_mp4_builder.py +10 -9
- mapillary_tools/mp4/simple_mp4_parser.py +13 -22
- mapillary_tools/process_geotag_properties.py +184 -414
- mapillary_tools/process_sequence_properties.py +594 -225
- mapillary_tools/sample_video.py +20 -26
- mapillary_tools/serializer/description.py +587 -0
- mapillary_tools/serializer/gpx.py +132 -0
- mapillary_tools/telemetry.py +26 -13
- mapillary_tools/types.py +98 -611
- mapillary_tools/upload.py +411 -387
- mapillary_tools/upload_api_v4.py +167 -142
- mapillary_tools/uploader.py +804 -284
- mapillary_tools/utils.py +49 -18
- {mapillary_tools-0.13.3.dist-info → mapillary_tools-0.14.0.dist-info}/METADATA +93 -35
- mapillary_tools-0.14.0.dist-info/RECORD +75 -0
- {mapillary_tools-0.13.3.dist-info → mapillary_tools-0.14.0.dist-info}/WHEEL +1 -1
- mapillary_tools/geotag/blackvue_parser.py +0 -118
- mapillary_tools/geotag/geotag_from_generic.py +0 -22
- mapillary_tools/geotag/geotag_images_from_exiftool_both_image_and_video.py +0 -93
- mapillary_tools/geotag/geotag_videos_from_exiftool_video.py +0 -145
- mapillary_tools/video_data_extraction/cli_options.py +0 -22
- mapillary_tools/video_data_extraction/extract_video_data.py +0 -176
- mapillary_tools/video_data_extraction/extractors/base_parser.py +0 -75
- mapillary_tools/video_data_extraction/extractors/blackvue_parser.py +0 -34
- mapillary_tools/video_data_extraction/extractors/camm_parser.py +0 -38
- mapillary_tools/video_data_extraction/extractors/exiftool_runtime_parser.py +0 -71
- mapillary_tools/video_data_extraction/extractors/exiftool_xml_parser.py +0 -53
- mapillary_tools/video_data_extraction/extractors/generic_video_parser.py +0 -52
- mapillary_tools/video_data_extraction/extractors/gopro_parser.py +0 -43
- mapillary_tools/video_data_extraction/extractors/gpx_parser.py +0 -108
- mapillary_tools/video_data_extraction/extractors/nmea_parser.py +0 -24
- mapillary_tools/video_data_extraction/video_data_parser_factory.py +0 -39
- mapillary_tools-0.13.3.dist-info/RECORD +0 -75
- /mapillary_tools/{geotag → gpmf}/gpmf_gps_filter.py +0 -0
- {mapillary_tools-0.13.3.dist-info → mapillary_tools-0.14.0.dist-info}/entry_points.txt +0 -0
- {mapillary_tools-0.13.3.dist-info → mapillary_tools-0.14.0.dist-info/licenses}/LICENSE +0 -0
- {mapillary_tools-0.13.3.dist-info → mapillary_tools-0.14.0.dist-info}/top_level.txt +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import datetime
|
|
2
|
-
import typing as T
|
|
3
4
|
from pathlib import Path
|
|
4
5
|
|
|
5
6
|
import pynmea2
|
|
@@ -11,15 +12,13 @@ from .geotag_images_from_gpx import GeotagImagesFromGPX
|
|
|
11
12
|
class GeotagImagesFromNMEAFile(GeotagImagesFromGPX):
|
|
12
13
|
def __init__(
|
|
13
14
|
self,
|
|
14
|
-
image_paths: T.Sequence[Path],
|
|
15
15
|
source_path: Path,
|
|
16
16
|
use_gpx_start_time: bool = False,
|
|
17
17
|
offset_time: float = 0.0,
|
|
18
|
-
num_processes:
|
|
18
|
+
num_processes: int | None = None,
|
|
19
19
|
):
|
|
20
20
|
points = get_lat_lon_time_from_nmea(source_path)
|
|
21
21
|
super().__init__(
|
|
22
|
-
image_paths,
|
|
23
22
|
points,
|
|
24
23
|
use_gpx_start_time=use_gpx_start_time,
|
|
25
24
|
offset_time=offset_time,
|
|
@@ -27,7 +26,7 @@ class GeotagImagesFromNMEAFile(GeotagImagesFromGPX):
|
|
|
27
26
|
)
|
|
28
27
|
|
|
29
28
|
|
|
30
|
-
def get_lat_lon_time_from_nmea(nmea_file: Path) ->
|
|
29
|
+
def get_lat_lon_time_from_nmea(nmea_file: Path) -> list[geo.Point]:
|
|
31
30
|
with nmea_file.open("r") as f:
|
|
32
31
|
lines = f.readlines()
|
|
33
32
|
lines = [line.rstrip("\n\r") for line in lines]
|
|
@@ -1,13 +1,19 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import logging
|
|
4
|
+
import sys
|
|
2
5
|
import typing as T
|
|
3
6
|
from pathlib import Path
|
|
4
7
|
|
|
5
|
-
|
|
8
|
+
if sys.version_info >= (3, 12):
|
|
9
|
+
from typing import override
|
|
10
|
+
else:
|
|
11
|
+
from typing_extensions import override
|
|
6
12
|
|
|
7
13
|
from .. import types, utils
|
|
8
|
-
|
|
9
|
-
from .
|
|
10
|
-
from .
|
|
14
|
+
from .base import GeotagImagesFromGeneric
|
|
15
|
+
from .geotag_images_from_gpx import GeotagImagesFromGPX
|
|
16
|
+
from .geotag_videos_from_video import GeotagVideosFromVideo
|
|
11
17
|
|
|
12
18
|
|
|
13
19
|
LOG = logging.getLogger(__name__)
|
|
@@ -16,75 +22,106 @@ LOG = logging.getLogger(__name__)
|
|
|
16
22
|
class GeotagImagesFromVideo(GeotagImagesFromGeneric):
|
|
17
23
|
def __init__(
|
|
18
24
|
self,
|
|
19
|
-
image_paths: T.Sequence[Path],
|
|
20
25
|
video_metadatas: T.Sequence[types.VideoMetadataOrError],
|
|
21
26
|
offset_time: float = 0.0,
|
|
22
|
-
num_processes:
|
|
27
|
+
num_processes: int | None = None,
|
|
23
28
|
):
|
|
24
|
-
|
|
29
|
+
super().__init__(num_processes=num_processes)
|
|
25
30
|
self.video_metadatas = video_metadatas
|
|
26
31
|
self.offset_time = offset_time
|
|
27
|
-
self.num_processes = num_processes
|
|
28
|
-
super().__init__()
|
|
29
32
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
+
@override
|
|
34
|
+
def to_description(
|
|
35
|
+
self, image_paths: T.Sequence[Path]
|
|
36
|
+
) -> list[types.ImageMetadataOrError]:
|
|
37
|
+
# Will return this list
|
|
38
|
+
final_image_metadatas: list[types.ImageMetadataOrError] = []
|
|
33
39
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
40
|
+
video_metadatas, video_error_metadatas = types.separate_errors(
|
|
41
|
+
self.video_metadatas
|
|
42
|
+
)
|
|
37
43
|
|
|
38
|
-
|
|
39
|
-
|
|
44
|
+
for video_error_metadata in video_error_metadatas:
|
|
45
|
+
video_path = video_error_metadata.filename
|
|
46
|
+
sample_paths = list(utils.filter_video_samples(image_paths, video_path))
|
|
47
|
+
LOG.debug(
|
|
48
|
+
"Found %d sample images from video %s with error: %s",
|
|
49
|
+
len(sample_paths),
|
|
50
|
+
video_path,
|
|
51
|
+
video_error_metadata.error,
|
|
40
52
|
)
|
|
53
|
+
for sample_path in sample_paths:
|
|
54
|
+
image_error_metadata = types.describe_error_metadata(
|
|
55
|
+
video_error_metadata.error,
|
|
56
|
+
sample_path,
|
|
57
|
+
filetype=types.FileType.IMAGE,
|
|
58
|
+
)
|
|
59
|
+
final_image_metadatas.append(image_error_metadata)
|
|
60
|
+
|
|
61
|
+
for video_metadata in video_metadatas:
|
|
62
|
+
video_path = video_metadata.filename
|
|
63
|
+
|
|
64
|
+
sample_paths = list(utils.filter_video_samples(image_paths, video_path))
|
|
41
65
|
LOG.debug(
|
|
42
66
|
"Found %d sample images from video %s",
|
|
43
|
-
len(
|
|
67
|
+
len(sample_paths),
|
|
44
68
|
video_path,
|
|
45
69
|
)
|
|
46
70
|
|
|
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,
|
|
71
|
+
geotag = GeotagImagesFromGPX(
|
|
72
|
+
video_metadata.points,
|
|
73
|
+
use_gpx_start_time=False,
|
|
74
|
+
use_image_start_time=True,
|
|
75
|
+
offset_time=self.offset_time,
|
|
76
|
+
num_processes=self.num_processes,
|
|
79
77
|
)
|
|
78
|
+
|
|
79
|
+
image_metadatas = geotag.to_description(image_paths)
|
|
80
|
+
|
|
80
81
|
for metadata in image_metadatas:
|
|
81
82
|
if isinstance(metadata, types.ImageMetadata):
|
|
82
83
|
metadata.MAPDeviceMake = video_metadata.make
|
|
83
84
|
metadata.MAPDeviceModel = video_metadata.model
|
|
84
85
|
|
|
86
|
+
final_image_metadatas.extend(image_metadatas)
|
|
87
|
+
|
|
85
88
|
# NOTE: this method only geotags images that have a corresponding video,
|
|
86
89
|
# so the number of image metadata objects returned might be less than
|
|
87
90
|
# the number of the input image_paths
|
|
88
|
-
assert len(final_image_metadatas) <= len(
|
|
91
|
+
assert len(final_image_metadatas) <= len(image_paths)
|
|
89
92
|
|
|
90
93
|
return final_image_metadatas
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class GeotagImageSamplesFromVideo(GeotagImagesFromGeneric):
|
|
97
|
+
def __init__(
|
|
98
|
+
self,
|
|
99
|
+
source_path: Path,
|
|
100
|
+
filetypes: set[types.FileType] | None = None,
|
|
101
|
+
offset_time: float = 0.0,
|
|
102
|
+
num_processes: int | None = None,
|
|
103
|
+
):
|
|
104
|
+
super().__init__(num_processes=num_processes)
|
|
105
|
+
self.source_path = source_path
|
|
106
|
+
self.filetypes = filetypes
|
|
107
|
+
self.offset_time = offset_time
|
|
108
|
+
|
|
109
|
+
@override
|
|
110
|
+
def to_description(
|
|
111
|
+
self, image_paths: T.Sequence[Path]
|
|
112
|
+
) -> list[types.ImageMetadataOrError]:
|
|
113
|
+
video_paths = utils.find_videos([self.source_path])
|
|
114
|
+
image_samples_by_video_path = utils.find_all_image_samples(
|
|
115
|
+
image_paths, video_paths
|
|
116
|
+
)
|
|
117
|
+
video_paths_with_image_samples = list(image_samples_by_video_path.keys())
|
|
118
|
+
video_metadatas = GeotagVideosFromVideo(
|
|
119
|
+
filetypes=self.filetypes,
|
|
120
|
+
num_processes=self.num_processes,
|
|
121
|
+
).to_description(video_paths_with_image_samples)
|
|
122
|
+
geotag = GeotagImagesFromVideo(
|
|
123
|
+
video_metadatas,
|
|
124
|
+
offset_time=self.offset_time,
|
|
125
|
+
num_processes=self.num_processes,
|
|
126
|
+
)
|
|
127
|
+
return geotag.to_description(image_paths)
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import sys
|
|
5
|
+
import typing as T
|
|
6
|
+
import xml.etree.ElementTree as ET
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
if sys.version_info >= (3, 12):
|
|
10
|
+
from typing import override
|
|
11
|
+
else:
|
|
12
|
+
from typing_extensions import override
|
|
13
|
+
|
|
14
|
+
from .. import constants, exceptions, exiftool_read, types
|
|
15
|
+
from ..exiftool_runner import ExiftoolRunner
|
|
16
|
+
from . import options
|
|
17
|
+
from .base import GeotagVideosFromGeneric
|
|
18
|
+
from .utils import index_rdf_description_by_path
|
|
19
|
+
from .video_extractors.exiftool import VideoExifToolExtractor
|
|
20
|
+
|
|
21
|
+
LOG = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class GeotagVideosFromExifToolXML(GeotagVideosFromGeneric):
|
|
25
|
+
def __init__(
|
|
26
|
+
self, source_path: options.SourcePathOption, num_processes: int | None = None
|
|
27
|
+
):
|
|
28
|
+
super().__init__(num_processes=num_processes)
|
|
29
|
+
self.source_path = source_path
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def build_video_extractors_from_etree(
|
|
33
|
+
cls, rdf_by_path: dict[str, ET.Element], video_paths: T.Iterable[Path]
|
|
34
|
+
) -> list[VideoExifToolExtractor | types.ErrorMetadata]:
|
|
35
|
+
results: list[VideoExifToolExtractor | types.ErrorMetadata] = []
|
|
36
|
+
|
|
37
|
+
for path in video_paths:
|
|
38
|
+
rdf = rdf_by_path.get(exiftool_read.canonical_path(path))
|
|
39
|
+
if rdf is None:
|
|
40
|
+
ex = exceptions.MapillaryExifToolXMLNotFoundError(
|
|
41
|
+
"Cannot find the video in the ExifTool XML"
|
|
42
|
+
)
|
|
43
|
+
results.append(
|
|
44
|
+
types.describe_error_metadata(
|
|
45
|
+
ex, path, filetype=types.FileType.VIDEO
|
|
46
|
+
)
|
|
47
|
+
)
|
|
48
|
+
else:
|
|
49
|
+
results.append(VideoExifToolExtractor(path, rdf))
|
|
50
|
+
|
|
51
|
+
return results
|
|
52
|
+
|
|
53
|
+
@override
|
|
54
|
+
def _generate_video_extractors(
|
|
55
|
+
self, video_paths: T.Sequence[Path]
|
|
56
|
+
) -> T.Sequence[VideoExifToolExtractor | types.ErrorMetadata]:
|
|
57
|
+
rdf_by_path = self.find_rdf_by_path(self.source_path, video_paths)
|
|
58
|
+
return self.build_video_extractors_from_etree(rdf_by_path, video_paths)
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def find_rdf_by_path(
|
|
62
|
+
cls, option: options.SourcePathOption, paths: T.Iterable[Path]
|
|
63
|
+
) -> dict[str, ET.Element]:
|
|
64
|
+
if option.source_path is not None:
|
|
65
|
+
return index_rdf_description_by_path([option.source_path])
|
|
66
|
+
|
|
67
|
+
elif option.pattern is not None:
|
|
68
|
+
rdf_by_path = {}
|
|
69
|
+
for path in paths:
|
|
70
|
+
source_path = option.resolve(path)
|
|
71
|
+
r = index_rdf_description_by_path([source_path])
|
|
72
|
+
rdfs = list(r.values())
|
|
73
|
+
if rdfs:
|
|
74
|
+
rdf_by_path[exiftool_read.canonical_path(path)] = rdfs[0]
|
|
75
|
+
return rdf_by_path
|
|
76
|
+
|
|
77
|
+
else:
|
|
78
|
+
assert False, "Either source_path or pattern must be provided"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class GeotagVideosFromExifToolRunner(GeotagVideosFromGeneric):
|
|
82
|
+
@override
|
|
83
|
+
def _generate_video_extractors(
|
|
84
|
+
self, video_paths: T.Sequence[Path]
|
|
85
|
+
) -> T.Sequence[VideoExifToolExtractor | types.ErrorMetadata]:
|
|
86
|
+
if constants.EXIFTOOL_PATH is None:
|
|
87
|
+
runner = ExiftoolRunner()
|
|
88
|
+
else:
|
|
89
|
+
runner = ExiftoolRunner(constants.EXIFTOOL_PATH)
|
|
90
|
+
|
|
91
|
+
LOG.debug(
|
|
92
|
+
"Extracting XML from %d videos with ExifTool command: %s",
|
|
93
|
+
len(video_paths),
|
|
94
|
+
" ".join(runner._build_args_read_stdin()),
|
|
95
|
+
)
|
|
96
|
+
try:
|
|
97
|
+
xml = runner.extract_xml(video_paths)
|
|
98
|
+
except FileNotFoundError as ex:
|
|
99
|
+
exiftool_ex = exceptions.MapillaryExiftoolNotFoundError(ex)
|
|
100
|
+
return [
|
|
101
|
+
types.describe_error_metadata(
|
|
102
|
+
exiftool_ex, video_path, filetype=types.FileType.VIDEO
|
|
103
|
+
)
|
|
104
|
+
for video_path in video_paths
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
xml_element = ET.fromstring(xml)
|
|
109
|
+
except ET.ParseError as ex:
|
|
110
|
+
LOG.warning(
|
|
111
|
+
"Failed to parse ExifTool XML: %s",
|
|
112
|
+
str(ex),
|
|
113
|
+
exc_info=LOG.getEffectiveLevel() <= logging.DEBUG,
|
|
114
|
+
)
|
|
115
|
+
rdf_by_path = {}
|
|
116
|
+
else:
|
|
117
|
+
rdf_by_path = exiftool_read.index_rdf_description_by_path_from_xml_element(
|
|
118
|
+
xml_element
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
return GeotagVideosFromExifToolXML.build_video_extractors_from_etree(
|
|
122
|
+
rdf_by_path, video_paths
|
|
123
|
+
)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import sys
|
|
5
|
+
import typing as T
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
if sys.version_info >= (3, 12):
|
|
9
|
+
from typing import override
|
|
10
|
+
else:
|
|
11
|
+
from typing_extensions import override
|
|
12
|
+
|
|
13
|
+
from .. import exceptions, types
|
|
14
|
+
from . import options
|
|
15
|
+
from .base import GeotagVideosFromGeneric
|
|
16
|
+
from .video_extractors.gpx import GPXVideoExtractor
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
LOG = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class GeotagVideosFromGPX(GeotagVideosFromGeneric):
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
source_path: options.SourcePathOption | None = None,
|
|
26
|
+
num_processes: int | None = None,
|
|
27
|
+
):
|
|
28
|
+
super().__init__(num_processes=num_processes)
|
|
29
|
+
if source_path is None:
|
|
30
|
+
source_path = options.SourcePathOption(pattern="%g.gpx")
|
|
31
|
+
self.source_path = source_path
|
|
32
|
+
|
|
33
|
+
@override
|
|
34
|
+
def _generate_video_extractors(
|
|
35
|
+
self, video_paths: T.Sequence[Path]
|
|
36
|
+
) -> T.Sequence[GPXVideoExtractor | types.ErrorMetadata]:
|
|
37
|
+
results: list[GPXVideoExtractor | types.ErrorMetadata] = []
|
|
38
|
+
for video_path in video_paths:
|
|
39
|
+
source_path = self.source_path.resolve(video_path)
|
|
40
|
+
if source_path.is_file():
|
|
41
|
+
results.append(GPXVideoExtractor(video_path, source_path))
|
|
42
|
+
else:
|
|
43
|
+
results.append(
|
|
44
|
+
types.describe_error_metadata(
|
|
45
|
+
exceptions.MapillaryVideoGPSNotFoundError(
|
|
46
|
+
"GPX file not found for video"
|
|
47
|
+
),
|
|
48
|
+
filename=video_path,
|
|
49
|
+
filetype=types.FileType.VIDEO,
|
|
50
|
+
)
|
|
51
|
+
)
|
|
52
|
+
return results
|
|
@@ -1,197 +1,32 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
3
4
|
import typing as T
|
|
4
|
-
from multiprocessing import Pool
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
from
|
|
11
|
-
from ..mp4 import simple_mp4_parser as sparser
|
|
12
|
-
from ..telemetry import GPSPoint
|
|
13
|
-
from . import blackvue_parser, gpmf_gps_filter, gpmf_parser, utils as video_utils
|
|
14
|
-
from .geotag_from_generic import GeotagVideosFromGeneric
|
|
7
|
+
if sys.version_info >= (3, 12):
|
|
8
|
+
from typing import override
|
|
9
|
+
else:
|
|
10
|
+
from typing_extensions import override
|
|
15
11
|
|
|
16
|
-
|
|
12
|
+
from ..types import FileType
|
|
13
|
+
from .base import GeotagVideosFromGeneric
|
|
14
|
+
from .video_extractors.native import NativeVideoExtractor
|
|
17
15
|
|
|
18
16
|
|
|
19
17
|
class GeotagVideosFromVideo(GeotagVideosFromGeneric):
|
|
20
18
|
def __init__(
|
|
21
19
|
self,
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
num_processes: T.Optional[int] = None,
|
|
20
|
+
filetypes: set[FileType] | None = None,
|
|
21
|
+
num_processes: int | None = None,
|
|
25
22
|
):
|
|
26
|
-
|
|
23
|
+
super().__init__(num_processes=num_processes)
|
|
27
24
|
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
|
-
)
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
def _geotag_video(
|
|
58
|
-
self,
|
|
59
|
-
video_path: Path,
|
|
60
|
-
) -> types.VideoMetadataOrError:
|
|
61
|
-
return GeotagVideosFromVideo.geotag_video(video_path, self.filetypes)
|
|
62
|
-
|
|
63
|
-
@staticmethod
|
|
64
|
-
def _extract_video_metadata(
|
|
65
|
-
video_path: Path,
|
|
66
|
-
filetypes: T.Optional[T.Set[types.FileType]] = None,
|
|
67
|
-
) -> T.Optional[types.VideoMetadata]:
|
|
68
|
-
if (
|
|
69
|
-
filetypes is None
|
|
70
|
-
or types.FileType.VIDEO in filetypes
|
|
71
|
-
or types.FileType.CAMM in filetypes
|
|
72
|
-
):
|
|
73
|
-
with video_path.open("rb") as fp:
|
|
74
|
-
try:
|
|
75
|
-
points = camm_parser.extract_points(fp)
|
|
76
|
-
except sparser.ParsingError:
|
|
77
|
-
points = None
|
|
78
|
-
|
|
79
|
-
if points is not None:
|
|
80
|
-
fp.seek(0, io.SEEK_SET)
|
|
81
|
-
make, model = camm_parser.extract_camera_make_and_model(fp)
|
|
82
|
-
return types.VideoMetadata(
|
|
83
|
-
filename=video_path,
|
|
84
|
-
md5sum=None,
|
|
85
|
-
filesize=utils.get_file_size(video_path),
|
|
86
|
-
filetype=types.FileType.CAMM,
|
|
87
|
-
points=points,
|
|
88
|
-
make=make,
|
|
89
|
-
model=model,
|
|
90
|
-
)
|
|
91
|
-
|
|
92
|
-
if (
|
|
93
|
-
filetypes is None
|
|
94
|
-
or types.FileType.VIDEO in filetypes
|
|
95
|
-
or types.FileType.GOPRO in filetypes
|
|
96
|
-
):
|
|
97
|
-
with video_path.open("rb") as fp:
|
|
98
|
-
try:
|
|
99
|
-
points_with_fix = gpmf_parser.extract_points(fp)
|
|
100
|
-
except sparser.ParsingError:
|
|
101
|
-
points_with_fix = None
|
|
102
|
-
|
|
103
|
-
if points_with_fix is not None:
|
|
104
|
-
fp.seek(0, io.SEEK_SET)
|
|
105
|
-
make, model = "GoPro", gpmf_parser.extract_camera_model(fp)
|
|
106
|
-
return types.VideoMetadata(
|
|
107
|
-
filename=video_path,
|
|
108
|
-
md5sum=None,
|
|
109
|
-
filesize=utils.get_file_size(video_path),
|
|
110
|
-
filetype=types.FileType.GOPRO,
|
|
111
|
-
points=T.cast(T.List[geo.Point], points_with_fix),
|
|
112
|
-
make=make,
|
|
113
|
-
model=model,
|
|
114
|
-
)
|
|
115
|
-
|
|
116
|
-
if (
|
|
117
|
-
filetypes is None
|
|
118
|
-
or types.FileType.VIDEO in filetypes
|
|
119
|
-
or types.FileType.BLACKVUE in filetypes
|
|
120
|
-
):
|
|
121
|
-
with video_path.open("rb") as fp:
|
|
122
|
-
try:
|
|
123
|
-
points = blackvue_parser.extract_points(fp)
|
|
124
|
-
except sparser.ParsingError:
|
|
125
|
-
points = None
|
|
126
|
-
|
|
127
|
-
if points is not None:
|
|
128
|
-
fp.seek(0, io.SEEK_SET)
|
|
129
|
-
make, model = "BlackVue", blackvue_parser.extract_camera_model(fp)
|
|
130
|
-
return types.VideoMetadata(
|
|
131
|
-
filename=video_path,
|
|
132
|
-
md5sum=None,
|
|
133
|
-
filesize=utils.get_file_size(video_path),
|
|
134
|
-
filetype=types.FileType.BLACKVUE,
|
|
135
|
-
points=points,
|
|
136
|
-
make=make,
|
|
137
|
-
model=model,
|
|
138
|
-
)
|
|
139
|
-
|
|
140
|
-
return None
|
|
141
|
-
|
|
142
|
-
@staticmethod
|
|
143
|
-
def geotag_video(
|
|
144
|
-
video_path: Path,
|
|
145
|
-
filetypes: T.Optional[T.Set[types.FileType]] = None,
|
|
146
|
-
) -> types.VideoMetadataOrError:
|
|
147
|
-
video_metadata = None
|
|
148
|
-
try:
|
|
149
|
-
video_metadata = GeotagVideosFromVideo._extract_video_metadata(
|
|
150
|
-
video_path, filetypes
|
|
151
|
-
)
|
|
152
|
-
|
|
153
|
-
if video_metadata is None:
|
|
154
|
-
raise exceptions.MapillaryVideoError("No GPS data found from the video")
|
|
155
|
-
|
|
156
|
-
if not video_metadata.points:
|
|
157
|
-
raise exceptions.MapillaryGPXEmptyError("Empty GPS data found")
|
|
158
|
-
|
|
159
|
-
video_metadata.points = geo.extend_deduplicate_points(video_metadata.points)
|
|
160
|
-
assert video_metadata.points, "must have at least one point"
|
|
161
|
-
|
|
162
|
-
if all(isinstance(p, GPSPoint) for p in video_metadata.points):
|
|
163
|
-
video_metadata.points = T.cast(
|
|
164
|
-
T.List[geo.Point],
|
|
165
|
-
gpmf_gps_filter.remove_noisy_points(
|
|
166
|
-
T.cast(T.List[GPSPoint], video_metadata.points)
|
|
167
|
-
),
|
|
168
|
-
)
|
|
169
|
-
if not video_metadata.points:
|
|
170
|
-
raise exceptions.MapillaryGPSNoiseError("GPS is too noisy")
|
|
171
|
-
|
|
172
|
-
stationary = video_utils.is_video_stationary(
|
|
173
|
-
geo.get_max_distance_from_start(
|
|
174
|
-
[(p.lat, p.lon) for p in video_metadata.points]
|
|
175
|
-
)
|
|
176
|
-
)
|
|
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
|
-
return types.describe_error_metadata(
|
|
192
|
-
ex,
|
|
193
|
-
video_path,
|
|
194
|
-
filetype=filetype,
|
|
195
|
-
)
|
|
196
25
|
|
|
197
|
-
|
|
26
|
+
@override
|
|
27
|
+
def _generate_video_extractors(
|
|
28
|
+
self, video_paths: T.Sequence[Path]
|
|
29
|
+
) -> T.Sequence[NativeVideoExtractor]:
|
|
30
|
+
return [
|
|
31
|
+
NativeVideoExtractor(path, filetypes=self.filetypes) for path in video_paths
|
|
32
|
+
]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import abc
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from ... import types
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BaseImageExtractor(abc.ABC):
|
|
10
|
+
"""
|
|
11
|
+
Extracts metadata from an image file.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, image_path: Path):
|
|
15
|
+
self.image_path = image_path
|
|
16
|
+
|
|
17
|
+
def extract(self) -> types.ImageMetadataOrError:
|
|
18
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import sys
|
|
5
|
+
import typing as T
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
if sys.version_info >= (3, 12):
|
|
9
|
+
from typing import override
|
|
10
|
+
else:
|
|
11
|
+
from typing_extensions import override
|
|
12
|
+
|
|
13
|
+
from ... import exceptions, exif_read, geo, types, utils
|
|
14
|
+
from .base import BaseImageExtractor
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ImageEXIFExtractor(BaseImageExtractor):
|
|
18
|
+
def __init__(self, image_path: Path, skip_lonlat_error: bool = False):
|
|
19
|
+
super().__init__(image_path)
|
|
20
|
+
self.skip_lonlat_error = skip_lonlat_error
|
|
21
|
+
|
|
22
|
+
@contextlib.contextmanager
|
|
23
|
+
def _exif_context(self) -> T.Generator[exif_read.ExifReadABC, None, None]:
|
|
24
|
+
with self.image_path.open("rb") as fp:
|
|
25
|
+
yield exif_read.ExifRead(fp)
|
|
26
|
+
|
|
27
|
+
@override
|
|
28
|
+
def extract(self) -> types.ImageMetadata:
|
|
29
|
+
with self._exif_context() as exif:
|
|
30
|
+
lonlat = exif.extract_lon_lat()
|
|
31
|
+
if lonlat is None:
|
|
32
|
+
if not self.skip_lonlat_error:
|
|
33
|
+
raise exceptions.MapillaryGeoTaggingError(
|
|
34
|
+
"Unable to extract GPS Longitude or GPS Latitude from the image"
|
|
35
|
+
)
|
|
36
|
+
lonlat = (0.0, 0.0)
|
|
37
|
+
lon, lat = lonlat
|
|
38
|
+
|
|
39
|
+
capture_time = exif.extract_capture_time()
|
|
40
|
+
if capture_time is None:
|
|
41
|
+
raise exceptions.MapillaryGeoTaggingError(
|
|
42
|
+
"Unable to extract timestamp from the image"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
image_metadata = types.ImageMetadata(
|
|
46
|
+
filename=self.image_path,
|
|
47
|
+
filesize=utils.get_file_size(self.image_path),
|
|
48
|
+
time=geo.as_unix_time(capture_time),
|
|
49
|
+
lat=lat,
|
|
50
|
+
lon=lon,
|
|
51
|
+
alt=exif.extract_altitude(),
|
|
52
|
+
angle=exif.extract_direction(),
|
|
53
|
+
width=exif.extract_width(),
|
|
54
|
+
height=exif.extract_height(),
|
|
55
|
+
MAPOrientation=exif.extract_orientation(),
|
|
56
|
+
MAPDeviceMake=exif.extract_make(),
|
|
57
|
+
MAPDeviceModel=exif.extract_model(),
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
return image_metadata
|