雑食性雑感雑記

知識の整理場。ため込んだ知識をブログ記事として再構築します。

Ubuntu で動作する .NET 系な GUI フレームワークお試し (Mono、AvaloniaUI)

仕事で、Ubuntu で動く GUI フレームワークについてアレコレ調べたのですが、思いの外深掘りになったので、改めて私的にまとめてみることにしました。

経緯

以前、Ubuntu を使って Qt を用いた GUI アプリケーションを作ったのですが、データのやり取りが絡んでくると結構複雑な構成となり、ちょっとトラウマ。。

改めて今回、同じく Ubuntu で動く GUI アプリケーションとなったので、使えるものを調査することにしました。
最近 WPF を使うことが増えてきたので、その互換のものがあればスキル的にも楽かと――

作るもの

実際作るものを簡易的にした構成の『動画ビューア』を作ります。
動画の読み込みは OpenCV を使った動的ライブラリ (.so ファイル) で行い、それを GUI から呼び出すようにします。

完成イメージはこんな感じ。
動画の読み込みダイアログ使ったファイルパス指定と、動画のスタート/ストップボタン。

f:id:kazuki_nagasawa:20201021121106p:plain
完成イメージ

作ったもの

今回作ったものは github に置いたので、興味ある方はどうぞ。
github.com

環境

Ubuntu 環境は WSL で試してます。

  • Windows 10
    • Visual Studio 2017
    • Ubuntu 18.04 (on WSL)
      • OpenCV 4.2.0
      • .NET Core SDK 2.1

以下、作ったものの説明です。

動的ライブラリ

これは普通に Linux C++ で動くものを作るだけだから特記事項はないですね。
忘れがちなのは 「extern "C"」 でエントリポイント作らないと外部呼出しで関数が見えないことですね。

extern "C" {
    int initialize(char* videoPath, int width, int height);
    int getFrame(byte* data);
}

使い方としては、
・initialize で読み込む動画パス、映像表示ウィンドウの幅、高さを指定。
・getFrame で 1 フレーム分の動画データを映像表示ウィンドウに合わせてリサイズ後、byte 配列として取得。
というのを想定しています。

Mono で Windows Forms アプリケーションを Linux 化

調べていてまず初めに出てきたのが Mono。オープンソースで .NET Framework ベースの GUI を使えるらしい。( そのうち .NET Framework に吸収される? )
https://www.mono-project.com/

いくつか使える中の Windows Forms アプリケーションを試してみます。

Visual Studio で GUI 構築

( どこまで新しいバージョンに対応できているか不明ですが、)
作成はそのまま Visual Studio でできるので、開発は楽で良いですね。

so ライブラリ読み込み部は Linux 側じゃないと確認できないので、表示・操作部分のみ VS 側で実装してしまいます。

f:id:kazuki_nagasawa:20201021123056p:plain
Visual Studio で表示部作成中

Linux 側に移してビルド

表示部が出来たら、C# ファイル (.cs) だけを Linux 側にコピー。今回は
・Program.cs ... コード全体のエントリポイント
・MainWindow.cs ... ウィンドウの処理実装部
・MainWindow.Designer.cs ... ウィンドウのコントロールやデザイン。大抵自動生成。
の3ファイル。
MainWindow.cs には so ライブラリとの結合処理も追加で作ります。

