Skip to content

Commit

Permalink
Improve Average Bluring (aleju#625)
Browse files Browse the repository at this point in the history
This patch adds `imgaug.augmenters.blur.blur_avg_()`,
which applies an averaging blur kernel to images. The method
is slightly faster for single image inputs (factor of 1.01x to
1.1x, more for medium-sized images around `128x128`) than
the one used in `AverageBlur`. The performance of `AverageBlur`
however is likely not changed significantly due to input
validation now being done per image instead of per batch.

Add functions:
* `imgaug.augmenters.blur.blur_avg_()`
  • Loading branch information
aleju authored Apr 22, 2020
1 parent f7e5e17 commit 761cc39
Show file tree
Hide file tree
Showing 3 changed files with 220 additions and 45 deletions.
12 changes: 12 additions & 0 deletions changelogs/master/improved/20200223_blur_avg.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Improved Average Bluring #625

This patch adds `imgaug.augmenters.blur.blur_avg_()`,
which applies an averaging blur kernel to images. The method
is slightly faster for single image inputs (factor of 1.01x to
1.1x, more for medium-sized images around `128x128`) than
the one used in `AverageBlur`. The performance of `AverageBlur`
however is likely not changed significantly due to input
validation now being done per image instead of per batch.

Add functions:
* `imgaug.augmenters.blur.blur_avg_()`
159 changes: 114 additions & 45 deletions imgaug/augmenters/blur.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,119 @@ def blur_gaussian_(image, sigma, ksize=None, backend="auto", eps=1e-3):
return image


def blur_avg_(image, k):
"""Blur an image in-place by computing averages over local neighbourhoods.
This operation *may* change the input image in-place.
The padding behaviour around the image borders is cv2's
``BORDER_REFLECT_101``.
Added in 0.5.0.
**Supported dtypes**:
* ``uint8``: yes; fully tested
* ``uint16``: yes; tested
* ``uint32``: no (1)
* ``uint64``: no (2)
* ``int8``: yes; tested (3)
* ``int16``: yes; tested
* ``int32``: no (4)
* ``int64``: no (5)
* ``float16``: yes; tested (6)
* ``float32``: yes; tested
* ``float64``: yes; tested
* ``float128``: no
* ``bool``: yes; tested (7)
- (1) rejected by ``cv2.blur()``
- (2) loss of resolution in ``cv2.blur()`` (result is ``int32``)
- (3) ``int8`` is mapped internally to ``int16``, ``int8`` itself
leads to cv2 error "Unsupported combination of source format
(=1), and buffer format (=4) in function 'getRowSumFilter'" in
``cv2``
- (4) results too inaccurate
- (5) loss of resolution in ``cv2.blur()`` (result is ``int32``)
- (6) ``float16`` is mapped internally to ``float32``
- (7) ``bool`` is mapped internally to ``float32``
Parameters
----------
image : numpy.ndarray
The image to blur. Expected to be of shape ``(H, W)`` or ``(H, W, C)``.
k : int or tuple of int
Kernel size to use. A single ``int`` will lead to an ``k x k``
kernel. Otherwise a ``tuple`` of two ``int`` ``(height, width)``
is expected.
Returns
-------
numpy.ndarray
The blurred image. Same shape and dtype as the input.
(Input image *might* have been altered in-place.)
"""
if isinstance(k, tuple):
k_height, k_width = k
else:
k_height, k_width = k, k

shape = image.shape
if 0 in shape:
return image

if k_height <= 0 or k_width <= 0 or (k_height, k_width) == (1, 1):
return image

iadt.gate_dtypes(
image,
allowed=["bool",
"uint8", "uint16", "int8", "int16",
"float16", "float32", "float64"],
disallowed=["uint32", "uint64", "uint128", "uint256",
"int32", "int64", "int128", "int256",
"float96", "float128", "float256"],
augmenter=None)

input_dtype = image.dtype
if image.dtype.name in ["bool", "float16"]:
image = image.astype(np.float32, copy=False)
elif image.dtype.name == "int8":
image = image.astype(np.int16, copy=False)

input_ndim = len(shape)
if input_ndim == 2 or shape[-1] <= 512:
image = _normalize_cv2_input_arr_(image)
image_aug = cv2.blur(
image,
(k_width, k_height),
dst=image
)
# cv2.blur() removes channel axis for single-channel images
if input_ndim == 3 and image_aug.ndim == 2:
image_aug = image_aug[..., np.newaxis]
else:
# TODO this is quite inefficient
# handling more than 512 channels in cv2.blur()
channels = [
cv2.blur(
_normalize_cv2_input_arr_(image[..., c]),
(k_width, k_height)
)
for c in sm.xrange(shape[-1])
]
image_aug = np.stack(channels, axis=-1)

if input_dtype.name == "bool":
image_aug = image_aug > 0.5
elif input_dtype.name in ["int8", "float16"]:
image_aug = iadt.restore_dtypes_(image_aug, input_dtype)

return image_aug


def blur_mean_shift_(image, spatial_window_radius, color_window_radius):
"""Apply a pyramidic mean shift filter to the input image in-place.
Expand Down Expand Up @@ -624,16 +737,6 @@ def _augment_batch_(self, batch, random_state, parents, hooks):

images = batch.images

iadt.gate_dtypes(
images,
allowed=["bool",
"uint8", "uint16", "int8", "int16",
"float16", "float32", "float64"],
disallowed=["uint32", "uint64", "uint128", "uint256",
"int32", "int64", "int128", "int256",
"float96", "float128", "float256"],
augmenter=self)

nb_images = len(images)
if self.mode == "single":
samples = self.k.draw_samples((nb_images,),
Expand All @@ -648,41 +751,7 @@ def _augment_batch_(self, batch, random_state, parents, hooks):

gen = enumerate(zip(images, samples[0], samples[1]))
for i, (image, ksize_h, ksize_w) in gen:
kernel_impossible = (ksize_h == 0 or ksize_w == 0)
kernel_does_nothing = (ksize_h == 1 and ksize_w == 1)
has_zero_sized_axes = (image.size == 0)
if (not kernel_impossible and not kernel_does_nothing
and not has_zero_sized_axes):
input_dtype = image.dtype
if image.dtype.name in ["bool", "float16"]:
image = image.astype(np.float32, copy=False)
elif image.dtype.name == "int8":
image = image.astype(np.int16, copy=False)

if image.ndim == 2 or image.shape[-1] <= 512:
image_aug = cv2.blur(
_normalize_cv2_input_arr_(image),
(ksize_h, ksize_w))
# cv2.blur() removes channel axis for single-channel images
if image_aug.ndim == 2:
image_aug = image_aug[..., np.newaxis]
else:
# TODO this is quite inefficient
# handling more than 512 channels in cv2.blur()
channels = [
cv2.blur(
_normalize_cv2_input_arr_(image[..., c]),
(ksize_h, ksize_w))
for c in sm.xrange(image.shape[-1])
]
image_aug = np.stack(channels, axis=-1)

if input_dtype.name == "bool":
image_aug = image_aug > 0.5
elif input_dtype.name in ["int8", "float16"]:
image_aug = iadt.restore_dtypes_(image_aug, input_dtype)

batch.images[i] = image_aug
batch.images[i] = blur_avg_(image, (ksize_h, ksize_w))
return batch

def get_parameters(self):
Expand Down
94 changes: 94 additions & 0 deletions test/augmenters/test_blur.py
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,100 @@ def test_other_dtypes_bool_at_sigma_06(self):
assert np.all(image_aug == expected)


class Test_blur_avg_(unittest.TestCase):
@classmethod
def _avg(cls, values):
return int(np.round(np.average(values)))

def test_kernel_size_is_int(self):
# reflection padded:
# [6, 5, 6, 7, 8, 7],
# [2, 1, 2, 3, 4, 3],
# [6, 5, 6, 7, 8, 7],
# [10, 9, 10, 11, 12, 11],
# [14, 13, 14, 15, 16, 15]
# [10, 9, 10, 11, 12, 11],
image = np.array([
[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12],
[13, 14, 15, 16]
], dtype=np.uint8)

image_aug = iaa.blur_avg_(np.copy(image), 3)

assert image_aug[0, 0] == self._avg([6, 5, 6, 2, 1, 2, 6, 5, 6])
assert image_aug[0, 1] == self._avg([5, 6, 7, 1, 2, 3, 5, 6, 7])
assert image_aug[3, 3] == self._avg([11, 12, 11, 15, 16, 15, 11, 12,
11])

def test_kernel_size_is_tuple(self):
# reflection padded:
# [6, 5, 6, 7, 8, 7],
# [2, 1, 2, 3, 4, 3],
# [6, 5, 6, 7, 8, 7],
# [10, 9, 10, 11, 12, 11],
# [14, 13, 14, 15, 16, 15]
# [10, 9, 10, 11, 12, 11],
image = np.array([
[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12],
[13, 14, 15, 16]
], dtype=np.uint8)

image_aug = iaa.blur_avg_(np.copy(image), (3, 1))

assert image_aug[0, 0] == self._avg([5, 1, 5])
assert image_aug[0, 1] == self._avg([6, 2, 6])
assert image_aug[3, 3] == self._avg([12, 16, 12])

def test_view(self):
# reflection padded (after crop):
# [6, 5, 6, 7, 8, 7],
# [2, 1, 2, 3, 4, 3],
# [6, 5, 6, 7, 8, 7],
# [10, 9, 10, 11, 12, 11],
# [14, 13, 14, 15, 16, 15]
# [10, 9, 10, 11, 12, 11],
image = np.array([
[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12],
[13, 14, 15, 16],
[0, 0, 0, 0]
], dtype=np.uint8)

image_aug = iaa.blur_avg_(np.copy(image)[0:4, :], 3)

assert image_aug[0, 0] == self._avg([6, 5, 6, 2, 1, 2, 6, 5, 6])
assert image_aug[0, 1] == self._avg([5, 6, 7, 1, 2, 3, 5, 6, 7])
assert image_aug[3, 3] == self._avg([11, 12, 11, 15, 16, 15, 11, 12,
11])

def test_noncontiguous(self):
# reflection padded:
# [6, 5, 6, 7, 8, 7],
# [2, 1, 2, 3, 4, 3],
# [6, 5, 6, 7, 8, 7],
# [10, 9, 10, 11, 12, 11],
# [14, 13, 14, 15, 16, 15]
# [10, 9, 10, 11, 12, 11],
image = np.array([
[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12],
[13, 14, 15, 16]
], dtype=np.uint8, order="F")

image_aug = iaa.blur_avg_(image, 3)

assert image_aug[0, 0] == self._avg([6, 5, 6, 2, 1, 2, 6, 5, 6])
assert image_aug[0, 1] == self._avg([5, 6, 7, 1, 2, 3, 5, 6, 7])
assert image_aug[3, 3] == self._avg([11, 12, 11, 15, 16, 15, 11, 12,
11])


class Test_blur_mean_shift_(unittest.TestCase):
@property
def image(self):
Expand Down

0 comments on commit 761cc39

Please sign in to comment.