单通道,三通道,多通道图像计算PSNR,SSIM 代码示例与函数注意事项讲解(peak_signal_noise_ratio,structural_similarity)

0、直接使用

单通道图片计算指标代码看2.2

三通道图片计算指标代码看2.3

1、PSNR,SSIM的知识点讲解、原理分析

1.1 PSNR

Peak Signal-to-Noise Ratio 峰值信噪比 单位为 d B dB dB

给定一个大小为 m × n m \times n m×n的干净图像 I I I和噪声图像 K K K,均方误差 M S E MSE MSE定义为:
M S E = 1 m n ∑ i = 0 m − 1 ∑ j = 0 n − 1 [ I ( i , j ) − K ( i , j ) ] 2 M S E=\frac{1}{m n} \sum_{i=0}^{m-1} \sum_{j=0}^{n-1}[I(i, j)-K(i, j)]^{2} MSE=mn1i=0m1j=0n1[I(i,j)K(i,j)]2
然后 P S N R PSNR PSNR就定义为:
P S N R = 10 ⋅ log ⁡ 10 ( M A X I 2 M S E ) 或 者 P S N R = 20 ⋅ log ⁡ 10 ( M A X I M S E ) P S N R=10 \cdot \log _{10}\left(\frac{M A X_{I}^{2}}{M S E}\right)\\ 或者\\ P S N R=20 \cdot \log _{10}\left(\frac{M A X_{I}}{\sqrt{M S E}}\right) PSNR=10log10(MSEMAXI2)PSNR=20log10(MSE MAXI)
其中 M A X I 2 M A X_{I}^{2} MAXI2为图片可能的最大像素值。如果每个像素都由 8 位二进制来表示,那么就为 255。通常,如果像素值由位 B B B二进制来表示,那么 M A X I = 2 B − 1 M A X_{I}=2^{B}-1 MAXI=2B1

一般地,针对 uint8 数据,最大像素值为 255;针对浮点型数据,最大像素值为 1。

上面是针对灰度图像的计算方法,如果是彩色图像,通常有三种方法来计算。其中,第二和第三种方法比较常见。

  • 分别计算 RGB 三个通道的 PSNR,然后取平均值。
  • 计算 RGB 三通道的 MSE ,然后再除以 3 。
  • 将图片转化为 YCbCr 格式,然后只计算 Y 分量也就是亮度分量的 PSNR。

针对超光谱图像,我们需要针对不同波段分别计算 P S N R PSNR PSNR,然后取平均值,这个指标称为 M P S N R MPSNR MPSNR

1.2 SSIM

Structural SIMilarity 结构相似性

S S I M SSIM SSIM公式基于样本 x x x和之 y y y间的三个比较衡量:亮度 (luminance)、对比度 (contrast) 和结构 (structure)。
l ( x , y ) = 2 μ x μ y + c 1 μ x 2 + μ y 2 + c 1 c ( x , y ) = 2 σ x σ y + c 2 σ x 2 + σ y 2 + c 2 s ( x , y ) = σ x y + c 3 σ x σ y + c 3 l(x, y)=\frac{2 \mu_{x} \mu_{y}+c_{1}}{\mu_{x}^{2}+\mu_{y}^{2}+c_{1}}\\c(x, y)=\frac{2 \sigma_{x} \sigma_{y}+c_{2}}{\sigma_{x}^{2}+\sigma_{y}^{2}+c_{2}}\\s(x, y)=\frac{\sigma_{x y}+c_{3}}{\sigma_{x} \sigma_{y}+c_{3}} l(x,y)=μx2+μy2+c12μxμy+c1c(x,y)=σx2+σy2+c22σxσy+c2s(x,y)=σxσy+c3σxy+c3
一般取 c 3 = c 2 / 2 c_{3}=c_{2} / 2 c3=c2/2

  • μ x \mu_x μx x x x的均值
  • μ y \mu_y μy y y y的均值
  • σ x 2 \sigma_x^2 σx2 x x x的方差
  • σ y 2 \sigma_y^2 σy2 y y y的方差
  • σ x y \sigma_{xy} σxy x x x y y y的协方差
  • c 1 = ( k 1 L ) 2 , c 2 = ( k 2 L ) 2 c_{1}=\left(k_{1} L\right)^{2}, c_{2}=\left(k_{2} L\right)^{2} c1=(k1L)2,c2=(k2L)2为两个常数,避免除零
  • L L L为像素值的范围, 2 B − 1 2^B-1 2B1
  • k 1 = 0.01 , k 2 = 0.03 k_{1}=0.01, k_{2}=0.03 k1=0.01,k2=0.03为默认值

