您当前的位置:首页 > 计算机 > 编程开发 > Python

利用Python的图像处理模块pillow和OCR模块pytesseract识别图形验证码

时间:08-20来源:作者:点击数:

我们在网上浏览网页或注册账号时,会经常遇到验证码(CAPTCHA),如下图:

image.png
image.png

本文将具体介绍如何利用Python的图像处理模块pillow和OCR模块pytesseract来识别上述验证码(数字加字母)。

我们识别上述验证码的算法过程如下:

  • 将原图像进行灰度处理,转化为灰度图像;
  • 获取图片中像素点数量最多的像素(此为图片背景),将该像素作为阈值进行二值化处理,将灰度图像转化为黑白图像(用来提高识别的准确率);
  • 去掉黑白图像中的噪声,噪声定义为:以该点为中心的九宫格的黑点的数量小于等于4;
  • 利用pytesseract模块识别,去掉识别结果中的特殊字符,获得识别结果。

完整的Python代码如下:

# -*- coding:utf-8 -*-
import os,pytesseract
from PIL import Image
from collections import defaultdict


# tesseract.exe所在的文件路径
pytesseract.pytesseract.tesseract_cmd = r'D:\soft\tesseract-ocr\tesseract.exe'

# 获取图片中像素点数量最多的像素
def get_threshold(image):
    pixel_dict = defaultdict(int)
    # 像素及该像素出现次数的字典
    rows, cols = image.size
    # print('rows, cols:',rows, cols)
    for i in range(rows):
        for j in range(cols):
            pixel = image.getpixel((i, j))#返回坐标处的pixel值
            pixel_dict[pixel] += 1
    # print('pixel_dict:',pixel_dict)
    count_max = max(pixel_dict.values()) # 获取像素出现出多的次数
    pixel_dict_reverse = {v:k for k,v in pixel_dict.items()}
    threshold = pixel_dict_reverse[count_max] # 获取出现次数最多的像素点

    return threshold

# 按照阈值进行二值化处理
# threshold: 像素阈值
def get_bin_table(threshold):
    # 获取灰度转二值的映射table
    table = []
    for i in range(256):
        rate = 0.1 # 在threshold的适当范围内进行处理
        if threshold*(1-rate)<= i <= threshold*(1+rate):
            table.append(1)
        else:
            table.append(0)
    return table

# 去掉二值化处理后的图片中的噪声点
def cut_noise(image):
    rows, cols = image.size # 图片的宽度和高度
    change_pos = [] # 记录噪声点位置
    # 遍历图片中的每个点,除掉边缘
    for i in range(1, rows-1):
        for j in range(1, cols-1):
            # pixel_set用来记录该店附近的黑色像素的数量
            pixel_set = []
            # 取该点的邻域为以该点为中心的九宫格
            for m in range(i-1, i+2):
                for n in range(j-1, j+2):
                    if image.getpixel((m, n)) != 1: # 1为白色,0位黑色
                        pixel_set.append(image.getpixel((m, n)))

            # 如果该位置的九宫内的黑色数量小于等于4,则判断为噪声
            if len(pixel_set) <= 4:
                change_pos.append((i,j))

    # 对相应位置进行像素修改,将噪声处的像素置为1(白色)
    for pos in change_pos:
        image.putpixel(pos, 1)

    return image # 返回修改后的图片

# 识别图片中的数字加字母
# 传入参数为图片路径,返回结果为:识别结果
def OCR_lmj(img_path):
    image = Image.open(img_path) # 打开图片文件
    imgry = image.convert('L')  # 转化为灰度图
    # 获取图片中的出现次数最多的像素,即为该图片的背景
    max_pixel = get_threshold(imgry)
    # 将图片进行二值化处理
    table = get_bin_table(threshold=max_pixel)
    out = imgry.point(table, '1')
    # 去掉图片中的噪声(孤立点)
    out = cut_noise(out)
    #保存图片
    out.save('./img_gray.jpg')
    # 仅识别图片中的数字
    #text = pytesseract.image_to_string(out, config='digits')
    # 识别图片中的数字和字母
    text = pytesseract.image_to_string(out)
    # 去掉识别结果中的特殊字符
    exclude_char_list = ' .:\\|\'\"?![],()~@#$%^&*_+-={};<>/¥'
    text = ''.join([x for x in text if x not in exclude_char_list])
    return text

def main():
    # 识别指定文件目录下的图片
    dir = '../captcha'
    correct_count = 0  # 图片总数
    total_count = 0    # 识别正确的图片数量
    # 遍历figures下的png,jpg文件
    for file in os.listdir(dir):
        if file.endswith('.png') or file.endswith('.jpg'):
            # print(file)
            image_path = '%s/%s'%(dir,file) # 图片路径
            answer = file.split('.')[0]  # 图片名称,即图片中的正确文字
            recognizition = OCR_lmj(image_path) # 图片识别的文字结果

            print((answer, recognizition))
            if recognizition == answer: # 如果识别结果正确,则total_count加1
                correct_count += 1
            total_count += 1

    print('Total count: %d, correct: %d.'%(total_count, correct_count))


if __name__=='__main__':
    # main()
    # 单张图片识别
    image_path = '2.jpg'
    print(OCR_lmj(image_path))

# -*- coding:utf-8 -*-
from PIL import Image
from pytesseract import *
from fnmatch import fnmatch
from queue import Queue
import matplotlib.pyplot as plt
import cv2,time,os


def clear_border(img,img_name):
    '''去除边框'''
    filename = './out_img/' + img_name.split('.')[0] + '-clearBorder.jpg'
    h, w = img.shape[:2]
    for y in range(0, w):
        for x in range(0, h):
            # if y ==0 or y == w -1 or y == w - 2:
            if y < 4 or y > w -4:
                img[x, y] = 255
            # if x == 0 or x == h - 1 or x == h - 2:
            if x < 4 or x > h - 4:
                img[x, y] = 255
    cv2.imwrite(filename,img)
    return img

def interference_line(img, img_name):
    '''干扰线降噪'''
    filename =    './out_img/' + img_name.split('.')[0] + '-interferenceline.jpg'
    h, w = img.shape[:2]
    # !!!opencv矩阵点是反的
    # img[1,2] 1:图片的高度,2:图片的宽度
    for y in range(1, w - 1):
        for x in range(1, h - 1):
            count = 0
            if img[x, y - 1] > 245:
                count = count + 1
            if img[x, y + 1] > 245:
                count = count + 1
            if img[x - 1, y] > 245:
                count = count + 1
            if img[x + 1, y] > 245:
                count = count + 1
            if count > 2:
                img[x, y] = 255
    cv2.imwrite(filename,img)
    return img

