要结合 tkinter 实现图片上传交互,同时核心逻辑仅依赖 OpenCV、无其他第三方库,并且要做一个复杂度更高的人脸表情识别程序——舍弃深度学习框架,完全基于 OpenCV 原生的传统计算机视觉技术(HOG+SVM、LBPH 特征、多尺度人脸检测、形态学优化)来实现,同时保证程序的鲁棒性和功能完整性。


import tkinter as tk
from tkinter import filedialog, Label, Button, Text, Scrollbar, messagebox, Frame
import cv2
import numpy as np
import os
import glob
# -------------------------- 核心配置(纯OpenCV) --------------------------
# 表情标签映射(7类)
EMOTION_LABELS = {
0: "中性 (Neutral)",
1: "开心 (Happy)",
2: "悲伤 (Sad)",
3: "愤怒 (Angry)",
4: "惊讶 (Surprise)",
5: "恐惧 (Fear)",
6: "厌恶 (Disgust)"
}
# 人脸检测器路径(OpenCV自带)
FACE_CASCADE_PATH = cv2.data.haarcascades + "haarcascade_frontalface_default.xml"
# 表情样本库路径(需自行创建,格式:0-6文件夹对应7类表情)
EMOTION_DATASET_PATH = "emotion_dataset"
# SVM模型保存路径
SVM_MODEL_PATH = "emotion_svm_model.xml"
# 人脸预处理尺寸
FACE_SIZE = (128, 128)
# HOG特征参数(手工设计,适配表情识别)
HOG_PARAMS = {
"win_size": (64, 64),
"block_size": (16, 16),
"block_stride": (8, 8),
"cell_size": (8, 8),
"nbins": 9
}
# -------------------------- 纯OpenCV工具类(无其他依赖) --------------------------
classOpenCVEmotionRecognizer:
def__init__(self):
# 加载人脸检测器
self.face_cascade = cv2.CascadeClassifier(FACE_CASCADE_PATH)
if self.face_cascade.empty():
raise Exception("人脸检测器加载失败!请检查OpenCV安装是否完整")
# 初始化SVM分类器
self.svm = cv2.ml.SVM_create()
self.svm.setType(cv2.ml.SVM_C_SVC)
self.svm.setKernel(cv2.ml.SVM_RBF) # 径向基核,适配非线性表情特征
self.svm.setC(10.0)
self.svm.setGamma(0.01)
# 加载/训练模型
self._load_or_train_model()
def_preprocess_face(self, face_gray):
"""纯OpenCV人脸预处理:灰度化、均衡化、去噪、归一化"""
# 1. 直方图均衡化(增强表情纹理)
equalized = cv2.equalizeHist(face_gray)
# 2. 双边滤波去噪(保留边缘,适合表情纹理)
denoised = cv2.bilateralFilter(equalized, 9, 75, 75)
# 3. 形态学降噪(小核开运算)
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))
morph = cv2.morphologyEx(denoised, cv2.MORPH_OPEN, kernel)
# 4. 尺寸归一化
resized = cv2.resize(morph, FACE_SIZE)
# 5. 像素值归一化(0-1)
normalized = resized.astype(np.float32) / 255.0
return normalized
def_extract_hog_feature(self, face_preprocessed):
"""纯OpenCV提取HOG特征(表情核心特征)"""
# 调整为HOG窗口尺寸
hog_face = cv2.resize(face_preprocessed, HOG_PARAMS["win_size"])
# 初始化HOG检测器
hog = cv2.HOGDescriptor(
HOG_PARAMS["win_size"],
HOG_PARAMS["block_size"],
HOG_PARAMS["block_stride"],
HOG_PARAMS["cell_size"],
HOG_PARAMS["nbins"]
)
# 计算HOG特征
hog_features = hog.compute(hog_face).reshape(1, -1)
return hog_features.astype(np.float32)
def_extract_lbph_feature(self, face_preprocessed):
"""纯OpenCV提取LBPH特征(补充局部纹理)"""
# LBPH参数(适配表情纹理)
lbph = cv2.face.LBPHFaceRecognizer_create(
radius=1, neighbors=8, grid_x=8, grid_y=8
)
# 构造伪标签(仅用于计算特征)
fake_labels = np.array([0], dtype=np.int32)
# 训练LBPH(实际是提取特征)
lbph.train([(face_preprocessed * 255).astype(np.uint8)], fake_labels)
# 获取LBPH特征(权重矩阵)
lbph_features = lbph.getHistograms()[0].reshape(1, -1).astype(np.float32)
return lbph_features
def_fuse_features(self, hog_feat, lbph_feat):
"""特征融合:HOG+LBPH拼接,提升识别鲁棒性"""
# 特征归一化(L2归一化)
hog_norm = cv2.normalize(hog_feat, None, norm_type=cv2.NORM_L2)
lbph_norm = cv2.normalize(lbph_feat, None, norm_type=cv2.NORM_L2)
# 拼接特征
fused = np.hstack((hog_norm, lbph_norm))
return fused
def_load_or_train_model(self):
"""加载预训练SVM模型,无则从样本库训练"""
if os.path.exists(SVM_MODEL_PATH):
self.svm = cv2.ml.SVM_load(SVM_MODEL_PATH)
print("SVM表情模型加载成功!")
return
# 无模型则训练,先加载样本库
print("未检测到预训练模型,开始从样本库训练...")
features, labels = self._load_dataset()
if features.empty() or len(labels) == 0:
raise Exception("样本库加载失败!请检查{}路径下的样本".format(EMOTION_DATASET_PATH))
# 训练SVM
self.svm.train(features, cv2.ml.ROW_SAMPLE, np.array(labels, dtype=np.int32))
# 保存模型
self.svm.save(SVM_MODEL_PATH)
print("SVM模型训练完成并保存至:{}".format(SVM_MODEL_PATH))
def_load_dataset(self):
"""加载表情样本库(纯OpenCV,无PIL依赖)"""
all_features = []
all_labels = []
# 创建样本库目录(若不存在)
ifnot os.path.exists(EMOTION_DATASET_PATH):
os.makedirs(EMOTION_DATASET_PATH)
for i in range(7):
os.makedirs(os.path.join(EMOTION_DATASET_PATH, str(i)), exist_ok=True)
raise Exception("样本库目录已创建,请在{}下按0-6分类放入表情图片".format(EMOTION_DATASET_PATH))
# 遍历每个表情类别
for label in range(7):
label_dir = os.path.join(EMOTION_DATASET_PATH, str(label))
img_paths = glob.glob(os.path.join(label_dir, "*.jpg")) + glob.glob(os.path.join(label_dir, "*.png"))
for img_path in img_paths:
# 纯OpenCV读取图片
img = cv2.imread(img_path)
if img isNone:
continue
# 人脸检测
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
faces = self.face_cascade.detectMultiScale(
gray, scaleFactor=1.1, minNeighbors=7, minSize=(40, 40)
)
if len(faces) == 0:
continue
# 取最大人脸(最可能是目标)
x, y, w, h = max(faces, key=lambda f: f[2]*f[3])
face_gray = gray[y:y+h, x:x+w]
# 预处理+特征提取+融合
preprocessed = self._preprocess_face(face_gray)
hog_feat = self._extract_hog_feature(preprocessed)
lbph_feat = self._extract_lbph_feature(preprocessed)
fused_feat = self._fuse_features(hog_feat, lbph_feat)
all_features.append(fused_feat)
all_labels.append(label)
# 转换为OpenCV SVM支持的格式
if all_features:
return np.vstack(all_features), all_labels
else:
return np.array([]), []
defdetect_and_recognize(self, img):
"""核心方法:检测人脸+识别表情(纯OpenCV)"""
result_img = img.copy()
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 多尺度人脸检测(优化参数,减少误检)
faces = self.face_cascade.detectMultiScale(
gray, scaleFactor=1.05, minNeighbors=7, minSize=(40, 40),
flags=cv2.CASCADE_SCALE_IMAGE
)
results = []
for idx, (x, y, w, h) in enumerate(faces):
# 裁剪人脸
face_gray = gray[y:y+h, x:x+w]
# 预处理
preprocessed = self._preprocess_face(face_gray)
# 特征提取+融合
hog_feat = self._extract_hog_feature(preprocessed)
lbph_feat = self._extract_lbph_feature(preprocessed)
fused_feat = self._fuse_features(hog_feat, lbph_feat)
# SVM预测
_, pred = self.svm.predict(fused_feat)
emotion_idx = int(pred[0][0])
emotion_name = EMOTION_LABELS[emotion_idx]
# 计算置信度(SVM距离→置信度)
_, resp = self.svm.predict(fused_feat, flags=cv2.ml.SVM_GET_DECISION_FUNCTIONS)
confidence = 100 - (abs(resp[0][0]) / max(abs(resp[0])) * 100)
confidence = round(max(confidence, 0), 2) # 确保非负
# 绘制标注(纯OpenCV)
# 人脸框(绿色)
cv2.rectangle(result_img, (x, y), (x+w, y+h), (0, 255, 0), 2)
# 标签背景(黑色半透明)
label_bg = (x, y-30) if y-30 > 0else (x, y+h+30)
cv2.rectangle(result_img, label_bg, (x+200, label_bg[1]+25), (0, 0, 0), -1)
# 表情标签(白色)
label_text = "{} ({}%)".format(emotion_name.split(" ")[0], confidence)
cv2.putText(
result_img, label_text, (label_bg[0]+5, label_bg[1]+18),
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 1
)
# 保存结果
results.append("人脸{}: {}(置信度:{}%)".format(idx+1, emotion_name, confidence))
if len(faces) == 0:
results.append("未检测到人脸!")
return result_img, results
# -------------------------- tkinter交互层(无PIL依赖) --------------------------
classEmotionApp:
def__init__(self, root):
self.root = root
self.root.title("纯OpenCV人脸表情识别工具(tkinter版)")
self.root.geometry("1100x700")
# 初始化识别器
try:
self.recognizer = OpenCVEmotionRecognizer()
except Exception as e:
messagebox.showerror("初始化失败", str(e))
self.root.quit()
# 初始化变量
self.original_img = None
self.result_img = None
# 创建UI
self._create_ui()
def_create_ui(self):
# 上传按钮
self.upload_btn = Button(
self.root, text="上传图片", command=self._upload_image,
font=("Arial", 12), width=18, height=2
)
self.upload_btn.pack(pady=10)
# 图片展示框架
img_frame = Frame(self.root)
img_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=5)
# 原始图片区域
original_label = Label(img_frame, text="原始图片", font=("Arial", 11))
original_label.grid(row=0, column=0, padx=10)
self.original_img_label = Label(img_frame, bg="#f0f0f0", width=50, height=25)
self.original_img_label.grid(row=1, column=0, padx=10)
# 结果图片区域
result_label = Label(img_frame, text="表情识别结果", font=("Arial", 11))
result_label.grid(row=0, column=1, padx=10)
self.result_img_label = Label(img_frame, bg="#f0f0f0", width=50, height=25)
self.result_img_label.grid(row=1, column=1, padx=10)
# 结果文本区域
result_text_frame = Frame(self.root)
result_text_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=10)
scrollbar = Scrollbar(result_text_frame)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self.result_text = Text(
result_text_frame, font=("Arial", 12), height=6,
yscrollcommand=scrollbar.set
)
self.result_text.pack(fill=tk.BOTH, expand=True)
scrollbar.config(command=self.result_text.yview)
def_cv2_to_tkinter(self, img):
"""纯OpenCV+tkinter格式转换(无PIL依赖)"""
# 调整尺寸(适配UI)
max_size = (400, 400)
h, w = img.shape[:2]
scale = min(max_size[0]/w, max_size[1]/h)
new_w, new_h = int(w*scale), int(h*scale)
resized = cv2.resize(img, (new_w, new_h))
# BGR→RGB(tkinter要求RGB)
rgb_img = cv2.cvtColor(resized, cv2.COLOR_BGR2RGB)
# 转换为tkinter可用格式
img_bytes = cv2.imencode('.png', rgb_img)[1].tobytes()
tk_img = tk.PhotoImage(data=img_bytes)
return tk_img
def_upload_image(self):
"""上传图片并处理"""
file_path = filedialog.askopenfilename(
filetypes=[("图片文件", "*.jpg *.jpeg *.png *.bmp")]
)
ifnot file_path:
return
# 纯OpenCV读取图片
self.original_img = cv2.imread(file_path)
if self.original_img isNone:
messagebox.showerror("错误", "图片读取失败!")
return
# 检测+识别
self.result_img, results = self.recognizer.detect_and_recognize(self.original_img)
# 显示图片(无PIL依赖)
original_tk_img = self._cv2_to_tkinter(self.original_img)
self.original_img_label.config(image=original_tk_img)
self.original_img_label.image = original_tk_img # 保留引用
result_tk_img = self._cv2_to_tkinter(self.result_img)
self.result_img_label.config(image=result_tk_img)
self.result_img_label.image = result_tk_img # 保留引用
# 显示结果文本
self.result_text.delete(1.0, tk.END)
for res in results:
self.result_text.insert(tk.END, res + "\n")
# -------------------------- 程序入口 --------------------------
if __name__ == "__main__":
# 关闭OpenCV无关日志
os.environ["OPENCV_LOG_LEVEL"] = "ERROR"
root = tk.Tk()
app = EmotionApp(root)
root.mainloop()
cv2.imencode + tkinter PhotoImage 原生实现;cv2.ml.SVM;cv2.HOGDescriptor、cv2.face.LBPHFaceRecognizer 等 OpenCV 原生接口。scaleFactor=1.05、minNeighbors=7,减少误检/漏检;cv2.face 模块):pip install opencv-python opencv-contrib-python # contrib版包含LBPH等扩展模块
emotion_dataset 文件夹;emotion_svm_model.xml;遇到的错误核心是 EmotionApp 实例没有 recognizer 属性,这是因为初始化 OpenCVEmotionRecognizer 时抛出了异常(样本库为空/路径问题),导致 self.recognizer 未被成功创建,后续调用就会触发 AttributeError。修复这个错误,并优化程序的异常处理逻辑,确保即使样本库未准备好,程序也能正常运行(提供测试模式)。
OpenCVEmotionRecognizer 时,样本库目录 emotion_dataset 要么不存在,要么为空,触发异常后 self.recognizer 未被赋值;import tkinter as tk
from tkinter import filedialog, Label, Button, Text, Scrollbar, messagebox, Frame
import cv2
import numpy as np
import os
import glob
# -------------------------- 核心配置(纯OpenCV) --------------------------
EMOTION_LABELS = {
0: "中性 (Neutral)",
1: "开心 (Happy)",
2: "悲伤 (Sad)",
3: "愤怒 (Angry)",
4: "惊讶 (Surprise)",
5: "恐惧 (Fear)",
6: "厌恶 (Disgust)"
}
FACE_CASCADE_PATH = cv2.data.haarcascades + "haarcascade_frontalface_default.xml"
EMOTION_DATASET_PATH = "emotion_dataset"
SVM_MODEL_PATH = "emotion_svm_model.xml"
FACE_SIZE = (128, 128)
HOG_PARAMS = {
"win_size": (64, 64),
"block_size": (16, 16),
"block_stride": (8, 8),
"cell_size": (8, 8),
"nbins": 9
}
# -------------------------- 纯OpenCV工具类(修复异常+测试模式) --------------------------
classOpenCVEmotionRecognizer:
def__init__(self, test_mode=False):
"""
初始化识别器
:param test_mode: 测试模式(无样本库时启用,返回随机表情结果,避免程序崩溃)
"""
self.test_mode = test_mode
self.svm_loaded = False# 标记SVM是否加载成功
# 加载人脸检测器(必选,即使测试模式也要检测人脸)
self.face_cascade = cv2.CascadeClassifier(FACE_CASCADE_PATH)
if self.face_cascade.empty():
raise Exception("人脸检测器加载失败!请检查OpenCV安装是否完整(需安装opencv-contrib-python)")
# 非测试模式下加载/训练SVM
ifnot test_mode:
self.svm = cv2.ml.SVM_create()
self.svm.setType(cv2.ml.SVM_C_SVC)
self.svm.setKernel(cv2.ml.SVM_RBF)
self.svm.setC(10.0)
self.svm.setGamma(0.01)
# 加载/训练模型(新增异常捕获)
try:
self._load_or_train_model()
self.svm_loaded = True
except Exception as e:
messagebox.showwarning("模型加载失败", f"无法加载/训练SVM模型:{str(e)}\n将启用测试模式(表情结果为随机模拟)")
self.test_mode = True
else:
self.svm_loaded = False
def_preprocess_face(self, face_gray):
equalized = cv2.equalizeHist(face_gray)
denoised = cv2.bilateralFilter(equalized, 9, 75, 75)
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))
morph = cv2.morphologyEx(denoised, cv2.MORPH_OPEN, kernel)
resized = cv2.resize(morph, FACE_SIZE)
normalized = resized.astype(np.float32) / 255.0
return normalized
def_extract_hog_feature(self, face_preprocessed):
hog_face = cv2.resize(face_preprocessed, HOG_PARAMS["win_size"])
hog = cv2.HOGDescriptor(
HOG_PARAMS["win_size"],
HOG_PARAMS["block_size"],
HOG_PARAMS["block_stride"],
HOG_PARAMS["cell_size"],
HOG_PARAMS["nbins"]
)
hog_features = hog.compute(hog_face).reshape(1, -1)
return hog_features.astype(np.float32)
def_extract_lbph_feature(self, face_preprocessed):
lbph = cv2.face.LBPHFaceRecognizer_create(
radius=1, neighbors=8, grid_x=8, grid_y=8
)
fake_labels = np.array([0], dtype=np.int32)
lbph.train([(face_preprocessed * 255).astype(np.uint8)], fake_labels)
lbph_features = lbph.getHistograms()[0].reshape(1, -1).astype(np.float32)
return lbph_features
def_fuse_features(self, hog_feat, lbph_feat):
hog_norm = cv2.normalize(hog_feat, None, norm_type=cv2.NORM_L2)
lbph_norm = cv2.normalize(lbph_feat, None, norm_type=cv2.NORM_L2)
fused = np.hstack((hog_norm, lbph_norm))
return fused
def_load_or_train_model(self):
if os.path.exists(SVM_MODEL_PATH):
self.svm = cv2.ml.SVM_load(SVM_MODEL_PATH)
print("SVM表情模型加载成功!")
return
print("未检测到预训练模型,开始从样本库训练...")
features, labels = self._load_dataset()
if features.size == 0or len(labels) == 0:
raise Exception(f"样本库加载失败!请检查{EMOTION_DATASET_PATH}路径下是否有有效表情图片(每类至少10张)")
# 训练前先打乱数据(提升泛化能力)
indices = np.arange(features.shape[0])
np.random.shuffle(indices)
features = features[indices]
labels = np.array(labels)[indices]
self.svm.train(features, cv2.ml.ROW_SAMPLE, labels.astype(np.int32))
self.svm.save(SVM_MODEL_PATH)
print(f"SVM模型训练完成并保存至:{SVM_MODEL_PATH}")
def_load_dataset(self):
all_features = []
all_labels = []
# 自动创建样本库目录(无则创建)
ifnot os.path.exists(EMOTION_DATASET_PATH):
os.makedirs(EMOTION_DATASET_PATH)
for i in range(7):
os.makedirs(os.path.join(EMOTION_DATASET_PATH, str(i)), exist_ok=True)
raise Exception(f"样本库目录已创建:{EMOTION_DATASET_PATH}\n请按以下规则放入图片:\n0=中性 1=开心 2=悲伤 3=愤怒 4=惊讶 5=恐惧 6=厌恶")
# 遍历每个表情类别
for label in range(7):
label_dir = os.path.join(EMOTION_DATASET_PATH, str(label))
img_paths = glob.glob(os.path.join(label_dir, "*.jpg")) + glob.glob(os.path.join(label_dir, "*.png"))
for img_path in img_paths:
img = cv2.imread(img_path)
if img isNone:
continue
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
faces = self.face_cascade.detectMultiScale(
gray, scaleFactor=1.1, minNeighbors=7, minSize=(40, 40)
)
if len(faces) == 0:
continue
x, y, w, h = max(faces, key=lambda f: f[2]*f[3])
face_gray = gray[y:y+h, x:x+w]
preprocessed = self._preprocess_face(face_gray)
hog_feat = self._extract_hog_feature(preprocessed)
lbph_feat = self._extract_lbph_feature(preprocessed)
fused_feat = self._fuse_features(hog_feat, lbph_feat)
all_features.append(fused_feat)
all_labels.append(label)
if all_features:
return np.vstack(all_features), all_labels
else:
return np.array([], dtype=np.float32), []
defdetect_and_recognize(self, img):
result_img = img.copy()
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 多尺度人脸检测
faces = self.face_cascade.detectMultiScale(
gray, scaleFactor=1.05, minNeighbors=7, minSize=(40, 40),
flags=cv2.CASCADE_SCALE_IMAGE
)
results = []
for idx, (x, y, w, h) in enumerate(faces):
face_gray = gray[y:y+h, x:x+w]
# 测试模式:返回随机表情(避免崩溃)
if self.test_mode:
emotion_idx = np.random.randint(0, 7)
confidence = round(np.random.uniform(70, 98), 2)
# 正常模式:SVM预测
else:
preprocessed = self._preprocess_face(face_gray)
hog_feat = self._extract_hog_feature(preprocessed)
lbph_feat = self._extract_lbph_feature(preprocessed)
fused_feat = self._fuse_features(hog_feat, lbph_feat)
_, pred = self.svm.predict(fused_feat)
emotion_idx = int(pred[0][0])
# 计算置信度
_, resp = self.svm.predict(fused_feat, flags=cv2.ml.SVM_GET_DECISION_FUNCTIONS)
confidence = 100 - (abs(resp[0][0]) / max(abs(resp[0])) * 100)
confidence = round(max(confidence, 0), 2)
emotion_name = EMOTION_LABELS[emotion_idx]
# 绘制标注
cv2.rectangle(result_img, (x, y), (x+w, y+h), (0, 255, 0), 2)
label_bg = (x, y-30) if y-30 > 0else (x, y+h+30)
cv2.rectangle(result_img, label_bg, (x+200, label_bg[1]+25), (0, 0, 0), -1)
label_text = "{} ({}%)".format(emotion_name.split(" ")[0], confidence)
cv2.putText(
result_img, label_text, (label_bg[0]+5, label_bg[1]+18),
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 1
)
results.append("人脸{}: {}(置信度:{}%)".format(idx+1, emotion_name, confidence))
if len(faces) == 0:
results.append("未检测到人脸!")
elif self.test_mode:
results.insert(0, "【测试模式】表情结果为随机模拟,需准备样本库训练后获取真实结果")
return result_img, results
# -------------------------- tkinter交互层(修复AttributeError) --------------------------
classEmotionApp:
def__init__(self, root):
self.root = root
self.root.title("纯OpenCV人脸表情识别工具(tkinter版)")
self.root.geometry("1100x700")
# 初始化识别器(优先尝试正常模式,失败则自动切测试模式)
self.recognizer = None# 先初始化属性,避免AttributeError
try:
# 先尝试正常模式
self.recognizer = OpenCVEmotionRecognizer(test_mode=False)
except Exception as e:
messagebox.showinfo("初始化提示", f"正常模式启动失败:{str(e)}\n启用测试模式(可正常检测人脸,表情为模拟结果)")
# 强制启用测试模式
self.recognizer = OpenCVEmotionRecognizer(test_mode=True)
# 初始化变量
self.original_img = None
self.result_img = None
# 创建UI
self._create_ui()
def_create_ui(self):
self.upload_btn = Button(
self.root, text="上传图片", command=self._upload_image,
font=("Arial", 12), width=18, height=2
)
self.upload_btn.pack(pady=10)
img_frame = Frame(self.root)
img_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=5)
original_label = Label(img_frame, text="原始图片", font=("Arial", 11))
original_label.grid(row=0, column=0, padx=10)
self.original_img_label = Label(img_frame, bg="#f0f0f0", width=50, height=25)
self.original_img_label.grid(row=1, column=0, padx=10)
result_label = Label(img_frame, text="表情识别结果", font=("Arial", 11))
result_label.grid(row=0, column=1, padx=10)
self.result_img_label = Label(img_frame, bg="#f0f0f0", width=50, height=25)
self.result_img_label.grid(row=1, column=1, padx=10)
result_text_frame = Frame(self.root)
result_text_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=10)
scrollbar = Scrollbar(result_text_frame)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self.result_text = Text(
result_text_frame, font=("Arial", 12), height=6,
yscrollcommand=scrollbar.set
)
self.result_text.pack(fill=tk.BOTH, expand=True)
scrollbar.config(command=self.result_text.yview)
def_cv2_to_tkinter(self, img):
max_size = (400, 400)
h, w = img.shape[:2]
scale = min(max_size[0]/w, max_size[1]/h)
new_w, new_h = int(w*scale), int(h*scale)
resized = cv2.resize(img, (new_w, new_h))
rgb_img = cv2.cvtColor(resized, cv2.COLOR_BGR2RGB)
img_bytes = cv2.imencode('.png', rgb_img)[1].tobytes()
tk_img = tk.PhotoImage(data=img_bytes)
return tk_img
def_upload_image(self):
file_path = filedialog.askopenfilename(
filetypes=[("图片文件", "*.jpg *.jpeg *.png *.bmp")]
)
ifnot file_path:
return
self.original_img = cv2.imread(file_path)
if self.original_img isNone:
messagebox.showerror("错误", "图片读取失败!")
return
# 确保recognizer属性存在(核心修复点)
if self.recognizer isNone:
messagebox.showerror("错误", "表情识别器未初始化!")
return
self.result_img, results = self.recognizer.detect_and_recognize(self.original_img)
original_tk_img = self._cv2_to_tkinter(self.original_img)
self.original_img_label.config(image=original_tk_img)
self.original_img_label.image = original_tk_img
result_tk_img = self._cv2_to_tkinter(self.result_img)
self.result_img_label.config(image=result_tk_img)
self.result_img_label.image = result_tk_img
self.result_text.delete(1.0, tk.END)
for res in results:
self.result_text.insert(tk.END, res + "\n")
# -------------------------- 程序入口 --------------------------
if __name__ == "__main__":
os.environ["OPENCV_LOG_LEVEL"] = "ERROR"
root = tk.Tk()
app = EmotionApp(root)
root.mainloop()
避免AttributeError的关键:
EmotionApp.__init__ 中,先初始化 self.recognizer = None,再尝试创建识别器,确保属性始终存在;_upload_image 中增加 if self.recognizer is None 的判断,提前拦截错误;test_mode 测试模式,即使样本库未准备好,也能创建 recognizer 实例,避免属性缺失。异常处理增强:
_load_or_train_model 调用逻辑,捕获样本库为空/路径错误等异常,自动切换到测试模式;模型训练优化:
features.size == 0 替代 features.empty(),兼容numpy版本)。emotion_dataset 文件夹,并提示你放入分类图片;emotion_dataset 下按0-6分类放入足够的表情图片(每类建议20+张),重新运行即可自动训练模型。你需要解决图片检测后显示不完整的问题,我为你提供修复后的完整可运行代码,核心优化了图片缩放逻辑,确保图片始终完整适配Label显示区域,且保持宽高比不拉伸变形。
import tkinter as tk
from tkinter import filedialog, Label, Button, Text, Scrollbar, messagebox, Frame
import cv2
import numpy as np
import os
import glob
# -------------------------- 核心配置(纯OpenCV) --------------------------
EMOTION_LABELS = {
0: "中性 (Neutral)",
1: "开心 (Happy)",
2: "悲伤 (Sad)",
3: "愤怒 (Angry)",
4: "惊讶 (Surprise)",
5: "恐惧 (Fear)",
6: "厌恶 (Disgust)"
}
FACE_CASCADE_PATH = cv2.data.haarcascades + "haarcascade_frontalface_default.xml"
EMOTION_DATASET_PATH = "emotion_dataset"
SVM_MODEL_PATH = "emotion_svm_model.xml"
FACE_SIZE = (128, 128)
HOG_PARAMS = {
"win_size": (64, 64),
"block_size": (16, 16),
"block_stride": (8, 8),
"cell_size": (8, 8),
"nbins": 9
}
# 图片显示区域固定尺寸(像素)
DISPLAY_WIDTH = 400
DISPLAY_HEIGHT = 300
# -------------------------- 纯OpenCV工具类 --------------------------
classOpenCVEmotionRecognizer:
def__init__(self, test_mode=False):
self.test_mode = test_mode
self.svm_loaded = False
# 加载人脸检测器
self.face_cascade = cv2.CascadeClassifier(FACE_CASCADE_PATH)
if self.face_cascade.empty():
raise Exception("人脸检测器加载失败!请安装opencv-contrib-python:pip install opencv-contrib-python")
# 非测试模式下加载/训练SVM
ifnot test_mode:
self.svm = cv2.ml.SVM_create()
self.svm.setType(cv2.ml.SVM_C_SVC)
self.svm.setKernel(cv2.ml.SVM_RBF)
self.svm.setC(10.0)
self.svm.setGamma(0.01)
try:
self._load_or_train_model()
self.svm_loaded = True
except Exception as e:
messagebox.showwarning("模型加载失败", f"无法加载/训练SVM模型:{str(e)}\n将启用测试模式(表情结果为随机模拟)")
self.test_mode = True
else:
self.svm_loaded = False
def_preprocess_face(self, face_gray):
equalized = cv2.equalizeHist(face_gray)
denoised = cv2.bilateralFilter(equalized, 9, 75, 75)
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))
morph = cv2.morphologyEx(denoised, cv2.MORPH_OPEN, kernel)
resized = cv2.resize(morph, FACE_SIZE)
normalized = resized.astype(np.float32) / 255.0
return normalized
def_extract_hog_feature(self, face_preprocessed):
hog_face = cv2.resize(face_preprocessed, HOG_PARAMS["win_size"])
hog = cv2.HOGDescriptor(
HOG_PARAMS["win_size"],
HOG_PARAMS["block_size"],
HOG_PARAMS["block_stride"],
HOG_PARAMS["cell_size"],
HOG_PARAMS["nbins"]
)
hog_features = hog.compute(hog_face).reshape(1, -1)
return hog_features.astype(np.float32)
def_extract_lbph_feature(self, face_preprocessed):
lbph = cv2.face.LBPHFaceRecognizer_create(
radius=1, neighbors=8, grid_x=8, grid_y=8
)
fake_labels = np.array([0], dtype=np.int32)
lbph.train([(face_preprocessed * 255).astype(np.uint8)], fake_labels)
lbph_features = lbph.getHistograms()[0].reshape(1, -1).astype(np.float32)
return lbph_features
def_fuse_features(self, hog_feat, lbph_feat):
hog_norm = cv2.normalize(hog_feat, None, norm_type=cv2.NORM_L2)
lbph_norm = cv2.normalize(lbph_feat, None, norm_type=cv2.NORM_L2)
fused = np.hstack((hog_norm, lbph_norm))
return fused
def_load_or_train_model(self):
if os.path.exists(SVM_MODEL_PATH):
self.svm = cv2.ml.SVM_load(SVM_MODEL_PATH)
print("SVM表情模型加载成功!")
return
print("未检测到预训练模型,开始从样本库训练...")
features, labels = self._load_dataset()
if features.size == 0or len(labels) == 0:
raise Exception(f"样本库加载失败!请检查{EMOTION_DATASET_PATH}路径下是否有有效表情图片(每类至少10张)")
# 打乱数据提升泛化能力
indices = np.arange(features.shape[0])
np.random.shuffle(indices)
features = features[indices]
labels = np.array(labels)[indices]
self.svm.train(features, cv2.ml.ROW_SAMPLE, labels.astype(np.int32))
self.svm.save(SVM_MODEL_PATH)
print(f"SVM模型训练完成并保存至:{SVM_MODEL_PATH}")
def_load_dataset(self):
all_features = []
all_labels = []
# 自动创建样本库目录
ifnot os.path.exists(EMOTION_DATASET_PATH):
os.makedirs(EMOTION_DATASET_PATH)
for i in range(7):
os.makedirs(os.path.join(EMOTION_DATASET_PATH, str(i)), exist_ok=True)
raise Exception(f"样本库目录已创建:{EMOTION_DATASET_PATH}\n请按以下规则放入图片:\n0=中性 1=开心 2=悲伤 3=愤怒 4=惊讶 5=恐惧 6=厌恶")
# 遍历每个表情类别
for label in range(7):
label_dir = os.path.join(EMOTION_DATASET_PATH, str(label))
img_paths = glob.glob(os.path.join(label_dir, "*.jpg")) + glob.glob(os.path.join(label_dir, "*.png"))
for img_path in img_paths:
img = cv2.imread(img_path)
if img isNone:
continue
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
faces = self.face_cascade.detectMultiScale(
gray, scaleFactor=1.1, minNeighbors=7, minSize=(40, 40)
)
if len(faces) == 0:
continue
x, y, w, h = max(faces, key=lambda f: f[2]*f[3])
face_gray = gray[y:y+h, x:x+w]
preprocessed = self._preprocess_face(face_gray)
hog_feat = self._extract_hog_feature(preprocessed)
lbph_feat = self._extract_lbph_feature(preprocessed)
fused_feat = self._fuse_features(hog_feat, lbph_feat)
all_features.append(fused_feat)
all_labels.append(label)
if all_features:
return np.vstack(all_features), all_labels
else:
return np.array([], dtype=np.float32), []
defdetect_and_recognize(self, img):
result_img = img.copy()
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 多尺度人脸检测
faces = self.face_cascade.detectMultiScale(
gray, scaleFactor=1.05, minNeighbors=7, minSize=(40, 40),
flags=cv2.CASCADE_SCALE_IMAGE
)
results = []
for idx, (x, y, w, h) in enumerate(faces):
face_gray = gray[y:y+h, x:x+w]
# 测试模式:随机表情
if self.test_mode:
emotion_idx = np.random.randint(0, 7)
confidence = round(np.random.uniform(70, 98), 2)
# 正常模式:SVM预测
else:
preprocessed = self._preprocess_face(face_gray)
hog_feat = self._extract_hog_feature(preprocessed)
lbph_feat = self._extract_lbph_feature(preprocessed)
fused_feat = self._fuse_features(hog_feat, lbph_feat)
_, pred = self.svm.predict(fused_feat)
emotion_idx = int(pred[0][0])
# 计算置信度
_, resp = self.svm.predict(fused_feat, flags=cv2.ml.SVM_GET_DECISION_FUNCTIONS)
confidence = 100 - (abs(resp[0][0]) / max(abs(resp[0])) * 100)
confidence = round(max(confidence, 0), 2)
emotion_name = EMOTION_LABELS[emotion_idx]
# 绘制标注
cv2.rectangle(result_img, (x, y), (x+w, y+h), (0, 255, 0), 2)
label_bg = (x, y-30) if y-30 > 0else (x, y+h+30)
cv2.rectangle(result_img, label_bg, (x+200, label_bg[1]+25), (0, 0, 0), -1)
label_text = "{} ({}%)".format(emotion_name.split(" ")[0], confidence)
cv2.putText(
result_img, label_text, (label_bg[0]+5, label_bg[1]+18),
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 1
)
results.append("人脸{}: {}(置信度:{}%)".format(idx+1, emotion_name, confidence))
if len(faces) == 0:
results.append("未检测到人脸!")
elif self.test_mode:
results.insert(0, "【测试模式】表情结果为随机模拟,需准备样本库训练后获取真实结果")
return result_img, results
# -------------------------- tkinter交互层(修复图片显示) --------------------------
classEmotionApp:
def__init__(self, root):
self.root = root
self.root.title("纯OpenCV人脸表情识别工具(完整显示版)")
self.root.geometry("1100x800") # 调整窗口高度,避免内容挤压
# 初始化识别器
self.recognizer = None
try:
self.recognizer = OpenCVEmotionRecognizer(test_mode=False)
except Exception as e:
messagebox.showinfo("初始化提示", f"正常模式启动失败:{str(e)}\n启用测试模式(可正常检测人脸,表情为模拟结果)")
self.recognizer = OpenCVEmotionRecognizer(test_mode=True)
# 初始化变量
self.original_img = None
self.result_img = None
# 创建UI
self._create_ui()
def_create_ui(self):
# 上传按钮
self.upload_btn = Button(
self.root, text="上传图片", command=self._upload_image,
font=("Arial", 12), width=18, height=2
)
self.upload_btn.pack(pady=10)
# 图片展示框架(固定尺寸,避免拉伸)
img_frame = Frame(self.root, width=DISPLAY_WIDTH*2 + 40, height=DISPLAY_HEIGHT + 40)
img_frame.pack(fill=tk.X, padx=20, pady=5)
img_frame.pack_propagate(False) # 禁止框架随内容缩放
# 原始图片区域
original_label = Label(img_frame, text="原始图片", font=("Arial", 11))
original_label.grid(row=0, column=0, padx=10, pady=5)
# 固定Label尺寸,背景灰色占位
self.original_img_label = Label(
img_frame, bg="#f0f0f0",
width=DISPLAY_WIDTH, height=DISPLAY_HEIGHT
)
self.original_img_label.grid(row=1, column=0, padx=10)
# 结果图片区域
result_label = Label(img_frame, text="表情识别结果", font=("Arial", 11))
result_label.grid(row=0, column=1, padx=10, pady=5)
self.result_img_label = Label(
img_frame, bg="#f0f0f0",
width=DISPLAY_WIDTH, height=DISPLAY_HEIGHT
)
self.result_img_label.grid(row=1, column=1, padx=10)
# 结果文本区域
result_text_frame = Frame(self.root)
result_text_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=10)
scrollbar = Scrollbar(result_text_frame)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self.result_text = Text(
result_text_frame, font=("Arial", 12), height=8,
yscrollcommand=scrollbar.set
)
self.result_text.pack(fill=tk.BOTH, expand=True)
scrollbar.config(command=self.result_text.yview)
def_cv2_to_tkinter(self, img):
"""
核心修复:按固定尺寸缩放图片,保持宽高比,完整显示
"""
h, w = img.shape[:2]
# 计算缩放比例(关键:保持宽高比,不拉伸)
scale_w = DISPLAY_WIDTH / w
scale_h = DISPLAY_HEIGHT / h
scale = min(scale_w, scale_h) # 取最小比例,确保图片完整显示
# 新尺寸
new_w = int(w * scale)
new_h = int(h * scale)
# 缩放图片(使用INTER_AREA优化缩小效果)
resized = cv2.resize(img, (new_w, new_h), interpolation=cv2.INTER_AREA)
# 创建空白画布,将缩放后的图片居中显示(避免背景空白)
canvas = np.ones((DISPLAY_HEIGHT, DISPLAY_WIDTH, 3), dtype=np.uint8) * 240# 浅灰色背景
# 计算居中位置
x_offset = (DISPLAY_WIDTH - new_w) // 2
y_offset = (DISPLAY_HEIGHT - new_h) // 2
canvas[y_offset:y_offset+new_h, x_offset:x_offset+new_w] = resized
# BGR转RGB,适配tkinter
rgb_img = cv2.cvtColor(canvas, cv2.COLOR_BGR2RGB)
# 转换为tkinter可用格式
img_bytes = cv2.imencode('.png', rgb_img)[1].tobytes()
tk_img = tk.PhotoImage(data=img_bytes)
return tk_img
def_upload_image(self):
file_path = filedialog.askopenfilename(
filetypes=[("图片文件", "*.jpg *.jpeg *.png *.bmp")]
)
ifnot file_path:
return
# 读取图片
self.original_img = cv2.imread(file_path)
if self.original_img isNone:
messagebox.showerror("错误", "图片读取失败!请检查图片格式或路径")
return
# 检测识别
if self.recognizer isNone:
messagebox.showerror("错误", "表情识别器未初始化!")
return
self.result_img, results = self.recognizer.detect_and_recognize(self.original_img)
# 转换并显示图片(核心:使用修复后的缩放函数)
original_tk_img = self._cv2_to_tkinter(self.original_img)
self.original_img_label.config(image=original_tk_img)
self.original_img_label.image = original_tk_img # 保留引用,防止被回收
result_tk_img = self._cv2_to_tkinter(self.result_img)
self.result_img_label.config(image=result_tk_img)
self.result_img_label.image = result_tk_img
# 显示识别结果
self.result_text.delete(1.0, tk.END)
for res in results:
self.result_text.insert(tk.END, res + "\n")
# -------------------------- 程序入口 --------------------------
if __name__ == "__main__":
# 关闭OpenCV日志
os.environ["OPENCV_LOG_LEVEL"] = "ERROR"
# 高DPI适配(避免界面模糊)
try:
from ctypes import windll
windll.shcore.SetProcessDpiAwareness(1)
except:
pass
root = tk.Tk()
app = EmotionApp(root)
root.mainloop()
固定显示区域尺寸:
DISPLAY_WIDTH=400、DISPLAY_HEIGHT=300 作为图片显示的固定像素尺寸;pack_propagate(False) 禁止随内容缩放,Label 固定尺寸避免挤压。智能图片缩放逻辑:
cv2.INTER_AREA 插值优化图片缩小效果,避免模糊。界面布局优化:
pip install opencv-python opencv-contrib-python numpy
emotion_dataset 文件夹中,按0-6分类放入对应表情图片(每类≥10张),重新运行即可训练真实模型。