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 ADDED
@@ -0,0 +1,3 @@
1
+ """LinFlo-Net: heart mesh generation from biomedical images."""
2
+
3
+ __version__ = "0.1.0"
linflonet/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from linflonet.cli import main
2
+
3
+ main()
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