mapillary-tools 0.13.3a1__py3-none-any.whl → 0.14.0a2__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 +237 -16
- 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 +429 -181
- mapillary_tools/commands/__main__.py +12 -6
- 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 +31 -13
- mapillary_tools/constants.py +47 -6
- mapillary_tools/exceptions.py +34 -35
- mapillary_tools/exif_read.py +221 -116
- mapillary_tools/exif_write.py +7 -7
- mapillary_tools/exiftool_read.py +33 -42
- mapillary_tools/exiftool_read_video.py +46 -33
- mapillary_tools/exiftool_runner.py +77 -0
- mapillary_tools/ffmpeg.py +24 -23
- mapillary_tools/geo.py +144 -120
- mapillary_tools/geotag/base.py +147 -0
- mapillary_tools/geotag/factory.py +291 -0
- mapillary_tools/geotag/geotag_images_from_exif.py +14 -131
- mapillary_tools/geotag/geotag_images_from_exiftool.py +126 -82
- mapillary_tools/geotag/geotag_images_from_gpx.py +53 -118
- 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 +53 -51
- mapillary_tools/geotag/geotag_videos_from_exiftool.py +97 -0
- mapillary_tools/geotag/geotag_videos_from_gpx.py +39 -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 +160 -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/{video_data_extraction/extractors/gpx_parser.py → geotag/video_extractors/gpx.py} +57 -39
- mapillary_tools/geotag/video_extractors/native.py +157 -0
- mapillary_tools/{geotag → gpmf}/gpmf_parser.py +205 -182
- mapillary_tools/{geotag → gpmf}/gps_filter.py +5 -3
- mapillary_tools/history.py +7 -13
- mapillary_tools/mp4/construct_mp4_parser.py +9 -8
- 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 +155 -392
- mapillary_tools/process_sequence_properties.py +562 -208
- mapillary_tools/sample_video.py +13 -20
- mapillary_tools/telemetry.py +26 -13
- mapillary_tools/types.py +111 -58
- mapillary_tools/upload.py +316 -298
- mapillary_tools/upload_api_v4.py +55 -122
- mapillary_tools/uploader.py +396 -254
- mapillary_tools/utils.py +42 -18
- {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0a2.dist-info}/METADATA +3 -2
- mapillary_tools-0.14.0a2.dist-info/RECORD +72 -0
- {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0a2.dist-info}/WHEEL +1 -1
- mapillary_tools/geotag/__init__.py +0 -1
- 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/nmea_parser.py +0 -24
- mapillary_tools/video_data_extraction/video_data_parser_factory.py +0 -39
- mapillary_tools-0.13.3a1.dist-info/RECORD +0 -75
- /mapillary_tools/{geotag → gpmf}/gpmf_gps_filter.py +0 -0
- {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0a2.dist-info}/entry_points.txt +0 -0
- {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0a2.dist-info/licenses}/LICENSE +0 -0
- {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0a2.dist-info}/top_level.txt +0 -0
|
@@ -1,13 +1,18 @@
|
|
|
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 .geotag_images_from_gpx import GeotagImagesFromGPXWithProgress
|
|
14
|
+
from .base import GeotagImagesFromGeneric
|
|
15
|
+
from .geotag_images_from_gpx import GeotagImagesFromGPX
|
|
11
16
|
|
|
12
17
|
|
|
13
18
|
LOG = logging.getLogger(__name__)
|
|
@@ -16,75 +21,72 @@ LOG = logging.getLogger(__name__)
|
|
|
16
21
|
class GeotagImagesFromVideo(GeotagImagesFromGeneric):
|
|
17
22
|
def __init__(
|
|
18
23
|
self,
|
|
19
|
-
image_paths: T.Sequence[Path],
|
|
20
24
|
video_metadatas: T.Sequence[types.VideoMetadataOrError],
|
|
21
25
|
offset_time: float = 0.0,
|
|
22
|
-
num_processes:
|
|
26
|
+
num_processes: int | None = None,
|
|
23
27
|
):
|
|
24
|
-
|
|
28
|
+
super().__init__(num_processes=num_processes)
|
|
25
29
|
self.video_metadatas = video_metadatas
|
|
26
30
|
self.offset_time = offset_time
|
|
27
|
-
self.num_processes = num_processes
|
|
28
|
-
super().__init__()
|
|
29
31
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
@override
|
|
33
|
+
def to_description(
|
|
34
|
+
self, image_paths: T.Sequence[Path]
|
|
35
|
+
) -> list[types.ImageMetadataOrError]:
|
|
36
|
+
# Will return this list
|
|
37
|
+
final_image_metadatas: list[types.ImageMetadataOrError] = []
|
|
33
38
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
39
|
+
video_metadatas, video_error_metadatas = types.separate_errors(
|
|
40
|
+
self.video_metadatas
|
|
41
|
+
)
|
|
37
42
|
|
|
38
|
-
|
|
39
|
-
|
|
43
|
+
for video_error_metadata in video_error_metadatas:
|
|
44
|
+
video_path = video_error_metadata.filename
|
|
45
|
+
sample_paths = list(utils.filter_video_samples(image_paths, video_path))
|
|
46
|
+
LOG.debug(
|
|
47
|
+
"Found %d sample images from video %s with error: %s",
|
|
48
|
+
len(sample_paths),
|
|
49
|
+
video_path,
|
|
50
|
+
video_error_metadata.error,
|
|
40
51
|
)
|
|
52
|
+
for sample_path in sample_paths:
|
|
53
|
+
image_error_metadata = types.describe_error_metadata(
|
|
54
|
+
video_error_metadata.error,
|
|
55
|
+
sample_path,
|
|
56
|
+
filetype=types.FileType.IMAGE,
|
|
57
|
+
)
|
|
58
|
+
final_image_metadatas.append(image_error_metadata)
|
|
59
|
+
|
|
60
|
+
for video_metadata in video_metadatas:
|
|
61
|
+
video_path = video_metadata.filename
|
|
62
|
+
|
|
63
|
+
sample_paths = list(utils.filter_video_samples(image_paths, video_path))
|
|
41
64
|
LOG.debug(
|
|
42
65
|
"Found %d sample images from video %s",
|
|
43
|
-
len(
|
|
66
|
+
len(sample_paths),
|
|
44
67
|
video_path,
|
|
45
68
|
)
|
|
46
69
|
|
|
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,
|
|
70
|
+
geotag = GeotagImagesFromGPX(
|
|
71
|
+
video_metadata.points,
|
|
72
|
+
use_gpx_start_time=False,
|
|
73
|
+
use_image_start_time=True,
|
|
74
|
+
offset_time=self.offset_time,
|
|
75
|
+
num_processes=self.num_processes,
|
|
79
76
|
)
|
|
77
|
+
|
|
78
|
+
image_metadatas = geotag.to_description(image_paths)
|
|
79
|
+
|
|
80
80
|
for metadata in image_metadatas:
|
|
81
81
|
if isinstance(metadata, types.ImageMetadata):
|
|
82
82
|
metadata.MAPDeviceMake = video_metadata.make
|
|
83
83
|
metadata.MAPDeviceModel = video_metadata.model
|
|
84
84
|
|
|
85
|
+
final_image_metadatas.extend(image_metadatas)
|
|
86
|
+
|
|
85
87
|
# NOTE: this method only geotags images that have a corresponding video,
|
|
86
88
|
# so the number of image metadata objects returned might be less than
|
|
87
89
|
# the number of the input image_paths
|
|
88
|
-
assert len(final_image_metadatas) <= len(
|
|
90
|
+
assert len(final_image_metadatas) <= len(image_paths)
|
|
89
91
|
|
|
90
92
|
return final_image_metadatas
|
|
@@ -0,0 +1,97 @@
|
|
|
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 .base import GeotagVideosFromGeneric
|
|
17
|
+
from .utils import index_rdf_description_by_path
|
|
18
|
+
from .video_extractors.exiftool import VideoExifToolExtractor
|
|
19
|
+
|
|
20
|
+
LOG = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class GeotagVideosFromExifToolXML(GeotagVideosFromGeneric):
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
xml_path: Path,
|
|
27
|
+
num_processes: int | None = None,
|
|
28
|
+
):
|
|
29
|
+
super().__init__(num_processes=num_processes)
|
|
30
|
+
self.xml_path = xml_path
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def build_image_extractors(
|
|
34
|
+
cls,
|
|
35
|
+
rdf_by_path: dict[str, ET.Element],
|
|
36
|
+
video_paths: T.Iterable[Path],
|
|
37
|
+
) -> list[VideoExifToolExtractor | types.ErrorMetadata]:
|
|
38
|
+
results: list[VideoExifToolExtractor | types.ErrorMetadata] = []
|
|
39
|
+
|
|
40
|
+
for path in video_paths:
|
|
41
|
+
rdf = rdf_by_path.get(exiftool_read.canonical_path(path))
|
|
42
|
+
if rdf is None:
|
|
43
|
+
ex = exceptions.MapillaryExifToolXMLNotFoundError(
|
|
44
|
+
"Cannot find the video in the ExifTool XML"
|
|
45
|
+
)
|
|
46
|
+
results.append(
|
|
47
|
+
types.describe_error_metadata(
|
|
48
|
+
ex, path, filetype=types.FileType.VIDEO
|
|
49
|
+
)
|
|
50
|
+
)
|
|
51
|
+
else:
|
|
52
|
+
results.append(VideoExifToolExtractor(path, rdf))
|
|
53
|
+
|
|
54
|
+
return results
|
|
55
|
+
|
|
56
|
+
@override
|
|
57
|
+
def _generate_video_extractors(
|
|
58
|
+
self, video_paths: T.Sequence[Path]
|
|
59
|
+
) -> T.Sequence[VideoExifToolExtractor | types.ErrorMetadata]:
|
|
60
|
+
rdf_by_path = index_rdf_description_by_path([self.xml_path])
|
|
61
|
+
return self.build_image_extractors(rdf_by_path, video_paths)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class GeotagVideosFromExifToolRunner(GeotagVideosFromGeneric):
|
|
65
|
+
@override
|
|
66
|
+
def _generate_video_extractors(
|
|
67
|
+
self, video_paths: T.Sequence[Path]
|
|
68
|
+
) -> T.Sequence[VideoExifToolExtractor | types.ErrorMetadata]:
|
|
69
|
+
runner = ExiftoolRunner(constants.EXIFTOOL_PATH)
|
|
70
|
+
|
|
71
|
+
LOG.debug(
|
|
72
|
+
"Extracting XML from %d videos with ExifTool command: %s",
|
|
73
|
+
len(video_paths),
|
|
74
|
+
" ".join(runner._build_args_read_stdin()),
|
|
75
|
+
)
|
|
76
|
+
try:
|
|
77
|
+
xml = runner.extract_xml(video_paths)
|
|
78
|
+
except FileNotFoundError as ex:
|
|
79
|
+
raise exceptions.MapillaryExiftoolNotFoundError(ex) from ex
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
xml_element = ET.fromstring(xml)
|
|
83
|
+
except ET.ParseError as ex:
|
|
84
|
+
LOG.warning(
|
|
85
|
+
"Failed to parse ExifTool XML: %s",
|
|
86
|
+
str(ex),
|
|
87
|
+
exc_info=LOG.getEffectiveLevel() <= logging.DEBUG,
|
|
88
|
+
)
|
|
89
|
+
rdf_by_path = {}
|
|
90
|
+
else:
|
|
91
|
+
rdf_by_path = exiftool_read.index_rdf_description_by_path_from_xml_element(
|
|
92
|
+
xml_element
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
return GeotagVideosFromExifToolXML.build_image_extractors(
|
|
96
|
+
rdf_by_path, video_paths
|
|
97
|
+
)
|
|
@@ -0,0 +1,39 @@
|
|
|
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 options
|
|
14
|
+
from .base import GeotagVideosFromGeneric
|
|
15
|
+
from .video_extractors.gpx import GPXVideoExtractor
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
LOG = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class GeotagVideosFromGPX(GeotagVideosFromGeneric):
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
option: options.SourcePathOption | None = None,
|
|
25
|
+
num_processes: int | None = None,
|
|
26
|
+
):
|
|
27
|
+
super().__init__(num_processes=num_processes)
|
|
28
|
+
if option is None:
|
|
29
|
+
option = options.SourcePathOption(pattern="%f.gpx")
|
|
30
|
+
self.option = option
|
|
31
|
+
|
|
32
|
+
@override
|
|
33
|
+
def _generate_video_extractors(
|
|
34
|
+
self, video_paths: T.Sequence[Path]
|
|
35
|
+
) -> T.Sequence[GPXVideoExtractor]:
|
|
36
|
+
return [
|
|
37
|
+
GPXVideoExtractor(video_path, self.option.resolve(video_path))
|
|
38
|
+
for video_path in video_paths
|
|
39
|
+
]
|
|
@@ -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
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import xml.etree.ElementTree as ET
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from ... import exiftool_read
|
|
8
|
+
from .exif import ImageEXIFExtractor
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ImageExifToolExtractor(ImageEXIFExtractor):
|
|
12
|
+
def __init__(self, image_path: Path, element: ET.Element):
|
|
13
|
+
super().__init__(image_path)
|
|
14
|
+
self.element = element
|
|
15
|
+
|
|
16
|
+
@contextlib.contextmanager
|
|
17
|
+
def _exif_context(self):
|
|
18
|
+
yield exiftool_read.ExifToolRead(ET.ElementTree(self.element))
|