Ежедневные челленджи hCaptcha — Глава 2
Если вы ещё не читали Главу 1, настоятельно рекомендую начать с неё. В этой главе мы разберём другой динамический челлендж hCaptcha.

Если вы ещё не читали Главу 1, настоятельно рекомендую начать с неё. В этой главе мы разберём другой динамический челлендж hCaptcha.
Челлендж с недостающей дугой
В этом челлендже показана фигура в форме пончика, у которой отсутствует один сегмент, или «дуга», и располагается он всегда с правой стороны. Наша основная задача проста: точно определить координаты середины этого недостающего сегмента. Именно эти координаты и есть точка сброса (drop-out), которую требует payload.
Давайте разберём всё по шагам — с концептуальными объяснениями и практическими фрагментами кода.


Подход
Имея в качестве единственных данных от hCaptcha только png-картинку челленджа, я действовал так:
- Предобработать изображение
- Определить форму пончика
- Визуализировать его для отладки на декартовой плоскости*****
- Определить недостающую дугу по непрерывности контура
- Применить это к исходному изображению и определить координаты
На практике проще, чем в теории.
Приступим к челленджу:

*******Этот шаг нужен исключительно для лучшего понимания ситуации; для итогового решения он не потребуется
Предобработка
Прежде чем начинать любые геометрические рассуждения, изображение нужно «очистить», чтобы остались только существенные детали.
- Преобразование в оттенки серого и размытие по Гауссу:
Перевод изображения в оттенки серого упрощает данные, сокращая число цветовых каналов, а размытие по Гауссу помогает подавить шум — благодаря этому мелкие неровности или артефакты не приводят к ложным срабатываниям при поиске границ.

- Обнаружение границ (алгоритм 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)
Определение формы пончика
Когда у нас есть чёткая карта границ, следующий шаг — отделить форму пончика от фона. Достигается это так:
- Маскирование области пончика:
Я создаю две маски: одну для внешней окружности (radius + 5), другую для внутренней (radius — 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)
В результате получается очищенное изображение, содержащее только точки границы пончика.
Визуализация в декартовых координатах
Чтобы упростить отладку и убедиться, что данные о границах интерпретируются правильно, я проецирую периметр пончика на декартову плоскость. Этот шаг включает:
- Задание опорных окружностей:
Я накладываю три окружности на пустой холст: - Зелёная окружность обозначает обнаруженную границу пончика.
- Синяя окружность обозначает внешний ориентир (radius + 5).
- Фиолетовая окружность очерчивает внутреннюю границу (radius — 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
)
- Вычисление точки сброса (drop-out):
Вычисляется середина этой недостающей дуги. Эта точка после соответствующего преобразования из декартовой проекции обратно в систему координат исходного изображения и есть та самая критически важная точка сброса (drop-out), которую нужно отправить в 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 пикселей внутрь от обнаруженной внешней границы. Эта поправка компенсирует любые неточности границы и даёт более надёжную координату для payload hCaptcha.

# 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 — мой сервис для обхода антибот-защиты, предоставляющий API для hCaptcha, Datadome, PerimeterX, Queue-IT и кастомные решения для веб-скрейпинга. Присоединяйтесь к нашему сообществу в Discord, чтобы получить пробный доступ к API и поддержку!
Понравилась статья? Вы можете поддержать меня через BuyMeACoffee.
Подписывайтесь и пишите мне
- GitHub: glizzykingdreko
- Medium: glizzykingdreko
- Discord: glizzykingdreko
- Email: [email protected]
Пропустите реверс-инжиниринг.
Takion возвращает свежие cookie, заголовки и токены для любой крупной антибот-стены. Один POST, без браузера, первый вызов в течение часа.