☰
掌纹识别大作业:从预处理到分类器融合的完整实战指南
2026/9/28 22:30:50 网站建设 项目流程

简介:一份面向机器学习课程设计与期末大作业的掌纹识别项目源码,基于Python实现,已获导师指导并被评为97分高分项目。资源共17个文件,其中11个ipynb过程笔记和5个py脚本构成,整体仅189KB,压缩包结构清晰,适合直接运行。内容覆盖掌纹图像预处理、特征提取、PCA降维、滤波器设计、CNN分类模型构建、预训练模型微调以及分类器融合与海明距离对比等关键环节,任务包括分类与认证两个方向的完整实现。已有401人学习,可用作期末大作业或课程设计的完整参考模板,无需修改即可复现结果,适合需要高质量机器学习项目源码的在校学生。

1. 掌纹识别大作业:97 分项目怎么把“识别”拆成四条技术线

把掌纹识别做成机器学习大作业,最容易踩的坑是把它当成猫狗分类的翻版,直接丢一个 CNN 上去调参。掌纹识别里真正决定分数的不是网络有多深,而是特征怎么提、PCA 怎么降维、分类和认证怎么区分,以及最后能不能用一组干净的实验数据把方法说明白。这份 97 分高分项目源码把掌纹数据的处理流程做成了完整闭环:图像预处理、特征提取、PCA 降维、CNN 分类、海明距离认证、分类器融合,还配了多进程加速和时间对比脚本。下文按文件组织顺序拆开讲,每个模块写清楚参数怎么设、坑在哪。适合机器学习课程设计与期末大作业复现,也适合想做生物特征识别基线的同学参考。

2. 图像预处理与数据组织:先把一张掌纹图变成模型爱吃的样本

预处理决定了后面所有实验的上限。项目里图像预处理.ipynb和classify.py前半段做的就是这个事。掌纹图不是通用图像,直接用原图喂模型,学到的往往是背景、手指缝和手腕边缘,而不是掌纹纹路。这一节把预处理流程和数据组织规则拆开讲。

2.1 分类任务和认证任务,对数据组织的要求完全不同

写代码之前得先想清楚:这份大作业做的是分类还是认证。项目里的分类与认证.ipynb和两个任务的对比.ipynb把两条线都做了。分类任务对应 1:N,每一类是一个人,输出一个 person_id;认证任务对应 1:1,判断两张图是否来自同一个人。分类要按人分 ID,认证要构造正负样本对:同一人的两张图是正样本对,不同人的两张图是负样本对。如果数据表里只有图像路径没有 person_id,后面所有实验都做不了,所以数据组织是整个项目的地基。

公开掌纹库每个类通常只有 5~10 张图像,这个样本量直接决定网络能不能做深。处理这类项目时,我一般先把每个人当成一个类别索引,建一个 CSV 记录 sample_id、person_id、img_path、split,再写 Dataset 按行读取。好处是后面分类和认证可以复用同一张表,不需要为两个任务各写一套目录遍历代码。

sample_idperson_idimg_pathsplit
P001_011data/palm/P001/train_01.pngtrain
P001_021data/palm/P001/train_02.pngtrain
P002_012data/palm/P002/test_01.pngtest
P002_022data/palm/P002/test_02.pngtest

注意看这张表,split 的划分单位是 person_id 而不是 image。同一个人的两张图必须只出现在 train 或只出现在 test 里,如果同一个人前后两张图一边训练一边测试,测试准确率会虚高,答辩时被问到很难说清。压缩包里没有显式列出 data 目录,复现时通常自己按上面的方式组织,再用 pandas 或 glob 读进来。

2.2 图像预处理.ipynb 的流程拆解

原始图经常是固定背景的整只手图片,预处理的目标只有四个:去掉背景、转灰度、拉平光照、统一尺寸。下面这段逻辑对应图像预处理.ipynb的核心部分,classify.py里也复用了同一套处理:

# image_preprocess 核心逻辑 import cv2 import numpy as np def preprocess_palm(img_path, out_size=(128, 128), roi_ratio=0.6): img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) h, w = img.shape # 只保留中心区域,去掉手指、手腕和背景 y0 = int(h * (1 - roi_ratio) / 2) y1 = int(h * (1 + roi_ratio) / 2) x0 = int(w * (1 - roi_ratio) / 2) x1 = int(w * (1 + roi_ratio) / 2) roi = img[y0:y1, x0:x1] roi = cv2.resize(roi, out_size) # 直方图均衡化:弱光照下纹路对比度更好 roi = cv2.equalizeHist(roi) # 转 float32 并归一化到 [0, 1] roi = roi.astype(np.float32) / 255.0 return roi

