Compare commits

...

10 Commits

Author SHA1 Message Date
ABelliqueux 31f02680d9 New playback method 2024-04-09 09:11:32 +02:00
ABelliqueux 3eb3d6054f Fix link 2024-03-29 18:54:52 +01:00
ABelliqueux 4ddfa390ad Add readme 2024-03-29 18:50:09 +01:00
ABelliqueux 420754ed32 Add cache_images option 2024-03-04 16:21:50 +01:00
ABelliqueux cd30da3426 Remove prints, change key mapping 2024-03-01 14:34:28 +01:00
ABelliqueux b795713018 Update l10n 2024-02-27 11:51:59 +01:00
ABelliqueux 588d301f13 Cancel export if esc used 2024-02-27 11:50:28 +01:00
ABelliqueux 1856d42ab4 Video export ask for filename, check cam type != 0 2024-02-27 11:05:22 +01:00
ABelliqueux b6cf074362 Picamera2 support 2024-02-26 18:32:18 +01:00
ABelliqueux bf156cb6e2 Fix settings not being applied 2024-02-23 18:02:48 +01:00
8 changed files with 204 additions and 76 deletions

View File

@ -1,4 +1,5 @@
[DEFAULT] [DEFAULT]
camera_type = 0
file_extension = 'JPG' file_extension = 'JPG'
trigger_mode = 'key' trigger_mode = 'key'
projects_folder = '' projects_folder = ''
@ -13,8 +14,12 @@ framerate = 16
vflip = false vflip = false
hflip = false hflip = false
export_options = 'scale=1920:-1,crop=1920:1080:0:102' export_options = 'scale=1920:-1,crop=1920:1080:0:102'
cache_images = false
[CAMERA] [CAMERA]
# Nikon D40x # Nikon D40x
# Add meter mode to center, focus mode to fixed selection
# /main/capturesettings/autofocusarea to 0
# /main/capturesettings/focusmetermode to 1
capturemode = 3 # use IR remote capturemode = 3 # use IR remote
imagesize = 2 # use size S (1936x1296) imagesize = 2 # use size S (1936x1296)
whitebalance = 1 # Natural light whitebalance = 1 # Natural light

Binary file not shown.

View File

@ -75,3 +75,5 @@ msgstr ""
msgid "Exporting to {}" msgid "Exporting to {}"
msgstr "" msgstr ""
msgid "Mp4 files"
msgstr "Fichier Mp4"

Binary file not shown.

View File

@ -84,3 +84,5 @@ msgstr "Terminaison du processus."
msgid "Exporting to {}" msgid "Exporting to {}"
msgstr "Exportation dans {}" msgstr "Exportation dans {}"
msgid "Mp4 files"
msgstr "Fichier Mp4"

189
main_c.py
View File

