C++,python,热爱算法和机器学习
全部博文(1214)
分类: Python/Ruby
2014-09-22 13:49:48
为了防垃圾机器人,验证码是一种常用的手段。而自己来实现验证码也是很简单的事,只需要了解一点图像处理的方法就可以了。 PIL 是 Python 的一个图像处理库,可以很方便地处理位图。
首先考虑验证的制作方法,我们只想简单点的情况:
就是这样,最简单的情况。我们先实现,再看怎么能加点变化,以至不那么容易被破掉。
后面不会细讲 PIL 的使用方法,有兴趣,请浏览。
先来看如何得到一张带指定字母的图片。 PIL 中对图片的操作一般是通过 image 对象来完成,这个对象可以是从图片文件中得到的,已经包含了位图信息的对象。也可以一个我们指定大小创建的,不包含图片信息的“空对象”。
# -*- coding: utf-8 -*- from PIL import Image
im = Image.new('1', (100, 100), 'white')
im.show()
上面所示的代码,我们就可以得到一个 100x100 的白底空图片了。并且你可以看到图片显示了出来。
Image 的 new 方法新创建一个图片对象,第一个参数指定“模式”,不同的模式对应的每个像素的颜色表示也不同。比如:
关于模式不细说了,我们只使用最简单的单色,我们最终的图片也只需要黑白两色。
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.pil 和 xxx.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)
好了,能写出字了,就可以当验证码来用了。剩下就是加入一些图像的变化,以使验证码不容易被机器识别。
现在我们只是生成了一张图片,至于图片中的字母,随便找一个 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()
要做的事差不多了,最后输出图片就可以了。因为我们是验证码应用,所以不需要把图像的数据写到具体的文件当中,只需要输出字节流让应用返回给浏览器即可。
保存图像信息,直接使用 Image 对象的 save 方法即可。这个方法接受两个参数,第一个参数是要写入的文件对象,第二个参数是指定文件类型。
fileio = StringIO()
im.save(fileio, 'gif')
im.show()
文件对象我们就使用 cStringIO 模块中的 StringIO 来代替了。
最后的效果是这样的:
更麻烦的,你可以给字符加入旋转效果,写一个字符就随机旋转一定角度。 PIL 本身提供了对图片进行线性变换的一些操作方法。如果这些不能满足你,你也可以精确控制每一个像素的值。
# -*- 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()