Распознавание текста PaddleOCR на практике

искусственный интеллект

Распознавание текста OCR на практике

В этом разделе будет рассказано, как использовать PaddleOCR для завершения обучения и работы алгоритма БД обнаружения текста, в том числе:

  1. Быстро вызовите пакет paddleocr, чтобы испытать обнаружение текста
  2. Понять принцип алгоритма обнаружения текста БД
  3. Освойте процесс построения модели обнаружения текста
  4. Освойте процесс обучения модели обнаружения текста

1. Быстрый старт

Этот раздел начинается сpaddleocrВ качестве примера мы представим, как быстро реализовать обнаружение текста в три шага.

  1. Установитьpaddleocr
  2. Однострочная команда для запуска алгоритма БД для получения результата обнаружения
  3. Визуализируйте результаты обнаружения текста

Установить пакет paddleocr whl

!pip install --upgrade pip
!pip install paddleocr

Однострочная команда для обнаружения текста

При первом запуске paddleocr автоматически загружает и использует PaddleOCR.Облегченная модель PP-OCRv2.

Используя установленный paddleocr с ./doc/imgs/12.jpg в качестве входного изображения, вы получите следующие прогнозы:


Рисунок 12.jpg

[[79.0, 555.0], [398.0, 542.0], [399.0, 571.0], [80.0, 584.0]]
[[21.0, 507.0], [512.0, 491.0], [513.0, 532.0], [22.0, 548.0]]
[[174.0, 458.0], [397.0, 449.0], [398.0, 480.0], [175.0, 489.0]]
[[42.0, 414.0], [482.0, 392.0], [484.0, 428.0], [44.0, 450.0]]

Результат предсказания содержит всего четыре текстовых поля, каждая строка содержит четыре координатные точки, представляющие собой набор координат текстового поля, расположенных по часовой стрелке из левого верхнего угла.

Командная строка paddleocr вызывает модель обнаружения текста для прогнозирования изображения ./doc/imgs/12.jpg следующим образом:

# --image_dir 指向要预测的图像路径  --rec false表示不使用识别识别,只执行文本检测
! paddleocr --image_dir ./PaddleOCR/doc/imgs/12.jpg --rec false
另外,除了命令行使用方式,paddleocr也提供了代码调用方式,如下:
# 首次运行需要打开下一行的注释,下载PaddleOCR代码
!git clone https://gitee.com/paddlepaddle/PaddleOCR
import os
# 修改代码运行的默认目录为 /home/aistudio/PaddleOCR
os.chdir("/home/aistudio/PaddleOCR")
# 安装PaddleOCR第三方依赖
!pip install --upgrade pip
!pip install -r requirements.txt
# 1. 从paddleocr中import PaddleOCR类
from paddleocr import PaddleOCR
import numpy as np
import cv2
import matplotlib.pyplot as plt
# 在notebook中使用matplotlib.pyplot绘图时,需要添加该命令进行显示
%matplotlib inline

# 2. 声明PaddleOCR类
ocr = PaddleOCR()  
img_path = './PaddleOCR/doc/imgs/12.jpg'
# 3. 执行预测
result = ocr.ocr(img_path, rec=False)
print(f"The predicted text box of {img_path} are follows.")
print(result)

Визуализируйте результаты предсказания обнаружения текста

# 4. 可视化检测结果
image = cv2.imread(img_path)
boxes = [line[0] for line in result]
for box in result:
    box = np.reshape(np.array(box), [-1, 1, 2]).astype(np.int64)
    image = cv2.polylines(np.array(image), [box], True, (255, 0, 0), 2)

# 画出读取的图片
plt.figure(figsize=(10, 10))
plt.imshow(image)
<matplotlib.image.AxesImage at 0x7f512eb60250>

2. Подробная реализация алгоритма обнаружения текста БД

2.1 Принцип алгоритма обнаружения текста БД

DBЭто алгоритм обнаружения текста на основе сегментации, который предлагает дифференцируемый порог. Модуль дифференцируемой бинаризации (модуль DB) использует динамический порог для различения текстовых областей и фона.


Рис. 1 Отличие модели БД от других методов


Процесс алгоритма обнаружения обычного текста на основе сегментации показан синей стрелкой на рисунке выше.После получения результата сегментации используется фиксированный порог для получения бинаризованного изображения сегментации, а затем применяется эвристический алгоритм, такой как кластеризация пикселей. используется для получения изображения сегментации текстовой области.

Поток алгоритма БД показан красной стрелкой на рисунке.Самое большое отличие состоит в том, что БД имеет карту пороговых значений, которая использует сеть для прогнозирования порогового значения в каждой позиции изображения вместо использования фиксированного значения. , чтобы лучше разделить текстовый фон и передний план.

Алгоритм БД имеет следующие преимущества:

  1. Структура алгоритма проста и не требует утомительной постобработки.
  2. Хорошая точность и производительность на данных из открытых источников

В традиционном алгоритме сегментации изображения после получения карты вероятностей для обработки используется стандартный метод бинаризации, а пикселям ниже порога присваивается значение 0, а пикселям выше порога присваивается значение 1. Формула выглядит следующим образом :

\begin{aligned} 1 , if P_{i,j} >= t ,\\ 0 , otherwise. \end{aligned} \right.

Но стандартные методы бинаризации не дифференцируемы, что делает сеть неспособной к сквозному обучению. Для решения этой проблемы алгоритм DB предлагает дифференцируемую бинаризацию (DB). Дифференцируемая бинаризация аппроксимирует ступенчатую функцию стандартной бинаризации и заменяет ее следующей формулой:

B^=11+ek(Pi,jTi,j)\hat{B} = \frac{1}{1 + e^{-k(P_{i,j}-T_{i,j})}}

Среди них P — карта вероятностей, полученная выше, T — карта порогов, полученная выше, и k — коэффициент усиления, выбранный равным 50 в соответствии с опытом эксперимента. Сравнительная таблица стандартной бинаризации и дифференцируемой бинаризации выглядит следующим образом.Рисунок 3(а) нижепоказано.

При использовании кросс-энтропийных потерь потери положительных и отрицательных образцов равныl+l_+иll_-:

l+=log(11+ek(Pi,jTi,j))l_+ = -log(\frac{1}{1 + e^{-k(P_{i,j}-T_{i,j})}})
l=log(111+ek(Pi,jTi,j))l_- = -log(1-\frac{1}{1 + e^{-k(P_{i,j}-T_{i,j})}})

для вводаxxВзятие частной производной дает:

δl+δx=kf(x)ekx\frac{\delta{l_+}}{\delta{x}} = -kf(x)e^{-kx}
δlδx=kf(x)\frac{\delta{l_-}}{\delta{x}} = -kf(x)

Можно обнаружить, что коэффициент улучшения будет усиливать градиент неправильного предсказания, тем самым оптимизируя модель для получения лучших результатов.Рисунок 3(б)середина,x<0x<0В части случая, когда положительный образец прогнозируется как отрицательный, можно видеть, что коэффициент усиления k усиливает градиент; иРисунок 3(с)серединаx>0x>0Градиент также усиливается, когда прогнозируется, что часть отрицательного образца будет положительным образцом.


Рисунок 3: Схематическая диаграмма алгоритма БД


Общая структура алгоритма БД показана на следующем рисунке:


Рисунок 2 Принципиальная схема сетевой структуры модели БД


Входное изображение извлекается через сеть Backbone и FPN, а извлеченные функции каскадируются вместе для получения функции размером в четверть исходного изображения, а затем слой свертки используется для получения карты вероятности прогнозирования текстовой области и карты порогов соответственно. , а затем через постобработку БД, чтобы получить кривую, охватывающую текст.

2.2 Построение модели обнаружения текста БД

Модель обнаружения текста БД можно разделить на три части:

  • Магистральная сеть, отвечающая за извлечение характеристик изображений.
  • Сеть FPN, пирамидальная структура функций для расширения возможностей
  • Головная сеть, рассчитайте карту вероятностей текстовой области

В этом разделе используется PaddlePaddle для реализации трех вышеуказанных сетевых модулей и завершения построения сети.

магистральная сеть

Магистральная часть сети обнаружения текста БД использует сеть классификации изображений.В документе используется ResNet50.В этом эксперименте, чтобы ускорить скорость обучения, в качестве основы используется большая структура MobileNetV3.

# 首次运行需要打开下一行的注释,下载PaddleOCR代码
#!git clone https://gitee.com/paddlepaddle/PaddleOCR
import os
# 修改代码运行的默认目录为 /home/aistudio/PaddleOCR
os.chdir("/home/aistudio/PaddleOCR")
# 安装PaddleOCR第三方依赖
!pip install --upgrade pip
!pip install -r requirements.txt
Looking in indexes: https://pypi.tuna.tsinghua.edu.cn/simple
Requirement already satisfied: pip in /opt/conda/envs/python35-paddle120-env/lib/python3.7/site-packages (21.3.1)
#  https://github.com/PaddlePaddle/PaddleOCR/blob/release%2F2.4/ppocr/modeling/backbones/det_mobilenet_v3.py
from ppocr.modeling.backbones.det_mobilenet_v3 import MobileNetV3