@ -30,7 +30,7 @@ from io import BytesIO
from itertools import count from itertools import count
import locale import locale
import os import os
from PIL import Image, ImageTk, ImageFilter, ImageDraw, ImageFont from PIL import Image, ImageTk, ImageFilter, ImageDraw, ImageOps, ImageFont
import sys import sys
import threading import threading
import time import time
@ -60,8 +60,10 @@ from send2trash import send2trash
# X Allow opening and exporting without a camera connected # X Allow opening and exporting without a camera connected
# o Better settings names # o Better settings names
# o webcam support (pygame, win and linux only) # o webcam support (pygame, win and linux only)
# o picam support (picamera2) # X picam support (picamera2)
# o notify export ending # o notify export ending
# o Use try/except for picam and pygame lib import
# o picam liveview
running_from_folder = os.path.realpath(__file__) 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'] 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']
@ -73,6 +75,8 @@ _ = gettext.translation('template', localedir='locales', languages=[LOCALE]).get
# Config # Config
# defaults # defaults
project_settings_defaults = { project_settings_defaults = {
# DSLR = 0, picam = 1, webcam = 2
'camera_type': 0,
'file_extension':'JPG', 'file_extension':'JPG',
'trigger_mode': 'key', 'trigger_mode': 'key',
'projects_folder': '', 'projects_folder': '',
@ -87,6 +91,7 @@ project_settings_defaults = {
'vflip' : False, 'vflip' : False,
'hflip' : False, 'hflip' : False,
'export_options' : 'scale=1920:-1,crop=1920:1080:0:102', 'export_options' : 'scale=1920:-1,crop=1920:1080:0:102',
'cache_images' : False,
} }
# Camera Settings (Nikon D40x) # Camera Settings (Nikon D40x)
@ -102,12 +107,12 @@ for location in config_locations:
if os.path.exists( os.path.expanduser(os.path.join(location, 'config.toml'))): 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: with open(os.path.expanduser(location + 'config.toml'), 'rb') as config_file:
project_settings = tomllib.load(config_file) project_settings = tomllib.load(config_file)
if 'DEFAULT' in project_settings: if 'CHECK' in project_settings:
project_settings = project_settings['DEFAULT'] camera_status = project_settings['CHECK']
if 'CAMERA' in project_settings: if 'CAMERA' in project_settings:
camera_settings = project_settings['CAMERA'] camera_settings = project_settings['CAMERA']
if 'CHECK' in project_settings: if 'DEFAULT' in project_settings:
camera_settings = project_settings['CHECK'] project_settings = project_settings['DEFAULT']
config_found_msg = _("Found configuration file in {}").format(os.path.expanduser(location)) config_found_msg = _("Found configuration file in {}").format(os.path.expanduser(location))
print(config_found_msg) print(config_found_msg)
@ -115,6 +120,8 @@ class KISStopmo(tk.Tk):
def __init__(self, *args, **kargs): def __init__(self, *args, **kargs):
self.check_config() self.check_config()
if project_settings['camera_type'] != 0:
from picamera2 import Picamera2
# Default config # Default config
# Set script settings according to config file # Set script settings according to config file
self.onion_skin = project_settings['onion_skin_onstartup'] self.onion_skin = project_settings['onion_skin_onstartup']
@ -123,9 +130,8 @@ class KISStopmo(tk.Tk):
self.fullscreen_bool = project_settings['fullscreen_bool'] self.fullscreen_bool = project_settings['fullscreen_bool']
self.screen_w, self.screen_h = project_settings['screen_w'], project_settings['screen_h'] self.screen_w, self.screen_h = project_settings['screen_w'], project_settings['screen_h']
self.framerate = project_settings['framerate'] self.framerate = project_settings['framerate']
self.playback = False
# ~ for setting in camera_settings:
# ~ print(setting)
# ~ self.photo = None # ~ self.photo = None
self.end_thread = False self.end_thread = False
# Window setup # Window setup
@ -157,51 +163,70 @@ class KISStopmo(tk.Tk):
self.img_index = self.check_range(len(self.img_list)-1, False) self.img_index = self.check_range(len(self.img_list)-1, False)
self.splash_text = _("No images yet! Start shooting...") self.splash_text = _("No images yet! Start shooting...")
# Camera setup # Camera setup
self.camera = gp.check_result(gp.gp_camera_new()) if project_settings['camera_type'] != 0:
try: try:
gp.check_result(gp.gp_camera_init(self.camera)) self.camera = Picamera2()
# get configuration tree self.picam_conf_full = self.camera.create_still_configuration(main={"size":(1920,1080)}, lores={"size":(800,600)})
self.current_camera_config = gp.check_result(gp.gp_camera_get_config(self.camera)) self.camera.configure(self.picam_conf_full)
self.apply_camera_settings(self.camera, self.current_camera_config) # Autofocus, get lens position and switch to manual mode
if self.check_status(self.camera, self.current_camera_config) is False: # Set Af mode to Manual (1). Default is Continuous (2), Auto is 1
print(_("Warning: Some settings are not set to the recommended value!")) # TODO: lock exposure, wb
except: self.camera.set_controls({'AfMode':1})
self.splash_text += _("\nCamera not found or busy.") self.camera.start(show_preview=False)
self.camera.autofocus_cycle()
self.camera_lenspos = self.camera.capture_metadata()['LensPosition']
self.camera.set_controls({'AfMode':0, 'AwbEnable': False, 'AeEnable': False})
self.camera.stop()
except:
self.camera = False
self.splash_text += _("\nCamera not found or busy.")
else:
self.camera = gp.check_result(gp.gp_camera_new())
try:
gp.check_result(gp.gp_camera_init(self.camera))
# get configuration tree
self.current_camera_config = gp.check_result(gp.gp_camera_get_config(self.camera))
self.apply_camera_settings(self.camera, self.current_camera_config)
if self.check_status(self.camera, self.current_camera_config) is False:
print(_("Warning: Some settings are not set to the recommended value!"))
except:
self.camera = False
self.splash_text += _("\nCamera not found or busy.")
self.timeout = 3000 # milliseconds self.timeout = 3000 # milliseconds
self.splashscreen = self.generate_splashscreen() self.splashscreen = self.generate_splashscreen()
image = self.update_image() self.image = None
if image: self.update_image()
if self.image:
if self.onion_skin: if self.onion_skin:
if self.img_index: if self.img_index:
photo = self.apply_onionskin(image, self.onionskin_alpha_default, self.onionskin_fx) photo = self.apply_onionskin(self.image, self.onionskin_alpha_default, self.onionskin_fx)
else: else:
photo = ImageTk.PhotoImage(image) photo = ImageTk.PhotoImage(self.image)
self.label.configure(image=photo) self.label.configure(image=photo)
self.label.image = photo self.label.image = photo
if project_settings['trigger_mode'] == 'event': if project_settings['trigger_mode'] == 'event':
root.after(1000, self.trigger_bg_loop) root.after(1000, self.trigger_bg_loop)
# ~ root.after(1000, self.wait_for_capture)
# Key binding # Key binding
root.bind("<Escape>", lambda event: root.attributes("-fullscreen", False)) root.bind("<Escape>", lambda event: root.attributes("-fullscreen", False))
root.bind("<f>", lambda event: root.attributes("-fullscreen", True)) root.bind("<f>", lambda event: root.attributes("-fullscreen", True))
root.bind("<n>", self.next_frame) root.bind("<e>", self.next_frame)
root.bind("<b>", self.previous_frame) root.bind("<j>", self.previous_frame)
root.bind("<o>", self.toggle_onionskin) root.bind("<J>", self.toggle_onionskin)
root.bind("<p>", self.preview_animation) root.bind("<N>", self.preview_animation)
root.bind("<e>", self.trigger_export_animation) root.bind("<E>", self.trigger_export_animation)
root.bind("<d>", self.remove_frame) root.bind("<d>", self.remove_frame)
root.bind("<a>", self.print_imglist) root.bind("<a>", self.print_imglist)
if project_settings['trigger_mode'] != 'event': if project_settings['trigger_mode'] != 'event':
root.bind("<j>", self.capture_image) root.bind("<n>", self.capture_image)
def check_config(self): def check_config(self):
global project_settings global project_settings
for setting in project_settings_defaults: for setting in project_settings_defaults:
@ -319,7 +344,6 @@ class KISStopmo(tk.Tk):
existing_animation_files = self.img_list existing_animation_files = self.img_list
# ~ existing_animation_files = # ~ existing_animation_files =
file_list = os.listdir(folder) file_list = os.listdir(folder)
# ~ print(file_list)
for file in file_list: for file in file_list:
if (file.startswith(self.project_letter) and file.endswith(project_settings['file_extension'])): if (file.startswith(self.project_letter) and file.endswith(project_settings['file_extension'])):
if file not in existing_animation_files: if file not in existing_animation_files:
@ -353,7 +377,6 @@ class KISStopmo(tk.Tk):
frame_list = self.get_frames_list(folder) frame_list = self.get_frames_list(folder)
counter = (".%04i." % x for x in count(0)) counter = (".%04i." % x for x in count(0))
# ~ for i in range(len(frame_list)): # ~ for i in range(len(frame_list)):
# ~ print(frame_list)
for i in frame_list.keys(): for i in frame_list.keys():
# ~ if os.path.exists(os.path.realpath(frame_list[i])): # ~ if os.path.exists(os.path.realpath(frame_list[i])):
if os.path.exists(os.path.join(folder, i)): if os.path.exists(os.path.join(folder, i)):
@ -385,7 +408,6 @@ class KISStopmo(tk.Tk):
if not os.path.exists(frame_path): if not os.path.exists(frame_path):
return 0 return 0
# ~ print(self.img_list)
print(_("Removing {}").format(frame_path)) print(_("Removing {}").format(frame_path))
# trash file # trash file
send2trash(frame_path) send2trash(frame_path)
@ -395,15 +417,10 @@ class KISStopmo(tk.Tk):
self.offset_dictvalues(self.img_index) self.offset_dictvalues(self.img_index)
# rename files and get new list # rename files and get new list
self.img_list = self.batch_rename(folder_path) self.img_list = self.batch_rename(folder_path)
print(self.img_list)
self.clean_img_list(folder_path) self.clean_img_list(folder_path)
# ~ print(self.img_list)
# update index if possible # update index if possible
# ~ print(self.img_index)
self.img_index = self.check_range(self.img_index, False) self.img_index = self.check_range(self.img_index, False)
# ~ print(self.img_index)
# update display # update display
# ~ print(self.img_index)
self.update_image(None, self.img_index) self.update_image(None, self.img_index)
# ~ def open_jpg(self, filepath:str, w:int, h:int): # ~ def open_jpg(self, filepath:str, w:int, h:int):
@ -411,28 +428,22 @@ class KISStopmo(tk.Tk):
# If pic not cached # If pic not cached
if filetuple[-1] is None: if filetuple[-1] is None:
try: try:
# ~ image = Image.open(filepath)
# ~ print( filetuple[0] + "is not in cache")
image = Image.open(os.path.join(self.savepath, filetuple[0])) image = Image.open(os.path.join(self.savepath, filetuple[0]))
image = image.resize((w, h)) image = ImageOps.fit(image, (w, h))
if vflip: if vflip:
image = image.transpose(Image.Transpose.FLIP_TOP_BOTTOM) image = image.transpose(Image.Transpose.FLIP_TOP_BOTTOM)
if hflip: if hflip:
image = image.transpose(Image.Transpose.FLIP_LEFT_RIGHT) image = image.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
self.img_list[filetuple[0]] = image if project_settings['cache_images']:
# TODO : Do not cache image to preserve memory
self.img_list[filetuple[0]] = image
except FileNotFoundError: except FileNotFoundError:
return False return False
else: else:
# ~ print( filetuple[0] + "is in cache") if project_settings['cache_images']:
image = filetuple[-1] image = filetuple[-1]
return image return image
# ~ def inc(self, x):
# ~ if x >= len(self.img_list)-1:
# ~ return 0
# ~ else:
# ~ return x + 1
def check_range(self, x, loop=True): def check_range(self, x, loop=True):
if x < 0: if x < 0:
@ -465,6 +476,7 @@ class KISStopmo(tk.Tk):
def update_image(self, event=None, index=None): def update_image(self, event=None, index=None):
# TODO : check event mode still works
if event is not None: if event is not None:
self.img_index = self.check_range(self.img_index+1) self.img_index = self.check_range(self.img_index+1)
if index is None: if index is None:
@ -484,10 +496,12 @@ class KISStopmo(tk.Tk):
if self.onion_skin: if self.onion_skin:
if index: if index:
photo = self.apply_onionskin(new_image, project_settings['onionskin_alpha_default'], project_settings['onionskin_fx']) photo = self.apply_onionskin(new_image, project_settings['onionskin_alpha_default'], project_settings['onionskin_fx'])
# ~ print(photo)
self.label.configure(image=photo) self.label.configure(image=photo)
self.label.image = photo self.label.image = photo
return new_image # ~ return new_image
self.image = new_image
# ~ new_image.close()
return True
else: else:
return False return False
@ -502,17 +516,31 @@ class KISStopmo(tk.Tk):
self.update_image(None, self.img_index) self.update_image(None, self.img_index)
def playback_animation(self):
while self.img_index < len(self.img_list)-1:
self.img_index += 1
root.after(62, self.update_image(None, self.img_index))
root.update_idletasks()
def preview_animation(self, event): def preview_animation(self, event):
# save OS state # save OS state
if self.onion_skin: if self.onion_skin:
self.onion_skin = False self.onion_skin = False
onion_skin_was_on = True onion_skin_was_on = True
# playback # playback
# TODO : Use async function for playback
# ~ self.playback = not self.playback
# ~ self.img_index = 0
# ~ self.playback_animation()
for img in self.img_list: for img in self.img_list:
# ~ self.update_image(None, self.img_list.index(img)) # ~ self.update_image(None, self.img_list.index(img))
# ~ if self.playback:
self.update_image(None, list(self.img_list.keys()).index(img)) self.update_image(None, list(self.img_list.keys()).index(img))
root.update_idletasks() root.update_idletasks()
time.sleep(1/self.framerate) time.sleep(1/self.framerate)
# ~ else:
# ~ break
# ~ self.update_image(None, self.img_index) # ~ self.update_image(None, self.img_index)
self.display_last_frame() self.display_last_frame()
# restore OS state # restore OS state
@ -632,31 +660,32 @@ class KISStopmo(tk.Tk):
print(speed) print(speed)
print(str(current_lightmeter_value) + " - " + str(current_shutterspeed_value)) print(str(current_lightmeter_value) + " - " + str(current_shutterspeed_value))
return True return True
# ~ print(str(current_lightmeter_value) + " - " + str(current_shutterspeed_value))
def capture_image(self, event=None, event_data=None): def capture_image(self, event=None, event_data=None):
# get net file name based on prefix, file count and extension # get net file name based on prefix, file count and extension
next_filename = self.return_next_frame_number(self.get_last_frame(self.savepath)) next_filename = self.return_next_frame_number(self.get_last_frame(self.savepath))
# build full path to file # build full path to file
target_path = os.path.join(self.savepath, next_filename) target_path = os.path.join(self.savepath, next_filename)
# ~ print(target_path) if project_settings['camera_type'] != 0:
# Get file from camera self.camera.start(show_preview=False)
if event_data is None: self.camera.set_controls({"LensPosition": self.camera_lenspos})
# ~ print("j pressed") self.camera.capture_file(target_path, 'main', format='jpeg')
new_frame_path = self.camera.capture(gp.GP_CAPTURE_IMAGE) self.camera.stop()
# ~ self.camera.trigger_capture()
new_frame = self.camera.file_get(
new_frame_path.folder, new_frame_path.name, gp.GP_FILE_TYPE_NORMAL)
print(_("Saving {}{}").format(new_frame_path.folder, new_frame_path.name))
else: else:
print(_("Getting file {}").format(event_data.name)) # Get file from DSLR camera
new_frame = self.camera.file_get( if event_data is None:
event_data.folder, event_data.name, gp.GP_FILE_TYPE_NORMAL) new_frame_path = self.camera.capture(gp.GP_CAPTURE_IMAGE)
new_frame.save(target_path) # ~ self.camera.trigger_capture()
new_frame = self.camera.file_get(
new_frame_path.folder, new_frame_path.name, gp.GP_FILE_TYPE_NORMAL)
print(_("Saving {}{}").format(new_frame_path.folder, new_frame_path.name))
else:
print(_("Getting file {}").format(event_data.name))
new_frame = self.camera.file_get(
event_data.folder, event_data.name, gp.GP_FILE_TYPE_NORMAL)
new_frame.save(target_path)
# ~ next_filename = prefix+next(filename)+ext # ~ next_filename = prefix+next(filename)+ext
# ~ print(self.img_list)
if '{letter}.-001.JPG'.format(letter=self.project_letter) in self.img_list: if '{letter}.-001.JPG'.format(letter=self.project_letter) in self.img_list:
# ~ self.img_list.pop('A.-001.JPG')
self.img_list.pop('{letter}.-001.JPG'.format(letter=self.project_letter)) self.img_list.pop('{letter}.-001.JPG'.format(letter=self.project_letter))
# Display new frame # Display new frame
self.display_last_frame() self.display_last_frame()
@ -668,7 +697,6 @@ class KISStopmo(tk.Tk):
return return
event_type, event_data = self.camera.wait_for_event(self.timeout) event_type, event_data = self.camera.wait_for_event(self.timeout)
if event_type == gp.GP_EVENT_FILE_ADDED: if event_type == gp.GP_EVENT_FILE_ADDED:
# ~ print("file added")
self.capture_image(None, event_data) self.capture_image(None, event_data)
# ~ root.after(self.timeout, self.wait_for_capture) # ~ root.after(self.timeout, self.wait_for_capture)
self.wait_for_capture() self.wait_for_capture()
@ -683,7 +711,8 @@ class KISStopmo(tk.Tk):
self.input_filename, self.input_filename,
self.input_options) self.input_options)
.output( .output(
self.output_filename, # ~ self.output_filename,
self.export_filename,
self.output_options self.output_options
) )
) )
@ -691,14 +720,22 @@ class KISStopmo(tk.Tk):
await ffmpeg.execute() await ffmpeg.execute()
def trigger_export_animation(self, event): def trigger_export_animation(self, event):
output_folder = filedialog.askdirectory() # ~ output_folder = filedialog.askdirectory()
self.output_filename = "{folder}{sep}{filename}".format(folder=output_folder, sep=os.sep, filename=self.output_filename) self.export_filename = ()
print(_("Exporting to {}").format(self.output_filename)) self.export_filename = filedialog.asksaveasfilename(defaultextension='.mp4', filetypes=((_("Mp4 files"), '*.mp4'),))
if not self.export_filename:
# ~ self.export_filename = "{folder}{sep}{filename}".format(folder=os.getcwd(), sep=os.sep, filename=self.output_filename)
return False
print(_("Exporting to {}").format(self.export_filename))
self.export_task = asyncio.run(self.export_animation()) self.export_task = asyncio.run(self.export_animation())
# check with self.export_task.done() == True ? https://stackoverflow.com/questions/69350645/proper-way-to-retrieve-the-result-of-tasks-in-asyncio # check with self.export_task.done() == True ? https://stackoverflow.com/questions/69350645/proper-way-to-retrieve-the-result-of-tasks-in-asyncio
def end_bg_loop(KISStopmo): def end_bg_loop(KISStopmo):
KISStopmo.camera.exit() if KISStopmo.camera is not False:
if project_settings['camera_type'] != 0:
KISStopmo.camera.stop()
else:
KISStopmo.camera.exit()
KISStopmo.end_thread = True KISStopmo.end_thread = True
root = tk.Tk() root = tk.Tk()

