Skip to content

Commit 1690b4d

Browse files
committed
ENH: Add the Torch-backed dense registration pipeline + examples
A fast, self-contained two-stage registration built on the IMPACT core, in the spirit of ConvexAdam but driven by anatomical features rather than raw intensities: - itk::ImpactCoarseRegistration stage 1 (coarse): a discrete SSD cost volume over a dense displacement window on a pooled coarse grid, coupled-convex global regularization, upsampled to a full-resolution initial field. Robust to large misalignments; 2D and 3D. - itk::ImpactFineRegistration stage 2 (fine): holds the displacement field as a GPU leaf tensor, warps with grid_sample, and minimizes an IMPACT feature (or intensity) loss plus a diffusion regularizer with torch::optim::Adam — the whole warp + comparison + optimizer in a single autograd graph. Optional low-resolution control grid and PCA channel reduction. Both emit a geometry-correct itk::DisplacementFieldTransform (physical millimetres, fixed->moving). Adds the Python wrapping, the GoogleTest backend suite (with a tiny committed TorchScript toy model), and C++/Python metric-registration examples.
1 parent bc834f0 commit 1690b4d

19 files changed

Lines changed: 3890 additions & 11 deletions

examples/CMakeLists.txt

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#==========================================================================
2+
#
3+
# Copyright NumFOCUS
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# https://www.apache.org/licenses/LICENSE-2.0.txt
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
#
17+
#==========================================================================
18+
19+
cmake_minimum_required(VERSION 3.22.1...3.29.0 FATAL_ERROR)
20+
project(ImpactExamples)
21+
22+
set(ExampleSpecificComponents
23+
Impact
24+
)
25+
26+
if(NOT ITK_SOURCE_DIR)
27+
find_package(ITK REQUIRED COMPONENTS ITKImageIO ITKTransformIO ${ExampleSpecificComponents})
28+
else()
29+
# When being built as part of ITK, ITKImageIO and ITKTransformIO
30+
# lists of modules are not yet ready, causing a configure error
31+
find_package(ITK REQUIRED COMPONENTS ${ExampleSpecificComponents})
32+
endif()
33+
include(${ITK_USE_FILE})
34+
35+
add_executable(ImpactMetricExample ImpactMetricExample.cxx )
36+
target_link_libraries(ImpactMetricExample ${ITK_LIBRARIES})

