Python+OpenCV图像点选验证码自动绕过思路详解

在网络自动化测试或爬虫开发中,验证码往往是最大的阻碍,但针对常见的如TCaptcha等需要按顺序选择图形验证码,利用Python的OpenCV库进行图像识别,并配合Camoufox即可实现验证码的全自动化解决流程

声明:本文仅供技术研究与学习原理使用,请勿用于非法用途,文中所有提及厂商、品牌仅作示例用途。

环境配置

首先,我们需要安装一下Python环境,但注意这里必须使用Python 3.13或者3.12,过高或者过低的版本会导致Camoufox没法正常安装:

1
2
3
4
5
6
7
8
9
# 创建虚拟环境 (可选)
python3 -m venv .venv
source .venv/bin/activate # Windows使用 venv\Scripts\activate

# 安装需要的pip包
pip install camoufox opencv-python-headless pillow numpy requests

# 安装 Camoufox 浏览器核心
camoufox fetch

算法原理

这类验证码的逻辑通常是:

给出一张小图,上面有3个需要寻找的图像(或文字);给出一张大图(背景),图像散落在背景中。因此我们可以识别出小图中的图像形状,并在大图中找到对应形状的坐标。

图像点选验证码示例

1. 图像预处理与分割

面对复杂的验证码图片,第一步是简化图像信息,仅提取我们需要用到的部分:

需要按顺序选择的图形

背景图片

灰度化与二值化

无论是目标图还是背景图,颜色通常是干扰信息。我们将图片转换为灰度图,然后通过阈值处理将其转化为只有黑与白的二值图像。

背景图片二值图

这里主要使用了Otsu算法(大津法)。Otsu算法会自动遍历所有可能的阈值,寻找一个能使前景和背景类间方差最大的值。

公式上,Otsu寻找阈值 $t$ 使得加权方差最小:
$$ \sigma_w^2(t) = \omega_0(t)\sigma_0^2(t) + \omega_1(t)\sigma_1^2(t) $$
其中 $\omega$ 是概率,$\sigma^2$ 是方差

1
2
3
img = cv2.imdecode(np.frombuffer(img_bytes, np.uint8), cv2.IMREAD_COLOR)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, mask = cv2.threshold(gray, 60, 255, cv2.THRESH_BINARY_INV)

形态学操作

在背景图中,噪点也是常见问题,因此需要使用开运算来去除噪点。开运算是先腐蚀后膨胀的过程,它可以消除细小的白色物体(噪点),同时保持较大物体的形状和尺寸不变。

原理:

$$ A \circ B = (A \ominus B) \oplus B $$

1
2
kernel = np.ones((3, 3), np.uint8)
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)

2. 轮廓提取

在二值化图像上,我们需要找到字符的边缘。OpenCV的 findContours 函数可以提取出图像中的所有闭合曲线。

  • 目标图处理:我们将目标栏切割成三等分(这里假设每个字符占据1/3宽度),分别提取每一部分中面积最大的轮廓作为模板
  • 背景图处理:提取背景中所有显著的轮廓作为候选集

3. 形状匹配

这是解决此类验证码的核心步骤,拿到模板轮廓后,需要去背景候选集中寻找形状最相似的轮廓。

这里使用的是Hu矩。Hu矩由7个数值组成,它们具有平移、旋转和缩放不变性。也就是说,即使背景里的字符比目标栏里的字符大一点、歪一点,Hu矩也能识别出它们是同一个字(抗缩放、旋转等变换)。

cv2.matchShapes 函数计算两个轮廓的Hu矩距离,返回值越小,相似度越高:

$$ D(A, B) = \sum_{i=1…7} \left| \frac{1}{m_i^A} - \frac{1}{m_i^B} \right| $$

背景图片中的所有轮廓

1
2
3
4
best_cnt = min(
bg_cnts,
key=lambda c: cv2.matchShapes(t_cnt, c, cv2.CONTOURS_MATCH_I1, 0.0),
)

4. 质心计算

找到最佳匹配轮廓后,我们需要知道点击哪里。通过计算轮廓的矩(Moments),可以得出图形的中心点坐标 $(C_x, C_y)$:

$$ C_x = \frac{M_{10}}{M_{00}}, \quad C_y = \frac{M_{01}}{M_{00}} $$

其中 $M_{00}$ 是零阶矩(面积),$M_{10}$ 和 $M_{01}$ 是一阶矩

1
2
3
4
5
6
M = cv2.moments(best_cnt)
points.append(
(int(M["m10"] / M["m00"]), int(M["m01"] / M["m00"]))
if M["m00"]
else (0, 0)
)

最终结果

浏览器自动化:Camoufox

传统的Selenium配合ChromeDriver特征极其明显,很容易被风控系统识别(如navigator.webdriver属性)。而Camoufox经过高度定制,模拟了真实用户的指纹,并且内置了反检测机制,非常适合处理此类任务。

刚才已经成功获取到了匹配到的图形位置,现在我们只需要获取这些图像相对页面的绝对坐标,然后模拟点击即可:

1
2
3
4
5
6
7
8
9
# 按顺序点击目标图形
for px, py in pts:
page.mouse.click(
box["x"] + px * (box["width"] / shape[1]),
box["y"] + py * (box["height"] / shape[0]),
)
page.wait_for_timeout(300)
# 点击确认按钮
page.click(".tencent-captcha-dy__verify-confirm-btn")

我们通过拦截Console日志来获取最终的验证Token,避免注入环境:

1
page.on("console", lambda m: ...)

最后,我们就完成了该类验证码的完整解决过程,拿到ticket后,在目标页面直接拦截并用获取到的ticket等数据调用callback即可。


