stopi/main_c.py

747 lines
32 KiB
Python
Raw Normal View History

2024-02-16 18:24:12 +01:00
#!/usr/bin/env python3
#
# main_c.py
#
# v0.1 - 2024 schnappy <contact@schnappy.xyz>
#
# The Tuffy font is public domain and was created by
# Thatcher Ulrich <tu@tulrich.com> http://tulrich.com
# Karoly Barta bartakarcsi@gmail.com
# Michael Evans http://www.evertype.com
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# A full copy of the GNU General Public License is available from
# https://www.gnu.org/licenses/gpl-3.0.en.html
2024-02-16 18:24:12 +01:00
#
import collections
import gettext
# DLSR support
import gphoto2 as gp
2024-02-23 12:02:24 +01:00
from io import BytesIO
from itertools import count
2024-02-16 18:24:12 +01:00
import locale
import os
2024-02-26 18:32:18 +01:00
from PIL import Image, ImageTk, ImageFilter, ImageDraw, ImageOps, ImageFont
2024-02-16 18:24:12 +01:00
import sys
import threading
import time
import tkinter as tk
from tkinter import filedialog, messagebox
import tomllib
2024-02-16 18:24:12 +01:00
# async FFMPEG exportation support
import asyncio
from ffmpeg.asyncio import FFmpeg
from send2trash import send2trash
# TODO : Todo List
2024-02-16 18:24:12 +01:00
#
# X wait event mode
# X remove frames
# X keep images in memory (odict > list ?)
2024-02-20 09:55:14 +01:00
# X resize images upon shooting/crop output video ?
# X project workflow
2024-02-16 18:24:12 +01:00
# X use a different folder for each project (A, B, C, etc...)
# X startup : check for existing folder with name A, B, C, etc.
# 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
2024-02-16 18:24:12 +01:00
# X colored onion skin frame (pillow filters)
# X startup frame
# X Import config values from config file
# X Translation
2024-02-20 09:55:14 +01:00
# X Allow opening and exporting without a camera connected
# o Better settings names
2024-02-20 09:55:14 +01:00
# o webcam support (pygame, win and linux only)
2024-03-01 14:34:28 +01:00
# X picam support (picamera2)
2024-02-20 09:55:14 +01:00
# o notify export ending
2024-03-01 14:34:28 +01:00
# o Use try/except for picam and pygame lib import
2024-04-09 09:11:32 +02:00
# o picam liveview
2024-02-16 18:24:12 +01:00
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']
# l10n
LOCALE = os.getenv('LANG', 'en_EN')
_ = gettext.translation('template', localedir='locales', languages=[LOCALE]).gettext
# Config
# defaults
project_settings_defaults = {
2024-02-26 18:32:18 +01:00
# DSLR = 0, picam = 1, webcam = 2
'camera_type': 0,
2024-02-16 18:24:12 +01:00
'file_extension':'JPG',
'trigger_mode': 'key',
2024-02-16 18:24:12 +01:00
'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',
2024-03-04 16:21:50 +01:00
'cache_images' : False,
2024-02-16 18:24:12 +01:00
}
# 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 'CHECK' in project_settings:
2024-02-26 18:32:18 +01:00
camera_status = project_settings['CHECK']
2024-02-23 18:02:48 +01:00
if 'CAMERA' in project_settings:
camera_settings = project_settings['CAMERA']
if 'DEFAULT' in project_settings:
project_settings = project_settings['DEFAULT']
config_found_msg = _("Found configuration file in {}").format(os.path.expanduser(location))
print(config_found_msg)
2024-02-16 18:24:12 +01:00
class KISStopmo(tk.Tk):
def __init__(self, *args, **kargs):
self.check_config()
if project_settings['camera_type'] != 0:
2024-02-26 18:32:18 +01:00
from picamera2 import Picamera2
2024-02-16 18:24:12 +01:00
# Default config
# Set script settings according to config file
self.onion_skin = project_settings['onion_skin_onstartup']
2024-02-20 09:55:14 +01:00
self.onionskin_alpha_default = 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']
2024-03-04 16:21:50 +01:00
self.playback = False
2024-02-16 18:24:12 +01:00
# ~ self.photo = None
2024-02-16 18:24:12 +01:00
self.end_thread = False
# Window setup
self.screen_w, self.screen_h = root.winfo_screenwidth(), root.winfo_screenheight()
root.wm_attributes("-fullscreen", self.fullscreen_bool)
root.title('KISStopmotion')
root.iconbitmap('@' + os.path.join(os.getcwd(), 'kisstopmo.xbm'))
# Image container creation
self.label = tk.Label(root)
self.label.pack()
# Savepath setup
self.projects_folder = tk.filedialog.askdirectory()
2024-02-16 18:24:12 +01:00
self.savepath = self.get_session_folder()
if len(self.savepath):
self.project_letter = self.savepath.split(os.sep)[-1]
else:
self.project_letter = 'A'
# Export settings
# ~ self.input_filename = "{folder}{sep}{letter}.%04d.{ext}".format(folder=self.savepath, sep=os.sep, letter=project_settings['project_letter'], ext=project_settings["file_extension"])
self.input_filename = "{folder}{sep}{letter}.%04d.{ext}".format(folder=self.savepath, sep=os.sep, letter=self.project_letter, ext=project_settings["file_extension"])
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 = self.parse_export_options(project_settings['export_options'], project_settings['vflip'], project_settings['hflip'] )
2024-02-16 18:24:12 +01:00
# Get frames list
self.img_list = {}
self.img_list = self.get_frames_list(self.savepath)
self.img_index = self.check_range(len(self.img_list)-1, False)
self.splash_text = _("No images yet! Start shooting...")
2024-02-16 18:24:12 +01:00
# Camera setup
2024-02-26 18:32:18 +01:00
if project_settings['camera_type'] != 0:
try:
self.camera = Picamera2()
self.picam_conf_full = self.camera.create_still_configuration(main={"size":(1920,1080)}, lores={"size":(800,600)})
self.camera.configure(self.picam_conf_full)
# Autofocus, get lens position and switch to manual mode
# Set Af mode to Manual (1). Default is Continuous (2), Auto is 1
# TODO: lock exposure, wb
self.camera.set_controls({'AfMode':1})
self.camera.start(show_preview=False)
self.camera.autofocus_cycle()
self.camera_lenspos = self.camera.capture_metadata()['LensPosition']
self.camera.set_controls({'AfMode':0, 'AwbEnable': False, 'AeEnable': False})
self.camera.stop()
except:
self.camera = False
self.splash_text += _("\nCamera not found or busy.")
else:
self.camera = gp.check_result(gp.gp_camera_new())
try:
gp.check_result(gp.gp_camera_init(self.camera))
# get configuration tree
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!"))
except:
self.camera = False
self.splash_text += _("\nCamera not found or busy.")
2024-02-16 18:24:12 +01:00
self.timeout = 3000 # milliseconds
self.splashscreen = self.generate_splashscreen()
2024-03-04 16:21:50 +01:00
self.image = None
self.update_image()
if self.image:
2024-02-16 18:24:12 +01:00
if self.onion_skin:
if self.img_index:
2024-03-04 16:21:50 +01:00
photo = self.apply_onionskin(self.image, self.onionskin_alpha_default, self.onionskin_fx)
2024-02-16 18:24:12 +01:00
else:
2024-03-04 16:21:50 +01:00
photo = ImageTk.PhotoImage(self.image)
2024-02-16 18:24:12 +01:00
self.label.configure(image=photo)
self.label.image = photo
2024-02-16 18:24:12 +01:00
if project_settings['trigger_mode'] == 'event':
root.after(1000, self.trigger_bg_loop)
# Key binding
root.bind("<Escape>", lambda event: root.attributes("-fullscreen", False))
root.bind("<f>", lambda event: root.attributes("-fullscreen", True))
2024-03-01 14:34:28 +01:00
root.bind("<e>", self.next_frame)
root.bind("<j>", self.previous_frame)
root.bind("<J>", self.toggle_onionskin)
root.bind("<N>", self.preview_animation)
2024-03-01 14:34:28 +01:00
root.bind("<E>", self.trigger_export_animation)
2024-02-16 18:24:12 +01:00
root.bind("<d>", self.remove_frame)
2024-03-04 16:21:50 +01:00
root.bind("<a>", self.print_imglist)
2024-02-16 18:24:12 +01:00
if project_settings['trigger_mode'] != 'event':
2024-03-01 14:34:28 +01:00
root.bind("<n>", self.capture_image)
2024-02-26 18:32:18 +01:00
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
2024-02-16 18:24:12 +01:00
def generate_splashscreen(self):
splash = Image.new('RGB', (self.screen_w, self.screen_h), (200,200,200))
splash_draw = ImageDraw.Draw(splash)
2024-02-16 18:24:12 +01:00
if self.splash_text is not None:
font = ImageFont.truetype("Tuffy_Bold.ttf", 60)
2024-02-23 12:02:24 +01:00
font_len = font.getlength(self.splash_text.split('\n')[0])
splash_draw.multiline_text((self.screen_w/2 - font_len/2, self.screen_h/2 ), self.splash_text, fill=(255, 255, 255), font=font, align='center', spacing=20)
2024-02-16 18:24:12 +01:00
# Use in-memory
splash_bytes = BytesIO()
splash.save(splash_bytes, 'png')
return splash
2024-02-16 18:24:12 +01:00
def find_letter_after(self, letter:str):
if letter in alphabet and alphabet.index(letter) < len(alphabet) - 1:
return alphabet[alphabet.index(letter) + 1]
else:
return False
def get_projects_folder(self):
if len(self.projects_folder):
project_folder = self.projects_folder
else:
# Get user folder
project_folder = os.path.expanduser('~')
# If a project folder is defined in settings, use it
if project_settings['projects_folder'] != '':
subfolder = project_settings['projects_folder']
else:
# If it doesn't exist, use a default name
subfolder = 'Stopmotion Projects'
project_folder = os.path.join(project_folder, subfolder)
# Create folder if it doesn't exist
if os.path.exists(project_folder) == False:
os.mkdir(project_folder)
else:
if not os.path.isdir(project_folder):
# If file exists but is not a folder, can't create it, abort
return False
return project_folder
def get_session_folder(self):
project_folder = self.get_projects_folder()
if project_folder:
sessions_list = []
dir_list = os.listdir(project_folder)
# Filter folders with name only one char long
for dir in dir_list:
if len(dir) == 1 and dir in alphabet:
sessions_list.append(dir)
# If folders exist, find last folder in alphabetical order
if len(sessions_list):
sessions_list.sort()
last_letter = sessions_list[-1]
# By default, find next letter for a new session
2024-02-16 18:24:12 +01:00
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
2024-02-16 18:24:12 +01:00
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)))
2024-02-16 18:24:12 +01:00
return os.path.join(project_folder, next_letter)
return False
def trigger_bg_loop(self):
self.event_thread = threading.Thread(target=self.wait_for_capture)
self.event_thread.start()
def wait_for_event(self):
# ~ while True:
event_type, event_data = self.camera.wait_for_event(self.timeout)
if event_type == gp.GP_EVENT_FILE_ADDED:
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(self.savepath))
2024-02-16 18:24:12 +01:00
target_path = os.path.join(os.getcwd(), next_filename)
print(_("Image is being saved to {}").format(target_path))
2024-02-16 18:24:12 +01:00
cam_file.save(target_path)
# ~ return 0
def print_imglist(self, event):
print(self.img_list)
def get_frames_list(self, folder:str):
# Get JPG files list in current directory
# ~ existing_animation_files = []
# ~ if not len(self.img_list):
existing_animation_files = self.img_list
# ~ existing_animation_files =
file_list = os.listdir(folder)
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:
# ~ existing_animation_files.append(file)
existing_animation_files[file] = None
if len(existing_animation_files) == 0:
# If no images were found, return fake name set to -001 to init file count to 000
# ~ return ["{}.{:04d}.{}".format(project_settings['project_letter'], -1, project_settings['file_extension'])]
return {"{}.{:04d}.{}".format(self.project_letter, -1, project_settings['file_extension']):None}
# ~ else:
# Remove fake file name as soon as we have real pics
# ~ if 'A.-001.JPG' in existing_animation_files:
# ~ existing_animation_files.pop('A.-001.JPG')
# ~ existing_animation_files.sort()
existing_animation_files = collections.OrderedDict(sorted(existing_animation_files.items()))
return existing_animation_files
def clean_img_list(self, folder_path):
# Check file in dict exists, else remove it
file_list = os.listdir(folder_path)
# Iterate over copy of dict to avoid OOR error
img_list_copy = dict(self.img_list)
for file in img_list_copy:
if file not in file_list:
self.img_list.pop(file)
2024-02-20 09:55:14 +01:00
def batch_rename(self, folder:str):
2024-02-16 18:24:12 +01:00
# initialize counter to 0
frame_list = self.get_frames_list(folder)
counter = (".%04i." % x for x in count(0))
# ~ for i in range(len(frame_list)):
for i in frame_list.keys():
# ~ if os.path.exists(os.path.realpath(frame_list[i])):
2024-02-20 09:55:14 +01:00
if os.path.exists(os.path.join(folder, i)):
2024-02-16 18:24:12 +01:00
# ~ os.rename(os.path.realpath(frame_list[i]), os.path.realpath("{}{}{}".format(project_settings['project_letter'], next(counter), project_settings['file_extension'])))
2024-02-20 09:55:14 +01:00
os.rename(os.path.join(folder, i), os.path.join(folder, "{}{}{}".format(self.project_letter, next(counter), project_settings['file_extension'])))
# ~ print(os.path.join(folder, "{}{}{}".format(self.project_letter, next(counter), project_settings['file_extension'])))
2024-02-16 18:24:12 +01:00
else:
print(_("{} does not exist").format(str(i)))
2024-02-16 18:24:12 +01:00
return self.get_frames_list(folder)
def offset_dictvalues(self, from_index=0):
dict_copy = dict(self.img_list)
for i in range(from_index, len(dict_copy)):
if i < len(self.img_list)-1:
self.img_list[list(self.img_list.keys())[i]] = list(self.img_list.values())[i+1]
else:
self.img_list[list(self.img_list.keys())[i]] = None
def remove_frame(self, event):
if len(list(self.img_list.items())):
folder_path = os.path.realpath(self.savepath)
frame_name = list(self.img_list.items())[self.img_index][0]
2024-02-20 09:55:14 +01:00
# ~ frame_path = os.path.realpath(frame_name)
frame_path = os.path.join(folder_path, frame_name)
2024-02-16 18:24:12 +01:00
if not os.path.exists(frame_path):
return 0
print(_("Removing {}").format(frame_path))
2024-02-16 18:24:12 +01:00
# trash file
send2trash(frame_path)
# remove entry from dict
self.img_list.pop(frame_name)
# offset cached images
self.offset_dictvalues(self.img_index)
# rename files and get new list
self.img_list = self.batch_rename(folder_path)
self.clean_img_list(folder_path)
# update index if possible
self.img_index = self.check_range(self.img_index, False)
# update display
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, vflip:bool=False, hflip:bool=False):
2024-02-16 18:24:12 +01:00
# If pic not cached
if filetuple[-1] is None:
try:
image = Image.open(os.path.join(self.savepath, filetuple[0]))
2024-02-26 18:32:18 +01:00
image = ImageOps.fit(image, (w, h))
if vflip:
image = image.transpose(Image.Transpose.FLIP_TOP_BOTTOM)
if hflip:
image = image.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
2024-03-04 16:21:50 +01:00
if project_settings['cache_images']:
# TODO : Do not cache image to preserve memory
self.img_list[filetuple[0]] = image
2024-02-16 18:24:12 +01:00
except FileNotFoundError:
return False
else:
2024-03-04 16:21:50 +01:00
if project_settings['cache_images']:
image = filetuple[-1]
2024-02-16 18:24:12 +01:00
return image
def check_range(self, x, loop=True):
if x < 0:
if loop:
return len(self.img_list)-1
else:
return 0
elif x > len(self.img_list)-1:
if loop:
return 0
else:
return len(self.img_list)-1
else:
return x
def apply_onionskin(self, image, alpha, fx=False):
# ~ prev_image = Image.open(self.img_list[self.img_index-1])
# ~ 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, project_settings['screen_w'], project_settings['screen_h'], project_settings['vflip'], project_settings['hflip'])
2024-02-16 18:24:12 +01:00
else:
prev_image = prev_image[-1]
if fx:
prev_image = prev_image.filter(ImageFilter.FIND_EDGES)
composite = Image.blend(prev_image, image, alpha)
photo = ImageTk.PhotoImage(composite)
return photo
def update_image(self, event=None, index=None):
2024-03-04 16:21:50 +01:00
# TODO : check event mode still works
2024-02-16 18:24:12 +01:00
if event is not None:
self.img_index = self.check_range(self.img_index+1)
if index is None:
# ~ index = self.img_index
index = self.check_range(self.img_index)
if len(list(self.img_list.items())):
# TODO: better approach
2024-02-20 09:55:14 +01:00
# If name == X.-001.JPG, we don't have any frame yet, so display splashscreen
2024-02-16 18:24:12 +01:00
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], project_settings['screen_w'], project_settings['screen_h'], project_settings['vflip'], project_settings['hflip'])
2024-02-16 18:24:12 +01:00
else:
new_image = self.splashscreen
if new_image:
photo = ImageTk.PhotoImage(new_image)
if self.onion_skin:
if index:
2024-02-20 09:55:14 +01:00
photo = self.apply_onionskin(new_image, project_settings['onionskin_alpha_default'], project_settings['onionskin_fx'])
2024-02-16 18:24:12 +01:00
self.label.configure(image=photo)
self.label.image = photo
2024-03-04 16:21:50 +01:00
# ~ return new_image
self.image = new_image
# ~ new_image.close()
return True
2024-02-16 18:24:12 +01:00
else:
return False
def next_frame(self, event):
self.img_index = self.check_range(self.img_index+1)
self.update_image(None, self.img_index)
def previous_frame(self, event):
self.img_index = self.check_range(self.img_index-1)
self.update_image(None, self.img_index)
2024-04-09 09:11:32 +02:00
def playback_animation(self):
while self.img_index < len(self.img_list)-1:
self.img_index += 1
root.after(62, self.update_image(None, self.img_index))
root.update_idletasks()
2024-02-16 18:24:12 +01:00
def preview_animation(self, event):
# save OS state
if self.onion_skin:
self.onion_skin = False
onion_skin_was_on = True
# playback
2024-03-04 16:21:50 +01:00
# TODO : Use async function for playback
# ~ self.playback = not self.playback
2024-04-09 09:11:32 +02:00
# ~ self.img_index = 0
# ~ self.playback_animation()
2024-02-16 18:24:12 +01:00
for img in self.img_list:
# ~ self.update_image(None, self.img_list.index(img))
2024-03-04 16:21:50 +01:00
# ~ if self.playback:
2024-02-16 18:24:12 +01:00
self.update_image(None, list(self.img_list.keys()).index(img))
root.update_idletasks()
time.sleep(1/self.framerate)
2024-03-04 16:21:50 +01:00
# ~ else:
# ~ break
2024-02-16 18:24:12 +01:00
# ~ self.update_image(None, self.img_index)
self.display_last_frame()
# restore OS state
if 'onion_skin_was_on' in locals():
self.toggle_onionskin()
return 0
def toggle_onionskin(self, event=None):
self.onion_skin = not self.onion_skin
self.update_image()
def get_last_frame(self, folder:str):
# Refresh file list
existing_animation_files = self.get_frames_list(folder)
# Get last file
# Filename pattern is A.0001.JPG
# ~ return existing_animation_files[-1].split('.')
print(next(reversed(existing_animation_files.keys())))
return next(reversed(existing_animation_files.keys())).split('.')
def display_last_frame(self):
self.img_list = self.get_frames_list(self.savepath)
self.img_index = len(self.img_list)-1
self.update_image(None, self.img_index)
def return_next_frame_number(self, last_frame_name):
prefix, filecount, ext = last_frame_name
filename = '.{:04d}.'.format(int(filecount)+1)
# ~ filename = (".%04i." % x for x in count(int(filecount) + 1))
# ~ return prefix + next(filename) + ext
return prefix + filename + ext
def set_config_value(self, config, setting, new_value):
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, new_value))
# set config value
gp.check_result(gp.gp_widget_set_value(cur_setting, cur_setting_choice))
def apply_camera_settings(self, camera, config):
# iterate over the settings dictionary
for setting in camera_settings:
2024-02-16 18:24:12 +01:00
# 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, camera_settings[setting])
2024-02-16 18:24:12 +01:00
# validate config
gp.check_result(gp.gp_camera_set_config(self.camera, config))
def check_status_value(self, config, value, optimal_value=None):
cur_check = gp.check_result(gp.gp_widget_get_child_by_name(config, value))
cur_check_value = gp.check_result(gp.gp_widget_get_value(cur_check))
if optimal_value is not None:
cur_check_choice = gp.check_result(gp.gp_widget_get_choice(cur_check, optimal_value[value]))
return [cur_check_value, cur_check_choice]
else:
return cur_check_value
def check_status(self, camera, config):
for value in camera_status:
2024-02-16 18:24:12 +01:00
# ~ 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, camera_status)
2024-02-16 18:24:12 +01:00
if cur_check_value == cur_check_choice:
return True
else:
# Some values are not optimal
return False
# ~ def find_shutterspeed(camera, config):
def find_shutterspeed(self, camera):
# get exposure using /main/status/lightmeter > should be 0
config = gp.check_result(gp.gp_camera_get_config(camera))
current_lightmeter_value = self.check_status_value(config, 'lightmeter')
current_shutterspeed_value = self.check_status_value(config, 'shutterspeed2')
# ~ previous_shutterspeed_value = -1
print(str(current_lightmeter_value) + " - " + str(current_shutterspeed_value))
speed = 30
if current_lightmeter_value < 0: # or current_lightmeter_value > 7:
# ~ while current_lightmeter_value < -7:
print(_("speed too high"))
2024-02-16 18:24:12 +01:00
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))
time.sleep(1)
config = gp.check_result(gp.gp_camera_get_config(camera))
# ~ previous_shutterspeed_value = current_shutterspeed_value
current_lightmeter_value = self.check_status_value(config, 'lightmeter')
current_shutterspeed_value = self.check_status_value(config, 'shutterspeed2')
speed += 1
print(speed)
print(str(current_lightmeter_value) + " - " + str(current_shutterspeed_value))
if current_lightmeter_value > 0:
print(_("Speed too low."))
2024-02-16 18:24:12 +01:00
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))
time.sleep(1)
config = gp.check_result(gp.gp_camera_get_config(self.camera))
# ~ previous_shutterspeed_value = current_shutterspeed_value
current_lightmeter_value = self.check_status_value(config, 'lightmeter')
current_shutterspeed_value = self.check_status_value(config, 'shutterspeed2')
speed -= 1
print(speed)
print(str(current_lightmeter_value) + " - " + str(current_shutterspeed_value))
return True
def capture_image(self, event=None, event_data=None):
# get net file name based on prefix, file count and extension
next_filename = self.return_next_frame_number(self.get_last_frame(self.savepath))
# build full path to file
target_path = os.path.join(self.savepath, next_filename)
if project_settings['camera_type'] != 0:
2024-02-26 18:32:18 +01:00
self.camera.start(show_preview=False)
self.camera.set_controls({"LensPosition": self.camera_lenspos})
self.camera.capture_file(target_path, 'main', format='jpeg')
self.camera.stop()
2024-02-16 18:24:12 +01:00
else:
2024-02-26 18:32:18 +01:00
# Get file from DSLR camera
if event_data is None:
new_frame_path = self.camera.capture(gp.GP_CAPTURE_IMAGE)
# ~ 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 {}{}").format(new_frame_path.folder, new_frame_path.name))
else:
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)
2024-02-16 18:24:12 +01:00
# ~ next_filename = prefix+next(filename)+ext
if '{letter}.-001.JPG'.format(letter=self.project_letter) in self.img_list:
self.img_list.pop('{letter}.-001.JPG'.format(letter=self.project_letter))
# Display new frame
self.display_last_frame()
def wait_for_capture(self):
if self.end_thread == True:
print(_("Ending thread"))
2024-02-16 18:24:12 +01:00
return
event_type, event_data = self.camera.wait_for_event(self.timeout)
if event_type == gp.GP_EVENT_FILE_ADDED:
self.capture_image(None, event_data)
# ~ root.after(self.timeout, self.wait_for_capture)
self.wait_for_capture()
async def export_animation(self):
ffmpeg = (
FFmpeg()
# overwrite file
.option("y")
.input(
self.input_filename,
self.input_options)
.output(
# ~ self.output_filename,
self.export_filename,
2024-02-16 18:24:12 +01:00
self.output_options
)
)
await ffmpeg.execute()
def trigger_export_animation(self, event):
# ~ output_folder = filedialog.askdirectory()
2024-02-27 11:50:28 +01:00
self.export_filename = ()
self.export_filename = filedialog.asksaveasfilename(defaultextension='.mp4', filetypes=((_("Mp4 files"), '*.mp4'),))
2024-02-27 11:50:28 +01:00
if not self.export_filename:
# ~ self.export_filename = "{folder}{sep}{filename}".format(folder=os.getcwd(), sep=os.sep, filename=self.output_filename)
return False
print(_("Exporting to {}").format(self.export_filename))
2024-02-16 18:24:12 +01:00
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):
2024-02-26 18:32:18 +01:00
if KISStopmo.camera is not False:
if project_settings['camera_type'] != 0:
2024-02-26 18:32:18 +01:00
KISStopmo.camera.stop()
else:
KISStopmo.camera.exit()
2024-02-16 18:24:12 +01:00
KISStopmo.end_thread = True
root = tk.Tk()
toot = KISStopmo(root)
root.protocol('WM_DELETE_WINDOW', end_bg_loop(toot))
2024-02-16 18:24:12 +01:00
root.mainloop()