Если вы хотите использовать ResNet в качестве магистрали для обучения, вы можете выбрать код PaddleOCR.ResNet, или изPaddleClasВыберите магистральную модель в .

Магистральная сеть БД используется для извлечения многомасштабных признаков изображения, как показано в следующем коде, предполагая форму ввода [640, 640], выход магистральной сети имеет четыре признака, формы которых [1 , 16, 160, 160], [1, 24, 80, 80], [1, 56, 40, 40], [1, 480, 20, 20]. Эти функции будут добавлены в сеть Feature Pyramid FPN для дальнейшего расширения функций.

import paddle 

fake_inputs = paddle.randn([1, 3, 640, 640], dtype="float32")

# 1. 声明Backbone
model_backbone = MobileNetV3()
model_backbone.eval()

# 2. 执行预测
outs = model_backbone(fake_inputs)

# 3. 打印网络结构
print(model_backbone)

# 4. 打印输出特征形状
for idx, out in enumerate(outs):
    print("The index is ", idx, "and the shape of output is ", out.shape)
W1222 14:40:35.323043   565 device_context.cc:447] Please NOTE: device: 0, GPU Compute Capability: 7.0, Driver API Version: 11.0, Runtime API Version: 10.1
W1222 14:40:35.328037   565 device_context.cc:465] device: 0, cuDNN Version: 7.6.


MobileNetV3(
  (conv): ConvBNLayer(
    (conv): Conv2D(3, 8, kernel_size=[3, 3], stride=[2, 2], padding=1, data_format=NCHW)
    (bn): BatchNorm()
  )
  (stage0): Sequential(
    (0): ResidualUnit(
      (expand_conv): ConvBNLayer(
        (conv): Conv2D(8, 8, kernel_size=[1, 1], data_format=NCHW)
        (bn): BatchNorm()
      )
      (bottleneck_conv): ConvBNLayer(
        (conv): Conv2D(8, 8, kernel_size=[3, 3], padding=1, groups=8, data_format=NCHW)
        (bn): BatchNorm()
      )
      (linear_conv): ConvBNLayer(
        (conv): Conv2D(8, 8, kernel_size=[1, 1], data_format=NCHW)
        (bn): BatchNorm()
      )
    )
    (1): ResidualUnit(
      (expand_conv): ConvBNLayer(
        (conv): Conv2D(8, 32, kernel_size=[1, 1], data_format=NCHW)
        (bn): BatchNorm()
      )
      (bottleneck_conv): ConvBNLayer(
        (conv): Conv2D(32, 32, kernel_size=[3, 3], stride=[2, 2], padding=1, groups=32, data_format=NCHW)
        (bn): BatchNorm()
      )
      (linear_conv): ConvBNLayer(
        (conv): Conv2D(32, 16, kernel_size=[1, 1], data_format=NCHW)
        (bn): BatchNorm()
      )
    )
    (2): ResidualUnit(
      (expand_conv): ConvBNLayer(
        (conv): Conv2D(16, 40, kernel_size=[1, 1], data_format=NCHW)
        (bn): BatchNorm()
      )
      (bottleneck_conv): ConvBNLayer(
        (conv): Conv2D(40, 40, kernel_size=[3, 3], padding=1, groups=40, data_format=NCHW)
        (bn): BatchNorm()
      )
      (linear_conv): ConvBNLayer(
        (conv): Conv2D(40, 16, kernel_size=[1, 1], data_format=NCHW)
        (bn): BatchNorm()
      )
    )
  )
  (stage1): Sequential(
    (0): ResidualUnit(
      (expand_conv): ConvBNLayer(
        (conv): Conv2D(16, 40, kernel_size=[1, 1], data_format=NCHW)
        (bn): BatchNorm()
      )
      (bottleneck_conv): ConvBNLayer(
        (conv): Conv2D(40, 40, kernel_size=[5, 5], stride=[2, 2], padding=2, groups=40, data_format=NCHW)
        (bn): BatchNorm()
      )
      (mid_se): SEModule(
        (avg_pool): AdaptiveAvgPool2D(output_size=1)
        (conv1): Conv2D(40, 10, kernel_size=[1, 1], data_format=NCHW)
        (conv2): Conv2D(10, 40, kernel_size=[1, 1], data_format=NCHW)
      )
      (linear_conv): ConvBNLayer(
        (conv): Conv2D(40, 24, kernel_size=[1, 1], data_format=NCHW)
        (bn): BatchNorm()
      )
    )
    (1): ResidualUnit(
      (expand_conv): ConvBNLayer(
        (conv): Conv2D(24, 64, kernel_size=[1, 1], data_format=NCHW)
        (bn): BatchNorm()
      )
      (bottleneck_conv): ConvBNLayer(
        (conv): Conv2D(64, 64, kernel_size=[5, 5], padding=2, groups=64, data_format=NCHW)
        (bn): BatchNorm()
      )
      (mid_se): SEModule(
        (avg_pool): AdaptiveAvgPool2D(output_size=1)
        (conv1): Conv2D(64, 16, kernel_size=[1, 1], data_format=NCHW)
        (conv2): Conv2D(16, 64, kernel_size=[1, 1], data_format=NCHW)
      )
      (linear_conv): ConvBNLayer(
        (conv): Conv2D(64, 24, kernel_size=[1, 1], data_format=NCHW)
        (bn): BatchNorm()
      )
    )
    (2): ResidualUnit(
      (expand_conv): ConvBNLayer(
        (conv): Conv2D(24, 64, kernel_size=[1, 1], data_format=NCHW)
        (bn): BatchNorm()
      )
      (bottleneck_conv): ConvBNLayer(
        (conv): Conv2D(64, 64, kernel_size=[5, 5], padding=2, groups=64, data_format=NCHW)
        (bn): BatchNorm()
      )
      (mid_se): SEModule(
        (avg_pool): AdaptiveAvgPool2D(output_size=1)
        (conv1): Conv2D(64, 16, kernel_size=[1, 1], data_format=NCHW)
        (conv2): Conv2D(16, 64, kernel_size=[1, 1], data_format=NCHW)
      )
      (linear_conv): ConvBNLayer(
        (conv): Conv2D(64, 24, kernel_size=[1, 1], data_format=NCHW)
        (bn): BatchNorm()
      )
    )
  )
  (stage2): Sequential(
    (0): ResidualUnit(
      (expand_conv): ConvBNLayer(
        (conv): Conv2D(24, 120, kernel_size=[1, 1], data_format=NCHW)
        (bn): BatchNorm()
      )
      (bottleneck_conv): ConvBNLayer(
        (conv): Conv2D(120, 120, kernel_size=[3, 3], stride=[2, 2], padding=1, groups=120, data_format=NCHW)
        (bn): BatchNorm()
      )
      (linear_conv): ConvBNLayer(
        (conv): Conv2D(120, 40, kernel_size=[1, 1], data_format=NCHW)
        (bn): BatchNorm()
      )
    )
    (1): ResidualUnit(
      (expand_conv): ConvBNLayer(
        (conv): Conv2D(40, 104, kernel_size=[1, 1], data_format=NCHW)
        (bn): BatchNorm()
      )
      (bottleneck_conv): ConvBNLayer(
        (conv): Conv2D(104, 104, kernel_size=[3, 3], padding=1, groups=104, data_format=NCHW)
        (bn): BatchNorm()
      )
      (linear_conv): ConvBNLayer(
        (conv): Conv2D(104, 40, kernel_size=[1, 1], data_format=NCHW)
        (bn): BatchNorm()
      )
    )
    (2): ResidualUnit(
      (expand_conv): ConvBNLayer(
        (conv): Conv2D(40, 96, kernel_size=[1, 1], data_format=NCHW)
        (bn): BatchNorm()
      )
      (bottleneck_conv): ConvBNLayer(
        (conv): Conv2D(96, 96, kernel_size=[3, 3], padding=1, groups=96, data_format=NCHW)
        (bn): BatchNorm()
      )
      (linear_conv): ConvBNLayer(
        (conv): Conv2D(96, 40, kernel_size=[1, 1], data_format=NCHW)
        (bn): BatchNorm()
      )
    )
    (3): ResidualUnit(
      (expand_conv): ConvBNLayer(
        (conv): Conv2D(40, 96, kernel_size=[1, 1], data_format=NCHW)
        (bn): BatchNorm()
      )
      (bottleneck_conv): ConvBNLayer(
        (conv): Conv2D(96, 96, kernel_size=[3, 3], padding=1, groups=96, data_format=NCHW)
        (bn): BatchNorm()
      )
      (linear_conv): ConvBNLayer(
        (conv): Conv2D(96, 40, kernel_size=[1, 1], data_format=NCHW)
        (bn): BatchNorm()
      )
    )
    (4): ResidualUnit(
      (expand_conv): ConvBNLayer(
        (conv): Conv2D(40, 240, kernel_size=[1, 1], data_format=NCHW)
        (bn): BatchNorm()
      )
      (bottleneck_conv): ConvBNLayer(
        (conv): Conv2D(240, 240, kernel_size=[3, 3], padding=1, groups=240, data_format=NCHW)
        (bn): BatchNorm()
      )
      (mid_se): SEModule(
        (avg_pool): AdaptiveAvgPool2D(output_size=1)
        (conv1): Conv2D(240, 60, kernel_size=[1, 1], data_format=NCHW)
        (conv2): Conv2D(60, 240, kernel_size=[1, 1], data_format=NCHW)
      )
      (linear_conv): ConvBNLayer(
        (conv): Conv2D(240, 56, kernel_size=[1, 1], data_format=NCHW)
        (bn): BatchNorm()
      )
    )
    (5): ResidualUnit(
      (expand_conv): ConvBNLayer(
        (conv): Conv2D(56, 336, kernel_size=[1, 1], data_format=NCHW)
        (bn): BatchNorm()
      )
      (bottleneck_conv): ConvBNLayer(
        (conv): Conv2D(336, 336, kernel_size=[3, 3], padding=1, groups=336, data_format=NCHW)
        (bn): BatchNorm()
      )
      (mid_se): SEModule(
        (avg_pool): AdaptiveAvgPool2D(output_size=1)
        (conv1): Conv2D(336, 84, kernel_size=[1, 1], data_format=NCHW)
        (conv2): Conv2D(84, 336, kernel_size=[1, 1], data_format=NCHW)
      )
      (linear_conv): ConvBNLayer(
        (conv): Conv2D(336, 56, kernel_size=[1, 1], data_format=NCHW)
        (bn): BatchNorm()
      )
    )
  )
  (stage3): Sequential(
    (0): ResidualUnit(
      (expand_conv): ConvBNLayer(
        (conv): Conv2D(56, 336, kernel_size=[1, 1], data_format=NCHW)
        (bn): BatchNorm()
      )
      (bottleneck_conv): ConvBNLayer(
        (conv): Conv2D(336, 336, kernel_size=[5, 5], stride=[2, 2], padding=2, groups=336, data_format=NCHW)
        (bn): BatchNorm()
      )
      (mid_se): SEModule(
        (avg_pool): AdaptiveAvgPool2D(output_size=1)
        (conv1): Conv2D(336, 84, kernel_size=[1, 1], data_format=NCHW)
        (conv2): Conv2D(84, 336, kernel_size=[1, 1], data_format=NCHW)
      )
      (linear_conv): ConvBNLayer(
        (conv): Conv2D(336, 80, kernel_size=[1, 1], data_format=NCHW)
        (bn): BatchNorm()
      )
    )
    (1): ResidualUnit(
      (expand_conv): ConvBNLayer(
        (conv): Conv2D(80, 480, kernel_size=[1, 1], data_format=NCHW)
        (bn): BatchNorm()
      )
      (bottleneck_conv): ConvBNLayer(
        (conv): Conv2D(480, 480, kernel_size=[5, 5], padding=2, groups=480, data_format=NCHW)
        (bn): BatchNorm()
      )
      (mid_se): SEModule(
        (avg_pool): AdaptiveAvgPool2D(output_size=1)
        (conv1): Conv2D(480, 120, kernel_size=[1, 1], data_format=NCHW)
        (conv2): Conv2D(120, 480, kernel_size=[1, 1], data_format=NCHW)
      )
      (linear_conv): ConvBNLayer(
        (conv): Conv2D(480, 80, kernel_size=[1, 1], data_format=NCHW)
        (bn): BatchNorm()
      )
    )
    (2): ResidualUnit(
      (expand_conv): ConvBNLayer(
        (conv): Conv2D(80, 480, kernel_size=[1, 1], data_format=NCHW)
        (bn): BatchNorm()
      )
      (bottleneck_conv): ConvBNLayer(
        (conv): Conv2D(480, 480, kernel_size=[5, 5], padding=2, groups=480, data_format=NCHW)
        (bn): BatchNorm()
      )
      (mid_se): SEModule(
        (avg_pool): AdaptiveAvgPool2D(output_size=1)
        (conv1): Conv2D(480, 120, kernel_size=[1, 1], data_format=NCHW)
        (conv2): Conv2D(120, 480, kernel_size=[1, 1], data_format=NCHW)
      )
      (linear_conv): ConvBNLayer(
        (conv): Conv2D(480, 80, kernel_size=[1, 1], data_format=NCHW)
        (bn): BatchNorm()
      )
    )
    (3): ConvBNLayer(
      (conv): Conv2D(80, 480, kernel_size=[1, 1], data_format=NCHW)
      (bn): BatchNorm()
    )
  )
)
The index is  0 and the shape of output is  [1, 16, 160, 160]
The index is  1 and the shape of output is  [1, 24, 80, 80]
The index is  2 and the shape of output is  [1, 56, 40, 40]
The index is  3 and the shape of output is  [1, 480, 20, 20]

