C++版SDKをPythonモジュール化する手順

# はじめに
C++版のSDKは公開されているけれどPython版がない、という時のために、具体例をもとにPythonバインディングを書く最低限の手順をまとめてみました。今回は、DJI社から公開されている[Thermal SDK](https://www.dji.com/jp/downloads/softwares/dji-thermal-sdk)をモジュール化してみます。

# pybind11
pybind11は、C++とPythonをバインドするためのライブラリです。詳しい使い方は[こちら](https://pybind11.readthedocs.io/en/stable/basics.html)をご参照ください。

# 手順
## pybind11のインストール
必要に応じて、venv等で仮想環境を構築して実施してください。

```bash
$ pip install pybind11
```

## パッケージフォルダの構成
予め前述のサイトからdji_thermal_sdk_v1.0_20201110.zipをダウンロードしておきます。

```bash
$ mkdir -p Thermal-SDK-Python/src
$ cd Thermal-SDK-Python
$ unzip dji_thermal_sdk_v1.0_20201110.zip
$ mv dji_thermal_sdk_v1.0_20201110/ sdk
```

## バインディングの実装
今回は、rjpegという名前のモジュールを作成します。PYBIND11_MODULEマクロを使用してクラスや関数を定義していきます。

```c++
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>

namespace py = pybind11;

PYBIND11_MODULE(rjpeg, m) {
    m.doc() = "Thermal SDK for Python";
    ...
}
```

### enum の定義
pybind11::enum_<>を使用して定義します。以下に一例を書きます。

```c++
PYBIND11_MODULE(rjpeg, m) {
    ...
    py::enum_<dirp_verbose_level_e>(m, "dirp_verbose_level_e")
    .value("DIRP_VERBOSE_LEVEL_NONE", DIRP_VERBOSE_LEVEL_NONE)
    .value("DIRP_VERBOSE_LEVEL_DEBUG", DIRP_VERBOSE_LEVEL_DEBUG)
    .value("DIRP_VERBOSE_LEVEL_DETAIL", DIRP_VERBOSE_LEVEL_DETAIL)
    .value("DIRP_VERBOSE_LEVEL_NUM", DIRP_VERBOSE_LEVEL_NUM)
    .export_values();
}
```

### struct/classの定義
pybind11::class_<>を使用して定義します。以下に一例を書きます。メンバ変数の配列はそのままでは登録できないので、stl::vectorを使用してstruct/classを定義し直しています。詳細は公式ドキュメントをご参照ください。

```c++
PYBIND11_MODULE(rjpeg, m) {
    ...
    py::class_<dirp_resolotion_t>(m, "dirp_resolotion_t")
    .def_readwrite("width", &dirp_resolotion_t::width)
    .def_readwrite("height", &dirp_resolotion_t::height);
}
```

### 関数の定義
def()関数に関数名、関数エントリポイント、説明を渡して定義します。引数や戻り値によっては元の関数をそのまま使えないor Pythonとして適切でない場合があるので、その場合はWrapper関数を作成します。インスタンスを戻り値とする場合、C++のメモリ管理を考慮する必要があります。詳細は、[Return value policies](https://pybind11.readthedocs.io/en/stable/advanced/functions.html)をご参照ください。

```c++
PYBIND11_MODULE(rjpeg, m) {
    ...
    m.def("dirp_create_from_rjpeg", &__dirp_create_from_rjpeg, "Create a new DIRP handle with specified R-JPEG binary data");
}
```

## setup.pyの作成
インストール用のスクリプトを作成します。SDKのlibdirp.soをリンクしています。

```py:setup.py
from setuptools import setup
from setuptools import find_packages
from pybind11.setup_helpers import Pybind11Extension, build_ext
from pybind11 import get_cmake_dir

import inspect
import os
import platform
import shutil
import sys

__version__ = "0.0.1"

def get_dirp_dir():
    system = platform.system().lower()
    arch = 'x64' if platform.architecture()[0] == '64bit' else 'x86'
    return 'sdk/tsdk-core/lib/' + system + '/release_' + arch

ext_modules = [
    Pybind11Extension(
        "rjpeg",
        ["src/rjpeg_module.cpp"],
        package_dir='rjpeg',
        define_macros = [('VERSION_INFO', '"' + __version__ + '"')],
        include_dirs=['sdk/tsdk-core/api'],
        library_dirs=[get_dirp_dir()],
        libraries=['dirp']
        ),
]

setup(
    name="rjpeg",
    version=__version__,
    author="Satoshi Tsutsumi",
    author_email="satoshi.tsutsumi@gmail.com",
    url="https://github.com/SatoshiTsutsumi/Thermal-SDK-Python",
    description="Thermal SDK for Python",
    long_description="",
    ext_modules=ext_modules,
    install_requires=['pybind11>=2.7'],
    extras_require={"test": "pytest"},
    cmdclass={"build_ext": build_ext},
    zip_safe=False
)
```

# 実行例
以下に、RJPEG形式の画像を読み込んで表示する例を示します。APIをそのままラップしたので画像を表示するのが手間です。もう少し使いやすいAPIにした方が良いかもしれません。
以下はThermal SDKに含まれるdataset/H20T/DJI_0001_R.JPGを表示した結果です。

```py
import numpy as np
from PIL import Image
from rjpeg import *

# Register app
dirp_register_app("DJI_TSDK")

with open("sdk/dataset/H20T/DJI_0001_R.JPG ", "rb") as f:
        # Get DIRP handle
        buf = f.read()
        h = dirp_create_from_rjpeg(buf, len(buf))

        # Get resolution
        resol = dirp_get_rjpeg_resolution(h)
        
        # Get colored image
        color_img_buf = dirp_process(h)
        color_img_array = np.array(color_img_buf, dtype=np.uint8)
        color_img_array = color_img_array.reshape([resol.height, resol.width, 3])
        color_img = Image.fromarray(color_img_array)
        color_img.show()
```

![DJI_001_R.JPG.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109483/d182a9ec-d0bc-4aa6-41d7-2555cb78beff.jpeg)

# 参照
上記のソースコードを公開しています。pybind11は初心者なので、ご指摘歓迎です。

[Thermal-SDK-Python](https://github.com/SatoshiTsutsumi/Thermal-SDK-Python)