CableClusterFilter.cpp
Go to the documentation of this file.
1/*
2 * This file is part of ArmarX.
3 *
4 * ArmarX is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License version 2 as
6 * published by the Free Software Foundation.
7 *
8 * ArmarX is distributed in the hope that it will be useful, but
9 * WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 *
16 * @package Navigation::ArmarXObjects::LaserScannerFeatureExtraction
17 * @author Niklas Arlt ( niklas dot arlt at student dot kit dot edu )
18 * @date 2026
19 * @copyright http://www.gnu.org/licenses/gpl-2.0.txt
20 * GNU General Public License
21 */
22
23#include "CableClusterFilter.h"
24
25#include <math.h>
26
27#include <algorithm>
28#include <cmath>
29#include <cstddef>
30#include <limits>
31#include <utility>
32#include <vector>
33
34#include <Eigen/Core>
35#include <Eigen/Eigenvalues>
36
37#include <VirtualRobot/MathTools.h>
38
40
42{
43
44 namespace
45 {
46 float
47 distanceToSegment(const Eigen::Vector2f& pt,
48 const Eigen::Vector2f& a,
49 const Eigen::Vector2f& b)
50 {
51 const Eigen::Vector2f ab = b - a;
52 const float squaredLength = ab.squaredNorm();
53 if (squaredLength < std::numeric_limits<float>::epsilon())
54 {
55 return (pt - a).norm();
56 }
57
58 const float t = std::clamp((pt - a).dot(ab) / squaredLength, 0.F, 1.F);
59 return (pt - (a + t * ab)).norm();
60 }
61 } // namespace
62
63 float
64 distanceToConvexHullBoundary(const Eigen::Vector2f& pt,
65 const VirtualRobot::MathTools::ConvexHull2D& hull)
66 {
67 const auto& vertices = hull.vertices;
68 ARMARX_CHECK_GREATER_EQUAL(vertices.size(), 3);
69
70 // The vertices of a convex hull are ordered along the boundary, so the point
71 // is inside iff it lies on the same side of all edges.
72 bool anyPositive = false;
73 bool anyNegative = false;
74
75 float minEdgeDistance = std::numeric_limits<float>::max();
76
77 for (std::size_t i = 0; i < vertices.size(); i++)
78 {
79 const Eigen::Vector2f& a = vertices[i];
80 const Eigen::Vector2f& b = vertices[(i + 1) % vertices.size()];
81
82 const Eigen::Vector2f edge = b - a;
83 const Eigen::Vector2f toPt = pt - a;
84 const float cross = edge.x() * toPt.y() - edge.y() * toPt.x();
85
86 anyPositive |= cross > 0;
87 anyNegative |= cross < 0;
88
89 minEdgeDistance = std::min(minEdgeDistance, distanceToSegment(pt, a, b));
90 }
91
92 const bool inside = not(anyPositive and anyNegative);
93 return inside ? 0.F : minEdgeDistance;
94 }
95
97 params(params), distanceToHull(std::move(distanceToHull))
98 {
99 ARMARX_CHECK(this->distanceToHull);
100
101 const float plugDistance = params.plugPosition.norm();
102 angularTestValid = plugDistance > 1.F; // [mm]
103 if (angularTestValid)
104 {
105 outward = params.plugPosition / plugDistance;
106 }
107 }
108
109 bool
110 CableClusterFilter::isCandidate(const std::vector<Eigen::Vector2f>& clusterPoints) const
111 {
112 if (clusterPoints.empty())
113 {
114 return false;
115 }
116
117 const float cosHalfWindowAngle = std::cos(params.windowAngle / 2);
118
119 for (const Eigen::Vector2f& pt : clusterPoints)
120 {
121 if (angularTestValid)
122 {
123 const Eigen::Vector2f fromPlug = pt - params.plugPosition;
124 const float distanceToPlug = fromPlug.norm();
125
126 // points at the plug-in point itself always pass the angular test
127 if (distanceToPlug > 1.F // [mm]
128 and fromPlug.dot(outward) / distanceToPlug < cosHalfWindowAngle)
129 {
130 return false;
131 }
132 }
133
134 if (distanceToHull(pt) > params.maxDistanceToHull)
135 {
136 return false;
137 }
138 }
139
140 return thickness(clusterPoints) <= params.maxThickness;
141 }
142
143 float
144 CableClusterFilter::thickness(const std::vector<Eigen::Vector2f>& points)
145 {
146 if (points.size() < 2)
147 {
148 return 0.F;
149 }
150
151 Eigen::Vector2f mean = Eigen::Vector2f::Zero();
152 for (const Eigen::Vector2f& pt : points)
153 {
154 mean += pt;
155 }
156 mean /= static_cast<float>(points.size());
157
158 Eigen::Matrix2f covariance = Eigen::Matrix2f::Zero();
159 for (const Eigen::Vector2f& pt : points)
160 {
161 const Eigen::Vector2f centered = pt - mean;
162 covariance += centered * centered.transpose();
163 }
164 covariance /= static_cast<float>(points.size());
165
166 // eigenvalues are sorted in increasing order -> col(0) is the minor axis
167 const Eigen::SelfAdjointEigenSolver<Eigen::Matrix2f> solver(covariance);
168 const Eigen::Vector2f minorAxis = solver.eigenvectors().col(0);
169
170 float minProjection = std::numeric_limits<float>::max();
171 float maxProjection = std::numeric_limits<float>::lowest();
172 for (const Eigen::Vector2f& pt : points)
173 {
174 const float projection = (pt - mean).dot(minorAxis);
175 minProjection = std::min(minProjection, projection);
176 maxProjection = std::max(maxProjection, projection);
177 }
178
179 return maxProjection - minProjection;
180 }
181
182 std::vector<Eigen::Vector2f>
183 CableClusterFilter::regionPolygon(std::size_t numArcSamples) const
184 {
185 ARMARX_CHECK_GREATER_EQUAL(numArcSamples, 2);
186
187 std::vector<Eigen::Vector2f> polygon;
188
189 if (not angularTestValid)
190 {
191 return polygon;
192 }
193
194 polygon.reserve(2 * numArcSamples);
195
196 const float centerAngle = std::atan2(outward.y(), outward.x());
197
198 // generous upper bound for the outer boundary: even a ray grazing the hull
199 // leaves the max-distance band before this radius
200 const float maxRadius = 4 * (params.plugPosition.norm() + params.maxDistanceToHull);
201
202 // The distance to a convex hull is monotonically non-decreasing along an
203 // outward ray, so the largest radius whose point is still within
204 // maxAllowedDistance of the hull is found by bisection. With
205 // maxAllowedDistance = 0, this yields the crossing of the robot outline.
206 const auto boundaryRadius =
207 [this, maxRadius](const Eigen::Vector2f& direction, float maxAllowedDistance)
208 {
209 float lo = 0.F;
210 float hi = maxRadius;
211 for (int iteration = 0; iteration < 32; iteration++)
212 {
213 const float mid = (lo + hi) / 2;
214 if (distanceToHull(params.plugPosition + mid * direction) <= maxAllowedDistance)
215 {
216 lo = mid;
217 }
218 else
219 {
220 hi = mid;
221 }
222 }
223 return lo;
224 };
225
226 const auto windowDirection = [this, centerAngle](std::size_t i, std::size_t n)
227 {
228 const float angle =
229 centerAngle - params.windowAngle / 2 +
230 params.windowAngle * static_cast<float>(i) / static_cast<float>(n - 1);
231 return Eigen::Vector2f(std::cos(angle), std::sin(angle));
232 };
233
234 // outer arc of the angular window on the max-distance boundary
235 for (std::size_t i = 0; i < numArcSamples; i++)
236 {
237 const Eigen::Vector2f direction = windowDirection(i, numArcSamples);
238 polygon.push_back(params.plugPosition +
239 boundaryRadius(direction, params.maxDistanceToHull) * direction);
240 }
241
242 // inner arc along the robot outline, traversed backwards to close the polygon
243 for (std::size_t i = 0; i < numArcSamples; i++)
244 {
245 const Eigen::Vector2f direction =
246 windowDirection(numArcSamples - 1 - i, numArcSamples);
247 polygon.push_back(params.plugPosition + boundaryRadius(direction, 0.F) * direction);
248 }
249
250 return polygon;
251 }
252
253} // namespace armarx::navigation::components::laser_scanner_feature_extraction
#define lo(x)
#define hi(x)
std::vector< Eigen::Vector2f > regionPolygon(std::size_t numArcSamples=24) const
Sampled boundary polygon of the filter region (robot root frame) for visualization: the outer arc of ...
std::function< float(const Eigen::Vector2f &)> DistanceToHullFn
Distance from a point (robot root frame) to the robot hull boundary; 0 if inside.
static float thickness(const std::vector< Eigen::Vector2f > &points)
Extent of the point set along its minor principal axis (PCA).
bool isCandidate(const std::vector< Eigen::Vector2f > &clusterPoints) const
Whether the cluster fulfills all cable criteria.
#define ARMARX_CHECK(expression)
Shortcut for ARMARX_CHECK_EXPRESSION.
#define ARMARX_CHECK_GREATER_EQUAL(lhs, rhs)
This macro evaluates whether lhs is greater or equal (>=) rhs and if it turns out to be false it will...
double a(double t, double a0, double j)
Definition CtrlUtil.h:45
float distanceToConvexHullBoundary(const Eigen::Vector2f &pt, const VirtualRobot::MathTools::ConvexHull2D &hull)
Distance from a point to the boundary of a convex hull; 0 if the point is inside.
std::optional< float > mean(const boost::circular_buffer< NameValueMap > &buffer, const std::string &key)
Point cross(const Point &x, const Point &y)
Definition point.hpp:35
double angle(const Point &a, const Point &b, const Point &c)
Definition point.hpp:109
double dot(const Point &x, const Point &y)
Definition point.hpp:57