Histopathology / app /preprocessing.py
Eyob-Sol's picture
Upload 29 files
5a274ee verified
Raw
History Blame Contribute Delete
3.6 kB
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