那么
S S I M ( x , y ) = [ l ( x , y ) α ⋅ c ( x , y ) β ⋅ s ( x , y ) γ ] S S I M(x, y)=\left[l(x, y)^{\alpha} \cdot c(x, y)^{\beta} \cdot s(x, y)^{\gamma}\right] SSIM(x,y)=[l(x,y)αc(x,y)βs(x,y)γ]
α , β , γ \alpha, \beta, \gamma α,β,γ设为1,可以得到
S S I M ( x , y ) = ( 2 μ x μ y + c 1 ) ( 2 σ x y + c 2 ) ( μ x 2 + μ y 2 + c 1 ) ( σ x 2 + σ y 2 + c 2 ) S S I M(x, y)=\frac{\left(2 \mu_{x} \mu_{y}+c_{1}\right)\left(2 \sigma_{x y}+c_{2}\right)}{\left(\mu_{x}^{2}+\mu_{y}^{2}+c_{1}\right)\left(\sigma_{x}^{2}+\sigma_{y}^{2}+c_{2}\right)} SSIM(x,y)=(μx2+μy2+c1)(σx2+σy2+c2)(2μxμy+c1)(2σxy+c2)
每次计算的时候都从图片上取一个 N × N N×N N×N的窗口,然后不断滑动窗口进行计算,最后取平均值作为全局的 SSIM。

对于多通道的SSIM

  • 分别计算 RGB 各个通道上的 PSNR\SSIM均值,然后取平均值(除以3)。
  • 将图像转换为YCbCr格式,然后只计算Y分量(亮度分量)的PSNR\SSIM。

针对超光谱图像,我们需要针对不同波段分别计算 SSIM,然后取平均值,这个指标称为 MSSIM。

2、计算代码

2.1 看一下skimage的源码

def peak_signal_noise_ratio(image_true, image_test, *, data_range=None):
    """
    Compute the peak signal to noise ratio (PSNR) for an image.
    Parameters
    ----------
    image_true : ndarray
        Ground-truth image, same shape as im_test.
    image_test : ndarray
        Test image.
    data_range : int, optional
        The data range of the input image (distance between minimum and
        maximum possible values).  By default, this is estimated from the image
        data-type.
    Returns
    -------
    psnr : float
        The PSNR metric.
    Notes
    -----
    .. versionchanged:: 0.16
        This function was renamed from ``skimage.measure.compare_psnr`` to
        ``skimage.metrics.peak_signal_noise_ratio``.
    References
    ----------
    .. [1] https://en.wikipedia.org/wiki/Peak_signal-to-noise_ratio
    """
    check_shape_equality(image_true, image_test)

    if data_range is None:
        if image_true.dtype != image_test.dtype:
            warn("Inputs have mismatched dtype.  Setting data_range based on "
                 "image_true.")
        dmin, dmax = dtype_range[image_true.dtype.type]#在dtype_range这个字典中会存储不同numpy类型所对应的可能去到的最大最小值  这个字典在下面一代码中
        true_min, true_max = np.min(image_true), np.max(image_true)
        if true_max > dmax or true_min < dmin:
            raise ValueError(
                "image_true has intensity values outside the range expected "
                "for its data type. Please manually specify the data_range.")
        # 真实图片最小值是不是会取到负的
        # 针对无符号整型 dmin, dmax 为0,255   data_range为255
        # 针对无符号整型 dmin, dmax 为-1,1    data_range为1
        if true_min >= 0:
            # most common case (255 for uint8, 1 for float)
            data_range = dmax
        else:
            data_range = dmax - dmin

    image_true, image_test = _as_floats(image_true, image_test)

    err = mean_squared_error(image_true, image_test)
    return 10 * np.log10((data_range ** 2) / err)

