NDT(Normal Distributions Transform)算法原理与公式推导
正态分布变换(NDT)算法是一个配准算法,它应用于三维点的统计模型,使用标准最优化技术来确定两个点云间的最优的匹配,因为其在配准过程中不利用对应点的特征计算和匹配,所以时间比其他方法快。下面的公式推导和MATLAB程序编写都参考论文:The Normal Distributions Transform: A New Approach to Laser Scan Matching
先回顾一下算法推导和实现过程中涉及到的几个知识点:
-
协方差矩阵
在概率论和统计中,协方差是对两个随机变量联合分布线性相关程度的一种度量。对多维随机变量 ,我们往往需要计算各维度两两之间的协方差,这样各协方差组成了一个的矩阵,称为协方差矩阵。协方差矩阵是个对称矩阵,而且是半正定的(证明可以参考这里)。对角线上的元素是各维度上随机变量的方差。我们定义协方差矩阵为,矩阵内的元素为
这样这个矩阵为
样本的协方差是样本集的一个统计量,可作为联合分布总体参数的一个估计,在实际中计算的通常是样本的协方差。样本的协方差矩阵与上面的协方差矩阵相同,只是矩阵内各元素以样本的协方差替换。样本集合为,为样本数量,所有样本可以表示成一个的矩阵。我们以表示样本的协方差矩阵,与区分。
公式中为样本数量,为样本均值,是一个列向量。为第个样本,也是一个列向量。
-
多元正态分布
若随机向量的概率密度函数为
则称服从元正态分布,记作,其中是一个维列向量,参数和分别为的均值和协方差矩阵,是协方差矩阵的行列式(Determinant)。
二维正态分布概率密度函数为钟形曲面,等高线是椭圆线族,并且二维正态分布的两个边缘分布都是一维正态分布的形式:
下面是服从,二维正态分布的概率密度及两个边缘分布的概率密度图:
用Mathematica可以画出二维正态分布概率密度函数的等高线:
ContourPlot[PDF[MultinormalDistribution[{0, 0}, {{1, 3/5}, {3/5, 2}}], {x, y}], {x, -3, 3}, {y, -3, 3}]
可以看出等高线是一族椭圆线:
也可以使用Python的FilterPy(Kalman filters and other optimal and non-optimal estimation filters in Python)库来计算多元正态分布概率密度,首先用pip安装filterpy
pip install filterpy
下面的代码计算x=[1.2, 2.5]处的概率密度

from filterpy.stats import multivariate_gaussian x = [1.2, 2.5] mu = [0, 0] cov = [[1,0.6],[0.6,2]] pdf = multivariate_gaussian(x, mu, cov) # output is 0.02302
协方差矩阵描述了随机点的概率密度的分布情况,颜色越深的地方表示概率密度值越大

