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,145 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
import typing as T
|
|
3
|
-
import xml.etree.ElementTree as ET
|
|
4
|
-
from multiprocessing import Pool
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
|
|
7
|
-
from tqdm import tqdm
|
|
8
|
-
|
|
9
|
-
from .. import exceptions, exiftool_read, geo, types, utils
|
|
10
|
-
from ..exiftool_read_video import ExifToolReadVideo
|
|
11
|
-
from ..telemetry import GPSPoint
|
|
12
|
-
from . import gpmf_gps_filter, utils as video_utils
|
|
13
|
-
from .geotag_from_generic import GeotagVideosFromGeneric
|
|
14
|
-
|
|
15
|
-
LOG = logging.getLogger(__name__)
|
|
16
|
-
_DESCRIPTION_TAG = "rdf:Description"
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class GeotagVideosFromExifToolVideo(GeotagVideosFromGeneric):
|
|
20
|
-
def __init__(
|
|
21
|
-
self,
|
|
22
|
-
video_paths: T.Sequence[Path],
|
|
23
|
-
xml_path: Path,
|
|
24
|
-
num_processes: T.Optional[int] = None,
|
|
25
|
-
):
|
|
26
|
-
self.video_paths = video_paths
|
|
27
|
-
self.xml_path = xml_path
|
|
28
|
-
self.num_processes = num_processes
|
|
29
|
-
super().__init__()
|
|
30
|
-
|
|
31
|
-
@staticmethod
|
|
32
|
-
def geotag_video(element: ET.Element) -> types.VideoMetadataOrError:
|
|
33
|
-
video_path = exiftool_read.find_rdf_description_path(element)
|
|
34
|
-
assert video_path is not None, "must find the path from the element"
|
|
35
|
-
|
|
36
|
-
try:
|
|
37
|
-
exif = ExifToolReadVideo(ET.ElementTree(element))
|
|
38
|
-
|
|
39
|
-
points = exif.extract_gps_track()
|
|
40
|
-
|
|
41
|
-
if not points:
|
|
42
|
-
raise exceptions.MapillaryVideoGPSNotFoundError(
|
|
43
|
-
"No GPS data found from the video"
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
points = geo.extend_deduplicate_points(points)
|
|
47
|
-
assert points, "must have at least one point"
|
|
48
|
-
|
|
49
|
-
if all(isinstance(p, GPSPoint) for p in points):
|
|
50
|
-
points = T.cast(
|
|
51
|
-
T.List[geo.Point],
|
|
52
|
-
gpmf_gps_filter.remove_noisy_points(
|
|
53
|
-
T.cast(T.List[GPSPoint], points)
|
|
54
|
-
),
|
|
55
|
-
)
|
|
56
|
-
if not points:
|
|
57
|
-
raise exceptions.MapillaryGPSNoiseError("GPS is too noisy")
|
|
58
|
-
|
|
59
|
-
stationary = video_utils.is_video_stationary(
|
|
60
|
-
geo.get_max_distance_from_start([(p.lat, p.lon) for p in points])
|
|
61
|
-
)
|
|
62
|
-
|
|
63
|
-
if stationary:
|
|
64
|
-
raise exceptions.MapillaryStationaryVideoError("Stationary video")
|
|
65
|
-
|
|
66
|
-
video_metadata = types.VideoMetadata(
|
|
67
|
-
video_path,
|
|
68
|
-
md5sum=None,
|
|
69
|
-
filesize=utils.get_file_size(video_path),
|
|
70
|
-
filetype=types.FileType.VIDEO,
|
|
71
|
-
points=points,
|
|
72
|
-
make=exif.extract_make(),
|
|
73
|
-
model=exif.extract_model(),
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
-
LOG.debug("Calculating MD5 checksum for %s", str(video_metadata.filename))
|
|
77
|
-
|
|
78
|
-
video_metadata.update_md5sum()
|
|
79
|
-
|
|
80
|
-
except Exception as ex:
|
|
81
|
-
if not isinstance(ex, exceptions.MapillaryDescriptionError):
|
|
82
|
-
LOG.warning(
|
|
83
|
-
"Failed to geotag video %s: %s",
|
|
84
|
-
video_path,
|
|
85
|
-
str(ex),
|
|
86
|
-
exc_info=LOG.getEffectiveLevel() <= logging.DEBUG,
|
|
87
|
-
)
|
|
88
|
-
return types.describe_error_metadata(
|
|
89
|
-
ex, video_path, filetype=types.FileType.VIDEO
|
|
90
|
-
)
|
|
91
|
-
|
|
92
|
-
return video_metadata
|
|
93
|
-
|
|
94
|
-
def to_description(self) -> T.List[types.VideoMetadataOrError]:
|
|
95
|
-
rdf_description_by_path = exiftool_read.index_rdf_description_by_path(
|
|
96
|
-
[self.xml_path]
|
|
97
|
-
)
|
|
98
|
-
|
|
99
|
-
error_metadatas: T.List[types.ErrorMetadata] = []
|
|
100
|
-
rdf_descriptions: T.List[ET.Element] = []
|
|
101
|
-
for path in self.video_paths:
|
|
102
|
-
rdf_description = rdf_description_by_path.get(
|
|
103
|
-
exiftool_read.canonical_path(path)
|
|
104
|
-
)
|
|
105
|
-
if rdf_description is None:
|
|
106
|
-
exc = exceptions.MapillaryEXIFNotFoundError(
|
|
107
|
-
f"The {_DESCRIPTION_TAG} XML element for the video not found"
|
|
108
|
-
)
|
|
109
|
-
error_metadatas.append(
|
|
110
|
-
types.describe_error_metadata(
|
|
111
|
-
exc, path, filetype=types.FileType.VIDEO
|
|
112
|
-
)
|
|
113
|
-
)
|
|
114
|
-
else:
|
|
115
|
-
rdf_descriptions.append(rdf_description)
|
|
116
|
-
|
|
117
|
-
if self.num_processes is None:
|
|
118
|
-
num_processes = self.num_processes
|
|
119
|
-
disable_multiprocessing = False
|
|
120
|
-
else:
|
|
121
|
-
num_processes = max(self.num_processes, 1)
|
|
122
|
-
disable_multiprocessing = self.num_processes <= 0
|
|
123
|
-
|
|
124
|
-
with Pool(processes=num_processes) as pool:
|
|
125
|
-
video_metadatas_iter: T.Iterator[types.VideoMetadataOrError]
|
|
126
|
-
if disable_multiprocessing:
|
|
127
|
-
video_metadatas_iter = map(
|
|
128
|
-
GeotagVideosFromExifToolVideo.geotag_video, rdf_descriptions
|
|
129
|
-
)
|
|
130
|
-
else:
|
|
131
|
-
video_metadatas_iter = pool.imap(
|
|
132
|
-
GeotagVideosFromExifToolVideo.geotag_video,
|
|
133
|
-
rdf_descriptions,
|
|
134
|
-
)
|
|
135
|
-
video_metadata_or_errors = list(
|
|
136
|
-
tqdm(
|
|
137
|
-
video_metadatas_iter,
|
|
138
|
-
desc="Extracting GPS tracks from ExifTool XML",
|
|
139
|
-
unit="videos",
|
|
140
|
-
disable=LOG.getEffectiveLevel() <= logging.DEBUG,
|
|
141
|
-
total=len(self.video_paths),
|
|
142
|
-
)
|
|
143
|
-
)
|
|
144
|
-
|
|
145
|
-
return error_metadatas + video_metadata_or_errors
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import typing as T
|
|
2
|
-
from pathlib import Path
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
known_parser_options = ["source", "pattern", "exiftool_path"]
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class CliParserOptions(T.TypedDict, total=False):
|
|
9
|
-
source: str
|
|
10
|
-
pattern: T.Optional[str]
|
|
11
|
-
exiftool_path: T.Optional[Path]
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class CliOptions(T.TypedDict, total=False):
|
|
15
|
-
paths: T.Sequence[Path]
|
|
16
|
-
recursive: bool
|
|
17
|
-
geotag_sources_options: T.Sequence[CliParserOptions]
|
|
18
|
-
geotag_source_path: Path
|
|
19
|
-
exiftool_path: Path
|
|
20
|
-
num_processes: int
|
|
21
|
-
device_make: T.Optional[str]
|
|
22
|
-
device_model: T.Optional[str]
|
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
import typing as T
|
|
3
|
-
from multiprocessing import Pool
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
|
|
6
|
-
import tqdm
|
|
7
|
-
|
|
8
|
-
from .. import exceptions, geo, utils
|
|
9
|
-
from ..geotag import gpmf_gps_filter, utils as video_utils
|
|
10
|
-
from ..telemetry import GPSPoint
|
|
11
|
-
from ..types import (
|
|
12
|
-
ErrorMetadata,
|
|
13
|
-
FileType,
|
|
14
|
-
MetadataOrError,
|
|
15
|
-
VideoMetadata,
|
|
16
|
-
VideoMetadataOrError,
|
|
17
|
-
)
|
|
18
|
-
from . import video_data_parser_factory
|
|
19
|
-
from .cli_options import CliOptions
|
|
20
|
-
from .extractors.base_parser import BaseParser
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
LOG = logging.getLogger(__name__)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
class VideoDataExtractor:
|
|
27
|
-
options: CliOptions
|
|
28
|
-
|
|
29
|
-
def __init__(self, options: CliOptions) -> None:
|
|
30
|
-
self.options = options
|
|
31
|
-
|
|
32
|
-
def process(self) -> T.List[MetadataOrError]:
|
|
33
|
-
paths = self.options["paths"]
|
|
34
|
-
self._check_paths(paths)
|
|
35
|
-
video_files = utils.find_videos(paths)
|
|
36
|
-
self._check_sources_cardinality(video_files)
|
|
37
|
-
|
|
38
|
-
num_processes = self.options["num_processes"] or None
|
|
39
|
-
with Pool(processes=num_processes) as pool:
|
|
40
|
-
if num_processes == 1:
|
|
41
|
-
iter: T.Iterator[VideoMetadataOrError] = map(
|
|
42
|
-
self.process_file, video_files
|
|
43
|
-
)
|
|
44
|
-
else:
|
|
45
|
-
iter = pool.imap(self.process_file, video_files)
|
|
46
|
-
|
|
47
|
-
video_metadata_or_errors = list(
|
|
48
|
-
tqdm.tqdm(
|
|
49
|
-
iter,
|
|
50
|
-
desc="Extracting GPS tracks",
|
|
51
|
-
unit="videos",
|
|
52
|
-
disable=LOG.getEffectiveLevel() <= logging.DEBUG,
|
|
53
|
-
total=len(video_files),
|
|
54
|
-
)
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
return video_metadata_or_errors
|
|
58
|
-
|
|
59
|
-
def process_file(self, file: Path) -> VideoMetadataOrError:
|
|
60
|
-
parsers = video_data_parser_factory.make_parsers(file, self.options)
|
|
61
|
-
points: T.Sequence[geo.Point] = []
|
|
62
|
-
make = self.options["device_make"]
|
|
63
|
-
model = self.options["device_model"]
|
|
64
|
-
|
|
65
|
-
ex: T.Optional[Exception]
|
|
66
|
-
for parser in parsers:
|
|
67
|
-
log_vars = {
|
|
68
|
-
"filename": file,
|
|
69
|
-
"parser": parser.parser_label,
|
|
70
|
-
"source": parser.geotag_source_path,
|
|
71
|
-
}
|
|
72
|
-
try:
|
|
73
|
-
if not points:
|
|
74
|
-
points = self._extract_points(parser, log_vars)
|
|
75
|
-
if not model:
|
|
76
|
-
model = parser.extract_model()
|
|
77
|
-
if not make:
|
|
78
|
-
make = parser.extract_make()
|
|
79
|
-
except Exception as e:
|
|
80
|
-
ex = e
|
|
81
|
-
LOG.warning(
|
|
82
|
-
'%(filename)s: Exception for parser %(parser)s while processing source %(source)s: "%(e)s"',
|
|
83
|
-
{**log_vars, "e": e},
|
|
84
|
-
)
|
|
85
|
-
|
|
86
|
-
# After trying all parsers, return the points if we found any, otherwise
|
|
87
|
-
# the last exception thrown or a default one.
|
|
88
|
-
# Note that if we have points, we return them, regardless of exceptions
|
|
89
|
-
# with make or model.
|
|
90
|
-
if points:
|
|
91
|
-
video_metadata = VideoMetadata(
|
|
92
|
-
filename=file,
|
|
93
|
-
filetype=FileType.VIDEO,
|
|
94
|
-
md5sum=None,
|
|
95
|
-
filesize=utils.get_file_size(file),
|
|
96
|
-
points=points,
|
|
97
|
-
make=make,
|
|
98
|
-
model=model,
|
|
99
|
-
)
|
|
100
|
-
video_metadata.update_md5sum()
|
|
101
|
-
return video_metadata
|
|
102
|
-
else:
|
|
103
|
-
return ErrorMetadata(
|
|
104
|
-
filename=file,
|
|
105
|
-
error=(
|
|
106
|
-
ex
|
|
107
|
-
if ex
|
|
108
|
-
else exceptions.MapillaryVideoGPSNotFoundError(
|
|
109
|
-
"No GPS data found from the video"
|
|
110
|
-
)
|
|
111
|
-
),
|
|
112
|
-
filetype=FileType.VIDEO,
|
|
113
|
-
)
|
|
114
|
-
|
|
115
|
-
def _extract_points(
|
|
116
|
-
self, parser: BaseParser, log_vars: T.Dict
|
|
117
|
-
) -> T.Sequence[geo.Point]:
|
|
118
|
-
points = parser.extract_points()
|
|
119
|
-
if points:
|
|
120
|
-
LOG.debug(
|
|
121
|
-
"%(filename)s: %(points)d points extracted by parser %(parser)s from file %(source)s}",
|
|
122
|
-
{**log_vars, "points": len(points)},
|
|
123
|
-
)
|
|
124
|
-
|
|
125
|
-
return self._sanitize_points(points)
|
|
126
|
-
|
|
127
|
-
@staticmethod
|
|
128
|
-
def _check_paths(import_paths: T.Sequence[Path]):
|
|
129
|
-
for path in import_paths:
|
|
130
|
-
if not path.is_file() and not path.is_dir():
|
|
131
|
-
raise exceptions.MapillaryFileNotFoundError(
|
|
132
|
-
f"Import file or directory not found: {path}"
|
|
133
|
-
)
|
|
134
|
-
|
|
135
|
-
def _check_sources_cardinality(self, files: T.Sequence[Path]):
|
|
136
|
-
if len(files) > 1:
|
|
137
|
-
for parser_opts in self.options["geotag_sources_options"]:
|
|
138
|
-
pattern = parser_opts.get("pattern")
|
|
139
|
-
if pattern and "%" not in pattern:
|
|
140
|
-
raise exceptions.MapillaryUserError(
|
|
141
|
-
"Multiple video files found: Geotag source pattern for source %s must include filename placeholders",
|
|
142
|
-
parser_opts["source"],
|
|
143
|
-
)
|
|
144
|
-
|
|
145
|
-
@staticmethod
|
|
146
|
-
def _sanitize_points(points: T.Sequence[geo.Point]) -> T.Sequence[geo.Point]:
|
|
147
|
-
"""
|
|
148
|
-
Deduplicates points, when possible removes noisy ones, and checks
|
|
149
|
-
against stationary videos
|
|
150
|
-
"""
|
|
151
|
-
|
|
152
|
-
if not points:
|
|
153
|
-
raise exceptions.MapillaryVideoGPSNotFoundError(
|
|
154
|
-
"No GPS data found in the given sources"
|
|
155
|
-
)
|
|
156
|
-
|
|
157
|
-
points = geo.extend_deduplicate_points(points)
|
|
158
|
-
|
|
159
|
-
if all(isinstance(p, GPSPoint) for p in points):
|
|
160
|
-
points = T.cast(
|
|
161
|
-
T.Sequence[geo.Point],
|
|
162
|
-
gpmf_gps_filter.remove_noisy_points(
|
|
163
|
-
T.cast(T.Sequence[GPSPoint], points)
|
|
164
|
-
),
|
|
165
|
-
)
|
|
166
|
-
if not points:
|
|
167
|
-
raise exceptions.MapillaryGPSNoiseError("GPS is too noisy")
|
|
168
|
-
|
|
169
|
-
stationary = video_utils.is_video_stationary(
|
|
170
|
-
geo.get_max_distance_from_start([(p.lat, p.lon) for p in points])
|
|
171
|
-
)
|
|
172
|
-
|
|
173
|
-
if stationary:
|
|
174
|
-
raise exceptions.MapillaryStationaryVideoError("Stationary video")
|
|
175
|
-
|
|
176
|
-
return points
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import abc
|
|
2
|
-
import functools
|
|
3
|
-
import logging
|
|
4
|
-
import os
|
|
5
|
-
import typing as T
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
|
|
8
|
-
from ... import geo
|
|
9
|
-
from ..cli_options import CliOptions, CliParserOptions
|
|
10
|
-
|
|
11
|
-
LOG = logging.getLogger(__name__)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class BaseParser(metaclass=abc.ABCMeta):
|
|
15
|
-
videoPath: Path
|
|
16
|
-
options: CliOptions
|
|
17
|
-
parserOptions: CliParserOptions
|
|
18
|
-
|
|
19
|
-
def __init__(
|
|
20
|
-
self, video_path: Path, options: CliOptions, parser_options: CliParserOptions
|
|
21
|
-
) -> None:
|
|
22
|
-
self.videoPath = video_path
|
|
23
|
-
self.options = options
|
|
24
|
-
self.parserOptions = parser_options
|
|
25
|
-
|
|
26
|
-
@property
|
|
27
|
-
@abc.abstractmethod
|
|
28
|
-
def default_source_pattern(self) -> str:
|
|
29
|
-
raise NotImplementedError
|
|
30
|
-
|
|
31
|
-
@property
|
|
32
|
-
@abc.abstractmethod
|
|
33
|
-
def parser_label(self) -> str:
|
|
34
|
-
raise NotImplementedError
|
|
35
|
-
|
|
36
|
-
@abc.abstractmethod
|
|
37
|
-
def extract_points(self) -> T.Sequence[geo.Point]:
|
|
38
|
-
raise NotImplementedError
|
|
39
|
-
|
|
40
|
-
@abc.abstractmethod
|
|
41
|
-
def extract_make(self) -> T.Optional[str]:
|
|
42
|
-
raise NotImplementedError
|
|
43
|
-
|
|
44
|
-
@abc.abstractmethod
|
|
45
|
-
def extract_model(self) -> T.Optional[str]:
|
|
46
|
-
raise NotImplementedError
|
|
47
|
-
|
|
48
|
-
@functools.cached_property
|
|
49
|
-
def geotag_source_path(self) -> T.Optional[Path]:
|
|
50
|
-
video_dir = self.videoPath.parent.resolve()
|
|
51
|
-
video_filename = self.videoPath.name
|
|
52
|
-
video_basename, video_ext = os.path.splitext(video_filename)
|
|
53
|
-
pattern = self.parserOptions.get("pattern") or self.default_source_pattern
|
|
54
|
-
|
|
55
|
-
replaced = Path(
|
|
56
|
-
pattern.replace("%f", video_filename)
|
|
57
|
-
.replace("%g", video_basename)
|
|
58
|
-
.replace("%e", video_ext)
|
|
59
|
-
)
|
|
60
|
-
abs_path = (
|
|
61
|
-
replaced if replaced.is_absolute() else Path.joinpath(video_dir, replaced)
|
|
62
|
-
).resolve()
|
|
63
|
-
|
|
64
|
-
return abs_path if abs_path.is_file() else None
|
|
65
|
-
|
|
66
|
-
@staticmethod
|
|
67
|
-
def _rebase_times(points: T.Sequence[geo.Point], offset: float = 0.0):
|
|
68
|
-
"""
|
|
69
|
-
Make point times start from 0
|
|
70
|
-
"""
|
|
71
|
-
if points:
|
|
72
|
-
first_timestamp = points[0].time
|
|
73
|
-
for p in points:
|
|
74
|
-
p.time = (p.time - first_timestamp) + offset
|
|
75
|
-
return points
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import typing as T
|
|
2
|
-
|
|
3
|
-
from ... import geo
|
|
4
|
-
from ...geotag import blackvue_parser
|
|
5
|
-
from ...mp4 import simple_mp4_parser as sparser
|
|
6
|
-
from .base_parser import BaseParser
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class BlackVueParser(BaseParser):
|
|
10
|
-
default_source_pattern = "%f"
|
|
11
|
-
must_rebase_times_to_zero = False
|
|
12
|
-
parser_label = "blackvue"
|
|
13
|
-
|
|
14
|
-
pointsFound: bool = False
|
|
15
|
-
|
|
16
|
-
def extract_points(self) -> T.Sequence[geo.Point]:
|
|
17
|
-
source_path = self.geotag_source_path
|
|
18
|
-
if not source_path:
|
|
19
|
-
return []
|
|
20
|
-
with source_path.open("rb") as fp:
|
|
21
|
-
try:
|
|
22
|
-
points = blackvue_parser.extract_points(fp) or []
|
|
23
|
-
self.pointsFound = len(points) > 0
|
|
24
|
-
return points
|
|
25
|
-
except sparser.ParsingError:
|
|
26
|
-
return []
|
|
27
|
-
|
|
28
|
-
def extract_make(self) -> T.Optional[str]:
|
|
29
|
-
# If no points were found, assume this is not a BlackVue
|
|
30
|
-
return "Blackvue" if self.pointsFound else None
|
|
31
|
-
|
|
32
|
-
def extract_model(self) -> T.Optional[str]:
|
|
33
|
-
with self.videoPath.open("rb") as fp:
|
|
34
|
-
return blackvue_parser.extract_camera_model(fp) or None
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import functools
|
|
2
|
-
import typing as T
|
|
3
|
-
|
|
4
|
-
from ... import geo
|
|
5
|
-
from ...camm import camm_parser
|
|
6
|
-
from ...mp4 import simple_mp4_parser as sparser
|
|
7
|
-
from .base_parser import BaseParser
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class CammParser(BaseParser):
|
|
11
|
-
default_source_pattern = "%f"
|
|
12
|
-
must_rebase_times_to_zero = False
|
|
13
|
-
parser_label = "camm"
|
|
14
|
-
|
|
15
|
-
@functools.cached_property
|
|
16
|
-
def _camera_info(self) -> T.Tuple[str, str]:
|
|
17
|
-
source_path = self.geotag_source_path
|
|
18
|
-
if not source_path:
|
|
19
|
-
return "", ""
|
|
20
|
-
|
|
21
|
-
with source_path.open("rb") as fp:
|
|
22
|
-
return camm_parser.extract_camera_make_and_model(fp)
|
|
23
|
-
|
|
24
|
-
def extract_points(self) -> T.Sequence[geo.Point]:
|
|
25
|
-
source_path = self.geotag_source_path
|
|
26
|
-
if not source_path:
|
|
27
|
-
return []
|
|
28
|
-
with source_path.open("rb") as fp:
|
|
29
|
-
try:
|
|
30
|
-
return camm_parser.extract_points(fp) or []
|
|
31
|
-
except sparser.ParsingError:
|
|
32
|
-
return []
|
|
33
|
-
|
|
34
|
-
def extract_make(self) -> T.Optional[str]:
|
|
35
|
-
return self._camera_info[0] or None
|
|
36
|
-
|
|
37
|
-
def extract_model(self) -> T.Optional[str]:
|
|
38
|
-
return self._camera_info[1] or None
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import shutil
|
|
2
|
-
import subprocess
|
|
3
|
-
import typing as T
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
|
|
6
|
-
from ... import constants, exceptions, geo
|
|
7
|
-
from ..cli_options import CliOptions, CliParserOptions
|
|
8
|
-
from .base_parser import BaseParser
|
|
9
|
-
from .exiftool_xml_parser import ExiftoolXmlParser
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class ExiftoolRuntimeParser(BaseParser):
|
|
13
|
-
"""
|
|
14
|
-
Wrapper around ExiftoolRdfParser that executes exiftool
|
|
15
|
-
"""
|
|
16
|
-
|
|
17
|
-
exiftoolXmlParser: ExiftoolXmlParser
|
|
18
|
-
|
|
19
|
-
default_source_pattern = "%f"
|
|
20
|
-
must_rebase_times_to_zero = True
|
|
21
|
-
parser_label = "exiftool_runtime"
|
|
22
|
-
|
|
23
|
-
def __init__(
|
|
24
|
-
self, video_path: Path, options: CliOptions, parser_options: CliParserOptions
|
|
25
|
-
):
|
|
26
|
-
super().__init__(video_path, options, parser_options)
|
|
27
|
-
exiftool_path = shutil.which(constants.EXIFTOOL_PATH)
|
|
28
|
-
|
|
29
|
-
if not exiftool_path:
|
|
30
|
-
raise exceptions.MapillaryExiftoolNotFoundError(
|
|
31
|
-
"Cannot execute exiftool. Please install it from https://exiftool.org/ or you package manager, or set the environment variable MAPILLARY_TOOLS_EXIFTOOL_PATH"
|
|
32
|
-
)
|
|
33
|
-
if not self.geotag_source_path:
|
|
34
|
-
return
|
|
35
|
-
|
|
36
|
-
# To handle non-latin1 filenames under Windows, we pass the path
|
|
37
|
-
# via stdin. See https://exiftool.org/faq.html#Q18
|
|
38
|
-
stdin = str(self.geotag_source_path)
|
|
39
|
-
args = [
|
|
40
|
-
exiftool_path,
|
|
41
|
-
"-q",
|
|
42
|
-
"-r",
|
|
43
|
-
"-n",
|
|
44
|
-
"-ee",
|
|
45
|
-
"-api",
|
|
46
|
-
"LargeFileSupport=1",
|
|
47
|
-
"-X",
|
|
48
|
-
"-charset",
|
|
49
|
-
"filename=utf8",
|
|
50
|
-
"-@",
|
|
51
|
-
"-",
|
|
52
|
-
]
|
|
53
|
-
|
|
54
|
-
process = subprocess.run(
|
|
55
|
-
args, capture_output=True, text=True, input=stdin, encoding="utf-8"
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
self.exiftoolXmlParser = ExiftoolXmlParser(
|
|
59
|
-
video_path, options, parser_options, process.stdout
|
|
60
|
-
)
|
|
61
|
-
|
|
62
|
-
def extract_points(self) -> T.Sequence[geo.Point]:
|
|
63
|
-
return self.exiftoolXmlParser.extract_points() if self.exiftoolXmlParser else []
|
|
64
|
-
|
|
65
|
-
def extract_make(self) -> T.Optional[str]:
|
|
66
|
-
return self.exiftoolXmlParser.extract_make() if self.exiftoolXmlParser else None
|
|
67
|
-
|
|
68
|
-
def extract_model(self) -> T.Optional[str]:
|
|
69
|
-
return (
|
|
70
|
-
self.exiftoolXmlParser.extract_model() if self.exiftoolXmlParser else None
|
|
71
|
-
)
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import typing as T
|
|
2
|
-
import xml.etree.ElementTree as ET
|
|
3
|
-
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
|
|
6
|
-
from ... import geo
|
|
7
|
-
from ...exiftool_read import EXIFTOOL_NAMESPACES
|
|
8
|
-
from ...exiftool_read_video import ExifToolReadVideo
|
|
9
|
-
from ...geotag.geotag_videos_from_exiftool_video import _DESCRIPTION_TAG
|
|
10
|
-
from ..cli_options import CliOptions, CliParserOptions
|
|
11
|
-
from .base_parser import BaseParser
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class ExiftoolXmlParser(BaseParser):
|
|
15
|
-
default_source_pattern = "%g.xml"
|
|
16
|
-
parser_label = "exiftool_xml"
|
|
17
|
-
|
|
18
|
-
exifToolReadVideo: T.Optional[ExifToolReadVideo] = None
|
|
19
|
-
|
|
20
|
-
def __init__(
|
|
21
|
-
self,
|
|
22
|
-
video_path: Path,
|
|
23
|
-
options: CliOptions,
|
|
24
|
-
parser_options: CliParserOptions,
|
|
25
|
-
xml_content: T.Optional[str] = None,
|
|
26
|
-
) -> None:
|
|
27
|
-
super().__init__(video_path, options, parser_options)
|
|
28
|
-
|
|
29
|
-
if xml_content:
|
|
30
|
-
etree = ET.fromstring(xml_content)
|
|
31
|
-
else:
|
|
32
|
-
xml_path = self.geotag_source_path
|
|
33
|
-
if not xml_path:
|
|
34
|
-
return
|
|
35
|
-
etree = ET.parse(xml_path).getroot()
|
|
36
|
-
|
|
37
|
-
element = next(etree.iterfind(_DESCRIPTION_TAG, namespaces=EXIFTOOL_NAMESPACES))
|
|
38
|
-
self.exifToolReadVideo = ExifToolReadVideo(ET.ElementTree(element))
|
|
39
|
-
|
|
40
|
-
def extract_points(self) -> T.Sequence[geo.Point]:
|
|
41
|
-
gps_points = (
|
|
42
|
-
self.exifToolReadVideo.extract_gps_track() if self.exifToolReadVideo else []
|
|
43
|
-
)
|
|
44
|
-
self._rebase_times(gps_points)
|
|
45
|
-
return gps_points
|
|
46
|
-
|
|
47
|
-
def extract_make(self) -> T.Optional[str]:
|
|
48
|
-
return self.exifToolReadVideo.extract_make() if self.exifToolReadVideo else None
|
|
49
|
-
|
|
50
|
-
def extract_model(self) -> T.Optional[str]:
|
|
51
|
-
return (
|
|
52
|
-
self.exifToolReadVideo.extract_model() if self.exifToolReadVideo else None
|
|
53
|
-
)
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import typing as T
|
|
2
|
-
from pathlib import Path
|
|
3
|
-
|
|
4
|
-
from ... import geo
|
|
5
|
-
from ..cli_options import CliOptions, CliParserOptions
|
|
6
|
-
from .base_parser import BaseParser
|
|
7
|
-
from .blackvue_parser import BlackVueParser
|
|
8
|
-
from .camm_parser import CammParser
|
|
9
|
-
from .gopro_parser import GoProParser
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class GenericVideoParser(BaseParser):
|
|
13
|
-
"""
|
|
14
|
-
Wrapper around the three native video parsers. It will try to execute them
|
|
15
|
-
in the order camm-gopro-blackvue, like the previous implementation
|
|
16
|
-
"""
|
|
17
|
-
|
|
18
|
-
parsers: T.Sequence[BaseParser] = []
|
|
19
|
-
|
|
20
|
-
default_source_pattern = "%f"
|
|
21
|
-
must_rebase_times_to_zero = False
|
|
22
|
-
parser_label = "video"
|
|
23
|
-
|
|
24
|
-
def __init__(
|
|
25
|
-
self, video_path: Path, options: CliOptions, parser_options: CliParserOptions
|
|
26
|
-
) -> None:
|
|
27
|
-
super().__init__(video_path, options, parser_options)
|
|
28
|
-
camm_parser = CammParser(video_path, options, parser_options)
|
|
29
|
-
gopro_parser = GoProParser(video_path, options, parser_options)
|
|
30
|
-
blackvue_parser = BlackVueParser(video_path, options, parser_options)
|
|
31
|
-
self.parsers = [camm_parser, gopro_parser, blackvue_parser]
|
|
32
|
-
|
|
33
|
-
def extract_points(self) -> T.Sequence[geo.Point]:
|
|
34
|
-
for parser in self.parsers:
|
|
35
|
-
points = parser.extract_points()
|
|
36
|
-
if points:
|
|
37
|
-
return points
|
|
38
|
-
return []
|
|
39
|
-
|
|
40
|
-
def extract_make(self) -> T.Optional[str]:
|
|
41
|
-
for parser in self.parsers:
|
|
42
|
-
make = parser.extract_make()
|
|
43
|
-
if make:
|
|
44
|
-
return make
|
|
45
|
-
return None
|
|
46
|
-
|
|
47
|
-
def extract_model(self) -> T.Optional[str]:
|
|
48
|
-
for parser in self.parsers:
|
|
49
|
-
model = parser.extract_model()
|
|
50
|
-
if model:
|
|
51
|
-
return model
|
|
52
|
-
return None
|