FPN-сеть

Структура пирамиды признаков FPN — это распространенный метод сверточной сети для эффективного извлечения признаков различных размеров в изображениях.

# https://github.com/PaddlePaddle/PaddleOCR/blob/release%2F2.4/ppocr/modeling/necks/db_fpn.py

import paddle
from paddle import nn
import paddle.nn.functional as F
from paddle import ParamAttr

class DBFPN(nn.Layer):
    def __init__(self, in_channels, out_channels, **kwargs):
        super(DBFPN, self).__init__()
        self.out_channels = out_channels

        # DBFPN详细实现参考: https://github.com/PaddlePaddle/PaddleOCRblob/release%2F2.4/ppocr/modeling/necks/db_fpn.py

    def forward(self, x):
        c2, c3, c4, c5 = x

        in5 = self.in5_conv(c5)
        in4 = self.in4_conv(c4)
        in3 = self.in3_conv(c3)
        in2 = self.in2_conv(c2)

        # 特征上采样
        out4 = in4 + F.upsample(
            in5, scale_factor=2, mode="nearest", align_mode=1)  # 1/16
        out3 = in3 + F.upsample(
            out4, scale_factor=2, mode="nearest", align_mode=1)  # 1/8
        out2 = in2 + F.upsample(
            out3, scale_factor=2, mode="nearest", align_mode=1)  # 1/4

        p5 = self.p5_conv(in5)
        p4 = self.p4_conv(out4)
        p3 = self.p3_conv(out3)
        p2 = self.p2_conv(out2)

        # 特征上采样
        p5 = F.upsample(p5, scale_factor=8, mode="nearest", align_mode=1)
        p4 = F.upsample(p4, scale_factor=4, mode="nearest", align_mode=1)
        p3 = F.upsample(p3, scale_factor=2, mode="nearest", align_mode=1)

        fuse = paddle.concat([p5, p4, p3, p2], axis=1)
        return fuse

Входные данные сети FPN являются выходными данными магистральной части, а высота и ширина выходной карты объектов составляют одну четвертую от исходного изображения. Предполагая, что форма входного изображения [1, 3, 640, 640], высота и ширина выходных функций FPN равны [160, 160]

import paddle 

# 1. 从PaddleOCR中import DBFPN
from ppocr.modeling.necks.db_fpn import DBFPN

# 2. 获得Backbone网络输出结果
fake_inputs = paddle.randn([1, 3, 640, 640], dtype="float32")
model_backbone = MobileNetV3()
in_channles = model_backbone.out_channels

# 3. 声明FPN网络
model_fpn = DBFPN(in_channels=in_channles, out_channels=256)

# 4. 打印FPN网络
print(model_fpn)

# 5. 计算得到FPN结果输出
outs = model_backbone(fake_inputs)
fpn_outs = model_fpn(outs)

# 6. 打印FPN输出特征形状
print(f"The shape of fpn outs {fpn_outs.shape}")
DBFPN(
  (in2_conv): Conv2D(16, 256, kernel_size=[1, 1], data_format=NCHW)
  (in3_conv): Conv2D(24, 256, kernel_size=[1, 1], data_format=NCHW)
  (in4_conv): Conv2D(56, 256, kernel_size=[1, 1], data_format=NCHW)
  (in5_conv): Conv2D(480, 256, kernel_size=[1, 1], data_format=NCHW)
  (p5_conv): Conv2D(256, 64, kernel_size=[3, 3], padding=1, data_format=NCHW)
  (p4_conv): Conv2D(256, 64, kernel_size=[3, 3], padding=1, data_format=NCHW)
  (p3_conv): Conv2D(256, 64, kernel_size=[3, 3], padding=1, data_format=NCHW)
  (p2_conv): Conv2D(256, 64, kernel_size=[3, 3], padding=1, data_format=NCHW)
)
The shape of fpn outs [1, 256, 160, 160]