import matplotlib.pyplot as plt import numpy as np import scipy.stats def plot_cov_ellipse_colormap(cov=[[1,1],[1,1]]): side = np.linspace(-3, 3, 200) X,Y = np.meshgrid(side,side) pos = np.empty(X.shape + (2,)) pos[:, :, 0] = X; pos[:, :, 1] = Y plt.axes(xticks=[], yticks=[], frameon=True) rv = scipy.stats.multivariate_normal((0,0), cov) plt.gca().grid(b=False) plt.gca().imshow(rv.pdf(pos), cmap=plt.cm.Greys, origin='lower') plt.show() plot_cov_ellipse_colormap(cov=[[1, 0.6], [0.6, 2]])
-
向量求导
为列向量,是一个矩阵,则有:
-
多元函数的泰勒展开式
多元目标函数可能是很复杂的函数,为了便于研究函数极值问题,必须用简单函数作局部逼近,通常采用泰勒展开式作为函数在某点附近的表达式。
由高等数学知识可知,对于一元函数在点,即的泰勒展开式为:
式中的余项为
在、之间。在点充分小的邻域内,余项的极限为零,因此可以用多项式来逼近函数。
二元函数的泰勒展开式可由一元函数泰勒展开推广得到。
函数在点附近的泰勒展开,若只取到二次项可写为
上式可写成矩阵形式,即
式中,,
是函数在点的二阶偏导数矩阵,称为海塞(Hessian)矩阵,可以用表示,Hessian矩阵是对称矩阵。引用上述符号后,二元函数泰勒展开式可简写为
-
无约束优化中的牛顿法
牛顿法是梯度法的进一步发展,梯度法在确定搜索方向时只考虑目标函数在选择迭代点的局部性质,即利用一阶偏导数的信息,而牛顿法进一步利用了目标函数的二阶偏导数,考虑了梯度的变化趋势,因而可更全面地确定合适的搜索方向,以便更快地搜索到极小点。
以一维问题来说明牛顿法的过程。设已知一维目标函数的初始点,过点做一与原目标函数相切的二次曲线,求抛物线的极小点的坐标,将代入原目标函数,求得得到点。过点再做一条与相切的二次曲线,得到下一个最小点,解得点。重复做下去直到找到原目标函数的极小点的坐标为止,如下图所示。
在逼近的过程中所用的二次函数是以原目标函数在迭代点附近的泰勒展开式的二次项。一维目标函数在点逼近所用的二次函数为:
该二次函数的极小点可由极值存在的必要条件求得,也即求得
把上述过程推广到维问题,即是
当时,可求得二次函数的极值点,对上式矩阵方程求导,可得到
若为可逆矩阵,从而得到这次逼近的二次函数的最小点
在每次逼近的极小点可由上式计算出。由于是的一个近似表达式,所求得的极小点并不是目标函数真正的极小点。只有当目标函数本身是二次函数时,所求的极小点才是目标函数的真正极小点,这种情况一次迭代就可以求出目标函数的极值点。
在一般情况下,不一定是二次函数,为了求得的极小值,可以把由上式求得的作为下一个迭代点,通过反复循环迭代,不断逼近到的极小点。于是得到牛顿法的迭代格式为:
或者写成
式中,称为牛顿搜索方向;.
牛顿法主要缺点是每次迭代过程都要计算函数二阶导数矩阵(Hessian矩阵),并且要对该矩阵求逆。这样工作量相当大,特别是矩阵求逆计算,当设计变量维数较高时工作量更大。因此,牛顿法很少被直接采用,但是对于那些容易计算一阶导数和海塞矩阵及其逆的二次函数采用牛顿法还是很方便的。
-
数值计算中的病态问题与条件数
求解过程涉及到计算协方差矩阵的逆,理论上没什么问题,但是实际中可能出现接近奇异的情况。对于一个数值问题本身如果输入数据有微小扰动(即误差),引起输出数据(即问题解)相对误差很大,这就是病态问题。在计算机上解线性方程组或计算矩阵的逆时要分析问题是否病态,例如解线性方程组,如果输入数据有微小误差引起解的巨大误差,就认为是病态方程组。
条件数定义:,表示矩阵的某种范数,若矩阵的条件数较大,则称为病态矩阵。
矩阵有3种常见的范数:
1-范数: (列和范数,A每一列元素绝对值之和的最大值)
∞-范数: (行和范数,A每一行元素绝对值之和的最大值)
2-范数: ,是矩阵的最大特征值,也称为谱范数
根据上面的定义,矩阵的谱条件数为
当为对称矩阵时
其中,为的绝对值最大和最小的特征值。
若条件数较小(接近1),就称关于求逆矩阵或解线性方程组为良态的;若条件数较大,就称关于求逆矩阵或解线性方程组为病态的。当矩阵十分病态时,就说明已十分接近一个奇异矩阵。要判别一个矩阵是否病态需要计算条件数,而计算是比较费劲的,那么在实际中如何发现病态情况呢?通常来说,如果系数矩阵的行列式值相对很小,或系数矩阵某些行近似线性相关,这时可能病态。或者矩阵元素间数量级相差很大,并且无一定规则,可能病态。
例如矩阵,可以看出矩阵的第一行和第二行非常接近线性相关(矩阵接近奇异)。用MATALB中的inv函数求逆,结果为[[-2000, 1000], [1000.5, -500]],如果将中的元素2.001改为2.002,此时求逆的结果为[[-1000, 500], [500.5, -250]],可以看出微小的扰动引起结果巨大的变化。
通过SVD分解来计算矩阵的奇异值
[u, s, v] = svd(A)
结果为:
s =
5.0004 0
0 0.0004
则矩阵的谱条件数为,可以看出矩阵非常病态。
基于数值计算的考虑,计算协方差矩阵的逆之前,可以先用奇异值分解来检查矩阵是否接近奇异。如果最大奇异值比最小的大1000倍以上(条件数大于1000),则将条件数限制在1000,用此时对应的协方差矩阵求逆。
% Prevent covariance matrix from going singular (and not be invertible)
[U, S, V] = svd(xyCov);
if S(2,2) < 0.001 * S(1,1)
S(2,2) = 0.001 * S(1,1);
xyCov = U * S * V';
end
NDT算法
NDT算法的基本思想是先根据参考数据(reference scan)来构建多维变量的正态分布,如果变换参数能使得两幅激光数据匹配的很好,那么变换点在参考系中的概率密度将会很大。因此,可以考虑用优化的方法求出使得概率密度之和最大的变换参数,此时两幅激光点云数据将匹配的最好。
算法的基本步骤如下:
1. 将参考点云(reference scan)所占的空间划分成指定大小(CellSize)的网格或体素(Voxel);并计算每个网格的多维正态分布参数:
-
- 均值
- 协方差矩阵
2. 初始化变换参数(赋予零值或者使用里程计数据赋值)
3. 对于要配准的点云(second scan),通过变换将其转换到参考点云的网格中
4. 根据正态分布参数计算每个转换点的概率密度
5. NDT配准得分(score)通过对每个网格计算出的概率密度相加得到
6. 根据牛顿优化算法对目标函数进行优化,即寻找变换参数使得的值最大。优化的关键步骤是要计算目标函数的梯度和Hessian矩阵:
目标函数由每个格子的值累加得到,令,则
根据链式求导法则以及向量、矩阵求导的公式,梯度方向为
其中对变换参数的偏导数即为变换的雅克比矩阵:
根据上面梯度的计算结果,继续求关于变量、的二阶偏导:
根据变换方程,向量对变换参数的二阶导数的向量为:
7. 跳转到第3步继续执行,直到达到收敛条件为止
NDT算法的MATLAB实现
preNDT函数将激光数据点划分到指定大小的网格中:

