1 """
2 Created on Mar 14, 2011
3 @author: svohara
4 """
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37 import pyvision as pv
38 import cv
39 import PIL.Image
40 import weakref
41
43 """
44 Displays thumbnails of a list of input images as a single
45 'montage' image. Supports scrolling if there are more images
46 than "viewports" in the layout.
47 """
48
49 - def __init__(self, image_list, layout=(2, 4), tile_size=(64, 48), gutter=2, by_row=True, labels='index',
50 keep_aspect=True, highlight_selected=False):
51 """
52 Constructor
53 @param image_list: A list of pyvision images that you wish to display
54 as a montage.
55 @param layout: A tuple (rows,cols) that indicates the number of tiles to
56 show in a single montage page, oriented in a grid.
57 @param tile_size: The size of each thumbnail image to display in the montage.
58 @param gutter: The width in pixels of the gutter between thumbnails.
59 @param by_row: If true, the image tiles are placed in row-major order, that
60 is, one row of the montage is filled before moving to the next. If false,
61 then column order is used instead.
62 @param labels: Used to show a label at the lower left corner of each image in the montage.
63 If this parameter is a list, then it should be the same length as len(image_list) and contain
64 the label to be used for the corresponding image. If labels == 'index', then the image
65 montage will simply display the index of the image in image_list. Set labels to None to suppress labels.
66 @param keep_aspect: If true the original image aspect ratio will be preserved.
67 @param highlight_selected: If true, any image tile in the montage which has been clicked will
68 be drawn with a rectangular highlight. This will toggle, such that if an image is clicked a second
69 time, the highlighting will be removed.
70 """
71 self._tileSize = tile_size
72 self._rows = layout[0]
73 self._cols = layout[1]
74 self._images = image_list
75 self._gutter = gutter
76 self._by_row = by_row
77 self._txtfont = cv.InitFont(cv.CV_FONT_HERSHEY_SIMPLEX, 0.5, 0.5)
78 self._txtcolor = (255, 255, 255)
79 self._imgPtr = 0
80 self._labels = labels
81 self._clickHandler = clickHandler(self)
82 self._keep_aspect = keep_aspect
83 self._image_positions = []
84 self._select_handler = None
85 self._highlighted = highlight_selected
86 self._selected_tiles = []
87
88
89 if self._rows * self._cols < len(image_list):
90 if by_row:
91 self._xpad = 0
92 self._ypad = 25
93 else:
94 self._ypad = 0
95 self._xpad = 25
96 else:
97
98 self._xpad = 0
99 self._ypad = 0
100
101 imgWidth = self._cols * ( tile_size[0] + gutter ) + gutter + 2 * self._xpad
102 imgHeight = self._rows * (tile_size[1] + gutter) + gutter + 2 * self._ypad
103 self._size = (imgWidth, imgHeight)
104
105 cvimg = cv.CreateImage(self._size, cv.IPL_DEPTH_8U, 3)
106 self._cvMontageImage = cvimg
107
108 self._initDecrementArrow()
109 self._initIncrementArrow()
110 self.draw()
111
112 - def draw(self, mousePos=None):
113 """
114 Computes the image montage from the source images based on the current
115 image pointer (position in list of images), etc. This internally constructs
116 the montage, but show() is required for display and mouse-click handling.
117 """
118 cv.SetZero(self._cvMontageImage)
119
120 img_ptr = self._imgPtr
121 if img_ptr > 0:
122
123
124 cv.FillConvexPoly(self._cvMontageImage, self._decrArrow, (125, 125, 125))
125
126 if img_ptr + (self._rows * self._cols) < len(self._images):
127
128
129 cv.FillConvexPoly(self._cvMontageImage, self._incrArrow, (125, 125, 125))
130
131 self._image_positions = []
132 if self._by_row:
133 for row in range(self._rows):
134 for col in range(self._cols):
135 if img_ptr > len(self._images) - 1: break
136 tile = pv.Image(self._images[img_ptr].asAnnotated())
137 self._composite(tile, (row, col), img_ptr)
138 img_ptr += 1
139 else:
140 for col in range(self._cols):
141 for row in range(self._rows):
142 if img_ptr > len(self._images) - 1: break
143 tile = pv.Image(self._images[img_ptr].asAnnotated())
144 self._composite(tile, (row, col), img_ptr)
145 img_ptr += 1
146
147
148
149
150
151
153 """
154 If you don't want to use the montage's built-in mouse-click handling by calling
155 the ImageMontage.show() method, then this method will return the montage image
156 computed from the last call to draw().
157 """
158 return pv.Image(self._cvMontageImage)
159
160 - def show(self, window="Image Montage", pos=None, delay=0):
161 """
162 Will display the montage image, as well as register the mouse handling callback
163 function so that the user can scroll the montage by clicking the increment/decrement
164 arrows.
165 @return: The key code of the key pressed, if any, that dismissed the window.
166 """
167 img = self.asImage()
168 cv.NamedWindow(window)
169 cv.SetMouseCallback(window, self._clickHandler.onClick, window)
170 key = img.show(window=window, pos=pos, delay=delay)
171 return key
172
174 """
175 Add a function that will be called when an image is selected.
176 The handler function should take an image, the image index,
177 a list of labels, and a dictionary of other info.
178 """
179 self._select_handler = handler
180
182 '''
183 If the montage was created with highlight_selected option enabled,
184 then this function will cause a set of tiles in the montage to be
185 highlighted.
186 @note: Calling this method will erase any previous selections made
187 by the user.
188 '''
189 self._selected_tiles = idxs
190 self.draw()
191
193 '''
194 Returns the index list of the tiles which were selected/highlighted
195 by the users
196 '''
197 return sorted(self._selected_tiles)
198
200 """
201 internal method to determine the clicked region of the montage.
202 @return: -1 for decrement region, 1 for increment region, and 0 otherwise.
203 If a select handler function was defined (via setSelectHandler), then
204 this function will be called when the user clicks within the region
205 of one of the tiles of the montage. Signature of selectHandler function
206 is f(img, imgNum, dict). As of now, the only key/value pair passed
207 into the dict is "imgLabel":<label>.
208 """
209 if self._by_row:
210
211 decr_rect = pv.Rect(0, 0, self._size[0], self._ypad)
212 incr_rect = pv.Rect(0, self._size[1] - self._ypad, self._size[0], self._ypad)
213 else:
214
215 decr_rect = pv.Rect(0, 0, self._xpad, self._size[1])
216 incr_rect = pv.Rect(self._size[0] - self._xpad, 0, self._xpad, self._size[1])
217
218 pt = pv.Point(x, y)
219 if incr_rect.containsPoint(pt):
220
221 return 1
222 elif decr_rect.containsPoint(pt):
223
224 return -1
225 else:
226
227 for img, imgNum, rect in self._image_positions:
228 if rect.containsPoint(pt):
229 if imgNum in self._selected_tiles:
230 self._selected_tiles.remove(imgNum)
231 else:
232 self._selected_tiles.append(imgNum)
233 if self._select_handler != None:
234 imgLabel = self._labels[imgNum] if type(self._labels) == list else str(imgNum)
235 self._select_handler(img, imgNum, {"imgLabel":imgLabel})
236 return 0
237
239 """
240 internal method to compute the list of points that represents
241 the appropriate decrement arrow (leftwards or upwards) depending
242 on the image montage layout.
243 """
244 if self._by_row:
245
246 x1 = self._size[0] / 2
247 y1 = 2
248 halfpad = self._ypad / 2
249 self._decrArrow = [(x1, y1), (x1 + halfpad, self._ypad - 2), (x1 - halfpad, self._ypad - 2)]
250 else:
251
252 x1 = 2
253 y1 = self._size[1] / 2
254 halfpad = self._xpad / 2
255 self._decrArrow = [(x1, y1), (x1 + self._xpad - 3, y1 - halfpad), (x1 + self._xpad - 3, y1 + halfpad)]
256
258 """
259 internal method to compute the list of points that represents
260 the appropriate increment arrow (rightwards or downwards) depending
261 on the image montage layout.
262 """
263 if self._by_row:
264
265 x1 = self._size[0] / 2
266 y1 = self._size[1] - 3
267 halfpad = self._ypad / 2
268 self._incrArrow = [(x1, y1), (x1 + halfpad, y1 - self._ypad + 3), (x1 - halfpad, y1 - self._ypad + 3)]
269 else:
270
271 x1 = self._size[0] - 2
272 y1 = self._size[1] / 2
273 halfpad = self._xpad / 2
274 self._incrArrow = [(x1, y1), (x1 - self._xpad + 2, y1 - halfpad), (x1 - self._xpad + 2, y1 + halfpad)]
275
277 """
278 internal method used by _onClick to compute the new imgPtr location after a decrement
279 """
280 tmp_ptr = self._imgPtr
281 if self._by_row:
282 tmp_ptr -= self._cols
283 else:
284 tmp_ptr -= self._rows
285 if tmp_ptr < 0:
286 self._imgPtr = 0
287 else:
288 self._imgPtr = tmp_ptr
289
291 """
292 internal method used by _onClick to compute the new imgPtr location after an increment
293 """
294 tmp_ptr = self._imgPtr
295 if self._by_row:
296 tmp_ptr += self._cols
297 else:
298 tmp_ptr += self._rows
299
300 self._imgPtr = tmp_ptr
301
302
304 """
305 Internal method to composite the thumbnail of a given image into the
306 correct position, given by (row,col).
307 @param img: The image from which a thumbnail will be composited onto the montage
308 @param pos: A tuple (row,col) for the position in the montage layout
309 @param imgNum: The image index of the tile being drawn, this helps us display the
310 appropriate label in the lower left corner if self._labels is not None.
311 """
312 (row, col) = pos
313
314 if self._keep_aspect:
315
316 w, h = img.size
317
318
319 scale = min(1.0 * self._tileSize[0] / w, 1.0 * self._tileSize[1] / h)
320 w = int(scale * w)
321 h = int(scale * h)
322
323
324 img2 = img.resize((w, h)).asPIL()
325
326
327 x = (self._tileSize[0] - w) / 2
328 y = (self._tileSize[1] - h) / 2
329 pil = PIL.Image.new('RGB', self._tileSize, "#000000")
330 pil.paste(img2, (x, y, x + w, y + h))
331
332
333 tile = pv.Image(pil)
334 else:
335 tile = img.resize(self._tileSize)
336
337 pos_x = col * (self._tileSize[0] + self._gutter) + self._gutter + self._xpad
338 pos_y = row * (self._tileSize[1] + self._gutter) + self._gutter + self._ypad
339
340 cvImg = self._cvMontageImage
341 cvTile = tile.asOpenCV()
342 cv.SetImageROI(cvImg, (pos_x, pos_y, self._tileSize[0], self._tileSize[1]))
343
344
345 self._image_positions.append(
346 [self._images[imgNum], imgNum, pv.Rect(pos_x, pos_y, self._tileSize[0], self._tileSize[1])])
347
348 depth = cvTile.nChannels
349 if depth == 1:
350 cvTileBGR = cv.CreateImage(self._tileSize, cv.IPL_DEPTH_8U, 3)
351 cv.CvtColor(cvTile, cvTileBGR, cv.CV_GRAY2BGR)
352 cv.Copy(cvTileBGR, cvImg)
353 else:
354 cv.Copy(cvTile, cvImg)
355
356 if self._labels == 'index':
357
358 lbltext = "%d" % imgNum
359 elif type(self._labels) == list:
360 lbltext = str(self._labels[imgNum])
361 else:
362 lbltext = None
363
364 if not lbltext is None:
365 ((tw, th), _) = cv.GetTextSize(lbltext, self._txtfont)
366
367 if tw > 0 and th > 0:
368 cv.Rectangle(cvImg, (0, self._tileSize[1] - 1), (tw + 1, self._tileSize[1] - (th + 1) - self._gutter),
369 (0, 0, 0), thickness=cv.CV_FILLED)
370 font = self._txtfont
371 color = self._txtcolor
372 cv.PutText(cvImg, lbltext, (1, self._tileSize[1] - self._gutter - 2), font, color)
373
374 if self._highlighted and (imgNum in self._selected_tiles):
375
376 cv.Rectangle(cvImg, (0, 0), (self._tileSize[0], self._tileSize[1]), (0, 255, 255), thickness=4)
377
378
379 cv.SetImageROI(cvImg, (0, 0, self._size[0], self._size[1]))
380
381
383 """
384 A class for objects designed to handle click events on ImageMontage objects.
385 We separate this out from the ImageMontage object to address a memory leak
386 when using cv.SetMouseCallback(window, self._onClick, window), because we
387 don't want the image data associated with the click handler
388 """
389
391
392 self.IM = weakref.ref(IM_Object)
393
394 - def onClick(self, event, x, y, flags, window):
395 """
396 Handle the mouse click for an image montage object.
397 Increment or Decrement the set of images shown in the montage
398 if appropriate.
399 """
400 IM = self.IM()
401 if IM is None: return
402
403
404 if event == cv.CV_EVENT_LBUTTONDOWN:
405 rc = IM._checkClickRegion(x, y)
406 if rc == -1 and IM._imgPtr > 0:
407
408 IM._decr()
409 elif rc == 1 and IM._imgPtr < (len(IM._images) - (IM._rows * IM._cols)):
410 IM._incr()
411 else:
412 pass
413
414 IM.draw((x, y))
415 cv.ShowImage(window, IM._cvMontageImage)
416
417
419 """
420 Provides a visualization of several videos playing back in
421 a single window. This can be very handy, for example, to
422 show tracking results of multiple objects from a single video,
423 or for minimizing screen real-estate when showing multiple
424 video sources.
425
426 A video montage object is an iterator, so you "play" the
427 montage by iterating through all the frames, just as with
428 a standard video object.
429 """
430
431 - def __init__(self, videoDict, layout=(2, 4), tile_size=(64, 48)):
432 """
433 @param videoDict: A dictionary of videos to display in the montage. The keys are the video labels, and
434 the values are objects adhering to the pyvision video interface. (pv.Video, pv.VideoFromImages, etc.)
435 @param layout: A tuple of (rows,cols) to indicate the layout of the montage. Videos will be separated by
436 a one-pixel gutter. Videos will be drawn to the montage such that a row is filled up prior to moving
437 to the next. The videos are drawn to the montage in the sorted order of the video keys in the dictionary.
438 @param tile_size: The window size to display each video in the montage. If the video frame sizes are larger than
439 this size, it will be cropped. If you wish to resize, use the size option in the pv.Video class to have
440 the output size of the video resized appropriately.
441 """
442 if len(videoDict) < 1:
443 raise ValueError("You must provide at least one video in the videoDict variable.")
444
445 self.vids = videoDict
446 self.layout = layout
447 self.vidsize = tile_size
448 self.imgs = {}
449 self.stopped = []
450
452 """ Return an iterator for this video """
453 return VideoMontage(self.vids, layout=self.layout, tile_size=self.vidsize)
454
456 if len(self.stopped) == len(self.vids.keys()):
457 print "All Videos in the Video Montage Have Completed."
458 raise StopIteration
459
460
461
462
463 for key in self.vids.keys():
464 if key in self.stopped: continue
465 v = self.vids[key]
466 try:
467 tmp = v.next()
468 self.imgs[key] = tmp
469 except StopIteration:
470
471 self.stopped.append(key)
472
473 keys = sorted(self.imgs.keys())
474 imageList = []
475 for k in keys:
476 imageList.append(self.imgs[k])
477
478 im = ImageMontage(imageList, self.layout, self.vidsize, gutter=2, by_row=True, labels=keys)
479 return im.asImage()
480
481
483 import os
484
485 imageList = []
486 counter = 0
487
488
489 JPGDIR = os.path.join(pv.__path__[0], 'data', 'misc')
490 filenames = os.listdir(JPGDIR)
491 jpgs = [os.path.join(JPGDIR, f) for f in filenames if f.endswith(".jpg")]
492
493 for fn in jpgs:
494 print counter
495 if counter > 8: break
496 imageList.append(pv.Image(fn))
497 counter += 1
498
499 im = ImageMontage(imageList, (2, 3), tile_size=(128, 96), gutter=2, by_row=False)
500 im.show(window="Image Montage", delay=0)
501 cv.DestroyWindow('Image Montage')
502
503
505 import os
506
507 TOYCAR_VIDEO = os.path.join(pv.__path__[0], 'data', 'test', 'toy_car.m4v')
508 TAZ_VIDEO = os.path.join(pv.__path__[0], 'data', 'test', 'TazSample.m4v')
509
510 vid1 = pv.Video(TOYCAR_VIDEO)
511 vid2 = pv.Video(TAZ_VIDEO)
512
513
514 vid_dict = {"V1": vid1, "V2": vid2}
515 vm = VideoMontage(vid_dict, layout=(2, 1), tile_size=(256, 192))
516 vm.play("Video Montage", delay=60, pos=(10, 10))
517
518
519
520
521
522
523
524
525