前回 、Numpyを利用したNWG法の実装を行なった。今回はこれを高速化していく。

Cython

Pythonを高速化する上では様々な方法があるように思う。Numpyを使用するのもその一例であるが、それ以外にはC言語のコードを呼び出すというものがある。

C、C++のバインディングライブラリとしては、pybind11やCythonなどが挙げられる。今回は既にPythonに存在するコードを書き換えるのでCythonを利用した。

元のコード

まずは前回作成したPythonのコードである。

import numpy as np

def NWG(img:np.ndarray, symmetric:bool=False) -> np.ndarray:
    src = np.copy(img)//255

    # zero padding
    ROW, COLUMN = src.shape[0]+2, src.shape[1]+2
    pad = np.zeros((ROW, COLUMN))
    pad[1:ROW-1, 1:COLUMN-1] = src
    src = pad

    switch = True
    while True:
        r, c = src.nonzero()
        nei = np.array((src[r-1, c], src[r-1, c+1], src[r, c+1],
                        src[r+1, c+1], src[r+1, c], src[r+1, c-1],
                        src[r, c-1], src[r-1, c-1]))

        # condition 1
        nei_sum = np.sum(nei, axis=0)
        cond1 = np.logical_and(2 <= nei_sum, nei_sum <= 6)

        # condition 2
        nei = np.concatenate([nei, np.array([nei[0]])], axis=0)
        cond2 = np.zeros(nei.shape[1], dtype=np.uint8)
        for i in range(1, 9):
            cond2 += np.array(
                np.logical_and(nei[i-1] == 0, nei[i] == 1), dtype=np.uint8)
        cond2 = cond2 == 1
        nei = nei[0:8]

        # condition 3
        if symmetric:
            if switch:
                c3a = np.logical_and(
                    nei[0]+nei[1]+nei[2]+nei[5] == 0, nei[4]+nei[6] == 2)
                c3b = np.logical_and(
                    nei[2]+nei[3]+nei[4]+nei[7] == 0, nei[0]+nei[6] == 2)
                cond3 = np.logical_or(c3a, c3b)
            else:
                c3c = np.logical_and(
                    nei[1]+nei[4]+nei[5]+nei[6] == 0, nei[0]+nei[2] == 2)
                c3d = np.logical_and(
                    nei[0]+nei[3]+nei[6]+nei[7] == 0, nei[2]+nei[4] == 2)
                cond3 = np.logical_or(c3c, c3d)
        else:
            c3a = np.logical_and(
                nei[0]+nei[1]+nei[2]+nei[5] == 0, nei[4]+nei[6] == 2)
            c3b = np.logical_and(
                nei[2]+nei[3]+nei[4]+nei[7] == 0, nei[0]+nei[6] == 2)
            cond3 = np.logical_or(c3a, c3b)    

        # condition 4
        if switch:
            cond4 = (nei[2]+nei[4])*nei[0]*nei[6] == 0
        else:
            cond4 = (nei[0]+nei[6])*nei[2]*nei[4] == 0

        cond = np.logical_and(cond1, np.logical_or(cond2, cond3))
        cond = np.logical_and(cond, cond4)
        if True in cond:
            switch = not switch
        else:
            return (src[1:ROW-1, 1:COLUMN-1]*255).astype(np.uint8)

        src[r[cond], c[cond]] = 0

今回は時間を計測するにあたり、以下のような二値画像を作成した。

これをNWG細線化するとこのようになる。

  • Non Symmetric
  • Symmetric

時間計測は細線化を100回行ったものを比率で評価する。

Pythonの実装でSymmetricとNon Symmetricの時間を比較した結果は以下に示す。

Symmetric / Non symmetricNon symmetric / Symmetric
99.00%101.01%

ほとんど同じであった。

高速化その1(とりあえずコンパイルしてみる)

CythonではNumpyを使用することができる。そこで、まずは簡単にできることから始める。とりあえずコンパイルしてみた。※コードは同じ