examples/ImpactMetricExample.cxx

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/*=========================================================================
2+
*
3+
* Copyright NumFOCUS
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* https://www.apache.org/licenses/LICENSE-2.0.txt
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*
17+
*=========================================================================*/
18+
19+
// IMPACT: semantic similarity registration from pretrained TorchScript features.
20+
//
21+
// This example mirrors ImpactMetricExample.py and shows the two ITK-facing pieces of
22+
// the module. No torch type ever crosses the API: it speaks only ITK/STL types (images,
23+
// a model-path string, a device string).
24+
//
25+
// 1. the core: itk::ImageToFeaturesMap extracts a feature map from an image;
26+
// 2. the metric: itk::ImpactImageToImageMetricv4 plugs into
27+
// itk::ImageRegistrationMethodv4 to register two images by comparing
28+
// those features instead of raw intensities.
29+
30+
#include "itkImageToFeaturesMap.h"
31+
#include "itkImpactImageToImageMetricv4.h"
32+
#include "itkModelConfiguration.h"
33+
34+
#include <itkImageFileReader.h>
35+
#include <itkImageFileWriter.h>
36+
#include <itkImageRegistrationMethodv4.h>
37+
#include <itkRegularStepGradientDescentOptimizerv4.h>
38+
#include <itkTranslationTransform.h>
39+
#include <itkResampleImageFilter.h>
40+
#include <itkBSplineInterpolateImageFunction.h>
41+
42+
int
43+
main(int argc, char * argv[])
44+
{
45+
if (argc < 5)
46+
{
47+
std::cerr << "IMPACT: semantic similarity registration from pretrained features.\n";
48+
std::cerr << "Usage: " << argv[0] << " model.pt fixedImage movingImage outputWarpedImage [device]\n";
49+
std::cerr << " device: \"cpu\" (default), \"cuda\", \"cuda:0\", ...\n";
50+
return EXIT_FAILURE;
51+
}
52+
const char * const modelPath = argv[1];
53+
const char * const fixedPath = argv[2];
54+
const char * const movingPath = argv[3];
55+
const char * const outputPath = argv[4];
56+
const std::string device = (argc > 5) ? argv[5] : "cpu";
57+
58+
constexpr unsigned int Dimension = 3;
59+
using PixelType = float;
60+
using ImageType = itk::Image<PixelType, Dimension>;
61+
62+
const auto fixed = itk::ReadImage<ImageType>(fixedPath);
63+
const auto moving = itk::ReadImage<ImageType>(movingPath);
64+
65+
// A TorchScript model configuration: (path, dimension, channels, patchSize, voxelSize,
66+
// overlap, layersMask, mixedPrecision). Only POD/STL types cross the API.
67+
const itk::ModelConfiguration config(
68+
modelPath, Dimension, 1, { 0, 0, 0 }, { 1.0f, 1.0f, 1.0f }, 2, { true }, false);
69+
70+
// --- 1. core: extract a dense feature map from the fixed image --------------------
71+
using InterpolatorType = itk::BSplineInterpolateImageFunction<ImageType, double>;
72+
auto interpolator = InterpolatorType::New();
73+
interpolator->SetSplineOrder(3);
74+
75+
auto features = itk::ImageToFeaturesMap<ImageType, InterpolatorType>::New();
76+
features->SetModelConfiguration(config);
77+
features->SetInterpolator(interpolator);
78+
features->SetDevice(device);
79+
features->AddInput(fixed);
80+
features->Update();
81+
const auto featureMap = features->GetOutput(0); // itk::VectorImage<float, 3>
82+
std::cout << "feature map: " << featureMap->GetLargestPossibleRegion().GetSize()
83+
<< " channels: " << featureMap->GetNumberOfComponentsPerPixel() << std::endl;
84+
85+
// --- 2. metric: register moving onto fixed by comparing anatomical features -------
86+
using MetricType = itk::ImpactImageToImageMetricv4<ImageType, ImageType>;
87+
auto metric = MetricType::New();
88+
std::vector<itk::ModelConfiguration> models{ config };
89+
metric->SetModelsConfiguration(models);
90+
metric->SetDistance({ "NCC" }); // per-layer loss: L1, L2, NCC, Cosine, Dice, ...
91+
metric->SetLayersWeight({ 1.0f });
92+
metric->SetSubsetFeatures({ 4 }); // random channel subset for speed (0 = all)
93+
metric->SetPCA({ 0 });
94+
metric->SetMode("Static"); // "Static" (precomputed features) or "Jacobian"
95+
metric->SetDevice(device);
96+
97+
using TransformType = itk::TranslationTransform<double, Dimension>;
98+
auto transform = TransformType::New();
99+
transform->SetIdentity();
100+
101+
using OptimizerType = itk::RegularStepGradientDescentOptimizerv4<double>;
102+
auto optimizer = OptimizerType::New();
103+
optimizer->SetNumberOfIterations(200);
104+
optimizer->SetLearningRate(2.0);
105+
optimizer->SetMinimumStepLength(1e-4);
106+
107+
using RegistrationType = itk::ImageRegistrationMethodv4<ImageType, ImageType, TransformType>;
108+
auto registration = RegistrationType::New();
109+
registration->SetFixedImage(fixed);
110+
registration->SetMovingImage(moving);
111+
registration->SetMetric(metric);
112+
registration->SetOptimizer(optimizer);
113+
registration->SetInitialTransform(transform);
114+
115+
// Single resolution level (no shrink, no smoothing).
116+
RegistrationType::ShrinkFactorsArrayType shrinkFactors(1);
117+
RegistrationType::SmoothingSigmasArrayType smoothingSigmas(1);
118+
shrinkFactors[0] = 1;
119+
smoothingSigmas[0] = 0;
120+
registration->SetNumberOfLevels(1);
121+
registration->SetShrinkFactorsPerLevel(shrinkFactors);
122+
registration->SetSmoothingSigmasPerLevel(smoothingSigmas);
123+
124+
registration->Update();
125+
std::cout << "recovered parameters: " << registration->GetTransform()->GetParameters() << std::endl;
126+
127+
// --- 3. resample the moving image with the recovered transform --------------------
128+
auto resample = itk::ResampleImageFilter<ImageType, ImageType>::New();
129+
resample->SetInput(moving);
130+
resample->SetTransform(registration->GetTransform());
131+
resample->SetUseReferenceImage(true);
132+
resample->SetReferenceImage(fixed);
133+
resample->SetDefaultPixelValue(0);
134+
itk::WriteImage(resample->GetOutput(), outputPath);
135+
136+
return EXIT_SUCCESS;
137+
}

