Continued refactoring

This commit is contained in:
Balthasar Reuter
2015-06-17 23:39:42 +02:00
parent 6757273635
commit 6b0a57af05
3 changed files with 229 additions and 395 deletions

49
events.py Normal file
View File

@@ -0,0 +1,49 @@
#!/usr/bin/env python
# Created by br@re-web.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)

28
gui.py
View File

@@ -13,6 +13,8 @@ try:
except ImportError: except ImportError:
import pygame.event as EventModule import pygame.event as EventModule
from events import Event
class TextRectException: class TextRectException:
def __init__(self, message = None): def __init__(self, message = None):
@@ -120,7 +122,7 @@ class GUI_PyGame:
"""A GUI class using PyGame""" """A GUI class using PyGame"""
def __init__(self, name, size): def __init__(self, name, size):
# Call init routines # Call init routines
pygame.init() pygame.init()
if hasattr(EventModule, 'init'): if hasattr(EventModule, 'init'):
EventModule.init() EventModule.init()
@@ -147,8 +149,8 @@ class GUI_PyGame:
def get_size(self): def get_size(self):
return self.size return self.size
def trigger_event(self, event_id, event_channel): def trigger_event(self, event_channel):
EventModule.post(EventModule.Event(event_id, channel=event_channel)) EventModule.post(EventModule.Event(pygame.USEREVENT, channel=event_channel))
def show_picture(self, filename, size=(0,0), offset=(0,0)): def show_picture(self, filename, size=(0,0), offset=(0,0)):
# Use window size if none given # Use window size if none given
@@ -176,6 +178,26 @@ class GUI_PyGame:
text = render_textrect(msg, font, rect, color, bg, 1, 1) text = render_textrect(msg, font, rect, color, bg, 1, 1)
self.screen.blit(text, rect.topleft) self.screen.blit(text, rect.topleft)
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
if event.type == pygame.QUIT:
return Event(0, 0)
elif event.type == pygame.KEYDOWN:
return Event(1, event.key)
elif event.type == pygame.MOUSEBUTTONUP:
return Event(2, (event.button, event.pos))
elif event.type >= pygame.USEREVENT:
return Event(3, event.channel)
def mainloop(self, filename, handle_keypress, handle_mousebutton, handle_gpio_event): def mainloop(self, filename, handle_keypress, handle_mousebutton, handle_gpio_event):
while True: while True:
# Ignore all input that happened before entering the loop # Ignore all input that happened before entering the loop

View File

