2023년 7월 11일 화요일

엔트와인 EPT 포인트 클라우드 타일 데이터 포맷 분석

EPT(Entwine Point Tile)는 포인트 클라우드 데이터를 위한 간단하고 유연한 옥트리 기반 저장 형식이다. 이 글은 포인트 클라우드 타일 데이터 포맷(EPT)을 분석한다.

EPT 이용한 포인트 클라우드 처리 예시

구조
EPT는 JSON 메타데이터와 바이너리 PCD(Point Cloud Data)를 포함한 파일 형식이다. 구조는 다음과 같다. 
├── ept.json
├── ept-data
│   └── 0-0-0-0.laz
├── ept-hierarchy
│   └── 0-0-0-0.json
└── ept-sources
    ├── list.json
    └── 0.json

ept.json은 메타데이터를 정의한다. 구성요소는 다음과 같다.

bounds는 옥트리의 경계를 표현한다. 
boundsConforming은 데이터의 경계를 표현하는 최소 영역을 정의한다.
dataType은 PCD파일 형식을 지정한다.
points는 포인트 개수를 의미한다.
schema는 포인트의 형식을 정의한다. x, y, z, 반사강도와 같은 데이터 유형을 정의한다. 
span은 옥트리의 각 x, y, z차원의 복셀 큐브 갯수를 정의한다. 이 값이 높을수록, 좀 더 정밀한 옥트리를 표현하지만, 사용 용량은 커진다. 
srs는 좌표계를 정의한다. 

다음은 메타데이터 예시이다. 
{
    "bounds": [634962.0, 848881.0, -1818.0, 639620.0, 853539.0, 2840.0],
    "boundsConforming": [635577.0, 848882.0, 406.0, 639004.0, 853538.0, 616.0],
    "dataType": "laszip",
    "hierarchyType": "json",
    "points": 10653336,
    "schema": [
        { "name": "X", "type": "signed", "size": 4, "scale": 0.01, "offset": 637291.0 },
        { "name": "Y", "type": "signed", "size": 4, "scale": 0.01, "offset": 851210.0 },
        { "name": "Z", "type": "signed", "size": 4, "scale": 0.01, "offset": 511.0 },
        { "name": "Intensity", "type": "unsigned", "size": 2 },
        { "name": "ReturnNumber", "type": "unsigned", "size": 1 },
        { "name": "NumberOfReturns", "type": "unsigned", "size": 1 },
        { "name": "ScanDirectionFlag", "type": "unsigned", "size": 1 },
        { "name": "EdgeOfFlightLine", "type": "unsigned", "size": 1 },
        { "name": "Classification", "type": "unsigned", "size": 1 },
        { "name": "ScanAngleRank", "type": "float", "size": 4 },
        { "name": "UserData", "type": "unsigned", "size": 1 },
        { "name": "PointSourceId", "type": "unsigned", "size": 2 },
        { "name": "GpsTime", "type": "float", "size": 8 },
        { "name": "Red", "type": "unsigned", "size": 2 },
        { "name": "Green", "type": "unsigned", "size": 2 },
        { "name": "Blue", "type": "unsigned", "size": 2 },
        { "name": "OriginId", "type": "unsigned", "size": 4 }
    ],
    "span" : 256,
    "srs": {
        "authority": "EPSG",
        "horizontal": "3857",
        "vertical": "5703",
        "wkt": "PROJCS[\"WGS 84 ... AUTHORITY[\"EPSG\",\"3857\"]]"
    },
    "version" : "1.1.0"
}

ept파일의 계층은 파일이름 형식에 표현된다. 
D-X-Y-Z 파일 이름 형식은 옥트리의 특정 큐브를 의미한다. 예를 들어, 0-0-0-0.json 파일 내용은 다음과 같다. 여기서, 2-0-1-0 옥트리 큐브는 322 포인트들이 포함된다.

{
    "0-0-0-0": 65341,
        "1-0-0-0": 438,
            "2-0-1-0": 322,
        "1-0-0-1": 56209,
            "2-0-1-2": 4332,
            "2-1-1-2": 20300,
            "2-1-1-3": 64020,
                "3-2-3-6": 32004,
                    "4-4-6-12": -1,
                    "4-5-6-13": -1,
                "3-3-3-7": 542,
        "1-0-1-0": 30390,
            "2-1-2-0": 2300,
        "1-1-1-1": 2303
}

