如何利用图片对比算法处理白屏检测

快应用 Jul 16, 2020

背景

做过小程序或者快应用的同学应该知道,先通过 sitemap 配置应用可以爬取的页面,最终用户可以通过在平台关键字,搜索触达爬取到的页面。这个 sitemap 技术的原理类似于搜索引擎:先通过爬虫去爬取相关的页面内容,保存快照和页面链接,等到与用户搜索内容匹配的时候,再展示快照;点击快照内容时,通过预先设置的页面链接,跳转到应用的实际页面,这样就完成了一次触达过程。

难点

为了保证外链的有效性,维护团队会定时去巡检爬取的外链,以检查是否能正常访问,排除已经失效的死链。此举可以保证链接的有效性,但还是无法保证内容的有效性。也就是快应用的业务每天可能都在发生改变,页面的链接内容可能已经下架了,链接却依然能正常访问,这是常见的一种异常情况-- 内容白屏,非常影响用户体验。为了提高用户体验,必须下架这一类内容白屏的链接。

据目前查到的资料判断,页面白屏通常是在页面加载后,判断页面上的关键 dom 是否有加载来判断。快应用开发者的业务千差万别,作为快应用的平台方,去判断快应用中的关键 dom 很明显是不可行的。另一种方式是判断快应用中 ajax 请求的状态来判断,当请求状态不是 200 就断定内容异常。但这种方式也无法处理请求状态为 200,但是 ajax 返回内容数据为空的情况。进一步 ajax 内容为空,与页面内容空白并不是成必然关系;就算能遍历判断 ajax 返回空白,也无法佐证页面就是空白的。

快应用之间的差异很大,要从中寻找出一条,放置于四海之内皆准的法则,来判断快应用内容白屏的情况,这似乎没有现成的方法可以借鉴,问题似乎陷入了困境。曾一度认为像这种内容白屏的情况,是否只能由快应用的开发者去通知平台方,平台方再去下架该链接。但是不到万不得已,我们并不想给开发者带来如此糟糕的开发体验。在查阅了一些资料后,我们有了一个新的解决思路:如果一个页面的截图,跟预先提供的一张纯白色图片,相比足够的相似,那么就可以认为该页面是一个内容白屏页面。判断内容白屏这个问题,就转变成判断两张图片相似度的需求。

常见的图片相似度方案比较

下面先来简单科普几种常见的图片对比方案:

  1. hash 算法

    将每个图片生成一个哈希值,最终比较两张图片的哈希值,结果越接近,图片越相似。

哈希算法一般的处理步骤是:首先将图片缩小得足够小,一般是(9*8)只保留结构、明暗等基本信息,摒弃不同尺寸、比例带来的图片差异。 然后将图片灰度化,最后计算像素差异值生成哈希指纹。根据计算差异值方式的不同,哈希算法又可以细分为 aHash(average hash)、pHash(Perceptual hash)、dHash(different hash),他们又各有优劣:

  • aHash:平均值哈希。速度比较快,但是常常不太精确。

  • pHash:感知哈希。精确度比较高,但是速度方面较差一些。

  • dHash:差异值哈希。精确度较高,且速度也非常快。

    2.ssim(structural similarity)算法

SSIM(structural similarity)结构相似度算法,一种全参考的图像质量评价指标,分别通过对图片亮度、对比度、结构进行对比,一般是用来评估图片压缩后的质量

3.PSNR: (Peak Signal to Noise Ratio)峰值信噪比算法

与 ssim 一样也是一种全参考的图像质量评价指标。它是基于对应像素点间的误差,即基于误差敏感的图像质量评价。由于并未考虑到人眼的视觉特性(人眼对空间频率较低的对比差异敏感度较高,人眼对亮度对比差异的敏感度较色度高,人眼对一个区域的感知结果,会受到其周围邻近区域的影响等),因而经常出现评价结果与人的主观感觉不一致的情况。

4.histogram 算法

将每个图片都生成颜色的直方图,如果两个图片的颜色直方图很接近,也可以认为图片本身很接近。

