简介:本资源是一套面向计算机专业本科生的毕业设计实战项目,聚焦笔迹识别这一司法鉴定与金融验签中的关键需求,提供基于OpenCV图像处理与PyQt界面开发的完整可运行系统。项目通过逐字图像比对与相似度分析实现签名真伪判别,有效替代传统人工识别,兼顾准确性与工程实用性。压缩包为ZIP格式,大小38.21MB,包含源码、详细项目文档及可视化中间结果展示模块;其中Python主程序负责OpenCV图像预处理与特征匹配,PyQt构建简洁交互界面,matplotlib用于后台分步图像处理可视化(仅调试用),文档涵盖需求分析、算法原理、模块说明与部署指南。目前已有199人学习下载,读者可直接复现完整识别流程,掌握图像二值化、轮廓提取、HOG特征匹配等核心技能,并获得结构清晰的工程目录、可调试的分步代码及面向实际场景的验证逻辑。
1. 笔迹识别不是OCR:为什么毕业设计选OpenCV做笔迹识别,反而比用深度学习模型更稳、更可控、更容易答辩通过?
很多同学一看到“笔迹识别”,第一反应就是去搜 PyTorch + CRNN + CTC,结果跑通一个 demo 花了三周,训练数据凑不齐、标注成本高、模型在自己手写体上准确率不到 65%,最后答辩时被问“你这个模型泛化到不同人、不同纸张、不同光照下怎么保证鲁棒性?”,当场哑火。而真正落地的本科毕设,尤其是工科类图像方向,核心诉求从来不是 SOTA,而是可解释、可调试、可复现、可讲清楚每一步物理意义——这恰恰是 OpenCV 的强项。本项目标题里“基于 OpenCV 图像识别的笔迹识别系统”,本质是把“笔迹”当作一种结构化图形信号来处理:不是认字,而是识别笔画走向、连通区域、骨架拓扑、端点/分叉点分布等底层视觉特征;它不依赖海量标注,不黑盒,不报错就崩溃,改一行cv2.threshold()参数就能看到效果变化。适合电子工程、自动化、测控技术等专业学生,在 4~6 周内完成从图像采集→预处理→特征提取→规则匹配→结果输出的完整闭环。文档里写的“详细项目文档”,不是 Word 套模板,而是每张图配对应代码行号、每个阈值有实测对比截图、每种干扰(阴影、折痕、墨水洇染)都列出了对应的 OpenCV 抗干扰策略。这不是一个“能跑就行”的玩具,而是一个能放进简历、能现场演示、能回答“为什么这里用中值滤波而不是高斯滤波”这种问题的扎实工程模块。
2. 从原始图像到二值骨架:OpenCV 笔迹识别的四步不可跳过流水线
笔迹识别在 OpenCV 框架下,不是“调个cv2.OCR就完事”——OpenCV 本身不提供端到端文字识别 API(cv2.ocr是 contrib 模块且仅限 Tesseract 绑定,非本项目范畴)。本项目走的是经典计算机视觉路径:图像 → 二值化 → 连通域分析 → 骨架提取 → 特征编码。这四步环环相扣,跳过任意一步都会导致后续特征失效。下面按实际开发顺序拆解,每步附可直接运行的最小验证代码,并说明参数选择依据。
2.1 图像预处理:为什么必须用自适应阈值而非全局阈值?
手写稿普遍存在光照不均、纸张泛黄、边缘阴影等问题。全局阈值(如cv2.THRESH_BINARY + cv2.THRESH_OTSU)在阴影区会大面积丢失笔迹,在高光区又会产生噪点。实测对比表明:同一张扫描件,Otsu 阈值为 183,但左上角阴影区实际有效阈值需低至 120,右下角反光区则需抬高到 210。因此必须用局部自适应方法。
import cv2 import numpy as np img = cv2.imread("handwriting.jpg", cv2.IMREAD_GRAYSCALE) # 关键:blockSize 必须为奇数,C 是常数偏移量(用于抑制背景噪声) binary = cv2.adaptiveThreshold( img, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, # 比 MEAN_C 更抗局部突变 cv2.THRESH_BINARY, blockSize=41, # 实测:31~61 之间最稳,太小易碎,太大失真 C=10 # 正向偏移,防止弱笔迹被误判为背景 ) cv2.imwrite("binary_adaptive.png", binary)参数逻辑说明:
blockSize决定局部窗口大小,影响笔迹粗细保真度。若笔迹平均宽度约 3px,则blockSize应 ≥ 11(3×3.7),否则窗口内无足够信息判断该区域是否为笔迹;C=10是经验值,对浅灰笔迹(如铅笔淡写)至关重要——它让算法在局部均值基础上再减 10,从而把灰度值 140~160 的有效笔迹也纳入前景。若C设为负值,会导致大量伪笔迹噪点。
2.2 噪声抑制与笔迹连通:中值滤波 + 形态学闭运算的组合为何不可替代?
自适应二值化后,图像常带椒盐噪声、断线、毛刺。此时不能直接开运算(会扩大笔迹),也不能只用高斯模糊(会模糊笔画边缘)。中值滤波 + 闭运算是经过上百张真实手稿验证的黄金组合:
- 中值滤波:消除孤立噪点,保持边缘锐度(高斯模糊会使笔画变粗、端点模糊);
- 闭运算(先膨胀后腐蚀):连接因纸张纤维或扫描抖动造成的笔画断裂,但不过度粘连相邻字符。
# 接上一步 binary 图像 denoised = cv2.medianBlur(binary, ksize=3) # ksize=3 是底线,5 会过度平滑 # 定义结构元素:矩形比椭圆更能保持笔画方向性 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 1)) # 水平方向连接断裂 closed = cv2.morphologyEx(denoised, cv2.MORPH_CLOSE, kernel, iterations=1) cv2.imwrite("closed.png", closed)为什么用
(3,1)而非(3,3)?笔迹主方向多为水平或斜向,(3,1)在 x 方向提供连接能力,y 方向保持约束,避免上下字符粘连;若用(3,3),在“i”、“j”等带点字符上极易将点与主干粘死,后续骨架提取失败。iterations=1是关键——迭代 2 次后,原本独立的“l”和“i”可能合并成一个连通域,特征编码彻底错乱。
2.3 连通域分割:如何精准切分单个字符/笔画单元?
OpenCV 的cv2.connectedComponentsWithStats不仅返回标签图,更关键的是stats输出每个连通域的left,top,width,height,area。这才是笔迹识别的真正起点——所有后续分析(骨架、端点、曲率)都必须基于单个连通域进行,而非整图。常见错误是直接对全图做骨架提取,结果多个字符纠缠成一团,无法区分“8”和“B”。
# 对 closed 图像做连通域分析 num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(closed, connectivity=8) # 过滤掉面积过小(噪点)和过大(整行)的区域 char_regions = [] for i in range(1, num_labels): # 跳过背景 label 0 x, y, w, h, area = stats[i] if 50 < area < 5000 and w > 5 and h > 5: # 面积阈值需根据实际扫描 DPI 调整 char_regions.append((x, y, w, h)) # 可视化验证:在原图上框出候选字符 vis = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR) for (x, y, w, h) in char_regions: cv2.rectangle(vis, (x, y), (x+w, y+h), (0, 255, 0), 1) cv2.imwrite("char_boxes.png", vis)面积阈值设定依据:以 A4 纸 300dpi 扫描为例,单个汉字(楷体)外接矩形面积约 800~2500 px²;数字“1”约 300 px²,“8”约 1200 px²。
50 < area < 5000覆盖了 95% 的单字符范围。若项目针对签名识别(长连笔),则需放宽上限至 15000,并增加长宽比过滤(w/h < 8防止整行误入)。
2.4 骨架提取:为什么cv2.ximgproc.thinning是唯一可靠选择?
OpenCV 官方未提供标准骨架算法,cv2.ximgproc.thinning(需 opencv-contrib-python ≥ 4.5.4)是目前最稳定、最接近文献标准的实现。它基于 Zhang-Suen 迭代细化,比手动实现的八邻域查表法更鲁棒,且支持THINNING_ZHANGSUEN和THINNING_GUOHALL两种模式。实测表明:ZHANGSUEN对细笔画保留更好,GUOHALL对粗笔画抗断更优。
# 注意:需先 pip install opencv-contrib-python>=4.5.4 import cv2.ximgproc as xip # 对每个 char_region 提取骨架 skeletons = [] for (x, y, w, h) in char_regions: roi = closed[y:y+h, x:x+w] # 确保输入为 uint8 二值图(0/255) roi_bin = (roi > 0).astype(np.uint8) * 255 skel = xip.thinning(roi_bin, thinningType=xip.THINNING_ZHANGSUEN) skeletons.append(skel) # 合并所有骨架用于可视化 merged_skel = np.zeros_like(closed) for i, skel in enumerate(skeletons): x, y, w, h = char_regions[i] merged_skel[y:y+h, x:x+w] = skel cv2.imwrite("skeletons.png", merged_skel)关键提醒:
xip.thinning输入必须是uint8类型且像素值严格为0或255。若传入bool数组或0/1数组,函数静默失败,返回全零图——这是毕业设计中最隐蔽的翻车点之一。务必加* 255强制转换。
3. 从骨架到特征:用 OpenCV 原生函数提取 5 类可判别笔迹特征
骨架只是中间表示,真正用于识别的是其拓扑与几何属性。本项目不采用深度学习提取高维 embedding,而是定义 5 类人工可解释、OpenCV 原生可算的特征,每类特征都对应明确的笔迹生理/书写习惯含义。这些特征共同构成一个轻量级分类器(如 SVM 或决策树)的输入,也是答辩时你能指着图说清“为什么这个特征值代表‘捺’”的底气所在。
3.1 端点与分叉点计数:最基础却最关键的拓扑特征
汉字笔画以“起笔—行笔—收笔”为单元,端点(degree=1)对应起笔/收笔,分叉点(degree=3 或 4)对应“横折钩”、“走之底”等复杂转折。OpenCV 无直接函数,但可用形态学+连通域统计实现:
def count_endpoints_and_junctions(skel): # 计算每个像素的 8 邻域和 kernel = np.array([[1,1,1], [1,0,1], [1,1,1]], dtype=np.uint8) neighbors = cv2.filter2D(skel, -1, kernel, borderType=cv2.BORDER_CONSTANT) # 端点:骨架点且邻域和 == 1 endpoints = (skel == 255) & (neighbors == 1) # 分叉点:骨架点且邻域和 >= 3 (Zhang-Suen 骨架中,分叉点邻域和通常为 3 或 4) junctions = (skel == 255) & (neighbors >= 3) return np.sum(endpoints), np.sum(junctions) # 示例:对第一个字符骨架计算 ep, jp = count_endpoints_and_junctions(skeletons[0]) print(f"端点数: {ep}, 分叉点数: {jp}") # 如“口”字:端点 4(四角),分叉点 0;“十”字:端点 4,分叉点 1(中心)为什么不用
cv2.findContours?findContours返回的是轮廓边界,而骨架是中心线,二者语义不同。强行用轮廓会把“口”识别为 1 个外轮廓,丢失内部结构信息。端点/分叉点必须基于骨架像素的邻域关系计算,这是拓扑分析的物理基础。
3.2 骨架长度与归一化曲率:量化笔画“舒展度”与“弯曲度”
单纯计数无法区分“一”和“乙”(都含 1 端点、0 分叉点)。需引入几何特征:
- 骨架长度:用
cv2.arcLength计算骨架轮廓周长(注意:需先将骨架转为轮廓格式); - 归一化曲率:对骨架做 Douglas-Peucker 简化,统计简化后顶点数与原骨架点数比值,比值越小说明曲线越平直。
def get_skeleton_metrics(skel): # 步骤1:将骨架转为轮廓(找所有非零点) points = np.column_stack(np.where(skel > 0)) if len(points) < 3: return 0, 0.0 # 步骤2:拟合多边形(Douglas-Peucker) # 注意:cv2.approxPolyDP 输入需为 shape (n,1,2) 的数组 pts = points.reshape((-1, 1, 2)).astype(np.int32) epsilon = 0.5 # 简化精度,越小保留越多细节 approx = cv2.approxPolyDP(pts, epsilon, closed=False) # 步骤3:计算长度(近似为所有线段长度和) length = 0 for i in range(len(approx)-1): p1, p2 = approx[i][0], approx[i+1][0] length += np.sqrt((p1[0]-p2[0])**2 + (p1[1]-p2[1])**2) # 步骤4:归一化曲率 = 简化后顶点数 / 原骨架点数 curvature = len(approx) / len(points) return int(length), round(curvature, 3) length, curv = get_skeleton_metrics(skeletons[0]) print(f"骨架长度: {length}px, 归一化曲率: {curv}") # “一”:length≈200, curv≈0.05;“乙”:length≈180, curv≈0.35
epsilon参数血泪经验:设为0.5是平衡点。若epsilon=0.1,所有笔画都接近原始点数,curv失去区分度;若epsilon=2.0,“乙”的弯曲部分被简化成直线,“一”和“乙”特征完全重叠。建议在项目文档中附epsilon取值与curv散点图,证明该参数对不同字形的稳定性。
3.3 笔画方向直方图(HOA):捕捉书写习惯的统计特征
不同于 HOG(梯度方向),笔迹方向直方图(HOA)统计骨架点处的切线方向。OpenCV 的cv2.calcHist可直接计算,但需先用 Sobel 算子求梯度:
def get_hoa(skel, bins=16): # 计算 x,y 方向梯度 grad_x = cv2.Sobel(skel, cv2.CV_32F, 1, 0, ksize=3) grad_y = cv2.Sobel(skel, cv2.CV_32F, 0, 1, ksize=3) # 计算每个骨架点的方向角(弧度转角度) angles = np.arctan2(grad_y, grad_x) * 180 / np.pi angles = (angles + 180) % 180 # 映射到 [0,180) 区间(无向线段) # 提取骨架点处的角度值 mask = skel > 0 angle_vals = angles[mask] # 计算直方图 hist, _ = np.histogram(angle_vals, bins=bins, range=(0, 180)) return hist.astype(np.float32) / (np.sum(hist) + 1e-6) # 归一化 hoa = get_hoa(skeletons[0]) print(f"HOA 直方图: {hoa.round(3)}") # 水平笔画多的字(如“三”)在 0°、180° bin 值高;竖笔画多的字(如“十”)在 90° bin 值高为什么用
arctan2(grad_y, grad_x)?这是计算梯度方向的标准方式,grad_y在分子确保 0° 对应水平向右,90° 对应垂直向下,符合书写直觉。若用arctan(grad_y/grad_x),会在grad_x=0时除零,且无法区分象限。
3.4 外接矩形长宽比与填充率:快速排除明显异常字符
这是最廉价的过滤特征,能在毫秒级筛掉 30% 的无效区域(如墨点、装订孔、扫描污渍):
def get_shape_features(x, y, w, h, skel): # 长宽比 aspect_ratio = max(w, h) / min(w, h) if min(w, h) > 0 else 1 # 填充率 = 骨架像素数 / 外接矩形面积 skel_area = np.sum(skel > 0) fill_ratio = skel_area / (w * h) if w * h > 0 else 0 return round(aspect_ratio, 2), round(fill_ratio, 3) ar, fr = get_shape_features(*char_regions[0], skeletons[0]) print(f"长宽比: {ar}, 填充率: {fr}") # “口”:ar≈1.0, fr≈0.2;“1”:ar≈5.0, fr≈0.05;污渍:ar≈1.2, fr≈0.01填充率的物理意义:它反映笔画在 bounding box 中的“密度”。正常汉字填充率在 0.05~0.3 之间;低于 0.02 多为噪点,高于 0.35 可能是涂抹或重叠字。此特征无需任何模型,纯规则即可剔除大量干扰。
3.5 特征向量组装与标准化:为后续分类器准备输入
将上述 5 类特征拼接成一个固定长度向量(本例共 1+1+16+1+1=20 维),并做 Z-score 标准化(非 Min-Max)——因为端点数、长度等量纲差异极大,Z-score 能让 SVM 等算法权重分配更合理:
from sklearn.preprocessing import StandardScaler # 假设 features_list 是所有字符的 [ep, jp, length, curv, hoa..., ar, fr] 列表 all_features = np.vstack(features_list) # shape: (N, 20) # 仅对数值型特征标准化(HOA 已归一化,不参与) scaler = StandardScaler() # 第0、1、2、3、18、19列是数值型(ep,jp,length,curv,ar,fr) numeric_cols = [0,1,2,3,18,19] all_features[:, numeric_cols] = scaler.fit_transform(all_features[:, numeric_cols]) # 保存 scaler 供部署时使用 import joblib joblib.dump(scaler, "feature_scaler.pkl")为什么 HOA 不标准化?它已是概率分布(和为 1),标准化会破坏其统计意义。Z-score 仅作用于有量纲的物理量(如像素长度、计数),这是特征工程的基本原则。
4. 避坑指南:毕业设计中最常踩的 4 个 OpenCV 笔迹识别深坑
这些坑不是网上教程会写的“环境配置错误”,而是你在调试第 3 天凌晨 2 点对着满屏白图抓狂时才意识到的底层陷阱。每一个都曾让至少 3 届学长推倒重写,这里按现象→原因→解决给出可立即执行的方案。
4.1 现象:骨架提取后全是断线,看不出任何笔画形状
原因:输入图像未严格二值化(像素值混杂 0~255 的灰度值),或xip.thinning输入类型错误(传入float32或bool)。
解决:
- 在
thinning前强制转换:skel_input = (roi > 0).astype(np.uint8) * 255; - 用
print(np.unique(roi))检查 ROI 是否只有 0 和 255; - 若仍有断线,降低
adaptiveThreshold的C值(如从 10 改为 5),增强弱笔迹响应。
4.2 现象:连通域分析得到上千个微小区域,char_regions列表为空
原因:形态学闭运算过度(iterations=2或kernel过大),导致原本分离的笔画粘连成巨型连通域,后续面积过滤全部失效。
解决:
- 回退到
iterations=1,kernel=(3,1); - 用
cv2.connectedComponents先看总连通域数:若num_labels > 500,说明噪声未滤净,需加强中值滤波(ksize=5)或增加闭运算前的开运算(MORPH_OPEN)去噪; - 在
stats过滤时打印area分布:print(np.sort([s[4] for s in stats[1:]])),观察是否集中在 1~10 区间——若是,说明blockSize太小,局部阈值窗口无法覆盖笔画宽度。
4.3 现象:端点计数为 0,所有骨架点邻域和都是 2
原因:骨架过于“干净”,Zhang-Suen 算法在笔画极细时(<2px)会将端点也细化掉,导致所有点度数为 2(形成环或直线)。
解决:
- 改用
xip.THINNING_GUOHALL模式,它对细线保留更强; - 或在骨架提取前,对二值图做轻微膨胀(
cv2.dilate(binary, kernel, iterations=1)),再 thinning; - 最根本:检查原始图像分辨率,若扫描 DPI < 200,需先用
cv2.resize放大 2 倍(插值用cv2.INTER_CUBIC),再处理。
4.4 现象:HOA 直方图所有 bin 值均为 0
原因:Sobel 梯度计算时,skel图像为uint8,但cv2.Sobel默认输出int16,负值被截断为 0,导致grad_x/grad_y全 0,arctan2返回 0。
解决:
- Sobel 前显式转换类型:
skel_f32 = skel.astype(np.float32); - 或指定
cv2.Sobel的ddepth=cv2.CV_32F; - 验证:
print(np.min(grad_x), np.max(grad_x)),若输出0 0,即确认此坑。
额外提示:所有坑的根因都指向同一个原则——OpenCV 是 C++ 库的 Python 封装,它不帮你做类型安全检查,所有输入必须精确满足文档要求。养成习惯:每次调用前
print(img.dtype, img.shape, np.unique(img)),比 debug 十分钟更省时间。
5. 从识别到实用:用 SVM 分类器 + 规则后处理打造可交付的笔迹识别模块
做到特征提取,你已完成了 80% 的工作;但答辩老师关心的是“能不能用”。本章不讲理论,只给一套经 3 届毕设验证的、零深度学习依赖、纯 OpenCV + scikit-learn 实现的可交付方案,包含训练、部署、结果可视化全流程,并附上我压箱底的两个实战技巧。
5.1 构建最小可行分类器:SVM 训练与交叉验证
本项目不追求 99% 准确率,而要在 50 个样本(5 类 × 10 个)上达到 85%+ 稳定准确率。SVM 是最佳选择:小样本友好、核函数可调、决策边界清晰(答辩时能画图解释)。我们用 RBF 核,C=1.0,gamma='scale'作为起点:
from sklearn.svm import SVC from sklearn.model_selection import StratifiedKFold from sklearn.metrics import classification_report # 假设 X_train (n_samples, 20), y_train (n_samples,) # 使用分层 K 折(保证每折各类样本比例一致) cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) svm = SVC(kernel='rbf', C=1.0, gamma='scale', random_state=42) # 交叉验证得分 scores = [] for train_idx, val_idx in cv.split(X_train, y_train): svm.fit(X_train[train_idx], y_train[train_idx]) score = svm.score(X_train[val_idx], y_train[val_idx]) scores.append(score) print(f"SVM 5折CV准确率: {np.mean(scores):.3f} ± {np.std(scores):.3f}") # 最终训练(用全部训练集) svm.fit(X_train, y_train) joblib.dump(svm, "笔迹_svm_model.pkl")为什么不用随机森林?RF 在小样本上易过拟合,且特征重要性排序对笔迹识别无直观物理意义(“端点数重要性=0.32”不如“SVM 支持向量落在端点-长度二维平面”好解释)。SVM 的支持向量可可视化,是答辩加分项。
5.2 规则后处理:用 OpenCV 实现“笔迹合理性校验”
模型会出错,但人可以加规则兜底。以下三个规则用 OpenCV 原生函数 10 行内实现,却能拦截 40% 的典型错误:
| 错误类型 | OpenCV 检测逻辑 | 代码片段 |
|---|---|---|
| 单字多识别(如“口”被分成“丨”和“一”) | 检查同一行内相邻字符 bounding box 的 x 距离:< 字符平均宽度 × 0.8 → 合并 | if x2 - (x1+w1) < avg_width * 0.8: |
| 笔画断裂误判为多字(如“丰”被识为“三+一”) | 计算相邻字符骨架的 Hausdorff 距离:< 15px → 合并 | dist = cv2.matchShapes(skel1, skel2, cv2.CONTOURS_MATCH_I1, 0) |
| 墨水洇染导致填充率畸高 | fill_ratio > 0.4且aspect_ratio < 2.0→ 标记为“疑似涂抹”,降权或人工复核 | if fr > 0.4 and ar < 2.0: confidence *= 0.5 |
这些规则不需训练,直接硬编码进识别流程,是体现工程思维的关键。
5.3 结果可视化与导出:让答辩老师一眼看懂你的系统
毕业设计不是跑通代码,而是展示价值。以下函数生成一张带原图、二值图、骨架、识别结果、置信度的综合报告图,直接用于答辩 PPT:
def generate_result_report(original_img, binary, skeletons, predictions, confidences): h, w = original_img.shape[:2] # 创建 2x2 拼图 report = np.zeros((h*2, w*2), dtype=np.uint8) # 左上:原图 report[0:h, 0:w] = cv2.cvtColor(original_img, cv2.COLOR_GRAY2BGR) if len(original_img.shape)==2 else original_img # 右上:二值图(绿色框出字符) binary_bgr = cv2.cvtColor(binary, cv2.COLOR_GRAY2BGR) for i, (x,y,w_,h_) in enumerate(char_regions): cv2.rectangle(binary_bgr, (x,y), (x+w_,y+h_), (0,255,0), 2) cv2.putText(binary_bgr, f"{predictions[i]}({confidences[i]:.2f})", (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0,255,0), 1) report[0:h, w:w*2] = binary_bgr # 左下:骨架叠加原图 skel_overlay = cv2.cvtColor(original_img, cv2.COLOR_GRAY2BGR) for i, skel in enumerate(skeletons): x,y,w_,h_ = char_regions[i] # 将骨架转为彩色并叠加 skel_color = np.zeros((h_, w_, 3), dtype=np.uint8) skel_color[:,:,1] = skel # 绿色通道显示骨架 skel_overlay[y:y+h_, x:x+w_] = cv2.addWeighted(skel_overlay[y:y+h_, x:x+w_], 0.7, skel_color, 0.3, 0) report[h:h*2, 0:w] = skel_overlay # 右下:识别结果表格(用 PIL 绘制,此处略) # ... 生成文本表格图像 ... return report # 调用 report_img = generate_result_report(img, binary, skeletons, preds, confs) cv2.imwrite("result_report.png", report_img)答辩技巧:把
result_report.png放 PPT 第一页,老师进来第一眼看到的就是“输入→处理→输出”的完整证据链。比讲 10 分钟原理更有说服力。
5.4 我的两个血泪经验:让毕设从“及格”到“优秀”的临门一脚
第一个经验是永远保留原始扫描件的 DPI 信息。我在第三版文档里才意识到:所有尺寸阈值(blockSize、area、epsilon)都依赖 DPI。于是我在项目开头加了一行:
# 在 main.py 顶部声明 SCANNED_DPI = 300 # 必须与实际扫描设置一致! # 后续所有尺寸参数按比例缩放,如: ADAPTIVE_BLOCKSIZE = int(41 * (300 / SCANNED_DPI))这样换一台扫描仪只需改一个数,而不是重调全部参数。导师看到这行注释,当场说“这个细节很工程”。
第二个经验是把“失败案例”做成文档的亮点章节。我专门建了一个failure_analysis/文件夹,收集了 12 张识别失败的图,每张配:
- 失败原因(如“纸张反光导致局部阈值失效”);
- OpenCV 解决方案(如“在此区域手动应用
cv2.createCLAHE增强对比度”); - 修复后效果对比图。 答辩时老师翻到这页,笑着说:“你比很多研究生想得都细。”
希望帮到你。
本文还有配套的精品资源,点击获取