Skip to content

Commit e9f0542

Browse files
authored
ENH: Add checks to ensure vertex cropping bounds are not the same. (#888)
Signed-off-by: Michael Jackson <[email protected]>
1 parent eed0c9b commit e9f0542

File tree

3 files changed

+84
-26
lines changed

3 files changed

+84
-26
lines changed

src/Plugins/SimplnxCore/docs/IterativeClosestPointFilter.md

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,43 @@
1-
# IterativeClosestPoint
1+
# Iterative Closest Point
22

33
## Group (Subgroup)
44

55
Reconstruction (Alignment)
66

77
## Description
88

9-
This **Filter** estimates the rigid body transformation (i.e., rotation and translation) between two sets of points represted by **Vertex Geometries** using the *iterative closest point* (ICP) algorithm. The two **Vertex Geometries** are not required to have the same number of points. The **Filter** first initializes temporary storage for each set of points and a global transformation. Then, the alignment algorithm iterates through the following steps:
9+
This **Filter** estimates the rigid body transformation (i.e., rotation and translation) between two sets of points represented
10+
by **Vertex Geometries** using the *iterative closest point* (ICP) algorithm. The two **Vertex Geometries** are not required
11+
to have the same number of points.
1012

11-
1. The closest point in the target geometry is determined for each point in the moving geoemetry; these are called the correspondence points. "Closeness" is measured based on Euclidean distance.
12-
2. The rigid body transformation between the moving and target correspondences is solved for analytically using least squares.
13-
3. The above transformation is applied to the moving points.
14-
4. The global transformation is updated with the transformation computed for the current iteration.
13+
The Iterative Closest Point (ICP) algorithm is a method used to minimize the difference between two clouds of points,
14+
which we can describe in terms of "moving geometry" and "target geometry." The basic flow of the algorithm is:
1515

16-
Iterations proceed for a fixed number of user-defined steps. The final rigid body transformation is stored as a 4x4 transformation matrix in row-major order. The user has the option to apply this transformation to the moving **Vertex Geometry**. Note that this transformation is applied to the moving geometry *in place* if the option is selected.
16+
1. Initial State: We start with two sets of points or geometries. The "moving geometry" is the one we aim to align with
17+
the "target geometry." Initially, the moving geometry may be in any orientation or position relative to the target geometry.
1718

18-
ICP has a number of advantages, such as robustness to noise and no requirement that the two sets of points to be the same size. However, peformance may suffer if the two sets of points are of siginficantly different size.
19+
1. Identify Correspondences: For each point in the moving geometry, we find the closest point in the target geometry. These
20+
pairs of points are considered correspondences, based on the assumption that they represent the same point in space
21+
after the moving geometry is properly aligned.
22+
23+
1. Estimate Transformation: With the correspondences identified, the algorithm calculates the optimal rigid body
24+
transformation (which includes translation and rotation) that best aligns the moving geometry to the target
25+
geometry. This step often involves minimizing a metric, such as the sum of squared distances between corresponding
26+
points, to find the best fit.
27+
28+
1. Apply Transformation: The calculated transformation is applied to the moving geometry, aligning it closer to the
29+
target geometry.
30+
31+
1. Iterate: Steps 2 through 4 are repeated iteratively. With each iteration, the moving geometry is brought into closer
32+
alignment with the target geometry. The iterations continue until a stopping criterion is met, which could be a predefined
33+
number of iterations, a minimum improvement threshold between iterations, or when the change in the error metric falls
34+
below a certain threshold.
35+
36+
Final Alignment: Once the iterations stop, the algorithm concludes with the moving geometry optimally aligned to the target
37+
geometry, based on the criteria set for minimizing the differences between them.
38+
39+
ICP has a number of advantages, such as robustness to noise and no requirement that the two sets of points to be the same
40+
size. However, performance may suffer if the two sets of points are of significantly different size.
1941

2042
% Auto generated parameter table will be inserted here
2143

src/Plugins/SimplnxCore/src/SimplnxCore/Filters/CropVertexGeometry.cpp

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,8 @@ Parameters CropVertexGeometry::parameters() const
8383
GeometrySelectionParameter::AllowedTypes{IGeometry::Type::Vertex}));
8484
params.insert(std::make_unique<DataGroupCreationParameter>(k_CroppedGeom_Key, "Cropped Vertex Geometry", "Created VertexGeom path", DataPath{}));
8585
params.insert(std::make_unique<DataObjectNameParameter>(k_VertexDataName_Key, "Vertex Data Name", "Name of the vertex data AttributeMatrix", INodeGeometry0D::k_VertexDataName));
86-
params.insert(std::make_unique<VectorFloat32Parameter>(k_MinPos_Key, "Min Pos", "Minimum vertex position", std::vector<float32>{0, 0, 0}, std::vector<std::string>{"X", "Y", "Z"}));
87-
params.insert(std::make_unique<VectorFloat32Parameter>(k_MaxPos_Key, "Max Pos", "Maximum vertex position", std::vector<float32>{0, 0, 0}, std::vector<std::string>{"X", "Y", "Z"}));
86+
params.insert(std::make_unique<VectorFloat32Parameter>(k_MinPos_Key, "Min Pos", "Minimum vertex position", std::vector<float32>{0.0f, 0.0f, 0.0f}, std::vector<std::string>{"X", "Y", "Z"}));
87+
params.insert(std::make_unique<VectorFloat32Parameter>(k_MaxPos_Key, "Max Pos", "Maximum vertex position", std::vector<float32>{0.0f, 0.0f, 0.0f}, std::vector<std::string>{"X", "Y", "Z"}));
8888
params.insert(std::make_unique<MultiArraySelectionParameter>(k_TargetArrayPaths_Key, "Vertex Data Arrays to crop", "The complete path to all the vertex data arrays to crop", std::vector<DataPath>(),
8989
MultiArraySelectionParameter::AllowedTypes{IArray::ArrayType::DataArray}, nx::core::GetAllDataTypes()));
9090
return params;
@@ -117,6 +117,32 @@ IFilter::PreflightResult CropVertexGeometry::preflightImpl(const DataStructure&
117117

118118
OutputActions actions;
119119

120+
{
121+
std::vector<Error> errors;
122+
bool xDimError = (xMax == xMin);
123+
bool yDimError = (yMax == yMin);
124+
bool zDimError = (zMax == zMin);
125+
if(xDimError)
126+
{
127+
std::string ss = fmt::format("X Max ({}) is equal to X Min ({}). There would be no points remaining in the geometry.", xMax, xMin);
128+
errors.push_back(Error{-58550, std::move(ss)});
129+
}
130+
if(yDimError)
131+
{
132+
std::string ss = fmt::format("Y Max ({}) is equal to Y Min ({}). There would be no points remaining in the geometry.", yMax, yMin);
133+
errors.push_back(Error{-58551, std::move(ss)});
134+
}
135+
if(zDimError)
136+
{
137+
std::string ss = fmt::format("Z Max ({}) is equal to Z Min ({}). There would be no points remaining in the geometry.", zMax, zMin);
138+
errors.push_back(Error{-58552, std::move(ss)});
139+
}
140+
if(xDimError || yDimError || zDimError)
141+
{
142+
return PreflightResult{{nonstd::make_unexpected(errors)}};
143+
}
144+
}
145+
120146
{
121147
std::vector<Error> errors;
122148
bool xDimError = (xMax < xMin);
@@ -125,17 +151,17 @@ IFilter::PreflightResult CropVertexGeometry::preflightImpl(const DataStructure&
125151
if(xDimError)
126152
{
127153
std::string ss = fmt::format("X Max ({}) less than X Min ({})", xMax, xMin);
128-
errors.push_back(Error{-5550, std::move(ss)});
154+
errors.push_back(Error{-58553, std::move(ss)});
129155
}
130156
if(yDimError)
131157
{
132158
std::string ss = fmt::format("Y Max ({}) less than Y Min ({})", yMax, yMin);
133-
errors.push_back(Error{-5550, std::move(ss)});
159+
errors.push_back(Error{-58554, std::move(ss)});
134160
}
135161
if(zDimError)
136162
{
137163
std::string ss = fmt::format("Z Max ({}) less than Z Min ({})", zMax, zMin);
138-
errors.push_back(Error{-5550, std::move(ss)});
164+
errors.push_back(Error{-58555, std::move(ss)});
139165
}
140166
if(xDimError || yDimError || zDimError)
141167
{
@@ -146,7 +172,7 @@ IFilter::PreflightResult CropVertexGeometry::preflightImpl(const DataStructure&
146172
auto* vertexAM = vertexGeom.getVertexAttributeMatrix();
147173
if(vertexAM == nullptr)
148174
{
149-
return {MakeErrorResult<OutputActions>(-5551, "Could not find vertex data AttributeMatrix in selected Vertex Geometry"), {}};
175+
return {MakeErrorResult<OutputActions>(-58556, "Could not find vertex data AttributeMatrix in selected Vertex Geometry"), {}};
150176
}
151177
auto tupleShape = vertexAM->getShape();
152178
usize numTuples = std::accumulate(tupleShape.cbegin(), tupleShape.cend(), static_cast<usize>(1), std::multiplies<>());
@@ -187,7 +213,7 @@ Result<> CropVertexGeometry::executeImpl(DataStructure& dataStructure, const Arg
187213
auto zMax = posMax[2];
188214

189215
auto& vertices = dataStructure.getDataRefAs<VertexGeom>(vertexGeomPath);
190-
int64 numVerts = vertices.getNumberOfVertices();
216+
int64 numVerts = static_cast<int64>(vertices.getNumberOfVertices());
191217
auto* verticesPtr = vertices.getVertices();
192218
auto& allVerts = verticesPtr->getDataStoreRef();
193219
std::vector<int64> croppedPoints;

src/Plugins/SimplnxCore/src/SimplnxCore/Filters/IterativeClosestPointFilter.cpp

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ constexpr int32 k_MissingMovingVertex = -4500;
2222
constexpr int32 k_MissingTargetVertex = -4501;
2323
constexpr int32 k_BadNumIterations = -4502;
2424
constexpr int32 k_MissingVertices = -4503;
25+
constexpr int32 k_EmptyVertices = -4505;
2526

2627
template <typename Derived>
2728
struct VertexGeomAdaptor
@@ -31,10 +32,10 @@ struct VertexGeomAdaptor
3132
size_t m_NumComponents = 0;
3233
size_t m_NumTuples = 0;
3334

34-
VertexGeomAdaptor(const Derived& obj_)
35+
explicit VertexGeomAdaptor(const Derived& obj_)
3536
: obj(obj_)
3637
{
37-
// These values never change for the life time of this object so cache them now.
38+
// These values never change for the lifetime of this object so cache them now.
3839
verts = derived()->getVertices();
3940
m_NumComponents = verts->getNumberOfComponents();
4041
m_NumTuples = verts->getNumberOfTuples();
@@ -187,10 +188,19 @@ Result<> IterativeClosestPointFilter::executeImpl(DataStructure& data, const Arg
187188
return {nonstd::make_unexpected(std::vector<Error>{Error{k_MissingVertices, ss}})};
188189
}
189190

190-
auto* movingPtr = movingVertexGeom->getVertices();
191-
Float32Array& targetPtr = *(targetVertexGeom->getVertices());
192-
193-
auto* movingStore = movingPtr->getDataStore();
191+
Float32Array& movingVerticesRef = *(movingVertexGeom->getVertices());
192+
if(movingVerticesRef.empty())
193+
{
194+
auto ss = fmt::format("Moving Vertex Geometry does not contain any vertices");
195+
return {nonstd::make_unexpected(std::vector<Error>{Error{k_EmptyVertices, ss}})};
196+
}
197+
Float32Array& targetVerticesRef = *(targetVertexGeom->getVertices());
198+
if(targetVerticesRef.empty())
199+
{
200+
auto ss = fmt::format("Target Vertex Geometry does not contain any vertices");
201+
return {nonstd::make_unexpected(std::vector<Error>{Error{k_EmptyVertices, ss}})};
202+
}
203+
auto* movingStore = movingVerticesRef.getDataStore();
194204
std::vector<float32> movingVector(movingStore->begin(), movingStore->end());
195205
float32* movingCopyPtr = movingVector.data();
196206
DataStructure tmp;
@@ -236,9 +246,9 @@ Result<> IterativeClosestPointFilter::executeImpl(DataStructure& data, const Arg
236246
nanoflann::KNNResultSet<float> results(nn);
237247
results.init(&identifier, &dist);
238248
index.findNeighbors(results, movingCopyPtr + (3 * j), nanoflann::SearchParams());
239-
dynTargetPtr[3 * j + 0] = targetPtr[3 * identifier + 0];
240-
dynTargetPtr[3 * j + 1] = targetPtr[3 * identifier + 1];
241-
dynTargetPtr[3 * j + 2] = targetPtr[3 * identifier + 2];
249+
dynTargetPtr[3 * j + 0] = targetVerticesRef[3 * identifier + 0];
250+
dynTargetPtr[3 * j + 1] = targetVerticesRef[3 * identifier + 1];
251+
dynTargetPtr[3 * j + 2] = targetVerticesRef[3 * identifier + 2];
242252
}
243253

244254
Eigen::Map<PointCloud> moving_(movingCopyPtr, 3, numMovingVerts);
@@ -271,11 +281,11 @@ Result<> IterativeClosestPointFilter::executeImpl(DataStructure& data, const Arg
271281
{
272282
for(usize j = 0; j < numMovingVerts; j++)
273283
{
274-
Eigen::Vector4f position((*movingPtr)[3 * j + 0], (*movingPtr)[3 * j + 1], (*movingPtr)[3 * j + 2], 1);
284+
Eigen::Vector4f position(movingVerticesRef[3 * j + 0], movingVerticesRef[3 * j + 1], movingVerticesRef[3 * j + 2], 1);
275285
Eigen::Vector4f transformedPosition = globalTransform * position;
276286
for(usize k = 0; k < 3; k++)
277287
{
278-
(*movingPtr)[3 * j + k] = transformedPosition.data()[k];
288+
movingVerticesRef[3 * j + k] = transformedPosition.data()[k];
279289
}
280290
}
281291
}

0 commit comments

Comments
 (0)