目录
0x01 了解霍夫变换
0x02 线检测技术
0x03 LSD快速直线检测
0x04 圆检测技术
0x05 轮廓检测
霍夫变换是要从图像钟识别几何形状的基本图像处理方法之一。经典的霍夫变换用来检测图像中的直线,改进的霍夫变换扩展到识别任意形状的物体(椭圆、圆等等)。优势在于它不受图行旋转的影响,易于进行几何图行的快速变换。
经典的霍夫变换用于检测图像中的直线,其原理是利用坐标空间变换将两个坐标进行相应的转换,或通过直线映射到另一坐标空间的点形成的峰值。从而把检测任意形状的问题转化为统计峰值的问题。
基本原理如下:
以直线检测为例,每个像素坐标点经过空间变换都编程对直线特质有贡献的统一度量。
对于二维图像数据f(x,y),平面坐标为(x,y),极坐标为(r,θ)。从坐标轴可看出公式:
xcosθ+ysinθ=r
对于图像像素平面坐标(x,y),我们需要做的就是通过空间坐标映射关系,将图像笛卡尔坐标系转换到极坐标霍夫空间系统,这种点到曲线的映射变换称为霍夫变换。
在许多实际的图像处理应用中,我们可以检测图像中的轮廓部分或图形,直线检测有利于分析图像中的角度及其结构特征。
霍夫线变换
基于图像二值化的变换,利用二值化图像中的点集来确定候选直线的集合。我们设定分程f(x,p)=0来表示任意曲线,其中p为曲线的参数向量,那么利用霍夫变换进行线检测算法的步骤:
(1)在参数p的范围内量化参数空间,将霍夫空间坐标(r,θ)初始化为0。
(2)在阈值化后的梯度图中,对每个图像点(i,j)进行遍历,对于满足参数p,加权累计所有满足f(x,p)=0的单位S(p),则S(p) = S(p)+delta(p)。
(3)计算当前霍夫空间的累计数组S(p)的局部最大值,那么对应的就是原始图像中f(x,p)=0的解析实现。
OpenCv算法并没有直接将曲线提取出来,而是返回相应的(r,θ)平面的局部最大值,因此要对OpenCV中的函数参数接口进一步理解分析。
OpenCV中提供了不同种类型的霍夫变换函数:
(1)标准的霍夫变换(SHT)
(2)多尺度霍夫变换(MHT)
(3)统计概率霍夫变换(PPHT)
实现SHT与MHT:
void HoughLinesP(InputArray image, //输入图像
OutputArray lines, //输出线向量
double rho, //累计像素的距离分辨率
double theta, //累计弧度的角度分辨率
int threshold, //检测一条直线所需最少的曲线交点
double minLineLength=0, //最小的线长度
double maxLineGap=0) //最大的长度,用于线连接
lines由4个元素(x1,y1,x2,y2)组成,其中(x1,y1)和(x2,y2)为线段的终点坐标。
实现统计概率的霍夫变换线检测:
void HoughLines(InputArray image, //二值化图像
OutputArray lines, //输出线向量
double rho, //累计像素的距离分辨率
double theta, //累计弧度的角度分辨率
int threshold, //检测一条直线所需最少的曲线交点
double srn=0, //多尺霍夫变换参数,距离分辨因子p
double stn = 0) //多尺度霍夫变换参数,距离分辨率θ因子
如何使用?(分别对应if0与if1)
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include
using namespace cv;
using namespace std;
int main( )
{
cv::Mat srcImage =
cv::imread("..\\images\\building.jpg", 0);
if (!srcImage.data)
return -1;
cv::Mat edgeMat, houghMat;
// Canny边缘检测 二值图像
Canny(srcImage, edgeMat, 50, 200, 3);
cvtColor(edgeMat, houghMat, CV_GRAY2BGR);
#if 0
// 标准的霍夫变换
vector lines;
HoughLines(edgeMat, lines, 1, CV_PI/180, 100, 0, 0 );
for( size_t i = 0; i < lines.size(); i++ )
{
// 根据直线参数表达式绘制相应检测结果
float rho = lines[i][0], theta = lines[i][1];
Point pt1, pt2;
double a = cos(theta), b = sin(theta);
double x0 = a*rho, y0 = b*rho;
pt1.x = cvRound(x0 + 1000*(-b));
pt1.y = cvRound(y0 + 1000*(a));
pt2.x = cvRound(x0 - 1000*(-b));
pt2.y = cvRound(y0 - 1000*(a));
line( houghMat, pt1, pt2, Scalar(0,0,255), 3, CV_AA);
}
#else
// 统计概率的霍夫变换
vector lines;
HoughLinesP(edgeMat, lines, 1, CV_PI/180, 50, 50, 10 );
for( size_t i = 0; i < lines.size(); i++ )
{
Vec4i l = lines[i];
// 绘制线检测结果
line( houghMat, Point(l[0], l[1]),
Point(l[2], l[3]), Scalar(0,0,255), 3, CV_AA);
}
#endif
cv::imshow("srcImage", srcImage);
cv::imshow("houghMat", houghMat);
cv::waitKey();
return 0;
}
该算法用于局部提取直线,时间复杂度较低。LSD算法通过对图像局部分析,得出直线的像素点集,再通过假设参数进行验证求解,将像素点集合与误差控制集合合并,进而自适应控制误检的数量。
检测图像中的直线最基本的思想就是检测图像中梯度变化较大的像素点的合集。LSD正是充分利用了梯度信息和行列线(level-line)来进行直线检测的。相关知识:
(1)行列线及支撑线
行列线:图像的灰度从黑到白或从白道黑剧烈变化的分割线,即梯度形成区域。
首先计算梯度图像中每个像素点行列线角度,以及生成行列线区域,对于单位向量域而言,该区域被分为若干子区域,所有向量与基准点行列线相切,连接区域包含同样或相似的容忍误差为t的角度。通过这样的运算可以确保它并不需要同样强度的梯度行列线,最后得到的效果:即使有锯齿线、混叠效应,同样可以使局部水平行线的方向大致是平行的。
每个支撑线区域都是以候选的直线分割方式对应支撑线区域最小外接矩形,同时满足相应的几何目标与其一一对应,几何形状矩形的主方向作为支撑线的主轴方向,矩形区域覆盖整个支撑线区域。
(2)对齐点
在每个支撑线区域形成的矩形区域内,若该像素点形成的行列线方向角度与矩形主轴方向在容忍误差t的范围内保持一致性,则矩形区域内的像素个数n及对齐点个数k都需要计算出来,然后通过相应准则来验证该矩形分割是否可作为正确的直线分割。
LSD算法并不重复计算单个行列线,行列线也会被基于其梯度大小进行排序,梯度变化分析最大的为最重要的行列线,该行列支撑线的获取将会根据其强梯度降序排列来得到。
LSD优点在于是可以根据几个参数的设置来提升不同应用场景下的检测性能,线性时间内就可以完成,但算法本身也存在局部算法的缺点。
下面将介绍LSD算法实现步骤:
图像缩放
将输入图像缩小至原图像的百分之八十,消除图像中的锯齿效应,利用高斯下采样的方式对输入图像进行操作(高斯模糊)。图像首先与高斯核卷积来平滑消除锯齿效应,然后采用降采样操作,防止其他干扰噪声。高斯核的标准差为:
图像的梯度计算是通过2*2模板来完成的,较小模板也使得计算相对较快,同时也保证了邻域方向分布的相对独立性。对于灰度图像f(i,j),图像的梯度由下式可得:
图像梯度的幅值G及行列线的角度θ可由下式得到:
这么一看这个算法跟canny中计算幅值及其方向的方式其实是一样的。。
梯度排序
梯度幅值剧烈变化的像素点区域一般是图像中较强边缘存在的区域。LSD算法中区域像素点的处理将会直接影响后续的梯度检测,中间像素一般具有最高的梯度幅值,需要将像素点梯度幅值进行从大到小排序,进而可以完成直线检测。
LSD中实现的排序算法是伪排序算法,可在线性时间内完成。等间距均匀设置种子点,像素按照对应幅值归类道种子区域,然后利用像素映射关系对梯度幅值进行排序。
梯度阈值
梯度幅值较小的像素点区域对应图像中平坦区域,较小梯度值往往出现在平滑区域,不在关注的范围内,但是它们的存在往往会严重影响直线角度的计算。
对平坦区域像素值计算其梯度时,很有可能出现更多的误差,所以在LSD中,是直接给定一个阈值,对于梯度幅值小于某个值,那么我们就限制其参与支撑线性区域的计算。
区域生成
在排序像素列表中选取状态为未使用的像素值作为种子点,搜索角度满足状态为未使用的点8邻域形成的区域生成支撑线区域。
邻域内得角度误差范围为t,像素将依次被添加道该区域内进行验证更新,种子点得行列线角度为lθ,那么区域得角度更新为:
* 其中lj为行列线区域角度,经验值t设置为PI/8为最优的参数。
* 矩形估计
支撑线区域的外接矩形对应于直线分割,在分割步骤实施之前,应先计算出矩形区域的**中心**,根据像素的梯度幅值,矩形区域的主轴方向将会被设置为**矩形的最小特征向量值对应的角度**。
(3)OpenCV中的LSD实现
包含于include/opencv2/imgproc.hpp。下面是cv::LineSegmentDetector所对应的类:
cv::LineSegmentDetector::compareSegment(); //绘制两组线(蓝色和红色),计算非重叠像素
cv::LineSegmentDetector::detect(); //对输入图像进行线检测
cv::LineSegmentDetector::drawSegment(); //给定图像的线分割绘画
如何使用:
#include
#include
#include "opencv2/core/core.hpp"
#include "opencv2/core/utility.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/imgcodecs.hpp"
#include "opencv2/highgui/highgui.hpp"
using namespace std;
using namespace cv;
int main()
{
cv::Mat srcImage =
cv::imread("./image/cross.jpg", 0);
if (!srcImage.data)
return -1;
cv::imshow("source", srcImage);
// canny边缘检测
Canny(srcImage, srcImage, 50, 200, 3);
// 创建LSD检测类
#if 1
Ptr ls = createLineSegmentDetector(LSD_REFINE_STD);
#else
Ptr ls = createLineSegmentDetector(LSD_REFINE_NONE);
#endif
double start = double(getTickCount());
vector lines_std;
// 线检测
ls->detect(srcImage, lines_std);
double duration_ms = (double(getTickCount()) - start) * 1000 / getTickFrequency();
std::cout << "It took " << duration_ms << " ms." << std::endl;
// 绘图线检测结果
Mat drawnLines(srcImage);
ls->drawSegments(drawnLines, lines_std);
cv::imshow("Standard refinement", drawnLines);
cv::waitKey();
return 0;
}
所消耗的时间确实很少,并且边界找的也是很准确。
霍夫圆变换的基本原则其实跟上面差不多,直线检测中的点对应极坐标空间,在变换中被三维空间中圆坐标空间代替。对于一条直线来说,一条直线可以使用(p,θ)来确定,但是对于圆的话需要三个参数来确定。
如果按照二维霍夫线变换原理来进行解释的话,三维空间曲线中相交于一点的边缘点集较多,那么它们经过的共同圆上的像素点就越多,设定阈值进行相应判断一个圆是否被检测到,那么就需要消耗很多的时间以及计算量,大可不必。
OpenCV的解决:
霍夫梯度算法:依据圆心一定出现在圆上的每个点的模向量上,圆上点的模向量交点就是圆心的所在位置。
第一步:找到圆心,这样就可以将三维的累加平面转为二维累加平面。
第二步:根据所有候选中心的边缘非零像素对其的支持程序来确定半径。
使用:
使用canny进行边缘检测,对边缘中的非零点,根据Sobel的水平和垂直方向计算其局部梯度。
对线上像素点进行累计,标记边缘图像中非零像素点的位置。
对像素中心距离排序。
对候选中心进行边缘图像非零像素验证和候选中心距离验证。
代码:
#include
#include
#include "opencv2/core/core.hpp"
#include "opencv2/core/utility.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/imgcodecs.hpp"
#include "opencv2/highgui/highgui.hpp"
#include
using namespace cv;
using namespace std;
int main(int argc, char** argv)
{
cv::Mat srcImage = imread("./image/circlecircle.png");
if (!srcImage.data)
return -1;
cv::imshow("srcImage", srcImage);
// 转换为灰度图像
cv::Mat src_gray;
cvtColor(srcImage, src_gray, COLOR_BGR2GRAY);
// 高斯平滑滤波
GaussianBlur(src_gray, src_gray, Size(9, 9), 2, 2);
vector circles;
// 霍夫圆检测
HoughCircles(src_gray, circles, HOUGH_GRADIENT,
1, src_gray.rows / 8, 200, 100, 0, 0);
// 将得到的结果绘图
for (size_t i = 0; i < circles.size(); i++)
{
Point center(cvRound(circles[i][0]), cvRound(circles[i][1]));
int radius = cvRound(circles[i][2]);
// 检测圆中心
circle(srcImage, center, 3, Scalar(0, 255, 0), -1, 8, 0);
// 检测圆轮廓
circle(srcImage, center, radius, Scalar(255, 0, 0), 3, 8, 0);
}
cv::imshow("HoughResult", srcImage);
cv::waitKey(0);
return 0;
}
对于函数HoughCircles:
void HoughCircles( InputArray image, //输入图像8-bit灰度图
OutputArray circles, //输出圆的结果
int method, //定义检测图像中圆的方法
double dp, //寻找圆弧圆心的累计分辨率
double minDist, //明显区分两个不同圆之间的最小距离
double param1 = 100, //canny边缘阈值上限,下限被设为上限的一半
double param2 = 100, //HOUGH_GRADIENT方法的累加器阈值,阈值越小,检测到的圈子越多
int minRadius = 0, //最小圆半径
int maxRadius = 0 ) //最大圆半径
关于dp的一些解释:寻找圆弧圆心的累计分辨率,允许创建一个比输入图像分辨率低的累加器。这样做是因为有利于认为图像中存在的圆会自然降低到与图像宽高相同宽高相同数量的范畴。如果dp设置为1,则分辨率是相同的,如果设置为比1更大的值,累加器的分辨率会受影响变小(缩小为二分之一)。dp的值不可以比1小。
按照上面这么说,我把dp设置为2,确实识别精度越来越高了,连半圆都可以识别到:
那越大越好?这不是的,当我改到3的时候:
看到这会是会用了,可是对其实现的原理其实也是一知半解,那么再往深处看看把。
我们先看看我们正常表示一个圆的时候是怎么表示的:
(x-a)^2 + (y-b)^2 = r^2。想必这条公式所有人对其都不陌生,那么霍夫变换的圆检测就是在这三个参数中组成的,也就是参数a、b、r。a、b为圆心的坐标,r为圆的半径。
原则上,霍夫变换可以检测任何形状。但是复杂形状的时候,需要的参数非常多的,所以在程序实现上所需的内容空间以及运行效率上都不利于把标准霍夫变换应用于实际复杂的图形检测中。所以就有很多一些改进的霍夫变换就相继提出,它们的基本原理就是尽可能减小霍夫空间的维数。
所以我们现在使用的函数HoughCircles,也是改进的霍夫变换——21HT。这个函数将霍夫变换分为两个阶段,从而减少了霍夫空间的维数。
第一阶段用于检测圆心,第二阶段从圆心推导出半径。
检测圆心的原理是圆心是它所在圆周所有法线的交汇处,因此只要找到这个交点,即可确定圆心。这个方法与上面所说的霍夫变换的原理是一样的,它仅仅就是在二维的空间中进行查找。
检测圆半径的原理是从圆心到圆周上的任意一点的距离(即半径)是相同,只要确定一个阈值,只要相同距离的数量大于该阈值,那我们就认定该距离就是该圆心所对应的半径。这个方法需要计算半径的直方图,不使用霍夫空间。圆心和圆半径都得到了,那么再根据公式就可以得到一个圆了。
那么最后总结一下上面的步骤,21HT把标准霍夫变换的三维霍夫空间缩小为二维霍夫空间,因此无论再内存的使用上还是在运行的效率上,21HT都远远优于标准霍夫变换。但是该算法的不足就是在于圆半径的检测完全取决于圆心的检测,因此如果圆心检测出现偏差,那么圆半径的检测那就肯定出错,那么具体步骤是这样的:
第一阶段:找圆心
对输入的图像进行边缘检测。
计算图形的梯度,并确定圆周线,其中圆周的梯度就是它的法线。
在二维霍夫空间内,绘出所有图形的梯度直线,某坐标点上累加和的值越大,说明在该点上直线相交的次数越多,也就是越有可能是圆心。
在霍夫空间的4邻域内进行非最大值抑制。
设定一个阈值,霍夫空间内累加和大于该阈值的点就对应于圆心。
第二阶段:检测圆半径
计算某一个圆心到所有圆周线的距离,这些距离中就有该圆心所应的圆的半径值,这些半径值当然是相等的,并且这些圆半径的数量要远远大于其他距离值相等的数量。
设定两个阈值,定义为最大半径和最小半径,保留距离在这两个半径之间的值。
对保留下来的距离进行排序。
找到距离相同的那些值,并计算相同值的数量。
设定一个阈值,只有相同值得数量大于该阈值,才认为该值是该圆心对应的圆半径。
每对一个圆心,需要完成以上的操作,可以得到所有的圆半径。
图像中目标物体的形状检测是图像识别中重要的技术之一,可以对物体进行检测,提取特征轮廓,再头发轮廓点集的特征选择相应的算法进行处理。轮廓提取的原理是这样的:
先对源图像进行二值化,利用边缘点连接的层次差别,提取位于数据特征高的区域点集构成的集合,这部分最可能是物体的轮廓。
OpenCV中提供了函数findContours()用于对物体的轮廓进行检测:
void findContours(
InputOutputArray image, //输入图像
OutputArrayOfArrays contours, //检测到的轮廓,每个轮廓都在向量中
OutputArray hierarchy, //包含图像拓扑结构的信息
int mode, //可选获取轮廓的方法
int method, //轮廓近似的方法
Point offset=Point() //可选的偏移量
)
mode(仅仅建立两层包含关系):CV_RETR_EXTERNAL(外轮廓)、CV_RETR_LIST(检测所有轮廓的不包含继承关系)、CV_RETR_TREE(检测所有轮廓的包含继承关系)、CV_RETR_CCOMP(检测所有轮廓)
method(轮廓近似方法):CV_CHAIN_APPROX_NONE(表示把轮廓上所有的点储存)、CV_CHAIN_APPROX_SIMPLE(表示只存储水平、垂直以及对角线的起始点)
我们可以使用一个滑动条来控制canny阈值,来看这个轮廓识别的效果,可以发现当阈值越高时轮廓找到的就越少:
#include
#include
#include "opencv2/core/core.hpp"
#include "opencv2/core/utility.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/imgcodecs.hpp"
#include "opencv2/highgui/highgui.hpp"
#include
#include
using namespace cv;
using namespace std;
Mat srcImage; Mat src_gray;
int thresh = 100;
int max_thresh = 255;
//用于生成随机数
RNG rng(12345);
void thresh_callback(int, void*)
{
Mat canny_output;
vector > contours;
vector hierarchy;
// 用Canny算子检测边缘
Canny(src_gray, canny_output, thresh, thresh * 2, 3);
// 寻找轮廓
findContours(canny_output, contours, hierarchy,
RETR_TREE, CHAIN_APPROX_SIMPLE, Point(0, 0));
/// 绘出轮廓
Mat drawing = Mat::zeros(canny_output.size(), CV_8UC3);
for (int i = 0; i < contours.size(); i++)
{
//随机生成颜色
Scalar color = Scalar(rng.uniform(0, 255),
rng.uniform(0, 255), rng.uniform(0, 255));
drawContours(drawing, contours, i, color, 2, 8, hierarchy, 0, Point());
}
// 显示轮廓结果
namedWindow("Contours", WINDOW_AUTOSIZE);
imshow("Contours", drawing);
}
int main()
{
cv::Mat srcImage =
cv::imread("./image/circlecircle.png");
if (!srcImage.data)
return -1;
// 转成灰度并平滑
cvtColor(srcImage, src_gray, COLOR_BGR2GRAY);
blur(src_gray, src_gray, Size(3, 3));
// 创建窗体
namedWindow("srcImage", WINDOW_AUTOSIZE);
imshow("srcImage", srcImage);
// 滑动条控制canny阈值
createTrackbar(" threth:", "srcImage", &thresh, max_thresh, thresh_callback);
thresh_callback(0, 0);
cv::waitKey(0);
return(0);
}
这里更改的是canny找边界时所对应的高低阈值,调的是低阈值,输出的高阈值是低阈值的两倍,低阈值越高,找到的边界数目肯定是会被限制的越少,所以找到的轮廓自然而然就少了。
用来找边界,确实也是一个好东西:
好困好困,快点睡觉T T....