news 2026/9/24 18:11:09

文字点选验证码识别实战:基于YOLOv5与OCR的Python课设指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
文字点选验证码识别实战:基于YOLOv5与OCR的Python课设指南

简介:这是一份面向Python课程设计或验证码识别入门者的完整项目资源,专注解决文字点选、选字类验证码的自动识别问题。项目在仅300张样本的小样本训练下达到96%准确率,单次识别耗时约100~300ms,且经过Windows平台Python3.6/3.8/3.10实测,低至1核2G的服务器也能流畅运行,适合作为毕业设计、课设或业务原型参考。资源包共48个文件,大小约121.82MB,以Python源码(.py)为主,辅以编译后的.pyd加速模块、训练好的.bin模型权重、示例图片(png/jpg)以及配置文件,覆盖数据预处理、模型训练、接口调用与Web展示等环节。其中纯文本说明和markdown文档便于快速理解项目结构,图片资源则可用于自测与效果展示。目前已有437人学习下载,适合希望快速上手验证码识别技术、借鉴小样本训练思路或在此项目基础上二次开发的开发者。

1. 文字点选验证码识别,难的不是“认字”,是“把字和位置对上”

文字点选验证码识别,核心不是“认字”。很多第一次做课设的同学,拿到题目第一时间去搜 OCR,想的是把整张图的文字全读出来——方向错了。真正的文字点选验证码,背景图里散落着若干候选文字,干扰线、噪点、旋转字符混在一起,系统任务是“按提示顺序依次点击正确的文字”,不是把整张图当作文本识别。所以识别链路拆开是三步:定位文字位置、读清文字内容、按顺序生成点击坐标。这篇笔记按照一个 Python 课设的实际路径来讲:从技术选型、造数据、训练检测模型,到坐标映射和常见坑,尽量让新手能一步步复现,让已经跑通基础流程的人看到边界和参数陷阱。文字点选方向的课设,可演示、可数据化评估、可扩展,是个性价比不错的 CV 入门项目。

2. 文字点选识别链路拆三步:定位、读字、排序

文字点选验证码虽然叫“验证码”,但它和传统数字字母验证码有本质区别。传统验证码是“一行文本”,而文字点选的结构是:一张背景图上有若干个候选文字,数量和位置随机,系统提示“请依次点击:甲、乙、丙”。算法要做的不是把全图文字读出来,而是先找到这些文字在哪、再判断哪个字是哪个,最后按提示顺序给出坐标。这个结构决定了它的识别链路必须拆成定位、读字、排序三段。

2.1 文字点选验证码的识别任务拆分:检测是主线,OCR 是辅助

先看三个常见误用方案,理解为什么不能直接套用现成工具。

方案一:直接整图 OCR。OCR 对印刷体整段文字有效,但文字点选里的文字是离散的、可能旋转、带干扰,整图 OCR 会漏识别,还会把干扰线识别成文字,更致命的是 OCR 输出的是文本流,没有位置和顺序语义。方案二:模板匹配。模板匹配需要给每个候选文字准备标准字模,实际样本里字体、大小、旋转、背景都在变,模板匹配几乎必崩,适合做对照组,不适合做主线。方案三:端到端模型直接输出点击坐标,理论上可行,但需要大量标注数据,课设时间和算力都不现实。

正确主线是:用目标检测模型先框出所有候选文字的位置,这一步只区分“是文字”和“不是文字”,不关心具体是什么字;然后对每个框裁图,用 OCR 或单字分类器识别框内文字;最后按提示文字顺序排序,输出点击坐标。三个任务的输出分别是:位置框、单字内容、有序坐标点。理解这条链路,后面所有参数调整都有了依据。

2.2 两条可落地的技术路线:轻量检测模型 vs 传统 CV

根据课设常见条件(一台普通笔记本、没有 GPU、两周到一个月时间),我一般让同学在下面两条路线里选,也建议两条都跑,一条保底一条冲指标。

