replace_colors_nodelet.cpp
Go to the documentation of this file.
1 // *****************************************************************************
2 //
3 // Copyright (c) 2017, Southwest Research Institute® (SwRI®)
4 // All rights reserved.
5 //
6 // Redistribution and use in source and binary forms, with or without
7 // modification, are permitted provided that the following conditions are met:
8 // * Redistributions of source code must retain the above copyright
9 // notice, this list of conditions and the following disclaimer.
10 // * Redistributions in binary form must reproduce the above copyright
11 // notice, this list of conditions and the following disclaimer in the
12 // documentation and/or other materials provided with the distribution.
13 // * Neither the name of Southwest Research Institute® (SwRI®) nor the
14 // names of its contributors may be used to endorse or promote products
15 // derived from this software without specific prior written permission.
16 //
17 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18 // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19 // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20 // ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
21 // DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22 // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23 // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
24 // ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25 // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
26 // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27 //
28 // *****************************************************************************
29 #include <opencv2/core/version.hpp>
30 #if CV_MAJOR_VERSION == 2
31 #include <opencv2/contrib/contrib.hpp>
32 #endif
33 #include <opencv2/imgproc/imgproc.hpp>
34 #include <ros/ros.h>
36 #include <cv_bridge/cv_bridge.h>
37 #include <nodelet/nodelet.h>
38 #include <sensor_msgs/Image.h>
41 
42 namespace swri_image_util
43 {
44  // This constant defines how large our lookup and transform tables are.
45  // Currently assumes 8 bit mono encoded images, so there are 256 gray colors
46  // to potentially replace with a different color
47  const int32_t NUM_GRAY_VALUES = 256;
48  // The output is an RGB8 image. This constant checks that the user passes
49  // in a valid RGB value to replace a gray level with
50  const int32_t MAX_RGB_VALUE = 255;
51 
52  // ROS nodelet for replacing colors in an image
54  {
55  public:
58 
59  private:
60  // Does the actual initialization since this is a nodelet
61  virtual void onInit();
62  // Callback for the input image
63  void imageCallback(const sensor_msgs::ImageConstPtr& image_msg);
64  // Initialize the lookup table
65  void initLut();
66  // Read in user requested colormap
68  // Helper function for getting color mapping from parameter server
69  void readUserLut(const XmlRpc::XmlRpcValue& param);
70  // Helper function for displaying the type of parameter that was read
71  std::string getValueTypeString(const XmlRpc::XmlRpcValue& value);
72 
73  // Lookup table defining color replacement strategy. The row indices
74  // correspond to the gray scale values, and the values in the rows are RGB
75  // values to replace the gray scale values with
76  cv::Mat color_lut_;
77  // Publishes the modified image
79  // Subscribes to the original image
81  // Mapping from a colormap name to the OpenCV integer representation
82  std::map<std::string, int32_t> colormap_names_;
83  };
84 
86  {
87  // Initialize the colormap name mapping. Every OpenCV colormap should have
88  // a string identifiying it. This allows the node to easily take a user
89  // parameter and convert it to a representation the algorithm can use.
90  // OpenCV 2.x does not have the Parula colormap, so only include this
91  // when OpenCV 3.x is available
92  colormap_names_["autumn"] = cv::COLORMAP_AUTUMN;
93  colormap_names_["bone"] = cv::COLORMAP_BONE;
94  colormap_names_["jet"] = cv::COLORMAP_JET;
95  colormap_names_["winter"] = cv::COLORMAP_WINTER;
96  colormap_names_["rainbow"] = cv::COLORMAP_RAINBOW;
97  colormap_names_["ocean"] = cv::COLORMAP_OCEAN;
98  colormap_names_["summer"] = cv::COLORMAP_SUMMER;
99  colormap_names_["spring"] = cv::COLORMAP_SPRING;
100  colormap_names_["cool"] = cv::COLORMAP_COOL;
101  colormap_names_["hsv"] = cv::COLORMAP_HSV;
102  colormap_names_["pink"] = cv::COLORMAP_PINK;
103  colormap_names_["hot"] = cv::COLORMAP_HOT;
104  #if CV_MAJOR_VERSION == 3
105  colormap_names_["parula"] = cv::COLORMAP_PARULA;
106  #endif
107  }
108 
110  {
111  // Nothing to clean up on destruction
112  }
113 
114  // Initialize the nodelet
116  {
117  // Node handles for interacting with ROS
120 
121  // Lookup table to replace colors with. By default will just convert the
122  // gray scale values to their RGB equivalents. If this node is ever extended
123  // to more than gray scale, this will have to be changed
124  color_lut_ = cv::Mat::zeros(1, NUM_GRAY_VALUES, CV_8UC3);
125  initLut();
126 
127  // Get the array representing the color mapping from the parameter server.
128  // This will be in the format [[u0, [r0, g0, b0]], [u1, [r1, g1, b1]]]
129  // where u is the gray scale value to replace, and r, g, b are the RGB
130  // components to replace it with.
131  XmlRpc::XmlRpcValue color_param;
132 
133  // This node has two different methods of changing gray scale values to
134  // color imagery. The first maps the gray scale values to OpenCV colormaps
135  // This call checks for that option
136  bool colormap_specified = priv_nh.getParam("colormap", color_param);
137  if (colormap_specified)
138  {
139  // Use the colormap the user has requested
140  readColormap(color_param);
141  }
142 
143  // The other option for modifying the grayscale images is to define a
144  // lookup table that maps grayscale levels to user defined RGB values.
145  // This can be used in conjunction with the colormap option to replace
146  // values in the OpenCV colormap with user values. This call checks
147  // if the user is attempting this operation, and reads the lookup table
148  // from the parameter server if necessary
149  if (priv_nh.getParam("colors", color_param))
150  {
151  // Try to read in the lookup values from the parameter server.
152  readUserLut(color_param);
153  }
154  else if (!colormap_specified)
155  {
156  ROS_ERROR("Color transformation was not specified. Images will ");
157  ROS_ERROR("only be converted to their gray scale equivalents");
158  }
159 
160  // Set up the ROS interface
162  image_pub_ = it.advertise("modified_image", 1);
163  image_sub_ = it.subscribe(
164  "image",
165  1,
167  this);
168  }
169 
170  // Callback for getting the input image to change the colors on
172  const sensor_msgs::ImageConstPtr& image_msg)
173  {
174  // Only do the color conversion if someone is subscribing to the data
175  if (image_pub_.getNumSubscribers() == 0)
176  {
177  return;
178  }
179 
180  // This node currently only supports changing gray scale images. Display an
181  // error message if this is not the case, but limit the error reporting rate
182  // to once every minute avoid spamming the user with redundant warnings
183  if (image_msg->encoding != sensor_msgs::image_encodings::MONO8)
184  {
185  ROS_ERROR_THROTTLE(60,
186  "Changing image colors is only supported for MONO8 images");
187  return;
188  }
189 
190  // Convert image data from ROS to OpenCV type
191  cv_bridge::CvImageConstPtr original_image = cv_bridge::toCvShare(image_msg);
192 
193  // Allocate space for the modified image
194  cv::Mat modified_image = cv::Mat::zeros(
195  original_image->image.rows,
196  original_image->image.cols,
197  CV_8UC3);
198 
199  // Do the actual color replacement
201  original_image->image,
202  color_lut_,
203  modified_image);
204 
205  // Copy results to output message and set up the header and encoding values
206  cv_bridge::CvImagePtr output = boost::make_shared<cv_bridge::CvImage>();
207  output->image = modified_image;
208  output->encoding = sensor_msgs::image_encodings::RGB8;
209  output->header = image_msg->header;
210 
211  // Publish the modified image to the rest of the system
212  image_pub_.publish(output->toImageMsg());
213  }
214 
215  // Initialize grayscale lookup table
217  {
218  // Sets every row in the lookup table to a triple <x, x, x>,
219  // where x is the grayscale value. This will directly map
220  // the gray values to their equivalent RGB representation
221  for (uint32_t idx; idx < NUM_GRAY_VALUES; idx++)
222  {
223  color_lut_.at<cv::Vec3b>(0, idx) = cv::Vec3b(idx, idx, idx);
224  }
225  }
226 
227  // Read in the colormap and parameters the parameter server has for this
228  // node
230  {
231  // This is a multistep process, and if any step goes wrong then the rest
232  // of the process should be aborted.
233  bool success = true;
234 
235  // The colormap parameters should be formatted like:
236  // ["colormap_name", num_colors]
237  // This checks to make sure the parameter is a two element array
238  if ((param.getType() != XmlRpc::XmlRpcValue::TypeArray) ||
239  (param.size() != 2))
240  {
241  ROS_ERROR("Colormap specification must be a two entry array with the");
242  ROS_ERROR("Following format:");
243  ROS_ERROR("\t[\"colormap_name\", num_colors]");
244  success = false;
245  }
246 
247  // These are the parameters to read from the parameter server
248  std::string colormap_name;
249  int32_t num_entries;
250  int32_t colormap_idx;
251 
252  if (success)
253  {
254  // Get the XmpRpc representation of the colormap name the number of
255  // colors to get from that colormap
256  XmlRpc::XmlRpcValue colormap_param = param[0];
257  XmlRpc::XmlRpcValue num_entries_param = param[1];
258 
259  // Make sure the colormap name is a string
260  if (colormap_param.getType() != XmlRpc::XmlRpcValue::TypeString)
261  {
262  ROS_ERROR("First colormap parameter must be a string");
263  success = false;
264  }
265 
266  // Make sure the number of colors to get from the colormap is
267  // an integer
268  if (num_entries_param.getType() != XmlRpc::XmlRpcValue::TypeInt)
269  {
270  ROS_ERROR("Second colormap parameter must be an integer");
271  success = false;
272  }
273 
274  // Parameters had the correct types. Now XmlRpc datatypes to C++ types
275  if (success)
276  {
277  colormap_name = static_cast<std::string>(colormap_param);
278  num_entries = static_cast<int32_t>(num_entries_param);
279  }
280  }
281 
282  // Sanity check on the number of classes the user specified
283  if (success && (num_entries <= 1))
284  {
285  ROS_ERROR("Must use at least two colors from the colormap");
286  success = false;
287  }
288 
289  // Make sure the number of values from the colormap is at most the number
290  // of grayscale values in our transformation
291  if (success)
292  {
293  if (num_entries > NUM_GRAY_VALUES)
294  {
295  ROS_ERROR("Number of colormap entries was greater");
296  ROS_ERROR(" than %d", NUM_GRAY_VALUES);
297  success = false;
298  }
299  }
300 
301  // Get the OpenCV representation of the requested colormap
302  if (success)
303  {
304  std::map<std::string, int32_t>::iterator iter =
305  colormap_names_.find(colormap_name);
306  if (iter != colormap_names_.end())
307  {
308  colormap_idx = colormap_names_[colormap_name];
309  }
310  else
311  {
312  ROS_ERROR("Unknown colormap: %s requested", colormap_name.c_str());
313  success = false;
314  }
315  }
316 
317  // Now get the specified number of colors from the requested colormap
318  if (success)
319  {
320  // Make a copy of the grayscale LUT as a working variable
321  cv::Mat original_colors = color_lut_.clone();
322  // color_lut_ will have the transformation from the grayscale values
323  // to the RGB values after this call for every grayscale value
324  cv::applyColorMap(original_colors, color_lut_, colormap_idx);
325 
326  // Now modify the original colormap to only have the number
327  // of distinct entries specified by the user
328  original_colors = color_lut_.clone();
329 
330  int32_t lut_size = color_lut_.cols;
331 
332  // Frequently the input image may have some small subset of values,
333  // like 0-5. In this case, just mapping to a colormap will make the
334  // resulting image look like one color, because the first 6 colors from
335  // the colormap will be used, which for most colormaps are almost the
336  // same value. This will more intelligently remap this values, so that
337  // the 0, 50, 100, 150, 200, 250 color indices are used from the
338  // colormap. This "pushes" the color values apart to make them more
339  // visually apparent.
340  for (int32_t replace_idx = 0; replace_idx < lut_size; replace_idx++)
341  {
342  int32_t lookup_idx = replace_idx % num_entries;
343  lookup_idx = lookup_idx * lut_size / num_entries;
344  color_lut_.at<cv::Vec3b>(0, static_cast<uint8_t>(replace_idx)) =
345  original_colors.at<cv::Vec3b>(0, static_cast<uint8_t>(lookup_idx));
346  }
347  }
348  }
349 
350  // Read the user lookup table from the parameter server
352  {
353  // Assume the parameter parsing works by default
354  bool success = true;
356  {
357  ROS_ERROR("LUT must be an array, but type was: %s",
358  getValueTypeString(param).c_str());
359  success = false;
360  }
361 
362  // Make a copy of the current LUT. The copy will be modified, and only
363  // if the complete parameter reading works will the real LUT be modified.
364  cv::Mat temp_lut = color_lut_.clone();
365 
366  // Loop over all values that will be replaced. The casting in here is
367  // complicated because XmlRpc cannot go directly from the parameters to
368  // uint8_t values, so they must first be cast to int32_t, and then to the
369  // smaller value
370  for (uint32_t lut_idx = 0; success && (lut_idx < param.size()); lut_idx++)
371  {
372  // Each row will be of the form [key, [r, g, b]]. The next two checks make
373  // sure the right type and number of entries was passed in
374  XmlRpc::XmlRpcValue lut_row = param[lut_idx];
375  if (lut_row.getType() != XmlRpc::XmlRpcValue::TypeArray)
376  {
377  ROS_ERROR("LUT entries must be two entry arrays with the following");
378  ROS_ERROR("format:");
379  ROS_ERROR("\t[gray_value, [r, g, b]]");
380  ROS_ERROR("but for entry %d a %s was given",
381  lut_idx, getValueTypeString(lut_row).c_str());
382  success = false;
383  break;
384  }
385 
386  if (lut_row.size() != 2)
387  {
388  ROS_ERROR("LUT entries must be two entry arrays with the following");
389  ROS_ERROR("format:");
390  ROS_ERROR("\t[gray_value, [r, g, b]]");
391  ROS_ERROR("but entry %d had an array with only %d values",
392  lut_idx, lut_row.size());
393  success = false;
394  break;
395  }
396 
397  // The first element in the gray scale value to replace. Make sure it
398  // is of the appropriate type
399  XmlRpc::XmlRpcValue map_key = lut_row[0];
400  if (map_key.getType() != XmlRpc::XmlRpcValue::TypeInt)
401  {
402  ROS_ERROR("Color to replace must be an integer, but a %s was given for",
403  getValueTypeString(map_key).c_str());
404  ROS_ERROR("entry %d", lut_idx);
405  success = false;
406  break;
407  }
408 
409  // Make sure the gray scale index is in the proper range
410  int32_t gray_index = static_cast<int32_t>(map_key);
411  if ((gray_index >= NUM_GRAY_VALUES) || (gray_index < 0))
412  {
413  ROS_ERROR("Gray scale value for LUT entry %d was %d, but must be",
414  lut_idx, gray_index);
415  ROS_ERROR("between 0 and %d", NUM_GRAY_VALUES - 1);
416  success = false;
417  break;
418  }
419 
420  // Convert to the value used to index into the LUT
421  uint8_t color_index = static_cast<uint8_t>(gray_index);
422 
423  // Now read the RGB values. There must be three of them, they must be
424  // integers, and less than MAX_RGB_VALUE
425  XmlRpc::XmlRpcValue rgb_values = lut_row[1];
426  if (rgb_values.getType() != XmlRpc::XmlRpcValue::TypeArray)
427  {
428  ROS_ERROR("RGB entries must be three entry arrays of integers, but");
429  ROS_ERROR("LUT row %d was a %s", lut_idx,
430  getValueTypeString(rgb_values).c_str());
431  success = false;
432  break;
433  }
434 
435  if (rgb_values.size() != 3)
436  {
437  ROS_ERROR("RGB entries must be three entry arrays of integers, but");
438  ROS_ERROR("LUT row %d had %d entries", lut_idx, rgb_values.size());
439  success = false;
440  break;
441  }
442 
443  if (rgb_values[0].getType() != XmlRpc::XmlRpcValue::TypeInt)
444  {
445  ROS_ERROR("RGB entries must be three entry arrays of integers, but");
446  ROS_ERROR("LUT row %d had a red value with type %s", lut_idx,
447  getValueTypeString(rgb_values[0]).c_str());
448  success = false;
449  break;
450  }
451 
452  if (rgb_values[1].getType() != XmlRpc::XmlRpcValue::TypeInt)
453  {
454  ROS_ERROR("RGB entries must be three entry arrays of integers, but");
455  ROS_ERROR("LUT row %d had a green value with type %s", lut_idx,
456  getValueTypeString(rgb_values[1]).c_str());
457  success = false;
458  break;
459  }
460 
461  if (rgb_values[2].getType() != XmlRpc::XmlRpcValue::TypeInt)
462  {
463  ROS_ERROR("RGB entries must be three entry arrays of integers, but");
464  ROS_ERROR("LUT row %d had a blue value with type %s", lut_idx,
465  getValueTypeString(rgb_values[2]).c_str());
466  success = false;
467  break;
468  }
469 
470  // Make sure the RGB values are between 0 and MAX_RGB_VALUE
471  int32_t original_red = static_cast<int32_t>(rgb_values[0]);
472  if ((original_red > MAX_RGB_VALUE) || (original_red < 0))
473  {
474  ROS_ERROR("Red value on row %d was %d, and must be between 0 and %d",
475  lut_idx, original_red, MAX_RGB_VALUE);
476  success = false;
477  break;
478  }
479  int32_t original_green = static_cast<int32_t>(rgb_values[1]);
480  if ((original_green > MAX_RGB_VALUE) || (original_green < 0))
481  {
482  ROS_ERROR("Green value on row %d was %d, and must be between 0 and %d",
483  lut_idx, original_green, MAX_RGB_VALUE);
484  success = false;
485  break;
486  }
487  int32_t original_blue = static_cast<int32_t>(rgb_values[2]);
488  if ((original_blue > MAX_RGB_VALUE) || (original_blue < 0))
489  {
490  ROS_ERROR("Blue value on row %d was %d, and must be between 0 and %d",
491  lut_idx, original_blue, MAX_RGB_VALUE);
492  success = false;
493  break;
494  }
495 
496  // make the RGB values the correct type
497  uint8_t red = static_cast<uint8_t>(original_red);
498  uint8_t green = static_cast<uint8_t>(original_green);
499  uint8_t blue = static_cast<uint8_t>(original_blue);
500 
501  // Convert the RGB values to OpenCV types and put them in the LUT
502  cv::Vec3b rgb_entry(red, green, blue);
503  temp_lut.at<cv::Vec3b>(0, color_index) = rgb_entry;
504  }
505 
506  // If the parametre were successfully read the modified LUT will be
507  // copied back to the persistent LUT.
508  if (success)
509  {
510  color_lut_ = temp_lut.clone();
511  }
512  }
513 
515  const XmlRpc::XmlRpcValue& value)
516  {
517  std::string type_str = "unknown";
518  switch(value.getType())
519  {
521  type_str = "invalid";
522  break;
523 
525  type_str = "boolean";
526  break;
527 
529  type_str = "int";
530  break;
531 
533  type_str = "double";
534  break;
535 
537  type_str = "string";
538  break;
539 
541  type_str = "date time";
542  break;
543 
545  type_str = "base 64";
546  break;
547 
549  type_str = "array";
550  break;
551 
553  type_str = "struct";
554  break;
555 
556  default:
557  ROS_ERROR("Unknown XML RPC value type encountered");
558  type_str = "unknown";
559  break;
560  }
561 
562  return type_str;
563  }
564 }
565 
566 // Register nodelet plugin
CvImageConstPtr toCvShare(const sensor_msgs::ImageConstPtr &source, const std::string &encoding=std::string())
Subscriber subscribe(const std::string &base_topic, uint32_t queue_size, const boost::function< void(const sensor_msgs::ImageConstPtr &)> &callback, const ros::VoidPtr &tracked_object=ros::VoidPtr(), const TransportHints &transport_hints=TransportHints())
const int32_t NUM_GRAY_VALUES
bool param(const std::string &param_name, T &param_val, const T &default_val)
void replaceColors(const cv::Mat &original_image, const cv::Mat &lut, cv::Mat &modified_image)
void readColormap(const XmlRpc::XmlRpcValue &param)
int size() const
void imageCallback(const sensor_msgs::ImageConstPtr &image_msg)
Publisher advertise(const std::string &base_topic, uint32_t queue_size, bool latch=false)
Type const & getType() const
uint32_t getNumSubscribers() const
ros::NodeHandle & getPrivateNodeHandle() const
std::map< std::string, int32_t > colormap_names_
void publish(const sensor_msgs::Image &message) const
SWRI_NODELET_EXPORT_CLASS(swri_image_util, DrawPolygonNodelet)
#define ROS_ERROR_THROTTLE(period,...)
ros::NodeHandle & getNodeHandle() const
void readUserLut(const XmlRpc::XmlRpcValue &param)
bool getParam(const std::string &key, std::string &s) const
std::string getValueTypeString(const XmlRpc::XmlRpcValue &value)
#define ROS_ERROR(...)


swri_image_util
Author(s): Kris Kozak
autogenerated on Fri Jun 7 2019 22:05:56