事前に mcs (Mono C# Compiler) をインストールした上でビルドすれば、アプリケーション (exe) がビルドできます。

実行

あとは mono コマンドで実行すれば OK 。
( Windows 上で実行する場合は X Window System 対応アプリケーションを事前に起動しておくこと。)

f:id:kazuki_nagasawa:20201022083215p:plain
WSL 上から起動した Mono 版アプリケーション

ファイル選択ウィンドウのフィルタ部の日本語は文字化けしてますね。。
この辺り正しく使うなら追加設定必要そう。

f:id:kazuki_nagasawa:20201021123835p:plain
ファイル選択ウィンドウ (Mono)

AvaloniaUI で WPF (に近い) アプリケーションを Linux 化

Windows Forms + Mono を使えばとりあえず Linux で作りやすい GUI ができることが分かったのですが、
・Windows Forms はアンカー設定等、画面サイズがコロコロ変わるものを作りにくい。
・配置には WPF の Grid layout が便利だったなー。
ということでやっぱり WPF 使いたい!!

「Mono で WPF も対応してくれないのー?」→ 「AvaloniaUI なるものがあるよ。」
https://www.mono-project.com/docs/gui/wpf/

ということだったので、それならと AvaloniaUI でも同じものを作って試してみることにしました。

Visual Studio に AvaloniaUI 適用

AvaloniaUI を Visual Studio で使うには、プラグインが配布されているのでそれを入れれば OK 。
https://avaloniaui.net/docs/quickstart/create-new-project

↑のチュートリアルの通り、Avalonia Application を作って進めていけば作れます。

axaml ? xaml ?

現状の AvaloniaUI では、xaml も対応しているらしいけど、axaml が標準らしい。( Avalonia の a がついている xaml ? )

Visual Studio の Solution Explorer で axaml を認識してくれなかったので、全部 xaml にリネームしたらビルド通らなくなった orz..
結局、フォルダ形式で閲覧することで axaml 見えるので、このままで開発を進めました。

コントロール

大体使うコントロールは docs に載ってます
https://avaloniaui.net/docs/controls/
が、やっぱり足りなくなると検索かける必要が。ドキュメントは全体的に少な目。。

また、厳密に WPF と互換性があるわけではなく、結構いろんなところに違いがあります。
例えば、OpenFileDialog だと、WPF 標準は

private void ButtonOpenVideo_Click(object sender, RoutedEventArgs e)
{
    Microsoft.Win32.OpenFileDialog dialog = new Microsoft.Win32.OpenFileDialog();
    dialog.Filter = "動画ファイル (*.avi,*.mov,*.mp4)|*.avi;*.mov;*.mp4|全てのファイル (*.*)|*.*";
    if (this.TextboxVideoFilePath.Text.Length > 0 && System.IO.File.Exists(this.TextboxVideoFilePath.Text))
    {
        dialog.InitialDirectory = System.IO.Path.GetDirectoryName(this.TextboxVideoFilePath.Text);
        dialog.FileName = System.IO.Path.GetFileName(this.TextboxVideoFilePath.Text);
    }
    else
    {
        dialog.InitialDirectory = System.AppDomain.CurrentDomain.BaseDirectory;
    }
    if (dialog.ShowDialog() == true)
    {
        this.TextboxVideoFilePath.Text = dialog.FileName;
    }
}

なのに対して、AvaloniaUI だと

private async void ButtonOpenVideo_Clicked(object sender, RoutedEventArgs e)
{
    OpenFileDialog dialog = new OpenFileDialog();
    FileDialogFilter f1 = new FileDialogFilter();
    f1.Name = "動画ファイル (*.avi,*.mov,*.mp4)";
    f1.Extensions = new System.Collections.Generic.List<string>();
    f1.Extensions.Add("avi");
    f1.Extensions.Add("mov");
    f1.Extensions.Add("mp4");
    FileDialogFilter f2 = new FileDialogFilter();
    f2.Name = "すべてのファイル (*.*)";
    f2.Extensions.Add("*.*");
    dialog.Filters.Add(f1);
    dialog.Filters.Add(f2);
    try {
        string[] pathes = await dialog.ShowAsync(this);
        if (pathes.Length > 0) {
           TextBoxVideoPath.Text = pathes[0];
        }
    } catch {};
}

な感じ。非同期になってますね。

また、WriteableBitmap も Avalonia.Media.Imaging にあるものを。

Linux 側に移してビルド

表示部が出来たら Linux 側に移してビルド。AvaloniaUI の場合、.NET Core でビルドするので、ソリューションファイル (.sln) 以外すべて必要です。
dotnet build でビルド。

実行

dotnet run で実行。

f:id:kazuki_nagasawa:20201022083324p:plain
WSL 上から起動した AvaloniaUI 版アプリケーション

こっちはファイル選択ウィンドウのフィルタ部の日本語も大丈夫。

f:id:kazuki_nagasawa:20201021125832p:plain
ファイル選択ウィンドウ (AvaloniaUI)

まとめ

.NET 系な GUI フレームワークである Mono と AvaloniaUI それぞれで動画再生 GUI を作ってみました。
ドキュメントが少ないですが、自分としては WPF に近く、レイアウトしやすい AvaloniaUI にとりあえず軍配でしょうか。

開発する側としては、早く OS を垣根を越えて簡単に作れるものが出てくれると嬉しいのですが。。

OpenCV + Matplotlib (Seaborn) で 2 画面の動画 + グラフ表示

前に「OpenCV映像とMatplotlibグラフを、Matplotlibの1つの画面で表示させる」というのをやりました。
OpenCV画像 + matplotlibグラフを合わせて表示する - 雑食性雑感雑記

今回は、綺麗に表示ができる「Seaborn」を使ってみたくなったのに合わせて、
OpenCV 側で2画面を出す方法が分かったので、それをやってみます。

完成形としては、こんな感じで動画のウィンドウとグラフのウィンドウが出る形です。

f:id:kazuki_nagasawa:20200519091112p:plain
完成形

※ 動画はフリーサイト Pexels より。https://www.pexels.com/video/river-with-strong-current-2019791/

コード

そこまで長くないので、全体のコードを貼っておきます。
グレースケール化した色値のヒストグラムを取ってます。
要 cv2, matplotlib, numpy, seaborn。

# for CLI
import matplotlib
matplotlib.use('TkAgg')

import cv2
import matplotlib.pyplot as plt
import numpy as np
import seaborn as sns

INPUT_MOVIE_PATH = "sample.mp4"


def hist(frame, fig) :
    """ Seaborn histgram """

    # Grayscale
    frame_g = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)

    # Graph
    plt.clf()
    sns.distplot(frame_g.flatten())
    fig.canvas.draw()

    # Graph to np img
    frame_graph = np.fromstring(fig.canvas.tostring_rgb(), dtype = np.uint8, sep = '')
    frame_graph = frame_graph.reshape(fig.canvas.get_width_height()[::-1] + (3,))
    frame_graph = cv2.cvtColor(frame_graph, cv2.COLOR_RGB2BGR)
    return frame_graph


