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
88 void
90 const std::string& tabName)
91 {
92 guiUser_ = guiUser;
93 remoteGuiTabName_ = tabName;
94 }
95
96 void
98 {
99 if (needsTabRebuild_.exchange(false))
100 {
101 createOrUpdateCalibrationTab();
102 }
103
104 // Sync widget values back to calibrationData_ whenever any value changed
105 bool anyChanged = false;
106 for (const auto& [frame, spinboxes] : calibrationTab_.sensors)
107 {
108 for (const auto& sb : spinboxes)
109 {
110 anyChanged = anyChanged || sb.hasValueChanged();
111 }
112 }
113
114 if (anyChanged)
115 {
116 std::lock_guard<std::mutex> lock(sensorMutex_);
117 for (const auto& [frame, spinboxes] : calibrationTab_.sensors)
118 {
119 calibrationData_[frame] = {
120 spinboxes[0].getValue(), spinboxes[1].getValue(), spinboxes[2].getValue()};
121 }
122 }
123 }
124
125 void
126 Visu::createOrUpdateCalibrationTab()
127 {
128 using namespace armarx::RemoteGui::Client;
129
130 // Snapshot current frames and their calibration data under lock
131 std::set<std::string> frames;
132 std::map<std::string, SensorCalibrationData> currentData;
133 {
134 std::lock_guard<std::mutex> lock(sensorMutex_);
135 frames = knownSensorFrames_;
136 currentData = calibrationData_;
137 }
138
139 // Rebuild spinboxes from scratch, pre-populated with current values
140 calibrationTab_.sensors.clear();
141
142 GridLayout grid;
143 int row = 0;
144
145 grid.add(Label("Sensor"), {.row = row, .column = 0})
146 .add(Label("x [mm]"), {.row = row, .column = 1})
147 .add(Label("y [mm]"), {.row = row, .column = 2})
148 .add(Label("yaw [deg]"), {.row = row, .column = 3});
149 ++row;
150
151 for (const auto& frame : frames)
152 {
153 const auto& d = currentData[frame];
154 auto& sb = calibrationTab_.sensors[frame];
155
156 sb[0].setRange(-10.f, 10.f);
157 sb[0].setDecimals(1);
158 sb[0].setValue(d.x);
159
160 sb[1].setRange(-10.f, 10.f);
161 sb[1].setDecimals(1);
162 sb[1].setValue(d.y);
163
164 sb[2].setRange(-5.f, 5.f);
165 sb[2].setDecimals(2);
166 sb[2].setValue(d.yaw);
167
168 grid.add(Label(frame), {.row = row, .column = 0})
169 .add(sb[0], {.row = row, .column = 1})
170 .add(sb[1], {.row = row, .column = 2})
171 .add(sb[2], {.row = row, .column = 3});
172 ++row;
173 }
174
175 VBoxLayout root = {grid, VSpacer()};
176 guiUser_->RemoteGui_createTab(remoteGuiTabName_, root, &calibrationTab_);
177 }
178
179 Eigen::Isometry3f
180 Visu::getCalibrationOffset(const std::string& sensorFrame) const
181 {
182 std::lock_guard<std::mutex> lock(sensorMutex_);
183 const auto it = calibrationData_.find(sensorFrame);
184 if (it == calibrationData_.end())
185 {
186 return Eigen::Isometry3f::Identity();
187 }
188
189 const auto& d = it->second;
190 constexpr float kDegToRad = static_cast<float>(M_PI) / 180.f;
191
192 Eigen::Isometry3f offset = Eigen::Isometry3f::Identity();
193 offset.translation().x() = d.x;
194 offset.translation().y() = d.y;
195 offset.linear() =
196 Eigen::AngleAxisf(d.yaw * kDegToRad, Eigen::Vector3f::UnitZ()).toRotationMatrix();
197 return offset;
198 }
199
200 void
201 Visu::visualizeRun()
202 {
203 CycleUtil cycle(static_cast<int>(1000 / p.frequencyHz));
204 while (updateTask and not updateTask->isStopped())
205 {
206 if (p.enabled)
207 {
208 const Time timestamp = Time::Now();
210
211 try
212 {
213 visualizeOnce(timestamp);
214 }
215 catch (const std::exception& e)
216 {
217 ARMARX_WARNING << "Caught exception while visualizing robots: \n" << e.what();
218 }
219 catch (...)
220 {
221 ARMARX_WARNING << "Caught unknown exception while visualizing robots.";
222 }
223
224 if (debugObserver.has_value())
225 {
226 debugObserver->sendDebugObserverBatch();
227 }
228 }
229 cycle.waitForCycleDuration();
230 }
231 }
232
233 void
234 Visu::visualizeScan(const std::vector<ScanPoint>& points,
235 const std::string& sensorName,
236 const std::string& agentName,
237 const viz::Color& color)
238 {
239 viz::PointCloud pointCloud("laser_scan");
240
241 ARMARX_VERBOSE << "Point cloud with " << points.size() << " points";
242
243 for (const auto& point : points)
244 {
245
246 // ARMARX_INFO << point.intensity;
247 const viz::Color specificColor = [&point, &color, this]() -> viz::Color
248 {
249 if (p.colorByIntensity)
250 {
251 Eigen::Vector3f hsv = simox::color::rgb_to_hsv(
252 Eigen::Vector3f(static_cast<float>(color.r) / 255.f,
253 static_cast<float>(color.g) / 255.f,
254 static_cast<float>(color.b) / 255.f));
255
256 // ARMARX_INFO << point.intensity;
257
258 hsv(2) = std::clamp<float>(point.intensity, 0., 1.);
259
260 const Eigen::Vector3f rgb = simox::color::hsv_to_rgb(hsv);
261
262 return viz::Color{rgb(0), rgb(1), rgb(2)};
263 }
264
265 return color;
266 }();
267
268 pointCloud.addPoint(point.point.x(), point.point.y(), point.point.z(), specificColor);
269 }
270
271 pointCloud.pointSizeInPixels(p.pointSizeInPixels);
272
273 viz::Layer l = arviz.layer(agentName + "/" + sensorName);
274 l.add(pointCloud);
275
276 arviz.commit(l);
277 }
278
279 std::vector<ScanPoint>
281 const Eigen::Isometry3f& global_T_sensor)
282 {
283 const auto scanCartesian =
285
286 std::vector<ScanPoint> points;
287 points.reserve(scan.data.size());
288
289 for (std::size_t i = 0; i < scan.data.size(); i++)
290 {
291 const auto& point = scanCartesian.at(i);
292 const auto& raw = scan.data.at(i);
293
294 const Eigen::Vector3f pointGlobal = global_T_sensor * point;
295 points.push_back(ScanPoint{.point = pointGlobal, .intensity = raw.intensity});
296 }
297
298 return points;
299 }
300
301 // void Segment::getLatestObjectPoses(const wm::CoreSegment& coreSeg, ObjectPoseMap& out)
302 // {
303 // coreSeg.forEachProviderSegment([&out](const wm::ProviderSegment & provSegment)
304 // {
305 // getLatestObjectPoses(provSegment, out);
306 // });
307 // }
308
309
310 // void Segment::getLatestObjectPoses(const wm::ProviderSegment& provSegment, ObjectPoseMap& out)
311 // {
312 // provSegment.forEachEntity([&out](const wm::Entity & entity)
313 // {
314 // if (!entity.empty())
315 // {
316 // ObjectPose pose = getLatestObjectPose(entity);
317 // // Try to insert. Fails and returns false if an entry already exists.
318 // const auto [it, success] = out.insert({pose.objectID, pose});
319 // if (!success)
320 // {
321 // // An entry with that ID already exists. We keep the newest.
322 // if (it->second.timestamp < pose.timestamp)
323 // {
324 // it->second = pose;
325 // }
326 // }
327 // }
328 // });
329 // }
330
331
332 // void Segment::getLatestObjectPose(const wm::Entity& entity, ObjectPose& out)
333 // {
334 // entity.getLatestSnapshot().forEachInstance([&out](const wm::EntityInstance & instance)
335 // {
336 // arondto::ObjectInstance dto;
337 // dto.fromAron(instance.data());
338
339 // fromAron(dto, out);
340 // });
341 // }
342
343 std::map<std::string, armem::laser_scans::LaserScanStamped>
344 Visu::getCurrentLaserScans()
345 {
346 ARMARX_CHECK_NOT_NULL(coreSegment);
347
348 const auto convert = [this](const wm::EntityInstance& entityInstance)
350 {
351 const std::optional<armarx::armem::laser_scans::arondto::LaserScanStamped> dto =
353
354 ARMARX_CHECK(dto.has_value());
355
357 fromAron(dto.value(), laserScanStamped);
358
359 const auto ndArrayNavigator =
360 aron::data::NDArray::DynamicCast(entityInstance.data()->getElement("scan"));
361
362 ARMARX_CHECK_NOT_NULL(ndArrayNavigator);
363
364 laserScanStamped.data =
366
367 ARMARX_VERBOSE << "Number of steps: " << laserScanStamped.data.size();
368
369 return laserScanStamped;
370 };
371
372 std::map<std::string, armem::laser_scans::LaserScanStamped> scans;
373
374 const auto applyToInstance = [&](const wm::EntityInstance& instance)
375 {
376 const auto scan = convert(instance);
377 scans[instance.id().providerSegmentName + "/" + instance.id().entityName] = scan;
378 };
379
380 const auto applyToEntity = [&](const wm::Entity& entity)
381 {
382 if (entity.empty())
383 {
384 return;
385 }
386
387 const auto& snapshot = entity.getLatestSnapshot();
388
389 snapshot.forEachInstance(applyToInstance);
390 };
391
392 const auto applyToProviderSegment = [&](const auto& providerSegment)
393 { providerSegment.forEachEntity(applyToEntity); };
394
395 coreSegment->forEachProviderSegment(applyToProviderSegment);
396
397 ARMARX_VERBOSE << scans.size() << " scans";
398 return scans;
399 }
400
401 void
402 Visu::visualizeOnce(const Time& timestamp)
403 {
404 std::map<std::string, armem::laser_scans::LaserScanStamped> currentLaserScans =
405 getCurrentLaserScans();
406
407 // Register newly discovered sensor frames and schedule a GUI rebuild
408 if (guiUser_)
409 {
410 bool newSensors = false;
411 {
412 std::lock_guard<std::mutex> lock(sensorMutex_);
413 for (const auto& [provider, scan] : currentLaserScans)
414 {
415 if (knownSensorFrames_.insert(scan.header.frame).second)
416 {
417 calibrationData_[scan.header.frame] = {};
418 newSensors = true;
419 }
420 }
421 }
422 if (newSensors)
423 {
424 needsTabRebuild_ = true;
425 }
426 }
427
428 int i = 0;
429
430 for (const auto& [provider, scan] : currentLaserScans)
431 {
432 ARMARX_VERBOSE << "Visualizing `" << provider << "`";
433
434 const auto global_T_sensor = [&]() -> Eigen::Isometry3f
435 {
436 const auto robot = getSynchronizedRobot(scan.header.agent, scan.header.timestamp);
437 if (not robot)
438 {
439 ARMARX_VERBOSE << deactivateSpam(1) << "Robot `" << scan.header.agent << "`"
440 << "not available";
441 return Eigen::Isometry3f::Identity();
442 }
443
444 const auto sensorNode = robot->getRobotNode(scan.header.frame);
445 ARMARX_CHECK_NOT_NULL(sensorNode) << "No robot node `" << scan.header.frame
446 << "` for robot `" << scan.header.agent << "`";
447
448 ARMARX_VERBOSE << "Sensor position for sensor `" << scan.header.frame << "` is "
449 << sensorNode->getGlobalPosition();
450 return Eigen::Isometry3f{sensorNode->getGlobalPose()};
451 }();
452
453 const Eigen::Isometry3f global_T_sensorCalibrated =
454 global_T_sensor * getCalibrationOffset(scan.header.frame);
455
456 const std::vector<ScanPoint> points =
457 convertScanToGlobal(scan, global_T_sensorCalibrated);
458
459 const auto color = [&]() -> simox::Color
460 {
461 if (p.uniformColor)
462 {
463 return simox::Color::red();
464 }
465
466 return simox::color::GlasbeyLUT::at(i++);
467 }();
468
469 visualizeScan(points, scan.header.frame, scan.header.agent, color);
470 }
471 }
472
474 Visu::getSynchronizedRobot(const std::string& name, const DateTime& timestamp)
475 {
476 if (robots.count(name) == 0)
477 {
478 ARMARX_CHECK_NOT_NULL(virtualRobotReader);
479 const auto robot = virtualRobotReader->getRobot(name);
480
481 if (robot)
482 {
483 robots[name] = {robot, DateTime::Invalid()};
484 }
485 else
486 {
487 return nullptr;
488 }
489 }
490
491 auto& entry = robots.at(name);
492 if (entry.second.isInvalid() ||
493 (timestamp - entry.second) > Duration::MilliSeconds(p.maxRobotAgeMs))
494 {
495 if (virtualRobotReader->synchronizeRobotPose(*entry.first, timestamp))
496 {
497 entry.second = timestamp;
498 }
499 else
500 {
501 ARMARX_INFO << deactivateSpam(10) << "Failed to synchronize robot `" << name << "`";
502 }
503 }
504 return entry.first;
505 }
506
507} // 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 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:89
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:181
#define ARMARX_DEBUG
The logging level for output that is only interesting while debugging.
Definition Logging.h:184
#define ARMARX_WARNING
The logging level for unexpected behaviour, but not a serious problem.
Definition Logging.h:193
#define ARMARX_VERBOSE
The logging level for verbose information.
Definition Logging.h:187
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:280
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