字典代码

dtype_range = {bool: (False, True),
               np.bool_: (False, True),
               np.bool8: (False, True),
               float: (-1, 1),
               np.float_: (-1, 1),
               np.float16: (-1, 1),
               np.float32: (-1, 1),
               np.float64: (-1, 1)}
dtype_range.update(_integer_ranges)#还补充了整型数据的取值范围  实际取值我算了一下是

测试一下这个dtype_range

image = image.astype(np.uint8)
print("数据类型:",type(image))
print("数据结构:",image.dtype)
print("最大最小值:",dtype_range[image.dtype.type])

image = image.astype(np.float64)
print("数据类型:",type(image))
print("数据结构:",image.dtype)
print("最大最小值:",dtype_range[image.dtype.type])

2.2 实际使用情况1:单通道情况

用这两张图片进行计算举例
在这里插入图片描述
在这里插入图片描述

尽可能将输入转化为

数据类型为uint8,范围为0-255的图像image1,image2

数据类型为float64,范围为0-1.0的图像image1,image2

错误示范:输入不符合标准

float64 对应的范围应该归一化到0-1,判断的时候会出错报错说你的范围超过了数据类型所对应的范围

from skimage.metrics import peak_signal_noise_ratio
from skimage.metrics import structural_similarity
import skimage.io as io

image_path1 = "./1.png"
image_path2 = "./2.png"
# 因为是张彩色图片所以截取出一个通道
image1 = io.imread(image_path1)[...,0]
image2 = io.imread(image_path2)[...,0]
image1 = image1/1.0
image2 = image2/1.0
# 至此image1为float64 且0-255.0  就会报错

print(image1.dtype)
psnr_val = peak_signal_noise_ratio(image1, image2)
ssim_val = structural_similarity(image1,image2,win_size=11,gaussian_weights=True,multichannel=True,data_range=1.0,K1=0.01,K2=0.03,sigma=1.5)
print("psnr_val",psnr_val)
print("ssim_val",ssim_val)

Windows 下面的报错是这样的

在这里插入图片描述
Linux下面的报错是这样的

正确示例:数据类型为uint8,范围为0-255的图像image1,image2

注意range应该设置为255

from skimage.metrics import peak_signal_noise_ratio
from skimage.metrics import structural_similarity
import skimage.io as io

image_path1 = "./1.png"
image_path2 = "./2.png"
# 因为是张彩色图片所以截取出一个通道
image1 = io.imread(image_path1)[...,0]
image2 = io.imread(image_path2)[...,0]
print(image1.dtype)# uint8 范围0-255
print(image1.shape)
psnr_val = peak_signal_noise_ratio(image1, image2)
ssim_val = structural_similarity(image1,image2,win_size=11,gaussian_weights=True,multichannel=False,data_range=255,K1=0.01,K2=0.03,sigma=1.5)
print("psnr_val",psnr_val)
print("ssim_val",ssim_val)

这里需要注意一个问题
如果图像已经是单通道了请记得一定要把multichannel关掉我下面举两个例子
(1)multichannel=True
在这里插入图片描述
(2)multichannel=False
在这里插入图片描述

为什么说我们后面一种方式是对的呢。我对图片进行了生维度,让他有了通道维度并且维度为1,的时候把multichannel打开

image1 = image1[:,:,np.newaxis]
image2 = image2[:,:,np.newaxis]

在这里插入图片描述

注意ssim在两种情况下是不一样的,如果打开了multichannel 他就会默认第二个维度是通道,也就是裁剪成一个一个小条形,然后进行计算之后求平均

正确示例:数据类型为float64,范围为0-1.0的图像image1,image2

注意range应该设置为1.0

from skimage.metrics import peak_signal_noise_ratio
from skimage.metrics import structural_similarity
import skimage.io as io

