Exemplo n.º 1
0
class Photobooth:
    """The main class.

    It contains all the logic for the photobooth.
    """
    def __init__(self, display_size, display_rotate, 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)
        if (display_size == (0, 0)):  # Detect actual resolution
            display_size = self.display.get_size()
        self.display_rotate = display_rotate
        if (display_rotate):
            self.display.set_rotate(True)

        self.pictures = PictureList(picture_basename)
        self.camera = CameraModule((picture_size[0] / 2, picture_size[1] / 2),
                                   camera_rotate=camera_rotate)
        self.camera_rotate = camera_rotate

        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)))
            if (display_rotate):
                self.slideshow.display.set_rotate(True)

        input_channels = [trigger_channel, shutdown_channel]
        output_channels = [lamp_channel]
        self.gpio = GPIO(self.handle_gpio, input_channels, output_channels)

        self.printer_module = PrinterModule()
        try:
            pygame.mixer.init(buffer=1024)
            self.shutter = pygame.mixer.Sound(shutter_sound)
            self.bip1 = pygame.mixer.Sound(bip1_sound)
            self.bip2 = pygame.mixer.Sound(bip2_sound)
            self.bip2.play()
        except pygame.error:
            self.shutter = None
            pass

    def teardown(self):
        self.display.msg("Shutting down...")
        self.gpio.set_output(self.lamp_channel, 0)
        sleep(0.5)
        self.display.teardown()
        self.gpio.teardown()
        self.remove_tempfiles()
        exit(0)

    def remove_tempfiles(self):
        for filename in glob(tmp_dir + "photobooth_*.jpg"):
            try:
                os.remove(filename)
            except OSError:
                pass

    def _run_plain(self):
        while True:
            self.camera.set_idle()

            # Display default message
            self.display.msg("Hit the button!")

            # 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 = time()
            while time() - 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:
                import sys
                print('SERIOUS ERROR' + repr(e))
                sys.excepthook(*sys.exc_info())
                self.handle_exception("SERIOUS ERROR!\n(see log file)")

    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 clear_event_queue(self):
        r, e = self.display.check_for_event()
        while r:
            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()
        elif key == ord('f'):
            self.display.toggle_fullscreen()
        elif key == ord('i'):  # Re-initialize the camera for debugging
            self.camera.reinit()
        elif key == ord('p'):
            self.toggle_auto_print()
        elif key == ord('r'):
            self.toggle_rotate()
        elif key == ord('1'):  # Just for debugging
            self.show_preview_fps_1(5)
        elif key == ord('2'):  # Just for debugging
            self.show_preview_fps_2(5)
        elif key == ord('3'):  # Just for debugging
            self.show_preview_fps_3(5)

    def toggle_auto_print(self):
        "Toggle auto print and show an error message if printing isn't possible."
        if self.printer_module.can_print():
            global auto_print
            auto_print = not auto_print
            self.display.msg("Autoprinting %s" %
                             ("enabled" if auto_print else "disabled"))
        else:
            self.display.msg("Printing not configured\n(see log file)")

    def toggle_rotate(self):
        "Toggle rotating the display and camera."
        self.toggle_display_rotate()
        self.toggle_camera_rotate()
        self.display.msg("Display and camera rotated")

    def toggle_display_rotate(self):
        "Toggle rotating the display 90 degrees counter clockwise."
        self.display_rotate = (not self.display_rotate)
        self.display.set_rotate(self.display_rotate)
        self.slideshow.display.set_rotate(self.display_rotate)

    def toggle_camera_rotate(self):
        "Toggle rotating the camera 90 degrees counter clockwise."
        self.camera_rotate = (not self.camera_rotate)
        self.camera.set_rotate(self.camera_rotate)

    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"""
        print("Error: " + msg)
        try:
            self.display.msg("ERROR:\n\n" + msg)
        except GuiException:
            self.display.msg("ERROR")
        sleep(3)

    def assemble_pictures(self, input_filenames):
        """Assembles four pictures into a 2x2 grid of thumbnails.

        The total size (WxH) is assigned in the global variable
        assembled_size at the top of this file. (E.g., 2352x1568)

        The outer border (a) is 2% of W
        The inner border (b) is 1% of W

        Note that if the camera is on its side, H and W will be
        swapped to create a portrait, rather than landscape, montage.

        Thumbnail sizes are calculated like so:
        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

        [Note that extra padding will be added on the sides if the
        aspect ratio of the camera images do not match the aspect
        ratio of the final assembled image.]

        """

        # If the display is in portrait orientation,
        # we should create an assembled image that fits it.
        if self.display.get_rotate():
            (H, W) = self.pic_size
        else:
            (W, H) = self.pic_size

        # Thumbnail size of pictures
        outer_border = int(2 * max(W, H) / 100)  # 2% of long edge
        inner_border = int(1 * max(W, H) / 100)  # 1% of long edge
        thumb_box = (int(W / 2), int(H / 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', (W, H), (255, 255, 255))

        # Image 0
        img = Image.open(input_filenames[0])
        img = img.resize(maxpect(img.size, thumb_size), Image.ANTIALIAS)
        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 = img.resize(maxpect(img.size, thumb_size), Image.ANTIALIAS)
        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 = img.resize(maxpect(img.size, thumb_size), Image.ANTIALIAS)
        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 = img.resize(maxpect(img.size, thumb_size), Image.ANTIALIAS)
        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_preview(self, message=""):
        """If camera allows previews, take a photo and show it so people can
        pose before the shot. For speed, previews are decimated to fit
        within the screen instead of being scaled. For even more
        speed, the previews are blitted directly to a subsurface of
        the display. (Converting to a pygame Surface is slower). 

        """
        self.display.clear()
        if self.camera.has_preview():
            f = self.camera.get_preview_array(self.display.get_size())
            self.display.blit_array(f)
        self.display.show_message(message)
        self.display.apply()

    def show_counter(self, seconds):
        """Loop over showing the preview (if possible), with a count down"""
        tic = time()
        toc = time() - tic
        old_t = None
        while toc < seconds:
            t = seconds - int(toc)
            if t != old_t and self.bip1:
                self.bip1.play()
            old_t = t

            self.show_preview(str(t))
            # Limit progress to 1 "second" per preview (e.g., too slow on Raspi 1)
            toc = min(toc + 1, time() - tic)

    def show_preview_fps_1(self, seconds):
        """XXX Debugging code for benchmarking XXX

        This is the original show_countdown preview code. (~5fps)
        """

        import cv2, pygame, numpy
        tic = time()
        toc = 0
        frames = 0

        while toc < seconds:
            frames = frames + 1

            self.display.clear()
            if self.camera.has_preview():
                self.camera.take_preview(tmp_dir + "photobooth_preview.jpg")
                self.display.show_picture(tmp_dir + "photobooth_preview.jpg",
                                          flip=True)
            self.display.show_message(str(seconds - int(toc)))
            self.display.apply()

            toc = time() - tic

        self.display.msg("FPS: %d/%.2f = %.2f" %
                         (frames, toc, float(frames) / toc))
        print("Method 1 FPS: %d/%.2f = %.2f" %
              (frames, toc, float(frames) / toc))
        sleep(3)

    def show_preview_fps_2(self, seconds):
        """XXX Debugging code for benchmarking XXX

        As a test, I'm trying a direct conversion from OpenCV to a
        PyGame Surface in memory and it's much faster than the
        original code, but still slower than subsurface blitting.
        (~10fps)

        """

        import cv2, pygame, numpy
        tic = time()
        toc = 0
        frames = 0

        while toc < seconds:
            frames = frames + 1

            self.display.clear()

            # Capture a preview image from the camera as a pygame surface.
            s = self.camera.get_preview_pygame_surface()
            (w, h) = s.get_size()
            (dw, dh) = self.display.get_size()

            # Figure out maximum proportional scaling
            size = (dw, dh)
            image_size = (w, h)
            offset = (0, 0)

            # New image size
            new_size = maxpect(image_size, size)
            # Update offset
            offset = tuple(a + int((b - c) / 2)
                           for a, b, c in zip(offset, size, new_size))
            # Apply scaling
            s = pygame.transform.scale(s, new_size).convert()

            # Display it using undocumented interface to GUI_Pygame
            self.display.surface_list.append((s, offset))

            self.display.show_message(str(seconds - int(toc)))
            self.display.apply()

            toc = time() - tic

        self.display.msg("FPS: %d/%.2f = %.2f" %
                         (frames, toc, float(frames) / toc))
        print("Method 2 FPS: %d/%.2f = %.2f" %
              (frames, toc, float(frames) / toc))
        sleep(3)

    def show_preview_fps_3(self, seconds):
        """XXX Debugging code for benchmarking XXX

        This is the fastest method, which decimates the array and
        blits it directly to a subsurface of the display. (~14fps)

        """

        import cv2, pygame, numpy
        tic = time()
        toc = 0
        frames = 0

        while toc < seconds:
            frames = frames + 1

            self.display.clear()

            # Grab a preview, decimated to fit within the screen size
            f = self.camera.get_preview_array(self.display.get_size())

            # Blit it to the center of the screen
            self.display.blit_array(f)

            self.display.show_message(str(seconds - int(toc)))
            self.display.apply()

            toc = time() - tic

        self.display.msg("FPS: %d/%.2f = %.2f" %
                         (frames, toc, float(frames) / toc))
        print "Method 3 FPS: %d/%.2f = %.2f" % (frames, toc,
                                                float(frames) / toc)
        sleep(3)

    def show_pose(self, seconds, message=""):
        """Loop over showing the preview (if possible), with a static message.

        Note that this is *necessary* for OpenCV webcams as V4L will ramp the
        brightness level only after a certain number of frames have been taken.
        """

        tic = time()
        toc = time() - tic
        while toc < seconds:
            self.show_preview(message)
            # Limit progress to 1 "second" per preview (e.g., too slow on Raspi 1)
            toc = min(toc + 1, time() - tic)

    def take_picture(self):
        """Implements the picture taking routine"""
        # Disable lamp
        self.gpio.set_output(self.lamp_channel, 0)

        # Show pose message
        self.show_pose(2, "POSE!\n\nTaking 4 pictures ...")

        # 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((255, 230, 200))
                self.display.show_message("S M I L E !!!\n\n" + str(x + 1) +
                                          " of 4")
                self.display.apply()

                tic = time()

                try:
                    filenames[x] = self.camera.take_picture(
                        tmp_dir + "photobooth_%02d.jpg" % x)
                    remaining_attempts = 0
                    if self.shutter:
                        self.shutter.play()
                except CameraException as e:
                    # On recoverable errors: display message and retry
                    if e.recoverable:
                        if remaining_attempts > 0:
                            self.display.msg(e.message)
                            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 = time() - tic
                if toc < 1.0:
                    sleep(1.0 - toc)

        # Show 'Wait'
        self.display.msg("Please wait!\n\nWorking\n...")

        # Assemble them
        outfile = self.assemble_pictures(filenames)

        if self.printer_module.can_print():
            # Show picture for 10 seconds and then send it to the printer.
            # If auto_print is True,  hitting the button cancels the print.
            # If auto_print is False, hitting the button sends the print
            tic = time()
            t = int(self.display_time - (time() - tic))
            old_t = self.display_time + 1
            button_pressed = False

            # Clear event queue (in case they hit the button twice accidentally)
            self.clear_event_queue()

            while t > 0:
                if t != old_t:
                    self.display.clear()
                    self.display.show_picture(outfile, size, (0, 0))
                    self.display.show_message(
                        "%s%d" %
                        ("Printing in " if auto_print else "Print photo?\n",
                         t))
                    self.display.apply()
                    old_t = t

                # Flash the lamp so they'll know they can hit the button
                self.gpio.set_output(self.lamp_channel, int(time() * 2) % 2)

                # Watch for button, gpio, mouse press to cancel/enable printing
                r, e = self.display.check_for_event()
                if r:  # Caught a button press.
                    self.display.clear()
                    self.display.show_picture(outfile, size, (0, 0))
                    self.display.show_message(
                        "Printing%s" % (" cancelled" if auto_print else ""))
                    self.display.apply()
                    self.gpio.set_output(self.lamp_channel, 0)
                    sleep(1)

                    # Discard extra events (e.g., they hit the button a bunch)
                    self.clear_event_queue()

                    button_pressed = True
                    break

                t = int(self.display_time - (time() - tic))

            # Either button pressed or countdown timed out
            self.gpio.set_output(self.lamp_channel, 0)
            if auto_print ^ button_pressed:
                self.display.msg("Printing")
                self.printer_module.enqueue(outfile)

        else:
            # No printer available, so just show montage 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)
Exemplo n.º 2
0
class Photobooth:
    """The main class.

    It contains all the logic for the photobooth.
    """
    def __init__(self, display_size, display_rotate, picture_size, pose_time,
                 display_time, idle_slideshow, slideshow_display_time):
        self.display = GuiModule('Photobooth', display_size)
        if display_size == (0, 0):  # Detect actual resolution
            display_size = self.display.get_size()
        self.display_rotate = display_rotate
        if display_rotate:
            self.display.set_rotate(True)
        # Image basename
        picture_basename = datetime.now().strftime("%Y-%m-%d/pic")
        assembled_picture_basename = datetime.now().strftime(
            "%Y-%m-%d/assembled/pic")

        self.pictures = PictureList(picture_basename)
        self.assembled_pictures = PictureList(assembled_picture_basename)
        self.taking_picture = Lock()
        self.arduino = ArduinoSerial()
        self.camera = CameraModule((picture_size[0] / 2, picture_size[1] / 2),
                                   camera_rotate=camera_rotate)
        self.camera_rotate = camera_rotate

        self.picture_size = picture_size
        self.pose_time = pose_time
        self.display_time = display_time

        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)),
                                       recursive=False)
            if display_rotate:
                self.slideshow.display.set_rotate(True)

        self.printer_module = PrinterModule()
        try:
            pygame.mixer.init(buffer=1024)
            self.shutter = pygame.mixer.Sound(shutter_sound)
            self.bip1 = pygame.mixer.Sound(bip1_sound)
            self.bip2 = pygame.mixer.Sound(bip2_sound)
            self.bip2.play()
        except pygame.error:
            self.shutter = None
            pass
        self.arduino.start()

    def teardown(self):
        self.arduino.stop()
        self.arduino.join()
        self.display.msg("Shutting down...")
        self.display.teardown()
        self.remove_tempfiles()
        sleep(0.5)

        exit(0)

    def remove_tempfiles(self):
        for filename in glob(tmp_dir + "photobooth_*.jpg"):
            try:
                os.remove(filename)
            except OSError:
                pass

    def _run_plain(self):
        while True:
            self.camera.set_idle()

            # Display default message
            self.display.msg("Hit the button!")

            # Wait for an event and handle it
            event = 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 = time()
            while time() - tic < self.slideshow_display_time:
                self.check_and_handle_events()
                sleep(0.1)

    def run(self):
        while True:
            try:
                # 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:
                import sys
                print('SERIOUS ERROR' + repr(e))
                sys.excepthook(*sys.exc_info())
                self.handle_exception("SERIOUS ERROR!\n(see log file)")

    def check_and_handle_events(self):
        r, e = check_for_event()
        while r:
            self.handle_event(e)
            r, e = check_for_event()

    def handle_serial_command(self, event):
        with self.taking_picture:
            self.take_picture()

    def clear_event_queue(self):
        r, e = check_for_event()
        while r:
            r, e = check_for_event()

    def handle_event(self, event):
        if event.type == 0:
            self.teardown()
        if self.taking_picture.locked():
            return
        if 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_serial_command(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'):
            with self.taking_picture:
                self.take_picture()
        elif key == ord('f'):
            print("ToggleScreen")
            self.display.toggle_fullscreen()
        elif key == ord('i'):  # Re-initialize the camera for debugging
            self.camera.reinit()
        elif key == ord('p'):
            self.toggle_auto_print()
        elif key == ord('r'):
            self.toggle_rotate()

    def toggle_auto_print(self):
        "Toggle auto print and show an error message if printing isn't possible."
        if self.printer_module.can_print():
            global auto_print
            auto_print = not auto_print
            self.display.msg("Autoprinting %s" %
                             ("enabled" if auto_print else "disabled"))
        else:
            self.display.msg("Printing not configured\n(see log file)")

    def toggle_rotate(self):
        "Toggle rotating the display and camera."
        self.toggle_display_rotate()
        self.toggle_camera_rotate()
        self.display.msg("Display and camera rotated")

    def toggle_display_rotate(self):
        "Toggle rotating the display 90 degrees counter clockwise."
        self.display_rotate = (not self.display_rotate)
        self.display.set_rotate(self.display_rotate)
        self.slideshow.display.set_rotate(self.display_rotate)

    def toggle_camera_rotate(self):
        "Toggle rotating the camera 90 degrees counter clockwise."
        self.camera_rotate = (not self.camera_rotate)
        self.camera.set_rotate(self.camera_rotate)

    def handle_mousebutton(self, key, pos):
        """Implements the actions for the different mousebutton events"""
        # Take a picture
        if key == 1:
            with self.taking_picture:
                self.take_picture()

    def handle_exception(self, msg):
        """Displays an error message and returns"""
        print("Error: " + msg)
        try:
            self.display.msg("ERROR:\n\n" + msg)
        except GuiException:
            self.display.msg("ERROR")
        sleep(3)

    def assemble_picture(self, input_filename):
        (H, W) = self.picture_size

        # Thumbnail size of pictures
        inner_border = 0
        thumb_box = (int(W), int(H / 2))
        thumb_size = (thumb_box[0], thumb_box[1] - inner_border / 2)

        # Create output image with white background
        output_image = Image.new('RGB', (W, H), (255, 255, 255))

        # Image 0
        img = Image.open(input_filename)
        img = img.resize(maxpect(img.size, thumb_size), Image.ANTIALIAS)

        crop = int((img.size[1] - thumb_size[1]) / 2)

        img = img.crop((0, crop, img.size[0], img.size[1] - crop))

        offset = (0, 0)
        output_image.paste(img, offset)

        # Image copy
        offset = (0, thumb_box[1] + inner_border)
        output_image.paste(img, offset)

        # Save assembled image
        ass_output_filename = self.assembled_pictures.get_next()
        output_image.save(ass_output_filename, "JPEG", quality=95)

        # Save Input Image
        output_filename = self.pictures.get_next()
        img.save(output_filename, "JPEG", quality=95)

        return ass_output_filename, output_filename

    def assemble_pictures(self, input_filenames):
        """Assembles four pictures into a 2x2 grid of thumbnails.

        The total size (WxH) is assigned in the global variable
        assembled_size at the top of this file. (E.g., 2352x1568)

        The outer border (a) is 2% of W
        The inner border (b) is 1% of W

        Note that if the camera is on its side, H and W will be
        swapped to create a portrait, rather than landscape, montage.

        Thumbnail sizes are calculated like so:
        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

        [Note that extra padding will be added on the sides if the
        aspect ratio of the camera images do not match the aspect
        ratio of the final assembled image.]

        """

        # If the display is in portrait orientation,
        # we should create an assembled image that fits it.
        if self.display.get_rotate():
            (H, W) = self.picture_size
        else:
            (W, H) = self.picture_size

        # Thumbnail size of pictures
        outer_border = int(2 * max(W, H) / 100)  # 2% of long edge
        inner_border = int(1 * max(W, H) / 100)  # 1% of long edge
        thumb_box = (int(W / 2), int(H / 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', (W, H), (255, 255, 255))

        # Image 0
        img = Image.open(input_filenames[0])
        img = img.resize(maxpect(img.size, thumb_size), Image.ANTIALIAS)
        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 = img.resize(maxpect(img.size, thumb_size), Image.ANTIALIAS)
        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 = img.resize(maxpect(img.size, thumb_size), Image.ANTIALIAS)
        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 = img.resize(maxpect(img.size, thumb_size), Image.ANTIALIAS)
        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_preview(self, message=""):
        """If camera allows previews, take a photo and show it so people can
        pose before the shot. For speed, previews are decimated to fit
        within the screen instead of being scaled. For even more
        speed, the previews are blitted directly to a subsurface of
        the display. (Converting to a pygame Surface is slower). 

        """
        self.display.clear()
        if self.camera.has_preview():
            f = self.camera.get_preview_array(self.display.get_size())
            self.display.blit_array(f)
        self.display.show_message(message)
        self.display.apply()

    def show_counter(self, seconds):
        """Loop over showing the preview (if possible), with a count down"""
        tic = time()
        toc = time() - tic
        old_t = None
        while toc < seconds:
            t = seconds - int(toc)
            if t != old_t and self.bip1:
                self.display.msg(str(t))
                self.bip1.play()
                self.say("{}".format(t))
            old_t = t
            self.display.msg(str(t))

            # Limit progress to 1 "second" per preview (e.g., too slow on Raspi 1)
            toc = min(toc + 1, time() - tic)

    def show_pose(self, seconds, message=""):
        """Loop over showing the preview (if possible), with a static message.

        Note that this is *necessary* for OpenCV webcams as V4L will ramp the
        brightness level only after a certain number of frames have been taken.
        """

        tic = time()
        toc = time() - tic
        while toc < seconds:
            self.show_preview(message)
            # Limit progress to 1 "second" per preview (e.g., too slow on Raspi 1)
            toc = min(toc + 1, time() - tic)

    def say(self, text, voice="Alex"):
        os.system("say -v {} -r 200 '{}'".format(voice, text))

    def say_with_delay(self, text, voice="Alex", delay=0.3):
        sleep(delay)
        self.say(text, voice)

    def take_picture(self):
        """Implements the picture taking routine"""
        # NUM PICs
        num_pics = 1
        t = threading.Thread(target=self.say, args=("Pose for the photo!", ))
        t.start()

        activate_olympus = """
        osascript -e 'tell application "OLYMPUS Capture" to activate'
        """
        os.system(activate_olympus)
        self.display.msg(
            "\n\n\n\n\n\n\nTaking {} picture ...".format(num_pics))
        sleep(3.0)
        t.join()
        # Extract display and image sizes
        display_size = self.display.get_size()

        focus_python()
        self.display.msg("Look at the camera...".format(num_pics))
        self.say("Look at the camera")

        sleep(0.5)
        words = random.choice(word_list)
        #words.append(0.3)
        # Take pictures
        filenames = [i for i in range(num_pics)]
        for x in range(num_pics):
            # Countdown
            self.show_counter(self.pose_time)
            self.display.msg("")
            #self.display.msg("S M I L E !!!\n\n {} of {}".format(x + 1, num_pics))

            t = threading.Thread(target=self.say_with_delay, args=tuple(words))
            t.start()
            try:
                #t = threading.Thread(target=self.camera.take_picture, args=(tmp_dir + "photobooth_%02d.jpg" % x, ))
                #t.start()

                filenames[x] = self.camera.take_picture(tmp_dir +
                                                        "photobooth_%02d.jpg" %
                                                        x)
                #if self.shutter:
                #    self.shutter.play()

            except CameraException as e:
                raise e
            t.join()
        # Show 'Wait'
        self.display.msg("Please wait!\n\nWorking\n...")

        # Assemble them
        ass_outfile, outfile = self.assemble_picture(filenames[0])

        if self.printer_module.can_print():
            # Show picture for 10 seconds and then send it to the printer.
            # If auto_print is True,  hitting the button cancels the print.
            # If auto_print is False, hitting the button sends the print
            tic = time()
            t = int(self.display_time - (time() - tic))
            old_t = self.display_time + 1
            do_print = auto_print
            # Clear event queue (in case they hit the button twice accidentally)
            self.clear_event_queue()

            while t > 0:
                if t != old_t:
                    self.display.clear()
                    self.display.show_picture(outfile, display_size, (0, 0))
                    self.display.show_message(
                        "%s%d" %
                        ("Wait for print \n or press button to cancel!\n " if
                         auto_print else "Press button to print photo!\n", t))
                    self.display.apply()
                    old_t = t

                # Watch for button, gpio, mouse press to cancel/enable printing
                r, e = check_for_event()
                if r:  # Caught a button press.
                    self.display.clear()
                    self.display.show_picture(outfile, display_size, (0, 0))
                    self.display.show_message(
                        "Printing%s" % (" cancelled" if auto_print else ""))
                    self.display.apply()
                    self.clear_event_queue()
                    # Discard extra events (e.g., they hit the button a bunch)
                    sleep(2)
                    self.clear_event_queue()
                    do_print = not do_print

                    break
                sleep(0.1)
                t = int(self.display_time - (time() - tic))

            # Either button pressed or countdown timed out
            if do_print:
                self.display.msg("Printing")
                self.printer_module.enqueue(ass_outfile)

        else:
            # No printer available, so just show montage for 10 seconds
            self.display.clear()
            self.display.show_picture(outfile, display_size, (0, 0))
            self.display.apply()
            sleep(self.display_time)
        self.clear_event_queue()