Skip to content

Commit 956f434

Browse files
committed
Address #2371
Add SignedMaurerDistanceMapImageFilter as default distance transform method This change makes SignedMaurerDistanceMapImageFilter the default method for computing distance transforms, replacing ReinitializeLevelSetImageFilter. The new default is faster and more robust, working directly on segmentations without requiring anti-aliasing. Changes: - Add DistanceTransformType enum to Image class with SignedMaurer and ReinitializeLevelSet options - Update Image::computeDT() to accept method parameter with SignedMaurer as default - Expose DistanceTransformType enum in Python bindings - Add --method option to CLI compute-dt command (choices: "maurer", "levelset") - Update existing tests to explicitly use ReinitializeLevelSet method to match existing ground truth files - Add new tests to verify SignedMaurer method and default behavior - Update GroomTests baseline to use new default method - Maintain backward compatibility by keeping legacy method available The legacy ReinitializeLevelSetImageFilter remains available for cases where the old behavior is needed.
1 parent f50ad2f commit 956f434

File tree

8 files changed

+73
-22
lines changed

8 files changed

+73
-22
lines changed

Applications/shapeworks/ImageCommands.cpp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -548,6 +548,8 @@ void ComputeDT::buildParser()
548548
parser.prog(prog).description(desc);
549549

550550
parser.add_option("--isovalue").action("store").type("double").set_default(0.0).help("Level set value that defines the interface between foreground and background [default: %default].");
551+
std::list<std::string> methods{"maurer", "levelset"};
552+
parser.add_option("--method").action("store").type("choice").choices(methods.begin(), methods.end()).set_default("maurer").help("Distance transform method: 'maurer' (SignedMaurerDistanceMapImageFilter - faster, more robust, default) or 'levelset' (ReinitializeLevelSetImageFilter - legacy) [default: %default].");
551553

552554
Command::buildParser();
553555
}
@@ -561,8 +563,10 @@ bool ComputeDT::execute(const optparse::Values &options, SharedCommandData &shar
561563
}
562564

563565
double isoValue = static_cast<double>(options.get("isovalue"));
566+
std::string method_str(static_cast<std::string>(options.get("method")));
567+
Image::DistanceTransformType method = (method_str == "levelset") ? Image::ReinitializeLevelSet : Image::SignedMaurer;
564568

565-
sharedData.image.computeDT(isoValue);
569+
sharedData.image.computeDT(isoValue, method);
566570
return true;
567571
}
568572

Libs/Image/Image.cpp

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
#include <itkOrientImageFilter.h>
2525
#include <itkRegionOfInterestImageFilter.h>
2626
#include <itkReinitializeLevelSetImageFilter.h>
27+
#include <itkSignedMaurerDistanceMapImageFilter.h>
2728
#include <itkRelabelComponentImageFilter.h>
2829
#include <itkResampleImageFilter.h>
2930
#include <itkScalableAffineTransform.h>
@@ -628,15 +629,28 @@ Image& Image::binarize(PixelType minVal, PixelType maxVal, PixelType innerVal, P
628629
return *this;
629630
}
630631

631-
Image& Image::computeDT(PixelType isoValue) {
632-
using FilterType = itk::ReinitializeLevelSetImageFilter<ImageType>;
633-
FilterType::Pointer filter = FilterType::New();
634-
635-
filter->SetInput(this->itk_image_);
636-
filter->NarrowBandingOff();
637-
filter->SetLevelSetValue(isoValue);
638-
filter->Update();
639-
this->itk_image_ = filter->GetOutput();
632+
Image& Image::computeDT(PixelType isoValue, DistanceTransformType method) {
633+
if (method == SignedMaurer) {
634+
using FilterType = itk::SignedMaurerDistanceMapImageFilter<ImageType, ImageType>;
635+
FilterType::Pointer filter = FilterType::New();
636+
637+
filter->SetInput(this->itk_image_);
638+
filter->SetBackgroundValue(isoValue);
639+
filter->SetInsideIsPositive(false);
640+
filter->SetSquaredDistance(false);
641+
filter->SetUseImageSpacing(true);
642+
filter->Update();
643+
this->itk_image_ = filter->GetOutput();
644+
} else { // ReinitializeLevelSet
645+
using FilterType = itk::ReinitializeLevelSetImageFilter<ImageType>;
646+
FilterType::Pointer filter = FilterType::New();
647+
648+
filter->SetInput(this->itk_image_);
649+
filter->NarrowBandingOff();
650+
filter->SetLevelSetValue(isoValue);
651+
filter->Update();
652+
this->itk_image_ = filter->GetOutput();
653+
}
640654

641655
return *this;
642656
}

Libs/Image/Image.h

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,14 @@ class Mesh;
2828
class Image {
2929
public:
3030
enum InterpolationType { Linear, NearestNeighbor };
31+
enum DistanceTransformType { SignedMaurer, ReinitializeLevelSet };
3132

3233
using PixelType = float;
3334
using ImageType = itk::Image<PixelType, 3>;
3435
using StatsPtr = itk::StatisticsImageFilter<ImageType>::Pointer;
3536
using ImageIterator = itk::ImageRegionIterator<ImageType>;
3637
using InterpolatorType = itk::LinearInterpolateImageFunction<ImageType>;
37-
38+
3839
// constructors and assignment operators //
3940
explicit Image(const Dims dims);
4041
explicit Image(const std::string& pathname) : itk_image_(read(pathname)) {}
@@ -161,7 +162,7 @@ class Image {
161162
PixelType innerVal = 1.0, PixelType outerVal = 0.0);
162163

163164
/// computes distance transform volume from a (preferably antialiased) binary image using the specified isovalue
164-
Image& computeDT(PixelType isoValue = 0.0);
165+
Image& computeDT(PixelType isoValue = 0.0, DistanceTransformType method = SignedMaurer);
165166

166167
/// denoises an image using curvature driven flow using curvature flow image filter
167168
Image& applyCurvatureFilter(unsigned iterations = 10);

Libs/Python/ShapeworksPython.cpp

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,12 @@ PYBIND11_MODULE(shapeworks_py, m) {
9494
.value("NearestNeighbor", Image::InterpolationType::NearestNeighbor)
9595
.export_values();
9696

97+
// Image::DistanceTransformType
98+
py::enum_<Image::DistanceTransformType>(m, "DistanceTransformType")
99+
.value("SignedMaurer", Image::DistanceTransformType::SignedMaurer)
100+
.value("ReinitializeLevelSet", Image::DistanceTransformType::ReinitializeLevelSet)
101+
.export_values();
102+
97103
m.def(
98104
"mean", [](py::array& field) { return mean(pyToArr(field, false /*take_ownership*/)); },
99105
"incrementally compute (single-component) mean of field");
@@ -323,7 +329,8 @@ PYBIND11_MODULE(shapeworks_py, m) {
323329
"outerVal"_a = 0.0)
324330

325331
.def("computeDT", &Image::computeDT,
326-
"computes signed distance transform volume from an image at the specified isovalue", "isovalue"_a = 0.0)
332+
"computes signed distance transform volume from an image at the specified isovalue using the specified method (SignedMaurer is default, faster and more robust)",
333+
"isovalue"_a = 0.0, "method"_a = Image::DistanceTransformType::SignedMaurer)
327334

328335
.def("applyCurvatureFilter", &Image::applyCurvatureFilter,
329336
"denoises an image using curvature driven flow using curvature flow image filter", "iterations"_a = 10)

Testing/ImageTests/ImageTests.cpp

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -280,20 +280,42 @@ TEST(ImageTests, binarizeTest2) {
280280

281281
TEST(ImageTests, computedtTest1) {
282282
Image image(std::string(TEST_DATA_DIR) + "/1x2x2.nrrd");
283-
image.computeDT();
283+
// Use legacy method to match existing ground truth
284+
image.computeDT(0.0, Image::ReinitializeLevelSet);
284285
Image ground_truth(std::string(TEST_DATA_DIR) + "/computedt1.nrrd");
285286

286287
ASSERT_TRUE(image == ground_truth);
287288
}
288289

289290
TEST(ImageTests, computedtTest2) {
290291
Image image(std::string(TEST_DATA_DIR) + "/1x2x2.nrrd");
291-
image.computeDT(1.0);
292+
// Use legacy method to match existing ground truth
293+
image.computeDT(1.0, Image::ReinitializeLevelSet);
292294
Image ground_truth(std::string(TEST_DATA_DIR) + "/computedt2.nrrd");
293295

294296
ASSERT_TRUE(image == ground_truth);
295297
}
296298

299+
// Test the new default SignedMaurer method
300+
TEST(ImageTests, computedtTest3) {
301+
Image image(std::string(TEST_DATA_DIR) + "/1x2x2.nrrd");
302+
// Use new default SignedMaurer method - should work without anti-aliasing
303+
image.computeDT(0.0, Image::SignedMaurer);
304+
305+
// Should produce a distance transform (negative inside, positive outside)
306+
ASSERT_TRUE(image.isDistanceTransform());
307+
}
308+
309+
TEST(ImageTests, computedtTest4) {
310+
Image image(std::string(TEST_DATA_DIR) + "/1x2x2.nrrd");
311+
// Test that default is SignedMaurer
312+
Image image_default(std::string(TEST_DATA_DIR) + "/1x2x2.nrrd");
313+
image.computeDT(0.0, Image::SignedMaurer);
314+
image_default.computeDT(0.0); // Should use SignedMaurer by default
315+
316+
ASSERT_TRUE(image == image_default);
317+
}
318+
297319
TEST(ImageTests, curvatureTest) {
298320
Image image(std::string(TEST_DATA_DIR) + "/1x2x2.nrrd");
299321
image.applyCurvatureFilter();

Testing/PythonTests/computedt.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66

77
def computedtTest1():
88
img = Image(os.environ["DATA"] + "/1x2x2.nrrd")
9-
img.computeDT()
9+
# Use legacy method to match existing ground truth
10+
img.computeDT(0.0, DistanceTransformType.ReinitializeLevelSet)
1011

1112
compareImg = Image(os.environ["DATA"] + "/computedt1.nrrd")
1213

@@ -16,7 +17,8 @@ def computedtTest1():
1617

1718
def computedtTest2():
1819
img = Image(os.environ["DATA"] + "/1x2x2.nrrd")
19-
img.computeDT(1.0)
20+
# Use legacy method to match existing ground truth
21+
img.computeDT(1.0, DistanceTransformType.ReinitializeLevelSet)
2022

2123
compareImg = Image(os.environ["DATA"] + "/computedt2.nrrd")
2224

@@ -26,7 +28,8 @@ def computedtTest2():
2628

2729
def computedtTest3():
2830
img = Image(os.environ["DATA"] + "/1x2x2.nrrd")
29-
img.computeDT(-1.0)
31+
# Use legacy method to match existing ground truth
32+
img.computeDT(-1.0, DistanceTransformType.ReinitializeLevelSet)
3033

3134
compareImg = Image(os.environ["DATA"] + "/computedt3.nrrd")
3235

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
version https://git-lfs.github.com/spec/v1
2-
oid sha256:849b13d964d22c6835946981fcbe1e9bd1e26625f7356075ba8f19660c73f65f
3-
size 62838
2+
oid sha256:1f6f60eca442bc9341779f907019897fa6eecd4880f71a5ed06c57d9cf915d52
3+
size 220399
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
version https://git-lfs.github.com/spec/v1
2-
oid sha256:adddca3e41326bdb30cdaba8f0a094d46c544013047b98e9083430ce2db67465
3-
size 169723
2+
oid sha256:b7a3c296a7c4bc40c75501a80147b78a962188ad7cce21a194523a7b0b787ab3
3+
size 165912

0 commit comments

Comments
 (0)