Compare commits

...

52 Commits
0.2 ... main

Author SHA1 Message Date
Marcus Scholz 67db75ece8 Fixed broken link to github help page. 2020-04-16 10:55:06 +02:00
Marcus Scholz 6cab491721 Changed Issue link to gitea. 2020-04-16 10:42:20 +02:00
Marcus Scholz 905ac497e1 Fixed release data in changelog. 2020-04-11 00:45:22 +02:00
Marcus Scholz 07ca626ca8 Unified mentions for Exif/ITPC/XMP tags. Finalized 0.3.2 release. 2020-04-11 00:43:29 +02:00
Marcus Scholz 2f94211507 Made documentation and changelog match the addition of additional ITPC/XMP tags. 2020-04-08 14:17:02 +02:00
Marcus Scholz 0998371ad6 Added alternative comment fields, as Lightroom discards Exif.Photo.UserComment. 2020-04-08 13:07:15 +02:00
Marcus Scholz a9453dd51e Added 0.3.1 release to changelog. 2020-04-03 18:00:11 +02:00
Marcus Scholz 98c5cad858 Added pipenv to changelog. 2020-04-03 10:46:37 +02:00
Marcus Scholz 2ad17eab2a Merge branch 'master' into develop 2020-04-03 10:38:53 +02:00
Marcus Scholz b816d04215 Prepared pipenv virtual environment and added documentation. 2020-04-03 10:34:05 +02:00
Marcus Scholz 58ae86db7e Added contributing guide, exchanged geigerlog main window screenshot with light colorscheme. 2020-03-27 13:21:23 +01:00
Marcus Scholz 0ae188a0ec Fixed typo. 2020-03-26 20:18:37 +01:00
Marcus Scholz 032e7c408e Merge branch 'oop' into 'master'
Major rewrite following OOP style. Adds GPS/GPX handling.

See merge request Commander1024/radiation-tagger!1
2020-03-23 08:45:42 +00:00
Marcus Scholz 3be3aac924 0.3 released. 2020-03-23 09:41:49 +01:00
Marcus Scholz 8947fb9f2f Prepared Changelog for 0.3 release. 2020-03-22 13:17:29 +01:00
Marcus Scholz 77cb2c9f45 Edited Readme.md to match the current development. Some parameters were added and minor behaviour changes. 2020-03-22 13:13:15 +01:00
Marcus Scholz 3987d484ad Fixed Photo return value for overwriting source files. 2020-03-22 12:59:12 +01:00
Marcus Scholz 5a39f08f35 Fixed formatting and fixed some comments, removed obsolete stuff that remained. 2020-03-22 11:35:53 +01:00
Marcus Scholz 59b94991d0 Made code better readable.
Added verbose instance attributes.
2020-03-21 19:05:27 +01:00
Marcus Scholz 3e5cfcf327 Remove obsolete comment. 2020-03-21 18:36:25 +01:00
Marcus Scholz c2ed0e1c2a Added Output class that formats matched data and returns string for printing.
Added beautified output.
2020-03-21 17:17:42 +01:00
Marcus Scholz d935cc1ea0 Created a match representation, figured how to access specific data from Match class. 2020-03-21 13:29:53 +01:00
Marcus Scholz 952f2726ba Added a working match class that returns best match for radiation and position.
Still ugly and a lot of debug output. Also not finished.
2020-03-20 20:05:58 +01:00
Marcus Scholz cf4007c909 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.
2020-03-15 01:57:02 +01:00
Marcus Scholz 4a7de7b518 Removed default value fuckery. The matching class wil have to provide proper values. 2020-03-14 20:42:01 +01:00
Marcus Scholz 13555a0505 Made optional Values optional in Exif class. 2020-03-14 20:21:44 +01:00
Marcus Scholz f6a54a5855 Holy Moly! Such chaos. Hopefully it is fixed now. 2020-03-14 18:03:59 +01:00
Marcus Scholz 9ac706075a Revert "Remove now obsoluete rad_tag.py"
This reverts commit da09323380.
2020-03-14 18:02:40 +01:00
Marcus Scholz eef986bdbc Revert "Remove now obsoluete rad_tag.py"
This reverts commit da09323380.
2020-03-14 17:53:38 +01:00
Marcus Scholz ce65eb4cee Revert "Enriched class comments with arguments and return values."
This reverts commit 8ea6524238.

