Skip to content

Commit 4f0defe

Browse files
ENH: Initial itkHalideDiscreteGaussianImageFilter (#10)
* WIP: ENH: Add trivial halide cast image filter * WIP: FIX: Use GenerateData() rather than DynamicThreadedGenerateData() * WIP: Simple itkHalideDiscreteGaussianImpl generator stub; integrated and working. * WIP: ENH: Implement HalideDiscreteGaussian with naive schedule. Implemented with separable convolution. Handles separate sigma and kernel size for each dimension to support anisotropic images. Strip out the CastImageFilter and MinimalStandardRandomVariateGenerator placeholders. * WIP: ENH: Add naive CUDA schedule * ENH: Improved CUDA schedule. About 200ms on my machine. Bandwidth-limited on reasonable kernel/image sizes. * ENH: Generate kernels with itk::GaussianOperator for parity with itk::DiscreteGaussianImageFilter. * FIX: Disable multithreading. It wasn't used anyway, but this should make the filter better-behaved. Not sure how to disable/remove the MultiThreader object; setting it null is not valid. * FIX: Verification Test Change the test to blur CTChest. The reference output was generated with itk::DiscreteGaussianImageFilter directly, so the Halide output is verified to be numerically compatible. --------- Co-authored-by: Anthony Lombardi <[email protected]>
1 parent 61a7b3c commit 4f0defe

16 files changed

+183
-277
lines changed

CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ FetchContent_MakeAvailable(halide)
6565
set(Halide_DIR "${halide_SOURCE_DIR}/lib/cmake/Halide")
6666
set(HalideHelpers_DIR "${halide_SOURCE_DIR}/lib/cmake/HalideHelpers")
6767

68-
set(HalideFilters_LIBRARIES HalideFilters my_first_generator)
68+
set(HalideFilters_LIBRARIES HalideFilters)
6969

7070
if(NOT ITK_SOURCE_DIR)
7171
find_package(ITK REQUIRED)

examples/SampleHalidePipeline.py

Lines changed: 23 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,34 @@
11
#!/usr/bin/env python
22

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-
# Run with:
18-
# ./SampleHalidePipeline.py <input_image> <output_image>
19-
# <parameters>
20-
# e.g.
21-
# ./SampleHalidePipeline.py MyImage.mha Output.mha 2 0.2
22-
# (A rule of thumb is to set the Threshold to be about 1 / 100 of the Level.)
23-
#
24-
# parameter_1: absolute minimum...
25-
# The assumption is that...
26-
# parameter_2: controls the..
27-
# A tradeoff between...
28-
293
import argparse
4+
from time import perf_counter
305

316
import itk
327

338

349
parser = argparse.ArgumentParser(description="Example short description.")
3510
parser.add_argument("input_image")
11+
parser.add_argument("variance", type=float)
3612
parser.add_argument("output_image")
37-
parser.add_argument("parameter_1")
38-
args = parser.parse_args()
3913

40-
# Please, write a complete, self-containted and useful example that
41-
# demonstrate a class when being used along with other ITK classes or in
42-
# the context of a wider or specific application.
14+
ImageType = itk.Image[itk.F, 3]
15+
FilterType = itk.HalideDiscreteGaussianImageFilter[ImageType, ImageType]
16+
17+
if __name__ == '__main__':
18+
args = parser.parse_args()
19+
20+
im = itk.imread(args.input_image, itk.F)
21+
22+
proc = FilterType.New()
23+
proc.SetInput(im)
24+
proc.SetVariance(args.variance)
25+
26+
start = perf_counter()
27+
proc.Update()
28+
end = perf_counter()
29+
30+
delta = end - start
31+
32+
print(f'took {delta * 1_000:.3f}ms')
33+
34+
itk.imwrite(proc.GetOutput(), args.output_image)

include/itkHalideDiscreteGaussianImageFilter.h

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ namespace itk
3232
*
3333
* \ingroup HalideFilters
3434
*
35+
* Limitations compared te itkDiscreteGaussianImageFilter:
36+
* - Only supports isotropic variance and maximum error (to simplify wrapper)
37+
* - Only supports 3d images (to simplify wrapper)
38+
*
3539
*/
3640
template <typename TInputImage, typename TOutputImage>
3741
class HalideDiscreteGaussianImageFilter : public ImageToImageFilter<TInputImage, TOutputImage>
@@ -59,23 +63,42 @@ class HalideDiscreteGaussianImageFilter : public ImageToImageFilter<TInputImage,
5963
/** Standard New macro. */
6064
itkNewMacro(Self);
6165

66+
itkSetMacro(Variance, float);
67+
itkGetMacro(Variance, float);
68+
69+
itkSetMacro(MaximumError, float);
70+
itkGetMacro(MaximumError, float);
71+
72+
itkGetMacro(MaximumKernelWidth, unsigned int);
73+
itkSetMacro(MaximumKernelWidth, unsigned int);
74+
75+
itkGetMacro(UseImageSpacing, bool);
76+
itkSetMacro(UseImageSpacing, bool);
77+
itkBooleanMacro(UseImageSpacing);
78+
6279
protected:
6380
HalideDiscreteGaussianImageFilter();
64-
~HalideDiscreteGaussianImageFilter() override = default;
81+
~
82+
HalideDiscreteGaussianImageFilter() override = default;
6583

6684
void
6785
PrintSelf(std::ostream & os, Indent indent) const override;
6886

6987
using OutputRegionType = typename OutputImageType::RegionType;
7088

7189
void
72-
DynamicThreadedGenerateData(const OutputRegionType & outputRegion) override;
90+
GenerateData() override;
7391

7492
private:
7593
#ifdef ITK_USE_CONCEPT_CHECKING
7694
// Add concept checking such as
77-
// itkConceptMacro( FloatingPointPixel, ( itk::Concept::IsFloatingPoint< typename InputImageType::PixelType > ) );
95+
itkConceptMacro(FloatingPointPixel, (itk::Concept::IsFloatingPoint<typename InputImageType::PixelType>));
7896
#endif
97+
98+
float m_Variance = 0;
99+
float m_MaximumError = 0.01;
100+
unsigned int m_MaximumKernelWidth = 32;
101+
bool m_UseImageSpacing = true;
79102
};
80103
} // namespace itk
81104

