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

1#!/usr/bin/env python3 

2 

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 

11 

12 # Import standard modules ... 

13 import datetime 

14 import math 

15 

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 

21 

22 # Import sub-functions ... 

23 from ..consts import RESOLUTION_OF_EARTH 

24 

25 # Create default dictionary answer ... 

26 ans = {} 

27 

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) 

32 

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 

39 

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 

53 

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 # [°] 

62 

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 

74 

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 # [°] 

83 

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 

92 

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] 

97 

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 ) 

125 

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) 

135 

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 

140 

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 

150 

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] 

155 

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] 

163 

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()) 

168 

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}°).' 

208 

209 # Return answer ... 

210 if not ans: 

211 return False 

212 return ans