对比项路线A:纯 OpenCV 传统视觉路线B:轻量目标检测模型(YOLOv5s)
开发难度低,不依赖深度学习框架中,需要装 torch 和模型文件
数据要求不需要训练数据需要几百张标注图,可合成
抗干扰能力弱,干扰线稍复杂就崩强,模型能学到干扰线特征
CPU 推理速度极快,单张几十毫秒较快,单张 0.1~0.3 秒
课设得分点流程完整,但指标一般准确率高,可扩展对比实验
主要风险真实场景泛化差合成数据和真实数据分布不一致

路线 A 的核心是灰度化、二值化、连通域分析,把每个文字块当作候选框,再用轮廓特征过滤干扰。路线 B 的核心是先用合成数据训练 YOLOv5s,推理时用检测框裁图,再交给单字分类器。如果只有 CPU 笔记本,YOLOv5 可以换 nano 版本并把输入尺寸降到 416,训练时间能省一半,检测精度损失不大。

环境准备方面,python 环境建议用 vscode 或 pycharm 配好解释器,装好 opencv-python、numpy、torch、paddleocr 这些第三方库。这里有个容易忽略的点:torch 在 CPU 和 GPU 版本的行为差异只在速度,不影响结果,课设用 CPU 版完全够。

3. 文字点选的数据从哪来:合成样本与图像预处理

目标检测模型训练的最大痛点是人工标注。手工框几百张图,每张图 5~8 个字,工作量极大,而且容易标歪。课设里最常见的做法是合成样本:用 OpenCV 和 PIL 把文字画到背景上,同时自动生成 YOLO 格式的标注文件。合成数据的可控性强,干扰线数量、文字旋转角度、背景复杂度都能调,后面做对比实验时天然有梯度。

3.1 用 OpenCV 合成带标注的样本:自动生成训练集

下面这个脚本会生成 500 张 416x416 的训练图,每张图随机放 5~8 个汉字,随机画干扰线,同时写出对应的 YOLO txt 标注文件。字体文件用 Windows 自带的 simhei.ttf,换成其他字体也行,但建议至少两种字体混合,避免模型过拟合到单一字形。

import os import random import cv2 import numpy as np from PIL import Image, ImageDraw, ImageFont chars = "甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥" out_dir = "dataset/images/train" label_dir = "dataset/labels/train" os.makedirs(out_dir, exist_ok=True) os.makedirs(label_dir, exist_ok=True) def draw_char(img_pil, ch, x, y, size, angle): # 每个字符画在独立透明图层上,方便旋转后贴回背景 font = ImageFont.truetype("simhei.ttf", size) tmp = Image.new("RGBA", (size * 2, size * 2), (0, 0, 0, 0)) tmp_draw = ImageDraw.Draw(tmp) tmp_draw.text((size // 2, size // 2), ch, font=font, fill=(0, 0, 0, 255), anchor="mm") tmp = tmp.rotate(angle, expand=True) img_pil.paste(tmp, (x, y), tmp) for idx in range(500): # 浅色背景,模拟真实验证码的底色 img = np.random.randint(200, 255, (416, 416, 3), dtype=np.uint8) img_pil = Image.fromarray(img) draw = ImageDraw.Draw(img_pil) num_words = random.randint(5, 8) selected = random.sample(chars, num_words) boxes = [] for ch in selected: x = random.randint(20, 320) y = random.randint(20, 320) size = random.randint(32, 52) angle = random.randint(-15, 15) draw_char(img_pil, ch, x, y, size, angle) boxes.append((x, y, size, ch)) # 干扰线:数量、粗细、颜色都随机 for _ in range(random.randint(3, 8)): x1, y1 = random.randint(0, 415), random.randint(0, 415) x2, y2 = random.randint(0, 415), random.randint(0, 415) draw.line((x1, y1, x2, y2), fill=(random.randint(0, 120),) * 3, width=random.randint(1, 2)) img = np.array(img_pil.convert("RGB")) cv2.imwrite(f"{out_dir}/syn_{idx:04d}.jpg", img) # YOLO 标注:类别0 + 归一化中心坐标和宽高 with open(f"{label_dir}/syn_{idx:04d}.txt", "w", encoding="utf-8") as f: for (x, y, size, ch) in boxes: cx = (x + size) / 2 / 416 cy = (y + size) / 2 / 416 w = size / 416 h = size / 416 f.write(f"0 {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}\n")