def interference_point(img,img_name, x = 0, y = 0):
    """点降噪
    9邻域框,以当前点为中心的田字框,黑点个数
    :param x:
    :param y:
    :return:
    """
    filename = './out_img/' + img_name.split('.')[0] + '-interferencePoint.jpg'
    # todo 判断图片的长宽度下限
    cur_pixel = img[x,y]# 当前像素点的值
    height,width = img.shape[:2]

    for y in range(0, width - 1):
        for x in range(0, height - 1):
            if y == 0:    # 第一行
                if x == 0:    # 左上顶点,4邻域
                    # 中心点旁边3个点
                    sum = int(cur_pixel) + int(img[x, y + 1]) \
                                + int(img[x + 1, y]) + int(img[x + 1, y + 1])
                    if sum <= 2 * 245:
                        img[x, y] = 0
                elif x == height - 1:    # 右上顶点
                    sum = int(cur_pixel) + int(img[x, y + 1]) \
                                + int(img[x - 1, y]) + int(img[x - 1, y + 1])
                    if sum <= 2 * 245:
                        img[x, y] = 0
                else:    # 最上非顶点,6邻域
                    sum = int(img[x - 1, y]) + int(img[x - 1, y + 1]) + int(cur_pixel) \
                                + int(img[x, y + 1]) + int(img[x + 1, y]) + int(img[x + 1, y + 1])
                    if sum <= 3 * 245:
                        img[x, y] = 0
            elif y == width - 1:    # 最下面一行
                if x == 0:    # 左下顶点
                    # 中心点旁边3个点
                    sum = int(cur_pixel) + int(img[x + 1, y]) \
                                + int(img[x + 1, y - 1]) + int(img[x, y - 1])
                    if sum <= 2 * 245:
                        img[x, y] = 0
                elif x == height - 1:    # 右下顶点
                    sum = int(cur_pixel) + int(img[x, y - 1]) \
                                + int(img[x - 1, y]) + int(img[x - 1, y - 1])
                    if sum <= 2 * 245:
                        img[x, y] = 0
                else:    # 最下非顶点,6邻域
                    sum = int(cur_pixel) + int(img[x - 1, y]) + int(img[x + 1, y]) \
                                + int(img[x, y - 1]) + int(img[x - 1, y - 1]) + int(img[x + 1, y - 1])
                    if sum <= 3 * 245:
                        img[x, y] = 0
            else:    # y不在边界
                if x == 0:    # 左边非顶点
                    sum = int(img[x, y - 1]) + int(cur_pixel) + int(img[x, y + 1]) \
                                + int(img[x + 1, y - 1]) + int(img[x + 1, y]) + int(img[x + 1, y + 1])
                    if sum <= 3 * 245:
                        img[x, y] = 0
                elif x == height - 1:    # 右边非顶点
                    sum = int(img[x, y - 1]) + int(cur_pixel) + int(img[x, y + 1]) \
                                + int(img[x - 1, y - 1]) + int(img[x - 1, y]) + int(img[x - 1, y + 1])
                    if sum <= 3 * 245:
                        img[x, y] = 0
                else:    # 具备9领域条件的
                    sum = int(img[x - 1, y - 1]) + int(img[x - 1, y]) + int(img[x - 1, y + 1]) \
                                + int(img[x, y - 1]) + int(cur_pixel) + int(img[x, y + 1]) \
                                + int(img[x + 1, y - 1]) + int(img[x + 1, y]) + int(img[x + 1, y + 1])
                    if sum <= 4 * 245:
                        img[x, y] = 0
    cv2.imwrite(filename,img)
    return img

def _get_dynamic_binary_image(filedir, img_name):
    '''
    自适应阀值二值化
    '''
    filename = './out_img/' + img_name.split('.')[0] + '-binary.jpg'
    img_name = filedir + '/' + img_name
    im = cv2.imread(img_name)
    im = cv2.cvtColor(im,cv2.COLOR_BGR2GRAY)
    th1 = cv2.adaptiveThreshold(im, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 21, 1)
    cv2.imwrite(filename,th1)
    return th1

def _get_static_binary_image(img, threshold = 140):
    '''
    手动二值化
    '''
    img = Image.open(img)
    img = img.convert('L')
    pixdata = img.load()
    w, h = img.size
    for y in range(h):
        for x in range(w):
            if pixdata[x, y] < threshold:
                pixdata[x, y] = 0
            else:
                pixdata[x, y] = 255
    return img

def cfs(im,x_fd,y_fd):
    '''用队列和集合记录遍历过的像素坐标代替单纯递归以解决cfs访问过深问题
    '''
    xaxis=[]
    yaxis=[]
    visited =set()
    q = Queue()
    q.put((x_fd, y_fd))
    visited.add((x_fd, y_fd))
    offsets=[(1, 0), (0, 1), (-1, 0), (0, -1)]#四邻域

    while not q.empty():
        x,y=q.get()
        for xoffset,yoffset in offsets:
            x_neighbor,y_neighbor = x+xoffset,y+yoffset

            if (x_neighbor,y_neighbor) in (visited):
                continue    # 已经访问过了

            visited.add((x_neighbor, y_neighbor))
            try:
                if im[x_neighbor, y_neighbor] == 0:
                    xaxis.append(x_neighbor)
                    yaxis.append(y_neighbor)
                    q.put((x_neighbor,y_neighbor))
            except IndexError:
                pass
    # print(xaxis)
    if (len(xaxis) == 0 | len(yaxis) == 0):
        xmax = x_fd + 1
        xmin = x_fd
        ymax = y_fd + 1
        ymin = y_fd
    else:
        xmax = max(xaxis)
        xmin = min(xaxis)
        ymax = max(yaxis)
        ymin = min(yaxis)
        #ymin,ymax=sort(yaxis)
    return ymax,ymin,xmax,xmin

