Skip to content

Commit b29d196

Browse files
committed
fix: Write fully reduced Exif rationals with full precision
Add debug (verbose) messages on written tags
1 parent 83a98ce commit b29d196

File tree

2 files changed

+66
-39
lines changed

2 files changed

+66
-39
lines changed

mapillary_tools/exif_write.py

Lines changed: 61 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import json
77
import logging
88
import math
9+
from fractions import Fraction
910
from pathlib import Path
1011

1112
import piexif
@@ -29,16 +30,19 @@ def __init__(self, filename_or_bytes: Path | bytes) -> None:
2930

3031
@staticmethod
3132
def decimal_to_dms(
32-
value: float, precision: int
33-
) -> tuple[tuple[float, int], tuple[float, int], tuple[float, int]]:
34-
"""
35-
Convert decimal position to degrees, minutes, seconds in a fromat supported by EXIF
36-
"""
37-
deg = math.floor(value)
38-
min = math.floor((value - deg) * 60)
39-
sec = math.floor((value - deg - min / 60) * 3600 * precision)
40-
41-
return (deg, 1), (min, 1), (sec, precision)
33+
value: float,
34+
) -> tuple[tuple[int, int], tuple[int, int], tuple[int, int]]:
35+
"""Convert decimal position to Exif degrees, minutes, and seconds rationals"""
36+
37+
deg: int = int(value)
38+
min: int = int(value := (value - deg) * 60)
39+
sec: float = (value - min) * 60
40+
41+
return (
42+
(deg, 1),
43+
(min, 1),
44+
(Fraction.from_float(sec).limit_denominator().as_integer_ratio()),
45+
)
4246

4347
def add_image_description(self, data: dict) -> None:
4448
"""Add a dict to image description."""
@@ -83,41 +87,69 @@ def add_gps_datetime(self, dt: datetime.datetime) -> None:
8387
self._ef["GPS"][piexif.GPSIFD.GPSTimeStamp] = (
8488
(dt.hour, 1),
8589
(dt.minute, 1),
86-
# num / den = (dt.second * 1e6 + dt.microsecond) / 1e6
87-
(int(dt.second * 1e6 + dt.microsecond), int(1e6)),
90+
(
91+
Fraction.from_float(dt.second + dt.microsecond / 1e6)
92+
.limit_denominator()
93+
.as_integer_ratio()
94+
),
8895
)
89-
90-
def add_lat_lon(self, lat: float, lon: float, precision: float = 1e7) -> None:
96+
if LOG.isEnabledFor(logging.DEBUG):
97+
LOG.debug(
98+
'GPSDateStamp: "%s"\tGPSTimeStamp: %s',
99+
self._ef["GPS"][piexif.GPSIFD.GPSDateStamp],
100+
self._ef["GPS"][piexif.GPSIFD.GPSTimeStamp],
101+
)
102+
103+
def add_lat_lon(self, lat: float, lon: float) -> None:
91104
"""Add lat, lon to gps (lat, lon in float)."""
105+
92106
self._ef["GPS"][piexif.GPSIFD.GPSLatitudeRef] = "N" if lat > 0 else "S"
107+
self._ef["GPS"][piexif.GPSIFD.GPSLatitude] = ExifEdit.decimal_to_dms(
108+
math.fabs(lat)
109+
)
93110
self._ef["GPS"][piexif.GPSIFD.GPSLongitudeRef] = "E" if lon > 0 else "W"
94111
self._ef["GPS"][piexif.GPSIFD.GPSLongitude] = ExifEdit.decimal_to_dms(
95-
abs(lon), int(precision)
96-
)
97-
self._ef["GPS"][piexif.GPSIFD.GPSLatitude] = ExifEdit.decimal_to_dms(
98-
abs(lat), int(precision)
112+
math.fabs(lon)
99113
)
114+
if LOG.isEnabledFor(logging.DEBUG):
115+
LOG.debug(
116+
"GPSLatitude: %s\tGPSLongitude: %s",
117+
self._ef["GPS"][piexif.GPSIFD.GPSLatitude],
118+
self._ef["GPS"][piexif.GPSIFD.GPSLongitude],
119+
)
120+
121+
def add_altitude(self, altitude: float) -> None:
122+
"""Add altitude."""
100123

