VisibilityCheck.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 ArmarXSimulation::components::FakeObjectDetector
17 * @author Timo Birr ( timo dot birr at 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 "VisibilityCheck.h"
24
25#include <algorithm>
26#include <cmath>
27#include <limits>
28
30{
31
32 namespace
33 {
34 /// Below this, a direction component is treated as parallel to a slab.
35 constexpr float parallelEpsilon = 1e-9F;
36
37 /// The local axis a face is perpendicular to.
38 int
39 faceAxis(int faceIndex)
40 {
41 return faceIndex / 2;
42 }
43
44 /// +1 for the positive face of an axis, -1 for the negative one.
45 float
46 faceSign(int faceIndex)
47 {
48 return (faceIndex % 2 == 0) ? 1.0F : -1.0F;
49 }
50 } // namespace
51
52 Eigen::Vector3f
54 {
55 return centerPose.topRightCorner<3, 1>();
56 }
57
58 Eigen::Matrix3f
59 Box::axes() const
60 {
61 return centerPose.topLeftCorner<3, 3>();
62 }
63
64 std::array<Eigen::Vector3f, 8>
66 {
67 const Eigen::Matrix3f r = axes();
68 const Eigen::Vector3f c = center();
69
70 std::array<Eigen::Vector3f, 8> result;
71 for (int i = 0; i < 8; ++i)
72 {
73 // Bit i of the index selects the sign along local axis i.
74 const Eigen::Vector3f signs{
75 (i & 1) ? 1.0F : -1.0F, (i & 2) ? 1.0F : -1.0F, (i & 4) ? 1.0F : -1.0F};
76 result[static_cast<std::size_t>(i)] = c + r * signs.cwiseProduct(halfExtents);
77 }
78 return result;
79 }
80
81 Eigen::Vector3f
83 {
84 return cameraPose.topRightCorner<3, 1>();
85 }
86
87 Eigen::Vector3f
88 Frustum::toViewFrame(const Eigen::Vector3f& globalPoint) const
89 {
90 const Eigen::Matrix3f r = cameraPose.topLeftCorner<3, 3>();
91 const Eigen::Vector3f local = r.transpose() * (globalPoint - position());
92
93 const Eigen::Vector3f forward = forwardLocal.normalized();
94 const Eigen::Vector3f up = upLocal.normalized();
95 // With forward = +Z and up = -Y this yields right = +X, the usual image convention.
96 const Eigen::Vector3f right = forward.cross(up);
97
98 return {local.dot(forward), local.dot(right), local.dot(up)};
99 }
100
101 bool
102 Frustum::contains(const Eigen::Vector3f& globalPoint) const
103 {
104 const Eigen::Vector3f view = toViewFrame(globalPoint);
105 const float depth = view.x();
106
107 // Also rejects everything behind the camera, as long as minDistance >= 0.
108 if (depth < minDistance or depth > maxDistance)
109 {
110 return false;
111 }
112
113 const float horizontalAngle = std::atan2(view.y(), depth);
114 const float verticalAngle = std::atan2(view.z(), depth);
115
116 return std::abs(horizontalAngle) <= horizontalFov / 2 and
117 std::abs(verticalAngle) <= verticalFov / 2;
118 }
119
120 bool
121 isInFieldOfView(const Frustum& frustum, const Box& box, bool requireFullyInside)
122 {
123 const std::array<Eigen::Vector3f, 8> corners = box.corners();
124
125 if (requireFullyInside)
126 {
127 return std::all_of(corners.begin(),
128 corners.end(),
129 [&frustum](const Eigen::Vector3f& c)
130 { return frustum.contains(c); });
131 }
132
133 if (std::any_of(corners.begin(),
134 corners.end(),
135 [&frustum](const Eigen::Vector3f& c) { return frustum.contains(c); }))
136 {
137 return true;
138 }
139
140 // Catches a box that is large relative to the frustum and whose corners all fall outside.
141 return frustum.contains(box.center());
142 }
143
144 Eigen::Vector3f
145 faceNormal(const Box& box, int faceIndex)
146 {
147 return faceSign(faceIndex) * box.axes().col(faceAxis(faceIndex)).normalized();
148 }
149
150 Eigen::Vector3f
151 faceCenter(const Box& box, int faceIndex)
152 {
153 const int axis = faceAxis(faceIndex);
154 return box.center() + faceSign(faceIndex) * box.axes().col(axis) * box.halfExtents(axis);
155 }
156
157 int
158 frontFaceIndex(const Frustum& frustum, const Box& box)
159 {
160 const Eigen::Vector3f cameraPosition = frustum.position();
161
162 int bestFace = 0;
163 float bestScore = -std::numeric_limits<float>::infinity();
164
165 for (int face = 0; face < 6; ++face)
166 {
167 const Eigen::Vector3f toFace = faceCenter(box, face) - cameraPosition;
168 const float distance = toFace.norm();
169 if (distance < parallelEpsilon)
170 {
171 // Camera sits on the face; treat it as perfectly front facing.
172 return face;
173 }
174
175 // The more the face normal opposes the viewing direction, the more it faces us.
176 const float score = -(toFace / distance).dot(faceNormal(box, face));
177 if (score > bestScore)
178 {
179 bestScore = score;
180 bestFace = face;
181 }
182 }
183 return bestFace;
184 }
185
186 std::vector<Eigen::Vector3f>
187 sampleFace(const Box& box, int faceIndex, int gridN)
188 {
189 std::vector<Eigen::Vector3f> samples;
190 if (gridN < 1)
191 {
192 return samples;
193 }
194
195 const int axis = faceAxis(faceIndex);
196 const int axisU = (axis + 1) % 3;
197 const int axisV = (axis + 2) % 3;
198
199 const Eigen::Matrix3f r = box.axes();
200 const Eigen::Vector3f center = faceCenter(box, faceIndex);
201
202 samples.reserve(static_cast<std::size_t>(gridN) * static_cast<std::size_t>(gridN));
203 for (int i = 0; i < gridN; ++i)
204 {
205 // Cell centres in [-1, 1], so samples stay clear of the face edges.
206 const float u = ((i + 0.5F) / gridN) * 2.0F - 1.0F;
207 for (int j = 0; j < gridN; ++j)
208 {
209 const float v = ((j + 0.5F) / gridN) * 2.0F - 1.0F;
210 samples.push_back(center + r.col(axisU) * (u * box.halfExtents(axisU)) +
211 r.col(axisV) * (v * box.halfExtents(axisV)));
212 }
213 }
214 return samples;
215 }
216
217 std::optional<float>
218 intersectRayBox(const Eigen::Vector3f& origin,
219 const Eigen::Vector3f& unitDirection,
220 const Box& box)
221 {
222 const Eigen::Matrix3f r = box.axes();
223
224 // Work in the box's local frame, where the slabs are axis aligned.
225 const Eigen::Vector3f localOrigin = r.transpose() * (origin - box.center());
226 const Eigen::Vector3f localDirection = r.transpose() * unitDirection;
227
228 float tMin = -std::numeric_limits<float>::infinity();
229 float tMax = std::numeric_limits<float>::infinity();
230
231 for (int i = 0; i < 3; ++i)
232 {
233 const float half = box.halfExtents(i);
234 if (std::abs(localDirection(i)) < parallelEpsilon)
235 {
236 // Parallel to this slab: a miss unless the origin is already between its planes.
237 if (std::abs(localOrigin(i)) > half)
238 {
239 return std::nullopt;
240 }
241 continue;
242 }
243
244 float tEnter = (-half - localOrigin(i)) / localDirection(i);
245 float tExit = (half - localOrigin(i)) / localDirection(i);
246 if (tEnter > tExit)
247 {
248 std::swap(tEnter, tExit);
249 }
250
251 tMin = std::max(tMin, tEnter);
252 tMax = std::min(tMax, tExit);
253
254 if (tMin > tMax)
255 {
256 return std::nullopt;
257 }
258 }
259
260 if (tMax < 0.0F)
261 {
262 // The box lies entirely behind the ray origin.
263 return std::nullopt;
264 }
265
266 return std::max(tMin, 0.0F);
267 }
268
269 float
270 visibleFraction(const Frustum& frustum,
271 const Box& target,
272 const std::vector<Box>& occluders,
273 int gridN,
274 float epsilon)
275 {
276 const std::vector<Eigen::Vector3f> samples =
277 sampleFace(target, frontFaceIndex(frustum, target), gridN);
278 if (samples.empty())
279 {
280 return 0.0F;
281 }
282
283 const Eigen::Vector3f cameraPosition = frustum.position();
284
285 std::size_t visible = 0;
286 for (const Eigen::Vector3f& sample : samples)
287 {
288 const Eigen::Vector3f toSample = sample - cameraPosition;
289 const float distance = toSample.norm();
290 if (distance < parallelEpsilon)
291 {
292 // The camera is on the sample point - nothing can be in between.
293 ++visible;
294 continue;
295 }
296
297 const Eigen::Vector3f direction = toSample / distance;
298 const bool blocked = std::any_of(
299 occluders.begin(),
300 occluders.end(),
301 [&](const Box& occluder)
302 {
303 const std::optional<float> hit =
304 intersectRayBox(cameraPosition, direction, occluder);
305 // A hit at ~0 means the camera is inside the occluder (e.g. the
306 // head link the camera is mounted in). That must not block.
307 return hit.has_value() and *hit > epsilon and *hit < distance - epsilon;
308 });
309
310 if (not blocked)
311 {
312 ++visible;
313 }
314 }
315
316 return static_cast<float>(visible) / static_cast<float>(samples.size());
317 }
318
319} // namespace armarx::fake_object_detector
constexpr T c
Geometric primitives for the fake object detector.
std::vector< Eigen::Vector3f > sampleFace(const Box &box, int faceIndex, int gridN)
A gridN x gridN grid of sample points spread over faceIndex, in global coordinates.
int frontFaceIndex(const Frustum &frustum, const Box &box)
The face of box that points most directly at the camera - the "front facing side" of the bounding box...
std::optional< float > intersectRayBox(const Eigen::Vector3f &origin, const Eigen::Vector3f &unitDirection, const Box &box)
Intersect a ray with an oriented box using the slab method.
bool isInFieldOfView(const Frustum &frustum, const Box &box, bool requireFullyInside)
Whether box is inside the camera frustum.
float visibleFraction(const Frustum &frustum, const Box &target, const std::vector< Box > &occluders, int gridN, float epsilon)
The fraction of sample points on the front face of target that the camera can see.
Eigen::Vector3f faceNormal(const Box &box, int faceIndex)
Outward unit normal of faceIndex in global coordinates.
Eigen::Vector3f faceCenter(const Box &box, int faceIndex)
Centre of faceIndex in global coordinates.
double distance(const Point &a, const Point &b)
Definition point.hpp:95
double dot(const Point &x, const Point &y)
Definition point.hpp:57
An oriented bounding box in the global frame.
std::array< Eigen::Vector3f, 8 > corners() const
The eight corners in global coordinates.
Eigen::Matrix4f centerPose
Rotation = box axes, translation = box centre.
Eigen::Vector3f halfExtents
Half the extent along each local box axis.
bool contains(const Eigen::Vector3f &globalPoint) const
Whether a global point lies inside the frustum.
Eigen::Vector3f upLocal
Up axis in the camera's local frame.
Eigen::Matrix4f cameraPose
Camera -> global.
Eigen::Vector3f toViewFrame(const Eigen::Vector3f &globalPoint) const
Express a global point in the camera's viewing frame.
float horizontalFov
Full opening angles [rad].
Eigen::Vector3f forwardLocal
Viewing axis in the camera's local frame.