
简介这是一套基于摄像头输入的智能测谎软件源代码面向对计算机视觉与情感计算感兴趣的学习者和开发者借助 MediaPipe 实现面部与手部关键点检测并结合情感识别技术完成心率监测等分析功能可用于课堂演示、项目实训或二次开发。资源包共 13 个文件约 1.55MB包含 3 个 Python 源码文件、3 张 png 示意图、1 个 yml 环境配置、1 个 ini 配置文件以及 README 说明、LICENSE 授权、ico 图标等覆盖从环境搭建到运行入口的完整结构。已有 130 人学习下载。读者可获得可直接运行的测谎程序源码支持视频设备、文件与屏幕区域三种输入源可显示面部和手部关键点、录制带时间戳的 AVI 视频、设置线索持续帧数并启用次要视频设备同时附带日志查看脚本与 conda、pip 两种依赖安装方式便于快速复现与排错。1. 从摄像头到微表情这套智能测谎源码到底能跑出什么结果你手头可能正好有个 USB 摄像头也听说过 MediaPipe 能做面部关键点但真要把「测谎」这件事落地成一个能跑的程序中间缺的往往不是算法而是一套把视频流、关键点、生理信号和情感分类串起来的工程骨架。这套源码解决的就是这个它用普通摄像头做输入通过 MediaPipe 提取面部 468 个关键点再结合远程光电容积脉搏波rPPG从额头区域估算心率最后叠加情感识别模型输出紧张/平静的概率分布。适合两类人一类是想快速验证「非接触式生理信号提取」可行性的开发者另一类是需要一个可演示的课程设计或原型系统但不想从零写视频采集和关键点对齐的人。我拆过不少类似项目多数卡在摄像头帧率不稳和光照干扰上这套代码把预处理和滤波部分做得比较完整省掉了最耗时的调参环节。2. 拆开看架构MediaPipe 关键点、rPPG 心率与情感分类怎么串2.1 为什么选 MediaPipe 而不是 dlib 或 OpenCV 自带检测器面部关键点检测是整条流水线的地基。常见做法有三种dlib 的 68 点、OpenCV 的 Haar 级联、以及 MediaPipe 的 Face Mesh。dlib 精度够但推理速度在 CPU 上跑 30fps 视频会掉帧Haar 级联对侧脸和光照变化太敏感测谎场景里用户头部微动就会丢点。MediaPipe Face Mesh 输出 468 个三维关键点自带轻量级 BlazeFace 检测器和注意力网格在普通笔记本 CPU 上能稳定跑 25 到 30fps而且关键点索引固定方便后续取额头、眼角、嘴角这些感兴趣区域。源码里关键点提取的核心调用大概是这样import cv2 import mediapipe as mp mp_face_mesh mp.solutions.face_mesh face_mesh mp_face_mesh.FaceMesh( max_num_faces1, # 测谎场景只处理单张脸减少误检 refine_landmarksTrue, # 开启虹膜关键点共 478 点 min_detection_confidence0.5, min_tracking_confidence0.5 ) cap cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) while cap.isOpened(): ret, frame cap.read() if not ret: break rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) results face_mesh.process(rgb) if results.multi_face_landmarks: landmarks results.multi_face_landmarks[0] # 后续取 ROI 做 rPPG 和情感特征refine_landmarksTrue会额外输出虹膜周围 10 个点对眨眼检测和视线估计有帮助但会略微增加推理耗时。如果目标机器是低功耗设备可以关掉这个选项帧率能回升 3 到 5 帧。min_detection_confidence设 0.5 是平衡值设太高在暗光下会频繁丢失人脸设太低会把背景误判成人脸。2.2 rPPG 心率监测从额头像素到 BPM 的完整链路远程心率监测的原理不复杂皮肤颜色会随血液灌注发生微小周期性变化这种变化在绿色通道最明显。源码的做法是取额头区域MediaPipe 索引 10、338、297、332 等点围成的多边形计算每帧该区域绿色通道均值得到一条时间序列再做带通滤波和快速傅里叶变换找主频。具体步骤拆开第一步定义额头 ROI。MediaPipe 关键点里额头区域大致由索引 10眉心上方、338左额、297右额以及 109、67 等点围成。源码里用cv2.fillPoly生成掩膜只保留掩膜内的像素。第二步滑动窗口缓存。心率估算需要至少 5 到 10 秒的数据源码用collections.deque存最近 150 帧30fps 下约 5 秒的绿色通道均值。第三步信号处理。原始信号包含呼吸和运动伪影需要先做 0.7Hz 到 4Hz 的带通滤波对应 42 到 240 BPM再对滤波后信号做 FFT取功率谱峰值对应的频率乘以 60 得到 BPM。import numpy as np from scipy.signal import butter, filtfilt def bandpass_filter(signal, fs30, low0.7, high4.0, order4): nyq fs / 2.0 b, a butter(order, [low/nyq, high/nyq], btypeband) # 信号太短时 filtfilt 会报错需要判断长度 if len(signal) 3 * max(len(a), len(b)): return signal return filtfilt(b, a, signal) def estimate_bpm(green_series, fs30): signal np.array(green_series) signal signal - np.mean(signal) # 去直流分量 filtered bandpass_filter(signal, fs) fft np.abs(np.fft.rfft(filtered)) freqs np.fft.rfftfreq(len(filtered), d1.0/fs) # 只取 0.7 到 4Hz 范围内的峰值 valid (freqs 0.7) (freqs 4.0) if not np.any(valid): return 0 peak_freq freqs[valid][np.argmax(fft[valid])] return int(peak_freq * 60)fs参数必须和实际摄像头帧率一致源码里用cap.get(cv2.CAP_PROP_FPS)动态获取但有些摄像头返回 0 或 30 不准我一般会手动用时间戳计算真实帧率。order4是滤波阶数阶数越高过渡带越陡但相位失真也越大4 阶是常用折中。filtfilt做零相位滤波避免心率峰值偏移。2.3 情感识别模块特征拼接与分类器选择情感识别部分源码没有用端到端深度学习而是走传统特征工程路线从关键点里计算眉毛到眼睛的距离、嘴角上扬角度、眨眼频率再拼接 rPPG 得到的心率变异性HRV特征送进一个预训练的 SVM 或轻量级 MLP。这样做的好处是模型小、可解释、在 CPU 上实时性有保障。特征清单大致包括特征名来源物理含义眉毛-眼睛距离关键点 105 与 159 的欧氏距离惊讶/紧张时眉毛上抬嘴角曲率关键点 61、291、13、14 拟合曲线微笑或抿嘴眨眼频率眼睛纵横比 EAR 阈值穿越次数紧张时眨眼频率升高心率均值rPPG 输出应激状态下心率上升HRV 低频功率心率序列功率谱交感神经活跃度分类器加载和推理的代码结构import joblib import numpy as np # 假设源码里附带 emotion_svm.pkl clf joblib.load(emotion_svm.pkl) scaler joblib.load(feature_scaler.pkl) def predict_emotion(feature_vector): # feature_vector 顺序必须和训练时一致 x np.array(feature_vector).reshape(1, -1) x scaler.transform(x) proba clf.predict_proba(x)[0] # 源码里标签顺序通常是 [平静, 紧张] return {calm: proba[0], nervous: proba[1]}feature_vector的顺序是坑最多的地方训练和推理必须严格对齐否则概率会完全乱掉。源码里用了一个FEATURE_ORDER列表做校验建议不要改动。3. 跑起来环境配置、参数调优与实时可视化3.1 依赖安装与摄像头兼容性检查源码依赖不算多但版本敏感。MediaPipe 在 0.10 之后 API 有变动源码如果写的是mp.solutions.face_mesh那 0.9 到 0.10 都能跑。OpenCV 建议用 4.5 以上低版本对CAP_PROP_FPS支持不好。pip install mediapipe0.10.9 pip install opencv-python4.8.1.78 pip install scipy1.11.4 pip install scikit-learn1.3.2 pip install joblib1.3.2装完后先跑一个摄像头自检脚本确认帧率和分辨率import cv2 cap cv2.VideoCapture(0) print(FPS:, cap.get(cv2.CAP_PROP_FPS)) print(W:, cap.get(cv2.CAP_PROP_FRAME_WIDTH)) print(H:, cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) cap.release()如果 FPS 显示 0说明驱动没上报需要在代码里用time.time()手动算帧间隔。分辨率建议设 640x480太高会拖慢 MediaPipe 推理太低额头 ROI 像素不够rPPG 信噪比会崩。3.2 心率估算的窗口长度与滤波参数怎么定窗口长度直接决定心率输出的稳定性和延迟。窗口太短比如 2 秒FFT 频率分辨率不够BPM 跳动能到 ±15窗口太长比如 15 秒心率变化跟不上测谎时用户紧张导致的心率上升会被平滑掉。源码默认 150 帧30fps 下 5 秒我实测下来 5 到 7 秒是比较好的平衡点。带通滤波的低频截止 0.7Hz 对应 42 BPM覆盖了绝大多数静息心率高频截止 4Hz 对应 240 BPM运动后也能测。如果用户心率低于 45可以把低频降到 0.6Hz。滤波阶数不要超过 6否则filtfilt在窗口边缘的瞬态响应会污染结果。还有一个隐藏参数是 ROI 的膨胀系数。源码取额头多边形后做了cv2.dilate膨胀核大小默认 5x5。膨胀太大会把头发和背景框进来绿色通道均值被污染膨胀太小则像素太少信号噪声大。如果用户刘海比较厚建议把 ROI 往下移取眉心到发际线中间的区域。3.3 实时可视化把关键点、波形和概率画在同一帧调试阶段最有用的是把中间结果画出来。源码里有一个draw_debug函数把面部关键点、额头 ROI 掩膜、绿色通道波形和情感概率条叠在视频帧上。这样一眼就能看出是丢点了、ROI 偏了还是波形饱和了。def draw_debug(frame, landmarks, roi_mask, green_series, proba): # 画关键点 for lm in landmarks.landmark: h, w frame.shape[:2] cx, cy int(lm.x * w), int(lm.y * h) cv2.circle(frame, (cx, cy), 1, (0, 255, 0), -1) # 叠 ROI 掩膜 frame cv2.addWeighted(frame, 1.0, roi_mask, 0.3, 0) # 画绿色通道波形 if len(green_series) 10: pts np.array([[i*2, 100 - (v - min(green_series)) * 50] for i, v in enumerate(green_series)], np.int32) cv2.polylines(frame, [pts], False, (0, 255, 255), 1) # 画概率条 cv2.rectangle(frame, (10, 10), (10 int(proba[nervous]*200), 30), (0, 0, 255), -1) return frame波形纵轴做了归一化不然绿色通道均值的小数点后变化根本看不见。概率条长度映射到 200 像素方便肉眼判断趋势。这个调试视图在最终演示时可以关掉但开发阶段强烈建议保留。4. 避坑与排查摄像头测谎最容易翻车的五个地方4.1 现象心率输出一直是 0 或 72 不变原因通常是 ROI 掩膜全黑或者绿色通道序列长度不够。MediaPipe 在侧脸或遮挡时会返回空multi_face_landmarks源码里如果没做空判断后续 ROI 计算会拿到空数组均值变成 NaN滤波后 FFT 找不到峰值就返回 0。另一个可能是摄像头自动曝光把绿色通道压死了信号方差极小。解决在 ROI 计算前加if landmarks is None: continue打印绿色通道序列的np.std()如果小于 0.5 说明信号太弱需要关掉摄像头自动曝光cap.set(cv2.CAP_PROP_AUTO_EXPOSURE, 0.25)或增加 ROI 面积。4.2 现象情感概率在 0.5 附近来回跳没有区分度特征没做归一化或者训练集和推理集的特征分布不一致。源码里虽然带了feature_scaler.pkl但如果用户摄像头帧率不是 30fps眨眼频率和 HRV 特征会偏移导致 scaler 变换后落在训练集没覆盖的区域。解决确认实际帧率如果不是 30fps要么在代码里做时间重采样要么重新用当前帧率采集一段数据微调 scaler 的均值方差。另外检查FEATURE_ORDER是否和训练时一致顺序错一位概率就完全反了。4.3 现象MediaPipe 报错 “No module named ‘mediapipe.python._framework_bindings’”这是版本冲突的典型症状通常是因为之前装过旧版 mediapipe 没卸干净或者 protobuf 版本不兼容。MediaPipe 0.10 需要 protobuf 3.20 以上但有些环境里 tensorflow 会把 protobuf 锁在 3.19。解决先pip uninstall mediapipe protobuf -y再重新按顺序装protobuf3.20和mediapipe0.10.9。如果还不行建一个干净的虚拟环境别在 base 环境里折腾。4.4 现象视频画面延迟越来越大最后卡死cap.read()是阻塞调用如果主循环里做了 FFT 或 SVM 推理处理时间超过帧间隔缓冲区会堆积。OpenCV 默认缓冲 4 到 5 帧延迟会累积。解决把cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)加上只保留最新帧。另外把心率估算和情感推理放到独立线程主线程只做采集和绘制。源码里如果没做多线程可以自己加一个queue.Queue(maxsize1)做帧传递。4.5 现象换一台电脑后关键点索引对不上额头 ROI 跑到下巴MediaPipe 的 468 点索引是固定的但refine_landmarksTrue时会变成 478 点虹膜点插在末尾。如果代码里硬编码了 468 点时的索引开启 refine 后某些索引会偏移。源码里额头 ROI 用的索引 10、338、297 在两种模式下位置不变但如果你自己加了其他点就要注意。解决统一用refine_landmarksFalse跑一遍确认索引或者用mp_face_mesh.FACEMESH_FACE_OVAL等官方常量而不是硬编码数字。换电脑后先跑一次关键点可视化确认 ROI 位置正确再继续。5. 进阶技巧用时间窗口投票稳住心率与情感输出实时系统最怕输出抖动。心率每隔 5 秒跳一次情感概率每帧都在变演示时看起来很不靠谱。我一般会加一层时间窗口投票维护最近 N 次心率估算值取中位数而不是最新值情感概率做指数移动平均平滑系数 0.3 左右。from collections import deque import numpy as np class StableEstimator: def __init__(self, window5, alpha0.3): self.bpm_window deque(maxlenwindow) self.alpha alpha self.smooth_proba {calm: 0.5, nervous: 0.5} def update_bpm(self, bpm): if bpm 0: self.bpm_window.append(bpm) if len(self.bpm_window) 0: return 0 return int(np.median(self.bpm_window)) def update_emotion(self, proba): for k in self.smooth_proba: self.smooth_proba[k] (self.alpha * proba[k] (1 - self.alpha) * self.smooth_proba[k]) return self.smooth_probawindow5对应 25 秒的心率中位数适合长时间监测如果要做实时反应可以降到 3。alpha0.3表示新值权重 30%旧值 70%数值越小越稳但越滞后。测谎演示里我一般用 0.2 到 0.3既能看出紧张时心率上升的趋势又不会因为一次误检就跳变。还有一个验证技巧让同一个人先静坐 2 分钟采集基线再让他做心算或快速问答观察心率中位数和紧张概率是否同步上升。如果心率没动但紧张概率飙升说明情感模型过拟合了面部特征需要重新平衡特征权重。我每次换摄像头或换环境光都会先跑一遍这个基线流程确认 rPPG 信噪比够用再开始正式采集。希望帮到你。本文还有配套的精品资源点击获取