Головная сеть

Вычислите карты вероятностей текстовой области, карты порогов текстовой области и двоичные карты текстовой области.

import math
import paddle
from paddle import nn
import paddle.nn.functional as F
from paddle import ParamAttr

class DBHead(nn.Layer):
    """
    Differentiable Binarization (DB) for text detection:
        see https://arxiv.org/abs/1911.08947
    args:
        params(dict): super parameters for build DB network
    """

    def __init__(self, in_channels, k=50, **kwargs):
        super(DBHead, self).__init__()
        self.k = k

        # DBHead详细实现参考 https://github.com/PaddlePaddle/PaddleOCR/blob/release%2F2.4/ppocr/modeling/heads/det_db_head.py

    def step_function(self, x, y):
        # 可微二值化实现,通过概率图和阈值图计算文本分割二值图
        return paddle.reciprocal(1 + paddle.exp(-self.k * (x - y)))

    def forward(self, x, targets=None):
        shrink_maps = self.binarize(x)
        if not self.training:
            return {'maps': shrink_maps}

        threshold_maps = self.thresh(x)
        binary_maps = self.step_function(shrink_maps, threshold_maps)
        y = paddle.concat([shrink_maps, threshold_maps, binary_maps], axis=1)
        return {'maps': y}

Сеть DB Head будет повышать дискретизацию на основе функций FPN и сопоставлять функции FPN с четверти исходного изображения до исходного размера изображения.

# 1. 从PaddleOCR中imort DBHead
from ppocr.modeling.heads.det_db_head import DBHead
import paddle 

# 2. 计算DBFPN网络输出结果
fake_inputs = paddle.randn([1, 3, 640, 640], dtype="float32")
model_backbone = MobileNetV3()
in_channles = model_backbone.out_channels
model_fpn = DBFPN(in_channels=in_channles, out_channels=256)
outs = model_backbone(fake_inputs)
fpn_outs = model_fpn(outs)

# 3. 声明Head网络
model_db_head = DBHead(in_channels=256)

# 4. 打印DBhead网络
print(model_db_head)

# 5. 计算Head网络的输出
db_head_outs = model_db_head(fpn_outs)
print(f"The shape of fpn outs {fpn_outs.shape}")
print(f"The shape of DB head outs {db_head_outs['maps'].shape}")
DBHead(
  (binarize): Head(
    (conv1): Conv2D(256, 64, kernel_size=[3, 3], padding=1, data_format=NCHW)
    (conv_bn1): BatchNorm()
    (conv2): Conv2DTranspose(64, 64, kernel_size=[2, 2], stride=[2, 2], data_format=NCHW)
    (conv_bn2): BatchNorm()
    (conv3): Conv2DTranspose(64, 1, kernel_size=[2, 2], stride=[2, 2], data_format=NCHW)
  )
  (thresh): Head(
    (conv1): Conv2D(256, 64, kernel_size=[3, 3], padding=1, data_format=NCHW)
    (conv_bn1): BatchNorm()
    (conv2): Conv2DTranspose(64, 64, kernel_size=[2, 2], stride=[2, 2], data_format=NCHW)
    (conv_bn2): BatchNorm()
    (conv3): Conv2DTranspose(64, 1, kernel_size=[2, 2], stride=[2, 2], data_format=NCHW)
  )
)
The shape of fpn outs [1, 256, 160, 160]
The shape of DB head outs [1, 3, 640, 640]

3 Обучающая модель обнаружения текста в БД

PaddleOCR предоставляет алгоритм обнаружения текста БД, поддерживает две магистральные сети MobileNetV3, ResNet50_vd, при необходимости можно выбрать соответствующий файл конфигурации для начала обучения.

В этом разделе используется набор данных icdar15 и модель обнаружения БД MobileNetV3 в качестве базовой сети (то есть конфигурация, используемая сверхлегкой моделью) в качестве примеров, чтобы представить, как завершить обучение, оценку и тестирование обнаружения китайского текста PaddleOCR. модель.

3.1 Подготовка данных

В этом эксперименте была выбрана задача обнаружения и распознавания текста сцены, самый известный и распространенный набор данных ICDAR2015. Схема набора данных ICDAR2015 показана ниже:


Рисунок Схематическая диаграмма набора данных icdar2015


Набор данных icdar2015 был загружен в этом проекте и сохранен в /home/aistudio/data/data96799.Вы можете запустить следующую команду, чтобы распаковать набор данных, или загрузить его самостоятельно по ссылке.

!cd ~/data/data96799/ && tar xf icdar2015.tar 

После выполнения вышеуказанной команды ~/train_data/icdar2015/text_localization имеет две папки и два файла, а именно:

~/train_data/icdar2015/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 представляет собой список, содержащий несколько словарей.Точки в словаре представляют собой координаты (x, y) четырех точек текстового поля, которые расположены по часовой стрелке от точки в верхнем левом углу. . Поля в транскрипции представляют собой текст текущего текстового поля, и эта информация не требуется в задаче обнаружения текста. Если вы хотите обучить PaddleOCR на других наборах данных, вы можете создать файл аннотации в форме выше.

Если текст поля «транскрипция» имеет вид «*» или «###», это означает, что соответствующую аннотацию можно игнорировать, поэтому при отсутствии текстовой метки в поле транскрипции можно указать пустую строку.

3.2 Предварительная обработка данных

При обучении предъявляются определенные требования к формату и размеру входного изображения, при этом также необходимо получить реальные метки карты порогов и карты вероятностей по информации аннотации. Поэтому перед вводом данных в модель данные необходимо предварительно обработать, чтобы изображения и метки соответствовали потребностям обучения и прогнозирования сети. Кроме того, чтобы расширить набор обучающих данных, подавить переоснащение и улучшить способность модели к обобщению, необходимо использовать несколько основных методов увеличения данных.

Предварительная обработка данных в этом эксперименте включает следующие методы:

  • Декодирование изображения: конвертировать изображение в формат Numpy;
  • Расшифровка этикетки: анализировать информацию этикетки в текстовом файле и сохранять ее в унифицированном формате;
  • Расширение базовых данных: в том числе: случайное горизонтальное переворачивание, случайное вращение, случайное масштабирование, случайное кадрирование и т. д.;
  • Получить метку карты порогов: используйте метод расширения, чтобы получить метку карты порогов, необходимую для обучения алгоритма;
  • Получить метку карты вероятностей: используйте метод сжатия, чтобы получить метку карты вероятностей, необходимую для обучения алгоритма;
  • Нормализация: посредством нормализации распределение входных значений любого нейрона в каждом слое нейронной сети изменяется на стандартное нормальное распределение со средним значением 0 и дисперсией 1, так что процесс оптимизации оптимального решения, очевидно, станет плавный и тренировочный процесс легче сходится;
  • Преобразование канала: формат данных изображения — [H, W, C] (т. е. высота, ширина и количество каналов), тогда как обучающие данные, используемые нейронной сетью, имеют формат [C, H, W], поэтому данные изображения необходимо переупорядочить, например [224, 224, 3] становится [3, 224, 224];

декодирование изображения

import sys
import six
import cv2
import numpy as np
# https://github.com/PaddlePaddle/PaddleOCR/blob/release%2F2.4/ppocr/data/imaug/operators.py
class DecodeImage(object):
    """ decode image """

    def __init__(self, img_mode='RGB', channel_first=False, **kwargs):
        self.img_mode = img_mode
        self.channel_first = channel_first

    def __call__(self, data):
        img = data['image']
        if six.PY2:
            assert type(img) is str and len(
                img) > 0, "invalid input 'img' in DecodeImage"
        else:
            assert type(img) is bytes and len(
                img) > 0, "invalid input 'img' in DecodeImage"
        # 1. 图像解码
        img = np.frombuffer(img, dtype='uint8')
        img = cv2.imdecode(img, 1)

        if img is None:
            return None
        if self.img_mode == 'GRAY':
            img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
        elif self.img_mode == 'RGB':
            assert img.shape[2] == 3, 'invalid shape of image[%s]' % (img.shape)
            img = img[:, :, ::-1]

        if self.channel_first:
            img = img.transpose((2, 0, 1))
        # 2. 解码后的图像放在字典中
        data['image'] = img
        return data

Затем прочитайте изображение из аннотаций обучающих данных, чтобы продемонстрировать использование класса DecodeImage.

import json
import cv2
import os
import numpy as np
import matplotlib.pyplot as plt
# 在notebook中使用matplotlib.pyplot绘图时,需要添加该命令进行显示
%matplotlib inline
from PIL import Image
import numpy as np


