Main Content

PointNet++ 딥러닝을 사용한 항공 라이다의 의미론적 분할

이 예제에서는 항공 라이다 데이터에 대해 의미론적 분할을 수행하도록 PointNet++ 딥러닝 신경망을 훈련시키는 방법을 보여줍니다.

공중 레이저 스캐닝 시스템에서 수집된 라이다 데이터는 지형도 작성, 도시 모델링, 바이오매스 함량 측정, 재난 관리와 같은 응용 분야에 사용됩니다. 이러한 데이터에서 의미 있는 정보를 추출하려면 포인트 클라우드의 각 점에 고유한 클래스 레이블을 할당하는 과정인 의미론적 분할이 필요합니다.

이 예제에서는 DALES(Dayton Annotated Lidar Earth Scan) 데이터셋을 사용하여 의미론적 분할을 수행하도록 PointNet++ 신경망을 훈련시킵니다[1]. 이 데이터셋에는 도시, 교외, 시골, 상업 환경에서 얻은 조밀하고 레이블이 지정된 항공 라이다 데이터의 장면이 포함되어 있습니다. 또한 이 데이터셋에는 건물, 자동차, 트럭, 전신주, 송전선, 울타리, 지면, 초목과 같은 8개 클래스에 대한 의미론적 분할 레이블을 제공합니다.

DALES 데이터 불러오기

DALES 데이터셋에는 항공 라이다 데이터의 40개 장면이 포함되어 있습니다. 40개 장면 중 29개 장면은 훈련에 사용되고 나머지 11개 장면은 테스트에 사용됩니다. 데이터의 각 픽셀에는 클래스 레이블이 있습니다. DALES 웹사이트의 지침에 따라 이 데이터셋을 dataFolder 변수로 지정된 폴더에 다운로드합니다. 훈련 데이터와 테스트 데이터를 저장할 폴더를 만듭니다.

dataFolder = fullfile(tempdir,'DALES');
trainDataFolder = fullfile(dataFolder,'dales_las','train');
testDataFolder = fullfile(dataFolder,'dales_las','test');

훈련 데이터의 포인트 클라우드를 미리 봅니다.

lasReader = lasFileReader(fullfile(trainDataFolder,'5080_54435.las'));
[pc,attr] = readPointCloud(lasReader,'Attributes','Classification');
labels = attr.Classification;

% Select only labeled data.
pc = select(pc,labels~=0);
labels = labels(labels~=0);
classNames = [
    "ground"
    "vegetation"
    "cars"
    "trucks"
    "powerlines"
    "fences"
    "poles"
    "buildings"
    ];
figure;
ax = pcshow(pc.Location,labels);
helperLabelColorbar(ax,classNames);
title("Point Cloud with Overlaid Semantic Labels");

데이터 전처리하기

DALES 데이터셋의 각 포인트 클라우드가 커버하는 면적은 500×500미터에 이르며, 이는 지상 라이다 포인트 클라우드가 커버하는 전형적인 면적보다 훨씬 넓습니다. 효율적인 메모리 처리를 위해, blockedPointCloud 객체를 사용하여 포인트 클라우드를 겹치지 않는 작은 블록으로 나눕니다.

blockSize 파라미터를 사용하여 블록 차원을 정의합니다. 데이터셋의 각 포인트 클라우드 크기가 다양하므로, z축을 따라 블록이 생성되지 않도록 블록의 z 차원을 Inf로 설정합니다.

blocksize = [51 51 Inf];

matlab.io.datastore.FileSet 객체를 만들어 훈련 데이터에서 모든 포인트 클라우드 파일을 수집합니다.

fs = matlab.io.datastore.FileSet(trainDataFolder);

Fileset 객체를 사용하여 blockedPointCloud 객체를 만듭니다.

bpc = blockedPointCloud(fs,blocksize);

참고: 처리하는 데 다소 시간이 걸릴 수 있습니다. 처리가 완료될 때까지 코드는 MATLAB® 실행을 일시 중단합니다.

