最小二乘法拟合圆心公式推导及基于opencv的程序实现

最小二乘法拟合圆心

文章为个人学习过程中笔记,原理部分参考其他作者内容,侵权必删
最小二乘法(least squares analysis)是一种数学优化技术,它通过最小化误差的平方和找到一组数据的最佳函数匹配。最小二乘法是用最简的方法求得一些绝对不可知的真值,而令误差平方之和为最小来寻找一组数据的最佳匹配函数的计算方法,最小二乘法通常用于曲线拟合 (least squares fitting) 。最小二乘圆拟合方法是一种基于统计的检测方法,即便是图像中圆形目标受光照强度不均等因素的影响而产生边缘缺失,也不会影响圆心的定位和半径的检测,若边缘定位精确轮廓清晰,最小二乘法可实现亚像素级别的精确拟合定位。

1.1公式推导过程

原理部分来自博客:Jacky_Ponder
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.基于opencv数据结构的c++代码实现

下面是一个最小二乘拟合圆心的函数:
参数 输入1.存储2d点坐标的向量 输出2.圆心坐标点 输出double类型的圆的半径

void CircleFitting(
	std::vector<cv::Point> &pts,		// 2d点坐标存储向量
	cv::Point2d& center,				//圆心坐标
	double& radius)						//圆的半径
{
	center = cv::Point2d(0, 0);			//初始化圆心为(0,0)
	radius = 0.0;						//半径为0
//	if (pts.size() < 3) return false;	//判断输入点的个数若小于三个则不能拟合

	//定义计算中间变量
	double sumX1 = 0.0; //代表Xi的和(从1~n) ,X1代表X的1次方
	double sumY1 = 0.0;
	double sumX2 = 0.0;	//代表(Xi)^2的和(i从1~n),X2代表X的二次方
	double sumY2 = 0.0;
	double sumX3 = 0.0;
	double sumY3 = 0.0;
	double sumX1Y1 = 0.0;
	double sumX1Y2 = 0.0;
	double sumX2Y1 = 0.0;
	const double N = (double)pts.size();//获得输入点的个数
	for (int i = 0; i < pts.size(); ++i)
	{
		double x = pts.at(i).x;			//获得第i个点的x坐标
		double y = pts.at(i).y;			//获得第i个点的y坐标
		double x2 = x * x;				//计算x^2
		double y2 = y * y;				//计算y^2
		double x3 = x2 * x;				//计算x^3
		double y3 = y2 * y;				//计算y^3
		double xy = x * y;				//计算xy
		double x1y2 = x * y2;			//计算x*y^2
		double x2y1 = x2 * y;			//计算x^2*y

		sumX1 += x;						//sumX=sumX+x;计算x坐标的和
		sumY1 += y;						//sumY=sumY+y;计算y坐标的和
		sumX2 += x2;					//计算x^2的和
		sumY2 += y2;					//计算各个点的y^2的和
		sumX3 += x3;					//计算x^3的和
		sumY3 += y3;
		sumX1Y1 += xy;
		sumX1Y2 += x1y2;
		sumX2Y1 += x2y1;
	}
	double C = N * sumX2 - sumX1 * sumX1;
	double D = N * sumX1Y1 - sumX1 * sumY1;
	double E = N * sumX3 + N * sumX1Y2 - (sumX2 + sumY2) * sumX1;
	double G = N * sumY2 - sumY1 * sumY1;
	double H = N * sumX2Y1 + N * sumY3 - (sumX2 + sumY2) * sumY1;

	double denominator = C * G - D * D;
//	if (std::abs(denominator) < DBL_EPSILON) return false;//判断分母的绝对值是否接近于(等于)0,使用此行判断须将函数改为bool类型,才能有返回值
	double a = (H * D - E * G) / (denominator);
	//denominator = D * D - G * C;
	//if (std::abs(denominator) < DBL_EPSILON) return false;
	double b = (H * C - E * D) / (-denominator);
	double c = -(a * sumX1 + b * sumY1 + sumX2 + sumY2) / N;

	center.x = a / (-2);
	center.y = b / (-2);
	radius = std::sqrt(a * a + b * b - 4 * c) / 2;
//	return true;将函数改为bool类型需要加返回值
}

以下为自己修改的程序,主要功能如下:
测试图片在下方
1.输入一张图片进行 高斯滤波和边缘检测检测出圆形led的边缘
2.使用opencv提供的findContours()函数将提取的边缘存储于Circle_Data中
3.调用CircleFitting()函数进行最小二乘圆拟合,计算的结果存放于向量Circle_Center 和向量Circle_R中