label_path = "/home/aistudio/data/data96799/icdar2015/text_localization/train_icdar2015_label.txt"
img_dir = "/home/aistudio/data/data96799/icdar2015/text_localization/"

# 1. 读取训练标签的第一条数据
f = open(label_path, "r")
lines = f.readlines()

# 2. 取第一条数据
line = lines[0]

print("The first data in train_icdar2015_label.txt is as follows.\n", line)
img_name, gt_label = line.strip().split("\t")

# 3. 读取图像
image = open(os.path.join(img_dir, img_name), 'rb').read()
data = {'image': image, 'label': gt_label}

Объявите класс DecodeImage, декодируйте изображение и верните новые данные словаря.

# 4. 声明DecodeImage类,解码图像
decode_image = DecodeImage(img_mode='RGB', channel_first=False)
data = decode_image(data)

# 5. 打印解码后图像的shape,并可视化图像
print("The shape of decoded image is ", data['image'].shape)

plt.figure(figsize=(10, 10))
plt.imshow(data['image'])
src_img = data['image']

output_33_2.png

output_9_1.png Декодирование тегов

Разобрать информацию о этикетке в файле txt и сохранить в едином формате;

import numpy as np
import string
import json

# 详细实现参考: https://github.com/PaddlePaddle/PaddleOCR/blob/release%2F2.4/ppocr/data/imaug/label_ops.py#L38
class DetLabelEncode(object):
    def __init__(self, **kwargs):
        pass

    def __call__(self, data):
        label = data['label']
        # 1. 使用json读入标签
        label = json.loads(label)
        nBox = len(label)
        boxes, txts, txt_tags = [], [], []
        for bno in range(0, nBox):
            box = label[bno]['points']
            txt = label[bno]['transcription']
            boxes.append(box)
            txts.append(txt)
            # 1.1 如果文本标注是*或者###,表示此标注无效
            if txt in ['*', '###']:
                txt_tags.append(True)
            else:
                txt_tags.append(False)
        if len(boxes) == 0:
            return None
        boxes = self.expand_points_num(boxes)
        boxes = np.array(boxes, dtype=np.float32)
        txt_tags = np.array(txt_tags, dtype=np.bool)

        # 2. 得到文字、box等信息
        data['polys'] = boxes
        data['texts'] = txts
        data['ignore_tags'] = txt_tags
        return data

Запустите следующий код, чтобы наблюдать за сравнением до и после того, как класс DetLabelEncode декодирует метку.

# 从PaddleOCR中import DetLabelEncode
from ppocr.data.imaug.label_ops import DetLabelEncode

# 1. 声明标签解码的类
decode_label = DetLabelEncode()

# 2. 打印解码前的标签
print("The label before decode are: ", data['label'])

# 3. 标签解码
data = decode_label(data)
print("\n")

# 4. 打印解码后的标签
print("The polygon after decode are: ", data['polys'])
print("The text after decode are: ", data['texts'])
The label before decode are:  [{"transcription": "###", "points": [[427, 293], [469, 293], [468, 315], [425, 314]]}, {"transcription": "###", "points": [[480, 291], [651, 289], [650, 311], [479, 313]]}, {"transcription": "Ave", "points": [[655, 287], [698, 287], [696, 309], [652, 309]]}, {"transcription": "West", "points": [[701, 285], [759, 285], [759, 308], [701, 308]]}, {"transcription": "YOU", "points": [[1044, 531], [1074, 536], [1076, 585], [1046, 579]]}, {"transcription": "CAN", "points": [[1077, 535], [1114, 539], [1117, 595], [1079, 585]]}, {"transcription": "PAY", "points": [[1119, 539], [1160, 543], [1158, 601], [1120, 593]]}, {"transcription": "LESS?", "points": [[1164, 542], [1252, 545], [1253, 624], [1166, 602]]}, {"transcription": "Singapore's", "points": [[1032, 177], [1185, 73], [1191, 143], [1038, 223]]}, {"transcription": "no.1", "points": [[1190, 73], [1270, 19], [1278, 91], [1194, 133]]}]


The polygon after decode are:  [[[ 427.  293.]
  [ 469.  293.]
  [ 468.  315.]
  [ 425.  314.]]

 [[ 480.  291.]
  [ 651.  289.]
  [ 650.  311.]
  [ 479.  313.]]

 [[ 655.  287.]
  [ 698.  287.]
  [ 696.  309.]
  [ 652.  309.]]

 [[ 701.  285.]
  [ 759.  285.]
  [ 759.  308.]
  [ 701.  308.]]

 [[1044.  531.]
  [1074.  536.]
  [1076.  585.]
  [1046.  579.]]

 [[1077.  535.]
  [1114.  539.]
  [1117.  595.]
  [1079.  585.]]

 [[1119.  539.]
  [1160.  543.]
  [1158.  601.]
  [1120.  593.]]

 [[1164.  542.]
  [1252.  545.]
  [1253.  624.]
  [1166.  602.]]

 [[1032.  177.]
  [1185.   73.]
  [1191.  143.]
  [1038.  223.]]

 [[1190.   73.]
  [1270.   19.]
  [1278.   91.]
  [1194.  133.]]]
The text after decode are:  ['###', '###', 'Ave', 'West', 'YOU', 'CAN', 'PAY', 'LESS?', "Singapore's", 'no.1']

Увеличение базовых данных

Расширение данных — это распространенный метод повышения точности обучения модели и обобщения модели.Увеличение данных, обычно используемое при обнаружении текста, включает случайное горизонтальное переворачивание, случайное вращение, случайное масштабирование и случайное кадрирование.

Справочник по реализации кода для произвольного горизонтального переворота, случайного поворота и случайного масштабированиякод. Справочник по реализации кода расширения случайных данныхкод.

Получить метки карты пороговых значений

Используйте метод расширения, чтобы получить метки карты порогов, необходимые для обучения алгоритма;

import numpy as np
import cv2

np.seterr(divide='ignore', invalid='ignore')
import pyclipper
from shapely.geometry import Polygon
import sys
import warnings

warnings.simplefilter("ignore")

# 计算文本区域阈值图标签类
# 详细实现代码参考:https://github.com/PaddlePaddle/PaddleOCR/blob/release%2F2.4/ppocr/data/imaug/make_border_map.py
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']

        # 1. 生成空模版
        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

            # 2. draw_border_map函数根据解码后的box信息计算阈值图标签
            self.draw_border_map(text_polys[i], canvas, mask=mask)
        canvas = canvas * (self.thresh_max - self.thresh_min) + self.thresh_min

        data['threshold_map'] = canvas
        data['threshold_mask'] = mask
        return data
# 从PaddleOCR中import MakeBorderMap
from ppocr.data.imaug.make_border_map import MakeBorderMap

# 1. 声明MakeBorderMap函数
generate_text_border = MakeBorderMap()

# 2. 根据解码后的输入数据计算bordermap信息
data = generate_text_border(data)

# 3. 阈值图可视化
plt.figure(figsize=(10, 10))
plt.imshow(src_img)

text_border_map = data['threshold_map']
plt.figure(figsize=(10, 10))
plt.imshow(text_border_map)
<matplotlib.image.AxesImage at 0x7f6dc25a7310>

Получить метки карты вероятностей output_40_2.png output_40_1.pngИспользуйте метод сжатия, чтобы получить метки карты вероятностей, необходимые для обучения алгоритма;

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

