python 发票验证码自动生成

有的时候我们使用一些自动化脚本需要自动登录到一些网站做一些操作,提高工作的效率。但验证码是一个拦路虎,面对各种复杂的甚至连人都可能搞错的验证码,机器的自动识别往往变得异常艰难,与验证码的斗争使我们头疼不已。
好消息是,随着深度学习在图像识别领域的发展,采用神经网络对验证码图像自动提取特征,其识别精度往往让人惊叹。但是,这类方法依赖于海量样本,当样本的数量达不到一定规模时,其识别效果也大打折扣。数据获取和数据信息标注耗费了大量的人力物力,在实际生成应用中难以普遍的推广。
那么,问题来了,有没有什么办法可以自动的获取数据并进行标注呢?答案是:有!

验证码生成规律解析

收集一些验证码,如图所示:
在这里插入图片描述
上图验证码的来源和用途参考:全国增值税发票查验平台验证码识别
通过观察图片我们发现了以下规律:

  1. 验证码由6个字符(数字/字母/汉字)组成,包括4种颜色(红/黄/蓝/黑)。
  2. 验证码图片为宽90、高35的三通道RGB图像。
  3. 图片背景由两条随机曲线划分成多块,包含两种随机的背景颜色。
  4. 图片上有1-3根位置和长度都随机干扰线,颜色为绿色。
  5. 图片上大约有50个随机噪点,颜色随机。
  6. 所有字符有相似的大小和统一的字体(汉字和数字字母为两种不同字体)。

验证码图片生成——Captcha

python中有一款验证码生成的库captcha。
pip install captcha

from captcha.image import ImageCaptcha
chars = 'haha'
image = ImageCaptcha().generate_image(chars)
image.show()
image.save("test.png")

效果如下:
在这里插入图片描述
这个ImageCaptcha类的generate_image,返回的是一个"PIL.Image.Image"对象,可知该验证码生成库是基于于PIL库的。于是,我们去查看ImageCaptcha类的源码:

class ImageCaptcha(_Captcha):
    """Create an image CAPTCHA.

    Many of the codes are borrowed from wheezy.captcha, with a modification
    for memory and developer friendly.

    ImageCaptcha has one built-in font, DroidSansMono, which is licensed under
    Apache License 2. You should always use your own fonts::

        captcha = ImageCaptcha(fonts=['/path/to/A.ttf', '/path/to/B.ttf'])

    You can put as many fonts as you like. But be aware of your memory, all of
    the fonts are loaded into your memory, so keep them a lot, but not too
    many.

    :param width: The width of the CAPTCHA image.
    :param height: The height of the CAPTCHA image.
    :param fonts: Fonts to be used to generate CAPTCHA images.
    :param font_sizes: Random choose a font size from this parameters.
    """
    def __init__(self, width=160, height=60, fonts=None, font_sizes=None):
        self._width = width
        self._height = height
        self._fonts = fonts or DEFAULT_FONTS
        self._font_sizes = font_sizes or (42, 50, 56)
        self._truefonts = []

    @property
    def truefonts(self):
        if self._truefonts:
            return self._truefonts
        self._truefonts = tuple([
            truetype(n, s)
            for n in self._fonts
            for s in self._font_sizes
        ])
        return self._truefonts

    @staticmethod
    def create_noise_curve(image, color):
        w, h = image.size
        x1 = random.randint(0, int(w / 5))
        x2 = random.randint(w - int(w / 5), w)
        y1 = random.randint(int(h / 5), h - int(h / 5))
        y2 = random.randint(y1, h - int(h / 5))
        points = [x1, y1, x2, y2]
        end = random.randint(160, 200)
        start = random.randint(0, 20)
        Draw(image).arc(points, start, end, fill=color)
        return image

    @staticmethod
    def create_noise_dots(image, color, width=3, number=30):
        draw = Draw(image)
        w, h = image.size
        while number:
            x1 = random.randint(0, w)
            y1 = random.randint(0, h)
            draw.line(((x1, y1), (x1 - 1, y1 - 1)), fill=color, width=width)
            number -= 1
        return image

    def create_captcha_image(self, chars, color, background):
        """Create the CAPTCHA image itself.

        :param chars: text to be generated.
        :param color: color of the text.
        :param background: color of the background.

        The color should be a tuple of 3 numbers, such as (0, 255, 255).
        """
        image = Image.new('RGB', (self._width, self._height), background)
        draw = Draw(image)

        def _draw_character(c):
            font = random.choice(self.truefonts)
            w, h = draw.textsize(c, font=font)

            dx = random.randint(0, 4)
            dy = random.randint(0, 6)
            im = Image.new('RGBA', (w + dx, h + dy))
            Draw(im).text((dx, dy), c, font=font, fill=color)

            # rotate
            im = im.crop(im.getbbox())
            im = im.rotate(random.uniform(-30, 30), Image.BILINEAR, expand=1)

            # warp
            dx = w * random.uniform(0.1, 0.3)
            dy = h * random.uniform(0.2, 0.3)
            x1 = int(random.uniform(-dx, dx))
            y1 = int(random.uniform(-dy, dy))
            x2 = int(random.uniform(-dx, dx))
            y2 = int(random.uniform(-dy, dy))
            w2 = w + abs(x1) + abs(x2)
            h2 = h + abs(y1) + abs(y2)
            data = (
                x1, y1,
                -x1, h2 - y2,
                w2 + x2, h2 + y2,
                w2 - x2, -y1,
            )
            im = im.resize((w2, h2))
            im = im.transform((w, h), Image.QUAD, data)
            return im

        images = []
        for c in chars:
            if random.random() > 0.5:
                images.append(_draw_character(" "))
            images.append(_draw_character(c))

        text_width = sum([im.size[0] for im in images])

        width = max(text_width, self._width)
        image = image.resize((width, self._height))

        average = int(text_width / len(chars))
        rand = int(0.25 * average)
        offset = int(average * 0.1)

        for im in images:
            w, h = im.size
            mask = im.convert('L').point(table)
            image.paste(im, (offset, int((self._height - h) / 2)), mask)
            offset = offset + w + random.randint(-rand, 0)

        if width > self._width:
            image = image.resize((self._width, self._height))

        return image

    def generate_image(self, chars):
        """Generate the image of the given characters.

        :param chars: text to be generated.
        """
        background = random_color(238, 255)
        color = random_color(10, 200, random.randint(220, 255))
        im = self.create_captcha_image(chars, color, background)
        self.create_noise_dots(im, color)
        self.create_noise_curve(im, color)
        im = im.filter(ImageFilter.SMOOTH)
        return im