这段代码的关键设计是:所有候选字的类别都写 0,因为检测模型只需要回答“这里有没有字”,不需要回答“这是什么字”。字的识别放到后面单独做,这样可以把检测模型简化成单类别目标检测,训练难度和过拟合风险都会明显下降,这是文字点选课设里一个很实用的小技巧。

参数说明:size 控制字号范围,32~52 对应 416 图上的实际像素,太小模型难学,太大文字容易相互遮挡;angle 控制在 -15~15 度,旋转超过 30 度后单字识别难度陡增,真实验证码很少超过这个范围;干扰线使用深色随机灰度,宽度 1~2 像素,和文字形成对比,模型才有东西可学。合成图的类别只有一类,所以 data.yaml 里的 nc 写 1 即可。

还有一个重要动作:真实验证码截图也要收集一小批,30~50 张就够,用 LabelImg 手工标注后加入训练集。合成数据负责让模型学会“文字长什么样”,真实数据负责纠正分布偏差,两者混合才能让准确率在真实场景不掉链子。

3.2 图像预处理:降噪、二值化、形态学开运算

很多同学一上来就做暴力预处理,把图二值化再用膨胀腐蚀,结果把文字也抠烂了。预处理在文字点选场景里的定位应该是“辅助工具,不是主线”。具体来说:模型训练阶段直接用原始图或轻微增强图,不做二值化;传统 CV 保底路线必须做二值化和连通域分析;推理阶段可根据图像质量做轻度降噪。

import cv2 import numpy as np def preprocess_for_detection(img_path): # 灰度化 + 中值滤波去椒盐噪声 img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) img = cv2.medianBlur(img, 3) # OTSU 自动阈值二值化,前景文字为白色 _, binary = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU) # 开运算:先腐蚀后膨胀,去掉孤立噪点和细干扰线 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) opened = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel, iterations=1) # 连通域分析,按面积过滤小噪点 num, labels, stats, _ = cv2.connectedComponentsWithStats( opened, connectivity=8) boxes = [] for i in range(1, num): x, y, w, h, area = stats[i] if area < 30 or w < 10 or h < 10: continue boxes.append((x, y, w, h)) return boxes

逻辑说明:medianBlur 的核大小选 3,过大会把文字笔画磨掉;OTSU 阈值适合背景和文字灰度差异明显的图,如果背景复杂可以改用自适应阈值,但要注意自适应阈值对噪声更敏感。开运算的 kernel 也是 3x3,迭代 1 次就停,目的是去掉 1~2 像素的椒盐噪声和细干扰线,不是让文字变形。连通域过滤的阈值 30 和 10 是针对 416 图经验值,换了大图要等比例放大。

这里必须提醒一句:预处理宁轻勿重。检测模型本身对噪声有一定鲁棒性,真正影响准确率的是“文字被破坏”和“干扰线被保留”,前者比后者严重得多。合成数据里的干扰线是模型训练的一部分,直接喂原始图让模型学习“哪些是干扰、哪些是文字”,效果往往比人工规则过滤更好。

4. 用 YOLO 训练文字检测:从模型输出到点击坐标

数据准备好之后,进入主流程:训练检测模型、推理得到候选框、把框映射成点击坐标。这一章是课设的核心,也是参数细节最多的地方。

4.1 用 YOLOv5s 训练文字检测模型:数据组织与训练命令

先按 YOLO 的目录规范把数据组织好,train 和 val 分开,标注文件与图片同名同目录规则。

dataset/ ├── images/ │ ├── train/ │ │ ├── syn_0000.jpg │ │ └── ... │ └── val/ │ ├── syn_0001.jpg │ └── ... └── labels/ ├── train/ │ ├── syn_0000.txt │ └── ... └── val/ └── ...

data.yaml 内容如下,类别只有一类,名称叫 char,表示“文字候选框”。

train: dataset/images/train val: dataset/images/val nc: 1 names: ['char']

训练命令:

python train.py --data data.yaml --weights yolov5s.pt \ --img 416 --batch 16 --epochs 50 --patience 10

