Spaces:
Build error
Build error
""" | |
This module contains an image viewer and drawing routines based on OpenCV. | |
""" | |
import numpy as np | |
import cv2 | |
import time | |
def is_in_bounds(mat, roi): | |
"""Check if ROI is fully contained in the image. | |
Parameters | |
---------- | |
mat : ndarray | |
An ndarray of ndim>=2. | |
roi : (int, int, int, int) | |
Region of interest (x, y, width, height) where (x, y) is the top-left | |
corner. | |
Returns | |
------- | |
bool | |
Returns true if the ROI is contain in mat. | |
""" | |
if roi[0] < 0 or roi[0] + roi[2] >= mat.shape[1]: | |
return False | |
if roi[1] < 0 or roi[1] + roi[3] >= mat.shape[0]: | |
return False | |
return True | |
def view_roi(mat, roi): | |
"""Get sub-array. | |
The ROI must be valid, i.e., fully contained in the image. | |
Parameters | |
---------- | |
mat : ndarray | |
An ndarray of ndim=2 or ndim=3. | |
roi : (int, int, int, int) | |
Region of interest (x, y, width, height) where (x, y) is the top-left | |
corner. | |
Returns | |
------- | |
ndarray | |
A view of the roi. | |
""" | |
sx, ex = roi[0], roi[0] + roi[2] | |
sy, ey = roi[1], roi[1] + roi[3] | |
if mat.ndim == 2: | |
return mat[sy:ey, sx:ex] | |
else: | |
return mat[sy:ey, sx:ex, :] | |
class ImageViewer(object): | |
"""An image viewer with drawing routines and video capture capabilities. | |
Key Bindings: | |
* 'SPACE' : pause | |
* 'ESC' : quit | |
Parameters | |
---------- | |
update_ms : int | |
Number of milliseconds between frames (1000 / frames per second). | |
window_shape : (int, int) | |
Shape of the window (width, height). | |
caption : Optional[str] | |
Title of the window. | |
Attributes | |
---------- | |
image : ndarray | |
Color image of shape (height, width, 3). You may directly manipulate | |
this image to change the view. Otherwise, you may call any of the | |
drawing routines of this class. Internally, the image is treated as | |
beeing in BGR color space. | |
Note that the image is resized to the the image viewers window_shape | |
just prior to visualization. Therefore, you may pass differently sized | |
images and call drawing routines with the appropriate, original point | |
coordinates. | |
color : (int, int, int) | |
Current BGR color code that applies to all drawing routines. | |
Values are in range [0-255]. | |
text_color : (int, int, int) | |
Current BGR text color code that applies to all text rendering | |
routines. Values are in range [0-255]. | |
thickness : int | |
Stroke width in pixels that applies to all drawing routines. | |
""" | |
def __init__(self, update_ms, window_shape=(640, 480), caption="Figure 1"): | |
self._window_shape = window_shape | |
self._caption = caption | |
self._update_ms = update_ms | |
self._video_writer = None | |
self._user_fun = lambda: None | |
self._terminate = False | |
self.image = np.zeros(self._window_shape + (3, ), dtype=np.uint8) | |
self._color = (0, 0, 0) | |
self.text_color = (255, 255, 255) | |
self.thickness = 1 | |
def color(self): | |
return self._color | |
def color(self, value): | |
if len(value) != 3: | |
raise ValueError("color must be tuple of 3") | |
self._color = tuple(int(c) for c in value) | |
def rectangle(self, x, y, w, h, label=None): | |
"""Draw a rectangle. | |
Parameters | |
---------- | |
x : float | int | |
Top left corner of the rectangle (x-axis). | |
y : float | int | |
Top let corner of the rectangle (y-axis). | |
w : float | int | |
Width of the rectangle. | |
h : float | int | |
Height of the rectangle. | |
label : Optional[str] | |
A text label that is placed at the top left corner of the | |
rectangle. | |
""" | |
pt1 = int(x), int(y) | |
pt2 = int(x + w), int(y + h) | |
cv2.rectangle(self.image, pt1, pt2, self._color, self.thickness) | |
if label is not None: | |
text_size = cv2.getTextSize( | |
label, cv2.FONT_HERSHEY_PLAIN, 1, self.thickness) | |
center = pt1[0] + 5, pt1[1] + 5 + text_size[0][1] | |
pt2 = pt1[0] + 10 + text_size[0][0], pt1[1] + 10 + \ | |
text_size[0][1] | |
cv2.rectangle(self.image, pt1, pt2, self._color, -1) | |
cv2.putText(self.image, label, center, cv2.FONT_HERSHEY_PLAIN, | |
1, (255, 255, 255), self.thickness) | |
def circle(self, x, y, radius, label=None): | |
"""Draw a circle. | |
Parameters | |
---------- | |
x : float | int | |
Center of the circle (x-axis). | |
y : float | int | |
Center of the circle (y-axis). | |
radius : float | int | |
Radius of the circle in pixels. | |
label : Optional[str] | |
A text label that is placed at the center of the circle. | |
""" | |
image_size = int(radius + self.thickness + 1.5) # actually half size | |
roi = int(x - image_size), int(y - image_size), \ | |
int(2 * image_size), int(2 * image_size) | |
if not is_in_bounds(self.image, roi): | |
return | |
image = view_roi(self.image, roi) | |
center = image.shape[1] // 2, image.shape[0] // 2 | |
cv2.circle( | |
image, center, int(radius + .5), self._color, self.thickness) | |
if label is not None: | |
cv2.putText( | |
self.image, label, center, cv2.FONT_HERSHEY_PLAIN, | |
2, self.text_color, 2) | |
def gaussian(self, mean, covariance, label=None): | |
"""Draw 95% confidence ellipse of a 2-D Gaussian distribution. | |
Parameters | |
---------- | |
mean : array_like | |
The mean vector of the Gaussian distribution (ndim=1). | |
covariance : array_like | |
The 2x2 covariance matrix of the Gaussian distribution. | |
label : Optional[str] | |
A text label that is placed at the center of the ellipse. | |
""" | |
# chi2inv(0.95, 2) = 5.9915 | |
vals, vecs = np.linalg.eigh(5.9915 * covariance) | |
indices = vals.argsort()[::-1] | |
vals, vecs = np.sqrt(vals[indices]), vecs[:, indices] | |
center = int(mean[0] + .5), int(mean[1] + .5) | |
axes = int(vals[0] + .5), int(vals[1] + .5) | |
angle = int(180. * np.arctan2(vecs[1, 0], vecs[0, 0]) / np.pi) | |
cv2.ellipse( | |
self.image, center, axes, angle, 0, 360, self._color, 2) | |
if label is not None: | |
cv2.putText(self.image, label, center, cv2.FONT_HERSHEY_PLAIN, | |
2, self.text_color, 2) | |
def annotate(self, x, y, text): | |
"""Draws a text string at a given location. | |
Parameters | |
---------- | |
x : int | float | |
Bottom-left corner of the text in the image (x-axis). | |
y : int | float | |
Bottom-left corner of the text in the image (y-axis). | |
text : str | |
The text to be drawn. | |
""" | |
cv2.putText(self.image, text, (int(x), int(y)), cv2.FONT_HERSHEY_PLAIN, | |
2, self.text_color, 2) | |
def colored_points(self, points, colors=None, skip_index_check=False): | |
"""Draw a collection of points. | |
The point size is fixed to 1. | |
Parameters | |
---------- | |
points : ndarray | |
The Nx2 array of image locations, where the first dimension is | |
the x-coordinate and the second dimension is the y-coordinate. | |
colors : Optional[ndarray] | |
The Nx3 array of colors (dtype=np.uint8). If None, the current | |
color attribute is used. | |
skip_index_check : Optional[bool] | |
If True, index range checks are skipped. This is faster, but | |
requires all points to lie within the image dimensions. | |
""" | |
if not skip_index_check: | |
cond1, cond2 = points[:, 0] >= 0, points[:, 0] < 480 | |
cond3, cond4 = points[:, 1] >= 0, points[:, 1] < 640 | |
indices = np.logical_and.reduce((cond1, cond2, cond3, cond4)) | |
points = points[indices, :] | |
if colors is None: | |
colors = np.repeat( | |
self._color, len(points)).reshape(3, len(points)).T | |
indices = (points + .5).astype(np.int) | |
self.image[indices[:, 1], indices[:, 0], :] = colors | |
def enable_videowriter(self, output_filename, fourcc_string="MJPG", | |
fps=None): | |
""" Write images to video file. | |
Parameters | |
---------- | |
output_filename : str | |
Output filename. | |
fourcc_string : str | |
The OpenCV FOURCC code that defines the video codec (check OpenCV | |
documentation for more information). | |
fps : Optional[float] | |
Frames per second. If None, configured according to current | |
parameters. | |
""" | |
fourcc = cv2.VideoWriter_fourcc(*fourcc_string) | |
if fps is None: | |
fps = int(1000. / self._update_ms) | |
self._video_writer = cv2.VideoWriter( | |
output_filename, fourcc, fps, self._window_shape) | |
def disable_videowriter(self): | |
""" Disable writing videos. | |
""" | |
self._video_writer = None | |
def run(self, update_fun=None): | |
"""Start the image viewer. | |
This method blocks until the user requests to close the window. | |
Parameters | |
---------- | |
update_fun : Optional[Callable[] -> None] | |
An optional callable that is invoked at each frame. May be used | |
to play an animation/a video sequence. | |
""" | |
if update_fun is not None: | |
self._user_fun = update_fun | |
self._terminate, is_paused = False, False | |
# print("ImageViewer is paused, press space to start.") | |
while not self._terminate: | |
t0 = time.time() | |
if not is_paused: | |
self._terminate = not self._user_fun() | |
if self._video_writer is not None: | |
self._video_writer.write( | |
cv2.resize(self.image, self._window_shape)) | |
t1 = time.time() | |
remaining_time = max(1, int(self._update_ms - 1e3*(t1-t0))) | |
cv2.imshow( | |
self._caption, cv2.resize(self.image, self._window_shape[:2])) | |
key = cv2.waitKey(remaining_time) | |
if key & 255 == 27: # ESC | |
print("terminating") | |
self._terminate = True | |
elif key & 255 == 32: # ' ' | |
print("toggeling pause: " + str(not is_paused)) | |
is_paused = not is_paused | |
elif key & 255 == 115: # 's' | |
print("stepping") | |
self._terminate = not self._user_fun() | |
is_paused = True | |
# Due to a bug in OpenCV we must call imshow after destroying the | |
# window. This will make the window appear again as soon as waitKey | |
# is called. | |
# | |
# see https://github.com/Itseez/opencv/issues/4535 | |
self.image[:] = 0 | |
cv2.destroyWindow(self._caption) | |
cv2.waitKey(1) | |
cv2.imshow(self._caption, self.image) | |
def stop(self): | |
"""Stop the control loop. | |
After calling this method, the viewer will stop execution before the | |
next frame and hand over control flow to the user. | |
Parameters | |
---------- | |
""" | |
self._terminate = True |