Fix picam support

This commit is contained in:
ABelliqueux 2025-02-15 10:41:26 +01:00
parent e03d5e7872
commit 4cead89259
4 changed files with 113 additions and 120 deletions

View File

@ -29,8 +29,8 @@ import sys
import gphoto2 as gp import gphoto2 as gp
camera_current_settings = { camera_current_settings = {
'capturemode' : dict(min=0, max=4, default=0, value=3), # 0: single,1: burst,2:Timer,3:2S Remote,4:Quick remote 'capturemode' : dict(min=0, max=4, default=0, value=3), # 0: single,1: burst,2:Timer,3:2S Remote,4:Quick remote
'imagesize' : dict(min=0, max=2, default=0, value=2), # 0:L, 1:M, 2: S (1936x1296) 'imagesize' : dict(min=0, max=2, default=0, value=2), # 0:L, 1:M, 2: S (1936x1296)
'whitebalance' : dict(min=0, max=7, default=0, value=1), # 0 Automatic 1 Daylight 2 Fluorescent 3 Tungsten 4 Flash 5 Cloudy 6 Shade 7 Preset 'whitebalance' : dict(min=0, max=7, default=0, 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 'capturetarget' : dict(min=0, max=1, default=0, value=0), # Internal memory
# ~ 'nocfcardrelease' : dict(min=0, max=1, default=0, value=0), # Allow capture without sd card # ~ 'nocfcardrelease' : dict(min=0, max=1, default=0, value=0), # Allow capture without sd card
@ -61,7 +61,7 @@ def apply_gphoto_setting(config, setting, new_value, inc=False):
choices = list(gp.check_result(gp.gp_widget_get_choices(cur_setting))) choices = list(gp.check_result(gp.gp_widget_get_choices(cur_setting)))
# Build dict with name/value equivalence # Build dict with name/value equivalence
choices_dict = {choices.index(i):i for i in list(choices)} choices_dict = {choices.index(i):i for i in list(choices)}
# Increment mode : current value is increased or looped # Increment mode : current value is increased or looped
if inc: if inc:
# Get current setting value # Get current setting value
new_value = gp.check_result(gp.gp_widget_get_value(cur_setting)) new_value = gp.check_result(gp.gp_widget_get_value(cur_setting))
@ -124,7 +124,7 @@ def find_file_ext(gp_name:str, full_path:str):
print(suffix) print(suffix)
print(prefix) print(prefix)
return os.path.join(dirname, '.'.join(prefix)) return os.path.join(dirname, '.'.join(prefix))
def capture_and_download(target:str='/tmp'): def capture_and_download(target:str='/tmp'):
camera, current_camera_config = initialize_camera() camera, current_camera_config = initialize_camera()

View File

@ -33,7 +33,7 @@ camera_status = []
LOCALE = os.getenv('LANG', 'en_EN') LOCALE = os.getenv('LANG', 'en_EN')
_ = gettext.translation('template', localedir='locales', languages=[LOCALE]).gettext _ = gettext.translation('template', localedir='locales', languages=[LOCALE]).gettext
# Config # Config
# defaults # defaults
project_settings_defaults = { project_settings_defaults = {
'cam_type': "webcam", 'cam_type': "webcam",
@ -88,8 +88,8 @@ class webcam():
} }
self.cam_settings_map = { self.cam_settings_map = {
'white_balance_auto_preset': 'white_balance_temperature', 'white_balance_auto_preset': 'white_balance_temperature',
'white_balance_automatic': 'white_balance_automatic', 'white_balance_automatic': 'white_balance_automatic',
'auto_exposure':'auto_exposure', 'auto_exposure':'auto_exposure',
'anti_flicker' : 'power_line_frequency', 'anti_flicker' : 'power_line_frequency',
'lenspos' : 'sharpness', 'lenspos' : 'sharpness',
} }
@ -118,13 +118,13 @@ class webcam():
sys.exit() sys.exit()
def test_device(self, source): def test_device(self, source):
self.cap = cv2.VideoCapture(source) self.cap = cv2.VideoCapture(source)
if self.cap is None or not self.cap.isOpened(): if self.cap is None or not self.cap.isOpened():
print(_("Warning: unable to open video source: {}").format(source)) print(_("Warning: unable to open video source: {}").format(source))
return False return False
self.cap.release() self.cap.release()
return True return True
def capture_preview(self): def capture_preview(self):
ret, overlay = self.cam.read() ret, overlay = self.cam.read()
if not ret: if not ret:
@ -251,7 +251,7 @@ class picam():
# Map generic config name to specific picamera setting name # Map generic config name to specific picamera setting name
self.cam_settings_map = { self.cam_settings_map = {
'white_balance_auto_preset': 'AwbMode', 'white_balance_auto_preset': 'AwbMode',
'auto_exposure':'AeExposureMode', 'auto_exposure':'AeExposureMode',
'anti_flicker' : 'AeFlickerMode', 'anti_flicker' : 'AeFlickerMode',
'lenspos' : 'LensPosition', 'lenspos' : 'LensPosition',
} }
@ -270,48 +270,41 @@ class picam():
# Pi Cam V3 setup # Pi Cam V3 setup
self.Picamera2 = getattr(import_module('picamera2'), 'Picamera2') self.Picamera2 = getattr(import_module('picamera2'), 'Picamera2')
self.Transform = getattr(import_module('libcamera'), 'Transform') self.Transform = getattr(import_module('libcamera'), 'Transform')
# ~ self.Picamera2 = __import__('picamera2.Picamera2') # Cam setup
# ~ self.Transform = __import__('libcamera.Transform') self.cam = self.Picamera2()
# ~ from picamera2 import Picamera2 self.picam_config = self.cam.create_video_configuration(main={"format": 'RGB888',"size": (camera_settings['cam_w'], camera_settings['cam_h'])})
# ~ from libcamera import Transform self.picam_config["transform"] = self.Transform(vflip=self.camera_current_settings['vertical_flip']['value'],hflip=self.camera_current_settings['horizontal_flip']['value'])
try:
self.cam = self.Picamera2() self.cam.configure(self.picam_config)
self.picam_config = self.cam.create_video_configuration(main={"format": 'RGB888',"size": (camera_settings['cam_w'], camera_settings['cam_h'])}) # Autofocus, get lens position and switch to manual mode
# ~ picam_config["transform"] = Transform(hflip=camera_settings['hflip'], vflip=camera_settings['vflip']) # Set Af mode to Auto then Manual (0). Default is Continuous (2), Auto is 1
self.picam_config["transform"] = self.Transform(vflip=camera_current_settings['vertical_flip']['value'],hflip=camera_current_settings['horizontal_flip']['value']) self.cam.set_controls({'AfMode':1})
self.cam.start()
self.cam.configure(self.picam_config) self.cam.autofocus_cycle()
# Autofocus, get lens position and switch to manual mode self.lenspos = self.cam.capture_metadata()['LensPosition']
# Set Af mode to Auto then Manual (0). Default is Continuous (2), Auto is 1 # Set focus, wb, exp to manual
self.cam.set_controls({'AfMode':1}) self.camera_default_settings = {'AfMode': 0,
self.cam.start() 'AwbEnable': 1,
self.cam.autofocus_cycle() 'AwbMode': self.camera_current_settings['white_balance_auto_preset']['default'],
self.lenspos = self.cam.capture_metadata()['LensPosition'] 'AeEnable': 1,
# Set focus, wb, exp to manual 'AeExposureMode': self.camera_current_settings['auto_exposure']['default'],
self.camera_default_settings = {'AfMode': 0, # Enable flicker avoidance due to mains
'AwbEnable': 1, 'AeFlickerMode': 1,
'AwbMode': self.camera_current_settings['white_balance_auto_preset']['default'], # Mains 50hz = 10000, 60hz = 8333
'AeEnable': 1, # ~ 'AeFlickerPeriod': 8333,
'AeExposureMode': self.camera_current_settings['auto_exposure']['default'], 'AeFlickerPeriod': 10000,
# Enable flicker avoidance due to mains # Format is (min, max, default) in ms
'AeFlickerMode': 1, # here: (60fps, 12fps, None)
# Mains 50hz = 10000, 60hz = 8333 # ~ 'FrameDurationLimits':(16666,83333,None)
# ~ 'AeFlickerPeriod': 8333, }
'AeFlickerPeriod': 10000, self.cam.set_controls(self.camera_default_settings)
# Format is (min, max, default) in ms
# here: (60fps, 12fps, None)
# ~ 'FrameDurationLimits':(16666,83333,None)
}
self.cam.set_controls(self.camera_default_settings)
except:
pass
def test_device(self, source): def test_device(self, source):
pass pass
# Same as in webcam() class # Same as in webcam() class
def capture_preview(self): def capture_preview(self):
overlay = cam.capture_array("main") overlay = self.cam.capture_array("main")
# Resize preview to fit screen # Resize preview to fit screen
overlay = cv2.resize(overlay, (project_settings['screen_w'], project_settings['screen_h'])) overlay = cv2.resize(overlay, (project_settings['screen_w'], project_settings['screen_h']))
if self.liveview_only: if self.liveview_only:
@ -328,7 +321,7 @@ class picam():
return True return True
self.frame = self.o_frame self.frame = self.o_frame
return True return True
# Same as in webcam() class # Same as in webcam() class
def capture_frame(self, img_path): def capture_frame(self, img_path):
if project_settings['file_extension'] == 'jpg': if project_settings['file_extension'] == 'jpg':
@ -336,7 +329,7 @@ class picam():
else: else:
capture_ok = cv2.imwrite(img_path, self.og_frame) capture_ok = cv2.imwrite(img_path, self.og_frame)
return capture_ok return capture_ok
def increment_setting(self, setting:str): def increment_setting(self, setting:str):
if setting in self.camera_current_settings: if setting in self.camera_current_settings:
if self.camera_current_settings[setting]['value'] + self.camera_current_settings[setting]['step'] in range(self.camera_current_settings[setting]['min'],self.camera_current_settings[setting]['max']+1): if self.camera_current_settings[setting]['value'] + self.camera_current_settings[setting]['step'] in range(self.camera_current_settings[setting]['min'],self.camera_current_settings[setting]['max']+1):
@ -356,21 +349,21 @@ class picam():
elif self.camera_current_settings['anti_flicker']['value'] == 1: elif self.camera_current_settings['anti_flicker']['value'] == 1:
self.cam.set_controls({'AeFlickerMode': 1, 'AeFlickerPeriod':8333}) self.cam.set_controls({'AeFlickerMode': 1, 'AeFlickerPeriod':8333})
else: else:
self.cam.set_controls({'AeFlickerMode': 1, 'AeFlickerPeriod':10000}) self.cam.set_controls({'AeFlickerMode': 1, 'AeFlickerPeriod':10000})
def apply_setting(self, to_set:list=None, inc:bool=False): def apply_setting(self, to_set:list=None, inc:bool=False):
if to_set is not None: if to_set is not None:
for setting in to_set: for setting in to_set:
if inc: if inc:
self.increment_setting(setting) self.increment_setting(setting)
self.cam.set_controls({self.cam_settings_map[setting] : self.camera_current_settings[setting]['value']}) self.cam.set_controls({self.cam_settings_map[setting] : self.camera_current_settings[setting]['value']})
def flip_image(self): def flip_image(self):
self.cam.stop() 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.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.configure(self.picam_config)
self.cam.start() self.cam.start()
def focus(self, direction:str='-'): def focus(self, direction:str='-'):
if direction == '+': if direction == '+':
self.lenspos += 0.2 self.lenspos += 0.2
@ -379,11 +372,11 @@ class picam():
# Set AfMode to Manual # Set AfMode to Manual
self.cam.set_controls({'AfMode': 0, 'LensPosition': self.lenspos}) self.cam.set_controls({'AfMode': 0, 'LensPosition': self.lenspos})
print(_("-Lens pos: {}".format(self.lenspos))) print(_("-Lens pos: {}".format(self.lenspos)))
def reset_picture_settings(self): def reset_picture_settings(self):
for setting in self.camera_default_settings: for setting in self.camera_default_settings:
self.cam.set_controls({setting : self.camera_default_settings[setting]}) self.cam.set_controls({setting : self.camera_default_settings[setting]})
def close(self): def close(self):
cam.close() cam.close()
@ -393,8 +386,8 @@ class dslr():
# ~ import gphoto2 as gp # ~ import gphoto2 as gp
self.gp = import_module('gphoto2') self.gp = import_module('gphoto2')
self.camera_current_settings = { 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 '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) '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 '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 '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 'capturetarget' : dict(min=0, max=1, default=0, value=0), # Internal memory
@ -405,7 +398,7 @@ class dslr():
# Map generic config name to specific picamera setting name # Map generic config name to specific picamera setting name
self.cam_settings_map = { self.cam_settings_map = {
'white_balance_auto_preset': 'whitebalance', 'white_balance_auto_preset': 'whitebalance',
'auto_exposure':'iso', 'auto_exposure':'iso',
'anti_flicker' : 'imagesize', 'anti_flicker' : 'imagesize',
'lenspos' : 'shutterspeed', 'lenspos' : 'shutterspeed',
} }
@ -434,14 +427,14 @@ class dslr():
self.cam.exit() self.cam.exit()
self.camera = None self.camera = None
self.current_camera_config = None self.current_camera_config = None
def test_device(self, source): def test_device(self, source):
pass pass
def capture_preview(self): def capture_preview(self):
# TODO : check DSLR has preview/live feed # TODO : check DSLR has preview/live feed
pass pass
def find_file_ext(self, gp_name:str, full_path:str): def find_file_ext(self, gp_name:str, full_path:str):
# TODO: use re to sub png with jpg ? # TODO: use re to sub png with jpg ?
# extract dir path # extract dir path
@ -455,7 +448,7 @@ class dslr():
prefix = new_name.split('.')[:-1] prefix = new_name.split('.')[:-1]
prefix.insert(len(prefix), suffix) prefix.insert(len(prefix), suffix)
return os.path.join(dirname, '.'.join(prefix)) return os.path.join(dirname, '.'.join(prefix))
def check_status_value(self, config, value, optimal_value=None): 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 = 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)) cur_check_value = self.gp.check_result(self.gp.gp_widget_get_value(cur_check))
@ -464,7 +457,7 @@ class dslr():
return [cur_check_value, cur_check_choice] return [cur_check_value, cur_check_choice]
else: else:
return cur_check_value return cur_check_value
def capture_frame(self, img_path): def capture_frame(self, img_path):
# CHECK: Should we init and close dslr for each frame ? # CHECK: Should we init and close dslr for each frame ?
# Check battery level # Check battery level
@ -515,7 +508,7 @@ class dslr():
choices = list(self.gp.check_result(self.gp.gp_widget_get_choices(cur_setting))) choices = list(self.gp.check_result(self.gp.gp_widget_get_choices(cur_setting)))
# Build dict with name/value equivalence # Build dict with name/value equivalence
choices_dict = {choices.index(i):i for i in list(choices)} choices_dict = {choices.index(i):i for i in list(choices)}
# Increment mode : current value is increased or looped # Increment mode : current value is increased or looped
# ~ if inc: # ~ if inc:
# Get current setting value # Get current setting value
# ~ new_value = gp.check_result(gp.gp_widget_get_value(cur_setting)) # ~ new_value = gp.check_result(gp.gp_widget_get_value(cur_setting))
@ -537,7 +530,7 @@ class dslr():
self.camera_current_settings[setting]['value'] += self.camera_current_settings[setting]['step'] self.camera_current_settings[setting]['value'] += self.camera_current_settings[setting]['step']
else: else:
self.camera_current_settings[setting]['value'] = self.camera_current_settings[setting]['min'] self.camera_current_settings[setting]['value'] = self.camera_current_settings[setting]['min']
def apply_setting(self, to_set:list=None, inc:bool=False): 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)) self.camera_current_config = self.gp.check_result(self.gp.gp_camera_get_config(self.cam))
# iterate over the settings dictionary # iterate over the settings dictionary
@ -559,13 +552,13 @@ class dslr():
# close camera # close camera
# ~ self.cam.exit() # ~ self.cam.exit()
return status return status
def flip_image(self): def flip_image(self):
self.frame = cv2.flip(self.frame, -1) self.frame = cv2.flip(self.frame, -1)
def focus(self, direction:str='-'): def focus(self, direction:str='-'):
self.apply_setting(['shutterspeed'], True) self.apply_setting(['shutterspeed'], True)
def reset_picture_settings(self): def reset_picture_settings(self):
self.camera_current_config = self.gp.check_result(self.gp.gp_camera_get_config(self.cam)) self.camera_current_config = self.gp.check_result(self.gp.gp_camera_get_config(self.cam))
for setting in self.camera_current_settings: for setting in self.camera_current_settings:
@ -573,7 +566,7 @@ class dslr():
# TODO: use self.apply_setting() instead # TODO: use self.apply_setting() instead
self.apply_gphoto_setting(setting) self.apply_gphoto_setting(setting)
status = self.gp.check_result(self.gp.gp_camera_set_config(self.cam, self.camera_current_config)) status = self.gp.check_result(self.gp.gp_camera_set_config(self.cam, self.camera_current_config))
def close(self): def close(self):
self.cam.exit() self.cam.exit()
@ -812,7 +805,7 @@ def update_image(img_list, img_index):
def next_frame(img_index, loop=True): def next_frame(img_index, loop=True):
img_index = check_range(img_index+1, loop) img_index = check_range(img_index+1, loop)
return img_index, update_image(img_list, img_index) return img_index, update_image(img_list, img_index)
def previous_frame(img_index): def previous_frame(img_index):
img_index = check_range(img_index-1) img_index = check_range(img_index-1)
@ -854,7 +847,7 @@ def check_range(x, loop=True):
return x return x
def batch_rename(folder:str): def batch_rename(folder:str):
# initialize counter to 0 # initialize counter to 0
frame_list = get_frames_list(folder) frame_list = get_frames_list(folder)
counter = (".%04i." % x for x in count(0)) counter = (".%04i." % x for x in count(0))
@ -900,7 +893,7 @@ def remove_frame(img_list, img_index):
def testDevice(source): def testDevice(source):
cap = cv2.VideoCapture(source) cap = cv2.VideoCapture(source)
if cap is None or not cap.isOpened(): if cap is None or not cap.isOpened():
print(_("Warning: unable to open video source: {}").format(source)) print(_("Warning: unable to open video source: {}").format(source))
return False return False
@ -929,7 +922,7 @@ def export_animation(input_filename, export_filename):
project_settings['ffmpeg_path'], project_settings['ffmpeg_path'],
'-v','quiet', '-v','quiet',
'-y', '-y',
'-f', input_format, '-f', input_format,
'-r', framerate, '-r', framerate,
'-i', input_filename, '-i', input_filename,
'-vf', output_options, '-vf', output_options,
@ -953,19 +946,19 @@ if cam is None:
def main(args): def main(args):
global img_list global img_list
playback = False playback = False
first_playback = True first_playback = True
playhead = 0 playhead = 0
loop_playback = True loop_playback = True
index = len(img_list)-1 index = len(img_list)-1
playhead = index playhead = index
cam.apply_setting() cam.apply_setting()
cam.frame = get_onionskin_frame(savepath) cam.frame = get_onionskin_frame(savepath)
cam.o_frame = cam.frame.copy() cam.o_frame = cam.frame.copy()
loop_delta = 0 loop_delta = 0
while True: while True:
start = timer() start = timer()
@ -1028,7 +1021,7 @@ def main(args):
# Key up, kp 8 # Key up, kp 8
elif (k%256 == 82) or (k%256 == 56) or (k%256 == 184): elif (k%256 == 82) or (k%256 == 56) or (k%256 == 184):
print(_("Last frame")) print(_("Last frame"))
if len(img_list): if len(img_list):
if playback: if playback:
playback = False playback = False
index, frame = last_frame(index) index, frame = last_frame(index)
@ -1036,7 +1029,7 @@ def main(args):
# Key down , kp 2 # Key down , kp 2
elif (k%256 == 84) or (k%256 == 50) or (k%256 == 178): elif (k%256 == 84) or (k%256 == 50) or (k%256 == 178):
print(_("First frame")) print(_("First frame"))
if len(img_list): if len(img_list):
if playback: if playback:
playback = False playback = False
index, frame = first_frame(index) index, frame = first_frame(index)
@ -1096,7 +1089,7 @@ def main(args):
img_name = return_next_frame_number(get_last_frame(savepath)) img_name = return_next_frame_number(get_last_frame(savepath))
img_path = os.path.join(savepath, img_name) img_path = os.path.join(savepath, img_name)
capture_ok = cam.capture_frame(img_path) capture_ok = cam.capture_frame(img_path)
print(_("File {} written.").format(img_path)) print(_("File {} written.").format(img_path))
# Special case when we've no frame yet # 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'])): 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 img_list[index] = img_name

