拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

多特征融合的微表情识别:光流、AUs距离与SVM实战

多特征融合的微表情识别:光流、AUs距离与SVM实战 简介针对微表情识别这一细分方向这份使用 Python 语言编写的项目将多特征融合作为核心思路主要面向计算机视觉初学者和有一定基础的学习者可以作为毕业设计、课程设计、大作业或初期工程实训的选题与参考。项目搭建了从人脸预处理到最终分类评估的完整流程包含基于关键点定位的局部加权均值人脸裁剪与配准方法并利用视频放大、时序插值等模块增强有效表情信息。压缩包内共有八个文件其中六个是源代码文件另有一个说明文档和一个属性配置文件压缩包整体仅二十一 KB体量轻但覆盖关键环节。代码按功能拆分为多个独立模块例如人脸配准、特征提取、分类评估等每部分职责单一便于替换算法或调整参数其中配准脚本提供 LWMRegister 类可自定义标准脸、预测器路径、目标宽高与偏移量说明文档也能帮助快速上手。目前已有 195 人学习浏览适合用于快速搭建微表情识别基线并在此基础上加入新的特征融合策略或改进网络结构。1. 多特征融合的微表情识别为什么单个特征总会翻车你拿现成的表情识别接口去测一段几秒的微表情视频对方只在0.3秒内嘴角动了一下接口往往毫无反应。微表情幅度常常只有几像素持续时间不超过0.5秒单纯靠纹理、光流或者关键点位移里的任何一项都很容易被摄像头噪声或头部晃动淹没。多特征融合要做的事情就是把运动、纹理、几何形状这几类证据同时拿出来互相印证最终得到一个比单特征可靠得多的判断结果。这篇笔记不是论文复现而是面向已经会写python、想从视频里实际跑通微表情识别的工程师。我会从特征选型讲到预处理、光流提取、特征融合和SVM调参最后把最容易翻车的几个坑指出来。这套流程同样适合舍不得花时间装深度环境、想先用轻量级模型验证效果的人后面换特征提取器、换分类器都很容易。2. 多特征融合的目标拆解特征选型、对齐与两层融合2.1 微表情识别到底在识别什么从时域到空域的特征分工微表情和普通表情最大的区别是幅度小、时长短。普通表情持续时间通常在0.5到4秒微表情常在0.3秒左右普通表情的颧骨上提、嘴角张开会很明显微表情可能只是嘴角向内收紧两毫米。因此静态图像分类直接套过来经常漏检因为它看不到时间方向上的变化。既然幅度小就必须从运动信息里做文章。一帧图里看不出嘴角动了没有但前后帧的光流能算出像素位移光流天然对微小位置变化敏感。不过光流噪声也大摄像头抖动、光照闪动都会被算成运动所以需要另一类证据来校准面部关键点之间的几何距离比如嘴角到鼻翼的欧氏距离是否短时间内发生了突变。这个特征等于在说嘴巴区域的形状确实变了。第三类证据是纹理。微表情发生时皮肤褶皱和局部表面会有明暗变化LBP这类纹理特征能把这些变化编码成直方图并且对光照变化相对鲁棒。三者的组合逻辑是光流负责“有没有动”几何距离负责“动的部位对不对”纹理负责“表面是否真的变了”。三者同时投票比任何一个特征单独发言更可靠。这就是多特征融合的第一层动机不同特征描述的是同一现象的不同侧面错误模式往往是互补的。单独的光流可能把头部晃动当成表情但此时关键点距离没有异常纹理也没有局部变化两个“反对票”就能把误判拉回去。2.2 特征选型光流、AUs距离、LBP纹理三者的边界和互补适合Python落地的微表情特征大概有下面这几种。实际项目里我不会一上来全用按成本从低到高建议先用“光流统计量加AUs距离”两条腿走路准确率上不去再加LBP纹理直方图。特征提取方式敏感对象典型问题稠密光流Farneback或FlowNet像素级短时运动噪声大需要平滑稀疏光流/关键点位移dlib/MediaPipe关键点局部面部运动幅度关键点抖动干扰AUs距离特征关键点对距离表情动作单元对齐误差会放大LBP-TOP三正交平面直方图纹理与时序变化维度高计算较重深度特征预训练CNN embedding高层语义小样本容易过拟合这里再多说一句AUs距离特征。它不是随便选两个点算距离而是要参考FACS定义的AU。例如AU12是嘴角向外上方拉对应口角关键点和鼻翼附近点之间的坐标变化AU4是眉毛下压对应眉心和上下眼睑距离的垂直变化。dlib的68点模型可以近似算出这些距离。虽然不如专业AUs标注准但对微表情来说相对变化量比绝对坐标稳定得多。选特征还要考虑计算资源。Farneback光流一帧在128x128的人脸区域上大约十几毫秒关键点检测一帧也要十几毫秒LBP-TOP虽然慢一些但加上之后提升往往有限。面对一个新数据集我先只跑光流和AUs距离如果验证集F1在0.6以下再考虑纹理特征。2.3 特征级融合与决策级融合两层都用效果才稳多特征融合不是简单拼向量。至少有两个层次特征级融合和决策级融合。特征级融合是在分类器之前把不同特征向量标准化后拼接让SVM、随机森林或MLP去学一个联合分布。优点是信息保留完整缺点是特征维度会膨胀小样本下不降维很容易过拟合。决策级融合是每个特征各自训练分类器最后用投票或概率平均得到最终结果。优点是每个分类器在自己的特征空间里更纯粹不容易被其他特征干扰缺点是如果某个特征本身能力弱投票时反而会拖后腿。我的常用方案是两层都做先做特征级融合训练一个主分类器再把三个独立分类器的概率输出做一次软投票与主分类器结果对比谁在验证集上表现好就用谁。这个策略在微表情这种小数据集上尤其有用因为特征重要性在不同被试者之间波动很大多一层决策融合等于多一个安全网。3. 用OpenCV和dlib把视频变成有效特征预处理与光流提取3.1 环境准备与视频抽帧先搞定“每段表情怎么进入内存”微表情样本是短视频帧数从几十到几百不等。工程第一步不是识别而是把每段视频变成一个可处理的灰度帧序列。后续的光流、纹理、关键点距离都会重复用到这个序列所以提前把灰度帧全部读进内存是划算的。依赖用pip装就够了pip install opencv-python dlib scikit-learn scikit-image matplotlibdlib编译失败很常见。macOS上通常需要cmakeWindows上建议用conda或直接找预编译wheel。实际上dlib的安装难度往往比后面整个识别逻辑都高。如果不想在这里浪费时间可以换成MediaPipe或OpenCV的DNN人脸检测器关键点数量会变但后续逻辑基本不动。读视频的代码我习惯写成这样import cv2 import numpy as np def load_video_gray(video_path, max_framesNone): cap cv2.VideoCapture(video_path) frames [] while True: ok, frame cap.read() if not ok: break gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) frames.append(gray) if max_frames and len(frames) max_frames: break cap.release() return np.stack(frames, axis0)这里返回灰度图因为光流和纹理都只需要单通道。返回数组的维度是[T, H, W]T是帧数H和W是高度与宽度。max_frames是防止长视频吃内存的兜底参数按表情长度设成“3秒乘以帧率”就够。如果视频本身只有30帧这个参数不会影响结果。3.2 人脸关键点对齐没有对齐的多特征融合等于硬拼光流和纹理对位置都有要求。如果第一帧和后续帧人脸有左右晃动算出来的位移是整体刚性运动会把微表情信号盖掉。所以抽光流前我会先做人脸对齐用第一帧的landmark作为参考把每一帧都仿射变换到参考坐标。这里最关键的参数是参考点选取。我一般取左右内眼角点dlib索引39和42连线的中点作为参考中心因为这个位置在人脸上相对稳定受嘴巴动作和眉毛动作影响小。比对中心之后做平移再缩放尺寸后续AUs距离特征才有可比性。import dlib detector dlib.get_frontal_face_detector() predictor dlib.shape_predictor(shape_predictor_68_face_landmarks.dat) def get_landmarks(gray): rects detector(gray, 1) if len(rects) 0: return None return np.array([[p.x, p.y] for p in predictor(gray, rects[0]).parts()]) def align_face(gray, ref_landmarks, size(128, 128)): lm get_landmarks(gray) if lm is None: return None, None ref_center (ref_landmarks[39] ref_landmarks[42]) / 2 cur_center (lm[39] lm[42]) / 2 delta cur_center - ref_center padded cv2.copyMakeBorder(gray, 20, 20, 20, 20, cv2.BORDER_CONSTANT) M np.float32([[1, 0, -delta[0] 20], [0, 1, -delta[1] 20]]) aligned cv2.warpAffine(padded, M, (size[1], size[0])) return aligned, lm - delta这段代码先给灰度图加一圈20像素的padding避免平移后边缘截断。然后做的是平移对齐没有处理旋转和缩放。真实场景里更稳的做法是用cv2.estimateAffinePartial2D(ref_landmarks, cur_landmarks)求一个2x3仿射矩阵一次解决旋转、平移和缩放。上面这个简化版本至少能解决最常见的左右平移避免光流被整体移动污染。对齐后的关键点坐标要除以或加上padding偏移量保证后续AUs距离计算用的是对齐后坐标。3.3 光流特征提取Farneback参数在微表情上的调法Farneback稠密光流是OpenCV自带且不需要深度模型的最实用方案。它在微表情场景下的参数要单独调不能照搬目标跟踪默认值。我常用的参数是pyr_scale0.5, levels3, winsize15, iterations3, poly_n5, poly_sigma1.2。winsize特别敏感太小噪声多太大把相邻区域运动糊在一起对微表情这种局部小运动很致命。def extract_optical_flow_features(gray_frames, landmarks): feature_list [] prev gray_frames[0] for i in range(1, len(gray_frames)): cur gray_frames[i] flow cv2.calcOpticalFlowFarneback( prev, cur, None, pyr_scale0.5, levels3, winsize15, iterations3, poly_n5, poly_sigma1.2, flags0) feat [] for lm in landmarks[i]: x, y int(lm[0]), int(lm[1]) window flow[max(0, y-2):y3, max(0, x-2):x3] feat.append([ window[..., 0].mean(), window[..., 1].mean(), window[..., 0].std(), window[..., 1].std() ]) feature_list.append(np.array(feat).flatten()) prev cur return np.stack(feature_list, axis0)这段代码对每个关键点取周围5x5窗口的光流水平分量和垂直分量各算均值和标准差拼成一个68x4272维的逐帧向量。均值代表局部运动方向和强度标准差代表区域内运动是否一致。如果只是整体平移窗口内标准差会很小只有出现形变时标准差才会变大。这个性质比直接盯原始光流更抗噪声。注意landmarks[i]必须与gray_frames[i]索引一致。我踩过一次坑关键点列表少了最后一帧导致特征矩阵行数和帧数对不上模型训练时直接报维度错误。所以这段函数最好在返回前打印一下feature_list.shape确认行数等于帧数减一。另外Farneback参数在微表情上的经验是levels不要超过5否则小运动被金字塔平滑掉winsize从9、15、21三档里选用小规模训练集验证而不是凭肉眼判断。4. 多特征融合与分类器调参从特征拼接到SVM / 随机森林4.1 三种特征的时间窗聚合把逐帧特征变成样本逐帧特征单看太不稳定裸着丢给分类器会学出一堆噪声。标准做法是滑窗聚合每个窗口内做统计量计算。统计量取均值、标准差、四分位数、最大值、最小值等窗口之间滑动几帧。窗口大小和步长直接决定样本量也决定时间上下文。def aggregate_window(features, window_size30, step5): if len(features) window_size: window_size len(features) windows [] for start in range(0, len(features) - window_size 1, step): seg features[start:start window_size] agg np.concatenate([ seg.mean(axis0), seg.std(axis0), np.percentile(seg, 25, axis0), np.percentile(seg, 75, axis0), seg.max(axis0), seg.min(axis0), ]) windows.append((start, start window_size - 1, agg)) return windows假设一段视频600帧窗口30、步长5会得到大约114个样本。这些样本不是独立样本相邻窗口高度相关所以不能直接随机划分训练测试集必须按原视频分组。我一般把函数返回的起始帧和结束帧也保留下来便于后面调试窗口偏移。窗口聚合后光流特征从272维变成272x61632维如果再加AUs距离和纹理特征维度会更高。4.2 特征融合与降维用标准缩放加PCA防止维度爆炸微表情数据集往往只有几十到几百个视频3000维特征直接喂给支持向量机会发生维度灾难。正确顺序是先拼合、再标准化、再PCA。注意PCA必须在训练集上fit不能全量数据fit否则测试集信息会泄漏到主成分里造成虚高的验证结果。from sklearn.preprocessing import StandardScaler from sklearn.decomposition import PCA def fuse_standardize_pca(feature_set, n_components0.95): merged np.concatenate(feature_set, axis1) scaler StandardScaler() scaled scaler.fit_transform(merged) pca PCA(n_componentsn_components) reduced pca.fit_transform(scaled) print(scaled shape:, scaled.shape, - reduced shape:, reduced.shape) return scaler, pca, reducedn_components0.95表示保留95%方差但很多时候保留95%仍然是上百维对十几个类别的微表情任务来说偏多。我更喜欢把n_components设成固定维数比如30、50、80然后画“维数对验证集F1”的曲线。经验上超过120维后准确率开始回落因为噪声成分进入了主成分。如果是三个特征分别降维再拼接注意每个特征要先独立缩放再拼接不要让某个高维特征天然占主导。4.3 分类器选择与调参为什么SVM在小样本上比深度模型稳微表情数据集小用ResNet、Transformer从头训练基本过拟合除非有大规模预训练模型做微调否则传统机器学习更适合验证特征有效性。带RBF核的SVM是小样本、高维特征的默认选择。它比随机森林更能刻画“微弱但一致”的运动模式代价是需要调C和gamma。from sklearn.svm import SVC from sklearn.model_selection import GridSearchCV clf SVC(kernelrbf, probabilityTrue, random_state42) param_grid {C: [1, 10, 50], gamma: [scale, 0.01, 0.001]} grid GridSearchCV(clf, param_grid, cv5, scoringf1_macro) grid.fit(X_train, y_train) print(best params:, grid.best_params_) print(best score:, grid.best_score_)C控制对误分类的惩罚太大容易过拟合太小欠拟合gamma控制RBF半径太小模型几乎只把样本周围当同类太大又会让边界过于复杂。这里再补一个建议如果类别不平衡给SVC设置class_weightbalanced微表情数据集中“惊讶”类往往远多于“厌恶”类不处理的话模型会偏向样本量大的类别。随机森林可以作对照组好处是几乎不用调参快速验证特征组合是否有效但最终精度通常略逊于调好的SVM。4.4 决策级融合的实现三个分类器做软投票主分类器之外我会为每个特征单独建模做决策级融合。每个特征都走“标准化—PCA—SVM”流程得到三类概率然后取平均或加权平均。微表情特征在不同人群间波动大决策级融合能缓解单特征主导。from scipy.special import softmax prob_list [] for model, scaler, pca in zip(models, scalers, pcas): X_test_scaled scaler.transform(X_test) X_test_reduced pca.transform(X_test_scaled) prob_list.append(model.predict_proba(X_test_reduced)) final_prob sum(softmax(p, axis1) for p in prob_list) / len(prob_list) y_pred np.argmax(final_prob, axis1)这里用softmax把每个SVM的输出概率先归一化再加权平均避免某个模型置信度天然偏高而主导结果。如果某个特征的独立验证F1不到0.5我会在权重里把它调低。特征级融合与决策级融合的结果可以同时保存最后在留一验证阶段比较选稳定的那个。5. 避坑数据集划分、时序对齐与维度问题5.1 样本划分坑按视频片段划分会让验证结果虚高现象用随机KFold做交叉验证训练集F1约0.9测试集也0.85换成新人的样本直接掉到0.4。原因同一被试者的多个视频被同时放进了训练集和测试集模型实际上在学“这个人”的面部特征而不是“这个表情”的通用模式测试时遇到同一个人自然得分高。解决使用按被试者ID分组的分组交叉验证例如GroupKFold或LeaveOneGroupOut。每个样本需要带一个subject_id它来自数据集标签中的人类编号。代码如下from sklearn.model_selection import GroupKFold gkf GroupKFold(n_splits5) for train_idx, test_idx in gkf.split(X, y, groupssubject_ids): X_train, X_test X[train_idx], X[test_idx] y_train, y_test y[train_idx], y[test_idx] # 在train上fit scaler和pca再转换test注意fit、transform的顺序不能错。所有标准化和降维都应在训练集上fit测试集只用transform。这是小样本实验最容易隐含泄漏的地方。5.2 光流噪声坑参考帧没选好光流特征乱跳现象明明没有表情每隔几帧光流特征剧烈跳动训练出来的模型对中性帧也给出高置信度。原因连续帧逐帧差分摄像头噪声和轻微头部晃动都被当成运动如果视频本身是压缩过的块效应也会放大。解决选择表情开始前的中性帧作为参考帧而不是用上一帧。让所有帧与参考帧做Farneback这样整体运动会被保留但微表情的局部形变也在同一坐标系里。若头部平移较大先做全局对齐再算光流。另外可以对光流场做时间方向的高斯滤波用cv2.GaussianBlur对每一帧的光流分量做平滑消除随机闪烁。5.3 特征维度爆炸坑特征维度超过样本数模型必过拟合现象融合后训练集准确率100%测试集只有50%特征维度远高于样本总数。原因三种特征拼起来3000维但数据只有几百个样本分类器把噪声当成边界记下来了。解决在训练集内部做嵌套交叉验证对每个PCA维数候选值跑一遍分组交叉验证选验证集F1最高的维数。粗略的规则是降维后维数不要超过样本数的20%。比如300个样本主成分最多保留60个。另外可以先对每个特征分别降维到50维再拼接拼接后总量也不会太大如果拼接后仍然过拟合就把决策级融合作为主要方案。5.4 数据集标注起始帧坑别把标注帧直接当表情起点现象按数据集给的表情起始帧对齐窗口训练出来的模型总是偏向于“表情已经发生”对真正过渡阶段反应迟钝。原因标注者标记的起始帧往往是微表情已经能够被目视识别的帧而微表情有一个从中性面到峰值的酝酿过程。直接以该帧为起点做滑窗会丢掉最关键的起始运动。解决把窗口起点前移5到10帧具体帧数取决于视频帧率。60fps的视频前移10帧是167毫秒30fps的视频前移5帧也是167毫秒正好覆盖微表情酝酿阶段。如果数据集提供的是峰帧而不是起始帧这个前移量要更大。另一个思路是在时间上做数据增强同一个视频采样多个偏移窗口起点随机落在起始帧前8帧到起始帧之间。6. 验证模型到底有没有用留一交叉验证与混淆矩阵的进阶技巧6.1 用留一被试者交叉验证替代普通KFold分组KFold能整体看性能但微表情数据集小我更推荐留一被试者交叉验证LeaveOneGroupOut。每次用一个被试者的全部样本做测试其他被试者训练模拟“对没见过的人识别微表情”的真实场景。虽然训练次数多但小数据集上一次LOSO几分钟就跑完值得做。from sklearn.model_selection import LeaveOneGroupOut logo LeaveOneGroupOut() for train_idx, test_idx in logo.split(X, y, groupssubject_ids): # train model on train_idx, collect predictions on test_idx最终把所有测试块预测结果汇总计算macro F1和准确率。相比普通交叉验证这个分数才算真正可信。6.2 混淆矩阵和帧级窗口级评估的差别评估不能只看准确率因为类别不平衡严重。我习惯打印macro F1、召回率和混淆矩阵。混淆矩阵能告诉你哪些类别总被混淆。例如微表情里的“厌恶”和“悲伤”在嘴角运动上相似AUs距离特征如果只选垂直距离会分不开必须加入特定的水平方向特征。此外要区分“帧级评估”和“窗口样本级评估”。帧级评估会虚高因为相邻帧高度相似相当于同一画面被重复评分。只用窗口聚合后独立样本做评估结论才可信。6.3 一个能提升鲁棒性的时序投票技巧如果最终产品要对整个视频给一个分类结果可以做窗口间投票。把每个窗口产生的概率累加最终得分最高的类别作为视频预测。我更倾向对窗口概率施加指数衰减权重越靠近峰值的窗口权重越大但具体峰值位置不确定时均匀累加更安全。final_scores np.zeros((n_classes,)) for window_prob in window_probs: final_scores window_prob pred np.argmax(final_scores)微表情是一个弱信号任务单个窗口可能落在噪声上但多数窗口的贡献会累积到真实类别。经验是至少统计3秒窗口内的概率不要看单帧。我看过太多人因为窗口投票后准确率上升了几个点反过来验证出特征融合是否有效的例子。最后说一点个人习惯。我给自己定了个死规矩每次实验记录三个数字——窗口大小、PCA维数、特征组合。微表情数据集太小任何一次预处理改动都会显著影响结论没有记录就等于白做。把这些变量固化成实验模板你会少走很多弯路。希望帮到你。本文还有配套的精品资源点击获取
返回列表