function [xgridcoords, ygridcoords] = preNDT(laserScan, cellSize) %preNDT Calculate cell coordinates based on laser scan % [XGRIDCOORDS, YGRIDCOORDS] = preNDT(LASERSCAN, CELLSIZE) calculated the x % (XGRIDCOORDS) and y (YGRIDCOORDS) coordinates of the cell center points that are % used to discretize the given laser scan. % % Inputs: % LASERSCAN - N-by-2 array of 2D Cartesian points % CELLSIZE - Defines the side length of each cell used to build NDT. % Each cell is square % % Outputs: % XGRIDCOORDS - 4-by-K, the discretized x coordinates using cells with size % equal to CELLSIZE. % YGRIDCOORDS: 4-by-K, the discretized y coordinates using cells with size % equal to CELLSIZE. xmin = min(laserScan(:,1)); ymin = min(laserScan(:,2)); xmax = max(laserScan(:,1)); ymax = max(laserScan(:,2)); halfCellSize = cellSize/2; lowerBoundX = floor(xmin/cellSize)*cellSize-cellSize; upperBoundX = ceil(xmax/cellSize)*cellSize+cellSize; lowerBoundY = floor(ymin/cellSize)*cellSize-cellSize; upperBoundY = ceil(ymax/cellSize)*cellSize+cellSize; % To minimize the effects of discretization,use four overlapping % grids. That is, one grid with side length cellSize of a single cell is placed % first, then a second one, shifted by cellSize/2 horizontally, a third % one, shifted by cellSize/2 vertically and a fourth one, shifted by % cellSize/2 horizontally and vertically. xgridcoords = [ lowerBoundX:cellSize:upperBoundX;... % Grid of cells in position 1 lowerBoundX+halfCellSize:cellSize:upperBoundX+halfCellSize;... % Grid of cells in position 2 (X Right, Y Same) lowerBoundX:cellSize:upperBoundX; ... % Grid of cells in position 3 (X Same, Y Up) lowerBoundX+halfCellSize:cellSize:upperBoundX+halfCellSize]; % Grid of cells in position 4 (X Right, Y Up) ygridcoords = [ lowerBoundY:cellSize:upperBoundY;... % Grid of cells in position 1 lowerBoundY:cellSize:upperBoundY;... % Grid of cells in position 2 (X Right, Y Same) lowerBoundY+halfCellSize:cellSize:upperBoundY+halfCellSize;... % Grid of cells in position 3 (X Same, Y Up) lowerBoundY+halfCellSize:cellSize:upperBoundY+halfCellSize]; % Grid of cells in position 4 (X Right, Y Up) end
为了减小离散化的影响,用4个尺寸都为cellSize的相互重叠的小格子组成一个大格子(下面示意图中蓝色边框的格子)来计算目标函数值等信息。将左下角的第1个小格子向右平移cellSize/2个单位得到第2个小格子;将第1个小格子向上平移cellSize/2个单位得到第3个小格子;将第1个小格子向右、向上平移cellSize/2个单位得到第4个小格子:
buildNDT函数根据划分好的网格,来计算每一个小格子中的二维正态分布参数(均值、协方差矩阵以及协方差矩阵的逆):

