mapillary-tools 0.14.0a1__py3-none-any.whl → 0.14.0b1__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 +5 -4
- mapillary_tools/authenticate.py +9 -9
- mapillary_tools/blackvue_parser.py +79 -22
- mapillary_tools/camm/camm_parser.py +5 -5
- mapillary_tools/commands/__main__.py +1 -2
- mapillary_tools/config.py +41 -18
- mapillary_tools/constants.py +3 -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 +88 -49
- mapillary_tools/exiftool_runner.py +4 -24
- mapillary_tools/ffmpeg.py +417 -242
- mapillary_tools/geo.py +4 -21
- mapillary_tools/geotag/__init__.py +0 -1
- mapillary_tools/geotag/{geotag_from_generic.py → base.py} +34 -50
- mapillary_tools/geotag/factory.py +105 -103
- mapillary_tools/geotag/geotag_images_from_exif.py +15 -51
- mapillary_tools/geotag/geotag_images_from_exiftool.py +118 -63
- mapillary_tools/geotag/geotag_images_from_gpx.py +33 -16
- 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 +51 -14
- mapillary_tools/geotag/geotag_videos_from_exiftool.py +123 -0
- mapillary_tools/geotag/geotag_videos_from_gpx.py +35 -123
- 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 +26 -3
- 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/geotag/video_extractors/gpx.py +116 -0
- mapillary_tools/geotag/video_extractors/native.py +135 -0
- mapillary_tools/gpmf/gpmf_parser.py +16 -16
- mapillary_tools/gpmf/gps_filter.py +5 -3
- mapillary_tools/history.py +8 -3
- 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 +21 -15
- mapillary_tools/process_sequence_properties.py +49 -49
- mapillary_tools/sample_video.py +15 -14
- mapillary_tools/serializer/description.py +587 -0
- mapillary_tools/serializer/gpx.py +132 -0
- mapillary_tools/telemetry.py +6 -5
- mapillary_tools/types.py +64 -635
- mapillary_tools/upload.py +176 -197
- mapillary_tools/upload_api_v4.py +94 -51
- mapillary_tools/uploader.py +284 -138
- mapillary_tools/utils.py +16 -18
- {mapillary_tools-0.14.0a1.dist-info → mapillary_tools-0.14.0b1.dist-info}/METADATA +87 -31
- mapillary_tools-0.14.0b1.dist-info/RECORD +75 -0
- {mapillary_tools-0.14.0a1.dist-info → mapillary_tools-0.14.0b1.dist-info}/WHEEL +1 -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/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.14.0a1.dist-info/RECORD +0 -78
- {mapillary_tools-0.14.0a1.dist-info → mapillary_tools-0.14.0b1.dist-info}/entry_points.txt +0 -0
- {mapillary_tools-0.14.0a1.dist-info → mapillary_tools-0.14.0b1.dist-info}/licenses/LICENSE +0 -0
- {mapillary_tools-0.14.0a1.dist-info → mapillary_tools-0.14.0b1.dist-info}/top_level.txt +0 -0
mapillary_tools/exif_read.py
CHANGED
|
@@ -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
|
mapillary_tools/exif_write.py
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# pyre-ignore-all-errors[5, 21, 24]
|
|
2
|
+
from __future__ import annotations
|
|
2
3
|
|
|
3
4
|
import datetime
|
|
4
5
|
import io
|
|
5
6
|
import json
|
|
6
7
|
import logging
|
|
7
8
|
import math
|
|
8
|
-
import typing as T
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
|
|
11
11
|
import piexif
|
|
@@ -15,9 +15,9 @@ LOG = logging.getLogger(__name__)
|
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
class ExifEdit:
|
|
18
|
-
_filename_or_bytes:
|
|
18
|
+
_filename_or_bytes: str | bytes
|
|
19
19
|
|
|
20
|
-
def __init__(self, filename_or_bytes:
|
|
20
|
+
def __init__(self, filename_or_bytes: Path | bytes) -> None:
|
|
21
21
|
"""Initialize the object"""
|
|
22
22
|
if isinstance(filename_or_bytes, Path):
|
|
23
23
|
# make sure filename is resolved to avoid to be interpretted as bytes in piexif
|
|
@@ -25,12 +25,12 @@ class ExifEdit:
|
|
|
25
25
|
self._filename_or_bytes = str(filename_or_bytes.resolve())
|
|
26
26
|
else:
|
|
27
27
|
self._filename_or_bytes = filename_or_bytes
|
|
28
|
-
self._ef:
|
|
28
|
+
self._ef: dict = piexif.load(self._filename_or_bytes)
|
|
29
29
|
|
|
30
30
|
@staticmethod
|
|
31
31
|
def decimal_to_dms(
|
|
32
32
|
value: float, precision: int
|
|
33
|
-
) ->
|
|
33
|
+
) -> tuple[tuple[float, int], tuple[float, int], tuple[float, int]]:
|
|
34
34
|
"""
|
|
35
35
|
Convert decimal position to degrees, minutes, seconds in a fromat supported by EXIF
|
|
36
36
|
"""
|
|
@@ -40,7 +40,7 @@ class ExifEdit:
|
|
|
40
40
|
|
|
41
41
|
return (deg, 1), (min, 1), (sec, precision)
|
|
42
42
|
|
|
43
|
-
def add_image_description(self, data:
|
|
43
|
+
def add_image_description(self, data: dict) -> None:
|
|
44
44
|
"""Add a dict to image description."""
|
|
45
45
|
self._ef["0th"][piexif.ImageIFD.ImageDescription] = json.dumps(data)
|
|
46
46
|
|
|
@@ -201,7 +201,7 @@ class ExifEdit:
|
|
|
201
201
|
piexif.insert(exif_bytes, self._filename_or_bytes, output)
|
|
202
202
|
return output.read()
|
|
203
203
|
|
|
204
|
-
def write(self, filename:
|
|
204
|
+
def write(self, filename: Path | None = None) -> None:
|
|
205
205
|
"""Save exif data to file."""
|
|
206
206
|
if filename is None:
|
|
207
207
|
if not isinstance(self._filename_or_bytes, str):
|