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