├── .gitignore ├── CONFIG.py ├── MAIN.py ├── datasets.py ├── mbbank_scrapping.py ├── model.pth ├── requirements.txt └── server.py /.gitignore: -------------------------------------------------------------------------------- 1 | /__pycache__ 2 | /mbbankenv 3 | /mbbank_dataset 4 | /temp.png -------------------------------------------------------------------------------- /CONFIG.py: -------------------------------------------------------------------------------- 1 | #coding:utf8 2 | import os 3 | import string 4 | 5 | 6 | source = list('123456789ABDEFGHJMNRTYabdefghjmnqrty') 7 | 8 | class LettersInt: 9 | content_range = ''.join(source) 10 | range_len = len(content_range) 11 | IMG_WIDTH = 200 12 | IMG_HEIGHT = 35 13 | PIC_NAME_LEN = 6 14 | model_path = os.path.dirname(os.path.realpath(__file__)) + '/model.pth' 15 | -------------------------------------------------------------------------------- /MAIN.py: -------------------------------------------------------------------------------- 1 | #coding:utf8 2 | import shortuuid 3 | import torch 4 | import torch.nn as nn 5 | from torch.autograd import Variable 6 | try: 7 | from .datasets import CaptchaData,CaptchaDataOne 8 | from .CONFIG import LettersInt 9 | except: 10 | from datasets import CaptchaData,CaptchaDataOne 11 | from CONFIG import LettersInt 12 | from torch.utils.data import DataLoader 13 | from torchvision.transforms import Compose, ToTensor 14 | import time 15 | from PIL import Image 16 | import os, requests 17 | import base64 18 | from io import BytesIO 19 | 20 | 21 | class CNN(nn.Module): 22 | def __init__(self, range_len=LettersInt.range_len, pic_name_len=LettersInt.PIC_NAME_LEN): 23 | super(CNN, self).__init__() 24 | self.range_len = range_len 25 | self.pic_name_len = pic_name_len 26 | 27 | IMG_WIDTH = LettersInt.IMG_WIDTH 28 | IMG_HEIGHT = LettersInt.IMG_HEIGHT 29 | 30 | IMG = 3 # RGB 31 | input1 = 3 32 | out1 = 16 33 | maxpool2d1 = 2 34 | IMG_HEIGHT = int(IMG_HEIGHT / maxpool2d1) 35 | IMG_WIDTH = int(IMG_WIDTH / maxpool2d1) 36 | input2 = 16 37 | out2 = 64 38 | maxpool2d2 = 2 39 | IMG_HEIGHT = int(IMG_HEIGHT / maxpool2d2) 40 | IMG_WIDTH = int(IMG_WIDTH / maxpool2d2) 41 | input3 = 64 42 | out3 = 512 43 | maxpool2d3 = 2 44 | IMG_HEIGHT = int(IMG_HEIGHT / maxpool2d3) 45 | IMG_WIDTH = int(IMG_WIDTH / maxpool2d3) 46 | 47 | self.linear = out3 * IMG_WIDTH * IMG_HEIGHT 48 | self.conv = nn.Sequential( 49 | # batch*3*180*100 # IMG_WIDTH,IMG_HEIGHT=100,30 50 | nn.Conv2d(input1, out1, IMG, padding=(1, 1)), 51 | # 参数分别对应着输入的通道数3,输出通道数16,卷积核大小为3(长宽都为3)adding为(1, 1)可以保证输入输出的长宽不变 52 | nn.MaxPool2d(maxpool2d1, maxpool2d1), 53 | nn.BatchNorm2d(out1), 54 | nn.ReLU(), 55 | # batch*16*90*50 56 | nn.Conv2d(input2, out2, IMG, padding=(1, 1)), 57 | nn.MaxPool2d(maxpool2d2, maxpool2d2), 58 | nn.BatchNorm2d(out2), 59 | nn.ReLU(), 60 | # batch*64*45*25 61 | nn.Conv2d(input3, out3, IMG, padding=(1, 1)), 62 | nn.MaxPool2d(maxpool2d3, maxpool2d3), 63 | nn.BatchNorm2d(out3), 64 | nn.ReLU(), 65 | # batch*512*22*12 66 | # nn.Conv2d(input4, out4, IMG, padding=(1, 1)), 67 | # nn.MaxPool2d(maxpool2d4, maxpool2d4), 68 | # nn.BatchNorm2d(out4), 69 | # nn.ReLU(), 70 | # batch*512*11*6 71 | ) 72 | self.fc = nn.Linear(self.linear, self.range_len * self.pic_name_len) 73 | 74 | def forward(self, x): 75 | x = self.conv(x) 76 | x = x.view(-1, self.linear) 77 | x = self.fc(x) 78 | return x 79 | 80 | class CrackLettesInt4: 81 | 82 | def __init__(self): 83 | self.batch_size = 1 84 | self.base_lr = 0.001 85 | self.max_epoch = 200 86 | self.model_path = LettersInt.model_path 87 | self.restor = False 88 | self.range_len = LettersInt.range_len 89 | 90 | def calculat_acc(self,output, target): 91 | output, target = output.view(-1, LettersInt.range_len), target.view(-1, LettersInt.range_len) 92 | output = nn.functional.softmax(output, dim=1) 93 | output = torch.argmax(output, dim=1) 94 | target = torch.argmax(target, dim=1) 95 | output, target = output.view(-1, LettersInt.PIC_NAME_LEN), target.view(-1, LettersInt.PIC_NAME_LEN) 96 | correct_list = [] 97 | for i, j in zip(target, output): 98 | if torch.equal(i, j): 99 | correct_list.append(1) 100 | else: 101 | correct_list.append(0) 102 | acc = sum(correct_list) / len(correct_list) 103 | return acc 104 | 105 | def train(self,train_folder): 106 | transforms = Compose([ToTensor()]) 107 | train_dataset = CaptchaData(train_folder, transform=transforms) 108 | train_data_loader = DataLoader(train_dataset, batch_size=self.batch_size, num_workers=0, shuffle=True, drop_last=True) 109 | cnn = CNN() 110 | if torch.cuda.is_available(): 111 | cnn.cuda() 112 | # if self.restor: 113 | # cnn.load_state_dict(torch.load(LettersInt.model_path)) 114 | # freezing_layers = list(cnn.named_parameters())[:10] 115 | # for param in freezing_layers: 116 | # param[1].requires_grad = False 117 | # print('freezing layer:', param[0]) 118 | 119 | optimizer = torch.optim.Adam(cnn.parameters(), lr=self.base_lr) 120 | criterion = nn.MultiLabelSoftMarginLoss() 121 | 122 | for epoch in range(self.max_epoch): 123 | start_ = time.time() 124 | loss_history = [] 125 | acc_history = [] 126 | cnn.train() 127 | for img, target in train_data_loader: 128 | img = Variable(img) 129 | target = Variable(target) 130 | if torch.cuda.is_available(): 131 | img = img.cuda() 132 | target = target.cuda() 133 | output = cnn(img) 134 | loss = criterion(output, target) 135 | optimizer.zero_grad() 136 | loss.backward() 137 | optimizer.step() 138 | acc = self.calculat_acc(output, target) 139 | acc_history.append(acc) 140 | loss_history.append(loss) 141 | print('train_loss: {:.4}|train_acc: {:.4}'.format( 142 | torch.mean(torch.Tensor(loss_history)), 143 | torch.mean(torch.Tensor(acc_history)), 144 | )) 145 | cnn.eval() 146 | print('epoch: {}|time: {:.4f}'.format(epoch, time.time() - start_)) 147 | torch.save(cnn.state_dict(), LettersInt.model_path) 148 | 149 | 150 | def predict_one(self,image_path): 151 | img = Image.open(image_path) 152 | img = img.convert('RGB') 153 | transforms = Compose([ToTensor()]) 154 | img = transforms(img) 155 | cnn = CNN() 156 | if torch.cuda.is_available(): 157 | cnn = cnn.cuda() 158 | cnn.eval() 159 | if torch.cuda.is_available(): 160 | cnn.load_state_dict(torch.load(LettersInt.model_path)) 161 | else: 162 | cnn.load_state_dict(torch.load(LettersInt.model_path,map_location=torch.device('cpu'))) 163 | try: 164 | img = img.view(1, 3, LettersInt.IMG_HEIGHT, LettersInt.IMG_WIDTH).cuda() 165 | except: 166 | img = img.view(1,3,LettersInt.IMG_HEIGHT, LettersInt.IMG_WIDTH) 167 | output = cnn(img) 168 | output = output.view(-1, self.range_len) 169 | output = nn.functional.softmax(output, dim=1) 170 | output = torch.argmax(output, dim=1) 171 | output = output.view(-1, LettersInt.PIC_NAME_LEN)[0] 172 | pred = ''.join([LettersInt.content_range[i] for i in output.cpu().numpy()]) 173 | print('result',pred) 174 | os.system('mv %s ./test/%s.jpg'%(image_path, pred)) 175 | return pred 176 | 177 | @classmethod 178 | def down_img(cls,url,c): 179 | for i in range(c): 180 | res = requests.get(url).content 181 | g = open('./data/test/%s.jpg' % int(time.time()*1000),'wb') 182 | g.write(res) 183 | g.close() 184 | print(i) 185 | 186 | @classmethod 187 | def check_name(cls): 188 | pp = os.path.dirname(os.path.abspath(__file__)) 189 | for d in os.listdir('./data/train'): 190 | if len(d) != 8: 191 | print(d) 192 | os.remove(pp+'\\data\\train\\'+d) 193 | 194 | def check_all(self,folder): 195 | for d in os.listdir(folder): 196 | f = './data/test/%s' % d 197 | self.predict_one(f) 198 | 199 | 200 | def getCaptcha(): 201 | url = 'https://ebanking.baca-bank.vn/IBSRetail/servlet/ImageServlet' 202 | headers = {} 203 | response = requests.get(url, headers=headers, timeout=15) 204 | return base64.b64encode(response.content).decode('utf-8') 205 | 206 | 207 | def img_base4_save(img_base64, savePath): 208 | ''' 209 | Base64图片保存本地 210 | ''' 211 | # if not img_base64.startswith('data:image/png;base64,'): 212 | # return 213 | 214 | # 去除前缀信息(如'data:image/png;base64,') 215 | if img_base64.startswith('data:'): 216 | _, data = img_base64.split(',', maxsplit=1) 217 | else: 218 | data = img_base64 219 | 220 | # 解码Base64数据 221 | decoded_data = base64.b64decode(data) 222 | # 创建BytesIO对象来读取二进制数据 223 | buffered_io = BytesIO() 224 | buffered_io.write(decoded_data) 225 | buffered_io.seek(0) 226 | # 打开图像文件 227 | img = Image.open(buffered_io) 228 | # 指定保存路径及名称 229 | # 保存图像到本地 230 | img.save(savePath) 231 | return True 232 | 233 | 234 | def ttt(): 235 | from PIL import Image 236 | # 打开图片 237 | img = Image.open('1709659296.jpg') 238 | 239 | # 获取图片的宽度和高度 240 | width, height = img.size 241 | 242 | # 计算每个子图的尺寸 243 | sub_width = width // 4 244 | 245 | # 分割图片 246 | for j in range(4): 247 | if j == 4: 248 | box = (j * sub_width, 0, (j + 1) * sub_width+3, height) 249 | elif j == 5: 250 | box = (j * sub_width+3, 0, (j + 1) * sub_width, height) 251 | else: 252 | box = (j * sub_width, 0, (j + 1) * sub_width, height) 253 | sub_img = img.crop(box) 254 | sub_img.save(f'./t/sub_img_{j}.jpg') 255 | 256 | 257 | 258 | def getImg(): 259 | for i in range(20): 260 | print(i) 261 | url = 'https://online.vietbank.com.vn/ibk/vn/login/capcha.jsp' 262 | headers = {} 263 | response = requests.get(url, headers=headers, timeout=20) 264 | with open('./data/test/' + shortuuid.uuid() + '.jpg', 'wb') as wf: 265 | wf.write(response.content) 266 | 267 | 268 | 269 | if __name__ == "__main__": 270 | # url = 'https://erp.hupun.com/service/captcha/generate?time=1638451991727&refresh=true' 271 | # CrackLettesInt4.down_img(url,100) 272 | CrackLettesInt4().check_all('./data/test') 273 | # CrackLettesInt4().train('./data/train') 274 | # dd = getCaptcha() 275 | # getImg() 276 | 277 | # for d in os.listdir('./data/train'): 278 | # print('d:', d) 279 | # if d.startswith('fff'): 280 | # os.remove('./data/train/'+d) 281 | 282 | -------------------------------------------------------------------------------- /datasets.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | from PIL import Image 4 | import torch 5 | from torch.utils.data import Dataset 6 | try: 7 | from .CONFIG import LettersInt 8 | except: 9 | from CONFIG import LettersInt 10 | 11 | content_range = LettersInt.content_range 12 | 13 | def img_loader(img_path): 14 | img = Image.open(img_path) 15 | return img.convert('RGB') 16 | 17 | def make_dataset(data_path, content_range, range_len, pic_name_len): 18 | img_names = os.listdir(data_path) 19 | samples = [] 20 | for img_name in img_names: 21 | img_path = os.path.join(data_path, img_name) 22 | target_str = img_name.split('/')[-1].split('.')[0] 23 | assert len(target_str) == pic_name_len 24 | target = [] 25 | for char in target_str: 26 | vec = [0] * range_len 27 | vec[content_range.find(char)] = 1 28 | target += vec 29 | samples.append((img_path, target)) 30 | return samples 31 | 32 | class CaptchaData(Dataset): 33 | 34 | def __init__(self, data_path, range_len=LettersInt.range_len, pic_name_len=LettersInt.PIC_NAME_LEN, 35 | transform=None, target_transform=None, content_range=content_range): 36 | super(Dataset, self).__init__() 37 | self.data_path = data_path 38 | self.range_len = range_len 39 | self.pic_name_len = pic_name_len 40 | self.transform = transform 41 | self.target_transform = target_transform 42 | self.content_range = content_range 43 | self.samples = make_dataset(self.data_path, self.content_range, 44 | self.range_len, self.pic_name_len) 45 | 46 | 47 | def __len__(self): 48 | return len(self.samples) 49 | 50 | def __getitem__(self, index): 51 | img_path, target = self.samples[index] 52 | img = img_loader(img_path) 53 | if self.transform is not None: 54 | img = self.transform(img) 55 | if self.target_transform is not None: 56 | target = self.target_transform(target) 57 | return img, torch.Tensor(target) 58 | 59 | 60 | class CaptchaDataOne(Dataset): 61 | 62 | def __init__(self, samples,transform=None): 63 | super(Dataset, self).__init__() 64 | self.transform = transform 65 | self.samples = samples 66 | 67 | def __len__(self): 68 | return len(self.samples) 69 | 70 | def __getitem__(self, index): 71 | img_path = self.samples[index] 72 | img = Image.open(img_path) 73 | img = img.convert('RGB') 74 | if self.transform is not None: 75 | img = self.transform(img) 76 | return img 77 | -------------------------------------------------------------------------------- /mbbank_scrapping.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import base64 3 | import os 4 | from PIL import Image 5 | from io import BytesIO 6 | from anticaptchaofficial.imagecaptcha import imagecaptcha 7 | 8 | # URL and body for the POST request 9 | url = "https://ebank.mbbank.com.vn/corp/common/generateCaptcha" 10 | body = { 11 | "refNo": "2024062400251990", 12 | "deviceId": "737a264c94e159fb79858bf979db3ad2" 13 | } 14 | 15 | # Create the directory to save images if it doesn't exist 16 | save_directory = "scrapped_images" 17 | if not os.path.exists(save_directory): 18 | os.makedirs(save_directory) 19 | 20 | # Initialize the captcha solver 21 | solver = imagecaptcha() 22 | solver.set_verbose(1) 23 | solver.set_key("f3a44e66302c61ffec07c80f4732baf3") 24 | 25 | # Generate 5 images and solve the captchas 26 | for i in range(10): 27 | response = requests.post(url, json=body) 28 | 29 | # Check if the request was successful 30 | if response.status_code == 200: 31 | # Decode the Base64 encoded image from the response 32 | image_base64 = response.json().get("imageBase64") 33 | image_data = base64.b64decode(image_base64) 34 | 35 | # Create an image from the decoded bytes 36 | image = Image.open(BytesIO(image_data)) 37 | 38 | # Save the image temporarily to solve the captcha 39 | temp_image_path = os.path.join(save_directory, "temp_image2.png") 40 | image.save(temp_image_path) 41 | 42 | # Solve the captcha 43 | captcha_text = solver.solve_and_return_solution(temp_image_path) 44 | if captcha_text != 0: 45 | # Save the image with the captcha text as the filename 46 | final_image_path = os.path.join(save_directory, f"{captcha_text}.png") 47 | image.save(final_image_path) 48 | print(f"Image {i+1} saved as {final_image_path} with text '{captcha_text}'") 49 | else: 50 | print(f"Failed to solve captcha for image {i+1}. Error: {solver.error_code}") 51 | else: 52 | print(f"Failed to retrieve captcha image. Status code: {response.status_code}") 53 | -------------------------------------------------------------------------------- /model.pth: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aimaster-dev/anti-ocr-recaptcha-model/b29f65418f5e79382d4b676dc470544a3d220543/model.pth -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | certifi==2024.6.2 2 | charset-normalizer==3.3.2 3 | filelock==3.15.4 4 | fsspec==2024.6.0 5 | idna==3.7 6 | intel-openmp==2021.4.0 7 | Jinja2==3.1.4 8 | MarkupSafe==2.1.5 9 | mkl==2021.4.0 10 | mpmath==1.3.0 11 | networkx==3.2.1 12 | numpy==1.22.0 13 | pillow==10.3.0 14 | requests==2.32.3 15 | shortuuid==1.0.13 16 | sympy==1.12.1 17 | tbb==2021.13.0 18 | torch==2.3.1 19 | torchvision==0.18.1 20 | typing_extensions==4.12.2 21 | urllib3==2.2.2 22 | -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | import shortuuid 2 | import torch 3 | import torch.nn as nn 4 | from torch.autograd import Variable 5 | try: 6 | from .datasets import CaptchaData,CaptchaDataOne 7 | from .CONFIG import LettersInt 8 | except: 9 | from datasets import CaptchaData,CaptchaDataOne 10 | from CONFIG import LettersInt 11 | from torch.utils.data import DataLoader 12 | from torchvision.transforms import Compose, ToTensor 13 | import time 14 | from PIL import Image 15 | import os, requests 16 | import base64 17 | from io import BytesIO 18 | from flask import Flask, request, jsonify 19 | from MAIN import CrackLettesInt4 20 | 21 | app = Flask(__name__) 22 | 23 | def decode_base64_image(base64_string): 24 | if base64_string.startswith("data:image/png;base64,"): 25 | base64_string = base64_string.replace("data:image/png;base64,", "") 26 | 27 | image_data = base64.b64decode(base64_string) 28 | image = Image.open(io.BytesIO(image_data)) 29 | image_path = 'temp.png' 30 | image.save(image_path) 31 | 32 | return image_path 33 | 34 | @app.route('/predict', methods=['POST']) 35 | def predict(): 36 | if 'image' in request.files: 37 | image_file = request.files['image'] 38 | image_path = 'temp.png' 39 | image_file.save(image_path) 40 | elif 'image_base64' in request.json: 41 | base64_string = request.json['image_base64'] 42 | image_path = decode_base64_image(base64_string) 43 | else: 44 | return jsonify({"error": "No image provided."}) 45 | 46 | try: 47 | pred_text = CrackLettesInt4().predict_one(image_path) 48 | return jsonify({'prediction': pred_text}) 49 | except Exception as e: 50 | return jsonify({'error': str(e)}) 51 | 52 | if __name__ == '__main__': 53 | app.run(host = "0.0.0.0", port = 8000, debug=False) --------------------------------------------------------------------------------