这段代码里两个参数要重点说明。roi_ratio 决定 ROI 占原图的比例,0.6 表示只留中心 60% 区域,太小会切掉靠近指尖和腕部的掌纹,太大则把手指边缘卷进来。out_size 是最终输入尺寸,手工特征流程里 64x64 就够提取统计特征,CNN 流程建议 128 往上,因为掌纹细纹需要空间分辨率。转 float32 再除以 255 是必要操作,否则后续 PCA 和距离计算会遇到类型警告和量纲问题。

提示:如果原始图像掌纹区域不是居中分布,中心裁剪不可靠,需要按手掌轮廓或指缝点做仿射变换。公开掌纹数据集基本都是标准姿态,中心裁剪够用。

2.3 数据增强与训练/验证/测试划分的边界

项目里同时跑分类和认证,数据集划分必须提前固定,否则两个 notebook 各切一次,结果对比就失效。我习惯写一个固定随机种子的划分函数,按 person_id 分组,确保同一个人的图像只出现在一个集合里。划分最小单位是“人”而不是“图”,这一点在报告里要写清楚。

数据增强只放训练集:随机水平翻转、小角度旋转、轻微平移。验证集和测试集必须保持原图。掌纹是有向纹理,旋转增强范围要克制,±10 度以内足够,旋转太大会让掌纹拓扑失真。增强在 batch 读取时实时做,不要提前落盘,否则磁盘占用成倍增长,后面跑多进程时间对比时还会把磁盘 IO 混进总时间,实验数据不好看。

3. 特征提取与 PCA:手工特征在小样本掌纹任务上为什么是刚需

这一章对应feature_extraction.py、滤波器PCA.ipynb、PCA.py和海明距离.ipynb。在每类样本极少的情况下,手工特征加分类器比直接上深度网络要稳得多,这也是这份项目能拿 97 分的重要原因。

3.1 先做特征提取,而不是直接端到端

开放题大作业里常见矛盾是:每类只有几张图,CNN 很快就过拟合。掌纹识别有经典方法论,主流路线就是“特征提取 + 分类器”。Gabor 滤波器组在不同方向、不同尺度下对掌纹图做卷积,得到响应图,压缩成统计量,再交给 SVM 或做距离匹配。项目里把这条路作为 CNN 的对比基线,在两个任务的对比.ipynb里用同一份数据做横向比较,这个设计本身就很有说服力。

Gabor 滤波器本质上是带方向选择性的带通滤波器,对特定方向的条纹非常敏感,天然匹配掌纹主线和皱纹的纹理方向。裸 CNN 要学到类似的方向选择性,需要大量卷积核拟合,每类 5~10 张图不够它吃。所以大作业里保留手工特征基线,既是方法线,也是防过拟合的手段。

3.2 feature_extraction.py:Gabor 滤波器组与特征向量拼接

feature_extraction.py的核心是构建 Gabor 核,对预处理图滤波,再提取统计量。常见配置是 4 个方向、1~2 个尺度,每个响应图取均值和标准差。下面这段是典型实现:

# feature_extraction.py 核心逻辑 import cv2 import numpy as np def build_gabor_bank(ksize=31, sigma=4.0, lambd=10.0, gamma=0.5): filters = [] # 四个方向:0、45、90、135 度,覆盖掌纹主线走向 for theta in (0, np.pi / 4, np.pi / 2, 3 * np.pi / 4): kernel = cv2.getGaborKernel( (ksize, ksize), sigma, theta, lambd, gamma, 0, ktype=cv2.CV_32F ) filters.append(kernel) return filters def extract_gabor_features(img, filters): # img: 预处理后的灰度图,float32,范围 [0,1] feats = [] for kernel in filters: filtered = cv2.filter2D(img, cv2.CV_32F, kernel) feats.append(filtered.mean()) # 整体响应强度 feats.append(filtered.std()) # 纹理起伏程度 return np.asarray(feats, dtype=np.float32)

Gabor 参数之间会互相影响,调试时按下面这张表逐个调:

