项目实战—文档区域MSER检测实战
上次已经讨论过相关的理论,这次我们来进行相关的实战。
OCR相关工作都有一个第一步,那就是检测图像中的文本区域,只有找到了文本区域,才能对其内容进行识别,也只有找到了文本区域,才能更有针对性地判断该文本图像的质量好坏,我们期望达到如下的文本区域检测效果:
MSER对一幅已经处理成灰度的图像做二值化处理,这个处理的阈值从0到255递增,这个阈值的递增类似于在一片土地上做水平面的上升,随着水平面上升,高高低低凹凸不平的土地区域就会不断被淹没,这就是分水岭算法,而这个高低不同,就是图像中灰度值的不同。而在一幅含有文字的图像上,有些区域(比如文字)由于颜色(灰度值)是一致的,因此在水平面(阈值)持续增长的一段时间内都不会被覆盖,直到阈值涨到文字本身的灰度值时才会被淹没,这些区域就叫做最大稳定极值区域。
该算法可以用来粗略地寻找图像中的文字区域,虽然算法思想简单,但要做到效果又快又好还是需要一定基础的,好在opencv直接提供了该算法的接口,它使用了一种比算法作者要快的实现方式,一般来说我们只用知道怎么用它就行了。
要使用也比较简单:
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()
但是上面效果中的文本框形状太多变了,我们检测文本区域一般都会设法得到一个包含文本的矩形框,以便于后续从图像中通过坐标获取该区域,那怎么把这些区域转换成矩形框呢?我们借用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是经常伴随图像区域检测的算法,作用是去除重复的区域,在人脸识别、物体检测等领域都经常使用,全称是非极大值抑制(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算法后,就可以去除我们重复的文本框了,效果如下:
完整代码:
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()
原图:
可以得到结果: