簡単な実装でTangoからPLYファイルをエクスポートする

メッシュをPLYフォーマットで出力すると、[MeshLab](http://www.meshlab.net/)等のツールで点群データとして可視化・加工することができます。

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">TangoのDynamic Meshから点群データ(.ply)のエクスポートが割と簡単にできた。なお、部屋は散らかっている模様。 <a href="https://t.co/IDo81DMljf">pic.twitter.com/IDo81DMljf</a></p>&mdash; jyuko (@jyuko49) <a href="https://twitter.com/jyuko49/status/861216507862278144">2017年5月7日</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

TangoでPLYフォーマットの点群データをエクスポートする方法として、[Matterport Scenes](https://matterport.com/matterport-scenes/)というアプリが公開されており、無料で使えます。このアプリは生成されるメッシュの品質とスキャンの速さのバランスが素晴らしく、室内まるごとのスキャンがサクサクっとできてしまいます。

大抵のケースでは、上記アプリで事足りるのですが、自作したアプリでスキャンした結果を保存・可視化したい場合などのために、Examplesをごにょごにょしてエクスポートしてみました。


## 前提条件
以下の開発環境で実装しています。

* Lenovo Phab2 Pro
* Unity 5.6
* Tango SDK for Unity (Version 1.52, Gankino)


## Tango Dynamic Meshからのエクスポート

### 実装方法
TangoDynamicMesh.csに、OBJフォーマットでファイルエクスポートするTangoDynamicMesh.ExportMeshToObj()があります。
これと同じ要領でPLYフォーマットにエクスポートするコードを書くだけです。

```TangoDynamicMesh.cs
public void ExportMeshToPointCloud(string filepath)
{
    AndroidHelper.ShowAndroidToastMessage("Exporting mesh...");
    StringBuilder sb = new StringBuilder();
    int startVertex = 0;

    foreach (TangoSingleDynamicMesh tmesh in m_meshes.Values)
    {
        Mesh mesh = tmesh.m_mesh;
        int meshVertices = 0;

        // Vertices.
        for (int i = 0; i < mesh.vertices.Length; i++)
        {
            Vector3 v = tmesh.transform.TransformPoint(mesh.vertices[i]);

            // (0 0 0)が大量に含まれていたので間引く
            if (v.x == 0 && v.y == 0 && v.z == 0)
                continue;

            meshVertices++;

            // Include vertex colors as part of vertex point for applications that support it.
            if (mesh.colors32.Length > 0)
            {
                string r = mesh.colors32[i].r.ToString();
                string g = mesh.colors32[i].g.ToString();
                string b = mesh.colors32[i].b.ToString();
                sb.Append(string.Format("{0} {1} {2} {3} {4} {5}\n", v.x, v.y, -v.z, r, g, b));
            }
            else
            {
                sb.Append(string.Format("{0} {1} {2} 0 0 0\n", v.x, v.y, -v.z));
            }
        }

        startVertex += meshVertices;
    }

    StreamWriter sw = new StreamWriter(filepath);
    sw.AutoFlush = true;
    sw.Write ("ply\nformat ascii 1.0\ncomment Tango Point Cloud\nelement vertex " + startVertex + "\n");
    sw.Write ("property float x\nproperty float y\nproperty float z\n");
    sw.Write ("property uchar red\nproperty uchar green\nproperty uchar blue\n");
    sw.Write ("end_header\n");
    sw.Write(sb.ToString());
    AndroidHelper.ShowAndroidToastMessage(string.Format("Exported: {0}", filepath));
}
```
PLYファイルのフォーマットは、以下の記事を参考にしました。
XYZとRGBのみを出力していますが、他の情報を持たせることもできます。
[PLYファイルの作成方法(Visual Studio 2013, Kinect, C/C++, 3D点群処理)]
(http://qiita.com/SatoshiRobatoFujimoto/items/d368ce499a549def1ba6)
[PLY - Polygon File Format](http://paulbourke.net/dataformats/ply/)

### 実行結果
Resolution=0.03(メッシュ解像度:3cm)でスキャンして、エクスポートしたファイルをMeshLabで読み込んだ結果です。
![スクリーンショット 2017-05-08 21.41.57.png](https://qiita-image-store.s3.amazonaws.com/0/153460/1a2af060-cb95-fd86-14a4-b05904d70cb3.png)
左がPLYフォーマットで2.1MB、右がOBJフォーマットで15.6MBでした。

PLYフォーマットの出力は簡単にできましたが、実行時のスキャンとエクスポートはMatterport Scenesの方が圧倒的に速いです。
Tangoの3DRはメッシュ解像度が3cmで最適なパフォーマンスになりますが、Matterport Scenesは1cm単位のPointCloudを同等以上の速さで生成してくれます。
3DRで1cm単位、5mm単位のメッシュを作ることも可能ではありますが、処理がかなり重いので部屋全体をスキャンするには相当の時間がかかります。
また、エクスポートについても1部屋分のメッシュを変換するのに、数十秒〜数分を要します。


## PointCloud(色情報なし)からのエクスポート

### 実装方法
PointCloudを更新していく処理の中で、デプス・カメラの生データをファイルに出力していきます。
Dynamic Meshの例では、空間のスキャンを行った後にエクスポートを実行してしばらく待つ形ですが、こちらはファイルを都度出力しているので、デバイスを向けて動き回るだけでデータが保存されていきます。

```PointCloud.cs
private int fileCount = 0;

public void OnTangoPointCloudAvailable(TangoPointCloudData pointCloud)
{
	// 元の処理はそのまま

	// ファイルエクスポートを実行する
	_ExportPlyFile();
}

private void _ExportPlyFile(){
	fileCount += 1;
	string filepath = Application.persistentDataPath + "/savedata"+fileCount.ToString()+".ply";

	FileStream fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write);
	StreamWriter sw = new StreamWriter(filepath);
	sw.AutoFlush = true;
	sw.Write ("ply\nformat ascii 1.0\ncomment Tango Point Cloud\nelement vertex " + m_pointsCount + "\n");
	sw.Write ("property float x\nproperty float y\nproperty float z\n");
	sw.Write ("end_header\n");
	for (int i = 0; i < m_pointsCount; ++i)
	{
		string vertex = m_points[i].x.ToString() + " " + m_points[i].y.ToString() + " " + m_points[i].z.ToString() + "\n";
		sw.Write (vertex);
	}
}
```

### 実行結果
![スクリーンショット 2017-05-09 0.38.45.png](https://qiita-image-store.s3.amazonaws.com/0/153460/2c6cbd90-80c6-44c6-7f30-ad1096ee8734.png)

上記は10フレーム分のファイルをMeshLabで表示した結果です。出力は時系列に並んだ複数のPLYファイルになり、1ファイルあたりのサイズは約1MBでした。
元データを無加工なので重複する頂点も多く、空間あたりのデータサイズも大きくなります。このままでの利用には適さないため、ツール等での加工用になるかと思います。


## まとめ
TangoアプリケーションからPLYフォーマットのファイルを簡単にエクスポートする方法を試してみました。

Dynamic Meshからのエクスポートは、3DRで色情報が付与されたメッシュから生成しているので、実装自体はシンプルですが、スキャンとエクスポートの性能でMatterport Scenesには劣ります。
一方、PointCloudからのエクスポートは特に加工をしていないため、出力は速いのですが、RGBを付与しておらず、データも冗長です。

Matterport Scenesと同等のパフォーマンスを得るには、Tangoの3DRを使わずに、DepthやImageからPointCloudを加工する必要がありそうですが、デバイスの姿勢を計算して頂点と色情報を紐付けたり、ダウンサンプリングで頂点数を減らすなどの処理が必要で、パフォーマンス向上を目的で自作するには、やや敷居が高いです。
自作アプリを実行時の出力が見たい場合には本記事のような方法で、別アプリでよければMatterport Scenesの利用を検討するのがよいかと思います。