• 移动最小二乘法在点云平滑和重采样中的应用


    导言

    这篇博客的目的是介绍移动最小二乘法(moving least squares)在点云平滑和重采样的应用。开头先简要介绍最小二乘法,用两种不同的方法来求解最小二乘法,并给出一个具体的算例、代码、数据。目前关于最小二乘法的博客和网上的讨论有很多,我没必要重复做这个工作(其实是我太菜不能形象的讲出来哈),我想做的是提供一个简要的概括,以及给出一个具体的例子帮助读者理解。接下来,介绍加权最小二乘法,在同样的数据上面,跑加权最小二乘法,看看拟合的结果又如何。之后,简略分析一下PCL是如何实现的点云平滑和重采样。参考的文献列在最后,比较有价值的是[1][2],基本上是看这两个理解的移动最小二乘法。由于我学力尚浅,自知这篇文章有很多的不足、错误,希望读者不吝指出。欢迎理性的讨论和交流。

    最小二乘法

    对于方程组\(A\mathbf{x}=b\),我们要去寻找一个\(Ax'\)最佳逼近b,即有

    \[|b - Ax'| \le |b - Ax| \]

    解释一下为什么是这样。对于方程组\(A\mathbf{x}=b\),我们并不一定可以在\(A\)的各列的线性组合找到\(b\),换句话说,找不到这样的 \(x\) 使等式成立,因此我们尝试在 \(A\) 的列空间中找到最逼近b的那个点。

    使用投影的方法来求解

    将向量\(b\)投影到\(A\)的列空间得到\(b'\)\((b - b')\)是正交于A的列空间,即有

    \[A^T*(b - Ax') = 0 \]

    如果\((A^T*A)\)可逆:

    \[x' = (A^T*A)^{-1}*A^T*b \]

    例1

    这组数据,来自于[2]。画出散点图。

    x = [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]
    y = [0, 4, 5, 14, 15, 14.5, 14, 12, 10, 5, 4]
    

    首先考虑使用\(y = mx + n\)来拟合(这里用m,n而不用a,b是怕读者混淆)。需要解决的问题是数据如何装填。A矩阵放什么,b放什么,x又是什么。
    欲求的是\(m,n\)\(x = [m \quad n]^T\)\(b\)放入的是每一个因变量。欲求\(Ax = b\),那么每一行A乘以x,会等于对应的每一行b:

    \[A = \begin{bmatrix}x_1&1\\x_2&1\\...&...\\x_n&1\\\end{bmatrix} \]

    \[b = \begin{bmatrix}y_1\\y_2\\...\\y_n\end{bmatrix} \]

    A = np.empty((0, 2))
    B = np.empty((0, 1))
    for (a, b) in zip(x, y):
        row1 = np.array([a, 1])
        row2 = np.array([b])
        A = np.vstack([A, row1])
        B = np.vstack([B, row2])
    ans = np.linalg.inv(np.mat(A.transpose()) * np.mat(A)) * np.mat(A.transpose()) * np.mat(B)
    print(ans)
    xx = np.arange(0, 1, 0.01)
    yy = ans[0, 0] * xx + ans[1, 0]
    plt.plot(xx, yy)
    plt.scatter(x, y, c='r')
    plt.show()
    

    拟合结果:

    例2

    可是这个拟合的结果太差劲了吧!我们观察数据,觉得这个可以用二次函数去拟合,那么可以使用新的拟合函数:\(y = ax^2 + bx + c\)

    \[A = \begin{bmatrix}x_1^2&x_1&1\\x_2^2&x_2&1\\...&...\\x_n^2&x_n&1\\\end{bmatrix} \]

    \[x = [a\quad b\quad c]^T \]

    再拟合一次:这次拟合的还行。\(a = -53.96270396, b = 57.05361305, c = -0.77622378\)

    A = np.empty((0, 3))
    B = np.empty((0, 1))
    for (a, b) in zip(x, y):
        row1 = np.array([a*a, a, 1])
        row2 = np.array([b])
        A = np.vstack([A, row1])
        B = np.vstack([B, row2])
    print(A)
    print(B)
    ans = np.linalg.inv(np.mat(A.transpose()) * np.mat(A)) * np.mat(A.transpose()) * np.mat(B)
    print(ans)
    xx = np.arange(0, 1, 0.01)
    yy = ans[0, 0] * xx**2 + ans[1, 0] * xx + ans[2, 0]
    plt.plot(xx, yy)
    plt.scatter(x, y, c='r')
    plt.show()
    

    使用求偏导的方法来求解

    另一种理解最小二乘法的方法是,找到使因变量的误差平方和最小的参数来求解。假设我们有二维数据点集\((x_i, y_i)\),对他拟合的函数为\(f(x)\)

    \[J_{LS} = \sum^{k}_{n=1}(f(x_i) - y_i)^2 \]

    于是问题化解为求\(min(J_{LS})\)
    为什么使用误差平方和呢?我觉得沟通这个方法和上一个方法的关键在于距离!误差平方和,其实是因变量的向量\((y_1, y_2, ... , y_n)\)和拟合函数对应函数值的向量\((f(x_1), f(x_2), ... , f(x_n))\)的距离平方。要使到这个距离最小,那就是投影!其实这两种方法本质上都是利用投影来使到偏差最小。不同之处在于一种使用了内积为0的方式来求,一种使用求偏导算最小值的方式来求。

    [1][2]都是用了一个系数向量\(\alpha\),和基函数\(p(x)\)来表示\(f(x) = p(x)^T * \alpha\)

    如果有一个自变量x,最高阶为3,那么\(p(x) = [1,x,x^2,x^3]^T\)
    如果有两个自变量x、y,最高阶为2,那么\(p(x) = [1, x, y, x^2, xy, y^2]^T\)

    系数向量是我们想要求的向量, \(\alpha = [c_1, c_2, ..., c_n]\)

    \(J_{LS}\)分别求\(c_1, c_2, ..., c_n\)的偏导,具体过程参考[1]

    \[\frac{\partial J_{LS}}{\partial \alpha} = \sum^n_{i=1} 2*p(x_i)*(p(x_i)^T * \alpha - y_i) \]

    令偏导为0

    \[\alpha = [\sum^n_{i=1}(p(x_i) * p(x_i)^T)]^{-1} * \sum^n_{i=1}p(x_i)*y_i \]

    注意到\(p(x_i)\)是一个\(n\times 1\)的向量,他的转置则是\(1\times n\),于是它和它的转置相乘是一个\(n\times n\)的矩阵。

    例子3

    使用例子2的函数去拟合:\(y = a + bx + cx^2\)
    基函数\(p(x) = [1, x, x^2]^T\), 系数向量\(\alpha = [a, b, c]\)

    \[p(x) * p(x)^T = \begin{bmatrix}1&x&x^2\\x&x^2&x^3\\x^2&x^3&x^4\\\end{bmatrix} \]

    拟合出来的结果为:\(a=-0.77622378,b=57.05361305,c=-53.96270396\)。殊途同归啊!

    sumx = sumxx = sumxxx = sumxxxx = sumf = sumxf = sumxxf = 0
    for (a, b) in zip(x, y):
        sumx += a
        sumxx += a ** 2
        sumxxx += a ** 3
        sumxxxx += a ** 4
        sumf += b
        sumxf += a * b
        sumxxf += a * a * b
    A = np.array([[len(x), sumx, sumxx],
                  [sumx, sumxx, sumxxx],
                  [sumxx, sumxxx, sumxxxx]])
    B = np.array([sumf, sumxf, sumxxf])
    ans = np.linalg.solve(A, B)
    print(ans)
    xx = np.arange(0, 1, 0.01)
    yy = ans[0] + ans[1] * xx + ans[2] * xx**2
    plt.plot(xx, yy)
    plt.scatter(x, y, c='r')
    plt.show()
    

    加权最小二乘法

    [2]论文中:

    引入紧支( Compact Support)概念,认为点 x 处的值 y 只受 x 附近子域内节点影响

    因变量只受到自变量某一邻域影响,引入一个权重函数,给重要的地方加权,不重要的地方削弱它对因变量的影响。

    \[J_{WLS} = \sum^n_{i=1}\theta(s) (f(x_i) - y_i)^2 \]

    其中\(s\)为想要求的自变量\(x\)到附近样本自变量的欧几里得距离。

    如果我们想根据一组数据去估计某一个自变量\(x\)的函数值,我们就得计算一次!这种方法计算量相对而言大很多。

    同样这是一个最小化问题\(min(J_{WLS})\),对它求偏导,令偏导为0。

    \[\alpha = [\sum^n_{i=1}\theta(s)(p(x_i) * p(x_i)^T)]^{-1} * \sum^n_{i=1}\theta(s)p(x_i)*y_i \]

    例子4

    仍然是那一组数据。使用[2]建议的加权函数来求解。只不过这次使用的是一个一次函数来拟合局部。\(p(x) = [1,x]^T\)
    所谓的移动最小二乘法,找到一个函数,这个函数有一系列的局部的函数组成\(f(x) \in f_X(x)\),本质上就是WLS。

    def w(dis):
        dis = dis / 0.3
        if dis < 0:
            return 0
        elif dis <= 0.5:
            return 2/3 - 4 * dis**2 + 4 * dis**3
        elif dis <= 1:
            return 4/3 - 4 * dis + 4 * dis**2 - 4/3 * dis**3
        else:
            return 0
    
    
    def mls(x_):
        sumxx = sumx = sumxf = sumf = sumw = 0
        for (a, b) in zip(x, y):
            weight = w(abs(x_ - a))
            sumw += weight
            sumx += a * weight
            sumxx += a * a * weight
            sumf += b * weight
            sumxf += a * b * weight
        A = np.array([[sumw, sumx],
                      [sumx, sumxx]])
        B = np.array([sumf, sumxf])
        ans = np.linalg.solve(A, B)
        return ans[0] + ans[1] * x_
    
    
    x = [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]
    y = [0, 4, 5, 14, 15, 14.5, 14, 12, 10, 5, 4]
    xx = np.arange(0, 1, 0.01)
    yy = [mls(xi) for xi in xx]
    plt.plot(xx, yy)
    plt.scatter(x, y, c='r')
    plt.show()
    

    使用mls平滑

    简单来说就是对每个样本计算一次加权最小二乘法,然后对该样本的自变量\(x_i\)求函数值\(f_X(x_i)\),算出来的\((x, f_X(x_i))\)就是平滑的结果。

    PCL中upsampling的实现

    RANDOM_UNIFORM_DENSITY

    在点的某个范围内,如果有足够的点,就不进行重采样。如果不够,那么就随机添加点到这个范围内,投影到计算出来的曲面上,直到达到足够的点数。

    SAMPLE_LOCAL_PLANE

    这个方法更加直接,按照一定的步长,一个一个点,整齐的添加点。

    Voronoi图上upsampling

    [3]中,使用一种在Voronoi图上重采样的方法。

    思路:从输入的点云里面选点,在局部做一个平面的拟合,将这些点投影到平面上。计算这些点的Voronoi图,每次选择Voronoi节点中到它临近点(多于三个点)最大的那个节点,作为一个新的点,将它投影到拟合出来的mls曲面上。重复这个过程,直到Voronoi到它的临近点的半径小于某个阈值为止。

    结语

    完。第一篇博客,欢迎理性的讨论。不足、错误之处,希望读者不吝指出。

    参考

    [1] http://www.nealen.com/projects/mls/asapmls.pdf
    [2] https://wenku.baidu.com/view/fe7a74976f1aff00bed51eb1.html
    [3] http://www.sci.utah.edu/~shachar/Publications/crpss.pdf

  • 相关阅读:
    dependencies 和 devDependencies 区别
    safari下地址跳转bug
    forEach,map和filter的区别
    设置video标签的默认样式
    还不懂移动端适配快看这篇文章
    使用微信帐号登录网站的实现
    基于阿里云市场API服务实现增值税发票识别【OCR】
    词云wordcloud的实现
    数据统计的简单实现思路
    京东联盟API简易PHP接口类
  • 原文地址:https://www.cnblogs.com/zzk0/p/10468502.html
Copyright © 2020-2023  润新知