参数取值作用
ksize31核尺寸,决定感受野范围
sigma4.0高斯包络标准差,控制频带宽度
theta0~135 度滤波器方向,对应掌纹线走向
lambd10.0波长,控制条纹疏密
gamma0.5纵横比,让核在垂直方向被拉扁

变量名写 lambd 是因为 lambda 是 Python 关键字。sigma 太大会平滑掉细纹,太小又变成噪声响应;lambd 越小条纹越密,适合掌纹里密集的褶皱。如果特征在验证集上分不开,先把每个方向的响应图打印出来看一眼,比盲目调分类器有用。另外 filter2D 有边界效应,图像边缘响应偏低,最好在 ROI 基础上再内缩 10 个像素,否则手指轮廓的响应会被混进特征。

3.3 PCA.py 与滤波器 PCA 的降维逻辑

Gabor 特征拼接后维度不高,为什么还要 PCA?因为不同方向响应之间有相关性,直接喂 SVM 会受共线性影响,而且同一张图在不同光照下特征浮动大。PCA 把高相关特征旋转到主成分空间,去掉冗余分量。PCA.py里的流程分三步:标准化、求解主成分、投影。

# PCA.py 核心流程:先标准化,再做主成分投影 from sklearn.preprocessing import StandardScaler from sklearn.decomposition import PCA # X: (n_samples, n_feature_dims),来自 extract_gabor_features scaler = StandardScaler() X_scaled = scaler.fit_transform(X) pca = PCA(n_components=0.95) # 保留 95% 方差 X_pca = pca.fit_transform(X_scaled) print("原始维度:", X.shape[1]) print("降维后维度:", X_pca.shape[1]) print("累计方差贡献率:", pca.explained_variance_ratio_.cumsum())

两个参数最容易出错。第一,StandardScaler 必须在 PCA 之前,否则量纲大的特征主导协方差矩阵;第二,n_components 用 0.95 表示保留 95% 方差,比固定写成 50 更稳,因为不同数据集上 Gabor 维度不同,固定维数要么丢信息要么降维不足。

这里还有一个隐蔽的数据泄漏问题:scaler 和 pca 都只能在训练集上 fit,验证集和测试集只做 transform。正确写法是这样:

# 正确做法:数据划分必须在 fit 之前 from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, stratify=y, random_state=42 ) scaler.fit(X_train) X_train_scaled = scaler.transform(X_train) X_test_scaled = scaler.transform(X_test) pca.fit(X_train_scaled) X_train_pca = pca.transform(X_train_scaled) X_test_pca = pca.transform(X_test_scaled)

如果把全量数据拿去 fit,测试集信息提前进入降维矩阵,测试指标会虚高,这是答辩最容易翻车的地方。

3.4 用 PCA 特征算海明距离:分类和认证的最后一公里

在认证任务里,模型输出的不是 ID,而是两张图是否匹配。海明距离.ipynb的做法是先把特征二值化,再逐位比较差异比例。PCA 特征可以按维度中位数转成 0/1,然后算海明距离:

def hamming_distance(a, b): # a, b 是两个二值化后的 0/1 向量 diff = np.bitwise_xor(a, b) return diff.sum() / a.size

这个 trick 在报告里很好写:特征从连续值变成二进制,占用空间小,匹配只靠 XOR 和 popcount。注意海明距离和 CNN 分类概率不是一回事,前者面向认证,后者面向分类。在两个任务的对比.ipynb里,两条线的评价指标要分开:分类用准确率,认证用 FAR/FRR 和 EER。

4. CNN 与分类器融合:自建网络、迁移学习与投票策略怎么组合

这份项目不是只用手工特征,CNN 是重要加分点。构建CNN分类掌纹.ipynb、预训练模型CNN.ipynb、分类器融合.ipynb三份文件合起来是一条完整的深度学习路线。

4.1 构建 CNN 分类掌纹.ipynb:轻量 CNN 的结构

PalmCNN 是轻量结构:两个卷积块加自适应池化加全连接头。为什么轻量?每类只有几张图,网络参数过多会先记住训练集,而不是学习泛化特征。两个卷积块已经足够学到掌纹局部方向纹理,分类头把 64 维特征映射到类别数。

# 构建CNN分类掌纹.ipynb 中的 PalmCNN import torch.nn as nn class PalmCNN(nn.Module): def __init__(self, num_classes, in_channels=1): super().__init__() self.features = nn.Sequential( nn.Conv2d(in_channels, 32, kernel_size=3, padding=1), nn.BatchNorm2d(32), nn.ReLU(inplace=True), nn.MaxPool2d(2), nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.MaxPool2d(2), nn.AdaptiveAvgPool2d(1) ) self.classifier = nn.Linear(64, num_classes) def forward(self, x): x = self.features(x) return self.classifier(x.view(x.size(0), -1))

