Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 61 additions & 29 deletions mapillary_tools/exif_write.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import json
import logging
import math
from fractions import Fraction
from pathlib import Path

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

@staticmethod
def decimal_to_dms(
value: float, precision: int
) -> tuple[tuple[float, int], tuple[float, int], tuple[float, int]]:
"""
Convert decimal position to degrees, minutes, seconds in a fromat supported by EXIF
"""
deg = math.floor(value)
min = math.floor((value - deg) * 60)
sec = math.floor((value - deg - min / 60) * 3600 * precision)

return (deg, 1), (min, 1), (sec, precision)
value: float,
) -> tuple[tuple[int, int], tuple[int, int], tuple[int, int]]:
"""Convert decimal position to Exif degrees, minutes, and seconds rationals"""

deg: int = int(value)
min: int = int(value := (value - deg) * 60)
sec: float = (value - min) * 60

return (
(deg, 1),
(min, 1),
(Fraction.from_float(sec).limit_denominator().as_integer_ratio()),
)

def add_image_description(self, data: dict) -> None:
"""Add a dict to image description."""
Expand Down Expand Up @@ -83,41 +87,69 @@ def add_gps_datetime(self, dt: datetime.datetime) -> None:
self._ef["GPS"][piexif.GPSIFD.GPSTimeStamp] = (
(dt.hour, 1),
(dt.minute, 1),
# num / den = (dt.second * 1e6 + dt.microsecond) / 1e6
(int(dt.second * 1e6 + dt.microsecond), int(1e6)),
(
Fraction.from_float(dt.second + dt.microsecond / 1e6)
.limit_denominator()
.as_integer_ratio()
),
)

def add_lat_lon(self, lat: float, lon: float, precision: float = 1e7) -> None:
if LOG.isEnabledFor(logging.DEBUG):
LOG.debug(
'GPSDateStamp: "%s"\tGPSTimeStamp: %s',
self._ef["GPS"][piexif.GPSIFD.GPSDateStamp],
self._ef["GPS"][piexif.GPSIFD.GPSTimeStamp],
)

def add_lat_lon(self, lat: float, lon: float) -> None:
"""Add lat, lon to gps (lat, lon in float)."""

self._ef["GPS"][piexif.GPSIFD.GPSLatitudeRef] = "N" if lat > 0 else "S"
self._ef["GPS"][piexif.GPSIFD.GPSLatitude] = ExifEdit.decimal_to_dms(
math.fabs(lat)
)
self._ef["GPS"][piexif.GPSIFD.GPSLongitudeRef] = "E" if lon > 0 else "W"
self._ef["GPS"][piexif.GPSIFD.GPSLongitude] = ExifEdit.decimal_to_dms(
abs(lon), int(precision)
)
self._ef["GPS"][piexif.GPSIFD.GPSLatitude] = ExifEdit.decimal_to_dms(
abs(lat), int(precision)
math.fabs(lon)
)
if LOG.isEnabledFor(logging.DEBUG):
LOG.debug(
"GPSLatitude: %s\tGPSLongitude: %s",
self._ef["GPS"][piexif.GPSIFD.GPSLatitude],
self._ef["GPS"][piexif.GPSIFD.GPSLongitude],
)

def add_altitude(self, altitude: float) -> None:
"""Add altitude."""

def add_altitude(self, altitude: float, precision: int = 100) -> None:
"""Add altitude (pre is the precision)."""
ref = 0 if altitude > 0 else 1
self._ef["GPS"][piexif.GPSIFD.GPSAltitude] = (
int(abs(altitude) * precision),
precision,
Fraction.from_float(math.fabs(altitude))
.limit_denominator()
.as_integer_ratio()
)
self._ef["GPS"][piexif.GPSIFD.GPSAltitudeRef] = ref

