Этот пример демонстрирует, как реализовать алгоритм Одновременной локализации и отображения (SLAM) на собранных 3-D данных о датчике лидара с помощью алгоритмов обработки облака точек и оптимизации графика положения. Цель этого примера состоит в том, чтобы оценить траекторию робота и создать 3-D карту заполнения среды от 3-D облаков точек лидара и оцененной траектории.
Продемонстрированный алгоритм SLAM оценивает траекторию с помощью основанного на Преобразовании нормального распределения (NDT) алгоритма регистрации облака точек и уменьшает дрейф с помощью оптимизации графика положения SE3 с помощью решателя доверительной области каждый раз, когда робот повторно посещает место.
Загрузите 3-D данные о лидаре, собранные от Хриплого робота Clearpath™ в гараже. Данные о лидаре содержат массив ячеек n
- 3 матрицы, где n является точками номера 3-D в собранных данных о лидаре и 3 столбцами, представляют xyz-координаты, сопоставленные с каждой полученной точкой.
load pClouds.mat
Задайте параметры для оценки траектории с помощью алгоритма регистрации облака точек. maxLidarRange
указывает максимальный диапазон 3-D лазерного сканера.
maxLidarRange = 20;
Данные об облаке точек, собранные во внутренней среде, содержат точки, лежащие на земле и перекрывающие плоскости, который путает алгоритмы регистрации облака точек. Так удалены из облака точек этими параметрами:
referenceVector
- Нормальный к наземной плоскости.
maxDistance
- Максимальное расстояние для inliers при удалении земли и потолке плоскостей.
maxAngularDistance
- Максимальное угловое отклонение от ссылочного вектора при подборе кривой земле и потолке плоскостей.
referenceVector = [0, 0, 1]; maxDistance = 0.5; maxAngularDistance = 15;
Чтобы повысить эффективность и точность регистрационного алгоритма, облака точек прорежены с помощью случайной выборки с демонстрационным отношением, заданным randomSampleRatio
.
randomSampleRatio = 0.25;
gridStep
задает размеры сетки вокселя в регистрационном алгоритме NDT. Сканирование принято только после перемещений робота расстоянием, больше, чем distanceMovedThreshold
.
gridStep = 2.5; distanceMovedThreshold = 0.3;
Настройте эти параметры для своего определенного робота и среды.
Задайте параметры для алгоритма оценки закрытия цикла. Закрытие цикла ищется только в радиусе вокруг текущего местоположения робота, заданного loopClosureSearchRadius
.
loopClosureSearchRadius = 3;
Алгоритм закрытия цикла основан на 2D подкарте и соответствии сканирования. Подкарта создается после каждого nScansPerSubmap
(Количество Сканирований на подкарту) принятые сканирования. Алгоритм закрытия цикла игнорирует новый subMapThresh
сканирования при поиске кандидатов цикла for.
nScansPerSubmap = 3; subMapThresh = 50;
Кольцевая область с пределами z, заданными annularRegionLimits
извлечен из облаков точек, чтобы удалить остающиеся точки на полу и перекрывающий после экстракции необходимой области с помощью алгоритмов подгонки плоскости облака точек.
annularRegionLimits = [-0.75,0.75];
Максимальная приемлемая Среднеквадратическая ошибка (RMSE) в оценке относительного положения между кандидатами цикла задана rmseThreshold
. Выберите нижнее значение для оценки точных ребер закрытия цикла, который оказывает высокое влияние на оптимизацию графика положения.
rmseThreshold = 0.26;
Порог по счету соответствия сканирования, чтобы принять закрытие цикла задан loopClosureThreshold
. Позируйте Оптимизация Графика называется после вставки optimizationInterval
ребра закрытия цикла в график положения.
loopClosureThreshold = 150; optimizationInterval = 2;
Настройте график положения, карту заполнения и необходимые переменные.
% 3D Posegraph object for storing estimated relative poses pGraph = poseGraph3D; % Default serialized upper-right triangle of 6-by-6 Information Matrix infoMat = [1,0,0,0,0,0,1,0,0,0,0,1,0,0,0,1,0,0,1,0,1]; % Number of loop closure edges added since last pose graph optimization and map refinement numLoopClosuresSinceLastOptimization = 0; % True after pose graph optimization until the next scan mapUpdated = false; % Equals to 1 if the scan is accepted scanAccepted = 0; % 3D Occupancy grid object for creating and visualizing 3D map mapResolution = 8; % cells per meter omap = occupancyMap3D(mapResolution);
Предварительно выделите переменные для обработанных облаков точек, лоцируйте сканирования и подкарты. Создайте прореженный набор облаков точек для того, чтобы быстро визуализировать карту.
pcProcessed = cell(1,length(pClouds)); lidarScans2d = cell(1,length(pClouds)); submaps = cell(1,length(pClouds)/nScansPerSubmap); pcsToView = cell(1,length(pClouds));
Создайте переменные в целях отображения.
% Set to 1 to visualize created map and posegraph during build process viewMap = 1; % Set to 1 to visualize processed point clouds during build process viewPC = 0;
Установите случайный seed гарантировать сопоставимую случайную выборку.
rng(0);
Инициализируйте окна рисунка при желании.
% If you want to view the point clouds while processing them sequentially if viewPC==1 pplayer = pcplayer([-50 50],[-50 50],[-10 10],'MarkerSize',10); end % If you want to view the created map and posegraph during build process if viewMap==1 ax = newplot; % Figure axis handle view(20,50); grid on; end
Траектория робота является набором положений робота (местоположение и ориентация в трехмерном пространстве). Положение робота оценивается в каждом 3-D экземпляре приобретения сканирования лидара с помощью 3-D лидара алгоритм SLAM. 3-D лидар алгоритм SLAM имеет следующие шаги:
Фильтрация облака точек
Субдискретизация облака точек
Регистрация облака точек
Запрос закрытия цикла
Изложите оптимизацию графика
Итеративно обработайте облака точек, чтобы оценить траекторию.
count = 0; % Counter to track number of scans added disp('Estimating robot trajectory...');
Estimating robot trajectory...
for i=1:length(pClouds) % Read point clouds in sequence pc = pClouds{i};
Фильтрация облака точек сделана, чтобы извлечь необходимую область из полученного сканирования. В этом примере необходимая область является кольцевой областью с землей и потолком удаленного.
Удалите недопустимые точки вне макс. области значений и ненужные точки позади робота, соответствующего человеческому драйверу.
ind = (-maxLidarRange < pc(:,1) & pc(:,1) < maxLidarRange ... & -maxLidarRange < pc(:,2) & pc(:,2) < maxLidarRange ... & (abs(pc(:,2))>abs(0.5*pc(:,1)) | pc(:,1)>0)); pcl = pointCloud(pc(ind,:));
Удалите точки на наземной плоскости.
[~, ~, outliers] = ... pcfitplane(pcl, maxDistance,referenceVector,maxAngularDistance); pcl_wogrd = select(pcl,outliers,'OutputSize','full');
Удалите точки на плоскости потолка.
[~, ~, outliers] = ... pcfitplane(pcl_wogrd,0.2,referenceVector,maxAngularDistance); pcl_wogrd = select(pcl_wogrd,outliers,'OutputSize','full');
Выберите точки в кольцевой области.
ind = (pcl_wogrd.Location(:,3)<annularRegionLimits(2))&(pcl_wogrd.Location(:,3)>annularRegionLimits(1)); pcl_wogrd = select(pcl_wogrd,ind,'OutputSize','full');
Субдискретизация облака точек улучшает скорость и точность алгоритма регистрации облака точек. Вниз выборка должна быть настроена для определенных потребностей. Случайный алгоритм выборки выбран опытным путем из вниз выборки вариантов ниже для текущего сценария.
pcl_wogrd_sampled = pcdownsample(pcl_wogrd,'random',randomSampleRatio); if viewPC==1 % Visualize down sampled point cloud view(pplayer,pcl_wogrd_sampled); pause(0.001) end
Регистрация облака точек оценивает относительное положение (вращение и перевод) между текущим сканированием и предыдущим сканированием. Первое сканирование всегда принимается (обработал далее и сохранил), но другие сканирования только приняты после перевода больше, чем заданный порог. poseGraph3D
используется, чтобы сохранить предполагаемые принятые относительные положения (траектория).
if count == 0 % First can tform = []; scanAccepted = 1; else if count == 1 tform = pcregisterndt(pcl_wogrd_sampled,prevPc,gridStep); else tform = pcregisterndt(pcl_wogrd_sampled,prevPc,gridStep,... 'InitialTransform',prevTform); end relPose = [tform2trvec(tform.T') tform2quat(tform.T')]; if sqrt(norm(relPose(1:3))) > distanceMovedThreshold addRelativePose(pGraph,relPose); scanAccepted = 1; else scanAccepted = 0; end end
Запрос закрытия цикла происходит после принятия сканирования. Запрос включает нахождение подобия между текущим сканированием и ранее принятыми сканированиями. Подобное найденное сканирование называется кандидатом цикла. Кандидат цикла оценка состоит из следующих шагов
Оцените, что подкарты, совпадающие с текущим сканированием, оцениваются с помощью подкарты и алгоритма соответствия сканирования.
Каждая подкарта представляет nScansPerSubmap
последовательные сканирования. Предыдущие сканирования, представленные всеми подкартами соответствия, рассматриваются как соответствие текущему сканированию (вероятные кандидаты цикла).
Относительное положение между текущим сканированием и вероятным кандидатом цикла вычисляется с помощью регистрационного алгоритма NDT. Кандидат цикла с наименее относительной ошибкой оценки положения (RMSE) рассматривается как лучшее соответствие для текущего сканирования.
Лучшее соответствие принято как допустимое закрытие цикла только, когда RMSE меньше заданного порога.
if scanAccepted == 1 count = count + 1; pcProcessed{count} = pcl_wogrd_sampled; lidarScans2d{count} = exampleHelperCreate2DScan(pcl_wogrd_sampled); % Submaps are created for faster loop closure query. if rem(count,nScansPerSubmap)==0 submaps{count/nScansPerSubmap} = exampleHelperCreateSubmap(lidarScans2d,... pGraph,count,nScansPerSubmap,maxLidarRange); end % loopSubmapIds contains matching submap ids if any otherwise empty. if (floor(count/nScansPerSubmap)>subMapThresh) [loopSubmapIds,~] = exampleHelperEstimateLoopCandidates(pGraph,... count,submaps,lidarScans2d{count},nScansPerSubmap,... loopClosureSearchRadius,loopClosureThreshold,subMapThresh); if ~isempty(loopSubmapIds) rmseMin = inf; % Estimate best match to the current scan for k = 1:length(loopSubmapIds) % For every scan within the submap for j = 1:nScansPerSubmap probableLoopCandidate = ... loopSubmapIds(k)*nScansPerSubmap - j + 1; [loopTform,~,rmse] = pcregisterndt(pcl_wogrd_sampled,... pcProcessed{probableLoopCandidate},gridStep); % Update best Loop Closure Candidate if rmse < rmseMin loopCandidate = probableLoopCandidate; rmseMin = rmse; end if rmseMin < rmseThreshold break; end end end % Check if loop candidate is valid if rmseMin < rmseThreshold % loop closure constraint relPose = [tform2trvec(loopTform.T') tform2quat(loopTform.T')]; addRelativePose(pGraph,relPose,infoMat,... loopCandidate,count); numLoopClosuresSinceLastOptimization = numLoopClosuresSinceLastOptimization + 1; end end end
Позируйте оптимизация графика выполняется после достаточного числа ребер цикла, как, принимают, уменьшают дрейф по оценке траектории. После каждой оптимизации закрытия цикла уменьшается радиус поиска закрытия цикла вследствие того, что неопределенность по оценке положения уменьшает после оптимизации.
if (numLoopClosuresSinceLastOptimization == optimizationInterval)||... ((numLoopClosuresSinceLastOptimization>0)&&(i==length(pClouds))) if loopClosureSearchRadius ~=1 disp('Doing Pose Graph Optimization to reduce drift.'); end % pose graph optimization pGraph = optimizePoseGraph(pGraph); loopClosureSearchRadius = 1; if viewMap == 1 position = pGraph.nodes; % Rebuild map after pose graph optimization omap = occupancyMap3D(mapResolution); for n = 1:(pGraph.NumNodes-1) insertPointCloud(omap,position(n,:),pcsToView{n}.removeInvalidPoints,maxLidarRange); end mapUpdated = true; ax = newplot; grid on; end numLoopClosuresSinceLastOptimization = 0; % Reduce the frequency of optimization after optimizing % the trjectory optimizationInterval = optimizationInterval*7; end
Визуализируйте карту и график положения во время процесса сборки. Эта визуализация является дорогостоящей, поэтому включите ее только при необходимости установкой viewMap
к 1. Если визуализация включена затем, график обновляется после каждых 15 добавленных сканирований.
pcToView = pcdownsample(pcl_wogrd_sampled, 'random', 0.5); pcsToView{count} = pcToView; if viewMap==1 % Insert point cloud to the occupance map in the right position position = pGraph.nodes(count); insertPointCloud(omap,position,pcToView.removeInvalidPoints,maxLidarRange); if (rem(count-1,15)==0)||mapUpdated exampleHelperVisualizeMapAndPoseGraph(omap, pGraph, ax); end mapUpdated = false; else % Give feedback to know that example is running if (rem(count-1,15)==0) fprintf('.'); end end
Обновите предыдущую относительную оценку положения и облако точек.
prevPc = pcl_wogrd_sampled; prevTform = tform; end end
Doing Pose Graph Optimization to reduce drift.
Облака точек вставляются в occupancyMap3D
использование предполагаемых глобальных положений. После итерации через все узлы, полную карту и оцененную траекторию транспортного средства показан.
if (viewMap ~= 1)||(numLoopClosuresSinceLastOptimization>0) nodesPositions = nodes(pGraph); % Create 3D Occupancy grid omapToView = occupancyMap3D(mapResolution); for i = 1:(size(nodesPositions,1)-1) pc = pcsToView{i}; position = nodesPositions(i,:); % Insert point cloud to the occupance map in the right position insertPointCloud(omapToView,position,pc.removeInvalidPoints,maxLidarRange); end figure; axisFinal = newplot; exampleHelperVisualizeMapAndPoseGraph(omapToView, pGraph, axisFinal); end