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)

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

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

image.png

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

因此:

我们可以看出:

  • 梯度被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是周长

源码实现:

# 使用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

是probability map的loss,是binary map的loss,是threshold map的loss,和设置为1和10。

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

使用L1 Loss,表示绿线以内的区域。

后处理

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

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

实验结果

消融实验

image.png

Total-Text dataset

image.png

CTW1500

image.png

ICDAR2015

image.png

MSRA-TD500

image.png

MLT-2017

image.png

你可能感兴趣的:(DB:Real-time Scene Text Detection with Differentiable Binarization)