Skip to content
This repository was archived by the owner on Aug 27, 2022. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,11 @@ vendor/simple-translator/
*.patch
.skip_welcome
.vscode
venv/

# ignore some IDE and OS files
*.iml
*.ipr
*.iws
.idea/
**/.DS_Store
301 changes: 301 additions & 0 deletions api/cameracontrol.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,301 @@
#!/usr/bin/env python

import signal
import sys
import time
import psutil
import zmq
import argparse
from argparse import Namespace
from subprocess import Popen, PIPE
import gphoto2 as gp


class CameraControl:
def __init__(self, args):
self.running = True
self.args = args
self.showVideo = True
self.chroma = {}
self.camera = None
self.socket = None
self.ffmpeg = None

signal.signal(signal.SIGINT, self.exit_gracefully)
signal.signal(signal.SIGTERM, self.exit_gracefully)

self.connect_to_camera()

if args.imgpath is not None:
try:
self.capture_image(args.imgpath)
if args.chroma_sensitivity is not None and args.chroma_sensitivity > 0:
self.handle_chroma_params(args)
self.chroma_key_image(args.imgpath)
sys.exit(0)
except gp.GPhoto2Error as e:
print('An error occured: %s' % e)
sys.exit(1)
else:
self.pipe_video_to_ffmpeg_and_wait_for_commands()

def connect_to_camera(self):
try:
self.camera = gp.Camera()
self.camera.init()
print('Connected to camera')
if self.args.config is not None:
print('Setting config %s' % self.args.config)
for c in self.args.config:
cs = c.split("=")
if len(cs) == 2:
self.set_config(cs[0], cs[1])
else:
print('Invalid config value %s' % c)
except gp.GPhoto2Error:
pass

def capture_image(self, path):
print('Capturing image')
self.print_config('capturetarget')
# refresh images on camera
self.camera.wait_for_event(1000)
file_path = self.camera.capture(gp.GP_CAPTURE_IMAGE)
print('Camera file path: {0}/{1}'.format(file_path.folder, file_path.name))
file_jpg = str(file_path.name).replace('.CR2', '.JPG')
print('Copying image to', path)
camera_file = self.camera.file_get(file_path.folder, file_jpg, gp.GP_FILE_TYPE_NORMAL)
camera_file.save(path)

def print_config(self, name):
config = self.camera.get_config()
setting = config.get_child_by_name(name)
print('%s=%s' % (name, setting.get_value()))

def set_config(self, name, value):
try:
config = self.camera.get_config()
setting = config.get_child_by_name(name)
setting_type = setting.get_type()
if setting_type == gp.GP_WIDGET_RADIO \
or setting_type == gp.GP_WIDGET_MENU \
or setting_type == gp.GP_WIDGET_TEXT:
try:
int_value = int(value)
count = setting.count_choices()
if int_value < 0 or int_value >= count:
print('Parameter out of range')
self.exit_gracefully()
choice = setting.get_choice(int_value)
setting.set_value(choice)
except ValueError:
setting.set_value(value)
elif setting_type == gp.GP_WIDGET_TOGGLE:
setting.set_value(int(value))
elif setting_type == gp.GP_WIDGET_RANGE:
setting.set_value(float(value))
else:
# unhandled types (most don't make any sense to handle)
# GP_WIDGET_SECTION, GP_WIDGET_WINDOW, GP_WIDGET_BUTTON, GP_WIDGET_DATE
print('Unhandled setting type %s for %s=%s' % (setting_type, name, value))
self.exit_gracefully()
self.camera.set_config(config)
print('Config set %s=%s' % (name, value))
except gp.GPhoto2Error or ValueError:
print('Config error for %s=%s' % (name, value))
self.exit_gracefully()

def disable_video(self):
self.showVideo = False
self.set_config('viewfinder', 0)
print('Video disabled')

