00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033 #include <rqt_image_view/image_view.h>
00034
00035 #include <pluginlib/class_list_macros.h>
00036 #include <ros/master.h>
00037 #include <sensor_msgs/image_encodings.h>
00038
00039 #include <cv_bridge/cv_bridge.h>
00040 #include <opencv2/imgproc/imgproc.hpp>
00041
00042 #include <QFileDialog>
00043 #include <QMessageBox>
00044 #include <QPainter>
00045
00046 namespace rqt_image_view {
00047
00048 ImageView::ImageView()
00049 : rqt_gui_cpp::Plugin()
00050 , widget_(0)
00051 , num_gridlines_(0)
00052 , rotate_state_(ROTATE_0)
00053 {
00054 setObjectName("ImageView");
00055 }
00056
00057 void ImageView::initPlugin(qt_gui_cpp::PluginContext& context)
00058 {
00059 widget_ = new QWidget();
00060 ui_.setupUi(widget_);
00061
00062 if (context.serialNumber() > 1)
00063 {
00064 widget_->setWindowTitle(widget_->windowTitle() + " (" + QString::number(context.serialNumber()) + ")");
00065 }
00066 context.addWidget(widget_);
00067
00068 updateTopicList();
00069 ui_.topics_combo_box->setCurrentIndex(ui_.topics_combo_box->findText(""));
00070 connect(ui_.topics_combo_box, SIGNAL(currentIndexChanged(int)), this, SLOT(onTopicChanged(int)));
00071
00072 ui_.refresh_topics_push_button->setIcon(QIcon::fromTheme("view-refresh"));
00073 connect(ui_.refresh_topics_push_button, SIGNAL(pressed()), this, SLOT(updateTopicList()));
00074
00075 ui_.zoom_1_push_button->setIcon(QIcon::fromTheme("zoom-original"));
00076 connect(ui_.zoom_1_push_button, SIGNAL(toggled(bool)), this, SLOT(onZoom1(bool)));
00077
00078 connect(ui_.dynamic_range_check_box, SIGNAL(toggled(bool)), this, SLOT(onDynamicRange(bool)));
00079
00080 ui_.save_as_image_push_button->setIcon(QIcon::fromTheme("document-save-as"));
00081 connect(ui_.save_as_image_push_button, SIGNAL(pressed()), this, SLOT(saveImage()));
00082
00083 connect(ui_.num_gridlines_spin_box, SIGNAL(valueChanged(int)), this, SLOT(updateNumGridlines()));
00084
00085
00086 const QStringList& argv = context.argv();
00087 if (!argv.empty()) {
00088 arg_topic_name = argv[0];
00089 selectTopic(arg_topic_name);
00090 }
00091 pub_topic_custom_ = false;
00092
00093 ui_.image_frame->setOuterLayout(ui_.image_layout);
00094
00095 QRegExp rx("([a-zA-Z/][a-zA-Z0-9_/]*)?");
00096 ui_.publish_click_location_topic_line_edit->setValidator(new QRegExpValidator(rx, this));
00097 connect(ui_.publish_click_location_check_box, SIGNAL(toggled(bool)), this, SLOT(onMousePublish(bool)));
00098 connect(ui_.image_frame, SIGNAL(mouseLeft(int, int)), this, SLOT(onMouseLeft(int, int)));
00099 connect(ui_.publish_click_location_topic_line_edit, SIGNAL(editingFinished()), this, SLOT(onPubTopicChanged()));
00100
00101 connect(ui_.smooth_image_check_box, SIGNAL(toggled(bool)), ui_.image_frame, SLOT(onSmoothImageChanged(bool)));
00102
00103 connect(ui_.rotate_left_push_button, SIGNAL(clicked(bool)), this, SLOT(onRotateLeft()));
00104 connect(ui_.rotate_right_push_button, SIGNAL(clicked(bool)), this, SLOT(onRotateRight()));
00105
00106
00107 ui_.rotate_label->setMinimumWidth(
00108 ui_.rotate_label->fontMetrics().width("XXX°")
00109 );
00110
00111 hide_toolbar_action_ = new QAction(tr("Hide toolbar"), this);
00112 hide_toolbar_action_->setCheckable(true);
00113 ui_.image_frame->addAction(hide_toolbar_action_);
00114 connect(hide_toolbar_action_, SIGNAL(toggled(bool)), this, SLOT(onHideToolbarChanged(bool)));
00115 }
00116
00117 void ImageView::shutdownPlugin()
00118 {
00119 subscriber_.shutdown();
00120 pub_mouse_left_.shutdown();
00121 }
00122
00123 void ImageView::saveSettings(qt_gui_cpp::Settings& plugin_settings, qt_gui_cpp::Settings& instance_settings) const
00124 {
00125 QString topic = ui_.topics_combo_box->currentText();
00126
00127 instance_settings.setValue("topic", topic);
00128 instance_settings.setValue("zoom1", ui_.zoom_1_push_button->isChecked());
00129 instance_settings.setValue("dynamic_range", ui_.dynamic_range_check_box->isChecked());
00130 instance_settings.setValue("max_range", ui_.max_range_double_spin_box->value());
00131 instance_settings.setValue("publish_click_location", ui_.publish_click_location_check_box->isChecked());
00132 instance_settings.setValue("mouse_pub_topic", ui_.publish_click_location_topic_line_edit->text());
00133 instance_settings.setValue("toolbar_hidden", hide_toolbar_action_->isChecked());
00134 instance_settings.setValue("num_gridlines", ui_.num_gridlines_spin_box->value());
00135 instance_settings.setValue("smooth_image", ui_.smooth_image_check_box->isChecked());
00136 instance_settings.setValue("rotate", rotate_state_);
00137 }
00138
00139 void ImageView::restoreSettings(const qt_gui_cpp::Settings& plugin_settings, const qt_gui_cpp::Settings& instance_settings)
00140 {
00141 bool zoom1_checked = instance_settings.value("zoom1", false).toBool();
00142 ui_.zoom_1_push_button->setChecked(zoom1_checked);
00143
00144 bool dynamic_range_checked = instance_settings.value("dynamic_range", false).toBool();
00145 ui_.dynamic_range_check_box->setChecked(dynamic_range_checked);
00146
00147 double max_range = instance_settings.value("max_range", ui_.max_range_double_spin_box->value()).toDouble();
00148 ui_.max_range_double_spin_box->setValue(max_range);
00149
00150 num_gridlines_ = instance_settings.value("num_gridlines", ui_.num_gridlines_spin_box->value()).toInt();
00151 ui_.num_gridlines_spin_box->setValue(num_gridlines_);
00152
00153 QString topic = instance_settings.value("topic", "").toString();
00154
00155 if (!arg_topic_name.isEmpty())
00156 {
00157 arg_topic_name = "";
00158 }
00159 else
00160 {
00161
00162 selectTopic(topic);
00163 }
00164
00165 bool publish_click_location = instance_settings.value("publish_click_location", false).toBool();
00166 ui_.publish_click_location_check_box->setChecked(publish_click_location);
00167
00168 QString pub_topic = instance_settings.value("mouse_pub_topic", "").toString();
00169 ui_.publish_click_location_topic_line_edit->setText(pub_topic);
00170
00171 bool toolbar_hidden = instance_settings.value("toolbar_hidden", false).toBool();
00172 hide_toolbar_action_->setChecked(toolbar_hidden);
00173
00174 bool smooth_image_checked = instance_settings.value("smooth_image", false).toBool();
00175 ui_.smooth_image_check_box->setChecked(smooth_image_checked);
00176
00177 rotate_state_ = static_cast<RotateState>(instance_settings.value("rotate", 0).toInt());
00178 if(rotate_state_ >= ROTATE_STATE_COUNT)
00179 rotate_state_ = ROTATE_0;
00180 syncRotateLabel();
00181 }
00182
00183 void ImageView::updateTopicList()
00184 {
00185 QSet<QString> message_types;
00186 message_types.insert("sensor_msgs/Image");
00187 QSet<QString> message_sub_types;
00188 message_sub_types.insert("sensor_msgs/CompressedImage");
00189
00190
00191 QList<QString> transports;
00192 image_transport::ImageTransport it(getNodeHandle());
00193 std::vector<std::string> declared = it.getDeclaredTransports();
00194 for (std::vector<std::string>::const_iterator it = declared.begin(); it != declared.end(); it++)
00195 {
00196
00197 QString transport = it->c_str();
00198
00199
00200 QString prefix = "image_transport/";
00201 if (transport.startsWith(prefix))
00202 {
00203 transport = transport.mid(prefix.length());
00204 }
00205 transports.append(transport);
00206 }
00207
00208 QString selected = ui_.topics_combo_box->currentText();
00209
00210
00211 QList<QString> topics = getTopics(message_types, message_sub_types, transports).values();
00212 topics.append("");
00213 qSort(topics);
00214 ui_.topics_combo_box->clear();
00215 for (QList<QString>::const_iterator it = topics.begin(); it != topics.end(); it++)
00216 {
00217 QString label(*it);
00218 label.replace(" ", "/");
00219 ui_.topics_combo_box->addItem(label, QVariant(*it));
00220 }
00221
00222
00223 selectTopic(selected);
00224 }
00225
00226 QList<QString> ImageView::getTopicList(const QSet<QString>& message_types, const QList<QString>& transports)
00227 {
00228 QSet<QString> message_sub_types;
00229 return getTopics(message_types, message_sub_types, transports).values();
00230 }
00231
00232 QSet<QString> ImageView::getTopics(const QSet<QString>& message_types, const QSet<QString>& message_sub_types, const QList<QString>& transports)
00233 {
00234 ros::master::V_TopicInfo topic_info;
00235 ros::master::getTopics(topic_info);
00236
00237 QSet<QString> all_topics;
00238 for (ros::master::V_TopicInfo::const_iterator it = topic_info.begin(); it != topic_info.end(); it++)
00239 {
00240 all_topics.insert(it->name.c_str());
00241 }
00242
00243 QSet<QString> topics;
00244 for (ros::master::V_TopicInfo::const_iterator it = topic_info.begin(); it != topic_info.end(); it++)
00245 {
00246 if (message_types.contains(it->datatype.c_str()))
00247 {
00248 QString topic = it->name.c_str();
00249
00250
00251 topics.insert(topic);
00252
00253
00254
00255 for (QList<QString>::const_iterator jt = transports.begin(); jt != transports.end(); jt++)
00256 {
00257 if (all_topics.contains(topic + "/" + *jt))
00258 {
00259 QString sub = topic + " " + *jt;
00260 topics.insert(sub);
00261
00262 }
00263 }
00264 }
00265 if (message_sub_types.contains(it->datatype.c_str()))
00266 {
00267 QString topic = it->name.c_str();
00268 int index = topic.lastIndexOf("/");
00269 if (index != -1)
00270 {
00271 topic.replace(index, 1, " ");
00272 topics.insert(topic);
00273
00274 }
00275 }
00276 }
00277 return topics;
00278 }
00279
00280 void ImageView::selectTopic(const QString& topic)
00281 {
00282 int index = ui_.topics_combo_box->findText(topic);
00283 if (index == -1)
00284 {
00285
00286 QString label(topic);
00287 label.replace(" ", "/");
00288 ui_.topics_combo_box->addItem(label, QVariant(topic));
00289 index = ui_.topics_combo_box->findText(topic);
00290 }
00291 ui_.topics_combo_box->setCurrentIndex(index);
00292 }
00293
00294 void ImageView::onTopicChanged(int index)
00295 {
00296 subscriber_.shutdown();
00297
00298
00299 ui_.image_frame->setImage(QImage());
00300
00301 QStringList parts = ui_.topics_combo_box->itemData(index).toString().split(" ");
00302 QString topic = parts.first();
00303 QString transport = parts.length() == 2 ? parts.last() : "raw";
00304
00305 if (!topic.isEmpty())
00306 {
00307 image_transport::ImageTransport it(getNodeHandle());
00308 image_transport::TransportHints hints(transport.toStdString());
00309 try {
00310 subscriber_ = it.subscribe(topic.toStdString(), 1, &ImageView::callbackImage, this, hints);
00311
00312 } catch (image_transport::TransportLoadException& e) {
00313 QMessageBox::warning(widget_, tr("Loading image transport plugin failed"), e.what());
00314 }
00315 }
00316
00317 onMousePublish(ui_.publish_click_location_check_box->isChecked());
00318 }
00319
00320 void ImageView::onZoom1(bool checked)
00321 {
00322 if (checked)
00323 {
00324 if (ui_.image_frame->getImage().isNull())
00325 {
00326 return;
00327 }
00328 ui_.image_frame->setInnerFrameFixedSize(ui_.image_frame->getImage().size());
00329 } else {
00330 ui_.image_frame->setInnerFrameMinimumSize(QSize(80, 60));
00331 ui_.image_frame->setMaximumSize(QSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX));
00332 widget_->setMinimumSize(QSize(80, 60));
00333 widget_->setMaximumSize(QSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX));
00334 }
00335 }
00336
00337 void ImageView::onDynamicRange(bool checked)
00338 {
00339 ui_.max_range_double_spin_box->setEnabled(!checked);
00340 }
00341
00342 void ImageView::updateNumGridlines()
00343 {
00344 num_gridlines_ = ui_.num_gridlines_spin_box->value();
00345 }
00346
00347 void ImageView::saveImage()
00348 {
00349
00350 QImage img = ui_.image_frame->getImageCopy();
00351
00352 QString file_name = QFileDialog::getSaveFileName(widget_, tr("Save as image"), "image.png", tr("Image (*.bmp *.jpg *.png *.tiff)"));
00353 if (file_name.isEmpty())
00354 {
00355 return;
00356 }
00357
00358 img.save(file_name);
00359 }
00360
00361 void ImageView::onMousePublish(bool checked)
00362 {
00363 std::string topicName;
00364 if(pub_topic_custom_)
00365 {
00366 topicName = ui_.publish_click_location_topic_line_edit->text().toStdString();
00367 } else {
00368 if(!subscriber_.getTopic().empty())
00369 {
00370 topicName = subscriber_.getTopic()+"_mouse_left";
00371 } else {
00372 topicName = "mouse_left";
00373 }
00374 ui_.publish_click_location_topic_line_edit->setText(QString::fromStdString(topicName));
00375 }
00376
00377 if(checked)
00378 {
00379 pub_mouse_left_ = getNodeHandle().advertise<geometry_msgs::Point>(topicName, 1000);
00380 } else {
00381 pub_mouse_left_.shutdown();
00382 }
00383 }
00384
00385 void ImageView::onMouseLeft(int x, int y)
00386 {
00387 if(ui_.publish_click_location_check_box->isChecked() && !ui_.image_frame->getImage().isNull())
00388 {
00389 geometry_msgs::Point clickCanvasLocation;
00390
00391 clickCanvasLocation.x = round((double)x/(double)ui_.image_frame->width()*(double)ui_.image_frame->getImage().width());
00392 clickCanvasLocation.y = round((double)y/(double)ui_.image_frame->height()*(double)ui_.image_frame->getImage().height());
00393 clickCanvasLocation.z = 0;
00394
00395 geometry_msgs::Point clickLocation = clickCanvasLocation;
00396
00397 switch(rotate_state_)
00398 {
00399 case ROTATE_90:
00400 clickLocation.x = clickCanvasLocation.y;
00401 clickLocation.y = ui_.image_frame->getImage().width() - clickCanvasLocation.x;
00402 break;
00403 case ROTATE_180:
00404 clickLocation.x = ui_.image_frame->getImage().width() - clickCanvasLocation.x;
00405 clickLocation.y = ui_.image_frame->getImage().height() - clickCanvasLocation.y;
00406 break;
00407 case ROTATE_270:
00408 clickLocation.x = ui_.image_frame->getImage().height() - clickCanvasLocation.y;
00409 clickLocation.y = clickCanvasLocation.x;
00410 break;
00411 default:
00412 break;
00413 }
00414
00415 pub_mouse_left_.publish(clickLocation);
00416 }
00417 }
00418
00419 void ImageView::onPubTopicChanged()
00420 {
00421 pub_topic_custom_ = !(ui_.publish_click_location_topic_line_edit->text().isEmpty());
00422 onMousePublish(ui_.publish_click_location_check_box->isChecked());
00423 }
00424
00425 void ImageView::onHideToolbarChanged(bool hide)
00426 {
00427 ui_.toolbar_widget->setVisible(!hide);
00428 }
00429
00430 void ImageView::onRotateLeft()
00431 {
00432 int m = rotate_state_ - 1;
00433 if(m < 0)
00434 m = ROTATE_STATE_COUNT-1;
00435
00436 rotate_state_ = static_cast<RotateState>(m);
00437 syncRotateLabel();
00438 }
00439
00440 void ImageView::onRotateRight()
00441 {
00442 rotate_state_ = static_cast<RotateState>((rotate_state_ + 1) % ROTATE_STATE_COUNT);
00443 syncRotateLabel();
00444 }
00445
00446 void ImageView::syncRotateLabel()
00447 {
00448 switch(rotate_state_)
00449 {
00450 default:
00451 case ROTATE_0: ui_.rotate_label->setText("0°"); break;
00452 case ROTATE_90: ui_.rotate_label->setText("90°"); break;
00453 case ROTATE_180: ui_.rotate_label->setText("180°"); break;
00454 case ROTATE_270: ui_.rotate_label->setText("270°"); break;
00455 }
00456 }
00457
00458 void ImageView::invertPixels(int x, int y)
00459 {
00460
00461 cv::Vec3b & pixel = conversion_mat_.at<cv::Vec3b>(cv::Point(x, y));
00462 if (pixel[0] + pixel[1] + pixel[2] > 3 * 127)
00463 pixel = cv::Vec3b(0,0,0);
00464 else
00465 pixel = cv::Vec3b(255,255,255);
00466 }
00467
00468 QList<int> ImageView::getGridIndices(int size) const
00469 {
00470 QList<int> indices;
00471
00472
00473 float grid_width = 1.0f * size / (num_gridlines_ + 1);
00474
00475
00476 float index;
00477 if (num_gridlines_ % 2)
00478 {
00479 indices.append(size / 2);
00480
00481 if (size % 2 == 0)
00482 indices.append(size / 2 - 1);
00483 index = 1.0f * (size - 1) / 2;
00484 }
00485 else
00486 {
00487 index = grid_width * (num_gridlines_ / 2);
00488
00489 indices.append(round(index));
00490
00491 indices.append(size - 1 - round(index));
00492 }
00493
00494
00495 int lines = (num_gridlines_ - 1) / 2;
00496 while (lines > 0)
00497 {
00498 index -= grid_width;
00499 indices.append(round(index));
00500 indices.append(size - 1 - round(index));
00501 lines--;
00502 }
00503
00504 return indices;
00505 }
00506
00507 void ImageView::overlayGrid()
00508 {
00509
00510 QList<int> columns = getGridIndices(conversion_mat_.cols);
00511 for (QList<int>::const_iterator x = columns.begin(); x != columns.end(); ++x)
00512 {
00513 for (int y = 0; y < conversion_mat_.rows; ++y)
00514 {
00515 invertPixels(*x, y);
00516 }
00517 }
00518
00519
00520 QList<int> rows = getGridIndices(conversion_mat_.rows);
00521 for (QList<int>::const_iterator y = rows.begin(); y != rows.end(); ++y)
00522 {
00523 for (int x = 0; x < conversion_mat_.cols; ++x)
00524 {
00525 invertPixels(x, *y);
00526 }
00527 }
00528 }
00529
00530 void ImageView::callbackImage(const sensor_msgs::Image::ConstPtr& msg)
00531 {
00532 try
00533 {
00534
00535 cv_bridge::CvImageConstPtr cv_ptr = cv_bridge::toCvShare(msg, sensor_msgs::image_encodings::RGB8);
00536 conversion_mat_ = cv_ptr->image;
00537
00538 if (num_gridlines_ > 0)
00539 overlayGrid();
00540 }
00541 catch (cv_bridge::Exception& e)
00542 {
00543 try
00544 {
00545
00546 cv_bridge::CvImageConstPtr cv_ptr = cv_bridge::toCvShare(msg);
00547 if (msg->encoding == "CV_8UC3")
00548 {
00549
00550 conversion_mat_ = cv_ptr->image;
00551 } else if (msg->encoding == "8UC1") {
00552
00553 cv::cvtColor(cv_ptr->image, conversion_mat_, CV_GRAY2RGB);
00554 } else if (msg->encoding == "16UC1" || msg->encoding == "32FC1") {
00555
00556 double min = 0;
00557 double max = ui_.max_range_double_spin_box->value();
00558 if (msg->encoding == "16UC1") max *= 1000;
00559 if (ui_.dynamic_range_check_box->isChecked())
00560 {
00561
00562 cv::minMaxLoc(cv_ptr->image, &min, &max);
00563 if (min == max) {
00564
00565 min = 0;
00566 max = 2;
00567 }
00568 }
00569 cv::Mat img_scaled_8u;
00570 cv::Mat(cv_ptr->image-min).convertTo(img_scaled_8u, CV_8UC1, 255. / (max - min));
00571 cv::cvtColor(img_scaled_8u, conversion_mat_, CV_GRAY2RGB);
00572 } else {
00573 qWarning("ImageView.callback_image() could not convert image from '%s' to 'rgb8' (%s)", msg->encoding.c_str(), e.what());
00574 ui_.image_frame->setImage(QImage());
00575 return;
00576 }
00577 }
00578 catch (cv_bridge::Exception& e)
00579 {
00580 qWarning("ImageView.callback_image() while trying to convert image from '%s' to 'rgb8' an exception was thrown (%s)", msg->encoding.c_str(), e.what());
00581 ui_.image_frame->setImage(QImage());
00582 return;
00583 }
00584 }
00585
00586
00587 switch(rotate_state_)
00588 {
00589 case ROTATE_90:
00590 {
00591 cv::Mat tmp;
00592 cv::transpose(conversion_mat_, tmp);
00593 cv::flip(tmp, conversion_mat_, 1);
00594 break;
00595 }
00596 case ROTATE_180:
00597 {
00598 cv::Mat tmp;
00599 cv::flip(conversion_mat_, tmp, -1);
00600 conversion_mat_ = tmp;
00601 break;
00602 }
00603 case ROTATE_270:
00604 {
00605 cv::Mat tmp;
00606 cv::transpose(conversion_mat_, tmp);
00607 cv::flip(tmp, conversion_mat_, 0);
00608 break;
00609 }
00610 default:
00611 break;
00612 }
00613
00614
00615 QImage image(conversion_mat_.data, conversion_mat_.cols, conversion_mat_.rows, conversion_mat_.step[0], QImage::Format_RGB888);
00616 ui_.image_frame->setImage(image);
00617
00618 if (!ui_.zoom_1_push_button->isEnabled())
00619 {
00620 ui_.zoom_1_push_button->setEnabled(true);
00621 }
00622
00623
00624 onZoom1(ui_.zoom_1_push_button->isChecked());
00625 }
00626 }
00627
00628 PLUGINLIB_EXPORT_CLASS(rqt_image_view::ImageView, rqt_gui_cpp::Plugin)