0 引言

 自然场景统计(NSS)经常被应用于图像的质量评价。人们发现,用高质量设备采集的自然图像(自然场景)有着一定的统计特征(如服从一些类高斯分布),而图像的失真会使这些统计特征发生改变,因此利用图像的一些统计特征作为图像特征,可以完成图像的无参考评价。
BRISQUE来自于论文《No-Reference Image Quality Assessment in the Spatial Domain》,是一个经典的利用NSS进行NR-IQA的模型。观察到自然图像局部归一化亮度系数(MSCN),强烈趋向于单位正态高斯特性,因此作者假设失真会改变MSCN的分布状况,并基于此提取特征。
 基于自然场景统计的工作还有很多,如BLIINDS-II、VIF、DIIVINE等,但是这些工作往往需要对图形进行分解(DCT、小波等)。BRISQUE的优美之处在于,它不需要对图像进行频域分解,仅仅使用简单的归一化过程,就使得数据呈现有规律的分布。模型简单且高效,可扩展性强,计算的复杂度较低。
 后来经过实验发现,四个方向内积的AGGD特征是模型的主要特征,他们给模型的性能带来主要的贡献。这表明,图像的结构是影响人类对图像质量感知的主要因素。在质量评价中,绝大部分模型都会考虑到图像的结构信息,如经典的SSIM,或者LGP、GMSD、GM-LOG使用梯度信息模型,以及其他一些频域分解的模型。而BRISQUE使用四个方向的点积来表示图像的结构,这种想法十分巧妙。

1 原理简介

1.局部亮度归一化

 如上图左列所示,像素与其周围的像素具有很高的相关性,因为图像函数通常是分段平滑的。我们在左列可以观察到一种对角结构。图像的归一化能够大大减少像素的相关性,如右列所示。
局部亮度归一化过程如下式:
I ^ ( i , j ) = I ( i , j ) − μ ( i , j ) σ ( i , j ) + C \hat{I}(i, j)=\frac{I(i, j)-\mu(i, j)}{\sigma(i, j)+C} I^(i,j)=σ(i,j)+CI(i,j)μ(i,j)
其中:
μ ( i , j ) = ∑ k = − K K ∑ l = − L L w k , l I k , l ( i , j ) σ ( i , j ) = ∑ k = − K K ∑ l = − L L w k , l ( I k , l ( i , j ) − μ ( i , j ) ) 2 \begin{aligned} &\mu(i, j)=\sum_{k=-K}^{K} \sum_{l=-L}^{L} w_{k, l} I_{k, l}(i, j)\\ &\sigma(i, j)=\sqrt{\sum_{k=-K}^{K} \sum_{l=-L}^{L} w_{k, l}\left(I_{k, l}(i, j)-\mu(i, j)\right)^{2}} \end{aligned} μ(i,j)=k=KKl=LLwk,lIk,l(i,j)σ(i,j)=k=KKl=LLwk,l(Ik,l(i,j)μ(i,j))2
C = 1 C=1 C=1,是为了防止分母为0。
 可见这其实很类似数据预处理过程中的z-score归一化过程。
 归一化之后的图像数据不再用 I I I表示,而是用 M S C N MSCN MSCN(mean subtracted contrast normalized coefficients)表示。
 局部亮度归一化的一种生物学解释是,人来在感知失真时,会存在一种“对比掩蔽效应”。这种效应通俗的解释就是,在图像纹理很丰富的地方,图像的失真往往很难被感受到,复杂的纹理掩盖了失真。这种归一化的过程,在其他的论文里面也称为Divisive Normalization。

2.利用广义高斯分布拟合 M S C N MSCN MSCN获得特征

  自然图像(未失真)的 M S C N MSCN MSCN具有一定的统计特征(如类高斯分布),而失真改变这一分布。下图可以观察到不同失真类型图像和自然图像具有不同分布特征,文献使用广义高斯分布(GGD)对图像 M S C N MSCN MSCN的分布进行拟合。