if __name__ == '__main__' :

    # Seaborn initialize
    sns.set()
    fig = plt.figure()

    # Start video capture
    cap = cv2.VideoCapture(INPUT_MOVIE_PATH)

    while True :
        ret, frame = cap.read()
        if not ret :
            break

        # Resize ... 大きすぎる場合に
        # size = (int(frame.shape[1] / 2), int(frame.shape[0] / 2))
        # frame = cv2.resize(frame, size)

        # Histogram
        frame_graph = hist(frame, fig)

        cv2.imshow("Window", frame)
        cv2.imshow("Graph", frame_graph)

        key = cv2.waitKey(30) & 0xFF
        if key == ord('q') :
            break

説明

OpenCV で動画表示

―― はどこでも書かれていると思うので略。

グラフの numpy 画像化

この↓

    # Graph
    plt.clf()
    sns.distplot(frame_g.flatten())
    fig.canvas.draw()

    # Graph to np img
    frame_graph = np.fromstring(fig.canvas.tostring_rgb(), dtype = np.uint8, sep = '')
    frame_graph = frame_graph.reshape(fig.canvas.get_width_height()[::-1] + (3,))
    frame_graph = cv2.cvtColor(frame_graph, cv2.COLOR_RGB2BGR)
    return frame_graph

あたり。

plt.clf() で canvas に書かれたグラフのクリア。
ここ入れないと同じ figure 上に描かれ続けてカラフルなグラフになる。(特定の場合には不要かもだけど…?)

sns.distplot() で描画し、それを fig.canvas.draw() で Matplotlib 内描画エリアに描画しているらしい。
その canvas エリアを numpy array として持ってきて reshape して cv2 用に色空間変換しているのが後半部分。

その他

今回、WSL 上の Python3 から MobaXTerm を使って実行しました。
CLI 実行の場合は python-tk が入ってないとダメとか、

import matplotlib
matplotlib.use('TkAgg')

の設定が無いとダメとかあるとのこと。

メモ

  • とりあえずやりたいことはできました。
  • ただし、遅い。。描画方法等要検討。
    • グラフ描画のための計算 (ヒストグラム) 自体は遅くないハズ。
    • fig.canvas.draw() 自体が遅いという話もある。特定の部分だけ描画更新すると速くなるらしい?
  • グラフも nparray の画像化できてるので、元画像上に重ねて貼るような表現にしてもよいかも。

参照

ProcedualMeshComponent で波打つメッシュを作ってみた

最近はお仕事で UE4 を扱ってます。
その中で ProcedualMeshComponent を扱う機会があったので、その習作。波打つメッシュを作ってみました。

ProcedualMeshComponent

