Skip to content

Commit

Permalink
complete functional filter with test case
Browse files Browse the repository at this point in the history
  • Loading branch information
nyoungbq committed Oct 11, 2024
1 parent ec4465d commit 37128ea
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -30,7 +30,7 @@ operations can be seen in Figures 1, 2 and 3

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, Y * ScalingFactor, Number of Images In Stack} (XYZ)`.
- 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)`.
- 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)`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,11 @@ const ChoicesParameter::ValueType k_NoImageTransform = 0;
const ChoicesParameter::ValueType k_FlipAboutXAxis = 1;
const ChoicesParameter::ValueType k_FlipAboutYAxis = 2;

inline constexpr nx::core::StringLiteral k_NoScalingMode = "Do Not Rescale (0)";
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_NoScalingMode, k_ScalingMode, k_ExactDimensions};
const nx::core::ChoicesParameter::ValueType k_NoScalingModeIndex = 0;
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;

Expand Down Expand Up @@ -159,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.");
}
Expand Down Expand Up @@ -236,41 +236,65 @@ Result<> ReadImageStack(DataStructure& dataStructure, const DataPath& imageGeomP
}

// ======================= Resample Image Geometry Section ===================
if(resample != k_NoScalingModeIndex && scalingFactor != 1.0f)
switch(resample)
{
Arguments resampleImageGeomArgs;
resampleImageGeomArgs.insertOrAssign("input_image_geometry_path", std::make_any<DataPath>(imageGeomPath));
if(resample == k_ScalingModeIndex)
case k_NoResampleModeIndex: {
break;
}
case k_ScalingModeIndex: {
if(scalingFactor == 100.0f)
{
auto scaling = scalingFactor * 100;
resampleImageGeomArgs.insertOrAssign("resampling_mode_index", std::make_any<ChoicesParameter::ValueType>(1));
resampleImageGeomArgs.insertOrAssign("scaling", std::make_any<VectorFloat32Parameter::ValueType>(std::vector<float32>{scaling, scaling, scaling}));
break;
}
else if(resample == k_ExactDimensionsModeIndex)

Arguments resampleImageGeomArgs;
resampleImageGeomArgs.insertOrAssign("input_image_geometry_path", std::make_any<DataPath>(imageGeomPath));
resampleImageGeomArgs.insertOrAssign("remove_original_geometry", std::make_any<bool>(true));

resampleImageGeomArgs.insertOrAssign("resampling_mode_index", std::make_any<ChoicesParameter::ValueType>(1));
resampleImageGeomArgs.insertOrAssign("scaling", std::make_any<VectorFloat32Parameter::ValueType>(std::vector<float32>{scalingFactor, scalingFactor, 100.0f}));

// Run resample image geometry filter and process results and messages
auto result = resampleImageGeomFilter->execute(importedDataStructure, resampleImageGeomArgs).result;
if(result.invalid())
{
resampleImageGeomArgs.insertOrAssign("resampling_mode_index", std::make_any<ChoicesParameter::ValueType>(2));
resampleImageGeomArgs.insertOrAssign("exact_dimensions", std::make_any<VectorUInt64Parameter::ValueType>(std::vector<uint64>{exactDims[0], exactDims[1], 1}));
return result;
}
break;
}
case k_ExactDimensionsModeIndex: {
Arguments resampleImageGeomArgs;
resampleImageGeomArgs.insertOrAssign("input_image_geometry_path", std::make_any<DataPath>(imageGeomPath));
resampleImageGeomArgs.insertOrAssign("remove_original_geometry", std::make_any<bool>(true));

resampleImageGeomArgs.insertOrAssign("resampling_mode_index", std::make_any<ChoicesParameter::ValueType>(2));
resampleImageGeomArgs.insertOrAssign("exact_dimensions", std::make_any<VectorUInt64Parameter::ValueType>(std::vector<uint64>{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;
}
}

// Check the ImageGeometry of the imported Image matches the destination
const auto& importedImageGeom = importedDataStructure.getDataRefAs<ImageGeom>(imageGeomPath);
SizeVec3 importedDims = importedImageGeom.getDimensions();
if(resample != k_ExactDimensionsModeIndex && (dims[0] != importedDims[0] || dims[1] != importedDims[1]))
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<DataArray<T>>(imageDataPath);
auto& tempDataStore = tempData.getDataStoreRef();
Expand All @@ -285,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++;
Expand Down Expand Up @@ -352,9 +377,12 @@ Parameters ITKImportImageStackFilter::parameters() const

params.insertLinkableParameter(std::make_unique<ChoicesParameter>(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_NoScalingModeIndex, ::k_ResamplingChoices));
params.insert(std::make_unique<Float32Parameter>(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));
::k_NoResampleModeIndex, ::k_ResamplingChoices));
params.insert(std::make_unique<Float32Parameter>(
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<VectorUInt64Parameter>(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<uint64>{100, 100}, std::vector<std::string>({"X", "Y"})));
Expand Down Expand Up @@ -384,6 +412,15 @@ Parameters ITKImportImageStackFilter::parameters() const
IFilter::VersionType ITKImportImageStackFilter::parametersVersion() const
{
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<ChoicesParameter::ValueType>(`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;
}

