吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 4354|回复: 23
收起左侧

[Python 转载] 手动实现数字验证码识别

[复制链接]
Dreace 发表于 2020-7-27 23:49

本文首发于我的博客 Python 手动实现数字验证码识别

本人维护的一个项目 中北信息 小程序需要模拟登录来获取信息,这就需要在后台识别验证码。需要识别的验证码比较简单且为纯数字,有简单到可以忽略不计的变形,像下面这个样子。

验证码样例

可以看到稍微有一点噪点,应该是压缩解压过程造成的(JPG 是有损压缩)。

这是近乎标准印刷体的四位数字,只是有稍微倾斜,最开始想到的办法是 OCR 。

最初使用 Tesseract 进行本地 OCR 。效果不是很理想,重要问题是 Tesseract 是通用 OCR,他会识别所有的英文字符和标点,这就造成大量的识别不准确。而且 Tesseract 是一个神经网络,在本地运行相当耗费资源。在平时可以应付大部分情况,但在访问高峰时会消耗大量时间在多个识别线程中切换,甚至直接造成服务器宕机。 在没有新的解决方案之前使用了接近一年,只能说勉强能用。

前不久,发现百度云的 OCR 提供每天 5 万次的免费调用,测试发现可以使用且效果不错。使用一段时间后发现一些问题,异步的调用方式造成的延迟不可忽略,尤其是网络延迟。测试发现这个解决方案每个验证码识别有将近 1s 的网络延迟,这使得后台响应时间被延长,影响用户体验。虽然百度云不会将数字识别成其他字符,但经常出现只识别出两位数字的情况造成大量重试,更加影响性能。

两度更换解决方案后,还是没能找到令人满意的方案。决定自己实现验证码的识别,这里不需要训练神经网络,直接通过规则匹配能得到很好的性能与效果,最终单个验证码识别时间为 4-5ms,正确率 100%。

识别步骤如下:

  1. 灰度化
  2. 二值化
  3. 切割为单个数字
  4. 和字库对比

导入需要用到库

import math
import time
from io import BytesIO

import numpy as np
from PIL import Image

import data

最后一行导入的是字库,后面会写到。

灰度化

从之前的例子可以看到图像有一些噪点,为了不让其干扰识别需要进行降噪处理,第一步是将图片灰度化。所谓灰度化是将 RGB 色彩空间中三个量合并成一个量 L,这样原本的彩色图像变成灰度图了。每个像素的取值也由 $256^{3}$(16777216)个减少到 256 个,能够减少后继的计算量。将上面的图片灰度化之后变成下面的样子。

灰度化后

处理代码:

# 灰度处理并创建二维矩阵
img_matrix = np.array(Image.open(BytesIO(image_bytes)).convert("L"))

其中 Image.open(BytesIO(image_bytes)).convert("L") 是从 image_bytes 中创建图像并转为灰度图, image_bytes  可以是从网络加载或从本地文件读入的字节数据。然后根据灰度图创建一个 NumPy 矩阵方便后面的运算。

灰度化之后与原图好像没有区别是因为原图本身是黑白的,如果原图是彩色的话能够很明显地看出区别。

二值化

二值化是将图像的像素与某个阈值比较,若大于这个阈值则设置为灰度最大值(这里是 1,白色),小于某个值则设为灰度最小值(这里是 0,黑色)。这样上一步的灰度图就转化为二值图,便于接下来的图像分割。选择合适的阈值还可以去除图像噪点。

二值化后

二值化后噪点已经完全消失,数字的边界更加锐利。

处理代码:

# 获取矩阵(图像)的长宽
rows, cols = img_matrix.shape
for i in range(rows):
    for j in range(cols):
        # 与阈值比较
        if img_matrix[i, j] <= 128:
            # 设为灰度最小值
            img_matrix[i, j] = 0
        else:
            # 设为灰度最大值
            img_matrix[i, j] = 1

切割为单个数字

二值化后可以看到图像中有大量的空白,这对于识别会造成一些影响,并且对比多个验证码发现数字的垂直位置并非固定,也就是上下边距是浮动的。因此要将空白部分删掉只保留有图像的部分 。 先看实现:

# 每行最小值
row_min = np.min(img_matrix, axis=1)
# 找到第一个有图像的行
row_start = np.argmin(row_min)
# 找到最后一个有图像的行
row_end = np.argmin(np.flip(row_min))
# 只取有图像的行
img_matrix = img_matrix[row_start:-row_end, :]

去除上下空白

row_min 的值为

[1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1]