参数说明:img 选 416 而不是默认的 640,因为课设数据是合成的,文字目标本身不大,416 在 CPU 上能省近一半训练时间;batch 16 是 CPU 和 4G 显存都基本能跑的值,显存不够就降到 8;epochs 从 50 起步,用 patience 10 做早停,合成数据简单,训练后期 loss 下降很快,没必要硬跑到 100 轮;weights 用 yolov5s.pt 作为预训练。为什么不选 nano?nano 虽然在 CPU 上更快,但对小尺寸文字的检测边框贴合度明显不如 s,课设验收时要展示检测框可视化,s 的效果更稳定。

训练完成后看 runs/train/exp/weights/best.pt 这个文件,后面的推理和演示都用它。如果训练时 loss 一直不降,先检查 data.yaml 路径是不是相对路径、图片和 txt 是否一一对应,这种问题占训失败原因的一半以上。

4.2 推理端:检测框坐标、OCR 识别、顺序整理

推理端是整篇代码里最容易翻车的地方,翻车点集中在坐标换算。模型输入是 letterbox 后的 416 图,输出的坐标是相对 416 图的,要还原到原图必须记录缩放比例和 padding 量。下面这段代码是完整的推理流程,按“检测 → 裁图 → 单字识别 → 排序输出”顺序执行。

import cv2 import torch import numpy as np model = torch.hub.load('ultralytics/yolov5', 'custom', path='runs/train/exp/weights/best.pt', force_reload=False) model.conf = 0.35 model.iou = 0.45 def letterbox(im, new_shape=416, color=(114, 114, 114)): h, w = im.shape[:2] r = min(new_shape / h, new_shape / w) new_unpad = (int(round(w * r)), int(round(h * r))) dw = (new_shape - new_unpad[0]) / 2 dh = (new_shape - new_unpad[1]) / 2 top, bottom = int(round(dh - 0.1)), int(round(dh + 0.1)) left, right = int(round(dw - 0.1)), int(round(dw + 0.1)) im = cv2.resize(im, new_unpad, interpolation=cv2.INTER_LINEAR) im = cv2.copyMakeBorder(im, top, bottom, left, right, cv2.BORDER_CONSTANT, value=color) return im, r, dw, dh def infer_and_get_points(img_path, prompt_chars): img = cv2.imread(img_path) orig_h, orig_w = img.shape[:2] img_input, r, dw, dh = letterbox(img) results = model(img_input) boxes = results.xyxy[0].cpu().numpy() char_boxes = [] for *xyxy, conf, cls in boxes: x1, y1, x2, y2 = [int(v) for v in xyxy] # 回原图坐标:先减 padding 再除缩放比 x1_orig = int((x1 - dw) / r) y1_orig = int((y1 - dh) / r) x2_orig = int((x2 - dw) / r) y2_orig = int((y2 - dh) / r) cx = (x1_orig + x2_orig) // 2 cy = (y1_orig + y2_orig) // 2 char_boxes.append((cx, cy, x1_orig, y1_orig, x2_orig, y2_orig)) # 对每个框裁图,交给单字识别 # 这里用 PaddleOCR rec 模型或自训练分类器 from paddleocr import PaddleOCR ocr = PaddleOCR(use_angle_cls=False, lang='ch', show_log=False, det=False) char_dict = {} for (cx, cy, x1, y1, x2, y2) in char_boxes: crop = img[y1:y2, x1:x2] if crop.size == 0: continue result = ocr.ocr(crop, cls=False) text = result[0][0][0] if result and result[0] else '' if text: char_dict[text] = (cx, cy) # 按提示文字顺序输出点击坐标 click_points = [] for ch in prompt_chars: if ch in char_dict: click_points.append(char_dict[ch]) return click_points

这段代码有三个关键点。第一是坐标回算,letterbox 时记录 r、dw、dh,推理坐标先减 padding 再除以缩放比,这里是“差一行代码坐标就偏”的高危区。第二是单字识别,PaddleOCR 的 det=False 表示只做识别不做检测,因为裁剪框已经由检测模型给出,再做检测会重复且更慢。第三是 char_dict 的键是文字内容,提示文字是什么就去字典里取对应坐标,而不是按检测框的 x 坐标从左到右排,顺序逻辑由提示字符串决定。

