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:
122
generators/threed_rotate_gen.py
Normal file
122
generators/threed_rotate_gen.py
Normal 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
|
||||
Reference in New Issue
Block a user