Source code for QVideo.dvr.QOpenCVWriter

'''OpenCV-backed video file writer supporting AVI, MKV, and MP4.'''
from QVideo.lib import QVideoWriter
from QVideo.lib.videotypes import Image
from pathlib import Path
import cv2
import logging


__all__ = ['QOpenCVWriter']


logger = logging.getLogger(__name__)


[docs] class QOpenCVWriter(QVideoWriter): '''OpenCV-backed video file writer supporting AVI, MKV, and MP4. Writes frames to a video file using ``cv2.VideoWriter``. The file is opened lazily on the first frame so that frame dimensions and color mode can be determined automatically. Codecs are selected based on the file extension using :attr:`CODEC_MAP`. When no codec is specified, the preference-ordered list for the extension is probed and the first one OpenCV accepts is used. Specifying *codec* explicitly bypasses probing. If the shape of a subsequent frame differs from the first, recording stops immediately and :attr:`~QVideo.lib.QVideoWriter.finished` is emitted. Parameters ---------- filename : str Path to the output video file. codec : str or None Four-character codec code passed to ``cv2.VideoWriter_fourcc``. If ``None``, codecs are chosen from :attr:`CODEC_MAP` based on the file extension. *args : Forwarded to :class:`~QVideo.lib.QVideoWriter`. **kwargs : Forwarded to :class:`~QVideo.lib.QVideoWriter`. Attributes ---------- CODEC_MAP : dict[str, tuple[str, ...]] Maps file extensions to preference-ordered codec codes. ''' CODEC_MAP: dict[str, tuple[str, ...]] = { '.avi': ('FFV1', 'HFYU'), '.mkv': ('FFV1', 'HFYU'), '.mp4': ('avc1', 'mp4v'), } def __init__(self, *args, codec: str | None = None, **kwargs) -> None: super().__init__(*args, **kwargs) if codec is not None: self._codecs = (codec,) else: suffix = Path(self.filename).suffix self._codecs = self.CODEC_MAP.get(suffix, ()) self._writer = None self._shape = None
[docs] def open(self, frame: Image) -> bool: '''Open the video file using the first available codec. Called automatically by :meth:`~QVideo.lib.QVideoWriter.write` on the first frame. Frame dimensions and color mode are determined from *frame*; codecs are probed via :meth:`_getWriter`. Parameters ---------- frame : Image The first video frame, used to determine dimensions and color mode. Returns ------- bool ``True`` if a codec was found and the file was opened; ``False`` otherwise. ''' color = (frame.ndim == 3) self._writer = self._getWriter(frame.shape, color) if self._writer is not None: self._shape = frame.shape return True return False
def _getWriter(self, shape: tuple[int, ...], color: bool) -> cv2.VideoWriter | None: '''Probe codecs in preference order and return the first that opens. Parameters ---------- shape : tuple[int, ...] Frame shape ``(height, width)`` used to configure the writer. color : bool ``True`` for color frames, ``False`` for grayscale. Returns ------- cv2.VideoWriter or None An open ``cv2.VideoWriter``, or ``None`` if no codec succeeded. ''' h, w = shape[:2] for codec in self._codecs: fourcc = cv2.VideoWriter_fourcc(*codec) writer = cv2.VideoWriter( self.filename, fourcc, self.fps, (w, h), color) if writer.isOpened(): logger.debug(f'Opened {self.filename!r} with codec {codec!r}') return writer writer.release() logger.debug(f'Codec {codec!r} not available') logger.warning(f'No supported codec available for {self.filename!r}') return None
[docs] def isOpen(self) -> bool: '''Return ``True`` if the video file is currently open.''' return (self._writer is not None) and self._writer.isOpened()
def _write(self, frame: Image) -> None: '''Write *frame* to the video file, stopping if the shape changes.''' if frame.shape != self._shape: logger.warning( f'Frame shape {frame.shape} does not match ' f'expected {self._shape}: stopping recording') self.finished.emit() return if frame.ndim == 3: frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR) self._writer.write(frame)
[docs] def close(self) -> None: '''Release the video file and reset internal state.''' if self.isOpen(): self._writer.release() self._writer = None self._shape = None