检测轮廓时我们使用canny边沿检测算法,这个算法其实也是基于梯度的。但是,与传统的梯度算法求边沿不同的是:
•对图像2进行扫描,当遇到一个非零灰度的像素p(x,y)时,跟踪以p(x,y)为开始点的轮廓线,直到轮廓线的终点q(x,y)。
•考察图像1中与图像2中q(x,y)点位置对应的点s(x,y)的8邻近区域。如果在s(x,y)点的8邻近区域中有非零像素s(x,y)存在,则将其包括到图像2中,作为r(x,y)点。从r(x,y)开始,重复第一步,直到我们在图像1和图像2中都无法继续为止。
•当完成对包含p(x,y)的轮廓线的连结之后,将这条轮廓线标记为已经访问。回到第一步,寻找下一条轮廓线。重复第一步、第二步、第三步,直到图像2中找不到新轮廓线为止。
•至此,完成canny算子的边缘检测。
在OpenCV中使用Canny函数来检测边沿。第一个参数是待检测的图像,第二个参数是检测结果;后两个参数是那两个门限,通常高低阈值比在 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);
检测直线使用的是霍夫(hough)变换。霍夫变换的主要思想是利用点与线的对偶性,将原始图像空间的给定的曲线通过曲线表达形式变为参数空间的一个点。这样就把原始图像中给定曲线的检测问题转化为寻找参数空间中的峰值问题。
具体的说,利用直线的参数方程ρ= x cosθ+ysinθ,将(x,y)空间中的一个点变成了一条正弦曲线,如果若干个点在一条直线上,那么他们对应的正弦曲线也会交于同一个点。所以检测直线的问题,就转化为了判断交点峰值的问题。你设置一个峰值,大于这个值,就判为直线。
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::vector<Vec2f> lines;来存放。
其次,还是由于上面的原因,画线的时候是自己选一个y(最小为0),求一个x,得到一个点;再选一个y(选为图像的高度)再求一个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<Vec4i>来储存。4个数分别是两个点的x,y坐标。
为了说明霍夫变换中一点映射为(ρ,θ)空间内一条正弦曲线,两个在一条直线上导致两条正弦曲线相交的道理,我们有如下代码:
//创建一个累加器
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<Point>类型的向量中:
//把点集中的点插入到向量中
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));
}
}
}
有了这两步准备工作,调用直线拟合函数就行了:
//储存拟合直线的容器
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] << ")\n";
注意到拟合的结果Vec4f类型的line中的前两个值给出的是直线的方向的单位向量,后两个值给出的是该直线通过的一个点。
为了验证我们拟合的正确性,我们又沿着拟合的方向画了一段线段:
//画一个线段
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);
可以看出,拟合的确没有偏离原来的方向。