diff --git a/events.py b/events.py new file mode 100644 index 0000000..88b3e39 --- /dev/null +++ b/events.py @@ -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) diff --git a/gui.py b/gui.py index eb1894e..3ee45f8 100644 --- a/gui.py +++ b/gui.py @@ -13,6 +13,8 @@ try: except ImportError: import pygame.event as EventModule +from events import Event + class TextRectException: def __init__(self, message = None): @@ -120,7 +122,7 @@ class GUI_PyGame: """A GUI class using PyGame""" def __init__(self, name, size): - # Call init routines + # Call init routines pygame.init() if hasattr(EventModule, 'init'): EventModule.init() @@ -147,8 +149,8 @@ class GUI_PyGame: 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 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)): # Use window size if none given @@ -176,6 +178,26 @@ class GUI_PyGame: text = render_textrect(msg, font, rect, color, bg, 1, 1) 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): while True: # Ignore all input that happened before entering the loop diff --git a/photobooth.py b/photobooth.py index 0b90bb5..243bb08 100755 --- a/photobooth.py +++ b/photobooth.py @@ -4,7 +4,6 @@ from __future__ import division import os -import subprocess from datetime import datetime from glob import glob from sys import exit @@ -12,17 +11,12 @@ from time import sleep from PIL import Image -import pygame -try: - import pygame.fastevent as eventmodule -except ImportError: - import pygame.event as eventmodule +from gui import GUI_PyGame as GuiModule -try: - import RPi.GPIO as GPIO - gpio_enabled = True -except ImportError: - gpio_enabled = False +from camera import Camera_gPhoto as CameraModule +from camera import CameraException + +from events import Rpi_GPIO as GPIO ##################### ### Configuration ### @@ -34,14 +28,8 @@ display_size = (1024, 600) # Maximum size of assembled image image_size = (2352, 1568) -# Idle image -image_idle = None - -# Pose image -image_pose = None - # 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_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_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 pose_time = 5 @@ -65,7 +50,7 @@ display_time = 10 ### Classes ### ############### -class Images: +class PictureList: """Class to manage images and count them""" def __init__(self, basename): # Set basename and suffix @@ -99,393 +84,171 @@ class Images: self.counter += 1 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): - """Returns a surface containing the passed text string, reformatted - to fit within the given rect, word-wrapping as necessary. The text - will be anti-aliased. +class Photobooth: + def __init__(self, picture_basename, picture_size, trigger_channel, shutdown_channel, lamp_channel): + self.display = GuiModule('Photobooth', display_size) + 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: - - string - the text you wish to render. \n begins a new line. - 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) + input_channels = [ trigger_channel, shutdown_channel ] + output_channels = [ lamp_channel ] + self.gpio = GPIO(self.handle_gpio, input_channels, output_channels) def teardown(self): - pygame.quit() + self.display.teardown() + self.gpio.teardown() + exit(0) -class CameraException(Exception): - """Custom exception class to handle gPhoto errors""" - pass + def run(self): + while True: + try: + # Enable lamp + self.gpio.set_output(self.lamp_channel, 1) -class Camera: - """Camera class providing functionality to take pictures""" - def __init__(self): - # Print the abilities of the connected camera - try: - print(self.call_gphoto("-a", "/dev/null")) - except CameraException as e: - handle_exception(e.message) + while True: + # 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 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 "Canon EOS Capture failed: 2019" in e.output: - raise CameraException("Can't focus! Move and try again!") - elif "No camera found" in e.output: - raise CameraException("No (supported) camera detected!") - else: - raise CameraException("Unknown error!\n" + '\n'.join(e.output.split('\n')[1:3])) - return output + except CameraException as e: + self.handle_exception(e.message) + + 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 == 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 ### ################# -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(): - setup_gpio() - while True: - try: - 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() + photobooth = Photobooth(picture_basename, image_size, gpio_trigger_channel, gpio_shutdown_channel, gpio_lamp_channel) + photobooth.run() + return photobooth.teardown() if __name__ == "__main__": exit(main()) \ No newline at end of file