platypush/platypush/plugins/camera/__init__.py

319 lines
12 KiB
Python

import os
import re
import shutil
import threading
import time
import cv2
from datetime import datetime
from platypush.config import Config
from platypush.plugins import Plugin, action
class CameraPlugin(Plugin):
"""
Plugin to control generic cameras over OpenCV.
Requires:
* **opencv** (``pip install opencv-python``)
"""
_default_frames_dir = os.path.join(Config.get('workdir'), 'camera',
'frames')
_default_warmup_frames = 5
_default_sleep_between_frames = 0
_default_color_transform = 'COLOR_BGR2BGRA'
_max_stored_frames = 100
def __init__(self, device_id=0, frames_dir=_default_frames_dir,
warmup_frames=_default_warmup_frames,
sleep_between_frames=_default_sleep_between_frames,
max_stored_frames=_max_stored_frames,
color_transform=_default_color_transform, *args, **kwargs):
"""
:param device_id: Index of the default video device to be used for
capturing (default: 0)
:type device_id: int
:param frames_dir: Directory where the camera frames will be stored
(default: ``~/.local/share/platypush/camera/frames``)
:type frames_dir: str
:param warmup_frames: Cameras usually take a while to adapt their
luminosity and focus to the environment when taking a picture.
This parameter allows you to specify the number of "warmup" frames
to capture upon picture command before actually capturing a frame
(default: 5 but you may want to calibrate this parameter for your
camera)
:type warmup_frames: int
:param sleep_between_frames: If set, the process will sleep for the
specified amount of seconds between two frames when recording
(default: 0)
:type sleep_between_frames: float
:param max_stored_frames: Maximum number of frames to store in
``frames_dir`` when recording with no persistence (e.g. streaming
over HTTP) (default: 100)
:type max_stored_frames: int
:param color_transform: Color transformation to apply to the captured
frames. See https://docs.opencv.org/3.2.0/d7/d1b/group__imgproc__misc.html
for a full list of supported color transformations.
(default: "``COLOR_BGR2BGRA``")
:type color_transform: str
"""
super().__init__(*args, **kwargs)
self.default_device_id = device_id
self.frames_dir = os.path.abspath(os.path.expanduser(frames_dir))
self.warmup_frames = warmup_frames
self.sleep_between_frames = sleep_between_frames
self.max_stored_frames = max_stored_frames
self.color_transform = color_transform
self._is_recording = {} # device_id => Event map
self._devices = {} # device_id => VideoCapture map
self._recording_threads = {} # device_id => Thread map
def _init_device(self, device_id):
self._release_device(device_id)
if device_id not in self._devices:
self._devices[device_id] = cv2.VideoCapture(device_id)
if device_id not in self._is_recording:
self._is_recording[device_id] = threading.Event()
return self._devices[device_id]
def _release_device(self, device_id, wait_thread_termination=True):
if device_id in self._is_recording:
self._is_recording[device_id].clear()
if wait_thread_termination and device_id in self._recording_threads:
self.logger.info('A recording thread is running, waiting for termination')
if self._recording_threads[device_id].is_alive():
self._recording_threads[device_id].join()
del self._recording_threads[device_id]
if device_id in self._devices:
self._devices[device_id].release()
del self._devices[device_id]
def _store_frame_to_file(self, frame, frames_dir, image_file):
if image_file:
filepath = image_file
else:
os.makedirs(frames_dir, exist_ok=True)
filepath = os.path.join(
frames_dir, datetime.now().strftime('%Y-%m-%d_%H-%M-%S-%f.jpg'))
cv2.imwrite(filepath, frame)
return filepath
def _get_stored_frames_files(self, frames_dir):
return sorted([
os.path.join(frames_dir, f) for f in os.listdir(frames_dir)
if os.path.isfile(os.path.join(frames_dir, f)) and f.endswith('.jpg')
])
def _get_avg_fps(self, frames_dir):
files = self._get_stored_frames_files(frames_dir)
regex = re.compile('(\d+)-(\d+)-(\d+)_(\d+)-(\d+)-(\d+)-(\d+).jpe?g$')
frame_time_diff = 0.0
n_frames = 0
for i in range(1, len(files)):
m1 = re.search(regex, files[i-1])
m2 = re.search(regex, files[i])
if not m1 or not m2:
continue
t1 = datetime.timestamp(datetime(*map(int, m1.groups())))
t2 = datetime.timestamp(datetime(*map(int, m2.groups())))
frame_time_diff += (t2-t1)
n_frames += 1
return n_frames/frame_time_diff if n_frames and frame_time_diff else 0
def _remove_expired_frames(self, frames_dir, max_stored_frames):
files = self._get_stored_frames_files(frames_dir)
for f in files[max_stored_frames+1:len(files)]:
os.unlink(f)
def _make_video_file(self, frames_dir, video_file):
files = self._get_stored_frames_files(frames_dir)
if not files:
self.logger.warning('No frames found in {}'.format(frames_dir))
return
frame = cv2.imread(files[0])
height, width, layers = frame.shape
fps = self._get_avg_fps(frames_dir)
video = cv2.VideoWriter(video_file, 0, fps, (width, height))
for f in files:
video.write(cv2.imread(f))
video.release()
shutil.rmtree(frames_dir, ignore_errors=True)
def _recording_thread(self, duration, video_file, image_file, device_id,
frames_dir, n_frames, sleep_between_frames,
max_stored_frames, color_transform):
device = self._devices[device_id]
color_transform = getattr(cv2, self.color_transform)
def thread():
self._is_recording[device_id].wait()
self.logger.info('Starting recording from video device {}'.
format(device_id))
recording_started_time = time.time()
captured_frames = 0
while self._is_recording[device_id].is_set():
if duration and time.time() - recording_started_time >= duration \
or n_frames and captured_frames >= n_frames:
break
ret, frame = device.read()
if not ret:
self.logger.warning('Error while retrieving video frame')
continue
frame = cv2.cvtColor(frame, color_transform)
self._store_frame_to_file(frame=frame, frames_dir=frames_dir,
image_file=image_file)
captured_frames += 1
if max_stored_frames and not video_file:
self._remove_expired_frames(
frames_dir=frames_dir,
max_stored_frames=max_stored_frames)
if sleep_between_frames:
time.sleep(sleep_between_frames)
self._release_device(device_id, wait_thread_termination=False)
self.logger.info('Recording terminated')
if video_file:
self.logger.info('Writing frames to video file {}'.
format(video_file))
self._make_video_file(frames_dir=frames_dir,
video_file=video_file)
self.logger.info('Video file {}: rendering completed'.
format(video_file))
return thread
@action
def start_recording(self, duration=None, video_file=None, device_id=None,
frames_dir=None, sleep_between_frames=None,
max_stored_frames=None, color_transform=None):
"""
Start recording
:param duration: Record duration in seconds (default: None, record until
``stop_recording``)
:type duration: float
:param video_file: If set, the stream will be recorded to the specified
video file (default: None)
:type video_file: str
:param device_id, frames_dir, sleep_between_frames, max_stored_frames: Set
these parameters if you want to override the default configured ones.
"""
device_id = device_id if device_id is not None else self.default_device_id
frames_dir = os.path.abspath(os.path.expanduser(frames_dir)) \
if frames_dir is not None else self.frames_dir
sleep_between_frames = sleep_between_frames if sleep_between_frames \
is not None else self.sleep_between_frames
max_stored_frames = max_stored_frames if max_stored_frames \
is not None else self.max_stored_frames
color_transform = color_transform if color_transform \
is not None else self.color_transform
self._init_device(device_id)
frames_dir = os.path.join(frames_dir, str(device_id))
if video_file:
video_file = os.path.abspath(os.path.expanduser(video_file))
frames_dir = os.path.join(frames_dir, 'recording_{}'.format(
datetime.now().strftime('%Y-%m-%d_%H-%M-%S-%f')))
self._recording_threads[device_id] = threading.Thread(
target=self._recording_thread(duration=duration,
video_file=video_file,
image_file=None, device_id=device_id,
frames_dir=frames_dir, n_frames=None,
sleep_between_frames=sleep_between_frames,
max_stored_frames=max_stored_frames,
color_transform=color_transform))
self._recording_threads[device_id].start()
self._is_recording[device_id].set()
return { 'path': video_file if video_file else frames_dir }
@action
def stop_recording(self, device_id=None):
"""
Stop recording
"""
device_id = device_id if device_id is not None else self.default_device_id
self._release_device(device_id)
@action
def take_picture(self, image_file, device_id=None, warmup_frames=None,
color_transform=None):
"""
Take a picture.
:param image_file: Path where the output image will be stored.
:type image_file: str
:param device_id, warmup_frames, color_transform: Overrides the configured default parameters
"""
image_file = os.path.abspath(os.path.expanduser(image_file))
device_id = device_id if device_id is not None else self.default_device_id
warmup_frames = warmup_frames if warmup_frames is not None else \
self.warmup_frames
color_transform = color_transform if color_transform \
is not None else self.color_transform
self._init_device(device_id)
self._recording_threads[device_id] = threading.Thread(
target=self._recording_thread(duration=None, video_file=None,
image_file=image_file,
device_id=device_id, frames_dir=None,
n_frames=warmup_frames,
sleep_between_frames=None,
max_stored_frames=None,
color_transform=color_transform))
self._recording_threads[device_id].start()
self._is_recording[device_id].set()
return { 'path': image_file }
# vim:sw=4:ts=4:et: