Skip to content

Commit a371d27

Browse files
committed
add strided patches
fixes #202 This adds a command line option '--patch-overlap-ratio' that controls the level of overlap between adjacent patches. Negative values create space between patches, and values closer to 1 makes patches overlap more.
1 parent 95e3b06 commit a371d27

File tree

3 files changed

+35
-5
lines changed

3 files changed

+35
-5
lines changed

wsinfer/cli/infer.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,16 @@ def get_stdout(args: list[str]) -> str:
303303
" area, it is filled with foreground. The default is 190um x 190um. The units of"
304304
" this argument are microns squared.",
305305
)
306+
@click.option(
307+
"--patch-overlap-ratio",
308+
default=0.0,
309+
type=click.FloatRange(min=None, max=1, max_open=True),
310+
help="The ratio of overlap among patches. The default value of 0 produces"
311+
" non-overlapping patches. A value in (0, 1) will produce overlapping patches."
312+
" Negative values will add space between patches. A value of -1 would skip"
313+
" every other patch. A value of 0.5 will provide 50%% of overlap between patches."
314+
" Values must be in (-inf, 1).",
315+
)
306316
def run(
307317
ctx: click.Context,
308318
*,
@@ -321,6 +331,7 @@ def run(
321331
seg_closing_kernel_size: int,
322332
seg_min_object_size_um2: float,
323333
seg_min_hole_size_um2: float,
334+
patch_overlap_ratio: float = 0.0,
324335
) -> None:
325336
"""Run model inference on a directory of whole slide images.
326337
@@ -398,6 +409,7 @@ def run(
398409
closing_kernel_size=seg_closing_kernel_size,
399410
min_object_size_um2=seg_min_object_size_um2,
400411
min_hole_size_um2=seg_min_hole_size_um2,
412+
overlap=patch_overlap_ratio,
401413
)
402414

403415
if not results_dir.joinpath("patches").exists():

wsinfer/patchlib/__init__.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from ..wsi import _validate_wsi_directory
1515
from ..wsi import get_avg_mpp
1616
from .patch import get_multipolygon_from_binary_arr
17-
from .patch import get_nonoverlapping_patch_coordinates_within_polygon
17+
from .patch import get_patch_coordinates_within_polygon
1818
from .segment import segment_tissue
1919

2020
logger = logging.getLogger(__name__)
@@ -34,6 +34,7 @@ def segment_and_patch_one_slide(
3434
closing_kernel_size: int = 6,
3535
min_object_size_um2: float = 200**2,
3636
min_hole_size_um2: float = 190**2,
37+
overlap: float = 0.0,
3738
) -> None:
3839
"""Get non-overlapping patch coordinates in tissue regions of a whole slide image.
3940
@@ -171,12 +172,13 @@ def segment_and_patch_one_slide(
171172
half_patch_size = round(patch_size / 2)
172173

173174
# Nx4 --> N x (minx, miny, width, height)
174-
coords = get_nonoverlapping_patch_coordinates_within_polygon(
175+
coords = get_patch_coordinates_within_polygon(
175176
slide_width=slide_width,
176177
slide_height=slide_height,
177178
patch_size=patch_size,
178179
half_patch_size=half_patch_size,
179180
polygon=polygon,
181+
overlap=overlap,
180182
)
181183
logger.info(f"Found {len(coords)} patches within tissue")
182184

@@ -299,6 +301,7 @@ def segment_and_patch_directory_of_slides(
299301
closing_kernel_size: int = 6,
300302
min_object_size_um2: float = 200**2,
301303
min_hole_size_um2: float = 190**2,
304+
overlap: float = 0.0,
302305
) -> None:
303306
"""Get non-overlapping patch coordinates in tissue regions for a directory of whole
304307
slide images.
@@ -373,6 +376,7 @@ def segment_and_patch_directory_of_slides(
373376
closing_kernel_size=closing_kernel_size,
374377
min_object_size_um2=min_object_size_um2,
375378
min_hole_size_um2=min_hole_size_um2,
379+
overlap=overlap,
376380
)
377381
except Exception as e:
378382
logger.error(f"Failed to segment and patch slide\n{slide_path}", exc_info=e)

wsinfer/patchlib/patch.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,12 +128,13 @@ def merge_polygons(polygon: MultiPolygon, idx: int, add: bool) -> MultiPolygon:
128128
return polygon, contours_unscaled, hierarchy[np.newaxis]
129129

130130

131-
def get_nonoverlapping_patch_coordinates_within_polygon(
131+
def get_patch_coordinates_within_polygon(
132132
slide_width: int,
133133
slide_height: int,
134134
patch_size: int,
135135
half_patch_size: int,
136136
polygon: Polygon,
137+
overlap: float = 0.0,
137138
) -> npt.NDArray[np.int_]:
138139
"""Get coordinates of patches within a polygon.
139140
@@ -149,6 +150,12 @@ def get_nonoverlapping_patch_coordinates_within_polygon(
149150
Half of the length of a patch in pixels.
150151
polygon : Polygon
151152
A shapely Polygon representing the presence of tissue.
153+
overlap : float
154+
The proportion of the patch_size to overlap. A value of 0.5
155+
would have an overlap of 50%. A value of 0.2 would have an
156+
overlap of 20%. Negative values will add space between patches.
157+
A value of -1 would skip every other patch. Value must be in (-inf, 1).
158+
The default value of 0.0 produces non-overlapping patches.
152159
153160
Returns
154161
-------
@@ -157,12 +164,19 @@ def get_nonoverlapping_patch_coordinates_within_polygon(
157164
contains the coordinates of the top-left of a tile: (minx, miny).
158165
"""
159166

167+
if overlap >= 1:
168+
raise ValueError(f"overlap must be in (-inf, 1) but got {overlap}")
169+
170+
# Handle potentially overlapping slides.
171+
step_size = round((1 - overlap) * patch_size)
172+
logger.info(f"Patches are {patch_size} px, with step size of {step_size} px.")
173+
160174
# Make an array of Nx2, where each row is (x, y) centroid of the patch.
161175
tile_centroids_arr: npt.NDArray[np.int_] = np.array(
162176
list(
163177
itertools.product(
164-
range(0 + half_patch_size, slide_width, patch_size),
165-
range(0 + half_patch_size, slide_height, patch_size),
178+
range(0 + half_patch_size, slide_width, step_size),
179+
range(0 + half_patch_size, slide_height, step_size),
166180
)
167181
)
168182
)

0 commit comments

Comments
 (0)