私のOpenCV学習ノート(19):輪郭、直線、円および直線フィットを検出する


輪郭を検出する際にcannyエッジ検出アルゴリズムを用い,このアルゴリズムも実際には勾配に基づいている.しかし、従来の勾配アルゴリズムのエッジを求めるのとは異なり、
1.エッジの位置を正確に決定します.幅角方向に沿ってモード値の極大値点であるエッジ点を検出することにより、8方向の画像画素を遍歴し、各画素のバイアス値を隣接画素のモード値と比較し、MAX値をエッジ点とし、画素階調値を0とする.この結果、エッジが非常に細くなります.
2.ダブルしきい値検出.通常、小さなしきい値は多くのエッジを保持し、彼らの一部は役に立たない.大きなしきい値では、主なエッジが保持されますが、エッジ情報が失われる可能性があります.どうやってそれらを組み合わせて使いますか?具体的には、(画像2は大きなしきい値で生成され、画像1は小さいしきい値で生成される)
•画像2をスキャンし、非ゼロ階調の画素p(x,y)に遭遇した場合、輪郭線の終点q(x,y)までp(x,y)を始点とする輪郭線を追跡する.
•画像1における画像2におけるq(x,y)点位置に対応する点s(x,y)の8近傍領域を考察する.s(x,y)点の8隣接領域に非ゼロ画素s(x,y)が存在する場合、r(x,y)点として画像2に含まれる.r(x,y)から、画像1と画像2の両方が継続できないまで、最初のステップを繰り返す.
•p(x,y)を含む輪郭線の連結が完了したら、この輪郭線をアクセス済みとしてマークします.最初のステップに戻って、次の輪郭線を探します.画像2に新しい輪郭線が見つからないまで、最初のステップ、2番目のステップ、3番目のステップを繰り返します.
•これで、canny演算子のエッジ検出が完了します.
OpenCVでCanny関数を使用してエッジを検出します.第1のパラメータは検出対象の画像であり、第2のパラメータは検出結果である.後の2つのパラメータはその2つの閾値であり、通常、高低閾値比は2:1〜3:1である.
cannyアルゴリズムと従来のsobelアルゴリズムの結果を比較するために、クラスを作成します.
#if ! defined SOBELEDGES
#define SOBELEDGES
#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>

#define PI 3.1415926
using namespace cv;
class EdgeDetector
{
private:
	Mat img;
	Mat sobel;
	int aperture;
	Mat sobelMagnitude;
	Mat sobelOrientation;
public:
	EdgeDetector():aperture(3){}
	//    
	void setAperture(int a)
	{
		aperture = a;
	}

	//    
	int getAperture() const 
	{
		return aperture;
	}

	//  Sobel  
	void computeSobel(const Mat &image,Mat &sobelX = cv::Mat(),Mat sobelY = cv::Mat())
	{
		Sobel(image,sobelX,CV_32F,1,0,aperture);
		Sobel(image,sobelY,CV_32F,0,1,aperture);
		cartToPolar(sobelX,sobelY,sobelMagnitude,sobelOrientation);
	}

	//    
	Mat getMagnitude()
	{
		return sobelMagnitude;
	}

	//    
	Mat getOrientation()
	{
		return sobelOrientation;
	}

	//          
	Mat getBinaryMap(double Threhhold)
	{
		Mat bgImage;
		threshold(sobelMagnitude,bgImage,Threhhold,255,THRESH_BINARY_INV);
		return bgImage;
	}

	//   CV_8U  
	Mat getSobelImage()
	{
		Mat bgImage;
		double minval,maxval;
		minMaxLoc(sobelMagnitude,&minval,&maxval);
		sobelMagnitude.convertTo(bgImage,CV_8U,255/maxval);
		return bgImage;
	}
	//    
	Mat getSobelOrientationImage()
	{
		Mat bgImage;
		sobelOrientation.convertTo(bgImage,CV_8U,90/PI);
		return bgImage;
	}
};

#endif

メイン関数:
	Mat image = imread("D:/picture/images/road.jpg",0);
	if(! image.data)
		return -1;	
	imshow("   ",image);

	//  sobel
	EdgeDetector ed;
	ed.computeSobel(image);

	//  sobel      
	imshow("  ",ed.getSobelOrientationImage());
	imshow("  ",ed.getSobelImage());

	//      
	imshow("       ",ed.getBinaryMap(125));
	imshow("       ",ed.getBinaryMap(350));

	//  canny  
	Mat contours;
	Canny(image,contours,125,350);
	Mat contoursInv;
	threshold(contours,contoursInv,128,255,THRESH_BINARY_INV);
	imshow("  ",contoursInv);

 
