前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >图像处理智能化的探索[二]:文字区块识别

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

作者头像
星回
发布2018-08-02 15:20:35
4.2K3
发布2018-08-02 15:20:35
举报
文章被收录于专栏:星回的实验室

0. 前言

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

1. 问题

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

比如这位:

图1
图1

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

2. 探索

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

  • 均值计算

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

代码语言:javascript
复制
mean, std = cv2.meanStdDev(img)  

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

图3
图3
图4
图4

好吧,我们继续……

  • OCR

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

  • 边缘检测

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

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

4. 实践

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

图5
图5

4.1 图像降噪

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

代码语言:javascript
复制
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)  
sobel = cv2.Sobel(gray, cv2.CV_8U, 1, 0, ksize=3)  

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

代码语言:javascript
复制
dst = cv2.Sobel(src, ddepth, dx, dy[, dst[, ksize[, scale[, delta[, borderType]]]]])  

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

图6
图6

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

4.2 膨胀与腐蚀

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

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

代码语言:javascript
复制
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分别是腐蚀和膨胀的核,表示与图像区域做卷积的面积大小。效果如图:

图7
图7

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

代码语言:javascript
复制
erosion = cv2.erode(dilation, c1, iterations=1)  
img_edge = cv2.dilate(erosion, c2, iterations=1)  

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

图8
图8

4.3 筛选文字区域

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

代码语言:javascript
复制
contours, hierarchy = cv2.findContours(img_edge, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)  
图9
图9

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

代码语言:javascript
复制
# 记录文字区块数量
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
图10

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

4.4 边缘调整

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

图11
图11

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

代码语言:javascript
复制
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. 尾声

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

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 0. 前言
  • 1. 问题
  • 2. 探索
  • 4. 实践
    • 4.1 图像降噪
      • 4.2 膨胀与腐蚀
        • 4.3 筛选文字区域
          • 4.4 边缘调整
          • 5. 尾声
          相关产品与服务
          图像处理
          图像处理基于腾讯云深度学习等人工智能技术,提供综合性的图像优化处理服务,包括图像质量评估、图像清晰度增强、图像智能裁剪等。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档