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

导言

这篇博客的目的是介绍移动最小二乘法(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

posted @ 2019-03-04 21:20  楷哥  阅读(6653)  评论(0编辑  收藏  举报