Python中如何实现验证码字符图像识别(数字字母混合)

需要识别的验证码图像,其中包含 4 个字符(数字字母)

验证码

验证码图片来源: http://my.cnki.net/elibregister/commonRegister.aspx

思路

  1. 灰度化:将图像转为灰度图像,即一个像素只有一种色阶(有 256 种不同灰度),值为 0 表示像素最黑,值为 255 表示像素最白。
  2. 二值化:将图像转为黑白图像,即一个像素只有黑白两种状态,不是黑就是白,没有灰色,值为 0 表示像素最黑,值为 1 表示像素最白.
  3. 图像转字符串:利用工具将图像中的字符串识别出来

前面两步都是对图像进行识别前处理,目的是提高计算机识别的准确度,毕竟计算机本身不能理解图像,一个像素值的微小变化都有可能导致错误识别

代码

import tesserocr
from PIL import Image

image = Image.open(“87FW.jpg”)

灰度化

image = image.convert(“L”)

二值化,传入的是数字 1,默认阈值是 127。一般不推荐使用,因为不够灵活

image = image.convert(“1”)

另一种二值化。自定义灰度,将灰度值在 115 以上的设置 1 (白色),其它设为 0 (黑色),相当于将阈值设置成了 115

table = [1] * 256 for i in range(256): table[i] = 0 if i > 115: break

image = image.point(table, “1”)

print(tesserocr.image_to_text(image))

打印:

87FW

所谓的阈( yu )值是指将不同的像素值分开的那个临界值

上面的代码没有保存图片,为了直观得看到经过不同的处理后图像的区别,下面展示的是两张图像分别是灰度处理和二值化(阈值 115 )后的图像

灰度处理

二值化(阈值 115 )

下面将每种不同阈值的图像保存至本地,主要代码如下:

...
image = Image.open("87FW.jpg")
image = image.convert("L")
table = [1] * 256
for i in range(256):
    table[i] = 0
    image.point(table, "1").save(f"87FW_{i}.jpg")

阈值为 0 代表将所有像素处理成白色(没有黑色);阈值为 255 代表将所有像素处理成黑色。

不同阈值的图片

可以发现阈值设置得越低,白色越多,能看得到的验证码(黑色)就少了,因为大部分灰度都处理成白色;反之,若阈值设置越大,黑色越多,更多的干扰像素处理成和验证码一样的黑色。

以下是将上面不同阈值的图片制作成的一个 gif 动态图像,可以看到如果阈值设定在 0 至 255 这个过程中,验证码会呈现出不同效果

阈值递增的动态图像

阈值是一个很难把控的关键,阈值设置大或小都会影响识别的准确性,以下是遍历所有阈值,测试阈值在哪个区间可以识别出正确的验证码。注:由于没有做优化,整个过程会比较慢

>>> for i in range(256):
...     if tesserocr.image_to_text(Image.open(f"87FW_{i}.jpg")).strip()=="87FW":
...             print(i, end=" ")
...
109 110 112 113 114 115 116 117 118 119 120 122 123 124 169 170 171 172 173

在 256 个阈值中只有 19 个(不足 7.42%)阈值可以正确识别出验证码,仔细察觉可以发现阈值区间被分成了多个,分别是 109 ~ 110、112 ~ 120、122 ~ 124、169 ~ 173,说明阈值区间不一定具有连续性。更糟糕的是,不同的验证码图片,能准确识别出其中验证码的阈值的数量、区间范围、区间数等都很可能不同。当然还有很多问题,比如选择一个“不恰当”的阈值导致图像处理过度,只识别出其中 3 个字符,不要试图随机添加一个字母或数字,因为需要考虑具体是哪个位置的字符没识别出来,这样瞎猜几乎是很难一次就命中的,好点的做法是:当识别出来的字符不足时可以尝试换一个阈值处理图像,所以能识别出验证码是概率事件。毕竟在正常的人机识别中,识别一个验证码通常只有一次机会,识别错了就会出现新的验证码,没有换阈值再重新试一次的机会,不过好在通常阈值的范围都是可以缩小的,比如可以忽略小于 70 和大于 200 的这些图像处理过度的阈值(正常人都很难识别是什么数字、字母),这样能命中的概率就会大大提高。

image.convert("1") 的默认阈值是 127,在上面 19 个可以准确识别验证码的阈值中没有 127,这也就是为什么直接使用 image.convert("1") 方法二值化的图像无法被准确识别出其中的验证码

