简介:一份基于 Python 的掌纹特征提取与分类任务完整源码及文档说明,面向计算机视觉、模式识别相关专业的毕业设计和课程设计,适合需要快速搭建可运行项目的中高级学习者。资源共 10 个文件,压缩包整体约 13.49MB,其中 5 个 ZIP 数据包按训练集、验证集和测试集划分,4 个 Python 脚本分别承担 ResNet 模型搭建、SIFT 特征工具函数、模型工具函数及测试入口,另有 README.md 说明部署步骤与运行方式。代码注释详细,目录结构清晰,从数据加载、特征提取到模型训练与评估均有完整实现,简单部署即可运行;同时,ZIP 数据文件已整理完毕,免去自行采集样本的环节,模型模块彼此解耦,便于替换数据集和调整参数。项目为作者手打的 98 分毕业设计,导师认可度较高,已有 76 人学习下载,适合需要借鉴完整掌纹识别流程、减少重复开发的同学参考。
1. 掌纹特征提取与分类毕设源码:两条路线一次性跑通
答辩被追问算法原理的时候卡壳,是很多毕设项目的通病——掌纹识别尤甚:既要讲清特征提取,又要说明分类模型,两边都糊就容易被判定为“非原创”。这份基于 python 的掌纹特征提取和分类任务源码,把两条技术路线都备齐了:SIFT 关键点描述子走传统特征工程,ResNet 残差网络走深度学习分类;训练脚本、测试脚本、数据集划分压缩包和 README 一应俱全,解压后按文档装依赖就能跑。适合急需交付毕业设计或期末大作业的学生,也适合 python 入门阶段想完整体验图像分类流程的读者。
2. 项目结构与数据组织:train / valid / test 三包必须先摆对位置
拿到的 zip 里不是只有一套模型脚本,而是一组互相配合的文件加三到四组数据包。很多新手第一反应是把训练脚本直接跑起来,结果FileNotFoundError一个接一个,原因基本都出在数据没按约定摆放。所以这篇笔记先从文件清单和目录结构讲起,这部分搞定了,后面训练和测试的坑至少少一半。
2.1 压缩包里的文件到底负责什么:一份清单与功能对照
先把 zip 里的内容摆开看,每份文件承担一段职责,对照关系如下:
| 文件 / 目录 | 在项目里的作用 |
|---|---|
main Resnet.py | ResNet 路线的训练主入口,负责数据加载、模型构建、训练循环 |
Resnet_UTIL.py | ResNet 配套工具,通常封装了预处理、可视化、评估辅助函数 |
SIFT_UTIL.py | SIFT 特征提取与匹配工具,走传统特征工程路线 |
test.py | 测试脚本,加载训练好的模型,跑测试集并输出分类指标 |
README.md | 环境依赖、目录结构、运行顺序的说明文档 |
train.zip | 训练集压缩包 |
valid.zip | 验证集压缩包 |
test.zip | 测试集压缩包 |
training.zip/testing.zip | 大概率是 train / test 的冗余副本或按类别拆分后的分片,部署时以 README 的说明为准,优先使用 train / valid / test 三套 |
先盯一下main Resnet.py这个文件名:中间带一个空格。在命令行里直接python main Resnet.py会被 shell 拆成两个参数,十有八九报can't open file。常见做法是先重命名成main_resnet.py,或者在命令里给整个文件名加引号。这个问题我放到第 5 章避坑里再展开,但它值得你从第一步就留意。
2.2 三个 zip 的摆放逻辑:为什么必须按“根目录 / 类别 / 图片”组织
PyTorch 的torchvision.datasets.ImageFolder默认严格要求目录结构:根目录下每个子目录是一个类别,子目录里的图片文件属于这个类别,目录名就是标签名。也就是说,data/train下面的class_a、class_b这样的文件夹名称,会被按字母序映射成 0、1、2 这样的标签编号。
解压命令很简单,但解压到哪很关键:
mkdir -p data unzip train.zip -d data/train unzip valid.zip -d data/valid unzip test.zip -d data/test-d参数指定解压目标目录。解压完成后应该是这样的结构:
data/ ├── train/ │ ├── class_a/ │ │ ├── 001.jpg │ │ ├── 002.jpg │ │ └── ... │ └── class_b/ │ ├── 001.jpg │ └── ... ├── valid/ │ ├── class_a/ │ └── class_b/ └── test/ ├── class_a/ └── class_b/为什么训练、验证、测试三套数据要物理隔离?训练集负责学习参数,验证集用于监控过拟合、调整超参数,测试集只在最终评估时跑一次。如果把验证集和测试集混着用,最终报出的准确率会虚高,答辩时“测试数据是不是参与过调参”这个问题一戳就破。这也是我在处理这类项目时一定会先跟 README 核对的部分,别指望脚本自己替你分好。
2.3 先做数据检查再训练:路径与标签对齐的检查脚本
数据摆放完毕,在敲训练命令之前,我会先跑一段检查脚本,确认三件事:每个类别目录下确实有图片、类别名没有中文或空格、训练集与测试集的类别集合一致。这段脚本可以直接保存成check_data.py使用:
import os def inspect_split(root): if not os.path.isdir(root): print(f'[缺失] {root} 不存在') return set() classes = set() for cls in sorted(os.listdir(root)): cls_path = os.path.join(root, cls) if not os.path.isdir(cls_path): continue imgs = [f for f in os.listdir(cls_path) if f.lower().endswith(('.jpg', '.jpeg', '.png', '.bmp'))] print(f'{root}/{cls}: {len(imgs)} 张') if len(imgs) == 0: print(f' [警告] {cls} 目录下没有任何图片文件') if any(ord(c) > 127 for c in cls): print(f' [警告] 类别名包含非 ASCII 字符: {cls}') classes.add(cls) return classes train_classes = inspect_split('data/train') valid_classes = inspect_split('data/valid') test_classes = inspect_split('data/test') if train_classes != test_classes: print('[错误] 训练集与测试集类别不一致') print(' 仅在训练集:', train_classes - test_classes) print(' 仅在测试集:', test_classes - train_classes)这个脚本的逻辑分三层:第一层判断目录是否存在,避免路径写错还傻等报错;第二层遍历每个类别目录,统计图片数量,并用endswith过滤出常见图片扩展名;第三层做集合比对,训练集和测试集的类别必须完全一致,否则模型最后一层全连接的输出维度会跟测试标签对不上,加载权重时直接抛错。
参数上要注意两点:endswith接收的是一个元组,因此jpg、jpeg、png、bmp都会匹配;ord(c) > 127用来检测中文在内的非 ASCII 字符。类目含中文不是不能跑,ImageFolder也能生成标签,但 zip 在不同操作系统间解压时中文目录名极易出现乱码或编码不一致,训练集和测试集只要有一边解压结果不同,标签编号就全错位了,这属于典型的“训练看着正常、测试一塌糊涂”的隐蔽问题。
注意:如果笔记本内存小于 8GB,这里先别急着开训练,往下看第 5 章的 DataLoader 那一条,能帮你避开训练到一半进程被杀的问题。
3. SIFT 特征提取与匹配:手工特征这条线为什么能跑出分类结果
深度学习模型确实在掌纹识别上表现更好,但毕设项目只放一条路线,答辩时“为什么不用另一种方法”这个问题很难答。SIFT_UTIL.py 的存在价值就在这里:它是一条完整的手工特征路线,能跟 ResNet 的结果做对比,也能让项目里的特征提取环节变得肉眼可见、可以画图解释。
3.1 选型理由:SIFT 为什么适合掌纹图像
SIFT(Scale-Invariant Feature Transform)做的事可以压缩成四步:构建高斯尺度空间金字塔、在金字塔中检测关键点、为关键点分配主方向、生成 128 维描述子。掌纹图像的特点是纹理密集、线纹走向复杂,SIFT 对尺度变化和旋转具有一定不变性,对褶皱、乳突纹这类局部纹理的响应比较稳定,所以它在掌纹这种“背景单纯、纹理丰富”的图像上比很多手工特征更好用。
为什么不用 ORB 或 Surf?常见做法是优先 SIFT,原因是它的描述子维度高、判别力强,匹配结果更适合拿来当作分类依据;ORB 更快但误匹配率偏高,Surf 有专利限制,在毕设这种要讲清楚出处的场景里 SIFT 最稳。注意这里指的是 OpenCV 的SIFT_create,open-source 版本可以直接用,不存在授权问题。
3.2 SIFT_UTIL 的接口形态与特征提取参数调节
SIFT_UTIL.py 里通常会封装特征提取和匹配函数,常见的接口形态是extract_sift_features(image_path)和match_two_images(desc1, desc2)这样的调用方式。如果 README 里没细说参数,我建议你按下面这套模板去检查和改写:
import cv2 sift = cv2.SIFT_create( nfeatures=300, # 每张图保留的关键点数量上限 nOctaveLayers=3, # 高斯金字塔每组内的层数 contrastThreshold=0.04, # 对比度阈值,低于该值的弱关键点被丢弃 edgeThreshold=10, # 边缘响应阈值,用于过滤细长边缘上的点 ) def extract_sift_features(img_path): # 掌纹识别不需要颜色信息,灰度图能显著加快关键点检测 img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) if img is None: return None, None kps, des = sift.detectAndCompute(img, None) return kps, des参数说明:nfeatures=300表示最多保留 300 个关键点,掌纹图像用这个量级足够,关键点太多不仅慢,还会引入大量背景干扰点;contrastThreshold控制低对比度点的过滤强度,默认 0.04,如果图像偏模糊,可以降低到 0.02 试试,代价是特征点数量会上升;edgeThreshold控制边缘响应的过滤,数值越小越严格,对掌纹这种纹理密集的图像,设在 10 附近比较合适。如果检测出来的特征点大量集中在手背景轮廓而不是掌纹纹路,打开nOctaveLayers到 4 或者先做一次 CLAHE 直方图均衡化,通常能救回来。
3.3 匹配分类:比值测试 0.7 与最近邻投票逻辑
拿到描述子之后怎么分类?常见流程是:把测试图与每个类别的模板图逐一匹配,匹配点数量最多的类别胜出。模板图可以从训练集每类里选几张,多张模板做投票能有效降低单张图质量波动的影响。
import cv2 def match_score(desc_query, desc_template, ratio=0.7): # 任一描述子为空时直接返回 0,避免空指针 if desc_query is None or desc_template is None: return 0 if len(desc_query) == 0 or len(desc_template) == 0: return 0 matcher = cv2.BFMatcher() # k=2 表示找最近邻和次近邻,用来做比值筛选 matches = matcher.knnMatch(desc_query, desc_template, k=2) good = [] for m, n in matches: # Lowe 论文中的经典比值测试:最近邻距离显著小于次近邻才算可靠匹配 if m.distance < ratio * n.distance: good.append(m) return len(good)这里最难讲清楚的是ratio=0.7。它的原理来自 Lowe 2004 年提出的比值测试:对查询点来说,如果最近邻距离和次近邻距离非常接近,说明这个点在模板里有多个相似的候选位置,匹配是模糊的、不可靠的;如果最近邻距离明显更小,才是“一眼认出”的稳定匹配。比值越小筛选越严格,0.7 是论文里的经验值,实际项目中可以在 0.6 到 0.8 之间调。返回的good列表长度就是两个描述子集的可靠匹配数,拿它作为相似度分数,比直接用平均距离稳定得多,因为平均距离会被少数噪声点拉偏。
3.4 这条线的效果边界与改进方向
实验做完你会发现,SIFT 路线的准确率通常低于深度学习路线,这很正常。掌纹图像如果光照不均、指缝阴影明显,SIFT 特征点会大量落在阴影边缘上,匹配结果被这些背景点主导。常见改进是加一步 RANSAC 几何校验,用基础矩阵过滤掉不符合单应关系的误匹配点;或者把描述子做 PCA 降维到 64 维,减少冗余同时还能提速。把这几个改进方案写进论文里的“进一步工作”,比单纯贴准确率更能体现对这条线的理解。
4. ResNet 分类训练与评估:从迁移学习到准确率上线的完整流程
SIFT 处理的是“怎么把像素变成特征”,ResNet 处理的是“怎么让网络自己学特征”。main Resnet.py 这条线是项目的核心,从数据加载到模型评估是一整套流程,下面按工程习惯拆开讲。
4.1 残差结构为什么适合小数据量的掌纹分类
ResNet 的核心是残差块:输入 x 经过两层卷积得到 F(x),输出不是 F(x) 而是 F(x) + x。这条恒等映射让深层网络的梯度可以直接回流到浅层,解决了网络加深后梯度消失的问题,因此可以堆到很深的层数而不退化。
对掌纹这种中小规模数据集来说,从零训练深网络很容易过拟合,所以常规做法是加载 ImageNet 预训练权重,把最后一层全连接替换成自己数据集的类别数,只微调后面的层。预训练模型的浅层学到的都是边缘、纹理这类通用特征,掌纹的线纹结构恰好也用得上这部分。唯一要改的就是输出维度:原来全连接层输出 1000 类,现在要变成len(train_ds.classes)。
4.2 main Resnet.py 里的训练流程与关键参数怎么定
训练入口一般长这样,对应的就是main Resnet.py的核心逻辑:
import torch import torch.nn as nn from torchvision import models, transforms, datasets from torch.utils.data import DataLoader transform_train = transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(p=0.5), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ]) train_ds = datasets.ImageFolder('data/train', transform=transform_train) train_dl = DataLoader(train_ds, batch_size=32, shuffle=True, num_workers=4) model = models.resnet18(pretrained=True) model.fc = nn.Linear(model.fc.in_features, len(train_ds.classes))逻辑说明:输入必须缩放到 224×224,因为预训练权重是在 ImageNet 224×224 图像上训练出来的;归一化用的均值[0.485, 0.456, 0.406]和方差[0.229, 0.224, 0.225]也是 ImageNet 的统计值,换数据集后不需要改,改了反而会让预训练权重首层输入失真。model.fc.in_features是把原全连接层输入维度取出来,再拼一个新的输出层,维度等于类别数,这一步是整个迁移学习的按钮。
训练循环部分,常见的配置如下:
criterion = nn.CrossEntropyLoss() optimizer = torch.optim.SGD(model.parameters(), lr=0.001, momentum=0.9, weight_decay=5e-4) for epoch in range(50): model.train() running_loss = 0.0 for inputs, labels in train_dl: optimizer.zero_grad() outputs = model(inputs) loss = criterion(outputs, labels) loss.backward() optimizer.step() running_loss += loss.item() * inputs.size(0) epoch_loss = running_loss / len(train_ds) print(f'epoch {epoch + 1}, loss: {epoch_loss:.4f}')参数说明:lr=0.001是迁移学习的常见起点,比从零训练小一个量级,因为预训练权重已经比较接近最优解,步子太大容易把学好的特征破坏掉;momentum=0.9让梯度更新更平稳;weight_decay=5e-4做 L2 正则,对掌纹这种样本量有限的任务能压住过拟合。50 个 epoch 对中小数据集通常是够用的,如果 loss 在最后十几个 epoch 基本不动,早停比硬跑完更划算。如果跑 10 个 epoch 后训练集 loss 还在 1.0 以上不降,先核对学习率,再核对数据增强是不是太重了。
4.3 test.py 的推理流程与分类评估指标
训练完成后,test.py 加载保存的模型权重,跑测试集输出指标。推理阶段的核心代码就几行:
model.eval() correct = 0 total = 0 with torch.no_grad(): for inputs, labels in test_dl: outputs = model(inputs) _, preds = torch.max(outputs, dim=1) total += labels.size(0) correct += (preds == labels).sum().item() accuracy = correct / total print(f'Test Accuracy: {accuracy * 100:.2f}%')model.eval()会切换 BatchNorm 和 Dropout 的行为,BatchNorm 用全局统计量而非当前 batch 统计量,Dropout 直接不生效,这一步忘了做,测试指标会比训练时的验证指标明显偏低。torch.no_grad()关闭自动求导图的构建,推理显存占用更小,速度也更快。
只看准确率是不够的,尤其是掌纹数据集类别不均衡时,准确率会被多数类带偏。常见的做法是补一张分类评估报告:
from sklearn.metrics import classification_report print(classification_report(y_true, y_pred, target_names=class_names))y_true和y_pred需要按样本顺序收集到一个列表里,不要跨 batch 乱序。classification_report会同时给出每个类别的精确率、召回率、F1 和支持样本数,答辩时这张表比单个准确率数字有说服力得多。类目少的那个类如果召回率明显低,说明该类样本不足或者特征与其他类重叠,这也是论文里可以展开讨论的点。
5. 避坑指南:数据、路径、显存与标签的五处翻车现场
这部分是拆项目过程中最容易浪费时间的部分,每一条都来自真实运行中会撞上的问题。按“现象 → 原因 → 解决”写,你可以直接把解决代码抄走。
5.1 DataLoader worker 进程被杀,报错却指向路径不存在
现象:训练刚启动,终端打印一条DataLoader worker (pid 12345) is killed by signal,紧接着报FileNotFoundError,但你检查路径发现文件明明存在。
原因:num_workers=4或更大时,PyTorch 会 fork 出多个子进程加载数据。每个子进程都会复制一部分数据到内存,机器物理内存不够时系统直接把 worker 进程杀死,随后主进程再访问一个没能正常加载的数据项时就报路径错误。路径异常只是表象,内存不足才是根因。
解决:内存小的机器先把num_workers改成 0,让主进程直接加载数据;再不行把batch_size从 32 降到 16。
train_dl = DataLoader(train_ds, batch_size=16, shuffle=True, num_workers=0, pin_memory=False)num_workers=0意味着不用子进程,内存占用直线下降,代价是数据加载变慢;如果数据量不大,这点差距完全可以接受。pin_memory=True在大多数场景能提一点速度,但内存本来就紧张时先关掉。
5.2main Resnet.py这个文件名在命令行里就是跑不起来
现象:执行python main Resnet.py,提示can't open file或直接报No such file or directory;有的 shell 会把它当成两个参数,Python 只会尝试打开main。
原因:文件名里有个空格,shell 按空白字符切分参数,main Resnet.py被拆成了main和Resnet.py两段。
解决:重命名是最省事的方式。
mv "main Resnet.py" main_resnet.py如果不想动原文件名,命令行里用引号包住整个文件名也行:
python "main Resnet.py"但后续在代码里import或subprocess调用这个文件时同样会被空格坑,所以统一改成下划线命名是长期省心的方案。
5.3 SIFT 提取不到特征点,des直接返回 None
现象:调用sift.detectAndCompute之后des为None,或者len(kps)只有个位数,匹配分数永远是 0。
原因:掌纹图像如果不是标准的灰度格式,读取时颜色通道会干扰关键点检测;更常见的是contrastThreshold设得偏高,掌纹纹路和背景的对比度本来就低,弱关键点全被过滤掉。图像太模糊或者背景复杂也是同款表现。
解决:先确认用cv2.IMREAD_GRAYSCALE读图,再做一次 CLAHE 增强,最后把contrastThreshold降下去。
img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)) img = clahe.apply(img) sift = cv2.SIFT_create(nfeatures=300, contrastThreshold=0.02, edgeThreshold=15) kps, des = sift.detectAndCompute(img, None)clipLimit=2.0控制对比度增强强度,太高会把噪声也放大;tileGridSize是分块大小,掌纹这种纹路细密的图像用默认 8×8 就好。调参到这一步如果关键点还是稀疏,再看一眼原图是不是有大量背景区域抢占了检测器注意力,裁剪掉无关背景比调参数更直接。
5.4 训练集准确率很高,测试集准确率却低得离谱
现象:训练日志里准确率到了 95% 以上,test.py一跑掉到 60% 以下,怎么看都像过拟合,但换了小模型也没用。
原因:大概率是训练集和测试集的类别目录名在解压后不一致,ImageFolder按字母序生成标签索引,两边只要有一个类目名编码不同,同一个标签编号对应的类就是错的。中文目录名跨平台解压成乱码是最常见的导火索。
解决:统一重命名三套数据里的类目录,保持名称完全一致,然后重跑 2.3 的检查脚本确认集合相等。
import os for root in ['data/train', 'data/valid', 'data/test']: if not os.path.isdir(root): continue for name in os.listdir(root): new_name = name.strip().replace(' ', '_') if new_name != name: os.rename(os.path.join(root, name), os.path.join(root, new_name))strip()去掉首尾空白,replace(' ', '_')把空格换成下划线,中文目录名在重命名时顺手改成拼音或英文即可。改完务必重新走一遍检查脚本,确认train_classes == test_classes。
5.5 GPU 不可用:torch 版本与驱动不匹配
现象:程序能跑,但训练速度比预计慢很多,查看日志发现torch.cuda.is_available()返回False,模型全程在 CPU 上跑。
原因:pip 默认安装的 torch 是 CPU 版本;或者本机显卡驱动太老,torch 要求的 CUDA 版本比驱动支持的新。
解决:先跑一段探针代码确认环境:
import torch print(torch.__version__) print(torch.cuda.is_available())如果is_available()为False,需要到 PyTorch 官网按本机 CUDA 版本重新生成安装命令。注意如果本机 python 安装版本太新,torch 可能还没有对应的预编译包,常见做法是把 python 环境降到 3.8~3.10 区间再装。装完重跑上面两行,确认输出True再开始训练。
注意:保模型权重时不要只存
state_dict,把类别名列表class_names一起存成 json,下次加载测试时标签顺序才不会对不上。
6. 让答辩不再被问倒:混淆矩阵与关键点可视化的准备习惯
准确率是一个数字,但评委真正想看的是你对结果的理解。我在处理这类项目时,最后总会补两样东西:混淆矩阵和 SIFT 匹配可视化。前者暴露类别间的混淆规律,后者把特征提取的过程变成肉眼可见的证据。
from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay import matplotlib.pyplot as plt cm = confusion_matrix(y_true, y_pred) disp = ConfusionMatrixDisplay(cm, display_labels=class_names) disp.plot(cmap='Blues') plt.savefig('confusion_matrix.png', dpi=150)y_true和y_pred需要在 test.py 里按样本顺序各自收集成一个列表,不能只留累计正确数。这张图能直接告诉你哪些类互相认错,答辩时被问“错误的点集中在哪”,可以指着图答:这两类掌纹纹理本来就接近,误判集中在它们之间。这比笼统地说“整体效果不错”扎实得多。
SIFT 路线的可视化更有演示效果。把两张测试图的 SIFT 关键点画出来并排对比,同类图匹配点多,异类图匹配点稀少,这个视觉差异当场就能讲清楚手工特征的分类依据:
img = cv2.imread('sample.jpg') kps, _ = extract_sift_features('sample.jpg') vis = cv2.drawKeypoints(img, kps, None, color=(0, 255, 0)) cv2.imwrite('sift_vis.jpg', vis)drawKeypoints会在原图上把关键点画成彩色圆圈,color=(0, 255, 0)是 BGR 空间下的绿色,保存下来的sift_vis.jpg可以直接贴进论文。Run 项目时把训练日志、验证集准确率曲线截图、混淆矩阵图统一放进 workspace,答辩现场按时间线展示,比你临时敲命令要稳得多。
我从第一次做这类毕设资源复盘时就被拖过进度:跑完test.py以为万事大吉,结果被问“测试图从哪批数据里选的”,当时答不上来。从那以后,每次交付类似项目我都强制走一遍完整流程——检查三个 split 的类别一致性、用训练好的模型重跑一次验证集、把混淆矩阵截图归档,确认日志里每个数字都能对上,再谈“跑通了”。这套习惯救了我好几次。希望这份拆解也能帮你把项目跑得明白,答辩站得住。
本文还有配套的精品资源,点击获取