Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Standardize LUT table calls throughout library #542

Merged
merged 2 commits into from Jan 7, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions changelogs/master/added/20191230_standardized_lut.md
@@ -0,0 +1,7 @@
# Standardized LUT Methods #542

* Added `imgaug.imgaug.apply_lut()`, which applies a lookup table to an image.
* Added `imgaug.imgaug.apply_lut_()`. In-place version of `apply_lut()`.
* Refactored all augmenters to use these new LUT functions.
This likely fixed some so-far undiscovered bugs in augmenters using LUT
tables.
54 changes: 13 additions & 41 deletions imgaug/augmenters/arithmetic.py
Expand Up @@ -153,23 +153,13 @@ def _add_scalar_to_uint8(image, value):
result = []
# TODO check if tile() is here actually needed
tables = np.tile(
value_range[np.newaxis, :],
(nb_channels, 1)
) + value[:, np.newaxis]
tables = np.clip(tables, 0, 255).astype(image.dtype)

for c, table in enumerate(tables):
result.append(cv2.LUT(image[..., c], table))

return np.stack(result, axis=-1)
value_range[:, np.newaxis],
(1, nb_channels)
) + value[np.newaxis, :]
else:
table = value_range + value
image_aug = cv2.LUT(
image,
iadt.clip_(table, 0, 255).astype(image.dtype))
if image_aug.ndim == 2 and image.ndim == 3:
image_aug = image_aug[..., np.newaxis]
return image_aug
tables = value_range + value
tables = np.clip(tables, 0, 255).astype(image.dtype)
return ia.apply_lut(image, tables)


def _add_scalar_to_non_uint8(image, value):
Expand Down Expand Up @@ -443,23 +433,13 @@ def _multiply_scalar_to_uint8(image, multiplier):
result = []
# TODO check if tile() is here actually needed
tables = np.tile(
value_range[np.newaxis, :],
(nb_channels, 1)
) * multiplier[:, np.newaxis]
tables = np.clip(tables, 0, 255).astype(image.dtype)

for c, table in enumerate(tables):
arr_aug = cv2.LUT(image[..., c], table)
result.append(arr_aug)

return np.stack(result, axis=-1)
value_range[:, np.newaxis],
(1, nb_channels)
) * multiplier[np.newaxis, :]
else:
table = value_range * multiplier
image_aug = cv2.LUT(
image, np.clip(table, 0, 255).astype(image.dtype))
if image_aug.ndim == 2 and image.ndim == 3:
image_aug = image_aug[..., np.newaxis]
return image_aug
tables = value_range * multiplier
tables = np.clip(tables, 0, 255).astype(image.dtype)
return ia.apply_lut(image, tables)


def _multiply_scalar_to_non_uint8(image, multiplier):
Expand Down Expand Up @@ -1198,17 +1178,9 @@ def _invert_bool(arr, min_value, max_value):

def _invert_uint8_(arr, min_value, max_value, threshold,
invert_above_threshold):
if 0 in arr.shape:
return np.copy(arr)

if arr.flags["OWNDATA"] is False:
arr = np.copy(arr)
if arr.flags["C_CONTIGUOUS"] is False:
arr = np.ascontiguousarray(arr)

table = _generate_table_for_invert_uint8(
min_value, max_value, threshold, invert_above_threshold)
arr = cv2.LUT(arr, table, dst=arr)
arr = ia.apply_lut_(arr, table)
return arr


Expand Down
1 change: 1 addition & 0 deletions imgaug/augmenters/blur.py
Expand Up @@ -344,6 +344,7 @@ def blur_mean_shift_(image, spatial_window_radius, color_window_radius):
image = np.tile(image, (1, 1, 3))

# prevent image from becoming cv2.UMat
# TODO merge this with apply_lut() normalization/validation
if image.flags["C_CONTIGUOUS"] is False:
image = np.ascontiguousarray(image)

Expand Down
19 changes: 10 additions & 9 deletions imgaug/augmenters/color.py
Expand Up @@ -241,6 +241,7 @@ def _get_dst(image, from_to_cspace):
# images that are views (e.g. image[..., 0:3]) and returns a
# cv2.UMat instance instead of an array. So we check here first
# if the array looks like it is non-contiguous or a view.
# TODO merge this with apply_lut() normalization/validation
if image.flags["C_CONTIGUOUS"]:
return image
return None
Expand Down Expand Up @@ -2315,11 +2316,13 @@ def _transform_image_cv2(cls, image_hsv, hue, saturation):
# code with using cache (at best maybe 10% faster for 64x64):
table_hue = cls._LUT_CACHE[0]
table_saturation = cls._LUT_CACHE[1]
tables = [
table_hue[255+int(hue)],
table_saturation[255+int(saturation)]
]

