188 lines
6.0 KiB
Python
188 lines
6.0 KiB
Python
|
import dlib
|
||
|
import numpy as np
|
||
|
import scipy.fftpack as fftpack
|
||
|
from matplotlib import pyplot as plt
|
||
|
from sklearn.decomposition import FastICA
|
||
|
import cv2
|
||
|
from scipy import signal
|
||
|
|
||
|
|
||
|
class HeartRateMonitor:
|
||
|
def __init__(self, fps, freqs_min, freqs_max):
|
||
|
self.fps = fps
|
||
|
self.freqs_min = freqs_min
|
||
|
self.freqs_max = freqs_max
|
||
|
self.all_hr_values = []
|
||
|
self.bvp_signal = []
|
||
|
|
||
|
def get_channel_signal(self, ROI):
|
||
|
blue = []
|
||
|
green = []
|
||
|
red = []
|
||
|
for roi in ROI:
|
||
|
b, g, r = cv2.split(roi)
|
||
|
b = np.mean(np.sum(b)) / np.std(b)
|
||
|
g = np.mean(np.sum(g)) / np.std(g)
|
||
|
r = np.mean(np.sum(r)) / np.std(r)
|
||
|
blue.append(b)
|
||
|
green.append(g)
|
||
|
red.append(r)
|
||
|
return blue, green, red
|
||
|
|
||
|
def ICA(self, matrix, n_component, max_iter=200):
|
||
|
matrix = matrix.T
|
||
|
ica = FastICA(n_components=n_component, max_iter=max_iter)
|
||
|
u = ica.fit_transform(matrix)
|
||
|
return u.T
|
||
|
|
||
|
def fft_filter(self, signal):
|
||
|
fft = fftpack.fft(signal, axis=0)
|
||
|
frequencies = fftpack.fftfreq(signal.shape[0], d=1.0 / self.fps)
|
||
|
bound_low = (np.abs(frequencies - self.freqs_min)).argmin()
|
||
|
bound_high = (np.abs(frequencies - self.freqs_max)).argmin()
|
||
|
fft[:bound_low] = 0
|
||
|
fft[bound_high:-bound_high] = 0
|
||
|
fft[-bound_low:] = 0
|
||
|
return fft, frequencies
|
||
|
|
||
|
def find_heart_rate(self, fft, freqs):
|
||
|
fft_maximums = []
|
||
|
|
||
|
for i in range(fft.shape[0]):
|
||
|
if self.freqs_min <= freqs[i] <= self.freqs_max:
|
||
|
fftMap = abs(fft[i])
|
||
|
fft_maximums.append(fftMap.max())
|
||
|
else:
|
||
|
fft_maximums.append(0)
|
||
|
|
||
|
peaks, properties = signal.find_peaks(fft_maximums)
|
||
|
max_peak = -1
|
||
|
max_freq = 0
|
||
|
|
||
|
for peak in peaks:
|
||
|
if fft_maximums[peak] > max_freq:
|
||
|
max_freq = fft_maximums[peak]
|
||
|
max_peak = peak
|
||
|
|
||
|
return freqs[max_peak] * 60
|
||
|
|
||
|
def fourier_transform(self, signal, N, fs):
|
||
|
result = fftpack.fft(signal, N)
|
||
|
result = np.abs(result)
|
||
|
freqs = np.arange(N) / N
|
||
|
freqs = freqs * fs
|
||
|
return result[:N // 2], freqs[:N // 2]
|
||
|
|
||
|
def calculate_hrv(self, hr_values, window_size=5):
|
||
|
num_values = int(window_size * self.fps)
|
||
|
start_idx = max(0, len(hr_values) - num_values)
|
||
|
recent_hr_values = hr_values[start_idx:]
|
||
|
rr_intervals = np.array(recent_hr_values)
|
||
|
|
||
|
# 计算SDNN
|
||
|
sdnn = np.std(rr_intervals)
|
||
|
|
||
|
# 计算RMSSD
|
||
|
nn_diffs = np.diff(rr_intervals)
|
||
|
rmssd = np.sqrt(np.mean(nn_diffs ** 2))
|
||
|
|
||
|
# 计算CV R-R
|
||
|
mean_rr = np.mean(rr_intervals)
|
||
|
cv_rr = sdnn / mean_rr if mean_rr != 0 else 0
|
||
|
|
||
|
return sdnn, rmssd, cv_rr
|
||
|
|
||
|
def process_roi(self, ROI):
|
||
|
blue, green, red = self.get_channel_signal(ROI)
|
||
|
matrix = np.array([blue, green, red])
|
||
|
component = self.ICA(matrix, 3)
|
||
|
|
||
|
# 使用三个组件的平均值作为rPPG信号
|
||
|
rppg_signal = np.mean(component, axis=0)
|
||
|
# 将rPPG信号转换为BVP信号
|
||
|
self.bvp_signal = self.rppg_to_bvp(rppg_signal)
|
||
|
# self.plot_bvp_signal()
|
||
|
hr_values = []
|
||
|
for i in range(3):
|
||
|
fft, freqs = self.fft_filter(component[i])
|
||
|
heartrate = self.find_heart_rate(fft, freqs)
|
||
|
hr_values.append(heartrate)
|
||
|
avg_hr = sum(hr_values) / 3
|
||
|
self.all_hr_values.append(avg_hr)
|
||
|
sdnn, rmssd, cv_rr = self.calculate_hrv(self.all_hr_values, window_size=5)
|
||
|
return avg_hr, sdnn, rmssd, cv_rr, self.bvp_signal
|
||
|
|
||
|
def rppg_to_bvp(self, rppg_signal):
|
||
|
# 应用带通滤波器
|
||
|
nyquist = 0.5 * self.fps
|
||
|
low = self.freqs_min / nyquist
|
||
|
high = self.freqs_max / nyquist
|
||
|
b, a = signal.butter(3, [low, high], btype='band')
|
||
|
bvp = signal.filtfilt(b, a, rppg_signal)
|
||
|
|
||
|
# 标准化
|
||
|
bvp = (bvp - np.mean(bvp)) / np.std(bvp)
|
||
|
|
||
|
return bvp
|
||
|
|
||
|
def plot_bvp_signal(self):
|
||
|
plt.figure(figsize=(12, 4))
|
||
|
plt.plot(self.bvp_signal)
|
||
|
plt.title('BVP Signal (Average of 3 ICA Components)')
|
||
|
plt.xlabel('Samples')
|
||
|
plt.ylabel('Amplitude')
|
||
|
plt.grid(True)
|
||
|
plt.show()
|
||
|
|
||
|
|
||
|
if __name__ == '__main__':
|
||
|
|
||
|
ROI = []
|
||
|
|
||
|
freqs_min = 0.8
|
||
|
freqs_max = 1.8
|
||
|
heartrate = 0
|
||
|
sdnn, rmssd, cv_rr = 0, 0, 0
|
||
|
camera_code = 0
|
||
|
capture = cv2.VideoCapture(camera_code)
|
||
|
fps = capture.get(cv2.CAP_PROP_FPS)
|
||
|
|
||
|
hr_monitor = HeartRateMonitor(fps, freqs_min, freqs_max)
|
||
|
|
||
|
detector = dlib.get_frontal_face_detector()
|
||
|
while capture.isOpened():
|
||
|
ret, frame = capture.read()
|
||
|
if not ret:
|
||
|
continue
|
||
|
dects = detector(frame)
|
||
|
for face in dects:
|
||
|
left = face.left()
|
||
|
right = face.right()
|
||
|
top = face.top()
|
||
|
bottom = face.bottom()
|
||
|
|
||
|
h = bottom - top
|
||
|
w = right - left
|
||
|
roi = frame[top + h // 10 * 2:top + h // 10 * 7, left + w // 9 * 2:left + w // 9 * 8]
|
||
|
|
||
|
cv2.rectangle(frame, (left + w // 9 * 2, top + h // 10 * 2), (left + w // 9 * 8, top + h // 10 * 7),
|
||
|
color=(0, 0, 255))
|
||
|
cv2.rectangle(frame, (left, top), (left + w, top + h), color=(0, 0, 255))
|
||
|
ROI.append(roi)
|
||
|
if len(ROI) == 300:
|
||
|
heartrate, sdnn, rmssd, cv_rr = hr_monitor.process_roi(ROI)
|
||
|
for i in range(30):
|
||
|
ROI.pop(0)
|
||
|
cv2.putText(frame, '{:.1f}bps, CV R-R: {:.2f}'.format(heartrate, cv_rr), (50, 50), cv2.FONT_HERSHEY_SIMPLEX,
|
||
|
1.2,
|
||
|
(255, 0, 255), 2)
|
||
|
cv2.putText(frame, 'SDNN: {:.2f}, RMSSD: {:.2f}'.format(sdnn, rmssd), (50, 80),
|
||
|
cv2.FONT_HERSHEY_SIMPLEX, 1,
|
||
|
(255, 0, 255), 2)
|
||
|
cv2.imshow('frame', frame)
|
||
|
if cv2.waitKey(1) & 0xFF == ord('q'):
|
||
|
break
|
||
|
|
||
|
cv2.destroyAllWindows()
|
||
|
capture.release()
|