From 0039d0c7fc1181ce0ce540c5eaf28d00871451ec Mon Sep 17 00:00:00 2001 From: Bradley Lowekamp Date: Wed, 8 Apr 2009 12:31:09 -0400 Subject: [PATCH 01/13] BUG: 8729 Problem using CannyImageFilter with non-zero index in region. Removed private InBounds met Applies ITK commit 0a20088c62a (2009-04-08) to the copy of itk::CannyEdgeDetectionImageFilter. --- ...dgeDetectionRecursiveGaussianImageFilter.h | 7 ++---- ...eDetectionRecursiveGaussianImageFilter.hxx | 22 ++++--------------- 2 files changed, 6 insertions(+), 23 deletions(-) diff --git a/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.h b/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.h index 0c1df5b2..fea112ca 100644 --- a/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.h +++ b/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.h @@ -134,7 +134,8 @@ class ITK_TEMPLATE_EXPORT CannyEdgeDetectionRecursiveGaussianImageFilter itkNewMacro(Self); /** Typedef to describe the output image region type. */ - using OutputImageRegionType = typename TOutputImage::RegionType; + typedef typename TOutputImage::RegionType OutputImageRegionType; + typedef typename TInputImage::RegionType InputImageRegionType; /** Run-time type information (and related methods). */ itkOverrideGetNameOfClassMacro(CannyEdgeDetectionRecursiveGaussianImageFilter); @@ -242,10 +243,6 @@ class ITK_TEMPLATE_EXPORT CannyEdgeDetectionRecursiveGaussianImageFilter void FollowEdge(IndexType index); - /** Check if the index is in bounds or not */ - bool - InBounds(IndexType index); - /** Calculate the second derivative of the smoothed image, it writes the * result to m_UpdateBuffer using the ThreadedCompute2ndDerivative() method diff --git a/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.hxx b/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.hxx index cab667ea..3ed52da7 100644 --- a/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.hxx +++ b/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.hxx @@ -112,6 +112,7 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Gener } // Set the kernel size. + // fix me this needs to be based on the variance of the gaussian filter unsigned long radius = 1; // get a copy of the input requested region (should equal the output @@ -363,6 +364,7 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Hyste ListNodeType * node; + // fix me ImageRegionIterator oit(input, input->GetRequestedRegion()); oit.GoToBegin(); @@ -399,6 +401,7 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Follo // gradients of the image. HysteresisThresholding of this image should give // the Canny output. typename OutputImageType::Pointer input = m_MultiplyImageFilter->GetOutput(); + InputImageRegionType inputRegion = input->GetRequestedRegion(); IndexType nIndex; IndexType cIndex; @@ -436,7 +439,7 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Follo { nIndex = oit.GetIndex(i); uit.SetIndex(nIndex); - if (InBounds(nIndex)) + if (inputRegion.IsInside(nIndex)) { if (oit.GetPixel(i) > m_LowerThreshold && uit.Value() != 1) { @@ -452,23 +455,6 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Follo } } -template -bool -CannyEdgeDetectionRecursiveGaussianImageFilter::InBounds(IndexType index) -{ - typename InputImageType::ConstPointer input = this->GetInput(); - typename InputImageType::SizeType sz; - sz = (input->GetRequestedRegion()).GetSize(); - - for (unsigned int i = 0; i < ImageDimension; i++) - { - if (index[i] < 0 || index[i] >= static_cast(sz[i])) - { - return false; - } - } - return true; -} template void From 6e8a13ed379330eb687a22c4d2874ddd85ac4156 Mon Sep 17 00:00:00 2001 From: Luis Ibanez Date: Sun, 12 Jul 2009 06:53:04 -0400 Subject: [PATCH 02/13] BUG: 9260. Inconsistent integer types used in Windows 64 bits. To fix this, Traits have been in Applies ITK commit 7ab73279a11 (2009-07-12) to the copy of itk::CannyEdgeDetectionImageFilter. --- include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.hxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.hxx b/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.hxx index 3ed52da7..3c225838 100644 --- a/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.hxx +++ b/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.hxx @@ -113,7 +113,7 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Gener // Set the kernel size. // fix me this needs to be based on the variance of the gaussian filter - unsigned long radius = 1; + typename TInputImage::SizeValueType radius = 1; // get a copy of the input requested region (should equal the output // requested region) From f3d7a94a8b2c4f38b8326bfe013f4751c07371cc Mon Sep 17 00:00:00 2001 From: Bradley Lowekamp Date: Mon, 17 Aug 2009 08:01:33 -0400 Subject: [PATCH 03/13] BUG: 9431 Fix to enable the canny edge detector to produce the correct results when called multiple Applies ITK commit c20831fdc0f (2009-08-17) to the copy of itk::CannyEdgeDetectionImageFilter. --- ...dgeDetectionRecursiveGaussianImageFilter.hxx | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.hxx b/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.hxx index 3c225838..185f88c5 100644 --- a/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.hxx +++ b/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.hxx @@ -320,6 +320,8 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Gener m_GaussianFilter->SetSigmaArray(this->m_Sigma); m_GaussianFilter->SetNormalizeAcrossScale(true); m_GaussianFilter->SetInput(input); + // modify to force excution, due to grafting complications + m_GaussianFilter->Modified(); m_GaussianFilter->Update(); // 2. Calculate 2nd order directional derivative------- @@ -330,6 +332,7 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Gener this->Compute2ndDerivativePos(); + // 3. Non-maximum suppression---------- // Calculate the zero crossings of the 2nd directional derivative and write @@ -373,7 +376,7 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Hyste uit.GoToBegin(); while (!uit.IsAtEnd()) { - uit.Value() = 0; + uit.Value() = NumericTraits::Zero; ++uit; } @@ -415,8 +418,14 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Follo ImageRegionIteratorWithIndex uit(this->GetOutput(), this->GetOutput()->GetRequestedRegion()); uit.SetIndex(index); - if (uit.Get() == 1) + if (uit.Get() == NumericTraits::One) { + // we must remove the node if we are not going to follow it! + + // Pop the front node from the list and read its index value. + node = m_NodeList->Front(); // get a pointer to the first node + m_NodeList->PopFront(); // unlink the front node + m_NodeStore->Return(node); // return the memory for reuse return; } @@ -441,14 +450,14 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Follo uit.SetIndex(nIndex); if (inputRegion.IsInside(nIndex)) { - if (oit.GetPixel(i) > m_LowerThreshold && uit.Value() != 1) + if (oit.GetPixel(i) > m_LowerThreshold && uit.Value() != NumericTraits::One) { node = m_NodeStore->Borrow(); // get a new node struct node->m_Value = nIndex; // set its value m_NodeList->PushFront(node); // add the new node to the list uit.SetIndex(nIndex); - uit.Value() = 1; + uit.Value() = NumericTraits::One; } } } From 1f8884ed80987a59c491e8852e20c4238b6a5b07 Mon Sep 17 00:00:00 2001 From: Mayeul Chassagnard Date: Fri, 2 Dec 2016 14:24:40 -0500 Subject: [PATCH 04/13] BUG: Add ITK_TEMPLATE_EXPORT to template classes Applies ITK commit be14aef1ec8 (2016-12-02) to the copy of itk::CannyEdgeDetectionImageFilter. --- include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.h b/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.h index fea112ca..1c9070de 100644 --- a/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.h +++ b/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.h @@ -37,7 +37,7 @@ using MultiThreader = MultiThreaderBase; template -class ListNode +class ITK_TEMPLATE_EXPORT ListNode { public: TValueType m_Value; From 8198b62eae781064d75bffe5baf037fa142df2ea Mon Sep 17 00:00:00 2001 From: Bradley Lowekamp Date: Tue, 19 May 2015 13:51:02 -0400 Subject: [PATCH 05/13] BUG: Isolate CannyEdgeDetection's internal filter from external Applies ITK commit acf4da8b05f (2015-05-19) to the copy of itk::CannyEdgeDetectionImageFilter, together with the private output pointer from ITK commit 563210160bf (2012-07-20) that it builds on. GenerateData grafts the input and output into private images, so the sub-filters and the internal passes no longer modify the pipeline's own data objects, and the filter can be updated again on a new input. --- ...dgeDetectionRecursiveGaussianImageFilter.h | 2 ++ ...eDetectionRecursiveGaussianImageFilter.hxx | 28 +++++++++++++------ 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.h b/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.h index 1c9070de..32fcc967 100644 --- a/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.h +++ b/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.h @@ -343,6 +343,8 @@ class ITK_TEMPLATE_EXPORT CannyEdgeDetectionRecursiveGaussianImageFilter typename ListNodeStorageType::Pointer m_NodeStore; ListPointerType m_NodeList; + + OutputImageType * m_OutputImage; }; } // end of namespace itk diff --git a/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.hxx b/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.hxx index 185f88c5..4864eaca 100644 --- a/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.hxx +++ b/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.hxx @@ -78,6 +78,8 @@ CannyEdgeDetectionRecursiveGaussianImageFilter @@ -163,7 +165,7 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Threa // Here input is the result from the gaussian filter // output is the update buffer. typename OutputImageType::Pointer input = m_GaussianFilter->GetOutput(); - typename OutputImageType::Pointer output = this->GetOutput(); + typename OutputImageType::Pointer output = this->m_OutputImage; // set iterator radius Size radius; @@ -299,11 +301,16 @@ template void CannyEdgeDetectionRecursiveGaussianImageFilter::GenerateData() { - // Allocate the output - this->GetOutput()->SetBufferedRegion(this->GetOutput()->GetRequestedRegion()); - this->GetOutput()->Allocate(); + // Use grafting of the input and output of this filter to isolate + // the mini-pipeline and other modifications from the pipeline. + typename InputImageType::Pointer input = InputImageType::New(); + input->Graft(const_cast(this->GetInput())); - typename InputImageType::ConstPointer input = this->GetInput(); + // Allocate the output, and graft + Superclass::AllocateOutputs(); + typename OutputImageType::Pointer output = OutputImageType::New(); + output->Graft(this->GetOutput()); + this->m_OutputImage = output; typename ZeroCrossingImageFilter::Pointer zeroCrossFilter = ZeroCrossingImageFilter::New(); @@ -337,7 +344,7 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Gener // Calculate the zero crossings of the 2nd directional derivative and write // the result to output buffer. - zeroCrossFilter->SetInput(this->GetOutput()); + zeroCrossFilter->SetInput(this->m_OutputImage); zeroCrossFilter->Update(); // 4. Hysteresis Thresholding--------- @@ -353,6 +360,9 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Gener // Then do the double threshoulding upon the edge reponses this->HysteresisThresholding(); + + this->GraftOutput(output); + this->m_OutputImage = nullptr; } template @@ -372,7 +382,7 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Hyste oit.GoToBegin(); - ImageRegionIterator uit(this->GetOutput(), this->GetOutput()->GetRequestedRegion()); + ImageRegionIterator uit(this->m_OutputImage, this->m_OutputImage->GetRequestedRegion()); uit.GoToBegin(); while (!uit.IsAtEnd()) { @@ -415,7 +425,7 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Follo radius.Fill(1); ConstNeighborhoodIterator oit(radius, input, input->GetRequestedRegion()); - ImageRegionIteratorWithIndex uit(this->GetOutput(), this->GetOutput()->GetRequestedRegion()); + ImageRegionIteratorWithIndex uit(this->m_OutputImage, this->m_OutputImage->GetRequestedRegion()); uit.SetIndex(index); if (uit.Get() == NumericTraits::One) @@ -482,7 +492,7 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Threa // Here input is the result from the gaussian filter // input1 is the 2nd derivative result // output is the gradient of 2nd derivative - typename OutputImageType::Pointer input1 = this->GetOutput(); + typename OutputImageType::Pointer input1 = this->m_OutputImage; typename OutputImageType::Pointer input = m_GaussianFilter->GetOutput(); typename InputImageType::Pointer output = m_UpdateBuffer1; From 32286b571e8b33ff1476ff1ea68b71092fbbe297 Mon Sep 17 00:00:00 2001 From: "Hans J. Johnson" Date: Wed, 14 Oct 2026 09:56:35 -0500 Subject: [PATCH 06/13] ENH: Apply the ITK API cleanups to the Canny filter copy Remove the legacy Threshold and OutsideValue parameters and the unreachable code in GenerateInputRequestedRegion, make the getters const, use SizeValueType for strides and offsets, replace floating-point equality checks, and drop the ITK_USE_CONCEPT_CHECKING guard, as ITK did. The feature generators and the example no longer call the removed methods. Applies these ITK commits to the copy of itk::CannyEdgeDetectionImageFilter: fede4ba39c4 (2009-04-25) Changed itkGetMacro to itkGetConstMacro. Now these Get methods can be used in other const metho e6a2204367c (2010-07-31) Removed all legacy code. 31f5b60c8e7 (2010-12-31) Add abstract integer types for use with image size c5e2f63b6f6 (2011-07-20) removed Threshold and OutsideVaule as parameters in Canny b6b6b1274d7 (2011-11-05) Removed unreachable code. 9ea5521e0fd (2015-08-14) Removes floating-point equality checks 9e582fd86c0 (2025-01-23) Remove the `ITK_USE_CONCEPT_CHECKING` option Co-Authored-By: Alexander Leinoff Co-Authored-By: Bill Lorensen Co-Authored-By: Bradley Lowekamp Co-Authored-By: David Cole Co-Authored-By: Shelly Belsky --- ...dgeDetectionRecursiveGaussianImageFilter.h | 41 ++---------- ...eDetectionRecursiveGaussianImageFilter.hxx | 65 ++----------------- ...DistanceAdvectionFieldFeatureGenerator.hxx | 1 - .../itkCannyEdgesDistanceFeatureGenerator.hxx | 1 - include/itkCannyEdgesFeatureGenerator.hxx | 1 - src/itkCannyEdgeDetectionImageFilter2.cxx | 1 - 6 files changed, 11 insertions(+), 99 deletions(-) diff --git a/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.h b/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.h index 32fcc967..4302fde7 100644 --- a/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.h +++ b/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.h @@ -29,6 +29,7 @@ #include "itkDerivativeOperator.h" #include "itkSparseFieldLayer.h" #include "itkObjectStore.h" +#include "itkMath.h" namespace itk @@ -111,6 +112,7 @@ class ITK_TEMPLATE_EXPORT CannyEdgeDetectionRecursiveGaussianImageFilter using InputImagePixelType = typename TInputImage::PixelType; using OutputImagePixelType = typename TOutputImage::PixelType; using IndexType = typename TInputImage::IndexType; + using SizeValueType = typename TInputImage::SizeValueType; using GaussianImageFilterType = SmoothingRecursiveGaussianImageFilter; using ScalarRealType = typename GaussianImageFilterType::ScalarRealType; @@ -157,33 +159,12 @@ class ITK_TEMPLATE_EXPORT CannyEdgeDetectionRecursiveGaussianImageFilter ScalarRealType GetSigma() const; - /* Set the Threshold value for detected edges. */ - void - SetThreshold(const OutputImagePixelType th) - { - this->m_Threshold = th; - this->m_UpperThreshold = m_Threshold; - this->m_LowerThreshold = m_Threshold / 2.0; - itkLegacyReplaceBodyMacro(SetThreshold, 2.2, SetUpperThreshold); - } - - OutputImagePixelType - GetThreshold(OutputImagePixelType itkNotUsed(th)) - { - itkLegacyReplaceBodyMacro(GetThreshold, 2.2, GetUpperThreshold); - return this->m_Threshold; - } - ///* Set the Threshold value for detected edges. */ itkSetMacro(UpperThreshold, OutputImagePixelType); - itkGetMacro(UpperThreshold, OutputImagePixelType); + itkGetConstMacro(UpperThreshold, OutputImagePixelType); itkSetMacro(LowerThreshold, OutputImagePixelType); - itkGetMacro(LowerThreshold, OutputImagePixelType); - - /* Set the Thresholdvalue for detected edges. */ - itkSetMacro(OutsideValue, OutputImagePixelType); - itkGetMacro(OutsideValue, OutputImagePixelType); + itkGetConstMacro(LowerThreshold, OutputImagePixelType); OutputImageType * GetNonMaximumSuppressionImage() const @@ -201,15 +182,11 @@ class ITK_TEMPLATE_EXPORT CannyEdgeDetectionRecursiveGaussianImageFilter void GenerateInputRequestedRegion() noexcept(false) override; -#ifdef ITK_USE_CONCEPT_CHECKING - /** Begin concept checking */ itkConceptMacro(InputHasNumericTraitsCheck, (Concept::HasNumericTraits)); itkConceptMacro(OutputHasNumericTraitsCheck, (Concept::HasNumericTraits)); itkConceptMacro(SameDimensionCheck, (Concept::SameDimension)); itkConceptMacro(InputIsFloatingPointCheck, (Concept::IsFloatingPoint)); itkConceptMacro(OutputIsFloatingPointCheck, (Concept::IsFloatingPoint)); - /** End concept checking */ -#endif protected: CannyEdgeDetectionRecursiveGaussianImageFilter(); @@ -315,12 +292,6 @@ class ITK_TEMPLATE_EXPORT CannyEdgeDetectionRecursiveGaussianImageFilter /** Lower threshold value for identifying edges. */ OutputImagePixelType m_LowerThreshold; // should be float here? - /** Threshold value for identifying edges. */ - OutputImagePixelType m_Threshold; - - /** "Background" value for use in thresholding. */ - OutputImagePixelType m_OutsideValue; - /** Update buffers used during calculation of multiple steps */ typename OutputImageType::Pointer m_UpdateBuffer1; @@ -338,8 +309,8 @@ class ITK_TEMPLATE_EXPORT CannyEdgeDetectionRecursiveGaussianImageFilter std::slice m_ComputeCannyEdgeSlice[ImageDimension]; - unsigned long m_Stride[ImageDimension]; - unsigned long m_Center; + SizeValueType m_Stride[ImageDimension]; + SizeValueType m_Center; typename ListNodeStorageType::Pointer m_NodeStore; ListPointerType m_NodeList; diff --git a/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.hxx b/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.hxx index 4864eaca..18519a99 100644 --- a/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.hxx +++ b/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.hxx @@ -24,7 +24,8 @@ #include "itkProgressReporter.h" #include "itkGradientMagnitudeImageFilter.h" #include "itkImageRegionIteratorWithIndex.h" -#include +#include "itkMath.h" + namespace itk { @@ -36,8 +37,6 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Zero; - m_Threshold = NumericTraits::Zero; m_UpperThreshold = NumericTraits::Zero; m_LowerThreshold = NumericTraits::Zero; @@ -104,50 +103,6 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Gener // call the superclass' implementation of this method Superclass::GenerateInputRequestedRegion(); return; - // get pointers to the input and output - typename Superclass::InputImagePointer inputPtr = const_cast(this->GetInput()); - typename Superclass::OutputImagePointer outputPtr = this->GetOutput(); - - if (!inputPtr || !outputPtr) - { - return; - } - - // Set the kernel size. - // fix me this needs to be based on the variance of the gaussian filter - typename TInputImage::SizeValueType radius = 1; - - // get a copy of the input requested region (should equal the output - // requested region) - typename TInputImage::RegionType inputRequestedRegion; - inputRequestedRegion = inputPtr->GetRequestedRegion(); - - // pad the input requested region by the operator radius - inputRequestedRegion.PadByRadius(radius); - - // crop the input requested region at the input's largest possible region - if (inputRequestedRegion.Crop(inputPtr->GetLargestPossibleRegion())) - { - inputPtr->SetRequestedRegion(inputRequestedRegion); - return; - } - else - { - // Couldn't crop the region (requested region is outside the largest - // possible region). Throw an exception. - - // store what we tried to request (prior to trying to crop) - inputPtr->SetRequestedRegion(inputRequestedRegion); - - // build an exception - InvalidRequestedRegionError e(__FILE__, __LINE__); - std::ostringstream msg; - msg << this->GetNameOfClass() << "::GenerateInputRequestedRegion()"; - e.SetLocation(msg.str().c_str()); - e.SetDescription("Requested region is (at least partially) outside the largest possible region."); - e.SetDataObject(inputPtr); - throw e; - } } template @@ -315,12 +270,6 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Gener typename ZeroCrossingImageFilter::Pointer zeroCrossFilter = ZeroCrossingImageFilter::New(); - typename GradientMagnitudeImageFilter::Pointer gradMag = - GradientMagnitudeImageFilter::New(); - - typename MultiplyImageFilter::Pointer multFilter = - MultiplyImageFilter::New(); - this->AllocateUpdateBuffer(); // 1.Apply the Gaussian Filter to the input image.------- @@ -428,7 +377,7 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Follo ImageRegionIteratorWithIndex uit(this->m_OutputImage, this->m_OutputImage->GetRequestedRegion()); uit.SetIndex(index); - if (uit.Get() == NumericTraits::One) + if (Math::ExactlyEquals(uit.Get(), NumericTraits::OneValue())) { // we must remove the node if we are not going to follow it! @@ -460,7 +409,8 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Follo uit.SetIndex(nIndex); if (inputRegion.IsInside(nIndex)) { - if (oit.GetPixel(i) > m_LowerThreshold && uit.Value() != NumericTraits::One) + if (oit.GetPixel(i) > m_LowerThreshold && + Math::NotExactlyEquals(uit.Value(), NumericTraits::OneValue())) { node = m_NodeStore->Borrow(); // get a new node struct node->m_Value = nIndex; // set its value @@ -669,17 +619,12 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Print Superclass::PrintSelf(os, indent); os << "Sigma: " << m_Sigma << std::endl; - os << indent << "Threshold: " << static_cast::PrintType>(m_Threshold) - << std::endl; os << indent << "UpperThreshold: " << static_cast::PrintType>(m_UpperThreshold) << std::endl; os << indent << "LowerThreshold: " << static_cast::PrintType>(m_LowerThreshold) << std::endl; - os << indent - << "OutsideValue: " << static_cast::PrintType>(m_OutsideValue) - << std::endl; os << "Center: " << m_Center << std::endl; os << "Stride: " << m_Stride << std::endl; os << "Gaussian Filter: " << std::endl; diff --git a/include/itkCannyEdgesDistanceAdvectionFieldFeatureGenerator.hxx b/include/itkCannyEdgesDistanceAdvectionFieldFeatureGenerator.hxx index 86ef1bb3..19200c54 100644 --- a/include/itkCannyEdgesDistanceAdvectionFieldFeatureGenerator.hxx +++ b/include/itkCannyEdgesDistanceAdvectionFieldFeatureGenerator.hxx @@ -109,7 +109,6 @@ CannyEdgesDistanceAdvectionFieldFeatureGenerator::GenerateData() this->m_CannyFilter->SetSigma(this->m_Sigma); this->m_CannyFilter->SetUpperThreshold(this->m_UpperThreshold); this->m_CannyFilter->SetLowerThreshold(this->m_LowerThreshold); - this->m_CannyFilter->SetOutsideValue(NumericTraits::Zero); this->m_DistanceMapFilter->Update(); diff --git a/include/itkCannyEdgesDistanceFeatureGenerator.hxx b/include/itkCannyEdgesDistanceFeatureGenerator.hxx index 0ad470c2..35b47281 100644 --- a/include/itkCannyEdgesDistanceFeatureGenerator.hxx +++ b/include/itkCannyEdgesDistanceFeatureGenerator.hxx @@ -106,7 +106,6 @@ CannyEdgesDistanceFeatureGenerator::GenerateData() this->m_CannyFilter->SetSigmaArray(this->m_Sigma); this->m_CannyFilter->SetUpperThreshold(this->m_UpperThreshold); this->m_CannyFilter->SetLowerThreshold(this->m_LowerThreshold); - this->m_CannyFilter->SetOutsideValue(NumericTraits::Zero); this->m_DistanceMapFilter->Update(); diff --git a/include/itkCannyEdgesFeatureGenerator.hxx b/include/itkCannyEdgesFeatureGenerator.hxx index fce5d680..022bae98 100644 --- a/include/itkCannyEdgesFeatureGenerator.hxx +++ b/include/itkCannyEdgesFeatureGenerator.hxx @@ -112,7 +112,6 @@ CannyEdgesFeatureGenerator::GenerateData() this->m_CannyFilter->SetSigmaArray(this->m_Sigma); this->m_CannyFilter->SetUpperThreshold(this->m_UpperThreshold); this->m_CannyFilter->SetLowerThreshold(this->m_LowerThreshold); - this->m_CannyFilter->SetOutsideValue(NumericTraits::Zero); this->m_RescaleFilter->Update(); diff --git a/src/itkCannyEdgeDetectionImageFilter2.cxx b/src/itkCannyEdgeDetectionImageFilter2.cxx index bd0a6a76..96892292 100644 --- a/src/itkCannyEdgeDetectionImageFilter2.cxx +++ b/src/itkCannyEdgeDetectionImageFilter2.cxx @@ -99,7 +99,6 @@ main(int argc, char * argv[]) cannyFilter->SetSigma(sigma); cannyFilter->SetUpperThreshold(upperThreshold); cannyFilter->SetLowerThreshold(lowerThreshold); - cannyFilter->SetOutsideValue(255); writer->SetInput(cannyFilter->GetOutput()); writer->UseCompressionOn(); From 45b2260721f6939927aa7c4466bfbccfdb06de85 Mon Sep 17 00:00:00 2001 From: "Hans J. Johnson" Date: Wed, 14 Oct 2026 09:56:35 -0500 Subject: [PATCH 07/13] ENH: Use the ITK 5 threading infrastructure in the Canny filter copy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the SetSingleMethod callbacks, the thread-data struct, and the per-pixel ProgressReporter with MultiThreaderBase, ParallelizeImageRegion, NumberOfWorkUnits, and ProgressTransformer, as ITK did. Applies these ITK commits to the copy of itk::CannyEdgeDetectionImageFilter: 7acd5a7fc14 (2011-05-16) Changed number of threads type. c7014107365 (2011-06-11) ThreadIdType changes. a24d6204e0d (2018-01-11) Use MultiThreaderBase e086233e890 (2018-04-13) simplifying Canny filter by using new threading infrastructure 011266e5ea8 (2018-05-29) rename NumberOfThreads into NumberOfWorkUnits in filters ce15429a230 (2018-05-29) renaming NumberOfThreads into NumberOfWorkUnits in MultiThreaderBase dd0b0d128d6 (2018-08-29) using progress transformer in recently refactored classes Co-Authored-By: Bill Lorensen Co-Authored-By: David Doria Co-Authored-By: Dženan Zukić --- ...dgeDetectionRecursiveGaussianImageFilter.h | 70 +-------- ...eDetectionRecursiveGaussianImageFilter.hxx | 142 ++++-------------- 2 files changed, 34 insertions(+), 178 deletions(-) diff --git a/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.h b/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.h index 4302fde7..be28720e 100644 --- a/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.h +++ b/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.h @@ -24,7 +24,6 @@ #include "itkConstNeighborhoodIterator.h" #include "itkSmoothingRecursiveGaussianImageFilter.h" #include "itkMultiplyImageFilter.h" -#include "itkZeroFluxNeumannBoundaryCondition.h" #include "itkMultiThreaderBase.h" #include "itkDerivativeOperator.h" #include "itkSparseFieldLayer.h" @@ -172,16 +171,6 @@ class ITK_TEMPLATE_EXPORT CannyEdgeDetectionRecursiveGaussianImageFilter return this->m_MultiplyImageFilter->GetOutput(); } - /** CannyEdgeDetectionRecursiveGaussianImageFilter needs a larger input requested - * region than the output requested region ( derivative operators, etc). - * As such, CannyEdgeDetectionRecursiveGaussianImageFilter needs to provide an implementation - * for GenerateInputRequestedRegion() in order to inform the - * pipeline execution model. - * - * \sa ImageToImageFilter::GenerateInputRequestedRegion() */ - void - GenerateInputRequestedRegion() noexcept(false) override; - itkConceptMacro(InputHasNumericTraitsCheck, (Concept::HasNumericTraits)); itkConceptMacro(OutputHasNumericTraitsCheck, (Concept::HasNumericTraits)); itkConceptMacro(SameDimensionCheck, (Concept::SameDimension)); @@ -202,13 +191,7 @@ class ITK_TEMPLATE_EXPORT CannyEdgeDetectionRecursiveGaussianImageFilter private: ~CannyEdgeDetectionRecursiveGaussianImageFilter() override = default; - /** Thread-Data Structure */ - struct CannyThreadStruct - { - CannyEdgeDetectionRecursiveGaussianImageFilter * Filter; - }; - - /** This allocate storage for m_UpdateBuffer, m_UpdateBuffer1 */ + /** Allocate storage for update buffers used during calculation of multiple steps. */ void AllocateUpdateBuffer(); @@ -222,35 +205,10 @@ class ITK_TEMPLATE_EXPORT CannyEdgeDetectionRecursiveGaussianImageFilter /** Calculate the second derivative of the smoothed image, it writes the - * result to m_UpdateBuffer using the ThreadedCompute2ndDerivative() method - * and multithreading mechanism. */ - void - Compute2ndDerivative(); - - /** - * Split the input into "num" pieces, returning region "i" as - * "splitRegion". This method is called "num" times to return non-overlapping - * regions. The method returns the number of pieces that the input - * can be split into by the routine. i.e. return value is less than or equal - * to "num". - * \sa ImageSource - */ - // virtual - // int SplitUpdateContainer(int i, int num, ThreadRegionType& splitRegion); - - /** Does the actual work of calculating of the 2nd derivative over a region - * supplied by the multithreading mechanism. - * - * \sa Compute2ndDerivative - * \sa Compute2ndDerivativeThreaderCallBack */ + * result to the update buffer */ void - ThreadedCompute2ndDerivative(const OutputImageRegionType & outputRegionForThread, int threadId); + ThreadedCompute2ndDerivative(const OutputImageRegionType & outputRegionForThread); - /** This callback method uses ImageSource::SplitRequestedRegion to acquire an - * output region that it passes to ThreadedCompute2ndDerivative for - * processing. */ - static ITK_THREAD_RETURN_TYPE - Compute2ndDerivativeThreaderCallback(void * arg); /** This methos is used to calculate the 2nd derivative for * non-boundary pixels. It is called by the ThreadedCompute2ndDerivative @@ -259,25 +217,9 @@ class ITK_TEMPLATE_EXPORT CannyEdgeDetectionRecursiveGaussianImageFilter ComputeCannyEdge(const NeighborhoodType & it, void * globalData); /** Calculate the gradient of the second derivative of the smoothed image, - * it writes the result to m_UpdateBuffer1 using the - * ThreadedCompute2ndDerivativePos() method and multithreading mechanism. - */ - void - Compute2ndDerivativePos(); - - /** Does the actual work of calculating of the 2nd derivative over a region - * supplied by the multithreading mechanism. - * - * \sa Compute2ndDerivativePos - * \sa Compute3ndDerivativePosThreaderCallBack */ + * it writes the result to m_UpdateBuffer1 */ void - ThreadedCompute2ndDerivativePos(const OutputImageRegionType & outputRegionForThread, int threadId); - - /**This callback method uses ImageSource::SplitRequestedRegion to acquire an - * output region that it passes to ThreadedCompute2ndDerivative for - * processing. */ - static ITK_THREAD_RETURN_TYPE - Compute2ndDerivativePosThreaderCallback(void * arg); + ThreadedCompute2ndDerivativePos(const OutputImageRegionType & outputRegionForThread); /** Standard deviation of the gaussian used for smoothing */ SigmaArrayType m_Sigma; @@ -288,8 +230,6 @@ class ITK_TEMPLATE_EXPORT CannyEdgeDetectionRecursiveGaussianImageFilter /** Upper threshold value for identifying edges. */ OutputImagePixelType m_UpperThreshold; // should be float here? - - /** Lower threshold value for identifying edges. */ OutputImagePixelType m_LowerThreshold; // should be float here? /** Update buffers used during calculation of multiple steps */ diff --git a/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.hxx b/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.hxx index 18519a99..65250ead 100644 --- a/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.hxx +++ b/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.hxx @@ -25,6 +25,7 @@ #include "itkGradientMagnitudeImageFilter.h" #include "itkImageRegionIteratorWithIndex.h" #include "itkMath.h" +#include "itkProgressTransformer.h" namespace itk { @@ -85,8 +86,7 @@ template void CannyEdgeDetectionRecursiveGaussianImageFilter::AllocateUpdateBuffer() { - // The update buffer looks just like the input. - + // The update buffer looks just like the input typename TInputImage::ConstPointer input = this->GetInput(); m_UpdateBuffer1->CopyInformation(input); @@ -95,21 +95,10 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Alloc m_UpdateBuffer1->Allocate(); } -template -void -CannyEdgeDetectionRecursiveGaussianImageFilter::GenerateInputRequestedRegion() noexcept( - false) -{ - // call the superclass' implementation of this method - Superclass::GenerateInputRequestedRegion(); - return; -} - template void CannyEdgeDetectionRecursiveGaussianImageFilter::ThreadedCompute2ndDerivative( - const OutputImageRegionType & outputRegionForThread, - int threadId) + const OutputImageRegionType & outputRegionForThread) { ZeroFluxNeumannBoundaryCondition nbc; @@ -133,9 +122,6 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Threa typename NeighborhoodAlgorithm::ImageBoundaryFacesCalculator::FaceListType::iterator fit; - // support progress methods/callbacks - ProgressReporter progress(this, threadId, outputRegionForThread.GetNumberOfPixels(), 100, 0.0f, 0.5f); - // Process the non-boundady region and then each of the boundary faces. // These are N-d regions which border the edge of the buffer. for (fit = faceList.begin(); fit != faceList.end(); ++fit) @@ -151,7 +137,6 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Threa it.Value() = ComputeCannyEdge(bit, globalData); ++bit; ++it; - progress.CompletedPixel(); } } } @@ -210,52 +195,13 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Compu return deriv; } -// Calculate the second derivative -template -void -CannyEdgeDetectionRecursiveGaussianImageFilter::Compute2ndDerivative() -{ - CannyThreadStruct str; - str.Filter = this; - - this->GetMultiThreader()->SetNumberOfWorkUnits(this->GetNumberOfWorkUnits()); - this->GetMultiThreader()->SetSingleMethod(this->Compute2ndDerivativeThreaderCallback, &str); - - this->GetMultiThreader()->SingleMethodExecute(); -} - -template -ITK_THREAD_RETURN_TYPE -CannyEdgeDetectionRecursiveGaussianImageFilter::Compute2ndDerivativeThreaderCallback( - void * arg) -{ - CannyThreadStruct * str; - - int total, threadId, threadCount; - - threadId = ((MultiThreaderBase::WorkUnitInfo *)(arg))->WorkUnitID; - threadCount = ((MultiThreaderBase::WorkUnitInfo *)(arg))->NumberOfWorkUnits; - - str = (CannyThreadStruct *)(((MultiThreaderBase::WorkUnitInfo *)(arg))->UserData); - - // Execute the actual method with appropriate output region - // first find out how many pieces extent can be split into. - // Using the SplitRequestedRegion method from itk::ImageSource. - OutputImageRegionType splitRegion; - total = str->Filter->SplitRequestedRegion(threadId, threadCount, splitRegion); - - if (threadId < total) - { - str->Filter->ThreadedCompute2ndDerivative(splitRegion, threadId); - } - - return ITK_THREAD_RETURN_DEFAULT_VALUE; -} template void CannyEdgeDetectionRecursiveGaussianImageFilter::GenerateData() { + this->UpdateProgress(0.0f); + // Use grafting of the input and output of this filter to isolate // the mini-pipeline and other modifications from the pipeline. typename InputImageType::Pointer input = InputImageType::New(); @@ -279,14 +225,28 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Gener // modify to force excution, due to grafting complications m_GaussianFilter->Modified(); m_GaussianFilter->Update(); + this->UpdateProgress(0.01f); - // 2. Calculate 2nd order directional derivative------- + ProgressTransformer progress1(0.01f, 0.45f, this); + // 2. Calculate 2nd order directional derivative // Calculate the 2nd order directional derivative of the smoothed image. // The output of this filter will be used to store the directional // derivative. - this->Compute2ndDerivative(); - - this->Compute2ndDerivativePos(); + this->GetMultiThreader()->SetNumberOfWorkUnits(this->GetNumberOfWorkUnits()); + this->GetMultiThreader()->template ParallelizeImageRegion( + this->GetOutput()->GetRequestedRegion(), + [this](const OutputImageRegionType & outputRegionForThread) { + this->ThreadedCompute2ndDerivative(outputRegionForThread); + }, + progress1.GetProcessObject()); + + ProgressTransformer progress2(0.45f, 0.9f, this); + this->GetMultiThreader()->template ParallelizeImageRegion( + this->GetOutput()->GetRequestedRegion(), + [this](const OutputImageRegionType & outputRegionForThread) { + this->ThreadedCompute2ndDerivativePos(outputRegionForThread); + }, + progress2.GetProcessObject()); // 3. Non-maximum suppression---------- @@ -295,6 +255,7 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Gener // the result to output buffer. zeroCrossFilter->SetInput(this->m_OutputImage); zeroCrossFilter->Update(); + this->UpdateProgress(0.92f); // 4. Hysteresis Thresholding--------- @@ -306,12 +267,15 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Gener // which is no longer needed, into the m_MultiplyImageFilter. m_MultiplyImageFilter->GraftOutput(m_GaussianFilter->GetOutput()); m_MultiplyImageFilter->Update(); + this->UpdateProgress(0.95f); // Then do the double threshoulding upon the edge reponses this->HysteresisThresholding(); + this->UpdateProgress(0.99f); this->GraftOutput(output); this->m_OutputImage = nullptr; + this->UpdateProgress(1.0f); } template @@ -428,8 +392,7 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Follo template void CannyEdgeDetectionRecursiveGaussianImageFilter::ThreadedCompute2ndDerivativePos( - const OutputImageRegionType & outputRegionForThread, - int threadId) + const OutputImageRegionType & outputRegionForThread) { ZeroFluxNeumannBoundaryCondition nbc; @@ -459,10 +422,7 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Threa typename NeighborhoodAlgorithm::ImageBoundaryFacesCalculator::FaceListType::iterator fit; - // support progress methods/callbacks - ProgressReporter progress(this, threadId, outputRegionForThread.GetNumberOfPixels(), 100, 0.5f, 0.5f); - - InputImagePixelType zero = NumericTraits::Zero; + InputImagePixelType zero = NumericTraits::ZeroValue(); OutputImagePixelType dx[ImageDimension]; OutputImagePixelType dx1[ImageDimension]; @@ -519,54 +479,10 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Threa ++bit; ++bit1; ++it; - progress.CompletedPixel(); } } } -// Calculate the second derivative -template -void -CannyEdgeDetectionRecursiveGaussianImageFilter::Compute2ndDerivativePos() -{ - CannyThreadStruct str; - str.Filter = this; - - this->GetMultiThreader()->SetNumberOfWorkUnits(this->GetNumberOfWorkUnits()); - this->GetMultiThreader()->SetSingleMethod(this->Compute2ndDerivativePosThreaderCallback, &str); - - this->GetMultiThreader()->SingleMethodExecute(); -} - -template -ITK_THREAD_RETURN_TYPE -CannyEdgeDetectionRecursiveGaussianImageFilter::Compute2ndDerivativePosThreaderCallback( - void * arg) -{ - CannyThreadStruct * str; - - int total, threadId, threadCount; - - threadId = ((MultiThreaderBase::WorkUnitInfo *)(arg))->WorkUnitID; - threadCount = ((MultiThreaderBase::WorkUnitInfo *)(arg))->NumberOfWorkUnits; - - str = (CannyThreadStruct *)(((MultiThreaderBase::WorkUnitInfo *)(arg))->UserData); - - // Execute the actual method with appropriate output region - // first find out how many pieces extent can be split into. - // Using the SplitRequestedRegion method from itk::ImageSource. - - OutputImageRegionType splitRegion; - total = str->Filter->SplitRequestedRegion(threadId, threadCount, splitRegion); - - if (threadId < total) - { - str->Filter->ThreadedCompute2ndDerivativePos(splitRegion, threadId); - } - - return ITK_THREAD_RETURN_DEFAULT_VALUE; -} - // Set value of Sigma (isotropic) template From d0c72fb247c263ade62074c238eec73d0934008e Mon Sep 17 00:00:00 2001 From: "Hans J. Johnson" Date: Wed, 14 Oct 2026 09:56:35 -0500 Subject: [PATCH 08/13] PERF: Hoist accessor calls out of the Canny inner loops Pass the multiply filter output to FollowEdge instead of fetching it per voxel, and cache the ComputeIndex() result of the hysteresis iterators, as ITK did. Applies these ITK commits to the copy of itk::CannyEdgeDetectionImageFilter: 563210160bf (2012-07-20) Pull expensive GetInput/GetOutput calls out of inner loops cc5b30193fa (2026-02-11) Replace GetIndex() calls on iterators with ComputeIndex() d086c8cf966 (2026-02-15) Locally cache ComputeIndex() result in filters Co-Authored-By: Kent Williams Co-Authored-By: Niels Dekker --- ...dgeDetectionRecursiveGaussianImageFilter.h | 2 +- ...eDetectionRecursiveGaussianImageFilter.hxx | 19 +++++++++++-------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.h b/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.h index be28720e..61618b7c 100644 --- a/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.h +++ b/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.h @@ -201,7 +201,7 @@ class ITK_TEMPLATE_EXPORT CannyEdgeDetectionRecursiveGaussianImageFilter /** Edge linking funciton */ void - FollowEdge(IndexType index); + FollowEdge(IndexType index, const OutputImageType * multiplyImageFilterOutput); /** Calculate the second derivative of the smoothed image, it writes the diff --git a/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.hxx b/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.hxx index 65250ead..7d21bca8 100644 --- a/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.hxx +++ b/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.hxx @@ -109,7 +109,6 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Threa // Here input is the result from the gaussian filter // output is the update buffer. typename OutputImageType::Pointer input = m_GaussianFilter->GetOutput(); - typename OutputImageType::Pointer output = this->m_OutputImage; // set iterator radius Size radius; @@ -128,7 +127,7 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Threa { NeighborhoodType bit(radius, input, *fit); - it = ImageRegionIterator(output, *fit); + it = ImageRegionIterator(this->m_OutputImage, *fit); bit.OverrideBoundaryCondition(&nbc); bit.GoToBegin(); @@ -303,16 +302,18 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Hyste ++uit; } + const OutputImageType * multiplyImageFilterOutput = this->m_MultiplyImageFilter->GetOutput(); while (!oit.IsAtEnd()) { value = oit.Value(); if (value > m_UpperThreshold) { + const auto computedIndex = oit.ComputeIndex(); node = m_NodeStore->Borrow(); - node->m_Value = oit.GetIndex(); + node->m_Value = computedIndex; m_NodeList->PushFront(node); - FollowEdge(oit.GetIndex()); + FollowEdge(computedIndex, multiplyImageFilterOutput); } ++oit; @@ -321,13 +322,14 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Hyste template void -CannyEdgeDetectionRecursiveGaussianImageFilter::FollowEdge(IndexType index) +CannyEdgeDetectionRecursiveGaussianImageFilter::FollowEdge( + IndexType index, + const OutputImageType * multiplyImageFilterOutput) { // This is the Zero crossings of the Second derivative multiplied with the // gradients of the image. HysteresisThresholding of this image should give // the Canny output. - typename OutputImageType::Pointer input = m_MultiplyImageFilter->GetOutput(); - InputImageRegionType inputRegion = input->GetRequestedRegion(); + InputImageRegionType inputRegion = multiplyImageFilterOutput->GetRequestedRegion(); IndexType nIndex; IndexType cIndex; @@ -337,7 +339,8 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Follo Size radius; radius.Fill(1); - ConstNeighborhoodIterator oit(radius, input, input->GetRequestedRegion()); + ConstNeighborhoodIterator oit( + radius, multiplyImageFilterOutput, multiplyImageFilterOutput->GetRequestedRegion()); ImageRegionIteratorWithIndex uit(this->m_OutputImage, this->m_OutputImage->GetRequestedRegion()); uit.SetIndex(index); From 643915964d2872518029a8a170b43d755d0b0cb8 Mon Sep 17 00:00:00 2001 From: "Hans J. Johnson" Date: Wed, 14 Oct 2026 09:56:35 -0500 Subject: [PATCH 09/13] COMP: Modernize the Canny filter copy to current C++ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use override, nullptr, type aliases, auto, constexpr, defaulted and deleted special members, in-class member initializers, range-based for loops, trailing return types, strongly typed enums, and value initialization in place of NumericTraits::ZeroValue(), as ITK did. The iterator template arguments stay spelled out because ITK 5.4 has no deduction guides for them. Applies these ITK commits to the copy of itk::CannyEdgeDetectionImageFilter: 90b8fb21cf1 (2011-09-26) fix warnings about unused variable and copy constructors 1b825a85101 (2014-01-16) Remove explicit typed exception specifications 0d61aa0798e (2014-05-07) Add ITK_NULLPTR supporting c++11 checks 3f712cd0955 (2014-10-31) Deprecate ::Zero and ::One 671d04333f5 (2014-12-13) Add c++11 override designation b6b9a8e8849 (2015-10-29) C++11 allows for rigorous =delete 4a3257e293a (2016-06-10) Provide a well documented macro for repeated code 576893dba8d (2017-10-13) Added ITK_OVERRIDE to hundreds of destructor declarations. 520a2493068 (2017-12-16) Preparing for ITKv5 by adding override 3ceacc0ad4e (2017-12-16) Use C++11 override directly 188a1632079 (2017-12-16) Use C++11 ITK_NULLPTR directly 66e5d6b3bcc (2018-01-27) Prefer C++11 type alias over typedef 5c14741e1e0 (2018-02-02) Replace itkStaticConstMacro with static constexpr de713e7ac52 (2018-02-05) Use auto for variable creation d72d28f5a1f (2018-03-04) Do not use itkGetStaticConstMacro in ITK b0f183b04fd (2018-03-25) Moved ITK_DISALLOW_COPY_AND_ASSIGN calls to public section 5142ed7bb5e (2018-09-23) Pefer = default to explicitly trivial implementations 0dd31a62f85 (2020-02-12) Consistently use strongly typed enums 4c0982ddec4 (2020-10-11) Call ITK_DISALLOW_COPY_AND_MOVE, not ITK_DISALLOW_COPY_AND_ASSIGN 828453d1bf6 (2021-10-19) Use `auto` for declaration of variables initialized by `New()` 2fa80e8bd32 (2022-03-30) Avoid old, C-style cast 2de7e24b0b5 (2022-12-01) Use range-based `for` loops to iterate over `faceList` variables 18a672368d7 (2022-12-02) Initialize `faceList` directly by ImageBoundaryFacesCalculator 5e2c49f9eb8 (2023-01-18) Add in-class `{}` member initializers to objects created by New() 8f233d177ab (2023-03-04) Add in-class `{}` member initializers having trailing comments 5a60ed00adb (2023-03-07) Remove unnecessary iterator `GoToBegin()` calls from Filtering 42b0bfc0771 (2023-03-09) Replace `T var = NumericTraits::ZeroValue()` with `T var{}` f7fa2693f61 (2023-04-26) Use trailing return types in multi-line function declarations 2c264ea2ef6 (2023-12-19) Replace itkTypeMacro calls with `itkOverrideGetNameOfClassMacro` 13ccff6af85 (2024-02-13) Replace `NumericTraits::ZeroValue()` with `T{}` 7fd1e7341bd (2024-10-27) Replace `Size var; var.Fill` with `auto var = Size::Filled` 6cb6bf98234 (2024-10-31) Replace `T var; var.Fill(x)` with `auto var = MakeFilled(x)` fd144827976 (2024-11-04) Add constexpr to variables initialized by MakeFilled c77f9f8da0d (2024-11-09) Add constexpr to variables initialized by T::Filled(int-literal) 677b76192a6 (2024-11-19) Replace T::Pointer with auto when initializing variables by New() 375ee8c47a0 (2024-11-27) Prefer explicit const designation e1acaf5aa8d (2024-12-15) Variable can be made constexpr 1649dee8e42 (2025-04-17) Replace TInputImage::SizeValueType with itk::SizeValueType caa61aa3cdf (2025-06-10) Assign initial values to variables f46b4c93fec (2025-06-13) Prefer member initializer to assignment 50210af9d3b (2026-03-12) Use CTAD for iterators "with index", in hxx files b6a38b188ca (2026-03-26) Use CTAD for `ImageRegionIterator` variables in hxx files dbab3e0faba (2026-03-29) Use CTAD when assigning an Image Iterator, in hxx files 4394f5d0eab (2026-04-03) Move region iterator declarations into `for` loops over faces Co-Authored-By: Bradley Lowekamp Co-Authored-By: Gaëtan Lehmann Co-Authored-By: Lee Newberg Co-Authored-By: Michka Popoff Co-Authored-By: Niels Dekker Co-Authored-By: Sean McBride --- ...dgeDetectionRecursiveGaussianImageFilter.h | 53 +++--- ...eDetectionRecursiveGaussianImageFilter.hxx | 158 ++++++++---------- 2 files changed, 94 insertions(+), 117 deletions(-) diff --git a/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.h b/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.h index 61618b7c..cc920e74 100644 --- a/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.h +++ b/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.h @@ -46,8 +46,10 @@ class ITK_TEMPLATE_EXPORT ListNode ListNode * Previous; }; - -/** \class CannyEdgeDetectionRecursiveGaussianImageFilter +/** + *\class CannyEdgeDetectionRecursiveGaussianImageFilter + * \brief This filter is an implementation of a Canny edge detector for + * scalar-valued images. * * This filter is an implementation of a Canny edge detector for scalar-valued * images. Based on John Canny's paper "A Computational Approach to Edge @@ -95,6 +97,8 @@ class ITK_TEMPLATE_EXPORT CannyEdgeDetectionRecursiveGaussianImageFilter : public ImageToImageFilter { public: + ITK_DISALLOW_COPY_AND_MOVE(CannyEdgeDetectionRecursiveGaussianImageFilter); + /** Standard "Self" & Superclass type alias. */ using Self = CannyEdgeDetectionRecursiveGaussianImageFilter; using Superclass = ImageToImageFilter; @@ -111,7 +115,7 @@ class ITK_TEMPLATE_EXPORT CannyEdgeDetectionRecursiveGaussianImageFilter using InputImagePixelType = typename TInputImage::PixelType; using OutputImagePixelType = typename TOutputImage::PixelType; using IndexType = typename TInputImage::IndexType; - using SizeValueType = typename TInputImage::SizeValueType; + using SizeValueType = itk::SizeValueType; using GaussianImageFilterType = SmoothingRecursiveGaussianImageFilter; using ScalarRealType = typename GaussianImageFilterType::ScalarRealType; @@ -135,17 +139,17 @@ class ITK_TEMPLATE_EXPORT CannyEdgeDetectionRecursiveGaussianImageFilter itkNewMacro(Self); /** Typedef to describe the output image region type. */ - typedef typename TOutputImage::RegionType OutputImageRegionType; - typedef typename TInputImage::RegionType InputImageRegionType; + using OutputImageRegionType = typename TOutputImage::RegionType; + using InputImageRegionType = typename TInputImage::RegionType; /** Run-time type information (and related methods). */ itkOverrideGetNameOfClassMacro(CannyEdgeDetectionRecursiveGaussianImageFilter); - /** ImageDimension constant */ + /** ImageDimension constant. */ static constexpr unsigned int ImageDimension = TInputImage::ImageDimension; static constexpr unsigned int OutputImageDimension = TOutputImage::ImageDimension; - /** Typedef of double containers */ + /** Typedef of double containers. */ using ArrayType = FixedArray; /** Smoothing parameters for the Gaussian filter. */ @@ -179,7 +183,6 @@ class ITK_TEMPLATE_EXPORT CannyEdgeDetectionRecursiveGaussianImageFilter protected: CannyEdgeDetectionRecursiveGaussianImageFilter(); - CannyEdgeDetectionRecursiveGaussianImageFilter(const Self &) {} void PrintSelf(std::ostream & os, Indent indent) const override; @@ -228,34 +231,32 @@ class ITK_TEMPLATE_EXPORT CannyEdgeDetectionRecursiveGaussianImageFilter * direction. */ ArrayType m_MaximumError; - /** Upper threshold value for identifying edges. */ - OutputImagePixelType m_UpperThreshold; // should be float here? - OutputImagePixelType m_LowerThreshold; // should be float here? + OutputImagePixelType m_UpperThreshold{}; // should be float here? + OutputImagePixelType m_LowerThreshold{}; // should be float here? - /** Update buffers used during calculation of multiple steps */ - typename OutputImageType::Pointer m_UpdateBuffer1; + typename OutputImageType::Pointer m_UpdateBuffer1{}; - /** Gaussian filter to smooth the input image */ - typename GaussianImageFilterType::Pointer m_GaussianFilter; + /** Gaussian filter to smooth the input image. */ + typename GaussianImageFilterType::Pointer m_GaussianFilter{}; /** Multiply image filter to multiply with the zero crossings of the second - * derivative. */ - typename MultiplyImageFilterType::Pointer m_MultiplyImageFilter; + * derivative. */ + typename MultiplyImageFilterType::Pointer m_MultiplyImageFilter{}; /** Function objects that are used in the inner loops of derivatiVex - calculations. */ - DerivativeOperator m_ComputeCannyEdge1stDerivativeOper; - DerivativeOperator m_ComputeCannyEdge2ndDerivativeOper; + * calculations. */ + DerivativeOperator m_ComputeCannyEdge1stDerivativeOper{}; + DerivativeOperator m_ComputeCannyEdge2ndDerivativeOper{}; - std::slice m_ComputeCannyEdgeSlice[ImageDimension]; + std::slice m_ComputeCannyEdgeSlice[ImageDimension]{}; - SizeValueType m_Stride[ImageDimension]; - SizeValueType m_Center; + SizeValueType m_Stride[ImageDimension]{}; + SizeValueType m_Center{}; - typename ListNodeStorageType::Pointer m_NodeStore; - ListPointerType m_NodeList; + typename ListNodeStorageType::Pointer m_NodeStore{}; + ListPointerType m_NodeList{}; - OutputImageType * m_OutputImage; + OutputImageType * m_OutputImage{}; }; } // end of namespace itk diff --git a/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.hxx b/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.hxx index 7d21bca8..507ed055 100644 --- a/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.hxx +++ b/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.hxx @@ -33,21 +33,18 @@ namespace itk template CannyEdgeDetectionRecursiveGaussianImageFilter::CannyEdgeDetectionRecursiveGaussianImageFilter() + : m_UpperThreshold(OutputImagePixelType{}) + , m_LowerThreshold(OutputImagePixelType{}) + , m_UpdateBuffer1(OutputImageType::New()) + , m_GaussianFilter(GaussianImageFilterType::New()) + , m_MultiplyImageFilter(MultiplyImageFilterType::New()) { unsigned int i; m_Sigma.Fill(1.0); - m_UpperThreshold = NumericTraits::Zero; - m_LowerThreshold = NumericTraits::Zero; - - m_GaussianFilter = GaussianImageFilterType::New(); - m_MultiplyImageFilter = MultiplyImageFilterType::New(); - m_UpdateBuffer1 = OutputImageType::New(); - // Set up neighborhood slices for all the dimensions. - typename Neighborhood::RadiusType r; - r.Fill(1); + constexpr auto r = MakeFilled::RadiusType>(1); // Dummy neighborhood used to set up the slices. Neighborhood it; @@ -56,12 +53,12 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::AllocateUpdateBuffer() { // The update buffer looks just like the input - typename TInputImage::ConstPointer input = this->GetInput(); + const typename TInputImage::ConstPointer input = this->GetInput(); m_UpdateBuffer1->CopyInformation(input); m_UpdateBuffer1->SetRequestedRegion(input->GetRequestedRegion()); @@ -102,32 +99,27 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Threa { ZeroFluxNeumannBoundaryCondition nbc; - ImageRegionIterator it; - void * globalData = nullptr; - // Here input is the result from the gaussian filter - // output is the update buffer. - typename OutputImageType::Pointer input = m_GaussianFilter->GetOutput(); + // Here input is the result from the gaussian filter output is the update + // buffer + const typename OutputImageType::Pointer input = m_GaussianFilter->GetOutput(); - // set iterator radius - Size radius; - radius.Fill(1); + // Set iterator radius + constexpr auto radius = Size::Filled(1); // Find the data-set boundary "faces" - typename NeighborhoodAlgorithm::ImageBoundaryFacesCalculator::FaceListType faceList; - NeighborhoodAlgorithm::ImageBoundaryFacesCalculator bC; - faceList = bC(input, outputRegionForThread, radius); - - typename NeighborhoodAlgorithm::ImageBoundaryFacesCalculator::FaceListType::iterator fit; + NeighborhoodAlgorithm::ImageBoundaryFacesCalculator bC; + const typename NeighborhoodAlgorithm::ImageBoundaryFacesCalculator::FaceListType faceList = + bC(input, outputRegionForThread, radius); // Process the non-boundady region and then each of the boundary faces. // These are N-d regions which border the edge of the buffer. - for (fit = faceList.begin(); fit != faceList.end(); ++fit) + for (const auto & face : faceList) { - NeighborhoodType bit(radius, input, *fit); + NeighborhoodType bit(radius, input, face); - it = ImageRegionIterator(this->m_OutputImage, *fit); + ImageRegionIterator it(this->m_OutputImage, face); bit.OverrideBoundaryCondition(&nbc); bit.GoToBegin(); @@ -141,36 +133,35 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Threa } template -typename CannyEdgeDetectionRecursiveGaussianImageFilter::OutputImagePixelType +auto CannyEdgeDetectionRecursiveGaussianImageFilter::ComputeCannyEdge( const NeighborhoodType & it, - void * itkNotUsed(globalData)) + void * itkNotUsed(globalData)) -> OutputImagePixelType { - unsigned int i, j; - NeighborhoodInnerProduct innerProduct; + + const NeighborhoodInnerProduct innerProduct; OutputImagePixelType dx[ImageDimension]; OutputImagePixelType dxx[ImageDimension]; OutputImagePixelType dxy[ImageDimension * (ImageDimension - 1) / 2]; - OutputImagePixelType deriv; - OutputImagePixelType gradMag; // double alpha = 0.01; // Calculate 1st & 2nd order derivative - for (i = 0; i < ImageDimension; i++) + for (unsigned int i = 0; i < ImageDimension; i++) { dx[i] = innerProduct(m_ComputeCannyEdgeSlice[i], it, m_ComputeCannyEdge1stDerivativeOper); dxx[i] = innerProduct(m_ComputeCannyEdgeSlice[i], it, m_ComputeCannyEdge2ndDerivativeOper); } - deriv = NumericTraits::Zero; + OutputImagePixelType deriv{}; + int k = 0; // Calculate the 2nd derivative - for (i = 0; i < ImageDimension - 1; i++) + for (unsigned int i = 0; i < ImageDimension - 1; i++) { - for (j = i + 1; j < ImageDimension; j++) + for (unsigned int j = i + 1; j < ImageDimension; j++) { dxy[k] = 0.25 * it.GetPixel(m_Center - m_Stride[i] - m_Stride[j]) - 0.25 * it.GetPixel(m_Center - m_Stride[i] + m_Stride[j]) - @@ -182,8 +173,8 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Compu } } - gradMag = 0.0001; // alpha * alpha; - for (i = 0; i < ImageDimension; i++) + auto gradMag = static_cast(0.0001); // alpha * alpha; + for (unsigned int i = 0; i < ImageDimension; i++) { deriv += dx[i] * dx[i] * dxx[i]; gradMag += dx[i] * dx[i]; @@ -203,17 +194,16 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Gener // Use grafting of the input and output of this filter to isolate // the mini-pipeline and other modifications from the pipeline. - typename InputImageType::Pointer input = InputImageType::New(); + auto input = InputImageType::New(); input->Graft(const_cast(this->GetInput())); // Allocate the output, and graft Superclass::AllocateOutputs(); - typename OutputImageType::Pointer output = OutputImageType::New(); + auto output = OutputImageType::New(); output->Graft(this->GetOutput()); this->m_OutputImage = output; - typename ZeroCrossingImageFilter::Pointer zeroCrossFilter = - ZeroCrossingImageFilter::New(); + auto zeroCrossFilter = ZeroCrossingImageFilter::New(); this->AllocateUpdateBuffer(); @@ -284,21 +274,18 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Hyste // This is the Zero crossings of the Second derivative multiplied with the // gradients of the image. HysteresisThresholding of this image should give // the Canny output. - typename OutputImageType::Pointer input = m_MultiplyImageFilter->GetOutput(); - float value; + const typename OutputImageType::Pointer input = m_MultiplyImageFilter->GetOutput(); + float value = NAN; - ListNodeType * node; + ListNodeType * node = nullptr; // fix me ImageRegionIterator oit(input, input->GetRequestedRegion()); - oit.GoToBegin(); - ImageRegionIterator uit(this->m_OutputImage, this->m_OutputImage->GetRequestedRegion()); - uit.GoToBegin(); while (!uit.IsAtEnd()) { - uit.Value() = NumericTraits::Zero; + uit.Value() = OutputImagePixelType{}; ++uit; } @@ -329,19 +316,14 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Follo // This is the Zero crossings of the Second derivative multiplied with the // gradients of the image. HysteresisThresholding of this image should give // the Canny output. - InputImageRegionType inputRegion = multiplyImageFilterOutput->GetRequestedRegion(); - - IndexType nIndex; - IndexType cIndex; - ListNodeType * node; + const InputImageRegionType inputRegion = multiplyImageFilterOutput->GetRequestedRegion(); - // assign iterator radius - Size radius; - radius.Fill(1); + // Assign iterator radius + constexpr auto radius = Size::Filled(1); ConstNeighborhoodIterator oit( radius, multiplyImageFilterOutput, multiplyImageFilterOutput->GetRequestedRegion()); - ImageRegionIteratorWithIndex uit(this->m_OutputImage, this->m_OutputImage->GetRequestedRegion()); + ImageRegionIteratorWithIndex uit(this->m_OutputImage, this->m_OutputImage->GetRequestedRegion()); uit.SetIndex(index); if (Math::ExactlyEquals(uit.Get(), NumericTraits::OneValue())) @@ -349,20 +331,20 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Follo // we must remove the node if we are not going to follow it! // Pop the front node from the list and read its index value. - node = m_NodeList->Front(); // get a pointer to the first node - m_NodeList->PopFront(); // unlink the front node - m_NodeStore->Return(node); // return the memory for reuse + auto node = m_NodeList->Front(); // get a pointer to the first node + m_NodeList->PopFront(); // unlink the front node + m_NodeStore->Return(node); // return the memory for reuse return; } - int nSize = m_Center * 2 + 1; + const int nSize = m_Center * 2 + 1; while (!m_NodeList->Empty()) { // Pop the front node from the list and read its index value. - node = m_NodeList->Front(); // get a pointer to the first node - cIndex = node->m_Value; // read the value of the first node - m_NodeList->PopFront(); // unlink the front node - m_NodeStore->Return(node); // return the memory for reuse + auto node = m_NodeList->Front(); // Get a pointer to the first node + const auto cIndex = node->m_Value; // Read the value of the first node + m_NodeList->PopFront(); // Unlink the front node + m_NodeStore->Return(node); // Return the memory for reuse // Move iterators to the correct index position. oit.SetLocation(cIndex); @@ -372,7 +354,7 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Follo // Search the neighbors for new indicies to add to the list. for (int i = 0; i < nSize; i++) { - nIndex = oit.GetIndex(i); + const auto nIndex = oit.GetIndex(i); uit.SetIndex(nIndex); if (inputRegion.IsInside(nIndex)) { @@ -384,7 +366,7 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Follo m_NodeList->PushFront(node); // add the new node to the list uit.SetIndex(nIndex); - uit.Value() = NumericTraits::One; + uit.Value() = NumericTraits::OneValue(); } } } @@ -403,29 +385,23 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Threa ConstNeighborhoodIterator bit; ConstNeighborhoodIterator bit1; - ImageRegionIterator it; - // Here input is the result from the gaussian filter // input1 is the 2nd derivative result // output is the gradient of 2nd derivative - typename OutputImageType::Pointer input1 = this->m_OutputImage; - typename OutputImageType::Pointer input = m_GaussianFilter->GetOutput(); - - typename InputImageType::Pointer output = m_UpdateBuffer1; + const typename OutputImageType::Pointer input1 = this->m_OutputImage; + const typename OutputImageType::Pointer input = m_GaussianFilter->GetOutput(); + const typename InputImageType::Pointer output = m_UpdateBuffer1; - // set iterator radius - Size radius; - radius.Fill(1); + // Set iterator radius + constexpr auto radius = Size::Filled(1); // Find the data-set boundary "faces" - typename NeighborhoodAlgorithm::ImageBoundaryFacesCalculator::FaceListType faceList; - NeighborhoodAlgorithm::ImageBoundaryFacesCalculator bC; - faceList = bC(input, outputRegionForThread, radius); - - typename NeighborhoodAlgorithm::ImageBoundaryFacesCalculator::FaceListType::iterator fit; + NeighborhoodAlgorithm::ImageBoundaryFacesCalculator bC; + const typename NeighborhoodAlgorithm::ImageBoundaryFacesCalculator::FaceListType faceList = + bC(input, outputRegionForThread, radius); - InputImagePixelType zero = NumericTraits::ZeroValue(); + constexpr InputImagePixelType zero{}; OutputImagePixelType dx[ImageDimension]; OutputImagePixelType dx1[ImageDimension]; @@ -438,13 +414,13 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Threa // Process the non-boundary region and then each of the boundary faces. // These are N-d regions which border the edge of the buffer. - NeighborhoodInnerProduct IP; + const NeighborhoodInnerProduct IP; - for (fit = faceList.begin(); fit != faceList.end(); ++fit) + for (const auto & face : faceList) { - bit = ConstNeighborhoodIterator(radius, input, *fit); - bit1 = ConstNeighborhoodIterator(radius, input1, *fit); - it = ImageRegionIterator(output, *fit); + bit = ConstNeighborhoodIterator(radius, input, face); + bit1 = ConstNeighborhoodIterator(radius, input1, face); + ImageRegionIterator it(output, face); bit.OverrideBoundaryCondition(&nbc); bit.GoToBegin(); bit1.GoToBegin(); @@ -463,7 +439,7 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Threa dx1[i] = IP(m_ComputeCannyEdgeSlice[i], bit1, m_ComputeCannyEdge1stDerivativeOper); } - gradMag = std::sqrt((double)gradMag); + gradMag = std::sqrt(static_cast(gradMag)); derivPos = zero; for (unsigned int i = 0; i < ImageDimension; i++) { From 107bf011363cc2242ae42856cf0151e22d70da47 Mon Sep 17 00:00:00 2001 From: "Hans J. Johnson" Date: Wed, 14 Oct 2026 09:56:35 -0500 Subject: [PATCH 10/13] STYLE: Apply ITK formatting, naming, and include conventions to the Canny filter copy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove trailing whitespace, rename module and template parameter names, update header guards, use typename, use the PrintSelf object macros and indentation, prefer prefix increments, and tidy includes, as ITK did. Applies these ITK commits to the copy of itk::CannyEdgeDetectionImageFilter: dc3ced8177a (2010-08-24) Uncrustify code to remove end of line spacing. dccd0de7b07 (2010-09-25) Remove redundant header includes. a1cb6f7621c (2011-03-18) let BinaryFunctorImageFilter take a constant as input 13f99f81510 (2011-07-15) Change for development tool compliance 540228ef42f (2011-07-15) Converted module names to use camel case f8e626290a8 (2011-08-13) Remove Win32 duplicate warning disable d1918d28f06 (2012-11-08) Update space before access specification a4603f252da (2013-08-06) Remove superfluous return statements at the end of void functions 86bbb9d092d (2013-09-14) Use "typename" for template parameters 26354b32667 (2013-12-13) Rename TValueType template parameters to TValue c57adf26874 (2014-06-16) Add itkPrintSelfObjectMacro to improve indenting dd673a7a7b3 (2014-12-15) Remove double underscore in header guards (.h & __itk*_h form) 422aa2da236 (2014-12-15) Remove double underscore in header guards (.hxx & __itk*_h form) 971aef211ca (2021-04-21) Replace postfix by prefix increment in `for` loops in ITK/Modules f84720ee082 (2021-12-13) Remove recursive inclusion of .h files ec510c17494 (2022-07-11) Replace postfix with prefix increment, decrement (`++`, `--`) 48c6ed76973 (2022-12-22) Add missing `<< indent` insertions to `PrintSelf` implementations Co-Authored-By: Bradley Lowekamp Co-Authored-By: Brian Helba Co-Authored-By: Gaëtan Lehmann Co-Authored-By: Niels Dekker Co-Authored-By: Sean McBride --- ...dgeDetectionRecursiveGaussianImageFilter.h | 17 +++----- ...eDetectionRecursiveGaussianImageFilter.hxx | 43 +++++++------------ 2 files changed, 20 insertions(+), 40 deletions(-) diff --git a/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.h b/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.h index cc920e74..2c747acf 100644 --- a/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.h +++ b/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.h @@ -18,9 +18,6 @@ #ifndef itkCannyEdgeDetectionRecursiveGaussianImageFilter_h #define itkCannyEdgeDetectionRecursiveGaussianImageFilter_h -#include "itkImageToImageFilter.h" -#include "itkImage.h" -#include "itkFixedArray.h" #include "itkConstNeighborhoodIterator.h" #include "itkSmoothingRecursiveGaussianImageFilter.h" #include "itkMultiplyImageFilter.h" @@ -30,17 +27,13 @@ #include "itkObjectStore.h" #include "itkMath.h" - namespace itk { -using MultiThreader = MultiThreaderBase; - - -template +template class ITK_TEMPLATE_EXPORT ListNode { public: - TValueType m_Value; + TValue m_Value; ListNode * Next; ListNode * Previous; @@ -91,7 +84,9 @@ class ITK_TEMPLATE_EXPORT ListNode * * \sa SmoothingRecursiveGaussianImageFilter * \sa ZeroCrossingImageFilter - * \sa ThresholdImageFilter */ + * \sa ThresholdImageFilter + * \ingroup ITKImageFeature + */ template class ITK_TEMPLATE_EXPORT CannyEdgeDetectionRecursiveGaussianImageFilter : public ImageToImageFilter @@ -206,7 +201,6 @@ class ITK_TEMPLATE_EXPORT CannyEdgeDetectionRecursiveGaussianImageFilter void FollowEdge(IndexType index, const OutputImageType * multiplyImageFilterOutput); - /** Calculate the second derivative of the smoothed image, it writes the * result to the update buffer */ void @@ -258,7 +252,6 @@ class ITK_TEMPLATE_EXPORT CannyEdgeDetectionRecursiveGaussianImageFilter OutputImageType * m_OutputImage{}; }; - } // end of namespace itk #ifndef ITK_MANUAL_INSTANTIATION diff --git a/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.hxx b/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.hxx index 507ed055..a5e35691 100644 --- a/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.hxx +++ b/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.hxx @@ -29,7 +29,6 @@ namespace itk { - template CannyEdgeDetectionRecursiveGaussianImageFilter::CannyEdgeDetectionRecursiveGaussianImageFilter() @@ -39,8 +38,6 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Compu // double alpha = 0.01; // Calculate 1st & 2nd order derivative - for (unsigned int i = 0; i < ImageDimension; i++) + for (unsigned int i = 0; i < ImageDimension; ++i) { dx[i] = innerProduct(m_ComputeCannyEdgeSlice[i], it, m_ComputeCannyEdge1stDerivativeOper); dxx[i] = innerProduct(m_ComputeCannyEdgeSlice[i], it, m_ComputeCannyEdge2ndDerivativeOper); @@ -157,11 +154,10 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Compu OutputImagePixelType deriv{}; int k = 0; - // Calculate the 2nd derivative - for (unsigned int i = 0; i < ImageDimension - 1; i++) + for (unsigned int i = 0; i < ImageDimension - 1; ++i) { - for (unsigned int j = i + 1; j < ImageDimension; j++) + for (unsigned int j = i + 1; j < ImageDimension; ++j) { dxy[k] = 0.25 * it.GetPixel(m_Center - m_Stride[i] - m_Stride[j]) - 0.25 * it.GetPixel(m_Center - m_Stride[i] + m_Stride[j]) - @@ -169,12 +165,12 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Compu 0.25 * it.GetPixel(m_Center + m_Stride[i] + m_Stride[j]); deriv += 2.0 * dx[i] * dx[j] * dxy[k]; - k++; + ++k; } } auto gradMag = static_cast(0.0001); // alpha * alpha; - for (unsigned int i = 0; i < ImageDimension; i++) + for (unsigned int i = 0; i < ImageDimension; ++i) { deriv += dx[i] * dx[i] * dxx[i]; gradMag += dx[i] * dx[i]; @@ -207,7 +203,7 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Gener this->AllocateUpdateBuffer(); - // 1.Apply the Gaussian Filter to the input image.------- + // 1.Apply the Gaussian Filter to the input image m_GaussianFilter->SetSigmaArray(this->m_Sigma); m_GaussianFilter->SetNormalizeAcrossScale(true); m_GaussianFilter->SetInput(input); @@ -237,7 +233,6 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Gener }, progress2.GetProcessObject()); - // 3. Non-maximum suppression---------- // Calculate the zero crossings of the 2nd directional derivative and write @@ -351,8 +346,8 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Follo uit.SetIndex(cIndex); uit.Value() = 1; - // Search the neighbors for new indicies to add to the list. - for (int i = 0; i < nSize; i++) + // Search the neighbors for new indices to add to the list. + for (int i = 0; i < nSize; ++i) { const auto nIndex = oit.GetIndex(i); uit.SetIndex(nIndex); @@ -373,13 +368,11 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Follo } } - template void CannyEdgeDetectionRecursiveGaussianImageFilter::ThreadedCompute2ndDerivativePos( const OutputImageRegionType & outputRegionForThread) { - ZeroFluxNeumannBoundaryCondition nbc; ConstNeighborhoodIterator bit; @@ -428,10 +421,9 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Threa while (!bit.IsAtEnd()) { - gradMag = 0.0001; - for (unsigned int i = 0; i < ImageDimension; i++) + for (unsigned int i = 0; i < ImageDimension; ++i) { dx[i] = IP(m_ComputeCannyEdgeSlice[i], bit, m_ComputeCannyEdge1stDerivativeOper); gradMag += dx[i] * dx[i]; @@ -441,9 +433,8 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Threa gradMag = std::sqrt(static_cast(gradMag)); derivPos = zero; - for (unsigned int i = 0; i < ImageDimension; i++) + for (unsigned int i = 0; i < ImageDimension; ++i) { - // First calculate the directional derivative directional[i] = dx[i] / gradMag; @@ -520,15 +511,11 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Print os << indent << "LowerThreshold: " << static_cast::PrintType>(m_LowerThreshold) << std::endl; - os << "Center: " << m_Center << std::endl; - os << "Stride: " << m_Stride << std::endl; - os << "Gaussian Filter: " << std::endl; - m_GaussianFilter->Print(os, indent.GetNextIndent()); - os << "Multiply image Filter: " << std::endl; - m_MultiplyImageFilter->Print(os, indent.GetNextIndent()); - os << "UpdateBuffer1: " << std::endl; - m_UpdateBuffer1->Print(os, indent.GetNextIndent()); + os << indent << "Center: " << m_Center << std::endl; + os << indent << "Stride: " << m_Stride << std::endl; + itkPrintSelfObjectMacro(GaussianFilter); + itkPrintSelfObjectMacro(MultiplyImageFilter); + itkPrintSelfObjectMacro(UpdateBuffer1); } - } // namespace itk #endif From c0653ed6ef69bfeda2d8ccd566e312a9d51193f1 Mon Sep 17 00:00:00 2001 From: "Hans J. Johnson" Date: Wed, 14 Oct 2026 09:56:35 -0500 Subject: [PATCH 11/13] DOC: Bring the Canny filter copy comments and license header in line with ITK MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update the license header, Doxygen briefs and groups, spelling, https links, the copyright holder, in-code citations (now bibtex), and the migration notes, as ITK did. Applies these ITK commits to the copy of itk::CannyEdgeDetectionImageFilter: b738d019148 (2010-10-25) Headers changed after running Hans script. df54d1bb59e (2010-08-05) Python,Tcl,&Java examples used outdated methods dc85ead03f7 (2011-04-15) add module name in the doxygen groups 3397115b3b8 (2011-06-13) Code review of EdgeDetectionReview d7d38eede7f (2012-03-11) Spelling erros in comments 17030da3289 (2013-10-16) Prevent concept checking comments from being parsed by Doxygen e6908a9bfd9 (2014-02-21) Make comment the Doxygen brief aacfcc330b4 (2014-06-26) Add break in brief description of Canny edge filter 621f43c7223 (2017-01-17) Improve the itkCannyEdgeDetectionImageFilter style. 344e9bc58e6 (2020-02-07) Update copyright assignment to NumFOCUS 9cd0f2053f0 (2022-05-16) Use https instead of http when https works eede9c9f790 (2022-08-02) fix typos in Modules/Filtering 1608256c5a4 (2022-08-26) Remove outdated migration information 87a66198e11 (2022-09-25) Make Doxygen class command indent consistent 2513f8f62e0 (2023-03-08) Fixed spelling bugs in Modules/Filtering headers ed7ad92dd64 (2023-03-27) Spellcheck Filtering HXX and Video headers 4d750726d06 (2024-03-01) Update documentation of `GetNameOfClass` macro calls 097e2dc8788 (2025-01-23) Convert from raw in-code citations to bibtex (1/N) Co-Authored-By: Bill Lorensen Co-Authored-By: Bradley Lowekamp Co-Authored-By: Brian Helba Co-Authored-By: Dave Chen Co-Authored-By: Gaëtan Lehmann Co-Authored-By: Jon Haitz Legarreta Co-Authored-By: Jon Haitz Legarreta Gorroño Co-Authored-By: Lee Newberg Co-Authored-By: Luis Ibanez Co-Authored-By: Matt McCormick Co-Authored-By: Niels Dekker Co-Authored-By: Peter Lamb Co-Authored-By: Ziv Yaniv Co-Authored-By: luz paz --- ...dgeDetectionRecursiveGaussianImageFilter.h | 26 ++++++++--------- ...eDetectionRecursiveGaussianImageFilter.hxx | 28 +++++++++---------- 2 files changed, 25 insertions(+), 29 deletions(-) diff --git a/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.h b/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.h index 2c747acf..0a57c3a5 100644 --- a/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.h +++ b/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.h @@ -40,15 +40,12 @@ class ITK_TEMPLATE_EXPORT ListNode }; /** - *\class CannyEdgeDetectionRecursiveGaussianImageFilter + * \class CannyEdgeDetectionRecursiveGaussianImageFilter * \brief This filter is an implementation of a Canny edge detector for * scalar-valued images. * - * This filter is an implementation of a Canny edge detector for scalar-valued - * images. Based on John Canny's paper "A Computational Approach to Edge - * Detection"(IEEE Transactions on Pattern Analysis and Machine Intelligence, - * Vol. PAMI-8, No.6, November 1986), there are four major steps used in the - * edge-detection scheme: + * Based on John Canny's paper \cite canny1986 there are four major steps + * used in the edge-detection scheme: * (1) Smooth the input image with Gaussian filter. * (2) Calculate the second directional derivatives of the smoothed image. * (3) Non-Maximum Suppression: the zero-crossings of 2nd derivative are found, @@ -137,7 +134,7 @@ class ITK_TEMPLATE_EXPORT CannyEdgeDetectionRecursiveGaussianImageFilter using OutputImageRegionType = typename TOutputImage::RegionType; using InputImageRegionType = typename TInputImage::RegionType; - /** Run-time type information (and related methods). */ + /** \see LightObject::GetNameOfClass() */ itkOverrideGetNameOfClassMacro(CannyEdgeDetectionRecursiveGaussianImageFilter); /** ImageDimension constant. */ @@ -157,10 +154,13 @@ class ITK_TEMPLATE_EXPORT CannyEdgeDetectionRecursiveGaussianImageFilter ScalarRealType GetSigma() const; - ///* Set the Threshold value for detected edges. */ + /** \brief Set the upper threshold value for detected edges. + */ itkSetMacro(UpperThreshold, OutputImagePixelType); itkGetConstMacro(UpperThreshold, OutputImagePixelType); + /** \brief Set the lower threshold value for detected edges. + */ itkSetMacro(LowerThreshold, OutputImagePixelType); itkGetConstMacro(LowerThreshold, OutputImagePixelType); @@ -193,11 +193,11 @@ class ITK_TEMPLATE_EXPORT CannyEdgeDetectionRecursiveGaussianImageFilter void AllocateUpdateBuffer(); - /** Implement hysteresis thresholding */ + /** Implement hysteresis thresholding. */ void HysteresisThresholding(); - /** Edge linking funciton */ + /** Edge linking function. */ void FollowEdge(IndexType index, const OutputImageType * multiplyImageFilterOutput); @@ -207,7 +207,7 @@ class ITK_TEMPLATE_EXPORT CannyEdgeDetectionRecursiveGaussianImageFilter ThreadedCompute2ndDerivative(const OutputImageRegionType & outputRegionForThread); - /** This methos is used to calculate the 2nd derivative for + /** This method is used to calculate the 2nd derivative for * non-boundary pixels. It is called by the ThreadedCompute2ndDerivative * method. */ OutputImagePixelType @@ -221,8 +221,6 @@ class ITK_TEMPLATE_EXPORT CannyEdgeDetectionRecursiveGaussianImageFilter /** Standard deviation of the gaussian used for smoothing */ SigmaArrayType m_Sigma; - /** The maximum error of the gaussian blurring kernel in each dimensional - * direction. */ ArrayType m_MaximumError; OutputImagePixelType m_UpperThreshold{}; // should be float here? @@ -237,7 +235,7 @@ class ITK_TEMPLATE_EXPORT CannyEdgeDetectionRecursiveGaussianImageFilter * derivative. */ typename MultiplyImageFilterType::Pointer m_MultiplyImageFilter{}; - /** Function objects that are used in the inner loops of derivatiVex + /** Function objects that are used in the inner loops of derivative * calculations. */ DerivativeOperator m_ComputeCannyEdge1stDerivativeOper{}; DerivativeOperator m_ComputeCannyEdge2ndDerivativeOper{}; diff --git a/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.hxx b/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.hxx index a5e35691..599a0f01 100644 --- a/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.hxx +++ b/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.hxx @@ -43,7 +43,7 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::RadiusType>(1); - // Dummy neighborhood used to set up the slices. + // Dummy neighborhood used to set up the slices Neighborhood it; it.SetRadius(r); @@ -60,7 +60,7 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Threa const typename NeighborhoodAlgorithm::ImageBoundaryFacesCalculator::FaceListType faceList = bC(input, outputRegionForThread, radius); - // Process the non-boundady region and then each of the boundary faces. + // Process the non-boundary region and then each of the boundary faces. // These are N-d regions which border the edge of the buffer. for (const auto & face : faceList) { @@ -207,7 +207,7 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Gener m_GaussianFilter->SetSigmaArray(this->m_Sigma); m_GaussianFilter->SetNormalizeAcrossScale(true); m_GaussianFilter->SetInput(input); - // modify to force excution, due to grafting complications + // Modify to force execution, due to grafting complications m_GaussianFilter->Modified(); m_GaussianFilter->Update(); this->UpdateProgress(0.01f); @@ -233,7 +233,7 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Gener }, progress2.GetProcessObject()); - // 3. Non-maximum suppression---------- + // 3. Non-maximum suppression // Calculate the zero crossings of the 2nd directional derivative and write // the result to output buffer. @@ -241,7 +241,7 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Gener zeroCrossFilter->Update(); this->UpdateProgress(0.92f); - // 4. Hysteresis Thresholding--------- + // 4. Hysteresis Thresholding // First get all the edges corresponding to zerocrossings m_MultiplyImageFilter->SetInput1(m_UpdateBuffer1); @@ -253,7 +253,7 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Gener m_MultiplyImageFilter->Update(); this->UpdateProgress(0.95f); - // Then do the double threshoulding upon the edge reponses + // Then do the double thresholding upon the edge responses this->HysteresisThresholding(); this->UpdateProgress(0.99f); @@ -323,8 +323,8 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Follo uit.SetIndex(index); if (Math::ExactlyEquals(uit.Get(), NumericTraits::OneValue())) { - // we must remove the node if we are not going to follow it! - + // Remove the node if we are not going to follow it! + // // Pop the front node from the list and read its index value. auto node = m_NodeList->Front(); // get a pointer to the first node m_NodeList->PopFront(); // unlink the front node @@ -356,9 +356,9 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Follo if (oit.GetPixel(i) > m_LowerThreshold && Math::NotExactlyEquals(uit.Value(), NumericTraits::OneValue())) { - node = m_NodeStore->Borrow(); // get a new node struct - node->m_Value = nIndex; // set its value - m_NodeList->PushFront(node); // add the new node to the list + node = m_NodeStore->Borrow(); // Get a new node struct + node->m_Value = nIndex; // Set its value + m_NodeList->PushFront(node); // Add the new node to the list uit.SetIndex(nIndex); uit.Value() = NumericTraits::OneValue(); @@ -436,11 +436,9 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Threa for (unsigned int i = 0; i < ImageDimension; ++i) { // First calculate the directional derivative - directional[i] = dx[i] / gradMag; - // calculate gradient of 2nd derivative - + // Calculate gradient of 2nd derivative derivPos += dx1[i] * directional[i]; } From 503a5e3ac62cd564c124a9e0996990d8231e6e0b Mon Sep 17 00:00:00 2001 From: "Hans J. Johnson" Date: Wed, 14 Oct 2026 09:56:35 -0500 Subject: [PATCH 12/13] ENH: Add tests for CannyEdgeDetectionRecursiveGaussianImageFilter The filter had no test of its own, only indirect use through the feature generators, so output values and re-execution were unchecked. Add eight tests that mirror ITK's CannyEdgeDetectionImageFilter tests: a baseline comparison and an A/B comparison after changing and restoring the thresholds on cthead1.png, the same A/B check on the 3D lesion volume, and tests of region start index, successive inputs of different sizes, parameters, hysteresis linking, error handling, and a downstream pipeline with the release-data flag on. The baseline was generated by the filter before the ITK history was applied. --- ...ctionRecursiveGaussianImageFilterTest3.png | Bin 0 -> 2943 bytes Data/Input/cthead1.png | Bin 0 -> 197266 bytes test/CMakeLists.txt | 57 +++++ ...ctionRecursiveGaussianImageFilterTest1.cxx | 232 ++++++++++++++++++ ...ctionRecursiveGaussianImageFilterTest2.cxx | 92 +++++++ ...ctionRecursiveGaussianImageFilterTest3.cxx | 67 +++++ ...ctionRecursiveGaussianImageFilterTest4.cxx | 70 ++++++ ...ctionRecursiveGaussianImageFilterTest5.cxx | 91 +++++++ ...ctionRecursiveGaussianImageFilterTest6.cxx | 171 +++++++++++++ ...ctionRecursiveGaussianImageFilterTest7.cxx | 55 +++++ ...ctionRecursiveGaussianImageFilterTest8.cxx | 90 +++++++ 11 files changed, 925 insertions(+) create mode 100644 Data/Baseline/CannyEdgeDetectionRecursiveGaussianImageFilterTest3.png create mode 100644 Data/Input/cthead1.png create mode 100644 test/itkCannyEdgeDetectionRecursiveGaussianImageFilterTest1.cxx create mode 100644 test/itkCannyEdgeDetectionRecursiveGaussianImageFilterTest2.cxx create mode 100644 test/itkCannyEdgeDetectionRecursiveGaussianImageFilterTest3.cxx create mode 100644 test/itkCannyEdgeDetectionRecursiveGaussianImageFilterTest4.cxx create mode 100644 test/itkCannyEdgeDetectionRecursiveGaussianImageFilterTest5.cxx create mode 100644 test/itkCannyEdgeDetectionRecursiveGaussianImageFilterTest6.cxx create mode 100644 test/itkCannyEdgeDetectionRecursiveGaussianImageFilterTest7.cxx create mode 100644 test/itkCannyEdgeDetectionRecursiveGaussianImageFilterTest8.cxx diff --git a/Data/Baseline/CannyEdgeDetectionRecursiveGaussianImageFilterTest3.png b/Data/Baseline/CannyEdgeDetectionRecursiveGaussianImageFilterTest3.png new file mode 100644 index 0000000000000000000000000000000000000000..1cea13e67cbd61b53c105e7a0e828daa3e6c6049 GIT binary patch literal 2943 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

