├── medmnistc ├── __init__.py ├── corruptions │ ├── __init__.py │ ├── __pycache__ │ │ ├── base.cpython-311.pyc │ │ ├── noise.cpython-311.pyc │ │ ├── __init__.cpython-311.pyc │ │ └── registry.cpython-311.pyc │ ├── base.py │ ├── compression.py │ ├── noise.py │ ├── enhance.py │ ├── filter.py │ ├── microscopy.py │ └── registry.py ├── assets │ └── inks.npz ├── __pycache__ │ └── __init__.cpython-311.pyc ├── utils │ ├── utils.py │ └── baselines.py ├── augmentation.py ├── dataset.py ├── dataset_manager.py ├── eval.py └── visualizer.py ├── .gitignore ├── assets ├── images │ └── wallpaper.gif └── examples │ ├── create_dataset.ipynb │ ├── evaluation.ipynb │ └── augment.ipynb ├── requirements.txt ├── pyproject.toml ├── README.md └── LICENSE /medmnistc/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /medmnistc/corruptions/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | build/ 4 | dist/ -------------------------------------------------------------------------------- /medmnistc/assets/inks.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/francescodisalvo05/medmnistc-api/HEAD/medmnistc/assets/inks.npz -------------------------------------------------------------------------------- /assets/images/wallpaper.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/francescodisalvo05/medmnistc-api/HEAD/assets/images/wallpaper.gif -------------------------------------------------------------------------------- /medmnistc/__pycache__/__init__.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/francescodisalvo05/medmnistc-api/HEAD/medmnistc/__pycache__/__init__.cpython-311.pyc -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | medmnist == 3.0.1 2 | scikit-image == 0.23.2 3 | scikit-learn > 1.2.2 4 | numpy 5 | torch 6 | torchvision 7 | opencv-python 8 | scipy 9 | wand > 0.6.10 -------------------------------------------------------------------------------- /medmnistc/corruptions/__pycache__/base.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/francescodisalvo05/medmnistc-api/HEAD/medmnistc/corruptions/__pycache__/base.cpython-311.pyc -------------------------------------------------------------------------------- /medmnistc/corruptions/__pycache__/noise.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/francescodisalvo05/medmnistc-api/HEAD/medmnistc/corruptions/__pycache__/noise.cpython-311.pyc -------------------------------------------------------------------------------- /medmnistc/corruptions/__pycache__/__init__.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/francescodisalvo05/medmnistc-api/HEAD/medmnistc/corruptions/__pycache__/__init__.cpython-311.pyc -------------------------------------------------------------------------------- /medmnistc/corruptions/__pycache__/registry.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/francescodisalvo05/medmnistc-api/HEAD/medmnistc/corruptions/__pycache__/registry.cpython-311.pyc -------------------------------------------------------------------------------- /medmnistc/utils/utils.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import random 3 | import torch 4 | import os 5 | 6 | 7 | def seed_everything(seed: int): 8 | random.seed(seed) 9 | os.environ['PYTHONHASHSEED'] = str(seed) 10 | np.random.seed(seed) 11 | torch.manual_seed(seed) 12 | torch.cuda.manual_seed(seed) 13 | torch.backends.cudnn.deterministic = True 14 | torch.backends.cudnn.benchmark = False 15 | rng = np.random.default_rng(seed) # rng will be used on skimage.util.random_noise 16 | return rng -------------------------------------------------------------------------------- /medmnistc/corruptions/base.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import cv2 3 | import os 4 | 5 | class BaseCorruption: 6 | def __init__(self, severity_params): 7 | self.severity_params = severity_params 8 | self.font = cv2.FONT_HERSHEY_DUPLEX 9 | self._inks = None 10 | 11 | @property 12 | def inks(self): 13 | if self._inks is None: 14 | # Get the directory of the current file (__file__ is the path to the current file) 15 | current_file_dir = os.path.dirname(os.path.realpath(__file__)) 16 | inks_path = os.path.join(current_file_dir, './../', 'assets', 'inks.npz') 17 | self._inks = np.load(inks_path, allow_pickle=True) 18 | return self._inks 19 | 20 | def apply(self, img): 21 | raise NotImplementedError("This method should be implemented by subclasses.") 22 | 23 | 24 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "medmnistc" 7 | authors = [ 8 | {name = "Francesco Di Salvo", email = "francesco.di-salvo@uni-bamberg.de"} 9 | ] 10 | description = "This Python library aims to evaluate model robustness under corrupted test sets and to enhance domain generalization through domain-specific augmentations." 11 | version = "0.1.0" 12 | readme = "README.md" 13 | requires-python = ">=3.7" 14 | dependencies = [ 15 | "medmnist == 3.0.1", 16 | "scikit-image == 0.23.2", 17 | "scikit-learn > 1.2.2", 18 | "numpy", 19 | "torch", 20 | "torchvision", 21 | "opencv-python", 22 | "scipy", 23 | "wand > 0.6.10" 24 | ] 25 | 26 | [project.urls] 27 | "Homepage" = "https://github.com/francescodisalvo05/medmnistc-api" 28 | "Issue Tracker" = "https://github.com/francescodisalvo05/medmnistc-api/issues" -------------------------------------------------------------------------------- /medmnistc/augmentation.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | class AugMedMNISTC(object): 5 | def __init__(self, 6 | train_corruptions : dict = {}, 7 | verbose: bool = False): 8 | """ 9 | Augmentation class based on the designed image corruptions. 10 | For each call, it will randomly choose *one* corruption (i.e., augmentation) using a 11 | uniformly sampled intensity hyperparameter in the range [min_intensity,max_intensity]. 12 | Notably, among the possible augmentations, we do include `identity` (i.e., no aug). 13 | 14 | :param train_corruptions: Dictionary containing the corruptions to use during training. 15 | :param verbose: If True, print the name of the selected corruption. 16 | """ 17 | assert len(train_corruptions) > 0, f"You need to define some corruptions firsts." 18 | 19 | self.verbose = verbose 20 | self.train_corruptions = train_corruptions 21 | self.train_corruptions_keys = list(self.train_corruptions.keys()) + ['identity'] 22 | 23 | 24 | def __call__(self, img): 25 | corr = np.random.choice(self.train_corruptions_keys) 26 | 27 | if self.verbose: 28 | print(corr) 29 | 30 | if corr == 'identity': 31 | return img 32 | 33 | return self.train_corruptions[corr].apply(img, augmentation=True) 34 | 35 | 36 | -------------------------------------------------------------------------------- /medmnistc/corruptions/compression.py: -------------------------------------------------------------------------------- 1 | from .base import BaseCorruption 2 | from PIL import Image 3 | from io import BytesIO 4 | 5 | import numpy as np 6 | 7 | 8 | class Pixelate(BaseCorruption): 9 | def apply(self, img, severity=-1, augmentation=False): 10 | 11 | if augmentation: 12 | range_min, range_max = self.severity_params[0], self.severity_params[-1] 13 | resize_factor = np.random.uniform(low=range_min, high=range_max, size=None) 14 | else: 15 | resize_factor = self.severity_params[severity] 16 | 17 | 18 | width, height = img.size 19 | img = img.resize((int(width * resize_factor), int(height * resize_factor)), Image.BOX) 20 | img = img.resize((width, height), Image.BOX) 21 | return np.array(img) 22 | 23 | 24 | class JPEGCompression(BaseCorruption): 25 | def apply(self, img, severity=-1, augmentation=False): 26 | 27 | if augmentation: 28 | range_min, range_max = self.severity_params[0], self.severity_params[-1] 29 | compression_quality = int(np.random.uniform(low=range_min, high=range_max, size=None)) 30 | else: 31 | compression_quality = self.severity_params[severity] 32 | 33 | output = BytesIO() 34 | img.save(output, 'JPEG', quality=compression_quality) 35 | img = Image.open(output) 36 | return np.array(img) -------------------------------------------------------------------------------- /medmnistc/corruptions/noise.py: -------------------------------------------------------------------------------- 1 | from .base import BaseCorruption 2 | 3 | import skimage as sk 4 | import numpy as np 5 | 6 | 7 | class GaussianNoise(BaseCorruption): 8 | def apply(self, img, severity=-1, augmentation=False): 9 | if augmentation: 10 | range_min, range_max = self.severity_params[0], self.severity_params[-1] 11 | c = np.random.uniform(low=range_min, high=range_max, size=None) 12 | else: 13 | c = self.severity_params[severity] 14 | 15 | img = np.array(img) / 255. 16 | noisy_image = np.clip(img + np.random.normal(size=img.shape, scale=c), 0, 1) 17 | return (noisy_image * 255).astype(np.uint8) 18 | 19 | 20 | class ImpulseNoise(BaseCorruption): 21 | def apply(self, img, severity=-1, augmentation=False): 22 | if augmentation: 23 | range_min, range_max = self.severity_params[0], self.severity_params[-1] 24 | c = np.random.uniform(low=range_min, high=range_max, size=None) 25 | noisy_image = sk.util.random_noise(np.array(img) / 255., mode='s&p', amount=c, rng=np.random.default_rng(99999)) 26 | else: 27 | c = self.severity_params[severity] 28 | noisy_image = sk.util.random_noise(np.array(img) / 255., mode='s&p', amount=c, rng=self.rng) 29 | noisy_image = np.clip(noisy_image, 0, 1) 30 | return (noisy_image * 255).astype(np.uint8) 31 | 32 | 33 | class SpeckleNoise(BaseCorruption): 34 | def apply(self, img, severity=-1, augmentation=False): 35 | if augmentation: 36 | range_min, range_max = self.severity_params[0], self.severity_params[-1] 37 | c = np.random.uniform(low=range_min, high=range_max, size=None) 38 | else: 39 | c = self.severity_params[severity] 40 | img = np.array(img) / 255. 41 | noise = np.random.normal(size=img.shape, scale=c) 42 | noisy_image = np.clip(img + img * noise, 0, 1) 43 | return (noisy_image * 255).astype(np.uint8) 44 | 45 | 46 | class ShotNoise(BaseCorruption): 47 | def apply(self, img, severity=-1, augmentation=False): 48 | if augmentation: 49 | range_min, range_max = self.severity_params[0], self.severity_params[-1] 50 | mult = np.random.uniform(low=range_min, high=range_max, size=None) 51 | else: 52 | mult = self.severity_params[severity] 53 | img = np.array(img) / 255. 54 | noisy_image = np.clip(np.random.poisson(img * mult) / mult, 0, 1) 55 | return (noisy_image * 255).astype(np.uint8) 56 | 57 | 58 | -------------------------------------------------------------------------------- /medmnistc/corruptions/enhance.py: -------------------------------------------------------------------------------- 1 | from .base import BaseCorruption 2 | from PIL import ImageEnhance 3 | import skimage as sk 4 | import numpy as np 5 | 6 | import torchvision.transforms.functional as TF 7 | 8 | 9 | class Brightness(BaseCorruption): 10 | def apply(self, img, severity=-1, augmentation=False): 11 | if augmentation: 12 | range_min, range_max = self.severity_params[0], self.severity_params[-1] 13 | brightness_factor = np.random.uniform(low=range_min, high=range_max, size=None) 14 | else: 15 | brightness_factor = self.severity_params[severity] 16 | enhancer = ImageEnhance.Brightness(img) 17 | brightened_img = enhancer.enhance(brightness_factor) 18 | return np.array(brightened_img).astype(np.uint8) 19 | 20 | 21 | class Contrast(BaseCorruption): 22 | def apply(self, img, severity=-1, augmentation=False): 23 | if augmentation: 24 | range_min, range_max = self.severity_params[0], self.severity_params[-1] 25 | contrast_factor = np.random.uniform(low=range_min, high=range_max, size=None) 26 | else: 27 | contrast_factor = self.severity_params[severity] 28 | enhancer = ImageEnhance.Contrast(img) 29 | contrasted_img = enhancer.enhance(contrast_factor) 30 | return np.array(contrasted_img).astype(np.uint8) 31 | 32 | 33 | class GammaCorrection(BaseCorruption): 34 | def apply(self, img, severity=-1, augmentation=False): 35 | if augmentation: 36 | range_min, range_max = self.severity_params[0], self.severity_params[-1] 37 | correction_factor = np.random.uniform(low=range_min, high=range_max, size=None) 38 | else: 39 | correction_factor = self.severity_params[severity] 40 | 41 | img = TF.adjust_gamma(img, correction_factor, gain=1) 42 | return np.array(img).astype(np.uint8) 43 | 44 | 45 | class Saturate(BaseCorruption): 46 | def apply(self, img, severity=-1, augmentation=False): 47 | if augmentation: 48 | range_min, range_max = self.severity_params[0], self.severity_params[-1] 49 | saturation_factor = np.random.uniform(low=range_min, high=range_max, size=None) 50 | else: 51 | saturation_factor = self.severity_params[severity] 52 | 53 | img = np.array(img) / 255. 54 | img = sk.color.rgb2hsv(img) 55 | img[:, :, 1] = np.clip(img[:, :, 1] + saturation_factor, 0, 1) 56 | img = sk.color.hsv2rgb(img) 57 | img = np.clip(img, 0, 1) * 255 58 | 59 | return img.astype(np.uint8) 60 | 61 | -------------------------------------------------------------------------------- /medmnistc/dataset.py: -------------------------------------------------------------------------------- 1 | from torchvision import transforms 2 | from torch.utils.data import Dataset 3 | from PIL import Image 4 | 5 | import numpy as np 6 | import os 7 | 8 | 9 | class CorruptedMedMNIST(Dataset): 10 | def __init__(self, 11 | dataset_name : str, 12 | corruption : str, 13 | norm_mean : list = [0.5], 14 | norm_std : list = [0.5], 15 | root : str = None, 16 | as_rgb : bool = True, 17 | mmap_mode : str = None): 18 | """ 19 | Dataset class of CorruptedMedMNIST 20 | 21 | :param dataset_name: Name of the reference medmnist dataset. 22 | :param corruption: Name of the desired corruption. 23 | :param norm_mean: Normalization mean. 24 | :param norm_std: Normalization standard deviation. 25 | :param root: Root path of the generated corrupted data. 26 | :param as_rgb: Flag for RGB of Greyscale data. 27 | :param mmap_mode: Memory mapping of the file: {None, ‘r+’, ‘r’, ‘w+’, ‘c’}. 28 | If not None, then memory-map the file, using the given mode 29 | (see numpy.memmap for a detailed description of the modes). 30 | Memory mapping is especially useful for accessing small 31 | fragments of large files without reading the entire file into memory. 32 | src: https://numpy.org/doc/stable/reference/generated/numpy.load.html 33 | 34 | This dataset class was greatly inspired from the MedMNIST APIs: 35 | https://github.com/MedMNIST/MedMNIST 36 | """ 37 | 38 | super(CorruptedMedMNIST, self).__init__() 39 | 40 | self.dataset_name = dataset_name 41 | self.corruption = corruption 42 | self.root = root 43 | self.as_rgb = as_rgb 44 | 45 | if root is not None and os.path.exists(root): 46 | self.root = root 47 | else: 48 | raise RuntimeError( 49 | "Failed to setup the default `root` directory. " 50 | + "Please specify and create the `root` directory manually." 51 | ) 52 | 53 | if not os.path.exists(os.path.join(self.root, self.dataset_name, f"{corruption}.npz")): 54 | print(os.path.join(self.root, self.dataset_name, f"{corruption}.npz")) 55 | raise RuntimeError( 56 | "Dataset not found." 57 | ) 58 | 59 | npz_file = np.load( 60 | os.path.join(self.root, self.dataset_name, f"{corruption}.npz"), 61 | mmap_mode=mmap_mode, 62 | ) 63 | 64 | self.imgs = npz_file["test_images"] 65 | self.labels = npz_file["test_labels"] 66 | self.transform = transforms.Compose([ 67 | transforms.ToTensor(), 68 | transforms.Normalize(mean=norm_mean, std=norm_std) 69 | ]) 70 | 71 | 72 | def __len__(self): 73 | return self.imgs.shape[0] 74 | 75 | 76 | def __getitem__(self, index): 77 | img, target = self.imgs[index], self.labels[index].astype(int) 78 | img = Image.fromarray(img) 79 | 80 | if self.as_rgb: 81 | img = img.convert("RGB") 82 | 83 | if self.transform is not None: 84 | img = self.transform(img) 85 | 86 | return img, target -------------------------------------------------------------------------------- /medmnistc/corruptions/filter.py: -------------------------------------------------------------------------------- 1 | from .base import BaseCorruption 2 | 3 | from scipy.ndimage import zoom as scizoom 4 | from wand.image import Image as WandImage 5 | from wand.api import library as wandlibrary 6 | from io import BytesIO 7 | 8 | import torchvision.transforms.functional as TF 9 | import numpy as np 10 | import cv2 11 | 12 | 13 | def disk(radius, alias_blur=0.1, dtype=np.float32): 14 | if radius <= 8: 15 | L = np.arange(-8, 8 + 1) 16 | ksize = (3, 3) 17 | else: 18 | L = np.arange(-radius, radius + 1) 19 | ksize = (5, 5) 20 | X, Y = np.meshgrid(L, L) 21 | aliased_disk = np.array((X ** 2 + Y ** 2) <= radius ** 2, dtype=dtype) 22 | aliased_disk /= np.sum(aliased_disk) 23 | return cv2.GaussianBlur(aliased_disk, ksize=ksize, sigmaX=alias_blur) 24 | 25 | 26 | def clipped_zoom(img, zoom_factor): 27 | h = img.shape[0] 28 | ch = int(np.ceil(h / zoom_factor)) 29 | 30 | top = (h - ch) // 2 31 | img = scizoom(img[top:top + ch, top:top + ch], (zoom_factor, zoom_factor, 1), order=1) 32 | trim_top = (img.shape[0] - h) // 2 33 | 34 | return img[trim_top:trim_top + h, trim_top:trim_top + h] 35 | 36 | 37 | 38 | class MotionImage(WandImage): 39 | def motion_blur(self, radius=0.0, sigma=0.0, angle=0.0): 40 | wandlibrary.MagickMotionBlurImage(self.wand, radius, sigma, angle) 41 | 42 | 43 | class GaussianBlur(BaseCorruption): 44 | def apply(self, img, severity=-1, augmentation=False): 45 | if augmentation: 46 | kernel_min, kernel_max = self.severity_params[0], self.severity_params[-1] 47 | kernel = int(np.random.uniform(low=kernel_min, high=kernel_max, size=None)) 48 | if kernel % 2 == 0: # it must be odd 49 | kernel -= 1 50 | else: 51 | kernel = self.severity_params[severity] 52 | img = TF.gaussian_blur(img, kernel_size=kernel) 53 | return np.array(img).astype(np.uint8) 54 | 55 | 56 | class MotionBlur(BaseCorruption): 57 | def apply(self, img, severity=-1, augmentation=False): 58 | if augmentation: 59 | radius_min, radius_max = self.severity_params[0][0], self.severity_params[-1][0] 60 | sigma_min, sigma_max = self.severity_params[0][1], self.severity_params[-1][1] 61 | radius = np.random.uniform(low=radius_min, high=radius_max, size=None) 62 | sigma = np.random.uniform(low=sigma_min, high=sigma_max, size=None) 63 | else: 64 | radius_sigma = self.severity_params[severity] 65 | radius, sigma = radius_sigma 66 | 67 | output = BytesIO() 68 | img.save(output, format='PNG') 69 | img = MotionImage(blob=output.getvalue()) 70 | 71 | img.motion_blur(radius=radius, sigma=sigma, angle=np.random.uniform(-45, 45)) 72 | img = cv2.imdecode(np.fromstring(img.make_blob(), np.uint8), 73 | cv2.IMREAD_UNCHANGED) 74 | 75 | output.close() 76 | 77 | if img.shape != (224, 224): 78 | return np.clip(img[..., [2, 1, 0]], 0, 255).astype(np.uint8) # BGR to RGB 79 | else: # greyscale to RGB 80 | return np.clip(np.array([img, img, img]).transpose((1, 2, 0)), 0, 255).astype(np.uint8) 81 | 82 | 83 | class DefocusBlur(BaseCorruption): 84 | def apply(self, img, severity=-1, augmentation=False): 85 | 86 | if augmentation: 87 | radius_min, radius_max = self.severity_params[0][0], self.severity_params[-1][0] 88 | alias_min, alias_max = self.severity_params[0][1], self.severity_params[-1][1] 89 | radius = np.random.uniform(low=radius_min, high=radius_max, size=None) 90 | alias = np.random.uniform(low=alias_min, high=alias_max, size=None) 91 | else: 92 | radius_alias = self.severity_params[severity] 93 | radius, alias = radius_alias 94 | 95 | img = np.array(img) / 255. 96 | kernel = disk(radius=radius, alias_blur=alias) 97 | 98 | channels = [] 99 | for d in range(3): 100 | channels.append(cv2.filter2D(img[:, :, d], -1, kernel)) 101 | 102 | channels = np.array(channels).transpose((1, 2, 0)) # 3x224x224 -> 224x224x3 103 | out = np.clip(channels, 0, 1) * 255 104 | 105 | return out.astype(np.uint8) 106 | 107 | 108 | class ZoomBlur(BaseCorruption): 109 | def apply(self, img, severity=-1, augmentation=False): 110 | 111 | if augmentation: # hard code 112 | min_factor, max_factor, = 1.0, self.severity_params[-1][-1] 113 | min_step = self.severity_params[0][1] - self.severity_params[0][0] 114 | max_step = self.severity_params[-1][1] - self.severity_params[-1][0] 115 | max_factor_sampled = np.random.uniform(low=min_factor, high=max_factor, size=None) 116 | step = np.random.uniform(low=min_step, high=max_step, size=None) 117 | zoom_factors = np.arange(min_factor, max_factor_sampled, step) 118 | else: 119 | zoom_factors = self.severity_params[severity] 120 | 121 | img = (np.array(img) / 255.).astype(np.float32) 122 | out = np.zeros_like(img) 123 | for zoom_factor in zoom_factors: 124 | out += clipped_zoom(img, zoom_factor) 125 | 126 | img = (img + out) / (len(zoom_factors) + 1) 127 | img = np.clip(img, 0, 1) * 255 128 | 129 | return img.astype(np.uint8) -------------------------------------------------------------------------------- /medmnistc/dataset_manager.py: -------------------------------------------------------------------------------- 1 | from medmnistc.corruptions.registry import CORRUPTIONS_DS, DATASET_RGB 2 | from medmnistc.utils.utils import seed_everything 3 | from medmnist import INFO 4 | from PIL import Image 5 | import numpy as np 6 | import os 7 | 8 | from tqdm import tqdm 9 | 10 | 11 | class DatasetManager: 12 | def __init__(self, 13 | medmnist_path: str, 14 | output_path: str, 15 | random_seed : int = 0): 16 | """ 17 | Class used to create the corrupted test sets. 18 | Speficially, it will create one `npz` file for each designed dataset-corruption. 19 | :param medmnist_path: Path to the medmnist datasets. Pre-download the 224 version. 20 | https://medmnist.com/ 21 | :param output_path: Path to the output folder of the `medmnistc` dataset. 22 | Path convention: {output_folder} / {dataset} / {corruption}.npz 23 | :param random_seed: Control stochastic process and ensure reproducibility. 24 | """ 25 | self.medmnist_path = medmnist_path 26 | self.output_path = output_path 27 | 28 | self.supported_datasets = [ 29 | 'bloodmnist', 'breastmnist', 'chestmnist', 'dermamnist', 30 | 'octmnist', 'organamnist', 'organcmnist', 'organsmnist', 31 | 'pathmnist', 'pneumoniamnist', 'retinamnist', 'tissuemnist' 32 | ] 33 | 34 | self.random_seed = random_seed 35 | 36 | 37 | 38 | 39 | def create_dataset(self, dataset_name: str): 40 | """ 41 | Create the corrupted dataset(s). 42 | 43 | :param dataset_name: Name of the dataset to corrupt. 44 | Options: {'all', 45 | 'bloodmnist', 'breastmnist', 'chestmnist', 'dermamnist', 46 | 'octmnist', 'organamnist', 'organcmnist', 'organsmnist', 47 | 'pathmnist', 'pneumoniamnist', 'retinamnist', 'tissuemnist'} 48 | If `all` is set, it will create create all the corrupted datasets. 49 | """ 50 | 51 | # Create all the corrupted datasets 52 | if dataset_name == 'all': 53 | for ds in self.supported_datasets: 54 | self.create_single_dataset(dataset_name=ds) 55 | 56 | # Create only the chosen corrupted dataset 57 | else: 58 | dataset_name = dataset_name.lower() 59 | assert dataset_name in self.supported_datasets, f"Dataset not found. Please choose one among : {self.supported_datasets}" 60 | self.create_single_dataset(dataset_name=dataset_name) 61 | 62 | 63 | def create_single_dataset(self, dataset_name: str): 64 | """ 65 | Generate one corrupted version for the required dataset. 66 | Note that we store one .npz file for each designed corruptions. 67 | 68 | :param dataset_name: Name of the dataset to corrupt. 69 | """ 70 | print(f"=========== {dataset_name} ===========") 71 | 72 | rng = seed_everything(self.random_seed) 73 | 74 | # Get the designed corruptions 75 | corruptions = CORRUPTIONS_DS[dataset_name] 76 | 77 | # Get MedMNIST's dataset class 78 | # It required the pre-download of the 224 datasets 79 | info = INFO[dataset_name] 80 | DatasetClass = getattr(__import__('medmnist', fromlist=[info['python_class']]), info['python_class']) 81 | 82 | dataset = DatasetClass( split = "test", 83 | as_rgb = True, 84 | download = False, 85 | transform = None, 86 | size = 224, 87 | root = self.medmnist_path) 88 | 89 | dataset_path = os.path.join(self.output_path,dataset_name) 90 | os.makedirs(dataset_path, exist_ok=True) 91 | 92 | # Create the corrupted datasets 93 | # NOTE: This could be computationally heavy (RAM-wise) for large datasets (e.g. TissueMNIST), 94 | # as it multiply 5 times the test set. 95 | dataset_c, labels = [], [] 96 | 97 | for (corruption,corruptor) in corruptions.items(): 98 | 99 | print(f'Starting {corruption}...') 100 | 101 | # Load the corrupted images and relative labels into lists 102 | dataset_c, labels = [], [] 103 | 104 | if corruption == "impulse_noise": 105 | corruptor.rng = rng #skimage.. 106 | 107 | # By design, we have 5 intensity levels 108 | for severity in range(0,5): 109 | 110 | # Define corruptor method 111 | lam_corruption = lambda img : corruptor.apply(img, severity) 112 | 113 | # Iterate over the MedMNIST dataset and apply corruptions 114 | for img_idx in tqdm(range(len(dataset.imgs)), f'Severity {str(severity+1).zfill(2)}'): 115 | 116 | img, label = dataset.imgs[img_idx], dataset.labels[img_idx] 117 | 118 | # The defined corruptions support RGB images 119 | img = Image.fromarray(img).convert('RGB') 120 | corrupted_img = lam_corruption(img) 121 | 122 | # Convert to greyscale, if required 123 | if not DATASET_RGB[dataset_name]: 124 | corrupted_img = Image.fromarray(corrupted_img).convert('L') 125 | 126 | np_corrupted = np.array(corrupted_img) 127 | assert np.min(np_corrupted) >= 0 or np.max(np_corrupted) <= 255, f"(min,max) = {(np.min(np_corrupted),np.max(np_corrupted))}" 128 | assert np_corrupted.dtype == np.uint8, f"{np_corrupted.dtype}" 129 | 130 | dataset_c.append(np_corrupted) 131 | labels.append(label) 132 | 133 | filepath = os.path.join(dataset_path,f'{corruption}.npz') 134 | np.savez_compressed(filepath, test_images=dataset_c, test_labels=labels) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🏥 MedMNIST-C 2 | 3 | We introduce MedMNIST-C [[preprint](https://arxiv.org/pdf/2406.17536)], a `benchmark dataset` based on the MedMNIST+ collection covering `12 2D datasets and 9 imaging modalities`. We simulate task and modality-specific image corruptions of varying severity to comprehensively evaluate the robustness of established algorithms against `real-world artifacts` and `distribution shifts`. We further show that our simple-to-use artificial corruptions allow for highly performant, lightweight `data augmentation` to enhance model robustness. 4 | 5 |

