有的時候我們使用一些自動化腳本需要自動登錄到一些網站做一些操作,提高工作的效率。但驗證碼是一個攔路虎,面對各種複雜的甚至連人都可能搞錯的驗證碼,機器的自動識別往往變得異常艱難,與驗證碼的鬥爭使我們頭疼不已。
好消息是,隨着深度學習在圖像識別領域的發展,採用神經網絡對驗證碼圖像自動提取特徵,其識別精度往往讓人驚歎。但是,這類方法依賴於海量樣本,當樣本的數量達不到一定規模時,其識別效果也大打折扣。數據獲取和數據信息標註耗費了大量的人力物力,在實際生成應用中難以普遍的推廣。
那麼,問題來了,有沒有什麼辦法可以自動的獲取數據並進行標註呢?答案是:有!
驗證碼生成規律解析
收集一些驗證碼,如圖所示:
上圖驗證碼的來源和用途參考:全國增值稅發票查驗平臺驗證碼識別
通過觀察圖片我們發現了以下規律:
- 驗證碼由6個字符(數字/字母/漢字)組成,包括4種顏色(紅/黃/藍/黑)。
- 驗證碼圖片爲寬90、高35的三通道RGB圖像。
- 圖片背景由兩條隨機曲線劃分成多塊,包含兩種隨機的背景顏色。
- 圖片上有1-3根位置和長度都隨機干擾線,顏色爲綠色。
- 圖片上大約有50個隨機噪點,顏色隨機。
- 所有字符有相似的大小和統一的字體(漢字和數字字母爲兩種不同字體)。
驗證碼圖片生成——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()
運行代碼,就可以生成圖片了。(圖片的名字就是標籤)
我們來看一下效果吧:
左邊爲網站真實圖片,右邊爲生成圖片。