hWGB>vsvm4#?TZ)At2S(sURaM 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 b{>{Jr!*BoYSBK%B|Lm`R`kO!h+kgIFOX +#include + +namespace +{ + +template +typename itk::Image::Pointer +CreateBoxImage(itk::SizeValueType sideLength, itk::IndexValueType boxLower, itk::IndexValueType boxUpper) +{ + using ImageType = itk::Image; + typename ImageType::SizeType size; + size.Fill(sideLength); + + auto image = ImageType::New(); + image->SetRegions(typename ImageType::RegionType(size)); + image->Allocate(true); + + itk::ImageRegionIteratorWithIndex it(image, image->GetBufferedRegion()); + for (it.GoToBegin(); !it.IsAtEnd(); ++it) + { + const auto index = it.GetIndex(); + bool inside = true; + for (unsigned int d = 0; d < VDimension; ++d) + { + inside = inside && index[d] >= boxLower && index[d] < boxUpper; + } + it.Set(inside ? 100.0f : 0.0f); + } + return image; +} + +// True when the index lies within `band` voxels of the surface of the box. +template +bool +IsNearBoxSurface(const itk::Index & index, + itk::IndexValueType boxLower, + itk::IndexValueType boxUpper, + itk::IndexValueType band) +{ + bool withinOuter = true; + bool withinInner = true; + for (unsigned int d = 0; d < VDimension; ++d) + { + withinOuter = withinOuter && index[d] >= boxLower - band && index[d] < boxUpper + band; + withinInner = withinInner && index[d] >= boxLower + band && index[d] < boxUpper - band; + } + return withinOuter && !withinInner; +} + +class ProgressRecorder : public itk::Command +{ +public: + using Self = ProgressRecorder; + using Superclass = itk::Command; + using Pointer = itk::SmartPointer; + itkNewMacro(Self); + + void + Execute(itk::Object * caller, const itk::EventObject & event) override + { + this->Execute(static_cast(caller), event); + } + + void + Execute(const itk::Object * caller, const itk::EventObject &) override + { + const auto * process = dynamic_cast(caller); + m_Values.push_back(process->GetProgress()); + } + + std::vector m_Values; + +protected: + ProgressRecorder() = default; +}; + +template +int +RunTest(itk::SizeValueType sideLength, itk::IndexValueType boxLower, itk::IndexValueType boxUpper) +{ + using ImageType = itk::Image; + using FilterType = itk::CannyEdgeDetectionRecursiveGaussianImageFilter; + + constexpr itk::IndexValueType band = 3; + + auto input = CreateBoxImage(sideLength, boxLower, boxUpper); + + auto filter = FilterType::New(); + ITK_EXERCISE_BASIC_OBJECT_METHODS(filter, CannyEdgeDetectionRecursiveGaussianImageFilter, ImageToImageFilter); + + filter->SetInput(input); + filter->SetSigma(1.0); + filter->SetUpperThreshold(20.0f); + filter->SetLowerThreshold(10.0f); + ITK_TEST_SET_GET_VALUE(20.0f, filter->GetUpperThreshold()); + ITK_TEST_SET_GET_VALUE(10.0f, filter->GetLowerThreshold()); + + auto progress = ProgressRecorder::New(); + filter->AddObserver(itk::ProgressEvent(), progress); + + // The result must not depend on the number of work units. + typename ImageType::Pointer reference; + for (const unsigned int workUnits : { 1u, 2u, 3u, 8u }) + { + filter->SetNumberOfWorkUnits(workUnits); + progress->m_Values.clear(); + filter->Modified(); + ITK_TRY_EXPECT_NO_EXCEPTION(filter->Update()); + + if (progress->m_Values.empty()) + { + std::cerr << "No progress events with " << workUnits << " work units." << std::endl; + return EXIT_FAILURE; + } + const auto [minProgress, maxProgress] = std::minmax_element(progress->m_Values.begin(), progress->m_Values.end()); + if (*minProgress < 0.0f || *maxProgress > 1.0f) + { + std::cerr << "Progress outside [0, 1]: [" << *minProgress << ", " << *maxProgress << "]" << std::endl; + return EXIT_FAILURE; + } + if (*maxProgress < 0.9f) + { + std::cerr << "Progress never approached completion: max " << *maxProgress << std::endl; + return EXIT_FAILURE; + } + + typename ImageType::Pointer output = filter->GetOutput(); + output->DisconnectPipeline(); + + if (reference.IsNull()) + { + reference = output; + continue; + } + itk::ImageRegionConstIteratorWithIndex refIt(reference, reference->GetBufferedRegion()); + itk::ImageRegionConstIteratorWithIndex outIt(output, output->GetBufferedRegion()); + for (; !refIt.IsAtEnd(); ++refIt, ++outIt) + { + if (refIt.Get() != outIt.Get()) + { + std::cerr << "Output differs between 1 and " << workUnits << " work units at " << refIt.GetIndex() << std::endl; + return EXIT_FAILURE; + } + } + } + + // Edges are binary, present, and lie on the surface of the box. + itk::SizeValueType edgeCount = 0; + itk::ImageRegionConstIteratorWithIndex it(reference, reference->GetBufferedRegion()); + for (it.GoToBegin(); !it.IsAtEnd(); ++it) + { + const float value = it.Get(); + if (value != 0.0f && value != 1.0f) + { + std::cerr << "Non-binary edge value " << value << " at " << it.GetIndex() << std::endl; + return EXIT_FAILURE; + } + if (value == 1.0f) + { + ++edgeCount; + if (!IsNearBoxSurface(it.GetIndex(), boxLower, boxUpper, band)) + { + std::cerr << "Edge pixel away from the box surface at " << it.GetIndex() << std::endl; + return EXIT_FAILURE; + } + } + } + if (edgeCount == 0) + { + std::cerr << "No edge pixels detected." << std::endl; + return EXIT_FAILURE; + } + + // A smaller box must move the edges, which guards against a constant output. + auto smallerInput = CreateBoxImage(sideLength, boxLower + 4, boxUpper - 4); + filter->SetInput(smallerInput); + filter->SetNumberOfWorkUnits(4); + ITK_TRY_EXPECT_NO_EXCEPTION(filter->Update()); + itk::ImageRegionConstIteratorWithIndex smallIt(filter->GetOutput(), + filter->GetOutput()->GetBufferedRegion()); + for (smallIt.GoToBegin(); !smallIt.IsAtEnd(); ++smallIt) + { + if (smallIt.Get() == 1.0f && !IsNearBoxSurface(smallIt.GetIndex(), boxLower + 4, boxUpper - 4, band)) + { + std::cerr << "Stale edge pixel after changing the input at " << smallIt.GetIndex() << std::endl; + return EXIT_FAILURE; + } + } + + return EXIT_SUCCESS; +} + +} // namespace + +int +itkCannyEdgeDetectionRecursiveGaussianImageFilterTest1(int, char *[]) +{ + if (RunTest<2>(64, 16, 48) != EXIT_SUCCESS) + { + return EXIT_FAILURE; + } + if (RunTest<3>(40, 10, 30) != EXIT_SUCCESS) + { + return EXIT_FAILURE; + } + + std::cout << "Test finished." << std::endl; + return EXIT_SUCCESS; +} diff --git a/test/itkCannyEdgeDetectionRecursiveGaussianImageFilterTest2.cxx b/test/itkCannyEdgeDetectionRecursiveGaussianImageFilterTest2.cxx new file mode 100644 index 00000000..4e2b681c --- /dev/null +++ b/test/itkCannyEdgeDetectionRecursiveGaussianImageFilterTest2.cxx @@ -0,0 +1,92 @@ +/*========================================================================= + + Program: Lesion Sizing Toolkit + Module: itkCannyEdgeDetectionRecursiveGaussianImageFilterTest2.cxx + + Copyright (c) Kitware Inc. + All rights reserved. + See Copyright.txt or https://www.kitware.com/Copyright.htm for details. + + This software is distributed WITHOUT ANY WARRANTY; without even + the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + PURPOSE. See the above copyright notice for more information. + +=========================================================================*/ + +#include "itkCannyEdgeDetectionRecursiveGaussianImageFilter.h" +#include "itkImageFileReader.h" +#include "itkImageFileWriter.h" +#include "itkRescaleIntensityImageFilter.h" +#include "itkTestingMacros.h" + +int +itkCannyEdgeDetectionRecursiveGaussianImageFilterTest2(int argc, char * argv[]) +{ + if (argc < 4) + { + std::cerr << "Missing parameters." << std::endl; + std::cerr << "Usage: " << itkNameOfTestExecutableMacro(argv) << " InputImage OutputImage1 OutputImage2" + << std::endl; + return EXIT_FAILURE; + } + + constexpr unsigned int Dimension{ 2 }; + using InputImage = itk::Image; + using OutputImage = itk::Image; + using FilterType = itk::CannyEdgeDetectionRecursiveGaussianImageFilter; + + auto reader = itk::ImageFileReader::New(); + reader->SetFileName(argv[1]); + + auto filter = FilterType::New(); + ITK_EXERCISE_BASIC_OBJECT_METHODS(filter, CannyEdgeDetectionRecursiveGaussianImageFilter, ImageToImageFilter); + + filter->SetInput(reader->GetOutput()); + + FilterType::OutputImagePixelType upperThreshold = 25; + filter->SetUpperThreshold(upperThreshold); + ITK_TEST_SET_GET_VALUE(upperThreshold, filter->GetUpperThreshold()); + + FilterType::OutputImagePixelType lowerThreshold = 10; + filter->SetLowerThreshold(lowerThreshold); + ITK_TEST_SET_GET_VALUE(lowerThreshold, filter->GetLowerThreshold()); + + constexpr FilterType::ScalarRealType sigma = 1.0; + filter->SetSigma(sigma); + ITK_TEST_SET_GET_VALUE(sigma, filter->GetSigma()); + + auto rescale = itk::RescaleIntensityImageFilter::New(); + rescale->SetInput(filter->GetOutput()); + rescale->SetOutputMinimum(0); + rescale->SetOutputMaximum(255); + + auto writer = itk::ImageFileWriter::New(); + writer->SetInput(rescale->GetOutput()); + writer->SetFileName(argv[2]); + ITK_TRY_EXPECT_NO_EXCEPTION(writer->Update()); + + // Different thresholds must re-execute the pipeline. + upperThreshold = 20; + filter->SetUpperThreshold(upperThreshold); + ITK_TEST_SET_GET_VALUE(upperThreshold, filter->GetUpperThreshold()); + + lowerThreshold = 5; + filter->SetLowerThreshold(lowerThreshold); + ITK_TEST_SET_GET_VALUE(lowerThreshold, filter->GetLowerThreshold()); + + ITK_TRY_EXPECT_NO_EXCEPTION(rescale->Update()); + + // Restoring the thresholds must reproduce the first result. + upperThreshold = 25; + filter->SetUpperThreshold(upperThreshold); + ITK_TEST_SET_GET_VALUE(upperThreshold, filter->GetUpperThreshold()); + + lowerThreshold = 10; + filter->SetLowerThreshold(lowerThreshold); + ITK_TEST_SET_GET_VALUE(lowerThreshold, filter->GetLowerThreshold()); + + writer->SetFileName(argv[3]); + ITK_TRY_EXPECT_NO_EXCEPTION(writer->Update()); + + return EXIT_SUCCESS; +} diff --git a/test/itkCannyEdgeDetectionRecursiveGaussianImageFilterTest3.cxx b/test/itkCannyEdgeDetectionRecursiveGaussianImageFilterTest3.cxx new file mode 100644 index 00000000..b0c4d7e9 --- /dev/null +++ b/test/itkCannyEdgeDetectionRecursiveGaussianImageFilterTest3.cxx @@ -0,0 +1,67 @@ +/*========================================================================= + + Program: Lesion Sizing Toolkit + Module: itkCannyEdgeDetectionRecursiveGaussianImageFilterTest3.cxx + + Copyright (c) Kitware Inc. + All rights reserved. + See Copyright.txt or https://www.kitware.com/Copyright.htm for details. + + This software is distributed WITHOUT ANY WARRANTY; without even + the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + PURPOSE. See the above copyright notice for more information. + +=========================================================================*/ + +#include "itkCannyEdgeDetectionRecursiveGaussianImageFilter.h" +#include "itkImageFileReader.h" +#include "itkImageFileWriter.h" +#include "itkRescaleIntensityImageFilter.h" +#include "itkSimpleFilterWatcher.h" +#include "itkTestingMacros.h" + +int +itkCannyEdgeDetectionRecursiveGaussianImageFilterTest3(int argc, char * argv[]) +{ + if (argc < 3) + { + std::cerr << "Usage: " << itkNameOfTestExecutableMacro(argv) << " InputImage OutputImage" << std::endl; + return EXIT_FAILURE; + } + + constexpr unsigned int Dimension{ 2 }; + using InputImage = itk::Image; + using OutputImage = itk::Image; + using FilterType = itk::CannyEdgeDetectionRecursiveGaussianImageFilter; + + auto reader = itk::ImageFileReader::New(); + reader->SetFileName(argv[1]); + + auto filter = FilterType::New(); + const itk::SimpleFilterWatcher watcher(filter); + filter->SetInput(reader->GetOutput()); + + constexpr FilterType::OutputImagePixelType upperThreshold{ 30 }; + filter->SetUpperThreshold(upperThreshold); + ITK_TEST_SET_GET_VALUE(upperThreshold, filter->GetUpperThreshold()); + + constexpr FilterType::OutputImagePixelType lowerThreshold{ 15 }; + filter->SetLowerThreshold(lowerThreshold); + ITK_TEST_SET_GET_VALUE(lowerThreshold, filter->GetLowerThreshold()); + + const FilterType::SigmaArrayType sigma(1.0); + filter->SetSigmaArray(sigma); + ITK_TEST_SET_GET_VALUE(sigma, filter->GetSigmaArray()); + + auto rescale = itk::RescaleIntensityImageFilter::New(); + rescale->SetInput(filter->GetOutput()); + rescale->SetOutputMinimum(0); + rescale->SetOutputMaximum(255); + + auto writer = itk::ImageFileWriter::New(); + writer->SetInput(rescale->GetOutput()); + writer->SetFileName(argv[2]); + ITK_TRY_EXPECT_NO_EXCEPTION(writer->Update()); + + return EXIT_SUCCESS; +} diff --git a/test/itkCannyEdgeDetectionRecursiveGaussianImageFilterTest4.cxx b/test/itkCannyEdgeDetectionRecursiveGaussianImageFilterTest4.cxx new file mode 100644 index 00000000..10ef1021 --- /dev/null +++ b/test/itkCannyEdgeDetectionRecursiveGaussianImageFilterTest4.cxx @@ -0,0 +1,70 @@ +/*========================================================================= + + Program: Lesion Sizing Toolkit + Module: itkCannyEdgeDetectionRecursiveGaussianImageFilterTest4.cxx + + Copyright (c) Kitware Inc. + All rights reserved. + See Copyright.txt or https://www.kitware.com/Copyright.htm for details. + + This software is distributed WITHOUT ANY WARRANTY; without even + the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + PURPOSE. See the above copyright notice for more information. + +=========================================================================*/ + +#include "itkCannyEdgeDetectionRecursiveGaussianImageFilter.h" +#include "itkImageFileReader.h" +#include "itkImageFileWriter.h" +#include "itkCastImageFilter.h" +#include "itkTestingMacros.h" + +int +itkCannyEdgeDetectionRecursiveGaussianImageFilterTest4(int argc, char * argv[]) +{ + if (argc < 4) + { + std::cerr << "Usage: " << itkNameOfTestExecutableMacro(argv) + << " InputImage OutputImage1 OutputImage2 [sigma upperThreshold lowerThreshold]" << std::endl; + return EXIT_FAILURE; + } + + constexpr unsigned int Dimension{ 3 }; + using InputImage = itk::Image; + using InternalImage = itk::Image; + using FilterType = itk::CannyEdgeDetectionRecursiveGaussianImageFilter; + + const double sigma = argc > 4 ? std::stod(argv[4]) : 0.7; + const float upperThreshold = argc > 5 ? std::stof(argv[5]) : 150.0f; + const float lowerThreshold = argc > 6 ? std::stof(argv[6]) : 75.0f; + + auto reader = itk::ImageFileReader::New(); + reader->SetFileName(argv[1]); + + auto cast = itk::CastImageFilter::New(); + cast->SetInput(reader->GetOutput()); + + auto filter = FilterType::New(); + filter->SetInput(cast->GetOutput()); + filter->SetSigma(sigma); + filter->SetUpperThreshold(upperThreshold); + filter->SetLowerThreshold(lowerThreshold); + + auto writer = itk::ImageFileWriter::New(); + writer->SetInput(filter->GetOutput()); + writer->SetFileName(argv[2]); + ITK_TRY_EXPECT_NO_EXCEPTION(writer->Update()); + + // Looser thresholds must produce a different, re-executed result. + filter->SetUpperThreshold(upperThreshold / 2.0f); + filter->SetLowerThreshold(lowerThreshold / 2.0f); + ITK_TRY_EXPECT_NO_EXCEPTION(filter->Update()); + + // Restoring the thresholds must reproduce the first result. + filter->SetUpperThreshold(upperThreshold); + filter->SetLowerThreshold(lowerThreshold); + writer->SetFileName(argv[3]); + ITK_TRY_EXPECT_NO_EXCEPTION(writer->Update()); + + return EXIT_SUCCESS; +} diff --git a/test/itkCannyEdgeDetectionRecursiveGaussianImageFilterTest5.cxx b/test/itkCannyEdgeDetectionRecursiveGaussianImageFilterTest5.cxx new file mode 100644 index 00000000..59b45e02 --- /dev/null +++ b/test/itkCannyEdgeDetectionRecursiveGaussianImageFilterTest5.cxx @@ -0,0 +1,91 @@ +/*========================================================================= + + Program: Lesion Sizing Toolkit + Module: itkCannyEdgeDetectionRecursiveGaussianImageFilterTest5.cxx + + Copyright (c) Kitware Inc. + All rights reserved. + See Copyright.txt or https://www.kitware.com/Copyright.htm for details. + + This software is distributed WITHOUT ANY WARRANTY; without even + the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + PURPOSE. See the above copyright notice for more information. + +=========================================================================*/ + +#include "itkCannyEdgeDetectionRecursiveGaussianImageFilter.h" +#include "itkImage.h" +#include "itkImageRegionConstIterator.h" +#include "itkImageRegionIteratorWithIndex.h" +#include "itkTestingMacros.h" + +namespace +{ +using ImageType = itk::Image; + +// A box whose edge strength varies along its contour, so that hysteresis +// linking decides which contour pixels survive. +ImageType::Pointer +CreateImage(const ImageType::IndexType & start) +{ + ImageType::SizeType size; + size.Fill(64); + + auto image = ImageType::New(); + image->SetRegions(ImageType::RegionType(start, size)); + image->Allocate(true); + + itk::ImageRegionIteratorWithIndex it(image, image->GetBufferedRegion()); + for (; !it.IsAtEnd(); ++it) + { + const auto offset = it.GetIndex() - start; + const bool inside = offset[0] >= 16 && offset[0] < 48 && offset[1] >= 16 && offset[1] < 48; + it.Set(inside ? 10.0f + 90.0f * offset[0] / 64.0f + 40.0f * offset[1] / 64.0f : 0.0f); + } + return image; +} + +ImageType::Pointer +Detect(const ImageType::IndexType & start) +{ + using FilterType = itk::CannyEdgeDetectionRecursiveGaussianImageFilter; + auto filter = FilterType::New(); + filter->SetInput(CreateImage(start)); + filter->SetSigma(1.0); + filter->SetUpperThreshold(32.0f); + filter->SetLowerThreshold(8.0f); + filter->Update(); + ImageType::Pointer output = filter->GetOutput(); + output->DisconnectPipeline(); + return output; +} +} // namespace + +// The detected edges must not depend on where the image region starts. +int +itkCannyEdgeDetectionRecursiveGaussianImageFilterTest5(int, char *[]) +{ + ImageType::IndexType origin; + origin.Fill(0); + const auto reference = Detect(origin); + + int status = EXIT_SUCCESS; + for (const auto & start : { ImageType::IndexType{ { 10, -7 } }, ImageType::IndexType{ { -30, 40 } } }) + { + const auto shifted = Detect(start); + itk::ImageRegionConstIterator refIt(reference, reference->GetBufferedRegion()); + itk::ImageRegionConstIterator shiftedIt(shifted, shifted->GetBufferedRegion()); + itk::SizeValueType differing = 0; + for (; !refIt.IsAtEnd(); ++refIt, ++shiftedIt) + { + differing += refIt.Get() != shiftedIt.Get() ? 1 : 0; + } + if (differing != 0) + { + std::cerr << "Region starting at " << start << " differs from the origin-based result in " << differing + << " pixels." << std::endl; + status = EXIT_FAILURE; + } + } + return status; +} diff --git a/test/itkCannyEdgeDetectionRecursiveGaussianImageFilterTest6.cxx b/test/itkCannyEdgeDetectionRecursiveGaussianImageFilterTest6.cxx new file mode 100644 index 00000000..a2a3d6a0 --- /dev/null +++ b/test/itkCannyEdgeDetectionRecursiveGaussianImageFilterTest6.cxx @@ -0,0 +1,171 @@ +/*========================================================================= + + Program: Lesion Sizing Toolkit + Module: itkCannyEdgeDetectionRecursiveGaussianImageFilterTest6.cxx + + Copyright (c) Kitware Inc. + All rights reserved. + See Copyright.txt or https://www.kitware.com/Copyright.htm for details. + + This software is distributed WITHOUT ANY WARRANTY; without even + the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + PURPOSE. See the above copyright notice for more information. + +=========================================================================*/ + +#include "itkCannyEdgeDetectionRecursiveGaussianImageFilter.h" +#include "itkImage.h" +#include "itkImageRegionConstIterator.h" +#include "itkImageRegionIteratorWithIndex.h" +#include "itkTestingMacros.h" + +#include +#include + +namespace +{ +using ImageType = itk::Image; +using FilterType = itk::CannyEdgeDetectionRecursiveGaussianImageFilter; + +ImageType::Pointer +CreateImage(itk::SizeValueType width, itk::SizeValueType height) +{ + ImageType::SizeType size{ { width, height } }; + auto image = ImageType::New(); + image->SetRegions(ImageType::RegionType(size)); + image->Allocate(true); + + itk::ImageRegionIteratorWithIndex it(image, image->GetBufferedRegion()); + for (; !it.IsAtEnd(); ++it) + { + const auto index = it.GetIndex(); + const bool inside = index[0] >= 12 && index[0] < 40 && index[1] >= 12 && index[1] < 40; + it.Set(inside ? 10.0f + 2.0f * index[0] + index[1] : 0.0f); + } + return image; +} + +FilterType::Pointer +Run(const ImageType * image, float upper, float lower, const FilterType::SigmaArrayType & sigma) +{ + auto filter = FilterType::New(); + filter->SetInput(image); + filter->SetSigmaArray(sigma); + filter->SetUpperThreshold(upper); + filter->SetLowerThreshold(lower); + filter->Update(); + return filter; +} + +itk::SizeValueType +CountEdges(const ImageType * image, float upper, float lower, const FilterType::SigmaArrayType & sigma) +{ + const auto filter = Run(image, upper, lower, sigma); + + itk::SizeValueType count = 0; + itk::ImageRegionConstIterator it(filter->GetOutput(), filter->GetOutput()->GetBufferedRegion()); + for (; !it.IsAtEnd(); ++it) + { + count += it.Get() == 1.0f ? 1 : 0; + } + return count; +} + +// Sorted nonzero responses of the non-maximum suppression stage. +std::vector +Responses(const ImageType * image, const FilterType::SigmaArrayType & sigma) +{ + const auto filter = Run(image, 0.0f, 0.0f, sigma); + const ImageType * nms = filter->GetNonMaximumSuppressionImage(); + std::vector values; + itk::ImageRegionConstIterator it(nms, nms->GetBufferedRegion()); + for (; !it.IsAtEnd(); ++it) + { + if (it.Get() > 0.0f) + { + values.push_back(it.Get()); + } + } + std::sort(values.begin(), values.end()); + return values; +} +} // namespace + +int +itkCannyEdgeDetectionRecursiveGaussianImageFilterTest6(int, char *[]) +{ + auto filter = FilterType::New(); + ITK_EXERCISE_BASIC_OBJECT_METHODS(filter, CannyEdgeDetectionRecursiveGaussianImageFilter, ImageToImageFilter); + + // Defaults. + ITK_TEST_SET_GET_VALUE(1.0, filter->GetSigma()); + ITK_TEST_SET_GET_VALUE(0.0f, filter->GetUpperThreshold()); + ITK_TEST_SET_GET_VALUE(0.0f, filter->GetLowerThreshold()); + + // Sigma accessors. + filter->SetSigma(2.0); + FilterType::SigmaArrayType expectedSigma(2.0); + ITK_TEST_SET_GET_VALUE(expectedSigma, filter->GetSigmaArray()); + + FilterType::SigmaArrayType anisotropic; + anisotropic[0] = 0.8; + anisotropic[1] = 1.6; + filter->SetSigmaArray(anisotropic); + ITK_TEST_SET_GET_VALUE(anisotropic, filter->GetSigmaArray()); + ITK_TEST_SET_GET_VALUE(0.8, filter->GetSigma()); + + const auto modifiedTime = filter->GetMTime(); + filter->SetSigmaArray(anisotropic); + ITK_TEST_SET_GET_VALUE(modifiedTime, filter->GetMTime()); + + + // No input. + ITK_TRY_EXPECT_EXCEPTION(filter->Update()); + + // The recursive Gaussian needs at least four pixels along each axis. + filter = FilterType::New(); + auto tiny = CreateImage(3, 40); + filter->SetInput(tiny); + ITK_TRY_EXPECT_EXCEPTION(filter->Update()); + + // Output matches the input geometry, and the non-maximum suppression image is available. + auto image = CreateImage(52, 44); + filter = FilterType::New(); + filter->SetInput(image); + filter->SetUpperThreshold(20.0f); + filter->SetLowerThreshold(10.0f); + ITK_TRY_EXPECT_NO_EXCEPTION(filter->Update()); + ITK_TEST_SET_GET_VALUE(image->GetLargestPossibleRegion(), filter->GetOutput()->GetLargestPossibleRegion()); + ITK_TEST_EXPECT_TRUE(filter->GetNonMaximumSuppressionImage() != nullptr); + ITK_TEST_SET_GET_VALUE(image->GetLargestPossibleRegion(), + filter->GetNonMaximumSuppressionImage()->GetLargestPossibleRegion()); + filter->Print(std::cout); + + // Strong seeds only, then the same seeds linked through weaker responses. + const FilterType::SigmaArrayType isotropic(1.0); + const auto responses = Responses(image, isotropic); + ITK_TEST_EXPECT_TRUE(responses.size() > 20); + const float weak = responses[responses.size() / 4]; + const float strong = responses[responses.size() * 9 / 10]; + + const auto all = CountEdges(image, weak, weak, isotropic); + const auto seedsOnly = CountEdges(image, strong, strong, isotropic); + const auto linked = CountEdges(image, strong, weak, isotropic); + const auto linkedMore = CountEdges(image, strong, 0.0f, isotropic); + const auto none = CountEdges(image, responses.back() + 1.0f, 0.0f, isotropic); + std::cout << "edges: all=" << all << " seedsOnly=" << seedsOnly << " linked=" << linked + << " linkedMore=" << linkedMore << " none=" << none << std::endl; + ITK_TEST_EXPECT_TRUE(seedsOnly > 0); + ITK_TEST_EXPECT_TRUE(linked > seedsOnly); + ITK_TEST_EXPECT_TRUE(linkedMore >= linked); + ITK_TEST_EXPECT_TRUE(all >= linked); + ITK_TEST_EXPECT_TRUE(none == 0); + + // Anisotropic smoothing changes the response. + const auto isotropicResponses = Responses(image, isotropic); + const auto anisotropicResponses = Responses(image, anisotropic); + ITK_TEST_EXPECT_TRUE(isotropicResponses != anisotropicResponses); + + std::cout << "Test finished." << std::endl; + return EXIT_SUCCESS; +} diff --git a/test/itkCannyEdgeDetectionRecursiveGaussianImageFilterTest7.cxx b/test/itkCannyEdgeDetectionRecursiveGaussianImageFilterTest7.cxx new file mode 100644 index 00000000..dab2ca42 --- /dev/null +++ b/test/itkCannyEdgeDetectionRecursiveGaussianImageFilterTest7.cxx @@ -0,0 +1,55 @@ +/*========================================================================= + + Program: Lesion Sizing Toolkit + Module: itkCannyEdgeDetectionRecursiveGaussianImageFilterTest7.cxx + + Copyright (c) Kitware Inc. + All rights reserved. + See Copyright.txt or https://www.kitware.com/Copyright.htm for details. + + This software is distributed WITHOUT ANY WARRANTY; without even + the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + PURPOSE. See the above copyright notice for more information. + +=========================================================================*/ + +#include "itkCannyEdgeDetectionRecursiveGaussianImageFilter.h" +#include "itkImage.h" +#include "itkTestingMacros.h" + +namespace +{ +using ImageType = itk::Image; + +ImageType::Pointer +CreateImage(itk::SizeValueType width, itk::SizeValueType height) +{ + ImageType::SizeType size{ { width, height } }; + auto image = ImageType::New(); + image->SetRegions(ImageType::RegionType(size)); + image->Allocate(true); + return image; +} +} // namespace + +// One filter instance must handle successive inputs of different sizes. +int +itkCannyEdgeDetectionRecursiveGaussianImageFilterTest7(int, char *[]) +{ + using FilterType = itk::CannyEdgeDetectionRecursiveGaussianImageFilter; + auto filter = FilterType::New(); + + for (const auto & size : { ImageType::SizeType{ { 52, 44 } }, + ImageType::SizeType{ { 70, 60 } }, + ImageType::SizeType{ { 30, 36 } }, + ImageType::SizeType{ { 52, 44 } } }) + { + auto image = CreateImage(size[0], size[1]); + filter->SetInput(image); + ITK_TRY_EXPECT_NO_EXCEPTION(filter->UpdateLargestPossibleRegion()); + ITK_TEST_SET_GET_VALUE(image->GetLargestPossibleRegion(), filter->GetOutput()->GetLargestPossibleRegion()); + } + + std::cout << "Test finished." << std::endl; + return EXIT_SUCCESS; +} diff --git a/test/itkCannyEdgeDetectionRecursiveGaussianImageFilterTest8.cxx b/test/itkCannyEdgeDetectionRecursiveGaussianImageFilterTest8.cxx new file mode 100644 index 00000000..31609c2d --- /dev/null +++ b/test/itkCannyEdgeDetectionRecursiveGaussianImageFilterTest8.cxx @@ -0,0 +1,90 @@ +/*========================================================================= + + Program: Lesion Sizing Toolkit + Module: itkCannyEdgeDetectionRecursiveGaussianImageFilterTest8.cxx + + Copyright (c) Kitware Inc. + All rights reserved. + See Copyright.txt or https://www.kitware.com/Copyright.htm for details. + + This software is distributed WITHOUT ANY WARRANTY; without even + the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + PURPOSE. See the above copyright notice for more information. + +=========================================================================*/ + +#include "itkCannyEdgeDetectionRecursiveGaussianImageFilter.h" +#include "itkAddImageFilter.h" +#include "itkCannyEdgeDetectionRecursiveGaussianImageFilter.h" +#include "itkImage.h" +#include "itkImageRegionConstIterator.h" +#include "itkImageRegionIteratorWithIndex.h" +#include "itkTestingMacros.h" + +namespace +{ +using ImageType = itk::Image; + +ImageType::Pointer +CreateImage() +{ + ImageType::SizeType size{ { 48, 40 } }; + auto image = ImageType::New(); + image->SetRegions(ImageType::RegionType(size)); + image->Allocate(true); + + itk::ImageRegionIteratorWithIndex it(image, image->GetBufferedRegion()); + for (; !it.IsAtEnd(); ++it) + { + const auto index = it.GetIndex(); + const bool inside = index[0] > 12 && index[0] < 36 && index[1] > 10 && index[1] < 30; + it.Set(inside ? 80.0f : 0.0f); + } + return image; +} + +double +Sum(const ImageType * image) +{ + double sum = 0.0; + itk::ImageRegionConstIterator it(image, image->GetBufferedRegion()); + for (; !it.IsAtEnd(); ++it) + { + sum += it.Get(); + } + return sum; +} +} // namespace + +// The internal sub-filters must not modify the output's pipeline state, which +// breaks a downstream consumer when the output may be released after use. +int +itkCannyEdgeDetectionRecursiveGaussianImageFilterTest8(int, char *[]) +{ + using FilterType = itk::CannyEdgeDetectionRecursiveGaussianImageFilter; + auto filter = FilterType::New(); + filter->SetInput(CreateImage()); + filter->SetSigma(1.0); + filter->SetUpperThreshold(20.0f); + filter->SetLowerThreshold(10.0f); + filter->ReleaseDataFlagOn(); + + auto add = itk::AddImageFilter::New(); + add->SetInput1(filter->GetOutput()); + add->SetInput2(filter->GetOutput()); + + double sums[3] = { 0.0, 0.0, 0.0 }; + for (double & sum : sums) + { + add->Modified(); + ITK_TRY_EXPECT_NO_EXCEPTION(add->Update()); + sum = Sum(add->GetOutput()); + } + + ITK_TEST_EXPECT_TRUE(sums[0] > 0.0); + ITK_TEST_EXPECT_TRUE(sums[0] == sums[1]); + ITK_TEST_EXPECT_TRUE(sums[1] == sums[2]); + + std::cout << "Test finished." << std::endl; + return EXIT_SUCCESS; +} From 31a2d1ca7fefdd86db8740d4bd860b8d2f7f2d50 Mon Sep 17 00:00:00 2001 From: "Hans J. Johnson" Date: Wed, 14 Oct 2026 11:19:05 -0500 Subject: [PATCH 13/13] ENH: Minimize differences with upstream ITK CannyEdgeDetection Remove the unused m_MaximumError member, give m_Sigma an in-class initializer, make GetNonMaximumSuppressionImage non-const, and place the \ingroup tags as ITK does, so the remaining differences from itk::CannyEdgeDetectionImageFilter are the smoothing parameters only. GaussianImageFilterType is protected as in ITK. ScalarRealType and SigmaArrayType stay public because the Sigma accessors use them. PrintSelf uses print_helper::PrintNumericTrait and the iterators use class template argument deduction when the ITK being built against has them. ITK_VERSION_MAJOR cannot tell, because v6.0b02 and current main both report 6.0.0, so CMake compares ITK_VERSION_TWEAK (the commit date) with the dates those features landed and defines LST_ITK_HAS_PRINT_NUMERIC_TRAIT and LST_ITK_HAS_ITERATOR_CTAD. Without the macros the code keeps the NumericTraits and explicit ImageRegionIterator forms, which build everywhere. --- CMakeLists.txt | 11 +++++++++ ...dgeDetectionRecursiveGaussianImageFilter.h | 20 +++++++--------- ...eDetectionRecursiveGaussianImageFilter.hxx | 24 +++++++++++++++++++ 3 files changed, 44 insertions(+), 11 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index a4a62d69..4ca1edcf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,6 +4,17 @@ project(LesionSizingToolkit) if(NOT ITK_SOURCE_DIR) find_package(ITK REQUIRED) list(APPEND CMAKE_MODULE_PATH ${ITK_CMAKE_DIR}) +endif() + +# ITK_VERSION_TWEAK is the commit date (YYYYMMDD) of ITK builds after v6.0b02; it is empty before that. +if(ITK_VERSION_TWEAK AND ITK_VERSION_TWEAK GREATER_EQUAL 20260227) + add_compile_definitions(LST_ITK_HAS_ITERATOR_CTAD) +endif() +if(ITK_VERSION_TWEAK AND ITK_VERSION_TWEAK GREATER_EQUAL 20260508) + add_compile_definitions(LST_ITK_HAS_PRINT_NUMERIC_TRAIT) +endif() + +if(NOT ITK_SOURCE_DIR) include(ITKModuleExternal) else() itk_module_impl() diff --git a/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.h b/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.h index 0a57c3a5..dfa6ad86 100644 --- a/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.h +++ b/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.h @@ -77,12 +77,11 @@ class ITK_TEMPLATE_EXPORT ListNode * \todo Edge-linking will be added when an itk connected component labeling * algorithm is available. * - * \ingroup LesionSizingToolkit - * * \sa SmoothingRecursiveGaussianImageFilter * \sa ZeroCrossingImageFilter * \sa ThresholdImageFilter * \ingroup ITKImageFeature + * \ingroup LesionSizingToolkit */ template class ITK_TEMPLATE_EXPORT CannyEdgeDetectionRecursiveGaussianImageFilter @@ -109,10 +108,6 @@ class ITK_TEMPLATE_EXPORT CannyEdgeDetectionRecursiveGaussianImageFilter using IndexType = typename TInputImage::IndexType; using SizeValueType = itk::SizeValueType; - using GaussianImageFilterType = SmoothingRecursiveGaussianImageFilter; - using ScalarRealType = typename GaussianImageFilterType::ScalarRealType; - using SigmaArrayType = typename GaussianImageFilterType::SigmaArrayType; - /** The default boundary condition is used unless overridden *in the Evaluate() method. */ using DefaultBoundaryConditionType = ZeroFluxNeumannBoundaryCondition; @@ -144,6 +139,11 @@ class ITK_TEMPLATE_EXPORT CannyEdgeDetectionRecursiveGaussianImageFilter /** Typedef of double containers. */ using ArrayType = FixedArray; + using ScalarRealType = + typename SmoothingRecursiveGaussianImageFilter::ScalarRealType; + using SigmaArrayType = + typename SmoothingRecursiveGaussianImageFilter::SigmaArrayType; + /** Smoothing parameters for the Gaussian filter. */ void SetSigmaArray(const SigmaArrayType & sigmas); @@ -165,7 +165,7 @@ class ITK_TEMPLATE_EXPORT CannyEdgeDetectionRecursiveGaussianImageFilter itkGetConstMacro(LowerThreshold, OutputImagePixelType); OutputImageType * - GetNonMaximumSuppressionImage() const + GetNonMaximumSuppressionImage() { return this->m_MultiplyImageFilter->GetOutput(); } @@ -184,6 +184,7 @@ class ITK_TEMPLATE_EXPORT CannyEdgeDetectionRecursiveGaussianImageFilter void GenerateData() override; + using GaussianImageFilterType = SmoothingRecursiveGaussianImageFilter; using MultiplyImageFilterType = MultiplyImageFilter; private: @@ -218,10 +219,7 @@ class ITK_TEMPLATE_EXPORT CannyEdgeDetectionRecursiveGaussianImageFilter void ThreadedCompute2ndDerivativePos(const OutputImageRegionType & outputRegionForThread); - /** Standard deviation of the gaussian used for smoothing */ - SigmaArrayType m_Sigma; - - ArrayType m_MaximumError; + SigmaArrayType m_Sigma{}; OutputImagePixelType m_UpperThreshold{}; // should be float here? OutputImagePixelType m_LowerThreshold{}; // should be float here? diff --git a/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.hxx b/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.hxx index 599a0f01..f2195e8f 100644 --- a/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.hxx +++ b/include/itkCannyEdgeDetectionRecursiveGaussianImageFilter.hxx @@ -26,6 +26,9 @@ #include "itkImageRegionIteratorWithIndex.h" #include "itkMath.h" #include "itkProgressTransformer.h" +#ifdef LST_ITK_HAS_PRINT_NUMERIC_TRAIT +# include "itkPrintHelper.h" +#endif namespace itk { @@ -116,7 +119,11 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Threa { NeighborhoodType bit(radius, input, face); +#ifdef LST_ITK_HAS_ITERATOR_CTAD + ImageRegionIterator it(this->m_OutputImage, face); +#else ImageRegionIterator it(this->m_OutputImage, face); +#endif bit.OverrideBoundaryCondition(&nbc); bit.GoToBegin(); @@ -275,9 +282,17 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Hyste ListNodeType * node = nullptr; // fix me +#ifdef LST_ITK_HAS_ITERATOR_CTAD + ImageRegionIterator oit(input, input->GetRequestedRegion()); +#else ImageRegionIterator oit(input, input->GetRequestedRegion()); +#endif +#ifdef LST_ITK_HAS_ITERATOR_CTAD + ImageRegionIterator uit(this->m_OutputImage, this->m_OutputImage->GetRequestedRegion()); +#else ImageRegionIterator uit(this->m_OutputImage, this->m_OutputImage->GetRequestedRegion()); +#endif while (!uit.IsAtEnd()) { uit.Value() = OutputImagePixelType{}; @@ -413,7 +428,11 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Threa { bit = ConstNeighborhoodIterator(radius, input, face); bit1 = ConstNeighborhoodIterator(radius, input1, face); +#ifdef LST_ITK_HAS_ITERATOR_CTAD + ImageRegionIterator it(output, face); +#else ImageRegionIterator it(output, face); +#endif bit.OverrideBoundaryCondition(&nbc); bit.GoToBegin(); bit1.GoToBegin(); @@ -503,12 +522,17 @@ CannyEdgeDetectionRecursiveGaussianImageFilter::Print Superclass::PrintSelf(os, indent); os << "Sigma: " << m_Sigma << std::endl; +#ifdef LST_ITK_HAS_PRINT_NUMERIC_TRAIT + print_helper::PrintNumericTrait(os, indent, "UpperThreshold", m_UpperThreshold); + print_helper::PrintNumericTrait(os, indent, "LowerThreshold", m_LowerThreshold); +#else os << indent << "UpperThreshold: " << static_cast::PrintType>(m_UpperThreshold) << std::endl; os << indent << "LowerThreshold: " << static_cast::PrintType>(m_LowerThreshold) << std::endl; +#endif os << indent << "Center: " << m_Center << std::endl; os << indent << "Stride: " << m_Stride << std::endl; itkPrintSelfObjectMacro(GaussianFilter);