image_path1 = "./1.png"
image_path2 = "./2.png"
image1 = io.imread(image_path1)[...,0]
image2 = io.imread(image_path2)[...,0]
image1 = image1/1.0
image2 = image2/1.0
# 至此image1为float64 且0-255.0
# 归一化到0-1.0
image1 = image1/255.0
image2 = image2/255.0
print(image1.dtype)
psnr_val = peak_signal_noise_ratio(image1, image2)
ssim_val = structural_similarity(image1,image2,win_size=11,gaussian_weights=True,multichannel=False,data_range=1.0,K1=0.01,K2=0.03,sigma=1.5)
print("psnr_val",psnr_val)
print("ssim_val",ssim_val)

在这里插入图片描述

2.3 实际使用情况2:RGB三通道

需要先转换成YCbCr空间然后对亮度进行求解PSNR,转换方法可以参照我的另一篇博客

RGB图像转换成YCbCr图像,rgb2ycbcr的使用,转换参数_呆呆象呆呆的博客-CSDN博客

同时也要保证数值范围和数值类型要相符合,尽可能将输入转化为

数据类型为uint8,范围为0-255的图像image1,image2(不太推荐因为算出Y通道后,大概率都是浮点型的数据,强行转换成uint8这样精度会下降,所以比较推荐下面一种方式)

数据类型为float64,范围为0-1.0的图像image1,image2

正确示例1:使用rgb2ycbcr计算Y通道后求PSNR或者SSIM

from skimage.metrics import peak_signal_noise_ratio
from skimage.metrics import structural_similarity
import skimage.io as io
import numpy as np

image_path1 = "./1.png"
image_path2 = "./2.png"
# 因为是张彩色图片 所以选一个通道
image1 = io.imread(image_path1)
image2 = io.imread(image_path2)

#我认为最简单的方法
image1 = image1/255.0
image2 = image2/255.0
image1  =  65.481 * image1[:,:,0] + 128.553 * image1[:,:,1] + 24.966 * image1[:,:,2] + 16  # 不加16是因为之后会抵消(计算PSNR的时候可以不加,但是SSIM需要加上)
image2  =  65.481 * image2[:,:,0] + 128.553 * image2[:,:,1] + 24.966 * image2[:,:,2] + 16 
image1 = image1/255.0
image2 = image2/255.0
# 只计算Y通道的值
print(image1.dtype)
image1 = np.expand_dims(image1,axis=2)
image2 = np.expand_dims(image2,axis=2)# 为什么要升维度,因为structural_similarity默认输入shape最后一个为通道数量,如果不加上的话,他求解的ssim就会是[H*W]所有[H*1]这样小图片的平均值源码地址可以参考本文备注的ssim源码  中间搜索变量 nch 就能发现
print(image1.shape)
psnr_val = peak_signal_noise_ratio(image1, image2)
ssim_val = structural_similarity(image1,image2,win_size=11,gaussian_weights=True,multichannel=False,data_range=1.0,K1=0.01,K2=0.03,sigma=1.5)
print("psnr_val",psnr_val)
print("ssim_val",ssim_val)

三通道转换成Y通道后的PSNR和上面一个例子中单通道的PSNR肯定是不一样的
在这里插入图片描述

正确示例2:直接计算Y通道后求PSNR或者SSIM

from skimage.metrics import peak_signal_noise_ratio
from skimage.metrics import structural_similarity
from skimage.color import rgb2ycbcr
import skimage.io as io

image_path1 = "./1.png"
image_path2 = "./2.png"
image1 = io.imread(image_path1)
image2 = io.imread(image_path2)

# rgb2ycbcr的输入需要归一化到0-1.0的float
#这个在上一篇blog中讲过了rgb2ycbcr输出为浮点型且范围是0-255.0 所以需要再次归一化0-1
image1 = image1/255.0
image2 = image2/255.0
image1 = rgb2ycbcr(image1)[:, :, 0:1]
image2 = rgb2ycbcr(image2)[:, :, 0:1] 
image1 = image1/255.0
image2 = image2/255.0

print(image1.dtype)
print(image1.shape)

psnr_val = peak_signal_noise_ratio(image1, image2)
ssim_val = structural_similarity(image1,image2,win_size=11,gaussian_weights=True,multichannel=True,data_range=1.0,K1=0.01,K2=0.03,sigma=1.5)
print("psnr_val",psnr_val)
print("ssim_val",ssim_val)

