From 3e27a46618254358ff423770f3205d1dbb2ff3e5 Mon Sep 17 00:00:00 2001 From: Balthasar Reuter Date: Fri, 23 Mar 2018 23:27:38 +0100 Subject: [PATCH] Rudimentary trigger emulation using OpenCV --- INSTALL.md | 1 + photobooth/Camera.py | 58 ++++++++++++++++++ photobooth/CameraOpenCV.py | 33 ++++++++++ photobooth/Gui.py | 89 +++++++++++++++++++++++++++ photobooth/Photobooth.py | 33 +++++++--- photobooth/PyQt5Gui.py | 119 +++++++++++++++++++++++++++---------- 6 files changed, 294 insertions(+), 39 deletions(-) create mode 100644 photobooth/Camera.py create mode 100644 photobooth/CameraOpenCV.py create mode 100644 photobooth/Gui.py diff --git a/INSTALL.md b/INSTALL.md index de07549..6df872c 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -1 +1,2 @@ pip install pyqt5 +pip install opencv-python diff --git a/photobooth/Camera.py b/photobooth/Camera.py new file mode 100644 index 0000000..1331927 --- /dev/null +++ b/photobooth/Camera.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +class Camera: + + def __init__(self): + + self.hasPreview = False + self.hasIdle = False + + @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 getPreview(self): + + if not self.hasPreview: + raise RuntimeError('Camera does not have preview functionality') + + raise NotImplementedError() + + + def getPicture(self): + + raise NotImplementedError() + + + def setIdle(self): + + if not self.hasIdle: + raise RuntimeError('Camera does not support idle state') + + raise NotImplementedError() \ No newline at end of file diff --git a/photobooth/CameraOpenCV.py b/photobooth/CameraOpenCV.py new file mode 100644 index 0000000..f097952 --- /dev/null +++ b/photobooth/CameraOpenCV.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from Camera import Camera + +import cv2 + +class CameraOpenCV(Camera): + + def __init__(self): + + super().__init__() + + self.hasPreview = True + self.hasIdle = False + + self._cap = cv2.VideoCapture(-1) + if not self._cap.isOpened(): + raise RuntimeError('Camera could not be opened') + + + def getPreview(self): + + return self.getPicture() + + + def getPicture(self): + + _, frame = self._cap.read() + # OpenCV yields frames in BGR format, + # see https://stackoverflow.com/a/32270308 + return cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + diff --git a/photobooth/Gui.py b/photobooth/Gui.py new file mode 100644 index 0000000..b33fc74 --- /dev/null +++ b/photobooth/Gui.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +class Gui: + + def __init__(self): + + pass + + + def run(self, send, recv): + + raise NotImplementedError() + + + +class GuiState: + + def __init__(self, **kwargs): + + assert not kwargs + + +class IdleState(GuiState): + + def __init__(self, **kwargs): + + super().__init__(**kwargs) + + + +class PictureState(GuiState): + + def __init__(self, picture, **kwargs): + + super().__init__(**kwargs) + + self._pic = picture + + + @property + def picture(self): + + return self._pic + + + @picture.setter + def picture(self, picture): + + self._pic = picture + + +class MessageState(GuiState): + + def __init__(self, message, **kwargs): + + super().__init__(**kwargs) + + self._msg = message + + + @property + def message(self): + + return self._msg + + + @message.setter + def message(self, message): + + if not isinstance(message, str): + raise ValueError('Message must be a string') + + self._msg = message + + + +class PoseState(GuiState): + + def __init__(self, **kwargs): + + super().__init__(**kwargs) + + +class PreviewState(MessageState, PictureState): + + def __init__(self, **kwargs): + + super().__init__(**kwargs) diff --git a/photobooth/Photobooth.py b/photobooth/Photobooth.py index ff09b85..67c610b 100644 --- a/photobooth/Photobooth.py +++ b/photobooth/Photobooth.py @@ -2,18 +2,20 @@ # -*- coding: utf-8 -*- from Config import Config +import Gui from PyQt5Gui import PyQt5Gui +from CameraOpenCV import CameraOpenCV as Camera from multiprocessing import Pipe, Process -from time import sleep +from time import clock, sleep class Photobooth: def __init__(self): - pass + self._cap = Camera() def run(self, send, recv): @@ -24,7 +26,6 @@ class Photobooth: except EOFError: return 1 else: - print('Photobooth: ' + event) self.trigger(send) return 0 @@ -32,15 +33,29 @@ class Photobooth: def trigger(self, send): - send.send('Pose') - - sleep(3) - - send.send('Picture') + send.send(Gui.PoseState()) sleep(2) - send.send('idle') + if self._cap.hasPreview: + tic = clock() + toc = clock() - tic + + while toc < 3: + send.send( Gui.PreviewState( + message = str(3 - int(toc)), + picture = self._cap.getPreview() ) ) + toc = clock() - tic + else: + for i in range(3): + send.send( Gui.PreviewState(str(i)) ) + sleep(1) + + send.send(Gui.PictureState(self._cap.getPicture())) + + sleep(2) + + send.send(Gui.IdleState()) def main_photobooth(send, recv): diff --git a/photobooth/PyQt5Gui.py b/photobooth/PyQt5Gui.py index 11c0764..f773018 100644 --- a/photobooth/PyQt5Gui.py +++ b/photobooth/PyQt5Gui.py @@ -1,14 +1,18 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- +import Gui + from PyQt5.QtCore import Qt, QObject, QThread, pyqtSignal from PyQt5.QtWidgets import (QApplication, QCheckBox, QComboBox, QFormLayout, QFrame, QGridLayout, QGroupBox, QHBoxLayout, QLabel, QLayout, QLineEdit, QMainWindow, QMessageBox, QPushButton, QVBoxLayout) -from PyQt5.QtGui import QPainter, QPixmap +from PyQt5.QtGui import QImage, QPainter, QPixmap -class PyQt5Gui: +class PyQt5Gui(Gui.Gui): def __init__(self, argv, config): + super().__init__() + global cfg cfg = config @@ -19,14 +23,56 @@ class PyQt5Gui: def run(self, send, recv): receiver = PyQt5Receiver(recv) - receiver.notify.connect(self._p.showMessage) + receiver.notify.connect(self.handleState) receiver.start() self._p.transport = send + self._p.handleEscape = self.showStart + + self.showStart() return self._app.exec_() + def close(self): + + self._p.close() + + + def handleState(self, state): + + if not isinstance(state, Gui.GuiState): + raise ValueError('Invalid data received') + + if isinstance(state, Gui.IdleState): + self.showIdle() + elif isinstance(state, Gui.PoseState): + self._p.setCentralWidget(PyQt5PictureMessage('Pose!')) + elif isinstance(state, Gui.PreviewState): + img = QImage(state.picture, state.picture.shape[1], state.picture.shape[0], QImage.Format_RGB888) + self._p.setCentralWidget(PyQt5PictureMessage(state.message, img)) + elif isinstance(state, Gui.PictureState): + img = QImage(state.picture, state.picture.shape[1], state.picture.shape[0], QImage.Format_RGB888) + self._p.setCentralWidget(PyQt5PictureMessage('', QPixmap.fromImage(img))) + else: + raise ValueError('Unknown state') + + + def showStart(self): + + self._p.setCentralWidget(PyQt5Start(self)) + + + def showSettings(self): + + self._p.setCentralWidget(PyQt5Settings(self)) + + + def showIdle(self): + + self._p.setCentralWidget(PyQt5PictureMessage('Hit the button!', 'homer.jpg')) + + class PyQt5Receiver(QThread): notify = pyqtSignal(object) @@ -38,21 +84,20 @@ class PyQt5Receiver(QThread): self._transport = transport - def handle(self, event): + def handle(self, state): - self.notify.emit(event) + self.notify.emit(state) def run(self): while True: try: - event = self._transport.recv() + state = self._transport.recv() except EOFError: break else: - print('Connector: ' + event) - self.handle(event) + self.handle(state) @@ -79,12 +124,25 @@ class PyQt5MainWindow(QMainWindow): self._transport = new_transport + @property + def handleEscape(self): + + return self._handle_escape + + @handleEscape.setter + def handleEscape(self, func): + + if not callable(func): + raise ValueError('Escape key handler must be callable') + + self._handle_escape = func + def initUI(self): global cfg - self.showStart() + # self.showStart() self.setWindowTitle('Photobooth') if cfg.getBool('Gui', 'fullscreen'): @@ -95,21 +153,21 @@ class PyQt5MainWindow(QMainWindow): self.show() - def showStart(self): + # def showStart(self): - content = PyQt5Start(self) - self.setCentralWidget(content) + # content = PyQt5Start(self) + # self.setCentralWidget(content) - def showSettings(self): + # def showSettings(self): - content = PyQt5Settings(self) - self.setCentralWidget(content) + # content = PyQt5Settings(self) + # self.setCentralWidget(content) - def showIdle(self): + # def showIdle(self): - self.showMessage('Hit the button!', 'homer.jpg') + # self.showMessage('Hit the button!', 'homer.jpg') def showMessage(self, message, picture=None): @@ -133,7 +191,7 @@ class PyQt5MainWindow(QMainWindow): def keyPressEvent(self, event): if event.key() == Qt.Key_Escape: - self.showStart() + self.handleEscape() elif event.key() == Qt.Key_Space: self.transport.send('triggered') @@ -142,14 +200,14 @@ class PyQt5MainWindow(QMainWindow): class PyQt5Start(QFrame): - def __init__(self, parent): + def __init__(self, gui): super().__init__() - self.initFrame(parent) + self.initFrame(gui) - def initFrame(self, parent): + def initFrame(self, gui): grid = QGridLayout() grid.setSpacing(100) @@ -157,28 +215,29 @@ class PyQt5Start(QFrame): btnStart = QPushButton('Start Photobooth') btnStart.resize(btnStart.sizeHint()) - btnStart.clicked.connect(parent.showIdle) + btnStart.clicked.connect(gui.showIdle) grid.addWidget(btnStart, 0, 0) btnSettings = QPushButton('Settings') btnSettings.resize(btnSettings.sizeHint()) - btnSettings.clicked.connect(parent.showSettings) + btnSettings.clicked.connect(gui.showSettings) grid.addWidget(btnSettings, 0, 1) btnQuit = QPushButton('Quit') btnQuit.resize(btnQuit.sizeHint()) - btnQuit.clicked.connect(parent.close) + btnQuit.clicked.connect(gui.close) grid.addWidget(btnQuit, 0, 2) class PyQt5Settings(QFrame): - def __init__(self, parent): + def __init__(self, gui): super().__init__() - self._parent = parent + self._gui = gui + self.initFrame() @@ -284,7 +343,7 @@ class PyQt5Settings(QFrame): btnCancel = QPushButton('Cancel') btnCancel.resize(btnCancel.sizeHint()) - btnCancel.clicked.connect(self._parent.showStart) + btnCancel.clicked.connect(self._gui.showStart) layout.addWidget(btnCancel) btnRestore = QPushButton('Restore defaults') @@ -314,7 +373,7 @@ class PyQt5Settings(QFrame): cfg.set('Camera', 'gphoto2_wrapper', wrapper_idx2val[self._value_widgets['Camera']['gphoto2_wrapper'].currentIndex()]) cfg.write() - self._parent.showStart() + self._gui.showStart() def restoreDefaults(self): @@ -322,7 +381,7 @@ class PyQt5Settings(QFrame): global cfg cfg.defaults() - self._parent.showSettings() + self._gui.showSettings() @@ -340,7 +399,7 @@ class PyQt5PictureMessage(QFrame): def initFrame(self): - self.setStyleSheet('background-color: black;') + self.setStyleSheet('background-color: white;') def paintEvent(self, event):