news 2026/11/3 13:20:40

基于OpenCV的笔迹识别系统:从图像预处理到分类实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于OpenCV的笔迹识别系统:从图像预处理到分类实战

简介:这份资源是面向高校计算机相关专业学生与OpenCV初学者的笔迹识别系统完整项目包,可直接用于毕业设计选题、课程实践或图像识别入门练手。项目以Python与OpenCV为核心,覆盖图像灰度化、二值化、边缘检测、最大外接矩形生成与矩形筛选等预处理流程,并采用余弦距离与直方图相似度两级比对策略完成笔迹相似度判定,最终将运行时间、结果与状态输出至txt文件,界面部分由PyQt5实现,包含文件选取、图片展示与运行触发等功能。包内共29个文件,含9个png与4个jpg测试素材、9个docx及doc、pptx等文档,另有2个py源码、2个md说明与1个txt日志,压缩包约43.92MB,涵盖需求分析、概要设计、详细设计、数据库设计、测试与使用说明等完整文档链路。目前已有147人学习,适合需要从源码到文档一站式参考的读者。

1. 从一张作业本照片到可检索的笔迹特征:这套 OpenCV 方案到底在做什么

很多人第一次接触笔迹识别,是导师甩过来一句“做个能认出手写字的系统”,然后自己搜到一堆 OCR 接口,调通之后发现识别的是印刷体,手写一塌糊涂。这套基于 OpenCV 图像识别的笔迹识别系统,解决的正是这个落差:它不依赖云端接口,从一张手机拍的作业本照片开始,做灰度化、二值化、去噪、倾斜校正、字符切分,再提取笔迹的几何与纹理特征,最后落到分类或比对。适合谁?做计算机毕业设计、需要一套能跑通、能讲清楚原理、能自己改参数的本科或硕士同学;也适合想入门 OpenCV 图像处理、拿一个完整小项目练手的工程师。核心词就三个:OpenCV、图像识别、笔迹识别。下面按“先跑通再调优”的顺序拆开讲。

2. 环境与数据准备:把 OpenCV 装对,把笔迹样本收齐

2.1 安装 OpenCV 的三种路径与选型理由

标题里带“源码+教程+全部数据文档”,意味着拿到压缩包后第一件事不是读代码,而是把运行环境对齐。OpenCV 的安装方式直接决定后面会不会出现ModuleNotFoundError: No module named 'opencv'或者cv2.error这类翻车现场。常见做法有三条路,我一般按项目类型选:

第一条是 pip 安装,最省事,适合纯 Python 的笔迹识别脚本:

# 推荐用虚拟环境隔离,避免和系统里的其他包打架 python -m venv venv # Windows 激活 venv\Scripts\activate # Linux / macOS 激活 source venv/bin/activate # 安装主包,opencv-python 带 GUI 功能,适合本地调试 pip install opencv-python==4.8.1.78 # 如果只需要算法、不需要 imshow 窗口,用 headless 版更小 # pip install opencv-python-headless

这里参数说明一下:opencv-python包含cv2.imshow等可视化函数,调试笔迹切分效果时能直接弹窗看中间结果;opencv-python-headless去掉了 GUI 依赖,适合部署到没有桌面的服务器。版本号建议锁在 4.5 以上,因为cv2.findContours的返回值和旧版 3.x 不同,网上很多老教程还在用三返回值写法,直接抄会报ValueError: not enough values to unpack。

第二条是 conda 安装,适合已经用 Anaconda 管理毕业设计环境的同学:

conda create -n handwriting python=3.9 conda activate handwriting conda install -c conda-forge opencv

conda 的好处是会自动处理底层依赖,Windows 上少踩编译坑。第三条是源码编译,只有你需要 CUDA 加速或者改 OpenCV 内部函数时才值得做,普通笔迹识别项目完全没必要,编译一次半小时起步,血泪经验是别在 deadline 前一周折腾。

装完必须验证,别信“安装成功”的提示:

