HarshithReddy01/srmamamba-liver-segmentation
1
1import os2import numpy as np3import torch4import nibabel as nib5from monai import transforms6from scipy import ndimage7from scipy.ndimage import binary_closing, binary_opening, binary_fill_holes, median_filter8from skimage.morphology import ball9from datetime import datetime10 11def load_nifti_ras(file_path):12 """13 Load NIfTI and reorient to RAS so orientation matches preprocess_nifti output.14 Returns (data_ras, affine_ras, spacing) for use as original_data and saving.15 """16 ras_transform = transforms.Compose([17 transforms.LoadImaged(keys=["image"]),18 transforms.Orientationd(keys=["image"], axcodes="RAS"),19 ])20 data = {"image": file_path}21 out = ras_transform(data)22 img = out["image"]23 if hasattr(img, "numpy"):24 data_ras = img.numpy().astype(np.float32)25 elif hasattr(img, "cpu"):26 data_ras = img.cpu().numpy().astype(np.float32)27 else:28 data_ras = np.asarray(img, dtype=np.float32)29 affine_ras = None30 spacing = (1.0, 1.0, 1.0)31 if hasattr(img, "meta") and img.meta is not None:32 affine_ras = img.meta.get("affine")33 if affine_ras is not None:34 affine_ras = np.array(affine_ras)35 pixdim = img.meta.get("pixdim")36 if pixdim is not None and len(pixdim) >= 4:37 spacing = (float(pixdim[1]), float(pixdim[2]), float(pixdim[3]))38 if affine_ras is None:39 nifti_img = nib.load(file_path)40 ras_nifti = nib.as_closest_canonical(nifti_img)41 affine_ras = np.array(ras_nifti.affine)42 spacing = ras_nifti.header.get_zooms()[:3]43 if len(spacing) < 3:44 spacing = (1.0, 1.0, 1.0)45 # Ensure 3D (D, H, W) to match model output spatial shape46 if data_ras.ndim == 4:47 data_ras = data_ras.squeeze(0)48 return data_ras, affine_ras, spacing49 50 51def validate_nifti(nifti_img):52 shape = nifti_img.shape53 if len(shape) < 3:54 raise ValueError(f"Invalid NIfTI shape: {shape}. Expected at least 3 dimensions.")55 if any(s <= 0 for s in shape):56 raise ValueError(f"Invalid NIfTI shape: {shape}. All dimensions must be positive.")57 if any(s > 2000 for s in shape):58 raise ValueError(f"Volume too large: {shape}. Maximum dimension size is 2000.")59 60 voxel_spacing = nifti_img.header.get_zooms()[:3] if len(nifti_img.header.get_zooms()) >= 3 else (1.0, 1.0, 1.0)61 if any(sp <= 0 for sp in voxel_spacing):62 raise ValueError(f"Invalid voxel spacing: {voxel_spacing}. All values must be positive.")63 64 raw_data = nifti_img.get_fdata()65 if np.isnan(raw_data).any():66 raise ValueError("NIfTI contains NaN values")67 if np.isinf(raw_data).any():68 raise ValueError("NIfTI contains Inf values")69 70 return True71 72def preprocess_nifti(file_path, device=None):73 try:74 print(f"Preprocessing file: {file_path}")75 if not os.path.exists(file_path):76 raise FileNotFoundError(f"File not found: {file_path}")77 78 file_size = os.path.getsize(file_path) / (1024**2)79 file_size_kb = os.path.getsize(file_path) / 102480 if file_size == 0:81 raise ValueError("NIfTI file is empty")82 if file_size > 2000:83 raise ValueError(f"NIfTI file too large: {file_size:.1f} MB. Maximum processing size is 2 GB. For larger files, consider compression or resampling.")84 85 if file_size_kb < 100:86 print(f" ⚠ WARNING: File size is very small ({file_size_kb:.1f} KB). This may indicate:")87 print(f" - Low resolution/compressed data (may lose texture and boundary cues)")88 print(f" - Single slice upload (incomplete anatomy)")89 print(f" - Data compression artifacts (may distort intensity gradients)")90 91 print(f"Loading NIfTI file with nibabel...")92 if file_size > 100:93 nifti_img = nib.load(file_path, mmap=True)94 else:95 nifti_img = nib.load(file_path)96 print(f"NIfTI shape: {nifti_img.shape}, dtype: {nifti_img.get_fdata().dtype}")97 98 if len(nifti_img.shape) == 3:99 if any(s < 10 for s in nifti_img.shape):100 print(f" ⚠ WARNING: Very small dimension detected ({nifti_img.shape}). May be a single slice or cropped volume.")101 if nifti_img.shape[2] < 20:102 print(f" ⚠ WARNING: Only {nifti_img.shape[2]} slices detected. Model expects full 3D volumes for best results.")103 104 validate_nifti(nifti_img)105 106 voxel_spacing = nifti_img.header.get_zooms()[:3] if len(nifti_img.header.get_zooms()) >= 3 else (1.0, 1.0, 1.0)107 if voxel_spacing == (1.0, 1.0, 1.0):108 print(f" ⚠ WARNING: Voxel spacing is (1.0, 1.0, 1.0) - metadata may be missing or lost during conversion.")109 print(f" This can cause incorrect volume calculations and scaling issues.")110 111 affine = nifti_img.affine112 affine_det = np.linalg.det(affine[:3, :3])113 print(f" → Voxel spacing: {voxel_spacing}")114 print(f" → Affine determinant: {affine_det:.6f}")115 116 if abs(affine_det) < 0.1 or abs(affine_det) > 100:117 print(f" ⚠ WARNING: Unusual affine determinant ({affine_det:.6f}). Spatial metadata may be corrupted.")118 119 raw_data_dtype = nifti_img.get_fdata().dtype120 if raw_data_dtype == np.uint8 or raw_data_dtype == np.uint16:121 print(f" ⚠ WARNING: Input data type is {raw_data_dtype} (integer). Model expects float32.")122 print(f" Integer data may indicate compression or conversion artifacts.")123 print(f" Converting to float32, but quality may be reduced.")124 125 raw_data = nifti_img.get_fdata(dtype=np.float32)126 print(f" → Raw data stats: min={raw_data.min():.4f}, max={raw_data.max():.4f}, mean={raw_data.mean():.4f}, std={raw_data.std():.4f}")127 128 if raw_data.max() - raw_data.min() < 1e-6:129 raise ValueError(f"Input NIfTI file contains constant values (min=max={raw_data.min():.4f}). Cannot process.")130 131 if raw_data.std() < 1e-3:132 print(f" ⚠ WARNING: Very low data variance (std={raw_data.std():.4f}). Data may be corrupted or over-compressed.")133 134 if raw_data.max() > 10000 or raw_data.min() < -1000:135 print(f" ⚠ WARNING: Extreme intensity values detected (range: [{raw_data.min():.1f}, {raw_data.max():.1f}]).")136 print(f" Data may not be properly normalized. Model expects normalized float32 tensors.")137 138 nonzero_mask = raw_data > 1e-6139 nonzero_count = nonzero_mask.sum()140 total_count = raw_data.size141 nonzero_ratio = nonzero_count / total_count if total_count > 0 else 0.0142 143 print(f" → Non-zero voxels: {nonzero_count:,} / {total_count:,} ({100*nonzero_ratio:.2f}%)")144 145 is_prenormalized = (raw_data.max() <= 1.0 and raw_data.min() >= 0.0)146 if is_prenormalized:147 print(f" → Detected pre-normalized data (range [0, 1]). Still applying training-matched NormalizeIntensityd.")148 149 use_enhanced_preprocessing = os.environ.get("USE_ENHANCED_PREPROCESSING", "false").lower() == "true"150 151 if use_enhanced_preprocessing:152 try:153 transform = transforms.Compose([154 transforms.LoadImaged(keys=["image"]),155 transforms.Orientationd(keys=["image"], axcodes="RAS"),156 transforms.Spacingd(keys=["image"], pixdim=(1.5, 1.5, 3.0), mode="bilinear"),157 transforms.EnsureChannelFirstD(keys=["image"], channel_dim="no_channel"),158 transforms.ScaleIntensityRangePercentilesd(keys="image", lower=2, upper=98, b_min=0.0, b_max=1.0, clip=True),159 transforms.NormalizeIntensityd(keys="image", nonzero=True, channel_wise=True),160 transforms.ToTensord(keys=["image"])161 ])162 print(" → Using enhanced preprocessing (orientation + spacing + percentile scaling)")163 except Exception as e:164 print(f" ⚠ Warning: Could not create enhanced transform pipeline: {e}. Falling back to training-matched preprocessing...")165 use_enhanced_preprocessing = False166 167 if not use_enhanced_preprocessing:168 # Training-matched: LoadImaged, Orientationd(RAS), EnsureChannelFirstD, NormalizeIntensityd, ToTensord169 # Same as SRMA-Mamba test.py but with RAS for consistent orientation with mask saving.170 transform = transforms.Compose([171 transforms.LoadImaged(keys=["image"]),172 transforms.Orientationd(keys=["image"], axcodes="RAS"),173 transforms.EnsureChannelFirstD(keys=["image"], channel_dim="no_channel"),174 transforms.NormalizeIntensityd(keys="image", nonzero=True, channel_wise=True),175 transforms.ToTensord(keys=["image"])176 ])177 print(" → Using training-matched preprocessing (RAS + NormalizeIntensityd)")178 179 data = {"image": file_path}180 print("Applying transforms...")181 182 try:183 augmented = transform(data)184 image_data = augmented["image"]185 except Exception as e:186 print(f" ⚠ Transform failed: {e}. Trying fallback preprocessing...")187 try:188 raw_data_norm = (raw_data - raw_data.min()) / (raw_data.max() - raw_data.min() + 1e-8)189 if raw_data_norm.std() < 1e-6:190 raise ValueError("Normalized data is still constant")191 image_data = torch.from_numpy(raw_data_norm).float()192 image_data = image_data.unsqueeze(0)193 print(" → Used fallback normalization (min-max scaling)")194 except Exception as e2:195 raise ValueError(f"Both standard and fallback preprocessing failed: {e2}")196 197 if not isinstance(image_data, torch.Tensor):198 image_data = torch.from_numpy(np.array(image_data))199 200 if image_data.dtype != torch.float32:201 image_data = image_data.float()202 203 img_np = image_data.numpy() if not hasattr(image_data, 'device') or image_data.device.type == 'cpu' else image_data.cpu().numpy()204 vmin, vmax = float(img_np.min()), float(img_np.max())205 206 if vmax - vmin < 1e-6:207 print(f" ⚠ WARNING: Preprocessing produced near-constant image (min={vmin:.6f}, max={vmax:.6f}). Trying alternative preprocessing...")208 try:209 if nonzero_ratio > 0.01:210 nonzero_mean = raw_data[nonzero_mask].mean()211 nonzero_std = raw_data[nonzero_mask].std() + 1e-8212 raw_data_norm = np.zeros_like(raw_data)213 raw_data_norm[nonzero_mask] = (raw_data[nonzero_mask] - nonzero_mean) / nonzero_std214 raw_data_norm = (raw_data_norm - raw_data_norm.min()) / (raw_data_norm.max() - raw_data_norm.min() + 1e-8)215 else:216 raw_data_norm = (raw_data - raw_data.min()) / (raw_data.max() - raw_data.min() + 1e-8)217 218 if raw_data_norm.std() < 1e-6:219 raise ValueError("Alternative normalization also produced constant data")220 221 image_data = torch.from_numpy(raw_data_norm).float()222 image_data = image_data.unsqueeze(0)223 img_np = image_data.numpy()224 vmin, vmax = float(img_np.min()), float(img_np.max())225 print(f" → Alternative preprocessing successful: min={vmin:.4f}, max={vmax:.4f}, mean={img_np.mean():.4f}, std={img_np.std():.4f}")226 except Exception as e3:227 raise ValueError(f"Preprocessing produced near-constant image: min={vmin:.6f}, max={vmax:.6f}. Alternative preprocessing also failed: {e3}")228 229 print(f" → After transforms: min={vmin:.4f}, max={vmax:.4f}, mean={img_np.mean():.4f}, std={img_np.std():.4f}")230 231 if device is not None and device.type == 'cuda':232 if image_data.is_pinned():233 image_data = image_data.to(device, non_blocking=True)234 else:235 image_data = image_data.pin_memory().to(device, non_blocking=True)236 237 if len(image_data.shape) >= 4:238 try:239 if hasattr(torch, "channels_last_3d"):240 image_data = image_data.contiguous(memory_format=torch.channels_last_3d)241 if image_data.is_contiguous(memory_format=torch.channels_last_3d):242 print(f" → Using channels-last 3D memory layout (optimized for GPU)")243 except:244 pass245 246 print(f"Preprocessed shape: {image_data.shape}, dtype: {image_data.dtype}, device: {image_data.device if hasattr(image_data, 'device') else 'CPU'}")247 if image_data.numel() == 0:248 raise ValueError("Preprocessed image is empty")249 return image_data250 except Exception as e:251 error_msg = f"Preprocessing error: {e}"252 print(f"✗ {error_msg}")253 import traceback254 traceback.print_exc()255 raise ValueError(f"Failed to preprocess NIfTI file: {e}") from e256 257def refine_liver_mask_enhanced(mask, voxel_spacing, pred_probabilities, threshold, modality):258 259 original_shape = mask.shape260 original_sum = mask.sum()261 262 was_4d = len(mask.shape) == 4263 was_5d = len(mask.shape) == 5264 265 if was_5d:266 mask_3d = mask[0, 0, 0] if mask.shape[0] == 1 and mask.shape[1] == 1 and mask.shape[2] == 1 else mask[0, 0]267 elif was_4d:268 mask_3d = mask[0, 0] if mask.shape[0] == 1 and mask.shape[1] == 1 else mask[0]269 else:270 mask_3d = mask.copy()271 272 if mask_3d.dtype != np.uint8:273 mask_3d = (mask_3d > 0.5).astype(np.uint8)274 275 if mask_3d.sum() == 0:276 return np.zeros(original_shape, dtype=np.uint8), {277 "original_voxels": 0, "refined_voxels": 0, "removed_voxels": 0,278 "connected_components_before": 0, "connected_components_after": 0,279 "volume_change_ml": 0.0, "volume_change_percent": 0.0,280 "guards_ok": False281 }, 0.0282 283 H, W, D = mask_3d.shape284 guards_ok = True285 286 print(f" NOTE: Spatial priors assume RAS orientation (Right-Anterior-Superior).")287 print(f" Input should be reoriented to RAS using nib.as_closest_canonical() before processing.")288 print(f" If orientation is unknown, spatial priors may remove valid liver tissue.")289 290 top_remove = max(1, int(0.15 * D))291 mask_3d[:, :, :top_remove] = 0292 if top_remove > 0:293 print(f" Spatial prior: Removed top {top_remove} slices (15% - diaphragm protection, assumes Superior axis)")294 295 right_trim = max(0, int(0.30 * W))296 mask_3d[:, W-right_trim:, :] = 0297 if right_trim > 0:298 print(f" Spatial prior: Removed right {right_trim} pixels (30% - stomach protection, assumes Right axis)")299 300 left_trim = max(0, int(0.15 * W))301 mask_3d[:, :left_trim, :] = 0302 if left_trim > 0:303 print(f" Spatial prior: Removed left {left_trim} pixels (15% - spleen protection, assumes Left axis)")304 305 bottom_remove = max(1, int(0.10 * D))306 mask_3d[:, :, -bottom_remove:] = 0307 if bottom_remove > 0:308 print(f" Spatial prior: Removed bottom {bottom_remove} slices (10% - lower abdomen protection, assumes Inferior axis)")309 310 if D > 2:311 bottom_slices = mask_3d[:, :, -2:]312 if bottom_slices.sum() > 0:313 mask_3d[:, :, -2:] = 0314 print(f" Bottom-cap trim: Removed bottom 2 slices (diaphragm protection)")315 guards_ok = False316 317 labels_before, num_components_before = ndimage.label(mask_3d)318 319 if num_components_before == 0:320 print(f" QC FAIL: No components after spatial priors. Attempting auto-rethreshold...")321 guards_ok = False322 if hasattr(pred_probabilities, 'shape') and len(pred_probabilities.shape) >= 3:323 if len(pred_probabilities.shape) == 4:324 pred_3d = pred_probabilities[0, 0]325 elif len(pred_probabilities.shape) == 5:326 pred_3d = pred_probabilities[0, 0, 0]327 else:328 pred_3d = pred_probabilities329 330 top_remove = max(1, int(0.15 * D))331 right_trim = max(0, int(0.30 * W))332 left_trim = max(0, int(0.15 * W))333 bottom_remove = max(1, int(0.10 * D))334 335 for retry_threshold in [0.70, 0.65, 0.60, 0.55, 0.50]:336 mask_retry = (pred_3d > retry_threshold).astype(np.uint8)337 mask_retry[:, :, :top_remove] = 0338 mask_retry[:, W-right_trim:, :] = 0339 mask_retry[:, :left_trim, :] = 0340 mask_retry[:, :, -bottom_remove:] = 0341 if mask_retry.sum() > 1000:342 mask_3d = mask_retry343 print(f" Auto-rethreshold: Found mask at threshold {retry_threshold:.3f}")344 break345 else:346 return np.zeros(original_shape, dtype=np.uint8), {347 "original_voxels": original_sum, "refined_voxels": 0, "removed_voxels": int(original_sum),348 "connected_components_before": 0, "connected_components_after": 0,349 "volume_change_ml": 0.0, "volume_change_percent": -100.0,350 "guards_ok": False351 }, 0.0352 353 labels_before, num_components_before = ndimage.label(mask_3d)354 component_sizes = ndimage.sum(mask_3d, labels_before, range(1, num_components_before + 1))355 largest_label = component_sizes.argmax() + 1356 mask_3d = (labels_before == largest_label).astype(np.uint8)357 print(f" Kept largest connected component ({component_sizes.max():,} voxels)")358 359 coords = np.where(mask_3d > 0)360 if len(coords[0]) > 0:361 z_span = (coords[2].max() - coords[2].min() + 1) / D if D > 0 else 0362 363 if z_span < 0.25:364 print(f" QC FAIL: Z-span only {z_span*100:.1f}% (<25%). Attempting iterative rethreshold...")365 guards_ok = False366 367 if hasattr(pred_probabilities, 'shape') and len(pred_probabilities.shape) >= 3:368 if len(pred_probabilities.shape) == 4:369 pred_3d = pred_probabilities[0, 0]370 elif len(pred_probabilities.shape) == 5:371 pred_3d = pred_probabilities[0, 0, 0]372 else:373 pred_3d = pred_probabilities374 375 best_mask = mask_3d376 best_z_span = z_span377 378 top_remove = max(1, int(0.12 * D))379 right_trim = max(0, int(0.25 * W))380 left_trim = max(0, int(0.10 * W))381 382 for retry_threshold in [0.65, 0.60, 0.55, 0.50, 0.45, 0.40, 0.35]:383 mask_retry = (pred_3d > retry_threshold).astype(np.uint8)384 mask_retry[:, :, :top_remove] = 0385 mask_retry[:, W-right_trim:, :] = 0386 mask_retry[:, :left_trim, :] = 0387 388 if mask_retry.sum() < 1000:389 continue390 391 labels_retry, _ = ndimage.label(mask_retry)392 if labels_retry.max() > 0:393 comp_sizes_retry = ndimage.sum(mask_retry, labels_retry, range(1, labels_retry.max() + 1))394 largest_retry = comp_sizes_retry.argmax() + 1395 mask_retry = (labels_retry == largest_retry).astype(np.uint8)396 397 coords_retry = np.where(mask_retry > 0)398 if len(coords_retry[0]) > 0:399 z_span_retry = (coords_retry[2].max() - coords_retry[2].min() + 1) / D400 401 if z_span_retry >= 0.25:402 mask_3d = mask_retry403 print(f" Auto-rethreshold SUCCESS: threshold={retry_threshold:.3f}, z-span={z_span_retry*100:.1f}%")404 break405 elif z_span_retry > best_z_span:406 best_mask = mask_retry407 best_z_span = z_span_retry408 else:409 if best_z_span > z_span:410 mask_3d = best_mask411 print(f" Auto-rethreshold: Using best z-span={best_z_span*100:.1f}% (still <25%)")412 else:413 print(f" Auto-rethreshold FAILED: No threshold yielded z-span >= 25%")414 415 labels_before_morph, _ = ndimage.label(mask_3d)416 if labels_before_morph.max() > 0:417 component_sizes_before_morph = ndimage.sum(mask_3d, labels_before_morph, range(1, labels_before_morph.max() + 1))418 if len(component_sizes_before_morph) > 0:419 largest_label_before_morph = component_sizes_before_morph.argmax() + 1420 mask_3d = (labels_before_morph == largest_label_before_morph).astype(np.uint8)421 print(f" Kept largest component before morphology")422 423 try:424 mask_3d = mask_3d.astype(bool)425 structure = ball(2)426 mask_3d = binary_closing(mask_3d, structure=structure)427 mask_3d = mask_3d.astype(np.uint8)428 print(f" Applied binary closing (ball radius=2)")429 except Exception as e:430 print(f" Binary closing failed: {e}")431 432 try:433 mask_3d = mask_3d.astype(bool)434 mask_3d = binary_fill_holes(mask_3d)435 mask_3d = mask_3d.astype(np.uint8)436 print(f" Filled holes")437 except Exception as e:438 print(f" Hole filling failed: {e}")439 440 try:441 mask_3d = median_filter(mask_3d, size=3)442 print(f" Applied 3D median filter (size=3)")443 except Exception as e:444 print(f" Median filter failed: {e}")445 446 labels_after_morph, _ = ndimage.label(mask_3d)447 if labels_after_morph.max() > 0:448 component_sizes_morph = ndimage.sum(mask_3d, labels_after_morph, range(1, labels_after_morph.max() + 1))449 if len(component_sizes_morph) > 0:450 largest_label_morph = component_sizes_morph.argmax() + 1451 mask_3d = (labels_after_morph == largest_label_morph).astype(np.uint8)452 print(f" Re-kept largest component after morphology")453 454 labels_after, num_components_after = ndimage.label(mask_3d)455 456 refined_sum = mask_3d.sum()457 removed_voxels = int(np.int64(original_sum) - np.int64(refined_sum))458 459 voxel_volume = voxel_spacing[0] * voxel_spacing[1] * voxel_spacing[2]460 volume_change_ml = (removed_voxels * voxel_volume) / 1000.0461 volume_change_percent = (removed_voxels / float(original_sum) * 100.0) if original_sum > 0 else 0.0462 463 volume_ml = (refined_sum * voxel_volume) / 1000.0464 465 coords_final = np.where(mask_3d > 0)466 if len(coords_final[0]) > 0:467 z_span_final = (coords_final[2].max() - coords_final[2].min() + 1) / D if D > 0 else 0468 x_centroid = np.mean(coords_final[1]) if len(coords_final) > 1 else W / 2469 y_centroid = np.mean(coords_final[0]) if len(coords_final) > 0 else H / 2470 471 if volume_ml < 800 or volume_ml > 2500:472 print(f" QC FAIL: Volume {volume_ml:.1f}ml outside normal range [800-2500ml]")473 guards_ok = False474 475 if z_span_final < 0.20:476 print(f" QC FAIL: Z-span {z_span_final*100:.1f}% too small (<20%)")477 guards_ok = False478 479 liver_x_min = 0.15 * W480 liver_x_max = 0.55 * W481 if x_centroid < liver_x_min or x_centroid > liver_x_max:482 print(f" QC FAIL: x-centroid {x_centroid:.1f} outside expected liver band [15%-55% of width]")483 guards_ok = False484 485 liver_y_min = 0.25 * H486 liver_y_max = 0.75 * H487 if y_centroid < liver_y_min or y_centroid > liver_y_max:488 print(f" QC FAIL: y-centroid {y_centroid:.1f} outside expected liver band [25%-75% of height]")489 guards_ok = False490 491 if volume_ml < 800:492 print(f" QC WARNING: Volume {volume_ml:.1f}ml suspiciously low - may be wrong organ")493 guards_ok = False494 495 if volume_change_percent > 80:496 print(f" QC FAIL: Refinement removed {volume_change_percent:.1f}% - too aggressive")497 guards_ok = False498 499 if was_5d:500 if original_shape[0] == 1 and original_shape[1] == 1 and original_shape[2] == 1:501 mask_3d = mask_3d[np.newaxis, np.newaxis, np.newaxis, :, :, :]502 else:503 mask_3d = mask_3d[np.newaxis, np.newaxis, :, :, :]504 elif was_4d:505 if original_shape[0] == 1 and original_shape[1] == 1:506 mask_3d = mask_3d[np.newaxis, np.newaxis, :, :, :]507 else:508 mask_3d = mask_3d[np.newaxis, :, :, :]509 510 mask_3d = mask_3d.astype(np.uint8)511 512 if mask_3d.shape != original_shape:513 if len(original_shape) == 3:514 while mask_3d.ndim > 3:515 mask_3d = mask_3d.squeeze(0)516 elif len(original_shape) == 4:517 while mask_3d.ndim < 4:518 mask_3d = mask_3d[np.newaxis, ...]519 while mask_3d.ndim > 4:520 mask_3d = mask_3d.squeeze(0)521 elif len(original_shape) == 5:522 while mask_3d.ndim < 5:523 mask_3d = mask_3d[np.newaxis, ...]524 while mask_3d.ndim > 5:525 mask_3d = mask_3d.squeeze(0)526 527 print(f" Refinement complete: {original_sum:,} -> {refined_sum:,} voxels ({removed_voxels:,} removed, {volume_change_percent:.2f}%)")528 print(f" Connected components: {num_components_before} -> {num_components_after}")529 530 confidence_score = calculate_confidence_score(mask_3d, pred_probabilities, threshold, num_components_after, volume_change_percent, guards_ok, voxel_spacing)531 532 metrics = {533 "original_voxels": int(original_sum),534 "refined_voxels": int(refined_sum),535 "removed_voxels": removed_voxels,536 "connected_components_before": int(num_components_before),537 "connected_components_after": int(num_components_after),538 "volume_change_ml": float(volume_change_ml),539 "volume_change_percent": float(volume_change_percent),540 "guards_ok": guards_ok541 }542 543 return mask_3d, metrics, confidence_score544 545def calculate_confidence_score(mask, pred_probabilities, threshold, num_components, volume_change_percent, guards_ok=True, voxel_spacing=(1.0, 1.0, 1.0)):546 if mask.sum() == 0:547 return 0.0548 549 if len(mask.shape) == 4:550 mask_3d = mask[0, 0]551 elif len(mask.shape) == 5:552 mask_3d = mask[0, 0, 0]553 else:554 mask_3d = mask555 556 if len(pred_probabilities.shape) == 4:557 pred_3d = pred_probabilities[0, 0]558 elif len(pred_probabilities.shape) == 5:559 pred_3d = pred_probabilities[0, 0, 0]560 else:561 pred_3d = pred_probabilities562 563 mask_indices = mask_3d > 0564 if mask_indices.sum() == 0:565 return 0.0566 567 avg_p = float(np.clip(pred_3d[mask_indices].mean(), 0.0, 1.0))568 comp_pen = 1.0 if num_components == 1 else max(0.5, 1.0 - 0.1 * (num_components - 1))569 vol_pen = 1.0 if abs(volume_change_percent) < 50 else 0.7570 571 if not guards_ok:572 guard_pen = 0.5573 else:574 guard_pen = 1.0575 576 volume_ml = (mask_3d.sum() * (voxel_spacing[0] * voxel_spacing[1] * voxel_spacing[2])) / 1000.0577 if volume_ml < 800:578 volume_penalty = 0.5579 elif volume_ml < 1000:580 volume_penalty = 0.7581 elif volume_ml < 1200:582 volume_penalty = 0.9583 else:584 volume_penalty = 1.0585 586 confidence = 100 * avg_p * comp_pen * vol_pen * guard_pen * volume_penalty587 confidence = float(np.clip(confidence, 0, 100))588 589 return confidence590 591def refine_liver_mask(mask, voxel_spacing=(1.0, 1.0, 1.0), enable_smoothing=True, min_component_size=None):592 """593 Refine liver segmentation mask to remove fragmentation, smooth boundaries, and ensure single connected component.594 595 Args:596 mask: 3D or 4D numpy array (H, W, D) or (1, 1, H, W, D) with binary values (0 or 1)597 voxel_spacing: Tuple of (z, y, x) voxel spacing in mm598 enable_smoothing: Whether to apply median filter smoothing (default: True)599 min_component_size: Minimum size for connected components to keep (None = keep only largest)600 601 Returns:602 refined_mask: Refined binary mask (same shape as input)603 metrics: Dictionary with refinement statistics604 """605 original_shape = mask.shape606 original_sum = mask.sum()607 608 was_4d = len(mask.shape) == 4609 was_5d = len(mask.shape) == 5610 611 if was_5d:612 mask = mask[0, 0, 0] if mask.shape[0] == 1 and mask.shape[1] == 1 and mask.shape[2] == 1 else mask[0, 0]613 elif was_4d:614 mask = mask[0, 0] if mask.shape[0] == 1 and mask.shape[1] == 1 else mask[0]615 616 if mask.dtype != np.uint8:617 mask = (mask > 0.5).astype(np.uint8)618 619 if mask.sum() == 0:620 print(" ⚠ Empty mask - no refinement possible")621 return np.zeros(original_shape, dtype=np.uint8), {622 "original_voxels": 0,623 "refined_voxels": 0,624 "removed_voxels": 0,625 "connected_components_before": 0,626 "connected_components_after": 0,627 "volume_change_ml": 0.0,628 "volume_change_percent": 0.0629 }630 631 labels_before, num_components_before = ndimage.label(mask)632 633 if num_components_before == 0:634 print(" ⚠ No connected components found")635 return np.zeros(original_shape, dtype=np.uint8), {636 "original_voxels": original_sum,637 "refined_voxels": 0,638 "removed_voxels": int(original_sum),639 "connected_components_before": 0,640 "connected_components_after": 0,641 "volume_change_ml": 0.0,642 "volume_change_percent": -100.0643 }644 645 component_sizes = ndimage.sum(mask, labels_before, range(1, num_components_before + 1))646 647 if min_component_size is None:648 largest_label = component_sizes.argmax() + 1649 mask = (labels_before == largest_label).astype(np.uint8)650 print(f" → Kept largest connected component ({component_sizes.max():,} voxels)")651 else:652 valid_labels = np.where(component_sizes >= min_component_size)[0] + 1653 if len(valid_labels) == 0:654 largest_label = component_sizes.argmax() + 1655 mask = (labels_before == largest_label).astype(np.uint8)656 print(f" → No components >= {min_component_size} voxels, kept largest ({component_sizes.max():,} voxels)")657 else:658 mask = np.isin(labels_before, valid_labels).astype(np.uint8)659 print(f" → Kept {len(valid_labels)} component(s) >= {min_component_size} voxels")660 661 after_cc = mask.sum()662 663 try:664 structure = ball(3)665 mask = binary_closing(mask, structure=structure)666 print(f" → Applied binary closing (ball radius=3)")667 except Exception as e:668 print(f" ⚠ Binary closing failed: {e}")669 670 try:671 mask = binary_fill_holes(mask)672 print(f" → Filled holes")673 except Exception as e:674 print(f" ⚠ Hole filling failed: {e}")675 676 try:677 structure = ball(2)678 mask = binary_opening(mask, structure=structure)679 print(f" → Applied binary opening (ball radius=2)")680 except Exception as e:681 print(f" ⚠ Binary opening failed: {e}")682 683 if enable_smoothing:684 try:685 mask = median_filter(mask, size=3)686 print(f" → Applied 3D median filter (size=3)")687 except Exception as e:688 print(f" ⚠ Median filter failed: {e}")689 690 labels_after, num_components_after = ndimage.label(mask)691 692 refined_sum = mask.sum()693 removed_voxels = int(original_sum - refined_sum)694 695 voxel_volume = voxel_spacing[0] * voxel_spacing[1] * voxel_spacing[2]696 volume_change_ml = (removed_voxels * voxel_volume) / 1000.0697 volume_change_percent = (removed_voxels / original_sum * 100.0) if original_sum > 0 else 0.0698 699 if was_5d:700 if original_shape[0] == 1 and original_shape[1] == 1 and original_shape[2] == 1:701 mask = mask[np.newaxis, np.newaxis, np.newaxis, :, :, :]702 else:703 mask = mask[np.newaxis, np.newaxis, :, :, :]704 elif was_4d:705 if original_shape[0] == 1 and original_shape[1] == 1:706 mask = mask[np.newaxis, np.newaxis, :, :, :]707 else:708 mask = mask[np.newaxis, :, :, :]709 710 mask = mask.astype(np.uint8)711 712 if mask.shape != original_shape:713 print(f" ⚠ Shape mismatch: {mask.shape} vs {original_shape}, fixing...")714 if len(original_shape) == 3:715 while mask.ndim > 3:716 mask = mask.squeeze(0)717 elif len(original_shape) == 4:718 while mask.ndim < 4:719 mask = mask[np.newaxis, ...]720 while mask.ndim > 4:721 mask = mask.squeeze(0)722 elif len(original_shape) == 5:723 while mask.ndim < 5:724 mask = mask[np.newaxis, ...]725 while mask.ndim > 5:726 mask = mask.squeeze(0)727 728 print(f" ✓ Refinement complete: {original_sum:,} → {refined_sum:,} voxels ({removed_voxels:,} removed, {volume_change_percent:.2f}%)")729 print(f" → Connected components: {num_components_before} → {num_components_after}")730 731 metrics = {732 "original_voxels": int(original_sum),733 "refined_voxels": int(refined_sum),734 "removed_voxels": removed_voxels,735 "connected_components_before": int(num_components_before),736 "connected_components_after": int(num_components_after),737 "volume_change_ml": float(volume_change_ml),738 "volume_change_percent": float(volume_change_percent)739 }740 741 return mask, metrics742 743def calculate_liver_volume(pred_binary, voxel_spacing=(1.0, 1.0, 1.0)):744 voxel_volume = voxel_spacing[0] * voxel_spacing[1] * voxel_spacing[2]745 liver_voxels = pred_binary.sum()746 volume_ml = liver_voxels * voxel_volume / 1000.0747 return volume_ml748 749def analyze_liver_morphology(pred_binary):750 if len(pred_binary.shape) == 4:751 mask_3d = pred_binary[0]752 elif len(pred_binary.shape) == 5:753 mask_3d = pred_binary[0, 0]754 else:755 mask_3d = pred_binary756 757 labeled_mask, num_features = ndimage.label(mask_3d)758 if num_features == 0:759 return {"connected_components": 0, "largest_component_ratio": 0.0, "fragmentation": "high"}760 761 component_sizes = [np.sum(labeled_mask == i) for i in range(1, num_features + 1)]762 largest_component = max(component_sizes)763 total_liver = pred_binary.sum()764 largest_ratio = largest_component / total_liver if total_liver > 0 else 0.0765 766 if largest_ratio > 0.95:767 fragmentation = "low"768 elif largest_ratio > 0.80:769 fragmentation = "moderate"770 else:771 fragmentation = "high"772 773 return {774 "connected_components": int(num_features),775 "largest_component_ratio": float(largest_ratio),776 "fragmentation": fragmentation777 }778 779def check_volume_sanity(volume_ml):780 normal_range = (float(os.getenv("LIVER_VOL_LOW", "1200")), float(os.getenv("LIVER_VOL_HIGH", "1800")))781 if volume_ml < normal_range[0] * 0.5:782 return "CRITICAL", f"Volume ({volume_ml:.1f} ml) is extremely low (<50% of normal). Please visually inspect overlay for segmentation errors."783 elif volume_ml < normal_range[0]:784 return "WARNING", f"Volume ({volume_ml:.1f} ml) is below normal range. Please visually inspect overlay."785 elif volume_ml > normal_range[1] * 1.5:786 return "CRITICAL", f"Volume ({volume_ml:.1f} ml) is extremely high (>150% of normal). Please visually inspect overlay for segmentation errors."787 elif volume_ml > normal_range[1]:788 return "WARNING", f"Volume ({volume_ml:.1f} ml) is above normal range. Please visually inspect overlay."789 return "OK", None790 791def generate_medical_report(statistics, volume_ml, morphology, modality, confidence_score=0.0):792 liver_percentage = statistics["liver_percentage"]793 volume_shape = statistics["volume_shape"]794 liver_voxels = statistics.get("liver_voxels", 0)795 total_voxels = statistics.get("total_voxels", 0)796 797 normal_liver_volume_range = (1200, 1800)798 normal_liver_percentage_range = (2.0, 3.5)799 800 findings = []801 recommendations = []802 clinical_notes = []803 quality_assessment = []804 805 if liver_voxels == 0:806 severity = "failure"807 status = "FAILURE"808 findings.append("**SEGMENTATION FAILURE:** No liver tissue detected (0 voxels segmented).")809 recommendations.append("**CRITICAL:** Segmentation failed completely. Possible causes:")810 recommendations.append(" • Input quality issues (low resolution, compression, missing metadata)")811 recommendations.append(" • Threshold too high for prediction distribution")812 recommendations.append(" • Model mismatch with input modality or preprocessing")813 recommendations.append(" • Please check input file quality and try again, or contact support.")814 clinical_notes.append("The automated segmentation system failed to identify any liver tissue. This indicates a technical failure rather than an anatomical finding.")815 quality_assessment.append("**Segmentation Failure:** No voxels were segmented. Manual review and re-processing required.")816 impression_parts = ["Automated liver segmentation FAILED. No liver tissue was detected."]817 impression_parts.append("This is a technical failure requiring investigation of input quality and model compatibility.")818 else:819 severity = "normal"820 status = "NORMAL"821 822 num_components = morphology.get("connected_components", 1)823 largest_ratio = morphology.get("largest_component_ratio", 1.0)824 825 if num_components > 1 and largest_ratio < 0.9:826 severity = "critical"827 status = "CRITICAL"828 findings.append(f"**CRITICAL: Fragmented Segmentation:** {num_components} disconnected components detected. Largest component is only {largest_ratio*100:.1f}% of total volume.")829 recommendations.append("**URGENT:** Segmentation shows severe fragmentation. Manual correction required.")830 clinical_notes.append("The segmentation contains multiple disconnected regions, indicating possible segmentation artifacts or severe anatomical abnormalities.")831 elif volume_ml < normal_liver_volume_range[0] * 0.5 or volume_ml > normal_liver_volume_range[1] * 1.5:832 if confidence_score < 50:833 severity = "critical"834 status = "CRITICAL"835 else:836 severity = "moderate"837 status = "WARNING"838 elif volume_ml < normal_liver_volume_range[0] or volume_ml > normal_liver_volume_range[1]:839 severity = "moderate"840 status = "WARNING"841 842 volume_sanity_status, volume_sanity_msg = check_volume_sanity(volume_ml)843 if volume_sanity_status == "CRITICAL":844 if severity != "critical":845 severity = "critical"846 status = "CRITICAL"847 findings.append(f"**CRITICAL FINDING:** {volume_sanity_msg}")848 recommendations.append("**URGENT:** Visual inspection and manual review required. Segmentation may contain significant errors that could affect clinical interpretation.")849 clinical_notes.append("The automated segmentation has produced results that fall outside expected physiological ranges. This may indicate technical issues with the segmentation algorithm or unusual patient anatomy.")850 elif volume_sanity_status == "WARNING":851 if severity == "normal":852 severity = "moderate"853 status = "WARNING"854 findings.append(f"**WARNING:** {volume_sanity_msg}")855 recommendations.append("Visual inspection recommended to verify segmentation accuracy and ensure clinical validity.")856 clinical_notes.append("The segmentation results are outside the typical range but may still be clinically valid depending on patient-specific factors.")857 clinical_notes.append("Note: Normal liver volume range (1200-1800 ml) is for average adult body size. Pediatric patients or extreme body sizes may have different normal ranges.")858 859 if volume_ml < normal_liver_volume_range[0]:860 findings.append(f"**Liver Volume Assessment:** Measured liver volume is **{volume_ml:.1f} ml**, which is below the normal reference range of {normal_liver_volume_range[0]}-{normal_liver_volume_range[1]} ml.")861 clinical_notes.append(f"This represents approximately **{((normal_liver_volume_range[0] - volume_ml) / normal_liver_volume_range[0] * 100):.1f}% reduction** compared to the lower limit of normal. Possible etiologies include:")862 clinical_notes.append(" • Chronic liver disease with parenchymal loss")863 clinical_notes.append(" • Post-surgical resection")864 clinical_notes.append(" • Cirrhosis with volume loss")865 clinical_notes.append(" • Age-related atrophy")866 recommendations.append("Consider follow-up imaging to monitor liver volume changes over time. Correlation with clinical history and liver function tests is recommended.")867 if severity == "normal":868 severity = "mild" if volume_ml > normal_liver_volume_range[0] * 0.7 else "moderate"869 elif volume_ml > normal_liver_volume_range[1]:870 findings.append(f"**Liver Volume Assessment:** Measured liver volume is **{volume_ml:.1f} ml**, which exceeds the normal reference range of {normal_liver_volume_range[0]}-{normal_liver_volume_range[1]} ml.")871 clinical_notes.append(f"This represents approximately **{((volume_ml - normal_liver_volume_range[1]) / normal_liver_volume_range[1] * 100):.1f}% increase** compared to the upper limit of normal, consistent with hepatomegaly. Potential causes include:")872 clinical_notes.append(" • Fatty liver disease (steatosis)")873 clinical_notes.append(" • Congestive hepatopathy")874 clinical_notes.append(" • Inflammatory conditions")875 clinical_notes.append(" • Storage diseases")876 clinical_notes.append(" • Neoplastic processes")877 recommendations.append("Further clinical evaluation recommended to identify underlying etiology. Consider correlation with laboratory findings, clinical history, and additional imaging studies.")878 if severity == "normal":879 severity = "mild" if volume_ml < normal_liver_volume_range[1] * 1.3 else "moderate"880 else:881 findings.append(f"**Liver Volume Assessment:** Measured liver volume is **{volume_ml:.1f} ml**, which falls within the normal reference range of {normal_liver_volume_range[0]}-{normal_liver_volume_range[1]} ml.")882 clinical_notes.append("The liver volume is within expected physiological parameters for an adult patient.")883 884 if morphology["connected_components"] > 1:885 if morphology["largest_component_ratio"] < 0.9:886 if severity != "critical":887 severity = "critical"888 status = "CRITICAL"889 findings.append(f"**CRITICAL: Fragmented Segmentation:** The liver segmentation identified **{morphology['connected_components']} separate connected components**. The largest component represents only **{morphology['largest_component_ratio']*100:.1f}%** of the total segmented volume.")890 quality_assessment.append("**Severe Fragmentation Detected:** Multiple disconnected regions suggest possible segmentation artifacts or severe anatomical variations.")891 recommendations.append("**URGENT:** Manual review and correction required. Fragmentation indicates potential segmentation errors.")892 elif morphology["largest_component_ratio"] < 0.95:893 if severity == "normal":894 severity = "moderate"895 status = "WARNING"896 findings.append(f"**Segmentation Quality:** The liver segmentation identified **{morphology['connected_components']} separate connected components**. The largest component represents **{morphology['largest_component_ratio']*100:.1f}%** of the total segmented volume.")897 quality_assessment.append("**Moderate Fragmentation Detected:** Multiple disconnected regions suggest possible segmentation artifacts or anatomical variations.")898 quality_assessment.append("Post-processing filters (largest-component selection, hole-filling, morphological operations) have been applied to optimize the segmentation.")899 recommendations.append("Review the segmentation overlay carefully. The presence of multiple components may indicate:")900 recommendations.append(" • Segmentation artifacts requiring manual correction")901 recommendations.append(" • Anatomical variants (e.g., accessory liver lobes)")902 recommendations.append(" • Pathological processes causing liver fragmentation")903 else:904 findings.append(f"**Segmentation Quality:** The liver segmentation shows **{morphology['connected_components']} components**, with the largest component comprising **{morphology['largest_component_ratio']*100:.1f}%** of the total volume, indicating good segmentation continuity.")905 quality_assessment.append("The segmentation demonstrates good connectivity with a dominant main component.")906 else:907 quality_assessment.append("**Excellent Segmentation Quality:** Single connected component indicates robust segmentation with good anatomical continuity.")908 909 if morphology["fragmentation"] == "high":910 findings.append(f"**High Fragmentation Detected:** The liver segmentation demonstrates high morphological fragmentation, which may reflect irregular liver surface or segmentation challenges.")911 quality_assessment.append("High fragmentation suggests the liver may have irregular borders or that the segmentation encountered challenging anatomical features.")912 recommendations.append("Manual review and potential refinement of the segmentation may be beneficial for optimal clinical interpretation.")913 if severity == "normal":914 severity = "mild"915 elif morphology["fragmentation"] == "moderate":916 quality_assessment.append("Moderate fragmentation observed, which is acceptable for clinical use but may benefit from review.")917 else:918 quality_assessment.append("Low fragmentation indicates smooth, well-defined liver boundaries.")919 920 if liver_percentage < normal_liver_percentage_range[0]:921 findings.append(f"**Spatial Distribution:** The liver occupies **{liver_percentage:.2f}%** of the total scan volume, which is below the typical range of {normal_liver_percentage_range[0]}-{normal_liver_percentage_range[1]}%.")922 clinical_notes.append("This may reflect a smaller liver relative to the field of view, or indicate that the scan includes a larger portion of the abdomen.")923 elif liver_percentage > normal_liver_percentage_range[1]:924 findings.append(f"**Spatial Distribution:** The liver occupies **{liver_percentage:.2f}%** of the total scan volume, which is above the typical range.")925 clinical_notes.append("This may indicate an enlarged liver or a scan field of view focused on the upper abdomen.")926 else:927 findings.append(f"**Spatial Distribution:** The liver occupies **{liver_percentage:.2f}%** of the scan volume, within the expected range.")928 929 if total_voxels > 0:930 voxel_density = liver_voxels / total_voxels * 100931 quality_assessment.append(f"**Segmentation Coverage:** {liver_voxels:,} voxels segmented out of {total_voxels:,} total voxels ({voxel_density:.2f}% coverage).")932 933 if volume_shape:934 quality_assessment.append(f"**Image Dimensions:** {volume_shape[0]} × {volume_shape[1]} × {volume_shape[2]} voxels")935 936 impression_parts = []937 if severity == "normal":938 impression_parts.append("Automated liver segmentation completed successfully using the SRMA-Mamba deep learning model.")939 impression_parts.append("The segmentation demonstrates good quality with measurements within expected physiological ranges.")940 elif severity == "mild":941 impression_parts.append("Automated liver segmentation completed with minor findings.")942 impression_parts.append("The segmentation is generally acceptable but requires clinical correlation and visual review.")943 elif severity == "moderate":944 impression_parts.append("Automated liver segmentation completed with notable findings requiring attention.")945 impression_parts.append("Visual inspection and clinical correlation are recommended to ensure accuracy.")946 elif severity == "critical":947 impression_parts.append("Automated liver segmentation completed with critical findings.")948 impression_parts.append("Immediate visual inspection and manual review are strongly recommended.")949 950 impression_parts.append(f"**{len(findings)} key finding(s)** identified during automated analysis.")951 952 report = {953 "patient_id": "N/A",954 "study_date": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),955 "modality": modality.upper(),956 "status": status,957 "findings": findings,958 "clinical_notes": clinical_notes,959 "quality_assessment": quality_assessment,960 "measurements": {961 "liver_volume_ml": round(volume_ml, 2),962 "liver_volume_liters": round(volume_ml / 1000.0, 3),963 "liver_percentage": round(liver_percentage, 2),964 "liver_voxels": int(liver_voxels),965 "total_voxels": int(total_voxels),966 "volume_shape": volume_shape,967 "morphology": morphology,968 "confidence_score": round(confidence_score, 1)969 },970 "impression": " ".join(impression_parts) if liver_voxels > 0 else impression_parts[0] if impression_parts else "Segmentation failed.",971 "recommendations": recommendations,972 "severity": severity,973 "methodology": "SRMA-Mamba: State Space Model for Medical Image Segmentation using Mamba architecture with sliding window inference",974 "disclaimer": "**IMPORTANT:** This is an automated analysis generated by artificial intelligence. Results should be reviewed and validated by a qualified radiologist or physician. This report is not intended for diagnostic use without appropriate clinical correlation and professional medical interpretation."975 }976 977 return report978 979 