Cython nofix symmetric / Python symmetricCython nofix non symmetric / Python non symmetric
97.05%107.77%

非常に若干の高速化ができた(?)、誤差の可能性もありそうではあるが特に有意差があるかについては検討しない。

高速化その2(型を設定する)

次に、Cythonの高速化として一般的である型の指定を行った。型の指定を行うと以下のようなプログラムになる。

import numpy as np
cimport numpy as cnp
cimport cython

DTYPE = np.uint8
ctypedef cnp.uint8_t DTYPE_t

@cython.boundscheck(False)
@cython.wraparound(False)
cpdef cnp.ndarray[DTYPE_t, ndim=2] NWG(cnp.ndarray[DTYPE_t, ndim=2] img, int symmetric=False):
    cdef cnp.ndarray[DTYPE_t, ndim=2] src = np.copy(img) // 255
    cdef int ROW = src.shape[0]+2
    cdef int COLUMN = src.shape[1]+2

    cdef cnp.ndarray[DTYPE_t, ndim=2] pad = np.zeros((ROW, COLUMN), dtype=DTYPE)

    cdef cnp.ndarray[DTYPE_t, ndim=2] nei
    cdef tuple temp
    cdef cnp.ndarray[cnp.int64_t, ndim=1] r, c
    cdef cnp.ndarray[cnp.uint8_t, ndim=1] nei_sum
    cdef cnp.ndarray[DTYPE_t, ndim=1] cond1, cond2, c3a, c3b, c3c, c3d, cond3, cond4, cond
    cdef DTYPE_t switch = True

    pad[1:ROW-1, 1:COLUMN-1] = src
    src = pad
    
    while True:
        r, c = src.nonzero()
        nei = np.array((src[r-1, c], src[r-1, c+1], src[r, c+1],
                        src[r+1, c+1], src[r+1, c], src[r+1, c-1],
                        src[r, c-1], src[r-1, c-1]))

        # condition 1
        nei_sum = np.sum(nei, axis=0, dtype=DTYPE)
        cond1 = np.logical_and(2 <= nei_sum, nei_sum <= 6)

        # condition 2
        nei = np.concatenate([nei, np.array([nei[0]])], axis=0)
        cond2 = np.zeros(nei.shape[1], dtype=DTYPE)
        for i in range(1, 9):
            cond2 += np.array(
                np.logical_and(nei[i-1] == 0, nei[i] == 1), dtype=np.uint8)
        cond2 = cond2 == 1
        nei = nei[0:8]

        # condition 3
        if symmetric:
            if switch:
                c3a = np.logical_and(
                    nei[0]+nei[1]+nei[2]+nei[5] == 0, nei[4]+nei[6] == 2)
                c3b = np.logical_and(
                    nei[2]+nei[3]+nei[4]+nei[7] == 0, nei[0]+nei[6] == 2)
                cond3 = np.logical_or(c3a, c3b)
            else:
                c3c = np.logical_and(
                    nei[1]+nei[4]+nei[5]+nei[6] == 0, nei[0]+nei[2] == 2)
                c3d = np.logical_and(
                    nei[0]+nei[3]+nei[6]+nei[7] == 0, nei[2]+nei[4] == 2)
                cond3 = np.logical_or(c3c, c3d)
        else:
            c3a = np.logical_and(
                nei[0]+nei[1]+nei[2]+nei[5] == 0, nei[4]+nei[6] == 2)
            c3b = np.logical_and(
                nei[2]+nei[3]+nei[4]+nei[7] == 0, nei[0]+nei[6] == 2)
            cond3 = np.logical_or(c3a, c3b)

        # condition 4
        if switch:
            cond4 = (nei[2]+nei[4])*nei[0]*nei[6] == 0
        else:
            cond4 = (nei[0]+nei[6])*nei[2]*nei[4] == 0

        cond = np.logical_and(cond1, np.logical_or(cond2, cond3))#1<b<7 and (a==1 or c==1)
        cond = np.logical_and(cond, cond4)
        if True in cond:
            switch = not switch
        else:
            return src[1:ROW-1, 1:COLUMN-1]*255

        src[r[cond], c[cond]] = 0
