Component.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 VisionX::ArmarXObjects::Yolo
17 * @author Christian R. G. Dreher <c.dreher@kit.edu>
18 * @date 2019
19 * @copyright http://www.gnu.org/licenses/gpl-2.0.txt
20 * GNU General Public License
21 */
22
23
25
27
28
29// STD/STL
30#include <algorithm> // std::find, std::max, std::min
31#include <chrono>
32#include <fstream> // std::ifstream
33#include <future>
34#include <iomanip> // std::setprecision
35#include <iostream> // std::fixed
36#include <iterator> // std::begin, std::end, std::distance
37#include <map>
38#include <thread> // std::sleep_for
39
40// IVT
41#include <Image/ImageProcessor.h>
42#include <Image/IplImageAdaptor.h>
43#include <Image/PrimitivesDrawer.h>
44#include <Image/PrimitivesDrawerCV.h> // Deprecated, but required to draw text.
45
46// Ice
47#include <IceUtil/Time.h>
48
49// ArmarX
53
54
55using namespace armarx;
56using namespace visionx;
57using namespace visionx::yolo;
58
59namespace
60{
61 IceUtil::Time timestamp_invalid = IceUtil::Time::milliSeconds(-1);
62}
63
64const std::string yolo::Component::default_name = "Yolo";
65
67{
68 // pass
69}
70
71void
73{
74 // Initialise runtime parameters from defaults.
76
77 m_image_received = false;
78
79 // Signal dependency on image provider
80 m_image_provider_id = getProperty<std::string>("ipc.ImageProviderName");
81 ARMARX_VERBOSE << "Using image provider with ID '" << m_image_provider_id << "'.";
82 usingImageProvider(m_image_provider_id);
83
84 // Get data channel.
85 m_image_provider_channel = getProperty<unsigned int>("ipc.ImageProviderChannel");
86
87 // Topic name under which the detection results are published.
88 {
89 std::string topic_name = getProperty<std::string>("ipc.ObjectDetectionResultTopicName");
90 offeringTopic(topic_name);
91 }
92
93 // Load known classes.
94 {
95 std::ifstream infile{getProperty<std::string>("darknet.namefile")};
96 std::string line;
97 while (std::getline(infile, line))
98 {
99 m_darknet_classes.push_back(line);
100 }
101
102 m_network.class_count(static_cast<int>(m_darknet_classes.size()));
103 }
104
105 // Boot Darknet. This may take several seconds to complete.
106 bootstrap_darknet();
107
108 ARMARX_INFO << "Darknet provider initialised.";
109}
110
111void
113{
114 // Connect to image provider.
115 m_image_provider_info = getImageProvider(m_image_provider_id);
116 m_image_provider = getProxy<visionx::ImageProviderInterfacePrx>(m_image_provider_id);
117
118 // Init input image.
119 {
120 const unsigned int num_images =
121 static_cast<unsigned int>(m_image_provider_info.numberImages);
122 m_input_image_buf = new ::CByteImage*[num_images];
123 for (unsigned int i = 0; i < num_images; ++i)
124 {
125 m_input_image_buf[i] = visionx::tools::createByteImage(m_image_provider_info);
126 }
127 m_input_image.reset(visionx::tools::createByteImage(m_image_provider_info));
128 m_output_image.reset(visionx::tools::createByteImage(m_image_provider_info));
129 }
130
131 // Topic of detected objects for consumers.
132 std::string topic_name = getProperty<std::string>("ipc.ObjectDetectionResultTopicName");
133 m_object_detection_listener = getTopic<ObjectListener::ProxyType>(topic_name);
134
135 // Initialise visual output if enabled.
136 if (getProperty<bool>("yolo.EnableVisualisation"))
137 {
138 const int num_images = 1;
139 enableResultImages(num_images,
140 m_image_provider_info.imageFormat.dimension,
141 m_image_provider_info.imageFormat.type);
142 }
143
144 // Kick off running task
145 m_task_object_detection =
146 new RunningTask<yolo::Component>(this, &yolo::Component::run_object_detection_task);
147 m_task_object_detection->start();
148
149 ARMARX_INFO << "Darknet provider connected. Operation begins now.";
150}
151
152void
154{
155 // Stop task.
156 {
157 const bool wait_for_join = true;
158 m_task_object_detection->stop(wait_for_join);
159 }
160
161 // Clear input image buffer.
162 {
163 const unsigned int num_images =
164 static_cast<unsigned int>(m_image_provider_info.numberImages);
165 for (unsigned int i = 0; i < num_images; ++i)
166 {
167 delete m_input_image_buf[i];
168 }
169 delete[] m_input_image_buf;
170 }
171
172 ARMARX_INFO << "Darknet provider disconnected.";
173}
174
175void
177{
178 ARMARX_INFO << "Darknet provider uninitialised.";
179}
180
181std::string
186
187std::string
189{
190 return GetDefaultName();
191}
192
193void
195{
196 ARMARX_VERBOSE << "Processing frame...";
197
198 {
199 const IceUtil::Time timeout = IceUtil::Time::milliSeconds(1000);
200 if (not waitForImages(m_image_provider_id, static_cast<int>(timeout.toMilliSeconds())))
201 {
202 ARMARX_WARNING << "Timeout while waiting for camera images (>" << timeout << ")";
203 return;
204 }
205 }
206
207 std::lock_guard<std::mutex> lock{m_input_image_mutex};
208
209 MetaInfoSizeBasePtr info;
210 int num_images = getImages(m_image_provider_id, m_input_image_buf, info);
211 m_timestamp_last_image = IceUtil::Time::microSeconds(info->timeProvided);
212
213 if (num_images >= 1)
214 {
215 // Only consider first image.
216 ::ImageProcessor::CopyImage(m_input_image_buf[m_image_provider_channel],
217 m_input_image.get());
218 m_image_received = true;
219 }
220 else
221 {
222 ARMARX_WARNING << "Didn't receive an image.";
223 }
224}
225
226double
227yolo::Component::getThresh(const Ice::Current&)
228{
229 return static_cast<double>(m_network.thresh());
230}
231
232void
233yolo::Component::setThresh(double thresh, const Ice::Current&)
234{
235 m_network.thresh(static_cast<float>(thresh));
236 ARMARX_VERBOSE << "Parameter thresh is now " << thresh << ".";
237}
238
239double
241{
242 return static_cast<double>(m_network.hier_thresh());
243}
244
245void
246yolo::Component::setHierThresh(double hier_thresh, const Ice::Current&)
247{
248 m_network.hier_thresh(static_cast<float>(hier_thresh));
249 ARMARX_VERBOSE << "Parameter hier_thresh is now " << hier_thresh << ".";
250}
251
252double
253yolo::Component::getNms(const Ice::Current&)
254{
255 return static_cast<double>(m_network.nms());
256}
257
258void
259yolo::Component::setNms(double nms, const Ice::Current&)
260{
261 m_network.nms(static_cast<float>(nms));
262 ARMARX_VERBOSE << "Parameter nms is now " << nms << ".";
263}
264
265float
266yolo::Component::getFpsCap(const Ice::Current&)
267{
268 // If FPS cap is disabled, always return -1 for a consistent API.
269 if (m_minimum_loop_time == ::timestamp_invalid)
270 {
271 return -1.;
272 }
273
274 // Derive the FPS cap from the minimum loop time.
275 return 1000.f / m_minimum_loop_time.toMilliSeconds();
276}
277
278void
279yolo::Component::setFpsCap(float fps_cap, const Ice::Current&)
280{
281 // Set m_minimum_loop_time to zero for all values for fps_cap <= 0 to disable the cap.
282 if (fps_cap <= 0)
283 {
284 m_minimum_loop_time = ::timestamp_invalid;
285 return;
286 }
287
288 // Don't actually store the fps cap, but calculate the minimum loop time in [ms].
289 m_minimum_loop_time = IceUtil::Time::milliSecondsDouble(1000. / static_cast<double>(fps_cap));
290}
291
292std::vector<std::string>
293yolo::Component::getClasses(const Ice::Current&)
294{
295 return m_darknet_classes;
296}
297
298void
300{
301 ARMARX_INFO << "Restoring default parameter values.";
302 m_network.thresh(static_cast<float>(getProperty<double>("darknet.thresh")));
303 m_network.hier_thresh(static_cast<float>(getProperty<double>("darknet.hier_thresh")));
304 m_network.nms(static_cast<float>(getProperty<double>("darknet.nms")));
305 setFpsCap(getProperty<float>("yolo.FPSCap"));
306}
307
308void
309yolo::Component::bootstrap_darknet()
310{
311 ARMARX_INFO << "Bootstrapping Darknet.";
312
313 ScopedStopWatch sw{[&](IceUtil::Time time)
314 {
315 ARMARX_INFO << std::fixed << std::setprecision(3)
316 << "Bootstrapped Darknet in " << time.toSecondsDouble()
317 << " seconds.";
318 }};
319
320 // Get properties.
321 const std::string cfgfile = getProperty<std::string>("darknet.cfgfile");
322 const std::string weightfile = getProperty<std::string>("darknet.weightfile");
323
324 // Bootstrap Darknet.
325 m_network.load(cfgfile, weightfile);
326 m_network.set_batch(1);
327}
328
329void
330yolo::Component::run_object_detection_task()
331{
332 // Initialise working copy instance of input_image.
333 CByteImageUPtr input_image;
334 {
335 visionx::ImageProviderInfo image_provider_info = getImageProvider(m_image_provider_id);
336 input_image.reset(visionx::tools::createByteImage(image_provider_info));
337 }
338
339 // Stop watch to assess timings.
340 StopWatch sw;
341
342 // Kick off detection loop.
343 while (not m_task_object_detection->isStopped())
344 {
345 // Assess loop start time.
346 sw.reset();
347
348 ARMARX_VERBOSE << "Running Darknet.";
349
350 IceUtil::Time timestamp_last_image;
351
352 ARMARX_VERBOSE << "Creating local working copies.";
353 {
354 std::lock_guard<std::mutex> lock{m_input_image_mutex};
355 if (not m_image_received)
356 {
357 ARMARX_VERBOSE << "No image provided yet, waiting...";
358 continue;
359 }
360 timestamp_last_image = m_timestamp_last_image;
361 const bool ok = ::ImageProcessor::CopyImage(m_input_image.get(), input_image.get());
362 // Skip frame if copying fails (usually when buffer is empty at the beginning).
363 if (not ok)
364 {
365 continue;
366 }
367 m_image_received = false;
368 }
369
370 ARMARX_VERBOSE << "Announcing which frame will be fed to Darknet.";
371 m_object_detection_listener->announceDetectedObjects(timestamp_last_image.toMicroSeconds());
372
373 // Have Darknet make preditions and convert to VisionX interface type. This call will
374 // usually consume a considerable amount of time.
375 std::vector<DetectedObject> detected_objects;
376 {
377 ::IplImage* input_image_conv =
378 ::IplImageAdaptor::Adapt(input_image.get()); // Shallow copy.
379 ARMARX_VERBOSE << "Predicting now...";
380 detected_objects =
381 convertOutput(m_network.predict(*input_image_conv), m_darknet_classes);
382 ::cvReleaseImageHeader(&input_image_conv); // Only header must be freed.
383 ARMARX_VERBOSE << "Found " << detected_objects.size() << " objects.";
384 }
385
386 ARMARX_VERBOSE << "Broadcasting all detections.";
387 m_object_detection_listener->reportDetectedObjects(detected_objects,
388 timestamp_last_image.toMicroSeconds());
389
390 // Render output bounding boxes if visualisation is enabled.
391 if (getProperty<bool>("yolo.EnableVisualisation"))
392 {
393 ARMARX_VERBOSE << "Rendering output bounding boxes.";
395 input_image.get(), detected_objects, m_output_image.get());
396 ::CByteImage* output_images[1] = {m_output_image.get()};
397 // Provide the images with the original timestamp.
398 resultImageProvider->provideResultImages(output_images,
399 timestamp_last_image.toMicroSeconds());
400 }
401
402 // Assess timings.
403 const IceUtil::Time loop_duration = sw.stop();
404
405 ARMARX_VERBOSE << "Ran Darknet in " << loop_duration << ".";
406
407 // If the cap is enabled and if the loop duration is less than the minimum loop time
408 // => sleep for the difference in [ms].
409 if (m_minimum_loop_time != ::timestamp_invalid and loop_duration < m_minimum_loop_time)
410 {
411 const IceUtil::Time time_delta = m_minimum_loop_time - loop_duration;
412 ARMARX_VERBOSE << "FPS cap enabled, suspending thead for " << time_delta << ".";
413 std::this_thread::sleep_for(std::chrono::milliseconds{time_delta.toMilliSeconds()});
414 }
415 }
416
417 ARMARX_INFO << "Detection loop properly shut down.";
418}
419
420std::vector<DetectedObject>
421yolo::Component::convertOutput(const std::vector<darknet::detection>& detections,
422 const std::vector<std::string>& classes)
423{
424 std::vector<DetectedObject> detections_conv;
425
426 for (const darknet::detection& detection : detections)
427 {
428 DetectedObject detection_conv;
429 const int class_count = static_cast<int>(classes.size());
430 detection_conv.classCount = class_count;
431 detection_conv.boundingBox.x = detection.x();
432 detection_conv.boundingBox.y = detection.y();
433 detection_conv.boundingBox.w = detection.w();
434 detection_conv.boundingBox.h = detection.h();
435
436 for (const auto& [class_index, certainty] : detection.candidates())
437 {
438 ClassCandidate candidate_conv;
439 candidate_conv.classIndex = class_index;
440 candidate_conv.certainty = certainty;
441 candidate_conv.className = classes.at(static_cast<unsigned int>(class_index));
442
443 DrawColor24Bit color;
444 std::tie(color.r, color.g, color.b) =
445 darknet::get_color(candidate_conv.classIndex, class_count);
446 candidate_conv.color = color;
447
448 detection_conv.candidates.push_back(candidate_conv);
449 }
450
451 detections_conv.push_back(detection_conv);
452 }
453
454 return detections_conv;
455}
456
457void
458yolo::Component::renderOutput(const ::CByteImage* input_image,
459 const std::vector<DetectedObject>& detected_objects,
460 ::CByteImage* output_image)
461{
462 ARMARX_VERBOSE_S << "Rendering output.";
463
464 ::ImageProcessor::CopyImage(input_image, output_image);
465
466 // Iterate over each detected object.
467 for (const DetectedObject& detected_object : detected_objects)
468 {
469 const BoundingBox2D& bounding_box = detected_object.boundingBox;
470 const ClassCandidate& class_candidate =
471 *std::max_element(detected_object.candidates.begin(),
472 detected_object.candidates.end(),
473 [](const ClassCandidate& c1, const ClassCandidate& c2) -> bool
474 { return c1.certainty < c2.certainty; });
475
476 const DrawColor24Bit& color = class_candidate.color;
477
478 // Draw bounding box.
479 {
480 const float angle = 0;
481 const ::Rectangle2d rectangle{::Vec2d{bounding_box.x * input_image->width,
482 bounding_box.y * input_image->height}, // center.
483 bounding_box.w * input_image->width, // width.
484 bounding_box.h * input_image->height, // height.
485 angle};
486 const int thickness = 1;
487 ::PrimitivesDrawer::DrawRectangle(
488 output_image, rectangle, color.r, color.g, color.b, thickness);
489 }
490
491 // Draw class label.
492 {
493 const std::string object_instance_name = detected_object.objectName == ""
494 ? class_candidate.className
495 : detected_object.objectName;
496 const double text_scale = .4;
497 const int text_thickness = 1;
498 const float left =
499 std::max((bounding_box.x - bounding_box.w / 2.f) * input_image->width, 0.f);
500 const float top =
501 std::max((bounding_box.y - bounding_box.h / 2.f) * input_image->height, 0.f);
502 const double text_pos_left = static_cast<double>(left + 2);
503 const double text_pos_top = static_cast<double>(top + (top < 10 ? 11 : 1));
504
505 // Blurry thick text in bbox color as visual hint.
506 ::PrimitivesDrawerCV::PutText(output_image,
507 object_instance_name.c_str(),
508 text_pos_left,
509 text_pos_top,
510 text_scale,
511 text_scale,
512 color.r,
513 color.g,
514 color.b,
515 text_thickness + 5);
516
517 // Slightly blurry white background (for good constrast with black class name label).
518 ::PrimitivesDrawerCV::PutText(output_image,
519 object_instance_name.c_str(),
520 text_pos_left,
521 text_pos_top,
522 text_scale,
523 text_scale,
524 255,
525 255,
526 255, // white.
527 text_thickness + 3);
528
529 // Sharp, actually readable class name in black.
530 ::PrimitivesDrawerCV::PutText(output_image,
531 object_instance_name.c_str(),
532 text_pos_left,
533 text_pos_top,
534 text_scale,
535 text_scale,
536 0,
537 0,
538 0, // black.
539 text_thickness);
540 }
541 }
542}
543
546{
548
549 // Options for inter process communication.
550 defs->defineOptionalProperty<std::string>("ipc.ObjectDetectionResultTopicName",
551 "YoloDetectionResult",
552 "Topic name for the object detection result");
553 defs->defineRequiredProperty<std::string>("ipc.ImageProviderName",
554 "Image provider as data source for Darknet");
555 defs->defineOptionalProperty<unsigned int>(
556 "ipc.ImageProviderChannel", 0, "Channel of the image provider with RGB images to consider");
557
558 // Options of the Yolo component.
559 defs->defineOptionalProperty<bool>(
560 "yolo.EnableVisualisation",
561 true,
562 "If set to true, a visualisation of the output will be provided under the topic defined in "
563 "`ipc.ObjectDetectionResultTopicName`");
564 defs->defineOptionalProperty<float>(
565 "yolo.FPSCap",
566 -1.,
567 "Cap the main loop to a fixed FPS rate to save resources. Values <= 0 disable the cap, so "
568 "that only the hardware limits the FPS");
569
570 // Options of Darknet.
571 defs->defineOptionalProperty<double>(
572 "darknet.thresh", .5, "Default thresh. May be overridden at runtime");
573 defs->defineOptionalProperty<double>(
574 "darknet.hier_thresh", .5, "Default hier_thresh. May be overridden at runtime");
575 defs->defineOptionalProperty<double>(
576 "darknet.nms", .45, "Default nms. May be overridden at runtime");
577 defs->defineRequiredProperty<std::string>("darknet.cfgfile", "Darknet configuration file");
578 defs->defineRequiredProperty<std::string>("darknet.weightfile", "Darknet weight file");
579 defs->defineRequiredProperty<std::string>(
580 "darknet.namefile",
581 "File where the classes are listed (usually a '*.names' file in Darknet's data-directory)");
582
583 return defs;
584}
585
#define ARMARX_REGISTER_COMPONENT_EXECUTABLE(ComponentT, applicationName)
Definition Decoupled.h:29
Default component property definition container.
Definition Component.h:70
std::string getConfigIdentifier()
Retrieve config identifier for this component as set in constructor.
Definition Component.cpp:90
Property< PropertyType > getProperty(const std::string &name)
void offeringTopic(const std::string &name)
Registers a topic for retrival after initialization.
TopicProxyType getTopic(const std::string &name)
Returns a proxy of the specified topic.
Ice::ObjectPrx getProxy(long timeoutMs=0, bool waitForScheduler=true) const
Returns the proxy of this object (optionally it waits for the proxy)
IceUtil::Time stop()
void enableResultImages(int numberImages, ImageDimension imageDimension, ImageType imageType, const std::string &name="")
Enables visualization.
void usingImageProvider(std::string name)
Registers a delayed topic subscription and a delayed provider proxy retrieval which all will be avail...
bool waitForImages(int milliseconds=1000)
Wait for new images.
ImageProviderInfo getImageProvider(std::string name, ImageType destinationImageType=eRgb, bool waitForProxy=false)
Select an ImageProvider.
int getImages(CByteImage **ppImages)
Poll images from provider.
static const std::string default_name
Definition Component.h:63
virtual double getNms(const Ice::Current &) override
Returns the currently set nms parameter.
virtual void onConnectImageProcessor() override
virtual void setThresh(double thresh, const Ice::Current &) override
Sets the new thresh parameter.
virtual void onExitImageProcessor() override
static void renderOutput(const ::CByteImage *inputImage, const std::vector< DetectedObject > &detectedObjects, ::CByteImage *outputImage)
Renders the objects detectedObjects, belonging to one of a class defined in classes onto the outputIm...
virtual void setHierThresh(double hier_thresh, const Ice::Current &) override
Sets the new hierThresh parameter.
virtual void setNms(double nms, const Ice::Current &) override
Sets the new nms parameter.
virtual void process() override
virtual float getFpsCap(const Ice::Current &) override
virtual ~Component() override
Definition Component.cpp:66
virtual void setFpsCap(float fps_cap, const Ice::Current &=Ice::Current{}) override
virtual void onInitImageProcessor() override
Definition Component.cpp:72
virtual armarx::PropertyDefinitionsPtr createPropertyDefinitions() override
static std::vector< DetectedObject > convertOutput(const std::vector< darknet::detection > &detections, const std::vector< std::string > &classes)
Converts Darknet types to VisionX types.
static std::string GetDefaultName()
virtual double getThresh(const Ice::Current &) override
Returns the currently set thresh parameter.
virtual void onDisconnectImageProcessor() override
virtual std::vector< std::string > getClasses(const Ice::Current &) override
Returns the list of all known classes.
virtual double getHierThresh(const Ice::Current &) override
Returns the currently set hierThresh parameter.
virtual std::string getDefaultName() const override
Retrieve default name of component.
virtual void restoreDefaults(const Ice::Current &=Ice::Current{}) override
Restores the default parameter values.
#define ARMARX_INFO
The normal logging level.
Definition Logging.h:181
#define ARMARX_VERBOSE_S
Definition Logging.h:207
#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
This file offers overloads of toIce() and fromIce() functions for STL container types.
IceUtil::Handle< class PropertyDefinitionContainer > PropertyDefinitionsPtr
PropertyDefinitions smart pointer type.
CByteImage * createByteImage(const ImageFormatInfo &imageFormat, const ImageType imageType)
Creates a ByteImage for the destination type specified in the given imageProviderInfo.
ArmarX headers.
std::unique_ptr< CByteImage > CByteImageUPtr
double angle(const Point &a, const Point &b, const Point &c)
Definition point.hpp:109