验证码对抗(四):弧度滑块——角度参数化、弧展开找缺口、沿弧拟人轨迹

张 张站长 · 2026-09-14 15:59 · 技术-验证码 · 41 次浏览 · 0 条评论

滑块不走直线走弧线。从零跑通:脚本自己画弧形验证码再自己解,讲透用角度描述弧、把弧展开成一维信号找缺口、在角度上插值生成沿弧拖动轨迹、服务端多维度校验轨迹(时序/速度/形状/终点),以及内部/显示双坐标换算。

前面讲过普通滑块——沿直线把滑块拖到缺口。这篇讲它的进阶版:弧度滑块,滑块不走直线,而是沿一条弧形轨道滑到目标缺口处。多了"弧"这一层,思路要变,但也就三个新东西:怎么用角度描述弧、怎么在弧上找到缺口、怎么生成沿弧的拖动轨迹。照例从零跑通,脚本自己画一条弧形验证码再自己解,不碰任何网站。

先看它长什么样——一条朝上凸的弧形轨道,左下角是滑块起点,弧上某处有个缺口(目标),要把滑块沿弧拖到缺口:

弧度滑块验证码

和普通滑块的根本区别:普通滑块的答案是"往右滑多少像素"(一个 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)

检测在大图上做(更清楚、更准),生成轨迹前统一乘上缩放比换成显示坐标。凡是"检测的图"和"操作的图"分辨率不一致的验证码,都要留意这一步。

小结

弧度滑块 = 普通滑块 + 一层"弧"。把这三件事想通就通了:

  1. 一切用角度描述,点 = 圆心 + 半径·(cos角, sin角);
  2. 找目标先降维,沿弧采样把弧拉直成一维信号,再找缺口(找峰/模板匹配);
  3. 走轨迹在角度上插值再换坐标(时间线性、角度缓动),让每一步都贴着弧、速度两头慢中间快;
  4. 别只顾终点——服务端从时序、速度、形状、终点多维校验轨迹,生成后自己按这几维先量一遍;
  5. 别忘了内部坐标和显示坐标的缩放换算。

至此"验证码对抗"系列的几种主流形态——点选、旋转、缺口滑块、弧度滑块、以及贯穿始终的拟人轨迹——就都覆盖到了。

评论 0

还没有评论
← 全部文章

找站长

站长微信二维码 扫码加微信
售后群二维码 扫码进售后群
站长收款码 建站不易,感谢打赏
微信 18581469390
本站不支持自助充值;需要充值或开通额度,请直接联系站长。
加微信可以找我重置密码、充值、问工具怎么用