2025/3/8

注:本文中所有代码均是问AI实现(因为我是一点不会写💀)

题目:2000个验证码,正确率95%即可通过

去搜了一下常见的本地ocr平台,尝试搭建 ddddocr

尝试了一下直接使用自带的模型发现效果非常不理想

所以先尝试对图片进行处理(滤波)(这里我要提一位老师了--赖志辉老师,他上个学期给我们分配任务去做pre,我负责的就是滤波器,这才知道世界上有滤波器这种东西,才能把这道题做出来)

先创一个虚拟环境吧,用的是python3.9(版本太高貌似不行)

"C:\Users\xiaoy\AppData\Local\Programs\Python\Python39\python.exe" -m venv venv_py39

venv_py39\Scripts\activate

先去掉蓝色混淆线条,并灰度处理

去除蓝色混淆线条

from PIL import Image
import numpy as np
import colorsys

def rgb_to_hsv(rgb):
    return colorsys.rgb_to_hsv(rgb[0]/255.0, rgb[1]/255.0, rgb[2]/255.0)

def remove_specific_rgba(img, target_rgba):
    """删除特定RGBA颜色的像素(设置为透明)"""
    img = img.convert("RGBA")
    data = np.array(img)
    r, g, b, a = data[:,:,0], data[:,:,1], data[:,:,2], data[:,:,3]
    mask = (r == target_rgba[0]) & (g == target_rgba[1]) & (b == target_rgba[2]) & (a == target_rgba[3])
    data[mask] = [0, 0, 0, 0]  # 设置为完全透明
    return Image.fromarray(data)

# 参数设置
target_blue = (64, 56, 247)
hue_range = (0.58, 0.72)
sat_threshold = 0.3
val_threshold = 0.5
remove_color = (254, 224, 222, 255)  # 新增要删除的RGBA颜色

# 读取图片
img = Image.open(r"F:\ctf\ocr proj\ddddocr-full\generate_captcha.png").convert("RGB")
pixels = np.array(img)

# 原始颜色过滤流程
hsv_pixels = np.array([[[colorsys.rgb_to_hsv(p[0]/255., p[1]/255., p[2]/255.)] for p in row] for row in pixels])
hsv_pixels = np.squeeze(hsv_pixels)

h_mask = (hsv_pixels[:,:,0] >= hue_range[0]) & (hsv_pixels[:,:,0] <= hue_range[1])
s_mask = hsv_pixels[:,:,1] >= sat_threshold
v_mask = hsv_pixels[:,:,2] >= val_threshold
combined_mask = h_mask & s_mask & v_mask

pixels[combined_mask] = [255, 255, 255]
color_img = Image.fromarray(pixels)

# 新增RGBA删除模块
color_img = remove_specific_rgba(color_img, remove_color)

# 保存彩色结果(带透明通道)
color_img.save("filtered_image_hsv.png")
print("改进后的过滤完成,彩色结果已保存为 filtered_image_hsv.png")

# 转换为灰度(自动忽略透明像素)
gray_img = color_img.convert('L')
gray_img.save("filtered_image_gray.png")
print("灰度结果已保存为 filtered_image_gray.png")

原图:

效果如下:

清晰多了,这时候去识别还是不理想,就又加了个锐化滤波器

效果如下:

还是识别错误

于是我尝试使用最后的方法

生成训练集,本地训练,用本地训练的模型来识别

用到了 ddddocr_train

安装cuda和pytorch这里不再赘述(反正我是第一次安装并用显卡训练,过程可以说是一波三折,有好多报错,不过好在有AI帮我把问题都解决了)

ddddocr_train的github有详细使用方法

这道题目给了php源码,所以先生成几万张标注好的验证码

<?php
// 设置保存路径
$savePath = 'D:/phpstudy_pro/WWW/IMAGE/captcha';

// 创建目录(如果不存在)
if (!is_dir($savePath)) {
    mkdir($savePath, 0755, true);
}