6 | Preview of image corruptions 7 |

8 | 9 | > You can download the corrupted datasets from [Zenodo](https://zenodo.org/records/11471504). 10 | Due to space constraints, we have uploaded all datasets except for TissueMNIST-C. However, You can still reproduce it using our APIs. 11 | 12 | ## Installation and Requirements 13 | 14 | ``` 15 | pip install medmnistc 16 | ``` 17 | 18 | We do require [Wand](https://docs.wand-py.org/en/latest/guide/install.html) for image manipulation, a Python binding for [ImageMagick](https://imagemagick.org/index.php). Thus, if you are using Ubuntu: 19 | 20 | ``` 21 | sudo apt-get install libmagickwand-dev 22 | ``` 23 | 24 | otherwise, please check the [tutorial](https://docs.wand-py.org/en/0.2.4/guide/install.html). 25 | 26 | ## Main components 27 | 28 | * `medmnistc/corruptions/registry.py`: List of all the corruptions and respective intensity hyperparameters. 29 | * `medmnistc/dataset_manager.py`: Dataset class responsible for the creation of the corrupted datasets. 30 | * `medmnistc/visualizer.py`: Class used to visualize and store the defined corruptions. 31 | * `medmnistc/augmentation.py`: Augumentation class based on the defined corruptions. 32 | * `medmnistc/dataset.py`: Dataset class used for the corrupted datasets. 33 | * `medmnistc/eval.py`: PyTorch class used for model evaluation under corrupted datasets. 34 | * `medmnistc/assets/baseline/*`: Normalization baselines used for model evaluation under corrupted datasets. 35 | 36 | ## Basic usage 37 | 38 | ### Create the corrupted datasets 39 | ```python 40 | from medmnistc.dataset_manager import DatasetManager 41 | 42 | medmnist_path = ... # PATH TO THE CLEAN IMAGES 43 | medmnistc_path = ... # PATH TO THE CORRUPTED IMAGES 44 | 45 | ds_manager = DatasetManager(medmnist_path = medmnist_path, output_path=output_path) 46 | ds_manager.create_dataset(dataset_name = "breastmnist") # create a single corrupted test set 47 | ds_manager.create_dataset(dataset_name = "all") # create all 48 | ``` 49 | 50 | ### Augmentations 51 | ```python 52 | from medmnistc.augmentation import AugMedMNISTC 53 | from medmnistc.corruptions.registry import CORRUPTIONS_DS 54 | import torchvision.transforms as transforms 55 | 56 | dataset = "breastmnist" # select dataset 57 | train_corruptions = CORRUPTIONS_DS[dataset] # load the designed corruptions for this dataset 58 | images = ... # load images 59 | 60 | # Augment with AugMedMNISTC 61 | augment = AugMedMNISTC(train_corruptions) 62 | augmented_img = augment(images[0]) 63 | 64 | # Integrate into transforms.Compose 65 | aug_compose = transforms.Compose([ 66 | AugMedMNISTC(train_corruptions), 67 | transforms.ToTensor(), 68 | transforms.Normalize(mean=..., std=...) 69 | ]) 70 | 71 | augmented_img = aug_compose(images[0]) 72 | ``` 73 | 74 | ### Notebooks 75 | 76 | * [Create the dataset](assets/examples/create_dataset.ipynb) 77 | * [Visualize the corruptions](assets/examples/visualize.ipynb) 78 | * [Evaluate the corruptions](assets/examples/evaluation.ipynb) 79 | * [Use the designed augmentations](assets/examples/augment.ipynb) 80 | 81 | ## Papers using MedMNIST-C 82 | 83 | | **Authors** | **Paper** | **Venue** | 84 | | ------------- | ------------- | ------------- | 85 | | Kuhn et al. | An autonomous agent for auditing and improving the reliability of clinical AI models | [ArXiv'25](https://arxiv.org/pdf/2507.05755) | 86 | | Manzari et al. | Medical image classification with kan-integrated transformers and dilated neighborhood attention | [ArXiv'25](https://arxiv.org/abs/2502.13693) | 87 | | Imam et al. | On the Robustness of Medical Vision-Language Models: Are they Truly Generalizable? | [MIUA'25](https://arxiv.org/abs/2505.15425) | 88 | | Zeevi et al. | Rate-In: Information-Driven Adaptive Dropout Rates for Improved Inference-Time Uncertainty Estimation | [CVPR'25](https://openaccess.thecvf.com/content/CVPR2025/papers/Zeevi_Rate-In_Information-Driven_Adaptive_Dropout_Rates_for_Improved_Inference-Time_Uncertainty_Estimation_CVPR_2025_paper.pdf) | 89 | | Hekler et al. | Beyond Overconfidence: Foundation Models Redefine Calibration in Deep Neural Networks | [ArXiv'25](https://www.arxiv.org/abs/2506.09593) | 90 | | Abhishek et al. | Investigating the Quality of DermaMNIST and Fitzpatrick17k Dermatological Image Datasets | [Scientific Data'25](https://www.nature.com/articles/s41597-025-04382-5) | 91 | | Singh et al. | Dynamic Filter Application in Graph Convolutional Networks for Enhanced Spectral Feature Analysis and Class Discrimination in Medical Imaging | [IEEE Access'24](https://ieeexplore.ieee.org/document/10637462) | 92 | 93 | ## License 94 | 95 | The code is under [Apache-2.0 License](./LICENSE). 96 | 97 | The MedMNIST-C dataset is licensed under Creative Commons Attribution 4.0 International ([CC BY 4.0](https://creativecommons.org/licenses/by/4.0/)), except DermaMNIST-C under Creative Commons Attribution-NonCommercial 4.0 International ([CC BY-NC 4.0](https://creativecommons.org/licenses/by-nc/4.0/)). 98 | 99 | ## Citation 100 | 101 | If you find this work useful, please consider citing us: 102 | ``` 103 | @misc{disalvo2024medmnistc, 104 | title={MedMNIST-C: Comprehensive benchmark and improved classifier robustness by simulating realistic image corruptions}, 105 | author={Francesco Di Salvo and Sebastian Doerrich and Christian Ledig}, 106 | year={2024}, 107 | eprint={2406.17536}, 108 | archivePrefix={arXiv}, 109 | primaryClass={eess.IV}, 110 | url={https://arxiv.org/abs/2406.17536}, 111 | } 112 | ``` 113 | 114 | `DISCLAIMER`: This repository is inspired by MedMNIST APIs and the ImageNet-C repository. Thus, please also consider citing [MedMNIST](https://www.nature.com/articles/s41597-022-01721-8), the respective source datasets (described [here](https://medmnist.com/)) and [ImageNet-C](https://arxiv.org/abs/1903.12261). 115 | 116 | ## Release versions 117 | 118 | * `v0.1.0`: MedMNIST-C beta release. 119 | -------------------------------------------------------------------------------- /medmnistc/corruptions/microscopy.py: -------------------------------------------------------------------------------- 1 | from .base import BaseCorruption 2 | from PIL import Image, ImageDraw 3 | 4 | import numpy as np 5 | import random 6 | import cv2 7 | import string 8 | import random 9 | 10 | 11 | class StainDeposit(BaseCorruption): 12 | def apply(self, img, severity=-1, augmentation=False): 13 | 14 | if augmentation: 15 | range_min, range_max = self.severity_params[0], self.severity_params[-1] 16 | max_marks = int(np.random.uniform(low=range_min, high=range_max, size=None)) 17 | else: 18 | max_marks = self.severity_params[severity] 19 | 20 | 21 | x = np.array(img) 22 | img_w, img_h = x.shape[1], x.shape[0] 23 | 24 | if max_marks > 1: 25 | num_marks = random.randint(1,max_marks) 26 | else: 27 | num_marks = max_marks 28 | 29 | inks = self.inks[str(max(3,severity))] # size 30 | 31 | for _ in range(num_marks): 32 | 33 | ink = inks[random.randint(0,len(inks)-1)] 34 | ink_height, ink_width = ink.shape 35 | rand_x = random.randint(10,img_w - ink_width - 10) 36 | rand_y = random.randint(10,img_h - ink_height - 10) 37 | 38 | for idx in range(3): #channels 39 | x[rand_y: rand_y + ink_height, rand_x:rand_x + ink_width,idx] *= (1-ink) # black 40 | 41 | return np.clip(x, 0, 255).astype(np.uint8) 42 | 43 | 44 | class Bubble(BaseCorruption): 45 | def apply(self, img, severity=-1, augmentation=False): 46 | 47 | if augmentation: 48 | range_min_rad, range_max_rad = self.severity_params[0][0], self.severity_params[-1][0] 49 | range_min_bub, range_max_bub = self.severity_params[0][1], self.severity_params[-1][1] 50 | max_radius = int(np.random.uniform(low=range_min_rad, high=range_max_rad, size=None)) 51 | max_bubbles = int(np.random.uniform(low=range_min_bub, high=range_max_bub, size=None)) 52 | else: 53 | maxradius_bubbles = self.severity_params[severity] 54 | max_radius, max_bubbles = maxradius_bubbles 55 | 56 | height, width = img.size 57 | output_image = img.copy() 58 | 59 | # create a new image for the bubbles with the same dimensions as the original image 60 | # and transparent background (RGBA mode) 61 | bubbles_image = Image.new('RGBA', output_image.size, (255, 255, 255, 0)) 62 | # create a drawing context for the bubble image 63 | draw = ImageDraw.Draw(bubbles_image) 64 | # define border effect of the bubble 65 | border = 2 66 | 67 | num_bubbles = random.randint(7,max_bubbles) 68 | 69 | # draw several bubbles 70 | for _ in range(num_bubbles): 71 | radius = random.randint(3, max_radius) # random radius 72 | x, y = random.randint(radius, width - radius), random.randint(radius, height - radius) 73 | alpha = 100 # transparency 74 | draw.ellipse((x - radius - border, y - radius - border, x + radius + border, y + radius + border), fill=(255, 255, 255, alpha + 30)) 75 | draw.ellipse((x - radius, y - radius, x + radius, y + radius), fill=(255, 255, 255, alpha)) 76 | 77 | # overlay the bubbles onto the original image 78 | output_image.paste(bubbles_image, (0, 0), bubbles_image) 79 | return np.array(output_image) 80 | 81 | 82 | class BlackCorner(BaseCorruption): 83 | def apply(self, img, severity=-1, augmentation=False): 84 | if augmentation: 85 | range_min, range_max = self.severity_params[0], self.severity_params[-1] 86 | multiplier = np.random.uniform(low=range_min, high=range_max, size=None) 87 | else: 88 | multiplier = self.severity_params[severity] 89 | 90 | width, height = img.size 91 | img = np.array(img) 92 | 93 | center = (width // 2, height // 2) 94 | 95 | # default radius 96 | radius = min(center[0], center[1], width - center[0], height - center[1]) 97 | # adjust radius based on the severity / aug 98 | circle = np.zeros((height, width), np.uint8) 99 | cv2.circle(circle, center, int(radius * multiplier), (255), thickness=-1) 100 | mask = circle == 255 101 | img[~mask] = 0 102 | 103 | return img 104 | 105 | 106 | class Characters(BaseCorruption): 107 | def apply(self, img, severity=-1, augmentation=False): 108 | 109 | if augmentation: 110 | 111 | range_min_w, range_max_w = self.severity_params[0][0], self.severity_params[-1][0] 112 | range_min_l, range_max_l = self.severity_params[0][1], self.severity_params[-1][1] 113 | range_min_fs, range_max_fs = self.severity_params[0][2], self.severity_params[-1][2] 114 | max_words = int(np.random.uniform(low=range_min_w, high=range_max_w, size=None)) 115 | max_letters = int(np.random.uniform(low=range_min_l, high=range_max_l, size=None)) 116 | max_font_scale = np.random.uniform(low=range_min_fs, high=range_max_fs, size=None) 117 | 118 | else: 119 | 120 | c = self.severity_params[severity] 121 | max_words, max_letters, max_font_scale = c 122 | 123 | num_words = random.randint(1,max_words) 124 | 125 | for _ in range(num_words): 126 | 127 | num_letters = random.randint(3,max_letters) 128 | font_scale = random.randint(14,int(max_font_scale * 100)) / 100. 129 | 130 | letters = string.ascii_lowercase 131 | random_str = ''.join(random.choice(letters) for _ in range(num_letters)) 132 | 133 | img = np.array(img) 134 | 135 | width, height = img.shape[1], img.shape[0] 136 | 137 | # randomly sample the position of the string with respect to the image 138 | # org = (x,y) represents the bottom left corner 139 | rand_x = random.randint(10,width - (8 * num_letters)) 140 | rand_y = random.randint(10,height - 10) 141 | org = (rand_x,rand_y) 142 | 143 | # black character 144 | color = self.random_color() 145 | thickness = 1 146 | 147 | img = cv2.putText(img, random_str, org, self.font, 148 | font_scale, color, thickness, cv2.LINE_AA) 149 | 150 | return img 151 | 152 | 153 | def random_color(self): 154 | red = random.randint(0, 255) 155 | green = random.randint(0, 255) 156 | blue = random.randint(0, 255) 157 | return (red, green, blue) -------------------------------------------------------------------------------- /assets/examples/create_dataset.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "from medmnistc.dataset_manager import DatasetManager\n", 10 | "\n", 11 | "import numpy as np" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": 2, 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "medmnist_path = \"/mnt/data/datasets/medmnist\" # PATH TO THE CLEAN IMAGES\n", 21 | "output_path = \"/mnt/data/datasets/medmnistc-tmp\" # PATH TO THE CORRUPTED IMAGES" 22 | ] 23 | }, 24 | { 25 | "cell_type": "markdown", 26 | "metadata": {}, 27 | "source": [ 28 | "### Create one dataset" 29 | ] 30 | }, 31 | { 32 | "cell_type": "code", 33 | "execution_count": 4, 34 | "metadata": {}, 35 | "outputs": [ 36 | { 37 | "name": "stdout", 38 | "output_type": "stream", 39 | "text": [ 40 | "=========== breastmnist ===========\n", 41 | "Starting pixelate...\n" 42 | ] 43 | }, 44 | { 45 | "name": "stderr", 46 | "output_type": "stream", 47 | "text": [ 48 | "Severity 01: 100%|██████████| 156/156 [00:00<00:00, 3096.10it/s]\n", 49 | "Severity 02: 100%|██████████| 156/156 [00:00<00:00, 3244.26it/s]\n", 50 | "Severity 03: 100%|██████████| 156/156 [00:00<00:00, 3382.45it/s]\n", 51 | "Severity 04: 100%|██████████| 156/156 [00:00<00:00, 3501.48it/s]\n", 52 | "Severity 05: 100%|██████████| 156/156 [00:00<00:00, 3726.08it/s]\n" 53 | ] 54 | }, 55 | { 56 | "name": "stdout", 57 | "output_type": "stream", 58 | "text": [ 59 | "Starting jpeg_compression...\n" 60 | ] 61 | }, 62 | { 63 | "name": "stderr", 64 | "output_type": "stream", 65 | "text": [ 66 | "Severity 01: 100%|██████████| 156/156 [00:00<00:00, 1731.67it/s]\n", 67 | "Severity 02: 100%|██████████| 156/156 [00:00<00:00, 2852.85it/s]\n", 68 | "Severity 03: 100%|██████████| 156/156 [00:00<00:00, 3198.72it/s]\n", 69 | "Severity 04: 100%|██████████| 156/156 [00:00<00:00, 3380.25it/s]\n", 70 | "Severity 05: 100%|██████████| 156/156 [00:00<00:00, 3404.72it/s]\n" 71 | ] 72 | }, 73 | { 74 | "name": "stdout", 75 | "output_type": "stream", 76 | "text": [ 77 | "Starting speckle_noise...\n" 78 | ] 79 | }, 80 | { 81 | "name": "stderr", 82 | "output_type": "stream", 83 | "text": [ 84 | "Severity 01: 100%|██████████| 156/156 [00:00<00:00, 450.27it/s]\n", 85 | "Severity 02: 100%|██████████| 156/156 [00:00<00:00, 517.73it/s]\n", 86 | "Severity 03: 100%|██████████| 156/156 [00:00<00:00, 521.73it/s]\n", 87 | "Severity 04: 100%|██████████| 156/156 [00:00<00:00, 516.61it/s]\n", 88 | "Severity 05: 100%|██████████| 156/156 [00:00<00:00, 520.55it/s]\n" 89 | ] 90 | }, 91 | { 92 | "name": "stdout", 93 | "output_type": "stream", 94 | "text": [ 95 | "Starting motion_blur...\n" 96 | ] 97 | }, 98 | { 99 | "name": "stderr", 100 | "output_type": "stream", 101 | "text": [ 102 | "Severity 01: 100%|██████████| 156/156 [00:07<00:00, 21.04it/s]\n", 103 | "Severity 02: 100%|██████████| 156/156 [00:10<00:00, 15.30it/s]\n", 104 | "Severity 03: 100%|██████████| 156/156 [00:10<00:00, 15.28it/s]\n", 105 | "Severity 04: 100%|██████████| 156/156 [00:13<00:00, 11.80it/s]\n", 106 | "Severity 05: 100%|██████████| 156/156 [00:16<00:00, 9.53it/s]\n" 107 | ] 108 | }, 109 | { 110 | "name": "stdout", 111 | "output_type": "stream", 112 | "text": [ 113 | "Starting brightness_up...\n" 114 | ] 115 | }, 116 | { 117 | "name": "stderr", 118 | "output_type": "stream", 119 | "text": [ 120 | "Severity 01: 100%|██████████| 156/156 [00:00<00:00, 3433.98it/s]\n", 121 | "Severity 02: 100%|██████████| 156/156 [00:00<00:00, 2850.21it/s]\n", 122 | "Severity 03: 100%|██████████| 156/156 [00:00<00:00, 3442.84it/s]\n", 123 | "Severity 04: 100%|██████████| 156/156 [00:00<00:00, 3390.16it/s]\n", 124 | "Severity 05: 100%|██████████| 156/156 [00:00<00:00, 3406.70it/s]\n" 125 | ] 126 | }, 127 | { 128 | "name": "stdout", 129 | "output_type": "stream", 130 | "text": [ 131 | "Starting brightness_down...\n" 132 | ] 133 | }, 134 | { 135 | "name": "stderr", 136 | "output_type": "stream", 137 | "text": [ 138 | "Severity 01: 100%|██████████| 156/156 [00:00<00:00, 4325.34it/s]\n", 139 | "Severity 02: 100%|██████████| 156/156 [00:00<00:00, 4256.60it/s]\n", 140 | "Severity 03: 100%|██████████| 156/156 [00:00<00:00, 4285.20it/s]\n", 141 | "Severity 04: 100%|██████████| 156/156 [00:00<00:00, 4243.43it/s]\n", 142 | "Severity 05: 100%|██████████| 156/156 [00:00<00:00, 4163.05it/s]\n" 143 | ] 144 | }, 145 | { 146 | "name": "stdout", 147 | "output_type": "stream", 148 | "text": [ 149 | "Starting contrast_down...\n" 150 | ] 151 | }, 152 | { 153 | "name": "stderr", 154 | "output_type": "stream", 155 | "text": [ 156 | "Severity 01: 100%|██████████| 156/156 [00:00<00:00, 3279.54it/s]\n", 157 | "Severity 02: 100%|██████████| 156/156 [00:00<00:00, 3282.44it/s]\n", 158 | "Severity 03: 100%|██████████| 156/156 [00:00<00:00, 3289.52it/s]\n", 159 | "Severity 04: 100%|██████████| 156/156 [00:00<00:00, 3302.50it/s]\n", 160 | "Severity 05: 100%|██████████| 156/156 [00:00<00:00, 3310.47it/s]\n" 161 | ] 162 | } 163 | ], 164 | "source": [ 165 | "dataset = \"breastmnist\"\n", 166 | "\n", 167 | "ds_manager = DatasetManager(medmnist_path = medmnist_path, output_path=output_path)\n", 168 | "ds_manager.create_dataset(dataset_name = dataset)" 169 | ] 170 | }, 171 | { 172 | "cell_type": "markdown", 173 | "metadata": {}, 174 | "source": [ 175 | "### Create all datasets" 176 | ] 177 | }, 178 | { 179 | "cell_type": "code", 180 | "execution_count": null, 181 | "metadata": {}, 182 | "outputs": [], 183 | "source": [ 184 | "ds_manager = DatasetManager(medmnist_path = medmnist_path, output_path=output_path)\n", 185 | "ds_manager.create_dataset(dataset = \"all\")" 186 | ] 187 | } 188 | ], 189 | "metadata": { 190 | "kernelspec": { 191 | "display_name": "medmnistc", 192 | "language": "python", 193 | "name": "python3" 194 | }, 195 | "language_info": { 196 | "codemirror_mode": { 197 | "name": "ipython", 198 | "version": 3 199 | }, 200 | "file_extension": ".py", 201 | "mimetype": "text/x-python", 202 | "name": "python", 203 | "nbconvert_exporter": "python", 204 | "pygments_lexer": "ipython3", 205 | "version": "3.11.7" 206 | } 207 | }, 208 | "nbformat": 4, 209 | "nbformat_minor": 2 210 | } 211 | -------------------------------------------------------------------------------- /medmnistc/utils/baselines.py: -------------------------------------------------------------------------------- 1 | BASELINES = { 2 | 3 | "bloodmnist" : { 4 | "clean_score": 0.018369282094266692, 5 | "raw_scores": { 6 | "pixelate": 0.05975618949678589, 7 | "jpeg_compression": 0.13976187053705183, 8 | "defocus_blur": 0.22399795597780647, 9 | "motion_blur": 0.1845192137218041, 10 | "brightness_up": 0.06946294328845175, 11 | "brightness_down": 0.0790077546902872, 12 | "contrast_up": 0.05215362284240639, 13 | "contrast_down": 0.07966068499560328, 14 | "saturate": 0.03478558170020538, 15 | "stain_deposit": 0.18536119379858546, 16 | "bubble": 0.12898833684644578 17 | } 18 | }, 19 | 20 | "breastmnist" : { 21 | "clean_score": 0.14536340852130336, 22 | "raw_scores": { 23 | "pixelate": 0.46127819548872184, 24 | "jpeg_compression": 0.2986215538847118, 25 | "speckle_noise": 0.38709273182957393, 26 | "motion_blur": 0.25902255639097743, 27 | "brightness_up": 0.3197994987468672, 28 | "brightness_down": 0.21127819548872181, 29 | "contrast_down": 0.15864661654135334 30 | } 31 | }, 32 | 33 | "chestmnist" : { 34 | "clean_score": 0.2839064387513035, 35 | "raw_scores": { 36 | "pixelate": 0.44895070087295996, 37 | "jpeg_compression": 0.34575690815860566, 38 | "gaussian_noise": 0.47688317292098203, 39 | "speckle_noise": 0.45687894795025785, 40 | "impulse_noise": 0.48553631547337917, 41 | "shot_noise": 0.4926966815396587, 42 | "gaussian_blur": 0.3176801263607164, 43 | "brightness_up": 0.2993721457134663, 44 | "brightness_down": 0.29711530406534037, 45 | "contrast_up": 0.28751004051276824, 46 | "contrast_down": 0.2954742427570406, 47 | "gamma_corr_up": 0.287462117104012, 48 | "gamma_corr_down": 0.2891642074561852 49 | } 50 | }, 51 | 52 | "dermamnist" : { 53 | "clean_score": 0.3985775597242611, 54 | "raw_scores": { 55 | "pixelate": 0.5105118313063095, 56 | "jpeg_compression": 0.47716296678659287, 57 | "gaussian_noise": 0.770161396521756, 58 | "speckle_noise": 0.7688784860204994, 59 | "impulse_noise": 0.7995552265467947, 60 | "shot_noise": 0.8391552104792783, 61 | "defocus_blur": 0.7008318467374395, 62 | "motion_blur": 0.6266414682428412, 63 | "zoom_blur": 0.5277668583085878, 64 | "brightness_up": 0.4473990148869178, 65 | "brightness_down": 0.5772968080812169, 66 | "contrast_up": 0.430415384126964, 67 | "contrast_down": 0.5398210493891896, 68 | "black_corner": 0.7591457853547467, 69 | "characters": 0.4461917314736283 70 | } 71 | }, 72 | 73 | "octmnist" : { 74 | "clean_score": 0.20199999999999996, 75 | "raw_scores": { 76 | "pixelate": 0.3124, 77 | "jpeg_compression": 0.29640000000000005, 78 | "speckle_noise": 0.386, 79 | "defocus_blur": 0.3084, 80 | "motion_blur": 0.4892, 81 | "contrast_down": 0.4196 82 | } 83 | }, 84 | 85 | "organamnist" : { 86 | "clean_score": 0.050053710403475504, 87 | "raw_scores": { 88 | "pixelate": 0.07589728744538755, 89 | "jpeg_compression": 0.10233182799848468, 90 | "gaussian_noise": 0.558867512509986, 91 | "speckle_noise": 0.5008977196696448, 92 | "impulse_noise": 0.6005122792556206, 93 | "shot_noise": 0.5704549211261293, 94 | "gaussian_blur": 0.23340661727240267, 95 | "brightness_up": 0.14763547790322412, 96 | "brightness_down": 0.2646445055204496, 97 | "contrast_up": 0.10977873034814234, 98 | "contrast_down": 0.1268424696095805, 99 | "gamma_corr_up": 0.10360009998047674, 100 | "gamma_corr_down": 0.10577092296366888 101 | } 102 | }, 103 | 104 | "organcmnist" : { 105 | "clean_score": 0.07596783210891289, 106 | "raw_scores": { 107 | "pixelate": 0.12161270665759313, 108 | "jpeg_compression": 0.10754481922990186, 109 | "gaussian_noise": 0.5736544084235357, 110 | "speckle_noise": 0.5130960298987614, 111 | "impulse_noise": 0.5997780092763433, 112 | "shot_noise": 0.5492477253691926, 113 | "gaussian_blur": 0.316895537101905, 114 | "brightness_up": 0.16011849294420644, 115 | "brightness_down": 0.2324271692594097, 116 | "contrast_up": 0.09991836613387597, 117 | "contrast_down": 0.15327395964015147, 118 | "gamma_corr_up": 0.1426418229218433, 119 | "gamma_corr_down": 0.12390902424472901 120 | } 121 | }, 122 | 123 | "organsmnist" : { 124 | "clean_score": 0.24300485476777922, 125 | "raw_scores": { 126 | "pixelate": 0.31259415418134306, 127 | "jpeg_compression": 0.26996994867350865, 128 | "gaussian_noise": 0.5889124203738291, 129 | "speckle_noise": 0.5450100158501043, 130 | "impulse_noise": 0.6010108858901404, 131 | "shot_noise": 0.5519125643755777, 132 | "gaussian_blur": 0.4466645657200248, 133 | "brightness_up": 0.3598501969979232, 134 | "brightness_down": 0.3592866403894267, 135 | "contrast_up": 0.2871702451645464, 136 | "contrast_down": 0.3128243336438499, 137 | "gamma_corr_up": 0.30253196514691594, 138 | "gamma_corr_down": 0.3116016263827458 139 | } 140 | }, 141 | 142 | "pathmnist" : { 143 | "clean_score": 0.08839299601334816, 144 | "raw_scores": { 145 | "pixelate": 0.20934201922262957, 146 | "jpeg_compression": 0.24709267033930377, 147 | "defocus_blur": 0.4621259742146214, 148 | "motion_blur": 0.3689076534699618, 149 | "brightness_up": 0.41551581022730166, 150 | "brightness_down": 0.2811688318161528, 151 | "contrast_up": 0.2338925659881773, 152 | "contrast_down": 0.13718687818913905, 153 | "saturate": 0.3341143518079194, 154 | "stain_deposit": 0.24930831751137275, 155 | "bubble": 0.10818301335634566 156 | } 157 | }, 158 | 159 | "pneumoniamnist" : { 160 | "clean_score": 0.10085470085470083, 161 | "raw_scores": { 162 | "pixelate": 0.22102564102564104, 163 | "jpeg_compression": 0.2798290598290598, 164 | "gaussian_noise": 0.4414529914529915, 165 | "speckle_noise": 0.3457264957264957, 166 | "impulse_noise": 0.4581196581196581, 167 | "shot_noise": 0.4106837606837607, 168 | "gaussian_blur": 0.2730769230769231, 169 | "brightness_up": 0.14649572649572645, 170 | "brightness_down": 0.2782051282051282, 171 | "contrast_up": 0.09529914529914527, 172 | "contrast_down": 0.23743589743589744, 173 | "gamma_corr_up": 0.09606837606837607, 174 | "gamma_corr_down": 0.16641025641025642 175 | } 176 | }, 177 | 178 | "retinamnist" : { 179 | "clean_score": 0.5582064849927977, 180 | "raw_scores": { 181 | "pixelate": 0.6017003263074344, 182 | "jpeg_compression": 0.6795223564688244, 183 | "gaussian_noise": 0.7040832524914012, 184 | "speckle_noise": 0.6135258841167651, 185 | "defocus_blur": 0.6882905018079196, 186 | "motion_blur": 0.7075760943057883, 187 | "brightness_down": 0.5844791133844842, 188 | "contrast_down": 0.6092882382338243 189 | } 190 | }, 191 | 192 | "tissuemnist" : { 193 | "clean_score": 0.3917787199385978, 194 | "raw_scores": { 195 | "pixelate": 0.49272908707597907, 196 | "jpeg_compression": 0.508070087070269, 197 | "impulse_noise": 0.7970869222746931, 198 | "gaussian_blur": 0.4805143361974326, 199 | "brightness_up": 0.4618274173802247, 200 | "brightness_down": 0.51981943234386, 201 | "contrast_up": 0.5026233641102424, 202 | "contrast_down": 0.5369342652194591 203 | } 204 | }, 205 | } -------------------------------------------------------------------------------- /medmnistc/eval.py: -------------------------------------------------------------------------------- 1 | from medmnistc.utils.baselines import BASELINES 2 | 3 | from sklearn.metrics import balanced_accuracy_score 4 | import numpy as np 5 | import json 6 | import os 7 | 8 | 9 | class Evaluator: 10 | def __init__(self, 11 | dataset_name: str, 12 | true_labels: list, 13 | corruption_types: list, 14 | output_folder: str, 15 | architecture: str, 16 | task: str, 17 | suffix_log: str = ''): 18 | """ 19 | Evaluates the robustness of a given model on a set of pre-defined corruptions. 20 | 21 | :param dataset_name: Name of the dataset (used for logging). 22 | :param true_labels: True labels of the current dataset. 23 | :param corruption_types: List of corruptions used for the current experiment. 24 | :param output_folder: Where to store the output logs (json file). 25 | :param architecture: Name of the architecture (logging purposes). 26 | :param task: Classification task (i.e., binary-class etc). 27 | :param suffix: Suffix of the logging file (e.g. seed of the current experiment) 28 | """ 29 | self.dataset_name = dataset_name 30 | self.len_dataset = len(true_labels) 31 | self.corruption_types = corruption_types 32 | self.output_folder = output_folder 33 | self.architecture = architecture 34 | self.task = task 35 | self.suffix_log = suffix_log 36 | 37 | self.initialize_evaluation() 38 | 39 | self.true_labels = np.array(true_labels) 40 | if self.true_labels.shape[1] == 1: # multi-class or binary 41 | self.true_labels = self.true_labels.reshape(-1,) # flatten 42 | 43 | 44 | def initialize_evaluation(self): 45 | """ 46 | Load the baseline logging file, if required, and setup evaluation function based 47 | on the current classification task. 48 | """ 49 | self.corruption_errors = {corruption: [] for corruption in self.corruption_types} 50 | self.clean_score = None # Init 51 | 52 | assert self.dataset_name in BASELINES.keys(), f"{self.dataset_name} has no pre-defined baselines in /utils/baselines.py" 53 | self.corruption_errors_alexnet = BASELINES[self.dataset_name] 54 | 55 | self.evaluation_metric = self.get_eval_metric() 56 | 57 | 58 | def get_eval_metric(self): 59 | """ 60 | Define the appropriate evaluation function based on the current task. 61 | """ 62 | # Return the average balanced accuracy per label, using the chosen operating points (per label). 63 | if self.task == "multi-label, binary-class": 64 | return lambda y_true, y_score, threshold: 1.0 - np.mean( 65 | [balanced_accuracy_score(y_true[:, i], y_score[:, i] > threshold[i]) for i in range(y_true.shape[1])] 66 | ) 67 | 68 | # Returns the balanced accuracy, using the chosen operating point. 69 | elif self.task == "binary-class": 70 | return lambda y_true, y_score, threshold: 1.0 - balanced_accuracy_score(y_true, y_score[:, -1] > threshold) 71 | 72 | # Returns the balanced accuracy, neglecting the default `threshold` argument. 73 | elif self.task == "multi-class" or self.task == "ordinal-regression": 74 | return lambda y_true, y_score, _: 1.0 - balanced_accuracy_score(y_true, np.argmax(y_score, axis=-1)) 75 | 76 | else: 77 | raise ValueError(f"Unknown task type {self.task}") 78 | 79 | 80 | def evaluate(self, predicted_probabilities, corruption_type, threshold=0.5): 81 | """ 82 | Evaluate the predictions of the current model. 83 | 84 | :param predicted_probabilities: List of raw predictions (i.e., probabilities) 85 | Note that the dataset is "repeatd" 5 times 86 | due to the 5 increasing severities. 87 | :param corruption_type: Name of the current corruption. 88 | :param threshold: Operating point(s) based on the given task. 89 | float if task == "binary-class" 90 | list[float] if task == "multi-label, binary-class" 91 | Note: we use a list of operating points, as we may want to 92 | tune label-specific operating points. 93 | If that's not the case, just use [0.5] * num_labels 94 | None if task == "multi-class" or task == "ordinal-regression" 95 | """ 96 | 97 | for severity in range(5): 98 | # get probabilities of the current severity slice 99 | index_range = slice(self.len_dataset * severity, self.len_dataset * (severity + 1)) 100 | curr_prob = predicted_probabilities[index_range] 101 | # calculate relative score and update evaluation metric 102 | score = self.evaluation_metric(self.true_labels, curr_prob, threshold) 103 | self.corruption_errors[corruption_type].append(score) 104 | 105 | 106 | def evaluate_clean(self, predicted_probabilities, threshold=0.5): 107 | """ 108 | Evaluate clean dataset in order to calculate the relative corruption error. 109 | 110 | :param predicted_probabilities: List of raw predictions (i.e., probabilities) 111 | Note that the dataset is "repeatd" 5 times 112 | due to the 5 increasing severities. 113 | :param threshold: Operating point(s) based on the given task. 114 | float if task == "binary-class" 115 | list[float] if task == "multi-label, binary-class" 116 | None if task == "multi-class" or task == "ordinal-regression" 117 | """ 118 | score = self.evaluation_metric(self.true_labels, predicted_probabilities, threshold) 119 | self.clean_score = score 120 | 121 | 122 | def dump_summary(self): 123 | """ 124 | Store a json file containing the aggregated and raw results. 125 | """ 126 | self.output_log = {} 127 | self.populate_summary() 128 | 129 | full_output_path = os.path.join(self.output_folder, f'{self.dataset_name}_{self.architecture}_{self.suffix_log}.json') 130 | with open(full_output_path, 'w') as f: 131 | json.dump(self.output_log, f, indent=4) 132 | 133 | print(f'Logs stored at `{full_output_path}`') 134 | 135 | 136 | def populate_summary(self): 137 | """ 138 | Calculate the error metrics according to the formulas reported on the paper. 139 | """ 140 | 141 | assert self.clean_score, "You first need to compute the clean error via self.evaluate_clean(...)" 142 | 143 | self.output_log['metrics'] = {'clean_score': self.clean_score or 0} 144 | self.output_log['be_scores'] = {} 145 | self.output_log['rbe_scores'] = {} 146 | 147 | for corruption, errors in self.corruption_errors.items(): 148 | 149 | # For other architectures, normalize errors against AlexNet's performance 150 | alexnet_error = self.corruption_errors_alexnet['raw_scores'][corruption] 151 | alexnet_clean_score = self.corruption_errors_alexnet['clean_score'] 152 | 153 | # Ensure there are scores to normalize against to avoid division by zero 154 | if alexnet_error and alexnet_clean_score is not None: 155 | # Normalized Balanced Error (BE) 156 | be = np.mean(errors) / alexnet_error # alexnet_error is already averaged across severities 157 | 158 | # Calculate Relative Balanced Error (RBE) 159 | rbe_num = np.mean(errors) - self.clean_score # the clean score would be subtracted 5 times and divided by 5 (mean). So, we can put this out 160 | rbe_denom = alexnet_error - alexnet_clean_score # same here 161 | rbe = rbe_num / rbe_denom 162 | 163 | self.output_log['be_scores'][corruption] = be 164 | self.output_log['rbe_scores'][corruption] = rbe 165 | 166 | # Compute overall corrupted score and relative corrupted error 167 | self.output_log['metrics']['be'] = np.mean(list(self.output_log['be_scores'].values())) 168 | self.output_log['metrics']['rbe'] = np.mean(list(self.output_log['rbe_scores'].values())) 169 | 170 | # Include raw scores for completeness 171 | self.output_log['raw_scores'] = {k:np.mean(v) for k,v in self.corruption_errors.items()} # store only the average 172 | -------------------------------------------------------------------------------- /assets/examples/evaluation.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Evaluation" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "### Import" 15 | ] 16 | }, 17 | { 18 | "cell_type": "code", 19 | "execution_count": 49, 20 | "metadata": {}, 21 | "outputs": [], 22 | "source": [ 23 | "%%capture\n", 24 | "\n", 25 | "from medmnistc.dataset import CorruptedMedMNIST\n", 26 | "from medmnistc.eval import Evaluator\n", 27 | "from medmnistc.corruptions.registry import CORRUPTIONS_DS\n", 28 | "\n", 29 | "from torch.utils.data import DataLoader\n", 30 | "from medmnist import INFO\n", 31 | "from copy import deepcopy\n", 32 | "from tqdm import tqdm\n", 33 | "\n", 34 | "import torchvision.transforms as transforms\n", 35 | "import medmnist\n", 36 | "import torch.nn as nn\n", 37 | "import torch\n", 38 | "import timm" 39 | ] 40 | }, 41 | { 42 | "cell_type": "markdown", 43 | "metadata": {}, 44 | "source": [ 45 | "### Setup experiment" 46 | ] 47 | }, 48 | { 49 | "cell_type": "code", 50 | "execution_count": 50, 51 | "metadata": {}, 52 | "outputs": [], 53 | "source": [ 54 | "config = {\n", 55 | " 'dataset' : 'breastmnist',\n", 56 | " 'architecture' : 'resnet18.tv_in1k', # timm-equivalent name\n", 57 | " 'medmnist_path' : '/mnt/data/datasets/medmnist',\n", 58 | " 'medmnistc_path' : '/mnt/data/datasets/medmnistc', \n", 59 | " 'logs_path' : './',\n", 60 | " 'seed' : 42, # training seed (if any) - here it is used in `Evaluator` as id for the output logs\n", 61 | "}\n", 62 | "\n", 63 | "info = INFO[config['dataset']]\n", 64 | "\n", 65 | "config.update({\n", 66 | " 'task': info['task'],\n", 67 | " 'in_channel': info['n_channels'],\n", 68 | " 'num_classes': len(info['label'])\n", 69 | "})\n", 70 | "\n", 71 | "# Define model - we are further training in this example\n", 72 | "model = timm.create_model(config['architecture'], pretrained=True)\n", 73 | "model = model.eval()\n", 74 | "\n", 75 | "mean, std = model.default_cfg['mean'], model.default_cfg['std']\n", 76 | "\n", 77 | "# Load clean dataset\n", 78 | "DataClass = getattr(medmnist, info['python_class'])\n", 79 | "\n", 80 | "data_transform = transforms.Compose([transforms.ToTensor(),transforms.Normalize(mean=mean, std=std)])\n", 81 | "test_dataset_clean = DataClass(split='test', transform=data_transform, download=False, as_rgb=True, size=224, root=config['medmnist_path']) \n", 82 | "test_loader_clean = DataLoader(test_dataset_clean, batch_size=128, shuffle=False, num_workers=4, persistent_workers=True)\n", 83 | "\n", 84 | "# Init the Evaluator class\n", 85 | "corruptions = CORRUPTIONS_DS[config['dataset']]\n", 86 | "evaluator = Evaluator(dataset_name=config['dataset'],\n", 87 | " true_labels=test_dataset_clean.labels,\n", 88 | " corruption_types=corruptions.keys(),\n", 89 | " output_folder=config['logs_path'],\n", 90 | " architecture=config['architecture'],\n", 91 | " task=config['task'],\n", 92 | " suffix_log=f\"s{config['seed']}\")" 93 | ] 94 | }, 95 | { 96 | "cell_type": "markdown", 97 | "metadata": {}, 98 | "source": [ 99 | "### Inference " 100 | ] 101 | }, 102 | { 103 | "cell_type": "code", 104 | "execution_count": 51, 105 | "metadata": {}, 106 | "outputs": [], 107 | "source": [ 108 | "def evaluate(model, dataloader, task, device = 'cuda:0'):\n", 109 | " \"\"\"\n", 110 | " Evaluate a model on the current corrupted test set.\n", 111 | "\n", 112 | " :param config: Dictionary containing the parameters and hyperparameters.\n", 113 | " :param dataloader: DataLoader for the test set.\n", 114 | " :param task: Classification task ('multi-label, binary-class','multi-class', and so on..).\n", 115 | " :param device: Running device (cuda or cpu).\n", 116 | " :return: Predictions (raw probabilities).\n", 117 | " \"\"\"\n", 118 | " \n", 119 | " # Load model and prediction function\n", 120 | " if task == \"multi-label, binary-class\":\n", 121 | " prediction = nn.Sigmoid()\n", 122 | " else:\n", 123 | " prediction = nn.Softmax(dim=1)\n", 124 | "\n", 125 | " model = model.to(device)\n", 126 | "\n", 127 | " # Run the Evaluation\n", 128 | " y_pred = torch.tensor([]).to(device)\n", 129 | "\n", 130 | " with torch.no_grad():\n", 131 | " for images, labels in tqdm(dataloader):\n", 132 | " # Map the data to the available device\n", 133 | " images, labels = images.to(device), labels.to(torch.float32).to(device)\n", 134 | " outputs = model(images)\n", 135 | " outputs = prediction(outputs)\n", 136 | " # Store the predictions\n", 137 | " y_pred = torch.cat((y_pred, deepcopy(outputs)), 0)\n", 138 | "\n", 139 | " return y_pred" 140 | ] 141 | }, 142 | { 143 | "cell_type": "code", 144 | "execution_count": 52, 145 | "metadata": {}, 146 | "outputs": [ 147 | { 148 | "name": "stderr", 149 | "output_type": "stream", 150 | "text": [ 151 | "100%|██████████| 2/2 [00:00<00:00, 11.78it/s]\n" 152 | ] 153 | }, 154 | { 155 | "name": "stdout", 156 | "output_type": "stream", 157 | "text": [ 158 | "pixelate\n" 159 | ] 160 | }, 161 | { 162 | "name": "stderr", 163 | "output_type": "stream", 164 | "text": [ 165 | "100%|██████████| 7/7 [00:00<00:00, 20.49it/s]\n" 166 | ] 167 | }, 168 | { 169 | "name": "stdout", 170 | "output_type": "stream", 171 | "text": [ 172 | "jpeg_compression\n" 173 | ] 174 | }, 175 | { 176 | "name": "stderr", 177 | "output_type": "stream", 178 | "text": [ 179 | "100%|██████████| 7/7 [00:00<00:00, 20.31it/s]\n" 180 | ] 181 | }, 182 | { 183 | "name": "stdout", 184 | "output_type": "stream", 185 | "text": [ 186 | "speckle_noise\n" 187 | ] 188 | }, 189 | { 190 | "name": "stderr", 191 | "output_type": "stream", 192 | "text": [ 193 | "100%|██████████| 7/7 [00:00<00:00, 20.65it/s]\n" 194 | ] 195 | }, 196 | { 197 | "name": "stdout", 198 | "output_type": "stream", 199 | "text": [ 200 | "motion_blur\n" 201 | ] 202 | }, 203 | { 204 | "name": "stderr", 205 | "output_type": "stream", 206 | "text": [ 207 | "100%|██████████| 7/7 [00:00<00:00, 19.97it/s]\n" 208 | ] 209 | }, 210 | { 211 | "name": "stdout", 212 | "output_type": "stream", 213 | "text": [ 214 | "brightness_up\n" 215 | ] 216 | }, 217 | { 218 | "name": "stderr", 219 | "output_type": "stream", 220 | "text": [ 221 | "100%|██████████| 7/7 [00:00<00:00, 20.03it/s]\n" 222 | ] 223 | }, 224 | { 225 | "name": "stdout", 226 | "output_type": "stream", 227 | "text": [ 228 | "brightness_down\n" 229 | ] 230 | }, 231 | { 232 | "name": "stderr", 233 | "output_type": "stream", 234 | "text": [ 235 | "100%|██████████| 7/7 [00:00<00:00, 20.29it/s]\n" 236 | ] 237 | }, 238 | { 239 | "name": "stdout", 240 | "output_type": "stream", 241 | "text": [ 242 | "contrast_down\n" 243 | ] 244 | }, 245 | { 246 | "name": "stderr", 247 | "output_type": "stream", 248 | "text": [ 249 | "100%|██████████| 7/7 [00:00<00:00, 20.43it/s]" 250 | ] 251 | }, 252 | { 253 | "name": "stdout", 254 | "output_type": "stream", 255 | "text": [ 256 | "Logs stored at `./breastmnist_resnet18.tv_in1k_s42.json`\n" 257 | ] 258 | }, 259 | { 260 | "name": "stderr", 261 | "output_type": "stream", 262 | "text": [ 263 | "\n" 264 | ] 265 | } 266 | ], 267 | "source": [ 268 | "# Evaluate clean performance\n", 269 | "y_pred = evaluate(model, test_loader_clean, config['task'])\n", 270 | "evaluator.evaluate_clean(y_pred.cpu().numpy())\n", 271 | "\n", 272 | "# Iterate over the designed corruptions.\n", 273 | "for corruption in corruptions.keys():\n", 274 | "\n", 275 | " print(corruption)\n", 276 | " \n", 277 | " # Load the corrupted test set, according to the selected corruption\n", 278 | " corrupted_test_test = CorruptedMedMNIST(\n", 279 | " dataset_name = config['dataset'], \n", 280 | " corruption = corruption,\n", 281 | " root = config['medmnistc_path'],\n", 282 | " as_rgb = test_dataset_clean.as_rgb,\n", 283 | " mmap_mode='r',\n", 284 | " norm_mean = mean,\n", 285 | " norm_std = std\n", 286 | " )\n", 287 | " \n", 288 | " # Get dataloader\n", 289 | " test_loader = DataLoader(corrupted_test_test, batch_size=128, shuffle=False, num_workers=4, persistent_workers=True)\n", 290 | "\n", 291 | " # Evaluate\n", 292 | " y_pred = evaluate(model, test_loader, config['task']) \n", 293 | "\n", 294 | " # Calculate the error\n", 295 | " evaluator.evaluate(y_pred.cpu().numpy(), corruption)\n", 296 | "\n", 297 | "# Create a json file containing the results\n", 298 | "evaluator.dump_summary()" 299 | ] 300 | } 301 | ], 302 | "metadata": { 303 | "kernelspec": { 304 | "display_name": "medmnistc", 305 | "language": "python", 306 | "name": "python3" 307 | }, 308 | "language_info": { 309 | "codemirror_mode": { 310 | "name": "ipython", 311 | "version": 3 312 | }, 313 | "file_extension": ".py", 314 | "mimetype": "text/x-python", 315 | "name": "python", 316 | "nbconvert_exporter": "python", 317 | "pygments_lexer": "ipython3", 318 | "version": "3.11.7" 319 | } 320 | }, 321 | "nbformat": 4, 322 | "nbformat_minor": 2 323 | } 324 | -------------------------------------------------------------------------------- /medmnistc/visualizer.py: -------------------------------------------------------------------------------- 1 | from medmnistc.corruptions.registry import DATASET_RGB, CORRUPTIONS_DS 2 | 3 | from skimage.util import montage as skimage_montage 4 | from PIL import Image 5 | 6 | import numpy as np 7 | import random 8 | import cv2 9 | import os 10 | 11 | 12 | class Visualizer: 13 | def __init__(self, 14 | medmnistc_path : str, 15 | medmnist_path : str, 16 | output_path : str): 17 | """ 18 | Class used to plot examples of the selected corruptions. 19 | 20 | :param medmnistc_path: Root path of the corrupted datasets. 21 | :param medmnist_path: Root path of the clean datasets. 22 | :param output_path: Root path of the generated visualizations. 23 | """ 24 | self.medmnist_path = medmnist_path 25 | self.medmnistc_path = medmnistc_path 26 | self.output_path = output_path 27 | 28 | self.supported_datasets = [ 29 | 'bloodmnist', 'breastmnist', 'chestmnist', 'dermamnist', 30 | 'octmnist', 'organamnist', 'organcmnist', 'organsmnist', 31 | 'pathmnist', 'pneumoniamnist', 'retinamnist', 'tissuemnist' 32 | ] 33 | 34 | # Annotation hyperparameters 35 | self.font = cv2.FONT_HERSHEY_SIMPLEX 36 | self.font_scale = 0.5 37 | self.thickness = 1 38 | self.text_offset_x = 30 39 | self.text_offset_y = 30 40 | self.rect_offset = 5 41 | 42 | # Create folder 43 | os.makedirs(self.output_path, exist_ok=True) 44 | 45 | 46 | def plot_extended(self, 47 | dataset_name : str = None, 48 | idx_image : int = None): 49 | """ 50 | Plot an image grid (N,5) where: 51 | - N is the number of the designed corruptions 52 | - 5 represents the 5 severity levels 53 | 54 | :param dataset_name: Name of the dataset to corrupt. 55 | Options: {'bloodmnist', 'breastmnist', 'chestmnist', 'dermamnist', 56 | 'octmnist', 'organamnist', 'organcmnist', 'organsmnist', 57 | 'pathmnist', 'pneumoniamnist', 'retinamnist', 'tissuemnist'} 58 | :param idx_image: Index of the selected image to corrupt and visualize. 59 | If None, a random index will be chosen. 60 | """ 61 | assert dataset_name in self.supported_datasets, f"Dataset not found. Please choose one among : {self.supported_datasets}" 62 | 63 | output_folder = os.path.join(self.output_path,'extended') 64 | os.makedirs(output_folder, exist_ok=True) 65 | 66 | # Load clean images 67 | clean_test_images = np.load(os.path.join(self.medmnist_path,f'{dataset_name}_224.npz'))['test_images'] 68 | num_images = len(clean_test_images) 69 | 70 | # Retrieve all corruption paths 71 | corruptions_path = os.listdir(os.path.join(self.medmnistc_path,dataset_name)) 72 | 73 | # Setup image grid 74 | num_rows, num_cols = len(corruptions_path), 6 75 | 76 | # Select a random image, if not selected 77 | if not idx_image: 78 | idx_image = random.randint(0,num_images-1) 79 | 80 | # Check wether the dataset is RGB or not 81 | n_channels = 3 if DATASET_RGB[dataset_name] else 1 82 | 83 | # Init images to display 84 | images = [] 85 | 86 | # Iterate over corruptions (ROWS) 87 | for corruption in CORRUPTIONS_DS[dataset_name].keys(): 88 | 89 | test_images = np.load(os.path.join(self.medmnistc_path,dataset_name,f'{corruption}.npz'))['test_images'] 90 | 91 | # Annotate the image in the first column 92 | corrutpion_name = corruption.split(".npz")[0].replace("_"," ") 93 | images.append(self._annotate_img(clean_test_images[idx_image].copy(),corrutpion_name)) 94 | 95 | # Iterate over remaining corruption severities (COLUMNS) 96 | for sev_idx in range(0,5): 97 | idx_corr = idx_image + sev_idx * num_images 98 | images.append(test_images[idx_corr]) 99 | 100 | 101 | # Create montage with all the selected images 102 | montage_arr = skimage_montage( 103 | images, channel_axis=3 if n_channels == 3 else None, 104 | grid_shape=(num_rows,num_cols), 105 | fill=(255,255,255) 106 | ) 107 | 108 | # Store output 109 | filename = f'{dataset_name}_id{idx_image}.png' 110 | img_path = os.path.join(output_folder,filename) 111 | 112 | print(f'Image stored at : {img_path}') 113 | 114 | Image.fromarray(montage_arr).save(img_path) 115 | 116 | 117 | def plot_one_severity(self, 118 | dataset_name: str = None, 119 | idx_image:int = None, 120 | severity: int = 3, 121 | max_per_row: int = -1): 122 | """ 123 | Plot an image along with all its corruptions in a row, with a user-specified severity. 124 | 125 | Name of the dataset to corrupt. 126 | Options: {'bloodmnist', 'breastmnist', 'chestmnist', 'dermamnist', 127 | 'octmnist', 'organamnist', 'organcmnist', 'organsmnist', 128 | 'pathmnist', 'pneumoniamnist', 'retinamnist', 'tissuemnist'} 129 | :param idx_image: Index of the selected image to corrupt and visualize. 130 | If None, a random index will be chosen. 131 | :param severity: Severity of the corruptions. This will be applied to all the corruptions. 132 | :param max_per_row: Maximum number of corruptions to show in a row. 133 | In `num_corruptions` > `max_per_row`, multiple images are stored. 134 | """ 135 | assert dataset_name in self.supported_datasets, f"Dataset not found. Please choose one among : {self.supported_datasets}" 136 | 137 | # Init output folder 138 | output_folder = os.path.join(self.output_path,'one_severity') 139 | os.makedirs(output_folder, exist_ok=True) 140 | 141 | # Select the number of channels 142 | n_channels = 3 if DATASET_RGB[dataset_name] else 1 143 | 144 | # Load the clean test set 145 | clean_test_images = np.load(os.path.join(self.medmnist_path,f'{dataset_name}_224.npz'))['test_images'] 146 | num_images = len(clean_test_images) 147 | 148 | # Retrieve all designed corruptions 149 | corruptions_path = os.listdir(os.path.join(self.medmnistc_path,dataset_name)) 150 | 151 | if not idx_image: 152 | idx_image = random.randint(0,num_images-1) 153 | 154 | # Init images to display 155 | images = [] 156 | 157 | # Define the output grid 158 | num_rows, num_cols = 1, len(corruptions_path) + 1 # +1 because of the clean one 159 | images.append(clean_test_images[idx_image]) # 1st image (clean one) 160 | 161 | # Iterate over corruptions 162 | for corruption in CORRUPTIONS_DS[dataset_name].keys(): 163 | 164 | test_images = np.load(os.path.join(self.medmnistc_path,dataset_name,f'{corruption}.npz'))['test_images'] 165 | corrutpion_name = corruption.split(".npz")[0].replace("_"," ") 166 | idx_corr = idx_image + (severity-1) * num_images 167 | 168 | # Annotate the image in the first column 169 | images.append(self._annotate_img(test_images[idx_corr],corrutpion_name)) 170 | 171 | # Check if we need to decompose it into multiple images 172 | if max_per_row > 0 and len(images) > max_per_row: 173 | 174 | clean_image = images[0] # extract clean one (it will be always shown) 175 | corrupted_images = images[1:] # extract corrupted images 176 | num_corrupted_images = len(corrupted_images) 177 | num_parts = num_corrupted_images // (max_per_row-1) # define the number of plots (i.e., parts) 178 | 179 | for pt in range(num_parts+1): 180 | 181 | # Append clean image with the current corrupted ones 182 | curr_images = [clean_image] + corrupted_images[(max_per_row-1)*pt:(max_per_row-1)*pt + (max_per_row-1)] 183 | 184 | # Create montage with all the selected images 185 | montage_arr = skimage_montage( 186 | curr_images, channel_axis=3 if n_channels == 3 else None, 187 | grid_shape=(num_rows,max_per_row), 188 | fill=(255,255,255) 189 | ) 190 | 191 | # Store output 192 | filename = f'{dataset_name}_id{idx_image}_sev{severity}_pt{pt+1}.png' 193 | img_path = os.path.join(output_folder,filename) 194 | 195 | print(f'Image stored at : {img_path}') 196 | 197 | Image.fromarray(montage_arr).save(img_path) 198 | 199 | elif max_per_row > 0: 200 | 201 | # Create montage with all the selected images 202 | montage_arr = skimage_montage( 203 | images, channel_axis=3 if n_channels == 3 else None, 204 | grid_shape=(num_rows,max_per_row), 205 | fill=(255,255,255) 206 | ) 207 | 208 | # Store output 209 | filename = f'{dataset_name}_id{idx_image}_sev{severity}.png' 210 | img_path = os.path.join(output_folder,filename) 211 | 212 | print(f'Image stored at : {img_path}') 213 | 214 | Image.fromarray(montage_arr).save(img_path) 215 | 216 | 217 | else: 218 | 219 | # Create montage with all the selected images 220 | montage_arr = skimage_montage( 221 | images, channel_axis=3 if n_channels == 3 else None, 222 | grid_shape=(num_rows,num_cols), 223 | fill=(255,255,255) 224 | ) 225 | 226 | # Store output 227 | filename = f'{dataset_name}_id{idx_image}_sev{severity}.png' 228 | img_path = os.path.join(output_folder,filename) 229 | 230 | print(f'Image stored at : {img_path}') 231 | 232 | Image.fromarray(montage_arr).save(img_path) 233 | 234 | 235 | 236 | def _annotate_img(self, img, text): 237 | """Annotate the selected image with the name of the corruption. 238 | Specifically, a white text over a black background is placed at: 239 | (text_offset_x, text_offset_y) 240 | 241 | :param img: Image to annotate (np.uint8) 242 | :param text: Text to add on the image. 243 | """ 244 | # Get width and height of the text box 245 | (text_width, text_height), _ = cv2.getTextSize(text, self.font, self.font_scale, self.thickness) 246 | 247 | # Define box coordinates 248 | box_coords = ( 249 | (self.text_offset_x - self.rect_offset, self.text_offset_y + self.rect_offset), 250 | (self.text_offset_x + text_width + self.rect_offset, self.text_offset_y - text_height - self.rect_offset) 251 | ) 252 | 253 | # Black background & white text 254 | cv2.rectangle(img, box_coords[0], box_coords[1], (0, 0, 0), cv2.FILLED) 255 | cv2.putText(img, 256 | text, 257 | (self.text_offset_x, self.text_offset_y), 258 | self.font, 259 | self.font_scale, 260 | (255, 255, 255), 261 | self.thickness, 262 | cv2.LINE_AA) 263 | 264 | return np.array(img) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2020-2023 MedMNIST Team 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /medmnistc/corruptions/registry.py: -------------------------------------------------------------------------------- 1 | from .noise import GaussianNoise, ImpulseNoise, SpeckleNoise, ShotNoise 2 | from .compression import JPEGCompression, Pixelate 3 | from .filter import MotionBlur, DefocusBlur, ZoomBlur, GaussianBlur 4 | from .enhance import Brightness, Contrast, Saturate, GammaCorrection 5 | from .microscopy import Bubble, StainDeposit, BlackCorner, Characters 6 | 7 | import numpy as np 8 | 9 | 10 | CORRUPTIONS_DS = { 11 | 12 | 'pathmnist' : { 13 | 'pixelate': Pixelate(severity_params=[0.8, 0.6, 0.40, 0.30, 0.25]), 14 | 'jpeg_compression' : JPEGCompression(severity_params=[50, 30, 15, 10, 7]), 15 | 'defocus_blur' : DefocusBlur(severity_params=[(3, 0.1), (4, 0.1), (5, 0.2), (6,0.2), (7, 0.3)]), 16 | 'motion_blur' : MotionBlur(severity_params=[(5,5), (10, 5), (15, 5), (15, 8), (15, 12)]), 17 | 'brightness_up' : Brightness(severity_params=[1.1, 1.15, 1.2, 1.22, 1.25]), 18 | 'brightness_down' : Brightness(severity_params=[0.85, 0.80, 0.75, 0.72, 0.70]), 19 | 'contrast_up' : Contrast(severity_params=[1.1, 1.2, 1.3, 1.4, 1.6]), 20 | 'contrast_down' : Contrast(severity_params=[0.8, 0.7, 0.6, 0.55, 0.5]), 21 | 'saturate' : Saturate(severity_params=[0.05, 0.10, 0.15, 0.20, 0.25]), 22 | 'stain_deposit' : StainDeposit(severity_params=[1,2,3,4,5]), 23 | 'bubble' : Bubble(severity_params=[(7,15),(10,15),(12,15),(15,20),(17,25)]) 24 | }, 25 | 26 | 27 | 'bloodmnist' : { 28 | 'pixelate': Pixelate(severity_params=[0.6, 0.5, 0.40, 0.30, 0.25]), 29 | 'jpeg_compression' : JPEGCompression(severity_params=[50, 30, 15, 10, 7]), 30 | 'defocus_blur' : DefocusBlur(severity_params=[(2, 0.01), (3, 0.1), (4,0.1), (5,0.1), (6, 0.1)]), 31 | 'motion_blur' : MotionBlur(severity_params=[(3,3), (5,5), (10, 5), (10,7), (10, 9)]), 32 | 'brightness_up' : Brightness(severity_params=[1.1, 1.2, 1.3, 1.35, 1.4]), 33 | 'brightness_down' : Brightness(severity_params=[0.9, 0.8, 0.7, 0.6, 0.5]), 34 | 'contrast_up' : Contrast(severity_params=[1.1, 1.15, 1.2, 1.25, 1.3]), 35 | 'contrast_down' : Contrast(severity_params=[0.9, 0.8, 0.7, 0.6, 0.5]), 36 | 'saturate' : Saturate(severity_params=[0.05, 0.10, 0.15, 0.17, 0.20]), 37 | 'stain_deposit' : StainDeposit(severity_params=[1,2,3,3,3]), 38 | 'bubble' : Bubble(severity_params=[(5,10),(7,10),(10,10),(12,12),(15,12)]) 39 | }, 40 | 41 | 42 | 'dermamnist' : { 43 | 'pixelate': Pixelate(severity_params=[0.7, 0.5, 0.40, 0.30, 0.25]), 44 | 'jpeg_compression' : JPEGCompression(severity_params=[30, 20, 15, 10, 7]), 45 | 'gaussian_noise' : GaussianNoise(severity_params=[0.04, .08, .12, 0.18, 0.26]), 46 | 'speckle_noise' : SpeckleNoise(severity_params=[0.05, 0.15, 0.2, 0.35, 0.45]), 47 | 'impulse_noise' : ImpulseNoise(severity_params=[0.01, 0.03, 0.06, 0.09, 0.17]), 48 | 'shot_noise' : ShotNoise(severity_params=[60, 25, 18, 10, 5]), 49 | 'defocus_blur' : DefocusBlur(severity_params=[(4, 0.1), (5, 0.2), (6, 0.3), (7, 0.4), (8,0.5)]), 50 | 'motion_blur' : MotionBlur(severity_params=[(10, 5), (15, 5), (15, 8), (15, 12), (20, 15)]), 51 | 'zoom_blur' : ZoomBlur(severity_params=[ 52 | np.arange(1, 1.11, 0.01), 53 | np.arange(1, 1.16, 0.01), 54 | np.arange(1, 1.21, 0.02), 55 | np.arange(1, 1.26, 0.02), 56 | np.arange(1, 1.31, 0.03) 57 | ]), 58 | 'brightness_up' : Brightness(severity_params=[1.1, 1.2, 1.3, 1.4, 1.5]), 59 | 'brightness_down' : Brightness(severity_params=[0.9, 0.8, 0.7, 0.6, 0.5]), 60 | 'contrast_up' : Contrast(severity_params=[1.1, 1.2, 1.3, 1.4, 1.6]), 61 | 'contrast_down' : Contrast(severity_params=[0.8, 0.7, 0.6, 0.5, 0.4]), 62 | 'black_corner' : BlackCorner(severity_params=[1.10, 1.05, 1.00, 0.90, 0.95]), 63 | 'characters' : Characters(severity_params=[(1,6,0.14),(2,7,0.15),(3,8,0.16),(4,9,0.17),(6,10,0.18)]) 64 | }, 65 | 66 | 67 | 'retinamnist' : { 68 | 'pixelate': Pixelate(severity_params=[0.8, 0.60, 0.50, 0.40, 0.35]), 69 | 'jpeg_compression' : JPEGCompression(severity_params=[30, 25, 20, 10, 5]), 70 | 'gaussian_noise' : GaussianNoise(severity_params=[0.04, 0.08, 0.12, 0.16, 0.20]), 71 | 'speckle_noise' : SpeckleNoise(severity_params=[0.10, 0.15, 0.20, 0.25, 0.30]), 72 | 'defocus_blur' : DefocusBlur(severity_params=[(4, 0.1), (5, 0.2), (6, 0.3), (7, 0.4), (8,0.5), (9,0.6)]), 73 | 'motion_blur' : MotionBlur(severity_params=[(8, 5), (15, 5), (15, 8), (15, 12), (20, 15)]), 74 | 'brightness_down' : Brightness(severity_params=[0.9, 0.8, 0.7, 0.6, 0.5]), 75 | 'contrast_down' : Contrast(severity_params=[0.9, 0.8, 0.7, 0.6, 0.4]), 76 | }, 77 | 78 | 79 | 'tissuemnist' : { 80 | 'pixelate': Pixelate(severity_params=[0.40, 0.30, 0.20, 0.15, 0.10]), 81 | 'jpeg_compression' : JPEGCompression(severity_params=[25, 20, 15, 10, 7]), 82 | 'impulse_noise' : ImpulseNoise(severity_params=[0.01, 0.015, 0.02, 0.025, 0.03]), 83 | 'gaussian_blur' : GaussianBlur(severity_params=[13, 15, 17, 21, 25]), 84 | 'brightness_up' : Brightness(severity_params=[1.3, 1.4, 1.5, 1.6, 1.7]), 85 | 'brightness_down' : Brightness(severity_params=[0.8, 0.7, 0.6, 0.5, 0.4]), 86 | 'contrast_up' : Contrast(severity_params=[1.1, 1.2, 1.3, 1.4, 1.6]), 87 | 'contrast_down' : Contrast(severity_params=[0.9, 0.8, 0.7, 0.6, 0.4]), 88 | }, 89 | 90 | 91 | 'octmnist' : { 92 | 'pixelate': Pixelate(severity_params=[0.30, 0.25, 0.20, 0.15, 0.10]), # 93 | 'jpeg_compression' : JPEGCompression(severity_params=[30, 15, 10, 7, 5]), 94 | 'speckle_noise' : SpeckleNoise(severity_params=[0.15, 0.30, 0.40, 0.50, 0.60]), 95 | 'defocus_blur' : DefocusBlur(severity_params=[(0.5, 0.6), (1, 0.5), (1.5, 0.1), (2.0,0.5), (2.5,0.1)]), 96 | 'motion_blur' : MotionBlur(severity_params=[(10, 3), (15, 5), (15, 8), (15, 12), (20, 15)]), 97 | 'contrast_down' : Contrast(severity_params=[0.6, 0.4, 0.3, 0.2, 0.15]) 98 | }, 99 | 100 | 101 | 'breastmnist' : { 102 | 'pixelate': Pixelate(severity_params=[0.30, 0.25, 0.20, 0.15, 0.10]), 103 | 'jpeg_compression' : JPEGCompression(severity_params=[50, 30, 15, 10, 7]), 104 | 'speckle_noise' : SpeckleNoise(severity_params=[0.10, 0.15, 0.20, 0.25, 0.30]), 105 | 'motion_blur' : MotionBlur(severity_params=[(5,5), (9, 7), (9,10), (13, 10), (17, 12)]), 106 | 'brightness_up' : Brightness(severity_params=[1.4, 1.5, 1.6, 1.8, 2.0]), 107 | 'brightness_down' : Brightness(severity_params=[0.55, 0.5, 0.45, 0.4, 0.3]), 108 | 'contrast_down' : Contrast(severity_params=[0.9, 0.8, 0.7, 0.6, 0.4]), 109 | }, 110 | 111 | 112 | 'chestmnist' : { 113 | 'pixelate': Pixelate(severity_params=[0.30, 0.25, 0.20, 0.15, 0.10]), 114 | 'jpeg_compression' : JPEGCompression(severity_params=[50, 30, 15, 10, 7]), 115 | 'gaussian_noise' : GaussianNoise(severity_params=[0.04, .08, .12, 0.18, 0.26]), 116 | 'speckle_noise' : SpeckleNoise(severity_params=[0.05, 0.15, 0.2, 0.35, 0.45]), 117 | 'impulse_noise' : ImpulseNoise(severity_params=[0.01, 0.03, 0.06, 0.09, 0.17]), 118 | 'shot_noise' : ShotNoise(severity_params=[60, 25, 18, 10, 5]), 119 | 'gaussian_blur' : GaussianBlur(severity_params=[3, 5, 7, 9, 11, 13]), 120 | 'brightness_up' : Brightness(severity_params=[1.1, 1.2, 1.3, 1.4, 1.5]), 121 | 'brightness_down' : Brightness(severity_params=[0.9, 0.8, 0.7, 0.6, 0.5]), 122 | 'contrast_up' : Contrast(severity_params=[1.1, 1.2, 1.3, 1.4, 1.6]), 123 | 'contrast_down' : Contrast(severity_params=[0.9, 0.8, 0.7, 0.6, 0.4]), 124 | 'gamma_corr_up' : GammaCorrection(severity_params=[1.1, 1.2, 1.3, 1.4, 1.6]), 125 | 'gamma_corr_down' : GammaCorrection(severity_params=[0.9, 0.8, 0.7, 0.6, 0.4]), 126 | }, 127 | 128 | 129 | 'pneumoniamnist' : { 130 | 'pixelate': Pixelate(severity_params=[0.8, 0.7, 0.6, 0.5, 0.40]), 131 | 'jpeg_compression' : JPEGCompression(severity_params=[50, 30, 15, 10, 7]), 132 | 'gaussian_noise' : GaussianNoise(severity_params=[0.04, 0.05, 0.06, 0.07, 0.08]), 133 | 'speckle_noise' : SpeckleNoise(severity_params=[0.05, 0.07, 0.10, 0.15, 0.20]), 134 | 'impulse_noise' : ImpulseNoise(severity_params=[0.005, 0.01, 0.013, 0.017, 0.02]), 135 | 'shot_noise' : ShotNoise(severity_params=[300, 200, 150, 100, 80]), 136 | 'gaussian_blur' : GaussianBlur(severity_params=[3, 5, 7, 9, 11, 13]), 137 | 'brightness_up' : Brightness(severity_params=[1.1, 1.2, 1.3, 1.4, 1.5]), 138 | 'brightness_down' : Brightness(severity_params=[0.9, 0.8, 0.7, 0.6, 0.5]), 139 | 'contrast_up' : Contrast(severity_params=[1.1, 1.2, 1.3, 1.4, 1.6]), 140 | 'contrast_down' : Contrast(severity_params=[0.9, 0.8, 0.7, 0.6, 0.4]), 141 | 'gamma_corr_up' : GammaCorrection(severity_params=[1.1, 1.2, 1.3, 1.4, 1.6]), 142 | 'gamma_corr_down' : GammaCorrection(severity_params=[0.9, 0.8, 0.7, 0.6, 0.4]), 143 | }, 144 | 145 | 146 | 'organamnist' : { 147 | 'pixelate': Pixelate(severity_params=[0.7, 0.6, 0.5, 0.40, 0.35]), 148 | 'jpeg_compression' : JPEGCompression(severity_params=[50, 30, 15, 10, 7]), 149 | 'gaussian_noise' : GaussianNoise(severity_params=[0.04, 0.08, 0.12, 0.16, 0.20]), 150 | 'speckle_noise' : SpeckleNoise(severity_params=[0.05, 0.10, 0.20, 0.30, 0.40]), 151 | 'impulse_noise' : ImpulseNoise(severity_params=[0.01, 0.02, 0.03, 0.05, 0.08]), 152 | 'shot_noise' : ShotNoise(severity_params=[200, 100, 50, 25, 15]), 153 | 'gaussian_blur' : GaussianBlur(severity_params=[11, 13, 15, 17, 21]), 154 | 'brightness_up' : Brightness(severity_params=[1.2, 1.3, 1.4, 1.5, 1.6]), 155 | 'brightness_down' : Brightness(severity_params=[0.8, 0.75, 0.7, 0.65, 0.60]), 156 | 'contrast_up' : Contrast(severity_params=[1.3, 1.4, 1.6, 1.7, 1.8]), 157 | 'contrast_down' : Contrast(severity_params=[0.8, 0.7, 0.6, 0.55, 0.5]), 158 | 'gamma_corr_up' : GammaCorrection(severity_params=[1.3, 1.4, 1.6, 1.8, 2.0]), 159 | 'gamma_corr_down' : GammaCorrection(severity_params=[0.9, 0.8, 0.7, 0.6, 0.4]), 160 | }, 161 | 162 | 163 | 'organcmnist' : { 164 | 'pixelate': Pixelate(severity_params=[0.7, 0.6, 0.5, 0.40, 0.35]), 165 | 'jpeg_compression' : JPEGCompression(severity_params=[50, 30, 15, 10, 7]), 166 | 'gaussian_noise' : GaussianNoise(severity_params=[0.04, 0.08, 0.12, 0.16, 0.20]), 167 | 'speckle_noise' : SpeckleNoise(severity_params=[0.05, 0.10, 0.20, 0.30, 0.40]), 168 | 'impulse_noise' : ImpulseNoise(severity_params=[0.01, 0.02, 0.03, 0.05, 0.08]), 169 | 'shot_noise' : ShotNoise(severity_params=[200, 100, 50, 25, 15]), 170 | 'gaussian_blur' : GaussianBlur(severity_params=[11, 13, 15, 17, 21]), 171 | 'brightness_up' : Brightness(severity_params=[1.2, 1.3, 1.4, 1.5, 1.6]), 172 | 'brightness_down' : Brightness(severity_params=[0.8, 0.75, 0.7, 0.65, 0.60]), 173 | 'contrast_up' : Contrast(severity_params=[1.3, 1.4, 1.6, 1.7, 1.8]), 174 | 'contrast_down' : Contrast(severity_params=[0.8, 0.7, 0.6, 0.55, 0.5]), 175 | 'gamma_corr_up' : GammaCorrection(severity_params=[1.3, 1.4, 1.6, 1.8, 2.0]), 176 | 'gamma_corr_down' : GammaCorrection(severity_params=[0.9, 0.8, 0.7, 0.6, 0.4]), 177 | }, 178 | 179 | 180 | 'organsmnist' : { 181 | 'pixelate': Pixelate(severity_params=[0.7, 0.6, 0.5, 0.40, 0.35]), 182 | 'jpeg_compression' : JPEGCompression(severity_params=[50, 30, 15, 10, 7]), 183 | 'gaussian_noise' : GaussianNoise(severity_params=[0.04, 0.08, 0.12, 0.16, 0.20]), 184 | 'speckle_noise' : SpeckleNoise(severity_params=[0.05, 0.10, 0.20, 0.30, 0.40]), 185 | 'impulse_noise' : ImpulseNoise(severity_params=[0.01, 0.02, 0.03, 0.05, 0.08]), 186 | 'shot_noise' : ShotNoise(severity_params=[200, 100, 50, 25, 15]), 187 | 'gaussian_blur' : GaussianBlur(severity_params=[11, 13, 15, 17, 21]), 188 | 'brightness_up' : Brightness(severity_params=[1.2, 1.3, 1.4, 1.5, 1.6]), 189 | 'brightness_down' : Brightness(severity_params=[0.8, 0.75, 0.7, 0.65, 0.60]), 190 | 'contrast_up' : Contrast(severity_params=[1.3, 1.4, 1.6, 1.7, 1.8]), 191 | 'contrast_down' : Contrast(severity_params=[0.8, 0.7, 0.6, 0.55, 0.5]), 192 | 'gamma_corr_up' : GammaCorrection(severity_params=[1.3, 1.4, 1.6, 1.8, 2.0]), 193 | 'gamma_corr_down' : GammaCorrection(severity_params=[0.9, 0.8, 0.7, 0.6, 0.4]), 194 | }, 195 | } 196 | 197 | 198 | CORRUPTIONS_DS_FOLDS = { 199 | 'digital' : ['pixelate','jpeg_compression'], 200 | 'noise' : ['gaussian_noise', 'speckle_noise', 'impulse_noise', 'shot_noise'], 201 | 'blur': ['defocus_blur','motion_blur','zoom_blur','gaussian_blur'], 202 | 'color' : ['brightness_up', 'brightness_down', 'contrast_up', 'contrast_down','saturate'], 203 | 'task-specific' : ['stain_deposit', 'bubble','black_corner', 'characters','gamma_corr_up','gamma_corr_down'] 204 | } 205 | 206 | 207 | DATASET_RGB = { 208 | 'bloodmnist': True, 209 | 'breastmnist': False, 210 | 'chestmnist': False, 211 | 'dermamnist': True, 212 | 'octmnist': False, 213 | 'organamnist': False, 214 | 'organcmnist': False, 215 | 'organsmnist': False, 216 | 'pathmnist': True, 217 | 'pneumoniamnist': False, 218 | 'retinamnist': True, 219 | 'tissuemnist': False 220 | } -------------------------------------------------------------------------------- /assets/examples/augment.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "from medmnistc.augmentation import AugMedMNISTC\n", 10 | "from medmnistc.corruptions.registry import CORRUPTIONS_DS\n", 11 | "\n", 12 | "from PIL import Image\n", 13 | "\n", 14 | "import torchvision.transforms as transforms\n", 15 | "import numpy as np\n", 16 | "import os" 17 | ] 18 | }, 19 | { 20 | "cell_type": "markdown", 21 | "metadata": {}, 22 | "source": [ 23 | "### Augmentation" 24 | ] 25 | }, 26 | { 27 | "cell_type": "code", 28 | "execution_count": 9, 29 | "metadata": {}, 30 | "outputs": [ 31 | { 32 | "name": "stdout", 33 | "output_type": "stream", 34 | "text": [ 35 | "speckle_noise\n" 36 | ] 37 | }, 38 | { 39 | "data": { 40 | "image/jpeg": "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", 41 | "image/png": "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", 42 | "text/plain": [ 43 | "" 44 | ] 45 | }, 46 | "execution_count": 9, 47 | "metadata": {}, 48 | "output_type": "execute_result" 49 | } 50 | ], 51 | "source": [ 52 | "dataset = \"breastmnist\"\n", 53 | "medmnist_path = \"/mnt/data/datasets/medmnist\" # PATH TO THE CLEAN IMAGES\n", 54 | "\n", 55 | "train_corruptions = CORRUPTIONS_DS[dataset]\n", 56 | "path = os.path.join(medmnist_path,f'{dataset}_224.npz')\n", 57 | "train_images = np.load(path)['train_images']\n", 58 | "\n", 59 | "aug = AugMedMNISTC(train_corruptions, verbose=True) \n", 60 | "\n", 61 | "img = Image.fromarray(train_images[0])\n", 62 | "Image.fromarray(aug(img))" 63 | ] 64 | }, 65 | { 66 | "cell_type": "markdown", 67 | "metadata": {}, 68 | "source": [ 69 | "## Integrate into transforms.Compose" 70 | ] 71 | }, 72 | { 73 | "cell_type": "code", 74 | "execution_count": 24, 75 | "metadata": {}, 76 | "outputs": [ 77 | { 78 | "data": { 79 | "text/plain": [ 80 | "tensor([[[ 2.2489, 2.1119, 1.5810, ..., 1.1872, 2.2489, 2.1119],\n", 81 | " [ 0.6563, 1.8550, 2.2489, ..., 1.8550, 2.2489, 1.4440],\n", 82 | " [ 1.4440, 1.3242, 1.7180, ..., 1.4440, 2.2489, 2.2489],\n", 83 | " ...,\n", 84 | " [ 1.4440, 1.4440, 0.6563, ..., 1.3242, 0.7933, -0.0116],\n", 85 | " [ 1.3242, 1.3242, 0.3994, ..., 1.1872, 1.1872, 1.1872],\n", 86 | " [ 2.2489, 0.5193, 1.4440, ..., 2.2489, 2.1119, 1.3242]],\n", 87 | "\n", 88 | " [[ 1.4832, 1.7458, -0.5476, ..., 0.5378, -0.0049, 1.4832],\n", 89 | " [ 0.2577, 0.2577, 1.7458, ..., -0.2850, -0.1450, -0.5476],\n", 90 | " [ 1.0805, 2.0259, 0.6604, ..., 0.3978, 0.2577, 0.5378],\n", 91 | " ...,\n", 92 | " [ 0.6604, 0.9405, 0.2577, ..., 1.2031, 0.6604, 1.2031],\n", 93 | " [-0.2850, 0.2577, 2.0259, ..., 1.7458, -0.2850, 0.6604],\n", 94 | " [ 0.8004, 0.8004, 0.5378, ..., -0.5476, 0.5378, -0.2850]],\n", 95 | "\n", 96 | " [[ 0.3393, 1.5594, 0.7576, ..., 0.7576, 1.6988, 2.3611],\n", 97 | " [ 2.5006, 1.1585, 1.5594, ..., 0.7576, 2.6400, 1.1585],\n", 98 | " [ 1.0191, 1.0191, 1.0191, ..., 0.3393, 1.4200, 0.8797],\n", 99 | " ...,\n", 100 | " [ 0.8797, 1.1585, 0.3393, ..., 1.4200, 0.4788, 0.3393],\n", 101 | " [ 1.0191, 0.2173, 0.7576, ..., 0.0779, -0.2010, 1.1585],\n", 102 | " [ 2.2391, 0.3393, 0.3393, ..., -0.0615, -0.4624, 0.6182]]])" 103 | ] 104 | }, 105 | "execution_count": 24, 106 | "metadata": {}, 107 | "output_type": "execute_result" 108 | } 109 | ], 110 | "source": [ 111 | "dataset = \"dermamnist\"\n", 112 | "medmnist_path = \"/mnt/data/datasets/medmnist\" # PATH TO THE CLEAN IMAGES\n", 113 | "\n", 114 | "train_corruptions = CORRUPTIONS_DS[dataset]\n", 115 | "path = os.path.join(medmnist_path,f'{dataset}_224.npz')\n", 116 | "train_images = np.load(path)['train_images']\n", 117 | "\n", 118 | "MEAN = [0.485, 0.456, 0.406]\n", 119 | "STD = [0.229, 0.224, 0.225]\n", 120 | "\n", 121 | "aug_compose = transforms.Compose([\n", 122 | " AugMedMNISTC(train_corruptions),\n", 123 | " transforms.ToTensor(),\n", 124 | " transforms.Normalize(mean=MEAN, std=STD)\n", 125 | "])\n", 126 | "\n", 127 | "img = Image.fromarray(train_images[0])\n", 128 | "aug_compose(img) # As before, but we have a normalized tensor now." 129 | ] 130 | } 131 | ], 132 | "metadata": { 133 | "kernelspec": { 134 | "display_name": "medmnistc", 135 | "language": "python", 136 | "name": "python3" 137 | }, 138 | "language_info": { 139 | "codemirror_mode": { 140 | "name": "ipython", 141 | "version": 3 142 | }, 143 | "file_extension": ".py", 144 | "mimetype": "text/x-python", 145 | "name": "python", 146 | "nbconvert_exporter": "python", 147 | "pygments_lexer": "ipython3", 148 | "version": "3.11.7" 149 | } 150 | }, 151 | "nbformat": 4, 152 | "nbformat_minor": 2 153 | } 154 | --------------------------------------------------------------------------------