brain-stl 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.
- brain_stl-0.0.1/LICENSE +21 -0
- brain_stl-0.0.1/PKG-INFO +95 -0
- brain_stl-0.0.1/README.md +67 -0
- brain_stl-0.0.1/brain_stl/__init__.py +1 -0
- brain_stl-0.0.1/brain_stl/cli.py +47 -0
- brain_stl-0.0.1/brain_stl/data/ref.png +0 -0
- brain_stl-0.0.1/brain_stl/main.py +136 -0
- brain_stl-0.0.1/pyproject.toml +29 -0
brain_stl-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Lukas Fisch
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
brain_stl-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: brain-stl
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Turn your brain MRI into a printable 3D model in seconds 🧠➡️🖨️
|
|
5
|
+
Home-page: https://github.com/codingfisch/brain_stl
|
|
6
|
+
License: MIT
|
|
7
|
+
Author: codingfisch
|
|
8
|
+
Author-email: l_fisc17@wwu.de
|
|
9
|
+
Requires-Python: >=3.9,<4.0
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Requires-Dist: PyMCubes
|
|
20
|
+
Requires-Dist: dcm2niix
|
|
21
|
+
Requires-Dist: deepmriprep
|
|
22
|
+
Requires-Dist: fast_simplification (==0.1.7)
|
|
23
|
+
Requires-Dist: niftiview
|
|
24
|
+
Requires-Dist: trimesh
|
|
25
|
+
Project-URL: Repository, https://github.com/codingfisch/brain_stl
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# brain-stl 🧠
|
|
29
|
+
The **easiest** and **fastest** way to turn brain MRI scans into **3D printable models** 🖨️ powered by [`deepmriprep`](https://github.com/wwu-mmll/deepmriprep) ✨
|
|
30
|
+
|
|
31
|
+
[Use the Colab demo](https://colab.research.google.com/drive/10WSG_qmKnbYrFTwZOlXsIgbda8iz2duQ?usp=sharing) or **install** it locally via `pip install brain_stl` 💨
|
|
32
|
+
|
|
33
|
+
By default, the model is saved as `brain.stl`, but you can customize the filename along with other options like
|
|
34
|
+
- **Threshold**: Pick a value between 0 and 3 for different tissue types (see GIF left, `2.5`: white matter only) 🧠
|
|
35
|
+
- **Hollowness**: Adjust how hollow the brain is (enables faster printing) 💨
|
|
36
|
+
- **Text**: Add personalized text that works as a support structure 🔤
|
|
37
|
+
- **Smoothing**: Achieve a polished surface via iterative Laplacian smoothing ✨
|
|
38
|
+
- **Mesh Reduction**: Simplify the mesh to reduce the size of `brain.stl` ✂️
|
|
39
|
+
|
|
40
|
+

|
|
41
|
+
|
|
42
|
+
## Usage 💻
|
|
43
|
+
To get your first `brain.stl` just run
|
|
44
|
+
```python
|
|
45
|
+
from brain_stl import run_brain_stl
|
|
46
|
+
|
|
47
|
+
run_brain_stl() # per default output_folder='.' (current folder)
|
|
48
|
+
```
|
|
49
|
+
and a publicly available brain MRI scan (from [OpenNeuro](https://openneuro.org/)) will be used 🌐
|
|
50
|
+
|
|
51
|
+
Scan files can be inputted in NifTi (`.nii` or `.nii.gz`) or DICOM (`.dcm` or [folder](https://pydicom.github.io/pydicom/stable/tutorials/filesets.html)) format
|
|
52
|
+
```python
|
|
53
|
+
from brain_stl import run_brain_stl
|
|
54
|
+
|
|
55
|
+
run_brain_stl('/path/to/scan.nii.gz') # or '/path/to/dicom_folder'
|
|
56
|
+
```
|
|
57
|
+
The main arguments of `run_brain_stl` are
|
|
58
|
+
- **`scan_filepath`**: Path to the input scan (if `None`, use scan from `url`). Default: `None`
|
|
59
|
+
- **`output_folder`**: Folder to save the `.stl` file and other outputs. Default: `'.'`
|
|
60
|
+
- **`stl_name`**: Name of the example `.stl` file (without extension). Default: `'brain'`
|
|
61
|
+
|
|
62
|
+
<details>
|
|
63
|
+
<summary><b>Click here</b>, to see <b>all</b> remaining <b>arguments</b> 📑</summary>
|
|
64
|
+
|
|
65
|
+
- **`nifti_name`**: If DICOM produces multiple NifTis, this NifTi filename is used to proceed. Default: `None`
|
|
66
|
+
- **`url`**: URL to download a brain scan if no `scan_filepath` is provided. Default: `OPENNEURO_URL`
|
|
67
|
+
- **`use_cache`**: If `True`, reuses intermediate outputs (e.g., tissue data). Default: `False`
|
|
68
|
+
- **`threshold`**: Threshold tissue value (`0.5`: fluid, `1.5`: gray matter, `2.5`: white matter). Default: `1.5`
|
|
69
|
+
- **`hollow`**: How hollow the brain is (0 to 1). Default: `0`
|
|
70
|
+
- **`in_template_space`**: If `True`, the 3D model is in standardized (template) orientation. Default: `False`
|
|
71
|
+
- **`mesh_reduction`**: Fraction of mesh reduction (0 to 1) for smaller `.stl` file size. Default: `0` (no reduction)
|
|
72
|
+
- **`smooth_iter`**: Number of iterations for Laplacian smoothing. Default: `0` (none)
|
|
73
|
+
- **`smooth_lamb`**: Strength of each Laplacian smoothing step. Default: `0.2`
|
|
74
|
+
- **`text`**: Custom text used as structural support. Default: `None`
|
|
75
|
+
- **`text2`**: Second line of text. Default: `None`
|
|
76
|
+
- **`textsize`**: Size (height) of the text. Default: `120`
|
|
77
|
+
- **`textback`**: Coronal (depth) position of the back of the text. Default: `300`
|
|
78
|
+
- **`textfront`**: Coronal (depth) position of the front of the text. Default: `330`
|
|
79
|
+
- **`textlinelength`**: Length of line below the text. Default: `10`
|
|
80
|
+
|
|
81
|
+
</details>
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
## Command-Line Interface 🖥️
|
|
85
|
+
Process your first brain scan from the terminal by just running
|
|
86
|
+
```bash
|
|
87
|
+
brain-stl
|
|
88
|
+
```
|
|
89
|
+
Without providing any arguments, it downloads a publicly available scan and creates a `brain.stl` of it 🧠
|
|
90
|
+
|
|
91
|
+
Run `brain-stl --help` (+copy&paste into ChatGPT) to understand the usage of custom settings 💡
|
|
92
|
+
|
|
93
|
+
## Share Your Brains 🧠🌐
|
|
94
|
+
Use `create_gif.py` to get a GIF of your model and share it on social media with the hashtag `#brain-stl` 🤗
|
|
95
|
+
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# brain-stl 🧠
|
|
2
|
+
The **easiest** and **fastest** way to turn brain MRI scans into **3D printable models** 🖨️ powered by [`deepmriprep`](https://github.com/wwu-mmll/deepmriprep) ✨
|
|
3
|
+
|
|
4
|
+
[Use the Colab demo](https://colab.research.google.com/drive/10WSG_qmKnbYrFTwZOlXsIgbda8iz2duQ?usp=sharing) or **install** it locally via `pip install brain_stl` 💨
|
|
5
|
+
|
|
6
|
+
By default, the model is saved as `brain.stl`, but you can customize the filename along with other options like
|
|
7
|
+
- **Threshold**: Pick a value between 0 and 3 for different tissue types (see GIF left, `2.5`: white matter only) 🧠
|
|
8
|
+
- **Hollowness**: Adjust how hollow the brain is (enables faster printing) 💨
|
|
9
|
+
- **Text**: Add personalized text that works as a support structure 🔤
|
|
10
|
+
- **Smoothing**: Achieve a polished surface via iterative Laplacian smoothing ✨
|
|
11
|
+
- **Mesh Reduction**: Simplify the mesh to reduce the size of `brain.stl` ✂️
|
|
12
|
+
|
|
13
|
+

|
|
14
|
+
|
|
15
|
+
## Usage 💻
|
|
16
|
+
To get your first `brain.stl` just run
|
|
17
|
+
```python
|
|
18
|
+
from brain_stl import run_brain_stl
|
|
19
|
+
|
|
20
|
+
run_brain_stl() # per default output_folder='.' (current folder)
|
|
21
|
+
```
|
|
22
|
+
and a publicly available brain MRI scan (from [OpenNeuro](https://openneuro.org/)) will be used 🌐
|
|
23
|
+
|
|
24
|
+
Scan files can be inputted in NifTi (`.nii` or `.nii.gz`) or DICOM (`.dcm` or [folder](https://pydicom.github.io/pydicom/stable/tutorials/filesets.html)) format
|
|
25
|
+
```python
|
|
26
|
+
from brain_stl import run_brain_stl
|
|
27
|
+
|
|
28
|
+
run_brain_stl('/path/to/scan.nii.gz') # or '/path/to/dicom_folder'
|
|
29
|
+
```
|
|
30
|
+
The main arguments of `run_brain_stl` are
|
|
31
|
+
- **`scan_filepath`**: Path to the input scan (if `None`, use scan from `url`). Default: `None`
|
|
32
|
+
- **`output_folder`**: Folder to save the `.stl` file and other outputs. Default: `'.'`
|
|
33
|
+
- **`stl_name`**: Name of the example `.stl` file (without extension). Default: `'brain'`
|
|
34
|
+
|
|
35
|
+
<details>
|
|
36
|
+
<summary><b>Click here</b>, to see <b>all</b> remaining <b>arguments</b> 📑</summary>
|
|
37
|
+
|
|
38
|
+
- **`nifti_name`**: If DICOM produces multiple NifTis, this NifTi filename is used to proceed. Default: `None`
|
|
39
|
+
- **`url`**: URL to download a brain scan if no `scan_filepath` is provided. Default: `OPENNEURO_URL`
|
|
40
|
+
- **`use_cache`**: If `True`, reuses intermediate outputs (e.g., tissue data). Default: `False`
|
|
41
|
+
- **`threshold`**: Threshold tissue value (`0.5`: fluid, `1.5`: gray matter, `2.5`: white matter). Default: `1.5`
|
|
42
|
+
- **`hollow`**: How hollow the brain is (0 to 1). Default: `0`
|
|
43
|
+
- **`in_template_space`**: If `True`, the 3D model is in standardized (template) orientation. Default: `False`
|
|
44
|
+
- **`mesh_reduction`**: Fraction of mesh reduction (0 to 1) for smaller `.stl` file size. Default: `0` (no reduction)
|
|
45
|
+
- **`smooth_iter`**: Number of iterations for Laplacian smoothing. Default: `0` (none)
|
|
46
|
+
- **`smooth_lamb`**: Strength of each Laplacian smoothing step. Default: `0.2`
|
|
47
|
+
- **`text`**: Custom text used as structural support. Default: `None`
|
|
48
|
+
- **`text2`**: Second line of text. Default: `None`
|
|
49
|
+
- **`textsize`**: Size (height) of the text. Default: `120`
|
|
50
|
+
- **`textback`**: Coronal (depth) position of the back of the text. Default: `300`
|
|
51
|
+
- **`textfront`**: Coronal (depth) position of the front of the text. Default: `330`
|
|
52
|
+
- **`textlinelength`**: Length of line below the text. Default: `10`
|
|
53
|
+
|
|
54
|
+
</details>
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
## Command-Line Interface 🖥️
|
|
58
|
+
Process your first brain scan from the terminal by just running
|
|
59
|
+
```bash
|
|
60
|
+
brain-stl
|
|
61
|
+
```
|
|
62
|
+
Without providing any arguments, it downloads a publicly available scan and creates a `brain.stl` of it 🧠
|
|
63
|
+
|
|
64
|
+
Run `brain-stl --help` (+copy&paste into ChatGPT) to understand the usage of custom settings 💡
|
|
65
|
+
|
|
66
|
+
## Share Your Brains 🧠🌐
|
|
67
|
+
Use `create_gif.py` to get a GIF of your model and share it on social media with the hashtag `#brain-stl` 🤗
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .main import run_brain_stl
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
from brain_stl.main import OPENNEURO_URL, run_brain_stl
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def run_cli():
|
|
6
|
+
parser = argparse.ArgumentParser(description='Generate a 3D STL model of the brain from a scan')
|
|
7
|
+
parser.add_argument('-i', '--scan_filepath', type=str, default=None, help='Path to the input scan (NIfTI or DICOM)')
|
|
8
|
+
parser.add_argument('-o', '--output_folder', type=str, default='.', help='Folder to save the STL file and other outputs')
|
|
9
|
+
parser.add_argument('-n', '--stl_name', type=str, default='brain', help='Name of the example STL file (without extension)')
|
|
10
|
+
parser.add_argument('-ni', '--nifti_name', type=str, help='If DICOM produces multiple NifTis, this NifTi filename is used to proceed')
|
|
11
|
+
parser.add_argument('-u', '--url', type=str, default=OPENNEURO_URL, help='URL to download from if scan_filepath is not provided')
|
|
12
|
+
parser.add_argument('-c', '--use_cache', action='store_true', help='Reuses intermediate outputs (e.g., tissue data)')
|
|
13
|
+
parser.add_argument('-th', '--threshold', type=float, default=1.5, help='Threshold value for brain segmentation (0.5:fluid+gray+white matter, 1.5=gray+white matter, 2.5=white matter)')
|
|
14
|
+
parser.add_argument('-hl', '--hollow', type=float, default=0.0, help='How hollow the brain is (0 to 1)')
|
|
15
|
+
parser.add_argument('-s', '--in_template_space', action='store_true', help='If True, the 3D model is in standardized (template) orientation')
|
|
16
|
+
parser.add_argument('-mr', '--mesh_reduction', type=float, default=0.0, help='Fraction of mesh reduction (0 to 1) for smaller STL file size')
|
|
17
|
+
parser.add_argument('-si', '--smooth_iter', type=int, default=0, help='Number of iterations for Laplacian smoothing')
|
|
18
|
+
parser.add_argument('-sl', '--smooth_lamb', type=float, default=0.2, help='Strength of each Laplacian smoothing step')
|
|
19
|
+
parser.add_argument('-t', '--text', type=str, help='Custom text used as structural support')
|
|
20
|
+
parser.add_argument('-t2', '--text2', type=str, help='Second line of text')
|
|
21
|
+
parser.add_argument('-ts', '--textsize', type=int, default=100, help='Size (height) of the text')
|
|
22
|
+
parser.add_argument('-tb', '--textback', type=int, default=300, help='Coronal (depth) position of the back of the text')
|
|
23
|
+
parser.add_argument('-tf', '--textfront', type=int, default=330, help='Coronal (depth) position of the front of the text')
|
|
24
|
+
parser.add_argument('-tl', '--textlinelength', type=int, default=10, help='Length of line below the text')
|
|
25
|
+
args = parser.parse_args()
|
|
26
|
+
run_brain_stl(scan_filepath=args.scan_filepath,
|
|
27
|
+
output_folder=args.output_folder,
|
|
28
|
+
stl_name=args.stl_name,
|
|
29
|
+
use_cache=args.use_cache,
|
|
30
|
+
hollow=args.hollow,
|
|
31
|
+
text=args.text,
|
|
32
|
+
text2=args.text2,
|
|
33
|
+
textsize=args.textsize,
|
|
34
|
+
textback=args.textback,
|
|
35
|
+
textfront=args.textfront,
|
|
36
|
+
textlinelength=args.textlinelength,
|
|
37
|
+
threshold=args.threshold,
|
|
38
|
+
in_template_space=args.in_template_space,
|
|
39
|
+
mesh_reduction=args.mesh_reduction,
|
|
40
|
+
smooth_iter=args.smooth_iter,
|
|
41
|
+
smooth_lamb=args.smooth_lamb,
|
|
42
|
+
nifti_name=args.nifti_name,
|
|
43
|
+
url=args.url)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
if __name__ == '__main__':
|
|
47
|
+
run_cli()
|
|
Binary file
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import trimesh
|
|
2
|
+
import dcm2niix
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pandas as pd
|
|
5
|
+
import nibabel as nib
|
|
6
|
+
import urllib.request
|
|
7
|
+
import fast_simplification
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from mcubes import marching_cubes
|
|
10
|
+
from deepbet.utils import dilate
|
|
11
|
+
from niftiview import NiftiImageGrid
|
|
12
|
+
from PIL import Image, ImageDraw, ImageFont
|
|
13
|
+
from deepmriprep.preprocess import AFFINE_TEMPLATE, Preprocess
|
|
14
|
+
OPENNEURO_URL = 'https://openneuro.org/crn/datasets/ds000001/snapshots/1.0.0/files/sub-01:anat:sub-01_T1w.nii.gz'
|
|
15
|
+
OUTPUTS = ['affine', 'rotation', 'p0_large']
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def run_brain_stl(scan_filepath=None, output_folder='.', stl_name='brain', nifti_name=None, url=OPENNEURO_URL,
|
|
19
|
+
use_cache=False, threshold=1.5, hollow=.0, in_template_space=False, mesh_reduction=.0, smooth_iter=0,
|
|
20
|
+
smooth_lamb=.2, text=None, text2=None, textsize=120, textback=300, textfront=330, textlinelength=10):
|
|
21
|
+
assert Path(output_folder).is_dir(), f'Output folder {output_folder} does not exist'
|
|
22
|
+
if scan_filepath is None:
|
|
23
|
+
print(f'Instead of a scan from disk, using a scan downloaded from {url}')
|
|
24
|
+
nifti_filepath = f'{output_folder}/{url.split(":")[-1]}'
|
|
25
|
+
urllib.request.urlretrieve(url, nifti_filepath)
|
|
26
|
+
else:
|
|
27
|
+
assert Path(str(scan_filepath)).exists(), f'Scan filepath {scan_filepath} does not exist'
|
|
28
|
+
if Path(scan_filepath).is_file() and not scan_filepath.endswith('.dcm'):
|
|
29
|
+
nifti_filepath = scan_filepath
|
|
30
|
+
else:
|
|
31
|
+
nifti_filepath = process_dicom(scan_filepath, output_folder, nifti_name)
|
|
32
|
+
output_paths = {out: f'{output_folder}/{"tissue.nii.gz" if out == "p0_large" else out+".csv"}' for out in OUTPUTS}
|
|
33
|
+
if not use_cache or not all([Path(output_paths[output]).is_file() for output in OUTPUTS]):
|
|
34
|
+
print(f'Calculate and cache (=write to output folder) affine, rotation and tissue of {nifti_filepath}')
|
|
35
|
+
prep = Preprocess()
|
|
36
|
+
prep.run(nifti_filepath, output_paths=output_paths, run_all=False)
|
|
37
|
+
tissue_img = nib.as_closest_canonical(nib.load(output_paths['p0_large']))
|
|
38
|
+
array = tissue_img.get_fdata(dtype=np.float32)
|
|
39
|
+
array = array if hollow == 0 else hollow_out_brain(array, hollow)
|
|
40
|
+
array = array if text is None else add_text(array, text, text2, textsize, textback, textfront, textlinelength)
|
|
41
|
+
vertices, faces = marching_cubes(array, threshold)
|
|
42
|
+
if in_template_space:
|
|
43
|
+
vertices = to_template_space(vertices)
|
|
44
|
+
else:
|
|
45
|
+
vertices = to_scanner_space(vertices, array, nifti_filepath, affine_filepath=output_paths['affine'])
|
|
46
|
+
if mesh_reduction > 0:
|
|
47
|
+
vertices, faces = fast_simplification.simplify(vertices.tolist(), faces.tolist(), mesh_reduction)
|
|
48
|
+
mesh = trimesh.Trimesh(vertices=vertices, faces=faces)
|
|
49
|
+
if smooth_iter > 0:
|
|
50
|
+
print('Smoothing')
|
|
51
|
+
mesh = trimesh.smoothing.filter_laplacian(mesh, lamb=smooth_lamb, iterations=smooth_iter)
|
|
52
|
+
if stl_name is not None:
|
|
53
|
+
mesh.export(f'{output_folder}/{stl_name}.stl')
|
|
54
|
+
return mesh
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def process_dicom(scan_filepath, output_folder, nifti_name):
|
|
58
|
+
dcm2niix.main([f'-o', output_folder, scan_filepath])
|
|
59
|
+
nifti_files = sorted(Path.glob(f'{output_folder}/*.ni*'))
|
|
60
|
+
assert len(nifti_files) > 0, f'Scan filepath {scan_filepath} is no valid DICOM'
|
|
61
|
+
if len(nifti_files) > 1:
|
|
62
|
+
im = get_t1w_png(nifti_files)
|
|
63
|
+
im.save(f'{output_folder}/pick_t1w_image.png')
|
|
64
|
+
raise RuntimeError(f'The inputted DICOM {scan_filepath} resulted in {len(nifti_files)} Nifti files.'
|
|
65
|
+
f'View {output_folder}/pick_t1w_image.png, select the filename'
|
|
66
|
+
f'of the appropriate(=T1w) Nifti and pass it via "run_brain_stl(..., nifti_name=*)"')
|
|
67
|
+
return nifti_files[0] if nifti_name is None else [fp for fp in nifti_files if nifti_name in fp][0]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def hollow_out_brain(array, hollow):
|
|
71
|
+
r = int(.3 * (array.shape[0] * array.shape[1] * array.shape[2]) ** (1 / 3))
|
|
72
|
+
hollow_mask = dilate(array > 0, n_layer=int((hollow - 1) * r))
|
|
73
|
+
array[hollow_mask] = 0
|
|
74
|
+
return array
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def to_template_space(vertices):
|
|
78
|
+
vertices = AFFINE_TEMPLATE.affine @ np.hstack([vertices, np.ones_like(vertices[:, :1])]).T
|
|
79
|
+
return vertices[:3].T
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def to_scanner_space(vertices, array, nifti_filepath, affine_filepath):
|
|
83
|
+
affine = pd.read_csv(affine_filepath).values
|
|
84
|
+
affine = affine[[2, 1, 0, 3]][:, [2, 1, 0, 3]] # undo pytorch zyx ordering
|
|
85
|
+
img = nib.as_closest_canonical(nib.load(nifti_filepath))
|
|
86
|
+
vertices = (2 * vertices / np.array(array.shape)) - 1 # apply pytorch grid scaling [-1;1]
|
|
87
|
+
vertices = (affine @ np.hstack([vertices, np.ones_like(vertices[:, :1])]).T)[:3]
|
|
88
|
+
vertices = .5 * np.array(img.get_fdata().shape) * (vertices.T + 1) # undo pytorch grid scaling [-1;1]
|
|
89
|
+
vertices = img.affine @ np.hstack([vertices, np.ones_like(vertices[:, :1])]).T
|
|
90
|
+
return vertices[:3].T
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def add_text(array, text, text2, size, back=300, front=330, linelength=10):
|
|
94
|
+
size = size if text2 is None else size // 2
|
|
95
|
+
textrange = range(back, front)
|
|
96
|
+
height = np.where(array[:, textrange].mean(axis=(0, 1)) > .2)[0][0]
|
|
97
|
+
text_array = get_text_array(text, array.shape[0], size)
|
|
98
|
+
text_left, text_right, text_low, text_up = get_text_bbox(text_array, linelength)
|
|
99
|
+
fontheight = text_up - text_low
|
|
100
|
+
array[:, textrange, height - fontheight:height] += text_array[:, None, text_low:text_up]
|
|
101
|
+
if text2 is None:
|
|
102
|
+
array[text_left:text_right, textrange, 1:height - fontheight + 2] += 3
|
|
103
|
+
else:
|
|
104
|
+
text_array = get_text_array(text2, array.shape[0], size)
|
|
105
|
+
text_left, text_right, text_low, text_up = get_text_bbox(text_array, linelength)
|
|
106
|
+
fontheight2 = text_up - text_low
|
|
107
|
+
array[:, textrange, 1:fontheight2+1] = text_array[:, None, text_low:text_up]
|
|
108
|
+
array[text_left:text_right, textrange, fontheight2+1:height-fontheight+1] += 3
|
|
109
|
+
array[text_left:text_right, textrange, 1:2] += 3
|
|
110
|
+
return array.clip(max=3)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def get_text_array(text, width, height):
|
|
114
|
+
im = Image.new('L', (width, height), color=0)
|
|
115
|
+
d = ImageDraw.Draw(im)
|
|
116
|
+
font = ImageFont.load_default(height)
|
|
117
|
+
d.text((width // 2, 0), text, fill=3, font=font, anchor='mt')
|
|
118
|
+
return np.array(im).T[::-1, ::-1]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def get_text_bbox(array, linelength):
|
|
122
|
+
text_left, text_right = np.where(array.sum(axis=1))[0][[0, -1]]
|
|
123
|
+
text_left = max(0, text_left - linelength - 1)
|
|
124
|
+
text_right = min(text_right + linelength - 1, array.shape[0])
|
|
125
|
+
text_low, text_up = np.where(array.sum(axis=0))[0][[0, -1]]
|
|
126
|
+
return text_left, text_right, text_low, text_up
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def get_t1w_png(nifti_files):
|
|
130
|
+
ref_im = Image.open(Path(__file__).parent.resolve() / 'data' / 'ref.png')
|
|
131
|
+
grid_im = NiftiImageGrid(nifti_files).get_image(height=800, layout='sagittal++', fpath=1, fontsize=10)
|
|
132
|
+
ref_im = ref_im.resize((grid_im.size[0], int(ref_im.size[1] * grid_im.size[0] / ref_im.size[0])))
|
|
133
|
+
im = Image.new('L', (ref_im.size[0], ref_im.size[1] + grid_im.size[1]))
|
|
134
|
+
im.paste(ref_im, (0, 0))
|
|
135
|
+
im.paste(grid_im, (0, ref_im.size[1]))
|
|
136
|
+
return im
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = 'brain-stl'
|
|
3
|
+
version = '0.0.1'
|
|
4
|
+
description = 'Turn your brain MRI into a printable 3D model in seconds 🧠➡️🖨️'
|
|
5
|
+
authors = ['codingfisch <l_fisc17@wwu.de>']
|
|
6
|
+
license = 'MIT'
|
|
7
|
+
readme = 'README.md'
|
|
8
|
+
repository = 'https://github.com/codingfisch/brain_stl'
|
|
9
|
+
classifiers = [
|
|
10
|
+
'Programming Language :: Python :: 3',
|
|
11
|
+
'Operating System :: OS Independent',
|
|
12
|
+
'Intended Audience :: Science/Research'
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[tool.poetry.dependencies]
|
|
16
|
+
python = '^3.9'
|
|
17
|
+
niftiview = '*'
|
|
18
|
+
deepmriprep = '*'
|
|
19
|
+
trimesh = '*'
|
|
20
|
+
dcm2niix = '*'
|
|
21
|
+
PyMCubes = '*'
|
|
22
|
+
fast_simplification = '0.1.7'
|
|
23
|
+
|
|
24
|
+
[tool.poetry.scripts]
|
|
25
|
+
brain-stl = 'brain_stl.cli:run_cli'
|
|
26
|
+
|
|
27
|
+
[build-system]
|
|
28
|
+
requires = ['poetry-core']
|
|
29
|
+
build-backend = 'poetry.core.masonry.api'
|