// 生成100张验证码
for ($i = 0; $i < 100; $i++) {
    generateCaptcha($savePath);
}

function generateCaptcha($savePath) {
    // 创建图像资源
    $image = imagecreate(100, 50);

    // 生成随机颜色
    $background_color = imagecolorallocate($image, rand(220, 255), rand(220, 255), rand(220, 255));
    $text_color1 = imagecolorallocate($image, rand(0, 100), rand(0, 100), rand(0, 100));
    $text_color2 = imagecolorallocate($image, rand(0, 100), rand(0, 100), rand(0, 100));
    $line_color = imagecolorallocate($image, 0, 0, 255);

    // 填充背景
    imagefill($image, 0, 0, $background_color);

    // 生成随机验证码
    $chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
    $code = substr(str_shuffle($chars), 0, 4);

    // 绘制验证码文本
    $font_size = 5;
    for ($i = 0; $i < strlen($code); $i++) {
        $color = ($i % 2 == 0) ? $text_color1 : $text_color2;
        $x = 10 + ($i * 15) + mt_rand(-5, 5);
        $y = mt_rand(10, 30);
        imagestring($image, $font_size, $x, $y, $code[$i], $color);
    }

    // 添加干扰线
    for ($i = 0; $i < 5; $i++) {
        imageline($image, 
            mt_rand(0, 100),
            mt_rand(0, 50),
            mt_rand(0, 100),
            mt_rand(0, 50),
            $line_color
        );
    }

    // 生成唯一文件名(验证码内容+微秒时间戳)
    $timestamp = str_replace('.', '_', microtime(true));
    $filename = "{$code}_{$timestamp}.png";
    $filePath = $savePath . '/' . $filename;

    // 保存图像
    imagepng($image, $filePath);
    imagedestroy($image);
}

echo "已生成100张验证码到目录:{$savePath}";
?>

这里用时间戳不对,训练不出来,后来又用python脚本把标注好的后面的时间戳全部换成ddddocr要求的32位随机hex值

import os
import shutil
import secrets

def rename_and_copy_images():
    # 定义源目录和目标目录
    source_dir = r'D:\phpstudy_pro\WWW\IMAGE\captcha_sharped'
    dest_dir = r'D:\phpstudy_pro\WWW\IMAGE\newcaptcha'

    # 支持的图片扩展名列表
    image_extensions = ('.png', '.jpg', '.jpeg', '.gif', '.bmp')

    # 创建目标目录(如果不存在)
    os.makedirs(dest_dir, exist_ok=True)

    # 遍历源目录中的所有文件
    for filename in os.listdir(source_dir):
        # 检查是否为图片文件
        if filename.lower().endswith(image_extensions):
            # 分离文件名和扩展名
            name_part, ext = os.path.splitext(filename)

            # 跳过不足5个字符的文件名
            if len(name_part) < 5:
                print(f"警告:跳过文件 '{filename}',文件名长度不足5个字符")
                continue

            # 构建新文件名
            prefix = name_part[:5]
            random_hex = secrets.token_hex(16)  # 生成32位随机hex
            new_filename = f"{prefix}{random_hex}{ext}"

            # 构建完整文件路径
            src_path = os.path.join(source_dir, filename)
            dst_path = os.path.join(dest_dir, new_filename)

            # 复制文件到目标目录
            shutil.copy2(src_path, dst_path)
            print(f"已处理:{filename} -> {new_filename}")


if __name__ == "__main__":
    rename_and_copy_images()
    print("所有图片处理完成!")

接着就是两个批量滤波(把原来的单张图片滤波代码丢进AI,给出原始图片路径和目标文件夹,让AI生成,如果有报错就把错误信息投喂给AI,根据AI生成的解决方案逐步排查)

去除蓝色混淆线条

from PIL import Image
import numpy as np
import colorsys
import os
import glob

def rgb_to_hsv(rgb):
    return colorsys.rgb_to_hsv(rgb[0]/255.0, rgb[1]/255.0, rgb[2]/255.0)

