В этом примере показано, как реализовать алгоритм одновременной локализации и отображения (SLAM) для отображения по воздуху с использованием 3-D функций. Цель этого примера - оценить траекторию робота и создать карту облака точек его среды.
Алгоритм SLAM в этом примере оценивает траекторию, обнаруживая грубое выравнивание между принятыми сканированиями с пониженной выборкой, используя дескрипторы быстрой гистограммы признаков (FPFH), извлеченные в каждой точке, затем применяет итеративный алгоритм ближайшей точки (ICP) для точной настройки выравнивания. 3-D оптимизации графика позы из Navigation Toolbox™ уменьшает дрейф в расчетной траектории.
Создание синтетического лидарного сканирования на основе фрагментов воздушных данных, загруженных с веб-сайта OpenTopography [1]. Загрузите MAT-файл, содержащий последовательность ППМ по воздушным данным, определяющим траекторию робота. Вычислите lidar сканирования на основе данных в каждом ППМ с помощью 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

Укажите параметры для оценки траектории и замыкания контура. Настройте эти параметры для конкретного робота и среды.
Укажите количество сканирований lidar, пропускаемых между проверками, принятыми для регистрации. Поскольку последовательные сканирования имеют высокое перекрытие, пропуск нескольких кадров может улучшить скорость алгоритма без ущерба для точности.
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;
Укажите размер каждой ячейки фильтра сетки поля, используемого для создания карт путем выравнивания сканирований lidar.
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), чтобы хранить оценочные относительные позы между принятыми сканированиями lidar.
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 lidar с помощью алгоритма 3-D lidar 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(Панель инструментов навигации). В общем, поза первого узла в графе поз представляет начало координат. Чтобы сравнить расчетную траекторию с вехами истинности земли, укажите первый план истинности земли в качестве позы первого узла.
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 (Панель инструментов навигации) | rayIntersection (Навигационный комплект инструментов) | addRelativePose (навигационный комплект инструментов) | optimizePoseGraph (навигационный комплект инструментов) | show (навигационный комплект инструментов) | extractFPFHFeatures | pcmatchfeatures | estimateGeometricTransform3D | pcdownsample | pctransform | pcregistericp | pcalign | tform2trvec (навигационный комплект инструментов) |tform2quat(Панель инструментов навигации)
lasFileReader | pointCloud | pcplayer | occupancyMap3D (Панель инструментов навигации) | poseGraph3D(Панель инструментов навигации) | rigid3d
[1] Старр, Скотт. «Tuscaloosa, AL: сезонная динамика инундации и сообщества беспозвоночных». Национальный центр воздушного лазерного картирования, 1 декабря 2011 года. OpenTopography (https://doi.org/10.5069/G9SF2T3K).