nclvmass-net 1.0__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.
@@ -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,37 @@
1
+ # ncLVMass-Net
2
+ 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.
3
+
4
+ # Quick Install
5
+ 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.
6
+ ```
7
+ pip install nclvmass-net
8
+ ```
9
+
10
+ # Usage
11
+ ```
12
+ nclvmnet -i ct.nii.gz -o output_dir
13
+ nclvmnet -i input_dir -o output_dir
14
+ ```
15
+
16
+ # Change pretrained model
17
+ 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`.
18
+
19
+ 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.
20
+
21
+ ```
22
+ nclvmnet -i ct.nii.gz -o output_dir_TS --mdl-local-dir $HOME/.totalsegmentator/nnunet/results --dataset Dataset301_heart_highres_1559subj
23
+ ```
24
+
25
+ For non-commercial purposes, you may download the pretrained model weights directly from Hugging Face and fine-tune them on your own data.
26
+ ```
27
+ hf download --local-dir $HOME/path/to/ncLVMass-Net/model NTU-AI2Lab/ncLVMass-Net
28
+ ```
29
+
30
+ # Further development
31
+ 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.
32
+ ```
33
+ git clone https://github.com/MIC-DKFZ/nnUNet.git
34
+ git clone https://github.com/AI-Application-and-Integration-Lab/ncLVMass-Net.git
35
+ pip install -e ./nnUNet
36
+ pip install -e ./ncLVMass-Net
37
+ ```
@@ -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,12 @@
1
+ README.md
2
+ pyproject.toml
3
+ setup.py
4
+ nclvmass_net.egg-info/PKG-INFO
5
+ nclvmass_net.egg-info/SOURCES.txt
6
+ nclvmass_net.egg-info/dependency_links.txt
7
+ nclvmass_net.egg-info/entry_points.txt
8
+ nclvmass_net.egg-info/requires.txt
9
+ nclvmass_net.egg-info/top_level.txt
10
+ nclvmnet/__init__.py
11
+ nclvmnet/label_mass.py
12
+ nclvmnet/nclvmnet.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ nclvmnet = nclvmnet.nclvmnet:main
@@ -0,0 +1,2 @@
1
+ nnunetv2>=2.6.0
2
+ huggingface-hub
@@ -0,0 +1 @@
1
+ nclvmnet
File without changes
@@ -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")
@@ -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()
@@ -0,0 +1,41 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68","wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "nclvmass-net"
7
+ version = "1.0"
8
+ requires-python = ">=3.10"
9
+ description = "An AI Model for Left Ventricular Myocardium Segmentation and Quantification from Non-Contrast Cardiac CT"
10
+ readme = "README.md"
11
+ license = { text = "Apache-2.0" }
12
+ classifiers = [
13
+ "Development Status :: 5 - Production/Stable",
14
+ "Programming Language :: Python :: 3",
15
+ "License :: OSI Approved :: Apache Software License",
16
+ "Intended Audience :: Developers",
17
+ "Intended Audience :: Science/Research",
18
+ "Intended Audience :: Healthcare Industry",
19
+ "Topic :: Scientific/Engineering :: Medical Science Apps.",
20
+ ]
21
+ authors = [
22
+ { name = "Jia-Da Li", email = "jiadali@ntu.edu.tw" },
23
+ { name = "Tzung-Dau Wang", email = "tdwang@ntu.edu.tw" },
24
+ { name = "Wen-Jeng Lee", email = "wenjeng@gmail.com" },
25
+ { name = "Cheng-Chia Huang", email = "r12922188@csie.ntu.edu.tw" },
26
+ { name = "Chu-Song Chen", email = "chusong@csie.ntu.edu.tw" }
27
+ ]
28
+
29
+ dependencies = [
30
+ "nnunetv2>=2.6.0",
31
+ "huggingface-hub",
32
+ ]
33
+
34
+ [project.optional-dependencies]
35
+
36
+ [project.urls]
37
+ codes = "https://github.com/AI-Application-and-Integration-Lab/ncLVMass-Net"
38
+ models = "https://huggingface.co/Jia-Da/ncLVMass-Net"
39
+
40
+ [project.scripts]
41
+ nclvmnet = "nclvmnet.nclvmnet:main"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,4 @@
1
+ import setuptools
2
+
3
+ if __name__ == "__main__":
4
+ setuptools.setup()