Source code for QVideo.lib.QSnapshot
'''Still-frame capture from a live video stream.'''
from qtpy import QtCore, QtGui, QtWidgets
from QVideo.lib.videotypes import Image
from pathlib import Path
import numpy as np
import datetime
import logging
logger = logging.getLogger(__name__)
__all__ = ['QSnapshot']
try:
_Fmt = QtGui.QImage.Format
except AttributeError:
_Fmt = QtGui.QImage
[docs]
class QSnapshot(QtCore.QObject):
'''Save still frames from a live video stream.
Connect any signal that emits an :class:`~QVideo.lib.videotypes.Image`
to :meth:`newFrame` so that the most recent frame is always cached.
Call :meth:`snap` (or press the hotkey) to write the cached frame to disk.
Call :meth:`snapAs` to choose the save path via a file dialog.
The source determines what is captured:
- ``QVideoSource.newFrame`` — raw camera frames, no filters
- ``QVideoScreen.newFrame`` — post-filter frames
- ``QVideoScreen.newFrame`` with ``composite=True`` — filtered + overlaid
Frames are saved as-is; no channel reordering is applied. For cameras
that deliver BGR data (e.g. OpenCV), the saved PNG will have swapped
red and blue channels compared to the on-screen display.
Parameters
----------
parent : QWidget
Parent widget. Shortcuts are registered on this widget.
key : str
Keyboard shortcut that triggers :meth:`snap` (auto-timestamp save).
Default: ``'Ctrl+Shift+S'``.
key_as : str
Keyboard shortcut that triggers :meth:`snapAs` (file-dialog save).
Default: ``'Ctrl+Shift+Alt+S'``.
Slots
-----
newFrame(Image)
Cache the most recent frame.
snap()
Save the cached frame to a timestamped PNG in the user's home directory.
snapAs()
Prompt for a filename pre-filled with the auto-generated name, then save.
'''
def __init__(self, parent: QtWidgets.QWidget,
key: str = 'Ctrl+Shift+S',
key_as: str = 'Ctrl+Shift+Alt+S') -> None:
super().__init__(parent)
self._frame: Image | None = None
shortcut = QtWidgets.QShortcut(QtGui.QKeySequence(key), parent)
shortcut.activated.connect(self.snap)
shortcut_as = QtWidgets.QShortcut(QtGui.QKeySequence(key_as), parent)
shortcut_as.activated.connect(self.snapAs)
[docs]
@QtCore.Slot(np.ndarray)
def newFrame(self, frame: Image) -> None:
'''Cache the most recent frame.'''
self._frame = frame
def _defaultPath(self) -> str:
'''Return a timestamped PNG path in the user's home directory.'''
timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
return str(Path.home() / f'snapshot_{timestamp}.png')
[docs]
@QtCore.Slot()
def snap(self) -> None:
'''Save the cached frame to a timestamped PNG in the home directory.'''
if self._frame is None:
logger.warning('snap: no frame available')
return
self._save(self._frame, self._defaultPath())
[docs]
@QtCore.Slot()
def snapAs(self) -> None:
'''Prompt for a filename pre-filled with the auto-generated name, then save.'''
if self._frame is None:
logger.warning('snapAs: no frame available')
return
filename, _ = QtWidgets.QFileDialog.getSaveFileName(
None, 'Save Snapshot', self._defaultPath(),
'PNG Images (*.png);;TIFF Images (*.tiff);;JPEG Images (*.jpg)')
if filename:
self._save(self._frame, filename)
def _save(self, frame: Image, filename: str) -> None:
'''Write *frame* to *filename* using Qt image facilities.
Parameters
----------
frame : Image
Frame to save. Must be uint8.
filename : str
Destination path. The file format is inferred from the extension.
'''
if frame.dtype != np.uint8:
logger.warning(f'_save: unsupported dtype {frame.dtype}; '
'expected uint8')
return
frame = np.ascontiguousarray(frame)
if frame.ndim == 2:
h, w = frame.shape
img = QtGui.QImage(frame.tobytes(), w, h, w,
_Fmt.Format_Grayscale8)
elif frame.ndim == 3 and frame.shape[2] == 3:
h, w = frame.shape[:2]
img = QtGui.QImage(frame.tobytes(), w, h, 3 * w,
_Fmt.Format_RGB888)
elif frame.ndim == 3 and frame.shape[2] == 4:
h, w = frame.shape[:2]
img = QtGui.QImage(frame.tobytes(), w, h, 4 * w,
_Fmt.Format_RGBA8888)
else:
logger.warning(f'_save: unsupported frame shape {frame.shape}')
return
if not img.save(filename):
logger.warning(f'_save: failed to save {filename!r}')
else:
logger.info(f'Snapshot saved: {filename!r}')
[docs]
@classmethod
def example(cls: type['QSnapshot']) -> None: # pragma: no cover
'''Demonstrate QSnapshot with a noise source.'''
import pyqtgraph as pg
from QVideo.cameras.Noise import QNoiseSource
app = pg.mkQApp()
window = QtWidgets.QWidget()
snapshot = cls(window)
source = QNoiseSource()
source.newFrame.connect(snapshot.newFrame)
source.start()
window.show()
pg.exec()
if __name__ == '__main__': # pragma: no cover
QSnapshot.example()