Removed files from previous version
This commit is contained in:
133
camera.py
133
camera.py
@@ -1,133 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# Created by br@re-web.eu, 2015
|
||||
|
||||
import subprocess
|
||||
|
||||
cv_enabled = False
|
||||
gphoto2cffi_enabled = False
|
||||
piggyphoto_enabled = False
|
||||
|
||||
try:
|
||||
import cv2 as cv
|
||||
cv_enabled = True
|
||||
print('OpenCV available')
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
try:
|
||||
import gphoto2cffi as gp
|
||||
gpExcept = gp.errors.GPhoto2Error
|
||||
gphoto2cffi_enabled = True
|
||||
print('Gphoto2cffi available')
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
if not gphoto2cffi_enabled:
|
||||
try:
|
||||
import piggyphoto as gp
|
||||
gpExcept = gp.libgphoto2error
|
||||
piggyphoto_enabled = True
|
||||
print('Piggyphoto available')
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
class CameraException(Exception):
|
||||
"""Custom exception class to handle camera class errors"""
|
||||
def __init__(self, message, recoverable=False):
|
||||
self.message = message
|
||||
self.recoverable = recoverable
|
||||
|
||||
|
||||
class Camera_cv:
|
||||
def __init__(self, picture_size):
|
||||
if cv_enabled:
|
||||
self.cap = cv.VideoCapture(0)
|
||||
self.cap.set(3, picture_size[0])
|
||||
self.cap.set(4, picture_size[1])
|
||||
|
||||
def has_preview(self):
|
||||
return True
|
||||
|
||||
def take_preview(self, filename="/tmp/preview.jpg"):
|
||||
self.take_picture(filename)
|
||||
|
||||
def take_picture(self, filename="/tmp/picture.jpg"):
|
||||
if cv_enabled:
|
||||
r, frame = self.cap.read()
|
||||
cv.imwrite(filename, frame)
|
||||
return filename
|
||||
else:
|
||||
raise CameraException("OpenCV not available!")
|
||||
|
||||
def set_idle(self):
|
||||
pass
|
||||
|
||||
|
||||
class Camera_gPhoto:
|
||||
"""Camera class providing functionality to take pictures using gPhoto 2"""
|
||||
|
||||
def __init__(self, picture_size):
|
||||
self.picture_size = picture_size
|
||||
# Print the capabilities of the connected camera
|
||||
try:
|
||||
if gphoto2cffi_enabled:
|
||||
self.cap = gp.Camera()
|
||||
elif piggyphoto_enabled:
|
||||
self.cap = gp.camera()
|
||||
print(self.cap.abilities)
|
||||
else:
|
||||
print(self.call_gphoto("-a", "/dev/null"))
|
||||
except CameraException as e:
|
||||
print('Warning: Listing camera capabilities failed (' + e.message + ')')
|
||||
except gpExcept as e:
|
||||
print('Warning: Listing camera capabilities failed (' + e.message + ')')
|
||||
|
||||
def call_gphoto(self, action, filename):
|
||||
# Try to run the command
|
||||
try:
|
||||
cmd = "gphoto2 --force-overwrite --quiet " + action + " --filename " + filename
|
||||
output = subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT)
|
||||
if "ERROR" in output:
|
||||
raise subprocess.CalledProcessError(returncode=0, cmd=cmd, output=output)
|
||||
except subprocess.CalledProcessError as e:
|
||||
if "EOS Capture failed: 2019" in e.output or "Perhaps no focus" in e.output:
|
||||
raise CameraException("Can't focus!\nMove a little bit!", True)
|
||||
elif "No camera found" in e.output:
|
||||
raise CameraException("No (supported) camera detected!", False)
|
||||
elif "command not found" in e.output:
|
||||
raise CameraException("gPhoto2 not found!", False)
|
||||
else:
|
||||
raise CameraException("Unknown error!\n" + '\n'.join(e.output.split('\n')[1:3]), False)
|
||||
return output
|
||||
|
||||
def has_preview(self):
|
||||
return gphoto2cffi_enabled or piggyphoto_enabled
|
||||
|
||||
def take_preview(self, filename="/tmp/preview.jpg"):
|
||||
if gphoto2cffi_enabled:
|
||||
self._save_picture(filename, self.cap.get_preview())
|
||||
elif piggyphoto_enabled:
|
||||
self.cap.capture_preview(filename)
|
||||
else:
|
||||
raise CameraException("No preview supported!")
|
||||
|
||||
def take_picture(self, filename="/tmp/picture.jpg"):
|
||||
if gphoto2cffi_enabled:
|
||||
self._save_picture(filename, self.cap.capture())
|
||||
elif piggyphoto_enabled:
|
||||
self.cap.capture_image(filename)
|
||||
else:
|
||||
self.call_gphoto("--capture-image-and-download", filename)
|
||||
return filename
|
||||
|
||||
def _save_picture(self, filename, data):
|
||||
f = open(filename, 'wb')
|
||||
f.write(data)
|
||||
f.close()
|
||||
|
||||
def set_idle(self):
|
||||
if gphoto2cffi_enabled:
|
||||
self.cap._get_config()['actions']['viewfinder'].set(False)
|
||||
elif piggyphoto_enabled:
|
||||
# This doesn't work...
|
||||
self.cap.config.main.actions.viewfinder.value = 0
|
||||
49
events.py
49
events.py
@@ -1,49 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# Created by br _at_ re-web _dot_ eu, 2015
|
||||
|
||||
try:
|
||||
import RPi.GPIO as GPIO
|
||||
gpio_enabled = True
|
||||
except ImportError:
|
||||
gpio_enabled = False
|
||||
|
||||
|
||||
class Event:
|
||||
def __init__(self, type, value):
|
||||
"""type 0: quit
|
||||
1: keystroke
|
||||
2: mouseclick
|
||||
3: gpio
|
||||
"""
|
||||
self.type = type
|
||||
self.value = value
|
||||
|
||||
class Rpi_GPIO:
|
||||
def __init__(self, handle_function, input_channels = [], output_channels = []):
|
||||
if gpio_enabled:
|
||||
# Display initial information
|
||||
print("Your Raspberry Pi is board revision " + str(GPIO.RPI_INFO['P1_REVISION']))
|
||||
print("RPi.GPIO version is " + str(GPIO.VERSION))
|
||||
|
||||
# Choose BCM numbering system
|
||||
GPIO.setmode(GPIO.BCM)
|
||||
|
||||
# Setup the input channels
|
||||
for input_channel in input_channels:
|
||||
GPIO.setup(input_channel, GPIO.IN, pull_up_down=GPIO.PUD_UP)
|
||||
GPIO.add_event_detect(input_channel, GPIO.RISING, callback=handle_function, bouncetime=200)
|
||||
|
||||
# Setup the output channels
|
||||
for output_channel in output_channels:
|
||||
GPIO.setup(output_channel, GPIO.OUT)
|
||||
GPIO.output(output_channel, GPIO.LOW)
|
||||
else:
|
||||
print("Warning: RPi.GPIO could not be loaded. GPIO disabled.")
|
||||
|
||||
def teardown(self):
|
||||
if gpio_enabled:
|
||||
GPIO.cleanup()
|
||||
|
||||
def set_output(self, channel, value=0):
|
||||
if gpio_enabled:
|
||||
GPIO.output(channel, GPIO.HIGH if value==1 else GPIO.LOW)
|
||||
245
gui.py
245
gui.py
@@ -1,245 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# Created by br _at_ re-web _dot_ eu, 2015
|
||||
|
||||
from __future__ import division
|
||||
|
||||
import pygame
|
||||
|
||||
try:
|
||||
import pygame.fastevent as EventModule
|
||||
except ImportError:
|
||||
import pygame.event as EventModule
|
||||
|
||||
from events import Event
|
||||
|
||||
class GuiException(Exception):
|
||||
"""Custom exception class to handle GUI class errors"""
|
||||
|
||||
class GUI_PyGame:
|
||||
"""A GUI class using PyGame"""
|
||||
|
||||
def __init__(self, name, size, hide_mouse=True):
|
||||
# Call init routines
|
||||
pygame.init()
|
||||
if hasattr(EventModule, 'init'):
|
||||
EventModule.init()
|
||||
|
||||
# Window name
|
||||
pygame.display.set_caption(name)
|
||||
|
||||
# Hide mouse cursor
|
||||
if hide_mouse:
|
||||
pygame.mouse.set_cursor(*pygame.cursors.load_xbm('transparent.xbm','transparent.msk'))
|
||||
|
||||
# Store screen and size
|
||||
self.size = size
|
||||
self.screen = pygame.display.set_mode(size, pygame.FULLSCREEN)
|
||||
|
||||
# Clear screen
|
||||
self.clear()
|
||||
self.apply()
|
||||
|
||||
def clear(self, color=(0,0,0)):
|
||||
self.screen.fill(color)
|
||||
self.surface_list = []
|
||||
|
||||
def apply(self):
|
||||
for surface in self.surface_list:
|
||||
self.screen.blit(surface[0], surface[1])
|
||||
pygame.display.update()
|
||||
|
||||
def get_size(self):
|
||||
return self.size
|
||||
|
||||
def trigger_event(self, event_channel):
|
||||
EventModule.post(EventModule.Event(pygame.USEREVENT, channel=event_channel))
|
||||
|
||||
def show_picture(self, filename, size=(0,0), offset=(0,0), flip=False):
|
||||
# Use window size if none given
|
||||
if size == (0,0):
|
||||
size = self.size
|
||||
try:
|
||||
# Load image from file
|
||||
image = pygame.image.load(filename)
|
||||
except pygame.error as e:
|
||||
raise GuiException("ERROR: Can't open image '" + filename + "': " + e.message)
|
||||
# Extract image size and determine scaling
|
||||
image_size = image.get_rect().size
|
||||
image_scale = min([min(a,b)/b for a,b in zip(size, image_size)])
|
||||
# New image size
|
||||
new_size = [int(a*image_scale) for a in image_size]
|
||||
# Update offset
|
||||
offset = tuple(a+int((b-c)/2) for a,b,c in zip(offset, size, new_size))
|
||||
# Apply scaling and display picture
|
||||
image = pygame.transform.scale(image, new_size).convert()
|
||||
# Create surface and blit the image to it
|
||||
surface = pygame.Surface(new_size)
|
||||
surface.blit(image, (0,0))
|
||||
if flip:
|
||||
surface = pygame.transform.flip(surface, True, False)
|
||||
self.surface_list.append((surface, offset))
|
||||
|
||||
def show_message(self, msg, color=(0,0,0), bg=(230,230,230), transparency=True, outline=(245,245,245)):
|
||||
# Choose font
|
||||
font = pygame.font.Font(None, 144)
|
||||
# Wrap and render text
|
||||
wrapped_text, text_height = self.wrap_text(msg, font, self.size)
|
||||
rendered_text = self.render_text(wrapped_text, text_height, 1, 1, font, color, bg, transparency, outline)
|
||||
|
||||
self.surface_list.append((rendered_text, (0,0)))
|
||||
|
||||
def show_button(self, text, pos, size=(0,0), color=(230,230,230), bg=(0,0,0), transparency=True, outline=(230,230,230)):
|
||||
# Choose font
|
||||
font = pygame.font.Font(None, 72)
|
||||
text_size = font.size(text)
|
||||
if size == (0,0):
|
||||
size = (text_size[0] + 4, text_size[1] + 4)
|
||||
offset = ( (size[0] - text_size[0]) // 2, (size[1] - text_size[1]) // 2 )
|
||||
|
||||
# Create Surface object and fill it with the given background
|
||||
surface = pygame.Surface(self.size)
|
||||
surface.fill(bg)
|
||||
|
||||
# Render text
|
||||
rendered_text = font.render(text, 1, color)
|
||||
surface.blit(rendered_text, pos)
|
||||
|
||||
# Render outline
|
||||
pygame.draw.rect(surface, outline, (pos[0]-offset[0], pos[1]-offset[0], size[0], size[1]), 1)
|
||||
|
||||
# Make background color transparent
|
||||
if transparency:
|
||||
surface.set_colorkey(bg)
|
||||
|
||||
self.surface_list.append((surface, (0,0)))
|
||||
|
||||
def wrap_text(self, msg, font, size):
|
||||
final_lines = [] # resulting wrapped text
|
||||
requested_lines = msg.splitlines() # wrap input along line breaks
|
||||
accumulated_height = 0 # accumulated height
|
||||
|
||||
# Form a series of lines
|
||||
for requested_line in requested_lines:
|
||||
# Handle too long lines
|
||||
if font.size(requested_line)[0] > size[0]:
|
||||
# Split at white spaces
|
||||
words = requested_line.split(' ')
|
||||
# if any of our words are too long to fit, trim them
|
||||
for word in words:
|
||||
while font.size(word)[0] >= size[0]:
|
||||
word = word[:-1]
|
||||
# Start a new line
|
||||
accumulated_line = ""
|
||||
# Put words on the line as long as they fit
|
||||
for word in words:
|
||||
test_line = accumulated_line + word + " "
|
||||
# Build the line while the words fit.
|
||||
if font.size(test_line)[0] < size[0]:
|
||||
accumulated_line = test_line
|
||||
else:
|
||||
# Start a new line
|
||||
line_height = font.size(accumulated_line)[1]
|
||||
if accumulated_height + line_height > size[1]:
|
||||
break
|
||||
else:
|
||||
accumulated_height += line_height
|
||||
final_lines.append(accumulated_line)
|
||||
accumulated_line = word + " "
|
||||
# Finish requested_line
|
||||
line_height = font.size(accumulated_line)[1]
|
||||
if accumulated_height + line_height > size[1]:
|
||||
break
|
||||
else:
|
||||
accumulated_height += line_height
|
||||
final_lines.append(accumulated_line)
|
||||
# Line fits as it is
|
||||
else:
|
||||
accumulated_height += font.size(requested_line)[1]
|
||||
final_lines.append(requested_line)
|
||||
|
||||
# Check height of wrapped text
|
||||
if accumulated_height >= size[1]:
|
||||
raise GuiException("Wrapped text is too high to fit.")
|
||||
|
||||
return final_lines, accumulated_height
|
||||
|
||||
def render_text(self, text, text_height, valign, halign, font, color, bg, transparency, outline):
|
||||
# Determine vertical position
|
||||
if valign == 0: # top aligned
|
||||
voffset = 0
|
||||
elif valign == 1: # centered
|
||||
voffset = int((self.size[1] - text_height) / 2)
|
||||
elif valign == 2: # bottom aligned
|
||||
voffset = self.size[1] - text_height
|
||||
else:
|
||||
raise GuiException("Invalid valign argument: " + str(valign))
|
||||
|
||||
# Create Surface object and fill it with the given background
|
||||
surface = pygame.Surface(self.size)
|
||||
surface.fill(bg)
|
||||
|
||||
# Blit one line after another
|
||||
accumulated_height = 0
|
||||
for line in text:
|
||||
maintext = font.render(line, 1, color)
|
||||
shadow = font.render(line, 1, outline)
|
||||
if halign == 0: # left aligned
|
||||
hoffset = 0
|
||||
elif halign == 1: # centered
|
||||
hoffset = (self.size[0] - maintext.get_width()) / 2
|
||||
elif halign == 2: # right aligned
|
||||
hoffset = rect.width - maintext.get_width()
|
||||
else:
|
||||
raise GuiException("Invalid halign argument: " + str(justification))
|
||||
pos = (hoffset, voffset + accumulated_height)
|
||||
# Outline
|
||||
surface.blit(shadow, (pos[0]-1,pos[1]-1))
|
||||
surface.blit(shadow, (pos[0]-1,pos[1]+1))
|
||||
surface.blit(shadow, (pos[0]+1,pos[1]-1))
|
||||
surface.blit(shadow, (pos[0]+1,pos[1]+1))
|
||||
# Text
|
||||
surface.blit(maintext, pos)
|
||||
accumulated_height += font.size(line)[1]
|
||||
|
||||
# Make background color transparent
|
||||
if transparency:
|
||||
surface.set_colorkey(bg)
|
||||
|
||||
# Return the rendered surface
|
||||
return surface
|
||||
|
||||
def convert_event(self, event):
|
||||
if event.type == pygame.QUIT:
|
||||
return True, Event(0, 0)
|
||||
elif event.type == pygame.KEYDOWN:
|
||||
return True, Event(1, event.key)
|
||||
elif event.type == pygame.MOUSEBUTTONUP:
|
||||
return True, Event(2, (event.button, event.pos))
|
||||
elif event.type >= pygame.USEREVENT:
|
||||
return True, Event(3, event.channel)
|
||||
else:
|
||||
return False, ''
|
||||
|
||||
def check_for_event(self):
|
||||
for event in EventModule.get():
|
||||
r, e = self.convert_event(event)
|
||||
if r:
|
||||
return r, e
|
||||
return False, ''
|
||||
|
||||
def wait_for_event(self):
|
||||
# Repeat until a relevant event happened
|
||||
while True:
|
||||
# Discard all input that happened before entering the loop
|
||||
EventModule.get()
|
||||
|
||||
# Wait for event
|
||||
event = EventModule.wait()
|
||||
|
||||
# Return Event-Object
|
||||
r, e = self.convert_event(event)
|
||||
if r:
|
||||
return e
|
||||
|
||||
def teardown(self):
|
||||
pygame.quit()
|
||||
425
photobooth.py
425
photobooth.py
@@ -1,425 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# Created by br _at_ re-web _dot_ eu, 2015-2016
|
||||
|
||||
import os
|
||||
from datetime import datetime
|
||||
from glob import glob
|
||||
from sys import exit
|
||||
from time import sleep, clock
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from gui import GUI_PyGame as GuiModule
|
||||
# from camera import CameraException, Camera_cv as CameraModule
|
||||
from camera import CameraException, Camera_gPhoto as CameraModule
|
||||
from slideshow import Slideshow
|
||||
from events import Rpi_GPIO as GPIO
|
||||
|
||||
#####################
|
||||
### Configuration ###
|
||||
#####################
|
||||
|
||||
# Screen size
|
||||
display_size = (1024, 600)
|
||||
|
||||
# Maximum size of assembled image
|
||||
image_size = (2352, 1568)
|
||||
|
||||
# Size of pictures in the assembled image
|
||||
thumb_size = (1176, 784)
|
||||
|
||||
# Image basename
|
||||
picture_basename = datetime.now().strftime("%Y-%m-%d/pic")
|
||||
|
||||
# GPIO channel of switch to shutdown the Photobooth
|
||||
gpio_shutdown_channel = 24 # pin 18 in all Raspi-Versions
|
||||
|
||||
# GPIO channel of switch to take pictures
|
||||
gpio_trigger_channel = 23 # pin 16 in all Raspi-Versions
|
||||
|
||||
# GPIO output channel for (blinking) lamp
|
||||
gpio_lamp_channel = 4 # pin 7 in all Raspi-Versions
|
||||
|
||||
# Waiting time in seconds for posing
|
||||
pose_time = 3
|
||||
|
||||
# Display time for assembled picture
|
||||
display_time = 10
|
||||
|
||||
# Show a slideshow of existing pictures when idle
|
||||
idle_slideshow = True
|
||||
|
||||
# Display time of pictures in the slideshow
|
||||
slideshow_display_time = 5
|
||||
|
||||
###############
|
||||
### Classes ###
|
||||
###############
|
||||
|
||||
class PictureList:
|
||||
"""A simple helper class.
|
||||
|
||||
It provides the filenames for the assembled pictures and keeps count
|
||||
of taken and previously existing pictures.
|
||||
"""
|
||||
|
||||
def __init__(self, basename):
|
||||
"""Initialize filenames to the given basename and search for
|
||||
existing files. Set the counter accordingly.
|
||||
"""
|
||||
|
||||
# Set basename and suffix
|
||||
self.basename = basename
|
||||
self.suffix = ".jpg"
|
||||
self.count_width = 5
|
||||
|
||||
# Ensure directory exists
|
||||
dirname = os.path.dirname(self.basename)
|
||||
if not os.path.exists(dirname):
|
||||
os.makedirs(dirname)
|
||||
|
||||
# Find existing files
|
||||
count_pattern = "[0-9]" * self.count_width
|
||||
pictures = glob(self.basename + count_pattern + self.suffix)
|
||||
|
||||
# Get number of latest file
|
||||
if len(pictures) == 0:
|
||||
self.counter = 0
|
||||
else:
|
||||
pictures.sort()
|
||||
last_picture = pictures[-1]
|
||||
self.counter = int(last_picture[-(self.count_width+len(self.suffix)):-len(self.suffix)])
|
||||
|
||||
# Print initial infos
|
||||
print("Info: Number of last existing file: " + str(self.counter))
|
||||
print("Info: Saving assembled pictures as: " + self.basename + "XXXXX.jpg")
|
||||
|
||||
def get(self, count):
|
||||
return self.basename + str(count).zfill(self.count_width) + self.suffix
|
||||
|
||||
def get_last(self):
|
||||
return self.get(self.counter)
|
||||
|
||||
def get_next(self):
|
||||
self.counter += 1
|
||||
return self.get(self.counter)
|
||||
|
||||
|
||||
class Photobooth:
|
||||
"""The main class.
|
||||
|
||||
It contains all the logic for the photobooth.
|
||||
"""
|
||||
|
||||
def __init__(self, display_size, picture_basename, picture_size, pose_time, display_time,
|
||||
trigger_channel, shutdown_channel, lamp_channel, idle_slideshow, slideshow_display_time):
|
||||
self.display = GuiModule('Photobooth', display_size)
|
||||
self.pictures = PictureList(picture_basename)
|
||||
self.camera = CameraModule(picture_size)
|
||||
|
||||
self.pic_size = picture_size
|
||||
self.pose_time = pose_time
|
||||
self.display_time = display_time
|
||||
|
||||
self.trigger_channel = trigger_channel
|
||||
self.shutdown_channel = shutdown_channel
|
||||
self.lamp_channel = lamp_channel
|
||||
|
||||
self.idle_slideshow = idle_slideshow
|
||||
if self.idle_slideshow:
|
||||
self.slideshow_display_time = slideshow_display_time
|
||||
self.slideshow = Slideshow(display_size, display_time,
|
||||
os.path.dirname(os.path.realpath(picture_basename)))
|
||||
|
||||
input_channels = [ trigger_channel, shutdown_channel ]
|
||||
output_channels = [ lamp_channel ]
|
||||
self.gpio = GPIO(self.handle_gpio, input_channels, output_channels)
|
||||
|
||||
def teardown(self):
|
||||
self.display.clear()
|
||||
self.display.show_message("Shutting down...")
|
||||
self.display.apply()
|
||||
self.gpio.set_output(self.lamp_channel, 0)
|
||||
sleep(0.5)
|
||||
self.display.teardown()
|
||||
self.gpio.teardown()
|
||||
exit(0)
|
||||
|
||||
def _run_plain(self):
|
||||
while True:
|
||||
self.camera.set_idle()
|
||||
|
||||
# Display default message
|
||||
self.display.clear()
|
||||
self.display.show_message("Hit the button!")
|
||||
self.display.apply()
|
||||
|
||||
# Wait for an event and handle it
|
||||
event = self.display.wait_for_event()
|
||||
self.handle_event(event)
|
||||
|
||||
def _run_slideshow(self):
|
||||
while True:
|
||||
self.camera.set_idle()
|
||||
self.slideshow.display_next("Hit the button!")
|
||||
tic = clock()
|
||||
while clock() - tic < self.slideshow_display_time:
|
||||
self.check_and_handle_events()
|
||||
|
||||
def run(self):
|
||||
while True:
|
||||
try:
|
||||
# Enable lamp
|
||||
self.gpio.set_output(self.lamp_channel, 1)
|
||||
|
||||
# Select idle screen type
|
||||
if self.idle_slideshow:
|
||||
self._run_slideshow()
|
||||
else:
|
||||
self._run_plain()
|
||||
|
||||
# Catch exceptions and display message
|
||||
except CameraException as e:
|
||||
self.handle_exception(e.message)
|
||||
# Do not catch KeyboardInterrupt and SystemExit
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
raise
|
||||
except Exception as e:
|
||||
print('SERIOUS ERROR: ' + repr(e))
|
||||
self.handle_exception("SERIOUS ERROR!")
|
||||
|
||||
def check_and_handle_events(self):
|
||||
r, e = self.display.check_for_event()
|
||||
while r:
|
||||
self.handle_event(e)
|
||||
r, e = self.display.check_for_event()
|
||||
|
||||
def handle_gpio(self, channel):
|
||||
if channel in [ self.trigger_channel, self.shutdown_channel ]:
|
||||
self.display.trigger_event(channel)
|
||||
|
||||
def handle_event(self, event):
|
||||
if event.type == 0:
|
||||
self.teardown()
|
||||
elif event.type == 1:
|
||||
self.handle_keypress(event.value)
|
||||
elif event.type == 2:
|
||||
self.handle_mousebutton(event.value[0], event.value[1])
|
||||
elif event.type == 3:
|
||||
self.handle_gpio_event(event.value)
|
||||
|
||||
def handle_keypress(self, key):
|
||||
"""Implements the actions for the different keypress events"""
|
||||
# Exit the application
|
||||
if key == ord('q'):
|
||||
self.teardown()
|
||||
# Take pictures
|
||||
elif key == ord('c'):
|
||||
self.take_picture()
|
||||
|
||||
def handle_mousebutton(self, key, pos):
|
||||
"""Implements the actions for the different mousebutton events"""
|
||||
# Take a picture
|
||||
if key == 1:
|
||||
self.take_picture()
|
||||
|
||||
def handle_gpio_event(self, channel):
|
||||
"""Implements the actions taken for a GPIO event"""
|
||||
if channel == self.trigger_channel:
|
||||
self.take_picture()
|
||||
elif channel == self.shutdown_channel:
|
||||
self.teardown()
|
||||
|
||||
def handle_exception(self, msg):
|
||||
"""Displays an error message and returns"""
|
||||
self.display.clear()
|
||||
print("Error: " + msg)
|
||||
self.display.show_message("ERROR:\n\n" + msg)
|
||||
self.display.apply()
|
||||
sleep(3)
|
||||
|
||||
|
||||
def assemble_pictures(self, input_filenames):
|
||||
"""Assembles four pictures into a 2x2 grid
|
||||
|
||||
It assumes, all original pictures have the same aspect ratio as
|
||||
the resulting image.
|
||||
|
||||
For the thumbnail sizes we have:
|
||||
h = (H - 2 * a - 2 * b) / 2
|
||||
w = (W - 2 * a - 2 * b) / 2
|
||||
|
||||
W
|
||||
|---------------------------------------|
|
||||
|
||||
--- +---+-------------+---+-------------+---+ ---
|
||||
| | | | a
|
||||
| | +-------------+ +-------------+ | ---
|
||||
| | | | | | | |
|
||||
| | | 0 | | 1 | | | h
|
||||
| | | | | | | |
|
||||
| | +-------------+ +-------------+ | ---
|
||||
H | | | | 2*b
|
||||
| | +-------------+ +-------------+ | ---
|
||||
| | | | | | | |
|
||||
| | | 2 | | 3 | | | h
|
||||
| | | | | | | |
|
||||
| | +-------------+ +-------------+ | ---
|
||||
| | | | a
|
||||
--- +---+-------------+---+-------------+---+ ---
|
||||
|
||||
|---|-------------|---|-------------|---|
|
||||
a w 2*b w a
|
||||
"""
|
||||
|
||||
# Thumbnail size of pictures
|
||||
outer_border = 50
|
||||
inner_border = 20
|
||||
thumb_box = ( int( self.pic_size[0] / 2 ) ,
|
||||
int( self.pic_size[1] / 2 ) )
|
||||
thumb_size = ( thumb_box[0] - outer_border - inner_border ,
|
||||
thumb_box[1] - outer_border - inner_border )
|
||||
|
||||
# Create output image with white background
|
||||
output_image = Image.new('RGB', self.pic_size, (255, 255, 255))
|
||||
|
||||
# Image 0
|
||||
img = Image.open(input_filenames[0])
|
||||
img.thumbnail(thumb_size)
|
||||
offset = ( thumb_box[0] - inner_border - img.size[0] ,
|
||||
thumb_box[1] - inner_border - img.size[1] )
|
||||
output_image.paste(img, offset)
|
||||
|
||||
# Image 1
|
||||
img = Image.open(input_filenames[1])
|
||||
img.thumbnail(thumb_size)
|
||||
offset = ( thumb_box[0] + inner_border,
|
||||
thumb_box[1] - inner_border - img.size[1] )
|
||||
output_image.paste(img, offset)
|
||||
|
||||
# Image 2
|
||||
img = Image.open(input_filenames[2])
|
||||
img.thumbnail(thumb_size)
|
||||
offset = ( thumb_box[0] - inner_border - img.size[0] ,
|
||||
thumb_box[1] + inner_border )
|
||||
output_image.paste(img, offset)
|
||||
|
||||
# Image 3
|
||||
img = Image.open(input_filenames[3])
|
||||
img.thumbnail(thumb_size)
|
||||
offset = ( thumb_box[0] + inner_border ,
|
||||
thumb_box[1] + inner_border )
|
||||
output_image.paste(img, offset)
|
||||
|
||||
# Save assembled image
|
||||
output_filename = self.pictures.get_next()
|
||||
output_image.save(output_filename, "JPEG")
|
||||
return output_filename
|
||||
|
||||
def show_counter(self, seconds):
|
||||
if self.camera.has_preview():
|
||||
tic = clock()
|
||||
toc = clock() - tic
|
||||
while toc < seconds:
|
||||
self.display.clear()
|
||||
self.camera.take_preview("/tmp/photobooth_preview.jpg")
|
||||
self.display.show_picture("/tmp/photobooth_preview.jpg", flip=True)
|
||||
self.display.show_message(str(seconds - int(toc)))
|
||||
self.display.apply()
|
||||
|
||||
# Limit progress to 1 "second" per preview (e.g., too slow on Raspi 1)
|
||||
toc = min(toc + 1, clock() - tic)
|
||||
else:
|
||||
for i in range(seconds):
|
||||
self.display.clear()
|
||||
self.display.show_message(str(seconds - i))
|
||||
self.display.apply()
|
||||
sleep(1)
|
||||
|
||||
def take_picture(self):
|
||||
"""Implements the picture taking routine"""
|
||||
# Disable lamp
|
||||
self.gpio.set_output(self.lamp_channel, 0)
|
||||
|
||||
# Show pose message
|
||||
self.display.clear()
|
||||
self.display.show_message("POSE!\n\nTaking four pictures...");
|
||||
self.display.apply()
|
||||
sleep(2)
|
||||
|
||||
# Extract display and image sizes
|
||||
size = self.display.get_size()
|
||||
outsize = (int(size[0]/2), int(size[1]/2))
|
||||
|
||||
# Take pictures
|
||||
filenames = [i for i in range(4)]
|
||||
for x in range(4):
|
||||
# Countdown
|
||||
self.show_counter(self.pose_time)
|
||||
|
||||
# Try each picture up to 3 times
|
||||
remaining_attempts = 3
|
||||
while remaining_attempts > 0:
|
||||
remaining_attempts = remaining_attempts - 1
|
||||
|
||||
self.display.clear()
|
||||
self.display.show_message("S M I L E !!!\n\n" + str(x+1) + " of 4")
|
||||
self.display.apply()
|
||||
|
||||
tic = clock()
|
||||
|
||||
try:
|
||||
filenames[x] = self.camera.take_picture("/tmp/photobooth_%02d.jpg" % x)
|
||||
remaining_attempts = 0
|
||||
except CameraException as e:
|
||||
# On recoverable errors: display message and retry
|
||||
if e.recoverable:
|
||||
if remaining_attempts > 0:
|
||||
self.display.clear()
|
||||
self.display.show_message(e.message)
|
||||
self.display.apply()
|
||||
sleep(5)
|
||||
else:
|
||||
raise CameraException("Giving up! Please start over!", False)
|
||||
else:
|
||||
raise e
|
||||
|
||||
# Measure used time and sleep a second if too fast
|
||||
toc = clock() - tic
|
||||
if toc < 1.0:
|
||||
sleep(1.0 - toc)
|
||||
|
||||
# Show 'Wait'
|
||||
self.display.clear()
|
||||
self.display.show_message("Please wait!\n\nProcessing...")
|
||||
self.display.apply()
|
||||
|
||||
# Assemble them
|
||||
outfile = self.assemble_pictures(filenames)
|
||||
|
||||
# Show pictures for 10 seconds
|
||||
self.display.clear()
|
||||
self.display.show_picture(outfile, size, (0,0))
|
||||
self.display.apply()
|
||||
sleep(self.display_time)
|
||||
|
||||
# Reenable lamp
|
||||
self.gpio.set_output(self.lamp_channel, 1)
|
||||
|
||||
|
||||
|
||||
|
||||
#################
|
||||
### Functions ###
|
||||
#################
|
||||
|
||||
def main():
|
||||
photobooth = Photobooth(display_size, picture_basename, image_size, pose_time, display_time,
|
||||
gpio_trigger_channel, gpio_shutdown_channel, gpio_lamp_channel,
|
||||
idle_slideshow, slideshow_display_time)
|
||||
photobooth.run()
|
||||
photobooth.teardown()
|
||||
return 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
exit(main())
|
||||
@@ -1,14 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
PHOTOBOOTH_DIR=/home/pi/photobooth
|
||||
|
||||
cd "${PHOTOBOOTH_DIR}"
|
||||
|
||||
if [[ $1 == "set-time" ]]; then
|
||||
sudo python set-time.py
|
||||
fi
|
||||
|
||||
sudo python photobooth.py >>photobooth.log 2>>photobooth.err
|
||||
|
||||
cd -
|
||||
|
||||
100
set-time.py
100
set-time.py
@@ -1,100 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# Created by br _at_ re-web _dot_ eu, 2016
|
||||
|
||||
from gui import GUI_PyGame as GuiModule
|
||||
from time import sleep
|
||||
|
||||
import subprocess
|
||||
|
||||
# Screen size
|
||||
display_size = (1024, 600)
|
||||
|
||||
# Button size
|
||||
button_size = (70, 70)
|
||||
|
||||
date_digits = ['D', 'D', 'M', 'M', 'Y', 'Y', 'Y', 'Y'] # DD-MM-YYYY
|
||||
time_digits = ['H', 'H', 'M', 'M'] # HH-MM
|
||||
|
||||
numpad = { '1': (100, 100), '2': (200, 100), '3': (300, 100),
|
||||
'4': (100, 200), '5': (200, 200), '6': (300, 200),
|
||||
'7': (100, 300), '8': (200, 300), '9': (300, 300),
|
||||
'0': (200, 400) }
|
||||
|
||||
#################
|
||||
### Functions ###
|
||||
#################
|
||||
|
||||
def check_and_handle_events(display, digit):
|
||||
r, e = display.check_for_event()
|
||||
while r:
|
||||
handle_event(e, digit)
|
||||
r, e = display.check_for_event()
|
||||
|
||||
def handle_event(event, digit, digits, numpad):
|
||||
# mouseclick
|
||||
if event.type == 2 and event.value[0] == 1:
|
||||
print(event.value[1])
|
||||
for num, pos in numpad.items():
|
||||
if (event.value[1][0] > pos[0] and
|
||||
event.value[1][0] < pos[0] + button_size[0] and
|
||||
event.value[1][1] > pos[1] and
|
||||
event.value[1][1] < pos[1] + button_size[1]):
|
||||
digits[digit] = num
|
||||
return True
|
||||
return False
|
||||
|
||||
def show_numpad(display, numpad, button_size):
|
||||
for num, pos in numpad.items():
|
||||
display.show_button(num, pos, button_size)
|
||||
|
||||
def show_digits(display, digits, button_size):
|
||||
for i in range(len(digits)):
|
||||
display.show_button(digits[i], (400 + i * (button_size[0] + 5), 200), button_size, outline=(0,0,0))
|
||||
|
||||
def main():
|
||||
display = GuiModule('set-time', display_size, hide_mouse=False)
|
||||
|
||||
for digit in range(len(date_digits)):
|
||||
display.clear()
|
||||
|
||||
show_numpad(display, numpad, button_size)
|
||||
display.show_button('Date:', (400, 100), outline=(0,0,0))
|
||||
show_digits(display, date_digits, button_size)
|
||||
|
||||
display.apply()
|
||||
|
||||
digit_done = False
|
||||
while not digit_done:
|
||||
r, e = display.check_for_event()
|
||||
while r:
|
||||
digit_done = handle_event(e, digit, date_digits, numpad)
|
||||
r, e = display.check_for_event()
|
||||
|
||||
for digit in range(len(time_digits)):
|
||||
display.clear()
|
||||
|
||||
show_numpad(display, numpad, button_size)
|
||||
display.show_button('Time:', (400, 100), outline=(0,0,0))
|
||||
show_digits(display, time_digits, button_size)
|
||||
|
||||
display.apply()
|
||||
|
||||
digit_done = False
|
||||
while not digit_done:
|
||||
event = display.wait_for_event()
|
||||
digit_done = handle_event(event, digit, time_digits, numpad)
|
||||
|
||||
# YYYY-MM-DD HH:mm
|
||||
date_str = ( date_digits[4] + date_digits[5] + date_digits[6] + date_digits[7] + '-' +
|
||||
date_digits[2] + date_digits[3] + '-' +
|
||||
date_digits[0] + date_digits[1] + ' ' +
|
||||
time_digits[0] + time_digits[1] + ':' + time_digits[2] + time_digits[3] )
|
||||
|
||||
subprocess.check_call(['date', '-s', date_str])
|
||||
|
||||
display.teardown()
|
||||
return 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
exit(main())
|
||||
|
||||
134
slideshow.py
134
slideshow.py
@@ -1,134 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# Created by br@re-web.eu, 2015
|
||||
|
||||
from gui import GUI_PyGame as GuiModule
|
||||
|
||||
import os
|
||||
from datetime import datetime
|
||||
import subprocess
|
||||
import thread
|
||||
from time import sleep
|
||||
|
||||
#####################
|
||||
### Configuration ###
|
||||
#####################
|
||||
|
||||
# Screen size
|
||||
display_size = (1024, 768)
|
||||
|
||||
# Directory for slideshow pictures
|
||||
slideshow_directory = "slideshow/"
|
||||
|
||||
# Source directory, can also be remote.
|
||||
# Leave empty to disable sync
|
||||
source_directory = "pi@photobooth:photobooth/" + datetime.now().strftime("%Y-%m-%d")
|
||||
|
||||
# Display time (in seconds) for slideshow pictures
|
||||
display_time = 3
|
||||
|
||||
# Waiting time (in seconds) between synchronizations
|
||||
sync_time = 60
|
||||
|
||||
###############
|
||||
### Classes ###
|
||||
###############
|
||||
|
||||
class Slideshow:
|
||||
def __init__(self, display_size, display_time, directory, recursive=True):
|
||||
self.directory = directory
|
||||
self.recursive = recursive
|
||||
self.filelist = []
|
||||
self.display = GuiModule("Slideshow", display_size)
|
||||
self.display_time = display_time
|
||||
self.next = 0
|
||||
|
||||
def scan(self):
|
||||
filelist = []
|
||||
|
||||
if self.recursive:
|
||||
# Recursively walk all entries in the directory
|
||||
for root, dirnames, filenames in os.walk(self.directory, followlinks=True):
|
||||
for filename in filenames:
|
||||
filelist.append(os.path.join(root, filename))
|
||||
else:
|
||||
# Add all entries in the directory
|
||||
for item in os.listdir(self.directory):
|
||||
filename = os.path.join(self.directory, item)
|
||||
if os.path.isfile(filename):
|
||||
filelist.append(filename)
|
||||
|
||||
self.filelist = filelist
|
||||
self.next = 0
|
||||
|
||||
def handle_event(self, event):
|
||||
if event.type == 0:
|
||||
self.teardown()
|
||||
elif event.type == 1:
|
||||
self.handle_keypress(event.value)
|
||||
|
||||
def handle_keypress(self, key):
|
||||
# Exit the application
|
||||
if key == ord('q'):
|
||||
self.teardown()
|
||||
|
||||
def display_next(self, text=""):
|
||||
if self.next >= len(self.filelist):
|
||||
self.scan()
|
||||
if not self.filelist:
|
||||
self.display.clear()
|
||||
if text:
|
||||
self.display.show_message(text)
|
||||
else:
|
||||
self.display.show_message("No pictures available!")
|
||||
self.display.apply()
|
||||
else:
|
||||
filename = self.filelist[self.next]
|
||||
self.next += 1
|
||||
self.display.clear()
|
||||
self.display.show_picture(filename)
|
||||
if text:
|
||||
self.display.show_message(text)
|
||||
self.display.apply()
|
||||
|
||||
def run(self):
|
||||
while True:
|
||||
self.display_next()
|
||||
sleep(self.display_time)
|
||||
r, e = self.display.check_for_event()
|
||||
if r:
|
||||
self.handle_event(e)
|
||||
|
||||
def teardown(self):
|
||||
self.display.teardown()
|
||||
exit(0)
|
||||
|
||||
|
||||
#################
|
||||
### Functions ###
|
||||
#################
|
||||
|
||||
def sync_folders(source_directory, target_directory, wait_time):
|
||||
sleep(5)
|
||||
while True:
|
||||
print("[" + datetime.now().strftime("%H:%M:%S") + "] Sync "
|
||||
+ source_directory + " --> " + target_directory)
|
||||
try:
|
||||
cmd = "rsync -rtu " + source_directory + " " + target_directory
|
||||
output = subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print("ERROR executing '" + e.cmd + "':\n" + e.output)
|
||||
sleep(wait_time)
|
||||
|
||||
def main():
|
||||
# Start a thread for syncing files
|
||||
if len(source_directory) > 0:
|
||||
thread.start_new_thread(sync_folders, (source_directory, slideshow_directory, sync_time) )
|
||||
|
||||
# Start the slideshow
|
||||
slideshow = Slideshow(display_size, display_time, slideshow_directory, True)
|
||||
slideshow.run()
|
||||
|
||||
return 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
exit(main())
|
||||
@@ -1,4 +0,0 @@
|
||||
#define transparent_width 8
|
||||
#define transparent_height 8
|
||||
static unsigned char transparent_bits[] = {
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
@@ -1,6 +0,0 @@
|
||||
#define transparent_width 8
|
||||
#define transparent_height 8
|
||||
#define transparent_x_hot 0
|
||||
#define transparent_y_hot 0
|
||||
static unsigned char transparent_bits[] = {
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
Reference in New Issue
Block a user