验证码对抗(四):弧度滑块——角度参数化、弧展开找缺口、沿弧拟人轨迹
滑块不走直线走弧线。从零跑通:脚本自己画弧形验证码再自己解,讲透用角度描述弧、把弧展开成一维信号找缺口、在角度上插值生成沿弧拖动轨迹、服务端多维度校验轨迹(时序/速度/形状/终点),以及内部/显示双坐标换算。
前面讲过普通滑块——沿直线把滑块拖到缺口。这篇讲它的进阶版:弧度滑块,滑块不走直线,而是沿一条弧形轨道滑到目标缺口处。多了"弧"这一层,思路要变,但也就三个新东西:怎么用角度描述弧、怎么在弧上找到缺口、怎么生成沿弧的拖动轨迹。照例从零跑通,脚本自己画一条弧形验证码再自己解,不碰任何网站。
先看它长什么样——一条朝上凸的弧形轨道,左下角是滑块起点,弧上某处有个缺口(目标),要把滑块沿弧拖到缺口:

和普通滑块的根本区别:普通滑块的答案是"往右滑多少像素"(一个 x 距离);弧度滑块的答案是"沿弧转多少度"(一个角度)。所有代码都得围着"角度"转。
第一步:装环境
pip install opencv-python numpy
第二步:完整代码,直接复制
新建 demo_arc.py,整段贴进去。它自己画弧形轨道+随机缺口+滑块,然后解出缺口角度、生成沿弧拖动轨迹:
# demo_arc.py 依赖:pip install opencv-python numpy
import cv2, numpy as np, random, math
W, H = 440, 320
CX, CY = 220, 360 # 圆心放在图片下方,弧线就朝上凸
R = 210 # 弧半径
BAND = 16 # 轨道粗细
A0, A1 = 210, 330 # 弧的角度范围(度):从左上扫到右上
BAND_COLOR = (70, 70, 70)
BG = (245, 245, 245)
# 核心:角度 → 像素坐标。弧上任意一点都由“圆心 + 半径 + 角度”决定
def pt(angle_deg):
a = math.radians(angle_deg)
return int(round(CX + R*math.cos(a))), int(round(CY + R*math.sin(a)))
# ---------- 造验证码:画弧形轨道 + 随机角度挖缺口 + 起点放滑块 ----------
def make_captcha(target_angle):
img = np.full((H, W, 3), BG, np.uint8)
for a in np.arange(A0, A1, 0.3): # 沿弧密集画圆点 → 一条轨道带
cv2.circle(img, pt(a), BAND//2, BAND_COLOR, -1)
for a in np.arange(target_angle-4, target_angle+4, 0.3): # 缺口:用背景色盖掉一小段
cv2.circle(img, pt(a), BAND//2+1, BG, -1)
cv2.circle(img, pt(A0), BAND//2+3, (40,160,90), -1) # 滑块:起点 A0 处一个圆
return img
# ---------- 解题:把弧“展开”成一维亮度信号,找缺口角度 ----------
def solve(img):
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
angles = np.arange(A0+6, A1-2, 0.5) # 逐角度采样(跳过起点滑块本身)
brightness = []
for a in angles:
x, y = pt(a)
patch = gray[max(0,y-3):y+4, max(0,x-3):x+4] # 取轨道中心一小块的平均亮度
brightness.append(patch.mean())
brightness = np.array(brightness)
# 有轨道处暗、缺口处是背景=亮。取“亮区”的加权质心角度,比直接取最亮点更准
thr = brightness.min() + (brightness.max()-brightness.min())*0.6
m = brightness >= thr
center = float(np.average(angles[m], weights=brightness[m]))
return center, angles, brightness
# ---------- 生成沿弧的拟人拖动轨迹(在“角度”上插值,不是在 x 上)----------
def drag_track(start_angle, target_angle):
n = random.randint(20, 30)
total_ms = random.randint(500, 900)
pts = []
for i in range(n):
raw = i/(n-1)
e = raw*raw*(3-2*raw) # smoothstep:位置两头慢中间快
a = start_angle + (target_angle-start_angle)*e # 角度用缓动 → 自然沿弧走
x, y = pt(a)
if 0.1 < raw < 0.9:
x += random.uniform(-1.2, 1.2); y += random.uniform(-1.2, 1.2) # 中段抖动
pts.append([round(x,1), round(y,1), int(total_ms*raw)]) # 时间线性推进,速度才真的变化
return pts
if __name__ == "__main__":
target = random.uniform(A0+25, A1-10)
img = make_captcha(target); cv2.imwrite("arc_captcha.png", img)
got, angles, bright = solve(img)
track = drag_track(A0, got)
print(f"缺口真实角度 = {target:.1f}°")
print(f"解出的角度 = {got:.1f}°")
print(f"角度误差 = {abs(target-got):.1f}°")
print(f"拖动轨迹 = {len(track)} 个点,{track[0][:2]} → {track[-1][:2]},耗时 {track[-1][2]}ms")
第三步:运行,看结果
python demo_arc.py
输出类似(每次随机):
缺口真实角度 = 285.0°
解出的角度 = 285.0°
角度误差 = 0.0°
拖动轨迹 = 26 个点,[38.0, 255.0] → [341.2, 168.4],耗时 712ms
误差基本为 0。下面把三个关键步骤逐个讲透。
关键一:用角度描述弧(pt 函数)
普通滑块只有一个自由度——x。弧度滑块的每个点都在一个圆上,所以用"圆心 + 半径 + 角度"来描述:
x = CX + R * cos(角度)
y = CY + R * sin(角度)
这就是 pt(angle) 干的事。把圆心放在图片下方(CY=360,超出图片高度 320),弧线就朝上凸,看起来像一道彩虹。角度从 A0=210° 到 A1=330° 扫过去,正好是一条从左上到右上的弧。想清楚这个映射,后面找目标、走轨迹全靠它——所有"角度"都用 pt() 换成屏幕上的像素点。
关键二:把弧"展开"成直线,就变回普通滑块(solve)
弧上找缺口,最省事的办法是降维:沿着弧、按角度一点点采样轨道中心的亮度,得到一条一维信号。有轨道的地方是深灰(暗),缺口处露出背景(亮)——缺口就是信号里那一段"亮起来"的地方。

找亮区的位置有个小技巧:不要直接取最亮的那个点(容易偏),而是取亮区的加权质心——把所有超过阈值的点按亮度加权求平均角度,落在缺口正中间,稳得多。代码里 thr 取"最暗到最亮"之间 60% 的位置做阈值,np.average(..., weights=...) 求质心。
这一步的通用价值:弧形、环形这类"沿曲线找目标"的问题,都可以先沿曲线采样、把它拉直成一维信号,再用一维的办法(找峰、找突变)解决。 上一篇旋转验证码取"环带"是同一个思路的变体。真实站点如果缺口不明显,把这里换成沿弧做模板匹配(拿滑块小图在展开后的信号里滑动匹配)即可,原理一样。
关键三:沿弧的拟人拖动轨迹(drag_track)
这是最容易做错的地方。很多人拿到目标点后,直接生成"起点到终点的直线轨迹"——但弧度滑块的滑块是被约束在弧上的,你提交一条直线轨迹,中间的点全都不在弧上,服务端一眼判定异常。
正确做法:在"角度"上插值,再用 pt() 换成坐标。滑块从起点角度 A0 走到目标角度,中间每一步都是先算一个角度、再换成弧上的点,这样生成的轨迹天然贴着弧:
a = start_angle + (target_angle - start_angle) * e # e 是缓动后的进度(角度)
x, y = pt(a) # 换成弧上的像素点
还有个容易忽略的细节:时间戳要线性推进(total_ms*raw),让角度去缓动,而不是两个都缓动。如果位置和时间用同一条缓动曲线,两边一约分,速度反而变回匀速了——匀速正是要避免的。时间匀速走、角度两头慢中间快,速度才会呈现"起步加速、临近减速"。
再叠上前几篇讲过的拟人处理——smoothstep 缓动(两头慢中间快)、中段抖动、总时长随机。生成的轨迹就是下面这样,红色点串沿着弧从滑块爬到缺口:

光拖到终点不够:服务端在多个维度校验你的轨迹
很多人以为"把滑块拖到缺口"就完事了。不是。你提交的不是一个终点坐标,而是整条轨迹(每个采样点的 x, y, t)。服务端拿到这串点,会从好几个互相独立的维度分别判断"这像不像人手拖的",任何一维露馅就判失败——哪怕终点分毫不差。所以终点对,只是及格线,不是通过线。
常见的几个校验维度:
- 时序:总耗时不能太短(几十毫秒 = 瞬移,一定是脚本),也不能太规整(时间戳等间隔递增,也是脚本)。人拖一下通常几百毫秒,且快慢不均。
- 速度:不能匀速。人是起步加速、临近目标减速。前面把时间做成线性、角度做成缓动,就是为了让速度自然地两头慢、中间快。
- 形状:轨迹要贴着它该走的路径——弧度滑块就得贴着弧,每个点都在弧上;但又不能是数学上完美的曲线,得有轻微抖动。太完美 = 假。
- 终点:确实要到位。这是最基本的一维,但只有它远远不够。
所以生成完轨迹,别急着提交,自己先按这几维量一遍。下面这个自检函数承接前面的 demo_arc.py,把每一维都打印出来:
import math
def check_track(track, target_pt, center):
xs=[p[0] for p in track]; ys=[p[1] for p in track]; ts=[p[2] for p in track]
# ① 终点:最后一个点离目标多远
end_err = math.dist((xs[-1], ys[-1]), target_pt)
# ② 时序:时间戳是否严格递增;总耗时是否在“人”的区间
mono = all(ts[i] < ts[i+1] for i in range(len(ts)-1))
# ③ 速度:算相邻点速度,看起步/中段/结尾(中段应明显更快)
v=[math.dist((xs[i],ys[i]),(xs[i-1],ys[i-1]))/max(1,ts[i]-ts[i-1]) for i in range(1,len(track))]
v_start=sum(v[:2])/2; v_mid=sum(v[len(v)//2-1:len(v)//2+1])/2; v_end=sum(v[-2:])/2
# ④ 形状:每个点到圆心的半径应几乎不变(贴弧),但不为 0(有抖动)
radii=[math.dist((x,y),center) for x,y in zip(xs,ys)]
r_dev=max(abs(r-sum(radii)/len(radii)) for r in radii)
print(f"① 终点误差 : {end_err:.1f} px")
print(f"② 时间戳递增 : {mono} 总耗时 {ts[-1]}ms")
print(f"③ 速度 : 起步 {v_start:.2f} / 中段 {v_mid:.2f} / 结尾 {v_end:.2f} px/ms")
print(f"④ 贴弧+抖动 : 半径波动 {r_dev:.1f}px")
# 用法:check_track(track, pt(got), (CX, CY))
对前面生成的那条轨迹跑一下,输出是这样的:
① 终点误差 : 0.0 px
② 时间戳递增 : True 总耗时 786ms
③ 速度 : 起步 0.08 / 中段 0.49 / 结尾 0.07 px/ms
④ 贴弧+抖动 : 半径波动 1.4px
四项都对上了:终点几乎无误差、时间几百毫秒且递增、速度中段(0.49)明显比两头(0.08 / 0.07)快、点都贴着弧但有 1.4 像素的自然波动。这条轨迹在这几个维度上就都"像人"了。反过来,如果你③打出来是三段几乎相等(匀速),或者④是 0(完美贴弧无抖动),那就是典型的脚本特征,八成过不了。
一个真实场景才会遇到的坑:双坐标系
真实的弧度滑块,验证码图片常常是大图下发、小图显示:服务端给你的图是内部分辨率(比如 880×640),页面上实际显示的是缩放后的尺寸(比如 440×320,正好一半)。你在下载的大图上检测出来的坐标,是内部坐标;但你要提交的拖动轨迹,得是显示坐标。 直接把内部坐标当轨迹交上去,位置全错。
换算就是按比例缩放:
scale_x = 显示宽 / 内部宽 # 440 / 880 = 0.5
scale_y = 显示高 / 内部高 # 320 / 640 = 0.5
显示坐标 = (内部坐标x * scale_x, 内部坐标y * scale_y)
检测在大图上做(更清楚、更准),生成轨迹前统一乘上缩放比换成显示坐标。凡是"检测的图"和"操作的图"分辨率不一致的验证码,都要留意这一步。
小结
弧度滑块 = 普通滑块 + 一层"弧"。把这三件事想通就通了:
- 一切用角度描述,
点 = 圆心 + 半径·(cos角, sin角); - 找目标先降维,沿弧采样把弧拉直成一维信号,再找缺口(找峰/模板匹配);
- 走轨迹在角度上插值再换坐标(时间线性、角度缓动),让每一步都贴着弧、速度两头慢中间快;
- 别只顾终点——服务端从时序、速度、形状、终点多维校验轨迹,生成后自己按这几维先量一遍;
- 别忘了内部坐标和显示坐标的缩放换算。
至此"验证码对抗"系列的几种主流形态——点选、旋转、缺口滑块、弧度滑块、以及贯穿始终的拟人轨迹——就都覆盖到了。
评论 0