手写文字检测方法有很多,传统图像算法,深度学习方法等。我遇到一个简单的场景是类似下面这种方格中的手写字定位,然后进行后续工作。
image.png

需求特点是:1.场景简单 2.需要定位出单个文字文字,以裁剪出单个文字做后续操作。这种场景用opencv的mser算法就可以了。
实践的过程中的一些小点,在这里记录一下吧,方便后面回忆。

opencv mser 轮廓检测,找出边框

关键步骤:
初始化mser算法,参数不列举了,可查文档

self._mser = cv2.MSER_create(_delta=4, _min_area=120,_max_area=1000, _max_variation=0.8,_edge_blur_size= 0)

检测轮廓,并根据轮廓获取边框,去掉长宽比不合理的框

def _runmser(self, gray, vis, img):
    # 获取文本区域
    regions, _ = self._mser.detectRegions(gray)
    boxes = []
    # 取轮廓方框,去掉长宽比过大的框,通过平均像素值去掉空白框
    for c in regions:
        x, y, w, h = cv2.boundingRect(c)
        x1,y1,x2,y2 = [x, y, x + w, y + h]
        if w/h>1.5 or h/w > 1.5:
            continue
        boxes.append([x1,y1,x2,y2])
    return boxes

image.png

可以看到,边框是有了,但是很多重复的

去除重复框

接上步,思路是去掉重合面积过大的框

# NMS 方法(Non Maximum Suppression,非极大值抑制)
def _nms(self, boxes, overlapThresh):
    if len(boxes) == 0:
        return []
    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)

        # 找剩下的其余框中最大坐标和最小坐标
        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]])

        # 计算重叠面积占对应框的比例,即 IoU
        w = np.maximum(0, xx2 - xx1 + 1)
        h = np.maximum(0, yy2 - yy1 + 1)
        overlap = (w * h) / area[idxs[:last]]

        # 如果 IoU 大于指定阈值,则删除
        idxs = np.delete(idxs, np.concatenate(([last], np.where(overlap > overlapThresh)[0])))

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

效果:
image.png

现在定位框看起来好多了,但是不含文字的一些方框也被定位出来了

去掉检测不全的文字

mser算法是组件增加像素阈值,来找出区域,但是汉字由很多偏旁部首组成,部首之间也会有很多空白区域,此时可能部分汉字只框出来了一个部首,上面图中就有一些字只框出来了一部分。
因为后续的步骤中需要完整汉字,如果不全的汉字比较多,就会比较麻烦,需要一些方法减少这种情况。
这里我的思路是,在预处理的时候先对图片做一个开运算,让文字的部首之间糊上一些像素

def _preprocess(self, img):
    final = img
    # 先开运算(腐蚀再膨胀),让文字部首间联通
    kernel = np.ones((5,5), np.uint8)
    final = cv2.morphologyEx(img, cv2.MORPH_OPEN, kernel)
    # 再灰度
    final = cv2.cvtColor(final, cv2.COLOR_BGR2GRAY)
    # # 二值化后再用mser算法,效果并不好
    # ret, final = cv2.threshold(final,80,255,cv2.THRESH_BINARY) 
    vis = img.copy()
    # cv2.imshow("final", final)
    # cv2.waitKey(0)
    return final,vis

加上开运算之后:
image.png

效果:
image.png

大部分检测不全的框被修复了,但是少量完整框没了, 经过多张测试图对比,整体效果更佳。

去掉背景方框

思路是将定位框缩小,并做一个自适应二值化(排除光线干扰),然后计算其平均像素,如果不含内容,则其像素值应该很高,去掉这种

def _removeWhite(self,gray, boxes, overlapThresh):
    keep = []
    scale = 0.25
    for idx,box in enumerate(boxes):
        region =gray[box[1]:box[3], box[0]:box[2]]
        # 边缘部分向内部缩进10%,排除作文纸方框边框干扰
        height, width = np.shape(region)
        if height==0 or width == 0:
            continue
        region = region[int(scale*height):int((1-scale)*height), int(scale*width):int((1-scale)*width)]
        # 普通二值化
        # ret,region = cv2.threshold(region,127,255,cv2.THRESH_BINARY)
        # 自适应二值化
        # region = cv2.adaptiveThreshold(region, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 5, 1)
        region = cv2.adaptiveThreshold(region, 255, cv2.ADAPTIVE_THRESH_MEAN_C,cv2.THRESH_BINARY, 11, 2)
        # 计算平均强度,过小则认为其内部无内容,或者是标点,丢弃
        avg = cv2.mean(region)[0]

        #cv2.waitKey(0)
        if avg > overlapThresh:
            continue
        keep.append(box)
    cv2.waitKey(0)
    return keep

效果:
image.png

这里边有比较多的根据事情情况的算法参数调整,还是挺蛋疼的。

其它

对于更加复杂的场景,这种方法就不适用了,mser算法无法得到文字轮廓,此时可以尝试用深度学习的CTPN网络来做,裁剪出文本行后,再尝试使用传统方法,或者改造网络来获取单个文字的位置。

☞ 参与评论