【DBNet模型】

DBNet模型

一、简述

  DBNet是基于分割的文本检测算法,算法将可微分二值化模块(Differentiable Binarization)引入了分割模型,使得模型能够通过自适应的阈值图进行二值化,并且自适应阈值图可以计算损失,能够在模型训练过程中起到辅助效果优化的效果。经过验证,该方案不仅提升了文本检测的效果而且简化了后处理过程。相较于其他文本检测模型,DBNet在效果和性能上都有比较大的优势,是当前常用的文本检测算法。

二、模型结构

在这里插入图片描述DB文本检测模型可以分为三个部分:

  • Backbone网络,负责提取图像的特征
  • FPN网络,特征金子塔,结构增强特征
  • Head网络,计算文本区域概率图

1.Backbone网络

  DB文本检测网络的Backbone部分采用的是图像分类网络,论文中分别使用了ResNet50和ResNet18网络。这里结合具体的输入图像尺寸进行说明如下:
  输入图像[1,3,640, 640] ,进入Backbone骨架网络,先经过一次卷积计算尺寸变为原来的1/2, 而后经过四次下采样,输出四个尺度特征图如下:

在这里插入图片描述

2.FPN网络

  特征金字塔结构FPN是一种卷积网络来高效提取图片中各维度特征的常用方法。
  FPN网络的输入为Backbone部分的输出,经FPN计算后输出的特征图的高度和宽度为原图的1/4, 即[1, 256, 160, 160] 。
在这里插入图片描述

1/32特征图: [1, N, 20, 20] ===> 卷积 + 8倍上采样 ===> [1, 64, 160, 160]
1/16特征图:[1, N, 40, 40] ===> 加1/32特征图的两倍上采样 ===> 新1/16特征图 ==> 卷积 + 4倍上采样 ===> [1, 64, 160, 160]
1/8特征图:[1, N, 80, 80] ===> 加新1/16特征图的两倍上采样 ===>新1/8特征图 ===> 卷积 + 2倍上采样 ===> [1, 64, 160, 160]
1/4特征图:[1, N, 160, 160] ===> 加新1/8特征图的两倍上采样 ===> 新1/4特征图 ===> 卷积 ===> [1, 64, 160, 160]
融合特征图:[1, 256, 160, 160] # 将1/4,1/8, 1/16, 1/32特征图按通道层合并在一起

3.Head网络

  计算文本区域概率图,文本区域阈值图以文本区域二值图。
  Head网络会在FPN特征的基础上作上采样,将FPN特征由原来的1/4大小映射到原图大小,最终将生成的三个图合并,输出为[1, 3, 640, 640]
在这里插入图片描述

三、标签生成

   DB算法在进行模型训练的时,需要根据标注框生成两幅图像:概率图和阈值图。生成过程如下图所示:
在这里插入图片描述
  image图像中的红线是文本的标注框,文本标注框的点集合用如下形式表示:
            G = { S k } k = 1 n G = \{S_k\}_{k=1}^n G={ Sk}k=1n,   n表示顶点的数量
  在polygon图像中,将红色的标注框外扩distance得到绿色的polygon框,内缩distance得到蓝色的polygon框。
  论文中标注框内缩和外扩使用相同的distance,其计算公式为:
                D = A ( 1 − r 2 ) L D =\cfrac{A(1 - r^2)}{L} D=LA(1r2),  L代表周长,A代表面积,r代表缩放比例,通常r=0.4
  多边形轮廓的周长L和面积A通过Polygon库计算获得。
  根据计算出的distance,对标注框进行外扩和内缩操作,用Vatti算法实现,参考链接https://github.com/fonttools/pyclipper和中文文档https://www.cnblogs.com/zhigu/p/11943118.html,在python中调用pyclipper库的接口操作即可,简单示例如下。

import cv2
import pyclipper
import numpy as np
from shapely.geometry import Polygon

def draw_img(subject, canvas, color=(255,0,0)):
    """作图函数"""
    for i in range(len(subject)):
        j = (i+1)%len(subject)
        cv2.line(canvas, subject[i], subject[j], color)

# 论文默认shrink值
r=0.4
# 假定标注框
subject = ((100, 100), (250, 100), (250, 200), (100, 200))
# 创建Polygon对象
polygon = Polygon(subject)
# 计算偏置distance
distance = polygon.area*(1-np.power(r, 2))/polygon.length
print(distance)
# 25.2

