В этом примере показано, как обрабатывать данные 3-D лидара от датчика, установленного на транспортном средстве, для постепенного построения карты и оценки траектории транспортного средства с использованием одновременной локализации и картирования (SLAM). Кроме 3-D лидарных данных, для помощи в построении карты используется также инерциальный навигационный датчик (ИНС). Карты, построенные таким образом, могут облегчить планирование пути для навигации транспортного средства или могут использоваться для локализации.
Строение Карты от примера Данных о Лидаре использует 3D данные о лидаре и чтения IMU, чтобы прогрессивно построить карту окружающей среды, пересеченной транспортным средством. Несмотря на то, что такой подход позволяет создать локальную согласованную карту, она подходит только для картирования небольших районов. Над более длинными последовательностями дрейф накапливается в значительную ошибку. Чтобы преодолеть это ограничение, этот пример распознает ранее посещенные места и пытается исправить накопленный дрейф с помощью подхода SLAM графика.
Данные, используемые в этом примере, являются частью набора данных Velodine SLAM и представляют собой почти 6 минут записанных данных. Загрузите данные во временный каталог.
Примечание: Загрузка может занять несколько минут.
baseDownloadURL = 'https://www.mrt.kit.edu/z/publ/download/velodyneslam/data/scenario1.zip'; dataFolder = fullfile(tempdir, 'kit_velodyneslam_data_scenario1', filesep); options = weboptions('Timeout', Inf); zipFileName = dataFolder + "scenario1.zip"; folderExists = exist(dataFolder, 'dir'); if ~folderExists % Create a folder in a temporary directory to save the downloaded zip % file. mkdir(dataFolder); disp('Downloading scenario1.zip (153 MB) ...') websave(zipFileName, baseDownloadURL, options); % Unzip downloaded file unzip(zipFileName, dataFolder); end
Используйте helperReadDataset для считывания данных из созданной папки в виде timetable. Точечные облака, захваченные лидаром, хранятся в виде файлов изображений PNG. Извлеките список имен файлов облаков точек из pointCloudTable переменная. Для считывания данных облака точек из файла изображения используйте helperReadPointCloudFromFile функция. Эта функция принимает имя файла изображения и возвращает pointCloud объект. Показания INS считываются непосредственно из файла конфигурации и сохраняются в insDataTable переменная.
datasetTable = helperReadDataset(dataFolder); pointCloudTable = datasetTable(:, 1); insDataTable = datasetTable(:, 2:end);
Прочтите первое облако точек и отобразите его в командной строке MATLAB ®
ptCloud = helperReadPointCloudFromFile(pointCloudTable.PointCloudFileName{1});
disp(ptCloud)
pointCloud with properties:
Location: [64×870×3 single]
Count: 55680
XLimits: [-78.4980 77.7062]
YLimits: [-76.8795 74.7502]
ZLimits: [-2.4839 2.6836]
Color: []
Normal: []
Intensity: []
Отображение первого считывания INS. timetable держится Heading, Pitch, Roll, X, Y, и Z информацию из INS.
disp(insDataTable(1, :))
Timestamps Heading Pitch Roll X Y Z
____________________ _______ ________ _________ _______ _______ ______
13-Jun-2010 06:27:31 1.9154 0.007438 -0.019888 -2889.1 -2183.9 116.47
Визуализация облаков точек с помощью pcplayer, отображение облака потоковой точки. Транспортное средство проходит путь, состоящий из двух петель. В первой петле транспортное средство совершает ряд поворотов и возвращается к начальной точке. Во второй петле транспортное средство совершает серию поворотов по другому маршруту и снова возвращается к начальной точке.
% Specify limits of the player xlimits = [-45 45]; % meters ylimits = [-45 45]; zlimits = [-10 20]; % Create a streaming point cloud display object lidarPlayer = pcplayer(xlimits, ylimits, zlimits); % Customize player axes labels xlabel(lidarPlayer.Axes, 'X (m)') ylabel(lidarPlayer.Axes, 'Y (m)') zlabel(lidarPlayer.Axes, 'Z (m)') title(lidarPlayer.Axes, 'Lidar Sensor Data') % Skip evey other frame since this is a long sequence skipFrames = 2; numFrames = height(pointCloudTable); for n = 1 : skipFrames : numFrames % Read a point cloud fileName = pointCloudTable.PointCloudFileName{n}; ptCloud = helperReadPointCloudFromFile(fileName); % Visualize point cloud view(lidarPlayer, ptCloud); pause(0.01) end

