nclvmass-net 1.0__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.
- nclvmass_net-1.0.dist-info/METADATA +57 -0
- nclvmass_net-1.0.dist-info/RECORD +8 -0
- nclvmass_net-1.0.dist-info/WHEEL +5 -0
- nclvmass_net-1.0.dist-info/entry_points.txt +2 -0
- nclvmass_net-1.0.dist-info/top_level.txt +1 -0
- nclvmnet/__init__.py +0 -0
- nclvmnet/label_mass.py +112 -0
- nclvmnet/nclvmnet.py +219 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nclvmass-net
|
|
3
|
+
Version: 1.0
|
|
4
|
+
Summary: An AI Model for Left Ventricular Myocardium Segmentation and Quantification from Non-Contrast Cardiac CT
|
|
5
|
+
Author-email: Jia-Da Li <jiadali@ntu.edu.tw>, Tzung-Dau Wang <tdwang@ntu.edu.tw>, Wen-Jeng Lee <wenjeng@gmail.com>, Cheng-Chia Huang <r12922188@csie.ntu.edu.tw>, Chu-Song Chen <chusong@csie.ntu.edu.tw>
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: codes, https://github.com/AI-Application-and-Integration-Lab/ncLVMass-Net
|
|
8
|
+
Project-URL: models, https://huggingface.co/Jia-Da/ncLVMass-Net
|
|
9
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: Intended Audience :: Healthcare Industry
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
Requires-Dist: nnunetv2>=2.6.0
|
|
19
|
+
Requires-Dist: huggingface-hub
|
|
20
|
+
|
|
21
|
+
# ncLVMass-Net
|
|
22
|
+
An AI model for left ventricular (LV) myocardium segmentation and mass quantification from non-contrast cardiac CT (NCCT) images. The model was trained with the [nnU-Net](https://github.com/mic-dkfz/nnunet) framework on 2,021 patients, all from Taiwan. It achieved an overall Dice score of 0.8827. To use it, provide an input image in NIfTI format (`.nii.gz`) and the model will output the myocardium mask, along with estimated LV mass.
|
|
23
|
+
|
|
24
|
+
# Quick Install
|
|
25
|
+
We recommend first installing [PyTorch](https://pytorch.org/get-started/locally/) in a Python environment (version 3.10 or later), and then installing ncLVMass-Net.
|
|
26
|
+
```
|
|
27
|
+
pip install nclvmass-net
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
# Usage
|
|
31
|
+
```
|
|
32
|
+
nclvmnet -i ct.nii.gz -o output_dir
|
|
33
|
+
nclvmnet -i input_dir -o output_dir
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
# Change pretrained model
|
|
37
|
+
The ncLVMass-Net pretrained model weights are automatically downloaded from [Hugging Face](https://huggingface.co/NTU-AI2Lab/ncLVMass-Net) and stored in the local Hugging Face cache. If you would like to use a different pretrained model, specify the model directory name with the `--dataset` argument and the parent directory containing the model with `--mdl-local-dir`.
|
|
38
|
+
|
|
39
|
+
For example, if you have previously downloaded the [TotalSegmentator](https://github.com/wasserth/TotalSegmentator) `heartchambers_highres` model (make sure you have obtained the appropriate [license](https://backend.totalsegmentator.com/license-academic/)), you can switch to this model for comparison.
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
nclvmnet -i ct.nii.gz -o output_dir_TS --mdl-local-dir $HOME/.totalsegmentator/nnunet/results --dataset Dataset301_heart_highres_1559subj
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
For non-commercial purposes, you may download the pretrained model weights directly from Hugging Face and fine-tune them on your own data.
|
|
46
|
+
```
|
|
47
|
+
hf download --local-dir $HOME/path/to/ncLVMass-Net/model NTU-AI2Lab/ncLVMass-Net
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
# Further development
|
|
51
|
+
The package directly invokes `nnUNetv2_predict` and `nnUNetv2_apply_postprocessing` with the specified input (`-i`), output (`-o`), and pretrained model (`--mdl-local-dir` and `--dataset`) paths, without modifying the original nnU-Net workflow. Note that the custom trainer used for other pretrained model (e.g., one with a non-default number of epochs) may not be available in the original nnU-Net codebase. For further customization or development, install the package in editable mode.
|
|
52
|
+
```
|
|
53
|
+
git clone https://github.com/MIC-DKFZ/nnUNet.git
|
|
54
|
+
git clone https://github.com/AI-Application-and-Integration-Lab/ncLVMass-Net.git
|
|
55
|
+
pip install -e ./nnUNet
|
|
56
|
+
pip install -e ./ncLVMass-Net
|
|
57
|
+
```
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
nclvmnet/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
nclvmnet/label_mass.py,sha256=d5t44dydXIt3QibJhhjcdtwT_nufwk16ZaLFfRR80GM,4510
|
|
3
|
+
nclvmnet/nclvmnet.py,sha256=xqf0bQhBAYFzmA3EkmaZjp402oQQisZOXWM82XpYAsM,12894
|
|
4
|
+
nclvmass_net-1.0.dist-info/METADATA,sha256=p5XhoFQmTWtQOpQMa6fFWTcOv7cxNmyBtxVpzhFTNFQ,3590
|
|
5
|
+
nclvmass_net-1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
6
|
+
nclvmass_net-1.0.dist-info/entry_points.txt,sha256=1zb86OfbHoJ6gTqH3HH9Kq6FLu1VacRQf-mA0pU--RE,52
|
|
7
|
+
nclvmass_net-1.0.dist-info/top_level.txt,sha256=gSPHUsqsda_dGNFW8SD9yOZ6cC_qXeBTkCy7jcMHnIQ,9
|
|
8
|
+
nclvmass_net-1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
nclvmnet
|
nclvmnet/__init__.py
ADDED
|
File without changes
|
nclvmnet/label_mass.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import os
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
NIFTI_EXTS = (".nii.gz", ".nii")
|
|
10
|
+
SITK_EXTS = (".nrrd", ".nhdr", ".mha", ".mhd")
|
|
11
|
+
ALL_EXTS = NIFTI_EXTS + SITK_EXTS
|
|
12
|
+
|
|
13
|
+
NIFTI_UNIT_TO_MM = {"unknown": 1.0, "meter": 1000.0, "mm": 1.0, "micron": 0.001}
|
|
14
|
+
|
|
15
|
+
# --------------------------------------------------------------------------------
|
|
16
|
+
# Format-specific readers: each returns (3D numpy array, spacing in mm as a tuple)
|
|
17
|
+
# --------------------------------------------------------------------------------
|
|
18
|
+
def read_nifti(entry: os.DirEntry):
|
|
19
|
+
import nibabel as nib
|
|
20
|
+
|
|
21
|
+
img = nib.load(entry.path)
|
|
22
|
+
data = np.asanyarray(img.dataobj)
|
|
23
|
+
zooms = img.header.get_zooms()[: data.ndim]
|
|
24
|
+
unit = img.header.get_xyzt_units()[0]
|
|
25
|
+
factor = NIFTI_UNIT_TO_MM.get(unit, 1.0)
|
|
26
|
+
if unit not in NIFTI_UNIT_TO_MM:
|
|
27
|
+
print(f"[Warning] {entry.name}: unknow unit '{unit}', assuming mm", file=sys.stderr)
|
|
28
|
+
spacing = tuple(float(z) * factor for z in zooms)
|
|
29
|
+
return data, spacing
|
|
30
|
+
|
|
31
|
+
def read_sitk(entry: os.DirEntry):
|
|
32
|
+
import SimpleITK as sitk
|
|
33
|
+
|
|
34
|
+
img = sitk.ReadImage(entry.path)
|
|
35
|
+
data = sitk.GetArrayFromImage(img) # array order is (z, y, x)
|
|
36
|
+
spacing = tuple(float(s) for s in img.GetSpacing()) # spacing order is (x, y, z), in mm
|
|
37
|
+
return data, spacing
|
|
38
|
+
|
|
39
|
+
def load_segmentation(entry: os.DirEntry):
|
|
40
|
+
name = entry.name.lower()
|
|
41
|
+
if name.endswith(NIFTI_EXTS):
|
|
42
|
+
data, spacing = read_nifti(entry)
|
|
43
|
+
elif name.endswith(SITK_EXTS):
|
|
44
|
+
data, spacing = read_sitk(entry)
|
|
45
|
+
else:
|
|
46
|
+
raise ValueError(f"Unsupported file format: {entry.name}")
|
|
47
|
+
|
|
48
|
+
# Handle non-single-volume data (e.g. 4D, or an extra channel dimension)
|
|
49
|
+
data = np.squeeze(data)
|
|
50
|
+
if data.ndim != 3:
|
|
51
|
+
raise ValueError(f"{entry.name}: data is {data.ndim}D, only 3D is supported (shape={data.shape})")
|
|
52
|
+
if len(spacing) != 3:
|
|
53
|
+
raise ValueError(f"{entry.name}: got {len(spacing)} spacing values, expected 3 ({spacing})")
|
|
54
|
+
return data, spacing
|
|
55
|
+
|
|
56
|
+
# ------------------
|
|
57
|
+
# Volume computation
|
|
58
|
+
# ------------------
|
|
59
|
+
def compute_volumes(data: np.ndarray, spacing_mm, target_label:int):
|
|
60
|
+
"""Return {label: (voxel_count, volume_cm3)}"""
|
|
61
|
+
if not np.issubdtype(data.dtype, np.integer):
|
|
62
|
+
rounded = np.rint(data)
|
|
63
|
+
if not np.allclose(data, rounded):
|
|
64
|
+
raise ValueError(
|
|
65
|
+
"Data contains non-integer values; this does not look like a label map "
|
|
66
|
+
"(is it a softmax / probability output?)"
|
|
67
|
+
)
|
|
68
|
+
data = rounded.astype(np.int64)
|
|
69
|
+
|
|
70
|
+
voxel_mm3 = float(np.prod(spacing_mm))
|
|
71
|
+
result = np.sum(data==target_label) * voxel_mm3 / 1000.0 # mm^3 -> cm^3
|
|
72
|
+
return result
|
|
73
|
+
|
|
74
|
+
def get_label(dataset_json, label_name):
|
|
75
|
+
"""Read {int: name} from an nnU-Net dataset.json."""
|
|
76
|
+
with open(dataset_json, 'rt') as f:
|
|
77
|
+
dataset = json.load(f)
|
|
78
|
+
labels = dataset.get('labels', {})
|
|
79
|
+
label = None
|
|
80
|
+
for k, v in labels.items():
|
|
81
|
+
if isinstance(k, str) and label_name in k.lower():
|
|
82
|
+
label = int(v)
|
|
83
|
+
elif isinstance(v, str) and label_name in v.lower():
|
|
84
|
+
label = int(k)
|
|
85
|
+
return label
|
|
86
|
+
|
|
87
|
+
def label_mass(label_name:str):
|
|
88
|
+
parser = argparse.ArgumentParser(description="Compute specific label volume (cm^3) and mass (g) from nnU-Net segmentations")
|
|
89
|
+
parser.add_argument("--input", type=str, required=True, help="The directory path which conaining mask files output from nnU-Net framework")
|
|
90
|
+
parser.add_argument("--dataset-json", type=str, required=True, help="nnU-Net dataset.json, used to display label names")
|
|
91
|
+
parser.add_argument("--rho", type=float, default=1.055, help="Myocardium density in g/cm^3. Default is 1.055 g/cm^3.")
|
|
92
|
+
args = parser.parse_args()
|
|
93
|
+
|
|
94
|
+
out = 'density (g/cm^3): ' + str(args.rho) + '\n'
|
|
95
|
+
for entry in os.scandir(args.input):
|
|
96
|
+
if entry.is_file() and entry.name.lower().endswith(ALL_EXTS):
|
|
97
|
+
data, spacing = load_segmentation(entry)
|
|
98
|
+
label = get_label(args.dataset_json, label_name)
|
|
99
|
+
if label is None:
|
|
100
|
+
print(f"[Warning] Label '{label_name}' not found in {args.dataset_json}")
|
|
101
|
+
continue
|
|
102
|
+
volume_cm3 = compute_volumes(data, spacing, label)
|
|
103
|
+
mass_g = volume_cm3 * args.rho
|
|
104
|
+
out += f"{entry.name}: volume={volume_cm3:.3f} cm^3, mass={mass_g:.3f} g\n"
|
|
105
|
+
|
|
106
|
+
txt_path = os.path.join(args.input, f'{label_name}_mass.txt')
|
|
107
|
+
with open(txt_path, 'wt') as f:
|
|
108
|
+
f.write(out)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
if __name__ == "__main__":
|
|
112
|
+
label_mass("myocardium")
|
nclvmnet/nclvmnet.py
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import argparse
|
|
3
|
+
import tempfile
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
import shutil
|
|
7
|
+
from huggingface_hub import snapshot_download, try_to_load_from_cache
|
|
8
|
+
from nnunetv2.inference.predict_from_raw_data import predict_entry_point
|
|
9
|
+
from nnunetv2.postprocessing.remove_connected_components import entry_point_apply_postprocessing
|
|
10
|
+
from nclvmnet.label_mass import label_mass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main():
|
|
14
|
+
parser = argparse.ArgumentParser(description="ncLVMass-Net: A tool for estimating left ventricular mass from non-contrast CT images using nnU-Net.")
|
|
15
|
+
parser.add_argument('-i', '--input', type=str, required=True, help="A CT nifti image path or a folder containing CT nifti images.")
|
|
16
|
+
parser.add_argument('-o', '--output', type=str, required=True, help='Output folder path.')
|
|
17
|
+
parser.add_argument('--mdl-local-dir', type=str, default=None, help='Local directory of the model folder.')
|
|
18
|
+
parser.add_argument('--hf-token', type=str, default=None, help='Hugging Face token for accelerate download process.')
|
|
19
|
+
parser.add_argument('-d', '--dataset', type=str, default=None, help='Select a dataset in the model folder to predict. If not specified, the first finding dataset will be selected.')
|
|
20
|
+
parser.add_argument('-tr', '--trainer', type=str, default=None, help='What nnU-Net trainer class was used for training? If not specified, the first finding folder will be selected.')
|
|
21
|
+
parser.add_argument('-p', '--plans', type=str, default=None, help='Plans identifier. If not specified, the first finding folder will be selected.')
|
|
22
|
+
parser.add_argument('-c', '--config', type=str, default=None, help='nnU-Net configuration (2d, 3d_fullres, 3d_lowres, 3d_cascade_fullres). If not specified, the first finding folder will be selected.')
|
|
23
|
+
parser.add_argument('-f', '--fold', nargs='+', type=str, default=['0','1','2','3','4'], help='Which fold(s) to use for prediction. Default is depends on first finding "crossval_results_folds_x_x_x..." folder. If not exist, select "all" if exist, otherwise select the first fold.')
|
|
24
|
+
parser.add_argument('-chk', '--checkpoint', type=str, default='checkpoint_best.pth', help='Checkpoint file name. Should be checkpoint_best.pth (default) or checkpoint_final.pth.')
|
|
25
|
+
parser.add_argument('--myo-density', type=float, default=1.055, help='Myocardium density in g/cm^3. Default is 1.055 g/cm^3.')
|
|
26
|
+
args = parser.parse_args()
|
|
27
|
+
|
|
28
|
+
# ---------------------------------
|
|
29
|
+
# verify and update input arguments
|
|
30
|
+
# ---------------------------------
|
|
31
|
+
if args.mdl_local_dir is None:
|
|
32
|
+
# Download repository from huggingface and get the local path of the model folder
|
|
33
|
+
repo_id = "Jia-Da/ncLVMass-Net"
|
|
34
|
+
# repo_id = "NTU-AI2Lab/ncLVMass-Net"
|
|
35
|
+
readme_path = try_to_load_from_cache(repo_id=repo_id, filename="README.md")
|
|
36
|
+
if readme_path is not None:
|
|
37
|
+
args.mdl_local_dir = os.path.dirname(readme_path)
|
|
38
|
+
print('ncLVMass-Net model weights already downloaded.')
|
|
39
|
+
else:
|
|
40
|
+
if args.hf_token is None:
|
|
41
|
+
args.mdl_local_dir = snapshot_download(repo_id=repo_id)
|
|
42
|
+
else:
|
|
43
|
+
args.mdl_local_dir = snapshot_download(repo_id=repo_id, token=args.hf_token)
|
|
44
|
+
else:
|
|
45
|
+
if not os.path.isdir(args.mdl_local_dir):
|
|
46
|
+
raise ValueError(f"The specified model local directory does not exist: {args.mdl_local_dir}")
|
|
47
|
+
|
|
48
|
+
# -------------------
|
|
49
|
+
# find Dataset folder
|
|
50
|
+
# -------------------
|
|
51
|
+
if args.dataset is None:
|
|
52
|
+
args.dataset = next(f.name for f in os.scandir(args.mdl_local_dir) if f.is_dir() and f.name.startswith("Dataset"))
|
|
53
|
+
else:
|
|
54
|
+
if not os.path.isdir(os.path.join(args.mdl_local_dir, args.dataset)):
|
|
55
|
+
if args.dataset.isdecimal():
|
|
56
|
+
args.dataset = next(f.name for f in os.scandir(args.mdl_local_dir) if f.is_dir() and f.name.startswith(f"Dataset{args.dataset}"))
|
|
57
|
+
else:
|
|
58
|
+
args.dataset = next(f.name for f in os.scandir(args.mdl_local_dir) if f.is_dir() and f.name.startswith("Dataset") and args.dataset in f.name)
|
|
59
|
+
|
|
60
|
+
if args.trainer is None or \
|
|
61
|
+
args.plans is None or \
|
|
62
|
+
args.config is None or \
|
|
63
|
+
not os.path.isdir(os.path.join(args.mdl_local_dir, args.dataset, f'{args.trainer}__{args.plans}__{args.config}')):
|
|
64
|
+
# find plans, trainer, config
|
|
65
|
+
for f in os.scandir(os.path.join(args.mdl_local_dir, args.dataset)):
|
|
66
|
+
if f.is_dir() and '__' in f.name:
|
|
67
|
+
parts = f.name.split('__')
|
|
68
|
+
if len(parts) == 3:
|
|
69
|
+
args.trainer, args.plans, args.config = parts
|
|
70
|
+
break
|
|
71
|
+
|
|
72
|
+
subfolder = os.path.join(args.mdl_local_dir, args.dataset, f'{args.trainer}__{args.plans}__{args.config}')
|
|
73
|
+
if os.path.isdir(os.path.join(subfolder, 'crossval_results_folds_' + '_'.join(args.fold))):
|
|
74
|
+
postprocess = True
|
|
75
|
+
for f in args.fold:
|
|
76
|
+
assert os.path.isdir(os.path.join(subfolder, f'fold_{f}')), 'Fold folder does not exist: ' + os.path.join(subfolder, f'fold_{f}')
|
|
77
|
+
if not os.path.isfile(os.path.join(subfolder, f'fold_{f}', args.checkpoint)):
|
|
78
|
+
if f in {'0', 'all'}:
|
|
79
|
+
for chk in ('checkpoint_best.pth', 'checkpoint_final.pth'):
|
|
80
|
+
if os.path.isfile(os.path.join(subfolder, f'fold_{f}', chk)):
|
|
81
|
+
args.checkpoint = chk
|
|
82
|
+
break
|
|
83
|
+
assert os.path.isfile(os.path.join(subfolder, f'fold_{f}', args.checkpoint)), 'Checkpoint file does not exist: ' + os.path.join(subfolder, f'fold_{f}', args.checkpoint)
|
|
84
|
+
else:
|
|
85
|
+
raise FileNotFoundError('Checkpoint file does not exist: ' + os.path.join(subfolder, f'fold_{f}', args.checkpoint))
|
|
86
|
+
else:
|
|
87
|
+
crf = next((f.name for f in os.scandir(subfolder) if f.is_dir() and f.name.startswith('crossval_results_folds_')), None)
|
|
88
|
+
if crf is not None:
|
|
89
|
+
if crf.split('_')[3:] != args.fold:
|
|
90
|
+
print(f'Warning: The specified folds {args.fold} do not match the folds in the cross-validation results folder "{crf}". Using folds from the folder instead.')
|
|
91
|
+
args.fold = crf.split('_')[3:]
|
|
92
|
+
postprocess = True
|
|
93
|
+
for f in args.fold:
|
|
94
|
+
assert os.path.isdir(os.path.join(subfolder, f'fold_{f}')), 'Fold folder does not exist: ' + os.path.join(subfolder, f'fold_{f}')
|
|
95
|
+
if not os.path.isfile(os.path.join(subfolder, f'fold_{f}', args.checkpoint)):
|
|
96
|
+
if f in {'0', 'all'}:
|
|
97
|
+
for chk in ('checkpoint_best.pth', 'checkpoint_final.pth'):
|
|
98
|
+
if os.path.isfile(os.path.join(subfolder, f'fold_{f}', chk)):
|
|
99
|
+
args.checkpoint = chk
|
|
100
|
+
break
|
|
101
|
+
assert os.path.isfile(os.path.join(subfolder, f'fold_{f}', args.checkpoint)), 'Checkpoint file does not exist: ' + os.path.join(subfolder, f'fold_{f}', args.checkpoint)
|
|
102
|
+
else:
|
|
103
|
+
raise FileNotFoundError('Checkpoint file does not exist: ' + os.path.join(subfolder, f'fold_{f}', args.checkpoint))
|
|
104
|
+
else:
|
|
105
|
+
postprocess = False
|
|
106
|
+
fold_folder = next((f.name for f in os.scandir(subfolder) if f.is_dir() and f.name.startswith('fold_')), None)
|
|
107
|
+
if fold_folder is not None:
|
|
108
|
+
if args.fold != [fold_folder.split('_')[1]]:
|
|
109
|
+
print(f'Warning: The specified folds {args.fold} do not match the fold in the folder "{fold_folder}". Using fold from the folder instead.')
|
|
110
|
+
args.fold = [fold_folder.split('_')[1]]
|
|
111
|
+
if not os.path.isfile(os.path.join(subfolder, fold_folder, args.checkpoint)):
|
|
112
|
+
for chk in ('checkpoint_best.pth', 'checkpoint_final.pth'):
|
|
113
|
+
if os.path.isfile(os.path.join(subfolder, fold_folder, chk)):
|
|
114
|
+
args.checkpoint = chk
|
|
115
|
+
break
|
|
116
|
+
assert os.path.isfile(os.path.join(subfolder, fold_folder, args.checkpoint)), 'Checkpoint file does not exist: ' + os.path.join(subfolder, fold_folder, args.checkpoint)
|
|
117
|
+
else:
|
|
118
|
+
raise FileNotFoundError('No fold folder found in: ' + subfolder)
|
|
119
|
+
|
|
120
|
+
# ---------------------------------------
|
|
121
|
+
# check input image file name and output folder
|
|
122
|
+
# ---------------------------------------
|
|
123
|
+
if os.path.exists(args.input):
|
|
124
|
+
args.input = os.path.abspath(args.input)
|
|
125
|
+
else:
|
|
126
|
+
raise FileNotFoundError(f"The specified input path does not exist: {args.input}")
|
|
127
|
+
|
|
128
|
+
dataset_json_path = os.path.join(subfolder, 'dataset.json')
|
|
129
|
+
if not os.path.isfile(dataset_json_path):
|
|
130
|
+
raise FileNotFoundError(f"dataset.json file does not exist in the model folder: {dataset_json_path}")
|
|
131
|
+
with open(dataset_json_path, 'rt') as f:
|
|
132
|
+
dataset_json = json.load(f)
|
|
133
|
+
|
|
134
|
+
# find the CT channel index
|
|
135
|
+
ct_channel = None
|
|
136
|
+
for k, v in dataset_json['channel_names'].items():
|
|
137
|
+
if v.upper()=='CT':
|
|
138
|
+
ct_channel = k.zfill(4)
|
|
139
|
+
break
|
|
140
|
+
assert ct_channel is not None, 'CT channel not found in dataset.json'
|
|
141
|
+
|
|
142
|
+
# build a temporary folder and copy input images in it, filename adding the CT channel suffix if necessary
|
|
143
|
+
if os.path.isfile(args.input):
|
|
144
|
+
src_img_dir = os.path.dirname(args.input)
|
|
145
|
+
src_img_name = [os.path.basename(args.input)]
|
|
146
|
+
else:
|
|
147
|
+
src_img_dir = args.input
|
|
148
|
+
src_img_name = [f.name for f in os.scandir(src_img_dir) if f.is_file() and f.name.endswith(dataset_json['file_ending'])]
|
|
149
|
+
if len(src_img_name) == 0:
|
|
150
|
+
raise FileNotFoundError(f"No input image files found in the specified path: {args.input}")
|
|
151
|
+
extL = len(dataset_json['file_ending'])
|
|
152
|
+
tmp_img_dir = tempfile.mkdtemp(dir=src_img_dir)
|
|
153
|
+
tmp_img_name = []
|
|
154
|
+
for src_name in src_img_name:
|
|
155
|
+
src = os.path.join(src_img_dir, src_name)
|
|
156
|
+
if src_name.endswith(f'_{ct_channel}{dataset_json["file_ending"]}'):
|
|
157
|
+
tmp_img_name.append(src_name)
|
|
158
|
+
else:
|
|
159
|
+
tmp_img_name.append(f'{src_name[:-extL]}_{ct_channel}{dataset_json["file_ending"]}') # add channel
|
|
160
|
+
dst = os.path.join(tmp_img_dir, tmp_img_name[-1])
|
|
161
|
+
shutil.copyfile(src, dst)
|
|
162
|
+
|
|
163
|
+
# prepare output folder
|
|
164
|
+
if os.path.exists(args.output):
|
|
165
|
+
if not os.path.isdir(args.output):
|
|
166
|
+
raise NotADirectoryError(f"The specified output path is not a directory: {args.output}")
|
|
167
|
+
else:
|
|
168
|
+
os.makedirs(args.output)
|
|
169
|
+
|
|
170
|
+
# ----------------------------------------
|
|
171
|
+
# create environment variables for nnU-Net
|
|
172
|
+
# ----------------------------------------
|
|
173
|
+
if os.environ.get('nnUNet_raw') is None:
|
|
174
|
+
os.environ['nnUNet_raw'] = '<path_to_nnUNet_raw>'
|
|
175
|
+
if os.environ.get('nnUNet_preprocessed') is None:
|
|
176
|
+
os.environ['nnUNet_preprocessed'] = '<path_to_nnUNet_preprocessed>'
|
|
177
|
+
# overwrite nnUNet_results to the model folder
|
|
178
|
+
os.environ['nnUNet_results'] = args.mdl_local_dir
|
|
179
|
+
|
|
180
|
+
# ======================
|
|
181
|
+
# run nnU-Net prediction
|
|
182
|
+
# ======================
|
|
183
|
+
if postprocess:
|
|
184
|
+
print('Starting prediction...')
|
|
185
|
+
output_predict = os.path.join(tmp_img_dir, 'predicted')
|
|
186
|
+
sys.argv = ["predict_entry_point", "-i", tmp_img_dir, "-o", output_predict, "-d", args.dataset, "-tr", args.trainer, "-p", args.plans, "-c", args.config, "-f"] + args.fold + ["-chk", args.checkpoint]
|
|
187
|
+
predict_entry_point()
|
|
188
|
+
|
|
189
|
+
# post-processing
|
|
190
|
+
print('Applying post-processing...')
|
|
191
|
+
pkl_file = os.path.join(subfolder, 'crossval_results_folds_' + '_'.join(args.fold), 'postprocessing.pkl')
|
|
192
|
+
plans_json_file = os.path.join(subfolder, 'plans.json')
|
|
193
|
+
sys.argv = ["entry_point_apply_postprocessing", "-i", output_predict, "-o", args.output, "-pp_pkl_file", pkl_file, "-plans_json", plans_json_file, "-dataset_json", dataset_json_path]
|
|
194
|
+
entry_point_apply_postprocessing()
|
|
195
|
+
|
|
196
|
+
# Move other files (e.g., *.json) in the output_predict folder to the final output folder
|
|
197
|
+
for f in os.scandir(output_predict):
|
|
198
|
+
if f.is_file() and not f.name.endswith(dataset_json['file_ending']):
|
|
199
|
+
os.rename(f.path, os.path.join(args.output, f.name))
|
|
200
|
+
else:
|
|
201
|
+
print('Starting prediction...')
|
|
202
|
+
sys.argv = ["predict_entry_point", "-i", tmp_img_dir, "-o", args.output, "-d", args.dataset, "-tr", args.trainer, "-p", args.plans, "-c", args.config, "-f"] + args.fold + ["-chk", args.checkpoint]
|
|
203
|
+
predict_entry_point()
|
|
204
|
+
|
|
205
|
+
# remove image files and temp directory after prediction
|
|
206
|
+
shutil.rmtree(tmp_img_dir)
|
|
207
|
+
|
|
208
|
+
# ----------------
|
|
209
|
+
# estimate LV mass
|
|
210
|
+
# ----------------
|
|
211
|
+
print('Estimating LV mass...')
|
|
212
|
+
sys.argv = ['label_mass', '--input', args.output, '--dataset-json', dataset_json_path, '--rho', str(args.myo_density)]
|
|
213
|
+
label_mass('myocardium')
|
|
214
|
+
print('All done!')
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
if __name__ == "__main__":
|
|
219
|
+
main()
|