OpenCV笔迹识别系统全解析:从图像预处理到特征比对

发布时间:2026/10/26 15:31:31
OpenCV笔迹识别系统全解析:从图像预处理到特征比对

简介:这是面向毕业设计和图像识别入门者的笔迹识别系统完整项目,解决了刑事案件笔迹比对与银行票据签名真伪鉴别中长期依赖人工、耗时费力的问题。系统以OpenCV为核心完成图像识别与文字相似度判断,逐字拆分、重点比对相似区域后输出相似度结果;界面采用pyQt构建,操作提示简洁,后台使用matplotlib分步可视化处理过程,最终只向用户呈现识别结论,兼顾易用性与直观性。资源以zip压缩包形式提供,大小38.21MB,平台显示文件总数为0且文件类型明细暂缺,故不逐一罗列内部清单;目前已有200人学习。下载后可获得可编译运行的完整工程、模块化识别流程说明、界面与算法设计思路,适合作为毕业设计参考、课设拓展或OpenCV图像识别实践的学习样板。

1. 笔迹识别系统到底在做什么:先想清楚这个毕设的边界

看到“基于opencv图像识别的笔迹识别系统”这个标题,很多人第一反应是:把两张手写图片丢进 OpenCV,算个相似度,大于阈值就是同一个人。实际跑过一遍源码就会发现,最花时间的根本不是“图像识别”这一步,而是怎么把一笔一画的书写习惯转成稳定的特征向量。这套系统的正确定位是:用传统计算机视觉方法,对手写样本做预处理、特征提取和距离比对,输出“这份笔迹是否属于某人”的判断,同时留下一套能改参数、能复现结果的源码和文档。

它适合两类人:一类是正在选毕业设计题目、需要快速落地又能讲清原理的本科生;另一类是刚接触 OpenCV 图像处理、想拿一个完整项目练习预处理和特征工程的人。看懂这个项目,你得到的不是一个黑盒模型,而是一条从图像到决策的完整链路。

2. 从图像到干净的字迹:预处理管线怎么把原图变成二值图

笔迹识别比的是写法习惯,不是像素颜色。一张直接用手机拍的手写照片,往往带着背景阴影、纸张纹理、倾斜角度和光照不均,这些干扰会全部混进后续特征。我一般会把预处理分成三步走:灰度化与二值化、去噪与倾斜校正、统一尺寸。这套管线的目标只有一个:把任意来源的原图变成“只有字迹、没有杂物”的二值图,后面的特征提取才有意义。

2.1 灰度化与二值化:为什么不能跳过中间步骤

OpenCV 读进来的图片默认是 BGR 三通道。如果直接在这个彩色图上提取特征,背景纸张颜色、阴影渐变都会被当成有效信息。笔迹的本质是墨迹在纸上留下的二值分布,所以第一步永远是降维成灰度图,再通过阈值把“墨迹”和“背景”彻底分开。

常见做法是用cvtColor转灰度,然后做二值化。这里有个坑:固定阈值threshold只适合光照均匀的扫描件,手写照片经常一边亮一边暗,固定阈值会把暗部的字迹吃掉。我会用adaptiveThreshold,它按局部区域自动算阈值,能救回大部分阴影下的笔画:

import cv2 import numpy as np def preprocess_image(image_path): img = cv2.imread(image_path) if img is None: raise ValueError(f"cannot read image: {image_path}") gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) binary = cv2.adaptiveThreshold( gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 35, 15 ) return binary

这段代码里ADAPTIVE_THRESH_GAUSSIAN_C表示用局部高斯加权均值做阈值基准,blockSize=35是局部邻域边长,必须是奇数,值越大对光照变化越不敏感,但也会丢掉细节;C=15是基准值减去的一个偏移,C 越大,被判定为前景的要求越严格。THRESH_BINARY_INV之所以取反,是因为笔迹是深色,取反后墨迹变成 255、背景变成 0,后面统计前景像素比例时更直观。

这里最值得花时间的是 blockSize 和 C 这一组参数。实际项目里,我会先挑一张光照最差的样本,把 blockSize 从 15 试到 51,C 从 5 试到 25,每换一组就存一张图肉眼看一遍。二值化效果直接决定后面所有指标的稳定性,这一步偷懒,后面识别率波动会全算到算法头上。

2.2 去噪与倾斜校正:让同一个人两次写的字站在同一坐标系

