霍夫变换是一种特征提取,被广泛应用在图像分析、电脑视觉以及数位影像处理。 霍夫变换的基本原理在于利用点与线的对偶性,将原始图像空间的给定的曲线通过曲线表达形式变为参数空间的一个点。这样就把原始图像中给定曲线的检测问题转化为寻找参数空间中的峰值问题。也即把检测整体特性转化为检测局部特性。
霍夫变换于1962年由Paul Hough 首次提出,后于1972年由Richard Duda和Peter Hart推广使用,经典霍夫变换用来检测图像中的直线,后来霍夫变换扩展到任意形状物体的识别,多为圆和椭圆。
霍夫变换在OpenCV中分为霍夫线变换和霍夫圆变换两种,我们下面将分别进行介绍。
众所周知, 一条直线在图像二维空间可由两个变量表示. 如:
对于霍夫变换, 我们将采用第二种方式极坐标系来表示直线. 因此, 直线的表达式可为:
y = ( − c o s θ s i n θ ) x + ( r s i n θ ) y = (-\frac{cos\theta}{sin\theta})x+(\frac{r}{sin\theta}) y=(−sinθcosθ)x+(sinθr)
即
r = x c o s θ + y s i n θ r = xcos\theta+ysin\theta r=xcosθ+ysinθ
对于任意一个点 ( x 0 , y 0 ) (x_0,y0) (x0,y0), 我们可以将通过这个点的一族直线统一定义为:
r = x c o s θ + y s i n θ r = xcos\theta+ysin\theta r=xcosθ+ysinθ
表示每一对 ( r θ , θ ) (r_{\theta},\theta) (rθ,θ)代表一条通过点 ( x 0 , y 0 ) (x_0,y_0) (x0,y0)的直线
如果对于一个给定点 ( x 0 , y 0 ) (x_0,y_0) (x0,y0),我们在极坐标对极径极角平面绘出所有通过它的直线, 将得到一条正弦曲线. 例如, 对于给定点 x 0 = 8 x_0= 8 x0=8 和 y 0 = 6 y_0= 6 y0=6 我们可以绘出下图 (在平面):
其中 r r r > 0, θ > 2 π \theta > 2\pi θ>2π
我们可以对图像中所有的点进行上述操作。如果两个不同点进行上述操作后得到的曲线在平面 θ − r \theta-r θ−r相交, 这就意味着它们通过同一条直线. 例如,使用上面的例子我们继续对点 ( x 1 , y 1 ) (x_1,y_1) (x1,y1)和点 ( x 2 , y 2 ) (x_2,y_2) (x2,y2)绘图, 得到下图:
这三条曲线在平面相交于点 (0.925, 9.6), 坐标表示的是参数对 ( θ , r ) (\theta,r) (θ,r)或者是经过点 ( x 0 , y 0 ) (x_0,y_0) (x0,y0) 点 ( x 1 , y 1 ) (x_1,y_1) (x1,y1)和点 ( x 3 , y 3 ) (x_3,y_3) (x3,y3)组成的平面内的的直线。
综上所述,一般来说, 一条直线能够通过在平面 ( θ , r ) (\theta,r) (θ,r) 寻找交于一点的曲线数量来检测。而越多曲线交于一点也就意味着这个交点表示的直线由更多的点组成. 。通常,我们可以定义检测一条线所需的最小交叉点数量的阈值。
这就是霍夫线变换的作用。它跟踪图像中每个点的曲线之间的交点。如果交叉点的数量大于某个阈值,那么可以认为这个交点所代表的参数对 ( θ , r ) (\theta,r) (θ,r)在原图像中为一条直线。
opencv中主要支持两种霍夫变换:标准霍夫变换和统计概率霍夫变换
void HoughLines(InputArray image, OutputArray lines, double rho,
double theta, int threshold, double srn=0, double stn=0 )
算法流程
void HoughLinesP(InputArray image, OutputArray lines, double rho, double theta,
int threshold, double minLineLength=0, double maxLineGap=0 )
标准霍夫变换本质上是把图像映射到它的参数空间上,它需要计算所有的M个边缘点,这样它的运算量和所需内存空间都会很大。如果在输入图像中只是处理m(m
算法流程
struct LinePolar
{
float rho;
float angle;
};
struct hough_cmp_gt
{
hough_cmp_gt(const int* _aux) : aux(_aux) {}
inline bool operator()(int l1, int l2) const
{
return aux[l1] > aux[l2] || (aux[l1] == aux[l2] && l1 < l2);
}
const int* aux;
};
static void createTrigTable(int numangle, double min_theta, double theta_step,
float irho, float *tabSin, float *tabCos)
{
float ang = static_cast<float>(min_theta);
for (int n = 0; n < numangle; ang += (float)theta_step, n++)
{
tabSin[n] = (float)(sin((double)ang) * irho);
tabCos[n] = (float)(cos((double)ang) * irho);
}
}
static void findLocalMaximums(int numrho, int numangle, int threshold,
const int *accum, std::vector<int>& sort_buf)
{
for (int r = 0; r < numrho; r++)
for (int n = 0; n < numangle; n++)
{
//得到当前值在累加器数组的位置
int base = (n + 1) * (numrho + 2) + r + 1;
//得到计数值,并以它为基准,看看它是不是局部极大值
if (accum[base] > threshold &&
accum[base] > accum[base - 1] && accum[base] >= accum[base + 1] &&
accum[base] > accum[base - numrho - 2] && accum[base] >= accum[base + numrho + 2])
sort_buf.push_back(base);//把极大值位置存入排序数组内——sort_buf
}
}
static void HoughLinesStandard(const Mat& img, std::vector<Vec2f>& lines, float rho,
float theta,int threshold, int linesMax,double min_theta, double max_theta)
{
int i, j;
float irho = 1 / rho;
//保证输入的图片的正确性
CV_Assert(img.type() == CV_8UC1);
//得到图像的指针
const uchar* image = img.ptr();
int step = (int)img.step; //得到图像的步长
int width = img.cols; //图像的宽
int height = img.rows; //图像的高
if (max_theta < min_theta) {
CV_Error(CV_StsBadArg, "max_theta must be greater than min_theta");
}
//由角和距离的分辨率得到角度和距离的数量,即霍夫变换后角度和距离的个数
int numangle = cvRound((max_theta - min_theta) / theta); //霍夫空间,角度方向的大小
int numrho = cvRound(((width + height) * 2 + 1) / rho); //r的范围,这里以图像的周长作为rho的最大值
//_accum为累加器数组,初始化该霍夫空间
Mat _accum = Mat::zeros((numangle + 2), (numrho + 2), CV_32SC1);
std::vector<int> _sort_buf;
AutoBuffer<float> _tabSin(numangle);
AutoBuffer<float> _tabCos(numangle);
int *accum = _accum.ptr<int>();
float *tabSin = _tabSin, *tabCos = _tabCos;
// 事先计算好sinθi/ρ和cosθi/ρ,查表
createTrigTable(numangle, min_theta, theta,
irho, tabSin, tabCos);
////执行步骤1,逐点进行霍夫空间变换,并把结果放入累加器数组内
for (i = 0; i < height; i++)
for (j = 0; j < width; j++)
{
if (image[i * step + j] != 0)
for (int n = 0; n < numangle; n++)
{
//根据公式: ρ = xcosθ + ysinθ
//cvRound()函数:四舍五入
int r = cvRound(j * tabCos[n] + i * tabSin[n]);
//因为theta是从0到π的,所以cos(theta)是有负的,所以就所有的r += 最大值的一半,让极径都>0
r += (numrho - 1) / 2;
//r表示的是距离,n表示的是角点,在累加器内找到它们所对应的位置(即霍夫空间内的位置),其值加1
accum[(n + 1) * (numrho + 2) + r + 1]++;
}
}
// 执行步骤2,找到局部极大值,即非极大值抑制
// 霍夫空间,局部最大点,采用四邻域判断,比较。(也可以使8邻域或者更大的方式),如果不判断局部最大值,同时选用次大值与最大值,就可能会是两个相邻的直线,但实际是一条直线。
// 选用最大值,也是去除离散的近似计算带来的误差,或合并近似曲线。
findLocalMaximums(numrho, numangle, threshold, accum, _sort_buf);
//执行步骤3,对存储在sort_buf数组内的累加器的数据按由大到小的顺序进行排序
std::sort(_sort_buf.begin(), _sort_buf.end(), hough_cmp_gt(accum));
// stage 4. store the first min(total,linesMax) lines to the output buffer 输出直线
//linesMax是参数,表示最多输出几条直线
linesMax = std::min(linesMax, (int)_sort_buf.size());
//事先定义一个尺度
double scale = 1. / (numrho + 2);
for (i = 0; i < linesMax; i++)
{
//LinePolar 直线的数据结构
//LinePolar结构在该文件的前面被定义
LinePolar line;
//idx为极大值在累加器数组的位置
int idx = _sort_buf[i];
//分离出该极大值在霍夫空间中的位置
//因为n是从0开始的,而之前为了防止越界,所以将所有的n+1了,因此下面要-1,同理r
int n = cvFloor(idx*scale) - 1;
int r = idx - (n + 1)*(numrho + 2) - 1;
line.rho = (r - (numrho - 1)*0.5f) * rho; //因为之前统一将r += (numrho - 1) / 2, 因此需要还原以获得真实的rho
line.angle = static_cast<float>(min_theta) + n * theta;
lines.push_back(Vec2f(line.rho, line.angle)); //用序列存放多条直线
}
}
static void HoughLinesProbabilistic(Mat& image, std::vector<Vec4i>& lines, float rho, float theta,
int threshold,int lineLength, int lineGap, int linesMax)
{
Point pt;
float irho = 1 / rho;
RNG rng((uint64)-1); //随机数
CV_Assert(image.type() == CV_8UC1);
int width = image.cols;
int height = image.rows;
int numangle = cvRound(CV_PI / theta);
int numrho = cvRound(((width + height) * 2 + 1) / rho);
//accum为累加器矩阵,霍夫空间,mask为掩码矩阵,大小与输入图像相同
Mat accum = Mat::zeros(numangle, numrho, CV_32SC1);
Mat mask(height, width, CV_8UC1);
//存储事先计算好的正弦余弦值
std::vector<float> trigtab(numangle * 2);
//事先计算好所需的所有正弦和余弦值
for (int n = 0; n < numangle; n++)
{
trigtab[n * 2] = (float)(cos((double)n*theta) * irho);
trigtab[n * 2 + 1] = (float)(sin((double)n*theta) * irho);
}
//复制首地址
const float* ttab = &trigtab[0];
uchar* mdata0 = mask.ptr();
std::vector<Point> nzloc;
// 步骤一:收集图像中的所有非零点,因为输入图像是边缘图像,所以非零点就是边缘点
for (pt.y = 0; pt.y < height; pt.y++)
{
//提取出输入图像和掩码矩阵的每行地址指针
const uchar* data = image.ptr(pt.y);
uchar* mdata = mask.ptr(pt.y);
for (pt.x = 0; pt.x < width; pt.x++)
{
if (data[pt.x])//是非零点
{
mdata[pt.x] = (uchar)1; //掩码相应位置置为1
nzloc.push_back(pt); //将该点加入序列中
}
else
mdata[pt.x] = 0;
}
}
//得到边缘点的数量
int count = (int)nzloc.size();
// 步骤二:随机处理所有的边缘点
for (; count > 0; count--)
{
// 在剩下的边缘点中随机选择一个点,idx为不大于count的随机数
int idx = rng.uniform(0, count);
//max_val为累加器的最大值,max_n为最大值所对应的角度
int max_val = threshold - 1, max_n = 0;
Point point = nzloc[idx];
Point line_end[2]; //定义直线的两个端点
float a, b;
//累加器的地址指针,也就是霍夫空间的地址指针
int* adata = accum.ptr<int>();
int i = point.y, j = point.x, k, x0, y0, dx0, dy0, xflag;
int good_line;
const int shift = 16;
//用序列中的最后一个元素替换被随机提取出来的元素
nzloc[idx] = nzloc[count - 1];
//检测这个坐标点是否已经计算过,也就是它已经属于其他直线
//因为计算过的坐标点会在掩码矩阵mask的相对应位置清零
if (!mdata0[i*width + j])
continue;
// 更新累加器矩阵,找到最有可能的直线
for (int n = 0; n < numangle; n++, adata += numrho)
{
//由角度计算距离
int r = cvRound(j * ttab[n * 2] + i * ttab[n * 2 + 1]);
r += (numrho - 1) / 2;
int val = ++adata[r];
if (max_val < val)
{
max_val = val;
max_n = n;
}
}
// 如果上面得到的最大值小于阈值,则放弃该点,继续下一个点的计算
if (max_val < threshold)
continue;
//从当前点出发,沿着它所在直线的方向前进,直到达到端点为止
a = -ttab[max_n * 2 + 1]; //a=-sinθ
b = ttab[max_n * 2]; //b = cosθ
x0 = j;
y0 = i;
//确定当前点所在直线的角度是在45度~135度之间,还是在0~45或135度~180度之间
//如过是在45度~135度之间
if (fabs(a) > fabs(b))
{
xflag = 1;//置标识位,标识直线的粗略方向
//确定横、纵坐标的位移量
dx0 = a > 0 ? 1 : -1;
dy0 = cvRound(b*(1 << shift) / fabs(a));
y0 = (y0 << shift) + (1 << (shift - 1));
}
//在0~45或135度~180度之间
else
{
xflag = 0; //清标志位
dy0 = b > 0 ? 1 : -1;
dx0 = cvRound(a*(1 << shift) / fabs(b));
x0 = (x0 << shift) + (1 << (shift - 1));
}
//搜索直线的两个端点
for (k = 0; k < 2; k++)
{
//gap表示两条直线的间隙,x和y为搜索位置,dx和dy为位移量
int gap = 0, x = x0, y = y0, dx = dx0, dy = dy0;
//搜索第二个端点的时候,反方向位移
if (k > 0)
dx = -dx, dy = -dy;
//沿着直线的方向位移,直到到达图像的边界或大的间隙为止
for (;; x += dx, y += dy)
{
uchar* mdata;
int i1, j1;
if (xflag)//确定新的位移后的坐标位置
{
j1 = x;
i1 = y >> shift;
}
else
{
j1 = x >> shift;
i1 = y;
}
//如果到达了图像的边界,停止位移,退出循环
if (j1 < 0 || j1 >= width || i1 < 0 || i1 >= height)
break;
//定位位移后掩码矩阵位置
mdata = mdata0 + i1*width + j1;
// for each non-zero point:
// update line end,
// clear the mask element
// reset the gap
//该掩码不为0,说明该点可能是在直线上
if (*mdata)
{
gap = 0;//设置间隙为0
//更新直线的端点位置
line_end[k].y = i1;
line_end[k].x = j1;
}
//掩码为0,说明不是直线,但仍继续位移,直到间隙大于所设置的阈值为止
else if (++gap > lineGap)
break;
}
}
//由检测到的直线的两个端点粗略计算直线的长度
//当直线长度大于所设置的阈值时,good_line为1,否则为0
good_line = std::abs(line_end[1].x - line_end[0].x) >= lineLength ||
std::abs(line_end[1].y - line_end[0].y) >= lineLength;
//再次搜索端点,目的是更新累加器矩阵和更新掩码矩阵,以备下一次循环使用
for (k = 0; k < 2; k++)
{
int x = x0, y = y0, dx = dx0, dy = dy0;
if (k > 0)
dx = -dx, dy = -dy;
// walk along the line using fixed-point arithmetics,
// stop at the image border or in case of too big gap
for (;; x += dx, y += dy)
{
uchar* mdata;
int i1, j1;
if (xflag)
{
j1 = x;
i1 = y >> shift;
}
else
{
j1 = x >> shift;
i1 = y;
}
mdata = mdata0 + i1*width + j1;
// for each non-zero point:
// update line end,
// clear the mask element
// reset the gap
if (*mdata)
{
//if语句的作用是清除那些已经判定是好的直线上的点对应的累加器的值,避免再次利用这些累加值
if (good_line)
{
adata = accum.ptr<int>();
for (int n = 0; n < numangle; n++, adata += numrho)
{
int r = cvRound(j1 * ttab[n * 2] + i1 * ttab[n * 2 + 1]);
r += (numrho - 1) / 2;
adata[r]--;//相应的累加器减1
}
}
//搜索过的位置,不管是好的直线,还是坏的直线,掩码相应位置都清0,这样下次就不会再重复搜索这些位置了,
//从而达到减小计算边缘点的目的
*mdata = 0;
}
//如果已经到达了直线的端点,则退出循环
if (i1 == line_end[k].y && j1 == line_end[k].x)
break;
}
}
//如果是好的直线
if (good_line)
{
Vec4i lr(line_end[0].x, line_end[0].y, line_end[1].x, line_end[1].y);
//把两个端点压入序列中
lines.push_back(lr);
//如果检测到的直线数量大于阈值,则退出该函数
if ((int)lines.size() >= linesMax)
return;
}
}
}
int main()
{
Mat dst, cdst, cdstP;
// 载入源图
Mat src = imread("3.jpg", IMREAD_GRAYSCALE);
// 边缘检测
Canny(src, dst, 50, 200, 3);
//转换为灰度图
cvtColor(dst, cdst, COLOR_GRAY2BGR);
cdstP = cdst.clone();
// 标准霍夫变换
vector<Vec2f> lines; //定义一个矢量结构lines用于存放得到的线段矢量集合
HoughLines(dst, lines, 1, CV_PI / 180, 90, 0, 0); // runs the actual detection
// Draw the lines
//HoughLinesStandard(dst, lines, 1, CV_PI / 180, 90, INT_MAX,0,CV_PI);
//依次在图中绘制出每条线段
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(cdst, pt1, pt2, Scalar(0, 0, 255), 1, CV_AA);
}
// Probabilistic Line Transform
vector<Vec4i> linesP; // will hold the results of the detection
//统计概率霍夫变换
HoughLinesP(dst, linesP, 1, CV_PI / 180, 80, 10, 5); // runs the actual detection
// Draw the lines
//HoughLinesProbabilistic(dst, linesP, 1, CV_PI / 180, 80, 10, 5,INT_MAX);
for (size_t i = 0; i < linesP.size(); i++)
{
Vec4i l = linesP[i];
line(cdstP, Point(l[0], l[1]), Point(l[2], l[3]), Scalar(0, 0, 255), 1, LINE_AA);
}
// Show results
imshow("src_pic", src);
imshow("HoughLines", cdst);
imshow("HoughLinesP", cdstP);
// Wait and Exit
waitKey();
return 0;
}
霍夫圆变换的基本原理和上面讲的霍夫线变化大体上是很类似的,只是点对应的二维极径极角空间被三维的圆心点x, y还有半径r空间取代。说“大体上类似”的原因是,如果完全用相同的方法的话,累加平面会被三维的累加容器所代替:在这三维中,一维是 x,一维是 y,另外一维是圆的半径 r。这就意味着需要大量的内存而且执行效率会很低,速度会很慢。
对直线来说, 一条直线能由参数极径极角 ( r , θ ) (r,\theta) (r,θ)表示. 而对圆来说, 我们需要三个参数来表示一个圆, 也就是: C : ( x c e n t e r , y c e n t e r , r ) C:(x_{center},y_{center},r) C:(xcenter,ycenter,r)
这里的 表示圆心的位置 (下图中的绿点) 而 r 表示半径, 这样我们就能唯一的定义一个圆了, 见下图:
在OpenCV中,我们一般通过一个叫做“霍夫梯度法”的方法来解决圆变换的问题。
这个实现可以使算法执行起来更高效,或许更加重要的是,能够帮助解决三维累加器中会产生许多噪声并且使得结果不稳定的稀疏分布问题。
缺陷
void HoughCircles(InputArray image,OutputArray circles, int method,double dp, double minDist, double param1=100,double param2=100, int minRadius=0, int maxRadius=0 )
static void HoughCirclesGradient(InputArray _image, OutputArray _circles, float dp, float minDist,
int minRadius, int maxRadius, int cannyThreshold,
int accThreshold, int maxCircles, int kernelSize, bool centersOnly)
{
CV_Assert(kernelSize == -1 || kernelSize == 3 || kernelSize == 5 || kernelSize == 7);
//控制dp不能比1小
dp = max(dp, 1.f);
float idp = 1.f / dp;
Mat edges, dx, dy;//edges表示图像边缘矩阵
//Sobel算子,一阶导数边缘检测算子.dx,dy分别表示x和y方向的差分阶数,kernersize核为3,
Sobel(_image, dx, CV_16S, 1, 0, kernelSize, 1, 0, BORDER_REPLICATE);
Sobel(_image, dy, CV_16S, 0, 1, kernelSize, 1, 0, BORDER_REPLICATE);
Canny(dx, dy, edges, std::max(1, cannyThreshold / 2), cannyThreshold, false);
Mutex mtx;//互斥信号量
//设置多线程数
int numThreads = std::max(1, getNumThreads());
//三维霍尔空间
std::vector<Mat> accumVec;
NZPointSet nz(_image.rows(), _image.cols());
//沿着梯度和梯度的反方向,并行计算边缘图像每个像素点
parallel_for_(Range(0, edges.rows),
HoughCirclesAccumInvoker(edges, dx, dy, minRadius, maxRadius, idp, accumVec, nz, mtx),
numThreads);
//计算圆周点的总数
int nzSz = cv::countNonZero(nz.positions);
if (nzSz <= 0)
return;
Mat accum = accumVec[0];
//二维累加器中每个候选中心点
for (size_t i = 1; i < accumVec.size(); i++)
{
accum += accumVec[i];
}
accumVec.clear();
std::vector<int> centers;
// 4 rows when multithreaded because there is a bit overhead
// and on the other side there are some row ranges where centers are concentrated
//并行遍历整个累加器矩阵,找到可能的圆心
parallel_for_(Range(1, accum.rows - 1),
HoughCirclesFindCentersInvoker(accum, centers, accThreshold, mtx),
(numThreads > 1) ? ((accum.rows - 2) / 4) : 1);
//计算圆心的总数
int centerCnt = (int)centers.size();
if (centerCnt == 0)
return;
//对圆心按照由大到小的顺序进行排序
std::sort(centers.begin(), centers.end(), hough_cmp_gt(accum.ptr<int>()));
std::vector<Vec3f> circles;
circles.reserve(256);
if (centersOnly)
{
// 最大半径小于0时,只能得到一个圆心
GetCircleCenters(centers, circles, accum.cols, minDist, dp);
}
else
{
std::vector<EstimatedCircle> circlesEst;
//分两种情况计算圆周半径,一种使用列表法,一种用矩阵法。
if (nzSz < maxRadius * maxRadius)
{
// Faster to use a list
NZPointList nzList(nzSz);
nz.toList(nzList);
// One loop iteration per thread if multithreaded.
//并行计算圆周的半径
parallel_for_(Range(0, centerCnt),
HoughCircleEstimateRadiusInvoker<NZPointList>(nzList, nzSz, centers, circlesEst, accum.cols,
accThreshold, minRadius, maxRadius, dp, mtx),
numThreads);
}
else
{
// 矩阵法,遍历圆周中心,并行计算圆周半径
parallel_for_(Range(0, centerCnt),
HoughCircleEstimateRadiusInvoker<NZPointSet>(nz, nzSz, centers, circlesEst, accum.cols,
accThreshold, minRadius, maxRadius, dp, mtx),
numThreads);
}
// Sort by accumulator value
std::sort(circlesEst.begin(), circlesEst.end(), cmpAccum);
//给定的GetCircle将被连续调用n-1次。结果保存在circles中
std::transform(circlesEst.begin(), circlesEst.end(), std::back_inserter(circles), GetCircle);
//剔除半径小于minRadius的半径圆
RemoveOverlaps(circles, minDist);
}
//返回所有的圆集合
if (circles.size() > 0)
{
int numCircles = std::min(maxCircles, int(circles.size()));
_circles.create(1, numCircles, CV_32FC3);
Mat(1, numCircles, CV_32FC3, &circles[0]).copyTo(_circles.getMat());
return;
}
}
int main()
{
// Loads an image
Mat src = imread("1.jpg", IMREAD_COLOR);
Mat gray;
cvtColor(src, gray, COLOR_BGR2GRAY);
medianBlur(gray, gray, 5);
vector<Vec3f> circles;
HoughCircles(gray, circles, HOUGH_GRADIENT, 1, gray.rows / 16, 100, 30, 1, 50);
//HoughCircles1(gray, circles, HOUGH_GRADIENT, 1,gray.rows / 16,100, 30, 1, 50,-1,3);
for (size_t i = 0; i < circles.size(); i++)
{
Vec3i c = circles[i];
Point center = Point(c[0], c[1]);
// circle center
circle(src, center, 1, Scalar(0, 100, 100), 3, LINE_AA);
// circle outline
int radius = c[2];
circle(src, center, radius, Scalar(0, 255, 0), 3, LINE_AA);
}
imshow("detected circles", src);
waitKey();
return 0;
}
https://docs.opencv.org/3.4.1/d4/d70/tutorial_hough_circle.html.
https://www.cnblogs.com/kk17/p/9693132.html.
https://docs.opencv.org/3.4.1/d4/d70/tutorial_hough_circle.html.