Main Content

3 次元 LiDAR 点群を使用した SLAM の実行

この例では、点群処理アルゴリズムと姿勢グラフの最適化を使用して、収集された 3 次元 LiDAR センサー データに "自己位置推定と環境地図作成の同時実行" (SLAM) アルゴリズムを実装する方法を説明します。この例の目標は、ロボットの軌跡を推定し、3 次元 LiDAR 点群と推定された軌跡から環境の 3 次元占有マップを作成することです。

ここで示す SLAM アルゴリズムでは、正規分布変換 (NDT) に基づく点群レジストレーション アルゴリズムを使用して軌跡を推定し、ロボットが同じ場所を通るたびに信頼領域ソルバーを使用した SE3 姿勢グラフの最適化を使用してドリフトを減らします。

データの読み込みと調整可能なパラメーターの設定

ある駐車場で Clearpath™ Husky ロボットから収集した 3 次元 LiDAR データを読み込みます。この LiDAR データには、"n" 行 3 列の行列の cell 配列が格納されています。ここで、"n" は取得された LiDAR データ内の 3 次元の点の数で、列は取得された各点に関連付けられている "xyz" 座標を表します。

load pClouds.mat

点群レジストレーション アルゴリズムのパラメーター

点群レジストレーション アルゴリズムを使用した軌跡の推定のパラメーターを指定します。maxLidarRange で 3 次元レーザー スキャナーの最大範囲を指定します。

maxLidarRange = 20;

屋内環境で取得された点群データには床面と天井面の点が含まれていて、点群レジストレーション アルゴリズムで混同されます。次のパラメーターにより、一部の点が点群から削除されます。

  • referenceVector - 床面の法線方向。

  • maxDistance - 床面と天井面を削除するときのインライアの最大距離。

  • maxAngularDistance - 床面と天井面を当てはめるときの基準ベクトルからの最大角度偏差。

referenceVector = [0 0 1];
maxDistance = 0.5;
maxAngularDistance = 15;

レジストレーション アルゴリズムの効率と精度を高めるために、randomSampleRatio で指定したサンプル比の無作為抽出を使用して点群がダウンサンプリングされます。

randomSampleRatio = 0.25;

NDT レジストレーション アルゴリズムで使用されるボクセルのグリッド サイズを gridStep で指定します。スキャンは、ロボットの移動距離が distanceMovedThreshold を超えた後にのみ受け入れられます。

gridStep = 2.5;
distanceMovedThreshold = 0.3;

これらのパラメーターを、実際のロボットと環境に合わせて調整します。

ループ閉じ込み推定アルゴリズムのパラメーター

ループ閉じ込み推定アルゴリズムのパラメーターを指定します。ループ閉じ込みの探索は、現在のロボットの位置を中心とする loopClosureSearchRadius で指定した半径の中でのみ行われます。

loopClosureSearchRadius = 3;

ループ閉じ込みアルゴリズムは、2 次元のサブマップとスキャン マッチングに基づきます。サブマップは、スキャンが nScansPerSubmap 個 (サブマップあたりのスキャン数) 受け入れられるたびに作成されます。ループ閉じ込みアルゴリズムでループ候補を探す際、最新の subMapThresh 個のスキャンが無視されます。

nScansPerSubmap = 3;
subMapThresh = 50;

annularRegionLimits で指定した "z" 軸範囲の環状領域が点群から抽出されます。この範囲から外れる床と天井の点は、点群の平面当てはめアルゴリズムで関心領域が特定された後に削除されます。

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);

処理された点群、LiDAR スキャン、およびサブマップの変数を事前に割り当てます。マップをすばやく可視化するために、ダウンサンプリングした点群のセットを作成します。

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;

無作為抽出の整合性を確保するために乱数シードを設定します。

rng(0);

必要に応じて Figure ウィンドウを初期化します。

% 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

Figure contains an axes object. The axes object is empty.

軌跡の推定と姿勢グラフの最適化を使用した調整

ロボットの軌跡は、ロボットの姿勢 (3 次元空間における位置と向き) の集合です。ロボットの姿勢は、3 次元 LiDAR スキャンを取得する各インスタンスで 3 次元 LiDAR SLAM アルゴリズムを使用して推定されます。3 次元 LiDAR 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
    

ループ閉じ込みのクエリ

ループ閉じ込みのクエリでは、現在のロボットの位置を前に通ったことがあるかどうかを調べます。この探索は、現在のロボットの位置を中心とする loopClosureSearchRadius で指定された小さい半径の中で、現在のスキャンを過去のスキャンと照合することにより行われます。過去のすべてのスキャンと照合して探索するには時間がかかるため、LiDAR オドメトリはドリフトが少ないことから、小さい半径の中を探索すれば十分です。ループ閉じ込みのクエリは次の手順で構成されます。

  • 連続する nScansPerSubmap 個のスキャンからサブマップを作成します。

  • 現在のスキャンを loopClosureSearchRadius の範囲内でサブマップと照合します。

  • 一致スコアが loopClosureThreshold を超えたら一致を受け入れます。受け入れられたサブマップを表すすべてのスキャンが可能性のあるループ候補と見なされます。

  • 可能性のあるループ候補と現在のスキャンの間の相対姿勢を推定します。相対姿勢は、RMSE が rmseThreshold 未満の場合のみ、ループ閉じ込みの拘束として受け入れられます。

    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.

Figure contains an axes object. The axes object contains 4 objects of type patch, line.

3 次元占有マップの作成と可視化

推定されたグローバルな姿勢を使用して 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