Rudimentary trigger emulation using OpenCV
This commit is contained in:
@@ -1 +1,2 @@
|
|||||||
pip install pyqt5
|
pip install pyqt5
|
||||||
|
pip install opencv-python
|
||||||
|
|||||||
58
photobooth/Camera.py
Normal file
58
photobooth/Camera.py
Normal 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()
|
||||||
33
photobooth/CameraOpenCV.py
Normal file
33
photobooth/CameraOpenCV.py
Normal 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
89
photobooth/Gui.py
Normal 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)
|
||||||
@@ -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):
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
Reference in New Issue
Block a user