import cv2 import numpy as np # 打印版本,确认不是装到了别的 Python 环境 print("OpenCV version:", cv2.__version__) # 造一张纯白图,验证基本读写和数组操作正常 img = np.ones((100, 100), dtype=np.uint8) * 255 cv2.imwrite("test_white.png", img) reload = cv2.imread("test_white.png", cv2.IMREAD_GRAYSCALE) print("shape:", reload.shape, "min/max:", reload.min(), reload.max())

如果import cv2报ModuleNotFoundError,九成是 pip 和 python 不是同一个环境,用which python(Linux/macOS)或where python(Windows)确认解释器路径,再pip show opencv-python看装到哪去了。这个排查顺序能省掉大量玄学时间。

2.2 笔迹数据集的采集与目录规范

笔迹识别和印刷体 OCR 最大的区别在数据:印刷体有现成的大规模标注集,笔迹样本往往要自己收。毕业设计里常见的做法是找 20 到 50 个同学,每人按模板写若干遍,形成“书写人—样本”两级结构。目录建议这样组织:

dataset/ ├── writer_01/ │ ├── sample_01.png │ ├── sample_02.png │ └── ... ├── writer_02/ │ └── ... └── labels.csv

labels.csv记录每张图对应的书写人 ID 和采集条件:

filepath,writer_id,pen_type,paper_type writer_01/sample_01.png,01,gel,lined writer_01/sample_02.png,01,ballpoint,blank writer_02/sample_01.png,02,gel,lined

为什么要记笔和纸?因为笔迹识别模型很容易把“笔的类型”当成“人的特征”学进去,换一支笔准确率就掉。把采集条件写进标签,后面做数据增强和误差分析时能定位问题。样本量上,每人至少 10 张,总样本低于 200 张时,传统特征加 SVM 往往比硬上深度学习更稳,这也是这套 OpenCV 方案的价值所在——它不要求你有 GPU 和大数据集。

采集时统一用手机后置摄像头、正对纸面、光线均匀,避免阴影横跨文字。分辨率控制在 1000×750 到 2000×1500 之间,太大后面处理慢,太小笔画糊成一团。这一步偷懒,后面预处理再花哨也救不回来。

3. 预处理流水线:灰度、二值化、去噪、倾斜校正

3.1 从彩色图到二值图的关键参数

笔迹识别的预处理目标只有一个:把“笔画的形状”干净地留下来,把纸张纹理、光照不均、拍摄噪声全部干掉。第一步灰度化:

import cv2 import numpy as np img = cv2.imread("writer_01/sample_01.png") # 灰度化,笔迹识别不需要颜色信息 gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 高斯模糊,核大小必须是奇数,5x5 适合 1000px 级别的图 blur = cv2.GaussianBlur(gray, (5, 5), 0) # 自适应阈值,比全局阈值更能扛光照不均 binary = cv2.adaptiveThreshold( blur, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, blockSize=25, C=10 ) cv2.imwrite("step_binary.png", binary)

逻辑说明:THRESH_BINARY_INV让笔画变成白色(255)、背景变成黑色(0),因为后续findContours默认找白色前景。blockSize=25表示每个像素参考周围 25×25 区域算阈值,图越大这个值要相应调大,一般取图像短边的 1/20 到 1/30。C=10是常数修正项,值越大越容易把浅色噪点判成背景。如果二值化后笔画断裂,先把C调小到 5;如果背景出现大片黑斑,把blockSize调大。

全局阈值cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY_INV)也能用,但它对光照敏感,同一批样本里有的偏亮有的偏暗时,固定阈值会翻车。自适应阈值是这套方案里我强烈建议保留的一步。

3.2 去噪与形态学:别把笔画当噪声删掉

二值化之后通常还有孤立噪点和小桥连。形态学开运算能去噪,但核不能太大:

# 3x3 开运算:先腐蚀后膨胀,去掉小噪点,保留笔画主体 kernel = np.ones((3, 3), np.uint8) opened = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel, iterations=1) # 闭运算:先膨胀后腐蚀,填补笔画内部小孔 closed = cv2.morphologyEx(opened, cv2.MORPH_CLOSE, kernel, iterations=1) cv2.imwrite("step_morph.png", closed)

