DB:Real-time Scene Text Detection with Differentiable Binarization

DBNet

简介

由于分割网络的结果可以准确描述诸如扭曲文本的场景,因而基于分割的自然场景文本检测方法变得流行起来。基于分割的方法其中关键的步骤是其后处理部分,这步中将分割的结果转换为文本框或是文本区域。这篇文章的文本检测方法也是基于分割的,但是通过提出Differenttiable Binarization module(DB module)来简化分割后处理步骤,并且可以设定自适应阈值来提升网络性能。文章的方法在现有5个数据上在检测精度与速度上均表现为state-of-art。在换用轻量级的backbone(ResNet-18)之后可以将检测帧率提升到62FPS,其与其它一些文本检测算法的性能与速率关系见图1所示。

image.png
image.png

传统意义上基于分割的文本检测算法其流程如图2中的蓝色箭头所示。在传统方法中得到分割结果之后采用一个固定的阈值得到二值化的分割图,之后采用诸如像素聚类的启发式算法得到文本区域。

而文章的检测算法流程是图2中红色箭头所示的,其中不同的地方也是这篇文章核心的一点就是在阈值选取上,通过网络去预测图片每个位置处的阈值,而不是采用一个固定的值,这样就可以很好将背景与前景分离出来。但是这样的操作会给训练带来梯度不可微的情况,对此对于二值化提出了一个叫做Differentiable Binarization来解决不可微的问题。

主要贡献

本文主要贡献是提出了DB模块,这使得CNN中的二值化过程可以端到端地训练。 通过结合用于语义分割的简单网络和DB模块,我们提出了一种健壮且快速的场景文本检测器。 从使用DB模块的性能评估中观察到,我们发现我们的检测器比之前最好的基于语义分割的方法有许多突出的优势。

  • 1、在五个基准数据集上有良好的表现,其中包括水平、多个方向、弯曲的文本。
  • 2、比之前的方法要快很多,因为DB可以提供健壮的二值化图,从而大大简化了后处理过程。
  • 3、使用轻量级的backbone(ResNet18)也有很好的表现。
  • 4、DB模块在推理过程中可以去除,因此不占用额外的内存和时间的消耗

其实作者说了这么多,就是一个优点快。

Methodology

1、网络结构

通过FPN网络结构得到1/4的特征图F,通过F得到probability map (P ) 和threshold map (T),通过P、T得到binary map(B)。在训练期间对P、T、B进行监督训练,P和B是用的相同的监督信号(即label)。在推理时,只修要P或B就可以得到文本框。

image.png

2、二值化

标准二值化(Standard Binarization,SB)

B_{i,j} = \begin{cases} 1 && P_{i,j}>=t & t是预先定义的二值化阈值 \\ 0 && otherwise \end{cases}

可微的二值化(Differentiable Binarization,DB)

\hat{B}_{i,j} = \frac{1}{1+ \exp^{-k(P_{i,j}-T_{i,j}) }}

其中,\hat{B} 是生成的近似二值图(binary map),T是生成的阈值特征图(threshold map),k是放大倍数,在试验中取值为k=50。这个函数的曲线与标准二值方法曲线具有较高的近似度,而且还是可微的,如图4左边图所示,右边的两幅图是其正负标签的导数曲线。通过这样的方式不仅可以定位文本区域,还可以帮助区分开距离很近的文本示例。

image.png

DB改进性能的原因可以通过梯度的反向传播来解释。正负样本的Loss分别为:

l_+=-log\frac{1}{1+e^-kx}

l_-=-log(1-\frac{1}{1+e^-kx})

因此:

\frac{\partial l_+}{\partial x} = -kf(x)e^{-kx}

\frac{\partial l_-}{\partial x} = kf(x)

我们可以看出:

  • 梯度被k放大

3、自适应阈值

即使没有监督阈值图,阈值图也会突出显示文本边界区域。 这表明类似边界的阈值图有利于最终结果。因此,我们在阈值图上应用了类似边界的监督,以提供更好的指导。

