MSER+NMS检测图像中文本区域

OCR相关工作都有一个第一步,那就是检测图像中的文本区域,只有找到了文本区域,才能对其内容进行识别,也只有找到了文本区域,才能更有针对性地判断该文本图像的质量好坏,我们期望达到如下的文本区域检测效果:

最终效果图

MSER

MSER就是一种检测图像中文本区域的方法,这是一种传统算法,所谓传统算法,是相对于现在大行其道的机器学习技术来说的,就准确率来说,MSER对文本区域的检测效果自然是不能和深度学习如CTPN、Pixellink等相比的,但是如果只是想要对文本图像的文本区域图像质量做一个前置检查,那么使用这样一个传统算法来在效果和效率之间求取一个平衡,是不错的。

MSER全称叫做最大稳定极值区域(MSER-Maximally Stable Extremal Regions),该算法是2002提出的,主要是基于分水岭的思想来做图像中斑点的检测。

形象一点解释这个原理就是:MSER对一幅已经处理成灰度的图像做二值化处理,这个处理的阈值从0到255递增,这个阈值的递增类似于在一片土地上做水平面的上升,随着水平面上升,高高低低凹凸不平的土地区域就会不断被淹没,这就是分水岭算法,而这个高低不同,就是图像中灰度值的不同。而在一幅含有文字的图像上,有些区域(比如文字)由于颜色(灰度值)是一致的,因此在水平面(阈值)持续增长的一段时间内都不会被覆盖,直到阈值涨到文字本身的灰度值时才会被淹没,这些区域就叫做最大稳定极值区域。

该算法可以用来粗略地寻找图像中的文字区域,虽然算法思想简单,但要做到效果又快又好还是需要一定基础的,好在opencv直接提供了该算法的接口,它使用了一种比算法作者要快的实现方式,有兴趣的可以看这篇文章:Opencv2.4.9源码分析——MSER。一般来说我们只用知道怎么用它就行了。

要使用也很简单:

import cv2
img = cv2.imread('img1.png')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 得到灰度图
mser = cv2.MSER_create() # 得到mser算法对象
regions, _ = mser.detectRegions(gray) # 获取文本区域
hulls = [cv2.convexHull(p.reshape(-1, 1, 2)) for p in regions] # 绘制文本区域
cv2.polylines(img, hulls, 1, (0, 255, 0))
cv2.namedWindow("img",0)
cv2.resizeWindow("img", 800, 640) # 限定显示图像的大小
cv2.imshow('img', img)
cv2.waitKey(0) # 显示图像直到按键盘任意键
cv2.destroyAllWindows()

效果像这样:

获取初始文本区域

注意上面代码中我们是用“cv2.MSER_create()”得到了一个默认的MSER算法对象,但其实这个对象也是可以设置参数的:

  • _delta it compares (sizei−sizei−delta)/sizei−delta
  • _min_area prune the area which smaller than minArea
  • _max_area prune the area which bigger than maxArea
  • _max_variation prune the area have similar size to its children
  • _min_diversity for color image, trace back to cut off mser with diversity less than min_diversity
  • _max_evolution for color image, the evolution steps
  • _area_threshold for color image, the area threshold to cause re-initialize
  • _min_margin for color image, ignore too small margin
  • _edge_blur_size for color image, the aperture size for edge blur

更多的使用细节可以参考cv::MSER官方文档

但是上面效果中的文本框形状太多变了,我们检测文本区域一般都会设法得到一个包含文本的矩形框,以便于后续从图像中通过坐标获取该区域,那怎么把这些区域转换成矩形框呢?我们借用opencv的“cv2.boundingRect”和“cv2.rectangle”函数就可以了:

# 绘制目前的矩形文本框
vis = img.copy()
for c in hulls:
    x, y, w, h = cv2.boundingRect(c)
    cv2.rectangle(vis, (x, y), (x + w, y + h), (255, 255, 0), 1)            
cv2.namedWindow("hulls",0)
cv2.resizeWindow("hulls", 800, 640)
cv2.imshow("hulls", vis)
cv2.waitKey(0)
cv2.destroyAllWindows()

得到效果如下:

改成矩形文本框

但问题又出现了,这么多矩形框,而且还互相包含,很明显很多框是没有必要的,要全部处理也很麻烦,能不能去掉重复的矩形框呢?这就要用到NMS算法了。

NMS

NMS是经常伴随图像区域检测的算法,作用是去除重复的区域,在人脸识别、物体检测等领域都经常使用,全称是非极大值抑制(non maximum suppression),顾名思义就是抑制不是极大值的元素,所以用在这里就是抑制不是最大框的框,也就是去除大框中包含的小框。

NMS的基本思想是遍历将所有的框得分排序,选中其中得分最高的框,然后遍历其余框找到和当前最高分的框的重叠面积(IOU)大于一定阈值的框,删除。然后继续这个过程,找另一个得分高的框,再删除IOU大于阈值的框,循环。

在这个例子中,就是设定一个IOU阈值(比如0.5,也就是如果两个框的重叠面积大于其中一个框的50%,那么就删除那个框),然后遍历所有框,对剩下的每个框,遍历判断其余框中与他重叠面积大于阈值的,则删除。最后剩下的就是不包含重叠部分的文本框了。

def non_max_suppression_fast(boxes, overlapThresh):
    # 空数组检测
    if len(boxes) == 0:
        return []
 
        # 将类型转为float
    if boxes.dtype.kind == "i":
        boxes = boxes.astype("float")
 
    pick = []
 
    # grab the coordinates of the bounding boxes
        # 四个坐标数组
    x1 = boxes[:,0]
    y1 = boxes[:,1]
    x2 = boxes[:,2]
    y2 = boxes[:,3]
 
    area = (x2 - x1 + 1) * (y2 - y1 + 1) # 计算面积数组
    idxs = np.argsort(y2) # 返回的是右下角坐标从小到大的索引值
 
        # 开始遍历删除重复的框
    while len(idxs) > 0:
                # 将最右下方的框放入pick数组
        last = len(idxs) - 1
        i = idxs[last]
        pick.append(i)
 
                # 找到剩下的其余框中最大的坐标x1y1,和最小的坐标x2y2,
        xx1 = np.maximum(x1[i], x1[idxs[:last]])
        yy1 = np.maximum(y1[i], y1[idxs[:last]])
        xx2 = np.minimum(x2[i], x2[idxs[:last]])
        yy2 = np.minimum(y2[i], y2[idxs[:last]])
 
                # 计算重叠面积占对应框的比例
        w = np.maximum(0, xx2 - xx1 + 1)
        h = np.maximum(0, yy2 - yy1 + 1)
        overlap = (w * h) / area[idxs[:last]]
 
        # 如果占比大于阈值,则删除
        idxs = np.delete(idxs, np.concatenate(([last], np.where(overlap > overlapThresh)[0])))

    return boxes[pick].astype("int")

pick = non_max_suppression_fast(keep, 0.5)
NMS坐标示意图

示意图如上。

使用NMS算法后,就可以去除我们重复的文本框了,效果如下:

不重叠的矩形文本框

完整代码如下:

import cv2
import numpy as np

def non_max_suppression_fast(boxes, overlapThresh):
    # 空数组检测
    if len(boxes) == 0:
        return []
 
        # 将类型转为float
    if boxes.dtype.kind == "i":
        boxes = boxes.astype("float")
 
    pick = []
 
        # 四个坐标数组
    x1 = boxes[:,0]
    y1 = boxes[:,1]
    x2 = boxes[:,2]
    y2 = boxes[:,3]
 
    area = (x2 - x1 + 1) * (y2 - y1 + 1) # 计算面积数组
    idxs = np.argsort(y2) # 返回的是右下角坐标从小到大的索引值
 
        # 开始遍历删除重复的框
    while len(idxs) > 0:
                # 将最右下方的框放入pick数组
        last = len(idxs) - 1
        i = idxs[last]
        pick.append(i)
 
                # 找到剩下的其余框中最大的坐标x1y1,和最小的坐标x2y2,
        xx1 = np.maximum(x1[i], x1[idxs[:last]])
        yy1 = np.maximum(y1[i], y1[idxs[:last]])
        xx2 = np.minimum(x2[i], x2[idxs[:last]])
        yy2 = np.minimum(y2[i], y2[idxs[:last]])
 
                # 计算重叠面积占对应框的比例
        w = np.maximum(0, xx2 - xx1 + 1)
        h = np.maximum(0, yy2 - yy1 + 1)
        overlap = (w * h) / area[idxs[:last]]
 
        # 如果占比大于阈值,则删除
        idxs = np.delete(idxs, np.concatenate(([last], np.where(overlap > overlapThresh)[0])))

    return boxes[pick].astype("int")

img = cv2.imread('1501728414965.png')
vis = img.copy() # 用于绘制矩形框图
orig = img.copy() # 用于绘制不重叠的矩形框图
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 得到灰度图
mser = cv2.MSER_create() # 得到mser算法对象
regions, _ = mser.detectRegions(gray) # 获取文本区域
hulls = [cv2.convexHull(p.reshape(-1, 1, 2)) for p in regions] # 绘制文本区域
cv2.polylines(img, hulls, 1, (255, 0, 0))
cv2.namedWindow("img",0)
cv2.resizeWindow("img", 800, 640) # 限定显示图像的大小
cv2.imshow('img', img)


keep = []
# 绘制目前的矩形文本框
for c in hulls:
    x, y, w, h = cv2.boundingRect(c)
    keep.append([x, y, x + w, y + h])
    cv2.rectangle(vis, (x, y), (x + w, y + h), (255, 255, 0), 1)            
print("[x] %d initial bounding boxes" % (len(keep)))
cv2.namedWindow("hulls",0)
cv2.resizeWindow("hulls", 800, 640)
cv2.imshow("hulls", vis)

# 筛选不重复的矩形框
keep2=np.array(keep)
pick = non_max_suppression_fast(keep2, 0.5)
print("[x] after applying non-maximum, %d bounding boxes" % (len(pick)))
for (startX, startY, endX, endY) in pick:
    cv2.rectangle(orig, (startX, startY), (endX, endY), (255, 185, 120), 2)
cv2.namedWindow("After NMS",0)
cv2.resizeWindow("After NMS", 800, 640)
cv2.imshow("After NMS", orig)

cv2.waitKey(0)
cv2.destroyAllWindows()

查看作者首页

参考文章:
https://www.jianshu.com/p/1b9c275698c9
https://blog.csdn.net/zhaocj/article/details/40742191
https://blog.csdn.net/pandav5/article/details/50997272
https://blog.csdn.net/shuzfan/article/details/52711706

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260

推荐阅读更多精彩内容