【DBNetモデル】

DBNetモデル

1. 簡単な紹介

  DBNet は、セグメンテーションに基づくテキスト検出アルゴリズムです。このアルゴリズムでは、セグメンテーション モデルに微分可能二値化モジュール (微分可能二値化) が導入されているため、モデルは適応しきい値マップを通じて二値化でき、適応しきい値マップは損失を計算できます。モデルトレーニングのプロセスにおいて、補助効果の最適化の役割を果たします。検証後、このスキームはテキスト検出の効果を向上させるだけでなく、後処理プロセスも簡素化します。他のテキスト検出モデルと比較して、DBNet は効果とパフォーマンスの点で比較的大きな利点があり、現在一般的に使用されているテキスト検出アルゴリズムです。

2. モデル構造

ここに画像の説明を挿入DB テキスト検出モデルは、次の 3 つの部分に分割できます。

  • 画像特徴の抽出を担当するバックボーン ネットワーク
  • FPN ネットワーク、機能ピラミッド、構造拡張機能
  • ヘッドネットワーク、テキストエリア確率マップを計算

1.バックボーンネットワーク

  DB テキスト検出ネットワークのバックボーン部分では画像分類ネットワークが使用されており、この論文では ResNet50 ネットワークと ResNet18 ネットワークが使用されています。ここで、具体的な入力画像サイズについて説明すると、
  入力画像 [1, 3, 640, 640] は、Backbone スケルトン ネットワークに入り、1 回の畳み込み計算後に元のサイズの 1/2 に変更され、その後、 4 回のダウンサンプリング。4 つのスケール特徴マップは次のように出力されます。

ここに画像の説明を挿入

2.FPNネットワーク

  特徴ピラミッド構造 FPN は、画像内の各次元の特徴を効率的に抽出する畳み込みネットワークの一般的な方法です。
  FPN ネットワークの入力はバックボーン部分の出力であり、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 特徴マップの 2 倍アップサンプリング ===> 新しい 1/16 特徴マップ ==> 畳み込み + 4 倍アップサンプリング ===> [1, 64, 160, 160] 1/8
特徴マップ: [1, N, 80, 80] ===> 新しい 1/16 特徴マップの 2 倍のアップサンプリングを追加 ===> 新しい 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 ネットワークは、FPN 特徴に基づいてアップサンプリングを実行し、FPN 特徴を元の 1/4 サイズから元の画像のサイズにマッピングし、最後に生成された 3 つの画像をマージします。出力は [1, 3, 640、640]
ここに画像の説明を挿入

3. ラベルの生成

   DB アルゴリズムがモデル トレーニングを実行する場合、ラベル ボックスに従って 2 つの画像 (確率マップとしきい値マップ) を生成する必要があります。生成プロセスは次の図に示されています。
ここに画像の説明を挿入
  画像内の赤い線はテキストの注釈ボックスであり、テキスト注釈ボックスの点集合は次の形式で表されます。 G = { S k }
           k = 1 n G = \{S_k\}_{k= 1}^nG={ S}k = 1, n は頂点の数を示します。
  ポリゴン画像で、赤色のアノテーション フレームをある距離だけ拡大すると緑色のポリゴン フレームが得られ、距離を縮小すると青色のポリゴン フレームが得られます。
  論文では、ラベル ボックスの縮小と拡大に同じ距離が使用され、計算式は次のとおりです。
               D = A ( 1 − r 2 ) LD =\cfrac{A(1 - r^2)}{L}D=L( 1r2 )、L は周長を表し、A は面積を表し、r はスケーリング比を表します。通常は r=0.4 多角形の輪郭の
  周長 L と面積 A は、Polygon ライブラリによって計算されます。
  計算された距離に従って、ラベル フレームを拡大および縮小し、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) 距離を計算する

   ラベル ボックスが拡張された後、テキスト領域も拡張され、領域内の各点からラベル ボックスまでの距離を計算する必要があります。ラベルボックスを4つの線分とみなし、各位置点からこの4つの線分までの距離を計算し、その最小値を最終的な距離とする。距離は、余弦の法則と面積の公式という 2 つの三角形の公式を利用して計算されます。具体的な計算プロセスは次のとおりです。