其中 1 表示该行最小值为 1 ,在本例中表示这一行都是 1 ,即没有黑色像素。0 则表示这一行有 0 的存在,即存在黑色像素,也就是这一行有图像,需要保留。np.argmin()row_min 中查找最小值第一次出现的位置,即为图像开始行。将 row_min 反转之后再次查找最小值下标就得到了结束下标,这个下标的是从右向左计数的。 得到 row_startrow_end 后对矩阵进行切片,row_end 要取负值表示表示从右开始计数。

接下来将数字切片成单个:

codes = [0] * 4
for i in range(4):
    # 切片
    imag_matrix_spited = img_matrix[:, 19 * i:19 * (i + 1)]
    col_min = np.min(imag_matrix_spited, axis=0)
    col_start = np.argmin(col_min)
    col_end = np.argmin(np.flip(col_min))
    # 图像宽度
    width = col_min.shape[0] - (col_start + col_end)
    # 宽度扩宽到 9 像素
    width_rest = 9 - width
    # 左边界
    col_start -= int(math.ceil(width_rest / 2.0))
    # 右边界
    col_end -= int(math.floor(width_rest / 2.0))
    # 裁剪为 9 像素宽的图像
    imag_matrix_spited = imag_matrix_spited[:, col_start:-col_end]

先粗略裁剪为每个数字 20 像素,然后再判断左边界和右边界,同之前一样的算法。但是这里不能直接使用得到的边界,每个数字的宽度不一样,直接切片会导致矩阵大小不一样,会影响之后的计算。因此要将将宽度统一为 9 像素,即为最宽的数字宽度。

切片结果-8

切片结果-4

切片结果-0

切片结果-3

和字库对比

res = [0] * 10
# 展开成一维
x = imag_matrix_spited.flatten()
for j in range(10):
    # 一次取字库中标准数据
    y = data.array_map[j]
    # 通过异或计算不同元素的数量
    res[j] = np.sum(x ^ y)
    # 取差异最小的下标
codes[i] = str(np.argmin(res))

首先将原先的二维矩阵展开为一维,再与字库中的数据对比找到差异最小的那个既是最终结果,这样整个验证码就识别出来了。字库则需要将展开后的矩阵按照顺序整理一下得到。

下面是针对这种验证码的字库(0~9):

import numpy as np

array_map = [0] * 10
array_map[0] = np.array(
    [1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1,
     0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1,
     1, 1, 1, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1])
array_map[1] = np.array(
    [1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1,
     1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1,
     0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1])
array_map[2] = np.array(
    [1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1,
     1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0,
     1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1])
array_map[3] = np.array(
    [1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1,
     1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1,
     1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1])
array_map[4] = np.array(
    [1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1,
     1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0,
     0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1])
array_map[5] = np.array(
    [1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1,
     0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1,
     1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1])
array_map[6] = np.array(
    [1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1,
     0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1,
     1, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1])
array_map[7] = np.array(
    [1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1,
     1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0,
     1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1])
array_map[8] = np.array(
    [1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1,
     1, 0, 0, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1,
     1, 1, 1, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1])
array_map[9] = np.array(
    [1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1,
     0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1,
     1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1])

至此验证码识别成功。

最后

完整的代码和一百个测试验证码可以在 Dreace/IVC 找到。

要求环境为 Python 3+,运行前请先安装 requirements.txt 中的依赖。

免费评分

参与人数 4吾爱币 +3 热心值 +4 收起 理由
nakasou + 1 + 1 热心回复!
shj2k + 1 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
yy95556864 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
joneqm + 1 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

 楼主| Dreace 发表于 2020-7-28 17:09
xinhan2012 发表于 2020-7-28 17:06
字库?稍微模糊点,识别率极速下降

同一个系统的验证码清晰度是一样的,这种写法也只针对一个系统的验证码
 楼主| Dreace 发表于 2020-8-7 16:28
caipei 发表于 2020-8-7 14:14
可以做成个软件,方便小白用呢

这是针对某一类验证码设计的一个方案,可以作为参考,但是本身不具备通用性。
niu645509965 发表于 2020-7-28 06:10
不错的文章,受教了,代码识别验证码是一个难题
wokai000 发表于 2020-7-28 06:43
看不懂,路过
SKgarlic 发表于 2020-7-28 07:23
虽然没懂,但看起来好厉害的样子
零度的轻吻 发表于 2020-7-28 07:38
不错,谢谢分享
shj2k 发表于 2020-7-28 07:47
不错,以后可能有用
nakasou 发表于 2020-7-28 08:07
嗯~完全看不懂,感谢分享了
sun12345 发表于 2020-7-28 08:08
还不错,用着很顺手
朱瑞宁 发表于 2020-7-28 08:10
感谢楼主分享
yu13740000 发表于 2020-7-28 08:29
厉害,学习了
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-11-26 02:04

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表