Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 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
Binary file added monai/docs/clinical_dicom_workflow.pdf
Binary file not shown.
63 changes: 63 additions & 0 deletions monai/tests/test_clinical_preprocessing.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"id": "30e4219e",
"metadata": {},
"outputs": [],
"source": [
"\"\"\"\n",
"Unit tests for clinical_preprocessing.py\n",
"Author: Hitendrasinh Rathod\n",
"\"\"\"\n",
"\n",
"import numpy as np\n",
"import pytest\n",
"from monai.transforms import CTWindowingTransform, MRINormalizationTransform\n",
"\n",
"def test_ct_windowing():\n",
" \"\"\"\n",
" Test the CTWindowingTransform to ensure output is scaled between 0 and 1\n",
" and the shape is preserved.\n",
" \"\"\"\n",
" # Mock CT image with Hounsfield Units\n",
" sample_ct = np.random.randint(-1024, 2048, size=(64, 64, 64), dtype=np.int16)\n",
"\n",
" transform = CTWindowingTransform()\n",
" output = transform(sample_ct)\n",
"\n",
" # Output must be in [0,1]\n",
" assert output.min() >= 0.0\n",
" assert output.max() <= 1.0\n",
" # Shape should be preserved\n",
" assert output.shape == sample_ct.shape\n",
"\n",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Assertions may break depending on transform return type (numpy vs torch/MetaTensor) + RNG flakiness.

  • Line 25/42: seed RNG for determinism.
  • Line 31-32: guard float comparisons with tolerance, and normalize output to a numpy array before min/max/mean/std.
  • Line 52: require both mean≈0 and std≈1 (optionally on nonzero mask), not or.
 def test_ct_windowing():
+    rng = np.random.default_rng(0)
     # Mock CT image with Hounsfield Units
-    sample_ct = np.random.randint(-1024, 2048, size=(64, 64, 64), dtype=np.int16)
+    sample_ct = rng.integers(-1024, 2048, size=(64, 64, 64), dtype=np.int16)

     transform = CTWindowingTransform()
-    output = transform(sample_ct)
+    output = transform(sample_ct)
+    output = np.asarray(output)

     # Output must be in [0,1]
-    assert output.min() >= 0.0
-    assert output.max() <= 1.0
+    assert np.isfinite(output).all()
+    assert output.min() >= -1e-6
+    assert output.max() <= 1.0 + 1e-6
     # Shape should be preserved
     assert output.shape == sample_ct.shape

 def test_mri_normalization():
+    rng = np.random.default_rng(0)
     # Mock MRI image with random float values
-    sample_mri = np.random.rand(64, 64, 64)
+    sample_mri = rng.random((64, 64, 64), dtype=np.float32)

     transform = MRINormalizationTransform()
-    output = transform(sample_mri)
+    output = transform(sample_mri)
+    output = np.asarray(output)

     # Shape should be preserved
     assert output.shape == sample_mri.shape
     # Values should be roughly normalized (mean near 0, std near 1)
-    mean_val = np.mean(output)
-    std_val = np.std(output)
-    assert np.isclose(mean_val, 0, atol=0.1) or np.isclose(std_val, 1, atol=0.1)
+    mean_val = float(np.mean(output))
+    std_val = float(np.std(output))
+    assert np.isclose(mean_val, 0.0, atol=0.1)
+    assert np.isclose(std_val, 1.0, atol=0.1)

Also applies to: 36-52

🤖 Prompt for AI Agents
In monai/tests/test_clinical_preprocessing.ipynb around lines 19 to 35 (and
likewise apply the same fix to lines 36 to 52), the test is flaky because the
RNG isn't seeded, comparisons assume a specific array type, and float
comparisons use strict equality; to fix: seed the RNG at the start of each test
for determinism, convert the transform output to a numpy array (or unwrap
MetaTensor/torch tensor) before calling min/max/mean/std, use tolerant
comparisons (e.g. abs(a - b) <= tol) for float checks, and for the normalization
test require both mean ≈ 0 AND std ≈ 1 (optionally computed over a nonzero mask)
rather than using OR.

"def test_mri_normalization():\n",
" \"\"\"\n",
" Test the MRINormalizationTransform to ensure normalization works\n",
" and shape is preserved.\n",
" \"\"\"\n",
" # Mock MRI image with random float values\n",
" sample_mri = np.random.rand(64, 64, 64)\n",
"\n",
" transform = MRINormalizationTransform()\n",
" output = transform(sample_mri)\n",
"\n",
" # Shape should be preserved\n",
" assert output.shape == sample_mri.shape\n",
" # Values should be roughly normalized (mean near 0, std near 1)\n",
" mean_val = np.mean(output)\n",
" std_val = np.std(output)\n",
" assert np.isclose(mean_val, 0, atol=0.1) or np.isclose(std_val, 1, atol=0.1)\n"
]
}
],
"metadata": {
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
183 changes: 183 additions & 0 deletions monai/transforms/clinical_preprocessing.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "WK53GWYq4eo1",
"metadata": {
"id": "WK53GWYq4eo1"
},
"source": [
"# Clinical DICOM CT and MRI Preprocessing with MONAI\n",
"This notebook demonstrates inference-time preprocessing pipelines for CT and MRI DICOM series using MONAI, suitable for PACS/RIS workflows in hospital radiology environments.\n",
"**Note:** This notebook focuses on preprocessing only and excludes training or patient-identifiable data."
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "LTKh48zD4eo4",
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "LTKh48zD4eo4",
"outputId": "dcdc9430-1d52-4272-9079-8faba741099e"
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\u001b[?25l \u001b[90m\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u001b[0m \u001b[32m0.0/2.7 MB\u001b[0m \u001b[31m?\u001b[0m eta \u001b[36m-:--:--\u001b[0m\r\u001b[2K \u001b[91m\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u001b[0m\u001b[90m\u257a\u001b[0m\u001b[90m\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u001b[0m \u001b[32m1.9/2.7 MB\u001b[0m \u001b[31m53.8 MB/s\u001b[0m eta \u001b[36m0:00:01\u001b[0m\r\u001b[2K \u001b[91m\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u001b[0m\u001b[91m\u2578\u001b[0m \u001b[32m2.7/2.7 MB\u001b[0m \u001b[31m43.7 MB/s\u001b[0m eta \u001b[36m0:00:01\u001b[0m\r\u001b[2K \u001b[90m\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u001b[0m \u001b[32m2.7/2.7 MB\u001b[0m \u001b[31m21.8 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n",
"\u001b[?25h\u001b[?25l \u001b[90m\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u001b[0m \u001b[32m0.0/2.4 MB\u001b[0m \u001b[31m?\u001b[0m eta \u001b[36m-:--:--\u001b[0m\r\u001b[2K \u001b[91m\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u001b[0m\u001b[91m\u2578\u001b[0m \u001b[32m2.4/2.4 MB\u001b[0m \u001b[31m147.2 MB/s\u001b[0m eta \u001b[36m0:00:01\u001b[0m\r\u001b[2K \u001b[90m\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u001b[0m \u001b[32m2.4/2.4 MB\u001b[0m \u001b[31m38.5 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n",
"\u001b[?25h"
]
}
],
"source": [
"# Install required packages\n",
"!pip install monai pydicom nibabel --quiet"
]
},
{
"cell_type": "markdown",
"id": "gyAtaTBP4eo7",
"metadata": {
"id": "gyAtaTBP4eo7"
},
"source": [
"## Import Libraries"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "k2DfCDZM4eo8",
"metadata": {
"id": "k2DfCDZM4eo8"
},
"outputs": [],
"source": [
"from monai.transforms import (\n",
" LoadImage,\n",
" EnsureChannelFirst,\n",
" ScaleIntensityRange,\n",
" NormalizeIntensity,\n",
" Compose\n",
")\n",
"import numpy as np"
]
},
{
"cell_type": "markdown",
"id": "HAxGJVgy4eo8",
"metadata": {
"id": "HAxGJVgy4eo8"
},
"source": [
"## Define Preprocessing Pipelines"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "cP-zDmqu4eo8",
"metadata": {
"id": "cP-zDmqu4eo8"
},
"outputs": [],
"source": [
"def get_ct_preprocessing_pipeline():\n",
" return Compose([\n",
" LoadImage(image_only=True),\n",
" EnsureChannelFirst(),\n",
" ScaleIntensityRange(a_min=-1000, a_max=400, b_min=0.0, b_max=1.0, clip=True)\n",
" ])\n",
"\n",
"def get_mri_preprocessing_pipeline():\n",
" return Compose([\n",
" LoadImage(image_only=True),\n",
" EnsureChannelFirst(),\n",
" NormalizeIntensity(nonzero=True)\n",
" ])"
]
},
{
"cell_type": "markdown",
"id": "OuRHidt_4eo9",
"metadata": {
"id": "OuRHidt_4eo9"
},
"source": [
"## Preprocessing Function"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "BcGeKvkc4eo-",
"metadata": {
"id": "BcGeKvkc4eo-"
},
"outputs": [],
"source": [
"def preprocess_dicom_series(dicom_path, modality):\n",
" modality = modality.upper()\n",
" if modality == 'CT':\n",
" transform = get_ct_preprocessing_pipeline()\n",
" elif modality == 'MRI':\n",
" transform = get_mri_preprocessing_pipeline()\n",
" else:\n",
" raise ValueError(\"Unsupported modality. Use 'CT' or 'MRI'.\")\n",
" image = transform(dicom_path)\n",
" return image"
]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Modality handling is too strict (DICOM uses MR) and input validation is missing.

  • Line 125: guard non-string modality.
  • Line 128-131: accept MR as synonym for MRI (and trim whitespace).
  • Line 131: Ruff TRY003—keep message short or factor to a constant.
 def preprocess_dicom_series(dicom_path, modality):
-    modality = modality.upper()
-    if modality == 'CT':
+    if not isinstance(modality, str):
+        raise TypeError("modality must be a string")
+    modality = modality.strip().upper()
+    if modality == "CT":
         transform = get_ct_preprocessing_pipeline()
-    elif modality == 'MRI':
+    elif modality in ("MR", "MRI"):
         transform = get_mri_preprocessing_pipeline()
     else:
-        raise ValueError("Unsupported modality. Use 'CT' or 'MRI'.")
+        raise ValueError("Unsupported modality")
     image = transform(dicom_path)
     return image
🤖 Prompt for AI Agents
In monai/transforms/clinical_preprocessing.ipynb around lines 124 to 134, the
modality handling is too strict and lacks input validation; update the function
to (1) validate modality is a string and raise a short constant error message if
not, (2) trim whitespace and uppercase the modality, treating "MR" as a synonym
for "MRI" before branching to select the CT or MRI pipeline, and (3) factor the
unsupported-modality message into a module-level constant (or keep it very
short) to satisfy Ruff TRY003; implement these changes so transform selection
accepts "MR" and non-string inputs are rejected cleanly.

},
{
"cell_type": "markdown",
"id": "3bWBPrp44eo-",
"metadata": {
"id": "3bWBPrp44eo-"
},
"source": [
"## Example Usage"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "VlZZgpHg4eo_",
"metadata": {
"id": "VlZZgpHg4eo_"
},
"outputs": [],
"source": [
"# Replace these paths with your own DICOM series paths\n",
"ct_dicom_path = '/path/to/ct/dicom/series'\n",
"mri_dicom_path = '/path/to/mri/dicom/series'\n",
"\n",
"ct_image = preprocess_dicom_series(ct_dicom_path, 'CT')\n",
"mri_image = preprocess_dicom_series(mri_dicom_path, 'MRI')\n",
"\n",
"print('CT image shape:', ct_image.shape)\n",
"print('MRI image shape:', mri_image.shape)"
]
}
],
"metadata": {
"colab": {
"provenance": []
},
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"name": "python",
"version": "3.10"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
Loading