Python深度学习基于Tensorflow(15)OCR验证码 文本检测与识别实例

文章目录

    • 文本检测
    • 文本识别
      • CTC层
      • 生成验证码并制作数据集
      • 建立模型
      • 模型推理
    • 参考

文本检测

文本检测和目标检测类似,其不同之处在于文本目标具有序列特征,有连续性,可以通过结合 Faster R-CNNLSTM 的方式进行文本检测,如 CTPN 网络,其网络结构来自论文: Detecting Text in Natural Image with Connectionist Text Proposal Network (arxiv.org);

![[Pasted image 20240603151528.png]]

CNN学习的是感受野内的空间信息,LSTM学习的是序列特征。对于文本序列检测,显然既需要CNN抽象空间特征,也需要序列特征(毕竟文字是连续的)。

详细可以查看场景文字检测—CTPN原理与实现 - 知乎 (zhihu.com),CTPN网络的缺点很明显,只能识别行排列文本,如果文本充满艺术排列效果就不是很好;

这里有其他更好的方法进行文本检测:

  1. FCENet:[2104.10442] Fourier Contour Embedding for Arbitrary-Shaped Text Detection (arxiv.org)
  2. DBNet:[1911.08947] Real-time Scene Text Detection with Differentiable Binarization (arxiv.org)
  3. DBNet++:[2202.10304] Real-Time Scene Text Detection with Differentiable Binarization and Adaptive Scale Fusion (arxiv.org)

详细介绍可以看这个网站:天天教程 (foobarweb.net)

文本识别

在检测到文本位置后,我们提取出文本图片,将文本图片转化为同一大小格式,接着我们需要执行将文本图片转化为字符串任务;

文章中使用的是 CNN+RNN+CTC(CRNN+CTC) 架构,具体如下图所示

![[Pasted image 20240603210003.png]]

CTC层

CTC (Connectionist Temporal Classification),其被设计用来解决输入序列和输出序列难以一一对应的问题。

我们在特征图展开为特征序列这一步骤中,将原始特征图切割成不同的小块,理想情况是不同小块分别顺序对应目标字符串中某一字符,由于图片的无规则性以及感受野的存在,这种情况是不现实的,目标字符串中的字符很有可能被不同的块分别对应,这会导致字符串重复的情况出现,因此我们需要对齐预测序列和真实序列,Sequence Modeling with CTC (distill.pub)详细介绍了这一效果;

CTC 本质上是一个 softmax 矩阵,其 row nums 由预测的字符类别数量决定,其 col nums 又特征图切割成不同的小块的数量决定,这里 col nums 又称为时间步 T , 假设我们预测的字符串长度为 true_string_length ,由于每一时间步只能预测一个字符再加上空字符和重复字符的出现,我们必须要保证时间步 T 要大于 true_string_length

对重复字符进行删除,如下图1所示,这会导致两个问题:

  1. 通常,强制每个输入步骤与某些输出对齐是没有意义的。例如,在语音识别中,输入可能会出现一段沉默而没有相应的输出。
  2. 我们无法产生连续多个字符的输出。考虑对齐方式 [h, h, e, l, l, l, o]。折叠重复将产生“helo”而不是“hello”。

![[Pasted image 20240603214024.png]]

这里引入一个空字符: ϵ \epsilon ϵ,利用该字符去间隔重复字符,然后进行删除就可以解决上面两个问题,具体效果如上图2所示;

如何具体实施这一操作呢,这里我们可以对 y 进行处理,对 y 做一个简单的变换: π 0 = [ ϵ , y 1 , ϵ , y 2 , ϵ , … , y n , ϵ ] \pi_0=[\epsilon, y_1,\epsilon,y_2,\epsilon,\dots,y_n,\epsilon] π0=[ϵ,y1,ϵ,y2,ϵ,,yn,ϵ]
得到其概率为 p ( π 0 ∣ X ) p(\pi_0|X) p(π0X),由于 y 的变换有很多,如果 y 1 y_1 y1 不等于 y 2 y_2 y2 ,那么有许多的新变换,如 π 1 = [ ϵ , y 1 , y 2 , ϵ , ϵ , … , y n , ϵ ] \pi_1=[\epsilon, y_1,y_2,\epsilon,\epsilon,\dots,y_n,\epsilon] π1=[ϵ,y1,y2,ϵ,ϵ,,yn,ϵ] 等等

