Skip to content

Instantly share code, notes, and snippets.

@N-Dekker
Last active April 6, 2019 11:57
Show Gist options
  • Save N-Dekker/9e1f5bcc6f65c7652c11d1cbd3d519ee to your computer and use it in GitHub Desktop.
Save N-Dekker/9e1f5bcc6f65c7652c11d1cbd3d519ee to your computer and use it in GitHub Desktop.
EstimateAxisAlignedBoundingBoxRegion (ITK5)
/*
Estimates the axis-aligned minimum bounding box of the buffered region of an image.
Roughly equivalent to ImageMaskSpatialObject::GetAxisAlignedBoundingBoxRegion() from
"itkImageMaskSpatialObject.hxx", at
github.com/InsightSoftwareConsortium/ITK/blob/91ab86f63eb5fc9799bb25fa49e76adf0d1b5251/Modules/Core/SpatialObjects/include
Related to:
"ENH: GTest for ImageMaskSpatialObject::GetAxisAlignedBoundingBoxRegion()"
https://github.com/InsightSoftwareConsortium/ITK/pull/681
"ENH: Add legacy interface to SpatialObjects"
https://github.com/InsightSoftwareConsortium/ITK/pull/676/
"ENH: Added ImageSpatialObject::GetAxisAlignedBoundingBoxRegion()"
https://github.com/InsightSoftwareConsortium/ITK/pull/645
"How to replace ImageMaskSpatialObject::GetAxisAlignedBoundingBoxRegion() calls?"
https://discourse.itk.org/t/how-to-replace-imagemaskspatialobject-getaxisalignedboundingboxregion-calls/1709
Niels Dekker, LKEB, Leiden University Medical Center, 2019
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
*/
#include <itkImage.h>
#include <itkImageRegion.h>
#include <itkImageRegionConstIterator.h>
namespace LKEB
{
template<typename TImage>
typename TImage::RegionType EstimateAxisAlignedBoundingBoxRegion(const TImage& image)
{
using namespace itk;
using PixelType = typename TImage::PixelType;
constexpr auto VImageDimension = TImage::ImageDimension;
using RegionType = ImageRegion<VImageDimension>;
using SizeType = Size<VImageDimension>;
using IndexType = Index<VImageDimension>;
const auto HasForegroundPixels = [&image](const RegionType& region)
{
for (ImageRegionConstIterator<TImage> it{ &image, region }; !it.IsAtEnd(); ++it)
{
constexpr auto zeroValue = NumericTraits<PixelType>::ZeroValue();
if (it.Get() != zeroValue)
{
return true;
}
}
return false;
};
const auto CreateRegion = [](const IndexType& minIndex, const IndexType& maxIndex)
{
SizeType regionSize;
for (unsigned dim = 0; dim < SizeType::Dimension; ++dim)
{
regionSize[dim] = static_cast<SizeValueType>(maxIndex[dim] + 1 - minIndex[dim]);
}
return RegionType{ minIndex, regionSize };
};
const RegionType requestedRegion = image.GetRequestedRegion();
if (requestedRegion.GetNumberOfPixels() == 0)
{
return {};
}
const SizeType imageSize = requestedRegion.GetSize();
IndexType minIndex = requestedRegion.GetIndex();
IndexType maxIndex = minIndex + imageSize;
for (auto& maxIndexValue : maxIndex)
{
--maxIndexValue;
}
// Iterate from high to low (for significant performance reasons).
for (int dim = VImageDimension - 1; dim >= 0; --dim)
{
auto subregion = CreateRegion(minIndex, maxIndex);
subregion.SetSize(dim, 1);
const auto initialMaxIndexValue = maxIndex[dim];
// Estimate minIndex[dim]
while(!HasForegroundPixels(subregion))
{
const auto indexValue = subregion.GetIndex(dim) + 1;
if (indexValue > initialMaxIndexValue)
{
// The requested image region has only zero-valued pixels.
return {};
}
subregion.SetIndex(dim, indexValue);
}
minIndex[dim] = subregion.GetIndex(dim);
// Estimate maxIndex[dim]
subregion.SetIndex(dim, initialMaxIndexValue);
while (!HasForegroundPixels(subregion))
{
subregion.SetIndex(dim, subregion.GetIndex(dim) - 1);
}
maxIndex[dim] = subregion.GetIndex(dim);
}
return CreateRegion(minIndex, maxIndex);
}
}
@aylward
Copy link

aylward commented Apr 4, 2019

Nice!

One suggestion - line 74 makes a pass through the image, verifying that there is at least one non-zero voxel. That makes this function O(2N-1) in the worse case. Also, if the masked region is small, then the actual runtime is near O(2N-c). Perhaps it would be better to eliminate the test in line 74, and instead make the loops in lines 89 and 102 conditional on minIndex[i]<maxIndex[i] and vice versa?

@N-Dekker
Copy link
Author

N-Dekker commented Apr 4, 2019

Thanks for doing such a thorough code reading, @aylward I found it easier to just skip any image that are entirely black, to keep the loop conditions simple. But it may indeed be worth considering to eliminate the test in line 74, and adjust the loops, as you suggested. I would have to see the performance results.

At least, this implementation (revision 5) is already a huge performance improvement, compared to the old (legacy) ITK GetAxisAlignedBoundingBoxRegion()! I did a benchmark:

2D (1024x1024):
Entirely black image: > 7 x as fast
Black except for 1 white center pixel: > 4 x as fast
Entirely white image: > 5000 x as fast!!!

3D (256x256x256):
Entirely black image: > 80 x as fast
Black except for 1 white center voxel: > 30 x as fast
Entirely white image: > 30 x as fast

Quite promising, isn't it? :-) A problem with further optimization could be that it improved the performance of one use case at the cost of another use case. Should I care about the performance on an entirely black image, or is that an irrelevant use case?

@N-Dekker
Copy link
Author

N-Dekker commented Apr 5, 2019

@aylward Revision 6 includes your suggestion to skip verifying that there is at least one non-zero voxel :-)

@N-Dekker
Copy link
Author

N-Dekker commented Apr 6, 2019

Revision 7 has added a check if the requested region is empty, and somewhat simplified the code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment