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.
Files changed (57) hide show
  1. {dsa_helpers-1.1.5.dev9/dsa_helpers.egg-info → dsa_helpers-1.1.6.dev0}/PKG-INFO +1 -1
  2. {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/__init__.py +1 -1
  3. {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/girder_utils.py +37 -44
  4. {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/image_utils.py +11 -1
  5. dsa_helpers-1.1.6.dev0/dsa_helpers/ml/segformer_semantic_segmentation/inference.py +199 -0
  6. {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/ml/segformer_semantic_segmentation/train.py +11 -10
  7. {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/tiling.py +24 -21
  8. {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0/dsa_helpers.egg-info}/PKG-INFO +1 -1
  9. {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/pyproject.toml +1 -1
  10. dsa_helpers-1.1.5.dev9/dsa_helpers/ml/segformer_semantic_segmentation/inference.py +0 -123
  11. {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/.gitignore +0 -0
  12. {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/.readthedocs.yaml +0 -0
  13. {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/LICENSE +0 -0
  14. {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/README.md +0 -0
  15. {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/app.py +0 -0
  16. {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dev.py +0 -0
  17. {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/docs/requirements.txt +0 -0
  18. {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/docs/source/conf.py +0 -0
  19. {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/docs/source/index.rst +0 -0
  20. {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/cli_utils.py +0 -0
  21. {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/dash/__init__.py +0 -0
  22. {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/dash/dash_paperdragon_utils.py +0 -0
  23. {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/dash/header.py +0 -0
  24. {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/dash/header_callbacks.py +0 -0
  25. {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/gpd_utils.py +0 -0
  26. {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/imread.py +0 -0
  27. {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/imwrite.py +0 -0
  28. {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/ml/__init__.py +0 -0
  29. {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/ml/callbacks.py +0 -0
  30. {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/ml/datasets/SegFormerSegmentationDataset.py +0 -0
  31. {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/ml/datasets/__init__.py +0 -0
  32. {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/ml/datasets/utils.py +0 -0
  33. {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/ml/evaluate.py +0 -0
  34. {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/ml/metrics.py +0 -0
  35. {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/ml/object_detection/__init__.py +0 -0
  36. {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/ml/object_detection/get_pearce_roi_images.py +0 -0
  37. {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/ml/object_detection/tile_image.py +0 -0
  38. {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/ml/object_detection/utils.py +0 -0
  39. {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/ml/segformer_semantic_segmentation/__init__.py +0 -0
  40. {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/ml/segformer_semantic_segmentation/datasets.py +0 -0
  41. {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/ml/segformer_semantic_segmentation/evaluate.py +0 -0
  42. {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/ml/segformer_semantic_segmentation/transforms.py +0 -0
  43. {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/ml/segformer_semantic_segmentation/utils.py +0 -0
  44. {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/ml/transforms/__init__.py +0 -0
  45. {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/ml/transforms/segformer_transforms.py +0 -0
  46. {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/mongo_utils.py +0 -0
  47. {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/tile_utils.py +0 -0
  48. {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers/utils.py +0 -0
  49. {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers.egg-info/SOURCES.txt +0 -0
  50. {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers.egg-info/dependency_links.txt +0 -0
  51. {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers.egg-info/requires.txt +0 -0
  52. {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/dsa_helpers.egg-info/top_level.txt +0 -0
  53. {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/setup.cfg +0 -0
  54. {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/tests/app/Dockerfile +0 -0
  55. {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/tests/app/app.py +0 -0
  56. {dsa_helpers-1.1.5.dev9 → dsa_helpers-1.1.6.dev0}/tests/docker-compose.yaml +0 -0
  57. {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.5.dev9
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>
@@ -3,7 +3,7 @@ from .imread import imread
3
3
  from .imwrite import imwrite
4
4
 
5
5
  # Version of the dsa-helpers package
6
- __version__ = "1.1.5.dev9"
6
+ __version__ = "1.1.6.dev0"
7
7
 
8
8
  # To avoid slow downs, do not allow from dsa_helpers import * to import anything.
9
9
  __all__ = []
@@ -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 _, row in gdf.iterrows():
544
- # Flatten multipolygon to list of polygons.
545
- list_of_polygons = list(
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
- if len(exterior_poly) > max_points:
557
- poly = poly.simplify(tolerance, preserve_topology=True)
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
- points = [
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
- holes = []
552
+ if len(points) == 0:
553
+ continue
565
554
 
566
- for interior_poly in interior_polys:
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
- element = {
573
- "points": points,
574
- "fillColor": idx_config[label]["fillColor"],
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
- if len(holes):
584
- element["holes"] = holes
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
- elements.append(element)
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, int2rgb: dict, alpha: bool = False
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
- val_dataset = create_segformer_segmentation_dataset(
132
- (
133
- val_data
134
- if max_val_size is None
135
- else val_data.sample(n=max_val_size, random_state=random_state)
136
- ),
137
- transforms=val_transforms,
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
- background_value,
36
- background_index,
35
+ edge_value,
36
+ background_id,
37
37
  edge_thr,
38
38
  ignore_existing,
39
- ignore_labels,
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=background_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=background_index
99
+ tile_gdf, (h, w), default_value=background_id
100
100
  ).copy()
101
101
 
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
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
- background_value: tuple[int, int, int] | int = (255, 255, 255),
125
- background_index: int = 0,
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
- ignore_index: int = 255,
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
- ignore_labels = [label2id[label] for label in ignore_labels]
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
- 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", ""))
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 and idx column, which is the map of the group to the label2id.
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
- background_value,
272
- background_index,
274
+ edge_value,
275
+ background_id,
273
276
  edge_thr,
274
277
  ignore_existing,
275
- ignore_labels,
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.5.dev9
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>
@@ -38,7 +38,7 @@ classifiers = [
38
38
  "License :: OSI Approved :: MIT License",
39
39
  ]
40
40
  keywords = ["dsa", "girder", "girder_client"]
41
- version = "v1.1.5.dev9"
41
+ version = "v1.1.6.dev0"
42
42
 
43
43
  [project.urls]
44
44
  Repository = "https://github.com/Gutman-Lab/dsa-python-utils"
@@ -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