2024-09-06 19:53:58 +02:00
|
|
|
import cv2
|
|
|
|
import gettext
|
|
|
|
from itertools import count
|
|
|
|
import os
|
2024-09-09 14:19:06 +02:00
|
|
|
# Needed for utf-8 text
|
|
|
|
from PIL import ImageFont, ImageDraw, Image
|
2024-09-06 19:53:58 +02:00
|
|
|
from send2trash import send2trash
|
|
|
|
import signal
|
|
|
|
import sys
|
2024-09-08 19:43:39 +02:00
|
|
|
import subprocess
|
2024-09-06 19:53:58 +02:00
|
|
|
import time
|
2024-09-11 20:21:33 +02:00
|
|
|
from timeit import default_timer as timer
|
2024-09-06 19:53:58 +02:00
|
|
|
import tomllib
|
|
|
|
import numpy as np
|
2024-09-11 20:21:33 +02:00
|
|
|
import serialutils
|
2024-09-06 19:53:58 +02: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-09-09 14:19:06 +02:00
|
|
|
|
2024-09-06 19:53:58 +02:00
|
|
|
|
|
|
|
# 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
|
2024-09-08 19:43:39 +02:00
|
|
|
'camera_type': 2,
|
2024-09-06 19:53:58 +02:00
|
|
|
'file_extension':'JPG',
|
|
|
|
'trigger_mode': 'key',
|
|
|
|
'projects_folder': '',
|
2024-09-09 14:19:06 +02:00
|
|
|
'use_date_for_folder': False,
|
2024-09-06 19:53:58 +02:00
|
|
|
'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,
|
2024-09-08 19:43:39 +02:00
|
|
|
'ffmpeg_path' : None,
|
2024-09-06 19:53:58 +02:00
|
|
|
'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 '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-09-11 20:21:33 +02:00
|
|
|
def reset_wb_exp():
|
|
|
|
# Flip auto-exposure, white-balance on to adapt to light environment
|
|
|
|
serialutils.send_serial_cmd(serialutils.find_cam_port(), 'v4l2-ctl -d /dev/video0 -c auto_exposure=0 -c white_balance_auto_preset=1')
|
|
|
|
# Give some time to the captor to adapt
|
|
|
|
time.sleep(1)
|
|
|
|
# Flip back, wb 3 = fluorescent
|
|
|
|
serialutils.send_serial_cmd(serialutils.find_cam_port(), 'v4l2-ctl -d /dev/video0 -c auto_exposure=1 -c white_balance_auto_preset=2')
|
|
|
|
|
|
|
|
|
2024-09-09 14:19:06 +02:00
|
|
|
def generate_text_image(text:str, screen_w, screen_h, bullets=False):
|
|
|
|
text_image = Image.new('RGB',
|
|
|
|
(screen_w, screen_h),
|
|
|
|
(0,0,0)
|
|
|
|
)
|
|
|
|
# ~ text_image = np.zeros((screen_h,screen_w,3),np.uint8)
|
|
|
|
# ~ text_image_pil = Image.fromarray(text_image)
|
|
|
|
text_image_draw = ImageDraw.Draw(text_image)
|
|
|
|
if text is not None:
|
|
|
|
font = ImageFont.truetype("Tuffy_Bold.ttf", (screen_w/32))
|
|
|
|
font_len = font.getlength(text.split('\n')[0])
|
|
|
|
text_image_draw.multiline_text((screen_w/2 - font_len/2, screen_h/2 ),
|
|
|
|
text,
|
|
|
|
fill=(255, 255, 255),
|
|
|
|
font=font,
|
|
|
|
align='center',
|
|
|
|
spacing=20
|
|
|
|
)
|
|
|
|
if bullets:
|
|
|
|
dot_radius = screen_w/24
|
|
|
|
x_unit = (screen_w/32)
|
|
|
|
y_unit = (screen_h/32)
|
|
|
|
green_dot = (x_unit*14, y_unit*24)
|
|
|
|
red_dot = (green_dot[0]+x_unit*4, green_dot[1])
|
|
|
|
# Green dot
|
|
|
|
text_image_draw.circle(green_dot,
|
|
|
|
dot_radius,
|
|
|
|
fill=(0,255,0),
|
|
|
|
outline=None,
|
|
|
|
width=1
|
|
|
|
)
|
|
|
|
text_image_draw.text(green_dot,
|
|
|
|
_("Yes"),
|
|
|
|
fill=(0, 0, 0),
|
|
|
|
font=font,
|
|
|
|
anchor="mm",
|
|
|
|
spacing=20
|
|
|
|
)
|
|
|
|
# Red dot
|
|
|
|
text_image_draw.circle(red_dot,
|
|
|
|
dot_radius,
|
|
|
|
fill=(0,0,255),
|
|
|
|
outline=None,
|
|
|
|
width=1
|
|
|
|
)
|
|
|
|
text_image_draw.text(red_dot,
|
|
|
|
_("No"),
|
|
|
|
fill=(0, 0, 0),
|
|
|
|
font=font,
|
|
|
|
anchor="mm",
|
|
|
|
spacing=20
|
|
|
|
)
|
|
|
|
text_image = np.array(text_image)
|
|
|
|
return text_image
|
|
|
|
|
|
|
|
|
|
|
|
def askyesno(text:str):
|
|
|
|
blank = generate_text_image(text, project_settings['screen_w'], project_settings['screen_h'], bullets=True)
|
|
|
|
cv2.imshow("StopiCV", blank)
|
|
|
|
# Wait for input to continue
|
|
|
|
answer = cv2.waitKey(0)
|
|
|
|
# Space pressed == yes
|
|
|
|
if answer%256 == 32 or answer%256 == 48 :
|
|
|
|
return True
|
|
|
|
# Any other key == no
|
2024-09-06 19:53:58 +02:00
|
|
|
else:
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
2024-09-09 14:19:06 +02:00
|
|
|
def find_letter_after(letter:str, date=False):
|
|
|
|
if letter in alphabet and alphabet.index(letter) < len(alphabet) - 1 and not date:
|
|
|
|
letter = alphabet[alphabet.index(letter) + 1]
|
|
|
|
else:
|
|
|
|
# Create folder with date
|
|
|
|
year,mon,day,hour,minute,sec,wd,yd,dst = time.localtime()
|
|
|
|
letter = '{}-{}-{}_{}-{}-{}'.format(year,mon,day,hour,minute,sec)
|
|
|
|
return letter
|
|
|
|
|
|
|
|
|
2024-09-06 19:53:58 +02:00
|
|
|
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():
|
2024-09-09 14:19:06 +02:00
|
|
|
global next_letter
|
2024-09-06 19:53:58 +02:00
|
|
|
project_folder = get_projects_folder()
|
|
|
|
if project_folder:
|
|
|
|
sessions_list = []
|
|
|
|
dir_list = os.listdir(project_folder)
|
|
|
|
# Filter folders with name only one char long
|
2024-09-09 14:19:06 +02:00
|
|
|
for folder in dir_list:
|
|
|
|
if len(folder) == 1 and folder in alphabet:
|
|
|
|
sessions_list.append(folder)
|
2024-09-06 19:53:58 +02:00
|
|
|
# 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-09-09 14:19:06 +02:00
|
|
|
next_letter = find_letter_after(last_letter, project_settings['use_date_for_folder'])
|
2024-09-06 19:53:58 +02:00
|
|
|
if next_letter is False:
|
|
|
|
return False
|
|
|
|
# A previous session folder was found; ask the user if they wish to resume session
|
2024-09-09 14:19:06 +02:00
|
|
|
if not project_settings['use_date_for_folder']:
|
|
|
|
resume_session = askyesno(_("A previous session was found in {},\n resume shooting ?").format(os.path.join(project_folder, last_letter)))
|
|
|
|
# ~ 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-09-06 19:53:58 +02:00
|
|
|
else:
|
2024-09-09 14:19:06 +02:00
|
|
|
if not project_settings['use_date_for_folder']:
|
|
|
|
next_letter = 'A'
|
|
|
|
else:
|
|
|
|
next_letter = find_letter_after('A', project_settings['use_date_for_folder'])
|
2024-09-06 19:53:58 +02:00
|
|
|
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 = img_list
|
|
|
|
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)
|
|
|
|
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'])]
|
|
|
|
existing_animation_files.sort()
|
|
|
|
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('.')
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
2024-09-11 12:08:42 +02:00
|
|
|
def last_frame(img_index):
|
|
|
|
img_index = len(img_list)-1
|
|
|
|
return img_index, update_image(img_list, img_index)
|
|
|
|
|
|
|
|
|
|
|
|
def first_frame(img_index):
|
|
|
|
img_index = 0
|
|
|
|
return img_index, update_image(img_list, img_index)
|
|
|
|
|
|
|
|
|
2024-09-06 19:53:58 +02:00
|
|
|
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):
|
2024-09-11 12:08:42 +02:00
|
|
|
return img_list, img_index, blank_image
|
2024-09-06 19:53:58 +02:00
|
|
|
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
|
2024-09-11 12:08:42 +02:00
|
|
|
return img_list, img_index, update_image(img_list, img_index)
|
2024-09-06 19:53:58 +02:00
|
|
|
else:
|
2024-09-11 12:08:42 +02:00
|
|
|
return img_list, 0, blank_image
|
2024-09-06 19:53:58 +02:00
|
|
|
|
2024-09-11 20:21:33 +02:00
|
|
|
# ~ def playback_animation(img_list, img_index):
|
|
|
|
# ~ # save onionskin state
|
|
|
|
# ~ if onionskin:
|
|
|
|
# ~ onionskin = False
|
|
|
|
# ~ onionskin_was_on = True
|
|
|
|
|
|
|
|
# ~ 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)
|
2024-09-06 19:53:58 +02:00
|
|
|
# ~ cv2.imshow("StopiCV", img)
|
2024-09-11 20:21:33 +02:00
|
|
|
# ~ # Restore onionskin
|
|
|
|
# ~ if 'onionskin_was_on' in locals():
|
|
|
|
# ~ onionskin = True
|
|
|
|
# ~ # Restore index
|
|
|
|
# ~ return img_index, frame_before_playback
|
2024-09-06 19:53:58 +02:00
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
2024-09-08 19:43:39 +02:00
|
|
|
def parse_export_options(options:str, vflip:bool=False, hflip:bool=False):
|
|
|
|
if vflip:
|
|
|
|
options += ',vflip'
|
|
|
|
if hflip:
|
|
|
|
options += ',hflip'
|
|
|
|
return options
|
|
|
|
|
|
|
|
|
|
|
|
def export_animation(input_filename, export_filename):
|
|
|
|
input_format, framerate = input_options
|
|
|
|
if project_settings['ffmpeg_path'] is None:
|
|
|
|
return False
|
|
|
|
ffmpeg_process = subprocess.Popen([
|
|
|
|
project_settings['ffmpeg_path'],
|
|
|
|
'-v','quiet',
|
|
|
|
'-y',
|
|
|
|
'-f', input_format,
|
|
|
|
'-r', framerate,
|
|
|
|
'-i', input_filename,
|
|
|
|
'-vf', output_options,
|
|
|
|
export_filename,
|
|
|
|
])
|
|
|
|
return ffmpeg_process
|
|
|
|
|
|
|
|
|
2024-09-06 19:53:58 +02:00
|
|
|
def main(args):
|
|
|
|
|
2024-09-11 12:08:42 +02:00
|
|
|
global onionskin, playback, loop_playback, playhead, index, img_list
|
2024-09-06 19:53:58 +02:00
|
|
|
|
|
|
|
if not testDevice(0):
|
|
|
|
print(_("No camera device found. Exiting..."))
|
|
|
|
return 1
|
|
|
|
cam = cv2.VideoCapture(0)
|
2024-09-11 12:08:42 +02:00
|
|
|
cam.set(cv2.CAP_PROP_FRAME_WIDTH, 1600)
|
|
|
|
cam.set(cv2.CAP_PROP_FRAME_HEIGHT, 900)
|
2024-09-06 19:53:58 +02:00
|
|
|
|
|
|
|
frame = get_onionskin_frame(savepath, index)
|
2024-09-11 20:21:33 +02:00
|
|
|
|
|
|
|
# TODO : (re)set camera settings on startup
|
|
|
|
# Using serial to send v4l2-ctl cmd to the camera
|
|
|
|
# Use v4l-ctl to flip frame
|
|
|
|
# v4l2-ctl -d /dev/video0 -c vertical_flip=1 -c control=value
|
|
|
|
# DEPRECATED : Using config file and loading with camera-ctl
|
|
|
|
# Write config file to /tmp/blah
|
|
|
|
# ~ write_config_file = '''rm /tmp/blah && echo "power_line_frequency=2
|
|
|
|
# ~ sharpness=50
|
|
|
|
# ~ video_bitrate=25000000
|
|
|
|
# ~ auto_exposure=1
|
|
|
|
# ~ auto_exposure_bias=2
|
|
|
|
# ~ white_balance_auto_preset=3
|
|
|
|
# ~ exposure_metering_mode=1
|
|
|
|
# ~ " > /tmp/blah'''
|
|
|
|
# Apply config
|
|
|
|
# ~ apply_config = '/opt/camera-control/camera-ctl -c /tmp/blah -v /dev/video0 -i repeat_sequence_header -i h264_i_frame_period -i h264_level -i h264_profile -i compression_quality\rl\rq\r'
|
|
|
|
# ~ serialutils.send_serial_cmd(serialutils.find_cam_port(), write_config_file)
|
|
|
|
# ~ serialutils.send_serial_cmd(serialutils.find_cam_port(), apply_config)
|
|
|
|
|
|
|
|
# ~ User Controls
|
|
|
|
|
|
|
|
# ~ brightness 0x00980900 (int) : min=0 max=100 step=1 default=50 value=50 flags=slider
|
|
|
|
# ~ contrast 0x00980901 (int) : min=-100 max=100 step=1 default=0 value=-2 flags=slider
|
|
|
|
# ~ saturation 0x00980902 (int) : min=-100 max=100 step=1 default=0 value=5 flags=slider
|
|
|
|
# ~ red_balance 0x0098090e (int) : min=1 max=7999 step=1 default=1000 value=1000 flags=slider
|
|
|
|
# ~ blue_balance 0x0098090f (int) : min=1 max=7999 step=1 default=1000 value=1000 flags=slider
|
|
|
|
# ~ horizontal_flip 0x00980914 (bool) : default=0 value=0
|
|
|
|
# ~ vertical_flip 0x00980915 (bool) : default=0 value=0
|
|
|
|
# ~ power_line_frequency 0x00980918 (menu) : min=0 max=3 default=1 value=1
|
|
|
|
# ~ sharpness 0x0098091b (int) : min=-100 max=100 step=1 default=0 value=10 flags=slider
|
|
|
|
# ~ color_effects 0x0098091f (menu) : min=0 max=15 default=0 value=0
|
|
|
|
# ~ rotate 0x00980922 (int) : min=0 max=360 step=90 default=0 value=0 flags=modify-layout
|
|
|
|
# ~ color_effects_cbcr 0x0098092a (int) : min=0 max=65535 step=1 default=32896 value=32896
|
|
|
|
|
|
|
|
# ~ Codec Controls
|
|
|
|
|
|
|
|
# ~ video_bitrate_mode 0x009909ce (menu) : min=0 max=1 default=0 value=0 flags=update
|
|
|
|
# ~ video_bitrate 0x009909cf (int) : min=25000 max=25000000 step=25000 default=10000000 value=25000000
|
|
|
|
# ~ repeat_sequence_header 0x009909e2 (bool) : default=0 value=0
|
|
|
|
# ~ h264_i_frame_period 0x00990a66 (int) : min=0 max=2147483647 step=1 default=60 value=60
|
|
|
|
# ~ h264_level 0x00990a67 (menu) : min=0 max=11 default=11 value=11
|
|
|
|
# ~ h264_profile 0x00990a6b (menu) : min=0 max=4 default=4 value=4
|
|
|
|
|
|
|
|
# ~ Camera Controls
|
|
|
|
|
|
|
|
# ~ auto_exposure 0x009a0901 (menu) : min=0 max=3 default=0 value=0
|
|
|
|
# ~ exposure_time_absolute 0x009a0902 (int) : min=1 max=10000 step=1 default=1000 value=1000
|
|
|
|
# ~ exposure_dynamic_framerate 0x009a0903 (bool) : default=0 value=0
|
|
|
|
# ~ auto_exposure_bias 0x009a0913 (intmenu): min=0 max=24 default=12 value=2
|
|
|
|
# ~ white_balance_auto_preset 0x009a0914 (menu) : min=0 max=9 default=1 value=1
|
|
|
|
# ~ image_stabilization 0x009a0916 (bool) : default=0 value=0
|
|
|
|
# ~ iso_sensitivity 0x009a0917 (intmenu): min=0 max=4 default=0 value=0
|
|
|
|
# ~ iso_sensitivity_auto 0x009a0918 (menu) : min=0 max=1 default=1 value=1
|
|
|
|
# ~ exposure_metering_mode 0x009a0919 (menu) : min=0 max=3 default=0 value=0
|
|
|
|
# ~ scene_mode 0x009a091a (menu) : min=0 max=13 default=0 value=0
|
|
|
|
|
|
|
|
# ~ JPEG Compression Controls
|
|
|
|
# ~ compression_quality 0x009d0903 (int) : min=1 max=100 step=1 default=30 value=30
|
|
|
|
|
|
|
|
# Make sure we're using max bitrate
|
|
|
|
serialutils.send_serial_cmd(serialutils.find_cam_port(), 'v4l2-ctl -d /dev/video0 -c video_bitrate=25000000')
|
|
|
|
# Flip auto-exposure, white-balance on and off to adapt to light environment
|
|
|
|
reset_wb_exp()
|
|
|
|
# Flip preview (0 = vert; 1 = hor)
|
|
|
|
if project_settings['vflip']:
|
|
|
|
serialutils.send_serial_cmd(serialutils.find_cam_port(), 'v4l2-ctl -d /dev/video0 -c vertical_flip=1')
|
|
|
|
else:
|
|
|
|
serialutils.send_serial_cmd(serialutils.find_cam_port(), 'v4l2-ctl -d /dev/video0 -c vertical_flip=0')
|
|
|
|
if project_settings['hflip']:
|
|
|
|
serialutils.send_serial_cmd(serialutils.find_cam_port(), 'v4l2-ctl -d /dev/video0 -c horizontal_flip=1')
|
|
|
|
else:
|
|
|
|
serialutils.send_serial_cmd(serialutils.find_cam_port(), 'v4l2-ctl -d /dev/video0 -c horizontal_flip=0')
|
|
|
|
time.sleep(.5)
|
|
|
|
loop_delta = 0
|
2024-09-06 19:53:58 +02:00
|
|
|
while True:
|
2024-09-11 20:21:33 +02:00
|
|
|
start = timer()
|
2024-09-06 19:53:58 +02:00
|
|
|
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)
|
2024-09-11 20:21:33 +02:00
|
|
|
# Calculate framerate according to loop execution time
|
|
|
|
frame_interval = 1.0/project_settings['framerate']-loop_delta
|
|
|
|
if frame_interval < 0:
|
|
|
|
frame_interval = 0
|
|
|
|
time.sleep(frame_interval)
|
2024-09-06 19:53:58 +02:00
|
|
|
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()
|
2024-09-11 20:21:33 +02:00
|
|
|
og_frame = overlay.copy()
|
2024-09-06 19:53:58 +02:00
|
|
|
# Resize preview
|
|
|
|
overlay = cv2.resize(overlay, (project_settings['screen_w'], project_settings['screen_h']))
|
2024-09-08 19:43:39 +02:00
|
|
|
# Apply onionskin
|
2024-09-11 12:08:42 +02:00
|
|
|
alpha = project_settings['onionskin_alpha_default']
|
|
|
|
beta = (1.0 - alpha)
|
|
|
|
overlay = cv2.addWeighted(frame, alpha, overlay, beta, 0)
|
2024-09-11 20:21:33 +02:00
|
|
|
|
2024-09-06 19:53:58 +02:00
|
|
|
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
|
2024-09-11 12:08:42 +02:00
|
|
|
# Key up
|
|
|
|
elif (k%256 == 82) or (k%256 == 56):
|
|
|
|
if len(img_list):
|
|
|
|
if playback:
|
|
|
|
playback = False
|
|
|
|
index, frame = last_frame(index)
|
|
|
|
# Key down
|
|
|
|
elif (k%256 == 84) or (k%256 == 50):
|
|
|
|
if len(img_list):
|
|
|
|
if playback:
|
|
|
|
playback = False
|
|
|
|
index, frame = first_frame(index)
|
|
|
|
print(index)
|
2024-09-06 19:53:58 +02:00
|
|
|
# Key left
|
|
|
|
elif (k%256 == 81) or (k%256 == 52):
|
|
|
|
# Displau previous frame
|
|
|
|
if len(img_list):
|
|
|
|
if playback:
|
|
|
|
playback = False
|
|
|
|
index, frame = previous_frame(index)
|
|
|
|
elif (k%256 == 83) or (k%256 == 54):
|
|
|
|
# Displau next frame
|
|
|
|
if len(img_list):
|
|
|
|
if playback:
|
|
|
|
playback = False
|
|
|
|
index, frame = next_frame(index)
|
2024-09-11 20:21:33 +02:00
|
|
|
# Key r / keypad 9 - reset wb,exp
|
|
|
|
elif (k%256 == 114) or (k%256 == 57):
|
|
|
|
reset_wb_exp()
|
2024-09-06 19:53:58 +02:00
|
|
|
# Key e / keypad *
|
|
|
|
elif (k%256 == 101) or (k%256 == 42):
|
2024-09-11 12:08:42 +02:00
|
|
|
print(_("Export"))
|
2024-09-08 19:43:39 +02:00
|
|
|
ffmpeg_process = export_animation(input_filename, export_filename)
|
2024-09-06 19:53:58 +02:00
|
|
|
# Key Return
|
|
|
|
elif (k%256 == 13):
|
2024-09-11 12:08:42 +02:00
|
|
|
print(_("Playback"))
|
2024-09-06 19:53:58 +02:00
|
|
|
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
|
2024-09-11 12:08:42 +02:00
|
|
|
print(_("Remove frame"))
|
|
|
|
img_list, index, frame = remove_frame(img_list, index)
|
2024-09-06 19:53:58 +02:00
|
|
|
# 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
|
2024-09-09 14:19:06 +02:00
|
|
|
# SPACE or numpad 0 pressed
|
2024-09-06 19:53:58 +02:00
|
|
|
elif (k%256 == 32) or (k%256 == 48):
|
|
|
|
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))
|
2024-09-11 12:08:42 +02:00
|
|
|
if len(img_list) and (img_list[index] == '{letter}.-001.{ext}'.format(letter=project_letter, ext=project_settings['file_extension'])):
|
2024-09-06 19:53:58 +02:00
|
|
|
img_list[index] = img_name
|
|
|
|
else:
|
|
|
|
index += 1
|
|
|
|
frame = get_onionskin_frame(savepath, index)
|
|
|
|
# REMOVE : Debug print keycode
|
|
|
|
elif k==-1: # normally -1 returned,so don't print it
|
|
|
|
continue
|
|
|
|
else:
|
|
|
|
print(k) # else print its value
|
2024-09-11 20:21:33 +02:00
|
|
|
end = timer()
|
|
|
|
loop_delta = end - start
|
|
|
|
print(loop_playback)
|
|
|
|
|
2024-09-08 19:43:39 +02:00
|
|
|
if 'ffmpeg_process' in locals():
|
2024-09-09 14:19:06 +02:00
|
|
|
if ffmpeg_process.poll() is None:
|
|
|
|
print(_("Ffmpeg is still running.\n Waiting for task to complete."))
|
|
|
|
msg = generate_text_image(_("Ffmpeg is still running.\n Waiting for task to complete."),
|
|
|
|
project_settings['screen_w'], project_settings['screen_h']
|
|
|
|
)
|
|
|
|
cv2.imshow("StopiCV", msg)
|
|
|
|
# Force window refresh
|
|
|
|
cv2.pollKey()
|
2024-09-08 19:43:39 +02:00
|
|
|
try:
|
2024-09-09 14:19:06 +02:00
|
|
|
ffmpeg_process.wait(timeout=20)
|
2024-09-08 19:43:39 +02:00
|
|
|
except:
|
2024-09-09 14:19:06 +02:00
|
|
|
print(_("Terminating running process..."))
|
2024-09-08 19:43:39 +02:00
|
|
|
ffmpeg_process.terminate()
|
2024-09-06 19:53:58 +02:00
|
|
|
cam.release()
|
|
|
|
cv2.destroyAllWindows()
|
|
|
|
|
2024-09-09 14:19:06 +02:00
|
|
|
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)
|
|
|
|
|
2024-09-06 19:53:58 +02:00
|
|
|
ctrlc_pressed = False
|
|
|
|
projects_folder = project_settings['projects_folder']
|
2024-09-09 14:19:06 +02:00
|
|
|
next_letter = 'Z'
|
2024-09-06 19:53:58 +02:00
|
|
|
img_list = []
|
|
|
|
savepath = get_session_folder()
|
|
|
|
onionskin = project_settings['onion_skin_onstartup']
|
|
|
|
playback = False
|
|
|
|
playhead = 0
|
|
|
|
loop_playback = True
|
2024-09-09 14:19:06 +02:00
|
|
|
blank_image = generate_text_image(_("No images yet! Start shooting..."), project_settings['screen_w'], project_settings['screen_h'])
|
2024-09-06 19:53:58 +02:00
|
|
|
|
|
|
|
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
|
2024-09-08 19:43:39 +02:00
|
|
|
print(project_letter)
|
|
|
|
|
|
|
|
# Export settings
|
|
|
|
input_filename = "{folder}{letter}{sep}{letter}.%04d.{ext}".format(folder=projects_folder, sep=os.sep, letter=project_letter, ext=project_settings['file_extension'])
|
|
|
|
print(input_filename)
|
|
|
|
input_options = ["image2", str(project_settings['framerate'])]
|
|
|
|
# ~ output_filename = "{folder}{sep}{filename}.mp4".format(folder=projects_folder, sep=os.sep, filename=savepath.split(os.sep)[-1])
|
|
|
|
output_filename = "{filename}.mp4".format(filename=project_letter)
|
|
|
|
output_options = parse_export_options(project_settings['export_options'], project_settings['vflip'], project_settings['hflip'] )
|
|
|
|
|
|
|
|
export_filename = os.path.join(savepath, output_filename)
|
2024-09-06 19:53:58 +02:00
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
signal.signal(signal.SIGINT, signal_handler)
|
|
|
|
sys.exit(main(sys.argv[1:]))
|