I'm stupid, and deleted the wrong file. Fixed it.
2020-03-14 17:32:29 +01:00
Marcus Scholz da09323380 Remove now obsoluete rad_tag.py 2020-03-14 17:20:33 +01:00
Marcus Scholz 8ea6524238 Enriched class comments with arguments and return values.
Made a text representation of Exif and removed debug outputs.
2020-03-14 16:21:23 +01:00
Marcus Scholz fad837d7bd Added new upcoming features to changelog. 2020-03-13 07:21:24 +01:00
Marcus Scholz 693412316e Added write_exif function, calculates location in degree / minutes.
Assembles metadata info and writes them to the target_image.
2020-03-12 23:25:18 +01:00
Marcus Scholz cafda4cf35 Added function which converts decimal position value to WGS-84 notation. 2020-03-12 20:35:49 +01:00
Marcus Scholz f07eb0c691 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.
2020-03-12 19:24:27 +01:00
Marcus Scholz 2ad8b4a1fb Moved copying of photos to functions.py, simplified decision whether to copy photo or not. 2020-03-11 22:26:03 +01:00
Marcus Scholz 92b08170f2 Changed CSV variable, only write Exif tags that hava values to fill in. 2020-03-10 21:53:43 +01:00
Marcus Scholz c19a94374e Created Radiation, Photo classes and slimmed down main program.
- 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
2020-03-10 21:09:25 +01:00
Marcus Scholz d347bbdf55 Fixed typo. 2020-03-10 00:03:06 +01:00
Marcus Scholz cbb21db442 Moved creation and propagation of radiation_list and position_list out of the main loop. 2020-03-09 23:52:03 +01:00
Marcus Scholz d51c28b753 Refactored variable names, fixed dry run decision. 2020-03-09 23:43:01 +01:00
Marcus Scholz 314e70faa9 Now got 2 lists with with all data relevant for one photo. 2020-03-09 23:22:34 +01:00
Marcus Scholz a2ae379883 Made file executable. 2020-03-09 23:00:19 +01:00
Marcus Scholz fcc466eb41 Started conversion to OOP.
Created class Radiation with functions time_conversion and radiation
2020-03-09 22:42:08 +01:00
Marcus Scholz 115d6a6ab4 Trying to increase precision of datematch. 2020-03-09 17:02:17 +01:00
Marcus Scholz f43116abec Added verbose output for dry-run mode. 2020-03-09 16:09:19 +01:00
Marcus Scholz 83977b50bb Changed timezone default (to utc).
Old behavior failed when TZ was omitted.
2020-03-09 15:56:28 +01:00
Marcus Scholz 5b4811b081 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
2020-03-07 21:29:25 +01:00
Marcus Scholz aef69f81a1 Removed debug output. 2020-03-07 19:12:00 +01:00
Marcus Scholz 940da3a772 Added dry-run optional parameter. 2020-03-07 19:10:56 +01:00
Marcus Scholz 3eb17f53b5 First baby steps towards GPX handling - mainly foolish test output. 2020-03-06 14:36:02 +01:00
9 changed files with 565 additions and 138 deletions

5
.gitignore vendored
View File

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

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,53 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.3.2] - 2020-04-11
### Added
- Added additional Exif and ITPC data fields for radiation comment.
## [0.3.1] - 2020-04-03
### Added
- Prepared pipenv virtual environment.
## [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

