Skip to content
Merged

Dev #80

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ patch_config.json
# Patch-workflow run byproducts (regenerated by the notebooks; e.g. research/*)
preprocessed/
cv_results_*/
inference_patch_config.json

# Claude - share skills/settings, keep local config private
.claude/settings.local.json
Expand Down
2 changes: 2 additions & 0 deletions fastMONAI/_modidx.py
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,8 @@
'fastMONAI.vision_plot': { 'fastMONAI.vision_plot._get_slice': ('vision_plot.html#_get_slice', 'fastMONAI/vision_plot.py'),
'fastMONAI.vision_plot.find_max_slice': ( 'vision_plot.html#find_max_slice',
'fastMONAI/vision_plot.py'),
'fastMONAI.vision_plot.show_mask_overlay': ( 'vision_plot.html#show_mask_overlay',
'fastMONAI/vision_plot.py'),
'fastMONAI.vision_plot.show_med_img': ('vision_plot.html#show_med_img', 'fastMONAI/vision_plot.py'),
'fastMONAI.vision_plot.show_segmentation_comparison': ( 'vision_plot.html#show_segmentation_comparison',
'fastMONAI/vision_plot.py'),
Expand Down
1 change: 1 addition & 0 deletions fastMONAI/vision_all.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#imports all of the functions and classes from the fastMONAI library
from .vision_plot import show_segmentation_comparison
from .vision_core import *
from .vision_data import *
from .vision_augmentation import *
Expand Down
56 changes: 55 additions & 1 deletion fastMONAI/vision_plot.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/00_vision_plot.ipynb.

# %% auto #0
__all__ = ['validate_anatomical_plane', 'show_med_img', 'find_max_slice', 'show_segmentation_comparison']
__all__ = ['validate_anatomical_plane', 'show_med_img', 'find_max_slice', 'show_segmentation_comparison', 'show_mask_overlay']

# %% ../nbs/00_vision_plot.ipynb #7ea97ee7-8a55-485d-a75c-ae1a8055f489
import warnings
Expand Down Expand Up @@ -196,3 +196,57 @@ def show_segmentation_comparison(
axes[2].axis('off')

plt.tight_layout()

# %% ../nbs/00_vision_plot.ipynb #72d68ebd
def show_mask_overlay(image, mask, ctx=None, channel: int = 0, slice_index: int = None,
anatomical_plane: int = 2, voxel_size=None, alpha: float = 0.5,
cmap_img: str = 'gray', cmap_mask: str = 'autumn',
ax=None, figsize=None, title=None, **kwargs):
"""Overlay `mask` on `image` on a single axis, using the same slicing and orientation as `.show()`.

Draws the image, then the mask on top with zero voxels transparent, so it lines up with
`MedImage.show()` / `MedMask.show()`. Reuses the same `_get_slice` those methods use.

Args:
image: Background image (MedImage or object with `.data` [C, H, W, D]).
mask: Overlay mask (MedMask or object with `.data` [C, H, W, D]); zero voxels are transparent.
ctx: Axis to draw on (fastai-style context); same role as `ax`.
channel: Channel to display.
slice_index: Slice to show; if None, uses `find_max_slice(mask, anatomical_plane)`.
anatomical_plane: 0=sagittal, 1=coronal, 2=axial (default).
voxel_size: Voxel spacing for aspect ratio; if None, aspect defaults to 1.
alpha: Overlay opacity.
cmap_img: Colormap for the background image.
cmap_mask: Colormap for the mask overlay.
ax: Axis to draw on (overrides `ctx`).
figsize: Figure size when a new axis is created.
title: Optional axis title.
kwargs: Extra args forwarded to the background image `imshow`.

Returns:
The matplotlib axis with the overlay.
"""
validate_anatomical_plane(anatomical_plane)

img_data = image.data if hasattr(image, 'data') else image
mask_data = mask.data if hasattr(mask, 'data') else mask
if hasattr(img_data, 'cpu'): img_data = img_data.cpu()
if hasattr(mask_data, 'cpu'): mask_data = mask_data.cpu()

if slice_index is None:
slice_index = find_max_slice(mask_data[channel].numpy(), anatomical_plane)

img_slice, aspect = _get_slice(img_data, channel, slice_index, anatomical_plane, voxel_size)
mask_slice, _ = _get_slice(mask_data, channel, slice_index, anatomical_plane, voxel_size)

ax = ax if ax is not None else ctx
if ax is None:
_, ax = plt.subplots(figsize=figsize)

ax.imshow(img_slice, cmap=cmap_img, aspect=aspect, **kwargs)
ax.imshow(np.ma.masked_where(mask_slice == 0, mask_slice), cmap=cmap_mask, alpha=alpha, aspect=aspect)

if title is not None:
ax.set_title(title)
ax.axis('off')
return ax
8 changes: 8 additions & 0 deletions nbs/00_vision_plot.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,14 @@
"metadata": {},
"outputs": [],
"source": "#| export\ndef show_segmentation_comparison(\n image, ground_truth, prediction,\n slice_index: int = None,\n anatomical_plane: int = 2,\n metric_value: float = None,\n metric_name: str = 'DSC',\n channel: int = 0,\n figsize: tuple = (15, 5),\n cmap_img: str = 'gray',\n cmap_mask: str = 'gray',\n voxel_size = None\n):\n \"\"\"Display 3-panel comparison: Image | Ground Truth | Prediction.\n \n Useful for validating segmentation results, especially after patch-based \n inference where results are not in standard fastai batch format.\n \n Args:\n image: Input image (MedImage, MedMask, or tensor [C, H, W, D])\n ground_truth: Ground truth mask (MedMask or tensor [C, H, W, D])\n prediction: Predicted mask (tensor [C, H, W, D])\n slice_index: Slice to display. If None, uses find_max_slice on ground_truth.\n anatomical_plane: 0=sagittal, 1=coronal, 2=axial (default)\n metric_value: Optional metric value to display in prediction title\n metric_name: Name of metric for title (default 'DSC')\n channel: Channel to display for multi-channel data (default 0)\n figsize: Figure size (default (15, 5))\n cmap_img: Colormap for image (default 'gray')\n cmap_mask: Colormap for masks (default 'gray')\n voxel_size: Voxel spacing for aspect ratio. If None, aspect=1.\n\n Example::\n \n # After patch_inference()\n show_segmentation_comparison(\n image=val_img,\n ground_truth=val_gt,\n prediction=predictions[0],\n metric_value=results_df.iloc[0]['DSC'],\n anatomical_plane=2\n )\n \"\"\"\n validate_anatomical_plane(anatomical_plane)\n \n img_data = image.data if hasattr(image, 'data') else image\n gt_data = ground_truth.data if hasattr(ground_truth, 'data') else ground_truth\n pred_data = prediction.data if hasattr(prediction, 'data') else prediction\n \n if hasattr(img_data, 'cpu'): img_data = img_data.cpu()\n if hasattr(gt_data, 'cpu'): gt_data = gt_data.cpu()\n if hasattr(pred_data, 'cpu'): pred_data = pred_data.cpu()\n \n if slice_index is None:\n gt_np = gt_data[channel].numpy()\n slice_index = find_max_slice(gt_np, anatomical_plane)\n \n fig, axes = plt.subplots(1, 3, figsize=figsize)\n \n img_slice, img_aspect = _get_slice(img_data, channel, slice_index, anatomical_plane, voxel_size)\n gt_slice, gt_aspect = _get_slice(gt_data, channel, slice_index, anatomical_plane, voxel_size)\n pred_slice, pred_aspect = _get_slice(pred_data, channel, slice_index, anatomical_plane, voxel_size)\n \n axes[0].imshow(img_slice, cmap=cmap_img, aspect=img_aspect)\n axes[0].set_title('Input Image')\n axes[0].axis('off')\n \n axes[1].imshow(gt_slice, cmap=cmap_mask, aspect=gt_aspect)\n axes[1].set_title('Ground Truth')\n axes[1].axis('off')\n \n pred_title = 'Prediction'\n if metric_value is not None:\n pred_title = f'Prediction ({metric_name}: {metric_value:.4f})'\n \n axes[2].imshow(pred_slice, cmap=cmap_mask, aspect=pred_aspect)\n axes[2].set_title(pred_title)\n axes[2].axis('off')\n \n plt.tight_layout()"
},
{
"cell_type": "code",
"execution_count": null,
"id": "72d68ebd",
"metadata": {},
"outputs": [],
"source": "#| export\ndef show_mask_overlay(image, mask, ctx=None, channel: int = 0, slice_index: int = None,\n anatomical_plane: int = 2, voxel_size=None, alpha: float = 0.5,\n cmap_img: str = 'gray', cmap_mask: str = 'autumn',\n ax=None, figsize=None, title=None, **kwargs):\n \"\"\"Overlay `mask` on `image` on a single axis, using the same slicing and orientation as `.show()`.\n\n Draws the image, then the mask on top with zero voxels transparent, so it lines up with\n `MedImage.show()` / `MedMask.show()`. Reuses the same `_get_slice` those methods use.\n\n Args:\n image: Background image (MedImage or object with `.data` [C, H, W, D]).\n mask: Overlay mask (MedMask or object with `.data` [C, H, W, D]); zero voxels are transparent.\n ctx: Axis to draw on (fastai-style context); same role as `ax`.\n channel: Channel to display.\n slice_index: Slice to show; if None, uses `find_max_slice(mask, anatomical_plane)`.\n anatomical_plane: 0=sagittal, 1=coronal, 2=axial (default).\n voxel_size: Voxel spacing for aspect ratio; if None, aspect defaults to 1.\n alpha: Overlay opacity.\n cmap_img: Colormap for the background image.\n cmap_mask: Colormap for the mask overlay.\n ax: Axis to draw on (overrides `ctx`).\n figsize: Figure size when a new axis is created.\n title: Optional axis title.\n kwargs: Extra args forwarded to the background image `imshow`.\n\n Returns:\n The matplotlib axis with the overlay.\n \"\"\"\n validate_anatomical_plane(anatomical_plane)\n\n img_data = image.data if hasattr(image, 'data') else image\n mask_data = mask.data if hasattr(mask, 'data') else mask\n if hasattr(img_data, 'cpu'): img_data = img_data.cpu()\n if hasattr(mask_data, 'cpu'): mask_data = mask_data.cpu()\n\n if slice_index is None:\n slice_index = find_max_slice(mask_data[channel].numpy(), anatomical_plane)\n\n img_slice, aspect = _get_slice(img_data, channel, slice_index, anatomical_plane, voxel_size)\n mask_slice, _ = _get_slice(mask_data, channel, slice_index, anatomical_plane, voxel_size)\n\n ax = ax if ax is not None else ctx\n if ax is None:\n _, ax = plt.subplots(figsize=figsize)\n\n ax.imshow(img_slice, cmap=cmap_img, aspect=aspect, **kwargs)\n ax.imshow(np.ma.masked_where(mask_slice == 0, mask_slice), cmap=cmap_mask, alpha=alpha, aspect=aspect)\n\n if title is not None:\n ax.set_title(title)\n ax.axis('off')\n return ax"
}
],
"metadata": {
Expand Down
Loading
Loading