@@ -4,7 +4,6 @@
from __future__ import division from __future__ import division
import os import os
import subprocess
from datetime import datetime from datetime import datetime
from glob import glob from glob import glob
from sys import exit from sys import exit
@@ -12,17 +11,12 @@ from time import sleep
from PIL import Image from PIL import Image
import pygame from gui import GUI_PyGame as GuiModule
try:
import pygame.fastevent as eventmodule
except ImportError:
import pygame.event as eventmodule
try: from camera import Camera_gPhoto as CameraModule
import RPi.GPIO as GPIO from camera import CameraException
gpio_enabled = True
except ImportError: from events import Rpi_GPIO as GPIO
gpio_enabled = False
##################### #####################
### Configuration ### ### Configuration ###
@@ -34,14 +28,8 @@ display_size = (1024, 600)
# Maximum size of assembled image # Maximum size of assembled image
image_size = (2352, 1568) image_size = (2352, 1568)
# Idle image
image_idle = None
# Pose image
image_pose = None
# Image basename # Image basename
image_basename = datetime.now().strftime("%Y-%m-%d/pic") picture_basename = datetime.now().strftime("%Y-%m-%d/pic")
# GPIO channel of switch to shutdown the Pi # GPIO channel of switch to shutdown the Pi
gpio_shutdown_channel = 24 # pin 18 in all Raspi-Versions gpio_shutdown_channel = 24 # pin 18 in all Raspi-Versions
@@ -52,9 +40,6 @@ gpio_trigger_channel = 23 # pin 16 in all Raspi-Versions
# GPIO output channel for (blinking) lamp # GPIO output channel for (blinking) lamp
gpio_lamp_channel = 4 # pin 7 in all Raspi-Versions gpio_lamp_channel = 4 # pin 7 in all Raspi-Versions
# PyGame event used to detect GPIO triggers
gpio_pygame_event = pygame.USEREVENT
# Waiting time in seconds for posing # Waiting time in seconds for posing
pose_time = 5 pose_time = 5
@@ -65,7 +50,7 @@ display_time = 10
### Classes ### ### Classes ###
############### ###############
class Images: class PictureList:
"""Class to manage images and count them""" """Class to manage images and count them"""
def __init__(self, basename): def __init__(self, basename):
# Set basename and suffix # Set basename and suffix
@@ -99,393 +84,171 @@ class Images:
self.counter += 1 self.counter += 1
return self.get(self.counter) return self.get(self.counter)
class TextRectException:
def __init__(self, message = None):
self.message = message
def __str__(self):
return self.message
def render_textrect(string, font, rect, text_color, background_color, justification=0, valign=0): class Photobooth:
"""Returns a surface containing the passed text string, reformatted def __init__(self, picture_basename, picture_size, trigger_channel, shutdown_channel, lamp_channel):
to fit within the given rect, word-wrapping as necessary. The text self.display = GuiModule('Photobooth', display_size)
will be anti-aliased. self.pictures = PictureList(picture_basename)
self.camera = CameraModule()
self.pic_size = picture_size
Source: http://www.pygame.org/pcr/text_rect/index.php self.trigger_channel = trigger_channel
self.shutdown_channel = shutdown_channel
self.lamp_channel = lamp_channel
Takes the following arguments: input_channels = [ trigger_channel, shutdown_channel ]
output_channels = [ lamp_channel ]
string - the text you wish to render. \n begins a new line. self.gpio = GPIO(self.handle_gpio, input_channels, output_channels)
font - a Font object
rect - a rectstyle giving the size of the surface requested.
text_color - a three-byte tuple of the rgb value of the
text color. ex (0, 0, 0) = BLACK
background_color - a three-byte tuple of the rgb value of the surface.
justification - 0 (default) left-justified
1 horizontally centered
2 right-justified
valign - 0 (default) top aligned
1 vertically centered
2 bottom aligned
Returns the following values:
Success - a surface object with the text rendered onto it.
Failure - raises a TextRectException if the text won't fit onto the surface.
"""
final_lines = []
requested_lines = string.splitlines()
# Create a series of lines that will fit on the provided
# rectangle.
accumulated_height = 0
for requested_line in requested_lines:
if font.size(requested_line)[0] > rect.width:
words = requested_line.split(' ')
# if any of our words are too long to fit, return.
for word in words:
if font.size(word)[0] >= rect.width:
raise TextRectException, "The word " + word + " is too long to fit in the rect passed."
# Start a new line
accumulated_line = ""
for word in words:
test_line = accumulated_line + word + " "
# Build the line while the words fit.
if font.size(test_line)[0] < rect.width:
accumulated_line = test_line
else:
accumulated_height += font.size(test_line)[1]
final_lines.append(accumulated_line)
accumulated_line = word + " "
accumulated_height += font.size(accumulated_line)[1]
final_lines.append(accumulated_line)
else:
accumulated_height += font.size(requested_line)[1]
final_lines.append(requested_line)
# Check height of the text and align vertically
if accumulated_height >= rect.height:
raise TextRectException, "Once word-wrapped, the text string was too tall to fit in the rect."
if valign == 0:
voffset = 0
elif valign == 1:
voffset = int((rect.height - accumulated_height) / 2)
elif valign == 2:
voffset = rect.height - accumulated_height
else:
raise TextRectException, "Invalid valign argument: " + str(valign)
# Let's try to write the text out on the surface.
surface = pygame.Surface(rect.size)
surface.fill(background_color)
accumulated_height = 0
for line in final_lines:
if line != "":
tempsurface = font.render(line, 1, text_color)
if justification == 0:
surface.blit(tempsurface, (0, voffset + accumulated_height))
elif justification == 1:
surface.blit(tempsurface, ((rect.width - tempsurface.get_width()) / 2, voffset + accumulated_height))
elif justification == 2:
surface.blit(tempsurface, (rect.width - tempsurface.get_width(), voffset + accumulated_height))
else:
raise TextRectException, "Invalid justification argument: " + str(justification)
accumulated_height += font.size(line)[1]
return surface
class GUI_PyGame:
"""The GUI class using PyGame"""
def __init__(self, name, size):
pygame.init()
if hasattr(eventmodule, 'init'):
eventmodule.init()
# Window name
pygame.display.set_caption(name)
# Hide mouse cursor
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(self.size, pygame.FULLSCREEN)
# Clear screen
self.clear()
def clear(self, color=(0,0,0)):
self.screen.fill(color)
def apply(self):
pygame.display.update()
def get_size(self):
return self.size
def trigger_event(self, event_id, event_channel):
eventmodule.post(eventmodule.Event(event_id, channel=event_channel))
def show_picture(self, filename, size=(0,0), offset=(0,0)):
# Use window size if none given
if size == (0,0):
size = self.size
# Load image from file
image = pygame.image.load(filename)
# 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()
self.screen.blit(image, offset)
def show_message(self, msg, color=(245,245,245), bg=(0,0,0)):
# Choose font
font = pygame.font.Font(None, 144)
# Create rectangle for text
rect = pygame.Rect((0, 0, self.size[0], self.size[1]))
# Render text
text = render_textrect(msg, font, rect, color, bg, 1, 1)
self.screen.blit(text, rect.topleft)
def mainloop(self, filename):
while True:
# Ignore all input that happened before entering the loop
eventmodule.get()
# Clear display
self.clear()
# Show idle-picture and message
if filename != None:
self.show_picture(filename)
self.show_message("Hit the button!")
# Render everything
self.apply()
# Wait for event
event = eventmodule.wait()
# Handle the event
if event.type == pygame.QUIT: return
elif event.type == pygame.KEYDOWN: handle_keypress(event.key)
elif event.type == pygame.MOUSEBUTTONUP: handle_mousebutton(event.button, event.pos)
elif event.type == gpio_pygame_event: handle_gpio_event(event.channel)
def teardown(self): def teardown(self):
pygame.quit() self.display.teardown()
self.gpio.teardown()
exit(0)
class CameraException(Exception): def run(self):
"""Custom exception class to handle gPhoto errors""" while True:
pass try:
# Enable lamp
self.gpio.set_output(self.lamp_channel, 1)
class Camera: while True:
"""Camera class providing functionality to take pictures""" # Display default message
def __init__(self): self.display.clear()
# Print the abilities of the connected camera self.display.show_message("Hit the button!")
try: self.display.apply()
print(self.call_gphoto("-a", "/dev/null")) # Wait for an event and handle it
except CameraException as e: event = self.display.wait_for_event()
handle_exception(e.message) self.handle_event(event)
def call_gphoto(self, action, filename): except CameraException as e:
# Try to run the command self.handle_exception(e.message)
try:
cmd = "gphoto2 --force-overwrite --quiet " + action + " --filename " + filename def handle_gpio(self, channel):
output = subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT) if channel in [ self.trigger_channel, self.shutdown_channel ]:
if "ERROR" in output: self.display.trigger_event(channel)
raise subprocess.CalledProcessError(returncode=0, cmd=cmd, output=output)
except subprocess.CalledProcessError as e: def handle_event(self, event):
if "Canon EOS Capture failed: 2019" in e.output: if event.type == 0:
raise CameraException("Can't focus! Move and try again!") self.teardown()
elif "No camera found" in e.output: elif event.type == 1:
raise CameraException("No (supported) camera detected!") self.handle_keypress(event.value)
else: elif event.type == 2:
raise CameraException("Unknown error!\n" + '\n'.join(e.output.split('\n')[1:3])) self.handle_mousebutton(event.value[0], event.value[1])
return output 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 == gpio_trigger_channel:
self.take_picture()
elif channel == gpio_shutdown_channel:
self.display.clear()
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, output_filename):
"""Assembles four pictures into a 2x2 grid"""
# Thumbnail size of pictures
size = (int(image_size[0]/2),int(image_size[1]/2))
# Create output image
output_image = Image.new('RGB', image_size)
# Load images and resize them
for i in range(2):
for j in range(2):
k = i * 2 + j
img = Image.open(input_filenames[k])
img.thumbnail(size)
offset = (j * size[0], i * size[1])
output_image.paste(img, offset)
output_image.save(output_filename, "JPEG")
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(pose_time - 3)
# Countdown
for i in range(3):
self.display.clear()
self.display.show_message(str(3 - i))
self.display.apply()
sleep(1)
# Show 'Cheese'
self.display.clear()
self.display.show_message("S M I L E !")
self.display.apply()
# 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):
filenames[x] = self.camera.take_picture("/tmp/photobooth_%02d.jpg" % x)
# Show 'Wait'
self.display.clear()
self.display.show_message("Please wait!\n\nProcessing...")
self.display.apply()
# Assemble them
outfile = self.pictures.get_next()
self.assemble_pictures(filenames, outfile)
# Show pictures for 10 seconds
self.display.clear()
self.display.show_picture(outfile, size, (0,0))
self.display.apply()
sleep(display_time)
# Reenable lamp
self.gpio.set_output(self.lamp_channel, 1)
def preview(self, filename="/tmp/preview.jpg"):
while not self.call_gphoto("--capture-preview", filename):
continue
return filename
def take_picture(self, filename="/tmp/picture.jpg"):
self.call_gphoto("--capture-image-and-download", filename)
return filename
################# #################
### Functions ### ### Functions ###
################# #################
def assemble_pictures(input_filenames, output_filename):
"""Assembles four pictures into a 2x2 grid"""
# Thumbnail size of pictures
size = (int(image_size[0]/2),int(image_size[1]/2))
# Create output image
output_image = Image.new('RGB', image_size)
# Load images and resize them
for i in range(2):
for j in range(2):
k = i * 2 + j
img = Image.open(input_filenames[k])
img.thumbnail(size)
offset = (j * size[0], i * size[1])
output_image.paste(img, offset)
output_image.save(output_filename, "JPEG")
def take_picture():
"""Implements the picture taking routine"""
# Disable the lamp
set_lamp(0)
# Show pose message
display.clear()
if image_pose != None:
display.show_picture(image_pose)
display.show_message("POSE!\n\nTaking four pictures...");
display.apply()
sleep(pose_time - 3)
# Countdown
for i in range(3):
display.clear()
display.show_message(str(3 - i))
display.apply()
sleep(1)
# Show 'Cheese'
display.clear()
display.show_message("S M I L E !")
display.apply()
# Extract display and image sizes
size = 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):
filenames[x] = camera.take_picture("/tmp/photobooth_%02d.jpg" % x)
# Show 'Wait'
display.clear()
display.show_message("Please wait!\n\nProcessing...")
display.apply()
# Assemble them
outfile = images.get_next()
assemble_pictures(filenames, outfile)
# Show pictures for 10 seconds
display.clear()
display.show_picture(outfile, size, (0,0))
display.apply()
sleep(display_time)
# Reenable lamp
set_lamp(1)
def handle_keypress(key):
"""Implements the actions for the different keypress events"""
# Exit the application
if key == ord('q'):
teardown()
# Take pictures
elif key == ord('c'):
take_picture()
def handle_mousebutton(key, pos):
"""Implements the actions for the different mousebutton events"""
# Take a picture
if key == 1:
take_picture()
def handle_gpio_event(channel):
"""Implements the actions taken for a GPIO event"""
if channel == gpio_trigger_channel:
take_picture()
elif channel == gpio_shutdown_channel:
display.clear()
print("Shutting down!")
display.show_message("Shutting down!")
display.apply()
sleep(1)
teardown()
def handle_exception(msg):
"""Displays an error message and returns"""
display.clear()
print("Error: " + msg)
display.show_message("ERROR:\n\n" + msg)
display.apply()
sleep(3)
def setup_gpio():
"""Enables GPIO in- and output and registers event handles"""
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 trigger channel as input and listen for events
GPIO.setup(gpio_trigger_channel, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(gpio_shutdown_channel, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.add_event_detect(gpio_trigger_channel, GPIO.RISING, callback=handle_gpio, bouncetime=200)
GPIO.add_event_detect(gpio_shutdown_channel, GPIO.RISING, callback=handle_gpio, bouncetime=200)
# Setup the lamp channel as output
GPIO.setup(gpio_lamp_channel, GPIO.OUT)
GPIO.output(gpio_lamp_channel, GPIO.LOW)
else:
print("Warning: RPi.GPIO could not be loaded. GPIO disabled.")
def handle_gpio(channel):
"""Interrupt handler for GPIO events"""
display.trigger_event(gpio_pygame_event, channel)
def set_lamp(status=0):
"""Switch the lamp on"""
if gpio_enabled:
GPIO.output(gpio_lamp_channel, GPIO.HIGH if status==1 else GPIO.LOW)
def teardown(exit_code=0):
display.teardown()
if gpio_enabled:
GPIO.cleanup()
exit(exit_code)
def main(): def main():
setup_gpio() photobooth = Photobooth(picture_basename, image_size, gpio_trigger_channel, gpio_shutdown_channel, gpio_lamp_channel)
while True: photobooth.run()
try: return photobooth.teardown()
set_lamp(1)
display.mainloop(image_idle)
except CameraException as e:
handle_exception(e.message)
teardown()
return 0
########################
### Global variables ###
########################
display = GUI_PyGame('Photobooth', display_size)
images = Images(image_basename)
camera = Camera()
if __name__ == "__main__": if __name__ == "__main__":
exit(main()) exit(main())