101-
def add_altitude(self, altitude: float, precision: int = 100) -> None:
102-
"""Add altitude (pre is the precision)."""
103124
ref = 0 if altitude > 0 else 1
104125
self._ef["GPS"][piexif.GPSIFD.GPSAltitude] = (
105-
int(abs(altitude) * precision),
106-
precision,
126+
Fraction.from_float(math.fabs(altitude))
127+
.limit_denominator()
128+
.as_integer_ratio()
107129
)
108130
self._ef["GPS"][piexif.GPSIFD.GPSAltitudeRef] = ref
109-
110-
def add_direction(
111-
self, direction: float, ref: str = "T", precision: int = 100
112-
) -> None:
131+
if LOG.isEnabledFor(logging.DEBUG):
132+
LOG.debug(
133+
'GPSAltitudeRef: "%s"\tGPSAltitude: %s',
134+
self._ef["GPS"][piexif.GPSIFD.GPSAltitudeRef],
135+
self._ef["GPS"][piexif.GPSIFD.GPSAltitude],
136+
)
137+
138+
def add_direction(self, direction: float, ref: str = "T") -> None:
113139
"""Add image direction."""
140+
114141
# normalize direction
115-
direction = direction % 360.0
142+
direction = math.fmod(direction, 360.0)
116143
self._ef["GPS"][piexif.GPSIFD.GPSImgDirection] = (
117-
int(abs(direction) * precision),
118-
precision,
144+
Fraction.from_float(direction).limit_denominator().as_integer_ratio()
119145
)
120146
self._ef["GPS"][piexif.GPSIFD.GPSImgDirectionRef] = ref
147+
if LOG.isEnabledFor(logging.DEBUG):
148+
LOG.debug(
149+
'GPSImgDirectionRef: "%s"\tGPSImgDirection: %s',
150+
self._ef["GPS"][piexif.GPSIFD.GPSImgDirectionRef],
151+
self._ef["GPS"][piexif.GPSIFD.GPSImgDirection],
152+
)
121153

122154
def add_make(self, make: str) -> None:
123155
if not make:

tests/unit/test_exifedit.py

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,10 @@ def add_date_time_original_general(test_obj, filename: Path):
6868
def add_lat_lon_general(test_obj, filename):
6969
test_latitude = 50.5475894785
7070
test_longitude = 15.595866685
71-
precision = 1e7
7271

7372
empty_exifedit = ExifEdit(filename)
7473

75-
empty_exifedit.add_lat_lon(test_latitude, test_longitude, precision)
74+
empty_exifedit.add_lat_lon(test_latitude, test_longitude)
7675
empty_exifedit.write(EMPTY_EXIF_FILE_TEST)
7776

7877
exif_data = ExifRead(EMPTY_EXIF_FILE_TEST)
@@ -82,11 +81,10 @@ def add_lat_lon_general(test_obj, filename):
8281

8382
def add_altitude_general(test_obj, filename: Path):
8483
test_altitude = 15.5
85-
test_altitude_precision = 100
8684

8785
empty_exifedit = ExifEdit(filename)
8886

89-
empty_exifedit.add_altitude(test_altitude, test_altitude_precision)
87+
empty_exifedit.add_altitude(test_altitude)
9088
empty_exifedit.write(EMPTY_EXIF_FILE_TEST)
9189

9290
exif_data = ExifRead(EMPTY_EXIF_FILE_TEST)
@@ -118,12 +116,11 @@ def add_repeatedly_time_original_general(test_obj, filename):
118116
def add_direction_general(test_obj, filename):
119117
test_direction = 1
120118
test_direction_ref = "T"
121-
test_direction_precision = 100
122119

123120
empty_exifedit = ExifEdit(filename)
124121

125122
empty_exifedit.add_direction(
126-
test_direction, test_direction_ref, test_direction_precision
123+
test_direction, test_direction_ref
127124
)
128125
empty_exifedit.write(EMPTY_EXIF_FILE_TEST)
129126

@@ -181,11 +178,10 @@ def test_add_repeatedly_time_original(self):
181178

182179
def test_add_time_original_to_existing_exif(self):
183180
test_altitude = 15.5
184-
test_altitude_precision = 100
185181

186182
empty_exifedit = ExifEdit(EMPTY_EXIF_FILE_TEST)
187183

188-
empty_exifedit.add_altitude(test_altitude, test_altitude_precision)
184+
empty_exifedit.add_altitude(test_altitude)
189185
empty_exifedit.write(EMPTY_EXIF_FILE_TEST)
190186

191187
test_datetime = datetime.datetime(2016, 9, 30, 8, 29, 26, 249000)
@@ -205,11 +201,10 @@ def test_add_time_original_to_existing_exif(self):
205201
def test_add_negative_lat_lon(self):
206202
test_latitude = -50.5
207203
test_longitude = -15.5
208-
precision = 1e7
209204

210205
empty_exifedit = ExifEdit(EMPTY_EXIF_FILE_TEST)
211206

212-
empty_exifedit.add_lat_lon(test_latitude, test_longitude, precision)
207+
empty_exifedit.add_lat_lon(test_latitude, test_longitude)
213208
empty_exifedit.write(EMPTY_EXIF_FILE_TEST)
214209

215210
exif_data = ExifRead(EMPTY_EXIF_FILE_TEST)

0 commit comments

Comments
 (0)