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.
Files changed (51) hide show
  1. {dsa_helpers-1.1.3.dev1/dsa_helpers.egg-info → dsa_helpers-1.1.4.dev0}/PKG-INFO +2 -1
  2. {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers/__init__.py +1 -1
  3. {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers/gpd_utils.py +59 -10
  4. {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers/tiling.py +125 -119
  5. {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0/dsa_helpers.egg-info}/PKG-INFO +2 -1
  6. {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers.egg-info/requires.txt +1 -0
  7. {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/pyproject.toml +3 -2
  8. {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/.gitignore +0 -0
  9. {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/.readthedocs.yaml +0 -0
  10. {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/LICENSE +0 -0
  11. {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/README.md +0 -0
  12. {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/app.py +0 -0
  13. {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dev.py +0 -0
  14. {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/docs/requirements.txt +0 -0
  15. {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/docs/source/conf.py +0 -0
  16. {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/docs/source/index.rst +0 -0
  17. {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers/cli_utils.py +0 -0
  18. {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers/dash/__init__.py +0 -0
  19. {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers/dash/dash_paperdragon_utils.py +0 -0
  20. {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers/dash/header.py +0 -0
  21. {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers/dash/header_callbacks.py +0 -0
  22. {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers/girder_utils.py +0 -0
  23. {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers/image_utils.py +0 -0
  24. {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers/imread.py +0 -0
  25. {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers/imwrite.py +0 -0
  26. {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers/ml/__init__.py +0 -0
  27. {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers/ml/callbacks.py +0 -0
  28. {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers/ml/datasets/SegFormerSegmentationDataset.py +0 -0
  29. {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers/ml/datasets/__init__.py +0 -0
  30. {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers/ml/datasets/utils.py +0 -0
  31. {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers/ml/evaluate.py +0 -0
  32. {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers/ml/metrics.py +0 -0
  33. {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers/ml/object_detection/__init__.py +0 -0
  34. {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers/ml/object_detection/get_pearce_roi_images.py +0 -0
  35. {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers/ml/object_detection/tile_image.py +0 -0
  36. {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers/ml/object_detection/utils.py +0 -0
  37. {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers/ml/segformer_semantic_segmentation/__init__.py +0 -0
  38. {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers/ml/segformer_semantic_segmentation/inference.py +0 -0
  39. {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers/ml/transforms/__init__.py +0 -0
  40. {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers/ml/transforms/segformer_transforms.py +0 -0
  41. {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers/mongo_utils.py +0 -0
  42. {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers/tile_utils.py +0 -0
  43. {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers/utils.py +0 -0
  44. {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers.egg-info/SOURCES.txt +0 -0
  45. {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers.egg-info/dependency_links.txt +0 -0
  46. {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/dsa_helpers.egg-info/top_level.txt +0 -0
  47. {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/setup.cfg +0 -0
  48. {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/tests/app/Dockerfile +0 -0
  49. {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/tests/app/app.py +0 -0
  50. {dsa_helpers-1.1.3.dev1 → dsa_helpers-1.1.4.dev0}/tests/docker-compose.yaml +0 -0
  51. {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.dev1
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
@@ -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.3.dev1"
6
+ __version__ = "1.1.4.dev0"
7
7
 
8
8
  # To avoid slow downs, do not allow from dsa_helpers import * to import anything.
9
9
  __all__ = []
@@ -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
- """Plot a geopandas dataframe.
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["label"].map(label2color)
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
- # # Create the plot
88
- ax = plt.subplots(figsize=figsize)[1]
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
- plt.title(f"Total points: {total_points:,}")
109
- plt.show()
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
- This module contains functions used to tile large images, usually for
5
- use in developing deep learning models.
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
- background_idx,
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
- # Calculate the x and y in tile magnification.
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
- tile_img = ts.getRegion(
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 = tile_img.shape[:2]
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
- tile_img = cv.copyMakeBorder(
68
- tile_img,
77
+ img = cv.copyMakeBorder(
78
+ img,
69
79
  0,
70
- tile_size - tile_img.shape[0],
80
+ tile_size - h,
71
81
  0,
72
- tile_size - tile_img.shape[1],
82
+ tile_size - w,
73
83
  cv.BORDER_CONSTANT,
74
- value=background_idx,
84
+ value=background_value,
75
85
  )
76
86
 
77
- # Create a blank tile mask.
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
- for hole in holes:
96
- if len(hole):
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
- # Use points to draw contours as positive.
100
- label_mask = cv.drawContours(label_mask, points, -1, 1, cv.FILLED)
101
-
102
- # Draw in the holes as background class zero.
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
- # Apply the label to tile mask where label mask is positive.
106
- tile_mask[label_mask == 1] = idx
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
- # Save the image and mask.
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
- imwrite(img_fp, tile_img)
111
- imwrite(mask_fp, tile_mask, grayscale=True)
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
- annotation_docs: list[dict],
119
- label2idx: dict,
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
- background_idx: int = 0,
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
- annotation_docs (list[dict]): list of annotation documents.
138
- label2idx (dict): mapping of label names to integer indices.
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
- background_idx (int, optional): index of the background class.
153
- Will be used when padding edges of WSI. Defaults to 0.
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
- # Get the tile source of WSI.
168
- ts = large_image.getTileSource(str(wsi_fp))
185
+ if notebook_tqdm:
186
+ from tqdm.notebook import tqdm
187
+ else:
188
+ from tqdm import tqdm
169
189
 
170
- # Get the metadata of the WSI.
171
- metadata = ts.getMetadata()
190
+ if isinstance(ignore_labels, str):
191
+ ignore_labels = [ignore_labels]
172
192
 
173
- # Calculate variables based on the desired magnification.
174
- wsi_mag = metadata["magnification"]
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
- # Calculate the tile size at full resolution (1 if mag is None or the same as scan mag).
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 = wsi_mag
184
- sf = 1
212
+ mag = scan_mag
213
+ scan_to_mag_sf = 1
185
214
  else:
186
- sf = mag / wsi_mag # scan mag -> desired mag
187
- scan_tile_size = int(tile_size / sf)
188
- scan_stride = int(stride / sf)
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
- for ann_doc in annotation_docs:
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
- for element in elements:
198
- # Check if element has a label.
199
- label = element.get("label", {}).get("value")
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
- if label not in label2idx:
202
- # Skip this element if it is not in the label2idx mapping.
203
- continue
225
+ # Filter to polygons in label2id.
226
+ gdf = gdf[gdf[label_col].isin(label2id.keys())]
204
227
 
205
- idx = label2idx[label]
228
+ # Remove rows with geometry is not a Polygon.
229
+ gdf = gdf[gdf["geometry"].type == "Polygon"]
206
230
 
207
- element_type = element["type"]
208
-
209
- if element_type == "polyline":
210
- points = element.get("points", [])
211
-
212
- if len(points):
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
- points = np.array(
231
- [[x1, y1], [x2, y1], [x2, y2], [x1, y2]], dtype=int
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
- # Scale by the factor.
235
- contours[idx]["points"].append(points)
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
- contours,
266
- sf,
268
+ scan_to_mag_sf,
267
269
  img_dir,
268
270
  mask_dir,
269
- background_idx,
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
- return tile_info
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.dev1
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
@@ -14,3 +14,4 @@ transformers[torch]
14
14
  datasets
15
15
  torchvision
16
16
  large-image
17
+ rasterio
@@ -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.3.dev1"
40
+ version = "v1.1.4.dev0"
40
41
 
41
42
  [project.urls]
42
43
  Repository = "https://github.com/Gutman-Lab/dsa-python-utils"