技術をかじる猫

適当に気になった技術や言語、思ったこと考えた事など。

画像を行列で処理する前準備

CNN で特徴的な動作に畳み込みっちゅう処理がある。

画像には局所性(隣接するピクセルの影響を受ける事)があるので、それを利用して画像の特徴を強調したりできる。
やってることはフィルタを用意して、画像に畳み込み計算を行う事。

以下の例みたいに、左上からフィルタを適用して 1 セル生成。したら、右に 1 セルずらして…と繰り返す。


    \left(
        \begin{array}{cc}
            1 & 0 \\
            0 & 1 \\
        \end{array}
    \right)
    \times
    \left(
        \begin{array}{cccc}
            2 & 2 & 1 & 2 \\
            1 & 1 & 2 & 1 \\
            1 & 2 & 1 & 0 \\
            1 & 0 & 0 & 1 \\
        \end{array}
    \right)
    \longrightarrow
    \left(
        \begin{array}{ccc}
            3 & 4 & 2 \\
            3 & 2 & 2 \\
            1 & 2 & 2 \\
        \end{array}
    \right)

見ての通り画像自体は小さくなる。

因みにこのずらす間隔をストライドと呼ぶ。
ストライド幅が大きいほど画像サイズは小さくなり、特徴が見逃されやすくなる。

で、この計算するとしたときに普通の画像に対して処理するのがしんどいなんで整形するのが一般的なのだそうだ。

im2col

画像に対して畳み込みを行うには形状がめんどくさいので以下みたいに配置しなおす。
これを im2col と呼ぶっぽい。


\left(
    \begin{array}{cc}
        2 & 2 \\
        1 & 1 \\
    \end{array}
\right)
\longrightarrow
\left(
    \begin{array}{c}
        2 \\
        2 \\
        1 \\
        1 \\
    \end{array}
\right)

例えば 2x2 のフィルタで 3x2 のデータに適用するとき、まずは im2col でこんな風にしてしまう


\left(
    \begin{array}{ccc}
        2 & 2 & 3 \\
        1 & 1 & 2 \\
    \end{array}
\right)
\longrightarrow
\left(
    \begin{array}{cc}
        2 & 2 \\
        2 & 3 \\
        1 & 1 \\
        1 & 2 \\
    \end{array}
\right)

こう変換すれば、フィルタをドット積で一括計算できる。

で、実際に変換と逆変換をやってみた。

画像は こちら のを使わせていただいた。

import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline

# 読み込んで表示しただけ
img = plt.imread('himawari.png')
plt.imshow(img)
plt.show()

f:id:white-azalea:20210322231756p:plain

import statistics
# カラー情報を捨ててモノクロに
arr_monochro = [[statistics.mean(cell) for cell in y] for y in img]
plt.imshow(arr_monochro, 'gray', vmin = 0, vmax = 1)
plt.show()

f:id:white-azalea:20210322231820p:plain

changed = np.array(arr_monochro)
changed.shape

(512, 512)

画像サイズは 512x512 に変更している。
これを変換していく

フィルタサイズは 3x3を想定して、スライド1 なら最終的な画像サイズは -2px で 510

def im2col(image, flt_h, flt_w, out_h, out_w):
    img_h, img_w = image.shape
    cols = np.zeros((flt_h, flt_w, out_h, out_w))
    for h in range(flt_h):
        h_lim = h + out_h
        for w in range(flt_w):
            w_lim = w + out_w
            cols[h, w, :, :] = image[h:h_lim, w:w_lim]
    cols = cols.reshape(flt_h*flt_w, out_h*out_w)
    return cols


cols = im2col(changed, 3, 3, 510, 510)
cols.shape

(9, 260100)

予想通りになっている。

今度はこれを逆変換する(画像に戻す)。手順は前回と逆手順。

def col2im(cols, img_shape, flt_h, flt_w, out_h, out_w):
    img_h, img_w = img_shape
    cols = cols.reshape(flt_h, flt_w, out_h, out_w).transpose(0, 1, 2, 3)
    images = np.zeros((img_h, img_w))
    for h in range(flt_h):
        h_lim = h + out_h
        for w in range(flt_w):
            w_lim = w + out_w
            images[h:h_lim, w:w_lim] += cols[h, w]
    return images / (flt_h * flt_w)


rev_img = col2im(cols, (512, 512), 3, 3, 510, 510)
plt.imshow(rev_img, 'gray', vmin = 0, vmax = 1)
plt.show()

f:id:white-azalea:20210322232343p:plain

重ね合わせ部分ができるので、そのあたりが大分明るくなるので、フィルタサイズで最終的に割ってる。
ほぼほぼ元の画像に戻せることも確認した。

これで行列のドット積でフィルタを掛けれるようになった。