CMU团队开源算法点云地面分割 terrainAnalysis 代码解析

在CMU团队开源的全套导航系统中,点云的地面分割是在局部路径规划的前提。之前的文章已经对局部路径规划算法做了详细的介绍,包括Matlab用采样的离散点做前向模拟三次样条生成路径点CMU团队最新开发的局部路径导航算法 localPlanner 代码详解以及如何适用于现实移动机器人

CMU所提供的点云地面分割算法在其提供的仿真环境下,效果比较明显,可以有效的识别地面与障碍物。
首先还是对参数简单的介绍一下:

double scanVoxelSize = 0.05;						---				点云下采样
double decayTime = 2.0;								---		    	点云时间差阈值 大于则不会被处理		
double noDecayDis = 4.0;							---		    	点云距离阈值 小于该阈值不考虑时间差	
double clearingDis = 8.0;							---		    	该距离外的点会被清除
bool clearingCloud = false;							---		    	清除距离外的点云
bool useSorting = true;
double quantileZ = 0.25;							---		    	考虑地面附近高程最小值会改变
bool considerDrop = false;
bool limitGroundLift = false;
double maxGroundLift = 0.15;
bool clearDyObs = false;
double minDyObsDis = 0.3;
double minDyObsAngle = 0;
double minDyObsRelZ = -0.5;
double minDyObsVFOV = -16.0;
double maxDyObsVFOV = 16.0;
int minDyObsPointNum = 1;
bool noDataObstacle = false;
int noDataBlockSkipNum = 0;
int minBlockPointNum = 10;							---				计算有效高程的最小点云数量
double vehicleHeight = 1.5;							---				车辆的高度
int voxelPointUpdateThre = 100;						---				需要处理的体素网格点云数量最小值
double voxelTimeUpdateThre = 2.0;					---				更新体素网格时间阈值
double minRelZ = -1.5;								---				点云处理的最小高度
double maxRelZ = 0.2;								---				点云处理的最大高度
double disRatioZ = 0.2;								---				点云处理的高度与距离的比例

// terrain voxel parameters
float terrainVoxelSize = 1.0;						---				地形的分辨率
int terrainVoxelShiftX = 0;							---				地形中心与车体的偏移量
int terrainVoxelShiftY = 0;							---				地形中心与车体的偏移量
const int terrainVoxelWidth = 21;					---				地形的宽度
int terrainVoxelHalfWidth = (terrainVoxelWidth - 1) / 2;
const int terrainVoxelNum = terrainVoxelWidth * terrainVoxelWidth;

// planar voxel parameters
float planarVoxelSize = 0.2;						---				平面的分辨率
const int planarVoxelWidth = 51;					---				平面的宽度
int planarVoxelHalfWidth = (planarVoxelWidth - 1) / 2;
const int planarVoxelNum = planarVoxelWidth * planarVoxelWidth;

LocalPlanner 中一样,地形分割所接收到的点云信息同样也是在map坐标系下,因此首先要将其转换到车体坐标系下,即base_link,还包括点云的裁剪。在进行地面分割之前,做了一个点云滚动的操作。如下图所示,紫色的点云为 terrainVoxelCloud 地形,其他彩色则为分割后的结果,可发现其分割的区域并不完全是地形点云。因此猜想,这个点云滚动的操作,可能是为了同步点云和车辆的位置,毕竟点云是在map坐标下下的,比如说因为某些延迟,导致车辆与切割的点云信息存在偏差。当偏差大于 terrainVoxelSize 一个格子大小时,就通过翻滚,将车辆位置与点云位置进行配准。

在这里插入图片描述

while (vehicleX - terrainVoxelCenX > terrainVoxelSize) {
  for (int indY = 0; indY < terrainVoxelWidth; indY++) {
    pcl::PointCloud<pcl::PointXYZI>::Ptr terrainVoxelCloudPtr =
        terrainVoxelCloud[indY];
    for (int indX = 0; indX < terrainVoxelWidth - 1; indX++) {
      terrainVoxelCloud[terrainVoxelWidth * indX + indY] =
          terrainVoxelCloud[terrainVoxelWidth * (indX + 1) + indY];
    }
    terrainVoxelCloud[terrainVoxelWidth * (terrainVoxelWidth - 1) +
                      indY] = terrainVoxelCloudPtr;
    terrainVoxelCloud[terrainVoxelWidth * (terrainVoxelWidth - 1) + indY]
        ->clear();
  }
  terrainVoxelShiftX++;
  terrainVoxelCenX = terrainVoxelSize * terrainVoxelShiftX;
}