検出直線はホフ変換を用いた.ホフ変換の主な考え方は,点と線の対偶性を用いて,元の画像空間の与えられた曲線を曲線表現形式によりパラメータ空間の点に変えることである.これにより,元の画像における所与の曲線の検出問題をパラメータ空間におけるピークを探す問題に変換する.
具体的には,直線のパラメータ方程式を用いてρ= x cosθ+ysinθ,(x,y)空間の1つの点を正弦曲線に変え、いくつかの点が1つの直線上にある場合、対応する正弦曲線も同じ点に交差します.したがって,直線を検出する問題は,交点ピークを判断する問題に転化する.この値より大きいピークを設定すると、直線と判定されます.
OpenCVはHoughLines関数を用いて直線を検出する.
	//           Hough  
	//          
	std::vector<Vec2f> lines;
	//    
	HoughLines(contours,lines,1,PI/180,80);
	//       
	Mat result;
	image.copyTo(result);
	std::cout<<"     :"<<lines.size()<<" "<<std::endl;
	//    
	std::vector<Vec2f>::const_iterator it = lines.begin();
	while(it != lines.end())
	{
		float rho = (*it)[0];
		float theta=(*it)[1];
		if(theta < PI/4. || theta > 3. *PI / 4.)
		{
			//       
			Point pt1(rho/cos(theta),0);
			Point pt2((rho-result.rows*sin(theta))/cos(theta),result.rows);
			line(result,pt1,pt2,Scalar(255),1);
		}
		else
		{
			//      
			Point pt1(0,rho/sin(theta));
			Point pt2(result.cols,(rho-result.cols*cos(theta))/sin(theta));
			line(result,pt1,pt2,Scalar(255),1);
		}
		++it;
	}
	//    
	imshow("           (   80)",result);

	//       
	HoughLines(contours,lines,1,PI/180,60);
	image.copyTo(result);
	std::cout<<"     :"<<lines.size()<<" "<<std::endl;
	//    
	it = lines.begin();
	while(it != lines.end())
	{
		float rho = (*it)[0];
		float theta=(*it)[1];
		if(theta < PI/4. || theta > 3. *PI / 4.)
		{
			//       
			Point pt1(rho/cos(theta),0);
			Point pt2((rho-result.rows*sin(theta))/cos(theta),result.rows);
			line(result,pt1,pt2,Scalar(255),1);
		}
		else
		{
			//      
			Point pt1(0,rho/sin(theta));
			Point pt2(result.cols,(rho-result.cols*cos(theta))/sin(theta));
			line(result,pt1,pt2,Scalar(255),1);
		}
		++it;
	}
	//    
	imshow("           (   60)",result);

注意すべき点はいくつかあります.
まず,HoughLinesが検出したのは線分ではなく(ρ,θ)はい、std::vectorlinesを使用します.預けに来ます.
次に、やはり上の原因のため、線を描く時自分で1つのy(最小は0)を選んで、1つのxを求めて、1つの点を得ます;もう1つのy(画像の高さとして選択)を選択し、もう1つのxを求めて別の点を得る.水平の線は似ている.このように描かれた線は画像全体を貫通します.
実際、OpenCVは、より良い効果を達成するために確率ホフ変換HoughLinesPを提供する.この関数をクラスでカプセル化します.
#if!defined LINEF
#define LINEF

#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#define PI 3.1415926

using namespace cv;

class LineFinder
{
private:
	//   
	Mat img;
	//         :
	std::vector<Vec4i> lines;
	//      
	double deltaRho;
	double deltaTheta;
	//          
	int minVote;
	//      
	double minLength;
	//         
	double maxGap;
public:
	//        :1   ,1      ,           
	LineFinder():deltaRho(1),deltaTheta(PI/180),minVote(10),minLength(0.),maxGap(0.){}

	//************       ************//
	//         
	void setAccResolution(double dRho,double dTheta)
	{
		deltaRho = dRho;
		deltaTheta = dTheta;
	}

	//        
	void setMinVote(int minv)
	{
		minVote = minv;
	}

	//       
	void setLineLengthAndGap(double length,double gap)
	{
		minLength = length;
		maxGap = gap;
	}

	//     huogh    
	std::vector<Vec4i> findLines(Mat &binary)
	{
		lines.clear();
		HoughLinesP(binary,lines,deltaRho,deltaTheta,minVote,minLength,maxGap);
		return lines;
	}

	//          
	void drawDetectedLines(Mat &image,Scalar color = Scalar(255,255,255))
	{
		//  
		std::vector<Vec4i>::const_iterator it2 = lines.begin();
		while(it2 != lines.end())
		{
			Point pt1((*it2)[0],(*it2)[1]);
			Point pt2((*it2)[2],(*it2)[3]);
			line(image,pt1,pt2,color);
			++it2;
		}
	}


};


#endif

メイン関数では、次のコードを使用して線の検出を行います.
	//  LineFinder    
	LineFinder finder;
	//  Huogh     
	finder.setLineLengthAndGap(100,20);
	finder.setMinVote(80);
	//        
	std::vector<Vec4i> li = finder.findLines(contours);
	finder.drawDetectedLines(image);
	imshow("              ",image);

	//      
	std::vector<Vec4i>::const_iterator it2 = li.begin();
	while(it2 != li.end())
	{
		std::cout<<"("<<(*it2)[0]<<","<<(*it2)[1]<<")-("<<(*it2)[2]<<","<<(*it2)[3]<<")"<<std::endl;
		++it2;
	}

