dsa-helpers 1.1.4.dev1__tar.gz → 1.1.5.dev0__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 (53) hide show
  1. {dsa_helpers-1.1.4.dev1/dsa_helpers.egg-info → dsa_helpers-1.1.5.dev0}/PKG-INFO +1 -1
  2. {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers/__init__.py +1 -1
  3. {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers/girder_utils.py +28 -2
  4. dsa_helpers-1.1.5.dev0/dsa_helpers/ml/evaluate.py +425 -0
  5. dsa_helpers-1.1.5.dev0/dsa_helpers/ml/segformer_semantic_segmentation/train.py +285 -0
  6. {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0/dsa_helpers.egg-info}/PKG-INFO +1 -1
  7. {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers.egg-info/SOURCES.txt +1 -0
  8. {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/pyproject.toml +1 -1
  9. dsa_helpers-1.1.4.dev1/dsa_helpers/ml/evaluate.py +0 -136
  10. {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/.gitignore +0 -0
  11. {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/.readthedocs.yaml +0 -0
  12. {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/LICENSE +0 -0
  13. {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/README.md +0 -0
  14. {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/app.py +0 -0
  15. {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dev.py +0 -0
  16. {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/docs/requirements.txt +0 -0
  17. {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/docs/source/conf.py +0 -0
  18. {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/docs/source/index.rst +0 -0
  19. {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers/cli_utils.py +0 -0
  20. {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers/dash/__init__.py +0 -0
  21. {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers/dash/dash_paperdragon_utils.py +0 -0
  22. {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers/dash/header.py +0 -0
  23. {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers/dash/header_callbacks.py +0 -0
  24. {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers/gpd_utils.py +0 -0
  25. {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers/image_utils.py +0 -0
  26. {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers/imread.py +0 -0
  27. {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers/imwrite.py +0 -0
  28. {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers/ml/__init__.py +0 -0
  29. {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers/ml/callbacks.py +0 -0
  30. {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers/ml/datasets/SegFormerSegmentationDataset.py +0 -0
  31. {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers/ml/datasets/__init__.py +0 -0
  32. {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers/ml/datasets/utils.py +0 -0
  33. {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers/ml/metrics.py +0 -0
  34. {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers/ml/object_detection/__init__.py +0 -0
  35. {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers/ml/object_detection/get_pearce_roi_images.py +0 -0
  36. {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers/ml/object_detection/tile_image.py +0 -0
  37. {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers/ml/object_detection/utils.py +0 -0
  38. {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers/ml/segformer_semantic_segmentation/__init__.py +0 -0
  39. {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers/ml/segformer_semantic_segmentation/inference.py +0 -0
  40. {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers/ml/transforms/__init__.py +0 -0
  41. {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers/ml/transforms/segformer_transforms.py +0 -0
  42. {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers/mongo_utils.py +0 -0
  43. {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers/tile_utils.py +0 -0
  44. {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers/tiling.py +0 -0
  45. {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers/utils.py +0 -0
  46. {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers.egg-info/dependency_links.txt +0 -0
  47. {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers.egg-info/requires.txt +0 -0
  48. {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers.egg-info/top_level.txt +0 -0
  49. {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/setup.cfg +0 -0
  50. {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/tests/app/Dockerfile +0 -0
  51. {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/tests/app/app.py +0 -0
  52. {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/tests/docker-compose.yaml +0 -0
  53. {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/tests/test.ipynb +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dsa_helpers
3
- Version: 1.1.4.dev1
3
+ Version: 1.1.5.dev0
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>
@@ -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.4.dev1"
6
+ __version__ = "1.1.5.dev0"
7
7
 
8
8
  # To avoid slow downs, do not allow from dsa_helpers import * to import anything.
9
9
  __all__ = []
@@ -10,11 +10,11 @@ Functions:
10
10
  - get_roi_images: Get regions of interest (ROIs) as images from DSA annotations
11
11
  - post_annotation: Post a new annotation to the DSA
12
12
  - remove_overlapping_annotations: Remove overlapping regions from elements
13
-
13
+ - upload_dir_to_dsa: Upload a local directory to a DSA item
14
14
  """
15
15
 
16
16
  from girder_client import GirderClient, HttpError
17
- import pickle
17
+ import pickle, shutil, tempfile
18
18
  import numpy as np
19
19
  import pandas as pd
20
20
  from pathlib import Path
@@ -878,3 +878,29 @@ def post_annotation(gc: GirderClient, item_id: str, annotation: dict) -> dict:
878
878
  parameters={"itemId": item_id},
879
879
  json=annotation,
880
880
  )
881
+
882
+
883
+ def upload_dir_to_dsa(
884
+ gc: GirderClient,
885
+ checkpoint_dir: str,
886
+ item_id: str,
887
+ ):
888
+ """Upload a local directory to a DSA item.
889
+
890
+ Args:
891
+ gc (girder_client.GirderClient): The girder client.
892
+ checkpoint_dir (str): The path to the local directory to upload.
893
+ item_id (str): The ID of the DSA item to upload the directory to.
894
+
895
+ """
896
+ with tempfile.TemporaryDirectory() as temp_dir:
897
+ temp_dir = Path(temp_dir)
898
+
899
+ # Zip the directory.
900
+ shutil.make_archive(temp_dir / "checkpoint", "zip", checkpoint_dir)
901
+
902
+ # Upload the zip file to the DSA item.
903
+ _ = gc.uploadFileToItem(
904
+ item_id,
905
+ str(temp_dir / "checkpoint.zip"),
906
+ )
@@ -0,0 +1,425 @@
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 per_class_dice_on_dataset(
147
+ model: nn.Module,
148
+ data: pd.DataFrame | str,
149
+ label2id: dict[str, int],
150
+ batch_size: int = 16,
151
+ device: torch.device | None = None,
152
+ tile_size: int = 512,
153
+ tqdm_notebook: bool = False,
154
+ ):
155
+ """Calculate the DICE coefficient for a given model and set of
156
+ images. The model must be a semantic segmentation model that outputs
157
+ an object with the logits attribute. The dataset must be iterable
158
+ and return a dictionary with the keys "pixel_values" and "labels".
159
+ The pixel values should be in the format the model expects for the
160
+ input.
161
+
162
+ Args:
163
+ model (nn.Module): The model to evaluate.
164
+ data (pd.DataFrame | str): The dataframe or path to a csv file
165
+ containing the data to evaluate on. The dataframe must have
166
+ the "fp" (filepath to image) and "mask_fp" (filepath to
167
+ mask) columns.
168
+ label2id (dict): A dictionary mapping labels to their integer
169
+ representations.
170
+ batch_size (int, optional): The batch size to use for evaluation.
171
+ Default is 16.
172
+ device (torch.device | None, optional): The device to use for
173
+ evaluation. If None, the device will be "cuda" if available,
174
+ otherwise "cpu". Default is None.
175
+ tile_size (int, optional): The size of the tiles to use for
176
+ evaluation. Default is 512.
177
+ tqdm_notebook (bool, optional): Whether to use a tqdm notebook.
178
+ Default is False.
179
+
180
+ Returns:
181
+ dict: A dictionary with the keys "mean_dice" and the DICE
182
+ coefficient for each label.
183
+ """
184
+ if tqdm_notebook:
185
+ from tqdm.notebook import tqdm
186
+ else:
187
+ from tqdm import tqdm
188
+
189
+ if isinstance(data, str):
190
+ data = pd.read_csv(data)
191
+
192
+ # Create the dataset object.
193
+ dataset = create_segformer_segmentation_dataset(
194
+ data, transforms=val_transforms
195
+ )
196
+
197
+ if device is None:
198
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
199
+
200
+ # Move the model to the device.
201
+ model.to(device)
202
+ model.eval() # model should be in evaluation mode
203
+
204
+ # Number of images in the dataset.
205
+ n = len(dataset)
206
+
207
+ # Initialize the intersection and denominator for each label.
208
+ intersection = {label: 0 for label in label2id}
209
+ denominator = {label: 0 for label in label2id}
210
+
211
+ batches = list(range(0, n, batch_size))
212
+
213
+ for i in tqdm(batches):
214
+ # Get the batch of images and labels.
215
+ batch = dataset[i : i + batch_size]
216
+
217
+ inputs = np.array(batch["pixel_values"])
218
+ labels = np.array(batch["labels"])
219
+ inputs = torch.tensor(inputs, dtype=torch.float32)
220
+ labels = np.array(labels)
221
+
222
+ with torch.no_grad():
223
+ inputs = inputs.to(device)
224
+ outputs = model(inputs)
225
+ logits = outputs.logits
226
+
227
+ # Get the logits out, resizing them to the original tile size.
228
+ logits = torch.nn.functional.interpolate(
229
+ logits,
230
+ size=tile_size,
231
+ mode="bilinear",
232
+ )
233
+
234
+ # Get predicted class labels for each pixel.
235
+ masks = torch.argmax(logits, dim=1).detach().cpu().numpy()
236
+
237
+ # Flatten both masks and labels.
238
+ masks = masks.flatten()
239
+ labels = labels.flatten()
240
+
241
+ for label, integer in label2id.items():
242
+ # Calculate where both mask and labels are equal to the integer.
243
+ gt_mask = labels == integer
244
+ pred_mask = masks == integer
245
+ intersection[label] += np.sum(gt_mask & pred_mask)
246
+ denominator[label] += np.sum(gt_mask) + np.sum(pred_mask)
247
+
248
+ # Calculate the dice for each class.
249
+ metrics = {}
250
+ for label in label2id:
251
+ denominator_value = denominator[label]
252
+
253
+ if denominator_value:
254
+ metrics[f"{label}_dice"] = float(
255
+ 2 * intersection[label] / denominator[label]
256
+ )
257
+ else:
258
+ metrics[f"{label}_dice"] = 1
259
+
260
+ # Calculate the mean dice.
261
+ return metrics
262
+
263
+
264
+ def evaluate_semantic_segmentation_segformer_by_wsi(
265
+ model: nn.Module,
266
+ data: pd.DataFrame | str,
267
+ group_col: str,
268
+ label2id: dict[str, int],
269
+ batch_size: int = 16,
270
+ tile_size: int = 512,
271
+ background_label: int | None = None,
272
+ device: torch.device | None = None,
273
+ ) -> float:
274
+ """Evaluate a segformer trainer model on a specific dataset provided
275
+ as a dataframe, grouped by a specific column. For a unique value of
276
+ the column it will predict on the images for that group and then
277
+ merge the predictions into a single multipolygon. The mean IoU will
278
+ be calculated agains the ground truth masks for that group. The
279
+ reported mean IoU will be the average of all the mean IoUs for each
280
+ group.
281
+
282
+ Args:
283
+ model (nn.Module): The model to evaluate.
284
+ data (pd.DataFrame | str): The dataframe or path to a csv file
285
+ containing the data to evaluate on. The dataframe must have
286
+ the "fp" (filepath to image) and "mask_fp" (filepath to
287
+ mask) columns. The column matching the "group_col" parameter
288
+ will be used to group tiles into groups. The "x" and "y"
289
+ parameters will be used for stiching the images back to
290
+ their original size.
291
+ group_col (str): The column to group the tiles by.
292
+ batch_size (int): The batch size to use for prediction. Note that
293
+ this is not the batch size use for predictions, rather it is
294
+ how many masks will be stored in memory. Batch size used
295
+ for predictions is specified on the trainer input. Default is
296
+ 16.
297
+ label2id (dict): A dictionary mapping labels to their integer
298
+ representations.
299
+ tile_size (int): The size of the tiles. Default is 512.
300
+ background_label (int | None): The label to use for the background
301
+ class, which will be ignored. If None then no label will be
302
+ ignored. Default is None.
303
+ device (torch.device | None, optional): The device to use for
304
+ evaluation. If None, the device will be "cuda" if available,
305
+ otherwise "cpu". Default is None.
306
+
307
+ Returns:
308
+ float: The mean IoU across all groups.
309
+
310
+ """
311
+ # Read the data.
312
+ if isinstance(data, str):
313
+ data = pd.read_csv(data)
314
+
315
+ ious = []
316
+
317
+ if device is None:
318
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
319
+
320
+ # Move the model to the device.
321
+ model.to(device)
322
+ model.eval() # model should be in evaluation mode
323
+
324
+ # Predict on the images for each group separately.
325
+ groups = list(data[group_col].unique())
326
+ for group in tqdm(groups, desc="Evaluating groups"):
327
+ # Get the tiles in this group.
328
+ group_df = data[data[group_col] == group]
329
+
330
+ pred_tile_list = []
331
+ true_tile_list = []
332
+
333
+ # Predict on the group in batches.
334
+ for i in range(0, len(group_df), batch_size):
335
+ # Create dataset for this batch.
336
+ batch_df = group_df.iloc[i : i + batch_size]
337
+ x_list = batch_df["x"].tolist()
338
+ y_list = batch_df["y"].tolist()
339
+
340
+ # Create dataset for this batch.
341
+ dataset = create_segformer_segmentation_dataset(
342
+ batch_df, transforms=val_transforms
343
+ )
344
+
345
+ # Get the inputs.
346
+ inputs = np.array(dataset[:]["pixel_values"])
347
+ inputs = torch.tensor(inputs, dtype=torch.float32)
348
+ inputs = inputs.to(device)
349
+
350
+ true = np.array(dataset[:]["labels"])
351
+
352
+ # Predict on the batch.
353
+ with torch.no_grad():
354
+ out = model(inputs)
355
+ preds = out.logits
356
+
357
+ # Reshape the prediction logits to tile size.
358
+ preds = torch.from_numpy(preds)
359
+
360
+ preds = nn.functional.interpolate(
361
+ preds,
362
+ size=(tile_size, tile_size),
363
+ mode="bilinear",
364
+ align_corners=False,
365
+ ).argmax(
366
+ dim=1
367
+ ) # logits -> class predictions
368
+
369
+ preds = preds.detach().cpu().numpy()
370
+
371
+ # Append to the tile list.
372
+ pred_tile_list.extend(
373
+ [(pred, x, y) for x, y, pred in zip(x_list, y_list, preds)]
374
+ )
375
+
376
+ true_tile_list.extend(
377
+ [(mask, x, y) for x, y, mask in zip(x_list, y_list, true)]
378
+ )
379
+
380
+ # Merge the tile masks.
381
+ pred_gdf = merge_tile_masks(
382
+ pred_tile_list, background_label=background_label
383
+ )
384
+ true_gdf = merge_tile_masks(
385
+ true_tile_list, background_label=background_label
386
+ )
387
+
388
+ # Calculate the Dice for each label.
389
+ metrics = {label: [] for label in label2id}
390
+
391
+ for label, integer in label2id.items():
392
+ if integer == background_label:
393
+ continue # don't calculate dice for background
394
+
395
+ # For this integer, get the prediction and ground truth.
396
+ pred_label_gdf = pred_gdf[pred_gdf["label"] == integer]
397
+ true_label_gdf = true_gdf[true_gdf["label"] == integer]
398
+
399
+ # If both are empty, the dice is 1.
400
+ if pred_label_gdf.empty and true_label_gdf.empty:
401
+ metrics[label].append(1)
402
+ else:
403
+ # Calculate the intersection and denominator.
404
+ intersection = pred_label_gdf.geometry.intersection(
405
+ true_label_gdf.geometry
406
+ ).area.sum()
407
+
408
+ # The denominator is the sum area of the two polygons.
409
+ denominator = (
410
+ pred_label_gdf.geometry.area.sum()
411
+ + true_label_gdf.geometry.area.sum()
412
+ )
413
+
414
+ # The dice is 2 * intersection / denominator.
415
+ metrics[label].append(float(2 * intersection / denominator))
416
+
417
+ # Return the dice as means and standard deviations.
418
+ metrics = {
419
+ label: {
420
+ "mean": float(np.mean(metrics[label])),
421
+ "std": float(np.std(metrics[label])),
422
+ }
423
+ for label in label2id
424
+ }
425
+ return metrics
@@ -0,0 +1,285 @@
1
+ import pandas as pd
2
+ from transformers import (
3
+ SegformerForSemanticSegmentation,
4
+ TrainingArguments,
5
+ Trainer,
6
+ )
7
+ import numpy as np
8
+ from pathlib import Path
9
+
10
+ import torch
11
+ from torch import nn
12
+
13
+ from ..datasets.utils import create_segformer_segmentation_dataset
14
+ from ..transforms.segformer_transforms import (
15
+ train_transforms,
16
+ val_transforms,
17
+ )
18
+ from ..callbacks import MetricsLoggerCallback
19
+ from ..evaluate import per_class_dice_on_dataset
20
+
21
+
22
+ def train_segformer_semantic_segmentation_model(
23
+ save_dir: str,
24
+ train_data: pd.DataFrame | str,
25
+ val_data: pd.DataFrame | str,
26
+ label2id: dict[str, int],
27
+ device: torch.device | None = None,
28
+ test_data: pd.DataFrame | str | None = None,
29
+ model_checkpoint: str | None = None,
30
+ learning_rate: float = 1e-4,
31
+ epochs: int = 10,
32
+ batch_size: int = 16,
33
+ eval_accumulation_steps: int = 100,
34
+ max_val_size: int | None = 500,
35
+ random_state: int = 42,
36
+ tile_size: int = 512,
37
+ tqdm_notebook: bool = False,
38
+ ):
39
+ """Workflow for training a SegFormer semantic segmentation model.
40
+
41
+ Args:
42
+ save_dir (str): Directory where model checkpoint directory will
43
+ be saved to.
44
+ train_data (pd.DataFrame | str): Train data, should be a pandas
45
+ dataframe or a path to a csv file containing the train data.
46
+ Must have the "fp" and "mask_fp" columns, which are the
47
+ filepaths to the images and masks, respectively.
48
+ val_data (pd.DataFrame | str): See train_data, but for the
49
+ validation data.
50
+ label2id (dict): A dictionary mapping the label names to their
51
+ corresponding integer ids.
52
+ device (torch.device | None, optional): Device to run the model
53
+ on. If None, will use "cuda" if available, otherwise "cpu".
54
+ Defaults to None.
55
+ test_data (pd.DataFrame | str | None, optional): See train_data,
56
+ but for the test data. Defaults to None.
57
+ model_checkpoint (str | None, optional): A local path to a model
58
+ checkpoint to load the model from. If None it will use the
59
+ default weights from huggingface: "nvidia/mit-b0". Defaults
60
+ to None.
61
+ learning_rate (float, optional): Learning rate for the model.
62
+ Defaults to 1e-4.
63
+ epochs (int, optional): Number of epochs to train the model.
64
+ Defaults to 10.
65
+ batch_size (int, optional): Batch size for the model.
66
+ Defaults to 16.
67
+ eval_accumulation_steps (int, optional): Number of steps to
68
+ accumulate the validation metrics over. Defaults to 100.
69
+ max_val_size (int | None, optional): Maximum number of
70
+ validation samples to use. If None, all validation samples
71
+ will be used. Defaults to None. Note that there is a bug
72
+ that this value must be kept short otherwise a cuda memory
73
+ error will occur.
74
+ random_state (int, optional): Random state for the validation
75
+ split. Defaults to 42.
76
+ tile_size (int, optional): Size of the tiles to use for the model.
77
+ Defaults to 512.
78
+ tqdm_notebook (bool, optional): Whether to use a tqdm notebook.
79
+ Defaults to False.
80
+
81
+ Returns:
82
+ tuple: A tuple containing the trainer and the results dictionary.
83
+
84
+ """
85
+ # Read the dataframes if they are strings.
86
+ if isinstance(train_data, str):
87
+ train_data = pd.read_csv(train_data)
88
+ if isinstance(val_data, str):
89
+ val_data = pd.read_csv(val_data)
90
+ if test_data is not None and isinstance(test_data, str):
91
+ test_data = pd.read_csv(test_data)
92
+
93
+ if device is None:
94
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
95
+
96
+ # Load the default pretrained model.
97
+ label2id = {k: int(v) for k, v in label2id.items()}
98
+ id2label = {int(v): k for k, v in label2id.items()}
99
+
100
+ if model_checkpoint is None:
101
+ model = SegformerForSemanticSegmentation.from_pretrained(
102
+ "nvidia/mit-b0", id2label=id2label, label2id=label2id
103
+ ).to(device)
104
+ else:
105
+ model = SegformerForSemanticSegmentation.from_pretrained(
106
+ model_checkpoint,
107
+ id2label=id2label,
108
+ label2id=label2id,
109
+ local_files_only=True,
110
+ ignore_mismatched_sizes=True,
111
+ ).to(device)
112
+
113
+ # Training arguments.
114
+ training_args = TrainingArguments(
115
+ str(save_dir),
116
+ learning_rate=learning_rate,
117
+ num_train_epochs=epochs,
118
+ per_device_train_batch_size=batch_size,
119
+ per_device_eval_batch_size=batch_size,
120
+ eval_accumulation_steps=eval_accumulation_steps,
121
+ eval_strategy="epoch",
122
+ save_strategy="epoch",
123
+ logging_strategy="epoch",
124
+ load_best_model_at_end=True,
125
+ save_total_limit=1,
126
+ )
127
+
128
+ # Create datasets for the model.
129
+ train_dataset = create_segformer_segmentation_dataset(
130
+ train_data, transforms=train_transforms
131
+ )
132
+
133
+ val_dataset = create_segformer_segmentation_dataset(
134
+ (
135
+ val_data
136
+ if max_val_size is None
137
+ else val_data.sample(n=max_val_size, random_state=random_state)
138
+ ),
139
+ transforms=val_transforms,
140
+ )
141
+
142
+ def compute_metrics(eval_pred):
143
+ """Function for computing DICE coefficient metrics for each
144
+ class, as well as the mean DICE and weighted mean DICE (maybe).
145
+ """
146
+ logits, labels = eval_pred
147
+
148
+ # Convert logits to tensor.
149
+ logits = torch.from_numpy(logits)
150
+
151
+ # Scale the logits back to the shape of the labels.
152
+ logits = nn.functional.interpolate(
153
+ logits,
154
+ size=tile_size,
155
+ mode="bilinear",
156
+ align_corners=False,
157
+ )
158
+
159
+ # Turn the logits to predictions.
160
+ pred = logits.argmax(dim=1)
161
+
162
+ # Calculate the Dice for each class.
163
+ intersection = {label: 0 for label in label2id}
164
+ denominator = {label: 0 for label in label2id}
165
+
166
+ # For easy calculations, convert pred to a numpy.
167
+ pred = pred.numpy()
168
+
169
+ # Flatten both arrays.
170
+ pred = pred.flatten()
171
+ labels = labels.flatten()
172
+
173
+ # Loop through each index.
174
+ for label, integer in label2id.items():
175
+ pred_mask = pred == integer
176
+ gt_mask = labels == integer
177
+
178
+ # Calculate the interesection.
179
+ intersection[label] += np.sum(gt_mask & pred_mask)
180
+ denominator[label] += np.sum(gt_mask) + np.sum(pred_mask)
181
+
182
+ # Calculate the Dice for each label.
183
+ metrics = {}
184
+ for label in label2id:
185
+ denominator_value = denominator[label]
186
+
187
+ if denominator_value:
188
+ metrics[label] = float(
189
+ 2 * intersection[label] / denominator_value
190
+ )
191
+ else:
192
+ metrics[label] = 1
193
+
194
+ # Add the mean Dice.
195
+ metrics["mean_dice"] = float(np.mean(list(metrics.values())))
196
+ return metrics
197
+
198
+ # Trainer
199
+ trainer = Trainer(
200
+ model=model,
201
+ args=training_args,
202
+ train_dataset=train_dataset,
203
+ eval_dataset=val_dataset,
204
+ compute_metrics=compute_metrics,
205
+ callbacks=[MetricsLoggerCallback], # Add the custom callback here
206
+ )
207
+
208
+ # Train the model.
209
+ _ = trainer.train()
210
+
211
+ # Rename the checkpoint to not include a step number.
212
+ save_dir = Path(save_dir)
213
+ for p in save_dir.iterdir():
214
+ if p.is_dir() and p.name.startswith("checkpoint-"):
215
+ p.rename(save_dir / "checkpoint")
216
+
217
+ results = {"train": {}, "val": {}}
218
+
219
+ for log in trainer.state.log_history[:-1]:
220
+ epoch = log["epoch"]
221
+
222
+ if epoch % 1:
223
+ continue # only track end of each epoch
224
+
225
+ # Check if this is tracking train or validation.
226
+ if "eval_loss" in log:
227
+ data = results["val"]
228
+ else:
229
+ data = results["train"]
230
+
231
+ for k, v in log.items():
232
+ if k.startswith("eval_"):
233
+ k = k[5:]
234
+
235
+ if k not in data:
236
+ data[k] = []
237
+
238
+ data[k].append(v)
239
+
240
+ # For each of the datasets provided, get the metrics.
241
+ model = trainer.model
242
+
243
+ dataset = create_segformer_segmentation_dataset(
244
+ train_data, transforms=val_transforms
245
+ )
246
+ results["train"]["metrics"] = per_class_dice_on_dataset(
247
+ model,
248
+ dataset,
249
+ label2id,
250
+ batch_size=batch_size,
251
+ device=device,
252
+ tile_size=tile_size,
253
+ tqdm_notebook=tqdm_notebook,
254
+ )
255
+
256
+ dataset = create_segformer_segmentation_dataset(
257
+ val_data, transforms=val_transforms
258
+ )
259
+ results["val"]["metrics"] = per_class_dice_on_dataset(
260
+ model,
261
+ dataset,
262
+ label2id,
263
+ batch_size=batch_size,
264
+ device=device,
265
+ tile_size=tile_size,
266
+ tqdm_notebook=tqdm_notebook,
267
+ )
268
+
269
+ if test_data is not None:
270
+ results["test"] = {}
271
+
272
+ dataset = create_segformer_segmentation_dataset(
273
+ test_data, transforms=val_transforms
274
+ )
275
+ results["test"]["metrics"] = per_class_dice_on_dataset(
276
+ model,
277
+ dataset,
278
+ label2id,
279
+ batch_size=batch_size,
280
+ device=device,
281
+ tile_size=tile_size,
282
+ tqdm_notebook=tqdm_notebook,
283
+ )
284
+
285
+ return trainer, results
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dsa_helpers
3
- Version: 1.1.4.dev1
3
+ Version: 1.1.5.dev0
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>
@@ -41,6 +41,7 @@ dsa_helpers/ml/object_detection/tile_image.py
41
41
  dsa_helpers/ml/object_detection/utils.py
42
42
  dsa_helpers/ml/segformer_semantic_segmentation/__init__.py
43
43
  dsa_helpers/ml/segformer_semantic_segmentation/inference.py
44
+ dsa_helpers/ml/segformer_semantic_segmentation/train.py
44
45
  dsa_helpers/ml/transforms/__init__.py
45
46
  dsa_helpers/ml/transforms/segformer_transforms.py
46
47
  tests/docker-compose.yaml
@@ -37,7 +37,7 @@ classifiers = [
37
37
  "License :: OSI Approved :: MIT License",
38
38
  ]
39
39
  keywords = ["dsa", "girder", "girder_client"]
40
- version = "v1.1.4.dev1"
40
+ version = "v1.1.5.dev0"
41
41
 
42
42
  [project.urls]
43
43
  Repository = "https://github.com/Gutman-Lab/dsa-python-utils"
@@ -1,136 +0,0 @@
1
- """This module contains functions for evaluating model performance on
2
- datasets."""
3
-
4
- import torch
5
- from torch import nn
6
- from tqdm import tqdm
7
- from transformers.trainer import Trainer
8
- import pandas as pd
9
-
10
- from .datasets.utils import create_segformer_segmentation_dataset
11
- from .transforms.segformer_transforms import val_transforms
12
- from ..tile_utils import merge_tile_masks
13
-
14
-
15
- def evaluate_semantic_segmentation_segformer_group(
16
- trainer: Trainer,
17
- df: pd.DataFrame,
18
- group_col: str,
19
- batch_size: int = 16,
20
- tile_size: int = 512,
21
- background_label: int | None = None,
22
- ) -> float:
23
- """Evaluate a segformer trainer model on a specific dataset provided
24
- as a dataframe, grouped by a specific column. For a unique value of
25
- the column it will predict on the images for that group and then
26
- merge the predictions into a single multipolygon. The mean IoU will
27
- be calculated agains the ground truth masks for that group. The
28
- reported mean IoU will be the average of all the mean IoUs for each
29
- group.
30
-
31
- Args:
32
- trainer (Trainer): The trainer object with the model to evaluate.
33
- df (pandas.DataFrame): The dataframe with the tile metadata.
34
- Must have the fp column (file path), x column (x coordinate),
35
- y column (y coordinate), and group_col.
36
- group_col (str): The column to group the tiles by.
37
- batch_size (int): The batch size to use for prediction. Note that
38
- this is not the batch size use for predictions, rather it is
39
- how many masks will be stored in memory. Batch size used
40
- for predictions is specified on the trainer input. Default is
41
- 16.
42
- tile_size (int): The size of the tiles. Default is 512.
43
- background_label (int | None): The label to use for the background
44
- class, which will be ignored. If None then no label will be
45
- ignored. Default is None.
46
-
47
- Returns:
48
- float: The mean IoU across all groups.
49
-
50
- """
51
- ious = []
52
-
53
- # Predict on the images for each group separately.
54
- groups = list(df[group_col].unique())
55
- for group in tqdm(groups, desc="Evaluating groups"):
56
- # Get the tiles in this group.
57
- group_df = df[df[group_col] == group]
58
-
59
- pred_tile_list = []
60
- true_tile_list = []
61
-
62
- # Predict on the group in batches.
63
- for i in range(0, len(group_df), batch_size):
64
- # Create dataset for this batch.
65
- batch_df = group_df.iloc[i : i + batch_size]
66
- x_list = batch_df["x"].tolist()
67
- y_list = batch_df["y"].tolist()
68
-
69
- # Create dataset for this batch.
70
- group_dataset = create_segformer_segmentation_dataset(
71
- batch_df, transforms=val_transforms
72
- )
73
-
74
- # Predict on the batch.
75
- out = trainer.predict(group_dataset)
76
- preds = out[0]
77
- true = out[1]
78
-
79
- # Reshape the prediction logits to tile size.
80
- preds = torch.from_numpy(preds).cpu()
81
-
82
- preds = nn.functional.interpolate(
83
- preds,
84
- size=(tile_size, tile_size),
85
- mode="bilinear",
86
- align_corners=False,
87
- ).argmax(
88
- dim=1
89
- ) # logits -> class predictions
90
-
91
- preds = preds.detach().numpy()
92
-
93
- # Append to the tile list.
94
- pred_tile_list.extend(
95
- [(pred, x, y) for x, y, pred in zip(x_list, y_list, preds)]
96
- )
97
-
98
- true_tile_list.extend(
99
- [(mask, x, y) for x, y, mask in zip(x_list, y_list, true)]
100
- )
101
-
102
- # Merge the tile masks.
103
- pred_gdf = merge_tile_masks(
104
- pred_tile_list, background_label=background_label
105
- )
106
- true_gdf = merge_tile_masks(
107
- true_tile_list, background_label=background_label
108
- )
109
-
110
- # Get the unique labels in either the prediction or the ground truth.
111
- labels = set(pred_gdf["label"].unique()) | set(
112
- true_gdf["label"].unique()
113
- )
114
-
115
- # Calculate the IoU for each label.
116
- ious = []
117
-
118
- for label in labels:
119
- pred_label_gdf = pred_gdf[pred_gdf["label"] == label]
120
- true_label_gdf = true_gdf[true_gdf["label"] == label]
121
-
122
- # If either the prediction or the ground truth is empty, the IoU is 0.
123
- if pred_label_gdf.empty or true_label_gdf.empty:
124
- ious.append(0)
125
- else:
126
- # Calculate the IoU.
127
- geom1 = pred_label_gdf.iloc[0]["geometry"]
128
- geom2 = true_label_gdf.iloc[0]["geometry"]
129
-
130
- intersection_area = geom1.intersection(geom2).area
131
- union = geom1.union(geom2).area
132
-
133
- ious.append(intersection_area / union)
134
-
135
- # Return the average of the ious.
136
- return float(sum(ious) / len(ious))