コンテンツへスキップ

windows10 , Visual Studio,C++で画像処理のハフ変換をやります。ついでにLiDARというセンサで取得した画像データから壁を検知してみます。

windows10 , Visual Studio2019,C++で画像処理のハフ変換をやります。ついでにLiDARというセンサで取得した画像データから壁を検知してみます。

目的

  • C++開発環境
  • visual studio でC++をやる
  • openCVのインストール
  • hough変換とは
  • コード
  • LiDAR
  • 参考

visual studio でC++をやる

Visual Studio のインストール -Microsoft

インストーラにしたがってインストールします。何ができる環境を作るかを選べるようですが、私は残りの記憶容量が少ないのでコンソールアプリだけにします。後で追加でインストールすることが出来ます。

起動してコンソールアプリを選択します。デフォルトでhello worldが出力できるようになっているみたいですね。

ビルド/ソリューションのビルド(Shift+Ctrl+B)して、デバック/デバッグなしで開始 をクリックすると、コンソール上で実行されます。 あるいは上の△を押しても実行できます。

今回はUnityでゲームを作ったりするわけではないので、これでOKです。では、ハフ変換をしていきたいと思います。

C++ コンソール アプリ プロジェクトのビルドと実行 -Microsoft

OpenCVのインストール

opencv/opencv から最新版をdownload。ファイルを7zipか何かで展開し、openCV41とか名前を変更しつつCドライブなど適当なところに配置します。C:\Users\user\Documents\opencv41にしました。

プロジェクト/(プロジェクト名)のプロパティ から構成マネージャーを起動します。(上のバーでもいけますし、使いやすいようにいろんなとこにあるらしい)。

VC++からインクルードディレクトリを編集します。

  • ソリューション構成 → Release(Debug も可)
  • ソリューションプラットフォーム → x64
  • 構成プロパティ/デバッグ:環境 → PATH=% C:\Users\user\Documents\opencv41 %\build\x64\vc15\bin;%PATH%
  • 構成プロパティ/VC++ ディレクトリ:インクルード ディレクトリ → C:\Users\user\Documents\opencv41\build\include
  • 構成プロパティ/VC++ ディレクトリ:ライブラリ ディレクトリ → % C:\Users\user\Documents\opencv41%\build\x64\vc15\lib
  • 構成プロパティ/リンカー/入力:追加の依存ファイル → opencv_world410.lib

参考サイト( OpenCV 4.1.0Visual Studio 2019から使用する時の手順 -qiita )のとおり、下記のコードをビルド・実行します。

zeros()の第一、第二引き数にサイズを与えます。申し訳ありませんが画像を作ったあとに値を変えたので、500、600は正しくありません。おそらく単位はピクセルだと思います。

#include <opencv2/opencv.hpp>

using namespace cv;
int main()
{
	Mat image = Mat::zeros(500, 600, CV_8UC3);
	imshow("", image);
	waitKey(0);
}

このような画像が出てくればopencvインストールが成功しています。

hough変換とは

ハフ変換とは、画像処理の1つで、計算処理を通して並んだ点から直線や円を認識する事です。この記事では直線を検知することを目的として使用しています。

画像は、1つ1つの画素に色の情報を与えた2次元配列で表されます。座標軸上のある点、すなわち画素を通る直線は、xy直交座標では(x,y)で表されますが、ここではθとρを用いて $$ \rho = xcos\theta + ysin\theta $$

で表すことも出来ます。ここで、ρは座標原点から直線へ降ろした垂線の長さ、θは垂線とx軸の角度を示します。

図1: ρとθによる表現

あるx_0とy_0に関して、ρとθを変更していくと、その点を通る様々な線(図2)を描くことができます。

図2: ある点についてρ、θを変更した場合の直線群

このときρ-θ平面上に曲線を描くことが出来ます。(図3)

図3: 図2の写像のイメージ

このことは、画素空間(xy平面上)の点が ρ-θ 平面上の曲線射影されたことを意味します。 様々な点(x,y)に関してそれぞれ ρ-θ 軸上に軌跡をプロットし、多数の軌跡が重なった点があると、その点(ρ,θ)は多くの画素が通る直線であるということになります。(図4)。

