diff --git a/.gitignore b/.gitignore index 4349e05..c62c900 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Project files .claude .coverage +*.code-workspace coverage.xml *~ *.swp diff --git a/experiments/Heart-Simpleware_Segmentation/.gitignore b/experiments/Heart-Simpleware_Segmentation/.gitignore new file mode 100644 index 0000000..40515a6 --- /dev/null +++ b/experiments/Heart-Simpleware_Segmentation/.gitignore @@ -0,0 +1,22 @@ +# Ignore results directory and temporary files +results/ +*.pyc +__pycache__/ +.ipynb_checkpoints/ +*.tmp +*.log + +# Ignore large data files +*.nii +*.nii.gz +*.mha +*.mhd +*.raw +*.dcm + +# Ignore visualization outputs +*.png +*.jpg +*.jpeg +*.mp4 +*.avi diff --git a/experiments/Heart-Simpleware_Segmentation/README.md b/experiments/Heart-Simpleware_Segmentation/README.md new file mode 100644 index 0000000..b561c40 --- /dev/null +++ b/experiments/Heart-Simpleware_Segmentation/README.md @@ -0,0 +1,299 @@ +# Heart Segmentation using Simpleware Medical + +This experiment demonstrates cardiac segmentation using Synopsys Simpleware Medical's ASCardio module integrated with PhysioMotion4D. + +## Overview + +The `SegmentHeartSimpleware` class provides integration between PhysioMotion4D and Synopsys Simpleware Medical for automated heart segmentation. This experiment shows how to: + +1. Load cardiac CT images +2. Segment heart structures using ASCardio +3. Visualize and analyze the results +4. Export data for further processing + +## Requirements + +### Software Requirements + +- **Synopsys Simpleware Medical X-2025.06 or later** + - Default installation path: `C:\Program Files\Synopsys\Simpleware Medical\X-2025.06\` + - Console mode: `ConsoleSimplewareMedical.exe` (command-line version) + - ASCardio module license required + +- **Python packages** (included in PhysioMotion4D environment): + - `itk` + - `numpy` + - `matplotlib` + - `pyvista` (optional, for 3D visualization) + +### Data Requirements + +- Cardiac CT image (3D volume) +- Recommended: Gated cardiac CT or high-resolution heart scan +- Format: NIfTI (.nii, .nii.gz) or any ITK-readable format +- Image should include complete heart anatomy + +## Files + +- **`simpleware_heart_segmentation.ipynb`**: Main demonstration notebook +- **`README.md`**: This file +- **`results/`**: Output directory (created automatically) + +## Usage + +### Quick Start + +1. Open `simpleware_heart_segmentation.ipynb` in Jupyter +2. Update the `input_image_path` in cell 3 to point to your cardiac CT image +3. Run all cells sequentially + +### Configuration + +Before running, configure these parameters in the notebook: + +```python +# Set input image path +input_image_path = "/path/to/your/cardiac_ct.nii.gz" + +# Set custom Simpleware path (if not default) +custom_simpleware_path = "D:/CustomPath/Simpleware/ConsoleSimplewareMedical.exe" +``` + +### Expected Output + +The notebook generates: + +1. **Segmentation files** (in `results/`): + - `heart_labelmap_simpleware.nii.gz` - Complete labelmap with all structures + - `heart_mask_simpleware.nii.gz` - Binary mask of heart structures + - `vessels_mask_simpleware.nii.gz` - Binary mask of major vessels + - `contrast_mask_simpleware.nii.gz` - Contrast-enhanced regions + +2. **Visualizations**: + - 2D slice views with segmentation overlays + - 3D surface renderings (if PyVista available) + - Statistical analysis tables + +## Segmented Structures + +### Heart Structures (Label IDs 1-6) + +- **1**: Left Ventricle (LV) +- **2**: Right Ventricle (RV) +- **3**: Left Atrium (LA) +- **4**: Right Atrium (RA) +- **5**: Myocardium +- **6**: Heart (combined heart mask) + +### Major Vessels (Label IDs 7-10) + +- **7**: Aorta +- **8**: Pulmonary Artery +- **9**: Right Coronary Artery +- **10**: Left Coronary Artery + +## Workflow Details + +### Step-by-Step Process + +1. **Preprocessing** (automatic): + - Resampling to 1.0mm isotropic spacing + - Intensity normalization if needed + +2. **Simpleware Integration**: + - Input image saved to a temporary NIfTI file + - ConsoleSimplewareMedical.exe is launched with `--input-file` (NIfTI) and `--input-value` (output directory) + - The Simpleware script runs ASCardio on the loaded image and exports per-structure masks as MHD + - PhysioMotion4D assembles the labelmap from the mask files and returns results + +3. **Postprocessing** (automatic): + - Labelmap resampled to original image space + - Anatomical masks created (heart, vessels, etc.) + - Optional contrast agent detection + +4. **Analysis & Visualization**: + - Volume calculations for each structure + - 2D slice visualization + - 3D surface rendering + +### Processing Time + +Typical processing times: +- Small CT (256³ voxels): 2-5 minutes +- Medium CT (512³ voxels): 5-10 minutes +- Large CT (1024³ voxels): 10-20 minutes + +Times depend on image size, system performance, and Simpleware configuration. + +## Troubleshooting + +### Common Issues + +**Issue**: `FileNotFoundError: Simpleware Medical executable not found` + +**Solution**: +- Verify Simpleware installation +- Ensure you're using `ConsoleSimplewareMedical.exe` (not `SimplewareMedical.exe`) +- Update `custom_simpleware_path` in the notebook +- Check default path: `C:\Program Files\Synopsys\Simpleware Medical\X-2025.06\ConsoleSimplewareMedical.exe`; use `set_simpleware_executable_path()` if installed elsewhere + +--- + +**Issue**: `unrecognised option '-python'` or similar command-line errors + +**Solution**: +- Use `ConsoleSimplewareMedical.exe` (console version), not `SimplewareMedical.exe` (GUI version) +- The script is automatically called with `--run-script` flag + +--- + +**Issue**: `ImportError: Failed to import Simpleware modules` + +**Solution**: +- Ensure ASCardio module is licensed +- Verify Simpleware version is X-2025.06 or later +- Check that console mode is properly installed + +--- + +**Issue**: `WARNING: No segmentation masks were created` or missing masks + +**Solution**: +- Ensure the input NIfTI is passed correctly via `--input-file` so Simpleware has an active document +- Check that ASCardio completed successfully (inspect Simpleware stdout in debug logging) +- Verify input image contains clear heart anatomy and adequate contrast + +--- + +**Issue**: Input image quality issues + +**Solution**: +- Verify input image contains heart anatomy +- Check image contrast and quality +- Ensure proper field of view (complete heart visible) +- Try adjusting image preprocessing parameters + +--- + +**Issue**: Segmentation timeout after 600 seconds + +**Solution**: +- Image may be too large; consider downsampling +- Check system resources (CPU, RAM) +- Increase timeout in `segment_heart_simpleware.py` if needed + +--- + +**Issue**: Poor segmentation quality + +**Solution**: +- Ensure image has adequate contrast +- Use contrast-enhanced CT if available +- Check that heart is centered in field of view +- Verify image orientation is correct + +### Debug Mode + +Enable detailed logging: + +```python +import logging +segmenter = SegmentHeartSimpleware(log_level=logging.DEBUG) +``` + +This provides: +- Detailed subprocess output +- Simpleware script messages +- File I/O operations +- Timing information + +## Integration with Other Workflows + +This experiment can be combined with other PhysioMotion4D workflows: + +### 4D Heart Animation +Use segmentation results with `Heart-GatedCT_To_USD` workflow: +```python +# After segmentation +from physiomotion4d.workflow_convert_heart_gated_ct_to_usd import WorkflowConvertHeartGatedCTToUSD + +workflow = WorkflowConvertHeartGatedCTToUSD() +workflow.set_static_labelmap(result["labelmap"]) +# Continue with 4D USD generation +``` + +### Statistical Model Registration +Register segmentation with heart model using `Heart-Statistical_Model_To_Patient`: +```python +from physiomotion4d.workflow_register_heart_model_to_patient import WorkflowRegisterHeartModelToPatient + +workflow = WorkflowRegisterHeartModelToPatient() +workflow.set_patient_segmentation(result["labelmap"]) +# Perform model-to-patient registration +``` + +### Custom Analysis +Extract specific structures for analysis: +```python +# Get left ventricle only +lv_mask = np.where(labelmap_array == 1, 1, 0) + +# Calculate LV volume +lv_volume_ml = np.sum(lv_mask) * voxel_volume / 1000 + +# Create mesh for computational modeling +from physiomotion4d.convert_vtk_to_usd import create_mesh_from_mask +lv_mesh = create_mesh_from_mask(lv_mask) +``` + +## Performance Optimization + +### For Faster Processing + +1. **Reduce image resolution**: +```python +# Before segmentation +segmenter.set_target_spacing(2.0) # Use 2mm instead of 1mm +``` + +2. **Use region of interest**: +```python +# Crop image to heart region before segmentation +from physiomotion4d.image_tools import crop_to_roi +cropped_image = crop_to_roi(input_image, roi_bounds) +``` + +## References + +- **Simpleware Medical Documentation**: See Synopsys user manual +- **ASCardio Module**: Refer to ASCardio technical documentation +- **PhysioMotion4D**: Main repository documentation + +## Citation + +If using this integration in research, please cite: +- Synopsys Simpleware Medical +- PhysioMotion4D framework +- Any relevant papers using ASCardio segmentation + +## License + +This integration code follows the PhysioMotion4D license. Simpleware Medical and ASCardio are commercial products requiring separate licenses from Synopsys. + +## Support + +For issues with: +- **PhysioMotion4D integration**: Submit issue to PhysioMotion4D repository +- **Simpleware Medical/ASCardio**: Contact Synopsys support +- **This experiment**: Check troubleshooting section above + +## Version History + +- **v0.2.0** (2026-02-06): Documentation and alignment with current implementation + - README reflects actual notebook name (`simpleware_heart_segmentation.ipynb`) and correct label IDs (heart 1–6, vessels 7–10) + - Workflow description updated for `--input-file` (NIfTI) and `--input-value` (output dir) invocation + - Removed obsolete “placeholder output” limitation; integration works as expected +- **v0.1.0** (2026-02-04): Initial implementation + - Basic ASCardio integration + - Heart and vessel segmentation + - 2D/3D visualization diff --git a/experiments/Heart-Simpleware_Segmentation/simpleware_heart_segmentation.ipynb b/experiments/Heart-Simpleware_Segmentation/simpleware_heart_segmentation.ipynb new file mode 100644 index 0000000..d222e68 --- /dev/null +++ b/experiments/Heart-Simpleware_Segmentation/simpleware_heart_segmentation.ipynb @@ -0,0 +1,539 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "a1b5c8f8", + "metadata": {}, + "source": [ + "# Heart Segmentation using Simpleware Medical ASCardio\n", + "\n", + "This notebook demonstrates the use of the `SegmentHeartSimpleware` class to perform automated cardiac segmentation using Synopsys Simpleware Medical's ASCardio module.\n", + "\n", + "## Requirements\n", + "\n", + "- Synopsys Simpleware Medical X-2025.06 or later installed\n", + "- ASCardio module license\n", + "- Cardiac CT image (gated or high-resolution)\n", + "\n", + "## Overview\n", + "\n", + "The `SegmentHeartSimpleware` class provides:\n", + "- Automated heart chamber segmentation (LV, RV, LA, RA)\n", + "- Myocardium segmentation\n", + "- Major vessel segmentation (aorta, pulmonary artery, coronary arteries)\n", + "- Integration with PhysioMotion4D workflows" + ] + }, + { + "cell_type": "markdown", + "id": "f5e8d2f1", + "metadata": {}, + "source": [ + "## 1. Setup and Imports" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3ce61753", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import logging\n", + "\n", + "import itk\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import pyvista as pv\n", + "\n", + "from physiomotion4d.segment_heart_simpleware import SegmentHeartSimpleware" + ] + }, + { + "cell_type": "markdown", + "id": "b60954cf", + "metadata": {}, + "source": [ + "## 2. Configuration" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5c2f5e00", + "metadata": {}, + "outputs": [], + "source": [ + "# Directory setup\n", + "output_dir = \"./results\"\n", + "os.makedirs(output_dir, exist_ok=True)\n", + "\n", + "# Optional: Set custom Simpleware path if not in default location\n", + "custom_simpleware_path = None\n", + "# Example:\n", + "# custom_simpleware_path = \"D:/Synopsys/Simpleware/ConsoleSimplewareMedical.exe\"\n", + "\n", + "# Enable detailed logging\n", + "log_level = logging.INFO # Change to logging.DEBUG for more detail" + ] + }, + { + "cell_type": "markdown", + "id": "9438634d", + "metadata": {}, + "source": [ + "## 3. Load Input CT Image\n", + "\n", + "Load a cardiac CT image for segmentation. This should be a 3D volume containing the heart." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a7b5c8e1", + "metadata": {}, + "outputs": [], + "source": [ + "input_image_path = \"../../data/CHOP-Valve4D/CT/RVOT28-Dias.nii.gz\"\n", + "\n", + "# Load the image\n", + "try:\n", + " input_image = itk.imread(input_image_path)\n", + " print(f\"Successfully loaded image from: {input_image_path}\")\n", + " print(f\"Image size: {itk.size(input_image)}\")\n", + " print(f\"Image spacing: {input_image.GetSpacing()}\")\n", + " print(f\"Image origin: {input_image.GetOrigin()}\")\n", + "except (FileNotFoundError, OSError) as e:\n", + " print(f\"Error loading image: {e}\")\n", + " print(\"Please update the input_image_path to point to a valid cardiac CT image.\")\n", + " input_image = None" + ] + }, + { + "cell_type": "markdown", + "id": "c2d8f3e2", + "metadata": {}, + "source": [ + "## 4. Visualize Input Image\n", + "\n", + "Display a few slices of the input image to verify it loaded correctly." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d1e9f4e3", + "metadata": {}, + "outputs": [], + "source": [ + "if input_image is not None:\n", + " # Get numpy array from ITK image\n", + " image_array = itk.array_from_image(input_image)\n", + "\n", + " # Display axial, sagittal, and coronal slices\n", + " fig, axes = plt.subplots(1, 3, figsize=(15, 5))\n", + "\n", + " # Axial slice (middle)\n", + " axial_slice = image_array[image_array.shape[0] // 2, :, :]\n", + " axes[0].imshow(axial_slice, cmap=\"gray\", vmin=-200, vmax=400)\n", + " axes[0].set_title(\"Axial View\")\n", + " axes[0].axis(\"off\")\n", + "\n", + " # Sagittal slice (middle)\n", + " sagittal_slice = image_array[::-1, :, image_array.shape[2] // 2]\n", + " axes[1].imshow(sagittal_slice, cmap=\"gray\", vmin=-200, vmax=400)\n", + " axes[1].set_title(\"Sagittal View\")\n", + " axes[1].axis(\"off\")\n", + "\n", + " # Coronal slice (middle)\n", + " coronal_slice = image_array[::-1, image_array.shape[1] // 2, :]\n", + " axes[2].imshow(coronal_slice, cmap=\"gray\", vmin=-200, vmax=400)\n", + " axes[2].set_title(\"Coronal View\")\n", + " axes[2].axis(\"off\")\n", + "\n", + " plt.tight_layout()\n", + " plt.show()\n", + "\n", + " print(f\"Image intensity range: [{image_array.min():.1f}, {image_array.max():.1f}]\")\n", + "else:\n", + " print(\"No input image available for visualization.\")" + ] + }, + { + "cell_type": "markdown", + "id": "e3f5g6h7", + "metadata": {}, + "source": [ + "## 5. Initialize Simpleware Segmentation\n", + "\n", + "Create an instance of the `SegmentHeartSimpleware` class." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f4g5h6i7", + "metadata": {}, + "outputs": [], + "source": [ + "# Create segmenter instance with logging\n", + "segmenter = SegmentHeartSimpleware(log_level=log_level)\n", + "\n", + "# Set custom Simpleware path if specified\n", + "if custom_simpleware_path is not None:\n", + " print(f\"Setting custom Simpleware path: {custom_simpleware_path}\")\n", + " segmenter.set_simpleware_executable_path(custom_simpleware_path)\n", + "else:\n", + " print(f\"Using default Simpleware path: {segmenter.simpleware_exe_path}\")\n", + "\n", + "# Display segmentation configuration\n", + "print(\"\\nSegmentation Configuration:\")\n", + "print(f\" Target spacing: {segmenter.target_spacing} mm\")\n", + "print(f\" Heart structures: {len(segmenter.heart_mask_ids)} labels\")\n", + "print(f\" Vessel structures: {len(segmenter.major_vessels_mask_ids)} labels\")\n", + "\n", + "print(\"\\nHeart Structure IDs:\")\n", + "for id, name in segmenter.heart_mask_ids.items():\n", + " print(f\" {id}: {name}\")\n", + "\n", + "print(\"\\nMajor Vessel IDs:\")\n", + "for id, name in segmenter.major_vessels_mask_ids.items():\n", + " print(f\" {id}: {name}\")" + ] + }, + { + "cell_type": "markdown", + "id": "g5h6i7j8", + "metadata": {}, + "source": [ + "## 6. Run Segmentation\n", + "\n", + "Perform the heart segmentation using Simpleware Medical ASCardio.\n", + "\n", + "**Note**: This step calls Simpleware Medical as an external process and may take several minutes depending on image size and system performance." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "h6i7j8k9", + "metadata": {}, + "outputs": [], + "source": [ + "if input_image is not None:\n", + " print(\"Starting heart segmentation with Simpleware Medical ASCardio...\")\n", + " print(\"This may take several minutes. Please wait...\\n\")\n", + "\n", + " try:\n", + " # Perform segmentation\n", + " # Set contrast_enhanced_study=True if your CT scan used contrast agent\n", + " result = segmenter.segment(input_image, contrast_enhanced_study=True)\n", + "\n", + " print(\"\\nSegmentation completed successfully!\")\n", + "\n", + " # Extract individual results\n", + " labelmap_image = result[\"labelmap\"]\n", + " heart_mask = result[\"heart\"]\n", + " major_vessels_mask = result[\"major_vessels\"]\n", + " contrast_mask = result[\"contrast\"]\n", + "\n", + " # Save results\n", + " print(\"\\nSaving segmentation results...\")\n", + " itk.imwrite(\n", + " labelmap_image,\n", + " os.path.join(output_dir, \"heart_labelmap_simpleware.nii.gz\"),\n", + " compression=True,\n", + " )\n", + " itk.imwrite(\n", + " heart_mask,\n", + " os.path.join(output_dir, \"heart_mask_simpleware.nii.gz\"),\n", + " compression=True,\n", + " )\n", + " itk.imwrite(\n", + " major_vessels_mask,\n", + " os.path.join(output_dir, \"vessels_mask_simpleware.nii.gz\"),\n", + " compression=True,\n", + " )\n", + " itk.imwrite(\n", + " contrast_mask,\n", + " os.path.join(output_dir, \"contrast_mask_simpleware.nii.gz\"),\n", + " compression=True,\n", + " )\n", + "\n", + " except FileNotFoundError as e:\n", + " print(f\"\\nError: {e}\")\n", + " print(\"Please ensure Simpleware Medical is installed at the correct path.\")\n", + " result = None\n", + "\n", + " except RuntimeError as e:\n", + " print(f\"\\nSegmentation failed: {e}\")\n", + " print(\"Please check the error messages above for details.\")\n", + " result = None\n", + "\n", + " except Exception as e:\n", + " print(f\"\\nUnexpected error: {e}\")\n", + " result = None\n", + "\n", + "else:\n", + " print(\"No input image available. Skipping segmentation.\")\n", + " result = None" + ] + }, + { + "cell_type": "markdown", + "id": "i7j8k9l0", + "metadata": {}, + "source": [ + "## 7. Analyze Segmentation Results\n", + "\n", + "Display statistics about the segmented structures." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "j8k9l0m1", + "metadata": {}, + "outputs": [], + "source": [ + "if result is not None:\n", + " labelmap_array = itk.array_from_image(result[\"labelmap\"])\n", + "\n", + " # Get unique labels and their counts\n", + " unique_labels, label_counts = np.unique(labelmap_array, return_counts=True)\n", + "\n", + " # Calculate voxel volume\n", + " spacing = result[\"labelmap\"].GetSpacing()\n", + " voxel_volume_mm3 = spacing[0] * spacing[1] * spacing[2]\n", + "\n", + " print(\"\\n=== Segmentation Statistics ===\")\n", + " print(f\"\\nTotal unique labels found: {len(unique_labels) - 1}\") # -1 for background\n", + " print(f\"Voxel volume: {voxel_volume_mm3:.3f} mm³\")\n", + " print(\"\\nStructure Volumes:\")\n", + "\n", + " # Combine all mask dictionaries for label lookup\n", + " all_labels = {\n", + " **segmenter.heart_mask_ids,\n", + " **segmenter.major_vessels_mask_ids,\n", + " **segmenter.contrast_mask_ids,\n", + " }\n", + "\n", + " for label, count in zip(unique_labels, label_counts):\n", + " if label == 0: # Skip background\n", + " continue\n", + "\n", + " volume_mm3 = count * voxel_volume_mm3\n", + " volume_ml = volume_mm3 / 1000\n", + "\n", + " label_name = all_labels.get(label, f\"unknown_{label}\")\n", + " print(f\" {label_name} (ID {label}): {volume_ml:.2f} mL ({count:,} voxels)\")\n", + "\n", + " # Calculate combined volumes\n", + " heart_array = itk.array_from_image(result[\"heart\"])\n", + " vessels_array = itk.array_from_image(result[\"major_vessels\"])\n", + "\n", + " heart_volume_ml = (np.sum(heart_array > 0) * voxel_volume_mm3) / 1000\n", + " vessels_volume_ml = (np.sum(vessels_array > 0) * voxel_volume_mm3) / 1000\n", + "\n", + " print(\"\\nCombined Volumes:\")\n", + " print(f\" Total heart structures: {heart_volume_ml:.2f} mL\")\n", + " print(f\" Total major vessels: {vessels_volume_ml:.2f} mL\")\n", + "else:\n", + " print(\"No segmentation results available for analysis.\")" + ] + }, + { + "cell_type": "markdown", + "id": "k9l0m1n2", + "metadata": {}, + "source": [ + "## 8. Visualize Segmentation Results (2D)\n", + "\n", + "Display the segmentation overlaid on the original image." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "l0m1n2o3", + "metadata": {}, + "outputs": [], + "source": [ + "if result is not None and input_image is not None:\n", + " # Get arrays\n", + " image_array = itk.array_from_image(input_image)\n", + " labelmap_array = itk.array_from_image(result[\"labelmap\"])\n", + " heart_array = itk.array_from_image(result[\"heart\"])\n", + " vessels_array = itk.array_from_image(result[\"major_vessels\"])\n", + "\n", + " # Select middle slice\n", + " mid_slice = image_array.shape[0] // 2\n", + "\n", + " # Create figure\n", + " fig, axes = plt.subplots(2, 3, figsize=(18, 12))\n", + "\n", + " # Row 1: Original, labelmap, heart mask\n", + " axes[0, 0].imshow(image_array[mid_slice, :, :], cmap=\"gray\", vmin=-200, vmax=400)\n", + " axes[0, 0].set_title(\"Original Image (Axial)\")\n", + " axes[0, 0].axis(\"off\")\n", + "\n", + " axes[0, 1].imshow(image_array[mid_slice, :, :], cmap=\"gray\", vmin=-200, vmax=400)\n", + " labelmap_overlay = np.ma.masked_where(\n", + " labelmap_array[mid_slice, :, :] == 0, labelmap_array[mid_slice, :, :]\n", + " )\n", + " axes[0, 1].imshow(labelmap_overlay, cmap=\"jet\", alpha=0.5, vmin=1, vmax=10)\n", + " axes[0, 1].set_title(\"Labelmap Overlay\")\n", + " axes[0, 1].axis(\"off\")\n", + "\n", + " axes[0, 2].imshow(image_array[mid_slice, :, :], cmap=\"gray\", vmin=-200, vmax=400)\n", + " heart_overlay = np.ma.masked_where(\n", + " heart_array[mid_slice, :, :] == 0, heart_array[mid_slice, :, :]\n", + " )\n", + " axes[0, 2].imshow(heart_overlay, cmap=\"Reds\", alpha=0.5)\n", + " axes[0, 2].set_title(\"Heart Mask\")\n", + " axes[0, 2].axis(\"off\")\n", + "\n", + " # Row 2: Vessels, sagittal view, coronal view\n", + " axes[1, 0].imshow(image_array[mid_slice, :, :], cmap=\"gray\", vmin=-200, vmax=400)\n", + " vessels_overlay = np.ma.masked_where(\n", + " vessels_array[mid_slice, :, :] == 0, vessels_array[mid_slice, :, :]\n", + " )\n", + " axes[1, 0].imshow(vessels_overlay, cmap=\"Blues\", alpha=0.5)\n", + " axes[1, 0].set_title(\"Vessels Mask\")\n", + " axes[1, 0].axis(\"off\")\n", + "\n", + " # Sagittal view\n", + " mid_sagittal = image_array.shape[2] // 2\n", + " axes[1, 1].imshow(image_array[:, :, mid_sagittal], cmap=\"gray\", vmin=-200, vmax=400)\n", + " sagittal_overlay = np.ma.masked_where(\n", + " labelmap_array[:, :, mid_sagittal] == 0, labelmap_array[:, :, mid_sagittal]\n", + " )\n", + " axes[1, 1].imshow(sagittal_overlay, cmap=\"jet\", alpha=0.5, vmin=1, vmax=10)\n", + " axes[1, 1].set_title(\"Sagittal View\")\n", + " axes[1, 1].axis(\"off\")\n", + "\n", + " # Coronal view\n", + " mid_coronal = image_array.shape[1] // 2\n", + " axes[1, 2].imshow(image_array[:, mid_coronal, :], cmap=\"gray\", vmin=-200, vmax=400)\n", + " coronal_overlay = np.ma.masked_where(\n", + " labelmap_array[:, mid_coronal, :] == 0, labelmap_array[:, mid_coronal, :]\n", + " )\n", + " axes[1, 2].imshow(coronal_overlay, cmap=\"jet\", alpha=0.5, vmin=1, vmax=10)\n", + " axes[1, 2].set_title(\"Coronal View\")\n", + " axes[1, 2].axis(\"off\")\n", + "\n", + " plt.tight_layout()\n", + " plt.savefig(os.path.join(output_dir, \"segmentation_visualization.png\"), dpi=150)\n", + " plt.show()\n", + "\n", + " print(\n", + " f\"Visualization saved to: {os.path.join(output_dir, 'segmentation_visualization.png')}\"\n", + " )\n", + "else:\n", + " print(\"No results available for visualization.\")" + ] + }, + { + "cell_type": "markdown", + "id": "m1n2o3p4", + "metadata": {}, + "source": [ + "## 9. 3D Visualization (Optional)\n", + "\n", + "Create 3D surface meshes of the segmented structures using PyVista." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "n2o3p4q5", + "metadata": {}, + "outputs": [], + "source": [ + "print(\"Creating 3D visualization...\")\n", + "\n", + "# Create VTK images from ITK images for PyVista\n", + "\n", + "# Convert heart mask to VTK\n", + "heart_vtk = itk.vtk_image_from_image(result[\"heart\"])\n", + "vessels_vtk = itk.vtk_image_from_image(result[\"major_vessels\"])\n", + "\n", + "# Create PyVista plotter\n", + "plotter = pv.Plotter()\n", + "\n", + "# Extract heart surface\n", + "heart_grid = pv.wrap(heart_vtk)\n", + "heart_surface = heart_grid.contour([0.5])\n", + "if heart_surface.n_points > 0:\n", + " plotter.add_mesh(heart_surface, color=\"red\", opacity=1.0, label=\"Heart\")\n", + "\n", + "# Extract vessels surface\n", + "vessels_grid = pv.wrap(vessels_vtk)\n", + "vessels_surface = vessels_grid.contour([0.5])\n", + "if vessels_surface.n_points > 0:\n", + " plotter.add_mesh(vessels_surface, color=\"blue\", opacity=1.0, label=\"Vessels\")\n", + "\n", + "# Configure plotter\n", + "plotter.add_legend()\n", + "plotter.set_background(\"white\")\n", + "plotter.add_axes()\n", + "\n", + "# Save screenshot\n", + "screenshot_path = os.path.join(output_dir, \"3d_visualization.png\")\n", + "plotter.show(screenshot=screenshot_path)\n", + "\n", + "print(f\"3D visualization saved to: {screenshot_path}\")" + ] + }, + { + "cell_type": "markdown", + "id": "o3p4q5r6", + "metadata": {}, + "source": [ + "## 10. Summary\n", + "\n", + "This notebook demonstrated:\n", + "1. Loading a cardiac CT image\n", + "2. Initializing the `SegmentHeartSimpleware` class\n", + "3. Running ASCardio heart segmentation through Simpleware Medical\n", + "4. Analyzing segmentation results\n", + "5. Visualizing the segmented structures in 2D and 3D\n", + "\n", + "The segmentation results can be used for:\n", + "- Cardiac motion analysis\n", + "- 4D heart visualization in USD format\n", + "- Registration with statistical heart models\n", + "- Clinical measurements and analysis\n", + "\n", + "## Next Steps\n", + "\n", + "- Export segmentation to USD format for Omniverse visualization\n", + "- Register with statistical heart model (see `Heart-Statistical_Model_To_Patient`)\n", + "- Create 4D heart animation (see `Heart-GatedCT_To_USD`)\n", + "- Perform cardiac function analysis" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/pyproject.toml b/pyproject.toml index 2436fb8..9c63265 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -223,6 +223,7 @@ module = [ "pyvista.*", "scripts.*", "SimpleITK", + "simpleware.*", "torch", "torch.*", "totalsegmentator.*", diff --git a/src/physiomotion4d/segment_heart_simpleware.py b/src/physiomotion4d/segment_heart_simpleware.py new file mode 100644 index 0000000..fec54b2 --- /dev/null +++ b/src/physiomotion4d/segment_heart_simpleware.py @@ -0,0 +1,271 @@ +"""Module for segmenting heart from chest CT images using Simpleware Medical. + +This module provides the SegmentHeartSimpleware class that implements +heart segmentation using Synopsys Simpleware Medical's ASCardio module. +It inherits from SegmentChestBase and provides heart-specific anatomical +structure mappings. +""" + +import logging +import os +import subprocess +import tempfile + +import itk +import numpy as np +from itk import TubeTK as tube + +from physiomotion4d.segment_chest_base import SegmentChestBase + + +class SegmentHeartSimpleware(SegmentChestBase): + """ + Heart CT segmentation using Simpleware Medical's ASCardio module. + + This class implements heart segmentation using Synopsys Simpleware Medical, + a commercial medical image processing platform. It specifically leverages + the ASCardio module for automated cardiac segmentation. The class handles + the external process communication with Simpleware Medical and converts + between ITK and Simpleware image formats. + + Simpleware Medical provides high-quality segmentation for cardiac + structures including chambers, myocardium, and major vessels. The + segmentation is performed by launching Simpleware Medical as an external + process and running a Python script within the Simpleware environment. + + The class maintains specific ID mappings for: + - Heart structures (left/right atrium, left/right ventricle, myocardium) + - Major vessels (aorta, pulmonary artery) + + Attributes: + target_spacing (float): Target spacing set to 1.0mm for Simpleware + heart_mask_ids (dict): Dictionary mapping heart structure IDs to names + major_vessels_mask_ids (dict): Dictionary mapping vessel IDs to names + simpleware_exe_path (str): Path to Simpleware Medical executable + simpleware_script_path (str): Path to Simpleware Python script + + Example: + >>> segmenter = SegmentHeartSimpleware() + >>> result = segmenter.segment(ct_image, contrast_enhanced_study=True) + >>> labelmap = result['labelmap'] + >>> heart_mask = result['heart'] + """ + + def __init__(self, log_level: int | str = logging.INFO): + """Initialize the Simpleware Medical based heart segmentation. + + Sets up the Simpleware-specific anatomical structure ID mappings + and processing parameters. The target spacing is set to 1.0mm which + is optimal for cardiac segmentation in Simpleware Medical. + + Args: + log_level: Logging level (default: logging.INFO) + """ + super().__init__(log_level=log_level) + + self.target_spacing = 1.0 + + # Heart structure IDs for Simpleware Medical ASCardio output + # These IDs should match the output from the ASCardio module + self.heart_mask_ids = { + 1: "left_ventricle", + 2: "right_ventricle", + 3: "left_atrium", + 4: "right_atrium", + 5: "myocardium", + 6: "heart", + } + + # Major vessel IDs for Simpleware Medical ASCardio output + self.major_vessels_mask_ids = { + 7: "aorta", + 8: "pulmonary_artery", + 9: "right_coronary_artery", + 10: "left_coronary_artery", + } + + # Lung structures are not segmented by ASCardio + self.lung_mask_ids = {} + + # Bone structures are not segmented by ASCardio + self.bone_mask_ids = {} + + # Soft tissue structures are not segmented by ASCardio + # (will be filled in by base class 'other' category) + self.soft_tissue_mask_ids = {} + + # From Base Class + # self.contrast_mask_ids = {135: "contrast"} + + self.set_other_and_all_mask_ids() + + # Path to Simpleware Medical console executable + self.simpleware_exe_path = "C:/Program Files/Synopsys/Simpleware Medical/X-2025.06/ConsoleSimplewareMedical.exe" + + # Path to the Simpleware Python script for heart segmentation + self.simpleware_script_path = os.path.join( + os.path.dirname(__file__), + "simpleware_medical", + "SimplewareScript_heart_segmentation.py", + ) + + def set_simpleware_executable_path(self, path: str) -> None: + """Set the path to the Simpleware Medical console executable. + + Args: + path (str): Full path to ConsoleSimplewareMedical.exe + + Example: + >>> segmenter.set_simpleware_executable_path( + ... "C:/Program Files/Synopsys/Simpleware Medical/X-2025.06/ConsoleSimplewareMedical.exe" + ... ) + """ + self.simpleware_exe_path = path + + def segmentation_method(self, preprocessed_image: itk.image) -> itk.image: + """ + Run Simpleware Medical ASCardio segmentation on the preprocessed image. + + This implementation calls Simpleware Medical as an external process, + passing the preprocessed image via a temporary file. The Simpleware + Python script (SimplewareScript_heart_segmentation.py) runs within the + Simpleware environment and uses the ASCardio module for heart + segmentation. The results are written as per-structure MHD mask files and assembled + into a labelmap, then read back as an ITK image. + + Args: + preprocessed_image (itk.image): The preprocessed CT image with + isotropic spacing and appropriate intensity scaling + + Returns: + itk.image: The segmentation labelmap with heart and vessel labels + from the ASCardio module + + Raises: + FileNotFoundError: If Simpleware Medical executable is not found + RuntimeError: If Simpleware Medical process fails + ValueError: If output segmentation is not produced + + Note: + Requires a valid installation of Synopsys Simpleware Medical + with the ASCardio module. The method creates temporary files + for input/output communication with Simpleware. + + Example: + >>> labelmap = segmenter.segmentation_method(preprocessed_ct) + """ + # Check if Simpleware Medical executable exists + if not os.path.exists(self.simpleware_exe_path): + raise FileNotFoundError( + f"Simpleware Medical executable not found at: {self.simpleware_exe_path}" + ) + + # Check if Simpleware script exists + if not os.path.exists(self.simpleware_script_path): + raise FileNotFoundError( + f"Simpleware script not found at: {self.simpleware_script_path}" + ) + + with tempfile.TemporaryDirectory() as tmp_dir: + # Save preprocessed image to temporary file + tmp_input_image_file = os.path.join(tmp_dir, "input_image.nii.gz") + + self.log_info("Writing input image to: %s", tmp_input_image_file) + itk.imwrite(preprocessed_image, tmp_input_image_file, compression=True) + + # Build command line for Simpleware Medical + # Pass the input NIfTI file path directly as a command-line argument + # Use --run-script to execute the Python script + # Use --exit-after-script to close after execution + cmd = [ + self.simpleware_exe_path, + "--input-file", # Use only with ConsoleSimplewareMedical.exe + tmp_input_image_file, # Input NIfTI file path as positional argument + "--input-value", + tmp_dir, + "--run-script", + self.simpleware_script_path, + "--exit-after-script", + "--no-progress", # Use only with ConsoleSimplewareMedical.exe + # "--no-splash", # Use only with SimplewareMedical.exe + ] + user_input = "y\n" + + self.log_info("Running Simpleware Medical ASCardio segmentation...") + self.log_info("Command: %s", " ".join(cmd)) + + try: + # Run Simpleware Medical as a subprocess + result = subprocess.run( + cmd, + input=user_input, + capture_output=True, + text=True, + check=True, + timeout=600, # 10 minute timeout + ) + + # Log output from Simpleware + if result.stdout: + self.log_info("Simpleware stdout:\n%s", result.stdout) + if result.stderr: + self.log_warning("Simpleware stderr:\n%s", result.stderr) + + except subprocess.TimeoutExpired as e: + raise RuntimeError( + f"Simpleware Medical segmentation timed out after 600 seconds: {e}" + ) + except subprocess.CalledProcessError as e: + raise RuntimeError( + f"Simpleware Medical segmentation failed with return code {e.returncode}:\n" + f"stdout: {e.stdout}\nstderr: {e.stderr}" + ) + + # Simpleware's right ventricle, left atrium, right atrium correspond to + # the interior of those regions. + mask_ids_of_interior_regions = [2, 3, 4] + + # Check if output file was created + sz = [s for s in preprocessed_image.GetLargestPossibleRegion().GetSize()] + sz = sz[::-1] + labelmap_array = np.zeros(sz, dtype=np.uint8) + interior_array = np.zeros(sz, dtype=np.uint8) + for mask_id, mask_name in self.all_mask_ids.items(): + output_file = os.path.join(tmp_dir, f"mask_{mask_name}.mhd") + if os.path.exists(output_file): + mask_image = itk.imread(output_file) + mask_array = itk.GetArrayFromImage(mask_image).astype(np.uint8) + if mask_id in mask_ids_of_interior_regions: + tmp_array = (mask_array > 128).astype(np.uint8) + interior_array = np.where( + interior_array == 0, tmp_array, interior_array + ) + mask_array = (mask_array > 128) * mask_id + labelmap_array = np.where( + labelmap_array == 0, mask_array, labelmap_array + ) + + interior_image = itk.GetImageFromArray(interior_array.astype(np.uint8)) + interior_image.CopyInformation(preprocessed_image) + imMath = tube.ImageMath.New(interior_image) + imMath.Dilate(7, 1, 0) + imMath.Erode(4, 1, 0) + exterior_image = imMath.GetOutputUChar() + exterior_array = itk.GetArrayFromImage(exterior_image) + mask_id = 6 # Heart mask id + exterior_array = exterior_array * mask_id + labelmap_array = np.where( + labelmap_array == 0, exterior_array, labelmap_array + ) + + if not np.any(labelmap_array != 0): + raise ValueError( + "Simpleware Medical produced no segmentation output: no mask_*.mhd " + "files found or all masks are empty. Check Simpleware logs above and " + "ensure the ASCardio module ran successfully." + ) + + labelmap_image = itk.GetImageFromArray(labelmap_array.astype(np.uint8)) + labelmap_image.CopyInformation(preprocessed_image) + + return labelmap_image diff --git a/src/physiomotion4d/simpleware_medical/README.md b/src/physiomotion4d/simpleware_medical/README.md new file mode 100644 index 0000000..d44039f --- /dev/null +++ b/src/physiomotion4d/simpleware_medical/README.md @@ -0,0 +1,234 @@ +# Simpleware Medical Integration for PhysioMotion4D + +This directory contains integration code for using Synopsys Simpleware Medical with PhysioMotion4D for heart segmentation. + +## Overview + +The integration enables PhysioMotion4D to leverage Simpleware Medical's ASCardio module for automated cardiac segmentation. The implementation uses a two-component architecture: + +1. **segment_heart_simpleware.py** (in parent directory): A Python class that inherits from `SegmentChestBase` and manages the external Simpleware Medical process +2. **SimplewareScript_heart_segmentation.py** (this directory): A Python script that runs within the Simpleware Medical environment and performs the actual segmentation using ASCardio + +## Requirements + +- Synopsys Simpleware Medical X-2025.06 or later +- ASCardio module license +- Valid Simpleware Medical installation with console mode and Python scripting support +- ConsoleSimplewareMedical.exe (command-line version) + +## Current Status + +**✅ FUNCTIONAL**: This integration works as expected. + +### How It Works + +The integration passes the input CT image and output directory directly to Simpleware Medical: + +1. **Input**: The preprocessed NIfTI image is written to a temporary file; its path is passed via `--input-file` so Simpleware opens it as the active document. +2. **Output directory**: The temporary output directory is passed via `--input-value`; the script reads it with `app.GetInputValue()`. +3. The script runs ASCardio on the current document (the loaded NIfTI), then exports each mask as `mask_.mhd` into that directory. +4. `SegmentHeartSimpleware` reads the MHD mask files, assembles the labelmap, and returns the result. + +```bash +ConsoleSimplewareMedical.exe \ + --input-file input_image.nii.gz \ # Input CT (becomes active document) + --input-value \ # Where to write mask_*.mhd files + --run-script SimplewareScript_heart_segmentation.py \ + --exit-after-script \ + --no-progress +``` + +The Python script then: +```python +output_dir = app.GetInputValue() # Output directory from --input-value +doc = sw.App.GetDocument() # Active document (loaded NIfTI) +as_cardio = doc.GetAutoSegmenters().GetASCardio() +# ... run ASCardio, then export each mask to mask_.mhd in output_dir ... +``` + +## Installation + +1. Install Simpleware Medical (default path: `C:\Program Files\Synopsys\Simpleware Medical\X-2025.06\`) +2. Ensure the ASCardio module is licensed and available +3. No additional Python packages are required (Simpleware has its own Python environment) + +## Usage + +### Basic Usage + +```python +from physiomotion4d.segment_heart_simpleware import SegmentHeartSimpleware +import itk + +# Create segmenter instance +segmenter = SegmentHeartSimpleware() + +# Load CT image +ct_image = itk.imread("heart_ct.nii.gz") + +# Perform segmentation +result = segmenter.segment(ct_image, contrast_enhanced_study=True) + +# Access results +labelmap = result['labelmap'] +heart_mask = result['heart'] +vessel_mask = result['major_vessels'] + +# Save results +itk.imwrite(labelmap, "heart_segmentation.nii.gz") +``` + +### Custom Simpleware Path + +If Simpleware Medical is installed in a non-default location: + +```python +segmenter = SegmentHeartSimpleware() +segmenter.set_simpleware_executable_path( + "D:/CustomPath/Simpleware/ConsoleSimplewareMedical.exe" +) +``` + +### Segmentation Output + +The ASCardio module segments the following cardiac structures (label IDs match `segment_heart_simpleware.py`): + +**Heart Structures (IDs 1-6):** +- 1: Left Ventricle +- 2: Right Ventricle +- 3: Left Atrium +- 4: Right Atrium +- 5: Myocardium +- 6: Heart (combined heart mask; derived from interior regions in postprocessing) + +**Major Vessels (IDs 7-10):** +- 7: Aorta +- 8: Pulmonary Artery +- 9: Right Coronary Artery +- 10: Left Coronary Artery + +## Architecture + +### Process Flow + +1. PhysioMotion4D preprocesses the CT image (resampling to 1 mm isotropic, intensity scaling). +2. Preprocessed image is saved to a temporary NIfTI file (e.g. `input_image.nii.gz`) in a temporary directory. +3. `ConsoleSimplewareMedical.exe` is launched with: + - `--input-file ` — the preprocessed CT (Simpleware opens it as the active document) + - `--input-value ` — directory where the script will write mask files + - `--run-script SimplewareScript_heart_segmentation.py` + - `--exit-after-script` and `--no-progress` +4. The script runs inside Simpleware: + - Gets the output directory from `app.GetInputValue()` + - Uses the current document (the loaded NIfTI) and ASCardio to segment heart and vessels + - Exports each mask as `mask_.mhd` into the output directory +5. PhysioMotion4D reads the `mask_*.mhd` files, builds the labelmap (including heart exterior from interior regions), and returns the result. + +### Communication + +- **Executable**: `ConsoleSimplewareMedical.exe` (command-line version) +- **Script**: `--run-script SimplewareScript_heart_segmentation.py` +- **Input**: NIfTI image path via `--input-file` (becomes the active document) +- **Output**: Directory path via `--input-value`; script writes `mask_.mhd` per structure +- **Protocol**: File-based I/O via temporary directory +- **Timeout**: 10 minutes (configurable in `segment_heart_simpleware.py`) + +## Troubleshooting + +### Common Issues + +**Issue**: `FileNotFoundError: Simpleware Medical executable not found` +- **Solution**: + - Verify Simpleware installation path + - Ensure you're using `ConsoleSimplewareMedical.exe` not `SimplewareMedical.exe` + - Use `set_simpleware_executable_path()` to specify custom location + +**Issue**: `unrecognised option '-python'` +- **Solution**: The GUI version `SimplewareMedical.exe` doesn't support command-line scripting. Use `ConsoleSimplewareMedical.exe` instead. + +**Issue**: `ImportError: Failed to import Simpleware modules` +- **Solution**: Ensure the script is being called with `--run-script` flag through `ConsoleSimplewareMedical.exe` + +**Issue**: `WARNING: No segmentation masks were created` or missing mask files +- **Solution**: Check that the input NIfTI is passed via `--input-file` so the document is loaded. Ensure input image quality, contrast, and field of view; the heart should be clearly visible. + +**Issue**: Segmentation timeout after 600 seconds +- **Solution**: Image may be too large or high resolution. Consider adjusting preprocessing parameters. + +### Logging + +Enable detailed logging to troubleshoot issues: + +```python +import logging + +segmenter = SegmentHeartSimpleware(log_level=logging.DEBUG) +``` + +## Customization + +### Modifying ASCardio Parameters + +To customize ASCardio segmentation parameters, edit `SimplewareScript_heart_segmentation.py`: + +```python +cardio.auto_segment( + image=image, + segment_chambers=True, + segment_myocardium=True, + segment_vessels=True, + # Add custom parameters here +) +``` + +## Reference Documentation + +For more information on Simpleware Medical and ASCardio: +- Simpleware Medical User Guide +- ASCardio Module Documentation +- Simpleware Python API Reference (ScriptingAPI.chm) +- Console Mode Documentation + +Located in: `C:\Program Files\Synopsys\Simpleware Medical\X-2025.06\Documentation\` + +### Console Mode Command Reference + +```bash +# View all command-line options +ConsoleSimplewareMedical.exe --help + +# Key options used by this integration: +--input-file # Open input (NIfTI image); becomes active document +--input-value # Single string passed to script via app.GetInputValue() (e.g. output dir) +--run-script