这里将所有的变换的概率值作为损失,得到损失如下: L = ∑ i − log ⁡ p ( y i ∣ X i ) = ∑ i ∑ j − log ⁡ p ( π i j ∣ X i ) \mathcal{L} = \sum_i -\log p(y_i|X_i)=\sum_i \sum_j-\log p(\pi_{ij}|X_i) L=ilogp(yiXi)=ijlogp(πijXi)
由于变换很多,单一计算非常困难,这里我们可以使用动态规划进行简化,详细请看:Sequence Modeling with CTC (distill.pub)

![[Pasted image 20240603220041.png]]

tensorflowCTC 损失计算的接口,接口如下:

tf.nn.ctc_loss(labels,inputs,sequence_length,preprocess_collapse_repeated=False,ctc_merge_repeated=True,ignore_longer_outputs_than_inputs=False,time_major=True
)

生成验证码并制作数据集

captcha 是 python 用来生成随机验证码的一个库,可以使用 pip install captcha 安装

定义两个函数 random_captcha_textgen_captcha_text_and_image 分别执行随机生成验证码文本和验证码图片生成任务;

import os
import random
from rich.progress import track
from captcha.image import ImageCaptchadef random_captcha_text(char_set=None, captcha_size=5):"""随机生成 number 和 alphabet 组合的字符串"""if char_set is None:number = [ '1', '2', '3', '4', '5', '6', '7', '8', '9']alphabet = [ 'a', 'd',  'h', 'j', 'k', 'q', 's', 't', 'y']char_set = number + alphabetcaptcha_text = []for i in range(captcha_size):c = random.choice(char_set)captcha_text.append(c)return ''.join(captcha_text)def gen_captcha_text_and_image(width=200, height=50, char_set=None, save_path='./captcha_imgs/'):"""随机生成验证码并保存在./captcha_imgs/文件目录下"""os.makedirs(save_path, exist_ok=True)ic = ImageCaptcha(width=width, height=height)captcha_text = random_captcha_text(char_set)img= ic.create_captcha_image(captcha_text,color='red', background='white')# create_noise_curve方法将上面生成的验证码 img 画上干扰线img = ic.create_noise_curve(img, color='black')img.save(save_path+captcha_text+".png")return captcha_text, img

利用 gen_captcha_text_and_image 生成3000个验证码图片作为数据集图片,下一步开始制作数据集

nums = 3000
for step in track(range(nums)):gen_captcha_text_and_image()# Working... ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 0:00:14

数据集制作

import os
import random
from pathlib import Path
import tensorflow as tf
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_splitdef process_data(img_path, label):"""dataset --> process_dataset"""img = tf.io.read_file(img_path)img = tf.io.decode_png(img, channels=1)img = tf.image.convert_image_dtype(img, tf.float32)label = tf.strings.unicode_split(label, input_encoding="UTF-8")label = char_num(label)# 固定 TensorSpecimg = tf.reshape(img, [50, 200, 1])label = tf.reshape(label, [5])return img, label# 存储验证码文件夹 ./captcha_imgs/
data_dir = Path('./captcha_imgs/')
image_paths = list(map(str, list(data_dir.glob("*.png"))))
labels = [image_path.split(os.path.sep)[1].split('.png')[0] for image_path in image_paths]# image_paths[0], labels[0]  --> ('captcha_imgs\\1113s.png', '1113s')characters = sorted(list(set(char for label in labels for char in label)))
# ['1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'd', 'h', 'j', 'k', 'q', 's', 't', 'y']# 定义两个转换,一个是将char转化为num,一个是将num转化为char
char_num = tf.keras.layers.StringLookup(vocabulary=characters, invert=False)
num_char = tf.keras.layers.StringLookup(vocabulary=characters, invert=True )# 切割数据集
X_train, X_test, y_train, y_test = train_test_split(image_paths, labels, test_size=0.33, random_state=42)# 定义超参数
batch_size = 16# 制作数据集
train_data = tf.data.Dataset.from_tensor_slices((X_train, y_train))
test_data = tf.data.Dataset.from_tensor_slices((X_test, y_test))
train_data = train_data.map(process_data).batch(batch_size).prefetch(buffer_size=tf.data.AUTOTUNE).cache()
test_data = test_data.map(process_data).batch(batch_size)