function [xgridcoords, ygridcoords, meanq, covar, covarInv] = buildNDT(laserScan, cellSize) %buildNDT Build Normal Distributions Transform from laser scan % [XGRIDCOORDS, YGRIDCOORDS, MEANQ, COVAR, COVARINV] = buildNDT(LASERSCAN, CELLSIZE) % discretizes the laser scan points into cells and approximates each cell % with a Normal distribution. % % Inputs: % LASERSCAN - N-by-2 array of 2D Cartesian points % CELLSIZE - Defines the side length of each cell used to build NDT. % Each cell is a square area used to discretize the space. % % Outputs: % XGRIDCOORDS - 4-by-K, the discretized x coordinates of the grid of cells, % with each cell having a side length equal to CELLSIZE. % Note that K increases when CELLSIZE decreases. % The second row shifts the first row by CELLSIZE/2 to % the right. The third row shifts the first row by CELLSIZE/2 to the % top. The fourth row shifts the first row by CELLSIZE/2 to the right and % top. The same row semantics apply to YGRIDCOORDS, MEANQ, COVAR, and COVARINV. % YGRIDCOORDS: 4-by-K, the discretized y coordinates of the grid of cells, % with each cell having a side length equal to CELLSIZE. % MEANQ: 4-by-K-by-K-by-2, the mean of the points in cells described by % XGRIDCOORDS and YGRIDCOORDS. % COVAR: 4-by-K-by-K-by-2-by-2, the covariance of the points in cells % COVARINV: 4-by-K-by-K-by-2-by-2, the inverse of the covariance of % the points in cells. % % [XGRIDCOORDS, YGRIDCOORDS, MEANQ, COVAR, COVARINV] describe the NDT statistics. % Copyright 2016 The MathWorks, Inc. % When the scan contains ONLY NaN values (no valid range readings), % the input laserScan is empty. Explicitly % initialize empty variables to support code generation. if isempty(laserScan) xgridcoords = zeros(4,0); ygridcoords = zeros(4,0); meanq = zeros(4,0,0,2); covar = zeros(4,0,0,2,2); covarInv = zeros(4,0,0,2,2); return; end % Discretize the laser scan into cells [xgridcoords, ygridcoords] = preNDT(laserScan, cellSize); xNumCells = size(xgridcoords,2); yNumCells = size(ygridcoords,2); % Preallocate outputs meanq = zeros(4,xNumCells,yNumCells,2); covar = zeros(4,xNumCells,yNumCells,2,2); covarInv = zeros(4,xNumCells,yNumCells,2,2); % For each cell, compute the normal distribution that can approximately % describe the distribution of the points within the cell. for cellShiftMode = 1:4 % Find the points in the cell % First find all points in the xgridcoords and ygridcoords separately and then combine the result. % indx的值表示laserScan的x坐标分别在xgridcoords划分的哪个范围中(例如1就表示落在第1个区间;若不在范围中,则返回0) [~, indx] = histc(laserScan(:,1), xgridcoords(cellShiftMode, :)); [~, indy] = histc(laserScan(:,2), ygridcoords(cellShiftMode, :)); for i = 1:xNumCells xflags = (indx == i); % xflags is a logical vector for j = 1:yNumCells yflags = (indy == j); xyflags = logical(xflags .* yflags); xymemberInCell = laserScan(xyflags,:); % laser points in the cell % If there are more than 3 points in the cell, compute the % statistics. Otherwise, all statistics remain zero. % See reference [1], section III. if size(xymemberInCell, 1) > 3 % Compute mean and covariance xymean = mean(xymemberInCell); xyCov = cov(xymemberInCell, 1); % Prevent covariance matrix from going singular (and not be % invertible). See reference [1], section III. [U,S,V] = svd(xyCov); if S(2,2) < 0.001 * S(1,1) S(2,2) = 0.001 * S(1,1); xyCov = U*S*V'; end [~, posDef] = chol(xyCov); if posDef ~= 0 % If the covariance matrix is not positive definite, % disregard the contributions of this cell. continue; end % Store statistics meanq(cellShiftMode,i,j,:) = xymean; covar(cellShiftMode,i,j,:,:) = xyCov; covarInv(cellShiftMode,i,j,:,:) = inv(xyCov); end end end end end
objectiveNDT函数根据变换参数计算目标函数值及其梯度和Hessian矩阵,objectiveNDT的输出参数将作为目标函数信息传入优化函数中:

function [score, gradient, hessian] = objectiveNDT(laserScan, laserTrans, xgridcoords, ygridcoords, meanq, covar, covarInv) %objectiveNDT Calculate objective function for NDT-based scan matching % [SCORE, GRADIENT, HESSIAN] = objectiveNDT(LASERSCAN, LASERTRANS, XGRIDCOORDS, % YGRIDCOORDS, MEANQ, COVAR, COVARINV) calculates the NDT objective function by % matching the LASERSCAN transformed by LASERTRANS to the NDT described % by XGRIDCOORDS, YGRIDCOORDS, MEANQ, COVAR, and COVARINV. % The NDT score is returned in SCORE, along with the optionally % calculated score GRADIENT, and score HESSIAN. % Copyright 2016 The MathWorks, Inc. % Create rotation matrix theta = laserTrans(3); sintheta = sin(theta); costheta = cos(theta); rotm = [costheta -sintheta; sintheta costheta]; % Create 2D homogeneous transform trvec = [laserTrans(1); laserTrans(2)]; tform = [rotm, trvec 0 0 1]; % Create homogeneous points for laser scan hom = [laserScan, ones(size(laserScan,1),1)]; % Apply homogeneous transform trPts = hom * tform'; % Eqn (2) % Convert back to Cartesian points laserTransformed = trPts(:,1:2); hessian = zeros(3,3); gradient = zeros(3,1); score = 0; % Compute the score, gradient and Hessian according to the NDT paper for i = 1:size(laserTransformed,1) % 对每一个转换点进行处理 xprime = laserTransformed(i,1); yprime = laserTransformed(i,2); x = laserScan(i,1); y = laserScan(i,2); % Eqn (11) jacobianT = [1 0 -x*sintheta - y*costheta; 0 1 x*costheta - y*sintheta]; % Eqn (13) qp3p3 = [-x*costheta + y*sintheta; -x*sintheta - y*costheta]; for cellShiftMode = 1:4 [~,m] = histc(xprime, xgridcoords(cellShiftMode, :)); % 转换点落在(m,n)格子中 [~,n] = histc(yprime, ygridcoords(cellShiftMode, :)); if m == 0 || n == 0 continue end meanmn = reshape(meanq(cellShiftMode,m,n,:),2,1); covarmn = reshape(covar(cellShiftMode,m,n,:,:),2,2); covarmninv = reshape(covarInv(cellShiftMode,m,n,:,:),2,2); if ~any([any(meanmn), any(covarmn)]) % Ignore cells that contained less than 3 points continue end % Eqn (3) q = [xprime;yprime] - meanmn; % As per the paper, this term should represent the probability of % the match of the point with the specific cell gaussianValue = exp(-q'*covarmninv*q/2); score = score - gaussianValue; for j = 1:3 % Eqn (10) gradient(j) = gradient(j) + q'*covarmninv*jacobianT(:,j)*gaussianValue; % Eqn (12) qpj = jacobianT(:,j); for k = j:3 % Hessian矩阵为对称矩阵,只需要计算对角线上的部分 qpk = jacobianT(:,k); if j == 3 && k == 3 hessian(j,k) = hessian(j,k) + gaussianValue*(-(q'*covarmninv*qpj)*(q'*covarmninv*qpk) +(q'*covarmninv*qp3p3) + (qpk'*covarmninv*qpj)); else hessian(j,k) = hessian(j,k) + gaussianValue*(-(q'*covarmninv*qpj)*(q'*covarmninv*qpk) + (qpk'*covarmninv*qpj)); end end end end end % 补全Hessian矩阵 for j = 1:3 for k = 1:j-1 hessian(j,k) = hessian(k,j); end end score = double(score); gradient = double(gradient); hessian = double(hessian); end
最后,可以利用MATLAB中的优化函数来计算最优变换参数。具体使用方法可以参考:fminunc(Find minimum of unconstrained multivariable function) 以及 Including Gradients and Hessians.
以上过程就是MATLAB Robotics System Toolbox工具箱中的函数matchScans的主要内容。matchScans函数用于匹配两帧激光雷达数据,输出两帧之间的姿态变换。
参考:
多元正态分布(Multivariate normal distribution)
The Normal Distributions Transform: A New Approach to Laser Scan Matching
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了