From 809db26109cb370459428edce6565c90ddc34689 Mon Sep 17 00:00:00 2001 From: Balthasar Reuter Date: Sat, 14 Jul 2018 01:09:47 +0200 Subject: [PATCH] Camera module rewritten for new communicator scheme --- photobooth/StateMachine.py | 14 +- photobooth/Threading.py | 13 ++ photobooth/camera/CameraDummy.py | 4 +- photobooth/camera/CameraGphoto2.py | 4 +- photobooth/camera/CameraGphoto2Cffi.py | 4 +- photobooth/camera/CameraGphoto2CommandLine.py | 4 +- photobooth/camera/CameraInterface.py | 89 +++++++++++++ photobooth/camera/CameraOpenCV.py | 4 +- photobooth/camera/CameraPicamera.py | 4 +- photobooth/camera/__init__.py | 120 +++++++++++------- photobooth/gui/GuiSkeleton.py | 2 - photobooth/gui/Qt5Gui/PyQt5Gui.py | 10 +- photobooth/main.py | 59 +++++---- 13 files changed, 242 insertions(+), 89 deletions(-) create mode 100644 photobooth/camera/CameraInterface.py diff --git a/photobooth/StateMachine.py b/photobooth/StateMachine.py index 0fef796..63753a8 100644 --- a/photobooth/StateMachine.py +++ b/photobooth/StateMachine.py @@ -22,9 +22,10 @@ import logging class Context: - def __init__(self): + def __init__(self, communicator): super().__init__() + self._comm = communicator self.state = WelcomeState() @property @@ -41,6 +42,7 @@ class Context: logging.debug('New state is "{}"'.format(new_state)) self._state = new_state + self._comm.bcast(self._state) def handleEvent(self, event): @@ -140,9 +142,15 @@ class GpioEvent(Event): class CameraEvent(Event): - def __init__(self, name): + def __init__(self, name, picture=None): super().__init__(name) + self._picture = picture + + @property + def picture(self): + + return self._picture class WorkerEvent(Event): @@ -343,7 +351,7 @@ class CaptureState(State): def handleEvent(self, event, context): - if isinstance(event, CameraEvent) and event.name == 'next': + if isinstance(event, CameraEvent) and event.name == 'countdown': context.state = CountdownState() elif isinstance(event, CameraEvent) and event.name == 'assemble': context.state = AssembleState() diff --git a/photobooth/Threading.py b/photobooth/Threading.py index ec723da..b35ea3f 100644 --- a/photobooth/Threading.py +++ b/photobooth/Threading.py @@ -29,6 +29,11 @@ class Communicator: self._queues = [Queue() for _ in Workers] + def bcast(self, message): + + for q in self._queues[1:]: + q.put(message) + def send(self, target, message): if not isinstance(target, Workers): @@ -50,8 +55,16 @@ class Communicator: return iter(self._queues[worker].get, None) + def empty(self, worker): + + if not isinstance(worker, Workers): + raise TypeError('worker must be a member of Workers') + + return self._queues[worker].empty() + class Workers(IntEnum): + MASTER = 0 GUI = 1 CAMERA = 2 diff --git a/photobooth/camera/CameraDummy.py b/photobooth/camera/CameraDummy.py index eacc241..c46adc3 100644 --- a/photobooth/camera/CameraDummy.py +++ b/photobooth/camera/CameraDummy.py @@ -22,10 +22,10 @@ from colorsys import hsv_to_rgb from PIL import Image -from . import Camera +from .CameraInterface import CameraInterface -class CameraDummy(Camera): +class CameraDummy(CameraInterface): def __init__(self): diff --git a/photobooth/camera/CameraGphoto2.py b/photobooth/camera/CameraGphoto2.py index 1d95039..71b131f 100644 --- a/photobooth/camera/CameraGphoto2.py +++ b/photobooth/camera/CameraGphoto2.py @@ -24,10 +24,10 @@ from PIL import Image import gphoto2 as gp -from . import Camera +from .CameraInterface import CameraInterface -class CameraGphoto2(Camera): +class CameraGphoto2(CameraInterface): def __init__(self): diff --git a/photobooth/camera/CameraGphoto2Cffi.py b/photobooth/camera/CameraGphoto2Cffi.py index 4f93e5a..e33c3e7 100644 --- a/photobooth/camera/CameraGphoto2Cffi.py +++ b/photobooth/camera/CameraGphoto2Cffi.py @@ -24,10 +24,10 @@ from PIL import Image import gphoto2cffi as gp -from . import Camera +from .CameraInterface import CameraInterface -class CameraGphoto2Cffi(Camera): +class CameraGphoto2Cffi(CameraInterface): def __init__(self): diff --git a/photobooth/camera/CameraGphoto2CommandLine.py b/photobooth/camera/CameraGphoto2CommandLine.py index 451d8bc..db87513 100644 --- a/photobooth/camera/CameraGphoto2CommandLine.py +++ b/photobooth/camera/CameraGphoto2CommandLine.py @@ -23,10 +23,10 @@ import subprocess from PIL import Image -from . import Camera +from .CameraInterface import CameraInterface -class CameraGphoto2CommandLine(Camera): +class CameraGphoto2CommandLine(CameraInterface): def __init__(self): diff --git a/photobooth/camera/CameraInterface.py b/photobooth/camera/CameraInterface.py new file mode 100644 index 0000000..64447a8 --- /dev/null +++ b/photobooth/camera/CameraInterface.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# Photobooth - a flexible photo booth software +# Copyright (C) 2018 Balthasar Reuter +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + + +class CameraInterface: + + def __init__(self): + + self.hasPreview = False + self.hasIdle = False + + def __enter__(self): + + return self + + def __exit__(self, exc_type, exc_value, traceback): + + self.cleanup() + + def cleanup(self): + + pass + + @property + def hasPreview(self): + + return self._has_preview + + @hasPreview.setter + def hasPreview(self, value): + + if not isinstance(value, bool): + raise ValueError('Expected bool') + + self._has_preview = value + + @property + def hasIdle(self): + + return self._has_idle + + @hasIdle.setter + def hasIdle(self, value): + + if not isinstance(value, bool): + raise ValueError('Expected bool') + + self._has_idle = value + + def setActive(self): + + if not self.hasIdle: + pass + else: + raise NotImplementedError() + + def setIdle(self): + + if not self.hasIdle: + raise RuntimeError('Camera does not have idle functionality') + + raise NotImplementedError() + + def getPreview(self): + + if not self.hasPreview: + raise RuntimeError('Camera does not have preview functionality') + + raise NotImplementedError() + + def getPicture(self): + + raise NotImplementedError() diff --git a/photobooth/camera/CameraOpenCV.py b/photobooth/camera/CameraOpenCV.py index d820ae6..b05adb4 100644 --- a/photobooth/camera/CameraOpenCV.py +++ b/photobooth/camera/CameraOpenCV.py @@ -23,10 +23,10 @@ from PIL import Image import cv2 -from . import Camera +from .CameraInterface import CameraInterface -class CameraOpenCV(Camera): +class CameraOpenCV(CameraInterface): def __init__(self): diff --git a/photobooth/camera/CameraPicamera.py b/photobooth/camera/CameraPicamera.py index 95d502d..77a0e67 100644 --- a/photobooth/camera/CameraPicamera.py +++ b/photobooth/camera/CameraPicamera.py @@ -24,10 +24,10 @@ from PIL import Image from picamera import PiCamera -from . import Camera +from .CameraInterface import CameraInterface -class CameraPicamera(Camera): +class CameraPicamera(CameraInterface): def __init__(self): diff --git a/photobooth/camera/__init__.py b/photobooth/camera/__init__.py index 98083f6..62601a3 100644 --- a/photobooth/camera/__init__.py +++ b/photobooth/camera/__init__.py @@ -17,6 +17,14 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +import logging + +from PIL import Image, ImageOps + +from ..PictureDimensions import PictureDimensions +from .. import StateMachine +from ..Threading import Workers + # Available camera modules as tuples of (config name, module name, class name) modules = ( ('python-gphoto2', 'CameraGphoto2', 'CameraGphoto2'), @@ -30,70 +38,94 @@ modules = ( class Camera: - def __init__(self): + def __init__(self, config, comm, CameraModule): - self.hasPreview = False - self.hasIdle = False + super().__init__() - def __enter__(self): + self._comm = comm + self._cap = CameraModule() + self._pic_dims = PictureDimensions(config, self._cap.getPicture().size) - return self + self._is_preview = (config.getBool('Photobooth', 'show_preview') and + self._cap.hasPreview) + self._is_keep_pictures = config.getBool('Photobooth', 'keep_pictures') - def __exit__(self, exc_type, exc_value, traceback): + logging.info('Using camera {} preview functionality'.format( + 'with' if self.is_preview else 'without')) - self.cleanup() + self.setIdle() - def cleanup(self): + def teardown(self): - pass + self._cap.cleanup() - @property - def hasPreview(self): + def run(self): - return self._has_preview + for state in self._comm.iter(Workers.CAMERA): + self.handleEvent(state) - @hasPreview.setter - def hasPreview(self, value): + def handleEvent(self, event): - if not isinstance(value, bool): - raise ValueError('Expected bool') - - self._has_preview = value - - @property - def hasIdle(self): - - return self._has_idle - - @hasIdle.setter - def hasIdle(self, value): - - if not isinstance(value, bool): - raise ValueError('Expected bool') - - self._has_idle = value + if isinstance(event, StateMachine.GreeterState): + self.prepareCapture() + elif isinstance(event, StateMachine.CountdownState): + self.capturePreview() + elif isinstance(event, StateMachine.CaptureState): + self.capturePicture() + elif isinstance(event, StateMachine.AssembleState): + self.assemblePicture() + elif isinstance(event, StateMachine.TeardownState): + self.teardown() def setActive(self): - if not self.hasIdle: - pass - else: - raise NotImplementedError() + self._cap.setActive() def setIdle(self): - if not self.hasIdle: - raise RuntimeError('Camera does not have idle functionality') + if self._cap.hasIdle: + self._cap.setIdle() - raise NotImplementedError() + def prepareCapture(self): - def getPreview(self): + self.setActive() + self._pictures = [] - if not self.hasPreview: - raise RuntimeError('Camera does not have preview functionality') + def capturePreview(self): - raise NotImplementedError() + if self._is_preview: + while self._comm.empty(Workers.CAMERA): + picture = ImageOps.mirror(self._cap.getPreview()) + self._comm.send(Workers.GUI, + StateMachine.CameraEvent('preview', picture)) - def getPicture(self): + def capturePicture(self): - raise NotImplementedError() + self.setIdle() + picture = self._cap.getPicture() + self._pictures.append(picture) + self.setActive() + + if self._is_keep_pictures: + self._comm.send(Workers.WORKER, + StateMachine.CameraEvent('capture', picture)) + + if len(self._pictures) < self._pic_dims.totalNumPictures: + self._comm.send(Workers.MASTER, + StateMachine.CameraEvent('countdown', picture)) + else: + self._comm.send(Workers.MASTER, + StateMachine.CameraEvent('assemble', picture)) + + def assemblePicture(self): + + self.setIdle() + + picture = Image.new('RGB', self._pic_dims.outputSize, (255, 255, 255)) + for i in range(self._pic_dims.totalNumPictures): + resized = self._pictures[i].resize(self._pic_dims.thumbnailSize) + picture.paste(resized, self._pic_dims.thumbnailOffset[i]) + + self._comm.send(Workers.MASTER, + StateMachine.CameraEvent('review', picture)) + self._pictures = [] diff --git a/photobooth/gui/GuiSkeleton.py b/photobooth/gui/GuiSkeleton.py index 3e36ad4..25bc1cc 100644 --- a/photobooth/gui/GuiSkeleton.py +++ b/photobooth/gui/GuiSkeleton.py @@ -19,8 +19,6 @@ from . import GuiState -from .. import StateMachine - class GuiSkeleton: diff --git a/photobooth/gui/Qt5Gui/PyQt5Gui.py b/photobooth/gui/Qt5Gui/PyQt5Gui.py index 29d42be..7ceb3c6 100644 --- a/photobooth/gui/Qt5Gui/PyQt5Gui.py +++ b/photobooth/gui/Qt5Gui/PyQt5Gui.py @@ -27,8 +27,8 @@ from PyQt5 import QtWidgets from PIL import ImageQt -from ... import StateMachine -from ...Threading import Workers +# from ... import StateMachine +# from ...Threading import Workers from .. import GuiState from ..GuiSkeleton import GuiSkeleton @@ -184,8 +184,8 @@ class PyQt5Gui(GuiSkeleton): self.restart)) def _showSettings(self): - - self._comm.send(Workers.MASTER, StateMachine.GuiEvent('settings')) + + # self._comm.send(Workers.MASTER, StateMachine.GuiEvent('settings')) self._disableTrigger() self._disableEscape() @@ -195,7 +195,7 @@ class PyQt5Gui(GuiSkeleton): def _showStart(self, state): - self._comm.send(Workers.MASTER, StateMachine.GuiEvent('start')) + # self._comm.send(Workers.MASTER, StateMachine.GuiEvent('start')) self._disableTrigger() self._enableEscape() diff --git a/photobooth/main.py b/photobooth/main.py index 1eb57d6..8b75040 100644 --- a/photobooth/main.py +++ b/photobooth/main.py @@ -30,33 +30,34 @@ import sys from . import camera, gui from .Config import Config -from .Photobooth import Photobooth +# from .Photobooth import Photobooth from .util import lookup_and_import -from .StateMachine import Context +from .StateMachine import Context, ErrorEvent from .Threading import Communicator, Workers from .Worker import Worker class CameraProcess(mp.Process): - def __init__(self, config, conn, worker_queue): + def __init__(self, config, comm): # conn, worker_queue): super().__init__() self.daemon = True self.cfg = config - self.conn = conn - self.worker_queue = worker_queue + self.comm = comm + # self.conn = conn + # self.worker_queue = worker_queue - def run_camera(self): + # def run_camera(self): # try: - cap = lookup_and_import( - camera.modules, self.cfg.get('Camera', 'module'), 'camera') + # # cap = lookup_and_import( + # # camera.modules, self.cfg.get('Camera', 'module'), 'camera') - photobooth = Photobooth( - self.cfg, cap, self.conn, self.worker_queue) - return photobooth.run() + # # photobooth = Photobooth( + # # self.cfg, cap, self.conn, self.worker_queue) + # # return photobooth.run() # except BaseException as e: # self.conn.send(gui.GuiState.ErrorState('Camera error', str(e))) @@ -69,19 +70,30 @@ class CameraProcess(mp.Process): def run(self): - status_code = 123 + CameraModule = lookup_and_import(camera.modules, + self.cfg.get('Camera', 'module'), + 'camera') + cap = camera.Camera(self.cfg, self.comm, CameraModule) - while status_code == 123: - event = self.conn.recv() + while True: + try: + cap.run() + except Exception as e: + self.comm.send(Workers.MASTER, ErrorEvent(e)) - if str(event) != 'start': - logging.warning('Unknown event received: %s', str(event)) - continue + # status_code = 123 - status_code = self.run_camera() - logging.info('Camera exited with status code %d', status_code) + # while status_code == 123: + # event = self.conn.recv() - sys.exit(status_code) + # if str(event) != 'start': + # logging.warning('Unknown event received: %s', str(event)) + # continue + + # status_code = self.run_camera() + # logging.info('Camera exited with status code %d', status_code) + + # sys.exit(status_code) class WorkerProcess(mp.Process): @@ -115,7 +127,8 @@ class GuiProcess(mp.Process): Gui = lookup_and_import(gui.modules, self.cfg.get('Gui', 'module'), 'gui') - sys.exit(Gui(self.argv, self.cfg, self.conn, self.queue, self.comm).run()) + sys.exit(Gui(self.argv, self.cfg, self.conn, self.queue, + self.comm).run()) def run(argv): @@ -126,7 +139,7 @@ def run(argv): config = Config('photobooth.cfg') comm = Communicator() - context = Context() + context = Context(comm) # Create communication objects: # 1. We use a pipe to connect GUI and camera process @@ -138,7 +151,7 @@ def run(argv): # 1. Camera processing # 2. Postprocessing # 3. GUI - camera_proc = CameraProcess(config, camera_conn, worker_queue) + camera_proc = CameraProcess(config, comm) # camera_conn, worker_queue) camera_proc.start() worker_proc = WorkerProcess(config, worker_queue)