dsa-helpers 1.1.5.dev4__tar.gz → 1.1.5.dev6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. {dsa_helpers-1.1.5.dev4/dsa_helpers.egg-info → dsa_helpers-1.1.5.dev6}/PKG-INFO +2 -1
  2. {dsa_helpers-1.1.5.dev4 → dsa_helpers-1.1.5.dev6}/dsa_helpers/__init__.py +1 -1
  3. {dsa_helpers-1.1.5.dev4 → dsa_helpers-1.1.5.dev6}/dsa_helpers/ml/datasets/SegFormerSegmentationDataset.py +10 -2
  4. {dsa_helpers-1.1.5.dev4 → dsa_helpers-1.1.5.dev6}/dsa_helpers/ml/datasets/utils.py +10 -1
  5. dsa_helpers-1.1.5.dev6/dsa_helpers/ml/evaluate.py +307 -0
  6. dsa_helpers-1.1.5.dev6/dsa_helpers/ml/segformer_semantic_segmentation/datasets.py +119 -0
  7. dsa_helpers-1.1.5.dev6/dsa_helpers/ml/segformer_semantic_segmentation/evaluate.py +126 -0
  8. {dsa_helpers-1.1.5.dev4 → dsa_helpers-1.1.5.dev6}/dsa_helpers/ml/segformer_semantic_segmentation/train.py +13 -27
  9. dsa_helpers-1.1.5.dev6/dsa_helpers/ml/segformer_semantic_segmentation/transforms.py +63 -0
  10. dsa_helpers-1.1.5.dev6/dsa_helpers/ml/transforms/segformer_transforms.py +40 -0
  11. {dsa_helpers-1.1.5.dev4 → dsa_helpers-1.1.5.dev6/dsa_helpers.egg-info}/PKG-INFO +2 -1
  12. {dsa_helpers-1.1.5.dev4 → dsa_helpers-1.1.5.dev6}/dsa_helpers.egg-info/SOURCES.txt +3 -0
  13. {dsa_helpers-1.1.5.dev4 → dsa_helpers-1.1.5.dev6}/dsa_helpers.egg-info/requires.txt +1 -0
  14. {dsa_helpers-1.1.5.dev4 → dsa_helpers-1.1.5.dev6}/pyproject.toml +3 -2
  15. dsa_helpers-1.1.5.dev4/dsa_helpers/ml/evaluate.py +0 -425
  16. dsa_helpers-1.1.5.dev4/dsa_helpers/ml/transforms/segformer_transforms.py +0 -23
  17. {dsa_helpers-1.1.5.dev4 → dsa_helpers-1.1.5.dev6}/.gitignore +0 -0
  18. {dsa_helpers-1.1.5.dev4 → dsa_helpers-1.1.5.dev6}/.readthedocs.yaml +0 -0
  19. {dsa_helpers-1.1.5.dev4 → dsa_helpers-1.1.5.dev6}/LICENSE +0 -0
  20. {dsa_helpers-1.1.5.dev4 → dsa_helpers-1.1.5.dev6}/README.md +0 -0
  21. {dsa_helpers-1.1.5.dev4 → dsa_helpers-1.1.5.dev6}/app.py +0 -0
  22. {dsa_helpers-1.1.5.dev4 → dsa_helpers-1.1.5.dev6}/dev.py +0 -0
  23. {dsa_helpers-1.1.5.dev4 → dsa_helpers-1.1.5.dev6}/docs/requirements.txt +0 -0
  24. {dsa_helpers-1.1.5.dev4 → dsa_helpers-1.1.5.dev6}/docs/source/conf.py +0 -0
  25. {dsa_helpers-1.1.5.dev4 → dsa_helpers-1.1.5.dev6}/docs/source/index.rst +0 -0
  26. {dsa_helpers-1.1.5.dev4 → dsa_helpers-1.1.5.dev6}/dsa_helpers/cli_utils.py +0 -0
  27. {dsa_helpers-1.1.5.dev4 → dsa_helpers-1.1.5.dev6}/dsa_helpers/dash/__init__.py +0 -0
  28. {dsa_helpers-1.1.5.dev4 → dsa_helpers-1.1.5.dev6}/dsa_helpers/dash/dash_paperdragon_utils.py +0 -0
  29. {dsa_helpers-1.1.5.dev4 → dsa_helpers-1.1.5.dev6}/dsa_helpers/dash/header.py +0 -0
  30. {dsa_helpers-1.1.5.dev4 → dsa_helpers-1.1.5.dev6}/dsa_helpers/dash/header_callbacks.py +0 -0
  31. {dsa_helpers-1.1.5.dev4 → dsa_helpers-1.1.5.dev6}/dsa_helpers/girder_utils.py +0 -0
  32. {dsa_helpers-1.1.5.dev4 → dsa_helpers-1.1.5.dev6}/dsa_helpers/gpd_utils.py +0 -0
  33. {dsa_helpers-1.1.5.dev4 → dsa_helpers-1.1.5.dev6}/dsa_helpers/image_utils.py +0 -0
  34. {dsa_helpers-1.1.5.dev4 → dsa_helpers-1.1.5.dev6}/dsa_helpers/imread.py +0 -0
  35. {dsa_helpers-1.1.5.dev4 → dsa_helpers-1.1.5.dev6}/dsa_helpers/imwrite.py +0 -0
  36. {dsa_helpers-1.1.5.dev4 → dsa_helpers-1.1.5.dev6}/dsa_helpers/ml/__init__.py +0 -0
  37. {dsa_helpers-1.1.5.dev4 → dsa_helpers-1.1.5.dev6}/dsa_helpers/ml/callbacks.py +0 -0
  38. {dsa_helpers-1.1.5.dev4 → dsa_helpers-1.1.5.dev6}/dsa_helpers/ml/datasets/__init__.py +0 -0
  39. {dsa_helpers-1.1.5.dev4 → dsa_helpers-1.1.5.dev6}/dsa_helpers/ml/metrics.py +0 -0
  40. {dsa_helpers-1.1.5.dev4 → dsa_helpers-1.1.5.dev6}/dsa_helpers/ml/object_detection/__init__.py +0 -0
  41. {dsa_helpers-1.1.5.dev4 → dsa_helpers-1.1.5.dev6}/dsa_helpers/ml/object_detection/get_pearce_roi_images.py +0 -0
  42. {dsa_helpers-1.1.5.dev4 → dsa_helpers-1.1.5.dev6}/dsa_helpers/ml/object_detection/tile_image.py +0 -0
  43. {dsa_helpers-1.1.5.dev4 → dsa_helpers-1.1.5.dev6}/dsa_helpers/ml/object_detection/utils.py +0 -0
  44. {dsa_helpers-1.1.5.dev4 → dsa_helpers-1.1.5.dev6}/dsa_helpers/ml/segformer_semantic_segmentation/__init__.py +0 -0
  45. {dsa_helpers-1.1.5.dev4 → dsa_helpers-1.1.5.dev6}/dsa_helpers/ml/segformer_semantic_segmentation/inference.py +0 -0
  46. {dsa_helpers-1.1.5.dev4 → dsa_helpers-1.1.5.dev6}/dsa_helpers/ml/transforms/__init__.py +0 -0
  47. {dsa_helpers-1.1.5.dev4 → dsa_helpers-1.1.5.dev6}/dsa_helpers/mongo_utils.py +0 -0
  48. {dsa_helpers-1.1.5.dev4 → dsa_helpers-1.1.5.dev6}/dsa_helpers/tile_utils.py +0 -0
  49. {dsa_helpers-1.1.5.dev4 → dsa_helpers-1.1.5.dev6}/dsa_helpers/tiling.py +0 -0
  50. {dsa_helpers-1.1.5.dev4 → dsa_helpers-1.1.5.dev6}/dsa_helpers/utils.py +0 -0
  51. {dsa_helpers-1.1.5.dev4 → dsa_helpers-1.1.5.dev6}/dsa_helpers.egg-info/dependency_links.txt +0 -0
  52. {dsa_helpers-1.1.5.dev4 → dsa_helpers-1.1.5.dev6}/dsa_helpers.egg-info/top_level.txt +0 -0
  53. {dsa_helpers-1.1.5.dev4 → dsa_helpers-1.1.5.dev6}/setup.cfg +0 -0
  54. {dsa_helpers-1.1.5.dev4 → dsa_helpers-1.1.5.dev6}/tests/app/Dockerfile +0 -0
  55. {dsa_helpers-1.1.5.dev4 → dsa_helpers-1.1.5.dev6}/tests/app/app.py +0 -0
  56. {dsa_helpers-1.1.5.dev4 → dsa_helpers-1.1.5.dev6}/tests/docker-compose.yaml +0 -0
  57. {dsa_helpers-1.1.5.dev4 → dsa_helpers-1.1.5.dev6}/tests/test.ipynb +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dsa_helpers
