NuitrackPointCloudProvider.cpp
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cmath>
5#include <cstdio>
6#include <numbers>
7#include <optional>
8#include <string>
9#include <vector>
10#include <filesystem>
11#include <functional>
12#include <mutex>
13#include <utility>
14
15#include <Ice/LocalException.h>
16#include <IceUtil/Time.h>
17
18#include <opencv2/core/hal/interface.h>
19#include <opencv2/imgproc/imgproc.hpp>
20
30
32
39
40#include "nuitrack/types/NuitrackDeviceCommon.h"
41#include <Calibration/Calibration.h>
42#include <Image/ImageProcessor.h>
43
44namespace
45{
46 // Mapping from Nuitrack JointType to k4abt joint index (matching k4a_bt_body_32 model)
47 std::optional<int>
48 getNuitrackToK4ABTMapping(tdv::nuitrack::JointType nuitrackJoint)
49 {
50 using namespace tdv::nuitrack;
52
53 static const std::map<JointType, K4AJoint> mapping = {
54 {JOINT_HEAD, K4AJoint::Head},
55 {JOINT_NECK, K4AJoint::Neck},
56 // {JOINT_TORSO, K4AJoint::SpineChest},
57 {JOINT_TORSO, K4AJoint::SpineNaval},
58 {JOINT_WAIST, K4AJoint::Pelvis},
59
60 {JOINT_LEFT_COLLAR, K4AJoint::ClavicleLeft},
61 {JOINT_LEFT_SHOULDER, K4AJoint::ShoulderLeft},
62 {JOINT_LEFT_ELBOW, K4AJoint::ElbowLeft},
63 {JOINT_LEFT_WRIST, K4AJoint::WristLeft},
64 {JOINT_LEFT_HAND, K4AJoint::HandLeft},
65 // {JOINT_LEFT_FINGERTIP, K4AJoint::HandtipLeft},
66
67 {JOINT_RIGHT_COLLAR, K4AJoint::ClavicleRight},
68 {JOINT_RIGHT_SHOULDER, K4AJoint::ShoulderRight},
69 {JOINT_RIGHT_ELBOW, K4AJoint::ElbowRight},
70 {JOINT_RIGHT_WRIST, K4AJoint::WristRight},
71 {JOINT_RIGHT_HAND, K4AJoint::HandRight},
72 // {JOINT_RIGHT_FINGERTIP, K4AJoint::HandtipRight},
73
74 {JOINT_LEFT_HIP, K4AJoint::HipLeft},
75 {JOINT_LEFT_KNEE, K4AJoint::KneeLeft},
76 {JOINT_LEFT_ANKLE, K4AJoint::AnkleLeft},
77 // {JOINT_LEFT_FOOT, K4AJoint::FootLeft},
78
79 {JOINT_RIGHT_HIP, K4AJoint::HipRight},
80 {JOINT_RIGHT_KNEE, K4AJoint::KneeRight},
81 {JOINT_RIGHT_ANKLE, K4AJoint::AnkleRight},
82 // {JOINT_RIGHT_FOOT, K4AJoint::FootRight},
83
84 };
85
86 auto it = mapping.find(nuitrackJoint);
87 if (it != mapping.end())
88 {
89 return static_cast<int>(it->second);
90 }
91 return std::nullopt;
92 }
93
94 void
95 printBodyInformation(const tdv::nuitrack::Skeleton& skeleton)
96 {
97 ARMARX_VERBOSE << "Skeleton ID: " << skeleton.id;
98 for (const auto& joint : skeleton.joints)
99 {
100 ARMARX_VERBOSE << "Joint " << joint.type << ": "
101 << "Position[mm] (" << joint.real.x << "," << joint.real.y << ","
102 << joint.real.z << "); "
103 << "Confidence " << joint.confidence;
104 }
105 }
106
107 // Minimum spine vector length (mm) required to accept a joint pair.
108 constexpr float minSpineLengthMm = 100.0F;
109
110} // namespace
111
112namespace visionx
113{
114 std::optional<Eigen::Vector3f>
116 {
117 // Ordered list of (bottom, top) keypoint pairs for estimating the body's
118 // upward direction. Arm joints are intentionally excluded. The first pair
119 // for which both keypoints are present and sufficiently far apart wins.
120 static const std::vector<std::pair<std::string, std::string>> pairs = {
121 {"Pelvis", "Head"},
122 {"Pelvis", "Neck"},
123 {"Pelvis", "SpineNaval"},
124 // {"SpineNaval", "Head"},
125 // {"SpineNaval", "Neck"},
126 {"HipLeft", "Neck"},
127 {"HipRight", "Neck"},
128 };
129
130 auto success = robotReader.synchronizeRobot(*robot, armarx::Clock::Now());
131
132 for (const auto& [bottomName, topName] : pairs)
133 {
134 auto bottomIt = pose.keypoints.find(bottomName);
135 auto topIt = pose.keypoints.find(topName);
136 if (bottomIt == pose.keypoints.end() || topIt == pose.keypoints.end())
137 {
138 continue;
139 }
140 Eigen::Vector3f vec = topIt->second.positionCamera.toEigen()
141 - bottomIt->second.positionCamera.toEigen();
142
143 armarx::FramedPosition vecCamera(vec, bodyCameraFrameName, "Armar7");
144 vec = vecCamera.toGlobalEigen(robot);
145 const float norm = vec.norm();
146 if (norm < minSpineLengthMm)
147 {
148 continue;
149 }
150 return vec / norm;
151 }
152 return std::nullopt;
153 }
154
155 void
156 NuitrackPointCloudProvider::filterPosesByUpright(std::vector<armarx::armem::human::HumanPose>& poses,
157 const Eigen::Vector3f& worldUp,
158 float maxAngleDeg)
159 {
160 // Convert to radians and check against pi; cos(pi) = -1 means no pose
161 // would ever be filtered, so skip the work entirely.
162 const float maxAngleRad = maxAngleDeg * std::numbers::pi_v<float> / 180.F;
163 if (maxAngleRad >= std::numbers::pi_v<float>)
164 {
165 return;
166 }
167
168 const float cosThreshold = std::cos(maxAngleRad);
169 const float radToDeg = 180.F / std::numbers::pi_v<float>;
170
171 std::erase_if(poses,
172 [&](const armarx::armem::human::HumanPose& pose) -> bool
173 {
174 const auto bodyUp = computeBodyUpVector(pose);
175 if (!bodyUp.has_value())
176 {
177 ARMARX_INFO << "Dropping pose (id "
178 << pose.humanTrackingId.value_or("?")
179 << "): Could not compute valid body pose up vector.";
180 return false; // keep if orientation cannot be determined
181 }
182 const float cosAngle = bodyUp->dot(worldUp);
183 if (cosAngle < cosThreshold)
184 {
185 const float angleDeg =
186 std::acos(std::clamp(cosAngle, -1.F, 1.F)) * radToDeg;
187 ARMARX_INFO << "Dropping pose (id "
188 << pose.humanTrackingId.value_or("?")
189 << "): body-up angle " << angleDeg << "° > max "
190 << maxAngleDeg << "°";
191 return true;
192 }
193 return false;
194 });
195 }
196
197 std::function<void(armarx::Duration)>
198 NuitrackPointCloudProvider::createSwCallback(const std::string& description)
199 {
200 return [description, this](armarx::Duration duration)
201 {
202 std::string name = "duration " + description + " in [ms]";
203 {
204 std::lock_guard g{metaInfoMtx};
205 setMetaInfo(name, new armarx::Variant{duration.toMilliSecondsDouble()});
206 }
207
208 {
209 std::lock_guard g{debugObserverMtx};
210 setDebugObserverDatafield(name, duration.toMilliSecondsDouble());
211 }
212 };
213 }
214
216 std::string prefix) :
218 {
219 defineOptionalProperty<int>("ColorWidth", 640, "Width of the RGB camera image.");
220
221 defineOptionalProperty<int>("ColorHeight", 480, "Height of the RGB camera image.");
222
223 defineOptionalProperty<int>("DepthWidth", 640, "Width of the depth camera image.");
224
225 defineOptionalProperty<int>("DepthHeight", 576, "Height of the depth camera image.");
226
228 6000.0,
229 "Max. allowed depth value in mm. Depth values above this "
230 "threshold will be set to nan.");
231 }
232
235 {
238
239 defs->optional(robotName, "robotName");
240 defs->optional(bodyCameraFrameName, "bodyCameraFrameName");
241 defs->optional(framerate.value,
242 "framerate.images",
243 "The framerate of RGB-D images [frames per second]."
244 "\nNote that the point cloud and body tracking frame rates are controlled by"
245 " the properties 'framerate' (point cloud) and 'framerate.bodyTracking'"
246 " (body tracking), respectively.")
247 .setMin(5)
248 .setMax(30);
249
250 defs->optional(bodyTrackingEnabled,
251 "bodyTrackingEnabled",
252 "Whether the body tracking should be enabled or not.");
253 defs->optional(
254 bodyTrackingRunAtStart,
255 "bodyTrackingRunAtStart",
256 "Whether the body tracking should directly run when the component is startet."
257 "Otherwise it has to be activated by the ice interface.");
258 defs->optional(bodyTrackingDepthMaskMinX, "bodyTrackingDepthMaskMinX");
259 defs->optional(bodyTrackingDepthMaskMaxX, "bodyTrackingDepthMaskMaxX");
260 defs->optional(bodyTrackingDepthMaskMaxZ, "bodyTrackingDepthMaskMaxZ");
261 defs->optional(framerate.bodyTracking.value,
262 "framerate.bodyTracking",
263 "The framerate with with the body tracking is run [frames per second]."
264 "\nNote that the RGB-D image and point cloud frame rates are controlled by"
265 " the properties 'framerate.image' (RGB-D images) and 'framerate'"
266 " (point cloud), respectively.")
267 .setMin(1.)
268 .setMax(30.);
269
270 defs->optional(confidenceThreshold, "confidenceThreshold");
271 defs->optional(minimumSegmentsWithConfidenceLargerThanThreshold,
272 "minimumSegmentsWithConfidenceLargerThanThreshold");
273 defs->optional(uprightFilterMaxAngleDeg,
274 "uprightFilterMaxAngleDeg",
275 "Maximum angle in degrees between the estimated body up vector "
276 "(spine direction, arm joints excluded) and the world up vector (0, 0, 1). "
277 "Poses exceeding this threshold are dropped. Set >= 180 to disable.")
278 .setMin(0.)
279 .setMax(180.);
280
281 humanPoseWriter.registerPropertyDefinitions(defs);
282
283 defs->optional(enableHeartbeat, "enableHeartbeat");
284
285 return defs;
286 }
287
288 void
290 {
291 // Validate framerate
292 if (framerate.value != 5 && framerate.value != 15 && framerate.value != 30)
293 {
294 throw armarx::LocalException("Invalid image framerate (property 'framerate.images'): ")
295 << framerate.value << ". Only framerates 5, 15 and 30 are supported.";
296 }
297
298 framerate.pointCloud.value =
299 getProperty<float>("framerate"); // point cloud provider framerate
300 framerate.pointCloud.update(framerate.value);
301
302 framerate.bodyTracking.update(framerate.value);
303
304 // Get image dimensions from properties
305 int colorWidth = getProperty<int>("ColorWidth").getValue();
306 int colorHeight = getProperty<int>("ColorHeight").getValue();
307 int depthWidth = getProperty<int>("DepthWidth").getValue();
308 int depthHeight = getProperty<int>("DepthHeight").getValue();
309
310 ARMARX_INFO << "Depth image size: " << depthWidth << "x" << depthHeight;
311 ARMARX_INFO << "Color image size: " << colorWidth << "x" << colorHeight;
312
313 // Set number of images per frame.
315
316 // Initialize OpenCV matrices for image storage
317 alignedDepthImage = cv::Mat(colorHeight, colorWidth, CV_16UC1);
318 colorImageRGB = cv::Mat(colorHeight, colorWidth, CV_8UC3);
319
320 setImageFormat(visionx::ImageDimension(colorWidth, colorHeight),
321 visionx::eRgb,
322 visionx::eBayerPatternGr);
323
324 resultDepthImage.reset(visionx::tools::createByteImage(getImageFormat(), visionx::eRgb));
325 resultColorImage =
326 std::make_unique<CByteImage>(colorWidth, colorHeight, CByteImage::eRGB24);
327
328 pointcloud = std::make_unique<pcl::PointCloud<CloudPointType>>();
329 }
330
331 void
333 {
334 ARMARX_DEBUG << "Connecting " << getName();
335
337
341 pointcloudTask->start();
342
343 // TODO: Remove hardcoding
344 bodyTrackingEnabled = true;
345 bodyTrackingRunAtStart = true;
346 robotName = "Armar7";
347
348
349 if (bodyTrackingEnabled)
350 {
351 ARMARX_INFO << "Connecting to human memory ...";
352 humanPoseWriter.connect(memoryNameSystem());
353 ARMARX_INFO << "Connected to human memory.";
354
355 bodyTrackingPublishTask = new armarx::RunningTask<NuitrackPointCloudProvider>(
356 this, &NuitrackPointCloudProvider::runPublishBodyTrackingResults);
357 }
358
359 if (enableHeartbeat)
360 {
361 ARMARX_CHECK_NOT_NULL(heartbeatPlugin);
362 heartbeatPlugin->signUp(armarx::Duration::MilliSeconds(500),
364 {"Vision", "Camera"},
365 "NuitrackPointCloudProvider");
366 }
367
368 ARMARX_VERBOSE << "Connected " << getName();
369 }
370
371 void
373 {
374 ARMARX_DEBUG << "Disconnecting " << getName();
375
376 // Stop task
377 {
378 std::lock_guard<std::mutex> lock(pointcloudProcMutex);
379 ARMARX_DEBUG << "Stopping pointcloud processing thread...";
380 const bool WAIT_FOR_JOIN = false;
381 pointcloudTask->stop(WAIT_FOR_JOIN);
382 pointcloudProcSignal.notify_all();
383 ARMARX_DEBUG << "Waiting for pointcloud processing thread to stop...";
384 pointcloudTask->waitForStop();
385 ARMARX_DEBUG << "Pointcloud processing thread stopped";
386 }
387
388 if (bodyTrackingIsRunning)
389 {
390 bodyTrackingIsRunning = false;
391 bodyTrackingPublishTask->stop();
392 }
393
394 ARMARX_DEBUG << "Disconnected " << getName();
395 }
396
397 void
403
404 void
408
409 void
411 {
413
414 cloudFormat = getPointCloudFormat();
415
416 try
417 {
418 ARMARX_INFO << "Initializing Nuitrack...";
419
420 // Initialize Nuitrack
421 tdv::nuitrack::Nuitrack::init();
422
423 ARMARX_INFO << "Init done";
424 ARMARX_INFO << "Querying devices...";
425
426 // Get the list of devices
427 auto devices = tdv::nuitrack::Nuitrack::getDeviceList();
428
429 if (devices.empty())
430 {
431 ARMARX_INFO << "No devices found!";
432 throw armarx::LocalException("No Nuitrack-compatible devices detected!");
433 }
434
435 ARMARX_INFO << "Found " << devices.size() << " Nuitrack-compatible devices";
436
437 // TODO: Handle case where we have multiple devices
438 // We currently just use the first device
439
440 // Set VideoMode for color
441 // TODO: Does not seem to take effect
442 // Maybe: https://community.nuitrack.com/t/azurekinect-dk-nuitrack-config-isnt-applying/3049/4
443 tdv::nuitrack::device::VideoMode colorMode{.width = resultColorImage->width,
444 .height = resultColorImage->height,
445 .fps = framerate.value};
446 devices[0]->setVideoMode(tdv::nuitrack::device::StreamType::COLOR, colorMode);
447
448
449 // Set VideoMode for depth
450 // TODO: Does not seem to take effect
451 tdv::nuitrack::device::VideoMode depthMode{.width = resultDepthImage->width,
452 .height = resultDepthImage->height,
453 .fps = framerate.value};
454 devices[0]->setVideoMode(tdv::nuitrack::device::StreamType::DEPTH, depthMode);
455
456 tdv::nuitrack::Nuitrack::setDevice(devices[0]);
457
458 ARMARX_INFO << "Using device: "
459 << devices[0]->getInfo(tdv::nuitrack::device::DeviceInfoType::DEVICE_NAME);
460
461 // Create depth and color sensors
462 depthSensor = tdv::nuitrack::DepthSensor::create();
463 colorSensor = tdv::nuitrack::ColorSensor::create();
464
465 // Connect frame callbacks
466 depthSensor->connectOnNewFrame([this](auto&& PH1)
467 { onNewDepthFrame(std::forward<decltype(PH1)>(PH1)); });
468 colorSensor->connectOnNewFrame([this](auto&& PH1)
469 { onNewColorFrame(std::forward<decltype(PH1)>(PH1)); });
470
471 if (bodyTrackingEnabled)
472 {
473 ARMARX_INFO << "Initializing Nuitrack skeleton tracking...";
474 skeletonTracker = tdv::nuitrack::SkeletonTracker::create();
475 skeletonTracker->connectOnUpdate(
476 [this](auto&& PH1) { onNewSkeletonData(std::forward<decltype(PH1)>(PH1)); });
477
478 if (bodyTrackingRunAtStart and not bodyTrackingIsRunning)
479 {
480 bodyTrackingIsRunning = true;
481 bodyTrackingPublishTask->start();
482 }
483 }
484
485 // Start Nuitrack
486 tdv::nuitrack::Nuitrack::run();
487
488 ARMARX_INFO << "Nuitrack initialized and running";
489 }
490 catch (const tdv::nuitrack::Exception& e)
491 {
492 ARMARX_ERROR << "Failed to initialize Nuitrack: " << e.what();
493 getArmarXManager()->asyncShutdown();
494 throw armarx::LocalException("Nuitrack initialization failed: ") << e.what();
495 }
496 }
497
498 void
500 {
501 ARMARX_INFO << "Releasing Nuitrack";
502 try
503 {
504 tdv::nuitrack::Nuitrack::release();
505 }
506 catch (const tdv::nuitrack::Exception& e)
507 {
508 ARMARX_WARNING << "Error releasing Nuitrack: " << e.what();
509 }
510 ARMARX_INFO << "Released Nuitrack";
511 }
512
513 void
514 NuitrackPointCloudProvider::onNewDepthFrame(tdv::nuitrack::DepthFrame::Ptr frame)
515 {
516 if (!frame)
517 {
518 return;
519 }
520
521 std::lock_guard<std::mutex> lock(depthFrameMutex);
522 latestDepthFrame = frame;
523 }
524
525 void
526 NuitrackPointCloudProvider::onNewColorFrame(tdv::nuitrack::RGBFrame::Ptr frame)
527 {
528 if (!frame)
529 {
530 return;
531 }
532 // We don't get a proper timestap from Nuitrack and just take the RGB receive time
533 latestTimestamp = armarx::Clock::Now().toMicroSecondsSinceEpoch();
534 std::lock_guard<std::mutex> lock(colorFrameMutex);
535 latestColorFrame = frame;
536 }
537
538 void
539 NuitrackPointCloudProvider::onNewSkeletonData(tdv::nuitrack::SkeletonData::Ptr skeletonData)
540 {
541 if (!skeletonData)
542 {
543 return;
544 }
545 std::lock_guard<std::mutex> lock(skeletonMutex);
546 latestSkeletonData = skeletonData;
547 }
548
549 bool
551 {
554
555 ScopedStopWatch sw_total{createSwCallback("doCapture")};
556
557 try
558 {
559 tdv::nuitrack::Nuitrack::waitUpdate(depthSensor);
560 }
561 catch (const tdv::nuitrack::Exception& e)
562 {
563 ARMARX_WARNING << "Failed to get frame from Nuitrack: " << e.what();
564 return false;
565 }
566
567 tdv::nuitrack::DepthFrame::Ptr depthFrame;
568 tdv::nuitrack::RGBFrame::Ptr colorFrame;
569
570 {
571 std::lock_guard<std::mutex> lock(colorFrameMutex);
572 std::lock_guard<std::mutex> lock_(depthFrameMutex);
573 depthFrame = latestDepthFrame;
574 colorFrame = latestColorFrame;
575 }
576
577 if (depthFrame && colorFrame)
578 {
579 ARMARX_DEBUG << "Processing Nuitrack frames";
580
581 imagesTime = IceUtil::Time::microSeconds(latestTimestamp);
582
583 // Convert Nuitrack RGB frame to IVT image
584 {
585 ScopedStopWatch sw{createSwCallback("convert Nuitrack image to IVT")};
586
587 const int width = colorFrame->getCols();
588 const int height = colorFrame->getRows();
589 const tdv::nuitrack::Color3* colorData = colorFrame->getData();
590
591 ARMARX_CHECK_EQUAL(resultColorImage->width, width);
592 ARMARX_CHECK_EQUAL(resultColorImage->height, height);
593
594 auto rgb_buffer_ivt = resultColorImage->pixels;
595 int index_ivt = 0;
596
597 for (int i = 0; i < width * height; ++i)
598 {
599 rgb_buffer_ivt[index_ivt] = colorData[i].red;
600 rgb_buffer_ivt[index_ivt + 1] = colorData[i].green;
601 rgb_buffer_ivt[index_ivt + 2] = colorData[i].blue;
602 index_ivt += 3;
603 }
604 }
605
606 // Process depth frame
607 {
608 ScopedStopWatch sw{createSwCallback("process depth frame")};
609
610 const int width = depthFrame->getCols();
611 const int height = depthFrame->getRows();
612 const uint16_t* depthData = depthFrame->getData();
613
614 // Copy depth data to OpenCV mat for pointcloud processing
615 if (not framerate.pointCloud.skip())
616 {
617 std::lock_guard lock{pointcloudProcMutex};
618
619 // Copy depth data to alignedDepthImage
620 std::memcpy(
621 alignedDepthImage.data, depthData, width * height * sizeof(uint16_t));
622
623 // Signal that point cloud processing may proceed
624 depthImageReady = true;
625 ARMARX_DEBUG << "Notifying pointcloud thread.";
626 pointcloudProcSignal.notify_one();
627 }
628 }
629
630 // Prepare result depth image for visualization
631 {
632 ScopedStopWatch sw{createSwCallback("prepare result depth image")};
633
634 const int width = depthFrame->getCols();
635 const int height = depthFrame->getRows();
636 const uint16_t* depthData = depthFrame->getData();
637
638 ARMARX_CHECK_EXPRESSION(resultDepthImage);
639 ARMARX_CHECK_EQUAL(resultDepthImage->width, width);
640 ARMARX_CHECK_EQUAL(resultDepthImage->height, height);
641
642 auto result_depth_buffer = resultDepthImage->pixels;
643
644 int index = 0;
645 for (int i = 0; i < width * height; ++i)
646 {
647 uint16_t depth_value = depthData[i];
649 result_depth_buffer[index],
650 result_depth_buffer[index + 1],
651 result_depth_buffer[index + 2]);
652 index += 3;
653 }
654 }
655
656 // Broadcast RGB-D image
657 {
658 ScopedStopWatch sw{createSwCallback("broadcast RGB-D image")};
659 CByteImage* images[2] = {resultColorImage.get(), resultDepthImage.get()};
660 provideImages(images, imagesTime);
661 }
662
663 // Wait until depth image was processed
664 {
665 std::unique_lock signal_lock{pointcloudProcMutex};
666 ARMARX_DEBUG << "Capturing thread waiting for signal...";
667 pointcloudProcSignal.wait(signal_lock, [&] { return depthImageProcessed; });
668 ARMARX_DEBUG << "Capturing thread received signal.";
669 }
670
671 {
672 std::lock_guard g{debugObserverMtx};
674 }
675
676 if (enableHeartbeat)
677 {
678 heartbeatPlugin->heartbeat();
679 }
680
681 return true;
682 }
683
684 ARMARX_INFO << deactivateSpam(30) << "Did not get frames from Nuitrack";
685
686 {
687 std::lock_guard g{debugObserverMtx};
689 }
690
691 return false;
692 }
693
694 void
695 NuitrackPointCloudProvider::runPublishBodyTrackingResults()
696 {
698 while (bodyTrackingIsRunning)
699 {
700 // Get latest skeleton data
701 tdv::nuitrack::SkeletonData::Ptr skeletonData;
702 {
703 std::lock_guard<std::mutex> lock(skeletonMutex);
704 skeletonData = latestSkeletonData;
705 }
706
707 if (skeletonData)
708 {
710 createSwCallback("publish body tracking result")};
711
712 armarx::DateTime timestamp;
713 {
714 std::lock_guard<std::mutex> lock(colorFrameMutex);
715 timestamp = armarx::DateTime(armarx::Duration::MicroSeconds(latestTimestamp));
716 }
717 const auto& skeletons = skeletonData->getSkeletons();
718 std::uint32_t num_bodies = skeletons.size();
719
720 {
721 std::lock_guard g{debugObserverMtx};
722 setDebugObserverDatafield("n_bodies_detected", num_bodies);
723 }
724
725 std::vector<armarx::armem::human::HumanPose> humanPoses;
726 humanPoses.reserve(num_bodies);
727
728 for (const auto& skeleton : skeletons)
729 {
730 printBodyInformation(skeleton);
731
732 armarx::armem::human::HumanPose humanPose;
733 humanPose.humanTrackingId = std::to_string(skeleton.id);
734 humanPose.cameraFrameName = bodyCameraFrameName;
736
737 int segmentsWithConfidenceLargerThanThreshold = 0;
738
739 // Iterate through all Nuitrack joints and map them to k4abt joints
740 for (const auto& nuitrackJoint : skeleton.joints)
741 {
742 auto k4aJointIdx = getNuitrackToK4ABTMapping(nuitrackJoint.type);
743 if (!k4aJointIdx.has_value())
744 {
745 // Skip joints that don't have a mapping
746 continue;
747 }
748
749 const auto joint =
751 k4aJointIdx.value());
752 const auto name =
754
755 // Nuitrack provides position in mm already
756 // In contrast to AzureKinect framework (positive y -> downwards)
757 // Nuitrack uses (positive y -> upward)
758 // Therefore, we take -1 * y
759 Eigen::Vector3f position(
760 nuitrackJoint.real.x, -nuitrackJoint.real.y, nuitrackJoint.real.z);
761
762 // Convert Nuitrack orientation (rotation matrix) to quaternion
763 Eigen::Matrix3f rotMatrix;
764 rotMatrix << nuitrackJoint.orient.matrix[0], nuitrackJoint.orient.matrix[1],
765 nuitrackJoint.orient.matrix[2], nuitrackJoint.orient.matrix[3],
766 nuitrackJoint.orient.matrix[4], nuitrackJoint.orient.matrix[5],
767 nuitrackJoint.orient.matrix[6], nuitrackJoint.orient.matrix[7],
768 nuitrackJoint.orient.matrix[8];
769
770 humanPose.keypoints[name] = armarx::armem::human::PoseKeypoint{
771 .label = name,
772 .confidence = nuitrackJoint.confidence,
773 .positionCamera =
774 armarx::FramedPosition(position, bodyCameraFrameName, robotName),
775 .orientationCamera = armarx::FramedOrientation(
776 rotMatrix, bodyCameraFrameName, robotName)};
777
778 segmentsWithConfidenceLargerThanThreshold +=
779 static_cast<int>(nuitrackJoint.confidence > confidenceThreshold);
780 }
781
782 if (segmentsWithConfidenceLargerThanThreshold >=
783 minimumSegmentsWithConfidenceLargerThanThreshold)
784 {
785 humanPoses.push_back(humanPose);
786 }
787 else
788 {
790 << "Ignoring human due to minimum confident segments="
791 << segmentsWithConfidenceLargerThanThreshold << "<"
792 << minimumSegmentsWithConfidenceLargerThanThreshold;
793 }
794 }
795
796 {
797 std::lock_guard g{debugObserverMtx};
798 setDebugObserverDatafield("bodyTrackingCaptureUntilPublish [ms]",
799 (armarx::Clock::Now() - timestamp).toMilliSeconds());
800 }
801
802 // In the K4ABT camera frame (positive y = down), world up is -y.
803 filterPosesByUpright(humanPoses,
804 Eigen::Vector3f::UnitZ(),
805 uprightFilterMaxAngleDeg);
806
807 if (not humanPoses.empty())
808 {
810 humanPoseWriter.commitHumanPosesInCameraFrame(humanPoses, getName(), timestamp);
811 }
812 }
813 else
814 {
815 ARMARX_VERBOSE << deactivateSpam(10) << "No skeleton data available";
816 }
817
818 metronome.waitForNextTick();
819 }
820 }
821
822 void
824 {
825 ARMARX_DEBUG << "Started pointcloud processing task.";
826
827 IceUtil::Time time;
828
829 // Main pointcloud processing loop.
830 while (not pointcloudTask->isStopped())
831 {
832 // Wait for data.
833 std::unique_lock signal_lock{pointcloudProcMutex};
834 ARMARX_DEBUG << "Pointcloud thread waiting for signal...";
835 pointcloudProcSignal.wait(
836 signal_lock, [&] { return pointcloudTask->isStopped() or depthImageReady; });
837 ARMARX_DEBUG << "Pointcloud thread received signal.";
838
839 // Assess timings, reset flags.
840 const IceUtil::Time TIMESTAMP = imagesTime;
841 depthImageReady = false;
842 depthImageProcessed = false;
843
844 // Construct PCL pointcloud from depth data
845 {
847
848 ARMARX_CHECK_EXPRESSION(pointcloud);
849
850 const int width = alignedDepthImage.cols;
851 const int height = alignedDepthImage.rows;
852
853 pointcloud->width = static_cast<uint32_t>(width);
854 pointcloud->height = static_cast<uint32_t>(height);
855 pointcloud->is_dense = false;
856 pointcloud->points.resize(pointcloud->width * pointcloud->height);
857
858 const uint16_t* depth_buffer =
859 reinterpret_cast<const uint16_t*>(alignedDepthImage.data);
860 unsigned char* color_buffer = resultColorImage->pixels;
861
862 ARMARX_CHECK_NOT_NULL(depth_buffer);
863 ARMARX_CHECK_NOT_NULL(color_buffer);
864 ARMARX_CHECK_EQUAL(width, resultColorImage->width);
865 ARMARX_CHECK_EQUAL(height, resultColorImage->height);
866
867 // TODO: Seems to be bugged...
868 // auto intrinsics = colorSensor->getOutputMode().intrinsics;
869 // const float fx = intrinsics.fx;
870 // const float fy = intrinsics.fy;
871 // const float cx = intrinsics.cx;
872 // const float cy = intrinsics.cy;
873
874 const float fx = 525.0f; // Focal length x (approximate for common RGB-D sensors)
875 const float fy = 525.0f; // Focal length y
876 const float cx = width / 2.0f; // Principal point x
877 const float cy = height / 2.0f; // Principal point y
878
879 float max_depth = getProperty<float>("MaxDepth").getValue();
880
881 size_t point_index = 0;
882 int color_index = 0;
883
884 for (int y = 0; y < height; ++y)
885 {
886 for (int x = 0; x < width; ++x, ++point_index)
887 {
888 auto& p = pointcloud->points[point_index];
889
890 // Get color
891 p.r = color_buffer[color_index++];
892 p.g = color_buffer[color_index++];
893 p.b = color_buffer[color_index++];
894
895 // Get depth and convert to 3D coordinates
896 uint16_t depth_value = depth_buffer[point_index];
897
898 if (depth_value > 0 && depth_value <= max_depth)
899 {
900 float depth_meters =
901 depth_value; // Nuitrack already provides depth in mm
902 p.z = depth_meters;
903 p.x = (x - cx) * depth_meters / fx;
904 p.y = (y - cy) * depth_meters / fy;
905 }
906 else
907 {
908 p.x = std::numeric_limits<float>::quiet_NaN();
909 p.y = std::numeric_limits<float>::quiet_NaN();
910 p.z = std::numeric_limits<float>::quiet_NaN();
911 }
912 }
913 }
914
915 ARMARX_DEBUG << "Constructing point cloud took "
917 }
918
919 // Notify capturing thread that data was processed and may be overridden.
920 depthImageProcessed = true;
921 signal_lock.unlock();
922 ARMARX_DEBUG << "Notifying capturing thread...";
923 pointcloudProcSignal.notify_all();
924
926
927 // Broadcast PCL pointcloud.
928 {
930 pointcloud->header.stamp = static_cast<unsigned long>(TIMESTAMP.toMicroSeconds());
931 providePointCloud(pointcloud);
932 ARMARX_DEBUG << "Broadcasting pointcloud took "
934 }
935 }
936
937 ARMARX_DEBUG << "Stopped pointcloud processing task.";
938 }
939
940 std::string
942 {
943 // TODO(@rietsch): Change to different name at some point
944 return "AzureKinectPointCloudProvider";
945 }
946
947 void
954
955 void
957 {
960 robotReader.connect(memoryNameSystem());
961 robot = robotReader.getRobot("Armar7");
962 }
963
964 void
966 {
967 captureEnabled = false;
968
969 if (bodyTrackingIsRunning)
970 {
971 bodyTrackingIsRunning = false;
972 bodyTrackingPublishTask->stop();
973 }
974
975 // Release Nuitrack so it can be re-initialized on reconnect.
976 stopCapture();
977
980 }
981
982 void
988
989 StereoCalibration
991 {
992 using namespace Eigen;
993 // using Matrix3FRowMajor = Matrix<float, 3, 3, StorageOptions::RowMajor>;
995
996 StereoCalibration stereo_calibration;
997
998 // Get image dimensions from properties
999 int colorWidth = getProperty<int>("ColorWidth").getValue();
1000 int colorHeight = getProperty<int>("ColorHeight").getValue();
1001
1002 // Create calibration for color camera (left)
1003 MonocularCalibration colorCalib;
1004 colorCalib.cameraParam.width = colorWidth;
1005 colorCalib.cameraParam.height = colorHeight;
1006
1007 auto intrinsics = colorSensor->getOutputMode().intrinsics;
1008 colorCalib.cameraParam.focalLength = {intrinsics.fx, intrinsics.fy};
1009
1010 colorCalib.cameraParam.principalPoint = {intrinsics.cx, intrinsics.cy};
1011
1012 // Minimal distortion (assuming Nuitrack provides undistorted images or handles distortion)
1013 colorCalib.cameraParam.distortion = {0.0f, 0.0f, 0.0f, 0.0f};
1014
1015 // Identity rotation and zero translation (color camera as reference)
1016 colorCalib.cameraParam.rotation = convertEigenMatToVisionX(Matrix3f::Identity());
1017 colorCalib.cameraParam.translation = {0.0f, 0.0f, 0.0f};
1018
1019 stereo_calibration.calibrationLeft = colorCalib;
1020
1021 // Depth camera calibration (right) - same as color for simplicity
1022 // since Nuitrack aligns depth to color frame
1023 MonocularCalibration depthCalib = colorCalib;
1024 depthCalib.cameraParam.distortion = {0.0f, 0.0f, 0.0f, 0.0f}; // Depth is rectified
1025 stereo_calibration.calibrationRight = depthCalib;
1026
1027 stereo_calibration.rectificationHomographyLeft =
1028 convertEigenMatToVisionX(Matrix3f::Identity());
1029 stereo_calibration.rectificationHomographyRight =
1030 convertEigenMatToVisionX(Matrix3f::Identity());
1031
1032 return stereo_calibration;
1033 }
1034
1035 bool
1037 {
1038 return true;
1039 }
1040
1041 std::string
1043 {
1044 return getProperty<std::string>("bodyCameraFrameName");
1045 }
1046
1047 std::vector<imrec::ChannelPreferences>
1049 {
1051
1052 imrec::ChannelPreferences rgb;
1053 rgb.requiresLossless = false;
1054 rgb.name = "rgb";
1055
1056 imrec::ChannelPreferences depth;
1057 depth.requiresLossless = true;
1058 depth.name = "depth";
1059
1060 return {rgb, depth};
1061 }
1062
1063 void
1065 const armarx::EnableHumanPoseEstimationInput& input,
1066 const Ice::Current&)
1067 {
1068 if (bodyTrackingEnabled)
1069 {
1070 // should not require a mutex
1071 if (not bodyTrackingIsRunning and input.enable3d)
1072 {
1073 bodyTrackingIsRunning = true;
1074 bodyTrackingPublishTask->start();
1075 }
1076 else if (bodyTrackingIsRunning and not input.enable3d)
1077 {
1078 bodyTrackingIsRunning = false;
1079 bodyTrackingPublishTask->stop();
1080 }
1081 }
1082 else
1083 {
1084 ARMARX_ERROR << "Azure Kinect Body Tracking is not enabled";
1085 }
1086 }
1087
1088 void
1089 NuitrackPointCloudProvider::setMaxDepthBodyTracking(int maxDepthInMM, const Ice::Current&)
1090 {
1091 std::scoped_lock lock(bodyTrackingParameterMutex);
1092 bodyTrackingDepthMaskMaxZ = maxDepthInMM;
1093 }
1094
1095 void
1097 int maxXinPixel,
1098 const Ice::Current&)
1099 {
1100 std::scoped_lock lock(bodyTrackingParameterMutex);
1101 bodyTrackingDepthMaskMinX = minXinPixel;
1102 bodyTrackingDepthMaskMaxX = maxXinPixel;
1103 }
1104
1109
1110 void
1112 {
1113 if (std::abs(std::fmod(higherFramerate, value)) > 1e-6)
1114 {
1115 std::stringstream ss;
1116 ss << "Invalid value (" << value << " fps) for framerate of '" << name << "'."
1117 << " Reason: The framerate has to be a divider of the property framerate.image ("
1118 << higherFramerate << " fps).";
1119 throw armarx::LocalException() << ss.str();
1120 }
1121 skipFrames = std::round(higherFramerate / value) - 1;
1122 if (skipFrames != 0)
1123 {
1124 ARMARX_INFO_S << "Only publishing every " << (skipFrames + 1) << "'th " << name
1125 << " result!";
1126 }
1127 }
1128
1129 bool
1131 {
1132 bool skip = false;
1133 if (skipFrames > 0)
1134 {
1137 skipFramesCount %= (skipFrames + 1);
1138 }
1139 return skip;
1140 }
1141
1142} // namespace visionx
std::string timestamp()
uint8_t index
static DateTime Now()
Current time on the virtual clock.
Definition Clock.cpp:93
std::string getConfigIdentifier()
Retrieve config identifier for this component as set in constructor.
Definition Component.cpp:90
Property< PropertyType > getProperty(const std::string &name)
static Duration MicroSeconds(std::int64_t microSeconds)
Constructs a duration in microseconds.
Definition Duration.cpp:24
static Duration MilliSeconds(std::int64_t milliSeconds)
Constructs a duration in milliseconds.
Definition Duration.cpp:48
The FramedPosition class.
Definition FramedPose.h:158
Eigen::Vector3f toGlobalEigen(const SharedRobotInterfacePrx &referenceRobot) const
static Frequency Hertz(std::int64_t hertz)
Definition Frequency.cpp:20
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
virtual void onExitComponent()
Hook for subclass.
bool usingProxy(const std::string &name, const std::string &endpoints="")
Registers a proxy for retrieval after initialization and adds it to the dependency list.
virtual void onDisconnectComponent()
Hook for subclass.
PluginT * addPlugin(const std::string prefix="", ParamsT &&... params)
virtual void onConnectComponent()=0
Pure virtual hook for the subclass.
std::string getName() const
Retrieve name of object.
virtual void onInitComponent()=0
Pure virtual hook for the subclass.
void setMetaInfo(const std::string &id, const VariantBasePtr &value)
Allows to set meta information that can be queried live via Ice interface on the ArmarXManager.
ArmarXManagerPtr getArmarXManager() const
Returns the ArmarX manager used to add and remove components.
std::string prefix
Prefix of the properties such as namespace, domain, component name, etc.
PropertyDefinition< PropertyType > & defineOptionalProperty(const std::string &name, PropertyType defaultValue, const std::string &description="", PropertyDefinitionBase::PropertyConstness constness=PropertyDefinitionBase::eConstant)
IceUtil::Handle< RunningTask< T > > pointer_type
Shared pointer type for convenience.
static IceUtil::Time GetTime(TimeMode timeMode=TimeMode::VirtualTime)
Get the current time.
Definition TimeUtil.cpp:42
static IceUtil::Time GetTimeSince(IceUtil::Time referenceTime, TimeMode timeMode=TimeMode::VirtualTime)
Get the difference between the current time and a reference time.
Definition TimeUtil.cpp:68
The Variant class is described here: Variants.
Definition Variant.h:224
std::int64_t toMicroSecondsSinceEpoch() const
Definition DateTime.cpp:87
Represents a duration.
Definition Duration.h:17
Simple rate limiter for use in loops to maintain a certain frequency given a clock.
Definition Metronome.h:57
Measures the time this stop watch was inside the current scope.
Measures the passed time between the construction or calling reset() and stop().
Definition StopWatch.h:42
void stopCapture(const Ice::Current &c=Ice::emptyCurrent) override
Stops point cloud capturing.
std::atomic_bool captureEnabled
Indicates that capturing is enabled and running.
void setPointCloudSyncMode(ImageSyncMode pointCloudSyncMode)
Sets the point cloud synchronization mode.
void onInitComponent() override
void onDisconnectComponent() override
Hook for subclass.
ImageFormatInfo getImageFormat(const Ice::Current &c=Ice::emptyCurrent) override
Returns the entire image format info struct via Ice.
void setImageFormat(ImageDimension imageDimension, ImageType imageType, BayerPatternType bayerPatternType=visionx::eBayerPatternRg)
Sets the image basic format data.
void provideImages(void **inputBuffers, const IceUtil::Time &imageTimestamp=IceUtil::Time())
send images raw.
void onConnectComponent() override
void setNumberImages(int numberImages)
Sets the number of images on each capture.
void onExitComponent() override
void onNewSkeletonData(tdv::nuitrack::SkeletonData::Ptr skeletonData)
void onInitComponent() override
Pure virtual hook for the subclass.
void setMaxDepthBodyTracking(int maxDepthInMM, const Ice::Current &=Ice::emptyCurrent) override
bool doCapture() override
Main capturing function.
void enableHumanPoseEstimation(const armarx::EnableHumanPoseEstimationInput &input, const Ice::Current &=Ice::emptyCurrent) override
std::optional< Eigen::Vector3f > computeBodyUpVector(const armarx::armem::human::HumanPose &pose)
void onExitCapturingPointCloudProvider() override
This is called when the Component::onExitComponent() setup is called.
void onDisconnectComponent() override
Hook for subclass.
void onNewColorFrame(tdv::nuitrack::RGBFrame::Ptr frame)
void onStartCapture(float frames_per_second) override
This is called when the point cloud provider capturing has been started.
void filterPosesByUpright(std::vector< armarx::armem::human::HumanPose > &poses, const Eigen::Vector3f &worldUp, float maxAngleDeg)
armarx::PropertyDefinitionsPtr createPropertyDefinitions() override
void onConnectImageProvider() override
This is called when the Component::onConnectComponent() setup is called.
visionx::StereoCalibration getStereoCalibration(const Ice::Current &c) override
std::string getReferenceFrame(const Ice::Current &c) override
std::function< void(armarx::Duration)> createSwCallback(const std::string &description)
void onInitCapturingPointCloudProvider() override
This is called when the Component::onInitComponent() is called.
void onConnectComponent() override
Pure virtual hook for the subclass.
void onStopCapture() override
This is called when the point cloud provider capturing has been stopped.
bool getImagesAreUndistorted(const ::Ice::Current &c) override
std::vector< imrec::ChannelPreferences > getImageRecordingChannelPreferences(const Ice::Current &) override
void onInitImageProvider() override
This is called when the Component::onInitComponent() is called.
void setWidthBodyTracking(int minXinPixel, int maxXinPixel, const Ice::Current &=Ice::emptyCurrent) override
void onExitComponent() override
Hook for subclass.
void onNewDepthFrame(tdv::nuitrack::DepthFrame::Ptr frame)
MetaPointCloudFormatPtr getPointCloudFormat(const Ice::Current &c=Ice::emptyCurrent) override
Returns the point cloud format info struct via Ice.
void providePointCloud(PointCloudPtrT pointCloudPtr)
offer the new point cloud.
#define ARMARX_CHECK_EXPRESSION(expression)
This macro evaluates the expression and if it turns out to be false it will throw an ExpressionExcept...
#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_CHECK_EQUAL(lhs, rhs)
This macro evaluates whether lhs is equal (==) rhs and if it turns out to be false it will throw an E...
#define ARMARX_INFO
The normal logging level.
Definition Logging.h:181
#define ARMARX_ERROR
The logging level for unexpected behaviour, that must be fixed.
Definition Logging.h:196
#define ARMARX_INFO_S
Definition Logging.h:202
#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
const simox::meta::EnumNames< Joints > JointNames
Names of the joints as defined in the body model.
Joints
Joints with index as defined in the body model.
This file offers overloads of toIce() and fromIce() functions for STL container types.
IceUtil::Handle< class PropertyDefinitionContainer > PropertyDefinitionsPtr
PropertyDefinitions smart pointer type.
void depthValueToRGB(unsigned int depthInMM, unsigned char &r, unsigned char &g, unsigned char &b, bool noiseResistant=false)
Definition ImageUtil.h:183
CByteImage * createByteImage(const ImageFormatInfo &imageFormat, const ImageType imageType)
Creates a ByteImage for the destination type specified in the given imageProviderInfo.
visionx::types::Mat convertEigenMatToVisionX(Eigen::MatrixXf m)
ArmarX headers.
double norm(const Point &a)
Definition point.hpp:102
std::optional< std::string > humanTrackingId
Definition types.h:47
#define ARMARX_TRACE
Definition trace.h:77