dsa-helpers 1.1.5.dev9__tar.gz → 1.1.6.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.5.dev9/dsa_helpers.egg-info → dsa_helpers-1.1.6.dev0}/PKG-INFO +1 -1
- {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/__init__.py +1 -1
- {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/girder_utils.py +37 -44
- {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/image_utils.py +11 -1
- dsa_helpers-1.1.6.dev0/dsa_helpers/ml/segformer_semantic_segmentation/inference.py +199 -0
- {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/ml/segformer_semantic_segmentation/train.py +11 -10
- {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/tiling.py +24 -21
- {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0/dsa_helpers.egg-info}/PKG-INFO +1 -1
- {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/pyproject.toml +1 -1
- dsa_helpers-1.1.5.dev9/dsa_helpers/ml/segformer_semantic_segmentation/inference.py +0 -123
- {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/.gitignore +0 -0
- {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/.readthedocs.yaml +0 -0
- {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/LICENSE +0 -0
- {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/README.md +0 -0
- {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/app.py +0 -0
- {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dev.py +0 -0
- {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/docs/requirements.txt +0 -0
- {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/docs/source/conf.py +0 -0
- {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/docs/source/index.rst +0 -0
- {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/cli_utils.py +0 -0
- {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/dash/__init__.py +0 -0
- {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/dash/dash_paperdragon_utils.py +0 -0
- {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/dash/header.py +0 -0
- {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/dash/header_callbacks.py +0 -0
- {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/gpd_utils.py +0 -0
- {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/imread.py +0 -0
- {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/imwrite.py +0 -0
- {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/ml/__init__.py +0 -0
- {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/ml/callbacks.py +0 -0
- {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/ml/datasets/SegFormerSegmentationDataset.py +0 -0
- {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/ml/datasets/__init__.py +0 -0
- {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/ml/datasets/utils.py +0 -0
- {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/ml/evaluate.py +0 -0
- {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/ml/metrics.py +0 -0
- {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/ml/object_detection/__init__.py +0 -0
- {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/ml/object_detection/get_pearce_roi_images.py +0 -0
- {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/ml/object_detection/tile_image.py +0 -0
- {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/ml/object_detection/utils.py +0 -0
- {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/ml/segformer_semantic_segmentation/__init__.py +0 -0
- {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/ml/segformer_semantic_segmentation/datasets.py +0 -0
- {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/ml/segformer_semantic_segmentation/evaluate.py +0 -0
- {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/ml/segformer_semantic_segmentation/transforms.py +0 -0
- {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/ml/segformer_semantic_segmentation/utils.py +0 -0
- {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/ml/transforms/__init__.py +0 -0
- {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/ml/transforms/segformer_transforms.py +0 -0
- {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/mongo_utils.py +0 -0
- {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/tile_utils.py +0 -0
- {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/utils.py +0 -0
- {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers.egg-info/SOURCES.txt +0 -0
- {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers.egg-info/dependency_links.txt +0 -0
- {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers.egg-info/requires.txt +0 -0
- {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers.egg-info/top_level.txt +0 -0
- {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/setup.cfg +0 -0
- {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/tests/app/Dockerfile +0 -0
- {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/tests/app/app.py +0 -0
- {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/tests/docker-compose.yaml +0 -0
- {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.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.6.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>
|
|
@@ -9,8 +9,10 @@ Functions:
|
|
|
9
9
|
- get_element_contours: Get the contours of an element, regardless of the type
|
|
10
10
|
- get_roi_images: Get regions of interest (ROIs) as images from DSA annotations
|
|
11
11
|
- post_annotation: Post a new annotation to the DSA
|
|
12
|
+
- post_annotations_from_gdf: Post annotations from a GeoDataFrame
|
|
12
13
|
- remove_overlapping_annotations: Remove overlapping regions from elements
|
|
13
14
|
- upload_dir_to_dsa: Upload a local directory to a DSA item
|
|
15
|
+
|
|
14
16
|
"""
|
|
15
17
|
|
|
16
18
|
from girder_client import GirderClient, HttpError
|
|
@@ -21,7 +23,7 @@ from pathlib import Path
|
|
|
21
23
|
import cv2 as cv
|
|
22
24
|
from copy import deepcopy
|
|
23
25
|
import geopandas as gpd
|
|
24
|
-
from shapely.geometry import Polygon, box
|
|
26
|
+
from shapely.geometry import Polygon, box, LineString
|
|
25
27
|
from .gpd_utils import remove_gdf_overlaps
|
|
26
28
|
|
|
27
29
|
|
|
@@ -515,8 +517,6 @@ def post_annotations_from_gdf(
|
|
|
515
517
|
doc_name: str,
|
|
516
518
|
gdf: gpd.GeoDataFrame,
|
|
517
519
|
idx_config: dict,
|
|
518
|
-
tolerance: float = 0.5,
|
|
519
|
-
max_points: int = 1000,
|
|
520
520
|
) -> dict:
|
|
521
521
|
"""Post annotations from a GeoDataFrame to the DSA.
|
|
522
522
|
|
|
@@ -528,10 +528,6 @@ def post_annotations_from_gdf(
|
|
|
528
528
|
idx_config (dict): For each label in the GeoDataFrame, the configuration
|
|
529
529
|
for the DSA annotation. Include "label", "fillColor", "lineColor", and
|
|
530
530
|
"group".
|
|
531
|
-
tolerance (float, optional): The tolerance to simplify the polygons.
|
|
532
|
-
Defaults to 0.5.
|
|
533
|
-
max_points (int, optional): If polygon has more than max_points,
|
|
534
|
-
simplify the polygon. Defaults to 1000.
|
|
535
531
|
|
|
536
532
|
Returns:
|
|
537
533
|
dict: The response from the DSA.
|
|
@@ -539,51 +535,44 @@ def post_annotations_from_gdf(
|
|
|
539
535
|
"""
|
|
540
536
|
elements = []
|
|
541
537
|
|
|
538
|
+
# This step makes sure that each row contains a single polygon.
|
|
539
|
+
# Multi-polygons will be exploded into multiple rows.
|
|
540
|
+
gdf = gdf.explode(index_parts=False).reset_index(drop=True)
|
|
541
|
+
|
|
542
542
|
# Process the dataframe.
|
|
543
|
-
for _,
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
row["geometry"].geoms
|
|
547
|
-
) # assuming multipolygons
|
|
548
|
-
label = row["label"]
|
|
549
|
-
|
|
550
|
-
for poly in list_of_polygons:
|
|
551
|
-
exterior_poly = list(poly.exterior.coords)
|
|
552
|
-
interior_polys = [
|
|
553
|
-
list(interior.coords) for interior in poly.interiors
|
|
554
|
-
]
|
|
543
|
+
for _, r in gdf.iterrows():
|
|
544
|
+
label = r["label"]
|
|
545
|
+
poly = r["geometry"]
|
|
555
546
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
exterior_poly = list(poly.exterior.coords)
|
|
547
|
+
exterior_poly = list(poly.exterior.coords)
|
|
548
|
+
interior_polys = [list(interior.coords) for interior in poly.interiors]
|
|
559
549
|
|
|
560
|
-
|
|
561
|
-
[int(xy[0]) * 2, int(xy[1]) * 2, 0] for xy in exterior_poly
|
|
562
|
-
]
|
|
550
|
+
points = [[int(xy[0]), int(xy[1]), 0] for xy in exterior_poly]
|
|
563
551
|
|
|
564
|
-
|
|
552
|
+
if len(points) == 0:
|
|
553
|
+
continue
|
|
565
554
|
|
|
566
|
-
|
|
567
|
-
hole = [
|
|
568
|
-
[int(xy[0]) * 2, int(xy[1]) * 2, 0] for xy in interior_poly
|
|
569
|
-
]
|
|
570
|
-
holes.append(hole)
|
|
555
|
+
holes = []
|
|
571
556
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
"lineColor": idx_config[label]["lineColor"],
|
|
576
|
-
"type": "polyline",
|
|
577
|
-
"lineWidth": 2,
|
|
578
|
-
"closed": True,
|
|
579
|
-
"label": {"value": idx_config[label]["label"]},
|
|
580
|
-
"group": idx_config[label]["group"],
|
|
581
|
-
}
|
|
557
|
+
for interior_poly in interior_polys:
|
|
558
|
+
hole = [[int(xy[0]), int(xy[1]), 0] for xy in interior_poly]
|
|
559
|
+
holes.append(hole)
|
|
582
560
|
|
|
583
|
-
|
|
584
|
-
|
|
561
|
+
element = {
|
|
562
|
+
"points": points,
|
|
563
|
+
"fillColor": idx_config[label]["fillColor"],
|
|
564
|
+
"lineColor": idx_config[label]["lineColor"],
|
|
565
|
+
"type": "polyline",
|
|
566
|
+
"lineWidth": 2,
|
|
567
|
+
"closed": True,
|
|
568
|
+
"label": {"value": idx_config[label]["label"]},
|
|
569
|
+
"group": idx_config[label]["group"],
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if len(holes):
|
|
573
|
+
element["holes"] = holes
|
|
585
574
|
|
|
586
|
-
|
|
575
|
+
elements.append(element)
|
|
587
576
|
|
|
588
577
|
response = gc.post(
|
|
589
578
|
"/annotation",
|
|
@@ -807,6 +796,10 @@ def remove_overlapping_annotations(
|
|
|
807
796
|
del element["order"]
|
|
808
797
|
geometry = element.pop("geometry")
|
|
809
798
|
|
|
799
|
+
# If the object ended up being a line, skipt it.
|
|
800
|
+
if isinstance(geometry, LineString):
|
|
801
|
+
continue
|
|
802
|
+
|
|
810
803
|
exterior_poly = list(geometry.exterior.coords)
|
|
811
804
|
interior_polys = [
|
|
812
805
|
list(interior.coords) for interior in geometry.interiors
|
|
@@ -103,7 +103,14 @@ def label_mask_to_polygons(
|
|
|
103
103
|
|
|
104
104
|
|
|
105
105
|
def convert_label_mask_to_rgb(
|
|
106
|
-
mask: np.ndarray,
|
|
106
|
+
mask: np.ndarray,
|
|
107
|
+
int2rgb: dict,
|
|
108
|
+
alpha: bool = False,
|
|
109
|
+
fill_color: tuple[int, int, int] | tuple[int, int, int, int] = (
|
|
110
|
+
255,
|
|
111
|
+
255,
|
|
112
|
+
255,
|
|
113
|
+
),
|
|
107
114
|
) -> np.ndarray:
|
|
108
115
|
"""
|
|
109
116
|
Convert a label mask to an RGB mask.
|
|
@@ -113,6 +120,7 @@ def convert_label_mask_to_rgb(
|
|
|
113
120
|
int2rgb: Dictionary mapping integer labels to RGB colors. If
|
|
114
121
|
passing alpha as True, then pass RGBA colors.
|
|
115
122
|
alpha: Whether to include the alpha channel in the RGB mask.
|
|
123
|
+
fill_color: Color to fill the mask with.
|
|
116
124
|
|
|
117
125
|
Returns:
|
|
118
126
|
(np.ndarray): RGB(A) mask.
|
|
@@ -123,6 +131,8 @@ def convert_label_mask_to_rgb(
|
|
|
123
131
|
(mask.shape[0], mask.shape[1], 4 if alpha else 3), dtype=np.uint8
|
|
124
132
|
)
|
|
125
133
|
|
|
134
|
+
rgb_mask[:] = fill_color
|
|
135
|
+
|
|
126
136
|
# Loop through the label mask.
|
|
127
137
|
for label, rgb in int2rgb.items():
|
|
128
138
|
rgb_mask[mask == label] = rgb
|
|
@@ -0,0 +1,199 @@
|
|
|
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
|
+
|
|
13
|
+
from dsa_helpers.image_utils import label_mask_to_polygons
|
|
14
|
+
from dsa_helpers.gpd_utils import remove_gdf_overlaps
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def inference(
|
|
18
|
+
model: str | torch.nn.Module,
|
|
19
|
+
wsi_fp: str,
|
|
20
|
+
batch_size: int = 16,
|
|
21
|
+
tile_size: int = 512,
|
|
22
|
+
mag: float | None = None,
|
|
23
|
+
workers: int = 8,
|
|
24
|
+
chunk_mult: int = 2,
|
|
25
|
+
prefetch: int = 2,
|
|
26
|
+
device: str | None = None,
|
|
27
|
+
tolerance: float | None = 2.0,
|
|
28
|
+
min_area: float = 1000,
|
|
29
|
+
) -> gpd.GeoDataFrame:
|
|
30
|
+
"""Inference using SegFormer semantic segmentation model on a WSI.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
model (str | torch.nn.Module): Path to the model checkpoint or
|
|
34
|
+
a pre-loaded model.
|
|
35
|
+
wsi_fp (str): File path to the WSI.
|
|
36
|
+
batch_size (int, optional): Batch size for inference. Defaults
|
|
37
|
+
to 16.
|
|
38
|
+
tile_size (int, optional): Tile size for inference. Defaults to
|
|
39
|
+
512.
|
|
40
|
+
mag (float, optional): Magnification for inference. Defaults to
|
|
41
|
+
None, which will use the scan magnification of WSI.
|
|
42
|
+
workers (int, optional): Number of workers for inference.
|
|
43
|
+
Defaults to 8.
|
|
44
|
+
chunk_mult (int, optional): Chunk multiplier for inference.
|
|
45
|
+
Defaults to 2.
|
|
46
|
+
prefetch (int, optional): Number of prefetch for inference.
|
|
47
|
+
Defaults to 2.
|
|
48
|
+
device (str, optional): Device for inference. Default is None,
|
|
49
|
+
will use "gpu" if available, otherwise "cpu".
|
|
50
|
+
tolerance (float | None, optional): Tolerance for simplification
|
|
51
|
+
of the predicted polygons. Guidelines: 0.5 for mild
|
|
52
|
+
simplification, 1.0 for moderate, 2.0 for more aggressive.
|
|
53
|
+
If set to None or 0.0, the polygons will not be simplified.
|
|
54
|
+
min_area (float, optional): Minimum area of a polygon to be
|
|
55
|
+
simplified. This is added to avoid simplifying small
|
|
56
|
+
polygons that would probably lose too much information.
|
|
57
|
+
Defaults to 1000.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
gpd.GeoDataFrame: A GeoDataFrame containing the predicted
|
|
61
|
+
polygons and labels.
|
|
62
|
+
|
|
63
|
+
"""
|
|
64
|
+
# Initiate the tile iterator.
|
|
65
|
+
iterator = LargeImagePrefetch(
|
|
66
|
+
wsi_fp,
|
|
67
|
+
batch=batch_size,
|
|
68
|
+
tile_size=(tile_size, tile_size),
|
|
69
|
+
scale_mode="mag",
|
|
70
|
+
target_scale=mag,
|
|
71
|
+
workers=workers,
|
|
72
|
+
chunk_mult=chunk_mult,
|
|
73
|
+
prefetch=prefetch,
|
|
74
|
+
nchw=False,
|
|
75
|
+
icc=True,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
if device is None:
|
|
79
|
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
|
80
|
+
print(f"Using device: {device}")
|
|
81
|
+
|
|
82
|
+
device = torch.device(device)
|
|
83
|
+
|
|
84
|
+
# Load the model.
|
|
85
|
+
if isinstance(model, str):
|
|
86
|
+
model = SegformerForSemanticSegmentation.from_pretrained(
|
|
87
|
+
model, local_files_only=True, device_map=device
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
model.eval()
|
|
91
|
+
|
|
92
|
+
# Iterate through batches.
|
|
93
|
+
batch_n = 0
|
|
94
|
+
|
|
95
|
+
# Image processor for images.
|
|
96
|
+
processor = SegformerImageProcessor()
|
|
97
|
+
|
|
98
|
+
# Track all predicted polygons.
|
|
99
|
+
wsi_polygons = []
|
|
100
|
+
|
|
101
|
+
# Scaling factor, multiply to go from scan magnification to desired mag.
|
|
102
|
+
ts_metadata = getTileSource(wsi_fp).getMetadata()
|
|
103
|
+
scan_mag = ts_metadata["magnification"]
|
|
104
|
+
|
|
105
|
+
if mag is None:
|
|
106
|
+
mag = scan_mag
|
|
107
|
+
sf = 1.0
|
|
108
|
+
else:
|
|
109
|
+
sf = mag / scan_mag
|
|
110
|
+
|
|
111
|
+
for batch in iterator:
|
|
112
|
+
# Get the batch of images.
|
|
113
|
+
imgs = batch[0].view() # returns a numpy array of shape (N, H, W, C)
|
|
114
|
+
coordinates = batch[1]
|
|
115
|
+
|
|
116
|
+
# Convert the numpy arrays to PIL images.
|
|
117
|
+
imgs = [Image.fromarray(img) for img in imgs]
|
|
118
|
+
|
|
119
|
+
# Pass the images through the processor.
|
|
120
|
+
inputs = processor(imgs, return_tensors="pt")
|
|
121
|
+
inputs = inputs.to(model.device)
|
|
122
|
+
|
|
123
|
+
# Predict on the batch.
|
|
124
|
+
with torch.no_grad():
|
|
125
|
+
output = model(inputs["pixel_values"])
|
|
126
|
+
logits = output.logits
|
|
127
|
+
|
|
128
|
+
# Get the logits out, resizing them to the original tile size.
|
|
129
|
+
logits = torch.nn.functional.interpolate(
|
|
130
|
+
logits,
|
|
131
|
+
size=tile_size,
|
|
132
|
+
mode="bilinear",
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# Get predicted class labels for each pixel.
|
|
136
|
+
masks = torch.argmax(logits, dim=1).detach().cpu().numpy()
|
|
137
|
+
|
|
138
|
+
# Loop through each mask to extract the contours as shapely polygons.
|
|
139
|
+
for i, mask in enumerate(masks):
|
|
140
|
+
img_metadata = coordinates[i]
|
|
141
|
+
x, y = img_metadata[6], img_metadata[4]
|
|
142
|
+
x = int(x * sf)
|
|
143
|
+
y = int(y * sf)
|
|
144
|
+
|
|
145
|
+
polygon_and_labels = label_mask_to_polygons(
|
|
146
|
+
mask,
|
|
147
|
+
x_offset=x,
|
|
148
|
+
y_offset=y,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
for polygon_and_label in polygon_and_labels:
|
|
152
|
+
polygon, label = polygon_and_label
|
|
153
|
+
label = int(label)
|
|
154
|
+
|
|
155
|
+
# Do something with the polygon and label.
|
|
156
|
+
wsi_polygons.append([polygon, label])
|
|
157
|
+
|
|
158
|
+
batch_n += 1
|
|
159
|
+
print(f"\r Processed batch {batch_n}. ", end="")
|
|
160
|
+
print()
|
|
161
|
+
|
|
162
|
+
# Convert polygons and labels to a GeoDataFrame.
|
|
163
|
+
gdf = gpd.GeoDataFrame(wsi_polygons, columns=["geometry", "label"])
|
|
164
|
+
gdf["geometry"] = gdf["geometry"].buffer(1)
|
|
165
|
+
gdf = gdf.dissolve(by="label", as_index=False)
|
|
166
|
+
gdf = gdf.explode(index_parts=False).reset_index(drop=True)
|
|
167
|
+
|
|
168
|
+
if tolerance:
|
|
169
|
+
# Add the area column.
|
|
170
|
+
gdf["area"] = gdf["geometry"].area
|
|
171
|
+
|
|
172
|
+
# Split the dataframe into those polygons that need to be simplified and those that do not.
|
|
173
|
+
gdf_to_simplify = gdf[gdf["area"] > min_area].reset_index(drop=True)
|
|
174
|
+
gdf_not_to_simplify = gdf[gdf["area"] <= min_area].reset_index(
|
|
175
|
+
drop=True
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# Simplify the polygons that need to be simplified.
|
|
179
|
+
gdf_to_simplify["geometry"] = gdf_to_simplify["geometry"].simplify(
|
|
180
|
+
tolerance=tolerance, preserve_topology=True
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
# Concatenate the simplified polygons with the polygons that do not need to be simplified.
|
|
184
|
+
gdf = pd.concat(
|
|
185
|
+
[gdf_to_simplify, gdf_not_to_simplify], ignore_index=True
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# Remove overlapping polygons.
|
|
189
|
+
no_overlap_gdf = remove_gdf_overlaps(
|
|
190
|
+
gdf,
|
|
191
|
+
columns="label",
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
# Scale the polygon to scan magnification.
|
|
195
|
+
no_overlap_gdf["geometry"] = no_overlap_gdf["geometry"].apply(
|
|
196
|
+
lambda geom: scale(geom, xfact=1 / sf, yfact=1 / sf, origin=(0, 0))
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
return no_overlap_gdf
|
|
@@ -128,14 +128,15 @@ def train_segformer_semantic_segmentation_model(
|
|
|
128
128
|
train_data, transforms=train_transforms
|
|
129
129
|
)
|
|
130
130
|
|
|
131
|
-
|
|
132
|
-
(
|
|
133
|
-
val_data
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
131
|
+
if val_data is not None and max_val_size < len(val_data):
|
|
132
|
+
val_dataset = create_segformer_segmentation_dataset(
|
|
133
|
+
val_data.sample(n=max_val_size, random_state=random_state),
|
|
134
|
+
transforms=val_transforms,
|
|
135
|
+
)
|
|
136
|
+
else:
|
|
137
|
+
val_dataset = create_segformer_segmentation_dataset(
|
|
138
|
+
val_data, transforms=val_transforms
|
|
139
|
+
)
|
|
139
140
|
|
|
140
141
|
def compute_metrics(eval_pred):
|
|
141
142
|
"""Function for computing DICE coefficient metrics for each
|
|
@@ -183,11 +184,11 @@ def train_segformer_semantic_segmentation_model(
|
|
|
183
184
|
denominator_value = denominator[label]
|
|
184
185
|
|
|
185
186
|
if denominator_value:
|
|
186
|
-
metrics[label] = float(
|
|
187
|
+
metrics[f"{label}_dice"] = float(
|
|
187
188
|
2 * intersection[label] / denominator_value
|
|
188
189
|
)
|
|
189
190
|
else:
|
|
190
|
-
metrics[label] = 1
|
|
191
|
+
metrics[f"{label}_dice"] = 1
|
|
191
192
|
|
|
192
193
|
# Add the mean Dice.
|
|
193
194
|
metrics["mean_dice"] = float(np.mean(list(metrics.values())))
|
|
@@ -32,13 +32,13 @@ def _proccess_tile_with_masks_from_dsa_annotations(
|
|
|
32
32
|
sf,
|
|
33
33
|
img_dir,
|
|
34
34
|
mask_dir,
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
edge_value,
|
|
36
|
+
background_id,
|
|
37
37
|
edge_thr,
|
|
38
38
|
ignore_existing,
|
|
39
|
-
|
|
40
|
-
ignore_index,
|
|
39
|
+
ignore_id,
|
|
41
40
|
ignore_value,
|
|
41
|
+
ignore_ids,
|
|
42
42
|
):
|
|
43
43
|
"""Processing tiles with masks, used in multiprocessing only."""
|
|
44
44
|
# Calcualte the x and y at magnification desired.
|
|
@@ -81,7 +81,7 @@ def _proccess_tile_with_masks_from_dsa_annotations(
|
|
|
81
81
|
0,
|
|
82
82
|
tile_size - w,
|
|
83
83
|
cv.BORDER_CONSTANT,
|
|
84
|
-
value=
|
|
84
|
+
value=edge_value,
|
|
85
85
|
)
|
|
86
86
|
|
|
87
87
|
h, w = img.shape[:2]
|
|
@@ -96,12 +96,13 @@ def _proccess_tile_with_masks_from_dsa_annotations(
|
|
|
96
96
|
|
|
97
97
|
# Draw the dataframe on the tile mask.
|
|
98
98
|
mask = draw_gdf_on_array(
|
|
99
|
-
tile_gdf, (h, w), default_value=
|
|
99
|
+
tile_gdf, (h, w), default_value=background_id
|
|
100
100
|
).copy()
|
|
101
101
|
|
|
102
|
-
if
|
|
103
|
-
|
|
104
|
-
|
|
102
|
+
if ignore_ids is not None:
|
|
103
|
+
# Apply the ignore ids to the image and mask.
|
|
104
|
+
img[np.isin(mask, ignore_ids)] = ignore_value
|
|
105
|
+
mask[np.isin(mask, ignore_ids)] = ignore_id
|
|
105
106
|
|
|
106
107
|
# Save the tile image and mask.
|
|
107
108
|
imwrite(img_fp, img)
|
|
@@ -121,12 +122,12 @@ def tile_wsi_with_masks_from_dsa_annotations(
|
|
|
121
122
|
mag: float | None = None,
|
|
122
123
|
prepend_name: str = "",
|
|
123
124
|
nproc: int = 1,
|
|
124
|
-
|
|
125
|
-
|
|
125
|
+
edge_value: tuple[int, int, int] | int = (255, 255, 255),
|
|
126
|
+
background_id: int = 0,
|
|
126
127
|
edge_thr: float = 0.25,
|
|
127
128
|
ignore_existing: bool = False,
|
|
128
129
|
ignore_labels: str | list[str] | None = None,
|
|
129
|
-
|
|
130
|
+
ignore_id: int = 255,
|
|
130
131
|
ignore_value: tuple[int, int, int] = (255, 255, 255),
|
|
131
132
|
notebook_tqdm: bool = False,
|
|
132
133
|
) -> list[str]:
|
|
@@ -187,11 +188,14 @@ def tile_wsi_with_masks_from_dsa_annotations(
|
|
|
187
188
|
else:
|
|
188
189
|
from tqdm import tqdm
|
|
189
190
|
|
|
191
|
+
# Should be a list of string labels.
|
|
190
192
|
if isinstance(ignore_labels, str):
|
|
191
193
|
ignore_labels = [ignore_labels]
|
|
192
194
|
|
|
193
195
|
if ignore_labels is not None:
|
|
194
|
-
|
|
196
|
+
ignore_ids = [label2id[label] for label in ignore_labels]
|
|
197
|
+
else:
|
|
198
|
+
ignore_ids = None
|
|
195
199
|
|
|
196
200
|
# Read the tile source.
|
|
197
201
|
ts = large_image.getTileSource(wsi_fp)
|
|
@@ -218,9 +222,8 @@ def tile_wsi_with_masks_from_dsa_annotations(
|
|
|
218
222
|
|
|
219
223
|
gdf = gpd.GeoDataFrame.from_features(geojson_ann_doc.get("features", []))
|
|
220
224
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
gdf["label"] = gdf["label"].apply(lambda x: x.get("value", ""))
|
|
225
|
+
# Convert the label column, which is a dictionary, set it to its 'value' key.
|
|
226
|
+
gdf["label"] = gdf["label"].apply(lambda x: x.get("value", ""))
|
|
224
227
|
|
|
225
228
|
# Filter to polygons in label2id.
|
|
226
229
|
gdf = gdf[gdf[label_col].isin(label2id.keys())]
|
|
@@ -235,7 +238,7 @@ def tile_wsi_with_masks_from_dsa_annotations(
|
|
|
235
238
|
)
|
|
236
239
|
)
|
|
237
240
|
|
|
238
|
-
# Add
|
|
241
|
+
# Add an idx column, which is the map of the group to the label2id.
|
|
239
242
|
gdf["idx"] = gdf[label_col].map(label2id)
|
|
240
243
|
|
|
241
244
|
# Get the top left corner of every tile in the WSI, at scan mag.
|
|
@@ -268,13 +271,13 @@ def tile_wsi_with_masks_from_dsa_annotations(
|
|
|
268
271
|
scan_to_mag_sf,
|
|
269
272
|
img_dir,
|
|
270
273
|
mask_dir,
|
|
271
|
-
|
|
272
|
-
|
|
274
|
+
edge_value,
|
|
275
|
+
background_id,
|
|
273
276
|
edge_thr,
|
|
274
277
|
ignore_existing,
|
|
275
|
-
|
|
276
|
-
ignore_index,
|
|
278
|
+
ignore_id,
|
|
277
279
|
ignore_value,
|
|
280
|
+
ignore_ids,
|
|
278
281
|
),
|
|
279
282
|
)
|
|
280
283
|
for xy in xys
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dsa_helpers
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.6.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>
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import numpy as np
|
|
2
|
-
from pathlib import Path
|
|
3
|
-
import cv2 as cv
|
|
4
|
-
import shutil
|
|
5
|
-
|
|
6
|
-
import transformers
|
|
7
|
-
from transformers import SegformerImageProcessor
|
|
8
|
-
|
|
9
|
-
import torch
|
|
10
|
-
import torch.nn as nn
|
|
11
|
-
|
|
12
|
-
from ... import imread
|
|
13
|
-
from ...tiling import tile_image
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def inference(
|
|
17
|
-
img: np.ndarray,
|
|
18
|
-
model: transformers.models.segformer.modeling_segformer.SegformerForSemanticSegmentation,
|
|
19
|
-
device: str | None = None,
|
|
20
|
-
tile_size: int = 512,
|
|
21
|
-
temp_dir: str = ".temp",
|
|
22
|
-
batch_size: int = 8,
|
|
23
|
-
) -> tuple[np.ndarray, list[np.ndarray]]:
|
|
24
|
-
"""Inference on an image using SegFormer model for semantic segmentation.
|
|
25
|
-
|
|
26
|
-
Args:
|
|
27
|
-
img (numpy.ndarray): Image to segment.
|
|
28
|
-
model (transformers.models.segformer.modeling_segformer.SegformerForSemanticSegmentation):
|
|
29
|
-
Pre-trained model.
|
|
30
|
-
device (str | None): Device to use. Defaults to None, in which case
|
|
31
|
-
it is inferred based on devices available.
|
|
32
|
-
tile_size (int): Size of the tiles. Defaults to 512.
|
|
33
|
-
temp_dir (str): Temporary directory to save tiles. Defaults to ".temp".
|
|
34
|
-
batch_size (int): Batch size. Defaults to 8.
|
|
35
|
-
|
|
36
|
-
Returns:
|
|
37
|
-
tuple[numpy.ndarray, list[numpy.ndarray]]: The predicted label mask and
|
|
38
|
-
the contours of the mask.
|
|
39
|
-
|
|
40
|
-
"""
|
|
41
|
-
# Create location to save tile images.
|
|
42
|
-
tile_dir = Path(temp_dir)
|
|
43
|
-
tile_dir.mkdir(exist_ok=True, parents=True)
|
|
44
|
-
|
|
45
|
-
# Tile the image.
|
|
46
|
-
tiles_df = tile_image(
|
|
47
|
-
img,
|
|
48
|
-
str(tile_dir),
|
|
49
|
-
tile_size=tile_size,
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
# Create instance of the image processor.
|
|
53
|
-
img_processor = SegformerImageProcessor()
|
|
54
|
-
|
|
55
|
-
if device is None:
|
|
56
|
-
device = "cuda" if torch.cuda.is_available() else "cpu"
|
|
57
|
-
|
|
58
|
-
print("Using device:", device)
|
|
59
|
-
|
|
60
|
-
# Set model to eval mode.
|
|
61
|
-
model.to(device) # to device
|
|
62
|
-
model.eval()
|
|
63
|
-
|
|
64
|
-
# Create an empty mask the size of the image.
|
|
65
|
-
h, w = img.shape[:2]
|
|
66
|
-
predicted_mask = np.zeros((h + tile_size, w + tile_size), dtype=np.uint8)
|
|
67
|
-
|
|
68
|
-
# Loop through tile images in batch size.
|
|
69
|
-
for i in range(0, len(tiles_df), batch_size):
|
|
70
|
-
# Get batch of tile images.
|
|
71
|
-
batch = tiles_df.iloc[i : i + batch_size]
|
|
72
|
-
|
|
73
|
-
# Read all the images in the batch.
|
|
74
|
-
images = [imread(fp) for fp in batch["fp"]]
|
|
75
|
-
|
|
76
|
-
# Process all the images.
|
|
77
|
-
inputs = img_processor(
|
|
78
|
-
images=images,
|
|
79
|
-
return_tensors="pt",
|
|
80
|
-
)
|
|
81
|
-
|
|
82
|
-
# Send to the same device as model.
|
|
83
|
-
inputs = inputs.to(device)
|
|
84
|
-
|
|
85
|
-
with torch.no_grad():
|
|
86
|
-
# Sent inputs through the model.
|
|
87
|
-
outputs = model(**inputs)
|
|
88
|
-
|
|
89
|
-
# Get out the logits.
|
|
90
|
-
logits = outputs.logits
|
|
91
|
-
|
|
92
|
-
# Reshape the logits to the original image size.
|
|
93
|
-
upsampled_logits = nn.functional.interpolate(
|
|
94
|
-
logits,
|
|
95
|
-
size=(tile_size, tile_size), # (height, width)
|
|
96
|
-
mode="bilinear",
|
|
97
|
-
align_corners=False,
|
|
98
|
-
)
|
|
99
|
-
|
|
100
|
-
# Take the max value of the class dimension.
|
|
101
|
-
pred_seg = upsampled_logits.argmax(dim=1)
|
|
102
|
-
|
|
103
|
-
# Place tile predictions into the overall predicted mask.
|
|
104
|
-
for j in range(len(batch)):
|
|
105
|
-
r = batch.iloc[j]
|
|
106
|
-
x, y = r["x"], r["y"]
|
|
107
|
-
predicted_mask[y : y + tile_size, x : x + tile_size] = (
|
|
108
|
-
pred_seg[j].cpu().numpy()
|
|
109
|
-
).astype(np.uint8)
|
|
110
|
-
|
|
111
|
-
# Remove the temp directory.
|
|
112
|
-
shutil.rmtree(temp_dir)
|
|
113
|
-
|
|
114
|
-
# Remove edges.
|
|
115
|
-
predicted_mask = predicted_mask[:h, :w]
|
|
116
|
-
|
|
117
|
-
# Extract the contours of the mask.
|
|
118
|
-
contours = cv.findContours(
|
|
119
|
-
predicted_mask, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE
|
|
120
|
-
)[0]
|
|
121
|
-
|
|
122
|
-
# Return the mask.
|
|
123
|
-
return predicted_mask, contours
|
|
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.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/dash/dash_paperdragon_utils.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/ml/object_detection/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/ml/transforms/segformer_transforms.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|