dsa-helpers 1.1.19.dev2__tar.gz → 2.0.0__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.19.dev2/dsa_helpers.egg-info → dsa_helpers-2.0.0}/PKG-INFO +2 -1
- {dsa_helpers-1.1.19.dev2 → dsa_helpers-2.0.0}/dsa_helpers/__init__.py +1 -1
- {dsa_helpers-1.1.19.dev2 → dsa_helpers-2.0.0}/dsa_helpers/gpd_utils.py +84 -2
- dsa_helpers-2.0.0/dsa_helpers/ml/segformer_semantic_segmentation/inference.py +571 -0
- {dsa_helpers-1.1.19.dev2 → dsa_helpers-2.0.0}/dsa_helpers/ml/segformer_semantic_segmentation/train.py +1 -1
- {dsa_helpers-1.1.19.dev2 → dsa_helpers-2.0.0/dsa_helpers.egg-info}/PKG-INFO +2 -1
- {dsa_helpers-1.1.19.dev2 → dsa_helpers-2.0.0}/dsa_helpers.egg-info/requires.txt +1 -0
- {dsa_helpers-1.1.19.dev2 → dsa_helpers-2.0.0}/pyproject.toml +3 -2
- dsa_helpers-1.1.19.dev2/dsa_helpers/ml/segformer_semantic_segmentation/inference.py +0 -231
- {dsa_helpers-1.1.19.dev2 → dsa_helpers-2.0.0}/.gitignore +0 -0
- {dsa_helpers-1.1.19.dev2 → dsa_helpers-2.0.0}/.readthedocs.yaml +0 -0
- {dsa_helpers-1.1.19.dev2 → dsa_helpers-2.0.0}/LICENSE +0 -0
- {dsa_helpers-1.1.19.dev2 → dsa_helpers-2.0.0}/README.md +0 -0
- {dsa_helpers-1.1.19.dev2 → dsa_helpers-2.0.0}/app.py +0 -0
- {dsa_helpers-1.1.19.dev2 → dsa_helpers-2.0.0}/dev.py +0 -0
- {dsa_helpers-1.1.19.dev2 → dsa_helpers-2.0.0}/docker/Dockerfile +0 -0
- {dsa_helpers-1.1.19.dev2 → dsa_helpers-2.0.0}/docker/README.md +0 -0
- {dsa_helpers-1.1.19.dev2 → dsa_helpers-2.0.0}/docs/requirements.txt +0 -0
- {dsa_helpers-1.1.19.dev2 → dsa_helpers-2.0.0}/docs/source/conf.py +0 -0
- {dsa_helpers-1.1.19.dev2 → dsa_helpers-2.0.0}/docs/source/index.rst +0 -0
- {dsa_helpers-1.1.19.dev2 → dsa_helpers-2.0.0}/dsa_helpers/cli_utils.py +0 -0
- {dsa_helpers-1.1.19.dev2 → dsa_helpers-2.0.0}/dsa_helpers/dash/__init__.py +0 -0
- {dsa_helpers-1.1.19.dev2 → dsa_helpers-2.0.0}/dsa_helpers/dash/dash_paperdragon_utils.py +0 -0
- {dsa_helpers-1.1.19.dev2 → dsa_helpers-2.0.0}/dsa_helpers/dash/header.py +0 -0
- {dsa_helpers-1.1.19.dev2 → dsa_helpers-2.0.0}/dsa_helpers/dash/header_callbacks.py +0 -0
- {dsa_helpers-1.1.19.dev2 → dsa_helpers-2.0.0}/dsa_helpers/girder_utils.py +0 -0
- {dsa_helpers-1.1.19.dev2 → dsa_helpers-2.0.0}/dsa_helpers/image_utils.py +0 -0
- {dsa_helpers-1.1.19.dev2 → dsa_helpers-2.0.0}/dsa_helpers/imread.py +0 -0
- {dsa_helpers-1.1.19.dev2 → dsa_helpers-2.0.0}/dsa_helpers/imwrite.py +0 -0
- {dsa_helpers-1.1.19.dev2 → dsa_helpers-2.0.0}/dsa_helpers/ml/__init__.py +0 -0
- {dsa_helpers-1.1.19.dev2 → dsa_helpers-2.0.0}/dsa_helpers/ml/callbacks.py +0 -0
- {dsa_helpers-1.1.19.dev2 → dsa_helpers-2.0.0}/dsa_helpers/ml/datasets/SegFormerSegmentationDataset.py +0 -0
- {dsa_helpers-1.1.19.dev2 → dsa_helpers-2.0.0}/dsa_helpers/ml/datasets/__init__.py +0 -0
- {dsa_helpers-1.1.19.dev2 → dsa_helpers-2.0.0}/dsa_helpers/ml/datasets/utils.py +0 -0
- {dsa_helpers-1.1.19.dev2 → dsa_helpers-2.0.0}/dsa_helpers/ml/evaluate.py +0 -0
- {dsa_helpers-1.1.19.dev2 → dsa_helpers-2.0.0}/dsa_helpers/ml/metrics.py +0 -0
- {dsa_helpers-1.1.19.dev2 → dsa_helpers-2.0.0}/dsa_helpers/ml/object_detection/__init__.py +0 -0
- {dsa_helpers-1.1.19.dev2 → dsa_helpers-2.0.0}/dsa_helpers/ml/object_detection/get_pearce_roi_images.py +0 -0
- {dsa_helpers-1.1.19.dev2 → dsa_helpers-2.0.0}/dsa_helpers/ml/object_detection/tile_image.py +0 -0
- {dsa_helpers-1.1.19.dev2 → dsa_helpers-2.0.0}/dsa_helpers/ml/object_detection/utils.py +0 -0
- {dsa_helpers-1.1.19.dev2 → dsa_helpers-2.0.0}/dsa_helpers/ml/segformer_semantic_segmentation/__init__.py +0 -0
- {dsa_helpers-1.1.19.dev2 → dsa_helpers-2.0.0}/dsa_helpers/ml/segformer_semantic_segmentation/datasets.py +0 -0
- {dsa_helpers-1.1.19.dev2 → dsa_helpers-2.0.0}/dsa_helpers/ml/segformer_semantic_segmentation/evaluate.py +0 -0
- {dsa_helpers-1.1.19.dev2 → dsa_helpers-2.0.0}/dsa_helpers/ml/segformer_semantic_segmentation/transforms.py +0 -0
- {dsa_helpers-1.1.19.dev2 → dsa_helpers-2.0.0}/dsa_helpers/ml/segformer_semantic_segmentation/utils.py +0 -0
- {dsa_helpers-1.1.19.dev2 → dsa_helpers-2.0.0}/dsa_helpers/ml/transforms/__init__.py +0 -0
- {dsa_helpers-1.1.19.dev2 → dsa_helpers-2.0.0}/dsa_helpers/ml/transforms/segformer_transforms.py +0 -0
- {dsa_helpers-1.1.19.dev2 → dsa_helpers-2.0.0}/dsa_helpers/mongo_utils.py +0 -0
- {dsa_helpers-1.1.19.dev2 → dsa_helpers-2.0.0}/dsa_helpers/tile_utils.py +0 -0
- {dsa_helpers-1.1.19.dev2 → dsa_helpers-2.0.0}/dsa_helpers/tiling.py +0 -0
- {dsa_helpers-1.1.19.dev2 → dsa_helpers-2.0.0}/dsa_helpers/utils.py +0 -0
- {dsa_helpers-1.1.19.dev2 → dsa_helpers-2.0.0}/dsa_helpers.egg-info/SOURCES.txt +0 -0
- {dsa_helpers-1.1.19.dev2 → dsa_helpers-2.0.0}/dsa_helpers.egg-info/dependency_links.txt +0 -0
- {dsa_helpers-1.1.19.dev2 → dsa_helpers-2.0.0}/dsa_helpers.egg-info/top_level.txt +0 -0
- {dsa_helpers-1.1.19.dev2 → dsa_helpers-2.0.0}/requirements.txt +0 -0
- {dsa_helpers-1.1.19.dev2 → dsa_helpers-2.0.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dsa_helpers
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0
|
|
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>
|
|
@@ -29,6 +29,7 @@ Requires-Dist: large-image
|
|
|
29
29
|
Requires-Dist: rasterio
|
|
30
30
|
Requires-Dist: colorama
|
|
31
31
|
Requires-Dist: albumentations
|
|
32
|
+
Requires-Dist: rdp
|
|
32
33
|
Provides-Extra: opencv
|
|
33
34
|
Requires-Dist: opencv-python; extra == "opencv"
|
|
34
35
|
Provides-Extra: opencv-headless
|
|
@@ -11,13 +11,95 @@ Functions:
|
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
13
|
import geopandas as gpd
|
|
14
|
-
from shapely.geometry import Polygon
|
|
15
|
-
from rasterio.features import rasterize
|
|
16
14
|
import numpy as np
|
|
15
|
+
from rasterio.features import rasterize
|
|
16
|
+
from rdp import rdp
|
|
17
|
+
from tqdm import tqdm
|
|
18
|
+
|
|
19
|
+
from shapely.geometry import Polygon
|
|
20
|
+
from shapely.ops import unary_union
|
|
21
|
+
|
|
17
22
|
import matplotlib.patches as mpatches
|
|
18
23
|
import matplotlib.pyplot as plt
|
|
19
24
|
|
|
20
25
|
|
|
26
|
+
def make_multi_polygons(
|
|
27
|
+
gdf: gpd.GeoDataFrame, label_col: str
|
|
28
|
+
) -> gpd.GeoDataFrame:
|
|
29
|
+
"""Make the input GeoDataFrame polygons into multi-polygons, grouped
|
|
30
|
+
by the label column.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
gdf (geopandas.GeoDataFrame): The input GeoDataFrame.
|
|
34
|
+
label_col (str): The column to group by.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
geopandas.GeoDataFrame: The output GeoDataFrame.
|
|
38
|
+
|
|
39
|
+
"""
|
|
40
|
+
unique_labels = gdf[label_col].unique()
|
|
41
|
+
|
|
42
|
+
data = []
|
|
43
|
+
|
|
44
|
+
for label in tqdm(unique_labels, desc="Making multi-polygons"):
|
|
45
|
+
label_df = gdf[gdf[label_col] == label]
|
|
46
|
+
|
|
47
|
+
if len(label_df) == 1:
|
|
48
|
+
data.append(label_df.iloc[0].copy())
|
|
49
|
+
else:
|
|
50
|
+
r = label_df.iloc[0].copy()
|
|
51
|
+
|
|
52
|
+
geom = unary_union(label_df["geometry"].tolist())
|
|
53
|
+
|
|
54
|
+
r["geometry"] = geom
|
|
55
|
+
|
|
56
|
+
data.append(r)
|
|
57
|
+
|
|
58
|
+
gdf = gpd.GeoDataFrame(data).reset_index(drop=True)
|
|
59
|
+
|
|
60
|
+
return gdf
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def rdp_by_fraction_of_max_dimension(geom, fraction=0.001):
|
|
64
|
+
# First grab the exterior coordinates.
|
|
65
|
+
coords = np.array(geom.exterior.coords)[:, :2]
|
|
66
|
+
|
|
67
|
+
# Get the bounding box dimensions
|
|
68
|
+
bounds = geom.bounds # (minx, miny, maxx, maxy)
|
|
69
|
+
width = bounds[2] - bounds[0]
|
|
70
|
+
height = bounds[3] - bounds[1]
|
|
71
|
+
max_dimension = max(width, height)
|
|
72
|
+
|
|
73
|
+
# Set epsilon as a small fraction of the max dimension
|
|
74
|
+
epsilon = max_dimension * fraction
|
|
75
|
+
|
|
76
|
+
# Run RDP on the exterior of the polygon.
|
|
77
|
+
mask = rdp(coords, algo="iter", return_mask=True, epsilon=epsilon)
|
|
78
|
+
exterior_coords = coords[mask]
|
|
79
|
+
|
|
80
|
+
# Run on the interior polygons if any.
|
|
81
|
+
interiors = []
|
|
82
|
+
if geom.interiors:
|
|
83
|
+
for interior in geom.interiors:
|
|
84
|
+
# Similary, calculate the epsilon for the interior.
|
|
85
|
+
bounds = interior.bounds
|
|
86
|
+
width = bounds[2] - bounds[0]
|
|
87
|
+
height = bounds[3] - bounds[1]
|
|
88
|
+
max_dimension = max(width, height)
|
|
89
|
+
epsilon = max_dimension * fraction
|
|
90
|
+
|
|
91
|
+
# Run RDP on the interior.
|
|
92
|
+
coords = np.array(interior.coords)[:, :2]
|
|
93
|
+
mask = rdp(coords, algo="iter", return_mask=True, epsilon=epsilon)
|
|
94
|
+
masked_coords = coords[mask]
|
|
95
|
+
interiors.append(masked_coords)
|
|
96
|
+
|
|
97
|
+
# Combine the exterior and interiors.
|
|
98
|
+
geom = Polygon(exterior_coords, interiors)
|
|
99
|
+
|
|
100
|
+
return geom
|
|
101
|
+
|
|
102
|
+
|
|
21
103
|
def count_polygon_points(polygon: Polygon) -> int:
|
|
22
104
|
"""
|
|
23
105
|
Count the total number of points in a Shapely polygon, including
|
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
import geopandas as gpd
|
|
3
|
+
import pandas as pd
|
|
4
|
+
import histomicstk as htk
|
|
5
|
+
import numpy as np
|
|
6
|
+
from transformers import (
|
|
7
|
+
SegformerForSemanticSegmentation,
|
|
8
|
+
SegformerImageProcessor,
|
|
9
|
+
)
|
|
10
|
+
from large_image_eager_iterator import LargeImagePrefetch
|
|
11
|
+
from PIL import Image
|
|
12
|
+
from time import perf_counter
|
|
13
|
+
from large_image import getTileSource
|
|
14
|
+
from tqdm import tqdm
|
|
15
|
+
from multiprocessing import Pool
|
|
16
|
+
|
|
17
|
+
from shapely import make_valid
|
|
18
|
+
from shapely.affinity import scale
|
|
19
|
+
from shapely.geometry import Polygon
|
|
20
|
+
from shapely.ops import unary_union
|
|
21
|
+
|
|
22
|
+
from ...image_utils import label_mask_to_polygons
|
|
23
|
+
from ...gpd_utils import (
|
|
24
|
+
remove_gdf_overlaps,
|
|
25
|
+
rdp_by_fraction_of_max_dimension,
|
|
26
|
+
make_multi_polygons,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
stain_color_map = htk.preprocessing.color_deconvolution.stain_color_map
|
|
30
|
+
|
|
31
|
+
# specify stains of input image
|
|
32
|
+
stains = [
|
|
33
|
+
"hematoxylin", # nuclei stain
|
|
34
|
+
"eosin", # cytoplasm stain
|
|
35
|
+
"null",
|
|
36
|
+
] # set to null if input contains only two stains
|
|
37
|
+
|
|
38
|
+
# create stain matrix
|
|
39
|
+
W = np.array([stain_color_map[st] for st in stains]).T
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def inference(
|
|
43
|
+
model: str | torch.nn.Module,
|
|
44
|
+
wsi_fp: str,
|
|
45
|
+
batch_size: int = 16,
|
|
46
|
+
tile_size: int = 512,
|
|
47
|
+
mag: float | None = None,
|
|
48
|
+
workers: int = 8,
|
|
49
|
+
chunk_mult: int = 2,
|
|
50
|
+
prefetch: int = 2,
|
|
51
|
+
device: str | None = None,
|
|
52
|
+
tolerance: float | None = 2.0,
|
|
53
|
+
min_area: float = 1000,
|
|
54
|
+
hematoxylin_channel: bool = False,
|
|
55
|
+
) -> gpd.GeoDataFrame:
|
|
56
|
+
"""Inference using SegFormer semantic segmentation model on a WSI.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
model (str | torch.nn.Module): Path to the model checkpoint or
|
|
60
|
+
a pre-loaded model.
|
|
61
|
+
wsi_fp (str): File path to the WSI.
|
|
62
|
+
batch_size (int, optional): Batch size for inference. Defaults
|
|
63
|
+
to 16.
|
|
64
|
+
tile_size (int, optional): Tile size for inference. Defaults to
|
|
65
|
+
512.
|
|
66
|
+
mag (float, optional): Magnification for inference. Defaults to
|
|
67
|
+
None, which will use the scan magnification of WSI.
|
|
68
|
+
workers (int, optional): Number of workers for inference.
|
|
69
|
+
Defaults to 8.
|
|
70
|
+
chunk_mult (int, optional): Chunk multiplier for inference.
|
|
71
|
+
Defaults to 2.
|
|
72
|
+
prefetch (int, optional): Number of prefetch for inference.
|
|
73
|
+
Defaults to 2.
|
|
74
|
+
device (str, optional): Device for inference. Default is None,
|
|
75
|
+
will use "gpu" if available, otherwise "cpu".
|
|
76
|
+
tolerance (float | None, optional): Tolerance for simplification
|
|
77
|
+
of the predicted polygons. Guidelines: 0.5 for mild
|
|
78
|
+
simplification, 1.0 for moderate, 2.0 for more aggressive.
|
|
79
|
+
If set to None or 0.0, the polygons will not be simplified.
|
|
80
|
+
min_area (float, optional): Minimum area of a polygon to be
|
|
81
|
+
simplified. This is added to avoid simplifying small
|
|
82
|
+
polygons that would probably lose too much information.
|
|
83
|
+
Defaults to 1000.
|
|
84
|
+
hematoxylin_channel (bool, optional): Whether to use the
|
|
85
|
+
hematoxylin channel when predicting the segmentation mask.
|
|
86
|
+
Defaults to False.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
gpd.GeoDataFrame: A GeoDataFrame containing the predicted
|
|
90
|
+
polygons and labels.
|
|
91
|
+
|
|
92
|
+
"""
|
|
93
|
+
# Initiate the tile iterator.
|
|
94
|
+
iterator = LargeImagePrefetch(
|
|
95
|
+
wsi_fp,
|
|
96
|
+
batch=batch_size,
|
|
97
|
+
tile_size=(tile_size, tile_size),
|
|
98
|
+
scale_mode="mag",
|
|
99
|
+
target_scale=mag,
|
|
100
|
+
workers=workers,
|
|
101
|
+
chunk_mult=chunk_mult,
|
|
102
|
+
prefetch=prefetch,
|
|
103
|
+
nchw=False,
|
|
104
|
+
icc=True,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
if device is None:
|
|
108
|
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
|
109
|
+
print(f"Using device: {device}")
|
|
110
|
+
|
|
111
|
+
device = torch.device(device)
|
|
112
|
+
|
|
113
|
+
# Load the model.
|
|
114
|
+
if isinstance(model, str):
|
|
115
|
+
model = SegformerForSemanticSegmentation.from_pretrained(
|
|
116
|
+
model, local_files_only=True, device_map=device
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
model.eval()
|
|
120
|
+
|
|
121
|
+
# Iterate through batches.
|
|
122
|
+
batch_n = 0
|
|
123
|
+
|
|
124
|
+
# Image processor for images.
|
|
125
|
+
processor = SegformerImageProcessor()
|
|
126
|
+
|
|
127
|
+
# Track all predicted polygons.
|
|
128
|
+
wsi_polygons = []
|
|
129
|
+
|
|
130
|
+
# Scaling factor, multiply to go from scan magnification to desired mag.
|
|
131
|
+
ts_metadata = getTileSource(wsi_fp).getMetadata()
|
|
132
|
+
scan_mag = ts_metadata["magnification"]
|
|
133
|
+
|
|
134
|
+
if mag is None:
|
|
135
|
+
mag = scan_mag
|
|
136
|
+
sf = 1.0
|
|
137
|
+
else:
|
|
138
|
+
sf = mag / scan_mag
|
|
139
|
+
|
|
140
|
+
for batch in iterator:
|
|
141
|
+
# Get the batch of images.
|
|
142
|
+
imgs = batch[0].view() # returns a numpy array of shape (N, H, W, C)
|
|
143
|
+
coordinates = batch[1]
|
|
144
|
+
|
|
145
|
+
if hematoxylin_channel:
|
|
146
|
+
img_list = []
|
|
147
|
+
|
|
148
|
+
for img in imgs:
|
|
149
|
+
img = (
|
|
150
|
+
htk.preprocessing.color_deconvolution.color_deconvolution(
|
|
151
|
+
img, W
|
|
152
|
+
).Stains[:, :, 0]
|
|
153
|
+
)
|
|
154
|
+
img = np.stack([img, img, img], axis=-1)
|
|
155
|
+
img_list.append(img)
|
|
156
|
+
|
|
157
|
+
imgs = img_list
|
|
158
|
+
|
|
159
|
+
# Convert the numpy arrays to PIL images.
|
|
160
|
+
imgs = [Image.fromarray(img) for img in imgs]
|
|
161
|
+
|
|
162
|
+
# Pass the images through the processor.
|
|
163
|
+
inputs = processor(imgs, return_tensors="pt")
|
|
164
|
+
inputs = inputs.to(model.device)
|
|
165
|
+
|
|
166
|
+
# Predict on the batch.
|
|
167
|
+
with torch.no_grad():
|
|
168
|
+
output = model(inputs["pixel_values"])
|
|
169
|
+
logits = output.logits
|
|
170
|
+
|
|
171
|
+
# Get the logits out, resizing them to the original tile size.
|
|
172
|
+
logits = torch.nn.functional.interpolate(
|
|
173
|
+
logits,
|
|
174
|
+
size=tile_size,
|
|
175
|
+
mode="bilinear",
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# Get predicted class labels for each pixel.
|
|
179
|
+
masks = torch.argmax(logits, dim=1).detach().cpu().numpy()
|
|
180
|
+
|
|
181
|
+
# Loop through each mask to extract the contours as shapely polygons.
|
|
182
|
+
for i, mask in enumerate(masks):
|
|
183
|
+
img_metadata = coordinates[i]
|
|
184
|
+
x, y = img_metadata[6], img_metadata[4]
|
|
185
|
+
x = int(x * sf)
|
|
186
|
+
y = int(y * sf)
|
|
187
|
+
|
|
188
|
+
polygon_and_labels = label_mask_to_polygons(
|
|
189
|
+
mask,
|
|
190
|
+
x_offset=x,
|
|
191
|
+
y_offset=y,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
for polygon_and_label in polygon_and_labels:
|
|
195
|
+
polygon, label = polygon_and_label
|
|
196
|
+
label = int(label)
|
|
197
|
+
|
|
198
|
+
# Do something with the polygon and label.
|
|
199
|
+
wsi_polygons.append([polygon, label])
|
|
200
|
+
|
|
201
|
+
batch_n += 1
|
|
202
|
+
print(f"\r Processed batch {batch_n}. ", end="")
|
|
203
|
+
print()
|
|
204
|
+
|
|
205
|
+
# Convert polygons and labels to a GeoDataFrame.
|
|
206
|
+
gdf = gpd.GeoDataFrame(wsi_polygons, columns=["geometry", "label"])
|
|
207
|
+
|
|
208
|
+
gdf["geometry"] = gdf["geometry"].buffer(1)
|
|
209
|
+
gdf = gdf.dissolve(by="label", as_index=False)
|
|
210
|
+
gdf = gdf.explode(index_parts=False).reset_index(drop=True)
|
|
211
|
+
|
|
212
|
+
if tolerance:
|
|
213
|
+
# Add the area column.
|
|
214
|
+
gdf["area"] = gdf["geometry"].area
|
|
215
|
+
|
|
216
|
+
# Split the dataframe into those polygons that need to be simplified and those that do not.
|
|
217
|
+
gdf_to_simplify = gdf[gdf["area"] > min_area].reset_index(drop=True)
|
|
218
|
+
gdf_not_to_simplify = gdf[gdf["area"] <= min_area].reset_index(
|
|
219
|
+
drop=True
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
# Simplify the polygons that need to be simplified.
|
|
223
|
+
gdf_to_simplify["geometry"] = gdf_to_simplify["geometry"].simplify(
|
|
224
|
+
tolerance=tolerance, preserve_topology=True
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
# Concatenate the simplified polygons with the polygons that do not need to be simplified.
|
|
228
|
+
gdf = pd.concat(
|
|
229
|
+
[gdf_to_simplify, gdf_not_to_simplify], ignore_index=True
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
# Remove overlapping polygons.
|
|
233
|
+
no_overlap_gdf = remove_gdf_overlaps(
|
|
234
|
+
gdf,
|
|
235
|
+
columns="label",
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
# Scale the polygon to scan magnification.
|
|
239
|
+
no_overlap_gdf["geometry"] = no_overlap_gdf["geometry"].apply(
|
|
240
|
+
lambda geom: scale(geom, xfact=1 / sf, yfact=1 / sf, origin=(0, 0))
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
return no_overlap_gdf
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
class SegFormerSSInferenceCleanup:
|
|
247
|
+
def __init__(
|
|
248
|
+
self,
|
|
249
|
+
gdf: gpd.GeoDataFrame,
|
|
250
|
+
label_ranks: list[int],
|
|
251
|
+
small_hole_thr: int = 50000,
|
|
252
|
+
buffer: int = 1,
|
|
253
|
+
fraction: float = 0.001,
|
|
254
|
+
nproc: int = 20,
|
|
255
|
+
interior_max_area: int = 100000,
|
|
256
|
+
):
|
|
257
|
+
"""Initiate the class for cleaning up the inference output.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
gdf (geopandas.GeoDataFrame): Input inference output.
|
|
261
|
+
label_ranks (list[int]): List of labels, ordered by rank
|
|
262
|
+
with index 0 being the lowest rank.
|
|
263
|
+
small_hole_thr (int, optional): Threshold in area to
|
|
264
|
+
identify small objects. Defaults to 50000.
|
|
265
|
+
buffer (int, optional): Buffer to add to polygons before
|
|
266
|
+
dissolving. Defaults to 1.
|
|
267
|
+
fraction (float, optional): Fraction of the maximum
|
|
268
|
+
dimension to use for RDP. Defaults to 0.001.
|
|
269
|
+
nproc (int, optional): Number of processes to use for
|
|
270
|
+
parallel RDP. Defaults to 20.
|
|
271
|
+
interior_max_area (int, optional): Maximum area of a hole
|
|
272
|
+
to fill. Used when filling gaps created by RDP.
|
|
273
|
+
Defaults to 100000.
|
|
274
|
+
|
|
275
|
+
"""
|
|
276
|
+
for i, r in gdf.iterrows():
|
|
277
|
+
label = r["label"]
|
|
278
|
+
|
|
279
|
+
if label not in label_ranks:
|
|
280
|
+
raise ValueError(f"Label {label} not in label_ranks.")
|
|
281
|
+
gdf.loc[i, "rank"] = label_ranks.index(r["label"])
|
|
282
|
+
|
|
283
|
+
self.__version__ = "1.0.0"
|
|
284
|
+
self.input_gdf = gdf
|
|
285
|
+
self.small_hole_thr = small_hole_thr
|
|
286
|
+
self.output_gdf = None
|
|
287
|
+
self.buffer = buffer
|
|
288
|
+
self.fraction = fraction
|
|
289
|
+
self.nproc = nproc
|
|
290
|
+
self.interior_max_area = interior_max_area
|
|
291
|
+
self.time = {}
|
|
292
|
+
|
|
293
|
+
def _make_gpd_valid(self, gdf: gpd.GeoDataFrame) -> gpd.GeoDataFrame:
|
|
294
|
+
# Make the geometries valid in the gdf, keep only polygons.
|
|
295
|
+
gdf["geometry"] = gdf["geometry"].apply(make_valid)
|
|
296
|
+
gdf = gdf.explode(index_parts=False)
|
|
297
|
+
gdf = gdf[
|
|
298
|
+
(gdf["geometry"].geom_type == "Polygon")
|
|
299
|
+
& (gdf["geometry"].is_valid)
|
|
300
|
+
]
|
|
301
|
+
|
|
302
|
+
return gdf.reset_index(drop=True)
|
|
303
|
+
|
|
304
|
+
def _remove_intersections(self, gdf: gpd.GeoDataFrame) -> gpd.GeoDataFrame:
|
|
305
|
+
# Remove intersections between polygons.
|
|
306
|
+
gdf = gdf.reset_index(drop=True)
|
|
307
|
+
n = len(gdf)
|
|
308
|
+
|
|
309
|
+
if n in (0, 1):
|
|
310
|
+
return gdf
|
|
311
|
+
|
|
312
|
+
# Loop until the second to last row.
|
|
313
|
+
for i in tqdm(
|
|
314
|
+
range(n - 1), total=n - 1, desc="Removing intersections"
|
|
315
|
+
):
|
|
316
|
+
r1 = gdf.iloc[i]
|
|
317
|
+
|
|
318
|
+
# Subtract the r1 geometry from all others.
|
|
319
|
+
for j in range(i + 1, n):
|
|
320
|
+
r2 = gdf.iloc[j]
|
|
321
|
+
|
|
322
|
+
# Subtract r1 from r2.
|
|
323
|
+
geom = r2["geometry"].difference(r1["geometry"])
|
|
324
|
+
|
|
325
|
+
gdf.loc[j, "geometry"] = geom
|
|
326
|
+
|
|
327
|
+
return self._make_gpd_valid(gdf)
|
|
328
|
+
|
|
329
|
+
def _remove_small_holes(self, gdf: gpd.GeoDataFrame) -> gpd.GeoDataFrame:
|
|
330
|
+
gdf = gdf.reset_index(drop=True)
|
|
331
|
+
|
|
332
|
+
n = len(gdf)
|
|
333
|
+
|
|
334
|
+
for i in tqdm(range(n), total=n, desc="Removing small holes"):
|
|
335
|
+
geom = gdf.iloc[i]["geometry"]
|
|
336
|
+
|
|
337
|
+
exterior = geom.exterior
|
|
338
|
+
interiors = geom.interiors
|
|
339
|
+
|
|
340
|
+
new_interiors = []
|
|
341
|
+
for interior in interiors:
|
|
342
|
+
if Polygon(interior).area > self.small_hole_thr:
|
|
343
|
+
new_interiors.append(interior)
|
|
344
|
+
|
|
345
|
+
geom = Polygon(exterior, new_interiors)
|
|
346
|
+
|
|
347
|
+
gdf.loc[i, "geometry"] = geom
|
|
348
|
+
|
|
349
|
+
return gdf
|
|
350
|
+
|
|
351
|
+
def _remove_small_contained_polygons(
|
|
352
|
+
self, gdf: gpd.GeoDataFrame
|
|
353
|
+
) -> gpd.GeoDataFrame:
|
|
354
|
+
n = len(gdf)
|
|
355
|
+
|
|
356
|
+
i_removed = []
|
|
357
|
+
|
|
358
|
+
for i in tqdm(
|
|
359
|
+
range(n), total=n, desc="Removing small contained polygons"
|
|
360
|
+
):
|
|
361
|
+
exterior = gdf.iloc[i]["geometry"].exterior
|
|
362
|
+
|
|
363
|
+
geom = Polygon(exterior)
|
|
364
|
+
|
|
365
|
+
if geom.area < self.small_hole_thr:
|
|
366
|
+
# Check if this is contained in another polygon.
|
|
367
|
+
contained = gdf[
|
|
368
|
+
(gdf["geometry"].contains(geom)) & (gdf.index != i)
|
|
369
|
+
]
|
|
370
|
+
|
|
371
|
+
if len(contained):
|
|
372
|
+
i_removed.append(i)
|
|
373
|
+
|
|
374
|
+
gdf = gdf.drop(i_removed)
|
|
375
|
+
|
|
376
|
+
return gdf
|
|
377
|
+
|
|
378
|
+
def _remove_small_polygons_not_contained(
|
|
379
|
+
self, gdf: gpd.GeoDataFrame
|
|
380
|
+
) -> gpd.GeoDataFrame:
|
|
381
|
+
gdf = gdf.reset_index(drop=True)
|
|
382
|
+
n = len(gdf)
|
|
383
|
+
|
|
384
|
+
i_removed = []
|
|
385
|
+
|
|
386
|
+
for i in tqdm(
|
|
387
|
+
range(n), total=n, desc="Removing small polygons not contained"
|
|
388
|
+
):
|
|
389
|
+
geom = gdf.iloc[i]["geometry"]
|
|
390
|
+
|
|
391
|
+
if geom.geom_type == "MultiPolygon":
|
|
392
|
+
area = geom.area
|
|
393
|
+
else:
|
|
394
|
+
area = Polygon(geom.exterior).area
|
|
395
|
+
|
|
396
|
+
if area < self.small_hole_thr:
|
|
397
|
+
# Check for any touching polygons.
|
|
398
|
+
touching = gdf[
|
|
399
|
+
(~gdf.index.isin(i_removed + [i]))
|
|
400
|
+
& (gdf["geometry"].touches(geom))
|
|
401
|
+
].copy()
|
|
402
|
+
if len(touching):
|
|
403
|
+
touching["intersection_length"] = (
|
|
404
|
+
touching["geometry"].intersection(geom).length
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
touching = touching.sort_values(
|
|
408
|
+
by="intersection_length", ascending=False
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
r = touching.iloc[0]
|
|
412
|
+
|
|
413
|
+
touching_geom = r["geometry"]
|
|
414
|
+
|
|
415
|
+
# Merge the polygons.
|
|
416
|
+
geom = geom.union(touching_geom)
|
|
417
|
+
|
|
418
|
+
gdf.loc[r.name, "geometry"] = geom
|
|
419
|
+
i_removed.append(i)
|
|
420
|
+
else:
|
|
421
|
+
# Remove this polygon.
|
|
422
|
+
i_removed.append(i)
|
|
423
|
+
|
|
424
|
+
gdf = gdf.drop(i_removed)
|
|
425
|
+
gdf = self._make_gpd_valid(gdf)
|
|
426
|
+
return gdf
|
|
427
|
+
|
|
428
|
+
def _rdp_polygon(self, geom, idx, fraction):
|
|
429
|
+
geom = rdp_by_fraction_of_max_dimension(geom, fraction=fraction)
|
|
430
|
+
return geom, idx
|
|
431
|
+
|
|
432
|
+
def _fill_rdp_gaps(
|
|
433
|
+
self, gdf: gpd.GeoDataFrame, interior_max_area: int = 100000
|
|
434
|
+
) -> gpd.GeoDataFrame:
|
|
435
|
+
# Fill the gaps between polygons created by RDP.
|
|
436
|
+
gdf["area"] = gdf["geometry"].area
|
|
437
|
+
|
|
438
|
+
gdf_union = gdf["geometry"].union_all()
|
|
439
|
+
|
|
440
|
+
# Collect all the holes / interiors.
|
|
441
|
+
interiors = []
|
|
442
|
+
|
|
443
|
+
for geom in gdf_union.geoms:
|
|
444
|
+
for interior in geom.interiors:
|
|
445
|
+
interior = Polygon(interior)
|
|
446
|
+
|
|
447
|
+
if interior.area < interior_max_area:
|
|
448
|
+
interiors.append(interior)
|
|
449
|
+
|
|
450
|
+
# Loop through each hole that was small.
|
|
451
|
+
for interior in tqdm(interiors, desc="Filling holes between polygons"):
|
|
452
|
+
# Check polygons that are touching this interior.
|
|
453
|
+
touching = gdf[gdf["geometry"].distance(interior) == 0]
|
|
454
|
+
|
|
455
|
+
unique_ranks = touching["rank"].unique()
|
|
456
|
+
|
|
457
|
+
if len(unique_ranks) > 1:
|
|
458
|
+
# Sort by rank then area.
|
|
459
|
+
touching = touching.sort_values(
|
|
460
|
+
by=["rank", "area"], ascending=False
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
# Get the first row.
|
|
464
|
+
r = touching.iloc[0]
|
|
465
|
+
|
|
466
|
+
# Merge the hole with the polygon.
|
|
467
|
+
geom = unary_union([r["geometry"], interior])
|
|
468
|
+
|
|
469
|
+
if geom.geom_type == "MultiPolygon":
|
|
470
|
+
# Buff the interior a bit.
|
|
471
|
+
interior_buffed = interior.buffer(1)
|
|
472
|
+
geom = unary_union([interior_buffed, geom])
|
|
473
|
+
|
|
474
|
+
if geom.geom_type == "MultiPolygon":
|
|
475
|
+
print(
|
|
476
|
+
"MultiPolygon after buffering and union, discarding hole."
|
|
477
|
+
)
|
|
478
|
+
continue
|
|
479
|
+
|
|
480
|
+
gdf.loc[r.name, "geometry"] = geom
|
|
481
|
+
gdf.loc[r.name, "area"] = geom.area
|
|
482
|
+
|
|
483
|
+
return gdf
|
|
484
|
+
|
|
485
|
+
def cleanup(self):
|
|
486
|
+
"""Pipeline for cleaning up the inference output."""
|
|
487
|
+
print("Running inference cleanup:\n")
|
|
488
|
+
time = self.time
|
|
489
|
+
gdf = self.input_gdf.copy()
|
|
490
|
+
|
|
491
|
+
print("[1/9] Applying buffer...")
|
|
492
|
+
start_time = perf_counter()
|
|
493
|
+
gdf["geometry"] = gdf["geometry"].buffer(self.buffer)
|
|
494
|
+
time["buffer"] = perf_counter() - start_time
|
|
495
|
+
gdf = self._make_gpd_valid(gdf)
|
|
496
|
+
|
|
497
|
+
print("[2/9] Dissolving...")
|
|
498
|
+
start_time = perf_counter()
|
|
499
|
+
gdf = gdf.dissolve(by="label", as_index=False)
|
|
500
|
+
time["dissolve"] = perf_counter() - start_time
|
|
501
|
+
gdf = gdf.explode(index_parts=False).reset_index(drop=True)
|
|
502
|
+
gdf = self._make_gpd_valid(gdf)
|
|
503
|
+
|
|
504
|
+
print("[3/9] Removing intersections...")
|
|
505
|
+
start_time = perf_counter()
|
|
506
|
+
gdf = make_multi_polygons(gdf, "label")
|
|
507
|
+
gdf = self._remove_intersections(gdf)
|
|
508
|
+
time["remove-intersections"] = perf_counter() - start_time
|
|
509
|
+
|
|
510
|
+
print("[4/9] Removing small holes...")
|
|
511
|
+
gdf = gdf.explode(index_parts=False).reset_index(drop=True)
|
|
512
|
+
gdf = self._make_gpd_valid(gdf)
|
|
513
|
+
start_time = perf_counter()
|
|
514
|
+
gdf = self._remove_small_holes(gdf)
|
|
515
|
+
time["remove-small-holes"] = perf_counter() - start_time
|
|
516
|
+
|
|
517
|
+
print("[5/9] Removing small polygons contained in other polygons...")
|
|
518
|
+
start_time = perf_counter()
|
|
519
|
+
gdf = self._remove_small_contained_polygons(gdf)
|
|
520
|
+
time["remove-small-contained-polygons"] = perf_counter() - start_time
|
|
521
|
+
|
|
522
|
+
print("[6/9] Removing small polygons not contained...")
|
|
523
|
+
start_time = perf_counter()
|
|
524
|
+
gdf = self._remove_small_polygons_not_contained(gdf)
|
|
525
|
+
time["remove-small-polygons-not-contained"] = (
|
|
526
|
+
perf_counter() - start_time
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
# Parallel RDP.
|
|
530
|
+
print("[7/9] Reducing points in polygons via RDP...")
|
|
531
|
+
start_time = perf_counter()
|
|
532
|
+
with Pool(processes=self.nproc) as pool:
|
|
533
|
+
jobs = [
|
|
534
|
+
pool.apply_async(
|
|
535
|
+
func=self._rdp_polygon,
|
|
536
|
+
args=(r["geometry"], i, self.fraction),
|
|
537
|
+
)
|
|
538
|
+
for i, r in gdf.iterrows()
|
|
539
|
+
]
|
|
540
|
+
|
|
541
|
+
n = len(gdf)
|
|
542
|
+
|
|
543
|
+
for job in tqdm(jobs, total=n, desc="Reducing points in polygons"):
|
|
544
|
+
geom, idx = job.get()
|
|
545
|
+
gdf.loc[idx, "geometry"] = geom
|
|
546
|
+
|
|
547
|
+
time["rdp"] = perf_counter() - start_time
|
|
548
|
+
|
|
549
|
+
gdf = self._make_gpd_valid(gdf)
|
|
550
|
+
|
|
551
|
+
print("[8/9] Removing intersections again...")
|
|
552
|
+
start_time = perf_counter()
|
|
553
|
+
gdf = make_multi_polygons(gdf, "label")
|
|
554
|
+
gdf = self._remove_intersections(gdf)
|
|
555
|
+
time["remove-intersections-2"] = perf_counter() - start_time
|
|
556
|
+
|
|
557
|
+
gdf = gdf.explode(index_parts=False).reset_index(drop=True)
|
|
558
|
+
gdf = self._make_gpd_valid(gdf)
|
|
559
|
+
|
|
560
|
+
print("[9/9] Filling RDP gaps...")
|
|
561
|
+
start_time = perf_counter()
|
|
562
|
+
gdf = self._fill_rdp_gaps(gdf, self.interior_max_area)
|
|
563
|
+
time["fill-rdp-gaps"] = perf_counter() - start_time
|
|
564
|
+
gdf = self._make_gpd_valid(gdf)
|
|
565
|
+
|
|
566
|
+
self.output_gdf = gdf
|
|
567
|
+
self.time = time
|
|
568
|
+
|
|
569
|
+
total_time = sum(time.values())
|
|
570
|
+
print(f"\nTotal time: {total_time:.2f} seconds")
|
|
571
|
+
return gdf
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dsa_helpers
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0
|
|
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>
|
|
@@ -29,6 +29,7 @@ Requires-Dist: large-image
|
|
|
29
29
|
Requires-Dist: rasterio
|
|
30
30
|
Requires-Dist: colorama
|
|
31
31
|
Requires-Dist: albumentations
|
|
32
|
+
Requires-Dist: rdp
|
|
32
33
|
Provides-Extra: opencv
|
|
33
34
|
Requires-Dist: opencv-python; extra == "opencv"
|
|
34
35
|
Provides-Extra: opencv-headless
|
|
@@ -23,7 +23,8 @@ dependencies = [
|
|
|
23
23
|
"large-image",
|
|
24
24
|
"rasterio",
|
|
25
25
|
"colorama",
|
|
26
|
-
"albumentations"
|
|
26
|
+
"albumentations",
|
|
27
|
+
"rdp"
|
|
27
28
|
]
|
|
28
29
|
|
|
29
30
|
name = "dsa_helpers"
|
|
@@ -41,7 +42,7 @@ classifiers = [
|
|
|
41
42
|
"License :: OSI Approved :: MIT License",
|
|
42
43
|
]
|
|
43
44
|
keywords = ["dsa", "girder", "girder_client"]
|
|
44
|
-
version = "
|
|
45
|
+
version = "v2.0.0"
|
|
45
46
|
|
|
46
47
|
[project.optional-dependencies]
|
|
47
48
|
opencv = ["opencv-python"]
|
|
@@ -1,231 +0,0 @@
|
|
|
1
|
-
import torch
|
|
2
|
-
from large_image_eager_iterator import LargeImagePrefetch
|
|
3
|
-
from transformers import (
|
|
4
|
-
SegformerForSemanticSegmentation,
|
|
5
|
-
SegformerImageProcessor,
|
|
6
|
-
)
|
|
7
|
-
from PIL import Image
|
|
8
|
-
from large_image import getTileSource
|
|
9
|
-
import geopandas as gpd
|
|
10
|
-
from shapely.affinity import scale
|
|
11
|
-
import pandas as pd
|
|
12
|
-
import histomicstk as htk
|
|
13
|
-
import numpy as np
|
|
14
|
-
|
|
15
|
-
from ...image_utils import label_mask_to_polygons
|
|
16
|
-
from ...gpd_utils import remove_gdf_overlaps
|
|
17
|
-
|
|
18
|
-
stain_color_map = htk.preprocessing.color_deconvolution.stain_color_map
|
|
19
|
-
|
|
20
|
-
# specify stains of input image
|
|
21
|
-
stains = [
|
|
22
|
-
"hematoxylin", # nuclei stain
|
|
23
|
-
"eosin", # cytoplasm stain
|
|
24
|
-
"null",
|
|
25
|
-
] # set to null if input contains only two stains
|
|
26
|
-
|
|
27
|
-
# create stain matrix
|
|
28
|
-
W = np.array([stain_color_map[st] for st in stains]).T
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def inference(
|
|
32
|
-
model: str | torch.nn.Module,
|
|
33
|
-
wsi_fp: str,
|
|
34
|
-
batch_size: int = 16,
|
|
35
|
-
tile_size: int = 512,
|
|
36
|
-
mag: float | None = None,
|
|
37
|
-
workers: int = 8,
|
|
38
|
-
chunk_mult: int = 2,
|
|
39
|
-
prefetch: int = 2,
|
|
40
|
-
device: str | None = None,
|
|
41
|
-
tolerance: float | None = 2.0,
|
|
42
|
-
min_area: float = 1000,
|
|
43
|
-
hematoxylin_channel: bool = False,
|
|
44
|
-
) -> gpd.GeoDataFrame:
|
|
45
|
-
"""Inference using SegFormer semantic segmentation model on a WSI.
|
|
46
|
-
|
|
47
|
-
Args:
|
|
48
|
-
model (str | torch.nn.Module): Path to the model checkpoint or
|
|
49
|
-
a pre-loaded model.
|
|
50
|
-
wsi_fp (str): File path to the WSI.
|
|
51
|
-
batch_size (int, optional): Batch size for inference. Defaults
|
|
52
|
-
to 16.
|
|
53
|
-
tile_size (int, optional): Tile size for inference. Defaults to
|
|
54
|
-
512.
|
|
55
|
-
mag (float, optional): Magnification for inference. Defaults to
|
|
56
|
-
None, which will use the scan magnification of WSI.
|
|
57
|
-
workers (int, optional): Number of workers for inference.
|
|
58
|
-
Defaults to 8.
|
|
59
|
-
chunk_mult (int, optional): Chunk multiplier for inference.
|
|
60
|
-
Defaults to 2.
|
|
61
|
-
prefetch (int, optional): Number of prefetch for inference.
|
|
62
|
-
Defaults to 2.
|
|
63
|
-
device (str, optional): Device for inference. Default is None,
|
|
64
|
-
will use "gpu" if available, otherwise "cpu".
|
|
65
|
-
tolerance (float | None, optional): Tolerance for simplification
|
|
66
|
-
of the predicted polygons. Guidelines: 0.5 for mild
|
|
67
|
-
simplification, 1.0 for moderate, 2.0 for more aggressive.
|
|
68
|
-
If set to None or 0.0, the polygons will not be simplified.
|
|
69
|
-
min_area (float, optional): Minimum area of a polygon to be
|
|
70
|
-
simplified. This is added to avoid simplifying small
|
|
71
|
-
polygons that would probably lose too much information.
|
|
72
|
-
Defaults to 1000.
|
|
73
|
-
hematoxylin_channel (bool, optional): Whether to use the
|
|
74
|
-
hematoxylin channel when predicting the segmentation mask.
|
|
75
|
-
Defaults to False.
|
|
76
|
-
|
|
77
|
-
Returns:
|
|
78
|
-
gpd.GeoDataFrame: A GeoDataFrame containing the predicted
|
|
79
|
-
polygons and labels.
|
|
80
|
-
|
|
81
|
-
"""
|
|
82
|
-
# Initiate the tile iterator.
|
|
83
|
-
iterator = LargeImagePrefetch(
|
|
84
|
-
wsi_fp,
|
|
85
|
-
batch=batch_size,
|
|
86
|
-
tile_size=(tile_size, tile_size),
|
|
87
|
-
scale_mode="mag",
|
|
88
|
-
target_scale=mag,
|
|
89
|
-
workers=workers,
|
|
90
|
-
chunk_mult=chunk_mult,
|
|
91
|
-
prefetch=prefetch,
|
|
92
|
-
nchw=False,
|
|
93
|
-
icc=True,
|
|
94
|
-
)
|
|
95
|
-
|
|
96
|
-
if device is None:
|
|
97
|
-
device = "cuda" if torch.cuda.is_available() else "cpu"
|
|
98
|
-
print(f"Using device: {device}")
|
|
99
|
-
|
|
100
|
-
device = torch.device(device)
|
|
101
|
-
|
|
102
|
-
# Load the model.
|
|
103
|
-
if isinstance(model, str):
|
|
104
|
-
model = SegformerForSemanticSegmentation.from_pretrained(
|
|
105
|
-
model, local_files_only=True, device_map=device
|
|
106
|
-
)
|
|
107
|
-
|
|
108
|
-
model.eval()
|
|
109
|
-
|
|
110
|
-
# Iterate through batches.
|
|
111
|
-
batch_n = 0
|
|
112
|
-
|
|
113
|
-
# Image processor for images.
|
|
114
|
-
processor = SegformerImageProcessor()
|
|
115
|
-
|
|
116
|
-
# Track all predicted polygons.
|
|
117
|
-
wsi_polygons = []
|
|
118
|
-
|
|
119
|
-
# Scaling factor, multiply to go from scan magnification to desired mag.
|
|
120
|
-
ts_metadata = getTileSource(wsi_fp).getMetadata()
|
|
121
|
-
scan_mag = ts_metadata["magnification"]
|
|
122
|
-
|
|
123
|
-
if mag is None:
|
|
124
|
-
mag = scan_mag
|
|
125
|
-
sf = 1.0
|
|
126
|
-
else:
|
|
127
|
-
sf = mag / scan_mag
|
|
128
|
-
|
|
129
|
-
for batch in iterator:
|
|
130
|
-
# Get the batch of images.
|
|
131
|
-
imgs = batch[0].view() # returns a numpy array of shape (N, H, W, C)
|
|
132
|
-
coordinates = batch[1]
|
|
133
|
-
|
|
134
|
-
if hematoxylin_channel:
|
|
135
|
-
img_list = []
|
|
136
|
-
|
|
137
|
-
for img in imgs:
|
|
138
|
-
img = (
|
|
139
|
-
htk.preprocessing.color_deconvolution.color_deconvolution(
|
|
140
|
-
img, W
|
|
141
|
-
).Stains[:, :, 0]
|
|
142
|
-
)
|
|
143
|
-
img = np.stack([img, img, img], axis=-1)
|
|
144
|
-
img_list.append(img)
|
|
145
|
-
|
|
146
|
-
imgs = img_list
|
|
147
|
-
|
|
148
|
-
# Convert the numpy arrays to PIL images.
|
|
149
|
-
imgs = [Image.fromarray(img) for img in imgs]
|
|
150
|
-
|
|
151
|
-
# Pass the images through the processor.
|
|
152
|
-
inputs = processor(imgs, return_tensors="pt")
|
|
153
|
-
inputs = inputs.to(model.device)
|
|
154
|
-
|
|
155
|
-
# Predict on the batch.
|
|
156
|
-
with torch.no_grad():
|
|
157
|
-
output = model(inputs["pixel_values"])
|
|
158
|
-
logits = output.logits
|
|
159
|
-
|
|
160
|
-
# Get the logits out, resizing them to the original tile size.
|
|
161
|
-
logits = torch.nn.functional.interpolate(
|
|
162
|
-
logits,
|
|
163
|
-
size=tile_size,
|
|
164
|
-
mode="bilinear",
|
|
165
|
-
)
|
|
166
|
-
|
|
167
|
-
# Get predicted class labels for each pixel.
|
|
168
|
-
masks = torch.argmax(logits, dim=1).detach().cpu().numpy()
|
|
169
|
-
|
|
170
|
-
# Loop through each mask to extract the contours as shapely polygons.
|
|
171
|
-
for i, mask in enumerate(masks):
|
|
172
|
-
img_metadata = coordinates[i]
|
|
173
|
-
x, y = img_metadata[6], img_metadata[4]
|
|
174
|
-
x = int(x * sf)
|
|
175
|
-
y = int(y * sf)
|
|
176
|
-
|
|
177
|
-
polygon_and_labels = label_mask_to_polygons(
|
|
178
|
-
mask,
|
|
179
|
-
x_offset=x,
|
|
180
|
-
y_offset=y,
|
|
181
|
-
)
|
|
182
|
-
|
|
183
|
-
for polygon_and_label in polygon_and_labels:
|
|
184
|
-
polygon, label = polygon_and_label
|
|
185
|
-
label = int(label)
|
|
186
|
-
|
|
187
|
-
# Do something with the polygon and label.
|
|
188
|
-
wsi_polygons.append([polygon, label])
|
|
189
|
-
|
|
190
|
-
batch_n += 1
|
|
191
|
-
print(f"\r Processed batch {batch_n}. ", end="")
|
|
192
|
-
print()
|
|
193
|
-
|
|
194
|
-
# Convert polygons and labels to a GeoDataFrame.
|
|
195
|
-
gdf = gpd.GeoDataFrame(wsi_polygons, columns=["geometry", "label"])
|
|
196
|
-
gdf["geometry"] = gdf["geometry"].buffer(1)
|
|
197
|
-
gdf = gdf.dissolve(by="label", as_index=False)
|
|
198
|
-
gdf = gdf.explode(index_parts=False).reset_index(drop=True)
|
|
199
|
-
|
|
200
|
-
if tolerance:
|
|
201
|
-
# Add the area column.
|
|
202
|
-
gdf["area"] = gdf["geometry"].area
|
|
203
|
-
|
|
204
|
-
# Split the dataframe into those polygons that need to be simplified and those that do not.
|
|
205
|
-
gdf_to_simplify = gdf[gdf["area"] > min_area].reset_index(drop=True)
|
|
206
|
-
gdf_not_to_simplify = gdf[gdf["area"] <= min_area].reset_index(
|
|
207
|
-
drop=True
|
|
208
|
-
)
|
|
209
|
-
|
|
210
|
-
# Simplify the polygons that need to be simplified.
|
|
211
|
-
gdf_to_simplify["geometry"] = gdf_to_simplify["geometry"].simplify(
|
|
212
|
-
tolerance=tolerance, preserve_topology=True
|
|
213
|
-
)
|
|
214
|
-
|
|
215
|
-
# Concatenate the simplified polygons with the polygons that do not need to be simplified.
|
|
216
|
-
gdf = pd.concat(
|
|
217
|
-
[gdf_to_simplify, gdf_not_to_simplify], ignore_index=True
|
|
218
|
-
)
|
|
219
|
-
|
|
220
|
-
# Remove overlapping polygons.
|
|
221
|
-
no_overlap_gdf = remove_gdf_overlaps(
|
|
222
|
-
gdf,
|
|
223
|
-
columns="label",
|
|
224
|
-
)
|
|
225
|
-
|
|
226
|
-
# Scale the polygon to scan magnification.
|
|
227
|
-
no_overlap_gdf["geometry"] = no_overlap_gdf["geometry"].apply(
|
|
228
|
-
lambda geom: scale(geom, xfact=1 / sf, yfact=1 / sf, origin=(0, 0))
|
|
229
|
-
)
|
|
230
|
-
|
|
231
|
-
return no_overlap_gdf
|
|
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
|
|
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
|
|
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.19.dev2 → dsa_helpers-2.0.0}/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
|