34
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,34 @@
# How to contribute
I'm really glad you're reading this, if you are willing to contribute to this project in some form.
This is a quite small project with relatively low complexity so conventions are not as strict as they might be elsewhere. I am keen to learn whether and how this thing was useful to you, where you had problems and what you think could be improved.
Here are some important resources:
* This [Blogpost](https://www.commander1024.de/wordpress/2020/03/fotos-mit-daten-zu-radioaktiver-strahlung-taggen) tells you about intention and scope for this tool (in German).
* For questions and suggestions, you can [E-Mail](mailto:commander@commander1024.de) me directly.
* Bugs? [Gitlab](https://git.commander1024.de/Commander1024/radiation-tager/issues) is where to report them.
## Submitting changes
Please send a [Pull Request to radiation_tagger](https://git.commander1024.de/Commander1024/radiation-tager/pulls) in the develop branch with a clear list of what you've done (read more about [pull requests](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request)). Please follow our coding conventions (below) and make sure all of your commits are atomic (one feature per commit).
Keep in mind that I am a bloody beginner and probably make more mistakes than you, so I am always open for improvements.
Always write a clear log message for your commits. One-line messages are fine for small changes, but bigger changes should look like this:
$ git commit -m "A brief summary of the commit
>
> A paragraph describing what changed and its impact."
## Coding conventions
Start reading the code and you'll get the hang of it. We optimize for readability:
* We indent using 4 spaces (soft tabs).
* We use "describing" variables with underscores like 'position_list'.
* Classes and functions go to functions.py to keep the main program small and easy to understand.
* We generally follow the Python 3 coding style guidelines.
* This is open source software. Consider the people who will read your code, and make it look nice for them. It's sort of like driving a car: Perhaps you love doing donuts when you're alone, but with passengers the goal is to make the ride as smooth as possible.
Thanks,
Commander1024

14
Pipfile Normal file
View File

@ -0,0 +1,14 @@
[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true
[dev-packages]
[packages]
pytz = "*"
gpxpy = "*"
py3exiv2 = "*"
[requires]
python_version = "3.7"

100
Readme.md
View File

@ -1,23 +1,44 @@
# 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 some Exif/ITPC/XMP Comment/Description tags 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:
Right now it depends on the following non-core Python 3 libraries. These can be installed using the package manager of your distribution.
* [py3exiv2](https://pypi.org/project/py3exiv2/) A Python 3 binding to the library 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.
* [py3exiv2](https://pypi.org/project/py3exiv2/) A Python 3 binding for (lib)exiv2.
* [pytz](https://pypi.org/project/pytz/) World timezone definitions, modern and historical.
* [gpxpy](https://pypi.org/project/gpxpy/) gpx-py is a python GPX parser. GPX (GPS eXchange Format) is an XML based file format for GPS tracks.
### Setting up a virtual environment using pipenv
If you prefer to use more updated versions of the dependencies or you do not want to use Python dependencies into your system, I prepared a pipenv virtual environment for you.
Using `pipenv install` all dependencies will be installed automatically. With `pipenv shell` you can source the venv.
For py3exivv2 to work / compile the following dependencies must be installed - preferably from your system's package manager:
* [exiv2](http://www.exiv2.org/) and it's development package. Exiv2 is a Cross-platform C++ library and a command line utility to manage image metadata.
* [boost](https://www.boost.org/) and it's development package. Boost provides free peer-reviewed portable C++ source libraries.
* [boost.python3](http://www.boost.org/libs/python/doc/index.html) and it's development package. A C++ library which enables seamless interoperability between C++ and the Python programming language.
#### Debian / Ubuntu
sudo apt install pipenv build-essential python-all-dev libexiv2-dev libboost-python-dev
#### Fedora
sudo dnf install pipenv exiv2-devel boost-devel boost-python3-devel make automake gcc gcc-c++
## 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 +52,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/ITPC/XMP tags of given photos.
positional arguments:
CSV Geiger counter history file in CSV format.
@ -45,31 +67,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 +127,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 +140,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, ensure the app can use GPS when the phone is locked. 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()

301
functions.py Normal file
View File

@ -0,0 +1,301 @@
#!/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/ITPC/XMP-Tags from given arguments.
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
metadata['Exif.Image.ImageDescription'] = new_comment
metadata['Iptc.Application2.Caption'] = [new_comment]
metadata['Xmp.dc.description'] = 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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 186 KiB

After

Width:  |  Height:  |  Size: 199 KiB

104
rad_tag.py Executable file
View File

@ -0,0 +1,104 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
''' Iterates over a bunch of .jpg or .cr2 files and matches
DateTimeOriginal from Exif tags to DateTime in a csv log
of a GeigerMuellerCounter and writes its value to Exif/ITPC/XMP tags in µS/h '''
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/ITPC/XMP tags of given photos.''')
parser.add_argument('-si', '--sifactor', type=float, default=0.0065,
help='Factor to multiply recorded CPM with.')
parser.add_argument('-tz', '--timezone', type=str, metavar='Timezone', default='utc',
help='''Manually set timezone of CSV / and Photo timestamp,
defaults to UTC if omitted. This is useful, if the GPS-Logger
saves the time incl. timezone''')
parser.add_argument('-d', '--dry', action='store_true',
help='Dry-run, do not actually write anything.')
parser.add_argument('csv', metavar='CSV', type=str,
help='Geiger counter history file in CSV format.')
parser.add_argument('-g', '--gpx', metavar='GPX', type=str,
help='GPS track in GPX format')
parser.add_argument('photos', metavar='Photo', type=str, nargs='+',
help='One or multiple photo image files to process.')
parser.add_argument('-o', '--outdir', type=str, default='.',
help='Directory to output processed photos.')
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, that's timestamp and CP/M
for _, csv_raw_time, csv_raw_cpm, _ in csv:
radiation = Radiation(csv_raw_time, csv_raw_cpm, local_timezone, args.sifactor)
radiation_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)