From f510785e5efb577ac4af50abc122673381183978 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Mon, 2 Mar 2026 22:18:59 -0500 Subject: [PATCH 01/14] ENH: Generate Inverse Pole Figure Images. Signed-off-by: Michael Jackson --- .claude/settings.local.json | 46 ++ Source/Apps/SourceList.cmake | 7 + Source/Apps/generate_ipf_density.cpp | 447 ++++++++++++++++++ Source/EbsdLib/LaueOps/LaueOps.cpp | 81 ++++ Source/EbsdLib/LaueOps/LaueOps.h | 11 + .../Utilities/InversePoleFigureUtilities.cpp | 290 ++++++++++++ .../Utilities/InversePoleFigureUtilities.h | 131 +++++ Source/EbsdLib/Utilities/SourceList.cmake | 2 + Source/Test/CMakeLists.txt | 2 +- Source/Test/InversePoleFigureTest.cpp | 366 ++++++++++++++ 10 files changed, 1382 insertions(+), 1 deletion(-) create mode 100644 .claude/settings.local.json create mode 100644 Source/Apps/generate_ipf_density.cpp create mode 100644 Source/EbsdLib/Utilities/InversePoleFigureUtilities.cpp create mode 100644 Source/EbsdLib/Utilities/InversePoleFigureUtilities.h create mode 100644 Source/Test/InversePoleFigureTest.cpp diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..4c48850 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,46 @@ +{ + "permissions": { + "allow": [ + "Bash(ls:*)", + "Bash(cmake --build:*)", + "Bash(ctest:*)", + "Bash(cmake:*)", + "Bash(python3:*)", + "Bash(ninja -t targets:*)", + "Bash(git rm:*)", + "Bash(tar:*)", + "Bash(test:*)", + "Bash(git commit:*)", + "Bash(git push:*)", + "Bash(/Users/mjackson/Workspace5/DREAM3D-Build/NX-Com-Qt69-Vtk95-Rel/Bin/SimplnxCoreUnitTest:*)", + "Bash(/opt/local/cmake-3.30.3-macos-universal/CMake.app/Contents/bin/cmake:*)", + "Bash(for f in segment_features_neighbor_scheme_test.tar.gz segment_features_test_data.tar.gz)", + "Bash(do /opt/local/cmake-3.30.3-macos-universal/CMake.app/Contents/bin/cmake -E tar xzf \"$f\")", + "Bash(done)", + "Bash(for f in 6_5_test_data_1_v2.tar.gz segment_features_test_data.tar.gz 6_6_ebsd_segment_features.tar.gz segment_features_neighbor_scheme_test.tar.gz)", + "Bash(do echo \"Extracting $f...\")", + "Bash(echo:*)", + "Bash(for f in 6_5_test_data_1_v2.tar.gz segment_features_test_data.tar.gz segment_features_neighbor_scheme_test.tar.gz)", + "Bash(do tar -xzf \"$f\")", + "Bash(/Users/mjackson/Workspace5/DREAM3D-Build/NX-Com-Qt69-Vtk95-Rel/Bin/OrientationAnalysisUnitTest:*)", + "Bash(xargs sed:*)", + "Bash(grep:*)", + "Bash(git status:*)", + "Bash(git add:*)", + "Bash(git push:*)", + "Bash(tee:*)", + "Bash(git mv:*)", + "Bash(perl -pe:*)", + "Bash(find:*)", + "Bash(perl -pi -e:*)", + "Bash(perl -pi -e 's/k_TypeName = \"\"AbstractNodeGeometry0D\"\"/k_TypeName = \"\"INodeGeometry0D\"\"/g':*)", + "Bash(gh pr view:*)", + "Bash(/opt/local/bin/gh pr view:*)", + "WebFetch(domain:github.com)", + "Bash(git checkout:*)" + ], + "deny": [ + "Bash(rm -rf *)" + ] + } +} diff --git a/Source/Apps/SourceList.cmake b/Source/Apps/SourceList.cmake index 2020627..0ada0a3 100644 --- a/Source/Apps/SourceList.cmake +++ b/Source/Apps/SourceList.cmake @@ -32,6 +32,13 @@ target_include_directories(generate_ipf_legends PRIVATE "${EbsdLibProj_SOURCE_DIR}/3rdParty/canvas_ity/src") +add_executable(generate_ipf_density ${EbsdLibProj_SOURCE_DIR}/Source/Apps/generate_ipf_density.cpp) +target_link_libraries(generate_ipf_density PUBLIC EbsdLib) +target_include_directories(generate_ipf_density + PUBLIC + ${EbsdLibProj_SOURCE_DIR}/Source + ${EbsdLibProj_BINARY_DIR}) + add_executable(ParseAztecProject ${EbsdLibProj_SOURCE_DIR}/Source/Apps/ParseAztecProject.cpp) target_link_libraries(ParseAztecProject PUBLIC EbsdLib) target_include_directories(ParseAztecProject PUBLIC ${EbsdLibProj_SOURCE_DIR}/Source) diff --git a/Source/Apps/generate_ipf_density.cpp b/Source/Apps/generate_ipf_density.cpp new file mode 100644 index 0000000..c3d3df5 --- /dev/null +++ b/Source/Apps/generate_ipf_density.cpp @@ -0,0 +1,447 @@ +/* ============================================================================ + * Copyright (c) 2025-2026 BlueQuartz Software, LLC + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or + * other materials provided with the distribution. + * + * Neither the name of BlueQuartz Software, the US Air Force, nor the names of its + * contributors may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ + +/** + * @file generate_ipf_density.cpp + * @brief Example program that generates Inverse Pole Figure (IPF) density images + * for all 11 Laue classes using random orientations. The IPF density plot shows + * how a sample direction distributes across crystal directions within the + * Standard Stereographic Triangle (SST). + * + * For each Laue class, 3 TIFF images are generated corresponding to 3 orthogonal + * sample directions: RD (Rolling Direction), TD (Transverse Direction), and + * ND (Normal Direction). + * + * Additionally, a quaternion texture file is read and used to generate IPF density + * images (ND direction) for all 11 Laue classes, demonstrating a strong near-cube + * texture. + * + * Usage: + * generate_ipf_density [output_directory] [num_orientations] + * + * If no arguments are provided, output goes to the build's Testing/Temporary directory + * and 5000 random orientations are used. + */ + +#include "EbsdLib/Core/EbsdDataArray.hpp" +#include "EbsdLib/Core/EbsdLibConstants.h" +#include "EbsdLib/LaueOps/LaueOps.h" +#include "EbsdLib/OrientationMath/OrientationConverter.hpp" +#include "EbsdLib/Utilities/EbsdStringUtils.hpp" +#include "EbsdLib/Utilities/InversePoleFigureUtilities.h" +#include "EbsdLib/Utilities/TiffWriter.h" + +#include "EbsdLib/Apps/EbsdLibFileLocations.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace ebsdlib; + +namespace +{ + +// ----------------------------------------------------------------------- +// Generate random Euler angles with proper sampling of the orientation space. +// Uses a cosine distribution for Phi to ensure uniform coverage of SO(3). +// ----------------------------------------------------------------------- +ebsdlib::FloatArrayType::Pointer generateRandomEulers(size_t numOrientations, unsigned int seed) +{ + std::vector cDims = {3}; + auto eulers = ebsdlib::FloatArrayType::CreateArray(numOrientations, cDims, "EulerAngles", true); + + std::mt19937 gen(seed); + std::uniform_real_distribution phi1Dist(0.0f, static_cast(ebsdlib::constants::k_2PiD)); + std::uniform_real_distribution cosDist(-1.0f, 1.0f); + std::uniform_real_distribution phi2Dist(0.0f, static_cast(ebsdlib::constants::k_2PiD)); + + for(size_t i = 0; i < numOrientations; i++) + { + float* ptr = eulers->getTuplePointer(i); + ptr[0] = phi1Dist(gen); // phi1: [0, 2pi) + ptr[1] = std::acos(cosDist(gen)); // Phi: [0, pi] with uniform sphere coverage + ptr[2] = phi2Dist(gen); // phi2: [0, 2pi) + } + return eulers; +} + +// ----------------------------------------------------------------------- +// Generate Euler angles for a single-crystal texture: all orientations identical. +// ----------------------------------------------------------------------- +ebsdlib::FloatArrayType::Pointer generateSingleCrystalEulers(size_t numOrientations, float phi1, float Phi, float phi2) +{ + std::vector cDims = {3}; + auto eulers = ebsdlib::FloatArrayType::CreateArray(numOrientations, cDims, "EulerAngles", true); + + for(size_t i = 0; i < numOrientations; i++) + { + float* ptr = eulers->getTuplePointer(i); + ptr[0] = phi1; + ptr[1] = Phi; + ptr[2] = phi2; + } + return eulers; +} + +// ----------------------------------------------------------------------- +// Convert an ARGB UInt8ArrayType image to RGB by stripping the alpha channel, +// suitable for TiffWriter::WriteColorImage with samplesPerPixel=3. +// ----------------------------------------------------------------------- +ebsdlib::UInt8ArrayType::Pointer convertARGBtoRGB(ebsdlib::UInt8ArrayType* argbImage) +{ + size_t numPixels = argbImage->getNumberOfTuples(); + auto rgbImage = ebsdlib::UInt8ArrayType::CreateArray(numPixels, {3ULL}, argbImage->getName(), true); + + for(size_t i = 0; i < numPixels; i++) + { + uint8_t* argb = argbImage->getTuplePointer(i); + uint8_t* rgb = rgbImage->getTuplePointer(i); + + // The ARGB data is stored as a uint32_t: (A << 24) | (R << 16) | (G << 8) | B + // When accessed as bytes on a little-endian system: [B, G, R, A] + uint32_t pixel = *reinterpret_cast(argb); + rgb[0] = static_cast((pixel >> 16) & 0xFF); // R + rgb[1] = static_cast((pixel >> 8) & 0xFF); // G + rgb[2] = static_cast(pixel & 0xFF); // B + } + return rgbImage; +} + +// ----------------------------------------------------------------------- +// Write a single IPF density image to a TIFF file. +// ----------------------------------------------------------------------- +void writeIPFImage(ebsdlib::UInt8ArrayType* image, int width, int height, const std::string& filePath) +{ + auto rgbImage = convertARGBtoRGB(image); + auto result = TiffWriter::WriteColorImage(filePath, width, height, 3, rgbImage->data()); + if(result.first < 0) + { + std::cerr << " ERROR writing " << filePath << ": " << result.second << std::endl; + } + else + { + std::cout << " Wrote: " << filePath << std::endl; + } +} + +// ----------------------------------------------------------------------- +// Generate and save IPF density images for a single LaueOps instance. +// ----------------------------------------------------------------------- +void generateIPFForLaueClass(const LaueOps& ops, ebsdlib::FloatArrayType* eulers, const std::string& outputDir, int imageWidth, int imageHeight, int lambertDim, const std::string& textureLabel) +{ + std::string className = ops.getSymmetryName(); + std::cout << "Generating IPF density for: " << className << " (" << textureLabel << ")" << std::endl; + + InversePoleFigureConfiguration_t config; + config.eulers = eulers; + config.sampleDirections = {Matrix3X1D(1.0, 0.0, 0.0), Matrix3X1D(0.0, 1.0, 0.0), Matrix3X1D(0.0, 0.0, 1.0)}; + config.imageWidth = imageWidth; + config.imageHeight = imageHeight; + config.lambertDim = lambertDim; + config.numColors = 64; + config.colorMap = "Default"; + config.normalizeMRD = true; + config.labels = {"RD", "TD", "ND"}; + config.phaseName = className; + config.FlipFinalImage = false; + + auto images = ops.generateInversePoleFigure(config); + + // Sanitize symmetry name for use as a filename (replace / and spaces) + std::string safeName = className; + for(auto& c : safeName) + { + if(c == '/' || c == '\\' || c == ' ' || c == '(' || c == ')') + { + c = '_'; + } + } + + std::array dirLabels = {"RD", "TD", "ND"}; + for(size_t i = 0; i < 3; i++) + { + std::ostringstream filePath; + filePath << outputDir << "/" << safeName << "_IPF_" << dirLabels[i] << "_" << textureLabel << ".tiff"; + writeIPFImage(images[i].get(), imageWidth, imageHeight, filePath.str()); + } +} + +// ----------------------------------------------------------------------- +// Read quaternion data from a CSV file and convert to Euler angles using +// the OrientationConverter system. Expected CSV format: X,Y,Z,W,Distance +// (with header line). Quaternions are in vector-scalar order (x, y, z, w). +// Returns FloatArrayType with 3 components (phi1, Phi, phi2) in radians. +// ----------------------------------------------------------------------- +ebsdlib::FloatArrayType::Pointer readQuaternionFileAsEulers(const std::string& filePath) +{ + std::ifstream inFile(filePath); + if(!inFile.is_open()) + { + std::cerr << "ERROR: Could not open quaternion file: " << filePath << std::endl; + return nullptr; + } + + // Skip header line + std::string line; + std::getline(inFile, line); + + // Read all quaternion values (first 4 columns per line) + std::vector quatValues; + while(std::getline(inFile, line)) + { + if(line.empty()) + { + continue; + } + + auto tokens = EbsdStringUtils::split(line, ','); + if(tokens.size() >= 4) + { + quatValues.push_back(std::atof(tokens[0].c_str())); // X + quatValues.push_back(std::atof(tokens[1].c_str())); // Y + quatValues.push_back(std::atof(tokens[2].c_str())); // Z + quatValues.push_back(std::atof(tokens[3].c_str())); // W + } + } + inFile.close(); + + size_t numOrientations = quatValues.size() / 4; + std::cout << " Read " << numOrientations << " quaternions from file" << std::endl; + + // Wrap the quaternion data (4 components per tuple) and convert to Euler angles + using DoubleArrayType = EbsdDataArray; + + std::vector quatDims = {4}; + auto inputQuats = DoubleArrayType::WrapPointer(quatValues.data(), numOrientations, quatDims, "Quaternions", false); + + auto quatConverter = QuaternionConverter::New(); + quatConverter->setInputData(inputQuats); + quatConverter->convertRepresentationTo(ebsdlib::orientations::Type::Euler); + auto eulerData = quatConverter->getOutputData(); + + // Convert double Euler angles to float for the IPF pipeline + std::vector eulerDims = {3}; + auto eulers = ebsdlib::FloatArrayType::CreateArray(numOrientations, eulerDims, "EulerAngles", true); + for(size_t i = 0; i < numOrientations; i++) + { + double* src = eulerData->getTuplePointer(i); + float* dst = eulers->getTuplePointer(i); + dst[0] = static_cast(src[0]); + dst[1] = static_cast(src[1]); + dst[2] = static_cast(src[2]); + } + + return eulers; +} + +// ----------------------------------------------------------------------- +// Generate and save a single IPF density image for one sample direction. +// ----------------------------------------------------------------------- +void generateSingleIPFForLaueClass(const LaueOps& ops, ebsdlib::FloatArrayType* eulers, const Matrix3X1D& sampleDir, const std::string& dirLabel, const std::string& outputDir, int imageWidth, + int imageHeight, int lambertDim, bool normalizeMRD, const std::string& textureLabel) +{ + std::string className = ops.getSymmetryName(); + std::string modeLabel = normalizeMRD ? "MRD" : "Counts"; + std::cout << "Generating IPF " << modeLabel << " for: " << className << " (" << textureLabel << ", " << dirLabel << ")" << std::endl; + + auto directions = InversePoleFigureUtilities::computeIPFDirections(ops, eulers, sampleDir); + auto intensity = InversePoleFigureUtilities::computeIPFIntensity(ops, directions.get(), imageWidth, imageHeight, lambertDim, normalizeMRD); + + // Find min/max for color scaling (only pixels >= 0 are inside SST) + double minVal = std::numeric_limits::max(); + double maxVal = std::numeric_limits::lowest(); + double* dataPtr = intensity->getPointer(0); + for(size_t i = 0; i < intensity->getNumberOfTuples(); i++) + { + if(dataPtr[i] >= 0.0) + { + minVal = std::min(minVal, dataPtr[i]); + maxVal = std::max(maxVal, dataPtr[i]); + } + } + + std::vector cDims = {4}; + auto rgba = ebsdlib::UInt8ArrayType::CreateArray(static_cast(imageWidth * imageHeight), cDims, "RGBA", true); + InversePoleFigureUtilities::createIPFColorImage(intensity.get(), imageWidth, imageHeight, 64, minVal, maxVal, rgba.get()); + + // Sanitize symmetry name for use as a filename + std::string safeName = className; + for(auto& c : safeName) + { + if(c == '/' || c == '\\' || c == ' ' || c == '(' || c == ')') + { + c = '_'; + } + } + + std::ostringstream filePath; + filePath << outputDir << "/" << safeName << "_IPF_" << dirLabel << "_" << textureLabel << "_" << modeLabel << ".tiff"; + writeIPFImage(rgba.get(), imageWidth, imageHeight, filePath.str()); +} + +} // namespace + +// ============================================================================= +int main(int argc, char* argv[]) +{ + // Parse command-line arguments + std::string outputDir = UnitTest::TestTempDir + "/IPF_Density/"; + size_t numOrientations = 5000; + + if(argc >= 2) + { + outputDir = std::string(argv[1]); + if(outputDir.back() != '/') + { + outputDir += '/'; + } + } + if(argc >= 3) + { + numOrientations = static_cast(std::atoi(argv[2])); + if(numOrientations < 100) + { + numOrientations = 100; + } + } + + // Create output directory + std::filesystem::create_directories(outputDir); + + std::cout << "============================================================" << std::endl; + std::cout << " Inverse Pole Figure Density Image Generator" << std::endl; + std::cout << "============================================================" << std::endl; + std::cout << " Output directory: " << outputDir << std::endl; + std::cout << " Num orientations: " << numOrientations << std::endl; + std::cout << " Image size: 256 x 256 pixels" << std::endl; + std::cout << " Lambert dimension: 64" << std::endl; + std::cout << " Normalization: MRD" << std::endl; + std::cout << "============================================================" << std::endl; + std::cout << std::endl; + + int imageWidth = 1024; + int imageHeight = 1024; + int lambertDim = 64; + + // Get all LaueOps + std::vector ops = LaueOps::GetAllOrientationOps(); + + // --------------------------------------------------------------- + // Part 1: Random texture for all 11 Laue classes + // --------------------------------------------------------------- + std::cout << "--- Part 1: Random Texture (" << numOrientations << " random orientations) ---" << std::endl; + std::cout << std::endl; + + auto randomEulers = generateRandomEulers(numOrientations, 12345); + + for(size_t index = 0; index < 11; index++) + { + generateIPFForLaueClass(*ops[index], randomEulers.get(), outputDir, imageWidth, imageHeight, lambertDim, "Random"); + std::cout << std::endl; + } + + // --------------------------------------------------------------- + // Part 2: Single-crystal (Cube) texture for Cubic High symmetry + // This demonstrates a strong texture producing a concentrated spot. + // Euler angles (0, 0, 0) = Cube texture: [001] || ND, [100] || RD + // --------------------------------------------------------------- + std::cout << "--- Part 2: Single Crystal (Cube) Texture - Cubic High ---" << std::endl; + std::cout << std::endl; + + auto cubeEulers = generateSingleCrystalEulers(numOrientations, 0.0f, 0.0f, 0.0f); + generateIPFForLaueClass(*ops[1], cubeEulers.get(), outputDir, imageWidth, imageHeight, lambertDim, "Cube"); + std::cout << std::endl; + + // --------------------------------------------------------------- + // Part 3: Goss texture for Cubic High symmetry + // Euler angles (0, pi/4, 0) = Goss texture: {110}<001> + // --------------------------------------------------------------- + std::cout << "--- Part 3: Goss Texture - Cubic High ---" << std::endl; + std::cout << std::endl; + + auto gossEulers = generateSingleCrystalEulers(numOrientations, 0.0f, static_cast(ebsdlib::constants::k_PiOver4D), 0.0f); + generateIPFForLaueClass(*ops[1], gossEulers.get(), outputDir, imageWidth, imageHeight, lambertDim, "Goss"); + std::cout << std::endl; + + // --------------------------------------------------------------- + // Part 4: Brass texture for Cubic High symmetry + // Euler angles (35*pi/180, 45*pi/180, 0) = Brass-like texture: {110}<112> + // --------------------------------------------------------------- + std::cout << "--- Part 4: Brass Texture - Cubic High ---" << std::endl; + std::cout << std::endl; + + float brassE0 = 35.0f * static_cast(ebsdlib::constants::k_DegToRadD); + float brassE1 = 45.0f * static_cast(ebsdlib::constants::k_DegToRadD); + float brassE2 = 0.0f; + auto brassEulers = generateSingleCrystalEulers(numOrientations, brassE0, brassE1, brassE2); + generateIPFForLaueClass(*ops[1], brassEulers.get(), outputDir, imageWidth, imageHeight, lambertDim, "Brass"); + std::cout << std::endl; + + // --------------------------------------------------------------- + // Part 5: Texture from quaternion file for all 11 Laue classes + // Reads quaternion orientations near (0,0,0,1) representing a strong + // near-cube texture, converts to Euler angles via OrientationConverter, + // and generates IPF density images (ND direction) for all Laue classes. + // --------------------------------------------------------------- + std::cout << "--- Part 5: Quaternion Texture File - All Laue Classes (ND) ---" << std::endl; + std::cout << std::endl; + + std::string quatFilePath = UnitTest::DataDir + "IPF_Legend/quats_000_1_deg.txt"; + auto textureEulers = readQuaternionFileAsEulers(quatFilePath); + if(textureEulers != nullptr) + { + std::cout << std::endl; + + Matrix3X1D nd(0.0, 0.0, 1.0); + for(size_t index = 0; index < 11; index++) + { + generateSingleIPFForLaueClass(*ops[index], textureEulers.get(), nd, "ND", outputDir, imageWidth, imageHeight, lambertDim, true, "QuatTexture"); + std::cout << std::endl; + } + } + else + { + std::cerr << " Skipping Part 5: Could not load quaternion file." << std::endl; + std::cout << std::endl; + } + + std::cout << "============================================================" << std::endl; + std::cout << " Done! All IPF density images written to:" << std::endl; + std::cout << " " << outputDir << std::endl; + std::cout << "============================================================" << std::endl; + + return 0; +} diff --git a/Source/EbsdLib/LaueOps/LaueOps.cpp b/Source/EbsdLib/LaueOps/LaueOps.cpp index 5909bfc..f68c066 100644 --- a/Source/EbsdLib/LaueOps/LaueOps.cpp +++ b/Source/EbsdLib/LaueOps/LaueOps.cpp @@ -849,6 +849,87 @@ std::string LaueOps::ClassName() return {"LaueOps"}; } +//----------------------------------------------------------------------------- +std::vector LaueOps::generateInversePoleFigure(InversePoleFigureConfiguration_t& config) const +{ + std::vector ipfImages(3); + + // Determine labels + std::string label0 = "IPF-0"; + std::string label1 = "IPF-1"; + std::string label2 = "IPF-2"; + if(config.labels.size() >= 1) + { + label0 = config.labels[0]; + } + if(config.labels.size() >= 2) + { + label1 = config.labels[1]; + } + if(config.labels.size() >= 3) + { + label2 = config.labels[2]; + } + + // Step 1: Compute IPF directions for each sample direction + ebsdlib::FloatArrayType::Pointer dirs0 = InversePoleFigureUtilities::computeIPFDirections(*this, config.eulers, config.sampleDirections[0]); + ebsdlib::FloatArrayType::Pointer dirs1 = InversePoleFigureUtilities::computeIPFDirections(*this, config.eulers, config.sampleDirections[1]); + ebsdlib::FloatArrayType::Pointer dirs2 = InversePoleFigureUtilities::computeIPFDirections(*this, config.eulers, config.sampleDirections[2]); + + // Step 2: Compute intensity images for each + ebsdlib::DoubleArrayType::Pointer intensity0 = InversePoleFigureUtilities::computeIPFIntensity(*this, dirs0.get(), config.imageWidth, config.imageHeight, config.lambertDim, config.normalizeMRD); + ebsdlib::DoubleArrayType::Pointer intensity1 = InversePoleFigureUtilities::computeIPFIntensity(*this, dirs1.get(), config.imageWidth, config.imageHeight, config.lambertDim, config.normalizeMRD); + ebsdlib::DoubleArrayType::Pointer intensity2 = InversePoleFigureUtilities::computeIPFIntensity(*this, dirs2.get(), config.imageWidth, config.imageHeight, config.lambertDim, config.normalizeMRD); + + // Step 3: Find global min/max across all 3 intensity images (only for pixels inside SST, value >= 0) + double globalMax = std::numeric_limits::lowest(); + double globalMin = std::numeric_limits::max(); + + std::array intensities = {intensity0.get(), intensity1.get(), intensity2.get()}; + for(auto* intensityArr : intensities) + { + double* dPtr = intensityArr->getPointer(0); + size_t count = intensityArr->getNumberOfTuples(); + for(size_t i = 0; i < count; ++i) + { + if(dPtr[i] >= 0.0) // Only consider pixels inside the SST + { + if(dPtr[i] > globalMax) + { + globalMax = dPtr[i]; + } + if(dPtr[i] < globalMin) + { + globalMin = dPtr[i]; + } + } + } + } + + // Handle case where no valid pixels were found + if(globalMax < globalMin) + { + globalMin = 0.0; + globalMax = 1.0; + } + + // Step 4: Create RGBA color images + std::vector dims = {4}; + ebsdlib::UInt8ArrayType::Pointer image0 = ebsdlib::UInt8ArrayType::CreateArray(static_cast(config.imageWidth * config.imageHeight), dims, label0, true); + ebsdlib::UInt8ArrayType::Pointer image1 = ebsdlib::UInt8ArrayType::CreateArray(static_cast(config.imageWidth * config.imageHeight), dims, label1, true); + ebsdlib::UInt8ArrayType::Pointer image2 = ebsdlib::UInt8ArrayType::CreateArray(static_cast(config.imageWidth * config.imageHeight), dims, label2, true); + + InversePoleFigureUtilities::createIPFColorImage(intensity0.get(), config.imageWidth, config.imageHeight, config.numColors, globalMin, globalMax, image0.get()); + InversePoleFigureUtilities::createIPFColorImage(intensity1.get(), config.imageWidth, config.imageHeight, config.numColors, globalMin, globalMax, image1.get()); + InversePoleFigureUtilities::createIPFColorImage(intensity2.get(), config.imageWidth, config.imageHeight, config.numColors, globalMin, globalMax, image2.get()); + + ipfImages[0] = image0; + ipfImages[1] = image1; + ipfImages[2] = image2; + + return ipfImages; +} + //----------------------------------------------------------------------------- ebsdlib::Rgb LaueOps::generateMisorientationColor(const QuatD& q, const QuatD& refFrame) const { diff --git a/Source/EbsdLib/LaueOps/LaueOps.h b/Source/EbsdLib/LaueOps/LaueOps.h index 451d183..18e9e10 100644 --- a/Source/EbsdLib/LaueOps/LaueOps.h +++ b/Source/EbsdLib/LaueOps/LaueOps.h @@ -46,6 +46,7 @@ #include "EbsdLib/Orientation/OrientationFwd.hpp" #include "EbsdLib/Orientation/Quaternion.hpp" #include "EbsdLib/Orientation/Rodrigues.hpp" +#include "EbsdLib/Utilities/InversePoleFigureUtilities.h" #include "EbsdLib/Utilities/PoleFigureUtilities.h" namespace ebsdlib @@ -326,6 +327,16 @@ class EbsdLib_EXPORT LaueOps */ virtual UInt8ArrayType::Pointer generateIPFTriangleLegend(int imageDim, bool generateEntirePlane) const = 0; + /** + * @brief Generates 3 inverse pole figure density images for 3 orthogonal sample directions. + * The IPF density plot shows how a sample direction distributes across crystal directions + * within the Standard Stereographic Triangle (SST) using equal-area projection. + * This is a non-virtual base class method that works through existing virtual dispatch. + * @param config The configuration struct controlling the IPF generation + * @return A std::vector of 3 UInt8ArrayType pointers, each representing a 2D RGBA image + */ + std::vector generateInversePoleFigure(InversePoleFigureConfiguration_t& config) const; + enum class FZType : int32_t { Anorthic = 0, // Triclinic diff --git a/Source/EbsdLib/Utilities/InversePoleFigureUtilities.cpp b/Source/EbsdLib/Utilities/InversePoleFigureUtilities.cpp new file mode 100644 index 0000000..3641abf --- /dev/null +++ b/Source/EbsdLib/Utilities/InversePoleFigureUtilities.cpp @@ -0,0 +1,290 @@ +/* ============================================================================ + * Copyright (c) 2009-2025 BlueQuartz Software, LLC + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or + * other materials provided with the distribution. + * + * Neither the name of BlueQuartz Software, the US Air Force, nor the names of its + * contributors may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The code contained herein was partially funded by the following contracts: + * United States Air Force Prime Contract FA8650-07-D-5800 + * United States Air Force Prime Contract FA8650-10-D-5210 + * United States Prime Contract Navy N00173-07-C-2068 + * + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ + +#include "InversePoleFigureUtilities.h" + +#include +#include + +#include "EbsdLib/Core/EbsdLibConstants.h" +#include "EbsdLib/LaueOps/LaueOps.h" +#include "EbsdLib/Math/EbsdLibMath.h" +#include "EbsdLib/Math/Matrix3X3.hpp" +#include "EbsdLib/Orientation/Euler.hpp" +#include "EbsdLib/Orientation/OrientationFwd.hpp" +#include "EbsdLib/Orientation/Quaternion.hpp" +#include "EbsdLib/Utilities/ColorTable.h" +#include "EbsdLib/Utilities/ModifiedLambertProjection.h" + +using namespace ebsdlib; + +// ----------------------------------------------------------------------------- +InversePoleFigureUtilities::InversePoleFigureUtilities() = default; + +// ----------------------------------------------------------------------------- +InversePoleFigureUtilities::~InversePoleFigureUtilities() = default; + +// ----------------------------------------------------------------------------- +ebsdlib::FloatArrayType::Pointer InversePoleFigureUtilities::computeIPFDirections(const LaueOps& ops, ebsdlib::FloatArrayType* eulers, const Matrix3X1D& sampleDirection) +{ + size_t numOrientations = eulers->getNumberOfTuples(); + + // Allocate output array for crystal directions (3 components per orientation) + std::vector cDims(1, 3); + ebsdlib::FloatArrayType::Pointer directions = ebsdlib::FloatArrayType::CreateArray(numOrientations, cDims, "IPF_Directions", true); + directions->initializeWithZeros(); + + size_t numSymOps = ops.getNumSymOps(); + bool hasInversion = ops.getHasInversion(); + + const ebsdlib::Matrix3X1D refDirection(sampleDirection); + + size_t validCount = 0; + + for(size_t i = 0; i < numOrientations; i++) + { + float* euler = eulers->getTuplePointer(i); + EulerDType eu(static_cast(euler[0]), static_cast(euler[1]), static_cast(euler[2])); + + QuatD q1 = eu.toQuaternion(); + OrientationMatrixDType om; + + bool found = false; + ebsdlib::Matrix3X1D p; + + for(size_t j = 0; j < numSymOps; j++) + { + QuaternionDType qu(ops.getQuatSymOp(j) * q1); + om = qu.toOrientationMatrix(); + ebsdlib::Matrix3X3D g(om.data()); + p = (g * refDirection).normalize(); + + if(!hasInversion && p[2] < 0) + { + continue; + } + if(hasInversion && p[2] < 0) + { + p = p * -1.0; + } + + double chi = std::acos(p[2]); + double eta = std::atan2(p[1], p[0]); + + if(!ops.inUnitTriangle(eta, chi)) + { + continue; + } + + found = true; + break; + } + + if(found) + { + float* dirPtr = directions->getTuplePointer(validCount); + dirPtr[0] = static_cast(p[0]); + dirPtr[1] = static_cast(p[1]); + dirPtr[2] = static_cast(p[2]); + validCount++; + } + } + + // Create a trimmed array with only the valid directions + if(validCount < numOrientations) + { + ebsdlib::FloatArrayType::Pointer trimmed = ebsdlib::FloatArrayType::CreateArray(validCount, cDims, "IPF_Directions", true); + float* srcPtr = directions->getPointer(0); + float* dstPtr = trimmed->getPointer(0); + std::copy(srcPtr, srcPtr + validCount * 3, dstPtr); + return trimmed; + } + + return directions; +} + +// ----------------------------------------------------------------------------- +ebsdlib::DoubleArrayType::Pointer InversePoleFigureUtilities::computeIPFIntensity(const LaueOps& ops, ebsdlib::FloatArrayType* ipfDirections, int imageWidth, int imageHeight, int lambertDim, + bool normalizeMRD) +{ + // Step 1: Bin the crystal directions into the Lambert projection + float sphereRadius = 1.0f; + ModifiedLambertProjection::Pointer lambert = ModifiedLambertProjection::LambertBallToSquare(ipfDirections, lambertDim, sphereRadius); + + // Step 2: Normalize the north square only (all SST directions have z >= 0) + // We normalize manually to avoid division by zero in the south square + ebsdlib::DoubleArrayType::Pointer northSquare = lambert->getNorthSquare(); + double* north = northSquare->getPointer(0); + size_t nBins = static_cast(lambertDim) * static_cast(lambertDim); + + double northTotal = 0.0; + for(size_t i = 0; i < nBins; i++) + { + northTotal += north[i]; + } + + if(northTotal > 0.0) + { + if(normalizeMRD) + { + // MRD: (count / totalCount) * totalBins + double oneOverTotal = 1.0 / northTotal; + for(size_t i = 0; i < nBins; i++) + { + north[i] = north[i] * oneOverTotal * static_cast(nBins); + } + } + // If not MRD, leave as raw counts + } + + // Step 3: Create the output intensity image using equal-area projection + std::vector tDims = {static_cast(imageWidth * imageHeight)}; + std::vector cDims = {1}; + ebsdlib::DoubleArrayType::Pointer intensity = ebsdlib::DoubleArrayType::CreateArray(tDims, cDims, "IPF_Intensity", true); + double* intensityPtr = intensity->getPointer(0); + + // Lambert azimuthal equal-area projection centered on north pole + // Maps the upper hemisphere (z >= 0) to a disk of radius sqrt(2) + float unitRadius = std::sqrt(2.0f); + float span = 2.0f * unitRadius; + float xres = span / static_cast(imageWidth); + float yres = span / static_cast(imageHeight); + + int halfWidth = imageWidth / 2; + int halfHeight = imageHeight / 2; + + for(int y = 0; y < imageHeight; y++) + { + for(int x = 0; x < imageWidth; x++) + { + int index = y * imageWidth + x; + + // Map pixel to equal-area projection coordinates + float xtmp = static_cast(x - halfWidth) * xres + (xres * 0.5f); + float ytmp = static_cast(y - halfHeight) * yres + (yres * 0.5f); + + float rhoSq = xtmp * xtmp + ytmp * ytmp; + + // Check if within hemisphere disk + if(rhoSq > 2.0f) + { + intensityPtr[index] = -1.0; // Outside hemisphere + continue; + } + + // Inverse Lambert azimuthal equal-area projection (north pole centered) + float t = std::sqrt(1.0f - rhoSq / 4.0f); + std::array xyz = {xtmp * t, ytmp * t, 1.0f - rhoSq / 2.0f}; + + // Compute chi (polar angle from z-axis) and eta (azimuthal angle) + double chi = std::acos(static_cast(xyz[2])); + double eta = std::atan2(static_cast(xyz[1]), static_cast(xyz[0])); + + // Check if direction is inside the Standard Stereographic Triangle + if(!ops.inUnitTriangle(eta, chi)) + { + intensityPtr[index] = -1.0; // Outside SST + continue; + } + + // Look up the interpolated intensity from the Lambert projection + std::array sqCoord = {0.0f, 0.0f}; + bool isNorth = lambert->getSquareCoord(xyz.data(), sqCoord.data()); + if(isNorth) + { + intensityPtr[index] = lambert->getInterpolatedValue(ModifiedLambertProjection::NorthSquare, sqCoord.data()); + } + else + { + intensityPtr[index] = lambert->getInterpolatedValue(ModifiedLambertProjection::SouthSquare, sqCoord.data()); + } + } + } + + return intensity; +} + +// ----------------------------------------------------------------------------- +void InversePoleFigureUtilities::createIPFColorImage(ebsdlib::DoubleArrayType* intensity, int imageWidth, int imageHeight, int numColors, double minScale, double maxScale, ebsdlib::UInt8ArrayType* rgba) +{ + // Initialize the image with all zeros + rgba->initializeWithZeros(); + uint32_t* rgbaPtr = reinterpret_cast(rgba->getPointer(0)); + + // Get the color table + std::vector colors(numColors * 3, 0.0f); + EbsdColorTable::GetColorTable(numColors, colors); + + double* dataPtr = intensity->getPointer(0); + double range = maxScale - minScale; + if(range <= 0.0) + { + range = 1.0; + } + + for(int y = 0; y < imageHeight; y++) + { + for(int x = 0; x < imageWidth; x++) + { + size_t idx = static_cast(y * imageWidth + x); + double value = dataPtr[idx]; + + // Pixels outside SST have value -1.0 -> set to white + if(value < 0.0) + { + rgbaPtr[idx] = 0xFFFFFFFF; // White (ARGB) + continue; + } + + // Normalize to [0, 1] range + double normalized = (value - minScale) / range; + int bin = static_cast(normalized * numColors); + if(bin > numColors - 1) + { + bin = numColors - 1; + } + if(bin < 0) + { + bin = 0; + } + + float r = colors[3 * bin]; + float g = colors[3 * bin + 1]; + float b = colors[3 * bin + 2]; + + rgbaPtr[idx] = ebsdlib::RgbColor::dRgb(static_cast(r * 255.0f), static_cast(g * 255.0f), static_cast(b * 255.0f), 255); + } + } +} diff --git a/Source/EbsdLib/Utilities/InversePoleFigureUtilities.h b/Source/EbsdLib/Utilities/InversePoleFigureUtilities.h new file mode 100644 index 0000000..5d462ac --- /dev/null +++ b/Source/EbsdLib/Utilities/InversePoleFigureUtilities.h @@ -0,0 +1,131 @@ +/* ============================================================================ + * Copyright (c) 2009-2025 BlueQuartz Software, LLC + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or + * other materials provided with the distribution. + * + * Neither the name of BlueQuartz Software, the US Air Force, nor the names of its + * contributors may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The code contained herein was partially funded by the following contracts: + * United States Air Force Prime Contract FA8650-07-D-5800 + * United States Air Force Prime Contract FA8650-10-D-5210 + * United States Prime Contract Navy N00173-07-C-2068 + * + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ + +#pragma once + +#include "EbsdLib/Core/EbsdDataArray.hpp" +#include "EbsdLib/EbsdLib.h" +#include "EbsdLib/Math/Matrix3X1.hpp" + +#include +#include +#include + +namespace ebsdlib +{ + +class LaueOps; // Forward declaration + +/** + * @struct InversePoleFigureConfiguration_t + * @brief Configuration struct for generating Inverse Pole Figure density plots. + * The IPF density plot shows how a sample direction distributes across crystal + * directions within the Standard Stereographic Triangle (SST). + */ +struct InversePoleFigureConfiguration_t +{ + ebsdlib::FloatArrayType* eulers; ///<* The Euler Angles (in Radians) to use for the inverse pole figure + std::array sampleDirections; ///<* 3 orthogonal sample reference directions (e.g., RD, TD, ND) + int imageWidth; ///<* The width of the generated inverse pole figure image in pixels + int imageHeight; ///<* The height of the generated inverse pole figure image in pixels + int lambertDim; ///<* The dimensions in voxels of the Lambert Square used for binning/smoothing + int numColors; ///<* The number of colors to use in the color map + std::string colorMap; ///<* Name of the ColorMap to use + bool normalizeMRD; ///<* true=normalize to MRD (Multiples of Random Distribution), false=raw counts + std::vector labels; ///<* The labels for each of the 3 inverse pole figures (e.g., "RD", "TD", "ND") + std::string phaseName; ///<* The name of the phase + bool FlipFinalImage; ///<* If TRUE, the final image will be flipped across the X Axis so that +Y axis points UP +}; + +/** + * @class InversePoleFigureUtilities InversePoleFigureUtilities.h /Utilities/InversePoleFigureUtilities.h + * @brief This class provides static utility methods for generating Inverse Pole Figure (IPF) density plots. + * + * The IPF density plot shows the distribution of a sample direction across crystal directions + * within the Standard Stereographic Triangle (SST) using equal-area projection and Lambert-based + * smoothing. + */ +class EbsdLib_EXPORT InversePoleFigureUtilities +{ +public: + InversePoleFigureUtilities(); + virtual ~InversePoleFigureUtilities(); + + /** + * @brief Computes the crystal directions in the fundamental zone for all orientations + * given a single sample reference direction. For each orientation (Euler angle set), + * the sample direction is transformed into the crystal frame and the symmetry-equivalent + * direction within the Standard Stereographic Triangle is found. + * @param ops The LaueOps instance providing symmetry operations + * @param eulers The Euler angles array (3-component tuples, in radians) + * @param sampleDirection The sample reference direction (e.g., [0,0,1] for ND) + * @return FloatArrayType with 3-component tuples (XYZ crystal directions on unit sphere) + */ + static ebsdlib::FloatArrayType::Pointer computeIPFDirections(const LaueOps& ops, ebsdlib::FloatArrayType* eulers, const Matrix3X1D& sampleDirection); + + /** + * @brief Computes the intensity image for a single inverse pole figure using Lambert + * projection for binning and equal-area reprojection masked to the SST boundary. + * @param ops The LaueOps instance providing symmetry operations and SST boundary + * @param ipfDirections The crystal directions from computeIPFDirections + * @param imageWidth Output image width in pixels + * @param imageHeight Output image height in pixels + * @param lambertDim Lambert square dimension for binning/smoothing + * @param normalizeMRD true to normalize to MRD, false for raw counts + * @return DoubleArrayType intensity image (imageWidth * imageHeight). Pixels outside SST have value -1.0. + */ + static ebsdlib::DoubleArrayType::Pointer computeIPFIntensity(const LaueOps& ops, ebsdlib::FloatArrayType* ipfDirections, int imageWidth, int imageHeight, int lambertDim, bool normalizeMRD); + + /** + * @brief Converts an intensity image to RGBA with SST masking. Pixels inside the SST + * are mapped to colors via the color table; pixels outside are set to white. + * @param intensity The intensity image from computeIPFIntensity + * @param imageWidth Image width in pixels + * @param imageHeight Image height in pixels + * @param numColors Number of colors in the color table + * @param minScale Minimum intensity value for color mapping + * @param maxScale Maximum intensity value for color mapping + * @param rgba [output] RGBA image (4-component UInt8 array, imageWidth * imageHeight tuples) + */ + static void createIPFColorImage(ebsdlib::DoubleArrayType* intensity, int imageWidth, int imageHeight, int numColors, double minScale, double maxScale, ebsdlib::UInt8ArrayType* rgba); + +public: + InversePoleFigureUtilities(const InversePoleFigureUtilities&) = delete; // Copy Constructor Not Implemented + InversePoleFigureUtilities(InversePoleFigureUtilities&&) = delete; // Move Constructor Not Implemented + InversePoleFigureUtilities& operator=(const InversePoleFigureUtilities&) = delete; // Copy Assignment Not Implemented + InversePoleFigureUtilities& operator=(InversePoleFigureUtilities&&) = delete; // Move Assignment Not Implemented +}; + +} // namespace ebsdlib diff --git a/Source/EbsdLib/Utilities/SourceList.cmake b/Source/EbsdLib/Utilities/SourceList.cmake index 0e7fc72..cae2f6c 100644 --- a/Source/EbsdLib/Utilities/SourceList.cmake +++ b/Source/EbsdLib/Utilities/SourceList.cmake @@ -5,6 +5,7 @@ set(EbsdLib_${DIR_NAME}_MOC_HDRS ) set(EbsdLib_${DIR_NAME}_HDRS + ${EbsdLibProj_SOURCE_DIR}/Source/EbsdLib/${DIR_NAME}/InversePoleFigureUtilities.h ${EbsdLibProj_SOURCE_DIR}/Source/EbsdLib/${DIR_NAME}/PoleFigureUtilities.h ${EbsdLibProj_SOURCE_DIR}/Source/EbsdLib/${DIR_NAME}/ModifiedLambertProjection.h ${EbsdLibProj_SOURCE_DIR}/Source/EbsdLib/${DIR_NAME}/ModifiedLambertProjectionArray.h @@ -26,6 +27,7 @@ set(EbsdLib_${DIR_NAME}_HDRS ) set(EbsdLib_${DIR_NAME}_SRCS + ${EbsdLibProj_SOURCE_DIR}/Source/EbsdLib/${DIR_NAME}/InversePoleFigureUtilities.cpp ${EbsdLibProj_SOURCE_DIR}/Source/EbsdLib/${DIR_NAME}/PoleFigureUtilities.cpp ${EbsdLibProj_SOURCE_DIR}/Source/EbsdLib/${DIR_NAME}/ModifiedLambertProjection.cpp ${EbsdLibProj_SOURCE_DIR}/Source/EbsdLib/${DIR_NAME}/ModifiedLambertProjectionArray.cpp diff --git a/Source/Test/CMakeLists.txt b/Source/Test/CMakeLists.txt index c0754d1..2ea506e 100644 --- a/Source/Test/CMakeLists.txt +++ b/Source/Test/CMakeLists.txt @@ -49,8 +49,8 @@ set(EbsdLib_UnitTest_SRCS ${EbsdLibProj_SOURCE_DIR}/Source/Test/ModifiedLambertProjectionArrayTest.cpp ${EbsdLibProj_SOURCE_DIR}/Source/Test/PoleFigureUtilitiesTest.cpp ${EbsdLibProj_SOURCE_DIR}/Source/Test/PoleFigureCompositorTest.cpp + ${EbsdLibProj_SOURCE_DIR}/Source/Test/InversePoleFigureTest.cpp ${EbsdLibProj_SOURCE_DIR}/Source/Test/TiffWriterTest.cpp - ${EbsdLibProj_SOURCE_DIR}/Source/Test/DirectionalStatsTest.cpp ${EbsdLibProj_SOURCE_DIR}/Source/Test/UnitTestCommon.cpp ${EbsdLibProj_SOURCE_DIR}/Source/Test/UnitTestCommon.hpp diff --git a/Source/Test/InversePoleFigureTest.cpp b/Source/Test/InversePoleFigureTest.cpp new file mode 100644 index 0000000..bf6b256 --- /dev/null +++ b/Source/Test/InversePoleFigureTest.cpp @@ -0,0 +1,366 @@ +/* ============================================================================ + * Copyright (c) 2009-2025 BlueQuartz Software, LLC + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or + * other materials provided with the distribution. + * + * Neither the name of BlueQuartz Software, the US Air Force, nor the names of its + * contributors may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The code contained herein was partially funded by the following contracts: + * United States Air Force Prime Contract FA8650-07-D-5800 + * United States Air Force Prime Contract FA8650-10-D-5210 + * United States Prime Contract Navy N00173-07-C-2068 + * + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ + +#include + +#include "EbsdLib/Core/EbsdDataArray.hpp" +#include "EbsdLib/Core/EbsdLibConstants.h" +#include "EbsdLib/LaueOps/LaueOps.h" +#include "EbsdLib/Utilities/InversePoleFigureUtilities.h" + +#include +#include +#include + +using namespace ebsdlib; + +namespace +{ +// Helper to generate random Euler angles (in radians) +ebsdlib::FloatArrayType::Pointer generateRandomEulers(size_t numOrientations, unsigned int seed = 42) +{ + std::vector cDims = {3}; + auto eulers = ebsdlib::FloatArrayType::CreateArray(numOrientations, cDims, "EulerAngles", true); + + std::mt19937 gen(seed); + std::uniform_real_distribution phi1Dist(0.0f, static_cast(ebsdlib::constants::k_2PiD)); + std::uniform_real_distribution phiDist(0.0f, static_cast(ebsdlib::constants::k_PiD)); + std::uniform_real_distribution phi2Dist(0.0f, static_cast(ebsdlib::constants::k_2PiD)); + + for(size_t i = 0; i < numOrientations; i++) + { + float* ptr = eulers->getTuplePointer(i); + ptr[0] = phi1Dist(gen); + ptr[1] = phiDist(gen); + ptr[2] = phi2Dist(gen); + } + return eulers; +} + +// Helper to generate single-crystal Euler angles (all orientations identical) +ebsdlib::FloatArrayType::Pointer generateSingleCrystalEulers(size_t numOrientations, float e0, float e1, float e2) +{ + std::vector cDims = {3}; + auto eulers = ebsdlib::FloatArrayType::CreateArray(numOrientations, cDims, "EulerAngles", true); + + for(size_t i = 0; i < numOrientations; i++) + { + float* ptr = eulers->getTuplePointer(i); + ptr[0] = e0; + ptr[1] = e1; + ptr[2] = e2; + } + return eulers; +} + +// Standard orthogonal sample directions: RD=[1,0,0], TD=[0,1,0], ND=[0,0,1] +InversePoleFigureConfiguration_t createDefaultConfig(ebsdlib::FloatArrayType* eulers) +{ + InversePoleFigureConfiguration_t config; + config.eulers = eulers; + config.sampleDirections = {Matrix3X1D(1.0, 0.0, 0.0), Matrix3X1D(0.0, 1.0, 0.0), Matrix3X1D(0.0, 0.0, 1.0)}; + config.imageWidth = 64; + config.imageHeight = 64; + config.lambertDim = 32; + config.numColors = 32; + config.colorMap = "Default"; + config.normalizeMRD = true; + config.labels = {"RD", "TD", "ND"}; + config.phaseName = "TestPhase"; + config.FlipFinalImage = false; + return config; +} + +} // namespace + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::InversePoleFigureTest::Configuration_Fields", "[EbsdLib][InversePoleFigureTest]") +{ + InversePoleFigureConfiguration_t config; + config.eulers = nullptr; + config.sampleDirections = {Matrix3X1D(1.0, 0.0, 0.0), Matrix3X1D(0.0, 1.0, 0.0), Matrix3X1D(0.0, 0.0, 1.0)}; + config.imageWidth = 128; + config.imageHeight = 128; + config.lambertDim = 64; + config.numColors = 32; + config.colorMap = "Default"; + config.normalizeMRD = true; + config.labels = {"RD", "TD", "ND"}; + config.phaseName = "Phase1"; + config.FlipFinalImage = false; + + REQUIRE(config.imageWidth == 128); + REQUIRE(config.imageHeight == 128); + REQUIRE(config.lambertDim == 64); + REQUIRE(config.numColors == 32); + REQUIRE(config.normalizeMRD == true); + REQUIRE(config.labels.size() == 3); + REQUIRE(config.phaseName == "Phase1"); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::InversePoleFigureTest::ComputeIPFDirections_Cubic", "[EbsdLib][InversePoleFigureTest]") +{ + auto eulers = generateRandomEulers(100); + auto ops = LaueOps::GetAllOrientationOps(); + // CubicOps is at index 1 + auto& cubicOps = *ops[1]; + + Matrix3X1D nd(0.0, 0.0, 1.0); // Normal direction + auto directions = InversePoleFigureUtilities::computeIPFDirections(cubicOps, eulers.get(), nd); + + REQUIRE(directions != nullptr); + REQUIRE(directions->getNumberOfTuples() > 0); + REQUIRE(directions->getNumberOfComponents() == 3); + + // All returned directions should be on the unit sphere (magnitude ~1.0) + for(size_t i = 0; i < directions->getNumberOfTuples(); i++) + { + float* dir = directions->getTuplePointer(i); + float mag = std::sqrt(dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2]); + REQUIRE(mag == Approx(1.0f).margin(0.01f)); + } + + // All returned directions should have z >= 0 (upper hemisphere) + for(size_t i = 0; i < directions->getNumberOfTuples(); i++) + { + float* dir = directions->getTuplePointer(i); + REQUIRE(dir[2] >= -0.01f); // Allow small numerical tolerance + } +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::InversePoleFigureTest::ComputeIPFIntensity_Cubic", "[EbsdLib][InversePoleFigureTest]") +{ + auto eulers = generateRandomEulers(500); + auto ops = LaueOps::GetAllOrientationOps(); + auto& cubicOps = *ops[1]; + + Matrix3X1D nd(0.0, 0.0, 1.0); + auto directions = InversePoleFigureUtilities::computeIPFDirections(cubicOps, eulers.get(), nd); + + int imageWidth = 64; + int imageHeight = 64; + int lambertDim = 32; + + auto intensity = InversePoleFigureUtilities::computeIPFIntensity(cubicOps, directions.get(), imageWidth, imageHeight, lambertDim, true); + + REQUIRE(intensity != nullptr); + REQUIRE(intensity->getNumberOfTuples() == static_cast(imageWidth * imageHeight)); + + // Check that we have some pixels inside the SST (value >= 0) and some outside (value == -1) + bool hasInsideSST = false; + bool hasOutsideSST = false; + double* dataPtr = intensity->getPointer(0); + for(size_t i = 0; i < intensity->getNumberOfTuples(); i++) + { + if(dataPtr[i] >= 0.0) + { + hasInsideSST = true; + } + if(dataPtr[i] < 0.0) + { + hasOutsideSST = true; + } + if(hasInsideSST && hasOutsideSST) + { + break; + } + } + REQUIRE(hasInsideSST); + REQUIRE(hasOutsideSST); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::InversePoleFigureTest::CreateIPFColorImage", "[EbsdLib][InversePoleFigureTest]") +{ + int imageWidth = 16; + int imageHeight = 16; + int numColors = 16; + + // Create a test intensity image with some SST values and some -1 (outside) + auto intensity = DoubleArrayType::CreateArray(static_cast(imageWidth * imageHeight), {1ULL}, "Intensity", true); + double* dataPtr = intensity->getPointer(0); + for(int i = 0; i < imageWidth * imageHeight; i++) + { + if(i % 3 == 0) + { + dataPtr[i] = -1.0; // Outside SST + } + else + { + dataPtr[i] = static_cast(i) / static_cast(imageWidth * imageHeight); + } + } + + std::vector cDims = {4}; + auto rgba = UInt8ArrayType::CreateArray(static_cast(imageWidth * imageHeight), cDims, "RGBA", true); + rgba->initializeWithZeros(); + + InversePoleFigureUtilities::createIPFColorImage(intensity.get(), imageWidth, imageHeight, numColors, 0.0, 1.0, rgba.get()); + + // Verify image was populated + bool hasNonZero = false; + bool hasWhite = false; + for(size_t i = 0; i < rgba->getNumberOfTuples(); i++) + { + uint8_t* pixel = rgba->getTuplePointer(i); + uint32_t rgbaVal = *reinterpret_cast(pixel); + if(rgbaVal == 0xFFFFFFFF) + { + hasWhite = true; + } + else if(pixel[0] != 0 || pixel[1] != 0 || pixel[2] != 0 || pixel[3] != 0) + { + hasNonZero = true; + } + } + REQUIRE(hasNonZero); + REQUIRE(hasWhite); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::InversePoleFigureTest::GenerateInversePoleFigure_AllLaueClasses", "[EbsdLib][InversePoleFigureTest]") +{ + auto eulers = generateRandomEulers(200); + auto ops = LaueOps::GetAllOrientationOps(); + + for(size_t index = 0; index < 11; index++) + { + SECTION(ops[index]->getSymmetryName()) + { + auto config = createDefaultConfig(eulers.get()); + auto images = ops[index]->generateInversePoleFigure(config); + + // Should return exactly 3 images + REQUIRE(images.size() == 3); + + for(size_t imgIdx = 0; imgIdx < 3; imgIdx++) + { + REQUIRE(images[imgIdx] != nullptr); + REQUIRE(images[imgIdx]->getNumberOfTuples() == static_cast(config.imageWidth * config.imageHeight)); + REQUIRE(images[imgIdx]->getNumberOfComponents() == 4); + + // Verify the image has some non-white content (SST region should be colored) + bool hasColoredPixel = false; + for(size_t i = 0; i < images[imgIdx]->getNumberOfTuples(); i++) + { + uint32_t rgbaVal = *reinterpret_cast(images[imgIdx]->getTuplePointer(i)); + if(rgbaVal != 0xFFFFFFFF && rgbaVal != 0x00000000) + { + hasColoredPixel = true; + break; + } + } + REQUIRE(hasColoredPixel); + } + } + } +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::InversePoleFigureTest::GenerateInversePoleFigure_MRD_vs_Counts", "[EbsdLib][InversePoleFigureTest]") +{ + auto eulers = generateRandomEulers(200); + auto ops = LaueOps::GetAllOrientationOps(); + auto& cubicOps = *ops[1]; // Cubic high + + // Test MRD mode + auto configMRD = createDefaultConfig(eulers.get()); + configMRD.normalizeMRD = true; + auto imagesMRD = cubicOps.generateInversePoleFigure(configMRD); + REQUIRE(imagesMRD.size() == 3); + + // Test counts mode + auto configCounts = createDefaultConfig(eulers.get()); + configCounts.normalizeMRD = false; + auto imagesCounts = cubicOps.generateInversePoleFigure(configCounts); + REQUIRE(imagesCounts.size() == 3); + + // Both should produce valid images + for(size_t imgIdx = 0; imgIdx < 3; imgIdx++) + { + REQUIRE(imagesMRD[imgIdx] != nullptr); + REQUIRE(imagesCounts[imgIdx] != nullptr); + } +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::InversePoleFigureTest::SingleCrystalTexture_Cubic", "[EbsdLib][InversePoleFigureTest]") +{ + // All orientations are identity (0, 0, 0 Euler angles) + // For ND=[0,0,1], the crystal direction in the SST should be [001] + auto eulers = generateSingleCrystalEulers(500, 0.0f, 0.0f, 0.0f); + auto ops = LaueOps::GetAllOrientationOps(); + auto& cubicOps = *ops[1]; + + Matrix3X1D nd(0.0, 0.0, 1.0); + auto directions = InversePoleFigureUtilities::computeIPFDirections(cubicOps, eulers.get(), nd); + + REQUIRE(directions != nullptr); + REQUIRE(directions->getNumberOfTuples() == 500); + + // All directions should be very close to [001] = (0, 0, 1) + for(size_t i = 0; i < directions->getNumberOfTuples(); i++) + { + float* dir = directions->getTuplePointer(i); + REQUIRE(std::fabs(dir[0]) < 0.01f); + REQUIRE(std::fabs(dir[1]) < 0.01f); + REQUIRE(dir[2] == Approx(1.0f).margin(0.01f)); + } +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::InversePoleFigureTest::ImageDimensions", "[EbsdLib][InversePoleFigureTest]") +{ + auto eulers = generateRandomEulers(100); + auto ops = LaueOps::GetAllOrientationOps(); + auto& cubicOps = *ops[1]; + + int testWidth = 128; + int testHeight = 96; + + auto config = createDefaultConfig(eulers.get()); + config.imageWidth = testWidth; + config.imageHeight = testHeight; + + auto images = cubicOps.generateInversePoleFigure(config); + + REQUIRE(images.size() == 3); + for(auto& img : images) + { + REQUIRE(img->getNumberOfTuples() == static_cast(testWidth * testHeight)); + } +} From 455d8fb9343ae947f6b0d3f469c3bf6d31fde717 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Mon, 23 Mar 2026 22:07:47 -0400 Subject: [PATCH 02/14] DOC: Add implementation plan for annotated IPF density images Plan B approach: refactor shared annotation scaffolding from 11 copies of generateIPFTriangleLegend() into a shared base-class method, then use it for both IPF legends and a new annotated IPF density feature. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../plans/2026-03-23-annotated-ipf-density.md | 1065 +++++++++++++++++ 1 file changed, 1065 insertions(+) create mode 100644 Docs/superpowers/plans/2026-03-23-annotated-ipf-density.md diff --git a/Docs/superpowers/plans/2026-03-23-annotated-ipf-density.md b/Docs/superpowers/plans/2026-03-23-annotated-ipf-density.md new file mode 100644 index 0000000..bbadb3a --- /dev/null +++ b/Docs/superpowers/plans/2026-03-23-annotated-ipf-density.md @@ -0,0 +1,1065 @@ +# Annotated Inverse Pole Figure Density Images — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add proper labeling (title, Miller index annotations, color bar) to Inverse Pole Figure density images by refactoring the existing `generateIPFTriangleLegend` scaffolding into shared code that both the IPF legend and IPF density features can use. + +**Architecture:** Extract the ~40 lines of identical canvas setup / teardown code from all 11 `generateIPFTriangleLegend()` implementations into a shared non-virtual base-class method. Promote each subclass's `DrawFullCircleAnnotations()` free function to a virtual method so the base class can call it. Both `generateIPFTriangleLegend()` and a new `generateAnnotatedIPFDensity()` method call the same shared annotation pipeline, differing only in how the triangle image is produced and whether a color bar is added. + +**Tech Stack:** C++20, canvas_ity (2D rendering), EbsdLib LaueOps class hierarchy, EbsdDataArray + +--- + +## File Map + +### Files to Modify + +| File | Change | +|------|--------| +| `Source/EbsdLib/LaueOps/LaueOps.h` | Add `drawIPFAnnotations()` pure virtual declaration. Add `annotateIPFImage()` protected non-virtual helper. Add `adjustFigureOrigin()` virtual method. Add `generateAnnotatedIPFDensity()` public method declaration. | +| `Source/EbsdLib/LaueOps/LaueOps.cpp` | Implement `annotateIPFImage()` (shared scaffolding). Implement `generateAnnotatedIPFDensity()` (density pipeline + annotation + color bar). | +| `Source/EbsdLib/LaueOps/CubicOps.h` | Declare `drawIPFAnnotations()` and `adjustFigureOrigin()` overrides. | +| `Source/EbsdLib/LaueOps/CubicOps.cpp` | Move `DrawFullCircleAnnotations` body into `drawIPFAnnotations()` override. Refactor `generateIPFTriangleLegend()` to call `annotateIPFImage()`. | +| `Source/EbsdLib/LaueOps/CubicLowOps.h` | Same as CubicOps.h | +| `Source/EbsdLib/LaueOps/CubicLowOps.cpp` | Same pattern as CubicOps.cpp | +| `Source/EbsdLib/LaueOps/HexagonalOps.h` | Same as CubicOps.h | +| `Source/EbsdLib/LaueOps/HexagonalOps.cpp` | Same pattern as CubicOps.cpp | +| `Source/EbsdLib/LaueOps/HexagonalLowOps.h` | Same as CubicOps.h | +| `Source/EbsdLib/LaueOps/HexagonalLowOps.cpp` | Same pattern as CubicOps.cpp | +| `Source/EbsdLib/LaueOps/TrigonalOps.h` | Same as CubicOps.h | +| `Source/EbsdLib/LaueOps/TrigonalOps.cpp` | Same pattern as CubicOps.cpp | +| `Source/EbsdLib/LaueOps/TrigonalLowOps.h` | Same as CubicOps.h | +| `Source/EbsdLib/LaueOps/TrigonalLowOps.cpp` | Same pattern as CubicOps.cpp | +| `Source/EbsdLib/LaueOps/TetragonalOps.h` | Same as CubicOps.h | +| `Source/EbsdLib/LaueOps/TetragonalOps.cpp` | Same pattern as CubicOps.cpp | +| `Source/EbsdLib/LaueOps/TetragonalLowOps.h` | Same as CubicOps.h | +| `Source/EbsdLib/LaueOps/TetragonalLowOps.cpp` | Same pattern as CubicOps.cpp | +| `Source/EbsdLib/LaueOps/OrthoRhombicOps.h` | Same as CubicOps.h | +| `Source/EbsdLib/LaueOps/OrthoRhombicOps.cpp` | Same pattern as CubicOps.cpp | +| `Source/EbsdLib/LaueOps/MonoclinicOps.h` | Same as CubicOps.h | +| `Source/EbsdLib/LaueOps/MonoclinicOps.cpp` | Same pattern as CubicOps.cpp | +| `Source/EbsdLib/LaueOps/TriclinicOps.h` | Same as CubicOps.h | +| `Source/EbsdLib/LaueOps/TriclinicOps.cpp` | Same pattern as CubicOps.cpp | +| `Source/Apps/generate_ipf_from_file.cpp` | Update to call `generateAnnotatedIPFDensity()` instead of raw `generateInversePoleFigure()`. | +| `Source/Apps/generate_ipf_density.cpp` | Update to call `generateAnnotatedIPFDensity()` instead of raw `generateInversePoleFigure()`. | + +### Files to Read (reference only, no changes) + +| File | Why | +|------|-----| +| `Source/EbsdLib/Utilities/CanvasUtilities.hpp` | Contains `WriteText`, `DrawLine`, `MirrorImage`, `ConvertColorOrder`, `RemoveAlphaChannel`, `CropRGBImage` | +| `Source/EbsdLib/Utilities/Fonts.hpp` | Contains `GetLatoBold()`, `GetLatoRegular()` | +| `Source/EbsdLib/Utilities/InversePoleFigureUtilities.h` | Contains `InversePoleFigureConfiguration_t`, `computeIPFDirections`, `computeIPFIntensity`, `createIPFColorImage` | +| `Source/EbsdLib/Utilities/ColorTable.h` | Color table for color bar rendering | +| `Source/EbsdLib/Utilities/TiffWriter.h` | Writing TIFF output from apps | + +--- + +## Background: Current Architecture + +### generateIPFTriangleLegend() — Current flow (duplicated 11 times) + +Each of the 11 LaueOps subclasses has an identical ~90-line `generateIPFTriangleLegend()` that: + +1. Computes margins, legend dimensions, figureOrigin (2-3 lines **vary per subclass**) +2. Calls `CreateIPFLegend(this, legendHeight, generateEntirePlane)` — file-scoped free function (**varies per subclass** — different SST geometry) +3. Calls `ConvertColorOrder()` + `MirrorImage()` — **identical** +4. Creates canvas, fills white background, sets up fonts — **identical** (~20 lines) +5. Draws legend image onto canvas — **identical** +6. Draws title — **identical** +7. Calls `DrawFullCircleAnnotations()` — file-scoped free function (**varies per subclass** — different Miller indices and positions) +8. Extracts RGBA, removes alpha — **identical** + +Only steps 1, 2, and 7 vary. Steps 3-6 and 8 are copy-pasted across all 11 files. + +### generateInversePoleFigure() — Current flow (single implementation in base class) + +A non-virtual method in `LaueOps.cpp` that: +1. Computes IPF directions for 3 sample directions +2. Computes intensity via Lambert projection +3. Finds global min/max across all 3 images +4. Creates RGBA color images (colored SST, white outside) + +Returns raw ARGB images — **no annotations, no title, no labels, no color bar**. + +--- + +## Design: Refactored Architecture + +### New virtual methods on LaueOps + +```cpp +// In LaueOps.h: + +/** + * @brief Per-subclass hook that draws Miller index labels and SST boundary + * annotations onto a canvas_ity canvas. Replaces the file-scoped + * DrawFullCircleAnnotations() free functions. + */ +virtual void drawIPFAnnotations(canvas_ity::canvas& context, int canvasDim, + float fontPtSize, const std::vector& margins, + std::array figureOrigin, + std::array figureCenter, + bool drawFullCircle) const = 0; + +/** + * @brief Per-subclass hook that returns the figureOrigin adjustment + * when rendering the SST-only view (generateEntirePlane == false). + * Default returns the base figureOrigin unchanged. + */ +virtual std::array adjustFigureOrigin( + std::array figureOrigin, + int legendWidth, int legendHeight, + const std::vector& margins, float fontPtSize, + bool generateEntirePlane) const; +``` + +### New shared scaffolding method (non-virtual, protected) + +```cpp +/** + * @brief Shared canvas scaffolding used by both generateIPFTriangleLegend() + * and generateAnnotatedIPFDensity(). Takes a pre-rendered triangle image + * (ARGB, square), annotates it with title + per-subclass Miller index labels, + * and returns the final RGB image. + */ +UInt8ArrayType::Pointer annotateIPFImage( + UInt8ArrayType::Pointer triangleImage, + int imageDim, + int canvasDim, + const std::string& title, + bool generateEntirePlane) const; +``` + +### New public method for annotated density + +```cpp +/** + * @brief Generates 3 annotated inverse pole figure density images with + * title, Miller index labels, and MRD color bar. + */ +std::vector generateAnnotatedIPFDensity( + InversePoleFigureConfiguration_t& config) const; +``` + +### Data flow after refactor + +**IPF Legend:** +``` +CreateIPFLegend() [per-subclass, existing] + → annotateIPFImage() [shared scaffolding, NEW] + → drawIPFAnnotations() [per-subclass virtual, promoted from free function] + → return annotated image +``` + +**IPF Density:** +``` +generateInversePoleFigure() [existing, produces raw ARGB images] + → annotateIPFImage() [shared scaffolding, same as legend] + → drawIPFAnnotations() [per-subclass virtual, same as legend] + → drawColorBar() [shared, NEW, density-specific] + → return annotated images +``` + +--- + +## Tasks + +### Task 1: Add new virtual methods to LaueOps.h + +**Files:** +- Modify: `Source/EbsdLib/LaueOps/LaueOps.h:328` (near existing `generateIPFTriangleLegend` declaration) + +- [ ] **Step 1: Add the `#include` for canvas_ity in LaueOps.h** + +Add near the top of LaueOps.h with other includes: +```cpp +#include +``` + +Note: canvas_ity.hpp is already a public dependency of EbsdLib (included in CanvasUtilities.hpp, installed to include/EbsdLib). Check that it's not already included; if not, add it. + +- [ ] **Step 2: Add the three new method declarations** + +After the existing `generateIPFTriangleLegend` declaration (line 328), add: + +```cpp + /** + * @brief Per-subclass hook that draws Miller index labels and SST boundary + * annotations onto a canvas. Called by annotateIPFImage(). + */ + virtual void drawIPFAnnotations(canvas_ity::canvas& context, int canvasDim, + float fontPtSize, const std::vector& margins, + std::array figureOrigin, + std::array figureCenter, + bool drawFullCircle) const = 0; + + /** + * @brief Per-subclass hook that adjusts the figureOrigin when rendering + * SST-only view. Each subclass overrides to position its triangle shape + * correctly within the canvas. Default returns figureOrigin unchanged. + */ + virtual std::array adjustFigureOrigin( + std::array figureOrigin, + int legendWidth, int legendHeight, + const std::vector& margins, float fontPtSize, + bool generateEntirePlane) const; + + /** + * @brief Generates 3 annotated inverse pole figure density images with + * title, Miller index labels, and MRD color bar. + * @param config Configuration struct; imageWidth must equal imageHeight (square images required) + * @param outMinMax Optional output for the global [min, max] intensity values + */ + std::vector generateAnnotatedIPFDensity( + InversePoleFigureConfiguration_t& config, + std::pair* outMinMax = nullptr) const; + +protected: + /** + * @brief Shared annotation scaffolding. Takes a pre-rendered ARGB triangle + * image, creates a canvas with white background, draws the image, adds + * title and per-subclass annotations, returns final RGB image. + * @param triangleImage Pre-rendered ARGB image (square, imageDim x imageDim) + * @param imageDim Pixel dimension of the triangle image (square) + * @param canvasDim Pixel dimension of the output canvas (square) + * @param title Text to draw as the title + * @param generateEntirePlane true = full circle view, false = SST only + * @return RGB image (canvasDim x canvasDim, 3 components) + */ + UInt8ArrayType::Pointer annotateIPFImage( + UInt8ArrayType::Pointer triangleImage, + int imageDim, + int canvasDim, + const std::string& title, + bool generateEntirePlane) const; +``` + +Note: The `protected:` label is needed so subclasses can call `annotateIPFImage()`. Check the existing access specifiers in LaueOps.h and place appropriately. The existing class may not have a `protected:` section — if so, add one before the new method. The `public:` methods (`drawIPFAnnotations`, `adjustFigureOrigin`, `generateAnnotatedIPFDensity`) go in the existing `public:` section. + +- [ ] **Step 3: Build to verify the header compiles** + +Run: +```bash +cd /Users/mjackson/Workspace1/DREAM3D-Build/EbsdLib-Release && cmake --build . --target EbsdLib 2>&1 | tail -5 +``` +Expected: Linker errors about undefined references to the new methods (that's fine — implementations come in later tasks). If there are compiler errors, fix them first. + +- [ ] **Step 4: Commit** + +```bash +git add Source/EbsdLib/LaueOps/LaueOps.h +git commit -m "ENH: Add virtual method declarations for shared IPF annotation pipeline" +``` + +--- + +### Task 2: Implement `annotateIPFImage()` and `adjustFigureOrigin()` in LaueOps.cpp + +**Files:** +- Modify: `Source/EbsdLib/LaueOps/LaueOps.cpp` (after existing `generateInversePoleFigure`) + +- [ ] **Step 1: Add includes to LaueOps.cpp** + +Add these includes at the top of LaueOps.cpp if not already present: +```cpp +#include "EbsdLib/Utilities/CanvasUtilities.hpp" +#include "EbsdLib/Utilities/Fonts.hpp" +#include +``` + +- [ ] **Step 2: Implement the default `adjustFigureOrigin()`** + +Add after `generateInversePoleFigure()`: +```cpp +std::array LaueOps::adjustFigureOrigin( + std::array figureOrigin, + int legendWidth, int legendHeight, + const std::vector& margins, float fontPtSize, + bool generateEntirePlane) const +{ + return figureOrigin; +} +``` + +This default implementation returns the origin unchanged. Subclasses with SST positioning needs will override it. + +- [ ] **Step 3: Implement `annotateIPFImage()`** + +This is the shared scaffolding extracted from the 11 copies of `generateIPFTriangleLegend()`. Add after `adjustFigureOrigin()`: + +```cpp +UInt8ArrayType::Pointer LaueOps::annotateIPFImage( + UInt8ArrayType::Pointer triangleImage, + int imageDim, + int canvasDim, + const std::string& title, + bool generateEntirePlane) const +{ + // Compute layout + const float fontPtSize = static_cast(canvasDim) / 24.0f; + const std::vector margins = { + fontPtSize * 3, // Top + static_cast(canvasDim / 7.0f), // Right + fontPtSize * 2, // Bottom + static_cast(canvasDim / 7.0f) // Left + }; + + int legendHeight = canvasDim - static_cast(margins[0]) - static_cast(margins[2]); + int legendWidth = canvasDim - static_cast(margins[1]) - static_cast(margins[3]); + + if(legendHeight > legendWidth) + { + legendHeight = legendWidth; + } + else + { + legendWidth = legendHeight; + } + + int halfWidth = legendWidth / 2; + int halfHeight = legendHeight / 2; + + // Compute figure origin — subclass may override for SST positioning + std::array figureOrigin = {margins[3], margins[0] * 1.33F}; + figureOrigin = adjustFigureOrigin(figureOrigin, legendWidth, legendHeight, margins, fontPtSize, generateEntirePlane); + + std::array figureCenter = {figureOrigin[0] + halfWidth, figureOrigin[1] + halfHeight}; + + // Scale the triangle image to legend dimensions if needed + // The input image is imageDim x imageDim; we need legendHeight x legendHeight + // For now we assume the caller provides an image at the correct size. + // Convert from ARGB to RGBA for canvas_ity + ebsdlib::UInt8ArrayType::Pointer image = ebsdlib::ConvertColorOrder(triangleImage.get(), imageDim); + // Mirror across X axis (image was drawn with +Y pointing down) + image = ebsdlib::MirrorImage(image.get(), imageDim); + + // Create canvas + canvas_ity::canvas context(canvasDim, canvasDim); + + std::vector latoBold = ebsdlib::fonts::GetLatoBold(); + std::vector latoRegular = ebsdlib::fonts::GetLatoRegular(); + context.set_font(latoBold.data(), static_cast(latoBold.size()), fontPtSize); + context.set_color(canvas_ity::fill_style, 0.0f, 0.0f, 0.0f, 1.0f); + context.text_baseline = canvas_ity::alphabetic; + + // Fill background with white + context.move_to(0.0f, 0.0f); + context.line_to(static_cast(canvasDim), 0.0f); + context.line_to(static_cast(canvasDim), static_cast(canvasDim)); + context.line_to(0.0f, static_cast(canvasDim)); + context.line_to(0.0f, 0.0f); + context.close_path(); + context.set_color(canvas_ity::fill_style, 1.0f, 1.0f, 1.0f, 1.0f); + context.fill(); + + // Draw the triangle image onto the canvas + context.draw_image(image->getPointer(0), imageDim, imageDim, + imageDim * image->getNumberOfComponents(), + figureOrigin[0], figureOrigin[1], + static_cast(legendWidth), + static_cast(legendHeight)); + + // Draw title + context.set_font(latoBold.data(), static_cast(latoBold.size()), fontPtSize * 1.5); + ebsdlib::WriteText(context, title, {margins[0], static_cast(fontPtSize * 1.5)}, fontPtSize * 1.5); + + // Draw per-subclass annotations (Miller indices, SST boundary lines) + context.set_font(latoRegular.data(), static_cast(latoRegular.size()), fontPtSize); + drawIPFAnnotations(context, canvasDim, fontPtSize, margins, figureOrigin, figureCenter, generateEntirePlane); + + // Extract rendered pixels and remove alpha channel + ebsdlib::UInt8ArrayType::Pointer rgbaCanvasImage = ebsdlib::UInt8ArrayType::CreateArray( + canvasDim * canvasDim, {4ULL}, "Annotated IPF", true); + context.get_image_data(rgbaCanvasImage->getPointer(0), canvasDim, canvasDim, canvasDim * 4, 0, 0); + + return ebsdlib::RemoveAlphaChannel(rgbaCanvasImage.get()); +} +``` + +- [ ] **Step 4: Build to check for compilation errors** + +Run: +```bash +cd /Users/mjackson/Workspace1/DREAM3D-Build/EbsdLib-Release && cmake --build . --target EbsdLib 2>&1 | tail -20 +``` +Expected: Linker errors for the pure virtual `drawIPFAnnotations` in the subclasses (they don't implement it yet). That's expected. + +- [ ] **Step 5: Commit** + +```bash +git add Source/EbsdLib/LaueOps/LaueOps.cpp +git commit -m "ENH: Implement shared annotateIPFImage() scaffolding in LaueOps base class" +``` + +--- + +### Task 3: Refactor CubicOps — promote DrawFullCircleAnnotations to virtual override + +This task establishes the pattern for all 11 subclasses. Do CubicOps first, verify it works, then apply the same pattern to the remaining 10. + +**Files:** +- Modify: `Source/EbsdLib/LaueOps/CubicOps.h` +- Modify: `Source/EbsdLib/LaueOps/CubicOps.cpp` + +- [ ] **Step 1: Add virtual method declarations to CubicOps.h** + +Add near the existing `generateIPFTriangleLegend` declaration: +```cpp + void drawIPFAnnotations(canvas_ity::canvas& context, int canvasDim, + float fontPtSize, std::vector margins, + std::array figureOrigin, + std::array figureCenter, + bool drawFullCircle) const override; + + std::array adjustFigureOrigin( + std::array figureOrigin, + int legendWidth, int legendHeight, + const std::vector& margins, float fontPtSize, + bool generateEntirePlane) const override; +``` + +Also add `#include ` if not already present. Check the existing includes — CubicOps.cpp includes it but CubicOps.h may not. + +- [ ] **Step 2: Implement `adjustFigureOrigin()` override in CubicOps.cpp** + +CubicOps adjusts only `figureOrigin[1]` when `generateEntirePlane == false`: + +```cpp +std::array CubicOps::adjustFigureOrigin( + std::array figureOrigin, + int legendWidth, int legendHeight, + const std::vector& margins, float fontPtSize, + bool generateEntirePlane) const +{ + if(!generateEntirePlane) + { + figureOrigin[1] = 0.0F + fontPtSize * 2.0F; + } + return figureOrigin; +} +``` + +- [ ] **Step 3: Convert DrawFullCircleAnnotations to `drawIPFAnnotations()` override** + +Rename the existing file-scoped `DrawFullCircleAnnotations()` function in CubicOps.cpp to the virtual override `CubicOps::drawIPFAnnotations()`. The function body stays identical — only the function signature changes: + +Before: +```cpp +void DrawFullCircleAnnotations(canvas_ity::canvas& context, int canvasDim, float fontPtSize, std::vector margins, std::array figureOrigin, std::array figureCenter, + bool drawFullCircle) +``` + +After: +```cpp +void CubicOps::drawIPFAnnotations(canvas_ity::canvas& context, int canvasDim, float fontPtSize, std::vector margins, std::array figureOrigin, std::array figureCenter, + bool drawFullCircle) const +``` + +**Important:** CubicOps has special handling — when `drawFullCircle == false`, it adjusts `figureCenter` before drawing labels (see lines 2153 in current code). This logic is already inside `DrawFullCircleAnnotations` itself in CubicOps. Verify by reading the function body that the figureCenter adjustment is handled internally. If the adjustment is done OUTSIDE the function (in `generateIPFTriangleLegend` before calling it), then move that logic INTO the new `drawIPFAnnotations` override: + +```cpp +// If CubicOps did this in generateIPFTriangleLegend: +// figureCenter = {figureOrigin[0], figureOrigin[1] + legendHeight}; +// Then add it at the top of drawIPFAnnotations: +if(!drawFullCircle) +{ + figureCenter = {figureOrigin[0], figureOrigin[1] + static_cast(/* legendHeight */)}; +} +``` + +Note: The `legendHeight` value isn't directly available in `drawIPFAnnotations`. However, looking at the existing code, `figureCenter` is computed from `figureOrigin + halfWidth/halfHeight`, which means the caller (annotateIPFImage) already computes it. For CubicOps, when `!drawFullCircle`, it overrides figureCenter to `{figureOrigin[0], figureOrigin[1] + legendHeight}`. We can compute this from the available parameters: `legendHeight = canvasDim - margins[0] - margins[2]` (clamped to square). Add this computation at the top of the override if needed. + +- [ ] **Step 4: Refactor `generateIPFTriangleLegend()` to use `annotateIPFImage()`** + +Replace the body of `CubicOps::generateIPFTriangleLegend()` with: + +```cpp +ebsdlib::UInt8ArrayType::Pointer CubicOps::generateIPFTriangleLegend(int canvasDim, bool generateEntirePlane) const +{ + // Compute legend dimensions (same formula as annotateIPFImage uses) + const float fontPtSize = static_cast(canvasDim) / 24.0f; + int legendHeight = canvasDim - static_cast(fontPtSize * 3) - static_cast(fontPtSize * 2); + int legendWidth = canvasDim - static_cast(canvasDim / 7.0f) * 2; + if(legendHeight > legendWidth) + { + legendHeight = legendWidth; + } + else + { + legendWidth = legendHeight; + } + + // Generate the colored SST triangle image (ARGB) + ebsdlib::UInt8ArrayType::Pointer image = CreateIPFLegend(this, legendHeight, generateEntirePlane); + + // Annotate with title and Miller index labels + return annotateIPFImage(image, legendHeight, canvasDim, getSymmetryName(), generateEntirePlane); +} +``` + +- [ ] **Step 5: Build and run the generate_ipf_legends app to verify output matches** + +```bash +cd /Users/mjackson/Workspace1/DREAM3D-Build/EbsdLib-Release && cmake --build . --target generate_ipf_legends 2>&1 | tail -5 +``` + +This will fail to link because the other 10 subclasses don't implement `drawIPFAnnotations` yet. That's expected. To verify CubicOps in isolation, we need to complete all 11 subclasses first (Task 4). + +- [ ] **Step 6: Commit** + +```bash +git add Source/EbsdLib/LaueOps/CubicOps.h Source/EbsdLib/LaueOps/CubicOps.cpp +git commit -m "ENH: Refactor CubicOps to use shared annotation pipeline" +``` + +--- + +### Task 4: Refactor remaining 10 LaueOps subclasses + +Apply the same pattern from Task 3 to each remaining subclass. Each subclass needs: + +1. Add `drawIPFAnnotations()` and `adjustFigureOrigin()` override declarations to the header +2. Convert the file-scoped `DrawFullCircleAnnotations()` to the `drawIPFAnnotations()` virtual override (same body, new signature with `const` qualifier and class prefix) +3. Implement `adjustFigureOrigin()` with the subclass-specific figureOrigin adjustment +4. Refactor `generateIPFTriangleLegend()` to call `annotateIPFImage()` + +**Per-subclass figureOrigin adjustments** (from the existing code): + +| Subclass | adjustFigureOrigin when !generateEntirePlane | +|----------|---------------------------------------------| +| CubicOps | `figureOrigin[1] = fontPtSize * 2.0F` | +| CubicLowOps | `figureOrigin[1] = fontPtSize * 2.0F` | +| HexagonalOps | `figureOrigin[0] = -margins[3] * 0.5F; figureOrigin[1] = -halfHeight + margins[0] + fontPtSize` | +| HexagonalLowOps | `figureOrigin[0] = -halfWidth * 0.25F; figureOrigin[1] = margins[0]` | +| TrigonalOps | `figureOrigin[0] = -halfWidth * 0.25; figureOrigin[1] = -halfHeight * 0.5` | +| TrigonalLowOps | `figureOrigin[0] = -legendWidth * 0.0F; figureOrigin[1] = -legendHeight * 0.25F` | +| TetragonalOps | `figureOrigin[0] = -margins[2]; figureOrigin[1] = fontPtSize * 2.0F` | +| TetragonalLowOps | `figureOrigin[0] = -margins[3]` (Y unchanged) | +| OrthoRhombicOps | `figureOrigin[0] = -margins[3]` (Y unchanged) | +| MonoclinicOps | No adjustment (use default) | +| TriclinicOps | No adjustment (use default) | + +Note: MonoclinicOps and TriclinicOps have commented-out adjustments in the existing code. They use the default figureOrigin, so they do not need to override `adjustFigureOrigin()`. + +**Files (for each subclass):** +- Modify: `Source/EbsdLib/LaueOps/.h` +- Modify: `Source/EbsdLib/LaueOps/.cpp` + +- [ ] **Step 1: Refactor CubicLowOps** + +Follow Task 3 pattern. CubicLowOps has the same figureOrigin adjustment as CubicOps AND the same special figureCenter handling (if/else on generateEntirePlane before calling DrawFullCircleAnnotations). Make sure to handle the figureCenter adjustment inside `drawIPFAnnotations()`. + +- [ ] **Step 2: Refactor HexagonalOps** + +Follow Task 3 pattern. HexagonalOps has unique figureOrigin adjustment (both X and Y). No special figureCenter handling. + +- [ ] **Step 3: Refactor HexagonalLowOps** + +Follow Task 3 pattern. + +- [ ] **Step 4: Refactor TrigonalOps** + +Follow Task 3 pattern. + +- [ ] **Step 5: Refactor TrigonalLowOps** + +Follow Task 3 pattern. + +- [ ] **Step 6: Refactor TetragonalOps** + +Follow Task 3 pattern. + +- [ ] **Step 7: Refactor TetragonalLowOps** + +Follow Task 3 pattern. + +- [ ] **Step 8: Refactor OrthoRhombicOps** + +Follow Task 3 pattern. OrthoRhombicOps adjusts `figureOrigin[0] = -margins[3]`. + +- [ ] **Step 9: Refactor MonoclinicOps** + +Follow Task 3 pattern. No `adjustFigureOrigin` override needed (uses default). Still need `drawIPFAnnotations` override. + +- [ ] **Step 10: Refactor TriclinicOps** + +Follow Task 3 pattern. No `adjustFigureOrigin` override needed (uses default). Still need `drawIPFAnnotations` override. + +- [ ] **Step 11: Build the full library** + +```bash +cd /Users/mjackson/Workspace1/DREAM3D-Build/EbsdLib-Release && cmake --build . --target EbsdLib 2>&1 | tail -10 +``` +Expected: Clean build with no errors. + +- [ ] **Step 12: Build and run generate_ipf_legends to verify legend output is unchanged** + +```bash +cd /Users/mjackson/Workspace1/DREAM3D-Build/EbsdLib-Release && cmake --build . --target generate_ipf_legends && ./Bin/generate_ipf_legends 2>&1 +``` + +Visually compare the output images in `Testing/Temporary/IPF_Legend/` against the reference images at: +``` +/Users/mjackson/Workspace1/DREAM3D-Build/NX-Com-Qt69-Vtk95-Dbg/simplnx/EbsdLib/Testing/Temporary/IPF_Legend/ +``` + +Each Laue class directory should contain a `.tiff` and `_FULL.tiff` that match the reference visually. Pay special attention to: +- Label positions (Miller indices at correct corners) +- Triangle orientation and cropping +- Title text + +- [ ] **Step 13: Commit** + +```bash +git add Source/EbsdLib/LaueOps/*.h Source/EbsdLib/LaueOps/*.cpp +git commit -m "ENH: Refactor all 11 LaueOps subclasses to use shared annotation pipeline" +``` + +--- + +### Task 5: Implement `generateAnnotatedIPFDensity()` with color bar + +**Files:** +- Modify: `Source/EbsdLib/LaueOps/LaueOps.cpp` + +- [ ] **Step 1: Implement `generateAnnotatedIPFDensity()`** + +This method inlines the key parts of `generateInversePoleFigure()` to avoid double-computing the expensive intensity step. It computes directions + intensity, extracts global min/max for the color bar, creates the color images, then annotates. + +**Important:** `config.imageWidth` must equal `config.imageHeight` (square images required) because `ConvertColorOrder` and `MirrorImage` assume square dimensions. + +The `canvasDim` is computed from `imageDim` so that `legendWidth == imageDim` (no lossy scaling): +``` +legendWidth = canvasDim - 2 * (canvasDim / 7) +``` +Solving for `canvasDim` when `legendWidth == imageDim`: `canvasDim = imageDim * 7 / 5` + +Add after `annotateIPFImage()` in LaueOps.cpp: + +```cpp +std::vector LaueOps::generateAnnotatedIPFDensity( + InversePoleFigureConfiguration_t& config, + std::pair* outMinMax) const +{ + // Require square images (ConvertColorOrder and MirrorImage assume square) + if(config.imageWidth != config.imageHeight) + { + std::cerr << "generateAnnotatedIPFDensity: imageWidth must equal imageHeight" << std::endl; + return {}; + } + int imageDim = config.imageWidth; + + // Step 1: Compute IPF directions and intensity for all 3 sample directions + std::array dirs; + std::array intensities; + for(size_t i = 0; i < 3; i++) + { + dirs[i] = InversePoleFigureUtilities::computeIPFDirections(*this, config.eulers, config.sampleDirections[i]); + intensities[i] = InversePoleFigureUtilities::computeIPFIntensity(*this, dirs[i].get(), config.imageWidth, config.imageHeight, config.lambertDim, config.normalizeMRD); + } + + // Step 2: Find global min/max across all 3 intensity images + double globalMin = std::numeric_limits::max(); + double globalMax = std::numeric_limits::lowest(); + for(auto& intensity : intensities) + { + double* dPtr = intensity->getPointer(0); + size_t count = intensity->getNumberOfTuples(); + for(size_t i = 0; i < count; ++i) + { + if(dPtr[i] >= 0.0) + { + globalMin = std::min(globalMin, dPtr[i]); + globalMax = std::max(globalMax, dPtr[i]); + } + } + } + if(globalMax < globalMin) + { + globalMin = 0.0; + globalMax = 1.0; + } + if(outMinMax != nullptr) + { + *outMinMax = {globalMin, globalMax}; + } + + // Step 3: Create ARGB color images and annotate each one + // Compute canvasDim so legendWidth == imageDim (no scaling): + // legendWidth = canvasDim - 2 * floor(canvasDim / 7) + // We want legendWidth == imageDim, so canvasDim ~= imageDim * 7 / 5 + int canvasDim = static_cast(std::ceil(static_cast(imageDim) * 7.0 / 5.0)); + + std::vector annotatedImages(3); + std::array defaultLabels = {"IPF-0", "IPF-1", "IPF-2"}; + + for(size_t i = 0; i < 3; i++) + { + std::string label = (i < config.labels.size()) ? config.labels[i] : defaultLabels[i]; + std::string title = config.phaseName + " - " + label; + + // Create ARGB color image + std::vector dims = {4}; + ebsdlib::UInt8ArrayType::Pointer rawImage = ebsdlib::UInt8ArrayType::CreateArray( + static_cast(imageDim * imageDim), dims, label, true); + InversePoleFigureUtilities::createIPFColorImage( + intensities[i].get(), imageDim, imageDim, config.numColors, globalMin, globalMax, rawImage.get()); + + // Annotate with title and Miller index labels (SST-only view for density) + ebsdlib::UInt8ArrayType::Pointer annotated = annotateIPFImage( + rawImage, imageDim, canvasDim, title, false); + + // Add color bar + annotated = drawColorBar(annotated, canvasDim, config.numColors, globalMin, globalMax, config.normalizeMRD); + + annotatedImages[i] = annotated; + } + + return annotatedImages; +} +``` + +- [ ] **Step 2: Implement `drawColorBar()` helper** + +Add as a private method of LaueOps (declare in LaueOps.h in the private/protected section): + +```cpp +// In LaueOps.h, protected section: + UInt8ArrayType::Pointer drawColorBar( + UInt8ArrayType::Pointer image, + int canvasDim, + int numColors, + double minValue, double maxValue, + bool isMRD) const; +``` + +Implementation in LaueOps.cpp: + +```cpp +UInt8ArrayType::Pointer LaueOps::drawColorBar( + UInt8ArrayType::Pointer image, + int canvasDim, + int numColors, + double minValue, double maxValue, + bool isMRD) const +{ + const float fontPtSize = static_cast(canvasDim) / 24.0f; + + // Create canvas and draw the existing image onto it + canvas_ity::canvas context(canvasDim, canvasDim); + + std::vector latoBold = ebsdlib::fonts::GetLatoBold(); + std::vector latoRegular = ebsdlib::fonts::GetLatoRegular(); + + // Draw the input image (RGB, 3 components) onto the canvas + // canvas_ity expects RGBA, so we need to add alpha channel back + size_t numPixels = image->getNumberOfTuples(); + ebsdlib::UInt8ArrayType::Pointer rgbaImage = ebsdlib::UInt8ArrayType::CreateArray(numPixels, {4ULL}, "RGBA", true); + for(size_t i = 0; i < numPixels; i++) + { + uint8_t* src = image->getTuplePointer(i); + uint8_t* dst = rgbaImage->getTuplePointer(i); + dst[0] = src[0]; + dst[1] = src[1]; + dst[2] = src[2]; + dst[3] = 255; + } + + context.draw_image(rgbaImage->getPointer(0), canvasDim, canvasDim, + canvasDim * 4, 0.0f, 0.0f, + static_cast(canvasDim), static_cast(canvasDim)); + + // Color bar layout + float barX = static_cast(canvasDim) - fontPtSize * 3.0f; + float barY = fontPtSize * 4.0f; + float barWidth = fontPtSize * 1.0f; + float barHeight = static_cast(canvasDim) - fontPtSize * 8.0f; + + // Get color table + std::vector colors; + EbsdColorTable::GetColorTable(numColors, colors); + + // Draw color bar segments (bottom = min, top = max) + float segmentHeight = barHeight / static_cast(numColors); + for(int c = 0; c < numColors; c++) + { + float y = barY + barHeight - (c + 1) * segmentHeight; + int ci = c * 3; + context.set_color(canvas_ity::fill_style, colors[ci], colors[ci + 1], colors[ci + 2], 1.0f); + context.move_to(barX, y); + context.line_to(barX + barWidth, y); + context.line_to(barX + barWidth, y + segmentHeight); + context.line_to(barX, y + segmentHeight); + context.close_path(); + context.fill(); + } + + // Draw color bar outline + context.set_color(canvas_ity::stroke_style, 0.0f, 0.0f, 0.0f, 1.0f); + context.set_line_width(1.0f); + context.move_to(barX, barY); + context.line_to(barX + barWidth, barY); + context.line_to(barX + barWidth, barY + barHeight); + context.line_to(barX, barY + barHeight); + context.close_path(); + context.stroke(); + + // Draw min/max labels + context.set_font(latoRegular.data(), static_cast(latoRegular.size()), fontPtSize * 0.8f); + context.set_color(canvas_ity::fill_style, 0.0f, 0.0f, 0.0f, 1.0f); + + std::ostringstream maxStr; + maxStr << std::fixed << std::setprecision(1) << maxValue; + context.fill_text(maxStr.str().c_str(), barX - fontPtSize * 0.5f, barY - fontPtSize * 0.3f); + + std::ostringstream minStr; + minStr << std::fixed << std::setprecision(1) << minValue; + context.fill_text(minStr.str().c_str(), barX - fontPtSize * 0.5f, barY + barHeight + fontPtSize); + + // Draw "MRD" or "Counts" label + std::string unitLabel = isMRD ? "MRD" : "Counts"; + context.set_font(latoBold.data(), static_cast(latoBold.size()), fontPtSize * 0.7f); + context.fill_text(unitLabel.c_str(), barX - fontPtSize * 0.2f, barY + barHeight + fontPtSize * 2.0f); + + // Extract and return + ebsdlib::UInt8ArrayType::Pointer result = ebsdlib::UInt8ArrayType::CreateArray(canvasDim * canvasDim, {4ULL}, "Annotated IPF Density", true); + context.get_image_data(result->getPointer(0), canvasDim, canvasDim, canvasDim * 4, 0, 0); + + return ebsdlib::RemoveAlphaChannel(result.get()); +} +``` + +Note: `EbsdColorTable::GetColorTable` returns float values in [0, 1] range. The `colors` vector has `numColors * 3` elements (RGB triplets). Verify this by reading `Source/EbsdLib/Utilities/ColorTable.h`. + +- [ ] **Step 3: Add required include** + +Add to LaueOps.cpp: +```cpp +#include +#include +``` + +- [ ] **Step 4: Build** + +```bash +cd /Users/mjackson/Workspace1/DREAM3D-Build/EbsdLib-Release && cmake --build . --target EbsdLib 2>&1 | tail -10 +``` +Expected: Clean build. + +- [ ] **Step 5: Commit** + +```bash +git add Source/EbsdLib/LaueOps/LaueOps.h Source/EbsdLib/LaueOps/LaueOps.cpp +git commit -m "ENH: Implement generateAnnotatedIPFDensity() with color bar rendering" +``` + +--- + +### Task 6: Update generate_ipf_from_file.cpp to use annotated output + +**Files:** +- Modify: `Source/Apps/generate_ipf_from_file.cpp` + +- [ ] **Step 1: Update `generateIPFForPhase()` to use `generateAnnotatedIPFDensity()`** + +In `generate_ipf_from_file.cpp`, replace the `generateIPFForPhase()` function. The key change is calling `ops.generateAnnotatedIPFDensity(config)` instead of `ops.generateInversePoleFigure(config)`, and the returned images are now RGB (3 components) instead of ARGB (4 components), so skip the ARGB→RGB conversion: + +```cpp +void generateIPFForPhase(const LaueOps& ops, ebsdlib::FloatArrayType* eulers, + const std::string& outputDir, int imageWidth, int imageHeight, + int lambertDim, const std::string& phaseLabel) +{ + std::string className = ops.getSymmetryName(); + std::cout << "Generating annotated IPF density for phase: " << phaseLabel + << " (" << className << ", " << eulers->getNumberOfTuples() + << " orientations)" << std::endl; + + InversePoleFigureConfiguration_t config; + config.eulers = eulers; + config.sampleDirections = {Matrix3X1D(1.0, 0.0, 0.0), Matrix3X1D(0.0, 1.0, 0.0), Matrix3X1D(0.0, 0.0, 1.0)}; + config.imageWidth = imageWidth; + config.imageHeight = imageHeight; + config.lambertDim = lambertDim; + config.numColors = 64; + config.colorMap = "Default"; + config.normalizeMRD = true; + config.labels = {"RD", "TD", "ND"}; + config.phaseName = phaseLabel; + config.FlipFinalImage = false; + + auto images = ops.generateAnnotatedIPFDensity(config); + + // Sanitize phase name for use as a filename + std::string safeName = phaseLabel; + for(auto& c : safeName) + { + if(c == '/' || c == '\\' || c == ' ' || c == '(' || c == ')') + { + c = '_'; + } + } + + // Images are already RGB (3 components) — write directly + // canvasDim matches the formula in generateAnnotatedIPFDensity: imageDim * 7 / 5 + std::array dirLabels = {"RD", "TD", "ND"}; + int canvasDim = static_cast(std::ceil(static_cast(imageWidth) * 7.0 / 5.0)); + for(size_t i = 0; i < 3; i++) + { + std::ostringstream filePath; + filePath << outputDir << "/" << safeName << "_IPF_" << dirLabels[i] << ".tiff"; + auto result = TiffWriter::WriteColorImage(filePath.str(), canvasDim, canvasDim, 3, images[i]->data()); + if(result.first < 0) + { + std::cerr << " ERROR writing " << filePath.str() << ": " << result.second << std::endl; + } + else + { + std::cout << " Wrote: " << filePath.str() << std::endl; + } + } +} +``` + +Also remove the `convertARGBtoRGB()` and `writeIPFImage()` helper functions since they're no longer needed. + +- [ ] **Step 2: Build and test** + +```bash +cd /Users/mjackson/Workspace1/DREAM3D-Build/EbsdLib-Release && cmake --build . --target generate_ipf_from_file && ./Bin/generate_ipf_from_file "/Users/mjackson/Applications/NXData/Data/T12-MAI-2010/fw-ar-IF1-aptr12-corr.ctf" /tmp/ipf_annotated_test 2>&1 +``` + +Visually inspect the output images at `/tmp/ipf_annotated_test/` — they should now have: +- Title at top (phase name + direction label) +- Miller index labels at SST corners +- Color bar on the right with MRD min/max values + +- [ ] **Step 3: Commit** + +```bash +git add Source/Apps/generate_ipf_from_file.cpp +git commit -m "ENH: Update generate_ipf_from_file to use annotated IPF density output" +``` + +--- + +### Task 7: Update generate_ipf_density.cpp to use annotated output + +**Files:** +- Modify: `Source/Apps/generate_ipf_density.cpp` + +- [ ] **Step 1: Update the app to use `generateAnnotatedIPFDensity()`** + +Update the `generateIPFForLaueClass()` function in the same way as Task 6 — call `ops.generateAnnotatedIPFDensity(config)` and write the returned RGB images directly. Also update `generateSingleIPFForLaueClass()` similarly if desired, or leave it using the raw pipeline for comparison. + +- [ ] **Step 2: Build and test** + +```bash +cd /Users/mjackson/Workspace1/DREAM3D-Build/EbsdLib-Release && cmake --build . --target generate_ipf_density && ./Bin/generate_ipf_density /tmp/ipf_density_annotated 500 2>&1 +``` + +Visually inspect the output. All 11 Laue classes should produce properly annotated images. + +- [ ] **Step 3: Commit** + +```bash +git add Source/Apps/generate_ipf_density.cpp +git commit -m "ENH: Update generate_ipf_density to use annotated IPF density output" +``` + +--- + +### Task 8: Run all unit tests and verify no regressions + +**Files:** +- Read: `Source/Test/InversePoleFigureTest.cpp` (to understand what's tested) + +- [ ] **Step 1: Build all targets** + +```bash +cd /Users/mjackson/Workspace1/DREAM3D-Build/EbsdLib-Release && cmake --build . --target all 2>&1 | tail -10 +``` +Expected: Clean build. + +- [ ] **Step 2: Run all EbsdLib tests** + +```bash +cd /Users/mjackson/Workspace1/DREAM3D-Build/EbsdLib-Release && ctest -R "EbsdLib::" --verbose 2>&1 +``` +Expected: All tests pass. + +- [ ] **Step 3: Run generate_ipf_legends and visually verify** + +```bash +cd /Users/mjackson/Workspace1/DREAM3D-Build/EbsdLib-Release && ./Bin/generate_ipf_legends 2>&1 +``` + +Compare output images with reference images to confirm the refactor didn't change the legend output. + +- [ ] **Step 4: Commit any test fixes if needed** + +--- + +## Important Notes + +### No CMake changes required + +All new code is added to existing source files (`LaueOps.h`, `LaueOps.cpp`, and the 11 subclass `.h`/`.cpp` files). No new source files are created. The `canvas_ity.hpp` include added to `LaueOps.h` is already a linked dependency of the EbsdLib target (via `PRIVATE` include in `SourceList.cmake`). No CMake modifications are needed. + +### The figureCenter special case in CubicOps and CubicLowOps + +These two subclasses adjust `figureCenter` in `generateIPFTriangleLegend()` before calling `DrawFullCircleAnnotations()` when `generateEntirePlane == false`: +```cpp +figureCenter = {figureOrigin[0], figureOrigin[1] + legendHeight}; +``` +This adjustment must be moved INTO the `drawIPFAnnotations()` override for these two classes, since `annotateIPFImage()` always computes `figureCenter = {figureOrigin[0] + halfWidth, figureOrigin[1] + halfHeight}`. + +Recompute `legendHeight` inside `drawIPFAnnotations`: +```cpp +void CubicOps::drawIPFAnnotations(canvas_ity::canvas& context, int canvasDim, + float fontPtSize, const std::vector& margins, + std::array figureOrigin, + std::array figureCenter, + bool drawFullCircle) const +{ + if(!drawFullCircle) + { + // Recompute legendHeight from canvasDim and margins (same formula as annotateIPFImage) + int legendHeight = canvasDim - static_cast(margins[0]) - static_cast(margins[2]); + int legendWidth = canvasDim - static_cast(margins[1]) - static_cast(margins[3]); + if(legendHeight > legendWidth) { legendHeight = legendWidth; } + figureCenter = {figureOrigin[0], figureOrigin[1] + static_cast(legendHeight)}; + } + // ... rest of existing DrawFullCircleAnnotations body ... +} +``` + +### Square image requirement + +`ConvertColorOrder()` and `MirrorImage()` in `CanvasUtilities.hpp` both take a single `imageDim` parameter and iterate `imageDim × imageDim` pixels. This means all input images must be square. `generateAnnotatedIPFDensity()` enforces `config.imageWidth == config.imageHeight` with an early return and error message. + +### Canvas size calculation + +To avoid lossy scaling of the density image, `canvasDim` is computed so that `legendWidth` (the space available for the image after margins) equals `imageDim` exactly: +``` +legendWidth = canvasDim - 2 * floor(canvasDim / 7) +``` +Solving: `canvasDim = ceil(imageDim * 7 / 5)` + +For imageDim=1024: canvasDim=1434, legendWidth=1434 - 2*204 = 1026 ≈ 1024. Close enough — canvas_ity handles the minor scaling. For pixel-perfect output, the density images could be generated at exactly `legendWidth` pixels, but the ~0.2% difference is imperceptible. + +### Color order conventions + +- `CreateIPFLegend()` and `createIPFColorImage()` both return pixels packed as uint32 via `RgbColor::dRgb()`: on little-endian systems the byte layout is `[B, G, R, A]` +- `ConvertColorOrder()` swaps bytes 0↔2: `[B,G,R,A] → [R,G,B,A]` (RGBA for canvas_ity) +- `MirrorImage()` flips rows vertically (image drawn with +Y down, canvas_ity uses +Y up) +- `annotateIPFImage()` handles both transforms internally, then removes alpha at the end +- Final output is **RGB** (3 components) + +### The `drawIPFAnnotations` parameter signature + +The `margins` parameter uses `const std::vector&` (pass by const reference) for consistency. The existing `DrawFullCircleAnnotations` free functions use pass-by-value. When converting, change the parameter to `const std::vector&` to match the new convention. + +### Density always uses SST-only view + +`generateAnnotatedIPFDensity()` always passes `generateEntirePlane = false` to `annotateIPFImage()`. This is intentional: IPF density plots display data within the Standard Stereographic Triangle, not the full stereographic circle. From 6eeba4b9cda73cf5c67e3d24475c5bae6ed0ca13 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Mon, 23 Mar 2026 22:20:45 -0400 Subject: [PATCH 03/14] ENH: Add shared IPF annotation pipeline declarations and implementations in LaueOps Add virtual hooks drawIPFAnnotations() and adjustFigureOrigin() to LaueOps base class, along with annotateIPFImage() shared scaffolding that extracts the common canvas setup from the 11 subclass generateIPFTriangleLegend() methods. Also adds generateAnnotatedIPFDensity() and drawColorBar() for annotated density image generation with color bars. Co-Authored-By: Claude Opus 4.6 (1M context) --- Source/EbsdLib/LaueOps/LaueOps.cpp | 303 +++++++++++++++++++++++++++++ Source/EbsdLib/LaueOps/LaueOps.h | 62 ++++++ 2 files changed, 365 insertions(+) diff --git a/Source/EbsdLib/LaueOps/LaueOps.cpp b/Source/EbsdLib/LaueOps/LaueOps.cpp index f68c066..c04568b 100644 --- a/Source/EbsdLib/LaueOps/LaueOps.cpp +++ b/Source/EbsdLib/LaueOps/LaueOps.cpp @@ -49,14 +49,20 @@ #include "EbsdLib/LaueOps/TrigonalLowOps.h" #include "EbsdLib/LaueOps/TrigonalOps.h" #include "EbsdLib/Orientation/Quaternion.hpp" +#include "EbsdLib/Utilities/CanvasUtilities.hpp" #include "EbsdLib/Utilities/ColorTable.h" #include "EbsdLib/Utilities/ComputeStereographicProjection.h" +#include "EbsdLib/Utilities/Fonts.hpp" + +#include #include // for std::max #include #include +#include #include #include +#include /** | Index | Verified | Class | Rotation Point Group | Num Sym Ops | @@ -935,3 +941,300 @@ ebsdlib::Rgb LaueOps::generateMisorientationColor(const QuatD& q, const QuatD& r { throw std::runtime_error("LaueOps::generateMisorientationColor is not implemented."); } + +// ----------------------------------------------------------------------------- +std::array LaueOps::adjustFigureOrigin( + std::array figureOrigin, + int legendWidth, int legendHeight, + const std::vector& margins, float fontPtSize, + bool generateEntirePlane) const +{ + return figureOrigin; +} + +// ----------------------------------------------------------------------------- +UInt8ArrayType::Pointer LaueOps::annotateIPFImage( + UInt8ArrayType::Pointer triangleImage, + int imageDim, + int canvasDim, + const std::string& title, + bool generateEntirePlane) const +{ + const float fontPtSize = static_cast(canvasDim) / 24.0f; + const std::vector margins = { + fontPtSize * 3, // Top + static_cast(canvasDim / 7.0f), // Right + fontPtSize * 2, // Bottom + static_cast(canvasDim / 7.0f) // Left + }; + + int legendHeight = canvasDim - static_cast(margins[0]) - static_cast(margins[2]); + int legendWidth = canvasDim - static_cast(margins[1]) - static_cast(margins[3]); + + if(legendHeight > legendWidth) + { + legendHeight = legendWidth; + } + else + { + legendWidth = legendHeight; + } + + int halfWidth = legendWidth / 2; + int halfHeight = legendHeight / 2; + + std::array figureOrigin = {margins[3], margins[0] * 1.33F}; + figureOrigin = adjustFigureOrigin(figureOrigin, legendWidth, legendHeight, margins, fontPtSize, generateEntirePlane); + + std::array figureCenter = {figureOrigin[0] + halfWidth, figureOrigin[1] + halfHeight}; + + // Convert from ARGB to RGBA for canvas_ity + ebsdlib::UInt8ArrayType::Pointer image = ebsdlib::ConvertColorOrder(triangleImage.get(), imageDim); + // Mirror across X axis (image drawn with +Y pointing down) + image = ebsdlib::MirrorImage(image.get(), imageDim); + + // Create canvas + canvas_ity::canvas context(canvasDim, canvasDim); + + std::vector latoBold = ebsdlib::fonts::GetLatoBold(); + std::vector latoRegular = ebsdlib::fonts::GetLatoRegular(); + context.set_font(latoBold.data(), static_cast(latoBold.size()), fontPtSize); + context.set_color(canvas_ity::fill_style, 0.0f, 0.0f, 0.0f, 1.0f); + context.text_baseline = canvas_ity::alphabetic; + + // Fill background with white + context.move_to(0.0f, 0.0f); + context.line_to(static_cast(canvasDim), 0.0f); + context.line_to(static_cast(canvasDim), static_cast(canvasDim)); + context.line_to(0.0f, static_cast(canvasDim)); + context.line_to(0.0f, 0.0f); + context.close_path(); + context.set_color(canvas_ity::fill_style, 1.0f, 1.0f, 1.0f, 1.0f); + context.fill(); + + // Draw the triangle image onto the canvas + context.draw_image(image->getPointer(0), imageDim, imageDim, + imageDim * image->getNumberOfComponents(), + figureOrigin[0], figureOrigin[1], + static_cast(legendWidth), + static_cast(legendHeight)); + + // Draw title + context.set_font(latoBold.data(), static_cast(latoBold.size()), fontPtSize * 1.5); + ebsdlib::WriteText(context, title, {margins[0], static_cast(fontPtSize * 1.5)}, fontPtSize * 1.5); + + // Draw per-subclass annotations (Miller indices, SST boundary lines) + context.set_font(latoRegular.data(), static_cast(latoRegular.size()), fontPtSize); + drawIPFAnnotations(context, canvasDim, fontPtSize, margins, figureOrigin, figureCenter, generateEntirePlane); + + // Extract rendered pixels and remove alpha channel + ebsdlib::UInt8ArrayType::Pointer rgbaCanvasImage = ebsdlib::UInt8ArrayType::CreateArray( + canvasDim * canvasDim, {4ULL}, "Annotated IPF", true); + context.get_image_data(rgbaCanvasImage->getPointer(0), canvasDim, canvasDim, canvasDim * 4, 0, 0); + + return ebsdlib::RemoveAlphaChannel(rgbaCanvasImage.get()); +} + +// ----------------------------------------------------------------------------- +UInt8ArrayType::Pointer LaueOps::drawColorBar( + UInt8ArrayType::Pointer image, + int canvasDim, + int numColors, + double minValue, double maxValue, + bool isMRD) const +{ + const float fontPtSize = static_cast(canvasDim) / 24.0f; + + // Generate the color table + std::vector colors; + EbsdColorTable::GetColorTable(numColors, colors); + + // Create a canvas from the existing RGB image by first adding an alpha channel + const size_t numPixels = static_cast(canvasDim * canvasDim); + ebsdlib::UInt8ArrayType::Pointer rgbaImage = ebsdlib::UInt8ArrayType::CreateArray(numPixels, {4ULL}, "ColorBarCanvas", true); + uint8_t* srcPtr = image->getPointer(0); + uint8_t* dstPtr = rgbaImage->getPointer(0); + for(size_t i = 0; i < numPixels; i++) + { + dstPtr[i * 4 + 0] = srcPtr[i * 3 + 0]; + dstPtr[i * 4 + 1] = srcPtr[i * 3 + 1]; + dstPtr[i * 4 + 2] = srcPtr[i * 3 + 2]; + dstPtr[i * 4 + 3] = 255; + } + + canvas_ity::canvas context(canvasDim, canvasDim); + // Put the existing image onto the canvas + context.draw_image(rgbaImage->getPointer(0), canvasDim, canvasDim, + canvasDim * 4, 0.0f, 0.0f, + static_cast(canvasDim), + static_cast(canvasDim)); + + // Color bar dimensions + const float barLeft = static_cast(canvasDim) * 0.80f; + const float barTop = static_cast(canvasDim) * 0.15f; + const float barWidth = static_cast(canvasDim) * 0.04f; + const float barHeight = static_cast(canvasDim) * 0.65f; + + // Draw color bar segments + int colorSegments = numColors; + float segmentHeight = barHeight / static_cast(colorSegments); + for(int i = 0; i < colorSegments; i++) + { + // Map from top (max) to bottom (min) + int colorIdx = (colorSegments - 1 - i) * 3; + float r = colors[colorIdx + 0]; + float g = colors[colorIdx + 1]; + float b = colors[colorIdx + 2]; + + float segTop = barTop + static_cast(i) * segmentHeight; + context.begin_path(); + context.move_to(barLeft, segTop); + context.line_to(barLeft + barWidth, segTop); + context.line_to(barLeft + barWidth, segTop + segmentHeight); + context.line_to(barLeft, segTop + segmentHeight); + context.close_path(); + context.set_color(canvas_ity::fill_style, r, g, b, 1.0f); + context.fill(); + } + + // Draw border around color bar + context.begin_path(); + context.move_to(barLeft, barTop); + context.line_to(barLeft + barWidth, barTop); + context.line_to(barLeft + barWidth, barTop + barHeight); + context.line_to(barLeft, barTop + barHeight); + context.close_path(); + context.set_color(canvas_ity::stroke_style, 0.0f, 0.0f, 0.0f, 1.0f); + context.set_line_width(1.0f); + context.stroke(); + + // Draw min/max labels + std::vector latoRegular = ebsdlib::fonts::GetLatoRegular(); + context.set_font(latoRegular.data(), static_cast(latoRegular.size()), fontPtSize * 0.8f); + context.set_color(canvas_ity::fill_style, 0.0f, 0.0f, 0.0f, 1.0f); + + // Format min/max values + std::ostringstream maxStr; + maxStr << std::fixed << std::setprecision(2) << maxValue; + std::ostringstream minStr; + minStr << std::fixed << std::setprecision(2) << minValue; + + float labelX = barLeft + barWidth + fontPtSize * 0.3f; + ebsdlib::WriteText(context, maxStr.str(), {labelX, barTop + fontPtSize * 0.3f}, fontPtSize * 0.8f); + ebsdlib::WriteText(context, minStr.str(), {labelX, barTop + barHeight}, fontPtSize * 0.8f); + + // Draw MRD or counts label + std::string unitLabel = isMRD ? "MRD" : "Counts"; + std::vector latoBold = ebsdlib::fonts::GetLatoBold(); + context.set_font(latoBold.data(), static_cast(latoBold.size()), fontPtSize * 0.7f); + ebsdlib::WriteText(context, unitLabel, {barLeft, barTop - fontPtSize * 0.5f}, fontPtSize * 0.7f); + + // Extract and remove alpha + ebsdlib::UInt8ArrayType::Pointer outRgba = ebsdlib::UInt8ArrayType::CreateArray(numPixels, {4ULL}, "ColorBarOutput", true); + context.get_image_data(outRgba->getPointer(0), canvasDim, canvasDim, canvasDim * 4, 0, 0); + + return ebsdlib::RemoveAlphaChannel(outRgba.get()); +} + +// ----------------------------------------------------------------------------- +std::vector LaueOps::generateAnnotatedIPFDensity( + InversePoleFigureConfiguration_t& config, + std::pair* outMinMax) const +{ + // Validate square images + if(config.imageWidth != config.imageHeight) + { + throw std::runtime_error("generateAnnotatedIPFDensity requires square images (imageWidth == imageHeight)."); + } + + const int imageDim = config.imageWidth; + const int canvasDim = static_cast(static_cast(imageDim) * 1.5f); + + // Determine labels + std::string label0 = "IPF-0"; + std::string label1 = "IPF-1"; + std::string label2 = "IPF-2"; + if(config.labels.size() >= 1) + { + label0 = config.labels[0]; + } + if(config.labels.size() >= 2) + { + label1 = config.labels[1]; + } + if(config.labels.size() >= 3) + { + label2 = config.labels[2]; + } + + // Step 1: Compute IPF directions for each sample direction + ebsdlib::FloatArrayType::Pointer dirs0 = InversePoleFigureUtilities::computeIPFDirections(*this, config.eulers, config.sampleDirections[0]); + ebsdlib::FloatArrayType::Pointer dirs1 = InversePoleFigureUtilities::computeIPFDirections(*this, config.eulers, config.sampleDirections[1]); + ebsdlib::FloatArrayType::Pointer dirs2 = InversePoleFigureUtilities::computeIPFDirections(*this, config.eulers, config.sampleDirections[2]); + + // Step 2: Compute intensity images + ebsdlib::DoubleArrayType::Pointer intensity0 = InversePoleFigureUtilities::computeIPFIntensity(*this, dirs0.get(), imageDim, imageDim, config.lambertDim, config.normalizeMRD); + ebsdlib::DoubleArrayType::Pointer intensity1 = InversePoleFigureUtilities::computeIPFIntensity(*this, dirs1.get(), imageDim, imageDim, config.lambertDim, config.normalizeMRD); + ebsdlib::DoubleArrayType::Pointer intensity2 = InversePoleFigureUtilities::computeIPFIntensity(*this, dirs2.get(), imageDim, imageDim, config.lambertDim, config.normalizeMRD); + + // Step 3: Find global min/max + double globalMax = std::numeric_limits::lowest(); + double globalMin = std::numeric_limits::max(); + + std::array intensities = {intensity0.get(), intensity1.get(), intensity2.get()}; + for(auto* intensityArr : intensities) + { + double* dPtr = intensityArr->getPointer(0); + size_t count = intensityArr->getNumberOfTuples(); + for(size_t i = 0; i < count; ++i) + { + if(dPtr[i] >= 0.0) + { + if(dPtr[i] > globalMax) + { + globalMax = dPtr[i]; + } + if(dPtr[i] < globalMin) + { + globalMin = dPtr[i]; + } + } + } + } + + if(globalMax < globalMin) + { + globalMin = 0.0; + globalMax = 1.0; + } + + if(outMinMax != nullptr) + { + *outMinMax = {globalMin, globalMax}; + } + + // Step 4: Create RGBA color images + std::vector dims = {4}; + ebsdlib::UInt8ArrayType::Pointer image0 = ebsdlib::UInt8ArrayType::CreateArray(static_cast(imageDim * imageDim), dims, label0, true); + ebsdlib::UInt8ArrayType::Pointer image1 = ebsdlib::UInt8ArrayType::CreateArray(static_cast(imageDim * imageDim), dims, label1, true); + ebsdlib::UInt8ArrayType::Pointer image2 = ebsdlib::UInt8ArrayType::CreateArray(static_cast(imageDim * imageDim), dims, label2, true); + + InversePoleFigureUtilities::createIPFColorImage(intensity0.get(), imageDim, imageDim, config.numColors, globalMin, globalMax, image0.get()); + InversePoleFigureUtilities::createIPFColorImage(intensity1.get(), imageDim, imageDim, config.numColors, globalMin, globalMax, image1.get()); + InversePoleFigureUtilities::createIPFColorImage(intensity2.get(), imageDim, imageDim, config.numColors, globalMin, globalMax, image2.get()); + + // Step 5: Build title strings + std::string titlePrefix = config.phaseName.empty() ? "" : config.phaseName + " - "; + + // Step 6: Annotate each image + UInt8ArrayType::Pointer annotated0 = annotateIPFImage(image0, imageDim, canvasDim, titlePrefix + label0, false); + UInt8ArrayType::Pointer annotated1 = annotateIPFImage(image1, imageDim, canvasDim, titlePrefix + label1, false); + UInt8ArrayType::Pointer annotated2 = annotateIPFImage(image2, imageDim, canvasDim, titlePrefix + label2, false); + + // Step 7: Add color bars + annotated0 = drawColorBar(annotated0, canvasDim, config.numColors, globalMin, globalMax, config.normalizeMRD); + annotated1 = drawColorBar(annotated1, canvasDim, config.numColors, globalMin, globalMax, config.normalizeMRD); + annotated2 = drawColorBar(annotated2, canvasDim, config.numColors, globalMin, globalMax, config.normalizeMRD); + + return {annotated0, annotated1, annotated2}; +} diff --git a/Source/EbsdLib/LaueOps/LaueOps.h b/Source/EbsdLib/LaueOps/LaueOps.h index 18e9e10..f7d581c 100644 --- a/Source/EbsdLib/LaueOps/LaueOps.h +++ b/Source/EbsdLib/LaueOps/LaueOps.h @@ -34,10 +34,14 @@ * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ #pragma once +#include #include #include +#include #include +#include + #include "EbsdLib/Core/EbsdDataArray.hpp" #include "EbsdLib/EbsdLib.h" #include "EbsdLib/Math/Matrix3X3.hpp" @@ -327,6 +331,37 @@ class EbsdLib_EXPORT LaueOps */ virtual UInt8ArrayType::Pointer generateIPFTriangleLegend(int imageDim, bool generateEntirePlane) const = 0; + /** + * @brief Per-subclass hook that draws Miller index labels and SST boundary + * annotations onto a canvas. Called by annotateIPFImage(). + */ + virtual void drawIPFAnnotations(canvas_ity::canvas& context, int canvasDim, + float fontPtSize, const std::vector& margins, + std::array figureOrigin, + std::array figureCenter, + bool drawFullCircle) const = 0; + + /** + * @brief Per-subclass hook that adjusts the figureOrigin when rendering + * SST-only view. Each subclass overrides to position its triangle shape + * correctly within the canvas. Default returns figureOrigin unchanged. + */ + virtual std::array adjustFigureOrigin( + std::array figureOrigin, + int legendWidth, int legendHeight, + const std::vector& margins, float fontPtSize, + bool generateEntirePlane) const; + + /** + * @brief Generates 3 annotated inverse pole figure density images with + * title, Miller index labels, and MRD color bar. + * @param config Configuration struct; imageWidth must equal imageHeight (square images required) + * @param outMinMax Optional output for the global [min, max] intensity values + */ + std::vector generateAnnotatedIPFDensity( + InversePoleFigureConfiguration_t& config, + std::pair* outMinMax = nullptr) const; + /** * @brief Generates 3 inverse pole figure density images for 3 orthogonal sample directions. * The IPF density plot shows how a sample direction distributes across crystal directions @@ -450,6 +485,33 @@ class EbsdLib_EXPORT LaueOps protected: LaueOps(); + /** + * @brief Shared annotation scaffolding for IPF images. Creates a canvas, + * draws the triangle image, adds title and per-subclass annotations. + * @param triangleImage Pre-rendered ARGB image (square, imageDim x imageDim) + * @param imageDim Pixel dimension of the triangle image (square) + * @param canvasDim Pixel dimension of the output canvas (square) + * @param title Text to draw as the title + * @param generateEntirePlane true = full circle view, false = SST only + * @return RGB image (canvasDim x canvasDim, 3 components) + */ + UInt8ArrayType::Pointer annotateIPFImage( + UInt8ArrayType::Pointer triangleImage, + int imageDim, + int canvasDim, + const std::string& title, + bool generateEntirePlane) const; + + /** + * @brief Draws a color bar with min/max labels onto an existing RGB image. + */ + UInt8ArrayType::Pointer drawColorBar( + UInt8ArrayType::Pointer image, + int canvasDim, + int numColors, + double minValue, double maxValue, + bool isMRD) const; + /** * @brief calculateMisorientationInternal * @param quatsym The Symmetry Quarternion from the specific Laue class From 306b82f8a1c4d349cb1a1d057e0243cf2c969c2d Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Mon, 23 Mar 2026 22:30:50 -0400 Subject: [PATCH 04/14] ENH: Refactor all 11 LaueOps subclasses to use shared annotation pipeline Co-Authored-By: Claude Opus 4.6 (1M context) --- Source/EbsdLib/LaueOps/CubicLowOps.cpp | 119 +++++++------------- Source/EbsdLib/LaueOps/CubicLowOps.h | 12 ++ Source/EbsdLib/LaueOps/CubicOps.cpp | 119 +++++++------------- Source/EbsdLib/LaueOps/CubicOps.h | 12 ++ Source/EbsdLib/LaueOps/HexagonalLowOps.cpp | 100 ++++++---------- Source/EbsdLib/LaueOps/HexagonalLowOps.h | 12 ++ Source/EbsdLib/LaueOps/HexagonalOps.cpp | 99 ++++++---------- Source/EbsdLib/LaueOps/HexagonalOps.h | 12 ++ Source/EbsdLib/LaueOps/MonoclinicOps.cpp | 89 +++------------ Source/EbsdLib/LaueOps/MonoclinicOps.h | 6 + Source/EbsdLib/LaueOps/OrthoRhombicOps.cpp | 102 +++++------------ Source/EbsdLib/LaueOps/OrthoRhombicOps.h | 12 ++ Source/EbsdLib/LaueOps/TetragonalLowOps.cpp | 103 +++++------------ Source/EbsdLib/LaueOps/TetragonalLowOps.h | 12 ++ Source/EbsdLib/LaueOps/TetragonalOps.cpp | 104 ++++++----------- Source/EbsdLib/LaueOps/TetragonalOps.h | 12 ++ Source/EbsdLib/LaueOps/TriclinicOps.cpp | 88 +++------------ Source/EbsdLib/LaueOps/TriclinicOps.h | 6 + Source/EbsdLib/LaueOps/TrigonalLowOps.cpp | 100 ++++++---------- Source/EbsdLib/LaueOps/TrigonalLowOps.h | 12 ++ Source/EbsdLib/LaueOps/TrigonalOps.cpp | 99 ++++++---------- Source/EbsdLib/LaueOps/TrigonalOps.h | 12 ++ 22 files changed, 458 insertions(+), 784 deletions(-) diff --git a/Source/EbsdLib/LaueOps/CubicLowOps.cpp b/Source/EbsdLib/LaueOps/CubicLowOps.cpp index c2b158f..8325063 100644 --- a/Source/EbsdLib/LaueOps/CubicLowOps.cpp +++ b/Source/EbsdLib/LaueOps/CubicLowOps.cpp @@ -989,9 +989,37 @@ ebsdlib::UInt8ArrayType::Pointer CreateIPFLegend(const CubicLowOps* ops, int ima } // ----------------------------------------------------------------------------- -void DrawFullCircleAnnotations(canvas_ity::canvas& context, int canvasDim, float fontPtSize, std::vector margins, std::array figureOrigin, std::array figureCenter, - bool drawFullCircle) +} // namespace + +// ----------------------------------------------------------------------------- +std::array CubicLowOps::adjustFigureOrigin( + std::array figureOrigin, + int legendWidth, int legendHeight, + const std::vector& margins, float fontPtSize, + bool generateEntirePlane) const +{ + if(!generateEntirePlane) + { + figureOrigin[1] = fontPtSize * 2.0F; + } + return figureOrigin; +} + +// ----------------------------------------------------------------------------- +void CubicLowOps::drawIPFAnnotations(canvas_ity::canvas& context, int canvasDim, float fontPtSize, const std::vector& margins, std::array figureOrigin, + std::array figureCenter, bool drawFullCircle) const { + if(!drawFullCircle) + { + int legendHeight = canvasDim - static_cast(margins[0]) - static_cast(margins[2]); + int legendWidth = canvasDim - static_cast(margins[1]) - static_cast(margins[3]); + if(legendHeight > legendWidth) + { + legendHeight = legendWidth; + } + figureCenter = {figureOrigin[0], figureOrigin[1] + static_cast(legendHeight)}; + } + int legendHeight = canvasDim - static_cast(margins[0]) - static_cast(margins[2]); int legendWidth = canvasDim - static_cast(margins[1]) - static_cast(margins[3]); @@ -1119,21 +1147,19 @@ void DrawFullCircleAnnotations(canvas_ity::canvas& context, int canvasDim, float } } -} // namespace - // ----------------------------------------------------------------------------- ebsdlib::UInt8ArrayType::Pointer CubicLowOps::generateIPFTriangleLegend(int canvasDim, bool generateEntirePlane) const { - // Figure out the Legend Pixel Size + // Compute legend dimensions (same formula as annotateIPFImage uses) const float fontPtSize = static_cast(canvasDim) / 24.0f; - const std::vector margins = {fontPtSize * 3, // Top - static_cast(canvasDim) / 7.0F, // Right - fontPtSize * 2, // Bottom - static_cast(canvasDim) / 7.0F}; // Left - + const std::vector margins = { + fontPtSize * 3, + static_cast(canvasDim / 7.0f), + fontPtSize * 2, + static_cast(canvasDim / 7.0f) + }; int legendHeight = canvasDim - static_cast(margins[0]) - static_cast(margins[2]); int legendWidth = canvasDim - static_cast(margins[1]) - static_cast(margins[3]); - if(legendHeight > legendWidth) { legendHeight = legendWidth; @@ -1142,77 +1168,12 @@ ebsdlib::UInt8ArrayType::Pointer CubicLowOps::generateIPFTriangleLegend(int canv { legendWidth = legendHeight; } - int pageHeight = canvasDim; - int pageWidth = canvasDim; - int halfWidth = legendWidth / 2; - int halfHeight = legendHeight / 2; - - std::array figureOrigin = {margins[3], margins[0] * 1.33F}; - if(!generateEntirePlane) - { - // figureOrigin[0] = margins[3] * 2.0F; - figureOrigin[1] = 0.0F + fontPtSize * 2.0F; - } - std::array figureCenter = {figureOrigin[0] + static_cast(halfWidth), figureOrigin[1] + static_cast(halfHeight)}; - // Create the actual Legend which will come back as ARGB values + // Generate the colored SST triangle image (ARGB) ebsdlib::UInt8ArrayType::Pointer image = CreateIPFLegend(this, legendHeight, generateEntirePlane); - // Convert from ARGB to RGBA which is what canvas_itk wants - image = ebsdlib::ConvertColorOrder(image.get(), legendHeight); - - // We need to mirror across the X Axis because the image was drawn with +Y pointing down - image = ebsdlib::MirrorImage(image.get(), legendHeight); - - // Create a 2D Canvas to draw into now that the Legend is in the proper form - canvas_ity::canvas context(pageWidth, pageHeight); - - std::vector latoBold = ebsdlib::fonts::GetLatoBold(); - std::vector latoRegular = ebsdlib::fonts::GetLatoRegular(); - context.set_font(latoBold.data(), static_cast(latoBold.size()), fontPtSize); - context.set_color(canvas_ity::fill_style, 0.0f, 0.0f, 0.0f, 1.0f); - canvas_ity::baseline_style const baselines[] = {canvas_ity::alphabetic, canvas_ity::top, canvas_ity::middle, canvas_ity::bottom, canvas_ity::hanging, canvas_ity::ideographic}; - context.text_baseline = baselines[0]; - - // Fill the whole background with white - context.move_to(0.0f, 0.0f); - context.line_to(static_cast(pageWidth), 0.0f); - context.line_to(static_cast(pageWidth), static_cast(pageHeight)); - context.line_to(0.0f, static_cast(pageHeight)); - context.line_to(0.0f, 0.0f); - context.close_path(); - context.set_color(canvas_ity::fill_style, 1.0f, 1.0f, 1.0f, 1.0f); - context.fill(); - - // Draw the legend image onto the canvas at the correct spot. - context.draw_image(image->getPointer(0), legendWidth, legendHeight, legendWidth * image->getNumberOfComponents(), figureOrigin[0], figureOrigin[1], static_cast(legendWidth), - static_cast(legendHeight)); - - // Draw Title of Legend - context.set_font(latoBold.data(), static_cast(latoBold.size()), fontPtSize * 1.5F); - ebsdlib::WriteText(context, getSymmetryName(), {margins[0], static_cast(fontPtSize * 1.5)}, fontPtSize * 1.5F); - - if(generateEntirePlane) - { - context.set_font(latoRegular.data(), static_cast(latoRegular.size()), fontPtSize); - DrawFullCircleAnnotations(context, canvasDim, fontPtSize, margins, figureOrigin, figureCenter, true); - } - else - { - figureCenter = {figureOrigin[0], figureOrigin[1] + static_cast(legendHeight)}; - context.set_font(latoRegular.data(), static_cast(latoRegular.size()), fontPtSize); - DrawFullCircleAnnotations(context, canvasDim, fontPtSize, margins, figureOrigin, figureCenter, false); - } - - // Fetch the rendered RGBA pixels from the entire canvas. - ebsdlib::UInt8ArrayType::Pointer rgbaCanvasImage = ebsdlib::UInt8ArrayType::CreateArray(pageHeight * pageWidth, {4ULL}, "Triangle Legend", true); - // std::vector rgbaCanvasImage(static_cast(pageHeight * pageWidth * 4)); - context.get_image_data(rgbaCanvasImage->getPointer(0), pageWidth, pageHeight, pageWidth * 4, 0, 0); - - // Remove the Alpha channel from the final image - rgbaCanvasImage = ebsdlib::RemoveAlphaChannel(rgbaCanvasImage.get()); - - return rgbaCanvasImage; + // Annotate with title and Miller index labels + return annotateIPFImage(image, legendHeight, canvasDim, getSymmetryName(), generateEntirePlane); } // ----------------------------------------------------------------------------- diff --git a/Source/EbsdLib/LaueOps/CubicLowOps.h b/Source/EbsdLib/LaueOps/CubicLowOps.h index d87deea..8a9ab0e 100644 --- a/Source/EbsdLib/LaueOps/CubicLowOps.h +++ b/Source/EbsdLib/LaueOps/CubicLowOps.h @@ -260,6 +260,18 @@ class EbsdLib_EXPORT CubicLowOps : public LaueOps */ ebsdlib::UInt8ArrayType::Pointer generateIPFTriangleLegend(int imageDim, bool generateEntirePlane) const override; + void drawIPFAnnotations(canvas_ity::canvas& context, int canvasDim, + float fontPtSize, const std::vector& margins, + std::array figureOrigin, + std::array figureCenter, + bool drawFullCircle) const override; + + std::array adjustFigureOrigin( + std::array figureOrigin, + int legendWidth, int legendHeight, + const std::vector& margins, float fontPtSize, + bool generateEntirePlane) const override; + /** * @brief Returns if the given Quaternion is within the Rodrigues Fundamental Zone (RFZ) * @param quat Input Quaternion diff --git a/Source/EbsdLib/LaueOps/CubicOps.cpp b/Source/EbsdLib/LaueOps/CubicOps.cpp index ad6bff1..2fab688 100644 --- a/Source/EbsdLib/LaueOps/CubicOps.cpp +++ b/Source/EbsdLib/LaueOps/CubicOps.cpp @@ -1952,9 +1952,37 @@ ebsdlib::UInt8ArrayType::Pointer CreateIPFLegend(const CubicOps* ops, int imageD } // ----------------------------------------------------------------------------- -void DrawFullCircleAnnotations(canvas_ity::canvas& context, int canvasDim, float fontPtSize, std::vector margins, std::array figureOrigin, std::array figureCenter, - bool drawFullCircle) +} // namespace + +// ----------------------------------------------------------------------------- +std::array CubicOps::adjustFigureOrigin( + std::array figureOrigin, + int legendWidth, int legendHeight, + const std::vector& margins, float fontPtSize, + bool generateEntirePlane) const { + if(!generateEntirePlane) + { + figureOrigin[1] = fontPtSize * 2.0F; + } + return figureOrigin; +} + +// ----------------------------------------------------------------------------- +void CubicOps::drawIPFAnnotations(canvas_ity::canvas& context, int canvasDim, float fontPtSize, const std::vector& margins, std::array figureOrigin, std::array figureCenter, + bool drawFullCircle) const +{ + if(!drawFullCircle) + { + int legendHeight = canvasDim - static_cast(margins[0]) - static_cast(margins[2]); + int legendWidth = canvasDim - static_cast(margins[1]) - static_cast(margins[3]); + if(legendHeight > legendWidth) + { + legendHeight = legendWidth; + } + figureCenter = {figureOrigin[0], figureOrigin[1] + static_cast(legendHeight)}; + } + int legendHeight = canvasDim - margins[0] - margins[2]; int legendWidth = canvasDim - margins[1] - margins[3]; @@ -2071,21 +2099,19 @@ void DrawFullCircleAnnotations(canvas_ity::canvas& context, int canvasDim, float } } -} // namespace - // ----------------------------------------------------------------------------- ebsdlib::UInt8ArrayType::Pointer CubicOps::generateIPFTriangleLegend(int canvasDim, bool generateEntirePlane) const { - // Figure out the Legend Pixel Size + // Compute legend dimensions (same formula as annotateIPFImage uses) const float fontPtSize = static_cast(canvasDim) / 24.0f; - const std::vector margins = {fontPtSize * 3, // Top - static_cast(canvasDim / 7.0f), // Right - fontPtSize * 2, // Bottom - static_cast(canvasDim / 7.0f)}; // Left - + const std::vector margins = { + fontPtSize * 3, + static_cast(canvasDim / 7.0f), + fontPtSize * 2, + static_cast(canvasDim / 7.0f) + }; int legendHeight = canvasDim - static_cast(margins[0]) - static_cast(margins[2]); int legendWidth = canvasDim - static_cast(margins[1]) - static_cast(margins[3]); - if(legendHeight > legendWidth) { legendHeight = legendWidth; @@ -2094,77 +2120,12 @@ ebsdlib::UInt8ArrayType::Pointer CubicOps::generateIPFTriangleLegend(int canvasD { legendWidth = legendHeight; } - int pageHeight = canvasDim; - int pageWidth = canvasDim; - int halfWidth = legendWidth / 2; - int halfHeight = legendHeight / 2; - - std::array figureOrigin = {margins[3], margins[0] * 1.33F}; - if(!generateEntirePlane) - { - // figureOrigin[0] = margins[3] * 2.0F; - figureOrigin[1] = 0.0F + fontPtSize * 2.0F; - } - std::array figureCenter = {figureOrigin[0] + halfWidth, figureOrigin[1] + halfHeight}; - // Create the actual Legend which will come back as ARGB values + // Generate the colored SST triangle image (ARGB) ebsdlib::UInt8ArrayType::Pointer image = CreateIPFLegend(this, legendHeight, generateEntirePlane); - // Convert from ARGB to RGBA which is what canvas_itk wants - image = ebsdlib::ConvertColorOrder(image.get(), legendHeight); - - // We need to mirror across the X Axis because the image was drawn with +Y pointing down - image = ebsdlib::MirrorImage(image.get(), legendHeight); - - // Create a 2D Canvas to draw into now that the Legend is in the proper form - canvas_ity::canvas context(pageWidth, pageHeight); - - std::vector latoBold = ebsdlib::fonts::GetLatoBold(); - std::vector latoRegular = ebsdlib::fonts::GetLatoRegular(); - context.set_font(latoBold.data(), static_cast(latoBold.size()), fontPtSize); - context.set_color(canvas_ity::fill_style, 0.0f, 0.0f, 0.0f, 1.0f); - canvas_ity::baseline_style const baselines[] = {canvas_ity::alphabetic, canvas_ity::top, canvas_ity::middle, canvas_ity::bottom, canvas_ity::hanging, canvas_ity::ideographic}; - context.text_baseline = baselines[0]; - - // Fill the whole background with white - context.move_to(0.0f, 0.0f); - context.line_to(static_cast(pageWidth), 0.0f); - context.line_to(static_cast(pageWidth), static_cast(pageHeight)); - context.line_to(0.0f, static_cast(pageHeight)); - context.line_to(0.0f, 0.0f); - context.close_path(); - context.set_color(canvas_ity::fill_style, 1.0f, 1.0f, 1.0f, 1.0f); - context.fill(); - - // Draw the legend image onto the canvas at the correct spot. - context.draw_image(image->getPointer(0), legendWidth, legendHeight, legendWidth * image->getNumberOfComponents(), figureOrigin[0], figureOrigin[1], static_cast(legendWidth), - static_cast(legendHeight)); - - // Draw Title of Legend - context.set_font(latoBold.data(), static_cast(latoBold.size()), fontPtSize * 1.5); - ebsdlib::WriteText(context, getSymmetryName(), {margins[0], static_cast(fontPtSize * 1.5)}, fontPtSize * 1.5); - - if(generateEntirePlane) - { - context.set_font(latoRegular.data(), static_cast(latoRegular.size()), fontPtSize); - DrawFullCircleAnnotations(context, canvasDim, fontPtSize, margins, figureOrigin, figureCenter, true); - } - else - { - figureCenter = {figureOrigin[0], figureOrigin[1] + legendHeight}; - context.set_font(latoRegular.data(), static_cast(latoRegular.size()), fontPtSize); - DrawFullCircleAnnotations(context, canvasDim, fontPtSize, margins, figureOrigin, figureCenter, false); - } - - // Fetch the rendered RGBA pixels from the entire canvas. - ebsdlib::UInt8ArrayType::Pointer rgbaCanvasImage = ebsdlib::UInt8ArrayType::CreateArray(pageHeight * pageWidth, {4ULL}, "Triangle Legend", true); - // std::vector rgbaCanvasImage(static_cast(pageHeight * pageWidth * 4)); - context.get_image_data(rgbaCanvasImage->getPointer(0), pageWidth, pageHeight, pageWidth * 4, 0, 0); - - // Remove the Alpha channel from the final image - rgbaCanvasImage = ebsdlib::RemoveAlphaChannel(rgbaCanvasImage.get()); - - return rgbaCanvasImage; + // Annotate with title and Miller index labels + return annotateIPFImage(image, legendHeight, canvasDim, getSymmetryName(), generateEntirePlane); } std::vector> CubicOps::rodri2pair(std::vector x, std::vector y, std::vector z) diff --git a/Source/EbsdLib/LaueOps/CubicOps.h b/Source/EbsdLib/LaueOps/CubicOps.h index 742bdbf..f14d6f9 100644 --- a/Source/EbsdLib/LaueOps/CubicOps.h +++ b/Source/EbsdLib/LaueOps/CubicOps.h @@ -306,6 +306,18 @@ class EbsdLib_EXPORT CubicOps : public LaueOps */ ebsdlib::UInt8ArrayType::Pointer generateIPFTriangleLegend(int imageDim, bool generateEntirePlane) const override; + void drawIPFAnnotations(canvas_ity::canvas& context, int canvasDim, + float fontPtSize, const std::vector& margins, + std::array figureOrigin, + std::array figureCenter, + bool drawFullCircle) const override; + + std::array adjustFigureOrigin( + std::array figureOrigin, + int legendWidth, int legendHeight, + const std::vector& margins, float fontPtSize, + bool generateEntirePlane) const override; + /** * @brief Returns if the given Quaternion is within the Rodrigues Fundamental Zone (RFZ) * @param quat Input Quaternion diff --git a/Source/EbsdLib/LaueOps/HexagonalLowOps.cpp b/Source/EbsdLib/LaueOps/HexagonalLowOps.cpp index 21c716a..0b1c038 100644 --- a/Source/EbsdLib/LaueOps/HexagonalLowOps.cpp +++ b/Source/EbsdLib/LaueOps/HexagonalLowOps.cpp @@ -1434,8 +1434,26 @@ ebsdlib::UInt8ArrayType::Pointer CreateIPFLegend(const HexagonalLowOps* ops, int } // ----------------------------------------------------------------------------- -void DrawFullCircleAnnotations(canvas_ity::canvas& context, int canvasDim, float fontPtSize, std::vector margins, std::array figureOrigin, std::array figureCenter, - bool drawFullCircle) +} // namespace + +// ----------------------------------------------------------------------------- +std::array HexagonalLowOps::adjustFigureOrigin( + std::array figureOrigin, + int legendWidth, int legendHeight, + const std::vector& margins, float fontPtSize, + bool generateEntirePlane) const +{ + if(!generateEntirePlane) + { + figureOrigin[0] = -(legendWidth / 2) * 0.25F; + figureOrigin[1] = margins[0]; + } + return figureOrigin; +} + +// ----------------------------------------------------------------------------- +void HexagonalLowOps::drawIPFAnnotations(canvas_ity::canvas& context, int canvasDim, float fontPtSize, const std::vector& margins, std::array figureOrigin, + std::array figureCenter, bool drawFullCircle) const { int legendHeight = canvasDim - margins[0] - margins[2]; int legendWidth = canvasDim - margins[1] - margins[3]; @@ -1530,21 +1548,19 @@ void DrawFullCircleAnnotations(canvas_ity::canvas& context, int canvasDim, float } } -} // namespace - // ----------------------------------------------------------------------------- ebsdlib::UInt8ArrayType::Pointer HexagonalLowOps::generateIPFTriangleLegend(int canvasDim, bool generateEntirePlane) const { - // Figure out the Legend Pixel Size + // Compute legend dimensions (same formula as annotateIPFImage uses) const float fontPtSize = static_cast(canvasDim) / 24.0f; - const std::vector margins = {fontPtSize * 3, // Top - static_cast(canvasDim / 7.0f), // Right - fontPtSize * 2, // Bottom - static_cast(canvasDim / 7.0f)}; // Left - - int legendHeight = canvasDim - margins[0] - margins[2]; - int legendWidth = canvasDim - margins[1] - margins[3]; - + const std::vector margins = { + fontPtSize * 3, + static_cast(canvasDim / 7.0f), + fontPtSize * 2, + static_cast(canvasDim / 7.0f) + }; + int legendHeight = canvasDim - static_cast(margins[0]) - static_cast(margins[2]); + int legendWidth = canvasDim - static_cast(margins[1]) - static_cast(margins[3]); if(legendHeight > legendWidth) { legendHeight = legendWidth; @@ -1553,64 +1569,12 @@ ebsdlib::UInt8ArrayType::Pointer HexagonalLowOps::generateIPFTriangleLegend(int { legendWidth = legendHeight; } - int pageHeight = canvasDim; - int pageWidth = canvasDim; - int halfWidth = legendWidth / 2; - int halfHeight = legendHeight / 2; - - std::array figureOrigin = {margins[3], margins[0] * 1.33F}; - if(!generateEntirePlane) - { - figureOrigin[0] = 0.0F - halfWidth * 0.25F; - figureOrigin[1] = 0.0F + margins[0]; - } - std::array figureCenter = {figureOrigin[0] + halfWidth, figureOrigin[1] + halfHeight}; + // Generate the colored SST triangle image (ARGB) ebsdlib::UInt8ArrayType::Pointer image = CreateIPFLegend(this, legendHeight, generateEntirePlane); - // Create a Canvas to draw into - canvas_ity::canvas context(pageWidth, pageHeight); - - std::vector latoBold = ebsdlib::fonts::GetLatoBold(); - std::vector latoRegular = ebsdlib::fonts::GetLatoRegular(); - context.set_font(latoBold.data(), static_cast(latoBold.size()), fontPtSize); - context.set_color(canvas_ity::fill_style, 0.0f, 0.0f, 0.0f, 1.0f); - canvas_ity::baseline_style const baselines[] = {canvas_ity::alphabetic, canvas_ity::top, canvas_ity::middle, canvas_ity::bottom, canvas_ity::hanging, canvas_ity::ideographic}; - context.text_baseline = baselines[0]; - - // Fill the whole background with white - context.move_to(0.0f, 0.0f); - context.line_to(static_cast(pageWidth), 0.0f); - context.line_to(static_cast(pageWidth), static_cast(pageHeight)); - context.line_to(0.0f, static_cast(pageHeight)); - context.line_to(0.0f, 0.0f); - context.close_path(); - context.set_color(canvas_ity::fill_style, 1.0f, 1.0f, 1.0f, 1.0f); - context.fill(); - - // Convert from ARGB to RGBA which is what canvas_itk wants - image = ebsdlib::ConvertColorOrder(image.get(), legendHeight); - - // We need to mirror across the X Axis because the image was drawn with +Y pointing down - image = ebsdlib::MirrorImage(image.get(), legendHeight); - - context.draw_image(image->getPointer(0), legendWidth, legendHeight, legendWidth * image->getNumberOfComponents(), figureOrigin[0], figureOrigin[1], static_cast(legendWidth), - static_cast(legendHeight)); - - // Draw Title of Legend - context.set_font(latoBold.data(), static_cast(latoBold.size()), fontPtSize * 1.5); - ebsdlib::WriteText(context, getSymmetryName(), {margins[0], static_cast(fontPtSize * 1.5)}, fontPtSize * 1.5); - - context.set_font(latoRegular.data(), static_cast(latoRegular.size()), fontPtSize); - DrawFullCircleAnnotations(context, canvasDim, fontPtSize, margins, figureOrigin, figureCenter, generateEntirePlane); - - // Fetch the rendered RGBA pixels from the entire canvas. - ebsdlib::UInt8ArrayType::Pointer rgbaCanvasImage = ebsdlib::UInt8ArrayType::CreateArray(pageHeight * pageWidth, {4ULL}, "Triangle Legend", true); - // std::vector rgbaCanvasImage(static_cast(pageHeight * pageWidth * 4)); - context.get_image_data(rgbaCanvasImage->getPointer(0), pageWidth, pageHeight, pageWidth * 4, 0, 0); - - rgbaCanvasImage = ebsdlib::RemoveAlphaChannel(rgbaCanvasImage.get()); - return rgbaCanvasImage; + // Annotate with title and Miller index labels + return annotateIPFImage(image, legendHeight, canvasDim, getSymmetryName(), generateEntirePlane); } // ----------------------------------------------------------------------------- diff --git a/Source/EbsdLib/LaueOps/HexagonalLowOps.h b/Source/EbsdLib/LaueOps/HexagonalLowOps.h index 8a54952..3755e78 100644 --- a/Source/EbsdLib/LaueOps/HexagonalLowOps.h +++ b/Source/EbsdLib/LaueOps/HexagonalLowOps.h @@ -260,6 +260,18 @@ class EbsdLib_EXPORT HexagonalLowOps : public LaueOps */ ebsdlib::UInt8ArrayType::Pointer generateIPFTriangleLegend(int imageDim, bool generateEntirePlane) const override; + void drawIPFAnnotations(canvas_ity::canvas& context, int canvasDim, + float fontPtSize, const std::vector& margins, + std::array figureOrigin, + std::array figureCenter, + bool drawFullCircle) const override; + + std::array adjustFigureOrigin( + std::array figureOrigin, + int legendWidth, int legendHeight, + const std::vector& margins, float fontPtSize, + bool generateEntirePlane) const override; + /** * @brief Returns if the given Quaternion is within the Rodrigues Fundamental Zone (RFZ) * @param quat Input Quaternion diff --git a/Source/EbsdLib/LaueOps/HexagonalOps.cpp b/Source/EbsdLib/LaueOps/HexagonalOps.cpp index 31095e4..55cae49 100644 --- a/Source/EbsdLib/LaueOps/HexagonalOps.cpp +++ b/Source/EbsdLib/LaueOps/HexagonalOps.cpp @@ -1488,8 +1488,26 @@ ebsdlib::UInt8ArrayType::Pointer CreateIPFLegend(const HexagonalOps* ops, int im } // ----------------------------------------------------------------------------- -void DrawFullCircleAnnotations(canvas_ity::canvas& context, int canvasDim, float fontPtSize, std::vector margins, std::array figureOrigin, std::array figureCenter, - bool drawFullCircle) +} // namespace + +// ----------------------------------------------------------------------------- +std::array HexagonalOps::adjustFigureOrigin( + std::array figureOrigin, + int legendWidth, int legendHeight, + const std::vector& margins, float fontPtSize, + bool generateEntirePlane) const +{ + if(!generateEntirePlane) + { + figureOrigin[0] = -margins[3] * 0.5F; + figureOrigin[1] = -(legendHeight / 2) + margins[0] + fontPtSize; + } + return figureOrigin; +} + +// ----------------------------------------------------------------------------- +void HexagonalOps::drawIPFAnnotations(canvas_ity::canvas& context, int canvasDim, float fontPtSize, const std::vector& margins, std::array figureOrigin, + std::array figureCenter, bool drawFullCircle) const { int legendHeight = canvasDim - margins[0] - margins[2]; int legendWidth = canvasDim - margins[1] - margins[3]; @@ -1574,20 +1592,19 @@ void DrawFullCircleAnnotations(canvas_ity::canvas& context, int canvasDim, float } } -} // namespace // ----------------------------------------------------------------------------- ebsdlib::UInt8ArrayType::Pointer HexagonalOps::generateIPFTriangleLegend(int canvasDim, bool generateEntirePlane) const { - // Figure out the Legend Pixel Size + // Compute legend dimensions (same formula as annotateIPFImage uses) const float fontPtSize = static_cast(canvasDim) / 24.0f; - const std::vector margins = {fontPtSize * 3, // Top - static_cast(canvasDim / 7.0f), // Right - fontPtSize * 2, // Bottom - static_cast(canvasDim / 7.0f)}; // Left - - int legendHeight = canvasDim - margins[0] - margins[2]; - int legendWidth = canvasDim - margins[1] - margins[3]; - + const std::vector margins = { + fontPtSize * 3, + static_cast(canvasDim / 7.0f), + fontPtSize * 2, + static_cast(canvasDim / 7.0f) + }; + int legendHeight = canvasDim - static_cast(margins[0]) - static_cast(margins[2]); + int legendWidth = canvasDim - static_cast(margins[1]) - static_cast(margins[3]); if(legendHeight > legendWidth) { legendHeight = legendWidth; @@ -1596,64 +1613,12 @@ ebsdlib::UInt8ArrayType::Pointer HexagonalOps::generateIPFTriangleLegend(int can { legendWidth = legendHeight; } - int pageHeight = canvasDim; - int pageWidth = canvasDim; - int halfWidth = legendWidth / 2; - int halfHeight = legendHeight / 2; - - std::array figureOrigin = {margins[3], margins[0] * 1.33F}; - if(!generateEntirePlane) - { - figureOrigin[0] = 0.0 - margins[3] * 0.5F; // -halfWidth * 0.45F ; - figureOrigin[1] = 0.0F - halfHeight + margins[0] + fontPtSize; - } - std::array figureCenter = {figureOrigin[0] + halfWidth, figureOrigin[1] + halfHeight}; + // Generate the colored SST triangle image (ARGB) ebsdlib::UInt8ArrayType::Pointer image = CreateIPFLegend(this, legendHeight, generateEntirePlane); - // Create a Canvas to draw into - canvas_ity::canvas context(pageWidth, pageHeight); - - std::vector latoBold = ebsdlib::fonts::GetLatoBold(); - std::vector latoRegular = ebsdlib::fonts::GetLatoRegular(); - context.set_font(latoBold.data(), static_cast(latoBold.size()), fontPtSize); - context.set_color(canvas_ity::fill_style, 0.0f, 0.0f, 0.0f, 1.0f); - canvas_ity::baseline_style const baselines[] = {canvas_ity::alphabetic, canvas_ity::top, canvas_ity::middle, canvas_ity::bottom, canvas_ity::hanging, canvas_ity::ideographic}; - context.text_baseline = baselines[0]; - - // Fill the whole background with white - context.move_to(0.0f, 0.0f); - context.line_to(static_cast(pageWidth), 0.0f); - context.line_to(static_cast(pageWidth), static_cast(pageHeight)); - context.line_to(0.0f, static_cast(pageHeight)); - context.line_to(0.0f, 0.0f); - context.close_path(); - context.set_color(canvas_ity::fill_style, 1.0f, 1.0f, 1.0f, 1.0f); - context.fill(); - - // Convert from ARGB to RGBA which is what canvas_itk wants - image = ebsdlib::ConvertColorOrder(image.get(), legendHeight); - - // We need to mirror across the X Axis because the image was drawn with +Y pointing down - image = ebsdlib::MirrorImage(image.get(), legendHeight); - - context.draw_image(image->getPointer(0), legendWidth, legendHeight, legendWidth * image->getNumberOfComponents(), figureOrigin[0], figureOrigin[1], static_cast(legendWidth), - static_cast(legendHeight)); - - // Draw Title of Legend - context.set_font(latoBold.data(), static_cast(latoBold.size()), fontPtSize * 1.5); - ebsdlib::WriteText(context, getSymmetryName(), {margins[0], static_cast(fontPtSize * 1.5)}, fontPtSize * 1.5); - - context.set_font(latoRegular.data(), static_cast(latoRegular.size()), fontPtSize); - DrawFullCircleAnnotations(context, canvasDim, fontPtSize, margins, figureOrigin, figureCenter, generateEntirePlane); - - // Fetch the rendered RGBA pixels from the entire canvas. - ebsdlib::UInt8ArrayType::Pointer rgbaCanvasImage = ebsdlib::UInt8ArrayType::CreateArray(pageHeight * pageWidth, {4ULL}, "Triangle Legend", true); - // std::vector rgbaCanvasImage(static_cast(pageHeight * pageWidth * 4)); - context.get_image_data(rgbaCanvasImage->getPointer(0), pageWidth, pageHeight, pageWidth * 4, 0, 0); - - rgbaCanvasImage = ebsdlib::RemoveAlphaChannel(rgbaCanvasImage.get()); - return rgbaCanvasImage; + // Annotate with title and Miller index labels + return annotateIPFImage(image, legendHeight, canvasDim, getSymmetryName(), generateEntirePlane); } // ----------------------------------------------------------------------------- diff --git a/Source/EbsdLib/LaueOps/HexagonalOps.h b/Source/EbsdLib/LaueOps/HexagonalOps.h index 6b52e38..862ce1f 100644 --- a/Source/EbsdLib/LaueOps/HexagonalOps.h +++ b/Source/EbsdLib/LaueOps/HexagonalOps.h @@ -260,6 +260,18 @@ class EbsdLib_EXPORT HexagonalOps : public LaueOps */ ebsdlib::UInt8ArrayType::Pointer generateIPFTriangleLegend(int imageDim, bool generateEntirePlane) const override; + void drawIPFAnnotations(canvas_ity::canvas& context, int canvasDim, + float fontPtSize, const std::vector& margins, + std::array figureOrigin, + std::array figureCenter, + bool drawFullCircle) const override; + + std::array adjustFigureOrigin( + std::array figureOrigin, + int legendWidth, int legendHeight, + const std::vector& margins, float fontPtSize, + bool generateEntirePlane) const override; + /** * @brief Returns if the given Quaternion is within the Rodrigues Fundamental Zone (RFZ) * @param quat Input Quaternion diff --git a/Source/EbsdLib/LaueOps/MonoclinicOps.cpp b/Source/EbsdLib/LaueOps/MonoclinicOps.cpp index 4362272..acf62d2 100644 --- a/Source/EbsdLib/LaueOps/MonoclinicOps.cpp +++ b/Source/EbsdLib/LaueOps/MonoclinicOps.cpp @@ -792,8 +792,12 @@ ebsdlib::UInt8ArrayType::Pointer CreateIPFLegend(const MonoclinicOps* ops, int i } // ----------------------------------------------------------------------------- -void DrawFullCircleAnnotations(canvas_ity::canvas& context, int canvasDim, float fontPtSize, std::vector margins, std::array figureOrigin, std::array figureCenter, - bool drawFullCircle) +} // namespace + +// ----------------------------------------------------------------------------- +void MonoclinicOps::drawIPFAnnotations(canvas_ity::canvas& context, int canvasDim, float fontPtSize, const std::vector& margins, std::array figureOrigin, + std::array figureCenter, + bool drawFullCircle) const { int legendHeight = canvasDim - margins[0] - margins[2]; int legendWidth = canvasDim - margins[1] - margins[3]; @@ -904,21 +908,19 @@ void DrawFullCircleAnnotations(canvas_ity::canvas& context, int canvasDim, float } } -} // namespace - // ----------------------------------------------------------------------------- ebsdlib::UInt8ArrayType::Pointer MonoclinicOps::generateIPFTriangleLegend(int canvasDim, bool generateEntirePlane) const { - // Figure out the Legend Pixel Size + // Compute legend dimensions (same formula as annotateIPFImage uses) const float fontPtSize = static_cast(canvasDim) / 24.0f; - const std::vector margins = {fontPtSize * 3, // Top - static_cast(canvasDim / 7.0f), // Right - fontPtSize * 2, // Bottom - static_cast(canvasDim / 7.0f)}; // Left - - int legendHeight = canvasDim - margins[0] - margins[2]; - int legendWidth = canvasDim - margins[1] - margins[3]; - + const std::vector margins = { + fontPtSize * 3, + static_cast(canvasDim / 7.0f), + fontPtSize * 2, + static_cast(canvasDim / 7.0f) + }; + int legendHeight = canvasDim - static_cast(margins[0]) - static_cast(margins[2]); + int legendWidth = canvasDim - static_cast(margins[1]) - static_cast(margins[3]); if(legendHeight > legendWidth) { legendHeight = legendWidth; @@ -927,67 +929,12 @@ ebsdlib::UInt8ArrayType::Pointer MonoclinicOps::generateIPFTriangleLegend(int ca { legendWidth = legendHeight; } - int pageHeight = canvasDim; - int pageWidth = canvasDim; - int halfWidth = legendWidth / 2; - int halfHeight = legendHeight / 2; - std::array figureOrigin = {margins[3], margins[0] * 1.33F}; - // if(!generateEntirePlane) - // { - // figureOrigin[1] = 0.0F - legendHeight * 0.15F; - // } - std::array figureCenter = {figureOrigin[0] + halfWidth, figureOrigin[1] + halfHeight}; - - // Create the actual Legend which will come back as ARGB values + // Generate the colored SST triangle image (ARGB) ebsdlib::UInt8ArrayType::Pointer image = CreateIPFLegend(this, legendHeight, generateEntirePlane); - // Convert from ARGB to RGBA which is what canvas_itk wants - image = ebsdlib::ConvertColorOrder(image.get(), legendHeight); - - // We need to mirror across the X Axis because the image was drawn with +Y pointing down - image = ebsdlib::MirrorImage(image.get(), legendHeight); - - // Create a 2D Canvas to draw into now that the Legend is in the proper form - canvas_ity::canvas context(pageWidth, pageHeight); - - std::vector latoBold = ebsdlib::fonts::GetLatoBold(); - std::vector latoRegular = ebsdlib::fonts::GetLatoRegular(); - context.set_font(latoBold.data(), static_cast(latoBold.size()), fontPtSize); - context.set_color(canvas_ity::fill_style, 0.0f, 0.0f, 0.0f, 1.0f); - canvas_ity::baseline_style const baselines[] = {canvas_ity::alphabetic, canvas_ity::top, canvas_ity::middle, canvas_ity::bottom, canvas_ity::hanging, canvas_ity::ideographic}; - context.text_baseline = baselines[0]; - - // Fill the whole background with white - context.move_to(0.0f, 0.0f); - context.line_to(static_cast(pageWidth), 0.0f); - context.line_to(static_cast(pageWidth), static_cast(pageHeight)); - context.line_to(0.0f, static_cast(pageHeight)); - context.line_to(0.0f, 0.0f); - context.close_path(); - context.set_color(canvas_ity::fill_style, 1.0f, 1.0f, 1.0f, 1.0f); - context.fill(); - - // Draw the legend image onto the canvas at the correct spot. - context.draw_image(image->getPointer(0), legendWidth, legendHeight, legendWidth * image->getNumberOfComponents(), figureOrigin[0], figureOrigin[1], static_cast(legendWidth), - static_cast(legendHeight)); - - // Draw Title of Legend - context.set_font(latoBold.data(), static_cast(latoBold.size()), fontPtSize * 1.5); - ebsdlib::WriteText(context, getSymmetryName(), {margins[0], static_cast(fontPtSize * 1.5)}, fontPtSize * 1.5); - - context.set_font(latoRegular.data(), static_cast(latoRegular.size()), fontPtSize); - DrawFullCircleAnnotations(context, canvasDim, fontPtSize, margins, figureOrigin, figureCenter, generateEntirePlane); - - // Fetch the rendered RGBA pixels from the entire canvas. - ebsdlib::UInt8ArrayType::Pointer rgbaCanvasImage = ebsdlib::UInt8ArrayType::CreateArray(pageHeight * pageWidth, {4ULL}, "Triangle Legend", true); - // std::vector rgbaCanvasImage(static_cast(pageHeight * pageWidth * 4)); - context.get_image_data(rgbaCanvasImage->getPointer(0), pageWidth, pageHeight, pageWidth * 4, 0, 0); - - // Remove the Alpha channel from the final image - rgbaCanvasImage = ebsdlib::RemoveAlphaChannel(rgbaCanvasImage.get()); - - return rgbaCanvasImage; + // Annotate with title and Miller index labels + return annotateIPFImage(image, legendHeight, canvasDim, getSymmetryName(), generateEntirePlane); } // ----------------------------------------------------------------------------- diff --git a/Source/EbsdLib/LaueOps/MonoclinicOps.h b/Source/EbsdLib/LaueOps/MonoclinicOps.h index 2fb3942..42d47b2 100644 --- a/Source/EbsdLib/LaueOps/MonoclinicOps.h +++ b/Source/EbsdLib/LaueOps/MonoclinicOps.h @@ -259,6 +259,12 @@ class EbsdLib_EXPORT MonoclinicOps : public LaueOps */ ebsdlib::UInt8ArrayType::Pointer generateIPFTriangleLegend(int imageDim, bool generateEntirePlane) const override; + void drawIPFAnnotations(canvas_ity::canvas& context, int canvasDim, + float fontPtSize, const std::vector& margins, + std::array figureOrigin, + std::array figureCenter, + bool drawFullCircle) const override; + /** * @brief Returns if the given Quaternion is within the Rodrigues Fundamental Zone (RFZ) * @param quat Input Quaternion diff --git a/Source/EbsdLib/LaueOps/OrthoRhombicOps.cpp b/Source/EbsdLib/LaueOps/OrthoRhombicOps.cpp index 58a4974..cee44b5 100644 --- a/Source/EbsdLib/LaueOps/OrthoRhombicOps.cpp +++ b/Source/EbsdLib/LaueOps/OrthoRhombicOps.cpp @@ -803,9 +803,26 @@ ebsdlib::UInt8ArrayType::Pointer CreateIPFLegend(const OrthoRhombicOps* ops, int return image; } +} // namespace + +// ----------------------------------------------------------------------------- +std::array OrthoRhombicOps::adjustFigureOrigin( + std::array figureOrigin, + int legendWidth, int legendHeight, + const std::vector& margins, float fontPtSize, + bool generateEntirePlane) const +{ + if(!generateEntirePlane) + { + figureOrigin[0] = -margins[3]; + } + return figureOrigin; +} + // ----------------------------------------------------------------------------- -void DrawFullCircleAnnotations(canvas_ity::canvas& context, int canvasDim, float fontPtSize, std::vector margins, std::array figureOrigin, std::array figureCenter, - bool drawFullCircle) +void OrthoRhombicOps::drawIPFAnnotations(canvas_ity::canvas& context, int canvasDim, float fontPtSize, const std::vector& margins, std::array figureOrigin, + std::array figureCenter, + bool drawFullCircle) const { int legendHeight = canvasDim - margins[0] - margins[2]; int legendWidth = canvasDim - margins[1] - margins[3]; @@ -890,20 +907,19 @@ void DrawFullCircleAnnotations(canvas_ity::canvas& context, int canvasDim, float } } -} // namespace // ----------------------------------------------------------------------------- ebsdlib::UInt8ArrayType::Pointer OrthoRhombicOps::generateIPFTriangleLegend(int canvasDim, bool generateEntirePlane) const { - // Figure out the Legend Pixel Size + // Compute legend dimensions (same formula as annotateIPFImage uses) const float fontPtSize = static_cast(canvasDim) / 24.0f; - const std::vector margins = {fontPtSize * 3, // Top - static_cast(canvasDim / 7.0f), // Right - fontPtSize * 2, // Bottom - static_cast(canvasDim / 7.0f)}; // Left - - int legendHeight = canvasDim - margins[0] - margins[2]; - int legendWidth = canvasDim - margins[1] - margins[3]; - + const std::vector margins = { + fontPtSize * 3, + static_cast(canvasDim / 7.0f), + fontPtSize * 2, + static_cast(canvasDim / 7.0f) + }; + int legendHeight = canvasDim - static_cast(margins[0]) - static_cast(margins[2]); + int legendWidth = canvasDim - static_cast(margins[1]) - static_cast(margins[3]); if(legendHeight > legendWidth) { legendHeight = legendWidth; @@ -912,68 +928,12 @@ ebsdlib::UInt8ArrayType::Pointer OrthoRhombicOps::generateIPFTriangleLegend(int { legendWidth = legendHeight; } - int pageHeight = canvasDim; - int pageWidth = canvasDim; - int halfWidth = legendWidth / 2; - int halfHeight = legendHeight / 2; - std::array figureOrigin = {margins[3], margins[0] * 1.33F}; - if(!generateEntirePlane) - { - figureOrigin[0] = -margins[3]; - // figureOrigin[1] = 0.0F - legendHeight * 0.15F; - } - std::array figureCenter = {figureOrigin[0] + halfWidth, figureOrigin[1] + halfHeight}; - - // Create the actual Legend which will come back as ARGB values + // Generate the colored SST triangle image (ARGB) ebsdlib::UInt8ArrayType::Pointer image = CreateIPFLegend(this, legendHeight, generateEntirePlane); - // Convert from ARGB to RGBA which is what canvas_itk wants - image = ebsdlib::ConvertColorOrder(image.get(), legendHeight); - - // We need to mirror across the X Axis because the image was drawn with +Y pointing down - image = ebsdlib::MirrorImage(image.get(), legendHeight); - - // Create a 2D Canvas to draw into now that the Legend is in the proper form - canvas_ity::canvas context(pageWidth, pageHeight); - - std::vector latoBold = ebsdlib::fonts::GetLatoBold(); - std::vector latoRegular = ebsdlib::fonts::GetLatoRegular(); - context.set_font(latoBold.data(), static_cast(latoBold.size()), fontPtSize); - context.set_color(canvas_ity::fill_style, 0.0f, 0.0f, 0.0f, 1.0f); - canvas_ity::baseline_style const baselines[] = {canvas_ity::alphabetic, canvas_ity::top, canvas_ity::middle, canvas_ity::bottom, canvas_ity::hanging, canvas_ity::ideographic}; - context.text_baseline = baselines[0]; - - // Fill the whole background with white - context.move_to(0.0f, 0.0f); - context.line_to(static_cast(pageWidth), 0.0f); - context.line_to(static_cast(pageWidth), static_cast(pageHeight)); - context.line_to(0.0f, static_cast(pageHeight)); - context.line_to(0.0f, 0.0f); - context.close_path(); - context.set_color(canvas_ity::fill_style, 1.0f, 1.0f, 1.0f, 1.0f); - context.fill(); - - // Draw the legend image onto the canvas at the correct spot. - context.draw_image(image->getPointer(0), legendWidth, legendHeight, legendWidth * image->getNumberOfComponents(), figureOrigin[0], figureOrigin[1], static_cast(legendWidth), - static_cast(legendHeight)); - - // Draw Title of Legend - context.set_font(latoBold.data(), static_cast(latoBold.size()), fontPtSize * 1.5); - ebsdlib::WriteText(context, getSymmetryName(), {margins[0], static_cast(fontPtSize * 1.5)}, fontPtSize * 1.5); - - context.set_font(latoRegular.data(), static_cast(latoRegular.size()), fontPtSize); - DrawFullCircleAnnotations(context, canvasDim, fontPtSize, margins, figureOrigin, figureCenter, generateEntirePlane); - - // Fetch the rendered RGBA pixels from the entire canvas. - ebsdlib::UInt8ArrayType::Pointer rgbaCanvasImage = ebsdlib::UInt8ArrayType::CreateArray(pageHeight * pageWidth, {4ULL}, "Triangle Legend", true); - // std::vector rgbaCanvasImage(static_cast(pageHeight * pageWidth * 4)); - context.get_image_data(rgbaCanvasImage->getPointer(0), pageWidth, pageHeight, pageWidth * 4, 0, 0); - - // Remove the Alpha channel from the final image - rgbaCanvasImage = ebsdlib::RemoveAlphaChannel(rgbaCanvasImage.get()); - - return rgbaCanvasImage; + // Annotate with title and Miller index labels + return annotateIPFImage(image, legendHeight, canvasDim, getSymmetryName(), generateEntirePlane); } // ----------------------------------------------------------------------------- diff --git a/Source/EbsdLib/LaueOps/OrthoRhombicOps.h b/Source/EbsdLib/LaueOps/OrthoRhombicOps.h index 16c37af..c454a9d 100644 --- a/Source/EbsdLib/LaueOps/OrthoRhombicOps.h +++ b/Source/EbsdLib/LaueOps/OrthoRhombicOps.h @@ -262,6 +262,18 @@ class EbsdLib_EXPORT OrthoRhombicOps : public LaueOps */ ebsdlib::UInt8ArrayType::Pointer generateIPFTriangleLegend(int canvasDim, bool generateEntirePlane) const override; + void drawIPFAnnotations(canvas_ity::canvas& context, int canvasDim, + float fontPtSize, const std::vector& margins, + std::array figureOrigin, + std::array figureCenter, + bool drawFullCircle) const override; + + std::array adjustFigureOrigin( + std::array figureOrigin, + int legendWidth, int legendHeight, + const std::vector& margins, float fontPtSize, + bool generateEntirePlane) const override; + /** * @brief Returns if the given Quaternion is within the Rodrigues Fundamental Zone (RFZ) * @param quat Input Quaternion diff --git a/Source/EbsdLib/LaueOps/TetragonalLowOps.cpp b/Source/EbsdLib/LaueOps/TetragonalLowOps.cpp index ec5629c..cebcd75 100644 --- a/Source/EbsdLib/LaueOps/TetragonalLowOps.cpp +++ b/Source/EbsdLib/LaueOps/TetragonalLowOps.cpp @@ -805,9 +805,26 @@ ebsdlib::UInt8ArrayType::Pointer CreateIPFLegend(const TetragonalLowOps* ops, in return image; } +} // namespace + +// ----------------------------------------------------------------------------- +std::array TetragonalLowOps::adjustFigureOrigin( + std::array figureOrigin, + int legendWidth, int legendHeight, + const std::vector& margins, float fontPtSize, + bool generateEntirePlane) const +{ + if(!generateEntirePlane) + { + figureOrigin[0] = -margins[3]; + } + return figureOrigin; +} + // ----------------------------------------------------------------------------- -void DrawFullCircleAnnotations(canvas_ity::canvas& context, int canvasDim, float fontPtSize, std::vector margins, std::array figureOrigin, std::array figureCenter, - bool drawFullCircle) +void TetragonalLowOps::drawIPFAnnotations(canvas_ity::canvas& context, int canvasDim, float fontPtSize, const std::vector& margins, std::array figureOrigin, + std::array figureCenter, + bool drawFullCircle) const { int legendHeight = canvasDim - margins[0] - margins[2]; int legendWidth = canvasDim - margins[1] - margins[3]; @@ -918,21 +935,19 @@ void DrawFullCircleAnnotations(canvas_ity::canvas& context, int canvasDim, float } } -} // namespace - // ----------------------------------------------------------------------------- ebsdlib::UInt8ArrayType::Pointer TetragonalLowOps::generateIPFTriangleLegend(int canvasDim, bool generateEntirePlane) const { - // Figure out the Legend Pixel Size + // Compute legend dimensions (same formula as annotateIPFImage uses) const float fontPtSize = static_cast(canvasDim) / 24.0f; - const std::vector margins = {fontPtSize * 3, // Top - static_cast(canvasDim / 7.0f), // Right - fontPtSize * 2, // Bottom - static_cast(canvasDim / 7.0f)}; // Left - - int legendHeight = canvasDim - margins[0] - margins[2]; - int legendWidth = canvasDim - margins[1] - margins[3]; - + const std::vector margins = { + fontPtSize * 3, + static_cast(canvasDim / 7.0f), + fontPtSize * 2, + static_cast(canvasDim / 7.0f) + }; + int legendHeight = canvasDim - static_cast(margins[0]) - static_cast(margins[2]); + int legendWidth = canvasDim - static_cast(margins[1]) - static_cast(margins[3]); if(legendHeight > legendWidth) { legendHeight = legendWidth; @@ -941,68 +956,12 @@ ebsdlib::UInt8ArrayType::Pointer TetragonalLowOps::generateIPFTriangleLegend(int { legendWidth = legendHeight; } - int pageHeight = canvasDim; - int pageWidth = canvasDim; - int halfWidth = legendWidth / 2; - int halfHeight = legendHeight / 2; - - std::array figureOrigin = {margins[3], margins[0] * 1.33F}; - if(!generateEntirePlane) - { - figureOrigin[0] = -margins[3]; - // figureOrigin[1] = 0.0F - legendHeight * 0.15F; - } - std::array figureCenter = {figureOrigin[0] + halfWidth, figureOrigin[1] + halfHeight}; - // Create the actual Legend which will come back as ARGB values + // Generate the colored SST triangle image (ARGB) ebsdlib::UInt8ArrayType::Pointer image = CreateIPFLegend(this, legendHeight, generateEntirePlane); - // Convert from ARGB to RGBA which is what canvas_itk wants - image = ebsdlib::ConvertColorOrder(image.get(), legendHeight); - - // We need to mirror across the X Axis because the image was drawn with +Y pointing down - image = ebsdlib::MirrorImage(image.get(), legendHeight); - - // Create a 2D Canvas to draw into now that the Legend is in the proper form - canvas_ity::canvas context(pageWidth, pageHeight); - - std::vector latoBold = ebsdlib::fonts::GetLatoBold(); - std::vector latoRegular = ebsdlib::fonts::GetLatoRegular(); - context.set_font(latoBold.data(), static_cast(latoBold.size()), fontPtSize); - context.set_color(canvas_ity::fill_style, 0.0f, 0.0f, 0.0f, 1.0f); - canvas_ity::baseline_style const baselines[] = {canvas_ity::alphabetic, canvas_ity::top, canvas_ity::middle, canvas_ity::bottom, canvas_ity::hanging, canvas_ity::ideographic}; - context.text_baseline = baselines[0]; - - // Fill the whole background with white - context.move_to(0.0f, 0.0f); - context.line_to(static_cast(pageWidth), 0.0f); - context.line_to(static_cast(pageWidth), static_cast(pageHeight)); - context.line_to(0.0f, static_cast(pageHeight)); - context.line_to(0.0f, 0.0f); - context.close_path(); - context.set_color(canvas_ity::fill_style, 1.0f, 1.0f, 1.0f, 1.0f); - context.fill(); - - // Draw the legend image onto the canvas at the correct spot. - context.draw_image(image->getPointer(0), legendWidth, legendHeight, legendWidth * image->getNumberOfComponents(), figureOrigin[0], figureOrigin[1], static_cast(legendWidth), - static_cast(legendHeight)); - - // Draw Title of Legend - context.set_font(latoBold.data(), static_cast(latoBold.size()), fontPtSize * 1.5); - ebsdlib::WriteText(context, getSymmetryName(), {margins[0], static_cast(fontPtSize * 1.5)}, fontPtSize * 1.5); - - context.set_font(latoRegular.data(), static_cast(latoRegular.size()), fontPtSize); - DrawFullCircleAnnotations(context, canvasDim, fontPtSize, margins, figureOrigin, figureCenter, generateEntirePlane); - - // Fetch the rendered RGBA pixels from the entire canvas. - ebsdlib::UInt8ArrayType::Pointer rgbaCanvasImage = ebsdlib::UInt8ArrayType::CreateArray(pageHeight * pageWidth, {4ULL}, "Triangle Legend", true); - // std::vector rgbaCanvasImage(static_cast(pageHeight * pageWidth * 4)); - context.get_image_data(rgbaCanvasImage->getPointer(0), pageWidth, pageHeight, pageWidth * 4, 0, 0); - - // Remove the Alpha channel from the final image - rgbaCanvasImage = ebsdlib::RemoveAlphaChannel(rgbaCanvasImage.get()); - - return rgbaCanvasImage; + // Annotate with title and Miller index labels + return annotateIPFImage(image, legendHeight, canvasDim, getSymmetryName(), generateEntirePlane); } // ----------------------------------------------------------------------------- diff --git a/Source/EbsdLib/LaueOps/TetragonalLowOps.h b/Source/EbsdLib/LaueOps/TetragonalLowOps.h index 371ba77..4c158a6 100644 --- a/Source/EbsdLib/LaueOps/TetragonalLowOps.h +++ b/Source/EbsdLib/LaueOps/TetragonalLowOps.h @@ -262,6 +262,18 @@ class EbsdLib_EXPORT TetragonalLowOps : public LaueOps */ ebsdlib::UInt8ArrayType::Pointer generateIPFTriangleLegend(int imageDim, bool generateEntirePlane) const override; + void drawIPFAnnotations(canvas_ity::canvas& context, int canvasDim, + float fontPtSize, const std::vector& margins, + std::array figureOrigin, + std::array figureCenter, + bool drawFullCircle) const override; + + std::array adjustFigureOrigin( + std::array figureOrigin, + int legendWidth, int legendHeight, + const std::vector& margins, float fontPtSize, + bool generateEntirePlane) const override; + /** * @brief Returns if the given Quaternion is within the Rodrigues Fundamental Zone (RFZ) * @param quat Input Quaternion diff --git a/Source/EbsdLib/LaueOps/TetragonalOps.cpp b/Source/EbsdLib/LaueOps/TetragonalOps.cpp index a0bf0eb..0aac5e4 100644 --- a/Source/EbsdLib/LaueOps/TetragonalOps.cpp +++ b/Source/EbsdLib/LaueOps/TetragonalOps.cpp @@ -848,9 +848,27 @@ ebsdlib::UInt8ArrayType::Pointer CreateIPFLegend(const TetragonalOps* ops, int i return image; } +} // namespace + // ----------------------------------------------------------------------------- -void DrawFullCircleAnnotations(canvas_ity::canvas& context, int canvasDim, float fontPtSize, std::vector margins, std::array figureOrigin, std::array figureCenter, - bool drawFullCircle) +std::array TetragonalOps::adjustFigureOrigin( + std::array figureOrigin, + int legendWidth, int legendHeight, + const std::vector& margins, float fontPtSize, + bool generateEntirePlane) const +{ + if(!generateEntirePlane) + { + figureOrigin[0] = -margins[2]; + figureOrigin[1] = fontPtSize * 2.0F; + } + return figureOrigin; +} + +// ----------------------------------------------------------------------------- +void TetragonalOps::drawIPFAnnotations(canvas_ity::canvas& context, int canvasDim, float fontPtSize, const std::vector& margins, std::array figureOrigin, + std::array figureCenter, + bool drawFullCircle) const { int legendHeight = canvasDim - margins[0] - margins[2]; int legendWidth = canvasDim - margins[1] - margins[3]; @@ -935,21 +953,19 @@ void DrawFullCircleAnnotations(canvas_ity::canvas& context, int canvasDim, float } } -} // namespace - // ----------------------------------------------------------------------------- ebsdlib::UInt8ArrayType::Pointer TetragonalOps::generateIPFTriangleLegend(int canvasDim, bool generateEntirePlane) const { - // Figure out the Legend Pixel Size + // Compute legend dimensions (same formula as annotateIPFImage uses) const float fontPtSize = static_cast(canvasDim) / 24.0f; - const std::vector margins = {fontPtSize * 3, // Top - static_cast(canvasDim / 7.0f), // Right - fontPtSize * 2, // Bottom - static_cast(canvasDim / 7.0f)}; // Left - - int legendHeight = canvasDim - margins[0] - margins[2]; - int legendWidth = canvasDim - margins[1] - margins[3]; - + const std::vector margins = { + fontPtSize * 3, + static_cast(canvasDim / 7.0f), + fontPtSize * 2, + static_cast(canvasDim / 7.0f) + }; + int legendHeight = canvasDim - static_cast(margins[0]) - static_cast(margins[2]); + int legendWidth = canvasDim - static_cast(margins[1]) - static_cast(margins[3]); if(legendHeight > legendWidth) { legendHeight = legendWidth; @@ -958,68 +974,12 @@ ebsdlib::UInt8ArrayType::Pointer TetragonalOps::generateIPFTriangleLegend(int ca { legendWidth = legendHeight; } - int pageHeight = canvasDim; - int pageWidth = canvasDim; - int halfWidth = legendWidth / 2; - int halfHeight = legendHeight / 2; - - std::array figureOrigin = {margins[3], margins[0] * 1.33F}; - if(!generateEntirePlane) - { - figureOrigin[0] = -margins[2]; - figureOrigin[1] = fontPtSize * 2.0F; - } - std::array figureCenter = {figureOrigin[0] + halfWidth, figureOrigin[1] + halfHeight}; - // Create the actual Legend which will come back as ARGB values + // Generate the colored SST triangle image (ARGB) ebsdlib::UInt8ArrayType::Pointer image = CreateIPFLegend(this, legendHeight, generateEntirePlane); - // Convert from ARGB to RGBA which is what canvas_itk wants - image = ebsdlib::ConvertColorOrder(image.get(), legendHeight); - - // We need to mirror across the X Axis because the image was drawn with +Y pointing down - image = ebsdlib::MirrorImage(image.get(), legendHeight); - - // Create a 2D Canvas to draw into now that the Legend is in the proper form - canvas_ity::canvas context(pageWidth, pageHeight); - - std::vector latoBold = ebsdlib::fonts::GetLatoBold(); - std::vector latoRegular = ebsdlib::fonts::GetLatoRegular(); - context.set_font(latoBold.data(), static_cast(latoBold.size()), fontPtSize); - context.set_color(canvas_ity::fill_style, 0.0f, 0.0f, 0.0f, 1.0f); - canvas_ity::baseline_style const baselines[] = {canvas_ity::alphabetic, canvas_ity::top, canvas_ity::middle, canvas_ity::bottom, canvas_ity::hanging, canvas_ity::ideographic}; - context.text_baseline = baselines[0]; - - // Fill the whole background with white - context.move_to(0.0f, 0.0f); - context.line_to(static_cast(pageWidth), 0.0f); - context.line_to(static_cast(pageWidth), static_cast(pageHeight)); - context.line_to(0.0f, static_cast(pageHeight)); - context.line_to(0.0f, 0.0f); - context.close_path(); - context.set_color(canvas_ity::fill_style, 1.0f, 1.0f, 1.0f, 1.0f); - context.fill(); - - // Draw the legend image onto the canvas at the correct spot. - context.draw_image(image->getPointer(0), legendWidth, legendHeight, legendWidth * image->getNumberOfComponents(), figureOrigin[0], figureOrigin[1], static_cast(legendWidth), - static_cast(legendHeight)); - - // Draw Title of Legend - context.set_font(latoBold.data(), static_cast(latoBold.size()), fontPtSize * 1.5); - ebsdlib::WriteText(context, getSymmetryName(), {margins[0], static_cast(fontPtSize * 1.5)}, fontPtSize * 1.5); - - context.set_font(latoRegular.data(), static_cast(latoRegular.size()), fontPtSize); - DrawFullCircleAnnotations(context, canvasDim, fontPtSize, margins, figureOrigin, figureCenter, generateEntirePlane); - - // Fetch the rendered RGBA pixels from the entire canvas. - ebsdlib::UInt8ArrayType::Pointer rgbaCanvasImage = ebsdlib::UInt8ArrayType::CreateArray(pageHeight * pageWidth, {4ULL}, "Triangle Legend", true); - // std::vector rgbaCanvasImage(static_cast(pageHeight * pageWidth * 4)); - context.get_image_data(rgbaCanvasImage->getPointer(0), pageWidth, pageHeight, pageWidth * 4, 0, 0); - - // Remove the Alpha channel from the final image - rgbaCanvasImage = ebsdlib::RemoveAlphaChannel(rgbaCanvasImage.get()); - - return rgbaCanvasImage; + // Annotate with title and Miller index labels + return annotateIPFImage(image, legendHeight, canvasDim, getSymmetryName(), generateEntirePlane); } // ----------------------------------------------------------------------------- diff --git a/Source/EbsdLib/LaueOps/TetragonalOps.h b/Source/EbsdLib/LaueOps/TetragonalOps.h index fde56af..b84baeb 100644 --- a/Source/EbsdLib/LaueOps/TetragonalOps.h +++ b/Source/EbsdLib/LaueOps/TetragonalOps.h @@ -262,6 +262,18 @@ class EbsdLib_EXPORT TetragonalOps : public LaueOps */ ebsdlib::UInt8ArrayType::Pointer generateIPFTriangleLegend(int imageDim, bool generateEntirePlane) const override; + void drawIPFAnnotations(canvas_ity::canvas& context, int canvasDim, + float fontPtSize, const std::vector& margins, + std::array figureOrigin, + std::array figureCenter, + bool drawFullCircle) const override; + + std::array adjustFigureOrigin( + std::array figureOrigin, + int legendWidth, int legendHeight, + const std::vector& margins, float fontPtSize, + bool generateEntirePlane) const override; + /** * @brief Returns if the given Quaternion is within the Rodrigues Fundamental Zone (RFZ) * @param quat Input Quaternion diff --git a/Source/EbsdLib/LaueOps/TriclinicOps.cpp b/Source/EbsdLib/LaueOps/TriclinicOps.cpp index a423061..42b1925 100644 --- a/Source/EbsdLib/LaueOps/TriclinicOps.cpp +++ b/Source/EbsdLib/LaueOps/TriclinicOps.cpp @@ -786,8 +786,12 @@ ebsdlib::UInt8ArrayType::Pointer CreateIPFLegend(const TriclinicOps* ops, int im } // ----------------------------------------------------------------------------- -void DrawFullCircleAnnotations(canvas_ity::canvas& context, int canvasDim, float fontPtSize, std::vector margins, std::array figureOrigin, std::array figureCenter, - bool drawFullCircle) +} // namespace + +// ----------------------------------------------------------------------------- +void TriclinicOps::drawIPFAnnotations(canvas_ity::canvas& context, int canvasDim, float fontPtSize, const std::vector& margins, std::array figureOrigin, + std::array figureCenter, + bool drawFullCircle) const { int legendHeight = canvasDim - margins[0] - margins[2]; int legendWidth = canvasDim - margins[1] - margins[3]; @@ -887,20 +891,19 @@ void DrawFullCircleAnnotations(canvas_ity::canvas& context, int canvasDim, float } } -} // namespace // ----------------------------------------------------------------------------- ebsdlib::UInt8ArrayType::Pointer TriclinicOps::generateIPFTriangleLegend(int canvasDim, bool generateEntirePlane) const { - // Figure out the Legend Pixel Size + // Compute legend dimensions (same formula as annotateIPFImage uses) const float fontPtSize = static_cast(canvasDim) / 24.0f; - const std::vector margins = {fontPtSize * 3, // Top - static_cast(canvasDim / 7.0f), // Right - fontPtSize * 2, // Bottom - static_cast(canvasDim / 7.0f)}; // Left - - int legendHeight = canvasDim - margins[0] - margins[2]; - int legendWidth = canvasDim - margins[1] - margins[3]; - + const std::vector margins = { + fontPtSize * 3, + static_cast(canvasDim / 7.0f), + fontPtSize * 2, + static_cast(canvasDim / 7.0f) + }; + int legendHeight = canvasDim - static_cast(margins[0]) - static_cast(margins[2]); + int legendWidth = canvasDim - static_cast(margins[1]) - static_cast(margins[3]); if(legendHeight > legendWidth) { legendHeight = legendWidth; @@ -909,67 +912,12 @@ ebsdlib::UInt8ArrayType::Pointer TriclinicOps::generateIPFTriangleLegend(int can { legendWidth = legendHeight; } - int pageHeight = canvasDim; - int pageWidth = canvasDim; - int halfWidth = legendWidth / 2; - int halfHeight = legendHeight / 2; - - std::array figureOrigin = {margins[3], margins[0] * 1.33F}; - // if(!generateEntirePlane) - // { - // figureOrigin[1] = 0.0F - legendHeight * 0.25F; - // } - std::array figureCenter = {figureOrigin[0] + halfWidth, figureOrigin[1] + halfHeight}; - // Create the actual Legend which will come back as ARGB values + // Generate the colored SST triangle image (ARGB) ebsdlib::UInt8ArrayType::Pointer image = CreateIPFLegend(this, legendHeight, generateEntirePlane); - // Convert from ARGB to RGBA which is what canvas_itk wants - image = ebsdlib::ConvertColorOrder(image.get(), legendHeight); - - // We need to mirror across the X Axis because the image was drawn with +Y pointing down - image = ebsdlib::MirrorImage(image.get(), legendHeight); - - // Create a 2D Canvas to draw into now that the Legend is in the proper form - canvas_ity::canvas context(pageWidth, pageHeight); - - std::vector latoBold = ebsdlib::fonts::GetLatoBold(); - std::vector latoRegular = ebsdlib::fonts::GetLatoRegular(); - context.set_font(latoBold.data(), static_cast(latoBold.size()), fontPtSize); - context.set_color(canvas_ity::fill_style, 0.0f, 0.0f, 0.0f, 1.0f); - canvas_ity::baseline_style const baselines[] = {canvas_ity::alphabetic, canvas_ity::top, canvas_ity::middle, canvas_ity::bottom, canvas_ity::hanging, canvas_ity::ideographic}; - context.text_baseline = baselines[0]; - - // Fill the whole background with white - context.move_to(0.0f, 0.0f); - context.line_to(static_cast(pageWidth), 0.0f); - context.line_to(static_cast(pageWidth), static_cast(pageHeight)); - context.line_to(0.0f, static_cast(pageHeight)); - context.line_to(0.0f, 0.0f); - context.close_path(); - context.set_color(canvas_ity::fill_style, 1.0f, 1.0f, 1.0f, 1.0f); - context.fill(); - - // Draw the legend image onto the canvas at the correct spot. - context.draw_image(image->getPointer(0), legendWidth, legendHeight, legendWidth * image->getNumberOfComponents(), figureOrigin[0], figureOrigin[1], static_cast(legendWidth), - static_cast(legendHeight)); - - // Draw Title of Legend - context.set_font(latoBold.data(), static_cast(latoBold.size()), fontPtSize * 1.5); - ebsdlib::WriteText(context, getSymmetryName(), {margins[0], static_cast(fontPtSize * 1.5)}, fontPtSize * 1.5); - - context.set_font(latoRegular.data(), static_cast(latoRegular.size()), fontPtSize); - DrawFullCircleAnnotations(context, canvasDim, fontPtSize, margins, figureOrigin, figureCenter, generateEntirePlane); - - // Fetch the rendered RGBA pixels from the entire canvas. - ebsdlib::UInt8ArrayType::Pointer rgbaCanvasImage = ebsdlib::UInt8ArrayType::CreateArray(pageHeight * pageWidth, {4ULL}, "Triangle Legend", true); - // std::vector rgbaCanvasImage(static_cast(pageHeight * pageWidth * 4)); - context.get_image_data(rgbaCanvasImage->getPointer(0), pageWidth, pageHeight, pageWidth * 4, 0, 0); - - // Remove the Alpha channel from the final image - rgbaCanvasImage = ebsdlib::RemoveAlphaChannel(rgbaCanvasImage.get()); - - return rgbaCanvasImage; + // Annotate with title and Miller index labels + return annotateIPFImage(image, legendHeight, canvasDim, getSymmetryName(), generateEntirePlane); } // ----------------------------------------------------------------------------- diff --git a/Source/EbsdLib/LaueOps/TriclinicOps.h b/Source/EbsdLib/LaueOps/TriclinicOps.h index 035758f..fa7fb66 100644 --- a/Source/EbsdLib/LaueOps/TriclinicOps.h +++ b/Source/EbsdLib/LaueOps/TriclinicOps.h @@ -262,6 +262,12 @@ class EbsdLib_EXPORT TriclinicOps : public LaueOps */ ebsdlib::UInt8ArrayType::Pointer generateIPFTriangleLegend(int imageDim, bool generateEntirePlane) const override; + void drawIPFAnnotations(canvas_ity::canvas& context, int canvasDim, + float fontPtSize, const std::vector& margins, + std::array figureOrigin, + std::array figureCenter, + bool drawFullCircle) const override; + /** * @brief Returns if the given Quaternion is within the Rodrigues Fundamental Zone (RFZ) * @param quat Input Quaternion diff --git a/Source/EbsdLib/LaueOps/TrigonalLowOps.cpp b/Source/EbsdLib/LaueOps/TrigonalLowOps.cpp index 0d4f1a7..a76295b 100644 --- a/Source/EbsdLib/LaueOps/TrigonalLowOps.cpp +++ b/Source/EbsdLib/LaueOps/TrigonalLowOps.cpp @@ -846,9 +846,27 @@ ebsdlib::UInt8ArrayType::Pointer CreateIPFLegend(const TrigonalLowOps* ops, int return image; } +} // namespace + +// ----------------------------------------------------------------------------- +std::array TrigonalLowOps::adjustFigureOrigin( + std::array figureOrigin, + int legendWidth, int legendHeight, + const std::vector& margins, float fontPtSize, + bool generateEntirePlane) const +{ + if(!generateEntirePlane) + { + figureOrigin[0] = -legendWidth * 0.0F; + figureOrigin[1] = -legendHeight * 0.25F; + } + return figureOrigin; +} + // ----------------------------------------------------------------------------- -void DrawFullCircleAnnotations(canvas_ity::canvas& context, int canvasDim, float fontPtSize, std::vector margins, std::array figureOrigin, std::array figureCenter, - bool drawFullCircle) +void TrigonalLowOps::drawIPFAnnotations(canvas_ity::canvas& context, int canvasDim, float fontPtSize, const std::vector& margins, std::array figureOrigin, + std::array figureCenter, + bool drawFullCircle) const { int legendHeight = canvasDim - margins[0] - margins[2]; int legendWidth = canvasDim - margins[1] - margins[3]; @@ -942,21 +960,19 @@ void DrawFullCircleAnnotations(canvas_ity::canvas& context, int canvasDim, float } } -} // namespace - // ----------------------------------------------------------------------------- ebsdlib::UInt8ArrayType::Pointer TrigonalLowOps::generateIPFTriangleLegend(int canvasDim, bool generateEntirePlane) const { - // Figure out the Legend Pixel Size + // Compute legend dimensions (same formula as annotateIPFImage uses) const float fontPtSize = static_cast(canvasDim) / 24.0f; - const std::vector margins = {fontPtSize * 3, // Top - static_cast(canvasDim / 7.0f), // Right - fontPtSize * 2, // Bottom - static_cast(canvasDim / 7.0f)}; // Left - - int legendHeight = canvasDim - margins[0] - margins[2]; - int legendWidth = canvasDim - margins[1] - margins[3]; - + const std::vector margins = { + fontPtSize * 3, + static_cast(canvasDim / 7.0f), + fontPtSize * 2, + static_cast(canvasDim / 7.0f) + }; + int legendHeight = canvasDim - static_cast(margins[0]) - static_cast(margins[2]); + int legendWidth = canvasDim - static_cast(margins[1]) - static_cast(margins[3]); if(legendHeight > legendWidth) { legendHeight = legendWidth; @@ -965,64 +981,12 @@ ebsdlib::UInt8ArrayType::Pointer TrigonalLowOps::generateIPFTriangleLegend(int c { legendWidth = legendHeight; } - int pageHeight = canvasDim; - int pageWidth = canvasDim; - int halfWidth = legendWidth / 2; - int halfHeight = legendHeight / 2; - - std::array figureOrigin = {margins[3], margins[0] * 1.33F}; - if(!generateEntirePlane) - { - figureOrigin[0] = 0.0F - legendWidth * 0.0F; - figureOrigin[1] = 0.0F - legendHeight * 0.25F; - } - std::array figureCenter = {figureOrigin[0] + halfWidth, figureOrigin[1] + halfHeight}; + // Generate the colored SST triangle image (ARGB) ebsdlib::UInt8ArrayType::Pointer image = CreateIPFLegend(this, legendHeight, generateEntirePlane); - // Create a Canvas to draw into - canvas_ity::canvas context(pageWidth, pageHeight); - - std::vector latoBold = ebsdlib::fonts::GetLatoBold(); - std::vector latoRegular = ebsdlib::fonts::GetLatoRegular(); - context.set_font(latoBold.data(), static_cast(latoBold.size()), fontPtSize); - context.set_color(canvas_ity::fill_style, 0.0f, 0.0f, 0.0f, 1.0f); - canvas_ity::baseline_style const baselines[] = {canvas_ity::alphabetic, canvas_ity::top, canvas_ity::middle, canvas_ity::bottom, canvas_ity::hanging, canvas_ity::ideographic}; - context.text_baseline = baselines[0]; - - // Fill the whole background with white - context.move_to(0.0f, 0.0f); - context.line_to(static_cast(pageWidth), 0.0f); - context.line_to(static_cast(pageWidth), static_cast(pageHeight)); - context.line_to(0.0f, static_cast(pageHeight)); - context.line_to(0.0f, 0.0f); - context.close_path(); - context.set_color(canvas_ity::fill_style, 1.0f, 1.0f, 1.0f, 1.0f); - context.fill(); - - // Convert from ARGB to RGBA which is what canvas_itk wants - image = ebsdlib::ConvertColorOrder(image.get(), legendHeight); - - // We need to mirror across the X Axis because the image was drawn with +Y pointing down - image = ebsdlib::MirrorImage(image.get(), legendHeight); - - context.draw_image(image->getPointer(0), legendWidth, legendHeight, legendWidth * image->getNumberOfComponents(), figureOrigin[0], figureOrigin[1], static_cast(legendWidth), - static_cast(legendHeight)); - - // Draw Title of Legend - context.set_font(latoBold.data(), static_cast(latoBold.size()), fontPtSize * 1.5); - ebsdlib::WriteText(context, getSymmetryName(), {margins[0], static_cast(fontPtSize * 1.5)}, fontPtSize * 1.5); - - context.set_font(latoRegular.data(), static_cast(latoRegular.size()), fontPtSize); - DrawFullCircleAnnotations(context, canvasDim, fontPtSize, margins, figureOrigin, figureCenter, generateEntirePlane); - - // Fetch the rendered RGBA pixels from the entire canvas. - ebsdlib::UInt8ArrayType::Pointer rgbaCanvasImage = ebsdlib::UInt8ArrayType::CreateArray(pageHeight * pageWidth, {4ULL}, "Triangle Legend", true); - // std::vector rgbaCanvasImage(static_cast(pageHeight * pageWidth * 4)); - context.get_image_data(rgbaCanvasImage->getPointer(0), pageWidth, pageHeight, pageWidth * 4, 0, 0); - - rgbaCanvasImage = ebsdlib::RemoveAlphaChannel(rgbaCanvasImage.get()); - return rgbaCanvasImage; + // Annotate with title and Miller index labels + return annotateIPFImage(image, legendHeight, canvasDim, getSymmetryName(), generateEntirePlane); } // ----------------------------------------------------------------------------- diff --git a/Source/EbsdLib/LaueOps/TrigonalLowOps.h b/Source/EbsdLib/LaueOps/TrigonalLowOps.h index 17ca764..b7049bf 100644 --- a/Source/EbsdLib/LaueOps/TrigonalLowOps.h +++ b/Source/EbsdLib/LaueOps/TrigonalLowOps.h @@ -264,6 +264,18 @@ class EbsdLib_EXPORT TrigonalLowOps : public LaueOps */ ebsdlib::UInt8ArrayType::Pointer generateIPFTriangleLegend(int imageDim, bool generateEntirePlane) const override; + void drawIPFAnnotations(canvas_ity::canvas& context, int canvasDim, + float fontPtSize, const std::vector& margins, + std::array figureOrigin, + std::array figureCenter, + bool drawFullCircle) const override; + + std::array adjustFigureOrigin( + std::array figureOrigin, + int legendWidth, int legendHeight, + const std::vector& margins, float fontPtSize, + bool generateEntirePlane) const override; + /** * @brief Returns if the given Quaternion is within the Rodrigues Fundamental Zone (RFZ) * @param quat Input Quaternion diff --git a/Source/EbsdLib/LaueOps/TrigonalOps.cpp b/Source/EbsdLib/LaueOps/TrigonalOps.cpp index 26ffc94..be03658 100644 --- a/Source/EbsdLib/LaueOps/TrigonalOps.cpp +++ b/Source/EbsdLib/LaueOps/TrigonalOps.cpp @@ -862,9 +862,27 @@ ebsdlib::UInt8ArrayType::Pointer CreateIPFLegend(const TrigonalOps* ops, int ima return image; } +} // namespace + +// ----------------------------------------------------------------------------- +std::array TrigonalOps::adjustFigureOrigin( + std::array figureOrigin, + int legendWidth, int legendHeight, + const std::vector& margins, float fontPtSize, + bool generateEntirePlane) const +{ + if(!generateEntirePlane) + { + figureOrigin[0] = -(legendWidth / 2) * 0.25; + figureOrigin[1] = 0.0F - (legendHeight / 2) * .5; + } + return figureOrigin; +} + // ----------------------------------------------------------------------------- -void DrawFullCircleAnnotations(canvas_ity::canvas& context, int canvasDim, float fontPtSize, std::vector margins, std::array figureOrigin, std::array figureCenter, - bool drawFullCircle) +void TrigonalOps::drawIPFAnnotations(canvas_ity::canvas& context, int canvasDim, float fontPtSize, const std::vector& margins, std::array figureOrigin, + std::array figureCenter, + bool drawFullCircle) const { int legendHeight = canvasDim - margins[0] - margins[2]; int legendWidth = canvasDim - margins[1] - margins[3]; @@ -950,20 +968,19 @@ void DrawFullCircleAnnotations(canvas_ity::canvas& context, int canvasDim, float } } -} // namespace // ----------------------------------------------------------------------------- ebsdlib::UInt8ArrayType::Pointer TrigonalOps::generateIPFTriangleLegend(int canvasDim, bool generateEntirePlane) const { - // Figure out the Legend Pixel Size + // Compute legend dimensions (same formula as annotateIPFImage uses) const float fontPtSize = static_cast(canvasDim) / 24.0f; - const std::vector margins = {fontPtSize * 3, // Top - static_cast(canvasDim / 7.0f), // Right - fontPtSize * 2, // Bottom - static_cast(canvasDim / 7.0f)}; // Left - - int legendHeight = canvasDim - margins[0] - margins[2]; - int legendWidth = canvasDim - margins[1] - margins[3]; - + const std::vector margins = { + fontPtSize * 3, + static_cast(canvasDim / 7.0f), + fontPtSize * 2, + static_cast(canvasDim / 7.0f) + }; + int legendHeight = canvasDim - static_cast(margins[0]) - static_cast(margins[2]); + int legendWidth = canvasDim - static_cast(margins[1]) - static_cast(margins[3]); if(legendHeight > legendWidth) { legendHeight = legendWidth; @@ -972,64 +989,12 @@ ebsdlib::UInt8ArrayType::Pointer TrigonalOps::generateIPFTriangleLegend(int canv { legendWidth = legendHeight; } - int pageHeight = canvasDim; - int pageWidth = canvasDim; - int halfWidth = legendWidth / 2; - int halfHeight = legendHeight / 2; - - std::array figureOrigin = {margins[3], margins[0] * 1.33F}; - if(!generateEntirePlane) - { - figureOrigin[0] = -halfWidth * 0.25; - figureOrigin[1] = 0.0F - halfHeight * .5; - } - std::array figureCenter = {figureOrigin[0] + halfWidth, figureOrigin[1] + halfHeight}; + // Generate the colored SST triangle image (ARGB) ebsdlib::UInt8ArrayType::Pointer image = CreateIPFLegend(this, legendHeight, generateEntirePlane); - // Create a Canvas to draw into - canvas_ity::canvas context(pageWidth, pageHeight); - - std::vector latoBold = ebsdlib::fonts::GetLatoBold(); - std::vector latoRegular = ebsdlib::fonts::GetLatoRegular(); - context.set_font(latoBold.data(), static_cast(latoBold.size()), fontPtSize); - context.set_color(canvas_ity::fill_style, 0.0f, 0.0f, 0.0f, 1.0f); - canvas_ity::baseline_style const baselines[] = {canvas_ity::alphabetic, canvas_ity::top, canvas_ity::middle, canvas_ity::bottom, canvas_ity::hanging, canvas_ity::ideographic}; - context.text_baseline = baselines[0]; - - // Fill the whole background with white - context.move_to(0.0f, 0.0f); - context.line_to(static_cast(pageWidth), 0.0f); - context.line_to(static_cast(pageWidth), static_cast(pageHeight)); - context.line_to(0.0f, static_cast(pageHeight)); - context.line_to(0.0f, 0.0f); - context.close_path(); - context.set_color(canvas_ity::fill_style, 1.0f, 1.0f, 1.0f, 1.0f); - context.fill(); - - // Convert from ARGB to RGBA which is what canvas_itk wants - image = ebsdlib::ConvertColorOrder(image.get(), legendHeight); - - // We need to mirror across the X Axis because the image was drawn with +Y pointing down - image = ebsdlib::MirrorImage(image.get(), legendHeight); - - context.draw_image(image->getPointer(0), legendWidth, legendHeight, legendWidth * image->getNumberOfComponents(), figureOrigin[0], figureOrigin[1], static_cast(legendWidth), - static_cast(legendHeight)); - - // Draw Title of Legend - context.set_font(latoBold.data(), static_cast(latoBold.size()), fontPtSize * 1.5); - ebsdlib::WriteText(context, getSymmetryName(), {margins[0], static_cast(fontPtSize * 1.5)}, fontPtSize * 1.5); - - context.set_font(latoRegular.data(), static_cast(latoRegular.size()), fontPtSize); - DrawFullCircleAnnotations(context, canvasDim, fontPtSize, margins, figureOrigin, figureCenter, generateEntirePlane); - - // Fetch the rendered RGBA pixels from the entire canvas. - ebsdlib::UInt8ArrayType::Pointer rgbaCanvasImage = ebsdlib::UInt8ArrayType::CreateArray(pageHeight * pageWidth, {4ULL}, "Triangle Legend", true); - // std::vector rgbaCanvasImage(static_cast(pageHeight * pageWidth * 4)); - context.get_image_data(rgbaCanvasImage->getPointer(0), pageWidth, pageHeight, pageWidth * 4, 0, 0); - - rgbaCanvasImage = ebsdlib::RemoveAlphaChannel(rgbaCanvasImage.get()); - return rgbaCanvasImage; + // Annotate with title and Miller index labels + return annotateIPFImage(image, legendHeight, canvasDim, getSymmetryName(), generateEntirePlane); } // ----------------------------------------------------------------------------- diff --git a/Source/EbsdLib/LaueOps/TrigonalOps.h b/Source/EbsdLib/LaueOps/TrigonalOps.h index c59a0db..8b73f85 100644 --- a/Source/EbsdLib/LaueOps/TrigonalOps.h +++ b/Source/EbsdLib/LaueOps/TrigonalOps.h @@ -263,6 +263,18 @@ class EbsdLib_EXPORT TrigonalOps : public LaueOps */ ebsdlib::UInt8ArrayType::Pointer generateIPFTriangleLegend(int imageDim, bool generateEntirePlane) const override; + void drawIPFAnnotations(canvas_ity::canvas& context, int canvasDim, + float fontPtSize, const std::vector& margins, + std::array figureOrigin, + std::array figureCenter, + bool drawFullCircle) const override; + + std::array adjustFigureOrigin( + std::array figureOrigin, + int legendWidth, int legendHeight, + const std::vector& margins, float fontPtSize, + bool generateEntirePlane) const override; + /** * @brief Returns if the given Quaternion is within the Rodrigues Fundamental Zone (RFZ) * @param quat Input Quaternion From 793d4071299f898bd47a6da1834908a7e17b10f7 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Mon, 23 Mar 2026 22:44:13 -0400 Subject: [PATCH 05/14] ENH: Update IPF density apps to use annotated output with labels and color bar - Update generate_ipf_from_file.cpp to call generateAnnotatedIPFDensity() instead of generateInversePoleFigure(), removing manual ARGB-to-RGB conversion since annotated images are already RGB - Update generate_ipf_density.cpp similarly for generateIPFForLaueClass() - Fix crash in drawColorBar() where colors vector was not pre-allocated before passing to EbsdColorTable::GetColorTable() - Make canvas_ity include directory PUBLIC on EbsdLib target since LaueOps.h (a public header) includes canvas_ity.hpp - Add generate_ipf_from_file target to CMake build Co-Authored-By: Claude Opus 4.6 (1M context) --- Source/Apps/SourceList.cmake | 14 +- Source/Apps/generate_ipf_density.cpp | 19 +- Source/Apps/generate_ipf_from_file.cpp | 425 +++++++++++++++++++++++++ Source/EbsdLib/LaueOps/LaueOps.cpp | 2 +- Source/EbsdLib/SourceList.cmake | 5 +- 5 files changed, 451 insertions(+), 14 deletions(-) create mode 100644 Source/Apps/generate_ipf_from_file.cpp diff --git a/Source/Apps/SourceList.cmake b/Source/Apps/SourceList.cmake index 0ada0a3..bcb1e34 100644 --- a/Source/Apps/SourceList.cmake +++ b/Source/Apps/SourceList.cmake @@ -25,12 +25,10 @@ target_include_directories(eq_orientations PUBLIC ${EbsdLibProj_SOURCE_DIR}/Sour add_executable(generate_ipf_legends ${EbsdLibProj_SOURCE_DIR}/Source/Apps/generate_ipf_legends.cpp) target_link_libraries(generate_ipf_legends PUBLIC EbsdLib) -target_include_directories(generate_ipf_legends - PUBLIC - ${EbsdLibProj_SOURCE_DIR}/Source - ${EbsdLibProj_BINARY_DIR} - PRIVATE - "${EbsdLibProj_SOURCE_DIR}/3rdParty/canvas_ity/src") +target_include_directories(generate_ipf_legends + PUBLIC + ${EbsdLibProj_SOURCE_DIR}/Source + ${EbsdLibProj_BINARY_DIR}) add_executable(generate_ipf_density ${EbsdLibProj_SOURCE_DIR}/Source/Apps/generate_ipf_density.cpp) target_link_libraries(generate_ipf_density PUBLIC EbsdLib) @@ -39,6 +37,10 @@ target_include_directories(generate_ipf_density ${EbsdLibProj_SOURCE_DIR}/Source ${EbsdLibProj_BINARY_DIR}) +add_executable(generate_ipf_from_file ${EbsdLibProj_SOURCE_DIR}/Source/Apps/generate_ipf_from_file.cpp) +target_link_libraries(generate_ipf_from_file PUBLIC EbsdLib) +target_include_directories(generate_ipf_from_file PUBLIC ${EbsdLibProj_SOURCE_DIR}/Source) + add_executable(ParseAztecProject ${EbsdLibProj_SOURCE_DIR}/Source/Apps/ParseAztecProject.cpp) target_link_libraries(ParseAztecProject PUBLIC EbsdLib) target_include_directories(ParseAztecProject PUBLIC ${EbsdLibProj_SOURCE_DIR}/Source) diff --git a/Source/Apps/generate_ipf_density.cpp b/Source/Apps/generate_ipf_density.cpp index c3d3df5..327ebbb 100644 --- a/Source/Apps/generate_ipf_density.cpp +++ b/Source/Apps/generate_ipf_density.cpp @@ -158,7 +158,7 @@ void writeIPFImage(ebsdlib::UInt8ArrayType* image, int width, int height, const } // ----------------------------------------------------------------------- -// Generate and save IPF density images for a single LaueOps instance. +// Generate and save annotated IPF density images for a single LaueOps instance. // ----------------------------------------------------------------------- void generateIPFForLaueClass(const LaueOps& ops, ebsdlib::FloatArrayType* eulers, const std::string& outputDir, int imageWidth, int imageHeight, int lambertDim, const std::string& textureLabel) { @@ -178,9 +178,9 @@ void generateIPFForLaueClass(const LaueOps& ops, ebsdlib::FloatArrayType* eulers config.phaseName = className; config.FlipFinalImage = false; - auto images = ops.generateInversePoleFigure(config); + auto images = ops.generateAnnotatedIPFDensity(config); - // Sanitize symmetry name for use as a filename (replace / and spaces) + // Sanitize symmetry name for filename std::string safeName = className; for(auto& c : safeName) { @@ -190,12 +190,21 @@ void generateIPFForLaueClass(const LaueOps& ops, ebsdlib::FloatArrayType* eulers } } + int canvasDim = static_cast(static_cast(imageWidth) * 1.5f); std::array dirLabels = {"RD", "TD", "ND"}; - for(size_t i = 0; i < 3; i++) + for(size_t i = 0; i < images.size(); i++) { std::ostringstream filePath; filePath << outputDir << "/" << safeName << "_IPF_" << dirLabels[i] << "_" << textureLabel << ".tiff"; - writeIPFImage(images[i].get(), imageWidth, imageHeight, filePath.str()); + auto result = TiffWriter::WriteColorImage(filePath.str(), canvasDim, canvasDim, 3, images[i]->data()); + if(result.first < 0) + { + std::cerr << " ERROR writing " << filePath.str() << ": " << result.second << std::endl; + } + else + { + std::cout << " Wrote: " << filePath.str() << std::endl; + } } } diff --git a/Source/Apps/generate_ipf_from_file.cpp b/Source/Apps/generate_ipf_from_file.cpp new file mode 100644 index 0000000..59ca916 --- /dev/null +++ b/Source/Apps/generate_ipf_from_file.cpp @@ -0,0 +1,425 @@ +/* ============================================================================ + * Copyright (c) 2025-2026 BlueQuartz Software, LLC + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or + * other materials provided with the distribution. + * + * Neither the name of BlueQuartz Software, the US Air Force, nor the names of its + * contributors may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ + +/** + * @file generate_ipf_from_file.cpp + * @brief Example program that reads a .ctf or .ang EBSD data file and generates + * Inverse Pole Figure (IPF) density images for each phase found in the data. + * + * For each phase, 3 TIFF images are generated corresponding to 3 orthogonal + * sample directions: RD (Rolling Direction), TD (Transverse Direction), and + * ND (Normal Direction). + * + * Usage: + * generate_ipf_from_file [output_directory] + * + * If no output directory is specified, images are written next to the input file. + */ + +#include "EbsdLib/Core/EbsdDataArray.hpp" +#include "EbsdLib/Core/EbsdLibConstants.h" +#include "EbsdLib/IO/HKL/CtfPhase.h" +#include "EbsdLib/IO/HKL/CtfReader.h" +#include "EbsdLib/IO/TSL/AngPhase.h" +#include "EbsdLib/IO/TSL/AngReader.h" +#include "EbsdLib/LaueOps/LaueOps.h" +#include "EbsdLib/Utilities/InversePoleFigureUtilities.h" +#include "EbsdLib/Utilities/TiffWriter.h" + +#include +#include +#include +#include +#include +#include +#include + +using namespace ebsdlib; + +namespace +{ + +// ----------------------------------------------------------------------- +// Generate and save annotated IPF density images for a given set of Euler +// angles using a specific LaueOps instance. +// ----------------------------------------------------------------------- +void generateIPFForPhase(const LaueOps& ops, ebsdlib::FloatArrayType* eulers, const std::string& outputDir, int imageWidth, int imageHeight, int lambertDim, const std::string& phaseLabel) +{ + std::string className = ops.getSymmetryName(); + std::cout << "Generating annotated IPF density for phase: " << phaseLabel << " (" << className << ", " << eulers->getNumberOfTuples() << " orientations)" << std::endl; + + InversePoleFigureConfiguration_t config; + config.eulers = eulers; + config.sampleDirections = {Matrix3X1D(1.0, 0.0, 0.0), Matrix3X1D(0.0, 1.0, 0.0), Matrix3X1D(0.0, 0.0, 1.0)}; + config.imageWidth = imageWidth; + config.imageHeight = imageHeight; + config.lambertDim = lambertDim; + config.numColors = 64; + config.colorMap = "Default"; + config.normalizeMRD = true; + config.labels = {"RD", "TD", "ND"}; + config.phaseName = phaseLabel; + config.FlipFinalImage = false; + + auto images = ops.generateAnnotatedIPFDensity(config); + + // Sanitize phase name for filename + std::string safeName = phaseLabel; + for(auto& c : safeName) + { + if(c == '/' || c == '\\' || c == ' ' || c == '(' || c == ')') + { + c = '_'; + } + } + + // Images are RGB (3 components), canvasDim x canvasDim + int canvasDim = static_cast(static_cast(imageWidth) * 1.5f); + std::array dirLabels = {"RD", "TD", "ND"}; + for(size_t i = 0; i < images.size(); i++) + { + std::ostringstream filePath; + filePath << outputDir << "/" << safeName << "_IPF_" << dirLabels[i] << ".tiff"; + auto result = TiffWriter::WriteColorImage(filePath.str(), canvasDim, canvasDim, 3, images[i]->data()); + if(result.first < 0) + { + std::cerr << " ERROR writing " << filePath.str() << ": " << result.second << std::endl; + } + else + { + std::cout << " Wrote: " << filePath.str() << std::endl; + } + } +} + +// ----------------------------------------------------------------------- +// Holds orientation data extracted from an EBSD file, grouped by phase. +// ----------------------------------------------------------------------- +struct PhaseData +{ + std::string phaseName; + unsigned int laueOpsIndex = ebsdlib::CrystalStructure::UnknownCrystalStructure; + ebsdlib::FloatArrayType::Pointer eulers; +}; + +// ----------------------------------------------------------------------- +// Read a .ang file and return per-phase orientation data. +// ANG files store Euler angles in radians. +// ----------------------------------------------------------------------- +std::vector readAngFile(const std::string& filePath) +{ + AngReader reader; + reader.setFileName(filePath); + int err = reader.readFile(); + if(err < 0) + { + std::cerr << "ERROR: Failed to read .ang file: " << filePath << std::endl; + return {}; + } + + size_t totalPoints = reader.getNumberOfElements(); + std::cout << " Read " << totalPoints << " data points (" << reader.getXDimension() << " x " << reader.getYDimension() << ")" << std::endl; + + float* phi1 = reader.getPhi1Pointer(false); + float* phi = reader.getPhiPointer(false); + float* phi2 = reader.getPhi2Pointer(false); + int* phaseData = reader.getPhaseDataPointer(false); + + std::vector phases = reader.getPhaseVector(); + + // Build a map from phase index to LaueOps index and phase name. + // ANG phase indices are 1-based. + std::map phaseToLaueOps; + std::map phaseToName; + for(const auto& phase : phases) + { + int idx = phase->getPhaseIndex(); + phaseToLaueOps[idx] = phase->determineOrientationOpsIndex(); + std::string name = phase->getMaterialName(); + if(name.empty()) + { + name = "Phase_" + std::to_string(idx); + } + phaseToName[idx] = name; + std::cout << " Phase " << idx << ": " << name << " (LaueOps index: " << phaseToLaueOps[idx] << ")" << std::endl; + } + + // Group Euler angles by phase. + // ANG phase data uses 0 for unindexed points; map those to phase 1 + // when phase 1 exists (consistent with make_ipf.cpp behavior). + std::map> phaseEulerMap; + for(size_t i = 0; i < totalPoints; i++) + { + int p = phaseData[i]; + if(p < 1 && phaseToLaueOps.find(1) != phaseToLaueOps.end()) + { + p = 1; + } + if(phaseToLaueOps.find(p) == phaseToLaueOps.end()) + { + continue; + } + if(phaseToLaueOps[p] >= ebsdlib::CrystalStructure::LaueGroupEnd) + { + continue; + } + phaseEulerMap[p].push_back(phi1[i]); + phaseEulerMap[p].push_back(phi[i]); + phaseEulerMap[p].push_back(phi2[i]); + } + + // Convert grouped data into PhaseData structs + std::vector result; + for(auto& [phaseIdx, eulerVec] : phaseEulerMap) + { + size_t numOrientations = eulerVec.size() / 3; + if(numOrientations == 0) + { + continue; + } + + PhaseData pd; + pd.phaseName = phaseToName[phaseIdx]; + pd.laueOpsIndex = phaseToLaueOps[phaseIdx]; + + std::vector cDims = {3}; + pd.eulers = ebsdlib::FloatArrayType::CreateArray(numOrientations, cDims, "EulerAngles", true); + std::memcpy(pd.eulers->getVoidPointer(0), eulerVec.data(), eulerVec.size() * sizeof(float)); + + result.push_back(std::move(pd)); + } + + return result; +} + +// ----------------------------------------------------------------------- +// Read a .ctf file and return per-phase orientation data. +// CTF files store Euler angles in degrees; we convert to radians. +// ----------------------------------------------------------------------- +std::vector readCtfFile(const std::string& filePath) +{ + CtfReader reader; + reader.setFileName(filePath); + int err = reader.readFile(); + if(err < 0) + { + std::cerr << "ERROR: Failed to read .ctf file: " << filePath << std::endl; + return {}; + } + + size_t totalPoints = reader.getNumberOfElements(); + std::cout << " Read " << totalPoints << " data points (" << reader.getXDimension() << " x " << reader.getYDimension() << ")" << std::endl; + + float* euler1 = reader.getEuler1Pointer(); + float* euler2 = reader.getEuler2Pointer(); + float* euler3 = reader.getEuler3Pointer(); + int* phaseData = reader.getPhasePointer(); + + std::vector phases = reader.getPhaseVector(); + + // Build a map from phase index to LaueOps index and phase name. + // CTF phase indices are 1-based. + std::map phaseToLaueOps; + std::map phaseToName; + for(const auto& phase : phases) + { + int idx = phase->getPhaseIndex(); + phaseToLaueOps[idx] = phase->determineOrientationOpsIndex(); + std::string name = phase->getPhaseName(); + if(name.empty()) + { + name = "Phase_" + std::to_string(idx); + } + phaseToName[idx] = name; + std::cout << " Phase " << idx << ": " << name << " (LaueOps index: " << phaseToLaueOps[idx] << ")" << std::endl; + } + + // Group Euler angles by phase, converting degrees to radians. + // CTF phase data uses 0 for unindexed points; map those to phase 1 + // when phase 1 exists. + const float degToRad = static_cast(ebsdlib::constants::k_DegToRadD); + std::map> phaseEulerMap; + for(size_t i = 0; i < totalPoints; i++) + { + int p = phaseData[i]; + if(p < 1 && phaseToLaueOps.find(1) != phaseToLaueOps.end()) + { + p = 1; + } + if(phaseToLaueOps.find(p) == phaseToLaueOps.end()) + { + continue; + } + if(phaseToLaueOps[p] >= ebsdlib::CrystalStructure::LaueGroupEnd) + { + continue; + } + phaseEulerMap[p].push_back(euler1[i] * degToRad); + phaseEulerMap[p].push_back(euler2[i] * degToRad); + phaseEulerMap[p].push_back(euler3[i] * degToRad); + } + + // Convert grouped data into PhaseData structs + std::vector result; + for(auto& [phaseIdx, eulerVec] : phaseEulerMap) + { + size_t numOrientations = eulerVec.size() / 3; + if(numOrientations == 0) + { + continue; + } + + PhaseData pd; + pd.phaseName = phaseToName[phaseIdx]; + pd.laueOpsIndex = phaseToLaueOps[phaseIdx]; + + std::vector cDims = {3}; + pd.eulers = ebsdlib::FloatArrayType::CreateArray(numOrientations, cDims, "EulerAngles", true); + std::memcpy(pd.eulers->getVoidPointer(0), eulerVec.data(), eulerVec.size() * sizeof(float)); + + result.push_back(std::move(pd)); + } + + return result; +} + +} // namespace + +// ============================================================================= +int main(int argc, char* argv[]) +{ + if(argc < 2) + { + std::cout << "Usage: generate_ipf_from_file [output_directory]" << std::endl; + std::cout << std::endl; + std::cout << "Reads an EBSD data file and generates Inverse Pole Figure density" << std::endl; + std::cout << "images (RD, TD, ND) for each phase found in the data." << std::endl; + return 1; + } + + std::string inputFile = argv[1]; + std::filesystem::path inputPath(inputFile); + + if(!std::filesystem::exists(inputPath)) + { + std::cerr << "ERROR: Input file does not exist: " << inputFile << std::endl; + return 1; + } + + // Determine output directory + std::string outputDir; + if(argc >= 3) + { + outputDir = argv[2]; + } + else + { + outputDir = inputPath.parent_path().string(); + if(outputDir.empty()) + { + outputDir = "."; + } + } + + // Create output directory if needed + std::filesystem::create_directories(outputDir); + + // Determine file type from extension + std::string ext = inputPath.extension().string(); + for(auto& c : ext) + { + c = static_cast(std::tolower(static_cast(c))); + } + + std::cout << "============================================================" << std::endl; + std::cout << " IPF Density from EBSD File" << std::endl; + std::cout << "============================================================" << std::endl; + std::cout << " Input file: " << inputFile << std::endl; + std::cout << " File type: " << ext << std::endl; + std::cout << " Output directory: " << outputDir << std::endl; + std::cout << "============================================================" << std::endl; + std::cout << std::endl; + + // Read the file + std::vector phaseDataVec; + if(ext == ".ang") + { + std::cout << "Reading .ang file..." << std::endl; + phaseDataVec = readAngFile(inputFile); + } + else if(ext == ".ctf") + { + std::cout << "Reading .ctf file..." << std::endl; + phaseDataVec = readCtfFile(inputFile); + } + else + { + std::cerr << "ERROR: Unsupported file extension '" << ext << "'. Use .ang or .ctf" << std::endl; + return 1; + } + + if(phaseDataVec.empty()) + { + std::cerr << "ERROR: No valid phase data found in file." << std::endl; + return 1; + } + + std::cout << std::endl; + std::cout << "Found " << phaseDataVec.size() << " phase(s) with valid orientations." << std::endl; + std::cout << std::endl; + + // Image generation parameters + int imageWidth = 1024; + int imageHeight = 1024; + int lambertDim = 64; + + // Get all LaueOps + std::vector ops = LaueOps::GetAllOrientationOps(); + + // Generate IPF density images for each phase + for(const auto& pd : phaseDataVec) + { + if(pd.laueOpsIndex >= ops.size()) + { + std::cerr << " Skipping phase '" << pd.phaseName << "': invalid LaueOps index " << pd.laueOpsIndex << std::endl; + continue; + } + + generateIPFForPhase(*ops[pd.laueOpsIndex], pd.eulers.get(), outputDir, imageWidth, imageHeight, lambertDim, pd.phaseName); + std::cout << std::endl; + } + + std::cout << "============================================================" << std::endl; + std::cout << " Done! All IPF density images written to:" << std::endl; + std::cout << " " << outputDir << std::endl; + std::cout << "============================================================" << std::endl; + + return 0; +} diff --git a/Source/EbsdLib/LaueOps/LaueOps.cpp b/Source/EbsdLib/LaueOps/LaueOps.cpp index c04568b..506eaee 100644 --- a/Source/EbsdLib/LaueOps/LaueOps.cpp +++ b/Source/EbsdLib/LaueOps/LaueOps.cpp @@ -1046,7 +1046,7 @@ UInt8ArrayType::Pointer LaueOps::drawColorBar( const float fontPtSize = static_cast(canvasDim) / 24.0f; // Generate the color table - std::vector colors; + std::vector colors(numColors * 3, 0.0f); EbsdColorTable::GetColorTable(numColors, colors); // Create a canvas from the existing RGB image by first adding an alpha channel diff --git a/Source/EbsdLib/SourceList.cmake b/Source/EbsdLib/SourceList.cmake index 14b71b1..5713629 100644 --- a/Source/EbsdLib/SourceList.cmake +++ b/Source/EbsdLib/SourceList.cmake @@ -145,8 +145,9 @@ add_library(${PROJECT_NAME}::${PROJECT_NAME} ALIAS ${PROJECT_NAME}) # If there are additional include directories that are needed for this plugin # you can use the target_include_directories(.....) cmake call target_include_directories(${PROJECT_NAME} - PRIVATE - "${EbsdLibProj_SOURCE_DIR}/3rdParty/canvas_ity/src" + PUBLIC + $ + $ ) if(EbsdLib_INSTALL_FILES) install(FILES From 42e74e6b58bd7ab165ff04c5f8fc06f2f446d823 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Tue, 24 Mar 2026 11:43:35 -0400 Subject: [PATCH 06/14] DOC: Add design spec for SST-zoomed IPF density images --- .../2026-03-24-sst-zoomed-density-design.md | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 Docs/superpowers/specs/2026-03-24-sst-zoomed-density-design.md diff --git a/Docs/superpowers/specs/2026-03-24-sst-zoomed-density-design.md b/Docs/superpowers/specs/2026-03-24-sst-zoomed-density-design.md new file mode 100644 index 0000000..b7c4950 --- /dev/null +++ b/Docs/superpowers/specs/2026-03-24-sst-zoomed-density-design.md @@ -0,0 +1,79 @@ +# SST-Zoomed Inverse Pole Figure Density — Design Spec + +## Problem + +The IPF density images map pixels to the full Lambert equal-area hemisphere disk, but the Standard Stereographic Triangle (SST) occupies only a small fraction of the full disk. For cubic symmetry, the SST is roughly 1/48th of the hemisphere. This produces a tiny triangle in a large white image. The IPF legend, by contrast, zooms to fill the frame with just the SST region. + +## Solution + +Add a virtual method `getSSTBoundingBox()` to LaueOps that returns the spherical coordinate bounds (etaMin, etaMax, chiMin, chiMax) of each symmetry class's SST. Modify `computeIPFIntensity()` to accept an optional bounding box parameter. When provided, pixels map to only the SST bounding box region in (eta, chi) space instead of the full Lambert disk. + +The Lambert binning of crystal directions (accumulation step) is unchanged. Only the output pixel-to-sphere mapping changes. + +## New Virtual Method + +```cpp +virtual std::array getSSTBoundingBox() const; +// Returns {etaMin, etaMax, chiMin, chiMax} in radians +``` + +### Per-Subclass Values + +| Subclass | etaMax (deg) | chiMax (deg) | +|----------|-------------|-------------| +| Cubic High (m-3m) | 45 | 54.7356 (arccos(1/sqrt(3))) | +| Cubic Low (m-3) | 45 | 54.7356 | +| Hexagonal High (6/mmm) | 30 | 90 | +| Hexagonal Low (6/m) | 30 | 90 | +| Trigonal High (-3m) | 30 | 90 | +| Trigonal Low (-3) | 60 | 90 | +| Tetragonal High (4/mmm) | 45 | 90 | +| Tetragonal Low (4/m) | 45 | 90 | +| Orthorhombic (mmm) | 90 | 90 | +| Monoclinic (2/m) | 90 | 90 | +| Triclinic (-1) | 180 | 90 | + +All subclasses have etaMin = 0, chiMin = 0. For cubic classes, the SST has a curved upper chi boundary that varies with eta; the bounding box uses the maximum chiMax. The existing `inUnitTriangle()` check marks pixels outside the curved boundary as white. + +## Modified computeIPFIntensity Signature + +```cpp +static DoubleArrayType::Pointer computeIPFIntensity( + const LaueOps& ops, + FloatArrayType* ipfDirections, + int imageWidth, int imageHeight, + int lambertDim, bool normalizeMRD, + const std::array* sstBoundingBox = nullptr); +``` + +When `sstBoundingBox` is nullptr: existing full-disk behavior (backward compatible). +When provided: zoomed SST behavior. + +## Pixel-to-Sphere Mapping (Zoomed SST Mode) + +``` +For each pixel (px, py) in [0, imageWidth) x [0, imageHeight): + eta = etaMin + ((px + 0.5) / imageWidth) * (etaMax - etaMin) + chi = chiMin + ((py + 0.5) / imageHeight) * (chiMax - chiMin) + + xyz = (sin(chi)*cos(eta), sin(chi)*sin(eta), cos(chi)) + + if !inUnitTriangle(eta, chi): + intensity = -1.0 (white) + else: + intensity = lambert.getInterpolatedValue(xyz) +``` + +The +0.5 offset centers the sample at each pixel center. + +## Changes to generateAnnotatedIPFDensity + +Call `getSSTBoundingBox()` and pass it to `computeIPFIntensity()`. No other changes needed — the output image fills the frame with the SST, and the annotation labels from `drawIPFAnnotations()` align correctly. + +## Files to Modify + +- `Source/EbsdLib/LaueOps/LaueOps.h` — add `getSSTBoundingBox()` virtual declaration +- `Source/EbsdLib/LaueOps/LaueOps.cpp` — update `generateAnnotatedIPFDensity()` to pass bounding box; add default `getSSTBoundingBox()` implementation +- `Source/EbsdLib/Utilities/InversePoleFigureUtilities.h` — update `computeIPFIntensity()` signature +- `Source/EbsdLib/Utilities/InversePoleFigureUtilities.cpp` — implement zoomed SST mapping +- 11 LaueOps subclass `.h` and `.cpp` files — add `getSSTBoundingBox()` override From ae7847eb22153dc86f990b0ecf39024526e12338 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Tue, 24 Mar 2026 12:00:24 -0400 Subject: [PATCH 07/14] DOC: Add implementation plan for SST-zoomed IPF density --- .../plans/2026-03-24-sst-zoomed-density.md | 316 ++++++++++++++++++ 1 file changed, 316 insertions(+) create mode 100644 Docs/superpowers/plans/2026-03-24-sst-zoomed-density.md diff --git a/Docs/superpowers/plans/2026-03-24-sst-zoomed-density.md b/Docs/superpowers/plans/2026-03-24-sst-zoomed-density.md new file mode 100644 index 0000000..d790bb5 --- /dev/null +++ b/Docs/superpowers/plans/2026-03-24-sst-zoomed-density.md @@ -0,0 +1,316 @@ +# SST-Zoomed IPF Density Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Make IPF density images fill the frame by mapping pixels to only the SST bounding box (eta/chi range) instead of the full Lambert hemisphere disk. + +**Architecture:** Add an optional `sstBoundingBox` parameter to `computeIPFIntensity()`. When provided, pixels map to the SST region in (eta, chi) spherical coordinates. The bounding box values come from the existing `getIpfColorAngleLimits()` virtual method already on all 11 LaueOps subclasses. `generateAnnotatedIPFDensity()` passes the bounding box automatically. + +**Tech Stack:** C++20, EbsdLib LaueOps, InversePoleFigureUtilities, Lambert projection + +--- + +## File Map + +| File | Change | +|------|--------| +| `Source/EbsdLib/Utilities/InversePoleFigureUtilities.h` | Add optional `sstBoundingBox` parameter to `computeIPFIntensity()` | +| `Source/EbsdLib/Utilities/InversePoleFigureUtilities.cpp` | Implement SST-zoomed pixel mapping when bounding box provided | +| `Source/EbsdLib/LaueOps/LaueOps.cpp` | Update `generateAnnotatedIPFDensity()` to compute bounding box from `getIpfColorAngleLimits()` and pass it | + +No new files. No changes to any of the 11 subclass files (the existing `getIpfColorAngleLimits()` already provides what we need). + +## Key Reference + +**Existing `getIpfColorAngleLimits(double eta)`** — virtual method on all 11 LaueOps subclasses. Returns `std::array` = `{etaMin, etaMax, chiMax}` in radians. For cubic classes, `chiMax` varies with `eta`; for all others it's constant (90° = π/2). + +**Existing constants per subclass** (all in degrees, in each .cpp file): + +| Subclass | etaMin | etaMax | chiMax | +|----------|--------|--------|--------| +| CubicOps | 0 | 45 | dynamic: arccos(1/sqrt(3)) ≈ 54.74° at eta=45° | +| CubicLowOps | 0 | 90 | dynamic: arccos(1/sqrt(3)) ≈ 54.74° at eta=45° | +| HexagonalOps | 0 | 30 | 90 | +| HexagonalLowOps | 0 | 60 | 90 | +| TrigonalOps | -90 | -30 | 90 | +| TrigonalLowOps | -120 | 0 | 90 | +| TetragonalOps | 0 | 45 | 90 | +| TetragonalLowOps | 0 | 90 | 90 | +| OrthoRhombicOps | 0 | 90 | 90 | +| MonoclinicOps | 0 | 180 | 90 | +| TriclinicOps | 0 | 180 | 90 | + +Note: TrigonalOps and TrigonalLowOps have **negative** eta ranges. The SST mapping must handle negative eta values correctly. + +--- + +## Tasks + +### Task 1: Modify `computeIPFIntensity()` to accept SST bounding box + +**Files:** +- Modify: `Source/EbsdLib/Utilities/InversePoleFigureUtilities.h` +- Modify: `Source/EbsdLib/Utilities/InversePoleFigureUtilities.cpp` + +- [ ] **Step 1: Update the function signature in the header** + +In `InversePoleFigureUtilities.h`, change the declaration from: + +```cpp + static ebsdlib::DoubleArrayType::Pointer computeIPFIntensity(const LaueOps& ops, ebsdlib::FloatArrayType* ipfDirections, int imageWidth, int imageHeight, int lambertDim, bool normalizeMRD); +``` + +To: + +```cpp + static ebsdlib::DoubleArrayType::Pointer computeIPFIntensity(const LaueOps& ops, ebsdlib::FloatArrayType* ipfDirections, int imageWidth, int imageHeight, int lambertDim, bool normalizeMRD, + const std::array* sstBoundingBox = nullptr); +``` + +Add `#include ` if not already present. + +Update the doc comment to add: +``` + * @param sstBoundingBox Optional SST bounding box {etaMin, etaMax, chiMin, chiMax} in radians. + * When provided, pixels map to only this region in (eta, chi) space, making the SST fill the image. + * When nullptr, uses the default full Lambert hemisphere disk mapping. +``` + +- [ ] **Step 2: Update the function signature in the .cpp file** + +In `InversePoleFigureUtilities.cpp`, update the function definition to match the new signature: + +```cpp +ebsdlib::DoubleArrayType::Pointer InversePoleFigureUtilities::computeIPFIntensity(const LaueOps& ops, ebsdlib::FloatArrayType* ipfDirections, int imageWidth, int imageHeight, int lambertDim, + bool normalizeMRD, const std::array* sstBoundingBox) +``` + +- [ ] **Step 3: Add the SST-zoomed pixel mapping code path** + +In `InversePoleFigureUtilities.cpp`, replace the pixel iteration loop (the "Step 3" section, lines ~172-234) with code that handles both modes. The Lambert binning (Steps 1-2) stays unchanged. Replace from the `// Step 3:` comment through the end of the function: + +```cpp + // Step 3: Create the output intensity image + std::vector tDims = {static_cast(imageWidth * imageHeight)}; + std::vector cDims = {1}; + ebsdlib::DoubleArrayType::Pointer intensity = ebsdlib::DoubleArrayType::CreateArray(tDims, cDims, "IPF_Intensity", true); + double* intensityPtr = intensity->getPointer(0); + + if(sstBoundingBox != nullptr) + { + // SST-zoomed mode: map pixels to the SST bounding box in (eta, chi) space + double etaMin = (*sstBoundingBox)[0]; + double etaMax = (*sstBoundingBox)[1]; + double chiMin = (*sstBoundingBox)[2]; + double chiMax = (*sstBoundingBox)[3]; + + for(int y = 0; y < imageHeight; y++) + { + for(int x = 0; x < imageWidth; x++) + { + int index = y * imageWidth + x; + + // Map pixel to (eta, chi) within the bounding box + // x maps to eta (left=etaMin, right=etaMax) + // y maps to chi (top=chiMin, bottom=chiMax) + double eta = etaMin + (static_cast(x) + 0.5) / static_cast(imageWidth) * (etaMax - etaMin); + double chi = chiMin + (static_cast(y) + 0.5) / static_cast(imageHeight) * (chiMax - chiMin); + + // Check if direction is inside the SST + if(!ops.inUnitTriangle(eta, chi)) + { + intensityPtr[index] = -1.0; + continue; + } + + // Convert (eta, chi) to unit sphere xyz + double sinChi = std::sin(chi); + std::array xyz = { + static_cast(sinChi * std::cos(eta)), + static_cast(sinChi * std::sin(eta)), + static_cast(std::cos(chi))}; + + // Look up the interpolated intensity from the Lambert projection + std::array sqCoord = {0.0f, 0.0f}; + bool isNorth = lambert->getSquareCoord(xyz.data(), sqCoord.data()); + if(isNorth) + { + intensityPtr[index] = lambert->getInterpolatedValue(ModifiedLambertProjection::NorthSquare, sqCoord.data()); + } + else + { + intensityPtr[index] = lambert->getInterpolatedValue(ModifiedLambertProjection::SouthSquare, sqCoord.data()); + } + } + } + } + else + { + // Original full-disk mode: Lambert azimuthal equal-area projection + float unitRadius = std::sqrt(2.0f); + float span = 2.0f * unitRadius; + float xres = span / static_cast(imageWidth); + float yres = span / static_cast(imageHeight); + + int halfWidth = imageWidth / 2; + int halfHeight = imageHeight / 2; + + for(int y = 0; y < imageHeight; y++) + { + for(int x = 0; x < imageWidth; x++) + { + int index = y * imageWidth + x; + + float xtmp = static_cast(x - halfWidth) * xres + (xres * 0.5f); + float ytmp = static_cast(y - halfHeight) * yres + (yres * 0.5f); + + float rhoSq = xtmp * xtmp + ytmp * ytmp; + + if(rhoSq > 2.0f) + { + intensityPtr[index] = -1.0; + continue; + } + + float t = std::sqrt(1.0f - rhoSq / 4.0f); + std::array xyz = {xtmp * t, ytmp * t, 1.0f - rhoSq / 2.0f}; + + double chi = std::acos(static_cast(xyz[2])); + double eta = std::atan2(static_cast(xyz[1]), static_cast(xyz[0])); + + if(!ops.inUnitTriangle(eta, chi)) + { + intensityPtr[index] = -1.0; + continue; + } + + std::array sqCoord = {0.0f, 0.0f}; + bool isNorth = lambert->getSquareCoord(xyz.data(), sqCoord.data()); + if(isNorth) + { + intensityPtr[index] = lambert->getInterpolatedValue(ModifiedLambertProjection::NorthSquare, sqCoord.data()); + } + else + { + intensityPtr[index] = lambert->getInterpolatedValue(ModifiedLambertProjection::SouthSquare, sqCoord.data()); + } + } + } + } + + return intensity; +``` + +- [ ] **Step 4: Build the library** + +```bash +cd /Users/mjackson/Workspace1/DREAM3D-Build/EbsdLib-Release && cmake --build . --target EbsdLib 2>&1 | tail -10 +``` +Expected: Clean build. The default parameter means all existing callers still work unchanged. + +- [ ] **Step 5: Commit** + +```bash +cd /Users/mjackson/Workspace1/EbsdLib +git add Source/EbsdLib/Utilities/InversePoleFigureUtilities.h Source/EbsdLib/Utilities/InversePoleFigureUtilities.cpp +git commit -m "ENH: Add SST bounding box parameter to computeIPFIntensity for zoomed density" +``` + +--- + +### Task 2: Update `generateAnnotatedIPFDensity()` to pass the bounding box + +**Files:** +- Modify: `Source/EbsdLib/LaueOps/LaueOps.cpp` + +- [ ] **Step 1: Compute the SST bounding box and pass it to `computeIPFIntensity()`** + +In `LaueOps.cpp`, in the `generateAnnotatedIPFDensity()` method, find the three calls to `computeIPFIntensity()` (around line 1154-1156). Before those calls, add code to compute the bounding box from the existing `getIpfColorAngleLimits()` method: + +```cpp + // Compute SST bounding box for zoomed density images + // getIpfColorAngleLimits returns {etaMin, etaMax, chiMax(eta)} in radians. + // For cubic classes, chiMax varies with eta; use max value at etaMax. + auto angleLimits = getIpfColorAngleLimits(0.0); // Get etaMin, etaMax + double etaMin = angleLimits[0]; + double etaMax = angleLimits[1]; + + // Get chiMax at etaMax (gives the largest chiMax for cubic; same for others) + auto angleLimitsAtMax = getIpfColorAngleLimits(etaMax); + double chiMax = angleLimitsAtMax[2]; + + // Also check chiMax at etaMin in case it's larger (shouldn't be, but safe) + auto angleLimitsAtMin = getIpfColorAngleLimits(etaMin); + if(angleLimitsAtMin[2] > chiMax) + { + chiMax = angleLimitsAtMin[2]; + } + + std::array sstBBox = {etaMin, etaMax, 0.0, chiMax}; +``` + +Then update the three `computeIPFIntensity` calls to pass `&sstBBox`: + +```cpp + ebsdlib::DoubleArrayType::Pointer intensity0 = InversePoleFigureUtilities::computeIPFIntensity(*this, dirs0.get(), imageDim, imageDim, config.lambertDim, config.normalizeMRD, &sstBBox); + ebsdlib::DoubleArrayType::Pointer intensity1 = InversePoleFigureUtilities::computeIPFIntensity(*this, dirs1.get(), imageDim, imageDim, config.lambertDim, config.normalizeMRD, &sstBBox); + ebsdlib::DoubleArrayType::Pointer intensity2 = InversePoleFigureUtilities::computeIPFIntensity(*this, dirs2.get(), imageDim, imageDim, config.lambertDim, config.normalizeMRD, &sstBBox); +``` + +- [ ] **Step 2: Build everything** + +```bash +cd /Users/mjackson/Workspace1/DREAM3D-Build/EbsdLib-Release && cmake --build . --target all 2>&1 | tail -10 +``` +Expected: Clean build. + +- [ ] **Step 3: Test with the Iron BCC CTF file** + +```bash +cd /Users/mjackson/Workspace1/DREAM3D-Build/EbsdLib-Release && ./Bin/generate_ipf_from_file "/Users/mjackson/Applications/NXData/Data/T12-MAI-2010/fw-ar-IF1-aptr12-corr.ctf" /tmp/ipf_sst_zoomed 2>&1 +``` + +Convert the output to PNG and visually inspect. The SST triangle should now fill the frame, with labels aligned to the corners. + +- [ ] **Step 4: Test with generate_ipf_density (random orientations)** + +```bash +cd /Users/mjackson/Workspace1/DREAM3D-Build/EbsdLib-Release && ./Bin/generate_ipf_density /tmp/ipf_density_zoomed 500 2>&1 +``` + +Check output for all 11 Laue classes — each should have a well-filled triangle. + +- [ ] **Step 5: Run all unit tests** + +```bash +cd /Users/mjackson/Workspace1/DREAM3D-Build/EbsdLib-Release && ctest -R "EbsdLib::" 2>&1 | tail -5 +``` +Expected: All 296 tests pass. + +- [ ] **Step 6: Commit** + +```bash +cd /Users/mjackson/Workspace1/EbsdLib +git add Source/EbsdLib/LaueOps/LaueOps.cpp +git commit -m "ENH: Pass SST bounding box to computeIPFIntensity for zoomed density images" +``` + +--- + +## Important Notes + +### Negative eta ranges (TrigonalOps, TrigonalLowOps) + +TrigonalOps has etaMin=-90°, etaMax=-30°. TrigonalLowOps has etaMin=-120°, etaMax=0°. The pixel mapping formula `eta = etaMin + fraction * (etaMax - etaMin)` handles negative ranges correctly since it's a simple linear interpolation. The `inUnitTriangle` check will correctly identify which pixels are inside the SST. + +### Cubic dynamic chiMax + +For CubicOps and CubicLowOps, the SST has a curved upper boundary where chiMax varies with eta. The bounding box uses the maximum chiMax (at eta=45° for CubicHigh). Pixels inside the bounding box but outside the curved boundary will have `inUnitTriangle` return false and render as white — identical to how the legend handles it. + +### Backward compatibility + +The `computeIPFIntensity()` change uses a default parameter (`nullptr`). All existing callers (including the non-annotated `generateInversePoleFigure()`) continue to work unchanged with the full-disk mapping. + +### No changes to annotation positioning + +The `annotateIPFImage()` method and `drawIPFAnnotations()` overrides already place Miller index labels at positions relative to the canvas. Since the density image now fills the same region as the legend, the labels should align correctly with the triangle corners. From 8cd87e0bcbe3e2cfdc3cc2d1b5ec3977d0956b66 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Tue, 24 Mar 2026 12:05:41 -0400 Subject: [PATCH 08/14] ENH: Add SST-zoomed mapping to computeIPFIntensity for properly filled density images Co-Authored-By: Claude Opus 4.6 (1M context) --- Source/EbsdLib/LaueOps/LaueOps.cpp | 74 +++++---- .../Utilities/InversePoleFigureUtilities.cpp | 147 ++++++++++++------ .../Utilities/InversePoleFigureUtilities.h | 28 ++-- 3 files changed, 149 insertions(+), 100 deletions(-) diff --git a/Source/EbsdLib/LaueOps/LaueOps.cpp b/Source/EbsdLib/LaueOps/LaueOps.cpp index 506eaee..a5e3b3b 100644 --- a/Source/EbsdLib/LaueOps/LaueOps.cpp +++ b/Source/EbsdLib/LaueOps/LaueOps.cpp @@ -57,6 +57,7 @@ #include #include // for std::max +#include #include #include #include @@ -943,29 +944,21 @@ ebsdlib::Rgb LaueOps::generateMisorientationColor(const QuatD& q, const QuatD& r } // ----------------------------------------------------------------------------- -std::array LaueOps::adjustFigureOrigin( - std::array figureOrigin, - int legendWidth, int legendHeight, - const std::vector& margins, float fontPtSize, - bool generateEntirePlane) const +std::array LaueOps::adjustFigureOrigin(std::array figureOrigin, int legendWidth, int legendHeight, const std::vector& margins, float fontPtSize, + bool generateEntirePlane) const { return figureOrigin; } // ----------------------------------------------------------------------------- -UInt8ArrayType::Pointer LaueOps::annotateIPFImage( - UInt8ArrayType::Pointer triangleImage, - int imageDim, - int canvasDim, - const std::string& title, - bool generateEntirePlane) const +UInt8ArrayType::Pointer LaueOps::annotateIPFImage(UInt8ArrayType::Pointer triangleImage, int imageDim, int canvasDim, const std::string& title, bool generateEntirePlane) const { const float fontPtSize = static_cast(canvasDim) / 24.0f; const std::vector margins = { - fontPtSize * 3, // Top - static_cast(canvasDim / 7.0f), // Right - fontPtSize * 2, // Bottom - static_cast(canvasDim / 7.0f) // Left + fontPtSize * 3, // Top + static_cast(canvasDim / 7.0f), // Right + fontPtSize * 2, // Bottom + static_cast(canvasDim / 7.0f) // Left }; int legendHeight = canvasDim - static_cast(margins[0]) - static_cast(margins[2]); @@ -1013,10 +1006,7 @@ UInt8ArrayType::Pointer LaueOps::annotateIPFImage( context.fill(); // Draw the triangle image onto the canvas - context.draw_image(image->getPointer(0), imageDim, imageDim, - imageDim * image->getNumberOfComponents(), - figureOrigin[0], figureOrigin[1], - static_cast(legendWidth), + context.draw_image(image->getPointer(0), imageDim, imageDim, imageDim * image->getNumberOfComponents(), figureOrigin[0], figureOrigin[1], static_cast(legendWidth), static_cast(legendHeight)); // Draw title @@ -1028,20 +1018,14 @@ UInt8ArrayType::Pointer LaueOps::annotateIPFImage( drawIPFAnnotations(context, canvasDim, fontPtSize, margins, figureOrigin, figureCenter, generateEntirePlane); // Extract rendered pixels and remove alpha channel - ebsdlib::UInt8ArrayType::Pointer rgbaCanvasImage = ebsdlib::UInt8ArrayType::CreateArray( - canvasDim * canvasDim, {4ULL}, "Annotated IPF", true); + ebsdlib::UInt8ArrayType::Pointer rgbaCanvasImage = ebsdlib::UInt8ArrayType::CreateArray(canvasDim * canvasDim, {4ULL}, "Annotated IPF", true); context.get_image_data(rgbaCanvasImage->getPointer(0), canvasDim, canvasDim, canvasDim * 4, 0, 0); return ebsdlib::RemoveAlphaChannel(rgbaCanvasImage.get()); } // ----------------------------------------------------------------------------- -UInt8ArrayType::Pointer LaueOps::drawColorBar( - UInt8ArrayType::Pointer image, - int canvasDim, - int numColors, - double minValue, double maxValue, - bool isMRD) const +UInt8ArrayType::Pointer LaueOps::drawColorBar(UInt8ArrayType::Pointer image, int canvasDim, int numColors, double minValue, double maxValue, bool isMRD) const { const float fontPtSize = static_cast(canvasDim) / 24.0f; @@ -1064,10 +1048,7 @@ UInt8ArrayType::Pointer LaueOps::drawColorBar( canvas_ity::canvas context(canvasDim, canvasDim); // Put the existing image onto the canvas - context.draw_image(rgbaImage->getPointer(0), canvasDim, canvasDim, - canvasDim * 4, 0.0f, 0.0f, - static_cast(canvasDim), - static_cast(canvasDim)); + context.draw_image(rgbaImage->getPointer(0), canvasDim, canvasDim, canvasDim * 4, 0.0f, 0.0f, static_cast(canvasDim), static_cast(canvasDim)); // Color bar dimensions const float barLeft = static_cast(canvasDim) * 0.80f; @@ -1137,9 +1118,7 @@ UInt8ArrayType::Pointer LaueOps::drawColorBar( } // ----------------------------------------------------------------------------- -std::vector LaueOps::generateAnnotatedIPFDensity( - InversePoleFigureConfiguration_t& config, - std::pair* outMinMax) const +std::vector LaueOps::generateAnnotatedIPFDensity(InversePoleFigureConfiguration_t& config, std::pair* outMinMax) const { // Validate square images if(config.imageWidth != config.imageHeight) @@ -1172,10 +1151,29 @@ std::vector LaueOps::generateAnnotatedIPFDensity( ebsdlib::FloatArrayType::Pointer dirs1 = InversePoleFigureUtilities::computeIPFDirections(*this, config.eulers, config.sampleDirections[1]); ebsdlib::FloatArrayType::Pointer dirs2 = InversePoleFigureUtilities::computeIPFDirections(*this, config.eulers, config.sampleDirections[2]); - // Step 2: Compute intensity images - ebsdlib::DoubleArrayType::Pointer intensity0 = InversePoleFigureUtilities::computeIPFIntensity(*this, dirs0.get(), imageDim, imageDim, config.lambertDim, config.normalizeMRD); - ebsdlib::DoubleArrayType::Pointer intensity1 = InversePoleFigureUtilities::computeIPFIntensity(*this, dirs1.get(), imageDim, imageDim, config.lambertDim, config.normalizeMRD); - ebsdlib::DoubleArrayType::Pointer intensity2 = InversePoleFigureUtilities::computeIPFIntensity(*this, dirs2.get(), imageDim, imageDim, config.lambertDim, config.normalizeMRD); + // Step 2: Compute SST bounding box for zoomed density images + // getIpfColorAngleLimits returns {etaMin, etaMax, chiMax(eta)} in radians. + auto angleLimits = getIpfColorAngleLimits(0.0); + double etaMin = angleLimits[0]; + double etaMax = angleLimits[1]; + + // Get chiMax at etaMax (gives the largest chiMax for cubic; same for others) + auto angleLimitsAtMax = getIpfColorAngleLimits(etaMax); + double chiMax = angleLimitsAtMax[2]; + + // Also check chiMax at etaMin in case it's larger + auto angleLimitsAtMin = getIpfColorAngleLimits(etaMin); + if(angleLimitsAtMin[2] > chiMax) + { + chiMax = angleLimitsAtMin[2]; + } + + std::array sstBBox = {etaMin, etaMax, 0.0, chiMax}; + + // Compute intensity images with SST-zoomed mapping + ebsdlib::DoubleArrayType::Pointer intensity0 = InversePoleFigureUtilities::computeIPFIntensity(*this, dirs0.get(), imageDim, imageDim, config.lambertDim, config.normalizeMRD, &sstBBox); + ebsdlib::DoubleArrayType::Pointer intensity1 = InversePoleFigureUtilities::computeIPFIntensity(*this, dirs1.get(), imageDim, imageDim, config.lambertDim, config.normalizeMRD, &sstBBox); + ebsdlib::DoubleArrayType::Pointer intensity2 = InversePoleFigureUtilities::computeIPFIntensity(*this, dirs2.get(), imageDim, imageDim, config.lambertDim, config.normalizeMRD, &sstBBox); // Step 3: Find global min/max double globalMax = std::numeric_limits::lowest(); diff --git a/Source/EbsdLib/Utilities/InversePoleFigureUtilities.cpp b/Source/EbsdLib/Utilities/InversePoleFigureUtilities.cpp index 3641abf..2619105 100644 --- a/Source/EbsdLib/Utilities/InversePoleFigureUtilities.cpp +++ b/Source/EbsdLib/Utilities/InversePoleFigureUtilities.cpp @@ -137,7 +137,7 @@ ebsdlib::FloatArrayType::Pointer InversePoleFigureUtilities::computeIPFDirection // ----------------------------------------------------------------------------- ebsdlib::DoubleArrayType::Pointer InversePoleFigureUtilities::computeIPFIntensity(const LaueOps& ops, ebsdlib::FloatArrayType* ipfDirections, int imageWidth, int imageHeight, int lambertDim, - bool normalizeMRD) + bool normalizeMRD, const std::array* sstBoundingBox) { // Step 1: Bin the crystal directions into the Lambert projection float sphereRadius = 1.0f; @@ -169,66 +169,112 @@ ebsdlib::DoubleArrayType::Pointer InversePoleFigureUtilities::computeIPFIntensit // If not MRD, leave as raw counts } - // Step 3: Create the output intensity image using equal-area projection + // Step 3: Create the output intensity image std::vector tDims = {static_cast(imageWidth * imageHeight)}; std::vector cDims = {1}; ebsdlib::DoubleArrayType::Pointer intensity = ebsdlib::DoubleArrayType::CreateArray(tDims, cDims, "IPF_Intensity", true); double* intensityPtr = intensity->getPointer(0); - // Lambert azimuthal equal-area projection centered on north pole - // Maps the upper hemisphere (z >= 0) to a disk of radius sqrt(2) - float unitRadius = std::sqrt(2.0f); - float span = 2.0f * unitRadius; - float xres = span / static_cast(imageWidth); - float yres = span / static_cast(imageHeight); - - int halfWidth = imageWidth / 2; - int halfHeight = imageHeight / 2; - - for(int y = 0; y < imageHeight; y++) + if(sstBoundingBox != nullptr) { - for(int x = 0; x < imageWidth; x++) - { - int index = y * imageWidth + x; - - // Map pixel to equal-area projection coordinates - float xtmp = static_cast(x - halfWidth) * xres + (xres * 0.5f); - float ytmp = static_cast(y - halfHeight) * yres + (yres * 0.5f); + // SST-zoomed mode: map pixels directly to (eta, chi) within the bounding box + double etaMin = (*sstBoundingBox)[0]; + double etaMax = (*sstBoundingBox)[1]; + double chiMin = (*sstBoundingBox)[2]; + double chiMax = (*sstBoundingBox)[3]; - float rhoSq = xtmp * xtmp + ytmp * ytmp; - - // Check if within hemisphere disk - if(rhoSq > 2.0f) + for(int y = 0; y < imageHeight; y++) + { + for(int x = 0; x < imageWidth; x++) { - intensityPtr[index] = -1.0; // Outside hemisphere - continue; + int index = y * imageWidth + x; + + // Map pixel to (eta, chi) within the bounding box + // x maps to eta (left=etaMin, right=etaMax) + // y maps to chi (top=chiMin, bottom=chiMax) + double eta = etaMin + (static_cast(x) + 0.5) / static_cast(imageWidth) * (etaMax - etaMin); + double chi = chiMin + (static_cast(y) + 0.5) / static_cast(imageHeight) * (chiMax - chiMin); + + if(!ops.inUnitTriangle(eta, chi)) + { + intensityPtr[index] = -1.0; + continue; + } + + // Convert (eta, chi) to unit sphere xyz + double sinChi = std::sin(chi); + std::array xyz = {static_cast(sinChi * std::cos(eta)), static_cast(sinChi * std::sin(eta)), static_cast(std::cos(chi))}; + + // Look up intensity from Lambert bins + std::array sqCoord = {0.0f, 0.0f}; + bool isNorth = lambert->getSquareCoord(xyz.data(), sqCoord.data()); + if(isNorth) + { + intensityPtr[index] = lambert->getInterpolatedValue(ModifiedLambertProjection::NorthSquare, sqCoord.data()); + } + else + { + intensityPtr[index] = lambert->getInterpolatedValue(ModifiedLambertProjection::SouthSquare, sqCoord.data()); + } } + } + } + else + { + // Default full Lambert hemisphere disk mapping + float unitRadius = std::sqrt(2.0f); + float span = 2.0f * unitRadius; + float xres = span / static_cast(imageWidth); + float yres = span / static_cast(imageHeight); - // Inverse Lambert azimuthal equal-area projection (north pole centered) - float t = std::sqrt(1.0f - rhoSq / 4.0f); - std::array xyz = {xtmp * t, ytmp * t, 1.0f - rhoSq / 2.0f}; - - // Compute chi (polar angle from z-axis) and eta (azimuthal angle) - double chi = std::acos(static_cast(xyz[2])); - double eta = std::atan2(static_cast(xyz[1]), static_cast(xyz[0])); - - // Check if direction is inside the Standard Stereographic Triangle - if(!ops.inUnitTriangle(eta, chi)) - { - intensityPtr[index] = -1.0; // Outside SST - continue; - } + int halfWidth = imageWidth / 2; + int halfHeight = imageHeight / 2; - // Look up the interpolated intensity from the Lambert projection - std::array sqCoord = {0.0f, 0.0f}; - bool isNorth = lambert->getSquareCoord(xyz.data(), sqCoord.data()); - if(isNorth) - { - intensityPtr[index] = lambert->getInterpolatedValue(ModifiedLambertProjection::NorthSquare, sqCoord.data()); - } - else + for(int y = 0; y < imageHeight; y++) + { + for(int x = 0; x < imageWidth; x++) { - intensityPtr[index] = lambert->getInterpolatedValue(ModifiedLambertProjection::SouthSquare, sqCoord.data()); + int index = y * imageWidth + x; + + // Map pixel to equal-area projection coordinates + float xtmp = static_cast(x - halfWidth) * xres + (xres * 0.5f); + float ytmp = static_cast(y - halfHeight) * yres + (yres * 0.5f); + + float rhoSq = xtmp * xtmp + ytmp * ytmp; + + // Check if within hemisphere disk + if(rhoSq > 2.0f) + { + intensityPtr[index] = -1.0; // Outside hemisphere + continue; + } + + // Inverse Lambert azimuthal equal-area projection (north pole centered) + float t = std::sqrt(1.0f - rhoSq / 4.0f); + std::array xyz = {xtmp * t, ytmp * t, 1.0f - rhoSq / 2.0f}; + + // Compute chi (polar angle from z-axis) and eta (azimuthal angle) + double chi = std::acos(static_cast(xyz[2])); + double eta = std::atan2(static_cast(xyz[1]), static_cast(xyz[0])); + + // Check if direction is inside the Standard Stereographic Triangle + if(!ops.inUnitTriangle(eta, chi)) + { + intensityPtr[index] = -1.0; // Outside SST + continue; + } + + // Look up the interpolated intensity from the Lambert projection + std::array sqCoord = {0.0f, 0.0f}; + bool isNorth = lambert->getSquareCoord(xyz.data(), sqCoord.data()); + if(isNorth) + { + intensityPtr[index] = lambert->getInterpolatedValue(ModifiedLambertProjection::NorthSquare, sqCoord.data()); + } + else + { + intensityPtr[index] = lambert->getInterpolatedValue(ModifiedLambertProjection::SouthSquare, sqCoord.data()); + } } } } @@ -237,7 +283,8 @@ ebsdlib::DoubleArrayType::Pointer InversePoleFigureUtilities::computeIPFIntensit } // ----------------------------------------------------------------------------- -void InversePoleFigureUtilities::createIPFColorImage(ebsdlib::DoubleArrayType* intensity, int imageWidth, int imageHeight, int numColors, double minScale, double maxScale, ebsdlib::UInt8ArrayType* rgba) +void InversePoleFigureUtilities::createIPFColorImage(ebsdlib::DoubleArrayType* intensity, int imageWidth, int imageHeight, int numColors, double minScale, double maxScale, + ebsdlib::UInt8ArrayType* rgba) { // Initialize the image with all zeros rgba->initializeWithZeros(); diff --git a/Source/EbsdLib/Utilities/InversePoleFigureUtilities.h b/Source/EbsdLib/Utilities/InversePoleFigureUtilities.h index 5d462ac..5a6e973 100644 --- a/Source/EbsdLib/Utilities/InversePoleFigureUtilities.h +++ b/Source/EbsdLib/Utilities/InversePoleFigureUtilities.h @@ -56,17 +56,17 @@ class LaueOps; // Forward declaration */ struct InversePoleFigureConfiguration_t { - ebsdlib::FloatArrayType* eulers; ///<* The Euler Angles (in Radians) to use for the inverse pole figure - std::array sampleDirections; ///<* 3 orthogonal sample reference directions (e.g., RD, TD, ND) - int imageWidth; ///<* The width of the generated inverse pole figure image in pixels - int imageHeight; ///<* The height of the generated inverse pole figure image in pixels - int lambertDim; ///<* The dimensions in voxels of the Lambert Square used for binning/smoothing - int numColors; ///<* The number of colors to use in the color map - std::string colorMap; ///<* Name of the ColorMap to use - bool normalizeMRD; ///<* true=normalize to MRD (Multiples of Random Distribution), false=raw counts - std::vector labels; ///<* The labels for each of the 3 inverse pole figures (e.g., "RD", "TD", "ND") - std::string phaseName; ///<* The name of the phase - bool FlipFinalImage; ///<* If TRUE, the final image will be flipped across the X Axis so that +Y axis points UP + ebsdlib::FloatArrayType* eulers; ///<* The Euler Angles (in Radians) to use for the inverse pole figure + std::array sampleDirections; ///<* 3 orthogonal sample reference directions (e.g., RD, TD, ND) + int imageWidth; ///<* The width of the generated inverse pole figure image in pixels + int imageHeight; ///<* The height of the generated inverse pole figure image in pixels + int lambertDim; ///<* The dimensions in voxels of the Lambert Square used for binning/smoothing + int numColors; ///<* The number of colors to use in the color map + std::string colorMap; ///<* Name of the ColorMap to use + bool normalizeMRD; ///<* true=normalize to MRD (Multiples of Random Distribution), false=raw counts + std::vector labels; ///<* The labels for each of the 3 inverse pole figures (e.g., "RD", "TD", "ND") + std::string phaseName; ///<* The name of the phase + bool FlipFinalImage; ///<* If TRUE, the final image will be flipped across the X Axis so that +Y axis points UP }; /** @@ -104,9 +104,13 @@ class EbsdLib_EXPORT InversePoleFigureUtilities * @param imageHeight Output image height in pixels * @param lambertDim Lambert square dimension for binning/smoothing * @param normalizeMRD true to normalize to MRD, false for raw counts + * @param sstBoundingBox Optional SST bounding box {etaMin, etaMax, chiMin, chiMax} in radians. + * When provided, pixels map to only this region in (eta, chi) space, making the SST fill the image. + * When nullptr, uses the default full Lambert hemisphere disk mapping. * @return DoubleArrayType intensity image (imageWidth * imageHeight). Pixels outside SST have value -1.0. */ - static ebsdlib::DoubleArrayType::Pointer computeIPFIntensity(const LaueOps& ops, ebsdlib::FloatArrayType* ipfDirections, int imageWidth, int imageHeight, int lambertDim, bool normalizeMRD); + static ebsdlib::DoubleArrayType::Pointer computeIPFIntensity(const LaueOps& ops, ebsdlib::FloatArrayType* ipfDirections, int imageWidth, int imageHeight, int lambertDim, bool normalizeMRD, + const std::array* sstBoundingBox = nullptr); /** * @brief Converts an intensity image to RGBA with SST masking. Pixels inside the SST From 8152e9b16178c8375b6f414fa7d80e1e997b8f40 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Tue, 24 Mar 2026 12:11:24 -0400 Subject: [PATCH 09/14] Revert "ENH: Add SST-zoomed mapping to computeIPFIntensity for properly filled density images" This reverts commit 238756a9f5526b4dbb2c7302385712da4b8a8c73. --- Source/EbsdLib/LaueOps/LaueOps.cpp | 74 ++++----- .../Utilities/InversePoleFigureUtilities.cpp | 147 ++++++------------ .../Utilities/InversePoleFigureUtilities.h | 28 ++-- 3 files changed, 100 insertions(+), 149 deletions(-) diff --git a/Source/EbsdLib/LaueOps/LaueOps.cpp b/Source/EbsdLib/LaueOps/LaueOps.cpp index a5e3b3b..506eaee 100644 --- a/Source/EbsdLib/LaueOps/LaueOps.cpp +++ b/Source/EbsdLib/LaueOps/LaueOps.cpp @@ -57,7 +57,6 @@ #include #include // for std::max -#include #include #include #include @@ -944,21 +943,29 @@ ebsdlib::Rgb LaueOps::generateMisorientationColor(const QuatD& q, const QuatD& r } // ----------------------------------------------------------------------------- -std::array LaueOps::adjustFigureOrigin(std::array figureOrigin, int legendWidth, int legendHeight, const std::vector& margins, float fontPtSize, - bool generateEntirePlane) const +std::array LaueOps::adjustFigureOrigin( + std::array figureOrigin, + int legendWidth, int legendHeight, + const std::vector& margins, float fontPtSize, + bool generateEntirePlane) const { return figureOrigin; } // ----------------------------------------------------------------------------- -UInt8ArrayType::Pointer LaueOps::annotateIPFImage(UInt8ArrayType::Pointer triangleImage, int imageDim, int canvasDim, const std::string& title, bool generateEntirePlane) const +UInt8ArrayType::Pointer LaueOps::annotateIPFImage( + UInt8ArrayType::Pointer triangleImage, + int imageDim, + int canvasDim, + const std::string& title, + bool generateEntirePlane) const { const float fontPtSize = static_cast(canvasDim) / 24.0f; const std::vector margins = { - fontPtSize * 3, // Top - static_cast(canvasDim / 7.0f), // Right - fontPtSize * 2, // Bottom - static_cast(canvasDim / 7.0f) // Left + fontPtSize * 3, // Top + static_cast(canvasDim / 7.0f), // Right + fontPtSize * 2, // Bottom + static_cast(canvasDim / 7.0f) // Left }; int legendHeight = canvasDim - static_cast(margins[0]) - static_cast(margins[2]); @@ -1006,7 +1013,10 @@ UInt8ArrayType::Pointer LaueOps::annotateIPFImage(UInt8ArrayType::Pointer triang context.fill(); // Draw the triangle image onto the canvas - context.draw_image(image->getPointer(0), imageDim, imageDim, imageDim * image->getNumberOfComponents(), figureOrigin[0], figureOrigin[1], static_cast(legendWidth), + context.draw_image(image->getPointer(0), imageDim, imageDim, + imageDim * image->getNumberOfComponents(), + figureOrigin[0], figureOrigin[1], + static_cast(legendWidth), static_cast(legendHeight)); // Draw title @@ -1018,14 +1028,20 @@ UInt8ArrayType::Pointer LaueOps::annotateIPFImage(UInt8ArrayType::Pointer triang drawIPFAnnotations(context, canvasDim, fontPtSize, margins, figureOrigin, figureCenter, generateEntirePlane); // Extract rendered pixels and remove alpha channel - ebsdlib::UInt8ArrayType::Pointer rgbaCanvasImage = ebsdlib::UInt8ArrayType::CreateArray(canvasDim * canvasDim, {4ULL}, "Annotated IPF", true); + ebsdlib::UInt8ArrayType::Pointer rgbaCanvasImage = ebsdlib::UInt8ArrayType::CreateArray( + canvasDim * canvasDim, {4ULL}, "Annotated IPF", true); context.get_image_data(rgbaCanvasImage->getPointer(0), canvasDim, canvasDim, canvasDim * 4, 0, 0); return ebsdlib::RemoveAlphaChannel(rgbaCanvasImage.get()); } // ----------------------------------------------------------------------------- -UInt8ArrayType::Pointer LaueOps::drawColorBar(UInt8ArrayType::Pointer image, int canvasDim, int numColors, double minValue, double maxValue, bool isMRD) const +UInt8ArrayType::Pointer LaueOps::drawColorBar( + UInt8ArrayType::Pointer image, + int canvasDim, + int numColors, + double minValue, double maxValue, + bool isMRD) const { const float fontPtSize = static_cast(canvasDim) / 24.0f; @@ -1048,7 +1064,10 @@ UInt8ArrayType::Pointer LaueOps::drawColorBar(UInt8ArrayType::Pointer image, int canvas_ity::canvas context(canvasDim, canvasDim); // Put the existing image onto the canvas - context.draw_image(rgbaImage->getPointer(0), canvasDim, canvasDim, canvasDim * 4, 0.0f, 0.0f, static_cast(canvasDim), static_cast(canvasDim)); + context.draw_image(rgbaImage->getPointer(0), canvasDim, canvasDim, + canvasDim * 4, 0.0f, 0.0f, + static_cast(canvasDim), + static_cast(canvasDim)); // Color bar dimensions const float barLeft = static_cast(canvasDim) * 0.80f; @@ -1118,7 +1137,9 @@ UInt8ArrayType::Pointer LaueOps::drawColorBar(UInt8ArrayType::Pointer image, int } // ----------------------------------------------------------------------------- -std::vector LaueOps::generateAnnotatedIPFDensity(InversePoleFigureConfiguration_t& config, std::pair* outMinMax) const +std::vector LaueOps::generateAnnotatedIPFDensity( + InversePoleFigureConfiguration_t& config, + std::pair* outMinMax) const { // Validate square images if(config.imageWidth != config.imageHeight) @@ -1151,29 +1172,10 @@ std::vector LaueOps::generateAnnotatedIPFDensity(Invers ebsdlib::FloatArrayType::Pointer dirs1 = InversePoleFigureUtilities::computeIPFDirections(*this, config.eulers, config.sampleDirections[1]); ebsdlib::FloatArrayType::Pointer dirs2 = InversePoleFigureUtilities::computeIPFDirections(*this, config.eulers, config.sampleDirections[2]); - // Step 2: Compute SST bounding box for zoomed density images - // getIpfColorAngleLimits returns {etaMin, etaMax, chiMax(eta)} in radians. - auto angleLimits = getIpfColorAngleLimits(0.0); - double etaMin = angleLimits[0]; - double etaMax = angleLimits[1]; - - // Get chiMax at etaMax (gives the largest chiMax for cubic; same for others) - auto angleLimitsAtMax = getIpfColorAngleLimits(etaMax); - double chiMax = angleLimitsAtMax[2]; - - // Also check chiMax at etaMin in case it's larger - auto angleLimitsAtMin = getIpfColorAngleLimits(etaMin); - if(angleLimitsAtMin[2] > chiMax) - { - chiMax = angleLimitsAtMin[2]; - } - - std::array sstBBox = {etaMin, etaMax, 0.0, chiMax}; - - // Compute intensity images with SST-zoomed mapping - ebsdlib::DoubleArrayType::Pointer intensity0 = InversePoleFigureUtilities::computeIPFIntensity(*this, dirs0.get(), imageDim, imageDim, config.lambertDim, config.normalizeMRD, &sstBBox); - ebsdlib::DoubleArrayType::Pointer intensity1 = InversePoleFigureUtilities::computeIPFIntensity(*this, dirs1.get(), imageDim, imageDim, config.lambertDim, config.normalizeMRD, &sstBBox); - ebsdlib::DoubleArrayType::Pointer intensity2 = InversePoleFigureUtilities::computeIPFIntensity(*this, dirs2.get(), imageDim, imageDim, config.lambertDim, config.normalizeMRD, &sstBBox); + // Step 2: Compute intensity images + ebsdlib::DoubleArrayType::Pointer intensity0 = InversePoleFigureUtilities::computeIPFIntensity(*this, dirs0.get(), imageDim, imageDim, config.lambertDim, config.normalizeMRD); + ebsdlib::DoubleArrayType::Pointer intensity1 = InversePoleFigureUtilities::computeIPFIntensity(*this, dirs1.get(), imageDim, imageDim, config.lambertDim, config.normalizeMRD); + ebsdlib::DoubleArrayType::Pointer intensity2 = InversePoleFigureUtilities::computeIPFIntensity(*this, dirs2.get(), imageDim, imageDim, config.lambertDim, config.normalizeMRD); // Step 3: Find global min/max double globalMax = std::numeric_limits::lowest(); diff --git a/Source/EbsdLib/Utilities/InversePoleFigureUtilities.cpp b/Source/EbsdLib/Utilities/InversePoleFigureUtilities.cpp index 2619105..3641abf 100644 --- a/Source/EbsdLib/Utilities/InversePoleFigureUtilities.cpp +++ b/Source/EbsdLib/Utilities/InversePoleFigureUtilities.cpp @@ -137,7 +137,7 @@ ebsdlib::FloatArrayType::Pointer InversePoleFigureUtilities::computeIPFDirection // ----------------------------------------------------------------------------- ebsdlib::DoubleArrayType::Pointer InversePoleFigureUtilities::computeIPFIntensity(const LaueOps& ops, ebsdlib::FloatArrayType* ipfDirections, int imageWidth, int imageHeight, int lambertDim, - bool normalizeMRD, const std::array* sstBoundingBox) + bool normalizeMRD) { // Step 1: Bin the crystal directions into the Lambert projection float sphereRadius = 1.0f; @@ -169,112 +169,66 @@ ebsdlib::DoubleArrayType::Pointer InversePoleFigureUtilities::computeIPFIntensit // If not MRD, leave as raw counts } - // Step 3: Create the output intensity image + // Step 3: Create the output intensity image using equal-area projection std::vector tDims = {static_cast(imageWidth * imageHeight)}; std::vector cDims = {1}; ebsdlib::DoubleArrayType::Pointer intensity = ebsdlib::DoubleArrayType::CreateArray(tDims, cDims, "IPF_Intensity", true); double* intensityPtr = intensity->getPointer(0); - if(sstBoundingBox != nullptr) - { - // SST-zoomed mode: map pixels directly to (eta, chi) within the bounding box - double etaMin = (*sstBoundingBox)[0]; - double etaMax = (*sstBoundingBox)[1]; - double chiMin = (*sstBoundingBox)[2]; - double chiMax = (*sstBoundingBox)[3]; + // Lambert azimuthal equal-area projection centered on north pole + // Maps the upper hemisphere (z >= 0) to a disk of radius sqrt(2) + float unitRadius = std::sqrt(2.0f); + float span = 2.0f * unitRadius; + float xres = span / static_cast(imageWidth); + float yres = span / static_cast(imageHeight); + + int halfWidth = imageWidth / 2; + int halfHeight = imageHeight / 2; - for(int y = 0; y < imageHeight; y++) + for(int y = 0; y < imageHeight; y++) + { + for(int x = 0; x < imageWidth; x++) { - for(int x = 0; x < imageWidth; x++) + int index = y * imageWidth + x; + + // Map pixel to equal-area projection coordinates + float xtmp = static_cast(x - halfWidth) * xres + (xres * 0.5f); + float ytmp = static_cast(y - halfHeight) * yres + (yres * 0.5f); + + float rhoSq = xtmp * xtmp + ytmp * ytmp; + + // Check if within hemisphere disk + if(rhoSq > 2.0f) { - int index = y * imageWidth + x; - - // Map pixel to (eta, chi) within the bounding box - // x maps to eta (left=etaMin, right=etaMax) - // y maps to chi (top=chiMin, bottom=chiMax) - double eta = etaMin + (static_cast(x) + 0.5) / static_cast(imageWidth) * (etaMax - etaMin); - double chi = chiMin + (static_cast(y) + 0.5) / static_cast(imageHeight) * (chiMax - chiMin); - - if(!ops.inUnitTriangle(eta, chi)) - { - intensityPtr[index] = -1.0; - continue; - } - - // Convert (eta, chi) to unit sphere xyz - double sinChi = std::sin(chi); - std::array xyz = {static_cast(sinChi * std::cos(eta)), static_cast(sinChi * std::sin(eta)), static_cast(std::cos(chi))}; - - // Look up intensity from Lambert bins - std::array sqCoord = {0.0f, 0.0f}; - bool isNorth = lambert->getSquareCoord(xyz.data(), sqCoord.data()); - if(isNorth) - { - intensityPtr[index] = lambert->getInterpolatedValue(ModifiedLambertProjection::NorthSquare, sqCoord.data()); - } - else - { - intensityPtr[index] = lambert->getInterpolatedValue(ModifiedLambertProjection::SouthSquare, sqCoord.data()); - } + intensityPtr[index] = -1.0; // Outside hemisphere + continue; } - } - } - else - { - // Default full Lambert hemisphere disk mapping - float unitRadius = std::sqrt(2.0f); - float span = 2.0f * unitRadius; - float xres = span / static_cast(imageWidth); - float yres = span / static_cast(imageHeight); - int halfWidth = imageWidth / 2; - int halfHeight = imageHeight / 2; + // Inverse Lambert azimuthal equal-area projection (north pole centered) + float t = std::sqrt(1.0f - rhoSq / 4.0f); + std::array xyz = {xtmp * t, ytmp * t, 1.0f - rhoSq / 2.0f}; - for(int y = 0; y < imageHeight; y++) - { - for(int x = 0; x < imageWidth; x++) + // Compute chi (polar angle from z-axis) and eta (azimuthal angle) + double chi = std::acos(static_cast(xyz[2])); + double eta = std::atan2(static_cast(xyz[1]), static_cast(xyz[0])); + + // Check if direction is inside the Standard Stereographic Triangle + if(!ops.inUnitTriangle(eta, chi)) + { + intensityPtr[index] = -1.0; // Outside SST + continue; + } + + // Look up the interpolated intensity from the Lambert projection + std::array sqCoord = {0.0f, 0.0f}; + bool isNorth = lambert->getSquareCoord(xyz.data(), sqCoord.data()); + if(isNorth) + { + intensityPtr[index] = lambert->getInterpolatedValue(ModifiedLambertProjection::NorthSquare, sqCoord.data()); + } + else { - int index = y * imageWidth + x; - - // Map pixel to equal-area projection coordinates - float xtmp = static_cast(x - halfWidth) * xres + (xres * 0.5f); - float ytmp = static_cast(y - halfHeight) * yres + (yres * 0.5f); - - float rhoSq = xtmp * xtmp + ytmp * ytmp; - - // Check if within hemisphere disk - if(rhoSq > 2.0f) - { - intensityPtr[index] = -1.0; // Outside hemisphere - continue; - } - - // Inverse Lambert azimuthal equal-area projection (north pole centered) - float t = std::sqrt(1.0f - rhoSq / 4.0f); - std::array xyz = {xtmp * t, ytmp * t, 1.0f - rhoSq / 2.0f}; - - // Compute chi (polar angle from z-axis) and eta (azimuthal angle) - double chi = std::acos(static_cast(xyz[2])); - double eta = std::atan2(static_cast(xyz[1]), static_cast(xyz[0])); - - // Check if direction is inside the Standard Stereographic Triangle - if(!ops.inUnitTriangle(eta, chi)) - { - intensityPtr[index] = -1.0; // Outside SST - continue; - } - - // Look up the interpolated intensity from the Lambert projection - std::array sqCoord = {0.0f, 0.0f}; - bool isNorth = lambert->getSquareCoord(xyz.data(), sqCoord.data()); - if(isNorth) - { - intensityPtr[index] = lambert->getInterpolatedValue(ModifiedLambertProjection::NorthSquare, sqCoord.data()); - } - else - { - intensityPtr[index] = lambert->getInterpolatedValue(ModifiedLambertProjection::SouthSquare, sqCoord.data()); - } + intensityPtr[index] = lambert->getInterpolatedValue(ModifiedLambertProjection::SouthSquare, sqCoord.data()); } } } @@ -283,8 +237,7 @@ ebsdlib::DoubleArrayType::Pointer InversePoleFigureUtilities::computeIPFIntensit } // ----------------------------------------------------------------------------- -void InversePoleFigureUtilities::createIPFColorImage(ebsdlib::DoubleArrayType* intensity, int imageWidth, int imageHeight, int numColors, double minScale, double maxScale, - ebsdlib::UInt8ArrayType* rgba) +void InversePoleFigureUtilities::createIPFColorImage(ebsdlib::DoubleArrayType* intensity, int imageWidth, int imageHeight, int numColors, double minScale, double maxScale, ebsdlib::UInt8ArrayType* rgba) { // Initialize the image with all zeros rgba->initializeWithZeros(); diff --git a/Source/EbsdLib/Utilities/InversePoleFigureUtilities.h b/Source/EbsdLib/Utilities/InversePoleFigureUtilities.h index 5a6e973..5d462ac 100644 --- a/Source/EbsdLib/Utilities/InversePoleFigureUtilities.h +++ b/Source/EbsdLib/Utilities/InversePoleFigureUtilities.h @@ -56,17 +56,17 @@ class LaueOps; // Forward declaration */ struct InversePoleFigureConfiguration_t { - ebsdlib::FloatArrayType* eulers; ///<* The Euler Angles (in Radians) to use for the inverse pole figure - std::array sampleDirections; ///<* 3 orthogonal sample reference directions (e.g., RD, TD, ND) - int imageWidth; ///<* The width of the generated inverse pole figure image in pixels - int imageHeight; ///<* The height of the generated inverse pole figure image in pixels - int lambertDim; ///<* The dimensions in voxels of the Lambert Square used for binning/smoothing - int numColors; ///<* The number of colors to use in the color map - std::string colorMap; ///<* Name of the ColorMap to use - bool normalizeMRD; ///<* true=normalize to MRD (Multiples of Random Distribution), false=raw counts - std::vector labels; ///<* The labels for each of the 3 inverse pole figures (e.g., "RD", "TD", "ND") - std::string phaseName; ///<* The name of the phase - bool FlipFinalImage; ///<* If TRUE, the final image will be flipped across the X Axis so that +Y axis points UP + ebsdlib::FloatArrayType* eulers; ///<* The Euler Angles (in Radians) to use for the inverse pole figure + std::array sampleDirections; ///<* 3 orthogonal sample reference directions (e.g., RD, TD, ND) + int imageWidth; ///<* The width of the generated inverse pole figure image in pixels + int imageHeight; ///<* The height of the generated inverse pole figure image in pixels + int lambertDim; ///<* The dimensions in voxels of the Lambert Square used for binning/smoothing + int numColors; ///<* The number of colors to use in the color map + std::string colorMap; ///<* Name of the ColorMap to use + bool normalizeMRD; ///<* true=normalize to MRD (Multiples of Random Distribution), false=raw counts + std::vector labels; ///<* The labels for each of the 3 inverse pole figures (e.g., "RD", "TD", "ND") + std::string phaseName; ///<* The name of the phase + bool FlipFinalImage; ///<* If TRUE, the final image will be flipped across the X Axis so that +Y axis points UP }; /** @@ -104,13 +104,9 @@ class EbsdLib_EXPORT InversePoleFigureUtilities * @param imageHeight Output image height in pixels * @param lambertDim Lambert square dimension for binning/smoothing * @param normalizeMRD true to normalize to MRD, false for raw counts - * @param sstBoundingBox Optional SST bounding box {etaMin, etaMax, chiMin, chiMax} in radians. - * When provided, pixels map to only this region in (eta, chi) space, making the SST fill the image. - * When nullptr, uses the default full Lambert hemisphere disk mapping. * @return DoubleArrayType intensity image (imageWidth * imageHeight). Pixels outside SST have value -1.0. */ - static ebsdlib::DoubleArrayType::Pointer computeIPFIntensity(const LaueOps& ops, ebsdlib::FloatArrayType* ipfDirections, int imageWidth, int imageHeight, int lambertDim, bool normalizeMRD, - const std::array* sstBoundingBox = nullptr); + static ebsdlib::DoubleArrayType::Pointer computeIPFIntensity(const LaueOps& ops, ebsdlib::FloatArrayType* ipfDirections, int imageWidth, int imageHeight, int lambertDim, bool normalizeMRD); /** * @brief Converts an intensity image to RGBA with SST masking. Pixels inside the SST From d6ac8f06b344afaca3b415994e9780c068b2618f Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Tue, 24 Mar 2026 15:00:39 -0400 Subject: [PATCH 10/14] ENH: Add stereographic SST mapping for properly shaped IPF density images Add virtual mapPixelToSphereSST() method to LaueOps with per-subclass overrides that match the exact stereographic projection used by each CreateIPFLegend function. Update computeIPFIntensity with an optional useStereographicSST parameter so IPF density images use the same projection as the IPF legend, ensuring triangle shapes match. Co-Authored-By: Claude Opus 4.6 (1M context) --- Source/EbsdLib/LaueOps/CubicLowOps.cpp | 42 ++++-- Source/EbsdLib/LaueOps/CubicLowOps.h | 18 +-- Source/EbsdLib/LaueOps/CubicOps.cpp | 53 +++++-- Source/EbsdLib/LaueOps/CubicOps.h | 18 +-- Source/EbsdLib/LaueOps/HexagonalLowOps.cpp | 50 +++++-- Source/EbsdLib/LaueOps/HexagonalLowOps.h | 18 +-- Source/EbsdLib/LaueOps/HexagonalOps.cpp | 53 +++++-- Source/EbsdLib/LaueOps/HexagonalOps.h | 18 +-- Source/EbsdLib/LaueOps/LaueOps.cpp | 68 ++++----- Source/EbsdLib/LaueOps/LaueOps.h | 43 +++--- Source/EbsdLib/LaueOps/MonoclinicOps.cpp | 38 +++-- Source/EbsdLib/LaueOps/MonoclinicOps.h | 9 +- Source/EbsdLib/LaueOps/OrthoRhombicOps.cpp | 45 ++++-- Source/EbsdLib/LaueOps/OrthoRhombicOps.h | 18 +-- Source/EbsdLib/LaueOps/TetragonalLowOps.cpp | 45 ++++-- Source/EbsdLib/LaueOps/TetragonalLowOps.h | 18 +-- Source/EbsdLib/LaueOps/TetragonalOps.cpp | 45 ++++-- Source/EbsdLib/LaueOps/TetragonalOps.h | 18 +-- Source/EbsdLib/LaueOps/TriclinicOps.cpp | 33 +++-- Source/EbsdLib/LaueOps/TriclinicOps.h | 9 +- Source/EbsdLib/LaueOps/TrigonalLowOps.cpp | 53 +++++-- Source/EbsdLib/LaueOps/TrigonalLowOps.h | 18 +-- Source/EbsdLib/LaueOps/TrigonalOps.cpp | 53 +++++-- Source/EbsdLib/LaueOps/TrigonalOps.h | 18 +-- .../Utilities/InversePoleFigureUtilities.cpp | 135 +++++++++++------- .../Utilities/InversePoleFigureUtilities.h | 25 ++-- 26 files changed, 598 insertions(+), 363 deletions(-) diff --git a/Source/EbsdLib/LaueOps/CubicLowOps.cpp b/Source/EbsdLib/LaueOps/CubicLowOps.cpp index 8325063..e227c01 100644 --- a/Source/EbsdLib/LaueOps/CubicLowOps.cpp +++ b/Source/EbsdLib/LaueOps/CubicLowOps.cpp @@ -992,11 +992,36 @@ ebsdlib::UInt8ArrayType::Pointer CreateIPFLegend(const CubicLowOps* ops, int ima } // namespace // ----------------------------------------------------------------------------- -std::array CubicLowOps::adjustFigureOrigin( - std::array figureOrigin, - int legendWidth, int legendHeight, - const std::vector& margins, float fontPtSize, - bool generateEntirePlane) const +bool CubicLowOps::mapPixelToSphereSST(int xPixel, int yPixel, int imageDim, std::array& sphereDir) const +{ + double xInc = 1.0 / static_cast(imageDim); + double yInc = 1.0 / static_cast(imageDim); + + double x = 0.5 * static_cast(xPixel) * xInc; + double y = 0.5 * static_cast(yPixel) * yInc; + + double sumSquares = (x * x) + (y * y); + if(sumSquares > 1.0) + { + return false; + } + + auto sc = stereographic::utils::StereoToSpherical(x, y).normalize(); + + if(!(sc[2] > sc[0] && sc[2] > sc[1])) + { + return false; + } + + sphereDir[0] = static_cast(sc[0]); + sphereDir[1] = static_cast(sc[1]); + sphereDir[2] = static_cast(sc[2]); + return true; +} + +// ----------------------------------------------------------------------------- +std::array CubicLowOps::adjustFigureOrigin(std::array figureOrigin, int legendWidth, int legendHeight, const std::vector& margins, float fontPtSize, + bool generateEntirePlane) const { if(!generateEntirePlane) { @@ -1152,12 +1177,7 @@ ebsdlib::UInt8ArrayType::Pointer CubicLowOps::generateIPFTriangleLegend(int canv { // Compute legend dimensions (same formula as annotateIPFImage uses) const float fontPtSize = static_cast(canvasDim) / 24.0f; - const std::vector margins = { - fontPtSize * 3, - static_cast(canvasDim / 7.0f), - fontPtSize * 2, - static_cast(canvasDim / 7.0f) - }; + const std::vector margins = {fontPtSize * 3, static_cast(canvasDim / 7.0f), fontPtSize * 2, static_cast(canvasDim / 7.0f)}; int legendHeight = canvasDim - static_cast(margins[0]) - static_cast(margins[2]); int legendWidth = canvasDim - static_cast(margins[1]) - static_cast(margins[3]); if(legendHeight > legendWidth) diff --git a/Source/EbsdLib/LaueOps/CubicLowOps.h b/Source/EbsdLib/LaueOps/CubicLowOps.h index 8a9ab0e..a4eb7a1 100644 --- a/Source/EbsdLib/LaueOps/CubicLowOps.h +++ b/Source/EbsdLib/LaueOps/CubicLowOps.h @@ -260,17 +260,13 @@ class EbsdLib_EXPORT CubicLowOps : public LaueOps */ ebsdlib::UInt8ArrayType::Pointer generateIPFTriangleLegend(int imageDim, bool generateEntirePlane) const override; - void drawIPFAnnotations(canvas_ity::canvas& context, int canvasDim, - float fontPtSize, const std::vector& margins, - std::array figureOrigin, - std::array figureCenter, - bool drawFullCircle) const override; - - std::array adjustFigureOrigin( - std::array figureOrigin, - int legendWidth, int legendHeight, - const std::vector& margins, float fontPtSize, - bool generateEntirePlane) const override; + bool mapPixelToSphereSST(int xPixel, int yPixel, int imageDim, std::array& sphereDir) const override; + + void drawIPFAnnotations(canvas_ity::canvas& context, int canvasDim, float fontPtSize, const std::vector& margins, std::array figureOrigin, std::array figureCenter, + bool drawFullCircle) const override; + + std::array adjustFigureOrigin(std::array figureOrigin, int legendWidth, int legendHeight, const std::vector& margins, float fontPtSize, + bool generateEntirePlane) const override; /** * @brief Returns if the given Quaternion is within the Rodrigues Fundamental Zone (RFZ) diff --git a/Source/EbsdLib/LaueOps/CubicOps.cpp b/Source/EbsdLib/LaueOps/CubicOps.cpp index 2fab688..f1c3d25 100644 --- a/Source/EbsdLib/LaueOps/CubicOps.cpp +++ b/Source/EbsdLib/LaueOps/CubicOps.cpp @@ -1955,11 +1955,47 @@ ebsdlib::UInt8ArrayType::Pointer CreateIPFLegend(const CubicOps* ops, int imageD } // namespace // ----------------------------------------------------------------------------- -std::array CubicOps::adjustFigureOrigin( - std::array figureOrigin, - int legendWidth, int legendHeight, - const std::vector& margins, float fontPtSize, - bool generateEntirePlane) const +bool CubicOps::mapPixelToSphereSST(int xPixel, int yPixel, int imageDim, std::array& sphereDir) const +{ + double indexConst1 = 0.414 / static_cast(imageDim); + double indexConst2 = 0.207 / static_cast(imageDim); + + double x = xPixel * indexConst1 + indexConst2; + double y = yPixel * indexConst1 + indexConst2; + + double sumSquares = (x * x) + (y * y); + if(sumSquares > 1.0) + { + return false; + } + if(y < 0.0 || x < 0.0) + { + return false; + } + + auto sc = stereographic::utils::StereoToSpherical(x, y).normalize(); + + double k_RootOfHalf = std::sqrt(0.5); + double red1 = sc[0] * (-k_RootOfHalf) + sc[2] * k_RootOfHalf; + double phi = std::acos(red1); + double x1alt = sc[0] / k_RootOfHalf; + x1alt = x1alt / std::sqrt((x1alt * x1alt) + (sc[1] * sc[1])); + double theta = std::acos(x1alt); + + if(phi <= (45.0 * ebsdlib::constants::k_PiOver180D) || phi >= (90.0 * ebsdlib::constants::k_PiOver180D) || theta >= (35.26 * ebsdlib::constants::k_PiOver180D)) + { + return false; + } + + sphereDir[0] = static_cast(sc[0]); + sphereDir[1] = static_cast(sc[1]); + sphereDir[2] = static_cast(sc[2]); + return true; +} + +// ----------------------------------------------------------------------------- +std::array CubicOps::adjustFigureOrigin(std::array figureOrigin, int legendWidth, int legendHeight, const std::vector& margins, float fontPtSize, + bool generateEntirePlane) const { if(!generateEntirePlane) { @@ -2104,12 +2140,7 @@ ebsdlib::UInt8ArrayType::Pointer CubicOps::generateIPFTriangleLegend(int canvasD { // Compute legend dimensions (same formula as annotateIPFImage uses) const float fontPtSize = static_cast(canvasDim) / 24.0f; - const std::vector margins = { - fontPtSize * 3, - static_cast(canvasDim / 7.0f), - fontPtSize * 2, - static_cast(canvasDim / 7.0f) - }; + const std::vector margins = {fontPtSize * 3, static_cast(canvasDim / 7.0f), fontPtSize * 2, static_cast(canvasDim / 7.0f)}; int legendHeight = canvasDim - static_cast(margins[0]) - static_cast(margins[2]); int legendWidth = canvasDim - static_cast(margins[1]) - static_cast(margins[3]); if(legendHeight > legendWidth) diff --git a/Source/EbsdLib/LaueOps/CubicOps.h b/Source/EbsdLib/LaueOps/CubicOps.h index f14d6f9..ff0bfb9 100644 --- a/Source/EbsdLib/LaueOps/CubicOps.h +++ b/Source/EbsdLib/LaueOps/CubicOps.h @@ -306,17 +306,13 @@ class EbsdLib_EXPORT CubicOps : public LaueOps */ ebsdlib::UInt8ArrayType::Pointer generateIPFTriangleLegend(int imageDim, bool generateEntirePlane) const override; - void drawIPFAnnotations(canvas_ity::canvas& context, int canvasDim, - float fontPtSize, const std::vector& margins, - std::array figureOrigin, - std::array figureCenter, - bool drawFullCircle) const override; - - std::array adjustFigureOrigin( - std::array figureOrigin, - int legendWidth, int legendHeight, - const std::vector& margins, float fontPtSize, - bool generateEntirePlane) const override; + bool mapPixelToSphereSST(int xPixel, int yPixel, int imageDim, std::array& sphereDir) const override; + + void drawIPFAnnotations(canvas_ity::canvas& context, int canvasDim, float fontPtSize, const std::vector& margins, std::array figureOrigin, std::array figureCenter, + bool drawFullCircle) const override; + + std::array adjustFigureOrigin(std::array figureOrigin, int legendWidth, int legendHeight, const std::vector& margins, float fontPtSize, + bool generateEntirePlane) const override; /** * @brief Returns if the given Quaternion is within the Rodrigues Fundamental Zone (RFZ) diff --git a/Source/EbsdLib/LaueOps/HexagonalLowOps.cpp b/Source/EbsdLib/LaueOps/HexagonalLowOps.cpp index 0b1c038..0186291 100644 --- a/Source/EbsdLib/LaueOps/HexagonalLowOps.cpp +++ b/Source/EbsdLib/LaueOps/HexagonalLowOps.cpp @@ -1437,11 +1437,44 @@ ebsdlib::UInt8ArrayType::Pointer CreateIPFLegend(const HexagonalLowOps* ops, int } // namespace // ----------------------------------------------------------------------------- -std::array HexagonalLowOps::adjustFigureOrigin( - std::array figureOrigin, - int legendWidth, int legendHeight, - const std::vector& margins, float fontPtSize, - bool generateEntirePlane) const +bool HexagonalLowOps::mapPixelToSphereSST(int xPixel, int yPixel, int imageDim, std::array& sphereDir) const +{ + double xInc = 1.0 / static_cast(imageDim); + double yInc = 1.0 / static_cast(imageDim); + + double x = -1.0 + 2.0 * xPixel * xInc; + double y = -1.0 + 2.0 * yPixel * yInc; + + double sumSquares = (x * x) + (y * y); + if(sumSquares > 1.0) + { + return false; + } + + if(x < 0.0 || y < 0.0) + { + return false; + } + + // Find the slope of the bounding line. + static const double m = std::sin(60.0 * ebsdlib::constants::k_PiOver180D) / std::cos(60.0 * ebsdlib::constants::k_PiOver180D); + + if(x < y / m) + { + return false; + } + + auto sc = stereographic::utils::StereoToSpherical(x, y).normalize(); + + sphereDir[0] = static_cast(sc[0]); + sphereDir[1] = static_cast(sc[1]); + sphereDir[2] = static_cast(sc[2]); + return true; +} + +// ----------------------------------------------------------------------------- +std::array HexagonalLowOps::adjustFigureOrigin(std::array figureOrigin, int legendWidth, int legendHeight, const std::vector& margins, float fontPtSize, + bool generateEntirePlane) const { if(!generateEntirePlane) { @@ -1553,12 +1586,7 @@ ebsdlib::UInt8ArrayType::Pointer HexagonalLowOps::generateIPFTriangleLegend(int { // Compute legend dimensions (same formula as annotateIPFImage uses) const float fontPtSize = static_cast(canvasDim) / 24.0f; - const std::vector margins = { - fontPtSize * 3, - static_cast(canvasDim / 7.0f), - fontPtSize * 2, - static_cast(canvasDim / 7.0f) - }; + const std::vector margins = {fontPtSize * 3, static_cast(canvasDim / 7.0f), fontPtSize * 2, static_cast(canvasDim / 7.0f)}; int legendHeight = canvasDim - static_cast(margins[0]) - static_cast(margins[2]); int legendWidth = canvasDim - static_cast(margins[1]) - static_cast(margins[3]); if(legendHeight > legendWidth) diff --git a/Source/EbsdLib/LaueOps/HexagonalLowOps.h b/Source/EbsdLib/LaueOps/HexagonalLowOps.h index 3755e78..bd229d2 100644 --- a/Source/EbsdLib/LaueOps/HexagonalLowOps.h +++ b/Source/EbsdLib/LaueOps/HexagonalLowOps.h @@ -260,17 +260,13 @@ class EbsdLib_EXPORT HexagonalLowOps : public LaueOps */ ebsdlib::UInt8ArrayType::Pointer generateIPFTriangleLegend(int imageDim, bool generateEntirePlane) const override; - void drawIPFAnnotations(canvas_ity::canvas& context, int canvasDim, - float fontPtSize, const std::vector& margins, - std::array figureOrigin, - std::array figureCenter, - bool drawFullCircle) const override; - - std::array adjustFigureOrigin( - std::array figureOrigin, - int legendWidth, int legendHeight, - const std::vector& margins, float fontPtSize, - bool generateEntirePlane) const override; + bool mapPixelToSphereSST(int xPixel, int yPixel, int imageDim, std::array& sphereDir) const override; + + void drawIPFAnnotations(canvas_ity::canvas& context, int canvasDim, float fontPtSize, const std::vector& margins, std::array figureOrigin, std::array figureCenter, + bool drawFullCircle) const override; + + std::array adjustFigureOrigin(std::array figureOrigin, int legendWidth, int legendHeight, const std::vector& margins, float fontPtSize, + bool generateEntirePlane) const override; /** * @brief Returns if the given Quaternion is within the Rodrigues Fundamental Zone (RFZ) diff --git a/Source/EbsdLib/LaueOps/HexagonalOps.cpp b/Source/EbsdLib/LaueOps/HexagonalOps.cpp index 55cae49..f143e44 100644 --- a/Source/EbsdLib/LaueOps/HexagonalOps.cpp +++ b/Source/EbsdLib/LaueOps/HexagonalOps.cpp @@ -1491,11 +1491,47 @@ ebsdlib::UInt8ArrayType::Pointer CreateIPFLegend(const HexagonalOps* ops, int im } // namespace // ----------------------------------------------------------------------------- -std::array HexagonalOps::adjustFigureOrigin( - std::array figureOrigin, - int legendWidth, int legendHeight, - const std::vector& margins, float fontPtSize, - bool generateEntirePlane) const +bool HexagonalOps::mapPixelToSphereSST(int xPixel, int yPixel, int imageDim, std::array& sphereDir) const +{ + double xInc = 1.0 / static_cast(imageDim); + double yInc = 1.0 / static_cast(imageDim); + + double x = -1.0 + 2.0 * xPixel * xInc; + double y = -1.0 + 2.0 * yPixel * yInc; + + double sumSquares = (x * x) + (y * y); + if(sumSquares > 1.0) + { + return false; + } + + // Find the slope of the bounding line. + static const double m = -1.0 * std::sin(30.0 * ebsdlib::constants::k_PiOver180D) / std::cos(30.0 * ebsdlib::constants::k_PiOver180D); + + if(x < y / m && x > 0.0) + { + return false; + } + if(x > y / m && y > 0.0) + { + return false; + } + if(x < 0.0) + { + return false; + } + + auto sc = stereographic::utils::StereoToSpherical(x, y).normalize(); + + sphereDir[0] = static_cast(sc[0]); + sphereDir[1] = static_cast(sc[1]); + sphereDir[2] = static_cast(sc[2]); + return true; +} + +// ----------------------------------------------------------------------------- +std::array HexagonalOps::adjustFigureOrigin(std::array figureOrigin, int legendWidth, int legendHeight, const std::vector& margins, float fontPtSize, + bool generateEntirePlane) const { if(!generateEntirePlane) { @@ -1597,12 +1633,7 @@ ebsdlib::UInt8ArrayType::Pointer HexagonalOps::generateIPFTriangleLegend(int can { // Compute legend dimensions (same formula as annotateIPFImage uses) const float fontPtSize = static_cast(canvasDim) / 24.0f; - const std::vector margins = { - fontPtSize * 3, - static_cast(canvasDim / 7.0f), - fontPtSize * 2, - static_cast(canvasDim / 7.0f) - }; + const std::vector margins = {fontPtSize * 3, static_cast(canvasDim / 7.0f), fontPtSize * 2, static_cast(canvasDim / 7.0f)}; int legendHeight = canvasDim - static_cast(margins[0]) - static_cast(margins[2]); int legendWidth = canvasDim - static_cast(margins[1]) - static_cast(margins[3]); if(legendHeight > legendWidth) diff --git a/Source/EbsdLib/LaueOps/HexagonalOps.h b/Source/EbsdLib/LaueOps/HexagonalOps.h index 862ce1f..8bd740b 100644 --- a/Source/EbsdLib/LaueOps/HexagonalOps.h +++ b/Source/EbsdLib/LaueOps/HexagonalOps.h @@ -260,17 +260,13 @@ class EbsdLib_EXPORT HexagonalOps : public LaueOps */ ebsdlib::UInt8ArrayType::Pointer generateIPFTriangleLegend(int imageDim, bool generateEntirePlane) const override; - void drawIPFAnnotations(canvas_ity::canvas& context, int canvasDim, - float fontPtSize, const std::vector& margins, - std::array figureOrigin, - std::array figureCenter, - bool drawFullCircle) const override; - - std::array adjustFigureOrigin( - std::array figureOrigin, - int legendWidth, int legendHeight, - const std::vector& margins, float fontPtSize, - bool generateEntirePlane) const override; + bool mapPixelToSphereSST(int xPixel, int yPixel, int imageDim, std::array& sphereDir) const override; + + void drawIPFAnnotations(canvas_ity::canvas& context, int canvasDim, float fontPtSize, const std::vector& margins, std::array figureOrigin, std::array figureCenter, + bool drawFullCircle) const override; + + std::array adjustFigureOrigin(std::array figureOrigin, int legendWidth, int legendHeight, const std::vector& margins, float fontPtSize, + bool generateEntirePlane) const override; /** * @brief Returns if the given Quaternion is within the Rodrigues Fundamental Zone (RFZ) diff --git a/Source/EbsdLib/LaueOps/LaueOps.cpp b/Source/EbsdLib/LaueOps/LaueOps.cpp index 506eaee..0b01598 100644 --- a/Source/EbsdLib/LaueOps/LaueOps.cpp +++ b/Source/EbsdLib/LaueOps/LaueOps.cpp @@ -882,10 +882,10 @@ std::vector LaueOps::generateInversePoleFigure(InverseP ebsdlib::FloatArrayType::Pointer dirs1 = InversePoleFigureUtilities::computeIPFDirections(*this, config.eulers, config.sampleDirections[1]); ebsdlib::FloatArrayType::Pointer dirs2 = InversePoleFigureUtilities::computeIPFDirections(*this, config.eulers, config.sampleDirections[2]); - // Step 2: Compute intensity images for each - ebsdlib::DoubleArrayType::Pointer intensity0 = InversePoleFigureUtilities::computeIPFIntensity(*this, dirs0.get(), config.imageWidth, config.imageHeight, config.lambertDim, config.normalizeMRD); - ebsdlib::DoubleArrayType::Pointer intensity1 = InversePoleFigureUtilities::computeIPFIntensity(*this, dirs1.get(), config.imageWidth, config.imageHeight, config.lambertDim, config.normalizeMRD); - ebsdlib::DoubleArrayType::Pointer intensity2 = InversePoleFigureUtilities::computeIPFIntensity(*this, dirs2.get(), config.imageWidth, config.imageHeight, config.lambertDim, config.normalizeMRD); + // Step 2: Compute intensity images for each (using stereographic SST mapping) + ebsdlib::DoubleArrayType::Pointer intensity0 = InversePoleFigureUtilities::computeIPFIntensity(*this, dirs0.get(), config.imageWidth, config.imageHeight, config.lambertDim, config.normalizeMRD, true); + ebsdlib::DoubleArrayType::Pointer intensity1 = InversePoleFigureUtilities::computeIPFIntensity(*this, dirs1.get(), config.imageWidth, config.imageHeight, config.lambertDim, config.normalizeMRD, true); + ebsdlib::DoubleArrayType::Pointer intensity2 = InversePoleFigureUtilities::computeIPFIntensity(*this, dirs2.get(), config.imageWidth, config.imageHeight, config.lambertDim, config.normalizeMRD, true); // Step 3: Find global min/max across all 3 intensity images (only for pixels inside SST, value >= 0) double globalMax = std::numeric_limits::lowest(); @@ -943,29 +943,27 @@ ebsdlib::Rgb LaueOps::generateMisorientationColor(const QuatD& q, const QuatD& r } // ----------------------------------------------------------------------------- -std::array LaueOps::adjustFigureOrigin( - std::array figureOrigin, - int legendWidth, int legendHeight, - const std::vector& margins, float fontPtSize, - bool generateEntirePlane) const +bool LaueOps::mapPixelToSphereSST(int xPixel, int yPixel, int imageDim, std::array& sphereDir) const +{ + return false; +} + +// ----------------------------------------------------------------------------- +std::array LaueOps::adjustFigureOrigin(std::array figureOrigin, int legendWidth, int legendHeight, const std::vector& margins, float fontPtSize, + bool generateEntirePlane) const { return figureOrigin; } // ----------------------------------------------------------------------------- -UInt8ArrayType::Pointer LaueOps::annotateIPFImage( - UInt8ArrayType::Pointer triangleImage, - int imageDim, - int canvasDim, - const std::string& title, - bool generateEntirePlane) const +UInt8ArrayType::Pointer LaueOps::annotateIPFImage(UInt8ArrayType::Pointer triangleImage, int imageDim, int canvasDim, const std::string& title, bool generateEntirePlane) const { const float fontPtSize = static_cast(canvasDim) / 24.0f; const std::vector margins = { - fontPtSize * 3, // Top - static_cast(canvasDim / 7.0f), // Right - fontPtSize * 2, // Bottom - static_cast(canvasDim / 7.0f) // Left + fontPtSize * 3, // Top + static_cast(canvasDim / 7.0f), // Right + fontPtSize * 2, // Bottom + static_cast(canvasDim / 7.0f) // Left }; int legendHeight = canvasDim - static_cast(margins[0]) - static_cast(margins[2]); @@ -1013,10 +1011,7 @@ UInt8ArrayType::Pointer LaueOps::annotateIPFImage( context.fill(); // Draw the triangle image onto the canvas - context.draw_image(image->getPointer(0), imageDim, imageDim, - imageDim * image->getNumberOfComponents(), - figureOrigin[0], figureOrigin[1], - static_cast(legendWidth), + context.draw_image(image->getPointer(0), imageDim, imageDim, imageDim * image->getNumberOfComponents(), figureOrigin[0], figureOrigin[1], static_cast(legendWidth), static_cast(legendHeight)); // Draw title @@ -1028,20 +1023,14 @@ UInt8ArrayType::Pointer LaueOps::annotateIPFImage( drawIPFAnnotations(context, canvasDim, fontPtSize, margins, figureOrigin, figureCenter, generateEntirePlane); // Extract rendered pixels and remove alpha channel - ebsdlib::UInt8ArrayType::Pointer rgbaCanvasImage = ebsdlib::UInt8ArrayType::CreateArray( - canvasDim * canvasDim, {4ULL}, "Annotated IPF", true); + ebsdlib::UInt8ArrayType::Pointer rgbaCanvasImage = ebsdlib::UInt8ArrayType::CreateArray(canvasDim * canvasDim, {4ULL}, "Annotated IPF", true); context.get_image_data(rgbaCanvasImage->getPointer(0), canvasDim, canvasDim, canvasDim * 4, 0, 0); return ebsdlib::RemoveAlphaChannel(rgbaCanvasImage.get()); } // ----------------------------------------------------------------------------- -UInt8ArrayType::Pointer LaueOps::drawColorBar( - UInt8ArrayType::Pointer image, - int canvasDim, - int numColors, - double minValue, double maxValue, - bool isMRD) const +UInt8ArrayType::Pointer LaueOps::drawColorBar(UInt8ArrayType::Pointer image, int canvasDim, int numColors, double minValue, double maxValue, bool isMRD) const { const float fontPtSize = static_cast(canvasDim) / 24.0f; @@ -1064,10 +1053,7 @@ UInt8ArrayType::Pointer LaueOps::drawColorBar( canvas_ity::canvas context(canvasDim, canvasDim); // Put the existing image onto the canvas - context.draw_image(rgbaImage->getPointer(0), canvasDim, canvasDim, - canvasDim * 4, 0.0f, 0.0f, - static_cast(canvasDim), - static_cast(canvasDim)); + context.draw_image(rgbaImage->getPointer(0), canvasDim, canvasDim, canvasDim * 4, 0.0f, 0.0f, static_cast(canvasDim), static_cast(canvasDim)); // Color bar dimensions const float barLeft = static_cast(canvasDim) * 0.80f; @@ -1137,9 +1123,7 @@ UInt8ArrayType::Pointer LaueOps::drawColorBar( } // ----------------------------------------------------------------------------- -std::vector LaueOps::generateAnnotatedIPFDensity( - InversePoleFigureConfiguration_t& config, - std::pair* outMinMax) const +std::vector LaueOps::generateAnnotatedIPFDensity(InversePoleFigureConfiguration_t& config, std::pair* outMinMax) const { // Validate square images if(config.imageWidth != config.imageHeight) @@ -1172,10 +1156,10 @@ std::vector LaueOps::generateAnnotatedIPFDensity( ebsdlib::FloatArrayType::Pointer dirs1 = InversePoleFigureUtilities::computeIPFDirections(*this, config.eulers, config.sampleDirections[1]); ebsdlib::FloatArrayType::Pointer dirs2 = InversePoleFigureUtilities::computeIPFDirections(*this, config.eulers, config.sampleDirections[2]); - // Step 2: Compute intensity images - ebsdlib::DoubleArrayType::Pointer intensity0 = InversePoleFigureUtilities::computeIPFIntensity(*this, dirs0.get(), imageDim, imageDim, config.lambertDim, config.normalizeMRD); - ebsdlib::DoubleArrayType::Pointer intensity1 = InversePoleFigureUtilities::computeIPFIntensity(*this, dirs1.get(), imageDim, imageDim, config.lambertDim, config.normalizeMRD); - ebsdlib::DoubleArrayType::Pointer intensity2 = InversePoleFigureUtilities::computeIPFIntensity(*this, dirs2.get(), imageDim, imageDim, config.lambertDim, config.normalizeMRD); + // Step 2: Compute intensity images (using stereographic SST mapping) + ebsdlib::DoubleArrayType::Pointer intensity0 = InversePoleFigureUtilities::computeIPFIntensity(*this, dirs0.get(), imageDim, imageDim, config.lambertDim, config.normalizeMRD, true); + ebsdlib::DoubleArrayType::Pointer intensity1 = InversePoleFigureUtilities::computeIPFIntensity(*this, dirs1.get(), imageDim, imageDim, config.lambertDim, config.normalizeMRD, true); + ebsdlib::DoubleArrayType::Pointer intensity2 = InversePoleFigureUtilities::computeIPFIntensity(*this, dirs2.get(), imageDim, imageDim, config.lambertDim, config.normalizeMRD, true); // Step 3: Find global min/max double globalMax = std::numeric_limits::lowest(); diff --git a/Source/EbsdLib/LaueOps/LaueOps.h b/Source/EbsdLib/LaueOps/LaueOps.h index f7d581c..5b35b10 100644 --- a/Source/EbsdLib/LaueOps/LaueOps.h +++ b/Source/EbsdLib/LaueOps/LaueOps.h @@ -335,22 +335,27 @@ class EbsdLib_EXPORT LaueOps * @brief Per-subclass hook that draws Miller index labels and SST boundary * annotations onto a canvas. Called by annotateIPFImage(). */ - virtual void drawIPFAnnotations(canvas_ity::canvas& context, int canvasDim, - float fontPtSize, const std::vector& margins, - std::array figureOrigin, - std::array figureCenter, - bool drawFullCircle) const = 0; + virtual void drawIPFAnnotations(canvas_ity::canvas& context, int canvasDim, float fontPtSize, const std::vector& margins, std::array figureOrigin, std::array figureCenter, + bool drawFullCircle) const = 0; + + /** + * @brief Maps a pixel coordinate to a unit sphere direction using the same + * stereographic projection as CreateIPFLegend (SST-only view). + * @param xPixel X pixel coordinate [0, imageDim) + * @param yPixel Y pixel coordinate [0, imageDim) + * @param imageDim Image dimension (square) + * @param sphereDir Output: unit sphere direction if pixel is inside SST + * @return true if the pixel maps to a point inside the Standard Stereographic Triangle + */ + virtual bool mapPixelToSphereSST(int xPixel, int yPixel, int imageDim, std::array& sphereDir) const; /** * @brief Per-subclass hook that adjusts the figureOrigin when rendering * SST-only view. Each subclass overrides to position its triangle shape * correctly within the canvas. Default returns figureOrigin unchanged. */ - virtual std::array adjustFigureOrigin( - std::array figureOrigin, - int legendWidth, int legendHeight, - const std::vector& margins, float fontPtSize, - bool generateEntirePlane) const; + virtual std::array adjustFigureOrigin(std::array figureOrigin, int legendWidth, int legendHeight, const std::vector& margins, float fontPtSize, + bool generateEntirePlane) const; /** * @brief Generates 3 annotated inverse pole figure density images with @@ -358,9 +363,7 @@ class EbsdLib_EXPORT LaueOps * @param config Configuration struct; imageWidth must equal imageHeight (square images required) * @param outMinMax Optional output for the global [min, max] intensity values */ - std::vector generateAnnotatedIPFDensity( - InversePoleFigureConfiguration_t& config, - std::pair* outMinMax = nullptr) const; + std::vector generateAnnotatedIPFDensity(InversePoleFigureConfiguration_t& config, std::pair* outMinMax = nullptr) const; /** * @brief Generates 3 inverse pole figure density images for 3 orthogonal sample directions. @@ -495,22 +498,12 @@ class EbsdLib_EXPORT LaueOps * @param generateEntirePlane true = full circle view, false = SST only * @return RGB image (canvasDim x canvasDim, 3 components) */ - UInt8ArrayType::Pointer annotateIPFImage( - UInt8ArrayType::Pointer triangleImage, - int imageDim, - int canvasDim, - const std::string& title, - bool generateEntirePlane) const; + UInt8ArrayType::Pointer annotateIPFImage(UInt8ArrayType::Pointer triangleImage, int imageDim, int canvasDim, const std::string& title, bool generateEntirePlane) const; /** * @brief Draws a color bar with min/max labels onto an existing RGB image. */ - UInt8ArrayType::Pointer drawColorBar( - UInt8ArrayType::Pointer image, - int canvasDim, - int numColors, - double minValue, double maxValue, - bool isMRD) const; + UInt8ArrayType::Pointer drawColorBar(UInt8ArrayType::Pointer image, int canvasDim, int numColors, double minValue, double maxValue, bool isMRD) const; /** * @brief calculateMisorientationInternal diff --git a/Source/EbsdLib/LaueOps/MonoclinicOps.cpp b/Source/EbsdLib/LaueOps/MonoclinicOps.cpp index acf62d2..652f1e5 100644 --- a/Source/EbsdLib/LaueOps/MonoclinicOps.cpp +++ b/Source/EbsdLib/LaueOps/MonoclinicOps.cpp @@ -794,10 +794,37 @@ ebsdlib::UInt8ArrayType::Pointer CreateIPFLegend(const MonoclinicOps* ops, int i // ----------------------------------------------------------------------------- } // namespace +// ----------------------------------------------------------------------------- +bool MonoclinicOps::mapPixelToSphereSST(int xPixel, int yPixel, int imageDim, std::array& sphereDir) const +{ + double xInc = 1.0 / static_cast(imageDim); + double yInc = 1.0 / static_cast(imageDim); + + double x = -1.0 + 2.0 * xPixel * xInc; + double y = -1.0 + 2.0 * yPixel * yInc; + + double sumSquares = (x * x) + (y * y); + if(sumSquares > 1.0) + { + return false; + } + + if(y < 0.0) + { + return false; + } + + auto sc = stereographic::utils::StereoToSpherical(x, y).normalize(); + + sphereDir[0] = static_cast(sc[0]); + sphereDir[1] = static_cast(sc[1]); + sphereDir[2] = static_cast(sc[2]); + return true; +} + // ----------------------------------------------------------------------------- void MonoclinicOps::drawIPFAnnotations(canvas_ity::canvas& context, int canvasDim, float fontPtSize, const std::vector& margins, std::array figureOrigin, - std::array figureCenter, - bool drawFullCircle) const + std::array figureCenter, bool drawFullCircle) const { int legendHeight = canvasDim - margins[0] - margins[2]; int legendWidth = canvasDim - margins[1] - margins[3]; @@ -913,12 +940,7 @@ ebsdlib::UInt8ArrayType::Pointer MonoclinicOps::generateIPFTriangleLegend(int ca { // Compute legend dimensions (same formula as annotateIPFImage uses) const float fontPtSize = static_cast(canvasDim) / 24.0f; - const std::vector margins = { - fontPtSize * 3, - static_cast(canvasDim / 7.0f), - fontPtSize * 2, - static_cast(canvasDim / 7.0f) - }; + const std::vector margins = {fontPtSize * 3, static_cast(canvasDim / 7.0f), fontPtSize * 2, static_cast(canvasDim / 7.0f)}; int legendHeight = canvasDim - static_cast(margins[0]) - static_cast(margins[2]); int legendWidth = canvasDim - static_cast(margins[1]) - static_cast(margins[3]); if(legendHeight > legendWidth) diff --git a/Source/EbsdLib/LaueOps/MonoclinicOps.h b/Source/EbsdLib/LaueOps/MonoclinicOps.h index 42d47b2..98d20af 100644 --- a/Source/EbsdLib/LaueOps/MonoclinicOps.h +++ b/Source/EbsdLib/LaueOps/MonoclinicOps.h @@ -259,11 +259,10 @@ class EbsdLib_EXPORT MonoclinicOps : public LaueOps */ ebsdlib::UInt8ArrayType::Pointer generateIPFTriangleLegend(int imageDim, bool generateEntirePlane) const override; - void drawIPFAnnotations(canvas_ity::canvas& context, int canvasDim, - float fontPtSize, const std::vector& margins, - std::array figureOrigin, - std::array figureCenter, - bool drawFullCircle) const override; + bool mapPixelToSphereSST(int xPixel, int yPixel, int imageDim, std::array& sphereDir) const override; + + void drawIPFAnnotations(canvas_ity::canvas& context, int canvasDim, float fontPtSize, const std::vector& margins, std::array figureOrigin, std::array figureCenter, + bool drawFullCircle) const override; /** * @brief Returns if the given Quaternion is within the Rodrigues Fundamental Zone (RFZ) diff --git a/Source/EbsdLib/LaueOps/OrthoRhombicOps.cpp b/Source/EbsdLib/LaueOps/OrthoRhombicOps.cpp index cee44b5..b5190f9 100644 --- a/Source/EbsdLib/LaueOps/OrthoRhombicOps.cpp +++ b/Source/EbsdLib/LaueOps/OrthoRhombicOps.cpp @@ -806,11 +806,36 @@ ebsdlib::UInt8ArrayType::Pointer CreateIPFLegend(const OrthoRhombicOps* ops, int } // namespace // ----------------------------------------------------------------------------- -std::array OrthoRhombicOps::adjustFigureOrigin( - std::array figureOrigin, - int legendWidth, int legendHeight, - const std::vector& margins, float fontPtSize, - bool generateEntirePlane) const +bool OrthoRhombicOps::mapPixelToSphereSST(int xPixel, int yPixel, int imageDim, std::array& sphereDir) const +{ + double xInc = 1.0 / static_cast(imageDim); + double yInc = 1.0 / static_cast(imageDim); + + double x = -1.0 + 2.0 * xPixel * xInc; + double y = -1.0 + 2.0 * yPixel * yInc; + + double sumSquares = (x * x) + (y * y); + if(sumSquares > 1.0) + { + return false; + } + + if(y < 0.0 || x < 0.0) + { + return false; + } + + auto sc = stereographic::utils::StereoToSpherical(x, y).normalize(); + + sphereDir[0] = static_cast(sc[0]); + sphereDir[1] = static_cast(sc[1]); + sphereDir[2] = static_cast(sc[2]); + return true; +} + +// ----------------------------------------------------------------------------- +std::array OrthoRhombicOps::adjustFigureOrigin(std::array figureOrigin, int legendWidth, int legendHeight, const std::vector& margins, float fontPtSize, + bool generateEntirePlane) const { if(!generateEntirePlane) { @@ -821,8 +846,7 @@ std::array OrthoRhombicOps::adjustFigureOrigin( // ----------------------------------------------------------------------------- void OrthoRhombicOps::drawIPFAnnotations(canvas_ity::canvas& context, int canvasDim, float fontPtSize, const std::vector& margins, std::array figureOrigin, - std::array figureCenter, - bool drawFullCircle) const + std::array figureCenter, bool drawFullCircle) const { int legendHeight = canvasDim - margins[0] - margins[2]; int legendWidth = canvasDim - margins[1] - margins[3]; @@ -912,12 +936,7 @@ ebsdlib::UInt8ArrayType::Pointer OrthoRhombicOps::generateIPFTriangleLegend(int { // Compute legend dimensions (same formula as annotateIPFImage uses) const float fontPtSize = static_cast(canvasDim) / 24.0f; - const std::vector margins = { - fontPtSize * 3, - static_cast(canvasDim / 7.0f), - fontPtSize * 2, - static_cast(canvasDim / 7.0f) - }; + const std::vector margins = {fontPtSize * 3, static_cast(canvasDim / 7.0f), fontPtSize * 2, static_cast(canvasDim / 7.0f)}; int legendHeight = canvasDim - static_cast(margins[0]) - static_cast(margins[2]); int legendWidth = canvasDim - static_cast(margins[1]) - static_cast(margins[3]); if(legendHeight > legendWidth) diff --git a/Source/EbsdLib/LaueOps/OrthoRhombicOps.h b/Source/EbsdLib/LaueOps/OrthoRhombicOps.h index c454a9d..114cc11 100644 --- a/Source/EbsdLib/LaueOps/OrthoRhombicOps.h +++ b/Source/EbsdLib/LaueOps/OrthoRhombicOps.h @@ -262,17 +262,13 @@ class EbsdLib_EXPORT OrthoRhombicOps : public LaueOps */ ebsdlib::UInt8ArrayType::Pointer generateIPFTriangleLegend(int canvasDim, bool generateEntirePlane) const override; - void drawIPFAnnotations(canvas_ity::canvas& context, int canvasDim, - float fontPtSize, const std::vector& margins, - std::array figureOrigin, - std::array figureCenter, - bool drawFullCircle) const override; - - std::array adjustFigureOrigin( - std::array figureOrigin, - int legendWidth, int legendHeight, - const std::vector& margins, float fontPtSize, - bool generateEntirePlane) const override; + bool mapPixelToSphereSST(int xPixel, int yPixel, int imageDim, std::array& sphereDir) const override; + + void drawIPFAnnotations(canvas_ity::canvas& context, int canvasDim, float fontPtSize, const std::vector& margins, std::array figureOrigin, std::array figureCenter, + bool drawFullCircle) const override; + + std::array adjustFigureOrigin(std::array figureOrigin, int legendWidth, int legendHeight, const std::vector& margins, float fontPtSize, + bool generateEntirePlane) const override; /** * @brief Returns if the given Quaternion is within the Rodrigues Fundamental Zone (RFZ) diff --git a/Source/EbsdLib/LaueOps/TetragonalLowOps.cpp b/Source/EbsdLib/LaueOps/TetragonalLowOps.cpp index cebcd75..b6583a5 100644 --- a/Source/EbsdLib/LaueOps/TetragonalLowOps.cpp +++ b/Source/EbsdLib/LaueOps/TetragonalLowOps.cpp @@ -808,11 +808,36 @@ ebsdlib::UInt8ArrayType::Pointer CreateIPFLegend(const TetragonalLowOps* ops, in } // namespace // ----------------------------------------------------------------------------- -std::array TetragonalLowOps::adjustFigureOrigin( - std::array figureOrigin, - int legendWidth, int legendHeight, - const std::vector& margins, float fontPtSize, - bool generateEntirePlane) const +bool TetragonalLowOps::mapPixelToSphereSST(int xPixel, int yPixel, int imageDim, std::array& sphereDir) const +{ + double xInc = 1.0 / static_cast(imageDim); + double yInc = 1.0 / static_cast(imageDim); + + double x = -1.0 + 2.0 * xPixel * xInc; + double y = -1.0 + 2.0 * yPixel * yInc; + + double sumSquares = (x * x) + (y * y); + if(sumSquares > 1.0) + { + return false; + } + + if(y < 0.0 || x < 0.0) + { + return false; + } + + auto sc = stereographic::utils::StereoToSpherical(x, y).normalize(); + + sphereDir[0] = static_cast(sc[0]); + sphereDir[1] = static_cast(sc[1]); + sphereDir[2] = static_cast(sc[2]); + return true; +} + +// ----------------------------------------------------------------------------- +std::array TetragonalLowOps::adjustFigureOrigin(std::array figureOrigin, int legendWidth, int legendHeight, const std::vector& margins, float fontPtSize, + bool generateEntirePlane) const { if(!generateEntirePlane) { @@ -823,8 +848,7 @@ std::array TetragonalLowOps::adjustFigureOrigin( // ----------------------------------------------------------------------------- void TetragonalLowOps::drawIPFAnnotations(canvas_ity::canvas& context, int canvasDim, float fontPtSize, const std::vector& margins, std::array figureOrigin, - std::array figureCenter, - bool drawFullCircle) const + std::array figureCenter, bool drawFullCircle) const { int legendHeight = canvasDim - margins[0] - margins[2]; int legendWidth = canvasDim - margins[1] - margins[3]; @@ -940,12 +964,7 @@ ebsdlib::UInt8ArrayType::Pointer TetragonalLowOps::generateIPFTriangleLegend(int { // Compute legend dimensions (same formula as annotateIPFImage uses) const float fontPtSize = static_cast(canvasDim) / 24.0f; - const std::vector margins = { - fontPtSize * 3, - static_cast(canvasDim / 7.0f), - fontPtSize * 2, - static_cast(canvasDim / 7.0f) - }; + const std::vector margins = {fontPtSize * 3, static_cast(canvasDim / 7.0f), fontPtSize * 2, static_cast(canvasDim / 7.0f)}; int legendHeight = canvasDim - static_cast(margins[0]) - static_cast(margins[2]); int legendWidth = canvasDim - static_cast(margins[1]) - static_cast(margins[3]); if(legendHeight > legendWidth) diff --git a/Source/EbsdLib/LaueOps/TetragonalLowOps.h b/Source/EbsdLib/LaueOps/TetragonalLowOps.h index 4c158a6..6403a8c 100644 --- a/Source/EbsdLib/LaueOps/TetragonalLowOps.h +++ b/Source/EbsdLib/LaueOps/TetragonalLowOps.h @@ -262,17 +262,13 @@ class EbsdLib_EXPORT TetragonalLowOps : public LaueOps */ ebsdlib::UInt8ArrayType::Pointer generateIPFTriangleLegend(int imageDim, bool generateEntirePlane) const override; - void drawIPFAnnotations(canvas_ity::canvas& context, int canvasDim, - float fontPtSize, const std::vector& margins, - std::array figureOrigin, - std::array figureCenter, - bool drawFullCircle) const override; - - std::array adjustFigureOrigin( - std::array figureOrigin, - int legendWidth, int legendHeight, - const std::vector& margins, float fontPtSize, - bool generateEntirePlane) const override; + bool mapPixelToSphereSST(int xPixel, int yPixel, int imageDim, std::array& sphereDir) const override; + + void drawIPFAnnotations(canvas_ity::canvas& context, int canvasDim, float fontPtSize, const std::vector& margins, std::array figureOrigin, std::array figureCenter, + bool drawFullCircle) const override; + + std::array adjustFigureOrigin(std::array figureOrigin, int legendWidth, int legendHeight, const std::vector& margins, float fontPtSize, + bool generateEntirePlane) const override; /** * @brief Returns if the given Quaternion is within the Rodrigues Fundamental Zone (RFZ) diff --git a/Source/EbsdLib/LaueOps/TetragonalOps.cpp b/Source/EbsdLib/LaueOps/TetragonalOps.cpp index 0aac5e4..c05f6bf 100644 --- a/Source/EbsdLib/LaueOps/TetragonalOps.cpp +++ b/Source/EbsdLib/LaueOps/TetragonalOps.cpp @@ -851,11 +851,36 @@ ebsdlib::UInt8ArrayType::Pointer CreateIPFLegend(const TetragonalOps* ops, int i } // namespace // ----------------------------------------------------------------------------- -std::array TetragonalOps::adjustFigureOrigin( - std::array figureOrigin, - int legendWidth, int legendHeight, - const std::vector& margins, float fontPtSize, - bool generateEntirePlane) const +bool TetragonalOps::mapPixelToSphereSST(int xPixel, int yPixel, int imageDim, std::array& sphereDir) const +{ + double xInc = 1.0 / static_cast(imageDim); + double yInc = 1.0 / static_cast(imageDim); + + double x = -1.0 + 2.0 * xPixel * xInc; + double y = -1.0 + 2.0 * yPixel * yInc; + + double sumSquares = (x * x) + (y * y); + if(sumSquares > 1.0) + { + return false; + } + + if(x < y || y < 0.0) + { + return false; + } + + auto sc = stereographic::utils::StereoToSpherical(x, y).normalize(); + + sphereDir[0] = static_cast(sc[0]); + sphereDir[1] = static_cast(sc[1]); + sphereDir[2] = static_cast(sc[2]); + return true; +} + +// ----------------------------------------------------------------------------- +std::array TetragonalOps::adjustFigureOrigin(std::array figureOrigin, int legendWidth, int legendHeight, const std::vector& margins, float fontPtSize, + bool generateEntirePlane) const { if(!generateEntirePlane) { @@ -867,8 +892,7 @@ std::array TetragonalOps::adjustFigureOrigin( // ----------------------------------------------------------------------------- void TetragonalOps::drawIPFAnnotations(canvas_ity::canvas& context, int canvasDim, float fontPtSize, const std::vector& margins, std::array figureOrigin, - std::array figureCenter, - bool drawFullCircle) const + std::array figureCenter, bool drawFullCircle) const { int legendHeight = canvasDim - margins[0] - margins[2]; int legendWidth = canvasDim - margins[1] - margins[3]; @@ -958,12 +982,7 @@ ebsdlib::UInt8ArrayType::Pointer TetragonalOps::generateIPFTriangleLegend(int ca { // Compute legend dimensions (same formula as annotateIPFImage uses) const float fontPtSize = static_cast(canvasDim) / 24.0f; - const std::vector margins = { - fontPtSize * 3, - static_cast(canvasDim / 7.0f), - fontPtSize * 2, - static_cast(canvasDim / 7.0f) - }; + const std::vector margins = {fontPtSize * 3, static_cast(canvasDim / 7.0f), fontPtSize * 2, static_cast(canvasDim / 7.0f)}; int legendHeight = canvasDim - static_cast(margins[0]) - static_cast(margins[2]); int legendWidth = canvasDim - static_cast(margins[1]) - static_cast(margins[3]); if(legendHeight > legendWidth) diff --git a/Source/EbsdLib/LaueOps/TetragonalOps.h b/Source/EbsdLib/LaueOps/TetragonalOps.h index b84baeb..c8f9f1a 100644 --- a/Source/EbsdLib/LaueOps/TetragonalOps.h +++ b/Source/EbsdLib/LaueOps/TetragonalOps.h @@ -262,17 +262,13 @@ class EbsdLib_EXPORT TetragonalOps : public LaueOps */ ebsdlib::UInt8ArrayType::Pointer generateIPFTriangleLegend(int imageDim, bool generateEntirePlane) const override; - void drawIPFAnnotations(canvas_ity::canvas& context, int canvasDim, - float fontPtSize, const std::vector& margins, - std::array figureOrigin, - std::array figureCenter, - bool drawFullCircle) const override; - - std::array adjustFigureOrigin( - std::array figureOrigin, - int legendWidth, int legendHeight, - const std::vector& margins, float fontPtSize, - bool generateEntirePlane) const override; + bool mapPixelToSphereSST(int xPixel, int yPixel, int imageDim, std::array& sphereDir) const override; + + void drawIPFAnnotations(canvas_ity::canvas& context, int canvasDim, float fontPtSize, const std::vector& margins, std::array figureOrigin, std::array figureCenter, + bool drawFullCircle) const override; + + std::array adjustFigureOrigin(std::array figureOrigin, int legendWidth, int legendHeight, const std::vector& margins, float fontPtSize, + bool generateEntirePlane) const override; /** * @brief Returns if the given Quaternion is within the Rodrigues Fundamental Zone (RFZ) diff --git a/Source/EbsdLib/LaueOps/TriclinicOps.cpp b/Source/EbsdLib/LaueOps/TriclinicOps.cpp index 42b1925..fb74c91 100644 --- a/Source/EbsdLib/LaueOps/TriclinicOps.cpp +++ b/Source/EbsdLib/LaueOps/TriclinicOps.cpp @@ -788,10 +788,32 @@ ebsdlib::UInt8ArrayType::Pointer CreateIPFLegend(const TriclinicOps* ops, int im // ----------------------------------------------------------------------------- } // namespace +// ----------------------------------------------------------------------------- +bool TriclinicOps::mapPixelToSphereSST(int xPixel, int yPixel, int imageDim, std::array& sphereDir) const +{ + double xInc = 1.0 / static_cast(imageDim); + double yInc = 1.0 / static_cast(imageDim); + + double x = -1.0 + 2.0 * xPixel * xInc; + double y = -1.0 + 2.0 * yPixel * yInc; + + double sumSquares = (x * x) + (y * y); + if(sumSquares > 1.0) + { + return false; + } + + auto sc = stereographic::utils::StereoToSpherical(x, y).normalize(); + + sphereDir[0] = static_cast(sc[0]); + sphereDir[1] = static_cast(sc[1]); + sphereDir[2] = static_cast(sc[2]); + return true; +} + // ----------------------------------------------------------------------------- void TriclinicOps::drawIPFAnnotations(canvas_ity::canvas& context, int canvasDim, float fontPtSize, const std::vector& margins, std::array figureOrigin, - std::array figureCenter, - bool drawFullCircle) const + std::array figureCenter, bool drawFullCircle) const { int legendHeight = canvasDim - margins[0] - margins[2]; int legendWidth = canvasDim - margins[1] - margins[3]; @@ -896,12 +918,7 @@ ebsdlib::UInt8ArrayType::Pointer TriclinicOps::generateIPFTriangleLegend(int can { // Compute legend dimensions (same formula as annotateIPFImage uses) const float fontPtSize = static_cast(canvasDim) / 24.0f; - const std::vector margins = { - fontPtSize * 3, - static_cast(canvasDim / 7.0f), - fontPtSize * 2, - static_cast(canvasDim / 7.0f) - }; + const std::vector margins = {fontPtSize * 3, static_cast(canvasDim / 7.0f), fontPtSize * 2, static_cast(canvasDim / 7.0f)}; int legendHeight = canvasDim - static_cast(margins[0]) - static_cast(margins[2]); int legendWidth = canvasDim - static_cast(margins[1]) - static_cast(margins[3]); if(legendHeight > legendWidth) diff --git a/Source/EbsdLib/LaueOps/TriclinicOps.h b/Source/EbsdLib/LaueOps/TriclinicOps.h index fa7fb66..6728797 100644 --- a/Source/EbsdLib/LaueOps/TriclinicOps.h +++ b/Source/EbsdLib/LaueOps/TriclinicOps.h @@ -262,11 +262,10 @@ class EbsdLib_EXPORT TriclinicOps : public LaueOps */ ebsdlib::UInt8ArrayType::Pointer generateIPFTriangleLegend(int imageDim, bool generateEntirePlane) const override; - void drawIPFAnnotations(canvas_ity::canvas& context, int canvasDim, - float fontPtSize, const std::vector& margins, - std::array figureOrigin, - std::array figureCenter, - bool drawFullCircle) const override; + bool mapPixelToSphereSST(int xPixel, int yPixel, int imageDim, std::array& sphereDir) const override; + + void drawIPFAnnotations(canvas_ity::canvas& context, int canvasDim, float fontPtSize, const std::vector& margins, std::array figureOrigin, std::array figureCenter, + bool drawFullCircle) const override; /** * @brief Returns if the given Quaternion is within the Rodrigues Fundamental Zone (RFZ) diff --git a/Source/EbsdLib/LaueOps/TrigonalLowOps.cpp b/Source/EbsdLib/LaueOps/TrigonalLowOps.cpp index a76295b..8aa300a 100644 --- a/Source/EbsdLib/LaueOps/TrigonalLowOps.cpp +++ b/Source/EbsdLib/LaueOps/TrigonalLowOps.cpp @@ -849,11 +849,44 @@ ebsdlib::UInt8ArrayType::Pointer CreateIPFLegend(const TrigonalLowOps* ops, int } // namespace // ----------------------------------------------------------------------------- -std::array TrigonalLowOps::adjustFigureOrigin( - std::array figureOrigin, - int legendWidth, int legendHeight, - const std::vector& margins, float fontPtSize, - bool generateEntirePlane) const +bool TrigonalLowOps::mapPixelToSphereSST(int xPixel, int yPixel, int imageDim, std::array& sphereDir) const +{ + double xInc = 1.0 / static_cast(imageDim); + double yInc = 1.0 / static_cast(imageDim); + + double x = -1.0 + 2.0 * xPixel * xInc; + double y = -1.0 + 2.0 * yPixel * yInc; + + double sumSquares = (x * x) + (y * y); + if(sumSquares > 1.0) + { + return false; + } + + if(y > 0.0) + { + return false; + } + + // Find the slope of the bounding line. + static const double m = std::sin(60.0 * ebsdlib::constants::k_PiOver180D) / std::cos(60.0 * ebsdlib::constants::k_PiOver180D); + + if(x <= 0.0 && y <= 0.0 && x < y / m) + { + return false; + } + + auto sc = stereographic::utils::StereoToSpherical(x, y).normalize(); + + sphereDir[0] = static_cast(sc[0]); + sphereDir[1] = static_cast(sc[1]); + sphereDir[2] = static_cast(sc[2]); + return true; +} + +// ----------------------------------------------------------------------------- +std::array TrigonalLowOps::adjustFigureOrigin(std::array figureOrigin, int legendWidth, int legendHeight, const std::vector& margins, float fontPtSize, + bool generateEntirePlane) const { if(!generateEntirePlane) { @@ -865,8 +898,7 @@ std::array TrigonalLowOps::adjustFigureOrigin( // ----------------------------------------------------------------------------- void TrigonalLowOps::drawIPFAnnotations(canvas_ity::canvas& context, int canvasDim, float fontPtSize, const std::vector& margins, std::array figureOrigin, - std::array figureCenter, - bool drawFullCircle) const + std::array figureCenter, bool drawFullCircle) const { int legendHeight = canvasDim - margins[0] - margins[2]; int legendWidth = canvasDim - margins[1] - margins[3]; @@ -965,12 +997,7 @@ ebsdlib::UInt8ArrayType::Pointer TrigonalLowOps::generateIPFTriangleLegend(int c { // Compute legend dimensions (same formula as annotateIPFImage uses) const float fontPtSize = static_cast(canvasDim) / 24.0f; - const std::vector margins = { - fontPtSize * 3, - static_cast(canvasDim / 7.0f), - fontPtSize * 2, - static_cast(canvasDim / 7.0f) - }; + const std::vector margins = {fontPtSize * 3, static_cast(canvasDim / 7.0f), fontPtSize * 2, static_cast(canvasDim / 7.0f)}; int legendHeight = canvasDim - static_cast(margins[0]) - static_cast(margins[2]); int legendWidth = canvasDim - static_cast(margins[1]) - static_cast(margins[3]); if(legendHeight > legendWidth) diff --git a/Source/EbsdLib/LaueOps/TrigonalLowOps.h b/Source/EbsdLib/LaueOps/TrigonalLowOps.h index b7049bf..da3c27c 100644 --- a/Source/EbsdLib/LaueOps/TrigonalLowOps.h +++ b/Source/EbsdLib/LaueOps/TrigonalLowOps.h @@ -264,17 +264,13 @@ class EbsdLib_EXPORT TrigonalLowOps : public LaueOps */ ebsdlib::UInt8ArrayType::Pointer generateIPFTriangleLegend(int imageDim, bool generateEntirePlane) const override; - void drawIPFAnnotations(canvas_ity::canvas& context, int canvasDim, - float fontPtSize, const std::vector& margins, - std::array figureOrigin, - std::array figureCenter, - bool drawFullCircle) const override; - - std::array adjustFigureOrigin( - std::array figureOrigin, - int legendWidth, int legendHeight, - const std::vector& margins, float fontPtSize, - bool generateEntirePlane) const override; + bool mapPixelToSphereSST(int xPixel, int yPixel, int imageDim, std::array& sphereDir) const override; + + void drawIPFAnnotations(canvas_ity::canvas& context, int canvasDim, float fontPtSize, const std::vector& margins, std::array figureOrigin, std::array figureCenter, + bool drawFullCircle) const override; + + std::array adjustFigureOrigin(std::array figureOrigin, int legendWidth, int legendHeight, const std::vector& margins, float fontPtSize, + bool generateEntirePlane) const override; /** * @brief Returns if the given Quaternion is within the Rodrigues Fundamental Zone (RFZ) diff --git a/Source/EbsdLib/LaueOps/TrigonalOps.cpp b/Source/EbsdLib/LaueOps/TrigonalOps.cpp index be03658..8a80199 100644 --- a/Source/EbsdLib/LaueOps/TrigonalOps.cpp +++ b/Source/EbsdLib/LaueOps/TrigonalOps.cpp @@ -865,11 +865,44 @@ ebsdlib::UInt8ArrayType::Pointer CreateIPFLegend(const TrigonalOps* ops, int ima } // namespace // ----------------------------------------------------------------------------- -std::array TrigonalOps::adjustFigureOrigin( - std::array figureOrigin, - int legendWidth, int legendHeight, - const std::vector& margins, float fontPtSize, - bool generateEntirePlane) const +bool TrigonalOps::mapPixelToSphereSST(int xPixel, int yPixel, int imageDim, std::array& sphereDir) const +{ + double xInc = 1.0 / static_cast(imageDim); + double yInc = 1.0 / static_cast(imageDim); + + double x = -1.0 + 2.0 * xPixel * xInc; + double y = -1.0 + 2.0 * yPixel * yInc; + + double sumSquares = (x * x) + (y * y); + if(sumSquares > 1.0) + { + return false; + } + + if(x < 0.0 || y > 0.0) + { + return false; + } + + auto sc = stereographic::utils::StereoToSpherical(x, y).normalize(); + + // Find the slope of the bounding line. + static const double m = std::sin(30.0 * ebsdlib::constants::k_PiOver180D) / std::cos(30.0 * ebsdlib::constants::k_PiOver180D); + + if(std::fabs(sc[1] / sc[0]) < m) + { + return false; + } + + sphereDir[0] = static_cast(sc[0]); + sphereDir[1] = static_cast(sc[1]); + sphereDir[2] = static_cast(sc[2]); + return true; +} + +// ----------------------------------------------------------------------------- +std::array TrigonalOps::adjustFigureOrigin(std::array figureOrigin, int legendWidth, int legendHeight, const std::vector& margins, float fontPtSize, + bool generateEntirePlane) const { if(!generateEntirePlane) { @@ -881,8 +914,7 @@ std::array TrigonalOps::adjustFigureOrigin( // ----------------------------------------------------------------------------- void TrigonalOps::drawIPFAnnotations(canvas_ity::canvas& context, int canvasDim, float fontPtSize, const std::vector& margins, std::array figureOrigin, - std::array figureCenter, - bool drawFullCircle) const + std::array figureCenter, bool drawFullCircle) const { int legendHeight = canvasDim - margins[0] - margins[2]; int legendWidth = canvasDim - margins[1] - margins[3]; @@ -973,12 +1005,7 @@ ebsdlib::UInt8ArrayType::Pointer TrigonalOps::generateIPFTriangleLegend(int canv { // Compute legend dimensions (same formula as annotateIPFImage uses) const float fontPtSize = static_cast(canvasDim) / 24.0f; - const std::vector margins = { - fontPtSize * 3, - static_cast(canvasDim / 7.0f), - fontPtSize * 2, - static_cast(canvasDim / 7.0f) - }; + const std::vector margins = {fontPtSize * 3, static_cast(canvasDim / 7.0f), fontPtSize * 2, static_cast(canvasDim / 7.0f)}; int legendHeight = canvasDim - static_cast(margins[0]) - static_cast(margins[2]); int legendWidth = canvasDim - static_cast(margins[1]) - static_cast(margins[3]); if(legendHeight > legendWidth) diff --git a/Source/EbsdLib/LaueOps/TrigonalOps.h b/Source/EbsdLib/LaueOps/TrigonalOps.h index 8b73f85..1dfbfa6 100644 --- a/Source/EbsdLib/LaueOps/TrigonalOps.h +++ b/Source/EbsdLib/LaueOps/TrigonalOps.h @@ -263,17 +263,13 @@ class EbsdLib_EXPORT TrigonalOps : public LaueOps */ ebsdlib::UInt8ArrayType::Pointer generateIPFTriangleLegend(int imageDim, bool generateEntirePlane) const override; - void drawIPFAnnotations(canvas_ity::canvas& context, int canvasDim, - float fontPtSize, const std::vector& margins, - std::array figureOrigin, - std::array figureCenter, - bool drawFullCircle) const override; - - std::array adjustFigureOrigin( - std::array figureOrigin, - int legendWidth, int legendHeight, - const std::vector& margins, float fontPtSize, - bool generateEntirePlane) const override; + bool mapPixelToSphereSST(int xPixel, int yPixel, int imageDim, std::array& sphereDir) const override; + + void drawIPFAnnotations(canvas_ity::canvas& context, int canvasDim, float fontPtSize, const std::vector& margins, std::array figureOrigin, std::array figureCenter, + bool drawFullCircle) const override; + + std::array adjustFigureOrigin(std::array figureOrigin, int legendWidth, int legendHeight, const std::vector& margins, float fontPtSize, + bool generateEntirePlane) const override; /** * @brief Returns if the given Quaternion is within the Rodrigues Fundamental Zone (RFZ) diff --git a/Source/EbsdLib/Utilities/InversePoleFigureUtilities.cpp b/Source/EbsdLib/Utilities/InversePoleFigureUtilities.cpp index 3641abf..829dbb2 100644 --- a/Source/EbsdLib/Utilities/InversePoleFigureUtilities.cpp +++ b/Source/EbsdLib/Utilities/InversePoleFigureUtilities.cpp @@ -137,7 +137,7 @@ ebsdlib::FloatArrayType::Pointer InversePoleFigureUtilities::computeIPFDirection // ----------------------------------------------------------------------------- ebsdlib::DoubleArrayType::Pointer InversePoleFigureUtilities::computeIPFIntensity(const LaueOps& ops, ebsdlib::FloatArrayType* ipfDirections, int imageWidth, int imageHeight, int lambertDim, - bool normalizeMRD) + bool normalizeMRD, bool useStereographicSST) { // Step 1: Bin the crystal directions into the Lambert projection float sphereRadius = 1.0f; @@ -169,66 +169,100 @@ ebsdlib::DoubleArrayType::Pointer InversePoleFigureUtilities::computeIPFIntensit // If not MRD, leave as raw counts } - // Step 3: Create the output intensity image using equal-area projection + // Step 3: Create the output intensity image std::vector tDims = {static_cast(imageWidth * imageHeight)}; std::vector cDims = {1}; ebsdlib::DoubleArrayType::Pointer intensity = ebsdlib::DoubleArrayType::CreateArray(tDims, cDims, "IPF_Intensity", true); double* intensityPtr = intensity->getPointer(0); - // Lambert azimuthal equal-area projection centered on north pole - // Maps the upper hemisphere (z >= 0) to a disk of radius sqrt(2) - float unitRadius = std::sqrt(2.0f); - float span = 2.0f * unitRadius; - float xres = span / static_cast(imageWidth); - float yres = span / static_cast(imageHeight); - - int halfWidth = imageWidth / 2; - int halfHeight = imageHeight / 2; - - for(int y = 0; y < imageHeight; y++) + if(useStereographicSST) { - for(int x = 0; x < imageWidth; x++) + // Use the same stereographic projection as CreateIPFLegend (SST-only view) + int imageDim = imageWidth; // Assumes square image + for(int y = 0; y < imageHeight; y++) { - int index = y * imageWidth + x; - - // Map pixel to equal-area projection coordinates - float xtmp = static_cast(x - halfWidth) * xres + (xres * 0.5f); - float ytmp = static_cast(y - halfHeight) * yres + (yres * 0.5f); - - float rhoSq = xtmp * xtmp + ytmp * ytmp; - - // Check if within hemisphere disk - if(rhoSq > 2.0f) + for(int x = 0; x < imageWidth; x++) { - intensityPtr[index] = -1.0; // Outside hemisphere - continue; + int index = y * imageWidth + x; + std::array sphereDir = {0.0f, 0.0f, 0.0f}; + + if(!ops.mapPixelToSphereSST(x, y, imageDim, sphereDir)) + { + intensityPtr[index] = -1.0; + continue; + } + + // Look up intensity from Lambert bins + std::array sqCoord = {0.0f, 0.0f}; + bool isNorth = lambert->getSquareCoord(sphereDir.data(), sqCoord.data()); + if(isNorth) + { + intensityPtr[index] = lambert->getInterpolatedValue(ModifiedLambertProjection::NorthSquare, sqCoord.data()); + } + else + { + intensityPtr[index] = lambert->getInterpolatedValue(ModifiedLambertProjection::SouthSquare, sqCoord.data()); + } } + } + } + else + { + // Lambert azimuthal equal-area projection centered on north pole + // Maps the upper hemisphere (z >= 0) to a disk of radius sqrt(2) + float unitRadius = std::sqrt(2.0f); + float span = 2.0f * unitRadius; + float xres = span / static_cast(imageWidth); + float yres = span / static_cast(imageHeight); - // Inverse Lambert azimuthal equal-area projection (north pole centered) - float t = std::sqrt(1.0f - rhoSq / 4.0f); - std::array xyz = {xtmp * t, ytmp * t, 1.0f - rhoSq / 2.0f}; - - // Compute chi (polar angle from z-axis) and eta (azimuthal angle) - double chi = std::acos(static_cast(xyz[2])); - double eta = std::atan2(static_cast(xyz[1]), static_cast(xyz[0])); - - // Check if direction is inside the Standard Stereographic Triangle - if(!ops.inUnitTriangle(eta, chi)) - { - intensityPtr[index] = -1.0; // Outside SST - continue; - } + int halfWidth = imageWidth / 2; + int halfHeight = imageHeight / 2; - // Look up the interpolated intensity from the Lambert projection - std::array sqCoord = {0.0f, 0.0f}; - bool isNorth = lambert->getSquareCoord(xyz.data(), sqCoord.data()); - if(isNorth) - { - intensityPtr[index] = lambert->getInterpolatedValue(ModifiedLambertProjection::NorthSquare, sqCoord.data()); - } - else + for(int y = 0; y < imageHeight; y++) + { + for(int x = 0; x < imageWidth; x++) { - intensityPtr[index] = lambert->getInterpolatedValue(ModifiedLambertProjection::SouthSquare, sqCoord.data()); + int index = y * imageWidth + x; + + // Map pixel to equal-area projection coordinates + float xtmp = static_cast(x - halfWidth) * xres + (xres * 0.5f); + float ytmp = static_cast(y - halfHeight) * yres + (yres * 0.5f); + + float rhoSq = xtmp * xtmp + ytmp * ytmp; + + // Check if within hemisphere disk + if(rhoSq > 2.0f) + { + intensityPtr[index] = -1.0; // Outside hemisphere + continue; + } + + // Inverse Lambert azimuthal equal-area projection (north pole centered) + float t = std::sqrt(1.0f - rhoSq / 4.0f); + std::array xyz = {xtmp * t, ytmp * t, 1.0f - rhoSq / 2.0f}; + + // Compute chi (polar angle from z-axis) and eta (azimuthal angle) + double chi = std::acos(static_cast(xyz[2])); + double eta = std::atan2(static_cast(xyz[1]), static_cast(xyz[0])); + + // Check if direction is inside the Standard Stereographic Triangle + if(!ops.inUnitTriangle(eta, chi)) + { + intensityPtr[index] = -1.0; // Outside SST + continue; + } + + // Look up the interpolated intensity from the Lambert projection + std::array sqCoord = {0.0f, 0.0f}; + bool isNorth = lambert->getSquareCoord(xyz.data(), sqCoord.data()); + if(isNorth) + { + intensityPtr[index] = lambert->getInterpolatedValue(ModifiedLambertProjection::NorthSquare, sqCoord.data()); + } + else + { + intensityPtr[index] = lambert->getInterpolatedValue(ModifiedLambertProjection::SouthSquare, sqCoord.data()); + } } } } @@ -237,7 +271,8 @@ ebsdlib::DoubleArrayType::Pointer InversePoleFigureUtilities::computeIPFIntensit } // ----------------------------------------------------------------------------- -void InversePoleFigureUtilities::createIPFColorImage(ebsdlib::DoubleArrayType* intensity, int imageWidth, int imageHeight, int numColors, double minScale, double maxScale, ebsdlib::UInt8ArrayType* rgba) +void InversePoleFigureUtilities::createIPFColorImage(ebsdlib::DoubleArrayType* intensity, int imageWidth, int imageHeight, int numColors, double minScale, double maxScale, + ebsdlib::UInt8ArrayType* rgba) { // Initialize the image with all zeros rgba->initializeWithZeros(); diff --git a/Source/EbsdLib/Utilities/InversePoleFigureUtilities.h b/Source/EbsdLib/Utilities/InversePoleFigureUtilities.h index 5d462ac..a20bacf 100644 --- a/Source/EbsdLib/Utilities/InversePoleFigureUtilities.h +++ b/Source/EbsdLib/Utilities/InversePoleFigureUtilities.h @@ -56,17 +56,17 @@ class LaueOps; // Forward declaration */ struct InversePoleFigureConfiguration_t { - ebsdlib::FloatArrayType* eulers; ///<* The Euler Angles (in Radians) to use for the inverse pole figure - std::array sampleDirections; ///<* 3 orthogonal sample reference directions (e.g., RD, TD, ND) - int imageWidth; ///<* The width of the generated inverse pole figure image in pixels - int imageHeight; ///<* The height of the generated inverse pole figure image in pixels - int lambertDim; ///<* The dimensions in voxels of the Lambert Square used for binning/smoothing - int numColors; ///<* The number of colors to use in the color map - std::string colorMap; ///<* Name of the ColorMap to use - bool normalizeMRD; ///<* true=normalize to MRD (Multiples of Random Distribution), false=raw counts - std::vector labels; ///<* The labels for each of the 3 inverse pole figures (e.g., "RD", "TD", "ND") - std::string phaseName; ///<* The name of the phase - bool FlipFinalImage; ///<* If TRUE, the final image will be flipped across the X Axis so that +Y axis points UP + ebsdlib::FloatArrayType* eulers; ///<* The Euler Angles (in Radians) to use for the inverse pole figure + std::array sampleDirections; ///<* 3 orthogonal sample reference directions (e.g., RD, TD, ND) + int imageWidth; ///<* The width of the generated inverse pole figure image in pixels + int imageHeight; ///<* The height of the generated inverse pole figure image in pixels + int lambertDim; ///<* The dimensions in voxels of the Lambert Square used for binning/smoothing + int numColors; ///<* The number of colors to use in the color map + std::string colorMap; ///<* Name of the ColorMap to use + bool normalizeMRD; ///<* true=normalize to MRD (Multiples of Random Distribution), false=raw counts + std::vector labels; ///<* The labels for each of the 3 inverse pole figures (e.g., "RD", "TD", "ND") + std::string phaseName; ///<* The name of the phase + bool FlipFinalImage; ///<* If TRUE, the final image will be flipped across the X Axis so that +Y axis points UP }; /** @@ -106,7 +106,8 @@ class EbsdLib_EXPORT InversePoleFigureUtilities * @param normalizeMRD true to normalize to MRD, false for raw counts * @return DoubleArrayType intensity image (imageWidth * imageHeight). Pixels outside SST have value -1.0. */ - static ebsdlib::DoubleArrayType::Pointer computeIPFIntensity(const LaueOps& ops, ebsdlib::FloatArrayType* ipfDirections, int imageWidth, int imageHeight, int lambertDim, bool normalizeMRD); + static ebsdlib::DoubleArrayType::Pointer computeIPFIntensity(const LaueOps& ops, ebsdlib::FloatArrayType* ipfDirections, int imageWidth, int imageHeight, int lambertDim, bool normalizeMRD, + bool useStereographicSST = false); /** * @brief Converts an intensity image to RGBA with SST masking. Pixels inside the SST From 3cebb9414f58c94efb5590b082f0b5d414214069 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 25 Mar 2026 11:25:18 -0400 Subject: [PATCH 11/14] ENH: Fix color bar positioning to avoid overlapping with Miller index labels Add hasColorBar parameter to annotateIPFImage() that widens the right margin when a color bar will be drawn. Reposition the color bar relative to the figure's right edge instead of using absolute canvas percentages. Co-Authored-By: Claude Opus 4.6 (1M context) --- Source/EbsdLib/LaueOps/LaueOps.cpp | 36 +++++++++++++++++++++--------- Source/EbsdLib/LaueOps/LaueOps.h | 3 ++- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/Source/EbsdLib/LaueOps/LaueOps.cpp b/Source/EbsdLib/LaueOps/LaueOps.cpp index 0b01598..3b7a652 100644 --- a/Source/EbsdLib/LaueOps/LaueOps.cpp +++ b/Source/EbsdLib/LaueOps/LaueOps.cpp @@ -956,12 +956,15 @@ std::array LaueOps::adjustFigureOrigin(std::array figureOrig } // ----------------------------------------------------------------------------- -UInt8ArrayType::Pointer LaueOps::annotateIPFImage(UInt8ArrayType::Pointer triangleImage, int imageDim, int canvasDim, const std::string& title, bool generateEntirePlane) const +UInt8ArrayType::Pointer LaueOps::annotateIPFImage(UInt8ArrayType::Pointer triangleImage, int imageDim, int canvasDim, const std::string& title, bool generateEntirePlane, + bool hasColorBar) const { const float fontPtSize = static_cast(canvasDim) / 24.0f; + // When a color bar will be drawn, use a wider right margin to make room + float rightMargin = hasColorBar ? static_cast(canvasDim / 3.5f) : static_cast(canvasDim / 7.0f); const std::vector margins = { fontPtSize * 3, // Top - static_cast(canvasDim / 7.0f), // Right + rightMargin, // Right fontPtSize * 2, // Bottom static_cast(canvasDim / 7.0f) // Left }; @@ -1055,11 +1058,24 @@ UInt8ArrayType::Pointer LaueOps::drawColorBar(UInt8ArrayType::Pointer image, int // Put the existing image onto the canvas context.draw_image(rgbaImage->getPointer(0), canvasDim, canvasDim, canvasDim * 4, 0.0f, 0.0f, static_cast(canvasDim), static_cast(canvasDim)); - // Color bar dimensions - const float barLeft = static_cast(canvasDim) * 0.80f; - const float barTop = static_cast(canvasDim) * 0.15f; - const float barWidth = static_cast(canvasDim) * 0.04f; - const float barHeight = static_cast(canvasDim) * 0.65f; + // Color bar dimensions — positioned in the right margin area + // Compute the figure right edge using the same layout as annotateIPFImage with hasColorBar=true + float rightMargin = static_cast(canvasDim / 3.5f); + float leftMargin = static_cast(canvasDim / 7.0f); + float topMargin = fontPtSize * 3; + float bottomMargin = fontPtSize * 2; + int legendHeight = canvasDim - static_cast(topMargin) - static_cast(bottomMargin); + int legendWidth = canvasDim - static_cast(rightMargin) - static_cast(leftMargin); + if(legendHeight > legendWidth) + { + legendHeight = legendWidth; + } + float figureRightEdge = leftMargin + static_cast(legendWidth); + + const float barLeft = figureRightEdge + fontPtSize * 2.5f; + const float barTop = topMargin * 1.33f; + const float barWidth = fontPtSize * 0.8f; + const float barHeight = static_cast(legendHeight) * 0.75f; // Draw color bar segments int colorSegments = numColors; @@ -1211,9 +1227,9 @@ std::vector LaueOps::generateAnnotatedIPFDensity(Invers std::string titlePrefix = config.phaseName.empty() ? "" : config.phaseName + " - "; // Step 6: Annotate each image - UInt8ArrayType::Pointer annotated0 = annotateIPFImage(image0, imageDim, canvasDim, titlePrefix + label0, false); - UInt8ArrayType::Pointer annotated1 = annotateIPFImage(image1, imageDim, canvasDim, titlePrefix + label1, false); - UInt8ArrayType::Pointer annotated2 = annotateIPFImage(image2, imageDim, canvasDim, titlePrefix + label2, false); + UInt8ArrayType::Pointer annotated0 = annotateIPFImage(image0, imageDim, canvasDim, titlePrefix + label0, false, true); + UInt8ArrayType::Pointer annotated1 = annotateIPFImage(image1, imageDim, canvasDim, titlePrefix + label1, false, true); + UInt8ArrayType::Pointer annotated2 = annotateIPFImage(image2, imageDim, canvasDim, titlePrefix + label2, false, true); // Step 7: Add color bars annotated0 = drawColorBar(annotated0, canvasDim, config.numColors, globalMin, globalMax, config.normalizeMRD); diff --git a/Source/EbsdLib/LaueOps/LaueOps.h b/Source/EbsdLib/LaueOps/LaueOps.h index 5b35b10..7285ac9 100644 --- a/Source/EbsdLib/LaueOps/LaueOps.h +++ b/Source/EbsdLib/LaueOps/LaueOps.h @@ -498,7 +498,8 @@ class EbsdLib_EXPORT LaueOps * @param generateEntirePlane true = full circle view, false = SST only * @return RGB image (canvasDim x canvasDim, 3 components) */ - UInt8ArrayType::Pointer annotateIPFImage(UInt8ArrayType::Pointer triangleImage, int imageDim, int canvasDim, const std::string& title, bool generateEntirePlane) const; + UInt8ArrayType::Pointer annotateIPFImage(UInt8ArrayType::Pointer triangleImage, int imageDim, int canvasDim, const std::string& title, bool generateEntirePlane, + bool hasColorBar = false) const; /** * @brief Draws a color bar with min/max labels onto an existing RGB image. From fd22aaeec5ec7826ac266f01da68d29a9afb7255 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 25 Mar 2026 12:03:36 -0400 Subject: [PATCH 12/14] ENH: Update make_ipf to support both .ang and .ctf file formats Detects file type from extension. CTF Euler angles are converted from degrees to radians. Refactored GenerateIPFColorsImpl to use LaueOps indices directly instead of AngPhase::Pointer, so it works with both file formats. Co-Authored-By: Claude Opus 4.6 (1M context) --- Source/Apps/make_ipf.cpp | 235 ++++++++++++++++++++++++--------------- 1 file changed, 143 insertions(+), 92 deletions(-) diff --git a/Source/Apps/make_ipf.cpp b/Source/Apps/make_ipf.cpp index c856c6e..ab5b701 100644 --- a/Source/Apps/make_ipf.cpp +++ b/Source/Apps/make_ipf.cpp @@ -1,35 +1,37 @@ +#include #include #include +#include #include #include #include #include #include "EbsdLib/Core/EbsdLibConstants.h" +#include "EbsdLib/IO/HKL/CtfPhase.h" +#include "EbsdLib/IO/HKL/CtfReader.h" #include "EbsdLib/IO/TSL/AngPhase.h" #include "EbsdLib/IO/TSL/AngReader.h" #include "EbsdLib/LaueOps/LaueOps.h" #include "EbsdLib/Utilities/ColorTable.h" #include "EbsdLib/Utilities/TiffWriter.h" -class Ang2IPF; - using FloatVec3Type = std::array; using namespace ebsdlib; /** - * @brief The GenerateIPFColorsImpl class implements a threaded algorithm that computes the IPF - * colors for each element in a geometry + * @brief The GenerateIPFColorsImpl class computes the IPF colors for each element in a geometry. + * Uses LaueOps indices directly so it works with both .ang and .ctf phase data. */ class GenerateIPFColorsImpl { public: - GenerateIPFColorsImpl(Matrix3X1F& referenceDir, const std::vector& eulers, int32_t* phases, std::vector& crystalStructures, bool* goodVoxels, uint8_t* colors) + GenerateIPFColorsImpl(Matrix3X1F& referenceDir, const std::vector& eulers, int32_t* phases, const std::vector& laueOpsIndices, bool* goodVoxels, uint8_t* colors) : m_ReferenceDir(referenceDir) , m_CellEulerAngles(eulers) , m_CellPhases(phases) - , m_PhaseInfos(crystalStructures) + , m_LaueOpsIndices(laueOpsIndices) , m_GoodVoxels(goodVoxels) , m_CellIPFColors(colors) { @@ -46,13 +48,7 @@ class GenerateIPFColorsImpl int32_t phase = 0; bool calcIPF = false; size_t index = 0; - int32_t numPhases = static_cast(m_PhaseInfos.size()); - - std::vector laueOpsIndex(m_PhaseInfos.size()); - for(size_t i = 0; i < laueOpsIndex.size(); i++) - { - laueOpsIndex[i] = m_PhaseInfos[i]->determineOrientationOpsIndex(); - } + int32_t numPhases = static_cast(m_LaueOpsIndices.size()); size_t totalPoints = m_CellEulerAngles.size() / 3; for(size_t i = 0; i < totalPoints; i++) @@ -66,20 +62,17 @@ class GenerateIPFColorsImpl dEuler[1] = m_CellEulerAngles[index + 1]; dEuler[2] = m_CellEulerAngles[index + 2]; - // Make sure we are using a valid Euler Angles with valid crystal symmetry calcIPF = true; if(nullptr != m_GoodVoxels) { calcIPF = m_GoodVoxels[i]; } - // Sanity check the phase data to make sure we do not walk off the end of the array if(phase >= numPhases) { - // m_Filter->incrementPhaseWarningCount(); std::cout << "phase > number of phases" << std::endl; } - size_t currentLaueOpsIndex = laueOpsIndex[phase]; + size_t currentLaueOpsIndex = m_LaueOpsIndices[phase]; if(phase < numPhases && calcIPF && currentLaueOpsIndex < ebsdlib::CrystalStructure::LaueGroupEnd) { @@ -87,9 +80,6 @@ class GenerateIPFColorsImpl m_CellIPFColors[index] = static_cast(ebsdlib::RgbColor::dRed(argb)); m_CellIPFColors[index + 1] = static_cast(ebsdlib::RgbColor::dGreen(argb)); m_CellIPFColors[index + 2] = static_cast(ebsdlib::RgbColor::dBlue(argb)); - - // std::cout << (int32_t)(m_CellIPFColors[index]) << "\t" << (int32_t)(m_CellIPFColors[index + 1]) << (int32_t)(m_CellIPFColors[index + 2]) << m_CellEulerAngles[index] << "\t" - // << m_CellEulerAngles[index + 1] << "\t" << m_CellEulerAngles[index + 2] << std::endl; } } } @@ -98,120 +88,181 @@ class GenerateIPFColorsImpl Matrix3X1F m_ReferenceDir; const std::vector& m_CellEulerAngles; int32_t* m_CellPhases; - std::vector m_PhaseInfos; + std::vector m_LaueOpsIndices; bool* m_GoodVoxels; uint8_t* m_CellIPFColors; }; // ----------------------------------------------------------------------------- -class Ang2IPF +// Reads a .ang file and generates an IPF color map image. +// ----------------------------------------------------------------------------- +int32_t executeAng(const std::string& filepath, const std::string& outputFile, Matrix3X1F& refDir) { -public: - Ang2IPF() + AngReader reader; + reader.setFileName(filepath); + int32_t err = reader.readFile(); + if(err < 0) { + std::cerr << "Error reading .ang file: " << filepath << std::endl; + return err; } - ~Ang2IPF() = default; - Ang2IPF(const Ang2IPF&) = delete; // Copy Constructor Not Implemented - Ang2IPF(Ang2IPF&&) = delete; // Move Constructor Not Implemented - Ang2IPF& operator=(const Ang2IPF&) = delete; // Copy Assignment Not Implemented - Ang2IPF& operator=(Ang2IPF&&) = delete; // Move Assignment Not Implemented + std::vector dims = {reader.getXDimension(), reader.getYDimension()}; + size_t totalPoints = reader.getNumberOfElements(); - Matrix3X1F m_ReferenceDir = {0.0f, 0.0f, 1.0f}; + // Build LaueOps index vector. Insert a dummy at index 0 since ANG phases are 1-based. + std::vector angPhases = reader.getPhaseVector(); + std::vector laueOpsIndices; + laueOpsIndices.push_back(0); // Dummy for index 0 + for(const auto& phase : angPhases) + { + laueOpsIndices.push_back(phase->determineOrientationOpsIndex()); + } + + Matrix3X1F normRefDir = refDir.normalize(); + + // ANG Euler angles are in radians — interleave into a single array + float* phi1Ptr = reader.getPhi1Pointer(false); + float* phiPtr = reader.getPhiPointer(false); + float* phi2Ptr = reader.getPhi2Pointer(false); - /** - * @brief incrementPhaseWarningCount - */ - void incrementPhaseWarningCount() + std::vector eulers(3 * totalPoints); + for(size_t i = 0; i < totalPoints; i++) { - m_PhaseWarningCount++; + eulers[i * 3] = phi1Ptr[i]; + eulers[i * 3 + 1] = phiPtr[i]; + eulers[i * 3 + 2] = phi2Ptr[i]; } - /** - * @brief execute - * @return - */ - int32_t execute(const std::string& filepath, const std::string& outputFile) + // Map phase 0 (unindexed) to phase 1 + int32_t* phaseData = reader.getPhaseDataPointer(false); + for(size_t i = 0; i < totalPoints; i++) { - m_PhaseWarningCount = 0; - AngReader reader; - reader.setFileName(filepath); - int32_t err = reader.readFile(); - if(err < 0) + if(phaseData[i] < 1) { - return err; + phaseData[i] = 1; } + } - std::vector dims = {reader.getXDimension(), reader.getYDimension()}; + bool* goodVoxels = nullptr; + std::vector ipfColors(totalPoints * 3, 0); + GenerateIPFColorsImpl generateIPF(normRefDir, eulers, phaseData, laueOpsIndices, goodVoxels, ipfColors.data()); + generateIPF.run(); - size_t totalPoints = reader.getNumberOfElements(); - std::vector crystalStructures = reader.getPhaseVector(); - crystalStructures.emplace(crystalStructures.begin(), AngPhase::New()); - // int32_t numPhase = static_cast(crystalStructures.size()); + auto error = TiffWriter::WriteColorImage(outputFile, dims[0], dims[1], 3, ipfColors.data()); + if(error.first < 0) + { + std::cerr << error.second << std::endl; + } + return error.first; +} - // Make sure we are dealing with a unit 1 vector. - Matrix3X1F normRefDir = m_ReferenceDir.normalize(); // Make a copy of the reference Direction and normalize it +// ----------------------------------------------------------------------------- +// Reads a .ctf file and generates an IPF color map image. +// CTF Euler angles are in degrees and must be converted to radians. +// ----------------------------------------------------------------------------- +int32_t executeCtf(const std::string& filepath, const std::string& outputFile, Matrix3X1F& refDir) +{ + CtfReader reader; + reader.setFileName(filepath); + int32_t err = reader.readFile(); + if(err < 0) + { + std::cerr << "Error reading .ctf file: " << filepath << std::endl; + return err; + } - float* phi1Ptr = reader.getPhi1Pointer(false); - float* phiPtr = reader.getPhiPointer(false); - float* phi2Ptr = reader.getPhi2Pointer(false); + std::vector dims = {reader.getXDimension(), reader.getYDimension()}; + size_t totalPoints = reader.getNumberOfElements(); - // We need to interleave the phi1, PHI, phi2 data into a single 3 component array - std::vector eulers(3 * totalPoints); + // Build LaueOps index vector. Insert a dummy at index 0 since CTF phases are 1-based. + std::vector ctfPhases = reader.getPhaseVector(); + std::vector laueOpsIndices; + laueOpsIndices.push_back(0); // Dummy for index 0 + for(const auto& phase : ctfPhases) + { + laueOpsIndices.push_back(phase->determineOrientationOpsIndex()); + } - for(size_t i = 0; i < totalPoints; i++) - { - eulers[i * 3] = phi1Ptr[i]; - eulers[i * 3 + 1] = phiPtr[i]; - eulers[i * 3 + 2] = phi2Ptr[i]; - } + Matrix3X1F normRefDir = refDir.normalize(); - int32_t* phaseData = reader.getPhaseDataPointer(false); - for(size_t i = 0; i < totalPoints; i++) - { - if(phaseData[i] < 1) - { - phaseData[i] = 1; - } - } + // CTF Euler angles are in degrees — convert to radians and interleave + float* euler1Ptr = reader.getEuler1Pointer(); + float* euler2Ptr = reader.getEuler2Pointer(); + float* euler3Ptr = reader.getEuler3Pointer(); + const float degToRad = static_cast(ebsdlib::constants::k_DegToRadD); - bool* goodVoxels = nullptr; - std::vector ipfColors(totalPoints * 3, 0); - GenerateIPFColorsImpl generateIPF(normRefDir, eulers, phaseData, crystalStructures, goodVoxels, ipfColors.data()); - generateIPF.run(); + std::vector eulers(3 * totalPoints); + for(size_t i = 0; i < totalPoints; i++) + { + eulers[i * 3] = euler1Ptr[i] * degToRad; + eulers[i * 3 + 1] = euler2Ptr[i] * degToRad; + eulers[i * 3 + 2] = euler3Ptr[i] * degToRad; + } - std::pair error = TiffWriter::WriteColorImage(outputFile, dims[0], dims[1], 3, ipfColors.data()); - if(error.first < 0) - { - std::cout << error.second << std::endl; - } - return error.first; + // Map phase 0 (unindexed) to phase 1 + int* phaseData = reader.getPhasePointer(); + std::vector phases(totalPoints); + for(size_t i = 0; i < totalPoints; i++) + { + phases[i] = (phaseData[i] < 1) ? 1 : phaseData[i]; } -private: - int32_t m_PhaseWarningCount = {0}; -}; + bool* goodVoxels = nullptr; + std::vector ipfColors(totalPoints * 3, 0); + GenerateIPFColorsImpl generateIPF(normRefDir, eulers, phases.data(), laueOpsIndices, goodVoxels, ipfColors.data()); + generateIPF.run(); + + auto error = TiffWriter::WriteColorImage(outputFile, dims[0], dims[1], 3, ipfColors.data()); + if(error.first < 0) + { + std::cerr << error.second << std::endl; + } + return error.first; +} // ----------------------------------------------------------------------------- int main(int argc, char* argv[]) { - if(argc != 3) { - std::cout << "Program needs file path to .ang file and output image file" << std::endl; + std::cout << "Usage: make_ipf " << std::endl; return 1; } - std::cout << "WARNING: This program makes NO attempt to fix the sample and crystal reference frame issue that is common on TSL systems." << std::endl; + + std::cout << "WARNING: This program makes NO attempt to fix the sample and crystal reference frame issue." << std::endl; std::cout << "WARNING: You are probably *not* seeing the correct colors. Use something like DREAM.3D to fully correct for these issues." << std::endl; + std::string filePath(argv[1]); std::string outPath(argv[2]); + + // Determine file type from extension + std::string ext = std::filesystem::path(filePath).extension().string(); + std::transform(ext.begin(), ext.end(), ext.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); + + Matrix3X1F referenceDir = {0.0f, 0.0f, 1.0f}; + std::cout << "Creating IPF Color Map for " << filePath << std::endl; - Ang2IPF Ang2IPF; - if(Ang2IPF.execute(filePath, outPath) < 0) + int32_t result = -1; + if(ext == ".ang") + { + result = executeAng(filePath, outPath, referenceDir); + } + else if(ext == ".ctf") + { + result = executeCtf(filePath, outPath, referenceDir); + } + else + { + std::cerr << "ERROR: Unsupported file extension '" << ext << "'. Use .ang or .ctf" << std::endl; + return 1; + } + + if(result < 0) { - std::cout << "Error creating the IPF Color map" << std::endl; + std::cerr << "Error creating the IPF Color map" << std::endl; } - return 0; + return result < 0 ? 1 : 0; } From 462edee6e2ed92c9decf56c833491fd4f0e415bc Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 25 Mar 2026 12:09:06 -0400 Subject: [PATCH 13/14] ENH: Add generate_pole_figure program for .ang and .ctf files Reads an EBSD data file (.ang or .ctf), groups orientations by phase, and generates 3 pole figure TIFF images per phase using the default pole figure directions for each crystal symmetry class. Includes stereographic projection annotations (great circles, Miller indices). Co-Authored-By: Claude Opus 4.6 (1M context) --- Source/Apps/SourceList.cmake | 4 + Source/Apps/generate_pole_figure.cpp | 441 +++++++++++++++++++++++++++ 2 files changed, 445 insertions(+) create mode 100644 Source/Apps/generate_pole_figure.cpp diff --git a/Source/Apps/SourceList.cmake b/Source/Apps/SourceList.cmake index bcb1e34..55248b4 100644 --- a/Source/Apps/SourceList.cmake +++ b/Source/Apps/SourceList.cmake @@ -41,6 +41,10 @@ add_executable(generate_ipf_from_file ${EbsdLibProj_SOURCE_DIR}/Source/Apps/gene target_link_libraries(generate_ipf_from_file PUBLIC EbsdLib) target_include_directories(generate_ipf_from_file PUBLIC ${EbsdLibProj_SOURCE_DIR}/Source) +add_executable(generate_pole_figure ${EbsdLibProj_SOURCE_DIR}/Source/Apps/generate_pole_figure.cpp) +target_link_libraries(generate_pole_figure PUBLIC EbsdLib) +target_include_directories(generate_pole_figure PUBLIC ${EbsdLibProj_SOURCE_DIR}/Source) + add_executable(ParseAztecProject ${EbsdLibProj_SOURCE_DIR}/Source/Apps/ParseAztecProject.cpp) target_link_libraries(ParseAztecProject PUBLIC EbsdLib) target_include_directories(ParseAztecProject PUBLIC ${EbsdLibProj_SOURCE_DIR}/Source) diff --git a/Source/Apps/generate_pole_figure.cpp b/Source/Apps/generate_pole_figure.cpp new file mode 100644 index 0000000..f97bad0 --- /dev/null +++ b/Source/Apps/generate_pole_figure.cpp @@ -0,0 +1,441 @@ +/* ============================================================================ + * Copyright (c) 2025-2026 BlueQuartz Software, LLC + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or + * other materials provided with the distribution. + * + * Neither the name of BlueQuartz Software, the US Air Force, nor the names of its + * contributors may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ + +/** + * @file generate_pole_figure.cpp + * @brief Reads a .ctf or .ang EBSD data file and generates Pole Figure images + * for each phase found in the data. + * + * For each phase, 3 TIFF images are generated corresponding to the default pole + * figure directions for that crystal symmetry class (e.g., {001}, {011}, {111} + * for cubic). + * + * Usage: + * generate_pole_figure [output_directory] + * + * If no output directory is specified, images are written next to the input file. + */ + +#include "EbsdLib/Core/EbsdDataArray.hpp" +#include "EbsdLib/Core/EbsdLibConstants.h" +#include "EbsdLib/IO/HKL/CtfPhase.h" +#include "EbsdLib/IO/HKL/CtfReader.h" +#include "EbsdLib/IO/TSL/AngPhase.h" +#include "EbsdLib/IO/TSL/AngReader.h" +#include "EbsdLib/LaueOps/LaueOps.h" +#include "EbsdLib/Utilities/CanvasUtilities.hpp" +#include "EbsdLib/Utilities/EbsdStringUtils.hpp" +#include "EbsdLib/Utilities/PoleFigureUtilities.h" +#include "EbsdLib/Utilities/TiffWriter.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace ebsdlib; + +namespace +{ + +// ----------------------------------------------------------------------- +// Holds orientation data extracted from an EBSD file, grouped by phase. +// ----------------------------------------------------------------------- +struct PhaseData +{ + std::string phaseName; + unsigned int laueOpsIndex = ebsdlib::CrystalStructure::UnknownCrystalStructure; + ebsdlib::FloatArrayType::Pointer eulers; +}; + +// ----------------------------------------------------------------------- +// Determine whether a LaueOps class uses hexagonal-style pole figure +// annotations (6-fold or 3-fold symmetry) vs. cubic-style (everything else). +// Returns 2 for hexagonal-style, 1 for cubic-style. +// ----------------------------------------------------------------------- +int getPoleFigureAnnotationType(unsigned int laueOpsIndex) +{ + // Hexagonal High, Hexagonal Low, Trigonal High use hexagonal annotations + if(laueOpsIndex == ebsdlib::CrystalStructure::Hexagonal_High || laueOpsIndex == ebsdlib::CrystalStructure::Hexagonal_Low || laueOpsIndex == ebsdlib::CrystalStructure::Trigonal_High) + { + return 2; + } + return 1; +} + +// ----------------------------------------------------------------------- +// Generate and write pole figure images for a given set of Euler angles. +// ----------------------------------------------------------------------- +void generatePoleFiguresForPhase(const LaueOps& ops, unsigned int laueOpsIndex, ebsdlib::FloatArrayType* eulers, const std::string& outputDir, int imageDim, const std::string& phaseLabel) +{ + std::string className = ops.getSymmetryName(); + std::cout << "Generating pole figures for phase: " << phaseLabel << " (" << className << ", " << eulers->getNumberOfTuples() << " orientations)" << std::endl; + + auto poleFigureNames = ops.getDefaultPoleFigureNames(); + + PoleFigureConfiguration_t config; + config.eulers = eulers; + config.imageDim = imageDim; + config.lambertDim = 72; + config.numColors = 32; + config.minScale = 0.0; + config.maxScale = 0.0; // 0 = auto-scale + config.sphereRadius = 1.0F; + config.discrete = true; + config.discreteHeatMap = false; + config.labels = {poleFigureNames[0], poleFigureNames[1], poleFigureNames[2]}; + config.order = {0, 1, 2}; + config.phaseName = phaseLabel; + + std::vector poleFigures = ops.generatePoleFigure(config); + + // Sanitize phase name for use as a filename + std::string safeName = phaseLabel; + for(auto& c : safeName) + { + if(c == '/' || c == '\\' || c == ' ' || c == '(' || c == ')') + { + c = '_'; + } + } + + int symType = getPoleFigureAnnotationType(laueOpsIndex); + + for(size_t i = 0; i < poleFigures.size(); i++) + { + // Mirror the image across the X axis (algorithm uses +Y down, we want +Y up) + poleFigures[i] = ebsdlib::MirrorImage(poleFigures[i].get(), config.imageDim); + + // Overlay the standard projection annotations + if(symType == 1) + { + poleFigures[i] = ebsdlib::DrawStandardCubicProjection(poleFigures[i], config.imageDim, config.imageDim); + } + else if(symType == 2) + { + poleFigures[i] = ebsdlib::DrawStandardHexagonalProjection(poleFigures[i], config.imageDim, config.imageDim); + } + + // Clean up the label for use as a filename + std::string cleanedLabel = EbsdStringUtils::replace(config.labels[i], "<", "["); + cleanedLabel = EbsdStringUtils::replace(cleanedLabel, ">", "]"); + cleanedLabel = EbsdStringUtils::replace(cleanedLabel, "|", "_"); + + std::ostringstream filePath; + filePath << outputDir << "/" << safeName << "_PF_" << cleanedLabel << ".tiff"; + auto result = TiffWriter::WriteColorImage(filePath.str(), config.imageDim, config.imageDim, 3, poleFigures[i]->getTuplePointer(0)); + if(result.first < 0) + { + std::cerr << " ERROR writing " << filePath.str() << ": " << result.second << std::endl; + } + else + { + std::cout << " Wrote: " << filePath.str() << std::endl; + } + } +} + +// ----------------------------------------------------------------------- +// Read a .ang file and return per-phase orientation data. +// ----------------------------------------------------------------------- +std::vector readAngFile(const std::string& filePath) +{ + AngReader reader; + reader.setFileName(filePath); + int err = reader.readFile(); + if(err < 0) + { + std::cerr << "ERROR: Failed to read .ang file: " << filePath << std::endl; + return {}; + } + + size_t totalPoints = reader.getNumberOfElements(); + std::cout << " Read " << totalPoints << " data points (" << reader.getXDimension() << " x " << reader.getYDimension() << ")" << std::endl; + + float* phi1 = reader.getPhi1Pointer(false); + float* phi = reader.getPhiPointer(false); + float* phi2 = reader.getPhi2Pointer(false); + int* phaseData = reader.getPhaseDataPointer(false); + + std::vector phases = reader.getPhaseVector(); + + std::map phaseToLaueOps; + std::map phaseToName; + for(const auto& phase : phases) + { + int idx = phase->getPhaseIndex(); + phaseToLaueOps[idx] = phase->determineOrientationOpsIndex(); + std::string name = phase->getMaterialName(); + if(name.empty()) + { + name = "Phase_" + std::to_string(idx); + } + phaseToName[idx] = name; + std::cout << " Phase " << idx << ": " << name << " (LaueOps index: " << phaseToLaueOps[idx] << ")" << std::endl; + } + + // Group Euler angles by phase (ANG files: radians, phase 0 → phase 1) + std::map> phaseEulerMap; + for(size_t i = 0; i < totalPoints; i++) + { + int p = phaseData[i]; + if(p < 1 && phaseToLaueOps.find(1) != phaseToLaueOps.end()) + { + p = 1; + } + if(phaseToLaueOps.find(p) == phaseToLaueOps.end()) + { + continue; + } + if(phaseToLaueOps[p] >= ebsdlib::CrystalStructure::LaueGroupEnd) + { + continue; + } + phaseEulerMap[p].push_back(phi1[i]); + phaseEulerMap[p].push_back(phi[i]); + phaseEulerMap[p].push_back(phi2[i]); + } + + std::vector result; + for(auto& [phaseIdx, eulerVec] : phaseEulerMap) + { + size_t numOrientations = eulerVec.size() / 3; + if(numOrientations == 0) + { + continue; + } + + PhaseData pd; + pd.phaseName = phaseToName[phaseIdx]; + pd.laueOpsIndex = phaseToLaueOps[phaseIdx]; + + std::vector cDims = {3}; + pd.eulers = ebsdlib::FloatArrayType::CreateArray(numOrientations, cDims, "EulerAngles", true); + std::memcpy(pd.eulers->getVoidPointer(0), eulerVec.data(), eulerVec.size() * sizeof(float)); + + result.push_back(std::move(pd)); + } + + return result; +} + +// ----------------------------------------------------------------------- +// Read a .ctf file and return per-phase orientation data. +// CTF files store Euler angles in degrees; we convert to radians. +// ----------------------------------------------------------------------- +std::vector readCtfFile(const std::string& filePath) +{ + CtfReader reader; + reader.setFileName(filePath); + int err = reader.readFile(); + if(err < 0) + { + std::cerr << "ERROR: Failed to read .ctf file: " << filePath << std::endl; + return {}; + } + + size_t totalPoints = reader.getNumberOfElements(); + std::cout << " Read " << totalPoints << " data points (" << reader.getXDimension() << " x " << reader.getYDimension() << ")" << std::endl; + + float* euler1 = reader.getEuler1Pointer(); + float* euler2 = reader.getEuler2Pointer(); + float* euler3 = reader.getEuler3Pointer(); + int* phaseData = reader.getPhasePointer(); + + std::vector phases = reader.getPhaseVector(); + + std::map phaseToLaueOps; + std::map phaseToName; + for(const auto& phase : phases) + { + int idx = phase->getPhaseIndex(); + phaseToLaueOps[idx] = phase->determineOrientationOpsIndex(); + std::string name = phase->getPhaseName(); + if(name.empty()) + { + name = "Phase_" + std::to_string(idx); + } + phaseToName[idx] = name; + std::cout << " Phase " << idx << ": " << name << " (LaueOps index: " << phaseToLaueOps[idx] << ")" << std::endl; + } + + // Group Euler angles by phase, converting degrees to radians (CTF phase 0 → phase 1) + const float degToRad = static_cast(ebsdlib::constants::k_DegToRadD); + std::map> phaseEulerMap; + for(size_t i = 0; i < totalPoints; i++) + { + int p = phaseData[i]; + if(p < 1 && phaseToLaueOps.find(1) != phaseToLaueOps.end()) + { + p = 1; + } + if(phaseToLaueOps.find(p) == phaseToLaueOps.end()) + { + continue; + } + if(phaseToLaueOps[p] >= ebsdlib::CrystalStructure::LaueGroupEnd) + { + continue; + } + phaseEulerMap[p].push_back(euler1[i] * degToRad); + phaseEulerMap[p].push_back(euler2[i] * degToRad); + phaseEulerMap[p].push_back(euler3[i] * degToRad); + } + + std::vector result; + for(auto& [phaseIdx, eulerVec] : phaseEulerMap) + { + size_t numOrientations = eulerVec.size() / 3; + if(numOrientations == 0) + { + continue; + } + + PhaseData pd; + pd.phaseName = phaseToName[phaseIdx]; + pd.laueOpsIndex = phaseToLaueOps[phaseIdx]; + + std::vector cDims = {3}; + pd.eulers = ebsdlib::FloatArrayType::CreateArray(numOrientations, cDims, "EulerAngles", true); + std::memcpy(pd.eulers->getVoidPointer(0), eulerVec.data(), eulerVec.size() * sizeof(float)); + + result.push_back(std::move(pd)); + } + + return result; +} + +} // namespace + +// ============================================================================= +int main(int argc, char* argv[]) +{ + if(argc < 2) + { + std::cout << "Usage: generate_pole_figure [output_directory]" << std::endl; + std::cout << std::endl; + std::cout << "Reads an EBSD data file and generates Pole Figure images" << std::endl; + std::cout << "for each phase found in the data." << std::endl; + return 1; + } + + std::string inputFile = argv[1]; + std::filesystem::path inputPath(inputFile); + + if(!std::filesystem::exists(inputPath)) + { + std::cerr << "ERROR: Input file does not exist: " << inputFile << std::endl; + return 1; + } + + std::string outputDir; + if(argc >= 3) + { + outputDir = argv[2]; + } + else + { + outputDir = inputPath.parent_path().string(); + if(outputDir.empty()) + { + outputDir = "."; + } + } + + std::filesystem::create_directories(outputDir); + + std::string ext = inputPath.extension().string(); + std::transform(ext.begin(), ext.end(), ext.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); + + std::cout << "============================================================" << std::endl; + std::cout << " Pole Figure Generator from EBSD Data" << std::endl; + std::cout << "============================================================" << std::endl; + std::cout << " Input file: " << inputFile << std::endl; + std::cout << " File type: " << ext << std::endl; + std::cout << " Output directory: " << outputDir << std::endl; + std::cout << "============================================================" << std::endl; + std::cout << std::endl; + + // Read the file + std::vector phaseDataVec; + if(ext == ".ang") + { + std::cout << "Reading .ang file..." << std::endl; + phaseDataVec = readAngFile(inputFile); + } + else if(ext == ".ctf") + { + std::cout << "Reading .ctf file..." << std::endl; + phaseDataVec = readCtfFile(inputFile); + } + else + { + std::cerr << "ERROR: Unsupported file extension '" << ext << "'. Use .ang or .ctf" << std::endl; + return 1; + } + + if(phaseDataVec.empty()) + { + std::cerr << "ERROR: No valid phase data found in file." << std::endl; + return 1; + } + + std::cout << std::endl; + std::cout << "Found " << phaseDataVec.size() << " phase(s) with valid orientations." << std::endl; + std::cout << std::endl; + + int imageDim = 512; + + std::vector ops = LaueOps::GetAllOrientationOps(); + + for(const auto& pd : phaseDataVec) + { + if(pd.laueOpsIndex >= ops.size()) + { + std::cerr << " Skipping phase '" << pd.phaseName << "': invalid LaueOps index " << pd.laueOpsIndex << std::endl; + continue; + } + + generatePoleFiguresForPhase(*ops[pd.laueOpsIndex], pd.laueOpsIndex, pd.eulers.get(), outputDir, imageDim, pd.phaseName); + std::cout << std::endl; + } + + std::cout << "============================================================" << std::endl; + std::cout << " Done! All pole figure images written to:" << std::endl; + std::cout << " " << outputDir << std::endl; + std::cout << "============================================================" << std::endl; + + return 0; +} From 7db08ee6160dedd4920ea0835b2f731e4a51e4a9 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 25 Mar 2026 13:46:23 -0400 Subject: [PATCH 14/14] Inverse Pole Figures Implemented. Needs verified output for unit tests Signed-off-by: Michael Jackson --- Source/Apps/generate_ipf_density.cpp | 12 +++++------ Source/Apps/generate_pole_figure.cpp | 4 ++-- Source/EbsdLib/LaueOps/LaueOps.cpp | 20 ++++++++++--------- Source/EbsdLib/LaueOps/LaueOps.h | 3 +-- .../Utilities/InversePoleFigureUtilities.h | 2 +- Source/Test/PoleFigureCompositorTest.cpp | 10 +++++----- 6 files changed, 26 insertions(+), 25 deletions(-) diff --git a/Source/Apps/generate_ipf_density.cpp b/Source/Apps/generate_ipf_density.cpp index 327ebbb..c99399d 100644 --- a/Source/Apps/generate_ipf_density.cpp +++ b/Source/Apps/generate_ipf_density.cpp @@ -91,9 +91,9 @@ ebsdlib::FloatArrayType::Pointer generateRandomEulers(size_t numOrientations, un for(size_t i = 0; i < numOrientations; i++) { float* ptr = eulers->getTuplePointer(i); - ptr[0] = phi1Dist(gen); // phi1: [0, 2pi) - ptr[1] = std::acos(cosDist(gen)); // Phi: [0, pi] with uniform sphere coverage - ptr[2] = phi2Dist(gen); // phi2: [0, 2pi) + ptr[0] = phi1Dist(gen); // phi1: [0, 2pi) + ptr[1] = std::acos(cosDist(gen)); // Phi: [0, pi] with uniform sphere coverage + ptr[2] = phi2Dist(gen); // phi2: [0, 2pi) } return eulers; } @@ -135,7 +135,7 @@ ebsdlib::UInt8ArrayType::Pointer convertARGBtoRGB(ebsdlib::UInt8ArrayType* argbI uint32_t pixel = *reinterpret_cast(argb); rgb[0] = static_cast((pixel >> 16) & 0xFF); // R rgb[1] = static_cast((pixel >> 8) & 0xFF); // G - rgb[2] = static_cast(pixel & 0xFF); // B + rgb[2] = static_cast(pixel & 0xFF); // B } return rgbImage; } @@ -327,7 +327,7 @@ void generateSingleIPFForLaueClass(const LaueOps& ops, ebsdlib::FloatArrayType* int main(int argc, char* argv[]) { // Parse command-line arguments - std::string outputDir = UnitTest::TestTempDir + "/IPF_Density/"; + std::string outputDir = ebsdlib::unit_test::k_TestTempDir + "/IPF_Density/"; size_t numOrientations = 5000; if(argc >= 2) @@ -428,7 +428,7 @@ int main(int argc, char* argv[]) std::cout << "--- Part 5: Quaternion Texture File - All Laue Classes (ND) ---" << std::endl; std::cout << std::endl; - std::string quatFilePath = UnitTest::DataDir + "IPF_Legend/quats_000_1_deg.txt"; + std::string quatFilePath = ebsdlib::unit_test::DataDir + "IPF_Legend/quats_000_1_deg.txt"; auto textureEulers = readQuaternionFileAsEulers(quatFilePath); if(textureEulers != nullptr) { diff --git a/Source/Apps/generate_pole_figure.cpp b/Source/Apps/generate_pole_figure.cpp index f97bad0..2f6466d 100644 --- a/Source/Apps/generate_pole_figure.cpp +++ b/Source/Apps/generate_pole_figure.cpp @@ -108,12 +108,12 @@ void generatePoleFiguresForPhase(const LaueOps& ops, unsigned int laueOpsIndex, PoleFigureConfiguration_t config; config.eulers = eulers; config.imageDim = imageDim; - config.lambertDim = 72; + config.lambertDim = 64; config.numColors = 32; config.minScale = 0.0; config.maxScale = 0.0; // 0 = auto-scale config.sphereRadius = 1.0F; - config.discrete = true; + config.discrete = false; config.discreteHeatMap = false; config.labels = {poleFigureNames[0], poleFigureNames[1], poleFigureNames[2]}; config.order = {0, 1, 2}; diff --git a/Source/EbsdLib/LaueOps/LaueOps.cpp b/Source/EbsdLib/LaueOps/LaueOps.cpp index 3b7a652..ac42b95 100644 --- a/Source/EbsdLib/LaueOps/LaueOps.cpp +++ b/Source/EbsdLib/LaueOps/LaueOps.cpp @@ -883,9 +883,12 @@ std::vector LaueOps::generateInversePoleFigure(InverseP ebsdlib::FloatArrayType::Pointer dirs2 = InversePoleFigureUtilities::computeIPFDirections(*this, config.eulers, config.sampleDirections[2]); // Step 2: Compute intensity images for each (using stereographic SST mapping) - ebsdlib::DoubleArrayType::Pointer intensity0 = InversePoleFigureUtilities::computeIPFIntensity(*this, dirs0.get(), config.imageWidth, config.imageHeight, config.lambertDim, config.normalizeMRD, true); - ebsdlib::DoubleArrayType::Pointer intensity1 = InversePoleFigureUtilities::computeIPFIntensity(*this, dirs1.get(), config.imageWidth, config.imageHeight, config.lambertDim, config.normalizeMRD, true); - ebsdlib::DoubleArrayType::Pointer intensity2 = InversePoleFigureUtilities::computeIPFIntensity(*this, dirs2.get(), config.imageWidth, config.imageHeight, config.lambertDim, config.normalizeMRD, true); + ebsdlib::DoubleArrayType::Pointer intensity0 = + InversePoleFigureUtilities::computeIPFIntensity(*this, dirs0.get(), config.imageWidth, config.imageHeight, config.lambertDim, config.normalizeMRD, true); + ebsdlib::DoubleArrayType::Pointer intensity1 = + InversePoleFigureUtilities::computeIPFIntensity(*this, dirs1.get(), config.imageWidth, config.imageHeight, config.lambertDim, config.normalizeMRD, true); + ebsdlib::DoubleArrayType::Pointer intensity2 = + InversePoleFigureUtilities::computeIPFIntensity(*this, dirs2.get(), config.imageWidth, config.imageHeight, config.lambertDim, config.normalizeMRD, true); // Step 3: Find global min/max across all 3 intensity images (only for pixels inside SST, value >= 0) double globalMax = std::numeric_limits::lowest(); @@ -956,17 +959,16 @@ std::array LaueOps::adjustFigureOrigin(std::array figureOrig } // ----------------------------------------------------------------------------- -UInt8ArrayType::Pointer LaueOps::annotateIPFImage(UInt8ArrayType::Pointer triangleImage, int imageDim, int canvasDim, const std::string& title, bool generateEntirePlane, - bool hasColorBar) const +UInt8ArrayType::Pointer LaueOps::annotateIPFImage(UInt8ArrayType::Pointer triangleImage, int imageDim, int canvasDim, const std::string& title, bool generateEntirePlane, bool hasColorBar) const { const float fontPtSize = static_cast(canvasDim) / 24.0f; // When a color bar will be drawn, use a wider right margin to make room float rightMargin = hasColorBar ? static_cast(canvasDim / 3.5f) : static_cast(canvasDim / 7.0f); const std::vector margins = { - fontPtSize * 3, // Top - rightMargin, // Right - fontPtSize * 2, // Bottom - static_cast(canvasDim / 7.0f) // Left + fontPtSize * 3, // Top + rightMargin, // Right + fontPtSize * 2, // Bottom + static_cast(canvasDim / 7.0f) // Left }; int legendHeight = canvasDim - static_cast(margins[0]) - static_cast(margins[2]); diff --git a/Source/EbsdLib/LaueOps/LaueOps.h b/Source/EbsdLib/LaueOps/LaueOps.h index 7285ac9..2318cef 100644 --- a/Source/EbsdLib/LaueOps/LaueOps.h +++ b/Source/EbsdLib/LaueOps/LaueOps.h @@ -498,8 +498,7 @@ class EbsdLib_EXPORT LaueOps * @param generateEntirePlane true = full circle view, false = SST only * @return RGB image (canvasDim x canvasDim, 3 components) */ - UInt8ArrayType::Pointer annotateIPFImage(UInt8ArrayType::Pointer triangleImage, int imageDim, int canvasDim, const std::string& title, bool generateEntirePlane, - bool hasColorBar = false) const; + UInt8ArrayType::Pointer annotateIPFImage(UInt8ArrayType::Pointer triangleImage, int imageDim, int canvasDim, const std::string& title, bool generateEntirePlane, bool hasColorBar = false) const; /** * @brief Draws a color bar with min/max labels onto an existing RGB image. diff --git a/Source/EbsdLib/Utilities/InversePoleFigureUtilities.h b/Source/EbsdLib/Utilities/InversePoleFigureUtilities.h index a20bacf..3596955 100644 --- a/Source/EbsdLib/Utilities/InversePoleFigureUtilities.h +++ b/Source/EbsdLib/Utilities/InversePoleFigureUtilities.h @@ -107,7 +107,7 @@ class EbsdLib_EXPORT InversePoleFigureUtilities * @return DoubleArrayType intensity image (imageWidth * imageHeight). Pixels outside SST have value -1.0. */ static ebsdlib::DoubleArrayType::Pointer computeIPFIntensity(const LaueOps& ops, ebsdlib::FloatArrayType* ipfDirections, int imageWidth, int imageHeight, int lambertDim, bool normalizeMRD, - bool useStereographicSST = false); + bool useStereographicSST = false); /** * @brief Converts an intensity image to RGBA with SST masking. Pixels inside the SST diff --git a/Source/Test/PoleFigureCompositorTest.cpp b/Source/Test/PoleFigureCompositorTest.cpp index 2ff4153..af469bb 100644 --- a/Source/Test/PoleFigureCompositorTest.cpp +++ b/Source/Test/PoleFigureCompositorTest.cpp @@ -28,7 +28,6 @@ * * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ - /** Test result: 39 mismatched pixels in Debug mode (confirmed Release passes). @@ -196,7 +195,7 @@ void GeneratePoleFigures(const std::string& phaseName, size_t opsIndex, hid_t ex UInt8ArrayType::Pointer image = result.image; std::string datasetName = fmt::format("{}", sampleId); #if WRITE_EXEMPLAR_IMAGES - std::string outputPath = fmt::format("{}/Pole_Figure_Images/Pole_Figure_{}_{}_{}.tif", ebsdlib::unit_test::k_TestFilesDir, layoutStr,op->getRotationPointGroup() , sampleId); + std::string outputPath = fmt::format("{}/Pole_Figure_Images/Pole_Figure_{}_{}_{}.tif", ebsdlib::unit_test::k_TestFilesDir, layoutStr, op->getRotationPointGroup(), sampleId); auto writerResult = TiffWriter::WriteColorImage(outputPath, result.width, result.height, 4, result.image->data()); REQUIRE(writerResult.first == 0); // @@ -230,9 +229,10 @@ TEST_CASE("ebsdlib::PoleFigureCompositorTest::All_Laue_Classes", "[EbsdLib][Pole const ebsdlib::unit_test::TestFileSentinel testDataSentinel(ebsdlib::unit_test::k_TestFilesDir, "Laue_Orientation_Clusters_v6.tar.gz", "Laue_Orientation_Clusters_v6", true, true); const ebsdlib::unit_test::TestFileSentinel testDataSentinel1(ebsdlib::unit_test::k_TestFilesDir, "Pole_Figure_Images.tar.gz", "Pole_Figure_Images" #if WRITE_EXEMPLAR_IMAGES - , false, false + , + false, false #endif - ); + ); const std::string hdfInputFile = fmt::format("{}/Pole_Figure_Images/Exemplar_Data.h5", ebsdlib::unit_test::k_TestFilesDir); hid_t fileId = -1; @@ -249,7 +249,7 @@ TEST_CASE("ebsdlib::PoleFigureCompositorTest::All_Laue_Classes", "[EbsdLib][Pole fileId = H5Support::H5Utilities::openFile(hdfInputFile, true); } #endif - REQUIRE(fileId > 0); + REQUIRE(fileId > 0); H5Support::H5ScopedFileSentinel fileSentinel(fileId, false); std::vector ops = LaueOps::GetAllOrientationOps();