def handle_message(self, msg):
args = Namespace(**msg)
if args.exit:
self.socket.send_string('Exiting service!')
self.exit_gracefully()
self.handle_chroma_params(args)
if args.device != self.args.device:
self.args.device = args.device
self.ffmpeg_open()
print('Video output device changed')
if args.config is not None and args.config != self.args.config:
self.args.config = args.config
self.connect_to_camera()
print('Applied updated config')
if args.imgpath is not None:
try:
self.capture_image(args.imgpath)
print('chroma')
if args.chroma_sensitivity is not None and args.chroma_sensitivity > 0:
print('do chroma')
self.chroma_key_image(args.imgpath)
self.socket.send_string('Image captured')
if self.args.bsm:
self.disable_video()
except gp.GPhoto2Error as e:
print('An error occured: %s' % e)
self.socket.send_string('failure')
else:
self.args.bsm = args.bsm
try:
if not self.showVideo:
self.showVideo = True
self.connect_to_camera()
self.socket.send_string('Starting Video')
else:
self.socket.send_string('Video already running')
except gp.GPhoto2Error:
self.socket.send_string('failure')

def ffmpeg_open(self):
input_chroma = []
filters = []
if self.chroma.get('active', False):
filters, input_chroma = self.get_chroma_ffmpeg_params()
input_gphoto = ['-i', '-', '-vcodec', 'rawvideo', '-pix_fmt', 'yuv420p']
ffmpeg_output = ['-preset', 'ultrafast', '-f', 'v4l2', self.args.device]
self.ffmpeg = Popen(['ffmpeg', *input_chroma, *input_gphoto, *filters, *ffmpeg_output], stdin=PIPE)

def handle_chroma_params(self, args):
chroma_color = args.chroma_color or self.chroma.get('color', '0xFFFFFF')
chroma_image = args.chroma_image or self.chroma.get('image')
chroma_sensitivity = float(args.chroma_sensitivity or self.chroma.get('sensitivity', 0.0))
if chroma_sensitivity < 0.0 or chroma_sensitivity > 1.0:
chroma_sensitivity = 0.0
chroma_blend = float(args.chroma_blend or self.chroma.get('blend', 0.0))
if chroma_blend < 0.0:
chroma_blend = 0.0
elif chroma_blend > 1.0:
chroma_blend = 1.0
chroma_active = chroma_sensitivity != 0.0 and chroma_image is not None
print('chromakeying active: %s' % chroma_active)
self.chroma = {
'active': chroma_active,
'image': chroma_image,
'color': chroma_color,
'sensitivity': str(chroma_sensitivity),
'blend': str(chroma_blend)
}

def get_chroma_ffmpeg_params(self):
input_chroma = ['-i', self.chroma['image']]
filters = ['-filter_complex', '[0:v][1:v]scale2ref[i][v];' +
'[v]colorkey=%s:%s:%s:[ck];[i][ck]overlay' %
(self.chroma['color'], self.chroma['sensitivity'], self.chroma['blend'])]
return filters, input_chroma

def chroma_key_image(self, path):
input_chroma = []
filters = []
if self.chroma.get('active', False):
filters, input_chroma = self.get_chroma_ffmpeg_params()
input_gphoto = ['-i', path]
tmp_path = "%s-chroma.jpg" % path
ffmpeg_output = [tmp_path]
Popen(['ffmpeg', *input_chroma, *input_gphoto, *filters, *ffmpeg_output]).wait(5)
Popen(['mv', tmp_path, path, '-f']).wait(1)

def pipe_video_to_ffmpeg_and_wait_for_commands(self):
context = zmq.Context()
self.socket = context.socket(zmq.REP)
self.socket.bind('tcp://*:5555')
self.handle_chroma_params(self.args)
self.ffmpeg_open()
try:
while True:
try:
message = self.socket.recv_json(flags=zmq.NOBLOCK)
print('Received: %s' % message)
self.handle_message(message)
except zmq.Again:
pass
try:
if self.showVideo:
capture = self.camera.capture_preview()
img_bytes = memoryview(capture.get_data_and_size()).tobytes()
self.ffmpeg.stdin.write(img_bytes)
else:
time.sleep(0.1)
except gp.GPhoto2Error:
time.sleep(1)
print('Not connected to camera. Trying to reconnect...')
self.connect_to_camera()
except KeyboardInterrupt:
self.exit_gracefully()

