Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FILTER: ITK Projection Filters #1162

Merged
merged 11 commits into from
Jan 16, 2025

Conversation

nyoungbq
Copy link
Contributor

Naming Conventions

Naming of variables should descriptive where needed. Loop Control Variables can use i if warranted. Most of these conventions are enforced through the clang-tidy and clang-format configuration files. See the file simplnx/docs/Code_Style_Guide.md for a more in depth explanation.

Filter Checklist

The help file simplnx/docs/Porting_Filters.md has documentation to help you port or write new filters. At the top is a nice checklist of items that should be noted when porting a filter.

Unit Testing

The idea of unit testing is to test the filter for proper execution and error handling. How many variations on a unit test each filter needs is entirely dependent on what the filter is doing. Generally, the variations can fall into a few categories:

  • 1 Unit test to test output from the filter against known exemplar set of data
  • 1 Unit test to test invalid input code paths that are specific to a filter. Don't test that a DataPath does not exist since that test is already performed as part of the SelectDataArrayAction.

Code Cleanup

  • No commented out code (rare exceptions to this is allowed..)
  • No API changes were made (or the changes have been approved)
  • No major design changes were made (or the changes have been approved)
  • Added test (or behavior not changed)
  • Updated API documentation (or API not changed)
  • Added license to new files (if any)
  • Added example pipelines that use the filter
  • Classes and methods are properly documented

@nyoungbq nyoungbq requested review from imikejackson and JDuffeyBQ and removed request for imikejackson December 13, 2024 22:01
@JDuffeyBQ
Copy link
Collaborator

I think this should allow for reuse of code between projection filters since they seem to be mostly identical. As for the the fixed output type, I don't think it should be an issue? I was able to define a helper structure to avoid having to define separate fixed output aliases.

Functions:

// uint8, int16, uint16, float32
using ITKProjectionSupportedOutputTypes = ArrayTypeOptions<false, false, true, true, true, false, false, false, false, true, false>;

template <class T>
struct FixedOutputTypeHelper
{
  template <class PixelT>
  using FilterOutputType = T;
};

template <class ArrayOptionsType>
struct RunITKProjectionDataCheckFunctor
{
  template <class FixedOutputType>
  auto operator()(const DataStructure& dataStructure, const DataPath& selectedInputArray, const DataPath& imageGeomPath, const DataPath& outputArrayPath) const
  {
    return ITK::DataCheck<ArrayOptionsType, typename FixedOutputTypeHelper<FixedOutputType>::FilterOutputType>(dataStructure, selectedInputArray, imageGeomPath, outputArrayPath);
  }
};

template <class ArrayOptionsType, class ITKFunctorType>
struct RunITKProjectionExecuteFunctor
{
  template <class FixedOutputType>
  auto operator()(DataStructure& dataStructure, const DataPath& selectedInputArray, const DataPath& imageGeomPath, const DataPath& outputArrayPath, ITKFunctorType&& itkFunctor,
                  const std::atomic_bool& shouldCancel) const
  {
    return ITK::Execute<ArrayOptionsType, typename FixedOutputTypeHelper<FixedOutputType>::FilterOutputType>(dataStructure, selectedInputArray, imageGeomPath, outputArrayPath, itkFunctor,
                                                                                                             shouldCancel);
  }
};