直方图比较也有以下 4 种方式

  • Correlation 相关性比较
  • Chi-Square 卡方比较
  • Intersection 十字交叉性
  • Bhattacharyya distance 巴氏距离

实践

实践是检验真理的唯一标准。单单看算法解释,是无法知道算法的最终结果 是不是符合我们预期的。下面博主我将逐一将这些算法进行实验:

首先以一张白色空白的图片为基准:

再选择几张合适的网页截图,作为这次测试的实验对象:

qichezhijia
为了方便对照结果,以上几张图片,从左到右分别给它们命名如下:`'temp1.png', 'temp2.png', 'temp3.png', 'temp4.png'`

temp1.png: 几乎完全白屏

temp2.png: 中间主要内容白屏

temp3.png: 内容丰富,完全不白屏

temp4.png: 内容一般,不白屏

  1. 感知哈希算法

使用的是 github 上开源的一份 dhash 源码 https://github.com/hjaurum/DHash

测试的结果是: {'temp1.png': 3, 'temp2.png': 11, 'temp3.png': 20, 'temp4.png': 4}

结果说明:测试结果越接近 0,说明测试对象与白屏越接近。temp2.png 的结果值比较高,算法断定与白屏较为不一样,temp4 的结果值比较低,算法断定与白屏比较接近,算法断言的结果与实际情况是违背的。

结论:不适用 。由于该算法要先将图片缩得非常小小,信息丢失得非常多,所以跟纯白色图片的对比结果十分受对比图片的颜色分布影响,对比结果很不准确。

  1. ssim 算法

ssim 使用的是compare_ssim这个库。

from SSIM_PIL import compare_ssim
from PIL import Image

def compare_image_in_ssim(imgpath1, imgpath2):
  image1 = Image.open(imgpath1)
  image2 = Image.open(imgpath2)
  value = compare_ssim(image1, image2)
  return value

测试的结果是:{'temp1.png': 0.9960819466091012, 'temp2.png': 0.9753513654872917, 'temp3.png': 0.7930904675108524, 'temp4.png': 0.9460743183359872}

结果说明: 测试结果越接近 1,说明测试对象与白屏越接近。同样地,该算法断言的结果与实际情况是相违背的。

结论:不适用 。由于 ssim 算法通常是用来评价图片质量,图片的失真程度的。所以在这里用来判断测试图片与白屏之间有多失真,显然用途是不正确的

由于 psnr 算法与 ssim 一样也是一种全参考的图像质量评价指标,与 ssim 算法的作用类似,所以这里就不再另外单独对 psnr 进行实验了。

  1. histogram 算法

根据 OpenCV 上的一个例子Histogram Comparison稍作修改

import cv2 as cv

def histogram(src_base, src_test1):
    ## [Convert to HSV]
    hsv_base = cv.cvtColor(src_base, cv.COLOR_BGR2HSV)
    hsv_test1 = cv.cvtColor(src_test1, cv.COLOR_BGR2HSV)
    ## [Using 50 bins for hue and 60 for saturation]
    h_bins = 50
    s_bins = 60
    histSize = [h_bins, s_bins]

    # hue varies from 0 to 179, saturation from 0 to 255
    h_ranges = [0, 180]
    s_ranges = [0, 256]
    ranges = h_ranges + s_ranges # concat lists

    # Use the 0-th and 1-st channels
    channels = [0, 1]
    ## [Using 50 bins for hue and 60 for saturation]

    ## [Calculate the histograms for the HSV images]
    hist_base = cv.calcHist([hsv_base], channels, None, histSize, ranges, accumulate=False)
    cv.normalize(hist_base, hist_base, alpha=0, beta=1, norm_type=cv.NORM_MINMAX)

    hist_test1 = cv.calcHist([hsv_test1], channels, None, histSize, ranges, accumulate=False)
    cv.normalize(hist_test1, hist_test1, alpha=0, beta=1, norm_type=cv.NORM_MINMAX)

    ## [Calculate the histograms for the HSV images]

    res = []
    ## [Apply the histogram comparison methods]
    for compare_method in range(4):
        base_base = cv.compareHist(hist_base, hist_base, compare_method)
        base_test1 = cv.compareHist(hist_base, hist_test1, compare_method)
        data = {"method": compare_method, "base_base": base_base, "base_test1": base_test1}
        res.append(data)