def detectFgPix(im,xmax):
    '''搜索区块起点
    '''
    h,w = im.shape[:2]
    for y_fd in range(xmax+1,w):
        for x_fd in range(h):
            if im[x_fd,y_fd] == 0:
                return x_fd,y_fd

def CFS(im):
    '''切割字符位置
    '''
    zoneL=[]#各区块长度L列表
    zoneWB=[]#各区块的X轴[起始,终点]列表
    zoneHB=[]#各区块的Y轴[起始,终点]列表

    xmax=0#上一区块结束黑点横坐标,这里是初始化
    for i in range(10):
        try:
            x_fd,y_fd = detectFgPix(im,xmax)
            # print(y_fd,x_fd)
            xmax,xmin,ymax,ymin=cfs(im,x_fd,y_fd)
            L = xmax - xmin
            H = ymax - ymin
            zoneL.append(L)
            zoneWB.append([xmin,xmax])
            zoneHB.append([ymin,ymax])
        except TypeError:
            return zoneL,zoneWB,zoneHB
    return zoneL,zoneWB,zoneHB

def cutting_img(im,im_position,img,xoffset = 1,yoffset = 1):
    filename = './out_img/' + img.split('.')[0]
    # 识别出的字符个数
    im_number = len(im_position[1])
    # 切割字符
    for i in range(im_number):
        im_start_X = im_position[1][i][0] - xoffset
        im_end_X = im_position[1][i][1] + xoffset
        im_start_Y = im_position[2][i][0] - yoffset
        im_end_Y = im_position[2][i][1] + yoffset
        cropped = im[im_start_Y:im_end_Y, im_start_X:im_end_X]
        cv2.imwrite(filename + '-cutting-' + str(i) + '.jpg',cropped)

def main():
    filedir = './captcha'
    for file in os.listdir(filedir):
        if fnmatch(file, '*.jpg'):
            img_name = file
            # 自适应阈值二值化
            im = _get_dynamic_binary_image(filedir, img_name)
            # 去除边框
            im = clear_border(im,img_name)
            # 对图片进行干扰线降噪
            im = interference_line(im,img_name)
            # 对图片进行点降噪
            im = interference_point(im,img_name)
            # 切割的位置
            im_position = CFS(im)
            maxL = max(im_position[0])
            minL = min(im_position[0])

            # 如果有粘连字符,如果一个字符的长度过长就认为是粘连字符,并从中间进行切割
            if(maxL > minL + minL * 0.7):
                maxL_index = im_position[0].index(maxL)
                minL_index = im_position[0].index(minL)
                # 设置字符的宽度
                im_position[0][maxL_index] = maxL // 2
                im_position[0].insert(maxL_index + 1, maxL // 2)
                # 设置字符X轴[起始,终点]位置
                im_position[1][maxL_index][1] = im_position[1][maxL_index][0] + maxL // 2
                im_position[1].insert(maxL_index + 1, [im_position[1][maxL_index][1] + 1, im_position[1][maxL_index][1] + 1 + maxL // 2])
                # 设置字符的Y轴[起始,终点]位置
                im_position[2].insert(maxL_index + 1, im_position[2][maxL_index])

            # 切割字符,要想切得好就得配置参数,通常 1 or 2 就可以
            cutting_img(im,im_position,img_name,1,1)
            # 识别验证码
            cutting_img_num = 0
            for file in os.listdir('./out_img'):
                str_img = ''
                if fnmatch(file, '%s-cutting-*.jpg' % img_name.split('.')[0]):
                    cutting_img_num += 1
            for i in range(cutting_img_num):
                try:
                    file = './out_img/%s-cutting-%s.jpg' % (img_name.split('.')[0], i)
                    # 识别验证码
                    str_img = str_img + image_to_string(Image.open(file),lang = 'eng', config='-psm 10') #单个字符是10,一行文本是7
                except Exception as err:
                    pass
            print('切图:%s' % cutting_img_num)
            print('识别为:%s' % str_img)

if __name__ == '__main__':
    main()
方便获取更多学习、工作、生活信息请关注本站微信公众号城东书院 微信服务号城东书院 微信订阅号
推荐内容
相关内容
栏目更新
栏目热门
本栏推荐