昨年(2017/12)Unityが32bitメッシュに対応したことで、Unityで点群を扱うのが簡単になりました。
これによりプラットフォームを跨いだ利用がしやすくなり、今回Kinect+Unityで撮影した点群を転送し、ARKit+Unityで表示しました。
ここでは主にどういった手法でARKitに実装したかをまとめています。
技術背景
- Unity2017.3(2017/12)で32bit頂点(約4億頂点)に対応
これまでは16bit制限(65,535頂点)で分割して表示する手間があった - 32bit対応によりKinectの点群をひとつのメッシュで表示可能となった
KinectV2の画素数: 512x424=217,088
動画
俯瞰図
図のようにKinectで撮影した点群をWebSocketで転送し、ARKitのImageTrackingで表示しています。
ハードウェア
- KinectV2
- iPad Pro 12.9インチ 第2世代
動作確認バージョン
- Win10 1803
- MacOS 10.14
- Unity2018.2.9f1
- Xcode 10.0
- iOS12.0
Kinect環境構築
KinectV2をWindowsのUnity上で動かすには、Kinect for Windows SDK 2.0と、Unity Proパッケージの導入が必要です。
- Unity Proパッケージ
- 解凍し、含まれるKinect.2.0.1410.19000.unitypackageをUnityにImportします。
Kinect実装
Kinectで撮影した点群を表示します。
コードはGistにアップしています。
https://gist.github.com/otmb/e4eeebeb8bfe7730e892d4ded28b31c7
コードの説明
32bit Meshを利用するので初めに宣言が必要です。
mesh = new Mesh()
{
indexFormat = UnityEngine.Rendering.IndexFormat.UInt32,
};
イベントハンドラに点群とBodyTrackingのデータを取得するように指示しています。
multiFrameSourceReader = _Sensor.OpenMultiSourceFrameReader(FrameSourceTypes.Depth
| FrameSourceTypes.Color | FrameSourceTypes.BodyIndex | FrameSourceTypes.Body);
multiFrameSourceReader.MultiSourceFrameArrived += Reader_MultiSourceFrameArrived;
イベントにデータが届けばあとは何となくわかります。
重要な箇所としては、マップでしょうか。
MapDepthFrameToColorSpaceでは、「Color座標系をDepth座標系にフレーム単位で変換」
MapDepthFrameToCameraSpaceでは、「Depth座標系をUnityの3D座標に変換」
_Mapper.MapDepthFrameToColorSpace(depthFrameData, colorSpacePoints);
_Mapper.MapDepthFrameToCameraSpace(depthFrameData, cameraSpacePoints);
ARKit ImageTrackingの中心に人物を表示したいので人物を検出し、人物の座標からずれた分を補正するようにしました。
Body[] bodies = new Body[bodyFrame.BodyCount];
bodyFrame.GetAndRefreshBodyData(bodies);
Body body = bodies.FirstOrDefault(b => b.IsTracked);
var bodyPos = (body == null) ? Vector3.zero : new Vector3(
body.Joints[JointType.SpineMid].Position.X,
body.Joints[JointType.SpineMid].Position.Y,
body.Joints[JointType.SpineMid].Position.Z);
...
vertices[counter] = new Vector3(p.X, p.Y, p.Z) - bodyPos;
JointType.SpineMidは骨格検出の背中あたりになります。
参照: https://docs.microsoft.com/en-us/previous-versions/windows/kinect/dn758662(v%3dieb.10)
転送データの作成
転送データは、点群を固定長のバイナリで渡しました。
コードでは以下を行っています。
- float配列(単精度)をushort配列(半精度)に置き換え
- ushort配列からバイト配列に置き換え
- ushort配列と色のバイト配列と結合したバイト配列を作成
byte[] CreateBinary(float[] vertexs, byte[] colors)
{
var ushorts = new ushort[vertexs.Length];
for (int i = 0; i < vertexs.Length; i++)
{
ushorts[i] = HalfHelper.SingleToHalf(vertexs[i]);
}
var v = GetUshotrToByte(ushorts);
var buf = v.Concat(colors).ToArray();
return buf;
}
byte[] GetUshotrToByte(ushort[] ushorts)
{
var bytes = new byte[ushorts.Length * 2];
Buffer.BlockCopy(ushorts, 0, bytes, 0, bytes.Length);
return bytes;
}
データを小さくする
単純にデータを送信すると1データ毎のサイズが大きい為、フレームレートを出すことが出来ません。
また、iPadで毎秒5MB程度受信するとバッファ処理が追い付かなくなり、溜まっていく現象が起きました。
1データを小さくし、受信するフレームを適当な量にするのがよさそうです。
- 単精度から半精度に変換
単精度から半精度にしたところ、実際の点群の表示に差異を感じなかったので半精度を採用しました。
先程のコードにあるSingleToHalfはHalfHelper.csを利用しています。
半精度にすることで、1データ3.3MB(217,088x16=3,473,408)が、1.86MB(217,088x9=1,953,792)まで減りました。
- Zstdで圧縮する
当初LZ4を試しましたが圧縮率が低いのと、秒単位で大量のフレームを処理できないことから、
速度がLZ4より多少遅いが、高圧縮なZstdを選択しました。
https://gist.github.com/otmb/66a0b40510fd544e3aceaab38192728b
このコードはUnity(Mac/iOS)にZstdを利用する内容です。
iOSの受信処理
iPadではWebSocketSharpで大きなデータを連続して受信すると稀にソケットのエラーが出ます。
色々試しましたが、エラーを検知することもかなり手間なことから、WebSocketSharpは諦め、ネイティブコードで実装しているJetfireを利用することにしました。
JetfireはStarscreamのObj-C版です。
ライブラリを変更することで通信が安定しました。
転送データを復元する
受信したバイナリを座標と色に分け、半精度を単精度に復元します。
これで頂点と色が取得できますので、KinectView.csを参考にメッシュを作成、表示が出来ます。
var pLen = buf.Length / 3 * 2;
var points = GetByteToUshort(buf, pLen);
var vertices = new Vector3[points.Length/3];
var colors = new Color[points.Length/3];
for (int i = 0; i < points.Length / 3; i++)
{
var n = i * 3;
var x = HalfHelper.HalfToSingle(points[n]);
var y = HalfHelper.HalfToSingle(points[n+1]);
var z = HalfHelper.HalfToSingle(points[n+2]);
var r = buf[pLen + n];
var g = buf[pLen + n + 1];
var b = buf[pLen + n + 2];
vertices[i] = new Vector3(x,y,z);
colors[i] = new Color32(r,g,b,1);
}
public static ushort[] GetByteToUshort(byte[] bytes,int length)
{
var ushorts = new ushort[length / 2];
Buffer.BlockCopy(bytes, 0, ushorts, 0, length);
return ushorts;
}
UnityARKitPlguinのImageTrackingで点群の表示を実装する
UnityではARKitを実装する為に、unity-arkit-pluginをUnityが公開しています。
ここでは、unity-arkit-pluginを導入したとしまして、Unity-ARKit-Plugin/Assets/UnityARKitPlugin/Examples/ARKit1.5/UnityARImageAnchorのフォルダを対象にどのように実装したかをまとめています。
ImageTracking用の表示するオブジェクトの処理は先程のフォルダにあるGenerateImageAnchor.csで行っています。
GenerateImageAnchor.csではXY座標を扱いますが、今回は点群を扱いますので床の位置(Z軸)を取得できるように修正します。
MyGenerateImageAnchor.csを用意しましたので参照ください。
MyGenerateImageAnchor.csではHitTestを行うことでZ軸を取得しています。
表示するオブジェクトは、GenerateImageAnchor.csで指定したPrefabが使われます。
Prefabを点群の表示用Prefabに置き換えることで、ImageTrackingで点群を表示します。
点群の表示用に、PointCloudViewer.csを用意しました。シェーダーはKinectで利用したkinectview.shaderを流用できます。
PointCloudViewer.csのメッシュを定期更新することでリアルタイムに点群を更新しています。
まとめ
今回はKinectを使って、ARKitでリアルタイムに点群を表示する手法についてまとめました。
プロジェクトで書いたコードは試し書きのコードが多く、整理しないとアップできない状態にある為、要点をまとめた内容で情報を公開する運びとしました。
苦労した点だと、通信周りはよくはまります。最初からネイティブのコードで動かしとけばよかった。。
3Dコミュニケーションは未来感がありますが、現段階ではまだまだ粗削りです。
もう少し発展することで今後利用シーンも増えてくるのではないかと考えています。
おまけ:ブラウザで表示
- Three.jsの点群処理を参照
- Zstdをwasmに対応
- エンコードはWebWorkerで実装
- 半精度対応
ブラウザでも受信サイズが大きいとフレームレートが稼げませんでした。
ここでも、圧縮や半精度を使い、データを小さくすることで高速にできました。
サンプルコード
https://gist.github.com/otmb/a27bf947b08dac1ed8179bf227073192
過去作例
以前にマルチカメラでリアルタイムに3Dメッシュの作成に取り組みました。
その時はPCL(Point Cloud Library)を利用しました。
よければこちらもご覧ください。