ProceduralMeshComponent | Unreal Engine Documentation
簡単には動的にメッシュを生成したり動かしたりする仕組みです。
作るためにはメッシュの約束事を覚える必要がありますが、フラグ一つで collision つけたりもできるので覚えると便利そう。

検索すると簡単な使い方紹介しているブログがいくつか見つかるので、詳細は省略。

こんなの作った。

作ったサンプルプロジェクトは github に置きました。( C++ プロジェクトです )
github.com

f:id:kazuki_nagasawa:20200409154923g:plain
sin カーブで波打つサンプルです。色をくるくる変えてます。

f:id:kazuki_nagasawa:20200409155033g:plain
Collision つけてオブジェクト乗せたりも可能。
( トランポリン的なものも作れるかも? )

技術的要点

( 以下添付のコードは説明用。一部が省略されています。詳しいコードは github 見てください。)

メッシュ構造

Initialize() でメッシュ頂点の定義をまとめて行っていますが、Triangle の通り、正方形 (0 -> 1 -> 2, 0 -> 2 -> 3) を一つの単位としています。

void AWaveActor::Initialize()
{
	int MeshIndexSize = (this->XMax - 1) * (this->YMax - 1) * 4;

	int midx;
	for (int my = 0; my < this->YMax - 1; my++) {
		for (int mx = 0; mx < this->XMax - 1; mx++) {
			midx = this->GetMeshIndex(mx, my);

			this->Triangles.Add(0 + midx * 4);
			this->Triangles.Add(1 + midx * 4);
			this->Triangles.Add(2 + midx * 4);
			this->Triangles.Add(0 + midx * 4);
			this->Triangles.Add(2 + midx * 4);
			this->Triangles.Add(3 + midx * 4);
		}
	}
}

各頂点は Tick() で動く UpdateMesh() で。

void AWaveActor::UpdateMesh(float DeltaTime)
{
	this->ElapsedTime += DeltaTime;

	TArray<FVector> Vertices;
	TArray<FLinearColor> VertexColors;

	float OffsetX = -this->SizeBase * this->cx;
	float OffsetY = -this->SizeBase * this->cy;

	// Calc per mesh
	FLinearColor Color = GetColor(this->ElapsedTime);
	float SizeBaseHalf = this->SizeBase / 2.0;
	float x0, x1, y0, y1;
	for (int my = 0; my < this->YMax - 1; my++) {
		for (int mx = 0; mx < this->XMax - 1; mx++) {
			x0 = this->SizeBase * (mx - this->cx) + SizeBaseHalf;
			x1 = x0 + this->SizeBase;
			y0 = this->SizeBase * (my - this->cy) + SizeBaseHalf;
			y1 = y0 + this->SizeBase;

			Vertices.Add(FVector(x0, y0, this->GetHeight(mx + 0, my + 0)));
			Vertices.Add(FVector(x0, y1, this->GetHeight(mx + 0, my + 1)));
			Vertices.Add(FVector(x1, y1, this->GetHeight(mx + 1, my + 1)));
			Vertices.Add(FVector(x1, y0, this->GetHeight(mx + 1, my + 0)));

			for (int c = 0; c < 4; c++) {
				VertexColors.Add(Color);
			}
		}
	}
}

と、正方形の各頂点を DeltaTime に従った値で更新しています。

メッシュの作成、更新

メッシュの作成は CreateMeshSection_LinearColor で行っています。第一引数の 0 はメッシュのインデックスの様で、これを増やしていけば複数レイヤに書き込みができるっぽいです (未検証 )。
更新は UpdateMeshSection_LinearColor の方で、対応するインデックスのメッシュの更新を行ってます。
( 全替えしてるのに処理的に重くないのかな…?いちおうサクサク動いてます。 )

void AWaveActor::UpdateMesh(float DeltaTime)
{
	( 中略 )

	if (this->IsCreated) {
		mesh->UpdateMeshSection_LinearColor(0, Vertices, Normals, UV0, VertexColors, Tangents);
	}
	else {
		this->IsCreated = true;
		mesh->CreateMeshSection_LinearColor(0, Vertices, Triangles, Normals, UV0, VertexColors, Tangents, this->IsCollision);
		mesh->ContainsPhysicsTriMeshData(this->IsCollision);
		mesh->CreateDynamicMaterialInstance(0, this->MaterialInterface);
	}
}

メッシュ可変色

