FFmpeg如何计算图像的SSIM

SSIM基本概念

关于SSIMSSIM的具体解释,此处不再介绍,具体可以参见数字视频相关概念中的SSIM算法一节的介绍。

直接给出SSIMSSIM的计算方法:

SSIM(x,y)=(2μxμy+C1)(2σxy+C2)(μx2+μy2+C1)(σx2+σy2+C2)SSIM(x,y)=\frac{(2\mu_x\mu_y+C_1)(2\sigma_{xy}+C_2)}{(\mu_x^2+\mu_y^2+C_1)(\sigma_x^2+\sigma_y^2+C_2)}

C1=(K1L)2,C2=(K2L)2C_1=(K_1L)^2, C_2=(K_2L)^2K11K_1\ll1K21K_2\ll1均为常数,计算时,一般K1=0.01K_1=0.01K2=0.03K_2=0.03LL是灰度的动态范围,由图像的数据类型决定,如果数据为uint8,则L=255L=255

SSIM计算中的图像分割

在整幅图片的跨度上,图像亮度的均值和方差变化较为剧烈;并且图像上不同区块的失真程度也有可能不同;再者人眼睛每次只能聚焦于一处,更关注局部数据而非全局数据。因此如上的SSIMSSIM算法不能直接作用于一整副图像。

在论文Image quality assessment: From error visibility to structural similarity中,作者采用11×1111 \times 11的滑动窗口将整副图像分割为NN个patch,然后计算每一个patch的SSIMSSIM,最后计算所有patch的SSIMSSIM值的平均数(Mean  SSIM:MSSIMMean \ \ SSIM:MSSIM)作为整副图像的SSIMSSIM。为了避免滑动窗口带来的块效应,在计算每个patch的均值μ\mu和方差σ2\sigma^2时,作者采用了σ=1.5\sigma=1.5的高斯卷积核作加权平均。

如果整副图像有NN个patch,则MSSIMMSSIM的计算方式为:

MSSIM(X,Y)=1Ni=1NSSIM(xi,yi)MSSIM(X,Y) = \frac{1}{N}\sum_{i=1}^{N}{SSIM(x_i, y_i)}

其中,SSIM(xi,yi)SSIM(x_i, y_i)为第ii个patch的SSIMSSIM

FFmpeg中计算SSIM的算法

在FFmpeg中,也提供了计算SSIMSSIM的实现:tiny_ssim。从代码的注释中可以看到:为了提升算法的性能,没有采用论文中的高斯加权方式计算每个patch的SSIMSSIM,而是采用了一个8×88 \times 8的块来计算每个patch的SSIMSSIM

1
2
3
4
5
6
7
8
9
10
11
/*
* tiny_ssim.c
* Computes the Structural Similarity Metric between two rawYV12 video files.
* original algorithm:
* Z. Wang, A. C. Bovik, H. R. Sheikh and E. P. Simoncelli,
* "Image quality assessment: From error visibility to structural similarity,"
* IEEE Transactions on Image Processing, vol. 13, no. 4, pp. 600-612, Apr. 2004.
*
* To improve speed, this implementation uses the standard approximation of
* overlapped 8x8 block sums, rather than the original gaussian weights.
*/

standard approximation of overlapped 8x8 block sums

接下来就解释一下注释中的standard approximation of overlapped 8x8 block sums究竟是什么含义。在解释的过程中会分解成两个部分来解释:overlapped 8x8 blocksums

overlapped 8x8 block的含义

FFmpeg在计算图像SSIMSSIM时,首先以4×44 \times 4的块大小把图1所示的分辨率为W×HW \times H的图像:
图1
图1:原始图像

分割为图2的样式。
图2

图2:分割后的图像

对于图2中的每一块用block(i,j)block(i,j)来表示(图2中的红色块),FFmpeg使用block(i,j)block(i,j)及其上、右、右上块图2中的绿色块)来计算其SSIM:SSIM(xij,yij)SSIM:SSIM(x_{ij},y_{ij})

block(i,j)block(i,j)及其上、右、右上块构成一个8×88\times8的像素块,并且该8×88\times8的块和计算block(i,j+1)block(i,j+1)SSIMSSIM用到的8×88\times8的块存在重合像素,这就是注释中的overlapped 8x8 block的真正含义。