이 예제에 지원 파일로 첨부된 helperCalculateClassWeights 헬퍼 함수를 사용하여 훈련 데이터셋의 모든 클래스에서 점 분포를 계산합니다.

numClasses = numel(classNames);
[weights,maxLabel,maxWeight] = helperCalculateClassWeights(fs,numClasses);

훈련에 사용할 datastore 객체 만들기

신경망을 훈련시키기 위해 블록 형식 포인트 클라우드 bpc를 사용하여 blockedPointCloudDatastore 객체를 만듭니다.

ldsTrain = blockedPointCloudDatastore(bpc);

1부터 클래스 개수까지 레이블 ID를 지정합니다.

labelIDs = 1 : numClasses;

포인트 클라우드를 미리 보고 표시합니다.

ptcld = preview(ldsTrain);
figure;
pcshow(ptcld.Location);
title("Cropped Point Cloud");

훈련 속도를 높이기 위해, 블록당 고정된 점 개수를 설정합니다.

numPoints = 8192;

이 예제의 끝부분에 정의된 helperTransformToTrainData 함수를 사용하여 신경망의 입력 계층과 호환되도록 데이터를 변환합니다. 다음 단계에 따라 변환을 적용합니다.

  • 포인트 클라우드와 각각의 레이블을 추출합니다.

  • 포인트 클라우드와 레이블을 지정된 수 numPoints로 다운샘플링합니다.

  • 포인트 클라우드를 범위 [0 1]로 정규화합니다.

  • 신경망의 입력 계층과 호환되도록 포인트 클라우드와 그에 대응하는 레이블을 변환합니다.

ldsTransformed = transform(ldsTrain,@(x,info) helperTransformToTrainData(x, ...
    numPoints,info,labelIDs,classNames),'IncludeInfo',true);
read(ldsTransformed)
ans=1×2 cell array
    {8192×3 double}    {8192×1 categorical}

PointNet++ 모델 정의하기

PointNet++는 비정렬 라이다 포인트 클라우드의 의미론적 분할에 사용되는 일반적인 신경망입니다. 의미론적 분할은 3차원 포인트 클라우드의 각 점을 자동차, 트럭, 지면 또는 초목과 같은 클래스 레이블과 연결합니다. 자세한 내용은 Get Started with PointNet++ 항목을 참조하십시오.

pointnetplusLayers 함수를 사용하여 PointNet++ 아키텍처를 정의합니다.

lgraph = pointnetplusLayers(numPoints,3,numClasses);

DALES 데이터셋의 클래스 불균형을 처리하기 위해 pixelClassificationLayer 함수의 가중 교차 엔트로피 손실이 사용됩니다. 따라서 더 낮은 가중치를 가진 클래스에 속한 점이 오분류된 경우 더 많은 벌점이 신경망에 적용됩니다.

% Replace the FocalLoss layer with pixelClassificationLayer.
larray = pixelClassificationLayer('Name','SegmentationLayer','ClassWeights', ...
    weights,'Classes',classNames);
lgraph = replaceLayer(lgraph,'FocalLoss',larray);

훈련 옵션 지정하기

Adam 최적화 알고리즘을 사용하여 신경망을 훈련시킵니다. trainingOptions (Deep Learning Toolbox) 함수를 사용하여 하이퍼파라미터를 지정합니다.

learningRate = 0.0005;
l2Regularization = 0.01;
numEpochs = 20;
miniBatchSize = 16;
learnRateDropFactor = 0.1;
learnRateDropPeriod = 10;
gradientDecayFactor = 0.9;
squaredGradientDecayFactor = 0.999;

options = trainingOptions('adam', ...
    'InitialLearnRate',learningRate, ...
    'L2Regularization',l2Regularization, ...
    'MaxEpochs',numEpochs, ...
    'MiniBatchSize',miniBatchSize, ...
    'LearnRateSchedule','piecewise', ...
    'LearnRateDropFactor',learnRateDropFactor, ...
    'LearnRateDropPeriod',learnRateDropPeriod, ...
    'GradientDecayFactor',gradientDecayFactor, ...
    'SquaredGradientDecayFactor',squaredGradientDecayFactor, ...
    'Plots','training-progress', ...
    'ExecutionEnvironment','gpu');

참고: 훈련 시 메모리 사용량을 제어하려면 miniBatchSize 값을 줄이십시오.

모델 훈련시키기

신경망을 훈련시키려면 doTraining 인수를 true로 설정합니다. 또는, 사전 훈련된 신경망을 불러옵니다. 신경망을 훈련시키기 위해 CPU 또는 GPU를 사용할 수 있습니다. GPU를 사용하려면 Parallel Computing Toolbox™와 CUDA® 지원 NVIDIA® GPU가 필요합니다. 자세한 내용은 GPU 연산 요구 사항 (Parallel Computing Toolbox) 항목을 참조하십시오.

doTraining = false;
if doTraining
    % Train the network on the ldsTransformed datastore using 
    % the trainNetwork function.
    [net,info] = trainNetwork(ldsTransformed,lgraph,options);
else
    % Load the pretrained network.
    load('pointnetplusTrained','net');
end

항공 포인트 클라우드 분할하기

테스트 포인트 클라우드에서 분할을 수행하려면, 먼저 blockedPointCloud 객체를 만든 다음 blockedPointCloudDatastore 객체를 만듭니다.

훈련 데이터에 사용된 유사한 변환을 테스트 데이터에 적용합니다.

  • 포인트 클라우드와 각각의 레이블을 추출합니다.

  • 포인트 클라우드를 지정된 수 numPoints로 다운샘플링합니다.

  • 포인트 클라우드를 범위 [0 1]로 정규화합니다.

  • 신경망의 입력 계층과 호환되도록 포인트 클라우드를 변환합니다.

tbpc = blockedPointCloud(fullfile(testDataFolder,'5080_54470.las'),blocksize);
tbpcds = blockedPointCloudDatastore(tbpc);

조밀한 포인트 클라우드의 각 점에 대해 다운샘플링된 포인트 클라우드에서 가장 가까운 점들을 찾아서 효과적으로 보간을 수행하도록 numNearestNeighborsradius를 정의합니다.

numNearestNeighbors = 20;
radius = 0.05;

예측을 위해 빈 자리 표시자를 초기화합니다.

labelsDensePred = [];

이 테스트 포인트 클라우드에 대해 추론을 수행하여 예측 레이블을 계산합니다. 조밀한 포인트 클라우드에 대한 예측 레이블을 얻기 위해 예측 레이블을 보간합니다. 겹치지 않는 블록 전체에 걸쳐 이 과정을 반복하고 pcsemanticseg 함수를 사용하여 레이블을 예측합니다.

while hasdata(tbpcds)

    % Read the block along with block information.
    ptCloudDense = read(tbpcds);

    % Use the helperDownsamplePoints function, attached to this example as a
    % supporting file, to extract a downsampled point cloud from the
    % dense point cloud.
    ptCloudSparse = helperDownsamplePoints(ptCloudDense{1},[],numPoints);
                       
    % Use the helperNormalizePointCloud function, attached to this example as
    % a supporting file, to normalize the point cloud between 0 and 1.
    ptCloudSparseNormalized = helperNormalizePointCloud(ptCloudSparse);
    ptCloudDenseNormalized = helperNormalizePointCloud(ptCloudDense{1});
    
    % Use the helperTransformToTestData function, defined at the end of this
    % example, to convert the point cloud to a cell array and to permute the
    % dimensions of the point cloud to make it compatible with the input layer
    % of the network.
    ptCloudSparseForPrediction = helperTransformToTestData(ptCloudSparseNormalized);
    
    % Get the output predictions.
    labelsSparsePred = pcsemanticseg(ptCloudSparseForPrediction{1,1}, ...
        net,'OutputType','uint8');
    
    % Use the helperInterpolate function, attached to this example as a
    % supporting file, to calculate labels for the dense point cloud,
    % using the sparse point cloud and labels predicted on the sparse point cloud.
    interpolatedLabels = helperInterpolate(ptCloudDenseNormalized, ...
        ptCloudSparseNormalized,labelsSparsePred,numNearestNeighbors, ...
        radius,maxLabel,numClasses);  
    
    % Concatenate the predicted labels from the blocks.
    labelsDensePred = vertcat(labelsDensePred,interpolatedLabels);
