Feibo.AntiCAP 3.3.6__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.
- feibo_anticap-3.3.6/AntiCAP/__init__.py +5 -0
- feibo_anticap-3.3.6/AntiCAP/core.py +116 -0
- feibo_anticap-3.3.6/AntiCAP/modules/__init__.py +8 -0
- feibo_anticap-3.3.6/AntiCAP/modules/click.py +113 -0
- feibo_anticap-3.3.6/AntiCAP/modules/detection.py +41 -0
- feibo_anticap-3.3.6/AntiCAP/modules/geetest.py +221 -0
- feibo_anticap-3.3.6/AntiCAP/modules/math_solver.py +62 -0
- feibo_anticap-3.3.6/AntiCAP/modules/ocr.py +46 -0
- feibo_anticap-3.3.6/AntiCAP/modules/rotation.py +196 -0
- feibo_anticap-3.3.6/AntiCAP/modules/similarity.py +80 -0
- feibo_anticap-3.3.6/AntiCAP/modules/slider.py +141 -0
- feibo_anticap-3.3.6/AntiCAP/utils/__init__.py +0 -0
- feibo_anticap-3.3.6/AntiCAP/utils/common.py +83 -0
- feibo_anticap-3.3.6/AntiCAP/utils/exceptions.py +2 -0
- feibo_anticap-3.3.6/AntiCAP/utils/manager.py +137 -0
- feibo_anticap-3.3.6/Feibo.AntiCAP.egg-info/PKG-INFO +611 -0
- feibo_anticap-3.3.6/Feibo.AntiCAP.egg-info/SOURCES.txt +24 -0
- feibo_anticap-3.3.6/Feibo.AntiCAP.egg-info/dependency_links.txt +1 -0
- feibo_anticap-3.3.6/Feibo.AntiCAP.egg-info/requires.txt +8 -0
- feibo_anticap-3.3.6/Feibo.AntiCAP.egg-info/top_level.txt +1 -0
- feibo_anticap-3.3.6/LICENSE +21 -0
- feibo_anticap-3.3.6/MANIFEST.in +0 -0
- feibo_anticap-3.3.6/PKG-INFO +611 -0
- feibo_anticap-3.3.6/README.md +574 -0
- feibo_anticap-3.3.6/setup.cfg +4 -0
- feibo_anticap-3.3.6/setup.py +38 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# coding=utf-8
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from AntiCAP.utils.common import get_model_path
|
|
6
|
+
from .utils.manager import ModelManager
|
|
7
|
+
from .modules import ocr, math_solver, detection, click, slider, similarity, rotation, geetest
|
|
8
|
+
|
|
9
|
+
class Handler(object):
|
|
10
|
+
logging.getLogger('ultralytics').setLevel(logging.WARNING)
|
|
11
|
+
|
|
12
|
+
def __init__(self, show_banner=True):
|
|
13
|
+
self.manager = ModelManager()
|
|
14
|
+
|
|
15
|
+
if show_banner:
|
|
16
|
+
print('''
|
|
17
|
+
-----------------------------------------------------------
|
|
18
|
+
| _ _ _ ____ _ ____ |
|
|
19
|
+
| / \ _ __ | |_ (_) / ___| / \ | _ \ |
|
|
20
|
+
| / _ \ | '_ \ | __| | | | | / _ \ | |_) | |
|
|
21
|
+
| / ___ \ | | | | | |_ | | | |___ / ___ \ | __/ |
|
|
22
|
+
| /_/ \_\ |_| |_| \__| |_| \____| /_/ \_\ |_| |
|
|
23
|
+
-----------------------------------------------------------
|
|
24
|
+
| Github: https://github.com/81NewArk/AntiCAP |
|
|
25
|
+
| Blog : https://www.newark81.vip/ |
|
|
26
|
+
| Author: 81NewArk |
|
|
27
|
+
-----------------------------------------------------------''')
|
|
28
|
+
|
|
29
|
+
# 文字识别
|
|
30
|
+
def OCR(self, img_base64: str = None, use_gpu: bool = False, png_fix: bool = False, probability=False):
|
|
31
|
+
return ocr.solve_ocr(self.manager, img_base64, use_gpu, png_fix, probability)
|
|
32
|
+
|
|
33
|
+
# 算术识别
|
|
34
|
+
def Math(self, img_base64: str, math_model_path: str = '', use_gpu: bool = False):
|
|
35
|
+
return math_solver.solve_math(self.manager, img_base64, math_model_path, use_gpu)
|
|
36
|
+
|
|
37
|
+
# 图标侦测
|
|
38
|
+
def Detection_Icon(self, img_base64: str = None, detectionIcon_model_path: str = '', use_gpu: bool = False):
|
|
39
|
+
return detection.solve_detection_icon(self.manager, img_base64, detectionIcon_model_path, use_gpu)
|
|
40
|
+
|
|
41
|
+
# 按序侦测图标
|
|
42
|
+
def ClickIcon_Order(self, order_img_base64: str, target_img_base64: str, detectionIcon_model_path: str = '', sim_onnx_model_path: str = '', use_gpu: bool = False):
|
|
43
|
+
return click.solve_click_icon_order(self.manager, order_img_base64, target_img_base64, detectionIcon_model_path, sim_onnx_model_path, use_gpu)
|
|
44
|
+
|
|
45
|
+
# 文字侦测
|
|
46
|
+
def Detection_Text(self, img_base64: str = None, detectionText_model_path: str = '', use_gpu: bool = False):
|
|
47
|
+
return detection.solve_detection_text(self.manager, img_base64, detectionText_model_path, use_gpu)
|
|
48
|
+
|
|
49
|
+
# 按序侦测文字
|
|
50
|
+
def ClickText_Order(self, order_img_base64: str, target_img_base64: str, detectionText_model_path: str = '', sim_onnx_model_path: str = '', use_gpu: bool = False):
|
|
51
|
+
return click.solve_click_text_order(self.manager, order_img_base64, target_img_base64, detectionText_model_path, sim_onnx_model_path, use_gpu)
|
|
52
|
+
|
|
53
|
+
# 缺口滑块
|
|
54
|
+
def Slider_Match(self, target_base64: str = None, background_base64: str = None, simple_target: bool = False, flag: bool = False):
|
|
55
|
+
return slider.solve_slider_match(self.manager, target_base64, background_base64, simple_target, flag)
|
|
56
|
+
|
|
57
|
+
# 阴影滑块
|
|
58
|
+
def Slider_Comparison(self, target_base64: str = None, background_base64: str = None):
|
|
59
|
+
return slider.solve_slider_comparison(self.manager, target_base64, background_base64)
|
|
60
|
+
|
|
61
|
+
# 图像相似度比较
|
|
62
|
+
def Compare_Image_Similarity(self, image1_base64: str, image2_base64: str, sim_onnx_model_path: str = None, use_gpu: bool = False):
|
|
63
|
+
return similarity.solve_compare_image_similarity(self.manager, image1_base64, image2_base64, sim_onnx_model_path, use_gpu)
|
|
64
|
+
|
|
65
|
+
# 单图旋转角度
|
|
66
|
+
def Single_Rotate(self, img_base64: str, rotate_onnx_modex_path: str = '', use_gpu: bool = False):
|
|
67
|
+
return rotation.solve_single_rotate(self.manager, img_base64, rotate_onnx_modex_path, use_gpu)
|
|
68
|
+
|
|
69
|
+
# 双图旋转
|
|
70
|
+
def Double_Rotate(self, inside_base64: str, outside_base64: str, check_pixel: int = 10, speed_ratio: float = 1, grayscale: bool = False, anticlockwise: bool = False, cut_pixel_value: int = 0):
|
|
71
|
+
return rotation.solve_double_rotate(self.manager, inside_base64, outside_base64, check_pixel, speed_ratio, grayscale, anticlockwise, cut_pixel_value)
|
|
72
|
+
|
|
73
|
+
# 极验滑块
|
|
74
|
+
def Geetest_SlideCAPTCHA(self, img_base64: str, use_gpu: bool = False):
|
|
75
|
+
return geetest.SlideCAPTCHA(self.manager, img_base64, use_gpu)
|
|
76
|
+
|
|
77
|
+
# 极验图标点选 - 图标识别
|
|
78
|
+
def Geetest_IconClick_Icon(self, img_base64: str, model_path: str = '', use_gpu: bool = False):
|
|
79
|
+
"""
|
|
80
|
+
识别极验图标点选验证码中的图标
|
|
81
|
+
|
|
82
|
+
使用 GeetestIconClick_Icon.pt 模型检测图片中的所有图标,
|
|
83
|
+
返回每个图标的类别、位置和置信度。
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
img_base64: base64 编码的验证码图片
|
|
87
|
+
model_path: 自定义模型路径(可选,默认使用内置模型)
|
|
88
|
+
use_gpu: 是否使用 GPU 推理
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
list: [{'class': str, 'box': [x1,y1,x2,y2], 'confidence': float}, ...]
|
|
92
|
+
"""
|
|
93
|
+
return geetest.IconClick_Icon(self.manager, img_base64, model_path, use_gpu)
|
|
94
|
+
|
|
95
|
+
# 极验图标点选 - 图标相似度比较 (ICONCNN)
|
|
96
|
+
def Geetest_IconClick_Similarity(self, img1_base64: str, img2_base64: str, model_path: str = '', use_gpu: bool = False):
|
|
97
|
+
"""
|
|
98
|
+
使用 ICONCNN 模型比较两张极验图标的相似度
|
|
99
|
+
|
|
100
|
+
通过 GeetestIconClick_Group_Icon-ICONCNN.onnx 模型提取两张图片的特征向量,
|
|
101
|
+
计算余弦相似度,并返回匹配结果与置信度。
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
img1_base64: 第一张图标的 base64 编码
|
|
105
|
+
img2_base64: 第二张图标的 base64 编码
|
|
106
|
+
model_path: 自定义模型路径(可选,默认使用内置 ICONCNN 模型)
|
|
107
|
+
use_gpu: 是否使用 GPU 推理
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
dict: {'similarity': float, 'is_match': bool, 'confidence': str}
|
|
111
|
+
"""
|
|
112
|
+
model_path = model_path or get_model_path('GeetestIconClick_Group_Icon-ICONCNN.onnx')
|
|
113
|
+
return geetest.IconClick_Similarity(self.manager, img1_base64, img2_base64, model_path, use_gpu)
|
|
114
|
+
def Geetest_IconClick_Similarity2(self, img1_base64: str, img2_base64: str, model_path: str = '', use_gpu: bool = False):
|
|
115
|
+
model_path= model_path or get_model_path('GeetestIconClick_Group_Icon-ICONCNN.onnx')
|
|
116
|
+
return similarity.solve_compare_image_similarity(self.manager, img1_base64, img2_base64, model_path , use_gpu)
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from scipy.optimize import linear_sum_assignment
|
|
3
|
+
from .similarity import get_siamese_similarity
|
|
4
|
+
from ..utils.common import get_model_path, decode_base64_to_image
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def handle_matching(manager, order_image, target_image, order_boxes_list, target_boxes_list, siamese_model_path, use_gpu, sim_onnx_model_path):
|
|
8
|
+
"""
|
|
9
|
+
通用匹配处理函数,使用匈牙利算法(Hungarian Algorithm)求解全局最优匹配
|
|
10
|
+
"""
|
|
11
|
+
if not order_boxes_list or not target_boxes_list:
|
|
12
|
+
return [[0, 0, 0, 0]] * len(order_boxes_list)
|
|
13
|
+
|
|
14
|
+
num_orders = len(order_boxes_list)
|
|
15
|
+
num_targets = len(target_boxes_list)
|
|
16
|
+
|
|
17
|
+
# 1. 构建相似度矩阵 (Cost Matrix)
|
|
18
|
+
# 我们希望总相似度最大,linear_sum_assignment 是求最小开销,所以存入负的相似度
|
|
19
|
+
cost_matrix = np.zeros((num_orders, num_targets))
|
|
20
|
+
|
|
21
|
+
for i, order_box in enumerate(order_boxes_list):
|
|
22
|
+
order_crop = order_image.crop(order_box)
|
|
23
|
+
if order_crop.width == 0 or order_crop.height == 0:
|
|
24
|
+
continue
|
|
25
|
+
|
|
26
|
+
for j, target_box in enumerate(target_boxes_list):
|
|
27
|
+
target_crop = target_image.crop(target_box)
|
|
28
|
+
if target_crop.width == 0 or target_crop.height == 0:
|
|
29
|
+
continue
|
|
30
|
+
|
|
31
|
+
similarity_score = get_siamese_similarity(
|
|
32
|
+
manager, order_crop, target_crop,
|
|
33
|
+
siamese_model_path, use_gpu,
|
|
34
|
+
is_custom_model=bool(sim_onnx_model_path)
|
|
35
|
+
)
|
|
36
|
+
# 记录分值 (由于求解器求最小,我们存负值)
|
|
37
|
+
cost_matrix[i, j] = -similarity_score
|
|
38
|
+
|
|
39
|
+
# 2. 执行匈牙利算法求解
|
|
40
|
+
# row_ind 会对应 order_boxes_list 的索引,col_ind 会对应最优匹配的 target_boxes_list 索引
|
|
41
|
+
row_ind, col_ind = linear_sum_assignment(cost_matrix)
|
|
42
|
+
|
|
43
|
+
# 3. 构造返回结果,保持与 order_boxes_list 顺序一致
|
|
44
|
+
# 初始化全 0 结果
|
|
45
|
+
best_matching_boxes = [[0, 0, 0, 0]] * num_orders
|
|
46
|
+
|
|
47
|
+
# 填充结果
|
|
48
|
+
# 注意:如果目标比候选框多,这里的 row_ind/col_ind 只会包含匹配上的部分
|
|
49
|
+
for r, c in zip(row_ind, col_ind):
|
|
50
|
+
# 即使匹配上了,如果得分太低(例如相似度为 0),也可以视作无效匹配
|
|
51
|
+
# 这里保留原逻辑的灵活性,目前只要有匹配索引就写入
|
|
52
|
+
target_box = target_boxes_list[c]
|
|
53
|
+
best_matching_boxes[r] = [int(coord) for coord in target_box]
|
|
54
|
+
|
|
55
|
+
return best_matching_boxes
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def solve_click_icon_order(manager, order_img_base64: str, target_img_base64: str, detectionIcon_model_path: str = '', sim_onnx_model_path: str = '', use_gpu: bool = False):
|
|
59
|
+
detectionIcon_model_path = detectionIcon_model_path or get_model_path('[AntiCAP]-Detection_Icon-YOLO.pt')
|
|
60
|
+
|
|
61
|
+
if sim_onnx_model_path:
|
|
62
|
+
siamese_model_path = sim_onnx_model_path
|
|
63
|
+
else:
|
|
64
|
+
siamese_model_path = get_model_path('[AntiCAP]-Siamese-ResNet18.onnx')
|
|
65
|
+
|
|
66
|
+
model = manager.get_yolo_model(detectionIcon_model_path, use_gpu)
|
|
67
|
+
|
|
68
|
+
order_image = decode_base64_to_image(order_img_base64).convert("RGB")
|
|
69
|
+
target_image = decode_base64_to_image(target_img_base64).convert("RGB")
|
|
70
|
+
|
|
71
|
+
order_results = model(order_image, verbose=False)
|
|
72
|
+
target_results = model(target_image, verbose=False)
|
|
73
|
+
|
|
74
|
+
order_boxes_list = []
|
|
75
|
+
if order_results and order_results[0].boxes:
|
|
76
|
+
order_boxes = order_results[0].boxes.xyxy.cpu().numpy().tolist()
|
|
77
|
+
order_boxes.sort(key=lambda x: x[0]) # 按提示图的 X 坐标排序(通常文字提示是从左到右的)
|
|
78
|
+
order_boxes_list = order_boxes
|
|
79
|
+
|
|
80
|
+
target_boxes_list = []
|
|
81
|
+
if target_results and target_results[0].boxes:
|
|
82
|
+
target_boxes_list = target_results[0].boxes.xyxy.cpu().numpy().tolist()
|
|
83
|
+
|
|
84
|
+
return handle_matching(manager, order_image, target_image, order_boxes_list, target_boxes_list, siamese_model_path, use_gpu, sim_onnx_model_path)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def solve_click_text_order(manager, order_img_base64: str, target_img_base64: str, detectionText_model_path: str = '', sim_onnx_model_path: str = '', use_gpu: bool = False):
|
|
88
|
+
detectionText_model_path = detectionText_model_path or get_model_path('[AntiCAP]-Detection_Text-YOLO.pt')
|
|
89
|
+
|
|
90
|
+
if sim_onnx_model_path:
|
|
91
|
+
siamese_model_path = sim_onnx_model_path
|
|
92
|
+
else:
|
|
93
|
+
siamese_model_path = get_model_path('[AntiCAP]-Siamese-ResNet18.onnx')
|
|
94
|
+
|
|
95
|
+
model = manager.get_yolo_model(detectionText_model_path, use_gpu)
|
|
96
|
+
|
|
97
|
+
order_image = decode_base64_to_image(order_img_base64).convert("RGB")
|
|
98
|
+
target_image = decode_base64_to_image(target_img_base64).convert("RGB")
|
|
99
|
+
|
|
100
|
+
order_results = model(order_image, verbose=False)
|
|
101
|
+
target_results = model(target_image, verbose=False)
|
|
102
|
+
|
|
103
|
+
order_boxes_list = []
|
|
104
|
+
if order_results and order_results[0].boxes:
|
|
105
|
+
order_boxes = order_results[0].boxes.xyxy.cpu().numpy().tolist()
|
|
106
|
+
order_boxes.sort(key=lambda x: x[0])
|
|
107
|
+
order_boxes_list = order_boxes
|
|
108
|
+
|
|
109
|
+
target_boxes_list = []
|
|
110
|
+
if target_results and target_results[0].boxes:
|
|
111
|
+
target_boxes_list = target_results[0].boxes.xyxy.cpu().numpy().tolist()
|
|
112
|
+
|
|
113
|
+
return handle_matching(manager, order_image, target_image, order_boxes_list, target_boxes_list, siamese_model_path, use_gpu, sim_onnx_model_path)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from ..utils.common import get_model_path, decode_base64_to_image
|
|
2
|
+
|
|
3
|
+
def solve_detection_icon(manager, img_base64: str = None, detectionIcon_model_path: str = '', use_gpu: bool = False):
|
|
4
|
+
detectionIcon_model_path = detectionIcon_model_path or get_model_path('[AntiCAP]-Detection_Icon-YOLO.pt')
|
|
5
|
+
model = manager.get_yolo_model(detectionIcon_model_path, use_gpu)
|
|
6
|
+
|
|
7
|
+
image = decode_base64_to_image(img_base64)
|
|
8
|
+
|
|
9
|
+
results = model(image)
|
|
10
|
+
|
|
11
|
+
detections = []
|
|
12
|
+
for box in results[0].boxes:
|
|
13
|
+
coords = box.xyxy[0].tolist()
|
|
14
|
+
rounded_box = [round(coord, 2) for coord in coords]
|
|
15
|
+
class_name = results[0].names[int(box.cls[0])]
|
|
16
|
+
detections.append({
|
|
17
|
+
'class': class_name,
|
|
18
|
+
'box': rounded_box
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
return detections
|
|
22
|
+
|
|
23
|
+
def solve_detection_text(manager, img_base64: str = None, detectionText_model_path: str = '', use_gpu: bool = False):
|
|
24
|
+
detectionText_model_path = detectionText_model_path or get_model_path('[AntiCAP]-Detection_Text-YOLO.pt')
|
|
25
|
+
model = manager.get_yolo_model(detectionText_model_path, use_gpu)
|
|
26
|
+
|
|
27
|
+
image = decode_base64_to_image(img_base64).convert("RGB")
|
|
28
|
+
|
|
29
|
+
results = model(image)
|
|
30
|
+
|
|
31
|
+
detections = []
|
|
32
|
+
for box in results[0].boxes:
|
|
33
|
+
coords = box.xyxy[0].tolist()
|
|
34
|
+
rounded_box = [round(coord, 2) for coord in coords]
|
|
35
|
+
class_name = results[0].names[int(box.cls[0])]
|
|
36
|
+
detections.append({
|
|
37
|
+
'class': class_name,
|
|
38
|
+
'box': rounded_box
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
return detections
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
极验验证码识别模块
|
|
4
|
+
|
|
5
|
+
包含两种极验验证码的识别功能:
|
|
6
|
+
1. SlideCAPTCHA: 极验滑块验证码缺口识别
|
|
7
|
+
- 使用 YOLO ONNX 模型识别极验滑块验证码中的缺口位置
|
|
8
|
+
- 模型输出格式为 [x, y, w, h, confidence],通过 NMS 去重后返回缺口坐标
|
|
9
|
+
|
|
10
|
+
2. IconClick_Icon: 极验图标点选验证码的图标识别
|
|
11
|
+
- 使用 YOLO PT 模型识别极验图标点选验证码中的图标位置和类别
|
|
12
|
+
- 返回每个图标的类别名称、位置坐标和置信度
|
|
13
|
+
- 可用于按顺序点选图标等场景
|
|
14
|
+
|
|
15
|
+
3. IconClick_GroupIcon: 极验图标点选验证码的图标相似度比较
|
|
16
|
+
- 使用 GeetestIconClick_Group_Icon-ICONCNN.onnx 模型提取图标特征向量
|
|
17
|
+
- 计算两张图片的余弦相似度,判断是否匹配及置信度
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import numpy as np
|
|
21
|
+
import cv2
|
|
22
|
+
from PIL import Image
|
|
23
|
+
import torch
|
|
24
|
+
import torch.nn.functional as F
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
from AntiCAP.modules import similarity
|
|
28
|
+
from ..utils.common import get_model_path, decode_base64_to_image
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def SlideCAPTCHA(manager, img_base64: str, use_gpu: bool = False):
|
|
32
|
+
"""
|
|
33
|
+
识别极验滑块验证码的缺口位置
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
manager: ModelManager 实例,用于管理 ONNX 模型会话
|
|
37
|
+
img_base64: base64 编码的验证码图片字符串
|
|
38
|
+
use_gpu: 是否使用 GPU 进行推理
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
list: 缺口坐标 [x1, y1, x2, y2],未检测到则返回空列表
|
|
42
|
+
x1, y1 为左上角坐标,x2, y2 为右下角坐标
|
|
43
|
+
"""
|
|
44
|
+
# 获取模型路径并创建 ONNX 推理会话
|
|
45
|
+
model_path = get_model_path('GeetestSlideCAPTCHA.onnx')
|
|
46
|
+
session = manager.get_onnx_session(model_path, use_gpu)
|
|
47
|
+
|
|
48
|
+
# 解码 base64 图片并转换为 RGB 格式
|
|
49
|
+
img = decode_base64_to_image(img_base64).convert("RGB")
|
|
50
|
+
|
|
51
|
+
# 获取模型输入尺寸,优先从模型元数据中读取,默认 640x640
|
|
52
|
+
input_meta = session.get_inputs()[0]
|
|
53
|
+
if len(input_meta.shape) == 4 and isinstance(input_meta.shape[2], int) and isinstance(input_meta.shape[3], int):
|
|
54
|
+
input_size = (input_meta.shape[3], input_meta.shape[2]) # (width, height)
|
|
55
|
+
else:
|
|
56
|
+
input_size = (640, 640)
|
|
57
|
+
|
|
58
|
+
# 记录原始图片尺寸,用于后续坐标还原
|
|
59
|
+
orig_w, orig_h = img.size
|
|
60
|
+
|
|
61
|
+
# 将图片缩放到模型输入尺寸,使用 LANCZOS 插值保持清晰度
|
|
62
|
+
img_resized = img.resize(input_size, Image.Resampling.LANCZOS)
|
|
63
|
+
|
|
64
|
+
# 图片预处理:归一化到 [0, 1] -> 转换为 CHW 格式 -> 添加 batch 维度
|
|
65
|
+
img_np = np.array(img_resized, dtype=np.float32) / 255.0
|
|
66
|
+
img_np = img_np.transpose(2, 0, 1) # HWC -> CHW
|
|
67
|
+
img_np = np.expand_dims(img_np, axis=0) # 添加 batch 维度
|
|
68
|
+
|
|
69
|
+
# 执行 ONNX 推理
|
|
70
|
+
ort_inputs = {session.get_inputs()[0].name: img_np}
|
|
71
|
+
outputs = session.run(None, ort_inputs)
|
|
72
|
+
output = outputs[0]
|
|
73
|
+
|
|
74
|
+
# 解析模型输出
|
|
75
|
+
# 输出格式: [batch, 5, anchors] -> 转置为 [anchors, 5]
|
|
76
|
+
# 每个 anchor 包含 [cx, cy, w, h, confidence]
|
|
77
|
+
detections = []
|
|
78
|
+
if output.ndim == 3:
|
|
79
|
+
output = output[0].T # 取第一个 batch 并转置
|
|
80
|
+
|
|
81
|
+
# 遍历所有预测框,过滤低置信度的结果
|
|
82
|
+
for detection in output:
|
|
83
|
+
cx, cy, w, h, confidence = detection[0], detection[1], detection[2], detection[3], detection[4]
|
|
84
|
+
if confidence > 0.25: # 置信度阈值
|
|
85
|
+
# 将中心点坐标 (cx, cy) 和宽高 (w, h) 转换为左上角和右下角坐标 (x1, y1, x2, y2)
|
|
86
|
+
# 同时将坐标从模型输入尺寸映射回原始图片尺寸
|
|
87
|
+
x1 = (cx - w / 2) * (orig_w / input_size[0])
|
|
88
|
+
y1 = (cy - h / 2) * (orig_h / input_size[1])
|
|
89
|
+
x2 = (cx + w / 2) * (orig_w / input_size[0])
|
|
90
|
+
y2 = (cy + h / 2) * (orig_h / input_size[1])
|
|
91
|
+
|
|
92
|
+
detections.append({
|
|
93
|
+
'class': 'quekou', # 缺口类别
|
|
94
|
+
'box': [x1, y1, x2, y2],
|
|
95
|
+
'confidence': float(confidence)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
# 使用 NMS(非极大值抑制)去除重叠的检测框
|
|
99
|
+
# YOLO 模型会对同一目标产生多个重叠框,NMS 可以保留最优的一个
|
|
100
|
+
if len(detections) > 1:
|
|
101
|
+
boxes = np.array([d['box'] for d in detections])
|
|
102
|
+
scores = np.array([d['confidence'] for d in detections])
|
|
103
|
+
# score_threshold: 置信度阈值,nms_threshold: IoU 阈值(重叠度超过此值则抑制)
|
|
104
|
+
indices = cv2.dnn.NMSBoxes(boxes.tolist(), scores.tolist(), score_threshold=0.25, nms_threshold=0.5)
|
|
105
|
+
|
|
106
|
+
if len(indices) > 0:
|
|
107
|
+
# 兼容不同版本的 NMSBoxes 返回值格式
|
|
108
|
+
indices = indices.flatten() if hasattr(indices, 'flatten') else np.array(indices).flatten()
|
|
109
|
+
detections = [detections[i] for i in indices]
|
|
110
|
+
|
|
111
|
+
# 坐标取整并移除置信度字段
|
|
112
|
+
for det in detections:
|
|
113
|
+
det['box'] = [int(coord) for coord in det['box']]
|
|
114
|
+
del det['confidence']
|
|
115
|
+
|
|
116
|
+
# 返回最优检测框的坐标,未检测到则返回空列表
|
|
117
|
+
if len(detections) > 0:
|
|
118
|
+
return detections[0]['box']
|
|
119
|
+
return []
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def IconClick_Icon(manager, img_base64: str, model_path: str = '', use_gpu: bool = False):
|
|
123
|
+
"""
|
|
124
|
+
识别极验图标点选验证码中的图标
|
|
125
|
+
|
|
126
|
+
使用 GeetestIconClick_Icon.pt YOLO 模型检测极验图标点选验证码图片中的所有图标,
|
|
127
|
+
返回每个图标的类别名称、位置坐标(左上角和右下角)和置信度。
|
|
128
|
+
|
|
129
|
+
极验图标点选验证码是一种常见的行为验证,用户需要按照给定的顺序
|
|
130
|
+
点击图片上的多个图标。该函数可以识别图片中所有可用图标及其位置,
|
|
131
|
+
结合图标排序逻辑即可完成自动点选。
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
manager: ModelManager 实例,用于管理 YOLO 模型
|
|
135
|
+
img_base64: base64 编码的验证码图片字符串
|
|
136
|
+
model_path: 自定义模型路径,默认为空时使用内置 GeetestIconClick_Detection_Icon-YOLO.pt
|
|
137
|
+
use_gpu: 是否使用 GPU 进行推理
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
list: 检测到的图标列表,每个元素包含:
|
|
141
|
+
- class: 图标类别名称 (str)
|
|
142
|
+
- box: 图标位置 [x1, y1, x2, y2] (左上角和右下角坐标)
|
|
143
|
+
- confidence: 检测置信度 (float, 0-1)
|
|
144
|
+
未检测到任何图标则返回空列表
|
|
145
|
+
"""
|
|
146
|
+
# 获取模型路径,优先使用自定义路径,否则使用内置 GeetestIconClick_Detection_Icon-YOLO.pt
|
|
147
|
+
model_path = model_path or get_model_path('GeetestIconClick_Detection_Icon-YOLO.pt')
|
|
148
|
+
model = manager.get_yolo_model(model_path, use_gpu)
|
|
149
|
+
|
|
150
|
+
# 解码 base64 图片并转换为 RGB 格式(YOLO 模型需要 RGB 输入)
|
|
151
|
+
image = decode_base64_to_image(img_base64).convert("RGB")
|
|
152
|
+
|
|
153
|
+
# 执行 YOLO 模型推理
|
|
154
|
+
results = model(image)
|
|
155
|
+
|
|
156
|
+
# 解析检测结果
|
|
157
|
+
detections = []
|
|
158
|
+
for box in results[0].boxes:
|
|
159
|
+
# 获取边界框坐标 (xyxy 格式: 左上角 x, 左上角 y, 右下角 x, 右下角 y)
|
|
160
|
+
coords = box.xyxy[0].tolist()
|
|
161
|
+
rounded_box = [round(coord, 2) for coord in coords]
|
|
162
|
+
|
|
163
|
+
# 获取类别名称(从模型的 names 字典中根据类别索引获取)
|
|
164
|
+
class_name = results[0].names[int(box.cls[0])]
|
|
165
|
+
|
|
166
|
+
# 获取检测置信度
|
|
167
|
+
confidence = round(float(box.conf[0]), 4)
|
|
168
|
+
|
|
169
|
+
detections.append({
|
|
170
|
+
'class': class_name,
|
|
171
|
+
'box': rounded_box,
|
|
172
|
+
'confidence': confidence
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
return detections
|
|
176
|
+
|
|
177
|
+
def IconClick_Similarity(manager, img1_base64: str, img2_base64: str, model_path: str = '', use_gpu: bool = False):
|
|
178
|
+
session= manager.get_onnx_session(model_path, use_gpu)
|
|
179
|
+
input_meta = session.get_inputs()[0]
|
|
180
|
+
input_name = input_meta.name
|
|
181
|
+
if len(input_meta.shape) == 4 and isinstance(input_meta.shape[2], int) and isinstance(input_meta.shape[3], int):
|
|
182
|
+
input_size = (input_meta.shape[3], input_meta.shape[2])
|
|
183
|
+
else:
|
|
184
|
+
input_size = (224, 224)
|
|
185
|
+
def preprocess(b64):
|
|
186
|
+
# base64 解码 -> RGB 转换 -> 缩放到模型输入尺寸
|
|
187
|
+
img = decode_base64_to_image(b64).convert("RGB").resize(input_size, Image.Resampling.LANCZOS)
|
|
188
|
+
# 归一化到 [0, 1] -> HWC 转 CHW -> 添加 batch 维度 (1, C, H, W)
|
|
189
|
+
arr = np.array(img, dtype=np.float32) / 255.0
|
|
190
|
+
arr = (arr - np.array([0.485, 0.456, 0.406], dtype=np.float32)) / np.array([0.229, 0.224, 0.225], dtype=np.float32)
|
|
191
|
+
arr = arr.transpose(2, 0, 1)[np.newaxis, ...] # (1, 3, 36, 36)
|
|
192
|
+
emb = session.run(None, {"input": arr})[0]
|
|
193
|
+
return torch.from_numpy(emb.squeeze(0))
|
|
194
|
+
|
|
195
|
+
def extract_embedding(b64):
|
|
196
|
+
tensor = preprocess(b64)
|
|
197
|
+
emb = session.run(None, {input_name: tensor})[0].flatten()
|
|
198
|
+
norm = np.linalg.norm(emb)
|
|
199
|
+
if norm > 0:
|
|
200
|
+
emb = emb / norm
|
|
201
|
+
return emb
|
|
202
|
+
|
|
203
|
+
emb1 = preprocess(img1_base64)
|
|
204
|
+
emb2 = preprocess(img2_base64)
|
|
205
|
+
sim = F.cosine_similarity(
|
|
206
|
+
emb1.unsqueeze(0), emb2.unsqueeze(0)
|
|
207
|
+
).item()
|
|
208
|
+
threshold = 0.8
|
|
209
|
+
is_match = sim >= threshold
|
|
210
|
+
if sim > 0.9:
|
|
211
|
+
confidence = "high"
|
|
212
|
+
elif sim >= threshold:
|
|
213
|
+
confidence = "medium"
|
|
214
|
+
else:
|
|
215
|
+
confidence = "low"
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
"similarity": round(sim, 4),
|
|
219
|
+
"is_match": is_match,
|
|
220
|
+
"confidence": confidence
|
|
221
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import re
|
|
3
|
+
from ..utils.common import get_model_path, decode_base64_to_image, resize_with_padding, decode
|
|
4
|
+
|
|
5
|
+
def solve_math(manager, img_base64: str, math_model_path: str = '', use_gpu: bool = False):
|
|
6
|
+
math_model_path = math_model_path or get_model_path('[AntiCAP]-CRNN_Math.onnx')
|
|
7
|
+
|
|
8
|
+
session = manager.get_onnx_session(math_model_path, use_gpu)
|
|
9
|
+
input_name = session.get_inputs()[0].name
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
IMG_H = 70
|
|
13
|
+
IMG_W = 200
|
|
14
|
+
CHARS = "0123456789+-*/÷×=?"
|
|
15
|
+
|
|
16
|
+
image = decode_base64_to_image(img_base64).convert('RGB')
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
image = resize_with_padding(image, (IMG_W, IMG_H))
|
|
20
|
+
|
|
21
|
+
img_np = np.array(image).astype(np.float32) / 255.0
|
|
22
|
+
img_np = np.transpose(img_np, (2, 0, 1)) # [C, H, W]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
img_np = (img_np - 0.5) / 0.5
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
img_np = np.expand_dims(img_np, axis=0) # [1, C, H, W]
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
ort_outs = session.run(None, {input_name: img_np})
|
|
32
|
+
preds = ort_outs[0] # Output 0
|
|
33
|
+
except Exception as e:
|
|
34
|
+
print(f"[AntiCAP] Inference error: {e}")
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
char_map_inv = {i + 1: c for i, c in enumerate(CHARS)}
|
|
39
|
+
result_str = decode(preds, char_map_inv)[0]
|
|
40
|
+
|
|
41
|
+
if not result_str:
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
expr = result_str
|
|
46
|
+
expr = expr.replace('×', '*').replace('÷', '/')
|
|
47
|
+
expr = expr.replace('?', '?')
|
|
48
|
+
expr = expr.replace('=', '')
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
expr = re.sub(r'[^0-9\+\-\*/]', '', expr)
|
|
52
|
+
|
|
53
|
+
if not expr:
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
result = eval(expr, {"__builtins__": None}, {})
|
|
59
|
+
return result
|
|
60
|
+
except Exception as e:
|
|
61
|
+
print(f"[AntiCAP] 表达式解析出错: {expr}, 错误: {e}")
|
|
62
|
+
return None
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from PIL import Image
|
|
3
|
+
|
|
4
|
+
from ..utils.common import get_model_path, decode_base64_to_image
|
|
5
|
+
|
|
6
|
+
def solve_ocr(manager, img_base64: str, use_gpu: bool = False, png_fix: bool = False, probability=False):
|
|
7
|
+
model_path = get_model_path('[Dddd]-OCR.onnx')
|
|
8
|
+
charset_path = get_model_path('[Dddd]-CharSets.txt')
|
|
9
|
+
|
|
10
|
+
charset = manager.get_charset(charset_path)
|
|
11
|
+
session = manager.get_onnx_session(model_path, use_gpu)
|
|
12
|
+
|
|
13
|
+
image = decode_base64_to_image(img_base64)
|
|
14
|
+
|
|
15
|
+
image = image.resize((int(image.size[0] * (64 / image.size[1])), 64), Image.Resampling.LANCZOS).convert('L')
|
|
16
|
+
image = np.array(image).astype(np.float32)
|
|
17
|
+
image = np.expand_dims(image, axis=0) / 255.
|
|
18
|
+
image = (image - 0.5) / 0.5
|
|
19
|
+
|
|
20
|
+
ort_inputs = {'input1': np.array([image]).astype(np.float32)}
|
|
21
|
+
ort_outs = session.run(None, ort_inputs)
|
|
22
|
+
|
|
23
|
+
result = []
|
|
24
|
+
last_item = 0
|
|
25
|
+
|
|
26
|
+
if not probability:
|
|
27
|
+
argmax_result = np.squeeze(np.argmax(ort_outs[0], axis=2))
|
|
28
|
+
for item in argmax_result:
|
|
29
|
+
if item == last_item:
|
|
30
|
+
continue
|
|
31
|
+
else:
|
|
32
|
+
last_item = item
|
|
33
|
+
if item != 0:
|
|
34
|
+
result.append(charset[item])
|
|
35
|
+
|
|
36
|
+
return ''.join(result)
|
|
37
|
+
else:
|
|
38
|
+
ort_outs = ort_outs[0]
|
|
39
|
+
ort_outs = np.exp(ort_outs) / np.sum(np.exp(ort_outs), axis=2, keepdims=True)
|
|
40
|
+
ort_outs_probability = np.squeeze(ort_outs).tolist()
|
|
41
|
+
|
|
42
|
+
result = {
|
|
43
|
+
'charsets': charset,
|
|
44
|
+
'probability': ort_outs_probability
|
|
45
|
+
}
|
|
46
|
+
return result
|