Add config file, l10n, session resuming
This commit is contained in:
parent
4d24f104d9
commit
a19f713d16
25
config.toml
Normal file
25
config.toml
Normal 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
|
BIN
locales/en/LC_MESSAGES/template.mo
Normal file
BIN
locales/en/LC_MESSAGES/template.mo
Normal file
Binary file not shown.
BIN
locales/fr/LC_MESSAGES/template.mo
Normal file
BIN
locales/fr/LC_MESSAGES/template.mo
Normal file
Binary file not shown.
199
main_c.py
199
main_c.py
@ -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
|
||||
|
||||
@ -152,14 +178,31 @@ 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
4
requirements.txt
Normal file
@ -0,0 +1,4 @@
|
||||
gphoto2
|
||||
pillow
|
||||
python-ffmpeg
|
||||
Send2Trash
|
Loading…
Reference in New Issue
Block a user