template <class ArrayTypeOptions, class FuncT, class FallbackFuncT, class... ArgsT>
auto RunTemplateFunctor(FuncT&& func, FallbackFuncT&& fallback, DataType dataType, ArgsT&&... args)
{
  if constexpr(ArrayTypeOptions::UsingBoolean)
  {
    if(dataType == DataType::boolean)
    {
      return func.template operator()<bool>(std::forward<ArgsT>(args)...);
    }
  }
  if constexpr(ArrayTypeOptions::UsingInt8)
  {
    if(dataType == DataType::int8)
    {
      return func.template operator()<int8>(std::forward<ArgsT>(args)...);
    }
  }
  if constexpr(ArrayTypeOptions::UsingInt16)
  {
    if(dataType == DataType::int16)
    {
      return func.template operator()<int16>(std::forward<ArgsT>(args)...);
    }
  }
  if constexpr(ArrayTypeOptions::UsingInt32)
  {
    if(dataType == DataType::int32)
    {
      return func.template operator()<int32>(std::forward<ArgsT>(args)...);
    }
  }
  if constexpr(ArrayTypeOptions::UsingInt64)
  {
    if(dataType == DataType::int64)
    {
      return func.template operator()<int64>(std::forward<ArgsT>(args)...);
    }
  }
  if constexpr(ArrayTypeOptions::UsingUInt8)
  {
    if(dataType == DataType::uint8)
    {
      return func.template operator()<uint8>(std::forward<ArgsT>(args)...);
    }
  }
  if constexpr(ArrayTypeOptions::UsingUInt16)
  {
    if(dataType == DataType::uint16)
    {
      return func.template operator()<uint16>(std::forward<ArgsT>(args)...);
    }
  }
  if constexpr(ArrayTypeOptions::UsingUInt32)
  {
    if(dataType == DataType::uint32)
    {
      return func.template operator()<uint32>(std::forward<ArgsT>(args)...);
    }
  }
  if constexpr(ArrayTypeOptions::UsingUInt64)
  {
    if(dataType == DataType::uint64)
    {
      return func.template operator()<uint64>(std::forward<ArgsT>(args)...);
    }
  }
  if constexpr(ArrayTypeOptions::UsingFloat32)
  {
    if(dataType == DataType::float32)
    {
      return func.template operator()<float32>(std::forward<ArgsT>(args)...);
    }
  }
  if constexpr(ArrayTypeOptions::UsingFloat64)
  {
    if(dataType == DataType::float64)
    {
      return func.template operator()<float64>(std::forward<ArgsT>(args)...);
    }
  }
  return fallback(dataType);
}

template <class ArrayOptionsType>
IFilter::PreflightResult RunITKProjectionDataCheck(const DataStructure& dataStructure, const DataPath& selectedInputArray, const DataPath& imageGeomPath, const std::string& outputGeomName,
                                                   bool performInPlace, const std::string& outputArrayName)
{
  DataPath outputArrayPath = selectedInputArray.replaceName(outputArrayName);
  Result<OutputActions> resultOutputActions;
  // The input geometry must be preserved, so we will just copy the needed array into newly created output geometry
  if(!performInPlace)
  {
    DataPath outputGeomPath({outputGeomName});

    const auto& originalGeometry = dataStructure.getDataRefAs<ImageGeom>(imageGeomPath);

    // Make copy of input geometry
    resultOutputActions.value().appendAction(std::make_unique<CreateImageGeometryAction>(
        outputGeomPath, originalGeometry.getDimensions().toContainer<CreateImageGeometryAction::DimensionType>(), originalGeometry.getOrigin().toContainer<CreateImageGeometryAction::OriginType>(),
        originalGeometry.getSpacing().toContainer<CreateImageGeometryAction::SpacingType>(), originalGeometry.getCellDataPath().getTargetName()));

    outputArrayPath = outputGeomPath.createChildPath(originalGeometry.getCellDataPath().getTargetName()).createChildPath(outputArrayName);
  }

  auto fallbackFunc = [](DataType dataType) {
    return MakeErrorResult<OutputActions>(-76590, fmt::format("Input {} type is not currently supported. Please reach out to devs if you have a use case.", DataTypeToString(dataType)));
  };

  DataType type = dataStructure.getDataRefAs<IDataArray>(selectedInputArray).getDataType();
  Result<OutputActions> helperOutputActions = RunTemplateFunctor<ITKProjectionSupportedOutputTypes>(RunITKProjectionDataCheckFunctor<ArrayOptionsType>{}, fallbackFunc, type, dataStructure,
                                                                                                    selectedInputArray, imageGeomPath, outputArrayPath);

  if(helperOutputActions.invalid())
  {
    return {std::move(helperOutputActions)};
  }

  // Consolidate actions
  resultOutputActions.value().actions.insert(resultOutputActions.value().actions.end(), helperOutputActions.value().actions.begin(), helperOutputActions.value().actions.end());

  return {std::move(resultOutputActions)};
}