可视化数据代码

def plot_16_images():plt.figure(figsize=(10, 4))imgs, labels = train_data.take(1).get_single_element()for ix in range(imgs.shape[0]):plt.subplot(4, 4, ix+1)plt.imshow(imgs[ix])plt.title(tf.strings.reduce_join(num_char(labels[ix])).numpy().decode('utf-8'))plt.axis('off')plt.tight_layout()plt.show()plot_16_images()

得到结果如下

![[Pasted image 20240603235836.png]]

建立模型

模型架构如图,现在使用代码实现该模型架构;

![[Pasted image 20240603210003.png]]

实现模型架构代码如下:

import tensorflow as tfclass CustomModel(tf.keras.models.Model):def __init__(self):super(CustomModel, self).__init__()self.conv_1 = tf.keras.layers.Conv2D(32, 3, activation='relu', padding='same')self.conv_2 = tf.keras.layers.Conv2D(64, 3, activation='relu', padding='same')self.max_pool_1 = tf.keras.layers.MaxPooling2D((2,2))self.max_pool_2 = tf.keras.layers.MaxPooling2D((2,2))self.blstm_1 = tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(128, return_sequences=True, dropout=0.25))self.blstm_2 = tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(64, return_sequences=True, dropout=0.25))self.dense_1 = tf.keras.layers.Dense(64, activation='relu')self.dense_2 = tf.keras.layers.Dense(len(characters) + 2, activation='softmax')self.dropout = tf.keras.layers.Dropout(0.2)def call(self, x):x = tf.transpose(x, perm=[0, 2, 1, 3])x = self.conv_1(x)x = self.max_pool_1(x)x = self.conv_2(x)x = self.max_pool_2(x)x = tf.reshape(x, [tf.shape(x)[0], tf.shape(x)[1], -1])x = self.dense_1(x)x = self.dropout(x)x = self.blstm_1(x)x = self.blstm_2(x)x = self.dense_2(x)return xdef custom_loss(y_true, y_pred):batch_len = tf.cast(tf.shape(y_true)[0], dtype="int64")input_length = tf.cast(tf.shape(y_pred)[1], dtype="int64")label_length = tf.cast(tf.shape(y_true)[1], dtype="int64")# 1 慢一些# input_length = input_length * tf.ones(shape=(batch_len), dtype="int64")# label_length = label_length * tf.ones(shape=(batch_len), dtype="int64")# loss = tf.nn.ctc_loss(y_true, y_pred, label_length, input_length, logits_time_major=False)# 2 快一些input_length = input_length * tf.ones(shape=(batch_len, 1), dtype="int64")label_length = label_length * tf.ones(shape=(batch_len, 1), dtype="int64")loss = tf.keras.backend.ctc_batch_cost(y_true, y_pred, input_length, label_length)return lossmodel = CustomModel()
# build the model
model(train_data.take(1).get_single_element()[0])# summary the model
model.summary()

compile 模型并开始训练:

model.compile(optimizer='adam',loss=custom_loss
)model.fit(train_data, validation_data=test_data, epochs=200)

得到训练过程如下:

Epoch 1/200
169/169 [==============================] - 21s 61ms/step - loss: 22.0665 - val_loss: 16.1782
Epoch 2/200
169/169 [==============================] - 7s 39ms/step - loss: 16.1598 - val_loss: 16.1217
.........................................................................................
169/169 [==============================] - 7s 39ms/step - loss: 15.4558 - val_loss: 15.6200
Epoch 27/200
169/169 [==============================] - 7s 39ms/step - loss: 15.4595 - val_loss: 15.6041
Epoch 28/200
169/169 [==============================] - 7s 40ms/step - loss: 15.4209 - val_loss: 15.4314
Epoch 29/200
169/169 [==============================] - 7s 40ms/step - loss: 15.1606 - val_loss: 14.4924
Epoch 30/200
169/169 [==============================] - 7s 40ms/step - loss: 14.1823 - val_loss: 12.9583
Epoch 31/200
.........................................................................................
Epoch 42/200
169/169 [==============================] - 7s 40ms/step - loss: 0.6783 - val_loss: 0.2937
Epoch 43/200
169/169 [==============================] - 7s 40ms/step - loss: 0.6130 - val_loss: 0.2544
Epoch 44/200
169/169 [==============================] - 7s 39ms/step - loss: 0.4716 - val_loss: 0.2368
.........................................................................................
Epoch 194/200
169/169 [==============================] - 7s 40ms/step - loss: 0.0463 - val_loss: 0.1134
Epoch 195/200
169/169 [==============================] - 7s 40ms/step - loss: 0.0439 - val_loss: 0.0840
Epoch 196/200
169/169 [==============================] - 7s 41ms/step - loss: 0.0767 - val_loss: 0.1057
Epoch 197/200
169/169 [==============================] - 7s 41ms/step - loss: 0.0326 - val_loss: 0.0906
Epoch 198/200
169/169 [==============================] - 7s 41ms/step - loss: 0.0224 - val_loss: 0.0844
Epoch 199/200
169/169 [==============================] - 7s 41ms/step - loss: 0.0701 - val_loss: 0.1003
Epoch 200/200
169/169 [==============================] - 7s 40ms/step - loss: 0.0477 - val_loss: 0.0911

模型推理

当模型训练完毕后,模型的输出并不是目标字符串,仍然是一个 softmax 矩阵,因此我们需要对该矩阵继续进行操作;

当我们训练好一个RNN模型时,给定一个输入序列X,我们需要找到最可能的输出,也就是求解
Y ∗ = a r g m a x k P ( Y / X ) Y^*=\underset{k}{argmax} P(Y/X) Y=kargmaxP(Y/X)
求解最可能的输出有两种方案,一种是Greedy Search,第二种是Beam Search

  1. Greedy Search:每个时间片均取该时间片概率最高的节点作为输出
  2. Beam Search:Beam Search是寻找全局最优值和Greedy Search在查找时间和模型精度的一个折中。一个简单的beam search在每个时间片计算所有可能假设的概率,并从中选出最高的几个作为一组。然后再从这组假设的基础上产生概率最高的几个作为一组假设,依次进行,直到达到最后一个时间片。
def decode_batch_predictions(X, mode='greedy'):""" mode 有两种模式 beam 和 greedy 一般来说 greedy 效果要好一些"""y_pred = model(X)if mode == 'beam':input_length = tf.cast(tf.shape(y_pred)[1], dtype="int64")zz = tf.nn.ctc_beam_search_decoder(tf.transpose(y_pred, perm=[1,0,2]),  [50]*16)[0][0]zz = tf.strings.reduce_join(num_char(tf.sparse.to_dense(zz)), axis=-1).numpy()zz = [s.decode('utf-8').replace('[UNK]', '')[:5] for s in list(zz)]elif mode == 'greedy':input_length = tf.cast(tf.shape(y_pred)[1], dtype="int64")zz = tf.nn.ctc_greedy_decoder(tf.transpose(y_pred, perm=[1,0,2]),  [50]*16)[0][0]zz = tf.strings.reduce_join(num_char(tf.sparse.to_dense(zz)), axis=-1).numpy()zz = [s.decode('utf-8').replace('[UNK]', '')[:5] for s in list(zz)]return zz

可视化如下

def plot_16_images_pred():plt.figure(figsize=(10, 4))imgs, labels = test_data.take(1).get_single_element()pred_labels = decode_batch_predictions(imgs, mode='greedy')labels = [zz.decode('utf-8') for zz in tf.strings.reduce_join(num_char(labels), axis=-1).numpy()]for ix in range(imgs.shape[0]):plt.subplot(4, 4, ix+1)plt.imshow(imgs[ix])plt.title(f'pred:{pred_labels[ix]}-real:{labels[ix]}')plt.axis('off')plt.tight_layout()plt.show()return pred_labelsplot_16_images_pred()