参数说明:iterations=1是安全值,迭代两次以上细笔画会被吃掉,尤其是用铅笔写的样本。如果样本笔画本来就细,可以跳过开运算,只做闭运算。判断标准很简单:把step_morph.png和原图并排看,笔画该连的连、该断的断,没有多余黑点,就合格。

3.3 倾斜校正:霍夫变换与最小外接矩形两条路

手机拍的作业本很难保证水平,倾斜会让后续切分和特征提取全部偏移。校正思路是先找到文字区域的主方向,再旋转回来。常见做法有两种:

第一种用最小外接矩形,适合整页文字方向一致的情况:

# 找所有前景点的坐标 coords = np.column_stack(np.where(closed > 0)) # 计算最小外接矩形,返回 (中心, 尺寸, 角度) rect = cv2.minAreaRect(coords) angle = rect[-1] # minAreaRect 的角度范围是 [-90, 0),需要归一化 if angle < -45: angle = 90 + angle (h, w) = closed.shape[:2] center = (w // 2, h // 2) M = cv2.getRotationMatrix2D(center, angle, 1.0) rotated = cv2.warpAffine(closed, M, (w, h), flags=cv2.INTER_CUBIC, borderMode=cv2.BORDER_REPLICATE) cv2.imwrite("step_rotated.png", rotated)

第二种用霍夫直线检测,适合有横线格的作业本,先检测横线再算倾角。cv2.HoughLinesP的参数里threshold和minLineLength最关键,线格样本可以把minLineLength设成图像宽度的 1/3。两条路我都试过,最小外接矩形代码短、对无格纸更稳,霍夫对有格纸更准。毕业设计里建议先跑最小外接矩形,效果不满意再换。

校正后一定要回看step_rotated.png,如果文字被转成倒立,说明角度符号取反了,把angle前面加负号即可。这个坑几乎每个人都会踩一次。

4. 笔迹特征提取与识别:从切分到分类的完整链路

4.1 字符切分:投影法与轮廓法的取舍

预处理完的图是一整页文字,要识别笔迹,先得把单个字切出来。投影法是最直观的:

# 水平投影,找行 row_sum = np.sum(closed, axis=1) # 阈值取最大投影的 5%,低于它的行视为空白 row_thresh = row_sum.max() * 0.05 rows = [] in_line = False for i, v in enumerate(row_sum): if v > row_thresh and not in_line: start = i; in_line = True elif v <= row_thresh and in_line: rows.append((start, i)); in_line = False # 对每一行做垂直投影,找字 chars = [] for (r0, r1) in rows: line = closed[r0:r1, :] col_sum = np.sum(line, axis=0) col_thresh = col_sum.max() * 0.05 in_char = False for j, v in enumerate(col_sum): if v > col_thresh and not in_char: c0 = j; in_char = True elif v <= col_thresh and in_char: chars.append((r0, r1, c0, j)); in_char = False print("切出字符数:", len(chars))

逻辑说明:投影法假设字与字之间有空白列,对工整书写有效,对连笔字会切错。阈值系数 0.05 是经验值,字距小的样本调到 0.02,字距大的调到 0.1。轮廓法用cv2.findContours找每个连通域,再按面积过滤,适合连笔但容易把一个字的偏旁拆成两块。实际项目里我一般先用投影法,切分结果画框回看,错得多的区域再局部用轮廓法补。

4.2 特征设计:几何特征加纹理特征

切出单字后,直接拿像素喂分类器效果很差,因为书写位置、大小、倾斜都会变。要提取对书写人稳定、对书写条件不敏感的特征。常用组合:

特征类别具体特征维度作用
几何特征宽高比、笔画密度、重心偏移5区分字形习惯
投影特征水平/垂直投影直方图32描述笔画分布
纹理特征HOG 方向梯度直方图可调描述笔画走向
矩特征Hu 矩 7 个不变量7对旋转缩放不敏感

