在Python中用PIL做验证码

邹业盛 2016-01-10 16:55 更新
  1. 基本操作
  2. 加入变化
  3. 最后完成
  4. 代码

为了防垃圾机器人,验证码是一种常用的手段。而自己来实现验证码也是很简单的事,只需要了解一点图像处理的方法就可以了。 PIL 是 Python 的一个图像处理库,可以很方便地处理位图。

首先考虑验证的制作方法,我们只想简单点的情况:

  1. 生成一个固定大小的白色图片。
  2. 在图片上随机写几个字母。

就是这样,最简单的情况。我们先实现,再看怎么能加点变化,以至不那么容易被破掉。

后面不会细讲 PIL 的使用方法,有兴趣,请浏览官方的文档

1. 基本操作

先来看如何得到一张带指定字母的图片。 PIL 中对图片的操作一般是通过 image 对象来完成,这个对象可以是从图片文件中得到的,已经包含了位图信息的对象。也可以一个我们指定大小创建的,不包含图片信息的“空对象”。

# -*- coding: utf-8 -*-

from PIL import Image

im = Image.new('1', (100, 100), 'white')
im.show()

上面所示的代码,我们就可以得到一个 100x100 的白底空图片了。并且你可以看到图片显示了出来。

Imagenew 方法新创建一个图片对象,第一个参数指定“模式”,不同的模式对应的每个像素的颜色表示也不同。比如:

关于模式不细说了,我们只使用最简单的单色,我们最终的图片也只需要黑白两色。

new 的第二个参数指定图片的大小,第三个参数指定背影色。

show 方法是使用系统提供的工具把图片马上显示出来。

接下来要做的事,就是在这张白色图片上写几个字符了,这要用到 ImageDraw 对象:

# -*- coding: utf-8 -*-

from PIL import Image
from PIL import ImageDraw

im = Image.new('1', (100, 100), 'white')
draw = ImageDraw.Draw(im)
draw.text((0, 0), 'hello world!')
im.show()

结果如下图:

我们使用 Draw 对象的在新创建的白底图片的 (0,0) 位置写了 hello world!

字有些小,这是使用默认的字体的原因。我们可以使用指定的字体来生成验证码。

对于传统的位图字体, PCF, BDF 扩展名结束,先使用 PIL 提供的 pilfont.py 工具,产生 PIL 使用的专用字体文件:

pilfont.py  xxx.pcf

当前目录下,会得到两个需要的文件, xxx.pilxxx.pbm ,然后要使用字体时:

# -*- coding: utf-8 -*-

from PIL import Image
from PIL import ImageDraw
from PIL import ImageFont
FONT = ImageFont.load('xxx.pil')

im = Image.new('1', (100, 100), 'white')
draw = ImageDraw.Draw(im)
draw.text((0, 0), 'hello world!', font=FONT)
im.show()

在使用 text 方法时,使用 font 参数指定字体就可以了。

现在字会变得大多了:

如何要使用现在常用的矢量字体,可以这样:

font = ImageFont.truetype("arial.ttf", 15)

好了,能写出字了,就可以当验证码来用了。剩下就是加入一些图像的变化,以使验证码不容易被机器识别。

2. 加入变化

现在我们只是生成了一张图片,至于图片中的字母,随便找一个 OCR 软件都可以识别出来,我们还需要对它做一些变化处理。

验证码识别的难点之一就是字符分割,只要单个字符分割出来了,通过提取的样本进行最简单的匹配都可以达到很高的识别率。而给字符分割制造麻烦的最简单办法就是让字符与字符粘在一起。

前面已经介绍了如何在图片上写字。而让字符粘在一起,只需要分别控制每个字符的位置即可实现。这里,我自己实现的方法,是在一个足够小的区域中,让每个字符随机分布,因为随机选择的区域有限,所以,字符与字符之前有很大的概率会连在一起。另外,随机分布的话,还需要判断字符与字符之间的水平距离差,这个差值要大于一个临界值,以使人可以容易分辨出字符从左到右的顺序。

代码看最后的吧,这里使用示意图说明实现方法:

假设我们最后得到的图片长是 3-4 的距离,那么 4 个字符可以随机分布的区域在 1-2 之间,因为字符的位置是按矩形的左上角算的,避免出免字符超出边界而看不到的情况。如图所示,当 1-2 之间距离足够小的时间, 4 个字符就有很大的概率会重叠了。