ただ色を変えて VertexColors に入れ込むだけでは色は変わりませんでした。
VertexColor を入力にもつ Material を作成し、それを CreateDynamicMaterialInstance() で設定してあげる必要があります。
f:id:kazuki_nagasawa:20200409155405p:plain

まとめ

  • ProcedualMeshComponent で遊んでみました。
    • プログラマブルに動きのあるものを作ってみるのは面白そうです。
    • 応用例を考えるのが大変そうですが…。

OpenCV画像 + matplotlibグラフを合わせて表示する

OpenCV映像とMatplotlibグラフを一つの画面で表示させてみたい。
というのをやってみました。

経緯

OpenCVで動画を解析し、その結果をMatplotlibで表示してみたものの、
更にそれを一画面で表示してみたくなったので、それをやってみました。

OpenCVでMatplotlibのグラフの表示は出来なさそうなので、
逆で、MatplotlibでOpenCVの映像を表示する形になりました。

技術要素と参照

サンプル

サンプルとして、猫画像集めてきて、画像のヒストグラムを取ってみるものを作成してみました。
ソースコードは github。
github.com

猫画像はVisual Geometry Group - University of Oxfordを使用しています。

実行するとこんな感じ。アニメーションで順次グラフが更新されます。
あとはパディングとかオフセット設定すればきれいに見えるかと。
f:id:kazuki_nagasawa:20181212200205p:plain

まとめとか

  • GridSpec便利。
    • 今までテキトーにMatplotlib使っていたけど、しっかり使うと凄いのが (片鱗だけ) 分かった。
  • 描画コストはそれなりにかかるっぽいので、重い映像の描画には向かないっぽい。
    • そういう意味では、わざわざ表示をくっ付けなくても良いかもしれない。。

Boost.numpy 活用 〜ライフゲーム作って高速化してみる〜

Boost.numpy が使えるようになってきたので、ちょっと遊んでみました。
まず、Only Python でライフゲームを作成し、それを Boost.numpy に置き換えて速くなるかどうか試してみました。

( お遊びなので、本格的な速度評価はしてないです。 )

一応、前記事は以下。
Boost.numpy ことはじめ - 雑食性雑感雑記
Boost.numpy ことはじめ その2 (戻り値) - 雑食性雑感雑記

ライフゲームについては――

特定のルールに従ってマス目を塗り替えていくアレです。ALifeとかに詳しい内容いっぱい載ってるので割愛。

今回は、一番よく使われているであろうルールで実装しています。
・ 自分が生きているとき → 周囲生存数が 2 or 3 なら生存。それ以外は死亡。
・ 自分が死んでいるとき → 周囲生存数が 3 なら復活。

あと、オリジナルルールとして、ランダムで問答無用に復活するようになっています。
まあ、ライフゲームのルール自体は本筋じゃないので。

コード

Github 上に置いたので、詳しくはそちらを参照してください。
https://github.com/KazukiNagasawa/boostnumpy_lifegame
( ブログ書くの忘れて3ヵ月放置していたから github の投稿日時と乖離があるの… )

なお、以下 Ubuntu OS、Intel I7 マシンで実行しています。

Python 版

numpy 使ってライフゲームを実行し、OpenCV で描画したものを動画キャプチャしてみました。それをアニメーション gif 表示。
やっぱり遅いです ( gif 作成時に fps 調整している訳ではないですよ。 )

( only python )
f:id:kazuki_nagasawa:20180719130533g:plain

Boost.numpy 版

ライフゲーム実装部をすべて C++ (Boost.numpy) に移してみました。処理に違いが出ないよう、できるだけ Python 実装に寄せた形で作成しています。
同サイズの実行ですが、爆速になりました!!

( boost numpy )
f:id:kazuki_nagasawa:20180719130549g:plain

おまけ

Boost.numpy での実行が爆速になったので、調子に乗って Color 版を作ってみました。
RGB 間のつながりがなく、3つのライフゲームが RGB レイヤで重なって表示されているだけなのであまり面白くないですが…。
( 繋がり意識して3次元的なライフゲーム作ってみても面白いかも。 )

f:id:kazuki_nagasawa:20180719134249g:plain

Cython で Python ⇔ C++ のインタフェースを作る

以前、Python と C++ を繋ぐためには SWIG を使ってたが、
Cython 0.13 以降では Cython 自身で Python と C++ を繋ぐことができる。 (2018/05時点で 0.28)

この機能を試してみた。
ここまで来てようやく cythonize を知ったので、ビルドはそちらで。

環境前提

  • Ubuntu 16.04
    • Python 3.5
    • Cython 0.28
    • その他必要なものはインストール済みとする。

サンプル概要

2つのクラスを作成し、連携して動作させてみる簡単なサンプル。
  1. file_reader ... 1行1整数のデータを読み込んでC++メモリ上で保持。
  2. calculator ... file_reader をメンバとして持ち、中のデータを使って計算。

ファイル構成

- setup.py ... ビルド設定ファイル
- file_read.pyx ... cython ソースコードファイル
- FileReader.cpp ... C++ クラス実装
- FileReader.h ... C++ クラス定義
- test.txt ... サンプルデータファイル

実装

setup.py

cythonize を使ってビルドするための設定を定義
0.17 以降は
  - 基本的な設定 → setup.py
  - C++コードとのリンク情報 → .pyx
に区分けされている。

from distutils.core import setup
from Cython.Build import cythonize

setup(
    name = "file_read_app",
    ext_modules = cythonize("*.pyx"),
    extra_compile_args=["-std=c++11"],
    extra_link_args=["-std=c++11"],
)

(2018/05/31追記)
実は上記C++11設定は効いていなかったことが判明。。
以下 export したのが効いてた様子。ビルド前に export で設定追加しておく。
上記設定は古いっぽい?

$ export CFLAGS='-std=c++11' 

Tips

  • extra_compile_args や extra_link_args でオプションを追加。C++11設定を追加しておく。

C++ コード

C++ 側で実際の処理を作る。

FileReader.h

#include <fstream>
#include <iostream>
#include <string>
#include <vector>

namespace reader {

    class FileReader {
    public :
        FileReader(std::string filePath);
        ~FileReader();

    public :
        int getSize();
        int getData(int index);
        void add(int datum);

    public : 
        std::string filePath;

    private :
        std::vector<int> data;
    };


    class DataCalc {
    public :
        DataCalc(FileReader *fileReader);
        ~DataCalc();

    public :
        int sum();

    private :
        FileReader *fileReader;

    };

}

FileReader.cpp

#include "FileReader.h"

using namespace reader;

FileReader::FileReader(std::string filePath)
{
    // Read file and save data
    std::cout << "Read file : " << filePath << std::endl;

    this->data = std::vector<int>();

    std::ifstream ifs(filePath.c_str());
    if (ifs.fail()) {
        std::cout << "Error : Failed to open file." << std::endl;
        return;
    }

    std::string str;
    while (getline(ifs, str)) {
        data.push_back(std::stoi(str));
    }

}

FileReader::~FileReader()
{}

int FileReader::getSize()
{
    return this->data.size();
}

int FileReader::getData(int index)
{
    if (index >= this->data.size()) {
        std::cout << "Error: Index number too large (" << index << " < " << this->data.size() << ")." << std::endl;
        return -1;
    }

    return this->data[index];
}

void FileReader::add(int datum)
{
    this->data.push_back(datum);
}

DataCalc::DataCalc(FileReader *fileReader)
{
    this->fileReader = fileReader;
}

DataCalc::~DataCalc()
{}

int DataCalc::sum()
{
    int ret = 0;
    int size = this->fileReader->getSize();
    for (int i = 0; i < size; i++) {
        ret += this->fileReader->getData(i);
    }

    return ret;
}

Cython コード

Cython 上では C++ のクラスを呼ぶため、こちらでもクラス定義する必要ある (これだけちょっと面倒。。。)
Python ⇔ C++ のインタフェース定義もここで。

file_read.pyx

# distutils: language = c++
# distutils: sources = FileReader.cpp

from libcpp.string cimport string


cdef extern from "FileReader.h" namespace "reader" :
    cdef cppclass FileReader :
        FileReader(string) except +
        int getSize()
        int getData(int)
        void add(int)

    cdef cppclass DataCalc :
        DataCalc(FileReader*) except +
        int sum()


cdef class PyFileReader :
    cdef FileReader *thisptr

    def __cinit__(self, bytes path) :
        cdef string s_path = path
        self.thisptr = new FileReader(s_path)

    def __dealloc__(self) :
        del self.thisptr

    def get_size(self) :
        return self.thisptr.getSize()

    def get_data(self, int index) :
        return self.thisptr.getData(index)

    def add(self, int datum) :
        self.thisptr.add(datum)