上面的验证码还算容易处理的,如果干扰像素的灰度值与验证码灰度差别比较大,可用上面的方法;但如果遇到干扰线条的灰度与验证码差不多、验证码重叠等情况,上面对图像仅做简单处理的方法就很难奏效了。这时就需要用到机器学习技术对识别器进行训练,听说识别率几乎 100%!

参考资料:

  • 《 Python3 网络爬虫开发实战》—— 8.1 图形验证码的识别

阅读更多


Python中如何实现验证码字符图像识别(数字字母混合)

2 回复

对于验证码字符识别(数字字母混合),核心思路是预处理图像后分割字符,然后用训练好的模型识别。这里提供一个基于OpenCV和TensorFlow/Keras的完整方案。

1. 环境准备

pip install opencv-python tensorflow numpy matplotlib

2. 完整代码示例

import cv2
import numpy as np
import os
from sklearn.model_selection import train_test_split
from tensorflow.keras import layers, models
from tensorflow.keras.utils import to_categorical

# 1. 数据准备函数(假设你有标注好的单字符图片数据集)
def load_data(data_dir, img_size=(30, 30)):
    images = []
    labels = []
    chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    char_to_idx = {ch: i for i, ch in enumerate(chars)}
    
    for char in chars:
        char_dir = os.path.join(data_dir, char)
        if not os.path.exists(char_dir):
            continue
        for img_name in os.listdir(char_dir):
            img_path = os.path.join(char_dir, img_name)
            img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE)
            img = cv2.resize(img, img_size)
            img = img / 255.0  # 归一化
            images.append(img)
            labels.append(char_to_idx[char])
    
    return np.array(images), np.array(labels)

# 2. 构建CNN模型
def build_model(input_shape, num_classes):
    model = models.Sequential([
        layers.Reshape((*input_shape, 1), input_shape=input_shape),
        layers.Conv2D(32, (3, 3), activation='relu'),
        layers.MaxPooling2D((2, 2)),
        layers.Conv2D(64, (3, 3), activation='relu'),
        layers.MaxPooling2D((2, 2)),
        layers.Flatten(),
        layers.Dense(128, activation='relu'),
        layers.Dropout(0.5),
        layers.Dense(num_classes, activation='softmax')
    ])
    model.compile(optimizer='adam',
                  loss='sparse_categorical_crossentropy',
                  metrics=['accuracy'])
    return model

# 3. 验证码预处理和字符分割
def preprocess_and_segment(captcha_path):
    # 读取并预处理
    img = cv2.imread(captcha_path)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    _, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
    
    # 字符分割
    contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    char_imgs = []
    for cnt in contours:
        x, y, w, h = cv2.boundingRect(cnt)
        if w > 10 and h > 10:  # 过滤噪声
            char_img = gray[y:y+h, x:x+w]
            char_img = cv2.resize(char_img, (30, 30))
            char_imgs.append((char_img, x))
    
    # 按x坐标排序(从左到右)
    char_imgs.sort(key=lambda item: item[1])
    return [img for img, _ in char_imgs]

# 4. 主流程
def main():
    # 训练模型(如果有标注数据)
    # X, y = load_data("dataset/")
    # X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
    # model = build_model((30, 30), 36)
    # model.fit(X_train, y_train, epochs=10, validation_data=(X_test, y_test))
    # model.save("captcha_model.h5")
    
    # 加载预训练模型
    model = models.load_model("captcha_model.h5")
    
    # 识别新验证码
    chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    segmented = preprocess_and_segment("new_captcha.png")
    
    result = ""
    for char_img in segmented:
        char_img = char_img / 255.0
        pred = model.predict(char_img.reshape(1, 30, 30), verbose=0)
        result += chars[np.argmax(pred)]
    
    print(f"识别结果: {result}")

if __name__ == "__main__":
    main()

关键点说明:

  1. 数据准备:需要收集标注的单字符图片(每个文件夹以字符命名)
  2. 图像预处理:灰度化、二值化、去噪
  3. 字符分割:用轮廓检测找到每个字符位置
  4. 模型训练:CNN模型对36类(10数字+26字母)进行分类
  5. 预测:分割后逐个字符识别并拼接结果

建议: 实际应用时注意验证码的干扰线和扭曲处理,可适当增加数据增强。


然后哩?难的是在拆分成单字那一步

回到顶部