import os import imghdr import numbers import warnings from io import BytesIO import cv2 import khandy import numpy as np from PIL import Image def imread(file_or_buffer, flags=-1): """Improvement on cv2.imread, make it support filename including chinese character. """ try: if isinstance(file_or_buffer, bytes): return cv2.imdecode(np.frombuffer(file_or_buffer, dtype=np.uint8), flags) else: # support type: file or str or Path return cv2.imdecode(np.fromfile(file_or_buffer, dtype=np.uint8), flags) except Exception as e: print(e) return None def imread_cv(file_or_buffer, flags=-1): warnings.warn('khandy.imread_cv will be deprecated, use khandy.imread instead!') return imread(file_or_buffer, flags) def imwrite(filename, image, params=None): """Improvement on cv2.imwrite, make it support filename including chinese character. """ cv2.imencode(os.path.splitext(filename)[-1], image, params)[1].tofile(filename) def imwrite_cv(filename, image, params=None): warnings.warn('khandy.imwrite_cv will be deprecated, use khandy.imwrite instead!') return imwrite(filename, image, params) def imread_pil(file_or_buffer, to_mode=None): """Improvement on Image.open to avoid ResourceWarning. """ try: if isinstance(file_or_buffer, bytes): buffer = BytesIO() buffer.write(file_or_buffer) buffer.seek(0) file_or_buffer = buffer if hasattr(file_or_buffer, 'read'): image = Image.open(file_or_buffer) if to_mode is not None: image = image.convert(to_mode) else: # open path as file to avoid ResourceWarning (https://github.com/python-pillow/Pillow/issues/835) with open(file_or_buffer, 'rb') as f: image = Image.open(f) # If convert outside with statement, will raise "seek of closed file" as # https://github.com/microsoft/Swin-Transformer/issues/66 if to_mode is not None: image = image.convert(to_mode) return image except Exception as e: print(e) return None def imwrite_bytes(filename, image_bytes: bytes, update_extension: bool = True): """Write image bytes to file. Args: filename: str filename which image_bytes is written into. image_bytes: bytes image content to be written. update_extension: bool whether update extension according to image_bytes or not. the cost of update extension is smaller than update image format. """ extension = imghdr.what('', image_bytes) file_extension = khandy.get_path_extension(filename) # imghdr.what fails to determine image format sometimes! # so when its return value is None, never update extension. if extension is None: image = cv2.imdecode(np.frombuffer(image_bytes, np.uint8), -1) image_bytes = cv2.imencode(file_extension, image)[1] elif (extension.lower() != file_extension.lower()[1:]): if update_extension: filename = khandy.replace_path_extension(filename, extension) else: image = cv2.imdecode(np.frombuffer(image_bytes, np.uint8), -1) image_bytes = cv2.imencode(file_extension, image)[1] with open(filename, "wb") as f: f.write(image_bytes) return filename def rescale_image(image: np.ndarray, rescale_factor='auto', dst_dtype=np.float32): """Rescale image by rescale_factor. Args: img (ndarray): Image to be rescaled. rescale_factor (str, int or float, *optional*, defaults to `'auto'`): rescale the image by the specified scale factor. When is `'auto'`, rescale the image to [0, 1). dtype (np.dtype, *optional*, defaults to `np.float32`): The dtype of the output image. Defaults to `np.float32`. Returns: ndarray: The rescaled image. """ if rescale_factor == 'auto': if np.issubdtype(image.dtype, np.unsignedinteger): rescale_factor = 1. / np.iinfo(image.dtype).max else: raise TypeError(f'Only support uint dtype ndarray when `rescale_factor` is `auto`, got {image.dtype}') elif issubclass(rescale_factor, (int, float)): pass else: raise TypeError('rescale_factor must be "auto", int or float') image = image.astype(dst_dtype, copy=True) image *= rescale_factor image = image.astype(dst_dtype) return image def normalize_image_value(image: np.ndarray, mean, std, rescale_factor=None): """Normalize an image with mean and std, rescale optionally. Args: image (ndarray): Image to be normalized. mean (int, float, Sequence[int], Sequence[float], ndarray): The mean to be used for normalize. std (int, float, Sequence[int], Sequence[float], ndarray): The std to be used for normalize. rescale_factor (None, 'auto', int or float, *optional*, defaults to `None`): rescale the image by the specified scale factor. When is `'auto'`, rescale the image to [0, 1); When is `None`, do not rescale. Returns: ndarray: The normalized image which dtype is np.float32. """ dst_dtype = np.float32 mean = np.array(mean, dtype=dst_dtype).flatten() std = np.array(std, dtype=dst_dtype).flatten() if rescale_factor == 'auto': if np.issubdtype(image.dtype, np.unsignedinteger): mean *= np.iinfo(image.dtype).max std *= np.iinfo(image.dtype).max else: raise TypeError(f'Only support uint dtype ndarray when `rescale_factor` is `auto`, got {image.dtype}') elif isinstance(rescale_factor, (int, float)): mean *= rescale_factor std *= rescale_factor image = image.astype(dst_dtype, copy=True) image -= mean image /= std return image def normalize_image_dtype(image, keep_num_channels=False): """Normalize image dtype to uint8 (usually for visualization). Args: image : ndarray Input image. keep_num_channels : bool, optional If this is set to True, the result is an array which has the same shape as input image, otherwise the result is an array whose channels number is 3. Returns: out: ndarray Image whose dtype is np.uint8. """ assert (image.ndim == 3 and image.shape[-1] in [1, 3]) or (image.ndim == 2) image = image.astype(np.float32) image = khandy.minmax_normalize(image, axis=None, copy=False) image = np.array(image * 255, dtype=np.uint8) if not keep_num_channels: if image.ndim == 2: image = np.expand_dims(image, -1) if image.shape[-1] == 1: image = np.tile(image, (1,1,3)) return image def normalize_image_channel(image, swap_rb=False): """Normalize image channel number and order to RGB or BGR. Args: image : ndarray Input image. swap_rb : bool, optional whether swap red and blue channel or not Returns: out: ndarray Image whose shape is (..., 3). """ if image.ndim == 2: image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR) elif image.ndim == 3: num_channels = image.shape[-1] if num_channels == 1: image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR) elif num_channels == 3: if swap_rb: image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) elif num_channels == 4: if swap_rb: image = cv2.cvtColor(image, cv2.COLOR_BGRA2RGB) else: image = cv2.cvtColor(image, cv2.COLOR_BGRA2BGR) else: raise ValueError(f'Unsupported image channel number, only support 1, 3 and 4, got {num_channels}!') else: raise ValueError(f'Unsupported image ndarray ndim, only support 2 and 3, got {image.ndim}!') return image def normalize_image_shape(image, swap_rb=False): warnings.warn('khandy.normalize_image_shape will be deprecated, use khandy.normalize_image_channel instead!') return normalize_image_channel(image, swap_rb) def stack_image_list(image_list, dtype=np.float32): """Join a sequence of image along a new axis before first axis. References: `im_list_to_blob` in `py-faster-rcnn-master/lib/utils/blob.py` """ assert isinstance(image_list, (tuple, list)) max_dimension = np.array([image.ndim for image in image_list]).max() assert max_dimension in [2, 3] max_shape = np.array([image.shape[:2] for image in image_list]).max(axis=0) num_channels = [] for image in image_list: if image.ndim == 2: num_channels.append(1) else: num_channels.append(image.shape[-1]) assert len(set(num_channels) - set([1])) in [0, 1] max_num_channels = np.max(num_channels) blob = np.empty((len(image_list), max_shape[0], max_shape[1], max_num_channels), dtype=dtype) for k, image in enumerate(image_list): blob[k, :image.shape[0], :image.shape[1], :] = np.atleast_3d(image).astype(dtype, copy=False) if max_dimension == 2: blob = np.squeeze(blob, axis=-1) return blob def is_numpy_image(image): return isinstance(image, np.ndarray) and image.ndim in {2, 3} def is_gray_image(image, tol=3): assert is_numpy_image(image) if image.ndim == 2: return True elif image.ndim == 3: num_channels = image.shape[-1] if num_channels == 1: return True elif num_channels == 3: gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) gray3 = cv2.cvtColor(gray, cv2.COLOR_GRAY2BGR) mae = np.mean(cv2.absdiff(image, gray3)) return mae <= tol elif num_channels == 4: rgb = cv2.cvtColor(image, cv2.COLOR_BGRA2BGR) gray = cv2.cvtColor(rgb, cv2.COLOR_BGR2GRAY) gray3 = cv2.cvtColor(gray, cv2.COLOR_GRAY2BGR) mae = np.mean(cv2.absdiff(rgb, gray3)) return mae <= tol else: return False else: return False def is_solid_color_image(image, tol=4): assert is_numpy_image(image) mean = np.array(cv2.mean(image)[:-1], dtype=np.float32) if image.ndim == 2: mae = np.mean(np.abs(image - mean[0])) return mae <= tol elif image.ndim == 3: num_channels = image.shape[-1] if num_channels == 1: mae = np.mean(np.abs(image - mean[0])) return mae <= tol elif num_channels == 3: mae = np.mean(np.abs(image - mean)) return mae <= tol elif num_channels == 4: mae = np.mean(np.abs(image[:,:,:-1] - mean)) return mae <= tol else: return False else: return False def create_solid_color_image(image_width, image_height, color, dtype=None): if isinstance(color, numbers.Real): image = np.full((image_height, image_width), color, dtype=dtype) elif isinstance(color, (tuple, list)): if len(color) == 1: image = np.full((image_height, image_width), color[0], dtype=dtype) elif len(color) in (3, 4): image = np.full((1, 1, len(color)), color, dtype=dtype) image = cv2.copyMakeBorder(image, 0, image_height-1, 0, image_width-1, cv2.BORDER_CONSTANT, value=color) else: color = np.asarray(color, dtype=dtype) image = np.empty((image_height, image_width, len(color)), dtype=dtype) image[:] = color else: raise TypeError(f'Invalid type {type(color)} for `color`.') return image