| | |
| | 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)) |
| |
|