포인트갯수 값이 -1일 경우, 별도의 해당 json파일 안에 옥트리 큐브 점 갯수가 정의되어 있다는 것을 의미한다 (예. 4-4-6-12.json).

PCD 파일 소스는 ept-sources/manifest.json 파일에 다음과 같이 저장된다. 
[
    {
        "path": "autzen-low.laz",
        "bounds": [635577.0, 848882.0, 406.0, 639004.0, 853538.0, 511.0],
        "inserted": true,
        "points": 6000 ,
        "metadataPath": "autzen-low.json"
    },
    {
        "path": "autzen-high.laz",
        "bounds": [635577.0, 848882.0, 511.0, 639004.0, 853538.0, 616.0],
        "inserted": true,
        "points": 4000,
        "metadataPath": "autzen-high.json"
    }
]

ept-sources 에 지정된 PCD 소스 파일 경로는 URL이 될 수 있다.

마무리
EPT 파일을 이용하면, 대용량 PCD를 표현하고 서비스하기 좋다. 확장성있는 구조를 가지고 있어, 다양한 스캔 분야에 활용할 수 있다.

레퍼런스

QGIS 포인트 클라우드 사용 방법

이 글은 QGIS에 추가된 최신 기능 중 하나인 포인트 클라우드 사용 방법을 간단히 정리한다.

점군 가져오기
포인트 클라우드는 포인트 집합이다. 각 포인트에는 x, y 및 z 좌표가 있다. 캡처 방법에 따라 포인트 클라우드에는 추가 속성도 포함된다. 

QGIS는 Entwine Point Tile (EPT) 및 LAS/LAZ 데이터 포맷을 지원한다. 포인트 클라우드로 작업하면, QGIS는 항상 EPT로 데이터를 저장한다. EPT는 공간 인덱싱을 사용한다. 데이터가 LAS 또는 LAZ 형식이라면, QGIS는 EPT 형식으로 변환할 것이다.

QGIS 에서 왼쪽 상단의 DATA SOURCE TOOLBAT 메뉴를 선택한다. 그리고, LAS파일을 다음 그림과 같이 선택한다.  그럼, 점군을 로딩해, QGIS에서 가시화 할 것이다.  

QGIS에서는 포인트 클라우드를 편집할 수 없다. 이 경우 포인트 클라우드, CloudCompare를 사용할 수 있다. 

포인트 클라우드 레이어 속성 확인 및 가시화
포인트 클라우드 레이어에 대한 Layer Properties 대화 상자를 다음처럼 제공한다. 


속성창은 메타데이터, 소스 원본 경로, 좌표계, 포인트 특징값 리스트 등을 표시한다. 

뷰의 3D VIEW 메뉴를 이용하면, 다음과 같이, 3D 보기가 가능하다.

각 속성 및 특징 값을 이용해, 다음과 같이 컬러 스키마를 지정해 가시화하는 방법을 설정할 수 있다. 이는 분석할 때 매우 유용하다.


레퍼런스

파이썬 기반 포인트 클라우드 특징 계산 방법 소개

이 글은 파이썬 기반 포인트 클라우드 특징 계산 방법을 소개한다. 점군은 포인트 간의 관계를 통해, 수직도, 수평도, 법선, 곡률 등 다양한 특징을 수학적으로 계산할 수 있다. 직접 코딩해도 되지만, jakteristics 라이브러리를 이용하면, 쉽게 이 값들을 얻을 수 있다. 이 라이브러리는 다음과 같은 point cloud data 특징을 계산해 준다.
  • Eigenvalue sum
  • Omnivariance
  • Eigenentropy
  • Anisotropy
  • Planarity
  • Linearity
  • PCA1
  • PCA2
  • Surface Variation
  • Sphericity
  • Verticality
  • Nx, Ny, Nz (The normal vector)

점군 특징 중 하나인 법선벡터

설치 
설치 및 테스트는 다음과 같다.
 
    python -m pip install -r requirements-dev.txt
    python setup.py pytest

