debug.py 38 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280
  1. """Augmenters that help with debugging.
  2. List of augmenters:
  3. * :class:`SaveDebugImageEveryNBatches`
  4. Added in 0.4.0.
  5. """
  6. from __future__ import print_function, division, absolute_import
  7. from abc import ABCMeta, abstractmethod, abstractproperty
  8. import os
  9. import collections
  10. import six
  11. import numpy as np
  12. import imageio
  13. import imgaug as ia
  14. from .. import dtypes as iadt
  15. from . import meta
  16. from . import size as sizelib
  17. from . import blend as blendlib
  18. _COLOR_PINK = (255, 192, 203)
  19. _COLOR_GRID_BACKGROUND = _COLOR_PINK
  20. def _resizepad_to_size(image, size, cval):
  21. """Resize and pad and image to given size.
  22. This first resizes until one image size matches one size in `size` (while
  23. retaining the aspect ratio).
  24. Then it pads the other side until both sides match `size`.
  25. Added in 0.4.0.
  26. """
  27. # resize to height H and width W while keeping aspect ratio
  28. height = size[0]
  29. width = size[1]
  30. height_im = image.shape[0]
  31. width_im = image.shape[1]
  32. aspect_ratio_im = width_im / height_im
  33. # we know that height_im <= height and width_im <= width
  34. height_diff = height - height_im
  35. width_diff = width - width_im
  36. if height_diff < width_diff:
  37. height_im_rs = height
  38. width_im_rs = height * aspect_ratio_im
  39. else:
  40. height_im_rs = width / aspect_ratio_im
  41. width_im_rs = width
  42. height_im_rs = max(int(np.round(height_im_rs)), 1)
  43. width_im_rs = max(int(np.round(width_im_rs)), 1)
  44. image_rs = ia.imresize_single_image(image, (height_im_rs, width_im_rs))
  45. # pad to remaining size
  46. pad_y = height - height_im_rs
  47. pad_x = width - width_im_rs
  48. pad_top = int(np.floor(pad_y / 2))
  49. pad_right = int(np.ceil(pad_x / 2))
  50. pad_bottom = int(np.ceil(pad_y / 2))
  51. pad_left = int(np.floor(pad_x / 2))
  52. image_rs_pad = sizelib.pad(image_rs,
  53. top=pad_top, right=pad_right,
  54. bottom=pad_bottom, left=pad_left,
  55. cval=cval)
  56. paddings = (pad_top, pad_right, pad_bottom, pad_left)
  57. return image_rs_pad, (height_im_rs, width_im_rs), paddings
  58. # TODO rename to Grid
  59. @six.add_metaclass(ABCMeta)
  60. class _IDebugGridCell(object):
  61. """A single cell within a debug image's grid.
  62. Usually corresponds to one image, but can also be e.g. a title/description.
  63. Added in 0.4.0.
  64. """
  65. @abstractproperty
  66. def min_width(self):
  67. """Minimum width in pixels that the cell requires.
  68. Added in 0.4.0.
  69. """
  70. @abstractproperty
  71. def min_height(self):
  72. """Minimum height in pixels that the cell requires.
  73. Added in 0.4.0.
  74. """
  75. @abstractmethod
  76. def draw(self, height, width):
  77. """Draw the debug image grid cell's content.
  78. Added in 0.4.0.
  79. Parameters
  80. ----------
  81. height : int
  82. Expected height of the drawn cell image/array.
  83. width : int
  84. Expected width of the drawn cell image/array.
  85. Returns
  86. -------
  87. ndarray
  88. ``(H,W,3)`` Image.
  89. """
  90. class _DebugGridBorderCell(_IDebugGridCell):
  91. """Helper to add a border around a cell within the debug image grid.
  92. Added in 0.4.0.
  93. """
  94. # Added in 0.4.0.
  95. def __init__(self, size, color, child):
  96. self.size = size
  97. self.color = color
  98. self.child = child
  99. # Added in 0.4.0.
  100. @property
  101. def min_height(self):
  102. return self.child.min_height
  103. # Added in 0.4.0.
  104. @property
  105. def min_width(self):
  106. return self.child.min_width
  107. # Added in 0.4.0.
  108. def draw(self, height, width):
  109. content = self.child.draw(height, width)
  110. content = sizelib.pad(content,
  111. top=self.size, right=self.size,
  112. bottom=self.size, left=self.size,
  113. mode="constant", cval=self.color)
  114. return content
  115. class _DebugGridTextCell(_IDebugGridCell):
  116. """Cell containing text.
  117. Added in 0.4.0.
  118. """
  119. # Added in 0.4.0.
  120. def __init__(self, text):
  121. self.text = text
  122. # Added in 0.4.0.
  123. @property
  124. def min_height(self):
  125. return max(20, len(self.text.split("\n")) * 17)
  126. # Added in 0.4.0.
  127. @property
  128. def min_width(self):
  129. lines = self.text.split("\n")
  130. if len(lines) == 0:
  131. return 20
  132. return max(20, int(7 * max([len(line) for line in lines])))
  133. # Added in 0.4.0.
  134. def draw(self, height, width):
  135. image = np.full((height, width, 3), 255, dtype=np.uint8)
  136. image = ia.draw_text(image, 0, 0, self.text, color=(0, 0, 0),
  137. size=12)
  138. return image
  139. class _DebugGridImageCell(_IDebugGridCell):
  140. """Cell containing an image, possibly with an different-shaped overlay.
  141. Added in 0.4.0.
  142. """
  143. # Added in 0.4.0.
  144. def __init__(self, image, overlay=None, overlay_alpha=0.75):
  145. self.image = image
  146. self.overlay = overlay
  147. self.overlay_alpha = overlay_alpha
  148. # Added in 0.4.0.
  149. @property
  150. def min_height(self):
  151. return self.image.shape[0]
  152. # Added in 0.4.0.
  153. @property
  154. def min_width(self):
  155. return self.image.shape[1]
  156. # Added in 0.4.0.
  157. def draw(self, height, width):
  158. image = self.image
  159. kind = image.dtype.kind
  160. if kind == "b":
  161. image = image.astype(np.uint8) * 255
  162. elif kind == "u":
  163. min_value, _, max_value = iadt.get_value_range_of_dtype(image.dtype)
  164. image = image.astype(np.float64) / max_value
  165. elif kind == "i":
  166. min_value, _, max_value = iadt.get_value_range_of_dtype(image.dtype)
  167. dynamic_range = (max_value - min_value)
  168. image = (min_value + image.astype(np.float64)) / dynamic_range
  169. if image.dtype.kind == "f":
  170. image = (np.clip(image, 0, 1.0) * 255).astype(np.uint8)
  171. image_rsp, size_rs, paddings = _resizepad_to_size(
  172. image, (height, width), cval=_COLOR_GRID_BACKGROUND)
  173. blend = image_rsp
  174. if self.overlay is not None:
  175. overlay_rs = self._resize_overlay(self.overlay,
  176. image.shape[0:2])
  177. overlay_rsp = self._resize_overlay(overlay_rs, size_rs)
  178. overlay_rsp = sizelib.pad(overlay_rsp,
  179. top=paddings[0], right=paddings[1],
  180. bottom=paddings[2], left=paddings[3],
  181. cval=_COLOR_GRID_BACKGROUND)
  182. blend = blendlib.blend_alpha(overlay_rsp, image_rsp,
  183. alpha=self.overlay_alpha)
  184. return blend
  185. # Added in 0.4.0.
  186. @classmethod
  187. def _resize_overlay(cls, arr, size):
  188. arr_rs = ia.imresize_single_image(arr, size, interpolation="nearest")
  189. return arr_rs
  190. class _DebugGridCBAsOICell(_IDebugGridCell):
  191. """Cell visualizing a coordinate-based augmentable.
  192. CBAsOI = coordinate-based augmentables on images,
  193. e.g. ``KeypointsOnImage``.
  194. Added in 0.4.0.
  195. """
  196. # Added in 0.4.0.
  197. def __init__(self, cbasoi, image):
  198. self.cbasoi = cbasoi
  199. self.image = image
  200. # Added in 0.4.0.
  201. @property
  202. def min_height(self):
  203. return self.image.shape[0]
  204. # Added in 0.4.0.
  205. @property
  206. def min_width(self):
  207. return self.image.shape[1]
  208. # Added in 0.4.0.
  209. def draw(self, height, width):
  210. image_rsp, size_rs, paddings = _resizepad_to_size(
  211. self.image, (height, width), cval=_COLOR_GRID_BACKGROUND)
  212. cbasoi = self.cbasoi.deepcopy()
  213. cbasoi = cbasoi.on_(size_rs)
  214. cbasoi = cbasoi.shift_(y=paddings[0], x=paddings[3])
  215. cbasoi.shape = image_rsp.shape
  216. return cbasoi.draw_on_image(image_rsp)
  217. class _DebugGridColumn(object):
  218. """A single column within the debug image grid.
  219. Added in 0.4.0.
  220. """
  221. def __init__(self, cells):
  222. self.cells = cells
  223. @property
  224. def nb_rows(self):
  225. """Number of rows in the column, i.e. examples in batch.
  226. Added in 0.4.0.
  227. """
  228. return len(self.cells)
  229. @property
  230. def max_cell_width(self):
  231. """Width in pixels of the widest cell in the column.
  232. Added in 0.4.0.
  233. """
  234. return max([cell.min_width for cell in self.cells])
  235. @property
  236. def max_cell_height(self):
  237. """Height in pixels of the tallest cell in the column.
  238. Added in 0.4.0.
  239. """
  240. return max([cell.min_height for cell in self.cells])
  241. def draw(self, heights):
  242. """Convert this column to an image array.
  243. Added in 0.4.0.
  244. """
  245. width = self.max_cell_width
  246. return np.vstack([cell.draw(height=height, width=width)
  247. for cell, height
  248. in zip(self.cells, heights)])
  249. class _DebugGrid(object):
  250. """A debug image grid.
  251. Columns correspond to the input datatypes (e.g. images, bounding boxes).
  252. Rows correspond to the examples within a batch.
  253. Added in 0.4.0.
  254. """
  255. # Added in 0.4.0.
  256. def __init__(self, columns):
  257. assert len(columns) > 0
  258. self.columns = columns
  259. def draw(self):
  260. """Convert this grid to an image array.
  261. Added in 0.4.0.
  262. """
  263. nb_rows_by_col = [column.nb_rows for column in self.columns]
  264. assert len(set(nb_rows_by_col)) == 1
  265. rowwise_heights = np.zeros((self.columns[0].nb_rows,), dtype=np.int32)
  266. for column in self.columns:
  267. heights = [cell.min_height for cell in column.cells]
  268. rowwise_heights = np.maximum(rowwise_heights, heights)
  269. return np.hstack([column.draw(heights=rowwise_heights)
  270. for column in self.columns])
  271. # TODO image subtitles
  272. # TODO run start date
  273. # TODO main process id, process id
  274. # TODO warning if map aspect ratio is different from image aspect ratio
  275. # TODO error if non-image shapes differ from image shapes
  276. def draw_debug_image(images, heatmaps=None, segmentation_maps=None,
  277. keypoints=None, bounding_boxes=None, polygons=None,
  278. line_strings=None):
  279. """Generate a debug image grid of a single batch and various datatypes.
  280. Added in 0.4.0.
  281. **Supported dtypes**:
  282. * ``uint8``: yes; tested
  283. * ``uint16``: ?
  284. * ``uint32``: ?
  285. * ``uint64``: ?
  286. * ``int8``: ?
  287. * ``int16``: ?
  288. * ``int32``: ?
  289. * ``int64``: ?
  290. * ``float16``: ?
  291. * ``float32``: ?
  292. * ``float64``: ?
  293. * ``float128``: ?
  294. * ``bool``: ?
  295. Parameters
  296. ----------
  297. images : ndarray or list of ndarray
  298. Images in the batch. Must always be provided. Batches without images
  299. cannot be visualized.
  300. heatmaps : None or list of imgaug.augmentables.heatmaps.HeatmapsOnImage, optional
  301. Heatmaps on the provided images.
  302. segmentation_maps : None or list of imgaug.augmentables.segmaps.SegmentationMapsOnImage, optional
  303. Segmentation maps on the provided images.
  304. keypoints : None or list of imgaug.augmentables.kps.KeypointsOnImage, optional
  305. Keypoints on the provided images.
  306. bounding_boxes : None or list of imgaug.augmentables.bbs.BoundingBoxesOnImage, optional
  307. Bounding boxes on the provided images.
  308. polygons : None or list of imgaug.augmentables.polys.PolygonsOnImage, optional
  309. Polygons on the provided images.
  310. line_strings : None or list of imgaug.augmentables.lines.LineStringsOnImage, optional
  311. Line strings on the provided images.
  312. Returns
  313. -------
  314. ndarray
  315. Visualized batch as RGB image.
  316. Examples
  317. --------
  318. >>> import numpy as np
  319. >>> import imgaug.augmenters as iaa
  320. >>> image = np.zeros((64, 64, 3), dtype=np.uint8)
  321. >>> debug_image = iaa.draw_debug_image(images=[image, image])
  322. Generate a debug image for two empty images.
  323. >>> from imgaug.augmentables.kps import KeypointsOnImage
  324. >>> kpsoi = KeypointsOnImage.from_xy_array([(10.5, 20.5), (30.5, 30.5)],
  325. >>> shape=image.shape)
  326. >>> debug_image = iaa.draw_debug_image(images=[image, image],
  327. >>> keypoints=[kpsoi, kpsoi])
  328. Generate a debug image for two empty images, each having two keypoints
  329. drawn on them.
  330. >>> from imgaug.augmentables.batches import UnnormalizedBatch
  331. >>> segmap_arr = np.zeros((32, 32, 1), dtype=np.int32)
  332. >>> kp_tuples = [(10.5, 20.5), (30.5, 30.5)]
  333. >>> batch = UnnormalizedBatch(images=[image, image],
  334. >>> segmentation_maps=[segmap_arr, segmap_arr],
  335. >>> keypoints=[kp_tuples, kp_tuples])
  336. >>> batch = batch.to_normalized_batch()
  337. >>> debug_image = iaa.draw_debug_image(
  338. >>> images=batch.images_unaug,
  339. >>> segmentation_maps=batch.segmentation_maps_unaug,
  340. >>> keypoints=batch.keypoints_unaug)
  341. Generate a debug image for two empty images, each having an empty
  342. segmentation map and two keypoints drawn on them. This example uses
  343. ``UnnormalizedBatch`` to show how to mostly evade going through imgaug
  344. classes.
  345. """
  346. columns = [_create_images_column(images)]
  347. if heatmaps is not None:
  348. columns.extend(_create_heatmaps_columns(heatmaps, images))
  349. if segmentation_maps is not None:
  350. columns.extend(_create_segmap_columns(segmentation_maps, images))
  351. if keypoints is not None:
  352. columns.append(_create_cbasois_column(keypoints, images, "Keypoints"))
  353. if bounding_boxes is not None:
  354. columns.append(_create_cbasois_column(bounding_boxes, images,
  355. "Bounding Boxes"))
  356. if polygons is not None:
  357. columns.append(_create_cbasois_column(polygons, images, "Polygons"))
  358. if line_strings is not None:
  359. columns.append(_create_cbasois_column(line_strings, images,
  360. "Line Strings"))
  361. result = _DebugGrid(columns)
  362. result = result.draw()
  363. result = sizelib.pad(result, top=1, right=1, bottom=1, left=1,
  364. mode="constant", cval=_COLOR_GRID_BACKGROUND)
  365. return result
  366. # Added in 0.4.0.
  367. def _add_borders(cells):
  368. """Add a border (cell) around a cell."""
  369. return [_DebugGridBorderCell(1, _COLOR_GRID_BACKGROUND, cell)
  370. for cell in cells]
  371. # Added in 0.4.0.
  372. def _add_text_cell(title, cells):
  373. """Add a text cell before other cells."""
  374. return [_DebugGridTextCell(title)] + cells
  375. # Added in 0.4.0.
  376. def _create_images_column(images):
  377. """Create columns for image data."""
  378. cells = [_DebugGridImageCell(image) for image in images]
  379. images_descr = _generate_images_description(images)
  380. column = _DebugGridColumn(
  381. _add_borders(
  382. _add_text_cell(
  383. "Images",
  384. _add_text_cell(
  385. images_descr,
  386. cells)
  387. )
  388. )
  389. )
  390. return column
  391. # Added in 0.4.0.
  392. def _create_heatmaps_columns(heatmaps, images):
  393. """Create columns for heatmap data."""
  394. nb_map_channels = max([heatmap.arr_0to1.shape[2]
  395. for heatmap in heatmaps])
  396. columns = [[] for _ in np.arange(nb_map_channels)]
  397. for image, heatmap in zip(images, heatmaps):
  398. heatmap_drawn = heatmap.draw()
  399. for c, heatmap_drawn_c in enumerate(heatmap_drawn):
  400. columns[c].append(
  401. _DebugGridImageCell(image, overlay=heatmap_drawn_c))
  402. columns = [
  403. _DebugGridColumn(
  404. _add_borders(
  405. _add_text_cell(
  406. "Heatmaps",
  407. _add_text_cell(
  408. _generate_heatmaps_description(
  409. heatmaps,
  410. channel_idx=c,
  411. show_details=(c == 0)),
  412. cells)
  413. )
  414. )
  415. )
  416. for c, cells in enumerate(columns)
  417. ]
  418. return columns
  419. # Added in 0.4.0.
  420. def _create_segmap_columns(segmentation_maps, images):
  421. """Create columns for segmentation map data."""
  422. nb_map_channels = max([segmap.arr.shape[2]
  423. for segmap in segmentation_maps])
  424. columns = [[] for _ in np.arange(nb_map_channels)]
  425. for image, segmap in zip(images, segmentation_maps):
  426. # TODO this currently draws the background in black, hence the
  427. # resulting blended image is dark at class id 0
  428. segmap_drawn = segmap.draw()
  429. for c, segmap_drawn_c in enumerate(segmap_drawn):
  430. columns[c].append(
  431. _DebugGridImageCell(image, overlay=segmap_drawn_c))
  432. columns = [
  433. _DebugGridColumn(
  434. _add_borders(
  435. _add_text_cell(
  436. "SegMaps",
  437. _add_text_cell(
  438. _generate_segmaps_description(
  439. segmentation_maps,
  440. channel_idx=c,
  441. show_details=(c == 0)),
  442. cells
  443. )
  444. )
  445. )
  446. )
  447. for c, cells in enumerate(columns)
  448. ]
  449. return columns
  450. # Added in 0.4.0.
  451. def _create_cbasois_column(cbasois, images, column_name):
  452. """Create a column for coordinate-based augmentables."""
  453. cells = [_DebugGridCBAsOICell(cbasoi, image)
  454. for cbasoi, image
  455. in zip(cbasois, images)]
  456. descr = _generate_cbasois_description(cbasois, images)
  457. column = _DebugGridColumn(
  458. _add_borders(
  459. _add_text_cell(
  460. column_name,
  461. _add_text_cell(descr, cells)
  462. )
  463. )
  464. )
  465. return column
  466. # Added in 0.4.0.
  467. def _generate_images_description(images):
  468. """Generate description for image columns."""
  469. if ia.is_np_array(images):
  470. shapes_str = "array, shape %11s" % (str(images.shape),)
  471. dtypes_str = "dtype %8s" % (images.dtype.name,)
  472. if len(images) == 0:
  473. value_range_str = ""
  474. elif images.dtype.kind in ["u", "i", "b"]:
  475. value_range_str = "value range: %3d to %3d" % (
  476. np.min(images), np.max(images))
  477. else:
  478. value_range_str = "value range: %7.4f to %7.4f" % (
  479. np.min(images), np.max(images))
  480. else:
  481. stats = _ListOfArraysStats(images)
  482. if stats.empty:
  483. shapes_str = ""
  484. elif stats.all_same_shape:
  485. shapes_str = (
  486. "list of %3d arrays\n"
  487. "all shape %11s"
  488. ) % (len(images), stats.shapes[0],)
  489. else:
  490. shapes_str = (
  491. "list of %3d arrays\n"
  492. "varying shapes\n"
  493. "smallest image: %11s\n"
  494. "largest image: %11s\n"
  495. "height: %3d to %3d\n"
  496. "width: %3d to %3d\n"
  497. "channels: %1s to %1s"
  498. ) % (len(images),
  499. stats.smallest_shape, stats.largest_shape,
  500. stats.height_min, stats.height_max,
  501. stats.width_min, stats.width_max,
  502. stats.get_channels_min("None"),
  503. stats.get_channels_max("None"))
  504. if stats.empty:
  505. dtypes_str = ""
  506. elif stats.all_same_dtype:
  507. dtypes_str = "all dtype %8s" % (stats.dtypes[0],)
  508. else:
  509. dtypes_str = "dtypes: %s" % (", ".join(stats.unique_dtype_names),)
  510. if stats.empty:
  511. value_range_str = ""
  512. else:
  513. value_range_str = "value range: %3d to %3d"
  514. if not stats.all_dtypes_intlike:
  515. value_range_str = "value range: %6.4f to %6.4f"
  516. value_range_str = value_range_str % (stats.value_min,
  517. stats.value_max)
  518. strs = [shapes_str, dtypes_str, value_range_str]
  519. return _join_description_strs(strs)
  520. # Added in 0.4.0.
  521. def _generate_segmaps_description(segmaps, channel_idx, show_details):
  522. """Generate description for segmap columns."""
  523. if len(segmaps) == 0:
  524. return "empty list"
  525. strs = _generate_sm_hm_description(segmaps, channel_idx, show_details)
  526. arrs_channel = [segmap.arr[:, :, channel_idx] for segmap in segmaps]
  527. stats_channel = _ListOfArraysStats(arrs_channel)
  528. value_range_str = (
  529. "value range: %3d to %3d\n"
  530. "number of unique classes: %2d"
  531. ) % (stats_channel.value_min, stats_channel.value_max,
  532. stats_channel.nb_unique_values)
  533. return _join_description_strs(strs + [value_range_str])
  534. # Added in 0.4.0.
  535. def _generate_heatmaps_description(heatmaps, channel_idx, show_details):
  536. """Generate description for heatmap columns."""
  537. if len(heatmaps) == 0:
  538. return "empty list"
  539. strs = _generate_sm_hm_description(heatmaps, channel_idx, show_details)
  540. arrs_channel = [heatmap.arr_0to1[:, :, channel_idx] for heatmap in heatmaps]
  541. stats_channel = _ListOfArraysStats(arrs_channel)
  542. value_range_str = (
  543. "value range: %6.4f to %6.4f\n"
  544. " (internal, max is [0.0, 1.0])"
  545. ) % (stats_channel.value_min, stats_channel.value_max)
  546. return _join_description_strs(strs + [value_range_str])
  547. # Added in 0.4.0.
  548. def _generate_sm_hm_description(augmentables, channel_idx, show_details):
  549. """Generate description for SegMap/Heatmap columns."""
  550. if augmentables is None:
  551. return ""
  552. if len(augmentables) == 0:
  553. return "empty list"
  554. arrs = [augmentable.get_arr() for augmentable in augmentables]
  555. stats = _ListOfArraysStats(arrs)
  556. if stats.get_channels_max(-1) > -1:
  557. channel_str = "Channel %1d of %1d" % (channel_idx+1,
  558. stats.get_channels_max(-1))
  559. else:
  560. channel_str = ""
  561. if not show_details:
  562. shapes_str = ""
  563. elif stats.all_same_shape:
  564. shapes_str = (
  565. "items for %3d images\n"
  566. "all arrays of shape %11s"
  567. ) % (len(augmentables), stats.shapes[0],)
  568. else:
  569. shapes_str = (
  570. "items for %3d images\n"
  571. "varying array shapes\n"
  572. "smallest: %11s\n"
  573. "largest: %11s\n"
  574. "height: %3d to %3d\n"
  575. "width: %3d to %3d\n"
  576. "channels: %1s to %1s"
  577. ) % (len(augmentables),
  578. stats.smallest_shape, stats.largest_shape,
  579. stats.height_min, stats.height_max,
  580. stats.width_min, stats.width_max,
  581. stats.get_channels_min("None"),
  582. stats.get_channels_max("None"))
  583. if not show_details:
  584. on_shapes_str = ""
  585. else:
  586. on_shapes_str = _generate_on_image_shapes_descr(augmentables)
  587. return [channel_str, shapes_str, on_shapes_str]
  588. # Added in 0.4.0.
  589. def _generate_cbasois_description(cbasois, images):
  590. """Generate description for coordinate-based augmentable columns."""
  591. images_str = "items for %d images" % (len(cbasois),)
  592. nb_items_lst = [len(cbasoi.items) for cbasoi in cbasois]
  593. nb_items_lst = nb_items_lst if len(cbasois) > 0 else [-1]
  594. nb_items = sum(nb_items_lst)
  595. items_str = (
  596. "fewest items on image: %3d\n"
  597. "most items on image: %3d\n"
  598. "total items: %6d"
  599. ) % (min(nb_items_lst), max(nb_items_lst), nb_items)
  600. areas = [
  601. cba.area if hasattr(cba, "area") else -1
  602. for cbasoi in cbasois
  603. for cba in cbasoi.items]
  604. areas = areas if len(cbasois) > 0 else [-1]
  605. areas_str = (
  606. "smallest area: %7.4f\n"
  607. "largest area: %7.4f"
  608. ) % (min(areas), max(areas))
  609. labels = list(ia.flatten([item.label if hasattr(item, "label") else None
  610. for cbasoi in cbasois
  611. for item in cbasoi.items]))
  612. labels_ctr = collections.Counter(labels)
  613. labels_most_common = []
  614. for label, count in labels_ctr.most_common(10):
  615. labels_most_common.append("\n - %s (%3d, %6.2f%%)" % (
  616. label, count, count/nb_items * 100))
  617. labels_str = (
  618. "unique labels: %2d\n"
  619. "most common labels:"
  620. "%s"
  621. ) % (len(labels_ctr.keys()), "".join(labels_most_common))
  622. coords_ooi = []
  623. dists = []
  624. for cbasoi, image in zip(cbasois, images):
  625. h, w = image.shape[0:2]
  626. for cba in cbasoi.items:
  627. coords = cba.coords
  628. for coord in coords:
  629. x, y = coord
  630. dist = (x - w/2)**2 + (y - h/2) ** 2
  631. coords_ooi.append(not (0 <= x < w and 0 <= y < h))
  632. dists.append(((x, y), dist))
  633. # use x_ and y_ because otherwise we get a 'redefines x' error in pylint
  634. coords_extreme = [(x_, y_)
  635. for (x_, y_), _
  636. in sorted(dists, key=lambda t: t[1])]
  637. nb_ooi = sum(coords_ooi)
  638. ooi_str = (
  639. "coords out of image: %d (%6.2f%%)\n"
  640. "most extreme coord: (%5.1f, %5.1f)"
  641. # TODO "items anyhow out of image: %d (%.2f%%)\n"
  642. # TODO "items fully out of image: %d (%.2f%%)\n"
  643. ) % (nb_ooi, nb_ooi / len(coords_ooi) * 100,
  644. coords_extreme[-1][0], coords_extreme[-1][1])
  645. on_shapes_str = _generate_on_image_shapes_descr(cbasois)
  646. return _join_description_strs([images_str, items_str, areas_str,
  647. labels_str, ooi_str, on_shapes_str])
  648. # Added in 0.4.0.
  649. def _generate_on_image_shapes_descr(augmentables):
  650. """Generate text block for non-image data describing their image shapes."""
  651. on_shapes = [augmentable.shape for augmentable in augmentables]
  652. stats_imgs = _ListOfArraysStats([np.empty(on_shape)
  653. for on_shape in on_shapes])
  654. if stats_imgs.all_same_shape:
  655. on_shapes_str = "all on image shape %11s" % (stats_imgs.shapes[0],)
  656. else:
  657. on_shapes_str = (
  658. "on varying image shapes\n"
  659. "smallest image: %11s\n"
  660. "largest image: %11s"
  661. ) % (stats_imgs.smallest_shape, stats_imgs.largest_shape)
  662. return on_shapes_str
  663. # Added in 0.4.0.
  664. def _join_description_strs(strs):
  665. """Join lines to a single string while removing empty lines."""
  666. strs = [str_i for str_i in strs if len(str_i) > 0]
  667. return "\n".join(strs)
  668. class _ListOfArraysStats(object):
  669. """Class to derive aggregated values from a list of arrays.
  670. E.g. shape of the largest array, number of unique dtypes etc.
  671. Added in 0.4.0.
  672. """
  673. def __init__(self, arrays):
  674. self.arrays = arrays
  675. # Added in 0.4.0.
  676. @property
  677. def empty(self):
  678. return len(self.arrays) == 0
  679. # Added in 0.4.0.
  680. @property
  681. def areas(self):
  682. return [np.prod(arr.shape[0:2]) for arr in self.arrays]
  683. # Added in 0.4.0.
  684. @property
  685. def arrays_by_area(self):
  686. arrays_by_area = [
  687. arr for arr, _
  688. in sorted(zip(self.arrays, self.areas), key=lambda t: t[1])
  689. ]
  690. return arrays_by_area
  691. # Added in 0.4.0.
  692. @property
  693. def shapes(self):
  694. return [arr.shape for arr in self.arrays]
  695. # Added in 0.4.0.
  696. @property
  697. def all_same_shape(self):
  698. if self.empty:
  699. return True
  700. return len(set(self.shapes)) == 1
  701. # Added in 0.4.0.
  702. @property
  703. def smallest_shape(self):
  704. if self.empty:
  705. return tuple()
  706. return self.arrays_by_area[0].shape
  707. # Added in 0.4.0.
  708. @property
  709. def largest_shape(self):
  710. if self.empty:
  711. return tuple()
  712. return self.arrays_by_area[-1].shape
  713. # Added in 0.4.0.
  714. @property
  715. def area_max(self):
  716. if self.empty:
  717. return tuple()
  718. return np.prod(self.arrays_by_area[-1][0:2])
  719. # Added in 0.4.0.
  720. @property
  721. def heights(self):
  722. return [arr.shape[0] for arr in self.arrays]
  723. # Added in 0.4.0.
  724. @property
  725. def height_min(self):
  726. heights = self.heights
  727. return min(heights) if len(heights) > 0 else 0
  728. # Added in 0.4.0.
  729. @property
  730. def height_max(self):
  731. heights = self.heights
  732. return max(heights) if len(heights) > 0 else 0
  733. # Added in 0.4.0.
  734. @property
  735. def widths(self):
  736. return [arr.shape[1] for arr in self.arrays]
  737. # Added in 0.4.0.
  738. @property
  739. def width_min(self):
  740. widths = self.widths
  741. return min(widths) if len(widths) > 0 else 0
  742. # Added in 0.4.0.
  743. @property
  744. def width_max(self):
  745. widths = self.widths
  746. return max(widths) if len(widths) > 0 else 0
  747. # Added in 0.4.0.
  748. def get_channels_min(self, default):
  749. if self.empty:
  750. return -1
  751. if any([arr.ndim == 2 for arr in self.arrays]):
  752. return default
  753. return min([arr.shape[2] for arr in self.arrays if arr.ndim > 2])
  754. # Added in 0.4.0.
  755. def get_channels_max(self, default):
  756. if self.empty:
  757. return -1
  758. if not any([arr.ndim > 2 for arr in self.arrays]):
  759. return default
  760. return max([arr.shape[2] for arr in self.arrays if arr.ndim > 2])
  761. # Added in 0.4.0.
  762. @property
  763. def dtypes(self):
  764. return [arr.dtype for arr in self.arrays]
  765. # Added in 0.4.0.
  766. @property
  767. def dtype_names(self):
  768. return [dtype.name for dtype in self.dtypes]
  769. # Added in 0.4.0.
  770. @property
  771. def all_same_dtype(self):
  772. return len(set(self.dtype_names)) in [0, 1]
  773. # Added in 0.4.0.
  774. @property
  775. def all_dtypes_intlike(self):
  776. if self.empty:
  777. return True
  778. return all([arr.dtype.kind in ["u", "i", "b"] for arr in self.arrays])
  779. # Added in 0.4.0.
  780. @property
  781. def unique_dtype_names(self):
  782. return sorted(list({arr.dtype.name for arr in self.arrays}))
  783. # Added in 0.4.0.
  784. @property
  785. def value_min(self):
  786. return min([np.min(arr) for arr in self.arrays])
  787. # Added in 0.4.0.
  788. @property
  789. def value_max(self):
  790. return max([np.max(arr) for arr in self.arrays])
  791. # Added in 0.4.0.
  792. @property
  793. def nb_unique_values(self):
  794. values_uq = set()
  795. for arr in self.arrays:
  796. values_uq.update(np.unique(arr))
  797. return len(values_uq)
  798. # Added in 0.4.0.
  799. @six.add_metaclass(ABCMeta)
  800. class _IImageDestination(object):
  801. """A destination which receives images to save."""
  802. def on_batch(self, batch):
  803. """Signal to the destination that a new batch is processed.
  804. This is intended to be used by the destination e.g. to count batches.
  805. Added in 0.4.0.
  806. Parameters
  807. ----------
  808. batch : imgaug.augmentables.batches._BatchInAugmentation
  809. A batch to which the next ``receive()`` call may correspond.
  810. """
  811. def receive(self, image):
  812. """Receive and handle an image.
  813. Added in 0.4.0.
  814. Parameters
  815. ----------
  816. image : ndarray
  817. Image to be handled by the destination.
  818. """
  819. # Added in 0.4.0.
  820. class _MultiDestination(_IImageDestination):
  821. """A list of multiple destinations behaving like a single one."""
  822. # Added in 0.4.0.
  823. def __init__(self, destinations):
  824. self.destinations = destinations
  825. # Added in 0.4.0.
  826. def on_batch(self, batch):
  827. for destination in self.destinations:
  828. destination.on_batch(batch)
  829. # Added in 0.4.0.
  830. def receive(self, image):
  831. for destination in self.destinations:
  832. destination.receive(image)
  833. # Added in 0.4.0.
  834. class _FolderImageDestination(_IImageDestination):
  835. """A destination which saves images to a directory."""
  836. # Added in 0.4.0.
  837. def __init__(self, folder_path,
  838. filename_pattern="batch_{batch_id:06d}.png"):
  839. super(_FolderImageDestination, self).__init__()
  840. self.folder_path = folder_path
  841. self.filename_pattern = filename_pattern
  842. self._batch_id = -1
  843. self._filepath = None
  844. # Added in 0.4.0.
  845. def on_batch(self, batch):
  846. self._batch_id += 1
  847. self._filepath = os.path.join(
  848. self.folder_path,
  849. self.filename_pattern.format(batch_id=self._batch_id))
  850. # Added in 0.4.0.
  851. def receive(self, image):
  852. imageio.imwrite(self._filepath, image)
  853. # Added in 0.4.0.
  854. @six.add_metaclass(ABCMeta)
  855. class _IBatchwiseSchedule(object):
  856. """A schedule determining per batch whether a condition is met."""
  857. def on_batch(self, batch):
  858. """Determine for the given batch whether the condition is met.
  859. Added in 0.4.0.
  860. Parameters
  861. ----------
  862. batch : _BatchInAugmentation
  863. Batch for which to evaluate the condition.
  864. Returns
  865. -------
  866. bool
  867. Signal whether the condition is met.
  868. """
  869. # Added in 0.4.0.
  870. class _EveryNBatchesSchedule(_IBatchwiseSchedule):
  871. """A schedule that generates a signal at every ``N`` th batch.
  872. This schedule must be called for *every* batch in order to count them.
  873. Added in 0.4.0.
  874. """
  875. def __init__(self, interval):
  876. self.interval = interval
  877. self._batch_id = -1
  878. # Added in 0.4.0.
  879. def on_batch(self, batch):
  880. self._batch_id += 1
  881. signal = (self._batch_id % self.interval == 0)
  882. return signal
  883. class _SaveDebugImage(meta.Augmenter):
  884. """Augmenter saving debug images to a destination according to a schedule.
  885. Added in 0.4.0.
  886. Parameters
  887. ----------
  888. destination : _IImageDestination
  889. The destination receiving debug images.
  890. schedule : _IBatchwiseSchedule
  891. The schedule to use to determine for which batches an image is
  892. supposed to be generated.
  893. seed : None or int or imgaug.random.RNG or numpy.random.Generator or numpy.random.BitGenerator or numpy.random.SeedSequence or numpy.random.RandomState, optional
  894. See :func:`~imgaug.augmenters.meta.Augmenter.__init__`.
  895. name : None or str, optional
  896. See :func:`~imgaug.augmenters.meta.Augmenter.__init__`.
  897. random_state : None or int or imgaug.random.RNG or numpy.random.Generator or numpy.random.BitGenerator or numpy.random.SeedSequence or numpy.random.RandomState, optional
  898. Old name for parameter `seed`.
  899. Its usage will not yet cause a deprecation warning,
  900. but it is still recommended to use `seed` now.
  901. Outdated since 0.4.0.
  902. deterministic : bool, optional
  903. Deprecated since 0.4.0.
  904. See method ``to_deterministic()`` for an alternative and for
  905. details about what the "deterministic mode" actually does.
  906. """
  907. # Added in 0.4.0.
  908. def __init__(self, destination, schedule,
  909. seed=None, name=None,
  910. random_state="deprecated", deterministic="deprecated"):
  911. super(_SaveDebugImage, self).__init__(
  912. seed=seed, name=name,
  913. random_state=random_state, deterministic=deterministic)
  914. self.destination = destination
  915. self.schedule = schedule
  916. # Added in 0.4.0.
  917. def _augment_batch_(self, batch, random_state, parents, hooks):
  918. save = self.schedule.on_batch(batch)
  919. self.destination.on_batch(batch)
  920. if save:
  921. image = draw_debug_image(
  922. images=batch.images,
  923. heatmaps=batch.heatmaps,
  924. segmentation_maps=batch.segmentation_maps,
  925. keypoints=batch.keypoints,
  926. bounding_boxes=batch.bounding_boxes,
  927. polygons=batch.polygons,
  928. line_strings=batch.line_strings)
  929. self.destination.receive(image)
  930. return batch
  931. class SaveDebugImageEveryNBatches(_SaveDebugImage):
  932. """Visualize data in batches and save corresponding plots to a folder.
  933. Added in 0.4.0.
  934. **Supported dtypes**:
  935. See :func:`~imgaug.augmenters.debug.draw_debug_image`.
  936. Parameters
  937. ----------
  938. destination : str or _IImageDestination
  939. Path to a folder. The saved images will follow a filename pattern
  940. of ``batch_<batch_id>.png``. The latest image will additionally be
  941. saved to ``latest.png``.
  942. interval : int
  943. Interval in batches. If set to ``N``, every ``N`` th batch an
  944. image will be generated and saved, starting with the first observed
  945. batch.
  946. Note that the augmenter only counts batches that it sees. If it is
  947. executed conditionally or re-instantiated, it may not see all batches
  948. or the counter may be wrong in other ways.
  949. seed : None or int or imgaug.random.RNG or numpy.random.Generator or numpy.random.BitGenerator or numpy.random.SeedSequence or numpy.random.RandomState, optional
  950. See :func:`~imgaug.augmenters.meta.Augmenter.__init__`.
  951. name : None or str, optional
  952. See :func:`~imgaug.augmenters.meta.Augmenter.__init__`.
  953. random_state : None or int or imgaug.random.RNG or numpy.random.Generator or numpy.random.BitGenerator or numpy.random.SeedSequence or numpy.random.RandomState, optional
  954. Old name for parameter `seed`.
  955. Its usage will not yet cause a deprecation warning,
  956. but it is still recommended to use `seed` now.
  957. Outdated since 0.4.0.
  958. deterministic : bool, optional
  959. Deprecated since 0.4.0.
  960. See method ``to_deterministic()`` for an alternative and for
  961. details about what the "deterministic mode" actually does.
  962. Examples
  963. --------
  964. >>> import imgaug.augmenters as iaa
  965. >>> import tempfile
  966. >>> folder_path = tempfile.mkdtemp()
  967. >>> seq = iaa.Sequential([
  968. >>> iaa.Sequential([
  969. >>> iaa.Fliplr(0.5),
  970. >>> iaa.Crop(px=(0, 16))
  971. >>> ], random_order=True),
  972. >>> iaa.SaveDebugImageEveryNBatches(folder_path, 100)
  973. >>> ])
  974. """
  975. # Added in 0.4.0.
  976. def __init__(self, destination, interval,
  977. seed=None, name=None,
  978. random_state="deprecated", deterministic="deprecated"):
  979. schedule = _EveryNBatchesSchedule(interval)
  980. if not isinstance(destination, _IImageDestination):
  981. assert os.path.isdir(destination), (
  982. "Expected 'destination' to be a string path to an existing "
  983. "directory. Got path '%s'." % (destination,))
  984. destination = _MultiDestination([
  985. _FolderImageDestination(destination),
  986. _FolderImageDestination(destination,
  987. filename_pattern="batch_latest.png")
  988. ])
  989. super(SaveDebugImageEveryNBatches, self).__init__(
  990. destination=destination, schedule=schedule,
  991. seed=seed, name=name,
  992. random_state=random_state, deterministic=deterministic)
  993. # Added in 0.4.0.
  994. def get_parameters(self):
  995. dests = self.destination.destinations
  996. return [
  997. dests[0].folder_path,
  998. dests[0].filename_pattern,
  999. dests[1].folder_path,
  1000. dests[1].filename_pattern,
  1001. self.schedule.interval
  1002. ]