#include<iostream>
#include<opencv2/opencv.hpp>

//****函数声明圆心拟合函数
void CircleFitting(
	std::vector<std::vector<cv::Point>>& pts,
	std::vector<cv::Point2d>& center, std::vector<double>& radius);
//****函数声明end

int main(int argc, char**argv) {

	//【0】输入图像并进行高斯滤波和canny边缘检测
	cv::Mat srcImg = cv::imread("Led_Point.jpg", 1);//以原图的形式输入RGB
	if (!srcImg.data) { std::cout << "enter srcImg wrong" << std::endl;	return false; }

	cv::Mat srcImg_1 = srcImg.clone();	//复制到srcImg_1中
	cv::Mat gray, dstImg;		//原图的灰度图,canny检测后输出

	cv::cvtColor(srcImg_1, gray, cv::COLOR_BGR2GRAY);//gray为滤波之前的灰度图
	cv::GssianBlur(gray, gray, cv::Size(3, 3), 0, 0, cv::BORDER_DEFAULT);

	cv::Canny(gray, gray, 3, 9, 3);//边缘检测后的图存放于gray中,单通道
	//std::cout << "gray.channels=" << gray.channels() << std::endl;
	dstImg.create(srcImg.size(), srcImg.type());//创建与原图相同尺寸和类型的dstImg
	dstImg = cv::Scalar::all(0);
	srcImg_1.copyTo(dstImg, gray);
	//测试通道数用
	//std::cout << "dstImg.channels=" << dstImg.channels() << std::endl;//三通道
	cv::cvtColor(dstImg, dstImg, cv::COLOR_BGR2GRAY);//转化为单通道灰度图
	cv::threshold(dstImg, dstImg, 100, 255, cv::THRESH_BINARY);//转化为二值图(单通道)

	//std::cout << "dstImg=" << dstImg.channels() << std::endl;//不使用cvtColor则为三通道

	//[1]使用opencv提供的findContours()函数将检测到的边缘按每个边缘一组存放于Circle_Data中
	std::vector<std::vector<cv::Point>> Circle_Data;//Circle_Data用于存放多个led边缘的坐标
	std::vector<cv::Vec4i> hierarchy;
	//函数findContours参数:dstImg必须为单通道图像
	cv::findContours(dstImg, Circle_Data, hierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_NONE, cv::Point());

	//测试函数findContours输出是否正确
	//for (int i = 0; i < Circle_Data.size(); i++) {
	//	//Circle_Data[i]代表的是第i个轮廓,Circle_Data[i].size()代表的是第i个轮廓上所有的像素点数  
	//	for (int j = 0; j < Circle_Data[i].size(); j++) {
	//		//每个点坐标为: cv::point(contours[i][j].x, contours[i][j].y);
	//		std::cout << "【" << i << "】" << "(" << Circle_Data[i][j].x << "," << Circle_Data[i][j].y << ")" << std::endl;
	//	}
	//}

	//[3]创建存储每个led轮廓的圆心坐标向量和半径存储向量进行最小二乘曲线拟合拟合圆心坐标和圆的半径
	std::vector<cv::Point2d> Circle_Center;	//声明用于存储圆心坐标的向量
	std::vector<double> Circle_R;			//声明用于存储半径的向量

	//函数可处理多个圆轮廓的圆心和半径的拟合
	CircleFitting(Circle_Data, Circle_Center, Circle_R);


	//测试用:输出圆的坐标和半径
	for (int i = 0; i < Circle_Data.size(); i++) {

		std::cout << "Circle_Center[" << i << "]= (" << Circle_Center[i].x << "," << Circle_Center[i].y << ")" << std::endl;
		std::cout << "Circle_R[" << i << "]=" << Circle_R[i] << std::endl;
	}


	system("PAUSE");//用于保持输出终端
	return 0;
}