# 计算概率图标签
# 详细代码实现参考: https://github.com/PaddlePaddle/PaddleOCR/blob/release%2F2.4/ppocr/data/imaug/make_shrink_map.py
class MakeShrinkMap(object):
    r'''
    Making binary mask from detection data with ICDAR format.
    Typically following the process of class `MakeICDARData`.
    '''

    def __init__(self, min_text_size=8, shrink_ratio=0.4, **kwargs):
        self.min_text_size = min_text_size
        self.shrink_ratio = shrink_ratio

    def __call__(self, data):
        image = data['image']
        text_polys = data['polys']
        ignore_tags = data['ignore_tags']

        h, w = image.shape[:2]
        # 1. 校验文本检测标签
        text_polys, ignore_tags = self.validate_polygons(text_polys,
                                                         ignore_tags, h, w)
        gt = np.zeros((h, w), dtype=np.float32)
        mask = np.ones((h, w), dtype=np.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:
                cv2.fillPoly(mask,
                             polygon.astype(np.int32)[np.newaxis, :, :], 0)
                ignore_tags[i] = True
            else:
                polygon_shape = Polygon(polygon)
                subject = [tuple(l) for l in polygon]
                padding = pyclipper.PyclipperOffset()
                padding.AddPath(subject, pyclipper.JT_ROUND,
                                pyclipper.ET_CLOSEDPOLYGON)
                shrinked = []

                # Increase the shrink ratio every time we get multiple polygon returned back
                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(f"Change shrink ratio to {ratio}")
                    distance = polygon_shape.area * (
                        1 - np.power(ratio, 2)) / polygon_shape.length
                    shrinked = padding.Execute(-distance)
                    if len(shrinked) == 1:
                        break

                if shrinked == []:
                    cv2.fillPoly(mask,
                                 polygon.astype(np.int32)[np.newaxis, :, :], 0)
                    ignore_tags[i] = True
                    continue

                for each_shrink in shrinked:
                    shrink = np.array(each_shrink).reshape(-1, 2)
                    cv2.fillPoly(gt, [shrink.astype(np.int32)], 1)

        data['shrink_map'] = gt
        data['shrink_mask'] = mask
        return data
# 从 PaddleOCR 中 import MakeShrinkMap
from ppocr.data.imaug.make_shrink_map import MakeShrinkMap

# 1. 声明文本概率图标签生成
generate_shrink_map = MakeShrinkMap()

# 2. 根据解码后的标签计算文本区域概率图
data = generate_shrink_map(data)

# 3. 文本区域概率图可视化
plt.figure(figsize=(10, 10))
plt.imshow(src_img)
text_border_map = data['shrink_map']
plt.figure(figsize=(10, 10))
plt.imshow(text_border_map)
<matplotlib.image.AxesImage at 0x7f6dc24dead0>

output_43_1.png

output_43_2.png

output_43_1.png Нормализованный

С помощью нормализации распределение входных значений любого нейрона в каждом слое нейронной сети преобразуется в стандартное нормальное распределение со средним значением 0 и дисперсией 1, так что процесс оптимизации оптимального решения, очевидно, примет вид плавно, и тренировочный процесс будет легче сходиться.

# 图像归一化类
class NormalizeImage(object):
    """ normalize image such as substract mean, divide std
    """

    def __init__(self, scale=None, mean=None, std=None, order='chw', **kwargs):
        if isinstance(scale, str):
            scale = eval(scale)
        self.scale = np.float32(scale if scale is not None else 1.0 / 255.0)
        # 1. 获得归一化的均值和方差
        mean = mean if mean is not None else [0.485, 0.456, 0.406]
        std = std if std is not None else [0.229, 0.224, 0.225]

        shape = (3, 1, 1) if order == 'chw' else (1, 1, 3)
        self.mean = np.array(mean).reshape(shape).astype('float32')
        self.std = np.array(std).reshape(shape).astype('float32')

    def __call__(self, data):
        # 2. 从字典中获取图像数据
        img = data['image']
        from PIL import Image
        if isinstance(img, Image.Image):
            img = np.array(img)
        assert isinstance(img, np.ndarray), "invalid input 'img' in NormalizeImage"

        # 3. 图像归一化
        data['image'] = (img.astype('float32') * self.scale - self.mean) / self.std
        return data

преобразование каналов

Формат данных изображения — [H, W, C] (то есть высота, ширина и количество каналов), тогда как обучающие данные, используемые нейронной сетью, имеют формат [C, H, W], поэтому данные изображения необходимо переупорядочить, например [ 224, 224, 3] становится [3, 224, 224];

# 改变图像的通道顺序,HWC to CHW
class ToCHWImage(object):
    """ convert hwc image to chw image
    """
    def __init__(self, **kwargs):
        pass

    def __call__(self, data):
        # 1. 从字典中获取图像数据
        img = data['image']
        from PIL import Image
        if isinstance(img, Image.Image):
            img = np.array(img)

        # 2. 通过转置改变图像的通道顺序
        data['image'] = img.transpose((2, 0, 1))
        return data

# 1. 声明通道变换类
transpose = ToCHWImage()

# 2. 打印变换前的图像
print("The shape of image before transpose", data['image'].shape)

# 3. 图像通道变换
data = transpose(data)

# 4. 打印通向通道变换后的图像
print("The shape of image after transpose", data['image'].shape)
The shape of image before transpose (720, 1280, 3)
The shape of image after transpose (3, 720, 1280)

3.3 Создание считывателя данных

В приведенном выше коде показан только метод чтения изображения и предварительной обработки, в реальном обучении модели в основном используется метод пакетного чтения и обработки данных.

В этом разделе используется PaddlePaddleDatasetиDatasetLoaderAPI создает средство чтения данных.

# dataloader构建详细代码参考:https://github.com/PaddlePaddle/PaddleOCR/blob/release%2F2.4/ppocr/data/simple_dataset.py

import numpy as np
import os
import random
from paddle.io import Dataset

def transform(data, ops=None):
    """ transform """
    if ops is None:
        ops = []
    for op in ops:
        data = op(data)
        if data is None:
            return None
    return data


def create_operators(op_param_list, global_config=None):
    """
    create operators based on the config
    Args:
        params(list): a dict list, used to create some operators
    """
    assert 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 error"
        op_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 ops


class SimpleDataSet(Dataset):
    def __init__(self, mode, label_file, data_dir, seed=None):
        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 lines

    def shuffle_data_random(self):
        #随机打乱数据
        random.seed(self.seed)
        random.shuffle(self.data_lines)
        return

    def __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 = None
        if outs is None:
            return self.__getitem__(np.random.randint(self.__len__()))
        return outs

    def __len__(self):
        # 返回数据集的大小
        return len(self.data_idx_order_list)

ВеслоВеслоDataloader APIМногопроцессорное чтение данных может использоваться в , а количество потоков может быть свободно установлено. Многопоточное чтение данных может ускорить обработку данных и обучение модели.Код реализации многопоточного чтения выглядит следующим образом:

from paddle.io import Dataset, DataLoader, BatchSampler, DistributedBatchSampler

def build_dataloader(mode, label_file, data_dir, batch_size, drop_last, shuffle, num_workers, seed=None):
    # 创建数据读取类
    dataset = SimpleDataSet(mode, label_file, data_dir, seed)
    # 定义 batch_sampler
    batch_sampler = BatchSampler(dataset=dataset, batch_size=batch_size, shuffle=shuffle, drop_last=drop_last)
    # 使用paddle.io.DataLoader创建数据读取器,并设置batchsize,进程数量num_workers等参数
    data_loader = DataLoader(dataset=dataset, batch_sampler=batch_sampler, num_workers=num_workers, return_list=True, use_shared_memory=False)

    return data_loader
ic15_data_path = "/home/aistudio/data/data96799/icdar2015/text_localization/"
train_data_label = "/home/aistudio/data/data96799/icdar2015/text_localization/train_icdar2015_label.txt"
eval_data_label = "/home/aistudio/data/data96799/icdar2015/text_localization/test_icdar2015_label.txt"

# 定义训练集数据读取器,进程数设置为8
train_dataloader = build_dataloader('Train', train_data_label, ic15_data_path, batch_size=8, drop_last=False, shuffle=True, num_workers=0)
# 定义验证集数据读取器
eval_dataloader = build_dataloader('Eval', eval_data_label, ic15_data_path, batch_size=1, drop_last=False, shuffle=False, num_workers=0)

3.4 Постобработка модели БД

Форма вывода головной сети БД такая же, как исходное изображение.Фактически, три функции канала, выдаваемые головной сетью БД, представляют собой карту вероятностей, карту порогов и двоичную карту текстовой области соответственно.

На этапе обучения три графика прогнозирования и реальные метки завершают расчет функции потерь и обучение модели;

На этапе прогнозирования вам нужно использовать только карту вероятности, а функция постобработки БД вычисляет координаты текстового поля, окружающего область текстового ответа, в соответствии с откликом текстовой области на карте вероятности.

Поскольку карта вероятностей, предсказанная сетью, является результатом сжатия, на этапе постобработки область предсказанного многоугольника расширяется с тем же значением смещения, чтобы получить окончательное текстовое поле. Реализация кода показана ниже.

# https://github.com/PaddlePaddle/PaddleOCR/blob/release%2F2.4/ppocr/postprocess/db_postprocess.py

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


class DBPostProcess(object):
    """
    The post process for Differentiable Binarization (DB).
    """

    def __init__(self,
                 thresh=0.3,
                 box_thresh=0.7,
                 max_candidates=1000,
                 unclip_ratio=2.0,
                 use_dilation=False,
                 score_mode="fast",
                 **kwargs):
        # 1. 获取后处理超参数
        self.thresh = thresh
        self.box_thresh = box_thresh
        self.max_candidates = max_candidates
        self.unclip_ratio = unclip_ratio
        self.min_size = 3
        self.score_mode = score_mode
        assert 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]])

        # DB后处理代码详细实现参考 https://github.com/PaddlePaddle/PaddleOCR/blob/release%2F2.4/ppocr/postprocess/db_postprocess.py

    def __call__(self, outs_dict, shape_list):

        # 1. 从字典中获取网络预测结果
        pred = outs_dict['maps']
        if isinstance(pred, paddle.Tensor):
            pred = pred.numpy()
        pred = pred[:, 0, :, :]

        # 2. 大于后处理参数阈值self.thresh的
        segmentation = pred > self.thresh

        boxes_batch = []
        for batch_index in range(pred.shape[0]):
            # 3. 获取原图的形状和resize比例
            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]

            # 4. 使用boxes_from_bitmap函数 完成 从预测的文本概率图中计算得到文本框
            boxes, scores = self.boxes_from_bitmap(pred[batch_index], mask,
                                                   src_w, src_h)

            boxes_batch.append({'points': boxes})
        return boxes_batch

