Skip to content

Commit 89d9583

Browse files
authored
BUG:ACCESS_VIOLATION crashes and NumPy shape bug in 4D CT pipeline (#68)
* BUG:ACCESS_VIOLATION crashes and NumPy shape bug in 4D CT pipeline Four bugs fixed: 1. NumPy array shape wrong in create_distance_map (contour_tools.py) ITK GetSize() returns (Nx, Ny, Nz) but NumPy needs (Nz, Ny, Nx) ordering. np.zeros(size) was creating the wrong shape, causing an IndexError when ix >= size[2]. 2. TubeTK ResampleImage crashes ~80% on Windows (workflow_fit_statistical_model_to_patient.py) ITK's lazy SWIG DLL loader for TubeTK is intermittently unstable on Windows. Replace TubeTK.ResampleImage.SetMakeHighResIso() with a new _make_isotropic_image() helper using standard ITK ResampleImageFilter + LinearInterpolateImageFunction. Equivalent output, no TubeTK DLL needed for this path. 3. GPU libraries initialized at import time conflict with TubeTK DLL loading Several modules imported CUDA-initializing libraries at module level, which interfered with TubeTK's DLL initialization on Windows: - register_images_icon.py: torch/icon_registration/unigradicon moved into a _load_icon() lazy loader function - segment_chest_total_segmentator.py: totalsegmentator import moved inside segmentation_method() where it is used - __init__.py: `import cupy` (which initializes CUDA at import time) replaced with importlib.util.find_spec("cupy") existence check - transform_tools.py: `import cupy` moved inside the one method that uses it for GPU-accelerated norm computation 4. ITK lazy-load reference dropped before .New() (workflow_fit_statistical_model_to_patient.py) ITK 6.0b2's __getattr__ returns a temporary that can be GC'd before .New() completes. Fixed by storing the class reference before calling .New() (now superseded by the TubeTK replacement in bug #2). * BUG: Remove test of removed function * BUG: Suggestions from coderabbit * BUG: CodeRabbit * ENH: Update env for cupy commit hooks * BUG: Force commit of cupy methods until commit hooks updated
1 parent f3bf2f8 commit 89d9583

18 files changed

Lines changed: 508 additions & 496 deletions

docs/architecture.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ Primary Workflows
5555

5656
``WorkflowFitStatisticalModelToPatient``
5757
Fits a template/statistical model to patient-specific surfaces with ICP,
58-
optional PCA fitting, mask-to-mask registration, and optional image
59-
refinement.
58+
optional PCA fitting, labelmap-to-labelmap registration, and optional
59+
labelmap-to-image refinement.
6060

6161
``WorkflowReconstructHighres4DCT``
6262
Reconstructs higher-resolution 4D CT frames from a time series and a fixed

docs/cli_scripts/fit_statistical_model_to_patient.rst

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ The registration pipeline consists of four stages:
1616

1717
1. **ICP Alignment**: Rigid/affine alignment using surface matching
1818
2. **PCA Registration** (optional): Statistical shape model fitting
19-
3. **Mask-to-Mask Registration**: Greedy affine + ICON deformable registration using distance maps
20-
4. **Mask-to-Image Refinement** (optional): Final intensity-based refinement
19+
3. **Labelmap-to-Labelmap Registration**: Greedy affine + ICON deformable registration using distance maps
20+
4. **Labelmap-to-Image Refinement** (optional): Final intensity-based refinement
2121

2222
Installation
2323
============
@@ -84,7 +84,7 @@ Optional inputs:
8484

8585
``--template-labelmap PATH``
8686
Path to template labelmap image (.nii.gz, .nrrd, .mha). Required only when
87-
``--mask-to-image`` is set.
87+
``--labelmap-to-image`` is set.
8888

8989
See :class:`physiomotion4d.WorkflowFitStatisticalModelToPatient` for API documentation.
9090

@@ -112,16 +112,16 @@ PCA Registration Options
112112
Registration Configuration
113113
---------------------------
114114

115-
``--no-mask-to-mask``
116-
Disable mask-to-mask deformable registration (default: enabled)
115+
``--no-labelmap-to-labelmap``
116+
Disable labelmap-to-labelmap deformable registration (default: enabled)
117117

118-
``--mask-to-image``
119-
Enable mask-to-image refinement registration. Requires
118+
``--labelmap-to-image``
119+
Enable labelmap-to-image refinement registration. Requires
120120
``--template-labelmap`` and template label IDs. Disabled by default.
121121

122122
``--use-ICON-refinement``
123-
Enable ICON deep learning refinement in the mask-to-image stage (Stage 4).
124-
The mask-to-mask stage always uses Greedy affine + ICON deformable.
123+
Enable ICON deep learning refinement in the labelmap-to-image stage (Stage 4).
124+
The labelmap-to-labelmap stage always uses Greedy affine + ICON deformable.
125125
Default: disabled
126126

127127
Output Options
@@ -180,7 +180,7 @@ Intermediate Results
180180

181181
* ``{prefix}_icp_surface.vtp`` - Result after ICP alignment
182182
* ``{prefix}_pca_surface.vtp`` - Result after PCA fitting (if used)
183-
* ``{prefix}_m2m_surface.vtp`` - Result after mask-to-mask registration
183+
* ``{prefix}_l2l_surface.vtp`` - Result after labelmap-to-labelmap registration
184184

185185
See Also
186186
========

experiments/Heart-Create_Statistical_Model/3-registration_based_correspondence.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@
107107
moving_model=moving_model,
108108
fixed_model=fixed_model,
109109
reference_image=reference_image,
110-
roi_dilation_mm=20.0, # Dilation for ROI mask
110+
mask_dilation_mm=20.0, # Dilation for binary registration mask
111111
)
112112

113113
# Perform Greedy affine + ICON deformable registration
@@ -338,5 +338,5 @@
338338
# - The `RegisterModelsDistanceMaps` class uses a two-stage pipeline:
339339
# 1. **Greedy affine** registration (fast CPU-based alignment)
340340
# 2. **ICON deformable** registration on the affine-pre-aligned masks (deep learning)
341-
# - The `roi_dilation_mm` parameter controls the dilation of the ROI mask (default 20mm)
341+
# - The `mask_dilation_mm` parameter controls the dilation of the binary registration mask (default 20mm)
342342
# - Composed Greedy + ICON transforms provide smooth, invertible deformation fields for anatomical correspondence

experiments/Heart-Statistical_Model_To_Patient/heart_model_to_patient-CHOPValve.py

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,21 +21,21 @@
2121

2222
# %%
2323
# Patient CT image (defines coordinate frame)
24-
patient_data_dir = Path.cwd().parent.parent / "data" / "CHOP-Valve4D" / "CT"
24+
patient_data_dir = Path(__file__).parent.parent.parent / "data" / "CHOP-Valve4D" / "CT"
2525
patient_ct_path = patient_data_dir / "RVOT28-Dias.mha"
2626

2727
# Template model (moving)
28-
model_data_dir = Path.cwd().parent.parent / "data" / "KCL-Heart-Model"
28+
model_data_dir = Path(__file__).parent.parent.parent / "data" / "KCL-Heart-Model"
2929
model_labelmap_path = model_data_dir / "labelmap" / "average_labelmap_with_bkg.mha"
3030
model_pca_data_dir = (
31-
Path.cwd().parent / "Heart-Create_Statistical_Model" / "kcl-heart-model"
31+
Path(__file__).parent.parent / "Heart-Create_Statistical_Model" / "kcl-heart-model"
3232
)
3333
model_pca_json_path = model_pca_data_dir / "pca_model.json"
3434
model_mesh_path = model_pca_data_dir / "pca_mean.vtp"
3535
model_pca_n_modes = 10
3636

3737
# Output directory
38-
output_dir = Path.cwd() / "results-chop"
38+
output_dir = Path(__file__).parent / "results-chop"
3939

4040
# %%
4141
patient_image = itk.imread(str(patient_ct_path))
@@ -58,7 +58,7 @@
5858
True, pca_model=model_pca_data, pca_number_of_modes=model_pca_n_modes
5959
)
6060