3
- Version: 1.1.5.dev4
3
+ Version: 1.1.5.dev6
4
4
  Summary: Utility functions for working with the DSA girder client.
5
5
  Author-email: "Juan C. Vizcarra" <jvizcar@emory.edu>
6
6
  Maintainer-email: "Juan C. Vizcarra" <jvizcar@emory.edu>
@@ -28,6 +28,7 @@ Requires-Dist: datasets
28
28
  Requires-Dist: torchvision
29
29
  Requires-Dist: large-image
30
30
  Requires-Dist: rasterio
31
+ Requires-Dist: colorama
31
32
  Dynamic: license-file
32
33
 
33
34
  # Digital Slide Archive (DSA) Helpers
@@ -3,7 +3,7 @@ from .imread import imread
3
3
  from .imwrite import imwrite
4
4
 
5
5
  # Version of the dsa-helpers package
6
- __version__ = "1.1.5.dev4"
6
+ __version__ = "1.1.5.dev6"
7
7
 
8
8
  # To avoid slow downs, do not allow from dsa_helpers import * to import anything.
9
9
  __all__ = []
@@ -1,11 +1,14 @@
1
1
  from torch.utils.data import Dataset
2
2
  import pandas as pd
3
3
  from PIL import Image
4
- import numpy as np
4
+ from colorama import Style, Fore
5
5
 
