note.nkmk.me

PythonでPSNRを算出(OpenCV, scikit-image, NumPy)

Posted: 2020-04-01 / Tags: Python, 画像処理, OpenCV, scikit-image, NumPy

Pythonで2つの画像のPSNR(ピーク信号対雑音比)を算出する方法について、OpenCV, scikit-image(skimage)で提供されている関数を使う方法と、NumPyの基本操作で処理する方法を説明する。

  • PSNR(ピーク信号対雑音比)とは
  • OpenCVでPSNR算出: cv2.PSNR()
  • scikit-imageでPSNR算出: skimage.metrics.peak_signal_noise_ratio()
  • numpy.ndarrayを計算してPSNR算出

単純な画像の比較については以下の記事を参照。

スポンサーリンク

PSNR(ピーク信号対雑音比)とは

PSNR(ピーク信号対雑音比、Peak signal-to-noise ratio)の定義は以下の通り。

Peak signal-to-noise ratio, often abbreviated PSNR, is an engineering term for the ratio between the maximum possible power of a signal and the power of corrupting noise that affects the fidelity of its representation.
...
PSNR is most commonly used to measure the quality of reconstruction of lossy compression codecs (e.g., for image compression).
Peak signal-to-noise ratio - Wikipedia

ピーク信号対雑音比 は画質の再現性に影響を与える、信号が取りうる最大のパワーと劣化をもたらすノイズの比率を表す工学用語で、しばしばPSNR(Peak signal-to-noise ratio) と略される。
...
PSNRが最も一般的に使用されるのは、画像圧縮など非可逆圧縮を使ったコーデックの再現性の品質の尺度としてである。 ピーク信号対雑音比 - Wikipedia

画像圧縮の画質評価指標として用いる場合は、元画像(オリジナル画像)と圧縮画像のPSNRを算出する。

数式は以下の通り。

$$ \mathit{PSNR} = 10 \cdot \log{10} \frac{\mathit{MAX}^2}{\mathit{MSE}} $$

ここでMSE(Mean Squared Error)は平均二乗誤差を表し、2枚の画像の画素ごとの差分の2乗の平均。MAXは画像の画素値が取りうる最大値で、例えばuint8(符号なし8ビット整数)で表された画像の場合は255

PSNRの単位はdB(デシベル)で、目安は以下の通り。

Typical values for the PSNR in lossy image and video compression are between 30 and 50 dB, provided the bit depth is 8 bits, where higher is better. For 16-bit data typical values for the PSNR are between 60 and 80 dB. Acceptable values for wireless transmission quality loss are considered to be about 20 dB to 25 dB. Peak signal-to-noise ratio - Wikipedia

非可逆の画像およびビデオ圧縮におけるPSNRの標準的な値は30~50dBで、高い方が画質が良い。無線通信において許容される品質の低下は約20dBから25dBの範囲と見なされる。 ピーク信号対雑音比 - Wikipedia

以降のサンプルコードでは、元画像と、それをJPEGの品質(quality)を変えて保存した2枚の画像を例とする。

from PIL import Image

img = Image.open('data/src/lena.jpg')

img.save('data/src/lena_q95.jpg', quality=95)
img.save('data/src/lena_q50.jpg', quality=50)

元画像。

lena original

quality=95

lena quality=95

quality=50

lena quality=50

OpenCVでPSNR算出: cv2.PSNR()

OpenCVでPSNRを算出するには、cv2.PSNR()を使う。

import cv2

img_org = cv2.imread('data/src/lena.jpg')
img_q95 = cv2.imread('data/src/lena_q95.jpg')
img_q50 = cv2.imread('data/src/lena_q50.jpg')

print(cv2.PSNR(img_org, img_q95))
# 39.02455758374567

print(cv2.PSNR(img_org, img_q50))
# 30.34829234238757

保存時のqualityが高いほうがPSNRの値が高くなっているのが分かる。

同じ画像(差分がない画像)に対しては分母のMSEが0になりゼロ除算になるのでPSNRは定義できないが、cv2.PSNR()では値が返される。

print(cv2.PSNR(img_org, img_org))
# 361.20199909921956

これはゼロ除算を防ぐための小さい値が分母に加えて計算されているため(以下のコード中のDBL_EPSILON)。

最大値は引数Rで指定する。デフォルトは255なのでこの例では省略可能。

print(cv2.PSNR(img_org, img_q95, R=255))
# 39.02455758374567

scikit-imageでPSNR算出: skimage.metrics.peak_signal_noise_ratio()

scikit-imageでPSNRを算出するには、metricsモジュールのpeak_signal_noise_ratio()を使う。

skimage.metrics.peak_signal_noise_ratio()はバージョン0.16で追加(リネーム)された関数。

0.15まではmeasureモジュールにcompare_psnr()という名前で提供されていた。

0.17時点ではskimage.measure.compare_psnr()もまだ存在するが0.18で削除予定とのことなので要注意。

使用例は以下の通り。

import skimage.io
import skimage.metrics

img_org = skimage.io.imread('data/src/lena.jpg')
img_q95 = skimage.io.imread('data/src/lena_q95.jpg')
img_q50 = skimage.io.imread('data/src/lena_q50.jpg')

print(skimage.metrics.peak_signal_noise_ratio(img_org, img_q95))
# 39.024557583745676

print(skimage.metrics.peak_signal_noise_ratio(img_org, img_q50))
# 30.34829234238757

同じ画像(差分がない画像)に対しては無限大infが返される。以下のように警告Warningが出る。

print(skimage.metrics.peak_signal_noise_ratio(img_org, img_org))
# inf
# 
# /usr/local/lib/python3.7/site-packages/skimage/metrics/simple_metrics.py:160: RuntimeWarning: divide by zero encountered in double_scalars
#   return 10 * np.log10((data_range ** 2) / err)

最大値は引数data_rangeで指定する。デフォルトは255なのでこの例では省略可能。

print(skimage.metrics.peak_signal_noise_ratio(img_org, img_q95, data_range=255))
# 39.024557583745676

numpy.ndarrayを計算してPSNR算出

PSNRはシンプルな式で計算できるので、NumPyの基本操作で算出可能。

参考までに例を示す。OpenCVは画像の読み込みに使っているだけ。

import numpy as np
import cv2

def psnr(img_1, img_2, data_range=255):
    mse = np.mean((img_1.astype(float) - img_2.astype(float)) ** 2)
    return 10 * np.log10((data_range ** 2) / mse)

img_org = cv2.imread('data/src/lena.jpg')
img_q95 = cv2.imread('data/src/lena_q95.jpg')
img_q50 = cv2.imread('data/src/lena_q50.jpg')

print(psnr(img_org, img_q95))
# 39.024557583745676

print(psnr(img_org, img_q50))
# 30.34829234238757

print(psnr(img_org, img_org))
# inf
# 
# /usr/local/lib/python3.7/site-packages/ipykernel_launcher.py:3: RuntimeWarning: divide by zero encountered in double_scalars
#   This is separate from the ipykernel package so we can avoid doing imports until

入力するnumpy.ndarrayuint8(符号なし8ビット整数)の場合、マイナスの値を表現できないため、astype()で浮動小数点数floatに変換している。

入力するnumpy.ndarrayuint8であれば符号ありの整数intに変換してもいいが、floatnumpy.ndarrayが入力されることも考慮してfloatに変換している。

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

関連カテゴリー

関連記事