def exit_gracefully(self, *_):
if self.running:
self.running = False
print('Exiting...')
if self.camera:
self.disable_video()
self.camera.exit()
print('Closed camera connection')
sys.exit(0)


class MessageSender:
def __init__(self, message):
try:
context = zmq.Context()
socket = context.socket(zmq.REQ)
socket.setsockopt(zmq.RCVTIMEO, 10000)
socket.connect('tcp://localhost:5555')
print('Sending message: %s' % message)
socket.send_json(message)
response = socket.recv_string()
print(response)
if response == 'failure':
sys.exit(1)
except zmq.Again:
print('Message receival not confirmed')
sys.exit(1)
except KeyboardInterrupt:
print('Interrupted!')


def is_already_running():
instances = 0
for p in psutil.process_iter(['name', 'cmdline']):
if p.name() == 'python3':
if p.cmdline()[1].endswith('cameracontrol.py'):
instances += 1
return instances > 1


def main():
parser = argparse.ArgumentParser(description='Simple Camera Control script using libgphoto2 through \
python-gphoto2.', epilog='If you don\'t want images to be stored on the camera make sure that capturetarget \
is set to internal ram (might be device dependent but it\'s 0 for Canon cameras. Additionally you should \
configure your camera to capture only jpeg images.', allow_abbrev=False)
parser.add_argument('-d', '--device', nargs='?', default='/dev/video0',
help='virtual device the ffmpeg stream is sent to')
parser.add_argument('-s', '--set-config', action='append', default=None, dest='config',
help='CONFIGENTRY=CONFIGVALUE analog to gphoto2 cli. Not tested for all config entries!')
parser.add_argument('-c', '--capture-image-and-download', default=None, type=str, dest='imgpath',
help='capture an image and download it to the computer. If it stays stored on the camera as \
well depends on the camera config')
parser.add_argument('-b', '--bsm', action='store_true', help='start preview, but quit preview after taking an \
image and wait for message to start preview again')
parser.add_argument('--chromaImage', type=str, help='chroma key background (full path)', dest='chroma_image')
parser.add_argument('--chromaColor', type=str,
help='chroma key color (color name or format like "0xFFFFFF" for white)', dest='chroma_color')
parser.add_argument('--chromaSensitivity', type=float,
help='chroma key sensitivity (value from 0.01 to 1.0 or 0.0 to disable). \
If this is set to a value distinct from 0.0 on capture immage command chroma keying using \
ffmpeg is applied on the image and only this modified image is stored on the pc.',
dest='chroma_sensitivity')
parser.add_argument('--chromaBlend', type=float, help='chroma key blend (0.0 to 1.0)', dest='chroma_blend')
parser.add_argument('--exit', action='store_true', help='exit the service')

args = parser.parse_args()
if not is_already_running():
CameraControl(args)
else:
MessageSender(vars(args))


