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

83
cli.py
View File

@@ -3,6 +3,9 @@ CaptchaBreaker 命令行入口
用法:
python cli.py generate --type normal --num 60000
python cli.py generate --type 3d_text --num 80000
python cli.py generate --type 3d_rotate --num 60000
python cli.py generate --type 3d_slider --num 60000
python cli.py train --model normal
python cli.py train --all
python cli.py export --all
@@ -20,15 +23,21 @@ from pathlib import Path
def cmd_generate(args):
"""生成训练数据。"""
from config import (
SYNTHETIC_NORMAL_DIR, SYNTHETIC_MATH_DIR, SYNTHETIC_3D_DIR,
SYNTHETIC_NORMAL_DIR, SYNTHETIC_MATH_DIR,
SYNTHETIC_3D_TEXT_DIR, SYNTHETIC_3D_ROTATE_DIR, SYNTHETIC_3D_SLIDER_DIR,
CLASSIFIER_DIR, TRAIN_CONFIG, CAPTCHA_TYPES, NUM_CAPTCHA_TYPES,
)
from generators import NormalCaptchaGenerator, MathCaptchaGenerator, ThreeDCaptchaGenerator
from generators import (
NormalCaptchaGenerator, MathCaptchaGenerator, ThreeDCaptchaGenerator,
ThreeDRotateGenerator, ThreeDSliderGenerator,
)
gen_map = {
"normal": (NormalCaptchaGenerator, SYNTHETIC_NORMAL_DIR),
"math": (MathCaptchaGenerator, SYNTHETIC_MATH_DIR),
"3d": (ThreeDCaptchaGenerator, SYNTHETIC_3D_DIR),
"3d_text": (ThreeDCaptchaGenerator, SYNTHETIC_3D_TEXT_DIR),
"3d_rotate": (ThreeDRotateGenerator, SYNTHETIC_3D_ROTATE_DIR),
"3d_slider": (ThreeDSliderGenerator, SYNTHETIC_3D_SLIDER_DIR),
}
captcha_type = args.type
@@ -50,25 +59,31 @@ def cmd_generate(args):
gen = gen_cls()
gen.generate_dataset(num, str(out_dir))
else:
print(f"未知类型: {captcha_type} 可选: normal, math, 3d, classifier")
valid = ", ".join(list(gen_map.keys()) + ["classifier"])
print(f"未知类型: {captcha_type} 可选: {valid}")
sys.exit(1)
def cmd_train(args):
"""训练模型。"""
if args.all:
# 按依赖顺序: normal → math → 3d → classifier
print("按顺序训练全部模型: normal → math → 3d → classifier\n")
print("按顺序训练全部模型: normal → math → 3d_text → 3d_rotate → 3d_slider → classifier\n")
from training.train_normal import main as train_normal
from training.train_math import main as train_math
from training.train_3d import main as train_3d
from training.train_3d_text import main as train_3d_text
from training.train_3d_rotate import main as train_3d_rotate
from training.train_3d_slider import main as train_3d_slider
from training.train_classifier import main as train_classifier
train_normal()
print("\n")
train_math()
print("\n")
train_3d()
train_3d_text()
print("\n")
train_3d_rotate()
print("\n")
train_3d_slider()
print("\n")
train_classifier()
return
@@ -78,12 +93,16 @@ def cmd_train(args):
from training.train_normal import main as train_fn
elif model == "math":
from training.train_math import main as train_fn
elif model == "3d":
from training.train_3d import main as train_fn
elif model == "3d_text":
from training.train_3d_text import main as train_fn
elif model == "3d_rotate":
from training.train_3d_rotate import main as train_fn
elif model == "3d_slider":
from training.train_3d_slider import main as train_fn
elif model == "classifier":
from training.train_classifier import main as train_fn
else:
print(f"未知模型: {model} 可选: normal, math, 3d, classifier")
print(f"未知模型: {model} 可选: normal, math, 3d_text, 3d_rotate, 3d_slider, classifier")
sys.exit(1)
train_fn()
@@ -96,7 +115,14 @@ def cmd_export(args):
if args.all:
export_all()
elif args.model:
_load_and_export(args.model)
# 别名映射
alias = {
"3d_text": "threed_text",
"3d_rotate": "threed_rotate",
"3d_slider": "threed_slider",
}
name = alias.get(args.model, args.model)
_load_and_export(name)
else:
print("请指定 --all 或 --model <name>")
sys.exit(1)
@@ -137,19 +163,19 @@ def cmd_predict_dir(args):
sys.exit(1)
print(f"批量识别: {len(images)} 张图片\n")
print(f"{'文件名':<30} {'类型':<8} {'结果':<15} {'耗时(ms)':>8}")
print("-" * 65)
print(f"{'文件名':<30} {'类型':<10} {'结果':<15} {'耗时(ms)':>8}")
print("-" * 67)
total_ms = 0.0
for img_path in images:
result = pipeline.solve(str(img_path), captcha_type=args.type)
total_ms += result["time_ms"]
print(
f"{img_path.name:<30} {result['type']:<8} "
f"{img_path.name:<30} {result['type']:<10} "
f"{result['result']:<15} {result['time_ms']:>8.1f}"
)
print("-" * 65)
print("-" * 67)
print(f"总计: {len(images)} 张 平均: {total_ms / len(images):.1f} ms 总耗时: {total_ms:.1f} ms")
@@ -178,28 +204,43 @@ def main():
# ---- generate ----
p_gen = subparsers.add_parser("generate", help="生成训练数据")
p_gen.add_argument("--type", required=True, help="验证码类型: normal, math, 3d, classifier")
p_gen.add_argument(
"--type", required=True,
help="验证码类型: normal, math, 3d_text, 3d_rotate, 3d_slider, classifier",
)
p_gen.add_argument("--num", type=int, required=True, help="生成数量")
# ---- train ----
p_train = subparsers.add_parser("train", help="训练模型")
p_train.add_argument("--model", help="模型名: normal, math, 3d, classifier")
p_train.add_argument(
"--model",
help="模型名: normal, math, 3d_text, 3d_rotate, 3d_slider, classifier",
)
p_train.add_argument("--all", action="store_true", help="按依赖顺序训练全部模型")
# ---- export ----
p_export = subparsers.add_parser("export", help="导出 ONNX 模型")
p_export.add_argument("--model", help="模型名: normal, math, 3d, classifier, threed")
p_export.add_argument(
"--model",
help="模型名: normal, math, 3d_text, 3d_rotate, 3d_slider, classifier",
)
p_export.add_argument("--all", action="store_true", help="导出全部模型")
# ---- predict ----
p_pred = subparsers.add_parser("predict", help="识别单张验证码")
p_pred.add_argument("image", help="图片路径")
p_pred.add_argument("--type", default=None, help="指定类型跳过分类: normal, math, 3d")
p_pred.add_argument(
"--type", default=None,
help="指定类型跳过分类: normal, math, 3d_text, 3d_rotate, 3d_slider",
)
# ---- predict-dir ----
p_pdir = subparsers.add_parser("predict-dir", help="批量识别目录中的验证码")
p_pdir.add_argument("directory", help="图片目录路径")
p_pdir.add_argument("--type", default=None, help="指定类型跳过分类: normal, math, 3d")
p_pdir.add_argument(
"--type", default=None,
help="指定类型跳过分类: normal, math, 3d_text, 3d_rotate, 3d_slider",
)
# ---- serve ----
p_serve = subparsers.add_parser("serve", help="启动 HTTP 识别服务")