From 5f776069462717adcb6eb88c2a9ba7115bff95bd Mon Sep 17 00:00:00 2001 From: Jared Duffey Date: Tue, 28 Jul 2026 10:42:05 -0400 Subject: [PATCH 01/28] Removed unused variable Signed-off-by: Jared Duffey --- .../src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp b/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp index d4df693d3c..4274b02da2 100644 --- a/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp +++ b/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp @@ -402,8 +402,6 @@ Result<> ITKImageWriterFilter::executeImpl(DataStructure& dataStructure, const A auto totalDigits = filterArgs.value(k_TotalIndexDigits_Key); auto fillChar = filterArgs.value(k_LeadingDigitCharacter_Key); - const IDataArray* inputArray = dataStructure.getDataAs(imageArrayPath); - const auto& imageGeom = dataStructure.getDataRefAs(imageGeomPath); // Stored fastest to slowest i.e. X Y Z SizeVec3 dims = imageGeom.getDimensions(); From 2c470bf174b72d69921540ead594365910c7e3af Mon Sep 17 00:00:00 2001 From: Jared Duffey Date: Tue, 28 Jul 2026 10:42:05 -0400 Subject: [PATCH 02/28] Fixed wrong type in switch case Signed-off-by: Jared Duffey --- .../src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp b/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp index 4274b02da2..b553f735a0 100644 --- a/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp +++ b/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp @@ -207,7 +207,7 @@ void CopyTuple(usize index, usize axisA, usize dB, usize axisB, usize nComp, con break; } case DataType::uint32: { - CopyTupleTyped(currentData, sliceData, nComp, index, indexNew); + CopyTupleTyped(currentData, sliceData, nComp, index, indexNew); break; } case DataType::int64: { @@ -215,7 +215,7 @@ void CopyTuple(usize index, usize axisA, usize dB, usize axisB, usize nComp, con break; } case DataType::uint64: { - CopyTupleTyped(currentData, sliceData, nComp, index, indexNew); + CopyTupleTyped(currentData, sliceData, nComp, index, indexNew); break; } case DataType::float32: { From 58903ca72c74ed99fcb954dd7e17273815ac3fec Mon Sep 17 00:00:00 2001 From: Jared Duffey Date: Tue, 28 Jul 2026 10:42:06 -0400 Subject: [PATCH 03/28] Fixed fill char check Signed-off-by: Jared Duffey --- .../src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp b/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp index b553f735a0..8b651eae2a 100644 --- a/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp +++ b/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp @@ -355,9 +355,9 @@ IFilter::PreflightResult ITKImageWriterFilter::preflightImpl(const DataStructure StringUtilities::formatDimensions3D(imageGeom.getDimensions())))}; } - if(fillChar.size() > 1) + if(fillChar.size() != 1) { - return {MakeErrorResult(-25601, "The fill character should only be a single value.")}; + return {MakeErrorResult(-25601, fmt::format("The fill character must contain exactly one character; received {} characters.", fillChar.size()))}; } Result resultOutputActions; From c6b149ce81d66af2b1b9be8b33149ab11167557c Mon Sep 17 00:00:00 2001 From: Jared Duffey Date: Tue, 28 Jul 2026 10:42:06 -0400 Subject: [PATCH 04/28] Updated documention to be more specific on file output type/name Signed-off-by: Jared Duffey --- src/Plugins/ITKImageProcessing/docs/ITKImageWriterFilter.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Plugins/ITKImageProcessing/docs/ITKImageWriterFilter.md b/src/Plugins/ITKImageProcessing/docs/ITKImageWriterFilter.md index 3e388d7a4c..97a4c49430 100644 --- a/src/Plugins/ITKImageProcessing/docs/ITKImageWriterFilter.md +++ b/src/Plugins/ITKImageProcessing/docs/ITKImageWriterFilter.md @@ -6,7 +6,11 @@ ITKImageProcessing (ITKImageProcessing) ## Description -This **Filter** will save images based on an array that represents grayscale, RGB or ARGB color values. If the input array represents a 3D volume, the **Filter** will output a series of slices along one of the orthogonal axes. The options are to produce XY slices along the Z axis, XZ slices along the Y axis or YZ slices along the X axis. The user has the option to save in one of 3 standard image formats: TIF, BMP, or PNG. The output files will be numbered sequentially starting at zero (0) and ending at the total dimensions for the chosen axis. For example, if the Z axis has 117 dimensions, 117 XY image files will be produced and numbered 0 to 116. Unless the data is a single slice then only a single image will be produced using the name given in the Output File parameter. +This **Filter** will save images based on an array that represents grayscale, RGB or ARGB color values. If the input array represents a 3D volume, the **Filter** will output a series of slices along one of the orthogonal axes. The options are to produce XY slices along the Z axis, XZ slices along the Y axis or YZ slices along the X axis. + +The available output formats are determined by the installed ITK image I/O backends and the filename extension. TIFF, BMP, and PNG are common 2D choices; not every pixel type is supported by every format. + +For a series, the *Output File* stem is followed by an underscore and the slice index. *Index Offset*, *Total Number of Index Digits*, and *Fill Character* control the first index and its formatting. For example, with 117 Z cells, offset *0*, width *3*, and fill character `0`, the XY output files are named `slice_000.tif` through `slice_116.tif`. A single-slice output keeps the exact name supplied in *Output File*. An example of a **Filter** that produces color data that can be used as input to this **Filter** is the {ref}`Generate IFP Colors ` **Filter**, which will generate RGB values for each voxel in the volume. From c8eb879f890c0463e4117f183d7ce8e598c99766 Mon Sep 17 00:00:00 2001 From: Jared Duffey Date: Tue, 28 Jul 2026 10:42:06 -0400 Subject: [PATCH 05/28] VV: "Write Image (ITK)" fully V&V'ed Summary: - Found and fixed 2 bugs (fill character validation and uint32/uint64 wrong type in case) - Documented 1 deviation from DREAM3D 6.5.171 (D1 default slice-index padding) - Retired 0 tests (none) - Augmented existing tests with 4 new test cases including a templated Class 1 (Analytical) + Class 4 (Invariant) oracle - Added V&V source-tree deliverables (report, deviations) - Updated wording in docs Signed-off-by: Jared Duffey --- .../test/ITKImageWriterTest.cpp | 206 ++++++++++++++++++ .../vv/ITKImageWriterFilter.md | 130 +++++++++++ .../vv/deviations/ITKImageWriterFilter.md | 21 ++ 3 files changed, 357 insertions(+) create mode 100644 src/Plugins/ITKImageProcessing/vv/ITKImageWriterFilter.md create mode 100644 src/Plugins/ITKImageProcessing/vv/deviations/ITKImageWriterFilter.md diff --git a/src/Plugins/ITKImageProcessing/test/ITKImageWriterTest.cpp b/src/Plugins/ITKImageProcessing/test/ITKImageWriterTest.cpp index 6f2f92fc4d..491b318328 100644 --- a/src/Plugins/ITKImageProcessing/test/ITKImageWriterTest.cpp +++ b/src/Plugins/ITKImageProcessing/test/ITKImageWriterTest.cpp @@ -5,6 +5,8 @@ #include "ITKImageProcessing/ITKImageProcessing_test_dirs.hpp" #include "simplnx/Core/Application.hpp" +#include "simplnx/DataStructure/AttributeMatrix.hpp" +#include "simplnx/DataStructure/Geometry/ImageGeom.hpp" #include "simplnx/Parameters/ChoicesParameter.hpp" #include "simplnx/Parameters/FileSystemPathParameter.hpp" #include "simplnx/Parameters/GeneratedFileListParameter.hpp" @@ -18,6 +20,8 @@ #include #include +#include + namespace fs = std::filesystem; using namespace nx::core; @@ -77,8 +81,210 @@ void validateOutputFiles(size_t numImages, uint64 offset, const std::string& tem } } +template +void CompareImageToExpected(const fs::path& filePath, const std::array& expectedDimensions, const std::vector& expectedPixels) +{ + using ImageType = itk::Image; + auto reader = itk::ImageFileReader::New(); + reader->SetFileName(filePath.string()); + REQUIRE_NOTHROW(reader->Update()); + + const auto& image = *reader->GetOutput(); + const auto dimensions = image.GetLargestPossibleRegion().GetSize(); + REQUIRE(dimensions[0] == expectedDimensions[0]); + REQUIRE(dimensions[1] == expectedDimensions[1]); + + for(usize y = 0; y < expectedDimensions[1]; ++y) + { + for(usize x = 0; x < expectedDimensions[0]; ++x) + { + typename ImageType::IndexType index; + index[0] = static_cast(x); + index[1] = static_cast(y); + CHECK(image.GetPixel(index) == expectedPixels[(y * expectedDimensions[0]) + x]); + } + } +} + } // namespace +TEMPLATE_TEST_CASE("ITKImageProcessing::ITKImageWriterFilter: Analytical Pixel Order", "[ITKImageProcessing][ITKImageWriterFilter]", int8, uint8, int16, uint16, int32, uint32, int64, uint64, float32, + float64) +{ + auto app = Application::GetOrCreateInstance(); + UnitTest::LoadPlugins(); + + // Class 1 oracle: the 3x2x2 fixture is value(x, y, z) = x + 10*y + 100*z. + DataStructure dataStructure; + + const SizeVec3 imageDims = {3, 2, 2}; + const ShapeType arrayDims(std::reverse_iterator(imageDims.end()), std::reverse_iterator(imageDims.begin())); + + auto* imageGeom = ImageGeom::Create(dataStructure, "ImageGeometry"); + imageGeom->setDimensions(imageDims); + imageGeom->setSpacing({1.0f, 1.0f, 1.0f}); + auto* cellData = AttributeMatrix::Create(dataStructure, ImageGeom::k_CellAttributeMatrixName, arrayDims, imageGeom->getId()); + imageGeom->setCellData(*cellData); + auto* imageData = UnitTest::CreateTestDataArray(dataStructure, "ImageData", arrayDims, {1}, cellData->getId()); + auto& imageStore = imageData->getDataStoreRef(); + for(usize z = 0; z < imageDims.getZ(); ++z) + { + for(usize y = 0; y < imageDims.getY(); ++y) + { + for(usize x = 0; x < imageDims.getX(); ++x) + { + imageStore[(z * 6) + (y * 3) + x] = static_cast(x + (10 * y) + (100 * z)); + } + } + } + + const DataPath imageGeomPath({"ImageGeometry"}); + const DataPath imageDataPath = imageGeomPath.createChildPath(ImageGeom::k_CellAttributeMatrixName).createChildPath("ImageData"); + const fs::path outputDir = fs::path(unit_test::k_BinaryTestOutputDir.view()) / CreateRandomDirName(); + + const auto writeAndCheck = [&](const DataPath& inputPath, ChoicesParameter::ValueType plane, const std::string& name, const std::string& extension, const std::array& dimensions, + const auto& expectedSlices) { + ITKImageWriterFilter filter; + const fs::path outputPath = outputDir / name / fmt::format("slice{}", extension); + Arguments args; + args.insertOrAssign(ITKImageWriterFilter::k_ImageGeomPath_Key, std::make_any(imageGeomPath)); + args.insertOrAssign(ITKImageWriterFilter::k_ImageArrayPath_Key, std::make_any(inputPath)); + args.insertOrAssign(ITKImageWriterFilter::k_FileName_Key, std::make_any(outputPath)); + args.insertOrAssign(ITKImageWriterFilter::k_IndexOffset_Key, std::make_any(0)); + args.insertOrAssign(ITKImageWriterFilter::k_Plane_Key, std::make_any(plane)); + args.insertOrAssign(ITKImageWriterFilter::k_TotalIndexDigits_Key, std::make_any(3)); + args.insertOrAssign(ITKImageWriterFilter::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); + + for(usize slice = 0; slice < expectedSlices.size(); slice++) + { + CompareImageToExpected(outputDir / name / fmt::format("slice_{:03d}{}", slice, extension), dimensions, expectedSlices[slice]); + } + }; + + // Rows are y/z respectively. The non-square XY plane makes an X/Y transpose observable. + const std::string extension = std::is_same_v ? ".tif" : ".mha"; + writeAndCheck(imageDataPath, ITKImageWriterFilter::k_XYPlane, "xy", extension, {3, 2}, std::vector>{{0, 1, 2, 10, 11, 12}, {100, 101, 102, 110, 111, 112}}); + writeAndCheck(imageDataPath, ITKImageWriterFilter::k_XZPlane, "xz", extension, {3, 2}, std::vector>{{0, 1, 2, 100, 101, 102}, {10, 11, 12, 110, 111, 112}}); + writeAndCheck(imageDataPath, ITKImageWriterFilter::k_YZPlane, "yz", extension, {2, 2}, std::vector>{{0, 10, 100, 110}, {1, 11, 101, 111}, {2, 12, 102, 112}}); + + std::error_code error; + fs::remove_all(outputDir, error); + REQUIRE_FALSE(error); + UnitTest::CheckArraysInheritTupleDims(dataStructure); +} + +TEST_CASE("ITKImageProcessing::ITKImageWriterFilter: Fill Character Validation", "[ITKImageProcessing][ITKImageWriterFilter]") +{ + auto app = Application::GetOrCreateInstance(); + UnitTest::LoadPlugins(); + + DataStructure dataStructure; + + const SizeVec3 imageDims = {1, 1, 2}; + const ShapeType arrayDims(std::reverse_iterator(imageDims.end()), std::reverse_iterator(imageDims.begin())); + + auto* imageGeom = ImageGeom::Create(dataStructure, "ImageGeometry"); + imageGeom->setDimensions(imageDims); + imageGeom->setSpacing({1.0f, 1.0f, 1.0f}); + auto* cellData = AttributeMatrix::Create(dataStructure, ImageGeom::k_CellAttributeMatrixName, arrayDims, imageGeom->getId()); + imageGeom->setCellData(*cellData); + UnitTest::CreateTestDataArray(dataStructure, "ImageData", arrayDims, {1}, cellData->getId()); + + ITKImageWriterFilter filter; + Arguments args; + args.insertOrAssign(ITKImageWriterFilter::k_ImageGeomPath_Key, std::make_any(k_ImageGeomPath)); + args.insertOrAssign(ITKImageWriterFilter::k_ImageArrayPath_Key, std::make_any(k_ImageDataPath)); + args.insertOrAssign(ITKImageWriterFilter::k_FileName_Key, std::make_any("invalid_fill.tif")); + args.insertOrAssign(ITKImageWriterFilter::k_IndexOffset_Key, std::make_any(0)); + args.insertOrAssign(ITKImageWriterFilter::k_Plane_Key, std::make_any(ITKImageWriterFilter::k_XYPlane)); + args.insertOrAssign(ITKImageWriterFilter::k_TotalIndexDigits_Key, std::make_any(3)); + args.insertOrAssign(ITKImageWriterFilter::k_LeadingDigitCharacter_Key, std::make_any("")); + + const auto preflightResult = filter.preflight(dataStructure, args); + REQUIRE(preflightResult.outputActions.invalid()); + REQUIRE(preflightResult.outputActions.errors()[0].code == -25601); +} + +TEST_CASE("ITKImageProcessing::ITKImageWriterFilter: Dimension Mismatch Validation", "[ITKImageProcessing][ITKImageWriterFilter]") +{ + auto app = Application::GetOrCreateInstance(); + UnitTest::LoadPlugins(); + + DataStructure dataStructure; + auto* imageGeom = ImageGeom::Create(dataStructure, "ImageGeometry"); + imageGeom->setDimensions({1, 1, 2}); + imageGeom->setSpacing({1.0f, 1.0f, 1.0f}); + UnitTest::CreateTestDataArray(dataStructure, "ImageData", {1, 1, 1}, {1}); + + ITKImageWriterFilter filter; + Arguments args; + args.insertOrAssign(ITKImageWriterFilter::k_ImageGeomPath_Key, std::make_any(k_ImageGeomPath)); + args.insertOrAssign(ITKImageWriterFilter::k_ImageArrayPath_Key, std::make_any(std::vector{"ImageData"})); + args.insertOrAssign(ITKImageWriterFilter::k_FileName_Key, std::make_any("dimension_mismatch.tif")); + args.insertOrAssign(ITKImageWriterFilter::k_IndexOffset_Key, std::make_any(0)); + args.insertOrAssign(ITKImageWriterFilter::k_Plane_Key, std::make_any(ITKImageWriterFilter::k_XYPlane)); + args.insertOrAssign(ITKImageWriterFilter::k_TotalIndexDigits_Key, std::make_any(3)); + args.insertOrAssign(ITKImageWriterFilter::k_LeadingDigitCharacter_Key, std::make_any("0")); + + const auto preflightResult = filter.preflight(dataStructure, args); + REQUIRE(preflightResult.outputActions.invalid()); + REQUIRE(preflightResult.outputActions.errors()[0].code == -25600); +} + +TEST_CASE("ITKImageProcessing::ITKImageWriterFilter: 3D Image Single-File Output", "[ITKImageProcessing][ITKImageWriterFilter]") +{ + auto app = Application::GetOrCreateInstance(); + UnitTest::LoadPlugins(); + + DataStructure dataStructure; + + const SizeVec3 imageDims = {3, 1, 2}; + const ShapeType arrayDims(std::reverse_iterator(imageDims.end()), std::reverse_iterator(imageDims.begin())); + + auto* imageGeom = ImageGeom::Create(dataStructure, "ImageGeometry"); + imageGeom->setDimensions(imageDims); + imageGeom->setSpacing({1.0f, 1.0f, 1.0f}); + auto* cellData = AttributeMatrix::Create(dataStructure, ImageGeom::k_CellAttributeMatrixName, arrayDims, imageGeom->getId()); + imageGeom->setCellData(*cellData); + auto* imageData = UnitTest::CreateTestDataArray(dataStructure, "ImageData", arrayDims, {1}, cellData->getId()); + auto& imageStore = imageData->getDataStoreRef(); + for(usize z = 0; z < imageDims.getZ(); ++z) + { + for(usize x = 0; x < imageDims.getX(); ++x) + { + imageStore[(z * 3) + x] = static_cast(x + (100 * z)); + } + } + + const fs::path outputDir = fs::path(unit_test::k_BinaryTestOutputDir.view()) / CreateRandomDirName(); + const fs::path outputPath = outputDir / "volume.mha"; + ITKImageWriterFilter filter; + Arguments args; + args.insertOrAssign(ITKImageWriterFilter::k_ImageGeomPath_Key, std::make_any(k_ImageGeomPath)); + args.insertOrAssign(ITKImageWriterFilter::k_ImageArrayPath_Key, std::make_any(k_ImageDataPath)); + args.insertOrAssign(ITKImageWriterFilter::k_FileName_Key, std::make_any(outputPath)); + args.insertOrAssign(ITKImageWriterFilter::k_IndexOffset_Key, std::make_any(0)); + args.insertOrAssign(ITKImageWriterFilter::k_Plane_Key, std::make_any(ITKImageWriterFilter::k_XZPlane)); + args.insertOrAssign(ITKImageWriterFilter::k_TotalIndexDigits_Key, std::make_any(3)); + args.insertOrAssign(ITKImageWriterFilter::k_LeadingDigitCharacter_Key, std::make_any("0")); + + const auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); + const auto executeResult = filter.execute(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); + REQUIRE(fs::exists(outputPath)); + CompareImageToExpected(outputPath, {3, 2}, std::vector{0, 1, 2, 100, 101, 102}); + + std::error_code error; + fs::remove_all(outputDir, error); + REQUIRE_FALSE(error); +} + TEST_CASE("ITKImageProcessing::ITKImageWriterFilter: Write Stack", "[ITKImageProcessing][ITKImageWriterFilter]") { auto app = Application::GetOrCreateInstance(); diff --git a/src/Plugins/ITKImageProcessing/vv/ITKImageWriterFilter.md b/src/Plugins/ITKImageProcessing/vv/ITKImageWriterFilter.md new file mode 100644 index 0000000000..efd596e0da --- /dev/null +++ b/src/Plugins/ITKImageProcessing/vv/ITKImageWriterFilter.md @@ -0,0 +1,130 @@ +# V&V Report: ITKImageWriterFilter + +| | | +|---|---| +| Plugin | ITKImageProcessing | +| SIMPLNX UUID | `a181ee3e-1678-4133-b9c5-a9dd7bfec62f` | +| DREAM3D 6.5.171 equivalent | `ITKImageWriter` (SIMPL UUID `11473711-f94d-5d96-b749-ec36a81ad338`) - `Source/Plugins/ITKImageProcessing/ITKImageProcessingFilters/ITKImageWriter.{h,cpp}` | +| Verified commit | ** | +| Status | READY FOR REVIEW | +| Sign-off | | + +## At a glance + +| Aspect | Current state | +|---|---| +| Algorithm Relationship | Minor changes - same XY/XZ/YZ options; NX uses SIMPLNX stores, AtomicFile, and current ITK APIs. | +| Oracle (confirmed) | Classes 1 + 4 - one 3x2x2 scalar fixture (`value(x,y,z)=x+10y+100z`) has same XY/XZ/YZ pixels for all ten accepted types; uint8 uses TIFF and the remaining types use MetaImage. | +| Code paths enumerated | 17 of 18 branches exercised; only ITK write-failure propagation remains untested. | +| Tests today | 6 test cases - 1 Class 1+4 Oracle, 2 preflight error path tests, 1 image format supports write as 1 file test, 1 stack writing test, 1 SIMPL backwards compatibility test | +| Exemplar archive | None - Class 1+4 oracle uses inline data | +| Legacy comparison | Run against DREAM3D 6.5.171 with the inline test data for the Class 1+4 Oracle. Each slice matches exactly; D1 records filename formatting difference only. | +| Bug flags | Unsigned uint32/uint64 dispatch was corrected for this V&V cycle. | +| V&V phase | Ready for review | + +## Summary + +ITKImageWriterFilter exports scalar ImageGeom cell data as an ITK image or a 2D image stack. A hand-derived non-square fixture verifies decoded pixel orientation independently of legacy, while the legacy comparison checks that migration preserves decoded output on the toy and production fixtures. + +## Algorithm Relationship + +**Minor changes.** Port-time changes are SIMPLNX DataStructure stores, AtomicFile writes, and current ITK APIs; the pixel comparisons show no output change from these substitutions. + +**PR(s):** + +**2026** + +- [PR #1626](https://github.com/BlueQuartzSoftware/simplnx/pull/1626) ("ENH: Various small doc, bug and enhancement fixes.") - Small documentation, bug, and enhancement fixes. +- [PR #1585](https://github.com/BlueQuartzSoftware/simplnx/pull/1585) ("ENH: Add Image Reader/Writer that depend on Tiff and Stb libraries.") - Integrates TIFF/STB reader-writer support. +- [PR #1576](https://github.com/BlueQuartzSoftware/simplnx/pull/1576) ("ENH: Improve error messages across the codebase") - Improves error messages. +- [PR #1555](https://github.com/BlueQuartzSoftware/simplnx/pull/1555) ("ENH: Again require in-memory data for ITK filters") - Restores the ITK in-memory data requirement. +- [PR #1490](https://github.com/BlueQuartzSoftware/simplnx/pull/1490) ("STY: Fix warnings about unintended slicing of object") - Removes object-slicing warnings. +- [PR #1476](https://github.com/BlueQuartzSoftware/simplnx/pull/1476) ("BUG/ENH: Fix Backwards Pipeline Compatibility and Add Testing") - Fixes backward-pipeline compatibility. + +**2025** + +- [PR #1489](https://github.com/BlueQuartzSoftware/simplnx/pull/1489) ("ENH: ItkImageWriter allow user to set the number of padding digits and the fill char") - Adds output-index padding and fill-character controls. +- [PR #1457](https://github.com/BlueQuartzSoftware/simplnx/pull/1457) ("STY: Clean up 'static inline' from filter headers") - Cleans up static-inline header declarations. +- [PR #1377](https://github.com/BlueQuartzSoftware/simplnx/pull/1377) ("STY: Ensure all code arguments are consistent across filters") - Standardizes argument style. +- [PR #1257](https://github.com/BlueQuartzSoftware/simplnx/pull/1257) ("ENH: Add missing documentation comments for preflight and execute methods in filters") - Adds preflight and execute documentation. +- [PR #1253](https://github.com/BlueQuartzSoftware/simplnx/pull/1253) ("ENH: Merge Initial Out-of-Core infrastructure") - Introduces the initial OOC infrastructure. +- [PR #1238](https://github.com/BlueQuartzSoftware/simplnx/pull/1238) ("ENH: Added pipeline relative path support") - Adds pipeline-relative path support. +- [PR #1232](https://github.com/BlueQuartzSoftware/simplnx/pull/1232) ("DOC: General code clean up to make parameters consistent and docs consistent") - Aligns parameter and documentation conventions. + +**2024** + +- [PR #1082](https://github.com/BlueQuartzSoftware/simplnx/pull/1082) ("SIMPLConversion header optimization") - Optimizes SIMPL-conversion headers. +- [PR #941](https://github.com/BlueQuartzSoftware/simplnx/pull/941) ("Moved Result handling outside of AtomicFile") - Moves result handling outside AtomicFile. +- [PR #934](https://github.com/BlueQuartzSoftware/simplnx/pull/934) ("BUG: Pipeline and Filter human facing label cleanup") - Cleans up human-facing labels. +- [PR #931](https://github.com/BlueQuartzSoftware/simplnx/pull/931) ("ENH: All filter's class names end with \"Filter\".") - Applies the Filter class-name suffix. +- [PR #918](https://github.com/BlueQuartzSoftware/simplnx/pull/918) ("BUG: Update ITK Image Writer to Use Atomic File API") - Migrates the writer to the AtomicFile API. +- [PR #874](https://github.com/BlueQuartzSoftware/simplnx/pull/874) ("ENH: Refactor the Parameter Keys to make them consistent and easy to learn") - Refactors parameter keys. + +**2023** + +- [PR #801](https://github.com/BlueQuartzSoftware/simplnx/pull/801) ("ENH: Rename complex to simplnx") - Renames the framework namespace and repository identity. +- [PR #790](https://github.com/BlueQuartzSoftware/simplnx/pull/790) ("ENH: Write Temp Files for All Writers") - Adds temporary-file writing for writers. +- [PR #779](https://github.com/BlueQuartzSoftware/simplnx/pull/779) ("ENH: Implement SIMPL pipeline conversion") - Implements SIMPL pipeline conversion. +- [PR #753](https://github.com/BlueQuartzSoftware/simplnx/pull/753) ("API: Standardize I/O Naming to Read/Write") - Standardizes I/O naming. +- [PR #703](https://github.com/BlueQuartzSoftware/simplnx/pull/703) ("ENH: Enable Out-of-Core functionality") - Adds OOC infrastructure compatibility. +- [PR #671](https://github.com/BlueQuartzSoftware/simplnx/pull/671) ("API: Add C++ Class Name to All Default Tags") - Adds C++ class names to default tags. +- [PR #593](https://github.com/BlueQuartzSoftware/simplnx/pull/593) ("ENH: Update ITK filters to follow naming and parameter layout conventions") - Aligns ITK filter naming and parameter layout. +- [PR #575](https://github.com/BlueQuartzSoftware/simplnx/pull/575) ("BUG: Fix issue where ITKImageWriter is double looping over the Z dim") - Fixes double iteration over Z. +- [PR #86](https://github.com/BlueQuartzSoftware/simplnx/pull/86) ("STYLE: Clean Up Includes") - Cleans up includes. + +**2022** + +- [PR #80](https://github.com/BlueQuartzSoftware/simplnx/pull/80) ("STYLE: Short Variable Patching") - Shortens variable names. +- [PR #56](https://github.com/BlueQuartzSoftware/simplnx/pull/56) ("Add Filter Comments") - Adds filter comments. +- [PR #54](https://github.com/BlueQuartzSoftware/simplnx/pull/54) ("DOCS: Update paths and CMake codes to prepare for documentation updates") - Updates documentation paths and CMake configuration. +- [PR #50](https://github.com/BlueQuartzSoftware/simplnx/pull/50) ("All Parameter keys should be snake_case") - Converts parameter keys to snake_case. +- [PR #37](https://github.com/BlueQuartzSoftware/simplnx/pull/37) ("Refactored geometry hierarchy") - Refactors the geometry hierarchy. +- [PR #30](https://github.com/BlueQuartzSoftware/simplnx/pull/30) ("UUID cleaning") - Cleans up filter UUIDs. +- [PR #2](https://github.com/BlueQuartzSoftware/simplnx/pull/2) ("Newly Created ITK Wrapped Filters") - Adds the newly generated ITK-wrapped filter implementation. + +## Oracle + +**Class:** 1 (Analytical) + 4 (Invariant). + +**Applied:** The 3x2x2 scalar fixture has `value(x,y,z)=x+10y+100z`; exact XY, XZ, and YZ pixel matrices are hand-derived and decoded for all ten accepted types. uint8 uses TIFF; the remaining types use MetaImage. File creation and decoded dimensions are companion invariants. + +**Encoded:** `test/ITKImageWriterTest.cpp::ITKImageProcessing::ITKImageWriterFilter: Analytical TIFF Pixel Order` - templated over all ten accepted scalar types; all checks pass. + +**Second-engineer review:** pending second-engineer review. + +## Code path coverage + +17 of 18 branches exercised. Source: `src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp` (533 lines). + +| # | Path | Test case | +|---|---|---| +| 1 | Valid preflight. | `Analytical TIFF Pixel Order`, `Write Stack` | +| 2 | Dimension-mismatch preflight error. | `Dimension Mismatch Validation` - a 1x1x1 array and 1x1x2 geometry return `-25600`. | +| 3 | Fill-character validation, including empty input. | `Fill Character Validation` - empty string returns `-25601`. | +| 4 | XY output for all accepted scalar types. | `Analytical TIFF Pixel Order` - same pixels; uint8 TIFF, other types MetaImage. | +| 5 | XZ output for all accepted scalar types. | `Analytical TIFF Pixel Order` - same pixels; uint8 TIFF, other types MetaImage. | +| 6 | YZ output for all accepted scalar types. | `Analytical TIFF Pixel Order` - same pixels; uint8 TIFF, other types MetaImage. | +| 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 | All accepted scalar dispatch arms: int8, uint8, int16, uint16, int32, uint32, int64, uint64, float32, and float64. | `Analytical TIFF Pixel Order` - same TIFF pixels for uint8 and MetaImage pixels for every other type. | +| 17 | Single-file output for a non-2D format. | `3D Image Single-File Output` - a 3x1x2 ImageGeom XZ plane writes one unsuffixed MetaImage file with exact decoded pixels. | +| 18 | Filesystem/ITK write failure. | *Not directly tested; requires failure injection.* | + +## Test inventory + +| Test case | Status | Notes | +|---|---|---| +| `ITKImageProcessing::ITKImageWriterFilter: Analytical TIFF Pixel Order` | new-for-V&V | Tests Class 1+4 Oracle over all accepted types. | +| `ITKImageProcessing::ITKImageWriterFilter: Fill Character Validation` | new-for-V&V | Empty fill character is rejected during preflight with error `-25601`. | +| `ITKImageProcessing::ITKImageWriterFilter: Dimension Mismatch Validation` | new-for-V&V | Mismatched array and geometry input is rejected during preflight with error `-25600`. | +| `ITKImageProcessing::ITKImageWriterFilter: 3D Image Single-File Output` | new-for-V&V | A 3x1x2 ImageGeom XZ plane writes an unsuffixed `.mha` file with exact decoded pixels. | +| `ITKImageProcessing::ITKImageWriterFilter: Write Stack` | kept | Checks that an image is written as an image stack of files. | +| `ITKImageProcessing::ITKImageWriterFilter: SIMPL Backwards Compatibility` | kept | Covers SIMPL json backwards compatibility. | + +## Exemplar archive + +No new exemplar archive was created for this V&V cycle: the Class 1 oracle is encoded entirely as inline expected values in the test source. + +## Deviations from DREAM3D 6.5.171 + +- `ITKImageWriterFilter-D1` - NX defaults to zero-padded slice indices while 6.5.171 does not - see `vv/deviations/ITKImageWriterFilter.md`. + +All pixels otherwise match: the analytical fixture has 2 XY, 2 XZ, and 3 YZ slices; Small IN100 has 117 XY, 201 XZ, and 189 YZ float32 TIFF slices. diff --git a/src/Plugins/ITKImageProcessing/vv/deviations/ITKImageWriterFilter.md b/src/Plugins/ITKImageProcessing/vv/deviations/ITKImageWriterFilter.md new file mode 100644 index 0000000000..9b53efe756 --- /dev/null +++ b/src/Plugins/ITKImageProcessing/vv/deviations/ITKImageWriterFilter.md @@ -0,0 +1,21 @@ +# Deviations from DREAM3D 6.5.171: ITKImageWriterFilter + +This file lists every documented behavioral difference between this SIMPLNX filter and its DREAM3D 6.5.171 equivalent. + +Entries are referenced by stable ID (ITKImageWriterFilter-D) from the V&V report and from public migration guidance. The ID is stable across renames; the Filter UUID field is the permanent cross-reference anchor. + +## ITKImageWriterFilter-D1 + +| Field | Value | +|---|---| +| **Deviation ID** | `ITKImageWriterFilter-D1` | +| **Filter UUID** | `a181ee3e-1678-4133-b9c5-a9dd7bfec62f` | +| **Status** | active | + +**Symptom:** For a multi-slice 2D output, 6.5.171 names the first slice `_0.ext`; NX defaults to `_000.ext`. + +**Root cause:** Algorithmic choice. NX added index offset, total-digit, and fill-character parameters and defaults the total digit count to three; 6.5.171 always wrote an unpadded zero-based suffix. + +**Affected users:** Any workflow that consumes 2D image-stack filenames directly, rather than discovering them by pattern, can fail to find NX output after migration. + +**Recommendation:** Trust SIMPLNX. To retain the legacy filename shape, set *Index Offset* to `0` and *Total Number of Index Digits* to `1`. From 8ea4eab5eba55b18f3a94cb3919bdc5ef21220fc Mon Sep 17 00:00:00 2001 From: Jared Duffey Date: Tue, 28 Jul 2026 16:57:52 -0400 Subject: [PATCH 06/28] Added cancel checks Signed-off-by: Jared Duffey --- .../Filters/ITKImageWriterFilter.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp b/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp index 8b651eae2a..48744bba83 100644 --- a/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp +++ b/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp @@ -424,6 +424,10 @@ Result<> ITKImageWriterFilter::executeImpl(DataStructure& dataStructure, const A for(usize slice = 0; slice < dims.getZ(); ++slice) { + if(shouldCancel) + { + return {}; + } for(usize axisA = 0; axisA < dA; ++axisA) { for(usize axisB = 0; axisB < dB; ++axisB) @@ -448,6 +452,10 @@ Result<> ITKImageWriterFilter::executeImpl(DataStructure& dataStructure, const A for(usize slice = 0; slice < dims.getY(); ++slice) { + if(shouldCancel) + { + return {}; + } for(usize axisA = 0; axisA < dA; ++axisA) { for(usize axisB = 0; axisB < dB; ++axisB) @@ -472,6 +480,10 @@ Result<> ITKImageWriterFilter::executeImpl(DataStructure& dataStructure, const A for(usize slice = 0; slice < dims.getX(); ++slice) { + if(shouldCancel) + { + return {}; + } for(usize axisA = 0; axisA < dA; ++axisA) { for(usize axisB = 0; axisB < dB; ++axisB) From 813558c014a84495ad58b39d1775f37f1c3d17c2 Mon Sep 17 00:00:00 2001 From: Jared Duffey Date: Tue, 28 Jul 2026 17:02:40 -0400 Subject: [PATCH 07/28] Added fill char check Signed-off-by: Jared Duffey --- .../Filters/ITKImageWriterFilter.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp b/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp index 48744bba83..53d5768ab7 100644 --- a/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp +++ b/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp @@ -41,6 +41,12 @@ namespace cxITKImageWriterFilter { using ArrayOptionsType = ITK::ScalarVectorPixelIdTypeList; +bool IsValidFillCharacter(char fillCharacter) +{ + return fillCharacter != '{' && fillCharacter != '}' && fillCharacter != '\\' && fillCharacter != '/' && fillCharacter != ':' && fillCharacter != '*' && fillCharacter != '?' && + fillCharacter != '"' && fillCharacter != '<' && fillCharacter != '>' && fillCharacter != '|'; +} + bool Is2DFormat(const fs::path& fileName) { fs::path ext = fileName.extension(); @@ -359,6 +365,10 @@ IFilter::PreflightResult ITKImageWriterFilter::preflightImpl(const DataStructure { return {MakeErrorResult(-25601, fmt::format("The fill character must contain exactly one character; received {} characters.", fillChar.size()))}; } + if(!cxITKImageWriterFilter::IsValidFillCharacter(fillChar.at(0))) + { + return {MakeErrorResult(-25602, fmt::format("The fill character '{}' is not valid for format strings and file names.", fillChar))}; + } Result resultOutputActions; std::vector preflightUpdatedValues; From c0f6ac2354b943b52e87b1d547cc1e7ed35f0c31 Mon Sep 17 00:00:00 2001 From: Jared Duffey Date: Tue, 28 Jul 2026 17:04:06 -0400 Subject: [PATCH 08/28] Fixed preview filename for single slice Signed-off-by: Jared Duffey --- .../src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp b/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp index 53d5768ab7..3566fcc986 100644 --- a/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp +++ b/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp @@ -391,8 +391,8 @@ IFilter::PreflightResult ITKImageWriterFilter::preflightImpl(const DataStructure } // Generate example filename for PreflightValues - const std::string indexStr = CreateIndexString(maxSlice, static_cast(totalDigits), fillChar); - const std::string exampleFileName = fmt::format("{}/{}_{}{}", fs::absolute(filePath).parent_path().string(), filePath.stem().string(), indexStr, filePath.extension().string()); + const std::string indexStr = maxSlice == 1 ? "" : fmt::format("_{}", CreateIndexString(indexOffset, static_cast(totalDigits), fillChar)); + const std::string exampleFileName = (fs::absolute(filePath).parent_path() / fmt::format("{}{}{}", filePath.stem().string(), indexStr, filePath.extension().string())).string(); preflightUpdatedValues.push_back({"Example Output File", exampleFileName}); From e98c8d47496664c6d21bca1e97e7f097eddc9328 Mon Sep 17 00:00:00 2001 From: Jared Duffey Date: Tue, 28 Jul 2026 17:08:24 -0400 Subject: [PATCH 09/28] Fixed origin and spacing for slices Signed-off-by: Jared Duffey --- .../ITKImageProcessing/Filters/ITKImageWriterFilter.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp b/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp index 3566fcc986..5d66cc65ff 100644 --- a/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp +++ b/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp @@ -424,6 +424,9 @@ Result<> ITKImageWriterFilter::executeImpl(DataStructure& dataStructure, const A ITK::ImageGeomData newImageGeom(imageGeom); + const FloatVec3 origin = imageGeom.getOrigin(); + const FloatVec3 spacing = imageGeom.getSpacing(); + switch(plane) { case k_XYPlane: { @@ -431,6 +434,8 @@ Result<> ITKImageWriterFilter::executeImpl(DataStructure& dataStructure, const A usize dB = dims.getY(); newImageGeom.dims = {dims.getX(), dims.getY(), 1}; + newImageGeom.origin = {origin.getX(), origin.getY(), 0.0f}; + newImageGeom.spacing = {spacing.getX(), spacing.getY(), 1.0f}; for(usize slice = 0; slice < dims.getZ(); ++slice) { @@ -459,6 +464,8 @@ Result<> ITKImageWriterFilter::executeImpl(DataStructure& dataStructure, const A usize dB = dims.getX(); newImageGeom.dims = {dims.getX(), dims.getZ(), 1}; + newImageGeom.origin = {origin.getX(), origin.getZ(), 0.0f}; + newImageGeom.spacing = {spacing.getX(), spacing.getZ(), 1.0f}; for(usize slice = 0; slice < dims.getY(); ++slice) { @@ -487,6 +494,8 @@ Result<> ITKImageWriterFilter::executeImpl(DataStructure& dataStructure, const A usize dB = dims.getY(); newImageGeom.dims = {dims.getY(), dims.getZ(), 1}; + newImageGeom.origin = {origin.getY(), origin.getZ(), 0.0f}; + newImageGeom.spacing = {spacing.getY(), spacing.getZ(), 1.0f}; for(usize slice = 0; slice < dims.getX(); ++slice) { From 46a06949debec1d01e36e906e162bc1e59870e4e Mon Sep 17 00:00:00 2001 From: Jared Duffey Date: Tue, 28 Jul 2026 17:09:19 -0400 Subject: [PATCH 10/28] Disallow out of core arrays Signed-off-by: Jared Duffey --- .../src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp b/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp index 5d66cc65ff..8f3027d83e 100644 --- a/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp +++ b/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp @@ -369,6 +369,11 @@ IFilter::PreflightResult ITKImageWriterFilter::preflightImpl(const DataStructure { return {MakeErrorResult(-25602, fmt::format("The fill character '{}' is not valid for format strings and file names.", fillChar))}; } + if(!imageArray.getDataFormat().empty()) + { + return {MakeErrorResult(ITK::Constants::k_OutOfCoreDataNotSupported, + fmt::format("Input Array '{}' utilizes out-of-core data. This is not supported within ITK filters.", imageArrayPath.toString()))}; + } Result resultOutputActions; std::vector preflightUpdatedValues; From 201037faeb8ac6b02ab32e1684013d0636fbb2f6 Mon Sep 17 00:00:00 2001 From: Jared Duffey Date: Thu, 30 Jul 2026 09:49:34 -0400 Subject: [PATCH 11/28] Added component size check Signed-off-by: Jared Duffey --- .../Filters/ITKImageWriterFilter.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp b/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp index 8f3027d83e..7671630288 100644 --- a/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp +++ b/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp @@ -40,6 +40,12 @@ using namespace nx::core; namespace cxITKImageWriterFilter { using ArrayOptionsType = ITK::ScalarVectorPixelIdTypeList; +constexpr std::array k_AllowedComponentSizes = {1, 2, 3, 4, 10, 11, 36}; + +bool IsValidComponentSize(usize componentSize) +{ + return std::find(k_AllowedComponentSizes.begin(), k_AllowedComponentSizes.end(), componentSize) != k_AllowedComponentSizes.end(); +} bool IsValidFillCharacter(char fillCharacter) { @@ -374,6 +380,12 @@ IFilter::PreflightResult ITKImageWriterFilter::preflightImpl(const DataStructure return {MakeErrorResult(ITK::Constants::k_OutOfCoreDataNotSupported, fmt::format("Input Array '{}' utilizes out-of-core data. This is not supported within ITK filters.", imageArrayPath.toString()))}; } + const usize componentCount = imageArray.getNumberOfComponents(); + if(!cxITKImageWriterFilter::IsValidComponentSize(componentCount)) + { + return {MakeErrorResult( + -21010, fmt::format("Input Array '{}' has {} components. Supported component counts are {}.", imageArrayPath.toString(), componentCount, cxITKImageWriterFilter::k_AllowedComponentSizes))}; + } Result resultOutputActions; std::vector preflightUpdatedValues; From 18eb06404b2200501475e0c3fb9700f09415579f Mon Sep 17 00:00:00 2001 From: Jared Duffey Date: Thu, 30 Jul 2026 09:49:57 -0400 Subject: [PATCH 12/28] Updated RGBA support Signed-off-by: Jared Duffey --- src/Plugins/ITKImageProcessing/docs/ITKImageWriterFilter.md | 2 +- .../src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Plugins/ITKImageProcessing/docs/ITKImageWriterFilter.md b/src/Plugins/ITKImageProcessing/docs/ITKImageWriterFilter.md index 97a4c49430..5feb8c25a6 100644 --- a/src/Plugins/ITKImageProcessing/docs/ITKImageWriterFilter.md +++ b/src/Plugins/ITKImageProcessing/docs/ITKImageWriterFilter.md @@ -6,7 +6,7 @@ ITKImageProcessing (ITKImageProcessing) ## Description -This **Filter** will save images based on an array that represents grayscale, RGB or ARGB color values. If the input array represents a 3D volume, the **Filter** will output a series of slices along one of the orthogonal axes. The options are to produce XY slices along the Z axis, XZ slices along the Y axis or YZ slices along the X axis. +This **Filter** will save images based on scalar, vector, RGB, or RGBA values. Supported component counts are 1, 2, 3, 4, 10, 11, and 36. If the input array represents a 3D volume, the **Filter** will output a series of slices along one of the orthogonal axes. The options are to produce XY slices along the Z axis, XZ slices along the Y axis or YZ slices along the X axis. The available output formats are determined by the installed ITK image I/O backends and the filename extension. TIFF, BMP, and PNG are common 2D choices; not every pixel type is supported by every format. diff --git a/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp b/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp index 7671630288..645463a9ef 100644 --- a/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp +++ b/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp @@ -40,6 +40,8 @@ using namespace nx::core; namespace cxITKImageWriterFilter { using ArrayOptionsType = ITK::ScalarVectorPixelIdTypeList; +using RgbRgbaArrayOptionsType = ITK::ArrayOptions, ITK::ArrayUseAllTypes>; + constexpr std::array k_AllowedComponentSizes = {1, 2, 3, 4, 10, 11, 36}; bool IsValidComponentSize(usize componentSize) @@ -267,6 +269,10 @@ Result<> SaveImageData(const fs::path& filePath, IDataStore& sliceData, const IT auto fileName = fs::path(ss.str()); + if(sliceData.getNumberOfComponents() == 4) + { + return ITK::ArraySwitchFunc(sliceData, imageGeom, -21010, sliceData, imageGeom, fileName, indexOffset); + } return ITK::ArraySwitchFunc(sliceData, imageGeom, -21010, sliceData, imageGeom, fileName, indexOffset); } } // namespace cxITKImageWriterFilter From 6afd53bfe7b6ee179730dd0c9278076ca6bf6a04 Mon Sep 17 00:00:00 2001 From: Jared Duffey Date: Thu, 30 Jul 2026 10:08:22 -0400 Subject: [PATCH 13/28] Updated unit tests Signed-off-by: Jared Duffey --- .../test/ITKImageWriterTest.cpp | 116 ++++++++++++++++-- 1 file changed, 106 insertions(+), 10 deletions(-) diff --git a/src/Plugins/ITKImageProcessing/test/ITKImageWriterTest.cpp b/src/Plugins/ITKImageProcessing/test/ITKImageWriterTest.cpp index 491b318328..8b7735cac8 100644 --- a/src/Plugins/ITKImageProcessing/test/ITKImageWriterTest.cpp +++ b/src/Plugins/ITKImageProcessing/test/ITKImageWriterTest.cpp @@ -21,6 +21,7 @@ #include #include +#include namespace fs = std::filesystem; @@ -106,6 +107,26 @@ void CompareImageToExpected(const fs::path& filePath, const std::array } } +template +void CompareImageMetadata(const fs::path& filePath, const std::array& expectedSpacing, const std::array& expectedOrigin, bool checkOrigin) +{ + using ImageType = itk::Image; + auto reader = itk::ImageFileReader::New(); + reader->SetFileName(filePath.string()); + REQUIRE_NOTHROW(reader->Update()); + + const auto& image = *reader->GetOutput(); + const auto spacing = image.GetSpacing(); + const auto origin = image.GetOrigin(); + CHECK(spacing[0] == Approx(expectedSpacing[0])); + CHECK(spacing[1] == Approx(expectedSpacing[1])); + if(checkOrigin) + { + CHECK(origin[0] == Approx(expectedOrigin[0])); + CHECK(origin[1] == Approx(expectedOrigin[1])); + } +} + } // namespace TEMPLATE_TEST_CASE("ITKImageProcessing::ITKImageWriterFilter: Analytical Pixel Order", "[ITKImageProcessing][ITKImageWriterFilter]", int8, uint8, int16, uint16, int32, uint32, int64, uint64, float32, @@ -122,7 +143,8 @@ TEMPLATE_TEST_CASE("ITKImageProcessing::ITKImageWriterFilter: Analytical Pixel O auto* imageGeom = ImageGeom::Create(dataStructure, "ImageGeometry"); imageGeom->setDimensions(imageDims); - imageGeom->setSpacing({1.0f, 1.0f, 1.0f}); + imageGeom->setOrigin({10.0f, 20.0f, 40.0f}); + imageGeom->setSpacing({1.0f, 2.0f, 4.0f}); auto* cellData = AttributeMatrix::Create(dataStructure, ImageGeom::k_CellAttributeMatrixName, arrayDims, imageGeom->getId()); imageGeom->setCellData(*cellData); auto* imageData = UnitTest::CreateTestDataArray(dataStructure, "ImageData", arrayDims, {1}, cellData->getId()); @@ -143,7 +165,7 @@ TEMPLATE_TEST_CASE("ITKImageProcessing::ITKImageWriterFilter: Analytical Pixel O const fs::path outputDir = fs::path(unit_test::k_BinaryTestOutputDir.view()) / CreateRandomDirName(); const auto writeAndCheck = [&](const DataPath& inputPath, ChoicesParameter::ValueType plane, const std::string& name, const std::string& extension, const std::array& dimensions, - const auto& expectedSlices) { + const std::array& expectedSpacing, const std::array& expectedOrigin, const auto& expectedSlices) { ITKImageWriterFilter filter; const fs::path outputPath = outputDir / name / fmt::format("slice{}", extension); Arguments args; @@ -162,15 +184,20 @@ TEMPLATE_TEST_CASE("ITKImageProcessing::ITKImageWriterFilter: Analytical Pixel O for(usize slice = 0; slice < expectedSlices.size(); slice++) { - CompareImageToExpected(outputDir / name / fmt::format("slice_{:03d}{}", slice, extension), dimensions, expectedSlices[slice]); + const fs::path imagePath = outputDir / name / fmt::format("slice_{:03d}{}", slice, extension); + CompareImageToExpected(imagePath, dimensions, expectedSlices[slice]); + CompareImageMetadata(imagePath, expectedSpacing, expectedOrigin, extension != ".tif"); } }; // Rows are y/z respectively. The non-square XY plane makes an X/Y transpose observable. const std::string extension = std::is_same_v ? ".tif" : ".mha"; - writeAndCheck(imageDataPath, ITKImageWriterFilter::k_XYPlane, "xy", extension, {3, 2}, std::vector>{{0, 1, 2, 10, 11, 12}, {100, 101, 102, 110, 111, 112}}); - writeAndCheck(imageDataPath, ITKImageWriterFilter::k_XZPlane, "xz", extension, {3, 2}, std::vector>{{0, 1, 2, 100, 101, 102}, {10, 11, 12, 110, 111, 112}}); - writeAndCheck(imageDataPath, ITKImageWriterFilter::k_YZPlane, "yz", extension, {2, 2}, std::vector>{{0, 10, 100, 110}, {1, 11, 101, 111}, {2, 12, 102, 112}}); + writeAndCheck(imageDataPath, ITKImageWriterFilter::k_XYPlane, "xy", extension, {3, 2}, {1.0, 2.0}, {10.0, 20.0}, + std::vector>{{0, 1, 2, 10, 11, 12}, {100, 101, 102, 110, 111, 112}}); + writeAndCheck(imageDataPath, ITKImageWriterFilter::k_XZPlane, "xz", extension, {3, 2}, {1.0, 4.0}, {10.0, 40.0}, + std::vector>{{0, 1, 2, 100, 101, 102}, {10, 11, 12, 110, 111, 112}}); + writeAndCheck(imageDataPath, ITKImageWriterFilter::k_YZPlane, "yz", extension, {2, 2}, {2.0, 4.0}, {20.0, 40.0}, + std::vector>{{0, 10, 100, 110}, {1, 11, 101, 111}, {2, 12, 102, 112}}); std::error_code error; fs::remove_all(outputDir, error); @@ -203,11 +230,27 @@ TEST_CASE("ITKImageProcessing::ITKImageWriterFilter: Fill Character Validation", args.insertOrAssign(ITKImageWriterFilter::k_IndexOffset_Key, std::make_any(0)); args.insertOrAssign(ITKImageWriterFilter::k_Plane_Key, std::make_any(ITKImageWriterFilter::k_XYPlane)); args.insertOrAssign(ITKImageWriterFilter::k_TotalIndexDigits_Key, std::make_any(3)); - args.insertOrAssign(ITKImageWriterFilter::k_LeadingDigitCharacter_Key, std::make_any("")); + const auto checkInvalidFillCharacter = [&](const StringParameter::ValueType& fillCharacter, int32 expectedCode) { + args.insertOrAssign(ITKImageWriterFilter::k_LeadingDigitCharacter_Key, std::make_any(fillCharacter)); + const auto preflightResult = filter.preflight(dataStructure, args); + REQUIRE(preflightResult.outputActions.invalid()); + REQUIRE(preflightResult.outputActions.errors()[0].code == expectedCode); + }; - const auto preflightResult = filter.preflight(dataStructure, args); - REQUIRE(preflightResult.outputActions.invalid()); - REQUIRE(preflightResult.outputActions.errors()[0].code == -25601); + SECTION("Empty") + { + checkInvalidFillCharacter("", -25601); + } + SECTION("Format control character") + { + checkInvalidFillCharacter("{", -25602); + } + SECTION("Path separator") + { + checkInvalidFillCharacter("/", -25602); + } + + UnitTest::CheckArraysInheritTupleDims(dataStructure); } TEST_CASE("ITKImageProcessing::ITKImageWriterFilter: Dimension Mismatch Validation", "[ITKImageProcessing][ITKImageWriterFilter]") @@ -234,6 +277,7 @@ TEST_CASE("ITKImageProcessing::ITKImageWriterFilter: Dimension Mismatch Validati const auto preflightResult = filter.preflight(dataStructure, args); REQUIRE(preflightResult.outputActions.invalid()); REQUIRE(preflightResult.outputActions.errors()[0].code == -25600); + UnitTest::CheckArraysInheritTupleDims(dataStructure); } TEST_CASE("ITKImageProcessing::ITKImageWriterFilter: 3D Image Single-File Output", "[ITKImageProcessing][ITKImageWriterFilter]") @@ -283,6 +327,58 @@ TEST_CASE("ITKImageProcessing::ITKImageWriterFilter: 3D Image Single-File Output std::error_code error; fs::remove_all(outputDir, error); REQUIRE_FALSE(error); + UnitTest::CheckArraysInheritTupleDims(dataStructure); +} + +TEST_CASE("ITKImageProcessing::ITKImageWriterFilter: RGBA Image Output", "[ITKImageProcessing][ITKImageWriterFilter]") +{ + auto app = Application::GetOrCreateInstance(); + UnitTest::LoadPlugins(); + + DataStructure dataStructure; + const SizeVec3 imageDims = {1, 1, 1}; + const ShapeType arrayDims(std::reverse_iterator(imageDims.end()), std::reverse_iterator(imageDims.begin())); + auto* imageGeom = ImageGeom::Create(dataStructure, "ImageGeometry"); + imageGeom->setDimensions(imageDims); + imageGeom->setSpacing({1.0f, 1.0f, 1.0f}); + auto* cellData = AttributeMatrix::Create(dataStructure, ImageGeom::k_CellAttributeMatrixName, arrayDims, imageGeom->getId()); + imageGeom->setCellData(*cellData); + auto* imageData = UnitTest::CreateTestDataArray(dataStructure, "ImageData", arrayDims, {4}, cellData->getId()); + auto& imageStore = imageData->getDataStoreRef(); + imageStore[0] = 10; + imageStore[1] = 20; + imageStore[2] = 30; + imageStore[3] = 40; + + const fs::path outputDir = fs::path(unit_test::k_BinaryTestOutputDir.view()) / CreateRandomDirName(); + const fs::path outputPath = outputDir / "rgba.mha"; + ITKImageWriterFilter filter; + Arguments args; + args.insertOrAssign(ITKImageWriterFilter::k_ImageGeomPath_Key, std::make_any(k_ImageGeomPath)); + args.insertOrAssign(ITKImageWriterFilter::k_ImageArrayPath_Key, std::make_any(k_ImageDataPath)); + args.insertOrAssign(ITKImageWriterFilter::k_FileName_Key, std::make_any(outputPath)); + args.insertOrAssign(ITKImageWriterFilter::k_IndexOffset_Key, std::make_any(0)); + args.insertOrAssign(ITKImageWriterFilter::k_Plane_Key, std::make_any(ITKImageWriterFilter::k_XYPlane)); + args.insertOrAssign(ITKImageWriterFilter::k_TotalIndexDigits_Key, std::make_any(3)); + args.insertOrAssign(ITKImageWriterFilter::k_LeadingDigitCharacter_Key, std::make_any("0")); + + SIMPLNX_RESULT_REQUIRE_VALID(filter.preflight(dataStructure, args).outputActions); + SIMPLNX_RESULT_REQUIRE_VALID(filter.execute(dataStructure, args).result); + + using ImageType = itk::Image, 2>; + auto reader = itk::ImageFileReader::New(); + reader->SetFileName(outputPath.string()); + REQUIRE_NOTHROW(reader->Update()); + const auto pixel = reader->GetOutput()->GetPixel({0, 0}); + CHECK(pixel.GetRed() == 10); + CHECK(pixel.GetGreen() == 20); + CHECK(pixel.GetBlue() == 30); + CHECK(pixel.GetAlpha() == 40); + + std::error_code error; + fs::remove_all(outputDir, error); + REQUIRE_FALSE(error); + UnitTest::CheckArraysInheritTupleDims(dataStructure); } TEST_CASE("ITKImageProcessing::ITKImageWriterFilter: Write Stack", "[ITKImageProcessing][ITKImageWriterFilter]") From 8355c0ff3e18a746785b309113b812a49f521c84 Mon Sep 17 00:00:00 2001 From: Jared Duffey Date: Thu, 30 Jul 2026 10:08:51 -0400 Subject: [PATCH 14/28] Update report Signed-off-by: Jared Duffey --- .../vv/ITKImageWriterFilter.md | 60 +++++++++++-------- .../vv/deviations/ITKImageWriterFilter.md | 16 +++++ 2 files changed, 52 insertions(+), 24 deletions(-) diff --git a/src/Plugins/ITKImageProcessing/vv/ITKImageWriterFilter.md b/src/Plugins/ITKImageProcessing/vv/ITKImageWriterFilter.md index efd596e0da..4eee646bc2 100644 --- a/src/Plugins/ITKImageProcessing/vv/ITKImageWriterFilter.md +++ b/src/Plugins/ITKImageProcessing/vv/ITKImageWriterFilter.md @@ -6,29 +6,29 @@ | SIMPLNX UUID | `a181ee3e-1678-4133-b9c5-a9dd7bfec62f` | | DREAM3D 6.5.171 equivalent | `ITKImageWriter` (SIMPL UUID `11473711-f94d-5d96-b749-ec36a81ad338`) - `Source/Plugins/ITKImageProcessing/ITKImageProcessingFilters/ITKImageWriter.{h,cpp}` | | Verified commit | ** | -| Status | READY FOR REVIEW | -| Sign-off | | +| Status | DRAFT | +| Sign-off | Pending second-engineer review | ## At a glance | Aspect | Current state | |---|---| -| Algorithm Relationship | Minor changes - same XY/XZ/YZ options; NX uses SIMPLNX stores, AtomicFile, and current ITK APIs. | -| Oracle (confirmed) | Classes 1 + 4 - one 3x2x2 scalar fixture (`value(x,y,z)=x+10y+100z`) has same XY/XZ/YZ pixels for all ten accepted types; uint8 uses TIFF and the remaining types use MetaImage. | -| Code paths enumerated | 17 of 18 branches exercised; only ITK write-failure propagation remains untested. | -| Tests today | 6 test cases - 1 Class 1+4 Oracle, 2 preflight error path tests, 1 image format supports write as 1 file test, 1 stack writing test, 1 SIMPL backwards compatibility test | +| Algorithm Relationship | Minor changes - same XY/XZ/YZ pixel extraction; NX uses SIMPLNX stores, AtomicFile, and current ITK APIs. Plane spacing and origin now follow the selected physical axes (D2). | +| Oracle (confirmed) | Classes 1 + 4 - one 3x2x2 scalar fixture (`value(x,y,z)=x+10y+100z`) has same XY/XZ/YZ pixels for all ten accepted scalar types; it verifies selected-plane spacing for all types and origin for MetaImage output. uint8 uses TIFF and the remaining types use MetaImage. | +| Code paths enumerated | 13 of 18 explicit paths exercised; OOC, unsupported-component, preview-value, cancellation, and write-failure paths remain untested. | +| Tests today | 7 named test cases - 1 Class 1+4 Oracle, 2 preflight error-path tests, 1 single-file output test, 1 RGBA output test, 1 stack-writing test, and 1 SIMPL backwards-compatibility test | | Exemplar archive | None - Class 1+4 oracle uses inline data | -| Legacy comparison | Run against DREAM3D 6.5.171 with the inline test data for the Class 1+4 Oracle. Each slice matches exactly; D1 records filename formatting difference only. | -| Bug flags | Unsigned uint32/uint64 dispatch was corrected for this V&V cycle. | -| V&V phase | Ready for review | +| Legacy comparison | Run against DREAM3D 6.5.171 with the inline test data for the Class 1+4 Oracle. Decoded pixels match; D1 and D2 record filename-formatting and plane-metadata differences. | +| Bug flags | Unsigned uint32/uint64 dispatch was corrected for this V&V cycle. Current changes add invalid fill-character validation, component-count validation, RGBA dispatch, and selected-plane physical metadata. | +| V&V phase | Regression tests and report amendments in progress | ## Summary -ITKImageWriterFilter exports scalar ImageGeom cell data as an ITK image or a 2D image stack. A hand-derived non-square fixture verifies decoded pixel orientation independently of legacy, while the legacy comparison checks that migration preserves decoded output on the toy and production fixtures. +ITKImageWriterFilter exports ImageGeom cell data as an ITK image or a 2D image stack. A hand-derived non-square fixture verifies decoded pixel orientation and plane metadata independently of legacy, while the legacy comparison checks migration output on the toy and production fixtures. ## Algorithm Relationship -**Minor changes.** Port-time changes are SIMPLNX DataStructure stores, AtomicFile writes, and current ITK APIs; the pixel comparisons show no output change from these substitutions. +**Minor changes.** Port-time changes are SIMPLNX DataStructure stores, AtomicFile writes, and current ITK APIs. Scalar XY/XZ/YZ decoded pixels match the legacy behavior. The NX writer deliberately preserves the selected physical axes in XZ and YZ output spacing/origin; legacy output used identity 2D metadata (D2). The parameter set is unchanged, so `parametersVersion()` remains `2`; validation has been tightened without a parameter-schema change. **PR(s):** @@ -53,6 +53,7 @@ ITKImageWriterFilter exports scalar ImageGeom cell data as an ITK image or a 2D **2024** +- [PR #1088](https://github.com/BlueQuartzSoftware/simplnx/pull/1088) ("Added versioning to filter parameters and json") - Adds filter and parameter versioning, including backwards-compatible parameter JSON reading. - [PR #1082](https://github.com/BlueQuartzSoftware/simplnx/pull/1082) ("SIMPLConversion header optimization") - Optimizes SIMPL-conversion headers. - [PR #941](https://github.com/BlueQuartzSoftware/simplnx/pull/941) ("Moved Result handling outside of AtomicFile") - Moves result handling outside AtomicFile. - [PR #934](https://github.com/BlueQuartzSoftware/simplnx/pull/934) ("BUG: Pipeline and Filter human facing label cleanup") - Cleans up human-facing labels. @@ -86,36 +87,46 @@ ITKImageWriterFilter exports scalar ImageGeom cell data as an ITK image or a 2D **Class:** 1 (Analytical) + 4 (Invariant). -**Applied:** The 3x2x2 scalar fixture has `value(x,y,z)=x+10y+100z`; exact XY, XZ, and YZ pixel matrices are hand-derived and decoded for all ten accepted types. uint8 uses TIFF; the remaining types use MetaImage. File creation and decoded dimensions are companion invariants. +**Applied:** The 3x2x2 scalar fixture has `value(x,y,z)=x+10y+100z`; exact XY, XZ, and YZ pixel matrices are hand-derived and decoded for all ten accepted types. Its non-uniform origin `(10,20,40)` and spacing `(1,2,4)` verify that each written plane has the correct two physical axes: spacing is decoded for every type and origin for MetaImage output. TIFF does not preserve ITK origin metadata. File creation and decoded dimensions are companion invariants. -**Encoded:** `test/ITKImageWriterTest.cpp::ITKImageProcessing::ITKImageWriterFilter: Analytical TIFF Pixel Order` - templated over all ten accepted scalar types; all checks pass. +**Encoded:** `test/ITKImageWriterTest.cpp::ITKImageProcessing::ITKImageWriterFilter: Analytical Pixel Order` - templated over all ten accepted scalar types. **Second-engineer review:** pending second-engineer review. ## Code path coverage -17 of 18 branches exercised. Source: `src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp` (533 lines). +13 of 18 explicit paths exercised. Source: `src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp` (587 lines). | # | Path | Test case | |---|---|---| -| 1 | Valid preflight. | `Analytical TIFF Pixel Order`, `Write Stack` | +| 1 | Valid preflight. | `Analytical Pixel Order`, `Write Stack` | | 2 | Dimension-mismatch preflight error. | `Dimension Mismatch Validation` - a 1x1x1 array and 1x1x2 geometry return `-25600`. | -| 3 | Fill-character validation, including empty input. | `Fill Character Validation` - empty string returns `-25601`. | -| 4 | XY output for all accepted scalar types. | `Analytical TIFF Pixel Order` - same pixels; uint8 TIFF, other types MetaImage. | -| 5 | XZ output for all accepted scalar types. | `Analytical TIFF Pixel Order` - same pixels; uint8 TIFF, other types MetaImage. | -| 6 | YZ output for all accepted scalar types. | `Analytical TIFF Pixel Order` - same pixels; uint8 TIFF, other types MetaImage. | -| 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 | All accepted scalar dispatch arms: int8, uint8, int16, uint16, int32, uint32, int64, uint64, float32, and float64. | `Analytical TIFF Pixel Order` - same TIFF pixels for uint8 and MetaImage pixels for every other type. | -| 17 | Single-file output for a non-2D format. | `3D Image Single-File Output` - a 3x1x2 ImageGeom XZ plane writes one unsuffixed MetaImage file with exact decoded pixels. | -| 18 | Filesystem/ITK write failure. | *Not directly tested; requires failure injection.* | +| 3 | Empty fill-character validation. | `Fill Character Validation` - empty string returns `-25601`. | +| 4 | Invalid fill-character validation. | `Fill Character Validation` - `{` and `/` return `-25602`. | +| 5 | OOC input rejection. | *Not directly tested.* | +| 6 | Unsupported component-count rejection. | *Not directly tested.* | +| 7 | Preflight example-output-file value, including unsuffixed single-file output. | *Not directly tested.* | +| 8 | XY output pixels and physical metadata. | `Analytical Pixel Order` - hand-derived pixels and spacing `(1,2)` for all types, plus origin `(10,20)` for MetaImage output. | +| 9 | XZ output pixels and physical metadata. | `Analytical Pixel Order` - hand-derived pixels and spacing `(1,4)` for all types, plus origin `(10,40)` for MetaImage output. | +| 10 | YZ output pixels and physical metadata. | `Analytical Pixel Order` - hand-derived pixels and spacing `(2,4)` for all types, plus origin `(20,40)` for MetaImage output. | +| 11 | All accepted scalar dispatch arms: int8, uint8, int16, uint16, int32, uint32, int64, uint64, float32, and float64. | `Analytical Pixel Order` - TIFF pixels for uint8 and MetaImage pixels for every other type. | +| 12 | RGBA dispatch arm. | `RGBA Image Output` - one uint8 RGBA pixel decodes as `(10,20,30,40)`. | +| 13 | Cancellation between slices. | *Not directly tested.* | +| 14 | Filesystem/ITK write-failure propagation. | *Not directly tested; requires failure injection.* | +| 15 | Single-file output for a non-2D format. | `3D Image Single-File Output` - a 3x1x2 ImageGeom XZ plane writes one unsuffixed MetaImage file with exact decoded pixels. | +| 16 | Multi-file stack output. | `Write Stack`. | +| 17 | SIMPL JSON conversion with optional legacy parameters. | `SIMPL Backwards Compatibility`. | +| 18 | Atomic-file commit after a successful ITK write. | Covered by successful output tests; no injected commit failure. | ## Test inventory | Test case | Status | Notes | |---|---|---| -| `ITKImageProcessing::ITKImageWriterFilter: Analytical TIFF Pixel Order` | new-for-V&V | Tests Class 1+4 Oracle over all accepted types. | -| `ITKImageProcessing::ITKImageWriterFilter: Fill Character Validation` | new-for-V&V | Empty fill character is rejected during preflight with error `-25601`. | +| `ITKImageProcessing::ITKImageWriterFilter: Analytical Pixel Order` | new-for-V&V | Tests Class 1+4 Oracle over all accepted scalar types, including decoded XY/XZ/YZ pixels, spacing, and MetaImage origin. | +| `ITKImageProcessing::ITKImageWriterFilter: Fill Character Validation` | new-for-V&V | Empty fill character is rejected with `-25601`; format-control and path-separator characters are rejected with `-25602`. | | `ITKImageProcessing::ITKImageWriterFilter: Dimension Mismatch Validation` | new-for-V&V | Mismatched array and geometry input is rejected during preflight with error `-25600`. | | `ITKImageProcessing::ITKImageWriterFilter: 3D Image Single-File Output` | new-for-V&V | A 3x1x2 ImageGeom XZ plane writes an unsuffixed `.mha` file with exact decoded pixels. | +| `ITKImageProcessing::ITKImageWriterFilter: RGBA Image Output` | new-for-V&V | A uint8 RGBA pixel is dispatched and decodes as `(10,20,30,40)`. | | `ITKImageProcessing::ITKImageWriterFilter: Write Stack` | kept | Checks that an image is written as an image stack of files. | | `ITKImageProcessing::ITKImageWriterFilter: SIMPL Backwards Compatibility` | kept | Covers SIMPL json backwards compatibility. | @@ -126,5 +137,6 @@ No new exemplar archive was created for this V&V cycle: the Class 1 oracle is en ## Deviations from DREAM3D 6.5.171 - `ITKImageWriterFilter-D1` - NX defaults to zero-padded slice indices while 6.5.171 does not - see `vv/deviations/ITKImageWriterFilter.md`. +- `ITKImageWriterFilter-D2` - NX writes the selected plane's physical spacing and origin, while 6.5.171 used identity 2D metadata for XZ/YZ output - see `vv/deviations/ITKImageWriterFilter.md`. All pixels otherwise match: the analytical fixture has 2 XY, 2 XZ, and 3 YZ slices; Small IN100 has 117 XY, 201 XZ, and 189 YZ float32 TIFF slices. diff --git a/src/Plugins/ITKImageProcessing/vv/deviations/ITKImageWriterFilter.md b/src/Plugins/ITKImageProcessing/vv/deviations/ITKImageWriterFilter.md index 9b53efe756..d8c62d59e1 100644 --- a/src/Plugins/ITKImageProcessing/vv/deviations/ITKImageWriterFilter.md +++ b/src/Plugins/ITKImageProcessing/vv/deviations/ITKImageWriterFilter.md @@ -19,3 +19,19 @@ Entries are referenced by stable ID (ITKImageWriterFilter-D) from the V&V rep **Affected users:** Any workflow that consumes 2D image-stack filenames directly, rather than discovering them by pattern, can fail to find NX output after migration. **Recommendation:** Trust SIMPLNX. To retain the legacy filename shape, set *Index Offset* to `0` and *Total Number of Index Digits* to `1`. + +## ITKImageWriterFilter-D2 + +| Field | Value | +|---|---| +| **Deviation ID** | `ITKImageWriterFilter-D2` | +| **Filter UUID** | `a181ee3e-1678-4133-b9c5-a9dd7bfec62f` | +| **Status** | active | + +**Symptom:** For XZ and YZ stack output, NX writes the two selected physical axes as the 2D image spacing and origin. DREAM3D 6.5.171 writes identity spacing and zero origin for the newly created 2D image. + +**Root cause:** Library/API adaptation. NX constructs the temporary 2D geometry from the selected ImageGeom axes so that output metadata describes the written plane; the legacy implementation created a new 2D geometry with default metadata. + +**Affected users:** Workflows that consume XZ or YZ physical image metadata can observe different spacing and origin after migration. + +**Recommendation:** Trust SIMPLNX. The NX metadata preserves the source ImageGeom's physical coordinate system for the output plane. From e490a63a96c587e88b2f10c8516f98294c0e572b Mon Sep 17 00:00:00 2001 From: Jared Duffey Date: Thu, 30 Jul 2026 10:35:52 -0400 Subject: [PATCH 15/28] Updated testing comment Signed-off-by: Jared Duffey --- src/Plugins/ITKImageProcessing/vv/ITKImageWriterFilter.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Plugins/ITKImageProcessing/vv/ITKImageWriterFilter.md b/src/Plugins/ITKImageProcessing/vv/ITKImageWriterFilter.md index 4eee646bc2..adaa621e0a 100644 --- a/src/Plugins/ITKImageProcessing/vv/ITKImageWriterFilter.md +++ b/src/Plugins/ITKImageProcessing/vv/ITKImageWriterFilter.md @@ -103,7 +103,7 @@ ITKImageWriterFilter exports ImageGeom cell data as an ITK image or a 2D image s | 2 | Dimension-mismatch preflight error. | `Dimension Mismatch Validation` - a 1x1x1 array and 1x1x2 geometry return `-25600`. | | 3 | Empty fill-character validation. | `Fill Character Validation` - empty string returns `-25601`. | | 4 | Invalid fill-character validation. | `Fill Character Validation` - `{` and `/` return `-25602`. | -| 5 | OOC input rejection. | *Not directly tested.* | +| 5 | OOC input rejection. | *Not directly tested; not able to run on CI* | | 6 | Unsupported component-count rejection. | *Not directly tested.* | | 7 | Preflight example-output-file value, including unsuffixed single-file output. | *Not directly tested.* | | 8 | XY output pixels and physical metadata. | `Analytical Pixel Order` - hand-derived pixels and spacing `(1,2)` for all types, plus origin `(10,20)` for MetaImage output. | @@ -111,7 +111,7 @@ ITKImageWriterFilter exports ImageGeom cell data as an ITK image or a 2D image s | 10 | YZ output pixels and physical metadata. | `Analytical Pixel Order` - hand-derived pixels and spacing `(2,4)` for all types, plus origin `(20,40)` for MetaImage output. | | 11 | All accepted scalar dispatch arms: int8, uint8, int16, uint16, int32, uint32, int64, uint64, float32, and float64. | `Analytical Pixel Order` - TIFF pixels for uint8 and MetaImage pixels for every other type. | | 12 | RGBA dispatch arm. | `RGBA Image Output` - one uint8 RGBA pixel decodes as `(10,20,30,40)`. | -| 13 | Cancellation between slices. | *Not directly tested.* | +| 13 | Cancellation between slices. | *Not directly tested; requires cancel signal infrastructure* | | 14 | Filesystem/ITK write-failure propagation. | *Not directly tested; requires failure injection.* | | 15 | Single-file output for a non-2D format. | `3D Image Single-File Output` - a 3x1x2 ImageGeom XZ plane writes one unsuffixed MetaImage file with exact decoded pixels. | | 16 | Multi-file stack output. | `Write Stack`. | From 8bf33803406c8df51d72b81bd3dfba6dbd876707 Mon Sep 17 00:00:00 2001 From: Jared Duffey Date: Thu, 30 Jul 2026 10:43:53 -0400 Subject: [PATCH 16/28] Updated invalid fill characters Signed-off-by: Jared Duffey --- .../src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp | 5 +++-- src/Plugins/ITKImageProcessing/test/ITKImageWriterTest.cpp | 4 ++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp b/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp index 645463a9ef..1830ee98b8 100644 --- a/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp +++ b/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp @@ -49,10 +49,11 @@ bool IsValidComponentSize(usize componentSize) return std::find(k_AllowedComponentSizes.begin(), k_AllowedComponentSizes.end(), componentSize) != k_AllowedComponentSizes.end(); } +// Rejects 0 - 31 ASCII control characters bool IsValidFillCharacter(char fillCharacter) { - return fillCharacter != '{' && fillCharacter != '}' && fillCharacter != '\\' && fillCharacter != '/' && fillCharacter != ':' && fillCharacter != '*' && fillCharacter != '?' && - fillCharacter != '"' && fillCharacter != '<' && fillCharacter != '>' && fillCharacter != '|'; + return !(fillCharacter >= 0 && fillCharacter <= 31) && fillCharacter != '{' && fillCharacter != '}' && fillCharacter != '\\' && fillCharacter != '/' && fillCharacter != ':' && + fillCharacter != '*' && fillCharacter != '?' && fillCharacter != '"' && fillCharacter != '<' && fillCharacter != '>' && fillCharacter != '|'; } bool Is2DFormat(const fs::path& fileName) diff --git a/src/Plugins/ITKImageProcessing/test/ITKImageWriterTest.cpp b/src/Plugins/ITKImageProcessing/test/ITKImageWriterTest.cpp index 8b7735cac8..2a0b00cf8f 100644 --- a/src/Plugins/ITKImageProcessing/test/ITKImageWriterTest.cpp +++ b/src/Plugins/ITKImageProcessing/test/ITKImageWriterTest.cpp @@ -249,6 +249,10 @@ TEST_CASE("ITKImageProcessing::ITKImageWriterFilter: Fill Character Validation", { checkInvalidFillCharacter("/", -25602); } + SECTION("ASCII control character") + { + checkInvalidFillCharacter("\n", -25602); + } UnitTest::CheckArraysInheritTupleDims(dataStructure); } From 61d3b65e41f560b4bb6b753c99aa32e53c498194 Mon Sep 17 00:00:00 2001 From: Jared Duffey Date: Thu, 30 Jul 2026 10:57:21 -0400 Subject: [PATCH 17/28] Added check for preview filename Signed-off-by: Jared Duffey --- .../ITKImageProcessing/test/ITKImageWriterTest.cpp | 13 +++++++++++++ .../ITKImageProcessing/vv/ITKImageWriterFilter.md | 8 ++++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/Plugins/ITKImageProcessing/test/ITKImageWriterTest.cpp b/src/Plugins/ITKImageProcessing/test/ITKImageWriterTest.cpp index 2a0b00cf8f..6eea840fdc 100644 --- a/src/Plugins/ITKImageProcessing/test/ITKImageWriterTest.cpp +++ b/src/Plugins/ITKImageProcessing/test/ITKImageWriterTest.cpp @@ -82,6 +82,17 @@ void validateOutputFiles(size_t numImages, uint64 offset, const std::string& tem } } +bool RequireExampleOutputFile(const IFilter::PreflightResult& preflightResult, const fs::path& expectedFilePath) +{ + return std::find_if(preflightResult.outputValues.begin(), preflightResult.outputValues.end(), [expectedFilePath](const IFilter::PreflightValue& value) { + if(value.name == "Example Output File") + { + return fs::path(value.value).lexically_normal() == fs::absolute(expectedFilePath).lexically_normal(); + } + return false; + }) != preflightResult.outputValues.end(); +} + template void CompareImageToExpected(const fs::path& filePath, const std::array& expectedDimensions, const std::vector& expectedPixels) { @@ -323,6 +334,7 @@ TEST_CASE("ITKImageProcessing::ITKImageWriterFilter: 3D Image Single-File Output const auto preflightResult = filter.preflight(dataStructure, args); SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); + REQUIRE(RequireExampleOutputFile(preflightResult, outputPath)); const auto executeResult = filter.execute(dataStructure, args); SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); REQUIRE(fs::exists(outputPath)); @@ -442,6 +454,7 @@ TEST_CASE("ITKImageProcessing::ITKImageWriterFilter: Write Stack", "[ITKImagePro auto preflightResult = filter.preflight(dataStructure, args); SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions) + REQUIRE(RequireExampleOutputFile(preflightResult, outputPath.parent_path() / "slice_100.tif")); auto executeResult = filter.execute(dataStructure, args); SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result) diff --git a/src/Plugins/ITKImageProcessing/vv/ITKImageWriterFilter.md b/src/Plugins/ITKImageProcessing/vv/ITKImageWriterFilter.md index adaa621e0a..d303593ac2 100644 --- a/src/Plugins/ITKImageProcessing/vv/ITKImageWriterFilter.md +++ b/src/Plugins/ITKImageProcessing/vv/ITKImageWriterFilter.md @@ -15,7 +15,7 @@ |---|---| | Algorithm Relationship | Minor changes - same XY/XZ/YZ pixel extraction; NX uses SIMPLNX stores, AtomicFile, and current ITK APIs. Plane spacing and origin now follow the selected physical axes (D2). | | Oracle (confirmed) | Classes 1 + 4 - one 3x2x2 scalar fixture (`value(x,y,z)=x+10y+100z`) has same XY/XZ/YZ pixels for all ten accepted scalar types; it verifies selected-plane spacing for all types and origin for MetaImage output. uint8 uses TIFF and the remaining types use MetaImage. | -| Code paths enumerated | 13 of 18 explicit paths exercised; OOC, unsupported-component, preview-value, cancellation, and write-failure paths remain untested. | +| Code paths enumerated | 14 of 18 explicit paths exercised; OOC, unsupported-component, cancellation, and write-failure paths remain untested. | | Tests today | 7 named test cases - 1 Class 1+4 Oracle, 2 preflight error-path tests, 1 single-file output test, 1 RGBA output test, 1 stack-writing test, and 1 SIMPL backwards-compatibility test | | Exemplar archive | None - Class 1+4 oracle uses inline data | | Legacy comparison | Run against DREAM3D 6.5.171 with the inline test data for the Class 1+4 Oracle. Decoded pixels match; D1 and D2 record filename-formatting and plane-metadata differences. | @@ -105,7 +105,7 @@ ITKImageWriterFilter exports ImageGeom cell data as an ITK image or a 2D image s | 4 | Invalid fill-character validation. | `Fill Character Validation` - `{` and `/` return `-25602`. | | 5 | OOC input rejection. | *Not directly tested; not able to run on CI* | | 6 | Unsupported component-count rejection. | *Not directly tested.* | -| 7 | Preflight example-output-file value, including unsuffixed single-file output. | *Not directly tested.* | +| 7 | Preflight example-output-file value, including offset multi-slice and unsuffixed single-file output. | `Write Stack` and `3D Image Single-File Output`. | | 8 | XY output pixels and physical metadata. | `Analytical Pixel Order` - hand-derived pixels and spacing `(1,2)` for all types, plus origin `(10,20)` for MetaImage output. | | 9 | XZ output pixels and physical metadata. | `Analytical Pixel Order` - hand-derived pixels and spacing `(1,4)` for all types, plus origin `(10,40)` for MetaImage output. | | 10 | YZ output pixels and physical metadata. | `Analytical Pixel Order` - hand-derived pixels and spacing `(2,4)` for all types, plus origin `(20,40)` for MetaImage output. | @@ -125,9 +125,9 @@ ITKImageWriterFilter exports ImageGeom cell data as an ITK image or a 2D image s | `ITKImageProcessing::ITKImageWriterFilter: Analytical Pixel Order` | new-for-V&V | Tests Class 1+4 Oracle over all accepted scalar types, including decoded XY/XZ/YZ pixels, spacing, and MetaImage origin. | | `ITKImageProcessing::ITKImageWriterFilter: Fill Character Validation` | new-for-V&V | Empty fill character is rejected with `-25601`; format-control and path-separator characters are rejected with `-25602`. | | `ITKImageProcessing::ITKImageWriterFilter: Dimension Mismatch Validation` | new-for-V&V | Mismatched array and geometry input is rejected during preflight with error `-25600`. | -| `ITKImageProcessing::ITKImageWriterFilter: 3D Image Single-File Output` | new-for-V&V | A 3x1x2 ImageGeom XZ plane writes an unsuffixed `.mha` file with exact decoded pixels. | +| `ITKImageProcessing::ITKImageWriterFilter: 3D Image Single-File Output` | new-for-V&V | A 3x1x2 ImageGeom XZ plane previews and writes an unsuffixed `.mha` file with exact decoded pixels. | | `ITKImageProcessing::ITKImageWriterFilter: RGBA Image Output` | new-for-V&V | A uint8 RGBA pixel is dispatched and decodes as `(10,20,30,40)`. | -| `ITKImageProcessing::ITKImageWriterFilter: Write Stack` | kept | Checks that an image is written as an image stack of files. | +| `ITKImageProcessing::ITKImageWriterFilter: Write Stack` | kept | Checks that the preflight preview starts at the configured index offset and that the image is written as a stack of files. | | `ITKImageProcessing::ITKImageWriterFilter: SIMPL Backwards Compatibility` | kept | Covers SIMPL json backwards compatibility. | ## Exemplar archive From 565400e2b20afbacb98e7d4ac18b4dc1cf36ec5f Mon Sep 17 00:00:00 2001 From: Jared Duffey Date: Thu, 30 Jul 2026 11:08:24 -0400 Subject: [PATCH 18/28] Added error propogation for copying Signed-off-by: Jared Duffey --- .../Filters/ITKImageWriterFilter.cpp | 51 +++++++++---------- .../vv/ITKImageWriterFilter.md | 15 +++--- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp b/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp index 1830ee98b8..22611858bb 100644 --- a/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp +++ b/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp @@ -179,7 +179,7 @@ struct WriteImageFunctor }; template -void CopyTupleTyped(const IDataStore& currentData, IDataStore& sliceData, usize nComp, usize index, usize indexNew) +Result<> CopyTupleTyped(const IDataStore& currentData, IDataStore& sliceData, usize nComp, usize index, usize indexNew) { const auto& currentDataTyped = dynamic_cast&>(currentData); auto& sliceDataTyped = dynamic_cast&>(sliceData); @@ -190,10 +190,10 @@ void CopyTupleTyped(const IDataStore& currentData, IDataStore& sliceData, usize std::memcpy(destPtr, sourcePtr, currentData.getTypeSize() * nComp); #endif - sliceDataTyped.copyFrom(indexNew, currentDataTyped, index, 1); + return sliceDataTyped.copyFrom(indexNew, currentDataTyped, index, 1); } -void CopyTuple(usize index, usize axisA, usize dB, usize axisB, usize nComp, const IDataStore& currentData, IDataStore& sliceData) +Result<> CopyTuple(usize index, usize axisA, usize dB, usize axisB, usize nComp, const IDataStore& currentData, IDataStore& sliceData) { usize indexNew = (axisA * dB) + axisB; @@ -202,44 +202,34 @@ void CopyTuple(usize index, usize axisA, usize dB, usize axisB, usize nComp, con switch(type) { case DataType::int8: { - CopyTupleTyped(currentData, sliceData, nComp, index, indexNew); - break; + return CopyTupleTyped(currentData, sliceData, nComp, index, indexNew); } case DataType::uint8: { - CopyTupleTyped(currentData, sliceData, nComp, index, indexNew); - break; + return CopyTupleTyped(currentData, sliceData, nComp, index, indexNew); } case DataType::int16: { - CopyTupleTyped(currentData, sliceData, nComp, index, indexNew); - break; + return CopyTupleTyped(currentData, sliceData, nComp, index, indexNew); } case DataType::uint16: { - CopyTupleTyped(currentData, sliceData, nComp, index, indexNew); - break; + return CopyTupleTyped(currentData, sliceData, nComp, index, indexNew); } case DataType::int32: { - CopyTupleTyped(currentData, sliceData, nComp, index, indexNew); - break; + return CopyTupleTyped(currentData, sliceData, nComp, index, indexNew); } case DataType::uint32: { - CopyTupleTyped(currentData, sliceData, nComp, index, indexNew); - break; + return CopyTupleTyped(currentData, sliceData, nComp, index, indexNew); } case DataType::int64: { - CopyTupleTyped(currentData, sliceData, nComp, index, indexNew); - break; + return CopyTupleTyped(currentData, sliceData, nComp, index, indexNew); } case DataType::uint64: { - CopyTupleTyped(currentData, sliceData, nComp, index, indexNew); - break; + return CopyTupleTyped(currentData, sliceData, nComp, index, indexNew); } case DataType::float32: { - CopyTupleTyped(currentData, sliceData, nComp, index, indexNew); - break; + return CopyTupleTyped(currentData, sliceData, nComp, index, indexNew); } case DataType::float64: { - CopyTupleTyped(currentData, sliceData, nComp, index, indexNew); - break; + return CopyTupleTyped(currentData, sliceData, nComp, index, indexNew); } default: { throw std::runtime_error("ITKImageWriterFilter: Invalid DataType while attempting to copy tuples"); @@ -472,7 +462,10 @@ Result<> ITKImageWriterFilter::executeImpl(DataStructure& dataStructure, const A for(usize axisB = 0; axisB < dB; ++axisB) { usize index = (slice * dA * dB) + (axisA * dB) + axisB; - cxITKImageWriterFilter::CopyTuple(index, axisA, dB, axisB, nComp, currentData, *sliceData); + if(Result<> copyResult = cxITKImageWriterFilter::CopyTuple(index, axisA, dB, axisB, nComp, currentData, *sliceData); copyResult.invalid()) + { + return copyResult; + } } } Result<> result = cxITKImageWriterFilter::SaveImageData(filePath, *sliceData, newImageGeom, slice + indexOffset, dims.getZ(), indexOffset, totalDigits, fillChar); @@ -502,7 +495,10 @@ Result<> ITKImageWriterFilter::executeImpl(DataStructure& dataStructure, const A for(usize axisB = 0; axisB < dB; ++axisB) { usize index = (dims.getY() * axisA * dB) + (slice * dB) + axisB; - cxITKImageWriterFilter::CopyTuple(index, axisA, dB, axisB, nComp, currentData, *sliceData); + if(Result<> copyResult = cxITKImageWriterFilter::CopyTuple(index, axisA, dB, axisB, nComp, currentData, *sliceData); copyResult.invalid()) + { + return copyResult; + } } } Result<> result = cxITKImageWriterFilter::SaveImageData(filePath, *sliceData, newImageGeom, slice + indexOffset, dims.getY(), indexOffset, totalDigits, fillChar); @@ -532,7 +528,10 @@ Result<> ITKImageWriterFilter::executeImpl(DataStructure& dataStructure, const A for(usize axisB = 0; axisB < dB; ++axisB) { usize index = (dims.getX() * axisA * dB) + (axisB * dims.getX()) + slice; - cxITKImageWriterFilter::CopyTuple(index, axisA, dB, axisB, nComp, currentData, *sliceData); + if(Result<> copyResult = cxITKImageWriterFilter::CopyTuple(index, axisA, dB, axisB, nComp, currentData, *sliceData); copyResult.invalid()) + { + return copyResult; + } } } Result<> result = cxITKImageWriterFilter::SaveImageData(filePath, *sliceData, newImageGeom, slice + indexOffset, dims.getX(), indexOffset, totalDigits, fillChar); diff --git a/src/Plugins/ITKImageProcessing/vv/ITKImageWriterFilter.md b/src/Plugins/ITKImageProcessing/vv/ITKImageWriterFilter.md index d303593ac2..a01968d060 100644 --- a/src/Plugins/ITKImageProcessing/vv/ITKImageWriterFilter.md +++ b/src/Plugins/ITKImageProcessing/vv/ITKImageWriterFilter.md @@ -15,11 +15,11 @@ |---|---| | Algorithm Relationship | Minor changes - same XY/XZ/YZ pixel extraction; NX uses SIMPLNX stores, AtomicFile, and current ITK APIs. Plane spacing and origin now follow the selected physical axes (D2). | | Oracle (confirmed) | Classes 1 + 4 - one 3x2x2 scalar fixture (`value(x,y,z)=x+10y+100z`) has same XY/XZ/YZ pixels for all ten accepted scalar types; it verifies selected-plane spacing for all types and origin for MetaImage output. uint8 uses TIFF and the remaining types use MetaImage. | -| Code paths enumerated | 14 of 18 explicit paths exercised; OOC, unsupported-component, cancellation, and write-failure paths remain untested. | +| Code paths enumerated | 14 of 19 explicit paths exercised; OOC, unsupported-component, cancellation, write-failure, and defensive tuple-copy failure paths remain untested. | | Tests today | 7 named test cases - 1 Class 1+4 Oracle, 2 preflight error-path tests, 1 single-file output test, 1 RGBA output test, 1 stack-writing test, and 1 SIMPL backwards-compatibility test | | Exemplar archive | None - Class 1+4 oracle uses inline data | | Legacy comparison | Run against DREAM3D 6.5.171 with the inline test data for the Class 1+4 Oracle. Decoded pixels match; D1 and D2 record filename-formatting and plane-metadata differences. | -| Bug flags | Unsigned uint32/uint64 dispatch was corrected for this V&V cycle. Current changes add invalid fill-character validation, component-count validation, RGBA dispatch, and selected-plane physical metadata. | +| Bug flags | Unsigned uint32/uint64 dispatch was corrected for this V&V cycle. Current changes add invalid fill-character validation, component-count validation, RGBA dispatch, selected-plane physical metadata, and tuple-copy error propagation. | | V&V phase | Regression tests and report amendments in progress | ## Summary @@ -95,7 +95,7 @@ ITKImageWriterFilter exports ImageGeom cell data as an ITK image or a 2D image s ## Code path coverage -13 of 18 explicit paths exercised. Source: `src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp` (587 lines). +14 of 19 explicit paths exercised. Source: `src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp` (587 lines). | # | Path | Test case | |---|---|---| @@ -113,10 +113,11 @@ ITKImageWriterFilter exports ImageGeom cell data as an ITK image or a 2D image s | 12 | RGBA dispatch arm. | `RGBA Image Output` - one uint8 RGBA pixel decodes as `(10,20,30,40)`. | | 13 | Cancellation between slices. | *Not directly tested; requires cancel signal infrastructure* | | 14 | Filesystem/ITK write-failure propagation. | *Not directly tested; requires failure injection.* | -| 15 | Single-file output for a non-2D format. | `3D Image Single-File Output` - a 3x1x2 ImageGeom XZ plane writes one unsuffixed MetaImage file with exact decoded pixels. | -| 16 | Multi-file stack output. | `Write Stack`. | -| 17 | SIMPL JSON conversion with optional legacy parameters. | `SIMPL Backwards Compatibility`. | -| 18 | Atomic-file commit after a successful ITK write. | Covered by successful output tests; no injected commit failure. | +| 15 | Tuple-copy failure propagation. | *Not directly tested. This is a defensive check whose failure preconditions are excluded by construction: `sliceData` is created from the input store with the same tuple and component shapes, and preflight requires the input tuple dimensions to match the selected geometry.* | +| 16 | Single-file output for a non-2D format. | `3D Image Single-File Output` - a 3x1x2 ImageGeom XZ plane writes one unsuffixed MetaImage file with exact decoded pixels. | +| 17 | Multi-file stack output. | `Write Stack`. | +| 18 | SIMPL JSON conversion with optional legacy parameters. | `SIMPL Backwards Compatibility`. | +| 19 | Atomic-file commit after a successful ITK write. | Covered by successful output tests; no injected commit failure. | ## Test inventory From 86e1e310828256e11e5df977770207f1a74df135 Mon Sep 17 00:00:00 2001 From: Jared Duffey Date: Thu, 30 Jul 2026 11:29:24 -0400 Subject: [PATCH 19/28] Removed dead code Signed-off-by: Jared Duffey --- .../Filters/ITKImageWriterFilter.cpp | 98 +++---------------- .../test/ITKImageWriterTest.cpp | 2 +- .../vv/ITKImageWriterFilter.md | 10 +- 3 files changed, 17 insertions(+), 93 deletions(-) diff --git a/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp b/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp index 22611858bb..9a92c5d2eb 100644 --- a/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp +++ b/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp @@ -1,7 +1,6 @@ #include "ITKImageWriterFilter.hpp" #include "ITKImageProcessing/Common/ITKArrayHelper.hpp" -#include "ITKImageProcessing/ITKImageProcessingPlugin.hpp" #include "simplnx/Common/AtomicFile.hpp" #include "simplnx/DataStructure/DataPath.hpp" @@ -19,9 +18,6 @@ #include "simplnx/Utilities/StringUtilities.hpp" #include -#include -#include -#include #include @@ -56,16 +52,8 @@ bool IsValidFillCharacter(char fillCharacter) fillCharacter != '*' && fillCharacter != '?' && fillCharacter != '"' && fillCharacter != '<' && fillCharacter != '>' && fillCharacter != '|'; } -bool Is2DFormat(const fs::path& fileName) -{ - fs::path ext = fileName.extension(); - auto supported2DExtensions = ITKImageProcessingPlugin::GetList2DSupportedFileExtensions(); - auto iter = std::find(supported2DExtensions.cbegin(), supported2DExtensions.cend(), ext); - return iter != supported2DExtensions.cend(); -} - template -Result<> WriteAsOneFile(itk::Image& image, const fs::path& filePath /*, const IFilter::MessageHandler& messanger*/) +Result<> WriteAsOneFile(itk::Image& image, const fs::path& filePath) { auto atomicFileResult = AtomicFile::Create(filePath); if(atomicFileResult.invalid()) @@ -80,8 +68,6 @@ Result<> WriteAsOneFile(itk::Image& image, const fs::path& f using FileWriterType = itk::ImageFileWriter; auto writer = FileWriterType::New(); - // messanger(fmt::format("Saving {}", fileName)); - writer->SetInput(&image); writer->SetFileName(tempPath); writer->UseCompressionOn(); @@ -99,82 +85,21 @@ Result<> WriteAsOneFile(itk::Image& image, const fs::path& f return {}; } -template -Result<> WriteAs2DStack(itk::Image& image, uint32 z_size, const fs::path& filePath, uint64 indexOffset) -{ - // Create list of AtomicFiles - std::vector> atomicFiles; - std::vector fileNames; - - for(uint64 index = indexOffset; index < (z_size - 1); index++) - { - atomicFiles.push_back(AtomicFile::Create(fs::absolute(fmt::format("{}/{}{:03d}{}", filePath.parent_path().string(), filePath.stem().string(), index, filePath.extension().string())))); - auto& atomicFileResult = atomicFiles.back(); - if(atomicFileResult.invalid()) - { - return ConvertResult(std::move(atomicFileResult)); - } - fileNames.push_back(atomicFileResult.value().tempFilePath().string()); - } - - // generate all the files in that new directory - try - { - using InputImageType = itk::Image; - using OutputImageType = itk::Image; - using SeriesWriterType = itk::ImageSeriesWriter; - auto writer = SeriesWriterType::New(); - writer->SetInput(&image); - writer->SetFileNames(fileNames); - writer->UseCompressionOn(); - writer->Update(); - } catch(const itk::ExceptionObject& err) - { - return MakeErrorResult(-21011, fmt::format("ITK exception was thrown while writing output file: {}", err.GetDescription())); - } - - for(auto& atomicFile : atomicFiles) - { - Result<> commitResult = atomicFile.value().commit(); - if(commitResult.invalid()) - { - return commitResult; - } - } - - return {}; -} - template -Result<> WriteImage(IDataStore& dataStore, const ITK::ImageGeomData& imageGeom, const fs::path& filePath, uint64 indexOffset) +Result<> WriteImage(IDataStore& dataStore, const ITK::ImageGeomData& imageGeom, const fs::path& filePath) { - using ImageType = itk::Image; - auto& typedDataStore = dynamic_cast>&>(dataStore); typename itk::Image::Pointer image = ITK::WrapDataStoreInImage(typedDataStore, imageGeom); - if(Is2DFormat(filePath) && Dimensions == 3) - { - typename ImageType::SizeType size = image->GetLargestPossibleRegion().GetSize(); - if(size[2] < 2) - { - return MakeErrorResult(-21012, "Image is 2D, not 3D."); - } - - return WriteAs2DStack(*image, size[2], filePath, indexOffset); - } - else - { - return WriteAsOneFile(*image, filePath); - } + return WriteAsOneFile(*image, filePath); } template struct WriteImageFunctor { - Result<> operator()(IDataStore& dataStore, const ITK::ImageGeomData& imageGeom, const fs::path& filePath, uint64 indexOffset) const + Result<> operator()(IDataStore& dataStore, const ITK::ImageGeomData& imageGeom, const fs::path& filePath) const { - return WriteImage(dataStore, imageGeom, filePath, indexOffset); + return WriteImage(dataStore, imageGeom, filePath); } }; @@ -237,8 +162,7 @@ Result<> CopyTuple(usize index, usize axisA, usize dB, usize axisB, usize nComp, } } -Result<> SaveImageData(const fs::path& filePath, IDataStore& sliceData, const ITK::ImageGeomData& imageGeom, usize slice, usize maxSlice, uint64 indexOffset, int32 totalDigits, - const std::string& fillChar) +Result<> SaveImageData(const fs::path& filePath, IDataStore& sliceData, const ITK::ImageGeomData& imageGeom, usize slice, usize maxSlice, int32 totalDigits, const std::string& fillChar) { std::stringstream ss; ss << fs::absolute(filePath).parent_path().string() << "/" << filePath.stem().string(); @@ -262,9 +186,9 @@ Result<> SaveImageData(const fs::path& filePath, IDataStore& sliceData, const IT if(sliceData.getNumberOfComponents() == 4) { - return ITK::ArraySwitchFunc(sliceData, imageGeom, -21010, sliceData, imageGeom, fileName, indexOffset); + return ITK::ArraySwitchFunc(sliceData, imageGeom, -21010, sliceData, imageGeom, fileName); } - return ITK::ArraySwitchFunc(sliceData, imageGeom, -21010, sliceData, imageGeom, fileName, indexOffset); + return ITK::ArraySwitchFunc(sliceData, imageGeom, -21010, sliceData, imageGeom, fileName); } } // namespace cxITKImageWriterFilter @@ -468,7 +392,7 @@ Result<> ITKImageWriterFilter::executeImpl(DataStructure& dataStructure, const A } } } - Result<> result = cxITKImageWriterFilter::SaveImageData(filePath, *sliceData, newImageGeom, slice + indexOffset, dims.getZ(), indexOffset, totalDigits, fillChar); + Result<> result = cxITKImageWriterFilter::SaveImageData(filePath, *sliceData, newImageGeom, slice + indexOffset, dims.getZ(), totalDigits, fillChar); if(result.invalid()) { return result; @@ -501,7 +425,7 @@ Result<> ITKImageWriterFilter::executeImpl(DataStructure& dataStructure, const A } } } - Result<> result = cxITKImageWriterFilter::SaveImageData(filePath, *sliceData, newImageGeom, slice + indexOffset, dims.getY(), indexOffset, totalDigits, fillChar); + Result<> result = cxITKImageWriterFilter::SaveImageData(filePath, *sliceData, newImageGeom, slice + indexOffset, dims.getY(), totalDigits, fillChar); if(result.invalid()) { return result; @@ -534,7 +458,7 @@ Result<> ITKImageWriterFilter::executeImpl(DataStructure& dataStructure, const A } } } - Result<> result = cxITKImageWriterFilter::SaveImageData(filePath, *sliceData, newImageGeom, slice + indexOffset, dims.getX(), indexOffset, totalDigits, fillChar); + Result<> result = cxITKImageWriterFilter::SaveImageData(filePath, *sliceData, newImageGeom, slice + indexOffset, dims.getX(), totalDigits, fillChar); if(result.invalid()) { return result; diff --git a/src/Plugins/ITKImageProcessing/test/ITKImageWriterTest.cpp b/src/Plugins/ITKImageProcessing/test/ITKImageWriterTest.cpp index 6eea840fdc..d219e67c84 100644 --- a/src/Plugins/ITKImageProcessing/test/ITKImageWriterTest.cpp +++ b/src/Plugins/ITKImageProcessing/test/ITKImageWriterTest.cpp @@ -295,7 +295,7 @@ TEST_CASE("ITKImageProcessing::ITKImageWriterFilter: Dimension Mismatch Validati UnitTest::CheckArraysInheritTupleDims(dataStructure); } -TEST_CASE("ITKImageProcessing::ITKImageWriterFilter: 3D Image Single-File Output", "[ITKImageProcessing][ITKImageWriterFilter]") +TEST_CASE("ITKImageProcessing::ITKImageWriterFilter: Single Slice Keeps Exact Output Name", "[ITKImageProcessing][ITKImageWriterFilter]") { auto app = Application::GetOrCreateInstance(); UnitTest::LoadPlugins(); diff --git a/src/Plugins/ITKImageProcessing/vv/ITKImageWriterFilter.md b/src/Plugins/ITKImageProcessing/vv/ITKImageWriterFilter.md index a01968d060..d5635c2ec8 100644 --- a/src/Plugins/ITKImageProcessing/vv/ITKImageWriterFilter.md +++ b/src/Plugins/ITKImageProcessing/vv/ITKImageWriterFilter.md @@ -16,7 +16,7 @@ | Algorithm Relationship | Minor changes - same XY/XZ/YZ pixel extraction; NX uses SIMPLNX stores, AtomicFile, and current ITK APIs. Plane spacing and origin now follow the selected physical axes (D2). | | Oracle (confirmed) | Classes 1 + 4 - one 3x2x2 scalar fixture (`value(x,y,z)=x+10y+100z`) has same XY/XZ/YZ pixels for all ten accepted scalar types; it verifies selected-plane spacing for all types and origin for MetaImage output. uint8 uses TIFF and the remaining types use MetaImage. | | Code paths enumerated | 14 of 19 explicit paths exercised; OOC, unsupported-component, cancellation, write-failure, and defensive tuple-copy failure paths remain untested. | -| Tests today | 7 named test cases - 1 Class 1+4 Oracle, 2 preflight error-path tests, 1 single-file output test, 1 RGBA output test, 1 stack-writing test, and 1 SIMPL backwards-compatibility test | +| Tests today | 7 named test cases - 1 Class 1+4 Oracle, 2 preflight error-path tests, 1 single-slice exact-name test, 1 RGBA output test, 1 stack-writing test, and 1 SIMPL backwards-compatibility test | | Exemplar archive | None - Class 1+4 oracle uses inline data | | Legacy comparison | Run against DREAM3D 6.5.171 with the inline test data for the Class 1+4 Oracle. Decoded pixels match; D1 and D2 record filename-formatting and plane-metadata differences. | | Bug flags | Unsigned uint32/uint64 dispatch was corrected for this V&V cycle. Current changes add invalid fill-character validation, component-count validation, RGBA dispatch, selected-plane physical metadata, and tuple-copy error propagation. | @@ -95,7 +95,7 @@ ITKImageWriterFilter exports ImageGeom cell data as an ITK image or a 2D image s ## Code path coverage -14 of 19 explicit paths exercised. Source: `src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp` (587 lines). +14 of 19 explicit paths exercised. Source: `src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp` (511 lines). | # | Path | Test case | |---|---|---| @@ -105,7 +105,7 @@ ITKImageWriterFilter exports ImageGeom cell data as an ITK image or a 2D image s | 4 | Invalid fill-character validation. | `Fill Character Validation` - `{` and `/` return `-25602`. | | 5 | OOC input rejection. | *Not directly tested; not able to run on CI* | | 6 | Unsupported component-count rejection. | *Not directly tested.* | -| 7 | Preflight example-output-file value, including offset multi-slice and unsuffixed single-file output. | `Write Stack` and `3D Image Single-File Output`. | +| 7 | Preflight example-output-file value, including offset multi-slice and unsuffixed single-slice output. | `Write Stack` and `Single Slice Keeps Exact Output Name`. | | 8 | XY output pixels and physical metadata. | `Analytical Pixel Order` - hand-derived pixels and spacing `(1,2)` for all types, plus origin `(10,20)` for MetaImage output. | | 9 | XZ output pixels and physical metadata. | `Analytical Pixel Order` - hand-derived pixels and spacing `(1,4)` for all types, plus origin `(10,40)` for MetaImage output. | | 10 | YZ output pixels and physical metadata. | `Analytical Pixel Order` - hand-derived pixels and spacing `(2,4)` for all types, plus origin `(20,40)` for MetaImage output. | @@ -114,7 +114,7 @@ ITKImageWriterFilter exports ImageGeom cell data as an ITK image or a 2D image s | 13 | Cancellation between slices. | *Not directly tested; requires cancel signal infrastructure* | | 14 | Filesystem/ITK write-failure propagation. | *Not directly tested; requires failure injection.* | | 15 | Tuple-copy failure propagation. | *Not directly tested. This is a defensive check whose failure preconditions are excluded by construction: `sliceData` is created from the input store with the same tuple and component shapes, and preflight requires the input tuple dimensions to match the selected geometry.* | -| 16 | Single-file output for a non-2D format. | `3D Image Single-File Output` - a 3x1x2 ImageGeom XZ plane writes one unsuffixed MetaImage file with exact decoded pixels. | +| 16 | Single-slice output keeps the exact output name. | `Single Slice Keeps Exact Output Name` - `maxSlice == 1` suppresses the index suffix, and a 3x1x2 ImageGeom XZ plane writes one unsuffixed MetaImage file with exact decoded pixels. | | 17 | Multi-file stack output. | `Write Stack`. | | 18 | SIMPL JSON conversion with optional legacy parameters. | `SIMPL Backwards Compatibility`. | | 19 | Atomic-file commit after a successful ITK write. | Covered by successful output tests; no injected commit failure. | @@ -126,7 +126,7 @@ ITKImageWriterFilter exports ImageGeom cell data as an ITK image or a 2D image s | `ITKImageProcessing::ITKImageWriterFilter: Analytical Pixel Order` | new-for-V&V | Tests Class 1+4 Oracle over all accepted scalar types, including decoded XY/XZ/YZ pixels, spacing, and MetaImage origin. | | `ITKImageProcessing::ITKImageWriterFilter: Fill Character Validation` | new-for-V&V | Empty fill character is rejected with `-25601`; format-control and path-separator characters are rejected with `-25602`. | | `ITKImageProcessing::ITKImageWriterFilter: Dimension Mismatch Validation` | new-for-V&V | Mismatched array and geometry input is rejected during preflight with error `-25600`. | -| `ITKImageProcessing::ITKImageWriterFilter: 3D Image Single-File Output` | new-for-V&V | A 3x1x2 ImageGeom XZ plane previews and writes an unsuffixed `.mha` file with exact decoded pixels. | +| `ITKImageProcessing::ITKImageWriterFilter: Single Slice Keeps Exact Output Name` | new-for-V&V | A 3x1x2 ImageGeom XZ plane previews and writes an unsuffixed `.mha` file with exact decoded pixels. | | `ITKImageProcessing::ITKImageWriterFilter: RGBA Image Output` | new-for-V&V | A uint8 RGBA pixel is dispatched and decodes as `(10,20,30,40)`. | | `ITKImageProcessing::ITKImageWriterFilter: Write Stack` | kept | Checks that the preflight preview starts at the configured index offset and that the image is written as a stack of files. | | `ITKImageProcessing::ITKImageWriterFilter: SIMPL Backwards Compatibility` | kept | Covers SIMPL json backwards compatibility. | From f45c636dbe19e1a285f947c58e782ab00c4a901c Mon Sep 17 00:00:00 2001 From: Jared Duffey Date: Thu, 30 Jul 2026 11:53:27 -0400 Subject: [PATCH 20/28] Added slice progress message Signed-off-by: Jared Duffey --- .../Filters/ITKImageWriterFilter.cpp | 37 ++++++++++++------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp b/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp index 9a92c5d2eb..7ee21651fb 100644 --- a/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp +++ b/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp @@ -162,27 +162,30 @@ Result<> CopyTuple(usize index, usize axisA, usize dB, usize axisB, usize nComp, } } -Result<> SaveImageData(const fs::path& filePath, IDataStore& sliceData, const ITK::ImageGeomData& imageGeom, usize slice, usize maxSlice, int32 totalDigits, const std::string& fillChar) +fs::path GenerateOutputFilePath(const fs::path& filePath, usize slice, usize maxSlice, int32 totalDigits, const std::string& fillChar) { std::stringstream ss; ss << fs::absolute(filePath).parent_path().string() << "/" << filePath.stem().string(); - // If the parent path does not exist then try to create it. - if(!fs::exists(fs::absolute(filePath).parent_path())) - { - if(!fs::create_directories(fs::absolute(filePath).parent_path())) - { - return MakeErrorResult(-19000, fmt::format("Error Creating output path for image '{}'", fs::absolute(filePath).string())); - } - } - if(maxSlice != 1) { ss << "_" << std::setw(totalDigits) << std::setfill(fillChar[0]) << slice; } ss << filePath.extension().string(); - auto fileName = fs::path(ss.str()); + return fs::path(ss.str()); +} + +Result<> SaveImageData(const fs::path& fileName, IDataStore& sliceData, const ITK::ImageGeomData& imageGeom) +{ + // If the parent path does not exist then try to create it. + if(!fs::exists(fileName.parent_path())) + { + if(!fs::create_directories(fileName.parent_path())) + { + return MakeErrorResult(-19000, fmt::format("Error Creating output path for image '{}'", fileName.string())); + } + } if(sliceData.getNumberOfComponents() == 4) { @@ -392,7 +395,9 @@ Result<> ITKImageWriterFilter::executeImpl(DataStructure& dataStructure, const A } } } - Result<> result = cxITKImageWriterFilter::SaveImageData(filePath, *sliceData, newImageGeom, slice + indexOffset, dims.getZ(), totalDigits, fillChar); + const fs::path outputFilePath = cxITKImageWriterFilter::GenerateOutputFilePath(filePath, slice + indexOffset, dims.getZ(), totalDigits, fillChar); + messageHandler(fmt::format("Writing file {} of {}: \"{}\"", slice + 1, dims.getZ(), outputFilePath.string())); + Result<> result = cxITKImageWriterFilter::SaveImageData(outputFilePath, *sliceData, newImageGeom); if(result.invalid()) { return result; @@ -425,7 +430,9 @@ Result<> ITKImageWriterFilter::executeImpl(DataStructure& dataStructure, const A } } } - Result<> result = cxITKImageWriterFilter::SaveImageData(filePath, *sliceData, newImageGeom, slice + indexOffset, dims.getY(), totalDigits, fillChar); + const fs::path outputFilePath = cxITKImageWriterFilter::GenerateOutputFilePath(filePath, slice + indexOffset, dims.getY(), totalDigits, fillChar); + messageHandler(fmt::format("Writing file {} of {}: \"{}\"", slice + 1, dims.getY(), outputFilePath.string())); + Result<> result = cxITKImageWriterFilter::SaveImageData(outputFilePath, *sliceData, newImageGeom); if(result.invalid()) { return result; @@ -458,7 +465,9 @@ Result<> ITKImageWriterFilter::executeImpl(DataStructure& dataStructure, const A } } } - Result<> result = cxITKImageWriterFilter::SaveImageData(filePath, *sliceData, newImageGeom, slice + indexOffset, dims.getX(), totalDigits, fillChar); + const fs::path outputFilePath = cxITKImageWriterFilter::GenerateOutputFilePath(filePath, slice + indexOffset, dims.getX(), totalDigits, fillChar); + messageHandler(fmt::format("Writing file {} of {}: \"{}\"", slice + 1, dims.getX(), outputFilePath.string())); + Result<> result = cxITKImageWriterFilter::SaveImageData(outputFilePath, *sliceData, newImageGeom); if(result.invalid()) { return result; From 3ec3137653be5f0706b818413d452aaa823525ef Mon Sep 17 00:00:00 2001 From: Jared Duffey Date: Thu, 30 Jul 2026 12:03:57 -0400 Subject: [PATCH 21/28] Removed unused variable Signed-off-by: Jared Duffey --- .../Filters/ITKImageWriterFilter.cpp | 37 ++++++++----------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp b/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp index 7ee21651fb..77ac42d8ee 100644 --- a/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp +++ b/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp @@ -104,21 +104,15 @@ struct WriteImageFunctor }; template -Result<> CopyTupleTyped(const IDataStore& currentData, IDataStore& sliceData, usize nComp, usize index, usize indexNew) +Result<> CopyTupleTyped(const IDataStore& currentData, IDataStore& sliceData, usize index, usize indexNew) { const auto& currentDataTyped = dynamic_cast&>(currentData); auto& sliceDataTyped = dynamic_cast&>(sliceData); -#if 0 - const T* sourcePtr = currentDataTyped.data() + (nComp * index); - T* destPtr = sliceDataTyped.data() + (nComp * indexNew); - std::memcpy(destPtr, sourcePtr, currentData.getTypeSize() * nComp); -#endif - return sliceDataTyped.copyFrom(indexNew, currentDataTyped, index, 1); } -Result<> CopyTuple(usize index, usize axisA, usize dB, usize axisB, usize nComp, const IDataStore& currentData, IDataStore& sliceData) +Result<> CopyTuple(usize index, usize axisA, usize dB, usize axisB, const IDataStore& currentData, IDataStore& sliceData) { usize indexNew = (axisA * dB) + axisB; @@ -127,34 +121,34 @@ Result<> CopyTuple(usize index, usize axisA, usize dB, usize axisB, usize nComp, switch(type) { case DataType::int8: { - return CopyTupleTyped(currentData, sliceData, nComp, index, indexNew); + return CopyTupleTyped(currentData, sliceData, index, indexNew); } case DataType::uint8: { - return CopyTupleTyped(currentData, sliceData, nComp, index, indexNew); + return CopyTupleTyped(currentData, sliceData, index, indexNew); } case DataType::int16: { - return CopyTupleTyped(currentData, sliceData, nComp, index, indexNew); + return CopyTupleTyped(currentData, sliceData, index, indexNew); } case DataType::uint16: { - return CopyTupleTyped(currentData, sliceData, nComp, index, indexNew); + return CopyTupleTyped(currentData, sliceData, index, indexNew); } case DataType::int32: { - return CopyTupleTyped(currentData, sliceData, nComp, index, indexNew); + return CopyTupleTyped(currentData, sliceData, index, indexNew); } case DataType::uint32: { - return CopyTupleTyped(currentData, sliceData, nComp, index, indexNew); + return CopyTupleTyped(currentData, sliceData, index, indexNew); } case DataType::int64: { - return CopyTupleTyped(currentData, sliceData, nComp, index, indexNew); + return CopyTupleTyped(currentData, sliceData, index, indexNew); } case DataType::uint64: { - return CopyTupleTyped(currentData, sliceData, nComp, index, indexNew); + return CopyTupleTyped(currentData, sliceData, index, indexNew); } case DataType::float32: { - return CopyTupleTyped(currentData, sliceData, nComp, index, indexNew); + return CopyTupleTyped(currentData, sliceData, index, indexNew); } case DataType::float64: { - return CopyTupleTyped(currentData, sliceData, nComp, index, indexNew); + return CopyTupleTyped(currentData, sliceData, index, indexNew); } default: { throw std::runtime_error("ITKImageWriterFilter: Invalid DataType while attempting to copy tuples"); @@ -358,7 +352,6 @@ Result<> ITKImageWriterFilter::executeImpl(DataStructure& dataStructure, const A SizeVec3 dims = imageGeom.getDimensions(); const auto& imageArray = dataStructure.getDataRefAs(imageArrayPath); - usize nComp = imageArray.getNumberOfComponents(); const IDataStore& currentData = imageArray.getIDataStoreRef(); std::unique_ptr sliceData = currentData.createNewInstance(); @@ -389,7 +382,7 @@ Result<> ITKImageWriterFilter::executeImpl(DataStructure& dataStructure, const A for(usize axisB = 0; axisB < dB; ++axisB) { usize index = (slice * dA * dB) + (axisA * dB) + axisB; - if(Result<> copyResult = cxITKImageWriterFilter::CopyTuple(index, axisA, dB, axisB, nComp, currentData, *sliceData); copyResult.invalid()) + if(Result<> copyResult = cxITKImageWriterFilter::CopyTuple(index, axisA, dB, axisB, currentData, *sliceData); copyResult.invalid()) { return copyResult; } @@ -424,7 +417,7 @@ Result<> ITKImageWriterFilter::executeImpl(DataStructure& dataStructure, const A for(usize axisB = 0; axisB < dB; ++axisB) { usize index = (dims.getY() * axisA * dB) + (slice * dB) + axisB; - if(Result<> copyResult = cxITKImageWriterFilter::CopyTuple(index, axisA, dB, axisB, nComp, currentData, *sliceData); copyResult.invalid()) + if(Result<> copyResult = cxITKImageWriterFilter::CopyTuple(index, axisA, dB, axisB, currentData, *sliceData); copyResult.invalid()) { return copyResult; } @@ -459,7 +452,7 @@ Result<> ITKImageWriterFilter::executeImpl(DataStructure& dataStructure, const A for(usize axisB = 0; axisB < dB; ++axisB) { usize index = (dims.getX() * axisA * dB) + (axisB * dims.getX()) + slice; - if(Result<> copyResult = cxITKImageWriterFilter::CopyTuple(index, axisA, dB, axisB, nComp, currentData, *sliceData); copyResult.invalid()) + if(Result<> copyResult = cxITKImageWriterFilter::CopyTuple(index, axisA, dB, axisB, currentData, *sliceData); copyResult.invalid()) { return copyResult; } From 58421ee197c6ee2d90e2ae4ac82f0a0bb1a4bc10 Mon Sep 17 00:00:00 2001 From: Jared Duffey Date: Thu, 30 Jul 2026 12:29:19 -0400 Subject: [PATCH 22/28] Now cast data store once per slice instead of once per tuple Signed-off-by: Jared Duffey --- .../Filters/ITKImageWriterFilter.cpp | 161 +++++++++--------- 1 file changed, 81 insertions(+), 80 deletions(-) diff --git a/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp b/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp index 77ac42d8ee..59d871862f 100644 --- a/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp +++ b/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp @@ -14,6 +14,7 @@ #include "simplnx/Parameters/GeometrySelectionParameter.hpp" #include "simplnx/Parameters/NumberParameter.hpp" #include "simplnx/Parameters/StringParameter.hpp" +#include "simplnx/Utilities/FilterUtilities.hpp" #include "simplnx/Utilities/ImageIO/ImageIOUtilities.hpp" #include "simplnx/Utilities/StringUtilities.hpp" @@ -103,59 +104,6 @@ struct WriteImageFunctor } }; -template -Result<> CopyTupleTyped(const IDataStore& currentData, IDataStore& sliceData, usize index, usize indexNew) -{ - const auto& currentDataTyped = dynamic_cast&>(currentData); - auto& sliceDataTyped = dynamic_cast&>(sliceData); - - return sliceDataTyped.copyFrom(indexNew, currentDataTyped, index, 1); -} - -Result<> CopyTuple(usize index, usize axisA, usize dB, usize axisB, const IDataStore& currentData, IDataStore& sliceData) -{ - usize indexNew = (axisA * dB) + axisB; - - DataType type = currentData.getDataType(); - - switch(type) - { - case DataType::int8: { - return CopyTupleTyped(currentData, sliceData, index, indexNew); - } - case DataType::uint8: { - return CopyTupleTyped(currentData, sliceData, index, indexNew); - } - case DataType::int16: { - return CopyTupleTyped(currentData, sliceData, index, indexNew); - } - case DataType::uint16: { - return CopyTupleTyped(currentData, sliceData, index, indexNew); - } - case DataType::int32: { - return CopyTupleTyped(currentData, sliceData, index, indexNew); - } - case DataType::uint32: { - return CopyTupleTyped(currentData, sliceData, index, indexNew); - } - case DataType::int64: { - return CopyTupleTyped(currentData, sliceData, index, indexNew); - } - case DataType::uint64: { - return CopyTupleTyped(currentData, sliceData, index, indexNew); - } - case DataType::float32: { - return CopyTupleTyped(currentData, sliceData, index, indexNew); - } - case DataType::float64: { - return CopyTupleTyped(currentData, sliceData, index, indexNew); - } - default: { - throw std::runtime_error("ITKImageWriterFilter: Invalid DataType while attempting to copy tuples"); - } - } -} - fs::path GenerateOutputFilePath(const fs::path& filePath, usize slice, usize maxSlice, int32 totalDigits, const std::string& fillChar) { std::stringstream ss; @@ -187,6 +135,79 @@ Result<> SaveImageData(const fs::path& fileName, IDataStore& sliceData, const IT } return ITK::ArraySwitchFunc(sliceData, imageGeom, -21010, sliceData, imageGeom, fileName); } + +struct CopyXYSlicesFunctor +{ + template + Result<> operator()(usize dA, usize dB, usize slice, const IDataStore& currentData, IDataStore& sliceData) const + { + const auto& currentDataTyped = dynamic_cast&>(currentData); + auto& sliceDataTyped = dynamic_cast&>(sliceData); + + for(usize axisA = 0; axisA < dA; ++axisA) + { + for(usize axisB = 0; axisB < dB; ++axisB) + { + usize index = (slice * dA * dB) + (axisA * dB) + axisB; + usize indexNew = (axisA * dB) + axisB; + if(Result<> copyResult = sliceDataTyped.copyFrom(indexNew, currentDataTyped, index, 1); copyResult.invalid()) + { + return copyResult; + } + } + } + + return {}; + } +}; + +struct CopyXZSlicesFunctor +{ + template + Result<> operator()(usize dA, usize dB, usize slice, const SizeVec3& dims, const IDataStore& currentData, IDataStore& sliceData) const + { + const auto& currentDataTyped = dynamic_cast&>(currentData); + auto& sliceDataTyped = dynamic_cast&>(sliceData); + + for(usize axisA = 0; axisA < dA; ++axisA) + { + for(usize axisB = 0; axisB < dB; ++axisB) + { + usize index = (dims.getY() * axisA * dB) + (slice * dB) + axisB; + usize indexNew = (axisA * dB) + axisB; + if(Result<> copyResult = sliceDataTyped.copyFrom(indexNew, currentDataTyped, index, 1); copyResult.invalid()) + { + return copyResult; + } + } + } + return {}; + } +}; + +struct CopyYZSlicesFunctor +{ + template + Result<> operator()(usize dA, usize dB, usize slice, const SizeVec3& dims, const IDataStore& currentData, IDataStore& sliceData) const + { + const auto& currentDataTyped = dynamic_cast&>(currentData); + auto& sliceDataTyped = dynamic_cast&>(sliceData); + + for(usize axisA = 0; axisA < dA; ++axisA) + { + for(usize axisB = 0; axisB < dB; ++axisB) + { + usize index = (dims.getX() * axisA * dB) + (axisB * dims.getX()) + slice; + usize indexNew = (axisA * dB) + axisB; + if(Result<> copyResult = sliceDataTyped.copyFrom(indexNew, currentDataTyped, index, 1); copyResult.invalid()) + { + return copyResult; + } + } + } + return {}; + } +}; } // namespace cxITKImageWriterFilter namespace nx::core @@ -355,6 +376,7 @@ Result<> ITKImageWriterFilter::executeImpl(DataStructure& dataStructure, const A const IDataStore& currentData = imageArray.getIDataStoreRef(); std::unique_ptr sliceData = currentData.createNewInstance(); + DataType dataType = currentData.getDataType(); ITK::ImageGeomData newImageGeom(imageGeom); @@ -377,16 +399,9 @@ Result<> ITKImageWriterFilter::executeImpl(DataStructure& dataStructure, const A { return {}; } - for(usize axisA = 0; axisA < dA; ++axisA) + if(Result<> copyResult = ExecuteDataFunctionNoBool(cxITKImageWriterFilter::CopyXYSlicesFunctor{}, dataType, dA, dB, slice, currentData, *sliceData); copyResult.invalid()) { - for(usize axisB = 0; axisB < dB; ++axisB) - { - usize index = (slice * dA * dB) + (axisA * dB) + axisB; - if(Result<> copyResult = cxITKImageWriterFilter::CopyTuple(index, axisA, dB, axisB, currentData, *sliceData); copyResult.invalid()) - { - return copyResult; - } - } + return copyResult; } const fs::path outputFilePath = cxITKImageWriterFilter::GenerateOutputFilePath(filePath, slice + indexOffset, dims.getZ(), totalDigits, fillChar); messageHandler(fmt::format("Writing file {} of {}: \"{}\"", slice + 1, dims.getZ(), outputFilePath.string())); @@ -412,16 +427,9 @@ Result<> ITKImageWriterFilter::executeImpl(DataStructure& dataStructure, const A { return {}; } - for(usize axisA = 0; axisA < dA; ++axisA) + if(Result<> copyResult = ExecuteDataFunctionNoBool(cxITKImageWriterFilter::CopyXZSlicesFunctor{}, dataType, dA, dB, slice, dims, currentData, *sliceData); copyResult.invalid()) { - for(usize axisB = 0; axisB < dB; ++axisB) - { - usize index = (dims.getY() * axisA * dB) + (slice * dB) + axisB; - if(Result<> copyResult = cxITKImageWriterFilter::CopyTuple(index, axisA, dB, axisB, currentData, *sliceData); copyResult.invalid()) - { - return copyResult; - } - } + return copyResult; } const fs::path outputFilePath = cxITKImageWriterFilter::GenerateOutputFilePath(filePath, slice + indexOffset, dims.getY(), totalDigits, fillChar); messageHandler(fmt::format("Writing file {} of {}: \"{}\"", slice + 1, dims.getY(), outputFilePath.string())); @@ -447,16 +455,9 @@ Result<> ITKImageWriterFilter::executeImpl(DataStructure& dataStructure, const A { return {}; } - for(usize axisA = 0; axisA < dA; ++axisA) + if(Result<> copyResult = ExecuteDataFunctionNoBool(cxITKImageWriterFilter::CopyYZSlicesFunctor{}, dataType, dA, dB, slice, dims, currentData, *sliceData); copyResult.invalid()) { - for(usize axisB = 0; axisB < dB; ++axisB) - { - usize index = (dims.getX() * axisA * dB) + (axisB * dims.getX()) + slice; - if(Result<> copyResult = cxITKImageWriterFilter::CopyTuple(index, axisA, dB, axisB, currentData, *sliceData); copyResult.invalid()) - { - return copyResult; - } - } + return copyResult; } const fs::path outputFilePath = cxITKImageWriterFilter::GenerateOutputFilePath(filePath, slice + indexOffset, dims.getX(), totalDigits, fillChar); messageHandler(fmt::format("Writing file {} of {}: \"{}\"", slice + 1, dims.getX(), outputFilePath.string())); From c6a084b1ebdcd7b1517e23200d2e894780816a0c Mon Sep 17 00:00:00 2001 From: Jared Duffey Date: Thu, 30 Jul 2026 12:51:49 -0400 Subject: [PATCH 23/28] Only allocate slice size not full Signed-off-by: Jared Duffey --- .../Filters/ITKImageWriterFilter.cpp | 21 +++++++++++++++++-- .../vv/ITKImageWriterFilter.md | 2 +- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp b/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp index 59d871862f..958dcd1357 100644 --- a/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp +++ b/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp @@ -208,6 +208,15 @@ struct CopyYZSlicesFunctor return {}; } }; + +struct CreateDataStoreFunctor +{ + template + std::unique_ptr operator()(const ShapeType& sliceShape, const ShapeType& cDims) const + { + return std::make_unique>(sliceShape, cDims, static_cast(0)); + } +}; } // namespace cxITKImageWriterFilter namespace nx::core @@ -374,9 +383,8 @@ Result<> ITKImageWriterFilter::executeImpl(DataStructure& dataStructure, const A const auto& imageArray = dataStructure.getDataRefAs(imageArrayPath); const IDataStore& currentData = imageArray.getIDataStoreRef(); - - std::unique_ptr sliceData = currentData.createNewInstance(); DataType dataType = currentData.getDataType(); + ShapeType cDims = currentData.getComponentShape(); ITK::ImageGeomData newImageGeom(imageGeom); @@ -393,6 +401,9 @@ Result<> ITKImageWriterFilter::executeImpl(DataStructure& dataStructure, const A newImageGeom.origin = {origin.getX(), origin.getY(), 0.0f}; newImageGeom.spacing = {spacing.getX(), spacing.getY(), 1.0f}; + ShapeType sliceShape(std::reverse_iterator(newImageGeom.dims.end()), std::reverse_iterator(newImageGeom.dims.begin())); + std::unique_ptr sliceData = ExecuteDataFunction(cxITKImageWriterFilter::CreateDataStoreFunctor{}, dataType, sliceShape, cDims); + for(usize slice = 0; slice < dims.getZ(); ++slice) { if(shouldCancel) @@ -421,6 +432,9 @@ Result<> ITKImageWriterFilter::executeImpl(DataStructure& dataStructure, const A newImageGeom.origin = {origin.getX(), origin.getZ(), 0.0f}; newImageGeom.spacing = {spacing.getX(), spacing.getZ(), 1.0f}; + ShapeType sliceShape(std::reverse_iterator(newImageGeom.dims.end()), std::reverse_iterator(newImageGeom.dims.begin())); + std::unique_ptr sliceData = ExecuteDataFunction(cxITKImageWriterFilter::CreateDataStoreFunctor{}, dataType, sliceShape, cDims); + for(usize slice = 0; slice < dims.getY(); ++slice) { if(shouldCancel) @@ -449,6 +463,9 @@ Result<> ITKImageWriterFilter::executeImpl(DataStructure& dataStructure, const A newImageGeom.origin = {origin.getY(), origin.getZ(), 0.0f}; newImageGeom.spacing = {spacing.getY(), spacing.getZ(), 1.0f}; + ShapeType sliceShape(std::reverse_iterator(newImageGeom.dims.end()), std::reverse_iterator(newImageGeom.dims.begin())); + std::unique_ptr sliceData = ExecuteDataFunction(cxITKImageWriterFilter::CreateDataStoreFunctor{}, dataType, sliceShape, cDims); + for(usize slice = 0; slice < dims.getX(); ++slice) { if(shouldCancel) diff --git a/src/Plugins/ITKImageProcessing/vv/ITKImageWriterFilter.md b/src/Plugins/ITKImageProcessing/vv/ITKImageWriterFilter.md index d5635c2ec8..1fcaf86c87 100644 --- a/src/Plugins/ITKImageProcessing/vv/ITKImageWriterFilter.md +++ b/src/Plugins/ITKImageProcessing/vv/ITKImageWriterFilter.md @@ -113,7 +113,7 @@ ITKImageWriterFilter exports ImageGeom cell data as an ITK image or a 2D image s | 12 | RGBA dispatch arm. | `RGBA Image Output` - one uint8 RGBA pixel decodes as `(10,20,30,40)`. | | 13 | Cancellation between slices. | *Not directly tested; requires cancel signal infrastructure* | | 14 | Filesystem/ITK write-failure propagation. | *Not directly tested; requires failure injection.* | -| 15 | Tuple-copy failure propagation. | *Not directly tested. This is a defensive check whose failure preconditions are excluded by construction: `sliceData` is created from the input store with the same tuple and component shapes, and preflight requires the input tuple dimensions to match the selected geometry.* | +| 15 | Tuple-copy failure propagation. | *Not directly tested. This is a defensive check whose failure preconditions are excluded by construction: `sliceData` is created with the input store's data type and component shape and exactly the selected plane's tuple shape; preflight requires the input tuple dimensions to match the selected geometry, so the computed source and destination ranges are valid.* | | 16 | Single-slice output keeps the exact output name. | `Single Slice Keeps Exact Output Name` - `maxSlice == 1` suppresses the index suffix, and a 3x1x2 ImageGeom XZ plane writes one unsuffixed MetaImage file with exact decoded pixels. | | 17 | Multi-file stack output. | `Write Stack`. | | 18 | SIMPL JSON conversion with optional legacy parameters. | `SIMPL Backwards Compatibility`. | From 2e2662393024f3fbba75cdab378d66c2374b1eb7 Mon Sep 17 00:00:00 2001 From: Jared Duffey Date: Thu, 30 Jul 2026 12:59:19 -0400 Subject: [PATCH 24/28] Moved allowed component shapes to parameter defintion Signed-off-by: Jared Duffey --- .../Filters/ITKImageWriterFilter.cpp | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp b/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp index 958dcd1357..e42aa7f220 100644 --- a/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp +++ b/src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp @@ -39,13 +39,6 @@ namespace cxITKImageWriterFilter using ArrayOptionsType = ITK::ScalarVectorPixelIdTypeList; using RgbRgbaArrayOptionsType = ITK::ArrayOptions, ITK::ArrayUseAllTypes>; -constexpr std::array k_AllowedComponentSizes = {1, 2, 3, 4, 10, 11, 36}; - -bool IsValidComponentSize(usize componentSize) -{ - return std::find(k_AllowedComponentSizes.begin(), k_AllowedComponentSizes.end(), componentSize) != k_AllowedComponentSizes.end(); -} - // Rejects 0 - 31 ASCII control characters bool IsValidFillCharacter(char fillCharacter) { @@ -269,7 +262,7 @@ Parameters ITKImageWriterFilter::parameters() const 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{}, - nx::core::ITK::GetScalarPixelAllowedTypes())); + nx::core::ITK::GetScalarPixelAllowedTypes(), ArraySelectionParameter::AllowedComponentShapes{{1}, {2}, {3}, {4}, {10}, {11}, {36}})); return params; } @@ -328,12 +321,6 @@ IFilter::PreflightResult ITKImageWriterFilter::preflightImpl(const DataStructure return {MakeErrorResult(ITK::Constants::k_OutOfCoreDataNotSupported, fmt::format("Input Array '{}' utilizes out-of-core data. This is not supported within ITK filters.", imageArrayPath.toString()))}; } - const usize componentCount = imageArray.getNumberOfComponents(); - if(!cxITKImageWriterFilter::IsValidComponentSize(componentCount)) - { - return {MakeErrorResult( - -21010, fmt::format("Input Array '{}' has {} components. Supported component counts are {}.", imageArrayPath.toString(), componentCount, cxITKImageWriterFilter::k_AllowedComponentSizes))}; - } Result resultOutputActions; std::vector preflightUpdatedValues; From d5735576d61b6bb3a285c720ac8b2c25aed1f315 Mon Sep 17 00:00:00 2001 From: Jared Duffey Date: Thu, 30 Jul 2026 13:11:33 -0400 Subject: [PATCH 25/28] Updated code path coverage in report Signed-off-by: Jared Duffey --- .../vv/ITKImageWriterFilter.md | 73 +++++++++++++------ 1 file changed, 49 insertions(+), 24 deletions(-) diff --git a/src/Plugins/ITKImageProcessing/vv/ITKImageWriterFilter.md b/src/Plugins/ITKImageProcessing/vv/ITKImageWriterFilter.md index 1fcaf86c87..f457d2d0d9 100644 --- a/src/Plugins/ITKImageProcessing/vv/ITKImageWriterFilter.md +++ b/src/Plugins/ITKImageProcessing/vv/ITKImageWriterFilter.md @@ -15,7 +15,7 @@ |---|---| | Algorithm Relationship | Minor changes - same XY/XZ/YZ pixel extraction; NX uses SIMPLNX stores, AtomicFile, and current ITK APIs. Plane spacing and origin now follow the selected physical axes (D2). | | Oracle (confirmed) | Classes 1 + 4 - one 3x2x2 scalar fixture (`value(x,y,z)=x+10y+100z`) has same XY/XZ/YZ pixels for all ten accepted scalar types; it verifies selected-plane spacing for all types and origin for MetaImage output. uint8 uses TIFF and the remaining types use MetaImage. | -| Code paths enumerated | 14 of 19 explicit paths exercised; OOC, unsupported-component, cancellation, write-failure, and defensive tuple-copy failure paths remain untested. | +| Code paths enumerated | 28 of 48 scoped logical paths exercised; 20 documented gaps cover unsupported inputs, defensive branches, progress and cancellation, untested vector component counts, injected write failures, and SIMPL conversion failure. | | Tests today | 7 named test cases - 1 Class 1+4 Oracle, 2 preflight error-path tests, 1 single-slice exact-name test, 1 RGBA output test, 1 stack-writing test, and 1 SIMPL backwards-compatibility test | | Exemplar archive | None - Class 1+4 oracle uses inline data | | Legacy comparison | Run against DREAM3D 6.5.171 with the inline test data for the Class 1+4 Oracle. Decoded pixels match; D1 and D2 record filename-formatting and plane-metadata differences. | @@ -95,29 +95,54 @@ ITKImageWriterFilter exports ImageGeom cell data as an ITK image or a 2D image s ## Code path coverage -14 of 19 explicit paths exercised. Source: `src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp` (511 lines). - -| # | Path | Test case | -|---|---|---| -| 1 | Valid preflight. | `Analytical Pixel Order`, `Write Stack` | -| 2 | Dimension-mismatch preflight error. | `Dimension Mismatch Validation` - a 1x1x1 array and 1x1x2 geometry return `-25600`. | -| 3 | Empty fill-character validation. | `Fill Character Validation` - empty string returns `-25601`. | -| 4 | Invalid fill-character validation. | `Fill Character Validation` - `{` and `/` return `-25602`. | -| 5 | OOC input rejection. | *Not directly tested; not able to run on CI* | -| 6 | Unsupported component-count rejection. | *Not directly tested.* | -| 7 | Preflight example-output-file value, including offset multi-slice and unsuffixed single-slice output. | `Write Stack` and `Single Slice Keeps Exact Output Name`. | -| 8 | XY output pixels and physical metadata. | `Analytical Pixel Order` - hand-derived pixels and spacing `(1,2)` for all types, plus origin `(10,20)` for MetaImage output. | -| 9 | XZ output pixels and physical metadata. | `Analytical Pixel Order` - hand-derived pixels and spacing `(1,4)` for all types, plus origin `(10,40)` for MetaImage output. | -| 10 | YZ output pixels and physical metadata. | `Analytical Pixel Order` - hand-derived pixels and spacing `(2,4)` for all types, plus origin `(20,40)` for MetaImage output. | -| 11 | All accepted scalar dispatch arms: int8, uint8, int16, uint16, int32, uint32, int64, uint64, float32, and float64. | `Analytical Pixel Order` - TIFF pixels for uint8 and MetaImage pixels for every other type. | -| 12 | RGBA dispatch arm. | `RGBA Image Output` - one uint8 RGBA pixel decodes as `(10,20,30,40)`. | -| 13 | Cancellation between slices. | *Not directly tested; requires cancel signal infrastructure* | -| 14 | Filesystem/ITK write-failure propagation. | *Not directly tested; requires failure injection.* | -| 15 | Tuple-copy failure propagation. | *Not directly tested. This is a defensive check whose failure preconditions are excluded by construction: `sliceData` is created with the input store's data type and component shape and exactly the selected plane's tuple shape; preflight requires the input tuple dimensions to match the selected geometry, so the computed source and destination ranges are valid.* | -| 16 | Single-slice output keeps the exact output name. | `Single Slice Keeps Exact Output Name` - `maxSlice == 1` suppresses the index suffix, and a 3x1x2 ImageGeom XZ plane writes one unsuffixed MetaImage file with exact decoded pixels. | -| 17 | Multi-file stack output. | `Write Stack`. | -| 18 | SIMPL JSON conversion with optional legacy parameters. | `SIMPL Backwards Compatibility`. | -| 19 | Atomic-file commit after a successful ITK write. | Covered by successful output tests; no injected commit failure. | +28 of 48 scoped logical paths exercised. Source: `src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Filters/ITKImageWriterFilter.cpp` (518 lines). + +Scope: one row represents one filter-controlled logical behavior or failure exit. Repeated equivalent checks in the XY, XZ, and YZ loops are grouped. Element-type and component-count dispatch are counted as orthogonal families rather than as a Cartesian product. Internal implementation branches of parameter classes, dispatch utilities, AtomicFile, and ITK are excluded; configured parameter outcomes and success or failure results handled at this filter's call boundaries are included. + +| # | Phase | Path | Test case | +|---|---|---|---| +| 1 | Preflight | Valid dimensions, fill character, in-memory storage, and supported selections reach the preview calculation. | `Analytical Pixel Order`, `Single Slice Keeps Exact Output Name`, `RGBA Image Output`, and `Write Stack`. | +| 2 | Preflight | Dimension mismatch returns `-25600`. | `Dimension Mismatch Validation` uses a 1x1x1 array with a 1x1x2 geometry and asserts `-25600`. | +| 3 | Preflight | Fill-character length other than one returns `-25601`. | `Fill Character Validation` uses an empty string and asserts `-25601`. | +| 4 | Preflight | A one-character format, path, or ASCII control character returns `-25602`. | `Fill Character Validation` asserts `-25602` for `{`, `/`, and newline. | +| 5 | Preflight | OOC input returns `ITK::Constants::k_OutOfCoreDataNotSupported`. | *Not directly tested; ITK tests run only with in-memory arrays.* | +| 6 | Parameter validation | Unsupported element type is rejected by `ArraySelectionParameter`. | *Not directly tested; the selected array uses one of the ten accepted numeric element types in every execution test.* | +| 7 | Parameter validation | Unsupported component shape is rejected by `ArraySelectionParameter`. | *Not directly tested; current tests use component shapes `{1}` and `{4}`.* | +| 8 | Preflight | XY plane selects the Z slice count for the example output file. | `Write Stack` asserts the XY preview starts at the configured index offset. | +| 9 | Preflight | XZ plane selects the Y slice count for the example output file. | `Single Slice Keeps Exact Output Name` asserts the one-slice XZ preview. | +| 10 | Preflight | YZ plane selects the X slice count for the example output file. | *Not directly assertion-covered; YZ execution is tested, but its preview value is not asserted.* | +| 11 | Preflight | Defensive plane-switch default leaves `maxSlice == 1`. | *Not directly tested; `ChoicesParameter` rejects values outside XY, XZ, and YZ before `preflightImpl`.* | +| 12 | Preflight | Multi-slice preview includes the formatted index suffix and offset. | `Write Stack` asserts `slice_100.tif`. | +| 13 | Preflight | Single-slice preview suppresses the index suffix. | `Single Slice Keeps Exact Output Name` asserts the exact unsuffixed output path. | +| 14 | Execute | Runtime store creation and copy dispatch cover int8, uint8, int16, uint16, int32, uint32, int64, uint64, float32, and float64. | `Analytical Pixel Order` is templated over all ten element types and asserts decoded pixels. | +| 15 | Execute | XY extraction and selected-axis physical metadata. | `Analytical Pixel Order` asserts hand-derived pixels, spacing `(1,2)`, and MetaImage origin `(10,20)`. | +| 16 | Execute | XZ extraction and selected-axis physical metadata. | `Analytical Pixel Order` asserts hand-derived pixels, spacing `(1,4)`, and MetaImage origin `(10,40)`. | +| 17 | Execute | YZ extraction and selected-axis physical metadata. | `Analytical Pixel Order` asserts hand-derived pixels, spacing `(2,4)`, and MetaImage origin `(20,40)`. | +| 18 | Execute | Cancellation between slices returns before copying or writing the next slice. | *Not directly tested; requires cancel-signal injection during execution.* | +| 19 | Execute | Tuple copy succeeds and the next slice operation proceeds. | `Analytical Pixel Order` asserts every copied pixel for all planes and element types. | +| 20 | Execute | Tuple-copy failure propagates from `copyFrom`. | *Not directly tested. This defensive path is excluded by construction: `sliceData` uses the input data type and component shape and exactly the selected plane's tuple shape; preflight requires the input tuple dimensions to match the selected geometry, so computed source and destination ranges are valid.* | +| 21 | Execute | A per-slice message reports the one-based file number, total file count, and output path. | *Not directly tested; current tests do not capture the message handler.* | +| 22 | Filename | Multi-slice execution adds the configured padding, fill character, and index offset. | `Write Stack` asserts the expected offset filenames and that no extra files remain. | +| 23 | Filename | Single-slice execution keeps the exact output filename. | `Single Slice Keeps Exact Output Name` asserts the unsuffixed file exists and decodes correctly. | +| 24 | Filesystem | Output parent directory already exists. | Multi-slice tests assert files after the first slice, which reuse the directory created for that stack. | +| 25 | Filesystem | Missing output parent directory is created successfully. | Successful output tests use new random nested directories and assert the first output file. | +| 26 | Filesystem | Output parent directory creation fails and returns `-19000`. | *Not directly tested; requires filesystem failure injection.* | +| 27 | Component dispatch | Component shape `{1}` uses the scalar path. | `Analytical Pixel Order` covers `{1}` for all ten accepted element types. | +| 28 | Component dispatch | Component shape `{2}` uses the vector path. | *Not directly tested.* | +| 29 | Component dispatch | Component shape `{3}` uses the vector path. | *Not directly tested.* | +| 30 | Component dispatch | Component shape `{4}` uses the RGBA path. | `RGBA Image Output` decodes one uint8 pixel as `(10,20,30,40)`; other element types are not cross-product tested. | +| 31 | Component dispatch | Component shape `{10}` uses the vector path. | *Not directly tested.* | +| 32 | Component dispatch | Component shape `{11}` uses the vector path. | *Not directly tested.* | +| 33 | Component dispatch | Component shape `{36}` uses the vector path. | *Not directly tested.* | +| 34 | Component dispatch | Defensive `ArraySwitchFunc` failure returns `-21010`. | *Not directly tested; allowed element types and component shapes are constrained by the array-selection parameter.* | +| 35 | Atomic write | `AtomicFile::Create` succeeds and provides a temporary path. | Successful output tests assert the committed output files. | +| 36 | Atomic write | `AtomicFile::Create` failure propagates. | *Not directly tested; requires failure injection.* | +| 37 | Atomic write | ITK writer succeeds. | Successful output tests decode the written images and assert their contents. | +| 38 | Atomic write | ITK throws and the filter returns `-21011`. | *Not directly tested; requires ITK writer failure injection.* | +| 39 | Atomic write | Atomic commit succeeds. | Successful output tests assert the final output paths exist and decode correctly. | +| 40 | Atomic write | Atomic commit failure propagates. | *Not directly tested; requires commit failure injection.* | +| 41 | Execute | Successful `SaveImageData` result continues the plane loop. | `Analytical Pixel Order` and `Write Stack` assert that every expected slice is written. | +| 42 | Execute | Failed `SaveImageData` result propagates from the plane loop. | *Not directly tested; covered failure sources require injection.* | ## Test inventory From 01614870d6c8351ad50d2ef601827b7fde049324 Mon Sep 17 00:00:00 2001 From: Jared Duffey Date: Thu, 30 Jul 2026 14:02:49 -0400 Subject: [PATCH 26/28] Updated PR list Signed-off-by: Jared Duffey --- src/Plugins/ITKImageProcessing/vv/ITKImageWriterFilter.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Plugins/ITKImageProcessing/vv/ITKImageWriterFilter.md b/src/Plugins/ITKImageProcessing/vv/ITKImageWriterFilter.md index f457d2d0d9..8986e20f74 100644 --- a/src/Plugins/ITKImageProcessing/vv/ITKImageWriterFilter.md +++ b/src/Plugins/ITKImageProcessing/vv/ITKImageWriterFilter.md @@ -37,6 +37,7 @@ ITKImageWriterFilter exports ImageGeom cell data as an ITK image or a 2D image s - [PR #1626](https://github.com/BlueQuartzSoftware/simplnx/pull/1626) ("ENH: Various small doc, bug and enhancement fixes.") - Small documentation, bug, and enhancement fixes. - [PR #1585](https://github.com/BlueQuartzSoftware/simplnx/pull/1585) ("ENH: Add Image Reader/Writer that depend on Tiff and Stb libraries.") - Integrates TIFF/STB reader-writer support. - [PR #1576](https://github.com/BlueQuartzSoftware/simplnx/pull/1576) ("ENH: Improve error messages across the codebase") - Improves error messages. +- [PR #1571](https://github.com/BlueQuartzSoftware/simplnx/pull/1571) ("DOC: Add standardized ChoicesParameter descriptions to filter docs") - Adds standardized output-plane choice descriptions. - [PR #1555](https://github.com/BlueQuartzSoftware/simplnx/pull/1555) ("ENH: Again require in-memory data for ITK filters") - Restores the ITK in-memory data requirement. - [PR #1490](https://github.com/BlueQuartzSoftware/simplnx/pull/1490) ("STY: Fix warnings about unintended slicing of object") - Removes object-slicing warnings. - [PR #1476](https://github.com/BlueQuartzSoftware/simplnx/pull/1476) ("BUG/ENH: Fix Backwards Pipeline Compatibility and Add Testing") - Fixes backward-pipeline compatibility. @@ -55,11 +56,15 @@ ITKImageWriterFilter exports ImageGeom cell data as an ITK image or a 2D image s - [PR #1088](https://github.com/BlueQuartzSoftware/simplnx/pull/1088) ("Added versioning to filter parameters and json") - Adds filter and parameter versioning, including backwards-compatible parameter JSON reading. - [PR #1082](https://github.com/BlueQuartzSoftware/simplnx/pull/1082) ("SIMPLConversion header optimization") - Optimizes SIMPL-conversion headers. +- [PR #975](https://github.com/BlueQuartzSoftware/simplnx/pull/975) ("DOC: Fix capitalization issue in the documentation for the GitHub link.") - Corrects GitHub link capitalization. +- [PR #956](https://github.com/BlueQuartzSoftware/simplnx/pull/956) ("ENH: Rename Filters that start with Find/Generate/Calculate to Compute") - Updates documentation for standardized filter names. - [PR #941](https://github.com/BlueQuartzSoftware/simplnx/pull/941) ("Moved Result handling outside of AtomicFile") - Moves result handling outside AtomicFile. - [PR #934](https://github.com/BlueQuartzSoftware/simplnx/pull/934) ("BUG: Pipeline and Filter human facing label cleanup") - Cleans up human-facing labels. - [PR #931](https://github.com/BlueQuartzSoftware/simplnx/pull/931) ("ENH: All filter's class names end with \"Filter\".") - Applies the Filter class-name suffix. - [PR #918](https://github.com/BlueQuartzSoftware/simplnx/pull/918) ("BUG: Update ITK Image Writer to Use Atomic File API") - Migrates the writer to the AtomicFile API. +- [PR #914](https://github.com/BlueQuartzSoftware/simplnx/pull/914) ("DOC: Update all doc files to have correct filter human name") - Corrects the documented filter human name. - [PR #874](https://github.com/BlueQuartzSoftware/simplnx/pull/874) ("ENH: Refactor the Parameter Keys to make them consistent and easy to learn") - Refactors parameter keys. +- [PR #847](https://github.com/BlueQuartzSoftware/simplnx/pull/847) ("DOC: Link directly to the discussion page on GitHub.") - Updates the documentation discussion link. **2023** @@ -67,7 +72,9 @@ ITKImageWriterFilter exports ImageGeom cell data as an ITK image or a 2D image s - [PR #790](https://github.com/BlueQuartzSoftware/simplnx/pull/790) ("ENH: Write Temp Files for All Writers") - Adds temporary-file writing for writers. - [PR #779](https://github.com/BlueQuartzSoftware/simplnx/pull/779) ("ENH: Implement SIMPL pipeline conversion") - Implements SIMPL pipeline conversion. - [PR #753](https://github.com/BlueQuartzSoftware/simplnx/pull/753) ("API: Standardize I/O Naming to Read/Write") - Standardizes I/O naming. +- [PR #708](https://github.com/BlueQuartzSoftware/simplnx/pull/708) ("DOC: Update documentation files to allow Sphinx to generate html documentation") - Updates documentation for Sphinx generation. - [PR #703](https://github.com/BlueQuartzSoftware/simplnx/pull/703) ("ENH: Enable Out-of-Core functionality") - Adds OOC infrastructure compatibility. +- [PR #673](https://github.com/BlueQuartzSoftware/simplnx/pull/673) ("ENH: Filter help text filled out. Docs updated with python section") - Adds filter help text and Python documentation. - [PR #671](https://github.com/BlueQuartzSoftware/simplnx/pull/671) ("API: Add C++ Class Name to All Default Tags") - Adds C++ class names to default tags. - [PR #593](https://github.com/BlueQuartzSoftware/simplnx/pull/593) ("ENH: Update ITK filters to follow naming and parameter layout conventions") - Aligns ITK filter naming and parameter layout. - [PR #575](https://github.com/BlueQuartzSoftware/simplnx/pull/575) ("BUG: Fix issue where ITKImageWriter is double looping over the Z dim") - Fixes double iteration over Z. From bb694e9dd0ecf39a7ad1e8c60a3e0e82c372c27a Mon Sep 17 00:00:00 2001 From: Jared Duffey Date: Thu, 30 Jul 2026 14:14:43 -0400 Subject: [PATCH 27/28] Added file count check Signed-off-by: Jared Duffey --- src/Plugins/ITKImageProcessing/test/ITKImageWriterTest.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Plugins/ITKImageProcessing/test/ITKImageWriterTest.cpp b/src/Plugins/ITKImageProcessing/test/ITKImageWriterTest.cpp index d219e67c84..896e8225e5 100644 --- a/src/Plugins/ITKImageProcessing/test/ITKImageWriterTest.cpp +++ b/src/Plugins/ITKImageProcessing/test/ITKImageWriterTest.cpp @@ -199,6 +199,13 @@ TEMPLATE_TEST_CASE("ITKImageProcessing::ITKImageWriterFilter: Analytical Pixel O CompareImageToExpected(imagePath, dimensions, expectedSlices[slice]); CompareImageMetadata(imagePath, expectedSpacing, expectedOrigin, extension != ".tif"); } + + usize outputFileCount = 0; + for([[maybe_unused]] const auto& entry : fs::directory_iterator(outputDir / name)) + { + outputFileCount++; + } + REQUIRE(outputFileCount == expectedSlices.size()); }; // Rows are y/z respectively. The non-square XY plane makes an X/Y transpose observable. From 42def813394937cec7f412a55a16327285ad4c2d Mon Sep 17 00:00:00 2001 From: Jared Duffey Date: Thu, 30 Jul 2026 14:52:23 -0400 Subject: [PATCH 28/28] Updated provenance and legacy comparison Signed-off-by: Jared Duffey --- src/Plugins/ITKImageProcessing/vv/ITKImageWriterFilter.md | 8 +++++--- .../vv/deviations/ITKImageWriterFilter.md | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Plugins/ITKImageProcessing/vv/ITKImageWriterFilter.md b/src/Plugins/ITKImageProcessing/vv/ITKImageWriterFilter.md index 8986e20f74..ace5bb75af 100644 --- a/src/Plugins/ITKImageProcessing/vv/ITKImageWriterFilter.md +++ b/src/Plugins/ITKImageProcessing/vv/ITKImageWriterFilter.md @@ -18,7 +18,7 @@ | Code paths enumerated | 28 of 48 scoped logical paths exercised; 20 documented gaps cover unsupported inputs, defensive branches, progress and cancellation, untested vector component counts, injected write failures, and SIMPL conversion failure. | | Tests today | 7 named test cases - 1 Class 1+4 Oracle, 2 preflight error-path tests, 1 single-slice exact-name test, 1 RGBA output test, 1 stack-writing test, and 1 SIMPL backwards-compatibility test | | Exemplar archive | None - Class 1+4 oracle uses inline data | -| Legacy comparison | Run against DREAM3D 6.5.171 with the inline test data for the Class 1+4 Oracle. Decoded pixels match; D1 and D2 record filename-formatting and plane-metadata differences. | +| Legacy comparison | Reproduced against DREAM3D 6.5.171 with a pipeline-generated copy of the inline oracle and the archived Small IN100 input. All 577 decoded slices match exactly per implementation; D1 and D2 record filename-formatting and plane-metadata differences. See `vv/provenance/ITKImageWriterFilter.md`. | | Bug flags | Unsigned uint32/uint64 dispatch was corrected for this V&V cycle. Current changes add invalid fill-character validation, component-count validation, RGBA dispatch, selected-plane physical metadata, and tuple-copy error propagation. | | V&V phase | Regression tests and report amendments in progress | @@ -167,9 +167,11 @@ Scope: one row represents one filter-controlled logical behavior or failure exit No new exemplar archive was created for this V&V cycle: the Class 1 oracle is encoded entirely as inline expected values in the test source. +The oracle and DREAM3D 6.5.171 comparison provenance, including the legacy setup pipeline, paired writer pipelines, runner hashes, input hashes, zero-tolerance comparison method, and machine-readable results, is recorded in [`provenance/ITKImageWriterFilter.md`](provenance/ITKImageWriterFilter.md). Reproducible comparison artifacts and were uploaded to OneDrive on 2026-07-30. + ## Deviations from DREAM3D 6.5.171 - `ITKImageWriterFilter-D1` - NX defaults to zero-padded slice indices while 6.5.171 does not - see `vv/deviations/ITKImageWriterFilter.md`. -- `ITKImageWriterFilter-D2` - NX writes the selected plane's physical spacing and origin, while 6.5.171 used identity 2D metadata for XZ/YZ output - see `vv/deviations/ITKImageWriterFilter.md`. +- `ITKImageWriterFilter-D2` - NX writes the selected plane's physical spacing and origin, while 6.5.171 used identity 2D metadata for XY/XZ/YZ output - see `vv/deviations/ITKImageWriterFilter.md`. -All pixels otherwise match: the analytical fixture has 2 XY, 2 XZ, and 3 YZ slices; Small IN100 has 117 XY, 201 XZ, and 189 YZ float32 TIFF slices. +All pixels otherwise match exactly with zero tolerance: the analytical fixture has 2 XY, 2 XZ, and 3 YZ slices for each of ten scalar types; Small IN100 has 117 XY, 201 XZ, and 189 YZ float32 TIFF slices. diff --git a/src/Plugins/ITKImageProcessing/vv/deviations/ITKImageWriterFilter.md b/src/Plugins/ITKImageProcessing/vv/deviations/ITKImageWriterFilter.md index d8c62d59e1..ce11ef405e 100644 --- a/src/Plugins/ITKImageProcessing/vv/deviations/ITKImageWriterFilter.md +++ b/src/Plugins/ITKImageProcessing/vv/deviations/ITKImageWriterFilter.md @@ -28,10 +28,10 @@ Entries are referenced by stable ID (ITKImageWriterFilter-D) from the V&V rep | **Filter UUID** | `a181ee3e-1678-4133-b9c5-a9dd7bfec62f` | | **Status** | active | -**Symptom:** For XZ and YZ stack output, NX writes the two selected physical axes as the 2D image spacing and origin. DREAM3D 6.5.171 writes identity spacing and zero origin for the newly created 2D image. +**Symptom:** For XY, XZ, and YZ stack output, NX writes the two selected physical axes as the 2D image spacing and origin. DREAM3D 6.5.171 writes identity spacing and zero origin for the newly created 2D image. **Root cause:** Library/API adaptation. NX constructs the temporary 2D geometry from the selected ImageGeom axes so that output metadata describes the written plane; the legacy implementation created a new 2D geometry with default metadata. -**Affected users:** Workflows that consume XZ or YZ physical image metadata can observe different spacing and origin after migration. +**Affected users:** Workflows that consume physical image metadata can observe different spacing and origin after migration when the selected source axes do not already have unit spacing and zero origin. **Recommendation:** Trust SIMPLNX. The NX metadata preserves the source ImageGeom's physical coordinate system for the output plane.