资讯动态

LBP+LPQ+SVM小样本表情识别:Matlab实现与避坑指南

发布时间:2026/10/16 13:54:34 来源:尧图企业网站定制
简介基于Matlab实现的表情识别完整源码包面向计算机视觉学习者与研究者聚焦LBP与LPQ特征提取、SVM分类及PCA降维的协同应用。资源以Jaffe人脸库为测试对象覆盖从图像预处理到分类识别的全流程适合入门到进阶阶段参考。压缩包共235个文件大小2.66MB其中213张tiff样本图片用于训练验证18个m源码文件实现核心算法与主程序另有少量txt说明文档结构清晰便于对照学习。已有1200人学习下载。通过该源码可掌握LBP/LPQ纹理特征编码、PCA降维原理及SVM多分类实现理解各模块如何衔接并用于表情识别任务对后续开展相关实验或模型调优有直接参考价值。1. 小样本表情识别LBPLPQSVM这条老路为什么还能打做表情识别很多人第一反应是上卷积神经网络但真到了手里只有几百张样本、机器没有独立显卡、还要给甲方讲清楚这个特征到底在抓什么的时候深度学习反而成了黑匣子。LBPLPQ做特征提取、PCA降维、SVM分类这套组合在公开小规模表情库和自采数据上依然能跑到85%~92%的准确率训练时间按秒算部署时连GPU都不需要。这篇笔记适合正在做毕业设计、课程项目或嵌入式原型验证的开发者我给你一套可以直接抄的Matlab实现从文件夹到混淆矩阵一次跑通并把最容易翻车的五个坑提前指出来。2. 特征先立住LBP和LPQ在Matlab里的落地与融合2.1 LBP一个函数提取等价模式直方图LBP的思想特别朴素把每个像素和它周围邻域比较比中心亮记为1、暗记为0一圈比下来得到一个二进制数。这个数就描述了该点附近的局部纹理结构。人脸表情本质上就是肌肉运动导致的局部纹理变化——嘴角上扬、眼睛闭合、眉毛提升都会改变局部灰度分布模式所以LBP天然适合这个任务。一个重要的改进是等价模式Uniform LBP。原始LBP在3×3邻域下有256个取值但实际图片里大部分模式都是跳变次数不超过2次的等价模式。把它们单独编号把稀有模式合并成一类维度就从256降到了59维。降维之后直方图更紧凑SVM训练速度明显更快准确率通常还不降反升。function hist lbp_histogram(img, R, P) % 圆形LBP等价模式直方图 % 输入: img 灰度图(double), R 半径, P 邻域采样点数 % 输出: 59维归一化直方图(P8时的等价模式维数) if nargin 2, R 1; end if nargin 3, P 8; end img im2double(img); [rows, cols] size(img); pad R; img_pad padarray(img, [pad pad], replicate); center img_pad(pad1:end-pad, pad1:end-pad); lbp zeros(rows, cols); for k 0:P-1 x R * cos(2*pi*k/P); y -R * sin(2*pi*k/P); % 最近邻取采样点利用矩阵平移批量计算 xi pad 1 round(x); yi pad 1 round(y); neighbor img_pad(yi:yirows-1, xi:xicols-1); lbp lbp double(neighbor center) * 2^k; end % 等价模式映射表 mapping zeros(1, 2^P); idx 1; for i 0:2^P-1 bits bitget(i, 1:P); u sum(abs(diff([bits(end), bits, bits(1)]))); if u 2 mapping(i1) idx; idx idx 1; else mapping(i1) P*(P-1)3; % 非等价模式归入最后一类 end end mapped mapping(lbp(:)1); hist histcounts(mapped, 1:P*(P-1)31); hist hist / sum(hist); end逻辑说明代码里没用双线性插值而是用了最近邻简化实现R1时两者几乎没有差别R2以上误差也不大。padarray的replicate模式复制边缘像素避免边界产生虚假模式。bitget取出0~255每个数的8个比特位diff统计相邻比特跳变次数这是等价模式判定的核心。参数上R控制纹理尺度R1抓细纹理眼周细纹、嘴角折痕R2抓稍大一点的块状纹理。表情识别我一般从R2、P8起步因为单纯R1对中等分辨率人脸图太敏感噪声容易被当成特征。P越大直方图越稀疏P16在几百样本的情况下反而容易过拟合。2.2 LPQ补上模糊短板手写一个256维直方图LPQ的全称是Local Phase Quantization思路和LBP完全不同它对每个像素的邻域做短时傅里叶变换取四个频点的相位信息量化成8位二进制码。为什么它对模糊鲁棒因为高斯模糊主要衰减幅度谱对相位谱的破坏小而LPQ只用相位所以运动模糊、轻微失焦的人脸图依然能提取到稳定特征。这正是LBP的短板LBP对清晰纹理敏感但图片一旦模糊局部比较的结果会随机化。表情采集过程经常有轻微头部运动把LBP和LPQ串起来用等于一个抓清晰纹理、一个抓模糊不变结构互补性很强。function hist lpq_histogram(img, winSize) % LPQ直方图短时傅里叶变换提取局部相位并量化 % 输入: img 灰度图(double), winSize 窗口尺寸(3或5) % 输出: 256维归一化直方图 img im2double(img); if nargin 2, winSize 3; end [rows, cols] size(img); alpha 1 / winSize; % 基频位置 u [alpha 0; 0 alpha; alpha alpha; alpha -alpha]; % 4个频点 xs -floor(winSize/2):floor(winSize/2); [X, Y] meshgrid(xs, xs); W ones(winSize) / winSize^2; % 矩形窗 resp zeros(rows, cols, 8); for k 1:4 % 实部核与虚部核 h_cos cos(2*pi*(u(k,1)*X u(k,2)*Y)) .* W; h_sin -sin(2*pi*(u(k,1)*X u(k,2)*Y)) .* W; resp(:,:,2*k-1) imfilter(img, h_cos, replicate, conv); resp(:,:,2*k) imfilter(img, h_sin, replicate, conv); end % 逐通道中位数量化 resp_th resp median(reshape(resp, [], 8), 1); code zeros(rows, cols); for k 1:8 code code resp_th(:,:,k) * 2^(k-1); end hist histcounts(code, 0:256); hist hist / sum(hist); end逻辑说明四个频点的选择是LPQ的标准配置alpha 1/winSize把频点放在局部窗口的基频上实际效果对输入图像分辨率不敏感。imfilter用replicate处理边界比默认补零稳很多。量化阈值用逐通道中位数比全局中位数更鲁棒如果用全局中位数亮暗分布不均的图会产生大量全0或全1的编码。参数上winSize3抓高频细节winSize5对模糊更稳但会牺牲部分精细纹理。我在实际项目里的经验图像分辨率低于64×64时用3128×128以上用5。这个参数值得和第4章的LBP半径一起做网格扫描。2.3 拼接串行融合与维度膨胀的代价LBP输出59维、LPQ输出256维直接拼接成315维特征向量。维度在绝对值上不高但要注意如果做多尺度扩展两个LBP半径加两个LPQ窗口就是590维左右而表情训练集常常只有每类几十张图几百个样本喂给SVM高维稀疏特征很容易过拟合。我常用的做法是先拼接、后统一PCA而不是分别对LBP和LPQ做PCA再拼接。原因很简单PCA是线性变换分开降维可能丢掉两种特征之间的互补相关性统一降维则让PCA自己决定每种特征保留多少分量。这个选择会直接影响后面的交叉验证分数值得在第3章细说。3. 降维和分类从315维特征到7类表情标签3.1 PCA降维k值别只信95%用交叉验证去选PCA在这里的任务不是可视化而是把315维特征压缩到几十维同时去除LBP和LPQ之间的冗余。Matlab的pca函数一次返回五个输出coeff是主成分方向score是投影后的坐标latent是特征值explained是每个主成分解释的方差百分比。最关键的步骤不是调用pca而是如何对验证集做变换。验证集必须减去训练集的均值再乘训练集的coeff绝不能自己单独做一次PCA。否则验证集的信息经过均值和协方差悄悄泄漏进了训练过程测试准确率会虚高几个点。mu mean(feat_train); train_c feat_train - mu; [coeff, score, ~, ~, explained] pca(train_c); cum_ratio cumsum(explained); k find(cum_ratio 95, 1); feat_train_pca score(:, 1:k); feat_val_pca (feat_val - mu) * coeff(:, 1:k);逻辑说明pca(train_c)内部会做中心化所以score对应的是去均值后的数据。验证集用(feat_val - mu) * coeff(:, 1:k)完成同样的中心化和投影。find(cum_ratio 95, 1)取累计贡献率首次达到95%的主成分个数。贡献率阈值95%是个常见起点但别把它当真理。我见过一个案例某开发者把阈值从95%调到99%k从24涨到58交叉验证准确率反而掉了3个百分点。原因是多出来的主成分主要编码噪声SVM把这些噪声当成了类别线索。更稳的做法是把k当成超参数在后续网格搜索里和SVM参数一起扫。3.2 多分类SVMfitcecoctemplateSVM的RBF参数表情识别是7分类问题Matlab里最省事的入口是fitcecoc它把多分类拆成多个二分类器。编码方式有两种onevsone一对一和onevsall一对多。对表情这种类别数量适中、样本不均衡明显的问题我固定用onevsone每个二分类器只需要在两类样本上训练受不均衡影响更小。核函数选RBF而不是线性核。LBPLPQ直方图特征经过PCA后类别边界通常不是线性的RBF能拟合出更复杂的边界。但RBF有两个敏感参数BoxConstraint惩罚系数和KernelScale核宽度。BoxConstraint太大SVM会死磕每个训练样本容易过拟合太小则欠拟合所有样本都被推成平滑边界。KernelScale太大RBF几乎变成线性核失去非线性能力太小则每个样本单独成岛泛化很差。t templateSVM(KernelFunction, rbf, ... BoxConstraint, 1, KernelScale, 1); mdl fitcecoc(feat_train_pca, label_train, ... Learners, t, Coding, onevsone);逻辑说明templateSVM创建一个SVM模板fitcecoc用这个模板为每对类别训练一个二分类器。第一行的BoxConstraint1和KernelScale1只是初始值几乎肯定不是最优的下一节的网格搜索就是为这两个参数服务的。3.3 网格搜索把BoxConstraint和KernelScale一起扫调参有个纪律不能拿验证集反复试试多了验证集就变成了训练集的一部分。正确做法是用训练集内部的5折交叉验证选参数选完后再用独立的验证集验一次。boxList [0.1, 1, 10, 100]; scaleList [0.5, 1, 2, 4, 8]; bestAcc 0; bestPair [1, 1]; for b boxList for s scaleList t templateSVM(KernelFunction, rbf, ... BoxConstraint, b, KernelScale, s); cv fitcecoc(feat_train_pca, label_train, ... Learners, t, KFold, 5); acc 1 - kfoldLoss(cv); if acc bestAcc bestAcc acc; bestPair [b, s]; end end end逻辑说明KFold5在训练集上做5折交叉验证kfoldLoss返回误分率1 - kfoldLoss就是交叉验证准确率。这个双层循环一共跑20组参数每组训练一个7类LBPLPQ的CECOC模型几百样本的情况下几秒到十几秒就能跑完。参数说明BoxConstraint用对数网格0.1到100KernelScale用0.5到8的线性网格。这两个范围覆盖了绝大多数表情识别场景。如果最优值在边界上说明范围取窄了把边界方向再延伸一格。选出的bestPair就是最终模型的参数用全量训练集重新训练一次然后只在验证集上做最终评估。4. 完整跑通从图片文件夹到混淆矩阵的一条龙代码4.1 数据组织与imageDatastore标签自动生成数据目录结构是最容易忽略却最影响结果的部分。我建议按如下方式组织让imageDatastore自动从文件夹名生成标签data/ train/ anger/ disgust/ fear/ happy/ neutral/ sad/ surprise/ val/读取只需两行train_ds imageDatastore(data/train, LabelSource, foldernames); val_ds imageDatastore(data/val, LabelSource, foldernames);逻辑说明LabelSource设为foldernamesMatlab自动把每个子文件夹名作为该文件夹内所有图片的标签返回的train_ds.Labels是categorical类型正好可以直接喂给fitcecoc。这里有一个重要提醒划分训练集和验证集时一定要按人来分不能按图片随机分。同一个人的不同表情图片纹理高度相似随机切分会让验证集里出现训练集同源图片准确率虚高。我一般的做法是先把人物编号列出来按人物切分再凑成文件夹。4.2 批量特征提取预处理、LBP、LPQ一次到位function featMat extract_features_from_ds(ds, params) % 遍历imageDatastore提取LBPLPQ拼接特征 n numel(ds.Files); featMat zeros(n, 59 256); % LBP等价模式59维 LPQ 256维 for i 1:n img readimage(ds, i); img imresize(img, [128 128]); if size(img, 3) 3 img rgb2gray(img); end img histeq(img); % 直方图均衡削弱光照差异 lbp_h lbp_histogram(img, params.lbp_radius, params.lbp_points); lpq_h lpq_histogram(img, params.lpq_win); featMat(i, :) [lbp_h, lpq_h]; end end逻辑说明预处理顺序固定为灰度化、尺寸归一化、直方图均衡。histeq这一步对LBP影响很大因为LBP本质是相对比较光照不均时亮区和暗区的纹理模式会被扭曲。均衡之后再提取特征同一表情在不同光照下的特征向量更接近。参数说明params是一个结构体调用前赋值params.lbp_radius 2; params.lbp_points 8; params.lpq_win 5; feat_train extract_features_from_ds(train_ds, params); feat_val extract_features_from_ds(val_ds, params);295维特征矩阵直接存在内存里就够了但如果你要反复试验不同参数建议把特征矩阵和标签存成.mat文件避免每次调参都重新提取一遍。4.3 训练评估与单张预测混淆矩阵和可信度把第3章的PCA、网格搜索和第4章的特征提取串起来完整训练脚本如下% 特征归一化与PCA mu mean(feat_train); [coeff, score, ~, ~, explained] pca(feat_train - mu); k find(cumsum(explained) 95, 1); feat_train_pca score(:, 1:k); feat_val_pca (feat_val - mu) * coeff(:, 1:k); % 网格搜索最优参数(代码见3.3节省略重复) % bestPair [10, 2]; % 示例输出 t templateSVM(KernelFunction, rbf, ... BoxConstraint, bestPair(1), KernelScale, bestPair(2)); mdl fitcecoc(feat_train_pca, label_train, ... Learners, t, Coding, onevsone); % 验证集评估 pred_val predict(mdl, feat_val_pca); acc mean(pred_val label_val); fprintf(验证集准确率: %.2f%%\n, acc * 100); % 混淆矩阵 figure; confusionchart(label_val, pred_val);逻辑说明confusionchart不仅给出总体准确率还能直接看到哪些类别互相混淆。表情识别里最常见的混淆是恐惧和惊讶、厌恶和愤怒因为这两组在肌肉运动模式上本就接近。如果混淆矩阵里某两类互相错分严重不要急着改参数先看看是不是样本量太少或者标注本身有歧义。单张预测要复用训练时的全部预处理步骤不能只调用predictimg imresize(readimage(val_ds, 1), [128 128]); if size(img, 3) 3, img rgb2gray(img); end img histeq(img); f [lbp_histogram(img, params.lbp_radius, params.lbp_points), ... lpq_histogram(img, params.lpq_win)]; f_pca (f - mu) * coeff(:, 1:k); [label_pred, score_pred] predict(mdl, f_pca);这里mu和coeff必须是训练集上算出来的不能拿单张图重新算PCA否则投影空间不一致结果没有意义。5. 避坑排查这套方案最容易翻车的五个地方5.1 训练集97%、验证集50%数据泄漏和PCA投影错误现象交叉验证和训练集准确率都接近97%一上验证集掉到50%甚至不如随机猜测。原因大概率是验证集的特征矩阵单独做了PCA或者划分数据集时没有按人来分。单独做PCA意味着验证集的均值和主成分方向来自验证集自身等于验证集信息在训练前已经泄漏按图片随机划分则让同一个人的不同表情图同时出现在训练和验证集里模型记住了人而不是表情。解决严格只用训练集计算mu和coeff验证集只做(feat_val - mu) * coeff一步数据集划分前先枚举人物ID按人物切分。这两条同时做到准确率才会真实可信。5.2 全部图都被判成一类样本不均衡与SVM默认参数现象预测结果几乎全落在样本量最大的那一类比如中性。原因表情数据集的类别分布天然不均衡中性和高兴图片多厌恶和恐惧图片少。SVM优化目标是整体准确率样本多的类别对决策边界影响力大。解决先看各类样本数最少类和最多类差距不要超过3倍如果差距大就做数据增强或欠采样。onevsone编码能缓解但不根治。还有一个容易被忽略的原因KernelScale太小导致RBF过拟合模型只会对训练集中密集区域有反应。5.3 LPQ特征全是0或NaN数值类型和uint8溢出现象lpq_histogram输出的直方图全部为0或者predict时报NaN。原因如果直接把uint8图像传进函数imfilter内部会做整数滤波负值被截断为0相位信息全部丢失im2double只接受灰度图如果输入是RGB三通道size(img,3)3的检查没做卷积后维度对不上也会出现异常。解决在lpq_histogram入口处加一句img im2double(rgb2gray(img))并在调用前显式判断通道数。我习惯在特征提取主循环里统一处理灰度化和double转换两个特征函数只接收double灰度图接口清晰就不容易踩这个坑。5.4 SVM训练慢到怀疑人生直方图稀疏和过度调参现象拟合CECOC模型时每折要等几十秒整个网格搜索跑完要十几分钟。原因LBP等价模式直方图虽然是59维但很多bin的值为0特征矩阵非常稀疏。SVM基于支持向量迭代求解稀疏高维数据会让内积计算变慢。解决PCA降维能显著压缩维度比如295维降到30~50维训练速度能快一个数量级。另外在网格搜索里KernelScale从默认的auto改成显式数值避免每次重新估计核宽度。5.5 换个被试就失灵人脸对齐比特征选择更重要现象在已有数据集上调好的模型换一个采集环境或换一批被试准确率直接从85%掉到60%。原因LBP和LPQ都是严格位置相关的特征。眼睛、鼻子、嘴巴如果不在图片的同一位置提取到的直方图描述的就不是同一块面部区域特征自然对不上。解决在特征提取前做人脸对齐。常见做法是检测双眼中心坐标做旋转和缩放把两眼连成水平线再裁剪到固定尺寸。这个步骤在工程上比换特征、调参数更重要。如果不想引入人脸检测器至少要在采集时约束被试正脸拍摄并在预处理里按瞳孔位置裁剪。6. 让准确率再稳一点的三件事6.1 把Radius和winSize放在一起扫特征参数和SVM参数不能分开调。LBP的Radius选1还是2LPQ的winSize选3还是5会改变特征分布进而影响PCA最优维度和SVM最佳核宽度。我一般把params.lbp_radius、params.lpq_win和SVM的BoxConstraint、KernelScale放进同一个循环里网格搜索每组参数都重新走一遍特征提取、PCA和5折交叉验证。搜索范围不要太大Radius取[1,2]winSize取[3,5]共4组特征参数每组再配20组SVM参数总量80组对几百样本的项目完全跑得起。6.2 多尺度特征两个LBP半径加两个LPQ窗口单一尺度的LBP只描述固定纹理粒度多尺度拼接能同时抓到细纹理和粗结构。比如Radius1和Radius3的LBP各自提取等价模式直方图winSize3和winSize5的LPQ各自提取256维直方图四段特征拼接后做PCA。代价是原始维度变成590维PCA后通常只保留40~60维SVM训练时间反而不会增长太多。我在这类配置下见过2~4个百分点的稳定提升前提是样本量不能太小——每类少于30张时多尺度带来的稀疏性问题会抵消收益。6.3 用SVM决策分数把不确定样本拦下来predict返回的第一个输出是类别第二个输出是每类的分数。对fitcecoc来说这个分数是负的平均二值损失分数越高表示样本越靠近该类别的决策区域中心。[labels, scores] predict(mdl, feat_val_pca); conf max(scores, [], 2); threshold prctile(conf, 10); % 以验证集置信度最低10%为拒识阈值 flag conf threshold;逻辑说明当最大分数低于验证集分布的某个分位数时把样本标记为不确定而不是硬分一个类。这在工程落地时很有用——表情识别的应用通常允许认不出来但不允许认错。我早年做表情Demo时吃了不少亏最深刻的一条教训是模型在训练集上的分数永远好看真正决定项目能不能交付的是换人、换环境之后验证集上的数字。把特征参数、PCA维度和SVM参数当成一个整体去搜索比迷信任何一个默认值都管用。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