Pythonで読み込んだ画像にフィルタをかけるモジュールをC言語で作った

仕事で、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`

### 関数定義の構造体を書く

``` .c
static PyMethodDef myMethods[] = {
	{ "helloworld", helloworld, METH_NOARGS, "My helloworld function." },
	{ NULL }
};
```

定義の内容は {（関数名）,（関数へのポインタ）,（引数受け渡し方法）,（説明文）} の順に記述します。

`METH_NOARGS`となっているのは、引数がない関数の場合です。引数を利用するには`METH_VARARGS`とします。

### モジュール定義の構造体を書く

``` .c
static struct PyModuleDef mymodule = {
	PyModuleDef_HEAD_INIT,
	"mymodule",
	"Python3 C API Module",
	-1,
	myMethods
};
```

モジュール名や説明などを書きます。

### モジュールを初期化する関数を書く

``` .c
PyMODINIT_FUNC PyInit_mymodule(void)
{
	import_array();
	return PyModule_Create(&mymodule);
}
```

`import_array()`という関数呼び出しは、必要に応じて書きます。今回のテーマでは、画像処理で配列を扱うのでこれが必要です。配列を使わないなら不要です。配列APIを使用する全てのソースコードで`import_array()`を実行する必要があります。

### 関数本体を書く

``` .c
static PyObject *helloworld(PyObject *self, PyObject *args)
{
	fprintf(stderr, "Hello, world\n");
	return Py_None;
}
```

最初のお試し関数なので`helloworld`です。後ほど、画像処理を行うコードを実装していきます。

### ビルド用スクリプトを書く

``` setup.py
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側）プログラムを書く

``` main.py
import mymodule

mymodule.helloworld()
```

### 実行する

Pythonのプログラムとモジュールの`.so`ファイルを同じディレクトリに置いた状態で実行します。

```
$ python3 main.py 
Hello, world
```

## 画像処理フィルタを作る

例として、セピア調のフィルタをかける関数にします。関数名は`helloworld`から`sepia`に変更しています。

Python側のプログラムは次のようになります。

``` main.py
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()
```
次のように動作します。

1. 画像ファイルを読み込む
2. フィルタをかける
3. 配列の情報を表示する
4. 画像を表示する

関数定義を以下のようにします。

``` .c
	{ "sepia", sepia, METH_VARARGS, "Sepia tone image filter" },
```

C言語側ソースの抜粋です。

``` mymodule.c
static PyObject *sepia(PyObject *self, PyObject *args)
{
	PyArrayObject *srcarray;
	if (!PyArg_ParseTuple(args, "O", &srcarray)) {
		fprintf(stderr, "invalid argument\n");
		return Py_None;
	}
```

最初の（と言っても１個だけ）引数は配列です。オブジェクトとして取得します。

入力の配列は、（高さ）×（幅）×（チャンネル）の３次元配列ですので、そうでなければエラーとします。チャンネル数は３（RGB）に限定します。

``` .c
	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;
	}
```

画像のサイズを取得します。

``` .c
	int h = srcarray->dimensions[0];
	int w = srcarray->dimensions[1];
```

フィルタ処理結果の画像を格納する配列を確保します。

``` .c
	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;
	}
```

（高さ）×（幅）×（チャンネル）の順番を間違えないでください。

配列として確保したオブジェクトは、そのまま配列構造体へのポインタにキャストできます。

``` .c
	PyArrayObject *dstarray = (PyArrayObject *)image;
```

フィルタをかけます。

``` .c
	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;
		}
	}
```

オブジェクトを返して終了です。

``` .c
	return image;
}
```

以上です。

エラー処理は省略しています。不正な画像ファイルを受け取った時など、関数が`None`（`Py_None`）を返した場合のために、実用のプログラムでは適切なエラー処理を行ってください。

冒頭にも書きましたが、全ソースコードは**[こちら](https://github.com/soramimi/pymodule-image-filter
)**で公開しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61222/77fca117-8c92-a8e1-14ac-6745941b5163.png)