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.
- {dsa_helpers-1.1.4.dev1/dsa_helpers.egg-info → dsa_helpers-1.1.5.dev0}/PKG-INFO +1 -1
- {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers/__init__.py +1 -1
- {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers/girder_utils.py +28 -2
- dsa_helpers-1.1.5.dev0/dsa_helpers/ml/evaluate.py +425 -0
- dsa_helpers-1.1.5.dev0/dsa_helpers/ml/segformer_semantic_segmentation/train.py +285 -0
- {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0/dsa_helpers.egg-info}/PKG-INFO +1 -1
- {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers.egg-info/SOURCES.txt +1 -0
- {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/pyproject.toml +1 -1
- dsa_helpers-1.1.4.dev1/dsa_helpers/ml/evaluate.py +0 -136
- {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/.gitignore +0 -0
- {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/.readthedocs.yaml +0 -0
- {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/LICENSE +0 -0
- {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/README.md +0 -0
- {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/app.py +0 -0
- {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dev.py +0 -0
- {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/docs/requirements.txt +0 -0
- {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/docs/source/conf.py +0 -0
- {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/docs/source/index.rst +0 -0
- {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers/cli_utils.py +0 -0
- {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers/dash/__init__.py +0 -0
- {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers/dash/dash_paperdragon_utils.py +0 -0
- {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers/dash/header.py +0 -0
- {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers/dash/header_callbacks.py +0 -0
- {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers/gpd_utils.py +0 -0
- {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers/image_utils.py +0 -0
- {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers/imread.py +0 -0
- {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers/imwrite.py +0 -0
- {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers/ml/__init__.py +0 -0
- {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers/ml/callbacks.py +0 -0
- {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers/ml/datasets/SegFormerSegmentationDataset.py +0 -0
- {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers/ml/datasets/__init__.py +0 -0
- {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers/ml/datasets/utils.py +0 -0
- {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers/ml/metrics.py +0 -0
- {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers/ml/object_detection/__init__.py +0 -0
- {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers/ml/object_detection/get_pearce_roi_images.py +0 -0
- {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers/ml/object_detection/tile_image.py +0 -0
- {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers/ml/object_detection/utils.py +0 -0
- {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers/ml/segformer_semantic_segmentation/__init__.py +0 -0
- {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers/ml/segformer_semantic_segmentation/inference.py +0 -0
- {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers/ml/transforms/__init__.py +0 -0
- {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers/ml/transforms/segformer_transforms.py +0 -0
- {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers/mongo_utils.py +0 -0
- {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers/tile_utils.py +0 -0
- {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers/tiling.py +0 -0
- {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers/utils.py +0 -0
- {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers.egg-info/dependency_links.txt +0 -0
- {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers.egg-info/requires.txt +0 -0
- {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers.egg-info/top_level.txt +0 -0
- {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/setup.cfg +0 -0
- {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/tests/app/Dockerfile +0 -0
- {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/tests/app/app.py +0 -0
- {dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/tests/docker-compose.yaml +0 -0
- {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.
|
|
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>
|
|
@@ -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.
|
|
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
|
|
@@ -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))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers/dash/dash_paperdragon_utils.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers/ml/object_detection/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers/ml/object_detection/tile_image.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dsa_helpers-1.1.4.dev1 → dsa_helpers-1.1.5.dev0}/dsa_helpers/ml/transforms/segformer_transforms.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|