From 3a1e77e8b36a163ead059cebe415fb56749533ce Mon Sep 17 00:00:00 2001
From: Michael Jackson <mike.jackson@bluequartz.net>
Date: Wed, 7 Feb 2024 12:38:06 -0500
Subject: [PATCH] BUG: GeneratePoleFigure-Only create RGB when creating the
 Image Geometry (#827)

Signed-off-by: Michael Jackson <mike.jackson@bluequartz.net>
---
 .../docs/WritePoleFigureFilter.md             | 21 ++++++--
 .../Filters/Algorithms/WritePoleFigure.cpp    | 46 ++++++++++------
 .../test/WritePoleFigureTest.cpp              | 53 ++++++++++++++++---
 3 files changed, 94 insertions(+), 26 deletions(-)

diff --git a/src/Plugins/OrientationAnalysis/docs/WritePoleFigureFilter.md b/src/Plugins/OrientationAnalysis/docs/WritePoleFigureFilter.md
index 36554c8f68..b0ca5ae9a7 100644
--- a/src/Plugins/OrientationAnalysis/docs/WritePoleFigureFilter.md
+++ b/src/Plugins/OrientationAnalysis/docs/WritePoleFigureFilter.md
@@ -6,7 +6,20 @@ IO (Output)
 
 ## Description
 
-This **Filter** creates a standard pole figure image for each **Ensemble** in a selected **Data Container** with an **Image Geometry**. The **Filter** uses Euler angles in radians and requires the crystal structures for each **Ensemble** array and the corresponding **Ensemble** Ids on the **Cells**. The **Filter** also optionally can use a *mask* array to determine which **Cells** are valid for the pole figure computation.
+This **Filter** creates a standard crystallographic pole figure image for each **Ensemble** (phase)in a selected **Data Container**. The **Filter** uses Euler angles in radians and requires the crystal structures and material names for each **Ensemble** array and the corresponding **Ensemble** Ids on the **Cells**. The **Filter** also optionally can use a *mask* array to determine which **Cells** are valid for the pole figure computation.
+
+In a practicale sense, this means that the following information is available to the filter:
+
+- Cell Level
+
+  - Euler Angles (Float 32) ordered as sets of (phi1, Phi, phi2).
+  - Phases (Int32) This is the phase that each Euler angle belongs to
+  - Optional Mask(boolean or uint8) True/1 if the Euler angle should be included in the pole figure.
+
+- Ensemble Level (Phase Information)
+
+  - Laue Class (UInt32)
+  - Material Names (String)
 
 ### Algorithm Choice
 
@@ -18,7 +31,7 @@ This **Filter** creates a standard pole figure image for each **Ensemble** in a
 
 ### Layout
 
-The 3 pole figures can be laid out in a Square, Horizontal row or vertical column. Supporting informatio (including the color bar legend for color pole figures) will also be printed on the image.
+The 3 pole figures can be laid out in a Square, Horizontal row or vertical column. Supporting information (including the color bar legend for color pole figures) will also be printed on the image.
 
 | Lambert Projection | Discrete |
 |--------------------|----------|
@@ -28,8 +41,8 @@ The 3 pole figures can be laid out in a Square, Horizontal row or vertical colum
 
 ## Example Pipelines
 
-+ TxCopper_Exposed
-+ TxCopper_Unexposed
+- TxCopper_Exposed
+- TxCopper_Unexposed
 
 ## License & Copyright
 
diff --git a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/WritePoleFigure.cpp b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/WritePoleFigure.cpp
index b86321b36f..72c9b5625b 100644
--- a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/WritePoleFigure.cpp
+++ b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/WritePoleFigure.cpp
@@ -362,21 +362,14 @@ Result<> WritePoleFigure::operator()()
   // Find how many phases we have by getting the number of Crystal Structures
   const size_t numPhases = crystalStructures.getNumberOfTuples();
 
-  // Loop over all the voxels gathering the Eulers for a specific phase into an array
+  // Create the Image Geometry that will serve as the final storage location for each
+  // pole figure. We are just giving it a default size for now, it will be resized
+  // further down the algorithm.
   std::vector<usize> tupleShape = {1, static_cast<usize>(m_InputValues->ImageSize), static_cast<usize>(m_InputValues->ImageSize)};
   auto& imageGeom = m_DataStructure.getDataRefAs<ImageGeom>(m_InputValues->OutputImageGeometryPath);
   auto cellAttrMatPath = imageGeom.getCellDataPath();
   imageGeom.setDimensions({static_cast<usize>(m_InputValues->ImageSize), static_cast<usize>(m_InputValues->ImageSize), 1});
   imageGeom.getCellData()->resizeTuples(tupleShape);
-  for(size_t phase = 1; phase < numPhases; ++phase)
-  {
-    auto imageArrayPath = cellAttrMatPath.createChildPath(fmt::format("{}Phase_{}", m_InputValues->ImagePrefix, phase));
-    auto arrayCreationResult = nx::core::CreateArray<uint8>(m_DataStructure, tupleShape, {4ULL}, imageArrayPath, IDataAction::Mode::Execute);
-    if(arrayCreationResult.invalid())
-    {
-      return arrayCreationResult;
-    }
-  }
 
   // Loop over all the voxels gathering the Eulers for a specific phase into an array
   for(size_t phase = 1; phase < numPhases; ++phase)
@@ -666,22 +659,43 @@ Result<> WritePoleFigure::operator()()
       }
 
       // Fetch the rendered RGBA pixels from the entire canvas.
