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