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 
53 using namespace armarx;
54 using namespace visionx;
55 using namespace visionx::yolo;
56 
57 namespace
58 {
59  IceUtil::Time timestamp_invalid = IceUtil::Time::milliSeconds(-1);
60 }
61 
62 const std::string yolo::Component::default_name = "Yolo";
63 
64 yolo::Component::~Component()
65 {
66  // pass
67 }
68 
69 void
70 yolo::Component::onInitImageProcessor()
71 {
72  // Initialise runtime parameters from defaults.
73  restoreDefaults();
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 
109 void
110 yolo::Component::onConnectImageProcessor()
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 
150 void
151 yolo::Component::onDisconnectImageProcessor()
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 
173 void
174 yolo::Component::onExitImageProcessor()
175 {
176  ARMARX_INFO << "Darknet provider uninitialised.";
177 }
178 
179 std::string
180 yolo::Component::getDefaultName() const
181 {
182  return yolo::Component::default_name;
183 }
184 
185 void
186 yolo::Component::process()
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 
218 double
219 yolo::Component::getThresh(const Ice::Current&)
220 {
221  return static_cast<double>(m_network.thresh());
222 }
223 
224 void
225 yolo::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 
231 double
232 yolo::Component::getHierThresh(const Ice::Current&)
233 {
234  return static_cast<double>(m_network.hier_thresh());
235 }
236 
237 void
238 yolo::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 
244 double
245 yolo::Component::getNms(const Ice::Current&)
246 {
247  return static_cast<double>(m_network.nms());
248 }
249 
250 void
251 yolo::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 
257 float
258 yolo::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 
270 void
271 yolo::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 
284 std::vector<std::string>
285 yolo::Component::getClasses(const Ice::Current&)
286 {
287  return m_darknet_classes;
288 }
289 
290 void
291 yolo::Component::restoreDefaults(const Ice::Current&)
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 
300 void
301 yolo::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 
321 void
322 yolo::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.";
386  yolo::Component::renderOutput(
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 
412 std::vector<DetectedObject>
413 yolo::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 
449 void
450 yolo::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 
537 yolo::Component::createPropertyDefinitions()
538 {
539  PropertyDefinitionsPtr defs{new ComponentPropertyDefinitions{getConfigIdentifier()}};
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 }
ARMARX_VERBOSE
#define ARMARX_VERBOSE
Definition: Logging.h:187
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
visionx::yolo
Definition: Component.h:53
armarx::ScopedStopWatch
Definition: ScopedStopWatch.h:57
Component.h
visionx::imrecman::ok
@ ok
Definition: ImageRecordingManagerInterface.ice:45
armarx::RunningTask
Definition: ArmarXMultipleObjectsScheduler.h:36
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:479
visionx::CByteImageUPtr
std::unique_ptr< CByteImage > CByteImageUPtr
Definition: ImageProvider.h:56
max
T max(T t1, T t2)
Definition: gdiam.h:51
armarx::armem::Time
armarx::core::time::DateTime Time
Definition: forward_declarations.h:13
GfxTL::Vec2d
VectorXD< 2, double > Vec2d
Definition: VectorXD.h:736
ExpressionException.h
armarx::ComponentPropertyDefinitions
Default component property definition container.
Definition: Component.h:69
armarx::StopWatch::reset
void reset()
Definition: StopWatch.cpp:118
ARMARX_INFO
#define ARMARX_INFO
Definition: Logging.h:181
IceUtil::Handle< class PropertyDefinitionContainer >
armarx::StopWatch::stop
IceUtil::Time stop()
Definition: StopWatch.cpp:111
angle
double angle(const Point &a, const Point &b, const Point &c)
Definition: point.hpp:109
ARMARX_VERBOSE_S
#define ARMARX_VERBOSE_S
Definition: Logging.h:207
ARMARX_WARNING
#define ARMARX_WARNING
Definition: Logging.h:193
ScopedStopWatch.h
armarx
This file offers overloads of toIce() and fromIce() functions for STL container types.
Definition: ArmarXTimeserver.cpp:27
armarx::StopWatch
Definition: StopWatch.h:118