Во-первых, для построения карты используйте подход, описанный в примере построения карты из данных Lidar. Подход состоит из следующих этапов:
Выравнивание сканирований lidar: выравнивание последовательных сканирований lidar с использованием метода регистрации облака точек. В этом примере используется pcregisterndt для регистрации сканирований. Последовательно составляя эти преобразования, каждое облако точек преобразуется обратно в опорную рамку первого облака точек.
Объединение выровненных сканирований: создание карты путем объединения всех преобразованных облаков точек.
Этот подход инкрементного построения карты и оценки траектории транспортного средства называется одометрией.
Использовать pcviewset объект для хранения данных и управления ими в нескольких представлениях. Набор видов состоит из набора связанных видов.
В каждом представлении хранится информация, связанная с одним представлением. Эта информация включает абсолютную позу вида, данные датчика облака точек, полученные в этом виде, и уникальный идентификатор вида. Добавление видов в набор видов с помощью addView.
Чтобы установить соединение между представлениями, используйте addConnection. Соединение хранит информацию, такую как относительное преобразование между соединяющимися представлениями, неопределенность, связанная с вычислением этого измерения (представленного в виде информационной матрицы), и связанные идентификаторы представлений.
Используйте plot способ визуализации соединений, установленных набором видов. Эти соединения можно использовать для визуализации пути, пройденного транспортным средством.
hide(lidarPlayer) % Set random seed to ensure reproducibility rng(0); % Create an empty view set vSet = pcviewset; % Create a figure for view set display hFigBefore = figure('Name', 'View Set Display'); hAxBefore = axes(hFigBefore); % Initialize point cloud processing parameters downsamplePercent = 0.1; regGridSize = 3; % Initialize transformations absTform = rigid3d; % Absolute transformation to reference frame relTform = rigid3d; % Relative transformation between successive scans viewId = 1; skipFrames = 5; numFrames = height(pointCloudTable); displayRate = 100; % Update display every 100 frames for n = 1 : skipFrames : numFrames % Read point cloud fileName = pointCloudTable.PointCloudFileName{n}; ptCloudOrig = helperReadPointCloudFromFile(fileName); % Process point cloud % - Segment and remove ground plane % - Segment and remove ego vehicle ptCloud = helperProcessPointCloud(ptCloudOrig); % Downsample the processed point cloud ptCloud = pcdownsample(ptCloud, "random", downsamplePercent); firstFrame = (n==1); if firstFrame % Add first point cloud scan as a view to the view set vSet = addView(vSet, viewId, absTform, "PointCloud", ptCloudOrig); viewId = viewId + 1; ptCloudPrev = ptCloud; continue; end % Use INS to estimate an initial transformation for registration initTform = helperComputeInitialEstimateFromINS(relTform, ... insDataTable(n-skipFrames:n, :)); % Compute rigid transformation that registers current point cloud with % previous point cloud relTform = pcregisterndt(ptCloud, ptCloudPrev, regGridSize, ... "InitialTransform", initTform); % Update absolute transformation to reference frame (first point cloud) absTform = rigid3d( relTform.T * absTform.T ); % Add current point cloud scan as a view to the view set vSet = addView(vSet, viewId, absTform, "PointCloud", ptCloudOrig); % Add a connection from the previous view to the current view, representing % the relative transformation between them vSet = addConnection(vSet, viewId-1, viewId, relTform); viewId = viewId + 1; ptCloudPrev = ptCloud; initTform = relTform; if n>1 && mod(n, displayRate) == 1 plot(vSet, "Parent", hAxBefore); drawnow update end end

