ARTICLE DETAIL

资讯详情

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

Gabor+PCA+LDA+SVM人脸表情识别:完整Python源码与工程实践

Gabor+PCA+LDA+SVM人脸表情识别:完整Python源码与工程实践 简介一套面向人脸表情与微表情识别任务的Python项目源码主要服务对图像处理与机器学习感兴趣的开发者、研究者与相关专业学生。系统以Gabor滤波提取人脸纹理特征结合PCA与LDA实现有效降维再通过SVM完成表情分类并基于PyQt搭建了可交互的图形界面。项目共808个文件压缩包约35.85MB其中包含749个JPG图片训练/测试用样本、21个XML配置、6个训练好的模型文件、5个Python源文件、5个数据库文件以及UI、DLL、可执行工具等各类型文件分工明确便于整体理解与二次开发。已有410人学习下载。值得强调的是源码附带重新编译的libSVM库支持多线程训练可明显提升模型训练效率从数据、特征提取、降维、分类到GUI设计流程完整适合作为传统机器学习表情识别方向的参考实现或毕设项目。1. 基于 Gabor 滤波、PCALDA 降维和 SVM 分类的人脸表情识别一套能直接跑通的 Python 源码拿到这套源码之前我以为又是常见的半成品仓库。打开一看808 个文件里光 JPG 表情图就 749 张加上训练好的模型文件、XML 配置、PyQt 界面和重新编译过、支持多线程的 libSVM基本是把一条完整链路铺开了Gabor 滤波提取纹理特征PCALDA 接力降维SVM 分类PyQt 兜底做图形界面。它解决的问题不是“能不能识别出笑脸”的演示级需求而是从数据组织、特征提取、模型训练到 GUI 落地的全流程工程。适合两类人一是做人脸表情识别课题、需要一套可复现基线系统的学生二是想把手头传统机器学习方案整理成工具的工程师。下面按我拆包复现的顺序讲。2. 算法链路拆解Gabor 滤波、PCALDA 和 SVM 为什么是表情识别的老牌黄金组合2.1 Gabor 滤波纹理特征提取为什么选它而不是 CNNGabor 滤波器本质上是一组带方向的带通滤波器核函数由高斯包络和正弦平面波相乘得到。调不同的尺度 sigma 和方向 theta就能在图像不同的频率段、不同的朝向上产生选择性响应。人脸做表情时眼角鱼尾纹、嘴角纹路、眉部肌肉的方向和深浅都在变Gabor 特征恰好把这些局部纹理变化编码成一组多方向响应值这也是它在传统人脸表情识别里长期占据主流位置的原因。常见实现是取 5 个尺度、8 个方向生成 40 个 Gabor 核对每张灰度人脸图做卷积。输出是一组复数响应工程上取幅值作为特征。如果你打开项目里的 Python 源文件大概率会看到一个 build_gabor_kernels 之类的函数里面用 cv2.getGaborKernel 循环生成核再用 filter2D 做卷积。用 OpenCV 而不是自己写卷积是因为 OpenCV 的 filter2D 底层做了优化40 个核卷积一张 128×128 的图也就几十毫秒。# Gabor 核生成与特征提取OpenCV 实现 import cv2 import numpy as np def build_gabor_kernels(ksize31, sigmas(2, 4, 8, 16), thetas(0, 45, 90, 135)): kernels [] for sigma in sigmas: for theta in thetas: kernel cv2.getGaborKernel( (ksize, ksize), sigma, np.deg2rad(theta), lambd10.0, gamma0.5, psi0 ) kernels.append(kernel) return kernels kernels build_gabor_kernels() def extract_gabor_features(face_gray): face_gray cv2.resize(face_gray, (128, 128)) responses [cv2.filter2D(face_gray, cv2.CV_32F, k) for k in kernels] return np.concatenate([r.ravel() for r in responses])这里的参数不是唯一解。我一般会先用 4 尺度、4 方向把流程跑通再根据识别率决定要不要扩到 5 尺度、8 方向。ksize 默认 31核太大卷积变慢核太小低频纹理响应不足。lambd 设 10 相当于波长接近核尺寸的三分之一gamma 0.5 是常见的椭圆纵横比。你需要知道的是这些参数影响的是特征的表征能力不是分类器的正确性后面调 SVM 往往比调 Gabor 参数收益更直接。2.2 PCALDA先无监督降维再监督降维两次降维各干各的Gabor 特征展开后维度非常吓人。一张 128×128 的灰度图配 40 个核展平后单张特征就是 128×128×40约 65 万维直接喂给 SVM 的话核矩阵计算会慢到让人怀疑人生。所以必须降维。PCA 是第一级降维它只在乎方差不在乎类别把原始高维特征投影到方差最大的若干个主成分方向上。PCA 在人脸识别里有个耳熟能详的名字——特征脸eigenface。原始论文把所有训练人脸做主成分分解第一个主成分就是一张平均脸后面的主成分对应不同方向的差异模式。表情识别里的 PCA 同样是这个逻辑主成分向量看起来像某种轮廓模板并不是可解释的语义特征。这里有个值得记住的点PCA 是无监督的它保留的是人脸图像里整体差异最大的结构这里面既有表情差异也有身份差异、光照差异。如果不做任何处理直接分类模型可能会把身份信息当成表情信息。LDA 是第二级降维它专门解决 PCA 忽略类别的问题。LDA 的目标是让类间散度最大、类内散度最小这个准则在文献里叫 Fisher 准则。基本表情通常定义为 7 类生气、厌恶、恐惧、开心、悲伤、惊讶、中性LDA 的投影维度上限是类别数减一也就是最多 6 维。实际流程是 PCA 先降到 100~300 维LDA 再压到 6 维最后 SVM 面对的是一个维数极低、判别力很强的特征。# PCA LDA 降维的典型写法 from sklearn.decomposition import PCA from sklearn.discriminant_analysis import LinearDiscriminantAnalysis pca PCA(n_components120, whitenTrue) X_pca pca.fit_transform(X_train) # X_train 是 Gabor 特征矩阵 lda LinearDiscriminantAnalysis(n_components6) X_lda lda.fit_transform(X_pca, y_train) # y_train 是表情标签逻辑说明pca 和 lda 都在训练集上 fit测试阶段只能 transform不能重新 fit。whitenTrue 会在 PCA 后把各主成分方差归一化这对后续 LDA 和 SVM 都有好处。n_components120 可以理解为保留了约 95% 方差的经验值实际看 explained_variance_ratio_ 的累计曲线来定。LDA 的 n_components 设成 6 是因为 7 类表情最多 6 个判别方向设多了 sklearn 会警告并自动截断。2.3 SVM 分类与 libSVM 多线程重编译命令行工具不是落后是快SVM 分类器的核心是找最大间隔超平面对非线性问题用核函数把样本映射到高维空间。表情识别里最常用的是 RBF 核两个关键参数是 -c惩罚系数和 -gRBF 核的 gamma。SVM 的优化本质上是在解一个凸二次规划问题不像神经网络那样靠梯度下降迭代这让它在小样本场景下更稳、结果可复现。这个项目有个容易被忽略的亮点libSVM 被重新编译成了支持多线程的版本压缩包里那 4 个可执行文件——svm-train、svm-scale、svm-predict、svm-toy——就是编译产物。为什么要重新编译原版 libSVM 训练是单线程的做交叉验证和网格搜索时每个参数组合都串行跑数据集一大就很煎熬。多线程版本把核矩阵计算并行化一个 7 分类、几百张训练样本的任务训练时间能从十几分钟压到几分钟。Python 代码里通过 svmutil 调用 libSVM 的训练逻辑训练和预测都走 C 实现比纯 sklearn 的 SVC 在同样参数下快不少。代价是输入格式要转成 libSVM 的列表格式特征矩阵的每一行变成普通的嵌套列表svm_problem 和 svm_parameter 这两个类就是干这个的。# 用 svmutil 调用重编译后的 libSVM from svmutil import svm_problem, svm_parameter, svm_train, svm_predict # y 是标签列表x 是特征矩阵list of list problem svm_problem(y.tolist(), X_lda.tolist()) param svm_parameter(-t 2 -c 16 -g 0.0078125 -b 1) model svm_train(problem, param) model.save(expression_model.libsvm)参数说明-t 2 选 RBF 核-c 16 是惩罚系数越大越不允许训练样本被分错但也越容易过拟合-g 0.0078125 是 2 的 -7 次方来自一次网格搜索的中间值-b 1 训练概率模型识别时能输出每个类别的置信度。这个组合是我在 JAFFE 风格数据集上常用的起点不是最优解后面第 4 章会讲怎么自己搜。3. 从源码到跑通环境搭建、数据组织和训练/识别两条主流程3.1 环境搭建Python 版本、依赖库和 libsvm.dll 的位数匹配这套源码的依赖不算复杂但有个容易翻车的点libsvm.dll 是编译好的二进制Python 解释器位数必须和它一致。Windows 下如果装了 64 位 Python却拿了 32 位编译的 libsvm.dllimport svmutil 时大概率直接报 OSError: [WinError 193]说 %1 不是有效的 Win32 应用程序。反过来也一样。所以第一步先确认你本机 Python 的位数再看项目里的 dll 是哪一种。依赖库方面建议用干净的虚拟环境安装。常见的坑是 PyQt5 和 OpenCV 的版本冲突特别是老代码里用的 PyQt5 5.15 以下版本在 Windows 上可能有平台插件问题。我一般用 Python 3.8 或 3.9 跑这类项目numpy 用 1.21 左右OpenCV 用 opencv-python 4.5 左右PyQt5 用 5.15.4。sklearn 记得装完整版因为 LinearDiscriminantAnalysis 在旧版本里对应的是 sklearn.discriminant_analysis 模块新老版本导入路径略有差别。# 创建虚拟环境并安装依赖Windows / Linux 通用 python -m venv expr_env source expr_env/bin/activate # Windows 下是 expr_env\Scripts\activate pip install numpy1.21.6 opencv-python4.5.5.64 PyQt55.15.4 scikit-learn pip install libsvm # 或者把项目里的 libsvm 目录加入 PYTHONPATH说明libsvm 的 pip 包不一定和项目里重新编译的 dll 完全兼容更稳的做法是直接把项目根目录加入 sys.path让 import svmutil 优先加载项目自带的版本。检查方式是在 Python 里执行 import svmutil; print(svmutil.file)确认路径是项目目录下的 svmutil.py。提示如果 import svmutil 报错先检查 Python 位数和 dll 位数是否一致这个排查顺序能省下不少时间。3.2 数据组织749 张 JPG 表情图怎么安排目录才能被脚本正确读取这个项目里 749 张 JPG 图片是核心数据集。拿到压缩包后先别急着跑训练看目录结构。常见组织方式有两种一是按人物 ID 分文件夹每个人物文件夹下是同一人的多种表情二是按表情类别分文件夹比如 anger/、happy/、sad/ 这种。源码里的训练脚本通常写死了其中一种路径规则直接改路径前缀就行。如果是按人物分文件夹训练脚本里一般会有一个映射表把文件名里的表情后缀比如 xxx_HA.jpg 里的 HA映射成标签。如果是按类别分文件夹脚本会把目录名当作标签循环读取更简单。我建议拿到源码后先打印一遍加载到的样本数和标签分布确认没有图片因为文件名编码问题被漏读。另外解压后如果看到一堆 Thumbs.db 文件那是 Windows 的缩略图缓存直接删掉不影响任何代码别被文件数量吓到。# 检查数据集加载是否正常的辅助脚本 import os import cv2 data_dir dataset count 0 labels {} for root, dirs, files in os.walk(data_dir): for fname in files: if fname.lower().endswith((.jpg, .jpeg, .png)): img_path os.path.join(root, fname) img cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) if img is None: print(无法读取, img_path) continue # 标签取上一级目录名 label os.path.basename(root) labels[label] labels.get(label, 0) 1 count 1 print(有效图片数, count) print(标签分布, labels)这段脚本的作用是快速定位两类问题图片损坏导致 cv2.imread 返回 None以及标签分布严重不均衡。如果某类只有十几张训练出的模型对该类几乎不会有好效果后面避坑章节会专门讲。3.3 训练主流程从原始图片到 .libsvm 模型文件训练脚本的完整流程是读图 → Gabor 卷积 → PCA → LDA → svm_train。代码不长但顺序不能乱。下面是我重新组织过的训练脚本骨架和项目源码的调用关系一致只是把关键节点拆开加了注释。# train_expression.py完整训练流程 import os import cv2 import numpy as np import pickle from sklearn.decomposition import PCA from sklearn.discriminant_analysis import LinearDiscriminantAnalysis from svmutil import svm_problem, svm_parameter, svm_train # 配置区 DATA_DIR dataset/train IMG_SIZE (128, 128) GABOR_SIGMAS (2, 4, 8, 16) GABOR_THETAS (0, 45, 90, 135) PCA_DIM 120 LDA_DIM 6 SVM_OPTS -t 2 -c 16 -g 0.0078125 -b 1 # 1. 加载图像 def load_data(data_dir): X, y, names [], [], [] for label, cat in enumerate(sorted(os.listdir(data_dir))): cat_dir os.path.join(data_dir, cat) if not os.path.isdir(cat_dir): continue for fname in os.listdir(cat_dir): img cv2.imread(os.path.join(cat_dir, fname), cv2.IMREAD_GRAYSCALE) if img is None: continue img cv2.resize(img, IMG_SIZE) X.append(img) y.append(label) names.append(cat) return X, y, names X_raw, y, names load_data(DATA_DIR) # 2. Gabor 特征提取 kernels [] for sigma in GABOR_SIGMAS: for theta in GABOR_THETAS: kernels.append(cv2.getGaborKernel( (31, 31), sigma, np.deg2rad(theta), lambd10.0, gamma0.5, psi0 )) def gabor_forward(img): feats [] for k in kernels: resp cv2.filter2D(img, cv2.CV_32F, k) feats.append(resp.ravel()) return np.concatenate(feats) X_feat np.array([gabor_forward(img) for img in X_raw]) # 3. PCA LDA 降维 pca PCA(n_componentsPCA_DIM, whitenTrue) X_pca pca.fit_transform(X_feat) lda LinearDiscriminantAnalysis(n_componentsLDA_DIM) X_lda lda.fit_transform(X_pca, y) # 4. 训练 SVM prob svm_problem(y, [row.tolist() for row in X_lda]) param svm_parameter(SVM_OPTS) model svm_train(prob, param) model.save(expression_model.libsvm) # 5. 保存 PCA/LDA 对象识别阶段要用 with open(pca_lda.pkl, wb) as f: pickle.dump({pca: pca, lda: lda, names: names}, f)逻辑说明第 2 步的 gabor_forward 对每张图做卷积并拼接特征这一步的向量长度是 128×128×16约 26 万维内存占用不小图片数量多的机器建议分批处理。第 3 步 PCA 和 LDA 的顺序不能换先无监督后有监督。第 5 步保存 pca_lda.pkl 是很多人会忘的没有它识别阶段根本无法把测试图片变换到同一特征空间。参数说明GABOR_SIGMAS 和 GABOR_THETAS 控制纹理提取的尺度和方向16 个卷积核是起步配置PCA_DIM 设 120 是经验值想在性能和准确率之间折中可以先用这个数再看累计方差曲线调整SVM_OPTS 里的 -b 1 表示输出概率后面做置信度过滤会用到。3.4 识别流程单张图片的表情预测与置信度输出识别脚本要做的事恰好是训练的反向展开读图 → 同样的 Gabor 卷积 → pca.transform → lda.transform → svm_predict。注意这里只用 transform不重新 fit否则特征空间完全对不上。# predict_expression.py加载模型预测新图片 import cv2 import numpy as np import pickle from svmutil import svm_load_model, svm_predict with open(pca_lda.pkl, rb) as f: saved pickle.load(f) pca saved[pca] lda saved[lda] labels saved[names] model svm_load_model(expression_model.libsvm) # 和训练时保持完全一致的 Gabor 核配置 kernels [] for sigma in (2, 4, 8, 16): for theta in (0, 45, 90, 135): kernels.append(cv2.getGaborKernel( (31, 31), sigma, np.deg2rad(theta), lambd10.0, gamma0.5, psi0 )) def predict_expression(img_path): img cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) if img is None: raise ValueError(f无法读取图片: {img_path}) img cv2.resize(img, (128, 128)) feats [] for k in kernels: resp cv2.filter2D(img, cv2.CV_32F, k) feats.append(resp.ravel()) X np.concatenate(feats).reshape(1, -1) X_pca pca.transform(X) X_lda lda.transform(X_pca) # 第一个参数是占位标签svm_predict 不依赖它 p_label, _, p_vals svm_predict([0], X_lda.tolist(), model, -b 1) proba max(p_vals[0]) return labels[p_label[0]], proba expr, conf predict_expression(test/happy.jpg) print(f预测表情: {expr}, 置信度: {conf:.4f})这里要提醒一个细节svm_predict 返回的 p_vals 是每个类别的概率列表顺序和训练时的类别顺序一致。如果你发现预测结果总是错位十有八九是训练和识别两个脚本里的类别排序不一致。训练脚本里用 sorted(os.listdir) 固定顺序识别脚本里保存 labels 列表两边对齐了才不会出问题。4. 参数调优指南Gabor 核、PCA 维度和 SVM 参数怎么一步步找到合适值4.1 Gabor 参数先用 4 尺度 4 方向跑通再决定要不要加细Gabor 滤波器的参数空间是连续的尺度、方向、波长、纵横比、相位每个都能调。但对表情识别来说真正值得花时间的是尺度和方向。尺度 sigma 越小核越小响应的是高频细节比如细皱纹和眼睛边缘sigma 越大响应的是低频轮廓比如脸颊和下巴的大块形状。表情既需要高频细节也需要低频结构所以用多尺度组合。方向方面人脸表情的纹理方向不是均匀分布的嘴角和眼角的皱纹方向因表情而异。4 方向0、45、90、135是最低配置8 方向在每个 22.5 度加一个核特征更细腻但特征维度翻倍。我的建议是先用 4 尺度 4 方向跑通全流程记录基线准确率再扩到 5 尺度 8 方向对比如果提升不到 1 个百分点就不值得为它付出翻倍的内存和训练时间。还要注意 Gabor 参数和数据分辨率的关系。同样是 sigma 2~32如果人脸图是 64×64大 sigma 的核几乎覆盖整张脸特征全是低频轮廓如果人脸图是 256×256小 sigma 的核连眉毛都抓不完整。所以换数据集时要同步调整 IMG_SIZE 和 sigma 范围常见做法是让最大 sigma 大约是人脸图短边的八分之一。4.2 PCA 维度选择别再拍脑袋设 120看累计方差曲线PCA 的 n_components 可以拍脑袋但更好的做法是画一条 cumulative explained variance ratio 曲线。sklearn 里 fit 完之后explained_variance_ratio_ 记录了每个主成分解释的方差比例累加后就能看到通常前 50 个主成分就解释了 80% 以上方差前 120 个到了 95% 左右再往后曲线明显变平。这意味着一味增加 PCA 维度对 SVM 的提升很小却会让 LDA 面临更多的噪声维度。还有个和 PCA 紧密相关的坑whiten 参数。whitenTrue 会对降维后的特征做方差归一化让每个主成分尺度一致。这对 SVM 是友好的因为 SVM 对特征尺度敏感。但如果你的 Gabor 特征已经做了归一化whiten 会让某些方差很小但判别力强的维度被放大噪声。我一般在 Gabor 特征归一化后用 whitenFalse在原始特征直接降维时用 whitenTrue两种配置跑一遍对比选验证集准确率高的。关于 PCA 降维后为什么还要 LDA有个很容易混淆的点PCA 已经把维度从 26 万压到 120 了直接用这 120 维训练 SVM 行不行技术上完全可行实际准确率也不会太差。但 LDA 这步能把 120 维继续压到 6 维而且这 6 个维度是朝着最大化类间可分性方向投影的SVM 在 6 维空间里只需要一个很简单的决策面就能分开。代价是损失一部分信息如果数据集很小LDA 的类内散度矩阵估计不准反而会拖累效果。所以在样本不足 50 张每类时可以考虑跳过 LDA。4.3 SVM 参数C 和 gamma 的网格搜索是唯一不靠玄学的调参方式SVM 的 RBF 核有两个参数C 和 gamma。C 是误分类惩罚项越大越严格越小越容忍错误gamma 是 RBF 核的宽度参数越大决策边界越复杂越小越平滑。这两个参数互相影响不存在单独调一个就能到最优的说法。最稳的做法是网格搜索加交叉验证把候选值排列组合每组跑 K 折交叉验证取平均准确率最高的组合。libSVM 自带的 grid.py 脚本就是干这个的但项目里重新编译的 svm-train 支持多线程网格搜索同样受益。你也可以在 Python 里用 sklearn 的 GridSearchCV 包一层只是要注意 sklearn 的 SVC 和 libSVM 的参数含义相同、默认值不同直接用项目里训练好的模型时参数必须用 libSVM 的写法。# 用 sklearn 做一次小规模网格搜索找到 C 和 gamma 的起点 from sklearn.model_selection import GridSearchCV, StratifiedKFold from sklearn.svm import SVC param_grid { C: [1, 4, 16, 64], gamma: [0.001, 0.004, 0.016, 0.064] } svc SVC(kernelrbf, probabilityTrue) grid GridSearchCV(svc, param_grid, cvStratifiedKFold(5), scoringaccuracy) grid.fit(X_lda, y) print(best params:, grid.best_params_) print(best score:, grid.best_score_)这段代码在 LDA 降维后的特征上做 5 折交叉验证16 个参数组合每次训练 5 次共 80 次 SVM 训练。特征维度只有 6 维、样本几百张的话几十秒就能跑完。找到最优 C 和 gamma 后再把这些值填回 libSVM 的 svm_parameter 重新训练完整数据集。网格搜索的范围一般按 2 的幂次取C 从 2 的 -2 次方到 2 的 6 次方gamma 先按 1/特征维度的量级估算再向两边扩展。环节参数常见范围调整策略Gaborsigma 尺度2~32小值抓皱纹大值抓轮廓按数据集分辨率缩放Gabortheta 方向4~8 个方向从 4 方向起步对比 8 方向收益Gaborksize 核大小21~41核越大越慢边界效应越明显PCAn_components50~300看累计方差曲线取 95% 对应值PCAwhitenTrue/False特征已归一化时优先 FalseLDAn_components类别数-17 类表情固定 6设更大自动截断SVMC1~64网格搜索配合交叉验证SVMgamma0.001~0.1网格搜索和 C 同时调5. 实战避坑这套表情识别项目最常见的五个坑5.1 现象import svmutil 报 OSError提示 WinError 193 或找不到指定的模块我在 Windows 上第一次跑这套源码就撞上了。Python 是 64 位项目里的 libsvm.dll 却是 32 位的import 时直接崩。原因很直接libSVM 的 C 源代码编译产物和 Python 解释器位数不匹配。解决方法是把项目里的 svmutil.py、svm.py 和对应位数的 libsvm.dll 放在一起或者重新编译。常见做法是装一个与 dll 位数一致的 Python 解释器64 位系统默认装 64 位 Python就找一个 64 位重编译的 libSVM 替换。注意还有一个小概率原因是系统缺少 VC 运行库。如果位数匹配仍然报错先装 Visual C Redistributable再试 import。5.2 现象训练时 PCA 和 LDA 都正常测试时准确率突然暴跌到接近随机猜测这是最隐蔽的坑之一。原因几乎可以肯定是测试脚本里对 PCA/LDA 重新做了 fit而不是用训练好的对象 transform。一旦重新 fit投影方向完全变了训练时的特征空间和测试时的特征空间根本不在同一个坐标系里。解决方法是把 pca 和 lda 对象 pickle 保存测试脚本里只调用 transform。我在项目里看到过有人把 PCA fit 写在 predict 函数里这种代码跑出来的准确率没有参考价值。5.3 现象Gabor 特征提取时内存占用暴涨训练到一半被系统杀掉一张 128×128 的图配 16 个 Gabor 核单张特征约 26 万维749 张图就是 1.9 亿个浮点数按 float32 算约 800MB。这还不算 PCA 内部要算协方差矩阵。原因就是特征矩阵每一行都保存了完整卷积结果没有做任何压缩。解决思路有两个一是把 Gabor 特征做降采样或分块统计比如把每张卷积响应图分成 4×4 的块取均值维度直接降到原来的 1/16判别力损失不大二是分批计算先算一批图片的 Gabor 特征PCA 拟合后立即丢弃原始特征矩阵只保留降维结果。5.4 现象识别结果总是偏向某一类比如动不动就预测成“开心”原因通常是训练集类别不均衡。749 张图如果 7 类表情分布不均匀SVM 会倾向于把边界样本分到样本量大的那一类。解决方法是先做类别均衡化按最少类别的样本数做下采样或者对少量类别做数据增强水平翻转、小幅旋转、亮度抖动。另外 LDA 本身对类别先验敏感如果原始数据不均衡LDA 投影方向会偏向大类必须在前一步就处理好。5.5 现象PyQt 界面能打开但摄像头画面黑屏或者点识别按钮程序直接未响应这类问题一半出在 OpenCV 和 PyQt 的事件循环冲突上。cv2.VideoCapture 的视频帧读取如果没有放在独立线程里会阻塞 PyQt 的 GUI 事件循环界面直接卡死。另一半出在摄像头索引上笔记本自带摄像头通常是 0外接 USB 摄像头可能是 1但有些机器 0 被虚拟摄像头占用导致黑屏。解决方法是把摄像头读取放到 QThread 里每读一帧通过信号发回主界面摄像头索引用 0、1、2 逐个尝试打印出能成功打开的索引。6. 进阶验证用 PyQt 做实时识别前先跑一遍混淆矩阵6.1 混淆矩阵比准确率更能说明问题准确率在类别均衡时有一定参考价值但表情识别里真正要看的混淆矩阵。比如“恐惧”和“惊讶”经常互相混那是因为这两类表情的眼部和嘴部特征确实接近错误集中在相邻类别说明模型学到了合理特征错误分散则说明特征提取环节有问题。画矩阵之前先保证测试集的类别分布和训练集一致否则对角线上的数字没有可比性。from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay import matplotlib.pyplot as plt # y_true 是测试集真实标签y_pred 是模型预测标签 cm confusion_matrix(y_true, y_pred) disp ConfusionMatrixDisplay(cm, display_labelslabels) disp.plot(cmapBlues, xticks_rotation45) plt.tight_layout() plt.savefig(confusion_matrix.png, dpi150)逻辑说明confusion_matrix 的输出是一个 7×7 矩阵第 i 行第 j 列表示真实标签 i 被预测成标签 j 的样本数。配合 display_labels 传入表情名称可视化结果里对角线的颜色越深说明正确率越高。如果发现某两类的混淆数明显偏高可以针对性采集更多区分样本或者回到 Gabor 方向参数里补两个更细的方向。6.2 PyQt 实时识别的最小实现思路实时识别的要点是把摄像头帧读取放到独立线程不要在主线程里循环读帧。收到一帧后先做人脸检测裁剪出人脸区域再送入 predict_expression整个耗时在几十毫秒量级足够流畅。我这里给出一个极简的线程分离写法QThread 里跑 VideoCapture.read()通过 pyqtSignal 把帧传回主线程主线程只负责显示和调用预测函数。不要在主线程里直接 sleep 等帧那会让界面看起来像死掉一样。从那以后我每次在这个项目里改完参数都强制走一遍“训练 → 混淆矩阵 → 网格搜索 → 再训练”的闭环不再只看命令行里那个总准确率。Gabor、PCA、LDA、SVM 这套传统链路虽然不如深度学习方法省心但每个环节都可以被解释、被调试出了问题能定位到具体模块这是它作为教学和工程基线最大的价值。希望帮到你。本文还有配套的精品资源点击获取
返回列表