From 68703bfc7c96bd10e1c653fc64124d57c6852b52 Mon Sep 17 00:00:00 2001 From: Nathan Young Date: Wed, 16 Oct 2024 19:39:48 -0400 Subject: [PATCH] ENH: ITKImportImageStack Z Axis Resampling and Improvements (#1104) * Add an option to resample along z axis on image stack import * complete functional filter with test case * swap grayscale and resample execution order --- .../docs/ITKImportImageStackFilter.md | 11 +- .../Filters/ITKImportImageStackFilter.cpp | 190 ++++++++++++------ .../Filters/ITKImportImageStackFilter.hpp | 3 +- .../test/ITKImportImageStackTest.cpp | 72 ++++++- .../Filters/ResampleImageGeomFilter.cpp | 1 - 5 files changed, 215 insertions(+), 62 deletions(-) diff --git a/src/Plugins/ITKImageProcessing/docs/ITKImportImageStackFilter.md b/src/Plugins/ITKImageProcessing/docs/ITKImportImageStackFilter.md index 2dca11cc06..78470b53cb 100644 --- a/src/Plugins/ITKImageProcessing/docs/ITKImportImageStackFilter.md +++ b/src/Plugins/ITKImageProcessing/docs/ITKImportImageStackFilter.md @@ -10,7 +10,7 @@ ITKImageProcessing (ITKImageProcessing) Read in a stack of 2D images and stack the images into a 3D Volume using the ITK library. Supports most common scalar pixel types and the many file formats supported by ITK. The filter will create a new Image Geometry. The user can specify a value for the origin and the spacing if the defaults are not appropriate. The default value for the origin will be at (0, 0, 0) and the default spacing value will be (1.0, 1.0, 1.0). If the user needs to have the create Image Geometry located in a different location in the global reference frame, the user can change the default origin value. The "origin" of the image is at a normal Cartesian style origin. -The user can decide to scale the images as they are being read in by turning on the Scale Images option, and setting a scale value. A scale value of 0.1 resamples the images in the stack to one-tenth the number of pixels, a scale value of 2 resamples the images in the stack to double the number of pixels. The default scale value is 1. +The user can decide to scale the images as they are being read in by turning on the Scale Images option, and setting a scale value. A scale value of 10.0 resamples the images in the stack to one-tenth the number of pixels, a scale value of 200.0 resamples the images in the stack to double the number of pixels. The default scale value is 100.0. ## Image Operations @@ -26,6 +26,15 @@ operations can be seen in Figures 1, 2 and 3 % Auto generated parameter table will be inserted here +## Note on Resampling + +The optional resampling parameter has two options that affect the output image and size of the resulting geometry. + +- Scaling Factor (1) - This is the scaling option that previously existed with the filter. It functions by providing a float value that becomes a XYZ scaling factor vector that is applied to each image before it is inserted into the final geometry. This means that the number of pixels in the resulting output image will be resampled to `{X * (ScalingFactor / 100.0), Y * (ScalingFactor / 100.0), Number of Images In Stack} (XYZ)`. This means that a value of 100 (Like 100%) will *NOT* perform any resampling. A value of 50 will produce a final output image that has half as many pixels along the X and Y Axis. A value of 200 will have twice as many voxels along the X and Y Axis. +- Exact XY Dimensions (2) - This is provided to allow for precision resampling along the Z Axis. The number of pixels in the resulting output image will be resampled to `{User Supplied X, User Supplied Y, Number of Images In Stack} (XYZ)`. + +Both options are different ways to parameterize the resampling functionality. The main difference should be that `Scaling Factor (1)` is implicity uniform in its resampling across the X and Y dimensions, but the same is not true for `Exact XY Dimensions (2)`. + ## Example Pipelines - (08) Image Initial Visualization diff --git a/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImportImageStackFilter.cpp b/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImportImageStackFilter.cpp index decf92dda6..209badd539 100644 --- a/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImportImageStackFilter.cpp +++ b/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImportImageStackFilter.cpp @@ -36,6 +36,14 @@ const ChoicesParameter::ValueType k_NoImageTransform = 0; const ChoicesParameter::ValueType k_FlipAboutXAxis = 1; const ChoicesParameter::ValueType k_FlipAboutYAxis = 2; +inline constexpr nx::core::StringLiteral k_NoResamplingMode = "Do Not Resample (0)"; +inline constexpr nx::core::StringLiteral k_ScalingMode = "Scaling (1)"; +inline 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); @@ -134,8 +142,9 @@ namespace cxITKImportImageStackFilter { template Result<> ReadImageStack(DataStructure& dataStructure, const DataPath& imageGeomPath, const std::string& cellDataName, const std::string& imageArrayName, const std::vector& files, - ChoicesParameter::ValueType transformType, bool convertToGrayscale, const VectorFloat32Parameter::ValueType& luminosityValues, bool resample, float32 scalingFactor, - bool changeDataType, ChoicesParameter::ValueType destType, const IFilter::MessageHandler& messageHandler, const std::atomic_bool& shouldCancel) + ChoicesParameter::ValueType transformType, bool convertToGrayscale, const VectorFloat32Parameter::ValueType& luminosityValues, ChoicesParameter::ValueType resample, + float32 scalingFactor, const VectorUInt64Parameter::ValueType& exactDims, bool changeDataType, ChoicesParameter::ValueType destType, + const IFilter::MessageHandler& messageHandler, const std::atomic_bool& shouldCancel) { auto& imageGeom = dataStructure.getDataRefAs(imageGeomPath); DataPath imageDataPath = imageGeomPath.createChildPath(cellDataName).createChildPath(imageArrayName); @@ -150,7 +159,7 @@ Result<> ReadImageStack(DataStructure& dataStructure, const DataPath& imageGeomP auto* filterListPtr = Application::Instance()->getFilterList(); - if(convertToGrayscale && !filterListPtr->containsPlugin(k_SimplnxCorePluginId)) + if((convertToGrayscale || resample != k_NoResampleModeIndex) && !filterListPtr->containsPlugin(k_SimplnxCorePluginId)) { return MakeErrorResult(-18542, "SimplnxCore was not instantiated in this instance, so color to grayscale is not a valid option."); } @@ -184,11 +193,58 @@ Result<> ReadImageStack(DataStructure& dataStructure, const DataPath& imageGeomP } } + // ======================= Resample Image Geometry Section =================== + switch(resample) + { + case k_NoResampleModeIndex: { + break; + } + case k_ScalingModeIndex: { + if(scalingFactor == 100.0f) + { + break; + } + + Arguments resampleImageGeomArgs; + resampleImageGeomArgs.insertOrAssign("input_image_geometry_path", std::make_any(imageGeomPath)); + resampleImageGeomArgs.insertOrAssign("remove_original_geometry", std::make_any(true)); + + resampleImageGeomArgs.insertOrAssign("resampling_mode_index", std::make_any(1)); + resampleImageGeomArgs.insertOrAssign("scaling", std::make_any(std::vector{scalingFactor, scalingFactor, 100.0f})); + + // Run resample image geometry filter and process results and messages + auto result = resampleImageGeomFilter->execute(importedDataStructure, resampleImageGeomArgs).result; + if(result.invalid()) + { + return result; + } + break; + } + case k_ExactDimensionsModeIndex: { + Arguments resampleImageGeomArgs; + resampleImageGeomArgs.insertOrAssign("input_image_geometry_path", std::make_any(imageGeomPath)); + resampleImageGeomArgs.insertOrAssign("remove_original_geometry", std::make_any(true)); + + resampleImageGeomArgs.insertOrAssign("resampling_mode_index", std::make_any(2)); + resampleImageGeomArgs.insertOrAssign("exact_dimensions", std::make_any(std::vector{exactDims[0], exactDims[1], 1})); + + // Run resample image geometry filter and process results and messages + auto result = resampleImageGeomFilter->execute(importedDataStructure, resampleImageGeomArgs).result; + if(result.invalid()) + { + return result; + } + break; + } + default: { + break; + } + } + // ======================= Convert to GrayScale Section =================== bool validInputForGrayScaleConversion = importedDataStructure.getDataRefAs(imageDataPath).getDataType() == DataType::uint8; if(convertToGrayscale && validInputForGrayScaleConversion && nullptr != grayScaleFilter.get()) { - // This same filter was used to preflight so as long as nothing changes on disk this really should work.... Arguments colorToGrayscaleArgs; colorToGrayscaleArgs.insertOrAssign("conversion_algorithm", std::make_any(0)); @@ -227,38 +283,18 @@ Result<> ReadImageStack(DataStructure& dataStructure, const DataPath& imageGeomP -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.", imageDataPath.getTargetName())}); } - // ======================= Resample Image Geometry Section =================== - if(resample && scalingFactor != 1.0f) - { - auto scaling = scalingFactor * 100; - const auto& importedImageGeom = importedDataStructure.getDataRefAs(imageGeomPath); - auto spacing = importedImageGeom.getSpacing() / scalingFactor; - - Arguments resampleImageGeomArgs; - resampleImageGeomArgs.insertOrAssign("input_image_geometry_path", std::make_any(imageGeomPath)); - resampleImageGeomArgs.insertOrAssign("resampling_mode_index", std::make_any(1)); - resampleImageGeomArgs.insertOrAssign("scaling", std::make_any(std::vector{scaling, scaling, scaling})); - resampleImageGeomArgs.insertOrAssign("spacing", std::make_any(spacing.toContainer>())); - - // Run resample image geometry filter and process results and messages - auto result = resampleImageGeomFilter->execute(importedDataStructure, resampleImageGeomArgs).result; - if(result.invalid()) - { - return result; - } - } - // Check the ImageGeometry of the imported Image matches the destination const auto& importedImageGeom = importedDataStructure.getDataRefAs(imageGeomPath); SizeVec3 importedDims = importedImageGeom.getDimensions(); if(dims[0] != importedDims[0] || dims[1] != importedDims[1]) { - return MakeErrorResult(-64510, fmt::format("Slice {} image dimensions are different than the first slice.\n First Slice Dims are: {} x {}\n Current Slice Dims are:{} x {}\n", slice, - importedDims[0], importedDims[1], dims[0], dims[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, + dims[0], dims[1], importedDims[0], importedDims[1])); } // Compute the Tuple Index we are at: const usize tupleIndex = (slice * dims[0] * dims[1]); + // get the current Slice data... auto& tempData = importedDataStructure.getDataRefAs>(imageDataPath); auto& tempDataStore = tempData.getDataStoreRef(); @@ -273,9 +309,10 @@ Result<> ReadImageStack(DataStructure& dataStructure, const DataPath& imageGeomP } // Copy that into the output array... - if(outputDataStore.copyFrom(tupleIndex, tempDataStore, 0, tuplesPerSlice).invalid()) + auto result = outputDataStore.copyFrom(tupleIndex, tempDataStore, 0, tuplesPerSlice); + if(result.invalid()) { - return MakeErrorResult(-64511, fmt::format("Error copying source image data into destination array.\n Slice:{}\n TupleIndex:{}\n MaxTupleIndex:{}", slice, tupleIndex, outputData.getSize())); + return result; } slice++; @@ -337,9 +374,18 @@ Parameters ITKImportImageStackFilter::parameters() const 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_ScaleImages_Key, "Scale Images", "Determines whether or not to scale each image as it is imported into the stack.", false)); - params.insert(std::make_unique(k_Scaling_Key, "Scaling", - "The scaling of the 3D volume. For example, 0.1 is one-tenth the original number of pixels. 2.0 is double the number of pixels.", 1.0)); + + 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", + "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.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, @@ -355,7 +401,8 @@ Parameters ITKImportImageStackFilter::parameters() const 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_ScaleImages_Key, k_Scaling_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); return params; @@ -364,7 +411,16 @@ Parameters ITKImportImageStackFilter::parameters() const //------------------------------------------------------------------------------ IFilter::VersionType ITKImportImageStackFilter::parametersVersion() const { - return 1; + return 2; + + // Version 1 -> 2 + // Change 1: + // Replaced - k_ScaleImages_Key = "scale_images" -> k_ResampleImagesChoice_Key = "resample_images_index"; + // Solution - `k_ResampleImagesChoice_Key Value` = static_cast(`k_ScaleImages_Key Value`); + // + // Change 2: + // Modified Existing - Scaling value to be in feature parity with ResampleImageGeomFilter's Scaling option (k_Scaling_Key = "scaling") + // Solution - `New k_Scaling_Key Value` = `Old k_Scaling_Key Value` * 100.0f; } //------------------------------------------------------------------------------ @@ -386,14 +442,14 @@ IFilter::PreflightResult ITKImportImageStackFilter::preflightImpl(const DataStru auto imageTransformValue = filterArgs.value(k_ImageTransformChoice_Key); auto pConvertToGrayScaleValue = filterArgs.value(k_ConvertToGrayScale_Key); auto pColorWeightsValue = filterArgs.value(k_ColorWeights_Key); - auto pScaleImagesValue = filterArgs.value(k_ScaleImages_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); - PreflightResult preflightResult; - nx::core::Result resultOutputActions = {}; + nx::core::Result resultOutputActions; std::vector preflightUpdatedValues; const DataPath imageDataPath = imageGeomPath.createChildPath(cellDataName).createChildPath(pImageDataArrayNameValue); @@ -414,7 +470,7 @@ IFilter::PreflightResult ITKImportImageStackFilter::preflightImpl(const DataStru return {MakeErrorResult(-1, "GeneratedFileList must not be empty")}; } - // Create a subfilter to read each image, although for preflight we are going to read the first image in the + // 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(ITKImageReaderFilter::k_ImageGeometryPath_Key, std::make_any(imageGeomPath)); @@ -452,13 +508,34 @@ IFilter::PreflightResult ITKImportImageStackFilter::preflightImpl(const DataStru auto dims = createImageGeomActionPtr->dims(); dims.back() = files.size(); - if(pScaleImagesValue) + switch(pResampleImagesChoiceValue) { + case k_NoResampleModeIndex: + break; + case k_ScalingModeIndex: { + if(pScalingValue < 1.0f) + { + // seemingly arbitrary numeric limit, only included for compatibility with ResampleImageGeomFilter + return MakePreflightErrorResult(-23508, fmt::format("Scaling value must be greater than or equal to 1.0f. Received: {}", pScalingValue)); + } + // Update the dimensions according to the scaling value - std::transform(dims.begin(), dims.end() - 1, dims.begin(), [pScalingValue](usize& elem) { return static_cast(static_cast(elem) * pScalingValue); }); + std::transform(dims.begin(), dims.end() - 1, dims.begin(), [pScalingValue](usize& elem) { return static_cast(static_cast(elem) * (pScalingValue / 100.0f)); }); // Update the spacing according to the scaling value - std::transform(spacing.begin(), spacing.end() - 1, spacing.begin(), [pScalingValue](auto& elem) { return elem / pScalingValue; }); + std::transform(spacing.begin(), spacing.end() - 1, spacing.begin(), [pScalingValue](auto& elem) { return elem / (pScalingValue / 100.0f); }); + break; + } + case k_ExactDimensionsModeIndex: { + spacing[0] = spacing[0] * static_cast(dims[0]) / static_cast(pExactXYDimsValue[0]); + spacing[1] = spacing[1] * static_cast(dims[1]) / static_cast(pExactXYDimsValue[1]); + + dims[0] = static_cast(pExactXYDimsValue[0]); + dims[1] = static_cast(pExactXYDimsValue[1]); + break; + } + default: + return MakePreflightErrorResult(-23507, fmt::format("Invalid Resampling Choice Type. Expected: [0-2] | Received: {}", pResampleImagesChoiceValue)); } // Z Y X @@ -516,8 +593,9 @@ Result<> ITKImportImageStackFilter::executeImpl(DataStructure& dataStructure, co auto imageTransformValue = filterArgs.value(k_ImageTransformChoice_Key); auto convertToGrayScaleValue = filterArgs.value(k_ConvertToGrayScale_Key); auto colorWeightsValue = filterArgs.value(k_ColorWeights_Key); - auto pScaleImagesValue = filterArgs.value(k_ScaleImages_Key); - auto pScalingValue = filterArgs.value(k_Scaling_Key); + auto resampleImageChoice = filterArgs.value(k_ResampleImagesChoice_Key); + auto scalingFactor = filterArgs.value(k_Scaling_Key); + auto exactXYDims = filterArgs.value(k_ExactXYDimensions_Key); auto changeDataType = filterArgs.value(k_ChangeDataType_Key); auto destType = filterArgs.value(k_ImageDataType_Key); @@ -548,17 +626,17 @@ Result<> ITKImportImageStackFilter::executeImpl(DataStructure& dataStructure, co { case DataType::uint8: { readResult = cxITKImportImageStackFilter::ReadImageStack(dataStructure, imageGeomPath, cellDataName, imageDataName, files, imageTransformValue, convertToGrayScaleValue, colorWeightsValue, - pScaleImagesValue, pScalingValue, changeDataType, destType, messageHandler, shouldCancel); + resampleImageChoice, scalingFactor, exactXYDims, changeDataType, destType, messageHandler, shouldCancel); break; } case DataType::uint16: { readResult = cxITKImportImageStackFilter::ReadImageStack(dataStructure, imageGeomPath, cellDataName, imageDataName, files, imageTransformValue, convertToGrayScaleValue, - colorWeightsValue, pScaleImagesValue, pScalingValue, changeDataType, destType, messageHandler, shouldCancel); + colorWeightsValue, resampleImageChoice, scalingFactor, exactXYDims, changeDataType, destType, messageHandler, shouldCancel); break; } case DataType::uint32: { readResult = cxITKImportImageStackFilter::ReadImageStack(dataStructure, imageGeomPath, cellDataName, imageDataName, files, imageTransformValue, convertToGrayScaleValue, - colorWeightsValue, pScaleImagesValue, pScalingValue, changeDataType, destType, messageHandler, shouldCancel); + colorWeightsValue, resampleImageChoice, scalingFactor, exactXYDims, changeDataType, destType, messageHandler, shouldCancel); break; } default: { @@ -572,52 +650,52 @@ Result<> ITKImportImageStackFilter::executeImpl(DataStructure& dataStructure, co { case NumericType::uint8: { readResult = cxITKImportImageStackFilter::ReadImageStack(dataStructure, imageGeomPath, cellDataName, imageDataName, files, imageTransformValue, convertToGrayScaleValue, colorWeightsValue, - pScaleImagesValue, pScalingValue, changeDataType, destType, messageHandler, shouldCancel); + resampleImageChoice, scalingFactor, exactXYDims, changeDataType, destType, messageHandler, shouldCancel); break; } case NumericType::int8: { readResult = cxITKImportImageStackFilter::ReadImageStack(dataStructure, imageGeomPath, cellDataName, imageDataName, files, imageTransformValue, convertToGrayScaleValue, colorWeightsValue, - pScaleImagesValue, pScalingValue, changeDataType, destType, messageHandler, shouldCancel); + resampleImageChoice, scalingFactor, exactXYDims, changeDataType, destType, messageHandler, shouldCancel); break; } case NumericType::uint16: { readResult = cxITKImportImageStackFilter::ReadImageStack(dataStructure, imageGeomPath, cellDataName, imageDataName, files, imageTransformValue, convertToGrayScaleValue, - colorWeightsValue, pScaleImagesValue, pScalingValue, changeDataType, destType, messageHandler, shouldCancel); + colorWeightsValue, resampleImageChoice, scalingFactor, exactXYDims, changeDataType, destType, messageHandler, shouldCancel); break; } case NumericType::int16: { readResult = cxITKImportImageStackFilter::ReadImageStack(dataStructure, imageGeomPath, cellDataName, imageDataName, files, imageTransformValue, convertToGrayScaleValue, colorWeightsValue, - pScaleImagesValue, pScalingValue, changeDataType, destType, messageHandler, shouldCancel); + resampleImageChoice, scalingFactor, exactXYDims, changeDataType, destType, messageHandler, shouldCancel); break; } case NumericType::uint32: { readResult = cxITKImportImageStackFilter::ReadImageStack(dataStructure, imageGeomPath, cellDataName, imageDataName, files, imageTransformValue, convertToGrayScaleValue, - colorWeightsValue, pScaleImagesValue, pScalingValue, changeDataType, destType, messageHandler, shouldCancel); + colorWeightsValue, resampleImageChoice, scalingFactor, exactXYDims, changeDataType, destType, messageHandler, shouldCancel); break; } case NumericType::int32: { readResult = cxITKImportImageStackFilter::ReadImageStack(dataStructure, imageGeomPath, cellDataName, imageDataName, files, imageTransformValue, convertToGrayScaleValue, colorWeightsValue, - pScaleImagesValue, pScalingValue, changeDataType, destType, messageHandler, shouldCancel); + resampleImageChoice, scalingFactor, exactXYDims, changeDataType, destType, messageHandler, shouldCancel); break; } case NumericType::uint64: { readResult = cxITKImportImageStackFilter::ReadImageStack(dataStructure, imageGeomPath, cellDataName, imageDataName, files, imageTransformValue, convertToGrayScaleValue, - colorWeightsValue, pScaleImagesValue, pScalingValue, changeDataType, destType, messageHandler, shouldCancel); + colorWeightsValue, resampleImageChoice, scalingFactor, exactXYDims, changeDataType, destType, messageHandler, shouldCancel); break; } case NumericType::int64: { readResult = cxITKImportImageStackFilter::ReadImageStack(dataStructure, imageGeomPath, cellDataName, imageDataName, files, imageTransformValue, convertToGrayScaleValue, colorWeightsValue, - pScaleImagesValue, pScalingValue, changeDataType, destType, messageHandler, shouldCancel); + resampleImageChoice, scalingFactor, exactXYDims, changeDataType, destType, messageHandler, shouldCancel); break; } case NumericType::float32: { readResult = cxITKImportImageStackFilter::ReadImageStack(dataStructure, imageGeomPath, cellDataName, imageDataName, files, imageTransformValue, convertToGrayScaleValue, - colorWeightsValue, pScaleImagesValue, pScalingValue, changeDataType, destType, messageHandler, shouldCancel); + colorWeightsValue, resampleImageChoice, scalingFactor, exactXYDims, changeDataType, destType, messageHandler, shouldCancel); break; } case NumericType::float64: { readResult = cxITKImportImageStackFilter::ReadImageStack(dataStructure, imageGeomPath, cellDataName, imageDataName, files, imageTransformValue, convertToGrayScaleValue, - colorWeightsValue, pScaleImagesValue, pScalingValue, changeDataType, destType, messageHandler, shouldCancel); + colorWeightsValue, resampleImageChoice, scalingFactor, exactXYDims, changeDataType, destType, messageHandler, shouldCancel); break; } default: { diff --git a/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImportImageStackFilter.hpp b/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImportImageStackFilter.hpp index 2552044b76..6cbd61c810 100644 --- a/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImportImageStackFilter.hpp +++ b/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImportImageStackFilter.hpp @@ -33,8 +33,9 @@ class ITKIMAGEPROCESSING_EXPORT ITKImportImageStackFilter : public IFilter static inline constexpr StringLiteral k_ImageTransformChoice_Key = "image_transform_index"; static inline constexpr StringLiteral k_ConvertToGrayScale_Key = "convert_to_gray_scale"; static inline constexpr StringLiteral k_ColorWeights_Key = "color_weights"; - static inline constexpr StringLiteral k_ScaleImages_Key = "scale_images"; + static inline constexpr StringLiteral k_ResampleImagesChoice_Key = "resample_images_index"; static inline constexpr StringLiteral k_Scaling_Key = "scaling"; + static inline constexpr StringLiteral k_ExactXYDimensions_Key = "exact_xy_dimensions"; static inline constexpr StringLiteral k_ChangeDataType_Key = "change_image_data_type"; static inline constexpr StringLiteral k_ImageDataType_Key = "image_data_type_index"; /** diff --git a/src/Plugins/ITKImageProcessing/test/ITKImportImageStackTest.cpp b/src/Plugins/ITKImageProcessing/test/ITKImportImageStackTest.cpp index 66cf8f6a68..1eba2a0a82 100644 --- a/src/Plugins/ITKImageProcessing/test/ITKImportImageStackTest.cpp +++ b/src/Plugins/ITKImageProcessing/test/ITKImportImageStackTest.cpp @@ -514,7 +514,7 @@ TEST_CASE("ITKImageProcessing::ITKImportImageStackFilter: RGB", "[ITKImageProces REQUIRE(md5Hash == "8b0b0393d6779156c88544bc4d75d3fc"); } -TEST_CASE("ITKImageProcessing::ITKImportImageStackFilter: Resampled", "[ITKImageProcessing][ITKImportImageStackFilter]") +TEST_CASE("ITKImageProcessing::ITKImportImageStackFilter: Resampled Scaled", "[ITKImageProcessing][ITKImportImageStackFilter]") { auto app = Application::GetOrCreateInstance(); app->loadPlugins(unit_test::k_BuildDir.view()); @@ -542,8 +542,8 @@ TEST_CASE("ITKImageProcessing::ITKImportImageStackFilter: Resampled", "[ITKImage args.insertOrAssign(ITKImportImageStackFilter::k_Spacing_Key, std::make_any>(spacing)); args.insertOrAssign(ITKImportImageStackFilter::k_ImageGeometryPath_Key, std::make_any(k_ImageGeomPath)); args.insertOrAssign(ITKImportImageStackFilter::k_ConvertToGrayScale_Key, std::make_any(false)); - args.insertOrAssign(ITKImportImageStackFilter::k_ScaleImages_Key, std::make_any(true)); - args.insertOrAssign(ITKImportImageStackFilter::k_Scaling_Key, std::make_any(0.5)); + args.insertOrAssign(ITKImportImageStackFilter::k_ResampleImagesChoice_Key, std::make_any(1)); + args.insertOrAssign(ITKImportImageStackFilter::k_Scaling_Key, std::make_any(50.0f)); auto preflightResult = filter.preflight(dataStructure, args); SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions) @@ -579,3 +579,69 @@ TEST_CASE("ITKImageProcessing::ITKImportImageStackFilter: Resampled", "[ITKImage const std::string md5Hash = ITKTestBase::ComputeMd5Hash(dataStructure, k_ImageDataPath); REQUIRE(md5Hash == "5969f0ae7507bfae14de3cb470d53e60"); } + +TEST_CASE("ITKImageProcessing::ITKImportImageStackFilter: Resampled Exact Dims", "[ITKImageProcessing][ITKImportImageStackFilter]") +{ + auto app = Application::GetOrCreateInstance(); + app->loadPlugins(unit_test::k_BuildDir.view()); + + ITKImportImageStackFilter filter; + DataStructure dataStructure; + Arguments args; + + GeneratedFileListParameter::ValueType fileListInfo; + fileListInfo.inputPath = k_ImageStackDir; + fileListInfo.startIndex = 0; + fileListInfo.endIndex = 2; + fileListInfo.incrementIndex = 1; + fileListInfo.fileExtension = ".png"; + fileListInfo.filePrefix = "rgb_"; + fileListInfo.fileSuffix = ""; + fileListInfo.paddingDigits = 1; + 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(ITKImportImageStackFilter::k_InputFileListInfo_Key, std::make_any(fileListInfo)); + args.insertOrAssign(ITKImportImageStackFilter::k_Origin_Key, std::make_any>(origin)); + args.insertOrAssign(ITKImportImageStackFilter::k_Spacing_Key, std::make_any>(spacing)); + args.insertOrAssign(ITKImportImageStackFilter::k_ImageGeometryPath_Key, std::make_any(k_ImageGeomPath)); + args.insertOrAssign(ITKImportImageStackFilter::k_ConvertToGrayScale_Key, std::make_any(false)); + args.insertOrAssign(ITKImportImageStackFilter::k_ResampleImagesChoice_Key, std::make_any(2)); + args.insertOrAssign(ITKImportImageStackFilter::k_ExactXYDimensions_Key, std::make_any>(std::vector{100, 100})); + + 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 = {100, 100, 3}; + std::vector new_spacing = {1.572f, 0.78f, 0.9f}; + + 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(new_spacing[0])); + REQUIRE(imageSpacing[1] == Approx(new_spacing[1])); + REQUIRE(imageSpacing[2] == Approx(new_spacing[2])); + + const auto* imageDataPtr = dataStructure.getDataAs(k_ImageDataPath); + REQUIRE(imageDataPtr != nullptr); + + const std::string md5Hash = ITKTestBase::ComputeMd5Hash(dataStructure, k_ImageDataPath); + REQUIRE(md5Hash == "e1e892c7e11eb55a57919053eee66f22"); +} diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ResampleImageGeomFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ResampleImageGeomFilter.cpp index 2bddf945b1..8140654290 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ResampleImageGeomFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ResampleImageGeomFilter.cpp @@ -36,7 +36,6 @@ const ChoicesParameter::Choices k_Choices = {k_SpacingMode, k_ScalingMode, k_Exa const ChoicesParameter::ValueType k_SpacingModeIndex = 0; const ChoicesParameter::ValueType k_ScalingModeIndex = 1; const ChoicesParameter::ValueType k_ExactDimensionsModeIndex = 2; - } // namespace namespace nx::core