您的位置:首页 > 脚本大全 > > 正文

python自动识别旋转验证码(Python实现字符型图片验证码识别完整过程详解)

更多 时间:2021-10-04 01:02:10 类别:脚本大全 浏览量:979

python自动识别旋转验证码

Python实现字符型图片验证码识别完整过程详解

1摘要

验证码是目前互联网上非常常见也是非常重要的一个事物,充当着很多系统的防火墙功能,但是随时ocr技术的发展,验证码暴露出来的安全问题也越来越严峻。本文介绍了一套字符验证码识别的完整流程,对于验证码安全和ocr识别技术都有一定的借鉴意义。

本文的基于传统的机器学习svm的源码共享:https://github.com/zhengwh/captcha-svm

2关键词

关键词:安全,字符图片,验证码识别,ocr,python,svm,pil

3免责声明

本文研究所用素材来自于某旧web框架的网站完全对外公开的公共图片资源。

本文只做了该网站对外公开的公共图片资源进行了爬取,并未越权做任何多余操作。

本文在书写相关报告的时候已经隐去漏洞网站的身份信息。

本文作者已经通知网站相关人员此系统漏洞,并积极向新系统转移。

本报告的主要目的也仅是用于ocr交流学习和引起大家对验证安全的警觉。

4引言

本章内容作为它的技术补充来给出相应的识别的解决方案,让读者对验证码的功能及安全性问题有更深刻的认识。

5基本工具

要达到本文的目的,只需要简单的编程知识即可,因为现在的机器学习领域的蓬勃发展,已经有很多封装好的开源解决方案来进行机器学习。普通程序员已经不需要了解复杂的数学原理,即可以实现对这些工具的应用了。

主要开发环境:

  • python3.5

python sdk版本

  • pil

图片处理库

  • libsvm

开源的svm机器学习库

关于环境的安装,不是本文的重点,故略去。

6基本流程

一般情况下,对于字符型验证码的识别流程如下:

1.准备原始图片素材
2.图片预处理
3.图片字符切割
4.图片尺寸归一化
5.图片字符标记
6.字符图片特征提取
7.生成特征和标记对应的训练数据集
8.训练特征标记数据生成识别模型
9.使用识别模型预测新的未知图片集
10.达到根据“图片”就能返回识别正确的字符集的目标

7素材准备

7.1素材选择

由于本文是以初级的学习研究目的为主,要求“有代表性,但又不会太难”,所以就直接在网上找个比较有代表性的简单的字符型验证码(感觉像在找漏洞一样)。

最后在一个比较旧的网站(估计是几十年前的网站框架)找到了这个验证码图片。

原始图:

python自动识别旋转验证码(Python实现字符型图片验证码识别完整过程详解)

放大清晰图:

python自动识别旋转验证码(Python实现字符型图片验证码识别完整过程详解)

此图片能满足要求,仔细观察其具有如下特点。

有利识别的特点:

由纯阿拉伯数字组成字数为4位字符排列有规律字体是用的统一字体

以上就是本文所说的此验证码简单的重要原因,后续代码实现中会用到

不利识别的特点:

图片背景有干扰噪点

这虽然是不利特点,但是这个干扰门槛太低,只需要简单的方法就可以除去

7.2素材获取

由于在做训练的时候,需要大量的素材,所以不可能用手工的方式一张张在浏览器中保存,故建议写个自动化下载的程序。

主要步骤如下:

通过浏览器的抓包功能获取随机图片验证码生成接口批量请求接口以获取图片将图片保存到本地磁盘目录中

这些都是一些it基本技能,本文就不再详细展开了。

