image_viewer.py
Go to the documentation of this file.
1 # vim: expandtab:ts=4:sw=4
2 """
3 This module contains an image viewer and drawing routines based on OpenCV.
4 """
5 import numpy as np
6 import cv2
7 import time
8 
9 
10 def is_in_bounds(mat, roi):
11  """Check if ROI is fully contained in the image.
12 
13  Parameters
14  ----------
15  mat : ndarray
16  An ndarray of ndim>=2.
17  roi : (int, int, int, int)
18  Region of interest (x, y, width, height) where (x, y) is the top-left
19  corner.
20 
21  Returns
22  -------
23  bool
24  Returns true if the ROI is contain in mat.
25 
26  """
27  if roi[0] < 0 or roi[0] + roi[2] >= mat.shape[1]:
28  return False
29  if roi[1] < 0 or roi[1] + roi[3] >= mat.shape[0]:
30  return False
31  return True
32 
33 
34 def view_roi(mat, roi):
35  """Get sub-array.
36 
37  The ROI must be valid, i.e., fully contained in the image.
38 
39  Parameters
40  ----------
41  mat : ndarray
42  An ndarray of ndim=2 or ndim=3.
43  roi : (int, int, int, int)
44  Region of interest (x, y, width, height) where (x, y) is the top-left
45  corner.
46 
47  Returns
48  -------
49  ndarray
50  A view of the roi.
51 
52  """
53  sx, ex = roi[0], roi[0] + roi[2]
54  sy, ey = roi[1], roi[1] + roi[3]
55  if mat.ndim == 2:
56  return mat[sy:ey, sx:ex]
57  else:
58  return mat[sy:ey, sx:ex, :]
59 
60 
62  """An image viewer with drawing routines and video capture capabilities.
63 
64  Key Bindings:
65 
66  * 'SPACE' : pause
67  * 'ESC' : quit
68 
69  Parameters
70  ----------
71  update_ms : int
72  Number of milliseconds between frames (1000 / frames per second).
73  window_shape : (int, int)
74  Shape of the window (width, height).
75  caption : Optional[str]
76  Title of the window.
77 
78  Attributes
79  ----------
80  image : ndarray
81  Color image of shape (height, width, 3). You may directly manipulate
82  this image to change the view. Otherwise, you may call any of the
83  drawing routines of this class. Internally, the image is treated as
84  beeing in BGR color space.
85 
86  Note that the image is resized to the the image viewers window_shape
87  just prior to visualization. Therefore, you may pass differently sized
88  images and call drawing routines with the appropriate, original point
89  coordinates.
90  color : (int, int, int)
91  Current BGR color code that applies to all drawing routines.
92  Values are in range [0-255].
93  text_color : (int, int, int)
94  Current BGR text color code that applies to all text rendering
95  routines. Values are in range [0-255].
96  thickness : int
97  Stroke width in pixels that applies to all drawing routines.
98 
99  """
100 
101  def __init__(self, update_ms, window_shape=(640, 480), caption="Figure 1"):
102  self._window_shape = window_shape
103  self._caption = caption
104  self._update_ms = update_ms
105  self._video_writer = None
106  self._user_fun = lambda: None
107  self._terminate = False
108 
109  self.image = np.zeros(self._window_shape + (3, ), dtype=np.uint8)
110  self._color = (0, 0, 0)
111  self.text_color = (255, 255, 255)
112  self.thickness = 1
113 
114  @property
115  def color(self):
116  return self._color
117 
118  @color.setter
119  def color(self, value):
120  if len(value) != 3:
121  raise ValueError("color must be tuple of 3")
122  self._color = tuple(int(c) for c in value)
123 
124  def rectangle(self, x, y, w, h, label=None):
125  """Draw a rectangle.
126 
127  Parameters
128  ----------
129  x : float | int
130  Top left corner of the rectangle (x-axis).
131  y : float | int
132  Top let corner of the rectangle (y-axis).
133  w : float | int
134  Width of the rectangle.
135  h : float | int
136  Height of the rectangle.
137  label : Optional[str]
138  A text label that is placed at the top left corner of the
139  rectangle.
140 
141  """
142  pt1 = int(x), int(y)
143  pt2 = int(x + w), int(y + h)
144  cv2.rectangle(self.image, pt1, pt2, self._color, self.thickness)
145  if label is not None:
146  text_size = cv2.getTextSize(
147  label, cv2.FONT_HERSHEY_PLAIN, 1, self.thickness)
148 
149  center = pt1[0] + 5, pt1[1] + 5 + text_size[0][1]
150  pt2 = pt1[0] + 10 + text_size[0][0], pt1[1] + 10 + \
151  text_size[0][1]
152  cv2.rectangle(self.image, pt1, pt2, self._color, -1)
153  cv2.putText(self.image, label, center, cv2.FONT_HERSHEY_PLAIN,
154  1, (255, 255, 255), self.thickness)
155 
156  def circle(self, x, y, radius, label=None):
157  """Draw a circle.
158 
159  Parameters
160  ----------
161  x : float | int
162  Center of the circle (x-axis).
163  y : float | int
164  Center of the circle (y-axis).
165  radius : float | int
166  Radius of the circle in pixels.
167  label : Optional[str]
168  A text label that is placed at the center of the circle.
169 
170  """
171  image_size = int(radius + self.thickness + 1.5) # actually half size
172  roi = int(x - image_size), int(y - image_size), \
173  int(2 * image_size), int(2 * image_size)
174  if not is_in_bounds(self.image, roi):
175  return
176 
177  image = view_roi(self.image, roi)
178  center = image.shape[1] // 2, image.shape[0] // 2
179  cv2.circle(
180  image, center, int(radius + .5), self._color, self.thickness)
181  if label is not None:
182  cv2.putText(
183  self.image, label, center, cv2.FONT_HERSHEY_PLAIN,
184  2, self.text_color, 2)
185 
186  def gaussian(self, mean, covariance, label=None):
187  """Draw 95% confidence ellipse of a 2-D Gaussian distribution.
188 
189  Parameters
190  ----------
191  mean : array_like
192  The mean vector of the Gaussian distribution (ndim=1).
193  covariance : array_like
194  The 2x2 covariance matrix of the Gaussian distribution.
195  label : Optional[str]
196  A text label that is placed at the center of the ellipse.
197 
198  """
199  # chi2inv(0.95, 2) = 5.9915
200  vals, vecs = np.linalg.eigh(5.9915 * covariance)
201  indices = vals.argsort()[::-1]
202  vals, vecs = np.sqrt(vals[indices]), vecs[:, indices]
203 
204  center = int(mean[0] + .5), int(mean[1] + .5)
205  axes = int(vals[0] + .5), int(vals[1] + .5)
206  angle = int(180. * np.arctan2(vecs[1, 0], vecs[0, 0]) / np.pi)
207  cv2.ellipse(
208  self.image, center, axes, angle, 0, 360, self._color, 2)
209  if label is not None:
210  cv2.putText(self.image, label, center, cv2.FONT_HERSHEY_PLAIN,
211  2, self.text_color, 2)
212 
213  def annotate(self, x, y, text):
214  """Draws a text string at a given location.
215 
216  Parameters
217  ----------
218  x : int | float
219  Bottom-left corner of the text in the image (x-axis).
220  y : int | float
221  Bottom-left corner of the text in the image (y-axis).
222  text : str
223  The text to be drawn.
224 
225  """
226  cv2.putText(self.image, text, (int(x), int(y)), cv2.FONT_HERSHEY_PLAIN,
227  2, self.text_color, 2)
228 
229  def colored_points(self, points, colors=None, skip_index_check=False):
230  """Draw a collection of points.
231 
232  The point size is fixed to 1.
233 
234  Parameters
235  ----------
236  points : ndarray
237  The Nx2 array of image locations, where the first dimension is
238  the x-coordinate and the second dimension is the y-coordinate.
239  colors : Optional[ndarray]
240  The Nx3 array of colors (dtype=np.uint8). If None, the current
241  color attribute is used.
242  skip_index_check : Optional[bool]
243  If True, index range checks are skipped. This is faster, but
244  requires all points to lie within the image dimensions.
245 
246  """
247  if not skip_index_check:
248  cond1, cond2 = points[:, 0] >= 0, points[:, 0] < 480
249  cond3, cond4 = points[:, 1] >= 0, points[:, 1] < 640
250  indices = np.logical_and.reduce((cond1, cond2, cond3, cond4))
251  points = points[indices, :]
252  if colors is None:
253  colors = np.repeat(
254  self._color, len(points)).reshape(3, len(points)).T
255  indices = (points + .5).astype(np.int)
256  self.image[indices[:, 1], indices[:, 0], :] = colors
257 
258  def enable_videowriter(self, output_filename, fourcc_string="MJPG",
259  fps=None):
260  """ Write images to video file.
261 
262  Parameters
263  ----------
264  output_filename : str
265  Output filename.
266  fourcc_string : str
267  The OpenCV FOURCC code that defines the video codec (check OpenCV
268  documentation for more information).
269  fps : Optional[float]
270  Frames per second. If None, configured according to current
271  parameters.
272 
273  """
274  fourcc = cv2.VideoWriter_fourcc(*fourcc_string)
275  if fps is None:
276  fps = int(1000. / self._update_ms)
277  self._video_writer = cv2.VideoWriter(
278  output_filename, fourcc, fps, self._window_shape)
279 
281  """ Disable writing videos.
282  """
283  self._video_writer = None
284 
285  def run(self, update_fun=None):
286  """Start the image viewer.
287 
288  This method blocks until the user requests to close the window.
289 
290  Parameters
291  ----------
292  update_fun : Optional[Callable[] -> None]
293  An optional callable that is invoked at each frame. May be used
294  to play an animation/a video sequence.
295 
296  """
297  if update_fun is not None:
298  self._user_fun = update_fun
299 
300  self._terminate, is_paused = False, False
301  # print("ImageViewer is paused, press space to start.")
302  while not self._terminate:
303  t0 = time.time()
304  if not is_paused:
305  self._terminate = not self._user_fun()
306  if self._video_writer is not None:
307  self._video_writer.write(
308  cv2.resize(self.image, self._window_shape))
309  t1 = time.time()
310  remaining_time = max(1, int(self._update_ms - 1e3*(t1-t0)))
311  cv2.imshow(
312  self._caption, cv2.resize(self.image, self._window_shape[:2]))
313  key = cv2.waitKey(remaining_time)
314  if key & 255 == 27: # ESC
315  print("terminating")
316  self._terminate = True
317  elif key & 255 == 32: # ' '
318  print("toggeling pause: " + str(not is_paused))
319  is_paused = not is_paused
320  elif key & 255 == 115: # 's'
321  print("stepping")
322  self._terminate = not self._user_fun()
323  is_paused = True
324 
325  # Due to a bug in OpenCV we must call imshow after destroying the
326  # window. This will make the window appear again as soon as waitKey
327  # is called.
328  #
329  # see https://github.com/Itseez/opencv/issues/4535
330  self.image[:] = 0
331  cv2.destroyWindow(self._caption)
332  cv2.waitKey(1)
333  cv2.imshow(self._caption, self.image)
334 
335  def stop(self):
336  """Stop the control loop.
337 
338  After calling this method, the viewer will stop execution before the
339  next frame and hand over control flow to the user.
340 
341  Parameters
342  ----------
343 
344  """
345  self._terminate = True
def colored_points(self, points, colors=None, skip_index_check=False)
def enable_videowriter(self, output_filename, fourcc_string="MJPG", fps=None)
def circle(self, x, y, radius, label=None)
def __init__(self, update_ms, window_shape=(640, 480), caption="Figure 1")
def gaussian(self, mean, covariance, label=None)
def rectangle(self, x, y, w, h, label=None)


jsk_perception
Author(s): Manabu Saito, Ryohei Ueda
autogenerated on Mon May 3 2021 03:03:27