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:
Hua
2026-03-11 13:55:53 +08:00
parent 760b80ee5e
commit f5be7671bc
20 changed files with 1109 additions and 142 deletions

View File

@@ -1,20 +1,26 @@
"""
数据生成器包
提供种验证码类型的数据生成器:
提供种验证码类型的数据生成器:
- NormalCaptchaGenerator: 普通字符验证码
- MathCaptchaGenerator: 算式验证码
- ThreeDCaptchaGenerator: 3D 立体验证码
- ThreeDCaptchaGenerator: 3D 立体文字验证码
- ThreeDRotateGenerator: 3D 旋转验证码
- ThreeDSliderGenerator: 3D 滑块验证码
"""
from generators.base import BaseCaptchaGenerator
from generators.normal_gen import NormalCaptchaGenerator
from generators.math_gen import MathCaptchaGenerator
from generators.threed_gen import ThreeDCaptchaGenerator
from generators.threed_rotate_gen import ThreeDRotateGenerator
from generators.threed_slider_gen import ThreeDSliderGenerator
__all__ = [
"BaseCaptchaGenerator",
"NormalCaptchaGenerator",
"MathCaptchaGenerator",
"ThreeDCaptchaGenerator",
"ThreeDRotateGenerator",
"ThreeDSliderGenerator",
]

View File

@@ -45,7 +45,7 @@ class ThreeDCaptchaGenerator(BaseCaptchaGenerator):
from config import RANDOM_SEED
super().__init__(seed=seed if seed is not None else RANDOM_SEED)
self.cfg = GENERATE_CONFIG["3d"]
self.cfg = GENERATE_CONFIG["3d_text"]
self.chars = THREED_CHARS
self.width, self.height = self.cfg["image_size"]
@@ -154,7 +154,7 @@ class ThreeDCaptchaGenerator(BaseCaptchaGenerator):
char_img = self._perspective_transform(char_img, rng)
# 随机旋转
angle = rng.randint(-20, 20)
angle = rng.randint(*self.cfg["rotation_range"])
char_img = char_img.rotate(angle, resample=Image.BICUBIC, expand=True)
# 粘贴到画布

View File

@@ -0,0 +1,122 @@
"""
3D 旋转验证码生成器
生成旋转验证码:圆盘上绘制字符 + 方向标记,随机旋转 0-359°。
用户需将圆盘旋转到正确角度。
标签 = 旋转角度(整数)
文件名格式: {angle}_{index:06d}.png
"""
import random
from PIL import Image, ImageDraw, ImageFilter, ImageFont
from config import GENERATE_CONFIG, THREED_CHARS
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",
]
_DISC_COLORS = [
(180, 200, 220),
(200, 220, 200),
(220, 200, 190),
(200, 200, 220),
(210, 210, 200),
]
class ThreeDRotateGenerator(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_rotate"]
self.chars = THREED_CHARS
self.width, self.height = self.cfg["image_size"]
self._fonts: list[str] = []
for p in _FONT_PATHS:
try:
ImageFont.truetype(p, 20)
self._fonts.append(p)
except OSError:
continue
if not self._fonts:
raise RuntimeError("未找到任何可用字体,无法生成验证码")
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)
# 1. 背景
bg_val = rng.randint(*self.cfg["bg_color_range"])
img = Image.new("RGB", (self.width, self.height), (bg_val, bg_val, bg_val))
draw = ImageDraw.Draw(img)
# 2. 绘制圆盘
cx, cy = self.width // 2, self.height // 2
r = self.cfg["disc_radius"]
disc_color = rng.choice(_DISC_COLORS)
draw.ellipse(
[cx - r, cy - r, cx + r, cy + r],
fill=disc_color, outline=(100, 100, 100), width=2,
)
# 3. 在圆盘上绘制字符和方向标记 (未旋转状态)
disc_img = Image.new("RGBA", (r * 2 + 4, r * 2 + 4), (0, 0, 0, 0))
disc_draw = ImageDraw.Draw(disc_img)
dc = r + 2 # disc center
# 字符 (圆盘中心)
font_path = rng.choice(self._fonts)
font_size = int(r * 0.6)
font = ImageFont.truetype(font_path, font_size)
ch = rng.choice(self.chars)
bbox = font.getbbox(ch)
tw = bbox[2] - bbox[0]
th = bbox[3] - bbox[1]
disc_draw.text(
(dc - tw // 2 - bbox[0], dc - th // 2 - bbox[1]),
ch, fill=(50, 50, 50, 255), font=font,
)
# 方向标记 (三角箭头,指向上方)
ms = self.cfg["marker_size"]
marker_y = dc - r + ms + 2
disc_draw.polygon(
[(dc, marker_y - ms), (dc - ms // 2, marker_y), (dc + ms // 2, marker_y)],
fill=(220, 60, 60, 255),
)
# 4. 旋转圆盘内容
disc_img = disc_img.rotate(-angle, resample=Image.BICUBIC, expand=False)
# 5. 粘贴到背景
paste_x = cx - dc
paste_y = cy - dc
img.paste(disc_img, (paste_x, paste_y), disc_img)
# 6. 添加少量噪点
for _ in range(rng.randint(20, 50)):
nx, ny = rng.randint(0, self.width - 1), rng.randint(0, self.height - 1)
nc = tuple(rng.randint(100, 200) for _ in range(3))
draw.point((nx, ny), fill=nc)
# 7. 轻微模糊
img = img.filter(ImageFilter.GaussianBlur(radius=0.5))
return img, text

View 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