pybind11で既存のC++ライブラリのPythonバインディングを作った話
この記事は,Qiitaの「Python Advent Calendar 2019」の12日目の記事です. 技術ブログをQiitaからこっちに移してから初めてのAdvent Calendar参加です.
モチベーション
近年のPythonを取り巻くコミュニティの発展により,Pythonから利用可能なパッケージが非常に多く開発されている. OpenCVやMATLABといった老舗のライブラリや言語もPythonとのインタフェースを備えるようになってきている. C++とPythonの間を繋ぐ方法のひとつとして,pybind11というライブラリが知られている.
pybind11を用いることで,C++のコードを書くだけでPythonのモジュールを作ることができる. さらに,自分で開発したライブラリだけでなく,既存のC++製のライブラリをpybind11でラップすることで 自分のPythonのパッケージの一部として取り込むことも可能である.
本稿では,pybind11を用いてC++のライブラリをPythonに取り込むまでの流れを
自分が作ったpylibfacedetection
を例に述べる.
これはC++で書かれた人の顔検出ライブラリであるlibfacedetectionのPythonバインディングである.
注意
- 本稿では,pybind11を使わない通常のPythonパッケージを作ったことがある程度の知識を前提にしている
- 筆者は本稿の内容をWindowsの環境でしか試していない
- 一か月以上前の作業内容を思い出しながらだらだら書いてるので全体的に記述が冗長になってる
pybind11について
pybind11についてはQiitaなどに先人たちの日本語記事がごろごろ転がってるのでここでは簡単な説明に留める.
pybind11はC++とPythonの双方向のやり取りを実現するためのヘッダーオンリーのライブラリ. 主に既成のC++のコードのPythonバインディングを作るためのもので,記法はBoost.Pythonに似てる.
色々なところで使われていて,筆者の知る範囲では例えばOpenPoseのPythonバインディングの実装にも使われている.
ここでは,pybind11を用いることで 「C++のコードを書くだけでPythonのパッケージを作ることができる」 と認識しておいてもらえれば良い. 本稿でもやっているような,Pythonのパッケージの一部のみをC++で実装する,みたいな器用なこともできる.
libfacedetectionについて
libfacedetectionはC++で実装された,畳込みニューラルネットワーク (CNN; Convolutional Neural Network) によって顔検出ができるライブラリである.
libfacedetectionによる顔検出をやってみた例はこちら.
例としてこのライブラリを選んだのは,適度に小規模なCMakeプロジェクトであることと, 動かせたら楽しそうだなーという適当な理由なので,CMakeListsちゃんと書いてあるものならなんでもよい.
やること
libfacedetectionをPythonから呼び出して検出結果を取得・利用できるようになるのを目標とする. また,本稿では以下の2点を重視した:
- 元のlibfacedetectionのコードには直接手を入れずに静的ライブラリとして利用するだけにする
- libfacedetectionをgit submoduleとして管理し,更新があったときに反映しやすくするため
pip install
だけでCMakeプロジェクトのビルドを実行し,インストールまで完了するようにする- 既成のC++のCMakeプロジェクトがpip叩くだけでインストールできて嬉しい!という夢の実現
Pythonから呼び出すときのコードは以下のようになる.
OpenCVで画像を読み込み,顔の検出結果をresults
に入れる.
results
の各要素について,スコアが50%以上ならその位置に枠を描画する,という感じ.
import cv2 from pylibfacedetection import FaceDetector filepath = './path/to/image' image = cv2.imread(filepath) detector = FaceDetector() results = detector.detect(image) image_out = image.copy() for result in results: x, y, w, h, p, a = result if p > 50: cv2.rectangle(image_out, (x, y), (x + w, y + h), (0, 255, 0), thickness=4) print(results) cv2.namedWindow('result', cv2.WINDOW_NORMAL) cv2.imshow('result', image_out) cv2.waitKey(0) cv2.destroyAllWindows()
pybind11とC++でPythonモジュールを作る最小の例
筆者はいきなりlibfacedetectionを弄ろうとして酷い目に遭ったので, 読者の皆さんはまず簡単なサンプルでC++とPython間の連携を見てほしい.
pybind11の公式のリポジトリに,C++でPythonモジュールを記述したCMakeプロジェクトのサンプルが公開されている.とてもありがたい.
このプロジェクトはcmake_example
という名前のPythonモジュールとしてインストール可能なので,
READMEの記述に従ってインストールしてみるといいだろう.
CIの設定やドキュメントなどを除いた最低限必要なファイルは以下:
pybind11/ # pybind11 (https://github.com/pybind/pybind11) ... # をsubmoduleとして取り込んだもの src/ main.cpp # Pythonモジュールを記述するcppファイル CMakeLists.txt # プロジェクトに含まれてるC++プログラムをCMake使ってビルドするための設定 setup.py # `pip install`で実行されるやつ.今回はこいつがCMakeの実行も担う.
コードの規模は非常に小さいので,これらを精読しておくと自前のプロジェクトで同じことをするときに役に立つと思う.
setup.py
の中をみてみると,通常のPythonパッケージのCython拡張で使われる
setuptools.Extension
, setuptools.command.build_ext.build_ext
それぞれの
CMake版である,CMakeExtension
とCMakeBuild
が用意されている.
setup.py
が実行されるとCMakeが呼び出され,CMakeLists.txtはpybind11の中にあるCMakeLists.txtを呼び出す (add_subdirectory(pybind11)
の行がこれに相当).
この中で定義されている関数pybind11_add_module
を用いてsrc/main.cpp
などのC++ファイルをプロジェクトに取り込む (pybind11_add_module(cmake_example src/main.cpp)
),というイメージ.
src/main.cpp
の中には足し算と引き算を行う関数add
とsubtract
がそれぞれ定義されている.
add
の方の記述だけ抜粋すると,次のようになっている.
解説をコメントとして入れてるので参照されたい.
#include <pybind11/pybind11.h> int add(int i, int j) { return i + j; } namespace py = pybind11; // ここにある`cmake_example`の部分がPythonから呼び出されるモジュール名になる PYBIND11_MODULE(cmake_example, m) { // Python側の関数`add`を3-5行目の`add`のように定義する m.def("add", &add); }
このモジュールをPythonにインストールすると,以下のように関数を呼び出すことができる.
import cmake_example cmake_example.add(1, 2) # -> 3
この例では単体のPythonモジュールの全体をC++で記述したが, 複数のモジュールを含むようなPythonパッケージのうち一部のモジュールをC++で書くこともできる. これは次の節で紹介する.
既存のC++ライブラリ (libfacedetection) をPythonパッケージに取り込む
前節では簡単な例でpybind11とC++でPythonモジュールを記述し,CMakeを用いてビルドする例を見た. 今度は,既存のC++ライブラリ (libfacedetection) を自分のパッケージに取り込む方法を見る.
ファイルの構成は以下のようになってる.
libfacedetection/ # libfacedetectionをgit submoduleで取り込んだもの ... pybind11/ # pybind11をgit submoduleで取り込んだもの ... pylibfacedetection/ # Pythonパッケージのディレクトリ ... src/ # Pythonパッケージの中のC++部分を記述するcppファイルを入れるディレクトリ ... CMakeLists.txt # プロジェクトに含まれてるC++プログラムをCMake使ってビルドするための設定 setup.py # `pip install`で実行されるやつ.CMakeの実行も担う.
ここで増えたのは,既存のC++ライブラリであるlibfacedetection
とこれから作る
Pythonパッケージpylibfacedetection
のディレクトリである.
libfacedetection
は静的ライブラリとしてビルドすれば自前のプログラムから呼び出すことができる.
C++から呼び出すサンプルがlibfacedetection
のリポジトリに用意されている.
このサンプルプログラムを適当に関数として書き直し,pybind11を用いてPythonパッケージの関数にしてしまえばPythonから呼び出すことができる. 関数化した結果は次のコードのようになる.この関数は画像をNumpy配列として受け取り,顔検出結果をNumpy配列として返す. C++でNumpyの操作を書くのがしんどいなら,呼び出し元のPythonモジュール内で簡単なデータ構造 (一次元のchar配列とか) に変換しておいてからC++側に渡すのでもいいかも.
関数ができたら,cmake_example
のときと同じように,作った関数をPythonモジュールに登録する.
このC++製Pythonモジュールの名前はclibfacedetection
とする.これは,あとでpylibfacedetection
のサブモジュールとして配置する.
#include <pybind11/pybind11.h> #include <pybind11/numpy.h> #include "facedetector.h" namespace py = pybind11; PYBIND11_MODULE(clibfacedetection, m) { m.doc() = "Python binding of libfacedetection"; py::class_<FaceDetector>(m, "FaceDetector") .def(py::init<>()) .def("detect", (py::array_t<int> (FaceDetector::*)(py::array_t<unsigned char>)) &FaceDetector::detect); };
次にCMakeの設定を書く.cmake_example
のときと異なり,libfacedetection
もビルドに含めないといけない.
詳しくはCMakeLists.txt
を読んでもらう方がわかりやすいと思う.
ここで大事なのは,add_subdirectory(libfacedetection)
でlibfacedetectionをプロジェクトに追加することと,
target_link_libraries(clibfacedetection PRIVATE facedetection)
でclibfacedetectionにlibfacedetectionをリンクすること
(これにより,clibfacedetectionはlibfacedetectionの関数を呼び出して使うことができる).
cmake_minimum_required(VERSION 3.15) project(clibfacedetection) add_subdirectory(pybind11) pybind11_add_module(clibfacedetection src/main.cpp src/facedetector.cpp) add_subdirectory(libfacedetection) include_directories(clibfacedetection src libfacedetection/src ) target_link_libraries(clibfacedetection PRIVATE facedetection)
setup.py
の内容はcmake_example
のときとほぼ同じだが,ひとつだけ異なるのはCMakeExtension
の引数.
CMakeExtension('pylibfacedetection.backend.clibfacedetection')
のようにpylibfacedetectionのサブモジュールとなる名前にしておき,
これと同じディレクトリ構造をpylibfacedetection
ディレクトリの中に作っておく必要がある.
setup( name="pylibfacedetection", ... cmdclass={'build_ext': CMakeBuild}, ext_modules=[CMakeExtension('pylibfacedetection.backend.clibfacedetection')], ... )
今回は(追加の機能が思いつかなかったので)pylibfacedetectionにはC++で書いたモジュールしか含まれていないが, このような作りをしておくことでPythonパッケージの中にPythonのコードとC++のコードを共存させやすくなる.
ここまでやれば,あとはpython setup.py install
(開発用ならpython setup.py develop
) でプロジェクトのビルドとインストールができる.
Pythonからの呼び出しは以下のコードのようにできる(上に書いたやつの再掲).
import cv2 from pylibfacedetection import FaceDetector filepath = './path/to/image' image = cv2.imread(filepath) detector = FaceDetector() results = detector.detect(image) image_out = image.copy() for result in results: x, y, w, h, p, a = result if p > 50: cv2.rectangle(image_out, (x, y), (x + w, y + h), (0, 255, 0), thickness=4) print(results) cv2.namedWindow('result', cv2.WINDOW_NORMAL) cv2.imshow('result', image_out) cv2.waitKey(0) cv2.destroyAllWindows()
まとめ
pybind11を用いて既存のC++ライブラリをPythonパッケージの中に入れる方法を紹介した. また,git submoduleを使うことで外部のライブラリのバージョン管理を簡単にし, CMakeを利用することでビルドの過程の記述を柔軟にできるようにした.
余談
個人的にはこの方法使ってopenFrameworksの Pythonバインディング作れたら嬉しいなーと思っている. PythonのREPL開いて即クリエイティブコーディング開始!みたいな感じで. 関数多すぎるのとpybind11でのテンプレートの扱いがわからなくてつらい….