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>
113 lines
3.7 KiB
Python
113 lines
3.7 KiB
Python
"""
|
|
滑块验证码数据生成器
|
|
|
|
生成滑块验证码训练数据:随机纹理/色块背景 + 方形缺口 + 阴影效果。
|
|
标签 = 缺口中心 x 坐标 (整数)
|
|
文件名格式: {gap_x}_{index:06d}.png
|
|
"""
|
|
|
|
import random
|
|
|
|
from PIL import Image, ImageDraw, ImageFilter
|
|
|
|
from config import SOLVER_CONFIG
|
|
from generators.base import BaseCaptchaGenerator
|
|
|
|
|
|
class SlideDataGenerator(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["slide"]
|
|
self.height, self.width = self.cfg["cnn_input_size"] # (H, W)
|
|
self.gap_size = 40 # 缺口大小
|
|
|
|
def generate(self, text: str | None = None) -> tuple[Image.Image, str]:
|
|
rng = self.rng
|
|
gs = self.gap_size
|
|
|
|
# 缺口 x 范围: 留出边距
|
|
margin = gs + 10
|
|
gap_x = rng.randint(margin, self.width - margin)
|
|
gap_y = rng.randint(10, self.height - gs - 10)
|
|
|
|
if text is None:
|
|
text = str(gap_x)
|
|
|
|
# 1. 生成纹理背景
|
|
img = self._textured_background(rng)
|
|
|
|
# 2. 绘制缺口 (半透明灰色区域 + 阴影)
|
|
overlay = Image.new("RGBA", img.size, (0, 0, 0, 0))
|
|
overlay_draw = ImageDraw.Draw(overlay)
|
|
|
|
# 阴影 (稍大一圈)
|
|
overlay_draw.rectangle(
|
|
[gap_x + 2, gap_y + 2, gap_x + gs + 2, gap_y + gs + 2],
|
|
fill=(0, 0, 0, 60),
|
|
)
|
|
# 缺口本体
|
|
overlay_draw.rectangle(
|
|
[gap_x, gap_y, gap_x + gs, gap_y + gs],
|
|
fill=(80, 80, 80, 160),
|
|
outline=(60, 60, 60, 200),
|
|
width=2,
|
|
)
|
|
|
|
img = img.convert("RGBA")
|
|
img = Image.alpha_composite(img, overlay)
|
|
img = img.convert("RGB")
|
|
|
|
# 3. 轻微模糊
|
|
img = img.filter(ImageFilter.GaussianBlur(radius=0.3))
|
|
|
|
return img, text
|
|
|
|
def _textured_background(self, rng: random.Random) -> Image.Image:
|
|
"""生成带纹理的彩色背景。"""
|
|
img = Image.new("RGB", (self.width, self.height))
|
|
draw = ImageDraw.Draw(img)
|
|
|
|
# 渐变底色
|
|
base_r = rng.randint(80, 200)
|
|
base_g = rng.randint(80, 200)
|
|
base_b = rng.randint(80, 200)
|
|
for y in range(self.height):
|
|
ratio = y / max(self.height - 1, 1)
|
|
r = int(base_r + 40 * ratio)
|
|
g = int(base_g - 20 * ratio)
|
|
b = int(base_b + 20 * ratio)
|
|
r, g, b = max(0, min(255, r)), max(0, min(255, g)), max(0, min(255, b))
|
|
draw.line([(0, y), (self.width, y)], fill=(r, g, b))
|
|
|
|
# 纹理噪声
|
|
for _ in range(self.width * self.height // 6):
|
|
x = rng.randint(0, self.width - 1)
|
|
y = rng.randint(0, self.height - 1)
|
|
pixel = img.getpixel((x, y))
|
|
noise = tuple(
|
|
max(0, min(255, c + rng.randint(-30, 30)))
|
|
for c in pixel
|
|
)
|
|
draw.point((x, y), fill=noise)
|
|
|
|
# 随机色块 (模拟图案)
|
|
for _ in range(rng.randint(4, 8)):
|
|
x1, y1 = rng.randint(0, self.width - 30), rng.randint(0, self.height - 20)
|
|
x2, y2 = x1 + rng.randint(15, 50), y1 + rng.randint(10, 30)
|
|
color = tuple(rng.randint(50, 230) for _ in range(3))
|
|
draw.rectangle([x1, y1, x2, y2], fill=color)
|
|
|
|
# 随机圆形
|
|
for _ in range(rng.randint(2, 5)):
|
|
cx = rng.randint(10, self.width - 10)
|
|
cy = rng.randint(10, self.height - 10)
|
|
cr = rng.randint(5, 20)
|
|
color = tuple(rng.randint(50, 230) for _ in range(3))
|
|
draw.ellipse([cx - cr, cy - cr, cx + cr, cy + cr], fill=color)
|
|
|
|
return img
|