def add_direction(
self, direction: float, ref: str = "T", precision: int = 100
) -> None:
if LOG.isEnabledFor(logging.DEBUG):
LOG.debug(
'GPSAltitudeRef: "%s"\tGPSAltitude: %s',
self._ef["GPS"][piexif.GPSIFD.GPSAltitudeRef],
self._ef["GPS"][piexif.GPSIFD.GPSAltitude],
)

def add_direction(self, direction: float, ref: str = "T") -> None:
"""Add image direction."""

# normalize direction
direction = direction % 360.0
direction = math.fmod(direction, 360.0)
self._ef["GPS"][piexif.GPSIFD.GPSImgDirection] = (
int(abs(direction) * precision),
precision,
Fraction.from_float(direction).limit_denominator().as_integer_ratio()
)
self._ef["GPS"][piexif.GPSIFD.GPSImgDirectionRef] = ref
if LOG.isEnabledFor(logging.DEBUG):
LOG.debug(
'GPSImgDirectionRef: "%s"\tGPSImgDirection: %s',
self._ef["GPS"][piexif.GPSIFD.GPSImgDirectionRef],
self._ef["GPS"][piexif.GPSIFD.GPSImgDirection],
)

def add_make(self, make: str) -> None:
if not make:
Expand Down
17 changes: 5 additions & 12 deletions tests/unit/test_exifedit.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,10 @@ def add_date_time_original_general(test_obj, filename: Path):
def add_lat_lon_general(test_obj, filename):
test_latitude = 50.5475894785
test_longitude = 15.595866685
precision = 1e7

empty_exifedit = ExifEdit(filename)

empty_exifedit.add_lat_lon(test_latitude, test_longitude, precision)
empty_exifedit.add_lat_lon(test_latitude, test_longitude)
empty_exifedit.write(EMPTY_EXIF_FILE_TEST)

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

def add_altitude_general(test_obj, filename: Path):
test_altitude = 15.5
test_altitude_precision = 100

empty_exifedit = ExifEdit(filename)

empty_exifedit.add_altitude(test_altitude, test_altitude_precision)
empty_exifedit.add_altitude(test_altitude)
empty_exifedit.write(EMPTY_EXIF_FILE_TEST)

exif_data = ExifRead(EMPTY_EXIF_FILE_TEST)
Expand Down Expand Up @@ -118,13 +116,10 @@ def add_repeatedly_time_original_general(test_obj, filename):
def add_direction_general(test_obj, filename):
test_direction = 1
test_direction_ref = "T"
test_direction_precision = 100

empty_exifedit = ExifEdit(filename)

empty_exifedit.add_direction(
test_direction, test_direction_ref, test_direction_precision
)
empty_exifedit.add_direction(test_direction, test_direction_ref)
empty_exifedit.write(EMPTY_EXIF_FILE_TEST)

exif_data = ExifRead(EMPTY_EXIF_FILE_TEST)
Expand Down Expand Up @@ -181,11 +176,10 @@ def test_add_repeatedly_time_original(self):

def test_add_time_original_to_existing_exif(self):
test_altitude = 15.5
test_altitude_precision = 100

empty_exifedit = ExifEdit(EMPTY_EXIF_FILE_TEST)

empty_exifedit.add_altitude(test_altitude, test_altitude_precision)
empty_exifedit.add_altitude(test_altitude)
empty_exifedit.write(EMPTY_EXIF_FILE_TEST)

test_datetime = datetime.datetime(2016, 9, 30, 8, 29, 26, 249000)
Expand All @@ -205,11 +199,10 @@ def test_add_time_original_to_existing_exif(self):
def test_add_negative_lat_lon(self):
test_latitude = -50.5
test_longitude = -15.5
precision = 1e7

empty_exifedit = ExifEdit(EMPTY_EXIF_FILE_TEST)

empty_exifedit.add_lat_lon(test_latitude, test_longitude, precision)
empty_exifedit.add_lat_lon(test_latitude, test_longitude)
empty_exifedit.write(EMPTY_EXIF_FILE_TEST)

exif_data = ExifRead(EMPTY_EXIF_FILE_TEST)
Expand Down
Loading