View File

@ -2,14 +2,14 @@
## Branche gphoto / réflexe nunmérique ## Branche gphoto / réflexe nunmérique
**Ceci est la branche qui restaure la possibilité d'utiliser des périphériques compatibles [gphoto](http://gphoto.org/doc/remote).** **Ceci est la branche qui restaure la possibilité d'utiliser des périphériques compatibles [gphoto](http://gphoto.org/doc/remote).**
<a style="max-height: 300px;display: inline-block;" href="./stopi2/raw/branch/master/stopi_station.jpg"><img src="./stopi_station.jpg"/><a/> <a style="max-height: 300px;display: inline-block;" href="./stopi2/raw/branch/master/stopi_station.jpg"><img src="./stopi_station.jpg"/><a/>
Seconde version du script python [stopi](https://git.arthus.net/arthus/stopi) destiné à être utilisé avec une télécommande [picote](/arthus/picote/src/branch/picamera). Seconde version du script python [stopi](https://git.arthus.net/arthus/stopi) destiné à être utilisé avec une télécommande [picote](/arthus/picote/src/branch/picamera).
Cette version utilise opencv et libcamera.Elle fonctionne avec une webcam ou un module vidéo Picamera (v1,v2 ou v3). Cette version utilise opencv et libcamera.Elle fonctionne avec une webcam ou un module vidéo Picamera (v1,v2 ou v3).
Encore une fois, l'objectif est de créer un logiciel simple et minimaliste dans son interface, dont les caractéristiques sont les suivantes : Encore une fois, l'objectif est de créer un logiciel simple et minimaliste dans son interface, dont les caractéristiques sont les suivantes :
* Affichage des images en plein écran sans interface : toutes les fonctions utilisent quelques touches du clavier. * Affichage des images en plein écran sans interface : toutes les fonctions utilisent quelques touches du clavier.
* [Pelure d'oignon](https://fr.wikipedia.org/wiki/Pelure_d'oignon#Sciences_et_techniques) entre la dernière image et le flux vidéo. * [Pelure d'oignon](https://fr.wikipedia.org/wiki/Pelure_d'oignon#Sciences_et_techniques) entre la dernière image et le flux vidéo.
@ -22,14 +22,14 @@ Encore une fois, l'objectif est de créer un logiciel simple et minimaliste dans
## Banc de test ## Banc de test
Ce script a été testé avec une webcam compatible V4L2, une ["showmewebcam"](https://github.com/showmewebcam/showmewebcam) à base de rpi 0 et d'un module caméra v2 (8Mp), et un ordinateur classique sous [Debian](https://debian.org) et un [RPI 4B](https://www.raspberrypi.com/products/raspberry-pi-4-model-b/) munis d'un module [Picamera V3](https://www.raspberrypi.com/products/camera-module-3/). Ce script a été testé avec une webcam compatible V4L2, une ["showmewebcam"](https://github.com/showmewebcam/showmewebcam) à base de rpi 0 et d'un module caméra v2 (8Mp), et un ordinateur classique sous [Debian](https://debian.org) et un [RPI 4B](https://www.raspberrypi.com/products/raspberry-pi-4-model-b/) munis d'un module [Picamera V3](https://www.raspberrypi.com/products/camera-module-3/).
Voici un récapitulatif des tests effectués : Voici un récapitulatif des tests effectués :
| Machine \ Type de Caméra | Webcam | [Showmewebcam](https://github.com/showmewebcam/showmewebcam) | RPI Caméra module V1 (5MP) | [RPI Caméra module V3](https://www.raspberrypi.com/products/camera-module-3/) (12MP) | [Réflexe numérique](http://gphoto.org/doc/remote) (Nikon D3000/D40x)| | Machine \ Type de Caméra | Webcam | [Showmewebcam](https://github.com/showmewebcam/showmewebcam) | RPI Caméra module V1 (5MP) | [RPI Caméra module V3](https://www.raspberrypi.com/products/camera-module-3/) (12MP) | [Réflexe numérique](http://gphoto.org/doc/remote) (Nikon D3000/D40x)|
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| Raspberry Pi 4B (Debian 12) | | | &check; | | | | Raspberry Pi 4B (Debian 12) | | | &check; | | |
| PC Linux (Debian, Manjaro) | &check; | &check; | N/A | N/A | &check; | | PC Linux (Debian, Manjaro) | &check; | &check; | N/A | N/A | &check; |
## Feuille de route ## Feuille de route
@ -39,22 +39,22 @@ Des fonctions supplémentaires sont prévues :
## Contributions ## Contributions
Les contributions et rapports de bugs sont les bienvenus ! Les contributions et rapports de bugs sont les bienvenus !
## Installation ## Installation
Dans un terminal : Dans un terminal :
0. (Utilisateurs Windows) Activer le [sous système Linux **version 2** (WSL2)](https://learn.microsoft.com/fr-fr/windows/wsl/install) et installer Debian. 0. (Utilisateurs Windows) Activer le [sous système Linux **version 2** (WSL2)](https://learn.microsoft.com/fr-fr/windows/wsl/install) et installer Debian.
1. Installer les dépendances suivantes : 1. Installer les dépendances suivantes :
``` ```
# Avec une distribution basée sur Debian (Ubuntu, Mint...) # Avec une distribution basée sur Debian (Ubuntu, Mint...)
sudo apt install --no-install-recommends --no-install-suggests git ffmpeg python3-pip python3-venv libtiff5-dev libopenjp2-7 libopenjp2-7-dev zlib1g-dev libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev libharfbuzz-dev libfribidi-dev libxcb1-dev python3-tk python3-dev libopenblas-dev libatlas-base-dev libhdf5-dev libhdf5-serial-dev libatlas-base-dev libjasper-dev libqtgui4 libqt4-test v4l-utils sudo apt install --no-install-recommends --no-install-suggests git ffmpeg python3-pip python3-venv libtiff5-dev libopenjp2-7 libopenjp2-7-dev zlib1g-dev libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev libharfbuzz-dev libfribidi-dev libxcb1-dev python3-tk python3-dev libopenblas-dev libatlas-base-dev libhdf5-dev libhdf5-serial-dev libatlas-base-dev libjasper-dev libqtgui4 libqt4-test v4l-utils
``` ```
- (Optionnel) Pour installer un environnement graphique minimal sur une [installation console](https://debian-facile.org/doc:install:installation-minimale) : `sudo apt install --no-install-recommends --no-install-suggests openbox xserver-xorg xinit pcmanfm gmrun lxterminal hsetroot unclutter plymouth plymouth-themes` - (Optionnel) Pour installer un environnement graphique minimal sur une [installation console](https://debian-facile.org/doc:install:installation-minimale) : `sudo apt install --no-install-recommends --no-install-suggests openbox xserver-xorg xinit pcmanfm gmrun lxterminal hsetroot unclutter plymouth plymouth-themes`
2. Cloner le dépôt dans le dossier de votre choix : `git clone https://git.arthus.net/arthus/stopi2.git` 2. Cloner le dépôt dans le dossier de votre choix : `git clone https://git.arthus.net/arthus/stopi2.git`
3. Aller dans répertoire du projet : `cd stopi2` 3. Aller dans répertoire du projet : `cd stopi2`
4. Créer un environnement virtuel (venv) Python : `python -m venv ./` 4. Créer un environnement virtuel (venv) Python : `python -m venv ./`
- `pip install -vvv --upgrade pip setuptools wheel` - `pip install -vvv --upgrade pip setuptools wheel`
- (Optionnel) Dans le cas de l'utilisation d'une "raspicam", il faudra ajouter le paramètre `--system-site-packages` pour avoir accès au module picamera2, et installer la librairie correspondante `python3-picamera2`. - (Optionnel) Dans le cas de l'utilisation d'une "raspicam", il faudra ajouter le paramètre `--system-site-packages` pour avoir accès au module picamera2, et installer la librairie correspondante `python3-picamera2`.
5. Activer l'environnement virtuel avec `source bin/activate` 5. Activer l'environnement virtuel avec `source bin/activate`
@ -64,26 +64,26 @@ Dans un terminal :
## Fonction par touches ## Fonction par touches
L'idéal est d'utiliser une télécommande [picote](/arthus/picote) mais le logiciel est aussi pilotable via un clavier/clavier numérique. L'idéal est d'utiliser une télécommande [picote](/arthus/picote) mais le logiciel est aussi pilotable via un clavier/clavier numérique.
| Fonction | Boutton | Clavier | | Fonction | Boutton | Clavier |
| --- | --- | --- | | --- | --- | --- |
| Capturer une image | 🟢 | touche Espace ou 0 sur le clavier numérique | | Capturer une image | 🟢 | touche Espace ou 0 sur le clavier numérique |
| Supprimer une image | 🔴 | touche Suppr, touche Backspace ou touche - sur le clavier numérique | | Supprimer une image | 🔴 | touche Suppr, touche Backspace ou touche - sur le clavier numérique |
| Lecture de l'animation | Alt + 🟢 | touches Entrée | | Lecture de l'animation | Alt + 🟢 | touches Entrée |
| Exporter l'animation | Alt + 🔴 | touche E ou * sur le clavier numérique | | Exporter l'animation | Alt + 🔴 | touche E ou * sur le clavier numérique |
| Image suivante | 🔵 | touche flèche droite ou 6 sur le clavier numérique | | Image suivante | 🔵 | touche flèche droite ou 6 sur le clavier numérique |
| Image précédente | 🟡 | touche flèche gauche ou 4 sur le clavier numérique | | Image précédente | 🟡 | touche flèche gauche ou 4 sur le clavier numérique |
| Aller à la dernière image | Alt + 🔵| touche flèche bas ou 2 sur le clavier numérique | | Aller à la dernière image | Alt + 🔵| touche flèche bas ou 2 sur le clavier numérique |
| Aller à la première image | Alt + 🟡 | touche flèche haut ou 8 sur le clavier numérique | | Aller à la première image | Alt + 🟡 | touche flèche haut ou 8 sur le clavier numérique |
| Activer/Désactiver onionskin | ⚫ | touche O ou / sur le clavier numérique | | Activer/Désactiver onionskin | ⚫ | touche O ou / sur le clavier numérique |
| Quitter le logiciel | n/a | touche Échap, Alt-F4 ou Ctrl-C | | Quitter le logiciel | n/a | touche Échap, Alt-F4 ou Ctrl-C |
| **Réglages de la caméra (compatible showmewebcam seulement)** | | | | **Réglages de la caméra (compatible showmewebcam seulement)** | | |
| Réinitialiser la caméra | Alt + ⚫ | touche R ou 9 sur le clavier numérique | | Réinitialiser la caméra | Alt + ⚫ | touche R ou 9 sur le clavier numérique |
| Changer le mode de balance des blancs | ① | touche W ou 7 sur le clavier numérique | | Changer le mode de balance des blancs | ① | touche W ou 7 sur le clavier numérique |
| Changer le mode d'exposition | Alt + ① | touche X ou 1 sur le clavier numérique | | Changer le mode d'exposition | Alt + ① | touche X ou 1 sur le clavier numérique |
| Afficher seulement le flux vidéo | ② | touche L ou 3 sur le clavier numérique | | Afficher seulement le flux vidéo | ② | touche L ou 3 sur le clavier numérique |
| Rotation de 180° de la capture vidéo | Alt + ② | touche F ou 5 sur le clavier numérique | | Rotation de 180° de la capture vidéo | Alt + ② | touche F ou 5 sur le clavier numérique |
## Installation Kiosque ## Installation Kiosque
@ -94,7 +94,7 @@ Pour créer un kiosque d'animation à partir d'une installation minimale de Debi
2. Suivre les [étapes d'installation](#installation) ci-dessus. 2. Suivre les [étapes d'installation](#installation) ci-dessus.
3. Activer le login automatique de votre utilisateur au démarrage : 3. Activer le login automatique de votre utilisateur au démarrage :
``` ```
sudo systemctl edit getty@tty1.service sudo systemctl edit getty@tty1.service
# Ajout du contenu suivant dans le fichier créé: # Ajout du contenu suivant dans le fichier créé:
[Service] [Service]
ExecStart= ExecStart=
@ -132,7 +132,7 @@ unclutter -idle 0.2 &
/home/$USER/stopi2.sh & /home/$USER/stopi2.sh &
``` ```
Au redémarrage, la session graphique devrait démarrer automatiquement. Au redémarrage, la session graphique devrait démarrer automatiquement.
# Démarrage 'silencieux' # Démarrage 'silencieux'
@ -147,13 +147,13 @@ et modifier la ligne :
en : en :
`GRUB_CMDLINE_LINUX_DEFAULT="quiet splash loglevel=3` `GRUB_CMDLINE_LINUX_DEFAULT="quiet splash loglevel=3`
Puis configurer plymouth : `sudo plymouth-set-default-theme` Puis configurer plymouth : `sudo plymouth-set-default-theme`
Appliquer les modifs avec `sudo update-grub`. Appliquer les modifs avec `sudo update-grub`.
## Raspberry Pi OS ## Raspberry Pi OS
Avec Raspberry Pi OS, il suffit d'ajouter les options suivantes dans '/boot/firmware/cmdline.txt': Avec Raspberry Pi OS, il suffit d'ajouter les options suivantes dans '/boot/firmware/cmdline.txt':
`loglevel=3 vt.global_cursor_default=0 logo.nologo consoleblank=3 quiet` `loglevel=3 vt.global_cursor_default=0 logo.nologo consoleblank=3 quiet`
`` ``

View File

@ -16,11 +16,11 @@ def send_serial_cmd(cam_port, cmd:str, clear=True):
append = b'\r' append = b'\r'
con.write(str.encode(cmd) + append) con.write(str.encode(cmd) + append)
con.close() con.close()
def main(): def main():
cmd = "/usr/bin/v4l2-ctl --all" cmd = "/usr/bin/v4l2-ctl --all"
send_serial_cmd(find_cam_port(), cmd) send_serial_cmd(find_cam_port(), cmd)
if __name__ == '__main__': if __name__ == '__main__':
import sys import sys
sys.exit(main()) sys.exit(main())