|
|
|
import cv2 |
|
import numpy as np |
|
|
|
from ..utils import is_tuple_of |
|
from .colorspace import bgr2gray, gray2bgr |
|
|
|
|
|
def imnormalize(img, mean, std, to_rgb=True): |
|
"""Normalize an image with mean and std. |
|
|
|
Args: |
|
img (ndarray): Image to be normalized. |
|
mean (ndarray): The mean to be used for normalize. |
|
std (ndarray): The std to be used for normalize. |
|
to_rgb (bool): Whether to convert to rgb. |
|
|
|
Returns: |
|
ndarray: The normalized image. |
|
""" |
|
img = img.copy().astype(np.float32) |
|
return imnormalize_(img, mean, std, to_rgb) |
|
|
|
|
|
def imnormalize_(img, mean, std, to_rgb=True): |
|
"""Inplace normalize an image with mean and std. |
|
|
|
Args: |
|
img (ndarray): Image to be normalized. |
|
mean (ndarray): The mean to be used for normalize. |
|
std (ndarray): The std to be used for normalize. |
|
to_rgb (bool): Whether to convert to rgb. |
|
|
|
Returns: |
|
ndarray: The normalized image. |
|
""" |
|
|
|
assert img.dtype != np.uint8 |
|
mean = np.float64(mean.reshape(1, -1)) |
|
stdinv = 1 / np.float64(std.reshape(1, -1)) |
|
if to_rgb: |
|
cv2.cvtColor(img, cv2.COLOR_BGR2RGB, img) |
|
cv2.subtract(img, mean, img) |
|
cv2.multiply(img, stdinv, img) |
|
return img |
|
|
|
|
|
def imdenormalize(img, mean, std, to_bgr=True): |
|
assert img.dtype != np.uint8 |
|
mean = mean.reshape(1, -1).astype(np.float64) |
|
std = std.reshape(1, -1).astype(np.float64) |
|
img = cv2.multiply(img, std) |
|
cv2.add(img, mean, img) |
|
if to_bgr: |
|
cv2.cvtColor(img, cv2.COLOR_RGB2BGR, img) |
|
return img |
|
|
|
|
|
def iminvert(img): |
|
"""Invert (negate) an image. |
|
|
|
Args: |
|
img (ndarray): Image to be inverted. |
|
|
|
Returns: |
|
ndarray: The inverted image. |
|
""" |
|
return np.full_like(img, 255) - img |
|
|
|
|
|
def solarize(img, thr=128): |
|
"""Solarize an image (invert all pixel values above a threshold) |
|
|
|
Args: |
|
img (ndarray): Image to be solarized. |
|
thr (int): Threshold for solarizing (0 - 255). |
|
|
|
Returns: |
|
ndarray: The solarized image. |
|
""" |
|
img = np.where(img < thr, img, 255 - img) |
|
return img |
|
|
|
|
|
def posterize(img, bits): |
|
"""Posterize an image (reduce the number of bits for each color channel) |
|
|
|
Args: |
|
img (ndarray): Image to be posterized. |
|
bits (int): Number of bits (1 to 8) to use for posterizing. |
|
|
|
Returns: |
|
ndarray: The posterized image. |
|
""" |
|
shift = 8 - bits |
|
img = np.left_shift(np.right_shift(img, shift), shift) |
|
return img |
|
|
|
|
|
def adjust_color(img, alpha=1, beta=None, gamma=0): |
|
r"""It blends the source image and its gray image: |
|
|
|
.. math:: |
|
output = img * alpha + gray\_img * beta + gamma |
|
|
|
Args: |
|
img (ndarray): The input source image. |
|
alpha (int | float): Weight for the source image. Default 1. |
|
beta (int | float): Weight for the converted gray image. |
|
If None, it's assigned the value (1 - `alpha`). |
|
gamma (int | float): Scalar added to each sum. |
|
Same as :func:`cv2.addWeighted`. Default 0. |
|
|
|
Returns: |
|
ndarray: Colored image which has the same size and dtype as input. |
|
""" |
|
gray_img = bgr2gray(img) |
|
gray_img = np.tile(gray_img[..., None], [1, 1, 3]) |
|
if beta is None: |
|
beta = 1 - alpha |
|
colored_img = cv2.addWeighted(img, alpha, gray_img, beta, gamma) |
|
if not colored_img.dtype == np.uint8: |
|
|
|
|
|
|
|
|
|
colored_img = np.clip(colored_img, 0, 255) |
|
return colored_img |
|
|
|
|
|
def imequalize(img): |
|
"""Equalize the image histogram. |
|
|
|
This function applies a non-linear mapping to the input image, |
|
in order to create a uniform distribution of grayscale values |
|
in the output image. |
|
|
|
Args: |
|
img (ndarray): Image to be equalized. |
|
|
|
Returns: |
|
ndarray: The equalized image. |
|
""" |
|
|
|
def _scale_channel(im, c): |
|
"""Scale the data in the corresponding channel.""" |
|
im = im[:, :, c] |
|
|
|
histo = np.histogram(im, 256, (0, 255))[0] |
|
|
|
nonzero_histo = histo[histo > 0] |
|
step = (np.sum(nonzero_histo) - nonzero_histo[-1]) // 255 |
|
if not step: |
|
lut = np.array(range(256)) |
|
else: |
|
|
|
|
|
lut = (np.cumsum(histo) + (step // 2)) // step |
|
|
|
lut = np.concatenate([[0], lut[:-1]], 0) |
|
|
|
lut[lut > 255] = 255 |
|
|
|
|
|
return np.where(np.equal(step, 0), im, lut[im]) |
|
|
|
|
|
|
|
s1 = _scale_channel(img, 0) |
|
s2 = _scale_channel(img, 1) |
|
s3 = _scale_channel(img, 2) |
|
equalized_img = np.stack([s1, s2, s3], axis=-1) |
|
return equalized_img.astype(img.dtype) |
|
|
|
|
|
def adjust_brightness(img, factor=1.): |
|
"""Adjust image brightness. |
|
|
|
This function controls the brightness of an image. An |
|
enhancement factor of 0.0 gives a black image. |
|
A factor of 1.0 gives the original image. This function |
|
blends the source image and the degenerated black image: |
|
|
|
.. math:: |
|
output = img * factor + degenerated * (1 - factor) |
|
|
|
Args: |
|
img (ndarray): Image to be brightened. |
|
factor (float): A value controls the enhancement. |
|
Factor 1.0 returns the original image, lower |
|
factors mean less color (brightness, contrast, |
|
etc), and higher values more. Default 1. |
|
|
|
Returns: |
|
ndarray: The brightened image. |
|
""" |
|
degenerated = np.zeros_like(img) |
|
|
|
|
|
|
|
brightened_img = cv2.addWeighted( |
|
img.astype(np.float32), factor, degenerated.astype(np.float32), |
|
1 - factor, 0) |
|
brightened_img = np.clip(brightened_img, 0, 255) |
|
return brightened_img.astype(img.dtype) |
|
|
|
|
|
def adjust_contrast(img, factor=1.): |
|
"""Adjust image contrast. |
|
|
|
This function controls the contrast of an image. An |
|
enhancement factor of 0.0 gives a solid grey |
|
image. A factor of 1.0 gives the original image. It |
|
blends the source image and the degenerated mean image: |
|
|
|
.. math:: |
|
output = img * factor + degenerated * (1 - factor) |
|
|
|
Args: |
|
img (ndarray): Image to be contrasted. BGR order. |
|
factor (float): Same as :func:`mmcv.adjust_brightness`. |
|
|
|
Returns: |
|
ndarray: The contrasted image. |
|
""" |
|
gray_img = bgr2gray(img) |
|
hist = np.histogram(gray_img, 256, (0, 255))[0] |
|
mean = round(np.sum(gray_img) / np.sum(hist)) |
|
degenerated = (np.ones_like(img[..., 0]) * mean).astype(img.dtype) |
|
degenerated = gray2bgr(degenerated) |
|
contrasted_img = cv2.addWeighted( |
|
img.astype(np.float32), factor, degenerated.astype(np.float32), |
|
1 - factor, 0) |
|
contrasted_img = np.clip(contrasted_img, 0, 255) |
|
return contrasted_img.astype(img.dtype) |
|
|
|
|
|
def auto_contrast(img, cutoff=0): |
|
"""Auto adjust image contrast. |
|
|
|
This function maximize (normalize) image contrast by first removing cutoff |
|
percent of the lightest and darkest pixels from the histogram and remapping |
|
the image so that the darkest pixel becomes black (0), and the lightest |
|
becomes white (255). |
|
|
|
Args: |
|
img (ndarray): Image to be contrasted. BGR order. |
|
cutoff (int | float | tuple): The cutoff percent of the lightest and |
|
darkest pixels to be removed. If given as tuple, it shall be |
|
(low, high). Otherwise, the single value will be used for both. |
|
Defaults to 0. |
|
|
|
Returns: |
|
ndarray: The contrasted image. |
|
""" |
|
|
|
def _auto_contrast_channel(im, c, cutoff): |
|
im = im[:, :, c] |
|
|
|
histo = np.histogram(im, 256, (0, 255))[0] |
|
|
|
histo_sum = np.cumsum(histo) |
|
cut_low = histo_sum[-1] * cutoff[0] // 100 |
|
cut_high = histo_sum[-1] - histo_sum[-1] * cutoff[1] // 100 |
|
histo_sum = np.clip(histo_sum, cut_low, cut_high) - cut_low |
|
histo = np.concatenate([[histo_sum[0]], np.diff(histo_sum)], 0) |
|
|
|
|
|
low, high = np.nonzero(histo)[0][0], np.nonzero(histo)[0][-1] |
|
|
|
if low >= high: |
|
return im |
|
scale = 255.0 / (high - low) |
|
offset = -low * scale |
|
lut = np.array(range(256)) |
|
lut = lut * scale + offset |
|
lut = np.clip(lut, 0, 255) |
|
return lut[im] |
|
|
|
if isinstance(cutoff, (int, float)): |
|
cutoff = (cutoff, cutoff) |
|
else: |
|
assert isinstance(cutoff, tuple), 'cutoff must be of type int, ' \ |
|
f'float or tuple, but got {type(cutoff)} instead.' |
|
|
|
|
|
s1 = _auto_contrast_channel(img, 0, cutoff) |
|
s2 = _auto_contrast_channel(img, 1, cutoff) |
|
s3 = _auto_contrast_channel(img, 2, cutoff) |
|
contrasted_img = np.stack([s1, s2, s3], axis=-1) |
|
return contrasted_img.astype(img.dtype) |
|
|
|
|
|
def adjust_sharpness(img, factor=1., kernel=None): |
|
"""Adjust image sharpness. |
|
|
|
This function controls the sharpness of an image. An |
|
enhancement factor of 0.0 gives a blurred image. A |
|
factor of 1.0 gives the original image. And a factor |
|
of 2.0 gives a sharpened image. It blends the source |
|
image and the degenerated mean image: |
|
|
|
.. math:: |
|
output = img * factor + degenerated * (1 - factor) |
|
|
|
Args: |
|
img (ndarray): Image to be sharpened. BGR order. |
|
factor (float): Same as :func:`mmcv.adjust_brightness`. |
|
kernel (np.ndarray, optional): Filter kernel to be applied on the img |
|
to obtain the degenerated img. Defaults to None. |
|
|
|
Note: |
|
No value sanity check is enforced on the kernel set by users. So with |
|
an inappropriate kernel, the ``adjust_sharpness`` may fail to perform |
|
the function its name indicates but end up performing whatever |
|
transform determined by the kernel. |
|
|
|
Returns: |
|
ndarray: The sharpened image. |
|
""" |
|
|
|
if kernel is None: |
|
|
|
kernel = np.array([[1., 1., 1.], [1., 5., 1.], [1., 1., 1.]]) / 13 |
|
assert isinstance(kernel, np.ndarray), \ |
|
f'kernel must be of type np.ndarray, but got {type(kernel)} instead.' |
|
assert kernel.ndim == 2, \ |
|
f'kernel must have a dimension of 2, but got {kernel.ndim} instead.' |
|
|
|
degenerated = cv2.filter2D(img, -1, kernel) |
|
sharpened_img = cv2.addWeighted( |
|
img.astype(np.float32), factor, degenerated.astype(np.float32), |
|
1 - factor, 0) |
|
sharpened_img = np.clip(sharpened_img, 0, 255) |
|
return sharpened_img.astype(img.dtype) |
|
|
|
|
|
def adjust_lighting(img, eigval, eigvec, alphastd=0.1, to_rgb=True): |
|
"""AlexNet-style PCA jitter. |
|
|
|
This data augmentation is proposed in `ImageNet Classification with Deep |
|
Convolutional Neural Networks |
|
<https://dl.acm.org/doi/pdf/10.1145/3065386>`_. |
|
|
|
Args: |
|
img (ndarray): Image to be adjusted lighting. BGR order. |
|
eigval (ndarray): the eigenvalue of the convariance matrix of pixel |
|
values, respectively. |
|
eigvec (ndarray): the eigenvector of the convariance matrix of pixel |
|
values, respectively. |
|
alphastd (float): The standard deviation for distribution of alpha. |
|
Defaults to 0.1 |
|
to_rgb (bool): Whether to convert img to rgb. |
|
|
|
Returns: |
|
ndarray: The adjusted image. |
|
""" |
|
assert isinstance(eigval, np.ndarray) and isinstance(eigvec, np.ndarray), \ |
|
f'eigval and eigvec should both be of type np.ndarray, got ' \ |
|
f'{type(eigval)} and {type(eigvec)} instead.' |
|
|
|
assert eigval.ndim == 1 and eigvec.ndim == 2 |
|
assert eigvec.shape == (3, eigval.shape[0]) |
|
n_eigval = eigval.shape[0] |
|
assert isinstance(alphastd, float), 'alphastd should be of type float, ' \ |
|
f'got {type(alphastd)} instead.' |
|
|
|
img = img.copy().astype(np.float32) |
|
if to_rgb: |
|
cv2.cvtColor(img, cv2.COLOR_BGR2RGB, img) |
|
|
|
alpha = np.random.normal(0, alphastd, n_eigval) |
|
alter = eigvec \ |
|
* np.broadcast_to(alpha.reshape(1, n_eigval), (3, n_eigval)) \ |
|
* np.broadcast_to(eigval.reshape(1, n_eigval), (3, n_eigval)) |
|
alter = np.broadcast_to(alter.sum(axis=1).reshape(1, 1, 3), img.shape) |
|
img_adjusted = img + alter |
|
return img_adjusted |
|
|
|
|
|
def lut_transform(img, lut_table): |
|
"""Transform array by look-up table. |
|
|
|
The function lut_transform fills the output array with values from the |
|
look-up table. Indices of the entries are taken from the input array. |
|
|
|
Args: |
|
img (ndarray): Image to be transformed. |
|
lut_table (ndarray): look-up table of 256 elements; in case of |
|
multi-channel input array, the table should either have a single |
|
channel (in this case the same table is used for all channels) or |
|
the same number of channels as in the input array. |
|
|
|
Returns: |
|
ndarray: The transformed image. |
|
""" |
|
assert isinstance(img, np.ndarray) |
|
assert 0 <= np.min(img) and np.max(img) <= 255 |
|
assert isinstance(lut_table, np.ndarray) |
|
assert lut_table.shape == (256, ) |
|
|
|
return cv2.LUT(np.array(img, dtype=np.uint8), lut_table) |
|
|
|
|
|
def clahe(img, clip_limit=40.0, tile_grid_size=(8, 8)): |
|
"""Use CLAHE method to process the image. |
|
|
|
See `ZUIDERVELD,K. Contrast Limited Adaptive Histogram Equalization[J]. |
|
Graphics Gems, 1994:474-485.` for more information. |
|
|
|
Args: |
|
img (ndarray): Image to be processed. |
|
clip_limit (float): Threshold for contrast limiting. Default: 40.0. |
|
tile_grid_size (tuple[int]): Size of grid for histogram equalization. |
|
Input image will be divided into equally sized rectangular tiles. |
|
It defines the number of tiles in row and column. Default: (8, 8). |
|
|
|
Returns: |
|
ndarray: The processed image. |
|
""" |
|
assert isinstance(img, np.ndarray) |
|
assert img.ndim == 2 |
|
assert isinstance(clip_limit, (float, int)) |
|
assert is_tuple_of(tile_grid_size, int) |
|
assert len(tile_grid_size) == 2 |
|
|
|
clahe = cv2.createCLAHE(clip_limit, tile_grid_size) |
|
return clahe.apply(np.array(img, dtype=np.uint8)) |
|
|