61-
registrar.set_use_mask_to_mask_registration(True)
61+
# registrar.set_use_labelmap_to_labelmap_registration(True)
6262

6363
# %%
6464
patient_image = registrar.patient_image
@@ -75,10 +75,6 @@
7575

7676
registered_model.save(str(output_dir / "registered_model.vtp"))
7777
registered_model_surface.save(str(output_dir / "registered_model_surface.vtp"))
78-
registered_labelmap = results["registered_template_labelmap"]
79-
itk.imwrite(
80-
registered_labelmap, str(output_dir / "registered_labelmap.mha"), compression=True
81-
)
8278

8379
# %%
8480
pca_model = registrar.pca_template_model

experiments/Heart-Statistical_Model_To_Patient/heart_model_to_patient.py

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -145,16 +145,16 @@
145145
registrar.set_use_pca_registration(
146146
True, pca_model=pca_model, pca_number_of_modes=pca_n_modes
147147
)
148-
registrar.set_use_mask_to_image_registration(
148+
registrar.set_use_labelmap_to_image_registration(
149149
True,
150150
template_labelmap=template_labelmap,
151151
template_labelmap_organ_mesh_ids=[1],
152152
template_labelmap_organ_extra_ids=[2, 3, 4, 5],
153153
template_labelmap_background_ids=[6],
154154
)
155155

156-
registrar.set_mask_dilation_mm(0)
157-
registrar.set_roi_dilation_mm(25)
156+
registrar.set_labelmap_dilation_mm(0)
157+
registrar.set_mask_dilation_mm(25)
158158

159159
patient_image = registrar.patient_image
160160
itk.imwrite(
@@ -184,37 +184,37 @@
184184
itk.imwrite(pca_labelmap, str(output_dir / "pca_labelmap.mha"), compression=True)
185185

186186
# %% [markdown]
187-
# ## Mask Alignment
187+
# ## Labelmap Alignment
188188

189189
# %%
190190
# Perform deformable registration
191-
print("Starting deformable mask-to-mask registration...")
191+
print("Starting deformable labelmap-to-labelmap registration...")
192192

193-
m2m_results = registrar.register_mask_to_mask()
194-
m2m_inverse_transform = m2m_results["inverse_transform"]
195-
m2m_forward_transform = m2m_results["forward_transform"]
196-
m2m_model_surface = m2m_results["registered_template_model_surface"]
197-
m2m_labelmap = m2m_results["registered_template_labelmap"]
193+
l2l_results = registrar.register_labelmap_to_labelmap()
194+
l2l_inverse_transform = l2l_results["inverse_transform"]
195+
l2l_forward_transform = l2l_results["forward_transform"]
196+
l2l_model_surface = l2l_results["registered_template_model_surface"]
197+
l2l_labelmap = l2l_results["registered_template_labelmap"]
198198

199199
print("Registration complete!")
200200

201-
m2m_model_surface.save(str(output_dir / "m2m_model_surface.vtp"))
202-
itk.imwrite(m2m_labelmap, str(output_dir / "m2m_labelmap.mha"), compression=True)
201+
l2l_model_surface.save(str(output_dir / "l2l_model_surface.vtp"))
202+
itk.imwrite(l2l_labelmap, str(output_dir / "l2l_labelmap.mha"), compression=True)
203203

204204
# %%
205205
print("Starting deformable registration...")
206206
print("This may take several minutes depending on GPU availability.")
207207

208-
m2i_results = registrar.register_labelmap_to_image()
209-
m2i_inverse_transform = m2i_results["inverse_transform"]
210-
m2i_forward_transform = m2i_results["forward_transform"]
211-
m2i_surface = m2i_results["registered_template_model_surface"]
212-
m2i_labelmap = m2i_results["registered_template_labelmap"]
208+
l2i_results = registrar.register_labelmap_to_image()
209+
l2i_inverse_transform = l2i_results["inverse_transform"]
210+
l2i_forward_transform = l2i_results["forward_transform"]
211+
l2i_surface = l2i_results["registered_template_model_surface"]
212+
l2i_labelmap = l2i_results["registered_template_labelmap"]
213213
print("\nRegistration complete!")
214214

215215
# Save registration results to output folder
216-
m2i_surface.save(str(output_dir / "m2i_model_surface.vtp"))
217-
itk.imwrite(m2i_labelmap, str(output_dir / "m2i_labelmap.mha"), compression=True)
216+
l2i_surface.save(str(output_dir / "l2i_model_surface.vtp"))
217+
itk.imwrite(l2i_labelmap, str(output_dir / "l2i_labelmap.mha"), compression=True)
218218

219219
# %%
220220
tmp_p = itk.Point[itk.D, 3]()
@@ -241,16 +241,16 @@
241241
print(f"PCA transform time: {time.time() - start_time} seconds", flush=True)
242242

243243
start_time = time.time()
244-
tmp_p = registrar.m2m_inverse_transform.TransformPoint(tmp_p)
245-
print(f"M2M inverse transform time: {time.time() - start_time} seconds", flush=True)
244+
tmp_p = registrar.l2l_inverse_transform.TransformPoint(tmp_p)
245+
print(f"L2L inverse transform time: {time.time() - start_time} seconds", flush=True)
246246

247247
start_time = time.time()
248-
tmp_p = registrar.m2i_inverse_transform.TransformPoint(tmp_p)
249-
print(f"M2I inverse transform time: {time.time() - start_time} seconds", flush=True)
248+
tmp_p = registrar.l2i_inverse_transform.TransformPoint(tmp_p)
249+
print(f"L2I inverse transform time: {time.time() - start_time} seconds", flush=True)
250250

251251
# %%
252252
# Verify registration using the transform member function
253-
surface_transformed = registrar.m2i_template_model_surface
253+
surface_transformed = registrar.l2i_template_model_surface
254254
surface_transformed.save(str(output_dir / "registered_template_surface.vtp"))
255255

256256
model_transformed = registrar.transform_model()
@@ -265,8 +265,8 @@
265265
registered_surface = registrar.registered_template_model_surface
266266
icp_surface = registrar.icp_template_model_surface
267267
pca_surface = registrar.pca_template_model_surface
268-
m2m_surface = registrar.m2m_template_model_surface
269-
m2i_surface = registrar.m2i_template_model_surface
268+
l2l_surface = registrar.l2l_template_model_surface
269+
l2i_surface = registrar.l2i_template_model_surface
270270

271271
# Create side-by-side comparison
272272
plotter = pv.Plotter(shape=(1, 2))
@@ -280,7 +280,7 @@
280280
# After deformable registration
281281
plotter.subplot(0, 1)
282282
plotter.add_mesh(patient_surface, color="red", opacity=0.5, label="Patient")
283-
plotter.add_mesh(m2i_surface, color="blue", opacity=1.0, label="Registered")
283+
plotter.add_mesh(l2i_surface, color="blue", opacity=1.0, label="Registered")
284284
plotter.add_title("Final Registration")
285285

286286
plotter.link_views()

experiments/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,9 +149,9 @@ to handle diverse cases.
149149

150150
**Technologies:**
151151
- ICP (Iterative Closest Point) registration for initial alignment
152-
- Mask-based deformable registration for anatomical fitting
152+
- Labelmap-based deformable registration for anatomical fitting
153153
- PCA (Principal Component Analysis) shape modeling for shape constraints
154-
- Three-stage registration pipeline (ICP → Mask-to-MaskMask-to-Image)
154+
- Three-stage registration pipeline (ICP → Labelmap-to-LabelmapLabelmap-to-Image)
155155
- Computationally intensive (>1 hour on typical PC)
156156

157157
**Prerequisites:**

pyproject.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,8 @@ show_error_codes = true
221221
module = [
222222
"ants",
223223
"cupy",
224+
"cupy_backends",
225+
"cupy_backends.*",
224226
"icon_registration",
225227
"icon_registration.*",
226228
"itk",
@@ -270,6 +272,12 @@ module = [
270272
]
271273
disable_error_code = ["import-not-found", "import-untyped"]
272274

275+
[[tool.mypy.overrides]]
276+
# torch/icon_registration/unigradicon are lazy-loaded at runtime; string
277+
# forward-reference annotations like "torch.Size" are intentional.
278+
module = ["physiomotion4d.register_images_icon"]
279+
ignore_errors = true
280+
273281
[tool.pyright]
274282
# Third-party packages (e.g. pyvista) are in dependencies but may have no stubs;
275283
# do not report import-not-found so analysis matches mypy overrides above.
@@ -398,6 +406,8 @@ ignore = [
398406
"test_*.py" = ["S101", "PLR2004", "ARG001", "ARG002"]
399407
"tests/*.py" = ["S101", "PLR2004", "ARG001", "ARG002"]
400408
"experiments/**/*.py" = ["T201", "S101"]
409+
# torch/icon_registration are lazy-loaded at runtime; forward-reference strings are intentional
410+
"src/physiomotion4d/register_images_icon.py" = ["F821"]
401411

402412
[tool.ruff.lint.isort]
403413
known-first-party = ["physiomotion4d"]

src/physiomotion4d/__init__.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,10 @@
1919

2020
__version__ = "2026.05.9"
2121

22+
import importlib.util as _importlib_util
2223
import warnings as _warnings
2324

24-
try:
25-
import cupy as _cupy # noqa: F401
26-
except ImportError:
25+
if _importlib_util.find_spec("cupy") is None:
2726
_warnings.warn(
2827
"CuPy is not installed — GPU acceleration is unavailable and processing "
2928
"will be slow. Re-install with uv to get CuPy and CUDA-enabled PyTorch "

0 commit comments

Comments
 (0)