in_channels=1 对应灰度图输入,如果预处理时复制成三通道就改成 3。AdaptiveAvgPool2d(1) 是懒人设计,不管输入 64 还是 128,池化后维度都是 64,分类头不用改。训练参数建议 batch size 16 到 32,学习率 1e-3,Adam,交叉熵损失。样本少时我只训 30~50 个 epoch,用验证集早停。训练震荡就把学习率降到 5e-4,再看增强强度是否过大。

4.2 预训练模型 CNN:迁移学习怎么用才不出错

预训练模型CNN.ipynb用 ImageNet 预训练 ResNet18 做迁移。最大的坑是输入通道和输入尺寸:ImageNet 是三通道 224 或 256,掌纹是灰度图。把灰度图直接喂进去是错的。推荐做法是把灰度图复制成三通道,保留 ImageNet 预训练权重,而不是替换第一层卷积。替换 conv1 会让预训练权重完全失效,反而更慢收敛。

# 预训练模型CNN.ipynb 核心思路 import torchvision.models as models import torch.nn as nn model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1) # 预处理阶段把灰度图复制成三通道 (B, 1, 128, 128) -> (B, 3, 128, 128) # 修改最后的全连接层,输出类别数 model.fc = nn.Sequential( nn.Dropout(0.3), nn.Linear(model.fc.in_features, num_classes) )

预训练模型要尊重它的输入尺寸,preprocess_palm的 out_size 改成 224 再放进这段流程。显存有限时冻结前 4 个 block,只训练最后 1 个 block 和 fc;学习率用 1e-4 到 3e-4,比从头训练小一些。Pytorch 在切换训练和 eval 模式时要注意 BatchNorm 行为,batch size 也别给太小的值,否则 BN 统计量不稳定。

4.3 分类器融合:加权投票为什么能反超单一强模型

分类器融合.ipynb的思路很直接:手工特征那条线用 SVM 出概率,CNN 那条线出 softmax 概率,在验证集上加权融合。两个模型错误模式互补:SVM 对精细纹理敏感,CNN 对整体结构敏感,互补性越大投票收益越大。

# 分类器融合.ipynb 中的加权投票 import numpy as np def weighted_vote(prob_svm, prob_cnn, w=0.6): # w 是 SVM 分支权重,1-w 是 CNN 分支权重 blended = w * prob_svm + (1 - w) * prob_cnn return np.argmax(blended) # 在验证集上搜索最优 w best_w, best_acc = 0.0, 0.0 for w in np.arange(0.0, 1.05, 0.05): acc = np.mean([ weighted_vote(svm_p[i], cnn_p[i], w) == y_val[i] for i in range(len(y_val)) ]) if acc > best_acc: best_w, best_acc = w, acc print(f"best w={best_w:.2f}, val_acc={best_acc:.4f}")

酒精藏在细节里:SVM 的 decision_function 分数和 CNN softmax 概率量纲完全不一样,融合前要先各自归一化。我一般把 svm_score 和 cnn_prob 都做 min-max 归一化,否则 w 的语义会被破坏。报告里写融合结果时,要分别列出 SVM、CNN、融合三个准确率,只写融合后的数字会被追问提升来源。

5. 掌纹识别避坑手册:五个典型翻车现场与排查路径

这一章把复现过程中最容易踩的五个坑单独列出来。每一条都是真实翻车场景,按现象、原因、解决三步排查。

5.1 训练 loss 不降,准确率恒等于多数类占比

现象:CNN 训练十几轮,loss 几乎不动,输出概率集中在某一个类,准确率恰好像多数类的占比。原因:标签和特征错位。常见于自己写 DataLoader 时对图片列表用 os.listdir 排序,同时又手动 shuffle,图片和标签没有同步;也可能是数据集中混入了背景图,模型学的是背景而不是掌纹。解决:先做数据完整性校验,随机抽 10 个 batch,把图像缩略图和 label 一起打印出来人工核对。给每个样本生成唯一 ID,DataLoader 里只按 ID 读取,不要一边排序一边打乱。

5.2 PCA 后准确率反而比原始特征更低