사용방법
사용법은 다음과 같다. 
jakteristics input/las/file.las output/file.las --search-radius 0.15 --num-threads 4

코딩방법
파이썬을 이용해, 다음과 같이 얻고자 하는 특징을 옵션으로 설정한 후, 함수를 호출해 주면 된다. 
from jakteristics import FEATURE_NAMES, extension, las_utils, utils
def compute_features(input_path, output_path):
'''
FEATURE_NAMES = [
"eigenvalue_sum",
"omnivariance",
"eigenentropy",
"anisotropy",
"planarity",
"linearity",
"PCA1",
"PCA2",
"surface_variation",
"sphericity",
"verticality",
"nx",
"ny",
"nz",
"number_of_neighbors",
"eigenvalue1",
"eigenvalue2",
"eigenvalue3",
"eigenvector1x",
"eigenvector1y",
"eigenvector1z",
"eigenvector2x",
"eigenvector2y",
"eigenvector2z",
"eigenvector3x",
"eigenvector3y",
"eigenvector3z",
]
'''
xyz = las_utils.read_las_xyz(input_path)
features = extension.compute_features(xyz, 0.1, feature_names=FEATURE_NAMES)
print(features)
las_utils.write_with_extra_dims(input_path, output_path, features, FEATURE_NAMES)

이 함수를 이용해, las 파일을 이용해 특징을 계산해 본다. 다음과 같이 호출하면 된다. 
compute_features('input.las', 'output.las') 

클라우드 컴페어를 통해 확인한 결과는 다음과 같다.
출력파일 수직도 확인 결과(청색 - 적색 스키마)

해당 도구에서 scalar 항목을 변경해 보면, 앞서 언급된 점군 특징이 모두 계산되어 있는 것을 확인할 수 있다.

레퍼런스

딥러닝 하드웨어 확장성 고려한 PyTorch Lightning 기반 그래프 딥러닝 지원 PyTorch Geometric 사용방법 소개

이 글은 딥러닝 하드웨어 확장성을 고려한 PyTorch Lightning 기반 그래프 딥러닝 지원 PyTorch Geometric 사용방법을 소개한다.
그래프 데이터셋 예시

PyTorch Lightning 
최신 딥러닝 하드웨어를 활용하려면, 하드웨어 지원 라이브러리를 사용해 딥러닝 모델을 개발해야 한다. 

PyTorch Lightning은 TPU와 같은 새로운 하드웨어에서 실행되도록, 모델을 간단하게 확장할 수 있을 뿐만 아니라, 일반적인 CPU와 GPU 간 전환 프로세스를 단순화할 수 있다. 아울러, 분산 학습도 쉽게 만든다.

PyTorch Lightning 기반 그래프 신경망 벤치마크
PyTorch Geometric을 사용하여 모델을 구축하고, 내장된 데이터 세트인 "PROTEINS"을 이용해 학습해 본다.

이 데이터세트 에는 Ivanov 등 논문에 설명된 데이터 세트 정리된 버전이 포함되어 있다 . 이 데이터세트 이외에 PyTorch Geometric의 데이터 세트 모듈의 일부로 사용할 수 있다. MNIST 의 그래프 버전을 분류 하거나 선택한 그래프 분류 문제로 아래 코드에 사용된 데이터 세트를 대체할 수 있다.

그래프 딥 러닝 라이브러리인 PyTorch Geometric은 에지(인접 행렬), 노드 특성, 에지 특성 및 그래프 인덱스를 나타내는 단일 매트릭스 세트로 묶어진다. 

PyTorch Lightning 및 PyTorch Geometric 가상 환경 설정
PyTorch Geometric을 Lightning과 함께 사용하고 프로젝트를 자체 가상 환경에 보관할 것이다.

virtualenv venv
source venv/bin/activate
pip install pytorch-lightning
pip install torch-scatter 
pip install torch-sparse 
pip install torch-cluster  
pip install  torch-spline-conv  
pip install torch-geometric  


다음으로 프로젝트 구축을 할 것이다. 이 프로젝트는 데이터셋 가져오기, 모델 정의, 교육 루프 정의, 데이터 세트 및 데이터 로더를 인스턴스화하고, 학습 루프를 호출하기 위한 루틴으로 구성된다.