在这里插入图片描述

LAST、参考文献

scikit-image/simple_metrics.py at main · scikit-image/scikit-image · GitHub

图像质量评价指标之 PSNR 和 SSIM - 知乎

PSNR与SSIM对于彩色图像和灰度图像的计算区别_风雪夜归人o的博客-CSDN博客

图像质量的客观评估指标PSNR与SSIM_小村长技术blog-CSDN博客

备注 SSIM的源码

 摘录于   https://github.com/scikit-image/scikit-image/blob/main/skimage/metrics/_structural_similarity.py
from warnings import warn
import numpy as np
from scipy.ndimage import uniform_filter, gaussian_filter

from ..util.dtype import dtype_range
from ..util.arraycrop import crop
from .._shared.utils import warn, check_shape_equality

__all__ = ['structural_similarity']


def structural_similarity(im1, im2,
                          *,
                          win_size=None, gradient=False, data_range=None,
                          multichannel=False, gaussian_weights=False,
                          full=False, **kwargs):
    """
    Compute the mean structural similarity index between two images.
    Parameters
    ----------
    im1, im2 : ndarray
        Images. Any dimensionality with same shape.
    win_size : int or None, optional
        The side-length of the sliding window used in comparison. Must be an
        odd value. If `gaussian_weights` is True, this is ignored and the
        window size will depend on `sigma`.
    gradient : bool, optional
        If True, also return the gradient with respect to im2.
    data_range : float, optional
        The data range of the input image (distance between minimum and
        maximum possible values). By default, this is estimated from the image
        data-type.
    multichannel : bool, optional
        If True, treat the last dimension of the array as channels. Similarity
        calculations are done independently for each channel then averaged.
    gaussian_weights : bool, optional
        If True, each patch has its mean and variance spatially weighted by a
        normalized Gaussian kernel of width sigma=1.5.
    full : bool, optional
        If True, also return the full structural similarity image.
    Other Parameters
    ----------------
    use_sample_covariance : bool
        If True, normalize covariances by N-1 rather than, N where N is the
        number of pixels within the sliding window.
    K1 : float
        Algorithm parameter, K1 (small constant, see [1]_).
    K2 : float
        Algorithm parameter, K2 (small constant, see [1]_).
    sigma : float
        Standard deviation for the Gaussian when `gaussian_weights` is True.
    Returns
    -------
    mssim : float
        The mean structural similarity index over the image.
    grad : ndarray
        The gradient of the structural similarity between im1 and im2 [2]_.
        This is only returned if `gradient` is set to True.
    S : ndarray
        The full SSIM image.  This is only returned if `full` is set to True.
    Notes
    -----
    To match the implementation of Wang et. al. [1]_, set `gaussian_weights`
    to True, `sigma` to 1.5, and `use_sample_covariance` to False.
    .. versionchanged:: 0.16
        This function was renamed from ``skimage.measure.compare_ssim`` to
        ``skimage.metrics.structural_similarity``.
    References
    ----------
    .. [1] Wang, Z., Bovik, A. C., Sheikh, H. R., & Simoncelli, E. P.
       (2004). Image quality assessment: From error visibility to
       structural similarity. IEEE Transactions on Image Processing,
       13, 600-612.
       https://ece.uwaterloo.ca/~z70wang/publications/ssim.pdf,
       :DOI:`10.1109/TIP.2003.819861`
    .. [2] Avanaki, A. N. (2009). Exact global histogram specification
       optimized for structural similarity. Optical Review, 16, 613-621.
       :arxiv:`0901.0065`
       :DOI:`10.1007/s10043-009-0119-z`
    """
    check_shape_equality(im1, im2)

    if multichannel:
        # loop over channels
        args = dict(win_size=win_size,
                    gradient=gradient,
                    data_range=data_range,
                    multichannel=False,
                    gaussian_weights=gaussian_weights,
                    full=full)
        args.update(kwargs)
        nch = im1.shape[-1]
        mssim = np.empty(nch)
        if gradient:
            G = np.empty(im1.shape)
        if full:
            S = np.empty(im1.shape)
        for ch in range(nch):
            ch_result = structural_similarity(im1[..., ch],
                                              im2[..., ch], **args)
            if gradient and full:
                mssim[..., ch], G[..., ch], S[..., ch] = ch_result
            elif gradient:
                mssim[..., ch], G[..., ch] = ch_result
            elif full:
                mssim[..., ch], S[..., ch] = ch_result
            else:
                mssim[..., ch] = ch_result
        mssim = mssim.mean()
        if gradient and full:
            return mssim, G, S
        elif gradient:
            return mssim, G
        elif full:
            return mssim, S
        else:
            return mssim

    K1 = kwargs.pop('K1', 0.01)
    K2 = kwargs.pop('K2', 0.03)
    sigma = kwargs.pop('sigma', 1.5)
    if K1 < 0:
        raise ValueError("K1 must be positive")
    if K2 < 0:
        raise ValueError("K2 must be positive")
    if sigma < 0:
        raise ValueError("sigma must be positive")
    use_sample_covariance = kwargs.pop('use_sample_covariance', True)

    if gaussian_weights:
        # Set to give an 11-tap filter with the default sigma of 1.5 to match
        # Wang et. al. 2004.
        truncate = 3.5

    if win_size is None:
        if gaussian_weights:
            # set win_size used by crop to match the filter size
            r = int(truncate * sigma + 0.5)  # radius as in ndimage
            win_size = 2 * r + 1
        else:
            win_size = 7   # backwards compatibility

    if np.any((np.asarray(im1.shape) - win_size) < 0):
        raise ValueError(
            "win_size exceeds image extent.  If the input is a multichannel "
            "(color) image, set multichannel=True.")

    if not (win_size % 2 == 1):
        raise ValueError('Window size must be odd.')

    if data_range is None:
        if im1.dtype != im2.dtype:
            warn("Inputs have mismatched dtype.  Setting data_range based on "
                 "im1.dtype.", stacklevel=2)
        dmin, dmax = dtype_range[im1.dtype.type]
        data_range = dmax - dmin

    ndim = im1.ndim

    if gaussian_weights:
        filter_func = gaussian_filter
        filter_args = {'sigma': sigma, 'truncate': truncate}
    else:
        filter_func = uniform_filter
        filter_args = {'size': win_size}

    # ndimage filters need floating point data
    im1 = im1.astype(np.float64)
    im2 = im2.astype(np.float64)

    NP = win_size ** ndim

    # filter has already normalized by NP
    if use_sample_covariance:
        cov_norm = NP / (NP - 1)  # sample covariance
    else:
        cov_norm = 1.0  # population covariance to match Wang et. al. 2004

    # compute (weighted) means
    ux = filter_func(im1, **filter_args)
    uy = filter_func(im2, **filter_args)

    # compute (weighted) variances and covariances
    uxx = filter_func(im1 * im1, **filter_args)
    uyy = filter_func(im2 * im2, **filter_args)
    uxy = filter_func(im1 * im2, **filter_args)
    vx = cov_norm * (uxx - ux * ux)
    vy = cov_norm * (uyy - uy * uy)
    vxy = cov_norm * (uxy - ux * uy)

    R = data_range
    C1 = (K1 * R) ** 2
    C2 = (K2 * R) ** 2

    A1, A2, B1, B2 = ((2 * ux * uy + C1,
                       2 * vxy + C2,
                       ux ** 2 + uy ** 2 + C1,
                       vx + vy + C2))
    D = B1 * B2
    S = (A1 * A2) / D

    # to avoid edge effects will ignore filter radius strip around edges
    pad = (win_size - 1) // 2

    # compute (weighted) mean of ssim
    mssim = crop(S, pad).mean()

    if gradient:
        # The following is Eqs. 7-8 of Avanaki 2009.
        grad = filter_func(A1 / D, **filter_args) * im1
        grad += filter_func(-S / B2, **filter_args) * im2
        grad += filter_func((ux * (A2 - A1) - uy * (B2 - B1) * S) / D,
                            **filter_args)
        grad *= (2 / im1.size)

        if full:
            return mssim, grad, S
        else:
            return mssim, grad
    else:
        if full:
            return mssim, S
        else:
            return mssim

评论 20
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值