二值图里仍然会有两类残留:扫描噪点和纸张纤维产生的小颗粒,以及文字整体倾斜。笔画里如果混进孤立噪点,网格特征里的前景比例会被抬高;倾斜角度不一致,同一批样本的横竖投影特征会错位。处理顺序通常是先去噪、再校正倾斜,因为先校斜再找轮廓会有更多干扰。

去噪我常用两步:先用 3×3 的矩形结构元做一次开运算(OpenCV 里是morphologyEx),把细小的孤立点吃掉;再用findContours找到所有连通域,面积小于阈值的小块直接丢弃。倾斜校正则用最小外接矩形计算整体倾角:

def denoise_and_deskew(binary): kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) opened = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel, iterations=1) contours, _ = cv2.findContours(opened, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) cleaned = np.zeros_like(opened) for cnt in contours: if cv2.contourArea(cnt) > 20: cv2.drawContours(cleaned, [cnt], -1, 255, -1) coords = cv2.findNonZero(cleaned) if coords is None: return cleaned rect = cv2.minAreaRect(coords) angle = rect[-1] if angle < -45: angle = -(90 + angle) else: angle = -angle h, w = cleaned.shape M = cv2.getRotationMatrix2D((w // 2, h // 2), angle, 1.0) deskewed = cv2.warpAffine(cleaned, M, (w, h), flags=cv2.INTER_CUBIC, borderValue=0) return deskewed

这段有几个细节值得说。RETR_EXTERNAL只提取最外层轮廓,对文字这种彼此不相连的笔画足够用,而且能避免内部孔洞带来的额外轮廓。minAreaRect返回的 angle 范围是[-90, 0),这个取值在横排文字和竖排文字上会差 90 度,所以要加一层判断把它归一化到[-45, 45),否则横写、竖写样本旋转方向不一致。borderValue=0表示旋转后空白区域补黑,避免补白后在二值图里变成新的前景。

开运算的核大小,我一般先用 (3, 3),如果噪点还是一大片,就换成 (5, 5)。注意核太大容易把笔画末端磨掉,最后识别率反而下降。倾斜校正不是万能的,个别样本如果纸面本身折角严重,透视变形并不能靠旋转解决,这种样本要么重拍,要么从数据集里剔除。

2.3 批量处理脚本:把一套预处理参数跑完整个样本集

毕设里通常有几十上百张样本,手动一张张跑不现实。常见的做法是写一个批量脚本,遍历目录里的所有图片,执行上面两个函数后写到新的文件夹。关键是要保留子目录结构,因为训练集和测试集一般是分文件夹管理的。

import os from glob import glob input_dir = "data/raw" output_dir = "data/processed" for path in glob(os.path.join(input_dir, "**", "*.jpg"), recursive=True): rel_path = os.path.relpath(path, input_dir) out_path = os.path.join(output_dir, rel_path) os.makedirs(os.path.dirname(out_path), exist_ok=True) try: binary = preprocess_image(path) deskewed = denoise_and_deskew(binary) cv2.imwrite(out_path, deskewed) print(f"done: {rel_path}") except Exception as exc: print(f"failed: {rel_path}, reason={exc}")

这个脚本用recursive=True匹配子目录,os.path.relpath保留相对路径,输出目录和输入目录一一对应。批量处理后一定要人工抽查至少 20% 的样本,特别是倾斜校正那一步,偶尔会碰到整行字被裁掉边缘的情况。

预处理参数一旦定下来,就不要在样本之间来回改。我见过不止一次:因为某张图二值化效果差就单独调参数,结果同一批样本特征口径不一致,后面比对全部漂移。正确做法是固定一套参数,处理完所有样本,再根据整体效果决定要不要调,而不是边跑边改。

注意:预处理参数一旦确定,请在项目文档里写明具体值,答辩时会用得到。

3. 特征提取与比对:笔迹识别最核心的“怎么比”

预处理只能让图像干净,真正决定“识别”效果的是特征。在基于 OpenCV 的 Python 图像识别方案里,笔迹识别和常见的深度学习分类不同,核心是把一张字迹图压缩成一个固定长度的向量,再用向量距离判定相似度。特征选得对不对,直接决定系统是“科学识别”还是“像素玄学”。

3.1 统计特征:网格特征与投影特征

最常用、也最容易讲清原理的是网格特征。把二值图平均切成 N×N 的小格子,每个格子里统计墨迹像素占比。这个特征能反映字迹在空间上的分布习惯,比如字体大小、疏密、上下左右留白。配合笔迹识别场景,网格特征对“同一个人写的字整体布局稳定”这个假设非常敏感。

def grid_feature(binary, grid_size=(8, 8)): h, w = binary.shape gh, gw = grid_size cell_h, cell_w = h // gh, w // gw features = [] for i in range(gh): for j in range(gw): cell = binary[i * cell_h:(i + 1) * cell_h, j * cell_w:(j + 1) * cell_w] ratio = cv2.countNonZero(cell) / (cell_h * cell_w) features.append(ratio) return np.array(features, dtype=np.float32)

这里的countNonZero统计每个格子里值为 255 的像素数,除以格子面积得到占比,这样不同分辨率、不同书写大小的样本在特征尺度上统一了。网格 (8, 8) 是起步值,网格越大特征越细致、但对位置偏移越敏感;网格越小越鲁棒、但区分度越低。毕设场景里建议从 (8, 8) 起步,先看结果再考虑换成 (16, 16)。

另一个统计特征是投影特征。把二值图分别按行和列做平均,得到两个一维向量,一个描述横向笔画密度分布,一个描述纵向。它和网格特征互补:网格描述局部密度,投影描述整体轮廓走势。

def projection_feature(binary): h, w = binary.shape vertical = cv2.reduce(binary, 0, cv2.REDUCE_AVG).flatten() horizontal = cv2.reduce(binary, 1, cv2.REDUCE_AVG).flatten() return np.concatenate([vertical, horizontal])

cv2.reduce的第二个参数 0 表示把整个图像压缩到一行,1 表示压缩到一列,REDUCE_AVG返回平均灰度,因为二值图的像素只有 0 和 255,平均值就等价于该行或列上墨迹的密度。投影特征向量长度是 w+h,比网格特征长得多,使用时一般会先降采样或者归一化,避免它主导距离计算。

3.2 结构特征:轮廓与笔画方向

网格和投影都是统计特征,它们有个弱点:对同样的墨迹量,字写歪一点或笔画粗细变化,数值就会跳动。所以在做笔迹识别时还要补结构特征。最常见的是 Hu 矩和笔画方向直方图。

Hu 矩是 OpenCV 自带的形状描述子,它用归一化中心矩构造了 7 个对平移、缩放、旋转不敏感的数值。对笔迹来说,字形轮廓的整体形状往往有个人特色,Hu 矩正好把这个“形”压成一组小向量。

def hu_moments_feature(binary): moments = cv2.moments(binary) hu = cv2.HuMoments(moments).flatten() return -np.sign(hu) * np.log10(np.abs(hu) + 1e-10)

注意 Hu 矩的数值范围跨度非常大,直接送进距离计算会被第一维主导,所以用-sign * log10做一次对数压缩,让 7 个维度处在相近量级。1e-10是防零保护,如果某个矩正好是 0,对数不会变成负无穷。

笔画方向直方图是另一个更贴近“笔迹”理解的特征。用 Sobel 算子算出每个像素的梯度方向和幅值,只保留幅值足够大的点,统计它们的角度分布。字的写法不同,笔画的走向偏好就会不同,比如有人横平竖直,有人撇长捺短,这个直方图能把这类习惯量化出来。

def stroke_direction_histogram(binary, bins=8): sobelx = cv2.Sobel(binary, cv2.CV_32F, 1, 0, ksize=3) sobely = cv2.Sobel(binary, cv2.CV_32F, 0, 1, ksize=3) magnitude = cv2.magnitude(sobelx, sobely) angle = cv2.phase(sobelx, sobely, angleInDegrees=True) mask = magnitude > 10 hist, _ = np.histogram(angle[mask], bins=bins, range=(0, 180)) hist = hist.astype(np.float32) total = hist.sum() if total > 0: hist /= total return hist

这里magnitude > 10是经验阈值,过滤掉二值图里那些由毛刺引起的弱梯度。range=(0, 180)把方向分成 8 个区间,最后除以总数做归一化,让这个特征只看方向分布、不看字迹总量。

3.3 比对与分类:欧氏距离、余弦相似度与KNN

特征提取之后,系统就变成了一堆向量。怎么判断属于同一个人?常见做法是算距离或者相似度。欧氏距离直观,适合网格特征这种“密度差值”语义;余弦相似度更适合笔画方向这种角度分布特征,它对向量长度不敏感。毕设源码里通常两个都有,开一个开关就能切换。

def euclidean_distance(f1, f2): return np.sqrt(np.sum((f1 - f2) ** 2)) def cosine_similarity(f1, f2): dot = np.dot(f1, f2) norm1, norm2 = np.linalg.norm(f1), np.linalg.norm(f2) if norm1 == 0 or norm2 == 0: return 0.0 return dot / (norm1 * norm2) def knn_predict(train_features, train_labels, test_feature, k=3): scored = sorted( ((euclidean_distance(test_feature, f), label) for f, label in zip(train_features, train_labels)), key=lambda x: x[0] ) votes = {} for _, label in scored[:k]: votes[label] = votes.get(label, 0) + 1 return max(votes.items(), key=lambda x: x[1])[0]

KNN 在这里承担分类器角色。k 是参与投票的近邻数,候选者每人有 3 个训练样本时,k=3 是常用值;样本更多时可以适当调大。距离函数切换会影响结果:网格特征用欧氏距离更稳,如果特征拼接了方向直方图,建议单独对这段特征用余弦。最稳的做法是先归一化所有特征再算距离,不然量级大的特征会把小的压没。

这里还有一个容易被忽略的问题:特征不是越多越好。把网格、投影、Hu 矩、方向直方图全拼成一个超长向量,维度变高之后距离会被稀释,而且有的维度互相冗余。我的习惯是先做单特征验证,看哪个特征区分度最好,再决定融合方式,而不是一开始就全拼。

4. 源码结构与项目文档:拿到毕设工程后先看哪里

标题里“源码+详细项目文档”这七个字,对准备交毕设的人来说才是真正的交付物。算法能跑通只是第一步,老师评审时看的是结构清不清楚、文档能不能支撑答辩。这一章按我惯用的工程组织方式,把源码和文档的骨架拆开讲。

4.1 源码模块划分:预处理、特征、比对、界面四层

常见做法是把代码分成四个模块,各管一段:预处理管图像,特征管向量,比对管距离和分类,main 管流程和交互。这样不只在答辩时好解释,更重要的是当识别率不对劲时,能快速定位是预处理的问题还是特征的问题。

handwriting_recognition/ ├── config.py # 全局参数 ├── preprocessing.py # 二值化、去噪、倾斜校正 ├── feature_extraction.py # 网格特征、投影特征、结构特征 ├── classifier.py # 距离计算、KNN ├── main.py # 训练/识别主流程 ├── requirements.txt ├── README.md ├── data/ │ ├── raw/ # 原始采集样本 │ ├── processed/ # 预处理后样本 │ └── features.npz # 导出的特征向量 └── docs/ └── 项目文档.md

main.py 的工作很简单:读配置,加载训练集,提取特征,然后对测试图走同一套预处理,最后输出预测标签。这样拆开之后,每个文件都能单独测试,也方便在论文里画模块图。feature_extraction.py里的extract_feature是组合入口,内部按顺序调用网格、投影、Hu 矩等函数。

import os from config import K_NEIGHBORS, TRAIN_DIR, TEST_IMAGE from preprocessing import preprocess_image, denoise_and_deskew from feature_extraction import extract_feature from classifier import knn_predict def load_train_set(train_dir): features, labels = [], [] for person_name in sorted(os.listdir(train_dir)): person_dir = os.path.join(train_dir, person_name) for img_name in sorted(os.listdir(person_dir)): img_path = os.path.join(person_dir, img_name) binary = preprocess_image(img_path) deskewed = denoise_and_deskew(binary) features.append(extract_feature(deskewed)) labels.append(person_name) return features, labels def main(): train_features, train_labels = load_train_set(TRAIN_DIR) test_binary = preprocess_image(TEST_IMAGE) test_deskewed = denoise_and_deskew(test_binary) test_feature = extract_feature(test_deskewed) result = knn_predict(train_features, train_labels, test_feature, K_NEIGHBORS) print(f"predicted writer: {result}")

这段逻辑里最需要注意的是:测试图片必须走完全相同的预处理链。很多人训练集过了一遍denoise_and_deskew,测试时图省事只做二值化,特征空间就错位了。load_train_set里直接按文件夹名作为标签,所以样本目录命名要规范,比如person_01、test_01。

4.2 配置与路径组织:让代码换一台机器还能跑

毕设源码最容易翻车的地方不是算法,是路径和依赖。我见过不少工程用绝对路径写死,比如C:/Users/xxx/Desktop/data,换台电脑就崩。正常交付的源码应该用相对路径,从项目根目录出发,并且把一改就影响结果的关键参数集中放到config.py。

# config.py import os BASE_DIR = os.path.dirname(os.path.abspath(__file__)) DATA_DIR = os.path.join(BASE_DIR, "data") RAW_DIR = os.path.join(DATA_DIR, "raw") PROCESSED_DIR = os.path.join(DATA_DIR, "processed") TRAIN_DIR = os.path.join(PROCESSED_DIR, "train") TEST_DIR = os.path.join(PROCESSED_DIR, "test") BLOCK_SIZE = 35 THRESH_C = 15 GRID_SIZE = (8, 8) MIN_AREA = 20 K_NEIGHBORS = 3

用os.path.dirname(os.path.abspath(__file__))拿到 config.py 所在目录,再往上拼路径,这样整个项目移动到哪里都能跑。requirements.txt 里列运行时依赖,常见就是opencv-python和numpy,环境装好之后执行pip install -r requirements.txt。

opencv-python numpy

注意:OpenCV 4.x 中cv2.findContours返回(contours, hierarchy)两个值,网上很多老教程按三个值接收,代码会直接报ValueError: not enough values to unpack。如果你的代码是从老教程抄的,改成两值接收即可。

另外,config.py里不要放中文路径和带空格的目录名,跨平台时容易出编码问题。依赖锁定版本也是值得做的事,但不用追求最新,重要的是保证环境一致。

4.3 项目文档怎么写:从算法原理到实验结果的自洽叙述

“详细项目文档”不是把代码逐行贴进去,而是要把“为什么这样做”说清楚。我一般建议按这个顺序写:首先写项目目标和数据来源;然后写算法选型——为什么用传统 OpenCV 图像处理而不是深度学习,这个在毕设答辩里是必问题;最后写实验结果,附上样本量、识别率、失败案例。

# 项目文档结构 ## 1. 项目背景与目标 ## 2. 数据采集与预处理 - 样本格式、数量 - 预处理步骤与参数 ## 3. 特征提取方法 - 网格特征、投影特征、Hu矩、笔画方向直方图 ## 4. 分类与识别流程 - 距离度量、KNN、阈值判断 ## 5. 实验与分析 - 测试方案(留一法/划分测试集) - 识别率、混淆矩阵 - 失败样本分析 ## 6. 项目运行说明 - 环境、依赖、命令行

写文档时最容易犯的错是把实验结果吹得太满。笔迹识别在毕设规模下能做到 85% 以上已经不错,关键是把样本量和测试方式写清楚,再补一段失败案例分析。答辩老师最看重的不是“百分百识别成功”,而是你能不能解释清楚一个错误结果到底错在哪一环——这也正是“详细项目文档”存在的意义。

5. 避坑/常见问题/排查:笔迹识别系统开发到答辩的高频翻车点

把这部分单独列出来,是因为这类毕设项目的大部分时间最后都耗在几个重复出现的坑上。下面这几条基本都是新手的血泪经验,按“现象 → 原因 → 解决”写,可以直接对照排查。

5.1 现象:import cv2 直接报 ModuleNotFoundError

原因:装的是 Python 3.11+,却用了系统级的 pip,或者是在没有激活的虚拟环境里直接安装。解决:先确认解释器和 pip 一致,再执行安装。最稳的做法是新建虚拟环境,然后执行pip install opencv-python numpy,装完在 python 交互式环境里import cv2; print(cv2.__version__)验证。不要在没确认 pip 属于哪个 Python 时就反复重装,越装越乱。

我自己的习惯是:项目目录下python -m venv venv,再用venv/bin/pip install,这样不管机器上有多少个 Python,都能锁住环境。这一步搞定之后,剩下的pip install -r requirements.txt才会可靠。

5.2 现象:识别率只有三成,原因在样本而不在算法

原因:新手最容易先怀疑特征算法,实际上绝大多数是训练样本太少或同一人书写不稳定。每人如果只有 3 张样本,KNN 的 k 值一调就没了;字迹潦草、字体时大时小,特征向量自然发散。解决:先扩大样本量。常见做法是每人采集 10~20 张规范文本的书写样本,内容统一、纸面干净;如果条件允许,分不同时间采集两次,增加“自然书写波动”。样本整理干净后,很多“算法问题”会自动消失。

写进项目文档时也要如实记录样本量,只拿 3 张样本跑出 90% 识别率,在答辩时是会被质疑的。

5.3 现象:同一人重新写的字被识别成不同人

原因:同一个人两次书写时,字的位置、大小、倾斜不完全一致。网格特征对位置偏移极其敏感,一个字在格子边缘挪动一格,十几个维度的值就同时变化,把欧氏距离直接拉大。解决:在预处理阶段做对齐和缩放,而不要指望分类器“学习”这种几何波动。常见做法是找到整张图的外接矩形,缩放到固定尺寸,比如 200×100,让特征提取面对的是标准画布。

另外,采集时要求书写者尽量写在统一大小的格纸里,也是成本最低的稳定手段。不要小看这一步,样本规范程度对识别率的影响往往比换算法大得多。

5.4 现象:答辩现场摄像头取图和测试集效果完全不一样

原因:测试集用的是扫描件或统一拍照,答辩现场变成了摄像头实时取图,光照、透视、分辨率全变了。预处理参数是按测试集调的,摄像头画面里的纸张发灰、反光亮斑直接被当成了前景。解决:准备一条固定演示链路。常见做法是提前用同一摄像头录好几段样本视频,把帧存成图片后走同一套预处理;现场演示时也尽量用同样的纸张、同样的摆放位置,必要时加一个台灯固定光照。

如果系统里有“拍照识别”按钮,务必把拍照图像也接入完整的preprocess_image + denoise_and_deskew + extract_feature管线,不要新建一条简化的处理路径,否则就是给自己挖坑。

5.5 现象:灰度图直接算相似度,结果离谱得像玄学

原因:跳过二值化和去噪,直接对灰度图做欧氏距离,背景纸张、噪声、光照渐变全部参与计算,笔画本身的差异反而被稀释,结果自然没有规律。解决:回到预处理管线,先把图像变成二值图再提取特征。这里需要强调,灰度图并不是不能用,但必须先把特征设计成对灰度不敏感的形态,对毕设场景来说没有这个必要,二值化是最稳的路径。

这条坑我见到过很多次,往往是为了“少写点代码”而跳步,最后调试时间反而成倍增加。预处理三件套(二值化、去噪、对齐)是一套捆在一起的流程,任何一步省略,后面的特征就是模糊的。

把这些坑串起来看,有一条共同主线:几乎每个问题都源于训练链路和测试链路不一致。只要你坚持让所有图片走同一套预处理和特征提取,大约一半的问题会自己消失。动手排查时,我会先在控制台打印每一步的中间结果,比如二值化的非零像素比例、特征向量的均值方差,一旦发现某一步数值异常,就能把问题框定到那一环。

6. 进阶:把识别率再往上提一截的三种验证与融合技巧

当基础链路能跑通、识别率还差口气的时候,我一般会依次试三件事:留一法评估、特征融合、相似度阈值校准。这三件事不改变算法框架,但能让你在项目文档里写出更有说服力的实验结果。

先做留一法。样本量不大的笔迹识别数据集,不值得划分固定测试集。留一法每次拿一个样本当测试,其余全部训练,最后统计正确率,能最大化利用样本,也更符合答辩时“你说结果可信”的预期。

def leave_one_out(features, labels, k=3): correct = 0 for i in range(len(features)): train_f = features[:i] + features[i + 1:] train_l = labels[:i] + labels[i + 1:] pred = knn_predict(train_f, train_l, features[i], k) if pred == labels[i]: correct += 1 return correct / len(features)

第二是特征融合。我习惯先用单特征做一遍留一法,记住每个特征的正确率,再按效果加权拼接。比如网格特征 80%、方向直方图 70%,拼接时可以给网格权重 0.6、方向权重 0.4。简单加权比直接拼接更抗噪声,也更容易在文档里解释。

第三是阈值校准。KNN 只给出标签,不给置信度;但在答辩演示时,最好能同时输出“相似度 0.87,识别为甲”。此时可以取测试样本到同类近邻的平均余弦相似度,设一个怀疑阈值,比如低于 0.55 就输出“无法确认”。这比硬报一个错误标签体面得多。

我现在做这个方向时,会先把样本整理规范、把留一法跑通,再考虑换更复杂的算法,因为绝大多数翻车都发生在数据这一层,而不是模型。希望帮到你。

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