Объект набора представлений vSet, теперь содержит виды и связи. В таблице Представления (Views) vSet, AbsolutePose переменная задает абсолютную позу каждого вида относительно первого вида. В Connections таблица vSet, RelativePose переменная определяет относительные ограничения между связанными видами, InformationMatrix переменная определяет для каждого края неопределенность, связанную с соединением.
% Display the first few views and connections
head(vSet.Views)
head(vSet.Connections)
ans =
8×3 table
ViewId AbsolutePose PointCloud
______ _____________ ________________
1 [1×1 rigid3d] [1×1 pointCloud]
2 [1×1 rigid3d] [1×1 pointCloud]
3 [1×1 rigid3d] [1×1 pointCloud]
4 [1×1 rigid3d] [1×1 pointCloud]
5 [1×1 rigid3d] [1×1 pointCloud]
6 [1×1 rigid3d] [1×1 pointCloud]
7 [1×1 rigid3d] [1×1 pointCloud]
8 [1×1 rigid3d] [1×1 pointCloud]
ans =
8×4 table
ViewId1 ViewId2 RelativePose InformationMatrix
_______ _______ _____________ _________________
1 2 [1×1 rigid3d] {6×6 double}
2 3 [1×1 rigid3d] {6×6 double}
3 4 [1×1 rigid3d] {6×6 double}
4 5 [1×1 rigid3d] {6×6 double}
5 6 [1×1 rigid3d] {6×6 double}
6 7 [1×1 rigid3d] {6×6 double}
7 8 [1×1 rigid3d] {6×6 double}
8 9 [1×1 rigid3d] {6×6 double}
Теперь создайте карту облака точек, используя созданный набор видов. Выравнивание абсолютных положений вида по облакам точек в наборе видов с помощью pcalign. Укажите размер сетки для управления разрешением карты. Карта возвращается как pointCloud объект.
ptClouds = vSet.Views.PointCloud; absPoses = vSet.Views.AbsolutePose; mapGridSize = 0.2; ptCloudMap = pcalign(ptClouds, absPoses, mapGridSize);
Обратите внимание, что траектория, пройденная с использованием этого подхода, со временем дрейфует. В то время как путь по первому контуру обратно к начальной точке кажется разумным, второй контур значительно смещается от начальной точки. Накопленный дрейф приводит ко второму контуру, заканчивающемуся в нескольких метрах от начальной точки.
Карта, построенная с использованием только одометрии, неточна. Отображение построенной карты облака точек с пересекаемым контуром. Обратите внимание, что карта и пройденный путь для второго цикла не согласуются с первым циклом.
hold(hAxBefore, 'on'); pcshow(ptCloudMap); hold(hAxBefore, 'off'); close(hAxBefore.Parent)
Граф SLAM является широко используемым методом разрешения дрейфа в одометрии. Подход SLAM графика постепенно создает график, где узлы соответствуют позициям транспортного средства, а края представляют измерения датчиков, ограничивающие связанные позы. Такой граф называется графом позы. Граф позы содержит рёбра, которые кодируют противоречивую информацию, обусловленную шумом или неточностями в измерении. Затем узлы в построенном графе оптимизируются для нахождения набора положений транспортного средства, которые оптимально объясняют измерения. Этот метод называется оптимизацией графов позы.
Чтобы создать график позы из набора видов, можно использовать createPoseGraph функция. Эта функция создает узел для каждого вида и ребро для каждого соединения в наборе видов. Чтобы оптимизировать график позы, можно использовать optimizePoseGraph(Панель инструментов навигации).
Ключевым аспектом, способствующим эффективности SLAM графа в коррекции дрейфа, является точное обнаружение петель, то есть мест, которые были ранее посещены. Это называется обнаружением замыкания контура или распознаванием места. Добавление рёбер к графу позы, соответствующему замыканиям цикла, обеспечивает противоречивое измерение для позы связного узла, которое можно разрешить во время оптимизации графа позы.
Замыкания петель могут быть обнаружены с помощью дескрипторов, характеризующих локальную среду, видимую для датчика Лидара. Дескриптор контекста сканирования [1] является таким дескриптором, который можно вычислить из облака точек с помощью scanContextDescriptor функция. В этом примере используется helperLoopClosureDetector класс для обнаружения замыканий петель. Этот класс использует дескрипторы контекста сканирования для описания отдельных облаков точек и двухфазный алгоритм поиска дескрипторов.
Вычисление дескриптора: Дескриптор контекста сканирования и ключ кольца извлекаются из каждого облака точек с помощью scanContextDescriptor функция.
Согласование дескрипторов: На первом этапе ключ кольца используется для поиска потенциальных кандидатов цикла. На второй фазе выполняется поиск ближайшего соседа с использованием дескрипторов признаков контекста сканирования среди потенциальных кандидатов цикла. scanContextDistance используется для вычисления расстояния между двумя дескрипторами контекста сканирования. См. раздел helperFeatureSearcher для получения дополнительных сведений.
% Set random seed to ensure reproducibility rng(0); % Create an empty view set vSet = pcviewset; % Create a loop closure detector matchThresh = 0.08; loopDetector = helperLoopClosureDetector('MatchThreshold', matchThresh); % Create a figure for view set display hFigBefore = figure('Name', 'View Set Display'); hAxBefore = axes(hFigBefore); % Initialize transformations absTform = rigid3d; % Absolute transformation to reference frame relTform = rigid3d; % Relative transformation between successive scans maxTolerableRMSE = 3; % Maximum allowed RMSE for a loop closure candidate to be accepted viewId = 1; for n = 1 : skipFrames : numFrames % Read point cloud fileName = pointCloudTable.PointCloudFileName{n}; ptCloudOrig = helperReadPointCloudFromFile(fileName); % Process point cloud % - Segment and remove ground plane % - Segment and remove ego vehicle ptCloud = helperProcessPointCloud(ptCloudOrig); % Downsample the processed point cloud ptCloud = pcdownsample(ptCloud, "random", downsamplePercent); firstFrame = (n==1); if firstFrame % Add first point cloud scan as a view to the view set vSet = addView(vSet, viewId, absTform, "PointCloud", ptCloudOrig); viewId = viewId + 1; ptCloudPrev = ptCloud; continue; end % Use INS to estimate an initial transformation for registration initTform = helperComputeInitialEstimateFromINS(relTform, ... insDataTable(n-skipFrames:n, :)); % Compute rigid transformation that registers current point cloud with % previous point cloud relTform = pcregisterndt(ptCloud, ptCloudPrev, regGridSize, ... "InitialTransform", initTform); % Update absolute transformation to reference frame (first point cloud) absTform = rigid3d( relTform.T * absTform.T ); % Add current point cloud scan as a view to the view set vSet = addView(vSet, viewId, absTform, "PointCloud", ptCloudOrig); % Add a connection from the previous view to the current view representing % the relative transformation between them vSet = addConnection(vSet, viewId-1, viewId, relTform); % Detect loop closure candidates [loopFound, loopViewId] = detectLoop(loopDetector, ptCloudOrig); % A loop candidate was found if loopFound loopViewId = loopViewId(1); % Retrieve point cloud from view set ptCloudOrig = vSet.Views.PointCloud( find(vSet.Views.ViewId == loopViewId, 1) ); % Process point cloud ptCloudOld = helperProcessPointCloud(ptCloudOrig); % Downsample point cloud ptCloudOld = pcdownsample(ptCloudOld, "random", downsamplePercent); % Use registration to estimate the relative pose [relTform, ~, rmse] = pcregisterndt(ptCloud, ptCloudOld, ... regGridSize, "MaxIterations", 50); acceptLoopClosure = rmse <= maxTolerableRMSE; if acceptLoopClosure % For simplicity, use a constant, small information matrix for % loop closure edges infoMat = 0.01 * eye(6); % Add a connection corresponding to a loop closure vSet = addConnection(vSet, loopViewId, viewId, relTform, infoMat); end end viewId = viewId + 1; ptCloudPrev = ptCloud; initTform = relTform; if n>1 && mod(n, displayRate) == 1 hG = plot(vSet, "Parent", hAxBefore); drawnow update end end

