├── 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 |
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 |
--------------------------------------------------------------------------------