我们在网上浏览网页或注册账号时,会经常遇到验证码(CAPTCHA),如下图:
本文将具体介绍如何利用Python的图像处理模块pillow和OCR模块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()
-