Numpyのndarray型を使ったOpenCVの画像データ覚書2

python コンピュータ
python

NumPy は Python で行列を扱うためのライブラリですが、その ndarray 型オブジェクトは OpenCV における画像データとしてそのまま利用できます。画像は縦×横の2次元配列として扱え、RGBA など複数チャンネルの場合でも 3 次元配列で表現できるため、配列演算が得意な NumPy を活用することで、通常の Python のループによるアクセスよりも圧倒的に高速に処理できます。

以前書いた記事の文書量が多くなってきたので、記事を分けます。

各チャンネルが同一か確認

import cv2
import numpy as np

# 配列の比較

# ファイルの読み込み(RGB)
src = cv2.imread("12-35.jpg")

# チャンネル分離
blur = src[:,:,0]
green = src[:,:,1]
red = src[:,:,2]

# 比較
if (np.all(blur == green) and np.all(green == red)):
    print("true")
else:
    print("false")
blur == greenのように配列同士を比較することができますが、各要素ごとに比較しその結果のブール値(真偽)の配列として返ります。
np.all()で配列のすべての要素が真か偽かを返します。
サンプルコードではRGBの3チャンネルがありますので、条件式をを2つ書いていますが、もっと効率的な方法があると思います。

np.all()全ての要素がTureか判定

NumPy 配列のすべての要素が True(真)かどうかを判定する関数です。

基本的な使い方

import numpy as np

a = np.array([True, True, True])
np.all(a)   # → True

1つでも False が含まれると False になります。

a = np.array([True, False, True])
np.all(a)   # → False

数値配列の場合
数値配列は 0 → False、それ以外 → True と判断されます。

np.all([1, 2, 3])   # → True
np.all([1, 0, 3])   # → False

多次元配列での使い方(axis 指定)
例えば 2 次元配列:

a = np.array([
    [1, 2, 3],
    [4, 0, 6]
])

全要素が True?

np.all(a)   # → False(0 があるため)

行ごとに判定(axis=1)

np.all(a, axis=1)
# → [ True, False ]

最初の行は1, 2, 3で、全て0では無いのでTrue
次の行は4, 0, 6で、0が含まれているのでFalseになります。

列ごとに判定(axis=0)

np.all(a, axis=0)
# → [ True, False, True ]

最初の列は1,4でTrue
次の列は2,0でFalse
最後の列は3,6でTrueになります。

全ての要素が255か判定

aがグレースケールの場合、真っ白な画像かの判定になります。

np.all(a == 255)

3×3カーネルで畳み込み演算

import numpy as np

def conv3x3_same(img, k):
    """
    8ビットグレースケール画像 (uint8) に対する3x3畳み込み(sameサイズ)
    - 出力サイズは img と同じ
    - 端は元画像のまま残す
    """
    h, w = img.shape

    # 出力画像:まず元の画像をコピーする
    out = img.copy()

    # カーネルの展開
    k00, k01, k02 = k[0]
    k10, k11, k12 = k[1]
    k20, k21, k22 = k[2]

    # ---- スライスによる畳み込み ----
    acc = (
        img[0:h-2, 0:w-2] * k00 +
        img[0:h-2, 1:w-1] * k01 +
        img[0:h-2, 2:w  ] * k02 +

        img[1:h-1, 0:w-2] * k10 +
        img[1:h-1, 1:w-1] * k11 +
        img[1:h-1, 2:w  ] * k12 +

        img[2:h  , 0:w-2] * k20 +
        img[2:h  , 1:w-1] * k21 +
        img[2:h  , 2:w  ] * k22
    )

    # 結果を書き込む(1〜h-2, 1〜w-2)
    out[1:h-1, 1:w-1] = np.clip(acc, 0, 255).astype(np.uint8)

    return out

使用例
・平均化カーネル

kernel = np.array([
    [ 1/9, 1/9, 1/9],
    [ 1/9, 1/9, 1/9],
    [ 1/9, 1/9, 1/9]
], dtype=float)

・シャープ化カーネル

kernel = np.array([
    [ 0, -1,  0],
    [-1,  5, -1],
    [ 0, -1,  0]
], dtype=float)

呼び出し:

out = conv3x3_same(img, kernel)

・カーネルの展開

k00, k01, k02 = k[0]

kの最初の行の各項目の値を、k00,k01,k02に順番に代入している式。

・畳み込み

img[0:h-2, 0:w-2] * k00 +

スライスで切り出した領域(img の一部)に対して、カーネル要素 k00 を要素ごとに乗算した結果を返します。
続く k01〜k22 の各スライスも同様に計算され、それらがすべて要素ごとに加算されることで、3×3 の畳み込み(積和演算)が成立します。

・参考二重ループ版

for y in range(1, h-1):
    for x in range(1, w-1):
        out[y, x] = (
            img[y-1, x-1] * k00 +
            img[y-1, x  ] * k01 +
            img[y-1, x+1] * k02 +

            img[y  , x-1] * k10 +
            img[y  , x  ] * k11 +
            img[y  , x+1] * k12 +

            img[y+1, x-1] * k20 +
            img[y+1, x  ] * k21 +
            img[y+1, x+1] * k22
        )

・結果を書き込む
np.clip(acc, 0, 255) は値を画像として扱える範囲(0〜255)に収め、
.astype(np.uint8) によって 8 ビットの画像形式に変換しています。

3×3カーネルで畳み込み演算(低速版)

二重ループを使った低速版

import numpy as np

def blur3x3_slow(img):
    """
    8ビットグレースケール画像 (uint8) に対する 3x3 ぼかし処理(低速版)。
    ・2重ループで素直に処理
    ・中心画素は3x3の平均をとる
    ・端の1ピクセルはそのままコピー
    """
    h, w = img.shape
    out = img.copy()  # 出力(sameサイズ)

    # 1〜h-2, 1〜w-2 の範囲だけ3x3を評価(境界はそのまま)
    for y in range(1, h-1):
        for x in range(1, w-1):
            # 3x3領域を取得
            region = img[y-1:y+2, x-1:x+2]

            # 平均値(ぼかし)
            avg = np.sum(region) / 9.0

            # 書き込み(0〜255に収める)
            out[y, x] = int(avg)  # または np.clip(int(avg), 0, 255)

    return out

np.sum()/9.0をnp.np.median()に置き換えるとメディアンフィルター

コメント