参数说明:model.conf 和 model.iou 是 YOLOv5 推理的核心阈值,conf 默认 0.25 对合成数据偏低,容易把干扰线框进来,建议 0.35~0.45;iou 0.45 控制相邻框的合并,文字点选场景里两个字间距通常足够,不用调太低。PaddleOCR 的 use_angle_cls=False 跳过方向分类,单字识别可以省一点耗时。如果候选文字是已知集合,也可以不接 OCR,直接用一个小分类网络或模板匹配,课设里更可控。

5. 避坑笔记:文字点选课设最容易翻车的五个细节

下面这些问题都是课设里实际见过、卡住半天以上的典型坑,按“现象 → 原因 → 解决”写清楚,训练前先看一遍能省不少时间。

5.1 模型训练与推理阶段的三个坑

坑一:置信度阈值太低,干扰线被识别成文字。现象:检测结果里多了很多框,把噪点和干扰线圈进去,点击坐标自然就错。原因:合成样本里的干扰线形态单一,模型学到的是“有深色块就框”,加上默认 conf 0.25 给了干扰线太多机会。解决:训练时把干扰线改成随机曲线、随机粗细和颜色,不要只用直线;推理时把 conf 从 0.25 提升到 0.35~0.45,并在验证集上扫一遍阈值,选准确率最高的值。

坑二:训练图尺寸和推理图尺寸不一致,坐标偏移。现象:单张测试时看着没问题,批量跑准确率骤降,检测框和实际字位置错开。原因:训练时 YOLO 内部统一缩放到 416,推理时如果直接 cv2.resize 到 416 而不是 letterbox,宽高比被拉伸,文字变形,坐标也对不齐。解决:推理时严格使用 letterbox,记住 r、dw、dh 三个量,坐标回算时先减 padding 再除缩放比。这条解决了,一半的坐标错位问题会消失。

坑三:合成样本里文字颜色太单一,模型对白底黑字过拟合。现象:在合成验证集上准确率 95%,换成真实截图直接掉到 40%。原因:合成代码里文字固定黑色,背景固定浅色,模型只是在学“深色是字,浅色是背景”这种简单规则。解决:合成时给文字加随机颜色抖动,RGB 三通道各加减 30~50,背景加渐变或噪声纹理;同时收集 30~50 张真实截图做手工标注,混合进训练集,泛化能力会明显改善。

5.2 坐标与交互阶段的两个坑

坑四:输出顺序不是提示顺序。现象:文字框识别出来了,但点击顺序错,准确率按整单计算就是 0。原因:很多代码把检测结果按 x 坐标从左到右排序,但提示文字是乱序的“丙、甲、乙”,必须按提示字符串顺序取坐标。解决:先解析提示文字列表,用识别结果建立“文字 → 坐标”的字典,然后按提示顺序遍历字典。注意识别结果里可能有多余字或漏字,遇到提示文字不在字典里时,宁可跳过也不要乱序补位。

坑五:标注框只框住部分文字,导致点击位置偏移。现象:检测边框歪斜,有的缺左上角,有的缺右下角,点击坐标偏出文字范围。原因:合成脚本里文字尺寸计算有偏差,手工标注时框得太紧,导致模型学到的边框本身就紧。解决:合成时在文字四周留 2~4 像素 padding,统一边框生成逻辑;对已有边框做等比膨胀 2 像素,再把膨胀后的框用于裁图和计算中心点。还有一个相关细节:角点坐标必须落在原图范围内,越界会导致裁图失败。

6. 给文字点选识别做可视化验证与评估

课设验收时,光有准确率数字不够,评委更愿意看到“图上有框、有顺序、有轨迹”的直观演示。我习惯把最终输出做成一张标注图:每个候选字画一个绿色矩形框,框上方写识别出的文字,点击顺序用黄色数字标在框中心,再用一条折线把点击顺序连起来。这样三秒钟就能看出系统有没有跑通,比一堆命令行日志有说服力得多。

