Expand 3D captcha into three subtypes: 3d_text, 3d_rotate, 3d_slider
Split the single "3d" captcha type into three independent expert models: - 3d_text: 3D perspective text OCR (renamed from old "3d", CTC-based ThreeDCNN) - 3d_rotate: rotation angle regression (new RegressionCNN, circular loss) - 3d_slider: slider offset regression (new RegressionCNN, SmoothL1 loss) CAPTCHA_TYPES expanded from 3 to 5 classes. Classifier samples updated to 50000 (10000 per class). New generators, model, dataset, training utilities, and full pipeline/export/CLI support for all subtypes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
113
generators/threed_slider_gen.py
Normal file
113
generators/threed_slider_gen.py
Normal file
@@ -0,0 +1,113 @@
|
||||
"""
|
||||
3D 滑块验证码生成器
|
||||
|
||||
生成滑块拼图验证码:纹理背景 + 拼图缺口 + 拼图块在左侧。
|
||||
用户需将拼图块滑动到缺口位置。
|
||||
|
||||
标签 = 缺口 x 坐标偏移(整数)
|
||||
文件名格式: {offset}_{index:06d}.png
|
||||
"""
|
||||
|
||||
import random
|
||||
|
||||
from PIL import Image, ImageDraw, ImageFilter
|
||||
|
||||
from config import GENERATE_CONFIG
|
||||
from generators.base import BaseCaptchaGenerator
|
||||
|
||||
|
||||
class ThreeDSliderGenerator(BaseCaptchaGenerator):
|
||||
"""3D 滑块验证码生成器。"""
|
||||
|
||||
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 = GENERATE_CONFIG["3d_slider"]
|
||||
self.width, self.height = self.cfg["image_size"]
|
||||
|
||||
def generate(self, text: str | None = None) -> tuple[Image.Image, str]:
|
||||
rng = self.rng
|
||||
pw, ph = self.cfg["puzzle_size"]
|
||||
gap_x_lo, gap_x_hi = self.cfg["gap_x_range"]
|
||||
|
||||
# 缺口位置
|
||||
gap_x = rng.randint(gap_x_lo, gap_x_hi)
|
||||
gap_y = rng.randint(10, self.height - ph - 10)
|
||||
|
||||
if text is None:
|
||||
text = str(gap_x)
|
||||
|
||||
# 1. 生成纹理背景
|
||||
img = self._textured_background(rng)
|
||||
|
||||
# 2. 从缺口位置截取拼图块内容
|
||||
piece_content = img.crop((gap_x, gap_y, gap_x + pw, gap_y + ph)).copy()
|
||||
|
||||
# 3. 绘制缺口 (半透明灰色区域)
|
||||
overlay = Image.new("RGBA", img.size, (0, 0, 0, 0))
|
||||
overlay_draw = ImageDraw.Draw(overlay)
|
||||
overlay_draw.rectangle(
|
||||
[gap_x, gap_y, gap_x + pw, gap_y + ph],
|
||||
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")
|
||||
|
||||
# 4. 绘制拼图块在左侧
|
||||
piece_x = self.cfg["piece_left_margin"]
|
||||
piece_img = Image.new("RGBA", (pw + 4, ph + 4), (0, 0, 0, 0))
|
||||
piece_draw = ImageDraw.Draw(piece_img)
|
||||
# 阴影
|
||||
piece_draw.rectangle([2, 2, pw + 3, ph + 3], fill=(0, 0, 0, 80))
|
||||
# 内容
|
||||
piece_img.paste(piece_content, (0, 0))
|
||||
# 边框
|
||||
piece_draw.rectangle([0, 0, pw - 1, ph - 1], outline=(255, 255, 255, 200), width=2)
|
||||
|
||||
img_rgba = img.convert("RGBA")
|
||||
img_rgba.paste(piece_img, (piece_x, gap_y), piece_img)
|
||||
img = img_rgba.convert("RGB")
|
||||
|
||||
# 5. 轻微模糊
|
||||
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, base_g, base_b = rng.randint(100, 180), rng.randint(100, 180), rng.randint(100, 180)
|
||||
for y in range(self.height):
|
||||
ratio = y / max(self.height - 1, 1)
|
||||
r = int(base_r + 30 * ratio)
|
||||
g = int(base_g - 20 * ratio)
|
||||
b = int(base_b + 10 * ratio)
|
||||
draw.line([(0, y), (self.width, y)], fill=(r, g, b))
|
||||
|
||||
# 添加纹理噪声
|
||||
noise_intensity = self.cfg["bg_noise_intensity"]
|
||||
for _ in range(self.width * self.height // 8):
|
||||
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(-noise_intensity, noise_intensity)))
|
||||
for c in pixel
|
||||
)
|
||||
draw.point((x, y), fill=noise)
|
||||
|
||||
# 随机色块 (模拟图案)
|
||||
for _ in range(rng.randint(3, 6)):
|
||||
x1, y1 = rng.randint(0, self.width - 30), rng.randint(0, self.height - 20)
|
||||
x2, y2 = x1 + rng.randint(15, 40), y1 + rng.randint(10, 25)
|
||||
color = tuple(rng.randint(60, 220) for _ in range(3))
|
||||
draw.rectangle([x1, y1, x2, y2], fill=color)
|
||||
|
||||
return img
|
||||
Reference in New Issue
Block a user