![[Pasted image 20240604012816.png]]

完毕!

参考

  1. 场景文字检测—CTPN原理与实现 - 知乎 (zhihu.com)
  2. Detecting Text in Natural Image with Connectionist Text Proposal Network (arxiv.org)
  3. CTC Loss 数学原理讲解:Connectionist Temporal Classification-CSDN博客
  4. Sequence Modeling with CTC (distill.pub)
  5. 一文读懂CRNN+CTC文字识别 - 知乎 (zhihu.com)
  6. CTC(Connectionist Temporal Classification)介绍 - PilgrimHui - 博客园 (cnblogs.com)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/23918.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

学习请求接口

axios的方法 方法一 方法二 方式三 方式四 ajax请求 fetch请求 学习一下

告别冗长代码:Java Lambda 表达式如何简化你的编程

在现代软件开发中,高效和简洁的代码变得越来越重要。Java作为一门成熟而广泛使用的编程语言,一直在不断进化,以满足开发者的需求。Java 8的推出标志着一次重要的飞跃,其中最引人注目的特性之一便是Lambda表达式。 Lambda表达式为J…

RK3588推理RetinaFace出现问题

RK3588推理RetinaFace出现问题 在RK3588上测试RockChip提供的RetinaFace模型时,出现下面的问题 $ python RetinaFace_pic.py done --> Init runtime environment I RKNN: [02:27:16.501] RKNN Runtime Information: librknnrt version: 1.5.2 (c6b7b351a2023…

Windows11下Docker使用记录(五)

目录 准备1. WSL安装cuda container toolkit2. win11 Docker Desktop 设置3. WSL创建docker container并连接cuda4. container安装miniconda(可选) Docker容器可以从底层虚拟化,使我们能够在 不降级 CUDA驱动程序的情况下使用 任何版本的CU…

音视频开发19 FFmpeg 视频解码- 将 h264 转化成 yuv

视频解码过程 视频解码过程如下图所示: ⼀般解出来的是420p FFmpeg流程 这里的流程是和音频的解码过程一样的,不同的只有在存储YUV数据的时候的形式 存储YUV 数据 如果知道YUV 数据的格式 前提:这里我们打开的h264文件,默认是YU…

储能逆变器测试负载箱是如何实现的

储能逆变器测试负载箱是专门用于模拟各种负载条件的设备,主要用于对储能逆变器进行性能测试和评估。它可以根据实际需求,模拟不同的负载类型、负载大小和负载变化率,从而为储能逆变器的设计和优化提供准确的数据支持。那么,储能逆…

【递归、搜索与回溯】搜索

搜索 1.计算布尔二叉树的值2.求根节点到叶节点数字之和3. 二叉树剪枝4.验证二叉搜索树5.二叉搜索树中第K小的元素6.二叉树的所有路径 点赞👍👍收藏🌟🌟关注💖💖 你的支持是对我最大的鼓励,我们一…

defineProps定义类型

1.props标注类型 原因&#xff1a;确保给组件传递的props是类型安全的 <script setup lang"ts"> //1.使用别名类型或者接口定义Props类型 type Props {name:stringage?:number } //2 使用defineProps注解类型 const props defineProps<Props>() <…

全年申报!2024年陕西省双软企业认定条件标准、申报好处费用

1.双软企业是什么? 答:双软认证并不是一个资质,而是"软件产品登记"和"软件企业认定"两个不同资质的统称.叫做"双软企业" 2.双软企业的优惠政策是什么? 答:(1)软件产品登记的优惠政策:软件产品增值税,从13%减按3%征收,实行即征即退; (2)软件…

2024Stable Diffusion WebUI详细使用指南

Stable Diffusion WebUI&#xff08;AUTOMATIC1111&#xff0c;简称A1111&#xff09;是一个为高级用户设计的图形用户界面&#xff08;GUI&#xff09;&#xff0c;它提供了丰富的功能和灵活性&#xff0c;以满足复杂和高级的图像生成需求。由于其强大的功能和社区的活跃参与&…

22 - 游戏玩法分析 IV(高频 SQL 50 题基础版)

22 - 游戏玩法分析 IV 考点&#xff1a; 聚合函数 # 日期相加 date_add(min(event_date),INTERVAL 1 DAY) select round(count(distinct player_id)/(select count(distinct player_id) from Activity),2) fraction fromActivity where-- 如果日期加一天的数据能在表中…

pytorch笔记:自动混合精度(AMP)

1 理论部分 1.1 FP16 VS FP32 FP32具有八个指数位和23个小数位&#xff0c;而FP16具有五个指数位和十个小数位Tensor内核支持混合精度数学&#xff0c;即输入为半精度&#xff08;FP16&#xff09;&#xff0c;输出为全精度&#xff08;FP32&#xff09; 1.1.1 使用FP16的优缺…

MySQL主从同步优化指南:架构、瓶颈与解决方案

前言 ​ 在现代数据库架构中&#xff0c;MySQL 主从同步是实现高可用性和负载均衡的关键技术。本文将深入探讨主从同步的架构、延迟原因以及优化策略&#xff0c;并提供专业的监控建议。 MySQL 主从同步架构 ​ 主从复制流程&#xff1a; 从库生成两个线程&#xff0c;一个…

20 - 每月交易 I(高频 SQL 50 题基础版)

20 - 每月交易 I -- 考点&#xff1a;日期转换格式 -- date_format(trans_date,%Y-%m)select date_format(trans_date,%Y-%m) month,country,count(*) trans_count,sum(if(stateapproved,1,0)) approved_count,sum(amount) trans_total_amount,sum(if(state"approved&qu…

【主题广泛|稳定检索】2024年食品安全与生物技术国际会议(ICFSB 2024)

2024年食品安全与生物技术国际会议&#xff08;ICFSB 2024&#xff09; 2024 International Conference on Food Safety and Biotechnology 【重要信息】 大会地点&#xff1a;贵阳 大会官网&#xff1a;http://www.icicfsb.com 投稿邮箱&#xff1a;icicfsbsub-conf.com 【注…

语言大模型qwen1.5全流程解析:微调,量化与推理

在前一篇文章中&#xff0c;主要使用llama-factory封装的推理模块对速度进行了测试&#xff0c;vllm速度快些&#xff0c;但仍没有传说中的快3-5倍&#xff0c;需要单独测试。这里使用qwen1.5-1.8B作为测试模型。 qwen1.5是qwen2的先行版&#xff0c;24年2月发布&#xff0c;与…

jenkins插件之Jdepend

JDepend插件是一个为构建生成JDepend报告的插件。 安装插件 JDepend Dashboard -->> 系统管理 -->> 插件管理 -->> Available plugins 搜索 Jdepend, 点击安装构建步骤新增执行shell #执行pdepend if docker exec phpfpm82 /tmp/composer/vendor/bin/pdepe…

ComfyUI工作流分享-黏土特效工作流

大家给的教程都是苹果端使用Remini的软件制作&#xff0c;免费白嫖7天&#xff0c;7天后就要收费&#xff0c;作为ComfyUI技术党&#xff0c;当然是选择自己实现了&#xff0c;搭建一套工作流就搞定&#xff0c;这不&#xff0c;今天就来分享一套对应的黏土效果工作流&#xff…

TSINGSEE青犀视频:城市道路积水智能监管,智慧城市的守护者

随着城市化进程的加快&#xff0c;城市道路网络日益复杂&#xff0c;尤其在夏季&#xff0c;由于暴雨频发&#xff0c;道路积水问题成为影响城市交通和市民生活的重要因素之一。传统的道路积水监测方式往往依赖于人工巡逻和简单的监控设备&#xff0c;这些方法存在效率低下、响…

CAN总线学习笔记-CAN帧结构

数据帧 数据帧&#xff1a;发送设备主动发送数据&#xff08;广播式&#xff09; 标准格式的11ID不够用了&#xff0c;由此产生了扩展格式 SOF&#xff1a;帧起始&#xff0c;表示后面一段波形为传输的数据位 ID&#xff1a;标识符&#xff0c;区分功能&#xff0c;同时决定优…