2024-02-16 18:24:12 +01:00
#!/usr/bin/env python3
2024-02-18 18:39:59 +01:00
#
# 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
2024-02-18 13:12:09 +01:00
import gettext
# DLSR support
import gphoto2 as gp
2024-02-23 12:02:24 +01:00
from io import BytesIO
2024-02-18 13:12:09 +01:00
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
2024-02-18 13:12:09 +01:00
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
2024-02-18 13:12:09 +01:00
# 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 ?
2024-02-18 13:12:09 +01:00
# 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.
2024-02-18 13:12:09 +01:00
# 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)
2024-02-18 13:12:09 +01:00
# 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
2024-02-18 13:12:09 +01:00
# o Better settings names
2024-02-20 09:55:14 +01:00
# o webcam support (pygame, win and linux only)
2024-02-23 12:02:24 +01:00
# o picam support (picamera2)
2024-02-20 09:55:14 +01:00
# o notify export ending
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 ' ]
2024-02-18 13:12:09 +01:00
# 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 ' ,
2024-02-18 13:12:09 +01:00
' trigger_mode ' : ' key ' ,
2024-02-16 18:24:12 +01:00
' projects_folder ' : ' ' ,
# ~ 'project_letter': 'A'
2024-02-18 13:12:09 +01:00
' 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-02-16 18:24:12 +01:00
}
2024-02-18 13:12:09 +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 ' ]
2024-02-18 13:12:09 +01:00
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 ) :
2024-02-18 13:12:09 +01:00
self . check_config ( )
2024-02-27 11:05:22 +01:00
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
2024-02-18 13:12:09 +01:00
# 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 ' ]
2024-02-18 13:12:09 +01:00
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-02-16 18:24:12 +01:00
2024-02-18 13:12:09 +01:00
# ~ for setting in camera_settings:
# ~ print(setting)
# ~ 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
2024-02-18 13:12:09 +01:00
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 )
2024-02-18 13:12:09 +01:00
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 )
2024-02-18 18:39:59 +01:00
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 + = _ ( " \n Camera 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 + = _ ( " \n Camera not found or busy. " )
2024-02-18 18:39:59 +01:00
2024-02-16 18:24:12 +01:00
self . timeout = 3000 # milliseconds
self . splashscreen = self . generate_splashscreen ( )
image = self . update_image ( )
if image :
if self . onion_skin :
if self . img_index :
2024-02-20 09:55:14 +01:00
photo = self . apply_onionskin ( image , self . onionskin_alpha_default , self . onionskin_fx )
2024-02-16 18:24:12 +01:00
else :
photo = ImageTk . PhotoImage ( image )
self . label . configure ( image = photo )
self . label . image = photo
2024-02-18 18:39:59 +01:00
2024-02-16 18:24:12 +01:00
if project_settings [ ' trigger_mode ' ] == ' event ' :
root . after ( 1000 , self . trigger_bg_loop )
# ~ root.after(1000, self.wait_for_capture)
# Key binding
root . bind ( " <Escape> " , lambda event : root . attributes ( " -fullscreen " , False ) )
root . bind ( " <f> " , lambda event : root . attributes ( " -fullscreen " , True ) )
root . bind ( " <n> " , self . next_frame )
root . bind ( " <b> " , self . previous_frame )
2024-02-27 11:05:22 +01:00
root . bind ( " <B> " , self . toggle_onionskin )
root . bind ( " <N> " , self . preview_animation )
2024-02-16 18:24:12 +01:00
root . bind ( " <e> " , self . trigger_export_animation )
root . bind ( " <d> " , self . remove_frame )
2024-02-27 11:05:22 +01:00
# ~ root.bind("<a>", self.print_imglist)
2024-02-16 18:24:12 +01:00
if project_settings [ ' trigger_mode ' ] != ' event ' :
root . bind ( " <j> " , self . capture_image )
2024-02-26 18:32:18 +01:00
2024-02-18 13:12:09 +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 ) :
2024-02-18 18:39:59 +01:00
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 :
2024-02-18 18:39:59 +01:00
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
2024-02-18 18:39:59 +01:00
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 ]
2024-02-18 13:12:09 +01:00
# 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
2024-02-18 13:12:09 +01:00
# 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 ) )
2024-02-18 13:12:09 +01:00
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
2024-02-18 13:12:09 +01:00
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 )
2024-02-18 13:12:09 +01:00
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 )
2024-02-18 18:39:59 +01:00
# ~ print(file_list)
2024-02-16 18:24:12 +01:00
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)):
2024-02-20 09:55:14 +01:00
# ~ print(frame_list)
2024-02-16 18:24:12 +01:00
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 :
2024-02-18 13:12:09 +01:00
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(self.img_list)
2024-02-18 13:12:09 +01:00
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 )
print ( self . img_list )
self . clean_img_list ( folder_path )
# ~ print(self.img_list)
# update index if possible
# ~ print(self.img_index)
self . img_index = self . check_range ( self . img_index , False )
# ~ print(self.img_index)
# update display
# ~ print(self.img_index)
self . update_image ( None , self . img_index )
# ~ def open_jpg(self, filepath:str, w:int, h:int):
2024-02-18 13:12:09 +01:00
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(filepath)
# ~ print( filetuple[0] + "is not in cache")
image = Image . open ( os . path . join ( self . savepath , filetuple [ 0 ] ) )
2024-02-26 18:32:18 +01:00
# ~ image = image.resize((w, h),reducing_gap=1.5)
image = ImageOps . fit ( image , ( w , h ) )
2024-02-18 13:12:09 +01:00
if vflip :
image = image . transpose ( Image . Transpose . FLIP_TOP_BOTTOM )
if hflip :
image = image . transpose ( Image . Transpose . FLIP_LEFT_RIGHT )
2024-02-16 18:24:12 +01:00
self . img_list [ filetuple [ 0 ] ] = image
except FileNotFoundError :
return False
else :
# ~ print( filetuple[0] + "is in cache")
image = filetuple [ - 1 ]
return image
# ~ def inc(self, x):
# ~ if x >= len(self.img_list)-1:
# ~ return 0
# ~ else:
# ~ return x + 1
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 :
2024-02-18 13:12:09 +01:00
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-02-26 18:32:18 +01:00
# TODO : check event mode stille 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 :
2024-02-18 13:12:09 +01:00
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
# ~ print(photo)
self . label . configure ( image = photo )
self . label . image = photo
return new_image
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 )
def preview_animation ( self , event ) :
# save OS state
if self . onion_skin :
self . onion_skin = False
onion_skin_was_on = True
# playback
for img in self . img_list :
# ~ self.update_image(None, self.img_list.index(img))
self . update_image ( None , list ( self . img_list . keys ( ) ) . index ( img ) )
root . update_idletasks ( )
time . sleep ( 1 / self . framerate )
# ~ 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
2024-02-18 13:12:09 +01:00
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))
2024-02-18 13:12:09 +01:00
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 ) :
2024-02-18 13:12:09 +01:00
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))
2024-02-18 13:12:09 +01:00
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:
2024-02-18 13:12:09 +01:00
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 :
2024-02-18 13:12:09 +01:00
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
# ~ print(str(current_lightmeter_value) + " - " + str(current_shutterspeed_value))
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 )
2024-02-27 11:05:22 +01:00
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 :
# ~ print("j pressed")
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
2024-02-18 18:39:59 +01:00
# ~ print(self.img_list)
2024-02-16 18:24:12 +01:00
if ' {letter} .-001.JPG ' . format ( letter = self . project_letter ) in self . img_list :
# ~ self.img_list.pop('A.-001.JPG')
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 :
2024-02-18 13:12:09 +01:00
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 :
# ~ print("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 (
2024-02-27 11:05:22 +01:00
# ~ 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 ) :
2024-02-27 11:05:22 +01:00
# ~ output_folder = filedialog.askdirectory()
2024-02-27 11:50:28 +01:00
# TODO : Check what happens when closing the window without entering a name
self . export_filename = ( )
2024-02-27 11:05:22 +01:00
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
2024-02-27 11:05:22 +01:00
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 :
2024-02-27 11:05:22 +01:00
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 )
2024-02-18 13:12:09 +01:00
root . protocol ( ' WM_DELETE_WINDOW ' , end_bg_loop ( toot ) )
2024-02-16 18:24:12 +01:00
root . mainloop ( )