ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Gabor+PCA+LDA+SVM:小样本人脸表情识别的传统视觉路线

Gabor+PCA+LDA+SVM:小样本人脸表情识别的传统视觉路线 简介这是一套基于Python的人脸表情/微表情识别系统完整源码面向计算机视觉初学者、研究人员及毕业设计开发者用于学习Gabor滤波、PCALDA降维、SVM分类和PyQt界面设计等核心技术。压缩包共808个文件大小35.85MB包含749张JPG样本图片、21个XML配置、6个训练好的模型、5个Python源码、5个数据库文件及可执行文件等图片与模型可直接支撑训练、验证和推理流程。系统重新编译了libSVM库并开启多线程支持训练效率更高同时借助PyQt构建图形界面兼顾算法能力与交互体验。已有410人学习下载适合需要完整项目参考、算法复现或二次开发的读者可直接获得各功能模块源码、模型参数、数据文件与清晰的目录结构减少从零搭建的重复工作。1. 这个项目到底在做什么一条不过时的传统视觉识别路线人脸表情识别现在一搜全是深度学习但真要落地一个微表情识别任务你会发现深度学习未必是最优解——微表情持续只有1/25到1/5秒样本量极少动不动就几百张图训一个CNN十有八九过拟合。而Gabor滤波加PCALDA降维再上SVM分类恰恰是这类小样本高维特征问题里最稳的一套组合拳。这个标题背后是一条完整的传统机器学习视觉路线Gabor提取纹理和方向特征PCA和LDA串行降维去冗余、保留判别信息最后用SVM做分类器。它适合两类人一类是正在做机器学习课程设计、需要可解释可复现代码的学生另一类是工作中遇到小样本图像分类、想快速搭一个基线系统的工程师。它不是最新潮的方案但它是那种夜深了你还能调通、讲得清每一步为什么这么做的方案。2. 先把特征工程立住Gabor滤波为什么适合人脸表情2.1 从人眼视觉机制说起Gabor核的尺度和方向Gabor滤波不是凭空发明的它模仿的是哺乳动物初级视觉皮层V1区神经元的感受野响应模式。人脸表情识别里表情差异集中在眼睛、眉毛、嘴巴这些区域的纹理走向和局部变化上比如笑的时候嘴角上扬、眼角出现鱼尾纹惊讶的时候眼睛和嘴巴张大的边缘轮廓变化剧烈。Gabor滤波器组用不同方向和尺度的核去卷积图像就能把这些局部纹理变化拆成一组响应图。一个二维Gabor核的表达式通常写成复数形式实部是余弦波乘高斯包络虚部是正弦波乘高斯包络。实际用OpenCV的cv2.getGaborKernel时最需要调的是这组参数ksize核大小、sigma高斯包络标准差、theta方向、lambd波长、gamma空间纵横比、psi相位偏移。表情识别领域常见做法是用5个尺度乘8个方向共40个滤波器组这个数量是经验值——太少了特征表达不够太多了计算量大而且相邻滤波器响应高度冗余。2.2 用OpenCV把Gabor滤波器组跑起来先把预处理说清楚。输入的人脸图必须统一尺寸和灰度我一般用cv2.resize统一到128x128再cv2.equalizeHist做直方图均衡化。直方图均衡化这一步特别关键它能把不同光照条件下的人脸拉到一个相近的灰度分布上——光照变化对Gabor响应的影响远比你想象的大同一个表情在不同光照下提取出的特征可能天差地别。import cv2 import numpy as np def build_gabor_filters(ksize31, sigma4.0, lambd10.0, gamma0.5, psi0): 构建 5 尺度 x 8 方向的 Gabor 滤波器组 ksize: 核大小必须为奇数31 表示 31x31 sigma: 高斯包络标准差控制滤波器的空间范围 lambd: 波长控制条纹的疏密越小纹理越细 gamma: 纵横比1.0 为圆形越小越细长 filters [] thetas np.arange(0, np.pi, np.pi / 8) # 8 个方向0 到 7pi/8 sigmas [2.0, 3.0, 4.0, 5.0, 6.0] # 5 个尺度 for sigma in sigmas: for theta in thetas: kern cv2.getGaborKernel((ksize, ksize), sigma, theta, lambd, gamma, psi, ktypecv2.CV_32F) filters.append(kern) return filters def extract_gabor_features(img, filters): 对输入灰度图做 Gabor 滤波返回拼接后的特征向量 img: 128x128 的灰度图已经过预处理 img cv2.resize(img, (128, 128)) img cv2.equalizeHist(img) img img.astype(np.float32) / 255.0 responses [] for kern in filters: filtered cv2.filter2D(img, cv2.CV_32F, kern) responses.append(filtered) # 对每张滤波响应图统计均值和标准差得到低维特征 feature [] for resp in responses: feature.append(np.mean(resp)) feature.append(np.std(resp)) return np.array(feature) # 使用示例读一张灰度图提取特征 filters build_gabor_filters() img cv2.imread(face.jpg, cv2.IMREAD_GRAYSCALE) feat extract_gabor_features(img, filters) print(feat.shape) # 输出 (80,)这段代码里有几个参数值得细说。sigma从2.0到6.0共5个尺度覆盖了从精细纹理到较粗轮廓的范围如果只做表情识别不做微表情尺度可以适当调小。theta取0到7π/8共8个方向少了会漏掉某些角度的纹理响应多了计算量线性上升而效果提升有限。特征向量取每张响应图的均值和标准差而不是把整个响应图拉平原因后面会说——PCA面对超高维特征时协方差矩阵的计算成本会失控。注意用cv2.getGaborKernel时lambd和ksize必须匹配。ksize至少要能包住一个完整的条纹周期否则核的边缘会被截断产生伪响应。一般lambd取10左右、ksize取31是比较稳的组合。2.3 微表情场景下Gabor参数怎么调整微表情和普通表情的差异在于变化幅度极小、持续时间极短。普通表情的纹理变化剧烈单个方向和单一尺度的滤波器就能捕获足够的判别信息但微表情需要更细的纹理敏感度。我一般会把gamma从0.5调大到0.7让Gabor核更圆润对微弱的方向响应更宽容同时把sigma的分布整体调小2.0到4.0因为微表情的局部变化范围更小太大的高斯包络会把微弱的纹理变化平滑掉。另外预处理在微表情场景下要更保守。直方图均衡化在普通表情上是加分项在微表情上可能把微小的灰度差异直接放大成噪声。我建议微表情数据上先做一次简单的CLAHE对比度受限自适应直方图均衡化代替全局均衡化它的对比度拉伸是局部的不会让整个图像的灰度范围发生剧烈变化这个调整往往能带来2到3个百分点的准确率提升。def preprocess_for_microexpression(img, clahe_clip2.0, clahe_grid8): 微表情场景的预处理CLAHE 局部对比度增强 clahe_clip: 对比度限制阈值越小对噪声的抑制越强 clahe_grid: 局部网格大小8 表示将图分成 8x8 块 img cv2.resize(img, (128, 128)) clahe cv2.createCLAHE(clipLimitclahe_clip, tileGridSize(clahe_grid, clahe_grid)) img clahe.apply(img) return img.astype(np.float32) / 255.0这段代码的原理不复杂clipLimit限制了局部直方图增强的幅度防止某些灰度区域被过度拉伸tileGridSize决定了局部统计的粒度8x8意味着图像被分成64个块每块内部独立做对比度增强。在微表情数据集上这个预处理方式比全局均衡化稳定得多。3. PCALDA串行降维高维Gabor特征怎么变得可用3.1 PCA为什么用协方差矩阵而不是直接用原始特征方差Gabor滤波器组如果每个尺度方向都保留完整响应图40张128x128的响应图拉平就是40x16384655360维直接拿这个维度去做SVM是灾难——不仅计算慢而且严重过拟合。所以需要降维。PCA在这里做的第一件事是去相关。Gabor滤波器组不同方向和尺度之间存在大量冗余同一张人脸图片经过不同参数滤波后相邻尺度的响应高度相似。PCA通过计算特征的协方差矩阵找到数据方差最大的正交方向把原始特征投影到这些方向上。协方差矩阵在这里的核心作用是描述特征维度之间的线性相关性它的特征向量就是主成分方向特征值就是该方向上的方差大小。这里有个初学者常犯的误区PCA不是直接对原始特征矩阵做分解而是先要中心化再计算协方差矩阵。中心化让每个特征维度的均值为0这样协方差矩阵表达的就是纯粹的波动关系而不是和均值混在一起。sklearn的PCA类内部已经做了中心化但如果你自己用numpy实现漏掉中心化这一步会让第一主成分被均值主导特征脸直接变成平均脸。3.2 LDA在这里补的是什么PCA不管分类LDA只管分类PCA是无监督的——它不关心每个样本是高兴还是悲伤只管方差最大方向。但表情识别的目标是分类PCA找出的主成分方向很可能和表情类别判别方向不一致。LDA线性判别分析则是有监督的它最大化类间散度与类内散度的比值寻找能让不同表情类别尽可能分开、同一表情类别尽可能聚合的投影方向。LDA的数学目标可以写成J(W) (W^T * Sb * W) / (W^T * Sw * W)其中Sb是类间散度矩阵Sw是类内散度矩阵W是投影矩阵。直接最大化这个比值需要对Sw求逆但Sw在样本少于特征维度时是奇异的——这正是人脸识别领域经典的“小样本问题”SSS问题。解决方法就是标题里写的串行策略先用PCA把维度压到样本数以下让Sw可逆再做LDA。这个顺序不是随意安排的PCA在前可以理解为先做无损的数据压缩和去相关LDA在后做有监督的判别投影。3.3 串行降维的实现代码与维度选择from sklearn.decomposition import PCA from sklearn.discriminant_analysis import LinearDiscriminantAnalysis as LDA import numpy as np def pca_lda_pipeline(X_train, y_train, pca_components100, lda_componentsNone): PCA LDA 串行降维 X_train: 训练集特征矩阵形状 (n_samples, n_features) y_train: 训练集标签 pca_components: PCA 保留的主成分数量 lda_components: LDA 保留的判别方向数量默认 n_classes - 1 n_classes len(np.unique(y_train)) if lda_components is None: lda_components n_classes - 1 # 第一步PCA 无监督降维到 pca_components 维 pca PCA(n_componentspca_components, whitenTrue) X_pca pca.fit_transform(X_train, y_train) # 第二步LDA 有监督降维到 n_classes - 1 维 lda LDA(n_componentslda_components) X_lda lda.fit_transform(X_pca, y_train) return pca, lda, X_lda # 使用示例 # X_train 是 (n_samples, 80) 的 Gabor 特征矩阵y_train 是对应的表情标签 pca, lda, X_train_lda pca_lda_pipeline(X_train, y_train, pca_components60) print(fPCA 保留方差占比: {np.sum(pca.explained_variance_ratio_):.3f}) print(fLDA 降维后特征维度: {X_train_lda.shape[1]})这段代码里的参数选择是有讲究的。pca.components_设置为60到100之间是一个经验范围需要看explained_variance_ratio_的累计贡献率来决定——一般累计贡献率到95%够用如果你发现60个主成分只覆盖了85%的方差就往上加。LDA的维度上限是类别数减1比如7类表情就是6维所以LDA这一步实际上是一个剧烈的降维把上百维压到个位数。这个最终的极低维特征就是SVM的输入既保留了Gabor特征的纹理信息又剔除了大量不相关的冗余。whitenTrue这个参数值得说明。白化让PCA输出的每个维度方差归一化这样LDA计算类内散度矩阵时不同维度的量纲不会因为方差差异而被某一维度主导。实测中不开启白化的话LDA的收敛性和最终的SVM精度都有一定波动。4. SVM分类器参数整定让模型在新样本上不翻车4.1 特征维度降下来之后SVM该用哪个核函数降到个位数甚至十几维的特征空间里线性可分性已经相当好了。此时SVM核函数的选择优先级很明确优先试RBF径向基函数核。RBF核本质上是在做特征映射把低维空间中的非线性边界映射到高维空间去线性划分而且它只有一个gamma参数调参压力小。线性核在特征已经经过LDA判别投影的情况下也能用效果一般不会差太多但遇到新数据时泛化能力略逊于RBF。不要一上来就用多项式核。表情识别任务的决策边界没有那么多复杂的交叉项多项式核的degree参数很难一次定准高了容易过拟合低了效果和线性核差不多。from sklearn.svm import SVC from sklearn.model_selection import GridSearchCV from sklearn.metrics import accuracy_score, classification_report def train_svm(X_train, y_train, X_val, y_val): 用网格搜索调 SVM 参数RBF 核 param_grid { C: [0.1, 1, 10, 100], gamma: [0.001, 0.01, 0.1, 1], kernel: [rbf] } svm SVC(probabilityTrue, class_weightbalanced, random_state42) grid GridSearchCV(svm, param_grid, cv5, scoringf1_macro, n_jobs-1) grid.fit(X_train, y_train) print(f最优参数: {grid.best_params_}) print(f交叉验证最优分数: {grid.best_score_:.4f}) y_pred grid.predict(X_val) print(验证集准确率: {:.4f}.format(accuracy_score(y_val, y_pred))) print(classification_report(y_val, y_pred)) return grid.best_estimator_ # X_train_lda 是 PCALDA 降维后的训练特征 # best_svm train_svm(X_train_lda, y_train, X_val_lda, y_val)参数范围的选择逻辑C是误分类惩罚系数C越大对训练样本的分类要求越严越容易过拟合表情识别这类小样本任务C取10到100比较常见gamma控制RBF核的宽度gamma越小决策边界越平滑但太小会欠拟合0.01到0.1之间值得多试。网格搜索的评分标准我选了f1_macro而不是accuracy——表情数据集普遍类别不平衡中性表情远多于惊讶、厌恶准确率会被多数类带偏宏F1对每个类别平等对待更能反映真实水平。4.2 训练集和测试集怎么切同人不同人的区别表情识别项目里数据划分是一个容易踩坑但很少被讲透的问题。如果训练集和测试集里出现了同一个人的不同表情图片模型学到的其实是“这个人长什么样”而不是“这个表情长什么样”这在计算机视觉里叫身份泄露。实际部署时的场景是遇到陌生人所以严格的做法是GroupShuffleSplit按人的身份分组划分保证同一人的所有图片只出现在训练集或测试集的一侧。from sklearn.model_selection import GroupShuffleSplit # 假设有 groups 数组每个元素是样本对应的人脸身份 ID gss GroupShuffleSplit(n_splits1, test_size0.2, random_state42) train_idx, test_idx next(gss.split(X_lda, y, groupsgroups)) X_train, X_test X_lda[train_idx], X_lda[test_idx] y_train, y_test y[train_idx], y[test_idx]这段代码做的事情是按照groups数组的身份信息把整个数据集分成训练和测试两部分n_splits1表示只做一次划分test_size0.2留出20%做测试。相比train_test_splitGroupShuffleSplit保证的是“同一个人不跨集合”切分后的评估结果才是真实的模型泛化能力。如果你的数据集本身就没有身份标注至少要随机打乱后划分并对此心存警惕——报告的准确率很可能虚高。4.3 验证策略怎么定小样本下的交叉验证微表情数据集样本量经常不足200张这时候把20%留出来做测试训练集可能只剩100多张SVM很容易欠拟合。这种情况下我一般用留一法交叉验证Leave-One-Out或者分层K折K取5到8之间。留一法在样本极少时是近乎无偏的估计但计算量随样本数线性增长样本超过200就不太划算了。分层K折的“分层”指的是每折中各类别比例和全量数据保持一致避免某折恰好没有惊讶类样本导致训练出来的模型对惊讶完全无感知。sklearn里传cvStratifiedKFold(n_splits5, shuffleTrue, random_state42)即可shuffleTrue保证打乱顺序防止数据集按标签排序导致每折分布不均。5. 避坑指南GaborPCALDASVM方案最常见的五个翻车点5.1 Gabor特征没有归一化PCA结果被高方差维度主导现象PCA累计方差贡献率前两个主成分就占了95%以上但下游分类准确率反而很低。原因Gabor滤波响应的均值和标准差不在同一个量纲级别某些方向特定尺度下的响应标准差可能是其他方向的几十倍。PCA对特征方差敏感方差大的维度会主导主成分方向但方差大不代表判别能力强。解决在进入PCA之前对Gabor特征做标准化让每个维度的均值为0、方差为1。用sklearn的StandardScaler在特征矩阵上先fit再transform。这一步做完PCA累计方差贡献率的曲线会明显平坦下来分类准确率通常能提升3到5个百分点。5.2 LDA的类内散度矩阵奇异直接崩在矩阵求逆现象不先做PCA直接对Gabor特征维度80到几百做LDA报错Singular matrix或者结果毫无判别性。原因LDA需要计算类内散度矩阵的逆当特征维度高于每类样本数时类内散度矩阵不满秩无法求逆。这是高维小样本问题的典型症状。解决记住串行顺序不能颠倒。先PCA把维度压到样本总数的1/3到1/2以下再进LDA。PCA本质上是LDA的前置保险它保证了进入LDA的数据协方差矩阵可逆。5.3 微表情和普通表情混在一起训练模型什么都学不好现象用普通表情数据集训练的模型去预测微表情准确率接近随机猜测把两类数据混合训练整体准确率下降。原因普通表情和微表情的纹理变化幅度差异太大Gabor滤波器参数是针对其中一类调优的另一类特征被严重压制。而且两者的标签分布不同微表情中抑制类压抑真实情绪和其他类别样本量严重失衡。解决分两个独立流程处理。普通表情用标准Gabor设置sigma 2到6gamma 0.5微表情单独用2.3小节的CLAHE预处理加更小尺度的Gabor参数。不要共用一个特征提取流水线。5.4 人脸对齐没做好特征全提取到了背景上现象验证集准确率在60%左右波动死活上不去查看Gabor响应图发现特征包含大量背景纹理。原因人脸检测之后直接送入特征提取但人脸位置、角度有偏差眼睛不在同一水平线嘴巴位置偏移。Gabor滤波是对位置敏感的同一个表情在不同位置上的响应完全不同。解决做人脸对齐。常见做法是检测双眼中心坐标用仿射变换把两只眼睛旋转到水平位置并固定间距。OpenCV的estimateAffinePartial2D加上眼睛目标位置可以实现标准化对齐对齐后再统一裁剪到128x128。5.5 类别不平衡让SVM偏向多数类现象报告的整体准确率90%但看混淆矩阵生气、厌恶的识别率不到30%全是中性表情占了大头。原因SVM的目标函数是最大化间隔没有显式处理类别不平衡。当某类样本远多于其他类时决策边界会为了多数类的准确率而牺牲少数类。解决SVC里加class_weightbalanced让它根据类别频率自动加权如果还不够手工调整class_weight字典把少数类的权重设到2到5之间。另外网格搜索的评分指标用f1_macro而不是accuracy这个在4.1节已经强调过但实际项目中还是很多人不改。6. 最后一公里把精度再往上顶的三个实用技巧到了这个阶段Gabor特征加PCALDA加SVM的主流程已经能跑出70%到80%的准确率了。如果想再进一步有三个成熟的小技巧值得尝试。第一个技巧是特征融合。Gabor提取的是局部纹理方向特征但它对图像块的局部结构细节不敏感LBP局部二值模式正好能弥补这一点。把Gabor的80维特征和LBP直方图的256维特征拼接成一个336维的联合特征再做一遍PCALDA准确率通常能再涨2到4个百分点。LBP用skimage.feature.local_binary_pattern提取P8, R1是最常用的参数对表情纹理来说分辨率足够。特征拼接后维度上升PCA的主成分数要从60上调到100左右否则会丢掉过多信息。第二个技巧是数据增强。微表情样本少是硬伤传统数据增强对这类小样本任务有效。做法是随机水平翻转、小角度旋转±10度、小范围平移±5像素、轻微加噪高斯噪声标准差0.01。要在特征提取之前做增强对每张图生成5个变体样本量扩大6倍。注意翻转对表情识别有副作用——有些微表情研究认为左脸和右脸的表情强度不对称翻转会破坏这种非对称信息所以保守起见只做旋转和平移不翻转。第三个技巧是模型输出后处理。SVM开了probabilityTrue后会输出每个类别的概率估计但这只是基于Platt缩放的经验概率。实际场景中当最大概率低于某个阈值比如0.6时与其强行分类不如判定为“不确定”交给人工复核。表情识别特别是微表情识别误判的代价往往高于“不知道”的代价。这个决策阈值可以用验证集做ROC曲线后根据可接受的假阳率来定不要拍脑袋设0.5。from skimage.feature import local_binary_pattern import numpy as np def gabor_lbp_fusion(img, filters): Gabor LBP 特征融合 先用 Gabor 滤波器组提取 80 维特征再计算 LBP 直方图拼接 # Gabor 部分 gabor_feat extract_gabor_features(img, filters) # LBP 部分 lbp local_binary_pattern(img, P8, R1, methoduniform) hist, _ np.histogram(lbp.ravel(), binsnp.arange(0, 10), densityTrue) lbp_feat hist # uniform LBP 的 bin 数为 9P8 时 1 个非 uniform 类 # 拼接 fused_feat np.concatenate([gabor_feat, lbp_feat]) return fused_feat这段代码里LBP用了uniform模式它把二进制模式按跳变次数分组8个采样点下跳变次数小于3的模式归为独立bin其余全部合并为一个bin总共10个bin。密度直方图作为特征能抵抗局部亮度变化。融合后的特征进入PCA前记得做标准化否则Gabor特征的数值范围远大于LBP直方图PCA又会偏向Gabor那半边。这套方案做到最后普通表情数据集上的准确率能稳定在85%到90%微表情数据集上能到65%到75%——后者受限于数据量和标注一致性已经接近传统方法的实用上限。我做这类项目最大的教训是先跑通全流程再回头调参数而不要一开始就纠结gamma应该取0.01还是0.1——模型跑起来看结果用数据说话。希望帮到你。本文还有配套的精品资源点击获取
返回列表