This commit is contained in:
ABelliqueux 2024-09-06 19:53:58 +02:00
commit bb5a8ceb15
9 changed files with 773 additions and 0 deletions

30
config.toml Normal file
View File

@ -0,0 +1,30 @@
[DEFAULT]
# gphoto2 = 0, picam = 1, webcam = 2
camera_type = 2
file_extension = 'png'
trigger_mode = 'key'
projects_folder = 'testcv/'
onion_skin_onstartup = true
onionskin_alpha_default = 0.5
onionskin_fx = false
fullscreen_bool = true
screen_w = 1920
screen_h = 1080
framerate = 16
vflip = false
hflip = false
export_options = 'scale=1920:-1,crop=1920:1080:0:102'
cache_images = false
[CAMERA]
# Nikon D40x
# Add meter mode to center, focus mode to fixed selection
# /main/capturesettings/autofocusarea to 0
# /main/capturesettings/focusmetermode to 1
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

475
frame_opencv.py Normal file
View File

@ -0,0 +1,475 @@
import collections
import cv2
import gettext
from itertools import count
import os
from send2trash import send2trash
import signal
import sys
import time
import tomllib
import tkinter as tk
from tkinter import filedialog, messagebox
import numpy as np
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']
next_letter = 'A'
# l10n
LOCALE = os.getenv('LANG', 'en_EN')
_ = gettext.translation('template', localedir='locales', languages=[LOCALE]).gettext
# Config
# defaults
project_settings_defaults = {
# gphoto2 = 0, picam = 1, webcam = 2
'camera_type': 0,
'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' : False,
'hflip' : False,
'export_options' : 'scale=1920:-1,crop=1920:1080:0:102',
'cache_images' : False,
'liveview' : False,
}
# 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:
camera_status = project_settings['CHECK']
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)
# Create blank image with 0s
blank_image = np.zeros((project_settings['screen_h'], project_settings['screen_w'],3), np.uint8)
# Set all pixels to light grey
blank_image[:,0:] = 200
font = cv2.FONT_HERSHEY_SIMPLEX
cv2.putText(blank_image,_("Pas d'image"),(int(project_settings['screen_w']/3),int(project_settings['screen_h']/2)), font, 4, (100, 100, 100), 8, cv2.LINE_AA)
def find_letter_after(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():
if len(projects_folder):
project_folder = 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():
project_folder = 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
next_letter = 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
def get_frames_list(folder:str):
# Get JPG files list in current directory
# ~ existing_animation_files = []
# ~ if not len(img_list):
existing_animation_files = img_list
# ~ existing_animation_files =
file_list = os.listdir(folder)
for file in file_list:
if (file.startswith(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(next_letter, -1, project_settings['file_extension'])]
# ~ return {"{}.{:04d}.{}".format(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 get_last_frame(folder:str):
# Refresh file list
existing_animation_files = 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 get_onionskin_frame(folder:str, index=None):
prev_image = get_last_frame(folder)
prev_image = '.'.join(prev_image)
if os.path.exists( os.path.expanduser(os.path.join(savepath, prev_image))):
frm = cv2.imread(os.path.join(savepath, prev_image))
frm = cv2.resize(frm, (project_settings['screen_w'], project_settings['screen_h']))
# Imge does not exist, load blank image
else:
frm = blank_image
return frm
def return_next_frame_number(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 update_image(img_list, img_index):
if len(img_list) == 0:
return 0
img_filename = img_list[img_index]
if os.path.exists( os.path.expanduser(os.path.join(savepath, img_filename))):
img = cv2.imread(os.path.join(savepath, img_filename))
img = cv2.resize(img, (project_settings['screen_w'], project_settings['screen_h']))
else:
img = blank_image
return img
def next_frame(img_index, loop=True):
img_index = check_range(img_index+1, loop)
return img_index, update_image(img_list, img_index)
def previous_frame(img_index):
img_index = check_range(img_index-1)
return img_index, update_image(img_list, img_index)
def clean_img_list(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 = img_list
for file in img_list_copy:
if file not in file_list:
img_list.remove(file)
def check_range(x, loop=True):
if x < 0:
if loop:
return len(img_list)-1
else:
return 0
elif x >= len(img_list)-1:
if loop:
return 0
else:
return len(img_list)-1
else:
return x
def batch_rename(folder:str):
# initialize counter to 0
frame_list = get_frames_list(folder)
counter = (".%04i." % x for x in count(0))
# ~ for i in range(len(frame_list)):
for i in frame_list:
# ~ if os.path.exists(os.path.realpath(frame_list[i])):
if os.path.exists(os.path.join(folder, i)):
# ~ 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.join(folder, i), os.path.join(folder, "{}{}{}".format(project_letter, next(counter), project_settings['file_extension'])))
# ~ print(os.path.join(folder, "{}{}{}".format(project_letter, next(counter), project_settings['file_extension'])))
else:
print(_("{} does not exist").format(str(i)))
return get_frames_list(folder)
def offset_dictvalues(from_index=0):
dict_copy = dict(img_list)
for i in range(from_index, len(dict_copy)):
if i < len(img_list)-1:
img_list[list(img_list.keys())[i]] = list(img_list.values())[i+1]
else:
img_list[list(img_list.keys())[i]] = None
def remove_frame(img_list, img_index):
if len(img_list):
folder_path = os.path.realpath(savepath)
frame_name = img_list[img_index]
# ~ frame_path = os.path.realpath(frame_name)
frame_path = os.path.join(folder_path, frame_name)
if not os.path.exists(frame_path):
return img_index, blank_image
print(_("Removing {}").format(frame_path))
# trash file
send2trash(frame_path)
# remove entry from dict
img_list.remove(frame_name)
# offset cached images
# ~ offset_dictvalues(img_index)
# rename files and get new list
img_list = batch_rename(folder_path)
clean_img_list(folder_path)
# update index if possible
img_index = check_range(img_index, False)
# update display
return img_index, update_image(img_list, img_index)
else:
return 0, blank_image
def playback_animation(img_list, img_index):
# save onionskin state
if onionskin:
onionskin = False
onionskin_was_on = True
# ~ index = 0
# Play all frames
# ~ while index < len(img_list):
# ~ print(img_list[index])
# ~ img = update_image(img_list, index)
# ~ cv2.imshow("StopiCV", img)
# ~ time.sleep(.5)
# ~ index += 1
for file in img_list:
img = update_image(img_list, img_list.index(file))
print(str(img_list.index(file)) + " : " + file )
cv2.imshow("StopiCV", img)
time.sleep(.5)
# Restore previous frame
print(img_index)
frame_before_playback = update_image(img_list, img_index)
# ~ cv2.imshow("StopiCV", img)
# Restore onionskin
if 'onionskin_was_on' in locals():
onionskin = True
# Restore index
return img_index, frame_before_playback
def testDevice(source):
cap = cv2.VideoCapture(source)
if cap is None or not cap.isOpened():
print(_("Warning: unable to open video source: {}").format(source))
return False
cap.release()
return True
def signal_handler(sig, frame):
global ctrlc_pressed
ctrlc_pressed = True
def main(args):
global onionskin, playback, loop_playback, playhead, index
if not testDevice(0):
print(_("No camera device found. Exiting..."))
return 1
cam = cv2.VideoCapture(0)
cam.set(cv2.CAP_PROP_FRAME_WIDTH, 1280)
cam.set(cv2.CAP_PROP_FRAME_HEIGHT, 720)
cv2.namedWindow("StopiCV", cv2.WINDOW_GUI_NORMAL)
cv2.setWindowProperty("StopiCV", cv2.WND_PROP_OPENGL, cv2.WINDOW_OPENGL)
cv2.setWindowProperty("StopiCV", cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_FULLSCREEN)
cv2.setWindowProperty("StopiCV", cv2.WND_PROP_ASPECT_RATIO, cv2.WINDOW_KEEPRATIO)
frame = get_onionskin_frame(savepath, index)
while True:
if playback:
if onionskin:
onionskin = False
onionskin_was_on = True
# Play all frames
if playhead < len(img_list)-1:
playhead, img = next_frame(playhead, loop_playback)
cv2.imshow("StopiCV", img)
time.sleep(1.0/project_settings['framerate'])
else:
playhead = index
img = update_image(img_list, index)
playback = False
# Restore onionskin
if 'onionskin_was_on' in locals():
onionskin = True
loop_playback = False
if onionskin:
ret, overlay = cam.read()
og_frame = overlay
# Resize preview
overlay = cv2.resize(overlay, (project_settings['screen_w'], project_settings['screen_h']))
# ~ if overlay is not None:
overlay = cv2.addWeighted(frame, 1.0, overlay, project_settings['onionskin_alpha_default'], 0)
# Flip preview (0 = vert; 1 = hor)
if project_settings['vflip'] or project_settings['hflip']:
if project_settings['vflip'] and project_settings['hflip']:
flip_dir = -1
elif project_settings['vflip']:
flip_dir = 0
elif project_settings['hflip']:
flip_dir = 1
frame = cv2.flip(overlay, flip_dir)
if not ret:
print(_("Failed to grab frame."))
break
cv2.imshow("StopiCV", overlay)
if not playback and not onionskin:
cv2.imshow("StopiCV", frame)
k = cv2.waitKey(1)
# Key o
if (k%256 == 111) or (k%256 == 47):
# Toggle onionskin
onionskin = not onionskin
# Key left
elif (k%256 == 81) or (k%256 == 52):
# Displau previous frame
print("Left")
if len(img_list):
if playback:
playback = False
index, frame = previous_frame(index)
print("L: {}".format(index))
elif (k%256 == 83) or (k%256 == 54):
# Displau next frame
print("Right")
if len(img_list):
if playback:
playback = False
index, frame = next_frame(index)
print("R: {}".format(index))
# Key e / keypad *
elif (k%256 == 101) or (k%256 == 42):
# TODO : Export
print("Export")
# Key Return
elif (k%256 == 13):
# TODO : Playback
print("Playback")
playhead = index
loop_playback = True
playback = not playback
# Key remove frame - backspace, del, numpad_minus
elif (k%256 == 8) or (k%256 == 45) or (k == 255):
# Remove frame
print("Remove frame")
index, frame = remove_frame(img_list, index)
# Quit app
elif k%256 == 27:
# ESC pressed
print(_("Escape hit, exiting..."))
break
elif ctrlc_pressed:
print(_("Ctrl-C hit, exiting..."))
break
elif cv2.getWindowProperty("StopiCV", cv2.WND_PROP_VISIBLE) < 1:
print(_("Window was closed, exiting..."))
break
# Take pic
elif (k%256 == 32) or (k%256 == 48):
# SPACE pressed
img_name = return_next_frame_number(get_last_frame(savepath))
img_path = os.path.join(savepath, img_name)
cv2.imwrite(img_path, og_frame)
print(_("File {} written.").format(img_path))
if len(img_list) and (img_list[index] == 'A.-001.png'):
img_list[index] = img_name
else:
index += 1
frame = get_onionskin_frame(savepath, index)
# TODO : go back to last frame ?
# REMOVE : Debug print keycode
elif k==-1: # normally -1 returned,so don't print it
continue
else:
print(k) # else print its value
cam.release()
cv2.destroyAllWindows()
ctrlc_pressed = False
projects_folder = project_settings['projects_folder']
img_list = []
savepath = get_session_folder()
onionskin = project_settings['onion_skin_onstartup']
playback = False
playhead = 0
loop_playback = True
if len(savepath):
project_letter = savepath.split(os.sep)[-1]
else:
project_letter = 'A'
img_list = get_frames_list(savepath)
index = len(img_list)-1
print(index)
if __name__ == '__main__':
signal.signal(signal.SIGINT, signal_handler)
sys.exit(main(sys.argv[1:]))

Binary file not shown.

View File

@ -0,0 +1,79 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR ORGANIZATION
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2024-02-18 11:30+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: pygettext.py 1.5\n"
#: ../main_c.py:87
msgid "Found configuration file in {}"
msgstr ""
#: ../main_c.py:144
msgid "Warning: Some settings are not set to the recommended value!"
msgstr ""
#: ../main_c.py:146
msgid "Camera not found or busy."
msgstr ""
#: ../main_c.py:256
msgid "A previous session was found in {}, resume shooting ?"
msgstr ""
#: ../main_c.py:256
msgid "Resume session?"
msgstr ""
#: ../main_c.py:263
msgid "Using {} as session folder."
msgstr ""
#: ../main_c.py:281
msgid "Image is being saved to {}"
msgstr ""
#: ../main_c.py:336
msgid "{} does not exist"
msgstr ""
#: ../main_c.py:361
msgid "Removing {}"
msgstr ""
#: ../main_c.py:583
msgid "speed too high"
msgstr ""
#: ../main_c.py:596
msgid "Speed too low."
msgstr ""
#: ../main_c.py:624
msgid "Saving {}{}"
msgstr ""
#: ../main_c.py:626
msgid "Getting file {}"
msgstr ""
#: ../main_c.py:641
msgid "Ending thread"
msgstr ""
#: ../main_c.py:670
msgid "Exporting to {}"
msgstr ""
msgid "Mp4 files"
msgstr "Fichier Mp4"

Binary file not shown.

View File

@ -0,0 +1,88 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR ORGANIZATION
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2024-02-18 11:30+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: pygettext.py 1.5\n"
#: ../main_c.py:96
msgid "No configuration file found, using defaults."
msgstr "Aucun fichier de configuration trouvé, utilisation des valeurs par défaut."
#: ../main_c.py:87
msgid "Found configuration file in {}"
msgstr "Fichier de configuration trouvé dans {}"
#: ../main_c.py:156
msgid "No images yet! Start shooting..."
msgstr "Aucune image! Commencez à tourner..."
#: ../main_c.py:144
msgid "Warning: Some settings are not set to the recommended value!"
msgstr "Attention: certains paramètres ne sont pas optimaux!"
#: ../main_c.py:146
msgid ""
"\n"
"Camera not found or busy."
msgstr "\nCaméra introuvable ou occupée."
#: ../main_c.py:256
msgid "A previous session was found in {}, resume shooting ?"
msgstr "Une session précédente à été trouvée dans {}, reprendre la session ?"
#: ../main_c.py:256
msgid "Resume session?"
msgstr "Reprendre la session ?"
#: ../main_c.py:263
msgid "Using {} as session folder."
msgstr "Utilisation de {} comme dossier de session."
#: ../main_c.py:281
msgid "Image is being saved to {}"
msgstr "Image sauvegardée dans {}"
#: ../main_c.py:320
msgid "{} does not exist"
msgstr "{} n'existe pas."
#: ../main_c.py:345
msgid "Removing {}"
msgstr "Suppression de {}"
#: ../main_c.py:563
msgid "speed too high"
msgstr "Vitesse trop élevée."
#: ../main_c.py:576
msgid "Speed too low."
msgstr "Vitesse trop basse."
#: ../main_c.py:604
msgid "Saving {}{}"
msgstr "Enregistrement de {}{}"
#: ../main_c.py:606
msgid "Getting file {}"
msgstr "Récupération du fichier {}"
#: ../main_c.py:621
msgid "Ending thread"
msgstr "Terminaison du processus."
#: ../main_c.py:650
msgid "Exporting to {}"
msgstr "Exportation dans {}"
msgid "Mp4 files"
msgstr "Fichier Mp4"

8
locales/gen_mo.sh Executable file
View File

@ -0,0 +1,8 @@
#!/usr/bin/env bash
# /usr/lib/python3.11/Tools/i18n/pygettext.py -o template.pot ../app.py
#
# Change to script dir
cd "$(dirname "$0")"
for locale in */LC_MESSAGES/template.pot; do
/usr/bin/msgfmt -o ${locale%.*}.mo $locale
done

87
locales/template.pot Normal file
View File

@ -0,0 +1,87 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR ORGANIZATION
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2024-02-19 18:47+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: pygettext.py 1.5\n"
#: ../main_c.py:96
msgid "No configuration file found, using defaults."
msgstr ""
#: ../main_c.py:109
msgid "Found configuration file in {}"
msgstr ""
#: ../main_c.py:156
msgid "No images yet! Start shooting..."
msgstr ""
#: ../main_c.py:165
msgid "Warning: Some settings are not set to the recommended value!"
msgstr ""
#: ../main_c.py:167
msgid ""
"\n"
"Camera not found or busy."
msgstr ""
#: ../main_c.py:281
msgid "A previous session was found in {}, resume shooting ?"
msgstr ""
#: ../main_c.py:281
msgid "Resume session?"
msgstr ""
#: ../main_c.py:288
msgid "Using {} as session folder."
msgstr ""
#: ../main_c.py:306
msgid "Image is being saved to {}"
msgstr ""
#: ../main_c.py:362
msgid "{} does not exist"
msgstr ""
#: ../main_c.py:387
msgid "Removing {}"
msgstr ""
#: ../main_c.py:607
msgid "speed too high"
msgstr ""
#: ../main_c.py:620
msgid "Speed too low."
msgstr ""
#: ../main_c.py:648
msgid "Saving {}{}"
msgstr ""
#: ../main_c.py:650
msgid "Getting file {}"
msgstr ""
#: ../main_c.py:665
msgid "Ending thread"
msgstr ""
#: ../main_c.py:694
msgid "Exporting to {}"
msgstr ""

6
requirements.txt Normal file
View File

@ -0,0 +1,6 @@
gphoto2
pillow
python-ffmpeg
Send2Trash
opencv-python
numpy