include/itkHalideDiscreteGaussianImageFilter.hxx

Lines changed: 52 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -20,44 +20,79 @@
2020

2121
#include "itkHalideDiscreteGaussianImageFilter.h"
2222

23-
#include "itkImageRegionIterator.h"
24-
#include "itkImageRegionConstIterator.h"
23+
#include "itkHalideSeparableConvolutionImpl.h"
24+
25+
#include "itkGaussianOperator.h"
26+
27+
#include <Halide.h>
28+
#include <HalideBuffer.h>
29+
#include <iomanip>
2530

2631
namespace itk
2732
{
2833

2934
template <typename TInputImage, typename TOutputImage>
30-
HalideDiscreteGaussianImageFilter<TInputImage, TOutputImage>
31-
::HalideDiscreteGaussianImageFilter()
32-
{}
35+
HalideDiscreteGaussianImageFilter<TInputImage, TOutputImage>::HalideDiscreteGaussianImageFilter()
36+
{
37+
this->DynamicMultiThreadingOff();
38+
this->ThreaderUpdateProgressOff();
39+
}
3340

3441

3542
template <typename TInputImage, typename TOutputImage>
3643
void
37-
HalideDiscreteGaussianImageFilter<TInputImage, TOutputImage>
38-
::PrintSelf(std::ostream & os, Indent indent) const
44+
HalideDiscreteGaussianImageFilter<TInputImage, TOutputImage>::PrintSelf(std::ostream & os, Indent indent) const
3945
{
4046
Superclass::PrintSelf(os, indent);
4147
}
4248

4349

4450
template <typename TInputImage, typename TOutputImage>
4551
void
46-
HalideDiscreteGaussianImageFilter<TInputImage, TOutputImage>
47-
::DynamicThreadedGenerateData(const OutputRegionType & outputRegion)
52+
HalideDiscreteGaussianImageFilter<TInputImage, TOutputImage>::GenerateData()
4853
{
49-
OutputImageType * output = this->GetOutput();
50-
const InputImageType * input = this->GetInput();
51-
using InputRegionType = typename InputImageType::RegionType;
52-
InputRegionType inputRegion = InputRegionType(outputRegion.GetSize());
54+
const InputImageType * input = this->GetInput();
55+
typename InputImageType::RegionType inputRegion = input->GetBufferedRegion();
56+
typename InputImageType::SizeType inputSize = inputRegion.GetSize();
57+
typename InputImageType::SpacingType inputSpacing = input->GetSpacing();
5358

54-
itk::ImageRegionConstIterator<InputImageType> in(input, inputRegion);
55-
itk::ImageRegionIterator<OutputImageType> out(output, outputRegion);
59+
std::vector<Halide::Runtime::Buffer<float, 1>> kernel_buffers{};
5660

57-
for (in.GoToBegin(), out.GoToBegin(); !in.IsAtEnd() && !out.IsAtEnd(); ++in, ++out)
61+
// compute kernel coefficients with itk::GaussianOperator to match behavior with itk::DiscreteGaussianImageFilter
62+
for (int dim = 0; dim < InputImageDimension; ++dim)
5863
{
59-
out.Set(in.Get());
64+
GaussianOperator<float, 1> oper{};
65+
oper.SetMaximumError(m_MaximumError);
66+
oper.SetMaximumKernelWidth(m_MaximumKernelWidth);
67+
68+
float variance = m_Variance;
69+
if (m_UseImageSpacing)
70+
{
71+
variance /= inputSpacing[dim];
72+
}
73+
oper.SetVariance(variance);
74+
75+
oper.CreateDirectional();
76+
77+
Halide::Runtime::Buffer<float, 1> & buf = kernel_buffers.emplace_back(static_cast<int>(oper.GetSize(0)));
78+
buf.set_min(-static_cast<int>(oper.GetRadius(0)));
79+
std::copy(oper.Begin(), oper.End(), buf.begin());
80+
buf.set_host_dirty();
6081
}
82+
83+
OutputImageType * output = this->GetOutput();
84+
output->SetRegions(inputRegion);
85+
output->Allocate();
86+
87+
std::vector<int> sizes(3, 1);
88+
std::copy(inputSize.begin(), inputSize.end(), sizes.begin());
89+
90+
Halide::Runtime::Buffer<const InputPixelType> inputBuffer(input->GetBufferPointer(), sizes);
91+
Halide::Runtime::Buffer<OutputPixelType> outputBuffer(output->GetBufferPointer(), sizes);
92+
93+
inputBuffer.set_host_dirty();
94+
itkHalideSeparableConvolutionImpl(inputBuffer, kernel_buffers[0], kernel_buffers[1], kernel_buffers[2], outputBuffer);
95+
outputBuffer.copy_to_host();
6196
}
6297

6398
} // end namespace itk