図4: 多数の点を通る直線のイメージ

以上のように、数学的な処理をすることで直線を検知することができます。これがハフ変換です。この方法は、画ぞが連続していなくても直線を検出でき、雑音に強いもので、発見以来、改良・拡張がなされています。

ハフ変換は白黒画像でなくてはいけませんので、白黒画像にするためのエッジ検出関数もセットで使用します。

cv::Canny(const Mat& image, Mat& edges, double threshold1, double threshold2, int apertureSize=3, bool L2gradient=false);

threshold1 と threshold2 の内,小さい方の値がエッジの接続に利用され,大きい方の値が明確なエッジの初期セグメントを検出するのに利用されます。 threshold1 は threshold2 の2倍から3倍の間が推奨されています。この二つは順番を並び替えても等価になります。

apertureSize は Sobel() オペレータのアパーチャサイズ(カーネルサイズ)を指定します。これはフィルタを選択しているみたいです。デフォルトの3を指定します。 L2gradient は 画像勾配の強度を求める計算方法を指定します。Falseがデフォルトです。この二つは特に理由が無ければ変更しない大丈夫です。

thresholdの値はいろいろ実験して決めました。

参考本: コンピュータ画像処理 田村秀行 ohm社

OpenV C++版のハフ変換の関数はこの関数です。

cv::HoughLinesP(
			edge,           // 8ビット,シングルチャンネル2値入力画像.この画像は関数により書き換えられる可能性がある.
			lines,          //出力先、ベクトル
			1,              //距離分解能、ピクセル
			CV_PI / 180.0,  //角度分解能、ラジアン
			80,             //しきい値。この値を超えている直線が出力される
			30,             //最小の線分長さ。たぶんピクセル
			10              //2点が同一線分上にあると見なす場合に許容される最大距離。たぶんピクセル。
		);
  • edge: 8ビット,シングルチャンネル2値入力画像です。
  • lines: 出力先、ベクトル
  • 1: 距離分解能、ピクセル
  • CV_PI / 180.0: 角度分解能、ラジアン
  • 80: しきい値。この値を超えている直線が出力される
  • 30: 最小の線分長さ。たぶんピクセル
  • 10: 2点が同一線分上にあると見なす場合に許容される最大距離。たぶんピクセル。

コード

山登りしたときの山小屋の写真を使用します。コードは丸パクです。ありがとうございます。( OpenCV 2値画像から線分を検出 -Maverick Project より)


#include <iostream>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>

int main(int argc, char **argv){
	int hr = -1;
	try {
        //変数の指定
		cv::Mat src, edge, dst; //OpenCVの2次元行列型Mat
		std::vector<cv::Vec4i> lines; //?たぶんベクトル

        //画像の読み込み
		img = cv::imread("images\\P1030554.JPG", cv::IMREAD_GRAYSCALE);//imread関数によって画像をsrcに格納。カラーで読み込むならIMREAD_UNCHANGEDを、グレースケールで読み込むならIMREAD_GRAYSCALE
		cv::namedWindow("img", 1);//
		imshow("img", img);//imgの表示。グレースケールになった後の画像を確認
       
        //輪郭抽出
		cv::Canny(img, edge, 50, 200, 3);//2値化する関数
		
        //画像領域確保
        dst = cv::Mat::zeros(img.rows, img.cols, CV_8UC3);
        
        //img[0]->dst[2],img[0] -> dst[1], img[0] -> dst[0].コピー元とコピー先を指定
		int fromTo[] = { 0,2,0,1,0,0 };
        
        //チャンネル数?を増やしてコピー
        cv::mixChannels(&img, 1, &dst, 1, fromTo, 3);
        
        //線分検出
		cv::HoughLinesP(
			edge,           // 8ビット,シングルチャンネル2値入力画像.この画像は関数により書き換えられる可能性がある.
			lines,          //出力先、ベクトル
			1,              //距離分解能、ピクセル
			CV_PI / 180.0,  //角度分解能、ラジアン
			80,             //しきい値。この値を超えている直線が出力される
			30,             //最小の線分長さ。たぶんピクセル
			10              //2点が同一線分上にあると見なす場合に許容される最大距離。たぶんピクセル。
		);
        
        //線分描画
		for (auto line : lines) {
			cv::line(dst, cv::Point(line[0], line[1]), cv::Point(line[2], line[3]), cv::Scalar(0, 0, 255), 1);//線の色と太さを指定(B,G,R,太さ)
		}

		cv::namedWindow("dst", 1);
		cv::imshow("dst", dst);
		cv::waitKey(0);
		hr = 0;
	}
	catch (cv::Exception ex) {
		std::cout << ex.err << std::endl;
	}
    //ウインドウの破棄
	cv::destroyAllWindows();
	
    return hr;
}
入力画像のグレースケール
ハフ変換の出力結果

