mapillary-tools 0.10.2a0__py3-none-any.whl → 0.10.3a1__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/commands/process.py +4 -3
- mapillary_tools/exceptions.py +4 -0
- mapillary_tools/exif_read.py +543 -65
- mapillary_tools/exiftool_read.py +406 -0
- mapillary_tools/exiftool_read_video.py +360 -0
- mapillary_tools/geo.py +10 -2
- mapillary_tools/geotag/geotag_from_generic.py +13 -2
- mapillary_tools/geotag/{geotag_from_exif.py → geotag_images_from_exif.py} +51 -67
- mapillary_tools/geotag/geotag_images_from_exiftool.py +123 -0
- mapillary_tools/geotag/geotag_images_from_exiftool_both_image_and_video.py +81 -0
- mapillary_tools/geotag/{geotag_from_gpx.py → geotag_images_from_gpx.py} +16 -13
- mapillary_tools/geotag/{geotag_from_gpx_file.py → geotag_images_from_gpx_file.py} +52 -36
- mapillary_tools/geotag/{geotag_from_nmea_file.py → geotag_images_from_nmea_file.py} +4 -5
- mapillary_tools/geotag/geotag_images_from_video.py +87 -0
- mapillary_tools/geotag/geotag_videos_from_exiftool_video.py +105 -0
- mapillary_tools/geotag/geotag_videos_from_video.py +175 -0
- mapillary_tools/process_geotag_properties.py +65 -31
- mapillary_tools/sample_video.py +19 -6
- mapillary_tools/types.py +2 -0
- mapillary_tools/utils.py +24 -2
- {mapillary_tools-0.10.2a0.dist-info → mapillary_tools-0.10.3a1.dist-info}/METADATA +1 -1
- {mapillary_tools-0.10.2a0.dist-info → mapillary_tools-0.10.3a1.dist-info}/RECORD +27 -24
- mapillary_tools/geotag/geotag_from_blackvue.py +0 -93
- mapillary_tools/geotag/geotag_from_camm.py +0 -94
- mapillary_tools/geotag/geotag_from_gopro.py +0 -96
- mapillary_tools/geotag/geotag_from_video.py +0 -145
- {mapillary_tools-0.10.2a0.dist-info → mapillary_tools-0.10.3a1.dist-info}/LICENSE +0 -0
- {mapillary_tools-0.10.2a0.dist-info → mapillary_tools-0.10.3a1.dist-info}/WHEEL +0 -0
- {mapillary_tools-0.10.2a0.dist-info → mapillary_tools-0.10.3a1.dist-info}/entry_points.txt +0 -0
- {mapillary_tools-0.10.2a0.dist-info → mapillary_tools-0.10.3a1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import typing as T
|
|
3
|
+
from xml.etree.ElementTree import ElementTree
|
|
4
|
+
|
|
5
|
+
from . import exif_read
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
EXIFTOOL_NAMESPACES = {
|
|
9
|
+
"Adobe": "http://ns.exiftool.org/APP14/Adobe/1.0/",
|
|
10
|
+
"Apple": "http://ns.exiftool.org/MakerNotes/Apple/1.0/",
|
|
11
|
+
"Composite": "http://ns.exiftool.org/Composite/1.0/",
|
|
12
|
+
"ExifIFD": "http://ns.exiftool.org/EXIF/ExifIFD/1.0/",
|
|
13
|
+
"ExifTool": "http://ns.exiftool.org/ExifTool/1.0/",
|
|
14
|
+
"File": "http://ns.exiftool.org/File/1.0/",
|
|
15
|
+
"GPS": "http://ns.exiftool.org/EXIF/GPS/1.0/",
|
|
16
|
+
"GoPro": "http://ns.exiftool.org/APP6/GoPro/1.0/",
|
|
17
|
+
"ICC-chrm": "http://ns.exiftool.org/ICC_Profile/ICC-chrm/1.0/",
|
|
18
|
+
"ICC-header": "http://ns.exiftool.org/ICC_Profile/ICC-header/1.0/",
|
|
19
|
+
"ICC-meas": "http://ns.exiftool.org/ICC_Profile/ICC-meas/1.0/",
|
|
20
|
+
"ICC-view": "http://ns.exiftool.org/ICC_Profile/ICC-view/1.0/",
|
|
21
|
+
"ICC_Profile": "http://ns.exiftool.org/ICC_Profile/ICC_Profile/1.0/",
|
|
22
|
+
"IFD0": "http://ns.exiftool.org/EXIF/IFD0/1.0/",
|
|
23
|
+
"IFD1": "http://ns.exiftool.org/EXIF/IFD1/1.0/",
|
|
24
|
+
"IPTC": "http://ns.exiftool.org/IPTC/IPTC/1.0/",
|
|
25
|
+
"InteropIFD": "http://ns.exiftool.org/EXIF/InteropIFD/1.0/",
|
|
26
|
+
"JFIF": "http://ns.exiftool.org/JFIF/JFIF/1.0/",
|
|
27
|
+
"MPF0": "http://ns.exiftool.org/MPF/MPF0/1.0/",
|
|
28
|
+
"MPImage1": "http://ns.exiftool.org/MPF/MPImage1/1.0/",
|
|
29
|
+
"MPImage2": "http://ns.exiftool.org/MPF/MPImage2/1.0/",
|
|
30
|
+
"Photoshop": "http://ns.exiftool.org/Photoshop/Photoshop/1.0/",
|
|
31
|
+
"Samsung": "http://ns.exiftool.org/MakerNotes/Samsung/1.0/",
|
|
32
|
+
"System": "http://ns.exiftool.org/File/System/1.0/",
|
|
33
|
+
"XMP-GAudio": "http://ns.exiftool.org/XMP/XMP-GAudio/1.0/",
|
|
34
|
+
"XMP-GImage": "http://ns.exiftool.org/XMP/XMP-GImage/1.0/",
|
|
35
|
+
"XMP-GPano": "http://ns.exiftool.org/XMP/XMP-GPano/1.0/",
|
|
36
|
+
"XMP-aux": "http://ns.exiftool.org/XMP/XMP-aux/1.0/",
|
|
37
|
+
"XMP-crs": "http://ns.exiftool.org/XMP/XMP-crs/1.0/",
|
|
38
|
+
"XMP-dc": "http://ns.exiftool.org/XMP/XMP-dc/1.0/",
|
|
39
|
+
"XMP-exif": "http://ns.exiftool.org/XMP/XMP-exif/1.0/",
|
|
40
|
+
"XMP-exifEX": "http://ns.exiftool.org/XMP/XMP-exifEX/1.0/",
|
|
41
|
+
"XMP-photoshop": "http://ns.exiftool.org/XMP/XMP-photoshop/1.0/",
|
|
42
|
+
"XMP-tiff": "http://ns.exiftool.org/XMP/XMP-tiff/1.0/",
|
|
43
|
+
"XMP-x": "http://ns.exiftool.org/XMP/XMP-x/1.0/",
|
|
44
|
+
"XMP-xmp": "http://ns.exiftool.org/XMP/XMP-xmp/1.0/",
|
|
45
|
+
"XMP-xmpMM": "http://ns.exiftool.org/XMP/XMP-xmpMM/1.0/",
|
|
46
|
+
"XMP-xmpNote": "http://ns.exiftool.org/XMP/XMP-xmpNote/1.0/",
|
|
47
|
+
"rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
_FIELD_TYPE = T.TypeVar("_FIELD_TYPE", int, float, str)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ExifToolRead(exif_read.ExifReadABC):
|
|
55
|
+
"""
|
|
56
|
+
Read exif from ExifTool XML output
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
etree: ElementTree,
|
|
62
|
+
) -> None:
|
|
63
|
+
self.etree = etree
|
|
64
|
+
|
|
65
|
+
def extract_altitude(self) -> T.Optional[float]:
|
|
66
|
+
"""
|
|
67
|
+
Extract altitude
|
|
68
|
+
"""
|
|
69
|
+
altitude = self._extract_alternative_fields(["GPS:GPSAltitude"], float)
|
|
70
|
+
if altitude is None:
|
|
71
|
+
return None
|
|
72
|
+
# 0 = Above sea level
|
|
73
|
+
# 1 = Below sea level
|
|
74
|
+
ref = self._extract_alternative_fields(["GPS:GPSAltitudeRef"], int)
|
|
75
|
+
if ref == 1:
|
|
76
|
+
altitude = -1 * altitude
|
|
77
|
+
return altitude
|
|
78
|
+
|
|
79
|
+
def _extract_gps_datetime(
|
|
80
|
+
self, date_tags: T.Sequence[str], time_tags: T.Sequence[str]
|
|
81
|
+
) -> T.Optional[datetime.datetime]:
|
|
82
|
+
"""
|
|
83
|
+
Extract timestamp from GPS field.
|
|
84
|
+
"""
|
|
85
|
+
gpsdate = self._extract_alternative_fields(date_tags, str)
|
|
86
|
+
if gpsdate is None:
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
gpstimestamp = self._extract_alternative_fields(time_tags, str)
|
|
90
|
+
if not gpstimestamp:
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
return exif_read.parse_gps_datetime_separately(gpsdate, gpstimestamp)
|
|
94
|
+
|
|
95
|
+
def extract_gps_datetime(self) -> T.Optional[datetime.datetime]:
|
|
96
|
+
"""
|
|
97
|
+
Extract timestamp from GPS field.
|
|
98
|
+
"""
|
|
99
|
+
return self._extract_gps_datetime(["GPS:GPSDateStamp"], ["GPS:GPSTimeStamp"])
|
|
100
|
+
|
|
101
|
+
def extract_gps_datetime_from_xmp(self) -> T.Optional[datetime.datetime]:
|
|
102
|
+
"""
|
|
103
|
+
Extract timestamp from XMP GPS field.
|
|
104
|
+
"""
|
|
105
|
+
# example: <XMP-exif:GPSDateStamp>2021:09:14</XMP-exif:GPSDateStamp>
|
|
106
|
+
# example: <XMP-exif:GPSDateTime>08:23:56.000000</XMP-exif:GPSDateTime>
|
|
107
|
+
return self._extract_gps_datetime(
|
|
108
|
+
["XMP-exif:GPSDateStamp"],
|
|
109
|
+
# Put both here but I do not see any XMP-exif:GPSTimeStamp in my samples
|
|
110
|
+
["XMP-exif:GPSDateTime", "XMP-exif:GPSTimeStamp"],
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
def _extract_exif_datetime(
|
|
114
|
+
self,
|
|
115
|
+
dt_tags: T.Sequence[str],
|
|
116
|
+
subsec_tags: T.Sequence[str],
|
|
117
|
+
offset_tags: T.Sequence[str],
|
|
118
|
+
) -> T.Optional[datetime.datetime]:
|
|
119
|
+
dtstr = self._extract_alternative_fields(dt_tags, str)
|
|
120
|
+
if dtstr is None:
|
|
121
|
+
return None
|
|
122
|
+
subsec = self._extract_alternative_fields(subsec_tags, str)
|
|
123
|
+
# See https://github.com/mapillary/mapillary_tools/issues/388#issuecomment-860198046
|
|
124
|
+
# and https://community.gopro.com/t5/Cameras/subsecond-timestamp-bug/m-p/1057505
|
|
125
|
+
if subsec:
|
|
126
|
+
subsec = subsec.replace(" ", "0")
|
|
127
|
+
offset = self._extract_alternative_fields(offset_tags, str)
|
|
128
|
+
dt = exif_read.parse_datetimestr_with_subsec_and_offset(dtstr, subsec, offset)
|
|
129
|
+
if dt is None:
|
|
130
|
+
return None
|
|
131
|
+
return dt
|
|
132
|
+
|
|
133
|
+
def extract_exif_datetime_from_xmp(self) -> T.Optional[datetime.datetime]:
|
|
134
|
+
# EXIF DateTimeOriginal: 0x9003 (date/time when original image was taken)
|
|
135
|
+
# EXIF SubSecTimeOriginal: 0x9291 (fractional seconds for DateTimeOriginal)
|
|
136
|
+
# EXIF OffsetTimeOriginal: 0x9011 (time zone for DateTimeOriginal)
|
|
137
|
+
dt = self._extract_exif_datetime(
|
|
138
|
+
["XMP-exif:DateTimeOriginal"],
|
|
139
|
+
# NOTE: it is Subsec instead of SubSec
|
|
140
|
+
["XMP-exif:SubsecTimeOriginal"],
|
|
141
|
+
["XMP-exif:OffsetTimeOriginal"],
|
|
142
|
+
)
|
|
143
|
+
if dt is not None:
|
|
144
|
+
return dt
|
|
145
|
+
|
|
146
|
+
# EXIF DateTimeDigitized: 0x9004 CreateDate in ExifTool (called DateTimeDigitized by the EXIF spec.)
|
|
147
|
+
# EXIF SubSecTimeDigitized: 0x9292 (fractional seconds for CreateDate)
|
|
148
|
+
# EXIF OffsetTimeDigitized: 0x9012 (time zone for CreateDate)
|
|
149
|
+
dt = self._extract_exif_datetime(
|
|
150
|
+
["XMP-exif:DateTimeDigitized", "XMP-xmp:CreateDate"],
|
|
151
|
+
# NOTE: it is Subsec instead of SubSec
|
|
152
|
+
["XMP-exif:SubsecTimeDigitized"],
|
|
153
|
+
["XMP-exif:OffsetTimeDigitized"],
|
|
154
|
+
)
|
|
155
|
+
if dt is not None:
|
|
156
|
+
return dt
|
|
157
|
+
|
|
158
|
+
# Image DateTime: 0x0132 ModifyDate in ExifTool (called DateTime by the EXIF spec.)
|
|
159
|
+
# EXIF SubSecTime: 0x9290 (fractional seconds for ModifyDate)
|
|
160
|
+
# EXIF OffsetTime: 0x9010 (time zone for ModifyDate)
|
|
161
|
+
dt = self._extract_exif_datetime(
|
|
162
|
+
["XMP-exif:ModifyDate"],
|
|
163
|
+
# NOTE: this tag might not exist in XMP
|
|
164
|
+
["XMP-exif:SubsecTime"],
|
|
165
|
+
["XMP-exif:OffsetTime"],
|
|
166
|
+
)
|
|
167
|
+
if dt is not None:
|
|
168
|
+
return dt
|
|
169
|
+
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
def extract_exif_datetime(self) -> T.Optional[datetime.datetime]:
|
|
173
|
+
# EXIF DateTimeOriginal: 0x9003 (date/time when original image was taken)
|
|
174
|
+
# EXIF SubSecTimeOriginal: 0x9291 (fractional seconds for DateTimeOriginal)
|
|
175
|
+
# EXIF OffsetTimeOriginal: 0x9011 (time zone for DateTimeOriginal)
|
|
176
|
+
dt = self._extract_exif_datetime(
|
|
177
|
+
["ExifIFD:DateTimeOriginal"],
|
|
178
|
+
["ExifIFD:SubSecTimeOriginal"],
|
|
179
|
+
["ExifIFD:OffsetTimeOriginal"],
|
|
180
|
+
)
|
|
181
|
+
if dt is not None:
|
|
182
|
+
return dt
|
|
183
|
+
|
|
184
|
+
# EXIF DateTimeDigitized: 0x9004 CreateDate in ExifTool (called DateTimeDigitized by the EXIF spec.)
|
|
185
|
+
# EXIF SubSecTimeDigitized: 0x9292 (fractional seconds for CreateDate)
|
|
186
|
+
# EXIF OffsetTimeDigitized: 0x9012 (time zone for CreateDate)
|
|
187
|
+
dt = self._extract_exif_datetime(
|
|
188
|
+
["ExifIFD:CreateDate"],
|
|
189
|
+
["ExifIFD:SubSecTimeDigitized"],
|
|
190
|
+
["ExifIFD:OffsetTimeDigitized"],
|
|
191
|
+
)
|
|
192
|
+
if dt is not None:
|
|
193
|
+
return dt
|
|
194
|
+
|
|
195
|
+
# Image DateTime: 0x0132 ModifyDate in ExifTool (called DateTime by the EXIF spec.)
|
|
196
|
+
# EXIF SubSecTime: 0x9290 (fractional seconds for ModifyDate)
|
|
197
|
+
# EXIF OffsetTime: 0x9010 (time zone for ModifyDate)
|
|
198
|
+
dt = self._extract_exif_datetime(
|
|
199
|
+
["ExifIFD:ModifyDate", "IFD0:ModifyDate", "IFD1:ModifyDate"],
|
|
200
|
+
["ExifIFD:SubSecTime"],
|
|
201
|
+
["ExifIFD:OffsetTime"],
|
|
202
|
+
)
|
|
203
|
+
if dt is not None:
|
|
204
|
+
return dt
|
|
205
|
+
|
|
206
|
+
return None
|
|
207
|
+
|
|
208
|
+
def extract_capture_time(self) -> T.Optional[datetime.datetime]:
|
|
209
|
+
"""
|
|
210
|
+
Extract capture time from EXIF DateTime tags
|
|
211
|
+
"""
|
|
212
|
+
# Prefer GPS datetime over EXIF timestamp
|
|
213
|
+
# NOTE: GPS datetime precision is usually 1 second, but this case is handled by the subsecond interpolation
|
|
214
|
+
try:
|
|
215
|
+
dt = self.extract_gps_datetime()
|
|
216
|
+
except (ValueError, TypeError, ZeroDivisionError):
|
|
217
|
+
dt = None
|
|
218
|
+
if dt is not None:
|
|
219
|
+
return dt
|
|
220
|
+
|
|
221
|
+
try:
|
|
222
|
+
dt = self.extract_gps_datetime_from_xmp()
|
|
223
|
+
except (ValueError, TypeError, ZeroDivisionError):
|
|
224
|
+
dt = None
|
|
225
|
+
if dt is not None:
|
|
226
|
+
return dt
|
|
227
|
+
|
|
228
|
+
dt = self.extract_exif_datetime()
|
|
229
|
+
if dt is not None:
|
|
230
|
+
return dt
|
|
231
|
+
|
|
232
|
+
dt = self.extract_exif_datetime_from_xmp()
|
|
233
|
+
if dt is not None:
|
|
234
|
+
return dt
|
|
235
|
+
|
|
236
|
+
return None
|
|
237
|
+
|
|
238
|
+
def extract_direction(self) -> T.Optional[float]:
|
|
239
|
+
"""
|
|
240
|
+
Extract image direction (i.e. compass, heading, bearing)
|
|
241
|
+
"""
|
|
242
|
+
# https://www.awaresystems.be/imaging/tiff/tifftags/privateifd/gps/gpsimgdirectionref.html
|
|
243
|
+
return self._extract_alternative_fields(
|
|
244
|
+
[
|
|
245
|
+
"GPS:GPSImgDirection",
|
|
246
|
+
"GPS:GPSTrack",
|
|
247
|
+
],
|
|
248
|
+
float,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
def extract_lon_lat(self) -> T.Optional[T.Tuple[float, float]]:
|
|
252
|
+
lon_lat = self._extract_lon_lat("GPS:GPSLongitude", "GPS:GPSLatitude")
|
|
253
|
+
if lon_lat is not None:
|
|
254
|
+
return lon_lat
|
|
255
|
+
|
|
256
|
+
lon_lat = self._extract_lon_lat(
|
|
257
|
+
"Composite:GPSLongitude", "Composite:GPSLatitude"
|
|
258
|
+
)
|
|
259
|
+
if lon_lat is not None:
|
|
260
|
+
return lon_lat
|
|
261
|
+
|
|
262
|
+
return None
|
|
263
|
+
|
|
264
|
+
def _extract_lon_lat(
|
|
265
|
+
self, lon_tag: str, lat_tag: str
|
|
266
|
+
) -> T.Optional[T.Tuple[float, float]]:
|
|
267
|
+
lon = self._extract_alternative_fields(
|
|
268
|
+
[lon_tag],
|
|
269
|
+
float,
|
|
270
|
+
)
|
|
271
|
+
if lon is None:
|
|
272
|
+
return None
|
|
273
|
+
ref = self._extract_alternative_fields([lon_tag + "Ref"], str)
|
|
274
|
+
if ref and ref.upper() in ["WEST", "W"]:
|
|
275
|
+
lon = -1 * lon
|
|
276
|
+
|
|
277
|
+
lat = self._extract_alternative_fields(
|
|
278
|
+
[lat_tag],
|
|
279
|
+
float,
|
|
280
|
+
)
|
|
281
|
+
if lat is None:
|
|
282
|
+
return None
|
|
283
|
+
ref = self._extract_alternative_fields([lat_tag + "Ref"], str)
|
|
284
|
+
if ref and ref.upper() in ["SOUTH", "S"]:
|
|
285
|
+
lat = -1 * lat
|
|
286
|
+
|
|
287
|
+
return lon, lat
|
|
288
|
+
|
|
289
|
+
def extract_make(self) -> T.Optional[str]:
|
|
290
|
+
"""
|
|
291
|
+
Extract camera make
|
|
292
|
+
"""
|
|
293
|
+
make = self._extract_alternative_fields(
|
|
294
|
+
[
|
|
295
|
+
"IFD0:Make",
|
|
296
|
+
"IFD1:Make",
|
|
297
|
+
"ExifIFD:Make",
|
|
298
|
+
"ExifIFD:LensMake",
|
|
299
|
+
"XMP-exif:Make",
|
|
300
|
+
"XMP-exifEX:LensMake",
|
|
301
|
+
],
|
|
302
|
+
str,
|
|
303
|
+
)
|
|
304
|
+
if make is None:
|
|
305
|
+
return None
|
|
306
|
+
return make.strip()
|
|
307
|
+
|
|
308
|
+
def extract_model(self) -> T.Optional[str]:
|
|
309
|
+
"""
|
|
310
|
+
Extract camera model
|
|
311
|
+
"""
|
|
312
|
+
model = self._extract_alternative_fields(
|
|
313
|
+
[
|
|
314
|
+
"IFD0:Model",
|
|
315
|
+
"IFD1:Model",
|
|
316
|
+
"ExifIFD:Model",
|
|
317
|
+
"GoPro:Model",
|
|
318
|
+
"ExifIFD:LensModel",
|
|
319
|
+
"XMP-exif:Model",
|
|
320
|
+
"XMP-exifEX:LensModel",
|
|
321
|
+
],
|
|
322
|
+
str,
|
|
323
|
+
)
|
|
324
|
+
if model is None:
|
|
325
|
+
return None
|
|
326
|
+
return model.strip()
|
|
327
|
+
|
|
328
|
+
def extract_width(self) -> T.Optional[int]:
|
|
329
|
+
"""
|
|
330
|
+
Extract image width in pixels
|
|
331
|
+
"""
|
|
332
|
+
return self._extract_alternative_fields(
|
|
333
|
+
[
|
|
334
|
+
"File:ImageWidth",
|
|
335
|
+
"ExifIFD:ExifImageWidth",
|
|
336
|
+
"IFD0:ExifImageWidth",
|
|
337
|
+
"IFD1:ExifImageWidth",
|
|
338
|
+
"XMP-exif:ExifImageWidth",
|
|
339
|
+
],
|
|
340
|
+
int,
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
def extract_height(self) -> T.Optional[int]:
|
|
344
|
+
"""
|
|
345
|
+
Extract image height in pixels
|
|
346
|
+
"""
|
|
347
|
+
return self._extract_alternative_fields(
|
|
348
|
+
[
|
|
349
|
+
"File:ImageHeight",
|
|
350
|
+
"ExifIFD:ExifImageHeight",
|
|
351
|
+
"IFD0:ExifImageHeight",
|
|
352
|
+
"IFD1:ExifImageHeight",
|
|
353
|
+
"XMP-exif:ExifImageHeight",
|
|
354
|
+
],
|
|
355
|
+
int,
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
def extract_orientation(self) -> int:
|
|
359
|
+
"""
|
|
360
|
+
Extract image orientation
|
|
361
|
+
"""
|
|
362
|
+
orientation = self._extract_alternative_fields(
|
|
363
|
+
[
|
|
364
|
+
"ExifIFD:Orientation",
|
|
365
|
+
"IFD0:Orientation",
|
|
366
|
+
"IFD1:Orientation",
|
|
367
|
+
"XMP-exif:Orientation",
|
|
368
|
+
"XMP-tiff:Orientation",
|
|
369
|
+
],
|
|
370
|
+
int,
|
|
371
|
+
)
|
|
372
|
+
if orientation is None:
|
|
373
|
+
return 1
|
|
374
|
+
if orientation not in range(1, 9):
|
|
375
|
+
return 1
|
|
376
|
+
return orientation
|
|
377
|
+
|
|
378
|
+
def _extract_alternative_fields(
|
|
379
|
+
self,
|
|
380
|
+
fields: T.Sequence[str],
|
|
381
|
+
field_type: T.Type[_FIELD_TYPE],
|
|
382
|
+
) -> T.Optional[_FIELD_TYPE]:
|
|
383
|
+
for field in fields:
|
|
384
|
+
value = self.etree.findtext(field, namespaces=EXIFTOOL_NAMESPACES)
|
|
385
|
+
if value is None:
|
|
386
|
+
continue
|
|
387
|
+
if value is None:
|
|
388
|
+
continue
|
|
389
|
+
if field_type is int:
|
|
390
|
+
try:
|
|
391
|
+
return T.cast(_FIELD_TYPE, int(value))
|
|
392
|
+
except (ValueError, TypeError):
|
|
393
|
+
pass
|
|
394
|
+
elif field_type is float:
|
|
395
|
+
try:
|
|
396
|
+
return T.cast(_FIELD_TYPE, float(value))
|
|
397
|
+
except (ValueError, TypeError):
|
|
398
|
+
pass
|
|
399
|
+
elif field_type is str:
|
|
400
|
+
try:
|
|
401
|
+
return T.cast(_FIELD_TYPE, str(value))
|
|
402
|
+
except (ValueError, TypeError):
|
|
403
|
+
pass
|
|
404
|
+
else:
|
|
405
|
+
raise ValueError(f"Invalid field type {field_type}")
|
|
406
|
+
return None
|