当前位置: 首页 > news >正文

北京建站者公司永安网页设计

北京建站者公司,永安网页设计,网站前端模板,poco摄影网OCR经典神经网络(二)文本检测算法DBNet算法原理及其在icdar15数据集上的应用 场景文本检测任务#xff0c;一直以来是OCR整个任务中最为重要的一环。虽然有一些相关工作是端对端的#xff0c;但是从工业界来看#xff0c;相关落地应用较为困难。因此#xff0c;两阶段的OC…OCR经典神经网络(二)文本检测算法DBNet算法原理及其在icdar15数据集上的应用 场景文本检测任务一直以来是OCR整个任务中最为重要的一环。虽然有一些相关工作是端对端的但是从工业界来看相关落地应用较为困难。因此两阶段的OCR方案一直是优先考虑的。在两阶段中文本检测文本识别算法中文本检测是极为重要的一环而DBNet算法是文本检测的首选。DB是一个基于分割的文本检测算法其提出可微分阈值Differenttiable Binarization moduleDB module采用动态的阈值区分文本区域与背景。 我们已经了解了文本识别算法CRNNOCR经典神经网络(一)文本识别算法CRNN算法原理及其在icdar15数据集上的应用今天我们了解仍旧由华中科技大学白翔老师团队在2019年提出的DBNet模型。论文链接https://arxiv.org/pdf/1911.08947同样百度开源的paddleocr中集成了此算法https://github.com/PaddlePaddle/PaddleOCR 1 DBNet算法原理 目前文字检测算法可以大致分为两类基于回归的方法和基于分割的方法。 基于分割的普通文本检测算法其流程如下图中的蓝色箭头所示 此类方法得到分割结果之后采用一个固定的阈值得到二值化的分割图之后采用诸如像素聚类的启发式算法得到文本区域。在基于分割的普通文本检测算法中阈值不同对性能影响较大。另外由于是在pixel层面操作后处理会比较复杂且时间消耗较大 DB算法的流程如下图红色箭头所示 最大的不同在于DB有一个阈值图通过网络去预测图片每个位置处的阈值而不是采用一个固定的值更好的分离文本背景与前景。二值化阈值由网络学习得到彻底将二值化这一步骤加入到网络里一起训练这样最终的输出图对于阈值就会具有非常强的鲁棒性在简化了后处理的同时提高了文本检测的效果。 图1 DB模型与其他方法的区别 1.1 DBNet网络 1.1.1 网络结构 图2 DB模型网络结构示意图 DBNet的整个流程如下 图像经过FPN网络结构得到四个特征图分别为1/4、1/8、1/16以及1/32大小将四个特征图分别上采样为1/4大小然后concat得到特征图F由F得到概率图probability map(P) 和阈值图threshold map(T)通过P、T计算(计算公式后面介绍)得到近似二值图approximate binary map 对于每个网络一定要区分训练和推理阶段的不同 训练阶段对P、T、B进行监督训练P和B是用的相同的监督信号label推理阶段通过P或B就可以得到文本框。 1.1.2 二值化操作 在传统的图像分割算法中获取概率图后会使用标准二值化Standard Binarize方法进行处理。 将低于阈值的像素点置0高于阈值的像素点置1公式如下 B i , j { 1 , P i , j t 0 , o t h e r w i s e . 其中 t 为预先设定的固定阈值 B_{i,j}\left\{ \begin{aligned} 1 , P_{i,j} t\\ 0 , otherwise. \end{aligned} \right.\\ 其中t为预先设定的固定阈值 Bi,j​{1,Pi,j​t0,otherwise.​其中t为预先设定的固定阈值 但是标准的二值化方法是不可微的导致网络无法端对端训练。 为了解决这个问题DB算法提出了可微二值化Differentiable BinarizationDB。 可微二值化将标准二值化中的阶跃函数进行了近似使用如下公式进行代替 B ^ 1 1 e − k ( P i , j − T i , j ) \hat{B} \frac{1}{1 e^{-k(P_{i,j}-T_{i,j})}} B^1e−k(Pi,j​−Ti,j​)1​ 其中P是上文中获取的概率图T是上文中获取的阈值图k是增益因子在实验中根据经验选取为50。 标准二值化和可微二值化的对比图如 下图3a 所示。可以看出蓝色的DB曲线与黄色的SB曲线(标准二值化曲线)具有很高的相似度并且DB曲线是可微分的从而达到了二值化的目的。 由上文可以知道 P i , j P_{i,j} Pi,j​ 和 T i , j T_{i,j} Ti,j​ 是输入所以可以将其看为输入即 P i , j − T i , j x P_{i,j}−T_{i,j}x Pi,j​−Ti,j​x这样来看的话这个函数其实就是一个带系数k的sigmoid函数。 那么为什么可微分二值化会带来性能提升? DBNet性能提升的原因我们可以通过梯度的反向传播进行解释 首先对于该任务的分割网络每个像素点都是二分类即文字区域(正样本为1)和非文字区域(负样本为0)可以使用BCELoss 那么当使用交叉熵损失时正负样本的loss分别为 l l_ l​ 和 l − l_- l−​ l − l o g ( 1 1 e − k ( P i , j − T i , j ) ) l − − l o g ( 1 − 1 1 e − k ( P i , j − T i , j ) ) l_ -log(\frac{1}{1 e^{-k(P_{i,j}-T_{i,j})}})\\ l_- -log(1-\frac{1}{1 e^{-k(P_{i,j}-T_{i,j})}}) l​−log(1e−k(Pi,j​−Ti,j​)1​)l−​−log(1−1e−k(Pi,j​−Ti,j​)1​) 对输入 x x x 求偏导则会得到下面式子 δ l δ x − k f ( x ) e − k x δ l − δ x − k f ( x ) \frac{\delta{l_}}{\delta{x}} -kf(x)e^{-kx}\\ \frac{\delta{l_-}}{\delta{x}} -kf(x) δxδl​​−kf(x)e−kxδxδl−​​−kf(x) 可以发现增强因子会放大错误预测的梯度从而优化模型得到更好的结果。 图3b 中 x 0 x0 x0 的部分为正样本预测为负样本的情况可以看到增益因子k将梯度进行了放大图3c 中 x 0 x0 x0 的部分为负样本预测为正样本时梯度同样也被放大了。 图3DB算法示意图 1.2 标签生成 我们已经介绍了二值化操作通过下面公式我们很容易就得到了approximate binary map接下来该计算损失函数从而反向传播进行参数优化。 B ^ 1 1 e − k ( P i , j − T i , j ) 其中 P 是概率图 T 是阈值图 k 是增益因子根据经验选取为 50 。 概率图 G T 取值范围为 0 或 1 , 文字区域为 1 非文字区域为 0 阈值图 G T 取值范围为 [ 0.3 , 0.7 ] , 生成逻辑如下文 \hat{B} \frac{1}{1 e^{-k(P_{i,j}-T_{i,j})}}\\ 其中P是概率图T是阈值图k是增益因子根据经验选取为50。\\ 概率图GT取值范围为0或1,文字区域为1非文字区域为0\\ 阈值图GT取值范围为[0.3, 0.7],生成逻辑如下文 B^1e−k(Pi,j​−Ti,j​)1​其中P是概率图T是阈值图k是增益因子根据经验选取为50。概率图GT取值范围为0或1,文字区域为1非文字区域为0阈值图GT取值范围为[0.3,0.7],生成逻辑如下文 概率图P和近似二值图 B ^ \hat{B} B^使用相同的标签加上阈值图T的标签所以需要生成两个标签。要计算损失就需要标签上文说到训练时需要同时对P、T、B进行监督训练P和B是用的相同的监督信号T是作者加入的自适应阈值那么为什么还需要对T进行监督呢 1.2.1 概率图标签 下图上半部分是概率图P(也是近似二值图 B ^ \hat{B} B^)的标签生成过程。使用Vatti clipping算法将原始的多边形文字区域G红线区域收缩到Gs蓝线区域标签为蓝线区域内为文字区域(标记为1)蓝线区域外为非文字区域(标记为0)。收缩的偏移量D的计算公式如下 D A ( 1 − r 2 ) L A 是原始区域 ( 红色框 ) 的面积 L 是原始区域的周长 r 是收缩系数依经验设置为 r 0.4 D\frac{A(1-r^2)}{L}\\ A是原始区域(红色框)的面积L是原始区域的周长r是收缩系数依经验设置为r0.4 DLA(1−r2)​A是原始区域(红色框)的面积L是原始区域的周长r是收缩系数依经验设置为r0.4 1.2.2 阈值图标签 阈值图的标签制作流程 如上图下半部分所示首先将原始的多边形文字区域G扩张到Gd绿线区域收缩偏移量D计算公式如上所示。 将收缩框Gs蓝线和扩张框Gd绿线之间的间隙视为文本区域的边界计算这个间隙里每个像素点到原始图像边界G红线的归一化距离最近线段的距离。 计算完之后可以发现扩张框Gd绿线上的像素点和收缩框Gs蓝线上的像素点的归一化距离的值是最大的并且文字红线上的像素点的值最小为0。呈现出以红线为基准向Gs和Gd方向的值逐渐变大。 再对计算完的这些值进行归一化也就是除以偏移量D此时Gs蓝线和Gd绿线上的值变为1再用1减去这些值。最后得到红线上的值为1Gs和Gd线上的值为0。呈现出以红线为基准向Gs蓝线和Gd绿线方向的值逐渐变小。此时区域内的值取值范围为[0,1]。 最后还需要进行一定的缩放比如将1缩放至0.7将0缩放至0.3此时区域内的值取值范围为[0.3,0.7]。 阈值图进行监督的原因 下图的图c为没有监督的阈值图虽然没有进行监督但阈值图也会突出显示文本边界区域这表明类似边界的阈值图有利于最终结果。因此作者在阈值图上应用了类似边界的监督以提供更好的指导。下图中的图d为带监督的阈值图显然效果更好了。 1.2.3 损失函数 损失函数为概率图的损失 L s L_s Ls​、二值化图的损失 L b L_b Lb​和阈值图的损失 L t L_t Lt​ 的和 L L s α L b β L t 其中 α 和 β 分别设置为 1 和 10 LL_s\alpha L_b\beta L_t\\ 其中\alpha和\beta分别设置为1和10\\ LLs​αLb​βLt​其中α和β分别设置为1和10 L b L_b Lb​和 L t L_t Lt​都采用BCE Loss二元交叉熵为平衡正负样本的比例使用OHEM进行困难样本挖掘正样本:负样本1:3 L b L s ∑ i ∈ S l y i l o g x i ( 1 − y i ) l o g ( 1 − x i ) S l 表示使用 O H E M 进行采样正负样本比例为 1 : 3 L_bL_s\sum_{i\in S_l}y_ilogx_i (1-y_i)log(1-x_i)\\ S_l表示使用OHEM进行采样正负样本比例为1:3 Lb​Ls​i∈Sl​∑​yi​logxi​(1−yi​)log(1−xi​)Sl​表示使用OHEM进行采样正负样本比例为1:3 L t L_t Lt​使用Gd中预测值和标签值的 L 1 L1 L1距离 L t ∑ i ∈ R d ∣ y i ∗ − x i ∗ ∣ R d 是 G d 区域内的所有像素点不仅仅是 G d 和 G s 区域内 y ∗ 是阈值图的标签 L_t\sum_{i\in R_d}|y_i^*-x_i^*| \\ R_d是Gd区域内的所有像素点不仅仅是Gd和Gs区域内\\ y^*是阈值图的标签 Lt​i∈Rd​∑​∣yi∗​−xi∗​∣Rd​是Gd区域内的所有像素点不仅仅是Gd和Gs区域内y∗是阈值图的标签 1.3 推理流程 在推理时采用概率图或近似二值图便可计算出文本框为了方便作者选择了概率图这样在推理时便可删掉阈值分支。文本框的形成可分为三个步骤 1使用固定阈值0.2对概率图或近似二值图进行二值化得到二值图 2从二值图中得到连通区域收缩文字区域 3将收缩文字区域按Vatti clipping算法的偏移系数D’进行扩张得到最终文本框D’的计算公式如下 D ′ A ′ ∗ r ′ L ′ 其中 A ′ 、 L ′ 是收缩区域的面积、周长 r ′ 设置为 1.5 对应收缩比例 r 0.4 D\frac{A*r}{L}\\ 其中A、L是收缩区域的面积、周长\\ r设置为1.5对应收缩比例r0.4 D′L′A′∗r′​其中A′、L′是收缩区域的面积、周长r′设置为1.5对应收缩比例r0.4 推理阶段使用固定阈值的原因: 效率考虑在推理阶段为了获得更高的处理速度通常会选择使用固定阈值对概率图进行二值化。这是因为自适应阈值图T的生成需要额外的计算资源而在实际应用中我们往往需要在保证检测精度的同时尽可能提高处理速度。 简化后处理在推理阶段我们主要关注的是如何从概率图中提取出文本区域并生成相应的边界框。使用固定阈值进行二值化后可以通过简单的后处理步骤如轮廓提取、边界框生成等来得到最终的检测结果。这种方式不仅简单高效而且能够满足大多数实际应用场景的需求。 2 DBNet在icdar15数据集上的微调(paddleocr) 我们这里使用百度开源的paddleocr来对DBNet模型有更深的认识 paddleocr地址https://github.com/PaddlePaddle/PaddleOCRpaddleocr中集成的算法列表https://github.com/PaddlePaddle/PaddleOCR/blob/main/docs/algorithm/overview.md # git拉取下来解压 git clone https://gitee.com/paddlepaddle/PaddleOCR# 然后进入PaddleOCR目录安装PaddleOCR第三方依赖 pip install -r requirements.txt我们在paddleocr/tests目录下创建py文件进行下图的测试 百度将文字检测算法以及文字识别算法进行串联构建了PP-OCR文字检测与识别系统。在实际使用过程中检测出的文字方向可能不是我们期望的方向最终导致文字识别错误因此又在PP-OCR系统中引入了方向分类器。PP-OCR从工业界实用性角度出发经历多次更新论文如下 PP-OCR: https://arxiv.org/pdf/2009.09941PP-OCRv2: https://arxiv.org/pdf/2109.03144PP-OCRv3: https://arxiv.org/abs/2206.03001PP-OCRv4: 无论文 import cv2 import numpy as np from paddleocr import PaddleOCRocr PaddleOCR()# 默认会下载官方训练好的模型并将下载的模型放到用户目录下(我这里是C:\\Users\\Undo/.paddleocr) # recFalse表示不使用识别模型进行识别只执行文本检测 result ocr.ocr(imgrD:\python\py_works\paddleocr\tests\imgs\img_ppocr.png, detTrue # 文本检测器默认算法为DBNet, clsTrue # 方向分类器, recFalse # 文本识别PP-OCRv2中默认为CRNN模型,不过从PP-OCRv3识别模块不再采用CRNN更新为SVTR)print( * 50) print(result) print( * 50)# 4. 可视化检测结果 image cv2.imread(rD:\python\py_works\paddleocr\tests\imgs\img_ppocr.png) for box in result[0]:box np.reshape(np.array(box), [-1, 1, 2]).astype(np.int64)image cv2.polylines(np.array(image), [box], True, (255, 0, 0), 2)# 画出读取的图片 cv2.imshow(image, image) cv2.waitKey(0) cv2.destroyAllWindows()[[[[75.0, 553.0], [448.0, 540.0], [449.0, 572.0], [77.0, 585.0]], [[18.0, 506.0], [514.0, 488.0], [516.0, 533.0], [20.0, 550.0]], [[187.0, 457.0], [398.0, 448.0], [400.0, 481.0], [188.0, 490.0]], [[40.0, 413.0], [483.0, 390.0], [485.0, 431.0], [42.0, 453.0]]]]2.1 DBNet网络的搭建 DBNet文本检测模型可以分为三个部分 Backbone网络负责提取图像的特征Neck部分使用FPN网络特征金字塔结构增强特征Head网络计算文本区域概率图 # paddleocr/configs/det/det_mv3_db.yml Architecture:model_type: detalgorithm: DBTransform:Backbone:name: MobileNetV3scale: 0.5model_name: largeNeck:name: DBFPNout_channels: 256Head:name: DBHeadk: 502.1.1 骨干网络 DB文本检测网络的Backbone部分采用的是图像分类网络论文中使用了ResNet50paddleocr/configs/det/det_mv3_db.yml中采用MobileNetV3 large结构作为backbone。 DB的Backbone用于提取图像的多尺度特征输入的形状为[640, 640]backbone网络的输出有四个特征其形状分别是下采样4倍的C2[1, 16, 160, 160]下采样8倍的C3[1, 24, 80, 80]下采样16倍的C4[1, 56, 40, 40]下采样32倍的C5[1, 480, 20, 20]。 # ppocr\modeling\backbones\det_mobilenet_v3.pyclass MobileNetV3(nn.Layer):def __init__(self, in_channels3, model_namelarge, scale0.5, disable_seFalse, **kwargs):the MobilenetV3 backbone network for detection module.Args:params(dict): the super parameters for build networksuper(MobileNetV3, self).__init__()self.disable_se disable_seif model_name large:cfg [# k, exp, c, se, nl, s,[3, 16, 16, False, relu, 1],[3, 64, 24, False, relu, 2],[3, 72, 24, False, relu, 1], # C2 下采样4倍[5, 72, 40, True, relu, 2],[5, 120, 40, True, relu, 1],[5, 120, 40, True, relu, 1], # C3 下采样8倍[3, 240, 80, False, hardswish, 2],[3, 200, 80, False, hardswish, 1],[3, 184, 80, False, hardswish, 1],[3, 184, 80, False, hardswish, 1],[3, 480, 112, True, hardswish, 1],[3, 672, 112, True, hardswish, 1], # C4 下采样16倍[5, 672, 160, True, hardswish, 2],[5, 960, 160, True, hardswish, 1],[5, 960, 160, True, hardswish, 1],]cls_ch_squeeze 960elif model_name small:cfg [# k, exp, c, se, nl, s,[3, 16, 16, True, relu, 2],[3, 72, 24, False, relu, 2],[3, 88, 24, False, relu, 1],[5, 96, 40, True, hardswish, 2],[5, 240, 40, True, hardswish, 1],[5, 240, 40, True, hardswish, 1],[5, 120, 48, True, hardswish, 1],[5, 144, 48, True, hardswish, 1],[5, 288, 96, True, hardswish, 2],[5, 576, 96, True, hardswish, 1],[5, 576, 96, True, hardswish, 1],]cls_ch_squeeze 576else:raise NotImplementedError(mode[ model_name _model] is not implemented!)supported_scale [0.35, 0.5, 0.75, 1.0, 1.25]assert (scale in supported_scale), supported scale are {} but input scale is {}.format(supported_scale, scale)inplanes 16# conv1self.conv ConvBNLayer(in_channelsin_channels,out_channelsmake_divisible(inplanes * scale),kernel_size3,stride2,padding1,groups1,if_actTrue,acthardswish,)self.stages []self.out_channels []block_list []i 0inplanes make_divisible(inplanes * scale)for k, exp, c, se, nl, s in cfg:se se and not self.disable_sestart_idx 2 if model_name large else 0if s 2 and i start_idx:self.out_channels.append(inplanes)self.stages.append(nn.Sequential(*block_list))block_list []block_list.append(ResidualUnit(in_channelsinplanes,mid_channelsmake_divisible(scale * exp),out_channelsmake_divisible(scale * c),kernel_sizek,strides,use_sese,actnl,))inplanes make_divisible(scale * c)i 1# 最后一层卷积层block_list.append(ConvBNLayer(in_channelsinplanes,out_channelsmake_divisible(scale * cls_ch_squeeze),kernel_size1,stride1,padding0,groups1,if_actTrue,acthardswish,)) # C5 下采样32倍self.stages.append(nn.Sequential(*block_list))self.out_channels.append(make_divisible(scale * cls_ch_squeeze))for i, stage in enumerate(self.stages):self.add_sublayer(sublayerstage, namestage{}.format(i))def forward(self, x):x self.conv(x)out_list []# 将C2、C3、C4以及C5层的feature map进行保存# C2 shape (bs, 16, 160, 160)# C3 shape (bs, 24, 80, 80)# C4 shape (bs, 56, 40, 40)# C5 shape (bs, 480, 20, 20)for stage in self.stages:x stage(x)out_list.append(x)return out_list2.1.2 Neck部分 FPN的工作就是在检测前先将多个尺度的特征图进行一次bottom-up的融合这被证明是极其有效的特征融合方式几乎成为了后来目标检测的标准模式之一DBNet中也使用了FPN结构进行多尺度特征融合。 # paddleocr\ppocr\modeling\necks\db_fpn.py class DBFPN(nn.Layer):def __init__(self, in_channels, out_channels, use_asfFalse, **kwargs):super(DBFPN, self).__init__()self.out_channels out_channelsself.use_asf use_asfweight_attr paddle.nn.initializer.KaimingUniform()......def forward(self, x):c2, c3, c4, c5 x# 1、将c2, c3, c4, c5的通道数都调整为256in5 self.in5_conv(c5) # (bs, 256, 20, 20)in4 self.in4_conv(c4) # (bs, 256, 40, 40)in3 self.in3_conv(c3) # (bs, 256, 80, 80)in2 self.in2_conv(c2) # (bs, 256, 160, 160)# 2、通过FPN进行融合out4 in4 F.upsample(in5, scale_factor2, modenearest, align_mode1) # 1/16out3 in3 F.upsample(out4, scale_factor2, modenearest, align_mode1) # 1/8out2 in2 F.upsample(out3, scale_factor2, modenearest, align_mode1) # 1/4# 3、通道数都调整为64(256/4)p5 self.p5_conv(in5) # (bs, 64, 20, 20)p4 self.p4_conv(out4) # (bs, 64, 40, 40)p3 self.p3_conv(out3) # (bs, 64, 80, 80)p2 self.p2_conv(out2) # (bs, 64, 160, 160)# 4、上采样到原图的1/4p5 F.upsample(p5, scale_factor8, modenearest, align_mode1)p4 F.upsample(p4, scale_factor4, modenearest, align_mode1)p3 F.upsample(p3, scale_factor2, modenearest, align_mode1)# 在通道维度拼接 shape(bs, 256, 160, 160)fuse paddle.concat([p5, p4, p3, p2], axis1)if self.use_asf is True:fuse self.asf(fuse, [p5, p4, p3, p2])return fuse2.1.3 Head网络 计算文本区域概率图文本区域阈值图以及文本区域二值图 # ppocr\modeling\heads\det_db_head.py class Head(nn.Layer):def __init__(self, in_channels, kernel_list[3, 2, 2], **kwargs):super(Head, self).__init__()self.conv1 nn.Conv2D(in_channelsin_channels,out_channelsin_channels // 4,kernel_sizekernel_list[0],paddingint(kernel_list[0] // 2),weight_attrParamAttr(),bias_attrFalse,)self.conv_bn1 nn.BatchNorm(num_channelsin_channels // 4,param_attrParamAttr(initializerpaddle.nn.initializer.Constant(value1.0)),bias_attrParamAttr(initializerpaddle.nn.initializer.Constant(value1e-4)),actrelu,)self.conv2 nn.Conv2DTranspose(in_channelsin_channels // 4,out_channelsin_channels // 4,kernel_sizekernel_list[1],stride2,weight_attrParamAttr(initializerpaddle.nn.initializer.KaimingUniform()),bias_attrget_bias_attr(in_channels // 4),)self.conv_bn2 nn.BatchNorm(num_channelsin_channels // 4,param_attrParamAttr(initializerpaddle.nn.initializer.Constant(value1.0)),bias_attrParamAttr(initializerpaddle.nn.initializer.Constant(value1e-4)),actrelu,)self.conv3 nn.Conv2DTranspose(in_channelsin_channels // 4,out_channels1,kernel_sizekernel_list[2],stride2,weight_attrParamAttr(initializerpaddle.nn.initializer.KaimingUniform()),bias_attrget_bias_attr(in_channels // 4),)def forward(self, x, return_fFalse):# 1、通过3×3卷积降维:(bs, 256, 160, 160) - (bs, 64, 160, 160)x self.conv1(x)x self.conv_bn1(x)# 2、通过转置卷积将feature map由原图的1/4大小映射到原图1/2: (bs, 64, 160, 160)- (bs, 64, 320, 320)x self.conv2(x)x self.conv_bn2(x)if return_f is True:f x# 3、通过转置卷积将feature map由原图的1/2大小映射到原图大小并且输出维度为1: (bs, 64, 320, 320)- (bs, 1, 640, 640)x self.conv3(x)x F.sigmoid(x)if return_f is True:return x, freturn xclass DBHead(nn.Layer):Differentiable Binarization (DB) for text detection:see https://arxiv.org/abs/1911.08947args:params(dict): super parameters for build DB networkdef __init__(self, in_channels, k50, **kwargs):super(DBHead, self).__init__()self.k kself.binarize Head(in_channels, **kwargs)self.thresh Head(in_channels, **kwargs)def step_function(self, x, y):可微二值化的实现通过概率图和阈值图计算近似二值图return paddle.reciprocal(1 paddle.exp(-self.k * (x - y)))def forward(self, x, targetsNone):# 1、获取概率图shrink_maps self.binarize(x) # (bs, 1, 640, 640)if not self.training:# 推理过程只需概率图return {maps: shrink_maps}# 2、获取阈值图threshold_maps self.thresh(x) # (bs, 1, 640, 640)# 3、通过概率图和阈值图计算得到近似二值图binary_maps self.step_function(shrink_maps, threshold_maps) # (bs, 1, 640, 640)# 4、训练时将概率图、阈值图以及近似二值图 按照通道进行拼接y paddle.concat([shrink_maps, threshold_maps, binary_maps], axis1)return {maps: y} 2.2 数据集加载及模型训练 2.2.1 数据集的下载 提供一份处理过的icdar15数据集 链接: https://pan.baidu.com/s/1ZYaS22cOv2FGIvOpd9FBdQ 提取码: rjbc 数据集应有如下文件结构 text_localization └─ icdar_c4_train_imgs/ icdar数据集的训练数据└─ ch4_test_images/ icdar数据集的测试数据└─ train_icdar2015_label.txt icdar数据集的训练标注└─ test_icdar2015_label.txt icdar数据集的测试标注提供的标注文件格式为 图像文件名 json.dumps编码的图像标注信息 ch4_test_images/img_61.jpg [{transcription: MASA, points: [[310, 104], [416, 141], [418, 216], [312, 179]], ...}]json.dumps编码前的图像标注信息是包含多个字典的list字典中的points表示文本框的四个点的坐标(x, y)从左上角的点开始顺时针排列。 transcription中的字段表示当前文本框的文字在文本检测任务中并不需要这个信息。 如果您想在其他数据集上训练PaddleOCR可以按照上述形式构建标注文件。 如果transcription字段的文字为’*‘或者’###‘表示对应的标注可以被忽略掉因此如果没有文字标签可以将transcription字段设置为空字符串。 下载完数据集后我们复制一份paddleocr/configs/det/det_mv3_db.yml文件到paddleocr\tests\configs进行修改 2.2.2 模型的训练与预测 我这里不用命令行执行在paddleocr\tests目录下创建一个py文件执行训练过程通过下面的py文件我们就可以愉快的查看源码了。 def train_det():from tools.train import program, set_seed, main# 配置文件的源地址地址 paddleocr/configs/det/det_mv3_db.ymlconfig, device, logger, vdl_writer program.preprocess(is_trainTrue)###############修改配置也可在yml文件中修改################### 加载预训练模型模型下载地址如下# https://paddleocr.bj.bcebos.com/pretrained/MobileNetV3_large_x0_5_pretrained.pdparamsconfig[Global][pretrained_model] rC:\Users\Undo\.paddleocr\whl\backbone\MobileNetV3_large_x0_5_pretrained# 评估频率config[Global][eval_batch_step] [0, 200]# log的打印频率config[Global][print_batch_step] 10# 训练的epochsconfig[Global][epoch_num] 1# 随机种子seed config[Global][seed] if seed in config[Global] else 1024set_seed(seed)###############模型训练##################main(config, device, logger, vdl_writer, seed)def infer_det():# 加载自己训练的模型from tools.infer_det import main, programconfig, device, logger, vdl_writer program.preprocess()config[Global][use_gpu] Falseconfig[Global][infer_img] rD:\python\py_works\paddleocr\doc\imgs_en\img_12.jpgconfig[Global][checkpoints] rD:\python\py_works\paddleocr\tests\output\db_mv3\best_accuracy# 这里加了add_config这个参数源码中没有main(add_config(config, device, logger, vdl_writer))if __name__ __main__:train_det()# infer_det()main方法中定义了训练的脚本 # paddleocr/tools/train.py def main(config, device, logger, vdl_writer, seed):# init dist environmentif config[Global][distributed]:dist.init_parallel_env()global_config config[Global]# build dataloaderset_signal_handlers()# 1、创建dataloadertrain_dataloader build_dataloader(config, Train, device, logger, seed)......if config[Eval]:valid_dataloader build_dataloader(config, Eval, device, logger, seed)else:valid_dataloader Nonestep_pre_epoch len(train_dataloader)# 2、后处理程序# build post processpost_process_class build_post_process(config[PostProcess], global_config)# 3、模型构建# build model.....model build_model(config[Architecture])use_sync_bn config[Global].get(use_sync_bn, False)if use_sync_bn:model paddle.nn.SyncBatchNorm.convert_sync_batchnorm(model)logger.info(convert_sync_batchnorm)model apply_to_static(model, config, logger)# 4、构建损失函数# build lossloss_class build_loss(config[Loss])# 5、构建优化器# build optimoptimizer, lr_scheduler build_optimizer(config[Optimizer],epochsconfig[Global][epoch_num],step_each_epochlen(train_dataloader),modelmodel,)# 6、创建评估函数# build metriceval_class build_metric(config[Metric])......# 7、加载预训练模型# load pretrain modelpre_best_model_dict load_model(config, model, optimizer, config[Architecture][model_type])if config[Global][distributed]:model paddle.DataParallel(model)# 8、模型训练# start trainprogram.train(config,train_dataloader,valid_dataloader,device,model,loss_class,optimizer,lr_scheduler,post_process_class,eval_class,pre_best_model_dict,logger,step_pre_epoch,vdl_writer,scaler,amp_level,amp_custom_black_list,amp_custom_white_list,amp_dtype,)2.2.3 数据预处理 数据预处理共包括如下方法 图像解码将图像转为Numpy格式标签解码解析txt文件中的标签信息并按统一格式进行保存基础数据增广包括随机水平翻转、随机旋转随机缩放随机裁剪等获取阈值图标签使用扩张的方式获取算法训练需要的阈值图标签获取概率图标签使用收缩的方式获取算法训练需要的概率图标签归一化通过规范化手段把神经网络每层中任意神经元的输入值分布改变成均值为0方差为1的标准正太分布使得最优解的寻优过程明显会变得平缓训练过程更容易收敛通道变换图像的数据格式为[H, W, C]即高度、宽度和通道数而神经网络使用的训练数据的格式为[C, H, W]因此需要对图像数据重新排列例如[224, 224, 3]变为[3, 224, 224] 这里我们主要看下获取阈值图标签以及获取概率图标签的实现, 原理可以参考1.2章节 Train:dataset:name: SimpleDataSetdata_dir: D:\python\datas\cv\icdar2015\text_localization\label_file_list:- D:\python\datas\cv\icdar2015\text_localization\train_icdar2015_label.txtratio_list: [1.0]transforms:- DecodeImage: # load imageimg_mode: BGRchannel_first: False- DetLabelEncode: # Class handling label- IaaAugment:augmenter_args:- { type: Fliplr, args: { p: 0.5 } } # 随机水平翻转- { type: Affine, args: { rotate: [-10, 10] } } # 随机旋转- { type: Resize, args: { size: [0.5, 3] } } # 随机缩放- EastRandomCropData:size: [640, 640] # 随机裁剪max_tries: 50keep_ratio: true- MakeBorderMap: # 阈值图标签的生成 ppocr/data/imaug/make_border_map.pyshrink_ratio: 0.4thresh_min: 0.3thresh_max: 0.7- MakeShrinkMap: # 概率图标签的生成 ppocr/data/imaug/make_shrink_map.pyshrink_ratio: 0.4min_text_size: 8- NormalizeImage: # 通过规范化手段把神经网络每层中任意神经元的输入值分布改变成均值为0方差为1的标准正太分布使得最优解的寻优过程明显会变得平缓训练过程更容易收敛scale: 1./255.mean: [0.485, 0.456, 0.406]std: [0.229, 0.224, 0.225]order: hwc- ToCHWImage: # 图像的数据格式为[H, W, C]即高度、宽度和通道数而神经网络使用的训练数据的格式为[C, H, W]因此需要对图像数据重新排列- KeepKeys:keep_keys: [image, threshold_map, threshold_mask, shrink_map, shrink_mask] # the order of the dataloader listloader:shuffle: Truedrop_last: Falsebatch_size_per_card: 4num_workers: 0use_shared_memory: FalseEval:dataset:name: SimpleDataSetdata_dir: D:\python\datas\cv\icdar2015\text_localization\label_file_list:- D:\python\datas\cv\icdar2015\text_localization\test_icdar2015_label.txttransforms:- DecodeImage: # load imageimg_mode: BGRchannel_first: False- DetLabelEncode: # Class handling label- DetResizeForTest:image_shape: [736, 1280]- NormalizeImage:scale: 1./255.mean: [0.485, 0.456, 0.406]std: [0.229, 0.224, 0.225]order: hwc- ToCHWImage:- KeepKeys:keep_keys: [image, shape, polys, ignore_tags]loader:shuffle: Falsedrop_last: Falsebatch_size_per_card: 1 # must be 1num_workers: 0use_shared_memory: True获取概率图标签 # paddleocr\ppocr\data\imaug\make_shrink_map.py class MakeShrinkMap(object):rMaking binary mask from detection data with ICDAR format.Typically following the process of class MakeICDARData.def __init__(self, min_text_size8, shrink_ratio0.4, **kwargs):self.min_text_size min_text_sizeself.shrink_ratio shrink_ratioif total_epoch in kwargs and epoch in kwargs and kwargs[epoch] ! None:self.shrink_ratio self.shrink_ratio 0.2 * kwargs[epoch] / float(kwargs[total_epoch])def __call__(self, data):image data[image] # (640, 640, 3), 一张图片的shapetext_polys data[polys] # (4, 4, 2), 一张图片有4个文字块每个文字块bbox坐标shape为(4, 2)ignore_tags data[ignore_tags] # [True, False, True, True] True表示此标注无效h, w image.shape[:2]# 1. 校验文本检测标签text_polys, ignore_tags self.validate_polygons(text_polys, ignore_tags, h, w)gt np.zeros((h, w), dtypenp.float32)mask np.ones((h, w), dtypenp.float32)# 2. 根据文本检测框计算文本区域概率图for i in range(len(text_polys)):polygon text_polys[i]height max(polygon[:, 1]) - min(polygon[:, 1])width max(polygon[:, 0]) - min(polygon[:, 0])if ignore_tags[i] or min(height, width) self.min_text_size:# 如果该文本块无效或其尺寸小于最小文本尺寸 (self.min_text_size)则将该区域在mask中设为0并标记为无效cv2.fillPoly(mask, polygon.astype(np.int32)[np.newaxis, :, :], 0)ignore_tags[i] Trueelse:# 对有效的文本块使用shapely库的Polygon创建多边形对象并使用pyclipper进行后续的收缩操作polygon_shape Polygon(polygon)subject [tuple(l) for l in polygon]padding pyclipper.PyclipperOffset()# 将多边形的顶点即subject添加到padding中# JT_ROUND表示在进行偏移时角落会被圆滑处理ET_CLOSEDPOLYGON表示这是一个封闭的多边形padding.AddPath(subject, pyclipper.JT_ROUND, pyclipper.ET_CLOSEDPOLYGON)shrinked []# Increase the shrink ratio every time we get multiple polygon returned back# 生成多个收缩系数这些系数用于缩小多边形的边界如: [0.4, 0.8]possible_ratios np.arange(self.shrink_ratio, 1, self.shrink_ratio)np.append(possible_ratios, 1)# print(possible_ratios)# 对每个收缩系数计算收缩的偏移量然后尝试收缩多边形。若成功收缩得到一个多边形则退出循环for ratio in possible_ratios:# print(fChange shrink ratio to {ratio})# 计算收缩的偏移量distance (polygon_shape.area # 文字块即原始区域的面积* (1 - np.power(ratio, 2)) # ratio为收缩系数/ polygon_shape.length # 文字块即原始区域的周长)# 参数 -distance表示收缩的距离。负值表示将多边形向内收缩# 如果distance合适且收缩成功通常会返回一个或多个新的多边形。shrinked padding.Execute(-distance)# 在某些情况下收缩操作可能会产生多个多边形尤其是在原始多边形形状复杂或者收缩比例不合适的情况下# 因此这里一旦找到一个有效的收缩结果使用break跳出循环避免继续使用其他收缩比例这样可以提高效率并确保结果的简洁性if len(shrinked) 1:breakif shrinked []:# 如果没有成功收缩标记该多边形为无效cv2.fillPoly(mask, polygon.astype(np.int32)[np.newaxis, :, :], 0)ignore_tags[i] Truecontinuefor each_shirnk in shrinked:# 如果成功收缩则更新gt概率图将收缩后的多边形区域设为1表示该区域包含文本shirnk np.array(each_shirnk).reshape(-1, 2)cv2.fillPoly(gt, [shirnk.astype(np.int32)], 1)# 将生成的概率图(gt)和掩码(mask)存入data字典并返回data[shrink_map] gtdata[shrink_mask] maskreturn data......获取阈值图标签 # paddleocr\ppocr\data\imaug\make_border_map.py class MakeBorderMap(object):def __init__(self, shrink_ratio0.4, thresh_min0.3, thresh_max0.7, **kwargs):self.shrink_ratio shrink_ratioself.thresh_min thresh_minself.thresh_max thresh_maxif total_epoch in kwargs and epoch in kwargs and kwargs[epoch] ! None:self.shrink_ratio self.shrink_ratio 0.2 * kwargs[epoch] / float(kwargs[total_epoch])def __call__(self, data):img data[image] # (640, 640, 3), 一张图片的shapetext_polys data[polys] # (4, 4, 2), 一张图片有4个文字块每个文字块bbox坐标shape为(4, 2)ignore_tags data[ignore_tags] # [True, False, True, True] True表示此标注无效# 1. 生成空模版canvas np.zeros(img.shape[:2], dtypenp.float32)mask np.zeros(img.shape[:2], dtypenp.float32)for i in range(len(text_polys)):if ignore_tags[i]:continue# 2. draw_border_map函数根据解码后的box信息计算阈值图标签self.draw_border_map(text_polys[i], canvas, maskmask)# 将canvas归一化到一个指定的阈值范围(thresh_min0.3和thresh_max0.7)canvas canvas * (self.thresh_max - self.thresh_min) self.thresh_mindata[threshold_map] canvasdata[threshold_mask] maskreturn datadef draw_border_map(self, polygon, canvas, mask):polygon: 输入的多边形顶点形状为(n, 2)其中n是顶点数量每个顶点由两个坐标(x, y) 表示。canvas: 用于存储最终生成的阈值图的画布2D数组。mask: 用于存储掩码的画布2D数组。polygon np.array(polygon)assert polygon.ndim 2assert polygon.shape[1] 2polygon_shape Polygon(polygon)if polygon_shape.area 0:return# 计算收缩后的距离 distancedistance (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)# 调用Execute(distance)来收缩多边形返回的结果是收缩后的多边形# 需要注意的是该方法返回一个列表通常只需要第一个元素padded_polygon np.array(padding.Execute(distance)[0])# 在掩码上填充收缩后的多边形区域值设置为 1.0表示该区域有效cv2.fillPoly(mask, [padded_polygon.astype(np.int32)], 1.0)# 计算收缩后多边形的最小和最大x, y坐标从而确定其边界框bounding box的位置和大小xmin padded_polygon[:, 0].min()xmax padded_polygon[:, 0].max()ymin padded_polygon[:, 1].min()ymax padded_polygon[:, 1].max()width xmax - xmin 1height ymax - ymin 1# 将多边形的坐标调整到边界框内以便后续计算距离图polygon[:, 0] polygon[:, 0] - xminpolygon[:, 1] polygon[:, 1] - ymin# 生成网格坐标: 创建一个宽度和高度的网格xs和ys分别表示每个点的x和y坐标xs np.broadcast_to(np.linspace(0, width - 1, numwidth).reshape(1, width), (height, width))ys np.broadcast_to(np.linspace(0, height - 1, numheight).reshape(height, 1), (height, width))distance_map np.zeros((polygon.shape[0], height, width), dtypenp.float32)# 遍历多边形的每条边调用_distance方法计算网格上每个点到边的距离并将其归一化到[0, 1]范围内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(axis0)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)# np.fmax用于逐元素比较两个数组或数值并返回它们的最大值# 使用np.fmax将计算出的距离图与现有的canvas进行比较更新画布的值# 这里的更新是通过将距离值反转1 - distance_map来实现的表示距离较近的区域将具有较高的值接近1canvas[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],)2.2.4 构建数据读取器 采用PaddlePaddle中的Dataset构建数据读取器 # ppocr/data/simple_dataset.py def transform(data, opsNone): transform if ops is None:ops []for op in ops:data op(data)if data is None:return Nonereturn datadef create_operators(op_param_list, global_configNone):create operators based on the configArgs:params(list): a dict list, used to create some operatorsassert isinstance(op_param_list, list), (operator config should be a list)ops []for operator in op_param_list:assert isinstance(operator,dict) and len(operator) 1, yaml format errorop_name list(operator)[0]param {} if operator[op_name] is None else operator[op_name]if global_config is not None:param.update(global_config)op eval(op_name)(**param)ops.append(op)return opsclass SimpleDataSet(Dataset):def __init__(self, mode, label_file, data_dir, seedNone):super(SimpleDataSet, self).__init__()# 标注文件中使用\t作为分隔符区分图片名称与标签self.delimiter \t# 数据集路径self.data_dir data_dir# 随机数种子self.seed seed# 获取所有数据以列表形式返回self.data_lines self.get_image_info_list(label_file)# 新建列表存放数据索引self.data_idx_order_list list(range(len(self.data_lines)))self.mode mode# 如果是训练过程将数据集进行随机打乱if self.mode.lower() train:self.shuffle_data_random()def get_image_info_list(self, label_file):# 获取标签文件中的所有数据with open(label_file, rb) as f:lines f.readlines()return linesdef shuffle_data_random(self):#随机打乱数据random.seed(self.seed)random.shuffle(self.data_lines)returndef __getitem__(self, idx):# 1. 获取索引为idx的数据file_idx self.data_idx_order_list[idx]data_line self.data_lines[file_idx]try:# 2. 获取图片名称以及标签data_line data_line.decode(utf-8)substr data_line.strip(\n).split(self.delimiter)file_name substr[0]label substr[1]# 3. 获取图片路径img_path os.path.join(self.data_dir, file_name)data {img_path: img_path, label: label}if not os.path.exists(img_path):raise Exception({} does not exist!.format(img_path))# 4. 读取图片并进行预处理with open(data[img_path], rb) as f:img f.read()data[image] img# 5. 完成数据增强操作outs transform(data, self.mode.lower())# 6. 如果当前数据读取失败重新随机读取一个新数据except Exception as e:outs Noneif outs is None:return self.__getitem__(np.random.randint(self.__len__()))return outsdef __len__(self):# 返回数据集的大小return len(self.data_idx_order_list)通过build_dataloader加载数据集 # paddleocr/ppocr/data/__init__.py def build_dataloader(config, mode, device, logger, seedNone):config copy.deepcopy(config)support_dict [SimpleDataSet, # 配置文件中为SimpleDataSetLMDBDataSet,PGDataSet,PubTabDataSet,LMDBDataSetSR,LMDBDataSetTableMaster,MultiScaleDataSet,TextDetDataset,TextRecDataset,MSTextRecDataset,PubTabTableRecDataset,KieDataset,LaTeXOCRDataSet,]module_name config[mode][dataset][name]assert module_name in support_dict, Exception(DataSet only support {}.format(support_dict))assert mode in [Train, Eval, Test], Mode should be Train, Eval or Test.# 1、创建datasetdataset eval(module_name)(config, mode, logger, seed)......# 2、创建data_loaderdata_loader DataLoader(datasetdataset,batch_samplerbatch_sampler,placesdevice,num_workersnum_workers,return_listTrue,use_shared_memoryuse_shared_memory,collate_fncollate_fn,)return data_loader2.2.5 损失函数的构建 由于训练阶段获取了3个预测图所以在损失函数中也需要结合这3个预测图与它们对应的真实标签分别构建3部分损失函数。总的损失函数的公式定义如下: L L b α × L s β × L t L L_b \alpha \times L_s \beta \times L_t LLb​α×Ls​β×Lt​ 其中 L L L为总的损失 L s L_s Ls​为概率图损失使用了带 OHEMonline hard example mining 的 Dice 损失 L t L_t Lt​为阈值图损失使用了预测值和标签间的 L 1 L_1 L1​距离 L b L_b Lb​为文本二值图的损失函数。 α \alpha α和 β \beta β为权重系数这里分别将其设为5和10。 三个loss L b L_b Lb​ L s L_s Ls​ L t L_t Lt​分别是Dice Loss、Dice Loss(OHEM)、MaskL1 Loss接下来分别定义这3个部分 Dice Loss是比较预测的文本二值图和标签之间的相似度常用于二值图像分割公式如下 d i c e _ l o s s 1 − 2 × i n t e r s e c t i o n _ a r e a t o t a l _ a r e a dice\_loss 1 - \frac{2 \times intersection\_area}{total\_area} dice_loss1−total_area2×intersection_area​ Dice Loss(OHEM)是采用带OHEM的Dice Loss目的是为了改善正负样本不均衡的问题。OHEM为一种特殊的自动采样方式可以自动的选择难样本进行loss的计算从而提升模型的训练效果。这里将正负样本的采样比率设为1:3。 MaskL1 Loss是计算预测的文本阈值图和标签间的 L 1 L_1 L1​距离。 # paddleocr\ppocr\losses\det_db_loss.py class DBLoss(nn.Layer):Differentiable Binarization (DB) Loss Functionargs:param (dict): the super paramter for DB Lossdef __init__(self,balance_lossTrue,main_loss_typeDiceLoss,alpha5,beta10,ohem_ratio3,eps1e-6,**kwargs,):super(DBLoss, self).__init__()self.alpha alphaself.beta betaself.dice_loss DiceLoss(epseps)self.l1_loss MaskL1Loss(epseps)self.bce_loss BalanceLoss(balance_lossbalance_loss,main_loss_typemain_loss_type,negative_ratioohem_ratio,)def forward(self, predicts, labels):# 1、获取阈值图标签以及概率图标签(也是二值图标签)(label_threshold_map,label_threshold_mask,label_shrink_map,label_shrink_mask,) labels[1:]# 2、获取预测的概率图、阈值图以及近似二值图predict_maps predicts[maps]shrink_maps predict_maps[:, 0, :, :]threshold_maps predict_maps[:, 1, :, :]binary_maps predict_maps[:, 2, :, :]# 概率图计算bce_lossloss_shrink_maps self.bce_loss(shrink_maps, label_shrink_map, label_shrink_mask)# 阈值图计算l1_lossloss_threshold_maps self.l1_loss(threshold_maps, label_threshold_map, label_threshold_mask)# 近似二值图计算dice_loss# dice_loss特别适用于像素级别的二分类或多分类任务loss_binary_maps self.dice_loss(binary_maps, label_shrink_map, label_shrink_mask)loss_shrink_maps self.alpha * loss_shrink_mapsloss_threshold_maps self.beta * loss_threshold_maps# CBN lossif distance_maps in predicts.keys():distance_maps predicts[distance_maps]cbn_maps predicts[cbn_maps]cbn_loss self.bce_loss(cbn_maps[:, 0, :, :], label_shrink_map, label_shrink_mask)else:dis_loss paddle.to_tensor([0.0])cbn_loss paddle.to_tensor([0.0])loss_all loss_shrink_maps loss_threshold_maps loss_binary_mapslosses {loss: loss_all cbn_loss,loss_shrink_maps: loss_shrink_maps,loss_threshold_maps: loss_threshold_maps,loss_binary_maps: loss_binary_maps,loss_cbn: cbn_loss,}return losses2.2.6 预测过程中的后处理程序 原理可以参考1.3章节 PostProcess:name: DBPostProcessthresh: 0.3box_thresh: 0.6max_candidates: 1000unclip_ratio: 1.5class DBPostProcess(object):The post process for Differentiable Binarization (DB).DB后处理有四个参数分别是thresh: DBPostProcess中分割图进行二值化的阈值默认值为0.3box_thresh: DBPostProcess中对输出框进行过滤的阈值低于此阈值的框不会输出unclip_ratio: DBPostProcess中对文本框进行放大的比例max_candidates: DBPostProcess中输出的最大文本框数量默认1000def __init__(self,thresh0.3,box_thresh0.7,max_candidates1000,unclip_ratio2.0,use_dilationFalse,score_modefast,box_typequad,**kwargs,):self.thresh threshself.box_thresh box_threshself.max_candidates max_candidatesself.unclip_ratio unclip_ratioself.min_size 3self.score_mode score_modeself.box_type box_typeassert score_mode in [slow,fast,], Score mode must be in [slow, fast] but got: {}.format(score_mode)self.dilation_kernel None if not use_dilation else np.array([[1, 1], [1, 1]])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}输入: pred_shape (736, 1280), mask_shape (736, 1280), src_w1680, src_h1048结果 boxes_shape (10, 4, 2), scores_len 10bitmap _bitmapheight, width bitmap.shape# cv2.findContours函数可以在二值图像中查找轮廓, 该函数可以检测图像中的白色或黑色区域并返回这些区域的边界outs cv2.findContours((bitmap * 255).astype(np.uint8), cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)if len(outs) 3:img, contours, _ outs[0], outs[1], outs[2]elif len(outs) 2:# contours一个Python列表其中每个元素都是一个轮廓轮廓是一个点集通常是numpy数组的形式contours, _ outs[0], outs[1]# 输出的最大文本框数量默认1000num_contours min(len(contours), self.max_candidates)boxes []scores []for index in range(num_contours):# contour_shape (4, 1, 2)contour contours[index]# 返回轮廓最小外接矩形的四个顶点坐标以及它的最小边长points, sside self.get_mini_boxes(contour)if sside self.min_size: # 最小边长阈值为3continuepoints np.array(points)if self.score_mode fast:# 通过计算边界框内部像素的平均值获取 scorescore self.box_score_fast(pred, points.reshape(-1, 2))else:score self.box_score_slow(pred, contour)if self.box_thresh score: # 对输出框进行过滤的阈值低于此阈值的框不会输出默认为0.6continue# 将收缩文字区域按Vatti clipping算法的偏移系数D进行扩张得到最终文本框box self.unclip(points, self.unclip_ratio)if len(box) 1:continuebox np.array(box).reshape(-1, 1, 2)# 返回扩张后轮廓最小外接矩形的四个顶点坐标以及它的最小边长box, sside self.get_mini_boxes(box)if sside self.min_size 2:continuebox 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(int32))scores.append(score)return np.array(boxes, dtypeint32), scoresdef unclip(self, box, unclip_ratio):poly Polygon(box)# 偏移系数D A * r / L, 其中A为收缩区域的面积L为收缩区域的周长 r默认设置为1.5distance poly.area * unclip_ratio / poly.lengthoffset pyclipper.PyclipperOffset()offset.AddPath(box, pyclipper.JT_ROUND, pyclipper.ET_CLOSEDPOLYGON)expanded offset.Execute(distance)return expandeddef get_mini_boxes(self, contour):从给定的轮廓(contour)中计算出一个最小外接矩形并返回这个矩形的四个顶点坐标以及它的最小边长# 1、计算给定轮廓的最小外接矩形# cv2.minAreaRect函数返回一个Box2D对象包含矩形的中心坐标、宽高宽度和高度可能以任意顺序给出以及旋转角度bounding_box cv2.minAreaRect(contour)# 2、获取矩形的四个顶点# cv2.boxPoints函数根据bounding_box计算出矩形的四个顶点坐标例如# array([[797.00006, 578. ],# [798.00006, 577. ],# [799.00006, 578. ],# [798.00006, 579. ]], dtypefloat32)# 然后将这些顶点坐标转换为一个列表并使用 sorted 函数对这些点进行排序。# 排序的依据是顶点的x坐标即顶点的水平位置这样可以确保顶点按照它们在矩形上的顺序排列# 转换顺序后# [ [797.00006, 578. ],# [798.00006, 577. ],# [798.00006, 579. ],# [799.00006, 578. ]]points sorted(list(cv2.boxPoints(bounding_box)), keylambda x: x[0])# 3、确定矩形的四个顶点顺序# 通过比较顶点的y坐标即顶点的垂直位置来确定矩形的左上角、右上角、右下角和左下角的顶点顺序# 由于最小外接矩形可能是倾斜的简单的上下左右判断不足以确定顶点顺序因此需要根据y坐标的比较来确定index_1, index_2, index_3, index_4 0, 1, 2, 3if points[1][1] points[0][1]:index_1 0index_4 1else:index_1 1index_4 0if points[3][1] points[2][1]:index_2 2index_3 3else:index_2 3index_3 2# ndex_1和index_4对应于矩形的左边界的两个顶点index_2和index_3对应于右边界的两个顶点# 最终的box信息# [[798.00006, 577.],# [799.00006, 578.],# [798.00006, 579.],# [797.00006, 578.]]box [points[index_1], points[index_2], points[index_3], points[index_4]]return box, min(bounding_box[1])......def __call__(self, outs_dict, shape_list):DB head网络的输出形状和原图相同实际上DB head网络输出的三个通道特征分别为文本区域的概率图、阈值图和二值图在训练阶段3个预测图与真实标签共同完成损失函数的计算以及模型训练【在预测阶段只需要使用概率图即可】DB后处理函数根据概率图中文本区域的响应计算出包围文本响应区域的文本框坐标由于网络预测的概率图是经过收缩后的结果所以在后处理步骤中使用相同的偏移值将预测的多边形区域进行扩张即可得到最终的文本框# 1. 从字典中获取网络预测结果pred outs_dict[maps] # shape (1, 1, 736, 1280)if isinstance(pred, paddle.Tensor):pred pred.numpy()# 获取预测的概率图pred pred[:, 0, :, :] # shape (1, 736, 1280)# 2. 大于后处理参数阈值self.thresh的, thresh默认为0.3segmentation pred self.threshboxes_batch []for batch_index in range(pred.shape[0]):# 3. 获取原图的形状和resize比例 (src_h1048, src_w1680, ratio_h0.702, ratio_w0.762)src_h, src_w, ratio_h, ratio_w shape_list[batch_index]if self.dilation_kernel is not None:mask cv2.dilate(np.array(segmentation[batch_index]).astype(np.uint8),self.dilation_kernel,)else:mask segmentation[batch_index]if self.box_type poly:boxes, scores self.polygons_from_bitmap(pred[batch_index], mask, src_w, src_h)elif self.box_type quad:# 4. 使用boxes_from_bitmap函数 完成 从预测的文本概率图中计算得到文本框# pred[batch_index] (736, 1280), mask_shape (736, 1280), src_w1680, src_h1048# boxes_shape (10, 4, 2), scores_len 10boxes, scores self.boxes_from_bitmap(pred[batch_index], mask, src_w, src_h)else:raise ValueError(box_type can only be one of [quad, poly])boxes_batch.append({points: boxes})return boxes_batchDB后处理有四个参数分别是 thresh: DBPostProcess中分割图进行二值化的阈值默认值为0.3box_thresh: DBPostProcess中对输出框进行过滤的阈值低于此阈值的框不会输出unclip_ratio: DBPostProcess中对文本框进行放大的比例max_candidates: DBPostProcess中输出的最大文本框数量默认1000 其他训练细节诸如构建优化器、创建评估函数、加载预训练模型、模型训练等大家可以查看源码不再赘述。
http://www.dnsts.com.cn/news/113567.html

