Skip to content

Commit

Permalink
add strided patches
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
kaczmarj committed Feb 22, 2024
1 parent 95e3b06 commit a371d27
Show file tree
Hide file tree
Showing 3 changed files with 35 additions and 5 deletions.
12 changes: 12 additions & 0 deletions wsinfer/cli/infer.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,16 @@ def get_stdout(args: list[str]) -> str:
" area, it is filled with foreground. The default is 190um x 190um. The units of"
" this argument are microns squared.",
)
@click.option(
"--patch-overlap-ratio",
default=0.0,
type=click.FloatRange(min=None, max=1, max_open=True),
help="The ratio of overlap among patches. The default value of 0 produces"
" non-overlapping patches. A value in (0, 1) will produce overlapping patches."
" Negative values will add space between patches. A value of -1 would skip"
" every other patch. A value of 0.5 will provide 50%% of overlap between patches."
" Values must be in (-inf, 1).",
)
def run(
ctx: click.Context,
*,
Expand All @@ -321,6 +331,7 @@ def run(
seg_closing_kernel_size: int,
seg_min_object_size_um2: float,
seg_min_hole_size_um2: float,
patch_overlap_ratio: float = 0.0,
) -> None:
"""Run model inference on a directory of whole slide images.
Expand Down Expand Up @@ -398,6 +409,7 @@ def run(
closing_kernel_size=seg_closing_kernel_size,
min_object_size_um2=seg_min_object_size_um2,
min_hole_size_um2=seg_min_hole_size_um2,
overlap=patch_overlap_ratio,
)

if not results_dir.joinpath("patches").exists():
Expand Down
8 changes: 6 additions & 2 deletions wsinfer/patchlib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from ..wsi import _validate_wsi_directory
from ..wsi import get_avg_mpp
from .patch import get_multipolygon_from_binary_arr
from .patch import get_nonoverlapping_patch_coordinates_within_polygon
from .patch import get_patch_coordinates_within_polygon
from .segment import segment_tissue

logger = logging.getLogger(__name__)
Expand All @@ -34,6 +34,7 @@ def segment_and_patch_one_slide(
closing_kernel_size: int = 6,
min_object_size_um2: float = 200**2,
min_hole_size_um2: float = 190**2,
overlap: float = 0.0,
) -> None:
"""Get non-overlapping patch coordinates in tissue regions of a whole slide image.
Expand Down Expand Up @@ -171,12 +172,13 @@ def segment_and_patch_one_slide(
half_patch_size = round(patch_size / 2)

# Nx4 --> N x (minx, miny, width, height)
coords = get_nonoverlapping_patch_coordinates_within_polygon(
coords = get_patch_coordinates_within_polygon(
slide_width=slide_width,
slide_height=slide_height,
patch_size=patch_size,
half_patch_size=half_patch_size,
polygon=polygon,
overlap=overlap,
)
logger.info(f"Found {len(coords)} patches within tissue")

Expand Down Expand Up @@ -299,6 +301,7 @@ def segment_and_patch_directory_of_slides(
closing_kernel_size: int = 6,
min_object_size_um2: float = 200**2,
min_hole_size_um2: float = 190**2,
overlap: float = 0.0,
) -> None:
"""Get non-overlapping patch coordinates in tissue regions for a directory of whole
slide images.
Expand Down Expand Up @@ -373,6 +376,7 @@ def segment_and_patch_directory_of_slides(
closing_kernel_size=closing_kernel_size,
min_object_size_um2=min_object_size_um2,
min_hole_size_um2=min_hole_size_um2,
overlap=overlap,
)
except Exception as e:
logger.error(f"Failed to segment and patch slide\n{slide_path}", exc_info=e)
Expand Down
20 changes: 17 additions & 3 deletions wsinfer/patchlib/patch.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,12 +128,13 @@ def merge_polygons(polygon: MultiPolygon, idx: int, add: bool) -> MultiPolygon:
return polygon, contours_unscaled, hierarchy[np.newaxis]


def get_nonoverlapping_patch_coordinates_within_polygon(
def get_patch_coordinates_within_polygon(
slide_width: int,
slide_height: int,
patch_size: int,
half_patch_size: int,
polygon: Polygon,
overlap: float = 0.0,
) -> npt.NDArray[np.int_]:
"""Get coordinates of patches within a polygon.
Expand All @@ -149,6 +150,12 @@ def get_nonoverlapping_patch_coordinates_within_polygon(
Half of the length of a patch in pixels.
polygon : Polygon
A shapely Polygon representing the presence of tissue.
overlap : float
The proportion of the patch_size to overlap. A value of 0.5
would have an overlap of 50%. A value of 0.2 would have an
overlap of 20%. Negative values will add space between patches.
A value of -1 would skip every other patch. Value must be in (-inf, 1).
The default value of 0.0 produces non-overlapping patches.
Returns
-------
Expand All @@ -157,12 +164,19 @@ def get_nonoverlapping_patch_coordinates_within_polygon(
contains the coordinates of the top-left of a tile: (minx, miny).
"""

if overlap >= 1:
raise ValueError(f"overlap must be in (-inf, 1) but got {overlap}")

# Handle potentially overlapping slides.
step_size = round((1 - overlap) * patch_size)
logger.info(f"Patches are {patch_size} px, with step size of {step_size} px.")

# Make an array of Nx2, where each row is (x, y) centroid of the patch.
tile_centroids_arr: npt.NDArray[np.int_] = np.array(
list(
itertools.product(
range(0 + half_patch_size, slide_width, patch_size),
range(0 + half_patch_size, slide_height, patch_size),
range(0 + half_patch_size, slide_width, step_size),
range(0 + half_patch_size, slide_height, step_size),
)
)
)
Expand Down

0 comments on commit a371d27

Please sign in to comment.