def remove_specific_rgba(img, target_rgba):
    """删除特定RGBA颜色的像素(设置为透明)"""
    img = img.convert("RGBA")
    data = np.array(img)
    r, g, b, a = data[:,:,0], data[:,:,1], data[:,:,2], data[:,:,3]
    mask = (r == target_rgba[0]) & (g == target_rgba[1]) & (b == target_rgba[2]) & (a == target_rgba[3])
    data[mask] = [0, 0, 0, 0]
    return Image.fromarray(data)

# 参数设置
target_blue = (64, 56, 247)
hue_range = (0.58, 0.72)
sat_threshold = 0.3
val_threshold = 0.5
remove_color = (254, 224, 222, 255)

# 路径设置
input_dir = r'D:\phpstudy_pro\WWW\IMAGE\captcha'
output_dir = r'D:\phpstudy_pro\WWW\IMAGE\captcha_filtered'

# 创建输出目录
os.makedirs(output_dir, exist_ok=True)

# 处理所有图片
for img_path in glob.glob(os.path.join(input_dir, '*')):
    try:
        # 读取图片
        img = Image.open(img_path).convert("RGB")
        pixels = np.array(img)
        
        # 颜色过滤
        hsv_pixels = np.array([[[colorsys.rgb_to_hsv(p[0]/255., p[1]/255., p[2]/255.)] for p in row] for row in pixels])
        hsv_pixels = np.squeeze(hsv_pixels)

        h_mask = (hsv_pixels[:,:,0] >= hue_range[0]) & (hsv_pixels[:,:,0] <= hue_range[1])
        s_mask = hsv_pixels[:,:,1] >= sat_threshold
        v_mask = hsv_pixels[:,:,2] >= val_threshold
        combined_mask = h_mask & s_mask & v_mask

        pixels[combined_mask] = [255, 255, 255]
        color_img = Image.fromarray(pixels)
        
        # RGBA删除
        color_img = remove_specific_rgba(color_img, remove_color)
        
        # 转换为灰度
        gray_img = color_img.convert('L')
        
        # 保存结果
        output_path = os.path.join(output_dir, os.path.basename(img_path))
        gray_img.save(output_path)
        print(f"处理完成: {os.path.basename(img_path)}")
        
    except Exception as e:
        print(f"处理失败: {os.path.basename(img_path)} - {str(e)}")

锐化处理

from PIL import Image
import numpy as np
import os
import glob

# 路径配置
input_dir = r'D:\phpstudy_pro\WWW\IMAGE\captcha_filtered'
output_dir = r'D:\phpstudy_pro\WWW\IMAGE\captcha_sharped'

# 锐化卷积核(拉普拉斯增强)
sharpen_kernel = np.array([
    [ 0, -1,  0],
    [-1,  5, -1],
    [ 0, -1,  0]
], dtype=np.float32)

def sharpen_image(img_array):
    """执行卷积锐化操作,忽略边缘像素"""
    filtered = np.zeros_like(img_array)
    for i in range(1, img_array.shape[0]-1):
        for j in range(1, img_array.shape[1]-1):
            filtered[i, j] = np.sum(img_array[i-1:i+2, j-1:j+2] * sharpen_kernel)
    return np.clip(filtered, 0, 255).astype(np.uint8)

# 创建输出目录
os.makedirs(output_dir, exist_ok=True)

# 批量处理
for img_path in glob.glob(os.path.join(input_dir, '*')):
    try:
        # 读取文件
        img = Image.open(img_path).convert('L')
        
        # 执行锐化
        sharpened_array = sharpen_image(np.array(img, dtype=np.float32))
        
        # 保存结果
        output_path = os.path.join(output_dir, os.path.basename(img_path))
        Image.fromarray(sharpened_array).save(output_path)
        print(f"锐化成功: {os.path.basename(img_path)}")
        
    except Exception as e:
        print(f"处理失败: {os.path.basename(img_path)} - {str(e)}")

