├── .gitattributes ├── .gitignore ├── captcha_cnn_model.py ├── captcha_setting.py ├── captcha_test.py ├── captcha_train.py ├── get_images.py ├── image_label.py ├── image_process.py ├── image_recog.py ├── image_resize.py ├── main.py ├── my_dataset.py ├── one_hot_encoding.py ├── readme.md └── requirements.txt /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | images 3 | dataset 4 | result.txt 5 | res.txt 6 | test.txt 7 | not.txt 8 | mobiles.txt 9 | captcha_predict.py 10 | captcha_gen.py 11 | docs/number2.png 12 | docs/number.png 13 | model/model.pkl 14 | model.pkl 15 | crack_phone.py 16 | get_phone.py 17 | -------------------------------------------------------------------------------- /captcha_cnn_model.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | import torch.nn as nn 3 | import captcha_setting 4 | 5 | # CNN Model (2 conv layer) 6 | class CNN(nn.Module): 7 | def __init__(self): 8 | super(CNN, self).__init__() 9 | self.layer1 = nn.Sequential( 10 | nn.Conv2d(1, 32, kernel_size=3, padding=1), 11 | nn.BatchNorm2d(32), 12 | nn.Dropout(0.5), # drop 50% of the neuron 13 | nn.ReLU(), 14 | nn.MaxPool2d(2)) 15 | self.layer2 = nn.Sequential( 16 | nn.Conv2d(32, 64, kernel_size=3, padding=1), 17 | nn.BatchNorm2d(64), 18 | nn.Dropout(0.5), # drop 50% of the neuron 19 | nn.ReLU(), 20 | nn.MaxPool2d(2)) 21 | self.layer3 = nn.Sequential( 22 | nn.Conv2d(64, 64, kernel_size=3, padding=1), 23 | nn.BatchNorm2d(64), 24 | nn.Dropout(0.5), # drop 50% of the neuron 25 | nn.ReLU(), 26 | nn.MaxPool2d(2)) 27 | self.fc = nn.Sequential( 28 | nn.Linear((captcha_setting.IMAGE_WIDTH//8)*(captcha_setting.IMAGE_HEIGHT//8)*64, 1024), 29 | nn.Dropout(0.5), # drop 50% of the neuron 30 | nn.ReLU()) 31 | self.rfc = nn.Sequential( 32 | nn.Linear(1024, captcha_setting.MAX_CAPTCHA*captcha_setting.ALL_CHAR_SET_LEN), 33 | ) 34 | 35 | def forward(self, x): 36 | out = self.layer1(x) 37 | out = self.layer2(out) 38 | out = self.layer3(out) 39 | out = out.view(out.size(0), -1) 40 | out = self.fc(out) 41 | out = self.rfc(out) 42 | return out 43 | 44 | -------------------------------------------------------------------------------- /captcha_setting.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | import os 3 | # 验证码中的字符 4 | # string.digits + string.ascii_uppercase 5 | NUMBER = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'] 6 | ALPHABET = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'] 7 | 8 | ALL_CHAR_SET = NUMBER + ALPHABET 9 | ALL_CHAR_SET_LEN = len(ALL_CHAR_SET) 10 | MAX_CAPTCHA = 4 11 | 12 | # 网络设置 13 | EPOCHS = 30 14 | BATCH_SIZE = 10 15 | 16 | # 图像大小 17 | IMAGE_HEIGHT = 55 18 | IMAGE_WIDTH = 40 19 | 20 | DATA_PATH = 'images/data/' 21 | SVAE_PATH = 'images/test/' 22 | TRAIN_PATH = 'images/crop/' 23 | TEMP_PATH = 'images/temp/' 24 | TEST_PATH = 'images/test/' -------------------------------------------------------------------------------- /captcha_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | import numpy as np 3 | import torch 4 | from torch.autograd import Variable 5 | import captcha_setting 6 | import my_dataset 7 | from captcha_cnn_model import CNN 8 | import one_hot_encoding 9 | 10 | def main(): 11 | cnn = CNN() 12 | cnn.eval() 13 | cnn.load_state_dict(torch.load('model.pkl')) 14 | print("load cnn net.") 15 | 16 | test_dataloader = my_dataset.get_test_data_loader() 17 | 18 | correct = 0 19 | total = 0 20 | wrong_letter = [] 21 | total_letter = {} 22 | for i in captcha_setting.ALPHABET: 23 | total_letter[i] = { 24 | 'times': 0, 25 | 'success': 0, 26 | 'fail': 0 27 | } 28 | for i in captcha_setting.NUMBER: 29 | total_letter[i] = { 30 | 'times': 0, 31 | 'success': 0, 32 | 'fail': 0 33 | } 34 | for i, (images, labels) in enumerate(test_dataloader): 35 | image = images 36 | vimage = Variable(image) 37 | predict_label = cnn(vimage) 38 | 39 | c = captcha_setting.ALL_CHAR_SET[np.argmax(predict_label[0, 0:captcha_setting.ALL_CHAR_SET_LEN].data.numpy())] 40 | 41 | predict_label = '%s' % (c) 42 | print('predict_label: ' + predict_label) 43 | true_label = one_hot_encoding.decode(labels.numpy()[0]) 44 | total += labels.size(0) 45 | print('true_label: ' + true_label) 46 | total_letter[true_label]['times'] += 1 47 | if(predict_label == true_label): 48 | total_letter[true_label]['success'] += 1 49 | correct += 1 50 | else: 51 | total_letter[true_label]['fail'] += 1 52 | wrong_letter.append(true_label) 53 | if(total%200==0): 54 | print('Test Accuracy of the model on the %d test images: %f %%' % (total, 100 * correct / total)) 55 | print('Test Accuracy of the model on the %d test images: %f %%' % (total, 100 * correct / total)) 56 | print(wrong_letter) 57 | for i in total_letter: 58 | print('%s, success: %d fail: %d' % (i, total_letter[i]['success'], total_letter[i]['fail'])) 59 | # print(total_letter) 60 | 61 | if __name__ == '__main__': 62 | main() 63 | 64 | 65 | -------------------------------------------------------------------------------- /captcha_train.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | import torch 3 | import torch.nn as nn 4 | from torch.autograd import Variable 5 | import my_dataset 6 | import captcha_setting 7 | from captcha_cnn_model import CNN 8 | 9 | # Hyper Parameters 10 | num_epochs = captcha_setting.EPOCHS 11 | batch_size = captcha_setting.BATCH_SIZE 12 | learning_rate = 0.001 13 | 14 | def main(): 15 | cnn = CNN() 16 | cnn.train() 17 | print('init net') 18 | criterion = nn.MultiLabelSoftMarginLoss() 19 | optimizer = torch.optim.Adam(cnn.parameters(), lr=learning_rate) 20 | 21 | # Train the Model 22 | train_dataloader = my_dataset.get_train_data_loader() 23 | for epoch in range(num_epochs): 24 | for i, (images, labels) in enumerate(train_dataloader): 25 | images = Variable(images) 26 | labels = Variable(labels.float()) 27 | predict_labels = cnn(images) 28 | # print(predict_labels) 29 | # print(labels) 30 | loss = criterion(predict_labels, labels) 31 | optimizer.zero_grad() 32 | loss.backward() 33 | optimizer.step() 34 | if (i+1) % 10 == 0: 35 | print("epoch:", epoch, "step:", i, "loss:", loss.item()) 36 | if (i+1) % 100 == 0: 37 | torch.save(cnn.state_dict(), "./model.pkl") #current is model.pkl 38 | print("save model") 39 | print("epoch:", epoch, "step:", i, "loss:", loss.item()) 40 | torch.save(cnn.state_dict(), "./model.pkl") #current is model.pkl 41 | print("save last model") 42 | 43 | if __name__ == '__main__': 44 | main() 45 | 46 | 47 | -------------------------------------------------------------------------------- /get_images.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | import base64 4 | import time 5 | import os 6 | from PIL import Image 7 | import image_label 8 | import uuid 9 | import captcha_setting 10 | 11 | def get_9f_captcha (i, uuid): 12 | res = requests.post( 13 | 'https://wx.9fbank.com/apiUrl/api/security/picVerifyUUidH5?time=1558436489355', 14 | data={'uuid': uuid} 15 | ) # 9fu验证码接口 16 | # print(res.text) 17 | data = json.loads(res.text)['data'] 18 | img = base64.b64decode(data['verifyCode']) 19 | now = str(int(time.time())) 20 | if not os.path.exists(captcha_setting.DATA_PATH): 21 | os.makedirs(captcha_setting.DATA_PATH) 22 | img_path = captcha_setting.DATA_PATH + str(i) + '_' + now + '.png' 23 | file = open(img_path, 'wb') 24 | file.write(img) 25 | file.close() 26 | return img_path 27 | -------------------------------------------------------------------------------- /image_label.py: -------------------------------------------------------------------------------- 1 | import os 2 | from PIL import Image 3 | import time 4 | import image_process 5 | import captcha_setting 6 | 7 | def captcha_label (is_crop, is_recog): 8 | image_file_paths = [os.path.join(captcha_setting.DATA_PATH, image_file) for image_file in os.listdir(captcha_setting.DATA_PATH)] 9 | for image in image_file_paths: 10 | img_captcha_label(image, is_crop, is_recog) 11 | os.remove(image) 12 | 13 | def img_captcha_label (image, is_crop, is_recog): 14 | im = Image.open(image) 15 | text = '0123' 16 | if (is_recog): 17 | im.show() 18 | text = input("请输入验证码中的字符:") 19 | text = text.upper() 20 | suffix = str(int(time.time() * 1e3)) 21 | if (im.size != (160,60)): 22 | im = im.resize((160, 60)) 23 | if not os.path.exists(captcha_setting.TEMP_PATH): 24 | os.makedirs(captcha_setting.TEMP_PATH) 25 | im.save(captcha_setting.TEMP_PATH + text + "_" + suffix + ".png") 26 | if not os.path.exists(captcha_setting.SVAE_PATH): 27 | os.makedirs(captcha_setting.SVAE_PATH) 28 | # image_process.delete_line(im, image, SVAE_PATH) 29 | return image_process.capt_process(im, text, is_crop) 30 | # os.remove(image) 31 | -------------------------------------------------------------------------------- /image_process.py: -------------------------------------------------------------------------------- 1 | import os 2 | from PIL import Image 3 | import time 4 | import captcha_setting 5 | 6 | THRESHOLD = 136 7 | LUT = [0]*THRESHOLD + [1]*(256 - THRESHOLD) 8 | 9 | def capt_process(capt, lable, is_crop): 10 | """ 11 | 图像预处理:将验证码图片转为二值型图片,按字符切割 12 | 13 | :require Image: from PIL import Image 14 | :require LUT: A lookup table, 包含256个值 15 | 16 | :param capt: 验证码Image对象 17 | :return capt_per_char_list: 一个数组包含四个元素,每个元素是一张包含单个字符的二值型图片 18 | """ 19 | capt_gray = capt.convert("L") 20 | capt_bw = capt_gray.point(LUT, "1") 21 | capt_per_char_list = [] 22 | for i in range(4): 23 | x = i * 40 24 | y = 5 25 | capt_per_char = capt_bw.crop((x, y, x + 40, y + 55)) 26 | capt_per_char_list.append(capt_per_char) 27 | # print(capt_per_char_list) 28 | suffix = str(int(time.time() * 1e3)) 29 | img_path = captcha_setting.SVAE_PATH + lable + '_' + suffix + '.png' 30 | # capt_bw.save(img_path) 31 | img = delete_line(capt_bw) 32 | if(is_crop): 33 | return crop(img, lable) 34 | else: 35 | img.save(img_path) 36 | return img_path 37 | 38 | def crop(capt_bw, lable): 39 | capt_per_char_list = [] 40 | img_path_list = [] 41 | for i in range(4): 42 | x = i * 40 43 | y = 5 44 | capt_per_char = capt_bw.crop((x, y, x + 40, y + 55)) 45 | capt_per_char_list.append(capt_per_char) 46 | suffix = str(int(time.time() * 1e3)) 47 | capt_per_char_list[i].save(captcha_setting.SVAE_PATH + lable[i] + '_' + suffix + '.png') 48 | img_path_list.append(captcha_setting.SVAE_PATH + lable[i] + '_' + suffix + '.png') 49 | return img_path_list 50 | 51 | def process (): 52 | image_file_paths = [os.path.join(captcha_setting.DATA_PATH, image_file) for image_file in os.listdir(captcha_setting.DATA_PATH)] 53 | for image in image_file_paths: 54 | lable = image.split('/')[2].split('_')[0] 55 | im = Image.open(image) 56 | delete_line(im, image, captcha_setting.SVAE_PATH) 57 | capt_process(im, captcha_setting.SVAE_PATH, lable) 58 | os.remove(image) 59 | 60 | def delete_line(image): 61 | data = image 62 | (w,h) = data.size 63 | #data.getpixel((x,y))获取目标像素点颜色。 64 | #data.putpixel((x,y),255)更改像素点颜色,255代表颜色。 65 | for x in range(0, w): 66 | for y in range(0, h): 67 | # data.putpixel((10,10),0) 68 | # data.putpixel((42, 35),0) 69 | 70 | if x <= 18 and y >= 41: 71 | data.putpixel((x,y),255) 72 | if x in range(28, 35) and y in range(39, 42): 73 | data.putpixel((x,y),255) 74 | if x in range(42, 117) and y in range(36, 39): 75 | data.putpixel((x,y),255) 76 | if x in range(117, 131) and y in range(39, 42): 77 | data.putpixel((x,y),255) 78 | if x in range(123, 142) and y in range(42, 45): 79 | data.putpixel((x,y),255) 80 | if x >= 142 and y >= 41: 81 | data.putpixel((x,y),255) 82 | # data.save(img_path, "png") 83 | return data 84 | 85 | # process('images/data/') -------------------------------------------------------------------------------- /image_recog.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | import numpy as np 3 | import torch 4 | import os 5 | from torch.autograd import Variable 6 | import captcha_setting 7 | from PIL import Image 8 | import one_hot_encoding as ohe 9 | from torch.utils.data import DataLoader,Dataset 10 | import torchvision.transforms as transforms 11 | from captcha_cnn_model import CNN 12 | import image_label 13 | 14 | class mydataset(Dataset): 15 | def __init__(self, path, transform=None): 16 | self.train_image_file_path = path 17 | self.transform = transform 18 | 19 | def __len__(self): 20 | return 1 21 | 22 | def __getitem__(self, idx): 23 | image_root = self.train_image_file_path 24 | image_name = image_root.split(os.path.sep)[-1] 25 | image = Image.open(image_root) 26 | if self.transform is not None: 27 | image = self.transform(image) 28 | label = ohe.encode(image_name.split('_')[0]) # 为了方便,在生成图片的时候,图片文件的命名格式 "4个数字或者数字_时间戳.PNG", 4个字母或者即是图片的验证码的值,字母大写,同时对该值做 one-hot 处理 29 | return image, label 30 | 31 | transform = transforms.Compose([ 32 | # transforms.ColorJitter(), 33 | transforms.Grayscale(), 34 | transforms.ToTensor(), 35 | # transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) 36 | ]) 37 | 38 | def get_recog_data_loader(image_path): 39 | dataset = mydataset(image_path, transform=transform) 40 | return DataLoader(dataset, batch_size=1, shuffle=True) 41 | 42 | def cnn_recog(image_path): 43 | cnn = CNN() 44 | cnn.eval() 45 | cnn.load_state_dict(torch.load('./model.pkl')) 46 | print("load cnn net.") 47 | 48 | recog_dataloader = get_recog_data_loader(image_path) 49 | print(image_path) 50 | #vis = Visdom() 51 | for i, (images, labels) in enumerate(recog_dataloader): 52 | image = images 53 | vimage = Variable(image) 54 | predict_label = cnn(vimage) 55 | 56 | c = captcha_setting.ALL_CHAR_SET[np.argmax(predict_label[0, 0:captcha_setting.ALL_CHAR_SET_LEN].data.numpy())] 57 | 58 | c = '%s' % (c) 59 | return c 60 | 61 | DATA_PATH = 'dataset/test/' 62 | SVAE_PATH = 'dataset/crop/' 63 | TEMP_PATH = 'dataset/temp/'å 64 | 65 | def recog(img_path): 66 | recog_lable = ['*', '*', '*', '*'] 67 | img_path_list = image_label.img_captcha_label(img_path, True, False) 68 | for crop_image in img_path_list: 69 | c = cnn_recog(crop_image) 70 | i = int(crop_image.split('/')[2].split('_')[0]) 71 | print('%s' % (c)) 72 | recog_lable[i] = c 73 | os.remove(crop_image) 74 | return ''.join(recog_lable) 75 | 76 | # print(recog('dataset/test/97V5_1559106051034.png')) 77 | 78 | -------------------------------------------------------------------------------- /image_resize.py: -------------------------------------------------------------------------------- 1 | import os 2 | from PIL import Image 3 | 4 | def resize (DATA_PATH): 5 | image_file_paths = [os.path.join(DATA_PATH, image_file) for image_file in os.listdir(DATA_PATH)] 6 | for image in image_file_paths: 7 | im = Image.open(image) 8 | if (im.size != (160,60)): 9 | im = im.resize((160, 60)) 10 | im.save(image) -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from PIL import Image 2 | import image_label 3 | import uuid 4 | import get_images 5 | import captcha_setting 6 | 7 | count = 100 8 | 9 | for i in range(count): 10 | uid = uuid.uuid1() 11 | get_images.get_9f_captcha(i, uid) 12 | 13 | image_label.captcha_label(True, True) -------------------------------------------------------------------------------- /my_dataset.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | import os 3 | from torch.utils.data import DataLoader,Dataset 4 | import torchvision.transforms as transforms 5 | from PIL import Image 6 | import one_hot_encoding as ohe 7 | import captcha_setting 8 | 9 | class mydataset(Dataset): 10 | 11 | def __init__(self, folder, transform=None): 12 | self.train_image_file_paths = [os.path.join(folder, image_file) for image_file in os.listdir(folder)] 13 | self.transform = transform 14 | 15 | def __len__(self): 16 | return len(self.train_image_file_paths) 17 | 18 | def __getitem__(self, idx): 19 | image_root = self.train_image_file_paths[idx] 20 | image_name = image_root.split(os.path.sep)[-1] 21 | image = Image.open(image_root) 22 | if self.transform is not None: 23 | image = self.transform(image) 24 | label = ohe.encode(image_name.split('_')[0]) # 为了方便,在生成图片的时候,图片文件的命名格式 "4个数字或者数字_时间戳.PNG", 4个字母或者即是图片的验证码的值,字母大写,同时对该值做 one-hot 处理 25 | return image, label 26 | 27 | transform = transforms.Compose([ 28 | # transforms.ColorJitter(), 29 | transforms.Grayscale(), 30 | transforms.ToTensor(), 31 | # transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) 32 | ]) 33 | def get_train_data_loader(): 34 | dataset = mydataset(captcha_setting.TRAIN_PATH, transform=transform) 35 | return DataLoader(dataset, batch_size=captcha_setting.BATCH_SIZE, shuffle=True) 36 | 37 | def get_test_data_loader(): 38 | dataset = mydataset(captcha_setting.TEST_PATH, transform=transform) 39 | return DataLoader(dataset, batch_size=1, shuffle=True) -------------------------------------------------------------------------------- /one_hot_encoding.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | import numpy as np 3 | import captcha_setting 4 | 5 | 6 | def encode(text): 7 | vector = np.zeros(captcha_setting.ALL_CHAR_SET_LEN * captcha_setting.MAX_CAPTCHA, dtype=float) 8 | def char2pos(c): 9 | if c =='_': 10 | k = 62 11 | return k 12 | k = ord(c)-48 13 | if k > 9: 14 | k = ord(c) - 65 + 10 15 | if k > 35: 16 | k = ord(c) - 97 + 26 + 10 17 | if k > 61: 18 | raise ValueError('error') 19 | return k 20 | for i, c in enumerate(text): 21 | idx = i * captcha_setting.ALL_CHAR_SET_LEN + char2pos(c) 22 | vector[idx] = 1.0 23 | return vector 24 | 25 | def decode(vec): 26 | char_pos = vec.nonzero()[0] 27 | text=[] 28 | for i, c in enumerate(char_pos): 29 | char_at_pos = i #c/63 30 | char_idx = c % captcha_setting.ALL_CHAR_SET_LEN 31 | if char_idx < 10: 32 | char_code = char_idx + ord('0') 33 | elif char_idx <36: 34 | char_code = char_idx - 10 + ord('A') 35 | elif char_idx < 62: 36 | char_code = char_idx - 36 + ord('a') 37 | elif char_idx == 62: 38 | char_code = ord('_') 39 | else: 40 | raise ValueError('error') 41 | text.append(chr(char_code)) 42 | return "".join(text) 43 | 44 | if __name__ == '__main__': 45 | e = encode("BK7H") 46 | print(decode(e)) -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ### pytorch 基于CNN的验证码识别 2 | 3 | #### 安装依赖 4 | ```python 5 | pip3 install -r requirements.txt 6 | ``` 7 | 8 | #### 实现思路 9 | - 获取图片 10 | - 灰度二值化 11 | - 裁切成单个数字或字母 12 | - 基于CNN训练识别单个验证码的数据模型,单个验证码识别成功率99% 13 | 14 | #### 使用方法 15 | 16 | - 生成训练数据 17 | ```python 18 | python main.py 19 | ``` 20 | 21 | - 训练数据集 22 | ```python 23 | python captcha_train.py 24 | ``` 25 | 26 | - 测试数据集 27 | ```python 28 | python captcha_test.py 29 | ``` 30 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy == 1.16.2 2 | uuid == 1.30 3 | pytorch == 1.0.2 4 | Pillow == 5.4.1 --------------------------------------------------------------------------------