仕事で、Pythonを使って画像処理をする必要に迫られました。OpenCVなどでできる範囲ならそれでいいのですが、独自アルゴリズムのフィルタはC/C++でないと実用的な速度が出ません。そこで、Python用のモジュールをC言語で作ってみました。
早く試してみたい方は、私のリポジトリをご利用ください。
https://github.com/soramimi/pymodule-image-filter
Python用自作モジュールの作り方
依存モジュールのインストール
最初のお試しはhelloworld
なので不要ですが、後の画像処理編ではnumpy、pillow、matplotlibを使用していますので、これらのモジュールがインストールされている必要があります。Ubuntuで開発する想定です。Pythonの開発用パッケージが必要です。
sudo apt install python3-dev python3-matplotlib
関数定義の構造体を書く
static PyMethodDef myMethods[] = {
{ "helloworld", helloworld, METH_NOARGS, "My helloworld function." },
{ NULL }
};
定義の内容は {(関数名),(関数へのポインタ),(引数受け渡し方法),(説明文)} の順に記述します。
METH_NOARGS
となっているのは、引数がない関数の場合です。引数を利用するにはMETH_VARARGS
とします。
モジュール定義の構造体を書く
static struct PyModuleDef mymodule = {
PyModuleDef_HEAD_INIT,
"mymodule",
"Python3 C API Module",
-1,
myMethods
};
モジュール名や説明などを書きます。
モジュールを初期化する関数を書く
PyMODINIT_FUNC PyInit_mymodule(void)
{
import_array();
return PyModule_Create(&mymodule);
}
import_array()
という関数呼び出しは、必要に応じて書きます。今回のテーマでは、画像処理で配列を扱うのでこれが必要です。配列を使わないなら不要です。配列APIを使用する全てのソースコードでimport_array()
を実行する必要があります。
関数本体を書く
static PyObject *helloworld(PyObject *self, PyObject *args)
{
fprintf(stderr, "Hello, world\n");
return Py_None;
}
最初のお試し関数なのでhelloworld
です。後ほど、画像処理を行うコードを実装していきます。
ビルド用スクリプトを書く
from distutils.core import setup, Extension
setup(name = 'mymodule', version = '1.0.0', ext_modules = [Extension('mymodule', ['mymodule.c'])])
ビルドする
python3 setup.py build_ext -i
長い名前の.so
ファイルができたら成功です。
$ ls *.so
mymodule.cpython-35m-x86_64-linux-gnu.so
呼び出し(Python側)プログラムを書く
import mymodule
mymodule.helloworld()
実行する
Pythonのプログラムとモジュールの.so
ファイルを同じディレクトリに置いた状態で実行します。
$ python3 main.py
Hello, world
画像処理フィルタを作る
例として、セピア調のフィルタをかける関数にします。関数名はhelloworld
からsepia
に変更しています。
Python側のプログラムは次のようになります。
from PIL import Image
import numpy as np
import matplotlib.pyplot as plt
import mymodule
im = np.array(Image.open('kamo.jpg'))
im = mymodule.sepia(im)
print(type(im))
print(im.dtype)
print(im.shape)
plt.imshow(im)
plt.show()
次のように動作します。
- 画像ファイルを読み込む
- フィルタをかける
- 配列の情報を表示する
- 画像を表示する
関数定義を以下のようにします。
{ "sepia", sepia, METH_VARARGS, "Sepia tone image filter" },
C言語側ソースの抜粋です。
static PyObject *sepia(PyObject *self, PyObject *args)
{
PyArrayObject *srcarray;
if (!PyArg_ParseTuple(args, "O", &srcarray)) {
fprintf(stderr, "invalid argument\n");
return Py_None;
}
最初の(と言っても1個だけ)引数は配列です。オブジェクトとして取得します。
入力の配列は、(高さ)×(幅)×(チャンネル)の3次元配列ですので、そうでなければエラーとします。チャンネル数は3(RGB)に限定します。
if (srcarray->nd != 3) {
fprintf(stderr, "invalid image\n");
return Py_None;
}
if (srcarray->dimensions[2] != 3) {
fprintf(stderr, "invalid image\n");
return Py_None;
}
画像のサイズを取得します。
int h = srcarray->dimensions[0];
int w = srcarray->dimensions[1];
フィルタ処理結果の画像を格納する配列を確保します。
npy_intp dims[] = { h, w, 3 };
PyObject *image = PyArray_SimpleNew(3, dims, NPY_UBYTE);
if (!image) {
fprintf(stderr, "failed to allocate array\n");
return Py_None;
}
(高さ)×(幅)×(チャンネル)の順番を間違えないでください。
配列として確保したオブジェクトは、そのまま配列構造体へのポインタにキャストできます。
PyArrayObject *dstarray = (PyArrayObject *)image;
フィルタをかけます。
for (int y = 0; y < h; y++) {
uint8_t const *src = (uint8_t const *)srcarray->data + y * w * 3;
uint8_t *dst = (uint8_t *)dstarray->data + y * w * 3;
for (int x = 0; x < w; x++) {
uint8_t r = src[x * 3 + 0];
uint8_t g = src[x * 3 + 1];
uint8_t b = src[x * 3 + 2];
r = pow(r / 255.0, 0.62) * 205 + 19;
g = pow(g / 255.0, 1.00) * 182 + 17;
b = pow(b / 255.0, 1.16) * 156 + 21;
dst[x * 3 + 0] = r;
dst[x * 3 + 1] = g;
dst[x * 3 + 2] = b;
}
}
オブジェクトを返して終了です。
return image;
}
以上です。
エラー処理は省略しています。不正な画像ファイルを受け取った時など、関数がNone
(Py_None
)を返した場合のために、実用のプログラムでは適切なエラー処理を行ってください。
冒頭にも書きましたが、全ソースコードは**こちら**で公開しています。