相关文章:

  • 中科网站建设新网站一般建设空间大小
  • 网站建设经理岗位职责国家企业信用公示信息年报官网
  • 奢侈品商城网站建设方案哪些官网用wordpress
  • 赣州网站建设jx25网站友链是什么情况
  • 斗门网站建设竞赛网站开发
  • 网站建设 主机选择郑州做网站哪家好熊掌号
  • 网站建设综合案例如何做背景不动的网站
  • 网站建设杭州搜索引擎排名国内
  • 沙洋建设局网站网络推广的工作内容
  • 网站建设优化是干嘛分销平台软件
  • 网站后端做留言板功能龙岩小程序建设
  • 网站服务器关闭怎么恢复高校网站建设资料库
  • 怎样自己建立一个网站建筑公司招聘岗位
  • 网站维护 年费网站建设公司华网天下官网
  • 网站域名是不是网址上海好的网站设计公司有哪些
  • 网站建设尽量天水网站开发
  • 加强网站信息内容建设的意见动力论坛源码网站后台地址是什么
  • 天津定制开发网站一般网站栏目结构
  • 淮安做网站公司网站策划论文
  • 教做发绳的网站p2p网站建设要多少钱
  • iis怎么加载网站郑州建设网站哪家好
  • 城网站建设广告优化师的职业规划
  • 大理住房和城乡建设局网站城乡建设部网站施工员证书查询
  • 上虞区驿亭镇新农村建设网站建筑工人找活正规平台
  • 外发加工网站源码下载优秀网页设计排版
  • 关于阅读类网站的建设规划书天津优化公司哪家好
  • 国外做游戏评测的视频网站有哪些给wordpress博客加上一个娃娃
  • 太原关键词优化报价外贸网站如何做seo
  • 营销网站建设设计周浦做网站
  • 大学生网站建设开题报告苏州家教网站建设