LiDAR

LiDAR(ライダー)とは、距離センサがぐるぐる回るヤツです。レーザー光を掃射し、物体に跳ね返ってくるでの時間を計測するものです。距離情報と時間(あるいは角度?)情報が流れてきます。

というわけで、とあるLiDAR画像を取ってきました。

ライダーの画像でやってみた。

cv::HoughLinesP(
			edge,
			lines,
			1,
			CV_PI / 180.0,
			20,
			15,
			3
		);

		for (auto line : lines) {
			cv::line(dst, cv::Point(line[0], line[1]), cv::Point(line[2], line[3]), cv::Scalar(0, 255, 0), 3);
		}

これ、実空間ではこんな感じなので、壁が感じてられますね。なんとなく。OK。

参考

以前、Raspberry Pi 3 B + に Ubuntu MATE(16.04) を入れ、ROSでhelloworldしました。 基本的な機能を確認します。
catkin_makeは重いかも。ラズパイちゃんが固まるので注意。

目次

  • ROSとは
  • Topic
  • Topicのサンプルコード
  • 動作
  • 雑記
  • 参考

ROSとは

ロボット開発を促進するミドルウェアです。ROSではすべてモジュールとモジュール間通信の形式で構成されています。誰かが作ったプログラムを、詳細を知らないままに使用できます。1つ1つのモジュールをノードと呼びます。

ROSではすべての処理が「ノード(Node)とノードがメッセージ(Message)をやりとりする」という構造になっています。それらの集合がパッケージ(Package)と呼ばれます。ノードとノードの通信を行うのがマスタ(Master)で、roscoreコマンドで起動します。

ビルドはすべてパッケージフォルダ内のCMakeLists.txtに記述され、catkinで行われます。(Hydroバージョン以降)

  • Topic
  • Service
  • Parameter

この投稿では参考の本のtopicついてさわりのみ扱います。

topic

濃い青が実際に作るプログラム、薄い青がプログラムによって作られたもの、矢印がデータの流れを表しています。

Topic はノード間でやり取りされるデータの入れ物(話題)です。データを送る動作をpublish、データを受け取る動作をSubscribeといいます。

Publisherはノードの起動時にTopic名をMasterに登録し、Messageで定めた形式で他のノードに送信します。 Subscriverは指定されたTopicを発振しているPublisherを問い合わせます。

名前と型などが設定されていて、Topicの名前を指定すればどのノードからでもデータを読み取ることができます。また一度接続されればメッセージ送受信が継続されます (非同期通信)ので、高頻度の通信を行うセンサ信号の送受信などに使用されます。

topicのサンプルコード

$cw
$catkin_create_pkg tutorial_topic roscpp std_msgs

まず上のようにパッケージを作成します。catkin_create_pkg パッケージ名 依存パッケージ のように書きます。

publisher.cpp

#include <ros/ros.h>
#include <std_msgs/Int32.h>