Создайте график позы из набора видов с помощью createPoseGraph способ. График позы является digraph объект с:
Узлы, содержащие абсолютную позу каждого вида
Кромки, содержащие относительные зависимости позы каждого соединения
G = createPoseGraph(vSet); disp(G)
digraph with properties:
Edges: [507×3 table]
Nodes: [503×2 table]
В дополнение к соединениям одометрии между последовательными видами набор видов теперь включает соединения замыкания контура. Например, обратите внимание на новые соединения между прохождением второго цикла и прохождением первого цикла. Это соединения замыкания петель. Они могут быть идентифицированы как рёбра в графе, конечные узлы которого не являются последовательными.
% Update axes limits to focus on loop closure connections xlim(hAxBefore, [-50 50]); ylim(hAxBefore, [-100 50]); % Find and highlight loop closure connections loopEdgeIds = find(abs(diff(G.Edges.EndNodes, 1, 2)) > 1); highlight(hG, 'Edges', loopEdgeIds, 'EdgeColor', 'red', 'LineWidth', 3)

Оптимизация графика позы с помощью optimizePoseGraph.
optimG = optimizePoseGraph(G, 'g2o-levenberg-marquardt');
vSetOptim = updateView(vSet, optimG.Nodes);
Отображение набора представлений с оптимизированными позициями. Обратите внимание, что обнаруженные контуры теперь объединены, что приводит к более точной траектории.
plot(vSetOptim, 'Parent', hAxBefore)

