Source code for QVideo.demos.filterrackdemo
#!/usr/bin/env python3
'''Camcorder with an interactive filter rack and three recording modes.
Run directly::
python -m QVideo.demos.filterrackdemo
The filter rack starts with :class:`~QVideo.filters.smoothing.QSmoothingFilter`
and :class:`~QVideo.filters.threshold.QThresholdFilter` pre-loaded.
Additional filters can be added interactively via the "Add filter…" button,
and any filter can be removed or reordered by dragging.
Three recording modes are offered:
**Raw**
The unfiltered camera stream at the full camera frame rate.
**Filtered**
The rack-processed stream at the full camera frame rate. Every
frame is filtered, regardless of the display throttle.
**Display**
The rack-processed stream at the throttled display rate. Only
frames that are actually rendered to screen are recorded.
'''
import numpy as np
from qtpy import QtCore, QtWidgets
from QVideo.QCamcorder import QCamcorder
from QVideo.lib import QCameraTree, QFilterRack
from QVideo.filters import QSmoothingFilter, QThresholdFilter
from QVideo.lib.videotypes import Image
__all__ = ['FilterRackDemo']
class _FilteredSource(QtCore.QObject):
'''Applies a :class:`~QVideo.lib.QFilterRack.QFilterRack` to every frame
from a source at the camera's full frame rate.
Connects to ``source.newFrame``, passes each frame through the rack,
and re-emits the result. This gives the DVR a valid source for
filtered full-speed recording, independent of the display throttle.
Parameters
----------
source : QVideoSource
The raw camera source to intercept.
rack : QFilterRack
The filter rack to apply to each frame.
parent : QtWidgets.QWidget or None
Parent object.
'''
newFrame = QtCore.Signal(np.ndarray)
def __init__(self,
source,
rack: QFilterRack,
parent: QtCore.QObject | None = None) -> None:
super().__init__(parent)
self._source = source
self._rack = rack
self._active = True
source.newFrame.connect(self._process)
@property
def fps(self) -> float | None:
'''Frame rate of the underlying source [fps].'''
return self._source.fps
def setActive(self, active: bool) -> None:
'''Connect or disconnect from the source.
Parameters
----------
active : bool
``True`` to resume processing; ``False`` to pause.
'''
if active == self._active:
return
self._active = active
if active:
self._source.newFrame.connect(self._process)
else:
self._source.newFrame.disconnect(self._process)
@QtCore.Slot(np.ndarray)
def _process(self, frame: Image) -> None:
self.newFrame.emit(self._rack(frame))
[docs]
class FilterRackDemo(QCamcorder):
'''Camcorder with an interactive filter rack and three recording modes.
Extends :class:`~QVideo.QCamcorder.QCamcorder` by adding a
:class:`~QVideo.lib.QFilterRack.QFilterRack` pre-loaded with
:class:`~QVideo.filters.smoothing.QSmoothingFilter` and
:class:`~QVideo.filters.threshold.QThresholdFilter`, and
a group of radio buttons to select the DVR recording source:
- **Raw**: unfiltered frames at the camera's full frame rate.
- **Filtered**: rack-processed frames at the camera's full frame rate.
- **Display**: rack-processed frames at the throttled display rate.
Parameters
----------
cameraWidget : QCameraTree
Camera control tree providing the video source.
**kwargs :
Additional keyword arguments forwarded to
:class:`~QVideo.QCamcorder.QCamcorder`.
'''
def __init__(self, cameraWidget: QCameraTree, **kwargs) -> None:
super().__init__(cameraWidget, **kwargs)
self.rack = QFilterRack(self)
self.rack.add(QSmoothingFilter())
self.rack.add(QThresholdFilter())
self.screen.filter = self.rack
self._filteredSource = _FilteredSource(self.source, self.rack, self)
self._setupModeUi()
self._connectModeSignals()
self.controls.layout().addWidget(self._modeBox)
self.controls.layout().addWidget(self.rack)
def _setupModeUi(self) -> None:
self._modeBox = QtWidgets.QGroupBox('Record from')
layout = QtWidgets.QHBoxLayout(self._modeBox)
self._modeRaw = QtWidgets.QRadioButton('Raw')
self._modeFiltered = QtWidgets.QRadioButton('Filtered')
self._modeDisplay = QtWidgets.QRadioButton('Display')
self._modeRaw.setChecked(True)
for btn in (self._modeRaw, self._modeFiltered, self._modeDisplay):
layout.addWidget(btn)
self._modeGroup = QtWidgets.QButtonGroup(self)
self._modeGroup.addButton(self._modeRaw)
self._modeGroup.addButton(self._modeFiltered)
self._modeGroup.addButton(self._modeDisplay)
def _connectModeSignals(self) -> None:
self._modeGroup.buttonToggled.connect(self._onModeToggled)
[docs]
@QtCore.Slot(bool)
def dvrPlayback(self, playback: bool) -> None:
'''Pause the filtered source during DVR playback.
Extends :meth:`~QVideo.QCamcorder.QCamcorder.dvrPlayback` by
suspending :class:`_FilteredSource` while the DVR is playing.
This prevents live camera frames from racing through the shared
filter rack alongside DVR frames, which would corrupt
:class:`~QVideo.lib.AsyncVideoFilter.AsyncVideoFilter` results.
Parameters
----------
playback : bool
``True`` when DVR playback begins, ``False`` when it ends.
'''
super().dvrPlayback(playback)
self._filteredSource.setActive(not playback)
@QtCore.Slot(QtWidgets.QAbstractButton, bool)
def _onModeToggled(self,
button: QtWidgets.QAbstractButton,
checked: bool) -> None:
'''Switch the DVR source when a recording-mode button is selected.
Parameters
----------
button : QAbstractButton
The button whose state changed.
checked : bool
``True`` when the button is selected; ``False`` on deselection.
'''
if not checked:
return
if button is self._modeRaw:
self.dvr.source = self.source
elif button is self._modeFiltered:
self.dvr.source = self._filteredSource
elif button is self._modeDisplay:
self.dvr.source = self.screen
def main() -> None: # pragma: no cover
'''Launch the filter rack demo with an interactively chosen camera.'''
import pyqtgraph as pg
from QVideo.lib import choose_camera
pg.mkQApp('Filter Rack Demo')
camera = choose_camera().start()
widget = FilterRackDemo(camera)
widget.show()
pg.exec()
if __name__ == '__main__': # pragma: no cover
main()