| """ | |||||
| The transform implementation refers to the following paper: | |||||
| "Selective Feature Aggregation Network with Area-Boundary Constraints for Polyp Segmentation" | |||||
| https://github.com/Yuqi-cuhk/Polyp-Seg | |||||
| """ | |||||
| import torch | |||||
| import torchvision.transforms.functional as F | |||||
| import scipy.ndimage | |||||
| import random | |||||
| from PIL import Image | |||||
| import numpy as np | |||||
| import cv2 | |||||
| from skimage import transform as tf | |||||
| import numbers | |||||
| class ToTensor(object): | |||||
| def __call__(self, data): | |||||
| image, label = data['image'], data['label'] | |||||
| return {'image': F.to_tensor(image), 'label': F.to_tensor(label)} | |||||
| class Resize(object): | |||||
| def __init__(self, size): | |||||
| self.size = size | |||||
| def __call__(self, data): | |||||
| image, label = data['image'], data['label'] | |||||
| return {'image': F.resize(image, self.size), 'label': F.resize(label, self.size)} | |||||
| class RandomHorizontalFlip(object): | |||||
| def __init__(self, p=0.5): | |||||
| self.p = p | |||||
| def __call__(self, data): | |||||
| image, label = data['image'], data['label'] | |||||
| if random.random() < self.p: | |||||
| return {'image': F.hflip(image), 'label': F.hflip(label)} | |||||
| return {'image': image, 'label': label} | |||||
| class RandomVerticalFlip(object): | |||||
| def __init__(self, p=0.5): | |||||
| self.p = p | |||||
| def __call__(self, data): | |||||
| image, label = data['image'], data['label'] | |||||
| if random.random() < self.p: | |||||
| return {'image': F.vflip(image), 'label': F.vflip(label)} | |||||
| return {'image': image, 'label': label} | |||||
| class RandomRotation(object): | |||||
| def __init__(self, degrees, resample=False, expand=False, center=None): | |||||
| if isinstance(degrees,numbers.Number): | |||||
| if degrees < 0: | |||||
| raise ValueError("If degrees is a single number, it must be positive.") | |||||
| self.degrees = (-degrees, degrees) | |||||
| else: | |||||
| if len(degrees) != 2: | |||||
| raise ValueError("If degrees is a sequence, it must be of len 2.") | |||||
| self.degrees = degrees | |||||
| self.resample = resample | |||||
| self.expand = expand | |||||
| self.center = center | |||||
| @staticmethod | |||||
| def get_params(degrees): | |||||
| """Get parameters for ``rotate`` for a random rotation. | |||||
| Returns: | |||||
| sequence: params to be passed to ``rotate`` for random rotation. | |||||
| """ | |||||
| angle = random.uniform(degrees[0], degrees[1]) | |||||
| return angle | |||||
| def __call__(self, data): | |||||
| """ | |||||
| img (PIL Image): Image to be rotated. | |||||
| Returns: | |||||
| PIL Image: Rotated image. | |||||
| """ | |||||
| image, label = data['image'], data['label'] | |||||
| if random.random() < 0.5: | |||||
| angle = self.get_params(self.degrees) | |||||
| return {'image': F.rotate(image, angle, self.resample, self.expand, self.center), | |||||
| 'label': F.rotate(label, angle, self.resample, self.expand, self.center)} | |||||
| return {'image': image, 'label': label} | |||||
| class RandomZoom(object): | |||||
| def __init__(self, zoom=(0.8, 1.2)): | |||||
| self.min, self.max = zoom[0], zoom[1] | |||||
| def __call__(self, data): | |||||
| image, label = data['image'], data['label'] | |||||
| if random.random() < 0.5: | |||||
| image = np.array(image) | |||||
| label = np.array(label) | |||||
| zoom = random.uniform(self.min, self.max) | |||||
| zoom_image = clipped_zoom(image, zoom) | |||||
| zoom_label = clipped_zoom(label, zoom) | |||||
| zoom_image = Image.fromarray(zoom_image.astype('uint8'), 'RGB') | |||||
| zoom_label = Image.fromarray(zoom_label.astype('uint8'), 'L') | |||||
| return {'image': zoom_image, 'label': zoom_label} | |||||
| return {'image': image, 'label': label} | |||||
| def clipped_zoom(img, zoom_factor, **kwargs): | |||||
| h, w = img.shape[:2] | |||||
| # For multichannel images we don't want to apply the zoom factor to the RGB | |||||
| # dimension, so instead we create a tuple of zoom factors, one per array | |||||
| # dimension, with 1's for any trailing dimensions after the width and height. | |||||
| zoom_tuple = (zoom_factor,) * 2 + (1,) * (img.ndim - 2) | |||||
| # Zooming out | |||||
| if zoom_factor < 1: | |||||
| # Bounding box of the zoomed-out image within the output array | |||||
| zh = int(np.round(h * zoom_factor)) | |||||
| zw = int(np.round(w * zoom_factor)) | |||||
| top = (h - zh) // 2 | |||||
| left = (w - zw) // 2 | |||||
| # Zero-padding | |||||
| out = np.zeros_like(img) | |||||
| out[top:top + zh, left:left + zw] = scipy.ndimage.zoom(img, zoom_tuple, **kwargs) | |||||
| # Zooming in | |||||
| elif zoom_factor > 1: | |||||
| # Bounding box of the zoomed-in region within the input array | |||||
| zh = int(np.round(h / zoom_factor)) | |||||
| zw = int(np.round(w / zoom_factor)) | |||||
| top = (h - zh) // 2 | |||||
| left = (w - zw) // 2 | |||||
| zoom_in = scipy.ndimage.zoom(img[top:top + zh, left:left + zw], zoom_tuple, **kwargs) | |||||
| # `zoom_in` might still be slightly different with `img` due to rounding, so | |||||
| # trim off any extra pixels at the edges or zero-padding | |||||
| if zoom_in.shape[0] >= h: | |||||
| zoom_top = (zoom_in.shape[0] - h) // 2 | |||||
| sh = h | |||||
| out_top = 0 | |||||
| oh = h | |||||
| else: | |||||
| zoom_top = 0 | |||||
| sh = zoom_in.shape[0] | |||||
| out_top = (h - zoom_in.shape[0]) // 2 | |||||
| oh = zoom_in.shape[0] | |||||
| if zoom_in.shape[1] >= w: | |||||
| zoom_left = (zoom_in.shape[1] - w) // 2 | |||||
| sw = w | |||||
| out_left = 0 | |||||
| ow = w | |||||
| else: | |||||
| zoom_left = 0 | |||||
| sw = zoom_in.shape[1] | |||||
| out_left = (w - zoom_in.shape[1]) // 2 | |||||
| ow = zoom_in.shape[1] | |||||
| out = np.zeros_like(img) | |||||
| out[out_top:out_top + oh, out_left:out_left + ow] = zoom_in[zoom_top:zoom_top + sh, zoom_left:zoom_left + sw] | |||||
| # If zoom_factor == 1, just return the input array | |||||
| else: | |||||
| out = img | |||||
| return out | |||||
| class Translation(object): | |||||
| def __init__(self, translation): | |||||
| self.translation = translation | |||||
| def __call__(self, data): | |||||
| image, label = data['image'], data['label'] | |||||
| if random.random() < 0.5: | |||||
| image = np.array(image) | |||||
| label = np.array(label) | |||||
| rows, cols, ch = image.shape | |||||
| translation = random.uniform(0, self.translation) | |||||
| tr_x = translation / 2 | |||||
| tr_y = translation / 2 | |||||
| Trans_M = np.float32([[1, 0, tr_x], [0, 1, tr_y]]) | |||||
| translate_image = cv2.warpAffine(image, Trans_M, (cols, rows)) | |||||
| translate_label = cv2.warpAffine(label, Trans_M, (cols, rows)) | |||||
| translate_image = Image.fromarray(translate_image.astype('uint8'), 'RGB') | |||||
| translate_label = Image.fromarray(translate_label.astype('uint8'), 'L') | |||||
| return {'image': translate_image, 'label': translate_label} | |||||
| return {'image': image, 'label': label} | |||||
| class RandomCrop(object): | |||||
| def __init__(self, size, padding=None, pad_if_needed=False, fill=0, padding_mode='constant'): | |||||
| if isinstance(size, numbers.Number): | |||||
| self.size = (int(size), int(size)) | |||||
| else: | |||||
| self.size = size | |||||
| self.padding = padding | |||||
| self.pad_if_needed = pad_if_needed | |||||
| self.fill = fill | |||||
| self.padding_mode = padding_mode | |||||
| @staticmethod | |||||
| def get_params(img, output_size): | |||||
| """Get parameters for ``crop`` for a random crop. | |||||
| Args: | |||||
| img (PIL Image): Image to be cropped. | |||||
| output_size (tuple): Expected output size of the crop. | |||||
| Returns: | |||||
| tuple: params (i, j, h, w) to be passed to ``crop`` for random crop. | |||||
| """ | |||||
| w, h = img.size | |||||
| th, tw = output_size | |||||
| if w == tw and h == th: | |||||
| return 0, 0, h, w | |||||
| #i = torch.randint(0, h - th + 1, size=(1, )).item() | |||||
| #j = torch.randint(0, w - tw + 1, size=(1, )).item() | |||||
| i = random.randint(0, h - th) | |||||
| j = random.randint(0, w - tw) | |||||
| return i, j, th, tw | |||||
| def __call__(self, data): | |||||
| """ | |||||
| Args: | |||||
| img (PIL Image): Image to be cropped. | |||||
| Returns: | |||||
| PIL Image: Cropped image. | |||||
| """ | |||||
| img, label = data['image'], data['label'] | |||||
| if self.padding is not None: | |||||
| img = F.pad(img, self.padding, self.fill, self.padding_mode) | |||||
| label = F.pad(label, self.padding, self.fill, self.padding_mode) | |||||
| # pad the width if needed | |||||
| if self.pad_if_needed and img.size[0] < self.size[1]: | |||||
| img = F.pad(img, (self.size[1] - img.size[0], 0), self.fill, self.padding_mode) | |||||
| label = F.pad(label, (self.size[1] - label.size[0], 0), self.fill, self.padding_mode) | |||||
| # pad the height if needed | |||||
| if self.pad_if_needed and img.size[1] < self.size[0]: | |||||
| img = F.pad(img, (0, self.size[0] - img.size[1]), self.fill, self.padding_mode) | |||||
| label = F.pad(label, (0, self.size[0] - img.size[1]), self.fill, self.padding_mode) | |||||
| i, j, h, w = self.get_params(img, self.size) | |||||
| img = F.crop(img, i, j ,h ,w) | |||||
| label = F.crop(label, i, j, h, w) | |||||
| return {"image": img, "label": label} | |||||
| class Normalization(object): | |||||
| def __init__(self, mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]): | |||||
| self.mean = mean | |||||
| self.std = std | |||||
| def __call__(self, sample): | |||||
| image, label = sample['image'], sample['label'] | |||||
| image = F.normalize(image, self.mean, self.std) | |||||
| return {'image': image, 'label': label} | |||||