image.png
image.png

4、Deformable convolution

使用Deformable convolution对于长宽比极高的文本实例特别有利。

5、标签生成

image.png

probability map生成

参考了PSENet,使用 Vatti clipping algorithm 将G缩减到Gs(蓝线内部),A是面积,r是shrink ratio,设置为0.4,L是周长

D=\frac{A(1-r^2)}{L}

源码实现:

# 使用Polygon库计算多边形区域的周长和面积,使用pyclipper库进行shrink
def shrink_polygon_pyclipper(polygon, shrink_ratio):
    from shapely.geometry import Polygon
    import pyclipper
    polygon_shape = Polygon(polygon)
    distance = polygon_shape.area * (1 - np.power(shrink_ratio, 2)) / polygon_shape.length
    subject = [tuple(l) for l in polygon]
    padding = pyclipper.PyclipperOffset()
    padding.AddPath(subject, pyclipper.JT_ROUND, pyclipper.ET_CLOSEDPOLYGON)
    shrinked = padding.Execute(-distance)
    if shrinked == []:
        shrinked = np.array(shrinked)
    else:
        shrinked = np.array(shrinked[0]).reshape(-1, 2)
    return shrinked

threshold map 生成

使用生成probability map一样的方法,向外进行扩张,得到绿线和蓝线中间的区域,根据到红线的距离制作标签,(设置最大值为thresh_max,作者取了0.7),其他区域使用thresh_min进行填充,作者取了0.3。

源码实现:

'''
# 当前位置到每一条变的距离
absolute_distance = self.distance(xs, ys, polygon[i], polygon[j])
# 规约到[0,1]
distance_map[i] = np.clip(absolute_distance / distance, 0, 1)
# 取该点到各条边的最小值,越靠近响应值越大
distance_map = distance_map.min(axis=0)
# 规约到[0.3,0.7]
canvas = canvas * (self.thresh_max - self.thresh_min) + self.thresh_min
'''
class MakeBorderMap():
    def __init__(self, shrink_ratio=0.4, thresh_min=0.3, thresh_max=0.7):
        self.shrink_ratio = shrink_ratio
        self.thresh_min = thresh_min
        self.thresh_max = thresh_max

    def __call__(self, data: dict) -> dict:
        """
        :param data: {'img':,'text_polys':,'texts':,'ignore_tags':}
        :return:
        """
        im = data['img']
        text_polys = data['text_polys']
        ignore_tags = data['ignore_tags']

        canvas = np.zeros(im.shape[:2], dtype=np.float32)
        mask = np.zeros(im.shape[:2], dtype=np.float32)

        for i in range(len(text_polys)):
            if ignore_tags[i]:
                continue
            self.draw_border_map(text_polys[i], canvas, mask=mask)
        # 设置最大值为0.7,最小值为0.3
        canvas = canvas * (self.thresh_max - self.thresh_min) + self.thresh_min

        data['threshold_map'] = canvas
        data['threshold_mask'] = mask
        return data

    def draw_border_map(self, polygon, canvas, mask):
        polygon = np.array(polygon)
        assert polygon.ndim == 2
        assert polygon.shape[1] == 2

        polygon_shape = Polygon(polygon)
        if polygon_shape.area <= 0:
            return
        # 向外扩张
        distance = polygon_shape.area * (1 - np.power(self.shrink_ratio, 2)) / polygon_shape.length
        subject = [tuple(l) for l in polygon]
        padding = pyclipper.PyclipperOffset()
        padding.AddPath(subject, pyclipper.JT_ROUND,
                        pyclipper.ET_CLOSEDPOLYGON)

        padded_polygon = np.array(padding.Execute(distance)[0])
        cv2.fillPoly(mask, [padded_polygon.astype(np.int32)], 1.0)

        xmin = padded_polygon[:, 0].min()
        xmax = padded_polygon[:, 0].max()
        ymin = padded_polygon[:, 1].min()
        ymax = padded_polygon[:, 1].max()
        width = xmax - xmin + 1
        height = ymax - ymin + 1
                
        polygon[:, 0] = polygon[:, 0] - xmin
        polygon[:, 1] = polygon[:, 1] - ymin
                # 生成x、y的loc
        xs = np.broadcast_to(
            np.linspace(0, width - 1, num=width).reshape(1, width), (height, width))
        ys = np.broadcast_to(
            np.linspace(0, height - 1, num=height).reshape(height, 1), (height, width))
                # 根据不同的距离得到不同的值
        distance_map = np.zeros(
            (polygon.shape[0], height, width), dtype=np.float32)
        for i in range(polygon.shape[0]):
            j = (i + 1) % polygon.shape[0]
            absolute_distance = self.distance(xs, ys, polygon[i], polygon[j])
            distance_map[i] = np.clip(absolute_distance / distance, 0, 1)
        distance_map = distance_map.min(axis=0)
                # 将值添加到canvas中
        xmin_valid = min(max(0, xmin), canvas.shape[1] - 1)
        xmax_valid = min(max(0, xmax), canvas.shape[1] - 1)
        ymin_valid = min(max(0, ymin), canvas.shape[0] - 1)
        ymax_valid = min(max(0, ymax), canvas.shape[0] - 1)
        canvas[ymin_valid:ymax_valid + 1, xmin_valid:xmax_valid + 1] = np.fmax(
            1 - distance_map[
                ymin_valid - ymin:ymax_valid - ymax + height,
                xmin_valid - xmin:xmax_valid - xmax + width],
            canvas[ymin_valid:ymax_valid + 1, xmin_valid:xmax_valid + 1])

    def distance(self, xs, ys, point_1, point_2):
        '''
        compute the distance from point to a line
        ys: coordinates in the first axis
        xs: coordinates in the second axis
        point_1, point_2: (x, y), the end of the line
        '''
        height, width = xs.shape[:2]
        square_distance_1 = np.square(xs - point_1[0]) + np.square(ys - point_1[1])
        square_distance_2 = np.square(xs - point_2[0]) + np.square(ys - point_2[1])
        square_distance = np.square(point_1[0] - point_2[0]) + np.square(point_1[1] - point_2[1])

        cosin = (square_distance - square_distance_1 - square_distance_2) / (2 * np.sqrt(square_distance_1 * square_distance_2))
        square_sin = 1 - np.square(cosin)
        square_sin = np.nan_to_num(square_sin)

        result = np.sqrt(square_distance_1 * square_distance_2 * square_sin / square_distance)
        result[cosin < 0] = np.sqrt(np.fmin(square_distance_1, square_distance_2))[cosin < 0]
        # self.extend_line(point_1, point_2, result)
        return result

因此最后得到的标签

蓝线以内区域 绿线和蓝线中间的区域 其他区域
threshold map 0.3 越靠近红线越接近0.7,越远离红线越接近0.3 0.3
probability map 1 0 0
binary map 1 0 0

Optimization

L = L_s + \alpha \times L_b + \beta \times L_t

L_s是probability map的loss,L_b是binary map的loss,L_t是threshold map的loss,\alpha\beta设置为1和10。

L_s=L_b= \sum_{i\in S_l}y_ilogx_i+(1-y_i)log(1-x_i)

S_l表示使用OHEM进行采样,正负样本的比例为1:3。

L_t=\sum_{i\in R_d}|y_i^{*}-x_i^{*}|

L_t使用L1 Loss,R_{d}表示绿线以内的区域。

后处理

在推理阶段,可以使用binary map或者probability map。作者使用了probability map

  • 使用0.3的阈值进行二值化
  • 将pixel连接成不同的文本实例
  • 将文本实例进行扩张,得到最终的文本框

D^{'}=\frac{A^{'}(1-r^{'})}{L^{'}}

实验结果

消融实验

image.png

Total-Text dataset

image.png

CTW1500

image.png

ICDAR2015

image.png

MSRA-TD500

image.png

MLT-2017

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