//最小二乘拟合圆心原理https://blog.csdn.net/Jacky_Ponder/article/details/70314919
void CircleFitting(
	std::vector<std::vector<cv::Point>> &pts,		// 按组存储2d点坐标
	std::vector<cv::Point2d>& center,				//圆心坐标
	std::vector<double>& radius)						//圆的半径
{
	for (int k = 0; k < pts.size(); k++) {
		//	std::cout << "k=" << pts.size() << std::endl;
		center.push_back(cv::Point2d(0, 0));			//初始化圆心为(0,0)
		radius.push_back(0.0);						//半径为0
	//	if (pts.size() < 3) return false;	//判断输入点的个数若小于三个则不能拟合

		//定义计算中间变量
		double sumX1 = 0.0; //代表Xi的和(从1~n) ,X1代表X的1次方
		double sumY1 = 0.0;
		double sumX2 = 0.0;	//代表(Xi)^2的和(i从1~n),X2代表X的二次方
		double sumY2 = 0.0;
		double sumX3 = 0.0;
		double sumY3 = 0.0;
		double sumX1Y1 = 0.0;
		double sumX1Y2 = 0.0;
		double sumX2Y1 = 0.0;
		const double N = (double)pts[k].size();//获得第k组输入点的个数
		for (int i = 0; i < pts[k].size(); ++i)//遍历第k组中所有数据
		{
			double x = 0;
			double y = 0;
			x = pts[k][i].x;			//获得第k组中第i个点的x坐标
			y = pts[k][i].y;			//获得第k组中第i个点的y坐标
			double x2 = x * x;				//计算x^2
			double y2 = y * y;				//计算y^2
			double x3 = x2 * x;				//计算x^3
			double y3 = y2 * y;				//计算y^3
			double xy = x * y;				//计算xy
			double x1y2 = x * y2;			//计算x*y^2
			double x2y1 = x2 * y;			//计算x^2*y

			sumX1 += x;						//sumX=sumX+x;计算x坐标的和
			sumY1 += y;						//sumY=sumY+y;计算y坐标的和
			sumX2 += x2;					//计算x^2的和
			sumY2 += y2;					//计算各个点的y^2的和
			sumX3 += x3;					//计算x^3的和
			sumY3 += y3;
			sumX1Y1 += xy;
			sumX1Y2 += x1y2;
			sumX2Y1 += x2y1;
		}
		double C = N * sumX2 - sumX1 * sumX1;
		double D = N * sumX1Y1 - sumX1 * sumY1;
		double E = N * sumX3 + N * sumX1Y2 - (sumX2 + sumY2) * sumX1;
		double G = N * sumY2 - sumY1 * sumY1;
		double H = N * sumX2Y1 + N * sumY3 - (sumX2 + sumY2) * sumY1;

		double denominator = C * G - D * D;
		//	if (std::abs(denominator) < DBL_EPSILON) return false;//判断分母的绝对值是否接近于(等于)0
		double a = (H * D - E * G) / (denominator);
		//denominator = D * D - G * C;
		//if (std::abs(denominator) < DBL_EPSILON) return false;
		double b = (H * C - E * D) / (-denominator);
		double c = -(a * sumX1 + b * sumY1 + sumX2 + sumY2) / N;

		center[k].x = a / (-2);
		center[k].y = b / (-2);
		radius[k] = std::sqrt(a * a + b * b - 4 * c) / 2;
		//	return true;
	}
}

测试程序用图:在这里插入图片描述
程序改进:将以上功能封装进一个函数中

#include<iostream>
#include<opencv2/opencv.hpp>

void LedCircleCenter(cv::Mat &srcImg, std::vector<std::vector<cv::Point>> &Circle_Data,
	std::vector<cv::Vec4i> &hierarchy, std::vector<cv::Point2d> &Circle_Center,
	std::vector<double> &Circle_R);

int main() {

	cv::Mat srcImg = cv::imread("Led_Point.jpg", 1);
	std::vector<std::vector<cv::Point>> CircleData;
	std::vector<cv::Vec4i> hierarchy;
	std::vector<cv::Point2d> CircleCenter;
	std::vector<double> CircleR;

	 LedCircleCenter(srcImg,CircleData,hierarchy,CircleCenter,CircleR);

	 for (int i = 0; i < CircleCenter.size(); i++) {
	 
		 std::cout<<"CircleCenter["<<i<<"]="<<CircleCenter[i]<<
			 "	"<<"CircleR["<<i<<"]="<<CircleR[i]<<std::endl;

	 }

	 system("Pause");
	 return 0;

}

