文章目录
我们在网上浏览网页或注册账号时,会经常遇到验证码(CAPTCHA),如下图:
本文将具体介绍如何利用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):# 获取灰度转二值的映射tabletable = []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 textdef 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加1correct_count += 1total_count += 1print('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,osdef 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] = 255cv2.imwrite(filename,img)return imgdef 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 = 0if img[x, y - 1] > 245:count = count + 1if img[x, y + 1] > 245:count = count + 1if img[x - 1, y] > 245:count = count + 1if img[x + 1, y] > 245:count = count + 1if count > 2:img[x, y] = 255cv2.imwrite(filename,img)return imgdef 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] = 0elif 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] = 0else: # 最上非顶点,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] = 0elif 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] = 0elif 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] = 0else: # 最下非顶点,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] = 0else: # 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] = 0elif 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] = 0else: # 具备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] = 0cv2.imwrite(filename,img)return imgdef _get_dynamic_binary_image(filedir, img_name):'''自适应阀值二值化'''filename = './out_img/' + img_name.split('.')[0] + '-binary.jpg'img_name = filedir + '/' + img_nameim = 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 th1def _get_static_binary_image(img, threshold = 140):'''手动二值化'''img = Image.open(img)img = img.convert('L')pixdata = img.load()w, h = img.sizefor y in range(h):for x in range(w):if pixdata[x, y] < threshold:pixdata[x, y] = 0else:pixdata[x, y] = 255return imgdef 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+yoffsetif (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 + 1xmin = x_fdymax = y_fd + 1ymin = y_fdelse:xmax = max(xaxis)xmin = min(xaxis)ymax = max(yaxis)ymin = min(yaxis)#ymin,ymax=sort(yaxis)return ymax,ymin,xmax,xmindef 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_fddef 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 - xminH = ymax - yminzoneL.append(L)zoneWB.append([xmin,xmax])zoneHB.append([ymin,ymax])except TypeError:return zoneL,zoneWB,zoneHBreturn zoneL,zoneWB,zoneHBdef 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] - xoffsetim_end_X = im_position[1][i][1] + xoffsetim_start_Y = im_position[2][i][0] - yoffsetim_end_Y = im_position[2][i][1] + yoffsetcropped = 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 // 2im_position[0].insert(maxL_index + 1, maxL // 2)# 设置字符X轴[起始,终点]位置im_position[1][maxL_index][1] = im_position[1][maxL_index][0] + maxL // 2im_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 = 0for file in os.listdir('./out_img'):str_img = ''if fnmatch(file, '%s-cutting-*.jpg' % img_name.split('.')[0]):cutting_img_num += 1for 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,一行文本是7except Exception as err:passprint('切图:%s' % cutting_img_num)print('识别为:%s' % str_img)if __name__ == '__main__':main()