end
Starting parallel pool (parpool) using the 'local' profile ...
Connected to parallel pool with 6 workers.

보다 나은 시각화를 위해, 포인트 클라우드 데이터에서 유추된 블록만 표시합니다.

figure;
ax = pcshow(ptCloudDense{1}.Location,interpolatedLabels);
axis off;
helperLabelColorbar(ax,classNames);
title("Point Cloud Overlaid with Detected Semantic Labels");

신경망 평가하기

테스트 데이터에 대해 평가를 수행하려면 테스트 포인트 클라우드에서 레이블을 가져옵니다. 테스트 데이터에 대한 레이블은 이미 앞선 단계에서 예측되었습니다. 따라서 포인트 클라우드의 겹치지 않는 블록에 대해 반복하고 ground truth 레이블을 추출합니다.

대상 레이블에 대한 자리 표시자를 초기화합니다.

labelsDenseTarget = [];

블록 포인트 클라우드 데이터저장소에 대해 루프를 실행하고 ground truth 레이블을 가져옵니다.

reset(tbpcds);

while hasdata(tbpcds)    
    % Read the block along with block information.
    [~,infoDense] = read(tbpcds);

    % Extract the labels from the block information.
    labelsDense = infoDense.PointAttributes.Classification;
    
    % Concatenate the target labels from the blocks.
    labelsDenseTarget = vertcat(labelsDenseTarget,labelsDense);
end

evaluateSemanticSegmentation 함수를 사용하여 테스트 세트 결과에서 의미론적 분할 메트릭을 계산합니다. 예측 레이블과 대상 레이블은 앞서 계산되었으며 각각 labelsDensePred 변수와 labelsDenseTarget 변수에 저장되어 있습니다.

confusionMatrix = segmentationConfusionMatrix(double(labelsDensePred), ...
    double(labelsDenseTarget),'Classes',1:numClasses);
metrics = evaluateSemanticSegmentation({confusionMatrix},classNames,'Verbose',false);

IoU(Intersection-over-Union) 메트릭을 사용하여 클래스당 겹쳐 있는 양을 측정할 수 있습니다.

evaluateSemanticSegmentation 함수는 전체 데이터 세트에 대한 메트릭, 개별 클래스에 대한 메트릭, 각 테스트 영상에 대한 메트릭을 반환합니다. 데이터 세트 수준에서 메트릭을 확인하려면 metrics.DataSetMetrics 속성을 사용합니다.

metrics.DataSetMetrics
ans=1×4 table
    GlobalAccuracy    MeanAccuracy    MeanIoU    WeightedIoU
    ______________    ____________    _______    ___________

       0.93132          0.65574       0.53304      0.87909  

데이터 세트 메트릭은 신경망 성능에 대한 개요를 제공합니다. 각 클래스가 전반적인 성능에 미치는 영향을 확인하려면 metrics.ClassMetrics 속성을 사용하여 각 클래스에 대한 메트릭을 검사합니다.

metrics.ClassMetrics
ans=8×2 table
                  Accuracy      IoU   
                  ________    ________

    ground         0.99436     0.93451
    vegetation     0.86286     0.83069
    cars           0.71078     0.40554
    trucks        0.066978    0.050248
    powerlines     0.75077     0.68651
    fences         0.41416     0.24919
    poles          0.54785      0.2444
    buildings       0.8982     0.86324

전반적인 신경망 성능이 좋을지라도 Trucks와 같은 몇몇 클래스에 대한 클래스 메트릭은 성능 향상을 위해 더 많은 훈련 데이터가 필요함을 나타냅니다.

지원 함수

helperLabelColorbar 함수는 현재 축에 컬러바를 추가합니다. 클래스 이름을 색상과 함께 표시하도록 컬러바의 형식이 지정됩니다.

