全部文章
4 分钟阅读glizzykingdreko

每日 hCaptcha 挑战 —— 第 2 章

如果你还没读过第 1 章,强烈建议你从那里开始。在这一章里,我们分析一种不同的动态 hCaptcha 挑战。

每日 hCaptcha 挑战 —— 第 2 章

如果你还没读过第 1 章,强烈建议你从那里开始。在这一章里,我们分析一种不同的动态 hCaptcha 挑战。

缺口圆弧挑战(The Missing Arch Challenge)

这个挑战呈现的是一个甜甜圈形状的图案,其中缺了一段或者说一个"圆弧",位置始终固定在右侧。我们的主要目标很直接:准确地确定这段缺失部分中点的坐标。这个坐标就是 payload 所需的落点位置。

我们一步步来,既有概念上的讲解,也有实际的代码片段。

思路

由于从 hCaptcha 拿到的唯一数据就是挑战的 png 图片,我的思路如下:

  1. 预处理图片
  2. 识别甜甜圈形状
  3. 在笛卡尔平面上把它可视化,用于调试*****
  4. 根据边界的连续性识别缺失的圆弧
  5. 把它应用到原图上并识别出坐标

实践起来比讲理论要容易。
我们就拿这个挑战来做:

*******这一步纯粹是为了更好地理解情况,最终的解决方案并不需要这一步

预处理

在进行任何几何推理之前,图片必须先"清理干净",只留下关键特征。

  • 灰度转换与高斯模糊:
    把图片转成灰度,通过减少颜色通道来简化数据;再施加高斯模糊有助于抑制噪声——确保细小的不规则处或伪影不会导致误判的边缘检测。

  • 边缘检测(Canny 算法):
    用 Canny 边缘检测器来突出图片中的边界。这一步至关重要,因为它把甜甜圈形状的轮廓孤立出来,让后续的轮廓检测和几何分析更可靠。

说了一大堆,其实就是 3 行相当高效的代码。

# 1. Grayscale
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# 2. Blur
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
# 3. Identify edges
edges = cv2.Canny(blurred, 50, 150)

识别甜甜圈形状

有了清晰的边缘图之后,下一步是把甜甜圈形状从背景里孤立出来。做法是:

  • 对甜甜圈区域做掩膜:
    我创建两个掩膜:一个用于外圆(半径 + 5),另一个用于内圆(半径 — 5)。这两个掩膜的交集(或差集)就能有效地孤立出定义甜甜圈的那个圆环。
  • 轮廓分析:
    通过在掩膜后的图片里查找轮廓,我就能选出最大的那个轮廓。这一步过滤掉了多余的噪声,确保只分析主要的甜甜圈形状。


circles = cv2.HoughCircles(
   edges,
    cv2.HOUGH_GRADIENT,
    dp=1,
    minDist=50,
    param1=50,
    param2=30,
    minRadius=20,
    maxRadius=100
)
circle_vis = cv2.cvtColor(edges, cv2.COLOR_GRAY2BGR)
h, w = edges.shape[:2]

circles = np.uint16(np.around(circles))
largest_circle = max(circles[0], key=lambda x: x[2])
x_center, y_center, r = largest_circle

# Draw circles for debug.
cv2.circle(circle_vis, (x_center, y_center), r, (0, 255, 0), 2)
cv2.circle(circle_vis, (x_center, y_center), r + 5, (255, 0, 0), 2)
cv2.circle(circle_vis, (x_center, y_center), r - 5, (255, 0, 255), 2)

结果是一张清理干净的图片,只包含甜甜圈的边缘点。

笛卡尔可视化

为了方便调试并确保边缘数据被正确解读,我把甜甜圈的周界投影到一个笛卡尔平面上。这一步包括:

  • 定义参考圆:
    我在一张空白画布上叠加三个圆:
  • 一个绿色圆代表检测到的甜甜圈边界。
  • 一个蓝色圆代表外侧参考(半径 + 5)。
  • 一个紫色圆划出内侧边界(半径 — 5)
  • 绘制有效的边缘点:
    红点代表从处理后图片里提取出的有效边缘点。这些点揭示了甜甜圈边界的连续性(或不连续性),让你能够直观地识别出缺失的圆弧。


