diff --git a/config.toml b/config.toml index 2e34522..a4bfa28 100644 --- a/config.toml +++ b/config.toml @@ -8,10 +8,11 @@ projects_folder = '' onion_skin_onstartup = true onionskin_alpha_default = 0.5 fullscreen_bool = true -screen_w = 1440 -screen_h = 900 +screen_w = 1920 +screen_h = 1080 framerate = 16 ffmpeg_path = '/usr/bin/ffmpeg' +v4l2-ctl_path = '/usr/bin/v4l2-ctl' export_options = 'scale=1920:-1,crop=1920:1080' [CAMERA] cam_w = 1920 diff --git a/frame_opencv.py b/frame_opencv.py index b8678eb..7337195 100644 --- a/frame_opencv.py +++ b/frame_opencv.py @@ -13,11 +13,6 @@ import time from timeit import default_timer as timer import tomllib import numpy as np -import serialutils - -# DSLR -# ~ import gphoto2 as gp -import dslr_helper # Run from SSH if not os.getenv('DISPLAY'): @@ -26,11 +21,9 @@ if not os.getenv('DISPLAY'): 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'] -cam_lenspos = 0 index = 0 playhead = 0 playback = 0 -liveview_only = 0 camera_settings = 0 camera_status = [] @@ -53,6 +46,7 @@ project_settings_defaults = { 'screen_h' : 1080, 'framerate' : 16, 'ffmpeg_path' : None, + 'v4l2-ctl_path' : None, 'export_options' : 'scale=1920:-1,crop=1920:1080:0:102', } @@ -82,67 +76,462 @@ for location in config_locations: config_found_msg = _("Found configuration file in {}").format(os.path.expanduser(location)) print(config_found_msg) -if project_settings['cam_type'] == "showmewebcam": - camera_current_settings = { - 'auto_exposure': dict(min=0, max=1, default=camera_settings['auto_exposure'], value=camera_settings['auto_exposure']), - 'white_balance_auto_preset': dict(min=0, max=9, default=camera_settings['white_balance_auto_preset'], value=camera_settings['white_balance_auto_preset']), - 'horizontal_flip': dict(min=0, max=1, default=camera_settings['hflip'], value=camera_settings['hflip']), - 'vertical_flip': dict(min=0, max=1, default=camera_settings['vflip'], value=camera_settings['vflip']), - 'video_bitrate': dict(min=25000000, max=25000000, default=camera_settings['video_bitrate'], value=camera_settings['video_bitrate']), - } -elif project_settings['cam_type'] == "picam": - camera_current_settings = { - 'auto_exposure': dict(min=0, max=4, default=camera_settings['auto_exposure'], value=camera_settings['auto_exposure']), - 'white_balance_auto_preset': dict(min=0, max=7, default=camera_settings['white_balance_auto_preset'], value=camera_settings['white_balance_auto_preset']), - 'horizontal_flip': dict(min=0, max=1, default=camera_settings['hflip'], value=camera_settings['hflip']), - 'vertical_flip': dict(min=0, max=1, default=camera_settings['vflip'], value=camera_settings['vflip']), - 'anti_flicker': dict(min=0, max=2, default=1, value=1), - } -elif project_settings['cam_type'] == "dslr": - # Sane values for Nikon D40X - camera_current_settings = { - '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 - } -else: - camera_current_settings = {} +class webcam(): + def __init__(self): + self.camera_current_settings = { + 'auto_exposure': dict(min=0, max=1, default=camera_settings['auto_exposure'], value=camera_settings['auto_exposure']), + 'white_balance_auto_preset': dict(min=0, max=9, default=camera_settings['white_balance_auto_preset'], value=camera_settings['white_balance_auto_preset']), + 'horizontal_flip': dict(min=0, max=1, default=camera_settings['hflip'], value=camera_settings['hflip']), + 'vertical_flip': dict(min=0, max=1, default=camera_settings['vflip'], value=camera_settings['vflip']), + 'video_bitrate': dict(min=25000000, max=25000000, default=camera_settings['video_bitrate'], value=camera_settings['video_bitrate']), + } + self.has_liveview = True + self.frame = None + self.overlay = None + self.onionskin = project_settings['onion_skin_onstartup'] + self.onionskin_was_on = self.onionskin + self.liveview_only = False + self.lenspos = None + # Test first device + if not self.test_device(0): + print(_("No camera device found. Exiting...")) + sys.exit() + try: + self.cam = cv2.VideoCapture(0) + self.cam.set(cv2.CAP_PROP_FRAME_WIDTH, camera_settings['cam_w']) + self.cam.set(cv2.CAP_PROP_FRAME_HEIGHT, camera_settings['cam_h']) + except: + sys.exit() -def apply_cam_setting(cam_settings:dict, to_set:list=None): - # TODO: Refactor so that we can call with whatever camera type and setting, and have the work of determining how to apply it done here - # This version should probably be kept and renamed construct_v4l2_cmd() - cmd = 'v4l2-ctl -d /dev/video0' - args = [] - for setting in cam_settings: - if to_set is None: - # Apply defaults - cam_settings[setting]['value'] = cam_settings[setting]['default'] - cmd += ' -c {}={}' - args.append(setting) - args.append(cam_settings[setting]['value']) + def test_device(self, source): + self.cap = cv2.VideoCapture(source) + if self.cap is None or not self.cap.isOpened(): + print(_("Warning: unable to open video source: {}").format(source)) + return False + self.cap.release() + return True + + def capture_preview(self): + ret, overlay = self.cam.read() + if not ret: + print(_("Failed to grab frame.")) + return False + # Resize preview + frame = cv2.resize(overlay, (project_settings['screen_w'], project_settings['screen_h'])) + if self.liveview_only: + # Don't mix it + self.frame = frame + # ~ return frame + if self.onionskin: + # Keep original pic in memory + self.og_frame = overlay.copy() + # calculate and apply alpha + alpha = project_settings['onionskin_alpha_default'] + beta = (1.0 - alpha) + frame = cv2.addWeighted(frame, alpha, overlay, beta, 0) + # ~ self.frame = frame + # ~ return frame + + def capture_frame(self, img_path): + if project_settings['file_extension'] == 'jpg': + capture_ok = cv2.imwrite(img_path, self.og_frame, [int(cv2.IMWRITE_JPEG_QUALITY), project_settings['jpg_quality']]) else: - # Increment settings in to_set - for setting in to_set: - if setting in cam_settings: - if cam_settings[setting]['value']+1 in range(cam_settings[setting]['min'],cam_settings[setting]['max']+1): - cam_settings[setting]['value'] += 1 + capture_ok = cv2.imwrite(img_path, self.og_frame) + return capture_ok + + def increment_setting(self, setting:str): + if self.camera_current_settings[setting]['value']+1 in range(self.camera_current_settings[setting]['min'],self.camera_current_settings[setting]['max']+1): + self.camera_current_settings[setting]['value'] += 1 + else: + self.camera_current_settings[setting]['value'] = self.camera_current_settings[setting]['min'] + + def build_v4l2_cmd(self, to_set:list=None): + self.cmd = '{v4l2-ctl_path} -d /dev/video0'.format(project_settings['v4l2-ctl_path']) + self.args = [] + for setting in self.camera_current_settings: + if to_set is None: + # Apply defaults + self.camera_current_settings[setting]['value'] = self.camera_current_settings[setting]['default'] + self.cmd += ' -c {}={}' + self.args.append(setting) + self.args.append(self.camera_current_settings[setting]['value']) + else: + # Increment settings in to_set + for setting in to_set: + if setting in self.camera_current_settings: + self.increment_setting(setting) + self.cmd += ' -c {}={}' + self.args.append(setting) + self.args.append(self.camera_current_settings[setting]['value']) else: - cam_settings[setting]['value'] = cam_settings[setting]['min'] - cmd += ' -c {}={}' - args.append(setting) - args.append(cam_settings[setting]['value']) - else: - print(_("Unknown setting!")) - break - if project_settings['cam_type'] == "showmewebcam": - serialutils.send_serial_cmd(serialutils.find_cam_port(), cmd.format(*args)) + print(_("Unknown setting!")) + break + return self.cmd, self.args + + def run_v4l2_ctl(self): + if project_settings['v4l2-ctl_path'] is None: + return False + v4l2_ctl_process = subprocess.Popen(self.cmd.split(' ')) + return ffmpeg_process + + def apply_setting(self, to_set:list=None): + self.cmd, self.args = self.build_v4l2_cmd(to_set) + run_v4l2_ctl(self.cmd) + return self.camera_current_settings + + def flip_image(self): + self.apply_setting(['vertical_flip','horizontal_flip']) + + def focus(self, direction:str='-'): + pass + + def reset_picture_settings(self): + self.apply_setting() + + def close(self): + self.cam.release() + + +class showmewebcam(webcam): + def __init__(self): + self.serialutils = __import__('serialutils') + super().__init__() + + def apply_setting(self, to_set:list=None): + self.cmd, self.args = self.build_v4l2_cmd(to_set) + self.serialutils.send_serial_cmd(self.serialutils.find_cam_port(), self.cmd.format(*self.args)) + return self.camera_current_settings + +class picam(): + def __init__(self): + self.camera_current_settings = { + 'auto_exposure': dict(min=0, max=4, default=camera_settings['auto_exposure'], value=camera_settings['auto_exposure']), + 'white_balance_auto_preset': dict(min=0, max=7, default=camera_settings['white_balance_auto_preset'], value=camera_settings['white_balance_auto_preset']), + 'horizontal_flip': dict(min=0, max=1, default=camera_settings['hflip'], value=camera_settings['hflip']), + 'vertical_flip': dict(min=0, max=1, default=camera_settings['vflip'], value=camera_settings['vflip']), + 'anti_flicker': dict(min=0, max=2, default=1, value=1), + } + self.has_liveview = True + self.frame = None + self.overlay = None + self.onionskin = project_settings['onion_skin_onstartup'] + self.onionskin_was_on = self.onionskin + self.liveview_only = False + # Pi Cam V3 setup + self.Picamera2 = __import__('picamera2.Picamera2') + self.Transform = __import__('libcamera.Transform') + # ~ from picamera2 import Picamera2 + # ~ from libcamera import Transform + try: + self.cam = self.Picamera2() + self.picam_config = self.cam.create_video_configuration(main={"format": 'RGB888',"size": (camera_settings['cam_w'], camera_settings['cam_h'])}) + # ~ picam_config["transform"] = Transform(hflip=camera_settings['hflip'], vflip=camera_settings['vflip']) + self.picam_config["transform"] = self.Transform(vflip=camera_current_settings['vertical_flip']['value'],hflip=camera_current_settings['horizontal_flip']['value']) + + self.cam.configure(self.picam_config) + # Autofocus, get lens position and switch to manual mode + # Set Af mode to Auto then Manual (0). Default is Continuous (2), Auto is 1 + self.cam.set_controls({'AfMode':1}) + self.cam.start() + self.cam.autofocus_cycle() + self.lenspos = self.cam.capture_metadata()['LensPosition'] + # Set focus, wb, exp to manual + self.camera_default_settings = {'AfMode': 0, + 'AwbEnable': 1, + 'AwbMode': self.camera_current_settings['white_balance_auto_preset']['default'], + 'AeEnable': 1, + 'AeExposureMode': self.camera_current_settings['auto_exposure']['default'], + # Enable flicker avoidance due to mains + 'AeFlickerMode': 1, + # Mains 50hz = 10000, 60hz = 8333 + # ~ 'AeFlickerPeriod': 8333, + 'AeFlickerPeriod': 10000, + # Format is (min, max, default) in ms + # here: (60fps, 12fps, None) + # ~ 'FrameDurationLimits':(16666,83333,None) + } + self.cam.set_controls(self.camera_default_settings) + # Map generic config name to specific picamera setting name + self.cam_settings_map = { + 'white_balance_auto_preset': 'AwbMode', + 'auto_exposure':'AeExposureMode', + 'anti_flicker' : 'AeFlickerMode', + 'lenspos' : 'LensPosition', + } + except: + pass + + def test_device(self, source): + pass + + def capture_preview(self): + ret, overlay = self.cam.read() + if not ret: + print(_("Failed to grab frame.")) + return False + # Resize preview + self.frame = cv2.resize(overlay, (project_settings['screen_w'], project_settings['screen_h'])) + if self.liveview_only: + # Don't mix it + frame = self.frame + return True + if self.onionskin: + # Keep original pic in memory + self.og_frame = overlay.copy() + # calculate and apply alpha + alpha = project_settings['onionskin_alpha_default'] + beta = (1.0 - alpha) + frame = cv2.addWeighted(self.frame, alpha, overlay, beta, 0) + # ~ self.frame = frame + # ~ return frame + + def capture_frame(self, img_path): + if project_settings['file_extension'] == 'jpg': + capture_ok = cv2.imwrite(img_path, self.og_frame, [int(cv2.IMWRITE_JPEG_QUALITY), project_settings['jpg_quality']]) + else: + capture_ok = cv2.imwrite(img_path, self.og_frame) + return capture_ok + + def increment_setting(self, setting:str): + if self.camera_current_settings[setting]['value']+1 in range(self.camera_current_settings[setting]['min'],self.camera_current_settings[setting]['max']+1): + self.camera_current_settings[setting]['value'] += 1 + else: + self.camera_current_settings[setting]['value'] = self.camera_current_settings[setting]['min'] + # Special cases + # Autoexposure + if setting == 'autoexposure' and self.camera_current_settings['autoexposure']['value'] == 4: + self.cam.set_controls({'AeEnable': 1}) + else: + self.cam.set_controls({'AeEnable': 0}) + self.cam.set_controls({"AeExposureMode": self.camera_current_settings['auto_exposure']['value']}) + # Antiflicker + if setting == 'anti_flicker' and self.camera_current_settings['anti_flicker']['value'] == 0: + self.cam.set_controls({'AeFlickerMode': 0}) + elif self.camera_current_settings['anti_flicker']['value'] == 1: + self.cam.set_controls({'AeFlickerMode': 1, 'AeFlickerPeriod':8333}) + else: + self.cam.set_controls({'AeFlickerMode': 1, 'AeFlickerPeriod':10000}) + + def apply_setting(self, to_set:list=None): + for setting in to_set: + self.increment_setting(setting) + self.cam.set_controls({self.cam_settings_map[setting] : self.camera_current_settings[setting]['value']}) + + def flip_image(self): + self.cam.stop() + self.picam_config["transform"] = self.Transform(vflip=self.camera_current_settings['vertical_flip']['value'],hflip=self.camera_current_settings['horizontal_flip']['value']) + self.cam.configure(self.picam_config) + self.cam.start() + + def focus(self, direction:str='-'): + if direction == '+': + self.lenspos += 0.2 + else: + self.lenspos -= 0.2 + # Set AfMode to Manual + self.cam.set_controls({'AfMode': 0, 'LensPosition': self.lenspos}) + print(_("-Lens pos: {}".format(self.lenspos))) + + def reset_picture_settings(self): + for setting in self.camera_default_settings: + self.cam.set_controls({setting : self.camera_default_settings[setting]}) + + def close(self): + cam.close() + + +class dslr(): + def __init__(self): + # ~ import gphoto2 as gp + self.gp = __import__('gphoto2') + self.camera_current_settings = { + 'capturemode' : dict(min=0, max=4, default=0, value=1), # 0: single,1: burst,2:Timer,3:2S Remote,4:Quick remote + 'imagesize' : dict(min=0, max=2, default=2, value=2), # 0:L, 1:M, 2: S (1936x1296) + 'imagequality' : dict(min=0, max=2, default=2, value=2), # 0 JPEG basic 1 JPEG normal 2 JPEG fine 3 raw 4 raw+jpg + 'whitebalance' : dict(min=0, max=7, default=2, value=1), # 0 Automatic 1 Daylight 2 Fluorescent 3 Tungsten 4 Flash 5 Cloudy 6 Shade 7 Preset + 'capturetarget' : dict(min=0, max=1, default=0, value=0), # Internal memory + 'iso' : dict(min=0, max=5, default=0, value=0), # 0:100, 5:3200 + 'shutterspeed' : dict(min=0, max=51, default=0, value=20), # 0 : 1/4000, 51: 30s + 'manualfocusdrive' : dict(min=0, max=1, default=0, value=0), # Trigger autofocus # manualfocusdrive + } + # Map generic config name to specific picamera setting name + self.cam_settings_map = { + 'white_balance_auto_preset': 'whitebalance', + 'auto_exposure':'iso', + 'anti_flicker' : 'imagesize', + 'lenspos' : 'shutterspeed', + } + self.frame = None + self.has_liveview = False + # TODO: check if DSLR has live feed and set accordingly + # ~ self.onionskin = project_settings['onion_skin_onstartup'] + self.onionskin = False + self.onionskin_was_on = self.onionskin + self.liveview_only = False + self.lenspos = None + self.cam = self.gp.check_result(self.gp.gp_camera_new()) + try: + self.gp.check_result(self.gp.gp_camera_init(self.cam)) + # get configuration tree + self.camera_current_config = self.gp.check_result(self.gp.gp_camera_get_config(self.cam)) + except: + print(_("No camera found.")) + self.cam.exit() + self.camera = None + self.current_camera_config = None + + def test_device(self, source): + pass + + def capture_preview(self): + # TODO : check DSLR has preview/live feed + pass + + def find_file_ext(self, gp_name:str, full_path:str): + # TODO: use re to sub png with jpg ? + # extract dir path + dirname = os.path.dirname(full_path) + # extract filename from path + new_name = os.path.basename(full_path) + # if the path doesn't contain file name, return camera's FS filename + if not full_path.endswith(('.jpg', '.JPG', '.raw')): + return gp_name + suffix = gp_name.split('.')[-1] + prefix = new_name.split('.')[:-1] + prefix.insert(len(prefix), suffix) + return os.path.join(dirname, '.'.join(prefix)) + + def check_status_value(self, config, value, optimal_value=None): + cur_check = self.gp.check_result(self.gp.gp_widget_get_child_by_name(config, value)) + cur_check_value = self.gp.check_result(self.gp.gp_widget_get_value(cur_check)) + if optimal_value is not None: + cur_check_choice = self.gp.check_result(self.gp.gp_widget_get_choice(cur_check, optimal_value[value])) + return [cur_check_value, cur_check_choice] + else: + return cur_check_value + + def capture_frame(self, img_path): + # CHECK: Should we init and close dslr for each frame ? + # Check battery level + battery_level = int(self.check_status_value(self.camera_current_config, 'batterylevel')[:-1]) + if battery_level < 10: + print("Battery level is too low, shutter disabled.") + return False + file_path = self.cam.capture(self.gp.GP_CAPTURE_IMAGE) + print('Camera file path: {0}/{1}'.format(file_path.folder, file_path.name)) + # We don't want to download a jpg or raw from the dslr and save it as a false *.png. + img_path = self.find_file_ext(file_path.name, img_path) + print('Copying image to', img_path) + try: + camera_file = self.cam.file_get( + file_path.folder, + file_path.name, + self.gp.GP_FILE_TYPE_NORMAL + ) + except: + print("Camera error. Check Battery and try restarting the camera.") + return False + try: + capture_ok = camera_file.save(img_path) + except: + print('File access error.') + return False + # ~ camera.exit() + # Update frame + frame = cv2.imread(img_path) + # ~ frame = cv2.resize(frame, (project_settings['screen_w'], project_settings['screen_h'])) + return capture_ok + + def apply_gphoto_setting(self, setting:str): + # Get corresponding setting name if possible + if setting in self.cam_settings_map: + setting = self.cam_settings_map[setting] + # Try to apply setting + if setting in self.camera_current_settings: + print(setting) + select_setting = self.camera_current_settings[setting] + # find the $setting config item + try: + # Get widget with name $setting + cur_setting = self.gp.check_result(self.gp.gp_widget_get_child_by_name(self.camera_current_config, setting)) + # Get a list of available choices + choices = list(self.gp.check_result(self.gp.gp_widget_get_choices(cur_setting))) + # Build dict with name/value equivalence + choices_dict = {choices.index(i):i for i in list(choices)} + # Increment mode : current value is increased or looped + # ~ if inc: + # Get current setting value + # ~ new_value = gp.check_result(gp.gp_widget_get_value(cur_setting)) + # Check current value + 1 is in range + # ~ if choices.index(new_value) in range(0, self.camera_current_settings[setting]['max']+1): + # Apply or loop value accordingly + # ~ pass + # If new_value exists in list, apply + if select_setting['value'] in choices_dict: + cur_setting_choice = self.gp.check_result(self.gp.gp_widget_get_choice(cur_setting, select_setting['value'])) + # set config value + self.gp.check_result(self.gp.gp_widget_set_value(cur_setting, cur_setting_choice)) + except: + print("Configuration error while setting {} to {}".format(setting, select_setting)) + + def increment_setting(self, setting:str): + if setting in self.camera_current_settings: + if self.camera_current_settings[setting]['value']+1 in range(self.camera_current_settings[setting]['min'],self.camera_current_settings[setting]['max']+1): + self.camera_current_settings[setting]['value'] += 1 + else: + self.camera_current_settings[setting]['value'] = self.camera_current_settings[setting]['min'] + + def apply_setting(self, to_set:list=None, inc:bool=False): + self.camera_current_config = self.gp.check_result(self.gp.gp_camera_get_config(self.cam)) + # iterate over the settings dictionary + if to_set is None: + for setting in self.camera_current_settings: + if inc: + self.increment_setting(setting) + self.apply_gphoto_setting(setting) + else: + # Get corresponding setting name if possible + for setting in to_set: + if setting in self.cam_settings_map: + setting = self.cam_settings_map[setting] + if inc: + self.increment_setting(setting) + self.apply_gphoto_setting(setting) + # validate config + status = self.gp.check_result(self.gp.gp_camera_set_config(self.cam, self.camera_current_config)) + # close camera + # ~ self.cam.exit() + return status + + def flip_image(self): + self.frame = cv2.flip(self.frame, -1) + + def focus(self, direction:str='-'): + self.apply_setting(['shutterspeed'], True) + + def reset_picture_settings(self): + self.camera_current_config = self.gp.check_result(self.gp.gp_camera_get_config(self.cam)) + for setting in self.camera_current_settings: + self.camera_current_settings[setting]['value'] = self.camera_current_settings[setting]['default'] + # TODO: use self.apply_setting() instead + self.apply_gphoto_setting(setting) + status = self.gp.check_result(self.gp.gp_camera_set_config(self.cam, self.camera_current_config)) + + def close(self): + self.cam.exit() + +def get_cam_class(camera_type): + if camera_type == 'showmewebcam': + return showmewebcam() + elif camera_type == 'picam': + return picam() + elif camera_type == 'dslr': + return dslr() + elif camera_type == 'webcam': + # ~ return webcam() + pass else: - # TODO: execute v4l2 directly - print(_("Camera function not supported.")) - return cam_settings + return None def generate_text_image(text:str, screen_w, screen_h, bullets=False): @@ -301,13 +690,27 @@ def get_frames_list(folder:str): return existing_animation_files +def get_frame_by_idx(folder:str, index:int): + # Refresh file list + existing_animation_files = get_frames_list(folder) + # Get last file + # Filename pattern is A.0001.JPG + if index and index in range(len(existing_animation_files)): + frm = cv2.imread(os.path.join(folder, existing_animation_files[index])) + frm = cv2.resize(frm, (project_settings['screen_w'], project_settings['screen_h'])) + return frm + else: + return generate_text_image(_("Image not found."), project_settings['screen_w'], project_settings['screen_h']) + + 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_before_last_frame(folder:str): # Refresh file list existing_animation_files = get_frames_list(folder) @@ -316,13 +719,13 @@ def get_before_last_frame(folder:str): return existing_animation_files[-2] -def get_onionskin_frame(folder:str, index=None): +def get_onionskin_frame(folder:str): 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 + # Img does not exist, load blank image else: frm = blank_image return frm @@ -345,6 +748,7 @@ def update_image(img_list, img_index): 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) @@ -381,7 +785,7 @@ def check_range(x, loop=True): return len(img_list)-1 else: return 0 - elif x >= len(img_list)-1: + elif x > len(img_list)-1: if loop: return 0 else: @@ -472,78 +876,42 @@ def export_animation(input_filename, export_filename): # Fix for Quicktime '-pix_fmt', 'yuv420p', '-vcodec', 'libx264', + # Visually lossless export + '-crf', '18', export_filename, ]) return ffmpeg_process +cam = get_cam_class(project_settings['cam_type']) + +if cam is None: + print(_("Wrong camera type in configuration.")) + time.sleep(1) + sys.exit() + + def main(args): - - global onionskin, liveview_only, playback, loop_playback, playhead, index, img_list, first_playback, camera_current_settings + global img_list - # Initialise camera - if project_settings['cam_type'] == "showmewebcam" or project_settings['cam_type'] == "webcam" : - if not testDevice(0): - print(_("No camera device found. Exiting...")) - return 1 - cam = cv2.VideoCapture(0) - cam.set(cv2.CAP_PROP_FRAME_WIDTH, camera_settings['cam_w']) - cam.set(cv2.CAP_PROP_FRAME_HEIGHT, camera_settings['cam_h']) - elif project_settings['cam_type'] == "picam": - # Pi Cam V3 setup - from picamera2 import Picamera2 - from libcamera import Transform - cam = Picamera2() - picam_config = cam.create_video_configuration(main={"format": 'RGB888',"size": (camera_settings['cam_w'], camera_settings['cam_h'])}) - # ~ picam_config["transform"] = Transform(hflip=camera_settings['hflip'], vflip=camera_settings['vflip']) - picam_config["transform"] = Transform(vflip=camera_current_settings['vertical_flip']['value'],hflip=camera_current_settings['horizontal_flip']['value']) - - cam.configure(picam_config) - # Autofocus, get lens position and switch to manual mode - # Set Af mode to Auto then Manual (0). Default is Continuous (2), Auto is 1 - cam.set_controls({'AfMode':1}) - cam.start() - cam.autofocus_cycle() - cam_lenspos = cam.capture_metadata()['LensPosition'] - # Set focus, wb, exp to manual - cam.set_controls({'AfMode': 0, - 'AwbEnable': 1, - 'AwbMode': camera_current_settings['white_balance_auto_preset']['default'], - 'AeEnable': 1, - 'AeExposureMode': camera_current_settings['auto_exposure']['default'], - # Enable flicker avoidance due to mains - 'AeFlickerMode': 1, - # Mains 50hz = 10000, 60hz = 8333 - # ~ 'AeFlickerPeriod': 8333, - 'AeFlickerPeriod': 10000, - # Format is (min, max, default) in ms - # here: (60fps, 12fps, None) - # ~ 'FrameDurationLimits':(16666,83333,None) - }) - # ~ cam.stop() - elif project_settings['cam_type'] == "dslr": - print(_("Applying DSLR settings...")) - dslr_helper.apply_dslr_settings(camera_current_settings) - else: - print(_("No camera type was defined in config.toml")) - - frame = get_onionskin_frame(savepath, index) - - if project_settings['cam_type'] == "showmewebcam": - # Apply config camera settings - camera_current_settings = apply_cam_setting(camera_current_settings) - time.sleep(.5) + playback = False + first_playback = True + playhead = 0 + loop_playback = True + index = len(img_list)-1 + playhead = index - if project_settings['cam_type'] == "dslr": - onionskin = False + cam.apply_setting() + + frame = get_onionskin_frame(savepath) loop_delta = 0 while True: start = timer() if playback: - if onionskin: - onionskin = False - onionskin_was_on = True + if cam.onionskin: + cam.onionskin = False + cam.onionskin_was_on = True if first_playback: playhead = 0 first_playback = False @@ -562,46 +930,13 @@ def main(args): first_playback = True playback = False # Restore onionskin - if 'onionskin_was_on' in locals(): - onionskin = True + if cam.onionskin_was_on: + cam.onionskin = True loop_playback = False - - # If cam is DLSR, we have no liveview/onionskin - if project_settings['cam_type'] != "dslr": - if liveview_only: - # ~ onionskin = False - if project_settings['cam_type'] == "showmewebcam" or project_settings['cam_type'] == "webcam" : - ret, overlay = cam.read() - if not ret: - print(_("Failed to grab frame.")) - break - elif project_settings['cam_type'] == "picam": - overlay = cam.capture_array("main") - # Resize preview - overlay = cv2.resize(overlay, (project_settings['screen_w'], project_settings['screen_h'])) - cv2.imshow("StopiCV", overlay) - # ~ else: - # ~ onionskin = True - - if project_settings['cam_type'] != "dslr": - if onionskin: - if project_settings['cam_type'] == "showmewebcam": - ret, overlay = cam.read() - if not ret: - print(_("Failed to grab frame.")) - break - elif project_settings['cam_type'] == "picam": - overlay = cam.capture_array("main") - og_frame = overlay.copy() - # Resize preview - overlay = cv2.resize(overlay, (project_settings['screen_w'], project_settings['screen_h'])) - # Apply onionskin - alpha = project_settings['onionskin_alpha_default'] - beta = (1.0 - alpha) - overlay = cv2.addWeighted(frame, alpha, overlay, beta, 0) - cv2.imshow("StopiCV", overlay) - - if not playback and not onionskin and not liveview_only: + + if not playback: + if cam.has_liveview: + frame = cam.capture_preview() cv2.imshow("StopiCV", frame) k = cv2.waitKey(1) @@ -609,42 +944,26 @@ def main(args): if (k%256 == 108) or (k%256 == 53) or (k%256 == 181): print(_("Liveview only")) # Toggle liveview - liveview_only = not liveview_only - onionskin = not onionskin + cam.liveview_only = not cam.liveview_only + cam.onionskin = not cam.onionskin # Key o / kp slash elif (k%256 == 111) or (k%256 == 47) or (k%256 == 175): print(_("Onionskin toggle")) # Toggle onionskin - onionskin = not onionskin - liveview_only = False + cam.onionskin = not cam.onionskin + cam.liveview_only = False # Key w / 7 - cycle wb elif (k%256 == 119) or (k%256 == 55) or (k%256 == 183): print(_("White balance mode")) - camera_current_settings = apply_cam_setting(camera_current_settings, ['white_balance_auto_preset']) - if project_settings['cam_type'] == "picam": - cam.set_controls({'AwbMode': camera_current_settings['white_balance_auto_preset']['value']}) + cam.apply_setting(['white_balance_auto_preset'], True) # Key x / 1 - cycle exposure elif (k%256 == 120) or (k%256 == 49) or (k%256 == 177): print(_("Exp. mode")) - camera_current_settings = apply_cam_setting(camera_current_settings, ['auto_exposure']) - # TODO : Move in function - if project_settings['cam_type'] == "picam": - print(camera_current_settings['auto_exposure']['value']) - if camera_current_settings['auto_exposure']['value'] == 4: - cam.set_controls({'AeEnable': 1}) - else: - cam.set_controls({'AeEnable': 0}) - cam.set_controls({"AeExposureMode": camera_current_settings['auto_exposure']['value']}) + cam.apply_setting(['auto_exposure'], True) # Key f / 3 - flip image elif (k%256 == 102) or (k%256 == 51) or (k%256 == 179): print(_("Flip image")) - camera_current_settings = apply_cam_setting(camera_current_settings, ['vertical_flip','horizontal_flip']) - # TODO : Move in function - if project_settings['cam_type'] == "picam": - cam.stop() - picam_config["transform"] = Transform(vflip=camera_current_settings['vertical_flip']['value'],hflip=camera_current_settings['horizontal_flip']['value']) - cam.configure(picam_config) - cam.start() + cam.flip_image() # Key up, kp 8 elif (k%256 == 82) or (k%256 == 56) or (k%256 == 184): print(_("Last frame")) @@ -678,19 +997,7 @@ def main(args): # Key r / keypad 9 - reset wb,exp elif (k%256 == 114) or (k%256 == 57) or (k%256 == 185) : print(_("Reset camera settings")) - camera_current_settings = apply_cam_setting(camera_current_settings) - # TODO : This should be in a function ? - if project_settings['cam_type'] == "picam": - if camera_current_settings['auto_exposure']['default'] == 4: - cam.set_controls({'AeEnable': 0}) - else: - cam.set_controls({'AeEnable': 1}) - cam.set_controls({"AeExposureMode": camera_current_settings['auto_exposure']['default']}) - cam.set_controls({'AwbMode': camera_current_settings['white_balance_auto_preset']['default']}) - cam.stop() - picam_config["transform"] = Transform(vflip=camera_current_settings['vertical_flip']['default'],hflip=camera_current_settings['horizontal_flip']['default']) - cam.configure(picam_config) - cam.start() + cam.reset_picture_settings() # Key e / keypad * elif (k%256 == 101) or (k%256 == 42) or (k%256 == 170) : print(_("Export")) @@ -707,56 +1014,37 @@ def main(args): print(_("Remove frame")) img_list, index, frame = remove_frame(img_list, index) # Focus +/- with a,z - elif (k%256 == 97) and project_settings['cam_type'] == "picam": - cam_lenspos += 0.2 - # Set AfMode to Manual - cam.set_controls({'AfMode': 0, 'LensPosition': cam_lenspos}) - print(_("+Lens pos: {}".format(cam_lenspos))) - elif (k%256 == 122) and project_settings['cam_type'] == "picam": - cam_lenspos -= 0.2 - # Set AfMode to Manual - cam.set_controls({'AfMode': 0, 'LensPosition': cam_lenspos}) - print(_("-Lens pos: {}".format(cam_lenspos))) + elif (k%256 == 97): + cam.focus('+') + print(_("+Lens pos: {}".format(cam.lenspos))) + elif (k%256 == 122): + cam.focus('-') + print(_("-Lens pos: {}".format(cam.lenspos))) # Set anti-flicker mode with q - elif (k%256 == 113) and project_settings['cam_type'] == "picam": + elif (k%256 == 113): print(_("Anti-flicker mode")) - camera_current_settings = apply_cam_setting(camera_current_settings, ['anti_flicker']) - # TODO : Move this to a function ? - if camera_current_settings['anti_flicker']['value'] == 0: - cam.set_controls({'AeFlickerMode': 0}) - elif camera_current_settings['anti_flicker']['value'] == 1: - cam.set_controls({'AeFlickerMode': 1, 'AeFlickerPeriod':8333}) - else: - cam.set_controls({'AeFlickerMode': 1, 'AeFlickerPeriod':10000}) - print(camera_current_settings['anti_flicker']['value']) + cam.apply_setting(['anti_flicker'], True) # SPACE or numpad 0 pressed elif (k%256 == 32) or (k%256 == 48) or (k%256 == 176): print(_("Capture frame")) img_name = return_next_frame_number(get_last_frame(savepath)) img_path = os.path.join(savepath, img_name) - if project_settings['cam_type'] == 'dslr': - # Get file from DSLR - capture_ok = dslr_helper.capture_and_download(img_path) - else: - # Cam is either webcam, showmewebcam or picam - if project_settings['file_extension'] == 'jpg': - capture_ok = cv2.imwrite(img_path, og_frame, [int(cv2.IMWRITE_JPEG_QUALITY), project_settings['jpg_quality']]) - else: - capture_ok = cv2.imwrite(img_path, og_frame) - print(_("File {} written.").format(img_path)) - + capture_ok = cam.capture_frame(img_path) + print(_("File {} written.").format(img_path)) # Special case when we've no frame yet if len(img_list) and (img_list[index] == '{letter}.-001.{ext}'.format(letter=project_letter, ext=project_settings['file_extension'])): img_list[index] = img_name else: index += 1 + print(index) # Display a message if capture was not successfull - if not capture_ok: + if capture_ok is not None: frame = generate_text_image(_("Error during capture."), project_settings['screen_w'], project_settings['screen_h'] ) else: - frame = get_onionskin_frame(savepath, index) + frame = get_onionskin_frame(savepath) + # ~ frame = cam.frame # Quit app elif k%256 == 27: # ESC pressed @@ -791,12 +1079,7 @@ def main(args): except: print(_("Terminating running process...")) ffmpeg_process.terminate() - if project_settings['cam_type'] == "showmewebcam": - cam.release() - elif project_settings['cam_type'] == "picam": - cam.close() - elif project_settings['cam_type'] == "dslr": - pass + cam.close() cv2.destroyAllWindows() cv2.namedWindow("StopiCV", cv2.WINDOW_GUI_NORMAL) @@ -811,10 +1094,6 @@ img_list = [] savepath = get_session_folder() onionskin = project_settings['onion_skin_onstartup'] liveview_only = False -playback = False -first_playback = True -playhead = 0 -loop_playback = True blank_image = generate_text_image(_("No images yet! Start shooting..."), project_settings['screen_w'], project_settings['screen_h']) if len(savepath): @@ -827,7 +1106,6 @@ index = len(img_list)-1 # Export settings input_filename = "{folder}{sep}{letter}.%04d.{ext}".format(folder=savepath, sep=os.sep, letter=project_letter, ext=project_settings['file_extension']) 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 = project_settings['export_options']