因此,根据如上规则:i[1,H4],j[0,W41]i \in [1,\frac{H}{4}],j \in [0,\frac{W}{4}-1]。也就是说:第0行和最后一列的块不会计算SSIMSSIM

最后得到FFmpeg中的SSIMSSIM计算方式为:

SSIM=MSSIM(X,Y)=1Ni=1H4j=0W41SSIM(xij,yij)SSIM = MSSIM(X,Y) = \frac{1}{N}\sum_{i=1}^{\frac{H}{4}}\sum_{j=0}^{\frac{W}{4}-1}{SSIM(x_{ij}, y_{ij})}

N=(H41)(W41)N=(\frac{H}{4}-1)(\frac{W}{4}-1)

sums的含义

如前所述,我们分析了FFmpeg计算图像的SSIMSSIM的整体思路,接下来我们继续分析FFmpeg是如何计算block(i,j)block(i,j)SSIM(xij,yij)SSIM(x_{ij},y_{ij})的。

首先利用函数ssim_4x4x2_core()来计算block(i,j)block(i,j)块的结构相似性指标,主要是如下的4个指标:

  • s1:参考图像在block(i,j)block(i,j)块的像素之和
  • s2:受损图像在block(i,j)block(i,j)块的像素之和
  • ss:参考图像和受损图像在block(i,j)block(i,j)块的像素平方之和
  • s12:参考图像和受损图像在block(i,j)block(i,j)块的对应像素乘积之和

s1=i=03j=03x(i,j)s1=\sum_{i=0}^{3}\sum_{j=0}^{3}{x(i,j)}
s2=i=03j=03y(i,j)s2=\sum_{i=0}^{3}\sum_{j=0}^{3}{y(i,j)}
ss=i=03j=03((x(i,j))2+(y(i,j))2)ss=\sum_{i=0}^{3}\sum_{j=0}^{3}{\Big(\big(x(i,j)\big)^2+\big(y(i,j)\big)^2\Big)}
s12=i=03j=03(x(i,j)y(i,j))s12=\sum_{i=0}^{3}\sum_{j=0}^{3}{\big(x(i,j) \cdot y(i,j)\big)}

如上的4个指标就是我们后续会用到的sums,该sums也就是overlapped 8x8 block sums中的sums的概念。

利用sums计算各块的SSIM

接下来利用该sums值计算SSIMSSIM

为了提升效率,FFmpeg会按照行来计算每一行的各个块的sums数据,并将每个行块的sums数据存储在长度为W4\frac{W}{4}的数组指针sum((int(*)[4]))中。

其中sum指针有两种:

  • sum0:存储当前行的各块的sums结果
  • sum1:存储当前行的上一行的sums结果

先计算第i1i-1行块和第ii行块的sums结果,并分别存入sum1sum0中。然后遍历第ii行块的每一个块,并利用sum1sum0中计算的结果来计算每一块的SSIMSSIM

函数ssim_end4()展示了如何利用block(i1,j)block(i-1,j)block(i1,j+1)block(i-1,j+1)block(i,j)block(i,j)block(i,j+1)block(i,j+1)的sums信息来计算SSIM(xij,yij)SSIM(x_{ij},y_{ij})

  • 先对4个块的sums结果进行加和处理,得到8×88\times8块的sums结果
  • 然后利用该8×88\times8块的sums来计算block(i,j)block(i,j)SSIMSSIM

函数ssim_end1()就展示了如何利用8×88\times8块的sums信息来计算SSIMSSIM。具体的计算方法如下。

将红色区块block(i,j)block(i,j)的图像放大一点,如图3所示。我们接下来计算其SSIMSSIM
图3
图3:block(i,j)block(i,j)的示意图

在计算时,首先将4个区块的sums值求和,得到8×88\times8区块的sums值,分别为:

  • s1=i=063xis1=\sum_{i=0}^{63}{x_i}
  • s2=i=063yis2=\sum_{i=0}^{63}{y_i}
  • ss=i=063(xi2+yi2)ss=\sum_{i=0}^{63}{(x_i^2+y_i^2)}
  • s12=i=063(xiyi)s12=\sum_{i=0}^{63}{(x_i \cdot y_i)}

