Tumor-Trace / utils.py
Vrajesharma's picture
Upload 9 files
5c75522 verified
Raw
History Blame Contribute Delete
7.45 kB
import base64
import io
from typing import Tuple
import numpy as np
from PIL import Image
import torch
import torch.nn.functional as F
from model import DEVICE, get_unet
# ---------- Image loading / preprocessing ----------
def load_image_from_bytes(data: bytes,
target_size: Tuple[int, int] = (240, 240)) -> np.ndarray:
"""
Load uploaded image (PNG/JPEG), resize to 240x240.
Returns float32 (H, W) in original pixel range (0-255).
"""
img = Image.open(io.BytesIO(data)).convert("L") # grayscale
img = img.resize(target_size, resample=Image.BILINEAR)
arr = np.array(img).astype(np.float32) # (H, W), 0..255
return arr
def build_4channel_input(arr: np.ndarray) -> np.ndarray:
"""
Model expects (H, W, 4) channels.
We synthesize 4 'pseudo-modalities' from a single grayscale slice:
ch0: z-score of original
ch1: z-score of gamma 0.5 (brightens)
ch2: z-score of gamma 2.0 (darkens)
ch3: z-score of local min-max (CLAHE-like).[file:1]
"""
def zscore(x):
m, s = x.mean(), x.std() + 1e-8
return (x - m) / s
x = arr / 255.0 # [0, 1]
ch0 = zscore(x)
ch1 = zscore(np.power(np.clip(x, 0, 1), 0.5))
ch2 = zscore(np.power(np.clip(x, 0, 1), 2.0))
# local min-max in 8x8 patches
x_local = x.copy()
h, w = x_local.shape
bs = 8
for i in range(0, h, bs):
for j in range(0, w, bs):
blk = x_local[i:i + bs, j:j + bs]
mn, mx = blk.min(), blk.max()
x_local[i:i + bs, j:j + bs] = (blk - mn) / (mx - mn + 1e-8)
ch3 = zscore(x_local)
img4 = np.stack([ch0, ch1, ch2, ch3], axis=-1) # (H, W, 4)
return img4.astype(np.float32)
# ---------- Heuristic survival from mask (new model) ----------
SURV_LABELS = {
0: "Short Survival",
1: "Mid Survival",
2: "Long Survival",
}
def heuristic_survival_from_masks(pred_masks: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
"""
pred_masks: (B, H, W) tensor with classes {0 BG, 1 NCR/NET, 2 Edema, 3 ET}.[file:1]
Returns:
surv_classes: (B,) long tensor in {0,1,2}
et_fracs : (B,) float tensor of ET fraction per slice
"""
batch_size = pred_masks.shape[0]
surv_classes = []
et_fracs = []
for i in range(batch_size):
mask = pred_masks[i]
total_pixels = mask.numel()
et_pixels = (mask == 3).sum().float()
edema_pixels = (mask == 2).sum().float()
ncr_pixels = (mask == 1).sum().float()
et_frac = et_pixels / total_pixels
tumor_frac = (et_pixels + edema_pixels + ncr_pixels) / total_pixels
# Heuristic from notebook: larger ET / tumour burden → poorer survival.[file:1]
if (et_frac >= 0.03) or (tumor_frac >= 0.12):
surv_class = 0 # Short Survival
elif (et_frac >= 0.01) or (tumor_frac >= 0.05):
surv_class = 1 # Mid Survival
else:
surv_class = 2 # Long Survival
surv_classes.append(surv_class)
et_fracs.append(et_frac)
surv_classes = torch.tensor(surv_classes, dtype=torch.long, device=pred_masks.device)
et_fracs = torch.stack(et_fracs).to(pred_masks.device)
return surv_classes, et_fracs
# ---------- Core inference: segmentation + survival ----------
def run_segmentation_and_survival(arr: np.ndarray,
patient_age: float):
"""
arr: (H, W) float32 in 0..255
patient_age: kept for API compatibility but not used by the heuristic.[file:1]
Returns
-------
mask : (H, W) uint8, labels 0..3
conf_map : (H, W) float32, max softmax prob per pixel
surv_probs : (3,) float32, one-hot probabilities [short, medium, long]
surv_class : scalar int, argmax of surv_probs
"""
unet = get_unet()
# ---- segmentation ----
img4 = build_4channel_input(arr) # (H, W, 4)
x = np.transpose(img4, (2, 0, 1)) # (4, H, W)
x = torch.from_numpy(x).unsqueeze(0).to(DEVICE) # (1, 4, H, W)
with torch.no_grad(), torch.amp.autocast(
"cuda", enabled=(DEVICE.type == "cuda")
):
logits = unet(x) # (1, 4, H, W)
probs = torch.softmax(logits, dim=1)
preds = torch.argmax(probs, dim=1) # (1, H, W)
conf = probs.max(dim=1)[0] # (1, H, W) max prob
# numpy outputs for downstream
mask = preds[0].cpu().numpy().astype(np.uint8)
conf_map = conf[0].cpu().numpy().astype(np.float32)
# ---- heuristic survival from predicted mask ----
surv_classes, et_fracs = heuristic_survival_from_masks(preds)
surv_idx = int(surv_classes[0].item())
# we don't have calibrated probabilities; return one-hot
surv_probs = np.zeros(3, dtype=np.float32)
surv_probs[surv_idx] = 1.0
return mask, conf_map, surv_probs, surv_idx
# ---------- Visualisation helpers ----------
def colorize_mask(mask: np.ndarray) -> Image.Image:
"""
mask: (H, W) labels 0..3
Colors:
1: Necrotic core (NCR/NET) -> yellow
2: Edema -> green
3: Effective Tumor (ET) -> red
"""
h, w = mask.shape
overlay = np.zeros((h, w, 4), dtype=np.uint8)
colors = {
1: (255, 255, 0, 180), # class 1: necrotic core - yellow
2: ( 0, 255, 0, 180), # class 2: edema - green
3: (255, 0, 0, 200), # class 3: Effective Tumor (ET) - red
}
for cls, (r, g, b, a) in colors.items():
overlay[mask == cls] = np.array([r, g, b, a], dtype=np.uint8)
return Image.fromarray(overlay, mode="RGBA")
def blend_overlay(arr: np.ndarray, overlay_rgba: Image.Image) -> Image.Image:
"""
arr: (H, W) float32, any range (auto-rescaled to 0..255)
overlay_rgba: RGBA PIL Image
Returns: RGB PIL Image — grayscale MRI with colored tumor overlay.
"""
mn, mx = arr.min(), arr.max()
display = ((arr - mn) / (mx - mn + 1e-8) * 255.0).astype(np.uint8)
base_img = Image.fromarray(display, mode="L").convert("RGBA")
blended = Image.alpha_composite(base_img, overlay_rgba)
return blended.convert("RGB")
def pil_to_base64(img: Image.Image) -> str:
buff = io.BytesIO()
img.save(buff, format="PNG")
return "data:image/png;base64," + base64.b64encode(buff.getvalue()).decode("utf-8")
# ---------- Stats ----------
def get_tumor_stats(mask: np.ndarray, conf_map: np.ndarray) -> dict:
total = mask.size
tumor_pixels = int((mask > 0).sum())
tumor_pct = round(tumor_pixels / total * 100.0, 2)
class_counts = {
"necrotic_core": int((mask == 1).sum()),
"edema": int((mask == 2).sum()),
"effective_tumor": int((mask == 3).sum()), # was "et" / "enhancing_tumor"
}
if tumor_pixels > 0:
mean_conf = float(conf_map[mask > 0].mean())
else:
mean_conf = 0.0
return {
"tumor_detected": tumor_pixels > 0,
"tumor_pixel_count": tumor_pixels,
"tumor_area_pct": tumor_pct,
"class_counts": class_counts,
"mean_tumor_confidence": mean_conf,
}