image_hsv[..., 0] = cv2.LUT(
image_hsv[..., 0], table_hue[255+int(hue)])
image_hsv[..., 1] = cv2.LUT(
image_hsv[..., 1], table_saturation[255+int(saturation)])
image_hsv[..., [0, 1]] = ia.apply_lut(image_hsv[..., [0, 1]],
tables)

return image_hsv

Expand Down Expand Up @@ -3084,7 +3087,7 @@ def _generate_pixelwise_alpha_mask(cls, image_hsv, hue_to_alpha):
hue = image_hsv[:, :, 0]
table = hue_to_alpha * 255
table = np.clip(np.round(table), 0, 255).astype(np.uint8)
mask = cv2.LUT(hue, table)
mask = ia.apply_lut(hue, table)
return mask.astype(np.float32) / 255.0

def get_parameters(self):
Expand Down Expand Up @@ -4055,22 +4058,20 @@ def quantize_uniform_(arr, nb_bins, to_bin_centers=True):
if nb_bins == 256 or 0 in arr.shape:
return arr

# TODO remove dtype check here? apply_lut_() does that already
assert arr.dtype.name == "uint8", "Expected uint8 image, got %s." % (
arr.dtype.name,)
assert 2 <= nb_bins <= 256, (
"Expected nb_bins to be in the discrete interval [2..256]. "
"Got a value of %d instead." % (nb_bins,))

if arr.flags["C_CONTIGUOUS"] is False:
arr = np.ascontiguousarray(arr)

table_class = (_QuantizeUniformCenterizedLUTTableSingleton
if to_bin_centers
else _QuantizeUniformNotCenterizedLUTTableSingleton)
table = (table_class
.get_instance()
.get_for_nb_bins(nb_bins))
arr = cv2.LUT(arr, table, dst=arr)
arr = ia.apply_lut_(arr, table)
return arr


Expand Down
40 changes: 16 additions & 24 deletions imgaug/augmenters/contrast.py
Expand Up @@ -146,8 +146,8 @@ def adjust_contrast_gamma(arr, gamma):
return np.copy(arr)

# int8 is also possible according to docs
# https://docs.opencv.org/3.0-beta/modules/core/doc/operations_on_arrays.html#cv2.LUT , but here it seemed
# like `d` was 0 for CV_8S, causing that to fail
# https://docs.opencv.org/3.0-beta/modules/core/doc/operations_on_arrays.html#cv2.LUT ,
# but here it seemed like `d` was 0 for CV_8S, causing that to fail
if arr.dtype.name == "uint8":
min_value, _center_value, max_value = \
iadt.get_value_range_of_dtype(arr.dtype)
Expand All @@ -162,10 +162,8 @@ def adjust_contrast_gamma(arr, gamma):
table = (min_value
+ (value_range ** np.float32(gamma))
* dynamic_range)
arr_aug = cv2.LUT(
arr, np.clip(table, min_value, max_value).astype(arr.dtype))
if arr.ndim == 3 and arr_aug.ndim == 2:
return arr_aug[..., np.newaxis]
table = np.clip(table, min_value, max_value).astype(arr.dtype)
arr_aug = ia.apply_lut(arr, table)
return arr_aug
return ski_exposure.adjust_gamma(arr, gamma)

Expand Down Expand Up @@ -232,8 +230,8 @@ def adjust_contrast_sigmoid(arr, gain, cutoff):
return np.copy(arr)

# int8 is also possible according to docs
# https://docs.opencv.org/3.0-beta/modules/core/doc/operations_on_arrays.html#cv2.LUT , but here it seemed
# like `d` was 0 for CV_8S, causing that to fail
# https://docs.opencv.org/3.0-beta/modules/core/doc/operations_on_arrays.html#cv2.LUT ,
# but here it seemed like `d` was 0 for CV_8S, causing that to fail
if arr.dtype.name == "uint8":
min_value, _center_value, max_value = \
iadt.get_value_range_of_dtype(arr.dtype)
Expand All @@ -250,10 +248,8 @@ def adjust_contrast_sigmoid(arr, gain, cutoff):
table = (min_value
+ dynamic_range
* 1/(1 + np.exp(gain * (cutoff - value_range))))
arr_aug = cv2.LUT(
arr, np.clip(table, min_value, max_value).astype(arr.dtype))
if arr.ndim == 3 and arr_aug.ndim == 2:
return arr_aug[..., np.newaxis]
table = np.clip(table, min_value, max_value).astype(arr.dtype)
arr_aug = ia.apply_lut(arr, table)
return arr_aug
return ski_exposure.adjust_sigmoid(arr, cutoff=cutoff, gain=gain)

Expand Down Expand Up @@ -319,8 +315,8 @@ def adjust_contrast_log(arr, gain):
return np.copy(arr)

