Fixed setting of output_file on FfmpegWriter.

Also, fixed parameters passed to camera
writer objects.
This commit is contained in:
Fabio Manganiello 2024-02-17 00:01:47 +01:00
parent a38ef6bc7a
commit c59446fdb1
Signed by: blacklight
GPG key ID: D90FBA7F76362774
3 changed files with 86 additions and 50 deletions

View file

@ -4,7 +4,6 @@ import pathlib
import socket
import threading
import time
from abc import ABC, abstractmethod
from contextlib import contextmanager
from dataclasses import asdict
@ -107,8 +106,8 @@ class CameraPlugin(Plugin, ABC):
video/sequence is captured (default: 0).
:param capture_timeout: Maximum number of seconds to wait between the programmed termination of a capture
session and the moment the device is released.
:param scale_x: If set, the images will be scaled along the x axis by the specified factor
:param scale_y: If set, the images will be scaled along the y axis by the specified factor
:param scale_x: If set, the images will be scaled along the x-axis by the specified factor
:param scale_y: If set, the images will be scaled along the y-axis by the specified factor
:param color_transform: Color transformation to apply to the images.
:param grayscale: Whether the output should be converted to grayscale.
:param rotate: If set, the images will be rotated by the specified number of degrees
@ -526,7 +525,7 @@ class CameraPlugin(Plugin, ABC):
if video_file:
self.fire_event(CameraVideoRenderedEvent(filename=video_file))
def start_camera(self, camera: Camera, preview: bool = False, *args, **kwargs):
def start_camera(self, camera: Camera, *args, preview: bool = False, **kwargs):
"""
Start a camera capture session.
@ -548,6 +547,7 @@ class CameraPlugin(Plugin, ABC):
@action
def capture_video(
self,
device: Optional[Union[int, str]] = None,
duration: Optional[float] = None,
video_file: Optional[str] = None,
preview: bool = False,
@ -556,6 +556,7 @@ class CameraPlugin(Plugin, ABC):
"""
Capture a video.
:param device: Name/path/ID of the device to capture from (default: None, use the default device).
:param duration: Record duration in seconds (default: None, record until ``stop_capture``).
:param video_file: If set, the stream will be recorded to the specified video file (default: None).
:param camera: Camera parameters override - see constructors parameters.
@ -563,7 +564,7 @@ class CameraPlugin(Plugin, ABC):
:return: If duration is specified, the method will wait until the recording is done and return the local path
to the recorded resource. Otherwise, it will return the status of the camera device after starting it.
"""
camera = self.open_device(**camera)
camera = self.open_device(device=device, **camera)
self.start_camera(
camera,
duration=duration,
@ -595,17 +596,24 @@ class CameraPlugin(Plugin, ABC):
self.close_device(dev)
@action
def capture_image(self, image_file: str, preview: bool = False, **camera) -> str:
def capture_image(
self,
image_file: str,
device: Optional[Union[int, str]] = None,
preview: bool = False,
**camera,
) -> str:
"""
Capture an image.
:param image_file: Path where the output image will be stored.
:param device: Name/path/ID of the device to capture from (default: None, use the default device).
:param camera: Camera parameters override - see constructors parameters.
:param preview: Show a preview of the camera frames.
:return: The local path to the saved image.
"""
with self.open(**camera) as camera:
with self.open(device=device, **camera) as camera:
warmup_frames = (
camera.info.warmup_frames if camera.info.warmup_frames else 1
)
@ -617,20 +625,23 @@ class CameraPlugin(Plugin, ABC):
return image_file
@action
def take_picture(self, image_file: str, **camera) -> str:
def take_picture(
self, image_file: str, device: Optional[Union[int, str]] = None, **camera
) -> str:
"""
Alias for :meth:`.capture_image`.
:param image_file: Path where the output image will be stored.
:param device: Name/path/ID of the device to capture from (default: None, use the default device).
:param camera: Camera parameters override - see constructors parameters.
:param preview: Show a preview of the camera frames.
:return: The local path to the saved image.
"""
return str(self.capture_image(image_file, **camera).output)
return str(self.capture_image(image_file, device=device, **camera).output)
@action
def capture_sequence(
self,
device: Optional[Union[int, str]] = None,
duration: Optional[float] = None,
n_frames: Optional[int] = None,
preview: bool = False,
@ -639,6 +650,7 @@ class CameraPlugin(Plugin, ABC):
"""
Capture a sequence of frames from a camera and store them to a directory.
:param device: Name/path/ID of the device to capture from (default: None, use the default device).
:param duration: Duration of the sequence in seconds (default: until :meth:`.stop_capture` is called).
:param n_frames: Number of images to be captured (default: until :meth:`.stop_capture` is called).
:param camera: Camera parameters override - see constructors parameters. ``frames_dir`` and ``fps`` in
@ -646,7 +658,7 @@ class CameraPlugin(Plugin, ABC):
:param preview: Show a preview of the camera frames.
:return: The directory where the image files have been stored.
"""
with self.open(**camera) as camera:
with self.open(device=device, **camera) as camera:
self.start_camera(
camera, duration=duration, n_frames=n_frames, preview=preview
)
@ -655,17 +667,22 @@ class CameraPlugin(Plugin, ABC):
@action
def capture_preview(
self, duration: Optional[float] = None, n_frames: Optional[int] = None, **camera
self,
device: Optional[Union[int, str]] = None,
duration: Optional[float] = None,
n_frames: Optional[int] = None,
**camera,
) -> dict:
"""
Start a camera preview session.
:param device: Name/path/ID of the device to capture from (default: None, use the default device).
:param duration: Preview duration (default: until :meth:`.stop_capture` is called).
:param n_frames: Number of frames to display before closing (default: until :meth:`.stop_capture` is called).
:param camera: Camera object properties.
:return: The status of the device.
"""
camera = self.open_device(frames_dir=None, **camera)
camera = self.open_device(device=device, frames_dir=None, **camera)
self.start_camera(camera, duration=duration, n_frames=n_frames, preview=True)
return self.status(camera.info.device) # type: ignore
@ -744,17 +761,24 @@ class CameraPlugin(Plugin, ABC):
@action
def start_streaming(
self, duration: Optional[float] = None, stream_format: str = 'mkv', **camera
self,
device: Optional[Union[int, str]] = None,
duration: Optional[float] = None,
stream_format: str = 'mkv',
**camera,
) -> dict:
"""
Expose the video stream of a camera over a TCP connection.
:param device: Name/path/ID of the device to capture from (default: None, use the default device).
:param duration: Streaming thread duration (default: until :meth:`.stop_streaming` is called).
:param stream_format: Format of the output stream - e.g. ``h264``, ``mjpeg``, ``mkv`` etc. (default: ``mkv``).
:param camera: Camera object properties - see constructor parameters.
:return: The status of the device.
"""
camera = self.open_device(stream=True, stream_format=stream_format, **camera)
camera = self.open_device(
device=device, stream=True, stream_format=stream_format, **camera
)
return self._start_streaming(camera, duration, stream_format) # type: ignore
def _start_streaming(
@ -900,7 +924,7 @@ class CameraPlugin(Plugin, ABC):
return frame
size = (int(frame.size[0] * scale_x), int(frame.size[1] * scale_y))
return frame.resize(size, Image.ANTIALIAS)
return frame.resize(size, Image.BICUBIC)
@staticmethod
def encode_frame(frame, encoding: str = 'jpeg') -> bytes:

View file

@ -1,12 +1,13 @@
import signal
import subprocess
from typing import Optional, Tuple
from typing import Iterable, Optional
from PIL import Image
from PIL.Image import Image as ImageType
from platypush.plugins.camera import CameraPlugin
from platypush.plugins.camera.ffmpeg.model import FFmpegCamera, FFmpegCameraInfo
from platypush.plugins.camera.model.camera import Camera
class CameraFfmpegPlugin(CameraPlugin):
@ -21,75 +22,83 @@ class CameraFfmpegPlugin(CameraPlugin):
self,
device: Optional[str] = '/dev/video0',
input_format: str = 'v4l2',
ffmpeg_args: Tuple[str] = (),
**opts
ffmpeg_args: Iterable[str] = (),
**opts,
):
"""
:param device: Path to the camera device (default: ``/dev/video0``).
:param input_format: FFmpeg input format for the the camera device (default: ``v4l2``).
:param input_format: FFmpeg input format for the camera device (default: ``v4l2``).
:param ffmpeg_args: Extra options to be passed to the FFmpeg executable.
:param opts: Camera options - see constructor of :class:`platypush.plugins.camera.CameraPlugin`.
"""
super().__init__(device=device, input_format=input_format, **opts)
self.camera_info.ffmpeg_args = ffmpeg_args or ()
self.camera_info.ffmpeg_args = ffmpeg_args or () # type: ignore
def prepare_device(self, camera: FFmpegCamera) -> subprocess.Popen:
warmup_seconds = self._get_warmup_seconds(camera)
def prepare_device(self, device: Camera) -> subprocess.Popen:
assert isinstance(device, FFmpegCamera)
warmup_seconds = self._get_warmup_seconds(device)
ffmpeg = [
camera.info.ffmpeg_bin,
device.info.ffmpeg_bin,
'-y',
'-f',
camera.info.input_format,
device.info.input_format,
'-i',
camera.info.device,
device.info.device,
'-s',
'{}x{}'.format(*camera.info.resolution),
*(
(f'{device.info.resolution[0]}x{device.info.resolution[1]}',)
if device.info.resolution
else ()
),
'-ss',
str(warmup_seconds),
*(('-r', str(camera.info.fps)) if camera.info.fps else ()),
*(('-r', str(device.info.fps)) if device.info.fps else ()),
'-pix_fmt',
'rgb24',
'-f',
'rawvideo',
*camera.info.ffmpeg_args,
*device.info.ffmpeg_args,
'-',
]
self.logger.info('Running FFmpeg: {}'.format(' '.join(ffmpeg)))
self.logger.info('Running FFmpeg command: "%s"', ' '.join(ffmpeg))
proc = subprocess.Popen(ffmpeg, stdout=subprocess.PIPE)
# Start in suspended mode
proc.send_signal(signal.SIGSTOP)
return proc
def start_camera(
self, camera: FFmpegCamera, preview: bool = False, *args, **kwargs
):
def start_camera(self, camera: Camera, *args, preview: bool = False, **kwargs):
assert isinstance(camera, FFmpegCamera)
super().start_camera(*args, camera=camera, preview=preview, **kwargs)
if camera.object:
camera.object.send_signal(signal.SIGCONT)
def release_device(self, camera: FFmpegCamera):
if camera.object:
camera.object.terminate()
if camera.object.stdout:
camera.object.stdout.close()
camera.object = None
def release_device(self, device: Camera):
assert isinstance(device, FFmpegCamera)
if device.object:
device.object.terminate()
if device.object.stdout:
device.object.stdout.close()
device.object = None # type: ignore
def wait_capture(self, camera: FFmpegCamera) -> None:
def wait_capture(self, camera: Camera):
assert isinstance(camera, FFmpegCamera)
if camera.object and camera.object.poll() is None:
try:
camera.object.wait(timeout=camera.info.capture_timeout)
except Exception as e:
self.logger.warning('Error on FFmpeg capture wait: {}'.format(str(e)))
self.logger.warning('Error on FFmpeg capture wait: %s', e)
def capture_frame(
self, camera: FFmpegCamera, *args, **kwargs
) -> Optional[ImageType]:
raw_size = camera.info.resolution[0] * camera.info.resolution[1] * 3
data = camera.object.stdout.read(raw_size)
def capture_frame(self, device: Camera, *_, **__) -> Optional[ImageType]:
assert isinstance(device, FFmpegCamera)
assert device.info.resolution, 'Resolution not set'
assert device.object.stdout, 'Camera not started'
raw_size = device.info.resolution[0] * device.info.resolution[1] * 3
data = device.object.stdout.read(raw_size)
if len(data) < raw_size:
return
return Image.frombytes('RGB', camera.info.resolution, data)
return Image.frombytes('RGB', device.info.resolution, data)
# vim:sw=4:ts=4:et:

View file

@ -3,7 +3,6 @@ import logging
import multiprocessing
import os
import time
from abc import ABC, abstractmethod
from typing import Optional, IO
@ -63,8 +62,12 @@ class FileVideoWriter(VideoWriter, ABC):
"""
def __init__(self, *args, output_file: str, **kwargs):
super().__init__(self, *args, **kwargs)
self.output_file = os.path.abspath(os.path.expanduser(output_file))
super().__init__(
self,
*args,
output_file=os.path.abspath(os.path.expanduser(output_file)),
**kwargs,
)
class StreamWriter(VideoWriter, ABC):