Rudimentary trigger emulation using OpenCV

This commit is contained in:
Balthasar Reuter
2018-03-23 23:27:38 +01:00
parent 0688fa5fa6
commit 3e27a46618
6 changed files with 294 additions and 39 deletions

View File

@@ -1 +1,2 @@
pip install pyqt5 pip install pyqt5
pip install opencv-python

58
photobooth/Camera.py Normal file
View File

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

View File

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

89
photobooth/Gui.py Normal file
View File

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

View File

@@ -2,18 +2,20 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from Config import Config from Config import Config
import Gui
from PyQt5Gui import PyQt5Gui from PyQt5Gui import PyQt5Gui
from CameraOpenCV import CameraOpenCV as Camera
from multiprocessing import Pipe, Process from multiprocessing import Pipe, Process
from time import sleep from time import clock, sleep
class Photobooth: class Photobooth:
def __init__(self): def __init__(self):
pass self._cap = Camera()
def run(self, send, recv): def run(self, send, recv):
@@ -24,7 +26,6 @@ class Photobooth:
except EOFError: except EOFError:
return 1 return 1
else: else:
print('Photobooth: ' + event)
self.trigger(send) self.trigger(send)
return 0 return 0
@@ -32,15 +33,29 @@ class Photobooth:
def trigger(self, send): def trigger(self, send):
send.send('Pose') send.send(Gui.PoseState())
sleep(3)
send.send('Picture')
sleep(2) 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): def main_photobooth(send, recv):

View File

@@ -1,14 +1,18 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import Gui
from PyQt5.QtCore import Qt, QObject, QThread, pyqtSignal 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.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): def __init__(self, argv, config):
super().__init__()
global cfg global cfg
cfg = config cfg = config
@@ -19,14 +23,56 @@ class PyQt5Gui:
def run(self, send, recv): def run(self, send, recv):
receiver = PyQt5Receiver(recv) receiver = PyQt5Receiver(recv)
receiver.notify.connect(self._p.showMessage) receiver.notify.connect(self.handleState)
receiver.start() receiver.start()
self._p.transport = send self._p.transport = send
self._p.handleEscape = self.showStart
self.showStart()
return self._app.exec_() 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): class PyQt5Receiver(QThread):
notify = pyqtSignal(object) notify = pyqtSignal(object)
@@ -38,21 +84,20 @@ class PyQt5Receiver(QThread):
self._transport = transport self._transport = transport
def handle(self, event): def handle(self, state):
self.notify.emit(event) self.notify.emit(state)
def run(self): def run(self):
while True: while True:
try: try:
event = self._transport.recv() state = self._transport.recv()
except EOFError: except EOFError:
break break
else: else:
print('Connector: ' + event) self.handle(state)
self.handle(event)
@@ -79,12 +124,25 @@ class PyQt5MainWindow(QMainWindow):
self._transport = new_transport 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): def initUI(self):
global cfg global cfg
self.showStart() # self.showStart()
self.setWindowTitle('Photobooth') self.setWindowTitle('Photobooth')
if cfg.getBool('Gui', 'fullscreen'): if cfg.getBool('Gui', 'fullscreen'):
@@ -95,21 +153,21 @@ class PyQt5MainWindow(QMainWindow):
self.show() self.show()
def showStart(self): # def showStart(self):
content = PyQt5Start(self) # content = PyQt5Start(self)
self.setCentralWidget(content) # self.setCentralWidget(content)
def showSettings(self): # def showSettings(self):
content = PyQt5Settings(self) # content = PyQt5Settings(self)
self.setCentralWidget(content) # 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): def showMessage(self, message, picture=None):
@@ -133,7 +191,7 @@ class PyQt5MainWindow(QMainWindow):
def keyPressEvent(self, event): def keyPressEvent(self, event):
if event.key() == Qt.Key_Escape: if event.key() == Qt.Key_Escape:
self.showStart() self.handleEscape()
elif event.key() == Qt.Key_Space: elif event.key() == Qt.Key_Space:
self.transport.send('triggered') self.transport.send('triggered')
@@ -142,14 +200,14 @@ class PyQt5MainWindow(QMainWindow):
class PyQt5Start(QFrame): class PyQt5Start(QFrame):
def __init__(self, parent): def __init__(self, gui):
super().__init__() super().__init__()
self.initFrame(parent) self.initFrame(gui)
def initFrame(self, parent): def initFrame(self, gui):
grid = QGridLayout() grid = QGridLayout()
grid.setSpacing(100) grid.setSpacing(100)
@@ -157,28 +215,29 @@ class PyQt5Start(QFrame):
btnStart = QPushButton('Start Photobooth') btnStart = QPushButton('Start Photobooth')
btnStart.resize(btnStart.sizeHint()) btnStart.resize(btnStart.sizeHint())
btnStart.clicked.connect(parent.showIdle) btnStart.clicked.connect(gui.showIdle)
grid.addWidget(btnStart, 0, 0) grid.addWidget(btnStart, 0, 0)
btnSettings = QPushButton('Settings') btnSettings = QPushButton('Settings')
btnSettings.resize(btnSettings.sizeHint()) btnSettings.resize(btnSettings.sizeHint())
btnSettings.clicked.connect(parent.showSettings) btnSettings.clicked.connect(gui.showSettings)
grid.addWidget(btnSettings, 0, 1) grid.addWidget(btnSettings, 0, 1)
btnQuit = QPushButton('Quit') btnQuit = QPushButton('Quit')
btnQuit.resize(btnQuit.sizeHint()) btnQuit.resize(btnQuit.sizeHint())
btnQuit.clicked.connect(parent.close) btnQuit.clicked.connect(gui.close)
grid.addWidget(btnQuit, 0, 2) grid.addWidget(btnQuit, 0, 2)
class PyQt5Settings(QFrame): class PyQt5Settings(QFrame):
def __init__(self, parent): def __init__(self, gui):
super().__init__() super().__init__()
self._parent = parent self._gui = gui
self.initFrame() self.initFrame()
@@ -284,7 +343,7 @@ class PyQt5Settings(QFrame):
btnCancel = QPushButton('Cancel') btnCancel = QPushButton('Cancel')
btnCancel.resize(btnCancel.sizeHint()) btnCancel.resize(btnCancel.sizeHint())
btnCancel.clicked.connect(self._parent.showStart) btnCancel.clicked.connect(self._gui.showStart)
layout.addWidget(btnCancel) layout.addWidget(btnCancel)
btnRestore = QPushButton('Restore defaults') 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.set('Camera', 'gphoto2_wrapper', wrapper_idx2val[self._value_widgets['Camera']['gphoto2_wrapper'].currentIndex()])
cfg.write() cfg.write()
self._parent.showStart() self._gui.showStart()
def restoreDefaults(self): def restoreDefaults(self):
@@ -322,7 +381,7 @@ class PyQt5Settings(QFrame):
global cfg global cfg
cfg.defaults() cfg.defaults()
self._parent.showSettings() self._gui.showSettings()
@@ -340,7 +399,7 @@ class PyQt5PictureMessage(QFrame):
def initFrame(self): def initFrame(self):
self.setStyleSheet('background-color: black;') self.setStyleSheet('background-color: white;')
def paintEvent(self, event): def paintEvent(self, event):