Source code for vuba.gui

import functools
import cv2
from typing import Callable, Iterable
from dataclasses import dataclass
import numpy as np

from vuba import imio


[docs]@dataclass class TrackbarMethod: """ Container for a trackbar method and it's associated variables. Parameters ---------- id : str Identification string of trackbar added in ``BaseGUI.trackbar``. min : int Minimum limit of trackbar. max : int Maximum limit of trackbar. method : callable Trackbar callback as specified in ``BaseGUI.trackbar``. current : int Current trackbar value. Returns ------- container : dataclass Mutable container for a trackbar and it's associated variables. """ __slots__ = ["id", "min", "max", "method", "current"] id: str #: Identification string of trackbar added in ``BaseGUI.trackbar``. min: int #: Minimum limit of trackbar. max: int #: Maximum limit of trackbar. method: Callable #: Trackbar callback as specified in ``BaseGUI.trackbar``. current: int #: Current trackbar value.
[docs]class BaseGUI: """ The base constructor class for creating HighGUI interfaces. Typical usage of this class is through the supplied wrappers around this class, although if those are ill-fit for a given application this constructor can be addressed directly. To use this constructor with single or multiple frames that are in memory, use the ``FrameGUI`` or ``FramesGUI`` classes respectively, Conversely, to use this constructor with video files or streams, use the ``VideoGUI`` or ``StreamGUI`` classes respectively. Parameters ---------- title : str Title of the interface window. Returns ------- gui : BaseGUI The newly created gui constructor. See Also -------- FrameGUI FramesGUI VideoGUI StreamGUI """
[docs] def __init__(self, title): self.title = title self.trackbars = {} cv2.namedWindow(self.title)
[docs] def values(self) -> "BaseGUI": """ Retrieve all current trackbar values from the interface. Returns ------- values : dict or None All current trackbar values, returns None if no trackbars declared. See Also -------- BaseGUI.__getitem__ BaseGUI.__setitem__ """ if self.trackbars: vals = {} for k in self.trackbars: vals[k] = self.trackbars[k].current else: vals = None return vals
[docs] def __getitem__(self, key) -> "BaseGUI": """ Access a trackbar's current value. Parameters ---------- key : str Trackbar identification string provided when using ``BaseGUI.trackbar``. Returns ------- value : int Trackbar's current value. See Also -------- BaseGUI.values BaseGUI.__setitem__ """ return self.trackbars[key].current
[docs] def __setitem__(self, key, val) -> "BaseGUI": """ Change a trackbar's current recorded value. Parameters ---------- key : str Trackbar identification string provided when using ``BaseGUI.trackbar``. val : int Value to record trackbar as. Notes ----- ``BaseGUI.__setitem__`` will not change the trackbar value in the OpenCV interface and will only change the value recorded in the ``BaseGUI`` class instance. See Also -------- BaseGUI.values BaseGUI.__getitem__ """ self.trackbars[key].current = val
[docs] def method(self, func) -> "BaseGUI": """ Add a main processing function to be executed on every trackbar call. All trackbar functions should call the function that is wrapped in this decorator. As such changes made to a single trackbar, will propagate to this main processing function and the changes will be returned to the named window. Parameters ---------- func : callable Main processing function for the interface. See Also -------- BaseGUI.trackbar Examples -------- To demonstrate this method's usage as a decorator we can construct a simple binary threshold viewer for a random binary image: >>> import vuba >>> import numpy as np >>> import cv2 >>> img = np.random.randint(low=0, high=255, size=(500, 500), dtype=np.uint8) >>> gui = vuba.BaseGUI('Binary threshold viewer') >>> @gui.method >>> def threshold(gui): ... frame = img.copy() ... thresh_val = gui['thresh_val'] ... _, thresh = cv2.threshold(frame, thresh_val, 255, cv2.THRESH_BINARY) ... return thresh >>> gui.trackbar('Threshold', id='thresh_val', min=0, max=255)(None) >>> gui.run() Note that this method does not have to be used as a decorator: >>> gui = vuba.BaseGUI('Binary threshold viewer') >>> def threshold(gui): ... frame = img.copy() ... thresh_val = gui['thresh_val'] ... _, thresh = cv2.threshold(frame, thresh_val, 255, cv2.THRESH_BINARY) ... return thresh >>> gui.method(threshold) >>> gui.trackbar('Threshold', id='thresh_val', min=0, max=255)(None) >>> gui.run() Also note that since we are not doing any additional processing on each trackbar call in the above two examples, we can supply None to the ``BaseGUI.trackbar`` decorator. This will default to using a simple callback that will pass the trackbar values to the main processing method and display the result in the interactive window. """ @functools.wraps(func) def wrap_to_proc(): img = func(self) return img self.process = wrap_to_proc return wrap_to_proc
@staticmethod def _basic_trackbar_callback(gui, id, val): """ Basic trackbar callback to be used when ``BaseGUI.trackbar`` is not supplied a function. """ gui[id] = val img = gui.process() cv2.imshow(gui.title, img)
[docs] def trackbar(self, name, id, min, max) -> "BaseGUI": """ Add a trackbar to the interactive window. Parameters ---------- name : str Name of trackbar to add. id : str Id of trackbar to add. This will be the key associated with the trackbar. min : int Minimum limit of trackbar max : int Maximum limit of trackbar func : callable, optional Trackbar callback to be called whenever the trackbar is changed. If None, then a basic callback will be used that simply passes the trackbar value to the ``BaseGUI.method`` method. Notes ----- This method can be called in much the same way as ``BaseGUI.method``, either as a decorator or as a typical method call. Examples -------- For these examples, we will use the same binary threshold viewer as used in ``BaseGUI.method``: >>> import vuba >>> import numpy as np >>> import cv2 >>> def threshold(gui): ... frame = img.copy() ... thresh_val = gui['thresh_val'] ... _, thresh = cv2.threshold(frame, thresh_val, 255, cv2.THRESH_BINARY) ... return thresh >>> img = np.random.randint(low=0, high=255, size=(500, 500), dtype=np.uint8) >>> gui = vuba.BaseGUI('Binary threshold viewer') >>> gui.method(threshold) For applications where there is no additional processing on each trackbar call, we can simply supply None to ``BaseGUI.trackbar`` as follows: >>> gui.trackbar('Threshold', id='thresh_val', min=0, max=255)(None) >>> gui.run() However, if we wanted to add some further processing, let's say to exclude all odd threshold values, we could handle this logic with a custom callback: >>> gui = vuba.BaseGUI('Binary threshold viewer') >>> gui.method(threshold) >>> @gui.trackbar('Threshold', id='thresh_val', min=0, max=255) >>> def even_threshold(gui, val): ... if val%2 == 0: ... gui['thresh_val'] = val ... ret = gui.process() ... cv2.imshow(gui.title, ret) >>> gui.run() """ def wrap_to_trackbar(func): if not func: @functools.wraps(self._basic_trackbar_callback) def on_exe(val): return self._basic_trackbar_callback(self, id, val) else: @functools.wraps(func) def on_exe(val): return func(self, val) self.trackbars[id] = TrackbarMethod(id, min, max, on_exe, min) cv2.createTrackbar(name, self.title, min, max, on_exe) return on_exe return wrap_to_trackbar
[docs] def run(self) -> "BaseGUI": """ Launch the interface. Examples -------- Note that you can access any variables from the class that you added/manipulated through the trackbars attribute. This contains a dict of the trackbars, with each key containing associated variables in a dataclass: >>> import vuba >>> import numpy as np >>> import cv2 >>> img = np.random.randint(low=0, high=255, size=(500, 500), dtype=np.uint8) >>> gui = vuba.BaseGUI('Binary threshold viewer') >>> @gui.method >>> def threshold(gui): ... frame = img.copy() ... thresh_val = gui['thresh_val'] ... _, thresh = cv2.threshold(frame, thresh_val, 255, cv2.THRESH_BINARY) ... return thresh >>> gui.trackbar('Threshold', id='thresh_val', min=0, max=255)(None) >>> gui.run() >>> name, min, max, method, last_value = gui.trackbars['thresh_val'] This can be useful for retrieving the ideal parameters for an image analysis method after adjusting them in an interface for example. """ # Execute first method to launch the gui firstfunc = self.trackbars[[*self.trackbars][0]] func = firstfunc.method min = firstfunc.min func(min) cv2.waitKey() cv2.destroyAllWindows()
[docs]class FrameGUI(BaseGUI): """ Class for creating interfaces for individual image manipulation. Parameters ---------- frame : ndarray Frame(s) to manipulate within the interface. title : str Title of the interface window. Returns ------- gui : FrameGUI Class object for creating the interactive window. See Also -------- BaseGUI FramesGUI VideoGUI StreamGUI Notes ----- This class does not impose any restrictions on the types/formats of the images you supply upon initiation. As such, we can supply a tuple or list of images that we want to manipulate at the same time for example (see examples below). Examples -------- As in other single image examples, we will use a series of randomly generated binary images with a variable binary threshold in the interfaces below: >>> import vuba >>> import numpy as np >>> import cv2 >>> img = np.random.randint(low=0, high=255, size=(500, 500), dtype=np.uint8) >>> gui = vuba.FrameGUI(img, 'Binary threshold viewer') >>> @gui.method >>> def threshold(gui): ... frame = gui.frame.copy() ... thresh_val = gui['thresh_val'] ... _, thresh = cv2.threshold(frame, thresh_val, 255, cv2.THRESH_BINARY) ... return thresh >>> gui.trackbar('Threshold', id='thresh_val', min=0, max=255)(None) >>> gui.run() As mentioned above, we can supply a series of images for simultaneuous manipulation as well: >>> img1 = np.random.randint(low=0, high=255, size=(500, 500), dtype=np.uint8) >>> img2 = np.random.randint(low=0, high=255, size=(500, 500), dtype=np.uint8) >>> gui = vuba.FrameGUI((img1, img2), 'Binary threshold viewer') >>> @gui.method >>> def threshold(gui): ... frame1, frame2 = gui.img ... thresh_val = gui['thresh_val'] ... _, thresh1 = cv2.threshold(frame1.copy(), thresh_val, 255, cv2.THRESH_BINARY) ... _, thresh2 = cv2.threshold(frame2.copy(), thresh_val, 255, cv2.THRESH_BINARY) ... return np.hstack((thresh1, thresh2)) >>> gui.trackbar('Threshold', id='thresh_val', min=0, max=255)(None) >>> gui.run() For an extension of this type of interface with drawing in addition, see ``/examples/interfaces/binarythreshod_viewer_with_drawing_video.py``. """
[docs] def __init__(self, frame, *args, **kwargs): self.frame = frame super(FrameGUI, self).__init__(*args, **kwargs)
[docs]class FramesGUI(BaseGUI): """ Class for creating interfaces for manipulating a sequence of frames. Parameters ---------- frames : list or ndarray Images to manipulate within the interface. indices : tuple Frame indices to limit GUI to, especially useful when analysing long sequences of footage. First index will always be interpreted as the minimum index and the second as the maximum index. If None is specified to either limit, then that limit will be ignored. Default is for no limits, i.e. (None, None). title : str Title of the interface window. Returns ------- gui : FramesGUI Class object for creating the interactive window. See Also -------- BaseGUI FrameGUI VideoGUI StreamGUI Notes ----- Any gui created with this class will by default have a frame trackbar and corresponding callback for retrieving frames from the sequence provided at initiation (see ``FramesGUI.read``). Examples -------- Since the API for creating an interface is the same regardless of footage format, we will demonstrate a basic frame viewer here. First, let's create a sequence of binary images that gradually increase in their grayscale value: >>> import numpy as np >>> frames = [np.full((500, 500), i, dtype=np.uint8) for i in range(1, 255)] Now let's pass these to our basic frame viewer to view the result: >>> import vuba >>> import cv2 >>> gui = vuba.FramesGUI(frames, 'Frame viewer') >>> @gui.method >>> def blank(gui): ... # Here we are not doing any image processing as we simply ... # want to view the individual frames ... frame = gui.frame.copy() ... return frame >>> gui.run() """
[docs] def __init__(self, frames, indices=(None, None), *args, **kwargs): self.frames = frames super(FramesGUI, self).__init__(*args, **kwargs) lower_ind, upper_ind = indices if not lower_ind: lower_ind = 0 if not upper_ind: upper_ind = len(frames) self.lower = lower_ind self.diff = upper_ind - lower_ind self.trackbar("Frames", "frames", 0, self.diff)(self.read)
[docs] @staticmethod def read(gui, val) -> "FramesGUI": """ Callback for reading and displaying a frame from the provided frames. Parameters ---------- val : int Frame index in the requested frames. Notes ----- Any image processing in the main method will be executed prior to displaying the frame. """ if val >= gui.trackbars["frames"].min and val < gui.trackbars["frames"].max: gui["frames"] = val gui.frame = gui.frames[int(val + gui.lower)] frame_proc = gui.process() cv2.imshow(gui.title, frame_proc)
[docs]class VideoGUI(BaseGUI): """ Class for creating interfaces for manipulating a sequence of frames. Parameters ---------- video : vuba.Video Video to manipulate within the interface. indices : tuple Frame indices to limit GUI to, especially useful when analysing long sequences of footage. First index will always be interpreted as the minimum index and the second as the maximum index. If None is specified to either limit, then that limit will be ignored. Default is for no limits, i.e. (None, None). title : str Title of the interface window. Returns ------- gui : VideoGUI Class object for creating the interactive window. See Also -------- BaseGUI FrameGUI FramesGUI StreamGUI Notes ----- Any gui created with this class will by default have a frame trackbar and corresponding callback for retrieving frames from the video provided at initiation (see ``VideoGUI.read``). Examples -------- Please see the following example scripts for some typical applications of this class: * examples/interfaces/frame_viewer.py * examples/interfaces/binary_threshold_viewer_with_drawing_video.py """
[docs] def __init__(self, video, indices=(None, None), *args, **kwargs): self.video, self.release = imio.open_video(video) super(VideoGUI, self).__init__(*args, **kwargs) lower_ind, upper_ind = indices if not lower_ind: lower_ind = 0 if not upper_ind: upper_ind = len(video) self.lower = lower_ind self.diff = upper_ind - lower_ind self.trackbar("Frames", "frames", 0, self.diff)(self.read)
[docs] @staticmethod def read(gui, val) -> "VideoGUI": """ Callback for reading and displaying a frame from the requested video. Parameters ---------- val : int Frame index in the requested video. Notes ----- Any image processing in the main method will be executed prior to displaying the frame. """ if val >= gui.trackbars["frames"].min and val < gui.trackbars["frames"].max: gui["frames"] = val gui.frame = gui.video.read(int(val + gui.lower), grayscale=False) frame_proc = gui.process() cv2.imshow(gui.title, frame_proc)
[docs] def run(self) -> "VideoGUI": """ Launch the interactive video interface. Notes ----- After running the interface, this function will close/teardown the video handler supplied if it was created at initiation. """ try: super().run() finally: if self.release: self.video.close()
[docs]class StreamGUI(BaseGUI): """ Class for creating interfaces for manipulation of images from video feeds. Parameters ---------- stream : Iterable An iterator to a camera stream. This can be a generator that will continuously pull frames from a capture device for example. title : str Title of the interface window. Returns ------- gui : StreamGUI Class object for creating the interactive window. See Also -------- BaseGUI FrameGUI FramesGUI VideoGUI Notes ----- Upon execution of the interface through ``StreamGUI.run``, this interface will continuously pull frames from the provided stream and display them in the interactive window. Examples -------- Please see the following example scripts for some typical applications of this class: * examples/interfaces/binary_threshold_viewer_with_drawing_camera.py """
[docs] def __init__(self, stream, *args, **kwargs): self.stream = stream super(StreamGUI, self).__init__(*args, **kwargs)
[docs] def run(self) -> "StreamGUI": """ Launch the interactive window. Notes ----- Whilst this class overrides ``BaseGUI.run``, exiting the interface is the same as other wrappers in that any key press will cause the interface to close. If you provide an iterable that has a limit, the interface created will exit once it has reached the end of the iterable. As such, we would recommend using ``FramesGUI`` when using limited sequences of footage rather streams or feeds. """ while True: try: self.frame = next(self.stream) except StopIteration: break except: raise frame_proc = self.process() cv2.imshow(self.title, frame_proc) k = cv2.waitKey(1) if k > 0: break