From de3bba104c19f1ac6b8044697bc57a5f62745a69 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Thu, 9 Apr 2026 23:08:13 -0400 Subject: [PATCH 01/16] FILT: ReadImageFilter, WriteImageFilter, ReadImageStackFilter These new filters use libTiff and Stb libraries to read and write images. The main utility classes IImageIO is placed into the simplnx library itself so any code can utilize it. --- cmake/Summary.cmake | 1 + .../Filters/ITKImageReaderFilter.cpp | 2 +- .../Filters/ITKImageWriterFilter.cpp | 2 +- src/Plugins/SimplnxCore/CMakeLists.txt | 33 +- .../SimplnxCore/docs/ReadImageFilter.md | 35 ++ .../SimplnxCore/docs/ReadImageStackFilter.md | 35 ++ .../SimplnxCore/docs/WriteImageFilter.md | 35 ++ .../Filters/Algorithms/ReadImage.cpp | 42 +++ .../Filters/Algorithms/ReadImage.hpp | 50 +++ .../Filters/Algorithms/ReadImageStack.cpp | 42 +++ .../Filters/Algorithms/ReadImageStack.hpp | 50 +++ .../Filters/Algorithms/WriteImage.cpp | 42 +++ .../Filters/Algorithms/WriteImage.hpp | 50 +++ .../SimplnxCore/Filters/ReadImageFilter.cpp | 248 ++++++++++++++ .../SimplnxCore/Filters/ReadImageFilter.hpp | 136 ++++++++ .../Filters/ReadImageStackFilter.cpp | 304 ++++++++++++++++++ .../Filters/ReadImageStackFilter.hpp | 135 ++++++++ .../SimplnxCore/Filters/WriteImageFilter.cpp | 176 ++++++++++ .../SimplnxCore/Filters/WriteImageFilter.hpp | 124 +++++++ src/Plugins/SimplnxCore/test/CMakeLists.txt | 7 +- .../SimplnxCore/test/ReadImageStackTest.cpp | 86 +++++ .../SimplnxCore/test/ReadImageTest.cpp | 86 +++++ .../SimplnxCore/test/WriteImageTest.cpp | 86 +++++ vcpkg-configuration.json | 8 +- vcpkg.json | 6 + 25 files changed, 1803 insertions(+), 18 deletions(-) create mode 100644 src/Plugins/SimplnxCore/docs/ReadImageFilter.md create mode 100644 src/Plugins/SimplnxCore/docs/ReadImageStackFilter.md create mode 100644 src/Plugins/SimplnxCore/docs/WriteImageFilter.md create mode 100644 src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ReadImage.cpp create mode 100644 src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ReadImage.hpp create mode 100644 src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ReadImageStack.cpp create mode 100644 src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ReadImageStack.hpp create mode 100644 src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/WriteImage.cpp create mode 100644 src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/WriteImage.hpp create mode 100644 src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ReadImageFilter.cpp create mode 100644 src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ReadImageFilter.hpp create mode 100644 src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ReadImageStackFilter.cpp create mode 100644 src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ReadImageStackFilter.hpp create mode 100644 src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteImageFilter.cpp create mode 100644 src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteImageFilter.hpp create mode 100644 src/Plugins/SimplnxCore/test/ReadImageStackTest.cpp create mode 100644 src/Plugins/SimplnxCore/test/ReadImageTest.cpp create mode 100644 src/Plugins/SimplnxCore/test/WriteImageTest.cpp diff --git a/cmake/Summary.cmake b/cmake/Summary.cmake index 47eb8ba4cc..1bae8198a4 100644 --- a/cmake/Summary.cmake +++ b/cmake/Summary.cmake @@ -58,6 +58,7 @@ message(STATUS "* span-lite (${span-lite_VERSION}) ${span-lite_DIR}") message(STATUS "* boost_mp11 (${boost_mp11_VERSION}) ${boost_mp11_DIR}") message(STATUS "* nod (${nod_VERSION}) ${nod_DIR}") message(STATUS "* reproc++ (${reproc_VERSION}) ${reproc++_DIR}") +message(STATUS "* stb (${stb_VERSION}) ${stb_DIR}") if(SIMPLNX_USE_LOCAL_EBSD_LIB) message(STATUS "* EbsdLib: Local repository being used") else() diff --git a/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageReaderFilter.cpp b/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageReaderFilter.cpp index 5cf1a4dd67..00fbcc908a 100644 --- a/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageReaderFilter.cpp +++ b/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageReaderFilter.cpp @@ -61,7 +61,7 @@ std::string ITKImageReaderFilter::humanName() const //------------------------------------------------------------------------------ std::vector ITKImageReaderFilter::defaultTags() const { - return {className(), "io", "input", "read", "import"}; + return {className(), "io", "input", "read", "import", "image", "jpg", "tiff", "bmp", "png"}; } //------------------------------------------------------------------------------ diff --git a/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp b/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp index d3ab7198de..9b6b1270cc 100644 --- a/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp +++ b/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp @@ -28,10 +28,10 @@ #include #include #include +#include #include "simplnx/Utilities/SIMPLConversion.hpp" -#include namespace fs = std::filesystem; diff --git a/src/Plugins/SimplnxCore/CMakeLists.txt b/src/Plugins/SimplnxCore/CMakeLists.txt index a19eff9ce3..49ac873858 100644 --- a/src/Plugins/SimplnxCore/CMakeLists.txt +++ b/src/Plugins/SimplnxCore/CMakeLists.txt @@ -112,6 +112,8 @@ set(FilterList ReadDeformKeyFileV12Filter ReadDREAM3DFilter ReadHDF5DatasetFilter + ReadImageFilter + ReadImageStackFilter ReadRawBinaryFilter ReadStlFileFilter ReadStringDataArrayFilter @@ -161,6 +163,7 @@ set(FilterList WriteStlFileFilter WriteVtkRectilinearGridFilter WriteVtkStructuredPointsFilter + WriteImageFilter ) set(ActionList @@ -274,6 +277,8 @@ set(AlgorithmList ReadDeformKeyFileV12 # ReadDREAM3D ReadHDF5Dataset + ReadImage + ReadImageStack ReadRawBinary ReadStlFile ReadStringDataArray @@ -324,6 +329,7 @@ set(AlgorithmList WriteStlFile WriteVtkRectilinearGrid WriteVtkStructuredPoints + WriteImage ) create_simplnx_plugin(NAME ${PLUGIN_NAME} @@ -357,11 +363,16 @@ file(TO_CMAKE_PATH "${reproc_dll_path}" reproc_dll_path) get_property(SIMPLNX_EXTRA_LIBRARY_DIRS GLOBAL PROPERTY SIMPLNX_EXTRA_LIBRARY_DIRS) set_property(GLOBAL PROPERTY SIMPLNX_EXTRA_LIBRARY_DIRS ${SIMPLNX_EXTRA_LIBRARY_DIRS} ${reproc_dll_path}) +#------------------------------------------------------------------------------ +# Find the cImg package to read/write images +#------------------------------------------------------------------------------ +find_package(Stb REQUIRED) +find_package(TIFF REQUIRED) #------------------------------------------------------------------------------ # If there are additional libraries that this plugin needs to link against you # can use the target_link_libraries() cmake call -target_link_libraries(${PLUGIN_NAME} PRIVATE reproc++) +target_link_libraries(${PLUGIN_NAME} PRIVATE reproc++ TIFF::TIFF) #------------------------------------------------------------------------------ # If there are additional source files that need to be compiled for this plugin @@ -568,18 +579,18 @@ install(FILES COMPONENT Applications ) -add_executable(txm_reader ${OLESS_SOURCES} ${${PLUGIN_NAME}_SOURCE_DIR}/src/${PLUGIN_NAME}/oless/txm_reader.cpp) -target_include_directories(txm_reader PRIVATE ${${PLUGIN_NAME}_SOURCE_DIR}/src/${PLUGIN_NAME}/oless) +# add_executable(txm_reader ${OLESS_SOURCES} ${${PLUGIN_NAME}_SOURCE_DIR}/src/${PLUGIN_NAME}/oless/txm_reader.cpp) +# target_include_directories(txm_reader PRIVATE ${${PLUGIN_NAME}_SOURCE_DIR}/src/${PLUGIN_NAME}/oless) -target_compile_features(txm_reader - PUBLIC - cxx_std_17 -) +# target_compile_features(txm_reader +# PUBLIC +# cxx_std_17 +# ) -set_target_properties(txm_reader - PROPERTIES - DEBUG_POSTFIX "_d" -) +# set_target_properties(txm_reader +# PROPERTIES +# DEBUG_POSTFIX "_d" +# ) # target_include_directories(txm_reader # PUBLIC # ${${PLUGIN_NAME}_SOURCE_DIR}/src/${PLUGIN_NAME}/oless diff --git a/src/Plugins/SimplnxCore/docs/ReadImageFilter.md b/src/Plugins/SimplnxCore/docs/ReadImageFilter.md new file mode 100644 index 0000000000..d347681217 --- /dev/null +++ b/src/Plugins/SimplnxCore/docs/ReadImageFilter.md @@ -0,0 +1,35 @@ +# INSERT_HUMAN_NAME + +## Group (Subgroup) + +What group (and possibly subgroup) does the filter belong to + +## Description + +This **Filter** ..... + +Images can be used with this: + +![](Images/ReadImage_1.png) + +## Warning + +## Notes + +## Caveats + +% Auto generated parameter table will be inserted here + +## Reference + + +## Example Pipelines + + +## License & Copyright + +Please see the description file distributed with this plugin. + +## DREAM3D Mailing Lists + +If you need help, need to file a bug report or want to request a new feature, please head over to the [DREAM3DNX-Issues](https://github.com/BlueQuartzSoftware/DREAM3DNX-Issues/discussions) GitHub site where the community of DREAM3D-NX users can help answer your questions. diff --git a/src/Plugins/SimplnxCore/docs/ReadImageStackFilter.md b/src/Plugins/SimplnxCore/docs/ReadImageStackFilter.md new file mode 100644 index 0000000000..189227e036 --- /dev/null +++ b/src/Plugins/SimplnxCore/docs/ReadImageStackFilter.md @@ -0,0 +1,35 @@ +# INSERT_HUMAN_NAME + +## Group (Subgroup) + +What group (and possibly subgroup) does the filter belong to + +## Description + +This **Filter** ..... + +Images can be used with this: + +![](Images/ReadImageStack_1.png) + +## Warning + +## Notes + +## Caveats + +% Auto generated parameter table will be inserted here + +## Reference + + +## Example Pipelines + + +## License & Copyright + +Please see the description file distributed with this plugin. + +## DREAM3D Mailing Lists + +If you need help, need to file a bug report or want to request a new feature, please head over to the [DREAM3DNX-Issues](https://github.com/BlueQuartzSoftware/DREAM3DNX-Issues/discussions) GitHub site where the community of DREAM3D-NX users can help answer your questions. diff --git a/src/Plugins/SimplnxCore/docs/WriteImageFilter.md b/src/Plugins/SimplnxCore/docs/WriteImageFilter.md new file mode 100644 index 0000000000..79a6b18424 --- /dev/null +++ b/src/Plugins/SimplnxCore/docs/WriteImageFilter.md @@ -0,0 +1,35 @@ +# INSERT_HUMAN_NAME + +## Group (Subgroup) + +What group (and possibly subgroup) does the filter belong to + +## Description + +This **Filter** ..... + +Images can be used with this: + +![](Images/WriteImage_1.png) + +## Warning + +## Notes + +## Caveats + +% Auto generated parameter table will be inserted here + +## Reference + + +## Example Pipelines + + +## License & Copyright + +Please see the description file distributed with this plugin. + +## DREAM3D Mailing Lists + +If you need help, need to file a bug report or want to request a new feature, please head over to the [DREAM3DNX-Issues](https://github.com/BlueQuartzSoftware/DREAM3DNX-Issues/discussions) GitHub site where the community of DREAM3D-NX users can help answer your questions. diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ReadImage.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ReadImage.cpp new file mode 100644 index 0000000000..22532f0dd4 --- /dev/null +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ReadImage.cpp @@ -0,0 +1,42 @@ +#include "ReadImage.hpp" + +#include "simplnx/DataStructure/DataArray.hpp" +#include "simplnx/DataStructure/DataGroup.hpp" + +using namespace nx::core; + +// ----------------------------------------------------------------------------- +ReadImage::ReadImage(DataStructure& dataStructure, const IFilter::MessageHandler& mesgHandler, const std::atomic_bool& shouldCancel, ReadImageInputValues* inputValues) +: m_DataStructure(dataStructure) +, m_InputValues(inputValues) +, m_ShouldCancel(shouldCancel) +, m_MessageHandler(mesgHandler) +{ +} + +// ----------------------------------------------------------------------------- +ReadImage::~ReadImage() noexcept = default; + +// ----------------------------------------------------------------------------- +Result<> ReadImage::operator()() +{ + /** + * This section of the code should contain the actual algorithmic codes that + * will accomplish the goal of the file. + * + * If you can parallelize the code there are a number of examples on how to do that. + * GenerateIPFColors is one example + * + * If you need to determine what kind of array you have (Int32Array, Float32Array, etc) + * look to the ExecuteDataFunction() in simplnx/Utilities/FilterUtilities.hpp template + * function to help with that code. + * An Example algorithm class is `CombineAttributeArrays` and `RemoveFlaggedVertices` + * + * There are other utility classes that can help alleviate the amount of code that needs + * to be written. + * + * REMOVE THIS COMMENT BLOCK WHEN YOU ARE FINISHED WITH THE FILTER_HUMAN_NAME + */ + + return {}; +} diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ReadImage.hpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ReadImage.hpp new file mode 100644 index 0000000000..bad730e37d --- /dev/null +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ReadImage.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include "SimplnxCore/SimplnxCore_export.hpp" + +#include "simplnx/DataStructure/DataPath.hpp" +#include "simplnx/DataStructure/DataStructure.hpp" +#include "simplnx/Filter/IFilter.hpp" + +//TODO: PARAMETER_INCLUDES + +/** +* This is example code to put in the Execute Method of the filter. +@EXECUTE_EXAMPLE_CODE@ +*/ + +namespace nx::core +{ + +struct SIMPLNXCORE_EXPORT ReadImageInputValues +{ +//TODO: INPUT_VALUE_STRUCT_DEF + +}; + +/** + * @class ReadImage + * @brief This algorithm implements support code for the ReadImageFilter + */ + +class SIMPLNXCORE_EXPORT ReadImage +{ +public: + ReadImage(DataStructure& dataStructure, const IFilter::MessageHandler& mesgHandler, const std::atomic_bool& shouldCancel, ReadImageInputValues* inputValues); + ~ReadImage() noexcept; + + ReadImage(const ReadImage&) = delete; + ReadImage(ReadImage&&) noexcept = delete; + ReadImage& operator=(const ReadImage&) = delete; + ReadImage& operator=(ReadImage&&) noexcept = delete; + + Result<> operator()(); + +private: + DataStructure& m_DataStructure; + const ReadImageInputValues* m_InputValues = nullptr; + const std::atomic_bool& m_ShouldCancel; + const IFilter::MessageHandler& m_MessageHandler; +}; + +} // namespace complex diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ReadImageStack.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ReadImageStack.cpp new file mode 100644 index 0000000000..2d6b8d309e --- /dev/null +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ReadImageStack.cpp @@ -0,0 +1,42 @@ +#include "ReadImageStack.hpp" + +#include "simplnx/DataStructure/DataArray.hpp" +#include "simplnx/DataStructure/DataGroup.hpp" + +using namespace nx::core; + +// ----------------------------------------------------------------------------- +ReadImageStack::ReadImageStack(DataStructure& dataStructure, const IFilter::MessageHandler& mesgHandler, const std::atomic_bool& shouldCancel, ReadImageStackInputValues* inputValues) +: m_DataStructure(dataStructure) +, m_InputValues(inputValues) +, m_ShouldCancel(shouldCancel) +, m_MessageHandler(mesgHandler) +{ +} + +// ----------------------------------------------------------------------------- +ReadImageStack::~ReadImageStack() noexcept = default; + +// ----------------------------------------------------------------------------- +Result<> ReadImageStack::operator()() +{ + /** + * This section of the code should contain the actual algorithmic codes that + * will accomplish the goal of the file. + * + * If you can parallelize the code there are a number of examples on how to do that. + * GenerateIPFColors is one example + * + * If you need to determine what kind of array you have (Int32Array, Float32Array, etc) + * look to the ExecuteDataFunction() in simplnx/Utilities/FilterUtilities.hpp template + * function to help with that code. + * An Example algorithm class is `CombineAttributeArrays` and `RemoveFlaggedVertices` + * + * There are other utility classes that can help alleviate the amount of code that needs + * to be written. + * + * REMOVE THIS COMMENT BLOCK WHEN YOU ARE FINISHED WITH THE FILTER_HUMAN_NAME + */ + + return {}; +} diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ReadImageStack.hpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ReadImageStack.hpp new file mode 100644 index 0000000000..ba88bc8ab5 --- /dev/null +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ReadImageStack.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include "SimplnxCore/SimplnxCore_export.hpp" + +#include "simplnx/DataStructure/DataPath.hpp" +#include "simplnx/DataStructure/DataStructure.hpp" +#include "simplnx/Filter/IFilter.hpp" + +//TODO: PARAMETER_INCLUDES + +/** +* This is example code to put in the Execute Method of the filter. +@EXECUTE_EXAMPLE_CODE@ +*/ + +namespace nx::core +{ + +struct SIMPLNXCORE_EXPORT ReadImageStackInputValues +{ +//TODO: INPUT_VALUE_STRUCT_DEF + +}; + +/** + * @class ReadImageStack + * @brief This algorithm implements support code for the ReadImageStackFilter + */ + +class SIMPLNXCORE_EXPORT ReadImageStack +{ +public: + ReadImageStack(DataStructure& dataStructure, const IFilter::MessageHandler& mesgHandler, const std::atomic_bool& shouldCancel, ReadImageStackInputValues* inputValues); + ~ReadImageStack() noexcept; + + ReadImageStack(const ReadImageStack&) = delete; + ReadImageStack(ReadImageStack&&) noexcept = delete; + ReadImageStack& operator=(const ReadImageStack&) = delete; + ReadImageStack& operator=(ReadImageStack&&) noexcept = delete; + + Result<> operator()(); + +private: + DataStructure& m_DataStructure; + const ReadImageStackInputValues* m_InputValues = nullptr; + const std::atomic_bool& m_ShouldCancel; + const IFilter::MessageHandler& m_MessageHandler; +}; + +} // namespace complex diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/WriteImage.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/WriteImage.cpp new file mode 100644 index 0000000000..5576d52c4e --- /dev/null +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/WriteImage.cpp @@ -0,0 +1,42 @@ +#include "WriteImage.hpp" + +#include "simplnx/DataStructure/DataArray.hpp" +#include "simplnx/DataStructure/DataGroup.hpp" + +using namespace nx::core; + +// ----------------------------------------------------------------------------- +WriteImage::WriteImage(DataStructure& dataStructure, const IFilter::MessageHandler& mesgHandler, const std::atomic_bool& shouldCancel, WriteImageInputValues* inputValues) +: m_DataStructure(dataStructure) +, m_InputValues(inputValues) +, m_ShouldCancel(shouldCancel) +, m_MessageHandler(mesgHandler) +{ +} + +// ----------------------------------------------------------------------------- +WriteImage::~WriteImage() noexcept = default; + +// ----------------------------------------------------------------------------- +Result<> WriteImage::operator()() +{ + /** + * This section of the code should contain the actual algorithmic codes that + * will accomplish the goal of the file. + * + * If you can parallelize the code there are a number of examples on how to do that. + * GenerateIPFColors is one example + * + * If you need to determine what kind of array you have (Int32Array, Float32Array, etc) + * look to the ExecuteDataFunction() in simplnx/Utilities/FilterUtilities.hpp template + * function to help with that code. + * An Example algorithm class is `CombineAttributeArrays` and `RemoveFlaggedVertices` + * + * There are other utility classes that can help alleviate the amount of code that needs + * to be written. + * + * REMOVE THIS COMMENT BLOCK WHEN YOU ARE FINISHED WITH THE FILTER_HUMAN_NAME + */ + + return {}; +} diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/WriteImage.hpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/WriteImage.hpp new file mode 100644 index 0000000000..e5cb1d087b --- /dev/null +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/WriteImage.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include "SimplnxCore/SimplnxCore_export.hpp" + +#include "simplnx/DataStructure/DataPath.hpp" +#include "simplnx/DataStructure/DataStructure.hpp" +#include "simplnx/Filter/IFilter.hpp" + +//TODO: PARAMETER_INCLUDES + +/** +* This is example code to put in the Execute Method of the filter. +@EXECUTE_EXAMPLE_CODE@ +*/ + +namespace nx::core +{ + +struct SIMPLNXCORE_EXPORT WriteImageInputValues +{ +//TODO: INPUT_VALUE_STRUCT_DEF + +}; + +/** + * @class WriteImage + * @brief This algorithm implements support code for the WriteImageFilter + */ + +class SIMPLNXCORE_EXPORT WriteImage +{ +public: + WriteImage(DataStructure& dataStructure, const IFilter::MessageHandler& mesgHandler, const std::atomic_bool& shouldCancel, WriteImageInputValues* inputValues); + ~WriteImage() noexcept; + + WriteImage(const WriteImage&) = delete; + WriteImage(WriteImage&&) noexcept = delete; + WriteImage& operator=(const WriteImage&) = delete; + WriteImage& operator=(WriteImage&&) noexcept = delete; + + Result<> operator()(); + +private: + DataStructure& m_DataStructure; + const WriteImageInputValues* m_InputValues = nullptr; + const std::atomic_bool& m_ShouldCancel; + const IFilter::MessageHandler& m_MessageHandler; +}; + +} // namespace complex diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ReadImageFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ReadImageFilter.cpp new file mode 100644 index 0000000000..7ab68dcd22 --- /dev/null +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ReadImageFilter.cpp @@ -0,0 +1,248 @@ +#include "ReadImageFilter.hpp" + +#include "SimplnxCore/Filters/Algorithms/ReadImage.hpp" + +#include "simplnx/Core/Application.hpp" +#include "simplnx/DataStructure/DataPath.hpp" +#include "simplnx/DataStructure/DataStore.hpp" +#include "simplnx/DataStructure/Geometry/ImageGeom.hpp" +#include "simplnx/Filter/Actions/CreateArrayAction.hpp" +#include "simplnx/Filter/Actions/UpdateImageGeomAction.hpp" +#include "simplnx/Filter/FilterHandle.hpp" +#include "simplnx/Parameters/BoolParameter.hpp" +#include "simplnx/Parameters/CropGeometryParameter.hpp" +#include "simplnx/Parameters/DataGroupCreationParameter.hpp" +#include "simplnx/Parameters/DataObjectNameParameter.hpp" +#include "simplnx/Parameters/FileSystemPathParameter.hpp" +#include "simplnx/Parameters/GeometrySelectionParameter.hpp" +#include "simplnx/Parameters/StringParameter.hpp" +#include "simplnx/Parameters/VectorParameter.hpp" +#include "simplnx/Utilities/SIMPLConversion.hpp" + + +#define STB_IMAGE_IMPLEMENTATION +#include + +#include +#include + +namespace fs = std::filesystem; + +using namespace nx::core; + +namespace +{ +const Uuid k_SimplnxCorePluginId = *Uuid::FromString("05cc618b-781f-4ac0-b9ac-43f26ce1854f"); +const Uuid k_CropImageGeomFilterId = *Uuid::FromString("e6476737-4aa7-48ba-a702-3dfab82c96e2"); +const FilterHandle k_CropImageGeomFilterHandle(k_CropImageGeomFilterId, k_SimplnxCorePluginId); + + +} // namespace + + + +namespace nx::core +{ +//------------------------------------------------------------------------------ +std::string ReadImageFilter::name() const +{ + return FilterTraits::name.str(); +} + +//------------------------------------------------------------------------------ +std::string ReadImageFilter::className() const +{ + return FilterTraits::className; +} + +//------------------------------------------------------------------------------ +Uuid ReadImageFilter::uuid() const +{ + return FilterTraits::uuid; +} + +//------------------------------------------------------------------------------ +std::string ReadImageFilter::humanName() const +{ + return "Read Image"; +} + +//------------------------------------------------------------------------------ +std::vector ReadImageFilter::defaultTags() const +{ + return {className(), "io", "input", "read", "import", "image", "jpg", "tiff", "bmp", "png"}; +} + +//------------------------------------------------------------------------------ +Parameters ReadImageFilter::parameters() const +{ + Parameters params; + + params.insertSeparator(Parameters::Separator{"Input Parameter(s)"}); + + params.insert(std::make_unique(k_FileName_Key, "File", "Input image file", fs::path(""), + FileSystemPathParameter::ExtensionsType{{".png"}, {".tiff"}, {".tif"}, {".bmp"}, {".jpeg"}, {".jpg"}, {".nrrd"}, {".mha"}}, + FileSystemPathParameter::PathType::InputFile, false)); + + params.insert(std::make_unique(k_LengthUnit_Key, "Length Unit", "The length unit that will be set into the created image geometry", + to_underlying(IGeometry::LengthUnit::Micrometer), IGeometry::GetAllLengthUnitStrings())); + + params.insertLinkableParameter(std::make_unique(k_ChangeDataType_Key, "Set Image Data Type", "Set the final created image data type.", false)); + params.insert(std::make_unique(k_ImageDataType_Key, "Output Data Type", "Numeric Type of data to create", 0ULL, + ChoicesParameter::Choices{"uint8", "uint16", "uint32"})); // Sequence Dependent DO NOT REORDER + + params.insertSeparator(Parameters::Separator{"Origin & Spacing Options"}); + params.insertLinkableParameter(std::make_unique(k_ChangeOrigin_Key, "Set Origin", "Specifies if the origin should be changed", false)); + params.insert( + std::make_unique(k_CenterOrigin_Key, "Put Input Origin at the Center of Geometry", "Specifies if the origin should be aligned with the corner (false) or center (true)", false)); + params.insert(std::make_unique(k_Origin_Key, "Origin (Physical Units)", "Specifies the new origin values in physical units.", std::vector{0.0, 0.0, 0.0}, + std::vector{"X", "Y", "Z"})); + + params.insertLinkableParameter(std::make_unique(k_ChangeSpacing_Key, "Set Spacing", "Specifies if the spacing should be changed", false)); + params.insert(std::make_unique(k_Spacing_Key, "Spacing (Physical Units)", "Specifies the new spacing values in physical units.", std::vector{1, 1, 1}, + std::vector{"X", "Y", "Z"})); + params.insert(std::make_unique(k_OriginSpacingProcessing_Key, "Origin & Spacing Processing", "Whether the origin & spacing should be preprocessed or postprocessed.", 1, + ChoicesParameter::Choices{"Preprocessed", "Postprocessed"})); + + params.linkParameters(k_ChangeDataType_Key, k_ImageDataType_Key, true); + + params.linkParameters(k_ChangeOrigin_Key, k_Origin_Key, std::make_any(true)); + params.linkParameters(k_ChangeOrigin_Key, k_CenterOrigin_Key, std::make_any(true)); + params.linkParameters(k_ChangeSpacing_Key, k_Spacing_Key, std::make_any(true)); + params.linkParameters(k_ChangeOrigin_Key, k_OriginSpacingProcessing_Key, true); + params.linkParameters(k_ChangeSpacing_Key, k_OriginSpacingProcessing_Key, true); + + params.insertSeparator(Parameters::Separator{"Cropping Options"}); + auto croppingOptions = CropGeometryParameter::ValueType{}; + croppingOptions.is2D = true; + params.insert(std::make_unique( + k_CroppingOptions_Key, "Cropping Options", + "The cropping options used to crop images. These include picking the cropping type, the cropping dimensions, and the cropping ranges for each chosen dimension.", croppingOptions)); + + params.insertSeparator(Parameters::Separator{"Output Data Object(s)"}); + params.insert(std::make_unique(k_ImageGeometryPath_Key, "Created Image Geometry", "The path to the created Image Geometry", DataPath({"ImageDataContainer"}))); + params.insert(std::make_unique(k_CellDataName_Key, "Created Cell Attribute Matrix", "The name of the created cell attribute matrix", ImageGeom::k_CellAttributeMatrixName)); + params.insert(std::make_unique(k_ImageDataArrayPath_Key, "Created Cell Data", + "The name of the created image data array. Will be stored in the created Cell Attribute Matrix", "ImageData")); + + return params; +} + +//------------------------------------------------------------------------------ +IFilter::VersionType ReadImageFilter::parametersVersion() const +{ + return 1; +} + +//------------------------------------------------------------------------------ +IFilter::UniquePointer ReadImageFilter::clone() const +{ + return std::make_unique(); +} + +//------------------------------------------------------------------------------ +IFilter::PreflightResult ReadImageFilter::preflightImpl(const DataStructure& dataStructure, const Arguments& filterArgs, const MessageHandler& messageHandler, + const std::atomic_bool& shouldCancel, const ExecutionContext& executionContext) const +{ + auto fileName = filterArgs.value(k_FileName_Key); + auto imageGeomPath = filterArgs.value(k_ImageGeometryPath_Key); + auto cellDataName = filterArgs.value(k_CellDataName_Key); + auto imageDataArrayName = filterArgs.value(k_ImageDataArrayPath_Key); + auto shouldChangeOrigin = filterArgs.value(k_ChangeOrigin_Key); + auto shouldCenterOrigin = filterArgs.value(k_CenterOrigin_Key); + auto shouldChangeSpacing = filterArgs.value(k_ChangeSpacing_Key); + auto origin = filterArgs.value(k_Origin_Key); + auto spacing = filterArgs.value(k_Spacing_Key); + // auto originSpacingProcessing = static_cast(filterArgs.value(k_OriginSpacingProcessing_Key)); + auto pChangeDataType = filterArgs.value(k_ChangeDataType_Key); + auto pChoiceType = filterArgs.value(k_ImageDataType_Key); + auto croppingOptions = filterArgs.value(k_CroppingOptions_Key); + + std::string fileNameString = fileName.string(); + + int col = 0, row = 0, comp = 0; + int result = stbi_info(fileNameString.c_str(), &col, &row, &comp); + + + // Declare the preflightResult variable that will be populated with the results + // of the preflight. The PreflightResult type contains the output Actions and + // any preflight updated values that you want to be displayed to the user, typically + // through a user interface (UI). + PreflightResult preflightResult; + + // If your filter is making structural changes to the DataStructure then the filter + // is going to create OutputActions subclasses that need to be returned. This will + // store those actions. + nx::core::Result resultOutputActions; + + // If your filter is going to pass back some `preflight updated values` then this is where you + // would create the code to store those values in the appropriate object. Note that we + // in line creating the pair (NOT a std::pair<>) of Key:Value that will get stored in + // the std::vector object. + std::vector preflightUpdatedValues; + + // If the filter needs to pass back some updated values via a key:value string:string set of values + // you can declare and update that string here. + + //TODO: PREFLIGHT_UPDATED_DEFS + // If this filter makes changes to the DataStructure in the form of + // creating/deleting/moving/renaming DataGroups, Geometries, DataArrays then you + // will need to use one of the `*Actions` classes located in complex/Filter/Actions + // to relay that information to the preflight and execute methods. This is done by + // creating an instance of the Action class and then storing it in the resultOutputActions variable. + // This is done through a `push_back()` method combined with a `std::move()`. For the + // newly initiated to `std::move` once that code is executed what was once inside the Action class + // instance variable is *no longer there*. The memory has been moved. If you try to access that + // variable after this line you will probably get a crash or have subtle bugs. To ensure that this + // does not happen we suggest using braces `{}` to scope each of the action's declaration and store + // so that the programmer is not tempted to use the action instance past where it should be used. + // You have to create your own Actions class if there isn't something specific for your filter's needs + + //TODO: PROPOSED_ACTIONS + // Store the preflight updated value(s) into the preflightUpdatedValues vector using + // the appropriate methods. + + //TODO: PREFLIGHT_UPDATED_VALUES + // Return both the resultOutputActions and the preflightUpdatedValues via std::move() + return {std::move(resultOutputActions), std::move(preflightUpdatedValues)}; +} + +//------------------------------------------------------------------------------ +Result<> ReadImageFilter::executeImpl(DataStructure& dataStructure, const Arguments& filterArgs, const PipelineFilter* pipelineNode, const MessageHandler& messageHandler, + const std::atomic_bool& shouldCancel, const ExecutionContext& executionContext) const +{ + + ReadImageInputValues inputValues; + + + //TODO: INPUT_VALUES_DEF + + return ReadImage(dataStructure, messageHandler, shouldCancel, &inputValues)(); +} + +namespace +{ +namespace SIMPL +{ + + //TODO: PARAMETER_JSON_CONSTANTS +} // namespace SIMPL +} // namespace + + +//------------------------------------------------------------------------------ +Result ReadImageFilter::FromSIMPLJson(const nlohmann::json& json) +{ + Arguments args = ReadImageFilter().getDefaultArguments(); + + std::vector> results; + + /* This is a NEW filter and not ported so this section does not matter */ + + Result<> conversionResult = MergeResults(std::move(results)); + + return ConvertResultTo(std::move(conversionResult), std::move(args)); +} + + +} // namespace nx::core diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ReadImageFilter.hpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ReadImageFilter.hpp new file mode 100644 index 0000000000..1cd6050d68 --- /dev/null +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ReadImageFilter.hpp @@ -0,0 +1,136 @@ +#pragma once + +#include "SimplnxCore/SimplnxCore_export.hpp" + +#include "simplnx/Filter/FilterTraits.hpp" +#include "simplnx/Filter/IFilter.hpp" + +namespace nx::core +{ +/** + * @class ReadImageFilter + * @brief This filter will .... + */ +class SIMPLNXCORE_EXPORT ReadImageFilter : public IFilter +{ +public: + ReadImageFilter() = default; + ~ReadImageFilter() noexcept override = default; + + ReadImageFilter(const ReadImageFilter&) = delete; + ReadImageFilter(ReadImageFilter&&) noexcept = delete; + + ReadImageFilter& operator=(const ReadImageFilter&) = delete; + ReadImageFilter& operator=(ReadImageFilter&&) noexcept = delete; + + // Parameter Keys + static constexpr StringLiteral k_FileName_Key = "file_name"; + static constexpr StringLiteral k_ImageGeometryPath_Key = "output_geometry_path"; + static constexpr StringLiteral k_ImageDataArrayPath_Key = "image_data_array_name"; + static constexpr StringLiteral k_CellDataName_Key = "cell_attribute_matrix_name"; + + static constexpr StringLiteral k_LengthUnit_Key = "length_unit_index"; + + static constexpr StringLiteral k_ChangeOrigin_Key = "change_origin"; + static constexpr StringLiteral k_CenterOrigin_Key = "center_origin"; + static constexpr StringLiteral k_Origin_Key = "origin"; + + static constexpr StringLiteral k_ChangeSpacing_Key = "change_spacing"; + static constexpr StringLiteral k_Spacing_Key = "spacing"; + static constexpr StringLiteral k_OriginSpacingProcessing_Key = "origin_spacing_processing_index"; + + static constexpr StringLiteral k_ChangeDataType_Key = "change_image_data_type"; + static constexpr StringLiteral k_ImageDataType_Key = "image_data_type_index"; + + static constexpr StringLiteral k_CroppingOptions_Key = "cropping_options"; + + /** + * @brief Reads SIMPL json and converts it simplnx Arguments. + * @param json + * @return Result + */ + static Result FromSIMPLJson(const nlohmann::json& json); + + /** + * @brief Returns the name of the filter. + * @return + */ + std::string name() const override; + + /** + * @brief Returns the C++ classname of this filter. + * @return + */ + std::string className() const override; + + /** + * @brief Returns the uuid of the filter. + * @return + */ + Uuid uuid() const override; + + /** + * @brief Returns the human readable name of the filter. + * @return + */ + std::string humanName() const override; + + /** + * @brief Returns the default tags for this filter. + * @return + */ + std::vector defaultTags() const override; + + /** + * @brief Returns the parameters of the filter (i.e. its inputs) + * @return + */ + Parameters parameters() const override; + + /** + * @brief Returns parameters version integer. + * Initial version should always be 1. + * Should be incremented everytime the parameters change. + * @return VersionType + */ + VersionType parametersVersion() const override; + + /** + * @brief Returns a copy of the filter. + * @return + */ + UniquePointer clone() const override; + +protected: + /** + * @brief Takes in a DataStructure and checks that the filter can be run on it with the given arguments. + * Returns any warnings/errors. Also returns the changes that would be applied to the DataStructure. + * Some parts of the actions may not be completely filled out if all the required information is not available at preflight time. + * @param dataStructure The input DataStructure instance + * @param filterArgs These are the input values for each parameter that is required for the filter + * @param messageHandler The MessageHandler object + * @param shouldCancel Atomic boolean value that can be checked to cancel the filter + * @param executionContext The ExecutionContext that can be used to determine the correct absolute path from a relative path + * @return Returns a Result object with error or warning values if any of those occurred during execution of this function + */ + PreflightResult preflightImpl(const DataStructure& dataStructure, const Arguments& filterArgs, const MessageHandler& messageHandler, const std::atomic_bool& shouldCancel, + const ExecutionContext& executionContext) const override; + + /** + * @brief Applies the filter's algorithm to the DataStructure with the given arguments. Returns any warnings/errors. + * On failure, there is no guarantee that the DataStructure is in a correct state. + * @param dataStructure The input DataStructure instance + * @param filterArgs These are the input values for each parameter that is required for the filter + * @param pipelineNode The node in the pipeline that is being executed + * @param messageHandler The MessageHandler object + * @param shouldCancel Atomic boolean value that can be checked to cancel the filter + * @param executionContext The ExecutionContext that can be used to determine the correct absolute path from a relative path + * @return Returns a Result object with error or warning values if any of those occurred during execution of this function + */ + Result<> executeImpl(DataStructure& dataStructure, const Arguments& filterArgs, const PipelineFilter* pipelineNode, const MessageHandler& messageHandler, const std::atomic_bool& shouldCancel, + const ExecutionContext& executionContext) const override; +}; +} // namespace complex + +SIMPLNX_DEF_FILTER_TRAITS(nx::core, ReadImageFilter, "7391288d-3d0b-492e-93c9-e128e05c9737"); +/* LEGACY UUID FOR THIS FILTER @OLD_UUID@ */ diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ReadImageStackFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ReadImageStackFilter.cpp new file mode 100644 index 0000000000..e6e0d71a5c --- /dev/null +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ReadImageStackFilter.cpp @@ -0,0 +1,304 @@ +#include "ReadImageStackFilter.hpp" + +#include "SimplnxCore/Filters/Algorithms/ReadImageStack.hpp" + +#include "simplnx/Common/TypesUtility.hpp" +#include "simplnx/Core/Application.hpp" +#include "simplnx/DataStructure/DataPath.hpp" +#include "simplnx/DataStructure/Geometry/ImageGeom.hpp" +#include "simplnx/Filter/Actions/CreateImageGeometryAction.hpp" +#include "simplnx/Filter/Actions/DeleteDataAction.hpp" +#include "simplnx/Filter/Actions/RenameDataAction.hpp" +#include "simplnx/Filter/Actions/UpdateImageGeomAction.hpp" +#include "simplnx/Parameters/BoolParameter.hpp" +#include "simplnx/Parameters/ChoicesParameter.hpp" +#include "simplnx/Parameters/CropGeometryParameter.hpp" +#include "simplnx/Parameters/DataGroupCreationParameter.hpp" +#include "simplnx/Parameters/DataObjectNameParameter.hpp" +#include "simplnx/Parameters/GeneratedFileListParameter.hpp" +#include "simplnx/Parameters/NumberParameter.hpp" +#include "simplnx/Parameters/VectorParameter.hpp" +#include "simplnx/Utilities/FilterUtilities.hpp" +#include "simplnx/Utilities/GeometryHelpers.hpp" + +#include + +namespace fs = std::filesystem; + +using namespace nx::core; + +namespace +{ +const ChoicesParameter::Choices k_SliceOperationChoices = {"None", "Flip about X axis", "Flip about Y axis"}; +const ChoicesParameter::ValueType k_NoImageTransform = 0; +const ChoicesParameter::ValueType k_FlipAboutXAxis = 1; +const ChoicesParameter::ValueType k_FlipAboutYAxis = 2; + +constexpr nx::core::StringLiteral k_NoResamplingMode = "Do Not Resample (0)"; +constexpr nx::core::StringLiteral k_ScalingMode = "Scaling (1)"; +constexpr nx::core::StringLiteral k_ExactDimensions = "Exact X/Y Dimensions (2)"; +const nx::core::ChoicesParameter::Choices k_ResamplingChoices = {k_NoResamplingMode, k_ScalingMode, k_ExactDimensions}; +const nx::core::ChoicesParameter::ValueType k_NoResampleModeIndex = 0; +const nx::core::ChoicesParameter::ValueType k_ScalingModeIndex = 1; +const nx::core::ChoicesParameter::ValueType k_ExactDimensionsModeIndex = 2; + +const Uuid k_SimplnxCorePluginId = *Uuid::FromString("05cc618b-781f-4ac0-b9ac-43f26ce1854f"); +const Uuid k_RotateSampleRefFrameFilterId = *Uuid::FromString("d2451dc1-a5a1-4ac2-a64d-7991669dcffc"); +const FilterHandle k_RotateSampleRefFrameFilterHandle(k_RotateSampleRefFrameFilterId, k_SimplnxCorePluginId); +const Uuid k_ColorToGrayScaleFilterId = *Uuid::FromString("d938a2aa-fee2-4db9-aa2f-2c34a9736580"); +const FilterHandle k_ColorToGrayScaleFilterHandle(k_ColorToGrayScaleFilterId, k_SimplnxCorePluginId); +const Uuid k_ResampleImageGeomFilterId = *Uuid::FromString("9783ea2c-4cf7-46de-ab21-b40d91a48c5b"); +const FilterHandle k_ResampleImageGeomFilterHandle(k_ResampleImageGeomFilterId, k_SimplnxCorePluginId); +const Uuid k_CropImageGeomFilterId = *Uuid::FromString("e6476737-4aa7-48ba-a702-3dfab82c96e2"); +const FilterHandle k_CropImageGeomFilterHandle(k_CropImageGeomFilterId, k_SimplnxCorePluginId); + + +// Make sure we can instantiate the RotateSampleRefFrame Filter +std::unique_ptr CreateRotateSampleRefFrameFilter() +{ + FilterList* filterListPtr = Application::Instance()->getFilterList(); + std::unique_ptr filter = filterListPtr->createFilter(k_RotateSampleRefFrameFilterHandle); + return filter; +} + +template +void FlipAboutYAxis(DataArray& dataArray, Vec3& dims) +{ + AbstractDataStore& tempDataStore = dataArray.getDataStoreRef(); + + usize numComp = tempDataStore.getNumberOfComponents(); + std::vector currentRowBuffer(dims[0] * dataArray.getNumberOfComponents()); + + for(usize row = 0; row < dims[1]; row++) + { + // Copy the current row into a temp buffer + typename AbstractDataStore::Iterator startIter = tempDataStore.begin() + (dims[0] * numComp * row); + typename AbstractDataStore::Iterator endIter = startIter + dims[0] * numComp; + std::copy(startIter, endIter, currentRowBuffer.begin()); + + // Starting at the last tuple in the buffer + usize bufferIndex = (dims[0] - 1) * numComp; + usize dataStoreIndex = row * dims[0] * numComp; + + for(usize tupleIdx = 0; tupleIdx < dims[0]; tupleIdx++) + { + for(usize cIdx = 0; cIdx < numComp; cIdx++) + { + tempDataStore.setValue(dataStoreIndex, currentRowBuffer[bufferIndex + cIdx]); + dataStoreIndex++; + } + bufferIndex = bufferIndex - numComp; + } + } +} + +template +void FlipAboutXAxis(DataArray& dataArray, Vec3& dims) +{ + AbstractDataStore& tempDataStore = dataArray.getDataStoreRef(); + usize numComp = tempDataStore.getNumberOfComponents(); + size_t rowLCV = (dims[1] % 2 == 1) ? ((dims[1] - 1) / 2) : dims[1] / 2; + usize bottomRow = dims[1] - 1; + + for(usize row = 0; row < rowLCV; row++) + { + // Copy the "top" row into a temp buffer + usize topStartIter = 0 + (dims[0] * numComp * row); + usize topEndIter = topStartIter + dims[0] * numComp; + usize bottomStartIter = 0 + (dims[0] * numComp * bottomRow); + + // Copy from bottom to top and then temp to bottom + for(usize eleIndex = topStartIter; eleIndex < topEndIter; eleIndex++) + { + T value = tempDataStore.getValue(eleIndex); + tempDataStore[eleIndex] = tempDataStore[bottomStartIter]; + tempDataStore[bottomStartIter] = value; + bottomStartIter++; + } + bottomRow--; + } +} + + +} + +namespace nx::core +{ +//------------------------------------------------------------------------------ +std::string ReadImageStackFilter::name() const +{ + return FilterTraits::name.str(); +} + +//------------------------------------------------------------------------------ +std::string ReadImageStackFilter::className() const +{ + return FilterTraits::className; +} + +//------------------------------------------------------------------------------ +Uuid ReadImageStackFilter::uuid() const +{ + return FilterTraits::uuid; +} + +//------------------------------------------------------------------------------ +std::string ReadImageStackFilter::humanName() const +{ + return "Read Image Stack"; +} + +//------------------------------------------------------------------------------ +std::vector ReadImageStackFilter::defaultTags() const +{ + return {className(), "IO", "Input", "Read", "Import", "Image", "Tif", "JPEG", "PNG"}; +} + +//------------------------------------------------------------------------------ +Parameters ReadImageStackFilter::parameters() const +{ + Parameters params; + + params.insertSeparator(Parameters::Separator{"Input Parameter(s)"}); + params.insert( + std::make_unique(k_InputFileListInfo_Key, "Input File List", "The list of 2D image files to be read in to a 3D volume", GeneratedFileListParameter::ValueType{})); + + params.insertSeparator(Parameters::Separator{"Cropping Options"}); + params.insert(std::make_unique( + k_CroppingOptions_Key, "Cropping Options", + "The cropping options used to crop images. These include picking the cropping type, the cropping dimensions, and the cropping ranges for each chosen dimension.", + CropGeometryParameter::ValueType{})); + + params.insertSeparator(Parameters::Separator{"Origin & Spacing Options"}); + params.insertLinkableParameter(std::make_unique(k_ChangeOrigin_Key, "Set Origin", "Specifies if the origin should be changed", false)); + params.insert(std::make_unique(k_Origin_Key, "Origin", "The origin of the 3D volume", std::vector{0.0F, 0.0F, 0.0F}, std::vector{"X", "y", "Z"})); + params.insertLinkableParameter(std::make_unique(k_ChangeSpacing_Key, "Set Spacing", "Specifies if the spacing should be changed", false)); + params.insert(std::make_unique(k_Spacing_Key, "Spacing", "The spacing of the 3D volume", std::vector{1.0F, 1.0F, 1.0F}, std::vector{"X", "y", "Z"})); + params.insert(std::make_unique(k_OriginSpacingProcessing_Key, "Origin & Spacing Processing", "Whether the origin & spacing should be preprocessed or postprocessed.", 1, + ChoicesParameter::Choices{"Preprocessed", "Postprocessed"})); + + params.insertSeparator(Parameters::Separator{"Resampling Options"}); + params.insertLinkableParameter(std::make_unique(k_ResampleImagesChoice_Key, "Resample Images", + "Mode can be [0] Do Not Rescale, [1] Scaling as Percent, [2] Exact X/Y Dimensions For Resampling Along Z Axis", + ::k_NoResampleModeIndex, ::k_ResamplingChoices)); + params.insert(std::make_unique( + k_Scaling_Key, "Scaling (%)", + "The scaling of the 3D volume, in percentages. Percentage must be greater than or equal to 1.0f. Larger percentages will cause more voxels, smaller percentages " + "will cause less voxels. For example, 10.0 is one-tenth the original number of pixels. 200.0 is double the number of pixels.", + 100.0f)); + params.insert(std::make_unique(k_ExactXYDimensions_Key, "Exact 2D Dimensions (Pixels)", + "The supplied dimensions will be used to determine the resampled output geometry size. See associated Filter documentation for further detail.", + std::vector{100, 100}, std::vector({"X", "Y"}))); + + params.insertSeparator(Parameters::Separator{"Other Slice Options"}); + params.insertLinkableParameter( + std::make_unique(k_ConvertToGrayScale_Key, "Convert To GrayScale", "The filter will show an error if the images are already in grayscale format", false)); + params.insert(std::make_unique(k_ColorWeights_Key, "Color Weighting", "RGB weights for the grayscale conversion using the luminosity algorithm.", + std::vector{0.2125f, 0.7154f, 0.0721f}, std::vector({"Red", "Green", "Blue"}))); + params.insertLinkableParameter( + std::make_unique(k_ImageTransformChoice_Key, "Flip Slice", "Operation that is performed on each slice. 0=None, 1=Flip about X, 2=Flip about Y", 0, k_SliceOperationChoices)); + + params.insertLinkableParameter(std::make_unique(k_ChangeDataType_Key, "Set Image Data Type", "Set the final created image data type.", false)); + params.insert(std::make_unique(k_ImageDataType_Key, "Output Data Type", "Numeric Type of data to create", 0ULL, + ChoicesParameter::Choices{"uint8", "uint16", "uint32"})); // Sequence Dependent DO NOT REORDER + + params.insertSeparator(Parameters::Separator{"Output Data"}); + params.insert(std::make_unique(k_ImageGeometryPath_Key, "Created Image Geometry", "The path to the created Image Geometry", DataPath({"ImageDataContainer"}))); + params.insert(std::make_unique(k_CellDataName_Key, "Cell Data Name", "The name of the created cell attribute matrix", ImageGeom::k_CellAttributeMatrixName)); + params.insert(std::make_unique(k_ImageDataArrayPath_Key, "Created Image Data", "The path to the created image data array", "ImageData")); + + params.linkParameters(k_ConvertToGrayScale_Key, k_ColorWeights_Key, true); + params.linkParameters(k_ResampleImagesChoice_Key, k_Scaling_Key, ::k_ScalingModeIndex); + params.linkParameters(k_ResampleImagesChoice_Key, k_ExactXYDimensions_Key, ::k_ExactDimensionsModeIndex); + params.linkParameters(k_ChangeDataType_Key, k_ImageDataType_Key, true); + params.linkParameters(k_ChangeOrigin_Key, k_Origin_Key, true); + params.linkParameters(k_ChangeSpacing_Key, k_Spacing_Key, true); + params.linkParameters(k_ChangeOrigin_Key, k_OriginSpacingProcessing_Key, true); + params.linkParameters(k_ChangeSpacing_Key, k_OriginSpacingProcessing_Key, true); + + return params; +} + +//------------------------------------------------------------------------------ +IFilter::VersionType ReadImageStackFilter::parametersVersion() const +{ + return 1; +} + +//------------------------------------------------------------------------------ +IFilter::UniquePointer ReadImageStackFilter::clone() const +{ + return std::make_unique(); +} + +//------------------------------------------------------------------------------ +IFilter::PreflightResult ReadImageStackFilter::preflightImpl(const DataStructure& dataStructure, const Arguments& filterArgs, const MessageHandler& messageHandler, + const std::atomic_bool& shouldCancel, const ExecutionContext& executionContext) const +{ + auto inputFileListInfo = filterArgs.value(k_InputFileListInfo_Key); + auto shouldChangeOrigin = filterArgs.value(k_ChangeOrigin_Key); + auto shouldChangeSpacing = filterArgs.value(k_ChangeSpacing_Key); + auto origin = filterArgs.value(k_Origin_Key); + auto spacing = filterArgs.value(k_Spacing_Key); + auto originSpacingProcessing = static_cast(filterArgs.value(k_OriginSpacingProcessing_Key)); + auto imageGeomPath = filterArgs.value(k_ImageGeometryPath_Key); + auto pImageDataArrayNameValue = filterArgs.value(k_ImageDataArrayPath_Key); + auto cellDataName = filterArgs.value(k_CellDataName_Key); + auto imageTransformValue = filterArgs.value(k_ImageTransformChoice_Key); + auto pConvertToGrayScaleValue = filterArgs.value(k_ConvertToGrayScale_Key); + auto pColorWeightsValue = filterArgs.value(k_ColorWeights_Key); + auto pResampleImagesChoiceValue = filterArgs.value(k_ResampleImagesChoice_Key); + auto pScalingValue = filterArgs.value(k_Scaling_Key); + auto pExactXYDimsValue = filterArgs.value(k_ExactXYDimensions_Key); + + auto pChangeDataType = filterArgs.value(k_ChangeDataType_Key); + auto numericType = filterArgs.value(k_ImageDataType_Key); + auto croppingOptions = filterArgs.value(k_CroppingOptions_Key); + + nx::core::Result resultOutputActions; + std::vector preflightUpdatedValues; + PreflightResult preflightResult; + + + return {std::move(resultOutputActions), std::move(preflightUpdatedValues)}; +} + +//------------------------------------------------------------------------------ +Result<> ReadImageStackFilter::executeImpl(DataStructure& dataStructure, const Arguments& filterArgs, const PipelineFilter* pipelineNode, const MessageHandler& messageHandler, + const std::atomic_bool& shouldCancel, const ExecutionContext& executionContext) const +{ + + ReadImageStackInputValues inputValues; + + + + + return ReadImageStack(dataStructure, messageHandler, shouldCancel, &inputValues)(); +} + +namespace +{ +namespace SIMPL +{ + + //TODO: PARAMETER_JSON_CONSTANTS +} // namespace SIMPL +} // namespace + + +//------------------------------------------------------------------------------ +Result ReadImageStackFilter::FromSIMPLJson(const nlohmann::json& json) +{ + Arguments args = ReadImageStackFilter().getDefaultArguments(); + + std::vector> results; + + /* This is a NEW filter and not ported so this section does not matter */ + + Result<> conversionResult = MergeResults(std::move(results)); + + return ConvertResultTo(std::move(conversionResult), std::move(args)); +} + + +} // namespace nx::core diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ReadImageStackFilter.hpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ReadImageStackFilter.hpp new file mode 100644 index 0000000000..b021bbe767 --- /dev/null +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ReadImageStackFilter.hpp @@ -0,0 +1,135 @@ +#pragma once + +#include "SimplnxCore/SimplnxCore_export.hpp" + +#include "simplnx/Filter/FilterTraits.hpp" +#include "simplnx/Filter/IFilter.hpp" + +namespace nx::core +{ +/** + * @class ReadImageStackFilter + * @brief This filter will .... + */ +class SIMPLNXCORE_EXPORT ReadImageStackFilter : public IFilter +{ +public: + ReadImageStackFilter() = default; + ~ReadImageStackFilter() noexcept override = default; + + ReadImageStackFilter(const ReadImageStackFilter&) = delete; + ReadImageStackFilter(ReadImageStackFilter&&) noexcept = delete; + + ReadImageStackFilter& operator=(const ReadImageStackFilter&) = delete; + ReadImageStackFilter& operator=(ReadImageStackFilter&&) noexcept = delete; + + // Parameter Keys + static constexpr StringLiteral k_InputFileListInfo_Key = "input_file_list_object"; + static constexpr StringLiteral k_CroppingOptions_Key = "cropping_options"; + static constexpr StringLiteral k_ChangeOrigin_Key = "change_origin"; + static constexpr StringLiteral k_Origin_Key = "origin"; + static constexpr StringLiteral k_ChangeSpacing_Key = "change_spacing"; + static constexpr StringLiteral k_Spacing_Key = "spacing"; + static constexpr StringLiteral k_OriginSpacingProcessing_Key = "origin_spacing_processing_index"; + static constexpr StringLiteral k_ImageGeometryPath_Key = "output_image_geometry_path"; + static constexpr StringLiteral k_ImageDataArrayPath_Key = "image_data_array_name"; + static constexpr StringLiteral k_CellDataName_Key = "cell_attribute_matrix_name"; + static constexpr StringLiteral k_ImageTransformChoice_Key = "image_transform_index"; + static constexpr StringLiteral k_ConvertToGrayScale_Key = "convert_to_gray_scale"; + static constexpr StringLiteral k_ColorWeights_Key = "color_weights"; + static constexpr StringLiteral k_ResampleImagesChoice_Key = "resample_images_index"; + static constexpr StringLiteral k_Scaling_Key = "scaling"; + static constexpr StringLiteral k_ExactXYDimensions_Key = "exact_xy_dimensions"; + static constexpr StringLiteral k_ChangeDataType_Key = "change_image_data_type"; + static constexpr StringLiteral k_ImageDataType_Key = "image_data_type_index"; + + /** + * @brief Reads SIMPL json and converts it simplnx Arguments. + * @param json + * @return Result + */ + static Result FromSIMPLJson(const nlohmann::json& json); + + /** + * @brief Returns the name of the filter. + * @return + */ + std::string name() const override; + + /** + * @brief Returns the C++ classname of this filter. + * @return + */ + std::string className() const override; + + /** + * @brief Returns the uuid of the filter. + * @return + */ + Uuid uuid() const override; + + /** + * @brief Returns the human readable name of the filter. + * @return + */ + std::string humanName() const override; + + /** + * @brief Returns the default tags for this filter. + * @return + */ + std::vector defaultTags() const override; + + /** + * @brief Returns the parameters of the filter (i.e. its inputs) + * @return + */ + Parameters parameters() const override; + + /** + * @brief Returns parameters version integer. + * Initial version should always be 1. + * Should be incremented everytime the parameters change. + * @return VersionType + */ + VersionType parametersVersion() const override; + + /** + * @brief Returns a copy of the filter. + * @return + */ + UniquePointer clone() const override; + +protected: + /** + * @brief Takes in a DataStructure and checks that the filter can be run on it with the given arguments. + * Returns any warnings/errors. Also returns the changes that would be applied to the DataStructure. + * Some parts of the actions may not be completely filled out if all the required information is not available at preflight time. + * @param dataStructure The input DataStructure instance + * @param filterArgs These are the input values for each parameter that is required for the filter + * @param messageHandler The MessageHandler object + * @param shouldCancel Atomic boolean value that can be checked to cancel the filter + * @param executionContext The ExecutionContext that can be used to determine the correct absolute path from a relative path + * @return Returns a Result object with error or warning values if any of those occurred during execution of this function + */ + PreflightResult preflightImpl(const DataStructure& dataStructure, const Arguments& filterArgs, const MessageHandler& messageHandler, const std::atomic_bool& shouldCancel, + const ExecutionContext& executionContext) const override; + + /** + * @brief Applies the filter's algorithm to the DataStructure with the given arguments. Returns any warnings/errors. + * On failure, there is no guarantee that the DataStructure is in a correct state. + * @param dataStructure The input DataStructure instance + * @param filterArgs These are the input values for each parameter that is required for the filter + * @param pipelineNode The node in the pipeline that is being executed + * @param messageHandler The MessageHandler object + * @param shouldCancel Atomic boolean value that can be checked to cancel the filter + * @param executionContext The ExecutionContext that can be used to determine the correct absolute path from a relative path + * @return Returns a Result object with error or warning values if any of those occurred during execution of this function + */ + Result<> executeImpl(DataStructure& dataStructure, const Arguments& filterArgs, const PipelineFilter* pipelineNode, const MessageHandler& messageHandler, const std::atomic_bool& shouldCancel, + const ExecutionContext& executionContext) const override; +}; +} // namespace complex + +SIMPLNX_DEF_FILTER_TRAITS(nx::core, ReadImageStackFilter, "15e7784e-6a9b-457c-9f5c-f5983246c8e8"); +/* LEGACY UUID FOR THIS FILTER @OLD_UUID@ */ diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteImageFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteImageFilter.cpp new file mode 100644 index 0000000000..2c47aeca30 --- /dev/null +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteImageFilter.cpp @@ -0,0 +1,176 @@ +#include "WriteImageFilter.hpp" + +#include "SimplnxCore/Filters/Algorithms/WriteImage.hpp" + +#include "simplnx/DataStructure/DataPath.hpp" +#include "simplnx/Filter/Actions/EmptyAction.hpp" +#include "simplnx/Common/AtomicFile.hpp" +#include "simplnx/DataStructure/DataPath.hpp" +#include "simplnx/DataStructure/DataStore.hpp" +#include "simplnx/DataStructure/Geometry/ImageGeom.hpp" +#include "simplnx/Parameters/ArraySelectionParameter.hpp" +#include "simplnx/Parameters/ChoicesParameter.hpp" +#include "simplnx/Parameters/DataGroupSelectionParameter.hpp" +#include "simplnx/Parameters/DataObjectNameParameter.hpp" +#include "simplnx/Parameters/FileSystemPathParameter.hpp" +#include "simplnx/Parameters/GeometrySelectionParameter.hpp" +#include "simplnx/Parameters/NumberParameter.hpp" +#include "simplnx/Parameters/StringParameter.hpp" +#include "simplnx/Utilities/StringUtilities.hpp" +#include "simplnx/Utilities/SIMPLConversion.hpp" + +#include + +#include +#include +#include +#include +#include + +namespace fs = std::filesystem; + +using namespace nx::core; + +namespace +{ +const std::set& GetScalarPixelAllowedTypes() +{ + static const std::set dataTypes = {nx::core::DataType::int8, nx::core::DataType::uint8, nx::core::DataType::int16, nx::core::DataType::uint16, nx::core::DataType::int32, + nx::core::DataType::uint32, nx::core::DataType::float32}; + return dataTypes; +} +} + +namespace nx::core +{ + +//------------------------------------------------------------------------------ +std::string WriteImageFilter::name() const +{ + return FilterTraits::name.str(); +} + +//------------------------------------------------------------------------------ +std::string WriteImageFilter::className() const +{ + return FilterTraits::className; +} + +//------------------------------------------------------------------------------ +Uuid WriteImageFilter::uuid() const +{ + return FilterTraits::uuid; +} + +//------------------------------------------------------------------------------ +std::string WriteImageFilter::humanName() const +{ + return "Write Image"; +} + +//------------------------------------------------------------------------------ +std::vector WriteImageFilter::defaultTags() const +{ + return {className(), "io", "output", "write", "export", "image", "jpg", "tiff", "bmp", "png"}; +} + +//------------------------------------------------------------------------------ +Parameters WriteImageFilter::parameters() const +{ + Parameters params; + + using ExtensionListType = std::unordered_set; + params.insertSeparator(Parameters::Separator{"Input Parameter(s)"}); + params.insert(std::make_unique(k_Plane_Key, "Plane", "Selection for plane normal for writing the images (XY, XZ, or YZ)", 0, ChoicesParameter::Choices{"XY", "XZ", "YZ"})); + params.insert( + std::make_unique(k_FileName_Key, "Output File", "Path to the output file to write.", fs::path(), ExtensionListType{}, FileSystemPathParameter::PathType::OutputFile)); + params.insert(std::make_unique(k_IndexOffset_Key, "Index Offset", "This is the starting index when writing multiple images", 0)); + params.insert(std::make_unique(k_TotalIndexDigits_Key, "Total Number of Index Digits", "This is the total number of digits to use when generating the index", 3)); + params.insert(std::make_unique(k_LeadingDigitCharacter_Key, "Fill Character", "The character to use for the leading digits if needed", "0")); + + params.insertSeparator(Parameters::Separator{"Input Cell Data"}); + params.insert(std::make_unique(k_ImageGeomPath_Key, "Image Geometry", "Select the Image Geometry Group from the DataStructure.", DataPath{}, + GeometrySelectionParameter::AllowedTypes{IGeometry::Type::Image})); + params.insert(std::make_unique(k_ImageArrayPath_Key, "Input Image Data Array", "The image data that will be processed by this filter.", DataPath{}, + ::GetScalarPixelAllowedTypes())); + + return params; +} + +//------------------------------------------------------------------------------ +IFilter::VersionType WriteImageFilter::parametersVersion() const +{ + return 1; +} + +//------------------------------------------------------------------------------ +IFilter::UniquePointer WriteImageFilter::clone() const +{ + return std::make_unique(); +} + +//------------------------------------------------------------------------------ +IFilter::PreflightResult WriteImageFilter::preflightImpl(const DataStructure& dataStructure, const Arguments& filterArgs, const MessageHandler& messageHandler, + const std::atomic_bool& shouldCancel, const ExecutionContext& executionContext) const +{ + auto plane = filterArgs.value(k_Plane_Key); + auto filePath = filterArgs.value(k_FileName_Key); + auto indexOffset = filterArgs.value(k_IndexOffset_Key); + auto imageArrayPath = filterArgs.value(k_ImageArrayPath_Key); + auto imageGeomPath = filterArgs.value(k_ImageGeomPath_Key); + + auto totalDigits = filterArgs.value(k_TotalIndexDigits_Key); + auto fillChar = filterArgs.value(k_LeadingDigitCharacter_Key); + + // Stored fastest to slowest i.e. X Y Z + const auto& imageGeom = dataStructure.getDataRefAs(imageGeomPath); + // Stored slowest to fastest i.e. Z Y X + const auto& imageArray = dataStructure.getDataRefAs(imageArrayPath); + + + PreflightResult preflightResult; + nx::core::Result resultOutputActions; + std::vector preflightUpdatedValues; + + return {std::move(resultOutputActions), std::move(preflightUpdatedValues)}; +} + +//------------------------------------------------------------------------------ +Result<> WriteImageFilter::executeImpl(DataStructure& dataStructure, const Arguments& filterArgs, const PipelineFilter* pipelineNode, const MessageHandler& messageHandler, + const std::atomic_bool& shouldCancel, const ExecutionContext& executionContext) const +{ + + WriteImageInputValues inputValues; + + + //TODO: INPUT_VALUES_DEF + + return WriteImage(dataStructure, messageHandler, shouldCancel, &inputValues)(); +} + +namespace +{ +namespace SIMPL +{ + + //TODO: PARAMETER_JSON_CONSTANTS +} // namespace SIMPL +} // namespace + + +//------------------------------------------------------------------------------ +Result WriteImageFilter::FromSIMPLJson(const nlohmann::json& json) +{ + Arguments args = WriteImageFilter().getDefaultArguments(); + + std::vector> results; + + /* This is a NEW filter and not ported so this section does not matter */ + + Result<> conversionResult = MergeResults(std::move(results)); + + return ConvertResultTo(std::move(conversionResult), std::move(args)); +} + + +} // namespace nx::core diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteImageFilter.hpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteImageFilter.hpp new file mode 100644 index 0000000000..a99a77edd8 --- /dev/null +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteImageFilter.hpp @@ -0,0 +1,124 @@ +#pragma once + +#include "SimplnxCore/SimplnxCore_export.hpp" + +#include "simplnx/Filter/FilterTraits.hpp" +#include "simplnx/Filter/IFilter.hpp" + +namespace nx::core +{ +/** + * @class WriteImageFilter + * @brief This filter will .... + */ +class SIMPLNXCORE_EXPORT WriteImageFilter : public IFilter +{ +public: + WriteImageFilter() = default; + ~WriteImageFilter() noexcept override = default; + + WriteImageFilter(const WriteImageFilter&) = delete; + WriteImageFilter(WriteImageFilter&&) noexcept = delete; + + WriteImageFilter& operator=(const WriteImageFilter&) = delete; + WriteImageFilter& operator=(WriteImageFilter&&) noexcept = delete; + + // Parameter Keys + static constexpr StringLiteral k_Plane_Key = "plane_index"; + static constexpr StringLiteral k_FileName_Key = "file_name"; + static constexpr StringLiteral k_IndexOffset_Key = "index_offset"; + static constexpr StringLiteral k_ImageArrayPath_Key = "image_array_path"; + static constexpr StringLiteral k_ImageGeomPath_Key = "input_image_geometry_path"; + static constexpr StringLiteral k_TotalIndexDigits_Key = "total_index_digits"; + static constexpr StringLiteral k_LeadingDigitCharacter_Key = "leading_digit_character"; + + /** + * @brief Reads SIMPL json and converts it simplnx Arguments. + * @param json + * @return Result + */ + static Result FromSIMPLJson(const nlohmann::json& json); + + /** + * @brief Returns the name of the filter. + * @return + */ + std::string name() const override; + + /** + * @brief Returns the C++ classname of this filter. + * @return + */ + std::string className() const override; + + /** + * @brief Returns the uuid of the filter. + * @return + */ + Uuid uuid() const override; + + /** + * @brief Returns the human readable name of the filter. + * @return + */ + std::string humanName() const override; + + /** + * @brief Returns the default tags for this filter. + * @return + */ + std::vector defaultTags() const override; + + /** + * @brief Returns the parameters of the filter (i.e. its inputs) + * @return + */ + Parameters parameters() const override; + + /** + * @brief Returns parameters version integer. + * Initial version should always be 1. + * Should be incremented everytime the parameters change. + * @return VersionType + */ + VersionType parametersVersion() const override; + + /** + * @brief Returns a copy of the filter. + * @return + */ + UniquePointer clone() const override; + +protected: + /** + * @brief Takes in a DataStructure and checks that the filter can be run on it with the given arguments. + * Returns any warnings/errors. Also returns the changes that would be applied to the DataStructure. + * Some parts of the actions may not be completely filled out if all the required information is not available at preflight time. + * @param dataStructure The input DataStructure instance + * @param filterArgs These are the input values for each parameter that is required for the filter + * @param messageHandler The MessageHandler object + * @param shouldCancel Atomic boolean value that can be checked to cancel the filter + * @param executionContext The ExecutionContext that can be used to determine the correct absolute path from a relative path + * @return Returns a Result object with error or warning values if any of those occurred during execution of this function + */ + PreflightResult preflightImpl(const DataStructure& dataStructure, const Arguments& filterArgs, const MessageHandler& messageHandler, const std::atomic_bool& shouldCancel, + const ExecutionContext& executionContext) const override; + + /** + * @brief Applies the filter's algorithm to the DataStructure with the given arguments. Returns any warnings/errors. + * On failure, there is no guarantee that the DataStructure is in a correct state. + * @param dataStructure The input DataStructure instance + * @param filterArgs These are the input values for each parameter that is required for the filter + * @param pipelineNode The node in the pipeline that is being executed + * @param messageHandler The MessageHandler object + * @param shouldCancel Atomic boolean value that can be checked to cancel the filter + * @param executionContext The ExecutionContext that can be used to determine the correct absolute path from a relative path + * @return Returns a Result object with error or warning values if any of those occurred during execution of this function + */ + Result<> executeImpl(DataStructure& dataStructure, const Arguments& filterArgs, const PipelineFilter* pipelineNode, const MessageHandler& messageHandler, const std::atomic_bool& shouldCancel, + const ExecutionContext& executionContext) const override; +}; +} // namespace complex + +SIMPLNX_DEF_FILTER_TRAITS(nx::core, WriteImageFilter, "a8b920c7-5445-4c8a-b7d7-6cabc578d587"); +/* LEGACY UUID FOR THIS FILTER @OLD_UUID@ */ diff --git a/src/Plugins/SimplnxCore/test/CMakeLists.txt b/src/Plugins/SimplnxCore/test/CMakeLists.txt index 148a53daa6..265e09e869 100644 --- a/src/Plugins/SimplnxCore/test/CMakeLists.txt +++ b/src/Plugins/SimplnxCore/test/CMakeLists.txt @@ -82,6 +82,7 @@ set(${PLUGIN_NAME}UnitTest_SRCS ErodeDilateMaskTest.cpp ExecuteProcessTest.cpp ExtractComponentAsArrayTest.cpp + ExtractFeatureBoundaries2DTest.cpp ExtractInternalSurfacesFromTriangleGeometryTest.cpp ExtractPipelineToFileTest.cpp ExtractVertexGeometryTest.cpp @@ -105,7 +106,6 @@ set(${PLUGIN_NAME}UnitTest_SRCS PadImageGeometryTest.cpp PartitionGeometryTest.cpp PipelineTest.cpp - ExtractFeatureBoundaries2DTest.cpp PointSampleEdgeGeometryTest.cpp PointSampleTriangleGeometryFilterTest.cpp QuickSurfaceMeshFilterTest.cpp @@ -113,12 +113,15 @@ set(${PLUGIN_NAME}UnitTest_SRCS ReadBinaryCTNorthstarTest.cpp ReadCSVFileTest.cpp ReadHDF5DatasetTest.cpp + ReadImageStackTest.cpp + ReadImageTest.cpp ReadRawBinaryTest.cpp ReadStlFileTest.cpp ReadStringDataArrayTest.cpp ReadTextDataArrayTest.cpp ReadVolumeGraphicsFileTest.cpp ReadVtkStructuredPointsTest.cpp + ReadZeissTxmFileTest.cpp RegularGridSampleSurfaceMeshTest.cpp RemoveFlaggedEdgesTest.cpp RemoveFlaggedFeaturesTest.cpp @@ -153,6 +156,7 @@ set(${PLUGIN_NAME}UnitTest_SRCS WriteAvizoUniformCoordinateTest.cpp WriteBinaryDataTest.cpp WriteFeatureDataCSVTest.cpp + WriteImageTest.cpp WriteLAMMPSFileTest.cpp WriteLosAlamosFFTTest.cpp WriteNodesAndElementsFilesTest.cpp @@ -160,7 +164,6 @@ set(${PLUGIN_NAME}UnitTest_SRCS WriteSPParksSitesTest.cpp WriteStlFileTest.cpp WriteVtkRectilinearGridTest.cpp - ReadZeissTxmFileTest.cpp ) create_simplnx_plugin_unit_test(PLUGIN_NAME ${PLUGIN_NAME} diff --git a/src/Plugins/SimplnxCore/test/ReadImageStackTest.cpp b/src/Plugins/SimplnxCore/test/ReadImageStackTest.cpp new file mode 100644 index 0000000000..6ee1ef912a --- /dev/null +++ b/src/Plugins/SimplnxCore/test/ReadImageStackTest.cpp @@ -0,0 +1,86 @@ +/** + * This file is auto generated from the original SimplnxCore/ReadImageStackFilter + * runtime information. These are the steps that need to be taken to utilize this + * unit test in the proper way. + * + * 1: Validate each of the default parameters that gets created. + * 2: Inspect the actual filter to determine if the filter in its default state + * would pass or fail BOTH the preflight() and execute() methods + * 3: UPDATE the ```REQUIRE(result.result.valid());``` code to have the proper + * + * 4: Add additional unit tests to actually test each code path within the filter + * + * There are some example Catch2 ```TEST_CASE``` sections for your inspiration. + * + * NOTE the format of the ```TEST_CASE``` macro. Please stick to this format to + * allow easier parsing of the unit tests. + * + * When you start working on this unit test remove "[ReadImageStackFilter][.][UNIMPLEMENTED]" + * from the TEST_CASE macro. This will enable this unit test to be run by default + * and report errors. + */ + + +#include + + + //TODO: PARAMETER_INCLUDES +#include "SimplnxCore/Filters/ReadImageStackFilter.hpp" +#include "SimplnxCore/SimplnxCore_test_dirs.hpp" + +#include "simplnx/UnitTest/UnitTestCommon.hpp" + +#include + +using namespace nx::core; +using namespace nx::core::UnitTest; +using namespace nx::core::Constants; + +TEST_CASE("SimplnxCore::ReadImageStackFilter: Valid Filter Execution","[SimplnxCore][ReadImageStackFilter]") +{ + UnitTest::LoadPlugins(); + + const nx::core::UnitTest::TestFileSentinel testDataSentinel( + nx::core::unit_test::k_TestFilesDir, + "ReadImageStackFilter.tar.gz", + "ReadImageStackFilter"); + + // Read the Small IN100 Data set + auto baseDataFilePath = fs::path(fmt::format("{}/ReadImageStackFilter/ReadImageStackFilter.dream3d", nx::core::unit_test::k_TestFilesDir)); + DataStructure dataStructure = UnitTest::LoadDataStructure(baseDataFilePath); + + // Instantiate the filter, a DataStructure object and an Arguments Object + { + Arguments args; + ReadImageStackFilter filter; + + // Create default Parameters for the filter. + // This next line is an example, your filter may be different + // args.insertOrAssign(ReadImageStackFilter::k_CreatedTriangleGeometryPath_Key, std::make_any(computedTriangleGeomPath)); + + // Preflight the filter and check result + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions) + + // Execute the filter and check the result + auto executeResult = filter.execute(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result) + + // Write the DataStructure out to the file system +#ifdef SIMPLNX_WRITE_TEST_OUTPUT + WriteTestDataStructure(dataStructure, fs::path(fmt::format("{}/ReadImageStackFilterTest.dream3d", unit_test::k_BinaryTestOutputDir))); +#endif + + } + // Compare exemplar data arrays with computed data arrays + //TODO: Insert verification codes + + // This should be in every unit test. If you think it does not apply, please review with another engineer + UnitTest::CheckArraysInheritTupleDims(dataStructure); + +} + +//TEST_CASE("SimplnxCore::ReadImageStackFilter: InValid Filter Execution") +//{ +// +//} diff --git a/src/Plugins/SimplnxCore/test/ReadImageTest.cpp b/src/Plugins/SimplnxCore/test/ReadImageTest.cpp new file mode 100644 index 0000000000..0109ae954b --- /dev/null +++ b/src/Plugins/SimplnxCore/test/ReadImageTest.cpp @@ -0,0 +1,86 @@ +/** + * This file is auto generated from the original SimplnxCore/ReadImageFilter + * runtime information. These are the steps that need to be taken to utilize this + * unit test in the proper way. + * + * 1: Validate each of the default parameters that gets created. + * 2: Inspect the actual filter to determine if the filter in its default state + * would pass or fail BOTH the preflight() and execute() methods + * 3: UPDATE the ```REQUIRE(result.result.valid());``` code to have the proper + * + * 4: Add additional unit tests to actually test each code path within the filter + * + * There are some example Catch2 ```TEST_CASE``` sections for your inspiration. + * + * NOTE the format of the ```TEST_CASE``` macro. Please stick to this format to + * allow easier parsing of the unit tests. + * + * When you start working on this unit test remove "[ReadImageFilter][.][UNIMPLEMENTED]" + * from the TEST_CASE macro. This will enable this unit test to be run by default + * and report errors. + */ + + +#include + + + //TODO: PARAMETER_INCLUDES +#include "SimplnxCore/Filters/ReadImageFilter.hpp" +#include "SimplnxCore/SimplnxCore_test_dirs.hpp" + +#include "simplnx/UnitTest/UnitTestCommon.hpp" + +#include + +using namespace nx::core; +using namespace nx::core::UnitTest; +using namespace nx::core::Constants; + +TEST_CASE("SimplnxCore::ReadImageFilter: Valid Filter Execution","[SimplnxCore][ReadImageFilter]") +{ + UnitTest::LoadPlugins(); + + const nx::core::UnitTest::TestFileSentinel testDataSentinel( + nx::core::unit_test::k_TestFilesDir, + "ReadImageFilter.tar.gz", + "ReadImageFilter"); + + // Read the Small IN100 Data set + auto baseDataFilePath = fs::path(fmt::format("{}/ReadImageFilter/ReadImageFilter.dream3d", nx::core::unit_test::k_TestFilesDir)); + DataStructure dataStructure = UnitTest::LoadDataStructure(baseDataFilePath); + + // Instantiate the filter, a DataStructure object and an Arguments Object + { + Arguments args; + ReadImageFilter filter; + + // Create default Parameters for the filter. + // This next line is an example, your filter may be different + // args.insertOrAssign(ReadImageFilter::k_CreatedTriangleGeometryPath_Key, std::make_any(computedTriangleGeomPath)); + + // Preflight the filter and check result + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions) + + // Execute the filter and check the result + auto executeResult = filter.execute(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result) + + // Write the DataStructure out to the file system +#ifdef SIMPLNX_WRITE_TEST_OUTPUT + WriteTestDataStructure(dataStructure, fs::path(fmt::format("{}/ReadImageFilterTest.dream3d", unit_test::k_BinaryTestOutputDir))); +#endif + + } + // Compare exemplar data arrays with computed data arrays + //TODO: Insert verification codes + + // This should be in every unit test. If you think it does not apply, please review with another engineer + UnitTest::CheckArraysInheritTupleDims(dataStructure); + +} + +//TEST_CASE("SimplnxCore::ReadImageFilter: InValid Filter Execution") +//{ +// +//} diff --git a/src/Plugins/SimplnxCore/test/WriteImageTest.cpp b/src/Plugins/SimplnxCore/test/WriteImageTest.cpp new file mode 100644 index 0000000000..b35d646991 --- /dev/null +++ b/src/Plugins/SimplnxCore/test/WriteImageTest.cpp @@ -0,0 +1,86 @@ +/** + * This file is auto generated from the original SimplnxCore/WriteImageFilter + * runtime information. These are the steps that need to be taken to utilize this + * unit test in the proper way. + * + * 1: Validate each of the default parameters that gets created. + * 2: Inspect the actual filter to determine if the filter in its default state + * would pass or fail BOTH the preflight() and execute() methods + * 3: UPDATE the ```REQUIRE(result.result.valid());``` code to have the proper + * + * 4: Add additional unit tests to actually test each code path within the filter + * + * There are some example Catch2 ```TEST_CASE``` sections for your inspiration. + * + * NOTE the format of the ```TEST_CASE``` macro. Please stick to this format to + * allow easier parsing of the unit tests. + * + * When you start working on this unit test remove "[WriteImageFilter][.][UNIMPLEMENTED]" + * from the TEST_CASE macro. This will enable this unit test to be run by default + * and report errors. + */ + + +#include + + + //TODO: PARAMETER_INCLUDES +#include "SimplnxCore/Filters/WriteImageFilter.hpp" +#include "SimplnxCore/SimplnxCore_test_dirs.hpp" + +#include "simplnx/UnitTest/UnitTestCommon.hpp" + +#include + +using namespace nx::core; +using namespace nx::core::UnitTest; +using namespace nx::core::Constants; + +TEST_CASE("SimplnxCore::WriteImageFilter: Valid Filter Execution","[SimplnxCore][WriteImageFilter]") +{ + UnitTest::LoadPlugins(); + + const nx::core::UnitTest::TestFileSentinel testDataSentinel( + nx::core::unit_test::k_TestFilesDir, + "WriteImageFilter.tar.gz", + "WriteImageFilter"); + + // Read the Small IN100 Data set + auto baseDataFilePath = fs::path(fmt::format("{}/WriteImageFilter/WriteImageFilter.dream3d", nx::core::unit_test::k_TestFilesDir)); + DataStructure dataStructure = UnitTest::LoadDataStructure(baseDataFilePath); + + // Instantiate the filter, a DataStructure object and an Arguments Object + { + Arguments args; + WriteImageFilter filter; + + // Create default Parameters for the filter. + // This next line is an example, your filter may be different + // args.insertOrAssign(WriteImageFilter::k_CreatedTriangleGeometryPath_Key, std::make_any(computedTriangleGeomPath)); + + // Preflight the filter and check result + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions) + + // Execute the filter and check the result + auto executeResult = filter.execute(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result) + + // Write the DataStructure out to the file system +#ifdef SIMPLNX_WRITE_TEST_OUTPUT + WriteTestDataStructure(dataStructure, fs::path(fmt::format("{}/WriteImageFilterTest.dream3d", unit_test::k_BinaryTestOutputDir))); +#endif + + } + // Compare exemplar data arrays with computed data arrays + //TODO: Insert verification codes + + // This should be in every unit test. If you think it does not apply, please review with another engineer + UnitTest::CheckArraysInheritTupleDims(dataStructure); + +} + +//TEST_CASE("SimplnxCore::WriteImageFilter: InValid Filter Execution") +//{ +// +//} diff --git a/vcpkg-configuration.json b/vcpkg-configuration.json index ab30d98c2d..5cb83bedfb 100644 --- a/vcpkg-configuration.json +++ b/vcpkg-configuration.json @@ -7,8 +7,8 @@ "registries": [ { "kind": "git", - "repository": "https://github.com/bluequartzsoftware/simplnx-registry", - "baseline": "abb3d4151d1425152fd85a75d35f9d7b035a3b0d", + "repository": "https://github.com/imikejackson/simplnx-registry", + "baseline": "0492974537e4cabdd6e52baeb918fd7bc9621e4c", "packages": [ "benchmark", "blosc", @@ -33,7 +33,9 @@ "vcpkg-cmake", "vcpkg-cmake-config", "zlib", - "zstd" + "zstd", + "tiff", + "stb" ] } ] diff --git a/vcpkg.json b/vcpkg.json index 0d9363aeef..9eb3a07643 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -15,6 +15,12 @@ { "name": "eigen3" }, + { + "name": "tiff" + }, + { + "name": "stb" + }, { "name": "hdf5", "features": [ From f805ee4a08ed559d3b5644cfedcc3663d1be8147 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Thu, 9 Apr 2026 23:31:31 -0400 Subject: [PATCH 02/16] Comment out cxItkImageReaderFilter reference in ReadImageStackFilter skeleton The skeleton references cxItkImageReaderFilter::OriginSpacingProcessingTiming which is an ITK plugin type not available in SimplnxCore. Commented out until ReadImageStackFilter implementation replaces it with a local enum. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/SimplnxCore/Filters/ReadImageStackFilter.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ReadImageStackFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ReadImageStackFilter.cpp index e6e0d71a5c..249114266d 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ReadImageStackFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ReadImageStackFilter.cpp @@ -240,7 +240,8 @@ IFilter::PreflightResult ReadImageStackFilter::preflightImpl(const DataStructure auto shouldChangeSpacing = filterArgs.value(k_ChangeSpacing_Key); auto origin = filterArgs.value(k_Origin_Key); auto spacing = filterArgs.value(k_Spacing_Key); - auto originSpacingProcessing = static_cast(filterArgs.value(k_OriginSpacingProcessing_Key)); + // TODO: Replace with local OriginSpacingProcessingTiming enum once ReadImageStackFilter is implemented + // auto originSpacingProcessing = static_cast(filterArgs.value(k_OriginSpacingProcessing_Key)); auto imageGeomPath = filterArgs.value(k_ImageGeometryPath_Key); auto pImageDataArrayNameValue = filterArgs.value(k_ImageDataArrayPath_Key); auto cellDataName = filterArgs.value(k_CellDataName_Key); From 7b6eced6ce8ca90c3a9b6cc0199b6db128c648de Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Thu, 9 Apr 2026 23:50:19 -0400 Subject: [PATCH 03/16] Add IImageIO abstraction layer with StbImageIO and TiffImageIO backends Introduces a strategy-pattern image I/O layer under Utilities/ImageIO/ that decouples image file reading/writing from simplnx data structures. Includes ImageMetadata struct, IImageIO abstract interface, factory function for backend selection by extension, and two concrete backends: StbImageIO (PNG/JPEG/BMP via stb_image/stb_image_write) and TiffImageIO (TIFF via libtiff with scanline and tiled image support). Co-Authored-By: Claude Opus 4.6 (1M context) --- src/simplnx/Utilities/ImageIO/IImageIO.hpp | 74 +++ .../Utilities/ImageIO/ImageIOFactory.cpp | 28 ++ .../Utilities/ImageIO/ImageIOFactory.hpp | 26 ++ .../Utilities/ImageIO/ImageMetadata.hpp | 32 ++ src/simplnx/Utilities/ImageIO/StbImageIO.cpp | 195 ++++++++ src/simplnx/Utilities/ImageIO/StbImageIO.hpp | 26 ++ src/simplnx/Utilities/ImageIO/TiffImageIO.cpp | 437 ++++++++++++++++++ src/simplnx/Utilities/ImageIO/TiffImageIO.hpp | 29 ++ 8 files changed, 847 insertions(+) create mode 100644 src/simplnx/Utilities/ImageIO/IImageIO.hpp create mode 100644 src/simplnx/Utilities/ImageIO/ImageIOFactory.cpp create mode 100644 src/simplnx/Utilities/ImageIO/ImageIOFactory.hpp create mode 100644 src/simplnx/Utilities/ImageIO/ImageMetadata.hpp create mode 100644 src/simplnx/Utilities/ImageIO/StbImageIO.cpp create mode 100644 src/simplnx/Utilities/ImageIO/StbImageIO.hpp create mode 100644 src/simplnx/Utilities/ImageIO/TiffImageIO.cpp create mode 100644 src/simplnx/Utilities/ImageIO/TiffImageIO.hpp diff --git a/src/simplnx/Utilities/ImageIO/IImageIO.hpp b/src/simplnx/Utilities/ImageIO/IImageIO.hpp new file mode 100644 index 0000000000..e7b0f17ada --- /dev/null +++ b/src/simplnx/Utilities/ImageIO/IImageIO.hpp @@ -0,0 +1,74 @@ +#pragma once + +#include "simplnx/simplnx_export.hpp" + +#include "simplnx/Common/Result.hpp" +#include "simplnx/Common/Types.hpp" +#include "simplnx/Utilities/ImageIO/ImageMetadata.hpp" + +#include +#include + +namespace nx::core +{ + +/** + * @class IImageIO + * @brief Abstract interface for reading and writing 2D image files. + * + * Implementations handle specific format backends (stb for PNG/JPEG/BMP, + * libtiff for TIFF). The interface operates on raw byte buffers and + * ImageMetadata structs -- it knows nothing about DataStore, ImageGeom, + * or any other simplnx data structures. + * + * Pixel data buffers are packed row-major, top-to-bottom: + * buffer[y * width * numComponents * bytesPerPixel + x * numComponents * bytesPerPixel + c * bytesPerPixel] + */ +class SIMPLNX_EXPORT IImageIO +{ +public: + virtual ~IImageIO() = default; + + IImageIO() = default; + IImageIO(const IImageIO&) = delete; + IImageIO(IImageIO&&) noexcept = delete; + IImageIO& operator=(const IImageIO&) = delete; + IImageIO& operator=(IImageIO&&) noexcept = delete; + + /** + * @brief Reads image metadata (dimensions, type, components, origin, spacing, page count) + * without loading pixel data into memory. + * @param filePath Path to the image file + * @return ImageMetadata on success, or error Result with library-provided message + */ + virtual Result readMetadata(const std::filesystem::path& filePath) const = 0; + + /** + * @brief Reads pixel data into a caller-owned byte buffer. + * + * The buffer must be pre-sized to: width * height * numComponents * bytesPerPixel. + * Caller should obtain dimensions from readMetadata() first. + * Data is packed row-major, top-to-bottom. + * + * @param filePath Path to the image file + * @param buffer Pre-allocated output buffer for pixel data + * @return Empty Result on success, or error Result with library-provided message + */ + virtual Result<> readPixelData(const std::filesystem::path& filePath, std::vector& buffer) const = 0; + + /** + * @brief Writes a 2D image from a raw byte buffer. + * + * Buffer layout: row-major, width * height * numComponents * bytesPerPixel. + * The metadata struct provides dimensions, component count, and data type + * so the backend knows how to interpret the buffer. + * + * @param filePath Path to the output image file + * @param buffer Pixel data to write + * @param metadata Image dimensions, type, and component info + * @return Empty Result on success, or error Result with library-provided message + */ + virtual Result<> writePixelData(const std::filesystem::path& filePath, const std::vector& buffer, const ImageMetadata& metadata) const = 0; +}; + +} // namespace nx::core diff --git a/src/simplnx/Utilities/ImageIO/ImageIOFactory.cpp b/src/simplnx/Utilities/ImageIO/ImageIOFactory.cpp new file mode 100644 index 0000000000..821721f404 --- /dev/null +++ b/src/simplnx/Utilities/ImageIO/ImageIOFactory.cpp @@ -0,0 +1,28 @@ +#include "ImageIOFactory.hpp" + +#include "simplnx/Utilities/ImageIO/StbImageIO.hpp" +#include "simplnx/Utilities/ImageIO/TiffImageIO.hpp" + +#include + +#include + +using namespace nx::core; + +Result> nx::core::CreateImageIO(const std::filesystem::path& filePath) +{ + std::string ext = filePath.extension().string(); + std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower); + + if(ext == ".png" || ext == ".jpg" || ext == ".jpeg" || ext == ".bmp") + { + return {std::make_unique()}; + } + + if(ext == ".tif" || ext == ".tiff") + { + return {std::make_unique()}; + } + + return MakeErrorResult>(-20200, fmt::format("Unsupported image format '{}'. Supported: .png, .jpg, .jpeg, .bmp, .tif, .tiff", ext)); +} diff --git a/src/simplnx/Utilities/ImageIO/ImageIOFactory.hpp b/src/simplnx/Utilities/ImageIO/ImageIOFactory.hpp new file mode 100644 index 0000000000..d301f0bf3e --- /dev/null +++ b/src/simplnx/Utilities/ImageIO/ImageIOFactory.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include "simplnx/simplnx_export.hpp" + +#include "simplnx/Common/Result.hpp" +#include "simplnx/Utilities/ImageIO/IImageIO.hpp" + +#include +#include + +namespace nx::core +{ + +/** + * @brief Creates the appropriate IImageIO backend based on file extension. + * + * Supported extensions: + * .png, .jpg, .jpeg, .bmp -> StbImageIO + * .tif, .tiff -> TiffImageIO + * + * @param filePath File path (only extension is examined) + * @return unique_ptr on success, or error Result for unsupported extensions + */ +SIMPLNX_EXPORT Result> CreateImageIO(const std::filesystem::path& filePath); + +} // namespace nx::core diff --git a/src/simplnx/Utilities/ImageIO/ImageMetadata.hpp b/src/simplnx/Utilities/ImageIO/ImageMetadata.hpp new file mode 100644 index 0000000000..c2dad7724b --- /dev/null +++ b/src/simplnx/Utilities/ImageIO/ImageMetadata.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include "simplnx/simplnx_export.hpp" + +#include "simplnx/Common/Array.hpp" +#include "simplnx/Common/Types.hpp" + +#include + +namespace nx::core +{ + +/** + * @struct ImageMetadata + * @brief Holds metadata extracted from an image file without loading pixel data. + * + * The origin and spacing fields are std::optional to distinguish between + * "the file contained this value" and "the file had no such metadata." + * This allows callers to decide whether to use file values or user-provided overrides. + */ +struct SIMPLNX_EXPORT ImageMetadata +{ + usize width = 0; ///< X dimension in pixels + usize height = 0; ///< Y dimension in pixels + usize numComponents = 0; ///< 1=grayscale, 3=RGB, 4=RGBA + DataType dataType = DataType::uint8; ///< Pixel data type (uint8, uint16, or float32) + usize numPages = 1; ///< Number of pages (>1 for multi-page TIFF) + std::optional origin; ///< Image origin if stored in file + std::optional spacing; ///< Image spacing/resolution if stored in file +}; + +} // namespace nx::core diff --git a/src/simplnx/Utilities/ImageIO/StbImageIO.cpp b/src/simplnx/Utilities/ImageIO/StbImageIO.cpp new file mode 100644 index 0000000000..cb27c16c3e --- /dev/null +++ b/src/simplnx/Utilities/ImageIO/StbImageIO.cpp @@ -0,0 +1,195 @@ +#include "StbImageIO.hpp" + +#include "simplnx/Common/Result.hpp" + +#define STB_IMAGE_IMPLEMENTATION +#include + +#define STB_IMAGE_WRITE_IMPLEMENTATION +#include + +#include + +#include +#include + +using namespace nx::core; + +namespace +{ +constexpr int32 k_ErrorInfoFailed = -20000; +constexpr int32 k_ErrorLoadFailed = -20001; +constexpr int32 k_ErrorWriteFailed = -20002; +constexpr int32 k_ErrorUnsupportedWriteFormat = -20003; +constexpr int32 k_ErrorUnsupportedDataType = -20004; +constexpr int32 k_ErrorBufferSizeMismatch = -20005; + +/** + * @brief Returns the number of bytes per element for the given DataType. + */ +usize bytesPerElement(DataType type) +{ + switch(type) + { + case DataType::uint8: + return 1; + case DataType::uint16: + return 2; + case DataType::float32: + return 4; + default: + return 0; + } +} +} // namespace + +// ----------------------------------------------------------------------------- +Result StbImageIO::readMetadata(const std::filesystem::path& filePath) const +{ + std::string pathStr = filePath.string(); + + int width = 0; + int height = 0; + int comp = 0; + int result = stbi_info(pathStr.c_str(), &width, &height, &comp); + if(result == 0) + { + const char* reason = stbi_failure_reason(); + return MakeErrorResult(k_ErrorInfoFailed, fmt::format("Failed to read image info from '{}': {}", pathStr, reason != nullptr ? reason : "unknown error")); + } + + ImageMetadata metadata; + metadata.width = static_cast(width); + metadata.height = static_cast(height); + metadata.numComponents = static_cast(comp); + + if(stbi_is_hdr(pathStr.c_str()) != 0) + { + metadata.dataType = DataType::float32; + } + else if(stbi_is_16_bit(pathStr.c_str()) != 0) + { + metadata.dataType = DataType::uint16; + } + else + { + metadata.dataType = DataType::uint8; + } + + metadata.numPages = 1; + // stb does not provide origin or spacing metadata + metadata.origin = std::nullopt; + metadata.spacing = std::nullopt; + + return {std::move(metadata)}; +} + +// ----------------------------------------------------------------------------- +Result<> StbImageIO::readPixelData(const std::filesystem::path& filePath, std::vector& buffer) const +{ + // First get metadata to determine the data type + Result metaResult = readMetadata(filePath); + if(metaResult.invalid()) + { + return ConvertResult(std::move(metaResult)); + } + const ImageMetadata& metadata = metaResult.value(); + + std::string pathStr = filePath.string(); + int width = 0; + int height = 0; + int comp = 0; + + usize bpe = bytesPerElement(metadata.dataType); + usize expectedSize = metadata.width * metadata.height * metadata.numComponents * bpe; + + if(buffer.size() != expectedSize) + { + return MakeErrorResult(k_ErrorBufferSizeMismatch, + fmt::format("Buffer size {} does not match expected size {} for image '{}'", buffer.size(), expectedSize, pathStr)); + } + + if(metadata.dataType == DataType::float32) + { + float* data = stbi_loadf(pathStr.c_str(), &width, &height, &comp, 0); + if(data == nullptr) + { + const char* reason = stbi_failure_reason(); + return MakeErrorResult(k_ErrorLoadFailed, fmt::format("Failed to load HDR image '{}': {}", pathStr, reason != nullptr ? reason : "unknown error")); + } + std::memcpy(buffer.data(), data, expectedSize); + stbi_image_free(data); + } + else if(metadata.dataType == DataType::uint16) + { + stbi_us* data = stbi_load_16(pathStr.c_str(), &width, &height, &comp, 0); + if(data == nullptr) + { + const char* reason = stbi_failure_reason(); + return MakeErrorResult(k_ErrorLoadFailed, fmt::format("Failed to load 16-bit image '{}': {}", pathStr, reason != nullptr ? reason : "unknown error")); + } + std::memcpy(buffer.data(), data, expectedSize); + stbi_image_free(data); + } + else + { + stbi_uc* data = stbi_load(pathStr.c_str(), &width, &height, &comp, 0); + if(data == nullptr) + { + const char* reason = stbi_failure_reason(); + return MakeErrorResult(k_ErrorLoadFailed, fmt::format("Failed to load image '{}': {}", pathStr, reason != nullptr ? reason : "unknown error")); + } + std::memcpy(buffer.data(), data, expectedSize); + stbi_image_free(data); + } + + return {}; +} + +// ----------------------------------------------------------------------------- +Result<> StbImageIO::writePixelData(const std::filesystem::path& filePath, const std::vector& buffer, const ImageMetadata& metadata) const +{ + if(metadata.dataType != DataType::uint8) + { + return MakeErrorResult(k_ErrorUnsupportedDataType, + fmt::format("stb_image_write only supports uint8 pixel data for writing. Got unsupported data type for '{}'.", filePath.string())); + } + + std::string pathStr = filePath.string(); + std::string ext = filePath.extension().string(); + std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower); + + int w = static_cast(metadata.width); + int h = static_cast(metadata.height); + int comp = static_cast(metadata.numComponents); + const void* data = buffer.data(); + + int result = 0; + + if(ext == ".png") + { + int strideBytes = w * comp; + result = stbi_write_png(pathStr.c_str(), w, h, comp, data, strideBytes); + } + else if(ext == ".bmp") + { + result = stbi_write_bmp(pathStr.c_str(), w, h, comp, data); + } + else if(ext == ".jpg" || ext == ".jpeg") + { + constexpr int k_JpegQuality = 95; + result = stbi_write_jpg(pathStr.c_str(), w, h, comp, data, k_JpegQuality); + } + else + { + return MakeErrorResult(k_ErrorUnsupportedWriteFormat, + fmt::format("Unsupported write format '{}' for stb backend. Supported: .png, .bmp, .jpg, .jpeg", ext)); + } + + if(result == 0) + { + return MakeErrorResult(k_ErrorWriteFailed, fmt::format("Failed to write image to '{}'", pathStr)); + } + + return {}; +} diff --git a/src/simplnx/Utilities/ImageIO/StbImageIO.hpp b/src/simplnx/Utilities/ImageIO/StbImageIO.hpp new file mode 100644 index 0000000000..c1811f48b6 --- /dev/null +++ b/src/simplnx/Utilities/ImageIO/StbImageIO.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include "simplnx/simplnx_export.hpp" + +#include "simplnx/Utilities/ImageIO/IImageIO.hpp" + +namespace nx::core +{ + +/** + * @class StbImageIO + * @brief IImageIO backend using stb_image (read) and stb_image_write (write) + * for PNG, JPEG, and BMP formats. + */ +class SIMPLNX_EXPORT StbImageIO : public IImageIO +{ +public: + StbImageIO() = default; + ~StbImageIO() noexcept override = default; + + Result readMetadata(const std::filesystem::path& filePath) const override; + Result<> readPixelData(const std::filesystem::path& filePath, std::vector& buffer) const override; + Result<> writePixelData(const std::filesystem::path& filePath, const std::vector& buffer, const ImageMetadata& metadata) const override; +}; + +} // namespace nx::core diff --git a/src/simplnx/Utilities/ImageIO/TiffImageIO.cpp b/src/simplnx/Utilities/ImageIO/TiffImageIO.cpp new file mode 100644 index 0000000000..46819af044 --- /dev/null +++ b/src/simplnx/Utilities/ImageIO/TiffImageIO.cpp @@ -0,0 +1,437 @@ +#include "TiffImageIO.hpp" + +#include "simplnx/Common/Result.hpp" + +#include + +#include + +#include + +using namespace nx::core; + +namespace +{ +constexpr int32 k_ErrorOpenFailed = -20100; +constexpr int32 k_ErrorReadMetadataFailed = -20101; +constexpr int32 k_ErrorReadPixelFailed = -20102; +constexpr int32 k_ErrorWriteFailed = -20103; +constexpr int32 k_ErrorUnsupportedFormat = -20104; +constexpr int32 k_ErrorBufferSizeMismatch = -20105; + +// --------------------------------------------------------------------------- +// Thread-local storage for capturing libtiff error/warning messages +// --------------------------------------------------------------------------- +thread_local std::string s_TiffErrorMessage; + +void tiffErrorHandler(const char* /*module*/, const char* formatStr, va_list args) +{ + char buf[1024]; + vsnprintf(buf, sizeof(buf), formatStr, args); + s_TiffErrorMessage = buf; +} + +void tiffWarningHandler(const char* /*module*/, const char* /*formatStr*/, va_list /*args*/) +{ + // Intentionally suppress warnings +} + +/** + * @class TiffErrorGuard + * @brief RAII class to install/restore libtiff error and warning handlers. + */ +class TiffErrorGuard +{ +public: + TiffErrorGuard() + { + s_TiffErrorMessage.clear(); + m_PrevErrorHandler = TIFFSetErrorHandler(tiffErrorHandler); + m_PrevWarningHandler = TIFFSetWarningHandler(tiffWarningHandler); + } + + ~TiffErrorGuard() + { + TIFFSetErrorHandler(m_PrevErrorHandler); + TIFFSetWarningHandler(m_PrevWarningHandler); + } + + TiffErrorGuard(const TiffErrorGuard&) = delete; + TiffErrorGuard& operator=(const TiffErrorGuard&) = delete; + TiffErrorGuard(TiffErrorGuard&&) = delete; + TiffErrorGuard& operator=(TiffErrorGuard&&) = delete; + +private: + TIFFErrorHandler m_PrevErrorHandler = nullptr; + TIFFErrorHandler m_PrevWarningHandler = nullptr; +}; + +/** + * @brief RAII wrapper for TIFF* that calls TIFFClose on destruction. + */ +class TiffHandleGuard +{ +public: + explicit TiffHandleGuard(TIFF* tiff) + : m_Tiff(tiff) + { + } + + ~TiffHandleGuard() + { + if(m_Tiff != nullptr) + { + TIFFClose(m_Tiff); + } + } + + TiffHandleGuard(const TiffHandleGuard&) = delete; + TiffHandleGuard& operator=(const TiffHandleGuard&) = delete; + TiffHandleGuard(TiffHandleGuard&&) = delete; + TiffHandleGuard& operator=(TiffHandleGuard&&) = delete; + + TIFF* get() const + { + return m_Tiff; + } + +private: + TIFF* m_Tiff = nullptr; +}; + +/** + * @brief Returns the number of bytes per element for the given DataType. + */ +usize bytesPerElement(DataType type) +{ + switch(type) + { + case DataType::uint8: + return 1; + case DataType::uint16: + return 2; + case DataType::float32: + return 4; + default: + return 0; + } +} + +/** + * @brief Determines the DataType from TIFF tags. + */ +DataType determineTiffDataType(TIFF* tiff) +{ + uint16 bitsPerSample = 8; + uint16 sampleFormat = SAMPLEFORMAT_UINT; + + TIFFGetFieldDefaulted(tiff, TIFFTAG_BITSPERSAMPLE, &bitsPerSample); + TIFFGetFieldDefaulted(tiff, TIFFTAG_SAMPLEFORMAT, &sampleFormat); + + if(sampleFormat == SAMPLEFORMAT_IEEEFP && bitsPerSample == 32) + { + return DataType::float32; + } + if(bitsPerSample == 16) + { + return DataType::uint16; + } + return DataType::uint8; +} +} // namespace + +// ----------------------------------------------------------------------------- +Result TiffImageIO::readMetadata(const std::filesystem::path& filePath) const +{ + TiffErrorGuard errorGuard; + + std::string pathStr = filePath.string(); + TiffHandleGuard tiffGuard(TIFFOpen(pathStr.c_str(), "r")); + if(tiffGuard.get() == nullptr) + { + return MakeErrorResult(k_ErrorOpenFailed, + fmt::format("Failed to open TIFF file '{}': {}", pathStr, s_TiffErrorMessage.empty() ? "unknown error" : s_TiffErrorMessage)); + } + + TIFF* tiff = tiffGuard.get(); + + uint32 width = 0; + uint32 height = 0; + if(TIFFGetField(tiff, TIFFTAG_IMAGEWIDTH, &width) == 0 || TIFFGetField(tiff, TIFFTAG_IMAGELENGTH, &height) == 0) + { + return MakeErrorResult(k_ErrorReadMetadataFailed, + fmt::format("Failed to read TIFF dimensions from '{}': {}", pathStr, s_TiffErrorMessage.empty() ? "missing width/height tags" : s_TiffErrorMessage)); + } + + uint16 samplesPerPixel = 1; + TIFFGetFieldDefaulted(tiff, TIFFTAG_SAMPLESPERPIXEL, &samplesPerPixel); + + ImageMetadata metadata; + metadata.width = static_cast(width); + metadata.height = static_cast(height); + metadata.numComponents = static_cast(samplesPerPixel); + metadata.dataType = determineTiffDataType(tiff); + + // Count pages (directories) + usize pageCount = 0; + do + { + pageCount++; + } while(TIFFReadDirectory(tiff) != 0); + metadata.numPages = pageCount; + + // Read optional origin (TIFF X/Y position tags) + float xPosition = 0.0f; + float yPosition = 0.0f; + bool hasOrigin = false; + if(TIFFGetField(tiff, TIFFTAG_XPOSITION, &xPosition) != 0) + { + hasOrigin = true; + } + if(TIFFGetField(tiff, TIFFTAG_YPOSITION, &yPosition) != 0) + { + hasOrigin = true; + } + if(hasOrigin) + { + metadata.origin = FloatVec3(xPosition, yPosition, 0.0f); + } + + // Read optional spacing (TIFF resolution tags) + float xRes = 0.0f; + float yRes = 0.0f; + bool hasSpacing = false; + if(TIFFGetField(tiff, TIFFTAG_XRESOLUTION, &xRes) != 0 && xRes > 0.0f) + { + hasSpacing = true; + } + if(TIFFGetField(tiff, TIFFTAG_YRESOLUTION, &yRes) != 0 && yRes > 0.0f) + { + hasSpacing = true; + } + if(hasSpacing) + { + // Resolution is in pixels-per-unit; spacing is the inverse + float32 xSpacing = (xRes > 0.0f) ? (1.0f / xRes) : 1.0f; + float32 ySpacing = (yRes > 0.0f) ? (1.0f / yRes) : 1.0f; + metadata.spacing = FloatVec3(xSpacing, ySpacing, 1.0f); + } + + return {std::move(metadata)}; +} + +// ----------------------------------------------------------------------------- +Result<> TiffImageIO::readPixelData(const std::filesystem::path& filePath, std::vector& buffer) const +{ + TiffErrorGuard errorGuard; + + std::string pathStr = filePath.string(); + TiffHandleGuard tiffGuard(TIFFOpen(pathStr.c_str(), "r")); + if(tiffGuard.get() == nullptr) + { + return MakeErrorResult(k_ErrorOpenFailed, + fmt::format("Failed to open TIFF file '{}': {}", pathStr, s_TiffErrorMessage.empty() ? "unknown error" : s_TiffErrorMessage)); + } + + TIFF* tiff = tiffGuard.get(); + + uint32 width = 0; + uint32 height = 0; + TIFFGetField(tiff, TIFFTAG_IMAGEWIDTH, &width); + TIFFGetField(tiff, TIFFTAG_IMAGELENGTH, &height); + + uint16 samplesPerPixel = 1; + TIFFGetFieldDefaulted(tiff, TIFFTAG_SAMPLESPERPIXEL, &samplesPerPixel); + + DataType dataType = determineTiffDataType(tiff); + usize bpe = bytesPerElement(dataType); + if(bpe == 0) + { + return MakeErrorResult(k_ErrorUnsupportedFormat, + fmt::format("Unsupported TIFF pixel format in '{}': could not determine bytes per element", pathStr)); + } + + usize expectedSize = static_cast(width) * static_cast(height) * static_cast(samplesPerPixel) * bpe; + if(buffer.size() != expectedSize) + { + return MakeErrorResult(k_ErrorBufferSizeMismatch, + fmt::format("Buffer size {} does not match expected size {} for TIFF image '{}'", buffer.size(), expectedSize, pathStr)); + } + + // Check if the image is tiled + if(TIFFIsTiled(tiff) != 0) + { + // For tiled TIFFs, use TIFFReadRGBAImageOriented as a fallback. + // This converts to uint8 RGBA, so it only works well for uint8 images. + if(dataType != DataType::uint8) + { + return MakeErrorResult(k_ErrorUnsupportedFormat, + fmt::format("Tiled TIFF with non-uint8 data is not supported for '{}'. Convert to stripped TIFF first.", pathStr)); + } + + std::vector raster(static_cast(width) * static_cast(height)); + if(TIFFReadRGBAImageOriented(tiff, width, height, raster.data(), ORIENTATION_TOPLEFT, 0) == 0) + { + return MakeErrorResult(k_ErrorReadPixelFailed, + fmt::format("Failed to read tiled TIFF pixel data from '{}': {}", pathStr, s_TiffErrorMessage.empty() ? "unknown error" : s_TiffErrorMessage)); + } + + // TIFFReadRGBAImageOriented produces ABGR uint32 packed pixels. + // Extract the requested number of components. + usize pixelCount = static_cast(width) * static_cast(height); + for(usize i = 0; i < pixelCount; i++) + { + uint32 pixel = raster[i]; + uint8 r = TIFFGetR(pixel); + uint8 g = TIFFGetG(pixel); + uint8 b = TIFFGetB(pixel); + uint8 a = TIFFGetA(pixel); + + usize offset = i * samplesPerPixel; + if(samplesPerPixel >= 1) + { + buffer[offset] = r; + } + if(samplesPerPixel >= 2) + { + buffer[offset + 1] = g; + } + if(samplesPerPixel >= 3) + { + buffer[offset + 2] = b; + } + if(samplesPerPixel >= 4) + { + buffer[offset + 3] = a; + } + } + + return {}; + } + + // Scanline-based reading + tsize_t scanlineSize = TIFFScanlineSize(tiff); + usize rowBytes = static_cast(width) * static_cast(samplesPerPixel) * bpe; + + // Use the larger of TIFFScanlineSize and our computed row size + usize scanlineBufSize = std::max(static_cast(scanlineSize), rowBytes); + std::vector scanlineBuf(scanlineBufSize); + + for(uint32 row = 0; row < height; row++) + { + if(TIFFReadScanline(tiff, scanlineBuf.data(), row) < 0) + { + return MakeErrorResult(k_ErrorReadPixelFailed, + fmt::format("Failed to read scanline {} from TIFF '{}': {}", row, pathStr, s_TiffErrorMessage.empty() ? "unknown error" : s_TiffErrorMessage)); + } + std::memcpy(buffer.data() + (static_cast(row) * rowBytes), scanlineBuf.data(), rowBytes); + } + + return {}; +} + +// ----------------------------------------------------------------------------- +Result<> TiffImageIO::writePixelData(const std::filesystem::path& filePath, const std::vector& buffer, const ImageMetadata& metadata) const +{ + TiffErrorGuard errorGuard; + + usize bpe = bytesPerElement(metadata.dataType); + if(bpe == 0) + { + return MakeErrorResult(k_ErrorUnsupportedFormat, + fmt::format("Unsupported data type for TIFF writing to '{}'. Supported: uint8, uint16, float32.", filePath.string())); + } + + usize expectedSize = metadata.width * metadata.height * metadata.numComponents * bpe; + if(buffer.size() != expectedSize) + { + return MakeErrorResult(k_ErrorBufferSizeMismatch, + fmt::format("Buffer size {} does not match expected size {} for TIFF write to '{}'", buffer.size(), expectedSize, filePath.string())); + } + + std::string pathStr = filePath.string(); + TiffHandleGuard tiffGuard(TIFFOpen(pathStr.c_str(), "w")); + if(tiffGuard.get() == nullptr) + { + return MakeErrorResult(k_ErrorOpenFailed, + fmt::format("Failed to open TIFF file for writing '{}': {}", pathStr, s_TiffErrorMessage.empty() ? "unknown error" : s_TiffErrorMessage)); + } + + TIFF* tiff = tiffGuard.get(); + + uint32 w = static_cast(metadata.width); + uint32 h = static_cast(metadata.height); + uint16 comp = static_cast(metadata.numComponents); + + TIFFSetField(tiff, TIFFTAG_IMAGEWIDTH, w); + TIFFSetField(tiff, TIFFTAG_IMAGELENGTH, h); + TIFFSetField(tiff, TIFFTAG_SAMPLESPERPIXEL, comp); + TIFFSetField(tiff, TIFFTAG_ORIENTATION, ORIENTATION_TOPLEFT); + TIFFSetField(tiff, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG); + TIFFSetField(tiff, TIFFTAG_COMPRESSION, COMPRESSION_LZW); + TIFFSetField(tiff, TIFFTAG_ROWSPERSTRIP, TIFFDefaultStripSize(tiff, 0)); + + // Set bits per sample and sample format based on data type + switch(metadata.dataType) + { + case DataType::uint8: + TIFFSetField(tiff, TIFFTAG_BITSPERSAMPLE, 8); + TIFFSetField(tiff, TIFFTAG_SAMPLEFORMAT, SAMPLEFORMAT_UINT); + break; + case DataType::uint16: + TIFFSetField(tiff, TIFFTAG_BITSPERSAMPLE, 16); + TIFFSetField(tiff, TIFFTAG_SAMPLEFORMAT, SAMPLEFORMAT_UINT); + break; + case DataType::float32: + TIFFSetField(tiff, TIFFTAG_BITSPERSAMPLE, 32); + TIFFSetField(tiff, TIFFTAG_SAMPLEFORMAT, SAMPLEFORMAT_IEEEFP); + break; + default: + return MakeErrorResult(k_ErrorUnsupportedFormat, + fmt::format("Unsupported data type for TIFF writing to '{}'. Supported: uint8, uint16, float32.", pathStr)); + } + + // Set photometric interpretation + if(comp == 1) + { + TIFFSetField(tiff, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_MINISBLACK); + } + else + { + TIFFSetField(tiff, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_RGB); + } + + // Write optional origin + if(metadata.origin.has_value()) + { + const FloatVec3& origin = metadata.origin.value(); + TIFFSetField(tiff, TIFFTAG_XPOSITION, origin[0]); + TIFFSetField(tiff, TIFFTAG_YPOSITION, origin[1]); + } + + // Write optional spacing as resolution + if(metadata.spacing.has_value()) + { + const FloatVec3& spacing = metadata.spacing.value(); + float32 xRes = (spacing[0] > 0.0f) ? (1.0f / spacing[0]) : 1.0f; + float32 yRes = (spacing[1] > 0.0f) ? (1.0f / spacing[1]) : 1.0f; + TIFFSetField(tiff, TIFFTAG_XRESOLUTION, xRes); + TIFFSetField(tiff, TIFFTAG_YRESOLUTION, yRes); + TIFFSetField(tiff, TIFFTAG_RESOLUTIONUNIT, RESUNIT_NONE); + } + + // Write scanlines + usize rowBytes = static_cast(w) * static_cast(comp) * bpe; + for(uint32 row = 0; row < h; row++) + { + const uint8* rowData = buffer.data() + (static_cast(row) * rowBytes); + // TIFFWriteScanline takes a non-const void* but does not modify the data + if(TIFFWriteScanline(tiff, const_cast(rowData), row) < 0) + { + return MakeErrorResult(k_ErrorWriteFailed, + fmt::format("Failed to write scanline {} to TIFF '{}': {}", row, pathStr, s_TiffErrorMessage.empty() ? "unknown error" : s_TiffErrorMessage)); + } + } + + return {}; +} diff --git a/src/simplnx/Utilities/ImageIO/TiffImageIO.hpp b/src/simplnx/Utilities/ImageIO/TiffImageIO.hpp new file mode 100644 index 0000000000..5c1bc031c3 --- /dev/null +++ b/src/simplnx/Utilities/ImageIO/TiffImageIO.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include "simplnx/simplnx_export.hpp" + +#include "simplnx/Utilities/ImageIO/IImageIO.hpp" + +namespace nx::core +{ + +/** + * @class TiffImageIO + * @brief IImageIO backend using libtiff for TIFF format support. + * + * Supports uint8, uint16, and float32 pixel types. + * Reads/writes scanline-by-scanline. + * Captures libtiff error messages via a thread-local error handler. + */ +class SIMPLNX_EXPORT TiffImageIO : public IImageIO +{ +public: + TiffImageIO() = default; + ~TiffImageIO() noexcept override = default; + + Result readMetadata(const std::filesystem::path& filePath) const override; + Result<> readPixelData(const std::filesystem::path& filePath, std::vector& buffer) const override; + Result<> writePixelData(const std::filesystem::path& filePath, const std::vector& buffer, const ImageMetadata& metadata) const override; +}; + +} // namespace nx::core From a5e20a831456f8dab706b63012ffa85c6b7614dc Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Thu, 9 Apr 2026 23:54:01 -0400 Subject: [PATCH 04/16] Add ImageIO files to CMake, fix libtiff type warnings - Add IImageIO, ImageMetadata, ImageIOFactory, StbImageIO, TiffImageIO to SIMPLNX_HDRS and SIMPLNX_SRCS in root CMakeLists.txt - Move STB_IMAGE_IMPLEMENTATION from ReadImageFilter.cpp to StbImageIO.cpp - Use uint16_t/uint32_t/int32_t for TIFF-interacting variables to avoid collision with libtiff's deprecated typedef names Co-Authored-By: Claude Opus 4.6 (1M context) --- CMakeLists.txt | 19 ++++++++- .../SimplnxCore/Filters/ReadImageFilter.cpp | 8 ++-- src/simplnx/Utilities/ImageIO/TiffImageIO.cpp | 42 +++++++++---------- 3 files changed, 43 insertions(+), 26 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8841931bfe..c2696a9753 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -189,6 +189,12 @@ find_package(boost_mp11 CONFIG REQUIRED) find_package(nod CONFIG REQUIRED) find_package(spdlog CONFIG REQUIRED) +#------------------------------------------------------------------------------ +# Find the Image IO libraries +#------------------------------------------------------------------------------ +find_package(Stb REQUIRED) +find_package(TIFF REQUIRED) + # ----------------------------------------------------------------------- # Find HDF5 and get the path to the DLL libraries and put that into a # global property for later install, debugging and packaging @@ -266,6 +272,7 @@ target_link_libraries(simplnx Boost::mp11 nod::nod spdlog::spdlog + TIFF::TIFF ) if(UNIX) @@ -573,7 +580,13 @@ set(SIMPLNX_HDRS ${SIMPLNX_SOURCE_DIR}/Utilities/MontageUtilities.hpp ${SIMPLNX_SOURCE_DIR}/Utilities/SIMPLConversion.hpp ${SIMPLNX_SOURCE_DIR}/Utilities/IntersectionUtilities.hpp - ${SIMPLNX_SOURCE_DIR}/Utilities/NeighborUtilities.hpp + ${SIMPLNX_SOURCE_DIR}/Utilities/NeighborUtilities.hpp + + ${SIMPLNX_SOURCE_DIR}/Utilities/ImageIO/IImageIO.hpp + ${SIMPLNX_SOURCE_DIR}/Utilities/ImageIO/ImageMetadata.hpp + ${SIMPLNX_SOURCE_DIR}/Utilities/ImageIO/ImageIOFactory.hpp + ${SIMPLNX_SOURCE_DIR}/Utilities/ImageIO/StbImageIO.hpp + ${SIMPLNX_SOURCE_DIR}/Utilities/ImageIO/TiffImageIO.hpp ${SIMPLNX_SOURCE_DIR}/Utilities/Math/GeometryMath.hpp # ${SIMPLNX_SOURCE_DIR}/Utilities/Math/MatrixMath.hpp @@ -791,6 +804,10 @@ set(SIMPLNX_SRCS ${SIMPLNX_SOURCE_DIR}/Utilities/Parsing/Text/CsvParser.cpp ${SIMPLNX_SOURCE_DIR}/Utilities/MD5.cpp + + ${SIMPLNX_SOURCE_DIR}/Utilities/ImageIO/ImageIOFactory.cpp + ${SIMPLNX_SOURCE_DIR}/Utilities/ImageIO/StbImageIO.cpp + ${SIMPLNX_SOURCE_DIR}/Utilities/ImageIO/TiffImageIO.cpp ) # Add Core FilterParameters diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ReadImageFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ReadImageFilter.cpp index 7ab68dcd22..50c3a055ef 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ReadImageFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ReadImageFilter.cpp @@ -20,8 +20,7 @@ #include "simplnx/Utilities/SIMPLConversion.hpp" -#define STB_IMAGE_IMPLEMENTATION -#include +#include "simplnx/Utilities/ImageIO/ImageIOFactory.hpp" #include #include @@ -160,8 +159,9 @@ IFilter::PreflightResult ReadImageFilter::preflightImpl(const DataStructure& dat std::string fileNameString = fileName.string(); - int col = 0, row = 0, comp = 0; - int result = stbi_info(fileNameString.c_str(), &col, &row, &comp); + // TODO: Replace with IImageIO-based metadata read in full implementation + // int col = 0, row = 0, comp = 0; + // int result = stbi_info(fileNameString.c_str(), &col, &row, &comp); // Declare the preflightResult variable that will be populated with the results diff --git a/src/simplnx/Utilities/ImageIO/TiffImageIO.cpp b/src/simplnx/Utilities/ImageIO/TiffImageIO.cpp index 46819af044..ac0f8001b0 100644 --- a/src/simplnx/Utilities/ImageIO/TiffImageIO.cpp +++ b/src/simplnx/Utilities/ImageIO/TiffImageIO.cpp @@ -12,12 +12,12 @@ using namespace nx::core; namespace { -constexpr int32 k_ErrorOpenFailed = -20100; -constexpr int32 k_ErrorReadMetadataFailed = -20101; -constexpr int32 k_ErrorReadPixelFailed = -20102; -constexpr int32 k_ErrorWriteFailed = -20103; -constexpr int32 k_ErrorUnsupportedFormat = -20104; -constexpr int32 k_ErrorBufferSizeMismatch = -20105; +constexpr int32_t k_ErrorOpenFailed = -20100; +constexpr int32_t k_ErrorReadMetadataFailed = -20101; +constexpr int32_t k_ErrorReadPixelFailed = -20102; +constexpr int32_t k_ErrorWriteFailed = -20103; +constexpr int32_t k_ErrorUnsupportedFormat = -20104; +constexpr int32_t k_ErrorBufferSizeMismatch = -20105; // --------------------------------------------------------------------------- // Thread-local storage for capturing libtiff error/warning messages @@ -122,8 +122,8 @@ usize bytesPerElement(DataType type) */ DataType determineTiffDataType(TIFF* tiff) { - uint16 bitsPerSample = 8; - uint16 sampleFormat = SAMPLEFORMAT_UINT; + uint16_t bitsPerSample = 8; + uint16_t sampleFormat = SAMPLEFORMAT_UINT; TIFFGetFieldDefaulted(tiff, TIFFTAG_BITSPERSAMPLE, &bitsPerSample); TIFFGetFieldDefaulted(tiff, TIFFTAG_SAMPLEFORMAT, &sampleFormat); @@ -155,15 +155,15 @@ Result TiffImageIO::readMetadata(const std::filesystem::path& fil TIFF* tiff = tiffGuard.get(); - uint32 width = 0; - uint32 height = 0; + uint32_t width = 0; + uint32_t height = 0; if(TIFFGetField(tiff, TIFFTAG_IMAGEWIDTH, &width) == 0 || TIFFGetField(tiff, TIFFTAG_IMAGELENGTH, &height) == 0) { return MakeErrorResult(k_ErrorReadMetadataFailed, fmt::format("Failed to read TIFF dimensions from '{}': {}", pathStr, s_TiffErrorMessage.empty() ? "missing width/height tags" : s_TiffErrorMessage)); } - uint16 samplesPerPixel = 1; + uint16_t samplesPerPixel = 1; TIFFGetFieldDefaulted(tiff, TIFFTAG_SAMPLESPERPIXEL, &samplesPerPixel); ImageMetadata metadata; @@ -235,12 +235,12 @@ Result<> TiffImageIO::readPixelData(const std::filesystem::path& filePath, std:: TIFF* tiff = tiffGuard.get(); - uint32 width = 0; - uint32 height = 0; + uint32_t width = 0; + uint32_t height = 0; TIFFGetField(tiff, TIFFTAG_IMAGEWIDTH, &width); TIFFGetField(tiff, TIFFTAG_IMAGELENGTH, &height); - uint16 samplesPerPixel = 1; + uint16_t samplesPerPixel = 1; TIFFGetFieldDefaulted(tiff, TIFFTAG_SAMPLESPERPIXEL, &samplesPerPixel); DataType dataType = determineTiffDataType(tiff); @@ -269,7 +269,7 @@ Result<> TiffImageIO::readPixelData(const std::filesystem::path& filePath, std:: fmt::format("Tiled TIFF with non-uint8 data is not supported for '{}'. Convert to stripped TIFF first.", pathStr)); } - std::vector raster(static_cast(width) * static_cast(height)); + std::vector raster(static_cast(width) * static_cast(height)); if(TIFFReadRGBAImageOriented(tiff, width, height, raster.data(), ORIENTATION_TOPLEFT, 0) == 0) { return MakeErrorResult(k_ErrorReadPixelFailed, @@ -281,7 +281,7 @@ Result<> TiffImageIO::readPixelData(const std::filesystem::path& filePath, std:: usize pixelCount = static_cast(width) * static_cast(height); for(usize i = 0; i < pixelCount; i++) { - uint32 pixel = raster[i]; + uint32_t pixel = raster[i]; uint8 r = TIFFGetR(pixel); uint8 g = TIFFGetG(pixel); uint8 b = TIFFGetB(pixel); @@ -317,7 +317,7 @@ Result<> TiffImageIO::readPixelData(const std::filesystem::path& filePath, std:: usize scanlineBufSize = std::max(static_cast(scanlineSize), rowBytes); std::vector scanlineBuf(scanlineBufSize); - for(uint32 row = 0; row < height; row++) + for(uint32_t row = 0; row < height; row++) { if(TIFFReadScanline(tiff, scanlineBuf.data(), row) < 0) { @@ -359,9 +359,9 @@ Result<> TiffImageIO::writePixelData(const std::filesystem::path& filePath, cons TIFF* tiff = tiffGuard.get(); - uint32 w = static_cast(metadata.width); - uint32 h = static_cast(metadata.height); - uint16 comp = static_cast(metadata.numComponents); + uint32_t w = static_cast(metadata.width); + uint32_t h = static_cast(metadata.height); + uint16_t comp = static_cast(metadata.numComponents); TIFFSetField(tiff, TIFFTAG_IMAGEWIDTH, w); TIFFSetField(tiff, TIFFTAG_IMAGELENGTH, h); @@ -422,7 +422,7 @@ Result<> TiffImageIO::writePixelData(const std::filesystem::path& filePath, cons // Write scanlines usize rowBytes = static_cast(w) * static_cast(comp) * bpe; - for(uint32 row = 0; row < h; row++) + for(uint32_t row = 0; row < h; row++) { const uint8* rowData = buffer.data() + (static_cast(row) * rowBytes); // TIFFWriteScanline takes a non-const void* but does not modify the data From d58058998799de31b151357d99ea331204c82332 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Fri, 10 Apr 2026 00:00:43 -0400 Subject: [PATCH 05/16] Implement ReadImage algorithm and ReadImageFilter ReadImage algorithm reads single 2D images via IImageIO into DataArrays. ReadImageFilter provides preflight with geometry/array creation and origin/spacing override support. Data type conversion with ratio scaling. Removes direct Stb/TIFF dependencies from SimplnxCore CMakeLists since those are now handled by the ImageIO abstraction layer. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/Plugins/SimplnxCore/CMakeLists.txt | 8 +- .../Filters/Algorithms/ReadImage.cpp | 159 +++++++++++-- .../Filters/Algorithms/ReadImage.hpp | 30 ++- .../SimplnxCore/Filters/ReadImageFilter.cpp | 212 +++++++++++++----- 4 files changed, 313 insertions(+), 96 deletions(-) diff --git a/src/Plugins/SimplnxCore/CMakeLists.txt b/src/Plugins/SimplnxCore/CMakeLists.txt index 49ac873858..917e3688b7 100644 --- a/src/Plugins/SimplnxCore/CMakeLists.txt +++ b/src/Plugins/SimplnxCore/CMakeLists.txt @@ -363,16 +363,10 @@ file(TO_CMAKE_PATH "${reproc_dll_path}" reproc_dll_path) get_property(SIMPLNX_EXTRA_LIBRARY_DIRS GLOBAL PROPERTY SIMPLNX_EXTRA_LIBRARY_DIRS) set_property(GLOBAL PROPERTY SIMPLNX_EXTRA_LIBRARY_DIRS ${SIMPLNX_EXTRA_LIBRARY_DIRS} ${reproc_dll_path}) -#------------------------------------------------------------------------------ -# Find the cImg package to read/write images -#------------------------------------------------------------------------------ -find_package(Stb REQUIRED) -find_package(TIFF REQUIRED) - #------------------------------------------------------------------------------ # If there are additional libraries that this plugin needs to link against you # can use the target_link_libraries() cmake call -target_link_libraries(${PLUGIN_NAME} PRIVATE reproc++ TIFF::TIFF) +target_link_libraries(${PLUGIN_NAME} PRIVATE reproc++) #------------------------------------------------------------------------------ # If there are additional source files that need to be compiled for this plugin diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ReadImage.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ReadImage.cpp index 22532f0dd4..6d2136ff7c 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ReadImage.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ReadImage.cpp @@ -2,9 +2,83 @@ #include "simplnx/DataStructure/DataArray.hpp" #include "simplnx/DataStructure/DataGroup.hpp" +#include "simplnx/DataStructure/Geometry/ImageGeom.hpp" +#include "simplnx/Utilities/FilterUtilities.hpp" +#include "simplnx/Utilities/ImageIO/IImageIO.hpp" +#include "simplnx/Utilities/ImageIO/ImageIOFactory.hpp" + +#include + +#include +#include using namespace nx::core; +namespace +{ +usize BytesPerComponent(DataType dt) +{ + switch(dt) + { + case DataType::uint8: + return 1; + case DataType::uint16: + return 2; + case DataType::uint32: + return 4; + case DataType::float32: + return 4; + default: + return 0; + } +} + +struct CopyPixelDataFunctor +{ + template + Result<> operator()(IDataArray& dataArray, const std::vector& buffer, usize totalElements) + { + auto& dataStore = dataArray.template getIDataStoreRefAs>(); + const T* typedBuffer = reinterpret_cast(buffer.data()); + for(usize i = 0; i < totalElements; i++) + { + dataStore[i] = typedBuffer[i]; + } + return {}; + } +}; + +template +struct ConvertPixelDataFunctor +{ + template + Result<> operator()(IDataArray& dataArray, const std::vector& buffer, usize totalElements) + { + auto& dataStore = dataArray.template getIDataStoreRefAs>(); + const SrcT* typedBuffer = reinterpret_cast(buffer.data()); + + constexpr double srcMax = static_cast(std::numeric_limits::max()); + constexpr double destMax = static_cast(std::numeric_limits::max()); + + for(usize i = 0; i < totalElements; i++) + { + double normalized = static_cast(typedBuffer[i]) / srcMax; + dataStore[i] = static_cast(normalized * destMax); + } + return {}; + } +}; + +struct DispatchConversionFunctor +{ + template + Result<> operator()(DataType destType, IDataArray& dataArray, const std::vector& buffer, usize totalElements) + { + return ExecuteDataFunction(ConvertPixelDataFunctor{}, destType, dataArray, buffer, totalElements); + } +}; +} // namespace + // ----------------------------------------------------------------------------- ReadImage::ReadImage(DataStructure& dataStructure, const IFilter::MessageHandler& mesgHandler, const std::atomic_bool& shouldCancel, ReadImageInputValues* inputValues) : m_DataStructure(dataStructure) @@ -20,23 +94,74 @@ ReadImage::~ReadImage() noexcept = default; // ----------------------------------------------------------------------------- Result<> ReadImage::operator()() { - /** - * This section of the code should contain the actual algorithmic codes that - * will accomplish the goal of the file. - * - * If you can parallelize the code there are a number of examples on how to do that. - * GenerateIPFColors is one example - * - * If you need to determine what kind of array you have (Int32Array, Float32Array, etc) - * look to the ExecuteDataFunction() in simplnx/Utilities/FilterUtilities.hpp template - * function to help with that code. - * An Example algorithm class is `CombineAttributeArrays` and `RemoveFlaggedVertices` - * - * There are other utility classes that can help alleviate the amount of code that needs - * to be written. - * - * REMOVE THIS COMMENT BLOCK WHEN YOU ARE FINISHED WITH THE FILTER_HUMAN_NAME - */ + const auto& inputFilePath = m_InputValues->inputFilePath; + + m_MessageHandler(IFilter::Message::Type::Info, fmt::format("Reading image file: {}", inputFilePath.string())); + + // Create the appropriate image IO backend + auto backendResult = CreateImageIO(inputFilePath); + if(backendResult.invalid()) + { + return ConvertResult(std::move(backendResult)); + } + auto& backend = backendResult.value(); + + // Read metadata + auto metadataResult = backend->readMetadata(inputFilePath); + if(metadataResult.invalid()) + { + return ConvertResult(std::move(metadataResult)); + } + const auto& metadata = metadataResult.value(); + + // Compute buffer size + usize bytesPerComp = BytesPerComponent(metadata.dataType); + if(bytesPerComp == 0) + { + return MakeErrorResult(-2000, fmt::format("Unsupported source data type in image file: {}", inputFilePath.string())); + } + usize bufferSize = metadata.width * metadata.height * metadata.numComponents * bytesPerComp; + + // Read pixel data + std::vector tempBuffer(bufferSize); + auto readResult = backend->readPixelData(inputFilePath, tempBuffer); + if(readResult.invalid()) + { + return readResult; + } + + if(m_ShouldCancel) + { + return {}; + } + + // Get the target DataArray + auto& dataArray = m_DataStructure.getDataRefAs(m_InputValues->imageDataArrayPath); + + usize totalElements = metadata.width * metadata.height * metadata.numComponents; + + // Determine if type conversion is needed + DataType destType = dataArray.getDataType(); + DataType srcType = metadata.dataType; + + if(m_InputValues->changeDataType && srcType != destType) + { + m_MessageHandler(IFilter::Message::Type::Info, fmt::format("Converting pixel data from {} to {}", DataTypeToString(srcType), DataTypeToString(destType))); + auto convResult = ExecuteDataFunction(DispatchConversionFunctor{}, srcType, destType, dataArray, tempBuffer, totalElements); + if(convResult.invalid()) + { + return convResult; + } + } + else + { + // Direct copy - source and dest types match + auto copyResult = ExecuteDataFunction(CopyPixelDataFunctor{}, srcType, dataArray, tempBuffer, totalElements); + if(copyResult.invalid()) + { + return copyResult; + } + } return {}; } diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ReadImage.hpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ReadImage.hpp index bad730e37d..9928299c1f 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ReadImage.hpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ReadImage.hpp @@ -2,31 +2,39 @@ #include "SimplnxCore/SimplnxCore_export.hpp" +#include "simplnx/Common/Array.hpp" +#include "simplnx/Common/Types.hpp" #include "simplnx/DataStructure/DataPath.hpp" #include "simplnx/DataStructure/DataStructure.hpp" #include "simplnx/Filter/IFilter.hpp" +#include "simplnx/Parameters/ChoicesParameter.hpp" -//TODO: PARAMETER_INCLUDES - -/** -* This is example code to put in the Execute Method of the filter. -@EXECUTE_EXAMPLE_CODE@ -*/ +#include namespace nx::core { struct SIMPLNXCORE_EXPORT ReadImageInputValues { -//TODO: INPUT_VALUE_STRUCT_DEF - + std::filesystem::path inputFilePath; + DataPath imageGeometryPath; + DataPath imageDataArrayPath; + std::string cellDataName; + bool changeOrigin = false; + bool centerOrigin = false; + FloatVec3 origin; + bool changeSpacing = false; + FloatVec3 spacing; + usize originSpacingProcessing = 1; // 0=Preprocessed, 1=Postprocessed + bool changeDataType = false; + DataType imageDataType = DataType::uint8; }; /** * @class ReadImage - * @brief This algorithm implements support code for the ReadImageFilter + * @brief This algorithm reads a single 2D image file into a pre-allocated DataArray + * using the IImageIO abstraction layer. */ - class SIMPLNXCORE_EXPORT ReadImage { public: @@ -47,4 +55,4 @@ class SIMPLNXCORE_EXPORT ReadImage const IFilter::MessageHandler& m_MessageHandler; }; -} // namespace complex +} // namespace nx::core diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ReadImageFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ReadImageFilter.cpp index 50c3a055ef..fa27655162 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ReadImageFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ReadImageFilter.cpp @@ -2,14 +2,17 @@ #include "SimplnxCore/Filters/Algorithms/ReadImage.hpp" +#include "simplnx/Common/TypesUtility.hpp" #include "simplnx/Core/Application.hpp" #include "simplnx/DataStructure/DataPath.hpp" #include "simplnx/DataStructure/DataStore.hpp" #include "simplnx/DataStructure/Geometry/ImageGeom.hpp" #include "simplnx/Filter/Actions/CreateArrayAction.hpp" +#include "simplnx/Filter/Actions/CreateImageGeometryAction.hpp" #include "simplnx/Filter/Actions/UpdateImageGeomAction.hpp" #include "simplnx/Filter/FilterHandle.hpp" #include "simplnx/Parameters/BoolParameter.hpp" +#include "simplnx/Parameters/ChoicesParameter.hpp" #include "simplnx/Parameters/CropGeometryParameter.hpp" #include "simplnx/Parameters/DataGroupCreationParameter.hpp" #include "simplnx/Parameters/DataObjectNameParameter.hpp" @@ -19,9 +22,11 @@ #include "simplnx/Parameters/VectorParameter.hpp" #include "simplnx/Utilities/SIMPLConversion.hpp" - +#include "simplnx/Utilities/ImageIO/IImageIO.hpp" #include "simplnx/Utilities/ImageIO/ImageIOFactory.hpp" +#include + #include #include @@ -35,11 +40,21 @@ const Uuid k_SimplnxCorePluginId = *Uuid::FromString("05cc618b-781f-4ac0-b9ac-43 const Uuid k_CropImageGeomFilterId = *Uuid::FromString("e6476737-4aa7-48ba-a702-3dfab82c96e2"); const FilterHandle k_CropImageGeomFilterHandle(k_CropImageGeomFilterId, k_SimplnxCorePluginId); - +DataType ConvertChoiceToDataType(usize choice) +{ + switch(choice) + { + case 0: + return DataType::uint8; + case 1: + return DataType::uint16; + case 2: + return DataType::uint32; + } + return DataType::uint8; +} } // namespace - - namespace nx::core { //------------------------------------------------------------------------------ @@ -80,7 +95,7 @@ Parameters ReadImageFilter::parameters() const params.insertSeparator(Parameters::Separator{"Input Parameter(s)"}); params.insert(std::make_unique(k_FileName_Key, "File", "Input image file", fs::path(""), - FileSystemPathParameter::ExtensionsType{{".png"}, {".tiff"}, {".tif"}, {".bmp"}, {".jpeg"}, {".jpg"}, {".nrrd"}, {".mha"}}, + FileSystemPathParameter::ExtensionsType{{".png"}, {".tiff"}, {".tif"}, {".bmp"}, {".jpeg"}, {".jpg"}}, FileSystemPathParameter::PathType::InputFile, false)); params.insert(std::make_unique(k_LengthUnit_Key, "Length Unit", "The length unit that will be set into the created image geometry", @@ -141,7 +156,7 @@ IFilter::UniquePointer ReadImageFilter::clone() const //------------------------------------------------------------------------------ IFilter::PreflightResult ReadImageFilter::preflightImpl(const DataStructure& dataStructure, const Arguments& filterArgs, const MessageHandler& messageHandler, - const std::atomic_bool& shouldCancel, const ExecutionContext& executionContext) const + const std::atomic_bool& shouldCancel, const ExecutionContext& executionContext) const { auto fileName = filterArgs.value(k_FileName_Key); auto imageGeomPath = filterArgs.value(k_ImageGeometryPath_Key); @@ -150,72 +165,151 @@ IFilter::PreflightResult ReadImageFilter::preflightImpl(const DataStructure& dat auto shouldChangeOrigin = filterArgs.value(k_ChangeOrigin_Key); auto shouldCenterOrigin = filterArgs.value(k_CenterOrigin_Key); auto shouldChangeSpacing = filterArgs.value(k_ChangeSpacing_Key); - auto origin = filterArgs.value(k_Origin_Key); - auto spacing = filterArgs.value(k_Spacing_Key); - // auto originSpacingProcessing = static_cast(filterArgs.value(k_OriginSpacingProcessing_Key)); + auto originValues = filterArgs.value(k_Origin_Key); + auto spacingValues = filterArgs.value(k_Spacing_Key); + auto originSpacingProcessing = filterArgs.value(k_OriginSpacingProcessing_Key); auto pChangeDataType = filterArgs.value(k_ChangeDataType_Key); auto pChoiceType = filterArgs.value(k_ImageDataType_Key); + auto lengthUnitIndex = filterArgs.value(k_LengthUnit_Key); auto croppingOptions = filterArgs.value(k_CroppingOptions_Key); - std::string fileNameString = fileName.string(); - - // TODO: Replace with IImageIO-based metadata read in full implementation - // int col = 0, row = 0, comp = 0; - // int result = stbi_info(fileNameString.c_str(), &col, &row, &comp); - - - // Declare the preflightResult variable that will be populated with the results - // of the preflight. The PreflightResult type contains the output Actions and - // any preflight updated values that you want to be displayed to the user, typically - // through a user interface (UI). - PreflightResult preflightResult; - - // If your filter is making structural changes to the DataStructure then the filter - // is going to create OutputActions subclasses that need to be returned. This will - // store those actions. nx::core::Result resultOutputActions; - - // If your filter is going to pass back some `preflight updated values` then this is where you - // would create the code to store those values in the appropriate object. Note that we - // in line creating the pair (NOT a std::pair<>) of Key:Value that will get stored in - // the std::vector object. std::vector preflightUpdatedValues; - // If the filter needs to pass back some updated values via a key:value string:string set of values - // you can declare and update that string here. - - //TODO: PREFLIGHT_UPDATED_DEFS - // If this filter makes changes to the DataStructure in the form of - // creating/deleting/moving/renaming DataGroups, Geometries, DataArrays then you - // will need to use one of the `*Actions` classes located in complex/Filter/Actions - // to relay that information to the preflight and execute methods. This is done by - // creating an instance of the Action class and then storing it in the resultOutputActions variable. - // This is done through a `push_back()` method combined with a `std::move()`. For the - // newly initiated to `std::move` once that code is executed what was once inside the Action class - // instance variable is *no longer there*. The memory has been moved. If you try to access that - // variable after this line you will probably get a crash or have subtle bugs. To ensure that this - // does not happen we suggest using braces `{}` to scope each of the action's declaration and store - // so that the programmer is not tempted to use the action instance past where it should be used. - // You have to create your own Actions class if there isn't something specific for your filter's needs - - //TODO: PROPOSED_ACTIONS - // Store the preflight updated value(s) into the preflightUpdatedValues vector using - // the appropriate methods. - - //TODO: PREFLIGHT_UPDATED_VALUES - // Return both the resultOutputActions and the preflightUpdatedValues via std::move() + // Validate the file extension by creating an IO backend + auto backendResult = CreateImageIO(fileName); + if(backendResult.invalid()) + { + return {ConvertResultTo(ConvertResult(std::move(backendResult)), {})}; + } + auto& backend = backendResult.value(); + + // Read metadata from the image file + auto metadataResult = backend->readMetadata(fileName); + if(metadataResult.invalid()) + { + return {ConvertResultTo(ConvertResult(std::move(metadataResult)), {})}; + } + const auto& metadata = metadataResult.value(); + + // Set up dims, origin, spacing from metadata + std::vector dims = {metadata.width, metadata.height, 1}; + FloatVec3 origin = metadata.origin.value_or(FloatVec3{0.0f, 0.0f, 0.0f}); + FloatVec3 spacing = metadata.spacing.value_or(FloatVec3{1.0f, 1.0f, 1.0f}); + + // Apply origin/spacing overrides (Preprocessed = before cropping) + if(originSpacingProcessing == 0) // Preprocessed + { + if(shouldChangeSpacing) + { + spacing = FloatVec3{static_cast(spacingValues[0]), static_cast(spacingValues[1]), static_cast(spacingValues[2])}; + } + + if(shouldChangeOrigin) + { + origin = FloatVec3{static_cast(originValues[0]), static_cast(originValues[1]), static_cast(originValues[2])}; + if(shouldCenterOrigin) + { + for(usize i = 0; i < 3; i++) + { + origin[i] = -0.5f * spacing[i] * static_cast(dims[i]); + } + } + } + } + + // TODO: Cropping will be added in a future iteration. For now, cropping options are accepted + // but not applied. The implementation will delegate to CropImageGeometry filter similar to + // the ITK-based ReadImagePreflight. + + // Apply origin/spacing overrides (Postprocessed = after cropping) + if(originSpacingProcessing == 1) // Postprocessed + { + if(shouldChangeSpacing) + { + spacing = FloatVec3{static_cast(spacingValues[0]), static_cast(spacingValues[1]), static_cast(spacingValues[2])}; + } + + if(shouldChangeOrigin) + { + origin = FloatVec3{static_cast(originValues[0]), static_cast(originValues[1]), static_cast(originValues[2])}; + if(shouldCenterOrigin) + { + for(usize i = 0; i < 3; i++) + { + origin[i] = -0.5f * spacing[i] * static_cast(dims[i]); + } + } + } + } + + // Determine the data type for the created array + DataType dataType = metadata.dataType; + if(pChangeDataType) + { + dataType = ConvertChoiceToDataType(pChoiceType); + } + + auto lengthUnit = static_cast(lengthUnitIndex); + + // DataArray dimensions are stored slowest to fastest (Z, Y, X), the opposite of ImageGeometry (X, Y, Z) + std::vector arrayDims(dims.crbegin(), dims.crend()); + + std::vector componentDims = {metadata.numComponents}; + + // Create the ImageGeometry + { + auto originVec = std::vector{origin[0], origin[1], origin[2]}; + auto spacingVec = std::vector{spacing[0], spacing[1], spacing[2]}; + resultOutputActions.value().appendAction( + std::make_unique(imageGeomPath, dims, originVec, spacingVec, cellDataName, lengthUnit)); + } + + // Create the data array + { + DataPath imageDataArrayPath = imageGeomPath.createChildPath(cellDataName).createChildPath(imageDataArrayName); + resultOutputActions.value().appendAction(std::make_unique(dataType, arrayDims, componentDims, imageDataArrayPath)); + } + + // Build a summary string for preflight updated values + std::string summary = fmt::format("Image Dimensions: {} x {}\n" + "Pixel Components: {}\n" + "Source Data Type: {}\n" + "Output Data Type: {}\n" + "Origin: [{}, {}, {}]\n" + "Spacing: [{}, {}, {}]", + metadata.width, metadata.height, metadata.numComponents, DataTypeToString(metadata.dataType), DataTypeToString(dataType), origin[0], origin[1], origin[2], + spacing[0], spacing[1], spacing[2]); + + preflightUpdatedValues.push_back({"Image Information", summary}); + return {std::move(resultOutputActions), std::move(preflightUpdatedValues)}; } //------------------------------------------------------------------------------ Result<> ReadImageFilter::executeImpl(DataStructure& dataStructure, const Arguments& filterArgs, const PipelineFilter* pipelineNode, const MessageHandler& messageHandler, - const std::atomic_bool& shouldCancel, const ExecutionContext& executionContext) const + const std::atomic_bool& shouldCancel, const ExecutionContext& executionContext) const { - ReadImageInputValues inputValues; - - //TODO: INPUT_VALUES_DEF + auto imageGeomPath = filterArgs.value(k_ImageGeometryPath_Key); + auto cellDataName = filterArgs.value(k_CellDataName_Key); + auto imageDataArrayName = filterArgs.value(k_ImageDataArrayPath_Key); + + inputValues.inputFilePath = filterArgs.value(k_FileName_Key); + inputValues.imageGeometryPath = imageGeomPath; + inputValues.imageDataArrayPath = imageGeomPath.createChildPath(cellDataName).createChildPath(imageDataArrayName); + inputValues.cellDataName = cellDataName; + inputValues.changeOrigin = filterArgs.value(k_ChangeOrigin_Key); + inputValues.centerOrigin = filterArgs.value(k_CenterOrigin_Key); + auto originValues = filterArgs.value(k_Origin_Key); + inputValues.origin = FloatVec3{static_cast(originValues[0]), static_cast(originValues[1]), static_cast(originValues[2])}; + inputValues.changeSpacing = filterArgs.value(k_ChangeSpacing_Key); + auto spacingValues = filterArgs.value(k_Spacing_Key); + inputValues.spacing = FloatVec3{static_cast(spacingValues[0]), static_cast(spacingValues[1]), static_cast(spacingValues[2])}; + inputValues.originSpacingProcessing = filterArgs.value(k_OriginSpacingProcessing_Key); + inputValues.changeDataType = filterArgs.value(k_ChangeDataType_Key); + inputValues.imageDataType = ConvertChoiceToDataType(filterArgs.value(k_ImageDataType_Key)); return ReadImage(dataStructure, messageHandler, shouldCancel, &inputValues)(); } @@ -224,12 +318,9 @@ namespace { namespace SIMPL { - - //TODO: PARAMETER_JSON_CONSTANTS } // namespace SIMPL } // namespace - //------------------------------------------------------------------------------ Result ReadImageFilter::FromSIMPLJson(const nlohmann::json& json) { @@ -244,5 +335,4 @@ Result ReadImageFilter::FromSIMPLJson(const nlohmann::json& json) return ConvertResultTo(std::move(conversionResult), std::move(args)); } - } // namespace nx::core From 66de15df20ede2f62edb5867275b3979cb7c5964 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Fri, 10 Apr 2026 00:05:28 -0400 Subject: [PATCH 06/16] Implement WriteImage algorithm and WriteImageFilter WriteImage extracts 2D slices along XY/XZ/YZ planes from 3D ImageGeometry and writes individual image files via IImageIO. Uses AtomicFile for safe writes. Temp buffers are one slice only. WriteImageFilter validates parameters and delegates to algorithm. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Filters/Algorithms/WriteImage.cpp | 222 ++++++++++++++++-- .../Filters/Algorithms/WriteImage.hpp | 23 +- .../SimplnxCore/Filters/WriteImageFilter.cpp | 85 +++++-- 3 files changed, 280 insertions(+), 50 deletions(-) diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/WriteImage.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/WriteImage.cpp index 5576d52c4e..92752af972 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/WriteImage.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/WriteImage.cpp @@ -1,10 +1,90 @@ #include "WriteImage.hpp" +#include "simplnx/Common/AtomicFile.hpp" #include "simplnx/DataStructure/DataArray.hpp" -#include "simplnx/DataStructure/DataGroup.hpp" +#include "simplnx/DataStructure/Geometry/ImageGeom.hpp" +#include "simplnx/Utilities/FilterUtilities.hpp" +#include "simplnx/Utilities/ImageIO/IImageIO.hpp" +#include "simplnx/Utilities/ImageIO/ImageIOFactory.hpp" +#include "simplnx/Utilities/ImageIO/ImageMetadata.hpp" + +#include + +#include +#include + +namespace fs = std::filesystem; using namespace nx::core; +namespace +{ +/** + * @brief Functor that extracts a single 2D slice from a typed DataStore + * into a raw byte buffer suitable for IImageIO::writePixelData(). + */ +struct ExtractSliceFunctor +{ + template + Result<> operator()(const IDataArray& dataArray, std::vector& buffer, usize sliceIndex, usize planeIndex, usize dimX, usize dimY, usize dimZ, usize nComp) + { + const auto& dataStore = dataArray.template getIDataStoreRefAs>(); + T* typedBuffer = reinterpret_cast(buffer.data()); + + if(planeIndex == 0) // XY plane — iterate over Z, slice width=X, slice height=Y + { + usize z = sliceIndex; + for(usize y = 0; y < dimY; ++y) + { + for(usize x = 0; x < dimX; ++x) + { + usize srcIndex = (z * dimY * dimX + y * dimX + x) * nComp; + usize dstIndex = (y * dimX + x) * nComp; + for(usize c = 0; c < nComp; ++c) + { + typedBuffer[dstIndex + c] = dataStore.getValue(srcIndex + c); + } + } + } + } + else if(planeIndex == 1) // XZ plane — iterate over Y, slice width=X, slice height=Z + { + usize y = sliceIndex; + for(usize z = 0; z < dimZ; ++z) + { + for(usize x = 0; x < dimX; ++x) + { + usize srcIndex = (z * dimY * dimX + y * dimX + x) * nComp; + usize dstIndex = (z * dimX + x) * nComp; + for(usize c = 0; c < nComp; ++c) + { + typedBuffer[dstIndex + c] = dataStore.getValue(srcIndex + c); + } + } + } + } + else if(planeIndex == 2) // YZ plane — iterate over X, slice width=Y, slice height=Z + { + usize x = sliceIndex; + for(usize z = 0; z < dimZ; ++z) + { + for(usize y = 0; y < dimY; ++y) + { + usize srcIndex = (z * dimY * dimX + y * dimX + x) * nComp; + usize dstIndex = (z * dimY + y) * nComp; + for(usize c = 0; c < nComp; ++c) + { + typedBuffer[dstIndex + c] = dataStore.getValue(srcIndex + c); + } + } + } + } + + return {}; + } +}; +} // namespace + // ----------------------------------------------------------------------------- WriteImage::WriteImage(DataStructure& dataStructure, const IFilter::MessageHandler& mesgHandler, const std::atomic_bool& shouldCancel, WriteImageInputValues* inputValues) : m_DataStructure(dataStructure) @@ -20,23 +100,129 @@ WriteImage::~WriteImage() noexcept = default; // ----------------------------------------------------------------------------- Result<> WriteImage::operator()() { - /** - * This section of the code should contain the actual algorithmic codes that - * will accomplish the goal of the file. - * - * If you can parallelize the code there are a number of examples on how to do that. - * GenerateIPFColors is one example - * - * If you need to determine what kind of array you have (Int32Array, Float32Array, etc) - * look to the ExecuteDataFunction() in simplnx/Utilities/FilterUtilities.hpp template - * function to help with that code. - * An Example algorithm class is `CombineAttributeArrays` and `RemoveFlaggedVertices` - * - * There are other utility classes that can help alleviate the amount of code that needs - * to be written. - * - * REMOVE THIS COMMENT BLOCK WHEN YOU ARE FINISHED WITH THE FILTER_HUMAN_NAME - */ + const auto& imageGeom = m_DataStructure.getDataRefAs(m_InputValues->imageGeometryPath); + const auto& imageArray = m_DataStructure.getDataRefAs(m_InputValues->imageDataArrayPath); + + // Stored fastest to slowest: X, Y, Z + SizeVec3 dims = imageGeom.getDimensions(); + usize dimX = dims[0]; + usize dimY = dims[1]; + usize dimZ = dims[2]; + + usize nComp = imageArray.getNumberOfComponents(); + DataType dataType = imageArray.getDataType(); + usize bytesPerComponent = imageArray.getIDataStoreRef().getTypeSize(); + + // Determine slice parameters based on plane + usize sliceCount = 0; + usize sliceW = 0; + usize sliceH = 0; + + switch(m_InputValues->planeIndex) + { + case 0: // XY + sliceCount = dimZ; + sliceW = dimX; + sliceH = dimY; + break; + case 1: // XZ + sliceCount = dimY; + sliceW = dimX; + sliceH = dimZ; + break; + case 2: // YZ + sliceCount = dimX; + sliceW = dimY; + sliceH = dimZ; + break; + default: + return MakeErrorResult(-27000, fmt::format("Invalid plane index: {}", m_InputValues->planeIndex)); + } + + // Create the IImageIO backend + auto imageIOResult = CreateImageIO(m_InputValues->outputFilePath); + if(imageIOResult.invalid()) + { + return ConvertResult(std::move(imageIOResult)); + } + const auto& imageIO = imageIOResult.value(); + + // Create output directory if needed + fs::path parentDir = fs::absolute(m_InputValues->outputFilePath).parent_path(); + if(!fs::exists(parentDir)) + { + if(!fs::create_directories(parentDir)) + { + return MakeErrorResult(-27001, fmt::format("Error creating output directory '{}'", parentDir.string())); + } + } + + // Pre-compute filename parts + fs::path stem = m_InputValues->outputFilePath.stem(); + fs::path ext = m_InputValues->outputFilePath.extension(); + fs::path parent = fs::absolute(m_InputValues->outputFilePath).parent_path(); + + // Allocate temp buffer for one slice + usize sliceBufferSize = sliceW * sliceH * nComp * bytesPerComponent; + std::vector sliceBuffer(sliceBufferSize); + + for(usize slice = 0; slice < sliceCount; ++slice) + { + if(m_ShouldCancel) + { + return {}; + } + + m_MessageHandler(IFilter::Message::Type::Info, fmt::format("Writing slice {}/{}", slice + 1, sliceCount)); + + // Zero out the buffer + std::fill(sliceBuffer.begin(), sliceBuffer.end(), static_cast(0)); + + // Extract slice data using type-dispatched functor + auto extractResult = ExecuteDataFunction(ExtractSliceFunctor{}, dataType, imageArray, sliceBuffer, slice, m_InputValues->planeIndex, dimX, dimY, dimZ, nComp); + if(extractResult.invalid()) + { + return extractResult; + } + + // Build ImageMetadata for this 2D slice + ImageMetadata metadata; + metadata.width = sliceW; + metadata.height = sliceH; + metadata.numComponents = nComp; + metadata.dataType = dataType; + metadata.numPages = 1; + + // Generate filename: stem_NNN.ext + std::string indexStr = fmt::format("{}", slice + m_InputValues->indexOffset); + while(indexStr.size() < static_cast(m_InputValues->totalIndexDigits)) + { + indexStr = m_InputValues->leadingDigitCharacter + indexStr; + } + fs::path slicePath = parent / fmt::format("{}_{}{}", stem.string(), indexStr, ext.string()); + + // Use AtomicFile for safe writes + auto atomicFileResult = AtomicFile::Create(slicePath); + if(atomicFileResult.invalid()) + { + return ConvertResult(std::move(atomicFileResult)); + } + AtomicFile atomicFile = std::move(atomicFileResult.value()); + + // Write via IImageIO + auto writeResult = imageIO->writePixelData(atomicFile.tempFilePath(), sliceBuffer, metadata); + if(writeResult.invalid()) + { + return writeResult; + } + + // Commit the atomic file + auto commitResult = atomicFile.commit(); + if(commitResult.invalid()) + { + return commitResult; + } + } return {}; } diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/WriteImage.hpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/WriteImage.hpp index e5cb1d087b..92536566ed 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/WriteImage.hpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/WriteImage.hpp @@ -6,27 +6,28 @@ #include "simplnx/DataStructure/DataStructure.hpp" #include "simplnx/Filter/IFilter.hpp" -//TODO: PARAMETER_INCLUDES - -/** -* This is example code to put in the Execute Method of the filter. -@EXECUTE_EXAMPLE_CODE@ -*/ +#include +#include namespace nx::core { struct SIMPLNXCORE_EXPORT WriteImageInputValues { -//TODO: INPUT_VALUE_STRUCT_DEF - + std::filesystem::path outputFilePath; + usize planeIndex = 0; ///< 0=XY, 1=XZ, 2=YZ + uint64 indexOffset = 0; + int32 totalIndexDigits = 3; + std::string leadingDigitCharacter = "0"; + DataPath imageGeometryPath; + DataPath imageDataArrayPath; }; /** * @class WriteImage - * @brief This algorithm implements support code for the WriteImageFilter + * @brief Extracts 2D slices from a 3D ImageGeometry and writes them as + * individual image files via the IImageIO layer. */ - class SIMPLNXCORE_EXPORT WriteImage { public: @@ -47,4 +48,4 @@ class SIMPLNXCORE_EXPORT WriteImage const IFilter::MessageHandler& m_MessageHandler; }; -} // namespace complex +} // namespace nx::core diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteImageFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteImageFilter.cpp index 2c47aeca30..2f5fbae258 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteImageFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteImageFilter.cpp @@ -2,12 +2,11 @@ #include "SimplnxCore/Filters/Algorithms/WriteImage.hpp" -#include "simplnx/DataStructure/DataPath.hpp" -#include "simplnx/Filter/Actions/EmptyAction.hpp" #include "simplnx/Common/AtomicFile.hpp" #include "simplnx/DataStructure/DataPath.hpp" #include "simplnx/DataStructure/DataStore.hpp" #include "simplnx/DataStructure/Geometry/ImageGeom.hpp" +#include "simplnx/Filter/Actions/EmptyAction.hpp" #include "simplnx/Parameters/ArraySelectionParameter.hpp" #include "simplnx/Parameters/ChoicesParameter.hpp" #include "simplnx/Parameters/DataGroupSelectionParameter.hpp" @@ -16,13 +15,13 @@ #include "simplnx/Parameters/GeometrySelectionParameter.hpp" #include "simplnx/Parameters/NumberParameter.hpp" #include "simplnx/Parameters/StringParameter.hpp" -#include "simplnx/Utilities/StringUtilities.hpp" +#include "simplnx/Utilities/ImageIO/ImageIOFactory.hpp" #include "simplnx/Utilities/SIMPLConversion.hpp" +#include "simplnx/Utilities/StringUtilities.hpp" #include #include -#include #include #include #include @@ -35,11 +34,12 @@ namespace { const std::set& GetScalarPixelAllowedTypes() { - static const std::set dataTypes = {nx::core::DataType::int8, nx::core::DataType::uint8, nx::core::DataType::int16, nx::core::DataType::uint16, nx::core::DataType::int32, - nx::core::DataType::uint32, nx::core::DataType::float32}; + static const std::set dataTypes = {nx::core::DataType::int8, nx::core::DataType::uint8, nx::core::DataType::int16, + nx::core::DataType::uint16, nx::core::DataType::int32, nx::core::DataType::uint32, + nx::core::DataType::float32}; return dataTypes; } -} +} // namespace namespace nx::core { @@ -111,39 +111,86 @@ IFilter::UniquePointer WriteImageFilter::clone() const //------------------------------------------------------------------------------ IFilter::PreflightResult WriteImageFilter::preflightImpl(const DataStructure& dataStructure, const Arguments& filterArgs, const MessageHandler& messageHandler, - const std::atomic_bool& shouldCancel, const ExecutionContext& executionContext) const + const std::atomic_bool& shouldCancel, const ExecutionContext& executionContext) const { auto plane = filterArgs.value(k_Plane_Key); auto filePath = filterArgs.value(k_FileName_Key); auto indexOffset = filterArgs.value(k_IndexOffset_Key); auto imageArrayPath = filterArgs.value(k_ImageArrayPath_Key); auto imageGeomPath = filterArgs.value(k_ImageGeomPath_Key); - auto totalDigits = filterArgs.value(k_TotalIndexDigits_Key); auto fillChar = filterArgs.value(k_LeadingDigitCharacter_Key); + // Validate output file format is supported + auto imageIOResult = CreateImageIO(filePath); + if(imageIOResult.invalid()) + { + return {ConvertResultTo(ConvertResult(std::move(imageIOResult)), {})}; + } + + // Validate fill character is a single character + if(fillChar.size() > 1) + { + return {MakeErrorResult(-27010, "The fill character should only be a single value.")}; + } + // Stored fastest to slowest i.e. X Y Z const auto& imageGeom = dataStructure.getDataRefAs(imageGeomPath); - // Stored slowest to fastest i.e. Z Y X const auto& imageArray = dataStructure.getDataRefAs(imageArrayPath); - - PreflightResult preflightResult; - nx::core::Result resultOutputActions; + // Validate data type is in the supported set + DataType arrayDataType = imageArray.getDataType(); + const auto& allowedTypes = ::GetScalarPixelAllowedTypes(); + if(allowedTypes.find(arrayDataType) == allowedTypes.end()) + { + return {MakeErrorResult(-27011, fmt::format("Unsupported data type '{}' for image writing. Supported types: int8, uint8, int16, uint16, int32, uint32, float32.", + DataTypeToString(arrayDataType)))}; + } + + // Compute slice count based on plane and geometry dims + auto imageGeomDims = imageGeom.getDimensions(); + usize maxSlice = 1; + switch(plane) + { + case 0: // XY + maxSlice = imageGeomDims[2]; + break; + case 1: // XZ + maxSlice = imageGeomDims[1]; + break; + case 2: // YZ + maxSlice = imageGeomDims[0]; + break; + default: + break; + } + + // Generate example filename for PreflightValues + std::stringstream ss; + ss << fs::absolute(filePath).parent_path().string() << "/" << filePath.stem().string(); + ss << "_" << std::setw(totalDigits) << std::setfill(fillChar.empty() ? '0' : fillChar[0]) << maxSlice; + ss << filePath.extension().string(); + + Result resultOutputActions; std::vector preflightUpdatedValues; + preflightUpdatedValues.push_back({"Example Output File", ss.str()}); return {std::move(resultOutputActions), std::move(preflightUpdatedValues)}; } //------------------------------------------------------------------------------ Result<> WriteImageFilter::executeImpl(DataStructure& dataStructure, const Arguments& filterArgs, const PipelineFilter* pipelineNode, const MessageHandler& messageHandler, - const std::atomic_bool& shouldCancel, const ExecutionContext& executionContext) const + const std::atomic_bool& shouldCancel, const ExecutionContext& executionContext) const { - WriteImageInputValues inputValues; - - //TODO: INPUT_VALUES_DEF + inputValues.outputFilePath = filterArgs.value(k_FileName_Key); + inputValues.planeIndex = filterArgs.value(k_Plane_Key); + inputValues.indexOffset = filterArgs.value(k_IndexOffset_Key); + inputValues.totalIndexDigits = filterArgs.value(k_TotalIndexDigits_Key); + inputValues.leadingDigitCharacter = filterArgs.value(k_LeadingDigitCharacter_Key); + inputValues.imageGeometryPath = filterArgs.value(k_ImageGeomPath_Key); + inputValues.imageDataArrayPath = filterArgs.value(k_ImageArrayPath_Key); return WriteImage(dataStructure, messageHandler, shouldCancel, &inputValues)(); } @@ -152,12 +199,9 @@ namespace { namespace SIMPL { - - //TODO: PARAMETER_JSON_CONSTANTS } // namespace SIMPL } // namespace - //------------------------------------------------------------------------------ Result WriteImageFilter::FromSIMPLJson(const nlohmann::json& json) { @@ -172,5 +216,4 @@ Result WriteImageFilter::FromSIMPLJson(const nlohmann::json& json) return ConvertResultTo(std::move(conversionResult), std::move(args)); } - } // namespace nx::core From ef1fe5038a7969fa765b6b0675dff4f2231b8767 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Fri, 10 Apr 2026 00:13:46 -0400 Subject: [PATCH 07/16] Implement ReadImageStack algorithm and ReadImageStackFilter Reads numbered image sequences into 3D ImageGeometry via IImageIO. Uses ReadImageFilter as sub-filter for per-slice reading. Supports resampling, grayscale conversion, flip transforms, origin/spacing overrides, and Z-slice cropping. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Filters/Algorithms/ReadImageStack.cpp | 378 ++++++++++++++++- .../Filters/Algorithms/ReadImageStack.hpp | 41 +- .../Filters/ReadImageStackFilter.cpp | 399 ++++++++++++++---- 3 files changed, 706 insertions(+), 112 deletions(-) diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ReadImageStack.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ReadImageStack.cpp index 2d6b8d309e..9e209fe9f7 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ReadImageStack.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ReadImageStack.cpp @@ -1,10 +1,359 @@ #include "ReadImageStack.hpp" +#include "SimplnxCore/Filters/ConvertColorToGrayScaleFilter.hpp" +#include "SimplnxCore/Filters/ReadImageFilter.hpp" +#include "SimplnxCore/Filters/ResampleImageGeomFilter.hpp" + +#include "simplnx/Common/TypesUtility.hpp" +#include "simplnx/Core/Application.hpp" #include "simplnx/DataStructure/DataArray.hpp" #include "simplnx/DataStructure/DataGroup.hpp" +#include "simplnx/DataStructure/Geometry/ImageGeom.hpp" +#include "simplnx/Filter/FilterHandle.hpp" +#include "simplnx/Parameters/BoolParameter.hpp" +#include "simplnx/Parameters/ChoicesParameter.hpp" +#include "simplnx/Parameters/CropGeometryParameter.hpp" +#include "simplnx/Parameters/DataObjectNameParameter.hpp" +#include "simplnx/Parameters/VectorParameter.hpp" +#include "simplnx/Utilities/FilterUtilities.hpp" + +#include + +#include + +namespace fs = std::filesystem; using namespace nx::core; +namespace +{ +const ChoicesParameter::ValueType k_NoImageTransform = 0; +const ChoicesParameter::ValueType k_FlipAboutXAxis = 1; +const ChoicesParameter::ValueType k_FlipAboutYAxis = 2; + +const ChoicesParameter::ValueType k_NoResampleModeIndex = 0; +const ChoicesParameter::ValueType k_ScalingModeIndex = 1; +const ChoicesParameter::ValueType k_ExactDimensionsModeIndex = 2; + +const Uuid k_SimplnxCorePluginId = *Uuid::FromString("05cc618b-781f-4ac0-b9ac-43f26ce1854f"); +const Uuid k_ReadImageFilterId = *Uuid::FromString("7391288d-3d0b-492e-93c9-e128e05c9737"); +const FilterHandle k_ReadImageFilterHandle(k_ReadImageFilterId, k_SimplnxCorePluginId); +const Uuid k_ColorToGrayScaleFilterId = *Uuid::FromString("d938a2aa-fee2-4db9-aa2f-2c34a9736580"); +const FilterHandle k_ColorToGrayScaleFilterHandle(k_ColorToGrayScaleFilterId, k_SimplnxCorePluginId); +const Uuid k_ResampleImageGeomFilterId = *Uuid::FromString("9783ea2c-4cf7-46de-ab21-b40d91a48c5b"); +const FilterHandle k_ResampleImageGeomFilterHandle(k_ResampleImageGeomFilterId, k_SimplnxCorePluginId); + +template +void FlipAboutYAxis(DataArray& dataArray, Vec3& dims) +{ + AbstractDataStore& tempDataStore = dataArray.getDataStoreRef(); + + usize numComp = tempDataStore.getNumberOfComponents(); + std::vector currentRowBuffer(dims[0] * dataArray.getNumberOfComponents()); + + for(usize row = 0; row < dims[1]; row++) + { + // Copy the current row into a temp buffer + typename AbstractDataStore::Iterator startIter = tempDataStore.begin() + (dims[0] * numComp * row); + typename AbstractDataStore::Iterator endIter = startIter + dims[0] * numComp; + std::copy(startIter, endIter, currentRowBuffer.begin()); + + // Starting at the last tuple in the buffer + usize bufferIndex = (dims[0] - 1) * numComp; + usize dataStoreIndex = row * dims[0] * numComp; + + for(usize tupleIdx = 0; tupleIdx < dims[0]; tupleIdx++) + { + for(usize cIdx = 0; cIdx < numComp; cIdx++) + { + tempDataStore.setValue(dataStoreIndex, currentRowBuffer[bufferIndex + cIdx]); + dataStoreIndex++; + } + bufferIndex = bufferIndex - numComp; + } + } +} + +template +void FlipAboutXAxis(DataArray& dataArray, Vec3& dims) +{ + AbstractDataStore& tempDataStore = dataArray.getDataStoreRef(); + usize numComp = tempDataStore.getNumberOfComponents(); + size_t rowLCV = (dims[1] % 2 == 1) ? ((dims[1] - 1) / 2) : dims[1] / 2; + usize bottomRow = dims[1] - 1; + + for(usize row = 0; row < rowLCV; row++) + { + // Copy the "top" row into a temp buffer + usize topStartIter = 0 + (dims[0] * numComp * row); + usize topEndIter = topStartIter + dims[0] * numComp; + usize bottomStartIter = 0 + (dims[0] * numComp * bottomRow); + + // Copy from bottom to top and then temp to bottom + for(usize eleIndex = topStartIter; eleIndex < topEndIter; eleIndex++) + { + T value = tempDataStore.getValue(eleIndex); + tempDataStore[eleIndex] = tempDataStore[bottomStartIter]; + tempDataStore[bottomStartIter] = value; + bottomStartIter++; + } + bottomRow--; + } +} + +template +Result<> ReadImageStackImpl(DataStructure& dataStructure, const ReadImageStackInputValues* inputValues, const IFilter::MessageHandler& messageHandler, const std::atomic_bool& shouldCancel) +{ + const DataPath& imageGeomPath = inputValues->imageGeometryPath; + const std::string& cellDataName = inputValues->cellDataName; + const std::string& imageArrayName = inputValues->imageDataArrayName; + const std::vector& files = inputValues->fileList; + const ChoicesParameter::ValueType transformType = inputValues->imageTransformChoice; + const bool convertToGrayscale = inputValues->convertToGrayScale; + const VectorFloat32Parameter::ValueType& luminosityValues = inputValues->colorWeights; + const ChoicesParameter::ValueType resample = inputValues->resampleImagesChoice; + const float32 scalingFactor = inputValues->scaling; + const VectorUInt64Parameter::ValueType& exactDims = inputValues->exactXYDimensions; + const bool changeDataType = inputValues->changeDataType; + const ChoicesParameter::ValueType destType = inputValues->imageDataTypeChoice; + const CropGeometryParameter::ValueType& croppingOptions = inputValues->croppingOptions; + const bool shouldChangeOrigin = inputValues->changeOrigin; + const VectorFloat64Parameter::ValueType& origin = inputValues->origin; + const bool shouldChangeSpacing = inputValues->changeSpacing; + const VectorFloat64Parameter::ValueType& spacing = inputValues->spacing; + const usize originSpacingProcessing = inputValues->originSpacingProcessing; + + DataPath destImageGeomPath = imageGeomPath; + auto& destImageGeomInitial = dataStructure.getDataRefAs(destImageGeomPath); + + FilterList* filterListPtr = Application::Instance()->getFilterList(); + + if((convertToGrayscale || resample != k_NoResampleModeIndex) && !filterListPtr->containsPlugin(k_SimplnxCorePluginId)) + { + return MakeErrorResult(-18542, "SimplnxCore was not instantiated in this instance, so color to grayscale / resample is not a valid option."); + } + std::unique_ptr grayScaleFilter = filterListPtr->createFilter(k_ColorToGrayScaleFilterHandle); + Result<> outputResult = {}; + + // Determine start/end slice based on Z cropping + usize startSlice = 0; + usize endSlice = files.size() - 1; + if(croppingOptions.cropZ && croppingOptions.type == CropGeometryParameter::ValueType::TypeEnum::VoxelSubvolume) + { + startSlice = static_cast(croppingOptions.zBoundVoxels[0]); + endSlice = static_cast(croppingOptions.zBoundVoxels[1]); + } + else if(croppingOptions.cropZ && croppingOptions.type == CropGeometryParameter::ValueType::TypeEnum::PhysicalSubvolume) + { + SizeVec3 destDims = destImageGeomInitial.getDimensions(); + FloatVec3 destOrigin = destImageGeomInitial.getOrigin(); + + std::optional result = destImageGeomInitial.getIndex(destOrigin[0], destOrigin[1], croppingOptions.zBoundPhysical[0]); + if(result.has_value()) + { + startSlice = result.value() / (destDims[0] * destDims[1]); + } + result = destImageGeomInitial.getIndex(destOrigin[0], destOrigin[1], croppingOptions.zBoundPhysical[1]); + if(result.has_value()) + { + endSlice = result.value() / (destDims[0] * destDims[1]); + } + } + + // Loop over all the files, importing them one by one and copying the data into the destination data array + usize slice = 0; + for(usize i = startSlice; i <= endSlice; i++) + { + const std::string& filePath = files[i]; + messageHandler(IFilter::Message::Type::Info, fmt::format("Importing: {}", filePath)); + + DataStructure importedDataStructure; + { + // Create a sub-filter instance to read each image + std::unique_ptr imageReader = filterListPtr->createFilter(k_ReadImageFilterHandle); + if(nullptr == imageReader.get()) + { + return MakeErrorResult(-18543, "Unable to create an instance of ReadImageFilter, so image stack reading is not available."); + } + + Arguments args; + args.insertOrAssign(ReadImageFilter::k_ImageGeometryPath_Key, std::make_any(imageGeomPath)); + args.insertOrAssign(ReadImageFilter::k_CellDataName_Key, std::make_any(cellDataName)); + args.insertOrAssign(ReadImageFilter::k_ImageDataArrayPath_Key, std::make_any(imageArrayName)); + args.insertOrAssign(ReadImageFilter::k_FileName_Key, std::make_any(filePath)); + args.insertOrAssign(ReadImageFilter::k_ChangeDataType_Key, std::make_any(changeDataType)); + args.insertOrAssign(ReadImageFilter::k_ImageDataType_Key, std::make_any(destType)); + args.insertOrAssign(ReadImageFilter::k_LengthUnit_Key, std::make_any(to_underlying(IGeometry::LengthUnit::Micrometer))); + // Do not set the origin/spacing if processing timing is postprocessed; the main filter will set it at the end + args.insertOrAssign(ReadImageFilter::k_ChangeOrigin_Key, std::make_any(shouldChangeOrigin && originSpacingProcessing == 0)); + args.insertOrAssign(ReadImageFilter::k_CenterOrigin_Key, std::make_any(false)); + args.insertOrAssign(ReadImageFilter::k_Origin_Key, std::make_any(origin)); + args.insertOrAssign(ReadImageFilter::k_ChangeSpacing_Key, std::make_any(shouldChangeSpacing && originSpacingProcessing == 0)); + args.insertOrAssign(ReadImageFilter::k_Spacing_Key, std::make_any(spacing)); + args.insertOrAssign(ReadImageFilter::k_OriginSpacingProcessing_Key, std::make_any(originSpacingProcessing)); + args.insertOrAssign(ReadImageFilter::k_CroppingOptions_Key, std::make_any(croppingOptions)); + + IFilter::ExecuteResult executeResult = imageReader->execute(importedDataStructure, args); + if(executeResult.result.invalid()) + { + return executeResult.result; + } + } + + // ======================= Resample Image Geometry Section =================== + if(resample != k_NoResampleModeIndex) + { + std::unique_ptr resampleImageGeomFilter = filterListPtr->createFilter(k_ResampleImageGeomFilterHandle); + if(nullptr == resampleImageGeomFilter.get()) + { + return MakeErrorResult(-18545, "Unable to create an instance of ResampleImageGeomFilter."); + } + if(resample == k_ScalingModeIndex) + { + if(scalingFactor == 100.0f) + { + // No-op + } + else + { + Arguments resampleImageGeomArgs; + resampleImageGeomArgs.insertOrAssign(ResampleImageGeomFilter::k_SelectedImageGeometryPath_Key, std::make_any(imageGeomPath)); + resampleImageGeomArgs.insertOrAssign(ResampleImageGeomFilter::k_RemoveOriginalGeometry_Key, std::make_any(true)); + + resampleImageGeomArgs.insertOrAssign(ResampleImageGeomFilter::k_ResamplingMode_Key, std::make_any(1)); + resampleImageGeomArgs.insertOrAssign(ResampleImageGeomFilter::k_Scaling_Key, std::make_any(std::vector{scalingFactor, scalingFactor, 100.0f})); + + // Run resample image geometry filter and process results and messages + Result<> result = resampleImageGeomFilter->execute(importedDataStructure, resampleImageGeomArgs).result; + if(result.invalid()) + { + return result; + } + } + } + else + { + Arguments resampleImageGeomArgs; + resampleImageGeomArgs.insertOrAssign(ResampleImageGeomFilter::k_SelectedImageGeometryPath_Key, std::make_any(imageGeomPath)); + resampleImageGeomArgs.insertOrAssign(ResampleImageGeomFilter::k_RemoveOriginalGeometry_Key, std::make_any(true)); + + resampleImageGeomArgs.insertOrAssign(ResampleImageGeomFilter::k_ResamplingMode_Key, std::make_any(2)); + resampleImageGeomArgs.insertOrAssign(ResampleImageGeomFilter::k_ExactDimensions_Key, std::make_any(std::vector{exactDims[0], exactDims[1], 1})); + + // Run resample image geometry filter and process results and messages + Result<> result = resampleImageGeomFilter->execute(importedDataStructure, resampleImageGeomArgs).result; + if(result.invalid()) + { + return result; + } + } + } + + // ======================= Convert to GrayScale Section =================== + DataPath srcImageDataPath = imageGeomPath.createChildPath(cellDataName).createChildPath(imageArrayName); + + bool validInputForGrayScaleConversion = importedDataStructure.getDataRefAs(srcImageDataPath).getDataType() == DataType::uint8; + if(convertToGrayscale && validInputForGrayScaleConversion && nullptr != grayScaleFilter.get()) + { + Arguments colorToGrayscaleArgs; + colorToGrayscaleArgs.insertOrAssign(ConvertColorToGrayScaleFilter::k_ConversionAlgorithm_Key, std::make_any(0)); + colorToGrayscaleArgs.insertOrAssign(ConvertColorToGrayScaleFilter::k_ColorWeights_Key, std::make_any(luminosityValues)); + colorToGrayscaleArgs.insertOrAssign(ConvertColorToGrayScaleFilter::k_InputDataArrayPath_Key, std::make_any>(std::vector{srcImageDataPath})); + colorToGrayscaleArgs.insertOrAssign(ConvertColorToGrayScaleFilter::k_OutputArrayPrefix_Key, std::make_any("gray")); + + // Run grayscale filter and process results and messages + Result<> result = grayScaleFilter->execute(importedDataStructure, colorToGrayscaleArgs).result; + if(result.invalid()) + { + return result; + } + + // deletion of non-grayscale array + DataObject::IdType id; + { // scoped for safety since this reference will be nonexistent in a moment + auto& oldArray = importedDataStructure.getDataRefAs(srcImageDataPath); + id = oldArray.getId(); + } + importedDataStructure.removeData(id); + + // rename grayscale array to reflect original + { + auto& gray = importedDataStructure.getDataRefAs(srcImageDataPath.replaceName("gray" + srcImageDataPath.getTargetName())); + if(!gray.canRename(srcImageDataPath.getTargetName())) + { + return MakeErrorResult(-64543, fmt::format("Unable to rename the internal grayscale array to {}", srcImageDataPath.getTargetName())); + } + gray.rename(srcImageDataPath.getTargetName()); + } + } + else if(convertToGrayscale && !validInputForGrayScaleConversion) + { + outputResult.warnings().emplace_back( + Warning{-74320, fmt::format("The array ({}) resulting from reading the input image file is not a UInt8Array. The input image will not be converted to grayscale.", + srcImageDataPath.getTargetName())}); + } + + auto& destImageGeom = dataStructure.getDataRefAs(destImageGeomPath); + SizeVec3 destDims = destImageGeom.getDimensions(); + const usize destTuplesPerSlice = destDims[0] * destDims[1]; + + // Check the ImageGeometry of the imported Image matches the destination + const auto& importedImageGeom = importedDataStructure.getDataRefAs(imageGeomPath); + SizeVec3 importedDims = importedImageGeom.getDimensions(); + if(destDims[0] != importedDims[0] || destDims[1] != importedDims[1]) + { + return MakeErrorResult(-64510, fmt::format("Slice {} image dimensions are different than expected dimensions.\n Expected Slice Dims are: {} x {}\n Received Slice Dims are: {} x {}\n", slice, + destDims[0], destDims[1], importedDims[0], importedDims[1])); + } + + // Compute the Tuple Index we are at: + const usize destTupleIndex = (slice * destDims[0] * destDims[1]); + + // get the current Slice data... + auto& srcData = importedDataStructure.getDataRefAs>(srcImageDataPath); + AbstractDataStore& srcDataStore = srcData.getDataStoreRef(); + + if(transformType == k_FlipAboutYAxis) + { + FlipAboutYAxis(srcData, destDims); + } + else if(transformType == k_FlipAboutXAxis) + { + FlipAboutXAxis(srcData, destDims); + } + + // Copy that into the output array... + DataPath destImageDataPath = destImageGeomPath.createChildPath(cellDataName).createChildPath(imageArrayName); + auto& outputData = dataStructure.getDataRefAs>(destImageDataPath); + AbstractDataStore& outputDataStore = outputData.getDataStoreRef(); + Result<> result = outputDataStore.copyFrom(destTupleIndex, srcDataStore, 0, destTuplesPerSlice); + if(result.invalid()) + { + return result; + } + + slice++; + + // Check to see if the filter got canceled. + if(shouldCancel) + { + return outputResult; + } + } + + return outputResult; +} + +struct ReadImageStackDispatchFunctor +{ + template + Result<> operator()(DataStructure& dataStructure, const ReadImageStackInputValues* inputValues, const IFilter::MessageHandler& messageHandler, const std::atomic_bool& shouldCancel) + { + return ReadImageStackImpl(dataStructure, inputValues, messageHandler, shouldCancel); + } +}; +} // namespace + // ----------------------------------------------------------------------------- ReadImageStack::ReadImageStack(DataStructure& dataStructure, const IFilter::MessageHandler& mesgHandler, const std::atomic_bool& shouldCancel, ReadImageStackInputValues* inputValues) : m_DataStructure(dataStructure) @@ -20,23 +369,14 @@ ReadImageStack::~ReadImageStack() noexcept = default; // ----------------------------------------------------------------------------- Result<> ReadImageStack::operator()() { - /** - * This section of the code should contain the actual algorithmic codes that - * will accomplish the goal of the file. - * - * If you can parallelize the code there are a number of examples on how to do that. - * GenerateIPFColors is one example - * - * If you need to determine what kind of array you have (Int32Array, Float32Array, etc) - * look to the ExecuteDataFunction() in simplnx/Utilities/FilterUtilities.hpp template - * function to help with that code. - * An Example algorithm class is `CombineAttributeArrays` and `RemoveFlaggedVertices` - * - * There are other utility classes that can help alleviate the amount of code that needs - * to be written. - * - * REMOVE THIS COMMENT BLOCK WHEN YOU ARE FINISHED WITH THE FILTER_HUMAN_NAME - */ - - return {}; + // Determine the DataType of the final destination DataArray from the DataStructure + const std::string& imageArrayName = m_InputValues->imageDataArrayName; + const std::string& cellDataName = m_InputValues->cellDataName; + DataPath destImageGeomPath = m_InputValues->imageGeometryPath; + DataPath destImageDataPath = destImageGeomPath.createChildPath(cellDataName).createChildPath(imageArrayName); + + auto& destArray = m_DataStructure.getDataRefAs(destImageDataPath); + DataType destDataType = destArray.getDataType(); + + return ExecuteDataFunction(ReadImageStackDispatchFunctor{}, destDataType, m_DataStructure, m_InputValues, m_MessageHandler, m_ShouldCancel); } diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ReadImageStack.hpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ReadImageStack.hpp index ba88bc8ab5..664583ac78 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ReadImageStack.hpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ReadImageStack.hpp @@ -2,31 +2,50 @@ #include "SimplnxCore/SimplnxCore_export.hpp" +#include "simplnx/Common/Array.hpp" +#include "simplnx/Common/Types.hpp" #include "simplnx/DataStructure/DataPath.hpp" #include "simplnx/DataStructure/DataStructure.hpp" #include "simplnx/Filter/IFilter.hpp" +#include "simplnx/Parameters/ChoicesParameter.hpp" +#include "simplnx/Parameters/CropGeometryParameter.hpp" +#include "simplnx/Parameters/VectorParameter.hpp" -//TODO: PARAMETER_INCLUDES - -/** -* This is example code to put in the Execute Method of the filter. -@EXECUTE_EXAMPLE_CODE@ -*/ +#include +#include +#include namespace nx::core { struct SIMPLNXCORE_EXPORT ReadImageStackInputValues { -//TODO: INPUT_VALUE_STRUCT_DEF - + std::vector fileList; + DataPath imageGeometryPath; + std::string imageDataArrayName; + std::string cellDataName; + bool changeOrigin = false; + VectorFloat64Parameter::ValueType origin; + bool changeSpacing = false; + VectorFloat64Parameter::ValueType spacing; + usize originSpacingProcessing = 1; // 0=Preprocessed, 1=Postprocessed + usize imageTransformChoice = 0; // 0=None, 1=FlipAboutXAxis, 2=FlipAboutYAxis + bool convertToGrayScale = false; + VectorFloat32Parameter::ValueType colorWeights; + usize resampleImagesChoice = 0; // 0=None, 1=Scaling, 2=ExactDims + float32 scaling = 100.0f; + VectorUInt64Parameter::ValueType exactXYDimensions; + bool changeDataType = false; + usize imageDataTypeChoice = 0; + CropGeometryParameter::ValueType croppingOptions; }; /** * @class ReadImageStack - * @brief This algorithm implements support code for the ReadImageStackFilter + * @brief This algorithm reads a numbered sequence of 2D image files into a 3D DataArray + * using ReadImageFilter as a sub-filter for per-slice reading. Supports resampling, + * grayscale conversion, flip transforms, origin/spacing overrides, and Z-slice cropping. */ - class SIMPLNXCORE_EXPORT ReadImageStack { public: @@ -47,4 +66,4 @@ class SIMPLNXCORE_EXPORT ReadImageStack const IFilter::MessageHandler& m_MessageHandler; }; -} // namespace complex +} // namespace nx::core diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ReadImageStackFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ReadImageStackFilter.cpp index 249114266d..3acc4c57ac 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ReadImageStackFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ReadImageStackFilter.cpp @@ -1,11 +1,13 @@ #include "ReadImageStackFilter.hpp" #include "SimplnxCore/Filters/Algorithms/ReadImageStack.hpp" +#include "SimplnxCore/Filters/ReadImageFilter.hpp" #include "simplnx/Common/TypesUtility.hpp" #include "simplnx/Core/Application.hpp" #include "simplnx/DataStructure/DataPath.hpp" #include "simplnx/DataStructure/Geometry/ImageGeom.hpp" +#include "simplnx/Filter/Actions/CreateArrayAction.hpp" #include "simplnx/Filter/Actions/CreateImageGeometryAction.hpp" #include "simplnx/Filter/Actions/DeleteDataAction.hpp" #include "simplnx/Filter/Actions/RenameDataAction.hpp" @@ -51,76 +53,7 @@ const Uuid k_ResampleImageGeomFilterId = *Uuid::FromString("9783ea2c-4cf7-46de-a const FilterHandle k_ResampleImageGeomFilterHandle(k_ResampleImageGeomFilterId, k_SimplnxCorePluginId); const Uuid k_CropImageGeomFilterId = *Uuid::FromString("e6476737-4aa7-48ba-a702-3dfab82c96e2"); const FilterHandle k_CropImageGeomFilterHandle(k_CropImageGeomFilterId, k_SimplnxCorePluginId); - - -// Make sure we can instantiate the RotateSampleRefFrame Filter -std::unique_ptr CreateRotateSampleRefFrameFilter() -{ - FilterList* filterListPtr = Application::Instance()->getFilterList(); - std::unique_ptr filter = filterListPtr->createFilter(k_RotateSampleRefFrameFilterHandle); - return filter; -} - -template -void FlipAboutYAxis(DataArray& dataArray, Vec3& dims) -{ - AbstractDataStore& tempDataStore = dataArray.getDataStoreRef(); - - usize numComp = tempDataStore.getNumberOfComponents(); - std::vector currentRowBuffer(dims[0] * dataArray.getNumberOfComponents()); - - for(usize row = 0; row < dims[1]; row++) - { - // Copy the current row into a temp buffer - typename AbstractDataStore::Iterator startIter = tempDataStore.begin() + (dims[0] * numComp * row); - typename AbstractDataStore::Iterator endIter = startIter + dims[0] * numComp; - std::copy(startIter, endIter, currentRowBuffer.begin()); - - // Starting at the last tuple in the buffer - usize bufferIndex = (dims[0] - 1) * numComp; - usize dataStoreIndex = row * dims[0] * numComp; - - for(usize tupleIdx = 0; tupleIdx < dims[0]; tupleIdx++) - { - for(usize cIdx = 0; cIdx < numComp; cIdx++) - { - tempDataStore.setValue(dataStoreIndex, currentRowBuffer[bufferIndex + cIdx]); - dataStoreIndex++; - } - bufferIndex = bufferIndex - numComp; - } - } -} - -template -void FlipAboutXAxis(DataArray& dataArray, Vec3& dims) -{ - AbstractDataStore& tempDataStore = dataArray.getDataStoreRef(); - usize numComp = tempDataStore.getNumberOfComponents(); - size_t rowLCV = (dims[1] % 2 == 1) ? ((dims[1] - 1) / 2) : dims[1] / 2; - usize bottomRow = dims[1] - 1; - - for(usize row = 0; row < rowLCV; row++) - { - // Copy the "top" row into a temp buffer - usize topStartIter = 0 + (dims[0] * numComp * row); - usize topEndIter = topStartIter + dims[0] * numComp; - usize bottomStartIter = 0 + (dims[0] * numComp * bottomRow); - - // Copy from bottom to top and then temp to bottom - for(usize eleIndex = topStartIter; eleIndex < topEndIter; eleIndex++) - { - T value = tempDataStore.getValue(eleIndex); - tempDataStore[eleIndex] = tempDataStore[bottomStartIter]; - tempDataStore[bottomStartIter] = value; - bottomStartIter++; - } - bottomRow--; - } -} - - -} +} // namespace namespace nx::core { @@ -233,15 +166,14 @@ IFilter::UniquePointer ReadImageStackFilter::clone() const //------------------------------------------------------------------------------ IFilter::PreflightResult ReadImageStackFilter::preflightImpl(const DataStructure& dataStructure, const Arguments& filterArgs, const MessageHandler& messageHandler, - const std::atomic_bool& shouldCancel, const ExecutionContext& executionContext) const + const std::atomic_bool& shouldCancel, const ExecutionContext& executionContext) const { auto inputFileListInfo = filterArgs.value(k_InputFileListInfo_Key); auto shouldChangeOrigin = filterArgs.value(k_ChangeOrigin_Key); auto shouldChangeSpacing = filterArgs.value(k_ChangeSpacing_Key); auto origin = filterArgs.value(k_Origin_Key); auto spacing = filterArgs.value(k_Spacing_Key); - // TODO: Replace with local OriginSpacingProcessingTiming enum once ReadImageStackFilter is implemented - // auto originSpacingProcessing = static_cast(filterArgs.value(k_OriginSpacingProcessing_Key)); + auto originSpacingProcessing = filterArgs.value(k_OriginSpacingProcessing_Key); auto imageGeomPath = filterArgs.value(k_ImageGeometryPath_Key); auto pImageDataArrayNameValue = filterArgs.value(k_ImageDataArrayPath_Key); auto cellDataName = filterArgs.value(k_CellDataName_Key); @@ -258,21 +190,326 @@ IFilter::PreflightResult ReadImageStackFilter::preflightImpl(const DataStructure nx::core::Result resultOutputActions; std::vector preflightUpdatedValues; - PreflightResult preflightResult; + std::vector files = inputFileListInfo.generate(); + + if(files.empty()) + { + return {MakeErrorResult(-1, "GeneratedFileList must not be empty")}; + } + + DataStructure tmpDs; + std::vector outputDims; + std::vector outputSpacing; + std::vector outputOrigin; + IGeometry::LengthUnit outputUnits = IGeometry::LengthUnit::Micrometer; + + // Create a sub-filter to read each image, although for preflight we are going to read the first image in the + // list and hope the rest are correct. + Arguments imageReaderArgs; + imageReaderArgs.insertOrAssign(ReadImageFilter::k_ImageGeometryPath_Key, std::make_any(imageGeomPath)); + imageReaderArgs.insertOrAssign(ReadImageFilter::k_CellDataName_Key, std::make_any(cellDataName)); + imageReaderArgs.insertOrAssign(ReadImageFilter::k_ImageDataArrayPath_Key, std::make_any(pImageDataArrayNameValue)); + imageReaderArgs.insertOrAssign(ReadImageFilter::k_FileName_Key, std::make_any(files.at(0))); + imageReaderArgs.insertOrAssign(ReadImageFilter::k_ChangeDataType_Key, std::make_any(pChangeDataType)); + imageReaderArgs.insertOrAssign(ReadImageFilter::k_ImageDataType_Key, std::make_any(numericType)); + imageReaderArgs.insertOrAssign(ReadImageFilter::k_LengthUnit_Key, std::make_any(to_underlying(IGeometry::LengthUnit::Micrometer))); + // Do not set the origin if processing timing is postprocessed, we will set the final origin & spacing at the end + imageReaderArgs.insertOrAssign(ReadImageFilter::k_ChangeOrigin_Key, std::make_any(shouldChangeOrigin && originSpacingProcessing == 0)); + imageReaderArgs.insertOrAssign(ReadImageFilter::k_CenterOrigin_Key, std::make_any(false)); + imageReaderArgs.insertOrAssign(ReadImageFilter::k_Origin_Key, std::make_any(origin)); + // Do not set the spacing if processing timing is postprocessed, we will set the final origin & spacing at the end + imageReaderArgs.insertOrAssign(ReadImageFilter::k_ChangeSpacing_Key, std::make_any(shouldChangeSpacing && originSpacingProcessing == 0)); + imageReaderArgs.insertOrAssign(ReadImageFilter::k_Spacing_Key, std::make_any(spacing)); + imageReaderArgs.insertOrAssign(ReadImageFilter::k_OriginSpacingProcessing_Key, std::make_any(originSpacingProcessing)); + imageReaderArgs.insertOrAssign(ReadImageFilter::k_CroppingOptions_Key, std::make_any(croppingOptions)); + + const ReadImageFilter imageReader; + PreflightResult imageReaderResult = imageReader.preflight(tmpDs, imageReaderArgs, messageHandler, shouldCancel); + if(imageReaderResult.outputActions.invalid()) + { + return imageReaderResult; + } + + // The first output actions should be the geometry creation + const IDataAction* action0Ptr = imageReaderResult.outputActions.value().actions.at(0).get(); + const auto* createImageGeomActionPtr = dynamic_cast(action0Ptr); + if(createImageGeomActionPtr != nullptr) + { + outputDims = createImageGeomActionPtr->dims(); + outputSpacing = createImageGeomActionPtr->spacing(); + outputOrigin = createImageGeomActionPtr->origin(); + outputUnits = createImageGeomActionPtr->units(); + + // Compute Z dimension, taking into account possible Z cropping + usize totalSlices = files.size(); + usize zDim = totalSlices; + + if(croppingOptions.cropZ) + { + if(croppingOptions.type == CropGeometryParameter::CropValues::TypeEnum::VoxelSubvolume) + { + // Voxel-based Z cropping: zBoundVoxels are inclusive indices + const auto zMin = static_cast(croppingOptions.zBoundVoxels[0]); + const auto zMax = static_cast(croppingOptions.zBoundVoxels[1]); + if(zMax >= zMin) + { + zDim = zMax - zMin + 1; + } + } + else if(croppingOptions.type == CropGeometryParameter::CropValues::TypeEnum::PhysicalSubvolume) + { + const float64 zMinPhys = croppingOptions.zBoundPhysical[0]; + const float64 zMaxPhys = croppingOptions.zBoundPhysical[1]; + + const float64 originZ = (shouldChangeOrigin && originSpacingProcessing == 0) ? origin[2] : 0; + const float64 spacingZ = (shouldChangeSpacing && originSpacingProcessing == 0) ? spacing[2] : 1; + + if(zMaxPhys < zMinPhys) + { + return MakePreflightErrorResult( + -23520, fmt::format("Invalid Z cropping range: the maximum physical Z value is smaller than the minimum. Please ensure the start Z is less than or equal to the end Z.")); + } + + if(spacingZ <= 0) + { + return MakePreflightErrorResult(-23521, fmt::format("Invalid Z spacing ({}). The Z spacing must be greater than zero to apply physical cropping.", spacingZ)); + } + + if(zMinPhys < originZ || zMinPhys > (static_cast(zDim) * spacingZ + originZ)) + { + return MakePreflightErrorResult(-23522, fmt::format("The minimum Z cropping value ({}) is outside the image bounds. Valid Z range is [{} to {}] in physical units.", zMinPhys, originZ, + (static_cast(zDim) * spacingZ + originZ))); + } + + if(zMaxPhys < originZ || zMaxPhys > (static_cast(zDim) * spacingZ + originZ)) + { + return MakePreflightErrorResult(-23523, fmt::format("The maximum Z cropping value ({}) is outside the image bounds. Valid Z range is [{} to {}] in physical units.", zMaxPhys, originZ, + (static_cast(zDim) * spacingZ + originZ))); + } + + const auto zMinIndex = static_cast(std::floor((zMinPhys - originZ) / spacingZ)); + if(zMinIndex >= zDim) + { + return MakePreflightErrorResult(-23524, fmt::format("The minimum Z cropping value ({}) converts to slice index {} which is outside the valid slice index range [0 to {}].", zMinPhys, + zMinIndex, (zDim > 0 ? zDim - 1 : 0))); + } + + const auto zMaxIndex = static_cast(std::floor((zMaxPhys - originZ) / spacingZ)); + if(zMaxIndex >= zDim) + { + return MakePreflightErrorResult(-23525, fmt::format("The maximum Z cropping value ({}) converts to slice index {} which is outside the valid slice index range [0 to {}].", zMaxPhys, + zMaxIndex, (zDim > 0 ? zDim - 1 : 0))); + } + + zDim = zMaxIndex - zMinIndex + 1; + } + } + + outputDims.back() = zDim; + + resultOutputActions.value().appendAction(std::make_unique(createImageGeomActionPtr->path(), outputDims, createImageGeomActionPtr->origin(), + createImageGeomActionPtr->spacing(), createImageGeomActionPtr->cellAttributeMatrixName(), + createImageGeomActionPtr->units())); + // The second action should be the array creation + const IDataAction* action1Ptr = imageReaderResult.outputActions.value().actions.at(1).get(); + const auto* createArrayActionPtr = dynamic_cast(action1Ptr); + if(createArrayActionPtr != nullptr) + { + resultOutputActions.value().appendAction(std::make_unique(createArrayActionPtr->type(), std::vector(outputDims.rbegin(), outputDims.rend()), + createArrayActionPtr->componentDims(), createArrayActionPtr->path(), createArrayActionPtr->dataFormat(), + createArrayActionPtr->fillValue())); + } + + Result<> actionsResult = resultOutputActions.value().applyAll(tmpDs, IDataAction::Mode::Preflight); + if(actionsResult.invalid()) + { + return {ConvertResultTo(std::move(actionsResult), {})}; + } + } + + DataPath currentImageGeomPath = imageGeomPath; + std::vector pathsToDelete; + + FilterList* filterListPtr = Application::Instance()->getFilterList(); + if(pResampleImagesChoiceValue != k_NoResampleModeIndex) + { + if(!filterListPtr->containsPlugin(k_SimplnxCorePluginId)) + { + PreflightResult errorResult = MakePreflightErrorResult(-18544, "The plugin SimplnxCore was not instantiated in this instance, so image resampling is not available."); + return errorResult; + } + + std::unique_ptr resampleImageGeomFilter = filterListPtr->createFilter(k_ResampleImageGeomFilterHandle); + if(nullptr == resampleImageGeomFilter.get()) + { + PreflightResult errorResult = MakePreflightErrorResult(-18545, "Unable to create an instance of the resample image geometry filter, so image resampling is not available."); + return errorResult; + } + + if(pResampleImagesChoiceValue == k_ScalingModeIndex && pScalingValue < 1.0f) + { + PreflightResult errorResult = MakePreflightErrorResult(-23508, fmt::format("Scaling value must be greater than or equal to 1.0f. Received: {}", pScalingValue)); + return errorResult; + } + + Arguments resampleImageGeomArgs; + resampleImageGeomArgs.insertOrAssign("input_image_geometry_path", std::make_any(currentImageGeomPath)); + resampleImageGeomArgs.insertOrAssign("remove_original_geometry", std::make_any(false)); + resampleImageGeomArgs.insertOrAssign("new_data_container_path", std::make_any(DataPath({imageGeomPath.getTargetName() + "_resampled"}))); + resampleImageGeomArgs.insertOrAssign("resampling_mode_index", std::make_any(pResampleImagesChoiceValue)); + resampleImageGeomArgs.insertOrAssign("scaling", std::make_any(std::vector{pScalingValue, pScalingValue, 100.0f})); + resampleImageGeomArgs.insertOrAssign("exact_dimensions", std::make_any(std::vector{pExactXYDimsValue[0], pExactXYDimsValue[1], outputDims[2]})); + + // Run resample image geometry filter and process results and messages + PreflightResult resampleImageResult = resampleImageGeomFilter->preflight(tmpDs, resampleImageGeomArgs, messageHandler, shouldCancel); + if(resampleImageResult.outputActions.invalid()) + { + return resampleImageResult; + } + + // The first output actions should be the geometry creation + action0Ptr = resampleImageResult.outputActions.value().actions.at(0).get(); + createImageGeomActionPtr = dynamic_cast(action0Ptr); + if(createImageGeomActionPtr != nullptr) + { + std::vector dims = createImageGeomActionPtr->dims(); + dims.back() = outputDims.back(); + outputDims = dims; + outputSpacing = createImageGeomActionPtr->spacing(); + outputOrigin = createImageGeomActionPtr->origin(); + outputUnits = createImageGeomActionPtr->units(); + resultOutputActions.value().appendAction(std::make_unique(createImageGeomActionPtr->path(), outputDims, createImageGeomActionPtr->origin(), + createImageGeomActionPtr->spacing(), createImageGeomActionPtr->cellAttributeMatrixName(), + createImageGeomActionPtr->units())); + // The second action should be the array creation + const IDataAction* action1Ptr = resampleImageResult.outputActions.value().actions.at(1).get(); + const auto* createArrayActionPtr = dynamic_cast(action1Ptr); + if(createArrayActionPtr != nullptr) + { + resultOutputActions.value().appendAction(std::make_unique(createArrayActionPtr->type(), std::vector(outputDims.rbegin(), outputDims.rend()), + createArrayActionPtr->componentDims(), createArrayActionPtr->path(), createArrayActionPtr->dataFormat(), + createArrayActionPtr->fillValue())); + } + + tmpDs = DataStructure(); + Result<> actionsResult = resultOutputActions.value().applyAll(tmpDs, IDataAction::Mode::Preflight); + if(actionsResult.invalid()) + { + return {ConvertResultTo(std::move(actionsResult), {})}; + } + } + + pathsToDelete.push_back(currentImageGeomPath); + currentImageGeomPath = DataPath({imageGeomPath.getTargetName() + "_resampled"}); + } + + const DataPath imageDataPath = currentImageGeomPath.createChildPath(cellDataName).createChildPath(pImageDataArrayNameValue); + auto& imageData = tmpDs.getDataRefAs(imageDataPath); + if(pConvertToGrayScaleValue) + { + if(imageData.getDataType() != DataType::uint8) + { + return MakePreflightErrorResult(-23504, fmt::format("The input DataType is {} which cannot be converted to grayscale. Please turn off the 'Convert To Grayscale' option.", + nx::core::DataTypeToString(imageData.getDataType()))); + } + + if(!filterListPtr->containsPlugin(k_SimplnxCorePluginId)) + { + PreflightResult errorResult = MakePreflightErrorResult(-23501, "Color to GrayScale conversion is disabled because the 'SimplnxCore' plugin was not loaded."); + return errorResult; + } + std::unique_ptr grayScaleFilter = filterListPtr->createFilter(k_ColorToGrayScaleFilterHandle); + if(nullptr == grayScaleFilter.get()) + { + PreflightResult errorResult = MakePreflightErrorResult(-23502, "Color to GrayScale conversion is disabled because the 'Color to GrayScale' filter is missing from the SimplnxCore plugin."); + return errorResult; + } + + Arguments grayscaleImageGeomArgs; + grayscaleImageGeomArgs.insertOrAssign("input_data_array_paths", std::make_any>({imageDataPath})); + grayscaleImageGeomArgs.insertOrAssign("output_array_prefix", std::make_any("grayscale_")); + grayscaleImageGeomArgs.insertOrAssign("color_weights", std::make_any(pColorWeightsValue)); + + // Run grayscale filter and process results and messages + PreflightResult grayscaleImageResult = grayScaleFilter->preflight(tmpDs, grayscaleImageGeomArgs, messageHandler, shouldCancel); + if(grayscaleImageResult.outputActions.invalid()) + { + return grayscaleImageResult; + } + Result<> actionsResult = grayscaleImageResult.outputActions.value().applyAll(tmpDs, IDataAction::Mode::Preflight); + if(actionsResult.invalid()) + { + return {ConvertResultTo(std::move(actionsResult), {})}; + } + + resultOutputActions = MergeOutputActionResults(resultOutputActions, grayscaleImageResult.outputActions); + + resultOutputActions.value().appendDeferredAction(std::make_unique(imageDataPath)); + const DataPath grayscaleImageDataPath = currentImageGeomPath.createChildPath(cellDataName).createChildPath("grayscale_" + pImageDataArrayNameValue); + resultOutputActions.value().appendDeferredAction(std::make_unique(grayscaleImageDataPath, pImageDataArrayNameValue)); + } + else + { + if(pChangeDataType && imageData.getComponentShape().at(0) != 1) + { + return MakePreflightErrorResult( + -23506, fmt::format("Changing the array type requires the input image data to be a scalar value OR the image data can be RGB but you must also select 'Convert to Grayscale'")); + } + } + + for(const DataPath& pathToDelete : pathsToDelete) + { + resultOutputActions.value().appendDeferredAction(std::make_unique(pathToDelete)); + } + + if(originSpacingProcessing == 1 && (shouldChangeOrigin || shouldChangeSpacing)) + { + std::vector originf(origin.size()); + std::ranges::transform(origin, originf.begin(), [](float64 v) { return static_cast(v); }); + std::vector spacingf(spacing.size()); + std::ranges::transform(spacing.begin(), spacing.end(), spacingf.begin(), [](float64 v) { return static_cast(v); }); + resultOutputActions.value().appendDeferredAction(std::make_unique(shouldChangeOrigin ? FloatVec3(originf) : std::optional{}, + shouldChangeSpacing ? FloatVec3(spacingf) : std::optional{}, currentImageGeomPath)); + outputSpacing = spacingf; + outputOrigin = originf; + } + + if(currentImageGeomPath != imageGeomPath) + { + resultOutputActions.value().appendDeferredAction(std::make_unique(currentImageGeomPath, imageGeomPath.getTargetName())); + } + + preflightUpdatedValues.push_back({"Output Geometry", nx::core::GeometryHelpers::Description::GenerateGeometryInfo(outputDims, outputSpacing, outputOrigin, outputUnits)}); return {std::move(resultOutputActions), std::move(preflightUpdatedValues)}; } //------------------------------------------------------------------------------ Result<> ReadImageStackFilter::executeImpl(DataStructure& dataStructure, const Arguments& filterArgs, const PipelineFilter* pipelineNode, const MessageHandler& messageHandler, - const std::atomic_bool& shouldCancel, const ExecutionContext& executionContext) const + const std::atomic_bool& shouldCancel, const ExecutionContext& executionContext) const { + auto inputFileListInfo = filterArgs.value(k_InputFileListInfo_Key); ReadImageStackInputValues inputValues; - - - + inputValues.fileList = inputFileListInfo.generate(); + inputValues.imageGeometryPath = filterArgs.value(k_ImageGeometryPath_Key); + inputValues.imageDataArrayName = filterArgs.value(k_ImageDataArrayPath_Key); + inputValues.cellDataName = filterArgs.value(k_CellDataName_Key); + inputValues.changeOrigin = filterArgs.value(k_ChangeOrigin_Key); + inputValues.origin = filterArgs.value(k_Origin_Key); + inputValues.changeSpacing = filterArgs.value(k_ChangeSpacing_Key); + inputValues.spacing = filterArgs.value(k_Spacing_Key); + inputValues.originSpacingProcessing = filterArgs.value(k_OriginSpacingProcessing_Key); + inputValues.imageTransformChoice = filterArgs.value(k_ImageTransformChoice_Key); + inputValues.convertToGrayScale = filterArgs.value(k_ConvertToGrayScale_Key); + inputValues.colorWeights = filterArgs.value(k_ColorWeights_Key); + inputValues.resampleImagesChoice = filterArgs.value(k_ResampleImagesChoice_Key); + inputValues.scaling = filterArgs.value(k_Scaling_Key); + inputValues.exactXYDimensions = filterArgs.value(k_ExactXYDimensions_Key); + inputValues.changeDataType = filterArgs.value(k_ChangeDataType_Key); + inputValues.imageDataTypeChoice = filterArgs.value(k_ImageDataType_Key); + inputValues.croppingOptions = filterArgs.value(k_CroppingOptions_Key); return ReadImageStack(dataStructure, messageHandler, shouldCancel, &inputValues)(); } @@ -281,12 +518,11 @@ namespace { namespace SIMPL { - - //TODO: PARAMETER_JSON_CONSTANTS + +// TODO: PARAMETER_JSON_CONSTANTS } // namespace SIMPL } // namespace - //------------------------------------------------------------------------------ Result ReadImageStackFilter::FromSIMPLJson(const nlohmann::json& json) { @@ -301,5 +537,4 @@ Result ReadImageStackFilter::FromSIMPLJson(const nlohmann::json& json return ConvertResultTo(std::move(conversionResult), std::move(args)); } - } // namespace nx::core From 8521f55b3d10dbaefa7476d4ac641fa5198ab974 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Fri, 10 Apr 2026 00:27:19 -0400 Subject: [PATCH 08/16] Implement unit tests for image I/O filters Tests mirror the ITK test patterns for ReadImageFilter, WriteImageFilter, and ReadImageStackFilter. Use same test data archives as the ITK tests. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/Plugins/SimplnxCore/test/CMakeLists.txt | 3 + .../SimplnxCore/test/ReadImageStackTest.cpp | 1223 ++++++++++++++++- .../SimplnxCore/test/ReadImageTest.cpp | 523 ++++++- .../SimplnxCore/test/WriteImageTest.cpp | 243 +++- 4 files changed, 1825 insertions(+), 167 deletions(-) diff --git a/src/Plugins/SimplnxCore/test/CMakeLists.txt b/src/Plugins/SimplnxCore/test/CMakeLists.txt index 265e09e869..bf520cf7bc 100644 --- a/src/Plugins/SimplnxCore/test/CMakeLists.txt +++ b/src/Plugins/SimplnxCore/test/CMakeLists.txt @@ -255,7 +255,10 @@ if(EXISTS "${DREAM3D_DATA_DIR}" AND SIMPLNX_DOWNLOAD_TEST_FILES) download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME generate_vector_colors.tar.gz SHA512 a30ac9ac35002f4a79b60a02a86de1661c4828c77b45d3848c6e2c37943c8874756f0e163cda99f2c0de372ff1cd9257288d5357fa3db8a539428df713325ee7) download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME identify_sample.tar.gz SHA512 0c1da22d411ac739d3e90618141a6eab71705b47de6d4cc7501e9408bf6fcbadd46738f5e86a80ab2e0bc2344c23584cc2b89f2102c13073490e1817797ec9bc) download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME image_coords_test.tar.gz SHA512 bca23117b00e77e0401b83098caba6c4586f3d7068eb5b513da1be365a3b56aabe4a27106cb1e30c5f06192c277fb7cce208e7b180f93807d72f5f2e01e26c43) + download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME image_flip_test_images.tar.gz SHA512 4e282a270251133004bf4b979d0d064631b618fc82f503184c602c40d388b725f81faf8e77654d285852acc3217d51534c9a71240be4a87a91dc46da7871e7d2) + download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME import_image_stack_test_v2.tar.gz SHA512 b3600c072ecbdb27ed3ed7298dac88708aa94d1eed21e6b0581b772311d1e3c2c6693713026ae512c86729079b41c629067fb1a7df75697d08dbdf2dfad0f553) download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME initialize_data_test_files.tar.gz SHA512 f04fe76ef96add4b775111ae7fc95233a7748a25501d9f00a8b2a162c87782b8cd2813e6e46ba7892e721976693da06965e624335dbb28224c9c5b877a05aa49) + download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME itk_image_reader_test.tar.gz SHA512 15c42c913df8e0aebfc4196ea96ed5fdc2fab71daacbaf27287d6aee08483acb37f2f38d88af6c549084bae892b07c7aca8537b2511920d18248c8bc18aab92f) download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME k_files_v2.tar.gz SHA512 d5479ad573f8ce582e32c9f7a4824885b965f08cd8ed6025daf383758088716b1e9109905ab830b9f4d4f9df0145cbeb2534258237846c47e4a046b24fb3f72c) download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME label_triangle_geometry_test_v2.tar.gz SHA512 3f2009a1e37d8f5c983ee125335f42900f364653a417d8c49c9409a41a032c6f77a80b0623f5c71b1ae736f528d3b07949f7703bc342876f5424d884c3d4226c) download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME LosAlamosFFTExemplar.tar.gz SHA512 c4f2f6cbdaea4512ca2911cb0c3169e62449fcdb7cb99a5e64640601239a08cc09ea35f81f17b056fd38da85add95a37b244b7f844fe0abc944c1ba2f7514812) diff --git a/src/Plugins/SimplnxCore/test/ReadImageStackTest.cpp b/src/Plugins/SimplnxCore/test/ReadImageStackTest.cpp index 6ee1ef912a..f46aca4b63 100644 --- a/src/Plugins/SimplnxCore/test/ReadImageStackTest.cpp +++ b/src/Plugins/SimplnxCore/test/ReadImageStackTest.cpp @@ -1,86 +1,1205 @@ -/** - * This file is auto generated from the original SimplnxCore/ReadImageStackFilter - * runtime information. These are the steps that need to be taken to utilize this - * unit test in the proper way. - * - * 1: Validate each of the default parameters that gets created. - * 2: Inspect the actual filter to determine if the filter in its default state - * would pass or fail BOTH the preflight() and execute() methods - * 3: UPDATE the ```REQUIRE(result.result.valid());``` code to have the proper - * - * 4: Add additional unit tests to actually test each code path within the filter - * - * There are some example Catch2 ```TEST_CASE``` sections for your inspiration. - * - * NOTE the format of the ```TEST_CASE``` macro. Please stick to this format to - * allow easier parsing of the unit tests. - * - * When you start working on this unit test remove "[ReadImageStackFilter][.][UNIMPLEMENTED]" - * from the TEST_CASE macro. This will enable this unit test to be run by default - * and report errors. - */ - - #include - - //TODO: PARAMETER_INCLUDES +#include "SimplnxCore/Filters/ReadImageFilter.hpp" #include "SimplnxCore/Filters/ReadImageStackFilter.hpp" #include "SimplnxCore/SimplnxCore_test_dirs.hpp" +#include "simplnx/Common/Types.hpp" +#include "simplnx/Core/Application.hpp" +#include "simplnx/DataStructure/Geometry/ImageGeom.hpp" +#include "simplnx/Parameters/ChoicesParameter.hpp" +#include "simplnx/Parameters/CropGeometryParameter.hpp" +#include "simplnx/Parameters/DataObjectNameParameter.hpp" +#include "simplnx/Parameters/GeneratedFileListParameter.hpp" +#include "simplnx/Parameters/VectorParameter.hpp" #include "simplnx/UnitTest/UnitTestCommon.hpp" -#include - using namespace nx::core; using namespace nx::core::UnitTest; -using namespace nx::core::Constants; -TEST_CASE("SimplnxCore::ReadImageStackFilter: Valid Filter Execution","[SimplnxCore][ReadImageStackFilter]") +namespace fs = std::filesystem; + +namespace +{ +// The read-image-stack test re-uses the ImageStack data from the ITKImageProcessing plugin source +// tree. See the note in WriteImageTest.cpp for why we reach across plugin boundaries here. +const std::string k_ImageStackDir = std::string(unit_test::k_SimplnxSourceDIr.view()) + "/src/Plugins/ITKImageProcessing/data/ImageStack"; +const DataPath k_ImageGeomPath = {{"ImageGeometry"}}; +const DataPath k_ImageDataPath = k_ImageGeomPath.createChildPath(ImageGeom::k_CellAttributeMatrixName).createChildPath("ImageData"); +const std::string k_FlippedImageStackDirName = "image_flip_test_images"; +const DataPath k_XGeneratedImageGeomPath = DataPath({"xGeneratedImageGeom"}); +const DataPath k_YGeneratedImageGeomPath = DataPath({"yGeneratedImageGeom"}); +const DataPath k_XFlipImageGeomPath = DataPath({"xFlipImageGeom"}); +const DataPath k_YFlipImageGeomPath = DataPath({"yFlipImageGeom"}); +const std::string k_ImageDataName = "ImageData"; +const ChoicesParameter::ValueType k_NoImageTransform = 0; +const ChoicesParameter::ValueType k_FlipAboutXAxis = 1; +const ChoicesParameter::ValueType k_FlipAboutYAxis = 2; +const fs::path k_ImageFlipStackDir = fs::path(fmt::format("{}/{}", unit_test::k_TestFilesDir, k_FlippedImageStackDirName)); + +// Exemplar Array Paths +const DataPath k_XFlippedImageDataPath = k_XFlipImageGeomPath.createChildPath(Constants::k_Cell_Data).createChildPath(::k_ImageDataName); +const DataPath k_YFlippedImageDataPath = k_YFlipImageGeomPath.createChildPath(Constants::k_Cell_Data).createChildPath(::k_ImageDataName); + +void ExecuteImportImageStackXY(DataStructure& dataStructure, const std::string& filePrefix) { UnitTest::LoadPlugins(); - const nx::core::UnitTest::TestFileSentinel testDataSentinel( - nx::core::unit_test::k_TestFilesDir, - "ReadImageStackFilter.tar.gz", - "ReadImageStackFilter"); + // Define Shared parameters + std::vector k_Origin = {0.0f, 0.0f, 0.0f}; + std::vector k_Spacing = {1.0f, 1.0f, 1.0f}; + GeneratedFileListParameter::ValueType k_FileListInfo; - // Read the Small IN100 Data set - auto baseDataFilePath = fs::path(fmt::format("{}/ReadImageStackFilter/ReadImageStackFilter.dream3d", nx::core::unit_test::k_TestFilesDir)); - DataStructure dataStructure = UnitTest::LoadDataStructure(baseDataFilePath); + // Set File list for reads + { + k_FileListInfo.inputPath = k_ImageFlipStackDir.string(); + k_FileListInfo.startIndex = 1; + k_FileListInfo.endIndex = 1; + k_FileListInfo.incrementIndex = 1; + k_FileListInfo.fileExtension = ".tiff"; + k_FileListInfo.filePrefix = filePrefix; + k_FileListInfo.fileSuffix = ""; + k_FileListInfo.paddingDigits = 1; + k_FileListInfo.ordering = GeneratedFileListParameter::Ordering::LowToHigh; + } - // Instantiate the filter, a DataStructure object and an Arguments Object + // Run generated X flip { + ReadImageStackFilter filter; Arguments args; + + args.insertOrAssign(ReadImageStackFilter::k_Origin_Key, std::make_any>(k_Origin)); + args.insertOrAssign(ReadImageStackFilter::k_Spacing_Key, std::make_any>(k_Spacing)); + args.insertOrAssign(ReadImageStackFilter::k_InputFileListInfo_Key, std::make_any(k_FileListInfo)); + args.insertOrAssign(ReadImageStackFilter::k_ImageGeometryPath_Key, std::make_any(::k_XGeneratedImageGeomPath)); + args.insertOrAssign(ReadImageStackFilter::k_ImageTransformChoice_Key, std::make_any(::k_FlipAboutXAxis)); + + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions) + + auto executeResult = filter.execute(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result) + } + + // Run generated Y flip + { ReadImageStackFilter filter; + Arguments args; + + args.insertOrAssign(ReadImageStackFilter::k_Origin_Key, std::make_any>(k_Origin)); + args.insertOrAssign(ReadImageStackFilter::k_Spacing_Key, std::make_any>(k_Spacing)); + args.insertOrAssign(ReadImageStackFilter::k_InputFileListInfo_Key, std::make_any(k_FileListInfo)); + args.insertOrAssign(ReadImageStackFilter::k_ImageGeometryPath_Key, std::make_any(::k_YGeneratedImageGeomPath)); + args.insertOrAssign(ReadImageStackFilter::k_ImageTransformChoice_Key, std::make_any(::k_FlipAboutYAxis)); + + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions) + + auto executeResult = filter.execute(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result) + } +} + +void ReadInFlippedXYExemplars(DataStructure& dataStructure, const std::string& filePrefix) +{ + { + ReadImageFilter filter; + Arguments args; + + fs::path filePath = k_ImageFlipStackDir / (filePrefix + "flip_x.tiff"); + args.insertOrAssign(ReadImageFilter::k_FileName_Key, filePath); + args.insertOrAssign(ReadImageFilter::k_ImageGeometryPath_Key, ::k_XFlipImageGeomPath); + args.insertOrAssign(ReadImageFilter::k_ImageDataArrayPath_Key, static_cast(::k_ImageDataName)); + + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions) - // Create default Parameters for the filter. - // This next line is an example, your filter may be different - // args.insertOrAssign(ReadImageStackFilter::k_CreatedTriangleGeometryPath_Key, std::make_any(computedTriangleGeomPath)); + auto executeResult = filter.execute(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result) + } + { + ReadImageFilter filter; + Arguments args; + + fs::path filePath = k_ImageFlipStackDir / (filePrefix + "flip_y.tiff"); + args.insertOrAssign(ReadImageFilter::k_FileName_Key, filePath); + args.insertOrAssign(ReadImageFilter::k_ImageGeometryPath_Key, ::k_YFlipImageGeomPath); + args.insertOrAssign(ReadImageFilter::k_ImageDataArrayPath_Key, static_cast(::k_ImageDataName)); - // Preflight the filter and check result auto preflightResult = filter.preflight(dataStructure, args); SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions) - // Execute the filter and check the result auto executeResult = filter.execute(dataStructure, args); SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result) + } +} + +void CompareXYFlippedGeometries(DataStructure& dataStructure) +{ + UnitTest::CompareImageGeometry(dataStructure, ::k_XFlipImageGeomPath, k_XGeneratedImageGeomPath); + UnitTest::CompareImageGeometry(dataStructure, ::k_YFlipImageGeomPath, k_YGeneratedImageGeomPath); + + // Processed + DataPath k_XGeneratedImageDataPath = k_XGeneratedImageGeomPath.createChildPath(Constants::k_Cell_Data).createChildPath(::k_ImageDataName); + DataPath k_YGeneratedImageDataPath = k_YGeneratedImageGeomPath.createChildPath(Constants::k_Cell_Data).createChildPath(::k_ImageDataName); + const auto& xGeneratedImageData = dataStructure.getDataRefAs(k_XGeneratedImageDataPath); + const auto& yGeneratedImageData = dataStructure.getDataRefAs(k_YGeneratedImageDataPath); + + // Exemplar + const auto& xFlippedImageData = dataStructure.getDataRefAs(k_XFlippedImageDataPath); + const auto& yFlippedImageData = dataStructure.getDataRefAs(k_YFlippedImageDataPath); + + UnitTest::CompareDataArrays(xGeneratedImageData, xFlippedImageData); + UnitTest::CompareDataArrays(yGeneratedImageData, yFlippedImageData); +} + +// Test data paths +const std::string k_TestDataDirName = "import_image_stack_test"; +const fs::path k_TestDataDir = fs::path(unit_test::k_TestFilesDir.view()) / k_TestDataDirName; +const fs::path k_InputImagesDir = k_TestDataDir / "input_images"; +const fs::path k_ExemplarFile = k_TestDataDir / "import_image_stack_test.dream3d"; + +// Standard test parameters +const std::string k_FilePrefix = "200x200_"; +const std::string k_FileExtension = ".tif"; + +// Cropping boundaries (crop to the colored square: 50:150 in X/Y, all Z) +const IntVec2Type k_VoxelCropX = {50, 150}; +const IntVec2Type k_VoxelCropY = {50, 150}; +const IntVec2Type k_VoxelCropZ = {0, 1}; + +const FloatVec2Type k_PhysicalCropX = {50.0f, 150.0f}; +const FloatVec2Type k_PhysicalCropY = {50.0f, 150.0f}; +const FloatVec2Type k_PhysicalCropZ = {0.0f, 1.0f}; + +// Resampling/flip/timing constants +const ChoicesParameter::ValueType k_NoResample = 0; +const ChoicesParameter::ValueType k_ScalingFactor = 1; +const ChoicesParameter::ValueType k_ExactDimensions = 2; +const ChoicesParameter::ValueType k_NoFlip = 0; +const ChoicesParameter::ValueType k_FlipX = 1; +const ChoicesParameter::ValueType k_FlipY = 2; +const ChoicesParameter::ValueType k_Preprocessed = 0; +const ChoicesParameter::ValueType k_Postprocessed = 1; + +/** + * @brief Helper to create standard file list for test images + */ +GeneratedFileListParameter::ValueType CreateStandardFileList() +{ + GeneratedFileListParameter::ValueType fileList; + fileList.inputPath = k_InputImagesDir.string(); + fileList.filePrefix = k_FilePrefix; + fileList.fileSuffix = ""; + fileList.fileExtension = k_FileExtension; + fileList.startIndex = 0; + fileList.endIndex = 2; + fileList.incrementIndex = 1; + fileList.paddingDigits = 1; + fileList.ordering = GeneratedFileListParameter::Ordering::LowToHigh; + return fileList; +} + +/** + * @brief Helper to create cropping options + */ +CropGeometryParameter::ValueType CreateCropOptions(CropGeometryParameter::CropValues::TypeEnum type, bool cropX, bool cropY, bool cropZ) +{ + CropGeometryParameter::ValueType crop; + crop.type = type; + crop.cropX = cropX; + crop.cropY = cropY; + crop.cropZ = cropZ; + + if(type == CropGeometryParameter::CropValues::TypeEnum::VoxelSubvolume) + { + crop.xBoundVoxels = k_VoxelCropX; + crop.yBoundVoxels = k_VoxelCropY; + crop.zBoundVoxels = k_VoxelCropZ; + } + else if(type == CropGeometryParameter::CropValues::TypeEnum::PhysicalSubvolume) + { + crop.xBoundPhysical = k_PhysicalCropX; + crop.yBoundPhysical = k_PhysicalCropY; + crop.zBoundPhysical = k_PhysicalCropZ; + } + + return crop; +} + +/** + * @brief Execute filter with standard parameters + custom overrides + */ +Result<> ExecuteImportImageStack(DataStructure& dataStructure, const DataPath& outputGeomPath, const CropGeometryParameter::ValueType& cropOptions = {}, + ChoicesParameter::ValueType resampleMode = k_NoResample, float32 scalingFactor = 100.0f, const VectorUInt64Parameter::ValueType& exactDims = {200, 200}, + ChoicesParameter::ValueType flipMode = k_NoFlip, bool convertToGrayscale = false, bool changeOrigin = false, const std::vector& origin = {0.0, 0.0, 0.0}, + bool changeSpacing = false, const std::vector& spacing = {1.0, 1.0, 1.0}, ChoicesParameter::ValueType originSpacingTiming = k_Postprocessed) +{ + ReadImageStackFilter filter; + Arguments args; + + auto fileList = CreateStandardFileList(); + + args.insertOrAssign(ReadImageStackFilter::k_InputFileListInfo_Key, fileList); + args.insertOrAssign(ReadImageStackFilter::k_ImageGeometryPath_Key, outputGeomPath); + args.insertOrAssign(ReadImageStackFilter::k_CroppingOptions_Key, cropOptions); + args.insertOrAssign(ReadImageStackFilter::k_ResampleImagesChoice_Key, resampleMode); + args.insertOrAssign(ReadImageStackFilter::k_ImageTransformChoice_Key, flipMode); + args.insertOrAssign(ReadImageStackFilter::k_ConvertToGrayScale_Key, convertToGrayscale); + args.insertOrAssign(ReadImageStackFilter::k_ChangeOrigin_Key, changeOrigin); + args.insertOrAssign(ReadImageStackFilter::k_Origin_Key, origin); + args.insertOrAssign(ReadImageStackFilter::k_ChangeSpacing_Key, changeSpacing); + args.insertOrAssign(ReadImageStackFilter::k_Spacing_Key, spacing); + args.insertOrAssign(ReadImageStackFilter::k_OriginSpacingProcessing_Key, originSpacingTiming); + + if(resampleMode == k_ScalingFactor) + { + args.insertOrAssign(ReadImageStackFilter::k_Scaling_Key, scalingFactor); + } + else if(resampleMode == k_ExactDimensions) + { + args.insertOrAssign(ReadImageStackFilter::k_ExactXYDimensions_Key, exactDims); + } + + auto preflightResult = filter.preflight(dataStructure, args); + if(preflightResult.outputActions.invalid()) + { + return ConvertResult(std::move(preflightResult.outputActions)); + } + + auto executeResult = filter.execute(dataStructure, args); + return executeResult.result; +} + +/** + * @brief Verify expected geometry dimensions + */ +void VerifyGeometryDimensions(const DataStructure& ds, const DataPath& geomPath, usize expectedX, usize expectedY, usize expectedZ) +{ + const auto* geom = ds.getDataAs(geomPath); + REQUIRE(geom != nullptr); + + SizeVec3 dims = geom->getDimensions(); + REQUIRE(dims[0] == expectedX); + REQUIRE(dims[1] == expectedY); + REQUIRE(dims[2] == expectedZ); +} + +/** + * @brief Verify origin and spacing + */ +void VerifyOriginSpacing(const DataStructure& ds, const DataPath& geomPath, const FloatVec3& expectedOrigin, const FloatVec3& expectedSpacing) +{ + const auto* geom = ds.getDataAs(geomPath); + REQUIRE(geom != nullptr); + + FloatVec3 origin = geom->getOrigin(); + FloatVec3 spacing = geom->getSpacing(); + + REQUIRE(origin[0] == Approx(expectedOrigin[0])); + REQUIRE(origin[1] == Approx(expectedOrigin[1])); + REQUIRE(origin[2] == Approx(expectedOrigin[2])); + + REQUIRE(spacing[0] == Approx(expectedSpacing[0])); + REQUIRE(spacing[1] == Approx(expectedSpacing[1])); + REQUIRE(spacing[2] == Approx(expectedSpacing[2])); +} + +} // namespace + +TEST_CASE("SimplnxCore::ReadImageStackFilter: NoInput", "[SimplnxCore][ReadImageStackFilter]") +{ + ReadImageStackFilter filter; + DataStructure dataStructure; + Arguments args; + + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions) + + UnitTest::CheckArraysInheritTupleDims(dataStructure); +} + +TEST_CASE("SimplnxCore::ReadImageStackFilter: NoImageGeometry", "[SimplnxCore][ReadImageStackFilter]") +{ + ReadImageStackFilter filter; + DataStructure dataStructure; + Arguments args; + + GeneratedFileListParameter::ValueType fileListInfo; + + fileListInfo.inputPath = k_ImageStackDir; + + args.insertOrAssign(ReadImageStackFilter::k_InputFileListInfo_Key, std::make_any(fileListInfo)); + + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions) + + UnitTest::CheckArraysInheritTupleDims(dataStructure); +} + +TEST_CASE("SimplnxCore::ReadImageStackFilter: NoFiles", "[SimplnxCore][ReadImageStackFilter]") +{ + ReadImageStackFilter filter; + DataStructure dataStructure; + Arguments args; + + GeneratedFileListParameter::ValueType fileListInfo; + fileListInfo.inputPath = "doesNotExist.ghost"; + fileListInfo.startIndex = 75; + fileListInfo.endIndex = 77; + fileListInfo.fileExtension = "dcm"; + fileListInfo.filePrefix = "Image"; + fileListInfo.fileSuffix = ""; + fileListInfo.paddingDigits = 4; + + args.insertOrAssign(ReadImageStackFilter::k_InputFileListInfo_Key, std::make_any(fileListInfo)); + args.insertOrAssign(ReadImageStackFilter::k_Origin_Key, std::make_any>(3)); + args.insertOrAssign(ReadImageStackFilter::k_Spacing_Key, std::make_any>(3)); + args.insertOrAssign(ReadImageStackFilter::k_ImageGeometryPath_Key, std::make_any(k_ImageGeomPath)); + + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions) + + UnitTest::CheckArraysInheritTupleDims(dataStructure); +} + +TEST_CASE("SimplnxCore::ReadImageStackFilter: FileDoesNotExist", "[SimplnxCore][ReadImageStackFilter]") +{ + ReadImageStackFilter filter; + DataStructure dataStructure; + Arguments args; + + GeneratedFileListParameter::ValueType fileListInfo; + fileListInfo.inputPath = k_ImageStackDir; + fileListInfo.startIndex = 75; + fileListInfo.endIndex = 79; + fileListInfo.fileExtension = "dcm"; + fileListInfo.filePrefix = "Image"; + fileListInfo.fileSuffix = ""; + fileListInfo.paddingDigits = 4; + + args.insertOrAssign(ReadImageStackFilter::k_InputFileListInfo_Key, std::make_any(fileListInfo)); + args.insertOrAssign(ReadImageStackFilter::k_Origin_Key, std::make_any>(3)); + args.insertOrAssign(ReadImageStackFilter::k_Spacing_Key, std::make_any>(3)); + args.insertOrAssign(ReadImageStackFilter::k_ImageGeometryPath_Key, std::make_any(k_ImageGeomPath)); + + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions) + + UnitTest::CheckArraysInheritTupleDims(dataStructure); +} + +TEST_CASE("SimplnxCore::ReadImageStackFilter: CompareImage", "[SimplnxCore][ReadImageStackFilter]") +{ + UnitTest::LoadPlugins(); + + ReadImageStackFilter filter; + DataStructure dataStructure; + Arguments args; + + GeneratedFileListParameter::ValueType fileListInfo; + fileListInfo.inputPath = k_ImageStackDir; + fileListInfo.startIndex = 11; + fileListInfo.endIndex = 13; + fileListInfo.incrementIndex = 1; + fileListInfo.fileExtension = ".tif"; + fileListInfo.filePrefix = "slice_"; + fileListInfo.fileSuffix = ""; + fileListInfo.paddingDigits = 2; + fileListInfo.ordering = GeneratedFileListParameter::Ordering::LowToHigh; + + std::vector origin = {1.0f, 4.0f, 8.0f}; + std::vector spacing = {0.3f, 0.2f, 0.9f}; + + args.insertOrAssign(ReadImageStackFilter::k_InputFileListInfo_Key, std::make_any(fileListInfo)); + args.insertOrAssign(ReadImageStackFilter::k_ChangeOrigin_Key, true); + args.insertOrAssign(ReadImageStackFilter::k_Origin_Key, std::make_any>(origin)); + args.insertOrAssign(ReadImageStackFilter::k_ChangeSpacing_Key, true); + args.insertOrAssign(ReadImageStackFilter::k_Spacing_Key, std::make_any>(spacing)); + args.insertOrAssign(ReadImageStackFilter::k_ImageGeometryPath_Key, std::make_any(k_ImageGeomPath)); + + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions) + + auto executeResult = filter.execute(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result) + + const auto* imageGeomPtr = dataStructure.getDataAs(k_ImageGeomPath); + REQUIRE(imageGeomPtr != nullptr); + + SizeVec3 imageDims = imageGeomPtr->getDimensions(); + FloatVec3 imageOrigin = imageGeomPtr->getOrigin(); + FloatVec3 imageSpacing = imageGeomPtr->getSpacing(); + + std::array dims = {524, 390, 3}; + + REQUIRE(imageDims[0] == dims[0]); + REQUIRE(imageDims[1] == dims[1]); + REQUIRE(imageDims[2] == dims[2]); + + REQUIRE(imageOrigin[0] == Approx(origin[0])); + REQUIRE(imageOrigin[1] == Approx(origin[1])); + REQUIRE(imageOrigin[2] == Approx(origin[2])); + + REQUIRE(imageSpacing[0] == Approx(spacing[0])); + REQUIRE(imageSpacing[1] == Approx(spacing[1])); + REQUIRE(imageSpacing[2] == Approx(spacing[2])); + + const auto* imageDataPtr = dataStructure.getDataAs(k_ImageDataPath); + REQUIRE(imageDataPtr != nullptr); + + UnitTest::CheckArraysInheritTupleDims(dataStructure); +} + +TEST_CASE("SimplnxCore::ReadImageStackFilter: Flipped Image Even-Even X/Y", "[SimplnxCore][ReadImageStackFilter]") +{ + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "image_flip_test_images.tar.gz", k_FlippedImageStackDirName); + + const std::string filePrefix = "image_flip_even_even_"; + + DataStructure dataStructure; + + // Generate XY Image Geometries with ReadImageStackFilter + ::ExecuteImportImageStackXY(dataStructure, filePrefix); + + // Read in exemplars + ::ReadInFlippedXYExemplars(dataStructure, filePrefix); - // Write the DataStructure out to the file system #ifdef SIMPLNX_WRITE_TEST_OUTPUT - WriteTestDataStructure(dataStructure, fs::path(fmt::format("{}/ReadImageStackFilterTest.dream3d", unit_test::k_BinaryTestOutputDir))); + UnitTest::WriteTestDataStructure(dataStructure, fmt::format("{}/even_even_import_image_stack_test.dream3d", unit_test::k_BinaryTestOutputDir)); #endif - } - // Compare exemplar data arrays with computed data arrays - //TODO: Insert verification codes + // Compare against exemplars + ::CompareXYFlippedGeometries(dataStructure); + + UnitTest::CheckArraysInheritTupleDims(dataStructure); +} + +TEST_CASE("SimplnxCore::ReadImageStackFilter: Flipped Image Even-Odd X/Y", "[SimplnxCore][ReadImageStackFilter]") +{ + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "image_flip_test_images.tar.gz", k_FlippedImageStackDirName); + + const std::string filePrefix = "image_flip_even_odd_"; + + DataStructure dataStructure; + + // Generate XY Image Geometries with ReadImageStackFilter + ::ExecuteImportImageStackXY(dataStructure, filePrefix); + + // Read in exemplars + ::ReadInFlippedXYExemplars(dataStructure, filePrefix); + +#ifdef SIMPLNX_WRITE_TEST_OUTPUT + UnitTest::WriteTestDataStructure(dataStructure, fmt::format("{}/even_odd_import_image_stack_test.dream3d", unit_test::k_BinaryTestOutputDir)); +#endif + + // Compare against exemplars + ::CompareXYFlippedGeometries(dataStructure); - // This should be in every unit test. If you think it does not apply, please review with another engineer UnitTest::CheckArraysInheritTupleDims(dataStructure); +} + +TEST_CASE("SimplnxCore::ReadImageStackFilter: Flipped Image Odd-Even X/Y", "[SimplnxCore][ReadImageStackFilter]") +{ + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "image_flip_test_images.tar.gz", k_FlippedImageStackDirName); + + const std::string filePrefix = "image_flip_odd_even_"; + + DataStructure dataStructure; + + ::ExecuteImportImageStackXY(dataStructure, filePrefix); + ::ReadInFlippedXYExemplars(dataStructure, filePrefix); + +#ifdef SIMPLNX_WRITE_TEST_OUTPUT + UnitTest::WriteTestDataStructure(dataStructure, fmt::format("{}/odd_even_import_image_stack_test.dream3d", unit_test::k_BinaryTestOutputDir)); +#endif + ::CompareXYFlippedGeometries(dataStructure); + + UnitTest::CheckArraysInheritTupleDims(dataStructure); } -//TEST_CASE("SimplnxCore::ReadImageStackFilter: InValid Filter Execution") -//{ -// -//} +TEST_CASE("SimplnxCore::ReadImageStackFilter: Flipped Image Odd-Odd X/Y", "[SimplnxCore][ReadImageStackFilter]") +{ + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "image_flip_test_images.tar.gz", k_FlippedImageStackDirName); + + const std::string filePrefix = "image_flip_odd_odd_"; + + DataStructure dataStructure; + + ::ExecuteImportImageStackXY(dataStructure, filePrefix); + ::ReadInFlippedXYExemplars(dataStructure, filePrefix); + +#ifdef SIMPLNX_WRITE_TEST_OUTPUT + UnitTest::WriteTestDataStructure(dataStructure, fmt::format("{}/odd_odd_import_image_stack_test.dream3d", unit_test::k_BinaryTestOutputDir)); +#endif + + ::CompareXYFlippedGeometries(dataStructure); + + UnitTest::CheckArraysInheritTupleDims(dataStructure); +} + +TEST_CASE("SimplnxCore::ReadImageStackFilter::Baseline_NoProcessing", "[SimplnxCore][ReadImageStackFilter][Baseline]") +{ + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + + UnitTest::LoadPlugins(); + DataStructure ds; + + const DataPath geomPath({"Baseline_Geometry"}); + auto result = ExecuteImportImageStack(ds, geomPath); + + SIMPLNX_RESULT_REQUIRE_VALID(result); + + VerifyGeometryDimensions(ds, geomPath, 200, 200, 3); + VerifyOriginSpacing(ds, geomPath, {0.0f, 0.0f, 0.0f}, {1.0f, 1.0f, 1.0f}); + + // Compare against exemplar + DataStructure exemplarDS = UnitTest::LoadDataStructure(k_ExemplarFile); + const auto* generatedGeom = ds.getDataAs(geomPath); + const auto* exemplarGeom = exemplarDS.getDataAs(DataPath({"Baseline_Geometry"})); + UnitTest::CompareImageGeometry(exemplarGeom, generatedGeom); + + DataPath generatedDataPath = geomPath.createChildPath(Constants::k_Cell_Data).createChildPath(k_ImageDataName); + DataPath exemplarDataPath = DataPath({"Baseline_Geometry", Constants::k_Cell_Data, k_ImageDataName}); + const auto& generatedArray = ds.getDataRefAs(generatedDataPath); + const auto& exemplarArray = exemplarDS.getDataRefAs(exemplarDataPath); + UnitTest::CompareDataArrays(exemplarArray, generatedArray); +} + +// ============================================================================= +// CROPPING TESTS +// ============================================================================= + +TEST_CASE("SimplnxCore::ReadImageStackFilter::Crop_Voxel_XOnly", "[SimplnxCore][ReadImageStackFilter][Cropping]") +{ + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + + UnitTest::LoadPlugins(); + DataStructure ds; + + const DataPath geomPath({"Crop_Voxel_X"}); + auto cropOptions = CreateCropOptions(CropGeometryParameter::CropValues::TypeEnum::VoxelSubvolume, true, false, false); + + auto result = ExecuteImportImageStack(ds, geomPath, cropOptions); + SIMPLNX_RESULT_REQUIRE_VALID(result); + + VerifyGeometryDimensions(ds, geomPath, 101, 200, 3); + + DataStructure exemplarDS = UnitTest::LoadDataStructure(k_ExemplarFile); + const auto* generatedGeom = ds.getDataAs(geomPath); + const auto* exemplarGeom = exemplarDS.getDataAs(DataPath({"Crop_Voxel_X"})); + UnitTest::CompareImageGeometry(exemplarGeom, generatedGeom); + + DataPath generatedDataPath = geomPath.createChildPath(Constants::k_Cell_Data).createChildPath(k_ImageDataName); + DataPath exemplarDataPath = DataPath({"Crop_Voxel_X", Constants::k_Cell_Data, k_ImageDataName}); + const auto& generatedArray = ds.getDataRefAs(generatedDataPath); + const auto& exemplarArray = exemplarDS.getDataRefAs(exemplarDataPath); + UnitTest::CompareDataArrays(exemplarArray, generatedArray); +} + +TEST_CASE("SimplnxCore::ReadImageStackFilter::Crop_Voxel_YOnly", "[SimplnxCore][ReadImageStackFilter][Cropping]") +{ + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + + UnitTest::LoadPlugins(); + DataStructure ds; + + const DataPath geomPath({"Crop_Voxel_Y"}); + auto cropOptions = CreateCropOptions(CropGeometryParameter::CropValues::TypeEnum::VoxelSubvolume, false, true, false); + + auto result = ExecuteImportImageStack(ds, geomPath, cropOptions); + SIMPLNX_RESULT_REQUIRE_VALID(result); + + VerifyGeometryDimensions(ds, geomPath, 200, 101, 3); + + DataStructure exemplarDS = UnitTest::LoadDataStructure(k_ExemplarFile); + const auto* generatedGeom = ds.getDataAs(geomPath); + const auto* exemplarGeom = exemplarDS.getDataAs(DataPath({"Crop_Voxel_Y"})); + UnitTest::CompareImageGeometry(exemplarGeom, generatedGeom); + + DataPath generatedDataPath = geomPath.createChildPath(Constants::k_Cell_Data).createChildPath(k_ImageDataName); + DataPath exemplarDataPath = DataPath({"Crop_Voxel_Y", Constants::k_Cell_Data, k_ImageDataName}); + const auto& generatedArray = ds.getDataRefAs(generatedDataPath); + const auto& exemplarArray = exemplarDS.getDataRefAs(exemplarDataPath); + UnitTest::CompareDataArrays(exemplarArray, generatedArray); +} + +TEST_CASE("SimplnxCore::ReadImageStackFilter::Crop_Voxel_ZOnly", "[SimplnxCore][ReadImageStackFilter][Cropping]") +{ + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + + UnitTest::LoadPlugins(); + DataStructure ds; + + const DataPath geomPath({"Crop_Voxel_Z"}); + auto cropOptions = CreateCropOptions(CropGeometryParameter::CropValues::TypeEnum::VoxelSubvolume, false, false, true); + + auto result = ExecuteImportImageStack(ds, geomPath, cropOptions); + SIMPLNX_RESULT_REQUIRE_VALID(result); + + VerifyGeometryDimensions(ds, geomPath, 200, 200, 2); + + DataStructure exemplarDS = UnitTest::LoadDataStructure(k_ExemplarFile); + const auto* generatedGeom = ds.getDataAs(geomPath); + const auto* exemplarGeom = exemplarDS.getDataAs(DataPath({"Crop_Voxel_Z"})); + UnitTest::CompareImageGeometry(exemplarGeom, generatedGeom); + + DataPath generatedDataPath = geomPath.createChildPath(Constants::k_Cell_Data).createChildPath(k_ImageDataName); + DataPath exemplarDataPath = DataPath({"Crop_Voxel_Z", Constants::k_Cell_Data, k_ImageDataName}); + const auto& generatedArray = ds.getDataRefAs(generatedDataPath); + const auto& exemplarArray = exemplarDS.getDataRefAs(exemplarDataPath); + UnitTest::CompareDataArrays(exemplarArray, generatedArray); +} + +TEST_CASE("SimplnxCore::ReadImageStackFilter::Crop_Voxel_XY", "[SimplnxCore][ReadImageStackFilter][Cropping]") +{ + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + + UnitTest::LoadPlugins(); + DataStructure ds; + + const DataPath geomPath({"Crop_Voxel_XY"}); + auto cropOptions = CreateCropOptions(CropGeometryParameter::CropValues::TypeEnum::VoxelSubvolume, true, true, false); + + auto result = ExecuteImportImageStack(ds, geomPath, cropOptions); + SIMPLNX_RESULT_REQUIRE_VALID(result); + + VerifyGeometryDimensions(ds, geomPath, 101, 101, 3); + + DataStructure exemplarDS = UnitTest::LoadDataStructure(k_ExemplarFile); + const auto* generatedGeom = ds.getDataAs(geomPath); + const auto* exemplarGeom = exemplarDS.getDataAs(DataPath({"Crop_Voxel_XY"})); + UnitTest::CompareImageGeometry(exemplarGeom, generatedGeom); + + DataPath generatedDataPath = geomPath.createChildPath(Constants::k_Cell_Data).createChildPath(k_ImageDataName); + DataPath exemplarDataPath = DataPath({"Crop_Voxel_XY", Constants::k_Cell_Data, k_ImageDataName}); + const auto& generatedArray = ds.getDataRefAs(generatedDataPath); + const auto& exemplarArray = exemplarDS.getDataRefAs(exemplarDataPath); + UnitTest::CompareDataArrays(exemplarArray, generatedArray); +} + +TEST_CASE("SimplnxCore::ReadImageStackFilter::Crop_Voxel_XYZ", "[SimplnxCore][ReadImageStackFilter][Cropping]") +{ + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + + UnitTest::LoadPlugins(); + DataStructure ds; + + const DataPath geomPath({"Crop_Voxel_XYZ"}); + auto cropOptions = CreateCropOptions(CropGeometryParameter::CropValues::TypeEnum::VoxelSubvolume, true, true, true); + + auto result = ExecuteImportImageStack(ds, geomPath, cropOptions); + SIMPLNX_RESULT_REQUIRE_VALID(result); + + VerifyGeometryDimensions(ds, geomPath, 101, 101, 2); + + DataStructure exemplarDS = UnitTest::LoadDataStructure(k_ExemplarFile); + const auto* generatedGeom = ds.getDataAs(geomPath); + const auto* exemplarGeom = exemplarDS.getDataAs(DataPath({"Crop_Voxel_XYZ"})); + UnitTest::CompareImageGeometry(exemplarGeom, generatedGeom); + + DataPath generatedDataPath = geomPath.createChildPath(Constants::k_Cell_Data).createChildPath(k_ImageDataName); + DataPath exemplarDataPath = DataPath({"Crop_Voxel_XYZ", Constants::k_Cell_Data, k_ImageDataName}); + const auto& generatedArray = ds.getDataRefAs(generatedDataPath); + const auto& exemplarArray = exemplarDS.getDataRefAs(exemplarDataPath); + UnitTest::CompareDataArrays(exemplarArray, generatedArray); +} + +TEST_CASE("SimplnxCore::ReadImageStackFilter::Crop_Physical_XY", "[SimplnxCore][ReadImageStackFilter][Cropping]") +{ + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + + UnitTest::LoadPlugins(); + DataStructure ds; + + const DataPath geomPath({"Crop_Physical_XY"}); + auto cropOptions = CreateCropOptions(CropGeometryParameter::CropValues::TypeEnum::PhysicalSubvolume, true, true, false); + + auto result = ExecuteImportImageStack(ds, geomPath, cropOptions); + SIMPLNX_RESULT_REQUIRE_VALID(result); + + VerifyGeometryDimensions(ds, geomPath, 101, 101, 3); + + DataStructure exemplarDS = UnitTest::LoadDataStructure(k_ExemplarFile); + const auto* generatedGeom = ds.getDataAs(geomPath); + const auto* exemplarGeom = exemplarDS.getDataAs(DataPath({"Crop_Physical_XY"})); + UnitTest::CompareImageGeometry(exemplarGeom, generatedGeom); + + DataPath generatedDataPath = geomPath.createChildPath(Constants::k_Cell_Data).createChildPath(k_ImageDataName); + DataPath exemplarDataPath = DataPath({"Crop_Physical_XY", Constants::k_Cell_Data, k_ImageDataName}); + const auto& generatedArray = ds.getDataRefAs(generatedDataPath); + const auto& exemplarArray = exemplarDS.getDataRefAs(exemplarDataPath); + UnitTest::CompareDataArrays(exemplarArray, generatedArray); +} + +TEST_CASE("SimplnxCore::ReadImageStackFilter::Crop_Physical_Z", "[SimplnxCore][ReadImageStackFilter][Cropping]") +{ + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + + UnitTest::LoadPlugins(); + DataStructure ds; + + const DataPath geomPath({"Crop_Physical_Z"}); + auto cropOptions = CreateCropOptions(CropGeometryParameter::CropValues::TypeEnum::PhysicalSubvolume, false, false, true); + + auto result = ExecuteImportImageStack(ds, geomPath, cropOptions); + SIMPLNX_RESULT_REQUIRE_VALID(result); + + VerifyGeometryDimensions(ds, geomPath, 200, 200, 2); + + DataStructure exemplarDS = UnitTest::LoadDataStructure(k_ExemplarFile); + const auto* generatedGeom = ds.getDataAs(geomPath); + const auto* exemplarGeom = exemplarDS.getDataAs(DataPath({"Crop_Physical_Z"})); + UnitTest::CompareImageGeometry(exemplarGeom, generatedGeom); + + DataPath generatedDataPath = geomPath.createChildPath(Constants::k_Cell_Data).createChildPath(k_ImageDataName); + DataPath exemplarDataPath = DataPath({"Crop_Physical_Z", Constants::k_Cell_Data, k_ImageDataName}); + const auto& generatedArray = ds.getDataRefAs(generatedDataPath); + const auto& exemplarArray = exemplarDS.getDataRefAs(exemplarDataPath); + UnitTest::CompareDataArrays(exemplarArray, generatedArray); +} + +// ============================================================================= +// RESAMPLING TESTS +// ============================================================================= + +TEST_CASE("SimplnxCore::ReadImageStackFilter::Resample_ScalingFactor", "[SimplnxCore][ReadImageStackFilter][Resampling]") +{ + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + + UnitTest::LoadPlugins(); + DataStructure ds; + + const DataPath geomPath({"Resample_Scaling50"}); + auto noCrop = CreateCropOptions(CropGeometryParameter::CropValues::TypeEnum::NoCropping, false, false, false); + + auto result = ExecuteImportImageStack(ds, geomPath, noCrop, k_ScalingFactor, 50.0f); + SIMPLNX_RESULT_REQUIRE_VALID(result); + + VerifyGeometryDimensions(ds, geomPath, 100, 100, 3); + + DataStructure exemplarDS = UnitTest::LoadDataStructure(k_ExemplarFile); + const auto* generatedGeom = ds.getDataAs(geomPath); + const auto* exemplarGeom = exemplarDS.getDataAs(DataPath({"Resample_Scaling_50"})); + UnitTest::CompareImageGeometry(exemplarGeom, generatedGeom); + + DataPath generatedDataPath = geomPath.createChildPath(Constants::k_Cell_Data).createChildPath(k_ImageDataName); + DataPath exemplarDataPath = DataPath({"Resample_Scaling_50", Constants::k_Cell_Data, k_ImageDataName}); + const auto& generatedArray = ds.getDataRefAs(generatedDataPath); + const auto& exemplarArray = exemplarDS.getDataRefAs(exemplarDataPath); + UnitTest::CompareDataArrays(exemplarArray, generatedArray); +} + +TEST_CASE("SimplnxCore::ReadImageStackFilter::Resample_ExactDimensions", "[SimplnxCore][ReadImageStackFilter][Resampling]") +{ + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + + UnitTest::LoadPlugins(); + DataStructure ds; + + const DataPath geomPath({"Resample_Exact128x128"}); + auto noCrop = CreateCropOptions(CropGeometryParameter::CropValues::TypeEnum::NoCropping, false, false, false); + + auto result = ExecuteImportImageStack(ds, geomPath, noCrop, k_ExactDimensions, 100.0f, {128, 128}); + SIMPLNX_RESULT_REQUIRE_VALID(result); + + VerifyGeometryDimensions(ds, geomPath, 128, 128, 3); + + DataStructure exemplarDS = UnitTest::LoadDataStructure(k_ExemplarFile); + const auto* generatedGeom = ds.getDataAs(geomPath); + const auto* exemplarGeom = exemplarDS.getDataAs(DataPath({"Resample_Exact_128x128"})); + UnitTest::CompareImageGeometry(exemplarGeom, generatedGeom); + + DataPath generatedDataPath = geomPath.createChildPath(Constants::k_Cell_Data).createChildPath(k_ImageDataName); + DataPath exemplarDataPath = DataPath({"Resample_Exact_128x128", Constants::k_Cell_Data, k_ImageDataName}); + const auto& generatedArray = ds.getDataRefAs(generatedDataPath); + const auto& exemplarArray = exemplarDS.getDataRefAs(exemplarDataPath); + UnitTest::CompareDataArrays(exemplarArray, generatedArray); +} + +// ============================================================================= +// GRAYSCALE TESTS +// ============================================================================= + +TEST_CASE("SimplnxCore::ReadImageStackFilter::Grayscale_Conversion", "[SimplnxCore][ReadImageStackFilter][Grayscale]") +{ + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + + UnitTest::LoadPlugins(); + DataStructure ds; + + const DataPath geomPath({"Grayscale"}); + auto noCrop = CreateCropOptions(CropGeometryParameter::CropValues::TypeEnum::NoCropping, false, false, false); + + auto result = ExecuteImportImageStack(ds, geomPath, noCrop, k_NoResample, 100.0f, {200, 200}, k_NoFlip, true); + SIMPLNX_RESULT_REQUIRE_VALID(result); + + VerifyGeometryDimensions(ds, geomPath, 200, 200, 3); + + DataPath grayscalePath = geomPath.createChildPath(Constants::k_Cell_Data).createChildPath(k_ImageDataName); + REQUIRE_NOTHROW(ds.getDataRefAs(grayscalePath)); + + DataStructure exemplarDS = UnitTest::LoadDataStructure(k_ExemplarFile); + const auto* generatedGeom = ds.getDataAs(geomPath); + const auto* exemplarGeom = exemplarDS.getDataAs(DataPath({"Grayscale_Conversion"})); + UnitTest::CompareImageGeometry(exemplarGeom, generatedGeom); + + DataPath generatedDataPath = geomPath.createChildPath(Constants::k_Cell_Data).createChildPath(k_ImageDataName); + DataPath exemplarDataPath = DataPath({"Grayscale_Conversion", Constants::k_Cell_Data, k_ImageDataName}); + const auto& generatedArray = ds.getDataRefAs(generatedDataPath); + const auto& exemplarArray = exemplarDS.getDataRefAs(exemplarDataPath); + UnitTest::CompareDataArrays(exemplarArray, generatedArray); +} + +// ============================================================================= +// FLIP TESTS +// ============================================================================= + +TEST_CASE("SimplnxCore::ReadImageStackFilter::FlipY", "[SimplnxCore][ReadImageStackFilter][Flip]") +{ + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + + UnitTest::LoadPlugins(); + DataStructure ds; + + const DataPath geomPath({"FlipY_Test"}); + auto noCrop = CreateCropOptions(CropGeometryParameter::CropValues::TypeEnum::NoCropping, false, false, false); + + auto result = ExecuteImportImageStack(ds, geomPath, noCrop, k_NoResample, 100.0f, {200, 200}, k_FlipY); + SIMPLNX_RESULT_REQUIRE_VALID(result); + + VerifyGeometryDimensions(ds, geomPath, 200, 200, 3); + + DataStructure exemplarDS = UnitTest::LoadDataStructure(k_ExemplarFile); + const auto* generatedGeom = ds.getDataAs(geomPath); + const auto* exemplarGeom = exemplarDS.getDataAs(DataPath({"FlipY_Test"})); + UnitTest::CompareImageGeometry(exemplarGeom, generatedGeom); + + DataPath generatedDataPath = geomPath.createChildPath(Constants::k_Cell_Data).createChildPath(k_ImageDataName); + DataPath exemplarDataPath = DataPath({"FlipY_Test", Constants::k_Cell_Data, k_ImageDataName}); + const auto& generatedArray = ds.getDataRefAs(generatedDataPath); + const auto& exemplarArray = exemplarDS.getDataRefAs(exemplarDataPath); + UnitTest::CompareDataArrays(exemplarArray, generatedArray); +} + +// ============================================================================= +// ORIGIN/SPACING TESTS +// ============================================================================= + +TEST_CASE("SimplnxCore::ReadImageStackFilter::OriginSpacing_Preprocessed", "[SimplnxCore][ReadImageStackFilter][OriginSpacing]") +{ + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + + UnitTest::LoadPlugins(); + DataStructure ds; + + const DataPath geomPath({"OriginSpacing_Preprocessed"}); + auto cropOptions = CreateCropOptions(CropGeometryParameter::CropValues::TypeEnum::VoxelSubvolume, true, true, false); + + auto result = ExecuteImportImageStack(ds, geomPath, cropOptions, k_NoResample, 100.0f, {200, 200}, k_NoFlip, false, true, {10.0, 20.0, 30.0}, true, {2.0, 2.0, 2.0}, k_Preprocessed); + SIMPLNX_RESULT_REQUIRE_VALID(result); + + VerifyGeometryDimensions(ds, geomPath, 101, 101, 3); + VerifyOriginSpacing(ds, geomPath, {110.0f, 120.0f, 30.0f}, {2.0f, 2.0f, 2.0f}); + + DataStructure exemplarDS = UnitTest::LoadDataStructure(k_ExemplarFile); + const auto* generatedGeom = ds.getDataAs(geomPath); + const auto* exemplarGeom = exemplarDS.getDataAs(DataPath({"OriginSpacing_Preprocessed"})); + UnitTest::CompareImageGeometry(exemplarGeom, generatedGeom); + + DataPath generatedDataPath = geomPath.createChildPath(Constants::k_Cell_Data).createChildPath(k_ImageDataName); + DataPath exemplarDataPath = DataPath({"OriginSpacing_Preprocessed", Constants::k_Cell_Data, k_ImageDataName}); + const auto& generatedArray = ds.getDataRefAs(generatedDataPath); + const auto& exemplarArray = exemplarDS.getDataRefAs(exemplarDataPath); + UnitTest::CompareDataArrays(exemplarArray, generatedArray); +} + +TEST_CASE("SimplnxCore::ReadImageStackFilter::OriginSpacing_Postprocessed", "[SimplnxCore][ReadImageStackFilter][OriginSpacing]") +{ + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + + UnitTest::LoadPlugins(); + DataStructure ds; + + const DataPath geomPath({"OriginSpacing_Postprocessed"}); + auto cropOptions = CreateCropOptions(CropGeometryParameter::CropValues::TypeEnum::VoxelSubvolume, true, true, false); + + auto result = ExecuteImportImageStack(ds, geomPath, cropOptions, k_NoResample, 100.0f, {200, 200}, k_NoFlip, false, true, {10.0, 20.0, 30.0}, true, {2.0, 2.0, 2.0}, k_Postprocessed); + SIMPLNX_RESULT_REQUIRE_VALID(result); + + VerifyGeometryDimensions(ds, geomPath, 101, 101, 3); + VerifyOriginSpacing(ds, geomPath, {10.0f, 20.0f, 30.0f}, {2.0f, 2.0f, 2.0f}); + + DataStructure exemplarDS = UnitTest::LoadDataStructure(k_ExemplarFile); + const auto* generatedGeom = ds.getDataAs(geomPath); + const auto* exemplarGeom = exemplarDS.getDataAs(DataPath({"OriginSpacing_Postprocessed"})); + UnitTest::CompareImageGeometry(exemplarGeom, generatedGeom); + + DataPath generatedDataPath = geomPath.createChildPath(Constants::k_Cell_Data).createChildPath(k_ImageDataName); + DataPath exemplarDataPath = DataPath({"OriginSpacing_Postprocessed", Constants::k_Cell_Data, k_ImageDataName}); + const auto& generatedArray = ds.getDataRefAs(generatedDataPath); + const auto& exemplarArray = exemplarDS.getDataRefAs(exemplarDataPath); + UnitTest::CompareDataArrays(exemplarArray, generatedArray); +} + +TEST_CASE("SimplnxCore::ReadImageStackFilter::OriginSpacing_Preprocessed_WithZCrop", "[SimplnxCore][ReadImageStackFilter][OriginSpacing]") +{ + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + + UnitTest::LoadPlugins(); + DataStructure ds; + + const DataPath geomPath({"OriginSpacing_Preprocessed_WithZCrop"}); + auto cropOptions = CreateCropOptions(CropGeometryParameter::CropValues::TypeEnum::VoxelSubvolume, false, false, true); + + auto result = ExecuteImportImageStack(ds, geomPath, cropOptions, k_NoResample, 100.0f, {200, 200}, k_NoFlip, false, true, {10.0, 20.0, 30.0}, true, {2.0, 2.0, 2.0}, k_Preprocessed); + SIMPLNX_RESULT_REQUIRE_VALID(result); + + VerifyGeometryDimensions(ds, geomPath, 200, 200, 2); + VerifyOriginSpacing(ds, geomPath, {10.0f, 20.0f, 30.0f}, {2.0f, 2.0f, 2.0f}); + + DataStructure exemplarDS = UnitTest::LoadDataStructure(k_ExemplarFile); + const auto* generatedGeom = ds.getDataAs(geomPath); + const auto* exemplarGeom = exemplarDS.getDataAs(DataPath({"OriginSpacing_Preprocessed_WithZCrop"})); + UnitTest::CompareImageGeometry(exemplarGeom, generatedGeom); + + DataPath generatedDataPath = geomPath.createChildPath(Constants::k_Cell_Data).createChildPath(k_ImageDataName); + DataPath exemplarDataPath = DataPath({"OriginSpacing_Preprocessed_WithZCrop", Constants::k_Cell_Data, k_ImageDataName}); + const auto& generatedArray = ds.getDataRefAs(generatedDataPath); + const auto& exemplarArray = exemplarDS.getDataRefAs(exemplarDataPath); + UnitTest::CompareDataArrays(exemplarArray, generatedArray); +} + +TEST_CASE("SimplnxCore::ReadImageStackFilter::OriginSpacing_Postprocessed_WithZCrop", "[SimplnxCore][ReadImageStackFilter][OriginSpacing]") +{ + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + + UnitTest::LoadPlugins(); + DataStructure ds; + + const DataPath geomPath({"OriginSpacing_Postprocessed_WithZCrop"}); + auto cropOptions = CreateCropOptions(CropGeometryParameter::CropValues::TypeEnum::VoxelSubvolume, false, false, true); + + auto result = ExecuteImportImageStack(ds, geomPath, cropOptions, k_NoResample, 100.0f, {200, 200}, k_NoFlip, false, true, {10.0, 20.0, 30.0}, true, {2.0, 2.0, 2.0}, k_Postprocessed); + SIMPLNX_RESULT_REQUIRE_VALID(result); + + VerifyGeometryDimensions(ds, geomPath, 200, 200, 2); + VerifyOriginSpacing(ds, geomPath, {10.0f, 20.0f, 30.0f}, {2.0f, 2.0f, 2.0f}); + + DataStructure exemplarDS = UnitTest::LoadDataStructure(k_ExemplarFile); + const auto* generatedGeom = ds.getDataAs(geomPath); + const auto* exemplarGeom = exemplarDS.getDataAs(DataPath({"OriginSpacing_Postprocessed_WithZCrop"})); + UnitTest::CompareImageGeometry(exemplarGeom, generatedGeom); + + DataPath generatedDataPath = geomPath.createChildPath(Constants::k_Cell_Data).createChildPath(k_ImageDataName); + DataPath exemplarDataPath = DataPath({"OriginSpacing_Postprocessed_WithZCrop", Constants::k_Cell_Data, k_ImageDataName}); + const auto& generatedArray = ds.getDataRefAs(generatedDataPath); + const auto& exemplarArray = exemplarDS.getDataRefAs(exemplarDataPath); + UnitTest::CompareDataArrays(exemplarArray, generatedArray); +} + +// ============================================================================= +// INTERACTION TESTS +// ============================================================================= + +TEST_CASE("SimplnxCore::ReadImageStackFilter::Interaction_Crop_Resample", "[SimplnxCore][ReadImageStackFilter][Interaction]") +{ + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + + UnitTest::LoadPlugins(); + DataStructure ds; + + const DataPath geomPath({"Crop_Then_Resample"}); + auto cropOptions = CreateCropOptions(CropGeometryParameter::CropValues::TypeEnum::VoxelSubvolume, true, true, false); + + auto result = ExecuteImportImageStack(ds, geomPath, cropOptions, k_ExactDimensions, 100, {64, 64}); + SIMPLNX_RESULT_REQUIRE_VALID(result); + + VerifyGeometryDimensions(ds, geomPath, 64, 64, 3); + + DataStructure exemplarDS = UnitTest::LoadDataStructure(k_ExemplarFile); + const auto* generatedGeom = ds.getDataAs(geomPath); + const auto* exemplarGeom = exemplarDS.getDataAs(DataPath({"Crop_And_Resample"})); + UnitTest::CompareImageGeometry(exemplarGeom, generatedGeom); + + DataPath generatedDataPath = geomPath.createChildPath(Constants::k_Cell_Data).createChildPath(k_ImageDataName); + DataPath exemplarDataPath = DataPath({"Crop_And_Resample", Constants::k_Cell_Data, k_ImageDataName}); + const auto& generatedArray = ds.getDataRefAs(generatedDataPath); + const auto& exemplarArray = exemplarDS.getDataRefAs(exemplarDataPath); + UnitTest::CompareDataArrays(exemplarArray, generatedArray); +} + +TEST_CASE("SimplnxCore::ReadImageStackFilter::Interaction_Crop_Flip", "[SimplnxCore][ReadImageStackFilter][Interaction]") +{ + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + + UnitTest::LoadPlugins(); + DataStructure ds; + + const DataPath geomPath({"Crop_Then_FlipX"}); + auto cropOptions = CreateCropOptions(CropGeometryParameter::CropValues::TypeEnum::VoxelSubvolume, true, true, false); + + auto result = ExecuteImportImageStack(ds, geomPath, cropOptions, k_NoResample, 100.0f, {200, 200}, k_FlipX); + SIMPLNX_RESULT_REQUIRE_VALID(result); + + VerifyGeometryDimensions(ds, geomPath, 101, 101, 3); + + DataStructure exemplarDS = UnitTest::LoadDataStructure(k_ExemplarFile); + const auto* generatedGeom = ds.getDataAs(geomPath); + const auto* exemplarGeom = exemplarDS.getDataAs(DataPath({"Crop_And_FlipX"})); + UnitTest::CompareImageGeometry(exemplarGeom, generatedGeom); + + DataPath generatedDataPath = geomPath.createChildPath(Constants::k_Cell_Data).createChildPath(k_ImageDataName); + DataPath exemplarDataPath = DataPath({"Crop_And_FlipX", Constants::k_Cell_Data, k_ImageDataName}); + const auto& generatedArray = ds.getDataRefAs(generatedDataPath); + const auto& exemplarArray = exemplarDS.getDataRefAs(exemplarDataPath); + UnitTest::CompareDataArrays(exemplarArray, generatedArray); +} + +TEST_CASE("SimplnxCore::ReadImageStackFilter::Interaction_Resample_Flip", "[SimplnxCore][ReadImageStackFilter][Interaction]") +{ + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + + UnitTest::LoadPlugins(); + DataStructure ds; + + const DataPath geomPath({"Resample_Then_FlipX"}); + auto noCrop = CreateCropOptions(CropGeometryParameter::CropValues::TypeEnum::NoCropping, false, false, false); + + auto result = ExecuteImportImageStack(ds, geomPath, noCrop, k_ExactDimensions, 100.0f, {128, 128}, k_FlipX); + SIMPLNX_RESULT_REQUIRE_VALID(result); + + VerifyGeometryDimensions(ds, geomPath, 128, 128, 3); + + DataStructure exemplarDS = UnitTest::LoadDataStructure(k_ExemplarFile); + const auto* generatedGeom = ds.getDataAs(geomPath); + const auto* exemplarGeom = exemplarDS.getDataAs(DataPath({"Resample_And_FlipX"})); + UnitTest::CompareImageGeometry(exemplarGeom, generatedGeom); + + DataPath generatedDataPath = geomPath.createChildPath(Constants::k_Cell_Data).createChildPath(k_ImageDataName); + DataPath exemplarDataPath = DataPath({"Resample_And_FlipX", Constants::k_Cell_Data, k_ImageDataName}); + const auto& generatedArray = ds.getDataRefAs(generatedDataPath); + const auto& exemplarArray = exemplarDS.getDataRefAs(exemplarDataPath); + UnitTest::CompareDataArrays(exemplarArray, generatedArray); +} + +TEST_CASE("SimplnxCore::ReadImageStackFilter::Interaction_Crop_Grayscale", "[SimplnxCore][ReadImageStackFilter][Interaction]") +{ + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + + UnitTest::LoadPlugins(); + DataStructure ds; + + const DataPath geomPath({"Crop_Then_Grayscale"}); + auto cropOptions = CreateCropOptions(CropGeometryParameter::CropValues::TypeEnum::VoxelSubvolume, true, true, false); + + auto result = ExecuteImportImageStack(ds, geomPath, cropOptions, k_NoResample, 100.0f, {200, 200}, k_NoFlip, true); + SIMPLNX_RESULT_REQUIRE_VALID(result); + + VerifyGeometryDimensions(ds, geomPath, 101, 101, 3); + + DataPath grayscalePath = geomPath.createChildPath(Constants::k_Cell_Data).createChildPath(k_ImageDataName); + REQUIRE_NOTHROW(ds.getDataRefAs(grayscalePath)); + + DataStructure exemplarDS = UnitTest::LoadDataStructure(k_ExemplarFile); + const auto* generatedGeom = ds.getDataAs(geomPath); + const auto* exemplarGeom = exemplarDS.getDataAs(DataPath({"Crop_And_Grayscale"})); + UnitTest::CompareImageGeometry(exemplarGeom, generatedGeom); + + DataPath generatedDataPath = geomPath.createChildPath(Constants::k_Cell_Data).createChildPath(k_ImageDataName); + DataPath exemplarDataPath = DataPath({"Crop_And_Grayscale", Constants::k_Cell_Data, k_ImageDataName}); + const auto& generatedArray = ds.getDataRefAs(generatedDataPath); + const auto& exemplarArray = exemplarDS.getDataRefAs(exemplarDataPath); + UnitTest::CompareDataArrays(exemplarArray, generatedArray); +} + +TEST_CASE("SimplnxCore::ReadImageStackFilter::Interaction_Resample_Grayscale", "[SimplnxCore][ReadImageStackFilter][Interaction]") +{ + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + + UnitTest::LoadPlugins(); + DataStructure ds; + + const DataPath geomPath({"Resample_Then_Grayscale"}); + auto noCrop = CreateCropOptions(CropGeometryParameter::CropValues::TypeEnum::NoCropping, false, false, false); + + auto result = ExecuteImportImageStack(ds, geomPath, noCrop, k_ExactDimensions, 100.0f, {128, 128}, k_NoFlip, true); + SIMPLNX_RESULT_REQUIRE_VALID(result); + + VerifyGeometryDimensions(ds, geomPath, 128, 128, 3); + + DataPath grayscalePath = geomPath.createChildPath(Constants::k_Cell_Data).createChildPath(k_ImageDataName); + REQUIRE_NOTHROW(ds.getDataRefAs(grayscalePath)); + + DataStructure exemplarDS = UnitTest::LoadDataStructure(k_ExemplarFile); + const auto* generatedGeom = ds.getDataAs(geomPath); + const auto* exemplarGeom = exemplarDS.getDataAs(DataPath({"Resample_And_Grayscale"})); + UnitTest::CompareImageGeometry(exemplarGeom, generatedGeom); + + DataPath generatedDataPath = geomPath.createChildPath(Constants::k_Cell_Data).createChildPath(k_ImageDataName); + DataPath exemplarDataPath = DataPath({"Resample_And_Grayscale", Constants::k_Cell_Data, k_ImageDataName}); + const auto& generatedArray = ds.getDataRefAs(generatedDataPath); + const auto& exemplarArray = exemplarDS.getDataRefAs(exemplarDataPath); + UnitTest::CompareDataArrays(exemplarArray, generatedArray); +} + +TEST_CASE("SimplnxCore::ReadImageStackFilter::Interaction_Grayscale_Flip", "[SimplnxCore][ReadImageStackFilter][Interaction]") +{ + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + + UnitTest::LoadPlugins(); + DataStructure ds; + + const DataPath geomPath({"Grayscale_Then_FlipX"}); + auto noCrop = CreateCropOptions(CropGeometryParameter::CropValues::TypeEnum::NoCropping, false, false, false); + + auto result = ExecuteImportImageStack(ds, geomPath, noCrop, k_NoResample, 100.0f, {200, 200}, k_FlipX, true); + SIMPLNX_RESULT_REQUIRE_VALID(result); + + VerifyGeometryDimensions(ds, geomPath, 200, 200, 3); + + DataPath grayscalePath = geomPath.createChildPath(Constants::k_Cell_Data).createChildPath(k_ImageDataName); + REQUIRE_NOTHROW(ds.getDataRefAs(grayscalePath)); + + DataStructure exemplarDS = UnitTest::LoadDataStructure(k_ExemplarFile); + const auto* generatedGeom = ds.getDataAs(geomPath); + const auto* exemplarGeom = exemplarDS.getDataAs(DataPath({"Grayscale_And_FlipX"})); + UnitTest::CompareImageGeometry(exemplarGeom, generatedGeom); + + DataPath generatedDataPath = geomPath.createChildPath(Constants::k_Cell_Data).createChildPath(k_ImageDataName); + DataPath exemplarDataPath = DataPath({"Grayscale_And_FlipX", Constants::k_Cell_Data, k_ImageDataName}); + const auto& generatedArray = ds.getDataRefAs(generatedDataPath); + const auto& exemplarArray = exemplarDS.getDataRefAs(exemplarDataPath); + UnitTest::CompareDataArrays(exemplarArray, generatedArray); +} + +TEST_CASE("SimplnxCore::ReadImageStackFilter::Interaction_FullPipeline", "[SimplnxCore][ReadImageStackFilter][Interaction]") +{ + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + + UnitTest::LoadPlugins(); + DataStructure ds; + + const DataPath geomPath({"Full_Pipeline_Calculated"}); + auto cropOptions = CreateCropOptions(CropGeometryParameter::CropValues::TypeEnum::VoxelSubvolume, true, true, false); + + auto result = ExecuteImportImageStack(ds, geomPath, cropOptions, k_ScalingFactor, 50.0f, {100, 100}, k_FlipX, true); + SIMPLNX_RESULT_REQUIRE_VALID(result); + + VerifyGeometryDimensions(ds, geomPath, 50, 50, 3); + + DataPath grayscalePath = geomPath.createChildPath(Constants::k_Cell_Data).createChildPath(k_ImageDataName); + REQUIRE_NOTHROW(ds.getDataRefAs(grayscalePath)); + + DataStructure exemplarDS = UnitTest::LoadDataStructure(k_ExemplarFile); + const auto* generatedGeom = ds.getDataAs(geomPath); + const auto* exemplarGeom = exemplarDS.getDataAs(DataPath({"Full_Pipeline"})); + UnitTest::CompareImageGeometry(exemplarGeom, generatedGeom); + + DataPath generatedDataPath = geomPath.createChildPath(Constants::k_Cell_Data).createChildPath(k_ImageDataName); + DataPath exemplarDataPath = DataPath({"Full_Pipeline", Constants::k_Cell_Data, k_ImageDataName}); + const auto& generatedArray = ds.getDataRefAs(generatedDataPath); + const auto& exemplarArray = exemplarDS.getDataRefAs(exemplarDataPath); + UnitTest::CompareDataArrays(exemplarArray, generatedArray); +} diff --git a/src/Plugins/SimplnxCore/test/ReadImageTest.cpp b/src/Plugins/SimplnxCore/test/ReadImageTest.cpp index 0109ae954b..59265ea9a3 100644 --- a/src/Plugins/SimplnxCore/test/ReadImageTest.cpp +++ b/src/Plugins/SimplnxCore/test/ReadImageTest.cpp @@ -1,86 +1,489 @@ -/** - * This file is auto generated from the original SimplnxCore/ReadImageFilter - * runtime information. These are the steps that need to be taken to utilize this - * unit test in the proper way. - * - * 1: Validate each of the default parameters that gets created. - * 2: Inspect the actual filter to determine if the filter in its default state - * would pass or fail BOTH the preflight() and execute() methods - * 3: UPDATE the ```REQUIRE(result.result.valid());``` code to have the proper - * - * 4: Add additional unit tests to actually test each code path within the filter - * - * There are some example Catch2 ```TEST_CASE``` sections for your inspiration. - * - * NOTE the format of the ```TEST_CASE``` macro. Please stick to this format to - * allow easier parsing of the unit tests. - * - * When you start working on this unit test remove "[ReadImageFilter][.][UNIMPLEMENTED]" - * from the TEST_CASE macro. This will enable this unit test to be run by default - * and report errors. - */ - - #include - - //TODO: PARAMETER_INCLUDES #include "SimplnxCore/Filters/ReadImageFilter.hpp" #include "SimplnxCore/SimplnxCore_test_dirs.hpp" +#include "simplnx/Parameters/ChoicesParameter.hpp" +#include "simplnx/Parameters/CropGeometryParameter.hpp" +#include "simplnx/Parameters/DataObjectNameParameter.hpp" #include "simplnx/UnitTest/UnitTestCommon.hpp" -#include +namespace fs = std::filesystem; using namespace nx::core; using namespace nx::core::UnitTest; -using namespace nx::core::Constants; -TEST_CASE("SimplnxCore::ReadImageFilter: Valid Filter Execution","[SimplnxCore][ReadImageFilter]") +namespace +{ +const std::string k_TestDataDirName = "itk_image_reader_test"; +const fs::path k_TestDataDir = fs::path(unit_test::k_TestFilesDir.view()) / k_TestDataDirName; +const fs::path k_ExemplarFile = k_TestDataDir / "itk_image_reader_test.dream3d"; +const fs::path k_InputImageFile = k_TestDataDir / "200x200_0.tif"; +const std::string k_ImageGeometryName = "[ImageGeometry]"; +const std::string k_ImageCellDataName = "Cell Data"; +const std::string k_ImageDataName = "ImageData"; + +// Values for ReadImageFilter::k_OriginSpacingProcessing_Key +// 0 = Preprocessed, 1 = Postprocessed +constexpr uint64 k_Preprocessed = 0; +constexpr uint64 k_Postprocessed = 1; +} // namespace + +TEST_CASE("SimplnxCore::ReadImageFilter: Read_Basic", "[SimplnxCore][ReadImageFilter]") +{ + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); + + UnitTest::LoadPlugins(); + ReadImageFilter filter; + DataStructure dataStructure; + Arguments args; + + const DataPath inputGeometryPath({k_ImageGeometryName}); + + args.insertOrAssign(ReadImageFilter::k_FileName_Key, k_InputImageFile); + args.insertOrAssign(ReadImageFilter::k_ImageGeometryPath_Key, inputGeometryPath); + args.insertOrAssign(ReadImageFilter::k_CellDataName_Key, static_cast(k_ImageCellDataName)); + args.insertOrAssign(ReadImageFilter::k_ImageDataArrayPath_Key, static_cast(k_ImageDataName)); + args.insertOrAssign(ReadImageFilter::k_ChangeOrigin_Key, false); + args.insertOrAssign(ReadImageFilter::k_ChangeSpacing_Key, false); + + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions) + + auto executeResult = filter.execute(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result) + + // Compare against exemplar + DataStructure exemplarDS = UnitTest::LoadDataStructure(k_ExemplarFile); + REQUIRE_NOTHROW(dataStructure.getDataRefAs(inputGeometryPath)); + const auto& generatedGeom = dataStructure.getDataRefAs(inputGeometryPath); + REQUIRE_NOTHROW(exemplarDS.getDataRefAs(DataPath({"Read_Basic"}))); + const auto& exemplarGeom = exemplarDS.getDataRefAs(DataPath({"Read_Basic"})); + UnitTest::CompareImageGeometry(&exemplarGeom, &generatedGeom); + + DataPath generatedDataPath = inputGeometryPath.createChildPath(k_ImageCellDataName).createChildPath(k_ImageDataName); + DataPath exemplarDataPath = DataPath({"Read_Basic", Constants::k_Cell_Data, k_ImageDataName}); + REQUIRE_NOTHROW(dataStructure.getDataRefAs(generatedDataPath)); + const auto& generatedArray = dataStructure.getDataRefAs(generatedDataPath); + REQUIRE_NOTHROW(exemplarDS.getDataRefAs(exemplarDataPath)); + const auto& exemplarArray = exemplarDS.getDataRefAs(exemplarDataPath); + UnitTest::CompareDataArrays(exemplarArray, generatedArray); + + UnitTest::CheckArraysInheritTupleDims(dataStructure); +} + +TEST_CASE("SimplnxCore::ReadImageFilter: Override_Origin", "[SimplnxCore][ReadImageFilter]") { + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); + UnitTest::LoadPlugins(); + ReadImageFilter filter; + DataStructure dataStructure; + Arguments args; + + std::vector k_Origin{-32.0, -32.0, 0.0}; + + const DataPath inputGeometryPath({k_ImageGeometryName}); + + args.insertOrAssign(ReadImageFilter::k_FileName_Key, k_InputImageFile); + args.insertOrAssign(ReadImageFilter::k_ImageGeometryPath_Key, inputGeometryPath); + args.insertOrAssign(ReadImageFilter::k_CellDataName_Key, static_cast(k_ImageCellDataName)); + args.insertOrAssign(ReadImageFilter::k_ImageDataArrayPath_Key, static_cast(k_ImageDataName)); + args.insertOrAssign(ReadImageFilter::k_ChangeOrigin_Key, true); + args.insertOrAssign(ReadImageFilter::k_Origin_Key, k_Origin); + args.insertOrAssign(ReadImageFilter::k_ChangeSpacing_Key, false); + + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions) + + auto executeResult = filter.execute(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result) + + // Compare against exemplar + DataStructure exemplarDS = UnitTest::LoadDataStructure(k_ExemplarFile); + REQUIRE_NOTHROW(dataStructure.getDataRefAs(inputGeometryPath)); + const auto& generatedGeom = dataStructure.getDataRefAs(inputGeometryPath); + REQUIRE_NOTHROW(exemplarDS.getDataRefAs(DataPath({"Override_Origin"}))); + const auto& exemplarGeom = exemplarDS.getDataRefAs(DataPath({"Override_Origin"})); + UnitTest::CompareImageGeometry(&exemplarGeom, &generatedGeom); + + DataPath generatedDataPath = inputGeometryPath.createChildPath(k_ImageCellDataName).createChildPath(k_ImageDataName); + DataPath exemplarDataPath = DataPath({"Override_Origin", Constants::k_Cell_Data, k_ImageDataName}); + REQUIRE_NOTHROW(dataStructure.getDataRefAs(generatedDataPath)); + const auto& generatedArray = dataStructure.getDataRefAs(generatedDataPath); + REQUIRE_NOTHROW(exemplarDS.getDataRefAs(exemplarDataPath)); + const auto& exemplarArray = exemplarDS.getDataRefAs(exemplarDataPath); + UnitTest::CompareDataArrays(exemplarArray, generatedArray); + + UnitTest::CheckArraysInheritTupleDims(dataStructure); +} - const nx::core::UnitTest::TestFileSentinel testDataSentinel( - nx::core::unit_test::k_TestFilesDir, - "ReadImageFilter.tar.gz", - "ReadImageFilter"); +TEST_CASE("SimplnxCore::ReadImageFilter: Centering_Origin", "[SimplnxCore][ReadImageFilter]") +{ + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); - // Read the Small IN100 Data set - auto baseDataFilePath = fs::path(fmt::format("{}/ReadImageFilter/ReadImageFilter.dream3d", nx::core::unit_test::k_TestFilesDir)); - DataStructure dataStructure = UnitTest::LoadDataStructure(baseDataFilePath); + UnitTest::LoadPlugins(); + ReadImageFilter filter; + DataStructure dataStructure; + Arguments args; - // Instantiate the filter, a DataStructure object and an Arguments Object - { - Arguments args; - ReadImageFilter filter; + const DataPath inputGeometryPath({k_ImageGeometryName}); - // Create default Parameters for the filter. - // This next line is an example, your filter may be different - // args.insertOrAssign(ReadImageFilter::k_CreatedTriangleGeometryPath_Key, std::make_any(computedTriangleGeomPath)); + args.insertOrAssign(ReadImageFilter::k_FileName_Key, k_InputImageFile); + args.insertOrAssign(ReadImageFilter::k_ImageGeometryPath_Key, inputGeometryPath); + args.insertOrAssign(ReadImageFilter::k_CellDataName_Key, static_cast(k_ImageCellDataName)); + args.insertOrAssign(ReadImageFilter::k_ImageDataArrayPath_Key, static_cast(k_ImageDataName)); + args.insertOrAssign(ReadImageFilter::k_ChangeOrigin_Key, true); + args.insertOrAssign(ReadImageFilter::k_CenterOrigin_Key, true); + args.insertOrAssign(ReadImageFilter::k_ChangeSpacing_Key, false); - // Preflight the filter and check result - auto preflightResult = filter.preflight(dataStructure, args); - SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions) + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions) - // Execute the filter and check the result - auto executeResult = filter.execute(dataStructure, args); - SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result) + auto executeResult = filter.execute(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result) - // Write the DataStructure out to the file system -#ifdef SIMPLNX_WRITE_TEST_OUTPUT - WriteTestDataStructure(dataStructure, fs::path(fmt::format("{}/ReadImageFilterTest.dream3d", unit_test::k_BinaryTestOutputDir))); -#endif + // Compare against exemplar + DataStructure exemplarDS = UnitTest::LoadDataStructure(k_ExemplarFile); + REQUIRE_NOTHROW(dataStructure.getDataRefAs(inputGeometryPath)); + const auto& generatedGeom = dataStructure.getDataRefAs(inputGeometryPath); + REQUIRE_NOTHROW(exemplarDS.getDataRefAs(DataPath({"Centering_Origin"}))); + const auto& exemplarGeom = exemplarDS.getDataRefAs(DataPath({"Centering_Origin"})); + UnitTest::CompareImageGeometry(&exemplarGeom, &generatedGeom); - } - // Compare exemplar data arrays with computed data arrays - //TODO: Insert verification codes + DataPath generatedDataPath = inputGeometryPath.createChildPath(k_ImageCellDataName).createChildPath(k_ImageDataName); + DataPath exemplarDataPath = DataPath({"Centering_Origin", Constants::k_Cell_Data, k_ImageDataName}); + REQUIRE_NOTHROW(dataStructure.getDataRefAs(generatedDataPath)); + const auto& generatedArray = dataStructure.getDataRefAs(generatedDataPath); + REQUIRE_NOTHROW(exemplarDS.getDataRefAs(exemplarDataPath)); + const auto& exemplarArray = exemplarDS.getDataRefAs(exemplarDataPath); + UnitTest::CompareDataArrays(exemplarArray, generatedArray); - // This should be in every unit test. If you think it does not apply, please review with another engineer UnitTest::CheckArraysInheritTupleDims(dataStructure); +} + +TEST_CASE("SimplnxCore::ReadImageFilter: Override_Spacing", "[SimplnxCore][ReadImageFilter]") +{ + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); + + UnitTest::LoadPlugins(); + ReadImageFilter filter; + DataStructure dataStructure; + Arguments args; + + std::vector k_Spacing{2.5, 3.0, 1.0}; + + const DataPath inputGeometryPath({k_ImageGeometryName}); + + args.insertOrAssign(ReadImageFilter::k_FileName_Key, k_InputImageFile); + args.insertOrAssign(ReadImageFilter::k_ImageGeometryPath_Key, inputGeometryPath); + args.insertOrAssign(ReadImageFilter::k_CellDataName_Key, static_cast(k_ImageCellDataName)); + args.insertOrAssign(ReadImageFilter::k_ImageDataArrayPath_Key, static_cast(k_ImageDataName)); + args.insertOrAssign(ReadImageFilter::k_ChangeOrigin_Key, false); + args.insertOrAssign(ReadImageFilter::k_ChangeSpacing_Key, true); + args.insertOrAssign(ReadImageFilter::k_Spacing_Key, k_Spacing); + + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions) + + auto executeResult = filter.execute(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result) + + // Compare against exemplar + DataStructure exemplarDS = UnitTest::LoadDataStructure(k_ExemplarFile); + REQUIRE_NOTHROW(dataStructure.getDataRefAs(inputGeometryPath)); + const auto& generatedGeom = dataStructure.getDataRefAs(inputGeometryPath); + REQUIRE_NOTHROW(exemplarDS.getDataRefAs(DataPath({"Override_Spacing"}))); + const auto& exemplarGeom = exemplarDS.getDataRefAs(DataPath({"Override_Spacing"})); + UnitTest::CompareImageGeometry(&exemplarGeom, &generatedGeom); + + DataPath generatedDataPath = inputGeometryPath.createChildPath(k_ImageCellDataName).createChildPath(k_ImageDataName); + DataPath exemplarDataPath = DataPath({"Override_Spacing", Constants::k_Cell_Data, k_ImageDataName}); + REQUIRE_NOTHROW(dataStructure.getDataRefAs(generatedDataPath)); + const auto& generatedArray = dataStructure.getDataRefAs(generatedDataPath); + REQUIRE_NOTHROW(exemplarDS.getDataRefAs(exemplarDataPath)); + const auto& exemplarArray = exemplarDS.getDataRefAs(exemplarDataPath); + UnitTest::CompareDataArrays(exemplarArray, generatedArray); + UnitTest::CheckArraysInheritTupleDims(dataStructure); } -//TEST_CASE("SimplnxCore::ReadImageFilter: InValid Filter Execution") -//{ -// -//} +TEST_CASE("SimplnxCore::ReadImageFilter: OriginSpacing_Preprocessed", "[SimplnxCore][ReadImageFilter]") +{ + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); + + UnitTest::LoadPlugins(); + ReadImageFilter filter; + DataStructure dataStructure; + Arguments args; + + std::vector k_Origin{10.0, 20.0, 0.0}; + std::vector k_Spacing{2.0, 2.0, 1.0}; + + const DataPath inputGeometryPath({k_ImageGeometryName}); + + auto cropOptions = CropGeometryParameter::ValueType(); + cropOptions.type = CropGeometryParameter::CropValues::TypeEnum::VoxelSubvolume; + cropOptions.cropX = true; + cropOptions.cropY = true; + cropOptions.cropZ = false; + cropOptions.xBoundVoxels = {50, 150}; + cropOptions.yBoundVoxels = {50, 150}; + + args.insertOrAssign(ReadImageFilter::k_FileName_Key, k_InputImageFile); + args.insertOrAssign(ReadImageFilter::k_ImageGeometryPath_Key, inputGeometryPath); + args.insertOrAssign(ReadImageFilter::k_CellDataName_Key, static_cast(k_ImageCellDataName)); + args.insertOrAssign(ReadImageFilter::k_ImageDataArrayPath_Key, static_cast(k_ImageDataName)); + args.insertOrAssign(ReadImageFilter::k_ChangeOrigin_Key, true); + args.insertOrAssign(ReadImageFilter::k_Origin_Key, k_Origin); + args.insertOrAssign(ReadImageFilter::k_ChangeSpacing_Key, true); + args.insertOrAssign(ReadImageFilter::k_Spacing_Key, k_Spacing); + args.insertOrAssign(ReadImageFilter::k_OriginSpacingProcessing_Key, k_Preprocessed); + args.insertOrAssign(ReadImageFilter::k_CroppingOptions_Key, cropOptions); + + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions) + + auto executeResult = filter.execute(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result) + + // Compare against exemplar + DataStructure exemplarDS = UnitTest::LoadDataStructure(k_ExemplarFile); + REQUIRE_NOTHROW(dataStructure.getDataRefAs(inputGeometryPath)); + const auto& generatedGeom = dataStructure.getDataRefAs(inputGeometryPath); + REQUIRE_NOTHROW(exemplarDS.getDataRefAs(DataPath({"OriginSpacing_Preprocessed"}))); + const auto& exemplarGeom = exemplarDS.getDataRefAs(DataPath({"OriginSpacing_Preprocessed"})); + UnitTest::CompareImageGeometry(&exemplarGeom, &generatedGeom); + + DataPath generatedDataPath = inputGeometryPath.createChildPath(k_ImageCellDataName).createChildPath(k_ImageDataName); + DataPath exemplarDataPath = DataPath({"OriginSpacing_Preprocessed", Constants::k_Cell_Data, k_ImageDataName}); + REQUIRE_NOTHROW(dataStructure.getDataRefAs(generatedDataPath)); + const auto& generatedArray = dataStructure.getDataRefAs(generatedDataPath); + REQUIRE_NOTHROW(exemplarDS.getDataRefAs(exemplarDataPath)); + const auto& exemplarArray = exemplarDS.getDataRefAs(exemplarDataPath); + UnitTest::CompareDataArrays(exemplarArray, generatedArray); + + UnitTest::CheckArraysInheritTupleDims(dataStructure); +} + +TEST_CASE("SimplnxCore::ReadImageFilter: OriginSpacing_Postprocessed", "[SimplnxCore][ReadImageFilter]") +{ + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); + + UnitTest::LoadPlugins(); + ReadImageFilter filter; + DataStructure dataStructure; + Arguments args; + + std::vector k_Origin{10.0, 20.0, 0.0}; + std::vector k_Spacing{2.0, 2.0, 1.0}; + + const DataPath inputGeometryPath({k_ImageGeometryName}); + + auto cropOptions = CropGeometryParameter::ValueType(); + cropOptions.type = CropGeometryParameter::CropValues::TypeEnum::VoxelSubvolume; + cropOptions.cropX = true; + cropOptions.cropY = true; + cropOptions.cropZ = false; + cropOptions.xBoundVoxels = {50, 150}; + cropOptions.yBoundVoxels = {50, 150}; + + args.insertOrAssign(ReadImageFilter::k_FileName_Key, k_InputImageFile); + args.insertOrAssign(ReadImageFilter::k_ImageGeometryPath_Key, inputGeometryPath); + args.insertOrAssign(ReadImageFilter::k_CellDataName_Key, static_cast(k_ImageCellDataName)); + args.insertOrAssign(ReadImageFilter::k_ImageDataArrayPath_Key, static_cast(k_ImageDataName)); + args.insertOrAssign(ReadImageFilter::k_ChangeOrigin_Key, true); + args.insertOrAssign(ReadImageFilter::k_Origin_Key, k_Origin); + args.insertOrAssign(ReadImageFilter::k_ChangeSpacing_Key, true); + args.insertOrAssign(ReadImageFilter::k_Spacing_Key, k_Spacing); + args.insertOrAssign(ReadImageFilter::k_OriginSpacingProcessing_Key, k_Postprocessed); + args.insertOrAssign(ReadImageFilter::k_CroppingOptions_Key, cropOptions); + + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions) + + auto executeResult = filter.execute(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result) + + // Compare against exemplar + DataStructure exemplarDS = UnitTest::LoadDataStructure(k_ExemplarFile); + REQUIRE_NOTHROW(dataStructure.getDataRefAs(inputGeometryPath)); + const auto& generatedGeom = dataStructure.getDataRefAs(inputGeometryPath); + REQUIRE_NOTHROW(exemplarDS.getDataRefAs(DataPath({"OriginSpacing_Postprocessed"}))); + const auto& exemplarGeom = exemplarDS.getDataRefAs(DataPath({"OriginSpacing_Postprocessed"})); + UnitTest::CompareImageGeometry(&exemplarGeom, &generatedGeom); + + DataPath generatedDataPath = inputGeometryPath.createChildPath(k_ImageCellDataName).createChildPath(k_ImageDataName); + DataPath exemplarDataPath = DataPath({"OriginSpacing_Postprocessed", Constants::k_Cell_Data, k_ImageDataName}); + REQUIRE_NOTHROW(dataStructure.getDataRefAs(generatedDataPath)); + const auto& generatedArray = dataStructure.getDataRefAs(generatedDataPath); + REQUIRE_NOTHROW(exemplarDS.getDataRefAs(exemplarDataPath)); + const auto& exemplarArray = exemplarDS.getDataRefAs(exemplarDataPath); + UnitTest::CompareDataArrays(exemplarArray, generatedArray); + + UnitTest::CheckArraysInheritTupleDims(dataStructure); +} + +TEST_CASE("SimplnxCore::ReadImageFilter: DataType_Conversion", "[SimplnxCore][ReadImageFilter]") +{ + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); + + UnitTest::LoadPlugins(); + ReadImageFilter filter; + DataStructure dataStructure; + Arguments args; + + const DataPath inputGeometryPath({k_ImageGeometryName}); + + const uint64 k_DataTypeUInt16 = 1; + + args.insertOrAssign(ReadImageFilter::k_FileName_Key, k_InputImageFile); + args.insertOrAssign(ReadImageFilter::k_ImageGeometryPath_Key, inputGeometryPath); + args.insertOrAssign(ReadImageFilter::k_CellDataName_Key, static_cast(k_ImageCellDataName)); + args.insertOrAssign(ReadImageFilter::k_ImageDataArrayPath_Key, static_cast(k_ImageDataName)); + args.insertOrAssign(ReadImageFilter::k_ChangeOrigin_Key, false); + args.insertOrAssign(ReadImageFilter::k_ChangeSpacing_Key, false); + args.insertOrAssign(ReadImageFilter::k_ChangeDataType_Key, true); + args.insertOrAssign(ReadImageFilter::k_ImageDataType_Key, k_DataTypeUInt16); + + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions) + + auto executeResult = filter.execute(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result) + + // Compare against exemplar + DataStructure exemplarDS = UnitTest::LoadDataStructure(k_ExemplarFile); + REQUIRE_NOTHROW(dataStructure.getDataRefAs(inputGeometryPath)); + const auto& generatedGeom = dataStructure.getDataRefAs(inputGeometryPath); + REQUIRE_NOTHROW(exemplarDS.getDataRefAs(DataPath({"DataType_Conversion"}))); + const auto& exemplarGeom = exemplarDS.getDataRefAs(DataPath({"DataType_Conversion"})); + UnitTest::CompareImageGeometry(&exemplarGeom, &generatedGeom); + + DataPath generatedDataPath = inputGeometryPath.createChildPath(k_ImageCellDataName).createChildPath(k_ImageDataName); + DataPath exemplarDataPath = DataPath({"DataType_Conversion", Constants::k_Cell_Data, k_ImageDataName}); + REQUIRE_NOTHROW(dataStructure.getDataRefAs(generatedDataPath)); + const auto& generatedArray = dataStructure.getDataRefAs(generatedDataPath); + REQUIRE_NOTHROW(exemplarDS.getDataRefAs(exemplarDataPath)); + const auto& exemplarArray = exemplarDS.getDataRefAs(exemplarDataPath); + UnitTest::CompareDataArrays(exemplarArray, generatedArray); + + UnitTest::CheckArraysInheritTupleDims(dataStructure); +} + +TEST_CASE("SimplnxCore::ReadImageFilter: Interaction_Crop_DataType", "[SimplnxCore][ReadImageFilter]") +{ + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); + + UnitTest::LoadPlugins(); + ReadImageFilter filter; + DataStructure dataStructure; + Arguments args; + + const DataPath inputGeometryPath({k_ImageGeometryName}); + + auto cropOptions = CropGeometryParameter::ValueType(); + cropOptions.type = CropGeometryParameter::CropValues::TypeEnum::VoxelSubvolume; + cropOptions.cropX = true; + cropOptions.cropY = true; + cropOptions.cropZ = false; + cropOptions.xBoundVoxels = {50, 150}; + cropOptions.yBoundVoxels = {50, 150}; + + const uint64 k_DataTypeUInt32 = 2; + + args.insertOrAssign(ReadImageFilter::k_FileName_Key, k_InputImageFile); + args.insertOrAssign(ReadImageFilter::k_ImageGeometryPath_Key, inputGeometryPath); + args.insertOrAssign(ReadImageFilter::k_CellDataName_Key, static_cast(k_ImageCellDataName)); + args.insertOrAssign(ReadImageFilter::k_ImageDataArrayPath_Key, static_cast(k_ImageDataName)); + args.insertOrAssign(ReadImageFilter::k_ChangeOrigin_Key, false); + args.insertOrAssign(ReadImageFilter::k_ChangeSpacing_Key, false); + args.insertOrAssign(ReadImageFilter::k_CroppingOptions_Key, cropOptions); + args.insertOrAssign(ReadImageFilter::k_ChangeDataType_Key, true); + args.insertOrAssign(ReadImageFilter::k_ImageDataType_Key, k_DataTypeUInt32); + + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions) + + auto executeResult = filter.execute(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result) + + // Compare against exemplar + DataStructure exemplarDS = UnitTest::LoadDataStructure(k_ExemplarFile); + REQUIRE_NOTHROW(dataStructure.getDataRefAs(inputGeometryPath)); + const auto& generatedGeom = dataStructure.getDataRefAs(inputGeometryPath); + REQUIRE_NOTHROW(exemplarDS.getDataRefAs(DataPath({"Interaction_Crop_DataType"}))); + const auto& exemplarGeom = exemplarDS.getDataRefAs(DataPath({"Interaction_Crop_DataType"})); + UnitTest::CompareImageGeometry(&exemplarGeom, &generatedGeom); + + DataPath generatedDataPath = inputGeometryPath.createChildPath(k_ImageCellDataName).createChildPath(k_ImageDataName); + DataPath exemplarDataPath = DataPath({"Interaction_Crop_DataType", Constants::k_Cell_Data, k_ImageDataName}); + REQUIRE_NOTHROW(dataStructure.getDataRefAs(generatedDataPath)); + const auto& generatedArray = dataStructure.getDataRefAs(generatedDataPath); + REQUIRE_NOTHROW(exemplarDS.getDataRefAs(exemplarDataPath)); + const auto& exemplarArray = exemplarDS.getDataRefAs(exemplarDataPath); + UnitTest::CompareDataArrays(exemplarArray, generatedArray); + + UnitTest::CheckArraysInheritTupleDims(dataStructure); +} + +TEST_CASE("SimplnxCore::ReadImageFilter: Interaction_All", "[SimplnxCore][ReadImageFilter]") +{ + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); + + UnitTest::LoadPlugins(); + ReadImageFilter filter; + DataStructure dataStructure; + Arguments args; + + std::vector k_Origin{5.0, 10.0, 0.0}; + std::vector k_Spacing{2.0, 2.0, 1.0}; + const DataPath inputGeometryPath({k_ImageGeometryName}); + + auto cropOptions = CropGeometryParameter::ValueType(); + cropOptions.type = CropGeometryParameter::CropValues::TypeEnum::VoxelSubvolume; + cropOptions.cropX = true; + cropOptions.cropY = true; + cropOptions.cropZ = false; + cropOptions.xBoundVoxels = {50, 150}; + cropOptions.yBoundVoxels = {50, 150}; + + const uint64 k_DataTypeUInt16 = 1; + + args.insertOrAssign(ReadImageFilter::k_FileName_Key, k_InputImageFile); + args.insertOrAssign(ReadImageFilter::k_ImageGeometryPath_Key, inputGeometryPath); + args.insertOrAssign(ReadImageFilter::k_CellDataName_Key, static_cast(k_ImageCellDataName)); + args.insertOrAssign(ReadImageFilter::k_ImageDataArrayPath_Key, static_cast(k_ImageDataName)); + args.insertOrAssign(ReadImageFilter::k_ChangeOrigin_Key, true); + args.insertOrAssign(ReadImageFilter::k_Origin_Key, k_Origin); + args.insertOrAssign(ReadImageFilter::k_ChangeSpacing_Key, true); + args.insertOrAssign(ReadImageFilter::k_Spacing_Key, k_Spacing); + args.insertOrAssign(ReadImageFilter::k_OriginSpacingProcessing_Key, k_Preprocessed); + args.insertOrAssign(ReadImageFilter::k_CroppingOptions_Key, cropOptions); + args.insertOrAssign(ReadImageFilter::k_ChangeDataType_Key, true); + args.insertOrAssign(ReadImageFilter::k_ImageDataType_Key, k_DataTypeUInt16); + + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions) + + auto executeResult = filter.execute(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result) + + // Compare against exemplar + DataStructure exemplarDS = UnitTest::LoadDataStructure(k_ExemplarFile); + REQUIRE_NOTHROW(dataStructure.getDataRefAs(inputGeometryPath)); + const auto& generatedGeom = dataStructure.getDataRefAs(inputGeometryPath); + REQUIRE_NOTHROW(exemplarDS.getDataRefAs(DataPath({"Interaction_Crop_OriginSpacing_Preprocessed_DataType"}))); + const auto& exemplarGeom = exemplarDS.getDataRefAs(DataPath({"Interaction_Crop_OriginSpacing_Preprocessed_DataType"})); + UnitTest::CompareImageGeometry(&exemplarGeom, &generatedGeom); + + DataPath generatedDataPath = inputGeometryPath.createChildPath(k_ImageCellDataName).createChildPath(k_ImageDataName); + DataPath exemplarDataPath = DataPath({"Interaction_Crop_OriginSpacing_Preprocessed_DataType", Constants::k_Cell_Data, k_ImageDataName}); + REQUIRE_NOTHROW(dataStructure.getDataRefAs(generatedDataPath)); + const auto& generatedArray = dataStructure.getDataRefAs(generatedDataPath); + REQUIRE_NOTHROW(exemplarDS.getDataRefAs(exemplarDataPath)); + const auto& exemplarArray = exemplarDS.getDataRefAs(exemplarDataPath); + UnitTest::CompareDataArrays(exemplarArray, generatedArray); + + UnitTest::CheckArraysInheritTupleDims(dataStructure); +} diff --git a/src/Plugins/SimplnxCore/test/WriteImageTest.cpp b/src/Plugins/SimplnxCore/test/WriteImageTest.cpp index b35d646991..83a273140d 100644 --- a/src/Plugins/SimplnxCore/test/WriteImageTest.cpp +++ b/src/Plugins/SimplnxCore/test/WriteImageTest.cpp @@ -1,86 +1,219 @@ -/** - * This file is auto generated from the original SimplnxCore/WriteImageFilter - * runtime information. These are the steps that need to be taken to utilize this - * unit test in the proper way. - * - * 1: Validate each of the default parameters that gets created. - * 2: Inspect the actual filter to determine if the filter in its default state - * would pass or fail BOTH the preflight() and execute() methods - * 3: UPDATE the ```REQUIRE(result.result.valid());``` code to have the proper - * - * 4: Add additional unit tests to actually test each code path within the filter - * - * There are some example Catch2 ```TEST_CASE``` sections for your inspiration. - * - * NOTE the format of the ```TEST_CASE``` macro. Please stick to this format to - * allow easier parsing of the unit tests. - * - * When you start working on this unit test remove "[WriteImageFilter][.][UNIMPLEMENTED]" - * from the TEST_CASE macro. This will enable this unit test to be run by default - * and report errors. - */ - - #include - - //TODO: PARAMETER_INCLUDES +#include "SimplnxCore/Filters/ReadImageStackFilter.hpp" #include "SimplnxCore/Filters/WriteImageFilter.hpp" #include "SimplnxCore/SimplnxCore_test_dirs.hpp" +#include "simplnx/Core/Application.hpp" +#include "simplnx/DataStructure/Geometry/ImageGeom.hpp" +#include "simplnx/Parameters/ChoicesParameter.hpp" +#include "simplnx/Parameters/GeneratedFileListParameter.hpp" +#include "simplnx/Parameters/NumberParameter.hpp" +#include "simplnx/Parameters/StringParameter.hpp" #include "simplnx/UnitTest/UnitTestCommon.hpp" -#include +#include +#include +#include + +namespace fs = std::filesystem; using namespace nx::core; -using namespace nx::core::UnitTest; -using namespace nx::core::Constants; -TEST_CASE("SimplnxCore::WriteImageFilter: Valid Filter Execution","[SimplnxCore][WriteImageFilter]") +namespace +{ +// The WriteImage test re-uses the ImageStack data from the ITKImageProcessing plugin source tree. +// The ITK plugin keeps a small set of input slices in its in-source data directory and the ITK +// test references them via `unit_test::k_DataDir`. When this test lives in SimplnxCore, the +// `k_DataDir` macro points to SimplnxCore's data directory instead, so we reach over to the ITK +// plugin's data directory using `k_SimplnxSourceDIr`. +const std::string k_ImageStackDir = std::string(unit_test::k_SimplnxSourceDIr.view()) + "/src/Plugins/ITKImageProcessing/data/ImageStack"; +const DataPath k_ImageGeomPath = {{"ImageGeometry"}}; +const DataPath k_ImageDataPath = k_ImageGeomPath.createChildPath(ImageGeom::k_CellAttributeMatrixName).createChildPath("ImageData"); + +// WriteImageFilter plane choices (ChoicesParameter index values) +constexpr uint64 k_XYPlane = 0; +constexpr uint64 k_XZPlane = 1; +constexpr uint64 k_YZPlane = 2; + +std::string CreateRandomDirName() +{ + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_int_distribution<> distrib(65, 90); + + std::string s(16, 'z'); + for(int i = 0; i < 16; ++i) + { + s[i] = static_cast(distrib(gen)); + } + + return s; +} + +void validateOutputFiles(size_t numImages, uint64 offset, const std::string& tempDirName, const std::string& tempDirPath) +{ + // Check for the existence of each image file, remove it as we go... + for(size_t i = 0; i < numImages; i++) + { + fs::path imagePath = fs::path() / fmt::format("{}/{}/slice_{:03d}.tif", unit_test::k_BinaryTestOutputDir.view(), tempDirName, i + offset); + INFO(fmt::format("Checking File: '{}' ", imagePath.string())); + REQUIRE(fs::exists(imagePath)); + REQUIRE(std::filesystem::remove(imagePath)); + } + + // Now make sure there are no files left in the directory. + int count = 0; + for(const auto& entry : std::filesystem::directory_iterator(tempDirPath)) + { + count++; + } + REQUIRE(count == 0); + + // Now delete the temp directory + try + { + std::filesystem::remove_all(tempDirPath); + std::cout << "Directory removed successfully: " << tempDirPath << std::endl; + } catch(std::filesystem::filesystem_error& e) + { + std::cout << "Error removing temp directory: " << tempDirPath << std::endl; + std::cout << " " << e.what() << std::endl; + } +} + +} // namespace + +TEST_CASE("SimplnxCore::WriteImageFilter: Write Stack", "[SimplnxCore][WriteImageFilter]") { + auto app = Application::GetOrCreateInstance(); UnitTest::LoadPlugins(); - const nx::core::UnitTest::TestFileSentinel testDataSentinel( - nx::core::unit_test::k_TestFilesDir, - "WriteImageFilter.tar.gz", - "WriteImageFilter"); + DataStructure dataStructure; + { + ReadImageStackFilter filter; + Arguments args; + + GeneratedFileListParameter::ValueType fileListInfo; + fileListInfo.inputPath = k_ImageStackDir; + fileListInfo.startIndex = 11; + fileListInfo.endIndex = 13; + fileListInfo.incrementIndex = 1; + fileListInfo.fileExtension = ".tif"; + fileListInfo.filePrefix = "slice_"; + fileListInfo.fileSuffix = ""; + fileListInfo.paddingDigits = 2; + fileListInfo.ordering = GeneratedFileListParameter::Ordering::LowToHigh; + + std::vector origin = {1.0f, 4.0f, 8.0f}; + std::vector spacing = {0.3f, 1.2f, 0.9f}; + + args.insertOrAssign(ReadImageStackFilter::k_InputFileListInfo_Key, std::make_any(fileListInfo)); + args.insertOrAssign(ReadImageStackFilter::k_ChangeOrigin_Key, true); + args.insertOrAssign(ReadImageStackFilter::k_Origin_Key, std::make_any>(origin)); + args.insertOrAssign(ReadImageStackFilter::k_ChangeSpacing_Key, true); + args.insertOrAssign(ReadImageStackFilter::k_Spacing_Key, std::make_any>(spacing)); + args.insertOrAssign(ReadImageStackFilter::k_ImageGeometryPath_Key, std::make_any(k_ImageGeomPath)); - // Read the Small IN100 Data set - auto baseDataFilePath = fs::path(fmt::format("{}/WriteImageFilter/WriteImageFilter.dream3d", nx::core::unit_test::k_TestFilesDir)); - DataStructure dataStructure = UnitTest::LoadDataStructure(baseDataFilePath); + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions) + + auto executeResult = filter.execute(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result) + } - // Instantiate the filter, a DataStructure object and an Arguments Object { + WriteImageFilter filter; + + const std::string tempDirName = CreateRandomDirName(); + const std::string tempDirPath = fmt::format("{}/{}", unit_test::k_BinaryTestOutputDir.view(), tempDirName); + const std::string path = fmt::format("{}/{}/slice.tif", unit_test::k_BinaryTestOutputDir.view(), tempDirName); + + const fs::path outputPath = fs::path() / path; + Arguments args; + const uint64 offset = 100; + args.insertOrAssign(WriteImageFilter::k_ImageGeomPath_Key, std::make_any(k_ImageGeomPath)); + args.insertOrAssign(WriteImageFilter::k_ImageArrayPath_Key, std::make_any(k_ImageDataPath)); + args.insertOrAssign(WriteImageFilter::k_FileName_Key, std::make_any(outputPath)); + args.insertOrAssign(WriteImageFilter::k_IndexOffset_Key, std::make_any(offset)); + args.insertOrAssign(WriteImageFilter::k_Plane_Key, std::make_any(k_XYPlane)); + args.insertOrAssign(WriteImageFilter::k_TotalIndexDigits_Key, std::make_any(3)); + args.insertOrAssign(WriteImageFilter::k_LeadingDigitCharacter_Key, std::make_any("0")); + + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions) + + auto executeResult = filter.execute(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result) + + const auto* imageGeom = dataStructure.getDataAs(k_ImageGeomPath); + SizeVec3 imageDims = imageGeom->getDimensions(); + + validateOutputFiles(imageDims[2], offset, tempDirName, tempDirPath); + } + + { WriteImageFilter filter; - // Create default Parameters for the filter. - // This next line is an example, your filter may be different - // args.insertOrAssign(WriteImageFilter::k_CreatedTriangleGeometryPath_Key, std::make_any(computedTriangleGeomPath)); + const std::string tempDirName = CreateRandomDirName(); + const std::string tempDirPath = fmt::format("{}/{}", unit_test::k_BinaryTestOutputDir.view(), tempDirName); + const std::string path = fmt::format("{}/{}/slice.tif", unit_test::k_BinaryTestOutputDir.view(), tempDirName); + + const fs::path outputPath = fs::path() / path; + + Arguments args; + const uint64 offset = 100; + args.insertOrAssign(WriteImageFilter::k_ImageGeomPath_Key, std::make_any(k_ImageGeomPath)); + args.insertOrAssign(WriteImageFilter::k_ImageArrayPath_Key, std::make_any(k_ImageDataPath)); + args.insertOrAssign(WriteImageFilter::k_FileName_Key, std::make_any(outputPath)); + args.insertOrAssign(WriteImageFilter::k_IndexOffset_Key, std::make_any(offset)); + args.insertOrAssign(WriteImageFilter::k_Plane_Key, std::make_any(k_XZPlane)); + args.insertOrAssign(WriteImageFilter::k_TotalIndexDigits_Key, std::make_any(3)); + args.insertOrAssign(WriteImageFilter::k_LeadingDigitCharacter_Key, std::make_any("0")); - // Preflight the filter and check result auto preflightResult = filter.preflight(dataStructure, args); SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions) - // Execute the filter and check the result auto executeResult = filter.execute(dataStructure, args); SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result) - // Write the DataStructure out to the file system -#ifdef SIMPLNX_WRITE_TEST_OUTPUT - WriteTestDataStructure(dataStructure, fs::path(fmt::format("{}/WriteImageFilterTest.dream3d", unit_test::k_BinaryTestOutputDir))); -#endif + const auto* imageGeom = dataStructure.getDataAs(k_ImageGeomPath); + SizeVec3 imageDims = imageGeom->getDimensions(); + validateOutputFiles(imageDims[1], offset, tempDirName, tempDirPath); } - // Compare exemplar data arrays with computed data arrays - //TODO: Insert verification codes - // This should be in every unit test. If you think it does not apply, please review with another engineer - UnitTest::CheckArraysInheritTupleDims(dataStructure); + { + WriteImageFilter filter; -} + const std::string tempDirName = CreateRandomDirName(); + const std::string tempDirPath = fmt::format("{}/{}", unit_test::k_BinaryTestOutputDir.view(), tempDirName); + const std::string path = fmt::format("{}/{}/slice.tif", unit_test::k_BinaryTestOutputDir.view(), tempDirName); + + const fs::path outputPath = fs::path() / path; + + Arguments args; + const uint64 offset = 100; + args.insertOrAssign(WriteImageFilter::k_ImageGeomPath_Key, std::make_any(k_ImageGeomPath)); + args.insertOrAssign(WriteImageFilter::k_ImageArrayPath_Key, std::make_any(k_ImageDataPath)); + args.insertOrAssign(WriteImageFilter::k_FileName_Key, std::make_any(outputPath)); + args.insertOrAssign(WriteImageFilter::k_IndexOffset_Key, std::make_any(offset)); + args.insertOrAssign(WriteImageFilter::k_Plane_Key, std::make_any(k_YZPlane)); + args.insertOrAssign(WriteImageFilter::k_TotalIndexDigits_Key, std::make_any(3)); + args.insertOrAssign(WriteImageFilter::k_LeadingDigitCharacter_Key, std::make_any("0")); + + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions) -//TEST_CASE("SimplnxCore::WriteImageFilter: InValid Filter Execution") -//{ -// -//} + auto executeResult = filter.execute(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result) + + const auto* imageGeom = dataStructure.getDataAs(k_ImageGeomPath); + SizeVec3 imageDims = imageGeom->getDimensions(); + + validateOutputFiles(imageDims[0], offset, tempDirName, tempDirPath); + } + + UnitTest::CheckArraysInheritTupleDims(dataStructure); +} From fd42dbbf97225b0f24caf41585888a1fc4818417 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Fri, 10 Apr 2026 00:44:34 -0400 Subject: [PATCH 09/16] Fix multi-component data type conversion in ITK image reader ConvertImageToDataStoreAsType in ReadImageUtils.hpp was only converting the first channel of multi-component (vector) images (e.g. RGB, RGBA) because std::transform was bounded by pixelContainer->Size(), which returns the number of pixels rather than the number of underlying scalar elements. For an N-component image this caused (N-1)/N of the destination buffer to be left uninitialized (zeros). Multiply pixelContainer->Size() by the number of components per pixel (derived from sizeof(PixelT)/sizeof(T)) so all channels are scaled. The itk_image_reader_test exemplar archive was regenerated with the correct conversion for the DataType_Conversion, Interaction_Crop_DataType, and Interaction_Crop_OriginSpacing_Preprocessed_DataType datasets. It is now published as itk_image_reader_test_v2.tar.gz with an updated SHA512. Both ITKImageReaderTest and ReadImageTest reference the v2 archive. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Common/ReadImageUtils.hpp | 9 ++++++++- .../ITKImageProcessing/test/CMakeLists.txt | 2 +- .../test/ITKImageReaderTest.cpp | 20 +++++++++---------- src/Plugins/SimplnxCore/test/CMakeLists.txt | 2 +- .../SimplnxCore/test/ReadImageTest.cpp | 18 ++++++++--------- 5 files changed, 29 insertions(+), 22 deletions(-) diff --git a/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Common/ReadImageUtils.hpp b/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Common/ReadImageUtils.hpp index c06946e9f0..9f3894c78c 100644 --- a/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Common/ReadImageUtils.hpp +++ b/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Common/ReadImageUtils.hpp @@ -30,9 +30,16 @@ void ConvertImageToDataStoreAsType(itk::Image& image, DataSto const auto* rawBufferPtr = reinterpret_cast(pixelContainer->GetBufferPointer()); + // pixelContainer->Size() returns the number of *pixels* (not the number of underlying elements). + // For a vector pixel type (e.g., itk::Vector), the total number of underlying elements is + // pixelContainer->Size() * numComponents. Multiply by the number of components per pixel so that + // every component is converted, not just the first channel. + constexpr usize numComponents = sizeof(PixelT) / sizeof(T); + const usize totalElements = static_cast(pixelContainer->Size()) * numComponents; + constexpr auto destMaxV = static_cast(std::numeric_limits::max()); constexpr auto originMaxV = std::numeric_limits::max(); - std::transform(rawBufferPtr, rawBufferPtr + pixelContainer->Size(), dataStore.data(), [](auto value) { + std::transform(rawBufferPtr, rawBufferPtr + totalElements, dataStore.data(), [](auto value) { float64 ratio = static_cast(value) / static_cast(originMaxV); return static_cast(ratio * destMaxV); }); diff --git a/src/Plugins/ITKImageProcessing/test/CMakeLists.txt b/src/Plugins/ITKImageProcessing/test/CMakeLists.txt index 5bbf6b9d2c..6482548fce 100644 --- a/src/Plugins/ITKImageProcessing/test/CMakeLists.txt +++ b/src/Plugins/ITKImageProcessing/test/CMakeLists.txt @@ -135,7 +135,7 @@ if(EXISTS "${DREAM3D_DATA_DIR}" AND SIMPLNX_DOWNLOAD_TEST_FILES) download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME image_flip_test_images.tar.gz SHA512 4e282a270251133004bf4b979d0d064631b618fc82f503184c602c40d388b725f81faf8e77654d285852acc3217d51534c9a71240be4a87a91dc46da7871e7d2) download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME import_image_stack_test.tar.gz SHA512 bec92d655d0d96928f616612d46b52cd51ffa5dbc1d16a64bb79040bbdd3c50f8193350771b177bb64aa68fc0ff7e459745265818c1ea34eb27431426ff15083) download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME import_image_stack_test_v2.tar.gz SHA512 b3600c072ecbdb27ed3ed7298dac88708aa94d1eed21e6b0581b772311d1e3c2c6693713026ae512c86729079b41c629067fb1a7df75697d08dbdf2dfad0f553) - download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME itk_image_reader_test.tar.gz SHA512 15c42c913df8e0aebfc4196ea96ed5fdc2fab71daacbaf27287d6aee08483acb37f2f38d88af6c549084bae892b07c7aca8537b2511920d18248c8bc18aab92f) + download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME itk_image_reader_test_v2.tar.gz SHA512 ae8c0725942a4e84da582fb31a2017f85c9fb5d21a8e5b2b557947d5bc3fd225ccfc4a1010c89ac0d184425184f25a26834f7417c9899eb67a88f197881fcb40) endif() create_pipeline_tests(PLUGIN_NAME ${PLUGIN_NAME} PIPELINE_LIST ${PREBUILT_PIPELINE_NAMES}) diff --git a/src/Plugins/ITKImageProcessing/test/ITKImageReaderTest.cpp b/src/Plugins/ITKImageProcessing/test/ITKImageReaderTest.cpp index 5024be291b..87afcd9749 100644 --- a/src/Plugins/ITKImageProcessing/test/ITKImageReaderTest.cpp +++ b/src/Plugins/ITKImageProcessing/test/ITKImageReaderTest.cpp @@ -23,7 +23,7 @@ const std::string k_ImageDataName = "ImageData"; TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: Read_Basic", "[ITKImageProcessing][ITKImageReaderFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test_v2.tar.gz", k_TestDataDirName); UnitTest::LoadPlugins(); ITKImageReaderFilter filter; @@ -66,7 +66,7 @@ TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: Read_Basic", "[ITKImageProc TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: Override_Origin", "[ITKImageProcessing][ITKImageReaderFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test_v2.tar.gz", k_TestDataDirName); UnitTest::LoadPlugins(); ITKImageReaderFilter filter; @@ -112,7 +112,7 @@ TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: Override_Origin", "[ITKImag TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: Centering_Origin", "[ITKImageProcessing][ITKImageReaderFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test_v2.tar.gz", k_TestDataDirName); UnitTest::LoadPlugins(); ITKImageReaderFilter filter; @@ -156,7 +156,7 @@ TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: Centering_Origin", "[ITKIma TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: Cropping", "[ITKImageProcessing][ITKImageReaderFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test_v2.tar.gz", k_TestDataDirName); // This block generates every combination of croppingOptions, changeOrigin, and changeSpacing and then the entire test executes for each combination std::vector spacing = {2.0, 2.0, 1.0}; @@ -233,7 +233,7 @@ TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: Cropping", "[ITKImageProces TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: Override_Spacing", "[ITKImageProcessing][ITKImageReaderFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test_v2.tar.gz", k_TestDataDirName); UnitTest::LoadPlugins(); ITKImageReaderFilter filter; @@ -279,7 +279,7 @@ TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: Override_Spacing", "[ITKIma TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: OriginSpacing_Preprocessed", "[ITKImageProcessing][ITKImageReaderFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test_v2.tar.gz", k_TestDataDirName); UnitTest::LoadPlugins(); ITKImageReaderFilter filter; @@ -337,7 +337,7 @@ TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: OriginSpacing_Preprocessed" TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: OriginSpacing_Postprocessed", "[ITKImageProcessing][ITKImageReaderFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test_v2.tar.gz", k_TestDataDirName); UnitTest::LoadPlugins(); ITKImageReaderFilter filter; @@ -396,7 +396,7 @@ TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: OriginSpacing_Postprocessed TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: DataType_Conversion", "[ITKImageProcessing][ITKImageReaderFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test_v2.tar.gz", k_TestDataDirName); UnitTest::LoadPlugins(); ITKImageReaderFilter filter; @@ -443,7 +443,7 @@ TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: DataType_Conversion", "[ITK TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: Interaction_Crop_DataType", "[ITKImageProcessing][ITKImageReaderFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test_v2.tar.gz", k_TestDataDirName); UnitTest::LoadPlugins(); ITKImageReaderFilter filter; @@ -499,7 +499,7 @@ TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: Interaction_Crop_DataType", TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: Interaction_All", "[ITKImageProcessing][ITKImageReaderFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test_v2.tar.gz", k_TestDataDirName); UnitTest::LoadPlugins(); ITKImageReaderFilter filter; diff --git a/src/Plugins/SimplnxCore/test/CMakeLists.txt b/src/Plugins/SimplnxCore/test/CMakeLists.txt index bf520cf7bc..59aee59cd4 100644 --- a/src/Plugins/SimplnxCore/test/CMakeLists.txt +++ b/src/Plugins/SimplnxCore/test/CMakeLists.txt @@ -258,7 +258,7 @@ if(EXISTS "${DREAM3D_DATA_DIR}" AND SIMPLNX_DOWNLOAD_TEST_FILES) download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME image_flip_test_images.tar.gz SHA512 4e282a270251133004bf4b979d0d064631b618fc82f503184c602c40d388b725f81faf8e77654d285852acc3217d51534c9a71240be4a87a91dc46da7871e7d2) download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME import_image_stack_test_v2.tar.gz SHA512 b3600c072ecbdb27ed3ed7298dac88708aa94d1eed21e6b0581b772311d1e3c2c6693713026ae512c86729079b41c629067fb1a7df75697d08dbdf2dfad0f553) download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME initialize_data_test_files.tar.gz SHA512 f04fe76ef96add4b775111ae7fc95233a7748a25501d9f00a8b2a162c87782b8cd2813e6e46ba7892e721976693da06965e624335dbb28224c9c5b877a05aa49) - download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME itk_image_reader_test.tar.gz SHA512 15c42c913df8e0aebfc4196ea96ed5fdc2fab71daacbaf27287d6aee08483acb37f2f38d88af6c549084bae892b07c7aca8537b2511920d18248c8bc18aab92f) + download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME itk_image_reader_test_v2.tar.gz SHA512 ae8c0725942a4e84da582fb31a2017f85c9fb5d21a8e5b2b557947d5bc3fd225ccfc4a1010c89ac0d184425184f25a26834f7417c9899eb67a88f197881fcb40) download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME k_files_v2.tar.gz SHA512 d5479ad573f8ce582e32c9f7a4824885b965f08cd8ed6025daf383758088716b1e9109905ab830b9f4d4f9df0145cbeb2534258237846c47e4a046b24fb3f72c) download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME label_triangle_geometry_test_v2.tar.gz SHA512 3f2009a1e37d8f5c983ee125335f42900f364653a417d8c49c9409a41a032c6f77a80b0623f5c71b1ae736f528d3b07949f7703bc342876f5424d884c3d4226c) download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME LosAlamosFFTExemplar.tar.gz SHA512 c4f2f6cbdaea4512ca2911cb0c3169e62449fcdb7cb99a5e64640601239a08cc09ea35f81f17b056fd38da85add95a37b244b7f844fe0abc944c1ba2f7514812) diff --git a/src/Plugins/SimplnxCore/test/ReadImageTest.cpp b/src/Plugins/SimplnxCore/test/ReadImageTest.cpp index 59265ea9a3..25fbc2154e 100644 --- a/src/Plugins/SimplnxCore/test/ReadImageTest.cpp +++ b/src/Plugins/SimplnxCore/test/ReadImageTest.cpp @@ -31,7 +31,7 @@ constexpr uint64 k_Postprocessed = 1; TEST_CASE("SimplnxCore::ReadImageFilter: Read_Basic", "[SimplnxCore][ReadImageFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test_v2.tar.gz", k_TestDataDirName); UnitTest::LoadPlugins(); ReadImageFilter filter; @@ -74,7 +74,7 @@ TEST_CASE("SimplnxCore::ReadImageFilter: Read_Basic", "[SimplnxCore][ReadImageFi TEST_CASE("SimplnxCore::ReadImageFilter: Override_Origin", "[SimplnxCore][ReadImageFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test_v2.tar.gz", k_TestDataDirName); UnitTest::LoadPlugins(); ReadImageFilter filter; @@ -120,7 +120,7 @@ TEST_CASE("SimplnxCore::ReadImageFilter: Override_Origin", "[SimplnxCore][ReadIm TEST_CASE("SimplnxCore::ReadImageFilter: Centering_Origin", "[SimplnxCore][ReadImageFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test_v2.tar.gz", k_TestDataDirName); UnitTest::LoadPlugins(); ReadImageFilter filter; @@ -164,7 +164,7 @@ TEST_CASE("SimplnxCore::ReadImageFilter: Centering_Origin", "[SimplnxCore][ReadI TEST_CASE("SimplnxCore::ReadImageFilter: Override_Spacing", "[SimplnxCore][ReadImageFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test_v2.tar.gz", k_TestDataDirName); UnitTest::LoadPlugins(); ReadImageFilter filter; @@ -210,7 +210,7 @@ TEST_CASE("SimplnxCore::ReadImageFilter: Override_Spacing", "[SimplnxCore][ReadI TEST_CASE("SimplnxCore::ReadImageFilter: OriginSpacing_Preprocessed", "[SimplnxCore][ReadImageFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test_v2.tar.gz", k_TestDataDirName); UnitTest::LoadPlugins(); ReadImageFilter filter; @@ -268,7 +268,7 @@ TEST_CASE("SimplnxCore::ReadImageFilter: OriginSpacing_Preprocessed", "[SimplnxC TEST_CASE("SimplnxCore::ReadImageFilter: OriginSpacing_Postprocessed", "[SimplnxCore][ReadImageFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test_v2.tar.gz", k_TestDataDirName); UnitTest::LoadPlugins(); ReadImageFilter filter; @@ -326,7 +326,7 @@ TEST_CASE("SimplnxCore::ReadImageFilter: OriginSpacing_Postprocessed", "[Simplnx TEST_CASE("SimplnxCore::ReadImageFilter: DataType_Conversion", "[SimplnxCore][ReadImageFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test_v2.tar.gz", k_TestDataDirName); UnitTest::LoadPlugins(); ReadImageFilter filter; @@ -373,7 +373,7 @@ TEST_CASE("SimplnxCore::ReadImageFilter: DataType_Conversion", "[SimplnxCore][Re TEST_CASE("SimplnxCore::ReadImageFilter: Interaction_Crop_DataType", "[SimplnxCore][ReadImageFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test_v2.tar.gz", k_TestDataDirName); UnitTest::LoadPlugins(); ReadImageFilter filter; @@ -429,7 +429,7 @@ TEST_CASE("SimplnxCore::ReadImageFilter: Interaction_Crop_DataType", "[SimplnxCo TEST_CASE("SimplnxCore::ReadImageFilter: Interaction_All", "[SimplnxCore][ReadImageFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test_v2.tar.gz", k_TestDataDirName); UnitTest::LoadPlugins(); ReadImageFilter filter; From 0a66a9b00f39b874a8bbcece8107cb5455644baa Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Fri, 10 Apr 2026 00:45:58 -0400 Subject: [PATCH 10/16] Revert "Fix multi-component data type conversion in ITK image reader" This reverts commit 04b49440d9729692a5354958c0eb6aa205404bc4. --- .../Common/ReadImageUtils.hpp | 9 +-------- .../ITKImageProcessing/test/CMakeLists.txt | 2 +- .../test/ITKImageReaderTest.cpp | 20 +++++++++---------- src/Plugins/SimplnxCore/test/CMakeLists.txt | 2 +- .../SimplnxCore/test/ReadImageTest.cpp | 18 ++++++++--------- 5 files changed, 22 insertions(+), 29 deletions(-) diff --git a/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Common/ReadImageUtils.hpp b/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Common/ReadImageUtils.hpp index 9f3894c78c..c06946e9f0 100644 --- a/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Common/ReadImageUtils.hpp +++ b/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Common/ReadImageUtils.hpp @@ -30,16 +30,9 @@ void ConvertImageToDataStoreAsType(itk::Image& image, DataSto const auto* rawBufferPtr = reinterpret_cast(pixelContainer->GetBufferPointer()); - // pixelContainer->Size() returns the number of *pixels* (not the number of underlying elements). - // For a vector pixel type (e.g., itk::Vector), the total number of underlying elements is - // pixelContainer->Size() * numComponents. Multiply by the number of components per pixel so that - // every component is converted, not just the first channel. - constexpr usize numComponents = sizeof(PixelT) / sizeof(T); - const usize totalElements = static_cast(pixelContainer->Size()) * numComponents; - constexpr auto destMaxV = static_cast(std::numeric_limits::max()); constexpr auto originMaxV = std::numeric_limits::max(); - std::transform(rawBufferPtr, rawBufferPtr + totalElements, dataStore.data(), [](auto value) { + std::transform(rawBufferPtr, rawBufferPtr + pixelContainer->Size(), dataStore.data(), [](auto value) { float64 ratio = static_cast(value) / static_cast(originMaxV); return static_cast(ratio * destMaxV); }); diff --git a/src/Plugins/ITKImageProcessing/test/CMakeLists.txt b/src/Plugins/ITKImageProcessing/test/CMakeLists.txt index 6482548fce..5bbf6b9d2c 100644 --- a/src/Plugins/ITKImageProcessing/test/CMakeLists.txt +++ b/src/Plugins/ITKImageProcessing/test/CMakeLists.txt @@ -135,7 +135,7 @@ if(EXISTS "${DREAM3D_DATA_DIR}" AND SIMPLNX_DOWNLOAD_TEST_FILES) download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME image_flip_test_images.tar.gz SHA512 4e282a270251133004bf4b979d0d064631b618fc82f503184c602c40d388b725f81faf8e77654d285852acc3217d51534c9a71240be4a87a91dc46da7871e7d2) download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME import_image_stack_test.tar.gz SHA512 bec92d655d0d96928f616612d46b52cd51ffa5dbc1d16a64bb79040bbdd3c50f8193350771b177bb64aa68fc0ff7e459745265818c1ea34eb27431426ff15083) download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME import_image_stack_test_v2.tar.gz SHA512 b3600c072ecbdb27ed3ed7298dac88708aa94d1eed21e6b0581b772311d1e3c2c6693713026ae512c86729079b41c629067fb1a7df75697d08dbdf2dfad0f553) - download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME itk_image_reader_test_v2.tar.gz SHA512 ae8c0725942a4e84da582fb31a2017f85c9fb5d21a8e5b2b557947d5bc3fd225ccfc4a1010c89ac0d184425184f25a26834f7417c9899eb67a88f197881fcb40) + download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME itk_image_reader_test.tar.gz SHA512 15c42c913df8e0aebfc4196ea96ed5fdc2fab71daacbaf27287d6aee08483acb37f2f38d88af6c549084bae892b07c7aca8537b2511920d18248c8bc18aab92f) endif() create_pipeline_tests(PLUGIN_NAME ${PLUGIN_NAME} PIPELINE_LIST ${PREBUILT_PIPELINE_NAMES}) diff --git a/src/Plugins/ITKImageProcessing/test/ITKImageReaderTest.cpp b/src/Plugins/ITKImageProcessing/test/ITKImageReaderTest.cpp index 87afcd9749..5024be291b 100644 --- a/src/Plugins/ITKImageProcessing/test/ITKImageReaderTest.cpp +++ b/src/Plugins/ITKImageProcessing/test/ITKImageReaderTest.cpp @@ -23,7 +23,7 @@ const std::string k_ImageDataName = "ImageData"; TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: Read_Basic", "[ITKImageProcessing][ITKImageReaderFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); UnitTest::LoadPlugins(); ITKImageReaderFilter filter; @@ -66,7 +66,7 @@ TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: Read_Basic", "[ITKImageProc TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: Override_Origin", "[ITKImageProcessing][ITKImageReaderFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); UnitTest::LoadPlugins(); ITKImageReaderFilter filter; @@ -112,7 +112,7 @@ TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: Override_Origin", "[ITKImag TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: Centering_Origin", "[ITKImageProcessing][ITKImageReaderFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); UnitTest::LoadPlugins(); ITKImageReaderFilter filter; @@ -156,7 +156,7 @@ TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: Centering_Origin", "[ITKIma TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: Cropping", "[ITKImageProcessing][ITKImageReaderFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); // This block generates every combination of croppingOptions, changeOrigin, and changeSpacing and then the entire test executes for each combination std::vector spacing = {2.0, 2.0, 1.0}; @@ -233,7 +233,7 @@ TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: Cropping", "[ITKImageProces TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: Override_Spacing", "[ITKImageProcessing][ITKImageReaderFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); UnitTest::LoadPlugins(); ITKImageReaderFilter filter; @@ -279,7 +279,7 @@ TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: Override_Spacing", "[ITKIma TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: OriginSpacing_Preprocessed", "[ITKImageProcessing][ITKImageReaderFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); UnitTest::LoadPlugins(); ITKImageReaderFilter filter; @@ -337,7 +337,7 @@ TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: OriginSpacing_Preprocessed" TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: OriginSpacing_Postprocessed", "[ITKImageProcessing][ITKImageReaderFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); UnitTest::LoadPlugins(); ITKImageReaderFilter filter; @@ -396,7 +396,7 @@ TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: OriginSpacing_Postprocessed TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: DataType_Conversion", "[ITKImageProcessing][ITKImageReaderFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); UnitTest::LoadPlugins(); ITKImageReaderFilter filter; @@ -443,7 +443,7 @@ TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: DataType_Conversion", "[ITK TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: Interaction_Crop_DataType", "[ITKImageProcessing][ITKImageReaderFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); UnitTest::LoadPlugins(); ITKImageReaderFilter filter; @@ -499,7 +499,7 @@ TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: Interaction_Crop_DataType", TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: Interaction_All", "[ITKImageProcessing][ITKImageReaderFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); UnitTest::LoadPlugins(); ITKImageReaderFilter filter; diff --git a/src/Plugins/SimplnxCore/test/CMakeLists.txt b/src/Plugins/SimplnxCore/test/CMakeLists.txt index 59aee59cd4..bf520cf7bc 100644 --- a/src/Plugins/SimplnxCore/test/CMakeLists.txt +++ b/src/Plugins/SimplnxCore/test/CMakeLists.txt @@ -258,7 +258,7 @@ if(EXISTS "${DREAM3D_DATA_DIR}" AND SIMPLNX_DOWNLOAD_TEST_FILES) download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME image_flip_test_images.tar.gz SHA512 4e282a270251133004bf4b979d0d064631b618fc82f503184c602c40d388b725f81faf8e77654d285852acc3217d51534c9a71240be4a87a91dc46da7871e7d2) download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME import_image_stack_test_v2.tar.gz SHA512 b3600c072ecbdb27ed3ed7298dac88708aa94d1eed21e6b0581b772311d1e3c2c6693713026ae512c86729079b41c629067fb1a7df75697d08dbdf2dfad0f553) download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME initialize_data_test_files.tar.gz SHA512 f04fe76ef96add4b775111ae7fc95233a7748a25501d9f00a8b2a162c87782b8cd2813e6e46ba7892e721976693da06965e624335dbb28224c9c5b877a05aa49) - download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME itk_image_reader_test_v2.tar.gz SHA512 ae8c0725942a4e84da582fb31a2017f85c9fb5d21a8e5b2b557947d5bc3fd225ccfc4a1010c89ac0d184425184f25a26834f7417c9899eb67a88f197881fcb40) + download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME itk_image_reader_test.tar.gz SHA512 15c42c913df8e0aebfc4196ea96ed5fdc2fab71daacbaf27287d6aee08483acb37f2f38d88af6c549084bae892b07c7aca8537b2511920d18248c8bc18aab92f) download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME k_files_v2.tar.gz SHA512 d5479ad573f8ce582e32c9f7a4824885b965f08cd8ed6025daf383758088716b1e9109905ab830b9f4d4f9df0145cbeb2534258237846c47e4a046b24fb3f72c) download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME label_triangle_geometry_test_v2.tar.gz SHA512 3f2009a1e37d8f5c983ee125335f42900f364653a417d8c49c9409a41a032c6f77a80b0623f5c71b1ae736f528d3b07949f7703bc342876f5424d884c3d4226c) download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME LosAlamosFFTExemplar.tar.gz SHA512 c4f2f6cbdaea4512ca2911cb0c3169e62449fcdb7cb99a5e64640601239a08cc09ea35f81f17b056fd38da85add95a37b244b7f844fe0abc944c1ba2f7514812) diff --git a/src/Plugins/SimplnxCore/test/ReadImageTest.cpp b/src/Plugins/SimplnxCore/test/ReadImageTest.cpp index 25fbc2154e..59265ea9a3 100644 --- a/src/Plugins/SimplnxCore/test/ReadImageTest.cpp +++ b/src/Plugins/SimplnxCore/test/ReadImageTest.cpp @@ -31,7 +31,7 @@ constexpr uint64 k_Postprocessed = 1; TEST_CASE("SimplnxCore::ReadImageFilter: Read_Basic", "[SimplnxCore][ReadImageFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); UnitTest::LoadPlugins(); ReadImageFilter filter; @@ -74,7 +74,7 @@ TEST_CASE("SimplnxCore::ReadImageFilter: Read_Basic", "[SimplnxCore][ReadImageFi TEST_CASE("SimplnxCore::ReadImageFilter: Override_Origin", "[SimplnxCore][ReadImageFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); UnitTest::LoadPlugins(); ReadImageFilter filter; @@ -120,7 +120,7 @@ TEST_CASE("SimplnxCore::ReadImageFilter: Override_Origin", "[SimplnxCore][ReadIm TEST_CASE("SimplnxCore::ReadImageFilter: Centering_Origin", "[SimplnxCore][ReadImageFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); UnitTest::LoadPlugins(); ReadImageFilter filter; @@ -164,7 +164,7 @@ TEST_CASE("SimplnxCore::ReadImageFilter: Centering_Origin", "[SimplnxCore][ReadI TEST_CASE("SimplnxCore::ReadImageFilter: Override_Spacing", "[SimplnxCore][ReadImageFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); UnitTest::LoadPlugins(); ReadImageFilter filter; @@ -210,7 +210,7 @@ TEST_CASE("SimplnxCore::ReadImageFilter: Override_Spacing", "[SimplnxCore][ReadI TEST_CASE("SimplnxCore::ReadImageFilter: OriginSpacing_Preprocessed", "[SimplnxCore][ReadImageFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); UnitTest::LoadPlugins(); ReadImageFilter filter; @@ -268,7 +268,7 @@ TEST_CASE("SimplnxCore::ReadImageFilter: OriginSpacing_Preprocessed", "[SimplnxC TEST_CASE("SimplnxCore::ReadImageFilter: OriginSpacing_Postprocessed", "[SimplnxCore][ReadImageFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); UnitTest::LoadPlugins(); ReadImageFilter filter; @@ -326,7 +326,7 @@ TEST_CASE("SimplnxCore::ReadImageFilter: OriginSpacing_Postprocessed", "[Simplnx TEST_CASE("SimplnxCore::ReadImageFilter: DataType_Conversion", "[SimplnxCore][ReadImageFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); UnitTest::LoadPlugins(); ReadImageFilter filter; @@ -373,7 +373,7 @@ TEST_CASE("SimplnxCore::ReadImageFilter: DataType_Conversion", "[SimplnxCore][Re TEST_CASE("SimplnxCore::ReadImageFilter: Interaction_Crop_DataType", "[SimplnxCore][ReadImageFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); UnitTest::LoadPlugins(); ReadImageFilter filter; @@ -429,7 +429,7 @@ TEST_CASE("SimplnxCore::ReadImageFilter: Interaction_Crop_DataType", "[SimplnxCo TEST_CASE("SimplnxCore::ReadImageFilter: Interaction_All", "[SimplnxCore][ReadImageFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); UnitTest::LoadPlugins(); ReadImageFilter filter; From 87ade7a94333ebe4e574356d50a1f8c70a56ba26 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Fri, 10 Apr 2026 08:09:51 -0400 Subject: [PATCH 11/16] Implement cropping in ReadImage algorithm Applies voxel and physical bounds cropping to 2D images. Uses CropImageGeomFilter in preflight to compute cropped dimensions, origin, and spacing. Execute extracts the cropped subvolume from the temp buffer before copying to DataStore. Respects pre/post- processed timing for origin/spacing overrides. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Filters/Algorithms/ReadImage.cpp | 142 ++++++++++++++++-- .../Filters/Algorithms/ReadImage.hpp | 2 + .../SimplnxCore/Filters/ReadImageFilter.cpp | 101 ++++++++++--- 3 files changed, 213 insertions(+), 32 deletions(-) diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ReadImage.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ReadImage.cpp index 6d2136ff7c..66d533e9ea 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ReadImage.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ReadImage.cpp @@ -33,16 +33,45 @@ usize BytesPerComponent(DataType dt) } } +// Cropping window inside the source image. If no cropping is used, the window covers the full image. +struct CropWindow +{ + usize srcWidth = 0; + usize srcHeight = 0; + usize dstWidth = 0; + usize dstHeight = 0; + usize xStart = 0; + usize yStart = 0; + usize numComponents = 1; +}; + struct CopyPixelDataFunctor { template - Result<> operator()(IDataArray& dataArray, const std::vector& buffer, usize totalElements) + Result<> operator()(IDataArray& dataArray, const std::vector& buffer, const CropWindow& window) { auto& dataStore = dataArray.template getIDataStoreRefAs>(); const T* typedBuffer = reinterpret_cast(buffer.data()); - for(usize i = 0; i < totalElements; i++) + + const usize nComps = window.numComponents; + const usize srcWidth = window.srcWidth; + const usize dstWidth = window.dstWidth; + const usize xStart = window.xStart; + const usize yStart = window.yStart; + + for(usize y = 0; y < window.dstHeight; y++) { - dataStore[i] = typedBuffer[i]; + const usize srcY = y + yStart; + for(usize x = 0; x < window.dstWidth; x++) + { + const usize srcX = x + xStart; + const usize srcIndex = (srcY * srcWidth + srcX) * nComps; + const usize dstIndex = (y * dstWidth + x) * nComps; + for(usize c = 0; c < nComps; c++) + { + dataStore[dstIndex + c] = typedBuffer[srcIndex + c]; + } + } } return {}; } @@ -52,7 +81,7 @@ template struct ConvertPixelDataFunctor { template - Result<> operator()(IDataArray& dataArray, const std::vector& buffer, usize totalElements) + Result<> operator()(IDataArray& dataArray, const std::vector& buffer, const CropWindow& window) { auto& dataStore = dataArray.template getIDataStoreRefAs>(); const SrcT* typedBuffer = reinterpret_cast(buffer.data()); @@ -60,10 +89,26 @@ struct ConvertPixelDataFunctor constexpr double srcMax = static_cast(std::numeric_limits::max()); constexpr double destMax = static_cast(std::numeric_limits::max()); - for(usize i = 0; i < totalElements; i++) + const usize nComps = window.numComponents; + const usize srcWidth = window.srcWidth; + const usize dstWidth = window.dstWidth; + const usize xStart = window.xStart; + const usize yStart = window.yStart; + + for(usize y = 0; y < window.dstHeight; y++) { - double normalized = static_cast(typedBuffer[i]) / srcMax; - dataStore[i] = static_cast(normalized * destMax); + const usize srcY = y + yStart; + for(usize x = 0; x < window.dstWidth; x++) + { + const usize srcX = x + xStart; + const usize srcIndex = (srcY * srcWidth + srcX) * nComps; + const usize dstIndex = (y * dstWidth + x) * nComps; + for(usize c = 0; c < nComps; c++) + { + const double normalized = static_cast(typedBuffer[srcIndex + c]) / srcMax; + dataStore[dstIndex + c] = static_cast(normalized * destMax); + } + } } return {}; } @@ -72,9 +117,9 @@ struct ConvertPixelDataFunctor struct DispatchConversionFunctor { template - Result<> operator()(DataType destType, IDataArray& dataArray, const std::vector& buffer, usize totalElements) + Result<> operator()(DataType destType, IDataArray& dataArray, const std::vector& buffer, const CropWindow& window) { - return ExecuteDataFunction(ConvertPixelDataFunctor{}, destType, dataArray, buffer, totalElements); + return ExecuteDataFunction(ConvertPixelDataFunctor{}, destType, dataArray, buffer, window); } }; } // namespace @@ -137,8 +182,81 @@ Result<> ReadImage::operator()() // Get the target DataArray auto& dataArray = m_DataStructure.getDataRefAs(m_InputValues->imageDataArrayPath); + const auto& imageGeom = m_DataStructure.getDataRefAs(m_InputValues->imageGeometryPath); + const SizeVec3 geomDims = imageGeom.getDimensions(); + + // Build a cropping window describing which source pixels go into the (already-sized) DataStore. + CropWindow window; + window.srcWidth = metadata.width; + window.srcHeight = metadata.height; + window.numComponents = metadata.numComponents; + window.dstWidth = geomDims[0]; + window.dstHeight = geomDims[1]; + window.xStart = 0; + window.yStart = 0; + + const auto& croppingOptions = m_InputValues->croppingOptions; + const bool cropImage = croppingOptions.type != CropGeometryParameter::CropValues::TypeEnum::NoCropping; + const bool crop2dImage = cropImage && (croppingOptions.cropX || croppingOptions.cropY); + if(crop2dImage) + { + if(croppingOptions.type == CropGeometryParameter::CropValues::TypeEnum::VoxelSubvolume) + { + if(croppingOptions.cropX) + { + window.xStart = static_cast(croppingOptions.xBoundVoxels[0]); + } + if(croppingOptions.cropY) + { + window.yStart = static_cast(croppingOptions.yBoundVoxels[0]); + } + } + else // PhysicalSubvolume + { + // Convert physical coordinates to source voxel indices using the file's native origin/spacing. + // The ImageGeom's origin/spacing may have been overridden in preflight, but cropping bounds are + // interpreted against whatever origin/spacing was active when the crop filter ran. In the + // Preprocessed case, overrides were applied before cropping, so we mirror them here. + FloatVec3 srcOrigin = metadata.origin.value_or(FloatVec3{0.0f, 0.0f, 0.0f}); + FloatVec3 srcSpacing = metadata.spacing.value_or(FloatVec3{1.0f, 1.0f, 1.0f}); + if(m_InputValues->originSpacingProcessing == 0) // Preprocessed + { + if(m_InputValues->changeSpacing) + { + srcSpacing = m_InputValues->spacing; + } + if(m_InputValues->changeOrigin) + { + srcOrigin = m_InputValues->origin; + if(m_InputValues->centerOrigin) + { + srcOrigin[0] = -0.5f * srcSpacing[0] * static_cast(metadata.width); + srcOrigin[1] = -0.5f * srcSpacing[1] * static_cast(metadata.height); + srcOrigin[2] = 0.0f; + } + } + } + if(croppingOptions.cropX) + { + const float64 xMin = static_cast(croppingOptions.xBoundPhysical[0]); + const int64 voxelX = static_cast((xMin - static_cast(srcOrigin[0])) / static_cast(srcSpacing[0])); + window.xStart = voxelX < 0 ? 0 : static_cast(voxelX); + } + if(croppingOptions.cropY) + { + const float64 yMin = static_cast(croppingOptions.yBoundPhysical[0]); + const int64 voxelY = static_cast((yMin - static_cast(srcOrigin[1])) / static_cast(srcSpacing[1])); + window.yStart = voxelY < 0 ? 0 : static_cast(voxelY); + } + } + } - usize totalElements = metadata.width * metadata.height * metadata.numComponents; + // Safety clamp: ensure the crop window is entirely within the source image bounds. + if(window.xStart + window.dstWidth > window.srcWidth || window.yStart + window.dstHeight > window.srcHeight) + { + return MakeErrorResult(-2001, fmt::format("Crop window (start=[{},{}], size=[{},{}]) does not fit within the source image (size=[{},{}])", window.xStart, window.yStart, window.dstWidth, + window.dstHeight, window.srcWidth, window.srcHeight)); + } // Determine if type conversion is needed DataType destType = dataArray.getDataType(); @@ -147,7 +265,7 @@ Result<> ReadImage::operator()() if(m_InputValues->changeDataType && srcType != destType) { m_MessageHandler(IFilter::Message::Type::Info, fmt::format("Converting pixel data from {} to {}", DataTypeToString(srcType), DataTypeToString(destType))); - auto convResult = ExecuteDataFunction(DispatchConversionFunctor{}, srcType, destType, dataArray, tempBuffer, totalElements); + auto convResult = ExecuteDataFunction(DispatchConversionFunctor{}, srcType, destType, dataArray, tempBuffer, window); if(convResult.invalid()) { return convResult; @@ -156,7 +274,7 @@ Result<> ReadImage::operator()() else { // Direct copy - source and dest types match - auto copyResult = ExecuteDataFunction(CopyPixelDataFunctor{}, srcType, dataArray, tempBuffer, totalElements); + auto copyResult = ExecuteDataFunction(CopyPixelDataFunctor{}, srcType, dataArray, tempBuffer, window); if(copyResult.invalid()) { return copyResult; diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ReadImage.hpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ReadImage.hpp index 9928299c1f..c025bde89e 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ReadImage.hpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ReadImage.hpp @@ -8,6 +8,7 @@ #include "simplnx/DataStructure/DataStructure.hpp" #include "simplnx/Filter/IFilter.hpp" #include "simplnx/Parameters/ChoicesParameter.hpp" +#include "simplnx/Parameters/CropGeometryParameter.hpp" #include @@ -28,6 +29,7 @@ struct SIMPLNXCORE_EXPORT ReadImageInputValues usize originSpacingProcessing = 1; // 0=Preprocessed, 1=Postprocessed bool changeDataType = false; DataType imageDataType = DataType::uint8; + CropGeometryParameter::ValueType croppingOptions; }; /** diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ReadImageFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ReadImageFilter.cpp index fa27655162..d5ac87c7a3 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ReadImageFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ReadImageFilter.cpp @@ -4,6 +4,7 @@ #include "simplnx/Common/TypesUtility.hpp" #include "simplnx/Core/Application.hpp" +#include "simplnx/DataStructure/AttributeMatrix.hpp" #include "simplnx/DataStructure/DataPath.hpp" #include "simplnx/DataStructure/DataStore.hpp" #include "simplnx/DataStructure/Geometry/ImageGeom.hpp" @@ -11,6 +12,7 @@ #include "simplnx/Filter/Actions/CreateImageGeometryAction.hpp" #include "simplnx/Filter/Actions/UpdateImageGeomAction.hpp" #include "simplnx/Filter/FilterHandle.hpp" +#include "simplnx/Filter/FilterList.hpp" #include "simplnx/Parameters/BoolParameter.hpp" #include "simplnx/Parameters/ChoicesParameter.hpp" #include "simplnx/Parameters/CropGeometryParameter.hpp" @@ -197,9 +199,8 @@ IFilter::PreflightResult ReadImageFilter::preflightImpl(const DataStructure& dat FloatVec3 origin = metadata.origin.value_or(FloatVec3{0.0f, 0.0f, 0.0f}); FloatVec3 spacing = metadata.spacing.value_or(FloatVec3{1.0f, 1.0f, 1.0f}); - // Apply origin/spacing overrides (Preprocessed = before cropping) - if(originSpacingProcessing == 0) // Preprocessed - { + // Lambda to apply origin/spacing overrides consistently at either pre- or post-processing stage. + auto applyOriginSpacingOverrides = [&]() { if(shouldChangeSpacing) { spacing = FloatVec3{static_cast(spacingValues[0]), static_cast(spacingValues[1]), static_cast(spacingValues[2])}; @@ -216,31 +217,90 @@ IFilter::PreflightResult ReadImageFilter::preflightImpl(const DataStructure& dat } } } - } + }; - // TODO: Cropping will be added in a future iteration. For now, cropping options are accepted - // but not applied. The implementation will delegate to CropImageGeometry filter similar to - // the ITK-based ReadImagePreflight. + // Apply origin/spacing overrides (Preprocessed = before cropping) + if(originSpacingProcessing == 0) // Preprocessed + { + applyOriginSpacingOverrides(); + } - // Apply origin/spacing overrides (Postprocessed = after cropping) - if(originSpacingProcessing == 1) // Postprocessed + // Apply cropping if requested by building a temporary ImageGeom and running CropImageGeomFilter preflight. + bool cropImage = croppingOptions.type != CropGeometryParameter::CropValues::TypeEnum::NoCropping; + bool crop2dImage = cropImage && (croppingOptions.cropX || croppingOptions.cropY); + if(crop2dImage) { - if(shouldChangeSpacing) + FilterList* filterListPtr = Application::Instance()->getFilterList(); + if(filterListPtr == nullptr || !filterListPtr->containsPlugin(k_SimplnxCorePluginId)) { - spacing = FloatVec3{static_cast(spacingValues[0]), static_cast(spacingValues[1]), static_cast(spacingValues[2])}; + return IFilter::MakePreflightErrorResult(-18542, "The plugin SimplnxCore was not instantiated in this instance, so image cropping is not available."); } - if(shouldChangeOrigin) + std::unique_ptr cropImageGeomFilter = filterListPtr->createFilter(k_CropImageGeomFilterHandle); + if(cropImageGeomFilter == nullptr) { - origin = FloatVec3{static_cast(originValues[0]), static_cast(originValues[1]), static_cast(originValues[2])}; - if(shouldCenterOrigin) - { - for(usize i = 0; i < 3; i++) - { - origin[i] = -0.5f * spacing[i] * static_cast(dims[i]); - } - } + return IFilter::MakePreflightErrorResult(-18543, "Unable to create an instance of the crop image geometry filter, so image cropping is not available."); + } + + DataStructure tmpDs; + DataPath tmpGeomPath = DataPath({"tmpGeom"}); + ImageGeom* tmpGeomPtr = ImageGeom::Create(tmpDs, tmpGeomPath.getTargetName()); + AttributeMatrix* amPtr = AttributeMatrix::Create(tmpDs, "CellData", std::vector(dims.crbegin(), dims.crend()), tmpGeomPtr->getId()); + tmpGeomPtr->setCellData(*amPtr); + tmpGeomPtr->setDimensions(dims); + tmpGeomPtr->setOrigin(origin); + tmpGeomPtr->setSpacing(spacing); + + Arguments cropImageGeomArgs; + cropImageGeomArgs.insertOrAssign("input_image_geometry_path", std::make_any(tmpGeomPath)); + cropImageGeomArgs.insertOrAssign("use_physical_bounds", std::make_any(croppingOptions.type == CropGeometryParameter::CropValues::TypeEnum::PhysicalSubvolume)); + cropImageGeomArgs.insertOrAssign("crop_x_dim", std::make_any(croppingOptions.cropX)); + cropImageGeomArgs.insertOrAssign("crop_y_dim", std::make_any(croppingOptions.cropY)); + cropImageGeomArgs.insertOrAssign("crop_z_dim", std::make_any(false)); // 2D image => no Z crop + if(croppingOptions.type == CropGeometryParameter::CropValues::TypeEnum::VoxelSubvolume) + { + cropImageGeomArgs.insertOrAssign("min_voxel", + std::make_any({static_cast(croppingOptions.xBoundVoxels[0]), static_cast(croppingOptions.yBoundVoxels[0]), + static_cast(croppingOptions.zBoundVoxels[0])})); + cropImageGeomArgs.insertOrAssign("max_voxel", + std::make_any({static_cast(croppingOptions.xBoundVoxels[1]), static_cast(croppingOptions.yBoundVoxels[1]), + static_cast(croppingOptions.zBoundVoxels[1])})); + } + else + { + cropImageGeomArgs.insertOrAssign("min_coord", std::make_any({static_cast(croppingOptions.xBoundPhysical[0]), + static_cast(croppingOptions.yBoundPhysical[0]), + static_cast(croppingOptions.zBoundPhysical[0])})); + cropImageGeomArgs.insertOrAssign("max_coord", std::make_any({static_cast(croppingOptions.xBoundPhysical[1]), + static_cast(croppingOptions.yBoundPhysical[1]), + static_cast(croppingOptions.zBoundPhysical[1])})); + } + cropImageGeomArgs.insertOrAssign("remove_original_geometry", std::make_any(true)); + + IFilter::PreflightResult cropImageResult = cropImageGeomFilter->preflight(tmpDs, cropImageGeomArgs); + if(cropImageResult.outputActions.invalid()) + { + return cropImageResult; } + + Result<> actionsResult = cropImageResult.outputActions.value().applyAll(tmpDs, IDataAction::Mode::Preflight); + if(actionsResult.invalid()) + { + return {ConvertResultTo(std::move(actionsResult), {})}; + } + // Apply any updated values (e.g. UpdateImageGeomAction) to the temporary image geom so we can read + // the cropped origin/spacing from the geometry after execution semantics of applyAll. + + const auto& croppedGeom = tmpDs.getDataRefAs(tmpGeomPath); + dims = croppedGeom.getDimensions().toContainer>(); + spacing = croppedGeom.getSpacing().toContainer>(); + origin = croppedGeom.getOrigin().toContainer>(); + } + + // Apply origin/spacing overrides (Postprocessed = after cropping) + if(originSpacingProcessing == 1) // Postprocessed + { + applyOriginSpacingOverrides(); } // Determine the data type for the created array @@ -310,6 +370,7 @@ Result<> ReadImageFilter::executeImpl(DataStructure& dataStructure, const Argume inputValues.originSpacingProcessing = filterArgs.value(k_OriginSpacingProcessing_Key); inputValues.changeDataType = filterArgs.value(k_ChangeDataType_Key); inputValues.imageDataType = ConvertChoiceToDataType(filterArgs.value(k_ImageDataType_Key)); + inputValues.croppingOptions = filterArgs.value(k_CroppingOptions_Key); return ReadImage(dataStructure, messageHandler, shouldCancel, &inputValues)(); } From c89f3f3ea9c2ae9a8a0017dba513770961c32fe6 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Fri, 10 Apr 2026 08:56:04 -0400 Subject: [PATCH 12/16] Fix ReadImageStack resample and grayscale sub-filter integration The ReadImageStackFilter preflight creates the resampled image geometry at a temporary "_resampled" path in the main DataStructure and renames it back to the original path via a deferred action that runs AFTER executeImpl completes. Similarly, when grayscale conversion is enabled, the preflight creates the output array with a "grayscale_" prefix and renames it back via a deferred action. The algorithm was writing to the ORIGINAL (un-resampled, un-prefixed) paths during execute, which pointed to stale arrays that still had the original un-resampled dimensions and/or RGB component count. This caused dimension mismatch errors (resample tests) and component count mismatch errors (grayscale tests) when copying slice data into the destination. Match the ITK version of ReadImageStack by updating destImageGeomPath to the "_resampled" path after running the resample sub-filter, and by using the "grayscale_" prefixed array name for the destination path when grayscale conversion is requested. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../SimplnxCore/Filters/Algorithms/ReadImageStack.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ReadImageStack.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ReadImageStack.cpp index 9e209fe9f7..47a98e6915 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ReadImageStack.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ReadImageStack.cpp @@ -247,6 +247,11 @@ Result<> ReadImageStackImpl(DataStructure& dataStructure, const ReadImageStackIn return result; } } + + // The preflight creates the resampled geometry at a "_resampled" path in the main DataStructure. + // The final rename back to the original path happens via a deferred action AFTER execute completes, + // so during algorithm execution we must write to the "_resampled" path. + destImageGeomPath = DataPath({imageGeomPath.getTargetName() + "_resampled"}); } // ======================= Convert to GrayScale Section =================== @@ -323,7 +328,11 @@ Result<> ReadImageStackImpl(DataStructure& dataStructure, const ReadImageStackIn } // Copy that into the output array... - DataPath destImageDataPath = destImageGeomPath.createChildPath(cellDataName).createChildPath(imageArrayName); + // When grayscale conversion is requested, the preflight creates the destination array with a + // "grayscale_" prefix and renames it back to the original name via a deferred action after + // execute completes. During algorithm execution we must write to the prefixed path. + DataPath destImageDataPath = convertToGrayscale ? destImageGeomPath.createChildPath(cellDataName).createChildPath("grayscale_" + imageArrayName) : + destImageGeomPath.createChildPath(cellDataName).createChildPath(imageArrayName); auto& outputData = dataStructure.getDataRefAs>(destImageDataPath); AbstractDataStore& outputDataStore = outputData.getDataStoreRef(); Result<> result = outputDataStore.copyFrom(destTupleIndex, srcDataStore, 0, destTuplesPerSlice); From d09e99a2dc6d5d56b2dc819ea58211192ee36a92 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Fri, 10 Apr 2026 09:29:24 -0400 Subject: [PATCH 13/16] Fix multi-component data type conversion in ITK image reader ConvertImageToDataStoreAsType in ReadImageUtils.hpp was only converting the first 1/N of the scalar buffer for multi-component (vector) images like RGB or RGBA. The std::transform range used pixelContainer->Size(), which returns the number of pixels in the container, not the number of underlying scalar elements. For an itk::Vector image, Size() returned width*height while the raw buffer cast to uint8* actually held width*height*3 elements, leaving two-thirds of the destination buffer uninitialized. Multiply by itk::NumericTraits::GetLength() (the canonical ITK component count) so every scalar in the buffer is converted. This breaks the DataType_Conversion, Interaction_Crop_DataType, and Interaction_All test exemplars in itk_image_reader_test.tar.gz, which were generated by the buggy code. The exemplar archive must be regenerated before those tests will pass. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/ITKImageProcessing/Common/ReadImageUtils.hpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Common/ReadImageUtils.hpp b/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Common/ReadImageUtils.hpp index c06946e9f0..2a2718e43e 100644 --- a/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Common/ReadImageUtils.hpp +++ b/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Common/ReadImageUtils.hpp @@ -30,9 +30,16 @@ void ConvertImageToDataStoreAsType(itk::Image& image, DataSto const auto* rawBufferPtr = reinterpret_cast(pixelContainer->GetBufferPointer()); + // pixelContainer->Size() returns the number of pixels, not the number of scalar + // elements. For multi-component pixel types (e.g. itk::Vector), the + // underlying scalar buffer contains pixelCount * componentsPerPixel elements. + // Multiply by the component count so std::transform iterates over every scalar. + const std::size_t componentsPerPixel = itk::NumericTraits::GetLength(); + const std::size_t totalElements = pixelContainer->Size() * componentsPerPixel; + constexpr auto destMaxV = static_cast(std::numeric_limits::max()); constexpr auto originMaxV = std::numeric_limits::max(); - std::transform(rawBufferPtr, rawBufferPtr + pixelContainer->Size(), dataStore.data(), [](auto value) { + std::transform(rawBufferPtr, rawBufferPtr + totalElements, dataStore.data(), [](auto value) { float64 ratio = static_cast(value) / static_cast(originMaxV); return static_cast(ratio * destMaxV); }); From 65fad887ed54e4d29c0ea31681c494f5931f0736 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Fri, 10 Apr 2026 10:20:26 -0400 Subject: [PATCH 14/16] Update ITK reader test sentinels to preserve test data directory Change TestFileSentinel construction in all ten ITKImageReaderFilter test cases to pass decompressFiles=false and removeTemp=false. This matches the pattern already used by the new SimplnxCore ReadImageFilter tests: the sentinel no longer decompresses the archive on construction (which would overwrite a freshly-regenerated exemplar with the stale archive contents) and no longer deletes the extracted directory on teardown. This allows the regeneration pipeline and both test suites to iterate against the same exemplar without interference. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../test/ITKImageReaderTest.cpp | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Plugins/ITKImageProcessing/test/ITKImageReaderTest.cpp b/src/Plugins/ITKImageProcessing/test/ITKImageReaderTest.cpp index 5024be291b..a4523f3e87 100644 --- a/src/Plugins/ITKImageProcessing/test/ITKImageReaderTest.cpp +++ b/src/Plugins/ITKImageProcessing/test/ITKImageReaderTest.cpp @@ -23,7 +23,7 @@ const std::string k_ImageDataName = "ImageData"; TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: Read_Basic", "[ITKImageProcessing][ITKImageReaderFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName, false, false); UnitTest::LoadPlugins(); ITKImageReaderFilter filter; @@ -66,7 +66,7 @@ TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: Read_Basic", "[ITKImageProc TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: Override_Origin", "[ITKImageProcessing][ITKImageReaderFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName, false, false); UnitTest::LoadPlugins(); ITKImageReaderFilter filter; @@ -112,7 +112,7 @@ TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: Override_Origin", "[ITKImag TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: Centering_Origin", "[ITKImageProcessing][ITKImageReaderFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName, false, false); UnitTest::LoadPlugins(); ITKImageReaderFilter filter; @@ -156,7 +156,7 @@ TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: Centering_Origin", "[ITKIma TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: Cropping", "[ITKImageProcessing][ITKImageReaderFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName, false, false); // This block generates every combination of croppingOptions, changeOrigin, and changeSpacing and then the entire test executes for each combination std::vector spacing = {2.0, 2.0, 1.0}; @@ -233,7 +233,7 @@ TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: Cropping", "[ITKImageProces TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: Override_Spacing", "[ITKImageProcessing][ITKImageReaderFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName, false, false); UnitTest::LoadPlugins(); ITKImageReaderFilter filter; @@ -279,7 +279,7 @@ TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: Override_Spacing", "[ITKIma TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: OriginSpacing_Preprocessed", "[ITKImageProcessing][ITKImageReaderFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName, false, false); UnitTest::LoadPlugins(); ITKImageReaderFilter filter; @@ -337,7 +337,7 @@ TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: OriginSpacing_Preprocessed" TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: OriginSpacing_Postprocessed", "[ITKImageProcessing][ITKImageReaderFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName, false, false); UnitTest::LoadPlugins(); ITKImageReaderFilter filter; @@ -396,7 +396,7 @@ TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: OriginSpacing_Postprocessed TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: DataType_Conversion", "[ITKImageProcessing][ITKImageReaderFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName, false, false); UnitTest::LoadPlugins(); ITKImageReaderFilter filter; @@ -443,7 +443,7 @@ TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: DataType_Conversion", "[ITK TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: Interaction_Crop_DataType", "[ITKImageProcessing][ITKImageReaderFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName, false, false); UnitTest::LoadPlugins(); ITKImageReaderFilter filter; @@ -499,7 +499,7 @@ TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: Interaction_Crop_DataType", TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: Interaction_All", "[ITKImageProcessing][ITKImageReaderFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName, false, false); UnitTest::LoadPlugins(); ITKImageReaderFilter filter; From b651a7c3b019d4cbd473cbdab41a1bf107f87564 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Fri, 10 Apr 2026 11:10:49 -0400 Subject: [PATCH 15/16] Data: SIMPLNX will now download Image Reading test data --- src/Plugins/ITKImageProcessing/test/CMakeLists.txt | 3 --- test/CMakeLists.txt | 11 ++++++++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/Plugins/ITKImageProcessing/test/CMakeLists.txt b/src/Plugins/ITKImageProcessing/test/CMakeLists.txt index 5bbf6b9d2c..759075b18f 100644 --- a/src/Plugins/ITKImageProcessing/test/CMakeLists.txt +++ b/src/Plugins/ITKImageProcessing/test/CMakeLists.txt @@ -133,9 +133,6 @@ if(EXISTS "${DREAM3D_DATA_DIR}" AND SIMPLNX_DOWNLOAD_TEST_FILES) endif() download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME fiji_montage.tar.gz SHA512 70139babc838ce3ab1f5adddfddc86dcc51996e614c6c2d757bcb2e59e8ebdc744dac269233494b1ef8d09397aecb4ccca3384f0a91bb017f2cf6309c4ac40fa) download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME image_flip_test_images.tar.gz SHA512 4e282a270251133004bf4b979d0d064631b618fc82f503184c602c40d388b725f81faf8e77654d285852acc3217d51534c9a71240be4a87a91dc46da7871e7d2) - download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME import_image_stack_test.tar.gz SHA512 bec92d655d0d96928f616612d46b52cd51ffa5dbc1d16a64bb79040bbdd3c50f8193350771b177bb64aa68fc0ff7e459745265818c1ea34eb27431426ff15083) - download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME import_image_stack_test_v2.tar.gz SHA512 b3600c072ecbdb27ed3ed7298dac88708aa94d1eed21e6b0581b772311d1e3c2c6693713026ae512c86729079b41c629067fb1a7df75697d08dbdf2dfad0f553) - download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME itk_image_reader_test.tar.gz SHA512 15c42c913df8e0aebfc4196ea96ed5fdc2fab71daacbaf27287d6aee08483acb37f2f38d88af6c549084bae892b07c7aca8537b2511920d18248c8bc18aab92f) endif() create_pipeline_tests(PLUGIN_NAME ${PLUGIN_NAME} PIPELINE_LIST ${PREBUILT_PIPELINE_NAMES}) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index aae13f39c1..4aa0e26ce4 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -93,7 +93,16 @@ target_include_directories(simplnx_test PRIVATE ${SIMPLNX_GENERATED_DIR}) catch_discover_tests(simplnx_test) -download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME simpl_json_exemplars.tar.gz SHA512 550a1ad3c9c31b1093d961fdda2057d63a48f08c04d2831c70c1ec65d499f39b354a5a569903ff68e9b703ef45c23faba340aa0c61f9a927fbc845c15b87d05e ) + +if(EXISTS "${DREAM3D_DATA_DIR}" AND SIMPLNX_DOWNLOAD_TEST_FILES) + if(NOT EXISTS ${DREAM3D_DATA_DIR}/TestFiles/) + file(MAKE_DIRECTORY "${DREAM3D_DATA_DIR}/TestFiles/") + endif() + download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME simpl_json_exemplars.tar.gz SHA512 550a1ad3c9c31b1093d961fdda2057d63a48f08c04d2831c70c1ec65d499f39b354a5a569903ff68e9b703ef45c23faba340aa0c61f9a927fbc845c15b87d05e ) + download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME import_image_stack_test.tar.gz SHA512 bec92d655d0d96928f616612d46b52cd51ffa5dbc1d16a64bb79040bbdd3c50f8193350771b177bb64aa68fc0ff7e459745265818c1ea34eb27431426ff15083) + download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME import_image_stack_test_v2.tar.gz SHA512 b3600c072ecbdb27ed3ed7298dac88708aa94d1eed21e6b0581b772311d1e3c2c6693713026ae512c86729079b41c629067fb1a7df75697d08dbdf2dfad0f553) + download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME itk_image_reader_test.tar.gz SHA512 15c42c913df8e0aebfc4196ea96ed5fdc2fab71daacbaf27287d6aee08483acb37f2f38d88af6c549084bae892b07c7aca8537b2511920d18248c8bc18aab92f) +endif() add_custom_target(Copy_simplnx_Test_Data ALL COMMAND ${CMAKE_COMMAND} -E copy_directory_if_different "${simplnx_SOURCE_DIR}/test/Data" "${DATA_DEST_DIR}/Test_Data" From 43284d23b3c70fac0cfce211004e2aecd0e67fd1 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Fri, 10 Apr 2026 12:21:03 -0400 Subject: [PATCH 16/16] TEST: Fix failing unit tests by uploading new data archives (v3) --- ...-07-arraycalculator-memory-optimization.md | 1250 ----------------- ...aycalculator-memory-optimization-design.md | 289 ---- .../Filters/ITKImageWriterFilter.cpp | 1 - .../ITKImageProcessing/test/CMakeLists.txt | 1 - .../test/ITKImageReaderTest.cpp | 24 +- .../test/ITKImportImageStackTest.cpp | 65 +- .../Filters/Algorithms/ReadImage.cpp | 2 +- .../SimplnxCore/Filters/ReadImageFilter.cpp | 19 +- .../SimplnxCore/Filters/ReadImageFilter.hpp | 4 +- .../Filters/ReadImageStackFilter.hpp | 4 +- .../SimplnxCore/Filters/WriteImageFilter.cpp | 17 +- .../SimplnxCore/Filters/WriteImageFilter.hpp | 4 +- src/Plugins/SimplnxCore/test/CMakeLists.txt | 3 - .../SimplnxCore/test/ReadImageStackTest.cpp | 65 +- .../SimplnxCore/test/ReadImageTest.cpp | 22 +- .../Utilities/ImageIO/ImageMetadata.hpp | 14 +- src/simplnx/Utilities/ImageIO/StbImageIO.cpp | 9 +- src/simplnx/Utilities/ImageIO/TiffImageIO.cpp | 36 +- test/CMakeLists.txt | 5 +- vcpkg-configuration.json | 2 +- vcpkg.json | 2 +- 21 files changed, 141 insertions(+), 1697 deletions(-) delete mode 100644 docs/superpowers/plans/2026-04-07-arraycalculator-memory-optimization.md delete mode 100644 docs/superpowers/specs/2026-04-07-arraycalculator-memory-optimization-design.md diff --git a/docs/superpowers/plans/2026-04-07-arraycalculator-memory-optimization.md b/docs/superpowers/plans/2026-04-07-arraycalculator-memory-optimization.md deleted file mode 100644 index a8db154391..0000000000 --- a/docs/superpowers/plans/2026-04-07-arraycalculator-memory-optimization.md +++ /dev/null @@ -1,1250 +0,0 @@ -# ArrayCalculator Memory Optimization Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Eliminate unnecessary memory allocations in the ArrayCalculator evaluator by introducing a CalcBuffer RAII sentinel class, making the parser data-free, and enabling direct-write to the output array. - -**Architecture:** CalcBuffer wraps Float64Array references (borrowed or owned) with automatic cleanup via DataStructure::removeData(). The parser produces data-free RPN items (DataPath + metadata, no DataObject allocation). The evaluator creates a local DataStructure for temp arrays, uses a CalcBuffer stack where intermediates are freed via RAII when consumed, and the last operator writes directly into the output DataArray when the output type is float64. - -**Tech Stack:** C++20, simplnx DataStructure/DataArray/DataStore, Catch2 tests - -**Design spec:** `docs/superpowers/specs/2026-04-07-arraycalculator-memory-optimization-design.md` - ---- - -### Task 1: Establish Baseline — Verify All Existing Tests Pass - -**Files:** -- Read: `src/Plugins/SimplnxCore/test/ArrayCalculatorTest.cpp` - -This task ensures we have a clean starting point before making changes. - -- [ ] **Step 1: Build the project** - -```bash -cd /Users/mjackson/Workspace2/DREAM3D-Build/simplnx-Rel && cmake --build . --target SimplnxCore -``` - -Expected: Build succeeds with no errors. - -- [ ] **Step 2: Run existing ArrayCalculator tests** - -```bash -cd /Users/mjackson/Workspace2/DREAM3D-Build/simplnx-Rel && ctest -R "SimplnxCore::ArrayCalculatorFilter" --verbose -``` - -Expected: All test cases pass (Filter Execution, Tokenizer, Array Resolution, Built-in Constants, Modulo Operator, Tuple Component Indexing, Sub-expression Component Access, Multi-word Array Names, Sub-expression Tuple Component Extraction). - ---- - -### Task 2: Add CalcBuffer Class Declaration to Header - -**Files:** -- Modify: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ArrayCalculator.hpp` - -Add the CalcBuffer class declaration. This task adds new code only — nothing is removed or changed yet. - -- [ ] **Step 1: Add CalcBuffer class declaration after the OperatorDef struct (after line 133)** - -Insert the following class declaration between the `OperatorDef` struct and the `CalcValue` struct (before line 138): - -```cpp -// --------------------------------------------------------------------------- -// RAII sentinel for temporary Float64Arrays in the evaluator. -// Move-only. When an Owned CalcBuffer is destroyed, it removes its -// DataArray from the scratch DataStructure via removeData(). -// --------------------------------------------------------------------------- -class SIMPLNXCORE_EXPORT CalcBuffer -{ -public: - // --- Factory methods --- - - /** - * @brief Zero-copy reference to an existing Float64Array in the real DataStructure. - * Read-only. Destructor: no-op. - */ - static CalcBuffer borrow(const Float64Array& source); - - /** - * @brief Allocate a temp Float64Array in tempDS and convert source data from any numeric type. - * Owned. Destructor: removes the temp array from tempDS. - */ - static CalcBuffer convertFrom(DataStructure& tempDS, const IDataArray& source, const std::string& name); - - /** - * @brief Allocate a 1-element temp Float64Array with the given scalar value. - * Owned. Destructor: removes the temp array from tempDS. - */ - static CalcBuffer scalar(DataStructure& tempDS, float64 value, const std::string& name); - - /** - * @brief Allocate an empty temp Float64Array with the given shape. - * Owned. Destructor: removes the temp array from tempDS. - */ - static CalcBuffer allocate(DataStructure& tempDS, const std::string& name, std::vector tupleShape, std::vector compShape); - - /** - * @brief Wrap the output DataArray for direct writing. - * Not owned. Destructor: no-op. - */ - static CalcBuffer wrapOutput(DataArray& outputArray); - - // --- Move-only, non-copyable --- - CalcBuffer(CalcBuffer&& other) noexcept; - CalcBuffer& operator=(CalcBuffer&& other) noexcept; - ~CalcBuffer(); - - CalcBuffer(const CalcBuffer&) = delete; - CalcBuffer& operator=(const CalcBuffer&) = delete; - - // --- Element access --- - float64 read(usize index) const; - void write(usize index, float64 value); - void fill(float64 value); - - // --- Metadata --- - usize size() const; - usize numTuples() const; - usize numComponents() const; - std::vector tupleShape() const; - std::vector compShape() const; - bool isScalar() const; - bool isOwned() const; - bool isOutputDirect() const; - void markAsScalar(); - - // --- Access underlying array (for final copy to non-float64 output) --- - const Float64Array& array() const; - -private: - CalcBuffer() = default; - - enum class Storage - { - Borrowed, - Owned, - OutputDirect - }; - - Storage m_Storage = Storage::Owned; - - // Borrowed: const pointer to source Float64Array in real DataStructure - const Float64Array* m_BorrowedArray = nullptr; - - // Owned: pointer to temp Float64Array + reference to its DataStructure for cleanup - DataStructure* m_TempDS = nullptr; - DataObject::IdType m_ArrayId = 0; - Float64Array* m_OwnedArray = nullptr; - - // OutputDirect: writable pointer to output DataArray - DataArray* m_OutputArray = nullptr; - - bool m_IsScalar = false; -}; -``` - -- [ ] **Step 2: Build to verify the header compiles** - -```bash -cd /Users/mjackson/Workspace2/DREAM3D-Build/simplnx-Rel && cmake --build . --target SimplnxCore -``` - -Expected: Build succeeds. CalcBuffer is declared but not yet defined — the linker won't complain because nothing references it yet. - -- [ ] **Step 3: Commit** - -```bash -git add src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ArrayCalculator.hpp -git commit -m "ENH: Add CalcBuffer RAII sentinel class declaration to ArrayCalculator.hpp" -``` - ---- - -### Task 3: Implement CalcBuffer in ArrayCalculator.cpp - -**Files:** -- Modify: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ArrayCalculator.cpp` - -Add the full CalcBuffer implementation. Place it after the anonymous namespace closing brace (after line 250) and before the `getOperatorRegistry()` function (line 255). This keeps it near the top of the file with other utility code. - -- [ ] **Step 1: Add CalcBuffer implementation** - -Insert the following after the anonymous namespace (after line 250, before `getOperatorRegistry()`): - -```cpp -// =========================================================================== -// CalcBuffer implementation -// =========================================================================== - -CalcBuffer::CalcBuffer(CalcBuffer&& other) noexcept -: m_Storage(other.m_Storage) -, m_BorrowedArray(other.m_BorrowedArray) -, m_TempDS(other.m_TempDS) -, m_ArrayId(other.m_ArrayId) -, m_OwnedArray(other.m_OwnedArray) -, m_OutputArray(other.m_OutputArray) -, m_IsScalar(other.m_IsScalar) -{ - other.m_TempDS = nullptr; - other.m_BorrowedArray = nullptr; - other.m_OwnedArray = nullptr; - other.m_OutputArray = nullptr; -} - -CalcBuffer& CalcBuffer::operator=(CalcBuffer&& other) noexcept -{ - if(this != &other) - { - // Clean up current state - if(m_Storage == Storage::Owned && m_TempDS != nullptr) - { - m_TempDS->removeData(m_ArrayId); - } - - m_Storage = other.m_Storage; - m_BorrowedArray = other.m_BorrowedArray; - m_TempDS = other.m_TempDS; - m_ArrayId = other.m_ArrayId; - m_OwnedArray = other.m_OwnedArray; - m_OutputArray = other.m_OutputArray; - m_IsScalar = other.m_IsScalar; - - other.m_TempDS = nullptr; - other.m_BorrowedArray = nullptr; - other.m_OwnedArray = nullptr; - other.m_OutputArray = nullptr; - } - return *this; -} - -CalcBuffer::~CalcBuffer() -{ - if(m_Storage == Storage::Owned && m_TempDS != nullptr) - { - m_TempDS->removeData(m_ArrayId); - } -} - -CalcBuffer CalcBuffer::borrow(const Float64Array& source) -{ - CalcBuffer buf; - buf.m_Storage = Storage::Borrowed; - buf.m_BorrowedArray = &source; - buf.m_IsScalar = false; - return buf; -} - -CalcBuffer CalcBuffer::convertFrom(DataStructure& tempDS, const IDataArray& source, const std::string& name) -{ - std::vector tupleShape = source.getTupleShape(); - std::vector compShape = source.getComponentShape(); - Float64Array* destArr = Float64Array::CreateWithStore(tempDS, name, tupleShape, compShape); - - usize totalElements = source.getSize(); - ExecuteDataFunction(CopyToFloat64Functor{}, source.getDataType(), source, *destArr); - - CalcBuffer buf; - buf.m_Storage = Storage::Owned; - buf.m_TempDS = &tempDS; - buf.m_ArrayId = destArr->getId(); - buf.m_OwnedArray = destArr; - buf.m_IsScalar = false; - return buf; -} - -CalcBuffer CalcBuffer::scalar(DataStructure& tempDS, float64 value, const std::string& name) -{ - Float64Array* arr = Float64Array::CreateWithStore(tempDS, name, std::vector{1}, std::vector{1}); - (*arr)[0] = value; - - CalcBuffer buf; - buf.m_Storage = Storage::Owned; - buf.m_TempDS = &tempDS; - buf.m_ArrayId = arr->getId(); - buf.m_OwnedArray = arr; - buf.m_IsScalar = true; - return buf; -} - -CalcBuffer CalcBuffer::allocate(DataStructure& tempDS, const std::string& name, std::vector tupleShape, std::vector compShape) -{ - Float64Array* arr = Float64Array::CreateWithStore(tempDS, name, tupleShape, compShape); - - CalcBuffer buf; - buf.m_Storage = Storage::Owned; - buf.m_TempDS = &tempDS; - buf.m_ArrayId = arr->getId(); - buf.m_OwnedArray = arr; - buf.m_IsScalar = false; - return buf; -} - -CalcBuffer CalcBuffer::wrapOutput(DataArray& outputArray) -{ - CalcBuffer buf; - buf.m_Storage = Storage::OutputDirect; - buf.m_OutputArray = &outputArray; - buf.m_IsScalar = false; - return buf; -} - -float64 CalcBuffer::read(usize index) const -{ - switch(m_Storage) - { - case Storage::Borrowed: - return m_BorrowedArray->at(index); - case Storage::Owned: - return m_OwnedArray->at(index); - case Storage::OutputDirect: - return m_OutputArray->at(index); - } - return 0.0; -} - -void CalcBuffer::write(usize index, float64 value) -{ - switch(m_Storage) - { - case Storage::Owned: - (*m_OwnedArray)[index] = value; - return; - case Storage::OutputDirect: - (*m_OutputArray)[index] = value; - return; - case Storage::Borrowed: - return; // read-only — should not be called - } -} - -void CalcBuffer::fill(float64 value) -{ - switch(m_Storage) - { - case Storage::Owned: - m_OwnedArray->fill(value); - return; - case Storage::OutputDirect: - m_OutputArray->fill(value); - return; - case Storage::Borrowed: - return; // read-only - } -} - -usize CalcBuffer::size() const -{ - switch(m_Storage) - { - case Storage::Borrowed: - return m_BorrowedArray->getSize(); - case Storage::Owned: - return m_OwnedArray->getSize(); - case Storage::OutputDirect: - return m_OutputArray->getSize(); - } - return 0; -} - -usize CalcBuffer::numTuples() const -{ - switch(m_Storage) - { - case Storage::Borrowed: - return m_BorrowedArray->getNumberOfTuples(); - case Storage::Owned: - return m_OwnedArray->getNumberOfTuples(); - case Storage::OutputDirect: - return m_OutputArray->getNumberOfTuples(); - } - return 0; -} - -usize CalcBuffer::numComponents() const -{ - switch(m_Storage) - { - case Storage::Borrowed: - return m_BorrowedArray->getNumberOfComponents(); - case Storage::Owned: - return m_OwnedArray->getNumberOfComponents(); - case Storage::OutputDirect: - return m_OutputArray->getNumberOfComponents(); - } - return 0; -} - -std::vector CalcBuffer::tupleShape() const -{ - switch(m_Storage) - { - case Storage::Borrowed: - return m_BorrowedArray->getTupleShape(); - case Storage::Owned: - return m_OwnedArray->getTupleShape(); - case Storage::OutputDirect: - return m_OutputArray->getTupleShape(); - } - return {}; -} - -std::vector CalcBuffer::compShape() const -{ - switch(m_Storage) - { - case Storage::Borrowed: - return m_BorrowedArray->getComponentShape(); - case Storage::Owned: - return m_OwnedArray->getComponentShape(); - case Storage::OutputDirect: - return m_OutputArray->getComponentShape(); - } - return {}; -} - -bool CalcBuffer::isScalar() const -{ - return m_IsScalar; -} - -bool CalcBuffer::isOwned() const -{ - return m_Storage == Storage::Owned; -} - -bool CalcBuffer::isOutputDirect() const -{ - return m_Storage == Storage::OutputDirect; -} - -void CalcBuffer::markAsScalar() -{ - m_IsScalar = true; -} - -const Float64Array& CalcBuffer::array() const -{ - switch(m_Storage) - { - case Storage::Borrowed: - return *m_BorrowedArray; - case Storage::Owned: - return *m_OwnedArray; - case Storage::OutputDirect: - return *m_OutputArray; - } - // Should never reach here; return owned as fallback - return *m_OwnedArray; -} -``` - -- [ ] **Step 2: Build to verify CalcBuffer compiles** - -```bash -cd /Users/mjackson/Workspace2/DREAM3D-Build/simplnx-Rel && cmake --build . --target SimplnxCore -``` - -Expected: Build succeeds. CalcBuffer is implemented but not yet used. - -- [ ] **Step 3: Run existing tests to verify no regressions** - -```bash -cd /Users/mjackson/Workspace2/DREAM3D-Build/simplnx-Rel && ctest -R "SimplnxCore::ArrayCalculatorFilter" --verbose -``` - -Expected: All tests pass (no change in behavior). - -- [ ] **Step 4: Commit** - -```bash -git add src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ArrayCalculator.cpp -git commit -m "ENH: Implement CalcBuffer RAII sentinel for ArrayCalculator temp arrays" -``` - ---- - -### Task 4: Make ParsedItem and RpnItem Data-Free - -**Files:** -- Modify: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ArrayCalculator.hpp` (RpnItem) -- Modify: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ArrayCalculator.cpp` (ParsedItem) - -Replace CalcValue-based data in ParsedItem and RpnItem with metadata-only fields. This task changes the data structures but does not yet change the parser or evaluator logic — those come next. - -- [ ] **Step 1: Update RpnItem in ArrayCalculator.hpp** - -Replace the current `RpnItem` struct (lines 152-166) with the data-free version. Also delete the `CalcValue` struct (lines 138-147): - -Delete `CalcValue`: -```cpp -// DELETE the entire CalcValue struct (lines 138-147): -// struct SIMPLNXCORE_EXPORT CalcValue { ... }; -``` - -Replace `RpnItem`: -```cpp -// --------------------------------------------------------------------------- -// A single item in the RPN (reverse-polish notation) evaluation sequence. -// Data-free: stores DataPath references and scalar values, not DataObject IDs. -// --------------------------------------------------------------------------- -struct SIMPLNXCORE_EXPORT RpnItem -{ - enum class Type - { - Scalar, - ArrayRef, - Operator, - ComponentExtract, - TupleComponentExtract - } type; - - // Scalar - float64 scalarValue = 0.0; - - // ArrayRef - DataPath arrayPath; - DataType sourceDataType = DataType::float64; - - // Operator - const OperatorDef* op = nullptr; - - // ComponentExtract / TupleComponentExtract - usize componentIndex = std::numeric_limits::max(); - usize tupleIndex = std::numeric_limits::max(); -}; -``` - -- [ ] **Step 2: Update ParsedItem in the anonymous namespace of ArrayCalculator.cpp** - -Replace the current `ParsedItem` struct (lines 26-44) with the data-free version: - -```cpp -struct ParsedItem -{ - enum class Kind - { - Scalar, - ArrayRef, - Operator, - LParen, - RParen, - Comma, - ComponentExtract, - TupleComponentExtract - } kind; - - // Scalar - float64 scalarValue = 0.0; - - // ArrayRef: metadata for validation (no data allocated) - DataPath arrayPath; - DataType sourceDataType = DataType::float64; - std::vector arrayTupleShape; - std::vector arrayCompShape; - - // Operator - const OperatorDef* op = nullptr; - bool isNegativePrefix = false; - - // ComponentExtract / TupleComponentExtract - usize componentIndex = std::numeric_limits::max(); - usize tupleIndex = std::numeric_limits::max(); -}; -``` - -- [ ] **Step 3: Update isBinaryOp helper function** - -The `isBinaryOp` function (around line 139-142) references `ParsedItem::Kind::Operator` which stays the same. No change needed to this function. - -- [ ] **Step 4: Note — do NOT build yet** - -The parser and evaluator still reference the old CalcValue and old ParsedItem/RpnItem fields. They must be updated in Tasks 5 and 6 before the code compiles. Proceed directly to Task 5. - ---- - -### Task 5: Rewrite Parser to Be Data-Free - -**Files:** -- Modify: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ArrayCalculator.hpp` (remove parser members) -- Modify: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ArrayCalculator.cpp` (rewrite parse()) - -This is the largest task. The parser's `parse()` method is rewritten so it allocates zero data — it only produces RPN items with DataPath/scalar metadata. The helper methods `createScalarInTemp()`, `copyArrayToTemp()`, and `nextScratchName()` are removed from the parser class. The `m_TempDataStructure`, `m_IsPreflight`, and `m_ScratchCounter` members are removed. The `getTempDataStructure()` method is removed. - -- [ ] **Step 1: Remove parser-only members from ArrayCalculatorParser in the header** - -In `ArrayCalculator.hpp`, remove the following from the `ArrayCalculatorParser` class: - -Remove these private method declarations: -- `std::string nextScratchName();` (line 234) -- `DataObject::IdType copyArrayToTemp(const IDataArray& sourceArray);` (line 242) -- `DataObject::IdType createScalarInTemp(double value);` (line 249) - -Remove these private member variables: -- `DataStructure m_TempDataStructure;` (line 252) -- `bool m_IsPreflight;` (line 255) -- `usize m_ScratchCounter = 0;` (line 256) - -Remove this public method: -- `DataStructure& getTempDataStructure() { ... }` (lines 217-221) - -Update the constructor signature — remove `bool isPreflight` parameter: -```cpp -ArrayCalculatorParser(const DataStructure& dataStructure, const DataPath& selectedGroupPath, const std::string& infixEquation, const std::atomic_bool& shouldCancel); -``` - -- [ ] **Step 2: Update ArrayCalculatorParser constructor implementation in .cpp** - -Replace the constructor (lines 305-312) with: - -```cpp -ArrayCalculatorParser::ArrayCalculatorParser(const DataStructure& dataStructure, const DataPath& selectedGroupPath, const std::string& infixEquation, const std::atomic_bool& shouldCancel) -: m_DataStructure(dataStructure) -, m_SelectedGroupPath(selectedGroupPath) -, m_InfixEquation(infixEquation) -, m_ShouldCancel(shouldCancel) -{ -} -``` - -- [ ] **Step 3: Delete the old helper method implementations from .cpp** - -Delete the following function bodies from ArrayCalculator.cpp: -- `ArrayCalculatorParser::nextScratchName()` (lines 443-446) -- `ArrayCalculatorParser::createScalarInTemp()` (lines 449-454) -- `ArrayCalculatorParser::copyArrayToTemp()` (lines 457-476) - -- [ ] **Step 4: Rewrite parse() — token resolution (steps 3+4)** - -This is the core change. In the token resolution loop (starting around line 595), every place that previously called `createScalarInTemp()` or `copyArrayToTemp()` must instead store metadata in the ParsedItem. - -**For numeric literals** (the `TokenType::Number` case, around line 773): -Replace: -```cpp -DataObject::IdType id = createScalarInTemp(numValue); -ParsedItem pi; -pi.kind = ParsedItem::Kind::Value; -pi.value = CalcValue{CalcValue::Kind::Number, id}; -``` -With: -```cpp -ParsedItem pi; -pi.kind = ParsedItem::Kind::Scalar; -pi.scalarValue = numValue; -``` - -**For constants `pi` and `e`** (around line 841): -Replace: -```cpp -double constValue = (tok.text == "pi") ? std::numbers::pi : std::numbers::e; -DataObject::IdType id = createScalarInTemp(constValue); -ParsedItem pi; -pi.kind = ParsedItem::Kind::Value; -pi.value = CalcValue{CalcValue::Kind::Number, id}; -``` -With: -```cpp -float64 constValue = (tok.text == "pi") ? std::numbers::pi : std::numbers::e; -ParsedItem pi; -pi.kind = ParsedItem::Kind::Scalar; -pi.scalarValue = constValue; -``` - -**For array references found via selected group** (around line 876): -Replace: -```cpp -DataObject::IdType id = copyArrayToTemp(*dataArray); -ParsedItem pi; -pi.kind = ParsedItem::Kind::Value; -pi.value = CalcValue{CalcValue::Kind::Array, id}; -``` -With: -```cpp -ParsedItem pi; -pi.kind = ParsedItem::Kind::ArrayRef; -pi.arrayPath = arrayPath; -pi.sourceDataType = dataArray->getDataType(); -pi.arrayTupleShape = dataArray->getTupleShape(); -pi.arrayCompShape = dataArray->getComponentShape(); -``` - -Apply the same pattern to **all other array resolution sites**: -- Array found via `findArraysByName()` (around line 916): same change — store DataPath, DataType, shapes -- Quoted string path resolution (around line 969): same change - -**For bracket indexing `Array[C]`** (the block starting around line 635): - -Currently this block accesses `m_TempDataStructure` to get the temp array and extract component data. Replace the entire bracket handling block. When `prevItem.kind == ParsedItem::Kind::ArrayRef`: - -For `[C]` (single bracket number): -```cpp -usize numComponents = 1; -for(usize d : prevItem.arrayCompShape) -{ - numComponents *= d; -} - -// Validate component index -if(compIdx >= numComponents) -{ - return MakeErrorResult(static_cast(CalculatorErrorCode::ComponentOutOfRange), - fmt::format("Component index {} is out of range for array with {} components.", compIdx, numComponents)); -} - -// Emit a ComponentExtract after the ArrayRef -ParsedItem ce; -ce.kind = ParsedItem::Kind::ComponentExtract; -ce.componentIndex = compIdx; -items.push_back(ce); -``` - -For `[T, C]` (two bracket numbers): -```cpp -usize numTuples = 1; -for(usize d : prevItem.arrayTupleShape) -{ - numTuples *= d; -} -usize numComponents = 1; -for(usize d : prevItem.arrayCompShape) -{ - numComponents *= d; -} - -if(tupleIdx >= numTuples) -{ - return MakeErrorResult(static_cast(CalculatorErrorCode::TupleOutOfRange), - fmt::format("Tuple index {} is out of range for array with {} tuples.", tupleIdx, numTuples)); -} -if(compIdx >= numComponents) -{ - return MakeErrorResult(static_cast(CalculatorErrorCode::ComponentOutOfRange), - fmt::format("Component index {} is out of range for array with {} components.", compIdx, numComponents)); -} - -ParsedItem tce; -tce.kind = ParsedItem::Kind::TupleComponentExtract; -tce.tupleIndex = tupleIdx; -tce.componentIndex = compIdx; -items.push_back(tce); -``` - -The `(expr)[C]` and `(expr)[T,C]` paths (when `prevItem.kind == ParsedItem::Kind::RParen`) remain unchanged — they already emit ComponentExtract/TupleComponentExtract items. - -- [ ] **Step 5: Rewrite parse() — validation step 7b** - -The validation step 7b (starting around line 1316) currently queries `m_TempDataStructure.getDataAs()` for each array value. Replace it to query `ParsedItem::arrayTupleShape` and `ParsedItem::arrayCompShape` directly: - -```cpp -// 7b: Collect array-type values and verify consistent tuple/component info -std::vector arrayTupleShape; -std::vector arrayCompShape; -usize arrayNumTuples = 0; -bool hasArray = false; -bool hasNumericValue = false; -bool tupleShapesMatch = true; - -for(const auto& item : items) -{ - if(item.kind == ParsedItem::Kind::Scalar || item.kind == ParsedItem::Kind::ArrayRef) - { - hasNumericValue = true; - } - if(item.kind == ParsedItem::Kind::ArrayRef) - { - std::vector ts = item.arrayTupleShape; - std::vector cs = item.arrayCompShape; - usize nt = 1; - for(usize d : ts) - { - nt *= d; - } - - if(hasArray) - { - if(!arrayCompShape.empty() && arrayCompShape != cs) - { - return MakeErrorResult(static_cast(CalculatorErrorCode::InconsistentCompDims), - "Attribute Array symbols in the infix expression have mismatching component dimensions."); - } - if(arrayNumTuples != 0 && nt != arrayNumTuples) - { - return MakeErrorResult(static_cast(CalculatorErrorCode::InconsistentTuples), - "Attribute Array symbols in the infix expression have mismatching number of tuples."); - } - if(!arrayTupleShape.empty() && arrayTupleShape != ts) - { - tupleShapesMatch = false; - } - } - - hasArray = true; - arrayTupleShape = ts; - arrayCompShape = cs; - arrayNumTuples = nt; - } -} -``` - -- [ ] **Step 6: Rewrite parse() — shunting-yard conversion to RPN** - -The shunting-yard loop (starting around line 1416) currently converts ParsedItems to RpnItems. Update the `Value` case to handle the new `Scalar` and `ArrayRef` kinds: - -Replace the `ParsedItem::Kind::Value` case with two cases: - -```cpp -case ParsedItem::Kind::Scalar: { - RpnItem rpn; - rpn.type = RpnItem::Type::Scalar; - rpn.scalarValue = item.scalarValue; - m_RpnItems.push_back(rpn); - break; -} - -case ParsedItem::Kind::ArrayRef: { - RpnItem rpn; - rpn.type = RpnItem::Type::ArrayRef; - rpn.arrayPath = item.arrayPath; - rpn.sourceDataType = item.sourceDataType; - m_RpnItems.push_back(rpn); - break; -} -``` - -Update the `ComponentExtract` and `TupleComponentExtract` cases similarly — they already match the new RpnItem fields. Just ensure the RpnItem type assignment uses `RpnItem::Type::ComponentExtract` / `RpnItem::Type::TupleComponentExtract` and sets `componentIndex`/`tupleIndex`. - -- [ ] **Step 7: Update constructor calls in ArrayCalculatorFilter.cpp** - -In `ArrayCalculatorFilter.cpp`, update the two places where `ArrayCalculatorParser` is constructed: - -In `preflightImpl()` (around line 88), remove the `true` (isPreflight) argument: -```cpp -ArrayCalculatorParser parser(dataStructure, pInfixEquationValue.m_SelectedGroup, pInfixEquationValue.m_Equation, m_ShouldCancel); -``` - -In `ArrayCalculator::operator()()` in ArrayCalculator.cpp (around line 1789), remove the `false` (isPreflight) argument: -```cpp -ArrayCalculatorParser parser(m_DataStructure, m_InputValues->SelectedGroup, m_InputValues->InfixEquation, m_ShouldCancel); -``` - -- [ ] **Step 8: Note — do NOT build yet** - -The evaluator (`evaluateInto()`) still references the old CalcValue-based eval stack. It must be updated in Task 6 before the code compiles. - ---- - -### Task 6: Rewrite Evaluator to Use CalcBuffer Stack - -**Files:** -- Modify: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ArrayCalculator.cpp` - -Rewrite `evaluateInto()` to use a `std::stack` with a local `DataStructure` for temps. Add the last-operator OutputDirect optimization and the final result copy logic. - -- [ ] **Step 1: Rewrite evaluateInto()** - -Replace the entire `evaluateInto()` method (starting at line 1534) with the following implementation. This is the complete new evaluator: - -```cpp -Result<> ArrayCalculatorParser::evaluateInto(DataStructure& dataStructure, const DataPath& outputPath, NumericType scalarType, CalculatorParameter::AngleUnits units) -{ - // 1. Parse (populates m_RpnItems via shunting-yard) - Result<> parseResult = parse(); - if(parseResult.invalid()) - { - return parseResult; - } - - // 2. Create local temp DataStructure for intermediate arrays - DataStructure tempDS; - usize scratchCounter = 0; - auto nextScratchName = [&scratchCounter]() -> std::string { - return "_calc_" + std::to_string(scratchCounter++); - }; - - // 3. Pre-scan RPN to find the index of the last operator/extract item - // for the OutputDirect optimization - DataType outputDataType = ConvertNumericTypeToDataType(scalarType); - bool outputIsFloat64 = (outputDataType == DataType::float64); - int64 lastOpIndex = -1; - for(int64 idx = static_cast(m_RpnItems.size()) - 1; idx >= 0; --idx) - { - RpnItem::Type t = m_RpnItems[static_cast(idx)].type; - if(t == RpnItem::Type::Operator || t == RpnItem::Type::ComponentExtract || t == RpnItem::Type::TupleComponentExtract) - { - lastOpIndex = idx; - break; - } - } - - // 4. Walk the RPN items using a CalcBuffer evaluation stack - std::stack evalStack; - - for(usize rpnIdx = 0; rpnIdx < m_RpnItems.size(); ++rpnIdx) - { - if(m_ShouldCancel) - { - return {}; - } - - const RpnItem& rpnItem = m_RpnItems[rpnIdx]; - bool isLastOp = (static_cast(rpnIdx) == lastOpIndex); - - switch(rpnItem.type) - { - case RpnItem::Type::Scalar: { - evalStack.push(CalcBuffer::scalar(tempDS, rpnItem.scalarValue, nextScratchName())); - break; - } - - case RpnItem::Type::ArrayRef: { - if(rpnItem.sourceDataType == DataType::float64) - { - const auto& sourceArray = m_DataStructure.getDataRefAs(rpnItem.arrayPath); - evalStack.push(CalcBuffer::borrow(sourceArray)); - } - else - { - const auto& sourceArray = m_DataStructure.getDataRefAs(rpnItem.arrayPath); - evalStack.push(CalcBuffer::convertFrom(tempDS, sourceArray, nextScratchName())); - } - break; - } - - case RpnItem::Type::Operator: { - const OperatorDef* op = rpnItem.op; - if(op == nullptr) - { - return MakeErrorResult(static_cast(CalculatorErrorCode::InvalidEquation), "Internal error: null operator in RPN evaluation."); - } - - if(op->numArgs == 1) - { - if(evalStack.empty()) - { - return MakeErrorResult(static_cast(CalculatorErrorCode::NotEnoughArguments), "Not enough arguments for unary operator."); - } - CalcBuffer operand = std::move(evalStack.top()); - evalStack.pop(); - - std::vector resultTupleShape = operand.tupleShape(); - std::vector resultCompShape = operand.compShape(); - usize totalSize = operand.size(); - - CalcBuffer result = (isLastOp && outputIsFloat64) - ? CalcBuffer::wrapOutput(dataStructure.getDataRefAs>(outputPath)) - : CalcBuffer::allocate(tempDS, nextScratchName(), resultTupleShape, resultCompShape); - - for(usize i = 0; i < totalSize; i++) - { - float64 val = operand.read(i); - - if(op->trigMode == OperatorDef::ForwardTrig && units == CalculatorParameter::AngleUnits::Degrees) - { - val = val * (std::numbers::pi / 180.0); - } - - float64 res = op->unaryOp(val); - - if(op->trigMode == OperatorDef::InverseTrig && units == CalculatorParameter::AngleUnits::Degrees) - { - res = res * (180.0 / std::numbers::pi); - } - - result.write(i, res); - } - - bool wasScalar = operand.isScalar(); - if(wasScalar) - { - result.markAsScalar(); - } - // operand destroyed here, RAII cleans up - evalStack.push(std::move(result)); - } - else if(op->numArgs == 2) - { - if(evalStack.size() < 2) - { - return MakeErrorResult(static_cast(CalculatorErrorCode::NotEnoughArguments), "Not enough arguments for binary operator."); - } - CalcBuffer right = std::move(evalStack.top()); - evalStack.pop(); - CalcBuffer left = std::move(evalStack.top()); - evalStack.pop(); - - // Determine output shape: use the array operand's shape (broadcast scalars) - std::vector outTupleShape; - std::vector outCompShape; - if(!left.isScalar()) - { - outTupleShape = left.tupleShape(); - outCompShape = left.compShape(); - } - else - { - outTupleShape = right.tupleShape(); - outCompShape = right.compShape(); - } - - usize totalSize = 1; - for(usize d : outTupleShape) - { - totalSize *= d; - } - for(usize d : outCompShape) - { - totalSize *= d; - } - - CalcBuffer result = (isLastOp && outputIsFloat64) - ? CalcBuffer::wrapOutput(dataStructure.getDataRefAs>(outputPath)) - : CalcBuffer::allocate(tempDS, nextScratchName(), outTupleShape, outCompShape); - - bool leftIsScalar = left.isScalar(); - bool rightIsScalar = right.isScalar(); - - for(usize i = 0; i < totalSize; i++) - { - float64 lv = left.read(leftIsScalar ? 0 : i); - float64 rv = right.read(rightIsScalar ? 0 : i); - result.write(i, op->binaryOp(lv, rv)); - } - - if(leftIsScalar && rightIsScalar) - { - result.markAsScalar(); - } - // left and right destroyed here, RAII cleans up owned temps - evalStack.push(std::move(result)); - } - else - { - return MakeErrorResult(static_cast(CalculatorErrorCode::InvalidEquation), - fmt::format("Internal error: operator '{}' has unsupported numArgs={}.", op->token, op->numArgs)); - } - break; - } - - case RpnItem::Type::ComponentExtract: { - if(evalStack.empty()) - { - return MakeErrorResult(static_cast(CalculatorErrorCode::NotEnoughArguments), "Not enough arguments for component extraction."); - } - CalcBuffer operand = std::move(evalStack.top()); - evalStack.pop(); - - usize numComps = operand.numComponents(); - usize numTuples = operand.numTuples(); - usize compIdx = rpnItem.componentIndex; - - if(compIdx >= numComps) - { - return MakeErrorResult(static_cast(CalculatorErrorCode::ComponentOutOfRange), - fmt::format("Component index {} is out of range for array with {} components.", compIdx, numComps)); - } - - CalcBuffer result = (isLastOp && outputIsFloat64) - ? CalcBuffer::wrapOutput(dataStructure.getDataRefAs>(outputPath)) - : CalcBuffer::allocate(tempDS, nextScratchName(), operand.tupleShape(), std::vector{1}); - - for(usize t = 0; t < numTuples; ++t) - { - result.write(t, operand.read(t * numComps + compIdx)); - } - - evalStack.push(std::move(result)); - break; - } - - case RpnItem::Type::TupleComponentExtract: { - if(evalStack.empty()) - { - return MakeErrorResult(static_cast(CalculatorErrorCode::NotEnoughArguments), "Not enough arguments for tuple+component extraction."); - } - CalcBuffer operand = std::move(evalStack.top()); - evalStack.pop(); - - usize numComps = operand.numComponents(); - usize numTuples = operand.numTuples(); - usize tupleIdx = rpnItem.tupleIndex; - usize compIdx = rpnItem.componentIndex; - - if(tupleIdx >= numTuples) - { - return MakeErrorResult(static_cast(CalculatorErrorCode::TupleOutOfRange), - fmt::format("Tuple index {} is out of range for array with {} tuples.", tupleIdx, numTuples)); - } - if(compIdx >= numComps) - { - return MakeErrorResult(static_cast(CalculatorErrorCode::ComponentOutOfRange), - fmt::format("Component index {} is out of range for array with {} components.", compIdx, numComps)); - } - - float64 value = operand.read(tupleIdx * numComps + compIdx); - // operand destroyed, RAII cleans up - evalStack.push(CalcBuffer::scalar(tempDS, value, nextScratchName())); - break; - } - - } // end switch - } - - // 5. Final result - if(evalStack.size() != 1) - { - return MakeErrorResult(static_cast(CalculatorErrorCode::InvalidEquation), - fmt::format("Internal error: evaluation stack has {} items remaining; expected exactly 1.", evalStack.size())); - } - - CalcBuffer finalResult = std::move(evalStack.top()); - evalStack.pop(); - - // 6. Copy/cast result into the output array (checked in order, first match wins) - if(finalResult.isScalar()) - { - // Fill entire output with the scalar value - float64 scalarVal = finalResult.read(0); - ExecuteDataFunction(CopyResultFunctor{}, outputDataType, dataStructure, outputPath, scalarVal); - } - else if(finalResult.isOutputDirect()) - { - // Data is already in the output array — nothing to do - } - else if(outputIsFloat64) - { - // Direct float64-to-float64 copy via operator[] (no type cast) - auto& outputArray = dataStructure.getDataRefAs>(outputPath); - usize totalSize = finalResult.size(); - for(usize i = 0; i < totalSize; i++) - { - outputArray[i] = finalResult.read(i); - } - } - else - { - // Type-casting copy via CopyResultFunctor - const Float64Array& resultArray = finalResult.array(); - ExecuteDataFunction(CopyResultFunctor{}, outputDataType, dataStructure, outputPath, &resultArray, false); - } - - return parseResult; -} -``` - -- [ ] **Step 2: Update CopyResultFunctor to support scalar fill** - -The scalar fill path now passes a `float64` value directly. Add an overload or update `CopyResultFunctor` in the anonymous namespace. Replace the existing `CopyResultFunctor` (lines 230-248) with: - -```cpp -struct CopyResultFunctor -{ - // Full array copy (non-float64 output) - template - void operator()(DataStructure& ds, const DataPath& outputPath, const Float64Array* resultArray, bool /*unused*/) - { - auto& output = ds.getDataRefAs>(outputPath).getDataStoreRef(); - for(usize i = 0; i < output.getSize(); i++) - { - output[i] = static_cast(resultArray->at(i)); - } - } - - // Scalar fill - template - void operator()(DataStructure& ds, const DataPath& outputPath, float64 scalarValue) - { - auto& output = ds.getDataRefAs>(outputPath); - output.fill(static_cast(scalarValue)); - } -}; -``` - -- [ ] **Step 3: Remove old CopyToFloat64Functor** - -The `CopyToFloat64Functor` (lines 122-134) is no longer needed at the top level since its logic is now inside `CalcBuffer::convertFrom()`. However, `CalcBuffer::convertFrom()` still calls it via `ExecuteDataFunction`. Keep `CopyToFloat64Functor` in the anonymous namespace — it is still referenced by `CalcBuffer::convertFrom()`. - -- [ ] **Step 4: Build the project** - -```bash -cd /Users/mjackson/Workspace2/DREAM3D-Build/simplnx-Rel && cmake --build . --target SimplnxCore -``` - -Expected: Build succeeds. Fix any compilation errors arising from ParsedItem/RpnItem field name mismatches — common issues: -- `item.kind == ParsedItem::Kind::Value` → split into `ParsedItem::Kind::Scalar` and `ParsedItem::Kind::ArrayRef` -- `item.value.kind == CalcValue::Kind::Array` → `item.kind == ParsedItem::Kind::ArrayRef` -- References to `item.value` → `item.scalarValue` or `item.arrayPath` - -- [ ] **Step 5: Run all ArrayCalculator tests** - -```bash -cd /Users/mjackson/Workspace2/DREAM3D-Build/simplnx-Rel && ctest -R "SimplnxCore::ArrayCalculatorFilter" --verbose -``` - -Expected: All test cases pass — identical behavior to baseline. If any fail, debug by comparing the error code or output value against the expected. Common issues: -- Bracket indexing `Array[C]` now emits ArrayRef + ComponentExtract, so the evaluator must handle ComponentExtract on borrowed arrays correctly -- Scalar detection: CalcBuffer created via `CalcBuffer::scalar()` has `m_IsScalar = true`, but CalcBuffers from binary operations where both operands are scalar should also be scalar. Check that the scalar fill path triggers correctly for all-scalar expressions. - -- [ ] **Step 6: Commit** - -```bash -git add src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ArrayCalculator.hpp \ - src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ArrayCalculator.cpp \ - src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ArrayCalculatorFilter.cpp -git commit -m "MEM: Rewrite ArrayCalculator parser and evaluator with CalcBuffer RAII - -Parser is now data-free: produces RPN items with DataPath/scalar metadata -instead of allocating temporary Float64Arrays. Evaluator uses a CalcBuffer -stack with RAII cleanup — intermediates are freed when consumed. Float64 -input arrays are borrowed (zero-copy). The last RPN operator writes -directly into the output DataArray when output type is float64." -``` - ---- - -### Task 7: Final Verification and Cleanup - -**Files:** -- Modify: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ArrayCalculator.hpp` (cleanup) -- Modify: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ArrayCalculator.cpp` (cleanup) - -- [ ] **Step 1: Run clang-format on modified files** - -```bash -cd /Users/mjackson/Workspace7/simplnx && clang-format -i \ - src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ArrayCalculator.hpp \ - src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ArrayCalculator.cpp \ - src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ArrayCalculatorFilter.cpp -``` - -- [ ] **Step 2: Build the full project (not just SimplnxCore)** - -```bash -cd /Users/mjackson/Workspace2/DREAM3D-Build/simplnx-Rel && cmake --build . --target all -``` - -Expected: Full build succeeds — no other files reference CalcValue or the removed parser members. - -- [ ] **Step 3: Run the full SimplnxCore test suite** - -```bash -cd /Users/mjackson/Workspace2/DREAM3D-Build/simplnx-Rel && ctest -R "SimplnxCore::" --verbose -``` - -Expected: All SimplnxCore tests pass. This catches any accidental regressions in other filters. - -- [ ] **Step 4: Run ArrayCalculator tests specifically and verify all assertions pass** - -```bash -cd /Users/mjackson/Workspace2/DREAM3D-Build/simplnx-Rel && ctest -R "SimplnxCore::ArrayCalculatorFilter" --verbose -``` - -Expected: All 9 test cases pass with all assertions (the test output should show the same assertion count as the baseline from Task 1). - -- [ ] **Step 5: Commit formatting changes (if any)** - -```bash -git add src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ArrayCalculator.hpp \ - src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ArrayCalculator.cpp \ - src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ArrayCalculatorFilter.cpp -git commit -m "STY: Run clang-format on ArrayCalculator files after memory optimization" -``` - diff --git a/docs/superpowers/specs/2026-04-07-arraycalculator-memory-optimization-design.md b/docs/superpowers/specs/2026-04-07-arraycalculator-memory-optimization-design.md deleted file mode 100644 index 177fadb63f..0000000000 --- a/docs/superpowers/specs/2026-04-07-arraycalculator-memory-optimization-design.md +++ /dev/null @@ -1,289 +0,0 @@ -# ArrayCalculator Memory Optimization Design - -## Problem - -The ArrayCalculator evaluator allocates temporary `Float64Array` objects in a scratch `DataStructure` (`m_TempDataStructure`) for every input array, every intermediate result, and the final result. None of these temporaries are freed until the parser is destroyed. For a simple expression like `a + b` with float64 inputs and float64 output, this produces 4 array-sized allocations when 0 would suffice. - -For large datasets with complex expressions, this causes memory usage to explode: an expression with N operations on an array of M elements allocates O(N * M * 8) bytes of temporaries that all stay alive simultaneously. - -### Specific waste patterns - -1. **Input float64 arrays are copied into temp Float64Arrays** even though the data is already float64 — a full O(M) allocation + O(M) copy per input reference. -2. **Every intermediate result allocates a new Float64Array** in `m_TempDataStructure`. None are freed until the parser destructs. -3. **The final result is copied element-by-element** from the temp Float64Array into the output DataArray, even when the output type is float64 — an unnecessary O(M) allocation + O(M) copy. - -## Constraints - -- **`m_TempDataStructure` must be retained.** Temporary arrays must live inside a `DataStructure` because the `DataArray`/`DataStore` abstraction is load-bearing: the project is moving to an out-of-core `DataStore` implementation where data may not be resident in memory. Raw `std::vector` or `float64*` buffers cannot replace `DataArray`. -- **No raw pointer access to DataArray data.** All element reads/writes must go through `DataArray::at()` or `operator[]`. Never use `T*` pointers obtained from `DataStore::data()` or similar. -- **Memory optimization is the first priority.** CPU computation performance is the second priority. -- **Backward compatibility.** All existing tests must continue to pass. Error codes, parameter keys, and pipeline JSON formats are unchanged by this work. - -## Solution: CalcBuffer — RAII Sentinel for Temp DataArrays - -### Overview - -Introduce a `CalcBuffer` class: a move-only RAII handle that wraps a `Float64Array` (either a temp array in `m_TempDataStructure` or a borrowed reference to an input array in the real `DataStructure`). When an owned `CalcBuffer` is destroyed, it removes its `DataArray` from the `DataStructure` via `DataStructure::removeData(DataObject::IdType)`. - -The evaluation stack changes from `std::stack` (with manual ID lookups) to `std::stack` (with automatic RAII cleanup). Intermediates are freed the instant they are consumed by an operator. - -### CalcBuffer class - -```cpp -class SIMPLNXCORE_EXPORT CalcBuffer -{ -public: - // --- Factory methods --- - - // Zero-copy reference to an existing Float64Array. Read-only. Destructor: no-op. - static CalcBuffer borrow(const Float64Array& source); - - // Allocate a temp Float64Array in tempDS, convert source data from any numeric type. - // Owned. Destructor: removes from tempDS. - static CalcBuffer convertFrom(DataStructure& tempDS, const IDataArray& source, - const std::string& name); - - // Allocate a 1-element temp Float64Array with the given scalar value. - static CalcBuffer scalar(DataStructure& tempDS, double value, const std::string& name); - - // Allocate an empty temp Float64Array with the given shape. - static CalcBuffer allocate(DataStructure& tempDS, const std::string& name, - std::vector tupleShape, std::vector compShape); - - // Wrap the output DataArray for direct writing. Destructor: no-op. - static CalcBuffer wrapOutput(DataArray& outputArray); - - // --- Move-only, non-copyable --- - CalcBuffer(CalcBuffer&& other) noexcept; - CalcBuffer& operator=(CalcBuffer&& other) noexcept; - ~CalcBuffer(); - - CalcBuffer(const CalcBuffer&) = delete; - CalcBuffer& operator=(const CalcBuffer&) = delete; - - // --- Element access --- - float64 read(usize index) const; - void write(usize index, float64 value); - void fill(float64 value); - - // --- Metadata --- - usize size() const; - usize numTuples() const; - usize numComponents() const; - std::vector tupleShape() const; - std::vector compShape() const; - bool isScalar() const; - bool isOwned() const; - bool isOutputDirect() const; - - // --- Access underlying array (for final copy to non-float64 output) --- - const Float64Array& array() const; - -private: - CalcBuffer() = default; - - enum class Storage - { - Borrowed, // const Float64Array* in real DataStructure, read-only - Owned, // Float64Array in m_TempDataStructure, read-write, cleaned up on destroy - OutputDirect // DataArray* output array, read-write, NOT cleaned up - }; - - Storage m_Storage = Storage::Owned; - - // Borrowed - const Float64Array* m_BorrowedArray = nullptr; - - // Owned - DataStructure* m_TempDS = nullptr; - DataObject::IdType m_ArrayId = 0; - Float64Array* m_OwnedArray = nullptr; - - // OutputDirect - DataArray* m_OutputArray = nullptr; - - bool m_IsScalar = false; -}; -``` - -#### Storage mode behavior - -| Mode | `read(i)` | `write(i, v)` | Destructor | -|------|-----------|---------------|------------| -| Borrowed | `m_BorrowedArray->at(i)` | assert/error | no-op | -| Owned | `m_OwnedArray->at(i)` | `(*m_OwnedArray)[i] = v` | `m_TempDS->removeData(m_ArrayId)` | -| OutputDirect | `m_OutputArray->at(i)` | `(*m_OutputArray)[i] = v` | no-op | - -#### Move semantics - -The move constructor transfers all fields and nulls out the source so the moved-from object's destructor is a no-op: - -```cpp -CalcBuffer::CalcBuffer(CalcBuffer&& other) noexcept -: m_Storage(other.m_Storage) -, m_BorrowedArray(other.m_BorrowedArray) -, m_TempDS(other.m_TempDS) -, m_ArrayId(other.m_ArrayId) -, m_OwnedArray(other.m_OwnedArray) -, m_OutputArray(other.m_OutputArray) -, m_IsScalar(other.m_IsScalar) -{ - other.m_TempDS = nullptr; // prevents other's destructor from removing the array - other.m_BorrowedArray = nullptr; - other.m_OwnedArray = nullptr; - other.m_OutputArray = nullptr; -} -``` - -### RPN item changes (data-free parser) - -`CalcValue` is deleted. `RpnItem` stores metadata only — no `DataObject::IdType`, no data allocation during parsing. - -```cpp -struct SIMPLNXCORE_EXPORT RpnItem -{ - enum class Type - { - Scalar, // Numeric literal or constant (pi, e) - ArrayRef, // Reference to a source array in the real DataStructure - Operator, // Math operator or function - ComponentExtract, // [C] on a sub-expression result - TupleComponentExtract // [T, C] on a sub-expression result - } type; - - // Scalar - float64 scalarValue = 0.0; - - // ArrayRef - DataPath arrayPath; - DataType sourceDataType = DataType::float64; - - // Operator - const OperatorDef* op = nullptr; - - // ComponentExtract / TupleComponentExtract - usize componentIndex = std::numeric_limits::max(); - usize tupleIndex = std::numeric_limits::max(); -}; -``` - -### Parser changes - -The parser becomes a pure validation + RPN construction pass with no data allocation: - -1. **`createScalarInTemp()` eliminated** — parser stores `float64 scalarValue` in RpnItem. -2. **`copyArrayToTemp()` eliminated** — parser stores `DataPath` + `DataType` from the source. -3. **`Array[C]` bracket indexing unified with `(expr)[C]`** — parser emits `ArrayRef` + `ComponentExtract` RPN items instead of extracting component data during parsing. -4. **`Array[T,C]` bracket indexing unified with `(expr)[T,C]`** — parser emits `ArrayRef` + `TupleComponentExtract`. -5. **Validation step 7b** queries source array shapes directly via `dataStructure.getDataRefAs(path)` instead of looking at temp arrays. -6. **`m_IsPreflight` flag eliminated** — parser is identical for preflight and execution. -7. **`m_TempDataStructure` removed from the parser** — it is created in the evaluator only. - -### Evaluator changes - -`m_TempDataStructure` becomes a local variable inside `evaluateInto()` (currently it is a member of `ArrayCalculatorParser`). Since the parser no longer needs it, the evaluator creates it on the stack and it is destroyed — along with any remaining temp arrays — when `evaluateInto()` returns. The eval stack is `std::stack`. - -#### Buffer creation by RPN type - -| RPN Type | CalcBuffer creation | -|----------|-------------------| -| `Scalar` | `CalcBuffer::scalar(tempDS, value, name)` | -| `ArrayRef` where `sourceDataType == float64` | `CalcBuffer::borrow(sourceFloat64Array)` | -| `ArrayRef` where `sourceDataType != float64` | `CalcBuffer::convertFrom(tempDS, source, name)` | - -#### RAII cleanup during operator evaluation - -When processing a binary operator, operands are moved out of the stack into locals. After the result is computed and pushed, the locals are destroyed at the closing brace, triggering `removeData()` for any owned temps: - -```cpp -{ - CalcBuffer right = std::move(evalStack.top()); - evalStack.pop(); - CalcBuffer left = std::move(evalStack.top()); - evalStack.pop(); - - CalcBuffer result = CalcBuffer::allocate(tempDS, name, outTupleShape, outCompShape); - - bool leftIsScalar = left.isScalar(); - bool rightIsScalar = right.isScalar(); - - for(usize i = 0; i < totalSize; i++) - { - float64 lv = left.read(leftIsScalar ? 0 : i); - float64 rv = right.read(rightIsScalar ? 0 : i); - result.write(i, op->binaryOp(lv, rv)); - } - - evalStack.push(std::move(result)); -} -// left and right destroyed here -> owned temp arrays removed from tempDS -``` - -#### Last-operator direct-write optimization - -For the last RPN operator, when the output type is `float64`: - -1. Pre-scan `m_RpnItems` to find the index of the last Operator/ComponentExtract/TupleComponentExtract. -2. When processing that item, use `CalcBuffer::wrapOutput(outputFloat64Array)` instead of `CalcBuffer::allocate()`. -3. Writes go directly into the output array via `operator[]`. -4. At the end, detect `isOutputDirect()` on the final CalcBuffer and skip the copy step. - -This eliminates the last temp array allocation entirely. - -#### Final result copy to output - -After the RPN loop, the stack has exactly one CalcBuffer: - -Checked in order (first match wins): - -| Final CalcBuffer | Output type | Action | -|-----------------|-------------|--------| -| isScalar() | any | Fill the output array with the scalar value via `DataArray::fill()` or equivalent loop | -| OutputDirect | float64 | No copy needed — data is already in the output | -| Owned or Borrowed | float64 | Element-by-element copy into output `DataArray` via `operator[]` (no type cast) | -| Owned or Borrowed | non-float64 | `CopyResultFunctor` cast-copy via `ExecuteDataFunction` | - -### CPU performance considerations - -- **`CalcBuffer::read()` branch on storage mode**: predictable per-CalcBuffer (same branch every call). Negligible cost compared to `std::function` dispatch through `op->binaryOp`/`op->unaryOp`. -- **Scalar broadcast check**: hoisted outside inner loops (`leftIsScalar`/`rightIsScalar` evaluated once before the loop). -- **OutputDirect adds a third branch to `write()`**: only applies to the single final-result CalcBuffer, so the branch predictor handles it trivially. -- **Borrowed reads via `Float64Array::at()` vs current temp array reads via `Float64Array::at()`**: identical per-element cost. Zero CPU regression for reads. - -### Memory impact analysis - -For expression `a + b + c + d + e + f` with float64 inputs and float64 output (M elements each): - -| Metric | Current | New | -|--------|---------|-----| -| Input copies | 6 arrays (6 * M * 8 bytes) | 0 (all borrowed) | -| Peak intermediate arrays | 5 (all alive simultaneously) | 1 (RAII frees consumed) | -| Final result temp | 1 array (M * 8 bytes) | 0 (OutputDirect writes to output) | -| **Total temp memory** | **12 * M * 8 bytes** | **1 * M * 8 bytes** (one intermediate) | - -For the same expression with non-float64 inputs (e.g., int32): - -| Metric | Current | New | -|--------|---------|-----| -| Input copies | 6 arrays | 6 arrays (conversion required) | -| Peak intermediate arrays | 5 | 1 | -| Final result temp | 1 | 0 (if output is float64) or 1 (if not) | -| **Total temp memory** | **12 * M * 8 bytes** | **7 * M * 8 bytes** (6 conversions + 1 intermediate) | - -### Files modified - -- `ArrayCalculator.hpp` — add `CalcBuffer` class, update `RpnItem`, remove `CalcValue`, remove `m_TempDataStructure`/`m_IsPreflight`/`createScalarInTemp`/`copyArrayToTemp` from parser, add `m_TempDataStructure` to evaluator or create locally -- `ArrayCalculator.cpp` — rewrite `parse()` to be data-free, rewrite `evaluateInto()` with CalcBuffer stack + RAII + OutputDirect, remove `CopyToFloat64Functor` (moved into `CalcBuffer::convertFrom`), update `CopyResultFunctor` for the non-float64 output path -- `ArrayCalculatorFilter.cpp` — no changes expected (parser/evaluator API stays the same) -- `ArrayCalculatorTest.cpp` — no changes expected (tests exercise the filter, not internal classes) - -### What is NOT changing - -- `OperatorDef` struct and operator registry — unchanged -- `Token` struct and `tokenize()` — unchanged -- `CalculatorErrorCode` / `CalculatorWarningCode` enums — unchanged -- `ArrayCalculatorInputValues` struct — unchanged -- `ArrayCalculator` algorithm class public API — unchanged -- `ArrayCalculatorParser::parseAndValidate()` signature — unchanged -- `ArrayCalculatorParser::evaluateInto()` signature — unchanged -- Filter parameter keys, UUID, pipeline JSON format — unchanged diff --git a/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp b/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp index 9b6b1270cc..8bc48f5877 100644 --- a/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp +++ b/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp @@ -32,7 +32,6 @@ #include "simplnx/Utilities/SIMPLConversion.hpp" - namespace fs = std::filesystem; using namespace nx::core; diff --git a/src/Plugins/ITKImageProcessing/test/CMakeLists.txt b/src/Plugins/ITKImageProcessing/test/CMakeLists.txt index 759075b18f..ae6eb225fc 100644 --- a/src/Plugins/ITKImageProcessing/test/CMakeLists.txt +++ b/src/Plugins/ITKImageProcessing/test/CMakeLists.txt @@ -132,7 +132,6 @@ if(EXISTS "${DREAM3D_DATA_DIR}" AND SIMPLNX_DOWNLOAD_TEST_FILES) file(MAKE_DIRECTORY "${DREAM3D_DATA_DIR}/TestFiles/") endif() download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME fiji_montage.tar.gz SHA512 70139babc838ce3ab1f5adddfddc86dcc51996e614c6c2d757bcb2e59e8ebdc744dac269233494b1ef8d09397aecb4ccca3384f0a91bb017f2cf6309c4ac40fa) - download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME image_flip_test_images.tar.gz SHA512 4e282a270251133004bf4b979d0d064631b618fc82f503184c602c40d388b725f81faf8e77654d285852acc3217d51534c9a71240be4a87a91dc46da7871e7d2) endif() create_pipeline_tests(PLUGIN_NAME ${PLUGIN_NAME} PIPELINE_LIST ${PREBUILT_PIPELINE_NAMES}) diff --git a/src/Plugins/ITKImageProcessing/test/ITKImageReaderTest.cpp b/src/Plugins/ITKImageProcessing/test/ITKImageReaderTest.cpp index a4523f3e87..d036fe4a36 100644 --- a/src/Plugins/ITKImageProcessing/test/ITKImageReaderTest.cpp +++ b/src/Plugins/ITKImageProcessing/test/ITKImageReaderTest.cpp @@ -14,16 +14,16 @@ using namespace nx::core; namespace { -const std::string k_TestDataDirName = "itk_image_reader_test"; +const std::string k_TestDataDirName = "itk_image_reader_test_v3"; const fs::path k_TestDataDir = fs::path(unit_test::k_TestFilesDir.view()) / k_TestDataDirName; -const fs::path k_ExemplarFile = k_TestDataDir / "itk_image_reader_test.dream3d"; +const fs::path k_ExemplarFile = k_TestDataDir / "itk_image_reader_test_v3.dream3d"; const fs::path k_InputImageFile = k_TestDataDir / "200x200_0.tif"; const std::string k_ImageDataName = "ImageData"; } // namespace TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: Read_Basic", "[ITKImageProcessing][ITKImageReaderFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName, false, false); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test_v3.tar.gz", k_TestDataDirName, true, true); UnitTest::LoadPlugins(); ITKImageReaderFilter filter; @@ -66,7 +66,7 @@ TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: Read_Basic", "[ITKImageProc TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: Override_Origin", "[ITKImageProcessing][ITKImageReaderFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName, false, false); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test_v3.tar.gz", k_TestDataDirName, true, true); UnitTest::LoadPlugins(); ITKImageReaderFilter filter; @@ -112,7 +112,7 @@ TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: Override_Origin", "[ITKImag TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: Centering_Origin", "[ITKImageProcessing][ITKImageReaderFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName, false, false); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test_v3.tar.gz", k_TestDataDirName, true, true); UnitTest::LoadPlugins(); ITKImageReaderFilter filter; @@ -156,7 +156,7 @@ TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: Centering_Origin", "[ITKIma TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: Cropping", "[ITKImageProcessing][ITKImageReaderFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName, false, false); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test_v3.tar.gz", k_TestDataDirName, true, true); // This block generates every combination of croppingOptions, changeOrigin, and changeSpacing and then the entire test executes for each combination std::vector spacing = {2.0, 2.0, 1.0}; @@ -233,7 +233,7 @@ TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: Cropping", "[ITKImageProces TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: Override_Spacing", "[ITKImageProcessing][ITKImageReaderFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName, false, false); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test_v3.tar.gz", k_TestDataDirName, true, true); UnitTest::LoadPlugins(); ITKImageReaderFilter filter; @@ -279,7 +279,7 @@ TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: Override_Spacing", "[ITKIma TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: OriginSpacing_Preprocessed", "[ITKImageProcessing][ITKImageReaderFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName, false, false); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test_v3.tar.gz", k_TestDataDirName, true, true); UnitTest::LoadPlugins(); ITKImageReaderFilter filter; @@ -337,7 +337,7 @@ TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: OriginSpacing_Preprocessed" TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: OriginSpacing_Postprocessed", "[ITKImageProcessing][ITKImageReaderFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName, false, false); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test_v3.tar.gz", k_TestDataDirName, true, true); UnitTest::LoadPlugins(); ITKImageReaderFilter filter; @@ -396,7 +396,7 @@ TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: OriginSpacing_Postprocessed TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: DataType_Conversion", "[ITKImageProcessing][ITKImageReaderFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName, false, false); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test_v3.tar.gz", k_TestDataDirName, true, true); UnitTest::LoadPlugins(); ITKImageReaderFilter filter; @@ -443,7 +443,7 @@ TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: DataType_Conversion", "[ITK TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: Interaction_Crop_DataType", "[ITKImageProcessing][ITKImageReaderFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName, false, false); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test_v3.tar.gz", k_TestDataDirName, true, true); UnitTest::LoadPlugins(); ITKImageReaderFilter filter; @@ -499,7 +499,7 @@ TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: Interaction_Crop_DataType", TEST_CASE("ITKImageProcessing::ITKImageReaderFilter: Interaction_All", "[ITKImageProcessing][ITKImageReaderFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName, false, false); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test_v3.tar.gz", k_TestDataDirName, true, true); UnitTest::LoadPlugins(); ITKImageReaderFilter filter; diff --git a/src/Plugins/ITKImageProcessing/test/ITKImportImageStackTest.cpp b/src/Plugins/ITKImageProcessing/test/ITKImportImageStackTest.cpp index 412009b7b3..c40aca7c7a 100644 --- a/src/Plugins/ITKImageProcessing/test/ITKImportImageStackTest.cpp +++ b/src/Plugins/ITKImageProcessing/test/ITKImportImageStackTest.cpp @@ -20,7 +20,10 @@ namespace const std::string k_ImageStackDir = unit_test::k_DataDir.str() + "/ImageStack"; const DataPath k_ImageGeomPath = {{"ImageGeometry"}}; const DataPath k_ImageDataPath = k_ImageGeomPath.createChildPath(ImageGeom::k_CellAttributeMatrixName).createChildPath("ImageData"); -const std::string k_FlippedImageStackDirName = "image_flip_test_images"; +// The image_flip_test_images directory lives inside the import_image_stack_test_v3 archive +// alongside the main exemplar file. k_ImageFlipStackDir below resolves to +// .../TestFiles/import_image_stack_test_v3/image_flip_test_images. +const std::string k_FlippedImageStackSubDirName = "image_flip_test_images"; const DataPath k_XGeneratedImageGeomPath = DataPath({"xGeneratedImageGeom"}); const DataPath k_YGeneratedImageGeomPath = DataPath({"yGeneratedImageGeom"}); const DataPath k_XFlipImageGeomPath = DataPath({"xFlipImageGeom"}); @@ -29,7 +32,7 @@ const std::string k_ImageDataName = "ImageData"; const ChoicesParameter::ValueType k_NoImageTransform = 0; const ChoicesParameter::ValueType k_FlipAboutXAxis = 1; const ChoicesParameter::ValueType k_FlipAboutYAxis = 2; -const fs::path k_ImageFlipStackDir = fs::path(fmt::format("{}/{}", unit_test::k_TestFilesDir, k_FlippedImageStackDirName)); +const fs::path k_ImageFlipStackDir = fs::path(fmt::format("{}/import_image_stack_test_v3/{}", unit_test::k_TestFilesDir, k_FlippedImageStackSubDirName)); // Exemplar Array Paths const DataPath k_XFlippedImageDataPath = k_XFlipImageGeomPath.createChildPath(Constants::k_Cell_Data).createChildPath(::k_ImageDataName); @@ -160,10 +163,10 @@ void CompareXYFlippedGeometries(DataStructure& dataStructure) } // Test data paths -const std::string k_TestDataDirName = "import_image_stack_test"; +const std::string k_TestDataDirName = "import_image_stack_test_v3"; const fs::path k_TestDataDir = fs::path(unit_test::k_TestFilesDir.view()) / k_TestDataDirName; const fs::path k_InputImagesDir = k_TestDataDir / "input_images"; -const fs::path k_ExemplarFile = k_TestDataDir / "import_image_stack_test.dream3d"; +const fs::path k_ExemplarFile = k_TestDataDir / "import_image_stack_test_v3.dream3d"; // Standard test parameters const std::string k_FilePrefix = "200x200_"; @@ -466,7 +469,7 @@ TEST_CASE("ITKImageProcessing::ITKImportImageStackFilter: CompareImage", "[ITKIm TEST_CASE("ITKImageProcessing::ITKImportImageStackFilter: Flipped Image Even-Even X/Y", "[ITKImageProcessing][ITKImportImageStackFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "image_flip_test_images.tar.gz", k_FlippedImageStackDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v3.tar.gz", k_TestDataDirName, true, true); const std::string filePrefix = "image_flip_even_even_"; @@ -490,7 +493,7 @@ TEST_CASE("ITKImageProcessing::ITKImportImageStackFilter: Flipped Image Even-Eve TEST_CASE("ITKImageProcessing::ITKImportImageStackFilter: Flipped Image Even-Odd X/Y", "[ITKImageProcessing][ITKImportImageStackFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "image_flip_test_images.tar.gz", k_FlippedImageStackDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v3.tar.gz", k_TestDataDirName, true, true); const std::string filePrefix = "image_flip_even_odd_"; @@ -514,7 +517,7 @@ TEST_CASE("ITKImageProcessing::ITKImportImageStackFilter: Flipped Image Even-Odd TEST_CASE("ITKImageProcessing::ITKImportImageStackFilter: Flipped Image Odd-Even X/Y", "[ITKImageProcessing][ITKImportImageStackFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "image_flip_test_images.tar.gz", k_FlippedImageStackDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v3.tar.gz", k_TestDataDirName, true, true); const std::string filePrefix = "image_flip_odd_even_"; @@ -538,7 +541,7 @@ TEST_CASE("ITKImageProcessing::ITKImportImageStackFilter: Flipped Image Odd-Even TEST_CASE("ITKImageProcessing::ITKImportImageStackFilter: Flipped Image Odd-Odd X/Y", "[ITKImageProcessing][ITKImportImageStackFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "image_flip_test_images.tar.gz", k_FlippedImageStackDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v3.tar.gz", k_TestDataDirName, true, true); const std::string filePrefix = "image_flip_odd_odd_"; @@ -562,7 +565,7 @@ TEST_CASE("ITKImageProcessing::ITKImportImageStackFilter: Flipped Image Odd-Odd TEST_CASE("ITKImportImageStack::Baseline_NoProcessing", "[ITKImageProcessing][ITKImportImageStackFilter][Baseline]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v3.tar.gz", k_TestDataDirName, true, true); UnitTest::LoadPlugins(); DataStructure ds; @@ -594,7 +597,7 @@ TEST_CASE("ITKImportImageStack::Baseline_NoProcessing", "[ITKImageProcessing][IT TEST_CASE("ITKImportImageStack::Crop_Voxel_XOnly", "[ITKImageProcessing][ITKImportImageStackFilter][Cropping]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v3.tar.gz", k_TestDataDirName, true, true); UnitTest::LoadPlugins(); DataStructure ds; @@ -623,7 +626,7 @@ TEST_CASE("ITKImportImageStack::Crop_Voxel_XOnly", "[ITKImageProcessing][ITKImpo TEST_CASE("ITKImportImageStack::Crop_Voxel_YOnly", "[ITKImageProcessing][ITKImportImageStackFilter][Cropping]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v3.tar.gz", k_TestDataDirName, true, true); UnitTest::LoadPlugins(); DataStructure ds; @@ -652,7 +655,7 @@ TEST_CASE("ITKImportImageStack::Crop_Voxel_YOnly", "[ITKImageProcessing][ITKImpo TEST_CASE("ITKImportImageStack::Crop_Voxel_ZOnly", "[ITKImageProcessing][ITKImportImageStackFilter][Cropping]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v3.tar.gz", k_TestDataDirName, true, true); UnitTest::LoadPlugins(); DataStructure ds; @@ -681,7 +684,7 @@ TEST_CASE("ITKImportImageStack::Crop_Voxel_ZOnly", "[ITKImageProcessing][ITKImpo TEST_CASE("ITKImportImageStack::Crop_Voxel_XY", "[ITKImageProcessing][ITKImportImageStackFilter][Cropping]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v3.tar.gz", k_TestDataDirName, true, true); UnitTest::LoadPlugins(); DataStructure ds; @@ -710,7 +713,7 @@ TEST_CASE("ITKImportImageStack::Crop_Voxel_XY", "[ITKImageProcessing][ITKImportI TEST_CASE("ITKImportImageStack::Crop_Voxel_XYZ", "[ITKImageProcessing][ITKImportImageStackFilter][Cropping]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v3.tar.gz", k_TestDataDirName, true, true); UnitTest::LoadPlugins(); DataStructure ds; @@ -739,7 +742,7 @@ TEST_CASE("ITKImportImageStack::Crop_Voxel_XYZ", "[ITKImageProcessing][ITKImport TEST_CASE("ITKImportImageStack::Crop_Physical_XY", "[ITKImageProcessing][ITKImportImageStackFilter][Cropping]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v3.tar.gz", k_TestDataDirName, true, true); UnitTest::LoadPlugins(); DataStructure ds; @@ -768,7 +771,7 @@ TEST_CASE("ITKImportImageStack::Crop_Physical_XY", "[ITKImageProcessing][ITKImpo TEST_CASE("ITKImportImageStack::Crop_Physical_Z", "[ITKImageProcessing][ITKImportImageStackFilter][Cropping]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v3.tar.gz", k_TestDataDirName, true, true); UnitTest::LoadPlugins(); DataStructure ds; @@ -801,7 +804,7 @@ TEST_CASE("ITKImportImageStack::Crop_Physical_Z", "[ITKImageProcessing][ITKImpor TEST_CASE("ITKImportImageStack::Resample_ScalingFactor", "[ITKImageProcessing][ITKImportImageStackFilter][Resampling]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v3.tar.gz", k_TestDataDirName, true, true); UnitTest::LoadPlugins(); DataStructure ds; @@ -830,7 +833,7 @@ TEST_CASE("ITKImportImageStack::Resample_ScalingFactor", "[ITKImageProcessing][I TEST_CASE("ITKImportImageStack::Resample_ExactDimensions", "[ITKImageProcessing][ITKImportImageStackFilter][Resampling]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v3.tar.gz", k_TestDataDirName, true, true); UnitTest::LoadPlugins(); DataStructure ds; @@ -863,7 +866,7 @@ TEST_CASE("ITKImportImageStack::Resample_ExactDimensions", "[ITKImageProcessing] TEST_CASE("ITKImportImageStack::Grayscale_Conversion", "[ITKImageProcessing][ITKImportImageStackFilter][Grayscale]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v3.tar.gz", k_TestDataDirName, true, true); UnitTest::LoadPlugins(); DataStructure ds; @@ -900,7 +903,7 @@ TEST_CASE("ITKImportImageStack::Grayscale_Conversion", "[ITKImageProcessing][ITK TEST_CASE("ITKImportImageStack::FlipY", "[ITKImageProcessing][ITKImportImageStackFilter][Flip]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v3.tar.gz", k_TestDataDirName, true, true); UnitTest::LoadPlugins(); DataStructure ds; @@ -932,7 +935,7 @@ TEST_CASE("ITKImportImageStack::FlipY", "[ITKImageProcessing][ITKImportImageStac TEST_CASE("ITKImportImageStack::OriginSpacing_Preprocessed", "[ITKImageProcessing][ITKImportImageStackFilter][OriginSpacing]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v3.tar.gz", k_TestDataDirName, true, true); UnitTest::LoadPlugins(); DataStructure ds; @@ -962,7 +965,7 @@ TEST_CASE("ITKImportImageStack::OriginSpacing_Preprocessed", "[ITKImageProcessin TEST_CASE("ITKImportImageStack::OriginSpacing_Postprocessed", "[ITKImageProcessing][ITKImportImageStackFilter][OriginSpacing]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v3.tar.gz", k_TestDataDirName, true, true); UnitTest::LoadPlugins(); DataStructure ds; @@ -991,7 +994,7 @@ TEST_CASE("ITKImportImageStack::OriginSpacing_Postprocessed", "[ITKImageProcessi TEST_CASE("ITKImportImageStack::OriginSpacing_Preprocessed_WithZCrop", "[ITKImageProcessing][ITKImportImageStackFilter][OriginSpacing]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v3.tar.gz", k_TestDataDirName, true, true); UnitTest::LoadPlugins(); DataStructure ds; @@ -1020,7 +1023,7 @@ TEST_CASE("ITKImportImageStack::OriginSpacing_Preprocessed_WithZCrop", "[ITKImag TEST_CASE("ITKImportImageStack::OriginSpacing_Postprocessed_WithZCrop", "[ITKImageProcessing][ITKImportImageStackFilter][OriginSpacing]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v3.tar.gz", k_TestDataDirName, true, true); UnitTest::LoadPlugins(); DataStructure ds; @@ -1053,7 +1056,7 @@ TEST_CASE("ITKImportImageStack::OriginSpacing_Postprocessed_WithZCrop", "[ITKIma TEST_CASE("ITKImportImageStack::Interaction_Crop_Resample", "[ITKImageProcessing][ITKImportImageStackFilter][Interaction]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v3.tar.gz", k_TestDataDirName, true, true); UnitTest::LoadPlugins(); DataStructure ds; @@ -1082,7 +1085,7 @@ TEST_CASE("ITKImportImageStack::Interaction_Crop_Resample", "[ITKImageProcessing TEST_CASE("ITKImportImageStack::Interaction_Crop_Flip", "[ITKImageProcessing][ITKImportImageStackFilter][Interaction]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v3.tar.gz", k_TestDataDirName, true, true); UnitTest::LoadPlugins(); DataStructure ds; @@ -1111,7 +1114,7 @@ TEST_CASE("ITKImportImageStack::Interaction_Crop_Flip", "[ITKImageProcessing][IT TEST_CASE("ITKImportImageStack::Interaction_Resample_Flip", "[ITKImageProcessing][ITKImportImageStackFilter][Interaction]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v3.tar.gz", k_TestDataDirName, true, true); UnitTest::LoadPlugins(); DataStructure ds; @@ -1140,7 +1143,7 @@ TEST_CASE("ITKImportImageStack::Interaction_Resample_Flip", "[ITKImageProcessing TEST_CASE("ITKImportImageStack::Interaction_Crop_Grayscale", "[ITKImageProcessing][ITKImportImageStackFilter][Interaction]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v3.tar.gz", k_TestDataDirName, true, true); UnitTest::LoadPlugins(); DataStructure ds; @@ -1172,7 +1175,7 @@ TEST_CASE("ITKImportImageStack::Interaction_Crop_Grayscale", "[ITKImageProcessin TEST_CASE("ITKImportImageStack::Interaction_Resample_Grayscale", "[ITKImageProcessing][ITKImportImageStackFilter][Interaction]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v3.tar.gz", k_TestDataDirName, true, true); UnitTest::LoadPlugins(); DataStructure ds; @@ -1205,7 +1208,7 @@ TEST_CASE("ITKImportImageStack::Interaction_Resample_Grayscale", "[ITKImageProce TEST_CASE("ITKImportImageStack::Interaction_Grayscale_Flip", "[ITKImageProcessing][ITKImportImageStackFilter][Interaction]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v3.tar.gz", k_TestDataDirName, true, true); UnitTest::LoadPlugins(); DataStructure ds; @@ -1237,7 +1240,7 @@ TEST_CASE("ITKImportImageStack::Interaction_Grayscale_Flip", "[ITKImageProcessin TEST_CASE("ITKImportImageStack::Interaction_FullPipeline", "[ITKImageProcessing][ITKImportImageStackFilter][Interaction]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v3.tar.gz", k_TestDataDirName, true, true); UnitTest::LoadPlugins(); DataStructure ds; diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ReadImage.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ReadImage.cpp index 66d533e9ea..cb6d4a903c 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ReadImage.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ReadImage.cpp @@ -255,7 +255,7 @@ Result<> ReadImage::operator()() if(window.xStart + window.dstWidth > window.srcWidth || window.yStart + window.dstHeight > window.srcHeight) { return MakeErrorResult(-2001, fmt::format("Crop window (start=[{},{}], size=[{},{}]) does not fit within the source image (size=[{},{}])", window.xStart, window.yStart, window.dstWidth, - window.dstHeight, window.srcWidth, window.srcHeight)); + window.dstHeight, window.srcWidth, window.srcHeight)); } // Determine if type conversion is needed diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ReadImageFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ReadImageFilter.cpp index d5ac87c7a3..4771a91dc2 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ReadImageFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ReadImageFilter.cpp @@ -157,8 +157,8 @@ IFilter::UniquePointer ReadImageFilter::clone() const } //------------------------------------------------------------------------------ -IFilter::PreflightResult ReadImageFilter::preflightImpl(const DataStructure& dataStructure, const Arguments& filterArgs, const MessageHandler& messageHandler, - const std::atomic_bool& shouldCancel, const ExecutionContext& executionContext) const +IFilter::PreflightResult ReadImageFilter::preflightImpl(const DataStructure& dataStructure, const Arguments& filterArgs, const MessageHandler& messageHandler, const std::atomic_bool& shouldCancel, + const ExecutionContext& executionContext) const { auto fileName = filterArgs.value(k_FileName_Key); auto imageGeomPath = filterArgs.value(k_ImageGeometryPath_Key); @@ -268,12 +268,12 @@ IFilter::PreflightResult ReadImageFilter::preflightImpl(const DataStructure& dat } else { - cropImageGeomArgs.insertOrAssign("min_coord", std::make_any({static_cast(croppingOptions.xBoundPhysical[0]), - static_cast(croppingOptions.yBoundPhysical[0]), - static_cast(croppingOptions.zBoundPhysical[0])})); - cropImageGeomArgs.insertOrAssign("max_coord", std::make_any({static_cast(croppingOptions.xBoundPhysical[1]), - static_cast(croppingOptions.yBoundPhysical[1]), - static_cast(croppingOptions.zBoundPhysical[1])})); + cropImageGeomArgs.insertOrAssign( + "min_coord", std::make_any({static_cast(croppingOptions.xBoundPhysical[0]), static_cast(croppingOptions.yBoundPhysical[0]), + static_cast(croppingOptions.zBoundPhysical[0])})); + cropImageGeomArgs.insertOrAssign( + "max_coord", std::make_any({static_cast(croppingOptions.xBoundPhysical[1]), static_cast(croppingOptions.yBoundPhysical[1]), + static_cast(croppingOptions.zBoundPhysical[1])})); } cropImageGeomArgs.insertOrAssign("remove_original_geometry", std::make_any(true)); @@ -321,8 +321,7 @@ IFilter::PreflightResult ReadImageFilter::preflightImpl(const DataStructure& dat { auto originVec = std::vector{origin[0], origin[1], origin[2]}; auto spacingVec = std::vector{spacing[0], spacing[1], spacing[2]}; - resultOutputActions.value().appendAction( - std::make_unique(imageGeomPath, dims, originVec, spacingVec, cellDataName, lengthUnit)); + resultOutputActions.value().appendAction(std::make_unique(imageGeomPath, dims, originVec, spacingVec, cellDataName, lengthUnit)); } // Create the data array diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ReadImageFilter.hpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ReadImageFilter.hpp index 1cd6050d68..b301e1351a 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ReadImageFilter.hpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ReadImageFilter.hpp @@ -50,7 +50,7 @@ class SIMPLNXCORE_EXPORT ReadImageFilter : public IFilter * @return Result */ static Result FromSIMPLJson(const nlohmann::json& json); - + /** * @brief Returns the name of the filter. * @return @@ -130,7 +130,7 @@ class SIMPLNXCORE_EXPORT ReadImageFilter : public IFilter Result<> executeImpl(DataStructure& dataStructure, const Arguments& filterArgs, const PipelineFilter* pipelineNode, const MessageHandler& messageHandler, const std::atomic_bool& shouldCancel, const ExecutionContext& executionContext) const override; }; -} // namespace complex +} // namespace nx::core SIMPLNX_DEF_FILTER_TRAITS(nx::core, ReadImageFilter, "7391288d-3d0b-492e-93c9-e128e05c9737"); /* LEGACY UUID FOR THIS FILTER @OLD_UUID@ */ diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ReadImageStackFilter.hpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ReadImageStackFilter.hpp index b021bbe767..ee11f1c0ab 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ReadImageStackFilter.hpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ReadImageStackFilter.hpp @@ -49,7 +49,7 @@ class SIMPLNXCORE_EXPORT ReadImageStackFilter : public IFilter * @return Result */ static Result FromSIMPLJson(const nlohmann::json& json); - + /** * @brief Returns the name of the filter. * @return @@ -129,7 +129,7 @@ class SIMPLNXCORE_EXPORT ReadImageStackFilter : public IFilter Result<> executeImpl(DataStructure& dataStructure, const Arguments& filterArgs, const PipelineFilter* pipelineNode, const MessageHandler& messageHandler, const std::atomic_bool& shouldCancel, const ExecutionContext& executionContext) const override; }; -} // namespace complex +} // namespace nx::core SIMPLNX_DEF_FILTER_TRAITS(nx::core, ReadImageStackFilter, "15e7784e-6a9b-457c-9f5c-f5983246c8e8"); /* LEGACY UUID FOR THIS FILTER @OLD_UUID@ */ diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteImageFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteImageFilter.cpp index 2f5fbae258..21a3094a77 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteImageFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteImageFilter.cpp @@ -34,9 +34,8 @@ namespace { const std::set& GetScalarPixelAllowedTypes() { - static const std::set dataTypes = {nx::core::DataType::int8, nx::core::DataType::uint8, nx::core::DataType::int16, - nx::core::DataType::uint16, nx::core::DataType::int32, nx::core::DataType::uint32, - nx::core::DataType::float32}; + static const std::set dataTypes = {nx::core::DataType::int8, nx::core::DataType::uint8, nx::core::DataType::int16, nx::core::DataType::uint16, + nx::core::DataType::int32, nx::core::DataType::uint32, nx::core::DataType::float32}; return dataTypes; } } // namespace @@ -91,8 +90,8 @@ Parameters WriteImageFilter::parameters() const params.insertSeparator(Parameters::Separator{"Input Cell Data"}); params.insert(std::make_unique(k_ImageGeomPath_Key, "Image Geometry", "Select the Image Geometry Group from the DataStructure.", DataPath{}, GeometrySelectionParameter::AllowedTypes{IGeometry::Type::Image})); - params.insert(std::make_unique(k_ImageArrayPath_Key, "Input Image Data Array", "The image data that will be processed by this filter.", DataPath{}, - ::GetScalarPixelAllowedTypes())); + params.insert( + std::make_unique(k_ImageArrayPath_Key, "Input Image Data Array", "The image data that will be processed by this filter.", DataPath{}, ::GetScalarPixelAllowedTypes())); return params; } @@ -110,8 +109,8 @@ IFilter::UniquePointer WriteImageFilter::clone() const } //------------------------------------------------------------------------------ -IFilter::PreflightResult WriteImageFilter::preflightImpl(const DataStructure& dataStructure, const Arguments& filterArgs, const MessageHandler& messageHandler, - const std::atomic_bool& shouldCancel, const ExecutionContext& executionContext) const +IFilter::PreflightResult WriteImageFilter::preflightImpl(const DataStructure& dataStructure, const Arguments& filterArgs, const MessageHandler& messageHandler, const std::atomic_bool& shouldCancel, + const ExecutionContext& executionContext) const { auto plane = filterArgs.value(k_Plane_Key); auto filePath = filterArgs.value(k_FileName_Key); @@ -143,8 +142,8 @@ IFilter::PreflightResult WriteImageFilter::preflightImpl(const DataStructure& da const auto& allowedTypes = ::GetScalarPixelAllowedTypes(); if(allowedTypes.find(arrayDataType) == allowedTypes.end()) { - return {MakeErrorResult(-27011, fmt::format("Unsupported data type '{}' for image writing. Supported types: int8, uint8, int16, uint16, int32, uint32, float32.", - DataTypeToString(arrayDataType)))}; + return {MakeErrorResult( + -27011, fmt::format("Unsupported data type '{}' for image writing. Supported types: int8, uint8, int16, uint16, int32, uint32, float32.", DataTypeToString(arrayDataType)))}; } // Compute slice count based on plane and geometry dims diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteImageFilter.hpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteImageFilter.hpp index a99a77edd8..d42ba4542a 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteImageFilter.hpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteImageFilter.hpp @@ -38,7 +38,7 @@ class SIMPLNXCORE_EXPORT WriteImageFilter : public IFilter * @return Result */ static Result FromSIMPLJson(const nlohmann::json& json); - + /** * @brief Returns the name of the filter. * @return @@ -118,7 +118,7 @@ class SIMPLNXCORE_EXPORT WriteImageFilter : public IFilter Result<> executeImpl(DataStructure& dataStructure, const Arguments& filterArgs, const PipelineFilter* pipelineNode, const MessageHandler& messageHandler, const std::atomic_bool& shouldCancel, const ExecutionContext& executionContext) const override; }; -} // namespace complex +} // namespace nx::core SIMPLNX_DEF_FILTER_TRAITS(nx::core, WriteImageFilter, "a8b920c7-5445-4c8a-b7d7-6cabc578d587"); /* LEGACY UUID FOR THIS FILTER @OLD_UUID@ */ diff --git a/src/Plugins/SimplnxCore/test/CMakeLists.txt b/src/Plugins/SimplnxCore/test/CMakeLists.txt index bf520cf7bc..265e09e869 100644 --- a/src/Plugins/SimplnxCore/test/CMakeLists.txt +++ b/src/Plugins/SimplnxCore/test/CMakeLists.txt @@ -255,10 +255,7 @@ if(EXISTS "${DREAM3D_DATA_DIR}" AND SIMPLNX_DOWNLOAD_TEST_FILES) download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME generate_vector_colors.tar.gz SHA512 a30ac9ac35002f4a79b60a02a86de1661c4828c77b45d3848c6e2c37943c8874756f0e163cda99f2c0de372ff1cd9257288d5357fa3db8a539428df713325ee7) download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME identify_sample.tar.gz SHA512 0c1da22d411ac739d3e90618141a6eab71705b47de6d4cc7501e9408bf6fcbadd46738f5e86a80ab2e0bc2344c23584cc2b89f2102c13073490e1817797ec9bc) download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME image_coords_test.tar.gz SHA512 bca23117b00e77e0401b83098caba6c4586f3d7068eb5b513da1be365a3b56aabe4a27106cb1e30c5f06192c277fb7cce208e7b180f93807d72f5f2e01e26c43) - download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME image_flip_test_images.tar.gz SHA512 4e282a270251133004bf4b979d0d064631b618fc82f503184c602c40d388b725f81faf8e77654d285852acc3217d51534c9a71240be4a87a91dc46da7871e7d2) - download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME import_image_stack_test_v2.tar.gz SHA512 b3600c072ecbdb27ed3ed7298dac88708aa94d1eed21e6b0581b772311d1e3c2c6693713026ae512c86729079b41c629067fb1a7df75697d08dbdf2dfad0f553) download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME initialize_data_test_files.tar.gz SHA512 f04fe76ef96add4b775111ae7fc95233a7748a25501d9f00a8b2a162c87782b8cd2813e6e46ba7892e721976693da06965e624335dbb28224c9c5b877a05aa49) - download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME itk_image_reader_test.tar.gz SHA512 15c42c913df8e0aebfc4196ea96ed5fdc2fab71daacbaf27287d6aee08483acb37f2f38d88af6c549084bae892b07c7aca8537b2511920d18248c8bc18aab92f) download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME k_files_v2.tar.gz SHA512 d5479ad573f8ce582e32c9f7a4824885b965f08cd8ed6025daf383758088716b1e9109905ab830b9f4d4f9df0145cbeb2534258237846c47e4a046b24fb3f72c) download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME label_triangle_geometry_test_v2.tar.gz SHA512 3f2009a1e37d8f5c983ee125335f42900f364653a417d8c49c9409a41a032c6f77a80b0623f5c71b1ae736f528d3b07949f7703bc342876f5424d884c3d4226c) download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME LosAlamosFFTExemplar.tar.gz SHA512 c4f2f6cbdaea4512ca2911cb0c3169e62449fcdb7cb99a5e64640601239a08cc09ea35f81f17b056fd38da85add95a37b244b7f844fe0abc944c1ba2f7514812) diff --git a/src/Plugins/SimplnxCore/test/ReadImageStackTest.cpp b/src/Plugins/SimplnxCore/test/ReadImageStackTest.cpp index f46aca4b63..5d1e299705 100644 --- a/src/Plugins/SimplnxCore/test/ReadImageStackTest.cpp +++ b/src/Plugins/SimplnxCore/test/ReadImageStackTest.cpp @@ -26,7 +26,10 @@ namespace const std::string k_ImageStackDir = std::string(unit_test::k_SimplnxSourceDIr.view()) + "/src/Plugins/ITKImageProcessing/data/ImageStack"; const DataPath k_ImageGeomPath = {{"ImageGeometry"}}; const DataPath k_ImageDataPath = k_ImageGeomPath.createChildPath(ImageGeom::k_CellAttributeMatrixName).createChildPath("ImageData"); -const std::string k_FlippedImageStackDirName = "image_flip_test_images"; +// The image_flip_test_images directory lives inside the import_image_stack_test_v3 archive +// alongside the main exemplar file. The k_ImageFlipStackDir path below resolves to +// .../TestFiles/import_image_stack_test_v3/image_flip_test_images. +const std::string k_FlippedImageStackSubDirName = "image_flip_test_images"; const DataPath k_XGeneratedImageGeomPath = DataPath({"xGeneratedImageGeom"}); const DataPath k_YGeneratedImageGeomPath = DataPath({"yGeneratedImageGeom"}); const DataPath k_XFlipImageGeomPath = DataPath({"xFlipImageGeom"}); @@ -35,7 +38,7 @@ const std::string k_ImageDataName = "ImageData"; const ChoicesParameter::ValueType k_NoImageTransform = 0; const ChoicesParameter::ValueType k_FlipAboutXAxis = 1; const ChoicesParameter::ValueType k_FlipAboutYAxis = 2; -const fs::path k_ImageFlipStackDir = fs::path(fmt::format("{}/{}", unit_test::k_TestFilesDir, k_FlippedImageStackDirName)); +const fs::path k_ImageFlipStackDir = fs::path(fmt::format("{}/import_image_stack_test_v3/{}", unit_test::k_TestFilesDir, k_FlippedImageStackSubDirName)); // Exemplar Array Paths const DataPath k_XFlippedImageDataPath = k_XFlipImageGeomPath.createChildPath(Constants::k_Cell_Data).createChildPath(::k_ImageDataName); @@ -154,10 +157,10 @@ void CompareXYFlippedGeometries(DataStructure& dataStructure) } // Test data paths -const std::string k_TestDataDirName = "import_image_stack_test"; +const std::string k_TestDataDirName = "import_image_stack_test_v3"; const fs::path k_TestDataDir = fs::path(unit_test::k_TestFilesDir.view()) / k_TestDataDirName; const fs::path k_InputImagesDir = k_TestDataDir / "input_images"; -const fs::path k_ExemplarFile = k_TestDataDir / "import_image_stack_test.dream3d"; +const fs::path k_ExemplarFile = k_TestDataDir / "import_image_stack_test_v3.dream3d"; // Standard test parameters const std::string k_FilePrefix = "200x200_"; @@ -453,7 +456,7 @@ TEST_CASE("SimplnxCore::ReadImageStackFilter: CompareImage", "[SimplnxCore][Read TEST_CASE("SimplnxCore::ReadImageStackFilter: Flipped Image Even-Even X/Y", "[SimplnxCore][ReadImageStackFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "image_flip_test_images.tar.gz", k_FlippedImageStackDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v3.tar.gz", k_TestDataDirName, true, true); const std::string filePrefix = "image_flip_even_even_"; @@ -477,7 +480,7 @@ TEST_CASE("SimplnxCore::ReadImageStackFilter: Flipped Image Even-Even X/Y", "[Si TEST_CASE("SimplnxCore::ReadImageStackFilter: Flipped Image Even-Odd X/Y", "[SimplnxCore][ReadImageStackFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "image_flip_test_images.tar.gz", k_FlippedImageStackDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v3.tar.gz", k_TestDataDirName, true, true); const std::string filePrefix = "image_flip_even_odd_"; @@ -501,7 +504,7 @@ TEST_CASE("SimplnxCore::ReadImageStackFilter: Flipped Image Even-Odd X/Y", "[Sim TEST_CASE("SimplnxCore::ReadImageStackFilter: Flipped Image Odd-Even X/Y", "[SimplnxCore][ReadImageStackFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "image_flip_test_images.tar.gz", k_FlippedImageStackDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v3.tar.gz", k_TestDataDirName, true, true); const std::string filePrefix = "image_flip_odd_even_"; @@ -521,7 +524,7 @@ TEST_CASE("SimplnxCore::ReadImageStackFilter: Flipped Image Odd-Even X/Y", "[Sim TEST_CASE("SimplnxCore::ReadImageStackFilter: Flipped Image Odd-Odd X/Y", "[SimplnxCore][ReadImageStackFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "image_flip_test_images.tar.gz", k_FlippedImageStackDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v3.tar.gz", k_TestDataDirName, true, true); const std::string filePrefix = "image_flip_odd_odd_"; @@ -541,7 +544,7 @@ TEST_CASE("SimplnxCore::ReadImageStackFilter: Flipped Image Odd-Odd X/Y", "[Simp TEST_CASE("SimplnxCore::ReadImageStackFilter::Baseline_NoProcessing", "[SimplnxCore][ReadImageStackFilter][Baseline]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v3.tar.gz", k_TestDataDirName, true, true); UnitTest::LoadPlugins(); DataStructure ds; @@ -573,7 +576,7 @@ TEST_CASE("SimplnxCore::ReadImageStackFilter::Baseline_NoProcessing", "[SimplnxC TEST_CASE("SimplnxCore::ReadImageStackFilter::Crop_Voxel_XOnly", "[SimplnxCore][ReadImageStackFilter][Cropping]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v3.tar.gz", k_TestDataDirName, true, true); UnitTest::LoadPlugins(); DataStructure ds; @@ -600,7 +603,7 @@ TEST_CASE("SimplnxCore::ReadImageStackFilter::Crop_Voxel_XOnly", "[SimplnxCore][ TEST_CASE("SimplnxCore::ReadImageStackFilter::Crop_Voxel_YOnly", "[SimplnxCore][ReadImageStackFilter][Cropping]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v3.tar.gz", k_TestDataDirName, true, true); UnitTest::LoadPlugins(); DataStructure ds; @@ -627,7 +630,7 @@ TEST_CASE("SimplnxCore::ReadImageStackFilter::Crop_Voxel_YOnly", "[SimplnxCore][ TEST_CASE("SimplnxCore::ReadImageStackFilter::Crop_Voxel_ZOnly", "[SimplnxCore][ReadImageStackFilter][Cropping]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v3.tar.gz", k_TestDataDirName, true, true); UnitTest::LoadPlugins(); DataStructure ds; @@ -654,7 +657,7 @@ TEST_CASE("SimplnxCore::ReadImageStackFilter::Crop_Voxel_ZOnly", "[SimplnxCore][ TEST_CASE("SimplnxCore::ReadImageStackFilter::Crop_Voxel_XY", "[SimplnxCore][ReadImageStackFilter][Cropping]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v3.tar.gz", k_TestDataDirName, true, true); UnitTest::LoadPlugins(); DataStructure ds; @@ -681,7 +684,7 @@ TEST_CASE("SimplnxCore::ReadImageStackFilter::Crop_Voxel_XY", "[SimplnxCore][Rea TEST_CASE("SimplnxCore::ReadImageStackFilter::Crop_Voxel_XYZ", "[SimplnxCore][ReadImageStackFilter][Cropping]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v3.tar.gz", k_TestDataDirName, true, true); UnitTest::LoadPlugins(); DataStructure ds; @@ -708,7 +711,7 @@ TEST_CASE("SimplnxCore::ReadImageStackFilter::Crop_Voxel_XYZ", "[SimplnxCore][Re TEST_CASE("SimplnxCore::ReadImageStackFilter::Crop_Physical_XY", "[SimplnxCore][ReadImageStackFilter][Cropping]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v3.tar.gz", k_TestDataDirName, true, true); UnitTest::LoadPlugins(); DataStructure ds; @@ -735,7 +738,7 @@ TEST_CASE("SimplnxCore::ReadImageStackFilter::Crop_Physical_XY", "[SimplnxCore][ TEST_CASE("SimplnxCore::ReadImageStackFilter::Crop_Physical_Z", "[SimplnxCore][ReadImageStackFilter][Cropping]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v3.tar.gz", k_TestDataDirName, true, true); UnitTest::LoadPlugins(); DataStructure ds; @@ -766,7 +769,7 @@ TEST_CASE("SimplnxCore::ReadImageStackFilter::Crop_Physical_Z", "[SimplnxCore][R TEST_CASE("SimplnxCore::ReadImageStackFilter::Resample_ScalingFactor", "[SimplnxCore][ReadImageStackFilter][Resampling]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v3.tar.gz", k_TestDataDirName, true, true); UnitTest::LoadPlugins(); DataStructure ds; @@ -793,7 +796,7 @@ TEST_CASE("SimplnxCore::ReadImageStackFilter::Resample_ScalingFactor", "[Simplnx TEST_CASE("SimplnxCore::ReadImageStackFilter::Resample_ExactDimensions", "[SimplnxCore][ReadImageStackFilter][Resampling]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v3.tar.gz", k_TestDataDirName, true, true); UnitTest::LoadPlugins(); DataStructure ds; @@ -824,7 +827,7 @@ TEST_CASE("SimplnxCore::ReadImageStackFilter::Resample_ExactDimensions", "[Simpl TEST_CASE("SimplnxCore::ReadImageStackFilter::Grayscale_Conversion", "[SimplnxCore][ReadImageStackFilter][Grayscale]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v3.tar.gz", k_TestDataDirName, true, true); UnitTest::LoadPlugins(); DataStructure ds; @@ -858,7 +861,7 @@ TEST_CASE("SimplnxCore::ReadImageStackFilter::Grayscale_Conversion", "[SimplnxCo TEST_CASE("SimplnxCore::ReadImageStackFilter::FlipY", "[SimplnxCore][ReadImageStackFilter][Flip]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v3.tar.gz", k_TestDataDirName, true, true); UnitTest::LoadPlugins(); DataStructure ds; @@ -889,7 +892,7 @@ TEST_CASE("SimplnxCore::ReadImageStackFilter::FlipY", "[SimplnxCore][ReadImageSt TEST_CASE("SimplnxCore::ReadImageStackFilter::OriginSpacing_Preprocessed", "[SimplnxCore][ReadImageStackFilter][OriginSpacing]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v3.tar.gz", k_TestDataDirName, true, true); UnitTest::LoadPlugins(); DataStructure ds; @@ -917,7 +920,7 @@ TEST_CASE("SimplnxCore::ReadImageStackFilter::OriginSpacing_Preprocessed", "[Sim TEST_CASE("SimplnxCore::ReadImageStackFilter::OriginSpacing_Postprocessed", "[SimplnxCore][ReadImageStackFilter][OriginSpacing]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v3.tar.gz", k_TestDataDirName, true, true); UnitTest::LoadPlugins(); DataStructure ds; @@ -945,7 +948,7 @@ TEST_CASE("SimplnxCore::ReadImageStackFilter::OriginSpacing_Postprocessed", "[Si TEST_CASE("SimplnxCore::ReadImageStackFilter::OriginSpacing_Preprocessed_WithZCrop", "[SimplnxCore][ReadImageStackFilter][OriginSpacing]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v3.tar.gz", k_TestDataDirName, true, true); UnitTest::LoadPlugins(); DataStructure ds; @@ -973,7 +976,7 @@ TEST_CASE("SimplnxCore::ReadImageStackFilter::OriginSpacing_Preprocessed_WithZCr TEST_CASE("SimplnxCore::ReadImageStackFilter::OriginSpacing_Postprocessed_WithZCrop", "[SimplnxCore][ReadImageStackFilter][OriginSpacing]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v3.tar.gz", k_TestDataDirName, true, true); UnitTest::LoadPlugins(); DataStructure ds; @@ -1005,7 +1008,7 @@ TEST_CASE("SimplnxCore::ReadImageStackFilter::OriginSpacing_Postprocessed_WithZC TEST_CASE("SimplnxCore::ReadImageStackFilter::Interaction_Crop_Resample", "[SimplnxCore][ReadImageStackFilter][Interaction]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v3.tar.gz", k_TestDataDirName, true, true); UnitTest::LoadPlugins(); DataStructure ds; @@ -1032,7 +1035,7 @@ TEST_CASE("SimplnxCore::ReadImageStackFilter::Interaction_Crop_Resample", "[Simp TEST_CASE("SimplnxCore::ReadImageStackFilter::Interaction_Crop_Flip", "[SimplnxCore][ReadImageStackFilter][Interaction]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v3.tar.gz", k_TestDataDirName, true, true); UnitTest::LoadPlugins(); DataStructure ds; @@ -1059,7 +1062,7 @@ TEST_CASE("SimplnxCore::ReadImageStackFilter::Interaction_Crop_Flip", "[SimplnxC TEST_CASE("SimplnxCore::ReadImageStackFilter::Interaction_Resample_Flip", "[SimplnxCore][ReadImageStackFilter][Interaction]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v3.tar.gz", k_TestDataDirName, true, true); UnitTest::LoadPlugins(); DataStructure ds; @@ -1086,7 +1089,7 @@ TEST_CASE("SimplnxCore::ReadImageStackFilter::Interaction_Resample_Flip", "[Simp TEST_CASE("SimplnxCore::ReadImageStackFilter::Interaction_Crop_Grayscale", "[SimplnxCore][ReadImageStackFilter][Interaction]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v3.tar.gz", k_TestDataDirName, true, true); UnitTest::LoadPlugins(); DataStructure ds; @@ -1116,7 +1119,7 @@ TEST_CASE("SimplnxCore::ReadImageStackFilter::Interaction_Crop_Grayscale", "[Sim TEST_CASE("SimplnxCore::ReadImageStackFilter::Interaction_Resample_Grayscale", "[SimplnxCore][ReadImageStackFilter][Interaction]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v3.tar.gz", k_TestDataDirName, true, true); UnitTest::LoadPlugins(); DataStructure ds; @@ -1146,7 +1149,7 @@ TEST_CASE("SimplnxCore::ReadImageStackFilter::Interaction_Resample_Grayscale", " TEST_CASE("SimplnxCore::ReadImageStackFilter::Interaction_Grayscale_Flip", "[SimplnxCore][ReadImageStackFilter][Interaction]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v3.tar.gz", k_TestDataDirName, true, true); UnitTest::LoadPlugins(); DataStructure ds; @@ -1176,7 +1179,7 @@ TEST_CASE("SimplnxCore::ReadImageStackFilter::Interaction_Grayscale_Flip", "[Sim TEST_CASE("SimplnxCore::ReadImageStackFilter::Interaction_FullPipeline", "[SimplnxCore][ReadImageStackFilter][Interaction]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v2.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "import_image_stack_test_v3.tar.gz", k_TestDataDirName, true, true); UnitTest::LoadPlugins(); DataStructure ds; diff --git a/src/Plugins/SimplnxCore/test/ReadImageTest.cpp b/src/Plugins/SimplnxCore/test/ReadImageTest.cpp index 59265ea9a3..51e99c616e 100644 --- a/src/Plugins/SimplnxCore/test/ReadImageTest.cpp +++ b/src/Plugins/SimplnxCore/test/ReadImageTest.cpp @@ -15,9 +15,9 @@ using namespace nx::core::UnitTest; namespace { -const std::string k_TestDataDirName = "itk_image_reader_test"; +const std::string k_TestDataDirName = "itk_image_reader_test_v3"; const fs::path k_TestDataDir = fs::path(unit_test::k_TestFilesDir.view()) / k_TestDataDirName; -const fs::path k_ExemplarFile = k_TestDataDir / "itk_image_reader_test.dream3d"; +const fs::path k_ExemplarFile = k_TestDataDir / "itk_image_reader_test_v3.dream3d"; const fs::path k_InputImageFile = k_TestDataDir / "200x200_0.tif"; const std::string k_ImageGeometryName = "[ImageGeometry]"; const std::string k_ImageCellDataName = "Cell Data"; @@ -31,7 +31,7 @@ constexpr uint64 k_Postprocessed = 1; TEST_CASE("SimplnxCore::ReadImageFilter: Read_Basic", "[SimplnxCore][ReadImageFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test_v3.tar.gz", k_TestDataDirName, true, true); UnitTest::LoadPlugins(); ReadImageFilter filter; @@ -74,7 +74,7 @@ TEST_CASE("SimplnxCore::ReadImageFilter: Read_Basic", "[SimplnxCore][ReadImageFi TEST_CASE("SimplnxCore::ReadImageFilter: Override_Origin", "[SimplnxCore][ReadImageFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test_v3.tar.gz", k_TestDataDirName, true, true); UnitTest::LoadPlugins(); ReadImageFilter filter; @@ -120,7 +120,7 @@ TEST_CASE("SimplnxCore::ReadImageFilter: Override_Origin", "[SimplnxCore][ReadIm TEST_CASE("SimplnxCore::ReadImageFilter: Centering_Origin", "[SimplnxCore][ReadImageFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test_v3.tar.gz", k_TestDataDirName, true, true); UnitTest::LoadPlugins(); ReadImageFilter filter; @@ -164,7 +164,7 @@ TEST_CASE("SimplnxCore::ReadImageFilter: Centering_Origin", "[SimplnxCore][ReadI TEST_CASE("SimplnxCore::ReadImageFilter: Override_Spacing", "[SimplnxCore][ReadImageFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test_v3.tar.gz", k_TestDataDirName, true, true); UnitTest::LoadPlugins(); ReadImageFilter filter; @@ -210,7 +210,7 @@ TEST_CASE("SimplnxCore::ReadImageFilter: Override_Spacing", "[SimplnxCore][ReadI TEST_CASE("SimplnxCore::ReadImageFilter: OriginSpacing_Preprocessed", "[SimplnxCore][ReadImageFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test_v3.tar.gz", k_TestDataDirName, true, true); UnitTest::LoadPlugins(); ReadImageFilter filter; @@ -268,7 +268,7 @@ TEST_CASE("SimplnxCore::ReadImageFilter: OriginSpacing_Preprocessed", "[SimplnxC TEST_CASE("SimplnxCore::ReadImageFilter: OriginSpacing_Postprocessed", "[SimplnxCore][ReadImageFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test_v3.tar.gz", k_TestDataDirName, true, true); UnitTest::LoadPlugins(); ReadImageFilter filter; @@ -326,7 +326,7 @@ TEST_CASE("SimplnxCore::ReadImageFilter: OriginSpacing_Postprocessed", "[Simplnx TEST_CASE("SimplnxCore::ReadImageFilter: DataType_Conversion", "[SimplnxCore][ReadImageFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test_v3.tar.gz", k_TestDataDirName, true, true); UnitTest::LoadPlugins(); ReadImageFilter filter; @@ -373,7 +373,7 @@ TEST_CASE("SimplnxCore::ReadImageFilter: DataType_Conversion", "[SimplnxCore][Re TEST_CASE("SimplnxCore::ReadImageFilter: Interaction_Crop_DataType", "[SimplnxCore][ReadImageFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test_v3.tar.gz", k_TestDataDirName, true, true); UnitTest::LoadPlugins(); ReadImageFilter filter; @@ -429,7 +429,7 @@ TEST_CASE("SimplnxCore::ReadImageFilter: Interaction_Crop_DataType", "[SimplnxCo TEST_CASE("SimplnxCore::ReadImageFilter: Interaction_All", "[SimplnxCore][ReadImageFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test.tar.gz", k_TestDataDirName); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "itk_image_reader_test_v3.tar.gz", k_TestDataDirName, true, true); UnitTest::LoadPlugins(); ReadImageFilter filter; diff --git a/src/simplnx/Utilities/ImageIO/ImageMetadata.hpp b/src/simplnx/Utilities/ImageIO/ImageMetadata.hpp index c2dad7724b..e2decdf2ef 100644 --- a/src/simplnx/Utilities/ImageIO/ImageMetadata.hpp +++ b/src/simplnx/Utilities/ImageIO/ImageMetadata.hpp @@ -20,13 +20,13 @@ namespace nx::core */ struct SIMPLNX_EXPORT ImageMetadata { - usize width = 0; ///< X dimension in pixels - usize height = 0; ///< Y dimension in pixels - usize numComponents = 0; ///< 1=grayscale, 3=RGB, 4=RGBA - DataType dataType = DataType::uint8; ///< Pixel data type (uint8, uint16, or float32) - usize numPages = 1; ///< Number of pages (>1 for multi-page TIFF) - std::optional origin; ///< Image origin if stored in file - std::optional spacing; ///< Image spacing/resolution if stored in file + usize width = 0; ///< X dimension in pixels + usize height = 0; ///< Y dimension in pixels + usize numComponents = 0; ///< 1=grayscale, 3=RGB, 4=RGBA + DataType dataType = DataType::uint8; ///< Pixel data type (uint8, uint16, or float32) + usize numPages = 1; ///< Number of pages (>1 for multi-page TIFF) + std::optional origin; ///< Image origin if stored in file + std::optional spacing; ///< Image spacing/resolution if stored in file }; } // namespace nx::core diff --git a/src/simplnx/Utilities/ImageIO/StbImageIO.cpp b/src/simplnx/Utilities/ImageIO/StbImageIO.cpp index cb27c16c3e..b25ae05d5a 100644 --- a/src/simplnx/Utilities/ImageIO/StbImageIO.cpp +++ b/src/simplnx/Utilities/ImageIO/StbImageIO.cpp @@ -105,8 +105,7 @@ Result<> StbImageIO::readPixelData(const std::filesystem::path& filePath, std::v if(buffer.size() != expectedSize) { - return MakeErrorResult(k_ErrorBufferSizeMismatch, - fmt::format("Buffer size {} does not match expected size {} for image '{}'", buffer.size(), expectedSize, pathStr)); + return MakeErrorResult(k_ErrorBufferSizeMismatch, fmt::format("Buffer size {} does not match expected size {} for image '{}'", buffer.size(), expectedSize, pathStr)); } if(metadata.dataType == DataType::float32) @@ -151,8 +150,7 @@ Result<> StbImageIO::writePixelData(const std::filesystem::path& filePath, const { if(metadata.dataType != DataType::uint8) { - return MakeErrorResult(k_ErrorUnsupportedDataType, - fmt::format("stb_image_write only supports uint8 pixel data for writing. Got unsupported data type for '{}'.", filePath.string())); + return MakeErrorResult(k_ErrorUnsupportedDataType, fmt::format("stb_image_write only supports uint8 pixel data for writing. Got unsupported data type for '{}'.", filePath.string())); } std::string pathStr = filePath.string(); @@ -182,8 +180,7 @@ Result<> StbImageIO::writePixelData(const std::filesystem::path& filePath, const } else { - return MakeErrorResult(k_ErrorUnsupportedWriteFormat, - fmt::format("Unsupported write format '{}' for stb backend. Supported: .png, .bmp, .jpg, .jpeg", ext)); + return MakeErrorResult(k_ErrorUnsupportedWriteFormat, fmt::format("Unsupported write format '{}' for stb backend. Supported: .png, .bmp, .jpg, .jpeg", ext)); } if(result == 0) diff --git a/src/simplnx/Utilities/ImageIO/TiffImageIO.cpp b/src/simplnx/Utilities/ImageIO/TiffImageIO.cpp index ac0f8001b0..239f417d00 100644 --- a/src/simplnx/Utilities/ImageIO/TiffImageIO.cpp +++ b/src/simplnx/Utilities/ImageIO/TiffImageIO.cpp @@ -149,8 +149,7 @@ Result TiffImageIO::readMetadata(const std::filesystem::path& fil TiffHandleGuard tiffGuard(TIFFOpen(pathStr.c_str(), "r")); if(tiffGuard.get() == nullptr) { - return MakeErrorResult(k_ErrorOpenFailed, - fmt::format("Failed to open TIFF file '{}': {}", pathStr, s_TiffErrorMessage.empty() ? "unknown error" : s_TiffErrorMessage)); + return MakeErrorResult(k_ErrorOpenFailed, fmt::format("Failed to open TIFF file '{}': {}", pathStr, s_TiffErrorMessage.empty() ? "unknown error" : s_TiffErrorMessage)); } TIFF* tiff = tiffGuard.get(); @@ -229,8 +228,7 @@ Result<> TiffImageIO::readPixelData(const std::filesystem::path& filePath, std:: TiffHandleGuard tiffGuard(TIFFOpen(pathStr.c_str(), "r")); if(tiffGuard.get() == nullptr) { - return MakeErrorResult(k_ErrorOpenFailed, - fmt::format("Failed to open TIFF file '{}': {}", pathStr, s_TiffErrorMessage.empty() ? "unknown error" : s_TiffErrorMessage)); + return MakeErrorResult(k_ErrorOpenFailed, fmt::format("Failed to open TIFF file '{}': {}", pathStr, s_TiffErrorMessage.empty() ? "unknown error" : s_TiffErrorMessage)); } TIFF* tiff = tiffGuard.get(); @@ -247,15 +245,13 @@ Result<> TiffImageIO::readPixelData(const std::filesystem::path& filePath, std:: usize bpe = bytesPerElement(dataType); if(bpe == 0) { - return MakeErrorResult(k_ErrorUnsupportedFormat, - fmt::format("Unsupported TIFF pixel format in '{}': could not determine bytes per element", pathStr)); + return MakeErrorResult(k_ErrorUnsupportedFormat, fmt::format("Unsupported TIFF pixel format in '{}': could not determine bytes per element", pathStr)); } usize expectedSize = static_cast(width) * static_cast(height) * static_cast(samplesPerPixel) * bpe; if(buffer.size() != expectedSize) { - return MakeErrorResult(k_ErrorBufferSizeMismatch, - fmt::format("Buffer size {} does not match expected size {} for TIFF image '{}'", buffer.size(), expectedSize, pathStr)); + return MakeErrorResult(k_ErrorBufferSizeMismatch, fmt::format("Buffer size {} does not match expected size {} for TIFF image '{}'", buffer.size(), expectedSize, pathStr)); } // Check if the image is tiled @@ -265,15 +261,13 @@ Result<> TiffImageIO::readPixelData(const std::filesystem::path& filePath, std:: // This converts to uint8 RGBA, so it only works well for uint8 images. if(dataType != DataType::uint8) { - return MakeErrorResult(k_ErrorUnsupportedFormat, - fmt::format("Tiled TIFF with non-uint8 data is not supported for '{}'. Convert to stripped TIFF first.", pathStr)); + return MakeErrorResult(k_ErrorUnsupportedFormat, fmt::format("Tiled TIFF with non-uint8 data is not supported for '{}'. Convert to stripped TIFF first.", pathStr)); } std::vector raster(static_cast(width) * static_cast(height)); if(TIFFReadRGBAImageOriented(tiff, width, height, raster.data(), ORIENTATION_TOPLEFT, 0) == 0) { - return MakeErrorResult(k_ErrorReadPixelFailed, - fmt::format("Failed to read tiled TIFF pixel data from '{}': {}", pathStr, s_TiffErrorMessage.empty() ? "unknown error" : s_TiffErrorMessage)); + return MakeErrorResult(k_ErrorReadPixelFailed, fmt::format("Failed to read tiled TIFF pixel data from '{}': {}", pathStr, s_TiffErrorMessage.empty() ? "unknown error" : s_TiffErrorMessage)); } // TIFFReadRGBAImageOriented produces ABGR uint32 packed pixels. @@ -321,8 +315,7 @@ Result<> TiffImageIO::readPixelData(const std::filesystem::path& filePath, std:: { if(TIFFReadScanline(tiff, scanlineBuf.data(), row) < 0) { - return MakeErrorResult(k_ErrorReadPixelFailed, - fmt::format("Failed to read scanline {} from TIFF '{}': {}", row, pathStr, s_TiffErrorMessage.empty() ? "unknown error" : s_TiffErrorMessage)); + return MakeErrorResult(k_ErrorReadPixelFailed, fmt::format("Failed to read scanline {} from TIFF '{}': {}", row, pathStr, s_TiffErrorMessage.empty() ? "unknown error" : s_TiffErrorMessage)); } std::memcpy(buffer.data() + (static_cast(row) * rowBytes), scanlineBuf.data(), rowBytes); } @@ -338,23 +331,20 @@ Result<> TiffImageIO::writePixelData(const std::filesystem::path& filePath, cons usize bpe = bytesPerElement(metadata.dataType); if(bpe == 0) { - return MakeErrorResult(k_ErrorUnsupportedFormat, - fmt::format("Unsupported data type for TIFF writing to '{}'. Supported: uint8, uint16, float32.", filePath.string())); + return MakeErrorResult(k_ErrorUnsupportedFormat, fmt::format("Unsupported data type for TIFF writing to '{}'. Supported: uint8, uint16, float32.", filePath.string())); } usize expectedSize = metadata.width * metadata.height * metadata.numComponents * bpe; if(buffer.size() != expectedSize) { - return MakeErrorResult(k_ErrorBufferSizeMismatch, - fmt::format("Buffer size {} does not match expected size {} for TIFF write to '{}'", buffer.size(), expectedSize, filePath.string())); + return MakeErrorResult(k_ErrorBufferSizeMismatch, fmt::format("Buffer size {} does not match expected size {} for TIFF write to '{}'", buffer.size(), expectedSize, filePath.string())); } std::string pathStr = filePath.string(); TiffHandleGuard tiffGuard(TIFFOpen(pathStr.c_str(), "w")); if(tiffGuard.get() == nullptr) { - return MakeErrorResult(k_ErrorOpenFailed, - fmt::format("Failed to open TIFF file for writing '{}': {}", pathStr, s_TiffErrorMessage.empty() ? "unknown error" : s_TiffErrorMessage)); + return MakeErrorResult(k_ErrorOpenFailed, fmt::format("Failed to open TIFF file for writing '{}': {}", pathStr, s_TiffErrorMessage.empty() ? "unknown error" : s_TiffErrorMessage)); } TIFF* tiff = tiffGuard.get(); @@ -387,8 +377,7 @@ Result<> TiffImageIO::writePixelData(const std::filesystem::path& filePath, cons TIFFSetField(tiff, TIFFTAG_SAMPLEFORMAT, SAMPLEFORMAT_IEEEFP); break; default: - return MakeErrorResult(k_ErrorUnsupportedFormat, - fmt::format("Unsupported data type for TIFF writing to '{}'. Supported: uint8, uint16, float32.", pathStr)); + return MakeErrorResult(k_ErrorUnsupportedFormat, fmt::format("Unsupported data type for TIFF writing to '{}'. Supported: uint8, uint16, float32.", pathStr)); } // Set photometric interpretation @@ -428,8 +417,7 @@ Result<> TiffImageIO::writePixelData(const std::filesystem::path& filePath, cons // TIFFWriteScanline takes a non-const void* but does not modify the data if(TIFFWriteScanline(tiff, const_cast(rowData), row) < 0) { - return MakeErrorResult(k_ErrorWriteFailed, - fmt::format("Failed to write scanline {} to TIFF '{}': {}", row, pathStr, s_TiffErrorMessage.empty() ? "unknown error" : s_TiffErrorMessage)); + return MakeErrorResult(k_ErrorWriteFailed, fmt::format("Failed to write scanline {} to TIFF '{}': {}", row, pathStr, s_TiffErrorMessage.empty() ? "unknown error" : s_TiffErrorMessage)); } } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 4aa0e26ce4..32dac83647 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -99,9 +99,8 @@ if(EXISTS "${DREAM3D_DATA_DIR}" AND SIMPLNX_DOWNLOAD_TEST_FILES) file(MAKE_DIRECTORY "${DREAM3D_DATA_DIR}/TestFiles/") endif() download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME simpl_json_exemplars.tar.gz SHA512 550a1ad3c9c31b1093d961fdda2057d63a48f08c04d2831c70c1ec65d499f39b354a5a569903ff68e9b703ef45c23faba340aa0c61f9a927fbc845c15b87d05e ) - download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME import_image_stack_test.tar.gz SHA512 bec92d655d0d96928f616612d46b52cd51ffa5dbc1d16a64bb79040bbdd3c50f8193350771b177bb64aa68fc0ff7e459745265818c1ea34eb27431426ff15083) - download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME import_image_stack_test_v2.tar.gz SHA512 b3600c072ecbdb27ed3ed7298dac88708aa94d1eed21e6b0581b772311d1e3c2c6693713026ae512c86729079b41c629067fb1a7df75697d08dbdf2dfad0f553) - download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME itk_image_reader_test.tar.gz SHA512 15c42c913df8e0aebfc4196ea96ed5fdc2fab71daacbaf27287d6aee08483acb37f2f38d88af6c549084bae892b07c7aca8537b2511920d18248c8bc18aab92f) + download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME itk_image_reader_test_v3.tar.gz SHA512 e583911e69958f5a270f72f22a07527af313c1e3870923f1c95f1f55bb7c242d9e99daeed7100bfa1f5b4af37c952cf98abe0eb2486c1ec5248470b0ca2dcb85) + download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME import_image_stack_test_v3.tar.gz SHA512 e9305ac5c3592129cba937bd426c3d987a020a05229cf4519d826bcc538d054b4a173d613ed3e72b7646a523f2f3874c7dab39404cc0ddabffa653cba845abd5) endif() add_custom_target(Copy_simplnx_Test_Data ALL diff --git a/vcpkg-configuration.json b/vcpkg-configuration.json index 5cb83bedfb..c7f622fb03 100644 --- a/vcpkg-configuration.json +++ b/vcpkg-configuration.json @@ -8,7 +8,7 @@ { "kind": "git", "repository": "https://github.com/imikejackson/simplnx-registry", - "baseline": "0492974537e4cabdd6e52baeb918fd7bc9621e4c", + "baseline": "89eeb9bd56b09266e5350a94a91aa7ef48ed0253", "packages": [ "benchmark", "blosc", diff --git a/vcpkg.json b/vcpkg.json index 9eb3a07643..a8576133a1 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -83,7 +83,7 @@ "dependencies": [ { "name": "ebsdlib", - "version>=": "2.3.0" + "version>=": "2.4.0" } ] },