From 3eb17f53b58ac54ecad7f37b0d6f5a4646fc3f16 Mon Sep 17 00:00:00 2001 From: Commander1024 Date: Fri, 6 Mar 2020 14:36:02 +0100 Subject: [PATCH 01/39] First baby steps towards GPX handling - mainly foolish test output. --- CHANGELOG.md | 2 +- Readme.md | 3 ++- exif_rad.py | 32 +++++++++++++++++++++++++++----- 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bcd1396..533955e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] - +- Added GPX parser ## [0.2] - 2020-02-05 ### Added diff --git a/Readme.md b/Readme.md index 6b62b86..bb745fd 100644 --- a/Readme.md +++ b/Readme.md @@ -11,9 +11,10 @@ It then creates a `UserComment` Exif tag with the actual measured radiation at t ## Dependencies Right now it depends on the following non-core Python 3 libraries: - * [py3exiv2](https://pypi.org/project/py3exiv2/) A Python 3 binding to the library exiv2. + * [py3exiv2](https://pypi.org/project/py3exiv2/) A Python 3 binding for (lib)exiv2. * [boost.python3](http://www.boost.org/libs/python/doc/index.html) Welcome to Boost.Python, a C++ library which enables seamless interoperability between C++ and the Python programming language. * [exiv2](http://www.exiv2.org/) Exiv2 is a Cross-platform C++ library and a command line utility to manage image metadata. + * [gpxpy](https://github.com/tkrajina/gpxpy) gpx-py is a python GPX parser. GPX (GPS eXchange Format) is an XML based file format for GPS tracks. ## Requirements * GeigerCounter log file in csv format as it is being exported by the software GeigerLog. diff --git a/exif_rad.py b/exif_rad.py index 7ad52ad..9f661c6 100755 --- a/exif_rad.py +++ b/exif_rad.py @@ -12,6 +12,7 @@ import shutil import csv import argparse import pyexiv2 +import gpxpy # SIFACTOR for GQ Geiger counters @@ -24,17 +25,19 @@ import pyexiv2 # Configure argument parser for cli options parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter, - description='''A tool that writes - radiation levels (and optionally geocoordinates) to image files - and extracts the infos from external sources.''') + description='''A unix-tyle tool that + extracts GPS and/or radiation data from GPX/CSV files and writes + them into the Exif tags of given photos.''') parser.add_argument('-si', '--sifactor', type=float, default=0.0065, help='Factor to multiply recorded CPM with.') 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') + help='Directory to output processed photos.') args = parser.parse_args() @@ -72,7 +75,6 @@ for srcphoto in args.photos: csvreader = csv.reader(filter(lambda row: row[0] != '#', f), delimiter=',', skipinitialspace=True) - print('Processing file:', photo_basename, end='\r') for _, csvrawtime, csvrawcpm, _ in csvreader: csvtime = datetime.fromisoformat(csvrawtime) # Process image if its timestamp is found in CSV log (compares 2 datetime objects) @@ -93,3 +95,23 @@ for srcphoto in args.photos: print('{:<15} {:<20} {:<22}'.format(photo_basename, str(pictime), 'NOT FOUND!')) # close CSV file f.close() + + # Import GPX track(s) + if hasattr(args, 'gpx'): + gpx_file = open(args.gpx, 'r') + gpxreader = gpxpy.parse(gpx_file) + + for waypoint in gpxreader.waypoints: + for track in gpxreader.tracks: + for segment in track.segments: + for point in segment.points: + #if pictime == point.time: + print(type(point.time)) + print(type(pictime)) + print(pictime, 'vs.', point.time) +# print('Point at ({0},{1}) -> {2}'.format(point.latitude, point.longitude, point.time)) + +''' + 2018-08-04 17:50:16 vs. 2018-08-04 17:50:03.535000+00:00 + + 2018-08-04 17:50:16 vs. 2018-08-04 17:50:23.327000+00:00''' From 940da3a772dc83f70dcf262989845f9448c30fc8 Mon Sep 17 00:00:00 2001 From: Commander1024 Date: Sat, 7 Mar 2020 19:10:56 +0100 Subject: [PATCH 02/39] Added dry-run optional parameter. --- CHANGELOG.md | 1 + exif_rad.py | 16 ++++++++++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 533955e..28ce529 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] - Added GPX parser +- Added optional dry-run modifier ## [0.2] - 2020-02-05 ### Added diff --git a/exif_rad.py b/exif_rad.py index 9f661c6..e9c068f 100755 --- a/exif_rad.py +++ b/exif_rad.py @@ -30,6 +30,8 @@ parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFo them into the Exif tags of given photos.''') parser.add_argument('-si', '--sifactor', type=float, default=0.0065, help='Factor to multiply recorded CPM with.') +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, @@ -60,8 +62,13 @@ for srcphoto in args.photos: else: # be os aware and use the correct directory delimiter for destfile dstphoto = os.path.join(args.outdir, photo_basename) - shutil.copy(srcphoto, dstphoto) - photo = dstphoto + print('dry-run:', args.dry) + if args.dry == 'True': + print('Copying file!') + shutil.copy(srcphoto, dstphoto) + photo = dstphoto + else: + photo = srcphoto # Load Exif data from image metadata = pyexiv2.ImageMetadata(photo) @@ -89,7 +96,8 @@ for srcphoto in args.photos: # print found radiation levels print('{:<15} {:<20} {:<22}'.format(photo_basename, str(pictime), new_comment)) # Write Exif tags to file - metadata.write() + if args.dry == 'True': + metadata.write() break else: print('{:<15} {:<20} {:<22}'.format(photo_basename, str(pictime), 'NOT FOUND!')) @@ -97,7 +105,7 @@ for srcphoto in args.photos: f.close() # Import GPX track(s) - if hasattr(args, 'gpx'): + if args.gpx is not None: gpx_file = open(args.gpx, 'r') gpxreader = gpxpy.parse(gpx_file) From aef69f81a1e457e855e96371b7e1bac9059c0cf7 Mon Sep 17 00:00:00 2001 From: Commander1024 Date: Sat, 7 Mar 2020 19:12:00 +0100 Subject: [PATCH 03/39] Removed debug output. --- exif_rad.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/exif_rad.py b/exif_rad.py index e9c068f..ee0820e 100755 --- a/exif_rad.py +++ b/exif_rad.py @@ -62,9 +62,7 @@ for srcphoto in args.photos: else: # be os aware and use the correct directory delimiter for destfile dstphoto = os.path.join(args.outdir, photo_basename) - print('dry-run:', args.dry) if args.dry == 'True': - print('Copying file!') shutil.copy(srcphoto, dstphoto) photo = dstphoto else: From 5b4811b08112a892a8432f1d1b00033ff2cdf1bc Mon Sep 17 00:00:00 2001 From: Commander1024 Date: Sat, 7 Mar 2020 21:29:25 +0100 Subject: [PATCH 04/39] Made timezone-aware (configurable) Added optional dry-run modifier Made timezone-aware for timesources without timezone Added optional parameter to override timezone to local timezone, defaults to localtime --- CHANGELOG.md | 2 ++ exif_rad.py | 41 ++++++++++++++++++++++++----------------- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 28ce529..a8c6511 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] - Added GPX parser - Added optional dry-run modifier +- Made timezone-aware for timesources without timezone +- Added optional parameter to override timezone to local timezone, defaults to localtime ## [0.2] - 2020-02-05 ### Added diff --git a/exif_rad.py b/exif_rad.py index ee0820e..8c5277c 100755 --- a/exif_rad.py +++ b/exif_rad.py @@ -6,11 +6,12 @@ DateTimeOriginal from Exif tag to DateTime in a csv log of a GeigerMuellerCounter and writes its value to the UserComment Exif tag in µS/h""" -from datetime import datetime +from datetime import datetime, timedelta import os import shutil import csv import argparse +import pytz import pyexiv2 import gpxpy @@ -30,6 +31,9 @@ parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFo them into the Exif 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', + help='''Manually set timezone of CSV / and Photo timestamp, + defaults to localtime if omitted.''') parser.add_argument('-d', '--dry', action='store_true', help='Dry-run, do not actually write anything.') parser.add_argument('csv', metavar='CSV', type=str, @@ -43,13 +47,16 @@ parser.add_argument('-o', '--outdir', type=str, default='.', args = parser.parse_args() +# Create timezone datetime object +localtz = pytz.timezone(args.timezone) + # Inform the user about what is going to happen if args.outdir == ".": print('Modifying photos in place (overwrite)') else: print('Modifying photos in', str(args.outdir), '(copy)') # Print table header -print('{:<15} {:<20} {:<22}'.format('filename', 'date / time', 'Exif UserComment')) +print('{:<15} {:<25} {:<22}'.format('filename', 'date / time', 'Exif UserComment')) for srcphoto in args.photos: # Get image file name out of path @@ -62,6 +69,7 @@ for srcphoto in args.photos: else: # be os aware and use the correct directory delimiter for destfile dstphoto = os.path.join(args.outdir, photo_basename) + # Don't copy image if in dry-run mode if args.dry == 'True': shutil.copy(srcphoto, dstphoto) photo = dstphoto @@ -73,15 +81,19 @@ for srcphoto in args.photos: metadata.read() tag = metadata['Exif.Photo.DateTimeOriginal'] # tag.value creates datetime object in pictime - pictime = tag.value - + picnaivetime = tag.value + # Set timezone + pictime = picnaivetime.astimezone(localtz) + # Import GeigerCounter log with open(args.csv, "r") as f: csvreader = csv.reader(filter(lambda row: row[0] != '#', f), delimiter=',', skipinitialspace=True) for _, csvrawtime, csvrawcpm, _ in csvreader: - csvtime = datetime.fromisoformat(csvrawtime) + csvnaivetime = datetime.fromisoformat(csvrawtime) + # Set timezone + csvtime = csvnaivetime.astimezone(localtz) # Process image if its timestamp is found in CSV log (compares 2 datetime objects) if csvtime == pictime: rad = round(float(csvrawcpm) * args.sifactor, 2) @@ -92,13 +104,13 @@ for srcphoto in args.photos: metadata[key] = pyexiv2.ExifTag(key, new_comment) # print found radiation levels - print('{:<15} {:<20} {:<22}'.format(photo_basename, str(pictime), new_comment)) - # Write Exif tags to file + print('{:<15} {:<25} {:<22}'.format(photo_basename, str(pictime), new_comment)) + # Write Exif tags to file, if not in dry-run mode if args.dry == 'True': metadata.write() break else: - print('{:<15} {:<20} {:<22}'.format(photo_basename, str(pictime), 'NOT FOUND!')) + print('{:<15} {:<25} {:<22}'.format(photo_basename, str(pictime), 'NOT FOUND!')) # close CSV file f.close() @@ -111,13 +123,8 @@ for srcphoto in args.photos: for track in gpxreader.tracks: for segment in track.segments: for point in segment.points: - #if pictime == point.time: - print(type(point.time)) - print(type(pictime)) - print(pictime, 'vs.', point.time) + # datetimes match with 1 minute precision + delta = timedelta(minutes=1) + if abs(point.time - pictime) < delta: + print(pictime, 'vs.', point.time, 'Delta:', pictime - point.time) # print('Point at ({0},{1}) -> {2}'.format(point.latitude, point.longitude, point.time)) - -''' - 2018-08-04 17:50:16 vs. 2018-08-04 17:50:03.535000+00:00 - - 2018-08-04 17:50:16 vs. 2018-08-04 17:50:23.327000+00:00''' From 83977b50bbe73d4bbba41ec3f7b70d89aba39c93 Mon Sep 17 00:00:00 2001 From: Commander1024 Date: Mon, 9 Mar 2020 15:56:28 +0100 Subject: [PATCH 05/39] Changed timezone default (to utc). Old behavior failed when TZ was omitted. --- Readme.md | 17 +++++++++++++---- exif_rad.py | 5 +++-- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/Readme.md b/Readme.md index bb745fd..604f1e2 100644 --- a/Readme.md +++ b/Readme.md @@ -32,10 +32,12 @@ These exported .his files look like this: ## Usage ``` -usage: exif_rad.py [-h] [-si SIFACTOR] [-o OUTDIR] CSV Photo [Photo ...] +usage: exif_rad.py [-h] [-si SIFACTOR] [-tz Timezone] [-d] [-g GPX] + [-o OUTDIR] + CSV Photo [Photo ...] -A tool that writes radiation levels (and optionally geocoordinates) to image -files and extracts the infos from external sources. +A unix-tyle tool that extracts GPS and/or radiation data from GPX/CSV files +and writes them into the Exif tags of given photos. positional arguments: CSV Geiger counter history file in CSV format. @@ -46,8 +48,15 @@ optional arguments: -si SIFACTOR, --sifactor SIFACTOR Factor to multiply recorded CPM with. (default: 0.0065) + -tz Timezone, --timezone Timezone + Manually set timezone of CSV / and Photo timestamp, + defaults to localtime if omitted. (default: None) + -d, --dry Dry-run, do not actually write anything. (default: + False) + -g GPX, --gpx GPX GPS track in GPX format (default: None) -o OUTDIR, --outdir OUTDIR - Directory to output processed photos (default: .) + Directory to output processed photos. (default: .) + ``` ### Examples diff --git a/exif_rad.py b/exif_rad.py index 8c5277c..d5bd091 100755 --- a/exif_rad.py +++ b/exif_rad.py @@ -31,9 +31,10 @@ parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFo them into the Exif 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', +parser.add_argument('-tz', '--timezone', type=str, metavar='Timezone', default='utc', help='''Manually set timezone of CSV / and Photo timestamp, - defaults to localtime if omitted.''') + 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, From f43116abece5cc869f553e37c9cbeb45d8a0fca3 Mon Sep 17 00:00:00 2001 From: Commander1024 Date: Mon, 9 Mar 2020 16:09:19 +0100 Subject: [PATCH 06/39] Added verbose output for dry-run mode. --- exif_rad.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/exif_rad.py b/exif_rad.py index d5bd091..7010f1b 100755 --- a/exif_rad.py +++ b/exif_rad.py @@ -52,10 +52,14 @@ args = parser.parse_args() localtz = pytz.timezone(args.timezone) # Inform the user about what is going to happen -if args.outdir == ".": - print('Modifying photos in place (overwrite)') +if args.dry is not None: + if args.outdir == ".": + print('Modifying photos in place (overwrite)') + else: + print('Modifying photos in', str(args.outdir), '(copy)') else: - print('Modifying photos in', str(args.outdir), '(copy)') + print('Not modifying anything. Just print what would happen without --dry') + # Print table header print('{:<15} {:<25} {:<22}'.format('filename', 'date / time', 'Exif UserComment')) From 115d6a6ab431781af1941558142290a53c8abdd0 Mon Sep 17 00:00:00 2001 From: Commander1024 Date: Mon, 9 Mar 2020 17:02:17 +0100 Subject: [PATCH 07/39] Trying to increase precision of datematch. --- exif_rad.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/exif_rad.py b/exif_rad.py index 7010f1b..3e4c4be 100755 --- a/exif_rad.py +++ b/exif_rad.py @@ -129,7 +129,11 @@ for srcphoto in args.photos: for segment in track.segments: for point in segment.points: # datetimes match with 1 minute precision - delta = timedelta(minutes=1) + delta = timedelta(seconds=10) if abs(point.time - pictime) < delta: - print(pictime, 'vs.', point.time, 'Delta:', pictime - point.time) + valuelist = [] + row = [point.time, point.latitude, point.longitude] + valuelist.append(row) + print(valuelist) + #print(pictime, 'vs.', point.time, 'Delta:', pictime - point.time) # print('Point at ({0},{1}) -> {2}'.format(point.latitude, point.longitude, point.time)) From fcc466eb417527e278a85a03a3a8548748c92e1b Mon Sep 17 00:00:00 2001 From: Commander1024 Date: Mon, 9 Mar 2020 22:42:08 +0100 Subject: [PATCH 08/39] Started conversion to OOP. Created class Radiation with functions time_conversion and radiation --- .gitignore | 1 + functions.py | 31 +++++++++++++ rad_tag.py | 128 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 160 insertions(+) create mode 100644 functions.py create mode 100644 rad_tag.py diff --git a/.gitignore b/.gitignore index 41ded9e..a62b1e5 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ testsource testdest testdata usercomment.sh +__pycache__ diff --git a/functions.py b/functions.py new file mode 100644 index 0000000..25824be --- /dev/null +++ b/functions.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +"""Classes used by main program. Handles CSV and GPX processing.""" + +from datetime import datetime + +class Radiation: + def __init__(self, timestamp, radiation, local_timezone, si_factor): + self.timestamp = self._time_conversion(timestamp, local_timezone) + self.radiation = self._radiation(radiation, si_factor) + + def __repr__(self): + return '%s %f µS/h' % (str(self.timestamp), self.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) + return csv_aware_time + + def _radiation(self, radiation, si_factor): + # Convert CP/M to µS/h using si_factor + radiation = round(float(radiation) * si_factor, 2) + return radiation + +class Position: + def __init__(self, timestamp, latitude, longitude): + self.timestamp = timestamp + self.latitude = latitude + self.longitude = longitude diff --git a/rad_tag.py b/rad_tag.py new file mode 100644 index 0000000..23aa67f --- /dev/null +++ b/rad_tag.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +"""Iterates over a bunch of .jpg or .cr2 files and matches +DateTimeOriginal from Exif tag to DateTime in a csv log +of a GeigerMuellerCounter and writes its value to the UserComment +Exif tag in µS/h""" + +from datetime import datetime, timedelta +import os +import shutil +import csv +import argparse +import pytz +import pyexiv2 +import gpxpy +from functions import Radiation, Position + +# SIFACTOR for GQ Geiger counters + +# 300 series: 0.0065 µSv/h / CPM +# 320 series: 0.0065 µSv/h / CPM +# 500 series: 0.0065 µSv/h / CPM +# 500+ series: 0.0065 µSv/h / CPM for the first tube +# 600 series: 0.0065 µSv/h / CPM +# 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 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.') + +args = parser.parse_args() + +# Create timezone datetime object +local_timezone = pytz.timezone(args.timezone) + +# Inform the user about what is going to happen +if args.dry is not None: + if args.outdir == ".": + print('Modifying photos in place (overwrite)') + else: + print('Modifying photos in', str(args.outdir), '(copy)') +else: + print('Not modifying anything. Just print what would happen without --dry') + +# Print table header +print('{:<15} {:<25} {:<22}'.format('filename', 'date / time', 'Exif UserComment')) + +for srcphoto in args.photos: + # Get image file name out of path + photo_basename = os.path.basename(srcphoto) + + # Decide whether to modify photo in place or to copy it to outdir first + # Then set the destination file as 'photo' to work on + if args.outdir == ".": + photo = srcphoto + else: + # be os aware and use the correct directory delimiter for destfile + dstphoto = os.path.join(args.outdir, photo_basename) + # Don't copy image if in dry-run mode + if args.dry == 'True': + shutil.copy(srcphoto, dstphoto) + photo = dstphoto + else: + photo = srcphoto + + # Load Exif data from image + metadata = pyexiv2.ImageMetadata(photo) + metadata.read() + tag = metadata['Exif.Photo.DateTimeOriginal'] + # tag.value creates datetime object in pictime + picnaivetime = tag.value + # Set timezone + pictime = picnaivetime.astimezone(local_timezone) + + radiation_list = [] + position_list = [] + + # Import GeigerCounter log + with open(args.csv, "r") as f: + csvreader = csv.reader(filter(lambda row: row[0] != '#', f), + delimiter=',', skipinitialspace=True) + + for _, csvrawtime, csvrawcpm, _ in csvreader: + radiation = Radiation(csvrawtime, csvrawcpm, local_timezone, args.sifactor) + radiation_list.append(radiation) + print(radiation_list) + + # close CSV file + f.close() + + # Import GPX track(s) + if args.gpx is not None: + gpx_file = open(args.gpx, 'r') + gpxreader = gpxpy.parse(gpx_file) + + for waypoint in gpxreader.waypoints: + for track in gpxreader.tracks: + for segment in track.segments: + for point in segment.points: + # datetimes match with 1 minute precision + delta = timedelta(seconds=60) + if abs(point.time - pictime) < delta: + valuelist = [] + row = [point.time - pictime, point.latitude, point.longitude] + valuelist.append(row) + print(valuelist) + min(valuelist[0][0]) + #print(pictime, 'vs.', point.time, 'Delta:', pictime - point.time) +# print('Point at ({0},{1}) -> {2}'.format(point.latitude, point.longitude, point.time)) From a2ae379883f726b4bf7437b0cf3f7d9ee9de0ff7 Mon Sep 17 00:00:00 2001 From: Commander1024 Date: Mon, 9 Mar 2020 23:00:19 +0100 Subject: [PATCH 09/39] Made file executable. --- rad_tag.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 rad_tag.py diff --git a/rad_tag.py b/rad_tag.py old mode 100644 new mode 100755 From 314e70faa9920179670663e926780d8f676a0189 Mon Sep 17 00:00:00 2001 From: Commander1024 Date: Mon, 9 Mar 2020 23:22:34 +0100 Subject: [PATCH 10/39] Now got 2 lists with with all data relevant for one photo. --- CHANGELOG.md | 1 + exif_rad.py | 2 ++ functions.py | 6 ------ rad_tag.py | 16 +++++----------- 4 files changed, 8 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a8c6511..06d4ebc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added optional dry-run modifier - Made timezone-aware for timesources without timezone - Added optional parameter to override timezone to local timezone, defaults to localtime +- Swith to oop style programming. Made code easier to read. ## [0.2] - 2020-02-05 ### Added diff --git a/exif_rad.py b/exif_rad.py index 3e4c4be..046ae64 100755 --- a/exif_rad.py +++ b/exif_rad.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +# This file is now obsolete and is going to disappear. + """Iterates over a bunch of .jpg or .cr2 files and matches DateTimeOriginal from Exif tag to DateTime in a csv log of a GeigerMuellerCounter and writes its value to the UserComment diff --git a/functions.py b/functions.py index 25824be..ca347df 100644 --- a/functions.py +++ b/functions.py @@ -23,9 +23,3 @@ class Radiation: # Convert CP/M to µS/h using si_factor radiation = round(float(radiation) * si_factor, 2) return radiation - -class Position: - def __init__(self, timestamp, latitude, longitude): - self.timestamp = timestamp - self.latitude = latitude - self.longitude = longitude diff --git a/rad_tag.py b/rad_tag.py index 23aa67f..d2d7db7 100755 --- a/rad_tag.py +++ b/rad_tag.py @@ -102,7 +102,7 @@ for srcphoto in args.photos: for _, csvrawtime, csvrawcpm, _ in csvreader: radiation = Radiation(csvrawtime, csvrawcpm, local_timezone, args.sifactor) radiation_list.append(radiation) - print(radiation_list) + #print(radiation_list) # close CSV file f.close() @@ -116,13 +116,7 @@ for srcphoto in args.photos: for track in gpxreader.tracks: for segment in track.segments: for point in segment.points: - # datetimes match with 1 minute precision - delta = timedelta(seconds=60) - if abs(point.time - pictime) < delta: - valuelist = [] - row = [point.time - pictime, point.latitude, point.longitude] - valuelist.append(row) - print(valuelist) - min(valuelist[0][0]) - #print(pictime, 'vs.', point.time, 'Delta:', pictime - point.time) -# print('Point at ({0},{1}) -> {2}'.format(point.latitude, point.longitude, point.time)) + position = [point.time, point.latitude, point.longitude] + position_list.append(position) + #print(position_list) + From d51c28b753085aebe66dc652b942e74c5aa8972c Mon Sep 17 00:00:00 2001 From: Commander1024 Date: Mon, 9 Mar 2020 23:43:01 +0100 Subject: [PATCH 11/39] Refactored variable names, fixed dry run decision. --- rad_tag.py | 46 +++++++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/rad_tag.py b/rad_tag.py index d2d7db7..7159dea 100755 --- a/rad_tag.py +++ b/rad_tag.py @@ -14,7 +14,7 @@ import argparse import pytz import pyexiv2 import gpxpy -from functions import Radiation, Position +from functions import Radiation # SIFACTOR for GQ Geiger counters @@ -53,54 +53,52 @@ args = parser.parse_args() local_timezone = pytz.timezone(args.timezone) # Inform the user about what is going to happen -if args.dry is not None: +if args.dry is True: + print('Not modifying anything. Just print what would happen without --dry') +else: if args.outdir == ".": print('Modifying photos in place (overwrite)') else: print('Modifying photos in', str(args.outdir), '(copy)') -else: - print('Not modifying anything. Just print what would happen without --dry') -# Print table header -print('{:<15} {:<25} {:<22}'.format('filename', 'date / time', 'Exif UserComment')) - -for srcphoto in args.photos: +for src_photo in args.photos: # Get image file name out of path - photo_basename = os.path.basename(srcphoto) + photo_basename = os.path.basename(src_photo) # Decide whether to modify photo in place or to copy it to outdir first # Then set the destination file as 'photo' to work on if args.outdir == ".": - photo = srcphoto + photo = src_photo else: # be os aware and use the correct directory delimiter for destfile - dstphoto = os.path.join(args.outdir, photo_basename) + dst_photo = os.path.join(args.outdir, photo_basename) # Don't copy image if in dry-run mode if args.dry == 'True': - shutil.copy(srcphoto, dstphoto) - photo = dstphoto + shutil.copy(src_photo, dst_photo) + photo = dst_photo else: - photo = srcphoto + photo = src_photo # Load Exif data from image metadata = pyexiv2.ImageMetadata(photo) metadata.read() tag = metadata['Exif.Photo.DateTimeOriginal'] # tag.value creates datetime object in pictime - picnaivetime = tag.value + pic_naive_time = tag.value # Set timezone - pictime = picnaivetime.astimezone(local_timezone) + pic_time = pic_naive_time.astimezone(local_timezone) + # Initialize two empty lists for comparison radiation_list = [] position_list = [] # Import GeigerCounter log with open(args.csv, "r") as f: - csvreader = csv.reader(filter(lambda row: row[0] != '#', f), - delimiter=',', skipinitialspace=True) + csv_reader = csv.reader(filter(lambda row: row[0] != '#', f), + delimiter=',', skipinitialspace=True) - for _, csvrawtime, csvrawcpm, _ in csvreader: - radiation = Radiation(csvrawtime, csvrawcpm, local_timezone, args.sifactor) + for _, csv_raw_time, csv_raw_cpm, _ in csv_reader: + radiation = Radiation(csv_raw_time, csv_raw_cpm, local_timezone, args.sifactor) radiation_list.append(radiation) #print(radiation_list) @@ -110,13 +108,15 @@ for srcphoto in args.photos: # Import GPX track(s) if args.gpx is not None: gpx_file = open(args.gpx, 'r') - gpxreader = gpxpy.parse(gpx_file) + gpx_reader = gpxpy.parse(gpx_file) - for waypoint in gpxreader.waypoints: - for track in gpxreader.tracks: + for waypoint in gpx_reader.waypoints: + for track in gpx_reader.tracks: for segment in track.segments: for point in segment.points: position = [point.time, point.latitude, point.longitude] position_list.append(position) #print(position_list) +# Print table header +print('{:<15} {:<25} {:<22}'.format('filename', 'date / time', 'Exif UserComment')) From cbb21db442aaf5e0382aea31d7fc7b9ed6a0c1ee Mon Sep 17 00:00:00 2001 From: Commander1024 Date: Mon, 9 Mar 2020 23:52:03 +0100 Subject: [PATCH 12/39] Moved creation and propagation of radiation_list and position_list out of the main loop. --- rad_tag.py | 61 +++++++++++++++++++++++++++--------------------------- 1 file changed, 31 insertions(+), 30 deletions(-) diff --git a/rad_tag.py b/rad_tag.py index 7159dea..efbefeb 100755 --- a/rad_tag.py +++ b/rad_tag.py @@ -52,6 +52,37 @@ args = parser.parse_args() # Create timezone datetime object local_timezone = pytz.timezone(args.timezone) +# Initialize two empty lists for comparison +radiation_list = [] +position_list = [] + +# Import GeigerCounter log +with open(args.csv, "r") as f: + csv_reader = csv.reader(filter(lambda row: row[0] != '#', f), + delimiter=',', skipinitialspace=True) + + for _, csv_raw_time, csv_raw_cpm, _ in csv_reader: + radiation = Radiation(csv_raw_time, csv_raw_cpm, local_timezone, args.sifactor) + radiation_list.append(radiation) + #print(radiation_list) + + # close CSV file + f.close() + +# Import GPX track(s) +if args.gpx is not None: + gpx_file = open(args.gpx, 'r') + gpx_reader = gpxpy.parse(gpx_file) + + for waypoint in gpx_reader.waypoints: + for track in gpx_reader.tracks: + for segment in track.segments: + for point in segment.points: + position = [point.time, point.latitude, point.longitude] + position_list.append(position) + #print(position_list) + + # Inform the user about what is going to happen if args.dry is True: print('Not modifying anything. Just print what would happen without --dry') @@ -88,35 +119,5 @@ for src_photo in args.photos: # Set timezone pic_time = pic_naive_time.astimezone(local_timezone) - # Initialize two empty lists for comparison - radiation_list = [] - position_list = [] - - # Import GeigerCounter log - with open(args.csv, "r") as f: - csv_reader = csv.reader(filter(lambda row: row[0] != '#', f), - delimiter=',', skipinitialspace=True) - - for _, csv_raw_time, csv_raw_cpm, _ in csv_reader: - radiation = Radiation(csv_raw_time, csv_raw_cpm, local_timezone, args.sifactor) - radiation_list.append(radiation) - #print(radiation_list) - - # close CSV file - f.close() - - # Import GPX track(s) - if args.gpx is not None: - gpx_file = open(args.gpx, 'r') - gpx_reader = gpxpy.parse(gpx_file) - - for waypoint in gpx_reader.waypoints: - for track in gpx_reader.tracks: - for segment in track.segments: - for point in segment.points: - position = [point.time, point.latitude, point.longitude] - position_list.append(position) - #print(position_list) - # Print table header print('{:<15} {:<25} {:<22}'.format('filename', 'date / time', 'Exif UserComment')) From d347bbdf55f4aaf72e5b996eb600f9d109ef7d7b Mon Sep 17 00:00:00 2001 From: Commander1024 Date: Tue, 10 Mar 2020 00:03:06 +0100 Subject: [PATCH 13/39] Fixed typo. --- rad_tag.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rad_tag.py b/rad_tag.py index efbefeb..a532732 100755 --- a/rad_tag.py +++ b/rad_tag.py @@ -101,7 +101,7 @@ for src_photo in args.photos: if args.outdir == ".": photo = src_photo else: - # be os aware and use the correct directory delimiter for destfile + # Be os aware and use the correct directory delimiter for destfile dst_photo = os.path.join(args.outdir, photo_basename) # Don't copy image if in dry-run mode if args.dry == 'True': From c19a94374e87526ce786b90df690f923515614a0 Mon Sep 17 00:00:00 2001 From: Commander1024 Date: Tue, 10 Mar 2020 21:09:25 +0100 Subject: [PATCH 14/39] Created Radiation, Photo classes and slimmed down main program. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Radiation creates a list with timezone_aware datetime and radiation in µS/h. - Photo reads DateTimeOriginal from photo. - Photo.write_exif compiles new metadata object and writes exif tags to photo. - Documentd changes in CHANGELOG.md. - Changed Readme.me to match changes in code. - Removed obsolete line from .gitignore --- .gitignore | 3 +-- CHANGELOG.md | 8 +++++--- Readme.md | 19 +++++++++++-------- functions.py | 50 +++++++++++++++++++++++++++++++++++++++++++++++--- rad_tag.py | 24 +++++++++++------------- 5 files changed, 75 insertions(+), 29 deletions(-) diff --git a/.gitignore b/.gitignore index a62b1e5..f8f9504 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ +testdata testsource testdest -testdata -usercomment.sh __pycache__ diff --git a/CHANGELOG.md b/CHANGELOG.md index 06d4ebc..ecf64b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,12 +8,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added GPX parser - Added optional dry-run modifier - Made timezone-aware for timesources without timezone -- Added optional parameter to override timezone to local timezone, defaults to localtime -- Swith to oop style programming. Made code easier to read. +- Added optional parameter to override timezone to local timezone, defaults to utc +- Refactored variable name_scheme. +- Switch to oop style programming. Made code easier to read. +- Put CSV processing and Exif reading / writing into a class. ## [0.2] - 2020-02-05 ### Added -- uses pyexiv2 instead of piexif which is also able to tag various camera raw formats, e. g. CR2 +- uses pyexiv2 instead of piexif which is also able to tag various camera raw formats, e. g. CR2 and is capable of writing properly formed UTF-8 strings. - added copy function to be able to place files to outdir before modification - added verbose output about what it going to happen - added table header for output diff --git a/Readme.md b/Readme.md index 604f1e2..c891f9f 100644 --- a/Readme.md +++ b/Readme.md @@ -1,12 +1,14 @@ # radiation tagger -exif_rad.py is a simple unix-style cross-platform Python 3 tool which can write certain tags to an image file. +rad_tag.py is a simple unix-style cross-platform Python 3 tool which can write certain tags to an image file. It can scan a couple of images, extract their Exif-tags, and compare the `DateTimeOriginal` with other sources. -By now it can parse a .his (CSV) file from a [GeigerLog](https://sourceforge.net/projects/Geigerlog/) file export and calculate the radiation in µS/h using the factor in `SIFACTOR`. +It can parse a .his (CSV) file from a [GeigerLog](https://sourceforge.net/projects/Geigerlog/) file export and calculate the radiation in µS/h using the factor in `sifactor`. -It then creates a `UserComment` Exif tag with the actual measured radiation at the time the photo has been taken. +Furthermore it can optionally read a gpx-file, compare the timestamps to 'DateTimeOriginal' and determine closest-matching latitude / longitude. If your gpx-file has times stored including the timezone, you can set --timezone to the local timezone, your camera / geiger counter ran at. + +It then creates a `UserComment` with the actual measured radiation at the time the photo has been taken and writes the geocoordinates into the appropiate Exif tags. ## Dependencies Right now it depends on the following non-core Python 3 libraries: @@ -32,7 +34,7 @@ These exported .his files look like this: ## Usage ``` -usage: exif_rad.py [-h] [-si SIFACTOR] [-tz Timezone] [-d] [-g GPX] +usage: rad_tag.py [-h] [-si SIFACTOR] [-tz Timezone] [-d] [-g GPX] [-o OUTDIR] CSV Photo [Photo ...] @@ -63,7 +65,7 @@ optional arguments: Use test.hisdb.his from current working dir and modify (overwrite) all .CR2 files in place. ``` -./exif_rad.py test.hisdb.his *.CR2 +./rat_tag.py test.hisdb.his *.CR2 Modifying photos in place (overwrite) filename date / time Exif UserComment DSC_0196.JPG 2020-03-03 18:33:33 NOT FOUND! @@ -74,7 +76,7 @@ DSC_0198.JPG 2020-03-03 22:18:13 Radiation ☢ 0.07 µS/h Use test.hisdb.his in folder 'testdata', read all .JPG files from 'testsource' and write them to 'testdest'. ``` -./exif_rad.py testdata/test.hisdb.his -o testdest/ testsource/*.JPG +./rad_tag.py testdata/test.hisdb.his -o testdest/ testsource/*.JPG Modifying photos in testdest/ (copy) filename date / time Exif UserComment DSC_0196.JPG 2020-03-03 18:33:33 NOT FOUND! @@ -99,6 +101,8 @@ The GMC* defaults are quite sane, but you might want to set the correct serial p `usbport = /dev/ttyUSB0` +GeigerLog can also use a bunch of other devices while still outputting a csv-file in compatible format. + ### Using GeigerLog to download history Now the program can be started by double-clicking `geigerlog` or by executing `./geigerlog` on the command prompt. @@ -110,11 +114,10 @@ GeigerLog now presents you a rendering of the radiation over time in its main wi [main_window]: images/geigerlog_main_window.png "GeigerLog Main Window with graph" -Once imported, you can export the history into a hisdb.his-file, which is basically the CSV-file `exif_rad.py` can process. Choose 'History' -> Save History Data into .his file (CSV)'. +Once imported, you can export the history into a hisdb.his-file, which is basically the CSV-file `rad_tag.py` can process. Choose 'History' -> Save History Data into .his file (CSV)'. ## future possibilities - * In the future it should also be able to do the same with a gpx-file to extract geolocations and to write them into the appropiate Exif-fields. * It might get a setup.py if I want to waste my time on it. * I might want to get rid of the requirement to use a bloated GUI application to download the history data off the Geigercounter. There must be a neat working command line tool. Maybe I'll write it myself. diff --git a/functions.py b/functions.py index ca347df..7f41ba0 100644 --- a/functions.py +++ b/functions.py @@ -1,14 +1,16 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -"""Classes used by main program. Handles CSV and GPX processing.""" +''' Classes used by main program. ''' from datetime import datetime +import pyexiv2 class Radiation: + ''' Handles CSV processing.''' def __init__(self, timestamp, radiation, local_timezone, si_factor): self.timestamp = self._time_conversion(timestamp, local_timezone) - self.radiation = self._radiation(radiation, si_factor) + self.radiation = self._radiation_conversion(radiation, si_factor) def __repr__(self): return '%s %f µS/h' % (str(self.timestamp), self.radiation) @@ -19,7 +21,49 @@ class Radiation: csv_aware_time = csv_naive_time.astimezone(local_timezone) return csv_aware_time - def _radiation(self, radiation, si_factor): + def _radiation_conversion(self, radiation, si_factor): # Convert CP/M to µS/h using si_factor radiation = round(float(radiation) * si_factor, 2) return radiation + +class Photo: + ''' Reads and writes Exif metadata''' + def __init__(self, photo, local_timezone): + self.get_date = self._get_creation_date(photo, local_timezone) + self.photo = photo + + def __repr__(self): + return 'Photo Creation Date: %s' % str(self.get_date) + + def _get_creation_date(self, photo, local_timezone): + # Load Exif data from photo + metadata = pyexiv2.ImageMetadata(photo) + metadata.read() + date = metadata['Exif.Photo.DateTimeOriginal'] + # 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) + return pic_aware_time + + def write_exif(self, radiation, latitude, longitude, dry_run): + + ''' UNTESTED ! ''' + + metadata = pyexiv2.ImageMetadata(self.photo) + + # Set new UserComment + new_comment = 'Radiation ☢ ' + str(radiation) + ' µS/h' + # Exif tags to write + keys = ['Exif.Photo.UserComment', 'Exif.Photo.latitude', 'Exif.Photo.longitude'] + # Values to write + values = [new_comment, latitude, longitude] + + # Create metadata object with all data to write + for key, value in zip(keys, values): + metadata[key] = pyexiv2.ExifTag(key, value) + + # Write Exif tags to file, if not in dry-run mode + if dry_run == 'True': + metadata.write() + return new_comment diff --git a/rad_tag.py b/rad_tag.py index a532732..6b14f8e 100755 --- a/rad_tag.py +++ b/rad_tag.py @@ -1,20 +1,18 @@ #!/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 tag to DateTime in a csv log of a GeigerMuellerCounter and writes its value to the UserComment -Exif tag in µS/h""" +Exif tag in µS/h ''' -from datetime import datetime, timedelta import os import shutil import csv import argparse import pytz -import pyexiv2 import gpxpy -from functions import Radiation +from functions import Radiation, Photo # SIFACTOR for GQ Geiger counters @@ -110,14 +108,14 @@ for src_photo in args.photos: else: photo = src_photo - # Load Exif data from image - metadata = pyexiv2.ImageMetadata(photo) - metadata.read() - tag = metadata['Exif.Photo.DateTimeOriginal'] - # tag.value creates datetime object in pictime - pic_naive_time = tag.value - # Set timezone - pic_time = pic_naive_time.astimezone(local_timezone) + pic_aware_time = Photo(photo, local_timezone) + print(photo_basename, pic_aware_time) + + # Here the matching magic has to happen + + # Write exif data + # exif_tags = Photo.write_exif(radiation, latitude, longitude, args.dry) + # print(exif_tags) # Print table header print('{:<15} {:<25} {:<22}'.format('filename', 'date / time', 'Exif UserComment')) From 92b08170f2a3dc6842f87ae05505c84198234ce6 Mon Sep 17 00:00:00 2001 From: Commander1024 Date: Tue, 10 Mar 2020 21:53:43 +0100 Subject: [PATCH 15/39] Changed CSV variable, only write Exif tags that hava values to fill in. --- functions.py | 4 +++- rad_tag.py | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/functions.py b/functions.py index 7f41ba0..539a569 100644 --- a/functions.py +++ b/functions.py @@ -61,7 +61,9 @@ class Photo: # Create metadata object with all data to write for key, value in zip(keys, values): - metadata[key] = pyexiv2.ExifTag(key, value) + # Only create object if there is anything to fill with + if value is not None: + metadata[key] = pyexiv2.ExifTag(key, value) # Write Exif tags to file, if not in dry-run mode if dry_run == 'True': diff --git a/rad_tag.py b/rad_tag.py index 6b14f8e..eec245a 100755 --- a/rad_tag.py +++ b/rad_tag.py @@ -56,10 +56,10 @@ position_list = [] # Import GeigerCounter log with open(args.csv, "r") as f: - csv_reader = csv.reader(filter(lambda row: row[0] != '#', f), - delimiter=',', skipinitialspace=True) + csv = csv.reader(filter(lambda row: row[0] != '#', f), + delimiter=',', skipinitialspace=True) - for _, csv_raw_time, csv_raw_cpm, _ in csv_reader: + for _, csv_raw_time, csv_raw_cpm, _ in csv: radiation = Radiation(csv_raw_time, csv_raw_cpm, local_timezone, args.sifactor) radiation_list.append(radiation) #print(radiation_list) From 2ad8b4a1fb9d94acf4ae5855e0a41af1762ce359 Mon Sep 17 00:00:00 2001 From: Commander1024 Date: Wed, 11 Mar 2020 22:26:03 +0100 Subject: [PATCH 16/39] Moved copying of photos to functions.py, simplified decision whether to copy photo or not. --- functions.py | 29 +++++++++++++++++++++++------ rad_tag.py | 25 +++---------------------- 2 files changed, 26 insertions(+), 28 deletions(-) diff --git a/functions.py b/functions.py index 539a569..8112c7f 100644 --- a/functions.py +++ b/functions.py @@ -4,6 +4,8 @@ ''' Classes used by main program. ''' from datetime import datetime +import os +import shutil import pyexiv2 class Radiation: @@ -28,12 +30,27 @@ class Radiation: class Photo: ''' Reads and writes Exif metadata''' - def __init__(self, photo, local_timezone): + def __init__(self, photo, local_timezone, dest_dir, dry_run): self.get_date = self._get_creation_date(photo, local_timezone) - self.photo = photo + self.copy_photo = self._copy_photo(photo, dest_dir, dry_run) def __repr__(self): - return 'Photo Creation Date: %s' % str(self.get_date) + return 'Photo: %s Creation Date: %s' % (str(self.copy_photo), 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. + # Get image file name out of path + photo_basename = os.path.basename(photo) + # be os aware and use the correct directory delimiter for destfile + dest_photo = os.path.join(dest_dir, photo_basename) + + # Copy photo to dest_dir and return its (new) filename + # if not in dry_run mode or if dest_dir is different from src_dir. + if dry_run is True: + return photo + elif dest_dir != '.': + shutil.copy(photo, dest_photo) + return dest_photo def _get_creation_date(self, photo, local_timezone): # Load Exif data from photo @@ -46,11 +63,11 @@ class Photo: pic_aware_time = pic_naive_time.astimezone(local_timezone) return pic_aware_time - def write_exif(self, radiation, latitude, longitude, dry_run): + def write_exif(self, photo, radiation, latitude, longitude, dry_run): ''' UNTESTED ! ''' - metadata = pyexiv2.ImageMetadata(self.photo) + metadata = pyexiv2.ImageMetadata(photo) # Set new UserComment new_comment = 'Radiation ☢ ' + str(radiation) + ' µS/h' @@ -66,6 +83,6 @@ class Photo: metadata[key] = pyexiv2.ExifTag(key, value) # Write Exif tags to file, if not in dry-run mode - if dry_run == 'True': + if dry_run is not True: metadata.write() return new_comment diff --git a/rad_tag.py b/rad_tag.py index eec245a..8ea2fdd 100755 --- a/rad_tag.py +++ b/rad_tag.py @@ -6,8 +6,6 @@ DateTimeOriginal from Exif tag to DateTime in a csv log of a GeigerMuellerCounter and writes its value to the UserComment Exif tag in µS/h ''' -import os -import shutil import csv import argparse import pytz @@ -91,30 +89,13 @@ else: print('Modifying photos in', str(args.outdir), '(copy)') for src_photo in args.photos: - # Get image file name out of path - photo_basename = os.path.basename(src_photo) - - # Decide whether to modify photo in place or to copy it to outdir first - # Then set the destination file as 'photo' to work on - if args.outdir == ".": - photo = src_photo - else: - # Be os aware and use the correct directory delimiter for destfile - dst_photo = os.path.join(args.outdir, photo_basename) - # Don't copy image if in dry-run mode - if args.dry == 'True': - shutil.copy(src_photo, dst_photo) - photo = dst_photo - else: - photo = src_photo - - pic_aware_time = Photo(photo, local_timezone) - print(photo_basename, pic_aware_time) + photo = Photo(src_photo, local_timezone, args.outdir, args.dry) + print(photo) # Here the matching magic has to happen # Write exif data - # exif_tags = Photo.write_exif(radiation, latitude, longitude, args.dry) + # exif_tags = Photo.write_exif(photo, radiation, latitude, longitude, args.dry) # print(exif_tags) # Print table header From f07eb0c6917611431fbba640d971b570fcc42130 Mon Sep 17 00:00:00 2001 From: Commander1024 Date: Thu, 12 Mar 2020 19:24:27 +0100 Subject: [PATCH 17/39] Renamed photo class attribute. Figured how to acces class objects. Moved write_exif to own Exif class to compile GPS coords and write them all. --- functions.py | 17 ++++++++++------- rad_tag.py | 18 ++++++++++-------- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/functions.py b/functions.py index 8112c7f..b1b2627 100644 --- a/functions.py +++ b/functions.py @@ -9,7 +9,7 @@ import shutil import pyexiv2 class Radiation: - ''' Handles CSV processing.''' + ''' Handles CSV processing. ''' 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) @@ -29,13 +29,13 @@ class Radiation: return radiation class Photo: - ''' Reads and writes Exif metadata''' + ''' Reads and writes Exif metadata. ''' def __init__(self, photo, local_timezone, dest_dir, dry_run): self.get_date = self._get_creation_date(photo, local_timezone) - self.copy_photo = self._copy_photo(photo, dest_dir, dry_run) + self.get_target_photo = self._copy_photo(photo, dest_dir, dry_run) def __repr__(self): - return 'Photo: %s Creation Date: %s' % (str(self.copy_photo), str(self.get_date)) + return 'Photo: %s Creation Date: %s' % (str(self.get_target_photo), 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. @@ -48,7 +48,7 @@ class Photo: # if not in dry_run mode or if dest_dir is different from src_dir. if dry_run is True: return photo - elif dest_dir != '.': + if dest_dir != '.': shutil.copy(photo, dest_photo) return dest_photo @@ -56,6 +56,7 @@ class Photo: # Load Exif data from photo metadata = pyexiv2.ImageMetadata(photo) metadata.read() + print(metadata) date = metadata['Exif.Photo.DateTimeOriginal'] # date.value creates datetime object in pic_naive_time pic_naive_time = date.value @@ -63,12 +64,14 @@ class Photo: pic_aware_time = pic_naive_time.astimezone(local_timezone) return pic_aware_time - def write_exif(self, photo, radiation, latitude, longitude, dry_run): +class Exif: + ''' Compiles and writes Exif-Tags from given values. ''' + def write_exif(photo, radiation, latitude, longitude, dry_run): ''' UNTESTED ! ''' metadata = pyexiv2.ImageMetadata(photo) - + metadata.read() # Set new UserComment new_comment = 'Radiation ☢ ' + str(radiation) + ' µS/h' # Exif tags to write diff --git a/rad_tag.py b/rad_tag.py index 8ea2fdd..ed614b5 100755 --- a/rad_tag.py +++ b/rad_tag.py @@ -6,6 +6,7 @@ DateTimeOriginal from Exif tag to DateTime in a csv log of a GeigerMuellerCounter and writes its value to the UserComment Exif tag in µS/h ''' +from datetime import datetime import csv import argparse import pytz @@ -74,10 +75,9 @@ if args.gpx is not None: for track in gpx_reader.tracks: for segment in track.segments: for point in segment.points: - position = [point.time, point.latitude, point.longitude] + position = [point.time, point.latitude, point.longitude, point.elevation] position_list.append(position) - #print(position_list) - + # print(position_list) # Inform the user about what is going to happen if args.dry is True: @@ -90,13 +90,15 @@ else: for src_photo in args.photos: photo = Photo(src_photo, local_timezone, args.outdir, args.dry) - print(photo) + print(photo.get_target_photo, photo.get_date) # Here the matching magic has to happen - + latitude = '51.0234024' + longitude = '7.248347' + radiation = '9001' # Write exif data - # exif_tags = Photo.write_exif(photo, radiation, latitude, longitude, args.dry) - # print(exif_tags) + #exif_tags = Photo.write_exif(photo.get_target_photo, radiation, latitude, longitude, args.dry) + #print(exif_tags) # Print table header -print('{:<15} {:<25} {:<22}'.format('filename', 'date / time', 'Exif UserComment')) +print('{:<15} {:<25} {:<22}'.format('filename', 'date / time', 'Exif UserComment')) \ No newline at end of file From cafda4cf354567339a50428986249bac7ddb904f Mon Sep 17 00:00:00 2001 From: Commander1024 Date: Thu, 12 Mar 2020 20:35:49 +0100 Subject: [PATCH 18/39] Added function which converts decimal position value to WGS-84 notation. --- functions.py | 44 ++++++++++++++++++++++++++++++++++++-------- rad_tag.py | 12 +++++++----- 2 files changed, 43 insertions(+), 13 deletions(-) diff --git a/functions.py b/functions.py index b1b2627..fcdb947 100644 --- a/functions.py +++ b/functions.py @@ -7,6 +7,7 @@ from datetime import datetime import os import shutil import pyexiv2 +import fractions class Radiation: ''' Handles CSV processing. ''' @@ -65,13 +66,39 @@ class Photo: return pic_aware_time class Exif: - ''' Compiles and writes Exif-Tags from given values. ''' - def write_exif(photo, radiation, latitude, longitude, dry_run): + ''' Converts, compiles and writes Exif-Tags from given values. ''' + def __init__(self, photo, radiation, latitude, longitude, elevation, dry_run): + # self.get_degree = self._to_degree(value, loc) + self.write_exif = self._write_exif(photo, radiation, latitude, longitude, elevation, dry_run) - ''' UNTESTED ! ''' +# def __repr__(self): +# return 'Photo: %s Creation Date: %s' % (str(self.get_target_photo), str(self.get_date)) + + def _to_degree(self, value, loc): + if value < 0: + loc_value = loc[0] + elif value > 0: + loc_value = loc[1] + else: + loc_value = "" + abs_value = abs(value) + deg = int(abs_value) + t1 = (abs_value - deg) * 60 + minute = int(t1) + second = round((t1 - minute) * 60, 5) + return (deg, minute, second, loc_value) + + def _write_exif(self, photo, radiation, latitude, longitude, elevation, dry_run): metadata = pyexiv2.ImageMetadata(photo) metadata.read() + + lat_deg = self._to_degree(latitude, ["S", "N"]) + lng_deg = self._to_degree(longitude, ["W", "E"]) + + print(lat_deg) + print(lng_deg) + # Set new UserComment new_comment = 'Radiation ☢ ' + str(radiation) + ' µS/h' # Exif tags to write @@ -80,12 +107,13 @@ class Exif: values = [new_comment, latitude, longitude] # Create metadata object with all data to write - for key, value in zip(keys, values): + #for key, value in zip(keys, values): # Only create object if there is anything to fill with - if value is not None: - metadata[key] = pyexiv2.ExifTag(key, value) + # if value is not None: + # metadata[key] = pyexiv2.ExifTag(key, value) # Write Exif tags to file, if not in dry-run mode - if dry_run is not True: - metadata.write() + #if dry_run is not True: + # metadata.write() + return new_comment diff --git a/rad_tag.py b/rad_tag.py index ed614b5..81b4196 100755 --- a/rad_tag.py +++ b/rad_tag.py @@ -11,7 +11,7 @@ import csv import argparse import pytz import gpxpy -from functions import Radiation, Photo +from functions import Radiation, Photo, Exif # SIFACTOR for GQ Geiger counters @@ -93,12 +93,14 @@ for src_photo in args.photos: print(photo.get_target_photo, photo.get_date) # Here the matching magic has to happen - latitude = '51.0234024' - longitude = '7.248347' + latitude = 51.0234024 + longitude = 7.248347 radiation = '9001' + elevation = '55' + # Write exif data - #exif_tags = Photo.write_exif(photo.get_target_photo, radiation, latitude, longitude, args.dry) - #print(exif_tags) + exif_tags = Exif(photo.get_target_photo, radiation, latitude, longitude, elevation, args.dry) + print(exif_tags) # Print table header print('{:<15} {:<25} {:<22}'.format('filename', 'date / time', 'Exif UserComment')) \ No newline at end of file From 693412316eb250a9dbd6327c0ea98fb807651d9e Mon Sep 17 00:00:00 2001 From: Commander1024 Date: Thu, 12 Mar 2020 23:25:18 +0100 Subject: [PATCH 19/39] Added write_exif function, calculates location in degree / minutes. Assembles metadata info and writes them to the target_image. --- functions.py | 56 ++++++++++++++++++++++++++++++++++++---------------- rad_tag.py | 8 ++------ 2 files changed, 41 insertions(+), 23 deletions(-) diff --git a/functions.py b/functions.py index fcdb947..3b65f81 100644 --- a/functions.py +++ b/functions.py @@ -6,8 +6,8 @@ from datetime import datetime import os import shutil +from fractions import Fraction import pyexiv2 -import fractions class Radiation: ''' Handles CSV processing. ''' @@ -66,7 +66,17 @@ class Photo: return pic_aware_time class Exif: - ''' Converts, compiles and writes Exif-Tags from given values. ''' + ''' Converts, compiles and writes Exif-Tags from given values. + + Arguments: + photo: file name of photo to modify + radiation: radiation levels in µS/h + latitude: latitude as float + longitude: longitude as float + elevation: elevation as float + dry_run: whether to acutally write (True / False) + ''' + def __init__(self, photo, radiation, latitude, longitude, elevation, dry_run): # self.get_degree = self._to_degree(value, loc) self.write_exif = self._write_exif(photo, radiation, latitude, longitude, elevation, dry_run) @@ -93,27 +103,39 @@ class Exif: metadata = pyexiv2.ImageMetadata(photo) metadata.read() - lat_deg = self._to_degree(latitude, ["S", "N"]) - lng_deg = self._to_degree(longitude, ["W", "E"]) + latitude_degree = self._to_degree(latitude, ["S", "N"]) + longitude_degree = self._to_degree(longitude, ["W", "E"]) - print(lat_deg) - print(lng_deg) + print(latitude_degree) + print(longitude_degree) + + # 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)) # Set new UserComment new_comment = 'Radiation ☢ ' + str(radiation) + ' µS/h' - # Exif tags to write - keys = ['Exif.Photo.UserComment', 'Exif.Photo.latitude', 'Exif.Photo.longitude'] - # Values to write - values = [new_comment, latitude, longitude] - # Create metadata object with all data to write - #for key, value in zip(keys, values): - # Only create object if there is anything to fill with - # if value is not None: - # metadata[key] = pyexiv2.ExifTag(key, value) + # Exif tags to write + metadata['Exif.GPSInfo.GPSLatitude'] = exiv2_latitude + metadata['Exif.GPSInfo.GPSLatitudeRef'] = latitude_degree[3] + metadata['Exif.GPSInfo.GPSLongitude'] = exiv2_longitude + metadata['Exif.GPSInfo.GPSLongitudeRef'] = longitude_degree[3] + metadata['Exif.GPSInfo.GPSAltitude'] = Fraction(elevation) + metadata['Exif.GPSInfo.GPSAltitudeRef'] = '0' + metadata['Exif.Image.GPSTag'] = 654 + metadata['Exif.GPSInfo.GPSMapDatum'] = "WGS-84" + metadata['Exif.GPSInfo.GPSVersionID'] = '2 0 0 0' + metadata['Exif.Photo.UserComment'] = new_comment + + print(new_comment) # Write Exif tags to file, if not in dry-run mode - #if dry_run is not True: - # metadata.write() + if dry_run is not True: + metadata.write() return new_comment diff --git a/rad_tag.py b/rad_tag.py index 81b4196..f346055 100755 --- a/rad_tag.py +++ b/rad_tag.py @@ -6,7 +6,6 @@ DateTimeOriginal from Exif tag to DateTime in a csv log of a GeigerMuellerCounter and writes its value to the UserComment Exif tag in µS/h ''' -from datetime import datetime import csv import argparse import pytz @@ -61,7 +60,6 @@ with open(args.csv, "r") as f: for _, csv_raw_time, csv_raw_cpm, _ in csv: radiation = Radiation(csv_raw_time, csv_raw_cpm, local_timezone, args.sifactor) radiation_list.append(radiation) - #print(radiation_list) # close CSV file f.close() @@ -77,7 +75,6 @@ if args.gpx is not None: for point in segment.points: position = [point.time, point.latitude, point.longitude, point.elevation] position_list.append(position) - # print(position_list) # Inform the user about what is going to happen if args.dry is True: @@ -95,12 +92,11 @@ for src_photo in args.photos: # Here the matching magic has to happen latitude = 51.0234024 longitude = 7.248347 - radiation = '9001' - elevation = '55' + radiation = 9001.15 + elevation = 56.079345703125 # Write exif data exif_tags = Exif(photo.get_target_photo, radiation, latitude, longitude, elevation, args.dry) - print(exif_tags) # Print table header print('{:<15} {:<25} {:<22}'.format('filename', 'date / time', 'Exif UserComment')) \ No newline at end of file From fad837d7bdf842ccf1152bfa1b105972358b7853 Mon Sep 17 00:00:00 2001 From: Commander1024 Date: Fri, 13 Mar 2020 07:21:24 +0100 Subject: [PATCH 20/39] Added new upcoming features to changelog. --- CHANGELOG.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ecf64b5..3e92bfc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# Changelog +## Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), @@ -11,7 +11,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added optional parameter to override timezone to local timezone, defaults to utc - Refactored variable name_scheme. - Switch to oop style programming. Made code easier to read. -- Put CSV processing and Exif reading / writing into a class. +- Moved CSV processing and Exif reading / writing into a class. +- Moved photo copying into Photo class +- Created Exif class. Can now also create GPS tags. ## [0.2] - 2020-02-05 ### Added From 8ea6524238cc771a9b0165361075695818adadf1 Mon Sep 17 00:00:00 2001 From: Commander1024 Date: Sat, 14 Mar 2020 16:21:23 +0100 Subject: [PATCH 21/39] Enriched class comments with arguments and return values. Made a text representation of Exif and removed debug outputs. --- functions.py | 53 ++++++++++++++++++++++++++++++++++++++-------------- rad_tag.py | 1 + 2 files changed, 40 insertions(+), 14 deletions(-) diff --git a/functions.py b/functions.py index 3b65f81..cc2d28b 100644 --- a/functions.py +++ b/functions.py @@ -10,7 +10,20 @@ from fractions import Fraction import pyexiv2 class Radiation: - ''' Handles CSV processing. ''' + ''' + Reiceives 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 + si_factor: CP/M to (µS/h) conversion factor - specific to GMC-tube + + Returns: + timestamp: timestamp of CSV value als datetime object + radiation: radiation in µS/h as str (for Exif comment, UTF-8) + ''' + 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) @@ -30,7 +43,20 @@ class Radiation: return radiation class Photo: - ''' Reads and writes Exif metadata. ''' + ''' + Reads Exif metadata. + + Arguments: + photo: source photo () + local_timezone: timezone for timezone-unware 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) + + Returns: + self.get_date: timestamp of photo als datetime object + self.get_target_photo: full path to photo file to work on + ''' + def __init__(self, photo, local_timezone, dest_dir, dry_run): self.get_date = self._get_creation_date(photo, local_timezone) self.get_target_photo = self._copy_photo(photo, dest_dir, dry_run) @@ -57,7 +83,6 @@ class Photo: # Load Exif data from photo metadata = pyexiv2.ImageMetadata(photo) metadata.read() - print(metadata) date = metadata['Exif.Photo.DateTimeOriginal'] # date.value creates datetime object in pic_naive_time pic_naive_time = date.value @@ -66,7 +91,8 @@ class Photo: return pic_aware_time class Exif: - ''' Converts, compiles and writes Exif-Tags from given values. + ''' + Converts, compiles and writes Exif-Tags from given arguemnts. Arguments: photo: file name of photo to modify @@ -75,14 +101,18 @@ class Exif: longitude: longitude as float elevation: elevation as float dry_run: whether to acutally write (True / False) + + Returns: + Latitude / Longitude in degrees + Exif-Comment that has been written (incl. radiation) ''' def __init__(self, photo, radiation, latitude, longitude, elevation, dry_run): - # self.get_degree = self._to_degree(value, loc) - self.write_exif = self._write_exif(photo, radiation, latitude, longitude, elevation, dry_run) + self.write_exif = self._write_exif(photo, radiation, latitude, + longitude, elevation, dry_run) -# def __repr__(self): -# return 'Photo: %s Creation Date: %s' % (str(self.get_target_photo), str(self.get_date)) + def __repr__(self): + return 'Position: %s, %s: %s ' % self.write_exif def _to_degree(self, value, loc): if value < 0: @@ -106,9 +136,6 @@ class Exif: latitude_degree = self._to_degree(latitude, ["S", "N"]) longitude_degree = self._to_degree(longitude, ["W", "E"]) - print(latitude_degree) - print(longitude_degree) - # 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), @@ -132,10 +159,8 @@ class Exif: metadata['Exif.GPSInfo.GPSVersionID'] = '2 0 0 0' metadata['Exif.Photo.UserComment'] = new_comment - print(new_comment) - # Write Exif tags to file, if not in dry-run mode if dry_run is not True: metadata.write() - return new_comment + return latitude_degree, longitude_degree, new_comment diff --git a/rad_tag.py b/rad_tag.py index f346055..26efd2d 100755 --- a/rad_tag.py +++ b/rad_tag.py @@ -97,6 +97,7 @@ for src_photo in args.photos: # Write exif data exif_tags = Exif(photo.get_target_photo, radiation, latitude, longitude, elevation, args.dry) + print(exif_tags) # Print table header print('{:<15} {:<25} {:<22}'.format('filename', 'date / time', 'Exif UserComment')) \ No newline at end of file From da0932338024a5c83efe173d01f108a447cd7c8d Mon Sep 17 00:00:00 2001 From: Commander1024 Date: Sat, 14 Mar 2020 17:20:33 +0100 Subject: [PATCH 22/39] Remove now obsoluete rad_tag.py --- rad_tag.py | 103 ----------------------------------------------------- 1 file changed, 103 deletions(-) delete mode 100755 rad_tag.py diff --git a/rad_tag.py b/rad_tag.py deleted file mode 100755 index 26efd2d..0000000 --- a/rad_tag.py +++ /dev/null @@ -1,103 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -''' Iterates over a bunch of .jpg or .cr2 files and matches -DateTimeOriginal from Exif tag to DateTime in a csv log -of a GeigerMuellerCounter and writes its value to the UserComment -Exif tag in µS/h ''' - -import csv -import argparse -import pytz -import gpxpy -from functions import Radiation, Photo, Exif - -# SIFACTOR for GQ Geiger counters - -# 300 series: 0.0065 µSv/h / CPM -# 320 series: 0.0065 µSv/h / CPM -# 500 series: 0.0065 µSv/h / CPM -# 500+ series: 0.0065 µSv/h / CPM for the first tube -# 600 series: 0.0065 µSv/h / CPM -# 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 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.') - -args = parser.parse_args() - -# Create timezone datetime object -local_timezone = pytz.timezone(args.timezone) - -# Initialize two empty lists for comparison -radiation_list = [] -position_list = [] - -# Import GeigerCounter log -with open(args.csv, "r") as f: - csv = csv.reader(filter(lambda row: row[0] != '#', f), - delimiter=',', skipinitialspace=True) - - for _, csv_raw_time, csv_raw_cpm, _ in csv: - radiation = Radiation(csv_raw_time, csv_raw_cpm, local_timezone, args.sifactor) - radiation_list.append(radiation) - - # close CSV file - f.close() - -# Import GPX track(s) -if args.gpx is not None: - gpx_file = open(args.gpx, 'r') - gpx_reader = gpxpy.parse(gpx_file) - - for waypoint in gpx_reader.waypoints: - for track in gpx_reader.tracks: - for segment in track.segments: - for point in segment.points: - position = [point.time, point.latitude, point.longitude, point.elevation] - position_list.append(position) - -# Inform the user about what is going to happen -if args.dry is True: - print('Not modifying anything. Just print what would happen without --dry') -else: - if args.outdir == ".": - print('Modifying photos in place (overwrite)') - else: - print('Modifying photos in', str(args.outdir), '(copy)') - -for src_photo in args.photos: - photo = Photo(src_photo, local_timezone, args.outdir, args.dry) - print(photo.get_target_photo, photo.get_date) - - # Here the matching magic has to happen - latitude = 51.0234024 - longitude = 7.248347 - radiation = 9001.15 - elevation = 56.079345703125 - - # Write exif data - exif_tags = Exif(photo.get_target_photo, radiation, latitude, longitude, elevation, args.dry) - print(exif_tags) - -# Print table header -print('{:<15} {:<25} {:<22}'.format('filename', 'date / time', 'Exif UserComment')) \ No newline at end of file From ce65eb4ceee7ae32cd8bcd6a487814c0bfd52127 Mon Sep 17 00:00:00 2001 From: Commander1024 Date: Sat, 14 Mar 2020 17:32:29 +0100 Subject: [PATCH 23/39] Revert "Enriched class comments with arguments and return values." This reverts commit 8ea6524238cc771a9b0165361075695818adadf1. I'm stupid, and deleted the wrong file. Fixed it. --- exif_rad.py | 141 --------------------------------------------------- functions.py | 53 +++++-------------- rad_tag.py | 103 +++++++++++++++++++++++++++++++++++++ 3 files changed, 117 insertions(+), 180 deletions(-) delete mode 100755 exif_rad.py create mode 100644 rad_tag.py diff --git a/exif_rad.py b/exif_rad.py deleted file mode 100755 index 046ae64..0000000 --- a/exif_rad.py +++ /dev/null @@ -1,141 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# This file is now obsolete and is going to disappear. - -"""Iterates over a bunch of .jpg or .cr2 files and matches -DateTimeOriginal from Exif tag to DateTime in a csv log -of a GeigerMuellerCounter and writes its value to the UserComment -Exif tag in µS/h""" - -from datetime import datetime, timedelta -import os -import shutil -import csv -import argparse -import pytz -import pyexiv2 -import gpxpy - -# SIFACTOR for GQ Geiger counters - -# 300 series: 0.0065 µSv/h / CPM -# 320 series: 0.0065 µSv/h / CPM -# 500 series: 0.0065 µSv/h / CPM -# 500+ series: 0.0065 µSv/h / CPM for the first tube -# 600 series: 0.0065 µSv/h / CPM -# 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 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.') - -args = parser.parse_args() - -# Create timezone datetime object -localtz = pytz.timezone(args.timezone) - -# Inform the user about what is going to happen -if args.dry is not None: - if args.outdir == ".": - print('Modifying photos in place (overwrite)') - else: - print('Modifying photos in', str(args.outdir), '(copy)') -else: - print('Not modifying anything. Just print what would happen without --dry') - -# Print table header -print('{:<15} {:<25} {:<22}'.format('filename', 'date / time', 'Exif UserComment')) - -for srcphoto in args.photos: - # Get image file name out of path - photo_basename = os.path.basename(srcphoto) - - # Decide whether to modify photo in place or to copy it to outdir first - # Then set the destination file as 'photo' to work on - if args.outdir == ".": - photo = srcphoto - else: - # be os aware and use the correct directory delimiter for destfile - dstphoto = os.path.join(args.outdir, photo_basename) - # Don't copy image if in dry-run mode - if args.dry == 'True': - shutil.copy(srcphoto, dstphoto) - photo = dstphoto - else: - photo = srcphoto - - # Load Exif data from image - metadata = pyexiv2.ImageMetadata(photo) - metadata.read() - tag = metadata['Exif.Photo.DateTimeOriginal'] - # tag.value creates datetime object in pictime - picnaivetime = tag.value - # Set timezone - pictime = picnaivetime.astimezone(localtz) - - # Import GeigerCounter log - with open(args.csv, "r") as f: - csvreader = csv.reader(filter(lambda row: row[0] != '#', f), - delimiter=',', skipinitialspace=True) - - for _, csvrawtime, csvrawcpm, _ in csvreader: - csvnaivetime = datetime.fromisoformat(csvrawtime) - # Set timezone - csvtime = csvnaivetime.astimezone(localtz) - # Process image if its timestamp is found in CSV log (compares 2 datetime objects) - if csvtime == pictime: - rad = round(float(csvrawcpm) * args.sifactor, 2) - - # Set key, value for new UserComment - key = 'Exif.Photo.UserComment' - new_comment = 'Radiation ☢ ' + str(rad) + ' µS/h' - metadata[key] = pyexiv2.ExifTag(key, new_comment) - - # print found radiation levels - print('{:<15} {:<25} {:<22}'.format(photo_basename, str(pictime), new_comment)) - # Write Exif tags to file, if not in dry-run mode - if args.dry == 'True': - metadata.write() - break - else: - print('{:<15} {:<25} {:<22}'.format(photo_basename, str(pictime), 'NOT FOUND!')) - # close CSV file - f.close() - - # Import GPX track(s) - if args.gpx is not None: - gpx_file = open(args.gpx, 'r') - gpxreader = gpxpy.parse(gpx_file) - - for waypoint in gpxreader.waypoints: - for track in gpxreader.tracks: - for segment in track.segments: - for point in segment.points: - # datetimes match with 1 minute precision - delta = timedelta(seconds=10) - if abs(point.time - pictime) < delta: - valuelist = [] - row = [point.time, point.latitude, point.longitude] - valuelist.append(row) - print(valuelist) - #print(pictime, 'vs.', point.time, 'Delta:', pictime - point.time) -# print('Point at ({0},{1}) -> {2}'.format(point.latitude, point.longitude, point.time)) diff --git a/functions.py b/functions.py index cc2d28b..3b65f81 100644 --- a/functions.py +++ b/functions.py @@ -10,20 +10,7 @@ from fractions import Fraction import pyexiv2 class Radiation: - ''' - Reiceives 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 - si_factor: CP/M to (µS/h) conversion factor - specific to GMC-tube - - Returns: - timestamp: timestamp of CSV value als datetime object - radiation: radiation in µS/h as str (for Exif comment, UTF-8) - ''' - + ''' Handles CSV processing. ''' 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) @@ -43,20 +30,7 @@ class Radiation: return radiation class Photo: - ''' - Reads Exif metadata. - - Arguments: - photo: source photo () - local_timezone: timezone for timezone-unware 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) - - Returns: - self.get_date: timestamp of photo als datetime object - self.get_target_photo: full path to photo file to work on - ''' - + ''' Reads and writes Exif metadata. ''' def __init__(self, photo, local_timezone, dest_dir, dry_run): self.get_date = self._get_creation_date(photo, local_timezone) self.get_target_photo = self._copy_photo(photo, dest_dir, dry_run) @@ -83,6 +57,7 @@ class Photo: # Load Exif data from photo metadata = pyexiv2.ImageMetadata(photo) metadata.read() + print(metadata) date = metadata['Exif.Photo.DateTimeOriginal'] # date.value creates datetime object in pic_naive_time pic_naive_time = date.value @@ -91,8 +66,7 @@ class Photo: return pic_aware_time class Exif: - ''' - Converts, compiles and writes Exif-Tags from given arguemnts. + ''' Converts, compiles and writes Exif-Tags from given values. Arguments: photo: file name of photo to modify @@ -101,18 +75,14 @@ class Exif: longitude: longitude as float elevation: elevation as float dry_run: whether to acutally write (True / False) - - Returns: - Latitude / Longitude in degrees - Exif-Comment that has been written (incl. radiation) ''' def __init__(self, photo, radiation, latitude, longitude, elevation, dry_run): - self.write_exif = self._write_exif(photo, radiation, latitude, - longitude, elevation, dry_run) + # self.get_degree = self._to_degree(value, loc) + self.write_exif = self._write_exif(photo, radiation, latitude, longitude, elevation, dry_run) - def __repr__(self): - return 'Position: %s, %s: %s ' % self.write_exif +# def __repr__(self): +# return 'Photo: %s Creation Date: %s' % (str(self.get_target_photo), str(self.get_date)) def _to_degree(self, value, loc): if value < 0: @@ -136,6 +106,9 @@ class Exif: latitude_degree = self._to_degree(latitude, ["S", "N"]) longitude_degree = self._to_degree(longitude, ["W", "E"]) + print(latitude_degree) + print(longitude_degree) + # 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), @@ -159,8 +132,10 @@ class Exif: metadata['Exif.GPSInfo.GPSVersionID'] = '2 0 0 0' metadata['Exif.Photo.UserComment'] = new_comment + print(new_comment) + # Write Exif tags to file, if not in dry-run mode if dry_run is not True: metadata.write() - return latitude_degree, longitude_degree, new_comment + return new_comment diff --git a/rad_tag.py b/rad_tag.py new file mode 100644 index 0000000..26efd2d --- /dev/null +++ b/rad_tag.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +''' Iterates over a bunch of .jpg or .cr2 files and matches +DateTimeOriginal from Exif tag to DateTime in a csv log +of a GeigerMuellerCounter and writes its value to the UserComment +Exif tag in µS/h ''' + +import csv +import argparse +import pytz +import gpxpy +from functions import Radiation, Photo, Exif + +# SIFACTOR for GQ Geiger counters + +# 300 series: 0.0065 µSv/h / CPM +# 320 series: 0.0065 µSv/h / CPM +# 500 series: 0.0065 µSv/h / CPM +# 500+ series: 0.0065 µSv/h / CPM for the first tube +# 600 series: 0.0065 µSv/h / CPM +# 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 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.') + +args = parser.parse_args() + +# Create timezone datetime object +local_timezone = pytz.timezone(args.timezone) + +# Initialize two empty lists for comparison +radiation_list = [] +position_list = [] + +# Import GeigerCounter log +with open(args.csv, "r") as f: + csv = csv.reader(filter(lambda row: row[0] != '#', f), + delimiter=',', skipinitialspace=True) + + for _, csv_raw_time, csv_raw_cpm, _ in csv: + radiation = Radiation(csv_raw_time, csv_raw_cpm, local_timezone, args.sifactor) + radiation_list.append(radiation) + + # close CSV file + f.close() + +# Import GPX track(s) +if args.gpx is not None: + gpx_file = open(args.gpx, 'r') + gpx_reader = gpxpy.parse(gpx_file) + + for waypoint in gpx_reader.waypoints: + for track in gpx_reader.tracks: + for segment in track.segments: + for point in segment.points: + position = [point.time, point.latitude, point.longitude, point.elevation] + position_list.append(position) + +# Inform the user about what is going to happen +if args.dry is True: + print('Not modifying anything. Just print what would happen without --dry') +else: + if args.outdir == ".": + print('Modifying photos in place (overwrite)') + else: + print('Modifying photos in', str(args.outdir), '(copy)') + +for src_photo in args.photos: + photo = Photo(src_photo, local_timezone, args.outdir, args.dry) + print(photo.get_target_photo, photo.get_date) + + # Here the matching magic has to happen + latitude = 51.0234024 + longitude = 7.248347 + radiation = 9001.15 + elevation = 56.079345703125 + + # Write exif data + exif_tags = Exif(photo.get_target_photo, radiation, latitude, longitude, elevation, args.dry) + print(exif_tags) + +# Print table header +print('{:<15} {:<25} {:<22}'.format('filename', 'date / time', 'Exif UserComment')) \ No newline at end of file From eef986bdbc97d20199cf8901ff0398fff2615d5f Mon Sep 17 00:00:00 2001 From: Commander1024 Date: Sat, 14 Mar 2020 17:53:38 +0100 Subject: [PATCH 24/39] Revert "Remove now obsoluete rad_tag.py" This reverts commit da0932338024a5c83efe173d01f108a447cd7c8d. --- rad_tag.py | 103 ----------------------------------------------------- 1 file changed, 103 deletions(-) delete mode 100644 rad_tag.py diff --git a/rad_tag.py b/rad_tag.py deleted file mode 100644 index 26efd2d..0000000 --- a/rad_tag.py +++ /dev/null @@ -1,103 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -''' Iterates over a bunch of .jpg or .cr2 files and matches -DateTimeOriginal from Exif tag to DateTime in a csv log -of a GeigerMuellerCounter and writes its value to the UserComment -Exif tag in µS/h ''' - -import csv -import argparse -import pytz -import gpxpy -from functions import Radiation, Photo, Exif - -# SIFACTOR for GQ Geiger counters - -# 300 series: 0.0065 µSv/h / CPM -# 320 series: 0.0065 µSv/h / CPM -# 500 series: 0.0065 µSv/h / CPM -# 500+ series: 0.0065 µSv/h / CPM for the first tube -# 600 series: 0.0065 µSv/h / CPM -# 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 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.') - -args = parser.parse_args() - -# Create timezone datetime object -local_timezone = pytz.timezone(args.timezone) - -# Initialize two empty lists for comparison -radiation_list = [] -position_list = [] - -# Import GeigerCounter log -with open(args.csv, "r") as f: - csv = csv.reader(filter(lambda row: row[0] != '#', f), - delimiter=',', skipinitialspace=True) - - for _, csv_raw_time, csv_raw_cpm, _ in csv: - radiation = Radiation(csv_raw_time, csv_raw_cpm, local_timezone, args.sifactor) - radiation_list.append(radiation) - - # close CSV file - f.close() - -# Import GPX track(s) -if args.gpx is not None: - gpx_file = open(args.gpx, 'r') - gpx_reader = gpxpy.parse(gpx_file) - - for waypoint in gpx_reader.waypoints: - for track in gpx_reader.tracks: - for segment in track.segments: - for point in segment.points: - position = [point.time, point.latitude, point.longitude, point.elevation] - position_list.append(position) - -# Inform the user about what is going to happen -if args.dry is True: - print('Not modifying anything. Just print what would happen without --dry') -else: - if args.outdir == ".": - print('Modifying photos in place (overwrite)') - else: - print('Modifying photos in', str(args.outdir), '(copy)') - -for src_photo in args.photos: - photo = Photo(src_photo, local_timezone, args.outdir, args.dry) - print(photo.get_target_photo, photo.get_date) - - # Here the matching magic has to happen - latitude = 51.0234024 - longitude = 7.248347 - radiation = 9001.15 - elevation = 56.079345703125 - - # Write exif data - exif_tags = Exif(photo.get_target_photo, radiation, latitude, longitude, elevation, args.dry) - print(exif_tags) - -# Print table header -print('{:<15} {:<25} {:<22}'.format('filename', 'date / time', 'Exif UserComment')) \ No newline at end of file From 9ac706075aa59efef02433701c822a07546829d0 Mon Sep 17 00:00:00 2001 From: Commander1024 Date: Sat, 14 Mar 2020 18:02:40 +0100 Subject: [PATCH 25/39] Revert "Remove now obsoluete rad_tag.py" This reverts commit da0932338024a5c83efe173d01f108a447cd7c8d. --- rad_tag.py | 103 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100755 rad_tag.py diff --git a/rad_tag.py b/rad_tag.py new file mode 100755 index 0000000..26efd2d --- /dev/null +++ b/rad_tag.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +''' Iterates over a bunch of .jpg or .cr2 files and matches +DateTimeOriginal from Exif tag to DateTime in a csv log +of a GeigerMuellerCounter and writes its value to the UserComment +Exif tag in µS/h ''' + +import csv +import argparse +import pytz +import gpxpy +from functions import Radiation, Photo, Exif + +# SIFACTOR for GQ Geiger counters + +# 300 series: 0.0065 µSv/h / CPM +# 320 series: 0.0065 µSv/h / CPM +# 500 series: 0.0065 µSv/h / CPM +# 500+ series: 0.0065 µSv/h / CPM for the first tube +# 600 series: 0.0065 µSv/h / CPM +# 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 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.') + +args = parser.parse_args() + +# Create timezone datetime object +local_timezone = pytz.timezone(args.timezone) + +# Initialize two empty lists for comparison +radiation_list = [] +position_list = [] + +# Import GeigerCounter log +with open(args.csv, "r") as f: + csv = csv.reader(filter(lambda row: row[0] != '#', f), + delimiter=',', skipinitialspace=True) + + for _, csv_raw_time, csv_raw_cpm, _ in csv: + radiation = Radiation(csv_raw_time, csv_raw_cpm, local_timezone, args.sifactor) + radiation_list.append(radiation) + + # close CSV file + f.close() + +# Import GPX track(s) +if args.gpx is not None: + gpx_file = open(args.gpx, 'r') + gpx_reader = gpxpy.parse(gpx_file) + + for waypoint in gpx_reader.waypoints: + for track in gpx_reader.tracks: + for segment in track.segments: + for point in segment.points: + position = [point.time, point.latitude, point.longitude, point.elevation] + position_list.append(position) + +# Inform the user about what is going to happen +if args.dry is True: + print('Not modifying anything. Just print what would happen without --dry') +else: + if args.outdir == ".": + print('Modifying photos in place (overwrite)') + else: + print('Modifying photos in', str(args.outdir), '(copy)') + +for src_photo in args.photos: + photo = Photo(src_photo, local_timezone, args.outdir, args.dry) + print(photo.get_target_photo, photo.get_date) + + # Here the matching magic has to happen + latitude = 51.0234024 + longitude = 7.248347 + radiation = 9001.15 + elevation = 56.079345703125 + + # Write exif data + exif_tags = Exif(photo.get_target_photo, radiation, latitude, longitude, elevation, args.dry) + print(exif_tags) + +# Print table header +print('{:<15} {:<25} {:<22}'.format('filename', 'date / time', 'Exif UserComment')) \ No newline at end of file From f6a54a5855e1f024d64cf0e7ec7320c8ef0cdefd Mon Sep 17 00:00:00 2001 From: Commander1024 Date: Sat, 14 Mar 2020 18:03:59 +0100 Subject: [PATCH 26/39] Holy Moly! Such chaos. Hopefully it is fixed now. --- functions.py | 55 ++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 40 insertions(+), 15 deletions(-) diff --git a/functions.py b/functions.py index 3b65f81..2050f9a 100644 --- a/functions.py +++ b/functions.py @@ -10,7 +10,20 @@ from fractions import Fraction import pyexiv2 class Radiation: - ''' Handles CSV processing. ''' + ''' + Reiceives 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 + si_factor: CP/M to (µS/h) conversion factor - specific to GMC-tube + + Returns: + timestamp: timestamp of CSV value als datetime object + radiation: radiation in µS/h as str (for Exif comment, UTF-8) + ''' + 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) @@ -30,7 +43,20 @@ class Radiation: return radiation class Photo: - ''' Reads and writes Exif metadata. ''' + ''' + Reads Exif metadata. + + Arguments: + photo: source photo () + local_timezone: timezone for timezone-unware 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) + + Returns: + self.get_date: timestamp of photo als datetime object + self.get_target_photo: full path to photo file to work on + ''' + def __init__(self, photo, local_timezone, dest_dir, dry_run): self.get_date = self._get_creation_date(photo, local_timezone) self.get_target_photo = self._copy_photo(photo, dest_dir, dry_run) @@ -57,7 +83,6 @@ class Photo: # Load Exif data from photo metadata = pyexiv2.ImageMetadata(photo) metadata.read() - print(metadata) date = metadata['Exif.Photo.DateTimeOriginal'] # date.value creates datetime object in pic_naive_time pic_naive_time = date.value @@ -66,7 +91,8 @@ class Photo: return pic_aware_time class Exif: - ''' Converts, compiles and writes Exif-Tags from given values. + ''' + Converts, compiles and writes Exif-Tags from given arguemnts. Arguments: photo: file name of photo to modify @@ -75,14 +101,18 @@ class Exif: longitude: longitude as float elevation: elevation as float dry_run: whether to acutally write (True / False) + + Returns: + Latitude / Longitude in degrees + Exif-Comment that has been written (incl. radiation) ''' def __init__(self, photo, radiation, latitude, longitude, elevation, dry_run): - # self.get_degree = self._to_degree(value, loc) - self.write_exif = self._write_exif(photo, radiation, latitude, longitude, elevation, dry_run) - -# def __repr__(self): -# return 'Photo: %s Creation Date: %s' % (str(self.get_target_photo), str(self.get_date)) + self.write_exif = self._write_exif(photo, radiation, latitude, + longitude, elevation, dry_run) + + def __repr__(self): + return 'Position: %s, %s: %s ' % self.write_exif def _to_degree(self, value, loc): if value < 0: @@ -106,9 +136,6 @@ class Exif: latitude_degree = self._to_degree(latitude, ["S", "N"]) longitude_degree = self._to_degree(longitude, ["W", "E"]) - print(latitude_degree) - print(longitude_degree) - # 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), @@ -132,10 +159,8 @@ class Exif: metadata['Exif.GPSInfo.GPSVersionID'] = '2 0 0 0' metadata['Exif.Photo.UserComment'] = new_comment - print(new_comment) - # Write Exif tags to file, if not in dry-run mode if dry_run is not True: metadata.write() - return new_comment + return latitude_degree, longitude_degree, new_comment From 13555a050531fba0fccd4512f24cbcf75e984f04 Mon Sep 17 00:00:00 2001 From: Commander1024 Date: Sat, 14 Mar 2020 20:21:44 +0100 Subject: [PATCH 27/39] Made optional Values optional in Exif class. --- functions.py | 59 ++++++++++++++++++++++++++++++---------------------- rad_tag.py | 8 +++---- 2 files changed, 38 insertions(+), 29 deletions(-) diff --git a/functions.py b/functions.py index 2050f9a..1b6ada2 100644 --- a/functions.py +++ b/functions.py @@ -107,7 +107,7 @@ class Exif: Exif-Comment that has been written (incl. radiation) ''' - def __init__(self, photo, radiation, latitude, longitude, elevation, dry_run): + def __init__(self, photo, dry_run, radiation=False, latitude=False, longitude=False, elevation=False): self.write_exif = self._write_exif(photo, radiation, latitude, longitude, elevation, dry_run) @@ -128,36 +128,45 @@ class Exif: second = round((t1 - minute) * 60, 5) return (deg, minute, second, loc_value) - def _write_exif(self, photo, radiation, latitude, longitude, elevation, dry_run): + def _write_exif(self, photo, dry_run, radiation, latitude, longitude, elevation): metadata = pyexiv2.ImageMetadata(photo) metadata.read() - latitude_degree = self._to_degree(latitude, ["S", "N"]) - longitude_degree = self._to_degree(longitude, ["W", "E"]) + if latitude or longitude: + latitude_degree = self._to_degree(latitude, ["S", "N"]) + 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)) + # 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)) - # Set new UserComment - new_comment = 'Radiation ☢ ' + str(radiation) + ' µS/h' - - # Exif tags to write - metadata['Exif.GPSInfo.GPSLatitude'] = exiv2_latitude - metadata['Exif.GPSInfo.GPSLatitudeRef'] = latitude_degree[3] - metadata['Exif.GPSInfo.GPSLongitude'] = exiv2_longitude - metadata['Exif.GPSInfo.GPSLongitudeRef'] = longitude_degree[3] - metadata['Exif.GPSInfo.GPSAltitude'] = Fraction(elevation) - metadata['Exif.GPSInfo.GPSAltitudeRef'] = '0' - metadata['Exif.Image.GPSTag'] = 654 - metadata['Exif.GPSInfo.GPSMapDatum'] = "WGS-84" - metadata['Exif.GPSInfo.GPSVersionID'] = '2 0 0 0' - metadata['Exif.Photo.UserComment'] = new_comment + # Exif tags to write + metadata['Exif.GPSInfo.GPSLatitude'] = exiv2_latitude + metadata['Exif.GPSInfo.GPSLatitudeRef'] = latitude_degree[3] + metadata['Exif.GPSInfo.GPSLongitude'] = exiv2_longitude + metadata['Exif.GPSInfo.GPSLongitudeRef'] = longitude_degree[3] + metadata['Exif.Image.GPSTag'] = 654 + metadata['Exif.GPSInfo.GPSMapDatum'] = "WGS-84" + metadata['Exif.GPSInfo.GPSVersionID'] = '2 0 0 0' + + if not elevation: + metadata['Exif.GPSInfo.GPSAltitude'] = Fraction(elevation) + metadata['Exif.GPSInfo.GPSAltitudeRef'] = '0' + else: + latitude_degree = None + longitude_degree = None + + if radiation: + # Set new UserComment + new_comment = 'Radiation ☢ ' + str(radiation) + ' µS/h' + metadata['Exif.Photo.UserComment'] = new_comment + else: + new_comment = None # Write Exif tags to file, if not in dry-run mode if dry_run is not True: diff --git a/rad_tag.py b/rad_tag.py index 26efd2d..43d3415 100755 --- a/rad_tag.py +++ b/rad_tag.py @@ -90,13 +90,13 @@ for src_photo in args.photos: print(photo.get_target_photo, photo.get_date) # Here the matching magic has to happen - latitude = 51.0234024 - longitude = 7.248347 + #latitude = 51.0234024 + #longitude = 7.248347 radiation = 9001.15 - elevation = 56.079345703125 + #elevation = 56.079345703125 # Write exif data - exif_tags = Exif(photo.get_target_photo, radiation, latitude, longitude, elevation, args.dry) + exif_tags = Exif(photo=photo.get_target_photo, dry_run=args.dry) print(exif_tags) # Print table header From 4a7de7b5186812927292694f3e25f1620b1f15d9 Mon Sep 17 00:00:00 2001 From: Commander1024 Date: Sat, 14 Mar 2020 20:42:01 +0100 Subject: [PATCH 28/39] Removed default value fuckery. The matching class wil have to provide proper values. --- functions.py | 10 +++++----- rad_tag.py | 9 +++++---- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/functions.py b/functions.py index 1b6ada2..2c5b3ef 100644 --- a/functions.py +++ b/functions.py @@ -107,9 +107,9 @@ class Exif: Exif-Comment that has been written (incl. radiation) ''' - def __init__(self, photo, dry_run, radiation=False, latitude=False, longitude=False, elevation=False): - self.write_exif = self._write_exif(photo, radiation, latitude, - longitude, elevation, dry_run) + 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 @@ -133,7 +133,7 @@ class Exif: metadata = pyexiv2.ImageMetadata(photo) metadata.read() - if latitude or longitude: + if latitude and longitude: latitude_degree = self._to_degree(latitude, ["S", "N"]) longitude_degree = self._to_degree(longitude, ["W", "E"]) @@ -163,7 +163,7 @@ class Exif: if radiation: # Set new UserComment - new_comment = 'Radiation ☢ ' + str(radiation) + ' µS/h' + new_comment = 'Radiation ☢ : ' + str(radiation) + ' µS/h' metadata['Exif.Photo.UserComment'] = new_comment else: new_comment = None diff --git a/rad_tag.py b/rad_tag.py index 43d3415..8432c02 100755 --- a/rad_tag.py +++ b/rad_tag.py @@ -90,13 +90,14 @@ for src_photo in args.photos: print(photo.get_target_photo, photo.get_date) # Here the matching magic has to happen - #latitude = 51.0234024 - #longitude = 7.248347 + + latitude = 51.0234024 + longitude = 7.248347 radiation = 9001.15 - #elevation = 56.079345703125 + elevation = 56.079345703125 # Write exif data - exif_tags = Exif(photo=photo.get_target_photo, dry_run=args.dry) + exif_tags = Exif(photo.get_target_photo, args.dry, radiation, latitude, longitude, elevation) print(exif_tags) # Print table header From cf4007c9092911b6f8e952d7931f581886468616 Mon Sep 17 00:00:00 2001 From: Commander1024 Date: Sun, 15 Mar 2020 01:57:02 +0100 Subject: [PATCH 29/39] Added Match class. Works on Radiation so far, but fails on Position, because there is not attribute timestamp. Going to write a wrapper class to be able to use same Match class on both of them. --- functions.py | 48 +++++++++++++++++++++++++++++++++++++++++++++--- rad_tag.py | 4 +++- 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/functions.py b/functions.py index 2c5b3ef..49b2909 100644 --- a/functions.py +++ b/functions.py @@ -3,7 +3,7 @@ ''' Classes used by main program. ''' -from datetime import datetime +from datetime import datetime, timedelta import os import shutil from fractions import Fraction @@ -90,6 +90,48 @@ class Photo: pic_aware_time = pic_naive_time.astimezone(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 + + Arguments: + photo_time: timestamp of photo + radiation_list: list of timestamp / radiation values + position_list: list of timestamp / position / elevation values + + Returns: + timestamp: as datetime object + radiation: in µS/h as string + latitude: in decimal format as float + longitude: in decimal format as float + elevation: in meters as float + ''' + + def __init__(self, photo_time, radiation_list, position_list): +# self.radiation = self._find_match(photo_time, radiation_list) + self.position = self._find_match(photo_time, position_list) + + def __repr__(self): + pass + + def _find_match(self, photo_time, list): + valuelist = [] + for row in list: + # Define timedelta and define timedelta datetime object. + delta = timedelta(seconds=60) + time_delta = abs(row.timestamp - photo_time) +# time_delta = abs(row[0] - photo_time) + # datetime objects should match with 1 minute precision. + if time_delta < delta: + element = (time_delta, row) + valuelist.append(element) + # Return the list item with the lowest timedelta in column 0. + # Column 2 contains the source objects untouched. + if valuelist: + return min(valuelist, key=lambda x: x[0]) + return None + class Exif: ''' Converts, compiles and writes Exif-Tags from given arguemnts. @@ -103,8 +145,8 @@ class Exif: dry_run: whether to acutally write (True / False) Returns: - Latitude / Longitude in degrees - Exif-Comment that has been written (incl. radiation) + Latitude / Longitude: in degrees + Exif-Comment: that has been written (incl. radiation) ''' def __init__(self, photo, dry_run, radiation, latitude, longitude, elevation): diff --git a/rad_tag.py b/rad_tag.py index 8432c02..24e5bb6 100755 --- a/rad_tag.py +++ b/rad_tag.py @@ -10,7 +10,7 @@ import csv import argparse import pytz import gpxpy -from functions import Radiation, Photo, Exif +from functions import Radiation, Photo, Exif, Match # SIFACTOR for GQ Geiger counters @@ -91,6 +91,8 @@ for src_photo in args.photos: # Here the matching magic has to happen + match = Match(photo.get_date, radiation_list, position_list) + latitude = 51.0234024 longitude = 7.248347 radiation = 9001.15 From 952f2726ba4dc35a68d32aa30a4813eecdead323 Mon Sep 17 00:00:00 2001 From: Commander1024 Date: Fri, 20 Mar 2020 20:05:58 +0100 Subject: [PATCH 30/39] Added a working match class that returns best match for radiation and position. Still ugly and a lot of debug output. Also not finished. --- functions.py | 38 +++++++++++++++++++++++++++----------- rad_tag.py | 24 +++++++++++++----------- 2 files changed, 40 insertions(+), 22 deletions(-) diff --git a/functions.py b/functions.py index 49b2909..2f7a45b 100644 --- a/functions.py +++ b/functions.py @@ -11,7 +11,7 @@ import pyexiv2 class Radiation: ''' - Reiceives Values vom CSV file and creates a list of the relevant data + Reiceives values vom CSV file and creates a list of the relevant data Arguments: timestamp: Date/time string from CSV as string @@ -101,27 +101,24 @@ class Match: position_list: list of timestamp / position / elevation values Returns: - timestamp: as datetime object - radiation: in µS/h as string - latitude: in decimal format as float - longitude: in decimal format as float - elevation: in meters as float + minimal timedelta: as timedelta object + best matching valuerow ''' def __init__(self, photo_time, radiation_list, position_list): -# self.radiation = self._find_match(photo_time, radiation_list) - self.position = self._find_match(photo_time, position_list) + self.radiation = self._find_radiation_match(photo_time, radiation_list) + self.position = self._find_position_match(photo_time, position_list) def __repr__(self): pass - def _find_match(self, photo_time, list): + def _find_radiation_match(self, photo_time, list): valuelist = [] for row in list: # Define timedelta and define timedelta datetime object. delta = timedelta(seconds=60) - time_delta = abs(row.timestamp - photo_time) -# time_delta = abs(row[0] - photo_time) + if row.timestamp: + time_delta = abs(row.timestamp - photo_time) # datetime objects should match with 1 minute precision. if time_delta < delta: element = (time_delta, row) @@ -129,6 +126,25 @@ class Match: # Return the list item with the lowest timedelta in column 0. # Column 2 contains the source objects untouched. if valuelist: + print(min(valuelist, key=lambda x: x[0])) + return min(valuelist, key=lambda x: x[0]) + return None + + def _find_position_match(self, photo_time, list): + valuelist = [] + for row in list: + # Define timedelta and define timedelta datetime object. + delta = timedelta(seconds=60) + if row[0]: + time_delta = abs(row[0] - photo_time) + # datetime objects should match with 1 minute precision. + if time_delta < delta: + element = (time_delta, row) + valuelist.append(element) + # Return the list item with the lowest timedelta in column 0. + # Column 2 contains the source objects untouched. + if valuelist: + print(min(valuelist, key=lambda x: x[0])) return min(valuelist, key=lambda x: x[0]) return None diff --git a/rad_tag.py b/rad_tag.py index 24e5bb6..318999b 100755 --- a/rad_tag.py +++ b/rad_tag.py @@ -10,7 +10,7 @@ import csv import argparse import pytz import gpxpy -from functions import Radiation, Photo, Exif, Match +from functions import Radiation, Photo, Match, Exif # SIFACTOR for GQ Geiger counters @@ -60,21 +60,22 @@ with open(args.csv, "r") as f: for _, csv_raw_time, csv_raw_cpm, _ in csv: radiation = Radiation(csv_raw_time, csv_raw_cpm, local_timezone, args.sifactor) radiation_list.append(radiation) - # close CSV file f.close() -# Import GPX track(s) +# Import GPX track(s)print if args.gpx is not None: gpx_file = open(args.gpx, 'r') gpx_reader = gpxpy.parse(gpx_file) - - for waypoint in gpx_reader.waypoints: - for track in gpx_reader.tracks: - for segment in track.segments: - for point in segment.points: - position = [point.time, point.latitude, point.longitude, point.elevation] - position_list.append(position) + #for waypoint in gpx_reader.waypoints: + for track in gpx_reader.tracks: + for segment in track.segments: + for point in segment.points: + point_aware_time = point.time.astimezone(local_timezone) + #point_aware_time = point_naive_time.astimezone(local_timezone) + position = (point_aware_time, point.latitude, point.longitude, + point.elevation, local_timezone) + position_list.append(position) # Inform the user about what is going to happen if args.dry is True: @@ -92,6 +93,7 @@ for src_photo in args.photos: # Here the matching magic has to happen match = Match(photo.get_date, radiation_list, position_list) + #print(match) latitude = 51.0234024 longitude = 7.248347 @@ -100,7 +102,7 @@ for src_photo in args.photos: # Write exif data exif_tags = Exif(photo.get_target_photo, args.dry, radiation, latitude, longitude, elevation) - print(exif_tags) + #print(exif_tags) # Print table header print('{:<15} {:<25} {:<22}'.format('filename', 'date / time', 'Exif UserComment')) \ No newline at end of file From d935cc1ea0c651232f955e27265b8520da941dbe Mon Sep 17 00:00:00 2001 From: Commander1024 Date: Sat, 21 Mar 2020 13:29:53 +0100 Subject: [PATCH 31/39] Created a match representation, figured how to access specific data from Match class. --- functions.py | 27 +++++++++++++++++---------- rad_tag.py | 16 ++++------------ 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/functions.py b/functions.py index 2f7a45b..98a2cc9 100644 --- a/functions.py +++ b/functions.py @@ -39,7 +39,7 @@ class Radiation: def _radiation_conversion(self, radiation, si_factor): # Convert CP/M to µS/h using si_factor - radiation = round(float(radiation) * si_factor, 2) + radiation = float(radiation) * si_factor return radiation class Photo: @@ -110,7 +110,13 @@ class Match: self.position = self._find_position_match(photo_time, position_list) def __repr__(self): - pass + if self.radiation[1]: + radiation = round(self.radiation[1], 2) + else: + radiation = None + return 'Radiation: %s µS/h (Δt %s) \nPosition: Lat: %s, Long: %s, Alt: %sm (Δt %s)' % \ + (str(radiation), str(self.radiation[0]), str(self.position[1][1]), \ + str(self.position[1][2]), str(self.position[1][3]), str(self.position[0])) def _find_radiation_match(self, photo_time, list): valuelist = [] @@ -126,27 +132,28 @@ class Match: # Return the list item with the lowest timedelta in column 0. # Column 2 contains the source objects untouched. if valuelist: - print(min(valuelist, key=lambda x: x[0])) - return min(valuelist, key=lambda x: x[0]) - return None + result = min(valuelist, key=lambda x: x[0]) + return result[0], result[1].radiation + # Return a tuple of 2x None, if there was no match. + return None, None def _find_position_match(self, photo_time, list): valuelist = [] for row in list: # Define timedelta and define timedelta datetime object. - delta = timedelta(seconds=60) + delta = timedelta(seconds=300) if row[0]: time_delta = abs(row[0] - photo_time) - # datetime objects should match with 1 minute precision. + # datetime objects should match with 5 minute precision. if time_delta < delta: element = (time_delta, row) valuelist.append(element) # Return the list item with the lowest timedelta in column 0. # Column 2 contains the source objects untouched. if valuelist: - print(min(valuelist, key=lambda x: x[0])) + #print(min(valuelist, key=lambda x: x[0])) return min(valuelist, key=lambda x: x[0]) - return None + return [None, [None, None, None, None]] class Exif: ''' @@ -221,7 +228,7 @@ class Exif: if radiation: # Set new UserComment - new_comment = 'Radiation ☢ : ' + str(radiation) + ' µS/h' + new_comment = 'Radiation ☢ : %s µS/h' % str(round(radiation, 2)) metadata['Exif.Photo.UserComment'] = new_comment else: new_comment = None diff --git a/rad_tag.py b/rad_tag.py index 318999b..f0f5a58 100755 --- a/rad_tag.py +++ b/rad_tag.py @@ -74,7 +74,7 @@ if args.gpx is not None: point_aware_time = point.time.astimezone(local_timezone) #point_aware_time = point_naive_time.astimezone(local_timezone) position = (point_aware_time, point.latitude, point.longitude, - point.elevation, local_timezone) + point.elevation) position_list.append(position) # Inform the user about what is going to happen @@ -93,16 +93,8 @@ for src_photo in args.photos: # Here the matching magic has to happen match = Match(photo.get_date, radiation_list, position_list) - #print(match) - latitude = 51.0234024 - longitude = 7.248347 - radiation = 9001.15 - elevation = 56.079345703125 - # Write exif data - exif_tags = Exif(photo.get_target_photo, args.dry, radiation, latitude, longitude, elevation) - #print(exif_tags) - -# Print table header -print('{:<15} {:<25} {:<22}'.format('filename', 'date / time', 'Exif UserComment')) \ No newline at end of file + exif_tags = Exif(photo.get_target_photo, args.dry, match.radiation[1], + match.position[1][1], match.position[1][2], match.position[1][3]) + print(exif_tags) From c2ed0e1c2a3e17e436fcee81a05431529e06ce07 Mon Sep 17 00:00:00 2001 From: Commander1024 Date: Sat, 21 Mar 2020 17:17:42 +0100 Subject: [PATCH 32/39] Added Output class that formats matched data and returns string for printing. Added beautified output. --- functions.py | 57 ++++++++++++++++++++++++++++++++++++++++++++++++---- rad_tag.py | 27 ++++++++++++++++--------- 2 files changed, 70 insertions(+), 14 deletions(-) diff --git a/functions.py b/functions.py index 98a2cc9..9274b42 100644 --- a/functions.py +++ b/functions.py @@ -74,10 +74,10 @@ class Photo: # Copy photo to dest_dir and return its (new) filename # if not in dry_run mode or if dest_dir is different from src_dir. if dry_run is True: - return photo + return photo_basename, photo if dest_dir != '.': shutil.copy(photo, dest_photo) - return dest_photo + return photo_basename, dest_photo def _get_creation_date(self, photo, local_timezone): # Load Exif data from photo @@ -114,9 +114,14 @@ class Match: radiation = round(self.radiation[1], 2) else: radiation = None + + if self.position[1][3]: + altitude = round(self.position[1][3]) + else: + altitude = None return 'Radiation: %s µS/h (Δt %s) \nPosition: Lat: %s, Long: %s, Alt: %sm (Δt %s)' % \ (str(radiation), str(self.radiation[0]), str(self.position[1][1]), \ - str(self.position[1][2]), str(self.position[1][3]), str(self.position[0])) + str(self.position[1][2]), altitude, str(self.position[0])) def _find_radiation_match(self, photo_time, list): valuelist = [] @@ -153,6 +158,7 @@ class Match: if valuelist: #print(min(valuelist, key=lambda x: x[0])) return min(valuelist, key=lambda x: x[0]) + # Return Nones in the same cascaded manner as if it matched. return [None, [None, None, None, None]] class Exif: @@ -161,7 +167,7 @@ class Exif: Arguments: photo: file name of photo to modify - radiation: radiation levels in µS/h + radiation: radiation levels float latitude: latitude as float longitude: longitude as float elevation: elevation as float @@ -238,3 +244,46 @@ class Exif: metadata.write() return latitude_degree, longitude_degree, new_comment + +class Output: + ''' + Receives values to be printed, formats them and returns a string for printing. + + Arguments: + radiation: radiation as float + latitude: latitude as float + longitude: longitude as float + elevation: elevation as float + + Returns: + A String that can be printed in output + ''' + + def __init__(self, radiation, latitude, longitude, altitude): + self.get_string = self._get_string(radiation, latitude, longitude, altitude) + + def __repr__(self): + return self.get_string + + def _get_string(self, radiation, latitude, longitude, altitude): + # Convert values to styled strings + if radiation: + rad = '☢: %sµS/h ' % str(round(radiation, 2)) + else: + rad = '☢: N/A ' + + if latitude and longitude: + latlon = 'Lat.: %s Long.: %s ' % (str(latitude), str(longitude)) + else: + latlon = 'Lat.: N/A, Long.: N/A ' + + if altitude: + alt = 'Alt.: %sm' % str(round(altitude, 1)) + else: + alt = 'Alt.: N/A' + + data = rad + latlon + alt + + # Return data string + return data + diff --git a/rad_tag.py b/rad_tag.py index f0f5a58..182dd79 100755 --- a/rad_tag.py +++ b/rad_tag.py @@ -10,7 +10,7 @@ import csv import argparse import pytz import gpxpy -from functions import Radiation, Photo, Match, Exif +from functions import Radiation, Photo, Match, Exif, Output # SIFACTOR for GQ Geiger counters @@ -64,7 +64,7 @@ with open(args.csv, "r") as f: f.close() # Import GPX track(s)print -if args.gpx is not None: +if args.gpx: gpx_file = open(args.gpx, 'r') gpx_reader = gpxpy.parse(gpx_file) #for waypoint in gpx_reader.waypoints: @@ -72,13 +72,12 @@ if args.gpx is not None: for segment in track.segments: for point in segment.points: point_aware_time = point.time.astimezone(local_timezone) - #point_aware_time = point_naive_time.astimezone(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 -if args.dry is True: +if args.dry: print('Not modifying anything. Just print what would happen without --dry') else: if args.outdir == ".": @@ -86,15 +85,23 @@ else: else: print('Modifying photos in', str(args.outdir), '(copy)') +# Print table header +print('{:<15} {:<25} {:<22}'.format('filename', 'date / time', 'Matched Data')) + for src_photo in args.photos: + # Instantiate photo, copy it to destdir if needed and receive filename to work on photo = Photo(src_photo, local_timezone, args.outdir, args.dry) - print(photo.get_target_photo, photo.get_date) - - # Here the matching magic has to happen + # Here the matching magic takes place match = Match(photo.get_date, radiation_list, position_list) + + # Formatted output: + #print(Output(photo.get_target_photo[0], photo.get_date, match.radiation[1], + # match.position[1][1], match.position[1][2], match.position[1][3])) + data = Output(match.radiation[1], match.position[1][1], match.position[1][2], match.position[1][3]) + + print('{:<15} {:<25} {:<22}'.format(photo.get_target_photo[0], str(photo.get_date), str(data))) # Write exif data - exif_tags = Exif(photo.get_target_photo, args.dry, match.radiation[1], - match.position[1][1], match.position[1][2], match.position[1][3]) - print(exif_tags) + Exif(photo.get_target_photo[1], args.dry, match.radiation[1], + match.position[1][1], match.position[1][2], match.position[1][3]) From 3e5cfcf327d060d2d1b7f182dc833372c919196d Mon Sep 17 00:00:00 2001 From: Commander1024 Date: Sat, 21 Mar 2020 18:36:25 +0100 Subject: [PATCH 33/39] Remove obsolete comment. --- rad_tag.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/rad_tag.py b/rad_tag.py index 182dd79..3146bfc 100755 --- a/rad_tag.py +++ b/rad_tag.py @@ -94,13 +94,11 @@ for src_photo in args.photos: # Here the matching magic takes place match = Match(photo.get_date, radiation_list, position_list) - - # Formatted output: - #print(Output(photo.get_target_photo[0], photo.get_date, match.radiation[1], - # match.position[1][1], match.position[1][2], match.position[1][3])) - data = Output(match.radiation[1], match.position[1][1], match.position[1][2], match.position[1][3]) + # Formatted output: + data = Output(match.radiation[1], match.position[1][1], match.position[1][2], match.position[1][3]) print('{:<15} {:<25} {:<22}'.format(photo.get_target_photo[0], str(photo.get_date), str(data))) + #print(match) # Write exif data Exif(photo.get_target_photo[1], args.dry, match.radiation[1], From 59b94991d01bfda707666e25b79eb69a0d9e7940 Mon Sep 17 00:00:00 2001 From: Commander1024 Date: Sat, 21 Mar 2020 19:05:27 +0100 Subject: [PATCH 34/39] Made code better readable. Added verbose instance attributes. --- functions.py | 25 +++++++++++++++---------- rad_tag.py | 8 ++++---- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/functions.py b/functions.py index 9274b42..1971785 100644 --- a/functions.py +++ b/functions.py @@ -59,10 +59,11 @@ class Photo: def __init__(self, photo, local_timezone, dest_dir, dry_run): self.get_date = self._get_creation_date(photo, local_timezone) - self.get_target_photo = self._copy_photo(photo, dest_dir, dry_run) + self.get_photo_filename = self._copy_photo(photo, dest_dir, dry_run)[1] + 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_target_photo), 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. @@ -106,22 +107,26 @@ class Match: ''' def __init__(self, photo_time, radiation_list, position_list): - self.radiation = self._find_radiation_match(photo_time, radiation_list) - self.position = self._find_position_match(photo_time, 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] def __repr__(self): - if self.radiation[1]: - radiation = round(self.radiation[1], 2) + if self.radiation_value: + radiation = round(self.radiation_value, 2) else: radiation = None - if self.position[1][3]: - altitude = round(self.position[1][3]) + if self.position_altitude: + altitude = round(self.position_altitude) else: altitude = None return 'Radiation: %s µS/h (Δt %s) \nPosition: Lat: %s, Long: %s, Alt: %sm (Δt %s)' % \ - (str(radiation), str(self.radiation[0]), str(self.position[1][1]), \ - str(self.position[1][2]), altitude, str(self.position[0])) + (str(radiation), str(self.radiation_delta), str(self.position_latitude), \ + str(self.position_longitude), altitude, str(self.position_delta)) def _find_radiation_match(self, photo_time, list): valuelist = [] diff --git a/rad_tag.py b/rad_tag.py index 3146bfc..13913b7 100755 --- a/rad_tag.py +++ b/rad_tag.py @@ -96,10 +96,10 @@ for src_photo in args.photos: match = Match(photo.get_date, radiation_list, position_list) # Formatted output: - data = Output(match.radiation[1], match.position[1][1], match.position[1][2], match.position[1][3]) - print('{:<15} {:<25} {:<22}'.format(photo.get_target_photo[0], 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))) #print(match) # Write exif data - Exif(photo.get_target_photo[1], args.dry, match.radiation[1], - match.position[1][1], match.position[1][2], match.position[1][3]) + Exif(photo.get_photo_filename, args.dry, match.radiation_value, + match.position_latitude, match.position_longitude, match.position_altitude) From 5a39f08f3530b0e872bb58e61bf47e7d3b381d7a Mon Sep 17 00:00:00 2001 From: Commander1024 Date: Sun, 22 Mar 2020 11:35:53 +0100 Subject: [PATCH 35/39] Fixed formatting and fixed some comments, removed obsolete stuff that remained. --- rad_tag.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rad_tag.py b/rad_tag.py index 13913b7..470d13b 100755 --- a/rad_tag.py +++ b/rad_tag.py @@ -48,7 +48,7 @@ args = parser.parse_args() # Create timezone datetime object local_timezone = pytz.timezone(args.timezone) -# Initialize two empty lists for comparison +# Initialize two empty lists for all radiation / gps values radiation_list = [] position_list = [] @@ -57,6 +57,7 @@ with open(args.csv, "r") as f: csv = csv.reader(filter(lambda row: row[0] != '#', f), delimiter=',', skipinitialspace=True) + # Import only relevant values, thats 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_list.append(radiation) @@ -67,7 +68,6 @@ with open(args.csv, "r") as f: if args.gpx: gpx_file = open(args.gpx, 'r') gpx_reader = gpxpy.parse(gpx_file) - #for waypoint in gpx_reader.waypoints: for track in gpx_reader.tracks: for segment in track.segments: for point in segment.points: @@ -96,9 +96,9 @@ for src_photo in args.photos: 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) + 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))) - #print(match) # Write exif data Exif(photo.get_photo_filename, args.dry, match.radiation_value, From 3987d484ad5b9230e2ab7584a035390c24fdcdeb Mon Sep 17 00:00:00 2001 From: Commander1024 Date: Sun, 22 Mar 2020 12:59:12 +0100 Subject: [PATCH 36/39] Fixed Photo return value for overwriting source files. --- functions.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/functions.py b/functions.py index 1971785..981b976 100644 --- a/functions.py +++ b/functions.py @@ -53,8 +53,9 @@ class Photo: dry_run: whether to acutally write (True / False) Returns: - self.get_date: timestamp of photo als datetime object - self.get_target_photo: full path to photo file to work on + get_date: timestamp of photo als datetime object + get_photo_filename: full path to photo file to work on + get_photo_basename: only filename (e. g. for print output) ''' def __init__(self, photo, local_timezone, dest_dir, dry_run): @@ -67,9 +68,10 @@ class Photo: def _copy_photo(self, photo, dest_dir, dry_run): # Determine where to work on photo and copy it there if needed. + # Get image file name out of path photo_basename = os.path.basename(photo) - # be os aware and use the correct directory delimiter for destfile + # be os aware and use the correct directory delimiter for dest_photo dest_photo = os.path.join(dest_dir, photo_basename) # Copy photo to dest_dir and return its (new) filename @@ -78,7 +80,8 @@ class Photo: return photo_basename, photo if dest_dir != '.': shutil.copy(photo, dest_photo) - return photo_basename, dest_photo + return photo_basename, dest_photo + return photo_basename, photo def _get_creation_date(self, photo, local_timezone): # Load Exif data from photo @@ -103,7 +106,7 @@ class Match: Returns: minimal timedelta: as timedelta object - best matching valuerow + best matching values ''' def __init__(self, photo_time, radiation_list, position_list): From 77cb2c9f459f92a8040a7a5291f1b7bf33d1a493 Mon Sep 17 00:00:00 2001 From: Commander1024 Date: Sun, 22 Mar 2020 13:13:15 +0100 Subject: [PATCH 37/39] Edited Readme.md to match the current development. Some parameters were added and minor behaviour changes. --- Readme.md | 54 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 33 insertions(+), 21 deletions(-) diff --git a/Readme.md b/Readme.md index c891f9f..57af6d0 100644 --- a/Readme.md +++ b/Readme.md @@ -6,7 +6,7 @@ It can scan a couple of images, extract their Exif-tags, and compare the `DateTi It can parse a .his (CSV) file from a [GeigerLog](https://sourceforge.net/projects/Geigerlog/) file export and calculate the radiation in µS/h using the factor in `sifactor`. -Furthermore it can optionally read a gpx-file, compare the timestamps to 'DateTimeOriginal' and determine closest-matching latitude / longitude. If your gpx-file has times stored including the timezone, you can set --timezone to the local timezone, your camera / geiger counter ran at. +It can optionally read a gpx-file, compare the timestamps to 'DateTimeOriginal' and determine closest-matching latitude / longitude / altitude. Timestamps in GPX files are ususally stored in UTC timezone, you can set --timezone to match the local timezone, your camera / geiger counter ran at. It then creates a `UserComment` with the actual measured radiation at the time the photo has been taken and writes the geocoordinates into the appropiate Exif tags. @@ -19,8 +19,9 @@ Right now it depends on the following non-core Python 3 libraries: * [gpxpy](https://github.com/tkrajina/gpxpy) gpx-py is a python GPX parser. GPX (GPS eXchange Format) is an XML based file format for GPS tracks. ## Requirements + * A bunch of images (jpg, cr2, etc.) with its time of creation stored in `DateTimeOriginal`. * GeigerCounter log file in csv format as it is being exported by the software GeigerLog. - * A bunch of images (jpg, cr2, etc.) with its time of creation stored in `DateTimeOriginal` + * Optionally a GPX (1.0 / 1.1) track that has been recorded during the same timeperiod. All sources are matched by their timestamp, so all sources have to be recorded during the same time (and timezone). The Geiger counter has to log a value every second, as the script compares the timestamps exactly. @@ -34,9 +35,8 @@ These exported .his files look like this: ## Usage ``` -usage: rad_tag.py [-h] [-si SIFACTOR] [-tz Timezone] [-d] [-g GPX] - [-o OUTDIR] - CSV Photo [Photo ...] +usage: rad_tag.py [-h] [-si SIFACTOR] [-tz Timezone] [-d] [-g GPX] [-o OUTDIR] + CSV Photo [Photo ...] A unix-tyle tool that extracts GPS and/or radiation data from GPX/CSV files and writes them into the Exif tags of given photos. @@ -52,7 +52,9 @@ optional arguments: 0.0065) -tz Timezone, --timezone Timezone Manually set timezone of CSV / and Photo timestamp, - defaults to localtime if omitted. (default: None) + defaults to UTC if omitted. This is useful, if the + GPS-Logger saves the time incl. timezone (default: + utc) -d, --dry Dry-run, do not actually write anything. (default: False) -g GPX, --gpx GPX GPS track in GPX format (default: None) @@ -62,26 +64,33 @@ optional arguments: ``` ### Examples -Use test.hisdb.his from current working dir and modify (overwrite) all .CR2 files in place. +Use test.hisdb.his and walk.gpx from testdata and modify (overwrite) all .JPG files in place. ``` -./rat_tag.py test.hisdb.his *.CR2 +./rad_tag.py ./testdata/walk.hisdb.his --gpx .d/testdata/walk.gpx -tz Europe/Berlin ./testdest/*.JPG Modifying photos in place (overwrite) -filename date / time Exif UserComment -DSC_0196.JPG 2020-03-03 18:33:33 NOT FOUND! -DSC_0197.JPG 2020-03-03 20:14:18 Radiation ☢ 0.15 µS/h -DSC_0198.JPG 2020-03-03 22:18:13 Radiation ☢ 0.07 µS/h -``` - -Use test.hisdb.his in folder 'testdata', read all .JPG files from 'testsource' and write them to 'testdest'. +filename date / time Matched Data +_MG_3824.JPG 2020-03-15 16:17:54+01:00 ☢: 0.05µS/h Lat.: 51.92611112 Long.: 7.69379252 Alt.: 93.0m +_MG_3825.JPG 2020-03-15 16:18:12+01:00 ☢: 0.08µS/h Lat.: 51.92620192 Long.: 7.69360727 Alt.: 91.7m +_MG_3826.JPG 2020-03-15 16:18:12+01:00 ☢: 0.08µS/h Lat.: 51.92620192 Long.: 7.69360727 Alt.: 91.7m +_MG_3827.JPG 2020-03-15 16:18:12+01:00 ☢: 0.08µS/h Lat.: 51.92620192 Long.: 7.69360727 Alt.: 91.7m ``` -./rad_tag.py testdata/test.hisdb.his -o testdest/ testsource/*.JPG -Modifying photos in testdest/ (copy) -filename date / time Exif UserComment -DSC_0196.JPG 2020-03-03 18:33:33 NOT FOUND! -DSC_0197.JPG 2020-03-03 20:14:18 Radiation ☢ 0.15 µS/h -DSC_0198.JPG 2020-03-03 22:18:13 Radiation ☢ 0.07 µS/h + +Use test.hisdb.his in folder 'testdata', read all files from 'testsource' and write them to 'testdest'. + +``` +./rad_tag.py ./testdata/walk.hisdb.his -o ./testdest --gpx ./testdata/walk.gpx -tz Europe/Berlin ./testsource/* +Modifying photos in /home/mscholz/testdest (copy) +filename date / time Matched Data +DSC_0226.JPG 2020-03-15 15:02:04+01:00 ☢: N/A Lat.: N/A, Long.: N/A Alt.: N/A +DSC_0227.JPG 2020-03-15 15:11:43+01:00 ☢: N/A Lat.: N/A, Long.: N/A Alt.: N/A +_MG_3804.JPG 2020-03-15 15:59:11+01:00 ☢: 0.06µS/h Lat.: 51.92582544 Long.: 7.68739496 Alt.: 95.4m +_MG_3805.CR2 2020-03-15 16:01:49+01:00 ☢: 0.05µS/h Lat.: 51.92314108 Long.: 7.69078156 Alt.: 104.2m +_MG_3805.JPG 2020-03-15 16:01:49+01:00 ☢: 0.05µS/h Lat.: 51.92314108 Long.: 7.69078156 Alt.: 104.2m +_MG_3807.CR2 2020-03-15 16:07:02+01:00 ☢: 0.08µS/h Lat.: 51.9235013 Long.: 7.69250565 Alt.: 101.3m +_MG_3807.JPG 2020-03-15 16:07:02+01:00 ☢: 0.08µS/h Lat.: 51.9235013 Long.: 7.69250565 Alt.: 101.3m + ``` ## GeigerLog setup @@ -116,6 +125,9 @@ GeigerLog now presents you a rendering of the radiation over time in its main wi Once imported, you can export the history into a hisdb.his-file, which is basically the CSV-file `rad_tag.py` can process. Choose 'History' -> Save History Data into .his file (CSV)'. +## GPS setup + +Especially if you use a mobile phone for GPS-logging. Take care, the app can use GPS when turned off, and let it write position sufficiently often. Threshold is 5 minutes by default, but precision will improve when logging more often. Especially "inactivity detection" might become a problem, when staying at one place for a period of time. ## future possibilities From 8947fb9f2f12c3a85286e14017d386203b0a2dac Mon Sep 17 00:00:00 2001 From: Commander1024 Date: Sun, 22 Mar 2020 13:17:29 +0100 Subject: [PATCH 38/39] Prepared Changelog for 0.3 release. --- CHANGELOG.md | 40 ++++++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e92bfc..2e1f1b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,36 +5,44 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] -- Added GPX parser -- Added optional dry-run modifier -- Made timezone-aware for timesources without timezone -- Added optional parameter to override timezone to local timezone, defaults to utc + +## [0.3] - tba + +Major rewrite following OOP style. Adds GPS/GPX handling. + +### Added +- Added GPX parser. +- Added optional dry-run modifier. +- Made timezone-aware for timesources that are not timezone aware. +- Added optional parameter to override timezone to local timezone, defaults to utc. - Refactored variable name_scheme. - Switch to oop style programming. Made code easier to read. - Moved CSV processing and Exif reading / writing into a class. -- Moved photo copying into Photo class -- Created Exif class. Can now also create GPS tags. +- Moved photo copying into Photo class. +- Created Exif writing class. Can now also create GPS tags. +- Created Output formatting class. +- Added new working Match class for matching time against CSV and GPX. ## [0.2] - 2020-02-05 ### Added - uses pyexiv2 instead of piexif which is also able to tag various camera raw formats, e. g. CR2 and is capable of writing properly formed UTF-8 strings. -- added copy function to be able to place files to outdir before modification -- added verbose output about what it going to happen -- added table header for output +- Added copy function to be able to place files to outdir before modification. +- Added verbose output about what it going to happen. +- Added table header for output. ### Changed -- removed unnecessary datetime object creation as pyexiv2 can output datetime objects itself -- removed obsolete "Processing..." message, as it was never visible. +- Removed unnecessary datetime object creation as pyexiv2 can output datetime objects itself. +- Removed obsolete "Processing..." message, as it was never visible. ## [0.1] - 2020-02-02 First prototype using piexif ### Added -- argument parsing enabled -- parametrized factor to calculate Sieverts -- parametrized output dir -- usage of os.path to be os-aware -- output in a tablish manner +- Argument parsing enabled. +- Parametrized factor to calculate Sieverts. +- Parametrized output dir. +- Usage of os.path to be os-aware. +- Output in a tablish manner. ### Changed - exchanged selfmade CSV parser by python's core CSV library From 3be3aac9242fa7ec74cc15cbb21bfe04bb4938f8 Mon Sep 17 00:00:00 2001 From: Commander1024 Date: Mon, 23 Mar 2020 09:41:49 +0100 Subject: [PATCH 39/39] 0.3 released. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e1f1b9..dd76fea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -## [0.3] - tba +## [0.3] - 2020-03-23 Major rewrite following OOP style. Adds GPS/GPX handling.