Абсолютные позы в оптимизированном наборе видов теперь можно использовать для построения более точной карты. Используйте pcalign функция, позволяющая выровнять облака уставок вида с абсолютными позициями оптимизированного вида в единую карту облаков точек. Укажите размер сетки для управления разрешением созданной карты облака точек.
mapGridSize = 0.2; ptClouds = vSetOptim.Views.PointCloud; absPoses = vSetOptim.Views.AbsolutePose; ptCloudMap = pcalign(ptClouds, absPoses, mapGridSize); hFigAfter = figure('Name', 'View Set Display (after optimization)'); hAxAfter = axes(hFigAfter); pcshow(ptCloudMap, 'Parent', hAxAfter); % Overlay view set display hold on plot(vSetOptim, 'Parent', hAxAfter); helperMakeFigurePublishFriendly(hFigAfter);

Хотя точность еще может быть улучшена, эта карта облака точек является значительно более точной.
Г. Ким и А. Ким, «Scan Context: Egocentric Spatial Descriptor for Place Recognition Within 3D Point Cloud Map», Международная конференция IEEE/RSJ 2018 по интеллектуальным роботам и системам (IROS), Мадрид, 2018, стр. 4802-4809.
helperReadDataset считывает данные из указанной папки в расписание.
function datasetTable = helperReadDataset(dataFolder) %helperReadDataset Read Velodyne SLAM Dataset data into a timetable % datasetTable = helperReadDataset(dataFolder) reads data from the % folder specified in dataFolder into a timetable. The function % expects data from the Velodyne SLAM Dataset. % % See also fileDatastore, helperReadINSConfigFile. % Point clouds are stored as PNG files in the scenario1 folder pointCloudFilePattern = fullfile(dataFolder, 'scenario1', 'scan*.png'); % Create a file datastore to read in files in the right order fileDS = fileDatastore(pointCloudFilePattern, 'ReadFcn', ... @helperReadPointCloudFromFile); % Extract the file list from the datastore pointCloudFiles = fileDS.Files; imuConfigFile = fullfile(dataFolder, 'scenario1', 'imu.cfg'); insDataTable = helperReadINSConfigFile(imuConfigFile); % Delete the bad row from the INS config file insDataTable(1447, :) = []; % Remove columns that will not be used datasetTable = removevars(insDataTable, ... {'Num_Satellites', 'Latitude', 'Longitude', 'Altitude', 'Omega_Heading', ... 'Omega_Pitch', 'Omega_Roll', 'V_X', 'V_Y', 'V_ZDown'}); datasetTable = addvars(datasetTable, pointCloudFiles, 'Before', 1, ... 'NewVariableNames', "PointCloudFileName"); end
helperProcessPointCloud обрабатывает облако точек путем удаления точек, принадлежащих плоскости земли и эго-транспортному средству.
function ptCloud = helperProcessPointCloud(ptCloudIn, method) %helperProcessPointCloud Process pointCloud to remove ground and ego vehicle % ptCloud = helperProcessPointCloud(ptCloudIn, method) processes % ptCloudIn by removing the ground plane and the ego vehicle. % method can be "planefit" or "rangefloodfill". % % See also pcfitplane, pointCloud/findNeighborsInRadius. arguments ptCloudIn (1,1) pointCloud method string {mustBeMember(method, ["planefit","rangefloodfill"])} = "rangefloodfill" end isOrganized = ~ismatrix(ptCloudIn.Location); if (method=="rangefloodfill" && isOrganized) % Segment ground using floodfill on range image groundFixedIdx = segmentGroundFromLidarData(ptCloudIn, ... "ElevationAngleDelta", 11); else % Segment ground as the dominant plane with reference normal % vector pointing in positive z-direction maxDistance = 0.4; maxAngularDistance = 5; referenceVector = [0 0 1]; [~, groundFixedIdx] = pcfitplane(ptCloudIn, maxDistance, ... referenceVector, maxAngularDistance); end if isOrganized groundFixed = false(size(ptCloudIn.Location,1),size(ptCloudIn.Location,2)); else groundFixed = false(ptCloudIn.Count, 1); end groundFixed(groundFixedIdx) = true; % Segment ego vehicle as points within a given radius of sensor sensorLocation = [0 0 0]; radius = 3.5; egoFixedIdx = findNeighborsInRadius(ptCloudIn, sensorLocation, radius); if isOrganized egoFixed = false(size(ptCloudIn.Location,1),size(ptCloudIn.Location,2)); else egoFixed = false(ptCloudIn.Count, 1); end egoFixed(egoFixedIdx) = true; % Retain subset of point cloud without ground and ego vehicle if isOrganized indices = ~groundFixed & ~egoFixed; else indices = find(~groundFixed & ~egoFixed); end ptCloud = select(ptCloudIn, indices); end
helperComputeInitialEstimateFromINS оценивает первоначальное преобразование для регистрации из показаний INS.
function initTform = helperComputeInitialEstimateFromINS(initTform, insData) % If no INS readings are available, return if isempty(insData) return; end % The INS readings are provided with X pointing to the front, Y to the left % and Z up. Translation below accounts for transformation into the lidar % frame. insToLidarOffset = [0 -0.79 -1.73]; % See DATAFORMAT.txt Tnow = [-insData.Y(end), insData.X(end), insData.Z(end)].' + insToLidarOffset'; Tbef = [-insData.Y(1) , insData.X(1) , insData.Z(1)].' + insToLidarOffset'; % Since the vehicle is expected to move along the ground, changes in roll % and pitch are minimal. Ignore changes in roll and pitch, use heading only. Rnow = rotmat(quaternion([insData.Heading(end) 0 0], 'euler', 'ZYX', 'point'), 'point'); Rbef = rotmat(quaternion([insData.Heading(1) 0 0], 'euler', 'ZYX', 'point'), 'point'); T = [Rbef Tbef;0 0 0 1] \ [Rnow Tnow;0 0 0 1]; initTform = rigid3d(T.'); end
helperMakeFigurePublishFriendly корректирует рисунки таким образом, чтобы снимок экрана, снятый при публикации, был правильным.
function helperMakeFigurePublishFriendly(hFig) if ~isempty(hFig) && isvalid(hFig) hFig.HandleVisibility = 'callback'; end end
helperFeatureSearcher создает объект, который можно использовать для поиска ближайших совпадений элементов.
helperLoopClosureDetector создает объект, который можно использовать для обнаружения замыканий цикла с помощью дескрипторов элементов контекста сканирования.