//void LedCircleCenter(cv::Mat &srcImg, std::vector<std::vector<cv::Point>> &Circle_Data,
//	std::vector<cv::Vec4i> &hierarchy, std::vector<cv::Point2d> &Circle_Center,
//	std::vector<double> &Circle_R)
//function:函数用于将输入图片中的led灯的边缘提取后进行最小二乘拟合圆心和半径
//step:1.高斯滤波 2.canny边缘提取 3.存储每个边缘的坐标点 4.采用最小二乘法拟合圆心
//prgm 1: cv::Mat 类型的输入图像srcImg(格式不限)
/*prgm 2: std::vector<std::vector<cv::Point>> 类型的Circle_Data,存储了n组圆的边缘坐标,第一维代表圆的组号i,
第二维代表第i组中的j个坐标点,第三维代表第i个圆中的第j个点的x坐标和y坐标*/
//prgm 3:std::vector<cv::Vec4i> 检测用hierarchy(必须声明)
//prgm 4:std::vector<cv::Point2d> 类型的Circle_Center,用于存放i组圆圆心坐标的向量
//prgm 5:std::vector<double> 类型的Circle_R,用于存放i组圆对应的半径
void LedCircleCenter(cv::Mat &srcImg, std::vector<std::vector<cv::Point>> &Circle_Data,
	std::vector<cv::Vec4i> &hierarchy, std::vector<cv::Point2d> &Circle_Center,
	std::vector<double> &Circle_R) {

	cv::Mat srcImg_1 = srcImg.clone();	//复制到srcImg_1中
	cv::Mat gray, dstImg;		//原图的灰度图,canny检测后输出

	cv::cvtColor(srcImg_1, gray, cv::COLOR_BGR2GRAY);//gray为滤波之前的灰度图

	cv::GaussianBlur(gray, gray, cv::Size(5, 5), 0, 0, cv::BORDER_DEFAULT);//高斯滤波

	cv::Canny(gray, gray, 40, 120, 3);//边缘检测后的图存放于gray中,单通道,数据来源于
	dstImg.create(srcImg.size(), srcImg.type());//创建与原图相同尺寸和类型的dstImg
	dstImg = cv::Scalar::all(0);
	srcImg_1.copyTo(dstImg, gray);

	cv::cvtColor(dstImg, dstImg, cv::COLOR_BGR2GRAY);//转化为单通道灰度图
	cv::threshold(dstImg, dstImg, 100, 255, cv::THRESH_BINARY);//转化为二值图(单通道)

	//函数findContours,将检测到的边缘存放于Circle_Data中 参数:dstImg必须为单通道图像
	cv::findContours(dstImg, Circle_Data, hierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_NONE, cv::Point());


	for (int k = 0; k < Circle_Data.size(); k++) {
		//	std::cout << "k=" << pts.size() << std::endl;
		Circle_Center.push_back(cv::Point2d(0, 0));			//初始化圆心为(0,0)
		Circle_R.push_back(0.0);						//半径为0
	//	if (pts.size() < 3) return false;	//判断输入点的个数若小于三个则不能拟合

		//定义计算中间变量
		double sumX1 = 0.0; //代表Xi的和(从1~n) ,X1代表X的1次方
		double sumY1 = 0.0;
		double sumX2 = 0.0;	//代表(Xi)^2的和(i从1~n),X2代表X的二次方
		double sumY2 = 0.0;
		double sumX3 = 0.0;
		double sumY3 = 0.0;
		double sumX1Y1 = 0.0;
		double sumX1Y2 = 0.0;
		double sumX2Y1 = 0.0;
		const double N = (double)Circle_Data[k].size();//获得第k组输入点的个数
		for (int i = 0; i < Circle_Data[k].size(); ++i)//遍历第k组中所有数据
		{
			double x = 0;
			double y = 0;
			x = Circle_Data[k][i].x;			//获得第k组中第i个点的x坐标
			y = Circle_Data[k][i].y;			//获得第k组中第i个点的y坐标
			double x2 = x * x;				//计算x^2
			double y2 = y * y;				//计算y^2
			double x3 = x2 * x;				//计算x^3
			double y3 = y2 * y;				//计算y^3
			double xy = x * y;				//计算xy
			double x1y2 = x * y2;			//计算x*y^2
			double x2y1 = x2 * y;			//计算x^2*y

			sumX1 += x;						//sumX=sumX+x;计算x坐标的和
			sumY1 += y;						//sumY=sumY+y;计算y坐标的和
			sumX2 += x2;					//计算x^2的和
			sumY2 += y2;					//计算各个点的y^2的和
			sumX3 += x3;					//计算x^3的和
			sumY3 += y3;
			sumX1Y1 += xy;
			sumX1Y2 += x1y2;
			sumX2Y1 += x2y1;
		}
		double C = N * sumX2 - sumX1 * sumX1;
		double D = N * sumX1Y1 - sumX1 * sumY1;
		double E = N * sumX3 + N * sumX1Y2 - (sumX2 + sumY2) * sumX1;
		double G = N * sumY2 - sumY1 * sumY1;
		double H = N * sumX2Y1 + N * sumY3 - (sumX2 + sumY2) * sumY1;

		double denominator = C * G - D * D;
		//if (std::abs(denominator) < DBL_EPSILON) return false;//判断分母的绝对值是否接近于(等于)0
		double a = (H * D - E * G) / (denominator);
		//denominator = D * D - G * C;
		//if (std::abs(denominator) < DBL_EPSILON) return false;
		double b = (H * C - E * D) / (-denominator);
		double c = -(a * sumX1 + b * sumY1 + sumX2 + sumY2) / N;

		Circle_Center[k].x = a / (-2);
		Circle_Center[k].y = b / (-2);
		Circle_R[k] = std::sqrt(a * a + b * b - 4 * c) / 2;
	}
}
  • 13
    点赞
  • 99
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在上一个问题中,我们已经使用Hough Circle Transform函数检测到了圆,现在我们需要使用最小二乘法拟合圆心最小二乘法是一种常用的拟合方法,可以用来拟合数据点到一个函数或者曲线。在这里,我们可以用最小二乘法拟合圆心。 假设我们已经检测到了n个圆,在检测到的圆中,第i个圆的圆心坐标为(xi, yi),我们需要出这些圆心坐标的最优拟合圆(也就是最小二乘法拟合的圆)。 最小二乘法拟合圆心的思路如下: 1. 对于每个圆心(xi, yi),我们可以将它表示为(x - xi)^2 + (y - yi)^2 = r^2的形式。 2. 将上式展开,得到x^2 + y^2 - 2xi*x - 2yi*y + (xi^2 + yi^2 - r^2) = 0。 3. 对于每个圆心(xi, yi),我们可以将上式表示为Aix + Biy + Ci = Di^2的形式,其中Ai = -2xi, Bi = -2yi, Ci = xi^2 + yi^2 - r^2, Di = xi^2 + yi^2。 4. 将上述方程组表示成矩阵形式:AX = B,其中X = [a, b, c]T,A为n x 3的矩阵,B为n x 1的矩阵,X为3 x 1的矩阵,T表示矩阵的转置。 5. 使用最小二乘法解X,即X = (ATA)^-1ATB。 6. 出a,b,c之后,可以得到最小二乘法拟合圆心为(x0, y0) = (-a/2, -b/2),半径为r = sqrt(a^2 + b^2 - 4c)/2。 下面是一个示例代码,演示如何使用最小二乘法拟合图像中检测到的圆心: ```c++ #include <opencv2/opencv.hpp> #include <iostream> #include <vector> using namespace std; using namespace cv; int main() { Mat image = imread("circles.png", IMREAD_GRAYSCALE); if (image.empty()) { cout << "Could not open or find the image" << endl; return -1; } Mat blurred; GaussianBlur(image, blurred, Size(5, 5), 2); vector<Vec3f> circles; HoughCircles(blurred, circles, HOUGH_GRADIENT, 1, 10, 100, 30, 5, 50); Mat A(circles.size(), 3, CV_32F); Mat B(circles.size(), 1, CV_32F); for (size_t i = 0; i < circles.size(); i++) { float xi = circles[i][0]; float yi = circles[i][1]; float ri = circles[i][2]; A.at<float>(i, 0) = -2 * xi; A.at<float>(i, 1) = -2 * yi; A.at<float>(i, 2) = xi * xi + yi * yi - ri * ri; B.at<float>(i, 0) = xi * xi + yi * yi; } Mat ATA, ATB, X; transpose(A, ATA); ATB = ATA * B; X = (ATA * A).inv() * ATB; float a = X.at<float>(0, 0); float b = X.at<float>(1, 0); float c = X.at<float>(2, 0); Point2f center(-a / 2, -b / 2); float radius = sqrt(a * a + b * b - 4 * c) / 2; // 用红色圆画出最小二乘法拟合圆心 circle(image, center, 3, Scalar(0, 0, 255), -1, 8, 0); // 用蓝色圆画出最小二乘法拟合的圆 circle(image, center, radius, Scalar(255, 0, 0), 3, 8, 0); // 输出最小二乘法拟合圆心和半径 cout << "Fitted Circle:" << endl; cout << "Center: (" << center.x << ", " << center.y << ")" << endl; cout << "Radius: " << radius << endl; namedWindow("Fitted Circle", WINDOW_NORMAL); imshow("Fitted Circle", image); waitKey(0); return 0; } ``` 在此示例中,我们使用前面的代码检测到了圆,并将每个圆心(xi, yi)表示为Aix + Biy + Ci = Di^2的形式。然后,我们将这些方程表示为矩阵形式,使用最小二乘法解出拟合圆的参数a, b, c。最后,我们计算出最小二乘法拟合圆心和半径,并在图像上画出。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值