HOG 是这套方案里性价比最高的:

from skimage.feature import hog # 统一缩放到固定尺寸,消除大小差异 char_img = cv2.resize(closed[r0:r1, c0:c1], (64, 64)) # HOG 提取,orientations 是方向 bin 数 features = hog(char_img, orientations=9, pixels_per_cell=(8, 8), cells_per_block=(2, 2), block_norm='L2-Hys') print("HOG 维度:", features.shape)

参数说明:orientations=9把 0 到 180 度分成 9 个方向 bin,是行人检测里的经典值,笔迹识别也够用;pixels_per_cell=(8,8)表示每个 cell 8×8 像素,64×64 的图会得到 8×8 个 cell;cells_per_block=(2,2)做块归一化,抵抗光照残差。维度算下来是 9×7×7×4=1764 维,对几百个样本来说偏高,可以用 PCA 降到 100 到 200 维再分类。

4.3 分类器选择:SVM 与 KNN 的实测差异

特征有了,分类器选型看样本量。样本少于 500 时,SVM 配 RBF 核通常比 KNN 稳:

from sklearn.svm import SVC from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler from sklearn.pipeline import make_pipeline from sklearn.metrics import classification_report # X 是特征矩阵,y 是书写人标签 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.3, random_state=42, stratify=y) clf = make_pipeline( StandardScaler(), SVC(C=10, gamma='scale', kernel='rbf', probability=True) ) clf.fit(X_train, y_train) pred = clf.predict(X_test) print(classification_report(y_test, pred))

参数说明:C=10是惩罚系数,越大越容易过拟合,样本少时从 1 开始试;gamma='scale'让 sklearn 自动按特征方差定核宽度,比手写gamma=0.01省心;stratify=y保证训练测试集里每个书写人比例一致,否则某个人的样本全进测试集,准确率会虚高。KNN 的n_neighbors一般取 3 到 5,样本分布均匀时表现接近 SVM,但预测时要算所有距离,样本上万后变慢。

识别阶段还要注意:如果目标是“识别写了什么字”,那是 OCR 任务,需要字符类别标签;如果目标是“识别是谁写的”,那是书写人识别,标签是书写人 ID。标题里的“笔迹识别”两种都可能指,毕业设计里建议先做书写人识别,因为不需要标注每个字是什么,数据成本低,效果也更容易讲清楚。

5. 避坑与排查:那些让准确率一夜归零的细节

5.1 二值化后笔画断裂,识别全乱

现象:预处理输出的图里,横画中间断开,竖画缺一截,切分时一个字被切成两三块。原因:自适应阈值的C值偏大,或者高斯模糊核太大把细笔画糊没了。解决:先把C从 10 降到 5,再把模糊核从 5×5 降到 3×3,重新看step_binary.png。如果还断,检查原图分辨率,低于 800px 宽的手机照片本身细节就不够,重新采集比调参有效。

5.2 训练准确率 99%,测试掉到 60%

现象:训练集上几乎全对,换一批新样本就崩。原因:同一个人的多张样本被随机分到了训练和测试集,模型记住了这个人的特定笔画位置,而不是书写习惯。解决:按书写人划分数据集,同一个人要么全在训练集,要么全在测试集。用GroupShuffleSplit代替train_test_split:

from sklearn.model_selection import GroupShuffleSplit gss = GroupShuffleSplit(n_splits=1, test_size=0.3, random_state=42) train_idx, test_idx = next(gss.split(X, y, groups=writer_ids))

groups传书写人 ID 数组,这样切分才反映真实泛化能力。这个坑在毕业设计答辩时最容易被老师问穿。

5.3 换一支笔,准确率断崖下跌

现象:用中性笔采集的样本训练,换成圆珠笔测试,准确率从 85% 掉到 50%。原因:笔迹的粗细、灰度分布和笔的类型强相关,模型把笔的特征当成了人的特征。解决:采集阶段就混入多种笔,标签里记录笔型,训练时做数据增强,比如随机膨胀腐蚀模拟笔画粗细变化:

kernel = np.ones((2, 2), np.uint8) # 随机选择膨胀或腐蚀,模拟不同笔的粗细 if np.random.rand() > 0.5: aug = cv2.dilate(closed, kernel, iterations=1) else: aug = cv2.erode(closed, kernel, iterations=1)

增强只在训练集做,测试集保持原始条件,这样才能测出真实鲁棒性。

5.4 cv2.imread 读进来是 None

现象:cv2.imread返回None,后面cvtColor直接报Assertion failed。原因:路径里有中文或空格,OpenCV 在 Windows 上对非 ASCII 路径支持不好。解决:用cv2.imdecode配合np.fromfile读:

# 兼容中文路径的读取方式 data = np.fromfile("数据集/样本_01.png", dtype=np.uint8) img = cv2.imdecode(data, cv2.IMREAD_COLOR)

或者干脆把数据集目录改成纯英文,这是最省事的后悔药。

5.5 切分结果里混入标点和边框

现象:投影法把作业本上的横线和标点也切成了“字符”,特征维度对不上。原因:预处理没去掉长直线,投影阈值太低。解决:在二值化后用霍夫直线检测找出长横线并抹掉,或者按切分框的宽高比过滤,宽高比大于 8 或小于 0.1 的框直接丢弃。这一步加在切分之后、特征提取之前,能明显减少脏样本。

6. 把准确率再抬一截:特征融合与交叉验证的实操技巧

走到这里,系统已经能跑通,但毕业设计想拿高分,或者实际项目想上线,还得在验证方法上做文章。我一般会做两件事:特征融合和分层交叉验证。

特征融合是把 HOG 和投影直方图拼在一起,让分类器同时看到局部纹理和全局分布:

from skimage.feature import hog def extract_features(char_img): # 统一尺寸 img = cv2.resize(char_img, (64, 64)) # HOG 纹理特征 hog_feat = hog(img, orientations=9, pixels_per_cell=(8, 8), cells_per_block=(2, 2), block_norm='L2-Hys') # 水平与垂直投影直方图,各 32 维 h_proj = np.sum(img, axis=1) / 255.0 v_proj = np.sum(img, axis=0) / 255.0 h_hist, _ = np.histogram(h_proj, bins=32, range=(0, 64)) v_hist, _ = np.histogram(v_proj, bins=32, range=(0, 64)) # 拼接,归一化避免量纲差异 feat = np.concatenate([hog_feat, h_hist, v_hist]) return feat / (np.linalg.norm(feat) + 1e-8)

逻辑说明:HOG 描述笔画走向,投影直方图描述笔画在字框里的分布,两者互补。最后做 L2 归一化,防止 HOG 的大数值淹没直方图。维度从 1764 涨到 1828,增加不多,但实测在 300 样本、20 个书写人的数据上,准确率能比单用 HOG 高 3 到 5 个百分点。

第二件事是分层交叉验证,别只做一次 train_test_split:

from sklearn.model_selection import StratifiedGroupKFold from sklearn.svm import SVC from sklearn.preprocessing import StandardScaler from sklearn.pipeline import make_pipeline import numpy as np sgkf = StratifiedGroupKFold(n_splits=5, shuffle=True, random_state=42) scores = [] for train_idx, test_idx in sgkf.split(X, y, groups=writer_ids): clf = make_pipeline(StandardScaler(), SVC(C=10, gamma='scale', kernel='rbf')) clf.fit(X[train_idx], y[train_idx]) scores.append(clf.score(X[test_idx], y[test_idx])) print("5 折准确率:", np.round(scores, 3)) print("均值:", np.mean(scores).round(3), "标准差:", np.std(scores).round(3))

StratifiedGroupKFold同时保证每折里类别比例一致、同一书写人不跨折,是笔迹识别里最该用的验证方式。标准差比均值更能说明问题:均值 0.85、标准差 0.02,说明模型稳;均值 0.85、标准差 0.12,说明某一折崩了,得回去看那折的样本是不是采集条件特殊。

