反面教師あり学習

*/

反面教師あり学習

Negative Supervised Learning

pybind11で既存のC++ライブラリのPythonバインディングを作った話

この記事は,Qiitaの「Python Advent Calendar 2019」の12日目の記事です. 技術ブログをQiitaからこっちに移してから初めてのAdvent Calendar参加です.

モチベーション

近年のPythonを取り巻くコミュニティの発展により,Pythonから利用可能なパッケージが非常に多く開発されている. OpenCVMATLABといった老舗のライブラリや言語もPythonとのインタフェースを備えるようになってきている. C++Pythonの間を繋ぐ方法のひとつとして,pybind11というライブラリが知られている.

pybind11を用いることで,C++のコードを書くだけでPythonのモジュールを作ることができる. さらに,自分で開発したライブラリだけでなく,既存のC++製のライブラリをpybind11でラップすることで 自分のPythonのパッケージの一部として取り込むことも可能である.

本稿では,pybind11を用いてC++のライブラリをPythonに取り込むまでの流れを 自分が作ったpylibfacedetectionを例に述べる. これはC++で書かれた人の顔検出ライブラリであるlibfacedetectionのPythonバインディングである.

github.com

注意

  • 本稿では,pybind11を使わない通常のPythonパッケージを作ったことがある程度の知識を前提にしている
  • 筆者は本稿の内容をWindowsの環境でしか試していない
  • 一か月以上前の作業内容を思い出しながらだらだら書いてるので全体的に記述が冗長になってる

pybind11について

pybind11についてはQiitaなどに先人たちの日本語記事がごろごろ転がってるのでここでは簡単な説明に留める.

pybind11はC++Pythonの双方向のやり取りを実現するためのヘッダーオンリーのライブラリ. 主に既成のC++のコードのPythonバインディングを作るためのもので,記法はBoost.Pythonに似てる.

github.com

色々なところで使われていて,筆者の知る範囲では例えばOpenPoseのPythonバインディングの実装にも使われている.

github.com

ここでは,pybind11を用いることで C++のコードを書くだけでPythonのパッケージを作ることができる」 と認識しておいてもらえれば良い. 本稿でもやっているような,Pythonのパッケージの一部のみをC++で実装する,みたいな器用なこともできる.

libfacedetectionについて

libfacedetectionはC++で実装された,畳込みニューラルネットワーク (CNN; Convolutional Neural Network) によって顔検出ができるライブラリである.

github.com

libfacedetectionによる顔検出をやってみた例はこちら.

f:id:eqseqs:20191212020115g:plain
元動画:https://www.youtube.com/watch?v=CWSLXXDR8XU

例としてこのライブラリを選んだのは,適度に小規模な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プロジェクトのサンプルが公開されている.とてもありがたい.

github.com

このプロジェクトは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版である,CMakeExtensionCMakeBuildが用意されている.

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の中には足し算と引き算を行う関数addsubtractがそれぞれ定義されている. 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) を自分のパッケージに取り込む方法を見る.

github.com

ファイルの構成は以下のようになってる.

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リポジトリに用意されている.

libfacedetection/libfacedetectcnn-example.cpp at 54b8e036b299b4763afa6a74af2502a8b13eb0ad · ShiqiYu/libfacedetection · GitHub

このサンプルプログラムを適当に関数として書き直し,pybind11を用いてPythonパッケージの関数にしてしまえばPythonから呼び出すことができる. 関数化した結果は次のコードのようになる.この関数は画像をNumpy配列として受け取り,顔検出結果をNumpy配列として返す. C++でNumpyの操作を書くのがしんどいなら,呼び出し元のPythonモジュール内で簡単なデータ構造 (一次元のchar配列とか) に変換しておいてからC++側に渡すのでもいいかも.

pylibfacedetection/facedetector.cpp at dc69895b48d38f8e956b148980e06d24b9558712 · eqs/pylibfacedetection · GitHub

関数ができたら,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);
};

pylibfacedetection/main.cpp at dc69895b48d38f8e956b148980e06d24b9558712 · eqs/pylibfacedetection · GitHub

次に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)

pylibfacedetection/CMakeLists.txt at dc69895b48d38f8e956b148980e06d24b9558712 · eqs/pylibfacedetection · GitHub

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/setup.py at dc69895b48d38f8e956b148980e06d24b9558712 · eqs/pylibfacedetection · GitHub

今回は(追加の機能が思いつかなかったので)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でのテンプレートの扱いがわからなくてつらい….