def random_color(start, end, opacity=None):
    red = random.randint(start, end)
    green = random.randint(start, end)
    blue = random.randint(start, end)
    if opacity is None:
        return (red, green, blue)
    return (red, green, blue, opacity)

我们发现 generate_image函数做了如下事情:
1、生成随机的背景颜色。
2、调用self.create_captcha_image将文字画到验证码图片上,具体使用的是PIL.ImageDraw.text方法。
3、self.create_noise_dots随机(数量、位置、颜色)生成多个噪点。
4、self.create_noise_curve随机(位置、颜色)生成一条干扰(曲)线。
5、im.filter图像平滑处理。

ImageCpatcha改写

ImageCaptcha类有四个初始化参数,分别是图片宽、图片高、字体(可以有多个)、字号(可以有多个)。通过设置这几个参数并不能达到模拟本文验证码生成的要求(譬如,字体颜色为固定四种,干扰线是直线)。于是我们来改写ImageCaptcha类。
首先是__init__函数

Color = {"red": (255, 0, 0), "yellow": (255, 255, 0), "blue": (0, 0, 255), "green": (0, 255, 0), "black": (0, 0, 0),
         "white": (255, 255, 255)}
class ImageCaptcha:

    def __init__(self, width, height, fonts, font_sizes, text_colors=None, noise_curve_color="green"):
        self._width = width
        self._height = height
        self._fonts = fonts
        self._font_sizes = font_sizes
        self._text_colors = [Color[x] for x in text_colors] if text_colors is not None else [Color["black"]]
        self._noise_curve_color = Color[noise_curve_color]
        self._truefonts = []
        self._font_sizes_len = len(self._font_sizes)

除了原来的四个参数,我们还增加了字体颜色和以及干扰线颜色两个参数。这样我们只需要在调用时设置高宽为90x35,字体为中文和英文的两种不同字体,字体大小18或19(观察对比后得到),字体颜色4种,干扰线颜色为绿色。如下:

imc = ImageCaptcha(width=90, 
				   height=35, 
				   fonts=[r"data\actionj.ttf", r"data\simsun.ttc"],
				   font_sizes=(18, 19),
				   text_colors=["black", "yellow", "blue", "red"],
				   noise_curve_color="green")
改写函数

干扰线: create_noise_curve

 @staticmethod
    def create_noise_line(image, color):
        w, h = image.size
        num = random.randint(0, 3)
        while num:
            x1 = random.randint(0, w)
            y1 = random.randint(0, h)
            x2 = random.randint(0, w)
            y2 = random.randint(0, h)
            points = [x1, y1, x2, y2]

            Draw(image).line(points, fill=color)
            num -= 1
        return image

噪点: create_noise_dots

 @staticmethod
    def create_noise_dots(image, number=50):

        draw = Draw(image)
        w, h = image.size
        while number:
            x1 = random.randint(0, w)
            y1 = random.randint(0, h)
            draw.point((x1, y1), fill=random_color(0, 255))
            number -= 1
        return image

