├── .gitignore ├── LICENSE ├── Readme.md ├── matplotlibrc ├── package.json ├── requirements.txt └── src ├── checkpoint ├── checkpoint ├── eval │ └── events.out.tfevents.1552384935.bogon ├── events.out.tfevents.1552384633.bogon ├── graph.pbtxt ├── model.ckpt-1.data-00000-of-00001 ├── model.ckpt-1.index ├── model.ckpt-1.meta ├── model.ckpt-3000.data-00000-of-00001 ├── model.ckpt-3000.index └── model.ckpt-3000.meta ├── create_train_data.py ├── data ├── 9371.jpg ├── captcha.json ├── captcha.npz ├── captcha │ ├── 1651.jpg │ ├── 3601.jpg │ ├── 3771.jpg │ ├── 6172.jpg │ ├── 7104.jpg │ ├── 7134.jpg │ ├── 8113.jpg │ └── 8395.jpg ├── test.jpg └── train │ ├── 0 │ ├── 3-3601.png │ └── 3-7104.png │ ├── 1 │ ├── 1-1651.png │ ├── 2-6172.png │ ├── 2-7104.png │ ├── 2-7134.png │ ├── 2-8113.png │ ├── 3-8113.png │ ├── 4-1651.png │ ├── 4-3601.png │ └── 4-3771.png │ ├── 2 │ └── 4-6172.png │ ├── 3 │ ├── 1-3601.png │ ├── 1-3771.png │ ├── 2-8395.png │ ├── 3-7134.png │ └── 4-8113.png │ ├── 4 │ ├── 4-7104.png │ └── 4-7134.png │ ├── 5 │ ├── 3-1651.png │ └── 4-8395.png │ ├── 6 │ ├── 1-6172.png │ ├── 2-1651.png │ └── 2-3601.png │ ├── 7 │ ├── 1-7104.png │ ├── 1-7134.png │ ├── 2-3771.png │ ├── 3-3771.png │ └── 3-6172.png │ ├── 8 │ ├── 1-8113.png │ └── 1-8395.png │ └── 9 │ └── 3-8395.png ├── img.py ├── img_test.py ├── model.py ├── predict.js ├── predict.py └── train.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | 107 | # model log 108 | log 109 | .DS_Store 110 | 111 | # node.js 112 | node_modules/ 113 | 114 | .idea -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Do Minh Hai 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # tensorflow验证码识别 2 | 3 | 1. 样本数据 4 | 2. 创建训练数据 5 | 3. 跑模型,现在全是数字 6 | 4. 预测 7 | 8 | 9 | 10 | ## 样本数据 11 | 12 | 在`src/data/captcha`下存放验证码图片,一般名字就是答案,然后需要在`src/data/captcha.json`中描写对应关系,例如 13 | 14 | ```json 15 | { 16 | "3601.jpg": "3601", 17 | "1651.jpg": "1651", 18 | "3771.jpg": "3771", 19 | "6172.jpg": "6172", 20 | "7104.jpg": "7104", 21 | "7134.jpg": "7134", 22 | "8113.jpg": "8113", 23 | "8395.jpg": "8395" 24 | } 25 | ``` 26 | 27 | 前面是文件名,后面是答案 28 | 29 | 30 | 31 | ## 创建训练数据 32 | 33 | 运行文件`src/create_train_data.py`,这将会创建文件`src/data/captcha.npz`和和图片1~9的数字,数字在`src/data/train`,可以打开看看,切割效果不好的话需要修改,打开文件`src/img.py`,修改如下几个参数 34 | 35 | ```python 36 | SHIFT_PIXEL = 7 # 将图像从右向左移动 37 | BINARY_THRESH = 30 # 图像二进制阈值 38 | LETTER_SIZE = (20, 23) # 字母 宽, 高 39 | ``` 40 | 41 | 如果图片位置非常规则,就像这种 42 | 43 | ![](https://ws2.sinaimg.cn/large/006tKfTcly1g12h31aamwj302000y3ye.jpg) 44 | 45 | 只有4个数字,每个数字位置都确定不变,可以直接将位置写死,如 46 | 47 | ```python 48 | letter_boxs = [[[0, 7], [11, 24]], [[13, 5], [30, 30]], [[30, 5], [45, 29]], [[47, 4], [61, 28]]] 49 | ``` 50 | 51 | 上面的点分别就是下图中的1、2、3、4、5、6、7、8 52 | 53 | ![](https://ws1.sinaimg.cn/large/006tKfTcly1g12h5m9i80j30fc07rjs5.jpg) 54 | 55 | 56 | 57 | ## 跑模型 58 | 59 | 这个就比较简单了,直接运行`src/train.py`,会出现模型并保存在`src/checkpoint`目录下, 60 | 61 | 62 | 63 | ## 预测 64 | 65 | 运行`src/predict.py`,传入进去的需要是一个图片对象,当然你可以直接传入图片url,但是并不能维持session状态,因为它是直接去下载图片的,`io.imread(argv, as_gray=True)`的源码实现 66 | 67 | ```python 68 | 69 | @contextmanager 70 | def file_or_url_context(resource_name): 71 | """Yield name of file from the given resource (i.e. file or url).""" 72 | if is_url(resource_name): 73 | _, ext = os.path.splitext(resource_name) 74 | try: 75 | with tempfile.NamedTemporaryFile(delete=False, suffix=ext) as f: 76 | u = urlopen(resource_name) 77 | f.write(u.read()) 78 | # f must be closed before yielding 79 | yield f.name 80 | finally: 81 | os.remove(f.name) 82 | else: 83 | yield resource_name 84 | ``` 85 | 86 | 他这里就是先创建了一个临时文件,将图片写进去,再读取图片。如果需要维持session状态,也可以按照他这样,先创建一个临时文件,之后再删除。 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /matplotlibrc: -------------------------------------------------------------------------------- 1 | backend: TkAgg 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "captcha-breaker", 3 | "version": "1.0.0", 4 | "description": "Breaking Captcha with Tensorflow", 5 | "main": "src/predict.js", 6 | "dependencies": { 7 | "python-shell": "^0.5.0" 8 | }, 9 | "devDependencies": {}, 10 | "scripts": { 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/dominhhai/captcha-breaker.git" 16 | }, 17 | "author": "Do Minh Hai", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/dominhhai/captcha-breaker/issues" 21 | }, 22 | "homepage": "https://github.com/dominhhai/captcha-breaker#README" 23 | } 24 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | absl-py==0.2.2 2 | astor==0.6.2 3 | backports.functools-lru-cache==1.5 4 | backports.weakref==1.0.post1 5 | bleach==1.5.0 6 | cloudpickle==0.5.3 7 | cycler==0.10.0 8 | dask==0.18.0 9 | decorator==4.3.0 10 | enum34==1.1.6 11 | funcsigs==1.0.2 12 | futures==3.2.0 13 | gast==0.2.0 14 | grpcio==1.12.1 15 | html5lib==0.9999999 16 | kiwisolver==1.0.1 17 | Markdown==2.6.11 18 | matplotlib==2.2.2 19 | mock==2.0.0 20 | networkx==2.1 21 | numpy==1.14.5 22 | pbr==4.0.4 23 | Pillow==5.1.0 24 | protobuf==3.6.0 25 | pyparsing==2.2.0 26 | python-dateutil==2.7.3 27 | pytz==2018.4 28 | PyWavelets==0.5.2 29 | scikit-image 30 | scipy==1.1.0 31 | six==1.11.0 32 | subprocess32==3.5.2 33 | tensorboard==1.8.0 34 | tensorflow==1.8.0 35 | termcolor==1.1.0 36 | toolz==0.9.0 37 | Werkzeug==0.14.1 38 | -------------------------------------------------------------------------------- /src/checkpoint/checkpoint: -------------------------------------------------------------------------------- 1 | model_checkpoint_path: "model.ckpt-3000" 2 | all_model_checkpoint_paths: "model.ckpt-1" 3 | all_model_checkpoint_paths: "model.ckpt-3000" 4 | -------------------------------------------------------------------------------- /src/checkpoint/eval/events.out.tfevents.1552384935.bogon: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangslob/captcha-breaker/3bcd8600d8a48d8c272e8252367795241452e8cf/src/checkpoint/eval/events.out.tfevents.1552384935.bogon -------------------------------------------------------------------------------- /src/checkpoint/events.out.tfevents.1552384633.bogon: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangslob/captcha-breaker/3bcd8600d8a48d8c272e8252367795241452e8cf/src/checkpoint/events.out.tfevents.1552384633.bogon -------------------------------------------------------------------------------- /src/checkpoint/model.ckpt-1.data-00000-of-00001: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangslob/captcha-breaker/3bcd8600d8a48d8c272e8252367795241452e8cf/src/checkpoint/model.ckpt-1.data-00000-of-00001 -------------------------------------------------------------------------------- /src/checkpoint/model.ckpt-1.index: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangslob/captcha-breaker/3bcd8600d8a48d8c272e8252367795241452e8cf/src/checkpoint/model.ckpt-1.index -------------------------------------------------------------------------------- /src/checkpoint/model.ckpt-1.meta: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangslob/captcha-breaker/3bcd8600d8a48d8c272e8252367795241452e8cf/src/checkpoint/model.ckpt-1.meta -------------------------------------------------------------------------------- /src/checkpoint/model.ckpt-3000.data-00000-of-00001: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangslob/captcha-breaker/3bcd8600d8a48d8c272e8252367795241452e8cf/src/checkpoint/model.ckpt-3000.data-00000-of-00001 -------------------------------------------------------------------------------- /src/checkpoint/model.ckpt-3000.index: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangslob/captcha-breaker/3bcd8600d8a48d8c272e8252367795241452e8cf/src/checkpoint/model.ckpt-3000.index -------------------------------------------------------------------------------- /src/checkpoint/model.ckpt-3000.meta: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangslob/captcha-breaker/3bcd8600d8a48d8c272e8252367795241452e8cf/src/checkpoint/model.ckpt-3000.meta -------------------------------------------------------------------------------- /src/create_train_data.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function, absolute_import, division 2 | import os 3 | import json 4 | from skimage import io 5 | from img import split_letters 6 | # from img_test import split_letters 7 | import numpy as np 8 | 9 | DATA_DIR = 'data' 10 | DATA_MAP = os.path.join(DATA_DIR, 'captcha.json') 11 | DATA_FULL_DIR = os.path.join(DATA_DIR, 'captcha') 12 | DATA_TRAIN_DIR = os.path.join(DATA_DIR, 'train') 13 | DATA_TRAIN_FILE = os.path.join(DATA_DIR, 'captcha') 14 | 15 | # https://xiaoyuan.zhaopin.com/LoginManager/GetValidateCode?r=0.5721792161614632 16 | # array of tuple of binary image and label 17 | data_x = [] 18 | data_y = [] 19 | 20 | # load image content json file 21 | with open(DATA_MAP) as f: 22 | image_contents = json.load(f) 23 | 24 | # load image and save letters 25 | counter = 0 26 | for fname, contents in image_contents.items(): 27 | counter += 1 28 | print(counter, fname, contents) 29 | image = io.imread(os.path.join(DATA_FULL_DIR, fname), as_gray=True) 30 | 31 | # split image 32 | letters = split_letters(image, debug=True) 33 | if letters is not None: 34 | fname = fname.replace('.jpg', '.png') 35 | for i, letter in enumerate(letters): 36 | content = contents[i] 37 | # add to dataset 38 | data_x.append(letter) 39 | data_y.append(np.uint8(ord(content) - 48)) # 65: 'A' 40 | 41 | # save letter into train folder 42 | fpath = os.path.join(DATA_TRAIN_DIR, content) 43 | if not os.path.exists(fpath): 44 | os.makedirs(fpath) 45 | letter_fname = os.path.join(fpath, str(i + 1) + '-' + fname) 46 | io.imsave(letter_fname, 255 - letter) # invert black <> white color 47 | else: 48 | print('Letters is not valid') 49 | 50 | # split into train and test data set 51 | train_num = int(len(data_y) * 0.8) # 80% 52 | 53 | # save train data 54 | print('saving dataset') 55 | np.savez_compressed(DATA_TRAIN_FILE, 56 | x_train=data_x[:train_num], y_train=data_y[:train_num], 57 | x_test=data_x[train_num:], y_test=data_y[train_num:]) 58 | -------------------------------------------------------------------------------- /src/data/9371.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangslob/captcha-breaker/3bcd8600d8a48d8c272e8252367795241452e8cf/src/data/9371.jpg -------------------------------------------------------------------------------- /src/data/captcha.json: -------------------------------------------------------------------------------- 1 | { 2 | "3601.jpg": "3601", 3 | "1651.jpg": "1651", 4 | "3771.jpg": "3771", 5 | "6172.jpg": "6172", 6 | "7104.jpg": "7104", 7 | "7134.jpg": "7134", 8 | "8113.jpg": "8113", 9 | "8395.jpg": "8395" 10 | } -------------------------------------------------------------------------------- /src/data/captcha.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangslob/captcha-breaker/3bcd8600d8a48d8c272e8252367795241452e8cf/src/data/captcha.npz -------------------------------------------------------------------------------- /src/data/captcha/1651.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangslob/captcha-breaker/3bcd8600d8a48d8c272e8252367795241452e8cf/src/data/captcha/1651.jpg -------------------------------------------------------------------------------- /src/data/captcha/3601.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangslob/captcha-breaker/3bcd8600d8a48d8c272e8252367795241452e8cf/src/data/captcha/3601.jpg -------------------------------------------------------------------------------- /src/data/captcha/3771.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangslob/captcha-breaker/3bcd8600d8a48d8c272e8252367795241452e8cf/src/data/captcha/3771.jpg -------------------------------------------------------------------------------- /src/data/captcha/6172.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangslob/captcha-breaker/3bcd8600d8a48d8c272e8252367795241452e8cf/src/data/captcha/6172.jpg -------------------------------------------------------------------------------- /src/data/captcha/7104.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangslob/captcha-breaker/3bcd8600d8a48d8c272e8252367795241452e8cf/src/data/captcha/7104.jpg -------------------------------------------------------------------------------- /src/data/captcha/7134.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangslob/captcha-breaker/3bcd8600d8a48d8c272e8252367795241452e8cf/src/data/captcha/7134.jpg -------------------------------------------------------------------------------- /src/data/captcha/8113.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangslob/captcha-breaker/3bcd8600d8a48d8c272e8252367795241452e8cf/src/data/captcha/8113.jpg -------------------------------------------------------------------------------- /src/data/captcha/8395.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangslob/captcha-breaker/3bcd8600d8a48d8c272e8252367795241452e8cf/src/data/captcha/8395.jpg -------------------------------------------------------------------------------- /src/data/test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangslob/captcha-breaker/3bcd8600d8a48d8c272e8252367795241452e8cf/src/data/test.jpg -------------------------------------------------------------------------------- /src/data/train/0/3-3601.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangslob/captcha-breaker/3bcd8600d8a48d8c272e8252367795241452e8cf/src/data/train/0/3-3601.png -------------------------------------------------------------------------------- /src/data/train/0/3-7104.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangslob/captcha-breaker/3bcd8600d8a48d8c272e8252367795241452e8cf/src/data/train/0/3-7104.png -------------------------------------------------------------------------------- /src/data/train/1/1-1651.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangslob/captcha-breaker/3bcd8600d8a48d8c272e8252367795241452e8cf/src/data/train/1/1-1651.png -------------------------------------------------------------------------------- /src/data/train/1/2-6172.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangslob/captcha-breaker/3bcd8600d8a48d8c272e8252367795241452e8cf/src/data/train/1/2-6172.png -------------------------------------------------------------------------------- /src/data/train/1/2-7104.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangslob/captcha-breaker/3bcd8600d8a48d8c272e8252367795241452e8cf/src/data/train/1/2-7104.png -------------------------------------------------------------------------------- /src/data/train/1/2-7134.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangslob/captcha-breaker/3bcd8600d8a48d8c272e8252367795241452e8cf/src/data/train/1/2-7134.png -------------------------------------------------------------------------------- /src/data/train/1/2-8113.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangslob/captcha-breaker/3bcd8600d8a48d8c272e8252367795241452e8cf/src/data/train/1/2-8113.png -------------------------------------------------------------------------------- /src/data/train/1/3-8113.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangslob/captcha-breaker/3bcd8600d8a48d8c272e8252367795241452e8cf/src/data/train/1/3-8113.png -------------------------------------------------------------------------------- /src/data/train/1/4-1651.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangslob/captcha-breaker/3bcd8600d8a48d8c272e8252367795241452e8cf/src/data/train/1/4-1651.png -------------------------------------------------------------------------------- /src/data/train/1/4-3601.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangslob/captcha-breaker/3bcd8600d8a48d8c272e8252367795241452e8cf/src/data/train/1/4-3601.png -------------------------------------------------------------------------------- /src/data/train/1/4-3771.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangslob/captcha-breaker/3bcd8600d8a48d8c272e8252367795241452e8cf/src/data/train/1/4-3771.png -------------------------------------------------------------------------------- /src/data/train/2/4-6172.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangslob/captcha-breaker/3bcd8600d8a48d8c272e8252367795241452e8cf/src/data/train/2/4-6172.png -------------------------------------------------------------------------------- /src/data/train/3/1-3601.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangslob/captcha-breaker/3bcd8600d8a48d8c272e8252367795241452e8cf/src/data/train/3/1-3601.png -------------------------------------------------------------------------------- /src/data/train/3/1-3771.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangslob/captcha-breaker/3bcd8600d8a48d8c272e8252367795241452e8cf/src/data/train/3/1-3771.png -------------------------------------------------------------------------------- /src/data/train/3/2-8395.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangslob/captcha-breaker/3bcd8600d8a48d8c272e8252367795241452e8cf/src/data/train/3/2-8395.png -------------------------------------------------------------------------------- /src/data/train/3/3-7134.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangslob/captcha-breaker/3bcd8600d8a48d8c272e8252367795241452e8cf/src/data/train/3/3-7134.png -------------------------------------------------------------------------------- /src/data/train/3/4-8113.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangslob/captcha-breaker/3bcd8600d8a48d8c272e8252367795241452e8cf/src/data/train/3/4-8113.png -------------------------------------------------------------------------------- /src/data/train/4/4-7104.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangslob/captcha-breaker/3bcd8600d8a48d8c272e8252367795241452e8cf/src/data/train/4/4-7104.png -------------------------------------------------------------------------------- /src/data/train/4/4-7134.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangslob/captcha-breaker/3bcd8600d8a48d8c272e8252367795241452e8cf/src/data/train/4/4-7134.png -------------------------------------------------------------------------------- /src/data/train/5/3-1651.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangslob/captcha-breaker/3bcd8600d8a48d8c272e8252367795241452e8cf/src/data/train/5/3-1651.png -------------------------------------------------------------------------------- /src/data/train/5/4-8395.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangslob/captcha-breaker/3bcd8600d8a48d8c272e8252367795241452e8cf/src/data/train/5/4-8395.png -------------------------------------------------------------------------------- /src/data/train/6/1-6172.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangslob/captcha-breaker/3bcd8600d8a48d8c272e8252367795241452e8cf/src/data/train/6/1-6172.png -------------------------------------------------------------------------------- /src/data/train/6/2-1651.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangslob/captcha-breaker/3bcd8600d8a48d8c272e8252367795241452e8cf/src/data/train/6/2-1651.png -------------------------------------------------------------------------------- /src/data/train/6/2-3601.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangslob/captcha-breaker/3bcd8600d8a48d8c272e8252367795241452e8cf/src/data/train/6/2-3601.png -------------------------------------------------------------------------------- /src/data/train/7/1-7104.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangslob/captcha-breaker/3bcd8600d8a48d8c272e8252367795241452e8cf/src/data/train/7/1-7104.png -------------------------------------------------------------------------------- /src/data/train/7/1-7134.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangslob/captcha-breaker/3bcd8600d8a48d8c272e8252367795241452e8cf/src/data/train/7/1-7134.png -------------------------------------------------------------------------------- /src/data/train/7/2-3771.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangslob/captcha-breaker/3bcd8600d8a48d8c272e8252367795241452e8cf/src/data/train/7/2-3771.png -------------------------------------------------------------------------------- /src/data/train/7/3-3771.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangslob/captcha-breaker/3bcd8600d8a48d8c272e8252367795241452e8cf/src/data/train/7/3-3771.png -------------------------------------------------------------------------------- /src/data/train/7/3-6172.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangslob/captcha-breaker/3bcd8600d8a48d8c272e8252367795241452e8cf/src/data/train/7/3-6172.png -------------------------------------------------------------------------------- /src/data/train/8/1-8113.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangslob/captcha-breaker/3bcd8600d8a48d8c272e8252367795241452e8cf/src/data/train/8/1-8113.png -------------------------------------------------------------------------------- /src/data/train/8/1-8395.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangslob/captcha-breaker/3bcd8600d8a48d8c272e8252367795241452e8cf/src/data/train/8/1-8395.png -------------------------------------------------------------------------------- /src/data/train/9/3-8395.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangslob/captcha-breaker/3bcd8600d8a48d8c272e8252367795241452e8cf/src/data/train/9/3-8395.png -------------------------------------------------------------------------------- /src/img.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function, absolute_import, division 2 | from math import floor, ceil 3 | from skimage import img_as_ubyte 4 | from skimage.measure import find_contours 5 | from skimage.util import crop 6 | from skimage.transform import resize 7 | 8 | import matplotlib.pyplot as plt 9 | 10 | SHIFT_PIXEL = 7 # shift image from right to left 11 | BINARY_THRESH = 30 # image binary thresh 12 | LETTER_SIZE = (20, 23) # letter width, heigth 13 | 14 | 15 | def split_letters(image, num_letters=4, debug=False): 16 | """ 17 | split full captcha image into `num_letters` lettersself. 18 | return list of letters binary image (0: white, 255: black) 19 | """ 20 | # move left 21 | left = crop(image, ((0, 0), (0, image.shape[1]-SHIFT_PIXEL)), copy=True) 22 | image[:, :-SHIFT_PIXEL] = image[:, SHIFT_PIXEL:] 23 | image[:, -SHIFT_PIXEL:] = left 24 | # binarization 25 | binary = image > BINARY_THRESH 26 | # find contours 27 | # contours = find_contours(binary, 0.5) 28 | # contours = [[ 29 | # [int(floor(min(contour[:, 1]))), int(floor(min(contour[:, 0])))], # top-left point 30 | # [int(ceil(max(contour[:, 1]))), int(ceil(max(contour[:, 0])))] # down-right point 31 | # ] for contour in contours] 32 | # # keep letters order 33 | # contours = sorted(contours, key=lambda contour: contour[0][0]) 34 | # # find letters box 35 | # letter_boxs = [] 36 | letter_boxs = [[[0, 7], [11, 24]], [[13, 5], [30, 30]], [[30, 5], [45, 29]], [[47, 4], [61, 28]]] 37 | 38 | # for contour in contours: 39 | # if len(letter_boxs) > 0 and contour[0][0] < letter_boxs[-1][1][0] - 5: 40 | # # skip inner contour 41 | # continue 42 | # # extract letter boxs by contour 43 | # boxs = get_letter_boxs(binary, contour) 44 | # for box in boxs: 45 | # letter_boxs.append(box) 46 | # check letter outer boxs number 47 | if len(letter_boxs) != num_letters: 48 | print('ERROR: number of letters is NOT valid', len(letter_boxs)) 49 | # debug 50 | if debug: 51 | print(letter_boxs) 52 | plt.imshow(binary, interpolation='nearest', cmap=plt.cm.gray) 53 | for [x_min, y_min], [x_max, y_max] in letter_boxs: 54 | plt.plot( 55 | [x_min, x_max, x_max, x_min, x_min], 56 | [y_min, y_min, y_max, y_max, y_min], 57 | linewidth=2) 58 | plt.xticks([]) 59 | plt.yticks([]) 60 | plt.show() 61 | return None 62 | 63 | # normalize size (40x40) 64 | letters = [] 65 | for [x_min, y_min], [x_max, y_max] in letter_boxs: 66 | letter = resize(image[y_min:y_max, x_min:x_max], LETTER_SIZE) 67 | letter = img_as_ubyte(letter < 0.6) 68 | letters.append(letter) 69 | 70 | return letters 71 | 72 | 73 | def get_letter_boxs(binary, contour): 74 | boxs = [] 75 | w = contour[1][0] - contour[0][0] # width 76 | h = contour[1][1] - contour[0][1] # height 77 | if w < 10: 78 | # skip too small contour (noise) 79 | return boxs 80 | 81 | if w < 37 and w / h < 1.1: 82 | boxs.append(contour) 83 | else: 84 | # split 2 letters if w is large 85 | x_mean = contour[0][0] + int(round(w / 2)) 86 | sub_contours = [ 87 | [contour[0], [x_mean, contour[1][1]]], 88 | [[x_mean, contour[0][1]], contour[1]] 89 | ] 90 | for [x_min, y_min], [x_max, y_max] in sub_contours: 91 | # fit y_min, y_max 92 | y_min_val = min(binary[y_min + 1, x_min:x_max]) 93 | y_max_val = min(binary[y_max - 1, x_min:x_max]) 94 | while y_min_val or y_max_val: 95 | if y_min_val: 96 | y_min += 1 97 | y_min_val = min(binary[y_min + 1, x_min:x_max]) 98 | if y_max_val: 99 | y_max -= 1 100 | y_max_val = min(binary[y_max - 1, x_min:x_max]) 101 | 102 | boxs.append([[x_min, y_min], [x_max, y_max]]) 103 | 104 | return boxs 105 | -------------------------------------------------------------------------------- /src/img_test.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function, absolute_import, division 2 | from math import floor, ceil 3 | from skimage import img_as_ubyte 4 | from skimage.measure import find_contours 5 | from skimage.util import crop 6 | from skimage.transform import resize 7 | 8 | # import matplotlib.pyplot as plt 9 | 10 | SHIFT_PIXEL = 3 # shift image from right to left 11 | BINARY_THRESH = 30 # image binary thresh 12 | LETTER_SIZE = (12, 10) # letter width, heigth 13 | 14 | 15 | def split_letters(image, num_letters=4, debug=False): 16 | """ 17 | split full captcha image into `num_letters` lettersself. 18 | return list of letters binary image (0: white, 255: black) 19 | """ 20 | 21 | # move left 22 | left = crop(image, ((0, 0), (0, image.shape[1] - SHIFT_PIXEL)), copy=True) 23 | image[:, :-SHIFT_PIXEL] = image[:, SHIFT_PIXEL:] 24 | image[:, -SHIFT_PIXEL:] = left 25 | 26 | # binarization 27 | binary = image > BINARY_THRESH 28 | 29 | letter_boxs = [[[0, 5], [10, 17]], [[9, 5], [19, 17]], [[18, 5], [28, 17]], [[27, 5], [37, 17]]] 30 | 31 | letters = [] 32 | for [x_min, y_min], [x_max, y_max] in letter_boxs: 33 | letter = resize(image[y_min:y_max, x_min:x_max], LETTER_SIZE) 34 | letter = img_as_ubyte(letter < 0.6) 35 | letters.append(letter) 36 | 37 | return letters 38 | -------------------------------------------------------------------------------- /src/model.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function, absolute_import, division 2 | import tensorflow as tf 3 | 4 | MODEL_LOG_DIR = 'checkpoint' 5 | 6 | 7 | def cnn_model_fn(features, labels, mode): 8 | """Model function for CNN.""" 9 | # Input Layer 10 | input_layer = tf.reshape(features['x'], [-1, 20, 23, 1]) 11 | 12 | # Convolutional Layer #1 13 | conv1 = tf.layers.conv2d( 14 | inputs=input_layer, 15 | filters=32, 16 | kernel_size=[5, 5], 17 | padding='same', 18 | activation=tf.nn.relu) 19 | 20 | # Pooling Layer #1 21 | pool1 = tf.layers.max_pooling2d(inputs=conv1, pool_size=[2, 2], strides=2) 22 | 23 | # Convolutional Layer #2 and Pooling Layer #2 24 | conv2 = tf.layers.conv2d( 25 | inputs=pool1, 26 | filters=64, 27 | kernel_size=[5, 5], 28 | padding='same', 29 | activation=tf.nn.relu) 30 | pool2 = tf.layers.max_pooling2d(inputs=conv2, pool_size=[2, 2], strides=2) 31 | 32 | # Dense Layer 33 | pool2_flat = tf.layers.flatten(inputs=pool2) 34 | dense = tf.layers.dense(inputs=pool2_flat, units=1024, activation=tf.nn.relu) 35 | dropout = tf.layers.dropout( 36 | inputs=dense, rate=0.4, training=mode == tf.estimator.ModeKeys.TRAIN) 37 | 38 | # Logits Layer 39 | logits = tf.layers.dense(inputs=dropout, units=10) 40 | 41 | predictions = { 42 | # Generate predictions (for PREDICT and EVAL mode) 43 | 'classes': tf.argmax(input=logits, axis=1), 44 | # Add `softmax_tensor` to the graph. It is used for PREDICT and by the 45 | # `logging_hook`. 46 | 'probabilities': tf.nn.softmax(logits, name='softmax_tensor') 47 | } 48 | 49 | if mode == tf.estimator.ModeKeys.PREDICT: 50 | return tf.estimator.EstimatorSpec(mode=mode, predictions=predictions) 51 | 52 | # Calculate Loss (for both TRAIN and EVAL modes) 53 | loss = tf.losses.sparse_softmax_cross_entropy(labels=labels, logits=logits) 54 | 55 | # Configure the Training Op (for TRAIN mode) 56 | if mode == tf.estimator.ModeKeys.TRAIN: 57 | optimizer = tf.train.GradientDescentOptimizer(learning_rate=0.001) 58 | train_op = optimizer.minimize( 59 | loss=loss, 60 | global_step=tf.train.get_global_step()) 61 | return tf.estimator.EstimatorSpec(mode=mode, loss=loss, train_op=train_op) 62 | 63 | # Add evaluation metrics (for EVAL mode) 64 | eval_metric_ops = { 65 | 'accuracy': tf.metrics.accuracy( 66 | labels=labels, predictions=predictions['classes'])} 67 | return tf.estimator.EstimatorSpec( 68 | mode=mode, loss=loss, eval_metric_ops=eval_metric_ops) 69 | 70 | 71 | captcha_classifier = tf.estimator.Estimator( 72 | model_fn=cnn_model_fn, 73 | model_dir=MODEL_LOG_DIR 74 | ) 75 | -------------------------------------------------------------------------------- /src/predict.js: -------------------------------------------------------------------------------- 1 | const PythonShell = require('python-shell') 2 | 3 | const PY_SCRIPT = 'src/predict.py' 4 | 5 | module.exports = image_path => (new Promise((resolve, reject) => { 6 | let opts = { } 7 | if (image_path) { 8 | opts.args = ['--fname', image_path] 9 | } 10 | PythonShell.run(PY_SCRIPT, opts, (err, results) => { 11 | if (err) return reject(err) 12 | resolve(results) 13 | }) 14 | })) 15 | -------------------------------------------------------------------------------- /src/predict.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division, print_function 2 | 3 | import argparse 4 | from skimage import io 5 | import tensorflow as tf 6 | from img import split_letters 7 | from model import captcha_classifier 8 | 9 | # disable all warnings 10 | import warnings 11 | warnings.filterwarnings('ignore') 12 | # disable tf runtime message 13 | import os 14 | os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2' 15 | 16 | 17 | def main(argv): 18 | 19 | # load image 20 | print('loading image:', argv) 21 | image = io.imread(argv, as_gray=True) 22 | 23 | # split into letters 24 | letters = split_letters(image) 25 | 26 | probs = [] 27 | for i, letter in enumerate(letters): 28 | letter = letter.astype('float32') / 255 29 | predict_input_fn = tf.estimator.inputs.numpy_input_fn( 30 | x={'x': letter}, 31 | y=None, 32 | num_epochs=1, 33 | shuffle=False) 34 | predictions = captcha_classifier.predict(input_fn=predict_input_fn) 35 | for pred_dict in predictions: 36 | class_id = pred_dict['classes'] 37 | probs.append(str(class_id)) 38 | print(''.join(probs)) 39 | 40 | 41 | def path(): 42 | r = [] 43 | base = os.path.os.getcwd() + '/data/captcha/' 44 | for i in os.listdir(base): 45 | if 'jpg' in i: 46 | r.append(base + i) 47 | return r 48 | 49 | 50 | if __name__ == '__main__': 51 | for i in path(): 52 | main(i) 53 | -------------------------------------------------------------------------------- /src/train.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function, absolute_import, division 2 | import argparse 3 | import numpy as np 4 | import tensorflow as tf 5 | from model import captcha_classifier 6 | 7 | MODEL_LOG_DIR = 'checkpoint' 8 | 9 | parser = argparse.ArgumentParser() 10 | parser.add_argument('--batch_size', default=100, type=int, help='batch size') 11 | parser.add_argument('--train_steps', default=3000, type=int, 12 | help='number of training steps') 13 | 14 | 15 | def main(argv): 16 | # run argv 17 | args = parser.parse_args(argv[1:]) 18 | print(args. batch_size, args.train_steps) 19 | # Load training and eval data 20 | with np.load('data/captcha.npz') as f: 21 | x_train, y_train = f['x_train'], f['y_train'] 22 | x_test, y_test = f['x_test'], f['y_test'] 23 | print(x_train.shape, y_train.shape, x_test.shape, y_test.shape) 24 | x_train = x_train.astype('float32') / 255 25 | x_test = x_test.astype('float32') / 255 26 | y_train = np.asarray(y_train, dtype=np.int32) 27 | y_test = np.asarray(y_test, dtype=np.int32) 28 | 29 | # Set up logging for predictions 30 | tensors_to_log = {'probabilities': 'softmax_tensor'} 31 | logging_hook = tf.train.LoggingTensorHook( 32 | tensors=tensors_to_log, 33 | every_n_iter=50 34 | ) 35 | 36 | # Train the model 37 | train_input_fn = tf.estimator.inputs.numpy_input_fn( 38 | x={'x': x_train}, 39 | y=y_train, 40 | batch_size=args.batch_size, 41 | num_epochs=None, 42 | shuffle=True 43 | ) 44 | captcha_classifier.train( 45 | input_fn=train_input_fn, 46 | steps=args.train_steps, 47 | hooks=[logging_hook] 48 | ) 49 | 50 | # Evaluate the model and print results 51 | eval_input_fn = tf.estimator.inputs.numpy_input_fn( 52 | x={'x': x_test}, 53 | y=y_test, 54 | num_epochs=1, 55 | shuffle=False) 56 | eval_results = captcha_classifier.evaluate(input_fn=eval_input_fn) 57 | print(eval_results) 58 | 59 | 60 | if __name__ == '__main__': 61 | tf.logging.set_verbosity(tf.logging.INFO) 62 | tf.app.run(main) 63 | --------------------------------------------------------------------------------