ここに画像の説明を挿入
無限数 h が与えられた場合:
{ S = 1 2 ⋅ c ⋅ h S = 1 2 ⋅ a ⋅ b ⋅ sin α ⇒ h = a ⋅ bc ⋅ sin α \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\empty S=21chS=21あるbα_h=cあるbs in α
このうち、a、b、c の値は位置距離、sin α sin\alphaαsはコサインの法則によって計算されます。
cos 2 α = a 2 + b 2 − c 2 2 ab ⇒ sin α = 1 − cos 2 α cos^2\alpha = \cfrac{a^2 + b^2 - c^2}{2ab} ⇒ sin\alpha = \sqrt{\smash[b]{1 - cos^2\alpha}}コス_2a _=2腹筋ある2+b2c2α_=1コス_2a _

(3) 距離の正規化

   各位置点の値を計算した後、正規化が実行されます。エリア内のすべての値は以前に計算された距離で除算され、制限値は [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. 2値化画像ラベル

3. 損失​​計算

1. BCELoss損失関数

   DBNet はバイナリ クロス エントロピー損失関数 (バイナリ クロス エントロピー) を使用し、単一ラベルです。つまり、1 つの入力サンプルが 1 つの分類出力 (1 または 0) に対応します。N サンプルを含むデータ D(x, y) の場合、BCE 損失の計算式は次のとおりです。
loss = 1 N ∑ 1 ≤ i ≤ nli loss =\frac{1}{N} \sum_{\mathclap{1\ le i \le n}} l_iロス_=N11 i n私は
ただし、li = − w ( yi ⋅ log ⁡ xi + ( 1 − yi ) ∗ log ⁡ ( 1 − xi ) ) l_i = -w(y_i\cdot \log x_i + (1-y_i)*\log(1- x_i))私は=w ( y私はログ_バツ私は+( 1y私は)ログ( 1 _バツ私は))は、i 番目のサンプルに対応する損失です。wwwはハイパーパラメータです。単一ラベルのバイナリ分類の場合、ww をwは効果がありません。i 番目のサンプルの損失は次のとおりです。
li = { − log ⁡ xi if y = 1 − log ⁡ ( 1 − xi ) if y = 0 l_i = \begin{cases} -\log x_i &\text{if } & y =1 \\ -\log(1-x_i) &\text{if } & y=0 \end{cases}私は={ ログ_バツ私はログ( 1 _バツ私は)もしも もし y=1y=0

2. 2値化機能

(1) 標準二値化

  セマンティック セグメンテーション ネットワークは、確率マップP ∈ RH ∗ WP\in R^{H*W} を生成します。PRH W、ここでHHH W W W はそれぞれ高さと幅を表します。確率マップは、テキスト領域を表すピクセル値が 1 であるバイナリ マップに変換する必要があります。標準的な 2 値化プロセスは次のとおりです。
B i , j = { 1 if P i , j > = t 0 if それ以外の場合 B_{i,j} = \begin{cases} 1 &\text{if } &P_{i,j } >=t \\ 0 &\text{if } &otherwise \end{cases}B j={ 10もしも もし P j>=tそれ以外賢明_ _
その中で、tttは固定しきい値( i , j ) (i,j)(j )はグラフ上の座標を表します。

(2) 微分可能な二値化

  標準の二値化関数は不連続であり、そのプロセスは微分不可能であり、セマンティック セグメンテーション ネットワークのトレーニング時に最適化することはできません。この問題を解決するために,本論文では,次のような二値化過程を近似するステップ関数,すなわち微分可能な二値化を提案する.
i , j ) \^{B}_{i,j} = \frac{1}{1+e^{-k(P_{i,j}-T_{i,j})}}B j=1+ek ( P j j)1
ここでB ^ \^{B}B^は出力二値化マップ、TTTは、ネットワークによって学習された適応しきい値マップです。kkk は倍率を表し、通常は 50 に設定されます。
下図(a)において、SBは標準二値化処理、DBは微分二値化処理を表し、図(b)、図©はそれぞれl + l_++そしてl − l_-DBNet の微分曲線のブースト効果の理由は

  、逆勾配伝播によって説明できます。BCELoss では、f ( x ) = 1 1 + e − kxf(x)=\frac{1}{1+e^{-kx}} を定義します。f ( x )=1 + ek x1,其中 x = P i , j − T i , j x=P_{i,j}-T_{i,j} バツ=P jT j次に、正のサンプル損失と負のサンプル損失は次のように計算されます。
{ l + = − log ⁡ 1 1 + e − kxl − = − log ⁡ ( 1 − 1 1 + e − kx ) \begin{cases} l_+ = - \log \frac{1}{1+e^{-kx}} \\ l_- = - \log (1- \frac{1}{1+e^{-kx}}) \end{cases}{ +=ログ_1 + ek x1=ログ( 1 _1 + ek x1)
x に関する損失の偏導関数は次のように計算されます。
{ ∂ l + ∂ x = − kf ( x ) e − kx ∂ l − ∂ x = kf ( x ) \begin{cases} \frac{\partial l_ +}{\partial x} = -kf(x)e^{-kx} \\ \frac{\partial l_-}{\partial x} = kf(x) \end{cases}{ ×l+=k f ( x ) ek x×l=k f ( x )
偏導関数によって、次のことがわかります。

(1) 予測ミスの勾配は強調係数kkを通過します。kが強化されると、ネットワークの最適な学習が促進され、予測結果がより明確になります
(2) 図 (b) はl + l_++の微分曲線では、偽陽性が発生した場合 (陽性サンプルが陰性サンプルとして予測される、つまり x<0)、図 (b) の 0 未満の偏微分値が非常に大きくなり、損失も非常に大きいことがわかります。とすると、勾配がより明確になります。 Return
(3) 図 © はl − l_-の微分曲線では、偽陽性が発生した場合 (陰性サンプルが陽性サンプルとして予測される、つまり x>0)、勾配も比較的大きく、損失も大きくなります。

3. 総合損失計算

  モデルのトレーニング中に、確率マップ、閾値マップ、二値化マップの 3 つのマップが出力されます。したがって、損失関数を計算するときは、これら 3 つのグラフとそれに対応する実ラベルを組み合わせて 3 つの部分からなる損失関数を構築することも必要です。全体の損失関数の式は次のように定義されます。
L = L s + ɑ × L b + β × L t L=L_s + ɑ×L_b + β×L_tL=Ls+ɑ×Lb+b×L
このうち、L は全損失、L s L_sLsは確率マップの損失、L b L_bLbはバイナリ イメージの損失、L t L_tLはしきい値マップの損失です。ああαβ bβは重み係数であり、論文ではそれぞれ 1 と 10 に設定されています。
L s = L b = ∑ i ∈ S lyi log ⁡ xi + ( 1 − yi ) log ⁡ ( 1 − xi ) L_s = L_b = \sum_{\mathclap{i \in S_l}} y_i \log x_i + (1 -y_i)\log(1-x_i)Ls=Lb=i Sy私はログ_バツ私は+( 1y私は)ログ( 1 _バツ私は) L s L_s
  の場合LsL b L_bLbすべての損失計算は BCELoss を使用します。不均衡な正と負のサンプルの問題を解決するために、損失計算プロセスで間違った質問セット戦略が使用され、正と負のサンプルの比率が 1:3 に設定されます。
  LtL_tL計算方法は多角形G d G_dを展開します。Gdイントラ予測結果としきい値マップ ラベルの L1 距離の合計。
L t = ∑ i ∈ R d ∣ yi ∗ − xi ∗ ∣ L_t = \sum_{\mathclap{i \in R_d}} |{y_i}^* - {x_i}^*|L=i Rdy私はバツ私は
ここで、R d R_dRdは拡張多角形G d G_dですGdy ∗ y^*内のピクセル インデックスy∗ は閾値マップのラベルです。

おすすめ

転載: blog.csdn.net/yewumeng123/article/details/127503815