include/itkMinimalStandardRandomVariateGenerator.h

Lines changed: 0 additions & 96 deletions
This file was deleted.

src/CMakeLists.txt

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
find_package(Halide REQUIRED shared)
22

3-
add_executable(my_generators generators.cpp)
4-
target_link_libraries(my_generators PRIVATE Halide::Generator)
5-
add_halide_library(my_first_generator FROM my_generators HEADER my_first_generator_h)
3+
add_executable(itkHalideGenerators generators.cpp)
4+
target_link_libraries(itkHalideGenerators PRIVATE Halide::Generator)
5+
6+
add_halide_library(itkHalideSeparableConvolutionImpl
7+
FROM itkHalideGenerators
8+
HEADER itkHalideSeparableConvolutionImpl_h
9+
FEATURES cuda
10+
)
611

712
set(HalideFilters_SRCS
8-
itkMinimalStandardRandomVariateGenerator.cxx
9-
${my_first_generator_h}
13+
${itkHalideSeparableConvolutionImpl_h}
1014
)
1115

1216
itk_module_add_library(HalideFilters ${HalideFilters_SRCS})
1317
target_include_directories(HalideFilters PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
18+
target_link_libraries(HalideFilters PUBLIC itkHalideSeparableConvolutionImpl)

0 commit comments

Comments
 (0)