首页 / 资讯中心 / 文章详情

从BMP到ArcFace:基于AR Database的人脸识别预处理与训练实战

从BMP到ArcFace:基于AR Database的人脸识别预处理与训练实战 ★ FEATURED ARTICLE
简介AR DatabaseAR数据库彩色裁剪图像数据集专供人脸识别、稀疏表示及光照处理等研究使用弥补了CSDN现有相关资源多为灰度图的空白。相比常见灰度AR库彩色图像完整保留人脸纹理与光照信息更贴近真实识别场景。资源基于经典论文“PCA versus LDA”中的处理流程每张人脸图像均附带裁剪区域txt坐标与裁剪后的bmp彩色图像方便直接进行特征提取、分类器设计或稀疏编码实验省去自行裁剪与对齐的预处理成本。压缩包共5201个文件包括2600个txt、2600个bmp以及1个m文件整体大小115.3MB目录结构清晰可在MATLAB等环境中快速构建AR库对照实验。目前已有510人学习下载作为少见的彩色版AR数据集特别适合计算机视觉领域的人脸识别与光照鲁棒性研究者用于论文复现和算法验证。1. AR Database拿到裁剪好的彩色 BMP 人脸图后先做什么人脸识别的第一道坎不是网络结构而是数据。很多团队调研时直接去抓大而全的通用数据集结果一张图动辄几 MB还要自己检测、对齐、裁剪做数据清洗就得耗掉一周。AR Database 的价值在于它把“裁剪后的彩色人脸图像”直接摆在你面前还是 bmp 格式无压缩、无二次损失适合快速验证识别算法。无论你是要做门禁机这类人脸识别硬件的原型验证还是想对比不同人脸识别算法的边界这份数据都能让你跳过采集和裁剪步骤。下面这份流程按我实际工作的经验整理新手照练就能跑通熟手也能从中看到参数边界和预处理坑。2. 整理裁剪后的 BMP 人脸图目录结构、批处理与标签生成拿到 AR Database 的裁剪版后最常见的问题是目录结构不统一。有的镜像包按“人 ID”拆好了目录有的则把所有人脸图平铺在一个文件夹里。在动手训练之前先把它规整成“人能看懂、后续代码能直接迭代”的结构否则后面切换训练脚本时每回都要改路径映射。2.1 按人分子目录并保留拍摄条件信息AR Database 的采集本身就考虑了多种变化同一个人的中性表情、微笑、愤怒、尖叫以及左侧光、右侧光、全光、墨镜遮挡、围巾遮挡等条件。预处理裁剪版一般把这些图统一缩放并转成 bmp但文件名里往往还带着条件后缀。我一般会整理成下面这种结构AR_face/ Subject01/ subject01_neutral.bmp subject01_smile.bmp subject01_leftlight.bmp subject01_sunglasses.bmp subject01_scarf.bmp Subject02/ subject02_neutral.bmp subject02_smile.bmp ...不推荐把所有人脸图塞进同一个目录哪怕你的训练脚本只关心“路径 标签”。原因有两点第一后续做遮挡泛化实验时你得能按条件把样本筛出来第二按人分组做训练集/测试集划分时GroupKFold 需要拿到人的 ID。如果目录平铺这两个信息都得靠文件名解析一个字符匹配错误就翻车。目录命名用“Subject01”这类带序号的格式比直接用中文名或姓名缩写稳妥。一是序号可以直接转 int 标签二是避免跨平台时中文编码不一致导致路径读取失败。2.2 批量读取 BMP 并统一尺寸不要直接拉伸裁剪后的 AR Database 图像来自不同预处理脚本尺寸可能并不是统一的。即便统一也未必匹配你后面要用的预训练模型输入。我见过有人直接cv2.resize(img, (112, 112))结果因为原图不是正方形人脸被横向或者纵向拉变形识别率跌好几个点。正确做法是等比例缩放再填充到目标大小import cv2 import numpy as np import pathlib SRC pathlib.Path(AR_face) DST pathlib.Path(processed_face_112) TARGET_SIZE 112 # ArcFace 类模型常见输入尺寸 for img_path in SRC.rglob(*.bmp): # AR Database 的 bmp 一般是 8 位彩色直接读成 BGR 三通道 img cv2.imread(str(img_path)) if img is None: print(f读取失败: {img_path}) # 偶尔会有 0 字节坏图跳过 continue h, w img.shape[:2] scale TARGET_SIZE / max(h, w) new_h, new_w int(h * scale), int(w * scale) resized cv2.resize(img, (new_w, new_h), interpolationcv2.INTER_AREA) # 用 114 填充到正方形接近人脸图像的平均亮度 canvas np.full((TARGET_SIZE, TARGET_SIZE, 3), 114, dtypenp.uint8) y_off (TARGET_SIZE - new_h) // 2 x_off (TARGET_SIZE - new_w) // 2 canvas[y_off:y_off new_h, x_off:x_off new_w] resized out_path DST / img_path.parent.name / img_path.name out_path.parent.mkdir(parentsTrue, exist_okTrue) cv2.imwrite(str(out_path), canvas)这段脚本做了三件事遍历所有子目录里的 bmp 文件按最长边等比缩放在正方形画布上居中放置。INTER_AREA适合下采样缩小尺寸时不容易出现锯齿如果从很小尺寸放大就要改用INTER_CUBIC否则人脸会发虚。TARGET_SIZE先取 112因为大部分人脸识别预训练模型都在这个分辨率上做的对齐和归一化。如果你的机器内存紧张用 64前提是下一步仅做快速验证最终再回到 112 跑正式训练。填充值用 114 是经验值接近人脸图像平均亮度不要用纯黑 0否则边缘会产生很强的梯度伪影干扰 HOG 特征。2.3 生成 CSV 标签把目录名和人 ID 对齐处理好图像后下一步是从目录结构自动生成标签文件。AR Database 的人 ID 是连续的但为了稳妥我建议在读目录名时做一次去重排序而不是直接拿range(1, N1)去猜避免某些版本缺了某个 ID 导致标签错位import csv import pathlib DST pathlib.Path(processed_face_112) rows [] for img_path in sorted(DST.rglob(*.bmp)): subject_id int(img_path.parent.name.replace(Subject, )) rows.append({ path: str(img_path), subject_id: subject_id, subject_name: img_path.parent.name, condition: img_path.stem }) with open(labels.csv, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnames[path, subject_id, subject_name, condition]) writer.writeheader() writer.writerows(rows) print(f共写入 {len(rows)} 张图像)这段逻辑很简单但有三个细节值得说。第一encodingutf-8-sig是为了方便在 Windows 的 Excel 里直接打开但后面用 pandas 读取时也要用同样的编码否则第一列列名会带\ufeff前缀匹配字段名时报 KeyError。第二condition字段从文件名里拆出来后续做分组评测时非常有用。第三保存成 CSV 而不是直接存 pickle是因为 CSV 是文本格式跨机器传输不挑版本别人拿到也能直接看。提示如果你拿到的 AR 版本是全单目录平铺、文件名里不留条件后缀那么在裁剪阶段就要先用眼睛坐标或默认框把人脸区域裁出来再按拍摄条件重命名。这个步骤没法跳过否则后面没法做遮挡和光照的独立评估。3. 先用传统方法跑通人脸识别HOG 特征与 SVM 的最快验证很多人拿到人脸数据集第一反应就是上深度模型但在 AR Database 上我强烈建议先跑一遍传统方法。原因很实在这套数据类别多、每类样本少直接训练 ResNet 很容易过拟合而传统方法在 CPU 上几分钟就能出结果能帮你先验证数据本身有没有问题——比如标签是否对齐、光照子集是否异常、遮挡子集是否真的覆盖了关键面部区域。3.1 为什么先跑 HOG SVM而不是直接上 CNN传统人脸识别的常见做法是手工特征 分类器。AR Database 的人脸是正面的、裁剪后的彩色图像尺度差异不大正好是 HOG 特征发挥稳定性的场景。HOG 提取局部梯度方向分布对人脸的轮廓、眼睛、鼻子边缘比较敏感SVM 在小样本分类上远优于线性回归或朴素贝叶斯。门禁机这类人脸识别设备在低算力芯片上也常保留 LBP 或 HOG 特征作为降级方案。先跑传统方法还有一个隐藏好处你可以借此确认这批 bmp 图像在读取、灰度化、直方图均衡化后语义信息是否完整。如果 HOG SVM 的准确率连 70% 都到不了多半是数据预处理环节出了问题而不是模型不行。3.2 完整管道读取 BMP、提取 HOG、训练 SVMimport cv2 import numpy as np import pandas as pd from sklearn.model_selection import train_test_split from sklearn.svm import SVC from sklearn.metrics import accuracy_score from skimage.feature import hog df pd.read_csv(labels.csv, encodingutf-8-sig) def extract_hog(img_path): # 灰度化后先做直方图均衡化削弱 AR 光照子集的亮度差异 img cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) if img is None: raise FileNotFoundError(img_path) img cv2.equalizeHist(img) feat hog( img, orientations9, pixels_per_cell(8, 8), cells_per_block(2, 2), block_normL2-Hys, feature_vectorTrue, ) return feat X np.array([extract_hog(p) for p in df[path]]) y df[subject_id].values X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, stratifyy, random_state42 ) clf SVC(kernelrbf, C10, gammascale, class_weightbalanced) clf.fit(X_train, y_train) y_pred clf.predict(X_test) print(准确率:, accuracy_score(y_test, y_pred))这段代码的调参重点在 HOG 参数和 SVM 的超参上。orientations9表示把梯度方向分成 9 个区间这是行人检测里的经典值对人脸也适用。pixels_per_cell(8, 8)意味着每个 cell 是 8x8 像素112x112 的图像会有 14x14 个 cell特征维度大约 6804SVM 跑起来不慢。如果原始图像缩小过且分辨率掉到 64x64 以下建议把 cell 改成 4x4否则信息被过度平均。SVC(kernelrbf, C10, gammascale)里C 控制误分类惩罚强度。AR Database 各类样本数接近均衡但如果你只取了遮挡子集训练类别不平衡就会出现所以才加class_weightbalanced。gammascale是让 sklearn 根据特征维度自动估算初期不用手调等准确率卡住时再拿GridSearchCV扫C[1, 10, 100]和gamma[0.001, 0.01, 0.1]。3.3 训练集与测试集划分按人分组别随机切直接随机切分在 AR Database 上是数据泄漏的重灾区。同一个人的微笑图和墨镜图会同时出现在训练集和测试集里模型等于记住了“这个人长什么样”而不是学会区分“这个人是谁”。真实的人脸识别门禁机部署时库里的人全是没见过的“陌生人”所以评估必须按人 ID 分组from sklearn.model_selection import GroupShuffleSplit groups df[subject_id].values splitter GroupShuffleSplit(n_splits1, test_size0.3, random_state42) train_idx, test_idx next(splitter.split(X, y, groupsgroups)) X_train, X_test X[train_idx], X[test_idx] y_train, y_test y[train_idx], y[test_idx]GroupShuffleSplit保证同一个subject_id的所有图像只落在训练集或测试集一个分区里。这样出来的准确率会比随机切分低几个百分点但更接近真实场景。还可以再做一层“遮挡泛化验证”训练时只用不戴围巾的样本测试时只用围巾子集。这能直观看出模型对大面积遮挡的鲁棒程度train_mask ~df[condition].str.contains(scarf, caseFalse) test_mask df[condition].str.contains(scarf, caseFalse) X_train, X_test X[train_mask], X[test_mask] y_train, y_test y[train_mask], y[test_mask]如果训练集不包含任何遮挡样本测试集全用围巾准确率通常会明显掉下来。这个数字不要藏起来它决定了后续深度学习方案要不要专门设计遮挡数据增强。4. 用 PyTorch 微调 ResNet ArcFace让遮挡和光照样本也能被分开传统方法在 AR Database 上跑通之后瓶颈会很快浮现围巾遮挡会大面积盖住嘴巴和下巴HOG 特征在这种区域里基本失效SVM 再调参也很难突破。深度学习方案的优势是能从预训练权重里继承高层语义特征再通过度量学习把同类人脸拉近、异类推远。4.1 为什么损失函数选 ArcFace 而不是普通交叉熵人脸识别不能简单当成多分类任务因为真实场景里的测试对象往往不出现在训练集里。模型要学的是“特征向量之间的余弦距离”而不是只输出一个类别概率。ArcFace 在特征和分类权重之间加了一个角度 margin让同类特征聚得更紧这正好匹配 AR Database 里“每类样本少、类间相似度高”的特点。常见的做法是拿 ImageNet 预训练的 ResNet 做 backbone去掉最后一层分类头把倒数第二层的输出当作 512 维特征向量。如果你想在门禁机、k10 行空板这类 AI 开发板上做原型验证这一段特征向量可以直接导出成 ONNX后续处理速度和存储占用都可控。4.2 自定义 Dataset 加载 BMP 图像用 PyTorch 加载 AR Database 不用先转成其他格式。BMP 是无压缩格式读取到内存就是原始像素不会像 JPEG 那样引入块状伪影对训练稳定性几乎没有副作用。但要小心通道顺序import torch from torch.utils.data import Dataset import pandas as pd import cv2 import torchvision.transforms as T class ARFaceDataset(Dataset): def __init__(self, csv_path, transformNone): self.df pd.read_csv(csv_path, encodingutf-8-sig) self.transform transform or T.Compose([ T.Resize((112, 112)), T.ToTensor(), T.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5]) ]) def __len__(self): return len(self.df) def __getitem__(self, idx): row self.df.iloc[idx] # 用 cv2 读 BMP 得到 BGR 顺序预训练网络通常吃 RGB image cv2.imread(row[path]) image cv2.cvtColor(image, cv2.COLOR_BGR2RGB) image self.transform(image) label row[subject_id] return image, label为什么要用cv2而不是 PILAR Database 的预处理版本里偶尔会混入 16 位 BMPPIL 可能直接报错而 OpenCV 对 BMP 的位深兼容性好得多。cv2.imread默认读成 BGR所以看到人脸偏色不要奇怪转一次COLOR_BGR2RGB即可。Normalize这里用了 0.5 均值方差能让像素落到 [-1, 1]如果你把预处理图填成了灰度偏亮的画布这个归一化比 ImageNet 的均值方差更稳。DataLoader 设置时batch_size32在主流显卡上够用num_workers设成 2 或 4 可以掩盖部分 BMP 文件较大的 IO 开销如果训练时显卡利用率上不去优先检查num_workers和pin_memoryTrue。4.3 ArcFace 头与训练循环参数ArcFace 头的作用是在分类层的余弦相似度上叠加一个 margin。这里给出一个简化实现便于理解参数含义import torch import torch.nn as nn import torch.nn.functional as F import math class ArcFaceHead(nn.Module): def __init__(self, in_features, num_classes, s32.0, m0.5): super().__init__() self.weight nn.Parameter(torch.FloatTensor(num_classes, in_features)) nn.init.xavier_normal_(self.weight) self.s s self.m m def forward(self, embedding, label): # 归一化特征和权重再算余弦相似度 cosine F.linear(F.normalize(embedding), F.normalize(self.weight)) theta torch.acos(torch.clamp(cosine, -1.0 1e-7, 1.0 - 1e-7)) # 只在目标类上加 margin其余类保持原余弦值 target_logits torch.cos(theta self.m) one_hot torch.zeros_like(cosine) one_hot.scatter_(1, label.view(-1, 1), 1.0) output cosine * (1 - one_hot) target_logits * one_hot return output * self.s参数s32.0是缩放因子控制特征向量模长对损失的影响m0.5是角度间隔越大类间边界越宽但过大容易让训练难收敛。用 112x112 输入、512 维特征时s32, m0.5是比较稳妥的起点。训练循环里建议用 SGD 而不是 Adam。ArcFace 论文和开源实现几乎都搭配 SGD momentumimport torchvision.models as models backbone models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) backbone.fc nn.Identity() # 去掉最后一层分类头 arcface ArcFaceHead(in_features512, num_classesN_CLASSES) model nn.Sequential(backbone, arcface) optimizer torch.optim.SGD([ {params: backbone.parameters(), lr: 0.001}, {params: arcface.parameters(), lr: 0.01}, ], momentum0.9, weight_decay5e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max30) for epoch in range(30): model.train() for images, labels in train_loader: outputs model(images, labels) loss F.cross_entropy(outputs, labels) optimizer.zero_grad() loss.backward() optimizer.step() scheduler.step()这里的两个学习率是关键ArcFace 头是随机初始化的学习率调成 backbone 的 10 倍否则分类头原地踏步特征学不出来。weight_decay5e-4对 ResNet 是常见值过大容易欠拟合。每个 epoch 结束时在验证集上算一次准确率并把最优权重复制到单独文件如果不保存最优权重最后几个 epoch 可能因学习率过低导致过拟合。注意ArcFace 的标签必须从 0 开始连续编号。AR Database 的人 ID 如果是从 1 开始直接拿来用会产生 one_hot 越界。先执行df[subject_id] df[subject_id] - 1或重新映射一次。5. 避坑清单BMP、标签和数据划分的五个翻车现场这一章把我实际踩过的坑集中列出来。每一条都按“现象 → 原因 → 解决”写遇到同类问题可以直接对照。5.1 BMP 的通道顺序与读取兼容性坑一训练时 loss 降得很快但可视化人脸时发现画面整体偏色皮肤呈蓝紫色。原因OpenCV 读取 BMP 默认输出 BGR 顺序直接送给预训练模型或画图时没有交换通道。解决在预处理统一转成 RGB可视化用 matplotlib 前先cv2.cvtColor(img, cv2.COLOR_BGR2RGB)。坑二用 PIL 读取某些 bmp 文件时报错提示无法识别图像文件。原因AR 数据集的多份镜像里存在位深不一致的情况比如 8 位、24 位、32 位混排。解决读取入口统一走cv2.imread并在加载后检查image.dtype不是uint8就做一次astype(np.uint8)必要时丢弃异常文件。给一版安全读取函数放进训练脚本开头即可import cv2 import numpy as np def safe_imread(path): img cv2.imread(str(path), cv2.IMREAD_COLOR) if img is None: raise ValueError(f无法读取该 BMP: {path}) if img.dtype ! np.uint8: img np.clip(img, 0, 255).astype(np.uint8) if img.ndim 2: img cv2.cvtColor(img, cv2.COLOR_GRAY2BGR) return cv2.cvtColor(img, cv2.COLOR_BGR2RGB)5.2 BMP 文件体积大导致的训练 IO 瓶颈坑三训练时 GPU 利用率只有 20%CPU 和磁盘占用却打满每个 epoch 等数据等得心焦。原因BMP 无压缩一张 112x112 彩色图约 36KBAR Database 全量几百人后图像数量上万每轮都从磁盘读取原图就成了瓶颈。解决第一次训练前把预处理后的图像全部读入内存数组或者批量转成 numpy 的.npy文件。小数据集完全塞得进内存train_loader直接用TensorDataset包装数组即可。import torch from torch.utils.data import TensorDataset, DataLoader # 假设 cache.npy 是预处理好的图像shape [N, 112, 112, 3] images np.load(cache.npy) images torch.from_numpy(images).permute(0, 3, 1, 2).float() labels torch.from_numpy(labels).long() dataset TensorDataset(images, labels) loader DataLoader(dataset, batch_size32, shuffleTrue, num_workers2)5.3 标签文件编码带来的隐性踩坑坑四用 pandas 读完后df[path]报 KeyError打印列名却能看到path。原因CSV 写入时用了 UTF-8 带 BOMpandas 默认utf-8读取时第一列列名变成\ufeffpath肉眼看着没区别程序匹配不上。解决写入用encodingutf-8-sig读取也用encodingutf-8-sig两边保持一致。这是小而隐蔽的坑却能让整个训练脚本在数据加载阶段直接崩掉。5.4 随机切分导致的人脸数据泄漏坑五随机切分训练集和测试集准确率刷到 99%但换到真实门禁机测试时效果很差。原因同一个人不同光照和遮挡的图像同时出现在训练和测试里模型记住了具体人脸而不是学会泛化。解决按subject_id分组做划分用GroupShuffleSplit或GroupKFold。这会让整体准确率下降 3 到 5 个百分点但这才反映真实身份识别能力。5.5 遮挡子集与光照子集不平衡坑六训练好的模型在墨镜遮挡样本上准确率尚可一换到围巾遮挡就崩。原因围巾遮盖的面部区域更大而且如果预处理版本里围巾样本本来就少模型几乎没学到这个遮挡模式。解决单独统计各条件子集的样本量对围巾类做更强的数据增强比如随机擦除、亮度扰动、局部模糊训练损失里加上class_weight或者给少样本子集设置更高的采样权重。from sklearn.utils.class_weight import compute_class_weight weights compute_class_weight(balanced, classesnp.unique(y), yy) sample_weights weights[y] sampler torch.utils.data.WeightedRandomSampler( sample_weights, num_sampleslen(sample_weights), replacementTrue ) train_loader DataLoader(dataset, batch_size32, samplersampler)这套组合拳能明显把围巾子集拉上来但也要接受一个现实大面积遮挡本来就难靠数据增强不能解决全部问题。6. 上板前先做端到端验证ROC 评估与嵌入式部署注意点训练准确率高不等于能部署到人脸识别门禁机或者 k10 行空板这类开发板上。原因在于门禁机做的是“底库比对”不是“分类预测”。部署前我会先跑一遍端到端相似度验证拿测试集里的人脸图生成特征向量和底库里的注册特征算余弦相似度再统计 ROC 曲线。from sklearn.metrics import roc_curve, auc model.eval() scores, labels [], [] with torch.no_grad(): for images, true_labels in test_loader: emb model[0](images) # 简化取当前 batch 内样本间的相似度真实场景可换成底库比对 emb F.normalize(emb, dim1) sim torch.mm(emb, emb.T) scores.extend(sim[torch.eye(emb.size(0)).bool()].cpu().numpy()) labels.extend(true_labels.cpu().numpy()) fpr, tpr, _ roc_curve(labels, scores, pos_label1) print(AUC:, auc(fpr, tpr))ROC 曲线比单一准确率更能说明问题它能反映阈值调高后漏识率和误识率怎么变化。门禁机场景里误放行比拒识更危险所以阈值要偏严。部署时还有几个注意点列成表如下注意点建议原因输入尺寸固定为 112x112 RGB与预训练/微调时一致图像格式bmp 转 npy 后打包减少设备端 IO 压力特征维度512 维 float32相似度计算快存储可控阈值余弦相似度 0.35 到 0.45更低容易误放行处理速度单帧特征推理控制在 50ms 内门禁机需要实时响应在嵌入式板卡上我只导出 backbone 的特征提取部分不导出 ArcFace 头。100 人以内的底库比对用矩阵乘法在毫秒级完成完全够用。跑试前我还有一个习惯数据准备当天就把原始 bmp 原图归档一份后续不管预处理脚本怎么改都有后悔药可吃训练前先跑一个数据 sanity 脚本检查每类样本数、图像尺寸分布和坏图数量。这个习惯救过我几次希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站