diff --git a/photobooth/Config.py b/photobooth/Config.py index 2e3694c..82fc896 100644 --- a/photobooth/Config.py +++ b/photobooth/Config.py @@ -10,7 +10,7 @@ class Config: self._filename = filename - self._cfg = configparser.ConfigParser() + self._cfg = configparser.ConfigParser(interpolation=None) self.defaults() self.read() diff --git a/photobooth/Photobooth.py b/photobooth/Photobooth.py index d8f024d..0904b70 100644 --- a/photobooth/Photobooth.py +++ b/photobooth/Photobooth.py @@ -4,6 +4,7 @@ from Config import Config from PictureList import PictureList +from PictureDimensions import PictureDimensions import Gui from PyQt5Gui import PyQt5Gui @@ -17,27 +18,29 @@ from multiprocessing import Pipe, Process from time import time, sleep, localtime, strftime -output_size = (1920, 1080) -min_distance = (10, 10) -# num_pictures = (2, 2) -pose_time = 2 -picture_basename = strftime('%Y-%m-%d/photobooth', localtime()) class Photobooth: def __init__(self, config): + picture_basename = strftime(config.get('Picture', 'basename'), localtime()) + self._cap = Camera() - self._cfg = config + self._pic_dims = PictureDimensions(config, self._cap.getPicture().size) + self._pic_list = PictureList(picture_basename) - if ( self._cfg.getBool('Photobooth', 'show_preview') + self._pose_time = config.getInt('Photobooth', 'pose_time') + self._countdown_time = config.getInt('Photobooth', 'countdown_time') + self._display_time = config.getInt('Photobooth', 'display_time') + + if ( config.getBool('Photobooth', 'show_preview') and self._cap.hasPreview ): - self.showCounter = self.showCounterPreview + self._show_counter = self.showCounterPreview else: - self.showCounter = self.showCounterNoPreview + self._show_counter = self.showCounterNoPreview + + self._get_next_filename = self._pic_list.getNext - self.numPictures = ( self._cfg.getInt('Photobooth', 'num_pictures_x') , - self._cfg.getInt('Photobooth', 'num_pictures_y') ) @property def getNextFilename(self): @@ -45,48 +48,34 @@ class Photobooth: return self._get_next_filename - @getNextFilename.setter - def getNextFilename(self, func): - - if not callable(func): - raise ValueError('getNextFilename must be callable') - - self._get_next_filename = func - - @property def showCounter(self): return self._show_counter - @showCounter.setter - def showCounter(self, func): + @property + def poseTime(self): - if not callable(func): - raise ValueError('showCounter must be callable') - - self._show_counter = func + return self._pose_time @property - def numPictures(self): + def countdownTime(self): - return self._num_pictures + return self._countdown_time - @numPictures.setter - def numPictures(self, num_pictures): + @property + def displayTime(self): - if len(num_pictures) != 2: - raise ValueError('num_pictures must have two entries') - - self._num_pictures = num_pictures + return self._display_time def run(self, send, recv): self._send = send + self.setCameraIdle() while True: try: @@ -118,16 +107,16 @@ class Photobooth: tic, toc = time(), 0 - while toc < pose_time: + while toc < self.countdownTime: self._send.send( Gui.PreviewState( - message = str(pose_time - int(toc)), + message = str(self.countdownTime - int(toc)), picture = ImageOps.mirror(self._cap.getPreview()) ) ) toc = time() - tic def showCounterNoPreview(self): - for i in range(pose_time): + for i in range(self.countdownTime): self._send.send( Gui.PreviewState(str(i)) ) sleep(1) @@ -141,37 +130,18 @@ class Photobooth: def assemblePictures(self, pictures): - # TODO: determine sizes only once - picture_size = pictures[0].size + output_image = Image.new('RGB', self._pic_dims.outputSize, (255, 255, 255)) - resize_factor = min( ( ( - ( output_size[i] - (self.numPictures[i] + 1) * min_distance[i] ) / - ( self.numPictures[i] * picture_size[i]) ) for i in range(2) ) ) - - output_picture_size = tuple( int(picture_size[i] * resize_factor) - for i in range(2) ) - output_picture_dist = tuple( ( output_size[i] - self.numPictures[i] * - output_picture_size[i] ) // (self.numPictures[i] + 1) - for i in range(2) ) - - output_image = Image.new('RGB', output_size, (255, 255, 255)) - - idx = 0 - for img in pictures: - pos = (idx % self.numPictures[0], idx // self.numPictures[0]) - img = img.resize(output_picture_size) - offset = tuple( (pos[i] + 1) * output_picture_dist[i] + - pos[i] * output_picture_size[i] for i in range(2) ) - output_image.paste(img, offset) - idx += 1 + for i in range(self._pic_dims.totalNumPictures): + output_image.paste(pictures[i].resize(self._pic_dims.thumbnailSize), + self._pic_dims.thumbnailOffset[i]) return output_image def capturePictures(self): - pictures = [self.captureSinglePicture() - for i in range(2) for _ in range(self.numPictures[i])] + pictures = [ self.captureSinglePicture() for _ in range(self._pic_dims.totalNumPictures) ] return self.assemblePictures(pictures) @@ -180,7 +150,7 @@ class Photobooth: self._send.send(Gui.PoseState()) self.setCameraActive() - sleep(2) + sleep(self.poseTime) img = self.capturePictures() img.save(self.getNextFilename(), 'JPEG') @@ -188,18 +158,14 @@ class Photobooth: self.setCameraIdle() - sleep(5) + sleep(self.displayTime) self._send.send(Gui.IdleState()) def main_photobooth(config, send, recv): - picture_list = PictureList(picture_basename) - photobooth = Photobooth(config) - photobooth.getNextFilename = picture_list.getNext - return photobooth.run(send, recv) diff --git a/photobooth/PictureDimensions.py b/photobooth/PictureDimensions.py new file mode 100644 index 0000000..d9cc092 --- /dev/null +++ b/photobooth/PictureDimensions.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +class PictureDimensions: + + def __init__(self, config, capture_size): + + self._num_pictures = ( config.getInt('Picture', 'num_x') , + config.getInt('Picture', 'num_y') ) + + self._capture_size = capture_size + + self._output_size = ( config.getInt('Picture', 'size_x') , + config.getInt('Picture', 'size_y') ) + + self._min_distance = ( config.getInt('Picture', 'min_dist_x') , + config.getInt('Picture', 'min_dist_y') ) + + self.computeThumbnailDimensions() + + + def computeThumbnailDimensions(self): + + resize_factor = min( ( ( + ( self.outputSize[i] - (self.numPictures[i] + 1) * self.minDistance[i] ) / + ( self.numPictures[i] * self.captureSize[i]) ) for i in range(2) ) ) + + self._thumb_size = tuple( int(self.captureSize[i] * resize_factor) + for i in range(2) ) + + output_picture_dist = tuple( ( self.outputSize[i] - self.numPictures[i] * + self.thumbnailSize[i] ) // (self.numPictures[i] + 1) + for i in range(2) ) + + self._thumb_offsets = [] + for i in range(self.totalNumPictures): + pos = (i % self.numPictures[0], i // self.numPictures[0]) + self._thumb_offsets.append( tuple( + (pos[j] + 1) * output_picture_dist[j] + + pos[j] * self.thumbnailSize[j] for j in range(2) ) ) + + + @property + def numPictures(self): + + return self._num_pictures + + + @property + def totalNumPictures(self): + + return self._num_pictures[0] * self._num_pictures[1] + + @property + def captureSize(self): + + return self._capture_size + + + @property + def outputSize(self): + + return self._output_size + + + @property + def minDistance(self): + + return self._min_distance + + + @property + def thumbnailSize(self): + + return self._thumb_size + + + @property + def thumbnailOffset(self): + + return self._thumb_offsets diff --git a/photobooth/PyQt5Gui.py b/photobooth/PyQt5Gui.py index 05a9a1e..70d1d5d 100644 --- a/photobooth/PyQt5Gui.py +++ b/photobooth/PyQt5Gui.py @@ -59,8 +59,8 @@ class PyQt5Gui(Gui.Gui): self.showIdle() elif isinstance(state, Gui.PoseState): global cfg - num_pictures = ( cfg.getInt('Photobooth', 'num_pictures_x') * - cfg.getInt('Photobooth', 'num_pictures_y') ) + num_pictures = ( cfg.getInt('Picture', 'num_x') * + cfg.getInt('Picture', 'num_y') ) self._p.setCentralWidget( PyQt5PictureMessage('Will capture {} pictures!'.format(num_pictures))) elif isinstance(state, Gui.PreviewState): @@ -259,6 +259,7 @@ class PyQt5Settings(QFrame): grid.addWidget(self.createGpioSettings(), 1, 0) grid.addWidget(self.createCameraSettings(), 0, 1) grid.addWidget(self.createPhotoboothSettings(), 1, 1) + grid.addWidget(self.createPictureSettings(), 2, 1) layout = QVBoxLayout() layout.addLayout(grid) @@ -303,8 +304,8 @@ class PyQt5Settings(QFrame): layout = QFormLayout() layout.addRow(self._value_widgets['Gpio']['enable']) layout.addRow(QLabel('Exit channel:'), self._value_widgets['Gpio']['exit_channel']) - layout.addRow(QLabel('Trigger channel'), self._value_widgets['Gpio']['trigger_channel']) - layout.addRow(QLabel('Lamp channel'), self._value_widgets['Gpio']['lamp_channel']) + layout.addRow(QLabel('Trigger channel:'), self._value_widgets['Gpio']['trigger_channel']) + layout.addRow(QLabel('Lamp channel:'), self._value_widgets['Gpio']['lamp_channel']) widget = QGroupBox('GPIO settings') widget.setLayout(layout) @@ -349,24 +350,64 @@ class PyQt5Settings(QFrame): self._value_widgets['Photobooth']['show_preview'] = QCheckBox('Show preview while countdown (restart required)') if cfg.getBool('Photobooth', 'show_preview'): self._value_widgets['Photobooth']['show_preview'].toggle() - self._value_widgets['Photobooth']['num_pictures_x'] = QLineEdit(cfg.get('Photobooth', 'num_pictures_x')) - self._value_widgets['Photobooth']['num_pictures_y'] = QLineEdit(cfg.get('Photobooth', 'num_pictures_y')) + self._value_widgets['Photobooth']['pose_time'] = QLineEdit(cfg.get('Photobooth', 'pose_time')) + self._value_widgets['Photobooth']['countdown_time'] = QLineEdit(cfg.get('Photobooth', 'countdown_time')) + self._value_widgets['Photobooth']['display_time'] = QLineEdit(cfg.get('Photobooth', 'display_time')) layout = QFormLayout() layout.addRow(self._value_widgets['Photobooth']['show_preview']) - - sublayout = QHBoxLayout() - sublayout.addWidget(QLabel('Number of pictures')) - sublayout.addWidget(self._value_widgets['Photobooth']['num_pictures_x']) - sublayout.addWidget(QLabel('x')) - sublayout.addWidget(self._value_widgets['Photobooth']['num_pictures_y']) - layout.addRow(sublayout) + layout.addRow(QLabel('Pose time [s]:'), self._value_widgets['Photobooth']['pose_time']) + layout.addRow(QLabel('Countdown time [s]:'), self._value_widgets['Photobooth']['countdown_time']) + layout.addRow(QLabel('Display time [s]:'), self._value_widgets['Photobooth']['display_time']) widget = QGroupBox('Photobooth settings') widget.setLayout(layout) return widget + def createPictureSettings(self): + + global cfg + + self._value_widgets['Picture'] = {} + self._value_widgets['Picture']['num_x'] = QLineEdit(cfg.get('Picture', 'num_x')) + self._value_widgets['Picture']['num_y'] = QLineEdit(cfg.get('Picture', 'num_y')) + self._value_widgets['Picture']['size_x'] = QLineEdit(cfg.get('Picture', 'size_x')) + self._value_widgets['Picture']['size_y'] = QLineEdit(cfg.get('Picture', 'size_y')) + self._value_widgets['Picture']['min_dist_x'] = QLineEdit(cfg.get('Picture', 'min_dist_x')) + self._value_widgets['Picture']['min_dist_y'] = QLineEdit(cfg.get('Picture', 'min_dist_y')) + self._value_widgets['Picture']['basename'] = QLineEdit(cfg.get('Picture', 'basename')) + + layout = QFormLayout() + + sublayout_num = QHBoxLayout() + sublayout_num.addWidget(QLabel('Number of shots per picture:')) + sublayout_num.addWidget(self._value_widgets['Picture']['num_x']) + sublayout_num.addWidget(QLabel('x')) + sublayout_num.addWidget(self._value_widgets['Picture']['num_y']) + layout.addRow(sublayout_num) + + sublayout_size = QHBoxLayout() + sublayout_size.addWidget(QLabel('Size of assembled picture:')) + sublayout_size.addWidget(self._value_widgets['Picture']['size_x']) + sublayout_size.addWidget(QLabel('x')) + sublayout_size.addWidget(self._value_widgets['Picture']['size_y']) + layout.addRow(sublayout_size) + + sublayout_dist = QHBoxLayout() + sublayout_dist.addWidget(QLabel('Min. distance between shots in picture:')) + sublayout_dist.addWidget(self._value_widgets['Picture']['min_dist_x']) + sublayout_dist.addWidget(QLabel('x')) + sublayout_dist.addWidget(self._value_widgets['Picture']['min_dist_y']) + layout.addRow(sublayout_dist) + + layout.addRow(QLabel('Basename of output files:'), self._value_widgets['Picture']['basename']) + + widget = QGroupBox('Picture settings') + widget.setLayout(layout) + return widget + + def createButtons(self): layout = QHBoxLayout() @@ -406,8 +447,17 @@ class PyQt5Settings(QFrame): cfg.set('Gpio', 'lamp_channel', self._value_widgets['Gpio']['lamp_channel'].text()) cfg.set('Photobooth', 'show_preview', str(self._value_widgets['Photobooth']['show_preview'].isChecked())) - cfg.set('Photobooth', 'num_pictures_x', self._value_widgets['Photobooth']['num_pictures_x'].text()) - cfg.set('Photobooth', 'num_pictures_y', self._value_widgets['Photobooth']['num_pictures_y'].text()) + cfg.set('Photobooth', 'pose_time', str(self._value_widgets['Photobooth']['pose_time'].text())) + cfg.set('Photobooth', 'countdown_time', str(self._value_widgets['Photobooth']['countdown_time'].text())) + cfg.set('Photobooth', 'display_time', str(self._value_widgets['Photobooth']['display_time'].text())) + + cfg.set('Picture', 'num_x', self._value_widgets['Picture']['num_x'].text()) + cfg.set('Picture', 'num_y', self._value_widgets['Picture']['num_y'].text()) + cfg.set('Picture', 'size_x', self._value_widgets['Picture']['size_x'].text()) + cfg.set('Picture', 'size_y', self._value_widgets['Picture']['size_y'].text()) + cfg.set('Picture', 'min_dist_x', self._value_widgets['Picture']['min_dist_x'].text()) + cfg.set('Picture', 'min_dist_y', self._value_widgets['Picture']['min_dist_y'].text()) + cfg.set('Picture', 'basename', self._value_widgets['Picture']['basename'].text()) wrapper_idx2val = [ 'commandline', 'piggyphoto', 'gphoto2-cffi' ] cfg.set('Camera', 'gphoto2_wrapper', wrapper_idx2val[self._value_widgets['Camera']['gphoto2_wrapper'].currentIndex()]) diff --git a/photobooth/defaults.cfg b/photobooth/defaults.cfg index b7b750b..f665272 100644 --- a/photobooth/defaults.cfg +++ b/photobooth/defaults.cfg @@ -23,7 +23,25 @@ lamp_channel = 4 [Photobooth] # Show preview while posing time (True/False) show_preview = True +# Pose time in seconds (shown before countdown) +pose_time = 3 +# Countdown length in seconds (shown before every shot) +countdown_time = 2 +# Display time of assembled picture (shown after last shot) +display_time = 5 + +[Picture] +# Basename of output pictures +basename = %Y-%m-%d/photobooth # Number of pictures in horizontal direction -num_pictures_x = 2 +num_x = 2 # Number of pictures in vertical direction -num_pictures_y = 2 +num_y = 2 +# Size of output picture in horizontal direction +size_x = 1920 +# Size of output picture in vertical direction +size_y = 1080 +# Minimum distance between thumbnails in horizontal direction +min_dist_x = 20 +# Minimum distance between thumbnails in vertical direction +min_dist_y = 20