int main(int argc, char **argv)
{
  ros::init(argc, argv, "publisher");
  ros::NodeHandle nh;
  ros::Publisher pub = nh.advertise<std_msgs::Int32>("number", 10);
  
  ros::Rate loop_rate(1);
  std_msgs::Int32 cnt;
  cnt.data = 0;
  
  while(ros::ok())
  {
    ROS_INFO("Count : %d", cnt.data);
    pub.publish(cnt);
    cnt.data++;
    loop_rate.sleep();
  }
  return 0;
}
  • ros::init(argc, argv, ノード名)はノードの初期化を行う関数です。
  • ros::NodeHandle はROSシステムにアクセスし、他のノードと通信を行うクラスです。
  • ros::NodeHandle::.advertise(topicの名前,バッファ容量)でtopicの登録を行っています。
  • loop_rate(1)で1Hzのウェイトをかけてループしています。
  • std_msgs::Int32は32bit符号付整数のラッパークラスです。ラッパークラスとは、別のプログラム(群)を包んで隠蔽し、使いやすくしたクラスです。今はデータはcnt.dataに格納されています。
  • ros::ok()は基本的にtrue、CNTR+Cで停止したときにfalseを返します。

subscriber.cpp

#include <ros/ros.h>
#include <std_msgs/Int32.h>

void onNumberSubscribed(const std_msgs::Int32 &msg)
{
  ROS_INFO("I heard: [%d]", msg.data);
}

int main(int argc, char **argv)
{
  ros::init(argc, argv, "subscriber");
  ros::NodeHandle nh;
  ros::Subscriber sub = nh.subscribe("number", 10, onNumberSubscribed);
  ros::spin();
  return 0;
}
  • ros::NodeHandle::subscribe(topic名, バッファ容量, コールバック関数名)でSubscribeするTopicを設定しています。今は"number"というTopicを受信するたびにonNumberSubscribedが呼び出されます。
  • ros::spin()は無限ループで、CNTR+Cで中断されると、以降の処理は行われません。

CMakeLists.txt

cmake_minimum_required(VERSION 2.8.3)
project(tutorial_topic)

find_package(catkin REQUIRED COMPONENTS
  roscpp
  std_msgs
)

catkin_package()
include_directories(${catkin_INCLUDE_DIRS})

add_executable(publisher src/publisher.cpp)
target_link_libraries(publisher ${catkin_LIBRARIES})

add_executable(subscriber src/subscriber.cpp)
target_link_libraries(subscriber ${catkin_LIBRARIES})

CMakeLists.txtはファイルを作成する指示書みたいなものです。自動で作成されるものを編集します。

  • cmake_minimum_requiredにはバージョン、prjectにはproject名、find_packageには依存パッケージが入っていると思います。
  • catkin_packageはcatkinビルドのオプションを指定します。
  • include_directories()はインクルードフォルダを指定します。今は各パッケージのincludeフォルダのヘッダーファイルを指定しています。

add_executable(実行ファイル名 src/実行ファイル名.cpp)以下をそれぞれ書き足します。add_executableはビルド後に実行するファイルを、target_link_librariesには生成時に必要なファイルを指定します。以下で ビルドします。

$cd ~/catkin_ws
$caikin_make -DCATKIN_WHITELIST_PACKAGES="パッケージ名"
$roscore
$rosrun tutorial_topic subscriber
$rosrun tutorial_topic publisher

指定したパッケージのみビルドし、それぞれ実行します。rosrunなどは起動したままにするので、端末は複数開いてください。

topicの動作

出力はこんな感じ。下のコマンドでノードとデータの移動をグラフとして表示できます。

$rosrun rqt_graph rqt_graph

publisherノードとsubscriberノードがnumberというtopicを受け渡ししているのがわかります。

topicはプリミティブ型は使えないので、ラッパーされたメッセージ型を使わなければなりません。int型の代わりにstd_msgsパッケージのInt32というメッセージ型を使っています。

その他

参考にした本には以下についてのサンプルコードが載っています。

  • 独自型の定義
  • spinOnce
  • AsyncSpinner
  • 同期

ROSでは独自型を定義する事ができます。ただし、ROSの「他の人のコードも利用できる」という性質上、独自型を作成するのは最低限にすべきでしょう。 また、上のサンプルコードにはspin関数を使っているので複数のTopicをSubscribeする事はできません。ノンブロッキングなspinOnceなどを使う必要があります。

また、Lidarの情報を扱う際には同期が活躍します。

雑記

モーターとセンサを取り付けたい。こいつ単体で何かできるようにしてやりたい。

参考