零均值广义高斯分布(GGD)的定义如下:
f ( x ; α , σ 2 ) = α 2 β Γ ( 1 / α ) exp ⁡ ( − ( ∣ x ∣ β ) α ) f\left(x ; \alpha, \sigma^{2}\right)=\frac{\alpha}{2 \beta \Gamma(1 / \alpha)} \exp \left(-\left(\frac{|x|}{\beta}\right)^{\alpha}\right) f(x;α,σ2)=2βΓ(1/α)αexp((βx)α)
where
β = σ Γ ( 1 / α ) Γ ( 3 / α ) \beta=\sigma \sqrt{\frac{\Gamma(1 / \alpha)}{\Gamma(3 / \alpha)}} β=σΓ(3/α)Γ(1/α)
and Γ ( ⋅ ) \Gamma(\cdot) Γ() is the gamma function:
Γ ( a ) = ∫ 0 ∞ t a − 1 e − t d t a > 0 \Gamma(a)=\int_{0}^{\infty} t^{a-1} e^{-t} d t \quad a>0 Γ(a)=0ta1etdta>0
 利用GGD拟合分布获得的形状参数 ( α , σ 2 ) (α, σ^2) (α,σ2)作为特征f1和特征f2。关于广义高斯分布的参数拟合方法实现问题,我在另一篇博客单独描述,这里主要介绍IQA模型原理,不再叙述。

3.利用非对称广义高斯分布拟合MSCN相邻系数内积

 如前面所说,图像的结构信息具有很大的价值,因此论文也研究了相邻MSCN系数之间的统计特征来自作为图像的结构信息。论文在四个不同方向计算相邻系数内积,并利用非对称广义高斯分布(AGGD)拟合分布特征。四个方向的内积定义如下:
H ( i , j ) = I ^ ( i , j ) ∗ I ^ ( i , j + 1 ) V ( i , j ) = I ^ ( i , j ) ∗ I ^ ( i + 1 , j ) D 1 ( i , j ) = I ^ ( i , j ) ∗ I ^ ( i + 1 , j + 1 ) D 2 ( i , j ) = I ^ ( i , j ) ∗ I ^ ( i + 1 , j − 1 ) \begin{array}{l} H(i, j)=\hat{I}(i, j) * \hat{I}(i, j+1) \\ V(i, j)=\hat{I}(i, j) * \hat{I}(i+1, j) \\ D 1(i, j)=\hat{I}(i, j) * \hat{I}(i+1, j+1) \\ D 2(i, j)=\hat{I}(i, j) * \hat{I}(i+1, j-1) \end{array} H(i,j)=I^(i,j)I^(i,j+1)V(i,j)=I^(i,j)I^(i+1,j)D1(i,j)=I^(i,j)I^(i+1,j+1)D2(i,j)=I^(i,j)I^(i+1,j1)
 下图展示了其中一个方向上内积数据的分布情况,可以发现失真的引入会使得分布带来显著改变。这个分布,论文使用非对称广义高斯分布AGGD拟合。
在这里插入图片描述
零均值非对称广义高斯分布(AGGD)的定义如下:

f ( x ; v , σ l 2 , σ r 2 ) = { v ( β 1 + β r ) Γ ( 1 v ) exp ⁡ ( − ( − x β l ) v ) x < 0 v ( β l + β r ) Γ ( 1 v ) exp ⁡ ( − ( x β r ) v ) x ≥ 0 f\left(x ; v, \sigma_{l}^{2}, \sigma_{r}^{2}\right)=\left\{\begin{array}{ll} \frac{v}{\left(\beta_{1}+\beta_{r}\right) \Gamma\left(\frac{1}{v}\right)} \exp \left(-\left(\frac{-x}{\beta_{l}}\right)^{v}\right) & x<0 \\ \frac{v}{\left(\beta_{l}+\beta_{r}\right) \Gamma\left(\frac{1}{v}\right)} \exp \left(-\left(\frac{x}{\beta_{r}}\right)^{v}\right) & x \geq 0 \end{array}\right. f(x;v,σl2,σr2)=(β1+βr)Γ(v1)vexp((βlx)v)(βl+βr)Γ(v1)vexp((βrx)v)x<0x0
where
β l = σ l Γ ( 1 v ) Γ ( 3 v ) β r = σ r Γ ( 1 v ) Γ ( 3 v ) \begin{array}{l} \beta_{l}=\sigma_{l} \sqrt{\frac{\Gamma\left(\frac{1}{v}\right)}{\Gamma\left(\frac{3}{v}\right)}} \\ \beta_{r}=\sigma_{r} \sqrt{\frac{\Gamma\left(\frac{1}{v}\right)}{\Gamma\left(\frac{3}{v}\right)}} \end{array} βl=σlΓ(v3)Γ(v1) βr=σrΓ(v3)Γ(v1)
η = ( β r − β l ) Γ ( 2 v ) Γ ( 1 v ) \eta=\left(\beta_{r}-\beta_{l}\right) \frac{\Gamma\left(\frac{2}{v}\right)}{\Gamma\left(\frac{1}{v}\right)} η=(βrβl)Γ(v1)Γ(v2)

 利用AGGD拟合获得的形状参数 ( η , ν , σ l 2 , σ r 2 ) (η, ν, σ_l^2 , σ_r^2 ) (η,ν,σl2,σr2)将作为特征f3-f18(4个方向 x 4个特征)。关于非对称广义高斯分布的参数拟合方法实现问题,我亦在另一篇博客单独描述,这里主要介绍IQA模型原理,不再叙述。

 至此,对图像的18个特征提取就完成了,具体的特征内容如下表。

 需要指出的是,多尺度的特征经常在IQA模型中使用。论文采用了多尺度特征的形式,具体操作为首先对原始图像采集f1-f18这18维特征,然后对图像进行一次下采样,在下采样的图像上再提取一次这18维的特征,这样一共获得36维特征。

2 代码实现

本文主要给出BRISQUE算法的特征提取部分:

import torch
import torch.nn.functional as F
import math
from scipy.special import gamma
import numpy as np
from typing import Type, Any, Callable, Union, List, Optional
def gaussian_2d_kernel(kernel_size, sigma):
    kernel = torch.zeros((kernel_size, kernel_size))
    center = kernel_size // 2
    if sigma == 0:
        sigma = ((kernel_size - 1) * 0.5 - 1) * 0.3 + 0.8
    s = 2 * (sigma ** 2)
    sum_val = 0
    for i in range(0, kernel_size):
        for j in range(0, kernel_size):
            x = i - center
            y = j - center
            kernel[i, j] = math.exp(-(x ** 2 + y ** 2) / s)
            sum_val += kernel[i, j]
    sum_val = 1 / sum_val
    return kernel * sum_val

def estimate_GGD_parameters(vec):
    vec=vec.view(-1)
    gam =np.arange(0.2,10.0,0.001)
    r_gam = (gamma(1/gam)*gamma(3/gam))/((gamma(2/gam))**2)#根据候选的γ计算r(γ)
    sigma_sq=torch.mean((vec)**2).to('cpu').numpy()
    E=torch.mean(torch.abs(vec)).to('cpu').numpy()
    r=sigma_sq/(E**2)#根据sigma^2和E计算r(γ)
    diff=np.abs(r-r_gam)
    gamma_param=gam[np.argmin(diff, axis=0)]
    return gamma_param,sigma_sq

def estimate_AGGD_parameters(vec):
    vec = vec.to('cpu').numpy()
    alpha =np.arange(0.2,10.0,0.001)#产生候选的α
    r_alpha=((gamma(2/alpha))**2)/(gamma(1/alpha)*gamma(3/alpha))#根据候选的γ计算r(α)
    sigma_l=np.sqrt(np.mean(vec[vec<0]**2))
    sigma_r=np.sqrt(np.mean(vec[vec>0]**2))
    gamma_=sigma_l/sigma_r
    u2=np.mean(vec**2)
    m1=np.mean(np.abs(vec))
    r_=m1**2/u2
    R_=r_*(gamma_**3+1)*(gamma_+1)/((gamma_**2+1)**2)
    diff=(R_-r_alpha)**2
    alpha_param=alpha[np.argmin(diff, axis=0)]
    const1 = np.sqrt(gamma(1 / alpha_param) / gamma(3 / alpha_param))
    const2 = gamma(2 / alpha_param) / gamma(1 / alpha_param)
    eta =(sigma_r-sigma_l)*const1*const2
    return alpha_param,eta,sigma_l**2,sigma_r**2


def brisque_feature(imdist:Type[Union[torch.Tensor,np.ndarray]],device='cuda'):
    
    #算法需要输入为灰度图像,像素值0-255
    if type(imdist)==np.ndarray:
        assert imdist.ndim==2 or imdist.ndim==3
        if imdist.ndim==2:
            imdist=torch.from_numpy(imdist).unsqueeze(0).unsqueeze(0)
        else:
            imdist = torch.from_numpy(imdist).unsqueeze(0)
    # input (Batch,1,H,W)
    assert imdist.dim()==4
    assert imdist.shape[1]==1 or imdist.shape[1]==3
    
    if torch.max(imdist)<=1:
        imdist = imdist * 255
    # RGB to Gray
    if imdist.shape[1]==3:
        imdist=imdist[:,0,:]*0.299+imdist[:,1,:]*0.587+imdist[:,2,:]*0.114
    # GPU is much much faster
    if 'cuda' in device:
        imdist=imdist.half().to(device)
    elif device=='cpu':
        imdist=imdist.float().to(device)
    else:
        raise ValueError('cpu or cuda',device)

    # 算法主体
    scalenum = 2
    window=gaussian_2d_kernel(7,7/6).unsqueeze(0).unsqueeze(0).float().to(device)
    if 'cuda' in device:
        window=window.half()

    feat=np.zeros((18*scalenum,))
    for i in range(scalenum):
        mu=F.conv2d(imdist,window,stride=1,padding=3)
        mu_sq=mu*mu
        sigma=torch.sqrt(torch.abs(F.conv2d(imdist*imdist,window,stride=1,padding=3)-mu_sq))
        structdis = (imdist - mu) / (sigma + 1)
        del mu, mu_sq,sigma
        feat[i*18],feat[i*18+1] = estimate_GGD_parameters(structdis)

        shifts = [(0,1),(1,0),(1,1),(-1,1)]
        for itr_shift in range(4):
            shifted_structdis=structdis.roll(shifts[itr_shift],(2,3))
            pair=structdis*shifted_structdis
            pair=pair.view(-1)
            feat[i*18+2+itr_shift*4],feat[i*18+3+itr_shift*4],feat[i*18+4+itr_shift*4],feat[i*18+5+itr_shift*4]=estimate_AGGD_parameters(pair)

        imdist=F.interpolate(imdist,scale_factor=(0.5,0.5),mode='bilinear')
    return feat

算法主要是利用Pytorch实现,在GPU上跑的话相对较快。上述代码仅为特征提取部分,模型训练时需要提取训练集图像特征,之后利用特征和对应的质量分数训练SVR;测试时需要先提取测试图像特征,之后应用SVR回归。下面的不完整代码简单描述了这个过程:

from sklearn.svm import SVR
from brisque_feature import brisque_feature
import numpy as np
import skimage.io
# 训练
X=[]
Y=[]
for img_name,img_dmos in train_set:
	img=skimage.io.imread(os.path.join(root_path,img_name),as_gray=True)#读图像
    feat=brisque_feature(img)#提特征
    X.append(feat)
    Y.append(img_dmos)
X=np.array(X)
Y=np.array(Y)
svr = SVR(kernel='rbf', C=C, gamma=Gamma)#训练SVR
svr.fit(X,Y)

# 测试
img=skimage.io.imread(os.path.join(test_img_path),as_gray=True)
feat=brisque_feature(img)
predicted_score=svr.predict(feat)

默认使用GPU进行计算,如果没有GPU可以选择使用CPU:

feat=brisque_feature(img,'cpu')
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