现象:原始特征喂 SVM 能到 80% 以上,PCA 降维到 50 维后面识别率掉到 70%。原因:没做标准化,或者 n_components 定得太低,丢掉了有区分度的低频信息。Gabor 特征维度间量纲差异不大但分布不同,直接 PCA 会被大方差方向带偏。解决:先 StandardScaler 再 PCA;n_components 在 0.90 到 0.99 之间做扫描对比;不要把 PCA 后的特征当成唯一答案,配合 SVM 的 C 参数一起调。

5.3 海明距离认证的阈值怎么调都不准

现象:类内距离和类间距离分布重叠严重,无论阈值取多少,FAR 和 FRR 都居高不下。原因:二值特征太短,滤波器方向太少,特征区分度不足;或者训练样本和测试样本采集时间跨度大,光照差异被反映到距离上。解决:先画类内和类间距离直方图,阈值取两个分布的交叉点附近;增加 Gabor 方向和尺度,让特征维度足够;报告里用 EER 作为单一指标,别只给一个手调阈值。

5.4 多进程.py 在 Jupyter 里跑起来就报错或直接卡死

现象:在 notebook 里调用 multiprocessing.Pool 没反应,Windows 下反复弹错误或直接杀掉内核。原因:multiprocessing 在 Windows 下用 spawn 启动新进程,会重新导入主模块;notebook 顶层没有 ifname== "main" 保护时,子进程递归执行 worker 函数之前的代码。解决:把多进程入口放到独立 .py 文件,用 ifname== "main": 包住 Pool 逻辑,任务函数放模块顶层。也可以换成 joblib.Parallel,和 notebook 的兼容性好很多。

5.5 预训练模型加载后训练更慢,显存还容易爆

现象:加上预训练 ResNet18 后,原来能跑完的实验 OOM,或一个 epoch 慢得离谱。原因:ResNet18 参数比轻量 CNN 大一个量级,学习率没调,前几轮 loss 跳动剧烈;batch size 沿用原来的 32,显存直接爆。解决:输入 resize 到 224;冻结前 4 个 block;batch size 从 8 或 16 开始;学习率降到 1e-4;有条件就开自动混合精度。报告里写明冻结和微调的比例,面试时这是一个有含金量的工程细节。

6. 多进程加速与结果评估:把报告里的实验数据做扎实

最后这一章处理两个容易被忽略但很拉分的点:多进程加速怎么测,评估指标怎么写。多进程.py和多进程的时间对比.py负责前者,结果评估.ipynb负责后者。

6.1 多进程加速:怎么测出可信的加速比

多进程加速适合放在预处理和特征提取阶段,因为每张图的处理相互独立。测速时要保证输入相同、核数一致,先跑单进程再跑多进程,每秒取平均。建议按下面这段写:

# 多进程的时间对比.py 测速思路 import time t0 = time.time() # 单进程处理所有图 single_time = time.time() - t0 t1 = time.time() # 多进程 Pool.map 处理同样一批图 multi_time = time.time() - t1 print(f"单进程 {single_time:.4f}s, 多进程 {multi_time:.4f}s, 加速比 {single_time / multi_time:.2f}x")

注意图像数量太少时多进程开销反而大,报告里写清楚“加速比随图像数量增大而上升”,最好给一组不同图像数量的折线图。

6.2 结果评估:混淆矩阵、分类报告与 FAR/FRR

结果评估.ipynb里至少要做三件事:分类报告、混淆矩阵、认证任务的 ROC 曲线。分类报告直接调 sklearn 输出即可,重点是把每个类别的 precision、recall、f1-score 都列出来,答辩时一眼看出哪些人容易被混淆。

from sklearn.metrics import classification_report, confusion_matrix print(classification_report(y_true, y_pred)) print(confusion_matrix(y_true, y_pred))

认证任务要用 FAR 和 FRR,不要只算一个准确率。FAR 是把别人认成自己的比例,FRR 是把自己认成别人的比例,两者随阈值变动画出 ROC 曲线,EER 取 FAR 与 FRR 交点。这个指标是掌纹认证任务的标准写法,报告里放一张 ROC 曲线,能直接拉高实验深度。

从那以后我每次做大作业,都会先把单进程跑通、把指标脚本写全,再开多进程优化和调参,否则根本分不清是业务 bug 还是并行引入的问题。这份源码更适合先按原始逻辑复现一遍,把数据表和评估脚本固定好,再动融合和多进程,希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询