描述

简单介绍B样条曲线,并给出实现代码

原理

有关B样条的介绍有很多,这里不会太详细的介绍B样条曲线的全部概念,只会列出一些我的理解

贝塞尔曲线

不得不提到贝塞尔曲线,之前我曾经写过一篇关于贝塞尔曲线的文章,将非常有助于理解B样条曲线。贝塞尔曲线是这样生成的,简述:

  1. 有n个控制点,那么第0个点和第1个点连接起来,第1个点和第2个点连接起来…第n-2个点和第n-1个点连接起来,这样我们有了n-1条线段
  2. 每条线段上,以一个间隔值去滑动取点,分别取直线长度的0.01倍、0.02倍…一直到1倍(也就是从线段的起点取到终点),n-1条线段又得到了n-1个新控制点
  3. 循环1、2步骤,直到剩下一个线段,这条线段上不断取的点构成的曲线就是贝塞尔曲线。

有兴趣的话可以去读我的原文,链接在这里https://blog.csdn.net/weixin_42156097/article/details/107919429

B样条曲线

贝塞尔曲线,会有一个公式,曲线上的点 = 求和(权重系数*每个控制点的坐标)

站在很高的角度来说,B样条曲线上的点是与贝塞尔曲线一直的,也是曲线上的点 = 求和(权重系数*每个控制点的坐标)

每个控制点的坐标,乘以它们各自的权重系数,然后综合所有控制点,就可以得到曲线上的插值点了

只不过,B样条曲线每个点的权重计算方式与贝塞尔曲线不同,贝塞尔曲线是一个固定的计算公式, 而B样条曲线每个控制点的权重系数是一个n次多项式,是由各自的0次项迭代而来的。这里面会涉及到迭代公式。这里我不写了,因为哪里都能看到。
这里列举两篇写的比较好的文章吧,可以去参考
https://zhuanlan.zhihu.com/p/144042470
https://zhuanlan.zhihu.com/p/50626506

优势

B样条曲线的生成,可以尽可能的与每个点解耦。

我这句话很好理解。贝塞尔曲线的生成过程可以看出,n个控制点只要有一个发生了变化,整个曲线的形状都会影响。

而B样条曲线的优势就在于此,某个控制点发生变化,即使是十分大的变化,整条曲线发生改变的也是控制点周围的一小块

而这一特性,我认为十分适合针对一些数据的平滑操作,B样条曲线可以使数据在渐进式变化时,已得到数据的曲线,不会受新数据点的影响。

代码

以下的代码还是很好理解的。

#include <iostream>
#include <cstdlib>
#include <ctime>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc.hpp>

#define random(a,b) (rand()%(b-a)+a)

int g_d = 3;
int g_n = 5;
int g_u[8] = {0, 1, 2, 3, 4, 5, 6, 7};
// int g_u[8] = {0, 0, 0, 1, 2, 3, 3, 3};

// 当现在曲线在u位置时,第i个控制点的权重系数多项式中,d次项的表示
float get_B_u_d(int n, int d, float u) {
    if (d == 1) {
        if ( u >= g_u[n] && u <= g_u[n+1]) {
            return 1.0;
        } else {
            return 0.0;
        }
    } else {
        float value1, value2;
        if (g_u[n + d-1] - g_u[n] == 0) {
            value1 = u - g_u[n];
        } else {
            value1 = (u - g_u[n]) / (g_u[n + d-1] - g_u[n]);
        }
        if (g_u[n + d] - g_u[n + 1] == 0) {
            value2 = g_u[n + d] - u;
        } else {
            value2 = (g_u[n + d] - u) / (g_u[n + d] - g_u[n + 1]);
        } 

        return value1 * get_B_u_d(n, d - 1, u) + value2 * get_B_u_d(n + 1, d-1, u);
    }
}