template <class ArrayOptionsType, class ITKFunctorType>
Result<> RunITKProjectionExecute(DataStructure& dataStructure, const DataPath& selectedInputArray, const DataPath& imageGeomPath, const std::atomic_bool& shouldCancel,
                                 const std::string& outputArrayName, bool performInPlace, ITKFunctorType&& itkFunctor, const std::string& outputImageGeomName)
{
  DataPath outputArrayPath = selectedInputArray.replaceName(outputArrayName);

  DataPath finalImageGeomPath = imageGeomPath;

  if(!performInPlace)
  {
    const auto& originalGeometry = dataStructure.getDataRefAs<ImageGeom>(imageGeomPath);

    finalImageGeomPath = DataPath({outputImageGeomName});
    outputArrayPath = finalImageGeomPath.createChildPath(originalGeometry.getCellDataPath().getTargetName()).createChildPath(outputArrayName);
  }

  DataType type = dataStructure.getDataRefAs<IDataArray>(selectedInputArray).getDataType();

  auto fallbackFunc = [](DataType dataType) {
    return MakeErrorResult(-76591, fmt::format("Input {} type is not currently supported. Please reach out to devs if you have a use case.", DataTypeToString(dataType)));
  };

  Result<> result = RunTemplateFunctor<ITKProjectionSupportedOutputTypes>(RunITKProjectionExecuteFunctor<ArrayOptionsType, ITKFunctorType>{}, fallbackFunc, type, dataStructure, selectedInputArray,
                                                                          finalImageGeomPath, outputArrayPath, itkFunctor, shouldCancel);

  if(result.invalid())
  {
    return result;
  }

  auto& imageGeom = dataStructure.getDataRefAs<ImageGeom>(finalImageGeomPath);
  auto iArrayTupleShape = dataStructure.getDataAs<IArray>(outputArrayPath)->getTupleShape();

  // Update the Image Geometry with the new dimensions
  imageGeom.setDimensions({iArrayTupleShape[2], iArrayTupleShape[1], iArrayTupleShape[0]});

  // Update the AttributeMatrix with the new tuple shape. THIS WILL ALSO CHANGE ANY OTHER DATA ARRAY THAT IS ALSO
  // STORED IN THAT ATTRIBUTE MATRIX
  dataStructure.getDataAs<AttributeMatrix>(outputArrayPath.getParent())->resizeTuples(iArrayTupleShape);

  return {};
}

Usage:

//------------------------------------------------------------------------------
IFilter::PreflightResult ITKMedianProjectionImageFilter::preflightImpl(const DataStructure& dataStructure, const Arguments& filterArgs, const MessageHandler& messageHandler,
                                                                       const std::atomic_bool& shouldCancel) const
{
  auto imageGeomPath = filterArgs.value<DataPath>(k_InputImageGeomPath_Key);
  auto selectedInputArray = filterArgs.value<DataPath>(k_InputImageDataPath_Key);
  auto outputArrayName = filterArgs.value<DataObjectNameParameter::ValueType>(k_OutputImageArrayName_Key);
  auto projectionDimension = filterArgs.value<uint32>(k_ProjectionDimension_Key);
  auto performInPlace = filterArgs.value<bool>(k_RemoveOriginalGeometry_Key);
  auto outputGeomName = filterArgs.value<std::string>(k_OutputImageGeomName_Key);

  return RunITKProjectionDataCheck<cxITKMedianProjectionImageFilter::ArrayOptionsType>(dataStructure, selectedInputArray, imageGeomPath, outputGeomName, performInPlace, outputArrayName);
}

