当前位置:首页>python>Python实现上传图片自动识别人脸和表情

Python实现上传图片自动识别人脸和表情

  • 2026-10-19 23:52:52
Python实现上传图片自动识别人脸和表情

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

实现思路(tkinter + 纯OpenCV)

  1. 交互层:用 tkinter 做图片上传、结果展示、日志输出的可视化界面,仅用 tkinter 原生组件(无 PIL 依赖,直接通过 OpenCV 处理图像格式转换);
  2. 核心层(纯OpenCV):
    • 人脸检测:优化级联分类器参数,加入多尺度检测、非极大值抑制(NMS)去重;
    • 特征提取:HOG 特征(手工设计)+ LBPH 局部纹理特征,双特征融合提升识别精度;
    • 分类器:OpenCV 的 SVM 分类器(C_SVC + RBF 核),支持模型训练、保存、加载;
    • 预处理增强:直方图均衡化、双边滤波、形态学降噪、像素归一化;
    • 复杂度提升:加入样本库批量加载、特征归一化、置信度计算、多表情匹配验证。

完整代码(tkinter + 纯OpenCV,无其他依赖)

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()

代码关键特性(复杂版+纯OpenCV)

1. 无额外依赖(仅tkinter+OpenCV)

  • 舍弃 PIL/Pillow:图片格式转换完全基于 cv2.imencode + tkinter PhotoImage 原生实现;
  • 舍弃 TensorFlow/sklearn:分类完全基于 OpenCV 内置的 cv2.ml.SVM;
  • 特征提取:仅用 cv2.HOGDescriptor、cv2.face.LBPHFaceRecognizer 等 OpenCV 原生接口。

2. 复杂度提升点

  • 双特征融合:HOG(全局表情轮廓)+ LBPH(局部纹理),比单一特征识别更鲁棒;
  • 高级预处理:直方图均衡化+双边滤波+形态学降噪,针对性优化表情纹理特征;
  • 优化的人脸检测:调整 scaleFactor=1.05、minNeighbors=7,减少误检/漏检;
  • 置信度计算:基于 SVM 决策函数距离转换为置信度,而非单纯的分类结果;
  • 样本库自动加载:支持按 0-6 分类的表情样本库,自动训练并保存 SVM 模型;
  • 异常处理:完善的模型加载、图片读取、人脸检测异常处理,提升程序稳定性。

3. 运行前置条件

  1. 安装 OpenCV(确保包含 cv2.face 模块):
    pip install opencv-python opencv-contrib-python  # contrib版包含LBPH等扩展模块
  2. 准备表情样本库:
    • 在程序同级目录创建 emotion_dataset 文件夹;
    • 内部创建 0-6 共7个文件夹,分别对应 EMOTION_LABELS 的7类表情;
    • 每个文件夹放入对应表情的人脸图片(建议至少每类20张以上,提升训练精度)。

4. 运行流程

  • 首次运行:程序会检测样本库,自动训练 SVM 模型并保存为 emotion_svm_model.xml;
  • 后续运行:直接加载预训练模型,无需重复训练;
  • 上传图片:选择图片后,自动检测人脸、识别表情,标注结果并展示置信度。

总结

  1. 该程序是纯 OpenCV + tkinter 实现的复杂版人脸表情识别工具,无任何其他第三方依赖,核心基于传统计算机视觉技术(HOG+SVM+LBPH);
  2. 关键复杂度体现在:双特征融合、多步骤预处理、置信度计算、样本库自动化训练,比基础版识别更鲁棒;
  3. 优化方向:可增加非极大值抑制(NMS)去重重复人脸框、加入表情相似度排序、支持批量图片处理,进一步提升复杂度和实用性。

遇到的错误核心是 EmotionApp 实例没有 recognizer 属性,这是因为初始化 OpenCVEmotionRecognizer 时抛出了异常(样本库为空/路径问题),导致 self.recognizer 未被成功创建,后续调用就会触发 AttributeError。修复这个错误,并优化程序的异常处理逻辑,确保即使样本库未准备好,程序也能正常运行(提供测试模式)。

