diff --git a/Modules/Filtering/LabelMap/ITKKWStyleOverwrite.txt b/Modules/Filtering/LabelMap/ITKKWStyleOverwrite.txt index 833fbdf5696..3ea0f336e71 100644 --- a/Modules/Filtering/LabelMap/ITKKWStyleOverwrite.txt +++ b/Modules/Filtering/LabelMap/ITKKWStyleOverwrite.txt @@ -2,4 +2,5 @@ test/*\.cxx Namespace Disable test/itkAttributePositionLabelMapFilterTest1.cxx Namespace Disable test/itkShapeLabelMapFilterGTest.cxx Namespace Disable test/itkStatisticsLabelMapFilterGTest.cxx Namespace Disable +test/itkUniqueLabelMapFiltersGTest.cxx Namespace Disable test/itkShapeLabelObjectAccessorsTest1.cxx SemicolonSpace Disable diff --git a/Modules/Filtering/LabelMap/include/itkAttributeUniqueLabelMapFilter.hxx b/Modules/Filtering/LabelMap/include/itkAttributeUniqueLabelMapFilter.hxx index 133d1266c57..11545e267bf 100644 --- a/Modules/Filtering/LabelMap/include/itkAttributeUniqueLabelMapFilter.hxx +++ b/Modules/Filtering/LabelMap/include/itkAttributeUniqueLabelMapFilter.hxx @@ -111,6 +111,8 @@ AttributeUniqueLabelMapFilter::GenerateData() } } + assert(newMainLine || (idx[0] >= prevIdx[0])); + if (newMainLine) { // just push the line @@ -121,7 +123,7 @@ AttributeUniqueLabelMapFilter::GenerateData() OffsetValueType prevLength = prev.line.GetLength(); OffsetValueType length = l.line.GetLength(); - if (prevIdx[0] + prevLength >= idx[0]) + if (prevIdx[0] + prevLength > idx[0]) { // the lines are overlapping. We need to choose which line to keep. // the label, the only "attribute" to be guaranteed to be unique, is @@ -193,7 +195,7 @@ AttributeUniqueLabelMapFilter::GenerateData() // keep the previous one. If the previous line fully overlap the // current one, // the current one is fully discarded. - if (prevIdx[0] + prevLength > idx[0] + length) + if (prevIdx[0] + prevLength >= idx[0] + length) { // discarding the current line - just do nothing } @@ -202,9 +204,15 @@ AttributeUniqueLabelMapFilter::GenerateData() IndexType newIdx = idx; newIdx[0] = prevIdx[0] + prevLength; OffsetValueType newLength = idx[0] + length - newIdx[0]; - l.line.SetIndex(newIdx); - l.line.SetLength(newLength); - lines.push_back(l); + + if (newLength > 0) + { + l.line.SetIndex(newIdx); + l.line.SetLength(newLength); + // The front of this line is trimmed, it may occur after a line in the queue + // so the queue is used for the proper ordering. + pq.push(l); + } } } } diff --git a/Modules/Filtering/LabelMap/include/itkShapeUniqueLabelMapFilter.h b/Modules/Filtering/LabelMap/include/itkShapeUniqueLabelMapFilter.h index 9fdaf98df0a..8ff188ab566 100644 --- a/Modules/Filtering/LabelMap/include/itkShapeUniqueLabelMapFilter.h +++ b/Modules/Filtering/LabelMap/include/itkShapeUniqueLabelMapFilter.h @@ -177,6 +177,8 @@ class ITK_TEMPLATE_EXPORT ShapeUniqueLabelMapFilter : public InPlaceLabelMapFilt } } + assert(newMainLine || (idx[0] >= prevIdx[0])); + if (newMainLine) { // just push the line @@ -187,7 +189,7 @@ class ITK_TEMPLATE_EXPORT ShapeUniqueLabelMapFilter : public InPlaceLabelMapFilt OffsetValueType prevLength = prev.line.GetLength(); OffsetValueType length = l.line.GetLength(); - if (prevIdx[0] + prevLength >= idx[0]) + if (prevIdx[0] + prevLength > idx[0]) { // the lines are overlapping. We need to choose which line to keep. // the label, the only "attribute" to be guaranteed to be unique, is @@ -244,6 +246,7 @@ class ITK_TEMPLATE_EXPORT ShapeUniqueLabelMapFilter : public InPlaceLabelMapFilt prevLength = idx[0] - prevIdx[0]; if (prevLength != 0) { + assert(prevIdx[0] <= idx[0]); lines.back().line.SetLength(idx[0] - prevIdx[0]); } else @@ -259,7 +262,7 @@ class ITK_TEMPLATE_EXPORT ShapeUniqueLabelMapFilter : public InPlaceLabelMapFilt // keep the previous one. If the previous line fully overlap the // current one, // the current one is fully discarded. - if (prevIdx[0] + prevLength > idx[0] + length) + if (prevIdx[0] + prevLength >= idx[0] + length) { // discarding the current line - just do nothing } @@ -268,9 +271,14 @@ class ITK_TEMPLATE_EXPORT ShapeUniqueLabelMapFilter : public InPlaceLabelMapFilt IndexType newIdx = idx; newIdx[0] = prevIdx[0] + prevLength; OffsetValueType newLength = idx[0] + length - newIdx[0]; - l.line.SetIndex(newIdx); - l.line.SetLength(newLength); - lines.push_back(l); + if (newLength > 0) + { + l.line.SetIndex(newIdx); + l.line.SetLength(newLength); + // The front of this line is trimmed, it may occur after a line in the queue + // so the queue is used for the proper ordering. + priorityQueue.push(l); + } } } } diff --git a/Modules/Filtering/LabelMap/test/CMakeLists.txt b/Modules/Filtering/LabelMap/test/CMakeLists.txt index dac23bd4f7c..0e15f959ea8 100644 --- a/Modules/Filtering/LabelMap/test/CMakeLists.txt +++ b/Modules/Filtering/LabelMap/test/CMakeLists.txt @@ -1569,14 +1569,10 @@ itk_add_test( --compare DATA{Baseline/cthead1Label-label-statistics-unique-labelmap-baseline1.png} ${ITK_TEST_OUTPUT_DIR}/itkStatisticsUniqueLabelMapFilterTest1.png - --compare - DATA{Baseline/cthead1Label-label-statistics-unique-labelmap-dilate-baseline1.png} - ${ITK_TEST_OUTPUT_DIR}/itkStatisticsUniqueLabelMapFilterDilationStability1.png itkStatisticsUniqueLabelMapFilterTest1 DATA{${ITK_DATA_ROOT}/Input/cthead1Label.png} DATA{${ITK_DATA_ROOT}/Input/cthead1.png} ${ITK_TEST_OUTPUT_DIR}/itkStatisticsUniqueLabelMapFilterTest1.png - ${ITK_TEST_OUTPUT_DIR}/itkStatisticsUniqueLabelMapFilterDilationStability1.png 0 100) itk_add_test( @@ -1587,19 +1583,17 @@ itk_add_test( --compare DATA{Baseline/cthead1Label-label-statistics-unique-labelmap-baseline2.png} ${ITK_TEST_OUTPUT_DIR}/itkStatisticsUniqueLabelMapFilterTest2.png - --compare - DATA{Baseline/cthead1Label-label-statistics-unique-labelmap-dilate-baseline2.png} - ${ITK_TEST_OUTPUT_DIR}/itkStatisticsUniqueLabelMapFilterDilationStability2.png --compareNumberOfPixelsTolerance 35 itkStatisticsUniqueLabelMapFilterTest1 DATA{${ITK_DATA_ROOT}/Input/cthead1Label.png} DATA{${ITK_DATA_ROOT}/Input/cthead1.png} ${ITK_TEST_OUTPUT_DIR}/itkStatisticsUniqueLabelMapFilterTest2.png - ${ITK_TEST_OUTPUT_DIR}/itkStatisticsUniqueLabelMapFilterDilationStability2.png 1 100) -set(ITKLabelMapGTests itkShapeLabelMapFilterGTest.cxx itkStatisticsLabelMapFilterGTest.cxx) +set(ITKLabelMapGTests itkShapeLabelMapFilterGTest.cxx + itkStatisticsLabelMapFilterGTest.cxx + itkUniqueLabelMapFiltersGTest.cxx) creategoogletestdriver(ITKLabelMap "${ITKLabelMap-Test_LIBRARIES}" "${ITKLabelMapGTests}") diff --git a/Modules/Filtering/LabelMap/test/itkStatisticsUniqueLabelMapFilterTest1.cxx b/Modules/Filtering/LabelMap/test/itkStatisticsUniqueLabelMapFilterTest1.cxx index c98f058a9ba..5d0a660fd1e 100644 --- a/Modules/Filtering/LabelMap/test/itkStatisticsUniqueLabelMapFilterTest1.cxx +++ b/Modules/Filtering/LabelMap/test/itkStatisticsUniqueLabelMapFilterTest1.cxx @@ -29,16 +29,47 @@ #include "itkGrayscaleDilateImageFilter.h" #include "itkObjectByObjectLabelMapFilter.h" +template +int +CheckLabelMapOverlap(TLabelMap * labelMap) +{ + int exitCode = EXIT_SUCCESS; + + for (auto & labelObject : labelMap->GetLabelObjects()) + { + // Manually check each label object against all other label objects, to ensure that no two label objects share an + // index. + for (itk::SizeValueType lineNumber = 0; lineNumber < labelObject->GetNumberOfLines(); ++lineNumber) + { + auto line = labelObject->GetLine(lineNumber); + auto idx = line.GetIndex(); + ITK_TEST_EXPECT_TRUE(line.GetLength() <= labelObject->GetNumberOfPixels()); + for (itk::SizeValueType lengthIndex = 0; lengthIndex < line.GetLength(); ++lengthIndex) + { + for (auto & checkObject : labelMap->GetLabelObjects()) + { + if (checkObject != labelObject && checkObject->HasIndex(idx)) + { + std::cerr << "Label: " << int(labelObject->GetLabel()) << " and " << int(checkObject->GetLabel()) + << " has index " << idx << std::endl; + exitCode = EXIT_FAILURE; + } + } + ++idx[0]; + } + } + } + return exitCode; +} + int itkStatisticsUniqueLabelMapFilterTest1(int argc, char * argv[]) { - // ToDo: remove dilationOutput once the JIRA issue 3370 has been solved - // Then, argc != 6 - if (argc != 7) + if (argc != 6) { std::cerr << "Missing parameters." << std::endl; std::cerr << "Usage: " << itkNameOfTestExecutableMacro(argv); - std::cerr << " input feature output dilationOutput"; + std::cerr << " input feature output"; std::cerr << " reverseOrdering attribute"; std::cerr << std::endl; return EXIT_FAILURE; @@ -46,9 +77,8 @@ itkStatisticsUniqueLabelMapFilterTest1(int argc, char * argv[]) const char * inputImage = argv[1]; const char * featureImage = argv[2]; const char * outputImage = argv[3]; - const char * dilationOutput = argv[4]; - const bool reverseOrdering = std::stoi(argv[5]); - const unsigned int attribute = std::stoi(argv[6]); + const bool reverseOrdering = std::stoi(argv[4]); + const unsigned int attribute = std::stoi(argv[5]); constexpr unsigned int Dimension = 2; @@ -123,6 +153,15 @@ itkStatisticsUniqueLabelMapFilterTest1(int argc, char * argv[]) itk::SimpleFilterWatcher watcher(unique, "filter"); + ITK_TRY_EXPECT_NO_EXCEPTION(unique->Update()); + + int exitCode = CheckLabelMapOverlap(unique->GetOutput()); + + if (exitCode == EXIT_FAILURE) + { + std::cerr << "Overlap detected in the label map." << std::endl; + } + using LabelMapToImageFilterType = itk::LabelMapToLabelImageFilter; auto labelMapToImageFilter = LabelMapToImageFilterType::New(); labelMapToImageFilter->SetInput(unique->GetOutput()); @@ -135,14 +174,7 @@ itkStatisticsUniqueLabelMapFilterTest1(int argc, char * argv[]) ITK_TRY_EXPECT_NO_EXCEPTION(writer->Update()); - - // WARNING: TEMPORARY: JIRA ISSUE 3370 - // Writing an additional output of just the dilated label - writer->SetInput(grayscaleDilateFilter->GetOutput()); - writer->SetFileName(dilationOutput); - writer->UseCompressionOn(); - ITK_TRY_EXPECT_NO_EXCEPTION(writer->Update()); - return EXIT_SUCCESS; + return exitCode; } diff --git a/Modules/Filtering/LabelMap/test/itkUniqueLabelMapFiltersGTest.cxx b/Modules/Filtering/LabelMap/test/itkUniqueLabelMapFiltersGTest.cxx new file mode 100644 index 00000000000..ae1c3957cfb --- /dev/null +++ b/Modules/Filtering/LabelMap/test/itkUniqueLabelMapFiltersGTest.cxx @@ -0,0 +1,309 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + *=========================================================================*/ + +#include "itkGTest.h" + +#include "itkImage.h" +#include "itkLabelImageToStatisticsLabelMapFilter.h" +#include "itkImageFileWriter.h" +#include + + +#include "itkLabelImageToLabelMapFilter.h" +#include "itkObjectByObjectLabelMapFilter.h" +#include "itkShapeLabelObjectAccessors.h" +#include "itkFlatStructuringElement.h" +#include "itkBinaryDilateImageFilter.h" +#include "itkLabelUniqueLabelMapFilter.h" +#include "itkTestingHashImageFilter.h" +#include "itksys/SystemTools.hxx" +#include "itkTestDriverIncludeRequiredFactories.h" + + +namespace +{ + +class UniqueLabelMapFixture : public ::testing::Test +{ +public: + UniqueLabelMapFixture() = default; + ~UniqueLabelMapFixture() override = default; + +protected: + void + SetUp() override + { + RegisterRequiredFactories(); + } + void + TearDown() override + {} + + std::string + GetTestName() const + { + return ::testing::UnitTest::GetInstance()->current_test_info()->name(); + } + + template + struct FixtureUtilities + { + static const unsigned int Dimension = D; + + using LabelPixelType = TPixelType; + using LabelImageType = itk::Image; + using IndexType = typename LabelImageType::IndexType; + + using LabelObjectType = itk::StatisticsLabelObject; + + using LabelMapType = itk::LabelMap>; + + + static typename LabelImageType::Pointer + CreateLabelImage(const std::vector & indices) + { + const size_t size = 25; + auto image = LabelImageType::New(); + + typename LabelImageType::SizeType imageSize; + imageSize.Fill(size); + image->SetRegions(typename LabelImageType::RegionType(imageSize)); + image->Allocate(); + image->FillBuffer(0); + + for (LabelPixelType id = 0; id < indices.size(); ++id) + { + image->SetPixel(indices[id], id + 1); + } + return image; + } + + static typename LabelMapType::Pointer + LabelMapFromLabelImage(const LabelImageType * image, unsigned int dilateRadius = 0) + { + using ToLabelMapType = itk::LabelImageToLabelMapFilter; + auto toLabelMap = ToLabelMapType::New(); + toLabelMap->SetInput(image); + + if (dilateRadius == 0) + { + toLabelMap->Update(); + return toLabelMap->GetOutput(); + } + + using KernelType = itk::FlatStructuringElement; + using DilateType = itk::BinaryDilateImageFilter; + auto dilate = DilateType::New(); + typename KernelType::SizeType rad; + rad.Fill(dilateRadius); + dilate->SetKernel(KernelType::Ball(rad)); + + + using OIType = itk::ObjectByObjectLabelMapFilter; + auto oi = OIType::New(); + oi->SetInput(toLabelMap->GetOutput()); + oi->SetFilter(dilate); + oi->SetPadSize(rad); + + oi->Update(); + + return oi->GetOutput(); + } + }; + + template + static typename itk::Image::Pointer + LabelMapToLabelImage(TLabelMap * labelMap) + { + using ImageType = itk::Image; + using L2IType = itk::LabelMapToLabelImageFilter; + auto l2i = L2IType::New(); + l2i->SetInput(labelMap); + l2i->Update(); + return l2i->GetOutput(); + } + + + template + void + CheckLabelMapOverlap(TLabelMap * labelMap) + { + for (auto & labelObject : labelMap->GetLabelObjects()) + { + // Manually check each label object against all other label objects, to ensure that no two label objects share an + // index. + for (itk::SizeValueType lineNumber = 0; lineNumber < labelObject->GetNumberOfLines(); ++lineNumber) + { + auto line = labelObject->GetLine(lineNumber); + auto idx = line.GetIndex(); + ASSERT_LE(line.GetLength(), labelObject->Size()); + for (itk::SizeValueType lengthIndex = 0; lengthIndex < line.GetLength(); ++lengthIndex) + { + for (auto & checkObject : labelMap->GetLabelObjects()) + { + if (checkObject != labelObject) + { + EXPECT_FALSE(checkObject->HasIndex(idx)) + << "Label: " << int(labelObject->GetLabel()) << " and " << int(checkObject->GetLabel()) << " has index " + << idx << std::endl; + } + } + ++idx[0]; + } + } + } + } + + template + static std::string + MD5Hash(const TImageType * image) + { + + using HashFilter = itk::Testing::HashImageFilter; + auto hasher = HashFilter::New(); + hasher->SetInput(image); + hasher->InPlaceOff(); + hasher->Update(); + return hasher->GetHash(); + } +}; +} // namespace + + +TEST_F(UniqueLabelMapFixture, EmptyImage) +{ + const std::vector::IndexType> indices = {}; + auto image = FixtureUtilities<2>::CreateLabelImage(indices); + auto labelMap = FixtureUtilities<2>::LabelMapFromLabelImage(image.GetPointer(), 15); + + auto filter = itk::LabelUniqueLabelMapFilter::New(); + filter->SetInput(labelMap); + + CheckLabelMapOverlap(filter->GetOutput()); + filter->Update(); + + auto out = LabelMapToLabelImage(filter->GetOutput()); + + // check the hash of out, should be all zeros + EXPECT_EQ(MD5Hash(out.GetPointer()), "393017b9101a884b66d64849d99a7d05"); +} + + +TEST_F(UniqueLabelMapFixture, OneLabel) +{ + const std::vector::IndexType> indices = { { 10, 10 } }; + auto image = FixtureUtilities<2>::CreateLabelImage(indices); + auto labelMap = FixtureUtilities<2>::LabelMapFromLabelImage(image.GetPointer(), 0); + + auto filter = itk::LabelUniqueLabelMapFilter::New(); + filter->SetInput(labelMap); + filter->Update(); + + CheckLabelMapOverlap(filter->GetOutput()); + + auto out = LabelMapToLabelImage(filter->GetOutput()); + + EXPECT_EQ(MD5Hash(out.GetPointer()), MD5Hash(image.GetPointer())); + EXPECT_EQ(MD5Hash(out.GetPointer()), "9c8ee8f2fe887fd6d2393d6416df3fb6"); +} + + +TEST_F(UniqueLabelMapFixture, OnesLabel) +{ + const std::vector::IndexType> indices = { { 0, 0 }, { 1, 0 }, { 2, 0 }, { 3, 0 }, + { 0, 4 }, { 2, 4 }, { 0, 5 } }; + auto image = FixtureUtilities<2>::CreateLabelImage(indices); + auto labelMap = FixtureUtilities<2>::LabelMapFromLabelImage(image.GetPointer(), 0); + + auto filter = itk::LabelUniqueLabelMapFilter::New(); + filter->SetInput(labelMap); + filter->Update(); + + CheckLabelMapOverlap(filter->GetOutput()); + + auto out = LabelMapToLabelImage(filter->GetOutput()); + + EXPECT_EQ(MD5Hash(out.GetPointer()), MD5Hash(image.GetPointer())); + EXPECT_EQ(MD5Hash(out.GetPointer()), "220048b56395d98a8f20a5b1733bdde6"); +} + + +TEST_F(UniqueLabelMapFixture, Dilate1) +{ + const std::vector::IndexType> indices = { { 0, 0 }, { 1, 0 }, { 2, 0 }, { 3, 0 }, + { 0, 4 }, { 2, 4 }, { 0, 10 } }; + auto image = FixtureUtilities<2>::CreateLabelImage(indices); + auto labelMap = FixtureUtilities<2>::LabelMapFromLabelImage(image.GetPointer(), 1); + + auto filter = itk::LabelUniqueLabelMapFilter::New(); + filter->SetInput(labelMap); + filter->InPlaceOff(); + filter->ReverseOrderingOff(); + filter->Update(); + + CheckLabelMapOverlap(filter->GetOutput()); + + auto out = LabelMapToLabelImage(filter->GetOutput()); + + EXPECT_EQ(MD5Hash(out.GetPointer()), "8bfc8570ee203fa68be18fe055cf389d"); + itk::WriteImage(out.GetPointer(), GetTestName() + "_off.png"); + + filter->ReverseOrderingOn(); + filter->Update(); + + CheckLabelMapOverlap(filter->GetOutput()); + + out = LabelMapToLabelImage(filter->GetOutput()); + + EXPECT_EQ(MD5Hash(out.GetPointer()), "bef76c79168969548f1a8090d46b5f7e"); + itk::WriteImage(out.GetPointer(), GetTestName() + "_on.png"); +} + + +TEST_F(UniqueLabelMapFixture, Dilate2) +{ + const std::vector::IndexType> indices = { { 0, 0 }, { 1, 0 }, { 2, 0 }, { 3, 0 }, + { 0, 4 }, { 2, 4 }, { 0, 5 } }; + auto image = FixtureUtilities<2>::CreateLabelImage(indices); + auto labelMap = FixtureUtilities<2>::LabelMapFromLabelImage(image.GetPointer(), 2); + + auto filter = itk::LabelUniqueLabelMapFilter::New(); + filter->SetInput(labelMap); + filter->InPlaceOff(); + filter->ReverseOrderingOff(); + filter->Update(); + + CheckLabelMapOverlap(filter->GetOutput()); + + auto out = LabelMapToLabelImage(filter->GetOutput()); + + EXPECT_EQ(MD5Hash(out.GetPointer()), "02ec62c193413dd82cb0feaee1ee0b12"); + itk::WriteImage(out.GetPointer(), GetTestName() + "_off.png"); + + + filter->ReverseOrderingOn(); + filter->Update(); + + CheckLabelMapOverlap(filter->GetOutput()); + + out = LabelMapToLabelImage(filter->GetOutput()); + + EXPECT_EQ(MD5Hash(out.GetPointer()), "1c6901199b01ac095511792f447578a3"); + + itk::WriteImage(out.GetPointer(), GetTestName() + "_on.png"); +}