Camera module rewritten for new communicator scheme
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
|
||||
89
photobooth/camera/CameraInterface.py
Normal file
89
photobooth/camera/CameraInterface.py
Normal 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()
|
||||
@@ -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):
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
@@ -19,8 +19,6 @@
|
||||
|
||||
from . import GuiState
|
||||
|
||||
from .. import StateMachine
|
||||
|
||||
|
||||
class GuiSkeleton:
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user