6
6
 
7
7
  class SegFormerSegmentationDataset(Dataset):
8
- """PyTorch dataset class for semantic segmentation using HuggingFaces SegFormer
8
+ """DEPRECATED: see
9
+ dsa_helpers.ml.segformer_semantic_segmentation.datasets.SegFormerSegmentationDataset
10
+
11
+ PyTorch dataset class for semantic segmentation using HuggingFaces SegFormer
9
12
  model.
10
13
 
11
14
  """
@@ -17,6 +20,11 @@ class SegFormerSegmentationDataset(Dataset):
17
20
  df (pandas.DataFrame): A pandas DataFrame with columns "fp" and "mask_fp".
18
21
 
19
22
  """
23
+ print(Fore.RED)
24
+ print(
25
+ "This is deprecated, please import from dsa_helpers.ml.segformer_semantic_segmentation.datasets"
26
+ )
27
+ print(Style.RESET_ALL)
20
28
  self.image_files = df["fp"].tolist()
21
29
  self.mask_files = df["mask_fp"].tolist()
22
30
  self.df = df # store potential metadata
@@ -2,6 +2,7 @@
2
2
  import pandas as pd
3
3
  from .SegFormerSegmentationDataset import SegFormerSegmentationDataset
4
4
  from datasets import Dataset, Features, Image
5
+ from colorama import Style, Fore
5
6
 
6
7
 
7
8
  def dataset_generator(dataset):
@@ -13,7 +14,10 @@ def dataset_generator(dataset):
13
14
  def create_segformer_segmentation_dataset(
14
15
  df: pd.DataFrame | str, low_memory: bool = True, transforms=None
15
16
  ):
16
- """Create a SegFormer segmentation dataset from a DataFrame.
17
+ """DEPRECATED: see
18
+ dsa_helpers.ml.segformer_semantic_segmentation.datasets.create_segformer_segmentation_dataset
19
+
20
+ Create a SegFormer segmentation dataset from a DataFrame.
17
21
 
18
22
  Args:
19
23
  df (pd.DataFrame | str): A pandas DataFrame with columns "fp" and "mask_fp" or a
