
简介一份面向计算机视觉课程设计与期末大作业的完整掌纹识别项目基于Python实现规划了从图像预处理、特征提取、滤波器PCA降维、CNN分类、分类器融合到结果评估的完整实验链路适合需要快速搭建可运行项目的在读学生。项目整体已获导师指导并评得97分结构和实验设计有较强参考价值。压缩包共17个文件包含11个交互式笔记、5个Python脚本和1个版本忽略配置整体仅189KB交互式笔记按步骤演示了图像预处理、PCA、CNN分类、预训练模型对比、海明距离等关键实验Python脚本则负责多进程加速、时间对比及独立分类函数。目前已有401人学习解压即可运行无需修改既可作为高分模板快速复用也能帮助理解掌纹识别从数据到结论的全过程。1. 掌纹识别大作业从“能跑”到“高分”的差距在哪每年到机器学习课程设计季总有人在群里问“掌纹识别项目怎么做”而真正拉开分数差距的从来不是模型有多新而是你能不能把一条完整链路跑通图像采集、ROI裁剪、特征提取、匹配识别、界面展示。 palmprint recognition 这个方向在生物特征识别里属于“比人脸简单、比指纹有区分度”的中间地带——不需要教会模型认复杂表情只需要抓住掌纹的主线和纹理走向特别适合作为机器学习大作业来验证特征工程和分类器设计的整套方法论。这套 Python 实现的掌纹识别项目核心思路是离线建立掌纹样本库在线完成“放上手、拍一张、识别出是谁”。它不像人脸识别那样对姿态和光照极度敏感也不像指纹识别那样依赖高价采集硬件用普通摄像头就能完成数据采集。对写大作业的同学来说这既是加分项也是坑设备自由度大意味着预处理必须更扎实否则光照和旋转噪声会直接把识别率打到及格线以下。接下来我会按实际开发顺序把整个项目的落地路径拆开讲清楚。2. 掌纹图像采集与 ROI 提取先把手掌从背景里干净地切出来2.1 为什么 ROI 提取是掌纹识别的第一个分水岭掌纹识别不是把整张照片扔进模型就完事。原始图像里有大量干扰信息背景、手指边缘、手背纹理、光线阴影。真正稳定携带身份信息的区域是掌心中央那一块也就是 ROIRegion of Interest。很多初版项目识别率低不是分类器不行而是 ROI 切得歪歪扭扭导致同一只手在不同时刻采集的图像特征区域对不齐。我一般会把 ROI 提取拆成四个步骤图像二值化、找手指谷点、算几何中心、固定尺寸裁剪。二值化的目的是把肤色区域和背景分开谷点检测是在手掌轮廓上找到食指和中指之间的凹陷、无名指和小指之间的凹陷这两个点能确定手掌的旋转角度几何中心则用来确定裁剪框的位置。这套方法不依赖任何第三方库的特殊算法OpenCV 自带函数就能完成。import cv2 import numpy as np def extract_roi(image_path, roi_size(128, 128)): # 读取图像并转为灰度 img cv2.imread(image_path) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 大津法二值化自动计算阈值适应不同光照 _, binary cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) # 找到最大连通域排除背景噪点干扰 contours, _ cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) hand_contour max(contours, keycv2.contourArea) # 计算轮廓凸包用于后续谷点检测 hull cv2.convexHull(hand_contour, returnPointsFalse) defects cv2.convexityDefects(hand_contour, hull) return gray, hand_contour, defects这段代码的关键在cv2.convexityDefects它返回的是轮廓上的凹陷点信息。掌心轮廓的凹陷点正好对应手指根部我们后面要的谷点就是从这些缺陷点里筛出来的。二值化用THRESH_OTSU而不是固定阈值是因为不同人肤色深浅、环境亮度都不一样固定阈值在换人测试时非常容易翻车。拿到缺陷点之后不能全盘接收得按深度排序过滤。缺陷点的第三个字段是凹陷深度只有深度超过一定阈值的才算真正的指谷否则边缘的小锯齿也会混进来。筛选之后取距离最远的两个谷点计算连线角度把图像旋转到水平位置——这一步做不好同一个人的掌纹会因为手掌摆放角度不同而被误判成两个人。def get_valley_points(defects, contour, min_depth30): valley_points [] for i in range(defects.shape[0]): s, e, f, d defects[i, 0] # d 是凹陷深度以像素为单位 if d min_depth: far_point tuple(contour[f][0]) valley_points.append(far_point) return valley_pointsmin_depth这个参数值得花时间调。设太小手指之间正常的弧度会被误判为谷点设太大指谷检测不到ROI 会偏移。以 640x480 的输入图像为例我通常从 30 开始尝试观察检测点是否稳定落在食指中指之间和无名指小指之间。这个参数和采集图像分辨率强相关换了摄像头或改了输入尺寸必须重新标定。2.2 旋转校正与 ROI 裁剪对不齐后面全白做找到两个谷点之后计算它们连线的角度然后反向旋转图像。这里有个细节两个谷点谁在左谁在右不重要角度是绝对的旋转后手掌方向就统一了。有的实现会额外用指尖方向做一次翻转校验但对大作业来说两个谷点对齐已经足够稳定。def rotate_and_crop(gray, valley_points, roi_size(128, 128)): # 按 x 坐标排序谷点保证角度计算方向一致 valley_points sorted(valley_points, keylambda p: p[0]) p1, p2 valley_points[0], valley_points[1] # 计算旋转角度将两谷点连线矫正到水平 angle np.degrees(np.arctan2(p2[1] - p1[1], p2[0] - p1[0])) rows, cols gray.shape matrix cv2.getRotationMatrix2D((cols / 2, rows / 2), angle, 1) rotated cv2.warpAffine(gray, matrix, (cols, rows)) # 重新检测旋转后的谷点计算掌心中心 _, binary_rot cv2.threshold(rotated, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) contours_rot, _ cv2.findContours(binary_rot, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) hull_rot cv2.convexHull(max(contours_rot, keycv2.contourArea), returnPointsFalse) defects_rot cv2.convexityDefects(max(contours_rot, keycv2.contourArea), hull_rot) valley_points_rot get_valley_points(defects_rot, max(contours_rot, keycv2.contourArea)) valley_points_rot sorted(valley_points_rot, keylambda p: p[0]) center_x int((valley_points_rot[0][0] valley_points_rot[1][0]) / 2) # 掌心中心在谷点连线下方偏移量取决于图像尺度 center_y int((valley_points_rot[0][1] valley_points_rot[1][1]) / 2) int(rows * 0.15) # 以掌心为中心裁剪固定尺寸 ROI half_w roi_size[0] // 2 half_h roi_size[1] // 2 roi rotated[center_y - half_h:center_y half_h, center_x - half_w:center_x half_w] return roi旋转后必须重新检测谷点而不是直接使用之前的坐标原因很简单图像旋转后所有像素点都移动了旧坐标不再有效。center_y的偏移量rows * 0.15是经验值手掌的中指根到掌心中心的距离大约是图像高度的 15% 左右。这个比例在不同人种、不同年龄段会有微小波动但对单一大作业数据集来说足够稳定。ROI 尺寸选 128x128 是我个人推荐的平衡点。太小会丢掉掌纹的细纹理信息太大则特征维度爆炸后续 LBP 或 Gabor 特征计算会明显变慢。128x128 在特征维度和计算速度之间处于舒服的位置而且图像缩放所需的插值误差也在可接受范围内。3. 掌纹特征提取LBP 纹理特征与 Gabor 滤波的取舍3.1 LBP 特征为什么适合掌纹纹理描述掌纹和指纹一样核心信息是纹理走向和密度分布。LBPLocal Binary Pattern局部二值模式天生就是干这个的对每个像素比较它和邻域像素的灰度关系生成一个二进制编码统计编码直方图作为特征。它不关心颜色、不关心绝对灰度值只关心局部的明暗对比结构恰好对光照变化不敏感——这正好补上普通摄像头采集时光照不一致的短板。from skimage import feature def compute_lbp_features(roi, radius2, n_points16): # radius 决定采样半径n_points 决定邻域采样点数 lbp feature.local_binary_pattern(roi, n_points, radius, methoduniform) # 统一模式的 LBP 会降维同时保留主要纹理模式 n_bins n_points 2 hist, _ np.histogram(lbp.ravel(), binsn_bins, range(0, n_bins)) # 归一化让特征与 ROI 尺寸无关 hist hist.astype(np.float32) hist / (hist.sum() 1e-6) return histmethoduniform是非常关键的选择。普通 LBP 一个像素点的模式数是 2^n_points16 个采样点就是 65536 种模式直方图稀疏且计算量爆炸。统一模式只保留“最多两次跳变”的纹理模式把模式数压缩到 n_points 2 个也就是 18 维再叠加不同半径和采样点数的组合最终特征维度也才几十到几百。这在机器学习大作业里有实打实的好处特征维度低SVM 或 KNN 训练速度极快而且不容易过拟合。radius2和n_points16是我调过一轮后的推荐值。radius 太小时 LBP 只捕捉到像素级噪声纹理单元感太弱radius 太大则纹理单元跨越了多条掌纹线统计意义被稀释。16 个采样点在 ROI 是 128x128 时能在细节和稳定性之间取得平衡。如果想要更丰富的特征表达可以分别计算(radius1, n_points8)和(radius3, n_points24)的特征并拼接成多尺度特征向量。3.2 Gabor 滤波做方向纹理增强与特征互补LBP 是统计视角Gabor 滤波是频域视角。掌纹线有四个主导方向水平、垂直、45 度、135 度。Gabor 滤波器能针对特定方向做纹理响应增强对掌纹主线的提取效果比 LBP 更语义化。常见做法是对 ROI 做多方向、多频率的 Gabor 滤波对滤波结果做方差或能量统计得到一组描述纹理方向分布的特征。import cv2 def compute_gabor_features(roi, orientations[0, 45, 90, 135], frequencies[0.1, 0.2]): features [] for theta in orientations: for freq in frequencies: # Gaussian 包络 正弦平面波构成 Gabor 核 kernel cv2.getGaborKernel((31, 31), sigma4.0, thetanp.deg2rad(theta), lambd1.0 / freq, gamma0.5, psi0) filtered cv2.filter2D(roi, cv2.CV_32F, kernel) # 用响应能量和标准差描述该方向和频率下的纹理强度 energy np.sum(filtered ** 2) std np.std(filtered) features.extend([energy, std]) return np.array(features)getGaborKernel里四个参数决定滤波器的形态sigma控制高斯包络的宽度lambd是正弦波的波长gamma是椭圆度0.5 表示纵向拉伸theta是滤波方向。对掌纹来说sigma4.0 能在保留主线宽度的同时不过度平滑gamma0.5 让滤波器对方向的敏感性更强。频率 0.1 和 0.2 对应粗纹理和细纹理两个尺度覆盖掌纹主线与细微褶皱。Gabor 特征和 LBP 特征的互补性很强LBP 擅长表达纹理的微观结构分布Gabor 擅长表达方向性主线的强度。我在项目中习惯把两套特征拼接成一个向量再用 PCA 降维到 80~120 维。这样 SVM 的训练速度更快维数灾难也得到控制。如果你的大作业要求“必须体现特征工程”这套组合写法就是现成的加分点。def extract_feature_vector(roi): lbp_hist compute_lbp_features(roi) gabor_vec compute_gabor_features(roi) # 拼接特征向量LBP 统一模式直方图 Gabor 能量统计 feature_vec np.concatenate([lbp_hist, gabor_vec]) return feature_vec拼接后的特征向量大约在 40 维到 60 维之间取决于 LBP 参数和 Gabor 方向和频率的乘积这个体量直接用 KNN 也能跑配合 SVM 的 RBF 核效果更佳。如果后续测试发现识别率不理想优先检查的不是模型而是 ROI 是否对齐——我见过太多人模型调了一周最后发现是 ROI 偏移导致特征本身就有问题。3.3 特征归一化的必要性与实现特征拼接之后必须先做归一化再进模型这一步省不得。LBP 直方图的值域在 0~1 之间Gabor 能量值可能上万如果不归一化SVM 的核距离计算会完全被 Gabor 特征主导LBP 的信息等于白做了。我用的是 StandardScaler按特征列做零均值单位方差变换比 MinMaxScaler 对离群值更稳健。from sklearn.preprocessing import StandardScaler scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test)注意测试集只用transform不能fit_transform。如果对测试集重新计算均值和方差等于把测试数据的信息泄漏进了预处理环节识别率虚高到 98% 但换一批数据就打回原形。这个错误在大作业答辩时被老师问出来会非常尴尬属于典型的“看起来分高、实际不能打”的实现。4. 分类器选择与识别链路搭建SVM、KNN 和最近邻的实战对比4.1 三种分类器在大作业场景下的表现差异掌纹识别本质上是多分类问题——每个注册用户是一个类别待识别的掌纹要判断属于哪一个类别。选分类器有三个现实约束训练样本少、特征维度适中、需要解释性强。我在这类项目中测过一轮KNN 简单但受离群点影响大SVM 在中小样本上精度最高而最近邻分类器1-NN配合好的特征甚至能达到接近 SVM 的效果且无需训练。from sklearn.svm import SVC from sklearn.neighbors import KNeighborsClassifier # SVMRBF 核适合特征维度不高但样本边界复杂的场景 svm_model SVC(kernelrbf, C10.0, gamma0.01, probabilityTrue) # KNNk5距离度量用欧氏距离 knn_model KNeighborsClassifier(n_neighbors5, metriceuclidean) # 1-NN直接以最相似样本的标签作为预测结果 nn_model KNeighborsClassifier(n_neighbors1, metriccosine)SVM 的参数 C 和 gamma 对结果影响最大。C 是误分类惩罚系数越大越不容忍训练集错误容易过拟合gamma 控制 RBF 核的径向作用范围越小决策边界越平滑。大作业数据集通常每类只有 10~30 张样本我推荐从 C1.0 和 gamma0.01 起步用网格搜索微调而不是上来就套默认参数。KNN 用欧氏距离在小样本下表现尚可但掌纹特征向量各维度经过标准化后方向性更重要余弦距离往往比欧氏距离稳定。最直接的验证方法是对同一批数据跑混淆矩阵距离度量不同的结果差异一眼就能看出来。很多人忽略这一点KNN 效果差就抱怨特征不好实际上是距离度量选错了。4.2 识别链路注册、训练、识别三阶段的完整流程一个完整的掌纹识别系统分三个模块注册阶段录入样本并提取特征存入数据库、训练阶段用特征矩阵训练分类器、识别阶段提取待识别特征并与已注册特征对比。import numpy as np import json class PalmprintRecognizer: def __init__(self, classifier): self.classifier classifier self.feature_db {} self.user_ids [] def register(self, user_id, roi): # 注册提取特征并保存到内存数据库 feature_vec extract_feature_vector(roi) self.feature_db.setdefault(user_id, []).append(feature_vec) if user_id not in self.user_ids: self.user_ids.append(user_id) def train(self): # 训练将所有注册样本组装成特征矩阵 X_train [] y_train [] for user_id, feats in self.feature_db.items(): for f in feats: X_train.append(f) y_train.append(user_id) X_train np.array(X_train) y_train np.array(y_train) X_train scaler.fit_transform(X_train) self.classifier.fit(X_train, y_train) def predict(self, roi): feature_vec extract_feature_vector(roi) feature_vec scaler.transform(feature_vec.reshape(1, -1)) label self.classifier.predict(feature_vec)[0] confidence self.classifier.predict_proba(feature_vec).max() return label, confidence训练阶段只对所有注册样本计算一次特征并拟合分类器识别阶段对单张 ROI 提取特征后送入分类器即可。这种设计的优势是注册新用户时不需要重新训练全量模型只需在数据库里新增特征。但要注意如果分类器是 SVM新增用户后核模型本身已经固定新类别的决策边界不会自动更新——需要在新样本达到一定数量后重新train()。4.3 置信度阈值识别不等于分类很多人把分类器的输出直接当成识别结果这在大作业里是比较浅的做法。真实场景中系统必须能拒绝“陌生人”的掌纹而不是硬把未知样本归到最近的类别。高效的做法是在分类器输出的概率分布上做阈值判断。# 只有最高置信度超过阈值的样本才判定为识别成功 THRESHOLD 0.75 def verify_with_threshold(recognizer, roi): label, confidence recognizer.predict(roi) if confidence THRESHOLD: return label, confidence, True else: return None, confidence, False阈值 0.75 是一个经验起始值具体根据识别率曲线调整。如果测试时发现“陌生人被误认为注册用户”的情况多就把阈值往上抬到 0.85发现自己人的样本频繁被拒就往下调到 0.65。这个过程就是论文里常说的 FARFalse Acceptance Rate和 FRRFalse Rejection Rate的权衡能把这个讲明白答辩时要比单纯汇报“识别率 95%”有说服力得多。5. 掌纹识别项目避坑从图像采集到结果验证的 5 个典型翻车现场5.1 背景分割失败光线一暗整个手掌变成黑色现象换到暗光环境采集时二值化结果全是黑的轮廓检测找不到手掌。原因大津法假设图像灰度直方图是双峰的前景和背景各自成群但暗光下手掌和背景灰度接近双峰变成单峰阈值分割失效。解决不要裸用大津法。先做一次 CLAHE 自适应直方图均衡化增强对比度再二值化。如果背景仍然复杂可以在采集时加一个纯色背景板——这是成本最低且最稳定的方案。我实际测试过深蓝色背景比白色背景更容易分割因为肤色和深蓝色的灰度差更稳定。5.2 谷点检测抖动手稍微一偏ROI 就换了个位置现象同一只手掌在同一位置拍摄两张照片第一张 ROI 正常第二张 ROI 明显上移或偏移导致特征不一致。原因凸包缺陷点的深度排序受轮廓微小扰动影响。手放松状态不同、手指开合变化都会让谷点位置轻微移动。解决第一对轮廓曲线做高斯平滑消除像素级锯齿第二不用单一的谷点位置而是取谷点周围一个小邻域的均值坐标第三ROI 裁剪的中心不直接用谷点中点而是先计算掌心中心再固定偏移。我在实现中加入了轮廓平滑步骤ROI 的稳定性明显提升。这个问题最容易在测试时暴露因为人手的姿态不可能完全复现。5.3 特征维度灾难特征拼接一时爽模型训练火葬场现象同时用了 LBP、Gabor、HOG 三种特征拼接后特征维度达到几千维SVM 训练变得极慢且识别率不升反降。原因特征不是越多越好。高维度特征在小样本下极易过拟合特征间的冗余也干扰了分类器的决策面学习。解决对拼接后的高维特征做 PCA 降维保留 85%~95% 的方差贡献率把维度压到 100 以内。另一个办法是改用特征选择用 SelectKBest 按 F 值挑最有区分度的特征维度。经验法则特征维度不要超过样本总量的 1/5否则就考虑降维。5.4 训练测试数据划分不当同一个人的手出现在两边现象评估识别率时高达 99%但实际演示时频繁识别失败。原因很多人把同一个人的所有样本既放进了训练集又放进了测试集。掌纹图像相似度极高同一人的训练样本和测试样本几乎同一张图分类器在测试时相当于直接查答案。解决按“用户”分组划分训练集和测试集中不允许出现同一人的不同样本必须是完全陌生的样本做测试。更严格的做法是分两次采集第一次采集做注册隔天再采集做测试保证样本有真实的姿态和光照差异。这也是大作业里最容易注水的地方老师一眼就能看出来。5.5 数据增强滥用旋转平移过度把掌纹变成抽象画现象对训练集做大量随机旋转、平移增强后识别率反而比不增强还低。原因掌纹识别对方向极其敏感ROI 旋转已经对齐了方向再多旋转增强等于告诉模型“方向不重要”反而破坏了纹路的方向性特征。解决数据增强只做小幅度的灰度抖动和高斯噪声不旋转超过 5 度不平移超过 ROI 尺寸的 5%。掌纹识别的地基是对齐增强操作要保留这个地基。如果想要更丰富的训练数据最靠谱的方式是采集时每个用户拍 10~15 张轻微变换手的摆放位置这比任何数据增强都有效。6. 精度提升三板斧参数标定、阈值调优与混淆矩阵分析6.1 用混淆矩阵定位“谁和谁分不清”训练完模型不要只看一个总体准确率要打印混淆矩阵。这是我每次做识别类项目的第一个动作。混淆矩阵能清晰展示哪两个用户之间最容易被混淆——通常发生在纹理相似度高的个体之间。定位到具体混淆对之后有针对性地增加这两个人的训练样本数比盲调全局参数有效得多。from sklearn.metrics import confusion_matrix, classification_report y_pred svm_model.predict(X_test_scaled) cm confusion_matrix(y_test, y_pred) print(cm) # 按类别输出精确率、召回率和 F1找出最弱的类别 print(classification_report(y_test, y_pred))如果 A 类和 B 类频繁互相误判我会单独抽取这两类的样本对比特征向量观察哪些维度太接近。常见原因是这两个人掌纹主线走向相近Gabor 方向特征几乎一致此时增加 LBP 多尺度特征的权重或调整 PCA 保留的方差比例通常能拉开差距。6.2 网格搜索把 SVM 参数调到边界SVM 的 C 和 gamma 对识别率的影响是立竿见影的。我在项目中用 GridSearchCV 做交叉验证找出当前特征集下的最优参数组合。小样本场景下用 3 折交叉验证就够了5 折会让每折的训练样本太少反而引入方差。from sklearn.model_selection import GridSearchCV param_grid { C: [0.1, 1.0, 10.0, 50.0], gamma: [0.001, 0.01, 0.1, 0.5], kernel: [rbf] } grid_search GridSearchCV(svm_model, param_grid, cv3, scoringaccuracy) grid_search.fit(X_train_scaled, y_train) print(grid_search.best_params_) print(grid_search.best_score_)网格搜索的粒度不用太细。大作业的数据量下C 从 0.1 到 50 对数采样四个档位、gamma 从 0.001 到 0.5 同样四个档位正好 16 组组合几秒钟跑完。如果每组都要手动试那才叫浪费时间。拿到最优参数后再用全部训练数据重新 fit 一次模型而不是直接用交叉验证时的模型这个细节很多人会忘。6.3 阈值曲线画出 FAR 和 FRR 的交点最后一个技巧是画出置信度阈值与 FAR/FRR 的关系曲线。做法是保留一批注册用户样本和一批完全未注册过的陌生人样本分别输入系统得到置信度分数然后从 0.5 到 1.0 滑动阈值统计每个阈值下的 FAR陌生人被接受的比例和 FRR自己人被拒绝的比例。两条曲线的交点就是当前系统的最优工作点。thresholds np.arange(0.5, 1.0, 0.02) FAR_list [] FRR_list [] for t in thresholds: # 假设已知每个样本的真实身份和置信度分数 far np.mean([conf t and pred ! true for pred, conf, true in stranger_scores]) frr np.mean([conf t or pred ! true for pred, conf, true in genuine_scores]) FAR_list.append(far) FRR_list.append(frr) # 找 FAR 和 FRR 最接近的阈值作为工作点 optimal_t thresholds[np.argmin(np.abs(np.array(FAR_list) - np.array(FRR_list)))]这个方法在答辩时是很大的加分项——它证明你不是只会跑通代码而是理解识别系统在实际部署时要做安全性和可用性的权衡。大作业评分老师见得最多的是只会调用 predict 的学生能把阈值分析讲清楚的项目高分段是稳的。我自己的习惯是每次调完一组参数就跑一次阈值曲线确保调参不是为了某几个固定测试样本过拟合。掌纹识别和做人脸识别不一样没有预训练模型可以偷懒每一步都得自己动手这也是这类项目在大作业里含金量高、容易拿高分的原因。整个项目做下来ROI 对齐最费时间但最值钱特征工程决定了识别率的天花板而分类器只是最后一步。希望这篇文章能把你在掌纹识别大作业上要走的弯路都提前标出来照着做就能少踩几个坑祝你的项目也能稳稳落在高分区间。本文还有配套的精品资源点击获取