点击展开查看完整代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- captcha.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="https://turing.captcha.qcloud.com/TJCaptcha.js"></script>
</head>
<body>
<button id="btn"></button>
<script>
const params = new URLSearchParams(window.location.search);
const APP_ID = params.get('appid');
function callback(res) { console.log('CAPTCHA_RESULT:' + JSON.stringify(res)); }
document.getElementById('btn').addEventListener('click', () => {
new TencentCaptcha(APP_ID, callback, { userLanguage: 'zh-cn' }).show();
});
</script>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
import os, re, json, base64, cv2, numpy as np, requests, argparse
from camoufox.sync_api import Camoufox

class CaptchaSolver:
def __init__(self, headless=True):
self.headless = headless
self.html_path = f"file://{os.path.abspath('captcha.html')}"
def _get_mask(self, img_bytes):
"""从图片字节流生成二值掩码"""
# 解码图片
img = cv2.imdecode(np.frombuffer(img_bytes, np.uint8), cv2.IMREAD_COLOR)
# 转换为灰度图
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 二值化处理
_, mask = cv2.threshold(gray, 60, 255, cv2.THRESH_BINARY_INV)
# 形态学开运算去除噪声
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, np.ones((3, 3), np.uint8))
return img, mask

def _solve_cv(self, top_bytes, bg_bytes):
"""使用计算机视觉解决验证码"""
# 解码顶部模板图
header = cv2.imdecode(np.frombuffer(top_bytes, np.uint8), cv2.IMREAD_COLOR)
h, w = header.shape[:2]
pw = w // 3 # 每个模板的宽度
templates = []
# 分割并处理三个模板
for i in range(3):
part = header[:, i * pw : (i + 1) * pw]
# 使用 Otsu 算法自动确定阈值并二值化
_, b = cv2.threshold(
cv2.cvtColor(part, cv2.COLOR_BGR2GRAY),
0,
255,
cv2.THRESH_BINARY + cv2.THRESH_OTSU,
)
# 如果黑色像素多,则反转
if np.sum(b == 255) < b.size / 2:
b = cv2.bitwise_not(b)
# 提取轮廓
cnts, _ = cv2.findContours(
cv2.bitwise_not(b), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
)
# 保存最大的轮廓作为模板
templates.append(max(cnts, key=cv2.contourArea) if cnts else None)
# 获取背景图和掩码
bg_img, mask = self._get_mask(bg_bytes)
# 从掩码中提取轮廓
bg_cnts, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# 过滤掉面积过小的轮廓
bg_cnts = [c for c in bg_cnts if cv2.contourArea(c) > 50]
points = []
# 对每个模板进行形状匹配
for t_cnt in templates:
if t_cnt is None:
continue
# 找到与模板最匹配的背景轮廓
best_cnt = min(
bg_cnts,
key=lambda c: cv2.matchShapes(t_cnt, c, cv2.CONTOURS_MATCH_I1, 0.0),
)
# 计算轮廓的质心作为点击位置
M = cv2.moments(best_cnt)
points.append(
(int(M["m10"] / M["m00"]), int(M["m01"] / M["m00"]))
if M["m00"]
else (0, 0)
)
return points, bg_img.shape
def get_token(self, appid):
"""获取验证码 token """
with Camoufox(headless=self.headless) as browser:
page = browser.new_page()
res_data = {"val": None}
# 监听控制台消息,捕获验证结果
page.on(
"console",
lambda m: (
res_data.update({"val": json.loads(m.text[15:])})
if m.text.startswith("CAPTCHA_RESULT:")
else None
),
)
# 加载验证码页面
page.goto(f"{self.html_path}?appid={appid}")
page.click("#btn")
page.wait_for_selector(".tencent-captcha-dy__verify-bg-img", timeout=10000)
# 获取验证码区域的位置信息
box = page.locator(".tencent-captcha-dy__verify-bg-img").bounding_box()
# 获取顶部模板图片
top_src = page.locator(
".tencent-captcha-dy__header-answer img"
).first.get_attribute("src")
# 获取背景图片
bg_style = page.locator(
".tencent-captcha-dy__verify-bg-img"
).first.evaluate("el=>window.getComputedStyle(el).backgroundImage")
bg_src = re.search(r'url\("?(.+?)"?\)', bg_style).group(1)
# 下载图片数据
t_bytes = (
base64.b64decode(s=top_src.split(",")[1])
if "base64" in top_src
else requests.get(top_src).content
)
b_bytes = requests.get(bg_src).content
# 使用 CV 算法解决验证码
pts, shape = self._solve_cv(t_bytes, b_bytes)
# 在计算出的位置依次点击
for px, py in pts:
page.mouse.click(
box["x"] + px * (box["width"] / shape[1]),
box["y"] + py * (box["height"] / shape[0]),
)
page.wait_for_timeout(300)
# 点击确认按钮
page.click(".tencent-captcha-dy__verify-confirm-btn")
# 等待验证结果
for _ in range(20):
if res_data["val"]:
return res_data["val"]
page.wait_for_timeout(500)
return None

def main():
parser = argparse.ArgumentParser(description='captcha demo')
parser.add_argument('--headless', action='store_true',
help='enable headless mode')
parser.add_argument('--appid', type=str, required=True,
help='captcha\'s appid')
args = parser.parse_args()
solver = CaptchaSolver(headless=args.headless)
token = solver.get_token(args.appid)
if token:
print(f"done!\ncaptcha data:")
print(json.dumps(token, indent=2, ensure_ascii=False))
else:
print("captcha failed!")

if __name__ == "__main__":
main()

Python+OpenCV图像点选验证码自动绕过思路详解
https://s3.fan/2026/02/12/Python-OpenCV图像点选验证码自动绕过思路详解/
作者
Steve3184
发布于
2026年2月12日
许可协议