cdef class PyCalc :
    cdef DataCalc *thisptr

    def __cinit__(self, PyFileReader file_reader) :
        self.thisptr = new DataCalc(file_reader.thisptr)

    def __dealloc__(self) :
        del self.thisptr

    def sum(self) :
        return self.thisptr.sum()

Tips :

  • 「except +」はおまじない的に付けとけばよい (本家ページに理由書いてある)
  • 文字列の扱い
    • Python から入ってくるものは (型推論に期待しても良いがせっかくなので) bytes 型で受け取る。
    • C++ には string で渡したいので、変換かける。

ビルド

ビルドしてエラー起きなければ OK 。
「-i」付けることで、ビルド実行したディレクトリ内に .so ファイルが生成される。

python3 setup.py build_ext

実行

test.txt

30
40
50
110

ipython で実行

$ ipython3
Python 3.5.2 (default, Nov 23 2017, 16:37:01) 
Type 'copyright', 'credits' or 'license' for more information
IPython 6.3.1 -- An enhanced Interactive Python. Type '?' for help.

In [1]: import file_read

In [2]: reader = file_read.PyFileReader(b"test.txt")
Read file : test.txt

In [3]: reader.get_size()
Out[3]: 4

In [4]: reader.get_data(2)
Out[4]: 50

In [5]: c = file_read.PyCalc(reader)

In [6]: c.sum()
Out[6]: 230

できた!!

トラブルシューティング的な

  • XXXX.pyx と XXXX.cpp (C++ソースコード) の名前を同じにしてはいけない。
    • pyx コンパイル時に XXXX.cpp が作り出され、重複定義と言われてしまう。
  • コンパイルに成功したが、python で import 時に「undefined symbol」のエラーで読み込み失敗。
    • クラス内デストラクタを定義したはいいものの実装していなかったのが原因。定義と実装揃ってないとエラー出る。(わかりやすいエラー出してくれ。。。)

その他

参照

Boost.numpy ことはじめ その2 (戻り値)

前回まででPythonからサンプルを使ってみることができるようになった。

今回は、2次元numpy配列をC++側で作成し、Python側に戻せるようにしてみる。

サンプル

「行列サイズを指定し、そのサイズにあった2次元numpy行列を返す」サンプルを作ってみる。

ディレクトリ構成

<root directory>
    - sample.cpp
    - CMakeLists.txt

sample.cpp

np::zeros を使ってゼロ配列を作成し、そこに値を入れ込む形で実装する。(場合によってはもっと高速な方法あるかもだけどとりあえず。)
2次元ができてしまえば、1次元はもっと簡単なので省略。

ポイント :

  • np::zeros の引数は「shape のサイズ、shape、タイプ」。基本的にはPythonのものと同じ。
  • 1次元vectorでデータを用意し、std::copyでデータを移す。
#include "boost/python/numpy.hpp"
#include <stdexcept>
#include <algorithm>

namespace p = boost::python;
namespace np = boost::python::numpy;

std::vector<double> func(int row, int col)
{
    std::vector<double> v;

    int count = 0;
    for (int y = 0; y < row; y++) {
        for (int x = 0; x < col; x++) {
            v.push_back(count++);
        }
    }

    return v;
}

np::ndarray retvec(int row, int col)
{
    std::vector<double> v = func(row, col);
    Py_intptr_t shape[2] = {row, col};
    np::ndarray result = np::zeros(2, shape, np::dtype::get_builtin<double>());
    std::copy(v.begin(), v.end(), reinterpret_cast<double*>(result.get_data()));
    return result;
}

/* BOOST_PYTHON_MODULE の引数は .so 名 */
BOOST_PYTHON_MODULE(libsample) {
    Py_Initialize();
    np::initialize();
    p::def("retvec", retvec);
}

ビルド

CMakeLists.txtは前回と同じなので割愛。

mkdir build
cd build
cmake ..
make

実行

ipython3で実行

ipython3
In [1]: import libsample as s

In [2]: import numpy as np

In [3]: a = s.retvec(3, 2)

In [4]: a
Out[4]: 
array([[0., 1.],
       [2., 3.],
       [4., 5.]])

3行2列の配列ができた!!

まとめ

前回と合わせて、
・ Boost.Numpy の実行
・ (C++で処理して) numpy 配列を戻す
ところまでできた。

ここまでで大体の処理はC++化して高速化できそう。

参照