//------------------------------------------------------------------------------
Result<> ITKMedianProjectionImageFilter::executeImpl(DataStructure& dataStructure, const Arguments& filterArgs, const PipelineFilter* pipelineNode, const MessageHandler& messageHandler,
                                                     const std::atomic_bool& shouldCancel) const
{
  auto imageGeomPath = filterArgs.value<DataPath>(k_InputImageGeomPath_Key);
  auto selectedInputArray = filterArgs.value<DataPath>(k_InputImageDataPath_Key);
  auto outputArrayName = filterArgs.value<DataObjectNameParameter::ValueType>(k_OutputImageArrayName_Key);
  auto performInPlace = filterArgs.value<bool>(k_RemoveOriginalGeometry_Key);
  auto outputImageGeomName = filterArgs.value<std::string>(k_OutputImageGeomName_Key);
  auto projectionDimension = filterArgs.value<uint32>(k_ProjectionDimension_Key);

  const cxITKMedianProjectionImageFilter::ITKMedianProjectionImageFilterFunctor itkFunctor = {projectionDimension};

  return RunITKProjectionExecute<cxITKMedianProjectionImageFilter::ArrayOptionsType>(dataStructure, selectedInputArray, imageGeomPath, shouldCancel, outputArrayName, performInPlace, itkFunctor,
                                                                                     outputImageGeomName);
}

@imikejackson imikejackson force-pushed the filter/itk_projection branch from eb345e3 to 85c9d6e Compare January 15, 2025 13:54
imikejackson and others added 11 commits January 16, 2025 12:50
…angleGeometrySizes (BlueQuartzSoftware#1134)

* BUG: Fixes issue where any component shape were allowed for ComputeTriangleGeometrySizes.cpp
* Fix all instances of "Face Labels" are only allowed to use 2 component input arrays
* ComputeTriangleGeomVolumes - fix possible walking off the end of the array.
	A negative value for the face label when used as an index will cast into an unsigned value and bad things are going to happen.
* BUG: Fixes negative volumes produced from calculations

---------

Signed-off-by: Michael Jackson <mike.jackson@bluequartz.net>
…efault)

Done by creating a new resized geom with only projected data in it
@imikejackson imikejackson force-pushed the filter/itk_projection branch from 6cea7f5 to 31c1dee Compare January 16, 2025 17:50
Copy link
Collaborator

@JDuffeyBQ JDuffeyBQ left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are ITKBinaryProjectionFilter and ITKMeanProjectionFilter able to use the ProjectionUtils functions?

@nyoungbq
Copy link
Contributor Author

nyoungbq commented Jan 16, 2025

@JDuffeyBQ No, ITKMeans is a fixed output type of double which doesn't work with the other options, so it would need further templating to allow for its use with the util functions. Binary projection is much of the same in that it doesn't need a defined Filter output type so it works with much of the existing ITK interface outside of the new util file. They are similar in preflight and execute in handling the geometries themselves but the typing handling needed by the other projections are unique in these.

@nyoungbq nyoungbq merged commit c94454c into BlueQuartzSoftware:develop Jan 16, 2025
7 checks passed
@nyoungbq nyoungbq deleted the filter/itk_projection branch January 16, 2025 19:47
mmarineBlueQuartz pushed a commit to mmarineBlueQuartz/complex that referenced this pull request Feb 11, 2025
* Maximum, Median, Minimum, Mean Projection Filters
* Update documentation, test cases, extraneous code removal
* Make Filters conform to the Binary Projection precedent
* Added functionality to preserve integrity of input geometry (off by default)
Done by creating a new resized geom with only projected data in it
* Limit typing and strip RGB/ARGB support

---------

Signed-off-by: Michael Jackson <mike.jackson@bluequartz.net>
Co-authored-by: Michael Jackson <mike.jackson@bluequartz.net>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants