現実世界にあるモノをスキャンして3次元データ化してみる～LiDARによる点群処理入門～

本記事をお読み頂き，ありがとうございます．

はじめまして．プログラミングサークルのみなみんと申します．
関係者に対しましては，随分と久しぶりです．

アドベントカレンダーの25日目，ここはプログラミングサークルの設立者である私がカレンダーの最後を飾ります．

２年程前に本サークルを設立して運営をしていた私ですが，いよいよ忙しくなり研究活動にも精進したいので交代してもらうことになりました．
最近は点群処理技術を中心に，身の周りのモノや空間を計測したり解析したり．それ以外の技術にも，かじる程度には触れているつもりです．

さて，今回は私の研究分野丸出しの記事を書きたいと思います．
（守秘義務はもちろん，あります．）

この記事では，iPhoneに搭載されているLiDARセンサーを使って，点群データを計測し，点群処理をしてみました．

## 経緯とか
まあ，アドベントカレンダーを書くことになったのが経緯そのものなんですけどね．
この内容にした経緯としては，学内で「点群データ」と聞いてピンとくる人が私の研究室以外で意外といないんですよね．ロボット分野の人で多少はピンとくる人は居てたのですが，それ以外では全くピンとくる人が居なかったのです．
（という筆者自身も，所属するまでは，知りませんでした．）

## モノをデジタル上で再現する一つの方法例
一般的な話ですが，ヒト・モノを３次元的にスキャンする方法には以下の方法が挙げられます．（ざっくりしすぎ？）
- 写真測量
- レーザ測量
今回のLiDARによる計測は「レーザ測量」．

## 用途例とか（一般の方からの感覚）
- 観賞用（写真と同じ感覚で？）
→思い出作りとか，とにかく保存したいみたいな．
- モデリング
→点群データを基にしてモデリングするとか？
- 管理・点検
→道路や橋とか，公共の構造物をスキャンして管理する事例が有名
- デジタルアーカイブ
→文章や画像等のデジタル資料が図書館等の公共施設に保管されているのと同じように，
その当時にあったモノをデジタルとしてデータを保存しておけば将来，役に立つ．それの３次元データ版．

## 点群データ？美味しいの？
点群データについての説明は，他の記事やWikipediaなどに詳しく載っているのですが，要するに点の座標の集合です．一点一点の座標値が書かれているだけです．これに，色の値とか，諸々の情報が載ってくる感じでしょうか．

## とりあえず，撮ってみたいんだ
手軽に計測できそうなもので計測していきたいと思います．今回は，筆者のiPhone 14 Proに搭載されているLiDARで大学内に飾ってあるオブジェでも計測してみます．時間がなかったのでざっくりとしか撮れませんでした．
（ちゃんと，ここだけはクリスマスに合わせたつもりですよ！）

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3946425/ee5fd134-7214-a546-a134-d22d47a060ae.png" width="250">


計測には，「Scaniverse」というアプリを使います．他にも計測アプリはあるので，それでも撮れます．

## PCに保存
アプリ側で，`ply`ファイルとして出力をかけます．それをPCで可視化した結果を表示したいのですが，筆者は「Cloud Compare」というオープンソースのソフトウェアがあるので，これを使います．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3946425/f8163c58-4cca-78af-b533-6e103c178591.png)

## プログラム上で読み込み
今回は，どのPC上でも比較的構築が容易なOpen3dをPython環境上で実行してみます．
以下のようなプロジェクトを作り，`main.py`に以下のプログラムを実装しました．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3946425/2065fd25-fa2d-a473-614b-1f8ad208f7f1.png)
```python
import os
import open3d as o3d
import numpy as np
import glob


def main(_target: str, _out: str) -> None:
    files = glob.glob(os.path.join(_target, "*.ply"))

    for file in files:
        print(file)
        pcd: o3d.geometry.PointCloud = o3d.io.read_point_cloud(file)
        points = np.array(pcd.points)

        print(f"Points: {len(points)}")
        print(points)
    return


if __name__ == "__main__":
    target = "./ply"
    out = "./out"

    main(target, out)

```

処理内容としては，プログラム上に点群データ`o3d.geometry.PointCloud`というデータ型で読み込み，その中の点座標配列のプロパティを参照するだけのものです．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3946425/3b10e8d5-7240-7606-60fe-dde2daf91034.png)
これを使って，プログラム上で点群データに対して処理をしていきます．

## 点群処理: 置物だけを取り出してみたい
計測した点群データというのは通常，壁や地面，あるいはノイズ等といった，対象物以外の要らない部分というのが必ず，付いてきます．なので，これを自動的に取り除いて対象の点群データのみを抽出してみようと思います．今回の対象物は，画像の赤枠に示すような，ガラス越しにあるサンタさんや雪だるま等といった「置物」とします．
もちろん，CloudCompare上で手作業で点群データの切り出しは可能なのですが，面倒なので自動化するという背景があります．

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3946425/b0838738-367e-0646-dd8f-8bd91b20fe1b.png)
今回の点群データの場合，壁や地面では，点の分布が平らである性質に着目し，平面検出によるアプローチで達成してみます．

### 対象物抽出へのアプローチ
1. 平面を検出する（できれば複数面）
2. 検出した平面に属する点を特定
3. 特定した点以外を残して抽出

こんな感じの，とても安易なアプローチで置物を抽出してみます．

### 実装ソースコード
これらのアプローチを基に，実装したソースコードがこちらです．

```python
import os
import open3d as o3d
import numpy as np
import glob


def main(_target: str, _out: str) -> None:
    files = glob.glob(os.path.join(_target, "*.ply"))

    for file in files:
        print(file)
        pcd: o3d.geometry.PointCloud = o3d.io.read_point_cloud(file)
        points = np.array(pcd.points)

        print(f"Points: {len(points)}")
        print(points)
        pcd.estimate_normals(
            search_param=o3d.geometry.KDTreeSearchParamHybrid(radius=0.1, max_nn=40))

        oboxes: list[o3d.geometry.OrientedBoundingBox] = pcd.detect_planar_patches(
            normal_variance_threshold_deg=60,
            coplanarity_deg=75,
            outlier_ratio=0.1,
            min_plane_edge_length=0,
            min_num_points=0,
            search_param=o3d.geometry.KDTreeSearchParamKNN(knn=30))

        print("Detected {} patches".format(len(oboxes)))

        patches_label: np.ndarray = np.zeros([int(points.shape[0])])

        geometries = []
        for obox in oboxes:
            mesh = o3d.geometry.TriangleMesh.create_from_oriented_bounding_box(obox, scale=[1, 1, 0.0001])
            mesh.paint_uniform_color(obox.color)
            geometries.append(mesh)
            geometries.append(obox)
            indices = np.array(obox.get_point_indices_within_bounding_box(pcd.points))
            patches_label[indices] = 1

        points_indices_final = np.where(patches_label == 1)[0]
        pcd_final: o3d.geometry.PointCloud = pcd.select_by_index(points_indices_final, invert=True)

        filename_final: str = os.path.join(
            _out,
            os.path.basename(file).replace(".ply", "_final.ply")
        )
        o3d.io.write_point_cloud(filename_final, pcd_final)
        geometries.append(pcd)
        o3d.visualization.draw_geometries(geometries)
    return


if __name__ == "__main__":
    target = "./ply"
    out = "./out"

    main(target, out)
    
```
平面検出には「Planar Patch Detection」という機能を使い，複数の平面を出す必要があることから，この手法を選択した．
他にも，平面検出の機能はOpen3d上で実装されていて，色んな方法があるので，調べてみると良いぞ．
（採用したこの手法については，点群データの平面検出に関する一つの手法として，論文にもなっているので，興味があれば，ご参照あれ）

このソースコードを実行すると，Open3d側でウィンドウが表示され，平面検出結果のプレビューが確認できます．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3946425/7cdafc2b-89e6-f716-72b1-195c9128a167.png)
平べったい部分，ほぼほぼ全てに平面が貼られているのが見て取れますね．

ウィンドウを閉じると，処理は全て終え，`out`フォルダにファイルが出力されているかと思います．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3946425/0c6a310b-d12e-d3f7-7ced-4e36f63a3841.png)

これを，確認してみましょう．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3946425/ac621ba5-b491-b3fc-5df4-e0dfdb36107a.png)

適用前が下記の画像
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3946425/d36b0c3c-1a9a-189c-522b-d0a9722d4a57.png)

まあ，概ねではありますが，取れてますね．
もちろん，取れていない部分も沢山あったり，散らかっている点もあったりと，色んな課題は残ります．
ここまでの所感としては，パラメータの微調整でも改善は期待できると考えてます．が，記事なので，ここまでにしておきましょう．

## さいごに
というわけで，今回は，現実空間上にある置物を点群データとしてデジタル上に取り込み，点群処理という形でデータ加工をしてみる記事でした．
このような，点群データを使った取り組みというのは，これ以外にも沢山あって，盛んに研究開発が行われ，実社会に取り入れられています．これからの技術の進歩に期待です！