Compare commits

...

6 Commits

4 changed files with 188 additions and 66 deletions

1
.gitignore vendored
View File

@ -3,3 +3,4 @@ testsource
testdest
__pycache__
Pipfile.lock
.vscode

View File

@ -4,11 +4,12 @@ url = "https://pypi.org/simple"
verify_ssl = true
[dev-packages]
pylint = "*"
[packages]
pytz = "*"
gpxpy = "*"
py3exiv2 = "*"
[requires]
python_version = "3.7"
# [requires]
# python_version = "3.8"

View File

@ -11,12 +11,13 @@ import pyexiv2
class Radiation:
'''
Reiceives values vom CSV file and creates a list of the relevant data
Receives values vom CSV file and creates a list of the relevant data
Arguments:
timestamp: Date/time string from CSV as string
radiation: Radiation from CSV in CP/M as float
local_timezone: timezone for timezone-unware CSV / Photo, if GPX is timezone aware
local_timezone: timezone for timezone-unaware CSV / Photo, if GPX is
timezone aware
si_factor: CP/M to (µS/h) conversion factor - specific to GMC-tube
Returns:
@ -24,7 +25,13 @@ class Radiation:
radiation: radiation in µS/h as str (for Exif comment, UTF-8)
'''
def __init__(self, timestamp, radiation, local_timezone, si_factor):
def __init__(
self,
timestamp,
radiation,
local_timezone,
si_factor
):
self.timestamp = self._time_conversion(timestamp, local_timezone)
self.radiation = self._radiation_conversion(radiation, si_factor)
@ -34,7 +41,7 @@ class Radiation:
def _time_conversion(self, timestamp, local_timezone):
csv_naive_time = datetime.fromisoformat(timestamp)
# Set timezone
csv_aware_time = csv_naive_time.astimezone(local_timezone)
csv_aware_time = csv_naive_time.localize(local_timezone)
return csv_aware_time
def _radiation_conversion(self, radiation, si_factor):
@ -48,9 +55,10 @@ class Photo:
Arguments:
photo: source photo ()
local_timezone: timezone for timezone-unware CSV / Photo, if GPX is timezone aware
local_timezone: timezone for timezone-unaware CSV / Photo, if GPX is
timezone aware
dest_dir: destination directory where the photo is going to be copied to.
dry_run: whether to acutally write (True / False)
dry_run: whether to actually write (True / False)
Returns:
get_date: timestamp of photo als datetime object
@ -64,7 +72,10 @@ class Photo:
self.get_photo_basename = self._copy_photo(photo, dest_dir, dry_run)[0]
def __repr__(self):
return 'Photo: %s Creation Date: %s' % (str(self.get_photo_basename), str(self.get_date))
return 'Photo: %s Creation Date: %s' % (
str(self.get_photo_basename),
str(self.get_date)
)
def _copy_photo(self, photo, dest_dir, dry_run):
# Determine where to work on photo and copy it there if needed.
@ -91,13 +102,13 @@ class Photo:
# date.value creates datetime object in pic_naive_time
pic_naive_time = date.value
# Set timezone
pic_aware_time = pic_naive_time.astimezone(local_timezone)
pic_aware_time = pic_naive_time.localize(local_timezone)
return pic_aware_time
class Match:
'''
Receives lists of time / radiation and GPS data and compares it to timestamp.
Then returns relevant values matching to time - or None
Receives lists of time / radiation and GPS data and compares it to
timestamp.Then returns relevant values matching to time - or None
Arguments:
photo_time: timestamp of photo
@ -110,12 +121,30 @@ class Match:
'''
def __init__(self, photo_time, radiation_list, position_list):
self.radiation_value = self._find_radiation_match(photo_time, radiation_list)[1]
self.radiation_delta = self._find_radiation_match(photo_time, radiation_list)[0]
self.position_delta = self._find_position_match(photo_time, position_list)[0]
self.position_latitude = self._find_position_match(photo_time, position_list)[1][1]
self.position_longitude = self._find_position_match(photo_time, position_list)[1][2]
self.position_altitude = self._find_position_match(photo_time, position_list)[1][3]
self.radiation_value = self._find_radiation_match(
photo_time,
radiation_list
)[1]
self.radiation_delta = self._find_radiation_match(
photo_time,
radiation_list
)[0]
self.position_delta = self._find_position_match(
photo_time,
position_list
)[0]
self.position_latitude = self._find_position_match(
photo_time,
position_list
)[1][1]
self.position_longitude = self._find_position_match(
photo_time,
position_list
)[1][2]
self.position_altitude = self._find_position_match(
photo_time,
position_list
)[1][3]
def __repr__(self):
if self.radiation_value:
@ -179,16 +208,28 @@ class Exif:
latitude: latitude as float
longitude: longitude as float
elevation: elevation as float
dry_run: whether to acutally write (True / False)
dry_run: whether to actually write (True / False)
Returns:
Latitude / Longitude: in degrees
Exif-Comment: that has been written (incl. radiation)
'''
def __init__(self, photo, dry_run, radiation, latitude, longitude, elevation):
self.write_exif = self._write_exif(photo, dry_run, radiation, latitude,
longitude, elevation)
def __init__(
self, photo,
dry_run,
radiation,
latitude,
longitude,
elevation
):
self.write_exif = self._write_exif(
photo,
dry_run,
radiation,
latitude,
longitude, elevation
)
def __repr__(self):
return 'Position: %s, %s: %s ' % self.write_exif
@ -207,7 +248,15 @@ class Exif:
second = round((t1 - minute) * 60, 5)
return (deg, minute, second, loc_value)
def _write_exif(self, photo, dry_run, radiation, latitude, longitude, elevation):
def _write_exif(
self,
photo,
dry_run,
radiation,
latitude,
longitude,
elevation
):
metadata = pyexiv2.ImageMetadata(photo)
metadata.read()
@ -217,12 +266,16 @@ class Exif:
longitude_degree = self._to_degree(longitude, ["W", "E"])
# convert decimal coordinates into fractions required for pyexiv2
exiv2_latitude = (Fraction(latitude_degree[0] * 60 + latitude_degree[1], 60),
Fraction(int(round(latitude_degree[2] * 100, 0)), 6000),
Fraction(0, 1))
exiv2_longitude = (Fraction(longitude_degree[0] * 60 + longitude_degree[1], 60),
Fraction(int(round(longitude_degree[2] * 100, 0)), 6000),
Fraction(0, 1))
exiv2_latitude = (
Fraction(latitude_degree[0] * 60 + latitude_degree[1], 60),
Fraction(int(round(latitude_degree[2] * 100, 0)), 6000),
Fraction(0, 1)
)
exiv2_longitude = (
Fraction(longitude_degree[0] * 60 + longitude_degree[1], 60),
Fraction(int(round(longitude_degree[2] * 100, 0)), 6000),
Fraction(0, 1)
)
# Exif tags to write
metadata['Exif.GPSInfo.GPSLatitude'] = exiv2_latitude
@ -259,7 +312,8 @@ class Exif:
class Output:
'''
Receives values to be printed, formats them and returns a string for printing.
Receives values to be printed, formats them and returns a string for
printing.
Arguments:
radiation: radiation as float
@ -272,7 +326,12 @@ class Output:
'''
def __init__(self, radiation, latitude, longitude, altitude):
self.get_string = self._get_string(radiation, latitude, longitude, altitude)
self.get_string = self._get_string(
radiation,
latitude,
longitude,
altitude
)
def __repr__(self):
return self.get_string
@ -298,4 +357,3 @@ class Output:
# Return data string
return data

View File

@ -1,9 +1,11 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
''' Iterates over a bunch of .jpg or .cr2 files and matches
'''
Iterates over a bunch of .jpg or .cr2 files and matches
DateTimeOriginal from Exif tags to DateTime in a csv log
of a GeigerMuellerCounter and writes its value to Exif/ITPC/XMP tags in µS/h '''
of a GeigerMuellerCounter and writes its value to Exif/ITPC/XMP tags in µS/h
'''
import csv
import argparse
@ -21,26 +23,57 @@ from functions import Radiation, Photo, Match, Exif, Output
# 600+ series: 0.002637 µSv/h / CPM
# Configure argument parser for cli options
parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter,
description='''A unix-tyle tool that
extracts GPS and/or radiation data from GPX/CSV files and writes
them into the Exif/ITPC/XMP tags of given photos.''')
parser.add_argument('-si', '--sifactor', type=float, default=0.0065,
help='Factor to multiply recorded CPM with.')
parser.add_argument('-tz', '--timezone', type=str, metavar='Timezone', default='utc',
help='''Manually set timezone of CSV / and Photo timestamp,
defaults to UTC if omitted. This is useful, if the GPS-Logger
saves the time incl. timezone''')
parser.add_argument('-d', '--dry', action='store_true',
help='Dry-run, do not actually write anything.')
parser.add_argument('csv', metavar='CSV', type=str,
help='Geiger counter history file in CSV format.')
parser.add_argument('-g', '--gpx', metavar='GPX', type=str,
help='GPS track in GPX format')
parser.add_argument('photos', metavar='Photo', type=str, nargs='+',
help='One or multiple photo image files to process.')
parser.add_argument('-o', '--outdir', type=str, default='.',
help='Directory to output processed photos.')
parser = argparse.ArgumentParser(
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
description='''A unix-tyle tool that extracts GPS and/or radiation data
from GPX/CSV files and writes them into the Exif/ITPC/XMP tags of given
photos.'''
)
parser.add_argument(
'-si', '--sifactor',
type=float,
default=0.0065,
help='Factor to multiply recorded CPM with.'
)
parser.add_argument(
'-tz', '--timezone',
type=str,
metavar='Timezone',
default='utc',
help='''Manually set timezone of CSV / and Photo timestamp, defaults to
UTC if omitted. This is useful, if the GPS-Logger saves the time in local
time (without timezone).'''
)
parser.add_argument(
'-d', '--dry',
action='store_true',
help='Dry-run, do not actually write anything.'
)
parser.add_argument(
'csv',
metavar='CSV',
type=str,
help='Geiger counter history file in CSV format.'
)
parser.add_argument(
'-g', '--gpx',
metavar='GPX',
type=str,
help='GPS track in GPX format'
)
parser.add_argument(
'photos',
metavar='Photo',
type=str,
nargs='+',
help='One or multiple photo image files to process.'
)
parser.add_argument(
'-o', '--outdir',
type=str,
default='.',
help='Directory to output processed photos.'
)
args = parser.parse_args()
@ -53,12 +86,21 @@ position_list = []
# Import GeigerCounter log
with open(args.csv, "r") as f:
csv = csv.reader(filter(lambda row: row[0] != '#', f),
delimiter=',', skipinitialspace=True)
# Read csv file, filter out lines beginning with #
csv = csv.reader(
filter(lambda row: row[0] != '#', f),
delimiter=',',
skipinitialspace=True
)
# Import only relevant values, that's timestamp and CP/M
for _, csv_raw_time, csv_raw_cpm, _ in csv:
radiation = Radiation(csv_raw_time, csv_raw_cpm, local_timezone, args.sifactor)
radiation = Radiation(
csv_raw_time,
csv_raw_cpm,
local_timezone,
args.sifactor
)
radiation_list.append(radiation)
# close CSV file
f.close()
@ -70,9 +112,13 @@ if args.gpx:
for track in gpx_reader.tracks:
for segment in track.segments:
for point in segment.points:
point_aware_time = point.time.astimezone(local_timezone)
position = (point_aware_time, point.latitude, point.longitude,
point.elevation)
point_aware_time = point.time.localize(local_timezone)
position = (
point_aware_time,
point.latitude,
point.longitude,
point.elevation
)
position_list.append(position)
# Inform the user about what is going to happen
@ -87,18 +133,34 @@ else:
# Print table header
print('{:<15} {:<25} {:<22}'.format('filename', 'date / time', 'Matched Data'))
# Iterate over list of photos
for src_photo in args.photos:
# Instantiate photo, copy it to destdir if needed and receive filename to work on
# Instantiate photo, copy it to destdir if needed and receive filename
# to work on
photo = Photo(src_photo, local_timezone, args.outdir, args.dry)
# Here the matching magic takes place
match = Match(photo.get_date, radiation_list, position_list)
# Formatted output:
data = Output(match.radiation_value, match.position_latitude,
match.position_longitude, match.position_altitude)
print('{:<15} {:<25} {:<22}'.format(photo.get_photo_basename, str(photo.get_date), str(data)))
data = Output(
match.radiation_value,
match.position_latitude,
match.position_longitude,
match.position_altitude
)
print(
'{:<15} {:<25} {:<22}'.format(photo.get_photo_basename,
str(photo.get_date),
str(data))
)
# Write exif data
Exif(photo.get_photo_filename, args.dry, match.radiation_value,
match.position_latitude, match.position_longitude, match.position_altitude)
Exif(
photo.get_photo_filename,
args.dry,
match.radiation_value,
match.position_latitude,
match.position_longitude,
match.position_altitude
)