Compare commits

..

14 Commits

Author SHA1 Message Date
44d5ba81ed Changed a lot of names in the geolocation module 2025-08-24 00:31:47 +01:00
8ab3962e6c Changed the spell checker for real time to button activated including other bug fixes and fixed tab movement
Made spell checker work on the title
Increased the word list for the spell checker and made it ingore special characters
2025-08-23 17:32:54 +01:00
70e1163519 Added a very primative spell check function 2025-08-23 14:14:10 +01:00
86060df5eb Fixed some more problems with Geolocation
Fixed invalid source media location showing up as 0,0 instead of empty fields
Added code to parse gps location from exif data
Added invalid default values to all geolocation sources to make it possible to test for have_data being true and no longituted or latitude being set
Changed the way some exif values and timestamp are recorded on the data dictionary
2025-08-22 12:23:19 +01:00
1e9b0108c4 Added option to geolocation module for different tile map servers 2025-08-21 17:50:44 +01:00
ce95656727 Changed the way event changes are attributed and fixed how the texts module gets saved 2025-08-21 16:15:47 +01:00
b024bd909d Fixed bugs in geolocation module
Fixed updating of global have_data value in the module
Fixed map marker staying in last position even if there is no valid data selected
2025-08-21 14:59:13 +01:00
fbee3aa458 Renamed the exit button 2025-08-21 00:07:27 +01:00
fe18f37a2d Added module for geolocation data, changed the way data is saved and fixed timestamp data timezone 2025-08-20 23:45:06 +01:00
eeae202a85 Overhauled the structure of the JSON data and added accuracy metric for the timestamp set of data 2025-08-17 00:05:43 +01:00
444e955bda Code cleanup and fixed spelling mistakes 2025-08-16 20:58:51 +01:00
20cdc275f0 Added support for variable text attributes in frame titles and rearranged some frames 2025-08-16 16:59:56 +01:00
5afa52b1d9 Changed the structure of the JSON output, added timestamp recording, exif reading and did some UI changes 2025-08-16 15:12:58 +01:00
2cadd3f8f8 Removed all unnecassary featurs appart from the title and description and released v0.0 to start tagging some photos 2025-06-20 20:50:17 +01:00

View File