根据如上的计算,可以得到μx\mu_xμy\mu_yμxμy\mu_x\mu_yμx2+μy2\mu_x^2+\mu_y^2σx2+σy2\sigma_x^2+\sigma_y^2σxy\sigma_{xy}

  • μx=164s1\mu_x=\frac{1}{64} \cdot s1
  • μy=164s2\mu_y=\frac{1}{64} \cdot s2
  • μxμy=16464(s1s2)\mu_x\mu_y=\frac{1}{64 \cdot 64}(s1 \cdot s2)
  • μx2+μy2=16464((s1)2+(s2)2)\mu_x^2+\mu_y^2=\frac{1}{64 \cdot 64}\big((s1)^2+(s2)^2\big)
  • σx2+σy2=16463(64ss(s1)2(s2)2)\sigma_x^2+\sigma_y^2=\frac{1}{64 \cdot 63}\big(64 \cdot ss-(s1)^2- (s2)^2\big)
  • σxy=16463(64s12s1s2)\sigma_{xy}=\frac{1}{64 \cdot 63}(64 \cdot s12 - s1 \cdot s2)

利用如上的公式(具体推导可以参考ssim_end1()的推导)对SSIMSSIM的公式进行计算可以得到:

SSIM(x,y)=(2μxμy+C1)(2σxy+C2)(μx2+μy2+C1)(σx2+σy2+C2)SSIM(x,y)=\frac{(2\mu_x\mu_y+C_1)(2\sigma_{xy}+C_2)}{(\mu_x^2+\mu_y^2+C_1)(\sigma_x^2+\sigma_y^2+C_2)}

=(2s1s2+642C1)(264s122s1s2+6463C2)(s12+s22+642C1)(64sss12s22+6463C2)=\frac{(2s1s2+64^2C_1)(2\cdot64s12-2s1s2+64\cdot63C_2)}{(s1^2+s2^2+64^2C_1)(64ss-s1^2-s2^2+64\cdot63C_2)}

FFmpeg中,对C1C_1C2C_2的定义中的因子6463也是根据上面的公式,但是从公式看,FFmpeg对ssim_c1的计算少乘了64

1
2
ssim_c1 = 0.01 * 0.01 * 255 * 255 * 64 + 0.5
ssim_c2 = 0.03 * 0.03 * 255 * 255 * 64 * 63 + 0.5

当然,为了简化处理,FFmpeg还做了如下的定义:

1
2
vars  = ss  * 64 - s1 * s1 - s2 * s2;
covar = s12 * 64 - s1 * s2;

因此,最终在FFmpeg中,计算SSIMSSIM的公式为:

SSIM(x,y)=(2s1s2+ssimC1)(2covar+ssimC2)(s12+s22+ssimC1)(vars+ssimC2)SSIM(x,y)=\frac{(2s1s2+ssimC_1)(2covar+ssimC_2)}{(s1^2+s2^2+ssimC_1)(vars + ssimC_2)}

如上的公式就是函数ssim_end1()中最终的计算方式。

利用各块的SSIM计算图像的SSIM

计算完所有块的SSIMSSIM之后,就可以计算所有块的平均SSIMSSIM并作为该图像的SSIMSSIM

SSIM(X,Y)=1Ni=1H4j=0W41SSIM(xij,yij)SSIM(X,Y)= \frac{1}{N}\sum_{i=1}^{\frac{H}{4}}\sum_{j=0}^{\frac{W}{4}-1}{SSIM(x_{ij}, y_{ij})}

N=(H41)(W41)N=(\frac{H}{4}-1)(\frac{W}{4}-1)

编码过程中的技巧

在FFmpeg计算SSIMSSIM的算法实现中,为了提升效率和抽象代码逻辑,也利用了很多的编程技巧,例如:

  • 计算YUV各分量图像宽度时用w >> !!i
  • 为了避免对第0行的特殊处理,采用两层循环来处理
  • 计算每一行的各块的sums信息时,为了降低循环次数,每次循环计算2个块的sums结果,ssim_4x4x2_core的函数名可能就是这么来的。
  • 计算每一行的各块的SSIMSSIM时,为了降低循环次数,每次循环计算4个块的SSIMSSIMssim_end4的函数名可能就是这么来的。

感谢

分析过程离不开和贤杰(github: @bodhisatan)的不断讨论和交流,感谢@贤杰在繁忙的工作之余抽出时间来一起分析FFmpeg中SSIM算法的实现原理。

打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2020-2024 Wang Wei
  • 本站访问人数: | 本站浏览次数:

请我喝杯咖啡吧~

微信