if __name__ == '__main__':
main()
6 changes: 4 additions & 2 deletions api/takePic.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,10 @@ function takePicture($filename) {
imagedestroy($im);
}
} else {
$dir = dirname($filename);
chdir($dir); //gphoto must be executed in a dir with write permission
//gphoto must be executed in a dir with write permission for other commands we stay in the api dir
if (substr($config['take_picture']['cmd'], 0, strlen('gphoto')) === 'gphoto') {
chdir(dirname($filename));
}
$cmd = sprintf($config['take_picture']['cmd'], $filename);
$cmd .= ' 2>&1'; //Redirect stderr to stdout, otherwise error messages get lost.

Expand Down
41 changes: 39 additions & 2 deletions faq/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,7 @@ Make sure to have a stream available you can use (e.g. from your Webcam, Smartph
A preview can also be done using the video mode of your DSLR (Linux only), but only works if you access Photobooth via [http://localhost](http://localhost) or [http://127.0.0.1](http://localhost):

- Liveview **must** be supported for your camera model, [check here](http://gphoto.org/proj/libgphoto2/support.php)
- install all dependencies `sudo apt install ffmpeg v4l2loopback-dkms -y`
- install all dependencies `sudo apt install ffmpeg v4l2loopback-dkms v4l-utils -y`
- create a virtual webcam `sudo modprobe v4l2loopback exclusive_caps=1 card_label="GPhoto2 Webcam"`
- `/dev/video0` is used by default, you can use `v4l2-ctl --list-devices` to check which `/dev/*` is the correct one:
If it doesn't match the default setup you need to adjust the `Command to generate a live preview` inside the admin panel!
Expand Down Expand Up @@ -411,6 +411,43 @@ Yes you can. There's different ways depending on your needs and personal setup:

<hr>

### How to get better performance using gphoto2 as preview?
By now the DSLR handling of Photobooth on Linux was done exclusively using `gphoto2 CLI` (command line interface). When taking pictures while using preview video from the same camera one command has to be stopped and another one is run after that.
The computer terminates the connection to the camera just to reconnect immediately. Because of that there was an ugly video gap and the noises of the camera could be irritating as stopping the video sounded very similar to taking a picture. But most cameras can shoot quickly from live-view...
The underlying libery of `gphoto2 CLI` is `libgphoto` and it can be accessed using several programming languages. Because of this we can have a python script that handles both preview and taking pictures without terminating the connection to the camera in between.

To try using `gphoto-python` first execute `install-gphoto-python.sh` from the Photobooth installation subdirectory `gphoto`.
```
bash gphoto/install-gphoto-python.sh
```
After that just change your commands to use the python script. For Live preview use:
```
python3 cameracontrol.py
```
And for the take picture command:
```
python3 cameracontrol.py --capture-image-and-download %s
```
There's no need for a command to end the live preview. So just empty that field.

As you possibly noticed the params of the script are designed to be similar to the ones of `gphoto2 CLI` but with some shortcuts like `-c` for `--capture-image-and-download`. If you want to know more check out the help of the script by running:
```
python3 cameracontrol.py --help
```
If you want to keep your images on the camera you need to use the same `capturetarget` config as when you were using `gphoto CLI` (see "How to keep pictures on my Camera using gphoto2?"). Set the config on the preview command like this:
```
python3 cameracontrol.py --set-config capturetarget=1
```
If you don't want to use the DSLR view as background video enable the respective setting of Photobooth and add `--bsm` to the preview command. The preview video is activated when the countdown for a photo starts and after taking a picture the video is deactivated while waiting for the next photo.

If you get errors from Photobooth and want to get more information try to run the preview command manually. The script is in Photobooth's `api` folder. To do so end all running services that potentially try to access the camera with `killall gphoto2` and `killall python3` (if you added any other python scripts manually you might have to be a bit more selective than this command).

Finally if you just run `venv/bin/python3 cameracontrol.py --capture-image-and-download %s` as take picture command without having a preview started it only takes a picture without starting any kind of preview and ends the script immediately after the picture. In theory `cameracontrol.py` might be able to completely replace `gphoto2 CLI` for all DSLR connection handling in the future.

But by now this was not tested with distinct setups and different cameras... so feel free to give feedback!

<hr>

### I've trouble setting up E-Mail config. How do I solve my problem?
If connection fails some help can be found [here](https://github.com/PHPMailer/PHPMailer/wiki/Troubleshooting), especially gmail needs some special config.

Expand Down Expand Up @@ -472,7 +509,7 @@ Open [http://localhost/phpinfo.php](http://localhost/phpinfo.php) in your browse
Take a look for "Loaded Configuration File", you need sudo rights to edit the file.
Page will look like this:
<details><summary>CLICK ME</summary>
<img src="../resources/img/faq/php-ini.png">
<img src="../resources/img/faq/php-ini.png" alt="php.ini Screenshot">
</details>

<hr>
Expand Down
9 changes: 9 additions & 0 deletions gphoto/ffmpeg-webcam.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[Unit]
Description=Setup a ffmpeg webcam

[Service]
Type=oneshot
ExecStart=/usr/ffmpeg-webcam.sh

[Install]
WantedBy=multi-user.target
Loading