Compare commits

...

4 Commits

Author SHA1 Message Date
ABelliqueux 0d8434136a Add anti-flicker control 2025-01-12 10:29:04 +01:00
ABelliqueux 6219e18b96 Add focus control for picamera modules 2025-01-11 14:54:17 +01:00
ABelliqueux 913a4b7eb6 Switch to JPG format 2025-01-04 11:51:16 +01:00
ABelliqueux 0bf7fb9d98 Add pi camera v3 support 2024-12-15 12:28:40 +01:00
4 changed files with 204 additions and 68 deletions

View File

@ -1,21 +1,23 @@
[DEFAULT]
cam_is_showmewebcam = true
cam_is_picam = true
cam_is_showmewebcam = false
use_date_for_folder = false
file_extension = 'png'
file_extension = 'jpg'
jpg_quality = 88
projects_folder = ''
onion_skin_onstartup = true
onionskin_alpha_default = 0.5
fullscreen_bool = true
screen_w = 1920
screen_h = 1080
screen_w = 1440
screen_h = 900
framerate = 16
ffmpeg_path = '/usr/bin/ffmpeg'
export_options = 'scale=1920:-1,crop=1920:1080'
[CAMERA]
cam_w = 1600
cam_h = 900
vflip = 1
hflip = 1
cam_w = 1920
cam_h = 1080
vflip = 0
hflip = 0
auto_exposure = 1
white_balance_auto_preset = 2
white_balance_auto_preset = 1
video_bitrate=25000000

View File

@ -1,3 +1,4 @@
#!/bin/env python
import cv2
import gettext
from itertools import count
@ -14,6 +15,10 @@ import tomllib
import numpy as np
import serialutils
# Run from SSH
if not os.getenv('DISPLAY'):
os.putenv('DISPLAY', ':0')
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']
@ -25,9 +30,11 @@ _ = gettext.translation('template', localedir='locales', languages=[LOCALE]).get
# Config
# defaults
project_settings_defaults = {
'cam_is_picam': True,
'cam_is_showmewebcam': False,
'use_date_for_folder': False,
'file_extension':'png',
'jpg_quality':90,
'projects_folder': '',
'onion_skin_onstartup' : False,
'onionskin_alpha_default' : 0.4,
@ -65,13 +72,22 @@ for location in config_locations:
config_found_msg = _("Found configuration file in {}").format(os.path.expanduser(location))
print(config_found_msg)
camera_current_settings = {
if project_settings['cam_is_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']),
}
else: # cam is 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),
}
def apply_cam_setting(cam_settings:dict, to_set:list=None):
@ -113,7 +129,7 @@ def generate_text_image(text:str, screen_w, screen_h, bullets=False):
)
text_image_draw = ImageDraw.Draw(text_image)
if text is not None:
font = ImageFont.truetype("Tuffy_Bold.ttf", (screen_w/32))
font = ImageFont.truetype("Tuffy_Bold.ttf", int(screen_w/32))
lines = text.split('\n')
longest_line = lines[0]
for line in lines:
@ -435,12 +451,45 @@ def main(args):
global onionskin, liveview_only, playback, loop_playback, playhead, index, img_list, first_playback, camera_current_settings
if not project_settings['cam_is_picam']:
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'])
else:
# 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()
frame = get_onionskin_frame(savepath, index)
@ -480,10 +529,13 @@ def main(args):
if liveview_only:
# ~ onionskin = False
if not project_settings['cam_is_picam']:
ret, overlay = cam.read()
if not ret:
print(_("Failed to grab frame."))
break
else:
overlay = cam.capture_array("main")
# Resize preview
overlay = cv2.resize(overlay, (project_settings['screen_w'], project_settings['screen_h']))
cv2.imshow("StopiCV", overlay)
@ -491,7 +543,13 @@ def main(args):
# ~ onionskin = True
if onionskin:
if not project_settings['cam_is_picam']:
ret, overlay = cam.read()
if not ret:
print(_("Failed to grab frame."))
break
else:
overlay = cam.capture_array("main")
og_frame = overlay.copy()
# Resize preview
overlay = cv2.resize(overlay, (project_settings['screen_w'], project_settings['screen_h']))
@ -499,81 +557,153 @@ def main(args):
alpha = project_settings['onionskin_alpha_default']
beta = (1.0 - alpha)
overlay = cv2.addWeighted(frame, alpha, overlay, beta, 0)
if not ret:
print(_("Failed to grab frame."))
break
cv2.imshow("StopiCV", overlay)
if not playback and not onionskin and not liveview_only:
cv2.imshow("StopiCV", frame)
k = cv2.waitKey(1)
# TODO : Show liveview only
# Key l / 5
if (k%256 == 108) or (k%256 == 53):
# Key l / kp 5
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
# Key o / 9
if (k%256 == 111) or (k%256 == 47):
# 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
# Key w / 7 - cycle wb
if (k%256 == 119) or (k%256 == 55):
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_is_picam']:
cam.set_controls({'AwbMode': camera_current_settings['white_balance_auto_preset']['value']})
# Key x / 1 - cycle exposure
if (k%256 == 120) or (k%256 == 49):
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'])
if project_settings['cam_is_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']})
# Key f / 3 - flip image
if (k%256 == 102) or (k%256 == 51):
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'])
# Key up
elif (k%256 == 82) or (k%256 == 56):
if project_settings['cam_is_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()
# Key up, kp 8
elif (k%256 == 82) or (k%256 == 56) or (k%256 == 184):
print(_("Last frame"))
if len(img_list):
if playback:
playback = False
index, frame = last_frame(index)
# Key down
elif (k%256 == 84) or (k%256 == 50):
# Key down , kp 2
elif (k%256 == 84) or (k%256 == 50) or (k%256 == 178):
print(_("First frame"))
if len(img_list):
if playback:
playback = False
index, frame = first_frame(index)
# Key left
elif (k%256 == 81) or (k%256 == 52):
# Key left, kp 4
elif (k%256 == 81) or (k%256 == 52) or (k%256 == 180):
print(_("Prev. frame"))
# Displau previous frame
if len(img_list):
if playback:
playback = False
index, frame = previous_frame(index)
# Key right
elif (k%256 == 83) or (k%256 == 54):
# Key right, kp 6
elif (k%256 == 83) or (k%256 == 54) or (k%256 == 182):
print(_("Next frame"))
# Displau next frame
if len(img_list):
if playback:
playback = False
index, frame = next_frame(index)
# Key r / keypad 9 - reset wb,exp
elif (k%256 == 114) or (k%256 == 57):
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)
if project_settings['cam_is_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()
# Key e / keypad *
elif (k%256 == 101) or (k%256 == 42):
elif (k%256 == 101) or (k%256 == 42) or (k%256 == 170) :
print(_("Export"))
ffmpeg_process = export_animation(input_filename, export_filename)
# Key Return
elif (k%256 == 13):
# Key Return, kp return
elif (k%256 == 13) or (k%256 == 141) :
print(_("Playback"))
playhead = index
loop_playback = True
playback = not playback
# Key remove frame - backspace, del, numpad_minus
elif (k%256 == 8) or (k%256 == 45) or (k == 255):
elif (k%256 == 8) or (k%256 == 45) or (k == 255) or (k%256 == 173) :
# Remove frame
print(_("Remove frame"))
img_list, index, frame = remove_frame(img_list, index)
# TODO: replace keys with rotary encoder
# Focus +/- with a,z
elif (k%256 == 97) and project_settings['cam_is_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_is_picam']:
cam_lenspos -= 0.2
# Set AfMode to Manual
cam.set_controls({'AfMode': 0, 'LensPosition': cam_lenspos})
print(_("-Lens pos: {}".format(cam_lenspos)))
# Set anti-flicker mode with q
elif (k%256 == 113) and project_settings['cam_is_picam']:
# Set AfMode to Manual
camera_current_settings = apply_cam_setting(camera_current_settings, ['anti_flicker'])
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'])
# ~ elif (k%256 == 115) and project_settings['cam_is_picam']:
# ~ # Set AfMode to Manual
# ~ cam.set_controls({'AeFlickerMode': 0, 'AeFlickerPeriod': 8333})
# Take pic
# 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['file_extension'] == 'jpg':
cv2.imwrite(img_path, og_frame, [int(cv2.IMWRITE_JPEG_QUALITY), project_settings['jpg_quality']])
else:
cv2.imwrite(img_path, og_frame)
print(_("File {} written.").format(img_path))
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
frame = get_onionskin_frame(savepath, index)
# Quit app
elif k%256 == 27:
# ESC pressed
@ -582,21 +712,10 @@ def main(args):
elif ctrlc_pressed:
print(_("Ctrl-C hit, exiting..."))
break
elif cv2.getWindowProperty("StopiCV", cv2.WND_PROP_VISIBLE) < 1:
elif cv2.getWindowProperty("StopiCV", cv2.WND_PROP_AUTOSIZE) == -1:
print(_("Window was closed, exiting..."))
# ~ pass
break
# Take pic
# SPACE or numpad 0 pressed
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))
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
frame = get_onionskin_frame(savepath, index)
# REMOVE : Debug print keycode
elif k==-1: # normally -1 returned,so don't print it
continue
@ -619,7 +738,10 @@ def main(args):
except:
print(_("Terminating running process..."))
ffmpeg_process.terminate()
if not project_settings["cam_is_picam"]:
cam.release()
else:
cam.close()
cv2.destroyAllWindows()
cv2.namedWindow("StopiCV", cv2.WINDOW_GUI_NORMAL)