# Prepare blank images
img1 = np.ones((size, size, 3), dtype=np.uint8) * 255
img2 = img1.copy()
img3 = img1.copy()

# Draw coordinate axes on each
for img in (img1, img2, img3):
    cv2.line(img, (0, size//2), (size, size//2), (0, 0, 0), 1)
    cv2.line(img, (size//2, 0), (size//2, size), (0, 0, 0), 1)

# Compute scale factor and center in Cartesian space
scale = (size * 0.8) / (2 * radius)
center_pt = (size // 2, size // 2)

# Mask the donut region
outer_mask = np.zeros_like(edges)
inner_mask = np.zeros_like(edges)
cv2.circle(outer_mask, (center_x, center_y), radius + 5, 255, -1)
cv2.circle(inner_mask, (center_x, center_y), radius - 5, 255, -1)
donut_region = cv2.bitwise_and(edges, cv2.bitwise_and(outer_mask, cv2.bitwise_not(inner_mask)))

# Keep only the largest contour
contours, _ = cv2.findContours(donut_region, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
if contours:
    largest_contour = max(contours, key=cv2.contourArea)
    cleaned = np.zeros_like(donut_region)
    cv2.drawContours(cleaned, [largest_contour], -1, 255, thickness=cv2.FILLED)
    donut_region = cleaned

# Collect valid points and compute angles
white_pixels = np.argwhere(donut_region > 0)
white_pixels = [(p[1], p[0]) for p in white_pixels]  # (x, y)

margin = 5
angle_points = {}
for (px, py) in white_pixels:
    dist = math.hypot(px - center_x, py - center_y)
    if (radius - margin) <= dist <= (radius + margin):
        angle_deg = math.degrees(math.atan2(py - center_y, px - center_x))
        if angle_deg < 0:
            angle_deg += 360
        angle_deg = int(round(angle_deg))
        angle_points.setdefault(angle_deg, []).append((px, py))

# Group angles into raw arcs
sorted_angles = sorted(angle_points.keys())
raw_arcs = []
current_arc = []
gap_thresh = 5
min_arc_len = 5
if sorted_angles:
    current_arc = [sorted_angles[0]]
    for i in range(1, len(sorted_angles)):
        curr_angle = sorted_angles[i]
        prev_angle = sorted_angles[i - 1]
        if (curr_angle - prev_angle) <= gap_thresh:
            current_arc.append(curr_angle)
        else:
            if len(current_arc) >= min_arc_len:
                raw_arcs.append((min(current_arc), max(current_arc)))
            current_arc = [curr_angle]
    if len(current_arc) >= min_arc_len:
        raw_arcs.append((min(current_arc), max(current_arc)))

# Merge raw arcs to get present arcs, then compute missing arcs
present_arcs = merge_arcs(raw_arcs, gap_threshold=5)
missing_arcs = find_missing_arcs(present_arcs)

# Draw red dots for all valid points on each image
for (px, py) in white_pixels:
    dx = px - center_x
    dy = py - center_y
    proj_x = int(center_pt[0] + dx * scale)
    proj_y = int(center_pt[1] + dy * scale)
    if 0 <= proj_x < size and 0 <= proj_y < size:
        for img in (img1, img2, img3):
            cv2.circle(img, (proj_x, proj_y), 1, (0, 0, 255), -1)

# Draw reference circles on each image
for img in (img1, img2, img3):
    cv2.circle(img, center_pt, int(radius * scale), (0, 255, 0), 1)       # Middle (green)
    cv2.circle(img, center_pt, int((radius + 5) * scale), (255, 0, 0), 1)   # Outer (blue)
    cv2.circle(img, center_pt, int((radius - 5) * scale), (255, 0, 255), 1) # Inner (purple)

提取缺失的圆弧

有了边缘数据,下一个挑战就是确定甜甜圈的边界在哪里断开。

  • 对角度进行分组与合并:
    我计算每个边缘点相对于甜甜圈圆心的角度。然后根据连续性把这些角度分组成一个个"圆弧"。小的间隙(在某个阈值以内)会被合并,只留下明显的不连续处。

# Draw present arcs in pink on all three images
for (arc_start, arc_end) in present_arcs:
    for img in (img1, img2, img3):
        cv2.ellipse(
            img,
            center_pt,
            (int(radius * scale), int(radius * scale)),
            0,
            arc_start,
            arc_end,
            (255, 0, 255),  # Pink
            2
        )
  • 识别缺失的部分:
    对已合并的"存在"圆弧取补集,就能算出那些间隙(缺失的圆弧)。然后选出最大的那段缺失圆弧,作为解决挑战的目标。


# Draw missing arcs in red on img2 and img3
for (arc_start, arc_end) in missing_arcs:
    for img in (img2, img3):
            cv2.ellipse(
                img,
                center_pt,
                (int(radius * scale), int(radius * scale)),
                0,
                arc_start,
                arc_end,
                (0, 0, 255),  # Red
                2
            )
  • 计算落点:
    计算这段缺失圆弧的中点。这个点,在从笛卡尔投影恰当地平移回原图坐标系之后,就是需要提交到挑战 payload 里的那个关键"落点"。

# We draw a green line and dot for the largest missing arc
largest_missing_arc = max(missing_arcs, key=lambda arc: arc[1] - arc[0])
arc_start, arc_end = largest_missing_arc
mid_angle = (arc_start + arc_end) / 2.0
mid_rad = math.radians(mid_angle)
scaled_r = int(radius * scale)
line_end_x = center_pt[0] + int(scaled_r * math.cos(mid_rad))
line_end_y = center_pt[1] + int(scaled_r * math.sin(mid_rad))
cv2.line(img3, center_pt, (line_end_x, line_end_y), (0, 255, 0), 2)
cv2.circle(img3, (line_end_x, line_end_y), 5, (0, 255, 0), -1)

映射回原图

最后一步是把算出的中点从笛卡尔平面平移回原图的坐标空间。为了提高准确度,中点被标注在一个"假想"圆上,这个圆相对检测到的外边界向内缩进了 7 像素。这一调整弥补了边界的任何不一致,为 hCaptcha 的 payload 提供了一个更可靠的坐标。

# Mark the largest missing arc midpoint on the original image.
original_with_missing_dot = image.copy()

largest_missing_arc = max(missing_arcs, key=lambda arc: arc[1] - arc[0])
arc_start, arc_end = largest_missing_arc
mid_angle = (arc_start + arc_end) / 2.0
mid_rad = math.radians(mid_angle)
dot_x = int(x_center + (r - 7) * math.cos(mid_rad))
dot_y = int(y_center + (r - 7) * math.sin(mid_rad))
cv2.circle(original_with_missing_dot, (dot_x, dot_y), 5, (0, 255, 0), -1)
coord_text = f"({dot_x},{dot_y})"
cv2.putText(original_with_missing_dot, coord_text, (dot_x+5, dot_y-5),
    cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1)

结论

高效解决"缺口圆弧挑战"的关键,在于精确的图像预处理、系统化的轮廓分析,以及准确的几何计算。调试用的可视化虽然是可选的,但对开发帮助很大。

你可以在我的 GitHub 仓库找到完整、优化过的解题器实现。另外,看看 TakionAPI,我的反机器人方案提供商,为 hCaptcha、Datadome、PerimeterX、Queue-IT 提供 API,也提供定制化的网页抓取方案。加入我们的 Discord 社区,获取 API 试用和支持!

喜欢这篇文章吗?欢迎通过 BuyMeACoffee 支持我。

关注与联系我

hcaptchaocranti-botbypasspython

跳过逆向工程。

Takion 为每一道主流反机器人防护墙返回新鲜的 cookie、请求头和令牌。一次 POST,无需浏览器,一小时内完成首次调用。