@@ -26,6 +30,11 @@ def create_segformer_segmentation_dataset(
26
30
  A Dataset object to be used for HuggingFaces SegFormer model training.
27
31
 
28
32
  """
33
+ print(Fore.RED)
34
+ print(
35
+ "This is deprecated, please import from dsa_helpers.ml.segformer_semantic_segmentation.datasets"
36
+ )
37
+ print(Style.RESET_ALL)
29
38
  if isinstance(df, str):
30
39
  df = pd.read_csv(df, low_memory=low_memory)
31
40
 
@@ -0,0 +1,307 @@
1
+ """Functions for evaluating datasets with machine learning models.
2
+
3
+ - evaluate_semantic_segmentation_segformer_group: Evaluate a segformer
4
+ trainer model on a specific dataset provided as a dataframe, grouped
5
+ by a specific column.
6
+ - per_class_dice_on_dataset: Per class DICE coefficient on a set of
7
+ images.
8
+
9
+ """
10
+
11
+ import torch
12
+ from torch import nn
13
+ from tqdm import tqdm
14
+ from transformers.trainer import Trainer
15
+ import pandas as pd
16
+ import numpy as np
17
+ from .datasets.utils import create_segformer_segmentation_dataset
18
+ from .transforms.segformer_transforms import val_transforms
19
+ from ..tile_utils import merge_tile_masks
20
+
21
+
22
+ def evaluate_semantic_segmentation_segformer_group(
23
+ trainer: Trainer,
24
+ df: pd.DataFrame,
25
+ group_col: str,
26
+ batch_size: int = 16,
27
+ tile_size: int = 512,
28
+ background_label: int | None = None,
29
+ ) -> float:
30
+ """Evaluate a segformer trainer model on a specific dataset provided
31
+ as a dataframe, grouped by a specific column. For a unique value of
32
+ the column it will predict on the images for that group and then
33
+ merge the predictions into a single multipolygon. The mean IoU will
34
+ be calculated agains the ground truth masks for that group. The
35
+ reported mean IoU will be the average of all the mean IoUs for each
36
+ group.
37
+
38
+ Args:
39
+ trainer (Trainer): The trainer object with the model to evaluate.
40
+ df (pandas.DataFrame): The dataframe with the tile metadata.
41
+ Must have the fp column (file path), x column (x coordinate),
42
+ y column (y coordinate), and group_col.
43
+ group_col (str): The column to group the tiles by.
44
+ batch_size (int): The batch size to use for prediction. Note that
45
+ this is not the batch size use for predictions, rather it is
46
+ how many masks will be stored in memory. Batch size used
47
+ for predictions is specified on the trainer input. Default is
48
+ 16.
49
+ tile_size (int): The size of the tiles. Default is 512.
50
+ background_label (int | None): The label to use for the background
51
+ class, which will be ignored. If None then no label will be
52
+ ignored. Default is None.
53
+
54
+ Returns:
55
+ float: The mean IoU across all groups.
56
+
57
+ """
58
+ ious = []
59
+
60
+ # Predict on the images for each group separately.
61
+ groups = list(df[group_col].unique())
62
+ for group in tqdm(groups, desc="Evaluating groups"):
63
+ # Get the tiles in this group.
64
+ group_df = df[df[group_col] == group]
65
+
66
+ pred_tile_list = []
67
+ true_tile_list = []
68
+
69
+ # Predict on the group in batches.
70
+ for i in range(0, len(group_df), batch_size):
71
+ # Create dataset for this batch.
72
+ batch_df = group_df.iloc[i : i + batch_size]
73
+ x_list = batch_df["x"].tolist()
74
+ y_list = batch_df["y"].tolist()
75
+
76
+ # Create dataset for this batch.
77
+ group_dataset = create_segformer_segmentation_dataset(
78
+ batch_df, transforms=val_transforms
79
+ )
80
+
81
+ # Predict on the batch.
82
+ out = trainer.predict(group_dataset)
83
+ preds = out[0]
84
+ true = out[1]
85
+
86
+ # Reshape the prediction logits to tile size.
87
+ preds = torch.from_numpy(preds).cpu()
88
+
89
+ preds = nn.functional.interpolate(
90
+ preds,
91
+ size=(tile_size, tile_size),
92
+ mode="bilinear",
93
+ align_corners=False,
94
+ ).argmax(
95
+ dim=1
96
+ ) # logits -> class predictions
97
+
98
+ preds = preds.detach().numpy()
99
+
100
+ # Append to the tile list.
101
+ pred_tile_list.extend(
102
+ [(pred, x, y) for x, y, pred in zip(x_list, y_list, preds)]
103
+ )
104
+
105
+ true_tile_list.extend(
106
+ [(mask, x, y) for x, y, mask in zip(x_list, y_list, true)]
107
+ )
108
+
109
+ # Merge the tile masks.
110
+ pred_gdf = merge_tile_masks(
111
+ pred_tile_list, background_label=background_label
112
+ )
113
+ true_gdf = merge_tile_masks(
114
+ true_tile_list, background_label=background_label
115
+ )
116
+
117
+ # Get the unique labels in either the prediction or the ground truth.
118
+ labels = set(pred_gdf["label"].unique()) | set(
119
+ true_gdf["label"].unique()
120
+ )
121
+
122
+ # Calculate the IoU for each label.
123
+ ious = []
124
+
125
+ for label in labels:
126
+ pred_label_gdf = pred_gdf[pred_gdf["label"] == label]
127
+ true_label_gdf = true_gdf[true_gdf["label"] == label]
128
+
129
+ # If either the prediction or the ground truth is empty, the IoU is 0.
130
+ if pred_label_gdf.empty or true_label_gdf.empty:
131
+ ious.append(0)
132
+ else:
133
+ # Calculate the IoU.
134
+ geom1 = pred_label_gdf.iloc[0]["geometry"]
135
+ geom2 = true_label_gdf.iloc[0]["geometry"]
136
+
137
+ intersection_area = geom1.intersection(geom2).area
138
+ union = geom1.union(geom2).area
139
+
140
+ ious.append(intersection_area / union)
141
+
142
+ # Return the average of the ious.
143
+ return float(sum(ious) / len(ious))
144
+
145
+
146
+ # def evaluate_semantic_segmentation_segformer_by_wsi(
147
+ # model: nn.Module,
148
+ # data: pd.DataFrame | str,
149
+ # group_col: str,
150
+ # label2id: dict[str, int],
151
+ # batch_size: int = 16,
152
+ # tile_size: int = 512,
153
+ # background_label: int | None = None,
154
+ # device: torch.device | None = None,
155
+ # ) -> float:
156
+ # """Evaluate a segformer trainer model on a specific dataset provided
157
+ # as a dataframe, grouped by a specific column. For a unique value of
158
+ # the column it will predict on the images for that group and then
159
+ # merge the predictions into a single multipolygon. The mean IoU will
160
+ # be calculated agains the ground truth masks for that group. The
161
+ # reported mean IoU will be the average of all the mean IoUs for each
162
+ # group.
163
+
164
+ # Args:
165
+ # model (nn.Module): The model to evaluate.
166
+ # data (pd.DataFrame | str): The dataframe or path to a csv file
167
+ # containing the data to evaluate on. The dataframe must have
168
+ # the "fp" (filepath to image) and "mask_fp" (filepath to
169
+ # mask) columns. The column matching the "group_col" parameter
170
+ # will be used to group tiles into groups. The "x" and "y"
171
+ # parameters will be used for stiching the images back to
172
+ # their original size.
173
+ # group_col (str): The column to group the tiles by.
174
+ # batch_size (int): The batch size to use for prediction. Note that
175
+ # this is not the batch size use for predictions, rather it is
176
+ # how many masks will be stored in memory. Batch size used
177
+ # for predictions is specified on the trainer input. Default is
178
+ # 16.
179
+ # label2id (dict): A dictionary mapping labels to their integer
180
+ # representations.
181
+ # tile_size (int): The size of the tiles. Default is 512.
182
+ # background_label (int | None): The label to use for the background
183
+ # class, which will be ignored. If None then no label will be
184
+ # ignored. Default is None.
185
+ # device (torch.device | None, optional): The device to use for
186
+ # evaluation. If None, the device will be "cuda" if available,
187
+ # otherwise "cpu". Default is None.
188
+
189
+ # Returns:
190
+ # float: The mean IoU across all groups.
191
+
192
+ # """
193
+ # # Read the data.
194
+ # if isinstance(data, str):
195
+ # data = pd.read_csv(data)
196
+
197
+ # ious = []
198
+
199
+ # if device is None:
200
+ # device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
201
+
202
+ # # Move the model to the device.
203
+ # model.to(device)
204
+ # model.eval() # model should be in evaluation mode
205
+
206
+ # # Predict on the images for each group separately.
207
+ # groups = list(data[group_col].unique())
208
+ # for group in tqdm(groups, desc="Evaluating groups"):
209
+ # # Get the tiles in this group.
210
+ # group_df = data[data[group_col] == group]
211
+
212
+ # pred_tile_list = []
213
+ # true_tile_list = []
214
+
215
+ # # Predict on the group in batches.
216
+ # for i in range(0, len(group_df), batch_size):
217
+ # # Create dataset for this batch.
218
+ # batch_df = group_df.iloc[i : i + batch_size]
219
+ # x_list = batch_df["x"].tolist()
220
+ # y_list = batch_df["y"].tolist()
221
+
222
+ # # Create dataset for this batch.
223
+ # dataset = create_segformer_segmentation_dataset(
224
+ # batch_df, transforms=val_transforms
225
+ # )
226
+
227
+ # # Get the inputs.
228
+ # inputs = np.array(dataset[:]["pixel_values"])
229
+ # inputs = torch.tensor(inputs, dtype=torch.float32)
230
+ # inputs = inputs.to(device)
231
+
232
+ # true = np.array(dataset[:]["labels"])
233
+
234
+ # # Predict on the batch.
235
+ # with torch.no_grad():
236
+ # out = model(inputs)
237
+ # preds = out.logits
238
+
239
+ # # Reshape the prediction logits to tile size.
240
+ # preds = torch.from_numpy(preds)
241
+
242
+ # preds = nn.functional.interpolate(
243
+ # preds,
244
+ # size=(tile_size, tile_size),
245
+ # mode="bilinear",
246
+ # align_corners=False,
247
+ # ).argmax(
248
+ # dim=1
249
+ # ) # logits -> class predictions
250
+
251
+ # preds = preds.detach().cpu().numpy()
252
+
253
+ # # Append to the tile list.
254
+ # pred_tile_list.extend(
255
+ # [(pred, x, y) for x, y, pred in zip(x_list, y_list, preds)]
256
+ # )
257
+
258
+ # true_tile_list.extend(
259
+ # [(mask, x, y) for x, y, mask in zip(x_list, y_list, true)]
260
+ # )
261
+
262
+ # # Merge the tile masks.
263
+ # pred_gdf = merge_tile_masks(
264
+ # pred_tile_list, background_label=background_label
265
+ # )
266
+ # true_gdf = merge_tile_masks(
267
+ # true_tile_list, background_label=background_label
268
+ # )
269
+
270
+ # # Calculate the Dice for each label.
271
+ # metrics = {label: [] for label in label2id}
272
+
273
+ # for label, integer in label2id.items():
274
+ # if integer == background_label:
275
+ # continue # don't calculate dice for background
276
+
277
+ # # For this integer, get the prediction and ground truth.
278
+ # pred_label_gdf = pred_gdf[pred_gdf["label"] == integer]
279
+ # true_label_gdf = true_gdf[true_gdf["label"] == integer]
280
+
281
+ # # If both are empty, the dice is 1.
282
+ # if pred_label_gdf.empty and true_label_gdf.empty:
283
+ # metrics[label].append(1)
284
+ # else:
285
+ # # Calculate the intersection and denominator.
286
+ # intersection = pred_label_gdf.geometry.intersection(
287
+ # true_label_gdf.geometry
288
+ # ).area.sum()
289
+
290
+ # # The denominator is the sum area of the two polygons.
291
+ # denominator = (
292
+ # pred_label_gdf.geometry.area.sum()
293
+ # + true_label_gdf.geometry.area.sum()
294
+ # )
295
+
296
+ # # The dice is 2 * intersection / denominator.
297
+ # metrics[label].append(float(2 * intersection / denominator))
298
+
299
+ # # Return the dice as means and standard deviations.
300
+ # metrics = {
301
+ # label: {
302
+ # "mean": float(np.mean(metrics[label])),
303
+ # "std": float(np.std(metrics[label])),
304
+ # }
305
+ # for label in label2id
306
+ # }
307
+ # return metrics
@@ -0,0 +1,119 @@
1
+ from torch.utils.data import Dataset
2
+ import pandas as pd
3
+ from PIL import Image
4
+ from datasets import Dataset, Features, Image, Value
5
+
6
+
7
+ class SegFormerSegmentationDataset(Dataset):
8
+ """PyTorch dataset class for semantic segmentation using
9
+ HuggingFaces SegFormer model.
10
+
11
+ """
12
+
13
+ def __init__(
14
+ self, df: pd.DataFrame, extras: bool = False, group: str = "wsi_name"
15
+ ):
16
+ """Initiate an instance of the segmentation dataset.
17
+
18
+ Args:
19
+ df (pandas.DataFrame): Must have columns "fp" and "mask_fp".
20
+ extras (bool, optional): Whether to include additional
21
+ columns "x", "y" and "group" in the dataset. Default
22
+ is False.
23
+ group (str | None, optional): Additional column to add when
24
+ extras is True. Default is "wsi_name".
25
+
26
+ """
27
+ self.image_files = df["fp"].tolist()
28
+ self.mask_files = df["mask_fp"].tolist()
29
+ self.extras = extras
30
+
31
+ if extras:
32
+ self.x = df["x"].tolist()
33
+ self.y = df["y"].tolist()
34
+ self.group = df[group].tolist()
35
+
36
+ def __len__(self):
37
+ return len(self.image_files)
38
+
39
+ def __getitem__(self, idx):
40
+ # Get the filepath to the image and its mask.
41
+ image_path = self.image_files[idx]
42
+ mask_path = self.mask_files[idx]
43
+
44
+ if self.extras:
45
+ return {
46
+ "pixel_values": Image.open(image_path),
47
+ "label": Image.open(mask_path),
48
+ "x": self.x[idx],
49
+ "y": self.y[idx],
50
+ "group": self.group[idx],
51
+ }
52
+ else:
53
+ return {
54
+ "pixel_values": Image.open(image_path),
55
+ "label": Image.open(mask_path),
56
+ }
57
+
58
+
59
+ def dataset_generator(dataset):
60
+ """Yield a dataset."""
61
+ for item in dataset:
62
+ yield item
63
+
64
+
65
+ def create_segformer_segmentation_dataset(
66
+ df: pd.DataFrame | str,
67
+ low_memory: bool = True,
68
+ transforms=None,
69
+ extras: bool = False,
70
+ group: str | None = None,
71
+ ) -> SegFormerSegmentationDataset:
72
+ """Create a SegFormer segmentation dataset from a DataFrame.
73
+
74
+ Args:
75
+ df (pandas.DataFrame): Must have columns "fp" and "mask_fp".
76
+ Additionally, it can have columns "x", "y". If these are not
77
+ present, then they will be set to 0.
78
+ low_memory (bool, optional): Whether to read the CSV file in low
79
+ memory mode. Default is True.
80
+ transforms: Function to add transforms to the images.
81
+ group (str | None, optional): Additional column to pass to
82
+ the model, if None it will be an empty string. Default
83
+ is None.
84
+
85
+ Returns:
86
+ (SegFormerSegmentationDataset): A Dataset object to be used for
87
+ HuggingFaces SegFormer model training.
88
+
89
+ """
90
+ if isinstance(df, str):
91
+ df = pd.read_csv(df, low_memory=low_memory)
92
+
93
+ dataset = SegFormerSegmentationDataset(df, group=group, extras=extras)
94
+
95
+ if extras:
96
+ features = Features(
97
+ {
98
+ "pixel_values": Image(),
99
+ "label": Image(),
100
+ "x": Value("int32"),
101
+ "y": Value("int32"),
102
+ "group": Value("string"),
103
+ }
104
+ )
105
+ else:
106
+ features = Features(
107
+ {
108
+ "pixel_values": Image(),
109
+ "label": Image(),
110
+ }
111
+ )
112
+ dataset = Dataset.from_generator(
113
+ generator=lambda: dataset_generator(dataset), features=features
114
+ )
115
+
116
+ if transforms is not None:
117
+ dataset.set_transform(transforms)
118
+
119
+ return dataset
@@ -0,0 +1,126 @@
1
+ from torch import nn
2
+ import pandas as pd
3
+ import numpy as np
4
+ import torch
5
+
6
+ from .transforms import val_transforms
7
+ from .datasets import create_segformer_segmentation_dataset
8
+
9
+
10
+ def per_class_dice_on_dataset(
11
+ model: nn.Module,
12
+ data: pd.DataFrame | str,
13
+ label2id: dict[str, int],
14
+ batch_size: int = 16,
15
+ device: torch.device | None = None,
16
+ tile_size: int = 512,
17
+ tqdm_notebook: bool = False,
18
+ ):
19
+ """Calculate the DICE coefficient for a given model and set of
20
+ images. The model must be a semantic segmentation model that outputs
21
+ an object with the logits attribute. The dataset must be iterable
22
+ and return a dictionary with the keys "pixel_values" and "labels".
23
+ The pixel values should be in the format the model expects for the
24
+ input.
25
+
26
+ Args:
27
+ model (nn.Module): The model to evaluate.
28
+ data (pd.DataFrame | str): The dataframe or path to a csv file
29
+ containing the data to evaluate on. The dataframe must have
30
+ the "fp" (filepath to image) and "mask_fp" (filepath to
31
+ mask) columns.
32
+ label2id (dict): A dictionary mapping labels to their integer
33
+ representations.
34
+ batch_size (int, optional): The batch size to use for evaluation.
35
+ Default is 16.
36
+ device (torch.device | None, optional): The device to use for
37
+ evaluation. If None, the device will be "cuda" if available,
38
+ otherwise "cpu". Default is None.
39
+ tile_size (int, optional): The size of the tiles to use for
40
+ evaluation. Default is 512.
41
+ tqdm_notebook (bool, optional): Whether to use a tqdm notebook.
42
+ Default is False.
43
+
44
+ Returns:
45
+ dict: A dictionary with the keys "mean_dice" and the DICE
46
+ coefficient for each label.
47
+ """
48
+ if tqdm_notebook:
49
+ from tqdm.notebook import tqdm
50
+ else:
51
+ from tqdm import tqdm
52
+
53
+ if isinstance(data, str):
54
+ data = pd.read_csv(data)
55
+
56
+ # Create the dataset object.
57
+ dataset = create_segformer_segmentation_dataset(
58
+ data, transforms=val_transforms
59
+ )
60
+
61
+ if device is None:
62
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
63
+ print(f"Using device: {device}")
64
+
65
+ # Move the model to the device.
66
+ model.to(device)
67
+ model.eval() # model should be in evaluation mode
68
+
69
+ # Number of images in the dataset.
70
+ n = len(dataset)
71
+
72
+ # Initialize the intersection and denominator for each label.
73
+ intersection = {label: 0 for label in label2id}
74
+ denominator = {label: 0 for label in label2id}
75
+
76
+ batches = list(range(0, n, batch_size))
77
+
78
+ for i in tqdm(batches):
79
+ # Get the batch of images and labels.
80
+ batch = dataset[i : i + batch_size]
81
+
82
+ inputs = np.array(batch["pixel_values"])
83
+ labels = np.array(batch["labels"])
84
+ inputs = torch.tensor(inputs, dtype=torch.float32)
85
+ labels = np.array(labels)
86
+
87
+ with torch.no_grad():
88
+ inputs = inputs.to(device)
89
+ outputs = model(inputs)
90
+ logits = outputs.logits
91
+
92
+ # Get the logits out, resizing them to the original tile size.
93
+ logits = torch.nn.functional.interpolate(
94
+ logits,
95
+ size=tile_size,
96
+ mode="bilinear",
97
+ )
98
+
99
+ # Get predicted class labels for each pixel.
100
+ masks = torch.argmax(logits, dim=1).detach().cpu().numpy()
101
+
102
+ # Flatten both masks and labels.
103
+ masks = masks.flatten()
104
+ labels = labels.flatten()
105
+
106
+ for label, integer in label2id.items():
107
+ # Calculate where both mask and labels are equal to the integer.
108
+ gt_mask = labels == integer
109
+ pred_mask = masks == integer
110
+ intersection[label] += np.sum(gt_mask & pred_mask)
111
+ denominator[label] += np.sum(gt_mask) + np.sum(pred_mask)
112
+
113
+ # Calculate the dice for each class.
114
+ metrics = {}
115
+ for label in label2id:
116
+ denominator_value = denominator[label]
117
+
118
+ if denominator_value:
119
+ metrics[f"{label}_dice"] = float(
120
+ 2 * intersection[label] / denominator[label]
121
+ )
122
+ else:
123
+ metrics[f"{label}_dice"] = 1
124
+
125
+ # Calculate the mean dice.
126
+ return metrics