diff --git a/Modules/Filtering/ImageStatistics/include/itkLabelOverlapLabelSetMeasures.h b/Modules/Filtering/ImageStatistics/include/itkLabelOverlapLabelSetMeasures.h new file mode 100644 index 000000000000..ba06a5b4dcbe --- /dev/null +++ b/Modules/Filtering/ImageStatistics/include/itkLabelOverlapLabelSetMeasures.h @@ -0,0 +1,76 @@ +/*========================================================================= + * + * 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. + * + *=========================================================================*/ +#ifndef itkLabelOverlapLabelSetMeasures_h +#define itkLabelOverlapLabelSetMeasures_h + +#include "itkIntTypes.h" + +namespace itk +{ +/** \class LabelOverlapLabelSetMeasures + * \brief Metrics stored per label + * \ingroup ITKImageStatistics + */ +struct LabelOverlapLabelSetMeasures +{ + SizeValueType m_Source{ 0 }; + SizeValueType m_Target{ 0 }; + SizeValueType m_Union{ 0 }; + SizeValueType m_Intersection{ 0 }; + SizeValueType m_SourceComplement{ 0 }; + SizeValueType m_TargetComplement{ 0 }; + + // ITK's igenerator wrapping pipeline does not expose public data members of + // a struct to Python. Provide explicit getters so wrapped consumers (Python + // tests, downstream bindings) can read these fields. Also provide + // descriptively-named aliases for the m_-prefixed forms; the m_Foo names + // remain accessible from C++ for backward compatibility with consumers that + // construct or mutate these values directly. + SizeValueType + GetSource() const + { + return m_Source; + } + SizeValueType + GetTarget() const + { + return m_Target; + } + SizeValueType + GetUnion() const + { + return m_Union; + } + SizeValueType + GetIntersection() const + { + return m_Intersection; + } + SizeValueType + GetSourceComplement() const + { + return m_SourceComplement; + } + SizeValueType + GetTargetComplement() const + { + return m_TargetComplement; + } +}; +} // namespace itk +#endif // itkLabelOverlapLabelSetMeasures_h diff --git a/Modules/Filtering/ImageStatistics/include/itkLabelOverlapMeasuresImageFilter.h b/Modules/Filtering/ImageStatistics/include/itkLabelOverlapMeasuresImageFilter.h index 2ca3429b9792..1e893152b114 100644 --- a/Modules/Filtering/ImageStatistics/include/itkLabelOverlapMeasuresImageFilter.h +++ b/Modules/Filtering/ImageStatistics/include/itkLabelOverlapMeasuresImageFilter.h @@ -20,12 +20,12 @@ #include "itkImageSink.h" #include "itkNumericTraits.h" +#include "itkLabelOverlapLabelSetMeasures.h" #include #include namespace itk { - /** \class LabelOverlapMeasuresImageFilter * \brief Computes overlap measures between the same set of labels of * pixels of two images. Background is assumed to be 0. @@ -71,25 +71,13 @@ class ITK_TEMPLATE_EXPORT LabelOverlapMeasuresImageFilter : public ImageSink::RealType; - /** \class LabelSetMeasures - * \brief Metrics stored per label - * \ingroup ITKImageStatistics - */ - class LabelSetMeasures - { - public: - // default constructor/copy/move etc... - - SizeValueType m_Source{ 0 }; - SizeValueType m_Target{ 0 }; - SizeValueType m_Union{ 0 }; - SizeValueType m_Intersection{ 0 }; - SizeValueType m_SourceComplement{ 0 }; - SizeValueType m_TargetComplement{ 0 }; - }; +#ifndef ITK_FUTURE_LEGACY_REMOVE + /** Deprecated backward-compatibility alias. Use LabelOverlapLabelSetMeasures directly. */ + using LabelSetMeasures = LabelOverlapLabelSetMeasures; +#endif // !ITK_FUTURE_LEGACY_REMOVE /** Type of the map used to store data per label */ - using MapType = std::unordered_map; + using MapType = std::unordered_map; using MapIterator = typename MapType::iterator; using MapConstIterator = typename MapType::const_iterator; @@ -111,6 +99,48 @@ class ITK_TEMPLATE_EXPORT LabelOverlapMeasuresImageFilter : public ImageSinkm_LabelSetMeasures; } + /** Get the labels for which set measures have been computed. + * + * Provided for Python ergonomics: SWIG cannot wrap + * `std::unordered_map` as a + * Python dict across submodule boundaries (the value type is `%import`-ed + * rather than `%include`-d, so `%template` is silently dropped). Pair + * this accessor with `GetMeasureForLabel()` to iterate labels and look up + * their measures from Python: + * + * \code{.py} + * for label in filter.GetLabels(): + * m = filter.GetMeasureForLabel(label) + * \endcode + */ + std::vector + GetLabels() const + { + std::vector labels; + labels.reserve(this->m_LabelSetMeasures.size()); + for (const auto & kv : this->m_LabelSetMeasures) + { + labels.push_back(kv.first); + } + return labels; + } + + /** Get the per-label measures struct for a single label. Throws + * itk::ExceptionObject if the label is not present in the map. + * See GetLabels() for the Python iteration idiom. + */ + LabelOverlapLabelSetMeasures + GetMeasureForLabel(LabelType label) const + { + const auto it = this->m_LabelSetMeasures.find(label); + if (it == this->m_LabelSetMeasures.end()) + { + itkExceptionMacro("Label " << static_cast::PrintType>(label) + << " is not present in the label set measures map."); + } + return it->second; + } + // Overlap agreement metrics /** Get the total overlap over all labels. */ diff --git a/Modules/Filtering/ImageStatistics/test/Input/DzZ_Seeds.seg.nrrd.sha512 b/Modules/Filtering/ImageStatistics/test/Input/DzZ_Seeds.seg.nrrd.sha512 new file mode 100644 index 000000000000..09acdc181124 --- /dev/null +++ b/Modules/Filtering/ImageStatistics/test/Input/DzZ_Seeds.seg.nrrd.sha512 @@ -0,0 +1 @@ +4106f7a97659761a7fd42594a4f19aa9dad312445a1c9ff63f397f0c32cb952ee5d6ba6c6582951d883e6f40587091437a872c5fce966670d57f4984125c7a5d diff --git a/Modules/Filtering/ImageStatistics/test/Input/DzZ_T1.seg.nrrd.sha512 b/Modules/Filtering/ImageStatistics/test/Input/DzZ_T1.seg.nrrd.sha512 new file mode 100644 index 000000000000..db3c2a61feca --- /dev/null +++ b/Modules/Filtering/ImageStatistics/test/Input/DzZ_T1.seg.nrrd.sha512 @@ -0,0 +1 @@ +eb93426d1ee5f00d722979fd9dbfd8701f27bbb4256522e3d25c4db0f88f33b6a2db1ce572613e38c1a91787ca911e2399d206a137f7e987421b357bd5925b5d diff --git a/Modules/Filtering/ImageStatistics/wrapping/CMakeLists.txt b/Modules/Filtering/ImageStatistics/wrapping/CMakeLists.txt index 61d2168cffbd..82dfc4eb1ef3 100644 --- a/Modules/Filtering/ImageStatistics/wrapping/CMakeLists.txt +++ b/Modules/Filtering/ImageStatistics/wrapping/CMakeLists.txt @@ -1,2 +1,3 @@ itk_wrap_module(ITKImageStatistics) +set(WRAPPER_SUBMODULE_ORDER itkLabelOverlapLabelSetMeasures) itk_auto_load_and_end_wrap_submodules() diff --git a/Modules/Filtering/ImageStatistics/wrapping/itkLabelOverlapLabelSetMeasures.wrap b/Modules/Filtering/ImageStatistics/wrapping/itkLabelOverlapLabelSetMeasures.wrap new file mode 100644 index 000000000000..5c74c57e67c0 --- /dev/null +++ b/Modules/Filtering/ImageStatistics/wrapping/itkLabelOverlapLabelSetMeasures.wrap @@ -0,0 +1 @@ +itk_wrap_simple_class("itk::LabelOverlapLabelSetMeasures") diff --git a/Modules/Filtering/ImageStatistics/wrapping/test/CMakeLists.txt b/Modules/Filtering/ImageStatistics/wrapping/test/CMakeLists.txt new file mode 100644 index 000000000000..6a266c79f337 --- /dev/null +++ b/Modules/Filtering/ImageStatistics/wrapping/test/CMakeLists.txt @@ -0,0 +1,14 @@ +set(test_input_dir ${itk-module_SOURCE_DIR}/test/Input) + +# let's make sure 3D uchar images are wrapped +list(FIND ITK_WRAP_IMAGE_DIMS 3 wrap_3_index) +if(ITK_WRAP_PYTHON AND ITK_WRAP_unsigned_char AND wrap_3_index GREATER -1) + itk_python_add_test( + NAME LabelOverlapMeasuresImageFilterTest + TEST_DRIVER_ARGS + COMMAND + ${CMAKE_CURRENT_SOURCE_DIR}/itkLabelOverlapMeasuresImageFilterTest.py + DATA{${test_input_dir}/DzZ_T1.seg.nrrd} + DATA{${test_input_dir}/DzZ_Seeds.seg.nrrd} + ) +endif() diff --git a/Modules/Filtering/ImageStatistics/wrapping/test/itkLabelOverlapMeasuresImageFilterTest.py b/Modules/Filtering/ImageStatistics/wrapping/test/itkLabelOverlapMeasuresImageFilterTest.py new file mode 100644 index 000000000000..79507a051671 --- /dev/null +++ b/Modules/Filtering/ImageStatistics/wrapping/test/itkLabelOverlapMeasuresImageFilterTest.py @@ -0,0 +1,51 @@ +# ========================================================================== +# +# 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. +# +# ==========================================================================*/ + + +import itk +from sys import argv + +itk.auto_progress(2) + +ref = itk.imread(argv[1], itk.UC) +seg = itk.imread(argv[2], itk.UC) + +lom_filter = itk.LabelOverlapMeasuresImageFilter[itk.Image[itk.UC, 3]].New() +lom_filter.SetTargetImage(seg) +lom_filter.SetSourceImage(ref) +lom_filter.UpdateLargestPossibleRegion() + +# GetLabelSetMeasures() returns std::unordered_map, which SWIG +# cannot wrap as a Python dict across submodule boundaries. Use the paired +# accessors GetLabels() + GetMeasureForLabel() for Python iteration. +labels = list(lom_filter.GetLabels()) +print(f"Found {len(labels)} labels") +assert len(labels) > 0, "GetLabels() returned no labels" +for label in sorted(labels): + measure = lom_filter.GetMeasureForLabel(label) + # Use the explicit Get* accessors (igenerator does not expose public data + # members of a struct to Python; the m_Foo fields are not visible). + intersection = measure.GetIntersection() + union = measure.GetUnion() + print(f"Label: {label}, i: {intersection}, u: {union}") + assert ( + intersection >= 0 + ), f"Label {label}: intersection ({intersection}) must be non-negative" + assert ( + union >= intersection + ), f"Label {label}: union ({union}) must be >= intersection ({intersection})"