Tous les articles
8 min de lectureglizzykingdreko

Défis hCaptcha du quotidien — Chap. 2

Si vous n'avez pas encore lu le Chap. 1, je vous recommande vivement de commencer par là. Dans ce chapitre, nous analysons un défi hCaptcha dynamique différent.

Défis hCaptcha du quotidien — Chap. 2

Si vous n'avez pas encore lu le Chap. 1, je vous recommande vivement de commencer par là. Dans ce chapitre, nous analysons un défi hCaptcha dynamique différent.

Le défi de l'arc manquant

Ce défi présente une figure en forme de donut, à laquelle il manque un segment ou « arc », toujours situé de façon constante sur le côté droit. Notre objectif principal est simple : déterminer avec précision les coordonnées du point milieu de ce segment manquant. Ces coordonnées représentent l'emplacement du drop-out requis par le payload.

Procédons étape par étape, avec à la fois des explications conceptuelles et des extraits de code pratiques.

L'approche

N'ayant comme seule donnée de hCaptcha que le png du défi, mon approche a été la suivante :

  1. Prétraiter l'image
  2. Identifier la forme du donut
  3. La visualiser pour le débogage sur un plan cartésien*****
  4. Identifier l'arc manquant à partir de la continuité de la bordure
  5. L'appliquer à l'image d'origine et identifier les coordonnées

Plus facile en pratique qu'en théorie.
Passons à ce défi :

*******Cette étape sert purement à mieux comprendre la situation, la résolution finale n'en aura pas besoin

Prétraitement

Avant tout raisonnement géométrique, l'image doit être « nettoyée » de sorte que seules les caractéristiques essentielles subsistent.

  • Conversion en niveaux de gris et flou gaussien :
    Convertir l'image en niveaux de gris simplifie les données en réduisant les canaux de couleur, et appliquer un flou gaussien aide à atténuer le bruit — garantissant que de petites irrégularités ou artefacts ne provoquent pas de fausses détections de contours.

  • Détection de contours (algorithme de Canny) :
    Le détecteur de contours de Canny est utilisé pour faire ressortir les frontières au sein de l'image. Cette étape est cruciale car elle isole les contours de la forme du donut, rendant la détection de contours ultérieure et l'analyse géométrique plus fiables.

Beaucoup de mots, mais en réalité seulement 3 lignes de code vraiment efficaces.

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

Identification de la forme du donut

Une fois que nous avons une carte de contours claire, l'étape suivante consiste à isoler la forme du donut de l'arrière-plan. Cela se fait par :

  • Masquage de la région du donut :
    Je crée deux masques : un pour un cercle extérieur (rayon + 5) et un autre pour un cercle intérieur (rayon — 5). L'intersection (ou la différence) de ces masques isole efficacement l'anneau qui définit le donut.
  • Analyse des contours :
    En trouvant les contours dans l'image masquée, je peux ensuite sélectionner le plus grand contour. Cette étape filtre le bruit superflu et garantit que seule la forme principale du donut est analysée.


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)

Le résultat est une image nettoyée qui ne contient que les points de contour du donut.

Visualisation cartésienne

Pour faciliter le débogage et garantir que les données de contour sont correctement interprétées, je projette le périmètre du donut sur un plan cartésien. Cette étape implique :

  • Définir des cercles de référence :
    Je superpose trois cercles sur un canevas vierge :
  • Un cercle vert représente la bordure détectée du donut.
  • Un cercle bleu représente une référence extérieure (rayon + 5).
  • Un cercle violet délimite une frontière intérieure (rayon — 5)
  • Tracer les points de contour valides :
    Les points rouges représentent les points de contour valides extraits de l'image traitée. Ces points révèlent la continuité (ou la discontinuité) de la bordure du donut, ce qui vous permet d'identifier visuellement l'arc manquant.


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

Extraction de l'arc manquant

Une fois les données de contour en main, le défi suivant consiste à déterminer où la bordure du donut est interrompue.

  • Regrouper et fusionner les angles :
    Je calcule l'angle de chaque point de contour par rapport au centre du donut. Ces angles sont ensuite regroupés en « arcs » selon leur continuité. Les petits écarts (dans une certaine limite) sont fusionnés de sorte que seules les discontinuités significatives subsistent.

# 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
        )
  • Identifier les sections manquantes :
    En prenant le complément des arcs « présents » fusionnés, on peut calculer les écarts (arcs manquants). Le plus grand arc manquant est alors choisi comme cible pour résoudre le défi.


# 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
            )
  • Calculer le point de drop-out :
    Le point milieu de cet arc manquant est calculé. Ce point, après une translation appropriée de la projection cartésienne vers le système de coordonnées de l'image d'origine, est le point de « drop-out » critique qui doit être soumis dans le payload du défi.

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

Retour vers l'image d'origine

L'étape finale consiste à traduire le point milieu calculé du plan cartésien vers l'espace de coordonnées de l'image d'origine. Pour une meilleure précision, le point milieu est marqué sur un cercle « imaginaire » en retrait de 7 pixels par rapport à la bordure extérieure détectée. Cet ajustement compense les éventuelles incohérences de la bordure et fournit une coordonnée plus fiable pour le 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)

Conclusion

La clé pour résoudre efficacement le « défi de l'arc manquant » réside dans un prétraitement précis de l'image, une analyse systématique des contours et un calcul géométrique exact. Les visualisations de débogage, bien qu'optionnelles, facilitent grandement le développement.

Vous trouverez l'implémentation complète et optimisée du solveur sur mon dépôt GitHub. Jetez aussi un œil à TakionAPI, mon fournisseur de solutions antibot proposant des API pour hCaptcha, Datadome, PerimeterX, Queue-IT, et des solutions de scraping web personnalisées. Rejoignez notre communauté Discord pour essayer l'API et obtenir de l'aide !

Cet article vous a plu ? Pensez à me soutenir via BuyMeACoffee.

Me suivre et me contacter

hcaptchaocranti-botbypasspython

Sautez l'étape de la rétro-ingénierie.

Takion renvoie des cookies, en-têtes et tokens frais pour tous les grands murs antibot. Un seul POST, aucun navigateur, premier appel dans l'heure.