最后拿到3万多张标注好的验证码

拿去训练

在ddddocr_train目录下新建虚拟环境,然后

python app.py create test
python app.py cache test D:\phpstudy_pro\WWW\IMAGE\newcaptcha
python app.py train test

让显卡去训练即可

这个训练程序默认97%准确率会停止并导出onnx模型,然后把原来的识别脚本加上导入这个onnx模型即可

每1000step会进行acc检测,如图,第一次是0

刚开始训练比较慢,后面会变快

在第7000step第一次出现acc不为0,耐心等待即可

用4060算的,跟大模型比起来,这个显存占用率非常低

第8000step直接0.71875

10000step到了0.9375

第14000首次到了1.0,之后在0.96到1.0之间浮动

训练到大约23000step,得到正确率超过97%的模型

训练数据在 F:\ctf\ocr proj\dddd-train\dddd_trainer\projects\test\models

找到onnx模型和charsets.json

先测试一下单张图片的识别情况

测试此onnx模型能否正常使用:

import ddddocr
import numpy as np  # 添加这行导入语句

ocr = ddddocr.DdddOcr(det=False, ocr=False, import_onnx_path=r"F:\ctf\ocr proj\dddd-train\dddd_trainer\projects\test\models\test_1.0_21_23000_2025-03-27-11-22-33.onnx", charsets_path=r"F:\ctf\ocr proj\dddd-train\dddd_trainer\projects\test\models\charsets.json")

# 设置字符范围为大小写英文+数字(对应参数6)
ocr.set_ranges(6)

# 读取图片(注意Windows路径需要转义)
image_path = r"F:\ctf\ocr proj\ddddocr-full\sharpened_image.png"
with open(image_path, "rb") as f:
    img_bytes = f.read()

# 进行识别并获取概率结果
result = ocr.classification(img_bytes, probability=True)

# 拼接识别结果
s = "".join([result['charsets'][np.argmax(char_probs)] for char_probs in result['probability']])

print("", s)

遇到了点问题

让AI改一下代码

拿到修改后的代码

import ddddocr
import numpy as np

ocr = ddddocr.DdddOcr(
    det=False,
    ocr=False,
    import_onnx_path=r"F:\ctf\ocr proj\dddd-train\dddd_trainer\projects\test\models\test_1.0_21_23000_2025-03-27-11-22-33.onnx",
    charsets_path=r"F:\ctf\ocr proj\dddd-train\dddd_trainer\projects\test\models\charsets.json"
)

ocr.set_ranges(6)

image_path = r"F:\ctf\ocr proj\ddddocr-full\sharpened_image.png"
with open(image_path, "rb") as f:
    img_bytes = f.read()

# 直接获取识别结果(推荐)
result = ocr.classification(img_bytes)
print("识别结果:", result)

# 若确实需要处理概率输出(需要确认数据结构)
# 先检查数据结构
result_with_probs = ocr.classification(img_bytes, probability=True)
print("数据结构:", type(result_with_probs))
print("包含的键:", result_with_probs.keys())
print("probability类型:", type(result_with_probs['probability']))
print("charsets类型:", type(result_with_probs['charsets']))

识别成功

让AI把前面的两个滤波+ocr+验证脚本合在一起

import requests
import ddddocr
import numpy as np
from PIL import Image
import colorsys
from io import BytesIO
import os  # 添加路径验证

