Add config file, l10n, session resuming

This commit is contained in:
ABelliqueux 2024-02-18 13:12:09 +01:00
parent 4d24f104d9
commit a19f713d16
5 changed files with 156 additions and 74 deletions

25
config.toml Normal file
View File

@ -0,0 +1,25 @@
[DEFAULT]
file_extension = 'JPG'
trigger_mode = 'key'
projects_folder = ''
# project_letter = A
onion_skin_onstartup = false
onionskin_alpha_default = 0.4
onionskin_fx = false
fullscreen_bool = true
screen_w = 1920
screen_h = 1080
framerate = 16
vflip = true
hflip = false
export_options = 'scale=1920:-1,crop=1920:1080:0:102'
[CAMERA]
# Nikon D40x
capturemode = 3 # use IR remote
imagesize = 2 # use size S (1936x1296)
whitebalance = 1 # Natural light
capturetarget = 0 # Internal memory
nocfcardrelease = 0 # Allow capture without sd card
recordingmedia = 1 # Write to RAM
[CHECK]
acpower = 0 # we d'rather have this set to 0 which means we're running on AC

Binary file not shown.

Binary file not shown.

201
main_c.py
View File

@ -1,82 +1,110 @@
#!/usr/bin/env python3
#
import collections
import gettext
# DLSR support
import gphoto2 as gp
from itertools import count
import locale
import os
from PIL import Image, ImageTk, ImageFilter
import sys
import threading
import time
import tkinter as tk
from tkinter import filedialog
from PIL import Image, ImageTk, ImageFilter
# DLSR support
import gphoto2 as gp
from tkinter import filedialog, messagebox
import tomllib
# async FFMPEG exportation support
import asyncio
from ffmpeg.asyncio import FFmpeg
from itertools import count
from send2trash import send2trash
# TODO
# TODO : Todo List
#
# X wait event mode
# X remove frames
# X keep images in memory (odict > list ?)
# o resize images upon shooting/ crop output video ?
# o project workflow
# / resize images upon shooting/crop output video ?
# X project workflow
# X use a different folder for each project (A, B, C, etc...)
# X startup : check for existing folder with name A, B, C, etc.
# - startup : offer to continue previous project or new project
# - if continue, find last frame in folder X else, find next letter and create folder
# o notify export endingS
# X startup : offer to continue previous project or new project
# X if continue, find last frame in folder X else, find next letter and create folder
# o notify export ending
# X colored onion skin frame (pillow filters)
# V startup frame
# o open existing project screen
# X startup frame
# o webcam support (pygame, win and linux only)
# X Import config values from config file
# X Translation
# o Better settings names
running_from_folder = os.path.realpath(__file__)
alphabet = ['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z']
project_settings = {
# l10n
LOCALE = os.getenv('LANG', 'en_EN')
_ = gettext.translation('template', localedir='locales', languages=[LOCALE]).gettext
# Config
# defaults
project_settings_defaults = {
'file_extension':'JPG',
'trigger_mode':'',
'trigger_mode': 'key',
'projects_folder': '',
# ~ 'project_letter': 'A'
'onion_skin_onstartup' : False,
'onionskin_alpha_default' : 0.4,
'onionskin_fx' : False,
'fullscreen_bool' : True,
'screen_w' : 640,
'screen_h' : 480,
'framerate' : 16,
'vflip' : False,
'hflip' : False,
'export_options' : 'scale=1920:-1,crop=1920:1080:0:102',
}
# Camera Settings (Nikon D40x)
# See `gphoto2 --list-config` to find your camera's values
camera_settings = camera_status = {}
# Load from file
config_locations = ["./", "~/.", "~/.config/"]
config_found_msg = _("No configuration file found, using defaults.")
project_settings = project_settings_defaults
for location in config_locations:
# Optional config files, ~ is expanded to $HOME on *nix, %USERPROFILE% on windows
if os.path.exists( os.path.expanduser(os.path.join(location, 'config.toml'))):
with open(os.path.expanduser(location + 'config.toml'), 'rb') as config_file:
project_settings = tomllib.load(config_file)
if 'DEFAULT' in project_settings:
project_settings = project_settings['DEFAULT']
if 'CAMERA' in project_settings:
camera_settings = project_settings['CAMERA']
if 'CHECK' in project_settings:
camera_settings = project_settings['CHECK']
config_found_msg = _("Found configuration file in {}").format(os.path.expanduser(location))
print(config_found_msg)
# TODO : Check resulting config has needed settings, fill missing ones
class KISStopmo(tk.Tk):
def __init__(self, *args, **kargs):
self.check_config()
print(project_settings)
# Default config
# TODO : Import value from config file
# Script settings
self.onion_skin = False
self.onionskin_alpha = 0.4
self.onionskin_fx = False
self.fullscreen_bool = True
self.screen_w, self.screen_h = 640, 480
self.framerate = 16
self.photo = None
# ~ self.savepath = os.getcwd()
# Set script settings according to config file
self.onion_skin = project_settings['onion_skin_onstartup']
self.onionskin_alpha = project_settings['onionskin_alpha_default']
self.onionskin_fx = project_settings['onionskin_fx']
self.fullscreen_bool = project_settings['fullscreen_bool']
self.screen_w, self.screen_h = project_settings['screen_w'], project_settings['screen_h']
self.framerate = project_settings['framerate']
# ~ for setting in camera_settings:
# ~ print(setting)
# ~ self.photo = None
self.end_thread = False
# ~ self.project_settings = {'file_extension' : 'JPG'}
# ~ self.project_letter = "A"
# Camera Settings
self.camera_settings = {'capturemode': 3,
'imagesize': 2,
'whitebalance': 5,
'capturetarget': 0,
'nocfcardrelease': 0,
'recordingmedia' : 1
}
self.camera_status = { 'acpower': 0 # we d'rather have this set to 0 which means we're running on AC
}
# Window setup
self.screen_w, self.screen_h = root.winfo_screenwidth(), root.winfo_screenheight()
root.wm_attributes("-fullscreen", self.fullscreen_bool)
@ -87,8 +115,7 @@ class KISStopmo(tk.Tk):
self.label.pack()
# Savepath setup
self.projects_folder = filedialog.askdirectory()
self.projects_folder = tk.filedialog.askdirectory()
self.savepath = self.get_session_folder()
if len(self.savepath):
self.project_letter = self.savepath.split(os.sep)[-1]
@ -100,8 +127,7 @@ class KISStopmo(tk.Tk):
self.input_options = {"f": "image2", "r": str(self.framerate)}
# ~ self.output_filename = "{folder}{sep}{filename}.mp4".format(folder=self.projects_folder, sep=os.sep, filename=self.savepath.split(os.sep)[-1])
self.output_filename = "{filename}.mp4".format(filename=self.project_letter)
self.output_options = {"vf":"scale=1920:-1,crop=1920:1080:0:102"}
self.output_options = self.parse_export_options(project_settings['export_options'], project_settings['vflip'], project_settings['hflip'] )
# Get frames list
self.img_list = {}
self.img_list = self.get_frames_list(self.savepath)
@ -115,9 +141,9 @@ class KISStopmo(tk.Tk):
self.current_camera_config = gp.check_result(gp.gp_camera_get_config(self.camera))
self.apply_camera_settings(self.camera, self.current_camera_config)
if self.check_status(self.camera, self.current_camera_config) is False:
print("Warning: Some settings are not set to the recommended value!")
print(_("Warning: Some settings are not set to the recommended value!"))
except:
self.splash_text = 'Camera not found or busy.'
self.splash_text = _("Camera not found or busy.")
self.timeout = 3000 # milliseconds
@ -151,15 +177,32 @@ class KISStopmo(tk.Tk):
if project_settings['trigger_mode'] != 'event':
root.bind("<j>", self.capture_image)
def check_config(self):
global project_settings
for setting in project_settings_defaults:
if setting not in project_settings:
project_settings[setting] = project_settings_defaults[setting]
def parse_export_options(self, options:str, vflip:bool=False, hflip:bool=False):
options = {"vf" : options}
if vflip:
options['vf'] += ',vflip'
if hflip:
options['vf'] += ',hflip'
return options
def generate_splashscreen(self):
from PIL import Image, ImageDraw, ImageFont
from io import BytesIO
img = Image.new('RGB', (self.screen_w, self.screen_h), (128,128,128))
d = ImageDraw.Draw(img)
if self.splash_text is not None:
fnt = ImageFont.truetype("FreeMono", 100)
d.text((900, 500 ), self.splash_text, fill=(255, 0, 0), font=fnt)
fnt = ImageFont.truetype("FreeMono", 50)
fnt_len = fnt.getlength(self.splash_text)
d.text((self.screen_w/2 - fnt_len/2, self.screen_h/2 ), self.splash_text, fill=(255, 0, 0), font=fnt)
# Save to file
# img.save('test.png')
# Use in-memory
@ -212,13 +255,19 @@ class KISStopmo(tk.Tk):
if len(sessions_list):
sessions_list.sort()
last_letter = sessions_list[-1]
# By default, find next letter for a new session
next_letter = self.find_letter_after(last_letter)
if next_letter is False:
return False
# A previous session folder was found; ask the user if they wish to resume session
resume_session = tk.messagebox.askyesno(_("Resume session?"), _("A previous session was found in {}, resume shooting ?").format(os.path.join(project_folder, last_letter)))
if resume_session:
next_letter = last_letter
else:
next_letter = 'A'
if os.path.exists(os.path.join(project_folder, next_letter)) is False:
os.mkdir(os.path.join(project_folder, next_letter))
print(_("Using {} as session folder.").format(os.path.join(project_folder, next_letter)))
return os.path.join(project_folder, next_letter)
return False
@ -234,9 +283,9 @@ class KISStopmo(tk.Tk):
cam_file = self.camera.file_get(
event_data.folder, event_data.name, gp.GP_FILE_TYPE_NORMAL)
# ~ next_filename = prefix+next(filename)+ext
next_filename = self.return_next_frame_number(self.get_last_frame(os.getcwd()))
next_filename = self.return_next_frame_number(self.get_last_frame(self.savepath))
target_path = os.path.join(os.getcwd(), next_filename)
print("Image is being saved to {}".format(target_path))
print(_("Image is being saved to {}").format(target_path))
cam_file.save(target_path)
# ~ return 0
@ -252,6 +301,7 @@ class KISStopmo(tk.Tk):
existing_animation_files = self.img_list
# ~ existing_animation_files =
file_list = os.listdir(folder)
print(file_list)
for file in file_list:
if (file.startswith(self.project_letter) and file.endswith(project_settings['file_extension'])):
if file not in existing_animation_files:
@ -291,7 +341,7 @@ class KISStopmo(tk.Tk):
# ~ os.rename(os.path.realpath(frame_list[i]), os.path.realpath("{}{}{}".format(project_settings['project_letter'], next(counter), project_settings['file_extension'])))
os.rename(os.path.realpath(i), os.path.realpath("{}{}{}".format(self.project_letter, next(counter), project_settings['file_extension'])))
else:
print(str(i) + " does not exist")
print(_("{} does not exist").format(str(i)))
return self.get_frames_list(folder)
@ -304,7 +354,7 @@ class KISStopmo(tk.Tk):
self.img_list[list(self.img_list.keys())[i]] = None
# FIXME
# FIXME: Does this still work ?
def remove_frame(self, event):
if len(list(self.img_list.items())):
@ -316,7 +366,7 @@ class KISStopmo(tk.Tk):
return 0
# ~ print(self.img_list)
print("removing {}".format(frame_path))
print(_("Removing {}").format(frame_path))
# trash file
send2trash(frame_path)
# remove entry from dict
@ -337,15 +387,18 @@ class KISStopmo(tk.Tk):
self.update_image(None, self.img_index)
# ~ def open_jpg(self, filepath:str, w:int, h:int):
def open_jpg(self, filetuple:tuple, w:int, h:int):
def open_jpg(self, filetuple:tuple, w:int, h:int, vflip:bool=False, hflip:bool=False):
# If pic not cached
if filetuple[-1] is None:
try:
# ~ image = Image.open(filepath)
# ~ print( filetuple[0] + "is not in cache")
image = Image.open(os.path.join(self.savepath, filetuple[0]))
# TODO : Keep aspect ratio
image = image.resize((w, h))
if vflip:
image = image.transpose(Image.Transpose.FLIP_TOP_BOTTOM)
if hflip:
image = image.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
self.img_list[filetuple[0]] = image
except FileNotFoundError:
return False
@ -381,7 +434,7 @@ class KISStopmo(tk.Tk):
# ~ prev_image = prev_image.resize((self.screen_w, self.screen_h))
prev_image = list(self.img_list.items())[self.img_index-1]
if prev_image[-1] is None:
prev_image = self.open_jpg(prev_image, self.screen_w, self.screen_h)
prev_image = self.open_jpg(prev_image, project_settings['screen_w'], project_settings['screen_h'], project_settings['vflip'], project_settings['hflip'])
else:
prev_image = prev_image[-1]
if fx:
@ -402,7 +455,7 @@ class KISStopmo(tk.Tk):
if list(self.img_list.items())[index][0] == "{}.{:04d}.{}".format(self.project_letter, -1, project_settings['file_extension']):
new_image = self.splashscreen
else:
new_image = self.open_jpg(list(self.img_list.items())[index], self.screen_w, self.screen_h)
new_image = self.open_jpg(list(self.img_list.items())[index], project_settings['screen_w'], project_settings['screen_h'], project_settings['vflip'], project_settings['hflip'])
else:
new_image = self.splashscreen
# ~ return False
@ -412,7 +465,7 @@ class KISStopmo(tk.Tk):
photo = ImageTk.PhotoImage(new_image)
if self.onion_skin:
if index:
photo = self.apply_onionskin(new_image, self.onionskin_alpha, self.onionskin_fx)
photo = self.apply_onionskin(new_image, project_settings['onionskin_alpha'], project_settings['onionskin_fx'])
# ~ print(photo)
self.label.configure(image=photo)
self.label.image = photo
@ -489,14 +542,14 @@ class KISStopmo(tk.Tk):
def apply_camera_settings(self, camera, config):
# iterate over the settings dictionary
for setting in self.camera_settings:
for setting in camera_settings:
# find the capture mode config item
# ~ cur_setting = gp.check_result(gp.gp_widget_get_child_by_name(config, setting))
# ~ # find corresponding choice
# ~ cur_setting_choice = gp.check_result(gp.gp_widget_get_choice(cur_setting, camera_settings[setting]))
# ~ # set config value
# ~ gp.check_result(gp.gp_widget_set_value(cur_setting, cur_setting_choice))
self.set_config_value(config, setting, self.camera_settings[setting])
self.set_config_value(config, setting, camera_settings[setting])
# validate config
gp.check_result(gp.gp_camera_set_config(self.camera, config))
@ -512,11 +565,11 @@ class KISStopmo(tk.Tk):
def check_status(self, camera, config):
for value in self.camera_status:
for value in camera_status:
# ~ cur_check = gp.check_result(gp.gp_widget_get_child_by_name(config, value))
# ~ cur_check_choice = gp.check_result(gp.gp_widget_get_choice(cur_check, camera_status[value]))
# ~ cur_check_value = gp.check_result(gp.gp_widget_get_value(cur_check))
cur_check_value, cur_check_choice = self.check_status_value(config, value, self.camera_status)
cur_check_value, cur_check_choice = self.check_status_value(config, value, camera_status)
if cur_check_value == cur_check_choice:
return True
else:
@ -535,7 +588,7 @@ class KISStopmo(tk.Tk):
speed = 30
if current_lightmeter_value < 0: # or current_lightmeter_value > 7:
# ~ while current_lightmeter_value < -7:
print("speed too high")
print(_("speed too high"))
while current_lightmeter_value < -7:
self.set_config_value(config, 'shutterspeed2', speed) # 0 to 46
gp.check_result(gp.gp_camera_set_config(camera, config))
@ -548,7 +601,7 @@ class KISStopmo(tk.Tk):
print(speed)
print(str(current_lightmeter_value) + " - " + str(current_shutterspeed_value))
if current_lightmeter_value > 0:
print("speed too low")
print(_("Speed too low."))
while current_lightmeter_value > 7:
self.set_config_value(config, 'shutterspeed2', speed) # 0 to 46
gp.check_result(gp.gp_camera_set_config(camera, config))
@ -576,9 +629,9 @@ class KISStopmo(tk.Tk):
# ~ self.camera.trigger_capture()
new_frame = self.camera.file_get(
new_frame_path.folder, new_frame_path.name, gp.GP_FILE_TYPE_NORMAL)
print("saving " + new_frame_path.folder + new_frame_path.name )
print(_("Saving {}{}").format(new_frame_path.folder, new_frame_path.name))
else:
print("getting file {}".format(event_data.name))
print(_("Getting file {}").format(event_data.name))
new_frame = self.camera.file_get(
event_data.folder, event_data.name, gp.GP_FILE_TYPE_NORMAL)
new_frame.save(target_path)
@ -593,9 +646,8 @@ class KISStopmo(tk.Tk):
def wait_for_capture(self):
if self.end_thread == True:
print("ending thread")
print(_("Ending thread"))
return
# ~ print("waiting for capture")
event_type, event_data = self.camera.wait_for_event(self.timeout)
if event_type == gp.GP_EVENT_FILE_ADDED:
# ~ print("file added")
@ -623,16 +675,17 @@ class KISStopmo(tk.Tk):
def trigger_export_animation(self, event):
output_folder = filedialog.askdirectory()
output_folder = "{folder}{sep}{filename}".format(folder=output_folder, sep=os.sep, filename=self.output_filename)
print("exporting to " + output_folder)
print(_("Exporting to {}").format(output_folder))
self.export_task = asyncio.run(self.export_animation())
# check with self.export_task.done() == True ? https://stackoverflow.com/questions/69350645/proper-way-to-retrieve-the-result-of-tasks-in-asyncio
def end_bg_loop(KISStopmo):
KISStopmo.camera.exit()
KISStopmo.end_thread = True
root = tk.Tk()
toot = KISStopmo(root)
# ~ root.protocol('WM_DELETE_WINDOW', end_bg_loop(toot))
root.protocol('WM_DELETE_WINDOW', end_bg_loop(toot))
root.mainloop()

4
requirements.txt Normal file
View File

@ -0,0 +1,4 @@
gphoto2
pillow
python-ffmpeg
Send2Trash