def draw_result(img, click_points, boxes): result = img.copy() for i, (cx, cy) in enumerate(click_points): cv2.circle(result, (cx, cy), 5, (0, 255, 255), -1) cv2.putText(result, str(i + 1), (cx + 8, cy - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 255), 2) for (x1, y1, x2, y2) in boxes: cv2.rectangle(result, (x1, y1), (x2, y2), (0, 255, 0), 2) return result

评估脚本也要写,目的是量化“点选顺序完全正确”的比例,而不是单个字对不对。判断标准很简单:算法输出的坐标序列和真实坐标序列逐点距离小于 15 像素,且顺序一致,才算这一单通过。批量跑测试集,输出整体通过率,这份指标就是课设报告里的核心数据。轨迹动画可以加,用模拟鼠标点击或画贝塞尔曲线动画都行,属于加分项。我当年做课设时只顾着调模型,最后演示脚本连顺序标注都没做,评委根本看不出点选逻辑,白白丢了印象分。后来花一晚上补了可视化,效果立竿见影。做这个方向,先跑通最小闭环,再逐步加参,顺序对了,坑踩完了,剩下的都是时间问题,希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/24 18:11:00

JSP+Access手机销售系统实战:环境搭建、分页优化与迁移MySQL

简介&#xff1a;这份资源是面向Java Web初学者与课程设计学习者的完整项目资料包&#xff0c;围绕基于JSP与Access数据库的手机销售系统展开&#xff0c;可用于毕业设计参考、课程实践或自学练手。压缩包共2.26MB&#xff0c;内含项目报告、详细设计说明书、需求说明书、数据库…

作者头像 李华
网站建设 2026/9/24 18:09:46

JavaEE二手图书交易平台实战:Spring+MyBatis从零搭建与避坑指南

简介&#xff1a;这是一套面向高校计算机相关专业学生的JavaEE课程设计完整项目&#xff0c;以二手图书交易平台为主题&#xff0c;适合作为期末大作业、课程设计或毕业设计参考。项目采用Java语言开发&#xff0c;功能覆盖用户注册登录、图书发布、分类浏览、订单管理等核心业…

作者头像 李华
网站建设 2026/9/24 18:08:46

YOLOv8行人检测实战:环境搭建、数据转换、训练调参与ONNX部署

简介&#xff1a;基于YOLOv8的行人检测项目&#xff0c;专为计算机科学、人工智能、通信工程、自动化等专业的课程设计、毕业设计及项目初期演示而准备&#xff0c;也适合有一定基础的学习者进阶。项目包含训练模式与视频检测两个Python脚本&#xff0c;配套yolov8n.pt、yolo11…

作者头像 李华
网站建设 2026/9/24 18:08:31

Cytoscape.js 布局事件 Promise 化:`layout.pon()` 用法与源码级解析

数据可视化 【免费下载链接】cytoscape.js Graph theory (network) library for visualisation and analysis 项目地址&#xff1a; https://gitcode.com/gh_mirrors/cy/cytoscape.js 点击查看 免费下载 本篇指南围绕 Cytoscape.js 布局对象提供的 layout.pon()&#xff08;别…

作者头像 李华
网站建设 2026/9/24 18:07:58

乳腺细胞癌症分割数据集实战:50张图跑通医学图像分割全流程

简介&#xff1a;本资源面向医学图像分割方向的初学者与算法实践者&#xff0c;提供一套乳腺细胞癌症分割的二分类数据集&#xff0c;可用于训练与验证语义分割网络&#xff0c;帮助解决医学影像中病灶区域自动提取的入门实验需求。压缩包共103个文件&#xff0c;以png图像为主…

作者头像 李华
网站建设 2026/9/24 18:07:42

Python实现Disco Diffusion本地图像生成:原理、配置与实战

简介&#xff1a;基于Python的Disco Diffusion图像生成工具&#xff0c;将CLIP语义理解与扩散模型结合&#xff0c;可根据文本提示直接生成高质量图像&#xff0c;面向AI绘画爱好者、设计师及深度学习初学者&#xff1b;项目对原始代码做了简化和修改&#xff0c;降低了上手门槛…

作者头像 李华