Coverage for pyguymer3/image/load_GPS_EXIF1.py: 1%
111 statements
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-08 18:47 +0000
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-08 18:47 +0000
1#!/usr/bin/env python3
3# Define function ...
4def load_GPS_EXIF1(
5 fname,
6 /,
7):
8 # NOTE: The following web pages were helpful:
9 # * https://gist.github.com/snakeye/fdc372dbf11370fe29eb
10 # * https://sno.phy.queensu.ca/~phil/exiftool/TagNames/GPS.html
12 # Import standard modules ...
13 import datetime
14 import math
16 # Import special modules ...
17 try:
18 import exifread
19 except:
20 raise Exception("\"exifread\" is not installed; run \"pip install --user ExifRead\"") from None
22 # Import sub-functions ...
23 from ..consts import RESOLUTION_OF_EARTH
25 # Create default dictionary answer ...
26 ans = {}
28 # Open RAW file read-only ...
29 with open(fname, "rb") as fObj:
30 # Load EXIF tags ...
31 tags = exifread.process_file(fObj, details = False)
33 # Check that there are EXIF tags ...
34 gps = False
35 for key in tags:
36 if key.startswith("GPS "):
37 gps = True
38 break
40 # Check that there are EXIF tags ...
41 if gps:
42 # Check that the required tags are preset ...
43 if "GPS GPSLongitude" in tags and "GPS GPSLongitudeRef" in tags:
44 # Extract longitude ...
45 d = float(tags["GPS GPSLongitude"].values[0].num) / float(tags["GPS GPSLongitude"].values[0].den) # [°]
46 m = float(tags["GPS GPSLongitude"].values[1].num) / float(tags["GPS GPSLongitude"].values[1].den) # [min]
47 s = float(tags["GPS GPSLongitude"].values[2].num) / float(tags["GPS GPSLongitude"].values[2].den) # [sec]
48 ans["lon"] = d + (m / 60.0) + (s / 3600.0) # [°]
49 if tags["GPS GPSLongitudeRef"].values[0] == "W":
50 ans["lon"] = 0.0 - ans["lon"] # [°]
51 elif tags["GPS GPSLongitudeRef"].values[0] != "E":
52 raise Exception("the longitude reference is unexpected", tags["GPS GPSLongitudeRef"].values) from None
54 # Deduce longitude precision ...
55 ans["lon_prec"] = 0.0 # [°]
56 if tags["GPS GPSLongitude"].values[0].den != 1:
57 ans["lon_prec"] += 1.0 / float(tags["GPS GPSLongitude"].values[0].den) # [°]
58 if tags["GPS GPSLongitude"].values[1].den != 1:
59 ans["lon_prec"] += 1.0 / float(tags["GPS GPSLongitude"].values[1].den) / 60.0 # [°]
60 if tags["GPS GPSLongitude"].values[2].den != 1:
61 ans["lon_prec"] += 1.0 / float(tags["GPS GPSLongitude"].values[2].den) / 3600.0 # [°]
63 # Check that the required tags are preset ...
64 if "GPS GPSLatitude" in tags and "GPS GPSLatitudeRef" in tags:
65 # Extract latitude ...
66 d = float(tags["GPS GPSLatitude"].values[0].num) / float(tags["GPS GPSLatitude"].values[0].den) # [°]
67 m = float(tags["GPS GPSLatitude"].values[1].num) / float(tags["GPS GPSLatitude"].values[1].den) # [min]
68 s = float(tags["GPS GPSLatitude"].values[2].num) / float(tags["GPS GPSLatitude"].values[2].den) # [sec]
69 ans["lat"] = d + (m / 60.0) + (s / 3600.0) # [°]
70 if tags["GPS GPSLatitudeRef"].values[0] == "S":
71 ans["lat"] = 0.0 - ans["lat"] # [°]
72 elif tags["GPS GPSLatitudeRef"].values[0] != "N":
73 raise Exception("the latitude reference is unexpected", tags["GPS GPSLatitudeRef"].values) from None
75 # Deduce latitude precision ...
76 ans["lat_prec"] = 0.0 # [°]
77 if tags["GPS GPSLatitude"].values[0].den != 1:
78 ans["lat_prec"] += 1.0 / float(tags["GPS GPSLatitude"].values[0].den) # [°]
79 if tags["GPS GPSLatitude"].values[1].den != 1:
80 ans["lat_prec"] += 1.0 / float(tags["GPS GPSLatitude"].values[1].den) / 60.0 # [°]
81 if tags["GPS GPSLatitude"].values[2].den != 1:
82 ans["lat_prec"] += 1.0 / float(tags["GPS GPSLatitude"].values[2].den) / 3600.0 # [°]
84 # Check that the required tags are preset ...
85 if "GPS GPSAltitude" in tags and "GPS GPSAltitudeRef" in tags:
86 # Extract altitude ...
87 ans["alt"] = float(tags["GPS GPSAltitude"].values[0].num) / float(tags["GPS GPSAltitude"].values[0].den) # [m]
88 if tags["GPS GPSAltitudeRef"].values[0] == 1:
89 ans["alt"] = 0.0 - ans["alt"] # [m]
90 elif tags["GPS GPSAltitudeRef"].values[0] != 0:
91 raise Exception("the altitude reference is unexpected", tags["GPS GPSAltitudeRef"].values) from None
93 # Deduce altitude precision ...
94 ans["alt_prec"] = 0.0 # [m]
95 if tags["GPS GPSAltitude"].values[0].den != 1:
96 ans["alt_prec"] += 1.0 / float(tags["GPS GPSAltitude"].values[0].den) # [m]
98 # Check that the required tags are preset ...
99 if "GPS GPSDate" in tags and "GPS GPSTimeStamp" in tags:
100 # Extract date/time and merge into one (TZ-aware) object (
101 # correcting mistakes that shouldn't exist) ...
102 tmp1 = tags["GPS GPSDate"].values.split(":")
103 ye = int(tmp1[0]) # [year]
104 mo = int(tmp1[1]) # [month]
105 da = int(tmp1[2]) # [day]
106 hr = int(tags["GPS GPSTimeStamp"].values[0].num) # [hour]
107 mi = int(tags["GPS GPSTimeStamp"].values[1].num) # [minute]
108 tmp2 = float(tags["GPS GPSTimeStamp"].values[2].num) / float(tags["GPS GPSTimeStamp"].values[2].den) # [s]
109 se = int(math.floor(tmp2)) # [s]
110 us = int(1.0e6 * (tmp2 - se)) # [μs]
111 if hr > 23:
112 # HACK: This particular gem is due to my Motorola Moto G3
113 # smartphone.
114 hr = hr % 24 # [hour]
115 ans["datetime"] = datetime.datetime(
116 year = ye,
117 month = mo,
118 day = da,
119 hour = hr,
120 minute = mi,
121 second = se,
122 microsecond = us,
123 tzinfo = datetime.UTC,
124 )
126 # Deduce time precision ...
127 tmp = 0.0 # [s]
128 if tags["GPS GPSTimeStamp"].values[0].den != 1:
129 tmp += 3600.0 / float(tags["GPS GPSTimeStamp"].values[0].den) # [s]
130 if tags["GPS GPSTimeStamp"].values[1].den != 1:
131 tmp += 60.0 / float(tags["GPS GPSTimeStamp"].values[1].den) # [s]
132 if tags["GPS GPSTimeStamp"].values[2].den != 1:
133 tmp += 1.0 / float(tags["GPS GPSTimeStamp"].values[2].den) # [s]
134 ans["time_prec"] = datetime.timedelta(seconds = tmp)
136 # Check that the required tags are preset ...
137 if "GPS GPSMapDatum" in tags:
138 # Extract map datum ...
139 ans["datum"] = tags["GPS GPSMapDatum"].values
141 # Check that the required tags are preset ...
142 if "GPS GPSMeasureMode" in tags:
143 # Extract measure mode ...
144 if tags["GPS GPSMeasureMode"].values[0] == "2":
145 ans["mode"] = "2D"
146 elif tags["GPS GPSMeasureMode"].values[0] == "3":
147 ans["mode"] = "3D"
148 else:
149 raise Exception("the mode is unexpected", tags["GPS GPSMeasureMode"].values) from None
151 # Check that the required tags are preset ...
152 if "GPS GPSDOP" in tags:
153 # Extract dilution of precision ...
154 ans["dop"] = float(tags["GPS GPSDOP"].values[0].num) / float(tags["GPS GPSDOP"].values[0].den) # [ratio]
156 # Estimate the location error ...
157 # NOTE: The longitude and latitude precisions are added in
158 # quadrature and then multiplied by the dilution of
159 # precision to give an estimate of the error, in degrees,
160 # which is then converted to metres.
161 ans["loc_err"] = ans["dop"] * math.hypot(ans["lon_prec"], ans["lat_prec"]) # [°]
162 ans["loc_err"] *= RESOLUTION_OF_EARTH # [m]
164 # Estimate the time error ...
165 # NOTE: The time precision is multiplied by the dilution of
166 # precision to give an estimate of the error
167 ans["time_err"] = datetime.timedelta(seconds = ans["dop"] * ans["time_prec"].total_seconds())
169 # Check that there is location information ...
170 if "lon" in ans and "lat" in ans:
171 # Check that there is date/time information ...
172 if "datetime" in ans:
173 # Check that there is altitude information ...
174 if "alt" in ans:
175 # Check that there is error information ...
176 if "loc_err" in ans and "time_err" in ans:
177 # Make a pretty string ...
178 ans["pretty"] = f'GPS fix returned ({ans["lon"]:.6f}°, {ans["lat"]:.6f}°, {ans["alt"]:.1f}m ASL) ± {ans["loc_err"]:.1f}m at \"{ans["datetime"].isoformat(sep = " ", timespec = "microseconds")}\" ± {ans["time_err"].total_seconds():.3f}s.'
179 else:
180 # Make a pretty string ...
181 ans["pretty"] = f'GPS fix returned ({ans["lon"]:.6f}°, {ans["lat"]:.6f}°, {ans["alt"]:.1f}m ASL) at \"{ans["datetime"].isoformat(sep = " ", timespec = "microseconds")}\".'
182 else:
183 # Check that there is error information ...
184 if "loc_err" in ans and "time_err" in ans:
185 # Make a pretty string ...
186 ans["pretty"] = f'GPS fix returned ({ans["lon"]:.6f}°, {ans["lat"]:.6f}°) ± {ans["loc_err"]:.1f}m at \"{ans["datetime"].isoformat(sep = " ", timespec = "microseconds")}\" ± {ans["time_err"].total_seconds():.3f}s.'
187 else:
188 # Make a pretty string ...
189 ans["pretty"] = f'GPS fix returned ({ans["lon"]:.6f}°, {ans["lat"]:.6f}°) at \"{ans["datetime"].isoformat(sep = " ", timespec = "microseconds")}\".'
190 else:
191 # Check that there is altitude information ...
192 if "alt" in ans:
193 # Check that there is error information ...
194 if "loc_err" in ans:
195 # Make a pretty string ...
196 ans["pretty"] = f'GPS fix returned ({ans["lon"]:.6f}°, {ans["lat"]:.6f}°, {ans["alt"]:.1f}m ASL) ± {ans["loc_err"]:.1f}m.'
197 else:
198 # Make a pretty string ...
199 ans["pretty"] = f'GPS fix returned ({ans["lon"]:.6f}°, {ans["lat"]:.6f}°, {ans["alt"]:.1f}m ASL).'
200 else:
201 # Check that there is error information ...
202 if "loc_err" in ans:
203 # Make a pretty string ...
204 ans["pretty"] = f'GPS fix returned ({ans["lon"]:.6f}°, {ans["lat"]:.6f}°) ± {ans["loc_err"]:.1f}m.'
205 else:
206 # Make a pretty string ...
207 ans["pretty"] = f'GPS fix returned ({ans["lon"]:.6f}°, {ans["lat"]:.6f}°).'
209 # Return answer ...
210 if not ans:
211 return False
212 return ans