-      std::vector<unsigned char> image(static_cast<size_t>(pageHeight * pageWidth * 4));
-      context.get_image_data(image.data(), pageWidth, pageHeight, pageWidth * 4, 0, 0);
+      std::vector<unsigned char> rgbaCanvasImage(static_cast<size_t>(pageHeight * pageWidth * 4));
+      context.get_image_data(rgbaCanvasImage.data(), pageWidth, pageHeight, pageWidth * 4, 0, 0);
       if(m_InputValues->SaveAsImageGeometry)
       {
+        // Ensure the final Image Geometry is sized correctly.
         imageGeom.setDimensions({static_cast<usize>(pageWidth), static_cast<usize>(pageHeight), 1});
         imageGeom.getCellData()->resizeTuples({1, static_cast<usize>(pageHeight), static_cast<usize>(pageWidth)});
+        tupleShape[0] = 1;
+        tupleShape[1] = pageHeight;
+        tupleShape[2] = pageWidth;
+        // Create an output array to hold the RGB formatted color image
+        auto imageArrayPath = cellAttrMatPath.createChildPath(fmt::format("{}{}", m_InputValues->ImagePrefix, phase));
+        auto arrayCreationResult = nx::core::CreateArray<uint8>(m_DataStructure, tupleShape, {3ULL}, imageArrayPath, IDataAction::Mode::Execute);
+        if(arrayCreationResult.invalid())
+        {
+          return arrayCreationResult;
+        }
 
-        auto imageArrayPath = cellAttrMatPath.createChildPath(fmt::format("{}Phase_{}", m_InputValues->ImagePrefix, phase));
+        // Get a reference to the RGB final array and then copy ONLY the RGB pixels from the
+        // canvas RGBA data.
         auto& imageData = m_DataStructure.getDataRefAs<UInt8Array>(imageArrayPath);
-        std::copy(image.begin(), image.end(), imageData.begin());
+
+        imageData.fill(0);
+        size_t tupleCount = pageHeight * pageWidth;
+        for(size_t t = 0; t < tupleCount; t++)
+        {
+          imageData[t * 3 + 0] = rgbaCanvasImage[t * 4 + 0];
+          imageData[t * 3 + 1] = rgbaCanvasImage[t * 4 + 1];
+          imageData[t * 3 + 2] = rgbaCanvasImage[t * 4 + 2];
+        }
       }
 
