Upload 21 files
Browse filesadded scr , bash and documentation files
- docs/HUGGINGFACE_MODEL_CARD.md +344 -0
- docs/TECHNICAL_DOCUMENTATION.md +535 -0
- output/DLCS24_KNN_2mm_Extend_Seg/DLCS_0001_mask.nii.gz +3 -0
- output/DLCS24_KNN_2mm_Extend_Seg/DLCS_0002_mask.nii.gz +3 -0
- scr/.ipynb_checkpoints/segmentation_utils-checkpoint.py +490 -0
- scr/Pyradiomics_feature_extarctor_pram.json +6 -0
- scr/__pycache__/cvseg_utils.cpython-310.pyc +0 -0
- scr/__pycache__/cvseg_utils.cpython-311.pyc +0 -0
- scr/__pycache__/cvseg_utils.cpython-38.pyc +0 -0
- scr/__pycache__/cvseg_utils.cpython-39.pyc +0 -0
- scr/__pycache__/segmentation_utils.cpython-38.pyc +0 -0
- scr/candidateSeg_pipiline.py +86 -0
- scr/candidateSeg_radiomicsExtractor_pipiline.py +213 -0
- scr/candidate_worldCoord_patchExtarctor_pipeline.py +177 -0
- scr/cvseg_utils.py +488 -0
- scr/segmentation_utils.py +490 -0
- scripts/DLCS24_CADe_64Qpatch.sh +76 -0
- scripts/DLCS24_KNN_2mm_Extend_Radiomics.sh +89 -0
- scripts/DLCS24_KNN_2mm_Extend_Seg.sh +84 -0
- scripts/build.sh +242 -0
- scripts/run.sh +385 -0
docs/HUGGINGFACE_MODEL_CARD.md
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
language: en
|
| 3 |
+
license: cc-by-nc-4.0
|
| 4 |
+
library_name: pins-toolkit
|
| 5 |
+
tags:
|
| 6 |
+
- medical-imaging
|
| 7 |
+
- computed-tomography
|
| 8 |
+
- pulmonary-nodules
|
| 9 |
+
- radiomics
|
| 10 |
+
- segmentation
|
| 11 |
+
- lung-cancer
|
| 12 |
+
- ct-analysis
|
| 13 |
+
- pyradiomics
|
| 14 |
+
- simpleitk
|
| 15 |
+
- pytorch
|
| 16 |
+
- monai
|
| 17 |
+
- opencv
|
| 18 |
+
- docker
|
| 19 |
+
datasets:
|
| 20 |
+
- dlcs24
|
| 21 |
+
metrics:
|
| 22 |
+
- dice-coefficient
|
| 23 |
+
- feature-reproducibility
|
| 24 |
+
pipeline_tag: image-segmentation
|
| 25 |
+
widget:
|
| 26 |
+
- example_title: Lung Nodule Segmentation
|
| 27 |
+
text: Automated segmentation of pulmonary nodules in chest CT scans
|
| 28 |
+
model-index:
|
| 29 |
+
- name: PiNS
|
| 30 |
+
results:
|
| 31 |
+
- task:
|
| 32 |
+
type: image-segmentation
|
| 33 |
+
name: Medical Image Segmentation
|
| 34 |
+
dataset:
|
| 35 |
+
name: DLCS24
|
| 36 |
+
type: medical-ct
|
| 37 |
+
metrics:
|
| 38 |
+
- type: dice-coefficient
|
| 39 |
+
value:
|
| 40 |
+
name: Dice Similarity Coefficient
|
| 41 |
+
---
|
| 42 |
+
|
| 43 |
+
# PiNS - Point-driven Nodule Segmentation
|
| 44 |
+
|
| 45 |
+
<div align="center">
|
| 46 |
+
<p align="center">
|
| 47 |
+
<img src="assets/PiNS_logo.png" alt="PiNS Logo" width="500">
|
| 48 |
+
</p>
|
| 49 |
+
|
| 50 |
+
**Medical imaging toolkit for automated pulmonary nodule detection, segmentation, and quantitative analysis**
|
| 51 |
+
|
| 52 |
+
[](https://hub.docker.com/r/ft42/pins)
|
| 53 |
+
[](https://opensource.org/licenses/MIT)
|
| 54 |
+
[](https://python.org)
|
| 55 |
+
[](https://simpleitk.org)
|
| 56 |
+
[](https://pytorch.org)
|
| 57 |
+
[](https://monai.io)
|
| 58 |
+
|
| 59 |
+
[🚀 Quick Start](#quick-start) • [📖 Documentation](https://github.com/ft42/PiNS/blob/main/docs/TECHNICAL_DOCUMENTATION.md) • [💻 GitHub](https://github.com/ft42/PiNS) • [🐳 Docker Hub](https://hub.docker.com/r/ft42/pins)
|
| 60 |
+
|
| 61 |
+
</div>
|
| 62 |
+
|
| 63 |
+
## Overview
|
| 64 |
+
|
| 65 |
+
**PiNS (Point-driven Nodule Segmentation)** is a medical imaging toolkit designed for analysis of pulmonary nodules in computed tomography (CT) scans. The toolkit provides three core functionalities:
|
| 66 |
+
|
| 67 |
+
🎯 **Automated Segmentation** - Multi-algorithm nodule segmentation with clinical validation
|
| 68 |
+
📊 **Quantitative Radiomics** - 100+ standardized imaging biomarkers
|
| 69 |
+
🧩 **3D Patch Extraction** - Deep learning-ready data preparation
|
| 70 |
+
|
| 71 |
+
## Model Architecture & Algorithms
|
| 72 |
+
|
| 73 |
+
### Segmentation Pipeline
|
| 74 |
+
|
| 75 |
+
```mermaid
|
| 76 |
+
graph TB
|
| 77 |
+
A[CT Image + Coordinates] --> B[Coordinate Transformation]
|
| 78 |
+
B --> C[ROI Extraction]
|
| 79 |
+
C --> D{Segmentation Algorithm}
|
| 80 |
+
D --> E[K-means Clustering]
|
| 81 |
+
D --> F[Gaussian Mixture Model]
|
| 82 |
+
D --> G[Fuzzy C-Means]
|
| 83 |
+
D --> H[Otsu Thresholding]
|
| 84 |
+
E --> I[Connected Components]
|
| 85 |
+
F --> I
|
| 86 |
+
G --> I
|
| 87 |
+
H --> I
|
| 88 |
+
I --> J[Morphological Operations]
|
| 89 |
+
J --> K[Expansion (2mm)]
|
| 90 |
+
K --> L[Binary Mask Output]
|
| 91 |
+
```
|
| 92 |
+
|
| 93 |
+
### Core Algorithms
|
| 94 |
+
|
| 95 |
+
1. **K-means Clustering** (Default)
|
| 96 |
+
- Binary classification: nodule vs. background
|
| 97 |
+
- Euclidean distance metric
|
| 98 |
+
- Automatic initialization
|
| 99 |
+
|
| 100 |
+
2. **Gaussian Mixture Model**
|
| 101 |
+
- Probabilistic clustering approach
|
| 102 |
+
- Expectation-maximization optimization
|
| 103 |
+
- Suitable for heterogeneous nodules
|
| 104 |
+
|
| 105 |
+
3. **Fuzzy C-Means**
|
| 106 |
+
- Soft clustering with membership degrees
|
| 107 |
+
- Iterative optimization
|
| 108 |
+
- Robust to noise and partial volume effects
|
| 109 |
+
|
| 110 |
+
4. **Otsu Thresholding**
|
| 111 |
+
- Automatic threshold selection
|
| 112 |
+
- Histogram-based method
|
| 113 |
+
- Fast execution for large datasets
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
## Quick Start
|
| 117 |
+
|
| 118 |
+
### Prerequisites
|
| 119 |
+
- Docker 20.10.0+ installed
|
| 120 |
+
- 8GB+ RAM
|
| 121 |
+
- 15GB+ free disk space
|
| 122 |
+
|
| 123 |
+
### Installation & Usage
|
| 124 |
+
|
| 125 |
+
```bash
|
| 126 |
+
# 1. Pull the Docker image (automatically handled)
|
| 127 |
+
docker pull ft42/pins:latest
|
| 128 |
+
|
| 129 |
+
# 2. Clone the repository
|
| 130 |
+
git clone https://github.com/ft42/PiNS.git
|
| 131 |
+
cd PiNS
|
| 132 |
+
|
| 133 |
+
# 3. Run segmentation pipeline
|
| 134 |
+
./scripts/DLCS24_KNN_2mm_Extend_Seg.sh
|
| 135 |
+
|
| 136 |
+
# 4. Extract radiomics features
|
| 137 |
+
./scripts/DLCS24_KNN_2mm_Extend_Radiomics.sh
|
| 138 |
+
|
| 139 |
+
# 5. Generate ML-ready patches
|
| 140 |
+
./scripts/DLCS24_CADe_64Qpatch.sh
|
| 141 |
+
```
|
| 142 |
+
|
| 143 |
+
### Expected Output
|
| 144 |
+
|
| 145 |
+
```
|
| 146 |
+
✅ Segmentation completed!
|
| 147 |
+
📊 Features extracted: 107 radiomics features per nodule
|
| 148 |
+
🧩 Patches generated: 64×64×64 voxel volumes
|
| 149 |
+
📁 Results saved to: demofolder/output/
|
| 150 |
+
```
|
| 151 |
+
|
| 152 |
+
## Input Data Requirements
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
### Image Specifications
|
| 157 |
+
- **Format**: NIfTI (.nii.gz) or DICOM
|
| 158 |
+
- **Modality**: CT chest/abdomen/CAP scans
|
| 159 |
+
- **Resolution**: 0.5-2.0 mm isotropic (preferred)
|
| 160 |
+
- **Matrix size**: 512×512 or larger
|
| 161 |
+
- **Bit depth**: 16-bit signed integers
|
| 162 |
+
- **Intensity range**: Standard HU values (-1024 to +3071)
|
| 163 |
+
- **Sample Dataset:** Duke Lung Cancer Screening Dataset 2024(DLCS24)[](https://doi.org/10.5281/zenodo.13799069)
|
| 164 |
+
### Annotation Format
|
| 165 |
+
|
| 166 |
+
```csv
|
| 167 |
+
ct_nifti_file,nodule_id,coordX,coordY,coordZ,w,h,d,Malignant_lbl
|
| 168 |
+
patient001.nii.gz,patient001_01,-106.55,-63.84,-211.68,4.39,4.39,4.30,0
|
| 169 |
+
patient001.nii.gz,patient001_02,88.69,39.48,-126.09,6.24,6.24,6.25,1
|
| 170 |
+
```
|
| 171 |
+
|
| 172 |
+
**Column Descriptions**:
|
| 173 |
+
- `coordX/Y/Z`: World coordinates in millimeters (ITK/SimpleITK standard)
|
| 174 |
+
- `w/h/d`: Bounding box dimensions in millimeters
|
| 175 |
+
- `Malignant_lbl`: Binary malignancy label (0=benign, 1=malignant)
|
| 176 |
+
|
| 177 |
+
## Output Specifications
|
| 178 |
+
|
| 179 |
+
### 1. Segmentation Masks
|
| 180 |
+
- **Format**: NIfTI binary masks (.nii.gz)
|
| 181 |
+
- **Values**: 0 (background), 1 (nodule)
|
| 182 |
+
- **Coordinate system**: Aligned with input CT
|
| 183 |
+
- **Quality**: Sub-voxel precision boundaries
|
| 184 |
+
|
| 185 |
+
### 2. Radiomics Features
|
| 186 |
+
|
| 187 |
+
**Feature Categories** (107 total features):
|
| 188 |
+
|
| 189 |
+
| Category | Count | Description |
|
| 190 |
+
|----------|-------|-------------|
|
| 191 |
+
| **Shape** | 14 | Volume, Surface Area, Sphericity, Compactness |
|
| 192 |
+
| **First-order** | 18 | Mean, Std, Skewness, Kurtosis, Percentiles |
|
| 193 |
+
| **GLCM** | 24 | Contrast, Correlation, Energy, Homogeneity |
|
| 194 |
+
| **GLRLM** | 16 | Run Length Non-uniformity, Gray Level Variance |
|
| 195 |
+
| **GLSZM** | 16 | Size Zone Matrix features |
|
| 196 |
+
| **GLDM** | 14 | Dependence Matrix features |
|
| 197 |
+
| **NGTDM** | 5 | Neighboring Gray Tone Difference |
|
| 198 |
+
|
| 199 |
+
### 3. 3D Patches
|
| 200 |
+
- **Dimensions**: 64×64×64 voxels (configurable)
|
| 201 |
+
- **Normalization**: Lung window (-1000 to 500 HU) → [0,1]
|
| 202 |
+
- **Format**: Individual NIfTI files per nodule
|
| 203 |
+
- **Centering**: Precise coordinate-based positioning
|
| 204 |
+
|
| 205 |
+
## Configuration Options
|
| 206 |
+
|
| 207 |
+
### Algorithm Selection
|
| 208 |
+
```bash
|
| 209 |
+
SEG_ALG="knn" # Options: knn, gmm, fcm, otsu
|
| 210 |
+
EXPANSION_MM=2.0 # Expansion radius in millimeters
|
| 211 |
+
```
|
| 212 |
+
|
| 213 |
+
### Radiomics Parameters
|
| 214 |
+
```json
|
| 215 |
+
{
|
| 216 |
+
"binWidth": 25,
|
| 217 |
+
"resampledPixelSpacing": [1, 1, 1],
|
| 218 |
+
"interpolator": "sitkBSpline",
|
| 219 |
+
"labelInterpolator": "sitkNearestNeighbor"
|
| 220 |
+
}
|
| 221 |
+
```
|
| 222 |
+
|
| 223 |
+
### Patch Extraction
|
| 224 |
+
```bash
|
| 225 |
+
PATCH_SIZE="64 64 64" # Voxel dimensions
|
| 226 |
+
NORMALIZATION="-1000 500 0 1" # HU window and output range
|
| 227 |
+
```
|
| 228 |
+
|
| 229 |
+
## Use Cases & Applications
|
| 230 |
+
|
| 231 |
+
### 🔬 Research Applications
|
| 232 |
+
- **Biomarker Discovery**: Large-scale radiomics studies
|
| 233 |
+
- **Algorithm Development**: Standardized evaluation protocols
|
| 234 |
+
- **Multi-institutional Studies**: Reproducible feature extraction
|
| 235 |
+
- **Longitudinal Analysis**: Change assessment over time
|
| 236 |
+
|
| 237 |
+
### 🤖 AI/ML Applications
|
| 238 |
+
- **Training Data Preparation**: Standardized patch generation
|
| 239 |
+
- **Feature Engineering**: Comprehensive radiomics features
|
| 240 |
+
- **Model Validation**: Consistent preprocessing pipeline
|
| 241 |
+
- **Transfer Learning**: Pre-processed medical imaging data
|
| 242 |
+
|
| 243 |
+
## Technical Specifications
|
| 244 |
+
|
| 245 |
+
### Docker Container Details
|
| 246 |
+
- **Base Image**: Ubuntu 20.04 LTS
|
| 247 |
+
- **Size**: ~1.5 GB
|
| 248 |
+
- **Python**: 3.9+
|
| 249 |
+
- **Key Libraries**:
|
| 250 |
+
- SimpleITK 2.2.1+ (medical image processing)
|
| 251 |
+
- PyRadiomics 3.1.0+ (feature extraction)
|
| 252 |
+
- scikit-learn 1.3.0+ (machine learning algorithms)
|
| 253 |
+
- pandas 2.0.3+ (data manipulation)
|
| 254 |
+
|
| 255 |
+
### Performance Characteristics
|
| 256 |
+
- **Memory Usage**: ~500MB per nodule
|
| 257 |
+
- **Processing Speed**: Linear scaling with nodule count
|
| 258 |
+
- **Concurrent Processing**: Multi-threading support
|
| 259 |
+
- **Storage Requirements**: ~1MB per output mask
|
| 260 |
+
|
| 261 |
+
## Validation & Quality Assurance
|
| 262 |
+
|
| 263 |
+
**Evaluation Criteria:** In the absence of voxel-level ground truth, we adopted a bounding box–supervised evaluation strategy to assess segmentation performance. Each CT volume was accompanied by annotations specifying the nodule center in world coordinates and its dimensions in millimeters, which were converted into voxel indices using the image spacing and clipped to the volume boundaries. A binary mask representing the bounding box was then constructed and used as a weak surrogate for ground truth. we extracted a patch centered on the bounding box, extending it by a fixed margin (64 voxels) to define the volume of interest (VOI). Predicted segmentation masks were cropped to the same VOI-constrained region of interest, and performance was quantified in terms of Dice similarity coefficient. Metrics were computed per lesion. This evaluation strategy enables consistent comparison of segmentation algorithms under weak supervision while acknowledging the limitations of not having voxel-level annotations.
|
| 264 |
+
|
| 265 |
+
Segmentation performance of **KNN (ours PiNS)**, **VISTA3D auto**, and **VISTA3D points** ([He et al. 2024](https://github.com/Project-MONAI/VISTA/tree/main/vista3d)) across different nodule size buckets. (top) Bar plots display the mean Dice similarity coefficient for each model and size category. (buttom) Boxplots show the distribution of Dice scores, with boxes representing the interquartile range, horizontal lines indicating the median, whiskers extending to 1.5× the interquartile range, and circles denoting outliers.
|
| 266 |
+
|
| 267 |
+
<p align="center">
|
| 268 |
+
<img src="assets/Segmentation_Evaluation_KNNVista3Dauto_DLCS24_HIST.png" alt="(a)" width="700">
|
| 269 |
+
</p>
|
| 270 |
+
|
| 271 |
+
<p align="center">
|
| 272 |
+
<img src="assets/Segmentation_Evaluation_KNNVista3Dauto_DLCS24_BOX.png" alt="(b)" width="700">
|
| 273 |
+
</p>
|
| 274 |
+
## Limitations & Considerations
|
| 275 |
+
|
| 276 |
+
### Current Limitations
|
| 277 |
+
- **Nodule Size**: Optimized for nodules 3-30mm diameter
|
| 278 |
+
- **Image Quality**: Requires standard clinical CT protocols
|
| 279 |
+
- **Coordinate Accuracy**: Dependent on annotation precision
|
| 280 |
+
- **Processing Time**: Sequential processing (parallelization possible)
|
| 281 |
+
|
| 282 |
+
|
| 283 |
+
|
| 284 |
+
## Contributing & Development
|
| 285 |
+
|
| 286 |
+
### Research Collaborations
|
| 287 |
+
We welcome collaborations from:
|
| 288 |
+
- **Academic Medical Centers**
|
| 289 |
+
- **Radiology Departments**
|
| 290 |
+
- **Medical AI Companies**
|
| 291 |
+
- **Open Source Contributors**
|
| 292 |
+
|
| 293 |
+
## Citation & References
|
| 294 |
+
|
| 295 |
+
### Primary Citation
|
| 296 |
+
```bibtex
|
| 297 |
+
@software{pins2025,
|
| 298 |
+
title={PiNS: Point-driven Nodule Segmentation Toolkit },
|
| 299 |
+
author={Fakrul Islam Tushar},
|
| 300 |
+
year={2025},
|
| 301 |
+
url={https://github.com/fitushar/PiNS},
|
| 302 |
+
version={1.0.0},
|
| 303 |
+
doi={10.5281/zenodo.17171571},
|
| 304 |
+
license={CC-BY-NC-4.0}
|
| 305 |
+
}
|
| 306 |
+
```
|
| 307 |
+
|
| 308 |
+
### Related Publications
|
| 309 |
+
1. **AI in Lung Health: Benchmarking** : [Tushar et al. arxiv (2024)](https://arxiv.org/abs/2405.04605)
|
| 310 |
+
2. **AI in Lung Health: Benchmarking** : [https://github.com/fitushar/AI-in-Lung-Health-Benchmarking](https://github.com/fitushar/AI-in-Lung-Health-Benchmarking-Detection-and-Diagnostic-Models-Across-Multiple-CT-Scan-Datasets)
|
| 311 |
+
4. **DLCS Dataset**: [Wang et al. Radiology AI 2024](https://doi.org/10.1148/ryai.240248);[Zenedo](https://zenodo.org/records/13799069)
|
| 312 |
+
5. **SYN-LUNGS**: [Tushar et al., arxiv 2025](https://arxiv.org/abs/2502.21187)
|
| 313 |
+
6. **Refining Focus in AI for Lung Cancer:** Comparing Lesion-Centric and Chest-Region Models with Performance Insights from Internal and External Validation. [](https://arxiv.org/abs/2411.16823)
|
| 314 |
+
7. **Peritumoral Expansion Radiomics** for Improved Lung Cancer Classification. [](https://arxiv.org/abs/2411.16008)
|
| 315 |
+
8. **PyRadiomics Framework**: [van Griethuysen et al., Cancer Research 2017](https://pubmed.ncbi.nlm.nih.gov/29092951/)
|
| 316 |
+
|
| 317 |
+
|
| 318 |
+
|
| 319 |
+
## License & Usage
|
| 320 |
+
**license: cc-by-nc-4.0**
|
| 321 |
+
### Academic Use License
|
| 322 |
+
This project is released for **academic and non-commercial research purposes only**.
|
| 323 |
+
You are free to use, modify, and distribute this code under the following conditions:
|
| 324 |
+
- ✅ Academic research use permitted
|
| 325 |
+
- ✅ Modification and redistribution permitted for research
|
| 326 |
+
- ❌ Commercial use prohibited without prior written permission
|
| 327 |
+
For commercial licensing inquiries, please contact: [email protected]
|
| 328 |
+
|
| 329 |
+
|
| 330 |
+
## Support & Community
|
| 331 |
+
|
| 332 |
+
### Getting Help
|
| 333 |
+
- **📖 Documentation**: [Comprehensive technical docs](https://github.com/fitushar/PiNS/blob/main/docs/)
|
| 334 |
+
- **🐛 Issues**: [GitHub Issues](https://github.com/fitushar/PiNS/issues)
|
| 335 |
+
- **💬 Discussions**: [GitHub Discussions](https://github.com/fitushar/PiNS/discussions)
|
| 336 |
+
- **📧 Email**: [email protected] ; [email protected]
|
| 337 |
+
|
| 338 |
+
### Community Stats
|
| 339 |
+
- **Publications**: 5+ research papers
|
| 340 |
+
- **Contributors**: Active open-source community
|
| 341 |
+
|
| 342 |
+
---
|
| 343 |
+
|
| 344 |
+
|
docs/TECHNICAL_DOCUMENTATION.md
ADDED
|
@@ -0,0 +1,535 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# PiNS (Point-driven Nodule Segmentation) - Technical Documentation
|
| 2 |
+
## Professional Technical Documentation
|
| 3 |
+
|
| 4 |
+
### Version: 1.0.0
|
| 5 |
+
### Authors: Fakrul Islam Tushar ([email protected])
|
| 6 |
+
### Date: September 2025
|
| 7 |
+
### License: CC-BY-NC-4.0
|
| 8 |
+
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
## Table of Contents
|
| 12 |
+
|
| 13 |
+
1. [Overview](#overview)
|
| 14 |
+
2. [Architecture](#architecture)
|
| 15 |
+
3. [Core Components](#core-components)
|
| 16 |
+
4. [Technical Specifications](#technical-specifications)
|
| 17 |
+
5. [API Reference](#api-reference)
|
| 18 |
+
6. [Implementation Details](#implementation-details)
|
| 19 |
+
7. [Performance Metrics](#performance-metrics)
|
| 20 |
+
8. [Clinical Applications](#clinical-applications)
|
| 21 |
+
9. [Validation](#validation)
|
| 22 |
+
10. [Future Developments](#future-developments)
|
| 23 |
+
|
| 24 |
+
---
|
| 25 |
+
|
| 26 |
+
## Overview
|
| 27 |
+
|
| 28 |
+
### Abstract
|
| 29 |
+
|
| 30 |
+
PiNS (Point-driven Nodule Segmentation) is a medical imaging toolkit designed for automated detection, segmentation, and analysis of pulmonary nodules in computed tomography (CT) scans. The toolkit provides an end-to-end pipeline from coordinate-based nodule identification to quantitative radiomics feature extraction.
|
| 31 |
+
|
| 32 |
+
### Key Capabilities
|
| 33 |
+
|
| 34 |
+
**1. Automated Nodule Segmentation**
|
| 35 |
+
- K-means clustering-based segmentation with configurable expansion
|
| 36 |
+
- Multi-algorithm support (K-means, Gaussian Mixture Models, Fuzzy C-Means, Otsu)
|
| 37 |
+
- Sub-voxel precision coordinate handling
|
| 38 |
+
- Adaptive region growing with millimeter-based expansion
|
| 39 |
+
|
| 40 |
+
**2. Quantitative Radiomics Analysis**
|
| 41 |
+
- PyRadiomics-compliant feature extraction
|
| 42 |
+
- 100+ standardized imaging biomarkers
|
| 43 |
+
- IBSI-compatible feature calculations
|
| 44 |
+
- Configurable intensity normalization and resampling
|
| 45 |
+
|
| 46 |
+
**3. Patch-based Data Preparation**
|
| 47 |
+
- 3D volumetric patch extraction (64³ default)
|
| 48 |
+
- Standardized intensity windowing for lung imaging
|
| 49 |
+
- Deep learning-ready data formatting
|
| 50 |
+
- Automated coordinate-to-voxel transformation
|
| 51 |
+
|
| 52 |
+
### Clinical Significance
|
| 53 |
+
|
| 54 |
+
PiNS addresses critical challenges in pulmonary nodule analysis:
|
| 55 |
+
- **Reproducibility**: Standardized segmentation protocols
|
| 56 |
+
- **Quantification**: Objective radiomics-based characterization
|
| 57 |
+
- **Scalability**: Batch processing capabilities for research cohorts
|
| 58 |
+
- **Interoperability**: NIfTI support with Docker containerization
|
| 59 |
+
|
| 60 |
+
---
|
| 61 |
+
|
| 62 |
+
## Architecture
|
| 63 |
+
|
| 64 |
+
### System Design
|
| 65 |
+
|
| 66 |
+
```
|
| 67 |
+
┌─────────────────────────────────────────────────────────────┐
|
| 68 |
+
│ PiNS Architecture │
|
| 69 |
+
├─────────────────────────────────────────────────────────────┤
|
| 70 |
+
│ Input Layer │
|
| 71 |
+
│ ├── CT DICOM/NIfTI Images │
|
| 72 |
+
│ ├── Coordinate Annotations (World/Voxel) │
|
| 73 |
+
│ └── Configuration Parameters │
|
| 74 |
+
├─────────────────────────────────────────────────────────────┤
|
| 75 |
+
│ Processing Layer │
|
| 76 |
+
│ ├── Image Preprocessing │
|
| 77 |
+
│ │ ├── Intensity Normalization │
|
| 78 |
+
│ │ ├── Resampling & Interpolation │
|
| 79 |
+
│ │ └── Coordinate Transformation │
|
| 80 |
+
│ ├── Segmentation Engine │
|
| 81 |
+
│ │ ├── K-means Clustering │
|
| 82 |
+
│ │ ├── Region Growing │
|
| 83 |
+
│ │ └── Morphological Operations │
|
| 84 |
+
│ └── Feature Extraction │
|
| 85 |
+
│ ├── Shape Features │
|
| 86 |
+
│ ├── First-order Statistics │
|
| 87 |
+
│ ├── Texture Features (GLCM, GLRLM, GLSZM, GLDM) │
|
| 88 |
+
│ └── Wavelet Features │
|
| 89 |
+
├─────────────────────────────────────────────────────────────┤
|
| 90 |
+
│ Output Layer │
|
| 91 |
+
│ ├── Segmentation Masks (NIfTI) │
|
| 92 |
+
│ ├── Quantitative Features (CSV) │
|
| 93 |
+
│ ├── Image Patches (NIfTI) │
|
| 94 |
+
│ └── Processing Logs │
|
| 95 |
+
└─────────────────────────────────────────────────────────────┘
|
| 96 |
+
```
|
| 97 |
+
|
| 98 |
+
### Technology Stack
|
| 99 |
+
|
| 100 |
+
- **Containerization**: Docker (Ubuntu 20.04 base)
|
| 101 |
+
- **Medical Imaging**: SimpleITK, PyRadiomics 3.1.0+
|
| 102 |
+
- **Scientific Computing**: NumPy, SciPy, scikit-learn
|
| 103 |
+
- **Data Management**: Pandas, NiBabel
|
| 104 |
+
- **Visualization**: Matplotlib
|
| 105 |
+
- **Languages**: Python 3.8+, Bash scripting
|
| 106 |
+
|
| 107 |
+
---
|
| 108 |
+
|
| 109 |
+
## Core Components
|
| 110 |
+
|
| 111 |
+
### Component 1: Nodule Segmentation Pipeline
|
| 112 |
+
|
| 113 |
+
**Script**: `DLCS24_KNN_2mm_Extend_Seg.sh`
|
| 114 |
+
**Purpose**: Automated segmentation of pulmonary nodules from coordinate annotations
|
| 115 |
+
|
| 116 |
+
**Algorithm Workflow**:
|
| 117 |
+
1. **Coordinate Processing**: Transform world coordinates to voxel indices
|
| 118 |
+
2. **Region Initialization**: Create bounding box around nodule center
|
| 119 |
+
3. **Clustering Segmentation**: Apply K-means with k=2 (nodule vs. background)
|
| 120 |
+
4. **Connected Component Analysis**: Extract largest connected component
|
| 121 |
+
5. **Morphological Refinement**: Apply expansion based on clinical parameters
|
| 122 |
+
6. **Quality Control**: Validate segmentation size and connectivity
|
| 123 |
+
|
| 124 |
+
**Technical Parameters**:
|
| 125 |
+
- Expansion radius: 2.0mm (configurable)
|
| 126 |
+
- Clustering algorithm: K-means (alternatives: GMM, FCM, Otsu)
|
| 127 |
+
- Output format: NIfTI (.nii.gz)
|
| 128 |
+
- Coordinate system: ITK/SimpleITK standard
|
| 129 |
+
|
| 130 |
+
### Component 2: Radiomics Feature Extraction
|
| 131 |
+
|
| 132 |
+
**Script**: `DLCS24_KNN_2mm_Extend_Radiomics.sh`
|
| 133 |
+
**Purpose**: Quantitative imaging biomarker extraction from segmented nodules
|
| 134 |
+
|
| 135 |
+
**Feature Categories**:
|
| 136 |
+
1. **Shape Features (14 features)**
|
| 137 |
+
- Sphericity, Compactness, Surface Area
|
| 138 |
+
- Volume, Maximum Diameter
|
| 139 |
+
- Elongation, Flatness
|
| 140 |
+
|
| 141 |
+
2. **First-order Statistics (18 features)**
|
| 142 |
+
- Mean, Median, Standard Deviation
|
| 143 |
+
- Skewness, Kurtosis, Entropy
|
| 144 |
+
- Percentiles (10th, 90th)
|
| 145 |
+
|
| 146 |
+
3. **Second-order Texture (75+ features)**
|
| 147 |
+
- Gray Level Co-occurrence Matrix (GLCM)
|
| 148 |
+
- Gray Level Run Length Matrix (GLRLM)
|
| 149 |
+
- Gray Level Size Zone Matrix (GLSZM)
|
| 150 |
+
- Gray Level Dependence Matrix (GLDM)
|
| 151 |
+
|
| 152 |
+
4. **Higher-order Features (100+ features)**
|
| 153 |
+
- Wavelet decomposition features
|
| 154 |
+
- Laplacian of Gaussian filters
|
| 155 |
+
|
| 156 |
+
**Normalization Protocol**:
|
| 157 |
+
- Bin width: 25 HU
|
| 158 |
+
- Resampling: 1×1×1 mm³
|
| 159 |
+
- Interpolation: B-spline (image), Nearest neighbor (mask)
|
| 160 |
+
|
| 161 |
+
### Component 3: Patch Extraction Pipeline
|
| 162 |
+
|
| 163 |
+
**Script**: `DLCS24_CADe_64Qpatch.sh`
|
| 164 |
+
**Purpose**: 3D volumetric patch extraction for deep learning applications
|
| 165 |
+
|
| 166 |
+
**Patch Specifications**:
|
| 167 |
+
- **Dimensions**: 64×64×64 voxels (configurable)
|
| 168 |
+
- **Centering**: World coordinate-based positioning
|
| 169 |
+
- **Windowing**: -1000 to 500 HU (lung window)
|
| 170 |
+
- **Normalization**: Min-max scaling to [0,1]
|
| 171 |
+
- **Boundary Handling**: Zero-padding for edge cases
|
| 172 |
+
|
| 173 |
+
**Output Format**:
|
| 174 |
+
- Individual NIfTI files per nodule
|
| 175 |
+
- CSV metadata with coordinates and labels
|
| 176 |
+
- Standardized naming convention
|
| 177 |
+
|
| 178 |
+
---
|
| 179 |
+
|
| 180 |
+
## Technical Specifications
|
| 181 |
+
|
| 182 |
+
### Hardware Requirements
|
| 183 |
+
|
| 184 |
+
**Minimum Requirements**:
|
| 185 |
+
- CPU: 4 cores, 2.0 GHz
|
| 186 |
+
- RAM: 8 GB
|
| 187 |
+
- Storage: 50 GB available space
|
| 188 |
+
- Docker: 20.10.0+
|
| 189 |
+
|
| 190 |
+
**Recommended Configuration**:
|
| 191 |
+
- CPU: 8+ cores, 3.0+ GHz
|
| 192 |
+
- RAM: 16+ GB
|
| 193 |
+
- Storage: 100+ GB SSD
|
| 194 |
+
- GPU: CUDA-compatible (for future ML extensions)
|
| 195 |
+
|
| 196 |
+
### Input Data Requirements
|
| 197 |
+
|
| 198 |
+
**Image Specifications**:
|
| 199 |
+
- Format: NIfTI
|
| 200 |
+
- Modality: CT (chest)
|
| 201 |
+
- Resolution: 0.5-2.0 mm³ voxel spacing
|
| 202 |
+
- Matrix size: 512×512 or larger
|
| 203 |
+
- Bit depth: 16-bit signed integers
|
| 204 |
+
|
| 205 |
+
**Annotation Format**:
|
| 206 |
+
```csv
|
| 207 |
+
ct_nifti_file,nodule_id,coordX,coordY,coordZ,w,h,d,Malignant_lbl
|
| 208 |
+
DLCS_0001.nii.gz,DLCS_0001_01,-106.55,-63.84,-211.68,4.39,4.39,4.30,0
|
| 209 |
+
```
|
| 210 |
+
|
| 211 |
+
**Required Columns**:
|
| 212 |
+
- `ct_nifti_file`: Image filename
|
| 213 |
+
- `coordX/Y/Z`: World coordinates (mm)
|
| 214 |
+
- `w/h/d`: Bounding box dimensions (mm)
|
| 215 |
+
- `Malignant_lbl`: Binary label (optional)
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
|
| 220 |
+
|
| 221 |
+
## API Reference
|
| 222 |
+
|
| 223 |
+
### Bash Script Interface
|
| 224 |
+
|
| 225 |
+
#### Segmentation Script
|
| 226 |
+
```bash
|
| 227 |
+
./scripts/DLCS24_KNN_2mm_Extend_Seg.sh
|
| 228 |
+
```
|
| 229 |
+
|
| 230 |
+
**Configuration Variables**:
|
| 231 |
+
```bash
|
| 232 |
+
DATASET_NAME="DLCSD24" # Dataset identifier
|
| 233 |
+
SEG_ALG="knn" # Segmentation algorithm
|
| 234 |
+
EXPANSION_MM=2.0 # Expansion radius (mm)
|
| 235 |
+
RAW_DATA_PATH="/app/demofolder/data/DLCS24/"
|
| 236 |
+
DATASET_CSV="/app/demofolder/data/DLCSD24_Annotations_N2.csv"
|
| 237 |
+
```
|
| 238 |
+
|
| 239 |
+
#### Radiomics Script
|
| 240 |
+
```bash
|
| 241 |
+
./scripts/DLCS24_KNN_2mm_Extend_Radiomics.sh
|
| 242 |
+
```
|
| 243 |
+
|
| 244 |
+
**Additional Parameters**:
|
| 245 |
+
```bash
|
| 246 |
+
EXTRACT_RADIOMICS_FLAG="--extract_radiomics"
|
| 247 |
+
PARAMS_JSON="/app/scr/Pyradiomics_feature_extarctor_pram.json"
|
| 248 |
+
```
|
| 249 |
+
|
| 250 |
+
#### Patch Extraction Script
|
| 251 |
+
```bash
|
| 252 |
+
./scripts/DLCS24_CADe_64Qpatch.sh
|
| 253 |
+
```
|
| 254 |
+
|
| 255 |
+
**Patch Parameters**:
|
| 256 |
+
```bash
|
| 257 |
+
PATCH_SIZE="64 64 64" # Voxel dimensions
|
| 258 |
+
NORMALIZATION="-1000 500 0 1" # HU window and output range
|
| 259 |
+
CLIP="True" # Enable intensity clipping
|
| 260 |
+
```
|
| 261 |
+
|
| 262 |
+
### Python API (Internal)
|
| 263 |
+
|
| 264 |
+
#### Segmentation Function
|
| 265 |
+
```python
|
| 266 |
+
def candidateSeg_main():
|
| 267 |
+
"""
|
| 268 |
+
Main segmentation pipeline
|
| 269 |
+
|
| 270 |
+
Parameters:
|
| 271 |
+
-----------
|
| 272 |
+
raw_data_path : str
|
| 273 |
+
Path to input CT images
|
| 274 |
+
dataset_csv : str
|
| 275 |
+
Path to coordinate annotations
|
| 276 |
+
seg_alg : str
|
| 277 |
+
Segmentation algorithm {'knn', 'gmm', 'fcm', 'otsu'}
|
| 278 |
+
expansion_mm : float
|
| 279 |
+
Expansion radius in millimeters
|
| 280 |
+
|
| 281 |
+
Returns:
|
| 282 |
+
--------
|
| 283 |
+
None (saves masks to disk)
|
| 284 |
+
"""
|
| 285 |
+
```
|
| 286 |
+
|
| 287 |
+
#### Radiomics Function
|
| 288 |
+
```python
|
| 289 |
+
def seg_pyradiomics_main():
|
| 290 |
+
"""
|
| 291 |
+
Radiomics feature extraction pipeline
|
| 292 |
+
|
| 293 |
+
Parameters:
|
| 294 |
+
-----------
|
| 295 |
+
params_json : str
|
| 296 |
+
PyRadiomics configuration file
|
| 297 |
+
extract_radiomics : bool
|
| 298 |
+
Enable feature extraction
|
| 299 |
+
|
| 300 |
+
Returns:
|
| 301 |
+
--------
|
| 302 |
+
features : DataFrame
|
| 303 |
+
Quantitative imaging features
|
| 304 |
+
"""
|
| 305 |
+
```
|
| 306 |
+
|
| 307 |
+
---
|
| 308 |
+
|
| 309 |
+
## Implementation Details
|
| 310 |
+
|
| 311 |
+
### Docker Container Specifications
|
| 312 |
+
|
| 313 |
+
**Base Image**: `ft42/pins:latest`
|
| 314 |
+
**Size**: ~11 GB (includes CUDA libraries)
|
| 315 |
+
**Dependencies**:
|
| 316 |
+
```dockerfile
|
| 317 |
+
# Core medical imaging libraries
|
| 318 |
+
SimpleITK==2.4+
|
| 319 |
+
pyradiomics==3.1.0
|
| 320 |
+
scikit-learn==1.3.0
|
| 321 |
+
|
| 322 |
+
# Deep learning and computer vision
|
| 323 |
+
torch==2.8.0
|
| 324 |
+
torchvision==0.23.0
|
| 325 |
+
monai==1.4.0
|
| 326 |
+
opencv-python-headless==4.11.0
|
| 327 |
+
|
| 328 |
+
# Scientific computing
|
| 329 |
+
numpy==1.24.4
|
| 330 |
+
scipy
|
| 331 |
+
pandas
|
| 332 |
+
scipy==1.11.1
|
| 333 |
+
nibabel==5.1.0
|
| 334 |
+
|
| 335 |
+
# Data processing
|
| 336 |
+
numpy==1.24.3
|
| 337 |
+
pandas==2.0.3
|
| 338 |
+
matplotlib==3.7.1
|
| 339 |
+
|
| 340 |
+
# Utilities
|
| 341 |
+
tqdm==4.65.0
|
| 342 |
+
```
|
| 343 |
+
|
| 344 |
+
### File Organization
|
| 345 |
+
|
| 346 |
+
```
|
| 347 |
+
PiNS/
|
| 348 |
+
├── scripts/
|
| 349 |
+
│ ├── DLCS24_KNN_2mm_Extend_Seg.sh
|
| 350 |
+
│ ├── DLCS24_KNN_2mm_Extend_Radiomics.sh
|
| 351 |
+
│ └── DLCS24_CADe_64Qpatch.sh
|
| 352 |
+
├── scr/
|
| 353 |
+
│ ├── candidateSeg_pipiline.py
|
| 354 |
+
│ ├── candidateSeg_radiomicsExtractor_pipiline.py
|
| 355 |
+
│ ├── candidate_worldCoord_patchExtarctor_pipeline.py
|
| 356 |
+
│ ├── cvseg_utils.py
|
| 357 |
+
│ └── Pyradiomics_feature_extarctor_pram.json
|
| 358 |
+
├── demofolder/
|
| 359 |
+
│ ├── data/
|
| 360 |
+
│ │ ├── DLCS24/
|
| 361 |
+
│ │ └── DLCSD24_Annotations_N2.csv
|
| 362 |
+
│ └── output/
|
| 363 |
+
└── docs/
|
| 364 |
+
├── README.md
|
| 365 |
+
├── TECHNICAL_DOCS.md
|
| 366 |
+
└── HUGGINGFACE_CARD.md
|
| 367 |
+
```
|
| 368 |
+
|
| 369 |
+
### Configuration Management
|
| 370 |
+
|
| 371 |
+
**PyRadiomics Parameters** (`Pyradiomics_feature_extarctor_pram.json`):
|
| 372 |
+
```json
|
| 373 |
+
{
|
| 374 |
+
"binWidth": 25,
|
| 375 |
+
"resampledPixelSpacing": [1, 1, 1],
|
| 376 |
+
"interpolator": "sitkBSpline",
|
| 377 |
+
"labelInterpolator": "sitkNearestNeighbor"
|
| 378 |
+
}
|
| 379 |
+
```
|
| 380 |
+
|
| 381 |
+
**Segmentation Parameters**:
|
| 382 |
+
- K-means clusters: 2 (nodule vs background)
|
| 383 |
+
- Connected component threshold: Largest component
|
| 384 |
+
- Morphological operations: Binary closing with 1mm kernel
|
| 385 |
+
|
| 386 |
+
---
|
| 387 |
+
|
| 388 |
+
|
| 389 |
+
|
| 390 |
+
### Computational Efficiency
|
| 391 |
+
|
| 392 |
+
**Processing Time Analysis**:
|
| 393 |
+
- Segmentation: 15-30 seconds per nodule
|
| 394 |
+
- Radiomics extraction: 5-10 seconds per mask
|
| 395 |
+
- Patch extraction: 2-5 seconds per patch
|
| 396 |
+
- Total pipeline: <2 minutes per case
|
| 397 |
+
|
| 398 |
+
**Scalability Analysis**:
|
| 399 |
+
- Linear scaling with nodule count
|
| 400 |
+
- Memory usage: ~500 MB per concurrent image
|
| 401 |
+
- Disk I/O: ~50 MB/s sustained throughput
|
| 402 |
+
- CPU utilization: 85-95% (multi-threaded operations)
|
| 403 |
+
|
| 404 |
+
---
|
| 405 |
+
|
| 406 |
+
## Research Applications
|
| 407 |
+
|
| 408 |
+
### Diagnostic Imaging
|
| 409 |
+
|
| 410 |
+
**Lung Cancer Screening**:
|
| 411 |
+
- Automated nodule characterization
|
| 412 |
+
- Growth assessment in follow-up studies
|
| 413 |
+
- Risk stratification based on radiomics profiles
|
| 414 |
+
|
| 415 |
+
**Research Applications**:
|
| 416 |
+
- Biomarker discovery studies
|
| 417 |
+
- Machine learning dataset preparation
|
| 418 |
+
- Multi-institutional validation studies
|
| 419 |
+
|
| 420 |
+
### Integration Pathways
|
| 421 |
+
|
| 422 |
+
**AI Pipeline Integration**:
|
| 423 |
+
- Preprocessed patch data for CNNs
|
| 424 |
+
- Feature vectors for traditional ML
|
| 425 |
+
- Standardized evaluation protocols
|
| 426 |
+
|
| 427 |
+
---
|
| 428 |
+
|
| 429 |
+
|
| 430 |
+
|
| 431 |
+
## License and Usage Terms
|
| 432 |
+
|
| 433 |
+
### Creative Commons Attribution-NonCommercial 4.0 International (CC-BY-NC-4.0)
|
| 434 |
+
|
| 435 |
+
**Permitted Uses**:
|
| 436 |
+
- Research and educational purposes
|
| 437 |
+
- Academic publications and presentations
|
| 438 |
+
- Non-commercial clinical research
|
| 439 |
+
- Open-source contributions and modifications
|
| 440 |
+
|
| 441 |
+
**Requirements**:
|
| 442 |
+
- Attribution to original authors and PiNS toolkit
|
| 443 |
+
- Citation of relevant publications
|
| 444 |
+
- Sharing of derivative works under same license
|
| 445 |
+
- Clear indication of any modifications made
|
| 446 |
+
|
| 447 |
+
**Restrictions**:
|
| 448 |
+
- Commercial use requires separate licensing agreement
|
| 449 |
+
- No warranty or liability provided
|
| 450 |
+
- Contact [email protected] for commercial licensing
|
| 451 |
+
|
| 452 |
+
**Citation Requirements**:
|
| 453 |
+
|
| 454 |
+
```bibtex
|
| 455 |
+
@software{pins2025,
|
| 456 |
+
title={PiNS: Point-driven Nodule Segmentation Toolkit },
|
| 457 |
+
author={Fakrul Islam Tushar},
|
| 458 |
+
year={2025},
|
| 459 |
+
url={https://github.com/fitushar/PiNS},
|
| 460 |
+
version={1.0.0},
|
| 461 |
+
doi={10.5281/zenodo.17171571},
|
| 462 |
+
license={CC-BY-NC-4.0}
|
| 463 |
+
}
|
| 464 |
+
```
|
| 465 |
+
|
| 466 |
+
---
|
| 467 |
+
|
| 468 |
+
## Validation & Quality Assurance
|
| 469 |
+
|
| 470 |
+
**Evaluation Criteria:** In the absence of voxel-level ground truth, we adopted a bounding box–supervised evaluation strategy to assess segmentation performance. Each CT volume was accompanied by annotations specifying the nodule center in world coordinates and its dimensions in millimeters, which were converted into voxel indices using the image spacing and clipped to the volume boundaries. A binary mask representing the bounding box was then constructed and used as a weak surrogate for ground truth. we extracted a patch centered on the bounding box, extending it by a fixed margin (64 voxels) to define the volume of interest (VOI). Predicted segmentation masks were cropped to the same VOI-constrained region of interest, and performance was quantified in terms of Dice similarity coefficient. Metrics were computed per lesion. This evaluation strategy enables consistent comparison of segmentation algorithms under weak supervision while acknowledging the limitations of not having voxel-level annotations.
|
| 471 |
+
|
| 472 |
+
Segmentation performance of **KNN (ours PiNS)**, **VISTA3D auto**, and **VISTA3D points** ([He et al. 2024](https://github.com/Project-MONAI/VISTA/tree/main/vista3d)) across different nodule size buckets. (top) Bar plots display the mean Dice similarity coefficient for each model and size category. (buttom) Boxplots show the distribution of Dice scores, with boxes representing the interquartile range, horizontal lines indicating the median, whiskers extending to 1.5× the interquartile range, and circles denoting outliers.
|
| 473 |
+
|
| 474 |
+
<p align="center">
|
| 475 |
+
<img src="assets/Segmentation_Evaluation_KNNVista3Dauto_DLCS24_HIST.png" alt="(a)" width="700">
|
| 476 |
+
</p>
|
| 477 |
+
|
| 478 |
+
<p align="center">
|
| 479 |
+
<img src="assets/Segmentation_Evaluation_KNNVista3Dauto_DLCS24_BOX.png" alt="(b)" width="700">
|
| 480 |
+
</p>
|
| 481 |
+
## Limitations & Considerations
|
| 482 |
+
|
| 483 |
+
### Current Limitations
|
| 484 |
+
- **Nodule Size**: Optimized for nodules 3-30mm diameter
|
| 485 |
+
- **Image Quality**: Requires standard clinical CT protocols
|
| 486 |
+
- **Coordinate Accuracy**: Dependent on annotation precision
|
| 487 |
+
- **Processing Time**: Sequential processing (parallelization possible)
|
| 488 |
+
|
| 489 |
+
## Contributing & Development
|
| 490 |
+
|
| 491 |
+
### Research Collaborations
|
| 492 |
+
We welcome collaborations from:
|
| 493 |
+
- **Academic Medical Centers**
|
| 494 |
+
- **Radiology Departments**
|
| 495 |
+
- **Medical AI Companies**
|
| 496 |
+
- **Open Source Contributors**
|
| 497 |
+
|
| 498 |
+
|
| 499 |
+
|
| 500 |
+
|
| 501 |
+
|
| 502 |
+
### Related Publications
|
| 503 |
+
1. **AI in Lung Health: Benchmarking** : [Tushar et al. arxiv (2024)](https://arxiv.org/abs/2405.04605)
|
| 504 |
+
2. **AI in Lung Health: Benchmarking** : [https://github.com/fitushar/AI-in-Lung-Health-Benchmarking](https://github.com/fitushar/AI-in-Lung-Health-Benchmarking-Detection-and-Diagnostic-Models-Across-Multiple-CT-Scan-Datasets)
|
| 505 |
+
4. **DLCS Dataset**: [Wang et al. Radiology AI 2024](https://doi.org/10.1148/ryai.240248);[Zenedo](https://zenodo.org/records/13799069)
|
| 506 |
+
5. **SYN-LUNGS**: [Tushar et al., arxiv 2025](https://arxiv.org/abs/2502.21187)
|
| 507 |
+
6. **Refining Focus in AI for Lung Cancer:** Comparing Lesion-Centric and Chest-Region Models with Performance Insights from Internal and External Validation. [](https://arxiv.org/abs/2411.16823)
|
| 508 |
+
7. **Peritumoral Expansion Radiomics** for Improved Lung Cancer Classification. [](https://arxiv.org/abs/2411.16008)
|
| 509 |
+
8. **PyRadiomics Framework**: [van Griethuysen et al., Cancer Research 2017](https://pubmed.ncbi.nlm.nih.gov/29092951/)
|
| 510 |
+
|
| 511 |
+
|
| 512 |
+
|
| 513 |
+
## License & Usage
|
| 514 |
+
**license: cc-by-nc-4.0**
|
| 515 |
+
### Academic Use License
|
| 516 |
+
This project is released for **academic and non-commercial research purposes only**.
|
| 517 |
+
You are free to use, modify, and distribute this code under the following conditions:
|
| 518 |
+
- ✅ Academic research use permitted
|
| 519 |
+
- ✅ Modification and redistribution permitted for research
|
| 520 |
+
- ❌ Commercial use prohibited without prior written permission
|
| 521 |
+
For commercial licensing inquiries, please contact: [email protected]
|
| 522 |
+
|
| 523 |
+
|
| 524 |
+
## Support & Community
|
| 525 |
+
|
| 526 |
+
### Getting Help
|
| 527 |
+
- **📖 Documentation**: [Comprehensive technical docs](https://github.com/fitushar/PiNS/blob/main/docs/)
|
| 528 |
+
- **🐛 Issues**: [GitHub Issues](https://github.com/fitushar/PiNS/issues)
|
| 529 |
+
- **💬 Discussions**: [GitHub Discussions](https://github.com/fitushar/PiNS/discussions)
|
| 530 |
+
- **📧 Email**: [email protected] ; [email protected]
|
| 531 |
+
|
| 532 |
+
### Community Stats
|
| 533 |
+
- **Publications**: 5+ research papers
|
| 534 |
+
- **Contributors**: Active open-source community
|
| 535 |
+
|
output/DLCS24_KNN_2mm_Extend_Seg/DLCS_0001_mask.nii.gz
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e5400fcb1e8d9e1c30f78638101ae7dc0764c78f0ae615c907215b722bb7650b
|
| 3 |
+
size 145806
|
output/DLCS24_KNN_2mm_Extend_Seg/DLCS_0002_mask.nii.gz
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:50084e964263391492028cee2bbce70387ee5f4d6f6dc1bc5178e38ffd18a9bd
|
| 3 |
+
size 145799
|
scr/.ipynb_checkpoints/segmentation_utils-checkpoint.py
ADDED
|
@@ -0,0 +1,490 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import SimpleITK as sitk
|
| 2 |
+
import numpy as np
|
| 3 |
+
import pandas as pd
|
| 4 |
+
import os
|
| 5 |
+
import radiomics
|
| 6 |
+
from radiomics import featureextractor
|
| 7 |
+
import argparse
|
| 8 |
+
import numpy as np
|
| 9 |
+
from sklearn.cluster import KMeans
|
| 10 |
+
import scipy.ndimage as ndimage
|
| 11 |
+
from sklearn.mixture import GaussianMixture
|
| 12 |
+
import skfuzzy as fuzz
|
| 13 |
+
from skimage.filters import threshold_otsu
|
| 14 |
+
from skimage.filters import threshold_otsu
|
| 15 |
+
from skimage.segmentation import watershed
|
| 16 |
+
from skimage.feature import peak_local_max
|
| 17 |
+
from skimage import morphology
|
| 18 |
+
from scipy.ndimage import distance_transform_edt, binary_erosion
|
| 19 |
+
from matplotlib import colors
|
| 20 |
+
import numpy as np
|
| 21 |
+
import matplotlib.pyplot as plt
|
| 22 |
+
import SimpleITK as sitk
|
| 23 |
+
import numpy as np
|
| 24 |
+
import pandas as pd
|
| 25 |
+
from matplotlib import colors
|
| 26 |
+
import cv2
|
| 27 |
+
from skimage import measure
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def make_bold(text):
|
| 31 |
+
return f"\033[1m{text}\033[0m"
|
| 32 |
+
|
| 33 |
+
def load_itk_image(filename):
|
| 34 |
+
itkimage = sitk.ReadImage(filename)
|
| 35 |
+
numpyImage = sitk.GetArrayFromImage(itkimage)
|
| 36 |
+
numpyOrigin = itkimage.GetOrigin()
|
| 37 |
+
numpySpacing = itkimage.GetSpacing()
|
| 38 |
+
return numpyImage, numpyOrigin, numpySpacing
|
| 39 |
+
|
| 40 |
+
def normalize_image_to_uint8(image, lower_bound=-1000, upper_bound=100):
|
| 41 |
+
clipped_img = np.clip(image, lower_bound, upper_bound)
|
| 42 |
+
normalized_img = ((clipped_img - lower_bound) / (upper_bound - lower_bound)) * 255.0
|
| 43 |
+
normalized_img = normalized_img.astype(np.uint8)
|
| 44 |
+
return normalized_img
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def segment_nodule_kmeans(ct_image, bbox_center, bbox_whd, margin=5, n_clusters=2):
|
| 50 |
+
"""
|
| 51 |
+
Segments a nodule in a 3D CT image using k-means clustering with a margin around the bounding box.
|
| 52 |
+
|
| 53 |
+
Parameters:
|
| 54 |
+
- ct_image: 3D NumPy array representing the CT image.
|
| 55 |
+
- bbox_center: Tuple of (x, y, z) coordinates for the center of the bounding box.
|
| 56 |
+
- bbox_whd: Tuple of (w, h, d) representing the width, height, and depth of the bounding box.
|
| 57 |
+
- margin: Margin to add around the bounding box (default is 5).
|
| 58 |
+
- n_clusters: Number of clusters to use in k-means (default is 2).
|
| 59 |
+
|
| 60 |
+
Returns:
|
| 61 |
+
- segmented_image: 3D NumPy array with the segmented nodule.
|
| 62 |
+
"""
|
| 63 |
+
|
| 64 |
+
x_center, y_center, z_center = bbox_center
|
| 65 |
+
w, h, d = bbox_whd
|
| 66 |
+
|
| 67 |
+
# Calculate the bounding box with margin
|
| 68 |
+
x_start, x_end = max(0, x_center - w//2 - margin), min(ct_image.shape[0], x_center + w//2 + margin)
|
| 69 |
+
y_start, y_end = max(0, y_center - h//2 - margin), min(ct_image.shape[1], y_center + h//2 + margin)
|
| 70 |
+
z_start, z_end = max(0, z_center - d//2 - margin), min(ct_image.shape[2], z_center + d//2 + margin)
|
| 71 |
+
|
| 72 |
+
bbox_region = ct_image[x_start:x_end, y_start:y_end, z_start:z_end]
|
| 73 |
+
|
| 74 |
+
# Reshape the region for k-means clustering
|
| 75 |
+
flat_region = bbox_region.reshape(-1, 1)
|
| 76 |
+
|
| 77 |
+
# Perform k-means clustering
|
| 78 |
+
kmeans = KMeans(n_clusters=n_clusters, random_state=0).fit(flat_region)
|
| 79 |
+
labels = kmeans.labels_
|
| 80 |
+
|
| 81 |
+
# Reshape the labels back to the original bounding box shape
|
| 82 |
+
clustered_region = labels.reshape(bbox_region.shape)
|
| 83 |
+
|
| 84 |
+
# Assume the nodule is in the cluster with the highest mean intensity
|
| 85 |
+
nodule_cluster = np.argmax(kmeans.cluster_centers_)
|
| 86 |
+
|
| 87 |
+
# Create a binary mask for the nodule
|
| 88 |
+
nodule_mask = (clustered_region == nodule_cluster)
|
| 89 |
+
|
| 90 |
+
# Apply morphological operations to refine the segmentation
|
| 91 |
+
nodule_mask = ndimage.binary_closing(nodule_mask, structure=np.ones((3, 3, 3)))
|
| 92 |
+
nodule_mask = ndimage.binary_opening(nodule_mask, structure=np.ones((2, 2, 2)))
|
| 93 |
+
|
| 94 |
+
# Initialize the segmented image
|
| 95 |
+
segmented_image = np.zeros_like(ct_image, dtype=np.uint8)
|
| 96 |
+
|
| 97 |
+
# Place the nodule mask in the correct position in the segmented image
|
| 98 |
+
segmented_image[x_start:x_end, y_start:y_end, z_start:z_end] = nodule_mask
|
| 99 |
+
|
| 100 |
+
return segmented_image
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def segment_nodule_gmm(ct_image, bbox_center, bbox_whd, margin=5, n_components=2):
|
| 104 |
+
"""
|
| 105 |
+
Segments a nodule in a 3D CT image using a Gaussian Mixture Model with a margin around the bounding box.
|
| 106 |
+
|
| 107 |
+
Parameters:
|
| 108 |
+
- ct_image: 3D NumPy array representing the CT image.
|
| 109 |
+
- bbox_center: Tuple of (x, y, z) coordinates for the center of the bounding box.
|
| 110 |
+
- bbox_whd: Tuple of (w, h, d) representing the width, height, and depth of the bounding box.
|
| 111 |
+
- margin: Margin to add around the bounding box (default is 5).
|
| 112 |
+
- n_components: Number of components to use in the Gaussian Mixture Model (default is 2).
|
| 113 |
+
|
| 114 |
+
Returns:
|
| 115 |
+
- segmented_image: 3D NumPy array with the segmented nodule.
|
| 116 |
+
"""
|
| 117 |
+
|
| 118 |
+
x_center, y_center, z_center = bbox_center
|
| 119 |
+
w, h, d = bbox_whd
|
| 120 |
+
|
| 121 |
+
# Calculate the bounding box with margin
|
| 122 |
+
x_start, x_end = max(0, x_center - w//2 - margin), min(ct_image.shape[0], x_center + w//2 + margin)
|
| 123 |
+
y_start, y_end = max(0, y_center - h//2 - margin), min(ct_image.shape[1], y_center + h//2 + margin)
|
| 124 |
+
z_start, z_end = max(0, z_center - d//2 - margin), min(ct_image.shape[2], z_center + d//2 + margin)
|
| 125 |
+
|
| 126 |
+
bbox_region = ct_image[x_start:x_end, y_start:y_end, z_start:z_end]
|
| 127 |
+
|
| 128 |
+
# Reshape the region for GMM
|
| 129 |
+
flat_region = bbox_region.reshape(-1, 1)
|
| 130 |
+
|
| 131 |
+
# Perform GMM
|
| 132 |
+
gmm = GaussianMixture(n_components=n_components, random_state=0).fit(flat_region)
|
| 133 |
+
labels = gmm.predict(flat_region)
|
| 134 |
+
|
| 135 |
+
# Reshape the labels back to the original bounding box shape
|
| 136 |
+
clustered_region = labels.reshape(bbox_region.shape)
|
| 137 |
+
|
| 138 |
+
# Assume the nodule is in the component with the highest mean intensity
|
| 139 |
+
nodule_component = np.argmax(gmm.means_)
|
| 140 |
+
|
| 141 |
+
# Create a binary mask for the nodule
|
| 142 |
+
nodule_mask = (clustered_region == nodule_component)
|
| 143 |
+
|
| 144 |
+
# Apply morphological operations to refine the segmentation
|
| 145 |
+
nodule_mask = ndimage.binary_closing(nodule_mask, structure=np.ones((3, 3, 3)))
|
| 146 |
+
nodule_mask = ndimage.binary_opening(nodule_mask, structure=np.ones((3, 3, 3)))
|
| 147 |
+
|
| 148 |
+
# Initialize the segmented image
|
| 149 |
+
segmented_image = np.zeros_like(ct_image, dtype=np.uint8)
|
| 150 |
+
|
| 151 |
+
# Place the nodule mask in the correct position in the segmented image
|
| 152 |
+
segmented_image[x_start:x_end, y_start:y_end, z_start:z_end] = nodule_mask
|
| 153 |
+
|
| 154 |
+
return segmented_image
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
def segment_nodule_fcm(ct_image, bbox_center, bbox_whd, margin=5, n_clusters=2):
|
| 158 |
+
"""
|
| 159 |
+
Segments a nodule in a 3D CT image using Fuzzy C-means clustering with a margin around the bounding box.
|
| 160 |
+
|
| 161 |
+
Parameters:
|
| 162 |
+
- ct_image: 3D NumPy array representing the CT image.
|
| 163 |
+
- bbox_center: Tuple of (x, y, z) coordinates for the center of the bounding box.
|
| 164 |
+
- bbox_whd: Tuple of (w, h, d) representing the width, height, and depth of the bounding box.
|
| 165 |
+
- margin: Margin to add around the bounding box (default is 5).
|
| 166 |
+
- n_clusters: Number of clusters to use in Fuzzy C-means (default is 2).
|
| 167 |
+
|
| 168 |
+
Returns:
|
| 169 |
+
- segmented_image: 3D NumPy array with the segmented nodule.
|
| 170 |
+
"""
|
| 171 |
+
|
| 172 |
+
x_center, y_center, z_center = bbox_center
|
| 173 |
+
w, h, d = bbox_whd
|
| 174 |
+
|
| 175 |
+
# Calculate the bounding box with margin
|
| 176 |
+
x_start, x_end = max(0, x_center - w//2 - margin), min(ct_image.shape[0], x_center + w//2 + margin)
|
| 177 |
+
y_start, y_end = max(0, y_center - h//2 - margin), min(ct_image.shape[1], y_center + h//2 + margin)
|
| 178 |
+
z_start, z_end = max(0, z_center - d//2 - margin), min(ct_image.shape[2], z_center + d//2 + margin)
|
| 179 |
+
|
| 180 |
+
bbox_region = ct_image[x_start:x_end, y_start:y_end, z_start:z_end]
|
| 181 |
+
|
| 182 |
+
# Reshape the region for FCM
|
| 183 |
+
flat_region = bbox_region.reshape(-1, 1)
|
| 184 |
+
|
| 185 |
+
# Perform FCM clustering
|
| 186 |
+
cntr, u, u0, d, jm, p, fpc = fuzz.cluster.cmeans(flat_region.T, n_clusters, 2, error=0.005, maxiter=1000, init=None)
|
| 187 |
+
|
| 188 |
+
# Assign each voxel to the cluster with the highest membership
|
| 189 |
+
labels = np.argmax(u, axis=0)
|
| 190 |
+
|
| 191 |
+
# Reshape the labels back to the original bounding box shape
|
| 192 |
+
clustered_region = labels.reshape(bbox_region.shape)
|
| 193 |
+
|
| 194 |
+
# Assume the nodule is in the cluster with the highest mean intensity
|
| 195 |
+
nodule_cluster = np.argmax(cntr)
|
| 196 |
+
|
| 197 |
+
# Create a binary mask for the nodule
|
| 198 |
+
nodule_mask = (clustered_region == nodule_cluster)
|
| 199 |
+
|
| 200 |
+
# Apply morphological operations to refine the segmentation
|
| 201 |
+
nodule_mask = ndimage.binary_closing(nodule_mask, structure=np.ones((3, 3, 3)))
|
| 202 |
+
nodule_mask = ndimage.binary_opening(nodule_mask, structure=np.ones((3, 3, 3)))
|
| 203 |
+
|
| 204 |
+
# Initialize the segmented image
|
| 205 |
+
segmented_image = np.zeros_like(ct_image, dtype=np.uint8)
|
| 206 |
+
|
| 207 |
+
# Place the nodule mask in the correct position in the segmented image
|
| 208 |
+
segmented_image[x_start:x_end, y_start:y_end, z_start:z_end] = nodule_mask
|
| 209 |
+
|
| 210 |
+
return segmented_image
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
def segment_nodule_otsu(ct_image, bbox_center, bbox_whd, margin=5):
|
| 215 |
+
"""
|
| 216 |
+
Segments a nodule in a 3D CT image using Otsu's thresholding with a margin around the bounding box.
|
| 217 |
+
|
| 218 |
+
Parameters:
|
| 219 |
+
- ct_image: 3D NumPy array representing the CT image.
|
| 220 |
+
- bbox_center: Tuple of (x, y, z) coordinates for the center of the bounding box.
|
| 221 |
+
- bbox_whd: Tuple of (w, h, d) representing the width, height, and depth of the bounding box.
|
| 222 |
+
- margin: Margin to add around the bounding box (default is 5).
|
| 223 |
+
|
| 224 |
+
Returns:
|
| 225 |
+
- segmented_image: 3D NumPy array with the segmented nodule.
|
| 226 |
+
"""
|
| 227 |
+
|
| 228 |
+
x_center, y_center, z_center = bbox_center
|
| 229 |
+
w, h, d = bbox_whd
|
| 230 |
+
|
| 231 |
+
# Calculate the bounding box with margin
|
| 232 |
+
x_start, x_end = max(0, x_center - w//2 - margin), min(ct_image.shape[0], x_center + w//2 + margin)
|
| 233 |
+
y_start, y_end = max(0, y_center - h//2 - margin), min(ct_image.shape[1], y_center + h//2 + margin)
|
| 234 |
+
z_start, z_end = max(0, z_center - d//2 - margin), min(ct_image.shape[2], z_center + d//2 + margin)
|
| 235 |
+
|
| 236 |
+
bbox_region = ct_image[x_start:x_end, y_start:y_end, z_start:z_end]
|
| 237 |
+
|
| 238 |
+
# Flatten the region for thresholding
|
| 239 |
+
flat_region = bbox_region.flatten()
|
| 240 |
+
|
| 241 |
+
# Calculate the Otsu threshold
|
| 242 |
+
otsu_threshold = threshold_otsu(flat_region)
|
| 243 |
+
|
| 244 |
+
# Apply the threshold to create a binary mask
|
| 245 |
+
nodule_mask = bbox_region >= otsu_threshold
|
| 246 |
+
|
| 247 |
+
# Apply morphological operations to refine the segmentation
|
| 248 |
+
nodule_mask = ndimage.binary_closing(nodule_mask, structure=np.ones((3, 3, 3)))
|
| 249 |
+
nodule_mask = ndimage.binary_opening(nodule_mask, structure=np.ones((3, 3, 3)))
|
| 250 |
+
|
| 251 |
+
# Initialize the segmented image
|
| 252 |
+
segmented_image = np.zeros_like(ct_image, dtype=np.uint8)
|
| 253 |
+
|
| 254 |
+
# Place the nodule mask in the correct position in the segmented image
|
| 255 |
+
segmented_image[x_start:x_end, y_start:y_end, z_start:z_end] = nodule_mask
|
| 256 |
+
|
| 257 |
+
return segmented_image
|
| 258 |
+
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
def expand_mask_by_distance(segmented_nodule_gmm, spacing, expansion_mm):
|
| 262 |
+
"""
|
| 263 |
+
Expands the segmentation mask by a given distance in mm in all directions by directly updating pixel values.
|
| 264 |
+
|
| 265 |
+
Parameters:
|
| 266 |
+
segmented_nodule_gmm (numpy array): 3D binary mask of the nodule (1 for nodule, 0 for background).
|
| 267 |
+
spacing (tuple): Spacing of the image in mm for each voxel, given as (spacing_x, spacing_y, spacing_z).
|
| 268 |
+
expansion_mm (float): Distance to expand the mask in millimeters.
|
| 269 |
+
|
| 270 |
+
Returns:
|
| 271 |
+
numpy array: Expanded segmentation mask.
|
| 272 |
+
"""
|
| 273 |
+
# Reorder spacing to match the numpy array's (z, y, x) format
|
| 274 |
+
spacing_reordered = (spacing[2], spacing[1], spacing[0]) # (spacing_z, spacing_y, spacing_x)
|
| 275 |
+
|
| 276 |
+
# Calculate the number of pixels to expand in each dimension
|
| 277 |
+
expand_pixels = np.array([int(np.round(expansion_mm / s)) for s in spacing_reordered])
|
| 278 |
+
|
| 279 |
+
# Create a new expanded mask with the same shape
|
| 280 |
+
expanded_mask = np.zeros_like(segmented_nodule_gmm)
|
| 281 |
+
|
| 282 |
+
# Get the coordinates of all white pixels in the original mask
|
| 283 |
+
white_pixel_coords = np.argwhere(segmented_nodule_gmm == 1)
|
| 284 |
+
|
| 285 |
+
# Expand each white pixel by adding the specified number of pixels in each direction
|
| 286 |
+
for coord in white_pixel_coords:
|
| 287 |
+
z, y, x = coord # Extract the z, y, x coordinates of each white pixel
|
| 288 |
+
|
| 289 |
+
# Define the range to expand for each coordinate
|
| 290 |
+
z_range = range(max(0, z - expand_pixels[0]), min(segmented_nodule_gmm.shape[0], z + expand_pixels[0] + 1))
|
| 291 |
+
y_range = range(max(0, y - expand_pixels[1]), min(segmented_nodule_gmm.shape[1], y + expand_pixels[1] + 1))
|
| 292 |
+
x_range = range(max(0, x - expand_pixels[2]), min(segmented_nodule_gmm.shape[2], x + expand_pixels[2] + 1))
|
| 293 |
+
|
| 294 |
+
# Update the new mask by setting all pixels in this range to 1
|
| 295 |
+
for z_new in z_range:
|
| 296 |
+
for y_new in y_range:
|
| 297 |
+
for x_new in x_range:
|
| 298 |
+
expanded_mask[z_new, y_new, x_new] = 1
|
| 299 |
+
|
| 300 |
+
return expanded_mask
|
| 301 |
+
|
| 302 |
+
|
| 303 |
+
def find_nodule_lobe(cccwhd, lung_mask, class_map):
|
| 304 |
+
"""
|
| 305 |
+
Determine the lung lobe where a nodule is located based on a 3D mask and bounding box.
|
| 306 |
+
|
| 307 |
+
Parameters:
|
| 308 |
+
cccwhd (list or tuple): Bounding box in the format [center_x, center_y, center_z, width, height, depth].
|
| 309 |
+
lung_mask (numpy array): 3D array representing the lung mask with different lung regions.
|
| 310 |
+
class_map (dict): Dictionary mapping lung region labels to their names.
|
| 311 |
+
|
| 312 |
+
Returns:
|
| 313 |
+
str: Name of the lung lobe where the nodule is located.
|
| 314 |
+
"""
|
| 315 |
+
center_x, center_y, center_z, width, height, depth = cccwhd
|
| 316 |
+
|
| 317 |
+
# Calculate the bounding box limits
|
| 318 |
+
start_x = int(center_x - width // 2)
|
| 319 |
+
end_x = int(center_x + width // 2)
|
| 320 |
+
start_y = int(center_y - height // 2)
|
| 321 |
+
end_y = int(center_y + height // 2)
|
| 322 |
+
start_z = int(center_z - depth // 2)
|
| 323 |
+
end_z = int(center_z + depth // 2)
|
| 324 |
+
|
| 325 |
+
# Ensure the indices are within the mask dimensions
|
| 326 |
+
start_x = max(0, start_x)
|
| 327 |
+
end_x = min(lung_mask.shape[0], end_x)
|
| 328 |
+
start_y = max(0, start_y)
|
| 329 |
+
end_y = min(lung_mask.shape[1], end_y)
|
| 330 |
+
start_z = max(0, start_z)
|
| 331 |
+
end_z = min(lung_mask.shape[2], end_z)
|
| 332 |
+
|
| 333 |
+
# Extract the region of interest (ROI) from the mask
|
| 334 |
+
roi = lung_mask[start_x:end_x, start_y:end_y, start_z:end_z]
|
| 335 |
+
|
| 336 |
+
# Count the occurrences of each lobe label within the ROI
|
| 337 |
+
unique, counts = np.unique(roi, return_counts=True)
|
| 338 |
+
label_counts = dict(zip(unique, counts))
|
| 339 |
+
|
| 340 |
+
# Exclude the background (label 0)
|
| 341 |
+
if 0 in label_counts:
|
| 342 |
+
del label_counts[0]
|
| 343 |
+
|
| 344 |
+
# Find the label with the maximum count
|
| 345 |
+
if label_counts:
|
| 346 |
+
nodule_lobe = max(label_counts, key=label_counts.get)
|
| 347 |
+
else:
|
| 348 |
+
nodule_lobe = None
|
| 349 |
+
|
| 350 |
+
# Map the label to the corresponding lung lobe
|
| 351 |
+
if nodule_lobe is not None:
|
| 352 |
+
nodule_lobe_name = class_map["lungs"][nodule_lobe]
|
| 353 |
+
else:
|
| 354 |
+
nodule_lobe_name = "Undefined"
|
| 355 |
+
|
| 356 |
+
return nodule_lobe_name
|
| 357 |
+
|
| 358 |
+
|
| 359 |
+
def find_nodule_lobe_and_distance(cccwhd, lung_mask, class_map,spacing):
|
| 360 |
+
"""
|
| 361 |
+
Determine the lung lobe where a nodule is located and measure its distance from the lung wall.
|
| 362 |
+
|
| 363 |
+
Parameters:
|
| 364 |
+
cccwhd (list or tuple): Bounding box in the format [center_x, center_y, center_z, width, height, depth].
|
| 365 |
+
lung_mask (numpy array): 3D array representing the lung mask with different lung regions.
|
| 366 |
+
class_map (dict): Dictionary mapping lung region labels to their names.
|
| 367 |
+
|
| 368 |
+
Returns:
|
| 369 |
+
tuple: (Name of the lung lobe, Distance from the lung wall)
|
| 370 |
+
"""
|
| 371 |
+
center_x, center_y, center_z, width, height, depth = cccwhd
|
| 372 |
+
|
| 373 |
+
# Calculate the bounding box limits
|
| 374 |
+
start_x = int(center_x - width // 2)
|
| 375 |
+
end_x = int(center_x + width // 2)
|
| 376 |
+
start_y = int(center_y - height // 2)
|
| 377 |
+
end_y = int(center_y + height // 2)
|
| 378 |
+
start_z = int(center_z - depth // 2)
|
| 379 |
+
end_z = int(center_z + depth // 2)
|
| 380 |
+
|
| 381 |
+
# Ensure the indices are within the mask dimensions
|
| 382 |
+
start_x = max(0, start_x)
|
| 383 |
+
end_x = min(lung_mask.shape[0], end_x)
|
| 384 |
+
start_y = max(0, start_y)
|
| 385 |
+
end_y = min(lung_mask.shape[1], end_y)
|
| 386 |
+
start_z = max(0, start_z)
|
| 387 |
+
end_z = min(lung_mask.shape[2], end_z)
|
| 388 |
+
|
| 389 |
+
# Extract the region of interest (ROI) from the mask
|
| 390 |
+
roi = lung_mask[start_x:end_x, start_y:end_y, start_z:end_z]
|
| 391 |
+
|
| 392 |
+
# Count the occurrences of each lobe label within the ROI
|
| 393 |
+
unique, counts = np.unique(roi, return_counts=True)
|
| 394 |
+
label_counts = dict(zip(unique, counts))
|
| 395 |
+
|
| 396 |
+
# Exclude the background (label 0)
|
| 397 |
+
if 0 in label_counts:
|
| 398 |
+
del label_counts[0]
|
| 399 |
+
|
| 400 |
+
# Find the label with the maximum count
|
| 401 |
+
if label_counts:
|
| 402 |
+
nodule_lobe = max(label_counts, key=label_counts.get)
|
| 403 |
+
else:
|
| 404 |
+
nodule_lobe = None
|
| 405 |
+
|
| 406 |
+
# Map the label to the corresponding lung lobe
|
| 407 |
+
if nodule_lobe is not None:
|
| 408 |
+
nodule_lobe_name = class_map["lungs"][nodule_lobe]
|
| 409 |
+
else:
|
| 410 |
+
nodule_lobe_name = "Undefined"
|
| 411 |
+
|
| 412 |
+
# Calculate the distance from the nodule centroid to the nearest lung wall
|
| 413 |
+
nodule_centroid = np.array([center_x, center_y, center_z])
|
| 414 |
+
|
| 415 |
+
# Create a binary lung mask where lung region is 1 and outside lung is 0
|
| 416 |
+
lung_binary_mask = lung_mask > 0
|
| 417 |
+
|
| 418 |
+
# Create the lung wall mask by finding the outer boundary
|
| 419 |
+
# Use binary erosion to shrink the lung mask, then subtract it from the original mask to get the boundary
|
| 420 |
+
lung_eroded = binary_erosion(lung_binary_mask)
|
| 421 |
+
lung_wall_mask = lung_binary_mask & ~lung_eroded # Lung wall mask is the outermost boundary (contour)
|
| 422 |
+
|
| 423 |
+
# Compute the distance transform from the lung wall
|
| 424 |
+
distance_transform = distance_transform_edt(~lung_wall_mask) # Compute distance to nearest lung boundary
|
| 425 |
+
|
| 426 |
+
|
| 427 |
+
|
| 428 |
+
# Get the distance from the nodule centroid to the nearest lung wall in voxel units
|
| 429 |
+
voxel_distance_to_lung_wall = distance_transform[center_x, center_y, center_z]
|
| 430 |
+
|
| 431 |
+
# Convert voxel distance to real-world distance in mm
|
| 432 |
+
physical_distance_to_lung_wall = voxel_distance_to_lung_wall * np.sqrt(
|
| 433 |
+
spacing[0]**2 + spacing[1]**2 + spacing[2]**2
|
| 434 |
+
)
|
| 435 |
+
|
| 436 |
+
|
| 437 |
+
|
| 438 |
+
return nodule_lobe_name, voxel_distance_to_lung_wall,physical_distance_to_lung_wall
|
| 439 |
+
|
| 440 |
+
|
| 441 |
+
# +
|
| 442 |
+
def expand_mask_by_distance(segmented_nodule_gmm, spacing, expansion_mm):
|
| 443 |
+
"""
|
| 444 |
+
Expands the segmentation mask by a given distance in mm in all directions by directly updating pixel values.
|
| 445 |
+
|
| 446 |
+
Parameters:
|
| 447 |
+
segmented_nodule_gmm (numpy array): 3D binary mask of the nodule (1 for nodule, 0 for background).
|
| 448 |
+
spacing (tuple): Spacing of the image in mm for each voxel, given as (spacing_x, spacing_y, spacing_z).
|
| 449 |
+
expansion_mm (float): Distance to expand the mask in millimeters.
|
| 450 |
+
|
| 451 |
+
Returns:
|
| 452 |
+
numpy array: Expanded segmentation mask.
|
| 453 |
+
"""
|
| 454 |
+
# Reorder spacing to match the numpy array's (z, y, x) format
|
| 455 |
+
spacing_reordered = (spacing[2], spacing[1], spacing[0]) # (spacing_z, spacing_y, spacing_x)
|
| 456 |
+
|
| 457 |
+
# Calculate the number of pixels to expand in each dimension
|
| 458 |
+
expand_pixels = np.array([int(np.round(expansion_mm / s)) for s in spacing_reordered])
|
| 459 |
+
|
| 460 |
+
# Create a new expanded mask with the same shape
|
| 461 |
+
expanded_mask = np.zeros_like(segmented_nodule_gmm)
|
| 462 |
+
|
| 463 |
+
# Get the coordinates of all white pixels in the original mask
|
| 464 |
+
white_pixel_coords = np.argwhere(segmented_nodule_gmm == 1)
|
| 465 |
+
|
| 466 |
+
# Expand each white pixel by adding the specified number of pixels in each direction
|
| 467 |
+
for coord in white_pixel_coords:
|
| 468 |
+
z, y, x = coord # Extract the z, y, x coordinates of each white pixel
|
| 469 |
+
|
| 470 |
+
# Define the range to expand for each coordinate
|
| 471 |
+
z_range = range(max(0, z - expand_pixels[0]), min(segmented_nodule_gmm.shape[0], z + expand_pixels[0] + 1))
|
| 472 |
+
y_range = range(max(0, y - expand_pixels[1]), min(segmented_nodule_gmm.shape[1], y + expand_pixels[1] + 1))
|
| 473 |
+
x_range = range(max(0, x - expand_pixels[2]), min(segmented_nodule_gmm.shape[2], x + expand_pixels[2] + 1))
|
| 474 |
+
|
| 475 |
+
# Update the new mask by setting all pixels in this range to 1
|
| 476 |
+
for z_new in z_range:
|
| 477 |
+
for y_new in y_range:
|
| 478 |
+
for x_new in x_range:
|
| 479 |
+
expanded_mask[z_new, y_new, x_new] = 1
|
| 480 |
+
|
| 481 |
+
return expanded_mask
|
| 482 |
+
|
| 483 |
+
|
| 484 |
+
|
| 485 |
+
# Function to plot the contours of a mask
|
| 486 |
+
def plot_contours(ax, mask, color, linewidth=1.5):
|
| 487 |
+
contours = measure.find_contours(mask, level=0.5) # Find contours at a constant level
|
| 488 |
+
for contour in contours:
|
| 489 |
+
ax.plot(contour[:, 1], contour[:, 0], color=color, linewidth=linewidth)
|
| 490 |
+
|
scr/Pyradiomics_feature_extarctor_pram.json
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"binWidth": 25,
|
| 3 |
+
"resampledPixelSpacing": [1, 1, 1],
|
| 4 |
+
"interpolator": "sitkBSpline",
|
| 5 |
+
"labelInterpolator": "sitkNearestNeighbor"
|
| 6 |
+
}
|
scr/__pycache__/cvseg_utils.cpython-310.pyc
ADDED
|
Binary file (11.6 kB). View file
|
|
|
scr/__pycache__/cvseg_utils.cpython-311.pyc
ADDED
|
Binary file (20.7 kB). View file
|
|
|
scr/__pycache__/cvseg_utils.cpython-38.pyc
ADDED
|
Binary file (12 kB). View file
|
|
|
scr/__pycache__/cvseg_utils.cpython-39.pyc
ADDED
|
Binary file (11.9 kB). View file
|
|
|
scr/__pycache__/segmentation_utils.cpython-38.pyc
ADDED
|
Binary file (12.2 kB). View file
|
|
|
scr/candidateSeg_pipiline.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from cvseg_utils import *
|
| 2 |
+
import warnings
|
| 3 |
+
warnings.filterwarnings("ignore", message="GLCM is symmetrical, therefore Sum Average = 2 * Joint Average")
|
| 4 |
+
import os
|
| 5 |
+
import logging
|
| 6 |
+
from datetime import datetime
|
| 7 |
+
|
| 8 |
+
def seg_main():
|
| 9 |
+
parser = argparse.ArgumentParser(description='Nodule segmentation and feature extraction from CT images.')
|
| 10 |
+
parser.add_argument('--raw_data_path', type=str, required=True, help='Path to raw CT images')
|
| 11 |
+
parser.add_argument('--csv_save_path', type=str, required=True, help='Path to save the CSV files')
|
| 12 |
+
parser.add_argument('--dataset_csv', type=str, required=True, help='Path to the dataset CSV')
|
| 13 |
+
parser.add_argument('--nifti_clm_name', type=str, required=True, help='name to the nifti column name')
|
| 14 |
+
parser.add_argument('--unique_Annotation_id', type=str, help='Column for unique annotation ID')
|
| 15 |
+
parser.add_argument('--Malignant_lbl', type=str, required=True, help='Column name for malignancy labels')
|
| 16 |
+
parser.add_argument('--coordX', type=str, required=True, help='Column name for X coordinate')
|
| 17 |
+
parser.add_argument('--coordY', type=str, required=True, help='Column name for Y coordinate')
|
| 18 |
+
parser.add_argument('--coordZ', type=str, required=True, help='Column name for Z coordinate')
|
| 19 |
+
parser.add_argument('--w', type=str, required=True, help='Column name for width')
|
| 20 |
+
parser.add_argument('--h', type=str, required=True, help='Column name for height')
|
| 21 |
+
parser.add_argument('--d', type=str, required=True, help='Column name for depth')
|
| 22 |
+
parser.add_argument('--seg_alg', type=str, default='gmm', choices=['gmm', 'knn', 'fcm', 'otsu'], help='Segmentation algorithm to use')
|
| 23 |
+
parser.add_argument('--dataset_name', type=str, default='DLCS24', help='Dataset to use')
|
| 24 |
+
parser.add_argument('--expansion_mm', type=float, default=1.0, help='Expansion in mm')
|
| 25 |
+
parser.add_argument('--use_expand', action='store_true', help='Use expansion if set')
|
| 26 |
+
parser.add_argument('--params_json', type=str, required=True, help="Path to JSON file with radiomics parameters")
|
| 27 |
+
parser.add_argument('--save_the_generated_mask', action='store_true', help='Save generated segmentation mask')
|
| 28 |
+
parser.add_argument('--save_nifti_path', type=str, help='Path to save the nifti files')
|
| 29 |
+
|
| 30 |
+
args = parser.parse_args()
|
| 31 |
+
|
| 32 |
+
df = pd.read_csv(args.dataset_csv)
|
| 33 |
+
final_dect = df[args.nifti_clm_name].unique()
|
| 34 |
+
|
| 35 |
+
for dictonary_list_i, ct_filename in enumerate(final_dect):
|
| 36 |
+
try:
|
| 37 |
+
filtered_df = df[df[args.nifti_clm_name] == ct_filename].reset_index()
|
| 38 |
+
ct_nifti_path = os.path.join(args.raw_data_path, ct_filename)
|
| 39 |
+
ct_image = sitk.ReadImage(ct_nifti_path)
|
| 40 |
+
ct_array = sitk.GetArrayFromImage(ct_image)
|
| 41 |
+
spacing = ct_image.GetSpacing()
|
| 42 |
+
|
| 43 |
+
full_mask_array = np.zeros_like(ct_array, dtype=np.uint8)
|
| 44 |
+
|
| 45 |
+
for idx, row in filtered_df.iterrows():
|
| 46 |
+
worldCoord = np.asarray([row[args.coordX], row[args.coordY], row[args.coordZ]])
|
| 47 |
+
voxelCoord = ct_image.TransformPhysicalPointToIndex(worldCoord.tolist())
|
| 48 |
+
spacing = ct_image.GetSpacing()
|
| 49 |
+
w = int(row[args.w] / spacing[0])
|
| 50 |
+
h = int(row[args.h] / spacing[1])
|
| 51 |
+
d = int(row[args.d] / spacing[2])
|
| 52 |
+
bbox_center = [voxelCoord[2], voxelCoord[1], voxelCoord[0]]
|
| 53 |
+
bbox_whd = [d, h, w]
|
| 54 |
+
|
| 55 |
+
if args.seg_alg == 'gmm':
|
| 56 |
+
mask_image_array = segment_nodule_gmm(ct_array, bbox_center, bbox_whd)
|
| 57 |
+
elif args.seg_alg == 'knn':
|
| 58 |
+
mask_image_array = segment_nodule_kmeans(ct_array, bbox_center, bbox_whd)
|
| 59 |
+
elif args.seg_alg == 'fcm':
|
| 60 |
+
mask_image_array = segment_nodule_fcm(ct_array, bbox_center, bbox_whd)
|
| 61 |
+
elif args.seg_alg == 'otsu':
|
| 62 |
+
mask_image_array = segment_nodule_otsu(ct_array, bbox_center, bbox_whd)
|
| 63 |
+
|
| 64 |
+
if args.use_expand:
|
| 65 |
+
mask_image_array = expand_mask_by_distance(mask_image_array, spacing=spacing, expansion_mm=args.expansion_mm)
|
| 66 |
+
#print("Segmented mask sum:", np.sum(mask_image_array))
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
full_mask_array[mask_image_array==1] = 1
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
if args.save_the_generated_mask:
|
| 74 |
+
print("Segmented mask sum:", np.sum(full_mask_array))
|
| 75 |
+
combined_mask_image = sitk.GetImageFromArray(full_mask_array)
|
| 76 |
+
combined_mask_image.SetSpacing(ct_image.GetSpacing())
|
| 77 |
+
combined_mask_image.SetDirection(ct_image.GetDirection())
|
| 78 |
+
combined_mask_image.SetOrigin(ct_image.GetOrigin())
|
| 79 |
+
mask_filename = ct_filename.split('.nii')[0]+"_mask.nii.gz"
|
| 80 |
+
sitk.WriteImage(combined_mask_image, os.path.join(args.save_nifti_path, mask_filename))
|
| 81 |
+
print(f"Saved {mask_filename}")
|
| 82 |
+
except Exception as e:
|
| 83 |
+
print(f"Error processing {ct_filename}: {e}")
|
| 84 |
+
|
| 85 |
+
if __name__ == "__main__":
|
| 86 |
+
seg_main()
|
scr/candidateSeg_radiomicsExtractor_pipiline.py
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from cvseg_utils import*
|
| 2 |
+
import warnings
|
| 3 |
+
warnings.filterwarnings("ignore", message="GLCM is symmetrical, therefore Sum Average = 2 * Joint Average")
|
| 4 |
+
import os
|
| 5 |
+
import logging
|
| 6 |
+
from datetime import datetime
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def seg_pyradiomics_main():
|
| 10 |
+
|
| 11 |
+
parser = argparse.ArgumentParser(description='Nodule segmentation and feature extraction from CT images.')
|
| 12 |
+
parser.add_argument('--raw_data_path', type=str, required=True, help='Path to raw CT images')
|
| 13 |
+
parser.add_argument('--csv_save_path', type=str, required=True, help='Path to save the CSV files')
|
| 14 |
+
parser.add_argument('--dataset_csv', type=str, required=True, help='Path to the dataset CSV')
|
| 15 |
+
|
| 16 |
+
# Allow multiple column names as input arguments
|
| 17 |
+
parser.add_argument('--nifti_clm_name', type=str, required=True, help='name to the nifti column name')
|
| 18 |
+
parser.add_argument('--unique_Annotation_id', type=str, help='Column for unique annotation ID')
|
| 19 |
+
parser.add_argument('--Malignant_lbl', type=str, help='Column name for malignancy labels')
|
| 20 |
+
parser.add_argument('--coordX', type=str, required=True, help='Column name for X coordinate')
|
| 21 |
+
parser.add_argument('--coordY', type=str, required=True, help='Column name for Y coordinate')
|
| 22 |
+
parser.add_argument('--coordZ', type=str, required=True, help='Column name for Z coordinate')
|
| 23 |
+
parser.add_argument('--w', type=str, required=True, help='Column name for width')
|
| 24 |
+
parser.add_argument('--h', type=str, required=True, help='Column name for height')
|
| 25 |
+
parser.add_argument('--d', type=str, required=True, help='Column name for depth')
|
| 26 |
+
|
| 27 |
+
parser.add_argument('--seg_alg', type=str, default='gmm', choices=['gmm', 'knn', 'fcm', 'otsu'], help='Segmentation algorithm to use')
|
| 28 |
+
parser.add_argument('--dataset_name', type=str, default='DLCS24', help='Dataset to use')
|
| 29 |
+
parser.add_argument('--expansion_mm', type=float, default=1.0, help='Expansion in mm')
|
| 30 |
+
parser.add_argument('--use_expand', action='store_true', help='Use expansion if set')
|
| 31 |
+
parser.add_argument('--extract_radiomics', action='store_true', help='extarct Radiomics if set')
|
| 32 |
+
parser.add_argument('--params_json', type=str, required=True, help="Path to JSON file with radiomics parameters")
|
| 33 |
+
parser.add_argument('--save_the_generated_mask', action='store_true', help='Use expansion if set')
|
| 34 |
+
parser.add_argument('--save_nifti_path', type=str, help='Path to save the nifti files')
|
| 35 |
+
|
| 36 |
+
args = parser.parse_args()
|
| 37 |
+
raw_data_path = args.raw_data_path
|
| 38 |
+
csv_save_path = args.csv_save_path
|
| 39 |
+
dataset_csv = args.dataset_csv
|
| 40 |
+
seg_alg = args.seg_alg
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
if args.use_expand:
|
| 44 |
+
if args.extract_radiomics:
|
| 45 |
+
output_csv = csv_save_path + f'PyRadiomics_CandidateSeg_{args.dataset_name}_{seg_alg}_expand_{args.expansion_mm}mm.csv'
|
| 46 |
+
Erroroutput_csv = csv_save_path + f'PyRadiomics_CandidateSeg_{args.dataset_name}_{seg_alg}_expand_{args.expansion_mm}mm_Error.csv'
|
| 47 |
+
else:
|
| 48 |
+
output_csv = csv_save_path + f'CandidateSeg_{args.dataset_name}_{seg_alg}_expand_{args.expansion_mm}mm.csv'
|
| 49 |
+
Erroroutput_csv = csv_save_path + f'CandidateSeg_{args.dataset_name}_{seg_alg}_expand_{args.expansion_mm}mm_Error.csv'
|
| 50 |
+
else:
|
| 51 |
+
if args.extract_radiomics:
|
| 52 |
+
output_csv = csv_save_path + f'PyRadiomics_CandidateSeg_{args.dataset_name}_{seg_alg}.csv'
|
| 53 |
+
Erroroutput_csv = csv_save_path + f'PyRadiomics_CandidateSeg_{args.dataset_name}_{seg_alg}_Error.csv'
|
| 54 |
+
else:
|
| 55 |
+
output_csv = csv_save_path + f'CandidateSeg_{args.dataset_name}_{seg_alg}.csv'
|
| 56 |
+
Erroroutput_csv = csv_save_path + f'CandidateSeg_{args.dataset_name}_{seg_alg}_Error.csv'
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
# Derive the log file name from the output CSV file
|
| 61 |
+
log_file = output_csv.replace('.csv', '.log')
|
| 62 |
+
|
| 63 |
+
# Configure logging
|
| 64 |
+
logging.basicConfig(
|
| 65 |
+
filename=log_file,
|
| 66 |
+
level=logging.INFO,
|
| 67 |
+
format="%(asctime)s - %(levelname)s - %(message)s",
|
| 68 |
+
datefmt="%Y-%m-%d %H:%M:%S"
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
logging.info(f"Output CSV File: {output_csv}")
|
| 72 |
+
logging.info(f"Error CSV File: {Erroroutput_csv}")
|
| 73 |
+
logging.info(f"Log File Created: {log_file}")
|
| 74 |
+
logging.info("File names generated successfully.")
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
###----input CSV
|
| 79 |
+
df = pd.read_csv(dataset_csv)
|
| 80 |
+
final_dect = df[args.nifti_clm_name].unique()
|
| 81 |
+
# Initialize the feature extractor
|
| 82 |
+
with open(args.params_json, 'r') as f:
|
| 83 |
+
params = json.load(f)
|
| 84 |
+
|
| 85 |
+
interpolator_map = {"sitkBSpline": sitk.sitkBSpline,"sitkNearestNeighbor": sitk.sitkNearestNeighbor}
|
| 86 |
+
params["interpolator"] = interpolator_map.get(params["interpolator"], sitk.sitkBSpline)
|
| 87 |
+
params["labelInterpolator"] = interpolator_map.get(params["labelInterpolator"], sitk.sitkNearestNeighbor)
|
| 88 |
+
|
| 89 |
+
extractor = featureextractor.RadiomicsFeatureExtractor(**params)
|
| 90 |
+
# Prepare the output CSV
|
| 91 |
+
output_df = pd.DataFrame()
|
| 92 |
+
Error_ids = []
|
| 93 |
+
for dictonary_list_i in range(0,len(final_dect)):
|
| 94 |
+
try:
|
| 95 |
+
logging.info(f"---Loading---: {dictonary_list_i+1}")
|
| 96 |
+
print(make_bold('|' + '-'*30 + ' No={} '.format(dictonary_list_i+1) + '-'*30 + '|'))
|
| 97 |
+
print('\n')
|
| 98 |
+
|
| 99 |
+
desired_value = final_dect[dictonary_list_i]
|
| 100 |
+
filtered_df = df[df[args.nifti_clm_name] == desired_value]
|
| 101 |
+
example_dictionary = filtered_df.reset_index()
|
| 102 |
+
|
| 103 |
+
logging.info(f"Loading the Image:{example_dictionary[args.nifti_clm_name][0]}")
|
| 104 |
+
logging.info(f"Number of Annotations:{len(example_dictionary)}")
|
| 105 |
+
print('Loading the Image: {}'.format(example_dictionary[args.nifti_clm_name][0]))
|
| 106 |
+
print('Number of Annotations: {}'.format(len(example_dictionary)))
|
| 107 |
+
ct_nifti_path = raw_data_path + example_dictionary[args.nifti_clm_name][0]
|
| 108 |
+
ct_image = sitk.ReadImage(ct_nifti_path)
|
| 109 |
+
ct_array = sitk.GetArrayFromImage(ct_image)
|
| 110 |
+
|
| 111 |
+
for Which_box_to_use in range(0,len(example_dictionary)):
|
| 112 |
+
|
| 113 |
+
print('-----------------------------------------------------------------------------------------------')
|
| 114 |
+
|
| 115 |
+
if args.unique_Annotation_id in example_dictionary.columns:
|
| 116 |
+
annotation_id = example_dictionary[args.unique_Annotation_id][Which_box_to_use]
|
| 117 |
+
else:
|
| 118 |
+
# Generate an ID using the image name (without extension) and an index
|
| 119 |
+
image_name = example_dictionary[args.nifti_clm_name][0].split('.nii')[0]
|
| 120 |
+
annotation_id = f"{image_name}_annotation_{Which_box_to_use+1}"
|
| 121 |
+
if args.Malignant_lbl in example_dictionary.columns:
|
| 122 |
+
annotation_lbl = example_dictionary[args.Malignant_lbl][Which_box_to_use]
|
| 123 |
+
print('Annotation-ID = {}'.format(annotation_id))
|
| 124 |
+
worldCoord = np.asarray([float(example_dictionary[args.coordX][Which_box_to_use]), float(example_dictionary[args.coordY][Which_box_to_use]), float(example_dictionary[args.coordZ][Which_box_to_use])])
|
| 125 |
+
voxelCoord = ct_image.TransformPhysicalPointToIndex(worldCoord)
|
| 126 |
+
voxel_coords = voxelCoord
|
| 127 |
+
print('WorldCoord CCC (x,y,z) = {}'.format(worldCoord))
|
| 128 |
+
print('VoxelCoord CCC (x,y,z) = {}'.format(voxelCoord))
|
| 129 |
+
|
| 130 |
+
whd_worldCoord = np.asarray([float(example_dictionary[args.w][Which_box_to_use]), float(example_dictionary[args.h][Which_box_to_use]), float(example_dictionary[args.d][Which_box_to_use])])
|
| 131 |
+
spacing = ct_image.GetSpacing()
|
| 132 |
+
w = int(whd_worldCoord[0] / spacing[0])
|
| 133 |
+
h = int(whd_worldCoord[1] / spacing[1])
|
| 134 |
+
d = int(whd_worldCoord[2] / spacing[2])
|
| 135 |
+
whd_voxelCoord = [w, h, d]
|
| 136 |
+
print('WorldCoord (w,h,d) = {}'.format(whd_worldCoord))
|
| 137 |
+
print('VoxelCoord (w,h,d) = {}'.format(whd_voxelCoord))
|
| 138 |
+
|
| 139 |
+
# Define bounding box
|
| 140 |
+
center_index = voxelCoord
|
| 141 |
+
size_voxel = np.array(whd_voxelCoord) / 2
|
| 142 |
+
bbox_center = [voxel_coords[2],voxel_coords[1],voxel_coords[0]]
|
| 143 |
+
bbox_whd = [d,h,w]
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
#--Image-processing algorithms for segmentations
|
| 147 |
+
if seg_alg=='gmm':
|
| 148 |
+
mask_image_array = segment_nodule_gmm(ct_array, bbox_center, bbox_whd)
|
| 149 |
+
if seg_alg=='knn':
|
| 150 |
+
mask_image_array = segment_nodule_kmeans(ct_array, bbox_center, bbox_whd)
|
| 151 |
+
if seg_alg=='fcm':
|
| 152 |
+
mask_image_array = segment_nodule_fcm(ct_array, bbox_center, bbox_whd)
|
| 153 |
+
if seg_alg=='otsu':
|
| 154 |
+
mask_image_array = segment_nodule_otsu(ct_array, bbox_center, bbox_whd)
|
| 155 |
+
|
| 156 |
+
if args.use_expand:
|
| 157 |
+
mask_image_array = expand_mask_by_distance(mask_image_array, spacing=spacing, expansion_mm=args.expansion_mm)
|
| 158 |
+
|
| 159 |
+
#--- Segmentation---#
|
| 160 |
+
mask_image = sitk.GetImageFromArray(mask_image_array)
|
| 161 |
+
mask_image.SetSpacing(ct_image.GetSpacing())
|
| 162 |
+
mask_image.SetDirection(ct_image.GetDirection())
|
| 163 |
+
mask_image.SetOrigin(ct_image.GetOrigin())
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
if args.extract_radiomics:
|
| 167 |
+
# Extract features
|
| 168 |
+
features = extractor.execute(ct_image, mask_image)
|
| 169 |
+
# Convert the features to a pandas DataFrame row
|
| 170 |
+
feature_row = pd.DataFrame([features])
|
| 171 |
+
feature_row[args.nifti_clm_name] = example_dictionary[args.nifti_clm_name][0]
|
| 172 |
+
feature_row['candidateID'] = annotation_id
|
| 173 |
+
if args.Malignant_lbl in example_dictionary.columns:
|
| 174 |
+
feature_row[args.Malignant_lbl] = annotation_lbl
|
| 175 |
+
else:
|
| 176 |
+
# Convert the features to a pandas DataFrame row
|
| 177 |
+
if args.Malignant_lbl in example_dictionary.columns:
|
| 178 |
+
feature_row = pd.DataFrame({args.nifti_clm_name: [example_dictionary[args.nifti_clm_name][0]],'candidateID': [annotation_id],args.Malignant_lbl: [annotation_lbl]})
|
| 179 |
+
else:
|
| 180 |
+
feature_row = pd.DataFrame({args.nifti_clm_name: [example_dictionary[args.nifti_clm_name][0]],'candidateID': [annotation_id]})
|
| 181 |
+
|
| 182 |
+
print(feature_row)
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
feature_row[args.coordX] = example_dictionary[args.coordX][Which_box_to_use]
|
| 186 |
+
feature_row[args.coordY] = example_dictionary[args.coordY][Which_box_to_use]
|
| 187 |
+
feature_row[args.coordZ] = example_dictionary[args.coordZ][Which_box_to_use]
|
| 188 |
+
feature_row[args.w] = example_dictionary[args.w][Which_box_to_use]
|
| 189 |
+
feature_row[args.h] = example_dictionary[args.h][Which_box_to_use]
|
| 190 |
+
feature_row[args.d] = example_dictionary[args.d][Which_box_to_use]
|
| 191 |
+
|
| 192 |
+
# Save mask if needed
|
| 193 |
+
if args.save_the_generated_mask:
|
| 194 |
+
output_nifti_path = os.path.join(args.save_nifti_path, f"{annotation_id}.nii.gz")
|
| 195 |
+
sitk.WriteImage(mask_image, output_nifti_path)
|
| 196 |
+
# Append the row to the output DataFrame
|
| 197 |
+
output_df = pd.concat([output_df, feature_row], ignore_index=True)
|
| 198 |
+
except Exception as e:
|
| 199 |
+
logging.error(f"An error occurred: {str(e)}")
|
| 200 |
+
print(f" Error occured: {e}")
|
| 201 |
+
Error_ids.append(final_dect[dictonary_list_i])
|
| 202 |
+
pass
|
| 203 |
+
|
| 204 |
+
# Save the output DataFrame to a CSV file
|
| 205 |
+
output_df.to_csv(output_csv, index=False,encoding='utf-8')
|
| 206 |
+
print("completed and saved to {}".format(output_csv))
|
| 207 |
+
Erroroutput_df = pd.DataFrame(list(Error_ids),columns=[args.nifti_clm_name])
|
| 208 |
+
Erroroutput_df.to_csv(Erroroutput_csv, index=False,encoding='utf-8')
|
| 209 |
+
print("completed and saved Error to {}".format(Erroroutput_csv))
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
if __name__ == "__main__":
|
| 213 |
+
seg_pyradiomics_main()
|
scr/candidate_worldCoord_patchExtarctor_pipeline.py
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from cvseg_utils import*
|
| 2 |
+
import warnings
|
| 3 |
+
import os
|
| 4 |
+
import logging
|
| 5 |
+
import random
|
| 6 |
+
import cv2
|
| 7 |
+
import json
|
| 8 |
+
import torch
|
| 9 |
+
import logging
|
| 10 |
+
import argparse
|
| 11 |
+
import numpy as np
|
| 12 |
+
import pandas as pd
|
| 13 |
+
from tqdm import tqdm
|
| 14 |
+
import SimpleITK as sitk
|
| 15 |
+
from monai.transforms import Compose, ScaleIntensityRanged
|
| 16 |
+
random.seed(200)
|
| 17 |
+
np.random.seed(200)
|
| 18 |
+
from datetime import datetime
|
| 19 |
+
|
| 20 |
+
def create_folder_if_not_exists(folder_path):
|
| 21 |
+
import os
|
| 22 |
+
# Check if the folder exists
|
| 23 |
+
if not os.path.exists(folder_path):
|
| 24 |
+
# If the folder doesn't exist, create it
|
| 25 |
+
os.makedirs(folder_path)
|
| 26 |
+
print(f"Folder created: {folder_path}")
|
| 27 |
+
else:
|
| 28 |
+
print(f"Folder already exists: {folder_path}")
|
| 29 |
+
|
| 30 |
+
def nifti_patche_extractor_for_worldCoord_main():
|
| 31 |
+
parser = argparse.ArgumentParser(description='Nodule segmentation and feature extraction from CT images.')
|
| 32 |
+
parser.add_argument('--raw_data_path', type=str, required=True, help='Path to raw CT images')
|
| 33 |
+
parser.add_argument('--csv_save_path', type=str, required=True, help='Path to save the CSV files')
|
| 34 |
+
parser.add_argument('--dataset_csv', type=str, required=True, help='Path to the dataset CSV')
|
| 35 |
+
parser.add_argument('--dataset_name', type=str, default='DLCS24', help='Dataset to use')
|
| 36 |
+
# Allow multiple column names as input arguments
|
| 37 |
+
parser.add_argument('--nifti_clm_name', type=str, required=True, help='name to the nifti column name')
|
| 38 |
+
parser.add_argument('--unique_Annotation_id', type=str, help='Column for unique annotation ID')
|
| 39 |
+
parser.add_argument('--Malignant_lbl', type=str, help='Column name for malignancy labels')
|
| 40 |
+
parser.add_argument('--coordX', type=str, required=True, help='Column name for X coordinate')
|
| 41 |
+
parser.add_argument('--coordY', type=str, required=True, help='Column name for Y coordinate')
|
| 42 |
+
parser.add_argument('--coordZ', type=str, required=True, help='Column name for Z coordinate')
|
| 43 |
+
parser.add_argument('--patch_size', type=int, nargs=3, default=[64, 64, 64], help="Patch size as three integers, e.g., --patch_size 64 64 64")
|
| 44 |
+
# Normalization (4 values)
|
| 45 |
+
parser.add_argument('--normalization', type=float, nargs=4, default=[-1000, 500.0, 0.0, 1.0],help="Normalization values as four floats: A_min A_max B_min B_max")
|
| 46 |
+
# Clip (Boolean from string input)
|
| 47 |
+
parser.add_argument('--clip', type=str, choices=["True", "False"], default="False",help="Enable or disable clipping (True/False). Default is False.")
|
| 48 |
+
parser.add_argument('--save_nifti_path', type=str, help='Path to save the nifti files')
|
| 49 |
+
|
| 50 |
+
args = parser.parse_args()
|
| 51 |
+
raw_data_path = args.raw_data_path
|
| 52 |
+
csv_save_path = args.csv_save_path
|
| 53 |
+
dataset_csv = args.dataset_csv
|
| 54 |
+
|
| 55 |
+
create_folder_if_not_exists(csv_save_path)
|
| 56 |
+
create_folder_if_not_exists(args.save_nifti_path)
|
| 57 |
+
# Extract normalization values
|
| 58 |
+
A_min, A_max, B_min, B_max = args.normalization
|
| 59 |
+
# Convert clip argument to boolean
|
| 60 |
+
CLIP = args.clip == "True"
|
| 61 |
+
output_csv = csv_save_path + f'CandidateSeg_{args.dataset_name}_patch{args.patch_size[0]}x{args.patch_size[1]}y{args.patch_size[2]}z.csv'
|
| 62 |
+
Erroroutput_csv = csv_save_path + f'CandidateSeg_{args.dataset_name}_patch{args.patch_size[0]}x{args.patch_size[1]}y{args.patch_size[2]}z_Error.csv'
|
| 63 |
+
|
| 64 |
+
# Derive the log file name from the output CSV file
|
| 65 |
+
log_file = output_csv.replace('.csv', '.log')
|
| 66 |
+
|
| 67 |
+
# Configure logging
|
| 68 |
+
logging.basicConfig(
|
| 69 |
+
filename=log_file,
|
| 70 |
+
level=logging.INFO,
|
| 71 |
+
format="%(asctime)s - %(levelname)s - %(message)s",
|
| 72 |
+
datefmt="%Y-%m-%d %H:%M:%S"
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
logging.info(f"Output CSV File: {output_csv}")
|
| 76 |
+
logging.info(f"Error CSV File: {Erroroutput_csv}")
|
| 77 |
+
logging.info(f"Log File Created: {log_file}")
|
| 78 |
+
logging.info("File names generated successfully.")
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
###----input CSV
|
| 82 |
+
df = pd.read_csv(dataset_csv)
|
| 83 |
+
final_dect = df[args.nifti_clm_name].unique()
|
| 84 |
+
output_df = pd.DataFrame()
|
| 85 |
+
Error_ids = []
|
| 86 |
+
for dictonary_list_i in tqdm(range(0,len(final_dect)), desc='Processing CTs'):
|
| 87 |
+
try:
|
| 88 |
+
logging.info(f"---Loading---: {dictonary_list_i+1}")
|
| 89 |
+
#print(make_bold('|' + '-'*30 + ' No={} '.format(dictonary_list_i+1) + '-'*30 + '|'))
|
| 90 |
+
#print('\n')
|
| 91 |
+
|
| 92 |
+
desired_value = final_dect[dictonary_list_i]
|
| 93 |
+
filtered_df = df[df[args.nifti_clm_name] == desired_value]
|
| 94 |
+
example_dictionary = filtered_df.reset_index()
|
| 95 |
+
|
| 96 |
+
logging.info(f"Loading the Image:{example_dictionary[args.nifti_clm_name][0]}")
|
| 97 |
+
logging.info(f"Number of Annotations:{len(example_dictionary)}")
|
| 98 |
+
#print('Loading the Image: {}'.format(example_dictionary[args.nifti_clm_name][0]))
|
| 99 |
+
#print('Number of Annotations: {}'.format(len(example_dictionary)))
|
| 100 |
+
ct_nifti_path = raw_data_path + example_dictionary[args.nifti_clm_name][0]
|
| 101 |
+
ct_image = sitk.ReadImage(ct_nifti_path)
|
| 102 |
+
ct_array = sitk.GetArrayFromImage(ct_image)
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
torch_image = torch.from_numpy(ct_array)
|
| 106 |
+
temp_torch_image = {"image": torch_image}
|
| 107 |
+
intensity_transform = Compose([ScaleIntensityRanged(keys=["image"], a_min=A_min, a_max=A_max, b_min=B_min, b_max=B_max, clip=CLIP),])
|
| 108 |
+
transformed_image = intensity_transform(temp_torch_image)
|
| 109 |
+
numpyImage = transformed_image["image"].numpy()
|
| 110 |
+
|
| 111 |
+
for Which_box_to_use in range(0,len(example_dictionary)):
|
| 112 |
+
|
| 113 |
+
#print('-----------------------------------------------------------------------------------------------')
|
| 114 |
+
if args.unique_Annotation_id in example_dictionary.columns:
|
| 115 |
+
annotation_id = example_dictionary[args.unique_Annotation_id][Which_box_to_use]
|
| 116 |
+
else:
|
| 117 |
+
# Generate an ID using the image name (without extension) and an index
|
| 118 |
+
image_name = example_dictionary[args.nifti_clm_name][0].split('.nii')[0]
|
| 119 |
+
annotation_id = f"{image_name}_candidate_{Which_box_to_use+1}"
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
worldCoord = np.asarray([float(example_dictionary[args.coordX][Which_box_to_use]), float(example_dictionary[args.coordY][Which_box_to_use]), float(example_dictionary[args.coordZ][Which_box_to_use])])
|
| 124 |
+
voxelCoord = ct_image.TransformPhysicalPointToIndex(worldCoord)
|
| 125 |
+
# Access individual values
|
| 126 |
+
w = args.patch_size[0]
|
| 127 |
+
h = args.patch_size[1]
|
| 128 |
+
d = args.patch_size[2]
|
| 129 |
+
start_x, end_x = int(voxelCoord[0] - w/2), int(voxelCoord[0] + w/2)
|
| 130 |
+
start_y, end_y = int(voxelCoord[1] - h/2), int(voxelCoord[1] + h/2)
|
| 131 |
+
start_z, end_z = int(voxelCoord[2] - d/2), int(voxelCoord[2] + d/2)
|
| 132 |
+
X, Y, Z = int(voxelCoord[0]), int(voxelCoord[1]), int(voxelCoord[2])
|
| 133 |
+
numpy_to_save_np = numpyImage[max(start_z,0):end_z, max(start_y,0):end_y, max(start_x,0):end_x]
|
| 134 |
+
|
| 135 |
+
# Pad if necessary
|
| 136 |
+
if np.any(numpy_to_save_np.shape != (d, h, w)):
|
| 137 |
+
dZ, dY, dX = numpyImage.shape
|
| 138 |
+
numpy_to_save_np = np.pad(numpy_to_save_np, ((max(d // 2 - Z, 0), d // 2 - min(dZ - Z, d // 2)),
|
| 139 |
+
(max(h // 2 - Y, 0), h // 2 - min(dY - Y, h // 2)),
|
| 140 |
+
(max(w // 2 - X, 0), w // 2 - min(dX - X, w // 2))), mode="constant", constant_values=0.)
|
| 141 |
+
|
| 142 |
+
#--- Segmentation---#
|
| 143 |
+
patch_image = sitk.GetImageFromArray(numpy_to_save_np)
|
| 144 |
+
patch_image.SetSpacing(ct_image.GetSpacing())
|
| 145 |
+
patch_image.SetDirection(ct_image.GetDirection())
|
| 146 |
+
patch_image.SetOrigin(ct_image.GetOrigin())
|
| 147 |
+
if args.Malignant_lbl in example_dictionary.columns:
|
| 148 |
+
feature_row = pd.DataFrame({args.nifti_clm_name: [example_dictionary[args.nifti_clm_name][0]],'candidateID': [annotation_id],args.Malignant_lbl: [example_dictionary[args.Malignant_lbl][Which_box_to_use]]})
|
| 149 |
+
else:
|
| 150 |
+
feature_row = pd.DataFrame({args.nifti_clm_name: [example_dictionary[args.nifti_clm_name][0]],'candidateID': [annotation_id]})
|
| 151 |
+
feature_row[args.coordX] = example_dictionary[args.coordX][Which_box_to_use]
|
| 152 |
+
feature_row[args.coordY] = example_dictionary[args.coordY][Which_box_to_use]
|
| 153 |
+
feature_row[args.coordZ] = example_dictionary[args.coordZ][Which_box_to_use]
|
| 154 |
+
# Save
|
| 155 |
+
output_nifti_path = os.path.join(args.save_nifti_path, f"{annotation_id}.nii.gz")
|
| 156 |
+
# Ensure the directory exists before writing the file
|
| 157 |
+
output_nifti_dir = os.path.dirname(output_nifti_path)
|
| 158 |
+
os.makedirs(output_nifti_dir, exist_ok=True) # Creates the directory if it doesn't exist
|
| 159 |
+
sitk.WriteImage(patch_image, output_nifti_path)
|
| 160 |
+
# Append the row to the output DataFrame
|
| 161 |
+
output_df = pd.concat([output_df, feature_row], ignore_index=True)
|
| 162 |
+
except Exception as e:
|
| 163 |
+
logging.error(f"An error occurred: {str(e)}")
|
| 164 |
+
print(f" Error occured: {e}")
|
| 165 |
+
Error_ids.append(final_dect[dictonary_list_i])
|
| 166 |
+
pass
|
| 167 |
+
|
| 168 |
+
# Save the output DataFrame to a CSV file
|
| 169 |
+
output_df.to_csv(output_csv, index=False,encoding='utf-8')
|
| 170 |
+
print("completed and saved to {}".format(output_csv))
|
| 171 |
+
Erroroutput_df = pd.DataFrame(list(Error_ids),columns=[args.nifti_clm_name])
|
| 172 |
+
Erroroutput_df.to_csv(Erroroutput_csv, index=False,encoding='utf-8')
|
| 173 |
+
print("completed and saved Error to {}".format(Erroroutput_csv))
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
if __name__ == "__main__":
|
| 177 |
+
nifti_patche_extractor_for_worldCoord_main()
|
scr/cvseg_utils.py
ADDED
|
@@ -0,0 +1,488 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#-- Import libraries
|
| 2 |
+
import os
|
| 3 |
+
import argparse
|
| 4 |
+
import json
|
| 5 |
+
|
| 6 |
+
# Numerical and Data Handling
|
| 7 |
+
import numpy as np
|
| 8 |
+
import pandas as pd
|
| 9 |
+
|
| 10 |
+
# Medical Imaging
|
| 11 |
+
import SimpleITK as sitk
|
| 12 |
+
import radiomics
|
| 13 |
+
from radiomics import featureextractor
|
| 14 |
+
|
| 15 |
+
# Machine Learning & Clustering
|
| 16 |
+
from sklearn.cluster import KMeans
|
| 17 |
+
from sklearn.mixture import GaussianMixture
|
| 18 |
+
import skfuzzy as fuzz
|
| 19 |
+
|
| 20 |
+
# Image Processing & Segmentation
|
| 21 |
+
import scipy.ndimage as ndimage
|
| 22 |
+
from skimage.filters import threshold_otsu
|
| 23 |
+
from skimage.segmentation import watershed
|
| 24 |
+
from skimage.feature import peak_local_max
|
| 25 |
+
from skimage import morphology
|
| 26 |
+
from scipy.ndimage import distance_transform_edt, binary_erosion
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def make_bold(text):
|
| 32 |
+
return f"\033[1m{text}\033[0m"
|
| 33 |
+
|
| 34 |
+
def load_itk_image(filename):
|
| 35 |
+
itkimage = sitk.ReadImage(filename)
|
| 36 |
+
numpyImage = sitk.GetArrayFromImage(itkimage)
|
| 37 |
+
numpyOrigin = itkimage.GetOrigin()
|
| 38 |
+
numpySpacing = itkimage.GetSpacing()
|
| 39 |
+
return numpyImage, numpyOrigin, numpySpacing
|
| 40 |
+
|
| 41 |
+
def normalize_image_to_uint8(image, lower_bound=-1000, upper_bound=100):
|
| 42 |
+
clipped_img = np.clip(image, lower_bound, upper_bound)
|
| 43 |
+
normalized_img = ((clipped_img - lower_bound) / (upper_bound - lower_bound)) * 255.0
|
| 44 |
+
normalized_img = normalized_img.astype(np.uint8)
|
| 45 |
+
return normalized_img
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def segment_nodule_kmeans(ct_image, bbox_center, bbox_whd, margin=5, n_clusters=2):
|
| 49 |
+
"""
|
| 50 |
+
Segments a nodule in a 3D CT image using k-means clustering with a margin around the bounding box.
|
| 51 |
+
|
| 52 |
+
Parameters:
|
| 53 |
+
- ct_image: 3D NumPy array representing the CT image.
|
| 54 |
+
- bbox_center: Tuple of (x, y, z) coordinates for the center of the bounding box.
|
| 55 |
+
- bbox_whd: Tuple of (w, h, d) representing the width, height, and depth of the bounding box.
|
| 56 |
+
- margin: Margin to add around the bounding box (default is 5).
|
| 57 |
+
- n_clusters: Number of clusters to use in k-means (default is 2).
|
| 58 |
+
|
| 59 |
+
Returns:
|
| 60 |
+
- segmented_image: 3D NumPy array with the segmented nodule.
|
| 61 |
+
"""
|
| 62 |
+
|
| 63 |
+
x_center, y_center, z_center = bbox_center
|
| 64 |
+
w, h, d = bbox_whd
|
| 65 |
+
|
| 66 |
+
# Calculate the bounding box with margin
|
| 67 |
+
x_start, x_end = max(0, x_center - w//2 - margin), min(ct_image.shape[0], x_center + w//2 + margin)
|
| 68 |
+
y_start, y_end = max(0, y_center - h//2 - margin), min(ct_image.shape[1], y_center + h//2 + margin)
|
| 69 |
+
z_start, z_end = max(0, z_center - d//2 - margin), min(ct_image.shape[2], z_center + d//2 + margin)
|
| 70 |
+
|
| 71 |
+
bbox_region = ct_image[x_start:x_end, y_start:y_end, z_start:z_end]
|
| 72 |
+
|
| 73 |
+
# Reshape the region for k-means clustering
|
| 74 |
+
flat_region = bbox_region.reshape(-1, 1)
|
| 75 |
+
|
| 76 |
+
# Perform k-means clustering
|
| 77 |
+
kmeans = KMeans(n_clusters=n_clusters, n_init=10,random_state=0).fit(flat_region)
|
| 78 |
+
labels = kmeans.labels_
|
| 79 |
+
|
| 80 |
+
# Reshape the labels back to the original bounding box shape
|
| 81 |
+
clustered_region = labels.reshape(bbox_region.shape)
|
| 82 |
+
|
| 83 |
+
# Assume the nodule is in the cluster with the highest mean intensity
|
| 84 |
+
nodule_cluster = np.argmax(kmeans.cluster_centers_)
|
| 85 |
+
|
| 86 |
+
# Create a binary mask for the nodule
|
| 87 |
+
nodule_mask = (clustered_region == nodule_cluster)
|
| 88 |
+
|
| 89 |
+
# Apply morphological operations to refine the segmentation
|
| 90 |
+
nodule_mask = ndimage.binary_closing(nodule_mask, structure=np.ones((3, 3, 3)))
|
| 91 |
+
nodule_mask = ndimage.binary_opening(nodule_mask, structure=np.ones((2, 2, 2)))
|
| 92 |
+
|
| 93 |
+
# Initialize the segmented image
|
| 94 |
+
segmented_image = np.zeros_like(ct_image, dtype=np.uint8)
|
| 95 |
+
|
| 96 |
+
# Place the nodule mask in the correct position in the segmented image
|
| 97 |
+
segmented_image[x_start:x_end, y_start:y_end, z_start:z_end] = nodule_mask
|
| 98 |
+
|
| 99 |
+
return segmented_image
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def segment_nodule_gmm(ct_image, bbox_center, bbox_whd, margin=5, n_components=2):
|
| 103 |
+
"""
|
| 104 |
+
Segments a nodule in a 3D CT image using a Gaussian Mixture Model with a margin around the bounding box.
|
| 105 |
+
|
| 106 |
+
Parameters:
|
| 107 |
+
- ct_image: 3D NumPy array representing the CT image.
|
| 108 |
+
- bbox_center: Tuple of (x, y, z) coordinates for the center of the bounding box.
|
| 109 |
+
- bbox_whd: Tuple of (w, h, d) representing the width, height, and depth of the bounding box.
|
| 110 |
+
- margin: Margin to add around the bounding box (default is 5).
|
| 111 |
+
- n_components: Number of components to use in the Gaussian Mixture Model (default is 2).
|
| 112 |
+
|
| 113 |
+
Returns:
|
| 114 |
+
- segmented_image: 3D NumPy array with the segmented nodule.
|
| 115 |
+
"""
|
| 116 |
+
|
| 117 |
+
x_center, y_center, z_center = bbox_center
|
| 118 |
+
w, h, d = bbox_whd
|
| 119 |
+
|
| 120 |
+
# Calculate the bounding box with margin
|
| 121 |
+
x_start, x_end = max(0, x_center - w//2 - margin), min(ct_image.shape[0], x_center + w//2 + margin)
|
| 122 |
+
y_start, y_end = max(0, y_center - h//2 - margin), min(ct_image.shape[1], y_center + h//2 + margin)
|
| 123 |
+
z_start, z_end = max(0, z_center - d//2 - margin), min(ct_image.shape[2], z_center + d//2 + margin)
|
| 124 |
+
|
| 125 |
+
bbox_region = ct_image[x_start:x_end, y_start:y_end, z_start:z_end]
|
| 126 |
+
|
| 127 |
+
# Reshape the region for GMM
|
| 128 |
+
flat_region = bbox_region.reshape(-1, 1)
|
| 129 |
+
|
| 130 |
+
# Perform GMM
|
| 131 |
+
gmm = GaussianMixture(n_components=n_components, random_state=0).fit(flat_region)
|
| 132 |
+
labels = gmm.predict(flat_region)
|
| 133 |
+
|
| 134 |
+
# Reshape the labels back to the original bounding box shape
|
| 135 |
+
clustered_region = labels.reshape(bbox_region.shape)
|
| 136 |
+
|
| 137 |
+
# Assume the nodule is in the component with the highest mean intensity
|
| 138 |
+
nodule_component = np.argmax(gmm.means_)
|
| 139 |
+
|
| 140 |
+
# Create a binary mask for the nodule
|
| 141 |
+
nodule_mask = (clustered_region == nodule_component)
|
| 142 |
+
|
| 143 |
+
# Apply morphological operations to refine the segmentation
|
| 144 |
+
nodule_mask = ndimage.binary_closing(nodule_mask, structure=np.ones((3, 3, 3)))
|
| 145 |
+
nodule_mask = ndimage.binary_opening(nodule_mask, structure=np.ones((3, 3, 3)))
|
| 146 |
+
|
| 147 |
+
# Initialize the segmented image
|
| 148 |
+
segmented_image = np.zeros_like(ct_image, dtype=np.uint8)
|
| 149 |
+
|
| 150 |
+
# Place the nodule mask in the correct position in the segmented image
|
| 151 |
+
segmented_image[x_start:x_end, y_start:y_end, z_start:z_end] = nodule_mask
|
| 152 |
+
|
| 153 |
+
return segmented_image
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def segment_nodule_fcm(ct_image, bbox_center, bbox_whd, margin=5, n_clusters=2):
|
| 157 |
+
"""
|
| 158 |
+
Segments a nodule in a 3D CT image using Fuzzy C-means clustering with a margin around the bounding box.
|
| 159 |
+
|
| 160 |
+
Parameters:
|
| 161 |
+
- ct_image: 3D NumPy array representing the CT image.
|
| 162 |
+
- bbox_center: Tuple of (x, y, z) coordinates for the center of the bounding box.
|
| 163 |
+
- bbox_whd: Tuple of (w, h, d) representing the width, height, and depth of the bounding box.
|
| 164 |
+
- margin: Margin to add around the bounding box (default is 5).
|
| 165 |
+
- n_clusters: Number of clusters to use in Fuzzy C-means (default is 2).
|
| 166 |
+
|
| 167 |
+
Returns:
|
| 168 |
+
- segmented_image: 3D NumPy array with the segmented nodule.
|
| 169 |
+
"""
|
| 170 |
+
|
| 171 |
+
x_center, y_center, z_center = bbox_center
|
| 172 |
+
w, h, d = bbox_whd
|
| 173 |
+
|
| 174 |
+
# Calculate the bounding box with margin
|
| 175 |
+
x_start, x_end = max(0, x_center - w//2 - margin), min(ct_image.shape[0], x_center + w//2 + margin)
|
| 176 |
+
y_start, y_end = max(0, y_center - h//2 - margin), min(ct_image.shape[1], y_center + h//2 + margin)
|
| 177 |
+
z_start, z_end = max(0, z_center - d//2 - margin), min(ct_image.shape[2], z_center + d//2 + margin)
|
| 178 |
+
|
| 179 |
+
bbox_region = ct_image[x_start:x_end, y_start:y_end, z_start:z_end]
|
| 180 |
+
|
| 181 |
+
# Reshape the region for FCM
|
| 182 |
+
flat_region = bbox_region.reshape(-1, 1)
|
| 183 |
+
|
| 184 |
+
# Perform FCM clustering
|
| 185 |
+
cntr, u, u0, d, jm, p, fpc = fuzz.cluster.cmeans(flat_region.T, n_clusters, 2, error=0.005, maxiter=1000, init=None)
|
| 186 |
+
|
| 187 |
+
# Assign each voxel to the cluster with the highest membership
|
| 188 |
+
labels = np.argmax(u, axis=0)
|
| 189 |
+
|
| 190 |
+
# Reshape the labels back to the original bounding box shape
|
| 191 |
+
clustered_region = labels.reshape(bbox_region.shape)
|
| 192 |
+
|
| 193 |
+
# Assume the nodule is in the cluster with the highest mean intensity
|
| 194 |
+
nodule_cluster = np.argmax(cntr)
|
| 195 |
+
|
| 196 |
+
# Create a binary mask for the nodule
|
| 197 |
+
nodule_mask = (clustered_region == nodule_cluster)
|
| 198 |
+
|
| 199 |
+
# Apply morphological operations to refine the segmentation
|
| 200 |
+
nodule_mask = ndimage.binary_closing(nodule_mask, structure=np.ones((3, 3, 3)))
|
| 201 |
+
nodule_mask = ndimage.binary_opening(nodule_mask, structure=np.ones((3, 3, 3)))
|
| 202 |
+
|
| 203 |
+
# Initialize the segmented image
|
| 204 |
+
segmented_image = np.zeros_like(ct_image, dtype=np.uint8)
|
| 205 |
+
|
| 206 |
+
# Place the nodule mask in the correct position in the segmented image
|
| 207 |
+
segmented_image[x_start:x_end, y_start:y_end, z_start:z_end] = nodule_mask
|
| 208 |
+
|
| 209 |
+
return segmented_image
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
def segment_nodule_otsu(ct_image, bbox_center, bbox_whd, margin=5):
|
| 214 |
+
"""
|
| 215 |
+
Segments a nodule in a 3D CT image using Otsu's thresholding with a margin around the bounding box.
|
| 216 |
+
|
| 217 |
+
Parameters:
|
| 218 |
+
- ct_image: 3D NumPy array representing the CT image.
|
| 219 |
+
- bbox_center: Tuple of (x, y, z) coordinates for the center of the bounding box.
|
| 220 |
+
- bbox_whd: Tuple of (w, h, d) representing the width, height, and depth of the bounding box.
|
| 221 |
+
- margin: Margin to add around the bounding box (default is 5).
|
| 222 |
+
|
| 223 |
+
Returns:
|
| 224 |
+
- segmented_image: 3D NumPy array with the segmented nodule.
|
| 225 |
+
"""
|
| 226 |
+
|
| 227 |
+
x_center, y_center, z_center = bbox_center
|
| 228 |
+
w, h, d = bbox_whd
|
| 229 |
+
|
| 230 |
+
# Calculate the bounding box with margin
|
| 231 |
+
x_start, x_end = max(0, x_center - w//2 - margin), min(ct_image.shape[0], x_center + w//2 + margin)
|
| 232 |
+
y_start, y_end = max(0, y_center - h//2 - margin), min(ct_image.shape[1], y_center + h//2 + margin)
|
| 233 |
+
z_start, z_end = max(0, z_center - d//2 - margin), min(ct_image.shape[2], z_center + d//2 + margin)
|
| 234 |
+
|
| 235 |
+
bbox_region = ct_image[x_start:x_end, y_start:y_end, z_start:z_end]
|
| 236 |
+
|
| 237 |
+
# Flatten the region for thresholding
|
| 238 |
+
flat_region = bbox_region.flatten()
|
| 239 |
+
|
| 240 |
+
# Calculate the Otsu threshold
|
| 241 |
+
otsu_threshold = threshold_otsu(flat_region)
|
| 242 |
+
|
| 243 |
+
# Apply the threshold to create a binary mask
|
| 244 |
+
nodule_mask = bbox_region >= otsu_threshold
|
| 245 |
+
|
| 246 |
+
# Apply morphological operations to refine the segmentation
|
| 247 |
+
nodule_mask = ndimage.binary_closing(nodule_mask, structure=np.ones((3, 3, 3)))
|
| 248 |
+
nodule_mask = ndimage.binary_opening(nodule_mask, structure=np.ones((3, 3, 3)))
|
| 249 |
+
|
| 250 |
+
# Initialize the segmented image
|
| 251 |
+
segmented_image = np.zeros_like(ct_image, dtype=np.uint8)
|
| 252 |
+
|
| 253 |
+
# Place the nodule mask in the correct position in the segmented image
|
| 254 |
+
segmented_image[x_start:x_end, y_start:y_end, z_start:z_end] = nodule_mask
|
| 255 |
+
|
| 256 |
+
return segmented_image
|
| 257 |
+
|
| 258 |
+
|
| 259 |
+
|
| 260 |
+
def expand_mask_by_distance(segmented_nodule_gmm, spacing, expansion_mm):
|
| 261 |
+
"""
|
| 262 |
+
Expands the segmentation mask by a given distance in mm in all directions by directly updating pixel values.
|
| 263 |
+
|
| 264 |
+
Parameters:
|
| 265 |
+
segmented_nodule_gmm (numpy array): 3D binary mask of the nodule (1 for nodule, 0 for background).
|
| 266 |
+
spacing (tuple): Spacing of the image in mm for each voxel, given as (spacing_x, spacing_y, spacing_z).
|
| 267 |
+
expansion_mm (float): Distance to expand the mask in millimeters.
|
| 268 |
+
|
| 269 |
+
Returns:
|
| 270 |
+
numpy array: Expanded segmentation mask.
|
| 271 |
+
"""
|
| 272 |
+
# Reorder spacing to match the numpy array's (z, y, x) format
|
| 273 |
+
spacing_reordered = (spacing[2], spacing[1], spacing[0]) # (spacing_z, spacing_y, spacing_x)
|
| 274 |
+
|
| 275 |
+
# Calculate the number of pixels to expand in each dimension
|
| 276 |
+
expand_pixels = np.array([int(np.round(expansion_mm / s)) for s in spacing_reordered])
|
| 277 |
+
|
| 278 |
+
# Create a new expanded mask with the same shape
|
| 279 |
+
expanded_mask = np.zeros_like(segmented_nodule_gmm)
|
| 280 |
+
|
| 281 |
+
# Get the coordinates of all white pixels in the original mask
|
| 282 |
+
white_pixel_coords = np.argwhere(segmented_nodule_gmm == 1)
|
| 283 |
+
|
| 284 |
+
# Expand each white pixel by adding the specified number of pixels in each direction
|
| 285 |
+
for coord in white_pixel_coords:
|
| 286 |
+
z, y, x = coord # Extract the z, y, x coordinates of each white pixel
|
| 287 |
+
|
| 288 |
+
# Define the range to expand for each coordinate
|
| 289 |
+
z_range = range(max(0, z - expand_pixels[0]), min(segmented_nodule_gmm.shape[0], z + expand_pixels[0] + 1))
|
| 290 |
+
y_range = range(max(0, y - expand_pixels[1]), min(segmented_nodule_gmm.shape[1], y + expand_pixels[1] + 1))
|
| 291 |
+
x_range = range(max(0, x - expand_pixels[2]), min(segmented_nodule_gmm.shape[2], x + expand_pixels[2] + 1))
|
| 292 |
+
|
| 293 |
+
# Update the new mask by setting all pixels in this range to 1
|
| 294 |
+
for z_new in z_range:
|
| 295 |
+
for y_new in y_range:
|
| 296 |
+
for x_new in x_range:
|
| 297 |
+
expanded_mask[z_new, y_new, x_new] = 1
|
| 298 |
+
|
| 299 |
+
return expanded_mask
|
| 300 |
+
|
| 301 |
+
|
| 302 |
+
def find_nodule_lobe(cccwhd, lung_mask, class_map):
|
| 303 |
+
"""
|
| 304 |
+
Determine the lung lobe where a nodule is located based on a 3D mask and bounding box.
|
| 305 |
+
|
| 306 |
+
Parameters:
|
| 307 |
+
cccwhd (list or tuple): Bounding box in the format [center_x, center_y, center_z, width, height, depth].
|
| 308 |
+
lung_mask (numpy array): 3D array representing the lung mask with different lung regions.
|
| 309 |
+
class_map (dict): Dictionary mapping lung region labels to their names.
|
| 310 |
+
|
| 311 |
+
Returns:
|
| 312 |
+
str: Name of the lung lobe where the nodule is located.
|
| 313 |
+
"""
|
| 314 |
+
center_x, center_y, center_z, width, height, depth = cccwhd
|
| 315 |
+
|
| 316 |
+
# Calculate the bounding box limits
|
| 317 |
+
start_x = int(center_x - width // 2)
|
| 318 |
+
end_x = int(center_x + width // 2)
|
| 319 |
+
start_y = int(center_y - height // 2)
|
| 320 |
+
end_y = int(center_y + height // 2)
|
| 321 |
+
start_z = int(center_z - depth // 2)
|
| 322 |
+
end_z = int(center_z + depth // 2)
|
| 323 |
+
|
| 324 |
+
# Ensure the indices are within the mask dimensions
|
| 325 |
+
start_x = max(0, start_x)
|
| 326 |
+
end_x = min(lung_mask.shape[0], end_x)
|
| 327 |
+
start_y = max(0, start_y)
|
| 328 |
+
end_y = min(lung_mask.shape[1], end_y)
|
| 329 |
+
start_z = max(0, start_z)
|
| 330 |
+
end_z = min(lung_mask.shape[2], end_z)
|
| 331 |
+
|
| 332 |
+
# Extract the region of interest (ROI) from the mask
|
| 333 |
+
roi = lung_mask[start_x:end_x, start_y:end_y, start_z:end_z]
|
| 334 |
+
|
| 335 |
+
# Count the occurrences of each lobe label within the ROI
|
| 336 |
+
unique, counts = np.unique(roi, return_counts=True)
|
| 337 |
+
label_counts = dict(zip(unique, counts))
|
| 338 |
+
|
| 339 |
+
# Exclude the background (label 0)
|
| 340 |
+
if 0 in label_counts:
|
| 341 |
+
del label_counts[0]
|
| 342 |
+
|
| 343 |
+
# Find the label with the maximum count
|
| 344 |
+
if label_counts:
|
| 345 |
+
nodule_lobe = max(label_counts, key=label_counts.get)
|
| 346 |
+
else:
|
| 347 |
+
nodule_lobe = None
|
| 348 |
+
|
| 349 |
+
# Map the label to the corresponding lung lobe
|
| 350 |
+
if nodule_lobe is not None:
|
| 351 |
+
nodule_lobe_name = class_map["lungs"][nodule_lobe]
|
| 352 |
+
else:
|
| 353 |
+
nodule_lobe_name = "Undefined"
|
| 354 |
+
|
| 355 |
+
return nodule_lobe_name
|
| 356 |
+
|
| 357 |
+
|
| 358 |
+
def find_nodule_lobe_and_distance(cccwhd, lung_mask, class_map,spacing):
|
| 359 |
+
"""
|
| 360 |
+
Determine the lung lobe where a nodule is located and measure its distance from the lung wall.
|
| 361 |
+
|
| 362 |
+
Parameters:
|
| 363 |
+
cccwhd (list or tuple): Bounding box in the format [center_x, center_y, center_z, width, height, depth].
|
| 364 |
+
lung_mask (numpy array): 3D array representing the lung mask with different lung regions.
|
| 365 |
+
class_map (dict): Dictionary mapping lung region labels to their names.
|
| 366 |
+
|
| 367 |
+
Returns:
|
| 368 |
+
tuple: (Name of the lung lobe, Distance from the lung wall)
|
| 369 |
+
"""
|
| 370 |
+
center_x, center_y, center_z, width, height, depth = cccwhd
|
| 371 |
+
|
| 372 |
+
# Calculate the bounding box limits
|
| 373 |
+
start_x = int(center_x - width // 2)
|
| 374 |
+
end_x = int(center_x + width // 2)
|
| 375 |
+
start_y = int(center_y - height // 2)
|
| 376 |
+
end_y = int(center_y + height // 2)
|
| 377 |
+
start_z = int(center_z - depth // 2)
|
| 378 |
+
end_z = int(center_z + depth // 2)
|
| 379 |
+
|
| 380 |
+
# Ensure the indices are within the mask dimensions
|
| 381 |
+
start_x = max(0, start_x)
|
| 382 |
+
end_x = min(lung_mask.shape[0], end_x)
|
| 383 |
+
start_y = max(0, start_y)
|
| 384 |
+
end_y = min(lung_mask.shape[1], end_y)
|
| 385 |
+
start_z = max(0, start_z)
|
| 386 |
+
end_z = min(lung_mask.shape[2], end_z)
|
| 387 |
+
|
| 388 |
+
# Extract the region of interest (ROI) from the mask
|
| 389 |
+
roi = lung_mask[start_x:end_x, start_y:end_y, start_z:end_z]
|
| 390 |
+
|
| 391 |
+
# Count the occurrences of each lobe label within the ROI
|
| 392 |
+
unique, counts = np.unique(roi, return_counts=True)
|
| 393 |
+
label_counts = dict(zip(unique, counts))
|
| 394 |
+
|
| 395 |
+
# Exclude the background (label 0)
|
| 396 |
+
if 0 in label_counts:
|
| 397 |
+
del label_counts[0]
|
| 398 |
+
|
| 399 |
+
# Find the label with the maximum count
|
| 400 |
+
if label_counts:
|
| 401 |
+
nodule_lobe = max(label_counts, key=label_counts.get)
|
| 402 |
+
else:
|
| 403 |
+
nodule_lobe = None
|
| 404 |
+
|
| 405 |
+
# Map the label to the corresponding lung lobe
|
| 406 |
+
if nodule_lobe is not None:
|
| 407 |
+
nodule_lobe_name = class_map["lungs"][nodule_lobe]
|
| 408 |
+
else:
|
| 409 |
+
nodule_lobe_name = "Undefined"
|
| 410 |
+
|
| 411 |
+
# Calculate the distance from the nodule centroid to the nearest lung wall
|
| 412 |
+
nodule_centroid = np.array([center_x, center_y, center_z])
|
| 413 |
+
|
| 414 |
+
# Create a binary lung mask where lung region is 1 and outside lung is 0
|
| 415 |
+
lung_binary_mask = lung_mask > 0
|
| 416 |
+
|
| 417 |
+
# Create the lung wall mask by finding the outer boundary
|
| 418 |
+
# Use binary erosion to shrink the lung mask, then subtract it from the original mask to get the boundary
|
| 419 |
+
lung_eroded = binary_erosion(lung_binary_mask)
|
| 420 |
+
lung_wall_mask = lung_binary_mask & ~lung_eroded # Lung wall mask is the outermost boundary (contour)
|
| 421 |
+
|
| 422 |
+
# Compute the distance transform from the lung wall
|
| 423 |
+
distance_transform = distance_transform_edt(~lung_wall_mask) # Compute distance to nearest lung boundary
|
| 424 |
+
|
| 425 |
+
|
| 426 |
+
|
| 427 |
+
# Get the distance from the nodule centroid to the nearest lung wall in voxel units
|
| 428 |
+
voxel_distance_to_lung_wall = distance_transform[center_x, center_y, center_z]
|
| 429 |
+
|
| 430 |
+
# Convert voxel distance to real-world distance in mm
|
| 431 |
+
physical_distance_to_lung_wall = voxel_distance_to_lung_wall * np.sqrt(
|
| 432 |
+
spacing[0]**2 + spacing[1]**2 + spacing[2]**2
|
| 433 |
+
)
|
| 434 |
+
|
| 435 |
+
|
| 436 |
+
|
| 437 |
+
return nodule_lobe_name, voxel_distance_to_lung_wall,physical_distance_to_lung_wall
|
| 438 |
+
|
| 439 |
+
|
| 440 |
+
# +
|
| 441 |
+
def expand_mask_by_distance(segmented_nodule_gmm, spacing, expansion_mm):
|
| 442 |
+
"""
|
| 443 |
+
Expands the segmentation mask by a given distance in mm in all directions by directly updating pixel values.
|
| 444 |
+
|
| 445 |
+
Parameters:
|
| 446 |
+
segmented_nodule_gmm (numpy array): 3D binary mask of the nodule (1 for nodule, 0 for background).
|
| 447 |
+
spacing (tuple): Spacing of the image in mm for each voxel, given as (spacing_x, spacing_y, spacing_z).
|
| 448 |
+
expansion_mm (float): Distance to expand the mask in millimeters.
|
| 449 |
+
|
| 450 |
+
Returns:
|
| 451 |
+
numpy array: Expanded segmentation mask.
|
| 452 |
+
"""
|
| 453 |
+
# Reorder spacing to match the numpy array's (z, y, x) format
|
| 454 |
+
spacing_reordered = (spacing[2], spacing[1], spacing[0]) # (spacing_z, spacing_y, spacing_x)
|
| 455 |
+
|
| 456 |
+
# Calculate the number of pixels to expand in each dimension
|
| 457 |
+
expand_pixels = np.array([int(np.round(expansion_mm / s)) for s in spacing_reordered])
|
| 458 |
+
|
| 459 |
+
# Create a new expanded mask with the same shape
|
| 460 |
+
expanded_mask = np.zeros_like(segmented_nodule_gmm)
|
| 461 |
+
|
| 462 |
+
# Get the coordinates of all white pixels in the original mask
|
| 463 |
+
white_pixel_coords = np.argwhere(segmented_nodule_gmm == 1)
|
| 464 |
+
|
| 465 |
+
# Expand each white pixel by adding the specified number of pixels in each direction
|
| 466 |
+
for coord in white_pixel_coords:
|
| 467 |
+
z, y, x = coord # Extract the z, y, x coordinates of each white pixel
|
| 468 |
+
|
| 469 |
+
# Define the range to expand for each coordinate
|
| 470 |
+
z_range = range(max(0, z - expand_pixels[0]), min(segmented_nodule_gmm.shape[0], z + expand_pixels[0] + 1))
|
| 471 |
+
y_range = range(max(0, y - expand_pixels[1]), min(segmented_nodule_gmm.shape[1], y + expand_pixels[1] + 1))
|
| 472 |
+
x_range = range(max(0, x - expand_pixels[2]), min(segmented_nodule_gmm.shape[2], x + expand_pixels[2] + 1))
|
| 473 |
+
|
| 474 |
+
# Update the new mask by setting all pixels in this range to 1
|
| 475 |
+
for z_new in z_range:
|
| 476 |
+
for y_new in y_range:
|
| 477 |
+
for x_new in x_range:
|
| 478 |
+
expanded_mask[z_new, y_new, x_new] = 1
|
| 479 |
+
|
| 480 |
+
return expanded_mask
|
| 481 |
+
|
| 482 |
+
|
| 483 |
+
|
| 484 |
+
# Function to plot the contours of a mask
|
| 485 |
+
def plot_contours(ax, mask, color, linewidth=1.5):
|
| 486 |
+
contours = measure.find_contours(mask, level=0.5) # Find contours at a constant level
|
| 487 |
+
for contour in contours:
|
| 488 |
+
ax.plot(contour[:, 1], contour[:, 0], color=color, linewidth=linewidth)
|
scr/segmentation_utils.py
ADDED
|
@@ -0,0 +1,490 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import SimpleITK as sitk
|
| 2 |
+
import numpy as np
|
| 3 |
+
import pandas as pd
|
| 4 |
+
import os
|
| 5 |
+
import radiomics
|
| 6 |
+
from radiomics import featureextractor
|
| 7 |
+
import argparse
|
| 8 |
+
import numpy as np
|
| 9 |
+
from sklearn.cluster import KMeans
|
| 10 |
+
import scipy.ndimage as ndimage
|
| 11 |
+
from sklearn.mixture import GaussianMixture
|
| 12 |
+
import skfuzzy as fuzz
|
| 13 |
+
from skimage.filters import threshold_otsu
|
| 14 |
+
from skimage.filters import threshold_otsu
|
| 15 |
+
from skimage.segmentation import watershed
|
| 16 |
+
from skimage.feature import peak_local_max
|
| 17 |
+
from skimage import morphology
|
| 18 |
+
from scipy.ndimage import distance_transform_edt, binary_erosion
|
| 19 |
+
from matplotlib import colors
|
| 20 |
+
import numpy as np
|
| 21 |
+
import matplotlib.pyplot as plt
|
| 22 |
+
import SimpleITK as sitk
|
| 23 |
+
import numpy as np
|
| 24 |
+
import pandas as pd
|
| 25 |
+
from matplotlib import colors
|
| 26 |
+
import cv2
|
| 27 |
+
from skimage import measure
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def make_bold(text):
|
| 31 |
+
return f"\033[1m{text}\033[0m"
|
| 32 |
+
|
| 33 |
+
def load_itk_image(filename):
|
| 34 |
+
itkimage = sitk.ReadImage(filename)
|
| 35 |
+
numpyImage = sitk.GetArrayFromImage(itkimage)
|
| 36 |
+
numpyOrigin = itkimage.GetOrigin()
|
| 37 |
+
numpySpacing = itkimage.GetSpacing()
|
| 38 |
+
return numpyImage, numpyOrigin, numpySpacing
|
| 39 |
+
|
| 40 |
+
def normalize_image_to_uint8(image, lower_bound=-1000, upper_bound=100):
|
| 41 |
+
clipped_img = np.clip(image, lower_bound, upper_bound)
|
| 42 |
+
normalized_img = ((clipped_img - lower_bound) / (upper_bound - lower_bound)) * 255.0
|
| 43 |
+
normalized_img = normalized_img.astype(np.uint8)
|
| 44 |
+
return normalized_img
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def segment_nodule_kmeans(ct_image, bbox_center, bbox_whd, margin=5, n_clusters=2):
|
| 50 |
+
"""
|
| 51 |
+
Segments a nodule in a 3D CT image using k-means clustering with a margin around the bounding box.
|
| 52 |
+
|
| 53 |
+
Parameters:
|
| 54 |
+
- ct_image: 3D NumPy array representing the CT image.
|
| 55 |
+
- bbox_center: Tuple of (x, y, z) coordinates for the center of the bounding box.
|
| 56 |
+
- bbox_whd: Tuple of (w, h, d) representing the width, height, and depth of the bounding box.
|
| 57 |
+
- margin: Margin to add around the bounding box (default is 5).
|
| 58 |
+
- n_clusters: Number of clusters to use in k-means (default is 2).
|
| 59 |
+
|
| 60 |
+
Returns:
|
| 61 |
+
- segmented_image: 3D NumPy array with the segmented nodule.
|
| 62 |
+
"""
|
| 63 |
+
|
| 64 |
+
x_center, y_center, z_center = bbox_center
|
| 65 |
+
w, h, d = bbox_whd
|
| 66 |
+
|
| 67 |
+
# Calculate the bounding box with margin
|
| 68 |
+
x_start, x_end = max(0, x_center - w//2 - margin), min(ct_image.shape[0], x_center + w//2 + margin)
|
| 69 |
+
y_start, y_end = max(0, y_center - h//2 - margin), min(ct_image.shape[1], y_center + h//2 + margin)
|
| 70 |
+
z_start, z_end = max(0, z_center - d//2 - margin), min(ct_image.shape[2], z_center + d//2 + margin)
|
| 71 |
+
|
| 72 |
+
bbox_region = ct_image[x_start:x_end, y_start:y_end, z_start:z_end]
|
| 73 |
+
|
| 74 |
+
# Reshape the region for k-means clustering
|
| 75 |
+
flat_region = bbox_region.reshape(-1, 1)
|
| 76 |
+
|
| 77 |
+
# Perform k-means clustering
|
| 78 |
+
kmeans = KMeans(n_clusters=n_clusters, random_state=0).fit(flat_region)
|
| 79 |
+
labels = kmeans.labels_
|
| 80 |
+
|
| 81 |
+
# Reshape the labels back to the original bounding box shape
|
| 82 |
+
clustered_region = labels.reshape(bbox_region.shape)
|
| 83 |
+
|
| 84 |
+
# Assume the nodule is in the cluster with the highest mean intensity
|
| 85 |
+
nodule_cluster = np.argmax(kmeans.cluster_centers_)
|
| 86 |
+
|
| 87 |
+
# Create a binary mask for the nodule
|
| 88 |
+
nodule_mask = (clustered_region == nodule_cluster)
|
| 89 |
+
|
| 90 |
+
# Apply morphological operations to refine the segmentation
|
| 91 |
+
nodule_mask = ndimage.binary_closing(nodule_mask, structure=np.ones((3, 3, 3)))
|
| 92 |
+
nodule_mask = ndimage.binary_opening(nodule_mask, structure=np.ones((2, 2, 2)))
|
| 93 |
+
|
| 94 |
+
# Initialize the segmented image
|
| 95 |
+
segmented_image = np.zeros_like(ct_image, dtype=np.uint8)
|
| 96 |
+
|
| 97 |
+
# Place the nodule mask in the correct position in the segmented image
|
| 98 |
+
segmented_image[x_start:x_end, y_start:y_end, z_start:z_end] = nodule_mask
|
| 99 |
+
|
| 100 |
+
return segmented_image
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def segment_nodule_gmm(ct_image, bbox_center, bbox_whd, margin=5, n_components=2):
|
| 104 |
+
"""
|
| 105 |
+
Segments a nodule in a 3D CT image using a Gaussian Mixture Model with a margin around the bounding box.
|
| 106 |
+
|
| 107 |
+
Parameters:
|
| 108 |
+
- ct_image: 3D NumPy array representing the CT image.
|
| 109 |
+
- bbox_center: Tuple of (x, y, z) coordinates for the center of the bounding box.
|
| 110 |
+
- bbox_whd: Tuple of (w, h, d) representing the width, height, and depth of the bounding box.
|
| 111 |
+
- margin: Margin to add around the bounding box (default is 5).
|
| 112 |
+
- n_components: Number of components to use in the Gaussian Mixture Model (default is 2).
|
| 113 |
+
|
| 114 |
+
Returns:
|
| 115 |
+
- segmented_image: 3D NumPy array with the segmented nodule.
|
| 116 |
+
"""
|
| 117 |
+
|
| 118 |
+
x_center, y_center, z_center = bbox_center
|
| 119 |
+
w, h, d = bbox_whd
|
| 120 |
+
|
| 121 |
+
# Calculate the bounding box with margin
|
| 122 |
+
x_start, x_end = max(0, x_center - w//2 - margin), min(ct_image.shape[0], x_center + w//2 + margin)
|
| 123 |
+
y_start, y_end = max(0, y_center - h//2 - margin), min(ct_image.shape[1], y_center + h//2 + margin)
|
| 124 |
+
z_start, z_end = max(0, z_center - d//2 - margin), min(ct_image.shape[2], z_center + d//2 + margin)
|
| 125 |
+
|
| 126 |
+
bbox_region = ct_image[x_start:x_end, y_start:y_end, z_start:z_end]
|
| 127 |
+
|
| 128 |
+
# Reshape the region for GMM
|
| 129 |
+
flat_region = bbox_region.reshape(-1, 1)
|
| 130 |
+
|
| 131 |
+
# Perform GMM
|
| 132 |
+
gmm = GaussianMixture(n_components=n_components, random_state=0).fit(flat_region)
|
| 133 |
+
labels = gmm.predict(flat_region)
|
| 134 |
+
|
| 135 |
+
# Reshape the labels back to the original bounding box shape
|
| 136 |
+
clustered_region = labels.reshape(bbox_region.shape)
|
| 137 |
+
|
| 138 |
+
# Assume the nodule is in the component with the highest mean intensity
|
| 139 |
+
nodule_component = np.argmax(gmm.means_)
|
| 140 |
+
|
| 141 |
+
# Create a binary mask for the nodule
|
| 142 |
+
nodule_mask = (clustered_region == nodule_component)
|
| 143 |
+
|
| 144 |
+
# Apply morphological operations to refine the segmentation
|
| 145 |
+
nodule_mask = ndimage.binary_closing(nodule_mask, structure=np.ones((3, 3, 3)))
|
| 146 |
+
nodule_mask = ndimage.binary_opening(nodule_mask, structure=np.ones((3, 3, 3)))
|
| 147 |
+
|
| 148 |
+
# Initialize the segmented image
|
| 149 |
+
segmented_image = np.zeros_like(ct_image, dtype=np.uint8)
|
| 150 |
+
|
| 151 |
+
# Place the nodule mask in the correct position in the segmented image
|
| 152 |
+
segmented_image[x_start:x_end, y_start:y_end, z_start:z_end] = nodule_mask
|
| 153 |
+
|
| 154 |
+
return segmented_image
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
def segment_nodule_fcm(ct_image, bbox_center, bbox_whd, margin=5, n_clusters=2):
|
| 158 |
+
"""
|
| 159 |
+
Segments a nodule in a 3D CT image using Fuzzy C-means clustering with a margin around the bounding box.
|
| 160 |
+
|
| 161 |
+
Parameters:
|
| 162 |
+
- ct_image: 3D NumPy array representing the CT image.
|
| 163 |
+
- bbox_center: Tuple of (x, y, z) coordinates for the center of the bounding box.
|
| 164 |
+
- bbox_whd: Tuple of (w, h, d) representing the width, height, and depth of the bounding box.
|
| 165 |
+
- margin: Margin to add around the bounding box (default is 5).
|
| 166 |
+
- n_clusters: Number of clusters to use in Fuzzy C-means (default is 2).
|
| 167 |
+
|
| 168 |
+
Returns:
|
| 169 |
+
- segmented_image: 3D NumPy array with the segmented nodule.
|
| 170 |
+
"""
|
| 171 |
+
|
| 172 |
+
x_center, y_center, z_center = bbox_center
|
| 173 |
+
w, h, d = bbox_whd
|
| 174 |
+
|
| 175 |
+
# Calculate the bounding box with margin
|
| 176 |
+
x_start, x_end = max(0, x_center - w//2 - margin), min(ct_image.shape[0], x_center + w//2 + margin)
|
| 177 |
+
y_start, y_end = max(0, y_center - h//2 - margin), min(ct_image.shape[1], y_center + h//2 + margin)
|
| 178 |
+
z_start, z_end = max(0, z_center - d//2 - margin), min(ct_image.shape[2], z_center + d//2 + margin)
|
| 179 |
+
|
| 180 |
+
bbox_region = ct_image[x_start:x_end, y_start:y_end, z_start:z_end]
|
| 181 |
+
|
| 182 |
+
# Reshape the region for FCM
|
| 183 |
+
flat_region = bbox_region.reshape(-1, 1)
|
| 184 |
+
|
| 185 |
+
# Perform FCM clustering
|
| 186 |
+
cntr, u, u0, d, jm, p, fpc = fuzz.cluster.cmeans(flat_region.T, n_clusters, 2, error=0.005, maxiter=1000, init=None)
|
| 187 |
+
|
| 188 |
+
# Assign each voxel to the cluster with the highest membership
|
| 189 |
+
labels = np.argmax(u, axis=0)
|
| 190 |
+
|
| 191 |
+
# Reshape the labels back to the original bounding box shape
|
| 192 |
+
clustered_region = labels.reshape(bbox_region.shape)
|
| 193 |
+
|
| 194 |
+
# Assume the nodule is in the cluster with the highest mean intensity
|
| 195 |
+
nodule_cluster = np.argmax(cntr)
|
| 196 |
+
|
| 197 |
+
# Create a binary mask for the nodule
|
| 198 |
+
nodule_mask = (clustered_region == nodule_cluster)
|
| 199 |
+
|
| 200 |
+
# Apply morphological operations to refine the segmentation
|
| 201 |
+
nodule_mask = ndimage.binary_closing(nodule_mask, structure=np.ones((3, 3, 3)))
|
| 202 |
+
nodule_mask = ndimage.binary_opening(nodule_mask, structure=np.ones((3, 3, 3)))
|
| 203 |
+
|
| 204 |
+
# Initialize the segmented image
|
| 205 |
+
segmented_image = np.zeros_like(ct_image, dtype=np.uint8)
|
| 206 |
+
|
| 207 |
+
# Place the nodule mask in the correct position in the segmented image
|
| 208 |
+
segmented_image[x_start:x_end, y_start:y_end, z_start:z_end] = nodule_mask
|
| 209 |
+
|
| 210 |
+
return segmented_image
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
def segment_nodule_otsu(ct_image, bbox_center, bbox_whd, margin=5):
|
| 215 |
+
"""
|
| 216 |
+
Segments a nodule in a 3D CT image using Otsu's thresholding with a margin around the bounding box.
|
| 217 |
+
|
| 218 |
+
Parameters:
|
| 219 |
+
- ct_image: 3D NumPy array representing the CT image.
|
| 220 |
+
- bbox_center: Tuple of (x, y, z) coordinates for the center of the bounding box.
|
| 221 |
+
- bbox_whd: Tuple of (w, h, d) representing the width, height, and depth of the bounding box.
|
| 222 |
+
- margin: Margin to add around the bounding box (default is 5).
|
| 223 |
+
|
| 224 |
+
Returns:
|
| 225 |
+
- segmented_image: 3D NumPy array with the segmented nodule.
|
| 226 |
+
"""
|
| 227 |
+
|
| 228 |
+
x_center, y_center, z_center = bbox_center
|
| 229 |
+
w, h, d = bbox_whd
|
| 230 |
+
|
| 231 |
+
# Calculate the bounding box with margin
|
| 232 |
+
x_start, x_end = max(0, x_center - w//2 - margin), min(ct_image.shape[0], x_center + w//2 + margin)
|
| 233 |
+
y_start, y_end = max(0, y_center - h//2 - margin), min(ct_image.shape[1], y_center + h//2 + margin)
|
| 234 |
+
z_start, z_end = max(0, z_center - d//2 - margin), min(ct_image.shape[2], z_center + d//2 + margin)
|
| 235 |
+
|
| 236 |
+
bbox_region = ct_image[x_start:x_end, y_start:y_end, z_start:z_end]
|
| 237 |
+
|
| 238 |
+
# Flatten the region for thresholding
|
| 239 |
+
flat_region = bbox_region.flatten()
|
| 240 |
+
|
| 241 |
+
# Calculate the Otsu threshold
|
| 242 |
+
otsu_threshold = threshold_otsu(flat_region)
|
| 243 |
+
|
| 244 |
+
# Apply the threshold to create a binary mask
|
| 245 |
+
nodule_mask = bbox_region >= otsu_threshold
|
| 246 |
+
|
| 247 |
+
# Apply morphological operations to refine the segmentation
|
| 248 |
+
nodule_mask = ndimage.binary_closing(nodule_mask, structure=np.ones((3, 3, 3)))
|
| 249 |
+
nodule_mask = ndimage.binary_opening(nodule_mask, structure=np.ones((3, 3, 3)))
|
| 250 |
+
|
| 251 |
+
# Initialize the segmented image
|
| 252 |
+
segmented_image = np.zeros_like(ct_image, dtype=np.uint8)
|
| 253 |
+
|
| 254 |
+
# Place the nodule mask in the correct position in the segmented image
|
| 255 |
+
segmented_image[x_start:x_end, y_start:y_end, z_start:z_end] = nodule_mask
|
| 256 |
+
|
| 257 |
+
return segmented_image
|
| 258 |
+
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
def expand_mask_by_distance(segmented_nodule_gmm, spacing, expansion_mm):
|
| 262 |
+
"""
|
| 263 |
+
Expands the segmentation mask by a given distance in mm in all directions by directly updating pixel values.
|
| 264 |
+
|
| 265 |
+
Parameters:
|
| 266 |
+
segmented_nodule_gmm (numpy array): 3D binary mask of the nodule (1 for nodule, 0 for background).
|
| 267 |
+
spacing (tuple): Spacing of the image in mm for each voxel, given as (spacing_x, spacing_y, spacing_z).
|
| 268 |
+
expansion_mm (float): Distance to expand the mask in millimeters.
|
| 269 |
+
|
| 270 |
+
Returns:
|
| 271 |
+
numpy array: Expanded segmentation mask.
|
| 272 |
+
"""
|
| 273 |
+
# Reorder spacing to match the numpy array's (z, y, x) format
|
| 274 |
+
spacing_reordered = (spacing[2], spacing[1], spacing[0]) # (spacing_z, spacing_y, spacing_x)
|
| 275 |
+
|
| 276 |
+
# Calculate the number of pixels to expand in each dimension
|
| 277 |
+
expand_pixels = np.array([int(np.round(expansion_mm / s)) for s in spacing_reordered])
|
| 278 |
+
|
| 279 |
+
# Create a new expanded mask with the same shape
|
| 280 |
+
expanded_mask = np.zeros_like(segmented_nodule_gmm)
|
| 281 |
+
|
| 282 |
+
# Get the coordinates of all white pixels in the original mask
|
| 283 |
+
white_pixel_coords = np.argwhere(segmented_nodule_gmm == 1)
|
| 284 |
+
|
| 285 |
+
# Expand each white pixel by adding the specified number of pixels in each direction
|
| 286 |
+
for coord in white_pixel_coords:
|
| 287 |
+
z, y, x = coord # Extract the z, y, x coordinates of each white pixel
|
| 288 |
+
|
| 289 |
+
# Define the range to expand for each coordinate
|
| 290 |
+
z_range = range(max(0, z - expand_pixels[0]), min(segmented_nodule_gmm.shape[0], z + expand_pixels[0] + 1))
|
| 291 |
+
y_range = range(max(0, y - expand_pixels[1]), min(segmented_nodule_gmm.shape[1], y + expand_pixels[1] + 1))
|
| 292 |
+
x_range = range(max(0, x - expand_pixels[2]), min(segmented_nodule_gmm.shape[2], x + expand_pixels[2] + 1))
|
| 293 |
+
|
| 294 |
+
# Update the new mask by setting all pixels in this range to 1
|
| 295 |
+
for z_new in z_range:
|
| 296 |
+
for y_new in y_range:
|
| 297 |
+
for x_new in x_range:
|
| 298 |
+
expanded_mask[z_new, y_new, x_new] = 1
|
| 299 |
+
|
| 300 |
+
return expanded_mask
|
| 301 |
+
|
| 302 |
+
|
| 303 |
+
def find_nodule_lobe(cccwhd, lung_mask, class_map):
|
| 304 |
+
"""
|
| 305 |
+
Determine the lung lobe where a nodule is located based on a 3D mask and bounding box.
|
| 306 |
+
|
| 307 |
+
Parameters:
|
| 308 |
+
cccwhd (list or tuple): Bounding box in the format [center_x, center_y, center_z, width, height, depth].
|
| 309 |
+
lung_mask (numpy array): 3D array representing the lung mask with different lung regions.
|
| 310 |
+
class_map (dict): Dictionary mapping lung region labels to their names.
|
| 311 |
+
|
| 312 |
+
Returns:
|
| 313 |
+
str: Name of the lung lobe where the nodule is located.
|
| 314 |
+
"""
|
| 315 |
+
center_x, center_y, center_z, width, height, depth = cccwhd
|
| 316 |
+
|
| 317 |
+
# Calculate the bounding box limits
|
| 318 |
+
start_x = int(center_x - width // 2)
|
| 319 |
+
end_x = int(center_x + width // 2)
|
| 320 |
+
start_y = int(center_y - height // 2)
|
| 321 |
+
end_y = int(center_y + height // 2)
|
| 322 |
+
start_z = int(center_z - depth // 2)
|
| 323 |
+
end_z = int(center_z + depth // 2)
|
| 324 |
+
|
| 325 |
+
# Ensure the indices are within the mask dimensions
|
| 326 |
+
start_x = max(0, start_x)
|
| 327 |
+
end_x = min(lung_mask.shape[0], end_x)
|
| 328 |
+
start_y = max(0, start_y)
|
| 329 |
+
end_y = min(lung_mask.shape[1], end_y)
|
| 330 |
+
start_z = max(0, start_z)
|
| 331 |
+
end_z = min(lung_mask.shape[2], end_z)
|
| 332 |
+
|
| 333 |
+
# Extract the region of interest (ROI) from the mask
|
| 334 |
+
roi = lung_mask[start_x:end_x, start_y:end_y, start_z:end_z]
|
| 335 |
+
|
| 336 |
+
# Count the occurrences of each lobe label within the ROI
|
| 337 |
+
unique, counts = np.unique(roi, return_counts=True)
|
| 338 |
+
label_counts = dict(zip(unique, counts))
|
| 339 |
+
|
| 340 |
+
# Exclude the background (label 0)
|
| 341 |
+
if 0 in label_counts:
|
| 342 |
+
del label_counts[0]
|
| 343 |
+
|
| 344 |
+
# Find the label with the maximum count
|
| 345 |
+
if label_counts:
|
| 346 |
+
nodule_lobe = max(label_counts, key=label_counts.get)
|
| 347 |
+
else:
|
| 348 |
+
nodule_lobe = None
|
| 349 |
+
|
| 350 |
+
# Map the label to the corresponding lung lobe
|
| 351 |
+
if nodule_lobe is not None:
|
| 352 |
+
nodule_lobe_name = class_map["lungs"][nodule_lobe]
|
| 353 |
+
else:
|
| 354 |
+
nodule_lobe_name = "Undefined"
|
| 355 |
+
|
| 356 |
+
return nodule_lobe_name
|
| 357 |
+
|
| 358 |
+
|
| 359 |
+
def find_nodule_lobe_and_distance(cccwhd, lung_mask, class_map,spacing):
|
| 360 |
+
"""
|
| 361 |
+
Determine the lung lobe where a nodule is located and measure its distance from the lung wall.
|
| 362 |
+
|
| 363 |
+
Parameters:
|
| 364 |
+
cccwhd (list or tuple): Bounding box in the format [center_x, center_y, center_z, width, height, depth].
|
| 365 |
+
lung_mask (numpy array): 3D array representing the lung mask with different lung regions.
|
| 366 |
+
class_map (dict): Dictionary mapping lung region labels to their names.
|
| 367 |
+
|
| 368 |
+
Returns:
|
| 369 |
+
tuple: (Name of the lung lobe, Distance from the lung wall)
|
| 370 |
+
"""
|
| 371 |
+
center_x, center_y, center_z, width, height, depth = cccwhd
|
| 372 |
+
|
| 373 |
+
# Calculate the bounding box limits
|
| 374 |
+
start_x = int(center_x - width // 2)
|
| 375 |
+
end_x = int(center_x + width // 2)
|
| 376 |
+
start_y = int(center_y - height // 2)
|
| 377 |
+
end_y = int(center_y + height // 2)
|
| 378 |
+
start_z = int(center_z - depth // 2)
|
| 379 |
+
end_z = int(center_z + depth // 2)
|
| 380 |
+
|
| 381 |
+
# Ensure the indices are within the mask dimensions
|
| 382 |
+
start_x = max(0, start_x)
|
| 383 |
+
end_x = min(lung_mask.shape[0], end_x)
|
| 384 |
+
start_y = max(0, start_y)
|
| 385 |
+
end_y = min(lung_mask.shape[1], end_y)
|
| 386 |
+
start_z = max(0, start_z)
|
| 387 |
+
end_z = min(lung_mask.shape[2], end_z)
|
| 388 |
+
|
| 389 |
+
# Extract the region of interest (ROI) from the mask
|
| 390 |
+
roi = lung_mask[start_x:end_x, start_y:end_y, start_z:end_z]
|
| 391 |
+
|
| 392 |
+
# Count the occurrences of each lobe label within the ROI
|
| 393 |
+
unique, counts = np.unique(roi, return_counts=True)
|
| 394 |
+
label_counts = dict(zip(unique, counts))
|
| 395 |
+
|
| 396 |
+
# Exclude the background (label 0)
|
| 397 |
+
if 0 in label_counts:
|
| 398 |
+
del label_counts[0]
|
| 399 |
+
|
| 400 |
+
# Find the label with the maximum count
|
| 401 |
+
if label_counts:
|
| 402 |
+
nodule_lobe = max(label_counts, key=label_counts.get)
|
| 403 |
+
else:
|
| 404 |
+
nodule_lobe = None
|
| 405 |
+
|
| 406 |
+
# Map the label to the corresponding lung lobe
|
| 407 |
+
if nodule_lobe is not None:
|
| 408 |
+
nodule_lobe_name = class_map["lungs"][nodule_lobe]
|
| 409 |
+
else:
|
| 410 |
+
nodule_lobe_name = "Undefined"
|
| 411 |
+
|
| 412 |
+
# Calculate the distance from the nodule centroid to the nearest lung wall
|
| 413 |
+
nodule_centroid = np.array([center_x, center_y, center_z])
|
| 414 |
+
|
| 415 |
+
# Create a binary lung mask where lung region is 1 and outside lung is 0
|
| 416 |
+
lung_binary_mask = lung_mask > 0
|
| 417 |
+
|
| 418 |
+
# Create the lung wall mask by finding the outer boundary
|
| 419 |
+
# Use binary erosion to shrink the lung mask, then subtract it from the original mask to get the boundary
|
| 420 |
+
lung_eroded = binary_erosion(lung_binary_mask)
|
| 421 |
+
lung_wall_mask = lung_binary_mask & ~lung_eroded # Lung wall mask is the outermost boundary (contour)
|
| 422 |
+
|
| 423 |
+
# Compute the distance transform from the lung wall
|
| 424 |
+
distance_transform = distance_transform_edt(~lung_wall_mask) # Compute distance to nearest lung boundary
|
| 425 |
+
|
| 426 |
+
|
| 427 |
+
|
| 428 |
+
# Get the distance from the nodule centroid to the nearest lung wall in voxel units
|
| 429 |
+
voxel_distance_to_lung_wall = distance_transform[center_x, center_y, center_z]
|
| 430 |
+
|
| 431 |
+
# Convert voxel distance to real-world distance in mm
|
| 432 |
+
physical_distance_to_lung_wall = voxel_distance_to_lung_wall * np.sqrt(
|
| 433 |
+
spacing[0]**2 + spacing[1]**2 + spacing[2]**2
|
| 434 |
+
)
|
| 435 |
+
|
| 436 |
+
|
| 437 |
+
|
| 438 |
+
return nodule_lobe_name, voxel_distance_to_lung_wall,physical_distance_to_lung_wall
|
| 439 |
+
|
| 440 |
+
|
| 441 |
+
# +
|
| 442 |
+
def expand_mask_by_distance(segmented_nodule_gmm, spacing, expansion_mm):
|
| 443 |
+
"""
|
| 444 |
+
Expands the segmentation mask by a given distance in mm in all directions by directly updating pixel values.
|
| 445 |
+
|
| 446 |
+
Parameters:
|
| 447 |
+
segmented_nodule_gmm (numpy array): 3D binary mask of the nodule (1 for nodule, 0 for background).
|
| 448 |
+
spacing (tuple): Spacing of the image in mm for each voxel, given as (spacing_x, spacing_y, spacing_z).
|
| 449 |
+
expansion_mm (float): Distance to expand the mask in millimeters.
|
| 450 |
+
|
| 451 |
+
Returns:
|
| 452 |
+
numpy array: Expanded segmentation mask.
|
| 453 |
+
"""
|
| 454 |
+
# Reorder spacing to match the numpy array's (z, y, x) format
|
| 455 |
+
spacing_reordered = (spacing[2], spacing[1], spacing[0]) # (spacing_z, spacing_y, spacing_x)
|
| 456 |
+
|
| 457 |
+
# Calculate the number of pixels to expand in each dimension
|
| 458 |
+
expand_pixels = np.array([int(np.round(expansion_mm / s)) for s in spacing_reordered])
|
| 459 |
+
|
| 460 |
+
# Create a new expanded mask with the same shape
|
| 461 |
+
expanded_mask = np.zeros_like(segmented_nodule_gmm)
|
| 462 |
+
|
| 463 |
+
# Get the coordinates of all white pixels in the original mask
|
| 464 |
+
white_pixel_coords = np.argwhere(segmented_nodule_gmm == 1)
|
| 465 |
+
|
| 466 |
+
# Expand each white pixel by adding the specified number of pixels in each direction
|
| 467 |
+
for coord in white_pixel_coords:
|
| 468 |
+
z, y, x = coord # Extract the z, y, x coordinates of each white pixel
|
| 469 |
+
|
| 470 |
+
# Define the range to expand for each coordinate
|
| 471 |
+
z_range = range(max(0, z - expand_pixels[0]), min(segmented_nodule_gmm.shape[0], z + expand_pixels[0] + 1))
|
| 472 |
+
y_range = range(max(0, y - expand_pixels[1]), min(segmented_nodule_gmm.shape[1], y + expand_pixels[1] + 1))
|
| 473 |
+
x_range = range(max(0, x - expand_pixels[2]), min(segmented_nodule_gmm.shape[2], x + expand_pixels[2] + 1))
|
| 474 |
+
|
| 475 |
+
# Update the new mask by setting all pixels in this range to 1
|
| 476 |
+
for z_new in z_range:
|
| 477 |
+
for y_new in y_range:
|
| 478 |
+
for x_new in x_range:
|
| 479 |
+
expanded_mask[z_new, y_new, x_new] = 1
|
| 480 |
+
|
| 481 |
+
return expanded_mask
|
| 482 |
+
|
| 483 |
+
|
| 484 |
+
|
| 485 |
+
# Function to plot the contours of a mask
|
| 486 |
+
def plot_contours(ax, mask, color, linewidth=1.5):
|
| 487 |
+
contours = measure.find_contours(mask, level=0.5) # Find contours at a constant level
|
| 488 |
+
for contour in contours:
|
| 489 |
+
ax.plot(contour[:, 1], contour[:, 0], color=color, linewidth=linewidth)
|
| 490 |
+
|
scripts/DLCS24_CADe_64Qpatch.sh
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
|
| 3 |
+
# ============================
|
| 4 |
+
# Docker Container Activation
|
| 5 |
+
# ============================
|
| 6 |
+
echo "Starting Docker container..."
|
| 7 |
+
cd "$(dirname "$0")/.." # Go to project root
|
| 8 |
+
|
| 9 |
+
# Remove existing container if it exists
|
| 10 |
+
docker rm -f nodule_seg_pipeline 2>/dev/null || true
|
| 11 |
+
|
| 12 |
+
# Start container using existing medical imaging image
|
| 13 |
+
docker run -d --name nodule_seg_pipeline \
|
| 14 |
+
-v "$(pwd):/app" \
|
| 15 |
+
-w /app \
|
| 16 |
+
ft42/pins:latest \
|
| 17 |
+
tail -f /dev/null
|
| 18 |
+
|
| 19 |
+
# Create output directory and set proper permissions
|
| 20 |
+
echo "Setting up output directories and permissions..."
|
| 21 |
+
docker exec nodule_seg_pipeline mkdir -p /app/demofolder/output/DLCS24_64Q_CAD_patches
|
| 22 |
+
docker exec nodule_seg_pipeline chmod -R 777 /app/demofolder/output/
|
| 23 |
+
|
| 24 |
+
echo "Installing missing Python packages..."
|
| 25 |
+
docker exec nodule_seg_pipeline apt-get update > /dev/null 2>&1
|
| 26 |
+
docker exec nodule_seg_pipeline apt-get install -y libgl1 libglib2.0-0 > /dev/null 2>&1
|
| 27 |
+
docker exec nodule_seg_pipeline pip install opencv-python-headless torch torchvision monai "numpy<2.0" --quiet
|
| 28 |
+
|
| 29 |
+
echo "Docker container is running with all dependencies installed"
|
| 30 |
+
|
| 31 |
+
# ============================
|
| 32 |
+
# Configuration Variables
|
| 33 |
+
# ============================
|
| 34 |
+
# Define paths
|
| 35 |
+
PYTHON_SCRIPT="/app/scr/candidate_worldCoord_patchExtarctor_pipeline.py" # Path inside container
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
DATASET_NAME="DLCSD24"
|
| 39 |
+
RAW_DATA_PATH="/app/demofolder/data/DLCS24/"
|
| 40 |
+
CSV_SAVE_PATH="/app/demofolder/output/DLCS24_64Q_CAD_patches/"
|
| 41 |
+
DATASET_CSV="/app/demofolder/data/DLCSD24_Annotations_N2.csv"
|
| 42 |
+
|
| 43 |
+
NIFTI_CLM_NAME="ct_nifti_file"
|
| 44 |
+
UNIQUE_ANNOTATION_ID="nodule_id" # Leave empty or remove if not in CSV
|
| 45 |
+
MALIGNANT_LBL="Malignant_lbl"
|
| 46 |
+
COORD_X="coordX"
|
| 47 |
+
COORD_Y="coordY"
|
| 48 |
+
COORD_Z="coordZ"
|
| 49 |
+
W="w"
|
| 50 |
+
H="h"
|
| 51 |
+
D="d"
|
| 52 |
+
SAVE_NIFTI_PATH="/app/demofolder/output/DLCS24_64Q_CAD_patches/nifti/"
|
| 53 |
+
PATCH_SIZE="64 64 64"
|
| 54 |
+
NORMALIZATION="-1000 500 0 1"
|
| 55 |
+
CLIP="True" # Change to "False" if needed
|
| 56 |
+
|
| 57 |
+
# ============================
|
| 58 |
+
# Run the Python script in Docker
|
| 59 |
+
# ============================
|
| 60 |
+
echo "Running segmentation in Docker container..."
|
| 61 |
+
docker exec nodule_seg_pipeline python3 "$PYTHON_SCRIPT" \
|
| 62 |
+
--dataset_name "$DATASET_NAME" \
|
| 63 |
+
--raw_data_path "$RAW_DATA_PATH" \
|
| 64 |
+
--csv_save_path "$CSV_SAVE_PATH" \
|
| 65 |
+
--dataset_csv "$DATASET_CSV" \
|
| 66 |
+
--nifti_clm_name "$NIFTI_CLM_NAME" \
|
| 67 |
+
--unique_Annotation_id "$UNIQUE_ANNOTATION_ID" \
|
| 68 |
+
--Malignant_lbl "$MALIGNANT_LBL" \
|
| 69 |
+
--coordX "$COORD_X" \
|
| 70 |
+
--coordY "$COORD_Y" \
|
| 71 |
+
--coordZ "$COORD_Z" \
|
| 72 |
+
--patch_size $PATCH_SIZE \
|
| 73 |
+
--normalization $NORMALIZATION \
|
| 74 |
+
--clip "$CLIP" \
|
| 75 |
+
--save_nifti_path "$SAVE_NIFTI_PATH"
|
| 76 |
+
|
scripts/DLCS24_KNN_2mm_Extend_Radiomics.sh
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
|
| 3 |
+
# ===# Create output directory and set proper permissions
|
| 4 |
+
echo "Setting up output directories and permissions..."
|
| 5 |
+
docker exec nodule_seg_pipeline mkdir -p /app/demofolder/output/DLCS24_KNN_2mm_Extend_Seg
|
| 6 |
+
docker exec nodule_seg_pipeline chmod -R 777 /app/demofolder/output/
|
| 7 |
+
|
| 8 |
+
echo "Installing missing Python packages if needed..."
|
| 9 |
+
docker exec nodule_seg_pipeline pip install opencv-python-headless --quiet > /dev/null 2>&1 || true
|
| 10 |
+
|
| 11 |
+
echo "Docker container is running with write permissions set"===================
|
| 12 |
+
# Docker Container Activation
|
| 13 |
+
# ============================
|
| 14 |
+
echo "Starting Docker container..."
|
| 15 |
+
cd "$(dirname "$0")/.." # Go to project root
|
| 16 |
+
|
| 17 |
+
# Remove existing container if it exists
|
| 18 |
+
docker rm -f nodule_seg_pipeline 2>/dev/null || true
|
| 19 |
+
|
| 20 |
+
# Start container using existing medical imaging image
|
| 21 |
+
docker run -d --name nodule_seg_pipeline \
|
| 22 |
+
-v "$(pwd):/app" \
|
| 23 |
+
-w /app \
|
| 24 |
+
ft42/pins:latest \
|
| 25 |
+
tail -f /dev/null
|
| 26 |
+
|
| 27 |
+
# Create output directory and set proper permissions
|
| 28 |
+
echo "Setting up output directories and permissions..."
|
| 29 |
+
docker exec nodule_seg_pipeline mkdir -p /app/demofolder/output/DLCS24_KNN_2mm_Extend_RadiomicsSeg
|
| 30 |
+
docker exec nodule_seg_pipeline chmod -R 777 /app/demofolder/output/
|
| 31 |
+
|
| 32 |
+
echo "Docker container is running with write permissions set"
|
| 33 |
+
|
| 34 |
+
# ============================
|
| 35 |
+
# Configuration Variables
|
| 36 |
+
# ============================
|
| 37 |
+
# Define paths
|
| 38 |
+
PYTHON_SCRIPT="/app/scr/candidateSeg_radiomicsExtractor_pipiline.py" # Path inside container
|
| 39 |
+
PARAMS_JSON="/app/scr/Pyradiomics_feature_extarctor_pram.json" # Path inside container
|
| 40 |
+
|
| 41 |
+
DATASET_NAME="DLCSD24"
|
| 42 |
+
RAW_DATA_PATH="/app/demofolder/data/DLCS24/"
|
| 43 |
+
CSV_SAVE_PATH="/app/demofolder/output/"
|
| 44 |
+
DATASET_CSV="/app/demofolder/data/DLCSD24_Annotations_N2.csv"
|
| 45 |
+
|
| 46 |
+
NIFTI_CLM_NAME="ct_nifti_file"
|
| 47 |
+
UNIQUE_ANNOTATION_ID="nodule_id" # Leave empty or remove if not in CSV
|
| 48 |
+
MALIGNANT_LBL="Malignant_lbl"
|
| 49 |
+
COORD_X="coordX"
|
| 50 |
+
COORD_Y="coordY"
|
| 51 |
+
COORD_Z="coordZ"
|
| 52 |
+
W="w"
|
| 53 |
+
H="h"
|
| 54 |
+
D="d"
|
| 55 |
+
|
| 56 |
+
SEG_ALG="knn" # Choose from: gmm, knn, fcm, otsu
|
| 57 |
+
EXPANSION_MM=2.0 # Set the expansion in millimeters
|
| 58 |
+
SAVE_NIFTI_PATH="/app/demofolder/output/DLCS24_KNN_2mm_Extend_RadiomicsSeg/"
|
| 59 |
+
SAVE_MASK_FLAG="--save_the_generated_mask" # Remove if you don't want to save masks
|
| 60 |
+
USE_EXPAND_FLAG="--use_expand" # Include if you want to use expansion
|
| 61 |
+
EXTRACT_RADIOMICS_FLAG="--extract_radiomics" # Include if you want to extract radiomics "--extract_radiomics"
|
| 62 |
+
|
| 63 |
+
# ============================
|
| 64 |
+
# Run the Python script in Docker
|
| 65 |
+
# ============================
|
| 66 |
+
echo "Running segmentation in Docker container..."
|
| 67 |
+
docker exec nodule_seg_pipeline python3 "$PYTHON_SCRIPT" \
|
| 68 |
+
--dataset_name "$DATASET_NAME" \
|
| 69 |
+
--raw_data_path "$RAW_DATA_PATH" \
|
| 70 |
+
--csv_save_path "$CSV_SAVE_PATH" \
|
| 71 |
+
--dataset_csv "$DATASET_CSV" \
|
| 72 |
+
--nifti_clm_name "$NIFTI_CLM_NAME" \
|
| 73 |
+
--unique_Annotation_id "$UNIQUE_ANNOTATION_ID" \
|
| 74 |
+
--Malignant_lbl "$MALIGNANT_LBL" \
|
| 75 |
+
--coordX "$COORD_X" \
|
| 76 |
+
--coordY "$COORD_Y" \
|
| 77 |
+
--coordZ "$COORD_Z" \
|
| 78 |
+
--w "$W" \
|
| 79 |
+
--h "$H" \
|
| 80 |
+
--d "$D" \
|
| 81 |
+
--seg_alg "$SEG_ALG" \
|
| 82 |
+
--expansion_mm "$EXPANSION_MM" \
|
| 83 |
+
--params_json "$PARAMS_JSON" \
|
| 84 |
+
--save_nifti_path "$SAVE_NIFTI_PATH" \
|
| 85 |
+
$USE_EXPAND_FLAG \
|
| 86 |
+
$EXTRACT_RADIOMICS_FLAG \
|
| 87 |
+
$SAVE_MASK_FLAG
|
| 88 |
+
|
| 89 |
+
echo "✅ Segmentation completed! Check demofolder/output/ directory for results."
|
scripts/DLCS24_KNN_2mm_Extend_Seg.sh
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
|
| 3 |
+
# ============================
|
| 4 |
+
# Docker Container Activation
|
| 5 |
+
# ============================
|
| 6 |
+
echo "Starting Docker container..."
|
| 7 |
+
cd "$(dirname "$0")/.." # Go to project root
|
| 8 |
+
|
| 9 |
+
# Remove existing container if it exists
|
| 10 |
+
docker rm -f nodule_seg_pipeline 2>/dev/null || true
|
| 11 |
+
|
| 12 |
+
# Start container using existing medical imaging image
|
| 13 |
+
docker run -d --name nodule_seg_pipeline \
|
| 14 |
+
-v "$(pwd):/app" \
|
| 15 |
+
-w /app \
|
| 16 |
+
ft42/pins:latest \
|
| 17 |
+
tail -f /dev/null
|
| 18 |
+
|
| 19 |
+
# Create output directory and set proper permissions
|
| 20 |
+
echo "Setting up output directories and permissions..."
|
| 21 |
+
docker exec nodule_seg_pipeline mkdir -p /app/demofolder/output/DLCS24_KNN_2mm_Extend_Seg
|
| 22 |
+
docker exec nodule_seg_pipeline chmod -R 777 /app/demofolder/output/
|
| 23 |
+
|
| 24 |
+
echo "Installing missing Python packages if needed..."
|
| 25 |
+
docker exec nodule_seg_pipeline pip install opencv-python-headless --quiet > /dev/null 2>&1 || true
|
| 26 |
+
|
| 27 |
+
echo "Docker container is running with write permissions set"
|
| 28 |
+
|
| 29 |
+
# ============================
|
| 30 |
+
# Configuration Variables
|
| 31 |
+
# ============================
|
| 32 |
+
# Define paths
|
| 33 |
+
PYTHON_SCRIPT="/app/scr/candidateSeg_pipiline.py" # Path inside container
|
| 34 |
+
PARAMS_JSON="/app/scr/Pyradiomics_feature_extarctor_pram.json" # Path inside container
|
| 35 |
+
|
| 36 |
+
DATASET_NAME="DLCSD24"
|
| 37 |
+
RAW_DATA_PATH="/app/demofolder/data/DLCS24/"
|
| 38 |
+
CSV_SAVE_PATH="/app/demofolder/output/"
|
| 39 |
+
DATASET_CSV="/app/demofolder/data/DLCSD24_Annotations_N2.csv"
|
| 40 |
+
|
| 41 |
+
NIFTI_CLM_NAME="ct_nifti_file"
|
| 42 |
+
UNIQUE_ANNOTATION_ID="nodule_id" # Leave empty or remove if not in CSV
|
| 43 |
+
MALIGNANT_LBL="Malignant_lbl"
|
| 44 |
+
COORD_X="coordX"
|
| 45 |
+
COORD_Y="coordY"
|
| 46 |
+
COORD_Z="coordZ"
|
| 47 |
+
W="w"
|
| 48 |
+
H="h"
|
| 49 |
+
D="d"
|
| 50 |
+
|
| 51 |
+
SEG_ALG="knn" # Choose from: gmm, knn, fcm, otsu
|
| 52 |
+
EXPANSION_MM=2.0 # Set the expansion in millimeters
|
| 53 |
+
SAVE_NIFTI_PATH="/app/demofolder/output/DLCS24_KNN_2mm_Extend_Seg/"
|
| 54 |
+
SAVE_MASK_FLAG="--save_the_generated_mask" # Remove if you don't want to save masks
|
| 55 |
+
USE_EXPAND_FLAG="--use_expand" # Include if you want to use expansion
|
| 56 |
+
EXTRACT_RADIOMICS_FLAG="" # Include if you want to extract radiomics "--extract_radiomics"
|
| 57 |
+
|
| 58 |
+
# ============================
|
| 59 |
+
# Run the Python script in Docker
|
| 60 |
+
# ============================
|
| 61 |
+
echo "Running segmentation in Docker container..."
|
| 62 |
+
docker exec nodule_seg_pipeline python3 "$PYTHON_SCRIPT" \
|
| 63 |
+
--dataset_name "$DATASET_NAME" \
|
| 64 |
+
--raw_data_path "$RAW_DATA_PATH" \
|
| 65 |
+
--csv_save_path "$CSV_SAVE_PATH" \
|
| 66 |
+
--dataset_csv "$DATASET_CSV" \
|
| 67 |
+
--nifti_clm_name "$NIFTI_CLM_NAME" \
|
| 68 |
+
--unique_Annotation_id "$UNIQUE_ANNOTATION_ID" \
|
| 69 |
+
--Malignant_lbl "$MALIGNANT_LBL" \
|
| 70 |
+
--coordX "$COORD_X" \
|
| 71 |
+
--coordY "$COORD_Y" \
|
| 72 |
+
--coordZ "$COORD_Z" \
|
| 73 |
+
--w "$W" \
|
| 74 |
+
--h "$H" \
|
| 75 |
+
--d "$D" \
|
| 76 |
+
--seg_alg "$SEG_ALG" \
|
| 77 |
+
--expansion_mm "$EXPANSION_MM" \
|
| 78 |
+
--params_json "$PARAMS_JSON" \
|
| 79 |
+
--save_nifti_path "$SAVE_NIFTI_PATH" \
|
| 80 |
+
$USE_EXPAND_FLAG \
|
| 81 |
+
$EXTRACT_RADIOMICS_FLAG \
|
| 82 |
+
$SAVE_MASK_FLAG
|
| 83 |
+
|
| 84 |
+
echo "✅ Segmentation completed! Check demofolder/output/ directory for results."
|
scripts/build.sh
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
|
| 3 |
+
# Medical Imaging Nodule Segmentation Pipeline - Build Script
|
| 4 |
+
# Automated Docker container build and setup
|
| 5 |
+
|
| 6 |
+
set -e # Exit on any error
|
| 7 |
+
|
| 8 |
+
# Color codes for output
|
| 9 |
+
RED='\033[0;31m'
|
| 10 |
+
GREEN='\033[0;32m'
|
| 11 |
+
YELLOW='\033[1;33m'
|
| 12 |
+
BLUE='\033[0;34m'
|
| 13 |
+
NC='\033[0m' # No Color
|
| 14 |
+
|
| 15 |
+
# Script configuration
|
| 16 |
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
| 17 |
+
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
| 18 |
+
IMAGE_NAME="medical-imaging/nodule-segmentation"
|
| 19 |
+
IMAGE_TAG="latest"
|
| 20 |
+
CONTAINER_NAME="nodule_seg_pipeline"
|
| 21 |
+
|
| 22 |
+
# Function to print colored output
|
| 23 |
+
print_status() {
|
| 24 |
+
echo -e "${BLUE}[INFO]${NC} $1"
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
print_success() {
|
| 28 |
+
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
print_warning() {
|
| 32 |
+
echo -e "${YELLOW}[WARNING]${NC} $1"
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
print_error() {
|
| 36 |
+
echo -e "${RED}[ERROR]${NC} $1"
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
# Function to check prerequisites
|
| 40 |
+
check_prerequisites() {
|
| 41 |
+
print_status "Checking prerequisites..."
|
| 42 |
+
|
| 43 |
+
# Check if Docker is installed and running
|
| 44 |
+
if ! command -v docker &> /dev/null; then
|
| 45 |
+
print_error "Docker is not installed. Please install Docker first."
|
| 46 |
+
exit 1
|
| 47 |
+
fi
|
| 48 |
+
|
| 49 |
+
# Check if Docker daemon is running
|
| 50 |
+
if ! docker info &> /dev/null; then
|
| 51 |
+
print_error "Docker daemon is not running. Please start Docker first."
|
| 52 |
+
exit 1
|
| 53 |
+
fi
|
| 54 |
+
|
| 55 |
+
# Check if Docker Compose is available
|
| 56 |
+
if ! command -v docker-compose &> /dev/null; then
|
| 57 |
+
print_warning "docker-compose not found. Checking for 'docker compose'..."
|
| 58 |
+
if ! docker compose version &> /dev/null; then
|
| 59 |
+
print_error "Docker Compose is not available. Please install Docker Compose."
|
| 60 |
+
exit 1
|
| 61 |
+
else
|
| 62 |
+
DOCKER_COMPOSE="docker compose"
|
| 63 |
+
fi
|
| 64 |
+
else
|
| 65 |
+
DOCKER_COMPOSE="docker-compose"
|
| 66 |
+
fi
|
| 67 |
+
|
| 68 |
+
print_success "Prerequisites check passed!"
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
# Function to create necessary directories
|
| 72 |
+
setup_directories() {
|
| 73 |
+
print_status "Setting up directory structure..."
|
| 74 |
+
|
| 75 |
+
# Create directories if they don't exist
|
| 76 |
+
directories=(
|
| 77 |
+
"docker/logs"
|
| 78 |
+
"docker/notebooks"
|
| 79 |
+
"src"
|
| 80 |
+
"scripts"
|
| 81 |
+
"params"
|
| 82 |
+
"data"
|
| 83 |
+
"output"
|
| 84 |
+
"logs"
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
for dir in "${directories[@]}"; do
|
| 88 |
+
if [ ! -d "$PROJECT_ROOT/$dir" ]; then
|
| 89 |
+
mkdir -p "$PROJECT_ROOT/$dir"
|
| 90 |
+
print_status "Created directory: $dir"
|
| 91 |
+
fi
|
| 92 |
+
done
|
| 93 |
+
|
| 94 |
+
print_success "Directory structure ready!"
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
# Function to copy source files
|
| 98 |
+
setup_source_files() {
|
| 99 |
+
print_status "Setting up source files..."
|
| 100 |
+
|
| 101 |
+
# Copy source files to appropriate directories
|
| 102 |
+
if [ -f "$PROJECT_ROOT/scr/candidateSeg_pipiline.py" ]; then
|
| 103 |
+
cp "$PROJECT_ROOT/scr/candidateSeg_pipiline.py" "$PROJECT_ROOT/src/"
|
| 104 |
+
print_status "Copied main pipeline script"
|
| 105 |
+
fi
|
| 106 |
+
|
| 107 |
+
if [ -f "$PROJECT_ROOT/scr/cvseg_utils.py" ]; then
|
| 108 |
+
cp "$PROJECT_ROOT/scr/cvseg_utils.py" "$PROJECT_ROOT/src/"
|
| 109 |
+
print_status "Copied utility scripts"
|
| 110 |
+
fi
|
| 111 |
+
|
| 112 |
+
if [ -f "$PROJECT_ROOT/DLCS24_KNN_2mm_Extend_Seg.sh" ]; then
|
| 113 |
+
cp "$PROJECT_ROOT/DLCS24_KNN_2mm_Extend_Seg.sh" "$PROJECT_ROOT/scripts/"
|
| 114 |
+
chmod +x "$PROJECT_ROOT/scripts/DLCS24_KNN_2mm_Extend_Seg.sh"
|
| 115 |
+
print_status "Copied execution scripts"
|
| 116 |
+
fi
|
| 117 |
+
|
| 118 |
+
if [ -f "$PROJECT_ROOT/scr/Pyradiomics_feature_extarctor_pram.json" ]; then
|
| 119 |
+
cp "$PROJECT_ROOT/scr/Pyradiomics_feature_extarctor_pram.json" "$PROJECT_ROOT/params/"
|
| 120 |
+
print_status "Copied parameter files"
|
| 121 |
+
fi
|
| 122 |
+
|
| 123 |
+
print_success "Source files ready!"
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
# Function to build Docker image
|
| 127 |
+
build_image() {
|
| 128 |
+
print_status "Building Docker image: $IMAGE_NAME:$IMAGE_TAG"
|
| 129 |
+
|
| 130 |
+
cd "$PROJECT_ROOT"
|
| 131 |
+
|
| 132 |
+
# Build with docker-compose
|
| 133 |
+
if $DOCKER_COMPOSE build --no-cache; then
|
| 134 |
+
print_success "Docker image built successfully!"
|
| 135 |
+
else
|
| 136 |
+
print_error "Failed to build Docker image"
|
| 137 |
+
exit 1
|
| 138 |
+
fi
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
# Function to verify the build
|
| 142 |
+
verify_build() {
|
| 143 |
+
print_status "Verifying Docker image..."
|
| 144 |
+
|
| 145 |
+
# Check if image exists
|
| 146 |
+
if docker images | grep -q "$IMAGE_NAME"; then
|
| 147 |
+
print_success "Docker image verified!"
|
| 148 |
+
|
| 149 |
+
# Show image details
|
| 150 |
+
print_status "Image details:"
|
| 151 |
+
docker images | grep "$IMAGE_NAME" | head -1
|
| 152 |
+
|
| 153 |
+
# Test basic functionality
|
| 154 |
+
print_status "Testing basic functionality..."
|
| 155 |
+
if docker run --rm "$IMAGE_NAME:$IMAGE_TAG" python3 -c "import SimpleITK, radiomics, sklearn, skimage, scipy, pandas, numpy; print('All dependencies available!')"; then
|
| 156 |
+
print_success "All dependencies are working correctly!"
|
| 157 |
+
else
|
| 158 |
+
print_warning "Some dependencies may not be working correctly"
|
| 159 |
+
fi
|
| 160 |
+
else
|
| 161 |
+
print_error "Docker image not found after build"
|
| 162 |
+
exit 1
|
| 163 |
+
fi
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
# Function to show usage instructions
|
| 167 |
+
show_usage() {
|
| 168 |
+
print_status "Build complete! Here's how to use the container:"
|
| 169 |
+
echo ""
|
| 170 |
+
echo "1. Start the container:"
|
| 171 |
+
echo " $DOCKER_COMPOSE up -d nodule-segmentation"
|
| 172 |
+
echo ""
|
| 173 |
+
echo "2. Run the segmentation pipeline:"
|
| 174 |
+
echo " $DOCKER_COMPOSE exec nodule-segmentation bash /app/scripts/DLCS24_KNN_2mm_Extend_Seg.sh"
|
| 175 |
+
echo ""
|
| 176 |
+
echo "3. Run interactively:"
|
| 177 |
+
echo " $DOCKER_COMPOSE exec nodule-segmentation bash"
|
| 178 |
+
echo ""
|
| 179 |
+
echo "4. Start Jupyter (optional):"
|
| 180 |
+
echo " $DOCKER_COMPOSE --profile jupyter up -d"
|
| 181 |
+
echo " # Access at http://localhost:8888 (token: medical_imaging_2024)"
|
| 182 |
+
echo ""
|
| 183 |
+
echo "5. View logs:"
|
| 184 |
+
echo " $DOCKER_COMPOSE logs -f nodule-segmentation"
|
| 185 |
+
echo ""
|
| 186 |
+
echo "6. Stop the container:"
|
| 187 |
+
echo " $DOCKER_COMPOSE down"
|
| 188 |
+
echo ""
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
# Function to clean up previous builds
|
| 192 |
+
cleanup() {
|
| 193 |
+
print_status "Cleaning up previous builds..."
|
| 194 |
+
|
| 195 |
+
# Stop and remove containers
|
| 196 |
+
$DOCKER_COMPOSE down --remove-orphans 2>/dev/null || true
|
| 197 |
+
|
| 198 |
+
# Remove previous images (optional)
|
| 199 |
+
if [ "$1" = "--clean" ]; then
|
| 200 |
+
docker rmi "$IMAGE_NAME:$IMAGE_TAG" 2>/dev/null || true
|
| 201 |
+
print_status "Removed previous image"
|
| 202 |
+
fi
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
# Main build process
|
| 206 |
+
main() {
|
| 207 |
+
echo "========================================"
|
| 208 |
+
echo "Medical Imaging Pipeline - Build Script"
|
| 209 |
+
echo "========================================"
|
| 210 |
+
echo ""
|
| 211 |
+
|
| 212 |
+
# Parse command line arguments
|
| 213 |
+
CLEAN_BUILD=false
|
| 214 |
+
if [ "$1" = "--clean" ]; then
|
| 215 |
+
CLEAN_BUILD=true
|
| 216 |
+
print_status "Clean build requested"
|
| 217 |
+
fi
|
| 218 |
+
|
| 219 |
+
# Execute build steps
|
| 220 |
+
check_prerequisites
|
| 221 |
+
|
| 222 |
+
if [ "$CLEAN_BUILD" = true ]; then
|
| 223 |
+
cleanup --clean
|
| 224 |
+
fi
|
| 225 |
+
|
| 226 |
+
setup_directories
|
| 227 |
+
setup_source_files
|
| 228 |
+
build_image
|
| 229 |
+
verify_build
|
| 230 |
+
show_usage
|
| 231 |
+
|
| 232 |
+
print_success "Build completed successfully!"
|
| 233 |
+
echo ""
|
| 234 |
+
echo "Next steps:"
|
| 235 |
+
echo "1. Review the README.md for detailed usage instructions"
|
| 236 |
+
echo "2. Prepare your input data in the expected format"
|
| 237 |
+
echo "3. Start the container and run your analysis"
|
| 238 |
+
echo ""
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
# Run main function with all arguments
|
| 242 |
+
main "$@"
|
scripts/run.sh
ADDED
|
@@ -0,0 +1,385 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
|
| 3 |
+
## Script configuration
|
| 4 |
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
| 5 |
+
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
| 6 |
+
CONTAINER_NAME="nodule_seg_pipeline"
|
| 7 |
+
|
| 8 |
+
# Load environment variables if .env file exists
|
| 9 |
+
if [ -f "$PROJECT_ROOT/.env" ]; then
|
| 10 |
+
print_status "Loading environment variables from .env file"
|
| 11 |
+
set -a
|
| 12 |
+
source "$PROJECT_ROOT/.env"
|
| 13 |
+
set +a
|
| 14 |
+
elif [ -f "$PROJECT_ROOT/.env.template" ]; then
|
| 15 |
+
print_warning "No .env file found. Copy .env.template to .env and customize paths"
|
| 16 |
+
print_warning "Using default paths for now"
|
| 17 |
+
fi
|
| 18 |
+
|
| 19 |
+
# Set default environment variables if not set
|
| 20 |
+
export PUID=${PUID:-$(id -u)}
|
| 21 |
+
export PGID=${PGID:-$(id -g)}
|
| 22 |
+
export DATA_PATH=${DATA_PATH:-"$PROJECT_ROOT/demofolder/data"}
|
| 23 |
+
export OUTPUT_PATH=${OUTPUT_PATH:-"$PROJECT_ROOT/output"}ical Imaging Nodule Segmentation Pipeline - Run Script
|
| 24 |
+
# Easy execution of the containerized pipeline
|
| 25 |
+
|
| 26 |
+
set -e # Exit on any error
|
| 27 |
+
|
| 28 |
+
# Color codes for output
|
| 29 |
+
RED='\033[0;31m'
|
| 30 |
+
GREEN='\033[0;32m'
|
| 31 |
+
YELLOW='\033[1;33m'
|
| 32 |
+
BLUE='\033[0;34m'
|
| 33 |
+
NC='\033[0m' # No Color
|
| 34 |
+
|
| 35 |
+
# Script configuration
|
| 36 |
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
| 37 |
+
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
| 38 |
+
CONTAINER_NAME="nodule_seg_pipeline"
|
| 39 |
+
|
| 40 |
+
# Function to print colored output
|
| 41 |
+
print_status() {
|
| 42 |
+
echo -e "${BLUE}[INFO]${NC} $1"
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
print_success() {
|
| 46 |
+
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
print_warning() {
|
| 50 |
+
echo -e "${YELLOW}[WARNING]${NC} $1"
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
print_error() {
|
| 54 |
+
echo -e "${RED}[ERROR]${NC} $1"
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
# Function to check if Docker Compose is available
|
| 58 |
+
check_docker_compose() {
|
| 59 |
+
if command -v docker-compose &> /dev/null; then
|
| 60 |
+
DOCKER_COMPOSE="docker-compose"
|
| 61 |
+
elif docker compose version &> /dev/null; then
|
| 62 |
+
DOCKER_COMPOSE="docker compose"
|
| 63 |
+
else
|
| 64 |
+
print_error "Docker Compose is not available"
|
| 65 |
+
exit 1
|
| 66 |
+
fi
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
# Function to show help
|
| 70 |
+
show_help() {
|
| 71 |
+
echo "Medical Imaging Nodule Segmentation Pipeline - Run Script"
|
| 72 |
+
echo ""
|
| 73 |
+
echo "Usage: $0 [COMMAND] [OPTIONS]"
|
| 74 |
+
echo ""
|
| 75 |
+
echo "Commands:"
|
| 76 |
+
echo " start Start the container"
|
| 77 |
+
echo " stop Stop the container"
|
| 78 |
+
echo " restart Restart the container"
|
| 79 |
+
echo " run Run the segmentation pipeline"
|
| 80 |
+
echo " shell Open interactive shell in container"
|
| 81 |
+
echo " jupyter Start Jupyter notebook service"
|
| 82 |
+
echo " logs Show container logs"
|
| 83 |
+
echo " status Show container status"
|
| 84 |
+
echo " clean Clean up containers and volumes"
|
| 85 |
+
echo " custom Run custom command in container"
|
| 86 |
+
echo ""
|
| 87 |
+
echo "Options:"
|
| 88 |
+
echo " -h, --help Show this help message"
|
| 89 |
+
echo " -f, --follow Follow logs (for logs command)"
|
| 90 |
+
echo " -v, --verbose Verbose output"
|
| 91 |
+
echo ""
|
| 92 |
+
echo "Examples:"
|
| 93 |
+
echo " $0 start # Start the container"
|
| 94 |
+
echo " $0 run # Run the segmentation pipeline"
|
| 95 |
+
echo " $0 shell # Open interactive shell"
|
| 96 |
+
echo " $0 logs -f # Follow logs"
|
| 97 |
+
echo " $0 custom 'python3 --version' # Run custom command"
|
| 98 |
+
echo ""
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
# Function to check container status
|
| 102 |
+
check_status() {
|
| 103 |
+
if $DOCKER_COMPOSE ps | grep -q "$CONTAINER_NAME.*Up"; then
|
| 104 |
+
return 0 # Container is running
|
| 105 |
+
else
|
| 106 |
+
return 1 # Container is not running
|
| 107 |
+
fi
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
# Function to start the container
|
| 111 |
+
start_container() {
|
| 112 |
+
print_status "Starting the nodule segmentation container..."
|
| 113 |
+
|
| 114 |
+
cd "$PROJECT_ROOT"
|
| 115 |
+
|
| 116 |
+
if check_status; then
|
| 117 |
+
print_warning "Container is already running"
|
| 118 |
+
return 0
|
| 119 |
+
fi
|
| 120 |
+
|
| 121 |
+
if $DOCKER_COMPOSE up -d nodule-segmentation; then
|
| 122 |
+
print_success "Container started successfully!"
|
| 123 |
+
|
| 124 |
+
# Wait for container to be ready
|
| 125 |
+
print_status "Waiting for container to be ready..."
|
| 126 |
+
sleep 5
|
| 127 |
+
|
| 128 |
+
# Check health
|
| 129 |
+
if $DOCKER_COMPOSE exec nodule-segmentation python3 -c "import SimpleITK; print('Container is ready!')" 2>/dev/null; then
|
| 130 |
+
print_success "Container is healthy and ready!"
|
| 131 |
+
else
|
| 132 |
+
print_warning "Container started but health check failed"
|
| 133 |
+
fi
|
| 134 |
+
else
|
| 135 |
+
print_error "Failed to start container"
|
| 136 |
+
exit 1
|
| 137 |
+
fi
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
# Function to stop the container
|
| 141 |
+
stop_container() {
|
| 142 |
+
print_status "Stopping the nodule segmentation container..."
|
| 143 |
+
|
| 144 |
+
cd "$PROJECT_ROOT"
|
| 145 |
+
|
| 146 |
+
if ! check_status; then
|
| 147 |
+
print_warning "Container is not running"
|
| 148 |
+
return 0
|
| 149 |
+
fi
|
| 150 |
+
|
| 151 |
+
if $DOCKER_COMPOSE stop nodule-segmentation; then
|
| 152 |
+
print_success "Container stopped successfully!"
|
| 153 |
+
else
|
| 154 |
+
print_error "Failed to stop container"
|
| 155 |
+
exit 1
|
| 156 |
+
fi
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
# Function to restart the container
|
| 160 |
+
restart_container() {
|
| 161 |
+
print_status "Restarting the nodule segmentation container..."
|
| 162 |
+
stop_container
|
| 163 |
+
sleep 2
|
| 164 |
+
start_container
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
# Function to run the segmentation pipeline
|
| 168 |
+
run_pipeline() {
|
| 169 |
+
print_status "Running the nodule segmentation pipeline..."
|
| 170 |
+
|
| 171 |
+
cd "$PROJECT_ROOT"
|
| 172 |
+
|
| 173 |
+
if ! check_status; then
|
| 174 |
+
print_status "Container is not running. Starting it first..."
|
| 175 |
+
start_container
|
| 176 |
+
fi
|
| 177 |
+
|
| 178 |
+
# Check if the segmentation script exists
|
| 179 |
+
if $DOCKER_COMPOSE exec nodule-segmentation test -f "/app/scripts/DLCS24_KNN_2mm_Extend_Seg.sh"; then
|
| 180 |
+
print_status "Executing segmentation pipeline..."
|
| 181 |
+
if $DOCKER_COMPOSE exec nodule-segmentation bash /app/scripts/DLCS24_KNN_2mm_Extend_Seg.sh; then
|
| 182 |
+
print_success "Pipeline executed successfully!"
|
| 183 |
+
else
|
| 184 |
+
print_error "Pipeline execution failed"
|
| 185 |
+
exit 1
|
| 186 |
+
fi
|
| 187 |
+
else
|
| 188 |
+
print_error "Segmentation script not found in container"
|
| 189 |
+
print_status "Available scripts:"
|
| 190 |
+
$DOCKER_COMPOSE exec nodule-segmentation ls -la /app/scripts/ || true
|
| 191 |
+
exit 1
|
| 192 |
+
fi
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
# Function to open interactive shell
|
| 196 |
+
open_shell() {
|
| 197 |
+
print_status "Opening interactive shell in container..."
|
| 198 |
+
|
| 199 |
+
cd "$PROJECT_ROOT"
|
| 200 |
+
|
| 201 |
+
if ! check_status; then
|
| 202 |
+
print_status "Container is not running. Starting it first..."
|
| 203 |
+
start_container
|
| 204 |
+
fi
|
| 205 |
+
|
| 206 |
+
print_status "Entering container shell..."
|
| 207 |
+
print_status "Type 'exit' to leave the container"
|
| 208 |
+
$DOCKER_COMPOSE exec nodule-segmentation bash
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
# Function to start Jupyter
|
| 212 |
+
start_jupyter() {
|
| 213 |
+
print_status "Starting Jupyter notebook service..."
|
| 214 |
+
|
| 215 |
+
cd "$PROJECT_ROOT"
|
| 216 |
+
|
| 217 |
+
if $DOCKER_COMPOSE --profile jupyter up -d; then
|
| 218 |
+
print_success "Jupyter started successfully!"
|
| 219 |
+
echo ""
|
| 220 |
+
echo "Access Jupyter at: http://localhost:8888"
|
| 221 |
+
echo "Token: medical_imaging_2024"
|
| 222 |
+
echo ""
|
| 223 |
+
echo "To stop Jupyter:"
|
| 224 |
+
echo " $DOCKER_COMPOSE --profile jupyter down"
|
| 225 |
+
else
|
| 226 |
+
print_error "Failed to start Jupyter"
|
| 227 |
+
exit 1
|
| 228 |
+
fi
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
# Function to show logs
|
| 232 |
+
show_logs() {
|
| 233 |
+
cd "$PROJECT_ROOT"
|
| 234 |
+
|
| 235 |
+
if [ "$1" = "-f" ] || [ "$1" = "--follow" ]; then
|
| 236 |
+
print_status "Following container logs (Ctrl+C to stop)..."
|
| 237 |
+
$DOCKER_COMPOSE logs -f nodule-segmentation
|
| 238 |
+
else
|
| 239 |
+
print_status "Showing container logs..."
|
| 240 |
+
$DOCKER_COMPOSE logs nodule-segmentation
|
| 241 |
+
fi
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
# Function to show status
|
| 245 |
+
show_status() {
|
| 246 |
+
print_status "Container status:"
|
| 247 |
+
|
| 248 |
+
cd "$PROJECT_ROOT"
|
| 249 |
+
|
| 250 |
+
echo ""
|
| 251 |
+
echo "=== Docker Compose Services ==="
|
| 252 |
+
$DOCKER_COMPOSE ps
|
| 253 |
+
|
| 254 |
+
echo ""
|
| 255 |
+
echo "=== Container Health ==="
|
| 256 |
+
if check_status; then
|
| 257 |
+
print_success "Container is running"
|
| 258 |
+
|
| 259 |
+
# Show resource usage
|
| 260 |
+
if command -v docker &> /dev/null; then
|
| 261 |
+
echo ""
|
| 262 |
+
echo "=== Resource Usage ==="
|
| 263 |
+
docker stats --no-stream --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}" | grep "$CONTAINER_NAME" || true
|
| 264 |
+
fi
|
| 265 |
+
|
| 266 |
+
# Test dependencies
|
| 267 |
+
echo ""
|
| 268 |
+
echo "=== Dependency Check ==="
|
| 269 |
+
if $DOCKER_COMPOSE exec nodule-segmentation python3 -c "
|
| 270 |
+
import SimpleITK
|
| 271 |
+
import radiomics
|
| 272 |
+
import sklearn
|
| 273 |
+
import skimage
|
| 274 |
+
import scipy
|
| 275 |
+
import pandas
|
| 276 |
+
import numpy
|
| 277 |
+
print('✓ All dependencies available')
|
| 278 |
+
" 2>/dev/null; then
|
| 279 |
+
print_success "All dependencies are working"
|
| 280 |
+
else
|
| 281 |
+
print_warning "Some dependencies may not be working"
|
| 282 |
+
fi
|
| 283 |
+
else
|
| 284 |
+
print_warning "Container is not running"
|
| 285 |
+
fi
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
# Function to clean up
|
| 289 |
+
cleanup() {
|
| 290 |
+
print_status "Cleaning up containers and volumes..."
|
| 291 |
+
|
| 292 |
+
cd "$PROJECT_ROOT"
|
| 293 |
+
|
| 294 |
+
# Stop all services
|
| 295 |
+
$DOCKER_COMPOSE down --remove-orphans
|
| 296 |
+
|
| 297 |
+
# Remove volumes (optional)
|
| 298 |
+
read -p "Remove data volumes? (y/N): " -n 1 -r
|
| 299 |
+
echo
|
| 300 |
+
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
| 301 |
+
$DOCKER_COMPOSE down -v
|
| 302 |
+
print_status "Volumes removed"
|
| 303 |
+
fi
|
| 304 |
+
|
| 305 |
+
print_success "Cleanup completed!"
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
# Function to run custom command
|
| 309 |
+
run_custom() {
|
| 310 |
+
local command="$1"
|
| 311 |
+
|
| 312 |
+
if [ -z "$command" ]; then
|
| 313 |
+
print_error "No command provided"
|
| 314 |
+
echo "Usage: $0 custom 'your command here'"
|
| 315 |
+
exit 1
|
| 316 |
+
fi
|
| 317 |
+
|
| 318 |
+
print_status "Running custom command: $command"
|
| 319 |
+
|
| 320 |
+
cd "$PROJECT_ROOT"
|
| 321 |
+
|
| 322 |
+
if ! check_status; then
|
| 323 |
+
print_status "Container is not running. Starting it first..."
|
| 324 |
+
start_container
|
| 325 |
+
fi
|
| 326 |
+
|
| 327 |
+
$DOCKER_COMPOSE exec nodule-segmentation bash -c "$command"
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
# Main function
|
| 331 |
+
main() {
|
| 332 |
+
# Change to project directory
|
| 333 |
+
cd "$PROJECT_ROOT"
|
| 334 |
+
|
| 335 |
+
# Check Docker Compose availability
|
| 336 |
+
check_docker_compose
|
| 337 |
+
|
| 338 |
+
local command="$1"
|
| 339 |
+
shift || true
|
| 340 |
+
|
| 341 |
+
case "$command" in
|
| 342 |
+
"start")
|
| 343 |
+
start_container
|
| 344 |
+
;;
|
| 345 |
+
"stop")
|
| 346 |
+
stop_container
|
| 347 |
+
;;
|
| 348 |
+
"restart")
|
| 349 |
+
restart_container
|
| 350 |
+
;;
|
| 351 |
+
"run")
|
| 352 |
+
run_pipeline
|
| 353 |
+
;;
|
| 354 |
+
"shell")
|
| 355 |
+
open_shell
|
| 356 |
+
;;
|
| 357 |
+
"jupyter")
|
| 358 |
+
start_jupyter
|
| 359 |
+
;;
|
| 360 |
+
"logs")
|
| 361 |
+
show_logs "$@"
|
| 362 |
+
;;
|
| 363 |
+
"status")
|
| 364 |
+
show_status
|
| 365 |
+
;;
|
| 366 |
+
"clean")
|
| 367 |
+
cleanup
|
| 368 |
+
;;
|
| 369 |
+
"custom")
|
| 370 |
+
run_custom "$1"
|
| 371 |
+
;;
|
| 372 |
+
"-h"|"--help"|"help"|"")
|
| 373 |
+
show_help
|
| 374 |
+
;;
|
| 375 |
+
*)
|
| 376 |
+
print_error "Unknown command: $command"
|
| 377 |
+
echo ""
|
| 378 |
+
show_help
|
| 379 |
+
exit 1
|
| 380 |
+
;;
|
| 381 |
+
esac
|
| 382 |
+
}
|
| 383 |
+
|
| 384 |
+
# Run main function with all arguments
|
| 385 |
+
main "$@"
|