ROS MelodicでOpenCV(テニスボールの位置検出)

前回ROS MelodicでOpenCV - 機械屋の呟き、ROS上でOpenCVを使う準備をしたので、実際に使ってみます。

サンプルコード

以下のコードをopencv_test.cppに書き、ビルドします。

#include <ros/ros.h>
#include <image_transport/image_transport.h>
#include <cv_bridge/cv_bridge.h>
#include <sensor_msgs/image_encodings.h>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/highgui/highgui.hpp>

static const std::string OPENCV_WINDOW = "Image window";

class ImageConverter{
  ros::NodeHandle nh_;
  image_transport::ImageTransport it_;
  image_transport::Subscriber image_sub_;
  image_transport::Publisher image_pub_;

  public:
    // コンストラクタ
    ImageConverter() : it_(nh_){
      // カラー画像をサブスクライブ                                                                
      image_sub_ = it_.subscribe("/image_raw", 1, &ImageConverter::imageCb, this);
      // 処理した画像をパブリッシュ                                                                                          
      image_pub_ = it_.advertise("/image_topic", 1);
      
      
    }

    // デストラクタ
    ~ImageConverter(){
      cv::destroyWindow(OPENCV_WINDOW);
    }
    // コールバック関数
    void imageCb(const sensor_msgs::ImageConstPtr& msg){
      cv::Point2f center, p1;
      float radius;

      cv_bridge::CvImagePtr cv_ptr, cv_ptr2, cv_ptr3;
      try{
        // ROSからOpenCVの形式にtoCvCopy()で変換。cv_ptr->imageがcv::Matフォーマット。
        cv_ptr = cv_bridge::toCvCopy(msg, sensor_msgs::image_encodings::BGR8);
        cv_ptr3 = cv_bridge::toCvCopy(msg, sensor_msgs::image_encodings::MONO8);
      }catch (cv_bridge::Exception& e){
        ROS_ERROR("cv_bridge exception: %s", e.what());
        return;
      }

      cv::Mat hsv_image, color_mask, gray_image, bin_image, cv_image2, cv_image3;
      // RGB表色系をHSV表色系へ変換して、hsv_imageに格納
      cv::cvtColor(cv_ptr->image, hsv_image, CV_BGR2HSV);

      // 色相(Hue), 彩度(Saturation), 明暗(Value, brightness) 
      // 指定した範囲の色でマスク画像color_mask(CV_8U:符号なし8ビット整数)を生成  
      // マスク画像は指定した範囲の色に該当する要素は255(8ビットすべて1)、それ以外は0                                                      
      //cv::inRange(hsv_image, cv::Scalar(0, 0, 100, 0) , cv::Scalar(180, 45, 255, 0), color_mask);       // 白
      //cv::inRange(hsv_image, cv::Scalar(150, 100, 50, 0) , cv::Scalar(180, 255, 255, 0), color_mask);   // 赤
      cv::inRange(hsv_image, cv::Scalar(20, 50, 50, 0) , cv::Scalar(100, 255, 255, 0), color_mask);   // 黄

      // ビット毎の論理積。マスク画像は指定した範囲以外は0で、指定範囲の要素は255なので、ビット毎の論理積を適用すると、指定した範囲の色に対応する要素はそのままで、他は0になる。
      cv::bitwise_and(cv_ptr->image, cv_ptr->image, cv_image2, color_mask);
      // グレースケールに変換
      cv::cvtColor(cv_image2, gray_image, CV_BGR2GRAY);
      // 閾値70で2値画像に変換
      cv::threshold(gray_image, bin_image, 80, 255, CV_THRESH_BINARY); 

      // エッジを検出するためにCannyアルゴリズムを適用
      //cv::Canny(gray_image, cv_ptr3->image, 15.0, 30.0, 3);

      // ウインドウに円を描画                                                
      //cv::circle(cv_ptr->image, cv::Point(100, 100), 20, CV_RGB(0,255,0));

      // 輪郭を格納するcontoursにfindContours関数に渡すと輪郭を点の集合として入れてくれる
      std::vector<std::vector<cv::Point>> contours;
      cv::findContours(bin_image, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE);    // 輪郭線を格納


      try{
        // 各輪郭をcontourArea関数に渡し、最大面積を持つ輪郭を探す
        double max_area=0;
        int max_area_contour=-1;
        for(int j=0; j<contours.size(); j++){
            double area = cv::contourArea(contours.at(j));
            if(max_area<area){
                max_area=area;
                max_area_contour=j;
            }
        }

        // 最大面積を持つ輪郭の最小外接円を取得
        cv::minEnclosingCircle(contours.at(max_area_contour), center, radius);
        ROS_INFO("radius = %f", radius);

        // 最小外接円を描画
        cv::circle(cv_ptr->image, center, radius, cv::Scalar(0,0,255),3,4);
        cv::circle(cv_image2, center, radius, cv::Scalar(0,0,255),3,4);
        cv::circle(bin_image, center, radius, cv::Scalar(0,0,255),3,4);
      }catch(CvErrorCallback){
        
      }

      // 画面中心から最小外接円の中心へのベクトルを描画
      p1 = cv::Point2f(cv_ptr->image.size().width/2,cv_ptr->image.size().height/2);
      cv::arrowedLine(cv_ptr->image, p1, center, cv::Scalar(0, 255, 0, 0), 3, 8, 0, 0.1);  

      // 画像サイズは縦横1/4に変更
      cv::Mat cv_half_image, cv_half_image2, cv_half_image3, cv_half_image4, cv_half_image5;
      cv::resize(cv_ptr->image, cv_half_image,cv::Size(),0.5,0.5);
      cv::resize(cv_image2, cv_half_image2,cv::Size(),0.5,0.5);
      //cv::resize(cv_ptr3->image, cv_half_image3,cv::Size(),0.5,0.5);
      cv::resize(gray_image, cv_half_image4,cv::Size(),0.5,0.5);
      cv::resize(bin_image, cv_half_image5,cv::Size(),0.5,0.5);

      // ウインドウ表示                                                                         
      cv::imshow("Original Image", cv_half_image);
      cv::imshow("Result Image", cv_half_image2);
      //cv::imshow("Edge Image", cv_half_image3);
      //cv::imshow("Gray Image", cv_half_image4);
      cv::imshow("Binary Image", cv_half_image5);
      cv::waitKey(3);
  
      // エッジ画像をパブリッシュ。OpenCVからROS形式にtoImageMsg()で変換。                                                            
      image_pub_.publish(cv_ptr3->toImageMsg());
    }
};

int main(int argc, char** argv)
{
  ros::init(argc, argv, "image_converter");
  ImageConverter ic;
  ros::spin();
  return 0;
}

実行

$ roscore
$ rosrun uvc_camera uvc_camera_node image:=/image_raw
$ rosrun opencv_tutorials opencv_test