图像处理智能化的探索[二]:文字区块识别


0. 前言

在很久很久以前,我发过一篇关于用人脸识别实现智能裁剪图片的文章:原文链接。写完这篇文后,我畅想了一下所有内容相关业务实现全自动化运营的盛世图景……现在回想起来,当时的我真是太年轻了。殊不知有句老话说得好(?):自动化运营的大坑茫茫多,图片特别多啊!总之不经历种种跌倒,就无法认识到现实有多残酷(以及有多奇葩),我们只好擦干眼泪,期望用自己的肉身在地雷阵里探出一片通途。坑这么多,那么我们就一个个来填平吧!

1. 问题

缩略图是一篇新闻展现在用户面前举足轻重的一环,俗话说:有图有真相,突出的就是图片对于用户获取信息的重要性。在很多业务场景下,新闻列表里通常只有一个标题和一幅缩略图,因此可以说缩略图的质量高低很大程度决定了用户会否愿意点进详情页。在应用了人脸识别后的一段时间,我们观察到,纵然脸部残缺不全、人像不翼而飞的情形大幅减少,但另一些骨骼清奇的图片却浮上了水面。

比如这位:

此类图片内容常包含微博发文、统计报表和频道推广、赞助商广告等,不胜枚举。好吧,我承认那些常出现在娱乐频道头条的微博发文和聊天记录一类的图可能在某种程度上满足了吃瓜群众的窥私欲,时常能赚来大量点击,然而当它们出现在120X68或者更小的尺寸下时,就算拿放大镜也是看不清内容的。这样的缩略图几乎等于白板,无法起到吸引用户眼球的作用,更别提茫茫多千奇百怪的广告图了。至于这些图是怎么抓过来的,我管不了,我们可以做到的是在输出图片之前设立一个切面,告诉接口这幅图能不能用,这就够了。

2. 探索

地雷位置探明了,接下来就是着手排除了。我们观察到这类图片的共同点就是——文字多,我们要做的工作也就是识别图像的文字占地面积。

  • 均值计算

本着一切从简的思路,考虑到微博文字一类图片通常是白色背景,且文字占用的像素较少,我们第一个想到的方法就是将图像灰度化后计算均值和方差,这一点通过opencv可以很轻易地实现:

mean, std = cv2.meanStdDev(img)  

通过一些样本的统计,一般均值在200以上,方差在40以内,可以判定为图像泛白,文字过多的可能性很大。然而,一张一点也不白的广告图横空出世,狠狠地打了我的脸:

好吧,我们继续……

  • OCR

既然不能用色值来简单地归类,那么我们就得把关注点挪到文字本身。文字识别提得最多的就是OCR了,识别流程大致为图像预处理(灰度、降噪、二值化)-> 特征提取 -> 分类 -> 后处理(模型校正)。这块成熟的东西很多,比如Tesseract-OCR、chongdata等,但要不就是限制过多,要不就是对中文的识别效果很差,在图示那种复杂背景下出现较小文字的话基本无法识别。若是自己实现一套OCR,光特征提取和分类训练就很费时间。况且我们的需求只是过滤“文字多的图片”,而不是“识别出文字内容”,使用OCR也就有种杀鸡用牛刀的感觉了。不过在OCR的流程中,也有值得我们提取出来加以利用的环节,那便是图像预处理部分。在OCR中,这一环节从图像里分离出文字区域,用来为下一步:字符切分和特征提取做准备,但对我来说,走到这一步就够了。

  • 边缘检测

文字区块通常的特征是他们的边缘非常齐整,可以连成一个长矩形。如果能够找出这些矩形,那么我们只需要计算他们的相对面积或者数量,便可以确定这张图中的文字是否过多。幸运的是,OCR的预处理中刚好有一种方法能用来解决这个问题,那便是边缘检测。

图像中,物体的边缘通常表现为亮度或者像素灰度急剧变化,通过计算这些数值变化的导数(反映出变化的剧烈程度),即可在图像中检测出一系列高于某个阈值的像素集合,这就是我们通常看到的边缘,或者轮廓。OK,方法找到了,话不多说,上OpenCV。

4. 实践

我们拿到这样一幅娱乐频道新闻中常见的图。

4.1 图像降噪

首先,为了除去一些噪声数据的干扰,我们将图片灰度化处理,得到单通道图像,调用OpenCV的边缘检测方法。

gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)  
sobel = cv2.Sobel(gray, cv2.CV_8U, 1, 0, ksize=3)  

这里我们使用的是Sobel算子,用来计算图像灰度函数的近似梯度。此外还有Canny算子、Laplacian算子等,各自的特性可查阅相关资料。

dst = cv2.Sobel(src, ddepth, dx, dy[, dst[, ksize[, scale[, delta[, borderType]]]]])  

其中,参数dx和dy分别表示x和y方向上的差分阶数,取1, 0表示只检测x方向上的边缘(因为我们要检测的文本大多是横向的)。检测完后的图像如下:

从图中可以看到,除了文字,还有一写其他的边缘包含在内(照片、景物等),接下来我们要做的就是去除这部分的干扰。这个时候,万能的OpenCV又站了出来,他表示:我认识一对好基友——膨胀和腐蚀,他们就是干这个的。

4.2 膨胀与腐蚀

膨胀(dilation)和腐蚀(erosion)是两种形态学运算方法,原理说来话长,简单表示他们的效果就是:膨胀会让图像的高亮区域变大,腐蚀会让图像的高亮区域变小,具体可阅读这篇博文:http://blog.csdn.net/xia316104/article/details/44748217,写得非常详细。通过膨胀与腐蚀,可以达到分割相连文字区域、去除噪声边缘的目的。

进行膨胀和腐蚀操作前,我们首先将图像二值化(即非黑:0即白:255的存储方式),进一步降噪,然后进行第一次膨胀。

c1 = cv2.getStructuringElement(cv2.MORPH_RECT, (20, 8))  
c2 = cv2.getStructuringElement(cv2.MORPH_RECT, (20, 6))

ret, bimg = cv2.threshold(sobel, 0, 255, cv2.THRESH_OTSU + cv2.THRESH_BINARY)  
dilation = cv2.dilate(bimg, c2, iterations=1)  

其中c1、c2中的参数20X8、20x6分别是腐蚀和膨胀的核,表示与图像区域做卷积的面积大小。效果如图:

可以看到经过膨胀后,文字区块已经被连成了一个矩形。但烦人的是,下部有一些竖直的边缘线也连到了一起。这时我们就要用到腐蚀了。

erosion = cv2.erode(dilation, c1, iterations=1)  
img_edge = cv2.dilate(erosion, c2, iterations=1)  

通过腐蚀,亮部的文字不会受到很大影响,而图像中参差不齐的边缘就遭了秧。此后我们再进行第二次膨胀,让文字边缘更加清晰整齐,这个过程有点类似PS抠图中调色阶的操作。最后,边缘结果如图所示:

4.3 筛选文字区域

完成上两步预处理后,我们现在可以正式开始着手筛选文字区域了。首先,我们根据边缘的连线得出所有的轮廓:

contours, hierarchy = cv2.findContours(img_edge, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)  

好吧,还是有一些图片中的轮廓混进来了。通过观察,我们发现这些轮廓相比文字区块是很不规则的,有句话叫以己之长攻彼之弱,我们就用文字区块这个特点来排除掉不规则的轮廓。利用OpenCV的minAreaRect方法,我们可以得到一块区域的像素点集中包含的最小面积的矩形。其中文字区块包含的矩形通常连成一片,相较其他轮廓更细长。因此我们通过内含矩形的长宽比则可以筛选出文字区块:

# 记录文字区块数量
area_text_num = 0  
region = []