Cython symmetric / Python symmetricCython non symmetric / Python non symmetric
66.76%69.03%

Pythonからおよそ2/3の時間まで高速化できた。これだけでも十分な高速化ができたと思う。

高速化その3(Numpyを使用しない)

最後にNumpyを使用しないで高速化を試みた。結果、以下のようなプログラムになった。

import numpy as np
cimport numpy as cnp
cimport cython

DTYPE = np.uint8
ctypedef cnp.uint8_t DTYPE_t

@cython.boundscheck(False)
@cython.wraparound(False)
cpdef cnp.ndarray[DTYPE_t, ndim=2] NWG(cnp.ndarray[DTYPE_t, ndim=2] img, int symmetric=False):
    cdef cnp.ndarray[DTYPE_t, ndim=2] src = np.pad(np.copy(img) // 255, (1, 1), 'constant')
    cdef cnp.ndarray[DTYPE_t, ndim=2] Q
    cdef int ROW = src.shape[0]
    cdef int COLUMN = src.shape[1]

    cdef int x, y, g, h, i, c, d
    cdef int[9] p
    cdef int cond, cond1, cond2, cond3, cond4
    cdef int cond3a, cond3b, cond3c, cond3d

    # 初期化
    g = 1
    h = 1
    while h==1:
        Q = np.zeros_like(src)
        g = 1 - g
        h = 0

        for x in range(1,COLUMN-1):
            for y in range(1,ROW-1):
                if src[y,x]==1:
                    p[0] = src[y-1, x]
                    p[1] = src[y-1, x+1]
                    p[2] = src[y, x+1]
                    p[3] = src[y+1, x+1]
                    p[4] = src[y+1, x]
                    p[5] = src[y+1, x-1]
                    p[6] = src[y, x-1]
                    p[7] = src[y-1, x-1]
                    p[8] = p[0]

                    # condition 1
                    p_sum = p[0]+p[1]+p[2]+p[3]+p[4]+p[5]+p[6]+p[7]
                    cond1 = 1<p_sum and p_sum<7

                    # condition 2
                    a=0
                    for i in range(1, 9):
                        a += 1 if p[i-1]==0 and p[i]==1 else 0
                    cond2 = a==1

                    # condition 3
                    cond3a = p[0]+p[1]+p[2]+p[5]==0 and p[4]+p[6]==2
                    cond3b = p[2]+p[3]+p[4]+p[7]==0 and p[0]+p[6]==2
                    if symmetric:
                        cond3c = p[1]+p[4]+p[5]+p[6]==0 and p[0]+p[2]==2
                        cond3d = p[0]+p[3]+p[6]+p[7]==0 and p[2]+p[4]==2
                        c = cond3a or cond3b
                        d = cond3c or cond3d
                        cond3 = (1 - g)*c + g*d == 1
                    else:
                        cond3 = cond3a or cond3b
                        

                    # condition 4
                    if g==0:
                        cond4 = (p[2]+p[4])*p[0]*p[6] == 0
                    else:
                        cond4 = (p[0]+p[6])*p[2]*p[4] == 0
                    cond = cond1 and (cond2 or cond3) and cond4

                    if cond:
                        h=1
                    else:
                        Q[y,x]=1
        src = Q    
    return src[1:ROW-1, 1:COLUMN-1]*255
Cython symmetric / Python symmetricCython non symmetric / Python non symmetric
42.05%41.89%

先ほどより高速化ができた。Pythonと比較して2倍以上の高速化が達成できた。

最後に

今回はCythonを利用してNWG細線化の高速化を行なった。これらの関数はVitLib で公開している。使用したいだけであればこちらからインストールすれば使用することができるので、よかったらどうぞ。(高速化前と後での出力結果が一致することは確認しています。)