Source code for QVideo.demos.ROIdemo
#!/usr/bin/env python3
'''Camcorder demo with a draggable ROI for recording a cropped video region.
Run directly::
python -m QVideo.demos.ROIdemo
'''
from qtpy import QtCore
import pyqtgraph as pg
import numpy as np
from pathlib import Path
from QVideo.QCamcorder import QCamcorder
__all__ = ['ROIFilter', 'ROIDemo']
[docs]
class ROIFilter(pg.RectROI):
'''Draggable rectangular ROI that crops video frames to its bounds.
Subclasses :class:`pyqtgraph.RectROI` and adds a :attr:`newFrame`
signal and :meth:`crop` slot, making it compatible with
:class:`~QVideo.lib.QVideoWriter` as a frame source.
Parameters
----------
fps : float
Frame rate of the video source [frames per second].
Stored as an attribute for :class:`~QVideo.lib.QVideoWriter`
compatibility.
pos : list[float]
Initial [x, y] position of the ROI.
size : list[float]
Initial [width, height] of the ROI.
**kwargs :
Additional keyword arguments forwarded to
:class:`~pyqtgraph.RectROI`.
Signals
-------
newFrame : np.ndarray
Emitted with the cropped frame each time :meth:`crop` is called.
'''
#: Emitted with the cropped frame each time :meth:`crop` is called.
newFrame = QtCore.Signal(np.ndarray)
def __init__(self, fps: float, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.fps = fps
[docs]
@QtCore.Slot(np.ndarray)
def crop(self, frame: np.ndarray) -> None:
'''Crop *frame* to the current ROI bounds and emit :attr:`newFrame`.
Parameters
----------
frame : np.ndarray
Input video frame to crop.
'''
x, y = self.pos()
w, h = self.size()
crop = frame[int(y):int(y + h), int(x):int(x + w)]
self.newFrame.emit(crop)
[docs]
class ROIDemo(QCamcorder):
'''Camcorder demo with a draggable ROI for recording cropped video.
Subclasses :class:`~QVideo.QCamcorder.QCamcorder` and overlays a
resizable :class:`ROIFilter` on the video screen. While recording,
camera frames are routed through the ROI cropper before being saved
by the DVR.
Parameters
----------
cameraTree : QCameraTree
Camera control tree providing the video source.
**kwargs :
Additional keyword arguments forwarded to
:class:`~QVideo.QCamcorder.QCamcorder`.
Attributes
----------
DISPLAY_RATE : int
Maximum display frame rate [fps]. Default: ``30``.
'''
DISPLAY_RATE: int = 30
def _roiGeometry(self) -> tuple[list[int], list[int]]:
'''Return default ROI position and size for the current source.
The ROI covers one quarter of the frame area (half each dimension),
rounded down to the nearest multiple of 8, and centered.
Returns
-------
pos : list[int]
``[x, y]`` top-left corner in image pixel coordinates.
size : list[int]
``[width, height]`` in image pixel coordinates.
'''
shape = self.source.shape
w = (shape.width() // 2 // 8) * 8
h = (shape.height() // 2 // 8) * 8
x = (shape.width() - w) // 2
y = (shape.height() - h) // 2
return [x, y], [w, h]
def _setupUi(self) -> None:
super()._setupUi()
self.screen.framerate = self.DISPLAY_RATE
pos, size = self._roiGeometry()
self.roi = ROIFilter(self.source.fps,
pos, size,
snapSize=8,
scaleSnap=True,
sideScalers=True,
movable=True,
invertible=False,
rotatable=False,
removable=False)
self.screen.view.addItem(self.roi)
self.dvr.filename = str(Path.home() / 'roidemo.avi')
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.dvr.source = self.roi
def _connectSignals(self) -> None:
super()._connectSignals()
self.dvr.recording.connect(self.recording)
[docs]
@QtCore.Slot(bool)
def recording(self, is_recording: bool) -> None:
'''Respond to DVR recording state changes.
Locks the ROI and connects the camera source to the ROI cropper
when recording starts; unlocks and disconnects when stopped.
Parameters
----------
is_recording : bool
``True`` when the DVR starts recording, ``False`` when stopped.
'''
if is_recording:
self.roi.movable = False
self.source.newFrame.connect(self.roi.crop)
else:
self.roi.movable = True
self.source.newFrame.disconnect(self.roi.crop)
def main() -> None: # pragma: no cover
'''Launch the ROI demo with an interactively chosen camera.'''
from QVideo.lib import choose_camera
pg.mkQApp('ROI Demo')
camera = choose_camera().start()
widget = ROIDemo(camera)
widget.show()
pg.exec()
if __name__ == '__main__': # pragma: no cover
main()