int main (int argc, const char** argv) {
    std::vector<cv::Point2d> input; // 输入的点
    int matrix_size = 800; // 空白图大小
    cv::Mat image = cv::Mat(matrix_size, matrix_size, CV_8UC3, cv::Scalar(255,255,255));

    srand((int)time(0));  // 提供随机种子,要加这句话不然还会生成不变的数
    for (int i = 0 ; i < g_n; i++) { // 随机生成输入点    
        int x= random(0, matrix_size);
        int y = random(0, matrix_size);
        cv::Point2d p(x, y);
        input.push_back(p);
    }

    // input = {cv::Point2d(100,50), 
    //      cv::Point2d(100,300), cv::Point2d(350,100), cv::Point2d(380, 200), cv::Point2d(400, 400)};
    // cv::circle(image, cv::Point(matrix_size/2+input[i].x, matrix_size/2-input[i].y), 3, cv::Scalar(0,255,0), -1);

    for (int i = 0; i < input.size(); i++) {
        cv::circle(image, cv::Point(input[i].x, input[i].y), 3, cv::Scalar(0,255,0), -1);
    }

    std::vector<cv::Point2d> output;
    for (float u = g_u[0]; u < g_u[g_n + g_d - 1]; u = u + 0.01) {
        std::cout<<u<<std::endl;
        float output_point_x = 0;
        float output_point_y = 0;
        for (int j = 0; j < g_n; j++) {
            float k = get_B_u_d(j, g_d, u);
            output_point_x = output_point_x + k * input[j].x;
            output_point_y = output_point_y + k * input[j].y;    
        }
        output.push_back(cv::Point2d(output_point_x, output_point_y));

        cv::circle(image, cv::Point(output[output.size()-1].x, 
            output[output.size()-1].y), 2, cv::Scalar(0,0,255), -1);

        cv::imshow("Bezier curve", image);
        cv::waitKey(50);
    }

    return 0;
}

代码中需要说明的有以下几点

  1. 贝塞尔曲线的插值过程是系数从0到1的,而B样条曲线的插值过程是从最小节点值取到最大节点值的。
  2. 代码中有两种节点值的设置,一种是顺序列表{0, 1, 2, 3, 4, 5, 6, 7},另外一种是Clamped列表{0, 0, 0, 1, 2, 3, 3, 3}
  3. 代码中也有两种控制点的输入方式,一种是随机生成,注释掉的是一组固定点。同时B样条曲线中的点是循环来一点点画出来的,这些都仅仅是为了帮助理解

如下是不同节点值设置的截图,忽略图片名字,哈哈还写的是贝塞尔我忘改了
顺序节点表如下

Clamped节点表如下

对于未来使用,我把它写成了接口,以下代码就可以成为插件式使用了。这是我一直追求的目标。

int g_n;
int g_d;
std::vector<int> g_u;
// 当现在曲线在u位置时,第i个控制点的权重系数多项式中,d次项的表示
float get_B_u_d(int n, int d, float u) {
    if (d == 1) {
        if ( u >= g_u[n] && u <= g_u[n+1]) {
            return 1.0;
        } else {
            return 0.0;
        }
    } else {
        float value1, value2;
        if (g_u[n + d-1] - g_u[n] == 0) {
            value1 = u - g_u[n];
        } else {
            value1 = (u - g_u[n]) / (g_u[n + d-1] - g_u[n]);
        }

        if (g_u[n + d] - g_u[n + 1] == 0) {
            value2 = g_u[n + d] - u;
        } else {
            value2 = (g_u[n + d] - u) / (g_u[n + d] - g_u[n + 1]);
        } 

        return value1 * get_B_u_d(n, d - 1, u) + value2 * get_B_u_d(n + 1, d-1, u);
    }
}

std::vector<cv::Point2d> getBSpline(std::vector<cv::Point2d> input) {

    g_d = 3;
    g_n = input.size();

    g_u.clear();
    for (int i = 0; i < g_d + g_n; i++) {
        g_u.push_back(i);
    }

    std::vector<cv::Point2d> output;
    output.clear();
    int o = 0;
    for (float u = g_u[5] + 0.01; u < g_u[g_n + g_d - 4] ; u = u + 0.01)
    {
        float output_point_x = 0;
        float output_point_y = 0;
        for (int j = 0; j < g_n; j++)
        {
            float k = get_B_u_d(j, g_d, u);

            output_point_x = output_point_x + k * input[j].x;
            output_point_y = output_point_y + k * input[j].y;
        }

        output.push_back(cv::Point2d(output_point_x, output_point_y));
    }
    return output;
}

总结

给定控制点,求出B样条曲线的简单理解,及代码

写的不容易,欢迎各位朋友点赞并加关注,谢谢!