neuroslice 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- neuroslice-0.0.1/PKG-INFO +165 -0
- neuroslice-0.0.1/README.md +150 -0
- neuroslice-0.0.1/neuroslice/__init__.py +10 -0
- neuroslice-0.0.1/neuroslice/cli.py +68 -0
- neuroslice-0.0.1/neuroslice/core.py +191 -0
- neuroslice-0.0.1/neuroslice/utils.py +30 -0
- neuroslice-0.0.1/neuroslice.egg-info/PKG-INFO +165 -0
- neuroslice-0.0.1/neuroslice.egg-info/SOURCES.txt +12 -0
- neuroslice-0.0.1/neuroslice.egg-info/dependency_links.txt +1 -0
- neuroslice-0.0.1/neuroslice.egg-info/entry_points.txt +2 -0
- neuroslice-0.0.1/neuroslice.egg-info/requires.txt +7 -0
- neuroslice-0.0.1/neuroslice.egg-info/top_level.txt +1 -0
- neuroslice-0.0.1/pyproject.toml +30 -0
- neuroslice-0.0.1/setup.cfg +4 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: neuroslice
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Slice-wise brain tumor detection and 3D mask reconstruction from MRI
|
|
5
|
+
Author-email: Alice Matoso <alice@example.com>, Bob Silva <bob@example.com>
|
|
6
|
+
Requires-Python: >=3.8
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: torch
|
|
9
|
+
Requires-Dist: nibabel
|
|
10
|
+
Requires-Dist: numpy
|
|
11
|
+
Requires-Dist: ultralytics
|
|
12
|
+
Requires-Dist: tqdm
|
|
13
|
+
Requires-Dist: opencv-python
|
|
14
|
+
Requires-Dist: huggingface-hub
|
|
15
|
+
|
|
16
|
+
# Neuroslice
|
|
17
|
+
|
|
18
|
+
A Python package for brain tumor segmentation using YOLO models on MRI FLAIR data.
|
|
19
|
+
|
|
20
|
+
## Table of Contents
|
|
21
|
+
|
|
22
|
+
- [Description](#description)
|
|
23
|
+
- [Installation](#installation)
|
|
24
|
+
- [Usage](#usage)
|
|
25
|
+
- [Command Line Interface](#command-line-interface)
|
|
26
|
+
- [Python API](#python-api)
|
|
27
|
+
- [Contributing](#contributing)
|
|
28
|
+
- [Citation](#citation)
|
|
29
|
+
|
|
30
|
+
## Description
|
|
31
|
+
|
|
32
|
+
Neuroslice provides automated brain tumor bounding box detection using pre-trained YOLO models. It uses FLAIR images to segment slice wise the image, though an option to create a cuboid is available.
|
|
33
|
+
The package supports three slice orientations (coronal, sagittal, and axial).
|
|
34
|
+
Models are automatically downloaded from [Hugging Face](https://huggingface.co/anamatoso/neuroslice) when first used.
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
You can install Neuroslice using pip:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install neuroslice
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
For development installation:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
git clone https://github.com/anamatoso/neuroslice.git
|
|
48
|
+
cd neuroslice
|
|
49
|
+
pip install -e .
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Usage
|
|
53
|
+
|
|
54
|
+
### Command Line Interface
|
|
55
|
+
|
|
56
|
+
Basic usage with default settings (coronal direction, union mode):
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
neuroslice input.nii.gz output_mask.nii.gz
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Specify slice direction and processing mode:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
neuroslice input.nii.gz output_mask.nii.gz --direction 2 --mode cuboid --verbose
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Arguments:**
|
|
69
|
+
|
|
70
|
+
Mandatory:
|
|
71
|
+
|
|
72
|
+
- `input`: Path to input NIfTI file (.nii or .nii.gz)
|
|
73
|
+
- `output`: Path to output mask NIfTI file
|
|
74
|
+
|
|
75
|
+
Optional:
|
|
76
|
+
|
|
77
|
+
- `--direction`: Slice direction - 0 (sagittal), 1 (coronal, default), 2 (axial)
|
|
78
|
+
- `--mode`: Processing mode - `union` (default) or `cuboid` (bounding box)
|
|
79
|
+
- `--verbose`: Print details
|
|
80
|
+
|
|
81
|
+
### Python API
|
|
82
|
+
|
|
83
|
+
**Generate a tumor mask:**
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from neuroslice import predict_mask
|
|
87
|
+
import nibabel as nib
|
|
88
|
+
|
|
89
|
+
# Generate mask from NIfTI file
|
|
90
|
+
mask = predict_mask("input.nii.gz", 1, verbose=True)
|
|
91
|
+
|
|
92
|
+
# Save the mask
|
|
93
|
+
nifti = nib.load("input.nii.gz")
|
|
94
|
+
output = nib.Nifti1Image(mask.astype("uint8"), nifti.affine, nifti.header)
|
|
95
|
+
nib.save(output, "output_mask.nii.gz")
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**Convert mask to bounding cuboid:**
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from neuroslice import mask2cuboid_array, mask2cuboid_nifti
|
|
102
|
+
|
|
103
|
+
# From array
|
|
104
|
+
cuboid_mask = mask2cuboid_array(mask)
|
|
105
|
+
|
|
106
|
+
# From NIfTI file
|
|
107
|
+
mask2cuboid_nifti("mask.nii.gz", "cuboid_mask.nii.gz")
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**Combine multiple masks:**
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
from neuroslice import unite_masks_array, unite_masks_nifti
|
|
114
|
+
|
|
115
|
+
# Combine arrays
|
|
116
|
+
combined = unite_masks_array(mask1, mask2, mask3, method="union")
|
|
117
|
+
|
|
118
|
+
# Combine NIfTI files
|
|
119
|
+
unite_masks_nifti(
|
|
120
|
+
["mask1.nii.gz", "mask2.nii.gz", "mask3.nii.gz"],
|
|
121
|
+
"combined_mask.nii.gz",
|
|
122
|
+
method="cuboid"
|
|
123
|
+
)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
**Advanced usage with direct predict function:**
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from neuroslice import predict
|
|
130
|
+
from ultralytics import YOLO
|
|
131
|
+
import nibabel as nib
|
|
132
|
+
|
|
133
|
+
# Load your data
|
|
134
|
+
nifti = nib.load("input.nii.gz")
|
|
135
|
+
data = nifti.get_fdata()
|
|
136
|
+
|
|
137
|
+
# Generate mask with custom axis
|
|
138
|
+
mask = predict(data, axis=0, verbose=True)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Contributing
|
|
142
|
+
|
|
143
|
+
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
|
|
144
|
+
|
|
145
|
+
1. Fork the repository
|
|
146
|
+
2. Create your branch (`git checkout -b feature/AmazingFeature`)
|
|
147
|
+
3. Add your changes to the branch
|
|
148
|
+
4. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
|
|
149
|
+
5. Push to the branch (`git push origin feature/AmazingFeature`)
|
|
150
|
+
6. Open a Pull Request
|
|
151
|
+
|
|
152
|
+
## Citation
|
|
153
|
+
|
|
154
|
+
If you use Neuroslice in your research, please cite:
|
|
155
|
+
|
|
156
|
+
TBD
|
|
157
|
+
|
|
158
|
+
<!-- ```bibtex
|
|
159
|
+
@software{neuroslice,
|
|
160
|
+
title = {Neuroslice: Brain Tumor Segmentation using YOLO},
|
|
161
|
+
author = {Your Name},
|
|
162
|
+
year = {2024},
|
|
163
|
+
url = {https://github.com/yourusername/neuroslice}
|
|
164
|
+
}
|
|
165
|
+
``` -->
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# Neuroslice
|
|
2
|
+
|
|
3
|
+
A Python package for brain tumor segmentation using YOLO models on MRI FLAIR data.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Description](#description)
|
|
8
|
+
- [Installation](#installation)
|
|
9
|
+
- [Usage](#usage)
|
|
10
|
+
- [Command Line Interface](#command-line-interface)
|
|
11
|
+
- [Python API](#python-api)
|
|
12
|
+
- [Contributing](#contributing)
|
|
13
|
+
- [Citation](#citation)
|
|
14
|
+
|
|
15
|
+
## Description
|
|
16
|
+
|
|
17
|
+
Neuroslice provides automated brain tumor bounding box detection using pre-trained YOLO models. It uses FLAIR images to segment slice wise the image, though an option to create a cuboid is available.
|
|
18
|
+
The package supports three slice orientations (coronal, sagittal, and axial).
|
|
19
|
+
Models are automatically downloaded from [Hugging Face](https://huggingface.co/anamatoso/neuroslice) when first used.
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
You can install Neuroslice using pip:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install neuroslice
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
For development installation:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
git clone https://github.com/anamatoso/neuroslice.git
|
|
33
|
+
cd neuroslice
|
|
34
|
+
pip install -e .
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Usage
|
|
38
|
+
|
|
39
|
+
### Command Line Interface
|
|
40
|
+
|
|
41
|
+
Basic usage with default settings (coronal direction, union mode):
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
neuroslice input.nii.gz output_mask.nii.gz
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Specify slice direction and processing mode:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
neuroslice input.nii.gz output_mask.nii.gz --direction 2 --mode cuboid --verbose
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Arguments:**
|
|
54
|
+
|
|
55
|
+
Mandatory:
|
|
56
|
+
|
|
57
|
+
- `input`: Path to input NIfTI file (.nii or .nii.gz)
|
|
58
|
+
- `output`: Path to output mask NIfTI file
|
|
59
|
+
|
|
60
|
+
Optional:
|
|
61
|
+
|
|
62
|
+
- `--direction`: Slice direction - 0 (sagittal), 1 (coronal, default), 2 (axial)
|
|
63
|
+
- `--mode`: Processing mode - `union` (default) or `cuboid` (bounding box)
|
|
64
|
+
- `--verbose`: Print details
|
|
65
|
+
|
|
66
|
+
### Python API
|
|
67
|
+
|
|
68
|
+
**Generate a tumor mask:**
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from neuroslice import predict_mask
|
|
72
|
+
import nibabel as nib
|
|
73
|
+
|
|
74
|
+
# Generate mask from NIfTI file
|
|
75
|
+
mask = predict_mask("input.nii.gz", 1, verbose=True)
|
|
76
|
+
|
|
77
|
+
# Save the mask
|
|
78
|
+
nifti = nib.load("input.nii.gz")
|
|
79
|
+
output = nib.Nifti1Image(mask.astype("uint8"), nifti.affine, nifti.header)
|
|
80
|
+
nib.save(output, "output_mask.nii.gz")
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**Convert mask to bounding cuboid:**
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from neuroslice import mask2cuboid_array, mask2cuboid_nifti
|
|
87
|
+
|
|
88
|
+
# From array
|
|
89
|
+
cuboid_mask = mask2cuboid_array(mask)
|
|
90
|
+
|
|
91
|
+
# From NIfTI file
|
|
92
|
+
mask2cuboid_nifti("mask.nii.gz", "cuboid_mask.nii.gz")
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Combine multiple masks:**
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from neuroslice import unite_masks_array, unite_masks_nifti
|
|
99
|
+
|
|
100
|
+
# Combine arrays
|
|
101
|
+
combined = unite_masks_array(mask1, mask2, mask3, method="union")
|
|
102
|
+
|
|
103
|
+
# Combine NIfTI files
|
|
104
|
+
unite_masks_nifti(
|
|
105
|
+
["mask1.nii.gz", "mask2.nii.gz", "mask3.nii.gz"],
|
|
106
|
+
"combined_mask.nii.gz",
|
|
107
|
+
method="cuboid"
|
|
108
|
+
)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**Advanced usage with direct predict function:**
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from neuroslice import predict
|
|
115
|
+
from ultralytics import YOLO
|
|
116
|
+
import nibabel as nib
|
|
117
|
+
|
|
118
|
+
# Load your data
|
|
119
|
+
nifti = nib.load("input.nii.gz")
|
|
120
|
+
data = nifti.get_fdata()
|
|
121
|
+
|
|
122
|
+
# Generate mask with custom axis
|
|
123
|
+
mask = predict(data, axis=0, verbose=True)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Contributing
|
|
127
|
+
|
|
128
|
+
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
|
|
129
|
+
|
|
130
|
+
1. Fork the repository
|
|
131
|
+
2. Create your branch (`git checkout -b feature/AmazingFeature`)
|
|
132
|
+
3. Add your changes to the branch
|
|
133
|
+
4. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
|
|
134
|
+
5. Push to the branch (`git push origin feature/AmazingFeature`)
|
|
135
|
+
6. Open a Pull Request
|
|
136
|
+
|
|
137
|
+
## Citation
|
|
138
|
+
|
|
139
|
+
If you use Neuroslice in your research, please cite:
|
|
140
|
+
|
|
141
|
+
TBD
|
|
142
|
+
|
|
143
|
+
<!-- ```bibtex
|
|
144
|
+
@software{neuroslice,
|
|
145
|
+
title = {Neuroslice: Brain Tumor Segmentation using YOLO},
|
|
146
|
+
author = {Your Name},
|
|
147
|
+
year = {2024},
|
|
148
|
+
url = {https://github.com/yourusername/neuroslice}
|
|
149
|
+
}
|
|
150
|
+
``` -->
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import nibabel as nib
|
|
4
|
+
from .core import predict_mask, mask2cuboid_array
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def main():
|
|
8
|
+
parser = argparse.ArgumentParser(
|
|
9
|
+
description="Neuroslice: Brain tumor segmentation using YOLO"
|
|
10
|
+
)
|
|
11
|
+
parser.add_argument(
|
|
12
|
+
"input",
|
|
13
|
+
type=str,
|
|
14
|
+
help="Path to input NIfTI file (.nii or .nii.gz)"
|
|
15
|
+
)
|
|
16
|
+
parser.add_argument(
|
|
17
|
+
"output",
|
|
18
|
+
type=str,
|
|
19
|
+
help="Path to output mask NIfTI file"
|
|
20
|
+
)
|
|
21
|
+
parser.add_argument(
|
|
22
|
+
"--mode",
|
|
23
|
+
type=str,
|
|
24
|
+
default="union",
|
|
25
|
+
choices=["union", "cuboid"],
|
|
26
|
+
help="Processing mode: union or cuboid (bounding box)"
|
|
27
|
+
)
|
|
28
|
+
parser.add_argument(
|
|
29
|
+
"--axis",
|
|
30
|
+
type=int,
|
|
31
|
+
default=1,
|
|
32
|
+
choices=[0, 1, 2],
|
|
33
|
+
help="Slice direction for single-direction mode (default: 0 = coronal)"
|
|
34
|
+
)
|
|
35
|
+
parser.add_argument(
|
|
36
|
+
"--verbose",
|
|
37
|
+
action="store_true",
|
|
38
|
+
help="Print detailed statistics"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
args = parser.parse_args()
|
|
42
|
+
|
|
43
|
+
# Validate input file exists
|
|
44
|
+
if not Path(args.input).exists():
|
|
45
|
+
print(f"Error: Input file '{args.input}' not found.")
|
|
46
|
+
return 1
|
|
47
|
+
|
|
48
|
+
# Load NIfTI for later saving
|
|
49
|
+
nifti = nib.load(args.input)
|
|
50
|
+
|
|
51
|
+
mask = predict_mask(args.input, args.axis, verbose=args.verbose)
|
|
52
|
+
|
|
53
|
+
if args.mode == "cuboid":
|
|
54
|
+
final_mask = mask2cuboid_array(mask)
|
|
55
|
+
else:
|
|
56
|
+
final_mask = mask
|
|
57
|
+
|
|
58
|
+
# Save output
|
|
59
|
+
output_nifti = nib.Nifti1Image(final_mask.astype("uint8"), nifti.affine, nifti.header)
|
|
60
|
+
nib.save(output_nifti, args.output)
|
|
61
|
+
if args.verbose:
|
|
62
|
+
print(f"Mask saved to: {args.output}")
|
|
63
|
+
|
|
64
|
+
return 0
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
if __name__ == "__main__":
|
|
68
|
+
exit(main())
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import cv2
|
|
2
|
+
import nibabel as nib
|
|
3
|
+
import numpy as np
|
|
4
|
+
from tqdm import tqdm
|
|
5
|
+
from ultralytics import YOLO
|
|
6
|
+
|
|
7
|
+
from .utils import download_model
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def predict(data, axis, verbose):
|
|
11
|
+
"""
|
|
12
|
+
Generate a binary tumor mask from a 3D image array using a trained YOLO model.
|
|
13
|
+
Args:
|
|
14
|
+
data (np.ndarray): 3D image array (e.g., FLAIR image).
|
|
15
|
+
axis (int): Axis along which to slice the 3D image (0: sagittal, 1: coronal, 2: axial).
|
|
16
|
+
model (YOLO): Loaded YOLO model.
|
|
17
|
+
verbose (bool): Whether to print statistics.
|
|
18
|
+
Returns:
|
|
19
|
+
np.ndarray: Binary mask of detected tumor regions.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
assert axis in [0, 1, 2], "Axis must be 0 (sagittal), 1 (coronal), or 2 (axial)."
|
|
23
|
+
|
|
24
|
+
# Load the YOLO model
|
|
25
|
+
model_path = download_model(axis, verbose)
|
|
26
|
+
model = YOLO(model_path)
|
|
27
|
+
|
|
28
|
+
# Initialize binary mask with same shape as input
|
|
29
|
+
binary_mask = np.zeros_like(data, dtype=np.uint8)
|
|
30
|
+
n_slices = data.shape[axis] # Number of coronal slices
|
|
31
|
+
|
|
32
|
+
# Process each coronal slice
|
|
33
|
+
for i in tqdm(range(n_slices), desc="Detecting tumors", disable=not verbose):
|
|
34
|
+
# Extract slice
|
|
35
|
+
if axis == 0:
|
|
36
|
+
slice_data = data[i, :, :]
|
|
37
|
+
elif axis == 1:
|
|
38
|
+
slice_data = data[:, i, :]
|
|
39
|
+
else:
|
|
40
|
+
slice_data = data[:, :, i]
|
|
41
|
+
|
|
42
|
+
# Normalize slice to 0-255 for YOLO input
|
|
43
|
+
slice_min, slice_max = np.min(slice_data), np.max(slice_data)
|
|
44
|
+
|
|
45
|
+
if slice_max - slice_min == 0:
|
|
46
|
+
continue # Skip empty slices
|
|
47
|
+
|
|
48
|
+
normalized_slice = ((slice_data - slice_min) / (slice_max - slice_min) * 255).astype(np.uint8)
|
|
49
|
+
|
|
50
|
+
# Convert to BGR format for YOLO (expects 3-channel image)
|
|
51
|
+
slice_bgr = cv2.cvtColor(normalized_slice, cv2.COLOR_GRAY2RGB) # pylint: disable=no-member
|
|
52
|
+
|
|
53
|
+
# Run YOLO prediction
|
|
54
|
+
results = model.predict(slice_bgr, verbose=False)
|
|
55
|
+
if len(results) == 0 or len(results[0].boxes) == 0:
|
|
56
|
+
continue
|
|
57
|
+
|
|
58
|
+
# Process detections
|
|
59
|
+
h, w = slice_data.shape
|
|
60
|
+
for box in results[0].boxes:
|
|
61
|
+
# Get bounding box in YOLO format (normalized)
|
|
62
|
+
x_center, y_center, width, height = box.xywhn[0].tolist()
|
|
63
|
+
|
|
64
|
+
# Convert to pixel coordinates
|
|
65
|
+
x1 = int((x_center - width / 2) * w)
|
|
66
|
+
y1 = int((y_center - height / 2) * h)
|
|
67
|
+
x2 = int((x_center + width / 2) * w)
|
|
68
|
+
y2 = int((y_center + height / 2) * h)
|
|
69
|
+
|
|
70
|
+
x1, y1 = max(0, x1), max(0, y1)
|
|
71
|
+
x2, y2 = min(w, x2), min(h, y2)
|
|
72
|
+
|
|
73
|
+
# Directly fill per-box region
|
|
74
|
+
if axis == 0:
|
|
75
|
+
binary_mask[i, y1:y2, x1:x2] = 1
|
|
76
|
+
elif axis == 1:
|
|
77
|
+
binary_mask[y1:y2, i, x1:x2] = 1
|
|
78
|
+
else:
|
|
79
|
+
binary_mask[y1:y2, x1:x2, i] = 1
|
|
80
|
+
|
|
81
|
+
if verbose:
|
|
82
|
+
# Print statistics
|
|
83
|
+
tumor_voxels = np.sum(binary_mask)
|
|
84
|
+
total_voxels = binary_mask.size
|
|
85
|
+
tumor_percentage = (tumor_voxels / total_voxels) * 100
|
|
86
|
+
|
|
87
|
+
print("Segmentation complete!")
|
|
88
|
+
print(f" Total voxels: {total_voxels:,}")
|
|
89
|
+
print(f" Tumor voxels: {tumor_voxels:,}")
|
|
90
|
+
print(f" Tumor percentage: {tumor_percentage:.2f}%")
|
|
91
|
+
|
|
92
|
+
return binary_mask
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def predict_mask(nifti_path, axis, verbose=False):
|
|
96
|
+
"""
|
|
97
|
+
Generate a binary tumor mask from a 3D NIfTI image using a trained YOLO model.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
nifti_path (str): Path to the input NIfTI file (e.g., FLAIR image).
|
|
101
|
+
model_path (str): Path to the trained YOLO model (.pt file).
|
|
102
|
+
conf_threshold (float): Confidence threshold for detections (default: 0.25).
|
|
103
|
+
output_path (str, optional): Path to save the output mask. If None, saves next to input.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
str: Path to the saved binary mask NIfTI file.
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
assert axis in [0, 1, 2], "Axis must be 0 (sagittal), 1 (coronal), or 2 (axial)."
|
|
110
|
+
|
|
111
|
+
# Load the NIfTI image
|
|
112
|
+
nifti = nib.load(nifti_path)
|
|
113
|
+
data = nifti.get_fdata()
|
|
114
|
+
|
|
115
|
+
# Predict the binary mask
|
|
116
|
+
if verbose:
|
|
117
|
+
print("Generating tumor mask...")
|
|
118
|
+
binary_mask = predict(data, axis, verbose)
|
|
119
|
+
|
|
120
|
+
return binary_mask
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def mask2cuboid_array(mask):
|
|
124
|
+
""" Convert a binary mask to its bounding cuboid mask.
|
|
125
|
+
Args:
|
|
126
|
+
mask (np.ndarray): 3D binary mask.
|
|
127
|
+
Returns:
|
|
128
|
+
np.ndarray: 3D binary mask of the bounding cuboid.
|
|
129
|
+
"""
|
|
130
|
+
coords = np.argwhere(mask)
|
|
131
|
+
minz, miny, minx = coords.min(axis=0)
|
|
132
|
+
maxz, maxy, maxx = coords.max(axis=0)
|
|
133
|
+
new_mask = np.zeros_like(mask)
|
|
134
|
+
new_mask[minz:maxz + 1, miny:maxy + 1, minx:maxx + 1] = 1
|
|
135
|
+
return new_mask
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def mask2cuboid_nifti(mask_path, output_path):
|
|
139
|
+
""" Convert a binary mask NIfTI file to its bounding cuboid mask NIfTI file.
|
|
140
|
+
Args:
|
|
141
|
+
mask_path (str): Path to the input binary mask NIfTI file.
|
|
142
|
+
output_path (str): Path to save the output bounding cuboid mask NIfTI file.
|
|
143
|
+
Returns:
|
|
144
|
+
str: Path to the saved bounding cuboid mask NIfTI file.
|
|
145
|
+
"""
|
|
146
|
+
nifti = nib.load(mask_path)
|
|
147
|
+
mask = nifti.get_fdata()
|
|
148
|
+
cuboid_mask = mask2cuboid_array(mask)
|
|
149
|
+
output_nifti = nib.Nifti1Image(cuboid_mask.astype("uint8"), nifti.affine, nifti.header)
|
|
150
|
+
nib.save(output_nifti, output_path)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def unite_masks_array(*masks, method="union"):
|
|
154
|
+
"""
|
|
155
|
+
Combine multiple binary masks into one using union or cuboid.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
*masks (np.ndarray): Multiple binary masks to combine.
|
|
159
|
+
method (str): Method to combine masks ('union' or 'cuboid').
|
|
160
|
+
Returns:
|
|
161
|
+
np.ndarray: Combined binary mask.
|
|
162
|
+
"""
|
|
163
|
+
if method == "union":
|
|
164
|
+
return np.clip(sum(masks), 0, 1)
|
|
165
|
+
elif method == "cuboid":
|
|
166
|
+
combined_mask = np.clip(sum(masks), 0, 1)
|
|
167
|
+
return mask2cuboid_array(combined_mask)
|
|
168
|
+
else:
|
|
169
|
+
raise ValueError(f"Unknown combine method: {method}")
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def unite_masks_nifti(mask_paths, output_path, method="union"):
|
|
173
|
+
"""
|
|
174
|
+
Combine multiple binary mask NIfTI files into one using union or cuboid.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
mask_paths (list of str): Paths to the input binary mask NIfTI files.
|
|
178
|
+
output_path (str): Path to save the output combined mask NIfTI file.
|
|
179
|
+
method (str): Method to combine masks ('union' or 'cuboid').
|
|
180
|
+
Returns:
|
|
181
|
+
str: Path to the saved combined binary mask NIfTI file.
|
|
182
|
+
"""
|
|
183
|
+
masks = []
|
|
184
|
+
for path in mask_paths:
|
|
185
|
+
nifti = nib.load(path)
|
|
186
|
+
mask = nifti.get_fdata()
|
|
187
|
+
masks.append(mask)
|
|
188
|
+
|
|
189
|
+
combined_mask = unite_masks_array(*masks, method=method)
|
|
190
|
+
output_nifti = nib.Nifti1Image(combined_mask.astype("uint8"), nifti.affine, nifti.header)
|
|
191
|
+
nib.save(output_nifti, output_path)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from huggingface_hub import hf_hub_download
|
|
2
|
+
from huggingface_hub.utils import disable_progress_bars
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def download_model(axis, verbose):
|
|
6
|
+
""" Download the YOLO model for the specified direction from Hugging Face Hub.
|
|
7
|
+
Args:
|
|
8
|
+
direction (str): Direction of slices ('coronal', 'sagittal', 'axial').
|
|
9
|
+
verbose (bool): Whether to show download progress.
|
|
10
|
+
Returns:
|
|
11
|
+
str: Path to the downloaded model file.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
assert axis in [0, 1, 2], "Axis must be 0 (sagittal), 1 (coronal), or 2 (axial)."
|
|
15
|
+
if axis == 0:
|
|
16
|
+
direction = "sagittal"
|
|
17
|
+
elif axis == 1:
|
|
18
|
+
direction = "coronal"
|
|
19
|
+
elif axis == 2:
|
|
20
|
+
direction = "axial"
|
|
21
|
+
|
|
22
|
+
if not verbose:
|
|
23
|
+
disable_progress_bars()
|
|
24
|
+
if verbose:
|
|
25
|
+
print("Downloading model...")
|
|
26
|
+
model_path = hf_hub_download(
|
|
27
|
+
repo_id="anamatoso/neuroslice",
|
|
28
|
+
filename=f"models/{direction}_best.pt",
|
|
29
|
+
)
|
|
30
|
+
return model_path
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: neuroslice
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Slice-wise brain tumor detection and 3D mask reconstruction from MRI
|
|
5
|
+
Author-email: Alice Matoso <alice@example.com>, Bob Silva <bob@example.com>
|
|
6
|
+
Requires-Python: >=3.8
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: torch
|
|
9
|
+
Requires-Dist: nibabel
|
|
10
|
+
Requires-Dist: numpy
|
|
11
|
+
Requires-Dist: ultralytics
|
|
12
|
+
Requires-Dist: tqdm
|
|
13
|
+
Requires-Dist: opencv-python
|
|
14
|
+
Requires-Dist: huggingface-hub
|
|
15
|
+
|
|
16
|
+
# Neuroslice
|
|
17
|
+
|
|
18
|
+
A Python package for brain tumor segmentation using YOLO models on MRI FLAIR data.
|
|
19
|
+
|
|
20
|
+
## Table of Contents
|
|
21
|
+
|
|
22
|
+
- [Description](#description)
|
|
23
|
+
- [Installation](#installation)
|
|
24
|
+
- [Usage](#usage)
|
|
25
|
+
- [Command Line Interface](#command-line-interface)
|
|
26
|
+
- [Python API](#python-api)
|
|
27
|
+
- [Contributing](#contributing)
|
|
28
|
+
- [Citation](#citation)
|
|
29
|
+
|
|
30
|
+
## Description
|
|
31
|
+
|
|
32
|
+
Neuroslice provides automated brain tumor bounding box detection using pre-trained YOLO models. It uses FLAIR images to segment slice wise the image, though an option to create a cuboid is available.
|
|
33
|
+
The package supports three slice orientations (coronal, sagittal, and axial).
|
|
34
|
+
Models are automatically downloaded from [Hugging Face](https://huggingface.co/anamatoso/neuroslice) when first used.
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
You can install Neuroslice using pip:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install neuroslice
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
For development installation:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
git clone https://github.com/anamatoso/neuroslice.git
|
|
48
|
+
cd neuroslice
|
|
49
|
+
pip install -e .
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Usage
|
|
53
|
+
|
|
54
|
+
### Command Line Interface
|
|
55
|
+
|
|
56
|
+
Basic usage with default settings (coronal direction, union mode):
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
neuroslice input.nii.gz output_mask.nii.gz
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Specify slice direction and processing mode:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
neuroslice input.nii.gz output_mask.nii.gz --direction 2 --mode cuboid --verbose
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Arguments:**
|
|
69
|
+
|
|
70
|
+
Mandatory:
|
|
71
|
+
|
|
72
|
+
- `input`: Path to input NIfTI file (.nii or .nii.gz)
|
|
73
|
+
- `output`: Path to output mask NIfTI file
|
|
74
|
+
|
|
75
|
+
Optional:
|
|
76
|
+
|
|
77
|
+
- `--direction`: Slice direction - 0 (sagittal), 1 (coronal, default), 2 (axial)
|
|
78
|
+
- `--mode`: Processing mode - `union` (default) or `cuboid` (bounding box)
|
|
79
|
+
- `--verbose`: Print details
|
|
80
|
+
|
|
81
|
+
### Python API
|
|
82
|
+
|
|
83
|
+
**Generate a tumor mask:**
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from neuroslice import predict_mask
|
|
87
|
+
import nibabel as nib
|
|
88
|
+
|
|
89
|
+
# Generate mask from NIfTI file
|
|
90
|
+
mask = predict_mask("input.nii.gz", 1, verbose=True)
|
|
91
|
+
|
|
92
|
+
# Save the mask
|
|
93
|
+
nifti = nib.load("input.nii.gz")
|
|
94
|
+
output = nib.Nifti1Image(mask.astype("uint8"), nifti.affine, nifti.header)
|
|
95
|
+
nib.save(output, "output_mask.nii.gz")
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**Convert mask to bounding cuboid:**
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from neuroslice import mask2cuboid_array, mask2cuboid_nifti
|
|
102
|
+
|
|
103
|
+
# From array
|
|
104
|
+
cuboid_mask = mask2cuboid_array(mask)
|
|
105
|
+
|
|
106
|
+
# From NIfTI file
|
|
107
|
+
mask2cuboid_nifti("mask.nii.gz", "cuboid_mask.nii.gz")
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**Combine multiple masks:**
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
from neuroslice import unite_masks_array, unite_masks_nifti
|
|
114
|
+
|
|
115
|
+
# Combine arrays
|
|
116
|
+
combined = unite_masks_array(mask1, mask2, mask3, method="union")
|
|
117
|
+
|
|
118
|
+
# Combine NIfTI files
|
|
119
|
+
unite_masks_nifti(
|
|
120
|
+
["mask1.nii.gz", "mask2.nii.gz", "mask3.nii.gz"],
|
|
121
|
+
"combined_mask.nii.gz",
|
|
122
|
+
method="cuboid"
|
|
123
|
+
)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
**Advanced usage with direct predict function:**
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from neuroslice import predict
|
|
130
|
+
from ultralytics import YOLO
|
|
131
|
+
import nibabel as nib
|
|
132
|
+
|
|
133
|
+
# Load your data
|
|
134
|
+
nifti = nib.load("input.nii.gz")
|
|
135
|
+
data = nifti.get_fdata()
|
|
136
|
+
|
|
137
|
+
# Generate mask with custom axis
|
|
138
|
+
mask = predict(data, axis=0, verbose=True)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Contributing
|
|
142
|
+
|
|
143
|
+
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
|
|
144
|
+
|
|
145
|
+
1. Fork the repository
|
|
146
|
+
2. Create your branch (`git checkout -b feature/AmazingFeature`)
|
|
147
|
+
3. Add your changes to the branch
|
|
148
|
+
4. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
|
|
149
|
+
5. Push to the branch (`git push origin feature/AmazingFeature`)
|
|
150
|
+
6. Open a Pull Request
|
|
151
|
+
|
|
152
|
+
## Citation
|
|
153
|
+
|
|
154
|
+
If you use Neuroslice in your research, please cite:
|
|
155
|
+
|
|
156
|
+
TBD
|
|
157
|
+
|
|
158
|
+
<!-- ```bibtex
|
|
159
|
+
@software{neuroslice,
|
|
160
|
+
title = {Neuroslice: Brain Tumor Segmentation using YOLO},
|
|
161
|
+
author = {Your Name},
|
|
162
|
+
year = {2024},
|
|
163
|
+
url = {https://github.com/yourusername/neuroslice}
|
|
164
|
+
}
|
|
165
|
+
``` -->
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
neuroslice/__init__.py
|
|
4
|
+
neuroslice/cli.py
|
|
5
|
+
neuroslice/core.py
|
|
6
|
+
neuroslice/utils.py
|
|
7
|
+
neuroslice.egg-info/PKG-INFO
|
|
8
|
+
neuroslice.egg-info/SOURCES.txt
|
|
9
|
+
neuroslice.egg-info/dependency_links.txt
|
|
10
|
+
neuroslice.egg-info/entry_points.txt
|
|
11
|
+
neuroslice.egg-info/requires.txt
|
|
12
|
+
neuroslice.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
neuroslice
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "neuroslice"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
description = "Slice-wise brain tumor detection and 3D mask reconstruction from MRI"
|
|
5
|
+
authors = [
|
|
6
|
+
{ name = "Alice Matoso", email = "alice@example.com" },
|
|
7
|
+
{ name = "Bob Silva", email = "bob@example.com" },
|
|
8
|
+
]
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
|
|
12
|
+
dependencies = [
|
|
13
|
+
"torch",
|
|
14
|
+
"nibabel",
|
|
15
|
+
"numpy",
|
|
16
|
+
"ultralytics",
|
|
17
|
+
"tqdm",
|
|
18
|
+
"opencv-python",
|
|
19
|
+
"huggingface-hub"
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.scripts]
|
|
23
|
+
neuroslice = "neuroslice.cli:main"
|
|
24
|
+
|
|
25
|
+
[build-system]
|
|
26
|
+
requires = ["setuptools>=61.0"]
|
|
27
|
+
build-backend = "setuptools.build_meta"
|
|
28
|
+
|
|
29
|
+
[tool.setuptools]
|
|
30
|
+
packages = ["neuroslice"]
|