目录
参考链接:https://blog.csdn.net/qq_40584593/article/details/89299548
方法一:霍夫变换检测
HoughCircles()函数官方:
https://docs.opencv.org/3.4.2/dd/d1a/group__imgproc__feature.html#ga47849c3be0d0406ad3ca45db65a25d2d
实现代码:
/* redball_detect.cpp
Description: the test example for detect the red ball.
Date: 2017/10/12
*/
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;
#define SRC_WINDOW_NAME "redball"
#define MID_WINDOWNAME "redball_gray"
Mat srcImage, dstImage;
Mat channel[3];
int main()
{
// 原图像读取
srcImage = imread("redball5.jpg", 1);
imshow(SRC_WINDOW_NAME, srcImage);
// 提取红色通道图像
int g_nHm = 9; // 可利用滑动条调节
split(srcImage, channel);
channel[0] = channel[0].mul(.1*g_nHm); // B (mul: per-element matrix multiplication)
channel[1] = channel[1].mul(.1*g_nHm); // G
channel[2] = channel[2] - channel[0] - channel[1]; // R
channel[2] = 3 * channel[2];
imshow(MID_WINDOWNAME, channel[2]);
dstImage = channel[2];
GaussianBlur(dstImage, dstImage, Size(9, 9), 2, 2); // 用于减少检测噪声
// 霍夫圆检测
vector<Vec3f> circles; // 3通道float型向量
HoughCircles(dstImage, circles, CV_HOUGH_GRADIENT, 1, srcImage.rows / 5, 200, 16, 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(155, 50, 255), 3, 8, 0);
cout << circles[i][0] << "\t" << circles[i][1] << "\t" << circles[i][2] << endl;
}
// cout << circles[0][0] << endl;
imshow(SRC_WINDOW_NAME, srcImage);
waitKey(0);
return 0;
}
代码中的几点说明:
- 因为需要检测的红色球,所以提取RGB图像中的红色通道值更有利于检测;
- g_nHm是可调节参数,可以利用滑动条确定最好的值;
- GaussianBlur()函数用于抑制检测过程中的噪声,可以参考检测结果中使用GaussianBlur前后的对比。
方法二:轮廓查找
使用findContours函数
1.方法一:使用判断外接矩阵是否近似正方形。
fitEllipse()函数
经过多次试验,发现利用OpenCV的霍夫圆变换查找出来的圆,其圆心位置并不准确,而且参数调节较为麻烦。于是想到利用轮廓查找的方式来进行圆检测,我们可以通过判断轮廓的外接矩形的横纵比来判断该轮廓是否为圆形。一般而言,圆的外接矩形肯定近似于一个正方形,因此宽高比接近1.0 。总体思路如下:阈值分割->形态学/滤波降噪->轮廓查找->范围过滤->圆形拟合。代码如下,仅供参考。
int MinR = 100; int MaxR = 300;
Mat src_img, binary_img, dst_img;
src_img = imread("test.bmp", IMREAD_GRAYSCALE);//灰度图读入
threshold(src_img, binary_img, 158, 255, THRESH_BINARY );
imshow("trs", binary_img);
//imwrite("trs.bmp", binary_img);
//可以根据实际需求选择使用的滤波进行降噪
//GaussianBlur(binary_img, binary_img, Size(3, 3), 0, 0);
//boxFilter(binary_img, binary_img,-1, Size(3, 3));
//blur(binary_img, binary_img, Size(3, 3));
vector<vector<Point>> contours;
vector<Vec4i> hireachy;
// 形态学操作
Mat kernel = getStructuringElement(MORPH_RECT, Size(3, 3), Point(-1, -1));
// 构建形态学操作的结构元
morphologyEx(binary_img, binary_img, MORPH_CLOSE, kernel, Point(-1, -1));
//闭操作
kernel = getStructuringElement(MORPH_RECT, Size(3, 3), Point(-1, -1));
// 构建形态学操作的结构元
morphologyEx(binary_img, binary_img, MORPH_OPEN, kernel, Point(-1, -1));
//开操作
//imshow("开操作", dst_img);
findContours(binary_img, contours, hireachy, RETR_TREE, CHAIN_APPROX_SIMPLE, Point());
imshow("bf", binary_img);
//imwrite("bf.bmp", binary_img);
Mat result_img = src_img.clone();
cvtColor(result_img, result_img, COLOR_GRAY2BGR);
//灰度图转为彩色图
for (int i = 0; i < hireachy.size(); i++)
{
// drawContours(src_img, contours, i, Scalar(0, 0, 255), -1, 8, Mat(), 0, Point());
if (contours[i].size() < 5)continue;
double area = contourArea(contours[i]);
if (area < 2000)continue;
//晒选出轮廓面积大于2000的轮廓
double arc_length = arcLength(contours[i], true);
double radius = arc_length / (2 * PI);
if (!(MinR < radius && radius < MaxR))
{
continue;
}
RotatedRect rect = fitEllipse(contours[i]);
float ratio = float(rect.size.width) / float(rect.size.height);
if (ratio < 1.1 && ratio > 0.9)
//因为圆的外接直立矩形肯定近似于一个正方形,因此宽高比接近1.0
{
printf("X: %f\n", rect.center.x);
printf("Y: %f\n", rect.center.y);
printf("圆的面积: %f\n", area);
printf("圆的半径: %f\n", radius);
ellipse(result_img, rect, Scalar(0,255, 255),2);
circle(result_img, rect.center, 2, Scalar(0,255,0), 2, 8, 0);
}
}
imshow("dst", result_img);
imwrite("dst.bmp", result_img);
waitKey();
算法检测出的圆形较为准确,且参数易调。主要根据调节阈值筛选轮廓,也可以采用自适应阈值。根据用户的需求进行半径与圆面积的筛选,最后得到想要的圆。如有不足,还请指正。
1.方法二:使用椭圆来拟合
首先建立一个概念,任何镜头下的圆,很多情况下都不是真正的 pi*r*r 的圆,会因各种物体与镜头之间的非完全平行关系或光线与物体不是完全平行等因素,造成相机中成像的图像不是一直完全意义上的圆,更多的情况下,就是一个椭圆,所以opencv只提供了一个椭圆拟合的方法,其实当长短轴相等时,这就是一个真正的正圆了。
在建立这个观点后,采用众所周知的方法,就可以拟合圆了。
//图像文件名strImgName, canny的低阈值dThreshold1, canny的高阈值dThreshold2, canny的核大小iSize, 筛选圆的条件:圆的最小面积iMinArea, 圆的最小面积int iMaxArea
void findCircles(string strImgName, double dThreshold1, double dThreshold2, int iSize, int iMinArea, int iMaxArea)
{
Mat q_MatImage;
Mat q_MatImageGray;
Mat q_MatImageShow;
q_MatImage=imread(strImgName);//读入一张图片
q_MatImage.copyTo(q_MatImageShow);
//灰度化
cvtColor(q_MatImage,q_MatImageGray,CV_RGB2GRAY);
//Canny找边界,也可以阈值二值化
Mat cannyEdge;
Canny(q_MatImageGray, cannyEdge, dThreshold1, dThreshold2, iSize);
//找轮廓
vector<vector<Point>> q_vPointContours;
findContours(cannyEdge, q_vPointContours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE,Point(0,0));
//drawContours(q_MatImageShow, q_vPointContours, -1, Scalar(0,255,0));//显示轮廓
//筛选目标轮廓点
vector<Point> vfindContours;
size_t q_iAmountContours = q_vPointContours.size();
size_t iIndex = 0;
for ( iIndex = 0; iIndex < q_iAmountContours; iIndex++)
{
//根据圆的面积判断是否为目标圆
double ddarea = contourArea(q_vPointContours[iIndex]);
if((iMinArea < ddarea) && (iMaxArea > ddarea))
{
break;
}
}
//存储目标圆的轮廓点
size_t findCount = q_vPointContours[iIndex].size();
for(int i=0; i<findCount; i++)
vfindContours.push_back(q_vPointContours[iIndex][i]);
//采用椭圆拟合来得到圆
RotatedRect rectElli = fitEllipse(vfindContours);
float fR = MIN(rectElli.size.width , rectElli.size.height);// 是否为圆,可以比较这两个值,若十分接近或相等,就是一个正圆
cout << "fitEllipse 中心: " << rectElli.center.x << ", " <<rectElli.center.y << " 半径:"<<fR/2<< endl;
circle(q_MatImageShow, Point(rectElli.center), fR/2, Scalar(0,0,255), 2);//圆周
circle(q_MatImageShow, Point(rectElli.center), 5, Scalar(0,0,255), 3);//圆心
namedWindow("Test"); //创建一个名为Test窗口
imshow("Test",q_MatImageShow);//窗口中显示图像
waitKey();
}