另外的一点,就是对于字符与字符之间的水平距离,比如图中 5 和 6 的水平距离,它们的距离应该大于一个值,以保证这 4 个字符可以被看得出从左到右的顺序。而我们的字符是随机生成,并且是随机分布,所以,我们最后也是根据这 4 个字符的 X 轴位置的升序排列来得到“正确答案”的。

字符随机分布后,为了进一步加大机器识别的难度,我们还可以添加几根干扰线,这个就比较简单了。如图所示:

我们把整个图片看成 4 个象限,干扰线总是从第一象限的随机一点开始,以另外三个象限的随机一点结束。这样,干扰线同样也有很大的概率可以覆盖到图片上的字符。

关于画线,在 PIL 中,可以使用 ImageDraw 对象的 line 方法:

# -*- coding: utf-8 -*-

import PIL
from PIL import Image
from PIL import ImageDraw

im = Image.new('1', (500, 500), 'white')
draw = ImageDraw.Draw(im)
draw.line(((0, 0), (100, 200)))
im.show()

3. 最后完成

要做的事差不多了,最后输出图片就可以了。因为我们是验证码应用,所以不需要把图像的数据写到具体的文件当中,只需要输出字节流让应用返回给浏览器即可。

保存图像信息,直接使用 Image 对象的 save 方法即可。这个方法接受两个参数,第一个参数是要写入的文件对象,第二个参数是指定文件类型。

fileio = StringIO()
im.save(fileio, 'gif')
im.show()  

文件对象我们就使用 cStringIO 模块中的 StringIO 来代替了。

最后的效果是这样的:

更麻烦的,你可以给字符加入旋转效果,写一个字符就随机旋转一定角度。 PIL 本身提供了对图片进行线性变换的一些操作方法。如果这些不能满足你,你也可以精确控制每一个像素的值。

4. 代码

# -*- coding: utf-8 -*-
#AUTHOR: yeshengzou # # gmail.com
#DATE: 2012.4.23
#LICENCE: GPLv3

import PIL
from PIL import Image
from PIL import ImageDraw
from PIL import ImageFont
from random import randint
from cStringIO import StringIO

CHAR = 'acdefghijkmnpqrstuvwxyABCDEFGHJKLMNPQRSTUVWXY345789'
LEN = len(CHAR) - 1
PADDING = 30 
X_SPACE = 6 #两个字符之间最少相隔多少个像素
TRY_COUNT = 30 #随机字符的位置尝试最多多少次,避免死循环
WIDTH = 70
HEIGHT = 40
FONT = ImageFont.load('font.pil')

def gen():
    im = Image.new('1', (WIDTH, HEIGHT), 'white')
    draw = ImageDraw.Draw(im)
    w, h = im.size

    #S = [(x, y, 'c')]
    S = []
    x_list = []
    y_list = []
    n = 0
    while True:
        n += 1
        if n > TRY_COUNT:
            break
        x = randint(0, w - PADDING)
        flag = True
        for i in x_list:
            if abs(x - i) < X_SPACE:
                flag = False
                continue
            if not flag:
                break
        if not flag:
            continue

        y = randint(0, h - PADDING)
        x_list.append(x)
        y_list.append(y)
        S.append((x, y, CHAR[randint(0, LEN)]))
        if len(S) == 4:
            break

    for x, y, c in S:
        draw.text((x, y), c, font=FONT)

    #加3根干扰线
    for i in range(3):
        x1 = randint(0, (w - PADDING) / 2)
        y1 = randint(0, (h- PADDING / 2))
        x2 = randint(0, w)
        y2 = randint((h - PADDING / 2), h)
        draw.line(((x1, y1), (x2, y2)), fill=0, width=1)

    S.sort(lambda x, y: 1 if x[0] > y[0] else -1)
    char = [x[2] for x in S]
    fileio = StringIO()
    im.save(fileio, 'gif')
    im.show()
    return ''.join(char), fileio

if __name__ == '__main__':
    print gen()  
评论
©2010-2016 zouyesheng.com All rights reserved. Powered by GitHub , txt2tags , MathJax