かみのメモ

コンピュータビジョン・プログラムな話題中心の勉強メモ

【ちょっとしたデータ処理に使える】Pythonで手軽に数値データを受け取る方法

今回はちょっとしたメモ書き。 他のプログラムで計算した数値データをPythonで手軽に受け取る小技について紹介します。

前置き

筆者はいつも重いデータ処理を書くときにC++やCUDAを使うのですが、アルゴリズムの確認や可視化のためにPythonで計算結果を受け取りたくなることがよくあります。

正攻法でいくならXML/JSON/Procol Buffersなどのフォーマットに変換してファイルに保存するべきだと思うのですが、ライブラリを導入するのが面倒ですし、C++Python両方のライブラリの使い方を覚えるのは面倒です。

数値データを渡す方法

ということで、筆者はいつも以下のようなフォーマットを利用しています。

<data>
0.2627536 , 0.47212708, 0.30627497
0.38864501, 0.70619836, 0.23251527
0.79015535, 0.20616244, 0.02252178
0.97898133, 0.27346775, 0.14835743
0.28076642, 0.49231602, 0.19847371
</data>
<size>5</size>
<mean>0.54026034  0.43005433  0.18162863</mean>

なんちゃってXMLですね。 エスケープとかは何も考えてないので値に<hoge>みたいな文字列が入るとバグりますが、今回は数値データの伝達を目的としているのでこれで十分です。

まずC++側のプログラムはデータをこのフォーマットでprintf/coutするように書いておきます。

次にPython側でそのデータを受け取ります。 私は以下のように標準入力から文字列を受け取りタグ別の辞書形式に整形するコードをテンプレートとして使っています。

import sys
import re
from io import StringIO
import numpy as np

datastr = sys.stdin.read()
pattern = re.compile(r'<([^<>]+)>\s*(.+)\s*</\1>', re.MULTILINE | re.DOTALL)
valuelist = re.findall(pattern, datastr)
valuedict = dict(valuelist)

print("Keys : " + ', '.join([key for key, val in valuelist]))
# Keys : data, size, mean

標準入力から受け取るようにしておけば./calculate | python3 vizualize.pyのようにコマンド一発で計算処理と可視化処理が実行されるので便利です。 一度ファイルに書き込む場合もcat data.txt | python3 visualize.pyのようにすればいいですね。

あとは受け取ったデータを適当な数値データに変換するだけです。

以下の例ではnumpy.loadtxtを使ってnumpy.arrayに変換しています。 loadtxtは区切り文字(delimiter)を自由に指定できるので、空白区切り/CSV/TSVどれでも読み込めます。

size = int(valuedict['size'])
print(size)
# 5

data = np.loadtxt(StringIO(valuedict['data']), delimiter=',')
print(data)
# [[0.2627536  0.47212708 0.30627497]
#  [0.38864501 0.70619836 0.23251527]
#  [0.79015535 0.20616244 0.02252178]
#  [0.97898133 0.27346775 0.14835743]
#  [0.28076642 0.49231602 0.19847371]]

mean = np.loadtxt(StringIO(valuedict['mean']))
print(mean)
# [0.54026034 0.43005433 0.18162863]

活用例

最後に実例を紹介してみます。 最近、主成分分析(PCA)を利用して三次元点群の擬似的な法線を計算するコードをC++で書いたので、これをPython+Plotlyで可視化してみます。

ちなみにPlotlyの使い方に関しては過去に以下のような記事を書いているのでよければ一緒に読んでみてください。

C++コード

#include <iostream>
#include <random>

#include <Eigen/Core>
#include <Eigen/Dense>
#include <Eigen/Geometry>

std::random_device rnd;
std::default_random_engine rnd_engine(rnd());

std::normal_distribution<> dist(0.0, 0.5);

double func(const double x, const double y) { return dist(rnd_engine); }

int main() {
  std::vector<Eigen::Vector3d> points;
  std::cout << "<points>" << std::endl;
  for (int x = 0; x <= 20; x++) {
    for (int y = 0; y <= 20; y++) {
      points.push_back(Eigen::Vector3d(x, y, func(x, y)));
      std::cout << points.back().x() << ", " << points.back().y() << ", " << points.back().z()
                << std::endl;
    }
  }
  std::cout << "</points>" << std::endl;

  Eigen::Vector3d mean;
  for (int i = 0; i < points.size(); i++) { mean += points[i]; }
  mean /= points.size();

  std::cout << "<mean>" << mean << "</mean>" << std::endl;

  Eigen::Matrix3d cov;
  for (int i = 0; i < points.size(); i++) {
    Eigen::Vector3d dif = points[i] - mean;
    cov += dif * dif.transpose();
  }

  Eigen::EigenSolver<Eigen::Matrix3d> solver(cov);

  int min = 0;
  auto eigvals = solver.eigenvalues().real();
  if (eigvals(1) < eigvals(0) && eigvals(1) < eigvals(2)) {
    min = 1;
  } else if (eigvals(2) < eigvals(0) && eigvals(2) < eigvals(1)) {
    min = 2;
  }
  std::cout << "<normal>" << solver.eigenvectors().row(min).real() << "</normal>" << std::endl;

  return 0;
}

Pythonコード

import sys
import re
from io import StringIO
import numpy as np

import plotly.offline as po
import plotly.graph_objs as go


datastr = sys.stdin.read()
pattern = re.compile(r'<([^<>]+)>\s*(.+)\s*</\1>', re.MULTILINE | re.DOTALL)
valuelist = re.findall(pattern, datastr)
valuedict = dict(valuelist)
print("Keys : " + ', '.join([key for key, val in valuelist]))

points = np.loadtxt(StringIO(valuedict['points']), delimiter=',')
mean = np.loadtxt(StringIO(valuedict['mean']))
normal = np.loadtxt(StringIO(valuedict['normal']))

print(mean)
print(normal)

trace1 = go.Scatter3d(text='point cloud', x=points[:, 0], y=points[:, 1],
                      z=points[:, 2], mode='markers', marker=dict(size=2))
trace2 = go.Scatter3d(text='center point', x=[mean[0]], y=[
                      mean[1]], z=[mean[2]], mode='markers')
vec_n = mean + 5*normal
trace3 = go.Scatter3d(text='normal', x=[mean[0], vec_n[0]], y=[mean[1], vec_n[1]], z=[
                      mean[2], vec_n[2]], mode='lines')

layout = dict(
    margin=dict(l=0, r=0, b=0, t=0),
    scene=dict(
        aspectmode='manual', aspectratio=dict(x=2.0, y=2.0, z=1.2),
        xaxis=dict(range=[0, 20]), yaxis=dict(range=[0, 20]), zaxis=dict(range=[-6, 6])))

po.plot(go.Figure(data=[trace1, trace2, trace3],
                  layout=layout), filename='pca_norm.html')

f:id:kamino-dev:20190929183928p:plain
Plotlyで可視化した結果

ちゃんとPython側で値を受け取ることができてます。 C++はライブラリを導入したりライブラリAPIに合わせて型を調整したりするのが大変なので、Printデバッグに近いこの方法は手軽でとっつきやすいのではないかと思います。


以上、標準入力ベースで数値データを受け取る小技を紹介しました。