验证码对抗(二):旋转验证码——环形掩膜 + 模板匹配求角度

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

从零跑通:脚本自己造旋转验证码再解出来,多次随机测试误差为 0,纯 OpenCV 不训模型。

这篇讲旋转验证码怎么自动求角度——就是那种给你一个大圆环、一个内圆盘,内盘被随机转了个角度,要你转正。我写成从零跑通的教程:脚本自己画一个圆盘、自己随机转一个角度、再自己解出来,不需要任何验证码图、也不碰任何网站。不训练模型,纯 OpenCV。

先讲清楚原理

这类验证码本质是同一张图被拆成内外两部分:外圈固定不动,内盘被随机转了角度 θ,你要把内盘转回来让内外纹理接上。所以问题就一句话:求出内盘被转了多少度。

思路很朴素:我不知道 θ,那就把内盘从 0° 到 359° 每个角度都试一遍,每转一个角度就量一下"此刻内外接得有多好",接得最好的那个角度就是答案。"接得多好"用 OpenCV 的模板匹配 cv2.matchTemplate 打分,越接近 1 越像。

一个关键处理:不比整个盘,只比内外交界那一圈环带。 因为需要"接上"的只有那条缝,中间的大片区域参与比较只会添乱。所以先做个"环形掩膜",两张图都只留边缘一圈。

第一步:装环境

pip install opencv-python numpy

第二步:完整代码,直接复制

新建 demo_rotate.py,把下面整段贴进去。它自己画底图、造一个"被转了随机角度"的验证码、再解出来:

# demo_rotate.py   依赖:pip install opencv-python numpy
import cv2, numpy as np, random

SIZE, R, RING = 400, 200, 46
CENTER = (SIZE // 2, SIZE // 2)

# ---------- 第 0 步:自己画一张“底图”(放射状彩色扇形,纹理明显好匹配)----------
def make_base():
    img = np.zeros((SIZE, SIZE, 3), np.uint8)
    for a in range(0, 360, 15):
        color = tuple(int(c) for c in np.random.randint(60, 256, 3))
        cv2.ellipse(img, CENTER, (R, R), 0, a, a + 15, color, -1)
    cv2.circle(img, CENTER, 40, (30, 30, 30), -1)
    return img

# 只保留一圈环带(旋转验证码只有内外交界那圈需要对上)
def annulus(img, radius=R, ring=RING, center=CENTER):
    mask = np.zeros(img.shape[:2], np.uint8)
    cv2.circle(mask, center, radius, 255, -1)          # 画大白圆
    cv2.circle(mask, center, radius - ring, 0, -1)     # 中心挖掉,剩一圈
    return cv2.bitwise_and(img, img, mask=mask)

# ---------- 第 1 步:造验证码——外圈不动,内盘随机转 true_angle 度 ----------
def make_captcha(base, true_angle):
    outer = base
    M = cv2.getRotationMatrix2D(CENTER, -true_angle, 1)
    inner = cv2.warpAffine(base, M, (SIZE, SIZE))
    return outer, inner

# ---------- 第 2 步:解题——逐角度暴力搜,相似度最高的就是答案 ----------
def solve(outer, inner):
    g_out = cv2.cvtColor(annulus(outer), cv2.COLOR_BGR2GRAY)
    best_a, best_score = 0, -1
    for a in range(360):
        M = cv2.getRotationMatrix2D(CENTER, a, 1)
        rot = cv2.warpAffine(inner, M, (SIZE, SIZE))    # 把内盘转 a 度
        ring = cv2.cvtColor(annulus(rot), cv2.COLOR_BGR2GRAY)  # 转完重新取环带
        score = cv2.minMaxLoc(cv2.matchTemplate(
            g_out, ring, cv2.TM_CCOEFF_NORMED))[1]      # 和外圈环带比一下
        if score > best_score:
            best_score, best_a = score, a
    return best_a

if __name__ == "__main__":
    base = make_base()
    true_angle = random.randint(30, 330)                # 随机转一个角度
    outer, inner = make_captcha(base, true_angle)
    cv2.imwrite("outer.png", outer)
    cv2.imwrite("inner.png", inner)
    got = solve(outer, inner)
    M = cv2.getRotationMatrix2D(CENTER, got, 1)
    cv2.imwrite("aligned.png", cv2.warpAffine(inner, M, (SIZE, SIZE)))
    print(f"随机生成的真实角度 = {true_angle}°")
    print(f"程序解出的角度     = {got}°")
    print(f"误差               = {abs(true_angle - got)}°")

第三步:运行,看结果

python demo_rotate.py

它每次随机转一个角度,输出类似:

随机生成的真实角度 = 221°
程序解出的角度     = 221°
误差               = 0°

多跑几次,误差基本都是 0。目录下还会生成三张图,就是下面这样——外圈不动、内盘被转了 73°(颜色和外圈错开了)、转回 73° 后内外颜色又接上了:

外圈 / 被转的内盘 / 转回后对齐

关键代码逐句讲

  • cv2.getRotationMatrix2D(CENTER, a, 1):生成一个"绕 CENTER 逆时针转 a 度、缩放 1 倍"的矩阵,cv2.warpAffine 拿它去转图。
  • 环形掩膜 annulus:先用 cv2.circle 画一个实心大白圆(最后那个 -1 是实心填充),再画一个实心黑圆把中间盖掉,剩下就是一圈;bitwise_and 用它抠图,环带以外全黑。
  • 转完要重新 annulus:图一转,落在环带里的像素就换了一批,所以每次旋转后都要重新取环带再比。
  • TM_CCOEFF_NORMED:归一化相关系数匹配,对整体明暗变化不敏感,最稳,验证码识别基本都用它。cv2.minMaxLoc(...)[1] 取的是匹配结果里的最大相似度。
  • 灰度化:颜色三通道一起比容易受噪声干扰,转成单通道灰度更稳。

把每个角度的分数画出来,是一条很干净的曲线,尖峰所在的横坐标就是答案:

逐角度搜索的相似度曲线

想更快、更准:先粗后细

上面是 1 度一步搜 360 次。要更快或要小数级精度,用先粗后细:先每 10 度粗搜一圈找到大概位置,再在峰值附近 ±10 度内 1 度细搜。匹配次数从 360 次降到几十次,精度反而更高。粗搜锁范围、细搜逼精确,是这类暴力搜索的通用提速套路。

顺带:缺口滑块

如果是"缺口滑块"(背景图有个凹槽,把滑块拖过去对上),更简单:先用 Canny 把两张图都变成边缘线稿,再用同样的 matchTemplate 找滑块在背景里的位置,返回的 x 坐标就是要滑的距离。

bg_edge = cv2.Canny(background, 100, 200)
slider_edge = cv2.Canny(slider, 100, 200)
res = cv2.matchTemplate(bg_edge, slider_edge, cv2.TM_CCOEFF_NORMED)
x = cv2.minMaxLoc(res)[3][0]        # 缺口的 x 像素位置

先转边缘再匹配,是因为缺口和滑块形状一致、但颜色明暗不一致,只比形状(边缘)最稳。

算出角度/距离只是一半——你一步转到精确角度、瞬间把滑块拖到位,行为太机械照样被拦。怎么把动作做得像人手,下一篇讲拟人轨迹。

评论 0

还没有评论
← 全部文章

找站长

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