DBNet 解析
1. 背景介绍
文本检测分为基于回归和基于分割两种方法,DBNet 的原理是基于分割算法。对于一般分割算法流程:先通过网络输出文本分割的概率图,然后使用设定阈值将概率图转化为二值图,然后通过后处理得到检测结果(文本框坐标)。但是缺点在于阈值的选取非常关键。
DBNet 针对这个问题,提出可微分二值化的概念:即对每一个像素点进行自适应二值化,二值化阈值由网络学习得到,彻底将二值化这一步骤加入到网络里一起训练,这样最终的输出图对于阈值就会非常鲁棒。
自适应阈值

上图中蓝线部分就是传统的文本检测算法流程:
- 首先,通过设置一个固定阈值将分割网络训练得到的概率图(segmentation map)转化为二值图(binarization map)。
- 然后,使用一些启发式技术(例如像素聚类)将像素分组为文本实例。
上图红色部分就是 DBNet 的算法流程:
通过分割网络获取概率图和阈值图,阈值图是网络预测得出,并不是固定的值,这样就可以很好将背景与前景分离出来,但是这样的操作会给训练带来梯度不可微的情况,对此对于二值化提出了一个叫做 Differentiable Binarization来解决不可微的问题。
2. DBNet 算法整体架构

上图是 DBNet 模型网络结构示意图,主要分为 3 个模块:
-
第一模块(1):左边的红框使用的是一个 FPN 结构,分为自底向上的卷积操作与自顶向下的上采样,以此来获取多尺度的特征。1 图下面部分是 3x3 的卷积操作,按照卷积公式分别获取原图大小比例的
1/2、1/4、1/8、1/16、1/32
的特征图;然后自顶向下进行上采样 x2,然后与自底向上生成的相同大小的特征图融合;融合之后再采用 3x3 的卷积消除上采样的混叠效应;最后对每层输出结果进行上采样,统一为 1/4 大小的特征图。 - 第二模块(2):将 1/4 大小的特征图经过一系列卷积和转置卷积的机构获取概率图 P 和阈值图 T,可参考 FCN 网络结构,目的是生成与原图一样大小的特征图 P 和 T。
- 第三模块(3):将特征图 P 和 T 经过 DB 方法(后续介绍)得到近似二值图。
经过上面三个模块,可以得到概率图、阈值图和近似二值图。在训练过程中,对这三个图进行监督学习,更新各个模块的参数。在推理过程中,直接使用概率图,然后使用固定阈值获取结果。
下面介绍可微二值化的发展流程:
标准二值化
在传统的图像分割算法中,我们获取概率图后,会使用标准二值化(Standard Binarize)方法进行处理,将低于阈值的像素点置0,高于阈值的像素点置1,公式如下:
是预先设置的阈值,
代表的是像素点坐标位置。可见标准的二值化是不可微的,所以也就无法放入到网络中进行优化学习。
可微二值化
可微二值化就是将标准二值化中的阶跃函数进行了近似,公式如下所示:
可微二值化本质上是一个 带系数 k 的 sigmoid 函数,取值范围为,k 是膨胀因子(经验型设置为50)。
是指概率图像素点,
是指阈值图像素点。
标准二值化和可微二值化的对比如下图 (a) 所示,SB 曲线代表标准二值化,DB 代表可微二值化,可以看到曲线变得更为平滑,也就是可微:

除了可微之外,DB 方法也会改善算法的性能,在反向传播是梯度的计算上进行观察。当使用交叉熵损失(代表文字区域)时,正负样本的 loss 分别为
和
,公式如下:
对输入 求偏导,则会得到:
可以看到 增强因子k 对于错误预测对梯度的影响变大了,从而可以促进模型的优化过程产生更为清晰的预测结果。对于时,按照(a)图属于正样本(文字区域),
时属于负样本(非文字区域)。
上图(b)是指 的导数曲线,如果发生漏报(正样本被预测为负样本
),图(b)小于 0 的部分导数非常大,证明损失也是非常大的,则更能清晰的进行梯度回传。同理,图(c)代表
的导数曲线,当发生误报(负样本被预测为正样本
),导数也是非常大的,损失也比较大。
3. 真实标签生成
DB 网络中,训练过程中网络有 3 个输出:概率图、阈值图和近似二值图:
- 概率图:图中每个像素点的值为该位置属于文本区域的概率。
- 阈值图:图中每个像素点的值为该位置的二值化阈值。
- 近似二值图:由概率图和阈值图通过 DB 算法计算得到,图中像素的值为 0 或 1。
概率图的标签 和阈值图标签
,DB 网络参考 PSENet 中的方法,使用扩张和收缩的方式构建阈值图和概率图。在该方法中,对于一幅文字图像,文本区域的每个多边形使用一组线段
来进行描述, n 为线段个数。
概率图标签
对于概率图和近似二值图来说,使用收缩的方式构建标签(Vatti clipping算法),收缩的偏移量D由多边形的周长 L 和面积 A 计算得到,公式如下:其中, r 是收缩因子,实验中经验设置为 0.4 。
阈值图标签
- 首先使用
计算过程中的偏移量 D 进行多边形的扩充。得到
和
之间的区域。
- 计算之间区域到原始框的距离,并得到最近边(长方形就是 4 条边)的距离。最外面的大框线上区域和最里面的小框线上区域计算为 D ,原始框位置的距离为 0。
- 进行第一次的归一化(除以D),这样距离控制到 [0,1] 之间,并且最中间的区域越接近0,越里面和越外面的区域越接近1。然后使用 1-X 操作,让越中心的距离为 1,越边缘的距离为 0。(这样图片显示就是中间亮两头暗)。
- 最终再进行缩放,比如归一化到 [0.3,0.7] 的值。
4. 损失函数
由于在训练阶段输出 3 个预测图(大小与原图一致),所以在损失函数中,也需要有对应的真实标签去构建 3 部分损失函数。总的损失函数的公式定义如下:
其中,为总的损失,
为近似二值图的损失,使用 Dice 损失;
为概率图损失,使用带 OHEM 的 Dice 损失;
为阈值图损失,使用预测值和标签间的 𝐿1 距离。其中,𝛼 和 𝛽 为权重系数。
接下来分析这 3 个loss:
1)首先是Dice Loss,Dice Loss是比较预测结果跟标签之间的相似度,常用于二值图像分割。
2)其次是MaskL1 Loss,是计算预测值和标签间的𝐿1距离
3)最后是Balance Loss,是带OHEM的Dice Loss,目的是为了改善正负样本不均衡的问题。OHEM 为一种特殊的自动采样方式,可以自动的选择难样本进行loss的计算,从而提升模型的训练效果。Dice Loss (迁移)
其中 |X∩Y| 是X和Y之间的交集,|X|和|Y|分表表示X和Y的元素的个数,其中,分子的系数为2,是因为分母存在重复计算X和Y之间的共同元素的原因。
同时,一般会加入平滑因子,防止分子分母全为0。对于分割任务而言,|X| 和 |Y| 代表分割的 ground truth 和 predict_mask。计算步骤:
首先,使用预测图 predict_mask 和 ground truth 之间的点乘。
![]()
逐元素相乘的结果元素的相加和。
![]()
计算|X|和|Y|,这里可以采用直接元素相加,也可以采用元素平方求和的方法:
![]()
5. 模型训练流程
以 PaddleOCR (https://github.com/PaddlePaddle/PaddleOCR/blob/0791714b91/deploy/lite/readme.md) 为例,模型的训练流程分为以下部分:
- 数据预处理:主要包括解析图片、真实标签处理、随机裁剪和图片增强。
- 模型结构部分:分为 Backbone、Neck、Head 部分。
- loss 部分:分为
。
- metric 部分:经过后处理之后进行评价指标的计算。
5.1 数据预处理
数据预处理包含以下部分:
DecodeImage 解析图片、DetLabelEncode 解析 label 文件、IaaAugment 进行数据增强、EastRandomCropData 随机裁剪(裁剪到指定 size (960, 960, 3))、MakeBorderMap 阈值图真实标签生成(batch, 960, 960)、MakeShrinkMap 概率图标签生成(batch, 960, 960)、NormalizeImage 归一化、ToCHWImage 纬度变化为(3, 960, 960)、KeepKeys 指定格式。

经过数据预处理 image 字段的大小变为(batch, 3, 960, 960)。
5.2 模型前向传播
模型依次经过 backbone(MobileNetV3)--> neck(DBFPN) --> head(DBHead) 步骤,纬度变化如下所示:
输入的 images size: [2, 3, 960, 960],batch 为 2
backbone 输出1:backbone behind [2, 16, 240, 240],尺寸对应 1/4
backbone 输出2:backbone behind [2, 24, 120, 120],尺寸对应 1/8
backbone 输出3:backbone behind [2, 56, 60, 60],尺寸对应 1/16
backbone 输出4:backbone behind [2, 480, 30, 30],尺寸对应 1/32
neck 输出:neck behind [2, 96, 240, 240],输出 1/4
head 输出:head behind [2, 3, 960, 960],与原图大小一致,分别代表 shrink_maps, threshold_maps, binary_maps,大小都为 [960, 960]
5.3 后处理
后处理的逻辑流程如下:
- 首先,对概率图进行固定阈值处理,得到分割图。
- 对分割图计算轮廓,遍历每个轮廓,去除太小的预测;对每个轮廓计算包围矩形,然后计算该矩形的预测score。
- 对矩形进行反向shrink操作,得到真实矩形大小;最后还原到原图 size 就可以了。
下面对 python 代码进行解析:
pred 是模型的输出,shape 为 (1,height, weight),训练阶段都为(1,960,960),推理阶段则不一定。

查看 boxes_from_bitmap 的代码:
def boxes_from_bitmap(self, pred, _bitmap, dest_width, dest_height):
'''
_bitmap: single map with shape (1, H, W),
whose values are binarized as {0, 1}
'''
bitmap = _bitmap
height, width = bitmap.shape
# findContours 获取轮廓,如长方形获取四点顶点坐标
outs = cv2.findContours((bitmap * 255).astype(np.uint8), cv2.RETR_LIST,
cv2.CHAIN_APPROX_SIMPLE)
# py2、py3 不同版本的情况
if len(outs) == 3:
img, contours, _ = outs[0], outs[1], outs[2]
elif len(outs) == 2:
contours, _ = outs[0], outs[1]
# 文本框最大数量
num_contours = min(len(contours), self.max_candidates)
boxes = []
scores = []
for index in range(num_contours):
contour = contours[index]
# 计算最小包围矩,获取四个坐标点,左上为起点(顺时针)
points, sside = self.get_mini_boxes(contour)
# 长方形中宽高最小值过滤
if sside < self.min_size:
continue
points = np.array(points)
# 利用 points 内部预测概率值,计算出一个score,作为实例的预测概率
score = self.box_score_fast(pred, points.reshape(-1, 2))
# score 得分的过滤
if self.box_thresh > score:
continue
# shrink反向还原,之前概率图进行了缩放,故还原
box = self.unclip(points).reshape(-1, 1, 2)
box, sside = self.get_mini_boxes(box)
if sside < self.min_size + 2:
continue
box = np.array(box)
# 还原到原始坐标,反向还原之后,还需要还原到原始图片(原始图片在预处理时被缩放处理)
box[:, 0] = np.clip(
np.round(box[:, 0] / width * dest_width), 0, dest_width)
box[:, 1] = np.clip(
np.round(box[:, 1] / height * dest_height), 0, dest_height)
boxes.append(box.astype(np.int16))
scores.append(score)
return np.array(boxes, dtype=np.int16), scores
def get_mini_boxes(self, contour):
# 返回点集 cnt 的最小外接矩形:# 得到最小外接矩形的(中心(x,y), (宽,高), 旋转角度)
bounding_box = cv2.minAreaRect(contour)
# 排序,最终以左上的坐标为起点,顺时针排列四个坐标点
points = sorted(list(cv2.boxPoints(bounding_box)), key=lambda x: x[0])
index_1, index_2, index_3, index_4 = 0, 1, 2, 3
if points[1][1] > points[0][1]:
index_1 = 0
index_4 = 1
else:
index_1 = 1
index_4 = 0
if points[3][1] > points[2][1]:
index_2 = 2
index_3 = 3
else:
index_2 = 3
index_3 = 2
box = [
points[index_1], points[index_2], points[index_3], points[index_4]
]
return box, min(bounding_box[1])
6. 模型推理流程
如果不考虑 label,则其处理逻辑和训练逻辑有一点不一样,其把图片统一 resize 到指定的长度进行预测。
数据预处理
数据预处理没有训练阶段的数据增强、随机裁剪和生成标签部分,但是会存在一个 resize 的操作,将宽高设置为 32 的倍数。
DetResizeForTest 步骤如下:
1)对图片进行等比例的放缩,设置最大的尺寸为 960。如 3 张图片分别为 (720,1280)、(230,230)、(1150,720)。
2)对图片进行放缩,以最大边为准(操作960的直接放缩到 960),缩小至能被 32 整除的最大尺寸。则输出:(512,960,3)、(224,224,3)、(960,576,3)。由于宽高都为 32 的倍数,则放缩的比例不统一,但尽可能相差不大。
后续的操作就与训练阶段一致,通过网络结构,然后进行后处理获得结果。