Можно обнаружить, что каждое слово окружено синей рамкой. Эти синие прямоугольники получаются путем постобработки результатов сегментации, выводимых БД. Добавьте следующий код в строку 177 файла PaddleOCR/ppocr/postprocess/db_postprocess.py, чтобы визуализировать карту сегментации, выводимую БД.Результат визуализации карты сегментации сохраняется как изображение vis_segmentation.png.

_maps = np.array(pred[0, :, :] * 255).astype(np.uint8)
import cv2
cv2.imwrite("vis_segmentation.png", _maps)
# 1. 下载训练好的模型
!wget -nc -P ./pretrain_models/ https://paddleocr.bj.bcebos.com/dygraph_v2.0/en/det_mv3_db_v2.0_train.tar
!cd ./pretrain_models/ && tar xf det_mv3_db_v2.0_train.tar && cd ../

# 2. 执行文本检测预测得到结果
!python tools/infer_det.py -c configs/det/det_mv3_db.yml \
                           -o Global.checkpoints=./pretrain_models/det_mv3_db_v2.0_train/best_accuracy \
                              Global.infer_img=./doc/imgs_en/img_12.jpg 
                              #PostProcess.unclip_ratio=4.0
# 注:有关PostProcess参数和Global参数介绍与使用参考 https://github.com/PaddlePaddle/PaddleOCR/blob/release%2F2.3/doc/doc_ch/config.md

Визуализируйте график вероятности текста, предсказанный моделью предсказания, и окончательный результат предсказанного текстового поля.

img = Image.open('./output/det_db/det_results/img_12.jpg')
img = np.array(img)

# 画出读取的图片
plt.figure(figsize=(10, 10))
plt.imshow(img)

img = Image.open('./vis_segmentation.png')
img = np.array(img)

# 画出读取的图片
plt.figure(figsize=(10, 10))
plt.imshow(img)
<matplotlib.image.AxesImage at 0x7f6e7a6ee350>

output_58_1.png

output_58_2.png

Из результатов визуализации видно, что выходным результатом БД является бинарное изображение текстовой области, значение отклика текстовой области выше, а значение отклика нетекстовой фоновой области низкое. Постобработка БД заключается в том, чтобы найти минимальную ограничивающую рамку этих областей отклика, а затем получить координаты каждой текстовой области. Кроме того, путем изменения параметров постобработки можно настроить размер текстового поля или отфильтровать текстовое поле с плохим эффектом обнаружения.

Постобработка БД имеет четыре параметра, а именно:

  • thresh: порог бинаризации изображения сегментации в DBPostProcess, значение по умолчанию 0,3
  • box_thresh: Порог для фильтрации выходных блоков в DBPostProcess, ящики ниже этого порога не будут выводиться
  • unclip_ratio: Коэффициент увеличения текстового поля в DBPostProcess.
  • max_candidates: максимальное количество текстовых полей, выводимых в DBPostProcess, по умолчанию 1000.
# 3. 增大DB后处理的参数unlip_ratio为4.0,默认为1.5,改变输出的文本框大小,参数执行文本检测预测得到结果
!python tools/infer_det.py -c configs/det/det_mv3_db.yml \
                           -o Global.checkpoints=./pretrain_models/det_mv3_db_v2.0_train/best_accuracy \
                              Global.infer_img=./doc/imgs_en/img_12.jpg \
                              PostProcess.unclip_ratio=4.0
# 注:有关PostProcess参数和Global参数介绍与使用参考 https://github.com/PaddlePaddle/PaddleOCR/blob/release%2F2.4/doc/doc_ch/config.md
img = Image.open('./output/det_db/det_results/img_12.jpg')
img = np.array(img)

# 画出读取的图片
plt.figure(figsize=(10, 10))
plt.imshow(img)

img = Image.open('./vis_segmentation.png')
img = np.array(img)

# 画出读取的图片
plt.figure(figsize=(10, 10))
plt.imshow(img)
<matplotlib.image.AxesImage at 0x7f6e7a6060d0>

output_61_1.png

output_61_2.png

Из результатов выполнения приведенного выше кода видно, что после увеличения параметра unclip_ratio постобработки БД прогнозируемое текстовое поле становится значительно больше. Поэтому, когда результаты обучения не соответствуют нашим ожиданиям, результаты обнаружения текста можно скорректировать, настроив параметры постобработки. Кроме того, вы можете попробовать настроить остальные три параметра thresh, box_thresh, max_candidates, чтобы сравнить результаты обнаружения.

3.5 Определение функции потерь

Поскольку на этапе обучения получаются три карты предсказания, в функции потерь также необходимо объединить эти три карты предсказания и соответствующие им реальные метки, чтобы построить три части функции потерь. Формула для функции общих потерь определяется следующим образом:

L=Lb+α×Ls+β×LtL = L_b + \alpha \times L_s + \beta \times L_t

в,LLэто общая потеря,LsL_sДля потери карты вероятности в этом эксперименте используется потеря Dice с OHEM (онлайн-моделирование сложных примеров).LtL_tДля пороговой потери карты в этом эксперименте мы использовали разницу между предсказанным значением и меткойL1L_1расстояние,LbL_b— функция потерь бинарного изображения текста.α\alphaиβ\beta- весовой коэффициент, который в этом эксперименте установлен соответственно на 5 и 10.

три пораженияLbL_b,LsL_s,LtL_tЭто Dice Loss, Dice Loss (OHEM), MaskL1 Loss, а затем определяют эти три части:

  • Функция Dice Loss предназначена для сравнения сходства между предсказанным текстовым двоичным изображением и меткой.Она часто используется для сегментации двоичного изображения.Справочник по реализации кодаСсылка на сайт. Формула выглядит следующим образом:

dice_loss=12×intersection_areatotal_areadice\_loss = 1 - \frac{2 \times intersection\_area}{total\_area}

  • Dice Loss (OHEM) использует Dice Loss с OHEM, чтобы решить проблему несбалансированных положительных и отрицательных образцов. OHEM — это специальный метод автоматической выборки, который может автоматически выбирать сложные выборки для расчета потерь, тем самым улучшая эффект обучения модели. Здесь коэффициент выборки положительных и отрицательных образцов установлен на 1:3. Справочник по реализации кодаСсылка на сайт.

  • MaskL1 Loss вычисляет разницу между прогнозируемой текстовой пороговой картой и меткой.L1L_1расстояние.

from paddle import nn
import paddle
from paddle import nn
import paddle.nn.functional as F


# DB损失函数
# 详细代码实现参考:https://github.com/PaddlePaddle/PaddleOCR/blob/release%2F2.4/ppocr/losses/det_db_loss.py
class DBLoss(nn.Layer):
    """
    Differentiable Binarization (DB) Loss Function
    args:
        param (dict): the super paramter for DB Loss
    """

    def __init__(self,
                 balance_loss=True,
                 main_loss_type='DiceLoss',
                 alpha=5,
                 beta=10,
                 ohem_ratio=3,
                 eps=1e-6,
                 **kwargs):
        super(DBLoss, self).__init__()
        self.alpha = alpha
        self.beta = beta
        # 声明不同的损失函数
        self.dice_loss = DiceLoss(eps=eps)
        self.l1_loss = MaskL1Loss(eps=eps)
        self.bce_loss = BalanceLoss(
            balance_loss=balance_loss,
            main_loss_type=main_loss_type,
            negative_ratio=ohem_ratio)

    def forward(self, predicts, labels):
        predict_maps = predicts['maps']
        label_threshold_map, label_threshold_mask, label_shrink_map, label_shrink_mask = labels[
            1:]
        shrink_maps = predict_maps[:, 0, :, :]
        threshold_maps = predict_maps[:, 1, :, :]
        binary_maps = predict_maps[:, 2, :, :]
        # 1. 针对文本预测概率图,使用二值交叉熵损失函数
        loss_shrink_maps = self.bce_loss(shrink_maps, label_shrink_map,
                                         label_shrink_mask)
        # 2. 针对文本预测阈值图使用L1距离损失函数
        loss_threshold_maps = self.l1_loss(threshold_maps, label_threshold_map,
                                           label_threshold_mask)
        # 3. 针对文本预测二值图,使用dice loss损失函数
        loss_binary_maps = self.dice_loss(binary_maps, label_shrink_map,
                                          label_shrink_mask)

        # 4. 不同的损失函数乘上不同的权重
        loss_shrink_maps = self.alpha * loss_shrink_maps
        loss_threshold_maps = self.beta * loss_threshold_maps

        loss_all = loss_shrink_maps + loss_threshold_maps \
                   + loss_binary_maps
        losses = {'loss': loss_all, \
                  "loss_shrink_maps": loss_shrink_maps, \
                  "loss_threshold_maps": loss_threshold_maps, \
                  "loss_binary_maps": loss_binary_maps}
        return losses