examples/ImpactMetricExample.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
#!/usr/bin/env python
2+
#==========================================================================
3+
#
4+
# Copyright NumFOCUS
5+
#
6+
# Licensed under the Apache License, Version 2.0 (the "License");
7+
# you may not use this file except in compliance with the License.
8+
# You may obtain a copy of the License at
9+
#
10+
# https://www.apache.org/licenses/LICENSE-2.0.txt
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
#
18+
#==========================================================================
19+
20+
"""IMPACT: semantic similarity registration from pretrained TorchScript features.
21+
22+
This example shows the two Python-facing pieces of the module -- note that no
23+
``import torch`` is needed: all LibTorch work happens inside the C++ implementation,
24+
and the public API speaks only ITK/STL types (images, a model-path string, a device
25+
string).
26+
27+
1. the core: ``itk.ImageToFeaturesMap`` extracts a feature map (an ``itk.VectorImage``)
28+
from an image with a TorchScript model;
29+
2. the metric: ``itk.ImpactImageToImageMetricv4`` plugs into
30+
``itk.ImageRegistrationMethodv4`` to register two images by comparing those features.
31+
32+
Run with: ./ImpactMetricExample.py model.pt fixed.mha moving.mha
33+
"""
34+
35+
import argparse
36+
import itk
37+
38+
parser = argparse.ArgumentParser(description="IMPACT feature extraction + registration demo.")
39+
parser.add_argument("model", help="TorchScript model (.pt) returning a list of feature maps")
40+
parser.add_argument("fixed_image")
41+
parser.add_argument("moving_image")
42+
parser.add_argument("--device", default="cpu", help='"cpu", "cuda", "cuda:0", ...')
43+
args = parser.parse_args()
44+
45+
Dimension = 3
46+
PixelType = itk.F
47+
ImageType = itk.Image[PixelType, Dimension]
48+
49+
fixed = itk.imread(args.fixed_image, PixelType)
50+
moving = itk.imread(args.moving_image, PixelType)
51+
52+
# A TorchScript model configuration: (path, dimension, channels, patchSize, voxelSize,
53+
# overlap, layersMask, mixedPrecision). All POD/STL -- no torch type crosses to Python.
54+
config = itk.ModelConfiguration(
55+
args.model, Dimension, 1, [0, 0, 0], [1.0, 1.0, 1.0], 2, [True], False
56+
)
57+
58+
# --- 1. core: extract a feature map -----------------------------------------------
59+
InterpolatorType = itk.BSplineInterpolateImageFunction[ImageType, itk.D]
60+
interpolator = InterpolatorType.New()
61+
interpolator.SetSplineOrder(3)
62+
63+
features = itk.ImageToFeaturesMap[ImageType, InterpolatorType].New()
64+
features.SetModelConfiguration(config)
65+
features.SetInterpolator(interpolator)
66+
features.SetDevice(args.device)
67+
features.AddInput(fixed)
68+
features.Update()
69+
feature_map = features.GetOutput(0) # itk.VectorImage[itk.F, 3]
70+
print("feature map:", feature_map.GetLargestPossibleRegion().GetSize(),
71+
"channels:", feature_map.GetNumberOfComponentsPerPixel())
72+
73+
# --- 2. metric: register moving onto fixed ----------------------------------------
74+
metric = itk.ImpactImageToImageMetricv4[ImageType, ImageType].New()
75+
metric.SetFixedImage(fixed)
76+
metric.SetMovingImage(moving)
77+
metric.SetFixedModelsConfiguration([config])
78+
metric.SetMovingModelsConfiguration([config])
79+
metric.SetDistance(["NCC"])
80+
metric.SetLayersWeight([1.0])
81+
metric.SetSubsetFeatures([4])
82+
metric.SetPCA([0])
83+
metric.SetMode("Static")
84+
metric.SetDevice(args.device)
85+
86+
transform = itk.TranslationTransform[itk.D, Dimension].New()
87+
transform.SetIdentity()
88+
metric.SetMovingTransform(transform)
89+
metric.SetFixedTransform(itk.IdentityTransform[itk.D, Dimension].New())
90+
91+
optimizer = itk.RegularStepGradientDescentOptimizerv4[itk.D].New()
92+
optimizer.SetNumberOfIterations(200)
93+
optimizer.SetLearningRate(2.0)
94+
optimizer.SetMinimumStepLength(1e-4)
95+
96+
registration = itk.ImageRegistrationMethodv4[ImageType, ImageType].New()
97+
registration.SetFixedImage(fixed)
98+
registration.SetMovingImage(moving)
99+
registration.SetMetric(metric)
100+
registration.SetOptimizer(optimizer)
101+
registration.SetInitialTransform(transform)
102+
registration.SetNumberOfLevels(1)
103+
registration.Update()
104+
105+
print("recovered transform parameters:", list(registration.GetTransform().GetParameters()))

0 commit comments

Comments
 (0)