diff --git a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/CAxisSegmentFeatures.cpp b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/CAxisSegmentFeatures.cpp index 587ecbf6c3..8dd04ec515 100644 --- a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/CAxisSegmentFeatures.cpp +++ b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/CAxisSegmentFeatures.cpp @@ -54,7 +54,18 @@ Result<> CAxisSegmentFeatures::operator()() for(usize cellIdx = 0; cellIdx < numCells; ++cellIdx) { int32 currentPhaseIdx = m_CellPhases->getValue(cellIdx); + // Only consider valid ebsd phase values + if(currentPhaseIdx < 1) + { + continue; + } + // Only consider valid mask values + if(m_InputValues->UseMask && !m_GoodVoxelsArray->isTrue(cellIdx)) + { + continue; + } const auto crystalStructureType = crystalStructures[currentPhaseIdx]; + if(crystalStructureType != ebsdlib::CrystalStructure::Hexagonal_High && crystalStructureType != ebsdlib::CrystalStructure::Hexagonal_Low) { return MakeErrorResult(-8363, fmt::format("Input data is using {} type crystal structures but segmenting features via c-axis mis orientation requires all phases to be either Hexagonal-Low 6/m " diff --git a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/CAxisSegmentFeaturesFilter.cpp b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/CAxisSegmentFeaturesFilter.cpp index e698b93e0f..bd15933916 100644 --- a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/CAxisSegmentFeaturesFilter.cpp +++ b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/CAxisSegmentFeaturesFilter.cpp @@ -216,7 +216,11 @@ Result CAxisSegmentFeaturesFilter::FromSIMPLJson(const nlohmann::json results.push_back(SIMPLConversion::ConvertParameter(args, json, SIMPL::k_QuatsArrayPathKey, k_SelectedImageGeometryPath_Key)); results.push_back(SIMPLConversion::ConvertParameter>(args, json, SIMPL::k_MisorientationToleranceKey, k_MisorientationTolerance_Key)); results.push_back(SIMPLConversion::ConvertParameter(args, json, SIMPL::k_UseGoodVoxelsKey, k_UseMask_Key)); - results.push_back(SIMPLConversion::ConvertParameter(args, json, SIMPL::k_RandomizeFeatureIdsKey, k_RandomizeFeatureIds_Key)); + // RandomizeFeatureIds was added to the legacy filter after 6.5.49; older pipelines omit the key. + if(json.contains(SIMPL::k_RandomizeFeatureIdsKey)) + { + results.push_back(SIMPLConversion::ConvertParameter(args, json, SIMPL::k_RandomizeFeatureIdsKey, k_RandomizeFeatureIds_Key)); + } results.push_back(SIMPLConversion::ConvertParameter(args, json, SIMPL::k_QuatsArrayPathKey, k_QuatsArrayPath_Key)); results.push_back(SIMPLConversion::ConvertParameter(args, json, SIMPL::k_CellPhasesArrayPathKey, k_CellPhasesArrayPath_Key)); results.push_back(SIMPLConversion::ConvertParameter(args, json, SIMPL::k_GoodVoxelsArrayPathKey, k_MaskArrayPath_Key)); diff --git a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeAvgCAxesFilter.cpp b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeAvgCAxesFilter.cpp index f3c7f13296..72aa2c31cd 100644 --- a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeAvgCAxesFilter.cpp +++ b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeAvgCAxesFilter.cpp @@ -145,13 +145,29 @@ Result ComputeAvgCAxesFilter::FromSIMPLJson(const nlohmann::json& jso std::vector> results; - results.push_back(SIMPLConversion::ConvertParameter(args, json, SIMPL::k_QuatsArrayPathKey, k_CellFeatureAttributeMatrixPath_Key)); results.push_back(SIMPLConversion::ConvertParameter(args, json, SIMPL::k_QuatsArrayPathKey, k_QuatsArrayPath_Key)); - results.push_back(SIMPLConversion::ConvertParameter(args, json, SIMPL::k_FeatureIdsArrayPathKey, k_CellPhasesArrayPath_Key)); results.push_back(SIMPLConversion::ConvertParameter(args, json, SIMPL::k_FeatureIdsArrayPathKey, k_FeatureIdsArrayPath_Key)); - results.push_back(SIMPLConversion::ConvertParameter(args, json, SIMPL::k_AvgCAxesArrayPathKey, k_CrystalStructuresArrayPath_Key)); + // The feature attribute matrix comes from the created AvgCAxes array (e.g. "CellFeatureData"), + // not from the cell-level Quats/FeatureIds arrays (e.g. "CellData"). + results.push_back( + SIMPLConversion::ConvertParameter(args, json, SIMPL::k_AvgCAxesArrayPathKey, k_CellFeatureAttributeMatrixPath_Key)); results.push_back(SIMPLConversion::ConvertParameter(args, json, SIMPL::k_AvgCAxesArrayPathKey, k_AvgCAxesArrayName_Key)); + // The legacy FindAvgCAxes filter had no Cell Phases or Crystal Structures inputs, so there is nothing + // to convert. Derive the conventional locations from the legacy FeatureIds path so common pipelines + // convert to a runnable filter: "Phases" sits next to the Feature Ids array and "CrystalStructures" + // lives in the DataContainer's "CellEnsembleData" attribute matrix. Non-standard names need editing. + if(json.contains(SIMPL::k_FeatureIdsArrayPathKey)) + { + Result dcNameResult = SIMPLConversion::ReadDataContainerName(json[SIMPL::k_FeatureIdsArrayPathKey.view()], "FindAvgCAxes"); + Result amNameResult = SIMPLConversion::ReadAttributeMatrixName(json[SIMPL::k_FeatureIdsArrayPathKey.view()], "FindAvgCAxes"); + if(dcNameResult.valid() && amNameResult.valid()) + { + args.insertOrAssign(k_CellPhasesArrayPath_Key, std::make_any(DataPath({dcNameResult.value(), amNameResult.value(), "Phases"}))); + args.insertOrAssign(k_CrystalStructuresArrayPath_Key, std::make_any(DataPath({dcNameResult.value(), "CellEnsembleData", "CrystalStructures"}))); + } + } + Result<> conversionResult = MergeResults(std::move(results)); return ConvertResultTo(std::move(conversionResult), std::move(args)); diff --git a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeAvgOrientationsFilter.cpp b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeAvgOrientationsFilter.cpp index 1c9fb7516c..98e2ff970e 100644 --- a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeAvgOrientationsFilter.cpp +++ b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeAvgOrientationsFilter.cpp @@ -277,8 +277,10 @@ Result ComputeAvgOrientationsFilter::FromSIMPLJson(const nlohmann::js std::vector> results; results.push_back(SIMPLConversion::ConvertParameter(args, json, SIMPL::k_FeatureIdsArrayPathKey, k_CellFeatureIdsArrayPath_Key)); + // The feature attribute matrix must come from the created feature arrays (e.g. AvgQuatsArrayPath -> "CellFeatureData"), + // not from FeatureIdsArrayPath, which lives in the cell attribute matrix (e.g. "CellData"). results.push_back( - SIMPLConversion::ConvertParameter(args, json, SIMPL::k_FeatureIdsArrayPathKey, k_CellFeatureAttributeMatrixPath_Key)); + SIMPLConversion::ConvertParameter(args, json, SIMPL::k_AvgQuatsArrayPathKey, k_CellFeatureAttributeMatrixPath_Key)); results.push_back(SIMPLConversion::ConvertParameter(args, json, SIMPL::k_CellPhasesArrayPathKey, k_CellPhasesArrayPath_Key)); results.push_back(SIMPLConversion::ConvertParameter(args, json, SIMPL::k_QuatsArrayPathKey, k_CellQuatsArrayPath_Key)); results.push_back( diff --git a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeFeatureReferenceMisorientationsFilter.cpp b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeFeatureReferenceMisorientationsFilter.cpp index f4a610eb27..e9c82af39b 100644 --- a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeFeatureReferenceMisorientationsFilter.cpp +++ b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeFeatureReferenceMisorientationsFilter.cpp @@ -239,7 +239,9 @@ Result ComputeFeatureReferenceMisorientationsFilter::FromSIMPLJson(co results.push_back(SIMPLConversion::ConvertParameter(args, json, SIMPL::k_ReferenceOrientationKey, k_ReferenceOrientation_Key)); results.push_back( - SIMPLConversion::ConvertParameter(args, json, SIMPL::k_FeatureIdsArrayPathKey, k_CellFeatureAttributeMatrixPath_Key)); + // The feature attribute matrix comes from the feature-level AvgQuats array (e.g. "CellFeatureData"), + // not from FeatureIdsArrayPath, which lives in the cell attribute matrix (e.g. "CellData"). + SIMPLConversion::ConvertParameter(args, json, SIMPL::k_AvgQuatsArrayPathKey, k_CellFeatureAttributeMatrixPath_Key)); results.push_back(SIMPLConversion::ConvertParameter(args, json, SIMPL::k_FeatureIdsArrayPathKey, k_CellFeatureIdsArrayPath_Key)); results.push_back(SIMPLConversion::ConvertParameter(args, json, SIMPL::k_CellPhasesArrayPathKey, k_CellPhasesArrayPath_Key)); results.push_back(SIMPLConversion::ConvertParameter(args, json, SIMPL::k_QuatsArrayPathKey, k_QuatsArrayPath_Key)); diff --git a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/NeighborOrientationCorrelationFilter.cpp b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/NeighborOrientationCorrelationFilter.cpp index c1a93f7f47..70196ae381 100644 --- a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/NeighborOrientationCorrelationFilter.cpp +++ b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/NeighborOrientationCorrelationFilter.cpp @@ -230,7 +230,11 @@ Result NeighborOrientationCorrelationFilter::FromSIMPLJson(const nloh results.push_back(SIMPLConversion::ConvertParameter(args, json, SIMPL::k_QuatsArrayPathKey, k_QuatsArrayPath_Key)); results.push_back( SIMPLConversion::ConvertParameter(args, json, SIMPL::k_CrystalStructuresArrayPathKey, k_CrystalStructuresArrayPath_Key)); - results.push_back(SIMPLConversion::ConvertParameter(args, json, SIMPL::k_IgnoredDataArrayPathsKey, k_IgnoredDataArrayPaths_Key)); + // IgnoredDataArrayPaths was added to the legacy filter after 6.5.49; older pipelines omit the key. + if(json.contains(SIMPL::k_IgnoredDataArrayPathsKey)) + { + results.push_back(SIMPLConversion::ConvertParameter(args, json, SIMPL::k_IgnoredDataArrayPathsKey, k_IgnoredDataArrayPaths_Key)); + } Result<> conversionResult = MergeResults(std::move(results)); diff --git a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/WritePoleFigureFilter.cpp b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/WritePoleFigureFilter.cpp index 117656aeab..01f161b062 100644 --- a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/WritePoleFigureFilter.cpp +++ b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/WritePoleFigureFilter.cpp @@ -396,7 +396,10 @@ Result WritePoleFigureFilter::FromSIMPLJson(const nlohmann::json& jso results.push_back(SIMPLConversion::ConvertParameter(args, json, SIMPL::k_CellEulerAnglesArrayPathKey, k_CellEulerAnglesArrayPath_Key)); results.push_back(SIMPLConversion::ConvertParameter(args, json, SIMPL::k_CellPhasesArrayPathKey, k_CellPhasesArrayPath_Key)); results.push_back(SIMPLConversion::ConvertParameter(args, json, SIMPL::k_GoodVoxelsArrayPathKey, k_MaskArrayPath_Key)); - results.push_back(SIMPLConversion::ConvertParameter(args, json, SIMPL::k_CrystalStructuresArrayPathKey, k_ImageGeometryPath_Key)); + // Do NOT map the legacy CrystalStructuresArrayPath's DataContainer onto k_ImageGeometryPath_Key: + // that parameter creates a new ImageGeom, and the legacy filter had no output geometry. Reusing the + // input container name (e.g. "ImageDataContainer") collides with the existing geometry in preflight. + // The default from getDefaultArguments() ("PoleFigure") is the correct value. results.push_back( SIMPLConversion::ConvertParameter(args, json, SIMPL::k_CrystalStructuresArrayPathKey, k_CrystalStructuresArrayPath_Key)); results.push_back(SIMPLConversion::ConvertParameter(args, json, SIMPL::k_MaterialNameArrayPathKey, k_MaterialNameArrayPath_Key)); diff --git a/src/Plugins/OrientationAnalysis/test/ComputeAvgCAxesTest.cpp b/src/Plugins/OrientationAnalysis/test/ComputeAvgCAxesTest.cpp index b29a45367d..717250629a 100644 --- a/src/Plugins/OrientationAnalysis/test/ComputeAvgCAxesTest.cpp +++ b/src/Plugins/OrientationAnalysis/test/ComputeAvgCAxesTest.cpp @@ -272,12 +272,16 @@ TEST_CASE("OrientationAnalysis::ComputeAvgCAxesFilter: SIMPL Backwards Compatibi CHECK(pipelineFilter->getComments().empty()); const Arguments args = pipelineFilter->getArguments(); + // The feature attribute matrix is derived from the legacy AvgCAxesArrayPath (the created array), + // and the AvgCAxes array name comes from that same path. CHECK(args.value(ComputeAvgCAxesFilter::k_CellFeatureAttributeMatrixPath_Key) == DataPath({"DataContainer", "CellData"})); CHECK(args.value(ComputeAvgCAxesFilter::k_QuatsArrayPath_Key) == DataPath({"DataContainer", "CellData", "TestArray"})); - CHECK(args.value(ComputeAvgCAxesFilter::k_CellPhasesArrayPath_Key) == DataPath({"DataContainer", "CellData"})); CHECK(args.value(ComputeAvgCAxesFilter::k_FeatureIdsArrayPath_Key) == DataPath({"DataContainer", "CellData", "TestArray"})); - CHECK(args.value(ComputeAvgCAxesFilter::k_CrystalStructuresArrayPath_Key) == DataPath({"DataContainer", "CellData"})); CHECK(args.value(ComputeAvgCAxesFilter::k_AvgCAxesArrayName_Key) == "TestArray"); + // The legacy filter had no Cell Phases or Crystal Structures inputs; the conversion derives the + // conventional locations from the legacy FeatureIds path. + CHECK(args.value(ComputeAvgCAxesFilter::k_CellPhasesArrayPath_Key) == DataPath({"DataContainer", "CellData", "Phases"})); + CHECK(args.value(ComputeAvgCAxesFilter::k_CrystalStructuresArrayPath_Key) == DataPath({"DataContainer", "CellEnsembleData", "CrystalStructures"})); } } } diff --git a/src/Plugins/OrientationAnalysis/test/WritePoleFigureTest.cpp b/src/Plugins/OrientationAnalysis/test/WritePoleFigureTest.cpp index c7198dd2c8..13fc8fb668 100644 --- a/src/Plugins/OrientationAnalysis/test/WritePoleFigureTest.cpp +++ b/src/Plugins/OrientationAnalysis/test/WritePoleFigureTest.cpp @@ -440,7 +440,9 @@ TEST_CASE("OrientationAnalysis::WritePoleFigureFilter: SIMPL Backwards Compatibi CHECK(args.value(WritePoleFigureFilter::k_CellEulerAnglesArrayPath_Key) == DataPath({"DataContainer", "CellData", "TestArray"})); CHECK(args.value(WritePoleFigureFilter::k_CellPhasesArrayPath_Key) == DataPath({"DataContainer", "CellData", "TestArray"})); CHECK(args.value(WritePoleFigureFilter::k_MaskArrayPath_Key) == DataPath({"DataContainer", "CellData", "TestArray"})); - CHECK(args.value(WritePoleFigureFilter::k_ImageGeometryPath_Key) == DataPath({"DataContainer"})); + // The legacy filter had no output geometry, so the created Image Geometry path is intentionally + // left at its default ("PoleFigure") instead of reusing the input DataContainer name. + CHECK(args.value(WritePoleFigureFilter::k_ImageGeometryPath_Key) == DataPath({"PoleFigure"})); CHECK(args.value(WritePoleFigureFilter::k_CrystalStructuresArrayPath_Key) == DataPath({"DataContainer", "CellData", "TestArray"})); CHECK(args.value(WritePoleFigureFilter::k_MaterialNameArrayPath_Key) == DataPath({"DataContainer", "CellData", "TestArray"})); } diff --git a/src/Plugins/SimplnxCore/docs/WriteImageFilter.md b/src/Plugins/SimplnxCore/docs/WriteImageFilter.md index c47a160161..ae520229c7 100644 --- a/src/Plugins/SimplnxCore/docs/WriteImageFilter.md +++ b/src/Plugins/SimplnxCore/docs/WriteImageFilter.md @@ -26,7 +26,7 @@ When *Create Color Table* is **disabled**, the filter writes the **Data Array**' For example, writing a float32 array directly to a `.png` file fails preflight, since PNG (via stb) only supports uint8 pixel data. The same float32 array can be written directly to a `.tif` file, or, if a PNG is required, *Create Color Table* can be enabled to normalize the array into a uint8 RGB image before writing (see below). Since color-table output is always uint8 RGB, it is compatible with every supported output format. -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 output files will be numbered sequentially starting at the *Index Offset* and ending at *Index Offset + (dim - 1)* for the chosen axis. For example, if the Z axis has 117 dimensions and *Index Offset* is 0, 117 XY image files will be produced and numbered 000 through 116. +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 output files will be numbered sequentially starting at the *Index Offset* and ending at *Index Offset + (dim - 1)* for the chosen axis. For example, if the Z axis has 117 dimensions and *Index Offset* is 0, 117 XY image files will be produced and numbered 000 through 116. If the volume produces only a single slice, no index is appended and the file is written with exactly the user-specified name. Writes are performed through an `AtomicFile`, so partially-written slices from an aborted run will not corrupt existing output files. @@ -40,7 +40,7 @@ The *Plane* parameter controls which orthogonal plane is used when writing a 3D ### Index Formatting -The *Total Number of Index Digits* and *Fill Character* parameters control the numeric suffix applied to each slice filename. For example, 3 total digits with a fill character of `0` produces `slice_000.tif`, `slice_001.tif`, etc. +The *Total Number of Index Digits* and *Fill Character* parameters control the numeric suffix applied to each slice filename. For example, 3 total digits with a fill character of `0` produces `slice_000.tif`, `slice_001.tif`, etc. These parameters have no effect when only a single slice is written, since single-slice output does not receive an index suffix. ### Flip Output Image (Optional) diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/WriteImage.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/WriteImage.cpp index 5ffd740464..6edaf1d1fb 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/WriteImage.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/WriteImage.cpp @@ -361,8 +361,14 @@ Result<> WriteImage::operator()() // Shared per-slice writer: names the file, writes via the ImageIO layer, commits atomically. auto writeSlice = [&](std::vector& sliceBuffer, usize slice) -> Result<> { m_MessageHandler(IFilter::Message::Type::Info, fmt::format("Writing slice {}/{}", slice + 1, sliceCount)); - std::string indexStr = CreateIndexString(slice + m_InputValues.indexOffset, static_cast(m_InputValues.totalIndexDigits), m_InputValues.leadingDigitCharacter); - fs::path slicePath = parent / fmt::format("{}_{}{}", stem.string(), indexStr, ext.string()); + // A single-slice volume writes exactly the user-specified file name; the index suffix is only + // appended when multiple slices are produced. + fs::path slicePath = parent / fmt::format("{}{}", stem.string(), ext.string()); + if(sliceCount > 1) + { + std::string indexStr = CreateIndexString(slice + m_InputValues.indexOffset, static_cast(m_InputValues.totalIndexDigits), m_InputValues.leadingDigitCharacter); + slicePath = parent / fmt::format("{}_{}{}", stem.string(), indexStr, ext.string()); + } auto atomicFileResult = AtomicFile::Create(slicePath); if(atomicFileResult.invalid()) diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeSurfaceFeaturesFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeSurfaceFeaturesFilter.cpp index 5a07499910..0ae10086ca 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeSurfaceFeaturesFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeSurfaceFeaturesFilter.cpp @@ -146,7 +146,9 @@ Result ComputeSurfaceFeaturesFilter::FromSIMPLJson(const nlohmann::js results.push_back(SIMPLConversion::ConvertParameter(args, json, SIMPL::k_FeatureIdsArrayPathKey, k_FeatureGeometryPath_Key)); results.push_back( - SIMPLConversion::ConvertParameter(args, json, SIMPL::k_FeatureIdsArrayPathKey, k_CellFeatureAttributeMatrixPath_Key)); + // The feature attribute matrix comes from the created SurfaceFeatures array (e.g. "CellFeatureData"), + // not from FeatureIdsArrayPath, which lives in the cell attribute matrix (e.g. "CellData"). + SIMPLConversion::ConvertParameter(args, json, SIMPL::k_SurfaceFeaturesArrayPathKey, k_CellFeatureAttributeMatrixPath_Key)); results.push_back(SIMPLConversion::ConvertParameter(args, json, SIMPL::k_FeatureIdsArrayPathKey, k_CellFeatureIdsArrayPath_Key)); results.push_back( SIMPLConversion::ConvertParameter(args, json, SIMPL::k_SurfaceFeaturesArrayPathKey, k_SurfaceFeaturesArrayName_Key)); diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/CopyFeatureArrayToElementArrayFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/CopyFeatureArrayToElementArrayFilter.cpp index e769b368ec..2662e08f45 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/CopyFeatureArrayToElementArrayFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/CopyFeatureArrayToElementArrayFilter.cpp @@ -146,7 +146,10 @@ Result CopyFeatureArrayToElementArrayFilter::FromSIMPLJson(const nloh results.push_back( SIMPLConversion::ConvertParameter(args, json, SIMPL::k_SelectedFeatureArrayPathKey, k_SelectedFeatureArrayPath_Key)); results.push_back(SIMPLConversion::ConvertParameter(args, json, SIMPL::k_FeatureIdsArrayPathKey, k_CellFeatureIdsArrayPath_Key)); - results.push_back(SIMPLConversion::ConvertParameter(args, json, SIMPL::k_CreatedArrayNameKey, k_CreatedArraySuffix_Key)); + // Do NOT map the legacy CreatedArrayName onto k_CreatedArraySuffix_Key: the legacy filter converted a + // single array whose output name was CreatedArrayName, but in SIMPLNX that string would be appended to + // the input array name as a suffix (e.g. "AvgEuler" + "AvgEulerAngles"). Leave the suffix blank so the + // copied array keeps the input array's name. Result<> conversionResult = MergeResults(std::move(results)); diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ErodeDilateBadDataFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ErodeDilateBadDataFilter.cpp index 0ca6ab44a7..30ab525c7f 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ErodeDilateBadDataFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ErodeDilateBadDataFilter.cpp @@ -162,7 +162,11 @@ Result ErodeDilateBadDataFilter::FromSIMPLJson(const nlohmann::json& results.push_back(SIMPLConversion::ConvertParameter(args, json, SIMPL::k_ZDirOnKey, k_ZDirOn_Key)); results.push_back(SIMPLConversion::ConvertParameter(args, json, SIMPL::k_FeatureIdsArrayPathKey, k_SelectedImageGeometryPath_Key)); results.push_back(SIMPLConversion::ConvertParameter(args, json, SIMPL::k_FeatureIdsArrayPathKey, k_CellFeatureIdsArrayPath_Key)); - results.push_back(SIMPLConversion::ConvertParameter(args, json, SIMPL::k_IgnoredDataArrayPathsKey, k_IgnoredDataArrayPaths_Key)); + // IgnoredDataArrayPaths was added to the legacy filter after 6.5.49; older pipelines omit the key. + if(json.contains(SIMPL::k_IgnoredDataArrayPathsKey)) + { + results.push_back(SIMPLConversion::ConvertParameter(args, json, SIMPL::k_IgnoredDataArrayPathsKey, k_IgnoredDataArrayPaths_Key)); + } Result<> conversionResult = MergeResults(std::move(results)); diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/FillBadDataFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/FillBadDataFilter.cpp index 723da54c4c..6c988c32a7 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/FillBadDataFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/FillBadDataFilter.cpp @@ -155,7 +155,11 @@ Result FillBadDataFilter::FromSIMPLJson(const nlohmann::json& json) results.push_back(SIMPLConversion::ConvertParameter(args, json, SIMPL::k_FeatureIdsArrayPathKey, k_SelectedCellDataGroup_Key)); results.push_back(SIMPLConversion::ConvertParameter(args, json, SIMPL::k_FeatureIdsArrayPathKey, k_CellFeatureIdsArrayPath_Key)); results.push_back(SIMPLConversion::ConvertParameter(args, json, SIMPL::k_CellPhasesArrayPathKey, k_CellPhasesArrayPath_Key)); - results.push_back(SIMPLConversion::ConvertParameter(args, json, SIMPL::k_IgnoredDataArrayPathsKey, k_IgnoredDataArrayPaths_Key)); + // IgnoredDataArrayPaths was added to the legacy filter after 6.5.49; older pipelines omit the key. + if(json.contains(SIMPL::k_IgnoredDataArrayPathsKey)) + { + results.push_back(SIMPLConversion::ConvertParameter(args, json, SIMPL::k_IgnoredDataArrayPathsKey, k_IgnoredDataArrayPaths_Key)); + } Result<> conversionResult = MergeResults(std::move(results)); diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/RequireMinNumNeighborsFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/RequireMinNumNeighborsFilter.cpp index d3e5665c9a..6d625dfc69 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/RequireMinNumNeighborsFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/RequireMinNumNeighborsFilter.cpp @@ -199,7 +199,11 @@ Result RequireMinNumNeighborsFilter::FromSIMPLJson(const nlohmann::js results.push_back(SIMPLConversion::ConvertParameter(args, json, SIMPL::k_FeatureIdsArrayPathKey, k_FeatureIdsPath_Key)); results.push_back(SIMPLConversion::ConvertParameter(args, json, SIMPL::k_FeaturePhasesArrayPathKey, k_FeaturePhasesPath_Key)); results.push_back(SIMPLConversion::ConvertParameter(args, json, SIMPL::k_NumNeighborsArrayPathKey, k_NumNeighborsPath_Key)); - results.push_back(SIMPLConversion::ConvertParameter(args, json, SIMPL::k_IgnoredDataArrayPathsKey, k_IgnoredVoxelArrays_Key)); + // IgnoredDataArrayPaths was added to the legacy filter after 6.5.49; older pipelines omit the key. + if(json.contains(SIMPL::k_IgnoredDataArrayPathsKey)) + { + results.push_back(SIMPLConversion::ConvertParameter(args, json, SIMPL::k_IgnoredDataArrayPathsKey, k_IgnoredVoxelArrays_Key)); + } Result<> conversionResult = MergeResults(std::move(results)); diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteImageFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteImageFilter.cpp index 2f521e7d92..184f5b914c 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteImageFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteImageFilter.cpp @@ -281,9 +281,15 @@ IFilter::PreflightResult WriteImageFilter::preflightImpl(const DataStructure& da break; } - // 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()); + // Generate example filename for PreflightValues. A single-slice volume writes exactly the + // user-specified file name; the index suffix is only appended when multiple slices are produced. + const fs::path absoluteParentPath = fs::absolute(filePath).parent_path(); + std::string exampleFileName = (absoluteParentPath / fmt::format("{}{}", filePath.stem().string(), filePath.extension().string())).string(); + if(maxSlice > 1) + { + const std::string indexStr = CreateIndexString(maxSlice, static_cast(totalDigits), fillChar); + exampleFileName = (absoluteParentPath / fmt::format("{}_{}{}", filePath.stem().string(), indexStr, filePath.extension().string())).string(); + } Result resultOutputActions; std::vector preflightUpdatedValues; diff --git a/src/Plugins/SimplnxCore/test/CopyFeatureArrayToElementArrayTest.cpp b/src/Plugins/SimplnxCore/test/CopyFeatureArrayToElementArrayTest.cpp index c0b3192d5b..1bf19975e3 100644 --- a/src/Plugins/SimplnxCore/test/CopyFeatureArrayToElementArrayTest.cpp +++ b/src/Plugins/SimplnxCore/test/CopyFeatureArrayToElementArrayTest.cpp @@ -189,7 +189,9 @@ TEST_CASE("SimplnxCore::CopyFeatureArrayToElementArrayFilter: SIMPL Backwards Co const Arguments args = pipelineFilter->getArguments(); // Complex type (SingleToMultiDataPathSelectionFilterParameterConverter) - verified by successful pipeline loading CHECK(args.value(CopyFeatureArrayToElementArrayFilter::k_CellFeatureIdsArrayPath_Key) == DataPath({"DataContainer", "CellData", "TestArray"})); - CHECK(args.value(CopyFeatureArrayToElementArrayFilter::k_CreatedArraySuffix_Key) == "TestName"); + // The legacy CreatedArrayName is intentionally NOT mapped onto the suffix; the copied array keeps + // the input array's name, so the suffix stays at its default (empty). + CHECK(args.value(CopyFeatureArrayToElementArrayFilter::k_CreatedArraySuffix_Key).empty()); } } } diff --git a/src/Plugins/SimplnxCore/test/WriteImageTest.cpp b/src/Plugins/SimplnxCore/test/WriteImageTest.cpp index e221c361b2..eca8cc63dd 100644 --- a/src/Plugins/SimplnxCore/test/WriteImageTest.cpp +++ b/src/Plugins/SimplnxCore/test/WriteImageTest.cpp @@ -1,6 +1,7 @@ #include #include "SimplnxCore/Filters/CreateColorMapFilter.hpp" +#include "SimplnxCore/Filters/ReadImageFilter.hpp" #include "SimplnxCore/Filters/ReadImageStackFilter.hpp" #include "SimplnxCore/Filters/WriteImageFilter.hpp" #include "SimplnxCore/SimplnxCore_test_dirs.hpp" @@ -123,6 +124,21 @@ void validateOutputFiles(size_t numImages, uint64 offset, const std::string& tem REQUIRE(count == 0); } +// Reads a single written image file back into `readDs` as ImageGeom "ReadGeom". Single-slice writes +// do not append an index to the file name, so tests read the file back directly with ReadImageFilter +// instead of through a generated file list. +void readBackSingleImage(DataStructure& readDs, const fs::path& imagePath) +{ + ReadImageFilter reader; + Arguments rArgs; + rArgs.insertOrAssign(ReadImageFilter::k_FileName_Key, std::make_any(imagePath)); + rArgs.insertOrAssign(ReadImageFilter::k_ImageGeometryPath_Key, std::make_any(DataPath({"ReadGeom"}))); + auto readerPreflight = reader.preflight(readDs, rArgs); + SIMPLNX_RESULT_REQUIRE_VALID(readerPreflight.outputActions); + auto readerExecute = reader.execute(readDs, rArgs); + SIMPLNX_RESULT_REQUIRE_VALID(readerExecute.result); +} + // Writes a single-slice scalar ramp of type T through the inline color-table path, then compares the // read-back RGB image tuple-by-tuple against an independent CreateColorMap -> RGB reference. The oracle // is the real CreateColorMap chain, so this asserts parity across every numeric input type. @@ -208,27 +224,7 @@ void RunColorRoundtripForType() // Read the inline-written slice back and compare pixel RGB to the Create Color Map result. DataStructure readDs; - { - ReadImageStackFilter reader; - GeneratedFileListParameter::ValueType fileList; - fileList.inputPath = inlineDir.path().string(); - fileList.startIndex = 0; - fileList.endIndex = 0; - fileList.incrementIndex = 1; - fileList.fileExtension = ".tif"; - fileList.filePrefix = "slice_"; - fileList.fileSuffix = ""; - fileList.paddingDigits = 3; - fileList.ordering = GeneratedFileListParameter::Ordering::LowToHigh; - - Arguments rArgs; - rArgs.insertOrAssign(ReadImageStackFilter::k_InputFileListInfo_Key, std::make_any(fileList)); - rArgs.insertOrAssign(ReadImageStackFilter::k_ImageGeometryPath_Key, std::make_any(DataPath({"ReadGeom"}))); - auto readerPreflight = reader.preflight(readDs, rArgs); - SIMPLNX_RESULT_REQUIRE_VALID(readerPreflight.outputActions); - auto readerExecute = reader.execute(readDs, rArgs); - SIMPLNX_RESULT_REQUIRE_VALID(readerExecute.result); - } + readBackSingleImage(readDs, inlineDir.path() / "slice.tif"); // Locate the single RGB image-data array created by the reader and compare tuple-by-tuple. const auto& readGeom = readDs.getDataRefAs(DataPath({"ReadGeom"})); @@ -237,7 +233,7 @@ void RunColorRoundtripForType() REQUIRE(readArrayPtr != nullptr); const auto& readStore = readArrayPtr->getDataStoreRef(); - // ReadImageStackFilter reads the color TIFF back as a 3-component (RGB) uint8 array; compare only + // ReadImageFilter reads the color TIFF back as a 3-component (RGB) uint8 array; compare only // the R,G,B components per pixel so the assertion is robust if a reader ever emits RGBA. const usize readComps = readArrayPtr->getNumberOfComponents(); const usize numPixels = readStore.getNumberOfTuples(); @@ -413,6 +409,63 @@ TEST_CASE("SimplnxCore::WriteImageFilter: Write Stack", "[SimplnxCore][WriteImag UnitTest::CheckArraysInheritTupleDims(dataStructure); } +TEST_CASE("SimplnxCore::WriteImageFilter: Single-slice write omits the index suffix", "[SimplnxCore][WriteImageFilter]") +{ + auto app = Application::GetOrCreateInstance(); + UnitTest::LoadPlugins(); + + DataStructure dataStructure; + auto* imageGeomPtr = ImageGeom::Create(dataStructure, "ImageGeometry"); + imageGeomPtr->setDimensions({4, 4, 1}); + auto* cellAmPtr = AttributeMatrix::Create(dataStructure, "CellData", {1, 4, 4}, imageGeomPtr->getId()); + imageGeomPtr->setCellData(*cellAmPtr); + UnitTest::CreateTestDataArray(dataStructure, "Scalar", {1, 4, 4}, {1}, cellAmPtr->getId()); + + const DataPath geomPath({"ImageGeometry"}); + const DataPath scalarPath = geomPath.createChildPath("CellData").createChildPath("Scalar"); + + ScopedTempDir tempDir(fs::path(unit_test::k_BinaryTestOutputDir.view())); + const fs::path outputPath = tempDir.path() / "single.tif"; + + WriteImageFilter filter; + Arguments args; + args.insertOrAssign(WriteImageFilter::k_ImageGeomPath_Key, std::make_any(geomPath)); + args.insertOrAssign(WriteImageFilter::k_ImageArrayPath_Key, std::make_any(scalarPath)); + args.insertOrAssign(WriteImageFilter::k_FileName_Key, std::make_any(outputPath)); + args.insertOrAssign(WriteImageFilter::k_Plane_Key, std::make_any(k_XYPlane)); + + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); + + // The preflight example previews the exact single-slice file name (no index suffix). + bool foundExample = false; + for(const auto& value : preflightResult.outputValues) + { + if(value.name == "Example Output File") + { + foundExample = true; + REQUIRE(value.value == fs::absolute(outputPath).string()); + } + } + REQUIRE(foundExample); + + auto executeResult = filter.execute(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); + + // The single slice is written with exactly the user-specified name and no other files appear. + REQUIRE(fs::exists(outputPath)); + REQUIRE(!fs::exists(tempDir.path() / "single_000.tif")); + usize fileCount = 0; + for(const auto& entry : fs::directory_iterator(tempDir.path())) + { + fileCount++; + } + REQUIRE(fileCount == 1); + REQUIRE(fs::remove(outputPath)); + + UnitTest::CheckArraysInheritTupleDims(dataStructure); +} + TEST_CASE("SimplnxCore::WriteImageFilter: Color table preflight validation", "[SimplnxCore][WriteImageFilter]") { auto app = Application::GetOrCreateInstance(); @@ -728,25 +781,7 @@ TEST_CASE("SimplnxCore::WriteImageFilter: Inline color table mask and constant-a // Reads a single-slice color image back from a directory and returns the pointer to the RGB array. auto readBackSlice = [&](const fs::path& dir, DataStructure& readDs) -> const UInt8Array* { - ReadImageStackFilter reader; - GeneratedFileListParameter::ValueType fileList; - fileList.inputPath = dir.string(); - fileList.startIndex = 0; - fileList.endIndex = 0; - fileList.incrementIndex = 1; - fileList.fileExtension = ".tif"; - fileList.filePrefix = "slice_"; - fileList.fileSuffix = ""; - fileList.paddingDigits = 3; - fileList.ordering = GeneratedFileListParameter::Ordering::LowToHigh; - - Arguments rArgs; - rArgs.insertOrAssign(ReadImageStackFilter::k_InputFileListInfo_Key, std::make_any(fileList)); - rArgs.insertOrAssign(ReadImageStackFilter::k_ImageGeometryPath_Key, std::make_any(DataPath({"ReadGeom"}))); - auto rPreflight = reader.preflight(readDs, rArgs); - SIMPLNX_RESULT_REQUIRE_VALID(rPreflight.outputActions); - auto rExecute = reader.execute(readDs, rArgs); - SIMPLNX_RESULT_REQUIRE_VALID(rExecute.result); + readBackSingleImage(readDs, dir / "slice.tif"); const auto& readGeom = readDs.getDataRefAs(DataPath({"ReadGeom"})); return dynamic_cast(readGeom.getCellData()->begin()->second.get()); }; @@ -958,25 +993,7 @@ TEST_CASE("SimplnxCore::WriteImageFilter: Flip output image about X or Y axis", UnitTest::CheckArraysInheritTupleDims(dataStructure); DataStructure readDs; - ReadImageStackFilter reader; - GeneratedFileListParameter::ValueType fileList; - fileList.inputPath = tempDir.path().string(); - fileList.startIndex = 0; - fileList.endIndex = 0; - fileList.incrementIndex = 1; - fileList.fileExtension = ".png"; - fileList.filePrefix = "slice_"; - fileList.fileSuffix = ""; - fileList.paddingDigits = 3; - fileList.ordering = GeneratedFileListParameter::Ordering::LowToHigh; - - Arguments rArgs; - rArgs.insertOrAssign(ReadImageStackFilter::k_InputFileListInfo_Key, std::make_any(fileList)); - rArgs.insertOrAssign(ReadImageStackFilter::k_ImageGeometryPath_Key, std::make_any(DataPath({"ReadGeom"}))); - auto readerPreflight = reader.preflight(readDs, rArgs); - SIMPLNX_RESULT_REQUIRE_VALID(readerPreflight.outputActions); - auto readerExecute = reader.execute(readDs, rArgs); - SIMPLNX_RESULT_REQUIRE_VALID(readerExecute.result); + readBackSingleImage(readDs, tempDir.path() / "slice.png"); const auto& readGeom = readDs.getDataRefAs(DataPath({"ReadGeom"})); const auto* readArrayPtr = dynamic_cast(readGeom.getCellData()->begin()->second.get()); @@ -1085,25 +1102,7 @@ TEST_CASE("SimplnxCore::WriteImageFilter: Output flip composes with the color-ta UnitTest::CheckArraysInheritTupleDims(dataStructure); DataStructure readDs; - ReadImageStackFilter reader; - GeneratedFileListParameter::ValueType fileList; - fileList.inputPath = tempDir.path().string(); - fileList.startIndex = 0; - fileList.endIndex = 0; - fileList.incrementIndex = 1; - fileList.fileExtension = ".tif"; - fileList.filePrefix = "slice_"; - fileList.fileSuffix = ""; - fileList.paddingDigits = 3; - fileList.ordering = GeneratedFileListParameter::Ordering::LowToHigh; - - Arguments rArgs; - rArgs.insertOrAssign(ReadImageStackFilter::k_InputFileListInfo_Key, std::make_any(fileList)); - rArgs.insertOrAssign(ReadImageStackFilter::k_ImageGeometryPath_Key, std::make_any(DataPath({"ReadGeom"}))); - auto readerPreflight = reader.preflight(readDs, rArgs); - SIMPLNX_RESULT_REQUIRE_VALID(readerPreflight.outputActions); - auto readerExecute = reader.execute(readDs, rArgs); - SIMPLNX_RESULT_REQUIRE_VALID(readerExecute.result); + readBackSingleImage(readDs, tempDir.path() / "slice.tif"); const auto& readGeom = readDs.getDataRefAs(DataPath({"ReadGeom"})); const auto* readArrayPtr = dynamic_cast(readGeom.getCellData()->begin()->second.get()); @@ -1293,25 +1292,7 @@ TEST_CASE("SimplnxCore::WriteImageFilter: Scale bar pads the written image with } UnitTest::CheckArraysInheritTupleDims(dataStructure); - ReadImageStackFilter reader; - GeneratedFileListParameter::ValueType fileList; - fileList.inputPath = tempDir.path().string(); - fileList.startIndex = 0; - fileList.endIndex = 0; - fileList.incrementIndex = 1; - fileList.fileExtension = ".png"; - fileList.filePrefix = "slice_"; - fileList.fileSuffix = ""; - fileList.paddingDigits = 3; - fileList.ordering = GeneratedFileListParameter::Ordering::LowToHigh; - - Arguments rArgs; - rArgs.insertOrAssign(ReadImageStackFilter::k_InputFileListInfo_Key, std::make_any(fileList)); - rArgs.insertOrAssign(ReadImageStackFilter::k_ImageGeometryPath_Key, std::make_any(DataPath({"ReadGeom"}))); - auto readerPreflight = reader.preflight(readDs, rArgs); - SIMPLNX_RESULT_REQUIRE_VALID(readerPreflight.outputActions); - auto readerExecute = reader.execute(readDs, rArgs); - SIMPLNX_RESULT_REQUIRE_VALID(readerExecute.result); + readBackSingleImage(readDs, tempDir.path() / "slice.png"); }; // Expected layout for 200x100 @ 0.5 µm/px: @@ -1480,24 +1461,7 @@ TEST_CASE("SimplnxCore::WriteImageFilter: Scale bar composes with flip and RGB i UnitTest::CheckArraysInheritTupleDims(dataStructure); DataStructure readDs; - ReadImageStackFilter reader; - GeneratedFileListParameter::ValueType fileList; - fileList.inputPath = tempDir.path().string(); - fileList.startIndex = 0; - fileList.endIndex = 0; - fileList.incrementIndex = 1; - fileList.fileExtension = ".png"; - fileList.filePrefix = "slice_"; - fileList.fileSuffix = ""; - fileList.paddingDigits = 3; - fileList.ordering = GeneratedFileListParameter::Ordering::LowToHigh; - Arguments rArgs; - rArgs.insertOrAssign(ReadImageStackFilter::k_InputFileListInfo_Key, std::make_any(fileList)); - rArgs.insertOrAssign(ReadImageStackFilter::k_ImageGeometryPath_Key, std::make_any(DataPath({"ReadGeom"}))); - auto readerPreflight = reader.preflight(readDs, rArgs); - SIMPLNX_RESULT_REQUIRE_VALID(readerPreflight.outputActions); - auto readerExecute = reader.execute(readDs, rArgs); - SIMPLNX_RESULT_REQUIRE_VALID(readerExecute.result); + readBackSingleImage(readDs, tempDir.path() / "slice.png"); REQUIRE_NOTHROW(readDs.getDataRefAs(DataPath({"ReadGeom"}))); const auto& readGeom = readDs.getDataRefAs(DataPath({"ReadGeom"}));