├── .github └── workflows │ ├── ci-test.yml │ └── release.yml ├── .gitignore ├── 1.png ├── LICENSE ├── README.md ├── README_ZH.md ├── img ├── .DS_Store ├── 0.png ├── 1.png ├── 2.png ├── 3.png ├── 4.png ├── 5.png ├── 6.png ├── 7.png ├── 8.png ├── 9.png ├── all.png ├── do.png ├── do1.png ├── do2.png ├── do3.png ├── do4.png ├── files.png ├── frcc-1.png ├── frcc0.png ├── frcc1.png ├── frcc2.png ├── frcc3.png ├── frcc4.png ├── frcc5.png ├── frcc6.png ├── last.png ├── pay.png └── train.png ├── pycapt ├── __init__.py ├── basic │ ├── __init__.py │ ├── appstore.png │ ├── reset_file_type.py │ ├── resize_img.py │ └── resized_image.png ├── logo │ ├── .gitignore │ ├── __init__.py │ ├── android_logo.py │ ├── appstore.png │ └── ios_logo.py ├── make_captcha │ ├── OpenSans-Bold.ttf │ ├── __init__.py │ ├── easy_mode.py │ ├── make_capt.py │ ├── my_any_img.py │ └── noise.py └── solve_it │ ├── 1.png │ ├── __init__.py │ ├── cut_img.py │ ├── de_line.py │ ├── de_point.py │ └── easy_solve.py ├── setup.py ├── tests ├── test.py └── test_train_img.py └── update_version.py /.github/workflows/ci-test.yml: -------------------------------------------------------------------------------- 1 | name: Auto CI and Build Test 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | auto-ci-build-test: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v2 16 | 17 | - name: Set up Python 18 | uses: actions/setup-python@v2 19 | with: 20 | python-version: "3.x" 21 | 22 | - name: Install setuptools 23 | run: | 24 | python3 -m pip install setuptools 25 | python3 -m pip install wheel 26 | python3 -m pip install Pillow 27 | python3 -m pip install numpy 28 | 29 | - name: Build and List contents of dist directory 30 | run: | 31 | python3 -m unittest discover -s tests 32 | python3 setup.py sdist bdist_wheel 33 | ls -l dist/ 34 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Auto Publish to PyPI and GitHub Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths: 8 | - "pycapt/**" 9 | - ".github/workflows/**" 10 | - "tests/**" 11 | - "setup.py" 12 | 13 | jobs: 14 | update-version-and-publish: 15 | runs-on: ubuntu-latest 16 | environment: 17 | name: pypi 18 | url: https://pypi.org/p/pycapt 19 | permissions: 20 | id-token: write # IMPORTANT: this permission is mandatory for trusted publishing 21 | 22 | steps: 23 | - name: Checkout code 24 | uses: actions/checkout@v2 25 | 26 | - name: Set up Python 27 | uses: actions/setup-python@v2 28 | with: 29 | python-version: "3.x" 30 | 31 | - name: Install setuptools 32 | run: | 33 | python3 -m pip install setuptools 34 | python3 -m pip install wheel 35 | 36 | - name: Build and List contents of dist directory 37 | run: | 38 | python3 setup.py sdist bdist_wheel 39 | ls -l dist/ 40 | 41 | - name: Publish to PyPI 42 | uses: pypa/gh-action-pypi-publish@v1.8.14 43 | with: 44 | repository-url: https://upload.pypi.org/legacy/ 45 | packages-dir: dist/ 46 | password: ${{ secrets.PYPI_API_TOKEN }} 47 | 48 | - name: Extract version from setup.py 49 | id: extract_version 50 | run: echo "::set-output name=version::$(sed -n 's/.*version="\([^"]*\)".*/\1/p' setup.py)" 51 | shell: bash 52 | 53 | - name: Create GitHub Release 54 | if: success() 55 | uses: softprops/action-gh-release@v1 56 | with: 57 | tag_name: ${{ steps.extract_version.outputs.version }} 58 | files: dist/* 59 | token: ${{ secrets.RESP_GITHUB_TOKEN }} 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | __pycache__ 3 | .vscode 4 | *.pyc 5 | build 6 | pycapt.egg-info -------------------------------------------------------------------------------- /1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aboutmydreams/pycapt/5b7777be374e58d96c871a68c38d5aa21043eca3/1.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 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 | # What is pycapt 2 | 3 | [![Auto CI and Build Tools](https://github.com/aboutmydreams/pycapt/actions/workflows/ci-test.yml/badge.svg)](https://github.com/aboutmydreams/pycapt/actions/workflows/ci-test.yml) 4 | [![Auto Publish to PyPI and GitHub Release](https://github.com/aboutmydreams/pycapt/actions/workflows/release.yml/badge.svg)](https://github.com/aboutmydreams/pycapt/actions/workflows/release.yml) 5 | [![label](https://img.shields.io/badge/%E4%B8%AD%E6%96%87%E6%96%87%E6%A1%A3-ZH-brightgreen)](https://github.com/aboutmydreams/pycapt/blob/main/README_ZH.md) 6 | [![label](https://img.shields.io/badge/English-EN-brightgreen)](https://github.com/aboutmydreams/pycapt/blob/main/README.md) 7 | [![Release Version](https://img.shields.io/github/release/aboutmydreams/pycapt.svg)](https://github.com/aboutmydreams/pycapt/releases) 8 | [![Visits](https://komarev.com/ghpvc/?username=aboutmydreams&repo=way3)](https://github.com/aboutmydreams/pycapt) 9 | [![License](https://img.shields.io/github/license/aboutmydreams/pycapt.svg)](https://github.com/aboutmydreams/pycapt/license) 10 | [![Stars](https://img.shields.io/github/stars/aboutmydreams/pycapt.svg)](https://github.com/aboutmydreams/pycapt/stargazers) 11 | [![Forks](https://img.shields.io/github/forks/aboutmydreams/pycapt.svg)](https://github.com/aboutmydreams/pycapt/network) 12 | [![Downloads](https://pepy.tech/badge/pycapt)](https://pepy.tech/project/pycapt) 13 | [![Contributors](https://img.shields.io/github/contributors/aboutmydreams/pycapt.svg)](https://github.com/aboutmydreams/pycapt/graphs/contributors) 14 | 15 | [GitHub Welcome to submit PRs, if there are bugs or new requests please feedback in issues](https://github.com/aboutmydreams/pycapt/issues) 16 | 17 | pycapt is a collection of image processing algorithms I created for handling CAPTCHAs. You can use it to denoise images, remove interference lines, and segment CAPTCHAs. pycapt encapsulates methods for manipulating image matrices, such as splitting images into standardized matrices and generating the required training images, which is helpful for using deep learning in image recognition. In 2024, pycapt released a new version that added some logo generation methods to generate Android or iOS logos with one click. 18 | 19 | pycapt includes both CAPTCHA processing and generation. Special thanks to my friends [exqlnet](https://github.com/exqlnet) and [ZhouYingSASA](https://github.com/ZhouYingSASA) for their support in releasing pycapt version 1.0.1. 20 | 21 | ## Dependencies and Installation 22 | 23 | ```bash 24 | pip3 install Pillow numpy pycapt 25 | ``` 26 | 27 | or use poetry to install: 28 | 29 | ```bash 30 | poetry add Pillow numpy pycapt 31 | ``` 32 | 33 | ### Directory Structure 34 | 35 | ![frcc0](img/files.png) 36 | 37 | ## Using pycapt for CAPTCHA Image Processing 38 | 39 | ### Importing 40 | 41 | ```py 42 | import pycapt 43 | from PIL import Image 44 | ``` 45 | 46 | ### Image Binarization 47 | 48 | **two_value**: This method binarizes the image. The required parameter `img` is the image, and the optional parameter `Threshold` is the gray threshold, where you can choose an appropriate value (default is 100). **Returns a newly processed image.** 49 | 50 | ```py 51 | img = Image.open('./img/frcc0.png') 52 | img = pycapt.two_value(img, Threshold=100) 53 | img.show() 54 | ``` 55 | 56 | ![frcc0](img/frcc0.png) 57 | 58 | ![frcc1](img/frcc1.png) 59 | 60 | ### Noise Reduction 61 | 62 | **dele_noise**: This method removes noise using an eight-neighborhood denoising technique. `N` is the number of neighborhood outliers, and `Z` is the number of processing iterations; more iterations will result in a smoother image. 63 | 64 | ```py 65 | img = pycapt.dele_noise(img, N=5, Z=2) 66 | img.show() 67 | ``` 68 | 69 | ![frcc2](img/frcc2.png) 70 | 71 | ### Removing Interference Lines 72 | 73 | **dele_line**: This method removes interference lines by deleting `N` consecutive vertical pixels. It works best when used in conjunction with the `dele_noise` method. 74 | 75 | ```py 76 | img = pycapt.dele_line(img, N=4) 77 | img.show() 78 | ``` 79 | 80 | **For better results, you can first transpose the image using the `tran_90(img)` method, then apply the line removal method, and finally transpose it back.** 81 | 82 | ```py 83 | img = pycapt.tran_90(img) 84 | img.show() 85 | img = pycapt.dele_line(img, 3) 86 | img = pycapt.dele_line(img, 2) 87 | img = pycapt.dele_line(img, 1) 88 | img = pycapt.tran_90(img) 89 | img.show() 90 | ``` 91 | 92 | ![frcc2](img/frcc4.png) 93 | 94 | ### Slant Correction 95 | 96 | **The purpose of slant correction is to improve segmentation and recognition.** The principle involves shifting each row left or right by different distances to create a correction effect. The `pans` list contains the shift values, where positive numbers shift left and negative numbers shift right. The number of elements in the `pans` list must equal the image height. 97 | 98 | **`rectify_img(img, pans)` returns a new image.** 99 | 100 | ```py 101 | pan = [18, 18, 18, 18, 17, 17, 17, 102 | 16, 16, 16, 15, 15, 15, 15, 14, 103 | 14, 14, 14, 13, 13, 10, 10, 104 | 10, 9, 9, 8, 7, 6, 5, 5, 4, 105 | 4, 4, 4, 4, 3, 1, 0, 0, 0] 106 | img = pycapt.rectify_img(img, pans=pan) 107 | img.show() 108 | ``` 109 | 110 | ![frcc2](img/frcc5.png) 111 | 112 | If you find it too unappealing, you can apply correction first and then use `dele_line` and `dele_noise`. Of course, addressing issues later is also acceptable. 113 | 114 | ```py 115 | img = pycapt.rectify_img(img, pans=pan) 116 | img = pycapt.dele_line(img, 3) 117 | img = pycapt.dele_line(img, 2) 118 | img = pycapt.dele_line(img, 1) 119 | img.show() 120 | ``` 121 | 122 | ![frcc2](img/frcc6.png) 123 | 124 | ### Image Segmentation 125 | 126 | **cut_img_to_img_list** sets a suitable length for the single image before cutting, returning the segmented image. The length can be set relatively large, and this method will pad the cut images on both sides. You can use this as a method for standardizing images. 127 | 128 | ```py 129 | img = Image.open('1.png') 130 | img_list = pycapt.cut_img_to_img_list(img, max_width=30, background=255) 131 | for i in img_list: 132 | i.show() 133 | ``` 134 | 135 | ![frcc2](img/last.png) 136 | 137 | When using **deep learning**, you can also use **cut_img_to_mode_list(image, max_width)** to obtain a standardized array. 138 | 139 | ### Image Cropping 140 | 141 | When your image height can be compressed, you can use **small_img(img, box)** to crop the image, reducing the computational load for later learning. 142 | 143 | ## Using pycapt to Generate CAPTCHA Training Sets 144 | 145 | ### do_captcha for Generating CAPTCHA Training Sets 146 | 147 | `width` is the length of the CAPTCHA image, `height` is the height, `num_of_str` is the number of characters in the CAPTCHA (default is 4), `font` is the font size (default is 30), `gray_value` is the background gray value (default is 255), and `font_family` is the font file. You can choose the thickness, style, etc., but the font must be installed on your computer. 148 | 149 | If you're unsure about which fonts are installed on your computer, please click [**here**](https://www.yuque.com/zhiwa/deepin/ahimr7). 150 | 151 | ```py 152 | name, img = pycapt.do_captcha( 153 | my_str_list=['A', 'B', 'C', 'D', '1', '2', '3'], 154 | width=160, 155 | height=40, 156 | num_of_str=4, 157 | font=30, 158 | gray_value=255, 159 | font_family=None 160 | ) 161 | 162 | print(name) 163 | img.show() 164 | 165 | # output: ['C', 'D', '2', 'A'] 166 | ``` 167 | 168 | ![frcc2](img/do.png) 169 | 170 | ### Adding Noise 171 | 172 | **more_noise**: `N` is the noise rate (0 < N < 1), and `Z` is the number of processing iterations. 173 | 174 | ```py 175 | img = pycapt.more_noise(img, N=0.5, Z=2) 176 | ``` 177 | 178 | ![frcc2](img/do1.png) 179 | 180 | ### Panning 181 | 182 | ```py 183 | img = pycapt.img_pan(img, 10, 3) 184 | ``` 185 | 186 | ![frcc2](img/do2.png) 187 | 188 | ### Inclining 189 | 190 | As before, use `rectify_img`. 191 | 192 | ```py 193 | pan = [18, 18, 18, 18, 17, 17, 17, 194 | 16, 16, 16, 15, 15, 15, 15, 14, 195 | 14, 14, 14, 13, 13, 10, 10, 196 | 10, 9, 9, 8, 7, 6, 5, 5, 4, 197 | 4, 4, 4, 4, 3, 1, 0, 0, 0] 198 | img = pycapt.rectify_img(img, pans=pan) 199 | ``` 200 | 201 | ![frcc2](img/do3.png) 202 | 203 | ### Denoising for Smoothness 204 | 205 | `clear_train_img` effectively applies `dele_line(line, N)` sequentially for `N=4, 3, 2, 1`, smoothing the image vertically. 206 | 207 | ```py 208 | img = pycapt.show_noise_img(img, 0.1, 1) 209 | img = pycapt.dele_noise(img, 5, 2) 210 | img = pycapt.clear_train_img(img) 211 | ``` 212 | 213 | ![frcc2](img/do4.png) 214 | 215 | Here, you can fully utilize pycapt to generate a training set for CAPTCHA with deep learning. 216 | 217 | If you want something more convenient, please see below. 218 | 219 | ### Directly Generating Training Set Method 220 | 221 | **easy_train_img** returns training set images. `my_str_list` is your character set list, `width` and `height` are the dimensions, and `num_of_str` is the number of characters displayed in the CAPTCHA image, which will be randomly selected from your `my_str_list`. 222 | 223 | ```py 224 | filename, img = pycapt.easy_train_img( 225 | my_str_list=['A', 'B', 'C', 'D', 'E'], 226 | width=30, 227 | height=32, 228 | num_of_str=1, 229 | font=30, 230 | xpan=3, 231 | ypan=2, 232 | rotate=15, 233 | noise_N=0.3, 234 | noise_Z=2, 235 | gray_value=255, 236 | font_family=None 237 | ) 238 | ``` 239 | 240 | You just need to write a loop like `img.save('train_img/{}.png'.format(file_name))` to generate thousands of training images 241 | 242 | , and you can obtain the label simply as `name = file_name[0]`. 243 | 244 | ![frcc2](img/train.png) 245 | 246 | ### 2024 Update: One-click Generation of Android or iOS Logos 247 | 248 | ```py 249 | current_dir = os.path.dirname(os.path.abspath(__file__)) 250 | 251 | # Android 252 | pycapt.generate_android_icon_assets( 253 | f"{current_dir}/appstore.png", f"{current_dir}/output_directory" 254 | ) 255 | 256 | # iOS 257 | pycapt.generate_ios_icon_assets( 258 | f"{current_dir}/appstore.png", 259 | f"{current_dir}/Assets.xcassets/AppIcon.appiconset", 260 | ) 261 | ``` 262 | 263 | The second parameter is your icon output directory, which will be created by default if it doesn't exist. 264 | 265 | ## Conclusion 266 | 267 | Theoretically, as long as you use pycapt to process images, call various methods, and use the `easy_train_img` method, you can solve 90% of CAPTCHA processing and generation problems. Feel free to star, PR, and submit issues. If you want to learn more about the underlying principles, click [here](https://www.yuque.com/zhiwa/deepin/og0te8). I look forward to hearing your thoughts or PR. 268 | 269 | ### Small Donations 270 | 271 | If you found this helpful, [buy me a cup of tea](https://www.yuque.com/zhiwa/deepin/hwnhg0)~ 272 | -------------------------------------------------------------------------------- /README_ZH.md: -------------------------------------------------------------------------------- 1 | ![all](img/all.png) 2 | 3 | # 什么是 pycapt 4 | 5 | [![Auto CI and Build Tools](https://github.com/aboutmydreams/pycapt/actions/workflows/ci-test.yml/badge.svg)](https://github.com/aboutmydreams/pycapt/actions/workflows/ci-test.yml) 6 | [![Auto Publish to PyPI and GitHub Release](https://github.com/aboutmydreams/pycapt/actions/workflows/release.yml/badge.svg)](https://github.com/aboutmydreams/pycapt/actions/workflows/release.yml) 7 | [![label](https://img.shields.io/badge/%E4%B8%AD%E6%96%87%E6%96%87%E6%A1%A3-ZH-brightgreen)](https://github.com/aboutmydreams/pycapt/blob/main/README_ZH.md) 8 | [![label](https://img.shields.io/badge/English-EN-brightgreen)](https://github.com/aboutmydreams/pycapt/blob/main/README.md) 9 | [![Release Version](https://img.shields.io/github/release/aboutmydreams/pycapt.svg)](https://github.com/aboutmydreams/pycapt/releases) 10 | [![Visits](https://komarev.com/ghpvc/?username=aboutmydreams&repo=way3)](https://github.com/aboutmydreams/pycapt) 11 | [![License](https://img.shields.io/github/license/aboutmydreams/pycapt.svg)](https://github.com/aboutmydreams/pycapt/license) 12 | [![Stars](https://img.shields.io/github/stars/aboutmydreams/pycapt.svg)](https://github.com/aboutmydreams/pycapt/stargazers) 13 | [![Forks](https://img.shields.io/github/forks/aboutmydreams/pycapt.svg)](https://github.com/aboutmydreams/pycapt/network) 14 | [![Downloads](https://pepy.tech/badge/pycapt)](https://pepy.tech/project/pycapt) 15 | [![Contributors](https://img.shields.io/github/contributors/aboutmydreams/pycapt.svg)](https://github.com/aboutmydreams/pycapt/graphs/contributors) 16 | 17 | [GitHub 欢迎提 pr,如果有 bug 或新需求 请反馈 issue](https://github.com/aboutmydreams/pycapt/issues) 18 | 19 | pycapt 是我在处理验证码时编写的一系列图像处理的算法包,你可以使用它来为图像去噪点、干扰线 以及分割验证码,pycapt 封装了一些关于图形矩阵的方法,例如将图片分割为标准化的矩阵、生成您所需要的训练集图片等,有助于您使用深度学习来进行图像识别。2024 年,pycapt 发布了新的版本,增加了一些 LOGO 生成的方法,可以一键生成 Android 或 iOS Logo。 20 | 21 | pycapt 包括处理验证码和生成验证码两部分,多谢我的好友 [exqlnet](https://github.com/exqlnet) [ZhouYingSASA](https://github.com/ZhouYingSASA) 的共同帮助 发布 pycapt 1.0.1 22 | 23 | ## 依赖与安装 24 | 25 | ```bash 26 | pip3 install Pillow numpy pycapt 27 | ``` 28 | 29 | 或者使用 poetry 安装: 30 | 31 | ```bash 32 | poetry add Pillow numpy pycapt 33 | ``` 34 | 35 | ### 目录结构 36 | 37 | ![frcc0](img/files.png) 38 | 39 | ## 使用 pycapt 进行验证码图像处理 40 | 41 | ### 导入 42 | 43 | ```py 44 | import pycapt 45 | from PIL import Image 46 | ``` 47 | 48 | ### 图像二值化 49 | 50 | **two_valve : 二值化方法,必选参数 img 为图片,可选参数 Threshold** 是灰度阀值,这里可以选择适合的值,默认值是 100 . **返回新处理过的图片** 51 | 52 | ```py 53 | img = Image.open('./img/frcc0.png') 54 | img = pycapt.two_value(img,Threshold=100) 55 | img.show() 56 | ``` 57 | 58 | ![frcc0](img/frcc0.png) 59 | 60 | ![frcc1](img/frcc1.png) 61 | 62 | ### 处理噪点 63 | 64 | **dele_noise :消除噪点方法,该方法使用的是八领域去噪点法,N 是领域异点个数,Z 是处理次数,处理次数越多 图形越圆滑**。 65 | 66 | ```py 67 | img = pycapt.dele_noise(img,N=5,Z=2) 68 | img.show() 69 | ``` 70 | 71 | ![frcc2](img/frcc2.png) 72 | 73 | ### 处理干扰线 74 | 75 | **dele_line : 去除干扰线,删除连续的 N 个竖直像素。配合 dele_noise 方法使用效果更佳。** 76 | 77 | ```py 78 | img = pycapt.dele_line(img,N=4) 79 | img.show() 80 | ``` 81 | 82 | **配合 dele_noise 方法使用效果更佳。** 83 | 84 | ```py 85 | img = pycapt.dele_line(img,4) 86 | img = pycapt.dele_noise(img,N=4,Z=2) 87 | img = pycapt.dele_line(img,3) 88 | img = pycapt.dele_noise(img,N=4,Z=2) 89 | img = pycapt.dele_line(img,3) 90 | img = pycapt.dele_line(img,2) 91 | img = pycapt.dele_line(img,1) 92 | img.show() 93 | ``` 94 | 95 | ![frcc2](img/frcc3.png) 96 | 97 | **想要更好的效果,你还可以先使用转置图片的 tran_90(img) 方法 再次使用去除干扰线的方法,最后再转置回来** 98 | 99 | ```py 100 | img = pycapt.tran_90(img) 101 | img.show() 102 | img = pycapt.dele_line(img,3) 103 | img = pycapt.dele_line(img,2) 104 | img = pycapt.dele_line(img,1) 105 | img = pycapt.tran_90(img) 106 | img.show() 107 | ``` 108 | 109 | ![frcc2](img/frcc4.png) 110 | 111 | ### 斜体矫正 112 | 113 | **斜体矫正的目的是为了更好的分割与识别。**原理是平移,将每一行向左或向右平移不同距离,最后形成矫正的效果。pans 就是矫正列表,正左负右平移。pans 列表的元素个数需要是图片的高度,例子中图片 height 是 40. 114 | 115 | **rectify_img(img,pans) 返回新的图片。** 116 | 117 | ```py 118 | pan = [18, 18, 18, 18, 17, 17, 17,\ 119 | 16, 16, 16, 15, 15, 15, 15, 14,\ 120 | 14, 14, 14, 13, 13, 10, 10,\ 121 | 10, 9, 9, 8, 7, 6, 5, 5, 4, \ 122 | 4, 4, 4, 4, 3, 1, 0, 0, 0] 123 | img = pycapt.rectify_img(img,pans=pan) 124 | img.show() 125 | ``` 126 | 127 | ![frcc2](img/frcc5.png) 128 | 129 | 如果你觉得太难看了,可以提前使用矫正再使用 dele_line 和 dele_noise, 当然亡羊补牢也不太坏。 130 | 131 | ```py 132 | img = pycapt.rectify_img(img,pans=pan) 133 | img = pycapt.dele_line(img,3) 134 | img = pycapt.dele_line(img,2) 135 | img = pycapt.dele_line(img,1) 136 | img.show() 137 | ``` 138 | 139 | ![frcc2](img/frcc6.png) 140 | 141 | ### 图形分割 142 | 143 | **cut_img_to_img_list** 设置单个图片合适长度后切割,返回该长度的切割图片,该长度可以设置的比较大,该方法会在切割图片的两边补白。你可以将这作为一种标准化图片的方法。 144 | 145 | ```py 146 | img = Image.open('1.png') 147 | img_list = pycapt.cut_img_to_img_list(img,max_width=30,background=255) 148 | for i in img_list: 149 | i.show() 150 | ``` 151 | 152 | ![frcc2](img/last.png) 153 | 154 | 当你使用**深度学习**时,还可以使用 **cut_img_to_mode_list(image,max_width)**来获得标准化的数组。 155 | 156 | ### 图片裁剪 157 | 158 | 当你的图片 height 可以压缩时,可以使用 **small_img(img,box)** 来裁剪图片,这样可以减少之后学习的计算量。 159 | 160 | 例如 161 | 162 | ## 使用 pycapt 生成验证码训练集 163 | 164 | ### do_captcha 生成验证码训练集 165 | 166 | width 验证码图片长度,height 验证码高度,num_of_str 验证码上字符数量 默认 4,font 字体大小 默认 30,gray_value 灰度值 默认 255,font_family 字体文件,在这里可以选择你需要的粗细,样式等,但前提是你电脑上有这种字体。 167 | 168 | 如果你不知道自己电脑有哪些字体,请点击 [**这里**](https://www.yuque.com/zhiwa/deepin/ahimr7) 。 169 | 170 | ```py 171 | name,img = pycapt.do_captcha( 172 | my_str_list=['A','B','C','D','1','2','3'], 173 | width=160, 174 | height=40, 175 | num_of_str=4, 176 | font=30, 177 | gray_value=255, 178 | font_family=None) 179 | 180 | print(name) 181 | img.show() 182 | 183 | # output: ['C', 'D', '2', 'A'] 184 | ``` 185 | 186 | ![frcc2](img/do.png) 187 | 188 | ### 增加噪点 189 | 190 | more_noise :N 是加噪率,0 < N < 1,Z 为处理次数 191 | 192 | ```py 193 | img = pycapt.more_noise(img,N=0.5,Z=2) 194 | ``` 195 | 196 | ![frcc2](img/do1.png) 197 | 198 | ### 偏移 199 | 200 | ```py 201 | img = pycapt.img_pan(img,10,3) 202 | ``` 203 | 204 | ![frcc2](img/do2.png) 205 | 206 | ### 倾斜 207 | 208 | 还是和之前一样, 使用 rectify_img 209 | 210 | ```py 211 | pan = [18, 18, 18, 18, 17, 17, 17,\ 212 | 16, 16, 16, 15, 15, 15, 15, 14,\ 213 | 14, 14, 14, 13, 13, 10, 10,\ 214 | 10, 9, 9, 8, 7, 6, 5, 5, 4, \ 215 | 4, 4, 4, 4, 3, 1, 0, 0, 0] 216 | img = pycapt.rectify_img(img,pans=pan) 217 | ``` 218 | 219 | ![frcc2](img/do3.png) 220 | 221 | ### 加去噪点 变平滑 222 | 223 | `clear_train_img` 相当于 dele_line(line,N) 分别对 line 消除了 N=4,3,2,1 的 4 次噪点,也就相当于纵向变平滑了 224 | 225 | ```py 226 | img = pycapt.show_noise_img(img,0.1,1) 227 | img = pycapt.dele_noise(img,5,2) 228 | img = pycapt.clear_train_img(img) 229 | ``` 230 | 231 | ![frcc2](img/do4.png) 232 | 233 | 这里 你完全可以使用 pycapt 生成深度学习验证码的训练集了。 234 | 235 | 那如果想要更方便点呢? 请看这里。 236 | 237 | ### 直接生成训练集的方法 238 | 239 | easy_train_img 返回训练集图片,my_str_list,你的字符集列表,width height 长度高度,num_of_str 显示在验证码图片上的字符串个数,会从你的 my_str_list 中随机挑选,font 显示的字体大小,xpan ypan 左右与上下随机偏移尺寸,rotate 字符随机旋转尺度,noise_N 加噪率(0 < N < 1),noise_Z 加噪次数,gray_value 背景灰度,默认白色,font_family 字体样式,如果你不知道自己电脑有哪些字体,请点击 [**这里**](https://www.yuque.com/zhiwa/deepin/ahimr7) 。 240 | 241 | ```py 242 | filename,img = pycapt.easy_train_img( 243 | my_str_list=['A','B','C','D','E'], 244 | width=30, 245 | height=32, 246 | num_of_str=1, 247 | font=30, 248 | xpan=3, 249 | ypan=2, 250 | rotate=15, 251 | noise_N=0.3, 252 | noise_Z=2, 253 | gray_value=255, 254 | font_family=None) 255 | ``` 256 | 257 | 只要你再写一个循环,**img.save('train_img/{}.png'.format(file_name))** 就可以生成成千上万张训练集图片 获取标签只需要 name = file_name[0] 就可以惹。 258 | 259 | ![frcc2](img/train.png) 260 | 261 | ### 2024 年更新: 一键生成 Android 或 iOS Logo 262 | 263 | ```py 264 | current_dir = os.path.dirname(os.path.abspath(__file__)) 265 | 266 | # Android 267 | pycapt.generate_android_icon_assets( 268 | f"{current_dir}/appstore.png", f"{current_dir}/output_directory" 269 | ) 270 | 271 | # iOS 272 | pycapt.generate_ios_icon_assets( 273 | f"{current_dir}/appstore.png", 274 | f"{current_dir}/Assets.xcassets/AppIcon.appiconset", 275 | ) 276 | ``` 277 | 278 | 第二个参数是你的图标输出目录,如果没有将会默认创建一个。 279 | 280 | ## Last 281 | 282 | 理论上只要你使用 pycapt 处理图片,调用各方法,并使用 `easy_train_img` 方法 ,理论上可以解决 90%验证码处理和模拟生成问题,欢迎 star pr 和提 issue,如果你想更了解其中的原理,点击[这里](https://www.yuque.com/zhiwa/deepin/og0te8),期望听见你的想法或 pr。 283 | 284 | ### 小额捐赠 285 | 286 | 如果有所帮助 [请我喝一碗茶](https://www.yuque.com/zhiwa/deepin/hwnhg0)~ 287 | -------------------------------------------------------------------------------- /img/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aboutmydreams/pycapt/5b7777be374e58d96c871a68c38d5aa21043eca3/img/.DS_Store -------------------------------------------------------------------------------- /img/0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aboutmydreams/pycapt/5b7777be374e58d96c871a68c38d5aa21043eca3/img/0.png -------------------------------------------------------------------------------- /img/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aboutmydreams/pycapt/5b7777be374e58d96c871a68c38d5aa21043eca3/img/1.png -------------------------------------------------------------------------------- /img/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aboutmydreams/pycapt/5b7777be374e58d96c871a68c38d5aa21043eca3/img/2.png -------------------------------------------------------------------------------- /img/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aboutmydreams/pycapt/5b7777be374e58d96c871a68c38d5aa21043eca3/img/3.png -------------------------------------------------------------------------------- /img/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aboutmydreams/pycapt/5b7777be374e58d96c871a68c38d5aa21043eca3/img/4.png -------------------------------------------------------------------------------- /img/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aboutmydreams/pycapt/5b7777be374e58d96c871a68c38d5aa21043eca3/img/5.png -------------------------------------------------------------------------------- /img/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aboutmydreams/pycapt/5b7777be374e58d96c871a68c38d5aa21043eca3/img/6.png -------------------------------------------------------------------------------- /img/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aboutmydreams/pycapt/5b7777be374e58d96c871a68c38d5aa21043eca3/img/7.png -------------------------------------------------------------------------------- /img/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aboutmydreams/pycapt/5b7777be374e58d96c871a68c38d5aa21043eca3/img/8.png -------------------------------------------------------------------------------- /img/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aboutmydreams/pycapt/5b7777be374e58d96c871a68c38d5aa21043eca3/img/9.png -------------------------------------------------------------------------------- /img/all.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aboutmydreams/pycapt/5b7777be374e58d96c871a68c38d5aa21043eca3/img/all.png -------------------------------------------------------------------------------- /img/do.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aboutmydreams/pycapt/5b7777be374e58d96c871a68c38d5aa21043eca3/img/do.png -------------------------------------------------------------------------------- /img/do1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aboutmydreams/pycapt/5b7777be374e58d96c871a68c38d5aa21043eca3/img/do1.png -------------------------------------------------------------------------------- /img/do2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aboutmydreams/pycapt/5b7777be374e58d96c871a68c38d5aa21043eca3/img/do2.png -------------------------------------------------------------------------------- /img/do3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aboutmydreams/pycapt/5b7777be374e58d96c871a68c38d5aa21043eca3/img/do3.png -------------------------------------------------------------------------------- /img/do4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aboutmydreams/pycapt/5b7777be374e58d96c871a68c38d5aa21043eca3/img/do4.png -------------------------------------------------------------------------------- /img/files.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aboutmydreams/pycapt/5b7777be374e58d96c871a68c38d5aa21043eca3/img/files.png -------------------------------------------------------------------------------- /img/frcc-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aboutmydreams/pycapt/5b7777be374e58d96c871a68c38d5aa21043eca3/img/frcc-1.png -------------------------------------------------------------------------------- /img/frcc0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aboutmydreams/pycapt/5b7777be374e58d96c871a68c38d5aa21043eca3/img/frcc0.png -------------------------------------------------------------------------------- /img/frcc1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aboutmydreams/pycapt/5b7777be374e58d96c871a68c38d5aa21043eca3/img/frcc1.png -------------------------------------------------------------------------------- /img/frcc2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aboutmydreams/pycapt/5b7777be374e58d96c871a68c38d5aa21043eca3/img/frcc2.png -------------------------------------------------------------------------------- /img/frcc3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aboutmydreams/pycapt/5b7777be374e58d96c871a68c38d5aa21043eca3/img/frcc3.png -------------------------------------------------------------------------------- /img/frcc4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aboutmydreams/pycapt/5b7777be374e58d96c871a68c38d5aa21043eca3/img/frcc4.png -------------------------------------------------------------------------------- /img/frcc5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aboutmydreams/pycapt/5b7777be374e58d96c871a68c38d5aa21043eca3/img/frcc5.png -------------------------------------------------------------------------------- /img/frcc6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aboutmydreams/pycapt/5b7777be374e58d96c871a68c38d5aa21043eca3/img/frcc6.png -------------------------------------------------------------------------------- /img/last.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aboutmydreams/pycapt/5b7777be374e58d96c871a68c38d5aa21043eca3/img/last.png -------------------------------------------------------------------------------- /img/pay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aboutmydreams/pycapt/5b7777be374e58d96c871a68c38d5aa21043eca3/img/pay.png -------------------------------------------------------------------------------- /img/train.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aboutmydreams/pycapt/5b7777be374e58d96c871a68c38d5aa21043eca3/img/train.png -------------------------------------------------------------------------------- /pycapt/__init__.py: -------------------------------------------------------------------------------- 1 | name = "pycapt" 2 | 3 | # 从 pycapt 中导入相关模块和函数 4 | from .solve_it.easy_solve import ( # noqa: E402 5 | get_modes as get_modes, 6 | mode_img as mode_img, 7 | two_value as two_value, 8 | mode_white_img as mode_white_img, 9 | dele_noise as dele_noise, 10 | dele_line as dele_line, 11 | clear_train_img as clear_train_img, 12 | clear_lib_line as clear_lib_line, 13 | cut_img_to_mode_list as cut_img_to_mode_list, 14 | cut_img_to_img_list as cut_img_to_img_list, 15 | rectify_img as rectify_img, 16 | rectify_mode as rectify_mode, 17 | tran_90 as tran_90, 18 | get_small_img as get_small_img, 19 | ) 20 | 21 | from .make_captcha.easy_mode import ( # noqa: E402 22 | show_noise_mode as show_noise_mode, 23 | show_noise_img as show_noise_img, 24 | mode_pan as mode_pan, 25 | easy_train_img as easy_train_img, 26 | get_train_img as get_train_img, 27 | img_pan as img_pan, 28 | train_img as train_img, 29 | do_captcha as do_captcha, 30 | more_noise as more_noise, 31 | get_mode as get_mode, 32 | ) 33 | 34 | from .logo.android_logo import ( # noqa: E402 35 | generate_android_icon_assets as generate_android_icon_assets, 36 | ) 37 | 38 | 39 | from .logo.ios_logo import ( # noqa: E402 40 | generate_ios_icon_assets as generate_ios_icon_assets, 41 | ) 42 | 43 | from .basic.resize_img import resize_and_save_image as resize_and_save_image # noqa: E402 44 | -------------------------------------------------------------------------------- /pycapt/basic/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aboutmydreams/pycapt/5b7777be374e58d96c871a68c38d5aa21043eca3/pycapt/basic/__init__.py -------------------------------------------------------------------------------- /pycapt/basic/appstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aboutmydreams/pycapt/5b7777be374e58d96c871a68c38d5aa21043eca3/pycapt/basic/appstore.png -------------------------------------------------------------------------------- /pycapt/basic/reset_file_type.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aboutmydreams/pycapt/5b7777be374e58d96c871a68c38d5aa21043eca3/pycapt/basic/reset_file_type.py -------------------------------------------------------------------------------- /pycapt/basic/resize_img.py: -------------------------------------------------------------------------------- 1 | from PIL import Image 2 | import os 3 | 4 | 5 | def resize_and_save_image(image_path, size, output_path): 6 | """根据给定的尺寸调整图片大小并保存到指定路径。""" 7 | try: 8 | original_image = Image.open(image_path) 9 | resized_image = original_image.resize(size, Image.LANCZOS) 10 | 11 | # 确保输出路径存在 12 | os.makedirs(os.path.dirname(output_path), exist_ok=True) 13 | 14 | resized_image.save(output_path) 15 | print(f"Saved resized image to: {output_path}") 16 | except Exception as e: 17 | print(f"Error processing image: {e}") 18 | 19 | 20 | # 示例调用 21 | if __name__ == "__main__": 22 | # 输入的图片路径 23 | current_dir = os.path.dirname(os.path.abspath(__file__)) 24 | image_path = f"{current_dir}/appstore.png" # 请替换为你的图片路径 25 | size = (100, 100) # 目标尺寸 26 | output_path = f"{current_dir}/resized_image.png" # 输出路径 27 | 28 | resize_and_save_image(image_path, size, output_path) 29 | -------------------------------------------------------------------------------- /pycapt/basic/resized_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aboutmydreams/pycapt/5b7777be374e58d96c871a68c38d5aa21043eca3/pycapt/basic/resized_image.png -------------------------------------------------------------------------------- /pycapt/logo/.gitignore: -------------------------------------------------------------------------------- 1 | output_directory 2 | Assets.xcassets -------------------------------------------------------------------------------- /pycapt/logo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aboutmydreams/pycapt/5b7777be374e58d96c871a68c38d5aa21043eca3/pycapt/logo/__init__.py -------------------------------------------------------------------------------- /pycapt/logo/android_logo.py: -------------------------------------------------------------------------------- 1 | import os 2 | from PIL import Image 3 | 4 | 5 | def generate_android_icon_assets(image_path, output_path="res"): 6 | # 定义目标文件夹和尺寸 7 | mipmap_sizes = { 8 | "mipmap-mdpi": (48, 48), # 基准 9 | "mipmap-hdpi": (72, 72), # 1.5x 10 | "mipmap-xhdpi": (96, 96), # 2.0x 11 | "mipmap-xxhdpi": (144, 144), # 3.0x 12 | "mipmap-xxxhdpi": (192, 192), # 4.0x 13 | "mipmap-xxxxhdpi": (240, 240), # 5.0x 14 | "mipmap-xxxxxhdpi": (384, 384), # 6.0x 15 | } 16 | 17 | # 打开原始图片 18 | try: 19 | original_image = Image.open(image_path) 20 | except Exception as e: 21 | print(f"Error opening image: {e}") 22 | return 23 | 24 | # 创建基文件夹 25 | os.makedirs(output_path, exist_ok=True) 26 | 27 | # 生成各个 mipmap 文件夹和缩放图片 28 | for folder, size in mipmap_sizes.items(): 29 | folder_path = os.path.join(output_path, folder) 30 | os.makedirs(folder_path, exist_ok=True) 31 | 32 | # 重新调整图片大小 33 | resized_image = original_image.resize(size, Image.LANCZOS) 34 | resized_image_path = os.path.join(folder_path, os.path.basename(image_path)) 35 | 36 | # 保存调整后的图片 37 | resized_image.save(resized_image_path) 38 | print(f"Saved resized image to: {resized_image_path}") 39 | 40 | 41 | # 示例调用 42 | if __name__ == "__main__": 43 | current_dir = os.path.dirname(os.path.abspath(__file__)) 44 | generate_android_icon_assets( 45 | f"{current_dir}/appstore.png", f"{current_dir}/output_directory" 46 | ) 47 | -------------------------------------------------------------------------------- /pycapt/logo/appstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aboutmydreams/pycapt/5b7777be374e58d96c871a68c38d5aa21043eca3/pycapt/logo/appstore.png -------------------------------------------------------------------------------- /pycapt/logo/ios_logo.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pycapt.basic.resize_img import resize_and_save_image 3 | 4 | 5 | def generate_ios_icon_assets( 6 | image_path, output_path="Assets.xcassets/AppIcon.appiconset" 7 | ): 8 | # 定义所需的图片尺寸和文件名 9 | icon_sizes = { 10 | "100.png": (100, 100), 11 | "102.png": (102, 102), 12 | "1024.png": (1024, 1024), 13 | "114.png": (114, 114), 14 | "120.png": (120, 120), 15 | "128.png": (128, 128), 16 | "144.png": (144, 144), 17 | "152.png": (152, 152), 18 | "16.png": (16, 16), 19 | "167.png": (167, 167), 20 | "172.png": (172, 172), 21 | "180.png": (180, 180), 22 | "196.png": (196, 196), 23 | "20.png": (20, 20), 24 | "216.png": (216, 216), 25 | "256.png": (256, 256), 26 | "29.png": (29, 29), 27 | "32.png": (32, 32), 28 | "40.png": (40, 40), 29 | "48.png": (48, 48), 30 | "50.png": (50, 50), 31 | "512.png": (512, 512), 32 | "55.png": (55, 55), 33 | "57.png": (57, 57), 34 | "58.png": (58, 58), 35 | "60.png": (60, 60), 36 | "64.png": (64, 64), 37 | "66.png": (66, 66), 38 | "72.png": (72, 72), 39 | "76.png": (76, 76), 40 | "80.png": (80, 80), 41 | "87.png": (87, 87), 42 | "88.png": (88, 88), 43 | "92.png": (92, 92), 44 | } 45 | 46 | # 创建输出目录 47 | os.makedirs(output_path, exist_ok=True) 48 | 49 | # 生成每个图标 50 | for filename, size in icon_sizes.items(): 51 | resized_image_path = os.path.join(output_path, filename) 52 | 53 | resize_and_save_image( 54 | image_path=image_path, size=size, output_path=resized_image_path 55 | ) 56 | # resized_image = generate_resized_image(image_path, size) 57 | # resized_image_path = os.path.join(output_path, filename) 58 | # resized_image.save(resized_image_path) 59 | # print(f"Saved resized image to: {resized_image_path}") 60 | 61 | # 创建 Contents.json 62 | create_contents_json(output_path) 63 | 64 | 65 | def create_contents_json(output_path): 66 | """创建 Contents.json 文件。""" 67 | contents = { 68 | "images": [ 69 | { 70 | "size": "60x60", 71 | "expected-size": "180", 72 | "filename": "180.png", 73 | "folder": output_path + "/", 74 | "idiom": "iphone", 75 | "scale": "3x", 76 | }, 77 | { 78 | "size": "40x40", 79 | "expected-size": "80", 80 | "filename": "80.png", 81 | "folder": output_path + "/", 82 | "idiom": "iphone", 83 | "scale": "2x", 84 | }, 85 | { 86 | "size": "40x40", 87 | "expected-size": "120", 88 | "filename": "120.png", 89 | "folder": output_path + "/", 90 | "idiom": "iphone", 91 | "scale": "3x", 92 | }, 93 | { 94 | "size": "60x60", 95 | "expected-size": "120", 96 | "filename": "120.png", 97 | "folder": output_path + "/", 98 | "idiom": "iphone", 99 | "scale": "2x", 100 | }, 101 | { 102 | "size": "57x57", 103 | "expected-size": "57", 104 | "filename": "57.png", 105 | "folder": output_path + "/", 106 | "idiom": "iphone", 107 | "scale": "1x", 108 | }, 109 | { 110 | "size": "29x29", 111 | "expected-size": "58", 112 | "filename": "58.png", 113 | "folder": output_path + "/", 114 | "idiom": "iphone", 115 | "scale": "2x", 116 | }, 117 | { 118 | "size": "29x29", 119 | "expected-size": "29", 120 | "filename": "29.png", 121 | "folder": output_path + "/", 122 | "idiom": "iphone", 123 | "scale": "1x", 124 | }, 125 | { 126 | "size": "29x29", 127 | "expected-size": "87", 128 | "filename": "87.png", 129 | "folder": output_path + "/", 130 | "idiom": "iphone", 131 | "scale": "3x", 132 | }, 133 | { 134 | "size": "57x57", 135 | "expected-size": "114", 136 | "filename": "114.png", 137 | "folder": output_path + "/", 138 | "idiom": "iphone", 139 | "scale": "2x", 140 | }, 141 | { 142 | "size": "20x20", 143 | "expected-size": "40", 144 | "filename": "40.png", 145 | "folder": output_path + "/", 146 | "idiom": "iphone", 147 | "scale": "2x", 148 | }, 149 | { 150 | "size": "20x20", 151 | "expected-size": "60", 152 | "filename": "60.png", 153 | "folder": output_path + "/", 154 | "idiom": "iphone", 155 | "scale": "3x", 156 | }, 157 | { 158 | "size": "1024x1024", 159 | "filename": "1024.png", 160 | "expected-size": "1024", 161 | "idiom": "ios-marketing", 162 | "folder": output_path + "/", 163 | "scale": "1x", 164 | }, 165 | { 166 | "size": "40x40", 167 | "expected-size": "80", 168 | "filename": "80.png", 169 | "folder": output_path + "/", 170 | "idiom": "ipad", 171 | "scale": "2x", 172 | }, 173 | { 174 | "size": "72x72", 175 | "expected-size": "72", 176 | "filename": "72.png", 177 | "folder": output_path + "/", 178 | "idiom": "ipad", 179 | "scale": "1x", 180 | }, 181 | { 182 | "size": "76x76", 183 | "expected-size": "152", 184 | "filename": "152.png", 185 | "folder": output_path + "/", 186 | "idiom": "ipad", 187 | "scale": "2x", 188 | }, 189 | { 190 | "size": "50x50", 191 | "expected-size": "100", 192 | "filename": "100.png", 193 | "folder": output_path + "/", 194 | "idiom": "ipad", 195 | "scale": "2x", 196 | }, 197 | { 198 | "size": "29x29", 199 | "expected-size": "58", 200 | "filename": "58.png", 201 | "folder": output_path + "/", 202 | "idiom": "ipad", 203 | "scale": "2x", 204 | }, 205 | { 206 | "size": "76x76", 207 | "expected-size": "76", 208 | "filename": "76.png", 209 | "folder": output_path + "/", 210 | "idiom": "ipad", 211 | "scale": "1x", 212 | }, 213 | { 214 | "size": "29x29", 215 | "expected-size": "29", 216 | "filename": "29.png", 217 | "folder": output_path + "/", 218 | "idiom": "ipad", 219 | "scale": "1x", 220 | }, 221 | { 222 | "size": "50x50", 223 | "expected-size": "50", 224 | "filename": "50.png", 225 | "folder": output_path + "/", 226 | "idiom": "ipad", 227 | "scale": "1x", 228 | }, 229 | { 230 | "size": "72x72", 231 | "expected-size": "144", 232 | "filename": "144.png", 233 | "folder": output_path + "/", 234 | "idiom": "ipad", 235 | "scale": "2x", 236 | }, 237 | { 238 | "size": "40x40", 239 | "expected-size": "40", 240 | "filename": "40.png", 241 | "folder": output_path + "/", 242 | "idiom": "ipad", 243 | "scale": "1x", 244 | }, 245 | { 246 | "size": "83.5x83.5", 247 | "expected-size": "167", 248 | "filename": "167.png", 249 | "folder": output_path + "/", 250 | "idiom": "ipad", 251 | "scale": "2x", 252 | }, 253 | { 254 | "size": "20x20", 255 | "expected-size": "20", 256 | "filename": "20.png", 257 | "folder": output_path + "/", 258 | "idiom": "ipad", 259 | "scale": "1x", 260 | }, 261 | { 262 | "size": "20x20", 263 | "expected-size": "40", 264 | "filename": "40.png", 265 | "folder": output_path + "/", 266 | "idiom": "ipad", 267 | "scale": "2x", 268 | }, 269 | { 270 | "idiom": "watch", 271 | "filename": "172.png", 272 | "folder": output_path + "/", 273 | "subtype": "38mm", 274 | "scale": "2x", 275 | "size": "86x86", 276 | "expected-size": "172", 277 | "role": "quickLook", 278 | }, 279 | { 280 | "idiom": "watch", 281 | "filename": "80.png", 282 | "folder": output_path + "/", 283 | "subtype": "38mm", 284 | "scale": "2x", 285 | "size": "40x40", 286 | "expected-size": "80", 287 | "role": "appLauncher", 288 | }, 289 | { 290 | "idiom": "watch", 291 | "filename": "88.png", 292 | "folder": output_path + "/", 293 | "subtype": "40mm", 294 | "scale": "2x", 295 | "size": "44x44", 296 | "expected-size": "88", 297 | "role": "appLauncher", 298 | }, 299 | { 300 | "idiom": "watch", 301 | "filename": "102.png", 302 | "folder": output_path + "/", 303 | "subtype": "41mm", 304 | "scale": "2x", 305 | "size": "45x45", 306 | "expected-size": "102", 307 | "role": "appLauncher", 308 | }, 309 | { 310 | "idiom": "watch", 311 | "filename": "92.png", 312 | "folder": output_path + "/", 313 | "subtype": "41mm", 314 | "scale": "2x", 315 | "size": "46x46", 316 | "expected-size": "92", 317 | "role": "appLauncher", 318 | }, 319 | { 320 | "idiom": "watch", 321 | "filename": "100.png", 322 | "folder": output_path + "/", 323 | "subtype": "44mm", 324 | "scale": "2x", 325 | "size": "50x50", 326 | "expected-size": "100", 327 | "role": "appLauncher", 328 | }, 329 | { 330 | "idiom": "watch", 331 | "filename": "196.png", 332 | "folder": output_path + "/", 333 | "subtype": "42mm", 334 | "scale": "2x", 335 | "size": "98x98", 336 | "expected-size": "196", 337 | "role": "quickLook", 338 | }, 339 | { 340 | "idiom": "watch", 341 | "filename": "216.png", 342 | "folder": output_path + "/", 343 | "subtype": "44mm", 344 | "scale": "2x", 345 | "size": "108x108", 346 | "expected-size": "216", 347 | "role": "quickLook", 348 | }, 349 | { 350 | "idiom": "watch", 351 | "filename": "48.png", 352 | "folder": output_path + "/", 353 | "subtype": "38mm", 354 | "scale": "2x", 355 | "size": "24x24", 356 | "expected-size": "48", 357 | "role": "notificationCenter", 358 | }, 359 | { 360 | "idiom": "watch", 361 | "filename": "55.png", 362 | "folder": output_path + "/", 363 | "subtype": "42mm", 364 | "scale": "2x", 365 | "size": "27.5x27.5", 366 | "expected-size": "55", 367 | "role": "notificationCenter", 368 | }, 369 | { 370 | "idiom": "watch", 371 | "filename": "66.png", 372 | "folder": output_path + "/", 373 | "subtype": "45mm", 374 | "scale": "2x", 375 | "size": "33x33", 376 | "expected-size": "66", 377 | "role": "notificationCenter", 378 | }, 379 | { 380 | "size": "29x29", 381 | "expected-size": "87", 382 | "filename": "87.png", 383 | "folder": output_path + "/", 384 | "idiom": "watch", 385 | "role": "companionSettings", 386 | "scale": "3x", 387 | }, 388 | { 389 | "size": "29x29", 390 | "expected-size": "58", 391 | "filename": "58.png", 392 | "folder": output_path + "/", 393 | "idiom": "watch", 394 | "role": "companionSettings", 395 | "scale": "2x", 396 | }, 397 | { 398 | "size": "1024x1024", 399 | "expected-size": "1024", 400 | "filename": "1024.png", 401 | "folder": output_path + "/", 402 | "idiom": "watch-marketing", 403 | "scale": "1x", 404 | }, 405 | { 406 | "size": "128x128", 407 | "expected-size": "128", 408 | "filename": "128.png", 409 | "folder": output_path + "/", 410 | "idiom": "mac", 411 | "scale": "1x", 412 | }, 413 | { 414 | "size": "256x256", 415 | "expected-size": "256", 416 | "filename": "256.png", 417 | "folder": output_path + "/", 418 | "idiom": "mac", 419 | "scale": "1x", 420 | }, 421 | { 422 | "size": "128x128", 423 | "expected-size": "256", 424 | "filename": "256.png", 425 | "folder": output_path + "/", 426 | "idiom": "mac", 427 | "scale": "2x", 428 | }, 429 | { 430 | "size": "256x256", 431 | "expected-size": "512", 432 | "filename": "512.png", 433 | "folder": output_path + "/", 434 | "idiom": "mac", 435 | "scale": "2x", 436 | }, 437 | { 438 | "size": "32x32", 439 | "expected-size": "32", 440 | "filename": "32.png", 441 | "folder": output_path + "/", 442 | "idiom": "mac", 443 | "scale": "1x", 444 | }, 445 | { 446 | "size": "512x512", 447 | "expected-size": "512", 448 | "filename": "512.png", 449 | "folder": output_path + "/", 450 | "idiom": "mac", 451 | "scale": "1x", 452 | }, 453 | { 454 | "size": "16x16", 455 | "expected-size": "16", 456 | "filename": "16.png", 457 | "folder": output_path + "/", 458 | "idiom": "mac", 459 | "scale": "1x", 460 | }, 461 | { 462 | "size": "16x16", 463 | "expected-size": "32", 464 | "filename": "32.png", 465 | "folder": output_path + "/", 466 | "idiom": "mac", 467 | "scale": "2x", 468 | }, 469 | { 470 | "size": "32x32", 471 | "expected-size": "64", 472 | "filename": "64.png", 473 | "folder": output_path + "/", 474 | "idiom": "mac", 475 | "scale": "2x", 476 | }, 477 | { 478 | "size": "512x512", 479 | "expected-size": "1024", 480 | "filename": "1024.png", 481 | "folder": output_path + "/", 482 | "idiom": "mac", 483 | "scale": "2x", 484 | }, 485 | ] 486 | } 487 | 488 | # 写入 Contents.json 文件 489 | json_path = os.path.join(output_path, "Contents.json") 490 | with open(json_path, "w") as json_file: 491 | import json 492 | 493 | json.dump(contents, json_file, indent=4) 494 | print(f"Created Contents.json at: {json_path}") 495 | 496 | 497 | # 示例调用 498 | if __name__ == "__main__": 499 | current_dir = os.path.dirname(os.path.abspath(__file__)) 500 | generate_ios_icon_assets( 501 | f"{current_dir}/appstore.png", 502 | f"{current_dir}/Assets.xcassets/AppIcon.appiconset", 503 | ) 504 | -------------------------------------------------------------------------------- /pycapt/make_captcha/OpenSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aboutmydreams/pycapt/5b7777be374e58d96c871a68c38d5aa21043eca3/pycapt/make_captcha/OpenSans-Bold.ttf -------------------------------------------------------------------------------- /pycapt/make_captcha/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aboutmydreams/pycapt/5b7777be374e58d96c871a68c38d5aa21043eca3/pycapt/make_captcha/__init__.py -------------------------------------------------------------------------------- /pycapt/make_captcha/easy_mode.py: -------------------------------------------------------------------------------- 1 | from pycapt.make_captcha.noise import more_noise 2 | from pycapt.make_captcha.make_capt import get_modes, img_pan, train_img, mode_to_img 3 | from pycapt.make_captcha.my_any_img import mk_captcha 4 | import time 5 | import random 6 | 7 | 8 | # 图像转化为01 np数组 Threshold为阀值 9 | def get_mode(img, Threshold=100): 10 | mode = get_modes(img, Threshold) 11 | return mode 12 | 13 | 14 | # 将np数组噪点处理,mode 数组,N是加噪率(边缘存在1个以上的黑点那么这个点有一点概率变黑) Z 加噪次数 返回数组 15 | def show_noise_mode(mode, N, Z): 16 | new_mode = more_noise(mode, N, Z) 17 | return new_mode 18 | 19 | 20 | # 将np数组噪点处理,mode 数组,N是加噪率(边缘存在1个以上的黑点那么这个点有一点概率变黑) Z 加噪次数 返回图片 21 | def show_noise_img(mode, N, Z): 22 | new_img = more_noise(mode, N, Z, to_img="toimg") 23 | return new_img 24 | 25 | 26 | # 偏移 传入np数组,横向偏移(默认右移),纵向偏移,传出新的mode 27 | def mode_pan(mode, width_x, height_y): 28 | new_mode = img_pan(mode, width_x, height_y) 29 | return new_mode 30 | 31 | 32 | # 生成验证码 长宽 字符串个数 背景颜色 一般要上线用的话看源码改一改就好了 33 | def make_capt_img( 34 | my_str_list, 35 | width, 36 | height, 37 | num_of_str, 38 | gray_value=255, 39 | font_family="OpenSans-Bold.ttf", 40 | ): 41 | str_list, image = mk_captcha( 42 | my_str_list, width, height, num_of_str, gray_value, font_family 43 | ) 44 | return str_list, image 45 | 46 | 47 | # 生成简单的大写字母训练集图片 48 | def get_train_img( 49 | width, height, num_of_str=1, xpan=3, ypan=2, rotate=15, gray_value=255 50 | ): 51 | file_name, image = train_img( 52 | width, height, num_of_str, xpan, ypan, rotate, gray_value 53 | ) 54 | return file_name, image 55 | 56 | 57 | def easy_train_img( 58 | my_str_list, 59 | width, 60 | height, 61 | num_of_str=1, 62 | font=30, 63 | xpan=3, 64 | ypan=2, 65 | rotate=15, 66 | noise_N=0.3, 67 | noise_Z=2, 68 | gray_value=255, 69 | font_family="OpenSans-Bold.ttf", 70 | ): 71 | char_list, image = do_captcha( 72 | my_str_list, width, height, num_of_str, font, gray_value, font_family 73 | ) 74 | file_name = ( 75 | "".join(char_list) 76 | + "-" 77 | + str(time.time())[-10:-3].replace(".", str(random.random())[2:4]) 78 | ) 79 | # image.show() 80 | # 在这里增加难度与异动 81 | mode = get_mode(image, 100) 82 | # 偏移 83 | mode = mode_pan(mode, random.randint(-xpan, xpan), random.randint(-ypan, ypan)) 84 | # 添加噪点 85 | mode = show_noise_mode(mode, noise_N, noise_Z) 86 | image = mode_to_img(mode, 255) 87 | # 旋转 88 | image = image.rotate( 89 | random.randint(-rotate, rotate), fillcolor=(gray_value, gray_value, gray_value) 90 | ) 91 | # print(image.size) 92 | # image.save('train_imgs/{}.png'.format(file_name)) 93 | return file_name, image 94 | 95 | 96 | def auto_more_noise(img, N=0.3, Z=2): 97 | """添加更多边缘噪点,返回处理后的图片。""" 98 | return show_noise_img(img, N, Z) 99 | 100 | 101 | def do_captcha( 102 | my_str_list, 103 | width, 104 | height, 105 | num_of_str, 106 | font=30, 107 | gray_value=255, 108 | font_family="OpenSans-Bold.ttf", 109 | ): 110 | """生成验证码。""" 111 | return mk_captcha( 112 | my_str_list, width, height, num_of_str, font, gray_value, font_family 113 | ) 114 | 115 | 116 | # 自定义生成训练图片 117 | # def my_train_img(): 118 | -------------------------------------------------------------------------------- /pycapt/make_captcha/make_capt.py: -------------------------------------------------------------------------------- 1 | from PIL import Image, ImageDraw, ImageFont 2 | from pycapt.make_captcha.noise import more_noise 3 | 4 | import numpy as np 5 | import random 6 | import time 7 | import os 8 | 9 | 10 | # 随机字母: 11 | def rndChar(): 12 | return chr(random.randint(65, 90)) 13 | 14 | 15 | # 随机颜色1: 16 | def rndColor(): 17 | return (random.randint(64, 255), random.randint(64, 255), random.randint(64, 255)) 18 | 19 | 20 | # 随机颜色2: 21 | def rndColor2(): 22 | return (random.randint(32, 127), random.randint(32, 127), random.randint(32, 127)) 23 | 24 | 25 | # 240 x 60: 26 | # width = 60 * 4 27 | # height = 60 28 | 29 | 30 | def get_captcha(width, height, num_of_str, gray_value=255): 31 | image = Image.new("RGB", (width, height), (255, 255, 255)) 32 | 33 | # 创建Font对象: 34 | current_dir = os.path.dirname(os.path.abspath(__file__)) 35 | font = ImageFont.truetype(f"{current_dir}/OpenSans-Bold.ttf", 31) 36 | 37 | # 创建Draw对象: 38 | draw = ImageDraw.Draw(image) 39 | 40 | # 填充每个像素: 41 | for x in range(width): 42 | for y in range(height): 43 | # draw.point((x, y), fill=rndColor()) 44 | draw.point((x, y), fill=(255, 255, 255)) 45 | 46 | # 输出文字: 47 | char_list = [rndChar() for i in range(num_of_str)] 48 | 49 | for t in range(num_of_str): 50 | # rndColor2() 51 | draw.text((height * t, 1), char_list[t], font=font, fill=(0, 0, 0)) 52 | 53 | # 模糊: 54 | # image = image.filter(ImageFilter.BLUR) 55 | # image.save('train_imgs/1.png', 'jpeg'); 56 | return char_list, image 57 | 58 | 59 | def get_modes(img, Threshold=100): 60 | img = img.convert("L") 61 | mode = np.array(img) 62 | mode = np.where(mode < Threshold, 0, 1) 63 | return mode 64 | 65 | 66 | def mode_to_img(mode, background=None): 67 | if background: 68 | mode = np.where(mode < 1, 0, background) 69 | array_mode = np.array(mode).astype("uint8") 70 | image = Image.fromarray(array_mode).convert("RGB") 71 | return image 72 | 73 | 74 | # 偏移 传入np数组,横向偏移(默认右移),纵向偏移,传出新的mode 75 | def img_pan(mode, width_x, height_y): 76 | new_mode = mode.copy() 77 | if width_x != 0: 78 | new_mode[:, :width_x] = mode[:, -width_x:] 79 | new_mode[:, width_x:] = mode[:, :-width_x] 80 | new_mode1 = new_mode.copy() 81 | if height_y != 0: 82 | new_mode1[:height_y, :] = new_mode[-height_y:, :] 83 | new_mode1[height_y:, :] = new_mode[:-height_y, :] 84 | return new_mode1 85 | 86 | 87 | ### 测试 88 | # img = Image.open('1.png') 89 | # mode = get_modes(img) 90 | # mode = img_pan(mode,50,10) 91 | # img = mode_to_img(mode,255) 92 | # img.show() 93 | 94 | 95 | # 生成训练集图片 96 | def train_img(width, height, num_of_str=1, xpan=3, ypan=2, rotate=15, gray_value=255): 97 | char_list, image = get_captcha(width, height, num_of_str, gray_value) 98 | file_name = ( 99 | char_list[0] 100 | + "-" 101 | + str(time.time())[-10:-3].replace(".", str(random.random())[2:4]) 102 | ) 103 | # image.show() 104 | # 在这里增加难度与异动 105 | mode = get_modes(image, 100) 106 | # 偏移 107 | mode = img_pan(mode, random.randint(-xpan, xpan), random.randint(-ypan, ypan)) 108 | # 添加噪点 109 | image = more_noise(mode, N=0.3, Z=2, to_img="to_img") 110 | # 旋转 111 | image = image.rotate(random.randint(-rotate, rotate), fillcolor=255) 112 | # print(image.size) 113 | # image.save('train_imgs/{}.png'.format(file_name)) 114 | return file_name, image 115 | -------------------------------------------------------------------------------- /pycapt/make_captcha/my_any_img.py: -------------------------------------------------------------------------------- 1 | from PIL import Image, ImageDraw, ImageFont 2 | import random 3 | import os 4 | 5 | 6 | # 随机字母: 7 | def rndChar(): 8 | return chr(random.randint(65, 90)) 9 | 10 | 11 | # 随机颜色1: 12 | def rndColor(): 13 | return (random.randint(64, 255), random.randint(64, 255), random.randint(64, 255)) 14 | 15 | 16 | # 随机颜色2: 17 | def rndColor2(): 18 | return (random.randint(32, 127), random.randint(32, 127), random.randint(32, 127)) 19 | 20 | 21 | # 随机自定义字符 22 | def my_random_str(my_str_list, n): 23 | return random.sample(my_str_list, n) 24 | 25 | 26 | def mk_captcha( 27 | my_str_list, 28 | width, 29 | height, 30 | num_of_str, 31 | font=30, 32 | gray_value=255, 33 | font_family=None, 34 | ): 35 | image = Image.new("RGB", (width, height), (255, 255, 255)) 36 | 37 | # 创建Font对象: 38 | if font_family is None or font_family == "OpenSans-Bold.ttf": 39 | # 创建Font对象: 40 | current_dir = os.path.dirname(os.path.abspath(__file__)) 41 | font = ImageFont.truetype(f"{current_dir}/OpenSans-Bold.ttf", 31) 42 | else: 43 | font = ImageFont.truetype(font_family, font) 44 | 45 | # 创建Draw对象: 46 | draw = ImageDraw.Draw(image) 47 | 48 | # 填充每个像素: 49 | for x in range(width): 50 | for y in range(height): 51 | # draw.point((x, y), fill=rndColor()) 52 | draw.point((x, y), fill=(255, 255, 255)) 53 | 54 | # 输出文字: 55 | char_list = my_random_str(my_str_list, num_of_str) 56 | 57 | for t in range(num_of_str): 58 | # rndColor2() 59 | draw.text((height * t, 1), char_list[t], font=font, fill=(0, 0, 0)) 60 | 61 | # 模糊: 62 | # image = image.filter(ImageFilter.BLUR) 63 | # image.save('train_imgs/1.png', 'jpeg'); 64 | return char_list, image 65 | 66 | 67 | ### 测试 68 | # a, b = mk_captcha(["A", "B", "1", "2", "3", "4"], 160, 40, 4) 69 | # print(a) 70 | # b.show() 71 | -------------------------------------------------------------------------------- /pycapt/make_captcha/noise.py: -------------------------------------------------------------------------------- 1 | from PIL import Image, ImageDraw 2 | from collections import Counter 3 | import random 4 | 5 | 6 | # 将np数组转化成01 dic 后面会废弃 7 | def mode_to_dic(mode): 8 | dic = {} 9 | for line in range(0, mode.shape[1]): 10 | for i in range(0, mode.shape[0]): 11 | dic[(i, line)] = mode[i, line] 12 | return dic 13 | 14 | 15 | # 将01 np数组转化为黑白图片 16 | def mode_to_draw(mode): 17 | image = Image.new("1", (mode.shape[1], mode.shape[0])) 18 | draw = ImageDraw.Draw(image) 19 | for x in range(0, mode.shape[0]): 20 | for y in range(0, mode.shape[1]): 21 | draw.point((y, x), int(mode[x, y])) 22 | return image 23 | 24 | 25 | # 根据一个点A的RGB值,与周围的8个点的RBG值比较,周围有大于1个不同的,那么考虑一定概率增加噪点 26 | # G: Integer 图像二值化阀值 27 | # N: Integer 加噪率 0 < N < 1 28 | # Z: Integer 加噪次数 29 | 30 | 31 | def more_noise(mode, N, Z, to_img=None): 32 | # 0和1互相转换 33 | def one_zero(num): 34 | if num == 1: 35 | return 0 36 | else: 37 | return 1 38 | 39 | # 二值数组 40 | img_dic = mode_to_dic(mode) 41 | 42 | for i in range(0, Z): 43 | img_dic[(0, 0)] = 1 44 | img_dic[(mode.shape[0] - 1, mode.shape[1] - 1)] = 1 45 | for x in range(1, mode.shape[0] - 1): 46 | for y in range(1, mode.shape[1] - 1): 47 | random_num = random.random() 48 | # 0或1 49 | L = img_dic[(x, y)] 50 | # 统计临近8个点是0还是1 51 | near8 = [ 52 | img_dic[(x - 1, y - 1)], 53 | img_dic[(x - 1, y)], 54 | img_dic[(x - 1, y + 1)], 55 | img_dic[(x, y - 1)], 56 | img_dic[(x, y + 1)], 57 | img_dic[(x + 1, y - 1)], 58 | img_dic[(x + 1, y)], 59 | img_dic[(x + 1, y + 1)], 60 | ] 61 | # data 计算0黑点数与 1白点数 62 | data = Counter(near8) 63 | # 一样的点小于8 64 | if (data[L] < 8) and random_num < N: 65 | # img_dic[(x, y)] = one_zero(L) 66 | # mode[x,y] = one_zero(L) 67 | mode[x, y] = 0 68 | # print(mode.shape) 69 | if to_img: 70 | image = mode_to_draw(mode) 71 | # print(image.size) 72 | return image 73 | else: 74 | return mode 75 | -------------------------------------------------------------------------------- /pycapt/solve_it/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aboutmydreams/pycapt/5b7777be374e58d96c871a68c38d5aa21043eca3/pycapt/solve_it/1.png -------------------------------------------------------------------------------- /pycapt/solve_it/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aboutmydreams/pycapt/5b7777be374e58d96c871a68c38d5aa21043eca3/pycapt/solve_it/__init__.py -------------------------------------------------------------------------------- /pycapt/solve_it/cut_img.py: -------------------------------------------------------------------------------- 1 | from PIL import Image 2 | import numpy as np 3 | import heapq 4 | 5 | 6 | # np.set_printoptions(threshold=np.inf) 7 | def mode_to_img(mode, background=None): 8 | if background: 9 | mode = np.where(mode < 1, 0, background) 10 | array_mode = np.array(mode).astype("uint8") 11 | image = Image.fromarray(array_mode).convert("RGB") 12 | return image 13 | 14 | 15 | def get_modes(img, Threshold=100): 16 | img = img.convert("L") 17 | mode = np.array(img) 18 | mode = np.where(mode < Threshold, 0, 1) 19 | return mode 20 | 21 | 22 | def get_small_modes(img, box=(10, 4, 150, 36), background=None): 23 | img = img.convert("L") 24 | img1 = img.crop(box) 25 | mode = np.array(img1) 26 | if background: 27 | mode = np.where(mode < 100, 0, background) 28 | else: 29 | mode = np.where(mode < 100, 0, 1) 30 | return mode 31 | 32 | 33 | def get_small_img(img, box=(10, 4, 150, 36), background=None): 34 | img = img.convert("L") 35 | img1 = img.crop(box) 36 | mode = np.array(img1) 37 | if background: 38 | mode = np.where(mode < 100, 0, background) 39 | else: 40 | mode = np.where(mode < 100, 0, 1) 41 | if background: 42 | return mode_to_img(mode, background) 43 | else: 44 | return mode_to_img(mode) 45 | 46 | 47 | def cut_mode(mode, max_width, need_num=4): 48 | ### 获取边缘列(字母边缘 白色)位置的列表 49 | 50 | # 转置矩阵 51 | mode = mode.T 52 | # 第一行和最后一行变1 白 53 | mode[[0, -1]] = 1 54 | mode = mode.tolist() 55 | # 边缘行列表 由数学知识可知道 边缘一定是成双的 56 | bian_yuan = [] 57 | all_num_list = list(range(len(mode) - 1)) 58 | for k, line in enumerate(mode[:-1]): 59 | is_one_num = list(set(line)) 60 | if is_one_num == [1]: 61 | all_num_list.remove(k) 62 | if (k - 1) in all_num_list: 63 | bian_yuan.append(k) 64 | if (is_one_num == [0, 1]) and ((k - 1) not in all_num_list): 65 | bian_yuan.append(k - 1) 66 | # print('边缘:',bian_yuan) 67 | ### 边缘列表俩俩组合成新的列表 68 | black_list = [] 69 | len_list = [] 70 | for k, i in enumerate(bian_yuan[::2]): 71 | alpha_list = list(range(i, bian_yuan[(k + 1) * 2 - 1] + 1)) 72 | # 判断长度大于等于4 73 | if len(alpha_list) > 3: 74 | black_list.append(alpha_list) 75 | len_list.append(len(alpha_list)) 76 | 77 | # print(len_list) 78 | ### 列表长度大于n 删除最小的几个,判断太长的 平均分 79 | # listTemp 为列表 平分后每份列表的的个数n 80 | def average_func(m, n): 81 | f = False 82 | s = len(m) // n 83 | lef = len(m) % n 84 | lop = 0 85 | stopat = 0 86 | if lef != 0: 87 | s += 1 88 | f = True 89 | ret = [] 90 | if f: 91 | for i in range(lef): 92 | ret.append(m[i * s : (i + 1) * s]) 93 | stopat = i * s + 1 94 | lop = i 95 | s -= 1 96 | for i in range(1, n - lop): 97 | ret.append(m[stopat + i * s : stopat + (i + 1) * s]) 98 | return ret 99 | else: 100 | for i in range(n): 101 | ret.append(m[i * s : (i + 1) * s]) 102 | return ret 103 | 104 | need_list = [] 105 | if len(black_list) > need_num: 106 | pass 107 | max_num_index_list = map(len_list.index, heapq.nlargest(need_num, len_list)) 108 | for i in max_num_index_list: 109 | need_list.append(black_list[i]) 110 | elif len(black_list) < need_num: 111 | for k, black in enumerate(black_list): 112 | if float(max_width) * 2.4 > len(black) > float(max_width) * 1.25: 113 | real_list = list(average_func(black, 2)) 114 | real_list.reverse() 115 | black_list.remove(black) 116 | for i in real_list: 117 | black_list.insert(k, i) 118 | elif float(max_width) * 3.4 > len(black) >= float(max_width) * 2.4: 119 | real_list = list(average_func(black, 3)) 120 | real_list.reverse() 121 | black_list.remove(black) 122 | for i in real_list: 123 | black_list.insert(k, i) 124 | elif len(black) >= float(max_width) * 3.4: 125 | real_list = list(average_func(black, need_num - len(black_list) + 1)) 126 | real_list.reverse() 127 | black_list.remove(black) 128 | for i in real_list: 129 | black_list.insert(k, i) 130 | 131 | mode_list = [] 132 | # print(black_list) 133 | for i in black_list: 134 | new_mode = np.array(mode)[i].T 135 | mode_list.append(new_mode) 136 | return mode_list 137 | 138 | 139 | def cut_img(img, max_width): 140 | my_mode = get_modes(img) 141 | 142 | # img2 = mode_to_img(my_mode,255) 143 | # img2.show() 144 | 145 | li = cut_mode(my_mode, max_width=30) 146 | for k, i in enumerate(li): 147 | its_height = np.shape(i)[0] 148 | its_width = np.shape(i)[1] 149 | # 当长度超过预计时,切割中间预计的部分 150 | if its_width > max_width: 151 | d = its_width - max_width 152 | li[k] = i[:, int(d / 2) : -int(d / 2) - d % 2] 153 | # 当长度小于预计的时候,两边填充全为1的列 154 | if its_width < max_width: 155 | d = max_width - its_width 156 | if d == 1: 157 | one_column1 = np.ones(its_height).reshape(its_height, 1).astype("uint8") 158 | li[k] = np.hstack((i, one_column1)) 159 | else: 160 | one_column = ( 161 | np.ones(its_height * int(d / 2)) 162 | .reshape(its_height, int(d / 2)) 163 | .astype("uint8") 164 | ) 165 | one_column1 = ( 166 | np.ones(its_height * (int(d / 2) + d % 2)) 167 | .reshape(its_height, int(d / 2) + d % 2) 168 | .astype("uint8") 169 | ) 170 | # print(i.shape) 171 | # print(one_column.shape) 172 | new_mode = np.hstack((one_column, i)) 173 | li[k] = np.hstack((new_mode, one_column1)) 174 | 175 | # img = mode_to_img(i,255) 176 | # img.show() 177 | return li 178 | 179 | 180 | ### 测试 181 | # import de_point,de_line 182 | # img = Image.open('RHLIN-45884951.png') 183 | # img = de_point.clear_noise(img, N=2, Z=1) 184 | # img = de_line.clear_my_line(img) 185 | 186 | # cut_img(img, 30) 187 | # img.show() 188 | -------------------------------------------------------------------------------- /pycapt/solve_it/de_line.py: -------------------------------------------------------------------------------- 1 | from PIL import Image 2 | import numpy as np 3 | 4 | # 保证所有数据能够显示,而不是用省略号表示 5 | np.set_printoptions(threshold=np.inf) 6 | 7 | 8 | # 将图片转化为数组 这里会预先灰度化 9 | def get_modes(img, Threshold=100): 10 | img = img.convert("L") 11 | mode = np.array(img) 12 | mode = np.where(mode < Threshold, 0, 1) 13 | return mode 14 | 15 | 16 | # 左平移 D为正,右平移,D为负,左平移 17 | def pan(line, D): 18 | if D == 0: 19 | return line 20 | else: 21 | line = line[D:] + line[:D] 22 | return line 23 | 24 | 25 | # N 干扰线纵向像素点个数 26 | def clear_line(image, N, pans=None): 27 | mode = get_modes(image) 28 | new_mode = [] 29 | for line in mode.T: 30 | new_column = is_three0(line, N) 31 | new_mode.append(new_column) 32 | 33 | new_mode = eval(str(new_mode).replace("1", "255").replace("0", "0")) 34 | 35 | array_mode = np.array(new_mode).T.astype("uint8") 36 | if pans: 37 | new_mode = [] 38 | for k, line in enumerate(array_mode.tolist()): 39 | line = pan(line, pans[k]) 40 | new_mode.append(line) 41 | array_mode = np.array(new_mode).astype("uint8") 42 | image = Image.fromarray(array_mode).convert("RGB") 43 | return image 44 | 45 | 46 | # 判断列表中连续的三个位置是否是0,且相邻位置是1,替换掉这3个0 47 | def is_three0(column, N): 48 | if N == 0: 49 | return column 50 | else: 51 | column_str = "".join(map(str, column)) 52 | zero_site_list = [i for i, v in enumerate(column) if v == 0] 53 | 54 | for i in zero_site_list[-N:]: 55 | if i > len(column) - N - 1: 56 | zero_site_list.remove(i) 57 | 58 | for i in zero_site_list: 59 | if ( 60 | i > 0 61 | and column_str[i : i + N] == "0" * N 62 | and column_str[i + N] == "1" 63 | and column_str[i - 1] == "1" 64 | ): 65 | column_str = column_str[:i] + "1" * N + column_str[i + N :] 66 | column = list(map(int, column_str)) 67 | return column 68 | 69 | 70 | # 处理真实图片 71 | def clear_my_line(img): 72 | panD_list = [ 73 | 18, 74 | 18, 75 | 18, 76 | 18, 77 | 17, 78 | 17, 79 | 17, 80 | 16, 81 | 16, 82 | 16, 83 | 15, 84 | 15, 85 | 15, 86 | 15, 87 | 14, 88 | 14, 89 | 14, 90 | 14, 91 | 13, 92 | 13, 93 | 10, 94 | 10, 95 | 10, 96 | 9, 97 | 9, 98 | 8, 99 | 7, 100 | 6, 101 | 5, 102 | 5, 103 | 4, 104 | 4, 105 | 4, 106 | 4, 107 | 4, 108 | 3, 109 | 1, 110 | 0, 111 | 0, 112 | 0, 113 | ] 114 | img2 = clear_line(img, 4) 115 | img2 = clear_line(img2, 3, panD_list) 116 | img2 = clear_line(img2, 4) 117 | img2 = clear_line(img2, 3) 118 | img2 = clear_line(img2, 2) 119 | img2 = clear_line(img2, 1) 120 | return img2 121 | 122 | 123 | # 清理训练集 124 | def clear_my_train_img(img): 125 | # panD_list = [18, 18, 18, 18, 17, 17, 17, 16, 16, 16, 15, 15, 15, 15, 14, 14, 14, 14, 13, 13, 10, 10, 10, 9, 9, 8, 7, 6, 5, 5, 4, 4, 4, 4, 4, 3, 1, 0, 0, 0] 126 | img2 = clear_line(img, 4) 127 | img2 = clear_line(img2, 3) 128 | img2 = clear_line(img2, 2) 129 | img2 = clear_line(img2, 1) 130 | return img2 131 | 132 | 133 | # 斜体图片纠正 134 | def rectify_img(image, pans): 135 | mode = get_modes(image) 136 | new_mode = [] 137 | for k, line in enumerate(mode.tolist()): 138 | line = pan(line, pans[k]) 139 | new_mode.append(line) 140 | new_mode = np.array(new_mode) 141 | new_mode = np.where(new_mode < 1, 0, 255) 142 | array_mode = np.array(new_mode).astype("uint8") 143 | image = Image.fromarray(array_mode).convert("RGB") 144 | return image 145 | 146 | 147 | ### 测试: 148 | """ 149 | img = Image.open('1.png') 150 | pan0 = [18, 18, 18, 18, 17, 17, 17,\ 151 | 16, 16, 16, 15, 15, 15, 15, 14,\ 152 | 14, 14, 14, 13, 13, 10, 10,\ 153 | 10, 9, 9, 8, 7, 6, 5, 5, 4, \ 154 | 4, 4, 4, 4, 3, 1, 0, 0, 0] 155 | 156 | def contrast(num): 157 | return -num 158 | 159 | pan1 = list(map(contrast,pan0)) 160 | print(pan) 161 | img = rectify_img(img,pans=pan1) 162 | img.show() 163 | """ 164 | 165 | 166 | # 斜体矩阵纠正 167 | def rectify_mode(mode, pans): 168 | new_mode = [] 169 | for k, line in enumerate(mode.tolist()): 170 | line = pan(line, pans[k]) 171 | new_mode.append(line) 172 | array_mode = np.array(new_mode).astype("uint8") 173 | return array_mode 174 | -------------------------------------------------------------------------------- /pycapt/solve_it/de_point.py: -------------------------------------------------------------------------------- 1 | # 去除噪点 2 | from PIL import Image, ImageDraw 3 | from collections import Counter 4 | import numpy as np 5 | 6 | 7 | # 根据一个点 A 的 RGB 值,与周围的 8 个点的 RBG 值比较,设定一个值N(0 < N < 8),当 A 的 RGB 值与周围 8 个点的 RGB 相等数小于 N 时,此点为噪点 8 | # G: Integer 图像二值化阀值 9 | # N: Integer 降噪率 0 < N < 8 10 | # Z: Integer 降噪次数 11 | # 输出 12 | # 0:降噪成功 13 | # 1:降噪失败 14 | 15 | 16 | def two_value(image, G): 17 | # 二值数组 18 | image = image.convert("L") 19 | img_dic = {} 20 | for y in range(0, image.size[1]): 21 | for x in range(0, image.size[0]): 22 | g = image.getpixel((x, y)) 23 | if g > G: 24 | img_dic[(x, y)] = 1 25 | else: 26 | img_dic[(x, y)] = 0 27 | return img_dic 28 | 29 | 30 | def get_modes(img, Threshold=100): 31 | img = img.convert("L") 32 | mode = np.array(img) 33 | mode = np.where(mode < Threshold, 0, 1) 34 | return mode 35 | 36 | 37 | def mode_to_img(mode, background=None): 38 | if background: 39 | mode = np.where(mode < 1, 0, background) 40 | array_mode = np.array(mode).astype("uint8") 41 | image = Image.fromarray(array_mode).convert("RGB") 42 | return image 43 | 44 | 45 | def clear_noise(image, N, Z): 46 | # 0和1互相转换 47 | def one_zero(num): 48 | if num == 1: 49 | return 0 50 | else: 51 | return 1 52 | 53 | # 二值数组 54 | image = image.convert("L") 55 | img_dic = two_value(image, 100) 56 | mode = get_modes(image) 57 | for i in range(0, Z): 58 | img_dic[(0, 0)] = 1 59 | img_dic[(image.size[0] - 1, image.size[1] - 1)] = 1 60 | 61 | for x in range(1, image.size[0] - 1): 62 | for y in range(1, image.size[1] - 1): 63 | L = img_dic[(x, y)] # 0或1 64 | # 统计临近8个点是0还是1 65 | near8 = [ 66 | img_dic[(x - 1, y - 1)], 67 | img_dic[(x - 1, y)], 68 | img_dic[(x - 1, y + 1)], 69 | img_dic[(x, y - 1)], 70 | img_dic[(x, y + 1)], 71 | img_dic[(x + 1, y - 1)], 72 | img_dic[(x + 1, y)], 73 | img_dic[(x + 1, y + 1)], 74 | ] 75 | # data 计算0黑点数与 1白点数 76 | data = Counter(near8) 77 | if data[L] < N: 78 | # img_dic[(x, y)] = one_zero(L) 79 | mode[y, x] = one_zero(L) 80 | return mode_to_img(mode, 255) 81 | 82 | 83 | def save_img(filename, size, img_dic): 84 | image = Image.new("1", size) 85 | draw = ImageDraw.Draw(image) 86 | 87 | for x in range(0, size[0]): 88 | for y in range(0, size[1]): 89 | draw.point((x, y), img_dic[(x, y)]) 90 | 91 | image.save(filename) 92 | 93 | 94 | # for i in range(1,2): 95 | # path = str(i) + ".png" 96 | # image = Image.open(path).convert("L") 97 | # img_dic = clear_noise(image, 2, 1) 98 | # print(img_dic) 99 | # path1 = str(i) + ".jpeg" 100 | # save_img(path1, image.size, img_dic) 101 | -------------------------------------------------------------------------------- /pycapt/solve_it/easy_solve.py: -------------------------------------------------------------------------------- 1 | from pycapt.solve_it.cut_img import get_modes, mode_to_img, cut_img, get_small_modes 2 | from pycapt.solve_it.de_line import ( 3 | clear_line, 4 | clear_my_line, 5 | clear_my_train_img, 6 | rectify_img as rectify_img, 7 | ) 8 | from pycapt.solve_it.de_point import clear_noise 9 | 10 | 11 | def mode_img(mode, background=None): 12 | img = mode_to_img(mode, background) 13 | return img 14 | 15 | 16 | # 将01数组转化为黑白图片 17 | def mode_white_img(mode): 18 | img = mode_img(mode, background=255) 19 | return img 20 | 21 | 22 | # 二值化图片,变为黑白 23 | def two_value(img, Threshold=100): 24 | mode = get_modes(img, Threshold) 25 | return mode_img(mode, background=255) 26 | 27 | 28 | def dele_noise(image, N, Z): 29 | img = clear_noise(image, N, Z) 30 | return img 31 | 32 | 33 | def dele_line(image, N, pans=None): 34 | if pans: 35 | img = clear_line(image, N, pans) 36 | return img 37 | else: 38 | img = clear_line(image, N, pans=None) 39 | return img 40 | 41 | 42 | def clear_train_img(image): 43 | img = clear_my_train_img(image) 44 | return img 45 | 46 | 47 | def clear_lib_line(image): 48 | img = clear_my_line(image) 49 | return img 50 | 51 | 52 | def rectify_mode(mode, pans): 53 | return rectify_mode(mode, pans) 54 | 55 | 56 | def cut_img_to_mode_list(image, max_width): 57 | img_mode_list = cut_img(image, max_width) 58 | return img_mode_list 59 | 60 | 61 | def get_small_img(img, box=(10, 4, 150, 36), background=None): 62 | if background: 63 | return get_small_modes(img, box, background) 64 | else: 65 | return get_small_modes(img, box, background=None) 66 | 67 | 68 | def cut_img_to_img_list(image, max_width, background=None): 69 | if background: 70 | mode_list = cut_img(image, max_width) 71 | img_list = map(mode_white_img, mode_list) 72 | return img_list 73 | else: 74 | img_mode_list = cut_img(image, max_width) 75 | return map(mode_img, img_mode_list) 76 | 77 | 78 | def tran_90(img): 79 | """图片翻转并旋转90度。""" 80 | mode = get_modes(img) 81 | return mode_img(mode.T, 255) 82 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name="pycapt", 8 | version="1.0.20", 9 | author="aboutmydreams", 10 | author_email="aboutmydreams@163.com", 11 | description="a library that processes verification codes", 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | url="https://github.com/aboutmydreams/pycapt", 15 | packages=setuptools.find_packages(), 16 | classifiers=[ 17 | "Programming Language :: Python :: 3", 18 | "License :: OSI Approved :: MIT License", 19 | "Operating System :: OS Independent", 20 | ], 21 | ) -------------------------------------------------------------------------------- /tests/test.py: -------------------------------------------------------------------------------- 1 | import pycapt 2 | from PIL import Image 3 | 4 | # 处理验证码 5 | """ 6 | img = Image.open('1.png') 7 | img.show() 8 | img = pycapt.two_value(img, Threshold=100) 9 | img = pycapt.dele_noise(img, N=5, Z=2) 10 | img = pycapt.dele_line(img, 4) 11 | img = pycapt.dele_noise(img, N=4, Z=2) 12 | img = pycapt.dele_line(img, 3) 13 | img = pycapt.dele_noise(img, N=4, Z=2) 14 | img = pycapt.dele_line(img, 3) 15 | img = pycapt.dele_line(img, 2) 16 | img = pycapt.dele_line(img, 1) 17 | 18 | 19 | img = pycapt.tran_90(img) 20 | img = pycapt.dele_line(img, 3) 21 | img = pycapt.dele_line(img, 2) 22 | img = pycapt.dele_line(img, 1) 23 | img = pycapt.tran_90(img) 24 | 25 | pan = [18, 18, 18, 18, 17, 17, 17, 26 | 16, 16, 16, 15, 15, 15, 15, 14, 27 | 14, 14, 14, 13, 13, 10, 10, 28 | 10, 9, 9, 8, 7, 6, 5, 5, 4, 29 | 4, 4, 4, 4, 3, 1, 0, 0, 0] 30 | img = pycapt.rectify_img(img, pans=pan) 31 | img = pycapt.dele_line(img, 3) 32 | img = pycapt.dele_line(img, 2) 33 | img = pycapt.dele_line(img, 1) 34 | # img.show() 35 | 36 | a = pycapt.cut_img_to_img_list(img, 30, 255) 37 | for i in a: 38 | i.show() 39 | """ 40 | 41 | # 生产验证码训练集 42 | """ 43 | name, img = pycapt.do_captcha( 44 | my_str_list=['A', 'B', 'C', 'D', '1', '2', '3'], 45 | width=160, 46 | height=40, 47 | num_of_str=4, 48 | font=30, 49 | gray_value=255, 50 | font_family='OpenSans-Bold.ttf') 51 | 52 | 53 | # pycapt.get_train_img() 54 | img = pycapt.more_noise(img, N=0.5, Z=2) 55 | # img = pycapt.img_pan(img,15,3) 56 | pans = [18, 18, 18, 18, 17, 17, 17, 57 | 16, 16, 16, 15, 15, 15, 15, 14, 58 | 14, 14, 14, 13, 13, 10, 10, 59 | 10, 9, 9, 8, 7, 6, 5, 5, 4, 60 | 4, 4, 4, 4, 3, 1, 0, 0, 0] 61 | 62 | 63 | def contrast(num): 64 | return -num 65 | 66 | 67 | pan0 = list(map(contrast, pans)) 68 | img = pycapt.rectify_img(img, pans=pan0) 69 | 70 | img = pycapt.show_noise_img(img, 0.1, 1) 71 | img = pycapt.dele_noise(img, 5, 2) 72 | img = pycapt.clear_train_img(img) 73 | img.show() 74 | """ 75 | 76 | 77 | # 直接生成验证码训练集 78 | """ 79 | file_name,img = pycapt.easy_train_img( 80 | my_str_list=['1','2','A','B'], 81 | width=30, 82 | height=32, 83 | num_of_str=4, 84 | font=30, 85 | xpan=3, 86 | ypan=2, 87 | rotate=15, 88 | noise_N=0.3, 89 | noise_Z=2, 90 | gray_value=255, 91 | font_family='OpenSans-Bold.ttf') 92 | 93 | print(file_name) 94 | img.show() 95 | """ 96 | -------------------------------------------------------------------------------- /tests/test_train_img.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | from PIL import Image 4 | import pycapt 5 | 6 | 7 | class TestCaptcha(unittest.TestCase): 8 | def setUp(self): 9 | self.image_path = "test_captcha.png" 10 | self.train_image_path = "train_captcha.png" 11 | 12 | def tearDown(self): 13 | # Remove any generated images after tests 14 | if os.path.exists(self.image_path): 15 | os.remove(self.image_path) 16 | if os.path.exists(self.train_image_path): 17 | os.remove(self.train_image_path) 18 | 19 | def test_generate_captcha_image(self): 20 | # Generate a CAPTCHA image 21 | name, img = pycapt.do_captcha( 22 | my_str_list=["A", "B", "C", "D", "1", "2", "3"], 23 | width=160, 24 | height=40, 25 | num_of_str=4, 26 | font=30, 27 | gray_value=255, 28 | font_family=None, 29 | ) 30 | img.save(self.image_path) # Save the generated image 31 | 32 | # Check if the image file is created 33 | self.assertTrue( 34 | os.path.exists(self.image_path), 35 | "result fail: expected CAPTCHA image not created", 36 | ) 37 | 38 | # Verify the image can be opened 39 | with Image.open(self.image_path) as opened_img: 40 | self.assertEqual( 41 | opened_img.size, 42 | (160, 40), 43 | "result fail: expected image size does not match", 44 | ) 45 | 46 | def test_generate_training_image(self): 47 | # Generate a training CAPTCHA image 48 | file_name, img = pycapt.easy_train_img( 49 | my_str_list=["1", "2", "A", "B", "C"], 50 | width=30, 51 | height=32, 52 | num_of_str=4, 53 | font=30, 54 | xpan=3, 55 | ypan=2, 56 | rotate=15, 57 | noise_N=0.3, 58 | noise_Z=2, 59 | gray_value=255, 60 | font_family="OpenSans-Bold.ttf", 61 | ) 62 | img.save(self.train_image_path) # Save the generated training image 63 | 64 | # Check if the training image file is created 65 | self.assertTrue( 66 | os.path.exists(self.train_image_path), 67 | "result fail: expected training image not created", 68 | ) 69 | 70 | # Verify the training image can be opened 71 | with Image.open(self.train_image_path) as opened_img: 72 | self.assertEqual( 73 | opened_img.size, 74 | (30, 32), 75 | "result fail: expected training image size does not match", 76 | ) 77 | 78 | 79 | if __name__ == "__main__": 80 | unittest.main() 81 | -------------------------------------------------------------------------------- /update_version.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | """ 4 | 5 | 1. vim .git/hooks/pre-commit 输入: 6 | #!/bin/bash 7 | # 在 commit 之前运行 update_version.py 脚本 8 | python3 update_version.py 9 | # 继续执行提交 10 | git add -A 11 | 12 | 2. 确保该脚本具有执行权限 13 | chmod +x .git/hooks/pre-commit 14 | 15 | """ 16 | 17 | 18 | def increment_version_string(version_string): 19 | parts = version_string.split(".") 20 | if len(parts) < 3: 21 | raise ValueError("Version string must have at least three parts") 22 | 23 | last_part = int(parts[-1]) 24 | new_last_part = last_part + 1 25 | parts[-1] = str(new_last_part) 26 | 27 | return ".".join(parts) 28 | 29 | 30 | def update_setup_py_version(setup_py_path): 31 | with open(setup_py_path, "r") as f: 32 | setup_py_content = f.read() 33 | f.close() 34 | 35 | version_pattern = r"version\s*=\s*['\"]([^'\"]+)['\"]" 36 | match = re.search(version_pattern, setup_py_content) 37 | 38 | if match: 39 | old_version = match.group(1) 40 | new_version = increment_version_string(old_version) 41 | filedata = setup_py_content.replace(old_version, new_version) 42 | with open(setup_py_path, "w") as f: 43 | f.write(filedata) 44 | f.close() 45 | print(f"Version updated to {new_version}") 46 | return new_version 47 | raise ValueError("Could not find version number in setup.py") 48 | 49 | 50 | update_setup_py_version("./setup.py") 51 | --------------------------------------------------------------------------------