Merge branch 'oop' into 'master'

Major rewrite following OOP style. Adds GPS/GPX handling.

See merge request Commander1024/radiation-tagger!1
This commit is contained in:
Marcus Scholz 2020-03-23 08:45:42 +00:00
commit 032e7c408e
6 changed files with 483 additions and 135 deletions

4
.gitignore vendored
View File

@ -1,4 +1,4 @@
testdata
testsource
testdest
testdata
usercomment.sh
__pycache__

View File

@ -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/),
@ -6,27 +6,43 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.3] - 2020-03-23
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 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
- 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
- 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.
### 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

View File

@ -1,23 +1,27 @@
# 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.
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.
## 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
* 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.
@ -31,10 +35,11 @@ These exported .his files look like this:
## Usage
```
usage: exif_rad.py [-h] [-si SIFACTOR] [-o OUTDIR] CSV Photo [Photo ...]
usage: rad_tag.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.
@ -45,31 +50,47 @@ 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 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)
-o OUTDIR, --outdir OUTDIR
Directory to output processed photos (default: .)
Directory to output processed photos. (default: .)
```
### 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.
```
./exif_rad.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
```
./exif_rad.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
@ -89,6 +110,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.
@ -100,11 +123,13 @@ 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)'.
## 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
* 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.

View File

@ -1,95 +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"""
from datetime import datetime
import os
import shutil
import csv
import argparse
import pyexiv2
# 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 tool that writes
radiation levels (and optionally geocoordinates) to image files
and extracts the infos from external sources.''')
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('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()
# 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'))
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)
shutil.copy(srcphoto, dstphoto)
photo = dstphoto
# Load Exif data from image
metadata = pyexiv2.ImageMetadata(photo)
metadata.read()
tag = metadata['Exif.Photo.DateTimeOriginal']
# tag.value creates datetime object in pictime
pictime = tag.value
# Import GeigerCounter log
with open(args.csv, "r") as f:
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)
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} {:<20} {:<22}'.format(photo_basename, str(pictime), new_comment))
# Write Exif tags to file
metadata.write()
break
else:
print('{:<15} {:<20} {:<22}'.format(photo_basename, str(pictime), 'NOT FOUND!'))
# close CSV file
f.close()

297
functions.py Normal file
View File

@ -0,0 +1,297 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
''' Classes used by main program. '''
from datetime import datetime, timedelta
import os
import shutil
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)
'''
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)
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_conversion(self, radiation, si_factor):
# Convert CP/M to µS/h using si_factor
radiation = float(radiation) * si_factor
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:
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):
self.get_date = self._get_creation_date(photo, local_timezone)
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_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.
# Get image file name out of path
photo_basename = os.path.basename(photo)
# 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
# if not in dry_run mode or if dest_dir is different from src_dir.
if dry_run is True:
return photo_basename, photo
if dest_dir != '.':
shutil.copy(photo, dest_photo)
return photo_basename, dest_photo
return photo_basename, photo
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
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:
minimal timedelta: as timedelta object
best matching values
'''
def __init__(self, photo_time, radiation_list, position_list):
self.radiation_value = self._find_radiation_match(photo_time, radiation_list)[1]
self.radiation_delta = self._find_radiation_match(photo_time, radiation_list)[0]
self.position_delta = self._find_position_match(photo_time, position_list)[0]
self.position_latitude = self._find_position_match(photo_time, position_list)[1][1]
self.position_longitude = self._find_position_match(photo_time, position_list)[1][2]
self.position_altitude = self._find_position_match(photo_time, position_list)[1][3]
def __repr__(self):
if self.radiation_value:
radiation = round(self.radiation_value, 2)
else:
radiation = None
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_delta), str(self.position_latitude), \
str(self.position_longitude), altitude, str(self.position_delta))
def _find_radiation_match(self, photo_time, list):
valuelist = []
for row in list:
# Define timedelta and define timedelta datetime object.
delta = timedelta(seconds=60)
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)
valuelist.append(element)
# Return the list item with the lowest timedelta in column 0.
# Column 2 contains the source objects untouched.
if valuelist:
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=300)
if row[0]:
time_delta = abs(row[0] - photo_time)
# 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]))
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:
'''
Converts, compiles and writes Exif-Tags from given arguemnts.
Arguments:
photo: file name of photo to modify
radiation: radiation levels float
latitude: latitude as float
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, 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
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, dry_run, radiation, latitude, longitude, elevation):
metadata = pyexiv2.ImageMetadata(photo)
metadata.read()
if latitude and 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))
# 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 ☢ : %s µS/h' % str(round(radiation, 2))
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:
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

105
rad_tag.py Executable file
View File

@ -0,0 +1,105 @@
#!/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, Match, Exif, Output
# 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 all radiation / gps values
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)
# 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)
# close CSV file
f.close()
# Import GPX track(s)print
if args.gpx:
gpx_file = open(args.gpx, 'r')
gpx_reader = gpxpy.parse(gpx_file)
for track in gpx_reader.tracks:
for segment in track.segments:
for point in segment.points:
point_aware_time = point.time.astimezone(local_timezone)
position = (point_aware_time, point.latitude, point.longitude,
point.elevation)
position_list.append(position)
# Inform the user about what is going to happen
if args.dry:
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)')
# 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)
# Here the matching magic takes place
match = Match(photo.get_date, radiation_list, position_list)
# Formatted output:
data = Output(match.radiation_value, match.position_latitude,
match.position_longitude, match.position_altitude)
print('{:<15} {:<25} {:<22}'.format(photo.get_photo_basename, str(photo.get_date), str(data)))
# Write exif data
Exif(photo.get_photo_filename, args.dry, match.radiation_value,
match.position_latitude, match.position_longitude, match.position_altitude)