@ -17,12 +17,24 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>. */
#import stuff that's needed for both gui and check mode plus tkinter to make inheritance easier (for now)
#TODO:
#Weather image is cropped
#Make save button red if any data is unparsable
#Add timezone setting for exif date
#Change the background of TitledFrames from the wnidow background
#Make computasionally heavy processes like searching for a point in gpx file in a sepparate thread asynchronously
#Do doable TODOs
#import stuff that's needed for both GUI and check mode plus tkinter to make inheritance easier (for now)
import sys
import hashlib
import json
import os
from datetime import datetime
from datetime import timezone
import tkinter as tk
from tkinter import ttk
import re
def main():
if len(sys.argv) < 2:
@ -36,52 +48,180 @@ def main():
import time
from tkinter import messagebox
from tkinter import Frame
from tkinter import ttk
from tkcalendar import Calendar
from PIL import Image, ImageTk
from PIL.ExifTags import TAGS, GPSTAGS
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from time import strftime, localtime
import tkintermapview
from pathlib import Path
from exif import Image as exifImage
from fractions import Fraction
import gpxpy
import gpxpy.gpx
import nltk
from nltk.corpus import words
from nltk.corpus import wordnet
global words
global wordnet
light_data = {
"lights": [ { "id": 0, "brand":"", "name": "other" },
{ "id": 1, "brand":"", "name": "Sun" },
{ "id": 2, "brand":"Godox", "name": "AD200 Pro" },
{ "id": 3, "brand":"Aputure", "name": "600D" }
]
nltk.download('words')
nltk.download('wordnet')
device_data = {
"lights": [ { "id": 0, "brand":"", "name": "other" },
{ "id": 1, "brand":"", "name": "Sun" },
{ "id": 2, "brand":"Godox", "name": "AD200 Pro" },
{ "id": 3, "brand":"Aputure", "name": "600D" }
],
"cameras": [
{
"id": 0,
"brand":"Unknown",
"model":"unknown",
"serial_number":None
},{
"id": 1,
"brand":"Sony",
"model":"ILCE7R4",
"serial_number":"ThE_S3r4l_#"
},{
"id": 2,
"brand":"Insta360",
"model":"x3",
"serial_number":"7777777777"
}
]
}
#JSON output template
data = {
"program_version": "v0.0-dev",
"data_spec_version": "v0.0-dev",
"title": "",
"capture_time_start": 0,
"capture_time_end": 0,
"image_sha512": sha512Checksum(image_path),
"description": "",
"events" : [{ "time": 1733055790, "text": "Data captured" },
{ "time": 1741745288, "text": "Raw file developed"},
{ "time": 1747012088, "text": "Metadata written" },
{ "time": 1747876088, "text": "Metadata modified" },
{ "time": 1759876088, "text": "Metadata version updated" }
],
"GPS_lat_dec_N": 51.500789280409016,
"GPS_long_dec_W": -0.12472196184719725,
"lights": [{ "source":2, "type":"Flash", "Usage":"pointing to his face" },
{ "source":3, "type":"continious", "Usage":"hair light" },
{ "source":1, "type":"continious", "Usage":"doing its thing" },
{ "source":0, "type":"continious", "Usage":"street light" }
]
"program_version": "v1.0-dev",
"data_spec_version": "v1.0-dev",
"events" : [{
"event_id":0,
"event_type": "capture_start",
"timestamp": 0,
"timestamp_accuracy_seconds": 0,
"text": ""
},
#{
# "event_id":2,
# "event_type": "data_modification",
# "timestamp": 1741745288,
# "text": "Raw file developed"
#},
{
"event_id":1,
"event_type": "metadata_modification",
"timestamp": int(time.time()),
"timestamp_accuracy_seconds": 0,
"text": "Initial metadata written",
"modified_metadata_modules":[
"texts",
"capture_timestamp",
"constants",
"geolocation_data"
]
},
],
"texts": {
"title" : "",
"description" : "",
},
"capture_timestamp": {
"capture_start_on_original_metadata_timestamp": -1,
"capture_duration_seconds": -1,
"single_capture_picture": True,
"capture_start_time_offset_seconds": 0,
},
"geolocation_data" : {
"have_data": False,
"valid_data_source": "uninitialised",
"display_map_tile_server" : "",
"source_gnss_track_file":{
"have_data": False,
"Latitude_decimal": 100000,
"Longitude_decimal": 100000,
"gnss_device_time_offset_seconds": 0,
"file_path": "",
"file_sha512sum":"",
"file_type": ""
},
"source_original_media_file":{
"have_data": False,
"Latitude_decimal": 100000,
"Longitude_decimal": 100000,
},
"source_manual_entry":{
"have_data": False,
"Latitude_decimal": 100000,
"Longitude_decimal": 100000,
}
},
"constants": {
"image_sha512": sha512Checksum(image_path),
"image_file_full_path": os.path.realpath(sys.argv[1])
}
#"lights": [{ "source":2, "type":"Flash", "Usage":"pointing to his face" },
# { "source":3, "type":"continuous", "Usage":"hair light" },
# { "source":1, "type":"continuous", "Usage":"doing its thing" },
# { "source":0, "type":"continuous", "Usage":"street light" }
# ]
}
def save_and_exit():
description_value = description_entry.get("1.0",'end-1c')
data["title"] = title.get()
data["capture_time_start"] = int(time.mktime(time.strptime(timestamp_start.get(), '%Y-%m-%d %H:%M:%S')))
data["capture_time_end"] = int(time.mktime(time.strptime(timestamp_end.get(), '%Y-%m-%d %H:%M:%S')))
data["description"] = description_value
#Get exif from image file
image = Image.open(image_path)
exif_data = image._getexif()
exif_data_ = image.getexif()
with open("output.json", "w") as f:
def nautical_to_decimal(value):
d = float(value[0])
m = float(value[1])
s = float(value[2])
return d + (m / 60.0) + (s / 3600.0)
for tag_id, value in exif_data.items():
tag = TAGS.get(tag_id, tag_id)
if tag == 'ExposureTime':
data["capture_timestamp"]["capture_duration_seconds"] = float(Fraction(value))
elif tag == 'DateTimeOriginal':
dt_str = value
dt = datetime.strptime(dt_str, '%Y:%m:%d %H:%M:%S')
utc_dt = dt.replace(tzinfo=timezone.utc)
data["capture_timestamp"]["capture_start_on_original_metadata_timestamp"]=int(utc_dt.timestamp()) # Unix epoch time
elif tag == 'GPSInfo':
for t in value:
sub_decoded = GPSTAGS.get(t, t)
if sub_decoded == "GPSLatitude":
latitude_nautical = value[t]
elif sub_decoded == "GPSLatitudeRef":
latitude_nautical_ref = value[t]
elif sub_decoded == "GPSLongitude":
longitude_nautical = value[t]
elif sub_decoded == "GPSLongitudeRef":
longitude_nautical_ref = value[t]
if latitude_nautical and latitude_nautical_ref and longitude_nautical and longitude_nautical_ref:
latitude_decimal = nautical_to_decimal(latitude_nautical)
if latitude_nautical_ref != "N":
latitude_decimal = 0 - latitude_decimal
longitude_decimal = nautical_to_decimal(longitude_nautical)
if longitude_nautical_ref != "E":
longitude_decimal = 0 - longitude_decimal
data["geolocation_data"]["source_original_media_file"]["Latitude_decimal"]=latitude_decimal
data["geolocation_data"]["source_original_media_file"]["Longitude_decimal"]=longitude_decimal
data["geolocation_data"]["source_original_media_file"]["have_data"]=True
def save_and_exit():
output_path = Path(data["constants"]["image_file_full_path"]).with_suffix(".json")
with open(output_path, "w") as f:
json.dump(data, f, indent=4)
root.destroy()
@ -91,91 +231,378 @@ def main():
root.title("Metadata Writer")
background_color=root.cget('bg')
# Load and display image
img = Image.open(image_path)
img.thumbnail((400, 400)) # Resize for display
photo = ImageTk.PhotoImage(img)
img_label = tk.Label(root, image=photo, borderwidth=15)
editables=Frame()
#title field
title=TitledEntry(editables,"Ttile","",input_state=tk.NORMAL)
#################
# display image #
#################
display_image_frame=TitledFrame(root, [("Image", ("TkDefaultFont", 10))] )
img = Image.open(image_path)
img.thumbnail((400, 400)) # Resize for display
photo = ImageTk.PhotoImage(img)
img_label = tk.Label(display_image_frame, image=photo, borderwidth=4)
#Description field
description=Frame(editables)
tk.Label(description, text="Description:").pack(side=tk.LEFT)
description_entry = TextScrollCombo(description)
description_entry.pack()
description_entry.config(width=600, height=100)
img_label.grid(row=0,column=0)
#Start/end timestamp fields
timestamp=Frame(editables)
start_var = tk.StringVar(value=strftime('%Y-%m-%d %H:%M:%S', localtime(data["capture_time_start"])))
end_var = tk.StringVar(value=strftime('%Y-%m-%d %H:%M:%S', localtime(data["capture_time_end"])))
timestamp_start = tk.Entry(timestamp,textvariable=start_var)
timestamp_end = tk.Entry(timestamp,textvariable=end_var)
tk.Label(timestamp, text="Shot time/date start:").grid(row=0,column=0,padx=(0,5))
tk.Label(timestamp, text="Shot time/date end:").grid(row=0,column=2,padx=5)
timestamp_start.grid(row=0,column=1)
timestamp_end.grid(row=0,column=3)
#########
# Texts #
#########
def update_texts(*args):
data["texts"]["title"]=title.get("1.0",'end-1c')
data["texts"]["description"]=description.get("1.0",'end-1c')
def spellchecks_trigger(*args):
description.spell_check()
title.spell_check()
texts_frame=TitledFrame(editables,[("[1]", ("TkDefaultFont", 12, "bold")),("Texts", ("TkDefaultFont", 10))])
#sha512 field
sha512sum=TitledEntry(editables,"Image SHA512",data["image_sha512"],input_state=tk.DISABLED)
title = TitledTextEntry(texts_frame,"Title",callback=update_texts)
description = TitledTextEntry(texts_frame,"Description:",callback=update_texts,scroll=True)
#version field
versions=Frame(editables)
program_version=TitledEntry(versions,"Program version",data["program_version"],width=8,input_state=tk.DISABLED)
data_spec_version=TitledEntry(versions,"Data specification version",data["data_spec_version"],width=8,input_state=tk.DISABLED)
program_version.grid(row=0,column=0,padx=(0,5))
data_spec_version.grid(row=0,column=1,padx=5)
spell_check_button = tk.Button(texts_frame, text="Spell check", command=spellchecks_trigger)
# Save button
save_button = tk.Button(editables, text="Save and Exit", command=save_and_exit)
title.grid (row=0,column=0,sticky='we',padx=3,pady=3)
description.grid (row=1,column=0,sticky='we',padx=3,pady=3)
spell_check_button.grid (row=2,column=0,sticky='w',padx=3,pady=3)
texts_frame.grid_columnconfigure(0, weight=1)
# Map widget
map_frame=Frame(root)
map_widget = tkintermapview.TkinterMapView(map_frame, width=400, height=400, corner_radius=10)
map_widget.set_position(data["GPS_lat_dec_N"], data["GPS_long_dec_W"])
marker_1=map_widget.set_marker(data["GPS_lat_dec_N"], data["GPS_long_dec_W"])
####################
# Geolocation data #
####################
def try_gpx_file(filepath):
gpx_file = open(filepath, 'r')
gpx = gpxpy.parse(gpx_file)
point_found=0
data["geolocation_data"]["source_gnss_track_file"]["have_data"]=False
for track in gpx.tracks:
for segment in track.segments:
for point in segment.points:
if(point.time == datetime.fromtimestamp(data["events"][0]["timestamp"]-data["geolocation_data"]["source_gnss_track_file"]["gnss_device_time_offset_seconds"],tz=timezone.utc)): #TODO don't hardcode this value
data["geolocation_data"]["source_gnss_track_file"]["Longitude_decimal"]=point.longitude
data["geolocation_data"]["source_gnss_track_file"]["Latitude_decimal"]=point.latitude
data["geolocation_data"]["source_gnss_track_file"]["file_path"]=filepath
data["geolocation_data"]["source_gnss_track_file"]["file_sha512sum"]=sha512Checksum(filepath)
data["geolocation_data"]["source_gnss_track_file"]["file_type"]="gpx"
data["geolocation_data"]["source_gnss_track_file"]["have_data"]=True
point_found=1
break
if point_found==1:
break
if point_found==1:
break
if point_found==0:
return 1
else:
return 0
def Geolocation_update(*args):
manual_latitude=geolocation_manual_entry_source.get_latitude()
manual_longitude=geolocation_manual_entry_source.get_longitude()
data["geolocation_data"]["display_map_tile_server"]=map_tile_server_selection.get()
data["geolocation_data"]["valid_data_source"]=human_name_to_source[geolocation_source_selection.get()]
map_widget.set_tile_server(tilemap_url_option_map[data["geolocation_data"]["display_map_tile_server"]], max_zoom=tilemap_maxzoom_option_map[data["geolocation_data"]["display_map_tile_server"]])
try:
manual_latitude=float(manual_latitude)
manual_longitude=float(manual_longitude)
data["geolocation_data"]["source_manual_entry"]["Latitude_decimal"]=manual_latitude
data["geolocation_data"]["source_manual_entry"]["Longitude_decimal"]=manual_longitude
data["geolocation_data"]["source_manual_entry"]["have_data"]=True
except ValueError as e:
data["geolocation_data"]["source_manual_entry"]["have_data"]=False
global map_marker
if data["geolocation_data"][data["geolocation_data"]["valid_data_source"]]["have_data"] == True:
new_latitude=data["geolocation_data"][data["geolocation_data"]["valid_data_source"]]["Latitude_decimal"]
new_longitude=data["geolocation_data"][data["geolocation_data"]["valid_data_source"]]["Longitude_decimal"]
if map_marker == None:
map_marker=map_widget.set_marker(new_latitude,new_longitude)
else:
map_marker.set_position(new_latitude,new_longitude)
map_widget.set_position(new_latitude,new_longitude)
data["geolocation_data"]["have_data"]=True
else:
data["geolocation_data"]["have_data"]=False
if map_marker != None:
map_marker.delete()
map_marker = None
def Geolocation_update_time(*args):
try:
data["geolocation_data"]["source_gnss_track_file"]["gnss_device_time_offset_seconds"]=float(gnss_device_time_offset.get())
except ValueError as e:
data["geolocation_data"]["source_gnss_track_file"]["gnss_device_time_offset_seconds"]=0
for file in os.listdir("/home/user/gnss_test/"): #TODO TODO TODO TODO
if file.endswith(".gpx"):
#print("Trying "+str(os.path.join("/home/user/gnss_test/", file)))
if try_gpx_file(os.path.join("/home/user/gnss_test/", file)) == 0:
break
if data["geolocation_data"]["source_gnss_track_file"]["have_data"] == True :
geolocation_gnss_track_file_source.update_latitude( data["geolocation_data"]["source_gnss_track_file"]["Latitude_decimal"])
geolocation_gnss_track_file_source.update_longitude( data["geolocation_data"]["source_gnss_track_file"]["Longitude_decimal"])
else:
geolocation_gnss_track_file_source.update_latitude("")
geolocation_gnss_track_file_source.update_longitude("")
Geolocation_update()
geolocation_data_frame=TitledFrame(root,[("[3]", ("TkDefaultFont", 12, "bold")),("Geolocation data", ("TkDefaultFont", 10))])
#Map Widget
map_widget = tkintermapview.TkinterMapView(geolocation_data_frame, width=400, height=250, corner_radius=10)
map_widget.set_position(data["geolocation_data"]["source_gnss_track_file"]["Latitude_decimal"], data["geolocation_data"]["source_gnss_track_file"]["Longitude_decimal"])
map_widget.set_zoom(15)
map_widget.pack(pady=15)
global map_marker
map_marker=None
#timeline field
timeline = event_timeline(root,data["events"],matplotlib.pyplot,numpy,FigureCanvasTkAgg,background_color)
map_tile_server_selection=TitledDropdown(geolocation_data_frame,"Map tile server",(
"OpenStreetMaps online",
"Google Maps default online",
"Google Maps satelite online"),0,callback=Geolocation_update)
tilemap_url_option_map={
"OpenStreetMaps online": "https://a.tile.openstreetmap.org/{z}/{x}/{y}.png",
"Google Maps default online": "https://mt0.google.com/vt/lyrs=m&hl=en&x={x}&y={y}&z={z}&s=Ga",
"Google Maps satelite online": "https://mt0.google.com/vt/lyrs=s&hl=en&x={x}&y={y}&z={z}&s=Ga"
}
tilemap_maxzoom_option_map={
"OpenStreetMaps online": 20,
"Google Maps default online": 22,
"Google Maps satelite online": 22
}
geolocation_source_selection=TitledDropdown(geolocation_data_frame,"Select geolocation source:",
("Original media file",
"GPX file",
"Manual entry")
,0,callback=Geolocation_update)
human_name_to_source = {
"Original media file": "source_original_media_file",
"GPX file": "source_gnss_track_file",
"Manual entry": "source_manual_entry"
}
gnss_device_time_offset=TitledEntry(geolocation_data_frame,"GNSS device time offset (seconds)",data["geolocation_data"]["source_gnss_track_file"]["gnss_device_time_offset_seconds"],callback=Geolocation_update_time)
#Sources
geolocation_gnss_track_file_source=Geolocation_source(geolocation_data_frame,
"GPX file:",
data["geolocation_data"]["source_gnss_track_file"]["Latitude_decimal"],
data["geolocation_data"]["source_gnss_track_file"]["Longitude_decimal"],
tk.DISABLED)
if data["geolocation_data"]["source_original_media_file"]["have_data"] == True:
source_file_latitude=data["geolocation_data"]["source_original_media_file"]["Latitude_decimal"]
source_file_longitude=data["geolocation_data"]["source_original_media_file"]["Longitude_decimal"]
else:
source_file_latitude=""
source_file_longitude=""
geolocation_original_media_file_source=Geolocation_source(geolocation_data_frame,
"Original media file:",
source_file_latitude,
source_file_longitude,
tk.DISABLED)
geolocation_manual_entry_source=Geolocation_source(geolocation_data_frame,
"Original media file:",
"",
"",
tk.NORMAL, callback=Geolocation_update)
map_widget.grid (row=0,column=0,pady=(0,3),padx=5)
map_tile_server_selection.grid (row=1,column=0,pady=(5,2),sticky='we')
geolocation_source_selection.grid (row=2,column=0,pady=(2,2),sticky='we')
gnss_device_time_offset.grid (row=3,column=0,pady=(2,5),sticky='w')
geolocation_gnss_track_file_source.grid (row=4,column=0,sticky='we')
geolocation_original_media_file_source.grid (row=5,column=0,sticky='we')
geolocation_manual_entry_source.grid (row=6,column=0,sticky='we')
#Geolocation_update_time() #Note, not needed because the capture timestamp callback will call it
#####################
# Capture timestamp #
#####################
capture_timestamp=TitledFrame(editables,[("[2]", ("TkDefaultFont", 12, "bold")),("Capture timestamp", ("TkDefaultFont", 10))])
#Callback for updating the explanation
def update_capture_timestamp_description(*args):
image_creation_event_id=0#TODO: don't hardcode this value
try:
data["capture_timestamp"]["capture_start_time_offset_seconds"] = float(cap_offset_var.get())
#If the capture time changes, update it and a list of thing that depend on it
if data["events"][image_creation_event_id]["timestamp"] != int(data["capture_timestamp"]["capture_start_time_offset_seconds"])+int(data["capture_timestamp"]["capture_start_on_original_metadata_timestamp"]):
data["events"][image_creation_event_id]["timestamp"] = int(data["capture_timestamp"]["capture_start_time_offset_seconds"])+int(data["capture_timestamp"]["capture_start_on_original_metadata_timestamp"])
Geolocation_update_time()
data["capture_timestamp"]["capture_duration_seconds"] = float(cap_duration_var.get())
data["capture_timestamp"]["single_capture_picture"] = one_capture_var.get()
data["events"][image_creation_event_id]["timestamp_accuracy_seconds"] = float(cap_accuracy_var.get())
except ValueError as e:
explanation_var.set("Invalid values!")
explanation.config(bg="red")
return
date=time.strftime('%A %-d of %B %Y %H:%M:%S',time.gmtime(data["capture_timestamp"]["capture_start_on_original_metadata_timestamp"]+int(cap_offset_var.get())))
if data["events"][image_creation_event_id]["timestamp_accuracy_seconds"] != 0.0:
acc_string=" plus/minus "+str(data["events"][image_creation_event_id]["timestamp_accuracy_seconds"])+" seconds"
else:
acc_string=""
if data["capture_timestamp"]["capture_duration_seconds"] == False:
explanation_var.set("A multi-picture image (focus stack/exposure stack/etc) that started being taken at " + date + acc_string + " and took " + str(data["capture_timestamp"]["capture_duration_seconds"]) + " seconds to capture" )
else:
explanation_var.set("An image taken at " + date + acc_string + " with a " + str(data["capture_timestamp"]["capture_duration_seconds"]) + " second shutter speed")
explanation.config(bg="grey64")
# explanation text
explanation_var = tk.StringVar()
explanation = tk.Label(capture_timestamp, textvariable=explanation_var, wraplength=450)
explanation.config(width=70)
# Original capture date
cap_start_var = tk.StringVar(value=strftime('%Y-%m-%d %H:%M:%S', time.gmtime(data["capture_timestamp"]["capture_start_on_original_metadata_timestamp"]) ))
cap_start_label=tk.Label(capture_timestamp, text="Original capture start date:")
cap_start = tk.Entry(capture_timestamp,textvariable=cap_start_var,state=tk.DISABLED)
# Capture date offset
cap_offset_var = tk.StringVar(value=data["capture_timestamp"]["capture_start_time_offset_seconds"])
cap_offset_var.trace_add("write", update_capture_timestamp_description)
cap_offset_label=tk.Label(capture_timestamp, text="Capture start date offset seconds:")
cap_offset = tk.Entry(capture_timestamp,textvariable=cap_offset_var)
# Capture duration
cap_duration_var = tk.StringVar(value=str(data["capture_timestamp"]["capture_duration_seconds"]))
cap_duration_var.trace_add("write", update_capture_timestamp_description)
cap_duration_label=tk.Label(capture_timestamp, text="Capture duration (seconds):")
cap_duration = tk.Entry(capture_timestamp,textvariable=cap_duration_var)
# Capture accuracy
cap_accuracy_var = tk.StringVar(value=str(data["events"][0]["timestamp_accuracy_seconds"])) #TODO: don't hardcode this value
cap_accuracy_var.trace_add("write", update_capture_timestamp_description)
cap_accuracy_label=tk.Label(capture_timestamp, text="Capture start accuracy (±seconds):")
cap_accuracy = tk.Entry(capture_timestamp,textvariable=cap_accuracy_var)
# One shot checkbox
one_capture_var = tk.BooleanVar()
one_capture_var.trace_add( "write", update_capture_timestamp_description)
one_capture = tk.Checkbutton(capture_timestamp, text="Final picture is comprised of one capture",variable=one_capture_var )
one_capture.select() #this also calls update_capture_timestamp_description. If removed place a call to it to write the initial value on the text box
cap_start_label.grid (row=0,column=0,padx=3,pady=3)
cap_start.grid (row=0,column=1,padx=3,pady=3)
cap_duration_label.grid (row=0,column=2,padx=3,pady=3)
cap_duration.grid (row=0,column=3,padx=3,pady=3)
cap_offset_label.grid (row=1,column=0,padx=3,pady=3)
cap_offset.grid (row=1,column=1,padx=3,pady=3)
cap_accuracy_label.grid (row=1,column=2,padx=3,pady=3)
cap_accuracy.grid (row=1,column=3,padx=3,pady=3)
one_capture.grid (row=2,column=3,padx=3,pady=3)
explanation.grid (row=2,column=0,padx=3,pady=3,columnspan=3)
#############
# Constants #
#############
constants_frame=TitledFrame(editables,[("Constants", ("TkDefaultFont", 10))])
sha512sum=TitledEntry(constants_frame,"Image SHA512",data["constants"]["image_sha512"],input_state=tk.DISABLED)
sha512sum=TitledEntry(constants_frame,"Image SHA512",data["constants"]["image_sha512"],input_state=tk.DISABLED)
filename=TitledEntry(constants_frame,"Image Filename",data["constants"]["image_file_full_path"],input_state=tk.DISABLED)
program_version=TitledEntry(constants_frame,"Program version",data["program_version"],width=8,input_state=tk.DISABLED)
data_spec_version=TitledEntry(constants_frame,"Data specification version",data["data_spec_version"],width=8,input_state=tk.DISABLED)
filename.grid (row=0,column=0,padx=3,pady=3,columnspan=2,sticky='we')
sha512sum.grid (row=1,column=0,padx=3,pady=3,columnspan=2,sticky='we')
program_version.grid (row=2,column=0,padx=3,pady=3,sticky='we')
data_spec_version.grid (row=2,column=1,padx=3,pady=3,sticky='we')
constants_frame.grid_columnconfigure(0, weight=1)
constants_frame.grid_columnconfigure(1, weight=1)
########
# Save #
########
save_frame=TitledFrame(editables,[("[4]", ("TkDefaultFont", 12, "bold")),("Save", ("TkDefaultFont", 10))])
save_button = tk.Button(save_frame, text="Write and Exit", command=save_and_exit)
save_button.config(bg='green')
event_list=[]
for item in data["events"]:
if item["event_type"] == "metadata_modification":
event_list.append("event id "+str(item["event_id"])+" : "+item["text"])
save_button.grid (row=0,column=1,padx=3,pady=3,sticky='e')
save_frame.grid_columnconfigure(0, weight=1)
##################
# timeline field #
##################
def events_to_tags(json_events):
capture_start=-1
return_data=[]
for item in json_events:
if item["event_type"] == "capture_start":
capture_start=item["timestamp"]
else:
return_data.append({"timestamp":item["timestamp"],"text":item["text"]})
return_data.append({"timestamp":capture_start,"text":"Captured data"})
return return_data
timeline_frame=TitledFrame(root,[("Timeline", ("TkDefaultFont", 10))])
timeline = event_timeline(timeline_frame,events_to_tags(data["events"]),matplotlib.pyplot,numpy,FigureCanvasTkAgg,background_color)
timeline.configure(bg=background_color)
#media_aqusition=TitledDropdown(root,"Media Aquisition",["unkown","Direct digital off of taking device","Received digitial unmodified from taking device","Received digital re-encoded and or metadata stripped","Received digital editied"],0)
timeline.grid(row=0,column=0)
#light table
table=[]
for item in data["lights"]:
for device in light_data["lights"]:
if device["id"] == item["source"]:
table.append([device["brand"]+device["name"],item["type"],item["Usage"]])
#####################
# Media acquisition #
#####################
#media_acquisition=TitledDropdown(root,"Media Acquisition",["unknown","Direct digital off of taking device","Received digital unmodified from taking device","Received digital re-encoded and or metadata stripped","Received digital edited"],0)
light_table=TitledTable(editables,"List of lights / flashes used:",ttk,table,["Device","Type","Usage"],[140,100,450],['w','w','w'])
##########
# Lights #
##########
# #light table
# table=[]
# for item in data["lights"]:
# for device in device_data["lights"]:
# if device["id"] == item["source"]:
# table.append([device["brand"]+device["name"],item["type"],item["Usage"]])
# light_table=TitledTable(editables,"List of lights / flashes used:",table,["Device","Type","Usage"],[140,100,450],['w','w','w'])
#Window layout
img_label .grid(row=0,column=0,sticky='n')
editables .grid(row=0,column=1,rowspan=2,sticky='ns')
map_frame .grid(row=1,column=0)
timeline .grid(row=2,column=0,columnspan=2)
#Root frame layout
display_image_frame .grid(row=0,column=0,sticky='n')
editables .grid(row=0,column=1,rowspan=2,sticky='ns')
geolocation_data_frame .grid(row=1,column=0)
timeline_frame .grid(row=2,column=0,columnspan=2)
title .grid(row=0,column=0,sticky="we",pady=(10,5))
description .grid(row=1,column=0,sticky="we",pady=5)
timestamp .grid(row=2,column=0,sticky="we",pady=5)
sha512sum .grid(row=3,column=0,sticky="we",pady=5)
versions .grid(row=4,column=0,sticky="we",pady=5)
light_table .grid(row=5,column=0,sticky="we",pady=5)
save_button .grid(row=6,column=0,pady=(20,5))
#editables frame layout
texts_frame .grid(row=0,column=0,sticky="we",pady=5)
capture_timestamp .grid(row=1,column=0,sticky="we",pady=5)
save_frame .grid(row=3,column=0,sticky="we",pady=5)
constants_frame .grid(row=4,column=0,sticky="we",pady=5)
# light_table .grid(row=6,column=0,sticky="we",pady=5)
#This updates the default geolocation source after the timestamp callback calls the geolocation callback that looks through all the files
if data["geolocation_data"]["source_original_media_file"]["have_data"] == True :
geolocation_source_selection.set(0)
elif data["geolocation_data"]["source_gnss_track_file"]["have_data"] == True :
geolocation_source_selection.set(1)
else:
geolocation_source_selection.set(2)
#Focus debug found online
#def debug_focus(event):
# print("Focus now on:", event.widget)
#for widget in root.winfo_children():
# widget.bind_all("<FocusIn>", debug_focus)
root.mainloop()
#Got md5Checksum (sha512Checksum now) from someones blog https://www.joelverhagen.com/blog/2011/02/md5-hash-of-file-in-python/
def sha512Checksum(filePath):
with open(filePath, 'rb') as fh:
@ -187,61 +614,117 @@ def sha512Checksum(filePath):
m.update(data)
return m.hexdigest()
def spell_check(self):
content=self.text.get("1.0",'end-1c')
wn_lemmas = set(wordnet.all_lemma_names())
for tag in self.text.tag_names():
self.text.tag_delete(tag)
for word in content.split(' '):
word_to_check=re.sub(r'[^\w]', '', word.lower()).lower()
if wordnet.synsets(word_to_check) == [] :
if word_to_check not in words.words():
position = content.find(word)
self.text.tag_add(word, f'1.{position}', f'1.{position + len(word)}')
self.text.tag_config(word, underline=True, underlinefg='red')
def on_key_press(event):
if event.keysym == "Return": # prevent newline
return "break"
if event.keysym == "Tab": # move focus instead of inserting tab
event.widget.tk_focusNext().focus()
return "break"
#Got TextScrollCombo from stack overflow https://stackoverflow.com/questions/13832720/how-to-attach-a-scrollbar-to-a-text-widget
class TextScrollCombo(tk.Frame):
class TitledTextEntry(tk.Frame):
def __init__(self, *args, **kwargs):
def __init__(self, root_window, title, callback=None, scroll=False):
super().__init__(*args, **kwargs)
super().__init__(root_window)
# ensure a consistent GUI size
self.grid_propagate(False)
# implement stretchability
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
# create a Text widget
self.txt = tk.Text(self,height=10)
self.txt.grid(row=0, column=0, sticky="nsew")
if scroll==True:
height=5
else:
height=1
# create a Scrollbar and associate it with txt
scrollb = tk.Scrollbar(self, command=self.txt.yview)
scrollb.grid(row=0, column=1, sticky='nsew')
self.txt['yscrollcommand'] = scrollb.set
self.text = tk.Text(self)
self.text.config(height=height)
def get(c,a,b):
return c.txt.get(a,b)
if callback != None:
self.text.bind('<KeyRelease>', callback)
#TODO: delete if unused before release
#class TitledDropdown(tk.Frame):
#
# def __init__(self, root_window, text, options, default_opt):
#
# super().__init__(root_window)
#
# self.titled_dropdown = tk.OptionMenu(self,tk.StringVar(value=options[default_opt]),*options)
# self.titled_dropdown.config(width=8)
# tk.Label(self, text=text).pack(side=tk.LEFT)
# self.titled_dropdown.pack(fill=tk.X)
# def get(c):
# return c.titled_dropdown.get()
if scroll==True:
tk.Label(self, text=title).grid(row=0, column=0, sticky="w")
self.text.grid(row=1, column=0, sticky="we")
else:
tk.Label(self, text=title).grid(row=0, column=0, sticky="w")
self.text.grid(row=0, column=1, sticky="we")
class TitledEntry(tk.Frame):
if scroll==True:
# create a Scrollbar and associate it with txt
scrollbar = tk.Scrollbar(self, command=self.text.yview)
scrollbar.grid(row=1, column=1, sticky='nsew')
scrollbar.configure(takefocus=0)
self.text['yscrollcommand'] = scrollbar.set
else:
self.text.config(wrap='none')
self.text.bind("<Return>", on_key_press)
self.text.bind("<Tab>", on_key_press)
def __init__(self, root_window, text, init_text, input_state=tk.NORMAL, width=None):
def get(self,a,b):
return self.text.get(a,b)
def spell_check(self):
spell_check(self)
class TitledDropdown(tk.Frame):
def __init__(self, root_window, text, options, default_opt, callback=None):
super().__init__(root_window)
self.title_entry = tk.Entry(self,state=input_state,textvariable=tk.StringVar(value=init_text),width=width)
tk.Label(self, text=text).pack(side=tk.LEFT)
self.title_entry.pack(fill=tk.X)
self.titled_dropdown = ttk.Combobox(self,value=options)
self.titled_dropdown.set(options[default_opt])
self.titled_dropdown.config(width=8)
tk.Label(self, text=text).grid (row=0,column=0,sticky='w')
self.titled_dropdown.grid (row=0,column=1,sticky='we')
self.grid_columnconfigure(1, weight=1)
self.callback=callback
if callback != None:
self.titled_dropdown.bind('<<ComboboxSelected>>', callback)
def get(c):
return c.title_entry.get()
return c.titled_dropdown.get()
def set(self,n):
self.titled_dropdown.current(n)
if self.callback != None:
self.callback()
class TitledEntry(tk.Frame):
def __init__(self, root_window, text, init_text, input_state=tk.NORMAL, width=None, callback=None):
super().__init__(root_window)
self.titled_entry_var=tk.StringVar(value=init_text)
self.text = tk.Entry(self,state=input_state,textvariable=self.titled_entry_var,width=width)
if callback != None:
self.titled_entry_var.trace_add("write", callback)
self.label=tk.Label(self, text=text)
self.label.grid(row=0,column=0,sticky='w')
self.text.grid(row=0,column=1,sticky='we')
self.grid_columnconfigure(1, weight=1)
def get(self):
return self.text.get()
def spell_check(self):
spell_check(self)
class TitledTable(tk.Frame):
def __init__(self, root_window, text, ttk, table, header, widths, anchors):
def __init__(self, root_window, text, table, header, widths, anchors):
super().__init__(root_window)
@ -263,7 +746,7 @@ class TitledTable(tk.Frame):
self.treeview.column(header[i], width = widths[i], anchor=anchors[i])
self.scrollb = tk.Scrollbar(self, command=self.treeview.yview)
self.scrollbar = tk.Scrollbar(self, command=self.treeview.yview)
self.utility_frame=tk.Frame(self)
self.modify_button=tk.Button(self.utility_frame,text="Modify",width=8)
@ -274,11 +757,57 @@ class TitledTable(tk.Frame):
tk.Label(self, text=text).grid(row=0,column=0,sticky="w")
self.treeview.grid(row=1,column=0,sticky='we')
self.scrollb.grid(row=1,column=1,sticky='ns')
self.treeview['yscrollcommand'] = self.scrollb.set
self.scrollbar.grid(row=1,column=1,sticky='ns')
self.scrollbar.configure(takefocus=0)
self.treeview['yscrollcommand'] = self.scrollbar.set
self.utility_frame.grid(row=1,column=2,sticky='n')
def get(c):
return c.title_entry.get()
class Geolocation_source(tk.Frame):
def __init__(self, root, text, lat_source, long_source, state, callback=None):
super().__init__(root)
self.separator = ttk.Separator(self, orient=tk.HORIZONTAL)
self.separator_label = tk.Label(self, text=text)
self.paste_button = tk.Button(self, text="Paste",state=state, command=self.paste_callback)
self.root=root
self.fields=tk.Frame(self)
self.lat_var = tk.StringVar(value=lat_source)
self.lat_label = tk.Label(self.fields, text="Latitude:")
self.lat = tk.Entry(self.fields,textvariable=self.lat_var,state=state)
self.long_var = tk.StringVar(value=long_source)
self.long_label = tk.Label(self.fields, text="Longtitude:")
self.long = tk.Entry(self.fields,textvariable=self.long_var,state=state)
self.lat_label.grid (row=0,column=0,pady=3)
self.lat.grid (row=0,column=1,pady=3)
self.long_label.grid (row=0,column=2,pady=3)
self.long.grid (row=0,column=3,pady=3)
self.separator.grid (row=0,column=0,columnspan=2,pady=4,sticky='we')
self.separator_label.grid (row=1,column=0,sticky='w')
self.paste_button.grid (row=1,column=1,sticky='e')
self.fields.grid (row=2,column=0,columnspan=2,sticky='w')
if callback != None:
self.lat_var.trace_add("write", callback)
self.long_var.trace_add("write", callback)
def get_latitude(self):
return self.lat_var.get()
def get_longitude(self):
return self.long_var.get()
def update_latitude(self,value):
self.lat_var.set(value)
def update_longitude(self,value):
self.long_var.set(value)
def paste_callback(self):
clipboard=self.root.clipboard_get()
self.lat_var.set(clipboard.split()[0])
self.long_var.set(clipboard.split()[1])
def event_timeline(window,events,plt,np,FigureCanvasTkAgg,background_color):
@ -293,7 +822,7 @@ def event_timeline(window,events,plt,np,FigureCanvasTkAgg,background_color):
labels=[]
for item in events:
labels.append(item["text"])
timelines.append(datetime.fromtimestamp(item["time"]))
timelines.append(datetime.fromtimestamp(item["timestamp"]))
offsets= [3,2,1]
levels = np.tile(offsets, int(np.ceil(len(timelines)/len(offsets))))[:len(timelines)]
@ -316,10 +845,20 @@ def event_timeline(window,events,plt,np,FigureCanvasTkAgg,background_color):
ax.spines['bottom'].set_position(('data', -8000))
canvas = FigureCanvasTkAgg(fig, master = window)
canvas.get_tk_widget().configure(takefocus=0)
canvas.draw()
plt.close()
return canvas.get_tk_widget()
def TitledFrame(root, title_parts):
lf = ttk.Labelframe(root, padding=2,borderwidth=4,relief="ridge")
title = ttk.Frame(lf)
for text, font in title_parts:
ttk.Label(title, text=text, font=font).pack(side="left")
lf.configure(labelwidget=title) # managed by the labelframe itself
return lf
if __name__ == "__main__":
main()