//------------------------------------------------------------------------------
Expand Down Expand Up @@ -433,7 +470,7 @@ IFilter::PreflightResult ITKImportImageStackFilter::preflightImpl(const DataStru
return {MakeErrorResult<OutputActions>(-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<DataPath>(imageGeomPath));
Expand Down Expand Up @@ -473,14 +510,20 @@ IFilter::PreflightResult ITKImportImageStackFilter::preflightImpl(const DataStru

switch(pResampleImagesChoiceValue)
{
case k_NoScalingModeIndex:
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<usize>(static_cast<float32>(elem) * pScalingValue); });
std::transform(dims.begin(), dims.end() - 1, dims.begin(), [pScalingValue](usize& elem) { return static_cast<usize>(static_cast<float32>(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: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -543,7 +543,7 @@ TEST_CASE("ITKImageProcessing::ITKImportImageStackFilter: Resampled Scaled", "[I
args.insertOrAssign(ITKImportImageStackFilter::k_ImageGeometryPath_Key, std::make_any<DataPath>(k_ImageGeomPath));
args.insertOrAssign(ITKImportImageStackFilter::k_ConvertToGrayScale_Key, std::make_any<BoolParameter::ValueType>(false));
args.insertOrAssign(ITKImportImageStackFilter::k_ResampleImagesChoice_Key, std::make_any<ChoicesParameter::ValueType>(1));
args.insertOrAssign(ITKImportImageStackFilter::k_Scaling_Key, std::make_any<Float32Parameter::ValueType>(0.5));
args.insertOrAssign(ITKImportImageStackFilter::k_Scaling_Key, std::make_any<Float32Parameter::ValueType>(50.0f));

auto preflightResult = filter.preflight(dataStructure, args);
SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions)
Expand Down Expand Up @@ -609,7 +609,7 @@ TEST_CASE("ITKImageProcessing::ITKImportImageStackFilter: Resampled Exact Dims",
args.insertOrAssign(ITKImportImageStackFilter::k_ImageGeometryPath_Key, std::make_any<DataPath>(k_ImageGeomPath));
args.insertOrAssign(ITKImportImageStackFilter::k_ConvertToGrayScale_Key, std::make_any<BoolParameter::ValueType>(false));
args.insertOrAssign(ITKImportImageStackFilter::k_ResampleImagesChoice_Key, std::make_any<ChoicesParameter::ValueType>(2));
args.insertOrAssign(ITKImportImageStackFilter::k_ExactXYDimensions_Key, std::make_any<std::vector<uint64>>(std::vector<uint64>{100,100}));
args.insertOrAssign(ITKImportImageStackFilter::k_ExactXYDimensions_Key, std::make_any<std::vector<uint64>>(std::vector<uint64>{100, 100}));

auto preflightResult = filter.preflight(dataStructure, args);
SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions)
Expand All @@ -625,7 +625,7 @@ TEST_CASE("ITKImageProcessing::ITKImportImageStackFilter: Resampled Exact Dims",
FloatVec3 imageSpacing = imageGeomPtr->getSpacing();

std::array<usize, 3> dims = {100, 100, 3};
std::vector<float32> new_spacing = {0.6f, 0.4f, 0.9f};
std::vector<float32> new_spacing = {1.572f, 0.78f, 0.9f};

REQUIRE(imageDims[0] == dims[0]);
REQUIRE(imageDims[1] == dims[1]);
Expand All @@ -643,5 +643,5 @@ TEST_CASE("ITKImageProcessing::ITKImportImageStackFilter: Resampled Exact Dims",
REQUIRE(imageDataPtr != nullptr);

const std::string md5Hash = ITKTestBase::ComputeMd5Hash(dataStructure, k_ImageDataPath);
REQUIRE(md5Hash == "5969f0ae7507bfae14de3cb470d53e60");
REQUIRE(md5Hash == "e1e892c7e11eb55a57919053eee66f22");
}

0 comments on commit 37128ea

Please sign in to comment.