dsa-helpers 1.1.5.dev5__tar.gz → 1.1.5.dev7__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.5.dev5/dsa_helpers.egg-info → dsa_helpers-1.1.5.dev7}/PKG-INFO +2 -1
- {dsa_helpers-1.1.5.dev5 → dsa_helpers-1.1.5.dev7}/dsa_helpers/__init__.py +1 -1
- {dsa_helpers-1.1.5.dev5 → dsa_helpers-1.1.5.dev7}/dsa_helpers/ml/datasets/SegFormerSegmentationDataset.py +10 -2
- {dsa_helpers-1.1.5.dev5 → dsa_helpers-1.1.5.dev7}/dsa_helpers/ml/datasets/utils.py +10 -1
- dsa_helpers-1.1.5.dev7/dsa_helpers/ml/evaluate.py +307 -0
- dsa_helpers-1.1.5.dev7/dsa_helpers/ml/segformer_semantic_segmentation/datasets.py +119 -0
- dsa_helpers-1.1.5.dev7/dsa_helpers/ml/segformer_semantic_segmentation/evaluate.py +126 -0
- {dsa_helpers-1.1.5.dev5 → dsa_helpers-1.1.5.dev7}/dsa_helpers/ml/segformer_semantic_segmentation/train.py +6 -6
- dsa_helpers-1.1.5.dev7/dsa_helpers/ml/segformer_semantic_segmentation/transforms.py +63 -0
- dsa_helpers-1.1.5.dev7/dsa_helpers/ml/transforms/segformer_transforms.py +40 -0
- {dsa_helpers-1.1.5.dev5 → dsa_helpers-1.1.5.dev7/dsa_helpers.egg-info}/PKG-INFO +2 -1
- {dsa_helpers-1.1.5.dev5 → dsa_helpers-1.1.5.dev7}/dsa_helpers.egg-info/SOURCES.txt +3 -0
- {dsa_helpers-1.1.5.dev5 → dsa_helpers-1.1.5.dev7}/dsa_helpers.egg-info/requires.txt +1 -0
- {dsa_helpers-1.1.5.dev5 → dsa_helpers-1.1.5.dev7}/pyproject.toml +3 -2
- dsa_helpers-1.1.5.dev5/dsa_helpers/ml/evaluate.py +0 -425
- dsa_helpers-1.1.5.dev5/dsa_helpers/ml/transforms/segformer_transforms.py +0 -23
- {dsa_helpers-1.1.5.dev5 → dsa_helpers-1.1.5.dev7}/.gitignore +0 -0
- {dsa_helpers-1.1.5.dev5 → dsa_helpers-1.1.5.dev7}/.readthedocs.yaml +0 -0
- {dsa_helpers-1.1.5.dev5 → dsa_helpers-1.1.5.dev7}/LICENSE +0 -0
- {dsa_helpers-1.1.5.dev5 → dsa_helpers-1.1.5.dev7}/README.md +0 -0
- {dsa_helpers-1.1.5.dev5 → dsa_helpers-1.1.5.dev7}/app.py +0 -0
- {dsa_helpers-1.1.5.dev5 → dsa_helpers-1.1.5.dev7}/dev.py +0 -0
- {dsa_helpers-1.1.5.dev5 → dsa_helpers-1.1.5.dev7}/docs/requirements.txt +0 -0
- {dsa_helpers-1.1.5.dev5 → dsa_helpers-1.1.5.dev7}/docs/source/conf.py +0 -0
- {dsa_helpers-1.1.5.dev5 → dsa_helpers-1.1.5.dev7}/docs/source/index.rst +0 -0
- {dsa_helpers-1.1.5.dev5 → dsa_helpers-1.1.5.dev7}/dsa_helpers/cli_utils.py +0 -0
- {dsa_helpers-1.1.5.dev5 → dsa_helpers-1.1.5.dev7}/dsa_helpers/dash/__init__.py +0 -0
- {dsa_helpers-1.1.5.dev5 → dsa_helpers-1.1.5.dev7}/dsa_helpers/dash/dash_paperdragon_utils.py +0 -0
- {dsa_helpers-1.1.5.dev5 → dsa_helpers-1.1.5.dev7}/dsa_helpers/dash/header.py +0 -0
- {dsa_helpers-1.1.5.dev5 → dsa_helpers-1.1.5.dev7}/dsa_helpers/dash/header_callbacks.py +0 -0
- {dsa_helpers-1.1.5.dev5 → dsa_helpers-1.1.5.dev7}/dsa_helpers/girder_utils.py +0 -0
- {dsa_helpers-1.1.5.dev5 → dsa_helpers-1.1.5.dev7}/dsa_helpers/gpd_utils.py +0 -0
- {dsa_helpers-1.1.5.dev5 → dsa_helpers-1.1.5.dev7}/dsa_helpers/image_utils.py +0 -0
- {dsa_helpers-1.1.5.dev5 → dsa_helpers-1.1.5.dev7}/dsa_helpers/imread.py +0 -0
- {dsa_helpers-1.1.5.dev5 → dsa_helpers-1.1.5.dev7}/dsa_helpers/imwrite.py +0 -0
- {dsa_helpers-1.1.5.dev5 → dsa_helpers-1.1.5.dev7}/dsa_helpers/ml/__init__.py +0 -0
- {dsa_helpers-1.1.5.dev5 → dsa_helpers-1.1.5.dev7}/dsa_helpers/ml/callbacks.py +0 -0
- {dsa_helpers-1.1.5.dev5 → dsa_helpers-1.1.5.dev7}/dsa_helpers/ml/datasets/__init__.py +0 -0
- {dsa_helpers-1.1.5.dev5 → dsa_helpers-1.1.5.dev7}/dsa_helpers/ml/metrics.py +0 -0
- {dsa_helpers-1.1.5.dev5 → dsa_helpers-1.1.5.dev7}/dsa_helpers/ml/object_detection/__init__.py +0 -0
- {dsa_helpers-1.1.5.dev5 → dsa_helpers-1.1.5.dev7}/dsa_helpers/ml/object_detection/get_pearce_roi_images.py +0 -0
- {dsa_helpers-1.1.5.dev5 → dsa_helpers-1.1.5.dev7}/dsa_helpers/ml/object_detection/tile_image.py +0 -0
- {dsa_helpers-1.1.5.dev5 → dsa_helpers-1.1.5.dev7}/dsa_helpers/ml/object_detection/utils.py +0 -0
- {dsa_helpers-1.1.5.dev5 → dsa_helpers-1.1.5.dev7}/dsa_helpers/ml/segformer_semantic_segmentation/__init__.py +0 -0
- {dsa_helpers-1.1.5.dev5 → dsa_helpers-1.1.5.dev7}/dsa_helpers/ml/segformer_semantic_segmentation/inference.py +0 -0
- {dsa_helpers-1.1.5.dev5 → dsa_helpers-1.1.5.dev7}/dsa_helpers/ml/transforms/__init__.py +0 -0
- {dsa_helpers-1.1.5.dev5 → dsa_helpers-1.1.5.dev7}/dsa_helpers/mongo_utils.py +0 -0
- {dsa_helpers-1.1.5.dev5 → dsa_helpers-1.1.5.dev7}/dsa_helpers/tile_utils.py +0 -0
- {dsa_helpers-1.1.5.dev5 → dsa_helpers-1.1.5.dev7}/dsa_helpers/tiling.py +0 -0
- {dsa_helpers-1.1.5.dev5 → dsa_helpers-1.1.5.dev7}/dsa_helpers/utils.py +0 -0
- {dsa_helpers-1.1.5.dev5 → dsa_helpers-1.1.5.dev7}/dsa_helpers.egg-info/dependency_links.txt +0 -0
- {dsa_helpers-1.1.5.dev5 → dsa_helpers-1.1.5.dev7}/dsa_helpers.egg-info/top_level.txt +0 -0
- {dsa_helpers-1.1.5.dev5 → dsa_helpers-1.1.5.dev7}/setup.cfg +0 -0
- {dsa_helpers-1.1.5.dev5 → dsa_helpers-1.1.5.dev7}/tests/app/Dockerfile +0 -0
- {dsa_helpers-1.1.5.dev5 → dsa_helpers-1.1.5.dev7}/tests/app/app.py +0 -0
- {dsa_helpers-1.1.5.dev5 → dsa_helpers-1.1.5.dev7}/tests/docker-compose.yaml +0 -0
- {dsa_helpers-1.1.5.dev5 → dsa_helpers-1.1.5.dev7}/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.
|
|
3
|
+
Version: 1.1.5.dev7
|
|
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
|
|
@@ -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
|
|
4
|
+
from colorama import Style, Fore
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
class SegFormerSegmentationDataset(Dataset):
|
|
8
|
-
"""
|
|
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
|
-
"""
|
|
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 as TorchDataset
|
|
2
|
+
import pandas as pd
|
|
3
|
+
from PIL import Image
|
|
4
|
+
from datasets import Dataset, Features, Image, Value
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SegFormerSegmentationDataset(TorchDataset):
|
|
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
|
|
@@ -10,13 +10,10 @@ from pathlib import Path
|
|
|
10
10
|
import torch
|
|
11
11
|
from torch import nn
|
|
12
12
|
|
|
13
|
-
from
|
|
14
|
-
from
|
|
15
|
-
|
|
16
|
-
val_transforms,
|
|
17
|
-
)
|
|
13
|
+
from .datasets import create_segformer_segmentation_dataset
|
|
14
|
+
from .transforms import train_transforms, val_transforms
|
|
15
|
+
from .evaluate import per_class_dice_on_dataset
|
|
18
16
|
from ..callbacks import MetricsLoggerCallback
|
|
19
|
-
from ..evaluate import per_class_dice_on_dataset
|
|
20
17
|
|
|
21
18
|
|
|
22
19
|
def train_segformer_semantic_segmentation_model(
|
|
@@ -241,6 +238,7 @@ def train_segformer_semantic_segmentation_model(
|
|
|
241
238
|
# For each of the datasets provided, get the metrics.
|
|
242
239
|
model = trainer.model
|
|
243
240
|
|
|
241
|
+
print("Calculating train metrics...")
|
|
244
242
|
results["train"]["metrics"] = per_class_dice_on_dataset(
|
|
245
243
|
model,
|
|
246
244
|
train_data,
|
|
@@ -251,6 +249,7 @@ def train_segformer_semantic_segmentation_model(
|
|
|
251
249
|
tqdm_notebook=tqdm_notebook,
|
|
252
250
|
)
|
|
253
251
|
|
|
252
|
+
print("Calculating val metrics...")
|
|
254
253
|
results["val"]["metrics"] = per_class_dice_on_dataset(
|
|
255
254
|
model,
|
|
256
255
|
val_data,
|
|
@@ -262,6 +261,7 @@ def train_segformer_semantic_segmentation_model(
|
|
|
262
261
|
)
|
|
263
262
|
|
|
264
263
|
if test_data is not None:
|
|
264
|
+
print("Calculating test metrics...")
|
|
265
265
|
results["test"] = {}
|
|
266
266
|
|
|
267
267
|
results["test"]["metrics"] = per_class_dice_on_dataset(
|