captcha-font-sdk 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- captcha_font_sdk-0.0.1/LICENSE +21 -0
- captcha_font_sdk-0.0.1/PKG-INFO +261 -0
- captcha_font_sdk-0.0.1/README.md +234 -0
- captcha_font_sdk-0.0.1/captcha_font_sdk/__init__.py +72 -0
- captcha_font_sdk-0.0.1/captcha_font_sdk/background_index.py +74 -0
- captcha_font_sdk-0.0.1/captcha_font_sdk/bitmap_ops.py +61 -0
- captcha_font_sdk-0.0.1/captcha_font_sdk/component_extractor.py +82 -0
- captcha_font_sdk-0.0.1/captcha_font_sdk/diff_engine.py +25 -0
- captcha_font_sdk-0.0.1/captcha_font_sdk/font_glyph_batch.py +85 -0
- captcha_font_sdk-0.0.1/captcha_font_sdk/font_glyph_dataset.py +108 -0
- captcha_font_sdk-0.0.1/captcha_font_sdk/font_glyph_extractor.py +95 -0
- captcha_font_sdk-0.0.1/captcha_font_sdk/font_glyph_features.py +45 -0
- captcha_font_sdk-0.0.1/captcha_font_sdk/font_glyph_images.py +57 -0
- captcha_font_sdk-0.0.1/captcha_font_sdk/font_glyph_slots.py +44 -0
- captcha_font_sdk-0.0.1/captcha_font_sdk/group_id.py +21 -0
- captcha_font_sdk-0.0.1/captcha_font_sdk/local_restore_fs.py +29 -0
- captcha_font_sdk-0.0.1/captcha_font_sdk/local_restore_paths.py +43 -0
- captcha_font_sdk-0.0.1/captcha_font_sdk/local_restore_runner.py +132 -0
- captcha_font_sdk-0.0.1/captcha_font_sdk/local_restore_types.py +81 -0
- captcha_font_sdk-0.0.1/captcha_font_sdk/local_restore_vote.py +116 -0
- captcha_font_sdk-0.0.1/captcha_font_sdk/locator.py +464 -0
- captcha_font_sdk-0.0.1/captcha_font_sdk/mask_component_extractor.py +68 -0
- captcha_font_sdk-0.0.1/captcha_font_sdk/recognizer.py +418 -0
- captcha_font_sdk-0.0.1/captcha_font_sdk/slider_locator.py +108 -0
- captcha_font_sdk-0.0.1/captcha_font_sdk/text_layer_renderer.py +70 -0
- captcha_font_sdk-0.0.1/captcha_font_sdk/text_region_builder.py +138 -0
- captcha_font_sdk-0.0.1/captcha_font_sdk/types.py +214 -0
- captcha_font_sdk-0.0.1/captcha_font_sdk.egg-info/PKG-INFO +261 -0
- captcha_font_sdk-0.0.1/captcha_font_sdk.egg-info/SOURCES.txt +32 -0
- captcha_font_sdk-0.0.1/captcha_font_sdk.egg-info/dependency_links.txt +1 -0
- captcha_font_sdk-0.0.1/captcha_font_sdk.egg-info/requires.txt +1 -0
- captcha_font_sdk-0.0.1/captcha_font_sdk.egg-info/top_level.txt +1 -0
- captcha_font_sdk-0.0.1/pyproject.toml +46 -0
- captcha_font_sdk-0.0.1/setup.cfg +4 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 JSREI
|
|
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.
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: captcha-font-sdk
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Captcha recognizer SDK for background mapping, font localization, slider gap detection, and local background restore.
|
|
5
|
+
Author: JSREI
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/JSREI/force-fuck-captcha-background
|
|
8
|
+
Project-URL: Repository, https://github.com/JSREI/force-fuck-captcha-background
|
|
9
|
+
Project-URL: Issues, https://github.com/JSREI/force-fuck-captcha-background/issues
|
|
10
|
+
Keywords: captcha,image-processing,ocr,slider,font
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Image Processing
|
|
22
|
+
Requires-Python: >=3.8
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: Pillow>=10.0.0
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# Captcha Recognizer SDK (Python)
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
pip install captcha-font-sdk
|
|
33
|
+
|
|
34
|
+
From source:
|
|
35
|
+
|
|
36
|
+
cd sdk/python/captcha-font-sdk
|
|
37
|
+
pip install -e .
|
|
38
|
+
|
|
39
|
+
If you run scripts from repo root, add SDK dir to `PYTHONPATH`:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
PYTHONPATH=sdk/python/captcha-font-sdk python sdk/python/captcha-font-sdk/examples/demo.py
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## What this SDK does
|
|
46
|
+
|
|
47
|
+
1. Build `group_id -> full background image` mapping from your background directory.
|
|
48
|
+
2. For a new captcha image:
|
|
49
|
+
- compute `group_id` from 4 corner RGB pixels,
|
|
50
|
+
- find matching full background,
|
|
51
|
+
- diff captcha vs background.
|
|
52
|
+
3. Provide **different APIs by captcha type**:
|
|
53
|
+
- font captcha: extract color-based connected components (字体验证码),
|
|
54
|
+
- slider captcha: locate the largest gap region and return bbox/center (缺口验证码).
|
|
55
|
+
4. For font captcha, provide:
|
|
56
|
+
- text position detection (文字位置框),
|
|
57
|
+
- text-pixel extraction to transparent PNG (文字像素扣图).
|
|
58
|
+
5. Provide local restore workflow (same capability as UI):
|
|
59
|
+
- recursive scan captcha directory,
|
|
60
|
+
- group by 4-corner bucket id,
|
|
61
|
+
- per-pixel voting to restore backgrounds,
|
|
62
|
+
- write background images + `summary.json`.
|
|
63
|
+
|
|
64
|
+
## group_id format
|
|
65
|
+
|
|
66
|
+
`{width}x{height}|lt_r,lt_g,lt_b|rt_r,rt_g,rt_b|lb_r,lb_g,lb_b|rb_r,rb_g,rb_b`
|
|
67
|
+
|
|
68
|
+
## API overview
|
|
69
|
+
|
|
70
|
+
### 1) Unified facade: `CaptchaRecognizer(...)`
|
|
71
|
+
|
|
72
|
+
- `build_background_index(background_dir, recursive=True, exts=None)`
|
|
73
|
+
- `recognize_font(captcha_path, include_pixels=True)`
|
|
74
|
+
- `recognize_text_positions(captcha_path, include_pixels=True)`
|
|
75
|
+
- `extract_text_layer(captcha_path, output_path=None, crop_to_content=False)`
|
|
76
|
+
- `restore_background_by_captcha(captcha_path, output_path=None)`
|
|
77
|
+
- `extract_font_glyphs(captcha_path, include_pixels=True, include_rgba_2d=False)`
|
|
78
|
+
- `extract_font_glyph_features(captcha_path, target_width=32, target_height=32, keep_aspect_ratio=True)`
|
|
79
|
+
- `extract_font_glyph_slots(captcha_path, slot_count=5, target_width=32, target_height=32, ...)`
|
|
80
|
+
- `export_font_glyph_images(captcha_path, output_dir, file_prefix=None)`
|
|
81
|
+
- `batch_extract_font_glyph_features(input_dir, target_width=32, target_height=32, ...)`
|
|
82
|
+
- `export_font_glyph_dataset_npz(input_dir, output_npz_path, target_width=32, target_height=32, ...)`
|
|
83
|
+
- `recognize_slider(captcha_path)`
|
|
84
|
+
- `run_local_restore(input_dir, output_dir, clear_output_before_run=False, recursive=True, max_error_items=200, progress_callback=None, stop_checker=None)`
|
|
85
|
+
- `recognize(captcha_path, captcha_type="font" | "slider", include_pixels=True)`
|
|
86
|
+
|
|
87
|
+
### 2) Font captcha API: `CaptchaFontLocator(...)`
|
|
88
|
+
|
|
89
|
+
- `locate_fonts(...)`
|
|
90
|
+
- `locate_fonts_dict(...)`
|
|
91
|
+
- `restore_background_by_captcha(...)`
|
|
92
|
+
- `restore_background_by_captcha_dict(...)`
|
|
93
|
+
- `extract_font_glyphs(...)`
|
|
94
|
+
- `extract_font_glyphs_dict(...)`
|
|
95
|
+
- `extract_font_glyph_features(...)`
|
|
96
|
+
- `extract_font_glyph_features_dict(...)`
|
|
97
|
+
- `extract_font_glyph_slots(...)`
|
|
98
|
+
- `extract_font_glyph_slots_dict(...)`
|
|
99
|
+
- `export_font_glyph_images(...)`
|
|
100
|
+
- `export_font_glyph_images_dict(...)`
|
|
101
|
+
- `locate_text_positions(...)`
|
|
102
|
+
- `locate_text_positions_dict(...)`
|
|
103
|
+
- `extract_text_layer(...)`
|
|
104
|
+
- `extract_text_layer_dict(...)`
|
|
105
|
+
|
|
106
|
+
### 3) Slider captcha API: `CaptchaSliderLocator(...)`
|
|
107
|
+
|
|
108
|
+
- `locate_gap(...)`
|
|
109
|
+
- `locate_gap_dict(...)`
|
|
110
|
+
|
|
111
|
+
### 4) Local restore API (UI parity)
|
|
112
|
+
|
|
113
|
+
- `CaptchaRecognizer.run_local_restore(...)`
|
|
114
|
+
- `CaptchaRecognizer.run_local_restore_dict(...)`
|
|
115
|
+
|
|
116
|
+
## Quick examples
|
|
117
|
+
|
|
118
|
+
### Unified API
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
from captcha_font_sdk import CaptchaRecognizer
|
|
122
|
+
|
|
123
|
+
sdk = CaptchaRecognizer(
|
|
124
|
+
diff_threshold=18,
|
|
125
|
+
font_min_component_pixels=8,
|
|
126
|
+
slider_min_gap_pixels=20,
|
|
127
|
+
connectivity=8,
|
|
128
|
+
)
|
|
129
|
+
sdk.build_background_index("/your/full-background-dir")
|
|
130
|
+
|
|
131
|
+
font_result = sdk.recognize_font_dict("/your/new-font-captcha.png", include_pixels=True)
|
|
132
|
+
text_positions = sdk.recognize_text_positions_dict("/your/new-font-captcha.png", include_pixels=True)
|
|
133
|
+
text_layer = sdk.extract_text_layer_dict(
|
|
134
|
+
"/your/new-font-captcha.png",
|
|
135
|
+
output_path="./text_layer.png",
|
|
136
|
+
crop_to_content=False,
|
|
137
|
+
)
|
|
138
|
+
restored_bg = sdk.restore_background_by_captcha_dict(
|
|
139
|
+
"/your/new-font-captcha.png",
|
|
140
|
+
output_path="./restored_background.png",
|
|
141
|
+
)
|
|
142
|
+
glyphs = sdk.extract_font_glyphs_dict(
|
|
143
|
+
"/your/new-font-captcha.png",
|
|
144
|
+
include_pixels=True,
|
|
145
|
+
include_rgba_2d=False,
|
|
146
|
+
)
|
|
147
|
+
glyph_features = sdk.extract_font_glyph_features_dict(
|
|
148
|
+
"/your/new-font-captcha.png",
|
|
149
|
+
target_width=32,
|
|
150
|
+
target_height=32,
|
|
151
|
+
)
|
|
152
|
+
glyph_slots = sdk.extract_font_glyph_slots_dict(
|
|
153
|
+
"/your/new-font-captcha.png",
|
|
154
|
+
slot_count=5,
|
|
155
|
+
target_width=32,
|
|
156
|
+
target_height=32,
|
|
157
|
+
)
|
|
158
|
+
glyph_images = sdk.export_font_glyph_images_dict(
|
|
159
|
+
"/your/new-font-captcha.png",
|
|
160
|
+
output_dir="./glyph_images",
|
|
161
|
+
)
|
|
162
|
+
slider_result = sdk.recognize_slider_dict("/your/new-slider-captcha.png")
|
|
163
|
+
|
|
164
|
+
print(font_result["stats"]["component_count"])
|
|
165
|
+
print(text_positions["stats"]["region_count"])
|
|
166
|
+
print(text_layer["output_path"])
|
|
167
|
+
print(restored_bg["background_path"], restored_bg["output_path"])
|
|
168
|
+
print(glyphs["stats"]["glyph_count"])
|
|
169
|
+
print(glyphs["glyphs"][0]["rect_index"], glyphs["glyphs"][0]["bbox"])
|
|
170
|
+
print(glyphs["glyphs"][0]["bitmap_2d"])
|
|
171
|
+
print(len(glyph_features["glyph_features"][0]["vector_1d"])) # 32*32
|
|
172
|
+
print(glyph_slots["stats"]["filled_slots"], glyph_slots["slot_count"])
|
|
173
|
+
print(glyph_images["stats"]["exported_count"], glyph_images["output_dir"])
|
|
174
|
+
print(slider_result["gap"])
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Batch glyph feature extraction
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
batch = sdk.batch_extract_font_glyph_features_dict(
|
|
181
|
+
input_dir="/path/to/captcha_dir",
|
|
182
|
+
target_width=32,
|
|
183
|
+
target_height=32,
|
|
184
|
+
recursive=True,
|
|
185
|
+
include_payload=False,
|
|
186
|
+
output_json_path="./glyph_batch_summary.json",
|
|
187
|
+
)
|
|
188
|
+
print(batch["success_count"], batch["error_count"])
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Export training dataset (`npz`)
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
dataset = sdk.export_font_glyph_dataset_npz_dict(
|
|
195
|
+
input_dir="/path/to/captcha_dir",
|
|
196
|
+
output_npz_path="./glyph_dataset.npz",
|
|
197
|
+
target_width=32,
|
|
198
|
+
target_height=32,
|
|
199
|
+
recursive=True,
|
|
200
|
+
output_json_path="./glyph_dataset_meta.json",
|
|
201
|
+
)
|
|
202
|
+
print(dataset["glyph_sample_count"], dataset["output_npz_path"])
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Local restore (same as UI "本地还原")
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
from captcha_font_sdk import CaptchaRecognizer
|
|
209
|
+
|
|
210
|
+
sdk = CaptchaRecognizer()
|
|
211
|
+
summary = sdk.run_local_restore_dict(
|
|
212
|
+
input_dir="/path/to/captcha_images",
|
|
213
|
+
output_dir="/path/to/output_backgrounds",
|
|
214
|
+
clear_output_before_run=True,
|
|
215
|
+
recursive=True,
|
|
216
|
+
max_error_items=200,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
print(summary["bucket_count"], summary["output_files"])
|
|
220
|
+
print(summary["summary_path"])
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
`run_local_restore(...)` will also refresh SDK background index from `output_dir`,
|
|
224
|
+
so you can call `recognize_*` APIs immediately after local restore.
|
|
225
|
+
|
|
226
|
+
### Separate APIs by captcha type
|
|
227
|
+
|
|
228
|
+
```python
|
|
229
|
+
from captcha_font_sdk import CaptchaFontLocator, CaptchaSliderLocator
|
|
230
|
+
|
|
231
|
+
background_dir = "/your/full-background-dir"
|
|
232
|
+
|
|
233
|
+
font_sdk = CaptchaFontLocator()
|
|
234
|
+
font_sdk.build_background_index(background_dir)
|
|
235
|
+
font_result = font_sdk.locate_fonts_dict("/your/new-font-captcha.png")
|
|
236
|
+
|
|
237
|
+
slider_sdk = CaptchaSliderLocator()
|
|
238
|
+
slider_sdk.build_background_index(background_dir)
|
|
239
|
+
slider_result = slider_sdk.locate_gap_dict("/your/new-slider-captcha.png")
|
|
240
|
+
|
|
241
|
+
text_positions = font_sdk.locate_text_positions_dict("/your/new-font-captcha.png", include_pixels=True)
|
|
242
|
+
text_layer = font_sdk.extract_text_layer_dict(
|
|
243
|
+
"/your/new-font-captcha.png",
|
|
244
|
+
output_path="./text_layer.png",
|
|
245
|
+
crop_to_content=True,
|
|
246
|
+
)
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## Notes
|
|
250
|
+
|
|
251
|
+
- If captcha size and matched background size differ, SDK raises an error.
|
|
252
|
+
- If `group_id` is not found, SDK raises `KeyError`.
|
|
253
|
+
- Slider result `gap` may be `null` when no connected region reaches `min_gap_pixels`.
|
|
254
|
+
- Text position detection uses exact-color connected components plus size/fill filtering and near-neighbor merge.
|
|
255
|
+
- `extract_text_layer(...)` returns transparent image with only text pixels preserved.
|
|
256
|
+
- `export_font_glyph_dataset_npz(...)` requires `numpy` (`pip install numpy`).
|
|
257
|
+
- You can tune:
|
|
258
|
+
- increase `diff_threshold` to reduce tiny color jitter,
|
|
259
|
+
- increase `font_min_component_pixels` / `min_component_pixels` to suppress font noise,
|
|
260
|
+
- increase `slider_min_gap_pixels` / `min_gap_pixels` to suppress slider noise,
|
|
261
|
+
- tune `text_min_width / text_min_height / text_min_fill_ratio / text_max_fill_ratio` for text boxes.
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# Captcha Recognizer SDK (Python)
|
|
2
|
+
|
|
3
|
+
## Install
|
|
4
|
+
|
|
5
|
+
pip install captcha-font-sdk
|
|
6
|
+
|
|
7
|
+
From source:
|
|
8
|
+
|
|
9
|
+
cd sdk/python/captcha-font-sdk
|
|
10
|
+
pip install -e .
|
|
11
|
+
|
|
12
|
+
If you run scripts from repo root, add SDK dir to `PYTHONPATH`:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
PYTHONPATH=sdk/python/captcha-font-sdk python sdk/python/captcha-font-sdk/examples/demo.py
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## What this SDK does
|
|
19
|
+
|
|
20
|
+
1. Build `group_id -> full background image` mapping from your background directory.
|
|
21
|
+
2. For a new captcha image:
|
|
22
|
+
- compute `group_id` from 4 corner RGB pixels,
|
|
23
|
+
- find matching full background,
|
|
24
|
+
- diff captcha vs background.
|
|
25
|
+
3. Provide **different APIs by captcha type**:
|
|
26
|
+
- font captcha: extract color-based connected components (字体验证码),
|
|
27
|
+
- slider captcha: locate the largest gap region and return bbox/center (缺口验证码).
|
|
28
|
+
4. For font captcha, provide:
|
|
29
|
+
- text position detection (文字位置框),
|
|
30
|
+
- text-pixel extraction to transparent PNG (文字像素扣图).
|
|
31
|
+
5. Provide local restore workflow (same capability as UI):
|
|
32
|
+
- recursive scan captcha directory,
|
|
33
|
+
- group by 4-corner bucket id,
|
|
34
|
+
- per-pixel voting to restore backgrounds,
|
|
35
|
+
- write background images + `summary.json`.
|
|
36
|
+
|
|
37
|
+
## group_id format
|
|
38
|
+
|
|
39
|
+
`{width}x{height}|lt_r,lt_g,lt_b|rt_r,rt_g,rt_b|lb_r,lb_g,lb_b|rb_r,rb_g,rb_b`
|
|
40
|
+
|
|
41
|
+
## API overview
|
|
42
|
+
|
|
43
|
+
### 1) Unified facade: `CaptchaRecognizer(...)`
|
|
44
|
+
|
|
45
|
+
- `build_background_index(background_dir, recursive=True, exts=None)`
|
|
46
|
+
- `recognize_font(captcha_path, include_pixels=True)`
|
|
47
|
+
- `recognize_text_positions(captcha_path, include_pixels=True)`
|
|
48
|
+
- `extract_text_layer(captcha_path, output_path=None, crop_to_content=False)`
|
|
49
|
+
- `restore_background_by_captcha(captcha_path, output_path=None)`
|
|
50
|
+
- `extract_font_glyphs(captcha_path, include_pixels=True, include_rgba_2d=False)`
|
|
51
|
+
- `extract_font_glyph_features(captcha_path, target_width=32, target_height=32, keep_aspect_ratio=True)`
|
|
52
|
+
- `extract_font_glyph_slots(captcha_path, slot_count=5, target_width=32, target_height=32, ...)`
|
|
53
|
+
- `export_font_glyph_images(captcha_path, output_dir, file_prefix=None)`
|
|
54
|
+
- `batch_extract_font_glyph_features(input_dir, target_width=32, target_height=32, ...)`
|
|
55
|
+
- `export_font_glyph_dataset_npz(input_dir, output_npz_path, target_width=32, target_height=32, ...)`
|
|
56
|
+
- `recognize_slider(captcha_path)`
|
|
57
|
+
- `run_local_restore(input_dir, output_dir, clear_output_before_run=False, recursive=True, max_error_items=200, progress_callback=None, stop_checker=None)`
|
|
58
|
+
- `recognize(captcha_path, captcha_type="font" | "slider", include_pixels=True)`
|
|
59
|
+
|
|
60
|
+
### 2) Font captcha API: `CaptchaFontLocator(...)`
|
|
61
|
+
|
|
62
|
+
- `locate_fonts(...)`
|
|
63
|
+
- `locate_fonts_dict(...)`
|
|
64
|
+
- `restore_background_by_captcha(...)`
|
|
65
|
+
- `restore_background_by_captcha_dict(...)`
|
|
66
|
+
- `extract_font_glyphs(...)`
|
|
67
|
+
- `extract_font_glyphs_dict(...)`
|
|
68
|
+
- `extract_font_glyph_features(...)`
|
|
69
|
+
- `extract_font_glyph_features_dict(...)`
|
|
70
|
+
- `extract_font_glyph_slots(...)`
|
|
71
|
+
- `extract_font_glyph_slots_dict(...)`
|
|
72
|
+
- `export_font_glyph_images(...)`
|
|
73
|
+
- `export_font_glyph_images_dict(...)`
|
|
74
|
+
- `locate_text_positions(...)`
|
|
75
|
+
- `locate_text_positions_dict(...)`
|
|
76
|
+
- `extract_text_layer(...)`
|
|
77
|
+
- `extract_text_layer_dict(...)`
|
|
78
|
+
|
|
79
|
+
### 3) Slider captcha API: `CaptchaSliderLocator(...)`
|
|
80
|
+
|
|
81
|
+
- `locate_gap(...)`
|
|
82
|
+
- `locate_gap_dict(...)`
|
|
83
|
+
|
|
84
|
+
### 4) Local restore API (UI parity)
|
|
85
|
+
|
|
86
|
+
- `CaptchaRecognizer.run_local_restore(...)`
|
|
87
|
+
- `CaptchaRecognizer.run_local_restore_dict(...)`
|
|
88
|
+
|
|
89
|
+
## Quick examples
|
|
90
|
+
|
|
91
|
+
### Unified API
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from captcha_font_sdk import CaptchaRecognizer
|
|
95
|
+
|
|
96
|
+
sdk = CaptchaRecognizer(
|
|
97
|
+
diff_threshold=18,
|
|
98
|
+
font_min_component_pixels=8,
|
|
99
|
+
slider_min_gap_pixels=20,
|
|
100
|
+
connectivity=8,
|
|
101
|
+
)
|
|
102
|
+
sdk.build_background_index("/your/full-background-dir")
|
|
103
|
+
|
|
104
|
+
font_result = sdk.recognize_font_dict("/your/new-font-captcha.png", include_pixels=True)
|
|
105
|
+
text_positions = sdk.recognize_text_positions_dict("/your/new-font-captcha.png", include_pixels=True)
|
|
106
|
+
text_layer = sdk.extract_text_layer_dict(
|
|
107
|
+
"/your/new-font-captcha.png",
|
|
108
|
+
output_path="./text_layer.png",
|
|
109
|
+
crop_to_content=False,
|
|
110
|
+
)
|
|
111
|
+
restored_bg = sdk.restore_background_by_captcha_dict(
|
|
112
|
+
"/your/new-font-captcha.png",
|
|
113
|
+
output_path="./restored_background.png",
|
|
114
|
+
)
|
|
115
|
+
glyphs = sdk.extract_font_glyphs_dict(
|
|
116
|
+
"/your/new-font-captcha.png",
|
|
117
|
+
include_pixels=True,
|
|
118
|
+
include_rgba_2d=False,
|
|
119
|
+
)
|
|
120
|
+
glyph_features = sdk.extract_font_glyph_features_dict(
|
|
121
|
+
"/your/new-font-captcha.png",
|
|
122
|
+
target_width=32,
|
|
123
|
+
target_height=32,
|
|
124
|
+
)
|
|
125
|
+
glyph_slots = sdk.extract_font_glyph_slots_dict(
|
|
126
|
+
"/your/new-font-captcha.png",
|
|
127
|
+
slot_count=5,
|
|
128
|
+
target_width=32,
|
|
129
|
+
target_height=32,
|
|
130
|
+
)
|
|
131
|
+
glyph_images = sdk.export_font_glyph_images_dict(
|
|
132
|
+
"/your/new-font-captcha.png",
|
|
133
|
+
output_dir="./glyph_images",
|
|
134
|
+
)
|
|
135
|
+
slider_result = sdk.recognize_slider_dict("/your/new-slider-captcha.png")
|
|
136
|
+
|
|
137
|
+
print(font_result["stats"]["component_count"])
|
|
138
|
+
print(text_positions["stats"]["region_count"])
|
|
139
|
+
print(text_layer["output_path"])
|
|
140
|
+
print(restored_bg["background_path"], restored_bg["output_path"])
|
|
141
|
+
print(glyphs["stats"]["glyph_count"])
|
|
142
|
+
print(glyphs["glyphs"][0]["rect_index"], glyphs["glyphs"][0]["bbox"])
|
|
143
|
+
print(glyphs["glyphs"][0]["bitmap_2d"])
|
|
144
|
+
print(len(glyph_features["glyph_features"][0]["vector_1d"])) # 32*32
|
|
145
|
+
print(glyph_slots["stats"]["filled_slots"], glyph_slots["slot_count"])
|
|
146
|
+
print(glyph_images["stats"]["exported_count"], glyph_images["output_dir"])
|
|
147
|
+
print(slider_result["gap"])
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Batch glyph feature extraction
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
batch = sdk.batch_extract_font_glyph_features_dict(
|
|
154
|
+
input_dir="/path/to/captcha_dir",
|
|
155
|
+
target_width=32,
|
|
156
|
+
target_height=32,
|
|
157
|
+
recursive=True,
|
|
158
|
+
include_payload=False,
|
|
159
|
+
output_json_path="./glyph_batch_summary.json",
|
|
160
|
+
)
|
|
161
|
+
print(batch["success_count"], batch["error_count"])
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Export training dataset (`npz`)
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
dataset = sdk.export_font_glyph_dataset_npz_dict(
|
|
168
|
+
input_dir="/path/to/captcha_dir",
|
|
169
|
+
output_npz_path="./glyph_dataset.npz",
|
|
170
|
+
target_width=32,
|
|
171
|
+
target_height=32,
|
|
172
|
+
recursive=True,
|
|
173
|
+
output_json_path="./glyph_dataset_meta.json",
|
|
174
|
+
)
|
|
175
|
+
print(dataset["glyph_sample_count"], dataset["output_npz_path"])
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Local restore (same as UI "本地还原")
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
from captcha_font_sdk import CaptchaRecognizer
|
|
182
|
+
|
|
183
|
+
sdk = CaptchaRecognizer()
|
|
184
|
+
summary = sdk.run_local_restore_dict(
|
|
185
|
+
input_dir="/path/to/captcha_images",
|
|
186
|
+
output_dir="/path/to/output_backgrounds",
|
|
187
|
+
clear_output_before_run=True,
|
|
188
|
+
recursive=True,
|
|
189
|
+
max_error_items=200,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
print(summary["bucket_count"], summary["output_files"])
|
|
193
|
+
print(summary["summary_path"])
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
`run_local_restore(...)` will also refresh SDK background index from `output_dir`,
|
|
197
|
+
so you can call `recognize_*` APIs immediately after local restore.
|
|
198
|
+
|
|
199
|
+
### Separate APIs by captcha type
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
from captcha_font_sdk import CaptchaFontLocator, CaptchaSliderLocator
|
|
203
|
+
|
|
204
|
+
background_dir = "/your/full-background-dir"
|
|
205
|
+
|
|
206
|
+
font_sdk = CaptchaFontLocator()
|
|
207
|
+
font_sdk.build_background_index(background_dir)
|
|
208
|
+
font_result = font_sdk.locate_fonts_dict("/your/new-font-captcha.png")
|
|
209
|
+
|
|
210
|
+
slider_sdk = CaptchaSliderLocator()
|
|
211
|
+
slider_sdk.build_background_index(background_dir)
|
|
212
|
+
slider_result = slider_sdk.locate_gap_dict("/your/new-slider-captcha.png")
|
|
213
|
+
|
|
214
|
+
text_positions = font_sdk.locate_text_positions_dict("/your/new-font-captcha.png", include_pixels=True)
|
|
215
|
+
text_layer = font_sdk.extract_text_layer_dict(
|
|
216
|
+
"/your/new-font-captcha.png",
|
|
217
|
+
output_path="./text_layer.png",
|
|
218
|
+
crop_to_content=True,
|
|
219
|
+
)
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Notes
|
|
223
|
+
|
|
224
|
+
- If captcha size and matched background size differ, SDK raises an error.
|
|
225
|
+
- If `group_id` is not found, SDK raises `KeyError`.
|
|
226
|
+
- Slider result `gap` may be `null` when no connected region reaches `min_gap_pixels`.
|
|
227
|
+
- Text position detection uses exact-color connected components plus size/fill filtering and near-neighbor merge.
|
|
228
|
+
- `extract_text_layer(...)` returns transparent image with only text pixels preserved.
|
|
229
|
+
- `export_font_glyph_dataset_npz(...)` requires `numpy` (`pip install numpy`).
|
|
230
|
+
- You can tune:
|
|
231
|
+
- increase `diff_threshold` to reduce tiny color jitter,
|
|
232
|
+
- increase `font_min_component_pixels` / `min_component_pixels` to suppress font noise,
|
|
233
|
+
- increase `slider_min_gap_pixels` / `min_gap_pixels` to suppress slider noise,
|
|
234
|
+
- tune `text_min_width / text_min_height / text_min_fill_ratio / text_max_fill_ratio` for text boxes.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
__version__ = "0.0.1"
|
|
2
|
+
|
|
3
|
+
from .locator import CaptchaFontLocator
|
|
4
|
+
from .local_restore_types import (
|
|
5
|
+
BucketSummary,
|
|
6
|
+
LocalRestoreConfig,
|
|
7
|
+
LocalRestoreStatus,
|
|
8
|
+
LocalRestoreSummary,
|
|
9
|
+
ProgressCallback,
|
|
10
|
+
ProcessingErrorItem,
|
|
11
|
+
StopChecker,
|
|
12
|
+
)
|
|
13
|
+
from .recognizer import CaptchaRecognizer
|
|
14
|
+
from .slider_locator import CaptchaSliderLocator
|
|
15
|
+
from .types import (
|
|
16
|
+
BackgroundRestoreResult,
|
|
17
|
+
BackgroundMeta,
|
|
18
|
+
BatchGlyphExtractItem,
|
|
19
|
+
BatchGlyphExtractResult,
|
|
20
|
+
CaptchaType,
|
|
21
|
+
FontGlyph,
|
|
22
|
+
FontGlyphImageExportResult,
|
|
23
|
+
FontGlyphImageItem,
|
|
24
|
+
FontGlyphFeature,
|
|
25
|
+
FontGlyphFeatureExtractResult,
|
|
26
|
+
FontGlyphExtractResult,
|
|
27
|
+
FontGlyphSlot,
|
|
28
|
+
FontGlyphSlotExtractResult,
|
|
29
|
+
GlyphDatasetExportResult,
|
|
30
|
+
FontComponent,
|
|
31
|
+
LocateResult,
|
|
32
|
+
SliderGap,
|
|
33
|
+
SliderLocateResult,
|
|
34
|
+
TextLayerResult,
|
|
35
|
+
TextLocateResult,
|
|
36
|
+
TextRegion,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
"__version__",
|
|
41
|
+
"CaptchaRecognizer",
|
|
42
|
+
"CaptchaFontLocator",
|
|
43
|
+
"CaptchaSliderLocator",
|
|
44
|
+
"BackgroundRestoreResult",
|
|
45
|
+
"BackgroundMeta",
|
|
46
|
+
"CaptchaType",
|
|
47
|
+
"BatchGlyphExtractItem",
|
|
48
|
+
"BatchGlyphExtractResult",
|
|
49
|
+
"FontGlyph",
|
|
50
|
+
"FontGlyphImageExportResult",
|
|
51
|
+
"FontGlyphImageItem",
|
|
52
|
+
"FontGlyphFeature",
|
|
53
|
+
"FontGlyphFeatureExtractResult",
|
|
54
|
+
"FontGlyphExtractResult",
|
|
55
|
+
"FontGlyphSlot",
|
|
56
|
+
"FontGlyphSlotExtractResult",
|
|
57
|
+
"GlyphDatasetExportResult",
|
|
58
|
+
"LocalRestoreConfig",
|
|
59
|
+
"LocalRestoreStatus",
|
|
60
|
+
"LocalRestoreSummary",
|
|
61
|
+
"BucketSummary",
|
|
62
|
+
"ProgressCallback",
|
|
63
|
+
"StopChecker",
|
|
64
|
+
"ProcessingErrorItem",
|
|
65
|
+
"FontComponent",
|
|
66
|
+
"LocateResult",
|
|
67
|
+
"SliderGap",
|
|
68
|
+
"SliderLocateResult",
|
|
69
|
+
"TextRegion",
|
|
70
|
+
"TextLocateResult",
|
|
71
|
+
"TextLayerResult",
|
|
72
|
+
]
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Dict, Iterable, List, Optional, Tuple
|
|
5
|
+
|
|
6
|
+
from PIL import Image
|
|
7
|
+
|
|
8
|
+
from .group_id import group_id_from_pixels
|
|
9
|
+
from .types import BackgroundMeta
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
_DEFAULT_IMAGE_EXTS = {".png", ".jpg", ".jpeg", ".webp", ".bmp"}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def load_rgba_pixels(image_path: str) -> Tuple[int, int, List[Tuple[int, ...]]]:
|
|
16
|
+
with Image.open(image_path) as im:
|
|
17
|
+
rgba = im.convert("RGBA")
|
|
18
|
+
width, height = rgba.size
|
|
19
|
+
return width, height, list(rgba.getdata())
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def compute_group_id(image_path: str) -> str:
|
|
23
|
+
width, height, pixels = load_rgba_pixels(image_path)
|
|
24
|
+
return group_id_from_pixels(pixels, width, height)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def build_background_index(
|
|
28
|
+
background_dir: str,
|
|
29
|
+
recursive: bool = True,
|
|
30
|
+
exts: Optional[Iterable[str]] = None,
|
|
31
|
+
) -> Dict[str, BackgroundMeta]:
|
|
32
|
+
root = Path(background_dir)
|
|
33
|
+
if not root.exists() or not root.is_dir():
|
|
34
|
+
raise FileNotFoundError(f"background_dir not found: {background_dir}")
|
|
35
|
+
|
|
36
|
+
valid_exts = _DEFAULT_IMAGE_EXTS if exts is None else {e.lower() for e in exts}
|
|
37
|
+
files = root.rglob("*") if recursive else root.glob("*")
|
|
38
|
+
|
|
39
|
+
index: Dict[str, BackgroundMeta] = {}
|
|
40
|
+
for file_path in files:
|
|
41
|
+
if not file_path.is_file() or file_path.suffix.lower() not in valid_exts:
|
|
42
|
+
continue
|
|
43
|
+
width, height, pixels = load_rgba_pixels(str(file_path))
|
|
44
|
+
group_id = group_id_from_pixels(pixels, width, height)
|
|
45
|
+
if group_id not in index:
|
|
46
|
+
index[group_id] = BackgroundMeta(
|
|
47
|
+
group_id=group_id,
|
|
48
|
+
image_path=str(file_path),
|
|
49
|
+
width=width,
|
|
50
|
+
height=height,
|
|
51
|
+
)
|
|
52
|
+
return index
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def build_background_index_from_files(
|
|
56
|
+
image_paths: Iterable[str],
|
|
57
|
+
exts: Optional[Iterable[str]] = None,
|
|
58
|
+
) -> Dict[str, BackgroundMeta]:
|
|
59
|
+
valid_exts = _DEFAULT_IMAGE_EXTS if exts is None else {e.lower() for e in exts}
|
|
60
|
+
index: Dict[str, BackgroundMeta] = {}
|
|
61
|
+
for image_path in image_paths:
|
|
62
|
+
file_path = Path(image_path)
|
|
63
|
+
if not file_path.is_file() or file_path.suffix.lower() not in valid_exts:
|
|
64
|
+
continue
|
|
65
|
+
width, height, pixels = load_rgba_pixels(str(file_path))
|
|
66
|
+
group_id = group_id_from_pixels(pixels, width, height)
|
|
67
|
+
if group_id not in index:
|
|
68
|
+
index[group_id] = BackgroundMeta(
|
|
69
|
+
group_id=group_id,
|
|
70
|
+
image_path=str(file_path),
|
|
71
|
+
width=width,
|
|
72
|
+
height=height,
|
|
73
|
+
)
|
|
74
|
+
return index
|