neuroslice 0.0.1__py3-none-any.whl

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/__init__.py ADDED
@@ -0,0 +1,10 @@
1
+ from .core import predict, predict_mask, mask2cuboid_array, mask2cuboid_nifti, unite_masks_array, unite_masks_nifti
2
+
3
+ __all__ = [
4
+ "predict",
5
+ "predict_mask",
6
+ "mask2cuboid_array",
7
+ "mask2cuboid_nifti",
8
+ "unite_masks_array",
9
+ "unite_masks_nifti",
10
+ ]
neuroslice/cli.py ADDED
@@ -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())
neuroslice/core.py ADDED
@@ -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)
neuroslice/utils.py ADDED
@@ -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,9 @@
1
+ neuroslice/__init__.py,sha256=cSNDBiF6kgUiOZhVSinoMOFgHhQlIMxV0OPQY-88vMA,266
2
+ neuroslice/cli.py,sha256=Bwt1GUtgtP_VML5QoDMuyKTExNhmhJY3QT-xQZ4GOss,1696
3
+ neuroslice/core.py,sha256=NYMt3lobbYWihQAptpIn3-UHf01wxlVs7m0pGNtjpLU,6542
4
+ neuroslice/utils.py,sha256=unpNY-NbvXHZeW-OW_4WPIOmk1dHjCDxxE7EnMaZZ0I,934
5
+ neuroslice-0.0.1.dist-info/METADATA,sha256=dlco1Y5KW9qfeWzFy7mqoHHfli5wP-dwsPK1WqmsMuw,4013
6
+ neuroslice-0.0.1.dist-info/WHEEL,sha256=iAkIy5fosb7FzIOwONchHf19Qu7_1wCWyFNR5gu9nU0,91
7
+ neuroslice-0.0.1.dist-info/entry_points.txt,sha256=1DBEBQCLeAluB89ipJVF4k4-IzYxINndMI0U9ZgaG0I,51
8
+ neuroslice-0.0.1.dist-info/top_level.txt,sha256=cuV5qXnoXz7p9NOz70Ty7aQ-G87XIFa0U0hazgn_WbU,11
9
+ neuroslice-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (75.3.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ neuroslice = neuroslice.cli:main
@@ -0,0 +1 @@
1
+ neuroslice