mapillary-tools 0.14.0a1__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 +4 -4
- mapillary_tools/camm/camm_parser.py +5 -5
- mapillary_tools/commands/__main__.py +1 -2
- mapillary_tools/config.py +7 -5
- mapillary_tools/constants.py +1 -2
- mapillary_tools/exceptions.py +1 -1
- mapillary_tools/exif_read.py +65 -65
- mapillary_tools/exif_write.py +7 -7
- mapillary_tools/exiftool_read.py +23 -46
- mapillary_tools/exiftool_read_video.py +36 -34
- mapillary_tools/ffmpeg.py +24 -23
- mapillary_tools/geo.py +4 -21
- mapillary_tools/geotag/{geotag_from_generic.py → base.py} +32 -48
- mapillary_tools/geotag/factory.py +27 -34
- mapillary_tools/geotag/geotag_images_from_exif.py +15 -51
- mapillary_tools/geotag/geotag_images_from_exiftool.py +107 -59
- mapillary_tools/geotag/geotag_images_from_gpx.py +20 -10
- mapillary_tools/geotag/geotag_images_from_gpx_file.py +2 -34
- mapillary_tools/geotag/geotag_images_from_nmea_file.py +0 -3
- mapillary_tools/geotag/geotag_images_from_video.py +16 -14
- mapillary_tools/geotag/geotag_videos_from_exiftool.py +97 -0
- mapillary_tools/geotag/geotag_videos_from_gpx.py +14 -115
- mapillary_tools/geotag/geotag_videos_from_video.py +14 -147
- 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 +1 -0
- mapillary_tools/geotag/utils.py +62 -0
- 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/gpmf/gpmf_parser.py +16 -16
- mapillary_tools/gpmf/gps_filter.py +5 -3
- mapillary_tools/history.py +4 -2
- mapillary_tools/mp4/construct_mp4_parser.py +9 -8
- mapillary_tools/mp4/mp4_sample_parser.py +27 -27
- mapillary_tools/mp4/simple_mp4_builder.py +10 -9
- mapillary_tools/mp4/simple_mp4_parser.py +13 -12
- mapillary_tools/process_geotag_properties.py +5 -7
- mapillary_tools/process_sequence_properties.py +40 -38
- mapillary_tools/sample_video.py +8 -8
- mapillary_tools/telemetry.py +6 -5
- mapillary_tools/types.py +33 -38
- mapillary_tools/utils.py +16 -18
- {mapillary_tools-0.14.0a1.dist-info → mapillary_tools-0.14.0a2.dist-info}/METADATA +1 -1
- mapillary_tools-0.14.0a2.dist-info/RECORD +72 -0
- mapillary_tools/geotag/__init__.py +0 -1
- mapillary_tools/geotag/geotag_images_from_exiftool_both_image_and_video.py +0 -77
- mapillary_tools/geotag/geotag_videos_from_exiftool_video.py +0 -151
- mapillary_tools/video_data_extraction/cli_options.py +0 -22
- mapillary_tools/video_data_extraction/extract_video_data.py +0 -157
- mapillary_tools/video_data_extraction/extractors/base_parser.py +0 -75
- mapillary_tools/video_data_extraction/extractors/blackvue_parser.py +0 -49
- mapillary_tools/video_data_extraction/extractors/camm_parser.py +0 -62
- mapillary_tools/video_data_extraction/extractors/exiftool_runtime_parser.py +0 -74
- mapillary_tools/video_data_extraction/extractors/exiftool_xml_parser.py +0 -52
- mapillary_tools/video_data_extraction/extractors/generic_video_parser.py +0 -52
- mapillary_tools/video_data_extraction/extractors/gopro_parser.py +0 -58
- 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.14.0a1.dist-info/RECORD +0 -78
- {mapillary_tools-0.14.0a1.dist-info → mapillary_tools-0.14.0a2.dist-info}/WHEEL +0 -0
- {mapillary_tools-0.14.0a1.dist-info → mapillary_tools-0.14.0a2.dist-info}/entry_points.txt +0 -0
- {mapillary_tools-0.14.0a1.dist-info → mapillary_tools-0.14.0a2.dist-info}/licenses/LICENSE +0 -0
- {mapillary_tools-0.14.0a1.dist-info → mapillary_tools-0.14.0a2.dist-info}/top_level.txt +0 -0
|
@@ -2,11 +2,17 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import dataclasses
|
|
4
4
|
import logging
|
|
5
|
+
import sys
|
|
5
6
|
import typing as T
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
|
|
9
|
+
if sys.version_info >= (3, 12):
|
|
10
|
+
from typing import override
|
|
11
|
+
else:
|
|
12
|
+
from typing_extensions import override
|
|
13
|
+
|
|
8
14
|
from .. import exceptions, geo, types
|
|
9
|
-
from .
|
|
15
|
+
from .base import GeotagImagesFromGeneric
|
|
10
16
|
from .geotag_images_from_exif import ImageEXIFExtractor
|
|
11
17
|
|
|
12
18
|
|
|
@@ -16,14 +22,13 @@ LOG = logging.getLogger(__name__)
|
|
|
16
22
|
class GeotagImagesFromGPX(GeotagImagesFromGeneric):
|
|
17
23
|
def __init__(
|
|
18
24
|
self,
|
|
19
|
-
image_paths: T.Sequence[Path],
|
|
20
25
|
points: T.Sequence[geo.Point],
|
|
21
26
|
use_gpx_start_time: bool = False,
|
|
22
27
|
use_image_start_time: bool = False,
|
|
23
28
|
offset_time: float = 0.0,
|
|
24
29
|
num_processes: int | None = None,
|
|
25
30
|
):
|
|
26
|
-
super().__init__(
|
|
31
|
+
super().__init__(num_processes=num_processes)
|
|
27
32
|
self.points = points
|
|
28
33
|
self.use_gpx_start_time = use_gpx_start_time
|
|
29
34
|
self.use_image_start_time = use_image_start_time
|
|
@@ -73,16 +78,21 @@ class GeotagImagesFromGPX(GeotagImagesFromGeneric):
|
|
|
73
78
|
time=interpolated.time,
|
|
74
79
|
)
|
|
75
80
|
|
|
76
|
-
|
|
81
|
+
@override
|
|
82
|
+
def _generate_image_extractors(
|
|
83
|
+
self, image_paths: T.Sequence[Path]
|
|
84
|
+
) -> T.Sequence[ImageEXIFExtractor]:
|
|
77
85
|
return [
|
|
78
|
-
ImageEXIFExtractor(path, skip_lonlat_error=True)
|
|
79
|
-
for path in self.image_paths
|
|
86
|
+
ImageEXIFExtractor(path, skip_lonlat_error=True) for path in image_paths
|
|
80
87
|
]
|
|
81
88
|
|
|
82
|
-
|
|
89
|
+
@override
|
|
90
|
+
def to_description(
|
|
91
|
+
self, image_paths: T.Sequence[Path]
|
|
92
|
+
) -> list[types.ImageMetadataOrError]:
|
|
83
93
|
final_metadatas: list[types.ImageMetadataOrError] = []
|
|
84
94
|
|
|
85
|
-
image_metadata_or_errors = super().to_description()
|
|
95
|
+
image_metadata_or_errors = super().to_description(image_paths)
|
|
86
96
|
|
|
87
97
|
image_metadatas, error_metadatas = types.separate_errors(
|
|
88
98
|
image_metadata_or_errors
|
|
@@ -90,7 +100,7 @@ class GeotagImagesFromGPX(GeotagImagesFromGeneric):
|
|
|
90
100
|
final_metadatas.extend(error_metadatas)
|
|
91
101
|
|
|
92
102
|
if not image_metadatas:
|
|
93
|
-
assert len(
|
|
103
|
+
assert len(image_paths) == len(final_metadatas)
|
|
94
104
|
return final_metadatas
|
|
95
105
|
|
|
96
106
|
# Do not use point itself for comparison because point.angle or point.alt could be None
|
|
@@ -145,6 +155,6 @@ class GeotagImagesFromGPX(GeotagImagesFromGeneric):
|
|
|
145
155
|
)
|
|
146
156
|
final_metadatas.append(error_metadata)
|
|
147
157
|
|
|
148
|
-
assert len(
|
|
158
|
+
assert len(image_paths) == len(final_metadatas)
|
|
149
159
|
|
|
150
160
|
return final_metadatas
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
-
import typing as T
|
|
5
4
|
from pathlib import Path
|
|
6
5
|
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
from .. import geo
|
|
6
|
+
from . import utils
|
|
10
7
|
from .geotag_images_from_gpx import GeotagImagesFromGPX
|
|
11
8
|
|
|
12
9
|
|
|
@@ -16,14 +13,13 @@ LOG = logging.getLogger(__name__)
|
|
|
16
13
|
class GeotagImagesFromGPXFile(GeotagImagesFromGPX):
|
|
17
14
|
def __init__(
|
|
18
15
|
self,
|
|
19
|
-
image_paths: T.Sequence[Path],
|
|
20
16
|
source_path: Path,
|
|
21
17
|
use_gpx_start_time: bool = False,
|
|
22
18
|
offset_time: float = 0.0,
|
|
23
19
|
num_processes: int | None = None,
|
|
24
20
|
):
|
|
25
21
|
try:
|
|
26
|
-
tracks = parse_gpx(source_path)
|
|
22
|
+
tracks = utils.parse_gpx(source_path)
|
|
27
23
|
except Exception as ex:
|
|
28
24
|
raise RuntimeError(
|
|
29
25
|
f"Error parsing GPX {source_path}: {ex.__class__.__name__}: {ex}"
|
|
@@ -37,36 +33,8 @@ class GeotagImagesFromGPXFile(GeotagImagesFromGPX):
|
|
|
37
33
|
)
|
|
38
34
|
points = sum(tracks, [])
|
|
39
35
|
super().__init__(
|
|
40
|
-
image_paths,
|
|
41
36
|
points,
|
|
42
37
|
use_gpx_start_time=use_gpx_start_time,
|
|
43
38
|
offset_time=offset_time,
|
|
44
39
|
num_processes=num_processes,
|
|
45
40
|
)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
Track = T.List[geo.Point]
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def parse_gpx(gpx_file: Path) -> list[Track]:
|
|
52
|
-
with gpx_file.open("r") as f:
|
|
53
|
-
gpx = gpxpy.parse(f)
|
|
54
|
-
|
|
55
|
-
tracks: list[Track] = []
|
|
56
|
-
|
|
57
|
-
for track in gpx.tracks:
|
|
58
|
-
for segment in track.segments:
|
|
59
|
-
tracks.append([])
|
|
60
|
-
for point in segment.points:
|
|
61
|
-
if point.time is not None:
|
|
62
|
-
tracks[-1].append(
|
|
63
|
-
geo.Point(
|
|
64
|
-
time=geo.as_unix_time(point.time),
|
|
65
|
-
lat=point.latitude,
|
|
66
|
-
lon=point.longitude,
|
|
67
|
-
alt=point.elevation,
|
|
68
|
-
angle=None,
|
|
69
|
-
)
|
|
70
|
-
)
|
|
71
|
-
|
|
72
|
-
return tracks
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import datetime
|
|
4
|
-
import typing as T
|
|
5
4
|
from pathlib import Path
|
|
6
5
|
|
|
7
6
|
import pynmea2
|
|
@@ -13,7 +12,6 @@ from .geotag_images_from_gpx import GeotagImagesFromGPX
|
|
|
13
12
|
class GeotagImagesFromNMEAFile(GeotagImagesFromGPX):
|
|
14
13
|
def __init__(
|
|
15
14
|
self,
|
|
16
|
-
image_paths: T.Sequence[Path],
|
|
17
15
|
source_path: Path,
|
|
18
16
|
use_gpx_start_time: bool = False,
|
|
19
17
|
offset_time: float = 0.0,
|
|
@@ -21,7 +19,6 @@ class GeotagImagesFromNMEAFile(GeotagImagesFromGPX):
|
|
|
21
19
|
):
|
|
22
20
|
points = get_lat_lon_time_from_nmea(source_path)
|
|
23
21
|
super().__init__(
|
|
24
|
-
image_paths,
|
|
25
22
|
points,
|
|
26
23
|
use_gpx_start_time=use_gpx_start_time,
|
|
27
24
|
offset_time=offset_time,
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
+
import sys
|
|
4
5
|
import typing as T
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
if sys.version_info >= (3, 12):
|
|
9
|
+
from typing import override
|
|
10
|
+
else:
|
|
11
|
+
from typing_extensions import override
|
|
8
12
|
|
|
9
|
-
from
|
|
13
|
+
from .. import types, utils
|
|
14
|
+
from .base import GeotagImagesFromGeneric
|
|
10
15
|
from .geotag_images_from_gpx import GeotagImagesFromGPX
|
|
11
16
|
|
|
12
17
|
|
|
@@ -16,16 +21,18 @@ 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
26
|
num_processes: int | None = None,
|
|
23
27
|
):
|
|
24
|
-
super().__init__(
|
|
28
|
+
super().__init__(num_processes=num_processes)
|
|
25
29
|
self.video_metadatas = video_metadatas
|
|
26
30
|
self.offset_time = offset_time
|
|
27
31
|
|
|
28
|
-
|
|
32
|
+
@override
|
|
33
|
+
def to_description(
|
|
34
|
+
self, image_paths: T.Sequence[Path]
|
|
35
|
+
) -> list[types.ImageMetadataOrError]:
|
|
29
36
|
# Will return this list
|
|
30
37
|
final_image_metadatas: list[types.ImageMetadataOrError] = []
|
|
31
38
|
|
|
@@ -35,9 +42,7 @@ class GeotagImagesFromVideo(GeotagImagesFromGeneric):
|
|
|
35
42
|
|
|
36
43
|
for video_error_metadata in video_error_metadatas:
|
|
37
44
|
video_path = video_error_metadata.filename
|
|
38
|
-
sample_paths = list(
|
|
39
|
-
utils.filter_video_samples(self.image_paths, video_path)
|
|
40
|
-
)
|
|
45
|
+
sample_paths = list(utils.filter_video_samples(image_paths, video_path))
|
|
41
46
|
LOG.debug(
|
|
42
47
|
"Found %d sample images from video %s with error: %s",
|
|
43
48
|
len(sample_paths),
|
|
@@ -55,9 +60,7 @@ class GeotagImagesFromVideo(GeotagImagesFromGeneric):
|
|
|
55
60
|
for video_metadata in video_metadatas:
|
|
56
61
|
video_path = video_metadata.filename
|
|
57
62
|
|
|
58
|
-
sample_paths = list(
|
|
59
|
-
utils.filter_video_samples(self.image_paths, video_path)
|
|
60
|
-
)
|
|
63
|
+
sample_paths = list(utils.filter_video_samples(image_paths, video_path))
|
|
61
64
|
LOG.debug(
|
|
62
65
|
"Found %d sample images from video %s",
|
|
63
66
|
len(sample_paths),
|
|
@@ -65,7 +68,6 @@ class GeotagImagesFromVideo(GeotagImagesFromGeneric):
|
|
|
65
68
|
)
|
|
66
69
|
|
|
67
70
|
geotag = GeotagImagesFromGPX(
|
|
68
|
-
sample_paths,
|
|
69
71
|
video_metadata.points,
|
|
70
72
|
use_gpx_start_time=False,
|
|
71
73
|
use_image_start_time=True,
|
|
@@ -73,7 +75,7 @@ class GeotagImagesFromVideo(GeotagImagesFromGeneric):
|
|
|
73
75
|
num_processes=self.num_processes,
|
|
74
76
|
)
|
|
75
77
|
|
|
76
|
-
image_metadatas = geotag.to_description()
|
|
78
|
+
image_metadatas = geotag.to_description(image_paths)
|
|
77
79
|
|
|
78
80
|
for metadata in image_metadatas:
|
|
79
81
|
if isinstance(metadata, types.ImageMetadata):
|
|
@@ -85,6 +87,6 @@ class GeotagImagesFromVideo(GeotagImagesFromGeneric):
|
|
|
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
|
+
)
|
|
@@ -1,140 +1,39 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import dataclasses
|
|
4
|
-
import datetime
|
|
5
3
|
import logging
|
|
6
|
-
|
|
4
|
+
import sys
|
|
7
5
|
import typing as T
|
|
8
6
|
from pathlib import Path
|
|
9
7
|
|
|
10
|
-
|
|
8
|
+
if sys.version_info >= (3, 12):
|
|
9
|
+
from typing import override
|
|
10
|
+
else:
|
|
11
|
+
from typing_extensions import override
|
|
12
|
+
|
|
11
13
|
from . import options
|
|
12
|
-
from .
|
|
13
|
-
from .
|
|
14
|
-
from .geotag_videos_from_video import NativeVideoExtractor
|
|
14
|
+
from .base import GeotagVideosFromGeneric
|
|
15
|
+
from .video_extractors.gpx import GPXVideoExtractor
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
LOG = logging.getLogger(__name__)
|
|
18
19
|
|
|
19
20
|
|
|
20
|
-
class GPXVideoExtractor(GenericVideoExtractor):
|
|
21
|
-
def __init__(self, video_path: Path, gpx_path: Path):
|
|
22
|
-
self.video_path = video_path
|
|
23
|
-
self.gpx_path = gpx_path
|
|
24
|
-
|
|
25
|
-
def extract(self) -> types.VideoMetadataOrError:
|
|
26
|
-
try:
|
|
27
|
-
gpx_tracks = parse_gpx(self.gpx_path)
|
|
28
|
-
except Exception as ex:
|
|
29
|
-
raise RuntimeError(
|
|
30
|
-
f"Error parsing GPX {self.gpx_path}: {ex.__class__.__name__}: {ex}"
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
if 1 < len(gpx_tracks):
|
|
34
|
-
LOG.warning(
|
|
35
|
-
"Found %s tracks in the GPX file %s. Will merge points in all the tracks as a single track for interpolation",
|
|
36
|
-
len(gpx_tracks),
|
|
37
|
-
self.gpx_path,
|
|
38
|
-
)
|
|
39
|
-
|
|
40
|
-
gpx_points: T.Sequence[geo.Point] = sum(gpx_tracks, [])
|
|
41
|
-
|
|
42
|
-
native_extractor = NativeVideoExtractor(self.video_path)
|
|
43
|
-
|
|
44
|
-
video_metadata_or_error = native_extractor.extract()
|
|
45
|
-
|
|
46
|
-
if isinstance(video_metadata_or_error, types.ErrorMetadata):
|
|
47
|
-
self._rebase_times(gpx_points)
|
|
48
|
-
return types.VideoMetadata(
|
|
49
|
-
filename=video_metadata_or_error.filename,
|
|
50
|
-
filetype=video_metadata_or_error.filetype or types.FileType.VIDEO,
|
|
51
|
-
points=gpx_points,
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
video_metadata = video_metadata_or_error
|
|
55
|
-
|
|
56
|
-
offset = self._synx_gpx_by_first_gps_timestamp(
|
|
57
|
-
gpx_points, video_metadata.points
|
|
58
|
-
)
|
|
59
|
-
|
|
60
|
-
self._rebase_times(gpx_points, offset=offset)
|
|
61
|
-
|
|
62
|
-
return dataclasses.replace(video_metadata_or_error, points=gpx_points)
|
|
63
|
-
|
|
64
|
-
@staticmethod
|
|
65
|
-
def _rebase_times(points: T.Sequence[geo.Point], offset: float = 0.0):
|
|
66
|
-
"""
|
|
67
|
-
Make point times start from 0
|
|
68
|
-
"""
|
|
69
|
-
if points:
|
|
70
|
-
first_timestamp = points[0].time
|
|
71
|
-
for p in points:
|
|
72
|
-
p.time = (p.time - first_timestamp) + offset
|
|
73
|
-
return points
|
|
74
|
-
|
|
75
|
-
def _synx_gpx_by_first_gps_timestamp(
|
|
76
|
-
self, gpx_points: T.Sequence[geo.Point], video_gps_points: T.Sequence[geo.Point]
|
|
77
|
-
) -> float:
|
|
78
|
-
offset: float = 0.0
|
|
79
|
-
|
|
80
|
-
if not gpx_points:
|
|
81
|
-
return offset
|
|
82
|
-
|
|
83
|
-
first_gpx_dt = datetime.datetime.fromtimestamp(
|
|
84
|
-
gpx_points[0].time, tz=datetime.timezone.utc
|
|
85
|
-
)
|
|
86
|
-
LOG.info("First GPX timestamp: %s", first_gpx_dt)
|
|
87
|
-
|
|
88
|
-
if not video_gps_points:
|
|
89
|
-
LOG.warning(
|
|
90
|
-
"Skip GPX synchronization because no GPS found in video %s",
|
|
91
|
-
self.video_path,
|
|
92
|
-
)
|
|
93
|
-
return offset
|
|
94
|
-
|
|
95
|
-
first_gps_point = video_gps_points[0]
|
|
96
|
-
if isinstance(first_gps_point, telemetry.GPSPoint):
|
|
97
|
-
if first_gps_point.epoch_time is not None:
|
|
98
|
-
first_gps_dt = datetime.datetime.fromtimestamp(
|
|
99
|
-
first_gps_point.epoch_time, tz=datetime.timezone.utc
|
|
100
|
-
)
|
|
101
|
-
LOG.info("First GPS timestamp: %s", first_gps_dt)
|
|
102
|
-
offset = gpx_points[0].time - first_gps_point.epoch_time
|
|
103
|
-
if offset:
|
|
104
|
-
LOG.warning(
|
|
105
|
-
"Found offset between GPX %s and video GPS timestamps %s: %s seconds",
|
|
106
|
-
first_gpx_dt,
|
|
107
|
-
first_gps_dt,
|
|
108
|
-
offset,
|
|
109
|
-
)
|
|
110
|
-
else:
|
|
111
|
-
LOG.info(
|
|
112
|
-
"GPX and GPS are perfectly synchronized (all starts from %s)",
|
|
113
|
-
first_gpx_dt,
|
|
114
|
-
)
|
|
115
|
-
else:
|
|
116
|
-
LOG.warning(
|
|
117
|
-
"Skip GPX synchronization because no GPS epoch time found in video %s",
|
|
118
|
-
self.video_path,
|
|
119
|
-
)
|
|
120
|
-
|
|
121
|
-
return offset
|
|
122
|
-
|
|
123
|
-
|
|
124
21
|
class GeotagVideosFromGPX(GeotagVideosFromGeneric):
|
|
125
22
|
def __init__(
|
|
126
23
|
self,
|
|
127
|
-
video_paths: T.Sequence[Path],
|
|
128
24
|
option: options.SourcePathOption | None = None,
|
|
129
25
|
num_processes: int | None = None,
|
|
130
26
|
):
|
|
131
|
-
super().__init__(
|
|
27
|
+
super().__init__(num_processes=num_processes)
|
|
132
28
|
if option is None:
|
|
133
29
|
option = options.SourcePathOption(pattern="%f.gpx")
|
|
134
30
|
self.option = option
|
|
135
31
|
|
|
136
|
-
|
|
32
|
+
@override
|
|
33
|
+
def _generate_video_extractors(
|
|
34
|
+
self, video_paths: T.Sequence[Path]
|
|
35
|
+
) -> T.Sequence[GPXVideoExtractor]:
|
|
137
36
|
return [
|
|
138
37
|
GPXVideoExtractor(video_path, self.option.resolve(video_path))
|
|
139
|
-
for video_path in
|
|
38
|
+
for video_path in video_paths
|
|
140
39
|
]
|
|
@@ -1,165 +1,32 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import sys
|
|
3
4
|
import typing as T
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
from
|
|
8
|
-
|
|
9
|
-
from
|
|
10
|
-
from .geotag_from_generic import GenericVideoExtractor, GeotagVideosFromGeneric
|
|
11
|
-
|
|
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)
|
|
17
|
-
|
|
18
|
-
if gopro_info is None:
|
|
19
|
-
raise exceptions.MapillaryVideoGPSNotFoundError(
|
|
20
|
-
"No GPS data found from the video"
|
|
21
|
-
)
|
|
22
|
-
|
|
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
|
-
|
|
7
|
+
if sys.version_info >= (3, 12):
|
|
8
|
+
from typing import override
|
|
9
|
+
else:
|
|
10
|
+
from typing_extensions import override
|
|
84
11
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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"
|
|
93
|
-
)
|
|
94
|
-
|
|
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")
|
|
99
|
-
)
|
|
100
|
-
return types.describe_error_metadata(
|
|
101
|
-
ex, self.video_path, filetype=FileType.BLACKVUE
|
|
102
|
-
)
|
|
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
|
-
|
|
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
|
-
)
|
|
12
|
+
from ..types import FileType
|
|
13
|
+
from .base import GeotagVideosFromGeneric
|
|
14
|
+
from .video_extractors.native import NativeVideoExtractor
|
|
149
15
|
|
|
150
16
|
|
|
151
17
|
class GeotagVideosFromVideo(GeotagVideosFromGeneric):
|
|
152
18
|
def __init__(
|
|
153
19
|
self,
|
|
154
|
-
video_paths: T.Sequence[Path],
|
|
155
20
|
filetypes: set[FileType] | None = None,
|
|
156
21
|
num_processes: int | None = None,
|
|
157
22
|
):
|
|
158
|
-
super().__init__(
|
|
23
|
+
super().__init__(num_processes=num_processes)
|
|
159
24
|
self.filetypes = filetypes
|
|
160
25
|
|
|
161
|
-
|
|
26
|
+
@override
|
|
27
|
+
def _generate_video_extractors(
|
|
28
|
+
self, video_paths: T.Sequence[Path]
|
|
29
|
+
) -> T.Sequence[NativeVideoExtractor]:
|
|
162
30
|
return [
|
|
163
|
-
NativeVideoExtractor(path, filetypes=self.filetypes)
|
|
164
|
-
for path in self.video_paths
|
|
31
|
+
NativeVideoExtractor(path, filetypes=self.filetypes) for path in video_paths
|
|
165
32
|
]
|