## [Apply the histogram comparison methods]

测试后的结果如下:

{
  "Correlation": {
    "temp1.png": 0.9986657002039772,
    "temp2.png": 0.9999985026552713,
    "temp3.png": 0.9975293986570175,
    "temp4.png": 0.9999895931732444
  },
  "Chi-Square": {
    "temp1.png": 0,
    "temp2.png": 0,
    "temp3.png": 0,
    "temp4.png": 0
  },
  "Intersection": {
    "temp1.png": 1,
    "temp2.png": 1,
    "temp3.png": 1,
    "temp4.png": 1
  },
  "Bhattacharyya distance": {
    "temp1.png": 0.1818559920237636,
    "temp2.png": 0.04000132137383061,
    "temp3.png": 0.3436786429149872,
    "temp4.png": 0.12853959709846752
  }
}

结果说明:

  • Correlation 相关性比较 : 结果越接近 1 越相似;
  • Chi-Square 卡方比较: 结果越接近 0 越相似;
  • Intersection 十字交叉性: 结果越接近 1 越相似;
  • Bhattacharyya distance 巴氏距离: 结果越接近 0 越相似;

结论: 不适用,Correlation 相关性比较,Chi-Square 卡方比较, Intersection 十字交叉性,这三种直方图比较方法完全没有辨识度,几乎断定每一个测试对象与白屏都是一样的。Bhattacharyya distance 巴氏距离,对于识别白屏具有一定的辨识度,但是结果比较离散,temp4.pngtemp1.png 更接近与白屏,这跟实际情况是相违背的,而且这样子的结果也无法通过找到一个阈值,来断定测试对象就是白屏了。

在经过上一轮的测试后,常用的图片相似度算法,拿过来直接运用,来判断白屏的测试宣告失败!

失败乃成功之母;知道失败的原因,这样才能为以后的成功做准备。回归到白屏检测这个问题上来,白屏检测的本质是:要断定这张图上面没有内容、或者仅有非常少量的内容。哈希对比,将图片缩小得非常小,图片信息丢失了很多。也就是将一张图片做到了高度马赛克了,空白位置占原图像的多大比例这个信息,已经完全丢失了。可以感受一下 temp1.png & temp4 png 高度马赛克后的图片。

ssim 算法主要是用来评估图片质量,反映图片失真程度的,用在白屏检测这里,用途也不是很“正当”。剩下 histogram 算法,主要是先生成颜色的直方图,再进行比较来代替图片本身的比较。一般来说正常的网页肯定是图文并茂的,直方图应该也比较丰富;白屏的网页,直方图的结果应该是比较单一的。

所有的结果都不是令人满意的,问题到这里还是陷入了死胡同。

最终解决方案

不想失败在这个地方,就只能收拾好心情重新再出发,去寻找真正适合的方案。在网上泡了很久,终于在看了阮一峰老师的这篇《相似图片搜索的原理(二)》里面的内容特征法之后,有了新的思路。

所谓内容特征法就是:如果两张图片是相近,那么他们的黑白轮廓也应该相近。上一步实验用的直方图比较是基于灰度图来做的(如上图中间部分),灰度图虽然比彩图要简单,但还是保留了很多的信息。只有少量内容的白屏如(temp1.png),它的灰度图也比完全无内容的白屏丰富不少,最终生成的直方图跟白屏的差异也会不小。这就给结果带来了不少干扰和不确定性。要排除这种干扰,就要将图片无关的信息都去掉,只保留黑白轮廓,最后只对比黑白轮廓,来断定内容的丰富情况与是否白屏。看起来这种内容特征法,似乎就是我们一直在苦苦寻找的可行方案。

通过博客中提到的“大律法”,将灰度图片转化成黑白二值图,最终通过直方图比较这份二值图。优化后的算法如下:

import cv2 as cv
def otsu_histogram(img, img1):
    dst = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
    dst1 = cv.cvtColor(img1, cv.COLOR_BGR2GRAY)
    ret,th = cv.threshold(dst1,0,255,cv.THRESH_BINARY+cv.THRESH_OTSU)

    hist_base = cv.calcHist([dst], [0], None, [256], (0, 256), accumulate=False)
    hist_test1 = cv.calcHist([th], [0], None, [256], (0, 256), accumulate=False)

    cv.normalize(hist_base, hist_base, alpha=0, beta=1, norm_type=cv.NORM_MINMAX)
    cv.normalize(hist_test1, hist_test1, alpha=0, beta=1, norm_type=cv.NORM_MINMAX)


    res = []
    ## [Apply the histogram comparison methods]
    for compare_method in range(4):
        base_base = cv.compareHist(hist_base, hist_base, compare_method)
        base_test1 = cv.compareHist(hist_base, hist_test1, compare_method)
        data = {"method": compare_method, "base_base": base_base, "base_test1": base_test1}
        res.append(data)
    return res

测试后的结果如下:

{
  "Correlation": {
    "temp1.png": 0.999998199473158,
    "temp2.png": 0.9999303693978338,
    "temp3.png": 0.9807698254660822,
    "temp4.png": 0.9994585418568566
  },
  "Chi-Square": {
    "temp1.png": 0.0,
    "temp2.png": 0.0,
    "temp3.png": 0.0,
    "temp4.png": 0.0
  },
  "Intersection": {
    "temp1.png": 1.0,
    "temp2.png": 1.0,
    "temp3.png": 1.0,
    "temp4.png": 1.0
  },
  "Bhattacharyya distance": {
    "temp1.png": 0.03078108788995894,
    "temp2.png": 0.07647753684216813,
    "temp3.png": 0.2944281499295871,
    "temp4.png": 0.12674005983573874
  }
}

结果分析:Bhattacharyya distance 巴氏距离的结果断定 temp1.png & temp2.png 与白屏比较相似,temp3.png & temp4.png 与白屏较不相似,断言结果与实际是相符合。不仅如此,测试对象与白屏有多接近的算法结果中temp1.png>temp2.png>temp4.png>temp3.png,结果是符合实际,且层次分明的,这是能通过某个阈值来判断是否白屏的前提条件。似乎看到了胜利的曙光……

但,个例的成功并不代表是可以推广开来的经验,仅仅是这四张图片的成功,还不能认为该算法是可以判断出白屏的。接下来,博主我用了 1600 张不同的网页截图进行了测试,其中有白屏和非白屏的网页截图。最终识别出来的白屏有 150 张左右,识别出来的白屏中误判的有少数几张是非白屏的,白屏识别的正确率有 95%左右。有这样子的一个识别率,是可喜的,所以现在才可以认为该算法是可靠的。基于测试结果,经过不断调整参数,0.08 这个阈值用来判断白屏是准确率最高的,优化后的白屏算法如下:

def is_white_page(img, img1, threadhold=0.08):
  '''
  大律法+直方图: 判断是否白屏
  Args:
    img: 白色的图片
    img1: 对比的图片
  Return: Boolean ,跟白屏的相似度小于阈值返回true
  '''
  dst = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
  dst1 = cv.cvtColor(img1, cv.COLOR_BGR2GRAY)
  ret,th = cv.threshold(dst1,0,255,cv.THRESH_BINARY+cv.THRESH_OTSU)

  hist_base = cv.calcHist([dst], [0], None, [256], (0, 256), accumulate=False)
  hist_test1 = cv.calcHist([th], [0], None, [256], (0, 256), accumulate=False)

  cv.normalize(hist_base, hist_base, alpha=0, beta=1, norm_type=cv.NORM_MINMAX)
  cv.normalize(hist_test1, hist_test1, alpha=0, beta=1, norm_type=cv.NORM_MINMAX)

  base_test1 = cv.compareHist(hist_base, hist_test1, 3)
  if base_test1 <= threadhold:
    return True
  else:
    return False