创建验证码图片:create_captcha_image

    def create_captcha_image(self, chars, background):
        """Create the CAPTCHA image itself.
        :param chars: text to be generated.
        :param background: color of the background.
        """
        image = Image.new('RGB', (self._width, self._height), background)
        image = self.random_sin_fill(image)
        draw = Draw(image)

        def _draw_character(c, color=(255, 255, 255)):
            font = self.font_choice(c)
            w, h = draw.textsize(c, font=font)

            im = Image.new('RGBA', (w, h), color=background)
            Draw(im).text((0, 0), c, font=font, fill=color)

            # rotate
            im = im.crop(im.getbbox())
            im = im.rotate(random.uniform(-30, 30), expand=1)

            fff = Image.new("RGBA", size=im.size, color=background)
            im = Image.composite(im, fff, im)

            return im

        images = []
        colors = ""
        for c in chars:  # 单个字符图片生成
            index = random.randint(0, len(self._text_colors)-1)
            colors += str(index)
            color = self._text_colors[index]
            images.append(_draw_character(c, color))

        start = random.randint(0, 4)
        last_w, _ = images[-1].size # 最后一个字符的宽度
        max_interval = (self._width - last_w - start)//(len(images)-1)  # 字符最大间距,保证不会超出
        # print(max_interval)
        offset = start

        # 字符图片拼接到大图上
        for im in images:
            w, h = im.size
            self.combine(image, im, (offset,  (self._height - h)//2 + random.randint(-2, 2)), background)
            offset = offset + min(max_interval, max(int(0.7*w), 11)) + random.randint(-2, 0)

        return image, colors
新增函数

正弦曲线填充:random_sin_fill
采用上下两条正弦曲线将背景划分,填充另一种颜色。

 @staticmethod
    def random_sin_fill(image):

        x = np.linspace(-10, 10, 1000)
        y = np.sin(x)
        color = random_color(100, 255)
        
        # 上曲线
        xy = np.asarray(np.stack((x * 30 + random.randint(0, 90), y * 15 - random.randint(2, 10)), axis=1), dtype=int)
        xy = list(map(tuple, xy))
        Draw(image).polygon(xy, fill=color)
        
        # 下曲线
        xy = np.asarray(np.stack((x * 30 + random.randint(0, 90), y * 15 + random.randint(37, 45)), axis=1), dtype=int)
        xy = list(map(tuple, xy))
        Draw(image).polygon(xy, fill=color)
移除函数 im.filter

本文验证码不需要平滑处理。平滑处理后,字根本连人都认不得了。

生成最终图片

生成图片:generate_image

    def generate_image(self, chars):
        """Generate the image of the given characters.

        :param chars: text to be generated.
        """
        background = random_color(100, 255, 255)
        im, colors = self.create_captcha_image(chars, background)
        self.create_noise_dots(im)
        self.create_noise_line(im, self._noise_curve_color)
        # im = im.filter(ImageFilter.SMOOTH)
        return im, colors

值得注意的是,该函数除了返回图片对象外,还需要返回每个字符颜色,我们需要保存颜色作为标签。

随机生成验证码

修改了ImageCaptcha后,我们可以输入任意字符生成验证码。这里汉字选用了常用的汉字3500个,可以web搜索一下获得,我的代码里也有。

import os
import random
from ImageCaptcha import ImageCaptcha
import string

with open("data/chars.txt", "r", encoding="utf-8") as f:
    captcha_cn = f.read()  # 中文字符集

captcha_en = string.digits + string.ascii_lowercase  # 英文字符集

color_dict = ["黑", "黄", "蓝", "红"]


def random_captcha_text(num):

    # 选择0-2个英文字母(英文字母种类较少,不需要太多,可根据需求自行设置)
    en_num = random.randint(0, 2)
    cn_num = num - en_num

    example_en = random.sample(captcha_en, en_num)
    example_cn = random.sample(captcha_cn, cn_num)
    example = example_cn + example_en
    random.shuffle(example)

    # 将列表里的片段变为字符串并返回
    verification_code = ''.join(example)
    return verification_code


# 生成字符对应的验证码
def generate_captcha_image(path="fake_pic", num=1):

    imc = ImageCaptcha(width=90, height=35, fonts=[r"data\actionj.ttf", r"data\simsun.ttc"], font_sizes=(18, 19),
                       text_colors=["black", "yellow", "blue", "red"])

    # 获得随机生成的6个验证码字符
    captcha_text = random_captcha_text(6)
    
    if not os.path.exists(path):
        print("目录不存在!,已自动创建")
        os.makedirs(path)
    for _ in range(num):
        image, colors = imc.generate_image(captcha_text)
        colors = "".join([color_dict[int(c)] for c in colors])
        print("生成的验证码的图片为:", captcha_text + "_" + colors)
        image.save(os.path.join(path, captcha_text + "_" + colors) + '.png')


if __name__ == '__main__':

    generate_captcha_image()

运行代码,就可以生成图片了。(图片的名字就是标签)
我们来看一下效果吧:

在这里插入图片描述
左边为网站真实图片,右边为生成图片。

写在最后

做了个小实验,拿20w生成图片作为训练集,1w生成图片做测试集,正确率为98%,模型见博客:全国增值税发票查验平台验证码识别
不过在实际的识别中,达不到这么好的效果,主要还是生成的验证码和真实的验证码还是有一定差距的,毕竟生成算法全靠懵和猜。
在训练集中加入一部分真实验证码能显著提升模型识别效果。如果追求很高的识别精度的话,尽量还是多用真实的数据。
代码地址:https://download.csdn.net/download/okfu_DL/12315368
看到这里了还不点在这里插入图片描述
  • 18
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 12
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值