关于网络请求和文件保存的代码,如下:

  • ?
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • def downloads_pic(**kwargs):
  •  pic_name = kwargs.get('pic_name', none)
  •  
  •  url = 'http://xxxx/rand_code_captcha/'
  •  res = requests.get(url, stream=true)
  •  with open(pic_path + pic_name+'.jpg" alt="python自动识别旋转验证码(Python实现字符型图片验证码识别完整过程详解)" border="0" />, 'wb') as f:
  •   for chunk in res.iter_content(chunk_size=1024):
  •    if chunk: # filter out keep-alive new chunks
  •     f.write(chunk)
  •     f.flush()
  •   f.close()
  • 循环执行n次,即可保存n张验证素材了。

    下面是收集的几十张素材库保存到本地文件的效果图:

    python自动识别旋转验证码(Python实现字符型图片验证码识别完整过程详解)

    8图片预处理

    虽然目前的机器学习算法已经相当先进了,但是为了减少后面训练时的复杂度,同时增加识别率,很有必要对图片进行预处理,使其对机器识别更友好。

    针对以上原始素材的处理步骤如下:

    1.读取原始图片素材
    2.将彩色图片二值化为黑白图片
    3.去除背景噪点

    8.1二值化图片

    主要步骤如下:

    1. 将rgb彩图转为灰度图
    2. 将灰度图按照设定阈值转化为二值图
  • ?
  • 1
  • 2
  • 3
  • 4
  • 5
  • image = image.open(img_path)
  • imgry = image.convert('l') # 转化为灰度图
  •  
  • table = get_bin_table()
  • out = imgry.point(table, '1')
  • 上面引用到的二值函数的定义如下:

  • ?
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • def get_bin_table(threshold=140):
  •  
  •  """
  •  
  •  获取灰度转二值的映射table
  •  
  •  :param threshold:
  •  
  •  :return:
  •  
  •  """
  •  
  •  table = []
  •  
  •  for i in range(256):
  •  
  •   if i < threshold:
  •  
  •    table.append(0)
  •  
  •   else:
  •  
  •    table.append(1)
  •  
  •  
  •  
  •  return table
  • 由pil转化后变成二值图片:0表示黑色,1表示白色。二值化后带噪点的6937的像素点输出后如下图:

    1110111011110111011111011110111100110111
    1101111111110110101111110101111111101111
    1100111011111000001111111001011111011111
    1101111011111111101111011110111111011111
    1110000111111000011101100001110111011111

    如果你是近视眼,然后离屏幕远一点,可以隐约看到6937的骨架了。

    8.2去除噪点

    在转化为二值图片后,就需要清除噪点。本文选择的素材比较简单,大部分噪点也是最简单的那种孤立点,所以可以通过检测这些孤立点就能移除大量的噪点。

    关于如何去除更复杂的噪点甚至干扰线和色块,有比较成熟的算法:洪水填充法 flood fill,后面有兴趣的时间可以继续研究一下。

    本文为了问题简单化,干脆就用一种简单的自己想的简单办法来解决掉这个问题:

    • 对某个 黑点 周边的九宫格里面的黑色点计数
    • 如果黑色点少于2个则证明此点为孤立点,然后得到所有的孤立点
    • 对所有孤立点一次批量移除。

    下面将详细介绍关于具体的算法原理。

    将所有的像素点如下图分成三大类

    顶点a非顶点的边界点b内部点c

    种类点示意图如下:

    python自动识别旋转验证码(Python实现字符型图片验证码识别完整过程详解)

    其中:

    •a类点计算周边相邻的3个点(如上图红框所示)
    •b类点计算周边相邻的5个点(如上图红框所示)
    •c类点计算周边相邻的8个点(如上图红框所示)

    当然,由于基准点在计算区域的方向不同,a类点和b类点还会有细分:

    •a类点继续细分为:左上,左下,右上,右下
    •b类点继续细分为:上,下,左,右
    •c类点不用细分

    然后这些细分点将成为后续坐标获取的准则。

    主要算法的python实现如下:

  • ?
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • def sum_9_region(img, x, y):
  •  """
  •  9邻域框,以当前点为中心的田字框,黑点个数
  •  :param x:
  •  :param y:
  •  :return:
  •  """
  •  # todo 判断图片的长宽度下限
  •  cur_pixel = img.getpixel((x, y)) # 当前像素点的值
  •  width = img.width
  •  height = img.height
  •  
  •  if cur_pixel == 1: # 如果当前点为白色区域,则不统计邻域值
  •   return 0
  •  
  •  if y == 0: # 第一行
  •   if x == 0: # 左上顶点,4邻域
  •    # 中心点旁边3个点
  •    sum = cur_pixel \
  •      + img.getpixel((x, y + 1)) \
  •      + img.getpixel((x + 1, y)) \
  •      + img.getpixel((x + 1, y + 1))
  •    return 4 - sum
  •   elif x == width - 1: # 右上顶点
  •    sum = cur_pixel \
  •      + img.getpixel((x, y + 1)) \
  •      + img.getpixel((x - 1, y)) \
  •      + img.getpixel((x - 1, y + 1))
  •  
  •    return 4 - sum
  •   else: # 最上非顶点,6邻域
  •    sum = img.getpixel((x - 1, y)) \
  •      + img.getpixel((x - 1, y + 1)) \
  •      + cur_pixel \
  •      + img.getpixel((x, y + 1)) \
  •      + img.getpixel((x + 1, y)) \
  •      + img.getpixel((x + 1, y + 1))
  •    return 6 - sum
  •  elif y == height - 1: # 最下面一行
  •   if x == 0: # 左下顶点
  •    # 中心点旁边3个点
  •    sum = cur_pixel \
  •      + img.getpixel((x + 1, y)) \
  •      + img.getpixel((x + 1, y - 1)) \
  •      + img.getpixel((x, y - 1))
  •    return 4 - sum
  •   elif x == width - 1: # 右下顶点
  •    sum = cur_pixel \
  •      + img.getpixel((x, y - 1)) \
  •      + img.getpixel((x - 1, y)) \
  •      + img.getpixel((x - 1, y - 1))
  •  
  •    return 4 - sum
  •   else: # 最下非顶点,6邻域
  •    sum = cur_pixel \
  •      + img.getpixel((x - 1, y)) \
  •      + img.getpixel((x + 1, y)) \
  •      + img.getpixel((x, y - 1)) \
  •      + img.getpixel((x - 1, y - 1)) \
  •      + img.getpixel((x + 1, y - 1))
  •    return 6 - sum
  •  else: # y不在边界
  •   if x == 0: # 左边非顶点
  •    sum = img.getpixel((x, y - 1)) \
  •      + cur_pixel \
  •      + img.getpixel((x, y + 1)) \
  •      + img.getpixel((x + 1, y - 1)) \
  •      + img.getpixel((x + 1, y)) \
  •      + img.getpixel((x + 1, y + 1))
  •  
  •    return 6 - sum
  •   elif x == width - 1: # 右边非顶点
  •    # print('%s,%s' % (x, y))
  •    sum = img.getpixel((x, y - 1)) \
  •      + cur_pixel \
  •      + img.getpixel((x, y + 1)) \
  •      + img.getpixel((x - 1, y - 1)) \
  •      + img.getpixel((x - 1, y)) \
  •      + img.getpixel((x - 1, y + 1))
  •  
  •    return 6 - sum
  •   else: # 具备9领域条件的
  •    sum = img.getpixel((x - 1, y - 1)) \
  •      + img.getpixel((x - 1, y)) \
  •      + img.getpixel((x - 1, y + 1)) \
  •      + img.getpixel((x, y - 1)) \
  •      + cur_pixel \
  •      + img.getpixel((x, y + 1)) \
  •      + img.getpixel((x + 1, y - 1)) \
  •      + img.getpixel((x + 1, y)) \
  •      + img.getpixel((x + 1, y + 1))
  •    return 9 - sum
  • tips:这个地方是相当考验人的细心和耐心程度了,这个地方的工作量还是蛮大的,花了半个晚上的时间才完成的。

    计算好每个像素点的周边像素黑点(注意:pil转化的图片黑点的值为0)个数后,只需要筛选出个数为1或者2的点的坐标即为孤立点。这个判断方法可能不太准确,但是基本上能够满足本文的需求了。

    经过预处理后的图片如下所示:

    python自动识别旋转验证码(Python实现字符型图片验证码识别完整过程详解)

    对比文章开头的原始图片,那些孤立点都被移除掉,相对比较干净的验证码图片已经生成。

    9图片字符切割

    由于字符型验证码图片本质就可以看着是由一系列的单个字符图片拼接而成,为了简化研究对象,我们也可以将这些图片分解到原子级,即:只包含单个字符的图片。

    于是,我们的研究对象由“n种字串的组合对象”变成“10种阿拉伯数字”的处理,极大的简化和减少了处理对象。

    9.1分割算法

    现实生活中的字符验证码的产生千奇百怪,有各种扭曲和变形。关于字符分割的算法,也没有很通用的方式。这个算法也是需要开发人员仔细研究所要识别的字符图片的特点来制定的。

    当然,本文所选的研究对象尽量简化了这个步骤的难度,下文将慢慢进行介绍。

    使用图像编辑软件(phoneshop或者其它)打开验证码图片,放大到像素级别,观察其它一些参数特点:

    python自动识别旋转验证码(Python实现字符型图片验证码识别完整过程详解)

    可以得到如下参数:

    •整个图片尺寸是 40*10
    •单个字符尺寸是 6*10
    •左右字符和左右边缘相距2个像素
    •字符上下紧挨边缘(即相距0个像素)

    这样就可以很容易就定位到每个字符在整个图片中占据的像素区域,然后就可以进行分割了,具体代码如下:

  • ?
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • def get_crop_imgs(img):
  •  """
  •  按照图片的特点,进行切割,这个要根据具体的验证码来进行工作. # 见原理图
  •  :param img:
  •  :return:
  •  """
  •  child_img_list = []
  •  for i in range(4):
  •   x = 2 + i * (6 + 4) # 见原理图
  •   y = 0
  •   child_img = img.crop((x, y, x + 6, y + 10))
  •   child_img_list.append(child_img)
  •  
  •  return child_img_list
  • 然后就能得到被切割的原子级的图片元素了:

    python自动识别旋转验证码(Python实现字符型图片验证码识别完整过程详解)

    9.2内容小结

    基于本部分的内容的讨论,相信大家已经了解到了,如果验证码的干扰(扭曲,噪点,干扰色块,干扰线……)做得不够强的话,可以得到如下两个结论:

    4位字符和40000位字符的验证码区别不大

    纯数字和数字及字母组合的验证码区别不大

    • 纯数字。分类数为10
    • 纯字母
      • 不区分大小写。分类数为26
      • 区分大小写。分类数为52
    • 数字和区分大小写的字母组合。分类数为62

    在没有形成指数级或者几何级的难度增加,而只是线性有限级增加计算量时,意义不太大。

    10尺寸归一

    本文所选择的研究对象本身尺寸就是统一状态:6*10的规格,所以此部分不需要额外处理。但是一些进行了扭曲和缩放的验证码,则此部分也会是一个图像处理的难点。

    11模型训练步骤

    在前面的环节,已经完成了对单个图片的处理和分割了。后面就开始进行识别模型的训练了。

    整个训练过程如下:

    1.大量完成预处理并切割到原子级的图片素材准备
    2.对素材图片进行人为分类,即:打标签
    3.定义单张图片的识别特征
    4.使用svm训练模型对打了标签的特征文件进行训练,得到模型文件

    12素材准备

    本文在训练阶段重新下载了同一模式的4数字的验证图片总计:3000张。然后对这3000张图片进行处理和切割,得到12000张原子级图片。

    在这12000张图片中删除一些会影响训练和识别的强干扰的干扰素材,切割后的效果图如下:

    python自动识别旋转验证码(Python实现字符型图片验证码识别完整过程详解)

    13素材标记

    由于本文使用的这种识别方法中,机器在最开始是不具备任何 数字的观念的。所以需要人为的对素材进行标识,告诉机器什么样的图片的内容是 1……。

    这个过程叫做“标记”。

    具体打标签的方法是:

    为0~9每个数字建立一个目录,目录名称为相应数字(相当于标签)

    人为判定图片内容,并将图片拖到指定数字目录中

    每个目录中存放100张左右的素材

    一般情况下,标记的素材越多,那么训练出的模型的分辨能力和预测能力越强。例如本文中,标记素材为十多张的时候,对新的测试图片识别率基本为零,但是到达100张时,则可以达到近乎100%的识别率

    python自动识别旋转验证码(Python实现字符型图片验证码识别完整过程详解)

    14特征选择

    对于切割后的单个字符图片,像素级放大图如下:

    python自动识别旋转验证码(Python实现字符型图片验证码识别完整过程详解)

    从宏观上看,不同的数字图片的本质就是将黑色按照一定规则填充在相应的像素点上,所以这些特征都是最后围绕像素点进行。

    字符图片宽6个像素,高10个像素,理论上可以最简单粗暴地可以定义出60个特征:60个像

    您可能感兴趣