Camera module rewritten for new communicator scheme

This commit is contained in:
Balthasar Reuter
2018-07-14 01:09:47 +02:00
parent f8918e895e
commit 809db26109
13 changed files with 242 additions and 89 deletions

View File

@@ -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()

View File

@@ -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

View File

@@ -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):

View File

@@ -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):

View File

@@ -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):

View File

@@ -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):

View File

@@ -0,0 +1,89 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Photobooth - a flexible photo booth software
# Copyright (C) 2018 Balthasar Reuter <photobooth at re - web dot eu>
#
# 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 <http://www.gnu.org/licenses/>.
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()

View File

@@ -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):

View File

@@ -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):

View File

@@ -17,6 +17,14 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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 = []

View File

@@ -19,8 +19,6 @@
from . import GuiState
from .. import StateMachine
class GuiSkeleton:

View File

@@ -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()

View File

@@ -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)