# 根据边缘连接得到所有轮廓
contours, hierarchy = cv2.findContours(img_edge, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

for i in range(len(contours)):  
    cnt = contours[i]
    area = cv2.contourArea(cnt)

    # 筛掉面积过小的区块
    if area < 1000:
        continue

    # 得到最小矩形区域,转换为顶点坐标形式(矩形可能会有角度)
    rect = cv2.minAreaRect(cnt)
    box = cv2.cv.BoxPoints(rect)
    box = np.asarray(box)
    box = box.astype(int)

    x0 = box[0][0] if box[0][0] > 0 else 0
    x1 = box[2][0] if box[2][0] > 0 else 0
    y0 = box[0][1] if box[0][1] > 0 else 0
    y1 = box[2][1] if box[2][1] > 0 else 0
    height = abs(y0 - y1)
    width = abs(x0 - x1)

    # 筛掉不够“扁”的的区块,它们更有可能不是文字
    if height > width * 0.3:
        continue
    area_text_num += height * width
    region.append(box)

return region, area_text_num  

筛选之后结果如下:

有了结果之后,我们再根据业务的具体情况来设定“文字过多”的阈值,根据简单的统计后,我目前界定的标准是文字区块面积占图片的10%,或者图片区块数量大于3。

4.4 边缘调整

在白色背景上,识别结果十分完美,那么我们是否可以开始坐下来喝杯茶,开始憧憬图片智能化处理的盛世图景了呢……等等,残酷的现实告诉我,永远不要觉得前面的道路是平坦的……这不,一个测试用例弹了出来,差点吓掉了我手中的茶杯。它是这样的:

这种文字出现在背景里的图多了,岂不是很尴尬……于是我只得对之前的方法加入了一些调整。这些非主体的文字(如球场赞助商广告、球衣背号)相比广告文字更为模糊,因此我不假思索借用了检测图片模糊量的神器——Laplacian函数来助我临门一脚。在前面的代码中,加入如下片段:

for i in range(len(contours)):  
    cnt = contours[i]
    area = cv2.contourArea(cnt)

    # 筛掉面积过小的区块
    if area < 1000:
        continue

    # 得到最小矩形区域,转换为顶点坐标形式(矩形可能会有角度)
    rect = cv2.minAreaRect(cnt)
    box = cv2.cv.BoxPoints(rect)
    box = np.asarray(box)
    box = box.astype(int)

    # 过滤掉过于模糊的区块
    lap = cv2.Laplacian(gray[box[1][1]:box[0][1], box[0][0]:box[3][0]], cv2.CV_64F)
    if lap is None or lap.var() < TEXT_LAPLACIAN_THRESHOLD:
        continue

    # Code...

文字主体区域一般都很清晰(嗯,不清晰做个啥广告),因此边缘也会比较多,正是Laplacian算子的用武之地。至于模糊量过滤的阈值多少,也需要根据实际情况来调整。

5. 尾声

只是为了解决文字图过滤的问题,便费了好些周章,可见图片智能化处理的前路漫漫呐。我们只能且行且止,不断填平路上的坑,这样后人才能走在一片坦途之上。

优质内容筛选与推荐>>
1、线程queue
2、node安装之npm报错(致命错误) 解决方案 新鲜出炉 附安装express
3、案例分析:设计模式与代码的结构特性
4、bzoj4006 [JLOI2015]管道连接
5、Holer实现外网访问本地MySQL数据库


长按二维码向我转账

受苹果公司新规定影响,微信 iOS 版的赞赏功能被关闭,可通过二维码转账支持公众号。

    阅读
    好看
    已推荐到看一看
    你的朋友可以在“发现”-“看一看”看到你认为好看的文章。
    已取消,“好看”想法已同步删除
    已推荐到看一看 和朋友分享想法
    最多200字,当前共 发送

    已发送

    朋友将在看一看看到

    确定
    分享你的想法...
    取消

    分享想法到看一看

    确定
    最多200字,当前共

    发送中

    网络异常,请稍后重试

    微信扫一扫
    关注该公众号