라이브러리 임포트
그래프 분류를 위한 모듈을 작성할 때 가장 먼저 해야 할 일은 라이브러리 임포트이다. 다음과 같이 코딩한다.

import time
import numpy as np
import torch
from torch.nn import Dropout, Linear, ReLU
import torch_geometric
from torch_geometric.datasets import TUDataset, GNNBenchmarkDataset
from torch_geometric.loader import DataLoader
from torch_geometric.nn import GCNConv, Sequential, global_mean_pool
# this import is only used in the plain PyTorch+Geometric version
from torch.utils.tensorboard import SummaryWriter
# these imports are only used in the Lighning version
import pytorch_lightning as pl
import torch.nn.functional as F

모델 구축
PyTorch Geometric를 이용해 그래프 컨벌루션 네트워크를 구축한다. PlainGCN모델은 nn.Module 클래스에서 상속하고, PyTorch Geometric의 모델 클래스를 사용하여 순방향 학습 경로를 정의한다.

성능 비교를 위해, 파이토치 버전과 파이토치 라이트닝 버전 두개 클래스 소스를 준비한다. 
class PlainGCN(torch.nn.Module):
    def __init__(self, **kwargs):
        super(PlainGCN, self).__init__()
        self.num_features = kwargs["num_features"] \
            if "num_features" in kwargs.keys() else 3
        self.num_classes = kwargs["num_classes"] \
            if "num_classes" in kwargs.keys() else 2
        # hidden layer node features
        self.hidden = 256
        self.model = Sequential("x, edge_index, batch_index", [
                (GCNConv(self.num_features, self.hidden), 
                    "x, edge_index -> x1"),
                (ReLU(), "x1 -> x1a"),
                (Dropout(p=0.5), "x1a -> x1d"),
                (GCNConv(self.hidden, self.hidden), "x1d, edge_index -> x2"), 
                (ReLU(), "x2 -> x2a"),
                (Dropout(p=0.5), "x2a -> x2d"),
                (GCNConv(self.hidden, self.hidden), "x2d, edge_index -> x3"), 
                (ReLU(), "x3 -> x3a"),
                (Dropout(p=0.5), "x3a -> x3d"),
                (GCNConv(self.hidden, self.hidden), "x3d, edge_index -> x4"), 
                (ReLU(), "x4 -> x4a"),
                (Dropout(p=0.5), "x4a -> x4d"),
                (GCNConv(self.hidden, self.hidden), "x4d, edge_index -> x5"), 
                (ReLU(), "x5 -> x5a"),
                (Dropout(p=0.5), "x5a -> x5d"),
                (global_mean_pool, "x5d, batch_index -> x6"),
                (Linear(self.hidden, self.num_classes), "x6 -> x_out")])    
       
    def forward(self, graph_data):
        x, edge_index, batch = graph_data.x, graph_data.edge_index,\
                    graph_data.batch
        x_out = self.model(x, edge_index, batch)
        return x_out


다음은 라이트닝 라이브러리를 사용한 코드이다. 

