Visu.cpp
Go to the documentation of this file.
1#include "Visu.h"
2
3#include <algorithm>
4#include <cmath>
5#include <exception>
6#include <string>
7
8#include <Eigen/Geometry>
9
10#include <SimoxUtility/algorithm/apply.hpp>
11#include <SimoxUtility/algorithm/get_map_keys_values.h>
12#include <SimoxUtility/color/Color.h>
13#include <SimoxUtility/color/ColorMap.h>
14#include <SimoxUtility/color/hsv.h>
15#include <SimoxUtility/math/pose.h>
16
20#include <ArmarXCore/interface/core/PackagePath.h>
21
23
30#include <RobotAPI/interface/components/TrajectoryPlayerInterface.h>
32#include <RobotAPI/libraries/armem_laser_scans/aron/LaserScan.aron.generated.h>
35
37{
38
39
41 {
42 Logging::setTag("Visu");
43 }
44
45 void
47 {
48 defs->optional(
49 p.enabled, prefix + "enabled", "Enable or disable visualization of objects.");
50 defs->optional(p.frequencyHz, prefix + "frequenzyHz", "Frequency of visualization.");
51 defs->optional(
52 p.uniformColor, prefix + "uniformColor", "If enabled, points will be drawn in red.");
53 defs->optional(p.maxRobotAgeMs,
54 prefix + "maxRobotAgeMs",
55 "Maximum age of robot state before a new one is retrieved in milliseconds.");
56 defs->optional(p.colorByIntensity, "colorByIntensity", "");
57 defs->optional(p.pointSizeInPixels, prefix + "pointSizeInPixels", "Point size in pixels.");
58 }
59
60 void
61 Visu::init(const wm::CoreSegment* coreSegment,
63 {
64 this->coreSegment = coreSegment;
65 this->virtualRobotReader = virtualRobotReader;
66 }
67
68 void
70 {
71 this->arviz = arviz;
72 if (debugObserver)
73 {
74 bool batchMode = true;
75 this->debugObserver = DebugObserverHelper("LaserScansMemory", debugObserver, batchMode);
76 }
77
78 if (updateTask)
79 {
80 updateTask->stop();
81 updateTask->join();
82 updateTask = nullptr;
83 }
84 updateTask = new SimpleRunningTask<>([this]() { this->visualizeRun(); });
85 updateTask->start();
86 }
87
89 {
90 disconnect();
91 }
92
93 void
95 {
96 // The visualization task walks the working memory and uses the ArViz client. Both outlive
97 // it only if it is stopped explicitly - otherwise it keeps running through component
98 // teardown and process shutdown.
99 if (updateTask)
100 {
101 updateTask->stop();
102 updateTask->join();
103 updateTask = nullptr;
104 }
105 }
106
107 void
109 const std::string& tabName)
110 {
111 guiUser_ = guiUser;
112 remoteGuiTabName_ = tabName;
113 }
114
115 void
117 {
118 if (needsTabRebuild_.exchange(false))
119 {
120 createOrUpdateCalibrationTab();
121 }
122
123 // Sync widget values back to calibrationData_ whenever any value changed
124 bool anyChanged = false;
125 for (const auto& [frame, spinboxes] : calibrationTab_.sensors)
126 {
127 for (const auto& sb : spinboxes)
128 {
129 anyChanged = anyChanged || sb.hasValueChanged();
130 }
131 }
132
133 if (anyChanged)
134 {
135 std::lock_guard<std::mutex> lock(sensorMutex_);
136 for (const auto& [frame, spinboxes] : calibrationTab_.sensors)
137 {
138 calibrationData_[frame] = {
139 spinboxes[0].getValue(), spinboxes[1].getValue(), spinboxes[2].getValue()};
140 }
141 }
142 }
143
144 void
145 Visu::createOrUpdateCalibrationTab()
146 {
147 using namespace armarx::RemoteGui::Client;
148
149 // Snapshot current frames and their calibration data under lock
150 std::set<std::string> frames;
151 std::map<std::string, SensorCalibrationData> currentData;
152 {
153 std::lock_guard<std::mutex> lock(sensorMutex_);
154 frames = knownSensorFrames_;
155 currentData = calibrationData_;
156 }
157
158 // Rebuild spinboxes from scratch, pre-populated with current values
159 calibrationTab_.sensors.clear();
160
161 GridLayout grid;
162 int row = 0;
163
164 grid.add(Label("Sensor"), {.row = row, .column = 0})
165 .add(Label("x [mm]"), {.row = row, .column = 1})
166 .add(Label("y [mm]"), {.row = row, .column = 2})
167 .add(Label("yaw [deg]"), {.row = row, .column = 3});
168 ++row;
169
170 for (const auto& frame : frames)
171 {
172 const auto& d = currentData[frame];
173 auto& sb = calibrationTab_.sensors[frame];
174
175 sb[0].setRange(-10.f, 10.f);
176 sb[0].setDecimals(1);
177 sb[0].setValue(d.x);
178
179 sb[1].setRange(-10.f, 10.f);
180 sb[1].setDecimals(1);
181 sb[1].setValue(d.y);
182
183 sb[2].setRange(-5.f, 5.f);
184 sb[2].setDecimals(2);
185 sb[2].setValue(d.yaw);
186
187 grid.add(Label(frame), {.row = row, .column = 0})
188 .add(sb[0], {.row = row, .column = 1})
189 .add(sb[1], {.row = row, .column = 2})
190 .add(sb[2], {.row = row, .column = 3});
191 ++row;
192 }
193
194 VBoxLayout root = {grid, VSpacer()};
195 guiUser_->RemoteGui_createTab(remoteGuiTabName_, root, &calibrationTab_);
196 }
197
198 Eigen::Isometry3f
199 Visu::getCalibrationOffset(const std::string& sensorFrame) const
200 {
201 std::lock_guard<std::mutex> lock(sensorMutex_);
202 const auto it = calibrationData_.find(sensorFrame);
203 if (it == calibrationData_.end())
204 {
205 return Eigen::Isometry3f::Identity();
206 }
207
208 const auto& d = it->second;
209 constexpr float kDegToRad = static_cast<float>(M_PI) / 180.f;
210
211 Eigen::Isometry3f offset = Eigen::Isometry3f::Identity();
212 offset.translation().x() = d.x;
213 offset.translation().y() = d.y;
214 offset.linear() =
215 Eigen::AngleAxisf(d.yaw * kDegToRad, Eigen::Vector3f::UnitZ()).toRotationMatrix();
216 return offset;
217 }
218
219 void
220 Visu::visualizeRun()
221 {
222 CycleUtil cycle(static_cast<int>(1000 / p.frequencyHz));
223 while (updateTask and not updateTask->isStopped())
224 {
225 if (p.enabled)
226 {
227 const Time timestamp = Time::Now();
229
230 try
231 {
232 visualizeOnce(timestamp);
233 }
234 catch (const std::exception& e)
235 {
236 ARMARX_WARNING << "Caught exception while visualizing robots: \n" << e.what();
237 }
238 catch (...)
239 {
240 ARMARX_WARNING << "Caught unknown exception while visualizing robots.";
241 }
242
243 if (debugObserver.has_value())
244 {
245 debugObserver->sendDebugObserverBatch();
246 }
247 }
248 cycle.waitForCycleDuration();
249 }
250 }
251
252 void
253 Visu::visualizeScan(const std::vector<ScanPoint>& points,
254 const std::string& sensorName,
255 const std::string& agentName,
256 const viz::Color& color)
257 {
258 viz::PointCloud pointCloud("laser_scan");
259
260 ARMARX_VERBOSE << "Point cloud with " << points.size() << " points";
261
262 for (const auto& point : points)
263 {
264
265 // ARMARX_INFO << point.intensity;
266 const viz::Color specificColor = [&point, &color, this]() -> viz::Color
267 {
268 if (p.colorByIntensity)
269 {
270 Eigen::Vector3f hsv = simox::color::rgb_to_hsv(
271 Eigen::Vector3f(static_cast<float>(color.r) / 255.f,
272 static_cast<float>(color.g) / 255.f,
273 static_cast<float>(color.b) / 255.f));
274
275 // ARMARX_INFO << point.intensity;
276
277 hsv(2) = std::clamp<float>(point.intensity, 0., 1.);
278
279 const Eigen::Vector3f rgb = simox::color::hsv_to_rgb(hsv);
280
281 return viz::Color{rgb(0), rgb(1), rgb(2)};
282 }
283
284 return color;
285 }();
286
287 pointCloud.addPoint(point.point.x(), point.point.y(), point.point.z(), specificColor);
288 }
289
290 pointCloud.pointSizeInPixels(p.pointSizeInPixels);
291
292 viz::Layer l = arviz.layer(agentName + "/" + sensorName);
293 l.add(pointCloud);
294
295 arviz.commit(l);
296 }
297
298 std::vector<ScanPoint>
300 const Eigen::Isometry3f& global_T_sensor)
301 {
302 const auto scanCartesian =
304
305 std::vector<ScanPoint> points;
306 points.reserve(scan.data.size());
307
308 for (std::size_t i = 0; i < scan.data.size(); i++)
309 {
310 const auto& point = scanCartesian.at(i);
311 const auto& raw = scan.data.at(i);
312
313 const Eigen::Vector3f pointGlobal = global_T_sensor * point;
314 points.push_back(ScanPoint{.point = pointGlobal, .intensity = raw.intensity});
315 }
316
317 return points;
318 }
319
320 // void Segment::getLatestObjectPoses(const wm::CoreSegment& coreSeg, ObjectPoseMap& out)
321 // {
322 // coreSeg.forEachProviderSegment([&out](const wm::ProviderSegment & provSegment)
323 // {
324 // getLatestObjectPoses(provSegment, out);
325 // });
326 // }
327
328
329 // void Segment::getLatestObjectPoses(const wm::ProviderSegment& provSegment, ObjectPoseMap& out)
330 // {
331 // provSegment.forEachEntity([&out](const wm::Entity & entity)
332 // {
333 // if (!entity.empty())
334 // {
335 // ObjectPose pose = getLatestObjectPose(entity);
336 // // Try to insert. Fails and returns false if an entry already exists.
337 // const auto [it, success] = out.insert({pose.objectID, pose});
338 // if (!success)
339 // {
340 // // An entry with that ID already exists. We keep the newest.
341 // if (it->second.timestamp < pose.timestamp)
342 // {
343 // it->second = pose;
344 // }
345 // }
346 // }
347 // });
348 // }
349
350
351 // void Segment::getLatestObjectPose(const wm::Entity& entity, ObjectPose& out)
352 // {
353 // entity.getLatestSnapshot().forEachInstance([&out](const wm::EntityInstance & instance)
354 // {
355 // arondto::ObjectInstance dto;
356 // dto.fromAron(instance.data());
357
358 // fromAron(dto, out);
359 // });
360 // }
361
362 std::map<std::string, armem::laser_scans::LaserScanStamped>
363 Visu::getCurrentLaserScans()
364 {
365 ARMARX_CHECK_NOT_NULL(coreSegment);
366
367 const auto convert = [this](const wm::EntityInstance& entityInstance)
369 {
370 const std::optional<armarx::armem::laser_scans::arondto::LaserScanStamped> dto =
372
373 ARMARX_CHECK(dto.has_value());
374
376 fromAron(dto.value(), laserScanStamped);
377
378 const auto ndArrayNavigator =
379 aron::data::NDArray::DynamicCast(entityInstance.data()->getElement("scan"));
380
381 ARMARX_CHECK_NOT_NULL(ndArrayNavigator);
382
383 laserScanStamped.data =
385
386 ARMARX_VERBOSE << "Number of steps: " << laserScanStamped.data.size();
387
388 return laserScanStamped;
389 };
390
391 std::map<std::string, armem::laser_scans::LaserScanStamped> scans;
392
393 const auto applyToInstance = [&](const wm::EntityInstance& instance)
394 {
395 const auto scan = convert(instance);
396 scans[instance.id().providerSegmentName + "/" + instance.id().entityName] = scan;
397 };
398
399 const auto applyToEntity = [&](const wm::Entity& entity)
400 {
401 if (entity.empty())
402 {
403 return;
404 }
405
406 const auto& snapshot = entity.getLatestSnapshot();
407
408 snapshot.forEachInstance(applyToInstance);
409 };
410
411 const auto applyToProviderSegment = [&](const auto& providerSegment)
412 { providerSegment.forEachEntity(applyToEntity); };
413
414 // The traversal must hold the core segment's read lock: writers commit under
415 // doLockedExclusive, and every commit can trigger Entity::truncate(), which erases
416 // snapshots from the map while `applyToEntity` holds a reference to the latest one.
417 // Everything collected here is a copy, so the lock is released before the scans are drawn.
418 coreSegment->doLocked([&] { coreSegment->forEachProviderSegment(applyToProviderSegment); });
419
420 ARMARX_VERBOSE << scans.size() << " scans";
421 return scans;
422 }
423
424 void
425 Visu::visualizeOnce(const Time& timestamp)
426 {
427 std::map<std::string, armem::laser_scans::LaserScanStamped> currentLaserScans =
428 getCurrentLaserScans();
429
430 // Register newly discovered sensor frames and schedule a GUI rebuild
431 if (guiUser_)
432 {
433 bool newSensors = false;
434 {
435 std::lock_guard<std::mutex> lock(sensorMutex_);
436 for (const auto& [provider, scan] : currentLaserScans)
437 {
438 if (knownSensorFrames_.insert(scan.header.frame).second)
439 {
440 calibrationData_[scan.header.frame] = {};
441 newSensors = true;
442 }
443 }
444 }
445 if (newSensors)
446 {
447 needsTabRebuild_ = true;
448 }
449 }
450
451 int i = 0;
452
453 for (const auto& [provider, scan] : currentLaserScans)
454 {
455 ARMARX_VERBOSE << "Visualizing `" << provider << "`";
456
457 const auto global_T_sensor = [&]() -> Eigen::Isometry3f
458 {
459 const auto robot = getSynchronizedRobot(scan.header.agent, scan.header.timestamp);
460 if (not robot)
461 {
462 ARMARX_VERBOSE << deactivateSpam(1) << "Robot `" << scan.header.agent << "`"
463 << "not available";
464 return Eigen::Isometry3f::Identity();
465 }
466
467 const auto sensorNode = robot->getRobotNode(scan.header.frame);
468 ARMARX_CHECK_NOT_NULL(sensorNode) << "No robot node `" << scan.header.frame
469 << "` for robot `" << scan.header.agent << "`";
470
471 ARMARX_VERBOSE << "Sensor position for sensor `" << scan.header.frame << "` is "
472 << sensorNode->getGlobalPosition();
473 return Eigen::Isometry3f{sensorNode->getGlobalPose()};
474 }();
475
476 const Eigen::Isometry3f global_T_sensorCalibrated =
477 global_T_sensor * getCalibrationOffset(scan.header.frame);
478
479 const std::vector<ScanPoint> points =
480 convertScanToGlobal(scan, global_T_sensorCalibrated);
481
482 const auto color = [&]() -> simox::Color
483 {
484 if (p.uniformColor)
485 {
486 return simox::Color::red();
487 }
488
489 return simox::color::GlasbeyLUT::at(i++);
490 }();
491
492 visualizeScan(points, scan.header.frame, scan.header.agent, color);
493 }
494 }
495
497 Visu::getSynchronizedRobot(const std::string& name, const DateTime& timestamp)
498 {
499 if (robots.count(name) == 0)
500 {
501 ARMARX_CHECK_NOT_NULL(virtualRobotReader);
502 const auto robot = virtualRobotReader->getRobot(name);
503
504 if (robot)
505 {
506 robots[name] = {robot, DateTime::Invalid()};
507 }
508 else
509 {
510 return nullptr;
511 }
512 }
513
514 auto& entry = robots.at(name);
515 if (entry.second.isInvalid() ||
516 (timestamp - entry.second) > Duration::MilliSeconds(p.maxRobotAgeMs))
517 {
518 if (virtualRobotReader->synchronizeRobotPose(*entry.first, timestamp))
519 {
520 entry.second = timestamp;
521 }
522 else
523 {
524 ARMARX_INFO << deactivateSpam(10) << "Failed to synchronize robot `" << name << "`";
525 }
526 }
527 return entry.first;
528 }
529
530} // namespace armarx::armem::server::laser_scans
int Label(int n[], int size, int *curLabel, MiscLib::Vector< std::pair< int, size_t > > *labels)
Definition Bitmap.cpp:801
std::string timestamp()
#define M_PI
Definition MathTools.h:17
static DateTime Invalid()
Definition DateTime.cpp:57
Brief description of class DebugObserverHelper.
SpamFilterDataPtr deactivateSpam(float deactivationDurationSec=10.0f, const std::string &identifier="", bool deactivate=true) const
disables the logging for the current line for the given amount of seconds.
Definition Logging.cpp:99
void setTag(const LogTag &tag)
Definition Logging.cpp:54
void defineProperties(armarx::PropertyDefinitionsPtr defs, const std::string &prefix="visu.")
Definition Visu.cpp:46
void connect(const viz::Client &arviz, DebugObserverInterfacePrx debugObserver=nullptr)
Definition Visu.cpp:69
void disconnect()
Stop and join the visualization task. Safe to call more than once.
Definition Visu.cpp:94
void init(const wm::CoreSegment *coreSegment, armem::robot_state::VirtualRobotReader *virtualRobotReader)
Definition Visu.cpp:61
void connectRemoteGui(armarx::LightweightRemoteGuiComponentPluginUser *guiUser, const std::string &tabName)
Definition Visu.cpp:108
static DateTime Now()
Definition DateTime.cpp:51
static Duration MilliSeconds(std::int64_t milliSeconds)
Constructs a duration in milliseconds.
Definition Duration.cpp:48
#define ARMARX_CHECK(expression)
Shortcut for ARMARX_CHECK_EXPRESSION.
#define ARMARX_CHECK_NOT_NULL(ptr)
This macro evaluates whether ptr is not null and if it turns out to be false it will throw an Express...
#define ARMARX_INFO
The normal logging level.
Definition Logging.h:179
#define ARMARX_DEBUG
The logging level for output that is only interesting while debugging.
Definition Logging.h:182
#define ARMARX_WARNING
The logging level for unexpected behaviour, but not a serious problem.
Definition Logging.h:191
#define ARMARX_VERBOSE
The logging level for verbose information.
Definition Logging.h:185
std::shared_ptr< class Robot > RobotPtr
Definition Bus.h:19
EigenVectorT toCartesian(const LaserScanStep &laserScanStep)
SensorHeader fromAron(const arondto::SensorHeader &aronSensorHeader)
This file is part of ArmarX.
std::vector< ScanPoint > convertScanToGlobal(const armem::laser_scans::LaserScanStamped &scan, const Eigen::Isometry3f &global_T_sensor)
Definition Visu.cpp:299
armem::wm::EntityInstance EntityInstance
void fromAron(const arondto::MemoryID &dto, MemoryID &bo)
armarx::core::time::DateTime Time
std::string toStringMilliSeconds(const Time &time, int decimals=3)
Returns time as e.g.
Definition Time.cpp:11
std::optional< AronClass > tryCast(const wm::EntityInstance &item)
Tries to cast a armem::EntityInstance to AronClass.
Definition util.h:45
::IceInternal::ProxyHandle<::IceProxy::armarx::DebugObserverInterface > DebugObserverInterfacePrx
armem::articulated_object::ArticulatedObject convert(const VirtualRobot::Robot &obj, const armem::Time &timestamp)
IceUtil::Handle< class PropertyDefinitionContainer > PropertyDefinitionsPtr
PropertyDefinitions smart pointer type.
SimpleRunningTask(Ts...) -> SimpleRunningTask< std::function< void(void)> >
GridLayout & add(Widget const &child, Pos pos, Span span=Span{1, 1})
Definition Widgets.cpp:438
void add(ElementT const &element)
Definition Layer.h:31