def process_image(img_content):
    def rgb_to_hsv(rgb):
        return colorsys.rgb_to_hsv(rgb[0]/255.0, rgb[1]/255.0, rgb[2]/255.0)

    def remove_specific_rgba(img, target_rgba):
        img = img.convert("RGBA")
        data = np.array(img)
        r, g, b, a = data[:,:,0], data[:,:,1], data[:,:,2], data[:,:,3]
        mask = (r == target_rgba[0]) & (g == target_rgba[1]) & (b == target_rgba[2]) & (a == target_rgba[3])
        data[mask] = [0, 0, 0, 0]
        return Image.fromarray(data)

    # 参数设置
    target_blue = (64, 56, 247)
    hue_range = (0.58, 0.72)
    sat_threshold = 0.3
    val_threshold = 0.5
    remove_color = (254, 224, 222, 255)

    # 从字节流读取图片
    img = Image.open(BytesIO(img_content)).convert("RGB")
    pixels = np.array(img)
    
    # HSV过滤
    hsv_pixels = np.array([[[colorsys.rgb_to_hsv(p[0]/255., p[1]/255., p[2]/255.)] for p in row] for row in pixels]).squeeze()
    combined_mask = (hsv_pixels[:,:,0] >= hue_range[0]) & (hsv_pixels[:,:,0] <= hue_range[1]) & \
                   (hsv_pixels[:,:,1] >= sat_threshold) & (hsv_pixels[:,:,2] >= val_threshold)
    pixels[combined_mask] = [255, 255, 255]
    color_img = remove_specific_rgba(Image.fromarray(pixels), remove_color)
    
    # 转换为灰度
    gray_img = color_img.convert('L')
    
    # 第二阶段:锐化处理
    sharpen_kernel = np.array([[0, -1, 0], [-1, 5, -1], [0, -1, 0]], dtype=np.float32)
    img_array = np.array(gray_img, dtype=np.float32)
    filtered = np.zeros_like(img_array)
    
    for i in range(1, img_array.shape[0]-1):
        for j in range(1, img_array.shape[1]-1):
            filtered[i, j] = np.sum(img_array[i-1:i+2, j-1:j+2] * sharpen_kernel)
    
    sharpened = np.clip(filtered, 0, 255).astype(np.uint8)
    
    # 第三阶段:OCR识别(关键修改部分)
    try:
        # 路径验证(调试用)
        print("模型存在:", os.path.exists(r"F:\ctf\ocr proj\dddd-train\dddd_trainer\projects\test\models\test_1.0_21_23000_2025-03-27-11-22-33.onnx"))
        print("字符集存在:", os.path.exists(r"F:\ctf\ocr proj\dddd-train\dddd_trainer\projects\test\models\charsets.json"))

        # 初始化OCR引擎
        ocr = ddddocr.DdddOcr(
            det=False,
            ocr=False,
            import_onnx_path=r"F:\ctf\ocr proj\dddd-train\dddd_trainer\projects\test\models\test_1.0_21_23000_2025-03-27-11-22-33.onnx",
            charsets_path=r"F:\ctf\ocr proj\dddd-train\dddd_trainer\projects\test\models\charsets.json"
        )
        ocr.set_ranges(6)  # 设置字符范围

        img_buffer = BytesIO()
        Image.fromarray(sharpened).save(img_buffer, format='PNG')
        img_bytes = img_buffer.getvalue()
        
        # 直接获取识别结果
        result_text = ocr.classification(img_bytes)
        return result_text.replace(" ", "").strip()

    except Exception as e:
        print(f"OCR引擎异常: {str(e)}")
        return "ERROR"

# 自动化循环部分
with requests.Session() as s:
    base_url = "http://c15f4704-4db8-4b6f-8e09-f80cd4c22d1b.ctf.szu.moe/"
    
    for i in range(1, 2001):
        try:
            # 获取验证码
            captcha_resp = s.get(f"{base_url}/generate_captcha.php")
            if captcha_resp.status_code != 200:
                print(f"第{i}次: 获取验证码失败")
                continue

            # 处理识别
            captcha_text = process_image(captcha_resp.content)
            print(f"第{i}次识别结果: {captcha_text}")
            
            # 提交验证
            post_resp = s.post(
                f"{base_url}/verify.php",
                data={"captcha_input": captcha_text}
            )
            
            print(f"第{i}次响应: {post_resp.text.strip()}")

        except Exception as e:
            print(f"第{i}次发生异常: {str(e)}")
            continue

print("所有循环完成")

直接打即可

拿到flag