"""Augmenters that help with debugging.
List of augmenters:
* :class:`SaveDebugImageEveryNBatches`
Added in 0.4.0.
"""
from __future__ import print_function, division, absolute_import
from abc import ABCMeta, abstractmethod, abstractproperty
import os
import collections
import six
import numpy as np
import imageio
import imgaug as ia
from .. import dtypes as iadt
from . import meta
from . import size as sizelib
from . import blend as blendlib
_COLOR_PINK = (255, 192, 203)
_COLOR_GRID_BACKGROUND = _COLOR_PINK
def _resizepad_to_size(image, size, cval):
"""Resize and pad and image to given size.
This first resizes until one image size matches one size in `size` (while
retaining the aspect ratio).
Then it pads the other side until both sides match `size`.
Added in 0.4.0.
"""
# resize to height H and width W while keeping aspect ratio
height = size[0]
width = size[1]
height_im = image.shape[0]
width_im = image.shape[1]
aspect_ratio_im = width_im / height_im
# we know that height_im <= height and width_im <= width
height_diff = height - height_im
width_diff = width - width_im
if height_diff < width_diff:
height_im_rs = height
width_im_rs = height * aspect_ratio_im
else:
height_im_rs = width / aspect_ratio_im
width_im_rs = width
height_im_rs = max(int(np.round(height_im_rs)), 1)
width_im_rs = max(int(np.round(width_im_rs)), 1)
image_rs = ia.imresize_single_image(image, (height_im_rs, width_im_rs))
# pad to remaining size
pad_y = height - height_im_rs
pad_x = width - width_im_rs
pad_top = int(np.floor(pad_y / 2))
pad_right = int(np.ceil(pad_x / 2))
pad_bottom = int(np.ceil(pad_y / 2))
pad_left = int(np.floor(pad_x / 2))
image_rs_pad = sizelib.pad(image_rs,
top=pad_top, right=pad_right,
bottom=pad_bottom, left=pad_left,
cval=cval)
paddings = (pad_top, pad_right, pad_bottom, pad_left)
return image_rs_pad, (height_im_rs, width_im_rs), paddings
# TODO rename to Grid
@six.add_metaclass(ABCMeta)
class _IDebugGridCell(object):
"""A single cell within a debug image's grid.
Usually corresponds to one image, but can also be e.g. a title/description.
Added in 0.4.0.
"""
@abstractproperty
def min_width(self):
"""Minimum width in pixels that the cell requires.
Added in 0.4.0.
"""
@abstractproperty
def min_height(self):
"""Minimum height in pixels that the cell requires.
Added in 0.4.0.
"""
@abstractmethod
def draw(self, height, width):
"""Draw the debug image grid cell's content.
Added in 0.4.0.
Parameters
----------
height : int
Expected height of the drawn cell image/array.
width : int
Expected width of the drawn cell image/array.
Returns
-------
ndarray
``(H,W,3)`` Image.
"""
class _DebugGridBorderCell(_IDebugGridCell):
"""Helper to add a border around a cell within the debug image grid.
Added in 0.4.0.
"""
# Added in 0.4.0.
def __init__(self, size, color, child):
self.size = size
self.color = color
self.child = child
# Added in 0.4.0.
@property
def min_height(self):
return self.child.min_height
# Added in 0.4.0.
@property
def min_width(self):
return self.child.min_width
# Added in 0.4.0.
def draw(self, height, width):
content = self.child.draw(height, width)
content = sizelib.pad(content,
top=self.size, right=self.size,
bottom=self.size, left=self.size,
mode="constant", cval=self.color)
return content
class _DebugGridTextCell(_IDebugGridCell):
"""Cell containing text.
Added in 0.4.0.
"""
# Added in 0.4.0.
def __init__(self, text):
self.text = text
# Added in 0.4.0.
@property
def min_height(self):
return max(20, len(self.text.split("\n")) * 17)
# Added in 0.4.0.
@property
def min_width(self):
lines = self.text.split("\n")
if len(lines) == 0:
return 20
return max(20, int(7 * max([len(line) for line in lines])))
# Added in 0.4.0.
def draw(self, height, width):
image = np.full((height, width, 3), 255, dtype=np.uint8)
image = ia.draw_text(image, 0, 0, self.text, color=(0, 0, 0),
size=12)
return image
class _DebugGridImageCell(_IDebugGridCell):
"""Cell containing an image, possibly with an different-shaped overlay.
Added in 0.4.0.
"""
# Added in 0.4.0.
def __init__(self, image, overlay=None, overlay_alpha=0.75):
self.image = image
self.overlay = overlay
self.overlay_alpha = overlay_alpha
# Added in 0.4.0.
@property
def min_height(self):
return self.image.shape[0]
# Added in 0.4.0.
@property
def min_width(self):
return self.image.shape[1]
# Added in 0.4.0.
def draw(self, height, width):
image = self.image
kind = image.dtype.kind
if kind == "b":
image = image.astype(np.uint8) * 255
elif kind == "u":
min_value, _, max_value = iadt.get_value_range_of_dtype(image.dtype)
image = image.astype(np.float64) / max_value
elif kind == "i":
min_value, _, max_value = iadt.get_value_range_of_dtype(image.dtype)
dynamic_range = (max_value - min_value)
image = (min_value + image.astype(np.float64)) / dynamic_range
if image.dtype.kind == "f":
image = (np.clip(image, 0, 1.0) * 255).astype(np.uint8)
image_rsp, size_rs, paddings = _resizepad_to_size(
image, (height, width), cval=_COLOR_GRID_BACKGROUND)
blend = image_rsp
if self.overlay is not None:
overlay_rs = self._resize_overlay(self.overlay,
image.shape[0:2])
overlay_rsp = self._resize_overlay(overlay_rs, size_rs)
overlay_rsp = sizelib.pad(overlay_rsp,
top=paddings[0], right=paddings[1],
bottom=paddings[2], left=paddings[3],
cval=_COLOR_GRID_BACKGROUND)
blend = blendlib.blend_alpha(overlay_rsp, image_rsp,
alpha=self.overlay_alpha)
return blend
# Added in 0.4.0.
@classmethod
def _resize_overlay(cls, arr, size):
arr_rs = ia.imresize_single_image(arr, size, interpolation="nearest")
return arr_rs
class _DebugGridCBAsOICell(_IDebugGridCell):
"""Cell visualizing a coordinate-based augmentable.
CBAsOI = coordinate-based augmentables on images,
e.g. ``KeypointsOnImage``.
Added in 0.4.0.
"""
# Added in 0.4.0.
def __init__(self, cbasoi, image):
self.cbasoi = cbasoi
self.image = image
# Added in 0.4.0.
@property
def min_height(self):
return self.image.shape[0]
# Added in 0.4.0.
@property
def min_width(self):
return self.image.shape[1]
# Added in 0.4.0.
def draw(self, height, width):
image_rsp, size_rs, paddings = _resizepad_to_size(
self.image, (height, width), cval=_COLOR_GRID_BACKGROUND)
cbasoi = self.cbasoi.deepcopy()
cbasoi = cbasoi.on_(size_rs)
cbasoi = cbasoi.shift_(y=paddings[0], x=paddings[3])
cbasoi.shape = image_rsp.shape
return cbasoi.draw_on_image(image_rsp)
class _DebugGridColumn(object):
"""A single column within the debug image grid.
Added in 0.4.0.
"""
def __init__(self, cells):
self.cells = cells
@property
def nb_rows(self):
"""Number of rows in the column, i.e. examples in batch.
Added in 0.4.0.
"""
return len(self.cells)
@property
def max_cell_width(self):
"""Width in pixels of the widest cell in the column.
Added in 0.4.0.
"""
return max([cell.min_width for cell in self.cells])
@property
def max_cell_height(self):
"""Height in pixels of the tallest cell in the column.
Added in 0.4.0.
"""
return max([cell.min_height for cell in self.cells])
def draw(self, heights):
"""Convert this column to an image array.
Added in 0.4.0.
"""
width = self.max_cell_width
return np.vstack([cell.draw(height=height, width=width)
for cell, height
in zip(self.cells, heights)])
class _DebugGrid(object):
"""A debug image grid.
Columns correspond to the input datatypes (e.g. images, bounding boxes).
Rows correspond to the examples within a batch.
Added in 0.4.0.
"""
# Added in 0.4.0.
def __init__(self, columns):
assert len(columns) > 0
self.columns = columns
def draw(self):
"""Convert this grid to an image array.
Added in 0.4.0.
"""
nb_rows_by_col = [column.nb_rows for column in self.columns]
assert len(set(nb_rows_by_col)) == 1
rowwise_heights = np.zeros((self.columns[0].nb_rows,), dtype=np.int32)
for column in self.columns:
heights = [cell.min_height for cell in column.cells]
rowwise_heights = np.maximum(rowwise_heights, heights)
return np.hstack([column.draw(heights=rowwise_heights)
for column in self.columns])
# TODO image subtitles
# TODO run start date
# TODO main process id, process id
# TODO warning if map aspect ratio is different from image aspect ratio
# TODO error if non-image shapes differ from image shapes
[docs]def draw_debug_image(images, heatmaps=None, segmentation_maps=None,
keypoints=None, bounding_boxes=None, polygons=None,
line_strings=None):
"""Generate a debug image grid of a single batch and various datatypes.
Added in 0.4.0.
**Supported dtypes**:
* ``uint8``: yes; tested
* ``uint16``: ?
* ``uint32``: ?
* ``uint64``: ?
* ``int8``: ?
* ``int16``: ?
* ``int32``: ?
* ``int64``: ?
* ``float16``: ?
* ``float32``: ?
* ``float64``: ?
* ``float128``: ?
* ``bool``: ?
Parameters
----------
images : ndarray or list of ndarray
Images in the batch. Must always be provided. Batches without images
cannot be visualized.
heatmaps : None or list of imgaug.augmentables.heatmaps.HeatmapsOnImage, optional
Heatmaps on the provided images.
segmentation_maps : None or list of imgaug.augmentables.segmaps.SegmentationMapsOnImage, optional
Segmentation maps on the provided images.
keypoints : None or list of imgaug.augmentables.kps.KeypointsOnImage, optional
Keypoints on the provided images.
bounding_boxes : None or list of imgaug.augmentables.bbs.BoundingBoxesOnImage, optional
Bounding boxes on the provided images.
polygons : None or list of imgaug.augmentables.polys.PolygonsOnImage, optional
Polygons on the provided images.
line_strings : None or list of imgaug.augmentables.lines.LineStringsOnImage, optional
Line strings on the provided images.
Returns
-------
ndarray
Visualized batch as RGB image.
Examples
--------
>>> import numpy as np
>>> import imgaug.augmenters as iaa
>>> image = np.zeros((64, 64, 3), dtype=np.uint8)
>>> debug_image = iaa.draw_debug_image(images=[image, image])
Generate a debug image for two empty images.
>>> from imgaug.augmentables.kps import KeypointsOnImage
>>> kpsoi = KeypointsOnImage.from_xy_array([(10.5, 20.5), (30.5, 30.5)],
>>> shape=image.shape)
>>> debug_image = iaa.draw_debug_image(images=[image, image],
>>> keypoints=[kpsoi, kpsoi])
Generate a debug image for two empty images, each having two keypoints
drawn on them.
>>> from imgaug.augmentables.batches import UnnormalizedBatch
>>> segmap_arr = np.zeros((32, 32, 1), dtype=np.int32)
>>> kp_tuples = [(10.5, 20.5), (30.5, 30.5)]
>>> batch = UnnormalizedBatch(images=[image, image],
>>> segmentation_maps=[segmap_arr, segmap_arr],
>>> keypoints=[kp_tuples, kp_tuples])
>>> batch = batch.to_normalized_batch()
>>> debug_image = iaa.draw_debug_image(
>>> images=batch.images_unaug,
>>> segmentation_maps=batch.segmentation_maps_unaug,
>>> keypoints=batch.keypoints_unaug)
Generate a debug image for two empty images, each having an empty
segmentation map and two keypoints drawn on them. This example uses
``UnnormalizedBatch`` to show how to mostly evade going through imgaug
classes.
"""
columns = [_create_images_column(images)]
if heatmaps is not None:
columns.extend(_create_heatmaps_columns(heatmaps, images))
if segmentation_maps is not None:
columns.extend(_create_segmap_columns(segmentation_maps, images))
if keypoints is not None:
columns.append(_create_cbasois_column(keypoints, images, "Keypoints"))
if bounding_boxes is not None:
columns.append(_create_cbasois_column(bounding_boxes, images,
"Bounding Boxes"))
if polygons is not None:
columns.append(_create_cbasois_column(polygons, images, "Polygons"))
if line_strings is not None:
columns.append(_create_cbasois_column(line_strings, images,
"Line Strings"))
result = _DebugGrid(columns)
result = result.draw()
result = sizelib.pad(result, top=1, right=1, bottom=1, left=1,
mode="constant", cval=_COLOR_GRID_BACKGROUND)
return result
# Added in 0.4.0.
def _add_borders(cells):
"""Add a border (cell) around a cell."""
return [_DebugGridBorderCell(1, _COLOR_GRID_BACKGROUND, cell)
for cell in cells]
# Added in 0.4.0.
def _add_text_cell(title, cells):
"""Add a text cell before other cells."""
return [_DebugGridTextCell(title)] + cells
# Added in 0.4.0.
def _create_images_column(images):
"""Create columns for image data."""
cells = [_DebugGridImageCell(image) for image in images]
images_descr = _generate_images_description(images)
column = _DebugGridColumn(
_add_borders(
_add_text_cell(
"Images",
_add_text_cell(
images_descr,
cells)
)
)
)
return column
# Added in 0.4.0.
def _create_heatmaps_columns(heatmaps, images):
"""Create columns for heatmap data."""
nb_map_channels = max([heatmap.arr_0to1.shape[2]
for heatmap in heatmaps])
columns = [[] for _ in np.arange(nb_map_channels)]
for image, heatmap in zip(images, heatmaps):
heatmap_drawn = heatmap.draw()
for c, heatmap_drawn_c in enumerate(heatmap_drawn):
columns[c].append(
_DebugGridImageCell(image, overlay=heatmap_drawn_c))
columns = [
_DebugGridColumn(
_add_borders(
_add_text_cell(
"Heatmaps",
_add_text_cell(
_generate_heatmaps_description(
heatmaps,
channel_idx=c,
show_details=(c == 0)),
cells)
)
)
)
for c, cells in enumerate(columns)
]
return columns
# Added in 0.4.0.
def _create_segmap_columns(segmentation_maps, images):
"""Create columns for segmentation map data."""
nb_map_channels = max([segmap.arr.shape[2]
for segmap in segmentation_maps])
columns = [[] for _ in np.arange(nb_map_channels)]
for image, segmap in zip(images, segmentation_maps):
# TODO this currently draws the background in black, hence the
# resulting blended image is dark at class id 0
segmap_drawn = segmap.draw()
for c, segmap_drawn_c in enumerate(segmap_drawn):
columns[c].append(
_DebugGridImageCell(image, overlay=segmap_drawn_c))
columns = [
_DebugGridColumn(
_add_borders(
_add_text_cell(
"SegMaps",
_add_text_cell(
_generate_segmaps_description(
segmentation_maps,
channel_idx=c,
show_details=(c == 0)),
cells
)
)
)
)
for c, cells in enumerate(columns)
]
return columns
# Added in 0.4.0.
def _create_cbasois_column(cbasois, images, column_name):
"""Create a column for coordinate-based augmentables."""
cells = [_DebugGridCBAsOICell(cbasoi, image)
for cbasoi, image
in zip(cbasois, images)]
descr = _generate_cbasois_description(cbasois, images)
column = _DebugGridColumn(
_add_borders(
_add_text_cell(
column_name,
_add_text_cell(descr, cells)
)
)
)
return column
# Added in 0.4.0.
def _generate_images_description(images):
"""Generate description for image columns."""
if ia.is_np_array(images):
shapes_str = "array, shape %11s" % (str(images.shape),)
dtypes_str = "dtype %8s" % (images.dtype.name,)
if len(images) == 0:
value_range_str = ""
elif images.dtype.kind in ["u", "i", "b"]:
value_range_str = "value range: %3d to %3d" % (
np.min(images), np.max(images))
else:
value_range_str = "value range: %7.4f to %7.4f" % (
np.min(images), np.max(images))
else:
stats = _ListOfArraysStats(images)
if stats.empty:
shapes_str = ""
elif stats.all_same_shape:
shapes_str = (
"list of %3d arrays\n"
"all shape %11s"
) % (len(images), stats.shapes[0],)
else:
shapes_str = (
"list of %3d arrays\n"
"varying shapes\n"
"smallest image: %11s\n"
"largest image: %11s\n"
"height: %3d to %3d\n"
"width: %3d to %3d\n"
"channels: %1s to %1s"
) % (len(images),
stats.smallest_shape, stats.largest_shape,
stats.height_min, stats.height_max,
stats.width_min, stats.width_max,
stats.get_channels_min("None"),
stats.get_channels_max("None"))
if stats.empty:
dtypes_str = ""
elif stats.all_same_dtype:
dtypes_str = "all dtype %8s" % (stats.dtypes[0],)
else:
dtypes_str = "dtypes: %s" % (", ".join(stats.unique_dtype_names),)
if stats.empty:
value_range_str = ""
else:
value_range_str = "value range: %3d to %3d"
if not stats.all_dtypes_intlike:
value_range_str = "value range: %6.4f to %6.4f"
value_range_str = value_range_str % (stats.value_min,
stats.value_max)
strs = [shapes_str, dtypes_str, value_range_str]
return _join_description_strs(strs)
# Added in 0.4.0.
def _generate_segmaps_description(segmaps, channel_idx, show_details):
"""Generate description for segmap columns."""
if len(segmaps) == 0:
return "empty list"
strs = _generate_sm_hm_description(segmaps, channel_idx, show_details)
arrs_channel = [segmap.arr[:, :, channel_idx] for segmap in segmaps]
stats_channel = _ListOfArraysStats(arrs_channel)
value_range_str = (
"value range: %3d to %3d\n"
"number of unique classes: %2d"
) % (stats_channel.value_min, stats_channel.value_max,
stats_channel.nb_unique_values)
return _join_description_strs(strs + [value_range_str])
# Added in 0.4.0.
def _generate_heatmaps_description(heatmaps, channel_idx, show_details):
"""Generate description for heatmap columns."""
if len(heatmaps) == 0:
return "empty list"
strs = _generate_sm_hm_description(heatmaps, channel_idx, show_details)
arrs_channel = [heatmap.arr_0to1[:, :, channel_idx] for heatmap in heatmaps]
stats_channel = _ListOfArraysStats(arrs_channel)
value_range_str = (
"value range: %6.4f to %6.4f\n"
" (internal, max is [0.0, 1.0])"
) % (stats_channel.value_min, stats_channel.value_max)
return _join_description_strs(strs + [value_range_str])
# Added in 0.4.0.
def _generate_sm_hm_description(augmentables, channel_idx, show_details):
"""Generate description for SegMap/Heatmap columns."""
if augmentables is None:
return ""
if len(augmentables) == 0:
return "empty list"
arrs = [augmentable.get_arr() for augmentable in augmentables]
stats = _ListOfArraysStats(arrs)
if stats.get_channels_max(-1) > -1:
channel_str = "Channel %1d of %1d" % (channel_idx+1,
stats.get_channels_max(-1))
else:
channel_str = ""
if not show_details:
shapes_str = ""
elif stats.all_same_shape:
shapes_str = (
"items for %3d images\n"
"all arrays of shape %11s"
) % (len(augmentables), stats.shapes[0],)
else:
shapes_str = (
"items for %3d images\n"
"varying array shapes\n"
"smallest: %11s\n"
"largest: %11s\n"
"height: %3d to %3d\n"
"width: %3d to %3d\n"
"channels: %1s to %1s"
) % (len(augmentables),
stats.smallest_shape, stats.largest_shape,
stats.height_min, stats.height_max,
stats.width_min, stats.width_max,
stats.get_channels_min("None"),
stats.get_channels_max("None"))
if not show_details:
on_shapes_str = ""
else:
on_shapes_str = _generate_on_image_shapes_descr(augmentables)
return [channel_str, shapes_str, on_shapes_str]
# Added in 0.4.0.
def _generate_cbasois_description(cbasois, images):
"""Generate description for coordinate-based augmentable columns."""
images_str = "items for %d images" % (len(cbasois),)
nb_items_lst = [len(cbasoi.items) for cbasoi in cbasois]
nb_items_lst = nb_items_lst if len(cbasois) > 0 else [-1]
nb_items = sum(nb_items_lst)
items_str = (
"fewest items on image: %3d\n"
"most items on image: %3d\n"
"total items: %6d"
) % (min(nb_items_lst), max(nb_items_lst), nb_items)
areas = [
cba.area if hasattr(cba, "area") else -1
for cbasoi in cbasois
for cba in cbasoi.items]
areas = areas if len(cbasois) > 0 else [-1]
areas_str = (
"smallest area: %7.4f\n"
"largest area: %7.4f"
) % (min(areas), max(areas))
labels = list(ia.flatten([item.label if hasattr(item, "label") else None
for cbasoi in cbasois
for item in cbasoi.items]))
labels_ctr = collections.Counter(labels)
labels_most_common = []
for label, count in labels_ctr.most_common(10):
labels_most_common.append("\n - %s (%3d, %6.2f%%)" % (
label, count, count/nb_items * 100))
labels_str = (
"unique labels: %2d\n"
"most common labels:"
"%s"
) % (len(labels_ctr.keys()), "".join(labels_most_common))
coords_ooi = []
dists = []
for cbasoi, image in zip(cbasois, images):
h, w = image.shape[0:2]
for cba in cbasoi.items:
coords = cba.coords
for coord in coords:
x, y = coord
dist = (x - w/2)**2 + (y - h/2) ** 2
coords_ooi.append(not (0 <= x < w and 0 <= y < h))
dists.append(((x, y), dist))
# use x_ and y_ because otherwise we get a 'redefines x' error in pylint
coords_extreme = [(x_, y_)
for (x_, y_), _
in sorted(dists, key=lambda t: t[1])]
nb_ooi = sum(coords_ooi)
ooi_str = (
"coords out of image: %d (%6.2f%%)\n"
"most extreme coord: (%5.1f, %5.1f)"
# TODO "items anyhow out of image: %d (%.2f%%)\n"
# TODO "items fully out of image: %d (%.2f%%)\n"
) % (nb_ooi, nb_ooi / len(coords_ooi) * 100,
coords_extreme[-1][0], coords_extreme[-1][1])
on_shapes_str = _generate_on_image_shapes_descr(cbasois)
return _join_description_strs([images_str, items_str, areas_str,
labels_str, ooi_str, on_shapes_str])
# Added in 0.4.0.
def _generate_on_image_shapes_descr(augmentables):
"""Generate text block for non-image data describing their image shapes."""
on_shapes = [augmentable.shape for augmentable in augmentables]
stats_imgs = _ListOfArraysStats([np.empty(on_shape)
for on_shape in on_shapes])
if stats_imgs.all_same_shape:
on_shapes_str = "all on image shape %11s" % (stats_imgs.shapes[0],)
else:
on_shapes_str = (
"on varying image shapes\n"
"smallest image: %11s\n"
"largest image: %11s"
) % (stats_imgs.smallest_shape, stats_imgs.largest_shape)
return on_shapes_str
# Added in 0.4.0.
def _join_description_strs(strs):
"""Join lines to a single string while removing empty lines."""
strs = [str_i for str_i in strs if len(str_i) > 0]
return "\n".join(strs)
class _ListOfArraysStats(object):
"""Class to derive aggregated values from a list of arrays.
E.g. shape of the largest array, number of unique dtypes etc.
Added in 0.4.0.
"""
def __init__(self, arrays):
self.arrays = arrays
# Added in 0.4.0.
@property
def empty(self):
return len(self.arrays) == 0
# Added in 0.4.0.
@property
def areas(self):
return [np.prod(arr.shape[0:2]) for arr in self.arrays]
# Added in 0.4.0.
@property
def arrays_by_area(self):
arrays_by_area = [
arr for arr, _
in sorted(zip(self.arrays, self.areas), key=lambda t: t[1])
]
return arrays_by_area
# Added in 0.4.0.
@property
def shapes(self):
return [arr.shape for arr in self.arrays]
# Added in 0.4.0.
@property
def all_same_shape(self):
if self.empty:
return True
return len(set(self.shapes)) == 1
# Added in 0.4.0.
@property
def smallest_shape(self):
if self.empty:
return tuple()
return self.arrays_by_area[0].shape
# Added in 0.4.0.
@property
def largest_shape(self):
if self.empty:
return tuple()
return self.arrays_by_area[-1].shape
# Added in 0.4.0.
@property
def area_max(self):
if self.empty:
return tuple()
return np.prod(self.arrays_by_area[-1][0:2])
# Added in 0.4.0.
@property
def heights(self):
return [arr.shape[0] for arr in self.arrays]
# Added in 0.4.0.
@property
def height_min(self):
heights = self.heights
return min(heights) if len(heights) > 0 else 0
# Added in 0.4.0.
@property
def height_max(self):
heights = self.heights
return max(heights) if len(heights) > 0 else 0
# Added in 0.4.0.
@property
def widths(self):
return [arr.shape[1] for arr in self.arrays]
# Added in 0.4.0.
@property
def width_min(self):
widths = self.widths
return min(widths) if len(widths) > 0 else 0
# Added in 0.4.0.
@property
def width_max(self):
widths = self.widths
return max(widths) if len(widths) > 0 else 0
# Added in 0.4.0.
def get_channels_min(self, default):
if self.empty:
return -1
if any([arr.ndim == 2 for arr in self.arrays]):
return default
return min([arr.shape[2] for arr in self.arrays if arr.ndim > 2])
# Added in 0.4.0.
def get_channels_max(self, default):
if self.empty:
return -1
if not any([arr.ndim > 2 for arr in self.arrays]):
return default
return max([arr.shape[2] for arr in self.arrays if arr.ndim > 2])
# Added in 0.4.0.
@property
def dtypes(self):
return [arr.dtype for arr in self.arrays]
# Added in 0.4.0.
@property
def dtype_names(self):
return [dtype.name for dtype in self.dtypes]
# Added in 0.4.0.
@property
def all_same_dtype(self):
return len(set(self.dtype_names)) in [0, 1]
# Added in 0.4.0.
@property
def all_dtypes_intlike(self):
if self.empty:
return True
return all([arr.dtype.kind in ["u", "i", "b"] for arr in self.arrays])
# Added in 0.4.0.
@property
def unique_dtype_names(self):
return sorted(list({arr.dtype.name for arr in self.arrays}))
# Added in 0.4.0.
@property
def value_min(self):
return min([np.min(arr) for arr in self.arrays])
# Added in 0.4.0.
@property
def value_max(self):
return max([np.max(arr) for arr in self.arrays])
# Added in 0.4.0.
@property
def nb_unique_values(self):
values_uq = set()
for arr in self.arrays:
values_uq.update(np.unique(arr))
return len(values_uq)
# Added in 0.4.0.
@six.add_metaclass(ABCMeta)
class _IImageDestination(object):
"""A destination which receives images to save."""
def on_batch(self, batch):
"""Signal to the destination that a new batch is processed.
This is intended to be used by the destination e.g. to count batches.
Added in 0.4.0.
Parameters
----------
batch : imgaug.augmentables.batches._BatchInAugmentation
A batch to which the next ``receive()`` call may correspond.
"""
def receive(self, image):
"""Receive and handle an image.
Added in 0.4.0.
Parameters
----------
image : ndarray
Image to be handled by the destination.
"""
# Added in 0.4.0.
class _MultiDestination(_IImageDestination):
"""A list of multiple destinations behaving like a single one."""
# Added in 0.4.0.
def __init__(self, destinations):
self.destinations = destinations
# Added in 0.4.0.
def on_batch(self, batch):
for destination in self.destinations:
destination.on_batch(batch)
# Added in 0.4.0.
def receive(self, image):
for destination in self.destinations:
destination.receive(image)
# Added in 0.4.0.
class _FolderImageDestination(_IImageDestination):
"""A destination which saves images to a directory."""
# Added in 0.4.0.
def __init__(self, folder_path,
filename_pattern="batch_{batch_id:06d}.png"):
super(_FolderImageDestination, self).__init__()
self.folder_path = folder_path
self.filename_pattern = filename_pattern
self._batch_id = -1
self._filepath = None
# Added in 0.4.0.
def on_batch(self, batch):
self._batch_id += 1
self._filepath = os.path.join(
self.folder_path,
self.filename_pattern.format(batch_id=self._batch_id))
# Added in 0.4.0.
def receive(self, image):
imageio.imwrite(self._filepath, image)
# Added in 0.4.0.
@six.add_metaclass(ABCMeta)
class _IBatchwiseSchedule(object):
"""A schedule determining per batch whether a condition is met."""
def on_batch(self, batch):
"""Determine for the given batch whether the condition is met.
Added in 0.4.0.
Parameters
----------
batch : _BatchInAugmentation
Batch for which to evaluate the condition.
Returns
-------
bool
Signal whether the condition is met.
"""
# Added in 0.4.0.
class _EveryNBatchesSchedule(_IBatchwiseSchedule):
"""A schedule that generates a signal at every ``N`` th batch.
This schedule must be called for *every* batch in order to count them.
Added in 0.4.0.
"""
def __init__(self, interval):
self.interval = interval
self._batch_id = -1
# Added in 0.4.0.
def on_batch(self, batch):
self._batch_id += 1
signal = (self._batch_id % self.interval == 0)
return signal
class _SaveDebugImage(meta.Augmenter):
"""Augmenter saving debug images to a destination according to a schedule.
Added in 0.4.0.
Parameters
----------
destination : _IImageDestination
The destination receiving debug images.
schedule : _IBatchwiseSchedule
The schedule to use to determine for which batches an image is
supposed to be generated.
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
See :func:`~imgaug.augmenters.meta.Augmenter.__init__`.
name : None or str, optional
See :func:`~imgaug.augmenters.meta.Augmenter.__init__`.
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
Old name for parameter `seed`.
Its usage will not yet cause a deprecation warning,
but it is still recommended to use `seed` now.
Outdated since 0.4.0.
deterministic : bool, optional
Deprecated since 0.4.0.
See method ``to_deterministic()`` for an alternative and for
details about what the "deterministic mode" actually does.
"""
# Added in 0.4.0.
def __init__(self, destination, schedule,
seed=None, name=None,
random_state="deprecated", deterministic="deprecated"):
super(_SaveDebugImage, self).__init__(
seed=seed, name=name,
random_state=random_state, deterministic=deterministic)
self.destination = destination
self.schedule = schedule
# Added in 0.4.0.
def _augment_batch_(self, batch, random_state, parents, hooks):
save = self.schedule.on_batch(batch)
self.destination.on_batch(batch)
if save:
image = draw_debug_image(
images=batch.images,
heatmaps=batch.heatmaps,
segmentation_maps=batch.segmentation_maps,
keypoints=batch.keypoints,
bounding_boxes=batch.bounding_boxes,
polygons=batch.polygons,
line_strings=batch.line_strings)
self.destination.receive(image)
return batch
[docs]class SaveDebugImageEveryNBatches(_SaveDebugImage):
"""Visualize data in batches and save corresponding plots to a folder.
Added in 0.4.0.
**Supported dtypes**:
See :func:`~imgaug.augmenters.debug.draw_debug_image`.
Parameters
----------
destination : str or _IImageDestination
Path to a folder. The saved images will follow a filename pattern
of ``batch_<batch_id>.png``. The latest image will additionally be
saved to ``latest.png``.
interval : int
Interval in batches. If set to ``N``, every ``N`` th batch an
image will be generated and saved, starting with the first observed
batch.
Note that the augmenter only counts batches that it sees. If it is
executed conditionally or re-instantiated, it may not see all batches
or the counter may be wrong in other ways.
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
See :func:`~imgaug.augmenters.meta.Augmenter.__init__`.
name : None or str, optional
See :func:`~imgaug.augmenters.meta.Augmenter.__init__`.
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
Old name for parameter `seed`.
Its usage will not yet cause a deprecation warning,
but it is still recommended to use `seed` now.
Outdated since 0.4.0.
deterministic : bool, optional
Deprecated since 0.4.0.
See method ``to_deterministic()`` for an alternative and for
details about what the "deterministic mode" actually does.
Examples
--------
>>> import imgaug.augmenters as iaa
>>> import tempfile
>>> folder_path = tempfile.mkdtemp()
>>> seq = iaa.Sequential([
>>> iaa.Sequential([
>>> iaa.Fliplr(0.5),
>>> iaa.Crop(px=(0, 16))
>>> ], random_order=True),
>>> iaa.SaveDebugImageEveryNBatches(folder_path, 100)
>>> ])
"""
# Added in 0.4.0.
def __init__(self, destination, interval,
seed=None, name=None,
random_state="deprecated", deterministic="deprecated"):
schedule = _EveryNBatchesSchedule(interval)
if not isinstance(destination, _IImageDestination):
assert os.path.isdir(destination), (
"Expected 'destination' to be a string path to an existing "
"directory. Got path '%s'." % (destination,))
destination = _MultiDestination([
_FolderImageDestination(destination),
_FolderImageDestination(destination,
filename_pattern="batch_latest.png")
])
super(SaveDebugImageEveryNBatches, self).__init__(
destination=destination, schedule=schedule,
seed=seed, name=name,
random_state=random_state, deterministic=deterministic)
# Added in 0.4.0.
[docs] def get_parameters(self):
dests = self.destination.destinations
return [
dests[0].folder_path,
dests[0].filename_pattern,
dests[1].folder_path,
dests[1].filename_pattern,
self.schedule.interval
]