眨眼频率、眼动分析、心率、视频录制
This commit is contained in:
139
reproject/HeartRateMonitor.py
Normal file
139
reproject/HeartRateMonitor.py
Normal file
@@ -0,0 +1,139 @@
|
||||
import numpy as np
|
||||
import collections
|
||||
from scipy import signal
|
||||
|
||||
class HeartRateMonitor:
|
||||
def __init__(self, fps=30, window_size=300):
|
||||
self.fps = fps
|
||||
self.buffer_size = window_size
|
||||
|
||||
# 存储 RGB 三个通道的均值
|
||||
self.times = np.zeros(window_size)
|
||||
self.r_buffer = collections.deque(maxlen=window_size)
|
||||
self.g_buffer = collections.deque(maxlen=window_size)
|
||||
self.b_buffer = collections.deque(maxlen=window_size)
|
||||
|
||||
# 滤波器状态
|
||||
self.bp_b, self.bp_a = self._create_bandpass_filter(0.75, 2.5, fps) # 45-150 BPM
|
||||
|
||||
# 平滑结果用的
|
||||
self.bpm_history = collections.deque(maxlen=10)
|
||||
|
||||
def _create_bandpass_filter(self, lowcut, highcut, fs, order=5):
|
||||
"""创建巴特沃斯带通滤波器"""
|
||||
nyq = 0.5 * fs
|
||||
low = lowcut / nyq
|
||||
high = highcut / nyq
|
||||
b, a = signal.butter(order, [low, high], btype='band')
|
||||
return b, a
|
||||
|
||||
def _pos_algorithm(self, r, g, b):
|
||||
"""
|
||||
POS (Plane-Orthogonal-to-Skin) 算法
|
||||
比单纯的绿色通道法强在一个地方:抗运动干扰
|
||||
"""
|
||||
# 1. 归一化 (除以均值)
|
||||
# 加上 1e-6 防止除零
|
||||
r_n = r / (np.mean(r) + 1e-6)
|
||||
g_n = g / (np.mean(g) + 1e-6)
|
||||
b_n = b / (np.mean(b) + 1e-6)
|
||||
|
||||
# 2. 投影到色度平面 (Matplotlib 里的经典公式)
|
||||
# S1 = G - B
|
||||
# S2 = G + B - 2R
|
||||
s1 = g_n - b_n
|
||||
s2 = g_n + b_n - 2 * r_n
|
||||
|
||||
# 3. Alpha 微调 (Alpha Tuning)
|
||||
# 这一步是为了消除镜面反射带来的运动噪声
|
||||
alpha = np.std(s1) / (np.std(s2) + 1e-6)
|
||||
|
||||
# 4. 融合信号
|
||||
h = s1 + alpha * s2
|
||||
return h
|
||||
|
||||
def process_frame(self, frame, face_loc):
|
||||
"""
|
||||
输入: 原始无损 frame (BGR), 人脸框 (top, right, bottom, left)
|
||||
输出: BPM 数值 或 None (数据不够时)
|
||||
"""
|
||||
top, right, bottom, left = face_loc
|
||||
|
||||
# --- 1. ROI 提取与保护 ---
|
||||
h_img, w_img = frame.shape[:2]
|
||||
|
||||
# 缩小 ROI 范围:只取脸中心 50% 区域 (避开背景和边缘)
|
||||
h_box = bottom - top
|
||||
w_box = right - left
|
||||
|
||||
# 修正 ROI 坐标
|
||||
roi_top = int(max(0, top + h_box * 0.3))
|
||||
roi_bottom = int(min(h_img, bottom - h_box * 0.3))
|
||||
roi_left = int(max(0, left + w_box * 0.3))
|
||||
roi_right = int(min(w_img, right - w_box * 0.3))
|
||||
|
||||
roi = frame[roi_top:roi_bottom, roi_left:roi_right]
|
||||
|
||||
if roi.size == 0:
|
||||
return None
|
||||
|
||||
# --- 2. 提取 RGB 均值 ---
|
||||
# OpenCV 是 BGR
|
||||
b_mean = np.mean(roi[:, :, 0])
|
||||
g_mean = np.mean(roi[:, :, 1])
|
||||
r_mean = np.mean(roi[:, :, 2])
|
||||
|
||||
self.r_buffer.append(r_mean)
|
||||
self.g_buffer.append(g_mean)
|
||||
self.b_buffer.append(b_mean)
|
||||
|
||||
# 数据不够,返回 None
|
||||
if len(self.r_buffer) < self.buffer_size:
|
||||
progress = int(len(self.r_buffer) / self.buffer_size * 100)
|
||||
return None # 或者返回 progress 表示进度
|
||||
|
||||
# --- 3. 信号处理 (核心升级部分) ---
|
||||
r = np.array(self.r_buffer)
|
||||
g = np.array(self.g_buffer)
|
||||
b = np.array(self.b_buffer)
|
||||
|
||||
# A. 使用 POS 算法融合三通道 (抗干扰)
|
||||
pulse_signal = self._pos_algorithm(r, g, b)
|
||||
|
||||
# B. 消除直流分量 (Detrending)
|
||||
# 这一步去掉了光线缓慢变化的干扰
|
||||
pulse_signal = signal.detrend(pulse_signal)
|
||||
|
||||
# C. 带通滤波 (Bandpass Filter)
|
||||
# 只保留 0.75Hz - 2.5Hz 的信号
|
||||
pulse_signal = signal.filtfilt(self.bp_b, self.bp_a, pulse_signal)
|
||||
|
||||
# --- 4. 频域分析 (FFT) ---
|
||||
# 加汉宁窗 (减少频谱泄露)
|
||||
window = np.hanning(len(pulse_signal))
|
||||
pulse_signal_windowed = pulse_signal * window
|
||||
|
||||
# FFT
|
||||
fft_res = np.fft.rfft(pulse_signal_windowed)
|
||||
freqs = np.fft.rfftfreq(len(pulse_signal), 1.0/self.fps)
|
||||
mag = np.abs(fft_res)
|
||||
|
||||
# D. 寻找峰值
|
||||
# 限制频率范围 (45 BPM - 180 BPM)
|
||||
interest_idx = np.where((freqs >= 0.75) & (freqs <= 3.0))
|
||||
valid_freqs = freqs[interest_idx]
|
||||
valid_mags = mag[interest_idx]
|
||||
|
||||
if len(valid_mags) == 0:
|
||||
return None
|
||||
|
||||
max_idx = np.argmax(valid_mags)
|
||||
peak_freq = valid_freqs[max_idx]
|
||||
bpm = peak_freq * 60.0
|
||||
|
||||
# --- 5. 结果平滑 ---
|
||||
# 防止数字乱跳
|
||||
self.bpm_history.append(bpm)
|
||||
avg_bpm = np.mean(self.bpm_history)
|
||||
|
||||
return int(avg_bpm)
|
||||
Reference in New Issue
Block a user