Todos os posts
8 min de leituraglizzykingdreko

Desafios Diários do hCaptcha — Cap. 2

Se você ainda não leu o Cap. 1, recomendo muito que comece por lá. Neste capítulo, analisamos um desafio dinâmico diferente do hCaptcha.

Desafios Diários do hCaptcha — Cap. 2

Se você ainda não leu o Cap. 1, recomendo muito que comece por lá. Neste capítulo, analisamos um desafio dinâmico diferente do hCaptcha.

O Desafio do Arco Faltante

Esse desafio apresenta uma figura em formato de rosquinha, com um segmento ou "arco" faltando, sempre localizado de forma consistente no lado direito. Nosso objetivo principal é direto: determinar com precisão as coordenadas do ponto médio desse segmento faltante. Essas coordenadas representam a localização de drop-out exigida pelo payload.

Vamos prosseguir passo a passo, incluindo tanto explicações conceituais quanto trechos práticos de código.

A abordagem

Tendo como único dado do hCaptcha o png do desafio, minha abordagem foi a seguinte:

  1. Pré-processar a imagem
  2. Identificar a forma de rosquinha
  3. Visualizá-la para debug em um plano cartesiano*****
  4. Identificar o arco faltante com base na continuidade da borda
  5. Aplicá-la à imagem original e identificar as coordenadas

Mais fácil na prática do que na teoria.
Vamos prosseguir com este desafio:

*******Este passo é puramente para um melhor entendimento da situação; a resolução final não vai precisar dele

Pré-processamento

Antes que qualquer raciocínio geométrico possa ocorrer, a imagem precisa ser "limpa" para que apenas as características essenciais permaneçam.

  • Conversão para Tons de Cinza e Gaussian Blur:
    Converter a imagem para tons de cinza simplifica os dados ao reduzir os canais de cor, e aplicar um Gaussian blur ajuda a mitigar o ruído — garantindo que pequenas irregularidades ou artefatos não levem a detecções de borda falsas.

  • Detecção de Bordas (Algoritmo Canny):
    O detector de bordas Canny é empregado para destacar os limites dentro da imagem. Esse passo é crucial, pois isola os contornos da forma de rosquinha, tornando a detecção de contornos e a análise geométrica subsequentes mais confiáveis.

Muitas palavras, mas na verdade 3 linhas de código bem eficientes.

# 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)

Identificação da Forma de Rosquinha

Uma vez que temos um mapa de bordas claro, o próximo passo é isolar a forma de rosquinha do fundo. Isso é feito por meio de:

  • Mascarando a Região da Rosquinha:
    Eu crio duas máscaras: uma para um círculo externo (raio + 5) e outra para um círculo interno (raio — 5). A interseção (ou diferença) dessas máscaras isola efetivamente o anel que define a rosquinha.
  • Análise de Contornos:
    Ao encontrar contornos na imagem mascarada, posso então selecionar o maior contorno. Esse passo filtra ruídos estranhos e garante que apenas a forma principal de rosquinha seja analisada.


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)

O resultado é uma imagem limpa que contém apenas os pontos de borda da rosquinha.

Visualização cartesiana

Para facilitar o debug e garantir que os dados de borda sejam interpretados corretamente, eu projeto o perímetro da rosquinha em um plano cartesiano. Esse passo envolve:

  • Definindo Círculos de Referência:
    Eu sobreponho três círculos em um canvas em branco:
  • Um círculo verde representa a borda detectada da rosquinha.
  • Um círculo azul representa uma referência externa (raio + 5).
  • Um círculo roxo delineia um limite interno (raio — 5)
  • Plotando Pontos de Borda Válidos:
    Os pontos vermelhos representam os pontos de borda válidos extraídos da imagem processada. Esses pontos revelam a continuidade (ou descontinuidade) da borda da rosquinha, permitindo que você identifique visualmente o arco faltante.


# 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)

Extraindo o Arco Faltante

Com os dados de borda em mãos, o próximo desafio é determinar onde a borda da rosquinha é interrompida.

  • Agrupando e Mesclando Ângulos:
    Eu calculo o ângulo de cada ponto de borda em relação ao centro da rosquinha. Esses ângulos são então agrupados em "arcos" com base em sua continuidade. Pequenas lacunas (dentro de um limite) são mescladas para que apenas descontinuidades significativas permaneçam.

# 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
        )
  • Identificando Seções Faltantes:
    Ao tomar o complemento dos arcos "presentes" mesclados, as lacunas (arcos faltantes) podem ser computadas. O maior arco faltante é então escolhido como o alvo para resolver o desafio.


# 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
            )
  • Calculando o Ponto de Drop-Out:
    O ponto médio desse arco faltante é computado. Esse ponto, após a devida translação da projeção cartesiana de volta para o sistema de coordenadas da imagem original, é o "ponto de drop-out" crítico que precisa ser enviado no payload do desafio.

# 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)

Mapeando de Volta para a Imagem Original

O passo final envolve traduzir o ponto médio computado do plano cartesiano de volta para o espaço de coordenadas da imagem original. Para maior precisão, o ponto médio é marcado em um círculo "imaginário" recuado em 7 pixels em relação à borda externa detectada. Esse ajuste compensa quaisquer inconsistências de borda e fornece uma coordenada mais confiável para o payload do 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)

Conclusão

A chave para resolver o "Desafio do Arco Faltante" de forma eficiente está no pré-processamento preciso da imagem, na análise sistemática de contornos e na computação geométrica acurada. As visualizações de debug, embora opcionais, ajudam significativamente no desenvolvimento.

Você pode encontrar a implementação completa e otimizada do solver no meu repositório do GitHub. Confira também a TakionAPI, meu provedor de soluções antibot que oferece APIs para hCaptcha, Datadome, PerimeterX, Queue-IT e soluções de web scraping sob medida. Junte-se à nossa comunidade no Discord para testes de API e suporte!

Gostou deste artigo? Considere me apoiar pelo BuyMeACoffee.

Me Siga e Entre em Contato

hcaptchaocranti-botbypasspython

Pule a engenharia reversa.

O Takion retorna cookies, headers e tokens novos para cada grande muro antibot. Um POST, sem navegador, primeira chamada em menos de uma hora.