I’m currently trying out itk::HoughTransform2DCirclesImageFilter::GetCircles(n), and while I get quite promising results, there still appears room for improvement of the code.
Thank you Matt! Actually I see two more possible performance improvements in the very same function!
At the beginning it makes a copy of GetOutput(0) to outputImage. Looks like an expensive operation that should not be necessary, as outputImage is only used as an input to DiscreteGaussianImageFilter. I think it might as well pass the result of GetOutput(0) directly to the filter, right?
It uses MinimumMaximumImageCalculator to find the maximum pixel value of postProcessImage, but then it iterates another time over the pixel data to find the actual index of that pixel. I think it should use MinimumMaximumImageCalculator::GetIndexOfMaximum(), right?
Hope you can help me to get these two PERF issues fixed as well
1 . The output image is copied because the the GetCircles method may be called multiple times and we don’t want to modify the output image. I would look into changing the loop to use ImageAlgorithm::Copy. It should be much faster.
The GetIndexOfMaximum seems reasonable.
You may want to look into the SmoothingRecursiveGaussianImageFilter over the Discrete.
Good catch. Yes, that looks right. One side effect we have to watch out for is unintended / unnecessary pipeline updates. Adding the PipelineMonitorImageFilter before the filter will hep to verify that unintended behavior does not occur.
Ahh! Using Image::Graft is a way to isolate an internal pipeline in a filter from the external execution. You can look at the CompositeFilterExample for reference. I believe you could replace the copy with something like:
outputImage->Graft(this->GetOutput(0));
The outputImage will still be a new image, but it will reference the same data as GetOutput(0).
Thank you very much for getting my first GetCircles patch http://review.source.kitware.com/#/c/22744/ (breaking out of loop as soon as requested number of circles is found) onto the master branch!
I still think it should be OK to directly use the data from GetOutput(0) as input to the DiscreteGaussianImageFilter, especially because the input parameter is a pointer-to-const, and the instance of DiscreteGaussianImageFilter is only a locally used object. When the function has returned, the DiscreteGaussianImageFilter and its output are gone. Right?
Do you still think that something like Image::Graft or PipelineMonitorImageFilter should be added in this specific case? I’m asking also because the unit tests now just appear to run successfully. Can you show me a test that would fail on my patch at http://review.source.kitware.com/#/c/22747/ ?
Unfortunately, the pipelining is not const correct and modification may still occur.
Yes.
This also brings up another possible area for performance optimization – if the DiscreteGaussianImageFilter can be re-used, its output will not get allocated and deallocated repeatedly.
Unnecessary pipeline updates for this filter are likely not covered by the tests and will need manual attention.
You may want to look at the ITK Softwareguide information about how the pipeline is executed. In should when update is called on a filter the update is processed all the way up the connected filters to ensure everything in the pipeline is up to date. For this case we don’t want that to happen. We want to run the DiscreteGaussian on just the “output” disconnected from the filter that generated it.
To test this cases would require complicated construction of pipelines, updates, and then modification of components of the pipeline. We generally rely on best practices for this type of thing.
As it appears less trivial than I expected to avoid the extra copying of GetOutput(0) (my patch http://review.source.kitware.com/#/c/22747/), I just posted a patch of GetCircles(n) on a different PERF issue (which does not include my proposed fix to avoid the copying). Please have a look and tell me what you think:
http://review.source.kitware.com/#/c/22748
It avoids redundant iterative searches for the maximum in the Hough Transform output image, by calling MinimumMaximumImageCalculator::GetIndexOfMaximum(), instead of GetMaximum().
When an empty image is passed as input to HoughTransform2DCirclesImageFilter, GetCircles() throws an exception. It already did so long before I started patching the function. Example:
const auto emptyImage = itk::Image<int>::New();
const auto filter = itk::HoughTransform2DCirclesImageFilter<int, int>::New();
filter->SetInput(emptyImage);
filter->Update();
filter->GetCircles(); // throws itk::InvalidRequestedRegionError.
This behavior may not be very elegant, but it doesn’t matter much to me, because it doesn’t seem to make much sense to pass an empty image to the filter. Moreover, this behavior has been like that for a very long time. I could even reproduce it on ITK 4.6.0.
or [what happens if] more circle are requested than exist?
For a non-empty input image, even when you ask for more circles than exist, you just get the number of circles you’re requesting! Again, GetCircles() already did behave like that long before I started patching. For example:
const auto image = itk::Image<int>::New();
const auto filter = itk::HoughTransform2DCirclesImageFilter<int, int>::New();
image->SetRegions({ 1, 1 });
image->Allocate(true);
filter->SetInput(image);
filter->SetNumberOfCircles(42);
filter->Update();
const auto numberOfFoundCircles = filter->GetCircles().size();
Here ‘numberOfFoundCircles’ is 42, just like the requested number of circles. I don’t think that’s very nice, but it’s the way it’s always been. At least I can reproduce it with ITK 4.6.0. None of my proposed PERF patches change this behavior.
I just submitted another patch to avoid unnecessary memory allocation and copying of pixel data from GetOutput(0), in GetCircles(n). It uses the Graft method suggested here by @blowekamp Please review: http://review.source.kitware.com/#/c/22760/
It could replace my original patch for that particular issue (which hasn’t been accepted so far), which directly passes a pointer from GetOutput(0) to the gaussianFilter: http://review.source.kitware.com/#/c/22746/ (I still think either of the two patches just work fine.)
Could it be that this parameter (n) was originally meant to allow the user to specify the number of requested circles? Currently, it doesn’t seem to be useful anymore.
I just submitted a patch to ensure that the function returns an empty list of circles, when a user has explicitly called filter->SetNumberOfCircles(0). Please review: http://review.source.kitware.com/#/c/22772/
FYI With previous ITK releases (<= ITK v4.12.2), filter->GetCircles() would typically return one or more (!) circles, when filter->SetNumberOfCircles(0) was called. Which seems very counter-intuitive to me.
I would like to mention that I think the bug fix is in accordance with the original intent of the function, as it originally tested on a variable called ‘found’, to check whether it should still continue searching for circles. However, it appeared that ‘found’ was always true, when it was tested:
filter->SetInput(image);
for (PixelType pixelValue = 0; pixelValue <= 2; ++pixelValue)
{
image->FillBuffer(pixelValue);
image->Modified();
filter->Update();
if (!filter->GetCircles().empty())
// ....
}
It appeared that doing image->Modified() was necessary to trigger a filter->GenerateData() on each iteration of the for-loop. Right? But then, shouldn’t the image already be marked as modified by image->FillBuffer(pixelValue)?
Update: While doing image->Modified() appears necessary to trigger filter->GenerateData() on each iteration of the for-loop, filter->GetCircles() only actively searches for circles on the first iteration. Is that a bug in the filter? Or should the test also do filter->Modified(), on each iteration?
Thanks for the continued contributions, @Niels_Dekker
quote=“Niels_Dekker, post:19, topic:350”]
It appeared that doing image->Modified() was necessary to trigger a filter->GenerateData() on each iteration of the for-loop. Right? But then, shouldn’t the image already be marked as modified by image->FillBuffer(pixelValue)?
[/quote]
Yes, looking into the implementation of FillBuffer(), it does not call ->Modified() internally. This may be historical, or it may be for performance reasons (we do not check to see if the image is not already filled with the value, so ->Modified() would not be correct).
Without looking into the details, it could be a bug, or it could be a deliberate performance choice.