linflonet 0.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.
- linflonet/__init__.py +3 -0
- linflonet/__main__.py +3 -0
- linflonet/cli.py +210 -0
- linflonet/data/__init__.py +0 -0
- linflonet/data/template/__init__.py +0 -0
- linflonet/data/template/highres_template.vtp +44 -0
- linflonet/data/template/highres_template_distance.vtk +0 -0
- linflonet/data/template/whole_heart_with_ao.vtp +44 -0
- linflonet/paths.py +32 -0
- linflonet/predict.py +236 -0
- linflonet-0.1.0.dist-info/METADATA +302 -0
- linflonet-0.1.0.dist-info/RECORD +37 -0
- linflonet-0.1.0.dist-info/WHEEL +5 -0
- linflonet-0.1.0.dist-info/entry_points.txt +2 -0
- linflonet-0.1.0.dist-info/licenses/LICENSE +21 -0
- linflonet-0.1.0.dist-info/top_level.txt +3 -0
- src/__init__.py +0 -0
- src/chamfer.py +224 -0
- src/dataset.py +68 -0
- src/finite_difference.py +39 -0
- src/flow.py +116 -0
- src/flow_loss.py +62 -0
- src/integrator.py +114 -0
- src/io_utils.py +204 -0
- src/linear_transform.py +208 -0
- src/loss.py +109 -0
- src/pre_process.py +146 -0
- src/random_linear_perturbations.py +29 -0
- src/segment_flow.py +516 -0
- src/signed_distance_function.py +221 -0
- src/template.py +237 -0
- src/unet.py +143 -0
- src/unet_components.py +193 -0
- src/unet_segment.py +57 -0
- src/utilities.py +40 -0
- vtk_utils/__init__.py +0 -0
- vtk_utils/vtk_utils.py +929 -0
linflonet/__init__.py
ADDED
linflonet/__main__.py
ADDED
linflonet/cli.py
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""Command-line interface for LinFlo-Net."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
from linflonet import __version__
|
|
10
|
+
from linflonet.paths import bundled_template, resolve_template_path
|
|
11
|
+
from linflonet.predict import (
|
|
12
|
+
PredictionConfig,
|
|
13
|
+
find_image_files,
|
|
14
|
+
predict_folder,
|
|
15
|
+
predict_images,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
20
|
+
parser = argparse.ArgumentParser(
|
|
21
|
+
prog="linflonet",
|
|
22
|
+
description="Generate simulation-ready heart meshes from CT or MR images.",
|
|
23
|
+
)
|
|
24
|
+
parser.add_argument("--version", action="version", version=f"linflonet {__version__}")
|
|
25
|
+
|
|
26
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
27
|
+
|
|
28
|
+
predict = subparsers.add_parser(
|
|
29
|
+
"predict",
|
|
30
|
+
help="Predict heart meshes and segmentations from one or more images",
|
|
31
|
+
)
|
|
32
|
+
predict.add_argument(
|
|
33
|
+
"--config",
|
|
34
|
+
help="YAML config file (overrides individual model/template options)",
|
|
35
|
+
)
|
|
36
|
+
predict.add_argument(
|
|
37
|
+
"-i",
|
|
38
|
+
"--image",
|
|
39
|
+
action="append",
|
|
40
|
+
dest="images",
|
|
41
|
+
help="Path to a single input image (.nii or .nii.gz). Repeat for multiple files.",
|
|
42
|
+
)
|
|
43
|
+
predict.add_argument(
|
|
44
|
+
"-f",
|
|
45
|
+
"--folder",
|
|
46
|
+
help="Folder containing images, or a folder with an image/ subdirectory",
|
|
47
|
+
)
|
|
48
|
+
predict.add_argument(
|
|
49
|
+
"-o",
|
|
50
|
+
"--output",
|
|
51
|
+
required=False,
|
|
52
|
+
help="Output directory for meshes/ and segmentation/ subfolders",
|
|
53
|
+
)
|
|
54
|
+
predict.add_argument(
|
|
55
|
+
"--model",
|
|
56
|
+
help="Path to trained model checkpoint (best_model.pth)",
|
|
57
|
+
)
|
|
58
|
+
predict.add_argument(
|
|
59
|
+
"--modality",
|
|
60
|
+
choices=["ct", "mr"],
|
|
61
|
+
help="Image modality used for intensity normalization",
|
|
62
|
+
)
|
|
63
|
+
predict.add_argument(
|
|
64
|
+
"--template",
|
|
65
|
+
help="Path to template mesh (.vtp). Defaults to bundled whole-heart template.",
|
|
66
|
+
)
|
|
67
|
+
predict.add_argument(
|
|
68
|
+
"--template-distance-map",
|
|
69
|
+
help="Path to template distance map (.vtk or .pth) for flow models",
|
|
70
|
+
)
|
|
71
|
+
predict.add_argument(
|
|
72
|
+
"--linear-transform",
|
|
73
|
+
action="store_true",
|
|
74
|
+
help="Use a linear-transform-only model (skip template distance map)",
|
|
75
|
+
)
|
|
76
|
+
predict.add_argument(
|
|
77
|
+
"-e",
|
|
78
|
+
"--extension",
|
|
79
|
+
default=".nii.gz",
|
|
80
|
+
help="Input image file extension (default: .nii.gz)",
|
|
81
|
+
)
|
|
82
|
+
predict.add_argument(
|
|
83
|
+
"--output-extension",
|
|
84
|
+
choices=[".nii.gz", ".vti"],
|
|
85
|
+
default=".nii.gz",
|
|
86
|
+
help="Segmentation output format (default: .nii.gz)",
|
|
87
|
+
)
|
|
88
|
+
predict.add_argument(
|
|
89
|
+
"-n",
|
|
90
|
+
type=int,
|
|
91
|
+
default=-1,
|
|
92
|
+
help="When using --folder, limit to the first N images (-1 for all)",
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
return parser
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _resolve_config(args: argparse.Namespace) -> tuple[PredictionConfig, str]:
|
|
99
|
+
if args.config:
|
|
100
|
+
config = PredictionConfig.from_yaml(args.config)
|
|
101
|
+
if args.model:
|
|
102
|
+
config.model = args.model
|
|
103
|
+
if args.modality:
|
|
104
|
+
config.modality = args.modality
|
|
105
|
+
if args.template:
|
|
106
|
+
config.template = resolve_template_path(args.template)
|
|
107
|
+
if args.template_distance_map:
|
|
108
|
+
config.template_distance_map = resolve_template_path(args.template_distance_map)
|
|
109
|
+
if args.output_extension:
|
|
110
|
+
config.output_extension = args.output_extension
|
|
111
|
+
if args.linear_transform:
|
|
112
|
+
if args.template_distance_map:
|
|
113
|
+
raise SystemExit(
|
|
114
|
+
"--linear-transform cannot be combined with --template-distance-map"
|
|
115
|
+
)
|
|
116
|
+
config.template_distance_map = None
|
|
117
|
+
else:
|
|
118
|
+
missing = []
|
|
119
|
+
if not args.model:
|
|
120
|
+
missing.append("--model")
|
|
121
|
+
if not args.modality:
|
|
122
|
+
missing.append("--modality")
|
|
123
|
+
if missing:
|
|
124
|
+
raise SystemExit(
|
|
125
|
+
"Either --config or all of --model and --modality are required.\n"
|
|
126
|
+
f"Missing: {', '.join(missing)}"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
template_distance_map = None
|
|
130
|
+
if args.linear_transform:
|
|
131
|
+
if args.template_distance_map:
|
|
132
|
+
raise SystemExit(
|
|
133
|
+
"--linear-transform cannot be combined with --template-distance-map"
|
|
134
|
+
)
|
|
135
|
+
elif args.template_distance_map:
|
|
136
|
+
template_distance_map = resolve_template_path(args.template_distance_map)
|
|
137
|
+
else:
|
|
138
|
+
template_distance_map = bundled_template("highres_template_distance.vtk")
|
|
139
|
+
|
|
140
|
+
config = PredictionConfig(
|
|
141
|
+
model=args.model,
|
|
142
|
+
template=resolve_template_path(
|
|
143
|
+
args.template or bundled_template("whole_heart_with_ao.vtp")
|
|
144
|
+
),
|
|
145
|
+
modality=args.modality,
|
|
146
|
+
template_distance_map=template_distance_map,
|
|
147
|
+
output_extension=args.output_extension,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
if args.output:
|
|
151
|
+
out_dir = args.output
|
|
152
|
+
elif args.images and len(args.images) == 1 and not args.folder:
|
|
153
|
+
out_dir = os.path.dirname(os.path.abspath(args.images[0]))
|
|
154
|
+
elif args.folder:
|
|
155
|
+
out_dir = args.folder
|
|
156
|
+
else:
|
|
157
|
+
out_dir = "."
|
|
158
|
+
|
|
159
|
+
return config, out_dir
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _run_predict(args: argparse.Namespace) -> None:
|
|
163
|
+
if not args.images and not args.folder:
|
|
164
|
+
raise SystemExit("Provide --image and/or --folder.")
|
|
165
|
+
|
|
166
|
+
if args.images:
|
|
167
|
+
for image_fn in args.images:
|
|
168
|
+
if not os.path.isfile(image_fn):
|
|
169
|
+
raise SystemExit(f"Did not find image file: {image_fn}")
|
|
170
|
+
|
|
171
|
+
if args.folder and not os.path.isdir(args.folder):
|
|
172
|
+
raise SystemExit(f"Did not find folder: {args.folder}")
|
|
173
|
+
|
|
174
|
+
config, out_dir = _resolve_config(args)
|
|
175
|
+
|
|
176
|
+
if args.images and args.folder:
|
|
177
|
+
image_files = list(args.images)
|
|
178
|
+
image_files.extend(find_image_files(args.folder, args.extension))
|
|
179
|
+
elif args.images:
|
|
180
|
+
image_files = args.images
|
|
181
|
+
elif args.folder:
|
|
182
|
+
predict_folder(
|
|
183
|
+
config,
|
|
184
|
+
args.folder,
|
|
185
|
+
out_dir,
|
|
186
|
+
extension=args.extension,
|
|
187
|
+
max_files=args.n,
|
|
188
|
+
)
|
|
189
|
+
return
|
|
190
|
+
else:
|
|
191
|
+
raise SystemExit("Provide --image and/or --folder.")
|
|
192
|
+
|
|
193
|
+
if args.n >= 0:
|
|
194
|
+
image_files = image_files[: args.n]
|
|
195
|
+
|
|
196
|
+
predict_images(config, image_files, out_dir, extension=args.extension)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def main(argv: list[str] | None = None) -> None:
|
|
200
|
+
parser = _build_parser()
|
|
201
|
+
args = parser.parse_args(argv)
|
|
202
|
+
|
|
203
|
+
if args.command == "predict":
|
|
204
|
+
_run_predict(args)
|
|
205
|
+
else:
|
|
206
|
+
parser.error(f"Unknown command: {args.command}")
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
if __name__ == "__main__":
|
|
210
|
+
main(sys.argv[1:])
|
|
File without changes
|
|
File without changes
|