note.nkmk.me

Python, OpenCV, NumPyで画像を比較(完全一致か判定、差分取得など)

Date: 2020-01-23 / tags: Python, NumPy, 画像処理, OpenCV

PythonのOpenCVでは画像をNumPy配列ndarrayとして扱う。NumPyの機能を使うと、2つの画像が完全一致しているか判定したり、差分画像を生成して保存したりできる。

ここでは以下の内容について説明する。

  • 画像が完全に一致しているか判定
  • 差分画像を算出し保存
    • 差分の絶対値
    • 差分0を128とする
    • 差分を二値化
  • 差分が生じている座標を取得

以下のサンプルコードではOpenCVで画像ファイルを読み書きしているが、画像の比較処理自体にはOpenCVの機能は使わない。

画像をNumPy配列ndarrayとして読み込めればOpenCVは必要なく、例えば、Pillowを使うことも可能。以下の記事を参照。ndarrayの処理例なども紹介している。

なお、ここでは画素(ピクセル)ごとの単純な差分を考える。圧縮画像などの画質を評価したい場合はPSNR(Peak signal-to-noise ratio、ピーク信号対雑音比)といった指標が使われることが多い。

スポンサーリンク

画像が完全に一致しているか判定

以下の画像を例とする。

import cv2
import numpy as np

im = cv2.imread('data/src/lena.jpg')
print(im.shape)
# (225, 400, 3)

print(im.dtype)
# uint8

lena

可逆圧縮のPNGで画像を保存してから再度読み込んで、元の画像と比較する。

np.array_equal()で配列の要素、すなわち、画像の画素値がすべて等しいかを判定できる。

cv2.imwrite('data/temp/lena.png', im)
im_png = cv2.imread('data/temp/lena.png')

print(np.array_equal(im, im_png))
# True

BMP(通常は無圧縮)で保存してから再度読み込んでも同様。

cv2.imwrite('data/temp/lena.bmp', im)
im_bmp = cv2.imread('data/temp/lena.bmp')

print(np.array_equal(im, im_bmp))
# True

あくまでも読み込んだ画像の画素値がすべて等しいかを判定しているだけなので、元のファイルが同一かは分からない。例えば同じ画像から保存されたPNGとBMPは一致すると判定される。

print(np.array_equal(im_png, im_bmp))
# True

完全一致しない例として、JPEG(非可逆圧縮)で保存してから再度読み込んで比較する。Falseが返され、元の画像と完全一致していないことが分かる。

cv2.imwrite('data/dst/lena_q25.jpg', im, [cv2.IMWRITE_JPEG_QUALITY, 25])
im_q25 = cv2.imread('data/dst/lena_q25.jpg')

print(np.array_equal(im, im_q25))
# False

この例では差分が分かりやすいようにJPEGの品質を低くして保存している。

lena low quality

差分画像を算出し保存

2つの画像が完全一致していない場合、その差分を算出し新たな画像として保存する方法を説明する。

なお、以降は2つの画像が同じ形状、すなわち、幅・高さが同じであることを前提とする。

ndarrayの形状はshapeで取得できる、それをそのまま==で比較すれば、2つの画像が同じ形状かを判定できる。同じであればTrue、異なっていればFalse

print(im.shape == im_q25.shape)
# True

print(im.shape == (250, 400, 3))
# False

差分の絶対値

同じ形状のndarray-演算子で差分を算出できる。

ただし、cv2.imread()で読み込んだndarrayのデータ型は符号なし8ビット整数unit8なので、そのままだとマイナスの値を扱えない。astype()で符号ありの整数intに変換する。

max()min()で差分の最大値と最小値を確認できる。

im_diff = im.astype(int) - im_q25.astype(int)

print(im_diff.max())
# 142

print(im_diff.min())
# -101

数値として確認したいのであればこのままでいいが、差分を画像として保存したい場合はnp.abs()で絶対値をとる。

im_diff_abs = np.abs(im_diff)

print(im_diff_abs.max())
# 142

print(im_diff_abs.min())
# 0

cv2.imwrite('data/dst/lena_diff_abs.png', im_diff_abs)

lena low quality difference abs

差分が小さくて画像で確認しにくいときは最大値で規格化すると分かりやすい。

im_diff_abs_norm = im_diff_abs / im_diff_abs.max() * 255

print(im_diff_abs_norm.max())
# 255.0

print(im_diff_abs_norm.min())
# 0.0

cv2.imwrite('data/dst/lena_diff_abs_norm.png', im_diff_abs_norm)

lena low quality difference abs normalize

なお、ここではカラー画像(三次元配列)の差分を取得しているが、グレースケール画像(二次元配列)の差分もやり方は同様。二次元配列同士の-演算は二次元配列が返される。

差分0を128とする

上の例のように絶対値をとると、差分の正負の情報が失われるため、元の画像のどちらが大きかったかが分からなくなってしまう。

正負の情報を残したい場合は、差分0を8bit(0 ~ 255)の中央である128とする方法が考えられる。

im_diff_center = np.floor_divide(im_diff, 2) + 128

print(im_diff_center.max())
# 199

print(im_diff_center.min())
# 77

cv2.imwrite('data/dst/lena_diff_center.png', im_diff_center)

lena low quality difference center

差分が取りうる値の範囲は-255 ~ 255なので、それが最終的に0 ~ 255に収まるように、np.floor_divide()を使って2で割っている。

絶対値の例のように規格化したい場合は以下のような処理を行えばよい。差分が強調されて見やすくなる。

im_diff_center_norm = im_diff / np.abs(im_diff).max() * 127.5 + 127.5

print(im_diff_center_norm.max())
# 255.0

print(im_diff_center_norm.min())
# 36.81338028169013

cv2.imwrite('data/dst/lena_diff_center_norm.png', im_diff_center_norm)

lena low quality difference center normalize

なお、ここで挙げた処理では問題ないはずだが、cv2.imwrite()で保存する際にndarrayの値が0 ~ 255の範囲に収まらないと保存される画像の値がおかしくなるので注意。

0 ~ 255に収まるような処理にするのが大前提ではあるが、複雑な処理でエッジケースを確認するのが面倒な場合はとりあえずnp.clip()を使うのが便利。

差分を二値化

差分が生じている位置を確認したい場合は差分を二値化する。

im_diff_bin = (im_diff_abs > 32) * 255

cv2.imwrite('data/dst/lena_diff_bin.png', im_diff_bin)

lena low quality difference binarize

ここでは差分の絶対値が32より大きい画素を255、そのほかの画素を0としている。

32は適当に決めたしきい値で特に意味はない。例えばim_diff_abs != 0という条件にすれば、差分がある画素はすべて255となる。

差分が生じている座標を取得

差分が生じている位置を、二値化の例のように画像で確認するのではなく正確な座標として取得するには、np.where()を使う。

例えば、差分が最大となる座標、および、差分の絶対値が100を超える座標は以下の通り。

print(list(zip(*np.where(im_diff_abs == np.max(im_diff_abs)))))
# [(155, 117, 2)]

print(list(zip(*np.where(im_diff_abs > 100))))
# [(135, 160, 0), (137, 157, 2), (155, 117, 2)]

結果の座標が一つの場合もタプルのリストが返される。

np.where()による条件を満たす座標の取得についての詳細は以下の記事を参照。

なお、座標は(高さ, 幅, 色)となる。グレースケール(二次元配列)の場合は(高さ, 幅)。原点は左上。

OpenCVのcv2.imread()で読み込んだ場合は色の並びがBGRとなるので注意。

スポンサーリンク
シェア
このエントリーをはてなブックマークに追加

関連カテゴリー

関連記事