function helperLabelColorbar(ax,classNames)
% Colormap for the original classes.
cmap = [[0 0 255];
    [0 255 0];
    [255 192 203];
    [255 255 0];
    [255 0 255];
    [255 165 0];
    [139 0 150];
    [255 0 0]];
cmap = cmap./255;
cmap = cmap(1:numel(classNames),:);
colormap(ax,cmap);

% Add colorbar to current figure.
c = colorbar(ax);
c.Color = 'w';

% Center tick labels and use class names for tick marks.
numClasses = size(classNames,1);
c.Ticks = 1:1:numClasses;
c.TickLabels = classNames;

% Remove tick mark.
c.TickLength = 0;
end

helperTransformToTrainData 함수는 입력 데이터에 대해 다음과 같은 일련의 변환을 수행합니다.

  • 포인트 클라우드와 각각의 레이블을 추출합니다.

  • 포인트 클라우드와 레이블을 지정된 수 numPoints로 다운샘플링합니다.

  • 포인트 클라우드를 범위 [0 1]로 정규화합니다.

  • 신경망의 입력 계층과 호환되도록 포인트 클라우드와 그에 대응하는 레이블을 변환합니다.

function [cellout,dataout] = helperTransformToTrainData(data,numPoints,info,...
    labelIDs,classNames)
if ~iscell(data)
    data = {data};
end
numObservations = size(data,1);
cellout = cell(numObservations,2);
dataout = cell(numObservations,2);
for i = 1:numObservations 
    classification = info.PointAttributes(i).Classification;

    % Use the helperDownsamplePoints function, attached to this example as a
    % supporting file, to extract a downsampled point cloud and its labels
    % from the dense point cloud.
    [ptCloudOut,labelsOut] = helperDownsamplePoints(data{i,1}, ...
    classification,numPoints);

    % Make the spatial extent of the dense point cloud and the sparse point
    % cloud same.
    limits = [ptCloudOut.XLimits;ptCloudOut.YLimits;...
                    ptCloudOut.ZLimits];
    ptCloudSparseLocation = ptCloudOut.Location;
    ptCloudSparseLocation(1:2,:) = limits(:,1:2)';
    ptCloudSparseUpdated = pointCloud(ptCloudSparseLocation, ...
        'Intensity',ptCloudOut.Intensity, ...
        'Color',ptCloudOut.Color, ...
        'Normal',ptCloudOut.Normal);

    % Use the helperNormalizePointCloud function, attached to this example as
    % a supporting file, to normalize the point cloud between 0 and 1.    
    ptCloudOutSparse = helperNormalizePointCloud( ...
        ptCloudSparseUpdated);
    cellout{i,1} = ptCloudOutSparse.Location;

    % Permuted output.
    cellout{i,2} = permute(categorical(labelsOut,labelIDs,classNames),[1 3 2]);

    % General output.
    dataout{i,1} = ptCloudOutSparse;
    dataout{i,2} = labelsOut;
end
end

helperTransformToTestData 함수는 포인트 클라우드를 셀형 배열로 변환하고 신경망의 입력 계층과 호환되도록 포인트 클라우드의 차원을 치환합니다.

function data = helperTransformToTestData(data)
if ~iscell(data)
    data = {data};
end
numObservations = size(data,1);
for i = 1:numObservations
    tmp = data{i,1}.Location;
    data{i,1} = tmp;
end
end

참고 문헌

[1] Varney, Nina, Vijayan K. Asari, and Quinn Graehling. "DALES: A Large-Scale Aerial LiDAR dataset for Semantic Segmentation." ArXiv:2004.11985 [Cs, Stat], April 14, 2020. https://arxiv.org/abs/2004.11985.

[2] Qi, Charles R., Li Yi, Hao Su, and Leonidas J. Guibas. "PointNet++: Deep Hierarchical Feature Learning on Point Sets in a Metric Space." ArXiv:1706.02413 [Cs], June 7, 2017. https://arxiv.org/abs/1706.02413.