阅读 4150

[译] 如何使用 Python 生成随机文本验证码

你肯定在网站上碰到过用于验证你是用户还是机器人的验证码。验证码起源于书本数字化的协作平台,现在演变成为一个由大众支持的图像标记项目(在某些情况下,还有音频识别),在这个项目中你甚至不知道你是一个服务提供商,而不是一个用户。

在这篇文章中,我们将会使用 Python 的 OpenCVPIL 库来学习如何生成我们自己的基本的文本验证码。

让我们现在开始!

创建画布

首先,我们需要从 PIL 中引入 ImageFontImageDrawImage 模块:

from PIL import ImageFont, ImageDraw, Image
复制代码

现在,我们要创建一个空白图像对象。为了达到这个目的,我们首先需要创建一个三维(对应三个颜色通道)numpy zeros 数组:

import numpy as np
img = np.zeros(shape=(25, 60, 3), dtype=np.uint8)
复制代码

这给了我们一个数组,其中每个元素表示图像中的一个像素,图像的大小为 60 x 25 像素:

array([[[0, 0, 0],
        [0, 0, 0],
        [0, 0, 0],
        ...,
        [0, 0, 0],
        [0, 0, 0],
        [0, 0, 0]]], dtype=uint8)
复制代码

为了使用这个数组创建一个图像,我们调用 Imagefromarray() 方法:

因为每一个像素值都是 (0, 0, 0),所以图像是黑色的,其中每一个值都表示红色,绿色,蓝色像素的亮度。亮度的取值范围为 0(最暗)到 255(最亮)。为了得到白色图像,我们在图像数组加上 255 即可:

由于我们需要使用白色背景,我们将会使用最后一个画布。

添加文本

现在,我们需要在我们的画布上绘制文本。为了达到这个目的,我们使用 ImageDrawDraw() 函数在我们的画布上创建一个绘制界面。

draw = ImageDraw.Draw(img_pil)
复制代码

然后,我们可以使用 Drawtext() 方法在画布上写文字。draw 方法需要以下参数:

  • xy:使用 (x, y) 元组指定文本的起始坐标。
  • text:需要绘制的文本。
  • font:使用的字体。这是 PIL 使用的 FreeType 或者 OpenType 字体,这也是 ImageFont 应用的地方。
  • fill:文本填充的颜色,表达式为 (R, G, B)

我们需要为这个函数准备字体对象。我们将会使用 ImageDrawtruetype() 函数:

font = ImageFont.truetype(font = ‘arial’, size=12)
复制代码

我们使用字体名称,但是 truetype 也可以使用系统上的字体路径。我们将会在稍后使用它。

现在,我们有了字体,让我们添加文本,然后看下验证码长什么样子:

我们也可以为图像添加线条,以此来迷惑任何尝试进入人类系统的机器。

元组指定线条的开始和结束的像素。第一条线条从 (0, 0) 开始,在 (60, 25) 结束。

使用更好的方式来显示根据数组渲染出来的图像是使用 OpenCV 的 imshow() 方法:

import cv2
cv2.imshow(‘OpenCV’,np.array(img_pil))
cv2.waitKey() #等待点击按钮再显示图像
cv2.destroyAllWindows()
复制代码

添加噪声

我们基本的验证码现在已经准备好了,但是跟你在网上看到的验证码相比,它很清晰,并且可读性高。让我们添加一些噪声。

添加噪声最简单的方式就是在图像上随机地添加白色和黑色的像素。这被称为盐和胡椒粉噪声。

首先,我们必须根据图像创建一个数组:

img = np.array(img_pil)
复制代码

然后,我定义需要被修改的像素的阈值。我们将它维持在 0.05 (5%)。添加噪声的代码就是使用一个简单的嵌套 for 循环,这个 for 循环随机的生成数字(0 - 1 之间)来决定是否将噪声添加到指定的像素。

import random
thresh = 0.05

for i in range(img.shape[0]):
    for j in range(img.shape[1]):
        rdn = random.random()
        if rdn < thresh:
            img[i][j] = 0
        elif rdn > 1-thresh:
            img[i][j] = 255
复制代码

在这里,如果随机值小于阈值(0.05),我们将它变为白色。如果它大于 0.95,我们将它变为黑色。除此之外,我们使它保持原样。添加了噪声之后的输出像下面这样:

胡椒粉噪声(黑色像素)非常的明显,但是一些之前文本上是黑色的像素现在也变为了白色(注意行间的中断)。

我们可以通过模糊图像来添加更多的噪声,这会使噪声扩散开来。使用 cv2,这只需要一行代码!

