若该文为原创文章,未经允许不得转载
原博主博客地址:https://blog.csdn.net/qq21497936
原博主博客导航:https://blog.csdn.net/qq21497936/article/details/102478062
本文章博客地址:https://blog.csdn.net/qq21497936/article/details/105575546
各位读者,知识无穷而人力有穷,要么改需求,要么找专业人士,要么自己研究
目录
OpenCV开发专栏(点击传送门)
OpenCV开发笔记(四十四):红胖子8分钟带你深入了解霍夫圆变换(图文并茂+浅显易懂+程序源码)
前言
红胖子来也!!!
去噪、边缘检测之后,就是特征提取了,识别图形的基本方法之一---霍夫变换,霍夫变换是图像处理中的一种特征提取技术,本篇章主要讲解霍夫圆变换。
Demo
前面2个Demo不怎么准确,是因为笔者对原图进行了缩放,导致原本的圆形有点变形了,最后一个则是专门按照缩放后的分辨率,自己绘制的。
霍夫变换
概述
霍夫变换(Hough Transform)是图像处理中的一种特征提取技术,改过程在一个参数空间中通过计算累计结果的局部最大值得到一个符合该特定形状的集合作为霍夫变换结果。
经典的霍夫变换用来检测图像中的直线,后来霍夫变换扩展到任意形状物体的识别,多为圆和椭圆。
霍夫变换运用两个坐标空间之间的变换将在一个空间中具有相同的形状的曲线或直线映射到另一个坐标控件的一个点上形成峰值,从而把检测任何形状的问题转化为统计峰值问题。
OpenCV中的霍夫变换分为两大类型,线变换下又分三种类型,如下图:
霍夫圆变换
概述
霍夫圆变换,从名字就可以知道其实针对圆,显而易见就是用来寻找圆的方法,此处特别注意,使用霍夫圆变换之前,肯定是需要对图片进行预处理:降噪、边缘检测处理,霍夫圆变换只寻找圆,只能识别边缘二值图像,所以输入也就只能是二值化(单通道8位)的图像了。
霍夫圆变换会找出大量的圆,但是有些圆其实是无用的,与霍夫线变换一样都可能会产生“噪声”数据。
原理
1.圆的图像二维空间可由笛卡尔坐标系表示
- 在笛卡尔坐标系(霍夫圆变换采用的方式):可有圆心(a,b),半径r表示;
在笛卡尔坐标系中圆的方式:
公式得出:
和
所以在abr组成的三维坐标系中,一个点可以唯一确定一个圆。
2.三维曲线的形成原理
在笛卡尔的xy坐标系中经过某一点的所有圆映射到abr坐标系中就是一条三维的曲线。
经过xy坐标系中所有的非零像素点的所有圆就构成了abr坐标系中很多条三维的曲线。
3.判断圆的依据
在xy坐标系中同一个圆上的所有点的圆方程是一样的,它们映射到abr坐标系中的是同一个点,所以在abr坐标系中该点就应该有圆的总像素N0个曲线相交。通过判断abr中每一点的相交(累积)数量,大于一定阈值的点就认为是圆。
以上是标准霍夫圆变换实现算法,问题是它的累加面是一个三维的空间,意味着比霍夫线变换需要更多的计算消耗。
Opencv霍夫圆变换对标准霍夫圆变换做了运算上的优化。它采用的是“霍夫梯度法”。它的检测思路是去遍历累加所有非零点对应的圆心,对圆心进行考量。
圆心一定是在圆上的每个点的模向量上,即在垂直于该点并且经过该点的切线的垂直线上,这些圆上的模向量的交点就是圆心。
霍夫梯度法就是要去查找这些圆心,根据该“圆心”上模向量相交数量的多少,根据阈值进行最终的判断。
霍夫梯度法原理
- 首先对图像进行边缘检测,比如用canny边缘检测;
- 对边缘图像中的每一个非零点,考虑其局部梯度,即用Sobel()函数计算x和y方向的Sobel一节导数得到梯度;
- 利用得到的梯度,有些率指定直线上的每一个点都在累加器中被累加,这里的斜率是从一个指定的最值到指定的最大值的距离;
- 同时,标记边缘图像中每一个非0像素的位置;
- 从二维累加器中这些点中选择候选的中心,这样中心都大于给定阈值并且大于其所有近邻。这些候选的中心按照累加值降序排列,一边最支持像素的中心首先出现;
- 对每个中心,考虑所有的非0像素;
- 这下像素按照其与中心的距离排列,从到最大半径的最小距离算起,选择非0像素最支持的一条半径;
- 如果一个中心收到边缘图像非0像素最充分的支持,并且到前期被选择的中心有足够的距离,那么它就会被保留下去;
霍夫梯度法优点
算法高效,并能够解决三维累加器中会产生许多噪声并且使得结果不稳定的稀疏不稳定问题;
霍夫梯度法缺点
- 使用Sobel导数来计算可能导致更多的噪声;
- 在边缘图像中整个非0像素都被当做候选中心,所以把累加器值设置偏低会导致点暴涨,以致于计算量较大,算法消耗时间过长;
- 中心按照其关联的累加器的升序排列的,并且如果新的中心过于接近之前已经接受的中心点的话,就不会保留该点,等于是会倾向于同心圆只保留最大半径的圆;
霍夫圆变换函数原型
-
void HoughCircles( InputArray image,
-
OutputArray circles,
-
int method,
-
double dp,
-
double minDist,
-
double param1 = 100,
-
double param2 = 100,
-
int minRadius = 0,
-
int maxRadius = 0);
- 参数一:InputArray类型的image,源图像8位,单通道二进制图像。可以将任意的原图载入进来,并由函数修改成此格式后,再填这里;
- 参数二:OutputArray类型的cirlces,输出圆向量,每个向量包括三个浮点型的元素——圆心横坐标,圆心纵坐标和圆半径;
- 参数三:int类型的method,为使用霍夫变换圆检测的算法;
序号 |
枚举 |
值 |
描述 |
1 |
HOUGH_STANDARD |
0 |
CV_HOUGH_STANDARD - 传统或标准 Hough 变换(SHT)。每一个线段由两个浮点数 (ρ, θ) 表示,其中 ρ 是直线与原点 (0,0) 之间的距离,θ 线段与 x-轴之间的夹角。因此,矩阵类型必须是 CV_32FC2 type; |
2 |
HOUGH_PROBABILISTIC |
1 |
CV_HOUGH_PROBABILISTIC- 概率 Hough 变换(PPHT)。如果图像包含一些长的线性分割,则效率更高。它返回线段分割而不是整个线段。每个分割用起点和终点来表示,所以矩阵(或创建的序列)类型是 CV_32SC4 type; |
3 |
HOUGH_MULTI_SCALE |
2 |
传统 Hough 变换的多尺度变种。线段的编码方式与 CV_HOUGH_STANDARD 的一致 |
4 |
HOUGH_GRADIENT |
3 |
基本上是 21HT |
- 参数四:double类型的dp,dp:寻找圆弧圆心的累计分辨率,这个参数允许创建一个比输入图像分辨率低的累加器。(这样做是因为有理由认为图像中存在的圆会自然降低到与图像宽高相同数量的范畴)。如果dp设置为1,则分辨率是相同的;如果设置为更大的值(比如2),累加器的分辨率受此影响会变小(此情况下为一半);dp的值不能比1小;
- 参数五:double类型的minDist,精度,该参数是让算法能明显区分的两个不同圆之间的最小距离;
- 参数六:double类型的param1,默认为100,用于Canny的边缘阀值上限,下限被置为上限的一半;
- 参数七:double类型的param2,默认为100,在霍夫梯度的情况下,它是检测阶段圆中心的累加器阈值。它越小,就越可以检测到更多根本不存在的圆,而越多,能通过检测的圆就是更加接近完美原型;
- 参数八:int类型的minRadius,默认为0,检测圆的最小半径;
- 参数九:int类型的maxRadius,默认为0,检测圆的最大半径,为0时,最大为像素矩阵大小;
Demo源码
-
void OpenCVManager::testHoughCircles()
-
{
-
QString fileName1 =
-
"E:/qtProject/openCVDemo/openCVDemo/modules/openCVManager/images/17.jpg";
-
cv::Mat srcMat = cv::imread(fileName1.toStdString());
-
int width =
400;
-
int height =
300;
-
-
cv::resize(srcMat, srcMat, cv::Size(width, height));
-
cv::Mat colorMat = srcMat.clone();
-
-
cv::String windowName = _windowTitle.toStdString();
-
cvui::init(windowName);
-
-
cv::Mat windowMat = cv::Mat(cv::Size(srcMat.cols *
2, srcMat.rows *
3),
-
srcMat.type());
-
-
cv::cvtColor(srcMat, srcMat, CV_BGR2GRAY);
-
-
int threshold1 =
200;
-
int threshold2 =
100;
-
int apertureSize =
1;
-
-
int dp =
10;
// 默认1像素
-
int minDist =
10;
// 默认1°
-
int minRadius =
0;
-
int maxRadius =
0;
-
-
while(
true)
-
{
-
qDebug() << __FILE__ << __LINE__;
-
windowMat = cv::Scalar(
0,
0,
0);
-
-
cv::Mat mat;
-
cv::Mat dstMat;
-
cv::Mat grayMat;
-
-
// 转换为灰度图像
-
// 原图先copy到左边
-
cv::Mat leftMat = windowMat(cv::Range(
0, srcMat.rows),
-
cv::Range(
0, srcMat.cols));
-
cv::cvtColor(srcMat, grayMat, CV_GRAY2BGR);
-
cv::addWeighted(leftMat,
0.0f, grayMat,
1.0f,
0.0f, leftMat);
-
-
{
-
cvui::
printf(windowMat,
-
width *
1 +
100,
-
height *
0 +
20,
-
"threshold1");
-
cvui::trackbar(windowMat,
-
width *
1 +
100,
-
height *
0 +
50,
-
200,
-
&threshold1,
-
0,
-
255);
-
cvui::
printf(windowMat,
-
width *
1 +
100,
-
height *
0 +
100,
"threshold2");
-
cvui::trackbar(windowMat,
-
width *
1 +
100,
-
srcMat.cols *
0 +
130,
-
200,
-
&threshold2,
-
0,
-
255);
-
-
qDebug() << __FILE__ << __LINE__;
-
cv::Canny(srcMat, dstMat, threshold1, threshold2, apertureSize *
2 +
1);
-
// copy
-
mat = windowMat(cv::Range(srcMat.rows *
1, srcMat.rows *
2),
-
cv::Range(srcMat.cols *
0, srcMat.cols *
1));
-
-
cv::cvtColor(dstMat, grayMat, CV_GRAY2BGR);
-
cv::addWeighted(mat,
0.0f, grayMat,
1.0f,
0.0f, mat);
-
-
cvui::
printf(windowMat,
-
width *
1 +
100,
-
height *
1 +
20 -
80,
-
"dp = value / 10");
-
cvui::trackbar(windowMat,
-
width *
1 +
100,
-
height *
1 +
50 -
80,
-
200,
-
&dp,
-
1,
-
1000);
-
cvui::
printf(windowMat,
-
width *
1 +
100,
-
height *
1 +
100 -
80,
-
"minDist = value / 2");
-
cvui::trackbar(windowMat,
-
width *
1 +
100,
-
height *
1 +
130 -
80,
-
200,
-
&minDist,
-
1,
-
720);
-
cvui::
printf(windowMat,
-
width *
1 +
100,
-
height *
1 +
180 -
80,
-
"minRadius");
-
cvui::trackbar(windowMat,
-
width *
1 +
100,
-
height *
1 +
210 -
80,
-
200,
-
&minRadius,
-
0,
-
100);
-
cvui::
printf(windowMat,
-
width *
1 +
100,
-
height *
1 +
260 -
80,
-
"maxRadius");
-
cvui::trackbar(windowMat,
-
width *
1 +
100,
-
height *
1 +
290 -
80,
-
200,
-
&maxRadius,
-
0,
-
1000);
-
// 边缘检测后,进行霍夫圆检测
-
std::
vector<cv::Vec3f> circles;
-
cv::HoughCircles(dstMat,
-
circles,
-
cv::HOUGH_GRADIENT,
-
dp /
10.0f,
-
minDist /
10.0f,
-
200,
-
100,
-
minRadius,
-
maxRadius);
-
// 在图中绘制出每条线段
-
dstMat = colorMat.clone();
-
for(
int index =
0; index < circles.size(); index++)
-
{
-
cv::
Point center(cvRound(circles[index][0]),
-
cvRound
(circles[index][1]));
-
int radius = cvRound(circles[index][
2]);
-
// 绘制圆心
-
cv::circle(dstMat, center,
3, cv::Scalar(
255,
255,
255));
-
// 绘制圆
-
cv::circle(dstMat, center, radius, cv::Scalar(
0,
0,
255));
-
}
-
// copy
-
mat = windowMat(cv::Range(srcMat.rows *
2, srcMat.rows *
3),
-
cv::Range(srcMat.cols *
1, srcMat.cols *
2));
-
cv::addWeighted(mat,
0.0f, dstMat,
1.0f,
0.0f, mat);
-
-
-
// 在图中绘制出每条线段
-
for(
int index =
0; index < circles.size(); index++)
-
{
-
cv::
Point center(cvRound(circles[index][0]),
-
cvRound
(circles[index][1]));
-
int radius = cvRound(circles[index][
2]);
-
// 绘制圆心
-
cv::circle(grayMat, center,
3, cv::Scalar(
255,
255,
255));
-
// 绘制圆
-
cv::circle(grayMat, center, radius, cv::Scalar(
0,
0,
255));
-
}
-
// copy
-
mat = windowMat(cv::Range(srcMat.rows *
2, srcMat.rows *
3),
-
cv::Range(srcMat.cols *
0, srcMat.cols *
1));
-
cv::addWeighted(mat,
0.0f, grayMat,
1.0f,
0.0f, mat);
-
}
-
// 更新
-
cvui::update();
-
// 显示
-
cv::imshow(windowName, windowMat);
-
// esc键退出
-
if(cv::waitKey(
25) ==
27)
-
{
-
break;
-
}
-
}
-
}
工程模板:对应版本号v1.39.0
对应版本号v1.39.0
原博主博客地址:https://blog.csdn.net/qq21497936
原博主博客导航:https://blog.csdn.net/qq21497936/article/details/102478062
本文章博客地址:https://blog.csdn.net/qq21497936/article/details/105575546
转载:https://blog.csdn.net/qq21497936/article/details/105575546