ここで注意すべきは,確率ホフ変換で検出された直線セグメントをstd::vectorで格納することである.4つの数はそれぞれ2つの点のx,y座標である.
ホーフ変換の一点が(ρ,θ)空間内の1つの正弦波曲線、2つの正弦波曲線が1つの直線上で2つの正弦波曲線が交差する理由には、次のコードがあります.
	//       
	Mat acc(200,180,CV_8U,Scalar(0));
	//     
	int x= 50,y = 30;
	//       
	for(int i = 0; i < 180;i++)
	{
		double theta = i * PI/180.;
		//
		double rho = x*cos(theta)+y*sin(theta);
		//
		int j = static_cast<int>(rho+100.5);
		std::cout<<i<<","<<j<<std::endl;
		//     
		acc.at<uchar>(j,i)++;
	}
	imshow("Hough   (1)",acc*100);
	//imwrite("Hough1.bmp",acc*100);
	//      
	x= 30, y = 10;
	//       
	for(int i = 0; i < 180;i++)
	{
		double theta = i*PI/180.;
		double rho = x*cos(theta)+y*sin(theta);
		int j = static_cast<int> (rho+100.5);
		acc.at<uchar>(j,i)++;
	}
	imshow("Hough   (2)",acc*100);
	//imwrite("Hough2.bmp",acc*100);

 
実はホフ変換も円を検出することができて、私たちが円をパラメータ空間に変換すれば、検出線の思想を利用して完成することができます.しかし,半径は円心で3次元空間を構成するが,ホフ変換の高次元空間での性能は不安定であることが分かったので,また多様な改良法が提案された.OpenCVはHoughCircles関数検出円を提供し、簡単な例は以下の通りである.
	//   
	image = imread("D:/picture/images/chariot.jpg",0);
	if(! image.data)
		return -1;	
	imshow("   ",image);
	//    
	GaussianBlur(image,image,Size(5,5),1.5);
	//        
	std::vector<Vec3f> circles;
	//  Hough     
	//   :     ,    ,    (      ),       ,       ,canny     (           ),            ,       
	HoughCircles(image,circles,CV_HOUGH_GRADIENT,2,50,200,100,25,100);
	std::cout<<"   "<<circles.size()<<" "<<std::endl;
	//   
	image = imread("D:/picture/images/chariot.jpg",0);
	std::vector<Vec3f>::const_iterator itc = circles.begin();
	while(itc != circles.end())
	{
		circle(image,Point((*itc)[0],(*itc)[1]),(*itc)[2],Scalar(255),2);
		++itc;
	}
	imshow("     ",image);

 
直線フィットの原理は比較的簡単で,最小二乗アルゴリズムである.これらの点から直線までの距離の和を最小にする.本来存在すべきでない点が直線フィッティングに生じる干渉を考慮すると、通常、重み付け最小二乗を用いて、重み値を点から直線までの距離に反比例させることもできる.OpenCVはfitLine関数を提供して直線フィットを行う.例を見てみましょう
まず、直線上に分布しているように見える点セットが必要です.ここでは、前に検出された直線の最初の輪郭とcannyで検出された輪郭とを合わせることによって、
	//       
	int n= 0;
	//     
	Mat oneLine(image.size(),CV_8U,Scalar(0));
	//  
	line(oneLine,Point(li[n][0],li[n][1]),Point(li[n][2],li[n][3]),Scalar(255),5);
	//         
	bitwise_and(contours,oneLine,oneLine);
	Mat oneLineInv;
	threshold(oneLine,oneLineInv,128,255,THRESH_BINARY_INV);
	imshow("    ",oneLineInv);

 
次に、その点をstd::vectorタイプのベクトルに入れます.
	//            
	std::vector<Point> points;
	//      
	for(int y = 0; y < oneLine.rows;y++)
	{
		uchar* rowPtr = oneLine.ptr<uchar>(y);
		for(int x = 0;x < oneLine.cols;x++)
		{
			if(rowPtr[x])
			{
				points.push_back(Point(x,y));
			}
		}
	}

この2つのステップの準備ができたら、直線フィット関数を呼び出せばいいです.
	//         
	Vec4f line;
	//      
	fitLine(Mat(points),line,CV_DIST_L2,0,0.01,0.01);
	std::cout << "line: (" << line[0] << "," << line[1] << ")(" << line[2] << "," << line[3] << ")
";

フィッティング結果Vec 4 fタイプのlineの最初の2つの値は、直線の方向の単位ベクトルを与え、後の2つの値は、直線が通過する点を与えることに留意されたい.
フィッティングの正確性を検証するために、フィッティングの方向に沿ってセグメントを描きました.
	//     
	int x0= line[2];
	int y0= line[3];
	int x1= x0-200*line[0];
	int y1= y0-200*line[1];
	image = imread("D:/picture/images/road.jpg",0);
	cv::line(image,Point(x0,y0),cv::Point(x1,y1),cv::Scalar(0),3);
	imshow("     ",image);

フィッティングは確かに元の方向から外れていないことがわかる.