# int8 is also possible according to docs
# https://docs.opencv.org/3.0-beta/modules/core/doc/operations_on_arrays.html#cv2.LUT , but here it seemed
# like `d` was 0 for CV_8S, causing that to fail
# https://docs.opencv.org/3.0-beta/modules/core/doc/operations_on_arrays.html#cv2.LUT ,
# but here it seemed like `d` was 0 for CV_8S, causing that to fail
if arr.dtype.name == "uint8":
min_value, _center_value, max_value = \
iadt.get_value_range_of_dtype(arr.dtype)
Expand All @@ -334,10 +330,8 @@ def adjust_contrast_log(arr, gain):
# of size 1
gain = np.float32(gain)
table = min_value + dynamic_range * gain * np.log2(1 + value_range)
arr_aug = cv2.LUT(
arr, np.clip(table, min_value, max_value).astype(arr.dtype))
if arr.ndim == 3 and arr_aug.ndim == 2:
return arr_aug[..., np.newaxis]
table = np.clip(table, min_value, max_value).astype(arr.dtype)
arr_aug = ia.apply_lut(arr, table)
return arr_aug
return ski_exposure.adjust_log(arr, gain=gain)

Expand Down Expand Up @@ -391,8 +385,8 @@ def adjust_contrast_linear(arr, alpha):
return np.copy(arr)

# int8 is also possible according to docs
# https://docs.opencv.org/3.0-beta/modules/core/doc/operations_on_arrays.html#cv2.LUT , but here it seemed
# like `d` was 0 for CV_8S, causing that to fail
# https://docs.opencv.org/3.0-beta/modules/core/doc/operations_on_arrays.html#cv2.LUT ,
# but here it seemed like `d` was 0 for CV_8S, causing that to fail
if arr.dtype.name == "uint8":
min_value, center_value, max_value = \
iadt.get_value_range_of_dtype(arr.dtype)
Expand All @@ -406,10 +400,8 @@ def adjust_contrast_linear(arr, alpha):
# of size 1
alpha = np.float32(alpha)
table = center_value + alpha * (value_range - center_value)
arr_aug = cv2.LUT(
arr, np.clip(table, min_value, max_value).astype(arr.dtype))
if arr.ndim == 3 and arr_aug.ndim == 2:
return arr_aug[..., np.newaxis]
table = np.clip(table, min_value, max_value).astype(arr.dtype)
arr_aug = ia.apply_lut(arr, table)
return arr_aug
else:
input_dtype = arr.dtype
Expand Down
17 changes: 6 additions & 11 deletions imgaug/augmenters/pillike.py
Expand Up @@ -282,13 +282,8 @@ def equalize_(image, mask=None):
# note that this is supposed to be a non-PIL reimplementation of PIL's
# equalize, which produces slightly different results from cv2.equalizeHist()
def _equalize_no_pil_(image, mask=None):
flags = image.flags
if not flags["OWNDATA"]:
image = np.copy(image)
if not flags["C_CONTIGUOUS"]:
image = np.ascontiguousarray(image)

nb_channels = 1 if image.ndim == 2 else image.shape[-1]
# TODO remove the first axis, no longer needed
lut = np.empty((1, 256, nb_channels), dtype=np.int32)

for c_idx in range(nb_channels):
Expand All @@ -312,9 +307,7 @@ def _equalize_no_pil_(image, mask=None):
lut[0, 1:, c_idx] = n + cumsum[0:-1]
lut[0, :, c_idx] //= int(step)
lut = np.clip(lut, None, 255, out=lut).astype(np.uint8)
image = cv2.LUT(image, lut, dst=image)
if image.ndim == 2 and image.ndim == 3:
return image[..., np.newaxis]
image = ia.apply_lut_(image, lut)
return image


Expand Down Expand Up @@ -420,7 +413,7 @@ def _autocontrast_no_pil(image, cutoff, ignore): # noqa: C901
# using [0] instead of [int(c_idx)] allows this to work with >4
# channels
if image.ndim == 2:
image_c = image
image_c = image[:, :, np.newaxis]
else:
image_c = image[:, :, c_idx:c_idx+1]
h = cv2.calcHist([image_c], [0], None, [256], [0, 256])
Expand Down Expand Up @@ -488,7 +481,9 @@ def _autocontrast_no_pil(image, cutoff, ignore): # noqa: C901
# ix = np.clip(ix, 0, 255).astype(np.uint8)
# lut = ix

result[:, :, c_idx] = cv2.LUT(image_c, lut)
# TODO change to a single call instead of one per channel
image_c_aug = ia.apply_lut(image_c, lut)
result[:, :, c_idx:c_idx+1] = image_c_aug
if image.ndim == 2:
return result[..., 0]
return result
Expand Down