错误原因分析

  1. 直接原因:初始化 OpenCVEmotionRecognizer 时,样本库目录 emotion_dataset 要么不存在,要么为空,触发异常后 self.recognizer 未被赋值;
  2. 根本原因:程序缺少“无样本库时的降级方案”,且异常处理逻辑不完整,导致属性初始化失败。

修复后的完整代码(解决AttributeError+新增测试模式)

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()

核心修复点说明

  1. 避免AttributeError的关键:

    • 在 EmotionApp.__init__ 中,先初始化 self.recognizer = None,再尝试创建识别器,确保属性始终存在;
    • 在 _upload_image 中增加 if self.recognizer is None 的判断,提前拦截错误;
    • 新增 test_mode 测试模式,即使样本库未准备好,也能创建 recognizer 实例,避免属性缺失。
  2. 异常处理增强:

    • 包裹 _load_or_train_model 调用逻辑,捕获样本库为空/路径错误等异常,自动切换到测试模式;
    • 测试模式下仍能检测人脸,仅表情结果为随机模拟(避免程序崩溃,方便先验证人脸检测功能);
    • 自动创建样本库目录,并给出清晰的图片分类提示,降低使用门槛。
  3. 模型训练优化:

    • 训练前打乱样本数据,提升SVM模型泛化能力;
    • 完善特征矩阵的空值判断(用 features.size == 0 替代 features.empty(),兼容numpy版本)。

运行验证步骤

  1. 直接运行修复后的代码,即使没有样本库,程序也不会崩溃:
    • 首次运行会自动创建 emotion_dataset 文件夹,并提示你放入分类图片;
    • 此时进入测试模式,上传图片可正常检测人脸,表情结果为随机模拟(用于验证界面和人脸检测功能);
  2. 准备样本库后(按提示在0-6文件夹放入对应表情图片),重新运行程序:
    • 程序会自动训练SVM模型并保存,后续上传图片即可得到真实的表情识别结果。

总结

  1. 本次修复核心解决了「初始化异常导致recognizer属性缺失」的问题,通过提前初始化属性+测试模式降级+完善异常捕获,确保程序不会因样本库问题崩溃;
  2. 测试模式保留了人脸检测功能,方便你先验证界面和基础逻辑,再逐步准备样本库训练真实模型;
  3. 若要获取真实的表情识别结果,需在 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()

核心修复点说明

  1. 固定显示区域尺寸:

    • 定义 DISPLAY_WIDTH=400、DISPLAY_HEIGHT=300 作为图片显示的固定像素尺寸;
    • 图片框架设置 pack_propagate(False) 禁止随内容缩放,Label 固定尺寸避免挤压。
  2. 智能图片缩放逻辑:

    • 计算宽/高缩放比例,取最小值确保图片完整显示,不超出Label范围;
    • 创建浅灰色画布,将缩放后的图片居中显示,解决小图显示不全、大图截断问题;
    • 使用 cv2.INTER_AREA 插值优化图片缩小效果,避免模糊。
  3. 界面布局优化:

    • 调整窗口高度到800px,避免文本区域挤压图片;
    • 增加高DPI适配代码,解决Windows系统下界面模糊问题;
    • 文本区域高度调整为8行,显示更多识别结果。

运行说明

  1. 安装依赖(确保包含扩展模块):
    pip install opencv-python opencv-contrib-python numpy
  2. 直接运行代码,即使无样本库也会进入测试模式,上传图片后:
    • 图片会自动缩放到400x300并居中显示,完整无截断;
    • 人脸检测正常,表情结果为随机模拟(准备样本库后可训练真实模型);
  3. 样本库准备:在程序目录下的 emotion_dataset 文件夹中,按0-6分类放入对应表情图片(每类≥10张),重新运行即可训练真实模型。

总结

  1. 本次修复核心解决了图片显示不全问题,通过固定显示尺寸+等比例缩放+居中显示确保图片完整呈现;
  2. 保留纯OpenCV+tkinter无其他依赖的特性,同时优化了界面布局和高DPI适配;
  3. 测试模式下仍可正常检测人脸,方便验证基础功能,样本库准备完成后可自动切换到真实表情识别。

最新文章

随机文章