"); //-->
在自动驾驶系统中,通常会用起点、终点和一个三阶多项式来表示一条车道线,多项式系数的求解一般用最小二乘法来实现。
本文首先介绍两种基于最小二乘法的多项式拟合方法的原理,然后基于OpenCV用c++编写了这两种拟合方法的代码,最后通过一个完整的示例来展示如何通过一个离散点集拟合出一条多项式曲线。
基于最小二乘法的多项式拟合原理推导代数方式求解多项式曲线拟合是指基于一系列的观测点去寻找一个多项式来表示这些点的关系,最小二乘法通过最小化误差的平方和去寻找数据的最佳匹配函数。假设有点集
其中,和的关系满足函数
假设次多项式函数为
其中为多项式系数。如果要用这个次多项式来表示与的关系,那么多项式值与真实值之间的误差为
采用最小二乘法进行多项式拟合的目的就是寻找一组最佳的多项式系数使得拟合后整个点集的总误差最小,而求总误差最小的问题可以转化为求误差平方和最小。整个点集的误差平方和为
要使最小,可以对()求偏导并令其为零:
可得
写成矩阵形式可得
把上式写为,那么只要计算出
和
,再代入上面的式子中构建出矩阵和,那么多项式系数就可以通过求解线性方程组得到。
矩阵方式求解从上一节的推导过程可以看到,用代数法求解多项式系数的方法非常繁琐而且计算量比较大,我们其实还可以用矩阵求解的方法来简化计算过程。
令
那么整个点集的误差平方和可以表示为
对求导可得
令导数为零,那么可以得到
解得
学习了前面的理论知识,我们用c++来编码实现一下,毕竟光看一堆数学公式还是不够直观。从前面的理论知识可以知道,用最小二乘法做多项式曲线拟合其实质就是一个矩阵求解的问题,我们可以借助一些常用的数学库比如OpenCV或Eigen来求解。本文将采用OpenCV来实现。
1. 代数方式求解的C++代码实现如果理解了前面的理论,写代码其实就比较简单了。对照前面理论部分的公式,构建好矩阵A和B,然后调用OpenCV的solve()函数来求解即可:
// 代数方式求解2. 矩阵方式求解的C++代码实现
void PolyFit(const std::vector<cv::Point2f> &points, const int order,
cv::Mat *coeff) {
const int n = points.size();
cv::Mat A = cv::Mat::zeros(order + 1, order + 1, CV_64FC1);
cv::Mat B = cv::Mat::zeros(order + 1, 1, CV_64FC1);
// 构建A矩阵
for (int i = 0; i < order + 1; ++i) {
for (int j = 0; j < order + 1; ++j) {
for (int k = 0; k < n; ++k) {
A.at<double>(i, j) += std::pow(points.at(k).x, i + j);
}
}
}
// 构建B矩阵
for (int i = 0; i < order + 1; ++i) {
for (int k = 0; k < n; ++k) {
B.at<double>(i, 0) += std::pow(points.at(k).x, i) * points.at(k).y;
}
}
(*coeff) = cv::Mat::zeros(order + 1, 1, CV_64FC1);
// 求解
if (!cv::solve(A, B, *coeff, cv::DECOMP_LU)) {
std::cout << "Failed to solve !" << std::endl;
}
}
矩阵方式求解的代码更加简单:
// 矩阵方式求解3. 一个完整的多项式曲线拟合示例
void PolyFit(const std::vector<cv::Point2f> &points, const int order,
cv::Mat *coeff) {
const int n = points.size();
cv::Mat A = cv::Mat::ones(n, order + 1, CV_64FC1);
cv::Mat B = cv::Mat::zeros(n, 1, CV_64FC1);
for (int i = 0; i < n; ++i) {
const double a = points.at(i).x;
const double b = points.at(i).y;
B.at<double>(i, 0) = b;
if (i > 0) {
for (int j = 1, v = a; j < order + 1; ++j, v *= a) {
A.at<double>(i, j) = v;
}
}
}
(*coeff) = (A.t() * A).inv() * A.t() * B;
}
接下来,我们来看一个简单的曲线拟合示例:
int main(int argc, char **argv) {
constexpr int kOrder = 3; // 多项式阶数
constexpr int kWidth = 1000;
constexpr int kHeight = 500;
cv::Mat canvas =
cv::Mat(cv::Size(kWidth, kHeight), CV_8UC3, cv::Scalar(255, 255, 255));
cv::RNG rng(0xFFFFFFFF); // 随机数
// 生成点集,y坐标添加一些随机噪声
std::vector<cv::Point2f> raw_points;
for (int i = 10; i < kWidth; i += 10) {
cv::Point2f p;
p.x = i;
const auto noise = rng.uniform(-kHeight / 10, kHeight / 10);
p.y = kHeight - p.x / kWidth * kHeight + noise;
cv::circle(canvas, p, 5, cv::Scalar(0, 0, 255), -1);
raw_points.emplace_back(p);
}
// 多项式拟合
cv::Mat coeff;
PolyFit(raw_points, kOrder, &coeff);
// 用拟合后的系数重新生成点集
std::vector<cv::Point> poly_points;
for (const auto &rp : raw_points) {
cv::Point p;
p.x = rp.x;
p.y = PolyValue(coeff, kOrder, rp.x);
cv::circle(canvas, p, 5, cv::Scalar(0, 255, 0), -1);
poly_points.emplace_back(p);
}
cv::polylines(canvas, poly_points, false, cv::Scalar(0, 255, 0), 3,
cv::LINE_4);
cv::imshow("PolyFit", canvas);
cv::waitKey(0);
cv::destroyAllWindows();
return 0;
}
代码的流程如下:
其中用于计算多项式值的函数PolyValue()实现如下:
float PolyValue(const cv::Mat &coeff, const int order, const float x) {
float v = 0;
for (int i = 0; i < order; ++i) {
v += coeff.at<double>(i, 0) * std::pow(x, i);
}
return v;
}
可视化的结果如下图所示,其中红色点为原始点,绿色点为拟合后的点。
本文详细介绍了基于最小二乘法的多项式拟合的原理和代码实现,希望对各位读者有所帮助。如果对本文内容有疑问,欢迎评论区留言与我交流。
*博客内容为网友个人发布,仅代表博主个人观点,如有侵权请联系工作人员删除。