class LightningGCN(pl.LightningModule):
    def __init__(self, **kwargs):
        super(LightningGCN, self).__init__()
        self.num_features = kwargs["num_features"] \
                    if "num_features" in kwargs.keys() else 3
        self.num_classes = kwargs["num_classes"] \
                    if "num_classes" in kwargs.keys() else 2
        # hidden layer node features
        self.hidden = 256 
        self.model = Sequential("x, edge_index, batch_index", [\                
                (GCNConv(self.num_features, self.hidden), \
                    "x, edge_index -> x1"),
                (ReLU(), "x1 -> x1a"),\                                         
                (Dropout(p=0.5), "x1a -> x1d"),\                                
                (GCNConv(self.hidden, self.hidden), "x1d, edge_index -> x2"), \ 
                (ReLU(), "x2 -> x2a"),\                                         
                (Dropout(p=0.5), "x2a -> x2d"),\                                
                (GCNConv(self.hidden, self.hidden), "x2d, edge_index -> x3"), \ 
                (ReLU(), "x3 -> x3a"),\                                         
                (Dropout(p=0.5), "x3a -> x3d"),\                                
                (GCNConv(self.hidden, self.hidden), "x3d, edge_index -> x4"), \ 
                (ReLU(), "x4 -> x4a"),\                                         
                (Dropout(p=0.5), "x4a -> x4d"),\                                
                (GCNConv(self.hidden, self.hidden), "x4d, edge_index -> x5"), \ 
                (ReLU(), "x5 -> x5a"),\                                         
                (Dropout(p=0.5), "x5a -> x5d"),\                                
                (global_mean_pool, "x5d, batch_index -> x6"),\                  
                (Linear(self.hidden, self.num_classes), "x6 -> x_out")])        

    def forward(self, x, edge_index, batch_index):
        x_out = self.model(x, edge_index, batch_index)
        return x_out

    def training_step(self, batch, batch_index):
        x, edge_index = batch.x, batch.edge_index
        batch_index = batch.batch
        x_out = self.forward(x, edge_index, batch_index)
        loss = F.cross_entropy(x_out, batch.y)
        # metrics here
        pred = x_out.argmax(-1)
        label = batch.y
        accuracy = (pred == label).sum() / pred.shape[0]
        self.log("loss/train", loss)
        self.log("accuracy/train", accuracy)
        return loss

    def validation_step(self, batch, batch_index):
        x, edge_index = batch.x, batch.edge_index
        batch_index = batch.batch
        x_out = self.forward(x, edge_index, batch_index)
        loss = F.cross_entropy(x_out, batch.y)
        pred = x_out.argmax(-1)
        return x_out, pred, batch.y

    def validation_epoch_end(self, validation_step_outputs):
        val_loss = 0.0
        num_correct = 0
        num_total = 0
        for output, pred, labels in validation_step_outputs:
            val_loss += F.cross_entropy(output, labels, reduction="sum")
            num_correct += (pred == labels).sum()
            num_total += pred.shape[0]
            val_accuracy = num_correct / num_total
            val_loss = val_loss / num_total
        self.log("accuracy/val", val_accuracy)
        self.log("loss/val", val_loss)

    def configure_optimizers(self):
        return torch.optim.Adam(self.parameters(), lr = 3e-4)

성능 평가
텐서보드와 같은 라이브러리를 사용하여 학습 진행 상황과 체크포인트를 기록할 수 있지만, 이 예제에서는 Lightning을 사용한 학습과 PyTorch만을 비교하고 있다.

def evaluate(model, test_loader, save_results=True, tag="_default", verbose=False):
    # get test accuracy score
    num_correct = 0.
    num_total = 0.
    my_device = "cuda" if torch.cuda.is_available() else "cpu"
    criterion = torch.nn.CrossEntropyLoss(reduction="sum")
    model.eval()
    total_loss = 0
    total_batches = 0
    for batch in test_loader:
        pred = model(batch.to(my_device))
        loss = criterion(pred, batch.y.to(my_device))
        num_correct += (pred.argmax(dim=1) == batch.y).sum()
        num_total += pred.shape[0]
        total_loss += loss.detach()
        total_batches += batch.batch.max()
    test_loss = total_loss / total_batches
    test_accuracy = num_correct / num_total
    if verbose:
        print(f"accuracy = {test_accuracy:.4f}")
    results = {"accuracy": test_accuracy, \
        "loss": test_loss, \
        "tag": tag }
    return results

학습 방법
학습 루프를 정의해 보겠다. 이것을 함수로 정의하고 평가 호출을 수행한다. 학습 데이터 로더의 배치를 통해 하나의 큰 루프 내에서 진행 상황을 저장한다.


def train_model(model, train_loader, criterion, optimizer, num_epochs=1000, \
verbose=True, val_loader=None, save_tag="default_run_"):
## call validation function and print progress at each epoch end
display_every = 1 #num_epochs // 10
my_device = "cuda" if torch.cuda.is_available() else "cpu"
model.to(my_device)
# we'll log progress to tensorboard
log_dir = f"lightning_logs/plain_model_{str(int(time.time()))[-8:]}/"
writer = SummaryWriter(log_dir=log_dir)
t0 = time.time()
for epoch in range(num_epochs):
total_loss = 0.0
batch_count = 0
for batch in train_loader:
optimizer.zero_grad()
pred = model(batch.to(my_device))
loss = criterion(pred, batch.y.to(my_device))
loss.backward()
optimizer.step()
total_loss += loss.detach()
batch_count += 1
mean_loss = total_loss / batch_count
writer.add_scalar("loss/train", mean_loss, epoch)

