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={
Sk}k = 1ん, n は頂点の数を示します。
ポリゴン画像で、赤色のアノテーション フレームをある距離だけ拡大すると緑色のポリゴン フレームが得られ、距離を縮小すると青色のポリゴン フレームが得られます。
論文では、ラベル ボックスの縮小と拡大に同じ距離が使用され、計算式は次のとおりです。
D = A ( 1 − r 2 ) LD =\cfrac{A(1 - r^2)}{L}D=Lあ( 1−r2 )、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=21⋅c⋅hS=21⋅ある⋅b⋅αの_⇒h=cある⋅b⋅s 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+b2−c2⇒αの_=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私は⋅ログ_バツ私は+( 1−y私は)∗ログ( 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} を生成します。P∈RH ∗ 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+e− k ( 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 + e− k x1,其中 x = P i , j − T i , j x=P_{i,j}-T_{i,j} バツ=P私、 j−T私、 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 + e− k x1私−=−ログ( 1 _−1 + e− k 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 ) e− k 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 ∈ S私∑y私はログ_バツ私は+( 1−y私は)ログ( 1 _−バツ私は) L s L_s
の場合LsとL 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 ∈ Rd∑∣ y私は∗−バツ私は∗ ∣
ここで、R d R_dRdは拡張多角形G d G_dですGdy ∗ y^*内のピクセル インデックスy∗ は閾値マップのラベルです。