那么关于terrainVoxelCloud 的构建,是一个 terrainVoxelWidth 宽的正方形区域,因为其索引号为非负值,需要将接收到的点云数据,根据距离插入到对应的 Voxel 中。具体的序号计算公式如下,这里对于坐标的取整方式是四舍五入。
i n d X = p o i n t . x − v e h i c l e X t e r r a i n V o x e l S i z e + t e r r a i n V o x e l S i z e 2 indX={point.x-vehicleX \over terrainVoxelSize}+{terrainVoxelSize \over 2} indX=terrainVoxelSizepoint.xvehicleX+2terrainVoxelSize i n d Y = p o i n t . y − v e h i c l e Y t e r r a i n V o x e l S i z e + t e r r a i n V o x e l S i z e 2 indY={point.y-vehicleY \over terrainVoxelSize}+{terrainVoxelSize \over 2} indY=terrainVoxelSizepoint.yvehicleY+2terrainVoxelSize对于每一个点云都需要进行处理,不过对于在车辆左侧或者后侧的序号都要减1,因为此时和坐标值为正时四舍五入计算方式不一样。对于处理后点云序号非负的数据,便加入到地形体素网格点云中。由 terrainVoxelUpdateNum 记录每一个体素网格中存入点云的数量。

int indX = int((point.x - vehicleX + terrainVoxelSize / 2) /
               terrainVoxelSize) + terrainVoxelHalfWidth;
int indY = int((point.y - vehicleY + terrainVoxelSize / 2) /
               terrainVoxelSize) + terrainVoxelHalfWidth;

if (indX >= 0 && indX < terrainVoxelWidth && indY >= 0 && indY < terrainVoxelWidth) {
  terrainVoxelCloud[terrainVoxelWidth * indX + indY]->push_back(point);
  terrainVoxelUpdateNum[terrainVoxelWidth * indX + indY]++;
}

在所有点云都被存入到体素网格内后,对其进行处理。接收到第一帧点云数据为初始系统时间 systemInitTime,点云的强度被设置为激光数据的时间差。

systemInitTime = laserCloudTime;
point.intensity = laserCloudTime - systemInitTime;

在遍历每一个网格时,仅对格内需要更新的点云数量大于 voxelPointUpdateThre 的网格进行处理。并且与上一次该网格处理的时间差大于 voxelTimeUpdateThre

if (terrainVoxelUpdateNum[ind] >= voxelPointUpdateThre || 
	laserCloudTime - systemInitTime - terrainVoxelUpdateTime[ind] >= voxelTimeUpdateThre ||
	clearingCloud
	)

在体素栅格中,需要被进行地面分割的点云满足以下要求,这些点云会被放入 terrainCloud ,用于地面分割。

  1. 点云高度大于最小阈值
  2. 点云高度小于最大阈值
  3. 当前点云的时间与要处理的点云时间差小于阈值 decayTime,或者距离小于 noDecayDis
  4. 此时不会清除距离外的点云,或者不在需要被清除的距离之内

接下来便分割地面并计算每一个点的高程信息。这里又重新创建了一个二维数组用于存放平面点高程,存入的方法和之前一样,不过分辨率从1变成了0.2。除此之外,每一个点云不只放在同一个网格中,而是复制到一个3*3的邻域。

for (int dX = -1; dX <= 1; dX++) {
  for (int dY = -1; dY <= 1; dY++) {
    if (indX + dX >= 0 && indX + dX < planarVoxelWidth && indY + dY >= 0 && indY + dY < planarVoxelWidth) {
      planarPointElev[planarVoxelWidth * (indX + dX) + indY + dY].push_back(point.z);
    }
  }
}

在这一个二维数组中,找到该网格内点云高程最小值,作为该体素网格的高程,存入到体素网格 planarVoxelElev 中。此时的 planarVoxelElev 相当于保存了附近点云高程的最小值。