if epoch % display_every == 0:
train_results = evaluate(model, train_loader, \
tag=f"train_ckpt_{epoch}_", verbose=False)
train_loss = train_results["loss"]
train_accuracy = train_results["accuracy"]

if verbose:
print(f"training loss & accuracy at epoch {epoch} = "\
f"{train_loss:.4f} & {train_accuracy:.4f}")

if val_loader is not None:
val_results = evaluate(model, val_loader, \
tag=f"val_ckpt_{epoch}_", verbose=False)
val_loss = val_results["loss"]
val_accuracy = val_results["accuracy"]

if verbose:
print(f"val. loss & accuracy at epoch {epoch} = "\
f"{val_loss:.4f} & {val_accuracy:.4f}")
else:
val_loss = float("Inf")
val_acc = - float("Inf")

writer.add_scalar("loss/train_eval", train_loss, epoch)
writer.add_scalar("loss/val", val_loss, epoch)
writer.add_scalar("accuracy/train", train_accuracy, epoch)
writer.add_scalar("accuracy/val", val_accuracy, epoch)

데이터 세트, 데이터 로더 및 통합
TUDataset PROTEINs 데이터 세트, GNNBenchmarkDataset의 MNIST 그래프 버전에서 훈련할지 여부를 선택할 수 있다. 

if __name__ == "__main__":
# choose the TUDataset or MNIST, 
# or another graph classification problem if preferred
dataset = TUDataset(root="./tmp", name="PROTEINS")
#dataset = GNNBenchmarkDataset(root="./tmp", name="MNIST")
# shuffle dataset and get train/validation/test splits
dataset = dataset.shuffle()
num_samples = len(dataset)
batch_size = 32
num_val = num_samples // 10
val_dataset = dataset[:num_val]
test_dataset = dataset[num_val:2 * num_val]
train_dataset = dataset[2 * num_val:]
train_loader = DataLoader(train_dataset, batch_size=batch_size)
test_loader = DataLoader(test_dataset, batch_size=batch_size)
val_loader = DataLoader(val_dataset, batch_size=batch_size)

for batch in train_loader:
num_features = batch.x.shape[1]
num_classes = dataset.num_classes

# TODO

앞의 코드의 # TODO 밑에 다음 코드를 추가하고 실행해 본다. 
일반적인 버전 코드는 다음과 같다. 

plain_model = PlainGCN(num_features=num_features, num_classes=num_classes)
lr = 1e-5
num_epochs = 2500
criterion = torch.nn.CrossEntropyLoss(reduction="sum")
optimizer = torch.optim.Adam(plain_model.parameters(), lr=lr)
train_model(plain_model, train_loader, criterion, optimizer,\
num_epochs=num_epochs, verbose=True, \
val_loader=val_loader)

Lightning 버전에서는 클래스를 사용하여 Trainer을 호출하고 학습을 조정한다. 

lightning_model = LightningGCN(num_features=num_features, num_classes=num_classes)
num_epochs = 2500
val_check_interval = len(train_loader)
trainer = pl.Trainer(max_epochs = num_epochs, \
val_check_interval=val_check_interval, gpus=[0])
trainer.fit(lightning_model, train_loader, val_loader)

코드 실행 결과는 다음과 같다. 

테스트 결과는 다음과 같다. 


마무리
PyTorch Lightning에서 Trainer 개체는 GPU, TPU 및 IPU와 같은 다양한 하드웨어 가속기에 대한 할당을 처리한다. 이를 통해 Google의 TPU 또는 Graphcore의 IPU와 같은 ASIC 하드웨어 가속기로 확장하는 것도 훨씬 쉽다.

Lightning은 각각의 새 프로젝트에 대한 상용구 코드의 양을 줄이고 하드웨어 가속을 쉽게 적용하는 좋은 방법이다.

레퍼런스