dsa-helpers 1.1.3.dev1__tar.gz → 1.1.4.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.3.dev1/dsa_helpers.egg-info → dsa_helpers-1.1.4.dev0}/PKG-INFO +2 -1
- {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers/__init__.py +1 -1
- {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers/gpd_utils.py +59 -10
- {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers/tiling.py +125 -119
- {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0/dsa_helpers.egg-info}/PKG-INFO +2 -1
- {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers.egg-info/requires.txt +1 -0
- {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/pyproject.toml +3 -2
- {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/.gitignore +0 -0
- {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/.readthedocs.yaml +0 -0
- {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/LICENSE +0 -0
- {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/README.md +0 -0
- {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/app.py +0 -0
- {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dev.py +0 -0
- {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/docs/requirements.txt +0 -0
- {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/docs/source/conf.py +0 -0
- {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/docs/source/index.rst +0 -0
- {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers/cli_utils.py +0 -0
- {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers/dash/__init__.py +0 -0
- {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers/dash/dash_paperdragon_utils.py +0 -0
- {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers/dash/header.py +0 -0
- {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers/dash/header_callbacks.py +0 -0
- {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers/girder_utils.py +0 -0
- {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers/image_utils.py +0 -0
- {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers/imread.py +0 -0
- {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers/imwrite.py +0 -0
- {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers/ml/__init__.py +0 -0
- {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers/ml/callbacks.py +0 -0
- {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers/ml/datasets/SegFormerSegmentationDataset.py +0 -0
- {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers/ml/datasets/__init__.py +0 -0
- {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers/ml/datasets/utils.py +0 -0
- {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers/ml/evaluate.py +0 -0
- {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers/ml/metrics.py +0 -0
- {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers/ml/object_detection/__init__.py +0 -0
- {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers/ml/object_detection/get_pearce_roi_images.py +0 -0
- {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers/ml/object_detection/tile_image.py +0 -0
- {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers/ml/object_detection/utils.py +0 -0
- {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers/ml/segformer_semantic_segmentation/__init__.py +0 -0
- {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers/ml/segformer_semantic_segmentation/inference.py +0 -0
- {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers/ml/transforms/__init__.py +0 -0
- {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers/ml/transforms/segformer_transforms.py +0 -0
- {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers/mongo_utils.py +0 -0
- {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers/tile_utils.py +0 -0
- {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers/utils.py +0 -0
- {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers.egg-info/SOURCES.txt +0 -0
- {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers.egg-info/dependency_links.txt +0 -0
- {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers.egg-info/top_level.txt +0 -0
- {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/setup.cfg +0 -0
- {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/tests/app/Dockerfile +0 -0
- {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/tests/app/app.py +0 -0
- {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/tests/docker-compose.yaml +0 -0
- {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.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.4.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>
|
|
@@ -27,6 +27,7 @@ Requires-Dist: transformers[torch]
|
|
|
27
27
|
Requires-Dist: datasets
|
|
28
28
|
Requires-Dist: torchvision
|
|
29
29
|
Requires-Dist: large-image
|
|
30
|
+
Requires-Dist: rasterio
|
|
30
31
|
Dynamic: license-file
|
|
31
32
|
|
|
32
33
|
# Digital Slide Archive (DSA) Helpers
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Utility functions for working with GeoPandas.
|
|
3
3
|
|
|
4
|
+
Functions:
|
|
5
|
+
- count_polygon_points: Count the total number of points in a Shapely polygon.
|
|
6
|
+
- total_points_in_gdf: Calculate the total number of points in all polygons in a GeoDataFrame.
|
|
7
|
+
- plot_gdf: Create a figure from a geopandas dataframe.
|
|
8
|
+
- remove_gdf_overlaps: Remove overlaps from a GeoDataFrame.
|
|
9
|
+
- draw_gdf_on_array: Draw a GeoDataFrame on an array.
|
|
10
|
+
|
|
4
11
|
"""
|
|
5
12
|
|
|
6
13
|
import geopandas as gpd
|
|
7
14
|
from shapely.geometry import Polygon
|
|
8
|
-
|
|
15
|
+
from rasterio.features import rasterize
|
|
16
|
+
import numpy as np
|
|
9
17
|
import matplotlib.patches as mpatches
|
|
10
18
|
import matplotlib.pyplot as plt
|
|
11
19
|
|
|
@@ -51,20 +59,25 @@ def total_points_in_gdf(gdf: gpd.GeoDataFrame) -> int:
|
|
|
51
59
|
def plot_gdf(
|
|
52
60
|
gdf: gpd.GeoDataFrame,
|
|
53
61
|
figsize: int | tuple[int, int] | None = None,
|
|
62
|
+
label_column: str = "group",
|
|
54
63
|
label2color: dict | None = None,
|
|
55
|
-
):
|
|
56
|
-
"""
|
|
64
|
+
) -> tuple[plt.Figure, plt.Axes]:
|
|
65
|
+
"""Create figure from a geopandas dataframe.
|
|
57
66
|
|
|
58
67
|
Args:
|
|
59
68
|
gdf (geopandas.GeoDataFrame): A GeoDataFrame containing
|
|
60
69
|
polygons. Must have a 'label' column.
|
|
61
70
|
figsize (int | tuple[int, int] | None): The size of the figure.
|
|
62
71
|
If None, the figure size will be (5, 5).
|
|
72
|
+
label_column (str): The column name of the label.
|
|
63
73
|
label2color (dict | None): A dictionary mapping labels to
|
|
64
74
|
colors. If None then a legend will not be included and the
|
|
65
75
|
colors will be random. Colors should be an accepted
|
|
66
76
|
matplotlib color.
|
|
67
77
|
|
|
78
|
+
Returns:
|
|
79
|
+
tuple[plt.Figure, plt.Axes]: The figure and axes objects.
|
|
80
|
+
|
|
68
81
|
"""
|
|
69
82
|
if figsize is None:
|
|
70
83
|
figsize = (5, 5)
|
|
@@ -76,16 +89,14 @@ def plot_gdf(
|
|
|
76
89
|
# Blow up any multi-polygons.
|
|
77
90
|
gdf = gdf.explode(index_parts=False).reset_index(drop=True)
|
|
78
91
|
|
|
79
|
-
total_points = total_points_in_gdf(gdf)
|
|
80
|
-
|
|
81
92
|
if label2color is not None:
|
|
82
|
-
gdf["color"] = gdf[
|
|
93
|
+
gdf["color"] = gdf[label_column].map(label2color)
|
|
83
94
|
|
|
84
95
|
# Remove rows where color is nan.
|
|
85
96
|
gdf = gdf.dropna(subset=["color"])
|
|
86
97
|
|
|
87
|
-
#
|
|
88
|
-
ax = plt.subplots(figsize=figsize)
|
|
98
|
+
# Create the plot
|
|
99
|
+
fig, ax = plt.subplots(figsize=figsize)
|
|
89
100
|
gdf.plot(color=gdf["color"], ax=ax)
|
|
90
101
|
|
|
91
102
|
if label2color is not None:
|
|
@@ -105,8 +116,8 @@ def plot_gdf(
|
|
|
105
116
|
), # x > 1 pushes it to the right, y = 1 keeps it top-aligned
|
|
106
117
|
borderaxespad=0,
|
|
107
118
|
)
|
|
108
|
-
|
|
109
|
-
|
|
119
|
+
|
|
120
|
+
return fig, ax
|
|
110
121
|
|
|
111
122
|
|
|
112
123
|
def remove_gdf_overlaps(
|
|
@@ -170,3 +181,41 @@ def remove_gdf_overlaps(
|
|
|
170
181
|
gdf = gdf.explode(index_parts=False).reset_index(drop=True)
|
|
171
182
|
|
|
172
183
|
return gdf
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def draw_gdf_on_array(gdf, shape, id_column="idx", default_value: int = 0):
|
|
187
|
+
"""Use the polygons in a geopandas dataframe to draw on a passed
|
|
188
|
+
numpy array.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
gdf (geopandas.GeoDataFrame): The GeoDataFrame to draw on the
|
|
192
|
+
array.
|
|
193
|
+
shape (tuple): Height and width of the array to draw on.
|
|
194
|
+
id_column (str): The column on the dataframe to use for the
|
|
195
|
+
value of each polygon drawn. The values in this column
|
|
196
|
+
must be integers.
|
|
197
|
+
default_value (int): The default value of the array, this will
|
|
198
|
+
the values returned for pixels that are not drawn on.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
numpy.ndarray: The array with the polygons drawn on it.
|
|
202
|
+
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
# Draw each polygon with its corresponding label
|
|
206
|
+
for _, row in gdf.iterrows():
|
|
207
|
+
# Create a list of (geometry, value) pairs
|
|
208
|
+
shapes = [(row.geometry, row[id_column])]
|
|
209
|
+
|
|
210
|
+
# Rasterize the shapes
|
|
211
|
+
burned = rasterize(
|
|
212
|
+
shapes=shapes,
|
|
213
|
+
out_shape=shape,
|
|
214
|
+
fill=default_value,
|
|
215
|
+
default_value=default_value,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
# Update the array where the polygon was burned
|
|
219
|
+
array = np.where(burned > 0, burned, array)
|
|
220
|
+
|
|
221
|
+
return array
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
"""Functions for tiling images.
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
Functions:
|
|
5
|
+
- tile_wsi_with_masks_from_dsa_annotations: Tile a WSI with semantic segmentation label masks created from DSA annotations.
|
|
6
|
+
- tile_image: Tile an image into smaller images.
|
|
7
|
+
- tile_image_with_mask: Tile an image and its corresponding mask.
|
|
6
8
|
|
|
7
9
|
"""
|
|
8
10
|
import large_image
|
|
@@ -10,12 +12,16 @@ import cv2 as cv
|
|
|
10
12
|
import numpy as np
|
|
11
13
|
from pathlib import Path
|
|
12
14
|
from multiprocessing import Pool
|
|
13
|
-
from tqdm import tqdm
|
|
14
|
-
from . import imwrite, imread
|
|
15
15
|
import pandas as pd
|
|
16
|
+
from shapely.affinity import scale, translate
|
|
17
|
+
import geopandas as gpd
|
|
18
|
+
|
|
19
|
+
from . import imwrite, imread
|
|
20
|
+
from .gpd_utils import draw_gdf_on_array
|
|
16
21
|
|
|
17
22
|
|
|
18
23
|
def _proccess_tile_with_masks_from_dsa_annotations(
|
|
24
|
+
gdf,
|
|
19
25
|
ts,
|
|
20
26
|
prepend_name,
|
|
21
27
|
x,
|
|
@@ -23,15 +29,19 @@ def _proccess_tile_with_masks_from_dsa_annotations(
|
|
|
23
29
|
scan_tile_size,
|
|
24
30
|
tile_size,
|
|
25
31
|
mag,
|
|
26
|
-
contours,
|
|
27
32
|
sf,
|
|
28
33
|
img_dir,
|
|
29
34
|
mask_dir,
|
|
30
|
-
|
|
35
|
+
background_value,
|
|
36
|
+
background_index,
|
|
31
37
|
edge_thr,
|
|
32
38
|
ignore_existing,
|
|
39
|
+
ignore_labels,
|
|
40
|
+
ignore_index,
|
|
41
|
+
ignore_value,
|
|
33
42
|
):
|
|
34
|
-
|
|
43
|
+
"""Processing tiles with masks, used in multiprocessing only."""
|
|
44
|
+
# Calcualte the x and y at magnification desired.
|
|
35
45
|
x_mag = int(x * sf)
|
|
36
46
|
y_mag = int(y * sf)
|
|
37
47
|
|
|
@@ -42,10 +52,10 @@ def _proccess_tile_with_masks_from_dsa_annotations(
|
|
|
42
52
|
|
|
43
53
|
# Ignore existing tiles.
|
|
44
54
|
if ignore_existing and img_fp.is_file() and mask_fp.is_file():
|
|
45
|
-
return str(img_fp), x_mag, y_mag
|
|
55
|
+
return str(img_fp), str(mask_fp), x_mag, y_mag
|
|
46
56
|
|
|
47
57
|
# Get the tile.
|
|
48
|
-
|
|
58
|
+
img = ts.getRegion(
|
|
49
59
|
region={
|
|
50
60
|
"left": x,
|
|
51
61
|
"top": y,
|
|
@@ -54,78 +64,71 @@ def _proccess_tile_with_masks_from_dsa_annotations(
|
|
|
54
64
|
},
|
|
55
65
|
format=large_image.constants.TILE_FORMAT_NUMPY,
|
|
56
66
|
scale={"magnification": mag},
|
|
57
|
-
)[0][:, :, :3]
|
|
67
|
+
)[0][:, :, :3].copy()
|
|
58
68
|
|
|
59
69
|
# Check if the tile area is below threshold.
|
|
60
|
-
h, w =
|
|
70
|
+
h, w = img.shape[:2]
|
|
61
71
|
|
|
62
72
|
if h * w / (tile_size * tile_size) < edge_thr:
|
|
63
73
|
return None
|
|
64
74
|
|
|
65
75
|
if (h, w) != (tile_size, tile_size):
|
|
66
76
|
# Pad the image with zeroes to make it the desired size.
|
|
67
|
-
|
|
68
|
-
|
|
77
|
+
img = cv.copyMakeBorder(
|
|
78
|
+
img,
|
|
69
79
|
0,
|
|
70
|
-
tile_size -
|
|
80
|
+
tile_size - h,
|
|
71
81
|
0,
|
|
72
|
-
tile_size -
|
|
82
|
+
tile_size - w,
|
|
73
83
|
cv.BORDER_CONSTANT,
|
|
74
|
-
value=
|
|
84
|
+
value=background_value,
|
|
75
85
|
)
|
|
76
86
|
|
|
77
|
-
|
|
78
|
-
tile_mask = np.zeros((tile_size, tile_size), dtype=np.uint8)
|
|
79
|
-
|
|
80
|
-
# Loop through contours to draw on tile mask.
|
|
81
|
-
for idx, contour_dict in contours.items():
|
|
82
|
-
# Create a mask for the specific label.
|
|
83
|
-
label_mask = np.zeros(tile_mask.shape, dtype=np.uint8)
|
|
84
|
-
|
|
85
|
-
points = contour_dict["points"].copy()
|
|
86
|
-
holes = contour_dict["holes"].copy()
|
|
87
|
-
|
|
88
|
-
# For points and hole:
|
|
89
|
-
# (1) Shift by top left of the current tile.
|
|
90
|
-
# (2) Scale by be desired magnification.
|
|
91
|
-
points = [((point - [x, y]) * sf).astype(int) for point in points]
|
|
92
|
-
|
|
93
|
-
corrected_holes = []
|
|
87
|
+
h, w = img.shape[:2]
|
|
94
88
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
corrected_holes.append(((hole - [x, y]) * sf).astype(int))
|
|
89
|
+
# Create a copy of the geodataframe for this tile.
|
|
90
|
+
tile_gdf = gdf.copy()
|
|
98
91
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
label_mask = cv.drawContours(label_mask, holes, -1, 0, cv.FILLED)
|
|
92
|
+
# Translate the coordinates so 0, 0 is the top left corner of the tile.
|
|
93
|
+
tile_gdf["geometry"] = tile_gdf["geometry"].apply(
|
|
94
|
+
lambda geom: translate(geom, xoff=-x_mag, yoff=-y_mag)
|
|
95
|
+
)
|
|
104
96
|
|
|
105
|
-
|
|
106
|
-
|
|
97
|
+
# Draw the dataframe on the tile mask.
|
|
98
|
+
mask = draw_gdf_on_array(
|
|
99
|
+
tile_gdf, (h, w), default_value=background_index
|
|
100
|
+
).copy()
|
|
107
101
|
|
|
108
|
-
|
|
102
|
+
if ignore_labels is not None:
|
|
103
|
+
img[np.isin(mask, ignore_labels)] = ignore_value
|
|
104
|
+
mask[np.isin(mask, ignore_labels)] = ignore_index
|
|
109
105
|
|
|
110
|
-
|
|
111
|
-
imwrite(
|
|
106
|
+
# Save the tile image and mask.
|
|
107
|
+
imwrite(img_fp, img)
|
|
108
|
+
imwrite(mask_fp, mask.astype(np.uint8), grayscale=True)
|
|
112
109
|
|
|
113
|
-
return str(img_fp), x_mag, y_mag
|
|
110
|
+
return str(img_fp), str(mask_fp), x_mag, y_mag
|
|
114
111
|
|
|
115
112
|
|
|
116
113
|
def tile_wsi_with_masks_from_dsa_annotations(
|
|
117
114
|
wsi_fp: str,
|
|
118
|
-
|
|
119
|
-
|
|
115
|
+
geojson_ann_doc: dict,
|
|
116
|
+
label2id: dict,
|
|
120
117
|
save_dir: str,
|
|
121
118
|
tile_size: int,
|
|
119
|
+
label_col: str = "group",
|
|
122
120
|
stride: int | None = None,
|
|
123
121
|
mag: float | None = None,
|
|
124
122
|
prepend_name: str = "",
|
|
125
123
|
nproc: int = 1,
|
|
126
|
-
|
|
124
|
+
background_value: tuple[int, int, int] | int = (255, 255, 255),
|
|
125
|
+
background_index: int = 0,
|
|
127
126
|
edge_thr: float = 0.25,
|
|
128
127
|
ignore_existing: bool = False,
|
|
128
|
+
ignore_labels: str | list[str] | None = None,
|
|
129
|
+
ignore_index: int = 255,
|
|
130
|
+
ignore_value: tuple[int, int, int] = (255, 255, 255),
|
|
131
|
+
notebook_tqdm: bool = False,
|
|
129
132
|
) -> list[str]:
|
|
130
133
|
"""Tile a WSI with semantic segmentation label masks created from
|
|
131
134
|
DSA annotations. DSA annotation class labels for elements are
|
|
@@ -134,11 +137,13 @@ def tile_wsi_with_masks_from_dsa_annotations(
|
|
|
134
137
|
|
|
135
138
|
Args:
|
|
136
139
|
wsi_fp (str): file path to WSI.
|
|
137
|
-
|
|
138
|
-
|
|
140
|
+
geojson_ann_doc dict: DSA annotation document in geojson format.
|
|
141
|
+
label2id (dict): mapping of label names to integer indices.
|
|
139
142
|
save_dir (str): directory to save the tiled images.
|
|
140
143
|
tile_size (int): size of the tile images at desired
|
|
141
144
|
magnificaiton.
|
|
145
|
+
label_col (str, optional): Column name for polygon labels.
|
|
146
|
+
Options are "group" or "label". Defaults to "group".
|
|
142
147
|
stride (int | None, optional): stride of the tiling. If None,
|
|
143
148
|
the stride will be the same as the tile size. Defaults to
|
|
144
149
|
None.
|
|
@@ -149,8 +154,12 @@ def tile_wsi_with_masks_from_dsa_annotations(
|
|
|
149
154
|
images and masks. Defaults to "".
|
|
150
155
|
nproc (int, optional): number of processes to use for tiling.
|
|
151
156
|
Defaults to 1.
|
|
152
|
-
|
|
153
|
-
|
|
157
|
+
background_value (int, optional): Value to use for the
|
|
158
|
+
background in the images, used when padding tiles at the
|
|
159
|
+
edge of WSI. Defaults to (255, 255, 255). If set to a single
|
|
160
|
+
value it will be used for all channels.
|
|
161
|
+
background_index (int, optional): Index of the background class.
|
|
162
|
+
Defaults to 0.
|
|
154
163
|
edge_thr (float, optional): For tiles at the edge, if most of
|
|
155
164
|
the tile is padded background then it is ignored. If the
|
|
156
165
|
amount of tile in WSI / amount of tile padded is less than
|
|
@@ -158,81 +167,83 @@ def tile_wsi_with_masks_from_dsa_annotations(
|
|
|
158
167
|
ignore_existing (bool, optional): Whether to ignore existing
|
|
159
168
|
tiles. If False, tiles will not be created if they already
|
|
160
169
|
exist. Defaults to False.
|
|
170
|
+
ignore_labels (str | list[str] | None, optional): Labels to
|
|
171
|
+
ignore. Defaults to None.
|
|
172
|
+
ignore_index (int, optional): Index of the ignore class. Defaults
|
|
173
|
+
to 255.
|
|
174
|
+
ignore_value (tuple[int, int, int], optional): Value to use for
|
|
175
|
+
the ignore class in the images. Defaults to (255, 255, 255).
|
|
176
|
+
If set to a single value it will be used for all channels.
|
|
177
|
+
notebook_tqdm (bool, optional): Whether to use tqdm in a
|
|
178
|
+
notebook. Defaults to False.
|
|
161
179
|
|
|
162
180
|
Returns:
|
|
163
181
|
list[str]: A list of tuples: (tile file path, x, y coordinates
|
|
164
182
|
of tile at magnification desired).
|
|
165
183
|
|
|
166
184
|
"""
|
|
167
|
-
|
|
168
|
-
|
|
185
|
+
if notebook_tqdm:
|
|
186
|
+
from tqdm.notebook import tqdm
|
|
187
|
+
else:
|
|
188
|
+
from tqdm import tqdm
|
|
169
189
|
|
|
170
|
-
|
|
171
|
-
|
|
190
|
+
if isinstance(ignore_labels, str):
|
|
191
|
+
ignore_labels = [ignore_labels]
|
|
172
192
|
|
|
173
|
-
|
|
174
|
-
|
|
193
|
+
if ignore_labels is not None:
|
|
194
|
+
ignore_labels = [label2id[label] for label in ignore_labels]
|
|
175
195
|
|
|
196
|
+
# Read the tile source.
|
|
197
|
+
ts = large_image.getTileSource(wsi_fp)
|
|
198
|
+
|
|
199
|
+
# Get the magnification at scan.
|
|
200
|
+
ts_metadata = ts.getMetadata()
|
|
201
|
+
scan_mag = ts_metadata["magnification"]
|
|
202
|
+
|
|
203
|
+
# Set the stride.
|
|
176
204
|
if stride is None:
|
|
177
205
|
stride = tile_size
|
|
178
206
|
|
|
179
|
-
#
|
|
207
|
+
# Get tile size & stride at scan magnification.
|
|
208
|
+
# Also, determine scaling factor to go from scan mag to desired mag.
|
|
180
209
|
if mag is None:
|
|
181
210
|
scan_tile_size = tile_size
|
|
182
211
|
scan_stride = stride
|
|
183
|
-
mag =
|
|
184
|
-
|
|
212
|
+
mag = scan_mag
|
|
213
|
+
scan_to_mag_sf = 1
|
|
185
214
|
else:
|
|
186
|
-
|
|
187
|
-
scan_tile_size = int(tile_size /
|
|
188
|
-
scan_stride = int(stride /
|
|
189
|
-
|
|
190
|
-
# Format annotation documents for drawing on tile masks.
|
|
191
|
-
contours = {idx: {"points": [], "holes": []} for idx in label2idx.values()}
|
|
215
|
+
scan_to_mag_sf = mag / scan_mag # scan mag -> desired mag
|
|
216
|
+
scan_tile_size = int(tile_size / scan_to_mag_sf)
|
|
217
|
+
scan_stride = int(stride / scan_to_mag_sf)
|
|
192
218
|
|
|
193
|
-
|
|
194
|
-
# Loop through the elements of the annotation document.
|
|
195
|
-
elements = ann_doc.get("annotation", {}).get("elements", [])
|
|
219
|
+
gdf = gpd.GeoDataFrame.from_features(geojson_ann_doc.get("features", []))
|
|
196
220
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
221
|
+
if label_col == "label":
|
|
222
|
+
# Convert the label column, which is a dictionary, set it to its 'value' key.
|
|
223
|
+
gdf["label"] = gdf["label"].apply(lambda x: x.get("value", ""))
|
|
200
224
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
continue
|
|
225
|
+
# Filter to polygons in label2id.
|
|
226
|
+
gdf = gdf[gdf[label_col].isin(label2id.keys())]
|
|
204
227
|
|
|
205
|
-
|
|
228
|
+
# Remove rows with geometry is not a Polygon.
|
|
229
|
+
gdf = gdf[gdf["geometry"].type == "Polygon"]
|
|
206
230
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
points = np.array(points, dtype=int)[:, :2]
|
|
214
|
-
contours[idx]["points"].append(points)
|
|
215
|
-
|
|
216
|
-
holes = element.get("holes", [])
|
|
217
|
-
|
|
218
|
-
if len(holes):
|
|
219
|
-
for hole in holes:
|
|
220
|
-
# Scale by the factor.
|
|
221
|
-
hole = np.array(hole, dtype=int)[:, :2]
|
|
222
|
-
contours[idx]["holes"].append(hole)
|
|
223
|
-
|
|
224
|
-
elif element_type == "rectangle":
|
|
225
|
-
# Convert the rectangle coordinates to polygon ones.
|
|
226
|
-
x1, y1 = element["center"][:2]
|
|
227
|
-
x2 = x1 + element["width"]
|
|
228
|
-
y2 = y1 + element["height"]
|
|
231
|
+
# Scale the coordinates to mag.
|
|
232
|
+
gdf["geometry"] = gdf["geometry"].apply(
|
|
233
|
+
lambda geom: scale(
|
|
234
|
+
geom, xfact=scan_to_mag_sf, yfact=scan_to_mag_sf, origin=(0, 0)
|
|
235
|
+
)
|
|
236
|
+
)
|
|
229
237
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
)
|
|
238
|
+
# Add and idx column, which is the map of the group to the label2id.
|
|
239
|
+
gdf["idx"] = gdf[label_col].map(label2id)
|
|
233
240
|
|
|
234
|
-
|
|
235
|
-
|
|
241
|
+
# Get the top left corner of every tile in the WSI, at scan mag.
|
|
242
|
+
xys = [
|
|
243
|
+
(x, y)
|
|
244
|
+
for x in range(0, ts_metadata["sizeX"], scan_stride)
|
|
245
|
+
for y in range(0, ts_metadata["sizeY"], scan_stride)
|
|
246
|
+
]
|
|
236
247
|
|
|
237
248
|
# Create directory to save tile images and masks.
|
|
238
249
|
save_dir = Path(save_dir)
|
|
@@ -241,20 +252,12 @@ def tile_wsi_with_masks_from_dsa_annotations(
|
|
|
241
252
|
img_dir.mkdir(parents=True, exist_ok=True)
|
|
242
253
|
mask_dir.mkdir(exist_ok=True)
|
|
243
254
|
|
|
244
|
-
# Get a list of x,y coordinates for the tile top left corners.
|
|
245
|
-
wsi_w, wsi_h = metadata["sizeX"], metadata["sizeY"]
|
|
246
|
-
|
|
247
|
-
xys = [
|
|
248
|
-
(x, y)
|
|
249
|
-
for x in range(0, wsi_w, scan_stride)
|
|
250
|
-
for y in range(0, wsi_h, scan_stride)
|
|
251
|
-
]
|
|
252
|
-
|
|
253
255
|
with Pool(nproc) as pool:
|
|
254
256
|
jobs = [
|
|
255
257
|
pool.apply_async(
|
|
256
258
|
_proccess_tile_with_masks_from_dsa_annotations,
|
|
257
259
|
(
|
|
260
|
+
gdf,
|
|
258
261
|
ts,
|
|
259
262
|
prepend_name,
|
|
260
263
|
xy[0],
|
|
@@ -262,13 +265,16 @@ def tile_wsi_with_masks_from_dsa_annotations(
|
|
|
262
265
|
scan_tile_size,
|
|
263
266
|
tile_size,
|
|
264
267
|
mag,
|
|
265
|
-
|
|
266
|
-
sf,
|
|
268
|
+
scan_to_mag_sf,
|
|
267
269
|
img_dir,
|
|
268
270
|
mask_dir,
|
|
269
|
-
|
|
271
|
+
background_value,
|
|
272
|
+
background_index,
|
|
270
273
|
edge_thr,
|
|
271
274
|
ignore_existing,
|
|
275
|
+
ignore_labels,
|
|
276
|
+
ignore_index,
|
|
277
|
+
ignore_value,
|
|
272
278
|
),
|
|
273
279
|
)
|
|
274
280
|
for xy in xys
|
|
@@ -282,7 +288,7 @@ def tile_wsi_with_masks_from_dsa_annotations(
|
|
|
282
288
|
if out is not None:
|
|
283
289
|
tile_info.append(job.get())
|
|
284
290
|
|
|
285
|
-
|
|
291
|
+
return tile_info
|
|
286
292
|
|
|
287
293
|
|
|
288
294
|
def tile_image(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dsa_helpers
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.4.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>
|
|
@@ -27,6 +27,7 @@ Requires-Dist: transformers[torch]
|
|
|
27
27
|
Requires-Dist: datasets
|
|
28
28
|
Requires-Dist: torchvision
|
|
29
29
|
Requires-Dist: large-image
|
|
30
|
+
Requires-Dist: rasterio
|
|
30
31
|
Dynamic: license-file
|
|
31
32
|
|
|
32
33
|
# Digital Slide Archive (DSA) Helpers
|
|
@@ -19,7 +19,8 @@ dependencies = [
|
|
|
19
19
|
"transformers[torch]",
|
|
20
20
|
"datasets",
|
|
21
21
|
"torchvision",
|
|
22
|
-
"large-image"
|
|
22
|
+
"large-image",
|
|
23
|
+
"rasterio"
|
|
23
24
|
]
|
|
24
25
|
name = "dsa_helpers"
|
|
25
26
|
authors = [
|
|
@@ -36,7 +37,7 @@ classifiers = [
|
|
|
36
37
|
"License :: OSI Approved :: MIT License",
|
|
37
38
|
]
|
|
38
39
|
keywords = ["dsa", "girder", "girder_client"]
|
|
39
|
-
version = "v1.1.
|
|
40
|
+
version = "v1.1.4.dev0"
|
|
40
41
|
|
|
41
42
|
[project.urls]
|
|
42
43
|
Repository = "https://github.com/Gutman-Lab/dsa-python-utils"
|
|
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.3.dev1 → dsa_helpers-1.1.4.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
|
|
File without changes
|
{dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers/ml/object_detection/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.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.3.dev1 → dsa_helpers-1.1.4.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
|