for (int j = 0; j < planarPointElevSize; j++) {
  if (planarPointElev[i][j] < minZ) {
    minZ = planarPointElev[i][j];
    minID = j;
  }
}

之前所做的工作都是为了点云分割,主要是用于地面分割的点云 terrainCloud ,和高程信息 planarVoxelElev ,下面才开始真正的对地面和障碍物进行分割。首先和存入到体素网格的思想一样,将点云信息和高程信息通过索引对应起来。

如果当前点云的高程比附近最小值大,小于车辆的高度,并且计算高程时的点云数量也足够多,就把当前点云放入到地形高程点云中。其中点云的强度为一个相对的高度。

if (planarVoxelDyObs[planarVoxelWidth * indX + indY] < minDyObsPointNum ||!clearDyObs) { // clearDyObs = false
  float disZ = point.z - planarVoxelElev[planarVoxelWidth * indX + indY];
  if (considerDrop) disZ = fabs(disZ); //considerDrop = false;
  int planarPointElevSize = planarPointElev[planarVoxelWidth * indX + indY].size();
  if (disZ >= 0 && disZ < vehicleHeight && planarPointElevSize >= minBlockPointNum) {
    terrainCloudElev->push_back(point);
    terrainCloudElev->points[terrainCloudElevSize].intensity = disZ;
    terrainCloudElevSize++;
  }
}

以上就是CMU开源导航算法中的地面分割,所用的思想就是计算当前位置与附近位置最小高程的一个相对高程。这样看来,在一个平滑的坡上效果表现还是可以,也可以改变邻域的大小,不过 3 ∗ 3 3*3 33也有0.6m的范围了。

除此之外还有一些其他的参数,例如 noDataObstacleclearDyObsuseSorting等,默认设置的为 false ,也就不会被执行,也就不再看了。在实际使用时,点云中心与车体中心或者说与激光雷达中心永远保持一致,则不需要通过翻滚配准。因为需要对上一帧翻滚,在源码中也没有对上一帧的点云进行清空,所以在实际使用时需要在主循环中对 terrainVoxelCloud 进行清空,否则会出现重影。可使用如下代码重置点云数据。

for (int i = 0; i < terrainVoxelNum; i++) {
  terrainVoxelCloud[i].reset(new pcl::PointCloud<pcl::PointXYZI>());
}
  • 9
    点赞
  • 59
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 9
    评论