78
readme.md Normal file
View File

@ -0,0 +1,78 @@
# Stopi
A python stop-motion script that keeps things simple and allows to use a DSLR or raspicam for capture.
* Full screen display of the last picture taken
* Optional Onion Skinning with the second to last picture
* No gui : everything is done with keyboard keys (or a [homemade rpi pico based remote](https://forge.aquilenet.fr/benjamin.duchenne/picoti)) and a config file
* Auto configuration of the DSLR on startup
* Key or event mode ; use your DSLR's trigger button or IR remote and images are automatically downloaded after capture
* Preview playback
* Full HD export with ffmpeg
* Uses translations
## Setup
0. (Windows users only) Setup WSL2 on your (P)OS and install a Debian based distro (Debian, Mint, Ubuntu...)
1. Install dependencides : `sudo apt install --no-install-recommends --no-install-suggests git ffmpeg gphoto2 python3-libcamera python3-picamera2 python3-tk`
(Optional) If you want a minimal graphical environment : `sudo apt install --no-install-recommends --no-install-suggests openbox xserver-xorg xinit pcmanfm gmrun lxterminal hsetroot unclutter`
2. Clone the repo : `git clone https://`
3. Change to directory : `cd stopimotion`
4. Create Python venv : `python -m venv ./` (If planning to use a raspicam, you need to also pass the `--system-site-packages` parameter to be able to import the GPIO module.)
5. Install dependencies : `pip install -r requirements.txt`
6. Plug your DSLR/setup your raspicam
7. Set Execution bit on script : `chmod +x stopi.sh`
8. Launch script : `./stopi.sh`
## Todo / Fix me
* UI freezes when exporting
* Better settings names ; currently they're kinda cryptic and sucky.
* Notify ffmpeg export ending
## Planned features
* Liveview (when I get a DSLR that supports it :))
* Webcam support (e.g; using pygame)
## Raspberry Pi image
For convenience, a disk image is available here for RPI users.
For advanced users, the steps for preparing RaspiOS for a minimal kiosk-like experience are these :
1. Flash Raspi OS bookworm lite version to a SD card, enabling SSH, Wifi, etc if needed.
2. Follow the steps in the 'Setup' section above.
3. Use the 'raspi-config' utility to enable console auto-login.
4. Add this content to '~/.bash_login' :
```
if [[ -z $DISPLAY ]] && [[ $(tty) = /dev/tty1 ]]; then
startx
fi
```
5. Add this content to '~/.xinitrc' :
```
#!/bin/sh
# /etc/X11/xinit/xinitrc
#
# global xinitrc file, used by all X sessions started by xinit (startx)
# invoke global X session script
. /etc/X11/Xsession
exec openbox-session
```
6. Add this content to '~/.config/openbox/autostart.sh' :
```
#!/bin/env bash
# Change X keyboard mapping
setxkbmap fr
# Set background color
hsetroot -solid "#8393CC"
# Hide mouse after 0.2 seconds
unclutter -idle 0.2 &
# Start script
/home/$USER/stopi.sh &
```
When you reboot, the X session should launch automatically, and then the script.

4
stopi.sh Executable file
View File

@ -0,0 +1,4 @@
#!/bin/env bash
cd "$(dirname "$0")"
source bin/activate
python main_c.py