# 创建PyclipperOffset对象
padding = pyclipper.PyclipperOffset()
# 向ClipperOffset对象添加一个路径用来准备偏置
# padding.AddPath(subject, pyclipper.JT_ROUND, pyclipper.ET_CLOSEDPOLYGON)
# adding.AddPath(subject, pyclipper.JT_SQUARE, pyclipper.ET_CLOSEDPOLYGON)
padding.AddPath(subject, pyclipper.JT_MITER, pyclipper.ET_CLOSEDPOLYGON)

# polygon外扩
polygon_expand = padding.Execute(distance)[0]
polygon_expand = [tuple(l) for l in polygon_expand]
print(polygon_expand)
# [(75, 75), (275, 75), (275, 225), (75, 225)]
# polygon内缩
polygon_shrink = padding.Execute(-distance)[0]
polygon_shrink = [tuple(l) for l in polygon_shrink]
print(polygon_shrink)
# [(125, 125), (225, 125), (225, 175), (125, 175)]

# 作图
canvas = np.zeros((300,350,3), dtype=np.uint8)
# 原轮廓用红色线条展示
draw_img(subject, canvas, color=(0,0,255))
# 外扩轮廓用绿色线条展示
draw_img(polygon_expand, canvas, color=(0,255,0))
# 内缩轮廓用蓝色线条展示
draw_img(polygon_shrink, canvas, color=(255,0,0))

cv2.imshow("Canvas", canvas) 
cv2.waitKey(0)

  整体效果图如下,红色框为标注框,绿色框外扩后的效果,蓝色框为内缩后的效果。
在这里插入图片描述

0.示例说明

   假定图像尺寸为(35,30,3), 图中存在文字标注框 text_box: [[10,10], [25,10], [25,20], [10,20]],如下图所示,红色框即为文本标注框。
   以此示例简单说明概率图、阈值图和二值化图的创建。
在这里插入图片描述

1.概率图标签

  使用收缩的方式获取算法训练需要的概率图标签。
  标注框内缩后,覆盖区域的概率值为1,其余区域概率值为0

# 创建概率图
h, w = 30, 35
probability_map = np.zeros((h, w), dtype=np.float32)
# 标注区域内缩 
# 经过distance的公式计算(D=2.52)和pyclipper库的内缩坐标处理
# text_box: [[10,10], [25,10], [25,20], [10,20]] ===> shrink_box: [[13,13], [22,13], [22,17], [13,17]]
# shrink_box为标注框经过内缩后的区域
shrink_box = [[13,13], [22,13], [22,17], [13,17]]
shrink_box = np.array(shrink_box).reshape(-1,2)
# 将概率图中的shrink区域赋值为1
cv2.fillPoly(probability_map, [shrink_box.astype(np.int32)], 1)

  下图所示即为概率图,其中文字区域的概率值为1,背景区域的概率值为0
在这里插入图片描述

2.阈值图标签

  阈值图中,需计算各位置到标注框的距离,距离越近的位置,阈值越高。
基本步骤如下:

(1) 标注框外扩

import pyclipper
import numpy as np
from shapely.geometry import Polygon

# 论文默认shrink值
r=0.4
# 标注框
subject = [[10,10],[25,10],[25,20],[10,20]]
# 创建Polygon对象
polygon = Polygon(subject)

# 计算偏置distance
distance = polygon.area*(1-np.power(r, 2))/polygon.length
print(distance)
# 2.52

# 创建PyclipperOffset对象
padding = pyclipper.PyclipperOffset()
# 向ClipperOffset对象添加一个路径用来准备偏置
padding.AddPath(subject, pyclipper.JT_MITER, pyclipper.ET_CLOSEDPOLYGON)

# polygon外扩
polygon_expand = padding.Execute(distance)[0]
polygon_expand = [tuple(l) for l in polygon_expand]
print(polygon_expand)
# [(7, 7), (28, 7), (28, 23), (7, 23)]

(2) 计算距离

   标注框外扩后,文字区域扩大,需计算区域内每个点到标注框的距离。标注框看作四条线段,计算出每个位置点到这四条线段的距离,取最小值为最终距离。距离的计算借助两个三角形公式: 余弦定理和面积公式。具体的计算过程如下:

在这里插入图片描述
由三角形面积公式,推导出h:
{ S = 1 2 ⋅ c ⋅ h S = 1 2 ⋅ a ⋅ b ⋅ s i n α ⇒ h = a ⋅ b c ⋅ s i n α \begin{cases} S = \cfrac{1}{2}\cdot c \cdot h \\ S = \cfrac{1}{2}\cdot a \cdot b \cdot sin\alpha \end{cases} ⇒ h = \cfrac{a \cdot b}{c} \cdot sin\alpha S=21chS=21absinαh=cabsinα
其中,a, b和c的值可以通过位置距离计算, s i n α sin\alpha sinα 通过余弦定理计算得出:
c o s 2 α = a 2 + b 2 − c 2 2 a b ⇒ s i n α = 1 − c o s 2 α cos^2\alpha = \cfrac{a^2 + b^2 - c^2}{2ab} ⇒ sin\alpha = \sqrt{\smash[b]{1 - cos^2\alpha}} cos2α=2aba2+b2c2sinα=1cos2α

(3) 距离归一化

   计算出每个位置点的值后,进行归一化。区域内所有的值除以前面计算出来的distance,限定取值在[0, 1]之间。得到相对距离比例的数值,如下图所示。
在这里插入图片描述

(4) 计算阈值图

   用1减去距离归一化的值,即获取到阈值图。靠近标注框的位置,阈值接近1,阈值图如下所示。
在这里插入图片描述参照核心代码如下,详见paddleocr。

import cv2
import numpy as np
import pyclipper
from shapely.geometry import Polygon
from matplotlib import pyplot as plt 


class MakeBorderMap(object):
    def __init__(self,
                 shrink_ratio=0.4,
                 thresh_min=0.3,
                 thresh_max=0.7,
                 **kwargs):
        self.shrink_ratio = shrink_ratio
        self.thresh_min = thresh_min
        self.thresh_max = thresh_max

    def __call__(self, data):

        img = data['image']
        text_polys = data['polys']
        ignore_tags = data['ignore_tags']

        canvas = np.zeros(img.shape[:2], dtype=np.float32)
        mask = np.zeros(img.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,data=data)
        #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, data):
        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)
        padding.AddPath(subject, pyclipper.JT_MITER, 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

        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)
        distance_map = np.round(distance_map, 3)
        data['distance_map'] = distance_map

        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]
        return result

if __name__ == "__main__":
    data = dict()
    data['image'] = np.zeros((30, 35, 1), dtype=np.uint8)
    data['polys'] = [[[10,10],[25,10],[25,20],[10,20]]]
    data['ignore_tags'] = [False] 
    # 1. 声名MakeBorderMap函数
    generate_text_border = MakeBorderMap()
    
    # 2. 根据解码后的输入数据计算border
    data = generate_text_border(data)
    threshold_map = data['threshold_map']
    
    # 3. 阈值图可视化 
    plt.imshow(threshold_map)

在这里插入图片描述

3.二值化图标签

三、损失计算

1. BCELoss损失函数

   DBNet使用二分类交叉熵损失函数(binary cross-entropy),并且是单标签的,即一个输入样本对应于一个分类输出(1或0)。对于包含N个样本的数据D(x, y),BCE损失计算公式如下:
l o s s = 1 N ∑ 1 ≤ i ≤ n l i loss =\frac{1}{N} \sum_{\mathclap{1\le i\le n}} l_i loss=N11inli
其中, l i = − w ( y i ⋅ log ⁡ x i + ( 1 − y i ) ∗ log ⁡ ( 1 − x i ) ) l_i = -w(y_i\cdot \log x_i + (1-y_i)*\log(1-x_i)) li=w(yilogxi+(1yi)log(1xi))为第i个样本对应的loss。 w w w是超参数,对于单标签二分类,设不设置 w w w没有影响。第i个样本的损失为:
l i = { − log ⁡ x i if  y = 1 − log ⁡ ( 1 − x i ) if  y = 0 l_i = \begin{cases} -\log x_i &\text{if } & y=1 \\ -\log(1-x_i) &\text{if } & y=0 \end{cases} li={ logxilog(1xi)if if y=1y=0

2. 二值化函数

(1) 标准二值化

  语义分割网络生成概率图 P ∈ R H ∗ W P\in R^{H*W} PRHW,其中 H H H W W W分别代表高度和宽度。概率图需要转换成二值化图,像素值为1的地方代表文字区域。标准二值化过程如下:
B i , j = { 1 if  P i , j > = t 0 if  o t h e r w i s e B_{i,j} = \begin{cases} 1 &\text{if } &P_{i,j}>=t \\ 0 &\text{if } &otherwise \end{cases} Bi,j={ 10if if Pi,j>=totherwise
其中, t t t是固定的阈值, ( i , j ) (i,j) (i,j)代表图上的坐标。

(2) 可微分二值化

  标准二值化函数是不连续的,其过程是不可微的,不能随着语义分割网络的训练而优化。为了解决这个问题,论文提出了一个近似二值化过程的阶跃函数,即可微分二值化(Differentiable binarization),其过程如下:
B ˆ i , j = 1 1 + e − k ( P i , j − T i , j ) \^{B}_{i,j} = \frac{1}{1+e^{-k(P_{i,j}-T_{i,j})}} Bˆi,j=1+ek(Pi,jTi,j)1
其中, B ˆ \^{B} Bˆ是输出的二值化图, T T T是网络学到的自适应阈值图, k k k表示放大因子,通常设置为50。
下图(a)中SB表示标准二值化过程,DB表示可微分二值化过程;图(b)和图©分别表示 l + l_+ l+ l − l_- l的导数曲线

  DBNet提升效果的原因可以通过反向梯度传播解释。在BCELoss中,定义 f ( x ) = 1 1 + e − k x f(x)=\frac{1}{1+e^{-kx}} f(x)=1+ekx1,其中 x = P i , j − T i , j x=P_{i,j}-T_{i,j} x=Pi,jTi,j。则正样本损失和负样本损失计算如下:
{ l + = − log ⁡ 1 1 + e − k x l − = − log ⁡ ( 1 − 1 1 + e − k x ) \begin{cases} l_+ = - \log \frac{1}{1+e^{-kx}} \\ l_- = - \log (1- \frac{1}{1+e^{-kx}}) \end{cases} { l+=log1+ekx1l=log(11+ekx1)
loss对于x的偏导数计算如下:
{ ∂ l + ∂ x = − k f ( x ) e − k x ∂ l − ∂ x = k f ( x ) \begin{cases} \frac{\partial l_+}{\partial x} = -kf(x)e^{-kx} \\ \frac{\partial l_-}{\partial x} = kf(x) \end{cases} { xl+=kf(x)ekxxl=kf(x)
通过偏导数我们可以注意到:

(1) 错误预测的梯度通过增强因子 k k k被加强,更利于网络的优化学习,使预测结果更清晰
(2) 图(b)为 l + l_+ l+的导数曲线,如果发生误报(正样本被预测为负样本,即x<0), 图(b)中小于0的部分导数值非常大,说明损失也非常大,则更能清晰的进行梯度回传
(3) 图©为 l − l_- l的导数曲线,如果发生误报(负样本被预测为正样本,即x>0), 梯度也比较大,损失也很大

3. 整体损失计算

  模型训练过程中输出三个图:概率图、阈值图和二值化图。从而在损失函数计算时,也要结合这3个图与它们对应的真实标签构建3部分损失函数。总的损失函数公式定义如下:
L = L s + ɑ × L b + β × L t L=L_s + ɑ×L_b + β×L_t L=Ls+ɑ×Lb+β×Lt
其中,L为总的损失, L s L_s Ls为概率图损失, L b L_b Lb为二值化图损失, L t L_t Lt为阈值图损失。 ɑ ɑ ɑ β β β为权重系数,论文中分别设置为1和10。
L s = L b = ∑ i ∈ S l y i log ⁡ x i + ( 1 − y i ) log ⁡ ( 1 − x i ) L_s = L_b = \sum_{\mathclap{i \in S_l}} y_i \log x_i + (1-y_i)\log(1-x_i) Ls=Lb=iSlyilogxi+(1yi)log(1xi)
  对于 L s L_s Ls L b L_b Lb的损失计算都使用BCELoss, 为了解决正负样本不均衡的问题,在损失计算过程中使用错题集策略,同时正负样本比例设为1:3。
   L t L_t Lt计算方式为扩展多边形 G d G_d Gd内预测结果和阈值图标签的L1距离之和。
L t = ∑ i ∈ R d ∣ y i ∗ − x i ∗ ∣ L_t = \sum_{\mathclap{i \in R_d}} |{y_i}^* - {x_i}^*| Lt=iRdyixi
其中, R d R_d Rd是扩展多边形 G d G_d Gd内的像素索引, y ∗ y^* y是阈值图标签。

猜你喜欢

转载自blog.csdn.net/yewumeng123/article/details/127503815
今日推荐