以下是恒模算法CMA的matlab源代码: ```matlab function [xopt, fopt] = cmaes(func, x0, sigma0, varargin) % CMA-ES algorithm for nonlinear optimization. % % function [xopt, fopt] = cmaes(func, x0, sigma0, varargin) % % INPUTS: % func - handle to the objective function % x0 - starting point (column vector) % sigma0 - initial step size (scalar) % varargin - additional parameters passed to the objective function % (optional) % % OUTPUTS: % xopt - optimal solution found by the algorithm (column vector) % fopt - function value at the optimal solution % % NOTE: % See 'cmaes_example.m' for an example of usage. % % REFERENCES: % [1] Hansen, N. "The CMA Evolution Strategy: A Tutorial" (2011) % [2] Hansen, N. "The CMA Evolution Strategy: A Comparing Review" % (2016) - https://arxiv.org/abs/1604.00772 % [3] Hansen, N. "Benchmarking a BI-population CMA-ES on the BBOB-2009 % Function Testbed" (2010) - https://arxiv.org/abs/1004.1745 % % AUTHOR: % Emanuele Ruffaldi, 2017 % dimension of the problem n = length(x0); % default options options.lambda = 4 + floor(3 * log(n)); % population size options.mu = floor(options.lambda / 2); % number of parents options.weights = log(options.mu + 1/2) - log(1:options.mu)'; % weights options.weights = options.weights / sum(options.weights); % normalize options.mueff = sum(options.weights)^2 / sum(options.weights.^2); % variance-effectiveness of sum w_i x_i options.cc = 4 / (n + 4); % time constant for cumulation for C options.cs = (options.mueff + 2) / (n + options.mueff + 5); % time constant for cumulation for sigma control options.c1 = 2 / ((n + 1.3)^2 + options.mueff); % learning rate for rank-one update of C options.cmu = min([1 - options.c1, 2 * (options.mueff - 2 + 1/options.mueff) / ((n + 2)^2 + options.mueff)]); % learning rate for rank-mu update of C options.damps = 1 + 2 * max([0, sqrt((options.mueff - 1) / (n + 1)) - 1]) + options.cs; % damping for sigma options.pc = zeros(n, 1); % evolution path for C options.ps = zeros(n, 1); % evolution path for sigma options.B = eye(n); % coordinate system matrix options.D = ones(n, 1); % diagonal matrix of square roots of eigenvalues options.eigenval = zeros(n, 1); % eigenvalues options.xmean = x0; % mean of the current population options.sigma = sigma0; % step size % parse user defined options names = fieldnames(options); for i = 1:2:length(varargin) if ismember(varargin{i}, names) options.(varargin{i}) = varargin{i + 1}; else error('Invalid parameter name: %s', varargin{i}); end end % initialize population pop = repmat(options.xmean, 1, options.lambda) + options.sigma * randn(n, options.lambda); fitness = zeros(options.lambda, 1); for i = 1:options.lambda fitness(i) = func(pop(:, i), varargin{:}); end % main loop for t = 1:1e6 % maximum number of iterations [~, idx] = sort(fitness, 'ascend'); xold = options.xmean; xmean = pop(:, idx(1:options.mu)) * options.weights; % calculate new mean z = options.B' * (pop(:, idx(1:options.mu)) - repmat(xold, 1, options.mu)); % calculate z pc = (1 - options.cc) * options.pc + sqrt(options.cc * (2 - options.cc) * options.mueff) * z / options.sigma; % update evolution path for C ps = (1 - options.cs) * options.ps + sqrt(options.cs * (2 - options.cs) * options.mueff) * options.B * z; % update evolution path for sigma B = options.B * (eye(n) + options.c1 * (pc * pc' - options.B * options.D * options.B') + options.cmu * z * (repmat(options.weights', n, 1) .* z')); % update coordinate system matrix D = diag(options.D.^2); D = D^(1/2) * (eye(n) + options.c1 * ((repmat(options.weights', n, 1) .* pc) * pc') + options.cmu * (z * (repmat(options.weights', n, 1) .* z'))) * D^(1/2); % update diagonal matrix of square roots of eigenvalues [~, ind] = sort(options.eigenval, 'descend'); options.D = diag(diag(D(ind, :))); % sort diagonal matrix options.B = B(:, ind); % sort coordinate system matrix options.eigenval = diag(D).^2; % update eigenvalues options.sigma = options.sigma * exp((options.cs / options.damps) * (norm(ps) / norm(normrnd(0, 1, n, 1))) - 1); % update step size if mod(t, ceil(1 / (options.cc + options.cs) / n / 10)) == 0 % check stopping conditions every n/10 iterations if max(diag(D)) / min(diag(D)) > 1e7 % check condition number of D break; end if sum((options.sigma * options.B(:, end)).^2) > ((1.4 + 2 / (n + 1)) * n)^2 % check if maximum coordinate exceeds typical range break; end if max(fitness) - min(fitness) < 1e-10 % check function value range break; end end % generate new population pop = repmat(xmean, 1, options.lambda) + options.sigma * options.B * diag(randn(n, options.lambda) * diag(sqrt(options.eigenval))) * options.B' * randn(n, options.lambda); for i = 1:options.lambda fitness(i) = func(pop(:, i), varargin{:}); end if min(fitness) < fopt % update best solution fopt = min(fitness); xopt = pop(:, fitness == fopt); end options.xmean = xmean; % update mean options.pc = pc; % update evolution path for C options.ps = ps; % update evolution path for sigma options.B = B; % update coordinate system matrix options.D = D; % update diagonal matrix of square roots of eigenvalues end ``` 其中,`func` 是指目标函数的函数句柄,`x0` 是指变量的初始值,`sigma0` 是指初始步长。`varargin` 是可选参数列表,将传递给目标函数。 函数的输出是 `xopt` 和 `fopt`,分别表示最佳解和最佳解的目标函数值。 该实现中使用了默认的参数,但是也可以通过在 `varargin` 中指定参数来更改默认值。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

秃头队长

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值