Skip to content

Commit 060464a

Browse files
authored
Merge pull request #33 from diffCheckOrg/semantic_segmentation
Backend cpp for model-based semantic segmentation
2 parents 73888c9 + 38c12c5 commit 060464a

File tree

7 files changed

+405
-17
lines changed

7 files changed

+405
-17
lines changed

src/diffCheck/geometry/DFMesh.cc

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,22 @@ namespace diffCheck::geometry
7979
return bboxPts;
8080
}
8181

82+
Eigen::Vector3d DFMesh::GetFirstNormal()
83+
{
84+
if (this->NormalsFace.size() == 0)
85+
{
86+
std::shared_ptr<open3d::geometry::TriangleMesh> O3DTriangleMesh = this->Cvt2O3DTriangleMesh();
87+
O3DTriangleMesh->ComputeTriangleNormals();
88+
this->NormalsFace.resize(O3DTriangleMesh->triangle_normals_.size());
89+
for (size_t i = 0; i < O3DTriangleMesh->triangle_normals_.size(); i++)
90+
{
91+
this->NormalsFace[i] = O3DTriangleMesh->triangle_normals_[i];
92+
}
93+
94+
}
95+
return this->NormalsFace[0];
96+
}
97+
8298
void DFMesh::LoadFromPLY(const std::string &path)
8399
{
84100
std::shared_ptr<diffCheck::geometry::DFMesh> tempMesh_ptr = diffCheck::io::ReadPLYMeshFromFile(path);

src/diffCheck/geometry/DFMesh.hh

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,13 @@ namespace diffCheck::geometry
7171
*/
7272
std::vector<Eigen::Vector3d> GetTightBoundingBox();
7373

74+
/**
75+
* @brief Get the first normal of the mesh. Meant for planar meshes
76+
*
77+
* @return Eigen::Vector3d the normal
78+
*/
79+
Eigen::Vector3d GetFirstNormal();
80+
7481
public: ///< I/O loader
7582
/**
7683
* @brief Read a mesh from a file

src/diffCheck/geometry/DFPointCloud.hh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ namespace diffCheck::geometry
8181
*/
8282
void EstimateNormals(
8383
bool useCilantroEvaluator = false,
84-
std::optional<int> knn = 100,
84+
std::optional<int> knn = 50,
8585
std::optional<double> searchRadius = std::nullopt);
8686

8787
/**

src/diffCheck/segmentation/DFSegmentation.cc

Lines changed: 254 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#include <cilantro/utilities/point_cloud.hpp>
44
#include <cilantro/core/nearest_neighbors.hpp>
55
#include <cilantro/clustering/connected_component_extraction.hpp>
6+
#include <cmath>
67

78
namespace diffCheck::segmentation
89
{
@@ -60,7 +61,6 @@ namespace diffCheck::segmentation
6061
else
6162
{
6263
cilantro::RadiusNeighborhoodSpecification<float> neighborhood(radiusNeighborhoodSize);
63-
6464
cilantro::NormalsProximityEvaluator<float, 3> similarityEvaluator(
6565
cilantroPointCloud->normals,
6666
normalThresholdDegree*M_PI/180.0f);
@@ -94,4 +94,257 @@ namespace diffCheck::segmentation
9494

9595
return segments;
9696
}
97+
98+
std::shared_ptr<geometry::DFPointCloud> DFSegmentation::AssociateClustersToMeshes(
99+
std::vector<std::shared_ptr<geometry::DFMesh>> referenceMesh,
100+
std::vector<std::shared_ptr<geometry::DFPointCloud>> &clusters,
101+
double angleThreshold,
102+
double associationThreshold)
103+
{
104+
std::shared_ptr<geometry::DFPointCloud> unifiedPointCloud = std::make_shared<geometry::DFPointCloud>();
105+
106+
// iterate through the mesh faces given as function argument
107+
if (referenceMesh.size() == 0)
108+
{
109+
DIFFCHECK_WARN("No mesh faces to associate with the clusters. Returning the first clusters untouched.");
110+
return clusters[0];
111+
}
112+
for (std::shared_ptr<diffCheck::geometry::DFMesh> face : referenceMesh)
113+
{
114+
std::shared_ptr<geometry::DFPointCloud> correspondingSegment;
115+
116+
// Getting the center of the mesh face
117+
Eigen::Vector3d faceCenter = face->Cvt2O3DTriangleMesh()->GetCenter();
118+
119+
// Getting the normal of the mesh face
120+
Eigen::Vector3d faceNormal = face->GetFirstNormal();
121+
faceNormal.normalize();
122+
123+
// iterate through the segments to find the closest one compared to the face center taking the normals into account
124+
Eigen::Vector3d segmentCenter;
125+
Eigen::Vector3d segmentNormal;
126+
double faceDistance = std::numeric_limits<double>::max();
127+
if (clusters.size() == 0)
128+
{
129+
DIFFCHECK_WARN("No clusters to associate with the mesh faces. Returning the first mesh face untouched.");
130+
return clusters[0];
131+
}
132+
for (auto segment : clusters)
133+
{
134+
for (auto point : segment->Points){segmentCenter += point;}
135+
segmentCenter /= segment->GetNumPoints();
136+
137+
for (auto normal : segment->Normals){segmentNormal += normal;}
138+
segmentNormal.normalize();
139+
140+
double currentDistance = (faceCenter - segmentCenter).norm() / std::abs(segmentNormal.dot(faceNormal));
141+
// if the distance is smaller than the previous one, update the distance and the corresponding segment
142+
if (std::abs(sin(acos(faceNormal.dot(segmentNormal)))) < angleThreshold && currentDistance < faceDistance)
143+
{
144+
correspondingSegment = segment;
145+
faceDistance = currentDistance;
146+
}
147+
}
148+
149+
// get the triangles of the face.
150+
std::vector<Eigen::Vector3i> faceTriangles = face->Faces;
151+
152+
if (correspondingSegment == nullptr)
153+
{
154+
DIFFCHECK_WARN("No segment found for the face. Skipping the face.");
155+
continue;
156+
}
157+
for (Eigen::Vector3d point : correspondingSegment->Points)
158+
{
159+
bool pointInFace = false;
160+
if (DFSegmentation::IsPointOnFace(face, point, associationThreshold))
161+
{
162+
unifiedPointCloud->Points.push_back(point);
163+
unifiedPointCloud->Normals.push_back(
164+
correspondingSegment->Normals[std::distance(
165+
correspondingSegment->Points.begin(),
166+
std::find(correspondingSegment->Points.begin(),
167+
correspondingSegment->Points.end(),
168+
point))]
169+
);
170+
}
171+
}
172+
// removing points from the segment that are in the face
173+
if (unifiedPointCloud->GetNumPoints() == 0)
174+
{
175+
DIFFCHECK_WARN("No point was associated to this segment. Skipping the segment.");
176+
continue;
177+
}
178+
for(Eigen::Vector3d point : unifiedPointCloud->Points)
179+
{
180+
correspondingSegment->Points.erase(
181+
std::remove(
182+
correspondingSegment->Points.begin(),
183+
correspondingSegment->Points.end(),
184+
point),
185+
correspondingSegment->Points.end());
186+
}
187+
// removing the corresponding segment if it is empty after the point transfer
188+
if (correspondingSegment->GetNumPoints() == 0)
189+
{
190+
clusters.erase(
191+
std::remove(
192+
clusters.begin(),
193+
clusters.end(),
194+
correspondingSegment),
195+
clusters.end());
196+
}
197+
}
198+
return unifiedPointCloud;
199+
}
200+
201+
void DFSegmentation::CleanUnassociatedClusters(
202+
std::vector<std::shared_ptr<geometry::DFPointCloud>> &unassociatedClusters,
203+
std::vector<std::shared_ptr<geometry::DFPointCloud>> &existingPointCloudSegments,
204+
std::vector<std::vector<std::shared_ptr<geometry::DFMesh>>> meshes,
205+
double angleThreshold,
206+
double associationThreshold)
207+
{
208+
if (unassociatedClusters.size() == 0)
209+
{
210+
DIFFCHECK_WARN("No unassociated clusters. Nothing is done");
211+
return;
212+
}
213+
for (std::shared_ptr<geometry::DFPointCloud> cluster : unassociatedClusters)
214+
{
215+
Eigen::Vector3d clusterCenter;
216+
Eigen::Vector3d clusterNormal;
217+
218+
if (cluster->GetNumPoints() == 0)
219+
{
220+
DIFFCHECK_WARN("Empty cluster. Skipping the cluster.");
221+
continue;
222+
}
223+
for (Eigen::Vector3d point : cluster->Points)
224+
{
225+
clusterCenter += point;
226+
}
227+
clusterCenter /= cluster->GetNumPoints();
228+
for (Eigen::Vector3d normal : cluster->Normals)
229+
{
230+
clusterNormal += normal;
231+
}
232+
clusterNormal.normalize();
233+
234+
int meshIndex = std::numeric_limits<int>::max();
235+
int faceIndex = std::numeric_limits<int>::max() ;
236+
double distance = std::numeric_limits<double>::max();
237+
238+
if (meshes.size() == 0)
239+
{
240+
DIFFCHECK_WARN("No meshes to associate with the clusters. Skipping the cluster.");
241+
continue;
242+
}
243+
for (std::vector<std::shared_ptr<geometry::DFMesh>> piece : meshes)
244+
{
245+
if (piece.size() == 0)
246+
{
247+
DIFFCHECK_WARN("Empty piece in the meshes vector. Skipping the mesh face vector.");
248+
continue;
249+
}
250+
for (std::shared_ptr<geometry::DFMesh> meshFace : piece)
251+
{
252+
Eigen::Vector3d faceCenter;
253+
Eigen::Vector3d faceNormal;
254+
255+
std::shared_ptr<open3d::geometry::TriangleMesh> o3dFace = meshFace->Cvt2O3DTriangleMesh();
256+
257+
faceNormal = meshFace->GetFirstNormal();
258+
faceNormal.normalize();
259+
faceCenter = o3dFace->GetCenter();
260+
/*
261+
To make sure we select the right meshFace, we add another metric:
262+
Indeed, from experimentation, sometimes the wrong mesh face is selected, because it is parallel to the correct one
263+
(so the normal don't play a role) and the center of the face is closer to the cluster center than the correct face.
264+
To prevent this, we take into the account the angle between the line linking the center of the meshFace considered
265+
and the center of the point cloud cluster and the normal of the cluster. This value should be close to pi/2
266+
267+
The following two lines are not super optimized but more readable than the optimized version
268+
*/
269+
270+
double clusterNormalToJunctionLineAngle = std::abs(std::acos(clusterNormal.dot((clusterCenter - faceCenter).normalized())));
271+
272+
double currentDistance = (clusterCenter - faceCenter).norm() * std::pow(std::cos(clusterNormalToJunctionLineAngle), 2) / std::pow(clusterNormal.dot(faceNormal), 2);
273+
if (std::abs(sin(acos(faceNormal.dot(clusterNormal)))) < angleThreshold && currentDistance < distance)
274+
{
275+
distance = currentDistance;
276+
meshIndex = std::distance(meshes.begin(), std::find(meshes.begin(), meshes.end(), piece));
277+
faceIndex = std::distance(piece.begin(), std::find(piece.begin(), piece.end(), meshFace));
278+
}
279+
}
280+
}
281+
if (meshIndex >= meshes.size() || faceIndex >= meshes[meshIndex].size())
282+
{
283+
// this one generates a lot of warnings
284+
DIFFCHECK_WARN("No mesh face found for the cluster. Skipping the cluster.");
285+
continue;
286+
}
287+
std::shared_ptr<geometry::DFPointCloud> completed_segment = existingPointCloudSegments[meshIndex];
288+
for (Eigen::Vector3d point : cluster->Points)
289+
{
290+
std::vector<Eigen::Vector3i> faceTriangles = meshes[meshIndex][faceIndex]->Faces;
291+
if (IsPointOnFace(meshes[meshIndex][faceIndex], point, associationThreshold))
292+
{
293+
completed_segment->Points.push_back(point);
294+
completed_segment->Normals.push_back(cluster->Normals[std::distance(cluster->Points.begin(), std::find(cluster->Points.begin(), cluster->Points.end(), point))]);
295+
}
296+
}
297+
std::vector<int> indicesToRemove;
298+
299+
for (int i = 0; i < cluster->Points.size(); ++i)
300+
{
301+
if (std::find(completed_segment->Points.begin(), completed_segment->Points.end(), cluster->Points[i]) != completed_segment->Points.end())
302+
{
303+
indicesToRemove.push_back(i);
304+
}
305+
}
306+
for (auto it = indicesToRemove.rbegin(); it != indicesToRemove.rend(); ++it)
307+
{
308+
std::swap(cluster->Points[*it], cluster->Points.back());
309+
cluster->Points.pop_back();
310+
std::swap(cluster->Normals[*it], cluster->Normals.back());
311+
cluster->Normals.pop_back();
312+
}
313+
}
314+
};
315+
316+
bool DFSegmentation::IsPointOnFace(
317+
std::shared_ptr<diffCheck::geometry::DFMesh> face,
318+
Eigen::Vector3d point,
319+
double associationThreshold)
320+
{
321+
/*
322+
To check if the point is in the face, we take into account all the triangles forming the face.
323+
We calculate the area of each triangle, then check if the sum of the areas of the tree triangles
324+
formed by two of the points of the referencr triangle and our point is equal to the reference triangle area
325+
(within a user-defined margin). If it is the case, the triangle is in the face.
326+
*/
327+
std::vector<Eigen::Vector3i> faceTriangles = face->Faces;
328+
for (Eigen::Vector3i triangle : faceTriangles)
329+
{
330+
Eigen::Vector3d v0 = face->Vertices[triangle[0]];
331+
Eigen::Vector3d v1 = face->Vertices[triangle[1]];
332+
Eigen::Vector3d v2 = face->Vertices[triangle[2]];
333+
Eigen::Vector3d n = (v1 - v0).cross(v2 - v0);
334+
double referenceTriangleArea = n.norm()*0.5;
335+
Eigen::Vector3d n1 = (v1 - v0).cross(point - v0);
336+
double area1 = n1.norm()*0.5;
337+
Eigen::Vector3d n2 = (v2 - v1).cross(point - v1);
338+
double area2 = n2.norm()*0.5;
339+
Eigen::Vector3d n3 = (v0 - v2).cross(point - v2);
340+
double area3 = n3.norm()*0.5;
341+
double res = ( area1 + area2 + area3 - referenceTriangleArea) / referenceTriangleArea;
342+
if (res < associationThreshold)
343+
{
344+
return true;
345+
break;
346+
}
347+
}
348+
return false;
349+
}
97350
} // namespace diffCheck::segmentation

