mapillary-tools 0.14.0a1__tar.gz → 0.14.0a2__tar.gz
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-0.14.0a1/mapillary_tools.egg-info → mapillary_tools-0.14.0a2}/PKG-INFO +1 -1
- mapillary_tools-0.14.0a2/mapillary_tools/__init__.py +1 -0
- {mapillary_tools-0.14.0a1 → mapillary_tools-0.14.0a2}/mapillary_tools/api_v4.py +4 -4
- {mapillary_tools-0.14.0a1 → mapillary_tools-0.14.0a2}/mapillary_tools/camm/camm_parser.py +5 -5
- {mapillary_tools-0.14.0a1 → mapillary_tools-0.14.0a2}/mapillary_tools/commands/__main__.py +1 -2
- {mapillary_tools-0.14.0a1 → mapillary_tools-0.14.0a2}/mapillary_tools/config.py +7 -5
- {mapillary_tools-0.14.0a1 → mapillary_tools-0.14.0a2}/mapillary_tools/constants.py +1 -2
- {mapillary_tools-0.14.0a1 → mapillary_tools-0.14.0a2}/mapillary_tools/exceptions.py +1 -1
- {mapillary_tools-0.14.0a1 → mapillary_tools-0.14.0a2}/mapillary_tools/exif_read.py +65 -65
- {mapillary_tools-0.14.0a1 → mapillary_tools-0.14.0a2}/mapillary_tools/exif_write.py +7 -7
- {mapillary_tools-0.14.0a1 → mapillary_tools-0.14.0a2}/mapillary_tools/exiftool_read.py +23 -46
- {mapillary_tools-0.14.0a1 → mapillary_tools-0.14.0a2}/mapillary_tools/exiftool_read_video.py +36 -34
- {mapillary_tools-0.14.0a1 → mapillary_tools-0.14.0a2}/mapillary_tools/ffmpeg.py +24 -23
- {mapillary_tools-0.14.0a1 → mapillary_tools-0.14.0a2}/mapillary_tools/geo.py +4 -21
- mapillary_tools-0.14.0a1/mapillary_tools/geotag/geotag_from_generic.py → mapillary_tools-0.14.0a2/mapillary_tools/geotag/base.py +32 -48
- {mapillary_tools-0.14.0a1 → mapillary_tools-0.14.0a2}/mapillary_tools/geotag/factory.py +27 -34
- mapillary_tools-0.14.0a2/mapillary_tools/geotag/geotag_images_from_exif.py +24 -0
- mapillary_tools-0.14.0a2/mapillary_tools/geotag/geotag_images_from_exiftool.py +153 -0
- {mapillary_tools-0.14.0a1 → mapillary_tools-0.14.0a2}/mapillary_tools/geotag/geotag_images_from_gpx.py +20 -10
- {mapillary_tools-0.14.0a1 → mapillary_tools-0.14.0a2}/mapillary_tools/geotag/geotag_images_from_gpx_file.py +2 -34
- {mapillary_tools-0.14.0a1 → mapillary_tools-0.14.0a2}/mapillary_tools/geotag/geotag_images_from_nmea_file.py +0 -3
- {mapillary_tools-0.14.0a1 → mapillary_tools-0.14.0a2}/mapillary_tools/geotag/geotag_images_from_video.py +16 -14
- mapillary_tools-0.14.0a2/mapillary_tools/geotag/geotag_videos_from_exiftool.py +97 -0
- mapillary_tools-0.14.0a2/mapillary_tools/geotag/geotag_videos_from_gpx.py +39 -0
- mapillary_tools-0.14.0a2/mapillary_tools/geotag/geotag_videos_from_video.py +32 -0
- mapillary_tools-0.14.0a2/mapillary_tools/geotag/image_extractors/base.py +18 -0
- mapillary_tools-0.14.0a1/mapillary_tools/geotag/geotag_images_from_exif.py → mapillary_tools-0.14.0a2/mapillary_tools/geotag/image_extractors/exif.py +13 -13
- mapillary_tools-0.14.0a2/mapillary_tools/geotag/image_extractors/exiftool.py +18 -0
- {mapillary_tools-0.14.0a1 → mapillary_tools-0.14.0a2}/mapillary_tools/geotag/options.py +1 -0
- mapillary_tools-0.14.0a2/mapillary_tools/geotag/utils.py +62 -0
- mapillary_tools-0.14.0a2/mapillary_tools/geotag/video_extractors/base.py +18 -0
- mapillary_tools-0.14.0a2/mapillary_tools/geotag/video_extractors/exiftool.py +70 -0
- mapillary_tools-0.14.0a1/mapillary_tools/geotag/geotag_videos_from_gpx.py → mapillary_tools-0.14.0a2/mapillary_tools/geotag/video_extractors/gpx.py +12 -26
- mapillary_tools-0.14.0a1/mapillary_tools/geotag/geotag_videos_from_video.py → mapillary_tools-0.14.0a2/mapillary_tools/geotag/video_extractors/native.py +30 -38
- {mapillary_tools-0.14.0a1 → mapillary_tools-0.14.0a2}/mapillary_tools/gpmf/gpmf_parser.py +16 -16
- {mapillary_tools-0.14.0a1 → mapillary_tools-0.14.0a2}/mapillary_tools/gpmf/gps_filter.py +5 -3
- {mapillary_tools-0.14.0a1 → mapillary_tools-0.14.0a2}/mapillary_tools/history.py +4 -2
- {mapillary_tools-0.14.0a1 → mapillary_tools-0.14.0a2}/mapillary_tools/mp4/construct_mp4_parser.py +9 -8
- {mapillary_tools-0.14.0a1 → mapillary_tools-0.14.0a2}/mapillary_tools/mp4/mp4_sample_parser.py +27 -27
- {mapillary_tools-0.14.0a1 → mapillary_tools-0.14.0a2}/mapillary_tools/mp4/simple_mp4_builder.py +10 -9
- {mapillary_tools-0.14.0a1 → mapillary_tools-0.14.0a2}/mapillary_tools/mp4/simple_mp4_parser.py +13 -12
- {mapillary_tools-0.14.0a1 → mapillary_tools-0.14.0a2}/mapillary_tools/process_geotag_properties.py +5 -7
- {mapillary_tools-0.14.0a1 → mapillary_tools-0.14.0a2}/mapillary_tools/process_sequence_properties.py +40 -38
- {mapillary_tools-0.14.0a1 → mapillary_tools-0.14.0a2}/mapillary_tools/sample_video.py +8 -8
- {mapillary_tools-0.14.0a1 → mapillary_tools-0.14.0a2}/mapillary_tools/telemetry.py +6 -5
- {mapillary_tools-0.14.0a1 → mapillary_tools-0.14.0a2}/mapillary_tools/types.py +33 -38
- {mapillary_tools-0.14.0a1 → mapillary_tools-0.14.0a2}/mapillary_tools/utils.py +16 -18
- {mapillary_tools-0.14.0a1 → mapillary_tools-0.14.0a2/mapillary_tools.egg-info}/PKG-INFO +1 -1
- {mapillary_tools-0.14.0a1 → mapillary_tools-0.14.0a2}/mapillary_tools.egg-info/SOURCES.txt +10 -16
- {mapillary_tools-0.14.0a1 → mapillary_tools-0.14.0a2}/setup.py +2 -2
- mapillary_tools-0.14.0a1/mapillary_tools/__init__.py +0 -1
- mapillary_tools-0.14.0a1/mapillary_tools/geotag/__init__.py +0 -1
- mapillary_tools-0.14.0a1/mapillary_tools/geotag/geotag_images_from_exiftool.py +0 -105
- mapillary_tools-0.14.0a1/mapillary_tools/geotag/geotag_images_from_exiftool_both_image_and_video.py +0 -77
- mapillary_tools-0.14.0a1/mapillary_tools/geotag/geotag_videos_from_exiftool_video.py +0 -151
- mapillary_tools-0.14.0a1/mapillary_tools/video_data_extraction/cli_options.py +0 -22
- mapillary_tools-0.14.0a1/mapillary_tools/video_data_extraction/extract_video_data.py +0 -157
- mapillary_tools-0.14.0a1/mapillary_tools/video_data_extraction/extractors/base_parser.py +0 -75
- mapillary_tools-0.14.0a1/mapillary_tools/video_data_extraction/extractors/blackvue_parser.py +0 -49
- mapillary_tools-0.14.0a1/mapillary_tools/video_data_extraction/extractors/camm_parser.py +0 -62
- mapillary_tools-0.14.0a1/mapillary_tools/video_data_extraction/extractors/exiftool_runtime_parser.py +0 -74
- mapillary_tools-0.14.0a1/mapillary_tools/video_data_extraction/extractors/exiftool_xml_parser.py +0 -52
- mapillary_tools-0.14.0a1/mapillary_tools/video_data_extraction/extractors/generic_video_parser.py +0 -52
- mapillary_tools-0.14.0a1/mapillary_tools/video_data_extraction/extractors/gopro_parser.py +0 -58
- mapillary_tools-0.14.0a1/mapillary_tools/video_data_extraction/extractors/gpx_parser.py +0 -108
- mapillary_tools-0.14.0a1/mapillary_tools/video_data_extraction/extractors/nmea_parser.py +0 -24
- mapillary_tools-0.14.0a1/mapillary_tools/video_data_extraction/video_data_parser_factory.py +0 -39
- {mapillary_tools-0.14.0a1 → mapillary_tools-0.14.0a2}/LICENSE +0 -0
- {mapillary_tools-0.14.0a1 → mapillary_tools-0.14.0a2}/MANIFEST.in +0 -0
- {mapillary_tools-0.14.0a1 → mapillary_tools-0.14.0a2}/README.md +0 -0
- {mapillary_tools-0.14.0a1 → mapillary_tools-0.14.0a2}/mapillary_tools/authenticate.py +0 -0
- {mapillary_tools-0.14.0a1 → mapillary_tools-0.14.0a2}/mapillary_tools/blackvue_parser.py +0 -0
- {mapillary_tools-0.14.0a1 → mapillary_tools-0.14.0a2}/mapillary_tools/camm/camm_builder.py +0 -0
- {mapillary_tools-0.14.0a1 → mapillary_tools-0.14.0a2}/mapillary_tools/commands/__init__.py +0 -0
- {mapillary_tools-0.14.0a1 → mapillary_tools-0.14.0a2}/mapillary_tools/commands/authenticate.py +0 -0
- {mapillary_tools-0.14.0a1 → mapillary_tools-0.14.0a2}/mapillary_tools/commands/process.py +0 -0
- {mapillary_tools-0.14.0a1 → mapillary_tools-0.14.0a2}/mapillary_tools/commands/process_and_upload.py +0 -0
- {mapillary_tools-0.14.0a1 → mapillary_tools-0.14.0a2}/mapillary_tools/commands/sample_video.py +0 -0
- {mapillary_tools-0.14.0a1 → mapillary_tools-0.14.0a2}/mapillary_tools/commands/upload.py +0 -0
- {mapillary_tools-0.14.0a1 → mapillary_tools-0.14.0a2}/mapillary_tools/commands/video_process.py +0 -0
- {mapillary_tools-0.14.0a1 → mapillary_tools-0.14.0a2}/mapillary_tools/commands/video_process_and_upload.py +0 -0
- {mapillary_tools-0.14.0a1 → mapillary_tools-0.14.0a2}/mapillary_tools/commands/zip.py +0 -0
- {mapillary_tools-0.14.0a1 → mapillary_tools-0.14.0a2}/mapillary_tools/exiftool_runner.py +0 -0
- {mapillary_tools-0.14.0a1 → mapillary_tools-0.14.0a2}/mapillary_tools/gpmf/gpmf_gps_filter.py +0 -0
- {mapillary_tools-0.14.0a1 → mapillary_tools-0.14.0a2}/mapillary_tools/ipc.py +0 -0
- {mapillary_tools-0.14.0a1 → mapillary_tools-0.14.0a2}/mapillary_tools/mp4/__init__.py +0 -0
- {mapillary_tools-0.14.0a1 → mapillary_tools-0.14.0a2}/mapillary_tools/mp4/io_utils.py +0 -0
- {mapillary_tools-0.14.0a1 → mapillary_tools-0.14.0a2}/mapillary_tools/upload.py +0 -0
- {mapillary_tools-0.14.0a1 → mapillary_tools-0.14.0a2}/mapillary_tools/upload_api_v4.py +0 -0
- {mapillary_tools-0.14.0a1 → mapillary_tools-0.14.0a2}/mapillary_tools/uploader.py +0 -0
- {mapillary_tools-0.14.0a1 → mapillary_tools-0.14.0a2}/mapillary_tools.egg-info/dependency_links.txt +0 -0
- {mapillary_tools-0.14.0a1 → mapillary_tools-0.14.0a2}/mapillary_tools.egg-info/entry_points.txt +0 -0
- {mapillary_tools-0.14.0a1 → mapillary_tools-0.14.0a2}/mapillary_tools.egg-info/requires.txt +0 -0
- {mapillary_tools-0.14.0a1 → mapillary_tools-0.14.0a2}/mapillary_tools.egg-info/top_level.txt +0 -0
- {mapillary_tools-0.14.0a1 → mapillary_tools-0.14.0a2}/requirements.txt +0 -0
- {mapillary_tools-0.14.0a1 → mapillary_tools-0.14.0a2}/schema/image_description_schema.json +0 -0
- {mapillary_tools-0.14.0a1 → mapillary_tools-0.14.0a2}/setup.cfg +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
VERSION = "0.14.0a2"
|
|
@@ -135,7 +135,7 @@ def _log_debug_response(resp: requests.Response):
|
|
|
135
135
|
if logging.getLogger().getEffectiveLevel() <= logging.DEBUG:
|
|
136
136
|
return
|
|
137
137
|
|
|
138
|
-
data:
|
|
138
|
+
data: str | bytes
|
|
139
139
|
try:
|
|
140
140
|
data = _truncate(dumps(_sanitize(resp.json())))
|
|
141
141
|
except Exception:
|
|
@@ -148,7 +148,7 @@ def readable_http_error(ex: requests.HTTPError) -> str:
|
|
|
148
148
|
req = ex.request
|
|
149
149
|
resp = ex.response
|
|
150
150
|
|
|
151
|
-
data:
|
|
151
|
+
data: str | bytes
|
|
152
152
|
try:
|
|
153
153
|
data = _truncate(dumps(_sanitize(resp.json())))
|
|
154
154
|
except Exception:
|
|
@@ -284,7 +284,7 @@ def get_upload_token(email: str, password: str) -> requests.Response:
|
|
|
284
284
|
|
|
285
285
|
|
|
286
286
|
def fetch_organization(
|
|
287
|
-
user_access_token: str, organization_id:
|
|
287
|
+
user_access_token: str, organization_id: int | str
|
|
288
288
|
) -> requests.Response:
|
|
289
289
|
resp = request_get(
|
|
290
290
|
f"{MAPILLARY_GRAPH_API_ENDPOINT}/{organization_id}",
|
|
@@ -329,7 +329,7 @@ ActionType = T.Literal[
|
|
|
329
329
|
]
|
|
330
330
|
|
|
331
331
|
|
|
332
|
-
def log_event(action_type: ActionType, properties:
|
|
332
|
+
def log_event(action_type: ActionType, properties: dict) -> requests.Response:
|
|
333
333
|
resp = request_post(
|
|
334
334
|
f"{MAPILLARY_GRAPH_API_ENDPOINT}/logging",
|
|
335
335
|
json={
|
|
@@ -373,7 +373,7 @@ SAMPLE_ENTRY_CLS_BY_CAMM_TYPE = {
|
|
|
373
373
|
assert len(SAMPLE_ENTRY_CLS_BY_CAMM_TYPE) == 5, SAMPLE_ENTRY_CLS_BY_CAMM_TYPE.keys()
|
|
374
374
|
|
|
375
375
|
|
|
376
|
-
_SWITCH:
|
|
376
|
+
_SWITCH: dict[int, C.Struct] = {
|
|
377
377
|
# Angle_axis
|
|
378
378
|
CAMMType.ANGLE_AXIS.value: _Float[3], # type: ignore
|
|
379
379
|
# Exposure time
|
|
@@ -436,7 +436,7 @@ def _parse_telemetry_from_sample(
|
|
|
436
436
|
|
|
437
437
|
def _filter_telemetry_by_elst_segments(
|
|
438
438
|
measurements: T.Iterable[TelemetryMeasurement],
|
|
439
|
-
elst: T.Sequence[
|
|
439
|
+
elst: T.Sequence[tuple[float, float]],
|
|
440
440
|
) -> T.Generator[TelemetryMeasurement, None, None]:
|
|
441
441
|
empty_elst = [entry for entry in elst if entry[0] == -1]
|
|
442
442
|
if empty_elst:
|
|
@@ -466,8 +466,8 @@ def _filter_telemetry_by_elst_segments(
|
|
|
466
466
|
|
|
467
467
|
|
|
468
468
|
def elst_entry_to_seconds(
|
|
469
|
-
entry:
|
|
470
|
-
) ->
|
|
469
|
+
entry: dict, movie_timescale: int, media_timescale: int
|
|
470
|
+
) -> tuple[float, float]:
|
|
471
471
|
assert movie_timescale > 0, "expected positive movie_timescale"
|
|
472
472
|
assert media_timescale > 0, "expected positive media_timescale"
|
|
473
473
|
media_time, duration = entry["media_time"], entry["segment_duration"]
|
|
@@ -477,7 +477,7 @@ def elst_entry_to_seconds(
|
|
|
477
477
|
return (media_time, duration)
|
|
478
478
|
|
|
479
479
|
|
|
480
|
-
def _is_camm_description(description:
|
|
480
|
+
def _is_camm_description(description: dict) -> bool:
|
|
481
481
|
return description["format"] == b"camm"
|
|
482
482
|
|
|
483
483
|
|
|
@@ -2,7 +2,6 @@ import argparse
|
|
|
2
2
|
import enum
|
|
3
3
|
import logging
|
|
4
4
|
import sys
|
|
5
|
-
import typing as T
|
|
6
5
|
from pathlib import Path
|
|
7
6
|
|
|
8
7
|
import requests
|
|
@@ -86,7 +85,7 @@ def configure_logger(logger: logging.Logger, stream=None) -> None:
|
|
|
86
85
|
logger.addHandler(handler)
|
|
87
86
|
|
|
88
87
|
|
|
89
|
-
def _log_params(argvars:
|
|
88
|
+
def _log_params(argvars: dict) -> None:
|
|
90
89
|
MAX_ENTRIES = 5
|
|
91
90
|
|
|
92
91
|
def _stringify(x) -> str:
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import configparser
|
|
2
4
|
import os
|
|
3
5
|
import typing as T
|
|
@@ -35,8 +37,8 @@ def _load_config(config_path: str) -> configparser.ConfigParser:
|
|
|
35
37
|
|
|
36
38
|
|
|
37
39
|
def load_user(
|
|
38
|
-
profile_name: str, config_path:
|
|
39
|
-
) ->
|
|
40
|
+
profile_name: str, config_path: str | None = None
|
|
41
|
+
) -> types.UserItem | None:
|
|
40
42
|
if config_path is None:
|
|
41
43
|
config_path = MAPILLARY_CONFIG_PATH
|
|
42
44
|
config = _load_config(config_path)
|
|
@@ -46,7 +48,7 @@ def load_user(
|
|
|
46
48
|
return T.cast(types.UserItem, user_items)
|
|
47
49
|
|
|
48
50
|
|
|
49
|
-
def list_all_users(config_path:
|
|
51
|
+
def list_all_users(config_path: str | None = None) -> dict[str, types.UserItem]:
|
|
50
52
|
if config_path is None:
|
|
51
53
|
config_path = MAPILLARY_CONFIG_PATH
|
|
52
54
|
cp = _load_config(config_path)
|
|
@@ -58,7 +60,7 @@ def list_all_users(config_path: T.Optional[str] = None) -> T.Dict[str, types.Use
|
|
|
58
60
|
|
|
59
61
|
|
|
60
62
|
def update_config(
|
|
61
|
-
profile_name: str, user_items: types.UserItem, config_path:
|
|
63
|
+
profile_name: str, user_items: types.UserItem, config_path: str | None = None
|
|
62
64
|
) -> None:
|
|
63
65
|
if config_path is None:
|
|
64
66
|
config_path = MAPILLARY_CONFIG_PATH
|
|
@@ -72,7 +74,7 @@ def update_config(
|
|
|
72
74
|
config.write(fp)
|
|
73
75
|
|
|
74
76
|
|
|
75
|
-
def remove_config(profile_name: str, config_path:
|
|
77
|
+
def remove_config(profile_name: str, config_path: str | None = None) -> None:
|
|
76
78
|
if config_path is None:
|
|
77
79
|
config_path = MAPILLARY_CONFIG_PATH
|
|
78
80
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
-
import typing as T
|
|
5
4
|
|
|
6
5
|
import appdirs
|
|
7
6
|
|
|
@@ -52,7 +51,7 @@ UPLOAD_CHUNK_SIZE_MB = float(os.getenv(_ENV_PREFIX + "UPLOAD_CHUNK_SIZE_MB", 1))
|
|
|
52
51
|
# It is used to filter out noisy points
|
|
53
52
|
GOPRO_MAX_DOP100 = int(os.getenv(_ENV_PREFIX + "GOPRO_MAX_DOP100", 1000))
|
|
54
53
|
# Within the GPS stream: 0 - no lock, 2 or 3 - 2D or 3D Lock
|
|
55
|
-
GOPRO_GPS_FIXES:
|
|
54
|
+
GOPRO_GPS_FIXES: set[int] = set(
|
|
56
55
|
int(fix) for fix in os.getenv(_ENV_PREFIX + "GOPRO_GPS_FIXES", "2,3").split(",")
|
|
57
56
|
)
|
|
58
57
|
MAX_UPLOAD_RETRIES: int = int(os.getenv(_ENV_PREFIX + "MAX_UPLOAD_RETRIES", 200))
|
|
@@ -36,7 +36,7 @@ def eval_frac(value: Ratio) -> float:
|
|
|
36
36
|
return float(value.num) / float(value.den)
|
|
37
37
|
|
|
38
38
|
|
|
39
|
-
def gps_to_decimal(values:
|
|
39
|
+
def gps_to_decimal(values: tuple[Ratio, Ratio, Ratio]) -> float | None:
|
|
40
40
|
try:
|
|
41
41
|
deg, min, sec, *_ = values
|
|
42
42
|
except (TypeError, ValueError):
|
|
@@ -56,14 +56,14 @@ def gps_to_decimal(values: T.Tuple[Ratio, Ratio, Ratio]) -> T.Optional[float]:
|
|
|
56
56
|
return degrees + minutes / 60 + seconds / 3600
|
|
57
57
|
|
|
58
58
|
|
|
59
|
-
def _parse_coord_numeric(coord: str, ref:
|
|
59
|
+
def _parse_coord_numeric(coord: str, ref: str | None) -> float | None:
|
|
60
60
|
try:
|
|
61
61
|
return float(coord) * SIGN_BY_DIRECTION[ref]
|
|
62
62
|
except (ValueError, KeyError):
|
|
63
63
|
return None
|
|
64
64
|
|
|
65
65
|
|
|
66
|
-
def _parse_coord_adobe(coord: str) ->
|
|
66
|
+
def _parse_coord_adobe(coord: str) -> float | None:
|
|
67
67
|
"""
|
|
68
68
|
Parse Adobe coordinate format: <degrees,fractionalminutes[NSEW]>
|
|
69
69
|
"""
|
|
@@ -79,7 +79,7 @@ def _parse_coord_adobe(coord: str) -> T.Optional[float]:
|
|
|
79
79
|
return None
|
|
80
80
|
|
|
81
81
|
|
|
82
|
-
def _parse_coord(coord:
|
|
82
|
+
def _parse_coord(coord: str | None, ref: str | None) -> float | None:
|
|
83
83
|
if coord is None:
|
|
84
84
|
return None
|
|
85
85
|
parsed = _parse_coord_numeric(coord, ref)
|
|
@@ -88,7 +88,7 @@ def _parse_coord(coord: T.Optional[str], ref: T.Optional[str]) -> T.Optional[flo
|
|
|
88
88
|
return parsed
|
|
89
89
|
|
|
90
90
|
|
|
91
|
-
def _parse_iso(dtstr: str) ->
|
|
91
|
+
def _parse_iso(dtstr: str) -> datetime.datetime | None:
|
|
92
92
|
try:
|
|
93
93
|
return datetime.datetime.fromisoformat(dtstr)
|
|
94
94
|
except ValueError:
|
|
@@ -99,8 +99,8 @@ def _parse_iso(dtstr: str) -> T.Optional[datetime.datetime]:
|
|
|
99
99
|
|
|
100
100
|
|
|
101
101
|
def strptime_alternative_formats(
|
|
102
|
-
dtstr: str, formats:
|
|
103
|
-
) ->
|
|
102
|
+
dtstr: str, formats: list[str]
|
|
103
|
+
) -> datetime.datetime | None:
|
|
104
104
|
for format in formats:
|
|
105
105
|
if format == "ISO":
|
|
106
106
|
dt = _parse_iso(dtstr)
|
|
@@ -114,7 +114,7 @@ def strptime_alternative_formats(
|
|
|
114
114
|
return None
|
|
115
115
|
|
|
116
116
|
|
|
117
|
-
def parse_timestr_as_timedelta(timestr: str) ->
|
|
117
|
+
def parse_timestr_as_timedelta(timestr: str) -> datetime.timedelta | None:
|
|
118
118
|
timestr = timestr.strip()
|
|
119
119
|
parts = timestr.strip().split(":")
|
|
120
120
|
try:
|
|
@@ -133,8 +133,8 @@ def parse_timestr_as_timedelta(timestr: str) -> T.Optional[datetime.timedelta]:
|
|
|
133
133
|
|
|
134
134
|
|
|
135
135
|
def parse_time_ratios_as_timedelta(
|
|
136
|
-
time_tuple:
|
|
137
|
-
) ->
|
|
136
|
+
time_tuple: list[Ratio],
|
|
137
|
+
) -> datetime.timedelta | None:
|
|
138
138
|
try:
|
|
139
139
|
hours, minutes, seconds, *_ = time_tuple
|
|
140
140
|
except (ValueError, TypeError):
|
|
@@ -156,8 +156,8 @@ def parse_time_ratios_as_timedelta(
|
|
|
156
156
|
|
|
157
157
|
def parse_gps_datetime(
|
|
158
158
|
dtstr: str,
|
|
159
|
-
default_tz:
|
|
160
|
-
) ->
|
|
159
|
+
default_tz: datetime.timezone | None = datetime.timezone.utc,
|
|
160
|
+
) -> datetime.datetime | None:
|
|
161
161
|
dtstr = dtstr.strip()
|
|
162
162
|
|
|
163
163
|
dt = strptime_alternative_formats(dtstr, ["ISO"])
|
|
@@ -176,8 +176,8 @@ def parse_gps_datetime(
|
|
|
176
176
|
def parse_gps_datetime_separately(
|
|
177
177
|
datestr: str,
|
|
178
178
|
timestr: str,
|
|
179
|
-
default_tz:
|
|
180
|
-
) ->
|
|
179
|
+
default_tz: datetime.timezone | None = datetime.timezone.utc,
|
|
180
|
+
) -> datetime.datetime | None:
|
|
181
181
|
"""
|
|
182
182
|
Parse GPSDateStamp and GPSTimeStamp and return the corresponding datetime object in GMT.
|
|
183
183
|
|
|
@@ -232,8 +232,8 @@ def parse_gps_datetime_separately(
|
|
|
232
232
|
|
|
233
233
|
|
|
234
234
|
def parse_datetimestr_with_subsec_and_offset(
|
|
235
|
-
dtstr: str, subsec:
|
|
236
|
-
) ->
|
|
235
|
+
dtstr: str, subsec: str | None = None, tz_offset: str | None = None
|
|
236
|
+
) -> datetime.datetime | None:
|
|
237
237
|
"""
|
|
238
238
|
Convert dtstr "YYYY:mm:dd HH:MM:SS[.sss]" to a datetime object.
|
|
239
239
|
It handles time "24:00:00" as "00:00:00" of the next day.
|
|
@@ -294,35 +294,35 @@ _FIELD_TYPE = T.TypeVar("_FIELD_TYPE", int, float, str)
|
|
|
294
294
|
|
|
295
295
|
class ExifReadABC(abc.ABC):
|
|
296
296
|
@abc.abstractmethod
|
|
297
|
-
def extract_altitude(self) ->
|
|
297
|
+
def extract_altitude(self) -> float | None:
|
|
298
298
|
raise NotImplementedError
|
|
299
299
|
|
|
300
300
|
@abc.abstractmethod
|
|
301
|
-
def extract_capture_time(self) ->
|
|
301
|
+
def extract_capture_time(self) -> datetime.datetime | None:
|
|
302
302
|
raise NotImplementedError
|
|
303
303
|
|
|
304
304
|
@abc.abstractmethod
|
|
305
|
-
def extract_direction(self) ->
|
|
305
|
+
def extract_direction(self) -> float | None:
|
|
306
306
|
raise NotImplementedError
|
|
307
307
|
|
|
308
308
|
@abc.abstractmethod
|
|
309
|
-
def extract_lon_lat(self) ->
|
|
309
|
+
def extract_lon_lat(self) -> tuple[float, float] | None:
|
|
310
310
|
raise NotImplementedError
|
|
311
311
|
|
|
312
312
|
@abc.abstractmethod
|
|
313
|
-
def extract_make(self) ->
|
|
313
|
+
def extract_make(self) -> str | None:
|
|
314
314
|
raise NotImplementedError
|
|
315
315
|
|
|
316
316
|
@abc.abstractmethod
|
|
317
|
-
def extract_model(self) ->
|
|
317
|
+
def extract_model(self) -> str | None:
|
|
318
318
|
raise NotImplementedError
|
|
319
319
|
|
|
320
320
|
@abc.abstractmethod
|
|
321
|
-
def extract_width(self) ->
|
|
321
|
+
def extract_width(self) -> int | None:
|
|
322
322
|
raise NotImplementedError
|
|
323
323
|
|
|
324
324
|
@abc.abstractmethod
|
|
325
|
-
def extract_height(self) ->
|
|
325
|
+
def extract_height(self) -> int | None:
|
|
326
326
|
raise NotImplementedError
|
|
327
327
|
|
|
328
328
|
@abc.abstractmethod
|
|
@@ -333,7 +333,7 @@ class ExifReadABC(abc.ABC):
|
|
|
333
333
|
class ExifReadFromXMP(ExifReadABC):
|
|
334
334
|
def __init__(self, etree: et.ElementTree):
|
|
335
335
|
self.etree = etree
|
|
336
|
-
self._tags_or_attrs:
|
|
336
|
+
self._tags_or_attrs: dict[str, str] = {}
|
|
337
337
|
for description in self.etree.iterfind(
|
|
338
338
|
".//rdf:Description", namespaces=XMP_NAMESPACES
|
|
339
339
|
):
|
|
@@ -343,12 +343,12 @@ class ExifReadFromXMP(ExifReadABC):
|
|
|
343
343
|
if child.text is not None:
|
|
344
344
|
self._tags_or_attrs[child.tag] = child.text
|
|
345
345
|
|
|
346
|
-
def extract_altitude(self) ->
|
|
346
|
+
def extract_altitude(self) -> float | None:
|
|
347
347
|
return self._extract_alternative_fields(["exif:GPSAltitude"], float)
|
|
348
348
|
|
|
349
349
|
def _extract_exif_datetime(
|
|
350
350
|
self, dt_tag: str, subsec_tag: str, offset_tag: str
|
|
351
|
-
) ->
|
|
351
|
+
) -> datetime.datetime | None:
|
|
352
352
|
dtstr = self._extract_alternative_fields([dt_tag], str)
|
|
353
353
|
if dtstr is None:
|
|
354
354
|
return None
|
|
@@ -363,7 +363,7 @@ class ExifReadFromXMP(ExifReadABC):
|
|
|
363
363
|
return None
|
|
364
364
|
return dt
|
|
365
365
|
|
|
366
|
-
def extract_exif_datetime(self) ->
|
|
366
|
+
def extract_exif_datetime(self) -> datetime.datetime | None:
|
|
367
367
|
dt = self._extract_exif_datetime(
|
|
368
368
|
"exif:DateTimeOriginal",
|
|
369
369
|
"exif:SubsecTimeOriginal",
|
|
@@ -382,7 +382,7 @@ class ExifReadFromXMP(ExifReadABC):
|
|
|
382
382
|
|
|
383
383
|
return None
|
|
384
384
|
|
|
385
|
-
def extract_gps_datetime(self) ->
|
|
385
|
+
def extract_gps_datetime(self) -> datetime.datetime | None:
|
|
386
386
|
"""
|
|
387
387
|
Extract timestamp from GPS field.
|
|
388
388
|
"""
|
|
@@ -402,7 +402,7 @@ class ExifReadFromXMP(ExifReadABC):
|
|
|
402
402
|
# handle: exif:GPSTimeStamp="17:22:05.999000"
|
|
403
403
|
return parse_gps_datetime_separately(datestr, timestr)
|
|
404
404
|
|
|
405
|
-
def extract_capture_time(self) ->
|
|
405
|
+
def extract_capture_time(self) -> datetime.datetime | None:
|
|
406
406
|
dt = self.extract_gps_datetime()
|
|
407
407
|
if dt is not None and dt.date() != datetime.date(1970, 1, 1):
|
|
408
408
|
return dt
|
|
@@ -413,22 +413,22 @@ class ExifReadFromXMP(ExifReadABC):
|
|
|
413
413
|
|
|
414
414
|
return None
|
|
415
415
|
|
|
416
|
-
def extract_direction(self) ->
|
|
416
|
+
def extract_direction(self) -> float | None:
|
|
417
417
|
return self._extract_alternative_fields(
|
|
418
418
|
["exif:GPSImgDirection", "exif:GPSTrack"], float
|
|
419
419
|
)
|
|
420
420
|
|
|
421
|
-
def extract_lon_lat(self) ->
|
|
421
|
+
def extract_lon_lat(self) -> tuple[float, float] | None:
|
|
422
422
|
lat_ref = self._extract_alternative_fields(["exif:GPSLatitudeRef"], str)
|
|
423
|
-
lat_str:
|
|
423
|
+
lat_str: str | None = self._extract_alternative_fields(
|
|
424
424
|
["exif:GPSLatitude"], str
|
|
425
425
|
)
|
|
426
|
-
lat:
|
|
426
|
+
lat: float | None = _parse_coord(lat_str, lat_ref)
|
|
427
427
|
if lat is None:
|
|
428
428
|
return None
|
|
429
429
|
|
|
430
430
|
lon_ref = self._extract_alternative_fields(["exif:GPSLongitudeRef"], str)
|
|
431
|
-
lon_str:
|
|
431
|
+
lon_str: str | None = self._extract_alternative_fields(
|
|
432
432
|
["exif:GPSLongitude"], str
|
|
433
433
|
)
|
|
434
434
|
lon = _parse_coord(lon_str, lon_ref)
|
|
@@ -437,13 +437,13 @@ class ExifReadFromXMP(ExifReadABC):
|
|
|
437
437
|
|
|
438
438
|
return lon, lat
|
|
439
439
|
|
|
440
|
-
def extract_make(self) ->
|
|
440
|
+
def extract_make(self) -> str | None:
|
|
441
441
|
make = self._extract_alternative_fields(["tiff:Make", "exifEX:LensMake"], str)
|
|
442
442
|
if make is None:
|
|
443
443
|
return None
|
|
444
444
|
return make.strip()
|
|
445
445
|
|
|
446
|
-
def extract_model(self) ->
|
|
446
|
+
def extract_model(self) -> str | None:
|
|
447
447
|
model = self._extract_alternative_fields(
|
|
448
448
|
["tiff:Model", "exifEX:LensModel"], str
|
|
449
449
|
)
|
|
@@ -451,7 +451,7 @@ class ExifReadFromXMP(ExifReadABC):
|
|
|
451
451
|
return None
|
|
452
452
|
return model.strip()
|
|
453
453
|
|
|
454
|
-
def extract_width(self) ->
|
|
454
|
+
def extract_width(self) -> int | None:
|
|
455
455
|
return self._extract_alternative_fields(
|
|
456
456
|
[
|
|
457
457
|
"exif:PixelXDimension",
|
|
@@ -461,7 +461,7 @@ class ExifReadFromXMP(ExifReadABC):
|
|
|
461
461
|
int,
|
|
462
462
|
)
|
|
463
463
|
|
|
464
|
-
def extract_height(self) ->
|
|
464
|
+
def extract_height(self) -> int | None:
|
|
465
465
|
return self._extract_alternative_fields(
|
|
466
466
|
[
|
|
467
467
|
"exif:PixelYDimension",
|
|
@@ -513,7 +513,7 @@ class ExifReadFromXMP(ExifReadABC):
|
|
|
513
513
|
return None
|
|
514
514
|
|
|
515
515
|
|
|
516
|
-
def extract_xmp_efficiently(fp) ->
|
|
516
|
+
def extract_xmp_efficiently(fp) -> str | None:
|
|
517
517
|
"""
|
|
518
518
|
Extract XMP metadata from a JPEG file efficiently by reading only necessary chunks.
|
|
519
519
|
|
|
@@ -598,7 +598,7 @@ class ExifReadFromEXIF(ExifReadABC):
|
|
|
598
598
|
EXIF class for reading exif from an image
|
|
599
599
|
"""
|
|
600
600
|
|
|
601
|
-
def __init__(self, path_or_stream:
|
|
601
|
+
def __init__(self, path_or_stream: Path | T.BinaryIO) -> None:
|
|
602
602
|
"""
|
|
603
603
|
Initialize EXIF object with FILE as filename or fileobj
|
|
604
604
|
"""
|
|
@@ -621,7 +621,7 @@ class ExifReadFromEXIF(ExifReadABC):
|
|
|
621
621
|
LOG.warning("Error reading EXIF: %s", ex)
|
|
622
622
|
self.tags = {}
|
|
623
623
|
|
|
624
|
-
def extract_altitude(self) ->
|
|
624
|
+
def extract_altitude(self) -> float | None:
|
|
625
625
|
"""
|
|
626
626
|
Extract altitude
|
|
627
627
|
"""
|
|
@@ -634,7 +634,7 @@ class ExifReadFromEXIF(ExifReadABC):
|
|
|
634
634
|
altitude_ref = {0: 1, 1: -1}
|
|
635
635
|
return altitude * altitude_ref.get(ref, 1)
|
|
636
636
|
|
|
637
|
-
def extract_gps_datetime(self) ->
|
|
637
|
+
def extract_gps_datetime(self) -> datetime.datetime | None:
|
|
638
638
|
"""
|
|
639
639
|
Extract timestamp from GPS field.
|
|
640
640
|
"""
|
|
@@ -662,7 +662,7 @@ class ExifReadFromEXIF(ExifReadABC):
|
|
|
662
662
|
|
|
663
663
|
def _extract_exif_datetime(
|
|
664
664
|
self, dt_tag: str, subsec_tag: str, offset_tag: str
|
|
665
|
-
) ->
|
|
665
|
+
) -> datetime.datetime | None:
|
|
666
666
|
dtstr = self._extract_alternative_fields([dt_tag], field_type=str)
|
|
667
667
|
if dtstr is None:
|
|
668
668
|
return None
|
|
@@ -677,7 +677,7 @@ class ExifReadFromEXIF(ExifReadABC):
|
|
|
677
677
|
return None
|
|
678
678
|
return dt
|
|
679
679
|
|
|
680
|
-
def extract_exif_datetime(self) ->
|
|
680
|
+
def extract_exif_datetime(self) -> datetime.datetime | None:
|
|
681
681
|
# EXIF DateTimeOriginal: 0x9003 (date/time when original image was taken)
|
|
682
682
|
# EXIF SubSecTimeOriginal: 0x9291 (fractional seconds for DateTimeOriginal)
|
|
683
683
|
# EXIF OffsetTimeOriginal: 0x9011 (time zone for DateTimeOriginal)
|
|
@@ -711,7 +711,7 @@ class ExifReadFromEXIF(ExifReadABC):
|
|
|
711
711
|
|
|
712
712
|
return None
|
|
713
713
|
|
|
714
|
-
def extract_capture_time(self) ->
|
|
714
|
+
def extract_capture_time(self) -> datetime.datetime | None:
|
|
715
715
|
"""
|
|
716
716
|
Extract capture time from EXIF DateTime tags
|
|
717
717
|
"""
|
|
@@ -730,7 +730,7 @@ class ExifReadFromEXIF(ExifReadABC):
|
|
|
730
730
|
|
|
731
731
|
return None
|
|
732
732
|
|
|
733
|
-
def extract_direction(self) ->
|
|
733
|
+
def extract_direction(self) -> float | None:
|
|
734
734
|
"""
|
|
735
735
|
Extract image direction (i.e. compass, heading, bearing)
|
|
736
736
|
"""
|
|
@@ -740,7 +740,7 @@ class ExifReadFromEXIF(ExifReadABC):
|
|
|
740
740
|
]
|
|
741
741
|
return self._extract_alternative_fields(fields, float)
|
|
742
742
|
|
|
743
|
-
def extract_lon_lat(self) ->
|
|
743
|
+
def extract_lon_lat(self) -> tuple[float, float] | None:
|
|
744
744
|
lat_tag = self.tags.get("GPS GPSLatitude")
|
|
745
745
|
lon_tag = self.tags.get("GPS GPSLongitude")
|
|
746
746
|
if lat_tag and lon_tag:
|
|
@@ -762,7 +762,7 @@ class ExifReadFromEXIF(ExifReadABC):
|
|
|
762
762
|
|
|
763
763
|
return None
|
|
764
764
|
|
|
765
|
-
def extract_make(self) ->
|
|
765
|
+
def extract_make(self) -> str | None:
|
|
766
766
|
"""
|
|
767
767
|
Extract camera make
|
|
768
768
|
"""
|
|
@@ -773,7 +773,7 @@ class ExifReadFromEXIF(ExifReadABC):
|
|
|
773
773
|
return None
|
|
774
774
|
return make.strip()
|
|
775
775
|
|
|
776
|
-
def extract_model(self) ->
|
|
776
|
+
def extract_model(self) -> str | None:
|
|
777
777
|
"""
|
|
778
778
|
Extract camera model
|
|
779
779
|
"""
|
|
@@ -784,7 +784,7 @@ class ExifReadFromEXIF(ExifReadABC):
|
|
|
784
784
|
return None
|
|
785
785
|
return model.strip()
|
|
786
786
|
|
|
787
|
-
def extract_width(self) ->
|
|
787
|
+
def extract_width(self) -> int | None:
|
|
788
788
|
"""
|
|
789
789
|
Extract image width in pixels
|
|
790
790
|
"""
|
|
@@ -792,7 +792,7 @@ class ExifReadFromEXIF(ExifReadABC):
|
|
|
792
792
|
["Image ImageWidth", "EXIF ExifImageWidth"], int
|
|
793
793
|
)
|
|
794
794
|
|
|
795
|
-
def extract_height(self) ->
|
|
795
|
+
def extract_height(self) -> int | None:
|
|
796
796
|
"""
|
|
797
797
|
Extract image height in pixels
|
|
798
798
|
"""
|
|
@@ -813,7 +813,7 @@ class ExifReadFromEXIF(ExifReadABC):
|
|
|
813
813
|
|
|
814
814
|
def _extract_alternative_fields(
|
|
815
815
|
self,
|
|
816
|
-
fields: T.
|
|
816
|
+
fields: T.Iterable[str],
|
|
817
817
|
field_type: type[_FIELD_TYPE],
|
|
818
818
|
) -> _FIELD_TYPE | None:
|
|
819
819
|
"""
|
|
@@ -847,7 +847,7 @@ class ExifReadFromEXIF(ExifReadABC):
|
|
|
847
847
|
raise ValueError(f"Invalid field type {field_type}")
|
|
848
848
|
return None
|
|
849
849
|
|
|
850
|
-
def extract_application_notes(self) ->
|
|
850
|
+
def extract_application_notes(self) -> str | None:
|
|
851
851
|
xmp = self.tags.get("Image ApplicationNotes")
|
|
852
852
|
if xmp is None:
|
|
853
853
|
return None
|
|
@@ -863,13 +863,13 @@ class ExifRead(ExifReadFromEXIF):
|
|
|
863
863
|
NOTE: For performance reasons, XMP is only extracted if EXIF does not contain the required fields
|
|
864
864
|
"""
|
|
865
865
|
|
|
866
|
-
def __init__(self, path_or_stream:
|
|
866
|
+
def __init__(self, path_or_stream: Path | T.BinaryIO) -> None:
|
|
867
867
|
super().__init__(path_or_stream)
|
|
868
868
|
self._path_or_stream = path_or_stream
|
|
869
869
|
self._xml_extracted: bool = False
|
|
870
|
-
self._cached_xml:
|
|
870
|
+
self._cached_xml: ExifReadFromXMP | None = None
|
|
871
871
|
|
|
872
|
-
def _xmp_with_reason(self, reason: str) ->
|
|
872
|
+
def _xmp_with_reason(self, reason: str) -> ExifReadFromXMP | None:
|
|
873
873
|
if not self._xml_extracted:
|
|
874
874
|
LOG.debug('Extracting XMP for "%s"', reason)
|
|
875
875
|
self._cached_xml = self._extract_xmp()
|
|
@@ -877,7 +877,7 @@ class ExifRead(ExifReadFromEXIF):
|
|
|
877
877
|
|
|
878
878
|
return self._cached_xml
|
|
879
879
|
|
|
880
|
-
def _extract_xmp(self) ->
|
|
880
|
+
def _extract_xmp(self) -> ExifReadFromXMP | None:
|
|
881
881
|
xml_str = self.extract_application_notes()
|
|
882
882
|
if xml_str is None:
|
|
883
883
|
if isinstance(self._path_or_stream, Path):
|
|
@@ -898,7 +898,7 @@ class ExifRead(ExifReadFromEXIF):
|
|
|
898
898
|
|
|
899
899
|
return ExifReadFromXMP(et.ElementTree(e))
|
|
900
900
|
|
|
901
|
-
def extract_altitude(self) ->
|
|
901
|
+
def extract_altitude(self) -> float | None:
|
|
902
902
|
val = super().extract_altitude()
|
|
903
903
|
if val is not None:
|
|
904
904
|
return val
|
|
@@ -910,7 +910,7 @@ class ExifRead(ExifReadFromEXIF):
|
|
|
910
910
|
return val
|
|
911
911
|
return None
|
|
912
912
|
|
|
913
|
-
def extract_capture_time(self) ->
|
|
913
|
+
def extract_capture_time(self) -> datetime.datetime | None:
|
|
914
914
|
val = super().extract_capture_time()
|
|
915
915
|
if val is not None:
|
|
916
916
|
return val
|
|
@@ -922,7 +922,7 @@ class ExifRead(ExifReadFromEXIF):
|
|
|
922
922
|
return val
|
|
923
923
|
return None
|
|
924
924
|
|
|
925
|
-
def extract_lon_lat(self) ->
|
|
925
|
+
def extract_lon_lat(self) -> tuple[float, float] | None:
|
|
926
926
|
val = super().extract_lon_lat()
|
|
927
927
|
if val is not None:
|
|
928
928
|
return val
|
|
@@ -934,7 +934,7 @@ class ExifRead(ExifReadFromEXIF):
|
|
|
934
934
|
return val
|
|
935
935
|
return None
|
|
936
936
|
|
|
937
|
-
def extract_make(self) ->
|
|
937
|
+
def extract_make(self) -> str | None:
|
|
938
938
|
val = super().extract_make()
|
|
939
939
|
if val is not None:
|
|
940
940
|
return val
|
|
@@ -946,7 +946,7 @@ class ExifRead(ExifReadFromEXIF):
|
|
|
946
946
|
return val
|
|
947
947
|
return None
|
|
948
948
|
|
|
949
|
-
def extract_model(self) ->
|
|
949
|
+
def extract_model(self) -> str | None:
|
|
950
950
|
val = super().extract_model()
|
|
951
951
|
if val is not None:
|
|
952
952
|
return val
|
|
@@ -958,7 +958,7 @@ class ExifRead(ExifReadFromEXIF):
|
|
|
958
958
|
return val
|
|
959
959
|
return None
|
|
960
960
|
|
|
961
|
-
def extract_width(self) ->
|
|
961
|
+
def extract_width(self) -> int | None:
|
|
962
962
|
val = super().extract_width()
|
|
963
963
|
if val is not None:
|
|
964
964
|
return val
|
|
@@ -970,7 +970,7 @@ class ExifRead(ExifReadFromEXIF):
|
|
|
970
970
|
return val
|
|
971
971
|
return None
|
|
972
972
|
|
|
973
|
-
def extract_height(self) ->
|
|
973
|
+
def extract_height(self) -> int | None:
|
|
974
974
|
val = super().extract_height()
|
|
975
975
|
if val is not None:
|
|
976
976
|
return val
|