View File

@ -1,10 +1,15 @@
# Stopi2
## Branche libcamera
**Ceci est la branche qui restaure la possibilité d'utiliser des périphériques compatibles rpi-libcamera (Modules Raspicam v1,v2 et v3).**
**En utilisant la [branche correspondante pour la télécommande picote](/arthus/picote/src/branch/picamera), vous pouvez régler la mise au point du module caméra avec un [codeur rotatif](https://fr.wikipedia.org/wiki/Codeur_rotatif).**
<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).
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 ne fonctionne pour le moment qu'avec une webcam.
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 :
* Affichage des images en plein écran sans interface : toutes les fonctions utilisent quelques touches du clavier.
@ -17,7 +22,7 @@ Encore une fois, l'objectif est de créer un logiciel simple et minimaliste dans
## Banc de test
Ce script a été testé avec une webcam compatible V4L2, et plus précisement 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).
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/).
Les contributions et rapports de bugs sont les bienvenus !
## Installation
@ -28,7 +33,7 @@ Dans un terminal :
1. Installer les dépendances suivantes :
```
# Avec une distribution basée sur Debian (Ubuntu, Mint...)
sudo apt install --no-install-recommends --no-install-suggests git ffmpeg python3-tk python3-pip python3-venv libtiff5-dev libtopenjp2 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
sudo apt install --no-install-recommends --no-install-suggests git ffmpeg python3-tk python3-pip python3-venv libtiff5-dev libtopenjp2 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
```
- (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`
@ -128,3 +133,10 @@ en :
Puis configurer plymouth : `sudo plymouth-set-default-theme`
Appliquer les modifs avec `sudo update-grub`.
## Raspberry Pi OS
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`
``

View File

@ -1,5 +1,5 @@
Send2Trash
opencv-python
numpy
pyserial
pillow
opencv-python