src/diffCheck/segmentation/DFSegmentation.hh

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ namespace diffCheck::segmentation
55
{
66
class DFSegmentation
77
{
8-
public:
8+
public: ///< main segmentation methods
99
/** @brief Downsamples and segments the point cloud using Cilantro's ConnectedComponentExtraction3f method. It uses the normals' variations to detect different parts in the point cloud.
1010
* @param pointCloud the point cloud to segment
1111
* @param normalThresholdDegree the normal threshold in degrees do differentiate segments. The higher the number, the more tolerent the segmentation will be to normal differences
@@ -24,5 +24,44 @@ namespace diffCheck::segmentation
2424
int knnNeighborhoodSize = 10,
2525
float radiusNeighborhoodSize = 10.f,
2626
bool colorClusters = false);
27+
28+
public: ///< segmentation refinement methods
29+
/** @brief Associates point cloud segments to mesh faces and merges them. It uses the center of mass of the segments and the mesh faces to find correspondances. For each mesh face it then iteratively associate the points of the segment that are actually on the mesh face.
30+
* @param referenceMesh the vector of mesh faces to associate with the segments
31+
* @param clusters the vector of clusters from cilantro to associate with the mesh faces of the reference mesh
32+
* @param angleThreshold the threshold to consider the a cluster as potential candidate for association. the value passed is the minimum sine of the angles. A value of 0 requires perfect alignment (angle = 0), while a value of 0.1 allows an angle of 5.7 degrees.
33+
* @param associationThreshold the threshold to consider the points of a segment and a mesh face as associable. It is the ratio between the surface of the closest mesh triangle and the sum of the areas of the three triangles that form the rest of the pyramid described by the mesh triangle and the point we want to associate or not. The lower the number, the more strict the association will be and some poinnts on the mesh face might be wrongfully excluded.
34+
* @return std::shared_ptr<geometry::DFPointCloud> The unified segments
35+
*/
36+
static std::shared_ptr<geometry::DFPointCloud> DFSegmentation::AssociateClustersToMeshes(
37+
std::vector<std::shared_ptr<geometry::DFMesh>> referenceMesh,
38+
std::vector<std::shared_ptr<geometry::DFPointCloud>> &clusters,
39+
double angleThreshold = 0.1,
40+
double associationThreshold = 0.1);
41+
42+
/** @brief Iterated through clusters and finds the corresponding mesh face. It then associates the points of the cluster that are on the mesh face to the segment already associated with the mesh face.
43+
* @param unassociatedClusters the clusters from the normal-based segmentatinon that haven't been associated yet.
44+
* @param existingPointCloudSegments the already associated segments
45+
* @param Meshes the mesh faces for all the model. This is used to associate the clusters to the mesh faces.
46+
* * @param angleThreshold the threshold to consider the a cluster as potential candidate for association. the value passed is the minimum sine of the angles. A value of 0 requires perfect alignment (angle = 0), while a value of 0.1 allows an angle of 5.7 degrees.
47+
* @param associationThreshold the threshold to consider the points of a segment and a mesh face as associable. It is the ratio between the surface of the closest mesh triangle and the sum of the areas of the three triangles that form the rest of the pyramid described by the mesh triangle and the point we want to associate or not. The lower the number, the more strict the association will be and some poinnts on the mesh face might be wrongfully excluded.
48+
*/
49+
static void DFSegmentation::CleanUnassociatedClusters(
50+
std::vector<std::shared_ptr<geometry::DFPointCloud>> &unassociatedClusters,
51+
std::vector<std::shared_ptr<geometry::DFPointCloud>> &existingPointCloudSegments,
52+
std::vector<std::vector<std::shared_ptr<geometry::DFMesh>>> Meshes,
53+
double angleThreshold = 0.1,
54+
double associationThreshold = 0.1);
55+
56+
private: ///< helper methods
57+
/** @brief private method to check if a point is on a face of a triangle mesh triangle, within a certain association threshold. This takes into account the fact that, in 3D, a point can be "above" a triangle of a triangle mesh but still considered as being on the mesh face.
58+
* @param face the triangle mesh face to check the point against
59+
* @param point the point to check
60+
* @param associationThreshold the threshold to consider the point associable to the mesh. It is the ratio between the surface of the closest mesh triangle and the sum of the areas of the three triangles that form the rest of the pyramid described by the mesh triangle and the point we want to associate or not. The lower the number, the more strict the association will be and some poinnts on the mesh face might be wrongfully excluded.
61+
*/
62+
static bool DFSegmentation::IsPointOnFace(
63+
std::shared_ptr<diffCheck::geometry::DFMesh> face,
64+
Eigen::Vector3d point,
65+
double associationThreshold);
2766
};
2867
} // namespace diffCheck::segmentation

0 commit comments

Comments
 (0)