读到这里,你以为终于就可以任务完成收工大吉了吗?不,还没有。

在上面对 1600 张网页截图的测试中还有两类截图,实际上是白屏,但是算法没有识别出来的:

(一)网页 header 跟 footer 内容比较丰富 ,但是 body 为空的

(二)网页背景色较深的

对于第一种情况,header & footer 内容丰富,会带起整张图片的丰富度,因此白屏判断会出现误判。header 跟 footer 在白屏判断中并不是重点,也就是它们有没有内容有什么内容,其实是无关紧要的,认定白屏的重点在于页面的主区域是否大面积空白无内容。很自然地,想到解决方案就是在比较之前将对结果干扰的,实际上不需要太关心的 header & footer 裁剪掉,再进行比较。

对于第二种情况,就比较复杂和艰难了。用“大律法”处理生成黑白图后,会是一张比较黑的,也就是算法断定内容丰富的一张图。既然图片大部分区域是黑色,博主我曾经反向思维想过用一张纯黑色的图片与这类图片比较,如果结果很接近,那是不是就说明,这张图片是一张背景较深的白屏图片呢?测试的结果是令人失望的,实际上内容丰富的图片与这类深色背景的图片,经过“大律法”处理后,都是一张接近全黑色的图片,两者之间并没有区分度。所以对于这种情况博主我想到的解决方案是 - - 还没想到,.囧.

不管怎么样,先对第一种情况进行优化处理,得到优化后的白屏检测算法如下:

def is_white_page(img, img1, threadhold=0.08):
  '''
  大律法+直方图: 判断是否白屏
  Args:
    img: 白色的图片
    img1: 对比的图片
  Return: Boolean ,跟白屏的相似度小于阈值返回true
  '''
  img1 = clip(img1)
  img = clip(img)
  dst = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
  dst1 = cv.cvtColor(img1, cv.COLOR_BGR2GRAY)
  ret,th = cv.threshold(dst1,0,255,cv.THRESH_BINARY+cv.THRESH_OTSU)

  hist_base = cv.calcHist([dst], [0], None, [256], (0, 256), accumulate=False)
  hist_test1 = cv.calcHist([th], [0], None, [256], (0, 256), accumulate=False)

  cv.normalize(hist_base, hist_base, alpha=0, beta=1, norm_type=cv.NORM_MINMAX)
  cv.normalize(hist_test1, hist_test1, alpha=0, beta=1, norm_type=cv.NORM_MINMAX)

  base_test1 = cv.compareHist(hist_base, hist_test1, 3)
  if base_test1 <= threadhold:
    return True
  else:
    return False

def clip(img, size ={'top': 160, 'bottom': 140}):
  '''
  裁剪图片
  top: 裁掉顶部区域的大小
  bottom:裁掉顶部区域的大小
  '''
  sz1 = img.shape[0]         #图像的高度(行 范围)
  sz2 = img.shape[1]         #图像的宽度(列 范围)

  a=size['top'] # y start
  b=sz1-size['bottom'] # y end
  c=0 # x start
  d=sz2 # x end
  return img[a:b,c:d]  #裁剪后的图像

后记: 历经波折,最终还是把一开始看似不可能实现的白屏检测方案给实现出来了。古人云:只要功夫深,铁杵磨成针。工作得久了,越来越感受到专注与坚持对于解决问题的重要性,特别是坚持,当你在面对较为艰难与复杂的工程问题时显得尤为重要。 因为这类问题,肯定不是一下子就有明确的解决思路,解决方案也不是一次两次就可以轻松成功的,在路上你会失败很多次,也会被教做人很多次。忍受住那一次次失望,在失败中总结经验,总会离成功越来越近的,但坚持能让你能走到最后,成功的那一步。

如果大家有任何的问题,欢迎给我留言反馈。

参考链接

vivo developer

快应用引擎、工具开发者、快应用生态拓展达人(vivo)。

Great! You've successfully subscribed.
Great! Next, complete checkout for full access.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.