3.6 Показатели оценки

Учитывая, что кадр обнаружения постобработки БД разнообразен и не горизонтален, в этом эксперименте для оценки используется простой способ вычисления IOU, а ссылка на код вычисленияМетод оценки обнаружения текста для icdar Challenges 4.

Существует три индикатора расчета для обнаружения текста, а именно Precision, Recall и Hmean, Логика расчета трех индикаторов выглядит следующим образом:

  1. Создайте матрицу размера [n, m] с именем iouMat, где n — количество блоков GT (наземной истины), а m — количество обнаруженных блоков; где n, m — количество блоков, исключая текст с меткой ## # ;
  2. В iouMat подсчитайте количество IOU, превышающее пороговое значение 0,5, и разделите это значение на число n в gt, чтобы получить отзыв;
  3. В iouMat подсчитайте количество IOU, превышающее пороговое значение 0,5, и разделите это значение на количество кадров обнаружения m, чтобы получить Precision;
  4. Метод расчета Hmean такой же, как и для оценки F1.Формула выглядит следующим образом:
Hmean=2.0*Precision*RecallPrecision+RecallHmean = 2.0* \frac{Precision * Recall}{Precision + Recall }

Основной код расчета метрики обнаружения текста выглядит следующим образом, полная ссылка на реализацию кодаСсылка на сайт:

# 文本检测metric指标计算方式如下:
# 完整代码参考 https://github.com/PaddlePaddle/PaddleOCR/blob/release%2F2.4/ppocr/metrics/det_metric.py
if len(gtPols) > 0 and len(detPols) > 0:
    outputShape = [len(gtPols), len(detPols)]

    # 1. 创建[n, m]大小的矩阵,用于保存计算的IOU
    iouMat = np.empty(outputShape)
    gtRectMat = np.zeros(len(gtPols), np.int8)
    detRectMat = np.zeros(len(detPols), np.int8)
    for gtNum in range(len(gtPols)):
        for detNum in range(len(detPols)):
            pG = gtPols[gtNum]
            pD = detPols[detNum]

            # 2. 计算预测框和GT框之间的IOU
            iouMat[gtNum, detNum] = get_intersection_over_union(pD, pG)
    for gtNum in range(len(gtPols)):
        for detNum in range(len(detPols)):
            if gtRectMat[gtNum] == 0 and detRectMat[
                    detNum] == 0 and gtNum not in gtDontCarePolsNum and detNum not in detDontCarePolsNum:

                # 2.1 统计IOU大于阈值0.5的个数
                if iouMat[gtNum, detNum] > self.iou_constraint:
                    gtRectMat[gtNum] = 1
                    detRectMat[detNum] = 1
                    detMatched += 1
                    pairs.append({'gt': gtNum, 'det': detNum})
                    detMatchedNums.append(detNum)

    # 3. IOU大于阈值0.5的个数除以GT框的个数numGtcare得到recall
    recall = float(detMatched) / numGtCare

    # 4. IOU大于阈值0.5的个数除以预测框的个数numDetcare得到precision
    precision = 0 if numDetCare == 0 else float(detMatched) / numDetCare

    # 5. 通过公式计算得到Hmean指标
    hmean = 0 if (precision + recall) == 0 else 2.0 * \
                                                    precision * recall / (precision + recall)

считать:

  1. Для ситуации на рисунке ниже, когда долговая расписка блока GT и поля прогноза больше 0,5, но текст пропущен, может ли приведенный выше расчет индикатора точно отражать точность модели?
  2. Как оптимизировать модель при возникновении таких проблем в экспериментальных сценариях?

Пример аннотации поля GT и поля предсказания


3.7 Обучение модели

После завершения обработки данных, определения сети и определения функции потерь можно запустить обучающую модель.

Обучение основано на обучении PaddleOCR в форме конфигурации параметров, ссылки на файл параметров.Ссылка на сайт, параметры сетевой структуры следующие:

Architecture:
  model_type: det
  algorithm: DB
  Transform:
  Backbone:
    name: MobileNetV3
    scale: 0.5
    model_name: large
  Neck:
    name: DBFPN
    out_channels: 256
  Head:
    name: DBHead
    k: 50

Параметры оптимизатора следующие:

Optimizer:
  name: Adam
  beta1: 0.9
  beta2: 0.999
  lr:
    learning_rate: 0.001
  regularizer:
    name: 'L2'
    factor: 0

Параметры постобработки следующие:

PostProcess:
  name: DBPostProcess
  thresh: 0.3
  box_thresh: 0.6
  max_candidates: 1000
  unclip_ratio: 1.5

...

Полный файл конфигурации параметров см.det_mv3_db.yml

!mkdir train_data 
!cd train_data && ln -s /home/aistudio/data/data96799/icdar2015  icdar2015
!wget -P ./pretrain_models/ https://paddle-imagenet-models-name.bj.bcebos.com/dygraph/MobileNetV3_large_x0_5_pretrained.pdparams

Модель после обучения сети по умолчанию сохраняется в каталоге PaddleOCR/output/db_mv3/.Если вы хотите изменить каталог сохранения, вы можете установить параметр Global.save_model_dir во время обучения, например:

# 设置参数文件里的Global.save_model_dir可以更改模型保存目录
python tools/train.py -c configs/det/det_mv3_db.yml -o Global.save_model_dir="./output/save_db_train/"

3.8 Оценка модели

В процессе обучения по умолчанию сохраняются две модели: одна — последняя обученная модель с именем last, а другая — модель с наивысшей точностью с именем best_accuracy. Затем используйте сохраненные параметры модели для оценки точности, полноты и hmean в тестовом наборе:

Код оценки точности обнаружения текста находится в PaddleOCR/ppocr/metrics/det_metric.py, Вы можете вызвать tools/eval.py, чтобы оценить точность обученной модели.

!python tools/eval.py -c configs/det/det_mv3_db.yml -o Global.checkpoints=./output/db_mv3/best_accuracy

3.9 Предсказание модели

После обучения модели вы также можете использовать сохраненную модель для выполнения вывода модели на изображении в наборе данных или изображении в папке, чтобы наблюдать эффект предсказания модели.

!python tools/infer_det.py -c configs/det/det_mv3_db.yml -o Global.checkpoints=./pretrain_models/det_mv3_db_v2.0_train/best_accuracy Global.infer_img=./doc/imgs_en/img_12.jpg

Прогнозируемое изображение сохраняется в каталоге ./output/det_db/det_results/ по умолчанию, а результаты визуализации с использованием библиотеки PIL следующие:

import matplotlib.pyplot as plt
# 在notebook中使用matplotlib.pyplot绘图时,需要添加该命令进行显示
%matplotlib inline
from PIL import Image
import numpy as np

img = Image.open('./output/det_db/det_results/img_12.jpg')
img = np.array(img)

# 画出读取的图片
plt.figure(figsize=(20, 20))
plt.imshow(img)
<matplotlib.image.AxesImage at 0x7f20c93d7050>

output_77_1.png

4. Резюме

В этом разделе рассказывается о быстром использовании модели обнаружения текста PaddleOCR, а в качестве примера используется алгоритм БД, чтобы представить процесс реализации от обработки данных до завершения обучения алгоритму обнаружения текста. В следующем разделе будет представлено соответствующее содержание алгоритмов распознавания текста.

FAQ

  1. Что делать, если я столкнулся с отсутствующей частью текста обнаружения на рисунке ниже?

Рисунок Обнаружение утечки в текстовой области


Приведенная выше проблема показывает, что часть текста обнаружена, но IOU поля прогнозирования текста и поля GT превышает пороговое значение 0,5, и индикаторы обнаружения не могут нормально возвращаться.Если таких результатов много, рекомендуется увеличить порог IOU. Кроме того, существенной причиной отсутствия обнаружения является то, что функции некоторых символов не реагируют.В конечном итоге сеть не изучает особенности отсутствия обнаружения. Рекомендуется детально проанализировать конкретные проблемы, визуализировать результаты прогноза и проанализировать причины пропущенного обнаружения, независимо от того, вызвано ли оно такими факторами, как освещение, деформация и длинный текст, а затем использовать улучшение данных, настройку сети или пост-обнаружение. методы обработки для оптимизации результатов обнаружения.

Дополнительные часто задаваемые вопросы по обнаружению текста см. в следующем разделе.