Add slide and rotate interactive captcha solvers
New solver subsystem with independent models: - GapDetectorCNN (1x128x256 grayscale → sigmoid) for slide gap detection - RotationRegressor (3x128x128 RGB → sin/cos via tanh) for rotation angle prediction - SlideSolver with 3-tier strategy: template match → edge detect → CNN fallback - RotateSolver with ONNX sin/cos → atan2 inference - Generators, training scripts, CLI commands, and slide track utility Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
156
generators/rotate_solver_gen.py
Normal file
156
generators/rotate_solver_gen.py
Normal file
@@ -0,0 +1,156 @@
|
||||
"""
|
||||
旋转验证码求解器数据生成器
|
||||
|
||||
生成旋转验证码训练数据:随机图案 (色块/渐变/几何图形),随机旋转 0-359°。
|
||||
裁剪为圆形 (黑色背景填充圆外区域)。
|
||||
|
||||
标签 = 旋转角度 (整数)
|
||||
文件名格式: {angle}_{index:06d}.png
|
||||
"""
|
||||
|
||||
import math
|
||||
import random
|
||||
|
||||
from PIL import Image, ImageDraw, ImageFilter, ImageFont
|
||||
|
||||
from config import SOLVER_CONFIG
|
||||
from generators.base import BaseCaptchaGenerator
|
||||
|
||||
_FONT_PATHS = [
|
||||
"/usr/share/fonts/TTF/DejaVuSans-Bold.ttf",
|
||||
"/usr/share/fonts/TTF/DejaVuSerif-Bold.ttf",
|
||||
"/usr/share/fonts/liberation/LiberationSans-Bold.ttf",
|
||||
"/usr/share/fonts/liberation/LiberationSerif-Bold.ttf",
|
||||
"/usr/share/fonts/gnu-free/FreeSansBold.otf",
|
||||
]
|
||||
|
||||
|
||||
class RotateSolverDataGenerator(BaseCaptchaGenerator):
|
||||
"""旋转验证码求解器数据生成器。"""
|
||||
|
||||
def __init__(self, seed: int | None = None):
|
||||
from config import RANDOM_SEED
|
||||
super().__init__(seed=seed if seed is not None else RANDOM_SEED)
|
||||
|
||||
self.cfg = SOLVER_CONFIG["rotate"]
|
||||
self.height, self.width = self.cfg["input_size"] # (H, W)
|
||||
|
||||
self._fonts: list[str] = []
|
||||
for p in _FONT_PATHS:
|
||||
try:
|
||||
ImageFont.truetype(p, 20)
|
||||
self._fonts.append(p)
|
||||
except OSError:
|
||||
continue
|
||||
|
||||
def generate(self, text: str | None = None) -> tuple[Image.Image, str]:
|
||||
rng = self.rng
|
||||
|
||||
# 随机旋转角度 0-359
|
||||
angle = rng.randint(0, 359)
|
||||
if text is None:
|
||||
text = str(angle)
|
||||
|
||||
size = self.width # 正方形
|
||||
radius = size // 2
|
||||
|
||||
# 1. 生成正向图案 (未旋转)
|
||||
content = self._random_pattern(rng, size)
|
||||
|
||||
# 2. 旋转图案
|
||||
rotated = content.rotate(-angle, resample=Image.BICUBIC, expand=False)
|
||||
|
||||
# 3. 裁剪为圆形 (黑色背景)
|
||||
result = Image.new("RGB", (size, size), (0, 0, 0))
|
||||
mask = Image.new("L", (size, size), 0)
|
||||
mask_draw = ImageDraw.Draw(mask)
|
||||
mask_draw.ellipse([0, 0, size - 1, size - 1], fill=255)
|
||||
|
||||
result.paste(rotated, (0, 0), mask)
|
||||
|
||||
# 4. 轻微模糊
|
||||
result = result.filter(ImageFilter.GaussianBlur(radius=0.5))
|
||||
|
||||
return result, text
|
||||
|
||||
def _random_pattern(self, rng: random.Random, size: int) -> Image.Image:
|
||||
"""生成随机图案 (带明显方向性,便于模型学习旋转)。"""
|
||||
img = Image.new("RGB", (size, size))
|
||||
draw = ImageDraw.Draw(img)
|
||||
|
||||
# 渐变背景
|
||||
base_r = rng.randint(100, 220)
|
||||
base_g = rng.randint(100, 220)
|
||||
base_b = rng.randint(100, 220)
|
||||
for y in range(size):
|
||||
ratio = y / max(size - 1, 1)
|
||||
r = int(base_r * (1 - ratio) + rng.randint(40, 120) * ratio)
|
||||
g = int(base_g * (1 - ratio) + rng.randint(40, 120) * ratio)
|
||||
b = int(base_b * (1 - ratio) + rng.randint(40, 120) * ratio)
|
||||
draw.line([(0, y), (size, y)], fill=(r, g, b))
|
||||
|
||||
cx, cy = size // 2, size // 2
|
||||
|
||||
# 添加不对称几何图形 (让模型能感知方向)
|
||||
pattern_type = rng.choice(["triangle", "arrow", "text", "shapes"])
|
||||
|
||||
if pattern_type == "triangle":
|
||||
# 顶部三角形标记
|
||||
color = tuple(rng.randint(180, 255) for _ in range(3))
|
||||
ts = size // 4
|
||||
draw.polygon(
|
||||
[(cx, cy - ts), (cx - ts // 2, cy), (cx + ts // 2, cy)],
|
||||
fill=color,
|
||||
)
|
||||
# 底部小圆
|
||||
draw.ellipse(
|
||||
[cx - 8, cy + ts // 2, cx + 8, cy + ts // 2 + 16],
|
||||
fill=tuple(rng.randint(50, 150) for _ in range(3)),
|
||||
)
|
||||
|
||||
elif pattern_type == "arrow":
|
||||
# 向上的箭头
|
||||
color = tuple(rng.randint(180, 255) for _ in range(3))
|
||||
arrow_len = size // 3
|
||||
draw.line([(cx, cy - arrow_len), (cx, cy + arrow_len // 2)], fill=color, width=4)
|
||||
draw.polygon(
|
||||
[(cx, cy - arrow_len - 5), (cx - 10, cy - arrow_len + 10), (cx + 10, cy - arrow_len + 10)],
|
||||
fill=color,
|
||||
)
|
||||
|
||||
elif pattern_type == "text" and self._fonts:
|
||||
# 文字 (有天然方向性)
|
||||
font_path = rng.choice(self._fonts)
|
||||
font_size = size // 3
|
||||
try:
|
||||
font = ImageFont.truetype(font_path, font_size)
|
||||
ch = rng.choice("ABCDEFGHJKLMNPRSTUVWXYZ23456789")
|
||||
bbox = font.getbbox(ch)
|
||||
tw, th = bbox[2] - bbox[0], bbox[3] - bbox[1]
|
||||
draw.text(
|
||||
(cx - tw // 2 - bbox[0], cy - th // 2 - bbox[1]),
|
||||
ch,
|
||||
fill=tuple(rng.randint(0, 80) for _ in range(3)),
|
||||
font=font,
|
||||
)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
else:
|
||||
# 混合不对称形状
|
||||
# 上方矩形
|
||||
w, h = rng.randint(15, 30), rng.randint(10, 20)
|
||||
color = tuple(rng.randint(150, 255) for _ in range(3))
|
||||
draw.rectangle([cx - w, cy - size // 3, cx + w, cy - size // 3 + h], fill=color)
|
||||
# 右下小圆
|
||||
r = rng.randint(5, 12)
|
||||
color2 = tuple(rng.randint(50, 150) for _ in range(3))
|
||||
draw.ellipse([cx + size // 5, cy + size // 5, cx + size // 5 + r * 2, cy + size // 5 + r * 2], fill=color2)
|
||||
|
||||
# 添加纹理噪声
|
||||
for _ in range(rng.randint(20, 60)):
|
||||
nx, ny = rng.randint(0, size - 1), rng.randint(0, size - 1)
|
||||
nc = tuple(rng.randint(80, 220) for _ in range(3))
|
||||
draw.point((nx, ny), fill=nc)
|
||||
|
||||
return img
|
||||
Reference in New Issue
Block a user