Help us understand the problem. What is going on with this article?

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

関数定義の構造体を書く

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です。後ほど、画像処理を行うコードを実装していきます。

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

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. 画像を表示する

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

    { "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;
    }

最初の(と言っても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;
}

以上です。

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

冒頭にも書きましたが、全ソースコードはこちらで公開しています。

image.png

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした