Этот пример демонстрирует, как реализовать алгоритм одновременной локализации и картографии (SLAM) для воздушного отображения с использованием 3-D функций. Цель этого примера состоит в том, чтобы оценить траекторию робота и создать карту облака точек его окружения.
Алгоритм SLAM в этом примере оценивает траекторию путем нахождения грубого выравнивания между уменьшенными принятыми сканами, используя дескрипторы гистограммы функций быстрой точки (FPFH), извлеченные в каждой точке, затем применяет итерационный алгоритм ближайшей точки (ICP), чтобы точно настроить выравнивание. 3-D оптимизации графика положения из Navigation Toolbox™ уменьшает дрейф в предполагаемой траектории.
Создайте синтетические сканы лидара из закрашенной фигуры с воздушными данными, загруженного с сайта OpenTopography [1]. Загрузите MAT-файл, содержащий последовательность путевых точек по воздушным данным, которая определяет траекторию робота. Вычислите сканы лидара из данных в каждой путевой точке с помощью helperCreateDataset
функция. Функция выводит сканы лидара, вычисленные в каждой путевой точке как массив pointCloud
объекты, оригинальная карта, покрытая роботом и основной истиной путевыми точками.
datafile = 'aerialMap.tar.gz'; wayPointsfile = 'gTruthWayPoints.mat'; % Generate a lidar scan at each waypoint using the helper function [pClouds,orgMap,gTruthWayPts] = helperCreateDataset(datafile,wayPointsfile);
Визуализируйте основную истину путевые точки на исходной карте, покрытой роботом.
% Create a figure window to visualize the ground truth map and waypoints hFigGT = figure; axGT = axes('Parent',hFigGT,'Color','black'); % Visualize the ground truth waypoints pcshow(gTruthWayPts,'red','MarkerSize',150,'Parent',axGT) hold on % Visualize the original map covered by the robot pcshow(orgMap,'MarkerSize',10,'Parent',axGT) hold off % Customize the axis labels xlabel(axGT,'X (m)') ylabel(axGT,'Y (m)') zlabel(axGT,'Z (m)') title(axGT,'Ground Truth Map And Robot Trajectory')
Визуализируйте извлеченные сканы лидара с помощью pcplayer
объект.
% Specify limits for the player xlimits = [-90 90]; ylimits = [-90 90]; zlimits = [-625 -587]; % Create a pcplayer object to visualize the lidar scans lidarPlayer = pcplayer(xlimits,ylimits,zlimits); % Customize the pcplayer axis labels xlabel(lidarPlayer.Axes,'X (m)') ylabel(lidarPlayer.Axes,'Y (m)') zlabel(lidarPlayer.Axes,'Z (m)') title(lidarPlayer.Axes,'Lidar Scans') % Loop over and visualize the data for l = 1:length(pClouds) % Extract the lidar scan ptCloud = pClouds(l); % Update the lidar display view(lidarPlayer,ptCloud) pause(0.05) end
Задайте параметры для оценки траектории и замыкания цикла. Настройте эти параметры для вашего конкретного робота и окружения.
Укажите количество сканов лидара, которые будут пропущены между каждым сканом, принятым для регистрации. Поскольку последующие сканы имеют высокое перекрытие, пропуск нескольких систем координат может улучшить скорость алгоритма, не ставя под угрозу точность.
skipFrames = 3;
Сканы лидара с понижающей дискретизацией могут улучшить скорость алгоритма. Прямоугольный сеточный фильтр понижает значение облака точек путем усреднения всех точек в каждой камере до одной точки. Задайте размер отдельных камер коробчатого сеточного фильтра в метрах.
gridStep = 1.5; % in meters
Дескрипторы FPFH извлекаются для каждой допустимой точки в облаке точек с понижающей дискретизацией. Укажите размер окрестности для метода поиска k-ближайших соседей (KNN), используемого для вычисления дескрипторов.
neighbors = 60;
Установите порог и отношение для соответствия дескрипторов FPFH, используемых для идентификации соответствия точек.
matchThreshold = 0.1; matchRatio = 0.97;
Установите максимальное расстояние и количество маршрутов для относительной оценки положения между последовательными сканами.
maxDistance = 1; maxNumTrails = 3000;
Укажите процент инлиеров, которые нужно учитывать для подстройки относительных положений.
inlierRatio = 0.1;
Задайте размер каждой камеры коробчатого сеточного фильтра, используемого для создания карт путем выравнивания сканов лидара.
alignGridStep = 1.2;
Задайте радиус вокруг текущего местоположения робота, чтобы найти кандидатов замыкания цикла.
loopClosureSearchRadius = 7.9;
Алгоритм замыкания цикла основан на создании и 3-D подкарты. Подкарта состоит из заданного количества принятых сканов nScansPerSubmap
. Алгоритм закрытия цикла также игнорирует указанное количество самых последних сканов subMapThresh
, во время поиска кандидатов циклов. Задайте корневую среднюю квадратичную невязку (RMSE) loopClosureThreshold,
для принятия подкарты в качестве совпадающей. Более низкий счет может указывать на лучшее соответствие, но счета варьируются на основе данных о датчике и предварительной обработки.
nScansPerSubmap = 3; subMapThresh = 15; loopClosureThreshold = 0.6;
Задайте максимально допустимую среднюю квадратичную невязку корня (RMSE) для оценки относительного положения между кандидатами цикла rmseThreshold
. Выбор более низкого значения может привести к более точным ребрам замыкания цикла, что оказывает высокое влияние на оптимизацию графика положения, но счета варьируются на основе данных о датчике и предварительной обработки.
rmseThreshold = 0.6;
Создайте график положения, используя poseGraph3D
Объект (Navigation Toolbox) для хранения предполагаемых относительных положений между принятыми сканами лидара.
pGraph = poseGraph3D;
% Default serialized upper-right triangle of a 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];
Предварительно выделите и инициализируйте переменные, необходимые для расчетов.
% Allocate memory to store submaps subMaps = cell(floor(length(pClouds)/(skipFrames*nScansPerSubmap)),1); % Allocate memory to store each submap pose subMapPoses = zeros(round(length(pClouds)/(skipFrames*nScansPerSubmap)),3); % Initialize variables to store accepted scans and their transforms for % submap creation pcAccepted = pointCloud.empty(0); tformAccepted = rigid3d.empty(0); % Initialize variable to store relative poses from the feature-based approach % without pose graph optimization fpfhTform = rigid3d.empty(0); % Counter to track the number of scans added count = 1;
Создайте переменные, чтобы визуализировать обработанные сканы лидара и предполагаемую траекторию.
% Set to 1 to visualize processed lidar scans during build process viewPC = 0; % Create a pcplayer object to view the lidar scans while % processing them sequentially, if viewPC is enabled if viewPC == 1 pplayer = pcplayer(xlimits,ylimits,zlimits); % Customize player axis labels xlabel(pplayer.Axes,'X (m)') ylabel(pplayer.Axes,'Y (m)') zlabel(pplayer.Axes,'Z (m)') title(pplayer.Axes,'Processed Scans') end % Create a figure window to visualize the estimated trajectory hFigTrajUpdate = figure; axTrajUpdate = axes('Parent',hFigTrajUpdate,'Color','black'); title(axTrajUpdate,'Sensor Pose Trajectory')
Траектория робота - это набор его положений, состоящий из его расположения и ориентации в трехмерном пространстве. Оцените положение робота из образца сканирования 3-D лидара с помощью 3-D алгоритма SLAM. Используйте эти процессы для выполнения 3-D оценки SLAM:
Понижающая дискретизация облака точек
Регистрация облака точек
Создание подкарты
Запрос замыкания цикла
Оптимизация графика положения
Итерационно обработайте сканы лидара, чтобы оценить траекторию робота.
rng('default') % Set random seed to guarantee consistent results in pcmatchfeatures for FrameIdx = 1:skipFrames:length(pClouds)
Понижающая дискретизация облака точек может улучшить скорость алгоритма регистрации облака точек. Понижающая дискретизация должна быть настроена на ваши конкретные потребности.
% Downsample the current scan curScan = pcdownsample(pClouds(FrameIdx),'gridAverage',gridStep); if viewPC == 1 % Visualize down sampled point cloud view(pplayer,curScan) end
Регистрация облака точек оценивает относительное положение между текущим сканом и предыдущим сканом. Пример использует следующие шаги для регистрации:
Извлекает дескрипторы FPFH из каждого скана с помощью extractFPFHFeatures
функция
Идентифицирует соответствия точек путем сравнения дескрипторов с помощью pcmatchfeatures
функция
Оценивает относительное положение из соответствия точек с помощью estimateGeometricTransform3D
функция
Точная настройка относительного положения с помощью pcregistericp
функция
% Extract FPFH features curFeature = extractFPFHFeatures(curScan,'NumNeighbors',neighbors); if FrameIdx == 1 % Update the acceptance scan and its tform pcAccepted(count,1) = curScan; tformAccepted(count,1) = rigid3d; % Update the initial pose to the first waypoint of ground truth for % comparison fpfhTform(count,1) = rigid3d(eye(3),gTruthWayPts(1,:)); else % Identify correspondences by matching current scan to previous scan indexPairs = pcmatchfeatures(curFeature,prevFeature,curScan,prevScan, ... 'MatchThreshold',matchThreshold,'RejectRatio',matchRatio); matchedPrevPts = select(prevScan,indexPairs(:,2)); matchedCurPts = select(curScan,indexPairs(:,1)); % Estimate relative pose between current scan and previous scan % using correspondences tform1 = estimateGeometricTransform3D(matchedCurPts.Location, ... matchedPrevPts.Location,'rigid','MaxDistance',maxDistance, ... 'MaxNumTrials',maxNumTrails); % Perform ICP registration to fine-tune relative pose tform = pcregistericp(curScan,prevScan,'InitialTransform',tform1, ... 'InlierRatio',inlierRatio);
Преобразуйте твердое преобразование в xyz-положение и кватернионную ориентацию, чтобы добавить его к графику положения.
relPose = [tform2trvec(tform.T') tform2quat(tform.T')];
% Add relative pose to pose graph
addRelativePose(pGraph,relPose);
Сохраните принятые сканы и их положения для создания подкарты.
% Update counter and store accepted scans and their poses count = count + 1; pcAccepted(count,1) = curScan; accumPose = pGraph.nodes(height(pGraph.nodes)); tformAccepted(count,1) = rigid3d((trvec2tform(accumPose(1:3)) * ... quat2tform(accumPose(4:7)))'); % Update estimated poses fpfhTform(count) = rigid3d(tform.T * fpfhTform(count-1).T); end
Создайте подкарты путем выравнивания последовательных, принятых сканов друг с другом в группах заданного размера nScansPerSubmap
, с использованием pcalign
функция. Использование подкартов может привести к более быстрым запросам на закрытие цикла.
currSubMapId = floor(count/nScansPerSubmap); if rem(count,nScansPerSubmap) == 0 % Align an array of lidar scans to create a submap subMaps{currSubMapId} = pcalign(... pcAccepted((count - nScansPerSubmap + 1):count,1), ... tformAccepted((count - nScansPerSubmap + 1):count,1), ... alignGridStep); % Assign center scan pose as pose of submap subMapPoses(currSubMapId,:) = tformAccepted(count - ... floor(nScansPerSubmap/2),1).Translation; end
Используйте запрос замыкания цикла, чтобы идентифицировать ранее посещенные места. Запрос на закрытие цикла состоит из нахождения подобия между текущим сканом и предыдущими подкартами. Если вы находите аналогичный скан, то текущий скан является кандидатом на закрытие цикла. Оценка кандидата на закрытие цикла состоит из следующих шагов:
Оценка соответствует между предыдущими подкартами и текущим сканом с помощью helperEstimateLoopCandidates
функция. Подчиненная карта соответствует, если RMSE между текущим сканом и подкартой меньше заданного значения loopClosureThreshold.
Предыдущие сканы, представленные всеми соответствующими подкартами, являются кандидатами на закрытие цикла.
Вычислите относительное положение между текущим сканом и кандидатом на закрытие цикла с помощью алгоритма регистрации ICP. Кандидат на закрытие цикла с самым низким RMSE является наилучшим вероятным соответствием для текущего скана.
Кандидат на закрытие цикла принимается как допустимое закрытие цикла только, когда RMSE ниже заданного порога.
if currSubMapId > subMapThresh mostRecentScanCenter = pGraph.nodes(height(pGraph.nodes)); % Estimate possible loop closure candidates by matching current % scan with submaps [loopSubmapIds,~] = helperEstimateLoopCandidates(subMaps,curScan, ... subMapPoses,mostRecentScanCenter,currSubMapId,subMapThresh, ... loopClosureSearchRadius,loopClosureThreshold); if ~isempty(loopSubmapIds) rmseMin = inf; % Estimate the best match for the current scan from the matching submap ids for k = 1:length(loopSubmapIds) % Check every scan within the submap for kf = 1:nScansPerSubmap probableLoopCandidate = ... loopSubmapIds(k)*nScansPerSubmap - kf + 1; [pose_Tform,~,rmse] = pcregistericp(curScan, ... pcAccepted(probableLoopCandidate)); % Update the best loop closure candidate if rmse < rmseMin rmseMin = rmse; matchingNode = probableLoopCandidate; Pose = [tform2trvec(pose_Tform.T') ... tform2quat(pose_Tform.T')]; end end end % Check if loop closure candidate is valid if rmseMin < rmseThreshold % Add relative pose of loop closure candidate to pose graph addRelativePose(pGraph,Pose,infoMat,matchingNode, ... pGraph.NumNodes); end end end % Update previous point cloud and feature prevScan = curScan; prevFeature = curFeature; % Visualize the estimated trajectory from the accepted scan. show(pGraph,'IDs','off','Parent',axTrajUpdate); drawnow end
Уменьшите дрейф в предполагаемой траектории при помощи optimizePoseGraph
(Navigation Toolbox) функция. В целом положение первого узла в графике положения представляет источник. Чтобы сравнить предполагаемую траекторию с основной истиной путевыми точками, задайте первую основную истину путевую точку в качестве положения первого узла.
pGraph = optimizePoseGraph(pGraph,'FirstNodePose',[gTruthWayPts(1,:) 1 0 0 0]);
Визуализируйте предполагаемую траекторию, используя функции без оптимизации графика положения, функции с оптимизацией графика положения и достоверные данные.
% Get estimated trajectory from pose graph pGraphTraj = pGraph.nodes; % Get estimated trajectory from feature-based registration without pose % graph optimization fpfhEstimatedTraj = zeros(count,3); for i = 1:count fpfhEstimatedTraj(i,:) = fpfhTform(i).Translation; end % Create a figure window to visualize the ground truth and estimated % trajectories hFigTraj = figure; axTraj = axes('Parent',hFigTraj,'Color','black'); plot3(fpfhEstimatedTraj(:,1),fpfhEstimatedTraj(:,2),fpfhEstimatedTraj(:,3), ... 'r*','Parent',axTraj) hold on plot3(pGraphTraj(:,1),pGraphTraj(:,2),pGraphTraj(:,3),'b.','Parent',axTraj) plot3(gTruthWayPts(:,1),gTruthWayPts(:,2),gTruthWayPts(:,3),'go','Parent',axTraj) hold off axis equal view(axTraj,2) xlabel(axTraj,'X (m)') ylabel(axTraj,'Y (m)') zlabel(axTraj,'Z (m)') title(axTraj,'Trajectory Comparison') legend(axTraj,'Estimated trajectory without pose graph optimization', ... 'Estimated trajectory with pose graph optimization', ... 'Ground Truth Trajectory','Location','bestoutside')
Преобразуйте и объедините сканы лидара, используя предполагаемые положения, чтобы создать накопленную карту облака точек.
% Get the estimated trajectory from poses estimatedTraj = pGraphTraj(:,1:3); % Convert relative poses to rigid transformations estimatedTforms = rigid3d.empty(0); for idx=1:pGraph.NumNodes pose = pGraph.nodes(idx); rigidPose = rigid3d((trvec2tform(pose(1:3)) * quat2tform(pose(4:7)))'); estimatedTforms(idx,1) = rigidPose; end % Create global map from processed point clouds and their relative poses globalMap = pcalign(pcAccepted,estimatedTforms,alignGridStep); % Create a figure window to visualize the estimated map and trajectory hFigTrajMap = figure; axTrajMap = axes('Parent',hFigTrajMap,'Color','black'); pcshow(estimatedTraj,'red','MarkerSize',150,'Parent',axTrajMap) hold on pcshow(globalMap,'MarkerSize',10,'Parent',axTrajMap) hold off % Customize axis labels xlabel(axTrajMap,'X (m)') ylabel(axTrajMap,'Y (m)') zlabel(axTrajMap,'Z (m)') title(axTrajMap,'Estimated Robot Trajectory And Generated Map')
Визуализируйте основную истину карту и предполагаемую карту.
% Create a figure window to display both the ground truth map and estimated map hFigMap = figure('Position',[0 0 700 400]); axMap1 = subplot(1,2,1,'Color','black','Parent',hFigMap); axMap1.Position = [0.08 0.2 0.4 0.55]; pcshow(orgMap,'Parent',axMap1) xlabel(axMap1,'X (m)') ylabel(axMap1,'Y (m)') zlabel(axMap1,'Z (m)') title(axMap1,'Ground Truth Map') axMap2 = subplot(1,2,2,'Color','black','Parent',hFigMap); axMap2.Position = [0.56 0.2 0.4 0.55]; pcshow(globalMap,'Parent',axMap2) xlabel(axMap2,'X (m)') ylabel(axMap2,'Y (m)') zlabel(axMap2,'Z (m)') title(axMap2,'Estimated Map')
readPointCloud
| insertPointCloud
(Navigation Toolbox) | rayIntersection
(Navigation Toolbox) | addRelativePose
(Navigation Toolbox) | optimizePoseGraph
(Navigation Toolbox) | show
(Navigation Toolbox) | extractFPFHFeatures
| pcmatchfeatures
| estimateGeometricTransform3D
| pcdownsample
| pctransform
| pcregistericp
| pcalign
| tform2trvec
(Navigation Toolbox) | tform2quat
(Navigation Toolbox)
lasFileReader
| pointCloud
| pcplayer
| occupancyMap3D
(Navigation Toolbox) | poseGraph3D
(Navigation Toolbox) | rigid3d
[1] Старр, Скотт. Tuscaloosa, AL: Seasonal Inundation Dynamics and Fertebrate Communities (неопр.) (недоступная ссылка). Национальный центр воздушного лазерного отображения, 1 декабря 2011 года. OpenTopography (https://doi.org/10.5069/G9SF2T3K).