还有一个容易被忽略的技巧:把预处理参数也纳入调参。很多人只调 SVM 的C和gamma,却把blockSize、C值、形态学核大小写死。实际上这几组参数对最终准确率的影响不比分类器小。我习惯用网格搜索把预处理和分类器一起搜,虽然慢,但能避免“分类器调到头,其实是二值化没调好”的尴尬。搜索空间不用大,blockSize取 [15, 25, 35],阈值C取 [5, 10, 15],SVM 的C取 [1, 10, 100],组合数可控。

最后说个习惯:每改一次参数,都把中间图存下来,按step_01_gray.png、step_02_binary.png这样编号。笔迹识别这条链路环节多,出问题时靠肉眼回看中间结果,比盯着最终准确率猜要快得多。我吃过亏,曾经花一晚上调 SVM,最后发现是二值化把笔画切断了。希望帮到你。

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

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

FreeRtos:SysTick的功能分析

SysTick 是 Cortex-M 内核中一个仅有 24 位计数器的简单定时器&#xff0c;几乎被所有 RTOS 用作调度心跳。但它的潜力远不止于此——从精准微秒延时到代码性能分析&#xff0c;从软件定时器链到任务超时监控&#xff0c;SysTick 是 Cortex-M 上最被低估的万能工具之一。01SysT…

作者头像 李华
网站建设 2026/11/2 19:27:56

渲染系统四层架构:HAL/RHI/管线/命令层深度解析

1. 这不是教科书&#xff0c;是引擎组夜班改bug时撕下来的架构笔记你点开这个标题&#xff0c;大概率不是想背诵“渲染管线有哪几个阶段”这种标准答案——而是刚在Unity里调NPR效果卡了三天&#xff0c;发现Shader编译报错提示里突然冒出一句“a d3d11-compatible gpu (featur…

作者头像 李华
网站建设 2026/11/3 6:29:01

游戏引擎渲染系统架构设计:从管线组织到RHI抽象与Shader管理

1. 渲染系统在引擎里到底扮演什么角色聊游戏引擎架构&#xff0c;渲染系统永远是那个最显眼、也最容易被误解的部分。很多人一提到渲染&#xff0c;脑子里第一反应就是"画东西"&#xff0c;觉得无非是把模型丢给显卡、跑个Shader、屏幕上出图就完事了。真做过引擎的人…

作者头像 李华
网站建设 2026/10/29 5:56:59

PDF转Word免费软件推荐!新手零门槛好用不踩雷

日常办公、学生写论文、整理资料&#xff0c;几乎人人都要用到PDF转Word功能。但市面上大部分工具要么要开会员、要么转换后带水印、排版错乱&#xff0c;还有不少工具暗藏广告和捆绑下载&#xff0c;一不小心就踩坑。今天给大家整理一套真正免费、无套路、实用性拉满的PDF转Wo…

作者头像 李华
网站建设 2026/10/24 14:27:58

游戏引擎渲染系统架构:从场景数据到GPU命令的完整链路解析

聊到游戏引擎架构&#xff0c;我见过不少团队一上来就扎进着色器代码里&#xff0c;追着某个光照模型调半天。但说实话&#xff0c;渲染系统架构真正的难点&#xff0c;从来不在画面上那些亮眼的公式&#xff0c;而在于怎么把场景数据、资源生命周期、多线程调度和 GPU 命令生成…

作者头像 李华
网站建设 2026/10/25 11:01:35

Godot编辑器移植鸿蒙PC:技术路线与可行性深度解析

1. 项目概述与需求拆解&#xff1a;这个"移植"到底指什么&#xff1f;最近在几个开发者群里反复被人问同一个问题&#xff1a;Godot 编辑器能不能跑在鸿蒙 PC 上&#xff1f;问的人多了&#xff0c;我就花了两周时间把这件事里里外外捋了一遍。先给结论&#xff1a;把…

作者头像 李华