简介:这份资源是面向高校计算机、人工智能及相关专业学生的非接触式掌静脉识别毕业设计完整方案,适合需要完成毕设、期末大作业或课程设计的学习者。项目以深度学习为核心,实现掌静脉图像采集、预处理、特征提取与身份识别等完整流程,代码注释详尽,新手也能理解部署。压缩包共约2000个文件,包含1980张bmp掌静脉样本图像、7个Python源码文件、5个xml配置、3份pdf论文文档以及1个pt模型权重,整体约209.79MB,覆盖数据、代码、模型与论文全链路。目前已有221人学习下载。读者可获得可直接运行的完整工程、训练好的模型、配套论文与清晰目录结构,便于快速复现实验、撰写论文并应对答辩,具有较高的实际应用与参考价值。
1. 掌静脉识别毕设:从一段红外手掌视频到能跑通的 Python 工程
手掌摊开,摄像头在十几厘米外按下快门,红外光穿透表皮,在皮下 1~3 毫米的静脉血管网里被血红蛋白吸收,成像后留下一张灰蒙蒙、布满暗色分叉纹路的图——这就是掌静脉识别要处理的原图。它和指纹、人脸最大的区别在于:静脉藏在皮肤下面,不接触、不磨损、难以伪造,所以非接触式掌静脉识别这几年在门禁、支付、考勤场景里被反复提起。如果你正在做基于深度学习的掌静脉识别毕业设计,手里大概率只有一份任务书和“Python 源码 + 论文”的目标,却不知道从哪张图开始、模型怎么选、论文里的实验表格怎么填。这篇笔记就按一线做项目的顺序,把数据、预处理、模型、训练、评估、论文写作这条链路拆开讲清楚,让新手能照着复现,熟手能看到参数边界和踩坑点。
2. 掌静脉数据从哪来:ROI 提取与数据集构建的完整流程
做深度学习项目,第一件事不是搭网络,而是把数据搞干净。掌静脉识别的原始图里,手掌位置、角度、大小都不固定,直接喂给网络,模型会去学背景而不是血管。所以中间必须插一步 ROI(感兴趣区域)提取,把手掌摆正、裁出固定尺寸的静脉区域。这一步做得好不好,直接决定后面模型能不能收敛。
2.1 为什么 ROI 提取是掌静脉识别的第一道命门
掌静脉图像的特点是低对比度、血管细、噪声多。如果 ROI 没对齐,同一个人的两张图在像素层面对不上,网络学到的就是“手掌在画面哪个位置”这种无用特征,而不是静脉纹理。常见做法是先用阈值分割或边缘检测找到手掌轮廓,再根据关键点(比如指缝谷点)做仿射变换,把掌心区域旋转到标准姿态,最后裁成 128×128 或 224×224。
我一般会走这条链路:灰度化 → 高斯滤波去噪 → 自适应阈值二值化 → 形态学闭运算补洞 → 找最大轮廓 → 定位指缝谷点 → 仿射校正 → 裁 ROI。指缝谷点定位是玄学重灾区,光照一变,轮廓就飘,所以后面要加一步基于距离变换的掌心圆拟合来兜底。
import cv2 import numpy as np def extract_roi(img_path, size=128): # 读灰度图,掌静脉原始图通常是单通道红外图 img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) # 高斯滤波,核大小 5x5 对细血管够用,太大会把血管抹掉 blur = cv2.GaussianBlur(img, (5, 5), 0) # 自适应阈值,blockSize 取 35 左右,C 取 5,适应不同光照 binary = cv2.adaptiveThreshold(blur, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 35, 5) # 闭运算连接断裂的手掌区域 kernel = np.ones((7, 7), np.uint8) closed = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel) # 找最大轮廓,假设手掌是画面里最大的连通区域 contours, _ = cv2.findContours(closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) palm = max(contours, key=cv2.contourArea) # 用最小外接矩形做粗校正,旋转到水平 rect = cv2.minAreaRect(palm) box = cv2.boxPoints(rect) box = np.int0(box) # 计算旋转角度并做仿射变换 angle = rect[-1] if angle < -45: angle = 90 + angle M = cv2.getRotationMatrix2D(rect[0], angle, 1.0) rotated = cv2.warpAffine(img, M, (img.shape[1], img.shape[0])) # 以掌心为中心裁固定大小 ROI cx, cy = int(rect[0][0]), int(rect[0][1]) half = size // 2 roi = rotated[max(0, cy-half):cy+half, max(0, cx-half):cx+half] # 统一尺寸,防止边界越界导致尺寸不一致 roi = cv2.resize(roi, (size, size)) return roi这段代码的逻辑是:先二值化把手掌从背景里抠出来,再用最小外接矩形估计手掌朝向并旋转校正,最后以掌心为中心裁固定窗口。参数上,blockSize=35和C=5是经验值,光照偏暗时把 C 调大,偏亮时调小;kernel用 7×7 是为了补上手指之间的缝隙,太小补不上,太大容易把相邻区域粘一起。失败时先看二值图,如果手掌和背景糊成一片,说明阈值参数要重调,或者原始图对比度太低需要先做 CLAHE 增强。
2.2 数据集划分与增强:别让模型记住“这个人”
掌静脉公开数据集不像人脸那么丰富,很多毕设用的是实验室自采数据或者几个公开库的组合。不管来源如何,划分必须按“人”来分,不能按“图”随机分。如果同一个人的图同时出现在训练集和测试集,准确率能刷到 99%,但这是自欺欺人,答辩时老师一问“跨人测试多少”就露馅。
常见做法是:每人取若干张,按 7:2:1 分训练、验证、测试,且同一个人的所有图只进一个集合。数据量少的时候,增强是必须的,但掌静脉的增强要克制。随机旋转 ±10 度、平移 5%、亮度微调是可以的;水平翻转要慎用,因为手掌左右手静脉分布不对称,翻转会造出不存在的人。下面是一个用torchvision做增强的配置片段。
from torchvision import transforms train_transform = transforms.Compose([ transforms.ToPILImage(), transforms.RandomRotation(10), # 小角度旋转,模拟手部姿态变化 transforms.RandomAffine(degrees=0, translate=(0.05, 0.05)), # 平移 transforms.ColorJitter(brightness=0.2, contrast=0.2), # 亮度对比度扰动 transforms.Resize((128, 128)), transforms.ToTensor(), transforms.Normalize(mean=[0.5], std=[0.5]) # 单通道归一化 ]) val_transform = transforms.Compose([ transforms.ToPILImage(), transforms.Resize((128, 128)), transforms.ToTensor(), transforms.Normalize(mean=[0.5], std=[0.5]) ])这里RandomRotation(10)的 10 度是上限,再大血管纹理就变形了;ColorJitter的 brightness 和 contrast 都控制在 0.2,因为掌静脉本身对比度就低,扰动太猛会把血管信号淹掉。归一化用 0.5 均值和方差是单通道图的通用做法,如果你用的是三通道伪彩色图,就改成 ImageNet 的均值和方差。注意增强只加在训练集,验证和测试集只做 resize 和归一化,保证评估结果可复现。
3. 模型选型:CNN、Siamese 还是 Transformer,毕设该怎么定
数据准备好之后,下一个问题是网络结构。掌静脉识别本质上是细粒度分类或者度量学习问题:同一个人不同角度的图要靠近,不同人的图要远离。毕设里常见的三种路线是:直接当分类任务训 CNN、用 Siamese 网络做配对度量、或者上 Vision Transformer 做特征提取。选哪个,取决于你的数据量、算力和论文想讲的故事。
3.1 分类 CNN 与 Siamese 网络的取舍
如果你的数据集里类别数固定(比如 100 个人),每人样本量在 10 张以上,直接训一个分类 CNN 是最省事的。 backbone 用 ResNet18 或 MobileNetV2,把最后一层全连接改成类别数,交叉熵损失一拉,就能跑。优点是代码简单、训练稳定、准确率好看;缺点是新增一个人就要重新训,泛化性在论文里不好吹。
Siamese 网络走的是另一条路:输入一对图,共享权重提取特征,用对比损失或三元组损失拉近同类、推远异类。它的好处是学到的特征可以直接算距离,适合开集识别和少样本场景,论文里也更容易讲“度量学习”的故事。代价是训练时样本对采样麻烦,三元组挖掘不好会导致模型塌缩。我一般建议:数据量小、想强调泛化,选 Siamese;数据量够、想快速出结果,选分类 CNN。
import torch import torch.nn as nn import torchvision.models as models class PalmVeinCNN(nn.Module): def __init__(self, num_classes=100): super().__init__() # 用 ResNet18 做 backbone,改成单通道输入 self.backbone = models.resnet18(pretrained=True) # 原始第一层是 3 通道,替换成 1 通道 self.backbone.conv1 = nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3, bias=False) # 替换最后的全连接层 self.backbone.fc = nn.Linear(512, num_classes) def forward(self, x): return self.backbone(x) class SiameseNet(nn.Module): def __init__(self): super().__init__() self.backbone = models.resnet18(pretrained=True) self.backbone.conv1 = nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3, bias=False) # 去掉原分类头,输出 512 维特征 self.backbone.fc = nn.Identity() def forward_one(self, x): return self.backbone(x) def forward(self, x1, x2): f1 = self.forward_one(x1) f2 = self.forward_one(x2) return f1, f2分类网络的改动点就两个:第一层卷积改成单通道,最后一层全连接改成你的类别数。Siamese 网络则把全连接换成Identity,直接输出 512 维特征向量,后面接距离计算。参数上,pretrained=True在数据量少的时候能救命,但要注意 ImageNet 预训练权重是三通道的,改成单通道后第一层相当于重新学,可以先把 backbone 冻结几轮再解冻。如果显存不够,把 ResNet18 换成 MobileNetV2,参数量从 11M 降到 3.5M,精度掉一两个点但能跑起来。
3.2 训练参数怎么设:学习率、batch size 与损失函数
训练参数没有万能公式,但掌静脉这个任务有一些经验区间。学习率用 1e-3 配 Adam,或者 1e-2 配 SGD 加 momentum 0.9;batch size 在 32 到 64 之间,太小梯度噪声大,太大显存吃不消。损失函数方面,分类任务用交叉熵,Siamese 用对比损失或者三元组损失。三元组损失的 margin 一般设 0.3 到 0.5,太小起不到推远作用,太大训练不稳定。
import torch.optim as optim from torch.optim.lr_scheduler import StepLR # 分类任务配置 model = PalmVeinCNN(num_classes=100) criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-4) # 每 20 个 epoch 学习率乘 0.1 scheduler = StepLR(optimizer, step_size=20, gamma=0.1) # Siamese 任务配置 # siamese = SiameseNet() # criterion = nn.TripletMarginLoss(margin=0.4) # optimizer = optim.Adam(siamese.parameters(), lr=5e-4)weight_decay=1e-4是轻量正则,防止过拟合;StepLR在 20 轮后降学习率,让模型在后期精细收敛。如果是 Siamese,学习率我一般降到 5e-4,因为配对训练梯度方差更大,学习率高了容易震荡。训练时盯着验证集准确率或者等错误率(EER),如果训练损失降但验证损失升,就是过拟合,加 dropout 或者早停。显存不够时把 batch size 减半,学习率也相应乘 0.5,这是线性缩放规则,能保持训练动态大致不变。
4. 训练与评估:把准确率、EER 和混淆矩阵跑出来
模型定义好之后,训练循环本身不复杂,难的是评估指标要算对。掌静脉识别论文里常出现的指标有准确率、等错误率(EER)、ROC 曲线和混淆矩阵。准确率只适合类别均衡的分类任务;如果是开集识别或者验证任务,EER 更能说明问题。这一章把训练循环和评估代码写清楚,让你能直接填进论文表格。
4.1 训练循环与验证集监控
训练循环的标准结构是:前向传播、算损失、反向传播、更新参数,每轮结束在验证集上跑一次。下面是一个精简但完整的训练框架,包含训练和验证两个阶段。
def train_one_epoch(model, loader, criterion, optimizer, device): model.train() total_loss, correct, total = 0, 0, 0 for imgs, labels in loader: imgs, labels = imgs.to(device), labels.to(device) optimizer.zero_grad() outputs = model(imgs) loss = criterion(outputs, labels) loss.backward() optimizer.step() total_loss += loss.item() * imgs.size(0) preds = outputs.argmax(dim=1) correct += (preds == labels).sum().item() total += imgs.size(0) return total_loss / total, correct / total def validate(model, loader, criterion, device): model.eval() total_loss, correct, total = 0, 0, 0 with torch.no_grad(): for imgs, labels in loader: imgs, labels = imgs.to(device), labels.to(device) outputs = model(imgs) loss = criterion(outputs, labels) total_loss += loss.item() * imgs.size(0) preds = outputs.argmax(dim=1) correct += (preds == labels).sum().item() total += imgs.size(0) return total_loss / total, correct / totalmodel.train()和model.eval()的切换很关键,前者启用 dropout 和 batch norm 的训练模式,后者固定它们。验证阶段用torch.no_grad()关掉梯度,省显存也加速。每轮记录训练损失、训练准确率、验证损失、验证准确率,画成曲线放进论文,能直观看出有没有过拟合。如果验证准确率在某个 epoch 后不再上升,就触发早停,保存验证集上最好的模型权重。
4.2 EER 与 ROC:验证任务的核心指标怎么算
当你的任务变成“给两张图判断是不是同一个人”时,准确率就不够用了。这时候要算相似度分数,然后扫一遍阈值,画 ROC 曲线,找等错误率。EER 是假接受率(FAR)和假拒绝率(FRR)相等时的错误率,越低越好。下面这段代码演示怎么从特征向量算 EER。
import numpy as np from sklearn.metrics import roc_curve def compute_eer(genuine_scores, impostor_scores): # genuine_scores: 同类样本对的相似度分数列表 # impostor_scores: 异类样本对的相似度分数列表 scores = np.concatenate([genuine_scores, impostor_scores]) # 同类标 1,异类标 0 labels = np.concatenate([np.ones(len(genuine_scores)), np.zeros(len(impostor_scores))]) fpr, tpr, thresholds = roc_curve(labels, scores) fnr = 1 - tpr # 找 FAR 和 FRR 最接近的点 eer_idx = np.nanargmin(np.abs(fnr - fpr)) eer = (fpr[eer_idx] + fnr[eer_idx]) / 2 return eer, fpr, tpr # 假设特征已经提取好,用余弦相似度算分数 def cosine_sim(f1, f2): f1 = f1 / np.linalg.norm(f1) f2 = f2 / np.linalg.norm(f2) return float(np.dot(f1, f2))roc_curve返回的 fpr 和 tpr 是扫描所有阈值得到的,fnr = 1 - tpr就是漏报率。EER 取 FAR 和 FRR 差值最小的那个点,两个错误率取平均。实际写论文时,把 EER 和准确率一起报,再附上 ROC 曲线图,说服力比单报准确率强得多。注意相似度分数要归一化,余弦相似度天然在 -1 到 1 之间,欧氏距离则要转成相似度(比如取负值)再送进roc_curve。
5. 避坑与排查:掌静脉毕设里最容易翻车的 5 个地方
做这个方向,代码跑通只是第一步,真正花时间的是调参和排错。下面这 5 个坑是我自己和身边人反复踩过的,按“现象 → 原因 → 解决”写清楚,你遇到时可以直接对照。
现象一:训练准确率很快到 99%,但测试集只有 60%。原因:数据划分时按图随机分,同一个人的图同时进了训练和测试,模型在背答案。 解决:改成按人划分,确保同一个人的所有图只出现在一个集合里。如果数据量太少,用 k 折交叉验证,按人分折。
现象二:损失不下降,准确率一直在随机水平。原因:学习率太大导致梯度爆炸,或者输入没有归一化,像素值在 0 到 255 之间直接喂进去。 解决:先把输入归一化到 0 到 1 或者减均值除方差;学习率从 1e-4 开始试,用torch.nn.utils.clip_grad_norm_裁剪梯度。
现象三:ROI 提取出来的图有的正有的歪,血管对不上。原因:指缝谷点定位受光照和手掌姿态影响,轮廓一飘,仿射变换就错。 解决:加掌心圆拟合兜底,用距离变换找掌心最大内切圆,以圆心为基准裁 ROI,比单纯靠轮廓稳。
现象四:Siamese 训练时损失直接降到 0,模型输出全一样。原因:三元组采样时负样本太容易区分,模型找到捷径,把所有图映射到同一个点。 解决:用半困难负样本挖掘,选那些距离比正样本远但还没远到 margin 之外的负样本;或者换对比损失,加正则项。
现象五:验证集 EER 很低,但换一批新数据就崩。原因:过拟合到采集设备的光照和噪声特征,模型学的是设备指纹而不是静脉纹理。 解决:增强里加亮度、对比度扰动,训练时用 dropout 和 weight decay,评估时留一个跨设备的测试集。
6. 论文写作与答辩:把工程结果翻译成学术语言
代码跑完、指标出来之后,最后一步是把结果写成论文。毕设论文的结构通常是:绪论、相关技术、方法、实验、结论。掌静脉识别的论文,实验章是重头戏,要把数据集、预处理、网络结构、训练参数、对比实验、消融实验都写清楚。我一般会准备三张表:一张是不同 backbone 的准确率和 EER 对比,一张是加不加 ROI 提取的消融,一张是和已有方法的对比。图方面,ROC 曲线、混淆矩阵、特征可视化(t-SNE)各一张,基本够用。
答辩时老师最爱问的三个问题:为什么选这个网络、数据量够不够、和别人的方法比优势在哪。提前把这三个问题的答案写成一句话,背熟。比如“选 ResNet18 是因为在 128×128 输入下参数量适中,预训练权重能缓解小样本问题;数据量每人 10 张,用增强扩到 3 倍;相比传统 LBP 方法,EER 从 8% 降到 3%。” 这种回答具体、有数字,比空谈“深度学习效果好”强得多。
最后一个习惯:所有实验跑完,把随机种子固定,把配置文件、训练日志、模型权重分目录存好。答辩前一周重新跑一遍主实验,确认结果能复现。我吃过亏,当时改了一行增强代码没记录,答辩前一天结果对不上,熬夜重训。希望帮到你。
本文还有配套的精品资源,点击获取