├── .gitignore ├── LICENSE ├── README.md ├── captcha.py ├── requirements.txt └── samples ├── BJNF_ccac.jpg ├── EGVL_2a78.jpg └── SBST_500c.jpg /.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 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 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 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | 103 | .idea/* 104 | .vscode/* 105 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 YuJun 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 | ## CAPTCHA generator. Just for fun :). 2 | 3 | The CAPTCHA created like this: 4 | 5 | ![1](https://github.com/skyduy/CAPTCHA_generator/raw/master/samples/BJNF_ccac.jpg) 6 | ![2](https://github.com/skyduy/CAPTCHA_generator/raw/master/samples/EGVL_2a78.jpg) 7 | ![3](https://github.com/skyduy/CAPTCHA_generator/raw/master/samples/EGVL_2a78.jpg) 8 | 9 | 10 | Actually this is used to generate training data of project [CNN_keras](https://github.com/skyduy/CNN_keras) 11 | 12 | ### example: 13 | ```python 14 | from captcha import Captcha 15 | 16 | letter_set = '0123456789' 17 | letters_per_img = 5 18 | min_width, min_height = 128, 36 19 | c = Captcha(min_width, min_height, letter_set, letters_per_img, debug=True) 20 | c.batch_create_img(5) 21 | ``` 22 | 23 | #### TODO 24 | - Add more customizable params. 25 | - Improve performance. 26 | -------------------------------------------------------------------------------- /captcha.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import cv2 3 | import string 4 | import math 5 | import os 6 | import uuid 7 | 8 | wd, _ = os.path.split(os.path.abspath(__file__)) 9 | 10 | 11 | class Captcha: 12 | def __init__(self, width, high, ls=None, lc=4, fs=None, 13 | folder=os.path.join(wd, 'samples'), debug=False): 14 | """ 15 | :param ls: letter set, all 16 | :param fs: font set 17 | :param lc: letter count in one pic 18 | :param folder: the folder to save img 19 | :param debug: debug mode 20 | """ 21 | if fs is None: 22 | fs = ['FONT_HERSHEY_COMPLEX', 'FONT_HERSHEY_SIMPLEX', 23 | 'FONT_ITALIC'] 24 | self.fs = fs 25 | 26 | if ls is None: 27 | ls = string.ascii_uppercase + string.digits 28 | if isinstance(ls, str): 29 | self.letter = [i for i in ls] 30 | elif isinstance(ls, list): 31 | self.letter = ls 32 | 33 | self.lc = lc 34 | self.width, self.high = width, high 35 | self.debug = debug 36 | self.folder = folder 37 | if not self.debug and folder: 38 | if not os.path.exists(self.folder): 39 | os.makedirs(self.folder) 40 | 41 | def _tilt_img(self, img): 42 | tmp_img = img.copy() 43 | tmp_img.fill(255) 44 | tile_angle = np.random.randint( 45 | int(100*-math.pi/6), int(100*math.pi/6) 46 | ) / 100 47 | high, width, _ = img.shape 48 | for y in range(width): 49 | for x in range(high): 50 | new_y = int(y + (x-high/2)*math.tanh(tile_angle)) 51 | try: 52 | tmp_img[x, new_y, :] = img[x, y, :] 53 | except IndexError: 54 | pass 55 | img[:, :, :] = tmp_img[:, :, :] 56 | 57 | def _shake_img(self, img, outer_top_left, outer_bottom_right, 58 | inner_top_left, inner_bottom_right): 59 | (x1, y1), (x2, y2) = outer_top_left, outer_bottom_right 60 | (i1, j1), (i2, j2) = inner_top_left, inner_bottom_right 61 | delta_x = np.random.randint(x1-i1, x2-i2) 62 | delta_y = np.random.randint(y1-j1, y2-j2) 63 | area = img[y1:y2, x1:x2, :] 64 | area_high, area_width, _ = area.shape 65 | tmp_area = area.copy() 66 | tmp_area.fill(255) 67 | 68 | for index_y in range(area_high): 69 | for index_x in range(area_width): 70 | new_x, new_y = index_x + delta_x, index_y + delta_y 71 | if new_x < area_width and new_y < area_high: 72 | tmp_area[new_y, new_x, :] = area[index_y, index_x, :] 73 | 74 | area[:, :, :] = tmp_area[:, :, :] 75 | 76 | def _distort_img(self, img): 77 | high, width, _ = img.shape 78 | tmp_img = img.copy() 79 | tmp_img.fill(255) 80 | 81 | coef_vertical = np.random.randint(1, 5) 82 | coef_horizontal = np.random.choice([2, 3, 4]) * math.pi / width 83 | scale_biase = np.random.randint(0, 360) * math.pi / 180 84 | 85 | def new_coordinate(x, y): 86 | return int(x+coef_vertical*math.sin(coef_horizontal*y+scale_biase)) 87 | 88 | for y in range(width): 89 | for x in range(high): 90 | new_x = new_coordinate(x, y) 91 | try: 92 | tmp_img[x, y, :] = img[new_x, y, :] 93 | except IndexError: 94 | pass 95 | 96 | img[:, :, :] = tmp_img[:, :, :] 97 | 98 | def _draw_basic(self, img, text): 99 | font_face = getattr(cv2, np.random.choice(self.fs)) 100 | font_scale = 1 101 | font_thickness = 2 102 | max_width = max_high = 0 103 | for i in text: 104 | (width, high), _ = cv2.getTextSize( 105 | i, font_face, font_scale, font_thickness) 106 | max_width, max_high = max(max_width, width), max(max_high, high) 107 | 108 | total_width = max_width * self.lc 109 | width_delta = np.random.randint(0, self.width - total_width) 110 | vertical_range = self.high - max_high 111 | images = list() 112 | for index, letter in enumerate(text): 113 | tmp_img = img.copy() 114 | delta_high = np.random.randint( 115 | int(2*vertical_range/5), int(3*vertical_range/5) 116 | ) 117 | bottom_left_coordinate = ( 118 | index*max_width + width_delta, 119 | self.high - delta_high 120 | ) 121 | font_color = tuple(int(np.random.choice(range(0, 156))) 122 | for _ in range(3)) 123 | cv2.putText(tmp_img, letter, bottom_left_coordinate, font_face, 124 | font_scale, font_color, font_thickness) 125 | self._tilt_img(tmp_img) 126 | images.append(tmp_img) 127 | high, width, _ = img.shape 128 | for y in range(width): 129 | for x in range(high): 130 | r, g, b = 0, 0, 0 131 | for tmp_img in images: 132 | r += tmp_img[x, y, 0] 133 | g += tmp_img[x, y, 1] 134 | b += tmp_img[x, y, 2] 135 | r, g, b = r % 256, g % 256, b % 256 136 | img[x, y, :] = (r, g, b) 137 | 138 | def _draw_line(self, img): 139 | left_x = np.random.randint(0, self.width//4) 140 | left_y = np.random.randint(self.high) 141 | right_x = np.random.randint(self.width*3//4, self.width) 142 | right_y = np.random.randint(self.high) 143 | start, end = (left_x, left_y), (right_x, right_y) 144 | line_color = tuple(int(np.random.choice(range(0, 156))) 145 | for _ in range(3)) 146 | line_thickness = np.random.randint(1, 3) 147 | cv2.line(img, start, end, line_color, line_thickness) 148 | 149 | def _put_noise(self, img): 150 | for i in range(600): 151 | x = np.random.randint(self.width) 152 | y = np.random.randint(self.high) 153 | dot_color = tuple(int(np.random.choice(range(0, 156))) 154 | for _ in range(3)) 155 | img[y, x, :] = dot_color 156 | 157 | def save_img(self, text): 158 | img = np.zeros((self.high, self.width, 3), np.uint8) 159 | img.fill(255) 160 | self._draw_basic(img, text) 161 | self._put_noise(img) 162 | self._distort_img(img) 163 | self._draw_line(img) 164 | 165 | if self.debug: 166 | cv2.imshow(text, img) 167 | cv2.waitKey(0) 168 | cv2.destroyAllWindows() 169 | else: 170 | fn = text + ('_'+str(uuid.uuid1())[4: 8]) 171 | cv2.imwrite('{}/{}.jpg'.format(self.folder, fn), img) 172 | 173 | def batch_create_img(self, number=5): 174 | exits = set() 175 | while(len(exits)) < number: 176 | word = ''.join(np.random.choice(self.letter, self.lc)) 177 | if word not in exits: 178 | exits.add(word) 179 | self.save_img(word) 180 | if not self.debug: 181 | if len(exits) % 10 == 0: 182 | print('{} generated.'.format(len(exits))) 183 | if not self.debug: 184 | print('{} captchas saved into {}.'.format(len(exits), self.folder)) 185 | 186 | 187 | if __name__ == '__main__': 188 | letters = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'K', 'M', 189 | 'N', 'P', 'R', 'T', 'U', 'V', 'W', 'X', 'Y'] 190 | c = Captcha(120, 36, letters, fs=['FONT_ITALIC'], debug=True) 191 | c.batch_create_img(3) 192 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | opencv-python -------------------------------------------------------------------------------- /samples/BJNF_ccac.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skyduy/CAPTCHA_generator/6bc27d7c09dab33b5eba464ed56c74274544ad80/samples/BJNF_ccac.jpg -------------------------------------------------------------------------------- /samples/EGVL_2a78.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skyduy/CAPTCHA_generator/6bc27d7c09dab33b5eba464ed56c74274544ad80/samples/EGVL_2a78.jpg -------------------------------------------------------------------------------- /samples/SBST_500c.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skyduy/CAPTCHA_generator/6bc27d7c09dab33b5eba464ed56c74274544ad80/samples/SBST_500c.jpg --------------------------------------------------------------------------------