
最近、Kinectを使う開発環境Picodeの実装を手直ししています。この開発環境はJavaで書かれているのですが、Kinect for Windows SDKのAPIを使う必要があります。
Kinect for Windows SDKはC++とC#向けのAPIしか提供していないため、Javaから使うためにはC++またはC#のプロセスと通信することになります。そこで今回は、ThriftというFacebookが開発したフレームワークを使ってプロセス間通信(Inter-process communication)してみました。
ソースコードとバイナリはGitHubにあります。
Inter-process communication (IPC)
Java VMと外界がInter-process communicationするための方便はJNI、Named pipe、Memory-mapped file……といろいろあるのですが、今回はKinectサーバをWindowsマシンで動かしてPicodeを別のMacマシンで動かすようなこともしてみたかったので、ソケット通信を利用することにしました。
実装の初期段階では自前のTCP/IPサーバとクライアントを書いていたのですが、クライアント側で呼び出したい機能が増えていくに従って俺々プロトコルが複雑になってしまいました。
そこで、TCP/IP越しのRPC用のフレームワークの中で使いやすそうなものがないか調べました。
Thrift
今回はC#がサーバ、Javaがクライアントになるので、ライブラリの実装状況から見て候補がThriftとMessagePackに絞られました。さらにMessagePackのほうはC#のRPCの実装がドキュメント不足で使いづらそうだったのでThriftを使うことにしました。
Thriftは最初に拡張子.thriftのテキストファイルでRPCの仕様を書いて、これをthriftバイナリに食わせることで各言語のテンプレートを生成することができます。Kinectサーバの場合は、.thriftをこんな具合に書いてやると、ちゃんとC#のテンプレートとJavaのテンプレートが生成されました。
サーバ側は、自動生成されるインタフェース(KinectService.Iface)を実装するクラス(KinectServiceHandler)を作り、次のようなコードを書いてやればサーバが起動します。
KinectServiceHandler handler = new KinectServiceHandler(); KinectService.Processor processor = new KinectService.Processor(handler); TServerTransport serverTransport = new TServerSocket(10000); TServer server = new TSimpleServer(processor, serverTransport); handler.Shutdown = server.Stop; Console.WriteLine("Starting the server..."); server.Serve();
クライアント側はさらに簡単で、自動生成されるClientクラス(KinectService.Client)をインスタンス化すればサーバに接続でき、リモートの関数を呼び出せます。
TTransport transport = new TSocket("localhost", 10000); TProtocol protocol = new TBinaryProtocol(transport); KinectService.Client client = new KinectService.Client(protocol);
GitHubにC#のサーバと、Javaの簡単なクライアントのサンプル、GUIがついてちょっと複雑なクライアントのサンプルを置いてあります。どちらもGit cloneするとバイナリが置いてあってすぐ実行できるようになっています。実行の仕方について詳しくはREADME.mdをどうぞ。
はまったところ
Thriftの型システムとエンディアン
上にさらっと書いた内容だけだとすごく簡単そうです(実際、通信部分のコードは全く気にせず済んだので、そこはよかったのです)が、Thriftの型システムにいただけないところがあって苦労しました。まず、intやlongなどの配列を作れません。list<i32>と書くと、配列ではなくListができます。しかも、Javaの場合はプリミティブ型のListが作れないのでList<Integer>になります。滅びればいいのに。
で、binaryと書くと単なるbyte型配列ができるので、short[]をクライアントに返すためにいったんbyte[]に変換するコード(C#)を書いたのですが、受け取ったbyte[]をshort[]に戻すコード(Java)を素直に書いたらデータが壊れました。原因はEndian-nessの不整合でした。
以下のC#側のコードはWindows上で動くので、Little endianでshort[]をbyte[]に書き込みます。
// Depth image processing. if (depthImageFrame != null && depthEnabled) { depthImageFrame.CopyPixelDataTo(depthImageData); Buffer.BlockCopy(depthImageData, 0, depthImageRawData, 0, depthImageRawData.Length); frame.DepthImage = depthImageRawData; }
一方、以下のJava側のコードはデフォルトでBig endianを使うため、データが壊れたというわけです。
if (frame.isSetDepthImage()) { depthByteBuffer.put(frame.getDepthImage()); depthByteBuffer.rewind(); depthShortBuffer.put(depthByteBuffer.asShortBuffer()); depthShortBuffer.rewind(); depthImageData = depthShortBuffer.array(); }
けっきょく、Java側でLittle endianを明示的に指定して復号することで対応しました。
// C# server running on Windows converts short[] to byte[] with little-endian. // Therefore, we need to specify the endian-ness here to reconstruct it correctly. depthByteBuffer.order(ByteOrder.LITTLE_ENDIAN);
Kinectのカラー画像のRGBオーダー
Thriftは全く関係ないのですが、Kinectのカラー画像はなぜかBGR(null)の順でbyte[]として取得できるようになっています……ふつうARGBか(null)BGR、(null)RGBだと思うんですけど。このJava側でBGR(null)順になっていた理由は上記と同じEndian-nessの問題でした。ともかく、こうして受信したbyte[]をJavaのBufferedImageとしてレンダリングしようと思うと、素直な実装ではbyte[]の要素数ループを回してRGBのオーダーを入れ替えなければなりません。それって何だかエレガントじゃない。
この問題を解決するため、ByteBufferを使い、先頭に1バイト(null)を足してお尻を1バイト短くしたうえでIntBufferとして読み出してやることにより、TYPE_INT_BGRなBufferedImage用のint[]を練成しました。要は「BGR0BGR0BG……BGR0」となってるものを「0BGR0BGR0BG……BGR」にしてint[]でラップしたわけです。実際のコードはこのあたり。
colorImageBuffer = (DataBufferInt) image.getRaster().getDataBuffer(); colorIntBuffer = IntBuffer.wrap(colorImageBuffer.getData()); // 中略 if (frame.isSetImage()) { byte[] imageData = frame.getImage(); colorByteBuffer.put((byte) 0); colorByteBuffer.put(imageData, 0, imageData.length - 1); colorByteBuffer.rewind(); colorIntBuffer.put(colorByteBuffer.asIntBuffer()); colorIntBuffer.rewind(); }
そんな努力の賜物のKinectサーバ/クライアント、よかったら使ってみてください。それなりのパフォーマンスで動きます。ThriftのファイルもGitHubにあげてあるので、他の言語のクライアントも比較的簡単に書けると思います。
No Comments
Be the first to start a conversation