Spaces:
Sleeping
Sleeping
| from dataclasses import asdict | |
| from typing import Dict, Iterator, List, Tuple | |
| import numpy as np | |
| from PIL import Image | |
| from .config import TileConfig | |
| from .utils import pil_to_np, np_to_pil | |
| def _tissue_mask_rgb(img: Image.Image, min_tissue_frac: float) -> np.ndarray: | |
| """ | |
| Very lightweight tissue detection surrogate using HSV thresholds. | |
| Returns a boolean mask [H,W] where True ≈ tissue. | |
| """ | |
| import cv2 | |
| rgb = pil_to_np(img) # uint8 [H,W,3] | |
| bgr = cv2.cvtColor(rgb, cv2.COLOR_RGB2BGR) | |
| hsv = cv2.cvtColor(bgr, cv2.COLOR_BGR2HSV) | |
| # Heuristic: keep pixels with moderate-to-high saturation/value. | |
| sat_ok = hsv[..., 1] > 20 | |
| val_ok = hsv[..., 2] > 30 | |
| mask = sat_ok & val_ok | |
| # Morphological open/close to clean noise | |
| kernel = np.ones((3, 3), np.uint8) | |
| mask = cv2.morphologyEx(mask.astype(np.uint8) * 255, cv2.MORPH_OPEN, kernel) | |
| mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) | |
| return mask.astype(bool) | |
| def _tile_coords(h: int, w: int, tile: int, stride: int) -> Iterator[Tuple[int, int, int, int]]: | |
| """ | |
| Yield (y0, y1, x0, x1) windows. Handles right/bottom edges by clipping. | |
| """ | |
| for y in range(0, max(1, h - tile + 1), stride): | |
| for x in range(0, max(1, w - tile + 1), stride): | |
| y1 = min(y + tile, h) | |
| x1 = min(x + tile, w) | |
| # enforce exact size by skipping partial tiles unless at edges with enough size | |
| if (y1 - y == tile) and (x1 - x == tile): | |
| yield (y, y1, x, x1) | |
| class Tiler: | |
| """ | |
| Slide/large-image tiler: | |
| - computes a simple tissue mask | |
| - yields tiles and their grid coordinates | |
| - filters low-tissue tiles by fraction threshold | |
| """ | |
| def __init__(self, cfg: TileConfig): | |
| self.cfg = cfg | |
| def describe(self) -> Dict: | |
| return asdict(self.cfg) | |
| def iter_tiles( | |
| self, img: Image.Image | |
| ) -> Iterator[Tuple[int, int, int, int, Image.Image, float]]: | |
| """ | |
| Yields: (y0, y1, x0, x1, tile_rgb_PIL, tissue_frac) | |
| Only tiles with tissue_frac >= min_tissue_frac are yielded. | |
| """ | |
| tile = self.cfg.tile_size | |
| stride = self.cfg.stride | |
| mask = _tissue_mask_rgb(img, self.cfg.min_tissue_frac) | |
| arr = pil_to_np(img) | |
| H, W = arr.shape[:2] | |
| for y0, y1, x0, x1 in _tile_coords(H, W, tile, stride): | |
| m = mask[y0:y1, x0:x1] | |
| tissue_frac = float(m.mean()) if m.size else 0.0 | |
| if tissue_frac < self.cfg.min_tissue_frac: | |
| continue | |
| crop = arr[y0:y1, x0:x1, :] | |
| yield (y0, y1, x0, x1, np_to_pil(crop), tissue_frac) | |
| def tile_image( | |
| self, img: Image.Image | |
| ) -> List[Dict[str, object]]: | |
| """ | |
| Convenience: materialize tiles into a list of dicts for caching. | |
| Each dict: {"y0": int, "y1": int, "x0": int, "x1": int, "tissue_frac": float, "image": PIL.Image} | |
| """ | |
| out: List[Dict[str, object]] = [] | |
| for y0, y1, x0, x1, tile_img, frac in self.iter_tiles(img): | |
| out.append( | |
| {"y0": y0, "y1": y1, "x0": x0, "x1": x1, "tissue_frac": frac, "image": tile_img} | |
| ) | |
| return out | |
| def grid_shape(self, img: Image.Image) -> Tuple[int, int]: | |
| """ | |
| Return approximate (rows, cols) of the tiling grid for visualization reference. | |
| """ | |
| H, W = pil_to_np(img).shape[:2] | |
| rows = max(1, (H - self.cfg.tile_size) // self.cfg.stride + 1) | |
| cols = max(1, (W - self.cfg.tile_size) // self.cfg.stride + 1) | |
| return rows, cols |