Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
19e119a
hacky recording of image series
patkan Aug 9, 2019
341cf0a
remove intermediate prototype states
patkan Aug 13, 2019
c79c4a2
record pictures with time inbetween
patkan Aug 14, 2019
862483e
use variables for capturemode
patkan Aug 20, 2019
8fbf5da
remove unused stub
patkan Aug 20, 2019
4e46d4c
fix typo
patkan Aug 20, 2019
3692d06
use variables for capturemode
patkan Aug 20, 2019
bfde5b7
rename trigger action
patkan Aug 20, 2019
c5f6036
adapt stylesheet slightly for prototyping
patkan Aug 20, 2019
8e96585
move differentiation for capturemode
patkan Aug 20, 2019
04820ef
store gif if available
patkan Aug 20, 2019
88782df
adapt assembleGIF to current usecase
patkan Aug 20, 2019
1c02818
make number of frames and duration configurable
patkan Aug 20, 2019
6d09619
try to show GIF with QMovie
patkan Aug 20, 2019
91eb1c0
show GIF with QMovie
patkan Sep 3, 2019
0e4f3e1
make wait time between captures configurable
patkan Sep 3, 2019
1d082e9
use true division
patkan Sep 5, 2019
31cc10d
improve debug message
patkan Sep 5, 2019
e8d8697
add model for eos450d
patkan Sep 5, 2019
6c506fc
show boomerang-specific gui
patkan Sep 9, 2019
f2b5cfe
instead of waiting, skip all except nth capture frame
patkan Sep 9, 2019
daa7113
better handle filenames for gifs
patkan Sep 9, 2019
cd0a39a
remove dead code
patkan Sep 9, 2019
b7196b1
keep gif stills if configured
patkan Sep 9, 2019
73270d7
cleanup
patkan Sep 9, 2019
7070a3f
call save with optimize
patkan Sep 9, 2019
73486da
add different postprocess lists for gif and jpeg
patkan Sep 10, 2019
4fe1e3d
add settings for GIF
patkan Sep 10, 2019
f81e6a2
adapt configs for camera eos 450d
patkan Sep 10, 2019
860e846
improve debug output
patkan Sep 10, 2019
d131804
clean up and adapt dark large layout
patkan Sep 10, 2019
9e6c560
allow GIF mode to be disabled, standard is off
patkan Sep 10, 2019
fa02dfb
remove unused import
patkan Sep 10, 2019
3b53231
update README
patkan Sep 10, 2019
2bb51cc
fix wrong task list in do
patkan Sep 12, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ This is a Python application to build your own photobooth.
* Support for external buttons and lamps via GPIO interface
* Rudimentary WebDAV upload functionality (saves pictures to WebDAV storage) and mailer feature (mails pictures to a fixed email address)
* Theming support using [Qt stylesheets](https://doc.qt.io/qt-5/stylesheet-syntax.html)
* Ability to take a short Boomerang-GIF (looping forwards and backwards) with the Live preview

### Screenshots
Screenshots produced using `CameraDummy` that produces unicolor images.
Expand All @@ -38,7 +39,7 @@ Screenshots produced using `CameraDummy` that produces unicolor images.
* Based on [Python 3](https://www.python.org/), [Pillow](https://pillow.readthedocs.io), and [Qt5](https://www.qt.io/developers/)

### History
I started this project for my own wedding in 2015.
I started this project for my own wedding in 2015.
See [Version 0.1](https://github.com/reuterbal/photobooth/tree/v0.1) for the original version.
Github user [hackerb9](https://github.com/hackerb9/photobooth) forked this version and added a print functionality.
However, I was not happy with the original software design and the limited options provided by the previously used [pygame](https://www.pygame.org) GUI library and thus abandoned the original version.
Expand Down
92 changes: 82 additions & 10 deletions photobooth/StateMachine.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,18 @@
import logging


CAPMODE_STATIC = 'static'
CAPMODE_BOOMERANG = 'boomerang'


class Context:

def __init__(self, communicator, omit_welcome=False):

super().__init__()
self._comm = communicator
self.is_running = False
self.capturemode = CAPMODE_STATIC
if omit_welcome:
self.state = StartupState()
else:
Expand All @@ -45,6 +50,21 @@ def is_running(self, running):

self._is_running = running

@property
def capturemode(self):

return self._capturemode

@capturemode.setter
def capturemode(self, mode):

if (mode != CAPMODE_BOOMERANG) and (mode != CAPMODE_STATIC):
raise TypeError('capturemode must be boomerang or static')

logging.debug('Context: Set capture mode to "{}"'.format(mode))

self._capturemode = mode

@property
def state(self):

Expand Down Expand Up @@ -177,16 +197,22 @@ class GpioEvent(Event):

class CameraEvent(Event):

def __init__(self, name, picture=None):
def __init__(self, name, picture=None, gif=None):

super().__init__(name)
self._picture = picture
self._gif = gif

@property
def picture(self):

return self._picture

@property
def gif(self):

return self._gif


class WorkerEvent(Event):

Expand Down Expand Up @@ -354,16 +380,27 @@ def handleEvent(self, event, context):

if ((isinstance(event, GuiEvent) or isinstance(event, GpioEvent)) and
event.name == 'trigger'):
context.capturemode = CAPMODE_STATIC
context.state = GreeterState()
elif ((isinstance(event, GuiEvent) or isinstance(event, GpioEvent)) and
event.name == 'triggerVideo'):
context.capturemode = CAPMODE_BOOMERANG
context.state = GreeterState(gif=True)
else:
raise TypeError('Unknown Event type "{}"'.format(event))


class GreeterState(State):

def __init__(self):
def __init__(self, gif=None):

super().__init__()
self._gif = gif

@property
def gif(self):

return self._gif

def handleEvent(self, event, context):

Expand Down Expand Up @@ -392,73 +429,108 @@ def handleEvent(self, event, context):
if isinstance(event, GuiEvent) and event.name == 'countdown':
pass
elif isinstance(event, GuiEvent) and event.name == 'capture':
context.state = CaptureState(self.num_picture)
context.state = CaptureState(self.num_picture, context.capturemode)
else:
raise TypeError('Unknown Event type "{}"'.format(event))


class CaptureState(State):

def __init__(self, num_picture):
def __init__(self, num_picture, capturemode):

super().__init__()

self._num_picture = num_picture
self._capturemode = capturemode

@property
def num_picture(self):

return self._num_picture

@property
def capturemode(self):

return self._capturemode

@property
def gif(self):

gif = False
if self.capturemode == CAPMODE_BOOMERANG:
gif = True
return gif

def handleEvent(self, event, context):

if isinstance(event, CameraEvent) and event.name == 'countdown':
context.state = CountdownState(self.num_picture + 1)
elif isinstance(event, CameraEvent) and event.name == 'assemble':
context.state = AssembleState()
context.state = AssembleState(self.capturemode)
else:
raise TypeError('Unknown Event type "{}"'.format(event))


class AssembleState(State):

def __init__(self):
def __init__(self, capturemode):

super().__init__()
self._capturemode = capturemode

@property
def capturemode(self):

return self._capturemode

def handleEvent(self, event, context):

if isinstance(event, CameraEvent) and event.name == 'review':
context.state = ReviewState(event.picture)
if self.capturemode == CAPMODE_BOOMERANG:
context.state = ReviewState(event.picture, event.gif)
else:
context.state = ReviewState(event.picture)
else:
raise TypeError('Unknown Event type "{}"'.format(event))


class ReviewState(State):

def __init__(self, picture):
def __init__(self, picture, gif=None):

super().__init__()
self._picture = picture
self._gif = gif

@property
def picture(self):

return self._picture

@property
def gif(self):

return self._gif

def handleEvent(self, event, context):

if isinstance(event, GuiEvent) and event.name == 'postprocess':
context.state = PostprocessState()
context.state = PostprocessState(self.gif)
else:
raise TypeError('Unknown Event type "{}"'.format(event))


class PostprocessState(State):

def __init__(self):
def __init__(self, gif=None):

super().__init__()
self._gif = gif

@property
def gif(self):

return self._gif

def handleEvent(self, event, context):

Expand Down
63 changes: 61 additions & 2 deletions photobooth/camera/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ def __init__(self, config, comm, CameraModule):
self._is_preview = self._cfg.getBool('Photobooth', 'show_preview')
self._is_keep_pictures = self._cfg.getBool('Storage', 'keep_pictures')

self._gif_num_frames = self._cfg.getInt('GIF', 'num_frames')
self._gif_num_img_to_take = ((self._gif_num_frames - 2) // 2) + 2
self._gif_frame_duration = self._cfg.getInt('GIF', 'frame_duration')
self._gif_use_nth_capture = self._cfg.getInt('GIF', 'use_nth_capture')

rot_vals = {0: None, 90: Image.ROTATE_90, 180: Image.ROTATE_180,
270: Image.ROTATE_270}
self._rotation = rot_vals[self._cfg.getInt('Camera', 'rotation')]
Expand Down Expand Up @@ -104,9 +109,19 @@ def handleState(self, state):
elif isinstance(state, StateMachine.CountdownState):
self.capturePreview()
elif isinstance(state, StateMachine.CaptureState):
self.capturePicture(state)
if state.capturemode == StateMachine.CAPMODE_STATIC:
self.capturePicture(state)
elif state.capturemode == StateMachine.CAPMODE_BOOMERANG:
self.captureVideo(state)
else:
raise TypeError('unknown capturemode in camera')
elif isinstance(state, StateMachine.AssembleState):
self.assemblePicture()
if state.capturemode == StateMachine.CAPMODE_STATIC:
self.assemblePicture()
elif state.capturemode == StateMachine.CAPMODE_BOOMERANG:
self.assembleGIF()
else:
raise TypeError('unknown capturemode in camera')
elif isinstance(state, StateMachine.TeardownState):
self.teardown(state)

Expand Down Expand Up @@ -160,6 +175,30 @@ def capturePicture(self, state):
self._comm.send(Workers.MASTER,
StateMachine.CameraEvent('assemble'))

def captureVideo(self, state):

logging.debug('entering boomerang capture')
number_pictures = 0

counter = 0
while number_pictures < self._gif_num_img_to_take:
picture = self._cap.getPreview()
if counter % self._gif_use_nth_capture == 0:
# skip images inbetween
number_pictures += 1
if self._rotation is not None:
picture = picture.transpose(self._rotation)
byte_data = BytesIO()
picture.save(byte_data, format='jpeg')
self._pictures.append(byte_data)
if self._is_keep_pictures:
self._comm.send(Workers.WORKER,
StateMachine.CameraEvent('capture', byte_data))
counter += 1

self._comm.send(Workers.MASTER,
StateMachine.CameraEvent('assemble'))

def assemblePicture(self):

self.setIdle()
Expand All @@ -175,3 +214,23 @@ def assemblePicture(self):
self._comm.send(Workers.MASTER,
StateMachine.CameraEvent('review', byte_data))
self._pictures = []

def assembleGIF(self):

self.setIdle()

picture = []
for i in range(self._gif_num_img_to_take):
logging.debug("appending frame {}".format(i))
picture.append(Image.open(self._pictures[i]))
for i in range((self._gif_num_frames - self._gif_num_img_to_take), 0, -1):
logging.debug("appending frame {}".format(i))
picture.append(Image.open(self._pictures[i]))

byte_data_gif = BytesIO()
picture[0].save(byte_data_gif, format='GIF', append_images=picture[1:],
save_all=True, optimize=True, duration=self._gif_frame_duration,
loop=0)
self._comm.send(Workers.MASTER,
StateMachine.CameraEvent('review', byte_data_gif, True))
self._pictures = []
19 changes: 19 additions & 0 deletions photobooth/camera/models/canoneos450d.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[Startup]
imageformat = Large Fine JPEG
imageformatsd = Large Fine JPEG
autopoweroff = 0
whitebalance = Shadow
picturestyle = Neutral

[Shutdown]
imageformat = RAW + Large Fine JPEG
imageformatsd = RAW + Large Fine JPEG
autopoweroff = 30
whitebalance = Auto
picturestyle = Standard

[Idle]
output = Off

[Active]
output = PC
12 changes: 11 additions & 1 deletion photobooth/defaults.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ hide_cursor = False
style = default

[Camera]
# Camera module to use (python-gphoto2, gphoto2-cffi, gphoto2-commandline,
# Camera module to use (python-gphoto2, gphoto2-cffi, gphoto2-commandline,
# opencv, picamera, dummy)
module = python-gphoto2
# Specify rotation of camera in degree (possible values: 0, 90, 180, 270)
Expand Down Expand Up @@ -85,6 +85,16 @@ skip =
# Specify background image (filename, optional)
background =

[GIF]
# Enable/disable GIF mode
enable = False
# Number of frames
num_frames = 12
# duration of one frame (in milliseconds)
frame_duration = 100
# use every n-th frame from the captured stream
use_nth_capture = 5

[Storage]
# Basedir of output pictures
basedir = %Y-%m-%d
Expand Down
Loading