+      // Write out the full RGBA data
       if(m_InputValues->WriteImageToDisk)
       {
-        const std::string filename = fmt::format("{}/{}Phase_{}.tiff", m_InputValues->OutputPath.string(), m_InputValues->ImagePrefix, phase);
-        auto result = TiffWriter::WriteImage(filename, pageWidth, pageHeight, 4, image.data());
+        const std::string filename = fmt::format("{}/{}{}.tiff", m_InputValues->OutputPath.string(), m_InputValues->ImagePrefix, phase);
+        auto result = TiffWriter::WriteImage(filename, pageWidth, pageHeight, 4, rgbaCanvasImage.data());
         if(result.first < 0)
         {
           return MakeErrorResult(-53900, fmt::format("Error writing pole figure image '{}' to disk.\n    Error Code from Tiff Writer: {}\n    Message: {}", filename, result.first, result.second));
diff --git a/src/Plugins/OrientationAnalysis/test/WritePoleFigureTest.cpp b/src/Plugins/OrientationAnalysis/test/WritePoleFigureTest.cpp
index 76e48687f7..f3126ef256 100644
--- a/src/Plugins/OrientationAnalysis/test/WritePoleFigureTest.cpp
+++ b/src/Plugins/OrientationAnalysis/test/WritePoleFigureTest.cpp
@@ -21,8 +21,43 @@ using namespace nx::core::UnitTest;
 namespace
 {
 const std::string k_ImagePrefix("fw-ar-IF1-aptr12-corr Discrete Pole Figure");
+
+template <typename T>
+void CompareComponentsOfArrays(const DataStructure& dataStructure, const DataPath& exemplaryDataPath, const DataPath& computedPath, usize compIndex)
+{
+  // DataPath exemplaryDataPath = featureGroup.createChildPath("SurfaceFeatures");
+  REQUIRE_NOTHROW(dataStructure.getDataRefAs<DataArray<T>>(exemplaryDataPath));
+  REQUIRE_NOTHROW(dataStructure.getDataRefAs<DataArray<T>>(computedPath));
+
+  const auto& exemplaryDataArray = dataStructure.getDataRefAs<DataArray<T>>(exemplaryDataPath);
+  const auto& generatedDataArray = dataStructure.getDataRefAs<DataArray<T>>(computedPath);
+  REQUIRE(generatedDataArray.getNumberOfTuples() == exemplaryDataArray.getNumberOfTuples());
+
+  usize exemplaryNumComp = exemplaryDataArray.getNumberOfComponents();
+  usize generatedNumComp = generatedDataArray.getNumberOfComponents();
+
+  REQUIRE(exemplaryNumComp == 4);
+  REQUIRE(generatedNumComp == 3);
+
+  REQUIRE(compIndex < exemplaryNumComp);
+  REQUIRE(compIndex < generatedNumComp);
+
+  INFO(fmt::format("Bad Comparison\n  Input Data Array:'{}'\n  Output DataArray: '{}'", exemplaryDataPath.toString(), computedPath.toString()));
+
+  usize start = 0;
+  usize numTuples = exemplaryDataArray.getNumberOfTuples();
+  for(usize i = start; i < numTuples; i++)
+  {
+    auto oldVal = exemplaryDataArray[i * exemplaryNumComp + compIndex];
+    auto newVal = generatedDataArray[i * generatedNumComp + compIndex];
+    INFO(fmt::format("Index: {} Comp: {}", i, compIndex));
+
+    REQUIRE(oldVal == newVal);
+  }
 }
 
+} // namespace
+
 TEST_CASE("OrientationAnalysis::WritePoleFigureFilter-1", "[OrientationAnalysis][WritePoleFigureFilter]")
 {
   Application::GetOrCreateInstance()->loadPlugins(unit_test::k_BuildDir.view(), true);
@@ -51,7 +86,7 @@ TEST_CASE("OrientationAnalysis::WritePoleFigureFilter-1", "[OrientationAnalysis]
   args.insertOrAssign(WritePoleFigureFilter::k_UseMask_Key, std::make_any<bool>(false));
   args.insertOrAssign(WritePoleFigureFilter::k_ImageGeometryPath_Key, std::make_any<DataPath>(DataPath({"fw-ar-IF1-aptr12-corr Discrete Pole Figure [CALCULATED]"})));
 
-  DataPath calculatedImageData({"fw-ar-IF1-aptr12-corr Discrete Pole Figure [CALCULATED]", "CellData", fmt::format("{}Phase_{}", k_ImagePrefix, 1)});
+  DataPath calculatedImageData({"fw-ar-IF1-aptr12-corr Discrete Pole Figure [CALCULATED]", "CellData", fmt::format("{}{}", k_ImagePrefix, 1)});
   DataPath exemplarImageData({"fw-ar-IF1-aptr12-corr Discrete Pole Figure", "CellData", "Image"});
 
   args.insertOrAssign(WritePoleFigureFilter::k_CellEulerAnglesArrayPath_Key, std::make_any<DataPath>(DataPath({"fw-ar-IF1-aptr12-corr", "Cell Data", "EulerAngles"})));
@@ -71,7 +106,9 @@ TEST_CASE("OrientationAnalysis::WritePoleFigureFilter-1", "[OrientationAnalysis]
   WriteTestDataStructure(dataStructure, fmt::format("{}/write_pole_figure-1.dream3d", unit_test::k_BinaryTestOutputDir));
 #endif
 
-  CompareArrays<uint8>(dataStructure, exemplarImageData, calculatedImageData);
+  CompareComponentsOfArrays<uint8>(dataStructure, exemplarImageData, calculatedImageData, 0);
+  CompareComponentsOfArrays<uint8>(dataStructure, exemplarImageData, calculatedImageData, 1);
+  CompareComponentsOfArrays<uint8>(dataStructure, exemplarImageData, calculatedImageData, 2);
 }
 
 TEST_CASE("OrientationAnalysis::WritePoleFigureFilter-2", "[OrientationAnalysis][WritePoleFigureFilter]")
@@ -102,7 +139,7 @@ TEST_CASE("OrientationAnalysis::WritePoleFigureFilter-2", "[OrientationAnalysis]
   args.insertOrAssign(WritePoleFigureFilter::k_UseMask_Key, std::make_any<bool>(true));
   args.insertOrAssign(WritePoleFigureFilter::k_ImageGeometryPath_Key, std::make_any<DataPath>(DataPath({"fw-ar-IF1-aptr12-corr Discrete Pole Figure Masked [CALCULATED]"})));
 
-  DataPath calculatedImageData({"fw-ar-IF1-aptr12-corr Discrete Pole Figure Masked [CALCULATED]", "CellData", fmt::format("{}Phase_{}", k_ImagePrefix, 1)});
+  DataPath calculatedImageData({"fw-ar-IF1-aptr12-corr Discrete Pole Figure Masked [CALCULATED]", "CellData", fmt::format("{}{}", k_ImagePrefix, 1)});
   DataPath exemplarImageData({"fw-ar-IF1-aptr12-corr Discrete Pole Figure Masked", "CellData", "Image"});
 
   args.insertOrAssign(WritePoleFigureFilter::k_CellEulerAnglesArrayPath_Key, std::make_any<DataPath>(DataPath({"fw-ar-IF1-aptr12-corr", "Cell Data", "EulerAngles"})));
@@ -123,7 +160,9 @@ TEST_CASE("OrientationAnalysis::WritePoleFigureFilter-2", "[OrientationAnalysis]
   WriteTestDataStructure(dataStructure, fmt::format("{}/write_pole_figure-2.dream3d", unit_test::k_BinaryTestOutputDir));
 #endif
 
-  CompareArrays<uint8>(dataStructure, exemplarImageData, calculatedImageData);
+  CompareComponentsOfArrays<uint8>(dataStructure, exemplarImageData, calculatedImageData, 0);
+  CompareComponentsOfArrays<uint8>(dataStructure, exemplarImageData, calculatedImageData, 1);
+  CompareComponentsOfArrays<uint8>(dataStructure, exemplarImageData, calculatedImageData, 2);
 }
 
 TEST_CASE("OrientationAnalysis::WritePoleFigureFilter-3", "[OrientationAnalysis][WritePoleFigureFilter]")
@@ -154,7 +193,7 @@ TEST_CASE("OrientationAnalysis::WritePoleFigureFilter-3", "[OrientationAnalysis]
   args.insertOrAssign(WritePoleFigureFilter::k_UseMask_Key, std::make_any<bool>(true));
   args.insertOrAssign(WritePoleFigureFilter::k_ImageGeometryPath_Key, std::make_any<DataPath>(DataPath({"fw-ar-IF1-aptr12-corr Discrete Pole Figure Masked Color [CALCULATED]"})));
 
-  DataPath calculatedImageData({"fw-ar-IF1-aptr12-corr Discrete Pole Figure Masked Color [CALCULATED]", "CellData", fmt::format("{}Phase_{}", k_ImagePrefix, 1)});
+  DataPath calculatedImageData({"fw-ar-IF1-aptr12-corr Discrete Pole Figure Masked Color [CALCULATED]", "CellData", fmt::format("{}{}", k_ImagePrefix, 1)});
   DataPath exemplarImageData({"fw-ar-IF1-aptr12-corr Discrete Pole Figure Masked Color", "CellData", "Image"});
 
   args.insertOrAssign(WritePoleFigureFilter::k_CellEulerAnglesArrayPath_Key, std::make_any<DataPath>(DataPath({"fw-ar-IF1-aptr12-corr", "Cell Data", "EulerAngles"})));
@@ -175,5 +214,7 @@ TEST_CASE("OrientationAnalysis::WritePoleFigureFilter-3", "[OrientationAnalysis]
   WriteTestDataStructure(dataStructure, fmt::format("{}/write_pole_figure-3.dream3d", unit_test::k_BinaryTestOutputDir));
 #endif
 
-  CompareArrays<uint8>(dataStructure, exemplarImageData, calculatedImageData);
+  CompareComponentsOfArrays<uint8>(dataStructure, exemplarImageData, calculatedImageData, 0);
+  CompareComponentsOfArrays<uint8>(dataStructure, exemplarImageData, calculatedImageData, 1);
+  CompareComponentsOfArrays<uint8>(dataStructure, exemplarImageData, calculatedImageData, 2);
 }