img_blurred = cv2.blur(img,(2,2))
复制代码

在这里,(2,2) 是平滑图像所使用的核心大小。阅读 OpenCV 的文档来了解更多相关的知识。

一切都随机化!

我们基本的代码已经准备好了......但是还缺少“随机文本验证码”中的“随机”。我们随机化大多数的事物,以此让每一个验证码都是唯一的。

所以,什么是可以被随机化的?好的,全都可以随机化,但我们只会专注于图像大小,文本(字符串,字体,字体大小,颜色),线条颜色,噪声阈值和噪声强度。

图像大小依赖于文本的长度和字体大小。所以我们先创建一个字体大小的变量和一个字符串长度的变量:

size = random.randint(10,16)
length = random.randint(4,8)
复制代码

在对字体大小、字符串长度和画布大小之间的关系进行大量实验之后,我得出的画布的大小如下:

img = np.zeros(((size*2)+5, length*size, 3), np.uint8)
复制代码

现在,为了随机化字体,我们可以使用 glob 库从系统字体路径中选择一些。我只使用 Arial 字体的变体。

然后我们可以通过使用 randint 选择一个随机字体:

fonts[random.randint(0, len(fonts)-1)]
复制代码

现在轮到文本了。我们生成一个给定字符串长度的随机的 ASCII 字母数字字符序列:

text = ''.join(
        random.choice(string.ascii_uppercase + string.digits + string.ascii_lowercase) 
                   for _ in range(length))
复制代码

因为长度已经是随机的了,我们只需要在调用这个函数的时候传递 length 参数:

对于文本和线条的颜色,之前我们仅仅使用了黑色(0, 0, 0)。这里可以使用 (random.randint(0,255), random.randint(0,255), random.randint(0,255)) 来进行随机化:

draw.text((5, 10), text, font=font, 
          fill=(random.randint(0,255), random.randint(0,255), random.randint(0,255)))
draw.line([(0, 0),(length*size,(size*2)+5)], width=1, 
          fill=(random.randint(0,255), random.randint(0,255), random.randint(0,255)))
复制代码

对于噪声阈值,我们可以设置它为 1% ~ 5% 之间的任意随机值:

thresh = random.randint(1,5)/100
复制代码

最后,对于噪声强度,我们不再对盐和胡椒粉像素使用绝对的白色和黑色,我们可以分别设置它们为随机的明和暗的阴影。

for i in range(img.shape[0]):
    for j in range(img.shape[1]):
        rdn = random.random()
        if rdn < thresh:
            img[i][j] = random.randint(0,123) #暗像素
        elif rdn > 1-thresh:
            img[i][j] = random.randint(123,255) #亮像素
复制代码

整合

# 配置画布
size = random.randint(10,16)
length = random.randint(4,8)
img = np.zeros(((size*2)+5, length*size, 3), np.uint8)
img_pil = Image.fromarray(img+255)

# 绘制文本和线条
font = ImageFont.truetype(fonts[random.randint(0, len(fonts)-1)], size)
draw = ImageDraw.Draw(img_pil)
text = ''.join(random.choice(string.ascii_uppercase + string.digits + string.ascii_lowercase) 
               for _ in range(length))
draw.text((5, 10), text, font=font, 
          fill=(random.randint(0,255), random.randint(0,255), random.randint(0,255)))
draw.line([(0, 0),(length*size,(size*2)+5)], width=1, 
          fill=(random.randint(0,255), random.randint(0,255), random.randint(0,255)))

# 添加噪声和模糊
img = np.array(img_pil)
thresh = random.randint(1,5)/100
for i in range(img.shape[0]):
    for j in range(img.shape[1]):
        rdn = random.random()
        if rdn < thresh:
            img[i][j] = random.randint(0,123)
        elif rdn > 1-thresh:
            img[i][j] = random.randint(123,255)
img = cv2.blur(img,(int(size/5),int(size/5)))

#显示图像
cv2.imshow(f"{text}", img)
cv2.waitKey()
cv2.destroyAllWindows()
复制代码

应用领域?

这项技术可以被用于生成验证码。你可以将带有文本的图像保存为要匹配的文件名。另一个新颖的应用是可以生成大量的标签图像来训练你的 OCR 模型。将上面的代码放入循环中,你将获得所需数量的图像!

如果你有更多的应用领域,请让我知晓!

如果发现译文存在错误或其他需要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可获得相应奖励积分。文章开头的 本文永久链接 即为本文在 GitHub 上的 MarkDown 链接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