Source code for facelift.window

# -*- encoding: utf-8 -*-
# Copyright (c) 2020 Stephen Bunn <stephen@bunn.io>
# ISC License <https://choosealicense.com/licenses/isc>

"""Contains some helper abstractions for OpenCV windows and frame rendering.

This collection of window helpers is just to help standardize and cleanup how to
interact with OpenCV window displays.
The :class:`~opencv_window` context manager is very easy to use for getting a quick
window for rendering frames as they are produced.

For example:

>>> from pathlib import Path
>>> from facelift.window import opencv_window
>>> from facelift.capture import iter_media_frames
>>> with opencv_window() as window:
...     for frame in iter_media_frames(Path("~/my-file.mp4")):
...         window.render(frame)

This context manager will produce a new window for rendering the frames read from
``my-file.mp4`` and will destroy the window once the context is exited.

I wouldn't recommend using this for any kind of production use; mostly the OpenCV window
is just useful for debugging.

Attributes:
    DEFAULT_WINDOW_TITLE (str):
        The default OpenCV window title if none is supplied.
        Defaults to "Facelift".
    DEFAULT_WINDOW_DELAY (int):
        The default number of milliseconds to wait between showing frames.
        Defaults to 1.
    DEFAULT_WINDOW_STEP_KEY (int):
        The default ASCII key index to use as the step key when step is enabled.
        Defaults to ``0x20`` (Space).
"""

from contextlib import AbstractContextManager
from types import TracebackType
from typing import Optional, Type

import attr
import cv2

from .types import Frame

DEFAULT_WINDOW_TITLE = "Facelift"
DEFAULT_WINDOW_DELAY = 1
DEFAULT_WINDOW_STEP_KEY = 0x20


[docs]class WindowStyle: """Object namespace of available OpenCV window styles. Attributes: DEFAULT (int): The default OpenCV window style. AUTOSIZE (int): Automatically fit window size on creation. GUI_NORMAL (int): Window with a basic GUI experience. GUI_EXPANDED (int): Window with an expanded GUI experience. FULLSCREEN (int): Window that displays frames fullscreen (full-canvas). FREE_RATIO (int): Window that allows for any window ratio. KEEP_RATIO (int): Window that maintains the original window ratio. OPENGL (int): Window rendered via OpenGL. May not work for some machines and will only work if OpenCV is compiled with GPU support. """ DEFAULT = cv2.WINDOW_NORMAL AUTOSIZE = cv2.WINDOW_AUTOSIZE GUI_NORMAL = cv2.WINDOW_GUI_NORMAL GUI_EXPANDED = cv2.WINDOW_GUI_EXPANDED FULLSCREEN = cv2.WINDOW_FULLSCREEN FREE_RATIO = cv2.WINDOW_FREERATIO KEEP_RATIO = cv2.WINDOW_KEEPRATIO OPENGL = cv2.WINDOW_OPENGL # This property is currently just used for testing __all__ = [ DEFAULT, AUTOSIZE, GUI_NORMAL, GUI_EXPANDED, FULLSCREEN, FREE_RATIO, KEEP_RATIO, OPENGL, ]
[docs]@attr.s class opencv_window(AbstractContextManager): """Create an OpenCV window that closes once the context exits. Examples: Easy usage of OpenCV's provided window to display read frames from a webcam. >>> from facelift.window import opencv_window >>> with opencv_window() as window: ... for frame in iter_stream_frames(): ... window.render(frame) Args: title (str): The title of the OpenCV window. style (int): The style of the OpenCV window. delay (float): The number of milliseconds to delay between displaying frames. step (bool): Flag that indicates if the window should wait for a press of the defined ``step_key`` before releasing the render call. Defaults to False. step_key (int): The ASCII integer index of the key to wait for press when ``step`` is True. Defaults to ``0x20`` (Space). Raises: ValueError: If the given window title is an empty string ValueError: If the given window delay is less or equal to 0 """ title: str = attr.ib(default=DEFAULT_WINDOW_TITLE) style: int = attr.ib(default=WindowStyle.DEFAULT) delay: float = attr.ib(default=DEFAULT_WINDOW_DELAY) step: bool = attr.ib(default=False) step_key: int = attr.ib(default=DEFAULT_WINDOW_STEP_KEY) @title.validator def _validate_title(self, attribute: attr.Attribute, value: str): """Validate the window context's title. Args: attribute (~attr.Attribute): The attribute containing the window's title value (str): The given value of the window title Raises: ValueError: If the title is an empty string """ if not isinstance(value, str) or len(value) <= 0: raise ValueError( f"Window title must be a non-empty string, received {value!r}" ) @delay.validator def _validate_delay(self, attribute: attr.Attribute, value: float): """Validate the window context's delay. Args: attribute (~attr.Attribute): The attribute containing the window's delay value (float): The given value of the window delay Raises: ValueError: If the delay is less than or equal to 0 """ if not isinstance(value, int) or value <= 0: raise ValueError( f"Window delay must be a non-zero value, received {value!r}" )
[docs] def __enter__(self): """Initialize the context of the window.""" self.create() return super().__enter__()
[docs] def __exit__( self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], traceback: Optional[TracebackType], ) -> Optional[bool]: """Destroy the context of the window.""" self.close() return super().__exit__(exc_type, exc_value, traceback)
[docs] def create(self): """Create a new window with the current context's title and style.""" cv2.namedWindow(winname=self.title, flags=self.style)
[docs] def close(self): """Destroy the window with the current context's title.""" cv2.destroyWindow(winname=self.title)
[docs] def render(self, frame: Frame): """Render a given frame in the current window. Args: frame (:attr:`~.types.Frame`): The frame to render within the window """ cv2.imshow(winname=self.title, mat=frame) cv2.waitKey(delay=self.delay) if self.step: while cv2.waitKey(delay=0) != self.step_key: continue