Skip to content

Commit

Permalink
ENH: Map Point Cloud to Regular Grid Filter Modernization (#1109)
Browse files Browse the repository at this point in the history
- Map Point Cloud to Regular Grid filter modernization
- Added out-of-bounds handling
- Use both bool and uint8 for mask arrays
- Documentation and error result fixes
  • Loading branch information
nyoungbq authored Oct 22, 2024
1 parent 21b8e79 commit 5cf455d
Show file tree
Hide file tree
Showing 4 changed files with 216 additions and 49 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ const ChoicesParameter::ValueType k_NoImageTransform = 0;
const ChoicesParameter::ValueType k_FlipAboutXAxis = 1;
const ChoicesParameter::ValueType k_FlipAboutYAxis = 2;

inline constexpr nx::core::StringLiteral k_NoResamplingMode = "Do Not Resample (0)";
inline constexpr nx::core::StringLiteral k_ScalingMode = "Scaling (1)";
inline constexpr nx::core::StringLiteral k_ExactDimensions = "Exact X/Y Dimensions (2)";
constexpr nx::core::StringLiteral k_NoResamplingMode = "Do Not Resample (0)";
constexpr nx::core::StringLiteral k_ScalingMode = "Scaling (1)";
constexpr nx::core::StringLiteral k_ExactDimensions = "Exact X/Y Dimensions (2)";
const nx::core::ChoicesParameter::Choices k_ResamplingChoices = {k_NoResamplingMode, k_ScalingMode, k_ExactDimensions};
const nx::core::ChoicesParameter::ValueType k_NoResampleModeIndex = 0;
const nx::core::ChoicesParameter::ValueType k_ScalingModeIndex = 1;
Expand Down
21 changes: 21 additions & 0 deletions src/Plugins/SimplnxCore/docs/MapPointCloudToRegularGridFilter.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,27 @@ This **Filter** determines, for a user-defined grid, in which voxel each point i

Additionally, the user may opt to use a mask; points for which the mask are false are ignored when computing voxel indices (instead, they are initialized to voxel 0).

One of the features provided to the user is control over the value used when a point is Out-of-Bounds. The three options are:

- `Silent`: (default) Will silently use the user supplied value
- `Warning with Count`: Will emit a filer warning that contains the number of out-of-bounds values were encountered.
- `Error at First Instance`: Will emit a filter error at the out-of-bounds value that is encountered.

The default selection is `Silent`, but it is mostly provided as a way to preserve existing functionality. What follows are a few use cases we had in mind when adding this functionality, organized by handling type:

- `Silent` option:
- User may want to preserve identical functionality between **SIMPL** and **simplnx**
- User may expect values to fall outside the target image geometry or intend to crop all that fall outside it anyway
- `Warning with Count` option:
- User may be intending to create a general use pipeline for various different tasks, for which monitoring and validation may be important
- User may intend to create a workflow that will be distributed in which the end user may not have control over the parameter, but should be monitoring for anomalies in output
- User may want to watch for unexpected behavior
- `Error at First Instance` option
- User may to trace down where a anomaly first occured
- User may be creating a pipeline in a known problem space with a well defined outcome where any data anomalies must be caught early to prevent downstream problems

Continuing along the Out-of-Bounds discussion, the Out-of-Bounds value allows the user to specify a specific `uint64` (0 - 18,446,744,073,709,551,616) value to use for every value from the vertex geometry that falls outside the image geometry. The default value is just the max `unsigned long long int` in an effort to make sure that it doesn't intersect with exisiting indexed values. This is identical to previous functionality. However, consider the situation where a user has a geometry that contains 1000 voxels, in this case the actual index values are 0-999, so a user could select 1000 and it wouldn't overlap any existing voxel index. Doing this may reduce skew of coloring or other statistic-based analysis. Advanced users may intentionally select a value that overlaps an existing voxel index they wish to remove in a later filter or to later downcast the datasize without overflow, but this is considered an edge case that is functional, but not recommended.

% Auto generated parameter table will be inserted here

## License & Copyright
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,45 @@
#include "simplnx/DataStructure/Geometry/VertexGeom.hpp"
#include "simplnx/Filter/Actions/CreateArrayAction.hpp"
#include "simplnx/Filter/Actions/CreateImageGeometryAction.hpp"
#include "simplnx/Filter/Actions/DeleteDataAction.hpp"
#include "simplnx/Parameters/ArraySelectionParameter.hpp"
#include "simplnx/Parameters/BoolParameter.hpp"
#include "simplnx/Parameters/ChoicesParameter.hpp"
#include "simplnx/Parameters/DataGroupCreationParameter.hpp"
#include "simplnx/Parameters/DataGroupSelectionParameter.hpp"
#include "simplnx/Parameters/DataObjectNameParameter.hpp"
#include "simplnx/Parameters/GeometrySelectionParameter.hpp"
#include "simplnx/Parameters/NumberParameter.hpp"
#include "simplnx/Parameters/VectorParameter.hpp"
#include "simplnx/Utilities/DataArrayUtilities.hpp"
#include "simplnx/Utilities/SIMPLConversion.hpp"

#include <chrono>
#include <cmath>

namespace nx::core
{
namespace
{
const std::string k_MaskName = "temp_mask";

constexpr nx::core::StringLiteral k_SilentMode = "Silent";
constexpr nx::core::StringLiteral k_WarningMode = "Warning with Count";
constexpr nx::core::StringLiteral k_ErrorMode = "Error at First Instance";
const nx::core::ChoicesParameter::Choices k_OutOfBoundsHandlingChoices = {k_SilentMode, k_WarningMode, k_ErrorMode};
const nx::core::ChoicesParameter::ValueType k_SilentModeIndex = 0;
const nx::core::ChoicesParameter::ValueType k_WarningModeIndex = 1;
const nx::core::ChoicesParameter::ValueType k_ErrorModeIndex = 2;

constexpr int64 k_BadGridDimensions = -2601;
constexpr int64 k_InvalidVertexGeometry = -2602;
constexpr int64 k_IncompatibleMaskVoxelArrays = -2603;
constexpr int64 k_MaskSelectedArrayInvalid = -2604;
constexpr int64 k_MaskCompareInvalid = -2605;
constexpr int64 k_InvalidImageGeometry = -2606;
constexpr int64 k_ErrorOutOfBounds = -2607;
constexpr int64 k_WarningOutOfBounds = -2608;
constexpr int64 k_InvalidHandlingValue = -2609;

Result<> CreateRegularGrid(DataStructure& dataStructure, const Arguments& args)
{
Expand Down Expand Up @@ -188,6 +207,91 @@ Result<> CreateRegularGrid(DataStructure& dataStructure, const Arguments& args)

return {};
}

template <bool UseSilent, bool UseWarning, bool UseError>
struct OutOfBoundsType
{
// Compile time checks for bounding, no runtime overhead
static_assert((UseSilent && !UseWarning && !UseError) || (!UseSilent && UseWarning && !UseError) || (!UseSilent && !UseWarning && UseError),
"struct `OutOfBoundsType` can only have one true bool in its instantiation");

static constexpr bool UsingSilent = UseSilent;
static constexpr bool UsingWarning = UseWarning;
static constexpr bool UsingError = UseError;
};

using SilentType = OutOfBoundsType<true, false, false>;
using WarningType = OutOfBoundsType<false, true, false>;
using ErrorType = OutOfBoundsType<false, false, true>;

template <class OutOfBoundsType = SilentType, bool UseMask = false>
Result<> ProcessVertices(const IFilter::MessageHandler& messageHandler, const VertexGeom& vertices, const ImageGeom* image, UInt64AbstractDataStore& voxelIndices,
const std::unique_ptr<MaskCompare>& maskCompare, uint64 outOfBoundsValue)
{
// Validation
if(image == nullptr)
{
return MakeErrorResult(k_InvalidImageGeometry, fmt::format("{}({}): Function {}: Error. Supplied `image` is a nullptr", "::ProcessVertices", __FILE__, __LINE__));
}

// Out of Bounds Counter
usize count = 0;

// Execution
usize numVerts = vertices.getNumberOfVertices();
auto start = std::chrono::steady_clock::now();
for(int64 i = 0; i < numVerts; i++)
{
if constexpr(UseMask)
{
if(!maskCompare->isTrue(i))
{
continue;
}
}

auto coords = vertices.getVertexCoordinate(i);
const auto indexResult = image->getIndex(coords[0], coords[1], coords[2]);
if(indexResult.has_value())
{
voxelIndices[i] = indexResult.value();
}
else
{
if constexpr(OutOfBoundsType::UsingError)
{
BoundingBox3Df imageBounds = image->getBoundingBoxf();
const Point3Df& minPoint = imageBounds.getMinPoint();
const Point3Df& maxPoint = imageBounds.getMaxPoint();
return MakeErrorResult(
k_ErrorOutOfBounds,
fmt::format("Out of bounds value encountered.\nVertex Index: {}\nVertex Coordinates [X,Y,Z]: [{},{},{}]\nImage Coordinate Bounds:\nX: {} to {}\nY: {} to {}\nZ: {} to {}", i, coords[0],
coords[1], coords[2], minPoint.getX(), maxPoint.getX(), minPoint.getY(), maxPoint.getY(), minPoint.getZ(), maxPoint.getZ()));
}

// Out of bounds value
voxelIndices[i] = outOfBoundsValue;
count++;
}

auto now = std::chrono::steady_clock::now();
if(std::chrono::duration_cast<std::chrono::milliseconds>(now - start).count() > 1000)
{
messageHandler(fmt::format("Computing Point Cloud Voxel Indices || {}% Completed", static_cast<int64>((static_cast<float32>(i) / numVerts) * 100.0f)));
start = now;
}
}

if constexpr(OutOfBoundsType::UsingWarning)
{
if(count > 0)
{
return MakeWarningVoidResult(k_WarningOutOfBounds, fmt::format("Mapping Complete. Number of value outside image bounds: {}", count));
}
}

return {};
}
} // namespace

//------------------------------------------------------------------------------
Expand Down Expand Up @@ -232,15 +336,20 @@ Parameters MapPointCloudToRegularGridFilter::parameters() const
params.insert(std::make_unique<DataGroupCreationParameter>(k_CreatedImageGeometryPath_Key, "Created Image Geometry", "Path to create the Image Geometry", DataPath()));
params.insert(std::make_unique<GeometrySelectionParameter>(k_SelectedImageGeometryPath_Key, "Existing Image Geometry", "Path to the existing Image Geometry", DataPath{},
GeometrySelectionParameter::AllowedTypes{IGeometry::Type::Image}));
params.insert(std::make_unique<ChoicesParameter>(k_OutOfBoundsHandlingType_Key, "Out of Bounds Handling",
"Specifies how data outside the image bounds is handled, see documentation for specification", k_SilentModeIndex, k_OutOfBoundsHandlingChoices));
params.insert(std::make_unique<UInt64Parameter>(k_OutOfBoundsValue_Key, "Out of Bounds Value",
"The value to be put in voxel indices slots, occurs when the vertex geometry's coordinate point falls outside the image geometry's bounds",
std::numeric_limits<UInt64Parameter::ValueType>::max()));

params.insertSeparator(Parameters::Separator{"Input Vertex Geometry"});
params.insert(std::make_unique<GeometrySelectionParameter>(k_SelectedVertexGeometryPath_Key, "Vertex Geometry", "Path to the target Vertex Geometry", DataPath{},
GeometrySelectionParameter::AllowedTypes{IGeometry::Type::Vertex}));

params.insertSeparator(Parameters::Separator{"Optional Data Mask"});
params.insertLinkableParameter(std::make_unique<BoolParameter>(k_UseMask_Key, "Use Mask Array", "Specifies whether or not to use a mask array", false));
params.insert(std::make_unique<ArraySelectionParameter>(k_InputMaskPath_Key, "Mask", "DataPath to the boolean mask array. Values that are true will mark that cell/point as usable.", DataPath(),
ArraySelectionParameter::AllowedTypes{DataType::boolean}, ArraySelectionParameter::AllowedComponentShapes{{1}}));
params.insert(std::make_unique<ArraySelectionParameter>(k_InputMaskPath_Key, "Mask", "DataPath to the boolean/uint8 mask array. Values that are true will mark that cell/point as usable.",
DataPath(), ArraySelectionParameter::AllowedTypes{DataType::boolean, DataType::uint8}, ArraySelectionParameter::AllowedComponentShapes{{1}}));

params.insertSeparator(Parameters::Separator{"Output Data Object(s)"});
params.insert(std::make_unique<DataObjectNameParameter>(k_VoxelIndicesName_Key, "Created Voxel Indices", "Path to the created Voxel Indices array", "Voxel Indices"));
Expand All @@ -253,13 +362,21 @@ Parameters MapPointCloudToRegularGridFilter::parameters() const
params.linkParameters(k_SamplingGridType_Key, k_CellDataName_Key, std::make_any<ChoicesParameter::ValueType>(0));

params.linkParameters(k_SamplingGridType_Key, k_SelectedImageGeometryPath_Key, std::make_any<ChoicesParameter::ValueType>(1));

return params;
}

//------------------------------------------------------------------------------
IFilter::VersionType MapPointCloudToRegularGridFilter::parametersVersion() const
{
return 1;
return 2;

// Version 1 -> 2
// Change 1:
// Added 2 new parameters, but defaults support original functionality
//
// Change 2:
// Extended the accepted typing for mask array, no change needed
}

//------------------------------------------------------------------------------
Expand Down Expand Up @@ -311,7 +428,7 @@ IFilter::PreflightResult MapPointCloudToRegularGridFilter::preflightImpl(const D
if(useMask)
{
auto maskArrayPath = args.value<DataPath>(k_InputMaskPath_Key);
const auto numMaskTuples = dataStructure.getDataRefAs<BoolArray>(maskArrayPath).getNumberOfTuples();
const auto numMaskTuples = dataStructure.getDataRefAs<IDataArray>(maskArrayPath).getNumberOfTuples();
const auto numVoxelTuples = vertexData->getNumTuples();
if(numMaskTuples != numVoxelTuples)
{
Expand All @@ -320,6 +437,16 @@ IFilter::PreflightResult MapPointCloudToRegularGridFilter::preflightImpl(const D
vertexDataPath.toString(), maskArrayPath.toString()));
}
}
else
{
DataPath tempPath = DataPath({k_MaskName});
{
auto createAction = std::make_unique<CreateArrayAction>(DataType::boolean, vertexData->getShape(), std::vector<usize>{1}, tempPath);
actions.appendAction(std::move(createAction));
}

actions.appendDeferredAction(std::make_unique<DeleteDataAction>(tempPath));
}

auto createArrayAction = std::make_unique<CreateArrayAction>(DataType::uint64, vertexData->getShape(), std::vector<usize>{1}, voxelIndicesPath);
actions.appendAction(std::move(createArrayAction));
Expand All @@ -331,12 +458,8 @@ IFilter::PreflightResult MapPointCloudToRegularGridFilter::preflightImpl(const D
Result<> MapPointCloudToRegularGridFilter::executeImpl(DataStructure& dataStructure, const Arguments& args, const PipelineFilter* pipelineNode, const MessageHandler& messageHandler,
const std::atomic_bool& shouldCancel) const
{
// Get the target image as a pointer
const auto samplingGridType = args.value<uint64>(k_SamplingGridType_Key);
const auto vertexGeomPath = args.value<DataPath>(k_SelectedVertexGeometryPath_Key);
const auto useMask = args.value<bool>(k_UseMask_Key);
const auto maskArrayPath = args.value<DataPath>(k_InputMaskPath_Key);
const auto voxelIndicesName = args.value<std::string>(k_VoxelIndicesName_Key);

const ImageGeom* image = nullptr;
if(samplingGridType == 0)
{
Expand All @@ -354,51 +477,72 @@ Result<> MapPointCloudToRegularGridFilter::executeImpl(DataStructure& dataStruct
image = dataStructure.getDataAs<ImageGeom>(args.value<DataPath>(k_SelectedImageGeometryPath_Key));
}

const auto& vertices = dataStructure.getDataRefAs<VertexGeom>(vertexGeomPath);
const DataPath voxelIndicesPath = vertexGeomPath.createChildPath(vertices.getVertexAttributeMatrix()->getName()).createChildPath(voxelIndicesName);
auto& voxelIndices = dataStructure.getDataAs<UInt64Array>(voxelIndicesPath)->getDataStoreRef();
const auto* mask = useMask ? dataStructure.getDataAs<BoolArray>(maskArrayPath)->getDataStore() : nullptr;
if(useMask && mask == nullptr)
// Create the Mask
const auto useMask = args.value<bool>(k_UseMask_Key);
auto maskPath = args.value<DataPath>(k_InputMaskPath_Key);
if(!args.value<bool>(k_UseMask_Key))
{
return MakeErrorResult(k_MaskSelectedArrayInvalid, "Use Mask was selected but mask array doesn't exist.");
maskPath = DataPath({k_MaskName});
dataStructure.getDataRefAs<BoolArray>(maskPath).fill(true);
}
std::unique_ptr<MaskCompare> maskCompare;
try
{
maskCompare = InstantiateMaskCompare(dataStructure, maskPath);
} catch(const std::out_of_range& exception)
{
// This really should NOT be happening as the path was verified during preflight BUT we may be calling this from
// somewhere else that is NOT going through the normal nx::core::IFilter API of Preflight and Execute
std::string message = fmt::format("Mask Array DataPath does not exist or is not of the correct type (Bool | UInt8) {}", maskPath.toString());
return MakeErrorResult(k_MaskCompareInvalid, message);
}

usize numVerts = vertices.getNumberOfVertices();
SizeVec3 dims = image->getDimensions();
FloatVec3 res = image->getSpacing();
FloatVec3 origin = image->getOrigin();
int64 progIncrement = numVerts / 100;
int64 prog = 1;
int64 progressInt = 0;
int64 counter = 0;
// Cache all the needed objects for ::ProcessVertices
const auto vertexGeomPath = args.value<DataPath>(k_SelectedVertexGeometryPath_Key);
const auto& vertices = dataStructure.getDataRefAs<VertexGeom>(vertexGeomPath);
const DataPath voxelIndicesPath = vertexGeomPath.createChildPath(vertices.getVertexAttributeMatrix()->getName()).createChildPath(args.value<std::string>(k_VoxelIndicesName_Key));
auto& voxelIndices = dataStructure.getDataAs<UInt64Array>(voxelIndicesPath)->getDataStoreRef();
auto outOfBoundsValue = args.value<uint64>(k_OutOfBoundsValue_Key);

for(int64 i = 0; i < numVerts; i++)
// Execute the correct ::ProcessVertices, else error out
switch(args.value<ChoicesParameter::ValueType>(k_OutOfBoundsHandlingType_Key))
{
if(!useMask || mask->getValue(i))
case k_SilentModeIndex: {
if(useMask)
{
auto coords = vertices.getVertexCoordinate(i);
const auto indexResult = image->getIndex(coords[0], coords[1], coords[2]);
if(indexResult.has_value())
{
voxelIndices[i] = indexResult.value();
}
else
{
voxelIndices[i] = std::numeric_limits<usize>::max();
}

if(counter > prog)
{
progressInt = static_cast<int64>((static_cast<float>(counter) / numVerts) * 100.0f);
std::string ss = fmt::format("Computing Point Cloud Voxel Indices || {}% Completed", progressInt);
messageHandler(ss);
prog = prog + progIncrement;
}
counter++;
return ProcessVertices<SilentType, true>(messageHandler, vertices, image, voxelIndices, maskCompare, outOfBoundsValue);
}
else
{
return ProcessVertices<SilentType, false>(messageHandler, vertices, image, voxelIndices, maskCompare, outOfBoundsValue);
}
}

return {};
case k_WarningModeIndex: {
if(useMask)
{
return ProcessVertices<WarningType, true>(messageHandler, vertices, image, voxelIndices, maskCompare, outOfBoundsValue);
}
else
{
return ProcessVertices<WarningType, false>(messageHandler, vertices, image, voxelIndices, maskCompare, outOfBoundsValue);
}
}
case k_ErrorModeIndex: {
if(useMask)
{
return ProcessVertices<ErrorType, true>(messageHandler, vertices, image, voxelIndices, maskCompare, outOfBoundsValue);
}
else
{
return ProcessVertices<ErrorType, false>(messageHandler, vertices, image, voxelIndices, maskCompare, outOfBoundsValue);
}
}
default: {
return MakeErrorResult(k_InvalidHandlingValue, fmt::format("Unexpected Out of Bounds Handing Option. Received : {}. Expected: {} ({}), {} ({}), {} ({})",
args.value<ChoicesParameter::ValueType>(k_OutOfBoundsHandlingType_Key), k_SilentMode, k_SilentModeIndex, k_WarningMode,
k_WarningModeIndex, k_ErrorMode, k_ErrorModeIndex));
}
}
}

namespace
Expand Down
Loading

0 comments on commit 5cf455d

Please sign in to comment.