From f44d398d1e51576a50128e3de7f55ceabdfb3d54 Mon Sep 17 00:00:00 2001 From: Chillout Charles Date: Sun, 11 Aug 2024 16:56:01 -0500 Subject: [PATCH 1/6] min peak dist and detrend fix --- logic/biometrics.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/logic/biometrics.py b/logic/biometrics.py index c101696..3c7b45d 100644 --- a/logic/biometrics.py +++ b/logic/biometrics.py @@ -41,20 +41,21 @@ def estimate_heart_rate(self, hr_ir, hr_red, ppg_ambient): lowcut = 0.5 highcut = 4.25 order = 4 + min_distance = 1 / highcut * self.ppg_sampling_rate # remove ambient light hr_ir = np.clip(hr_ir - hr_ambient, 0, None) hr_red = np.clip(hr_red - hr_ambient, 0, None) # detrend and filter down to possible heart rates - DataFilter.detrend(hr_red, DetrendOperations.LINEAR) - DataFilter.detrend(hr_ir, DetrendOperations.LINEAR) DataFilter.perform_bandpass(hr_red, self.ppg_sampling_rate, lowcut, highcut, order, FilterTypes.BUTTERWORTH, 0) DataFilter.perform_bandpass(hr_ir, self.ppg_sampling_rate, lowcut, highcut, order, FilterTypes.BUTTERWORTH, 0) + DataFilter.detrend(hr_red, DetrendOperations.LINEAR) + DataFilter.detrend(hr_ir, DetrendOperations.LINEAR) # find peaks in signal - red_peaks, _ = find_peaks(hr_red, distance=self.ppg_sampling_rate/2) - ir_peaks, _ = find_peaks(hr_ir, distance=self.ppg_sampling_rate/2) + red_peaks, _ = find_peaks(hr_red, distance=min_distance) + ir_peaks, _ = find_peaks(hr_ir, distance=min_distance) # get inter-peak intervals red_ipis = np.diff(red_peaks) / self.ppg_sampling_rate From 25df73b2ffbf86b5239c1e58cddb1b35a2d40f0b Mon Sep 17 00:00:00 2001 From: Chillout Charles Date: Tue, 13 Aug 2024 18:26:50 -0500 Subject: [PATCH 2/6] improved peak detection via z score --- logic/biometrics.py | 51 ++++++++++++++++++++++----------------------- main.py | 7 +------ 2 files changed, 26 insertions(+), 32 deletions(-) diff --git a/logic/biometrics.py b/logic/biometrics.py index 3c7b45d..6bd76b4 100644 --- a/logic/biometrics.py +++ b/logic/biometrics.py @@ -1,7 +1,7 @@ from logic.base_logic import OptionalBaseLogic from brainflow.board_shim import BoardShim, BrainFlowPresets -from brainflow.data_filter import DataFilter, AggOperations, NoiseTypes, FilterTypes, DetrendOperations, WindowOperations +from brainflow.data_filter import DataFilter, FilterTypes, DetrendOperations from scipy.signal import find_peaks import numpy as np @@ -14,7 +14,7 @@ class Biometrics(OptionalBaseLogic): RESP_FREQ = "BreathsPerSecond" RESP_BPM = "BreathsPerMinute" - def __init__(self, board, supported=True, fft_size=1024, ema_decay=0.025): + def __init__(self, board, supported=True, window_seconds=10, ema_decay=0.025): super().__init__(board, supported) if supported: @@ -25,45 +25,44 @@ def __init__(self, board, supported=True, fft_size=1024, ema_decay=0.025): self.ppg_sampling_rate = BoardShim.get_sampling_rate( board_id, BrainFlowPresets.ANCILLARY_PRESET) - self.window_seconds = int(fft_size / self.ppg_sampling_rate) + 1 + self.window_seconds = window_seconds self.max_sample_size = self.ppg_sampling_rate * self.window_seconds - self.fft_size = fft_size + + # heart rate filter params + self.lowcut = 30 / 60 + self.highcut = 240 / 60 + self.order = 4 + self.min_distance = 1 / self.highcut * self.ppg_sampling_rate # ema smoothing variables self.current_values = None self.ema_decay = ema_decay - def estimate_heart_rate(self, hr_ir, hr_red, ppg_ambient): + def estimate_heart_rate(self, ppg_ir, ppg_red, ppg_ambient): # do not modify data - hr_ir, hr_red, hr_ambient = np.copy(hr_ir), np.copy(hr_red), np.copy(ppg_ambient) - - # Possible min and max heart rate in hz - lowcut = 0.5 - highcut = 4.25 - order = 4 - min_distance = 1 / highcut * self.ppg_sampling_rate + ppg_ir, ppg_red, ppg_ambient = np.copy(ppg_ir), np.copy(ppg_red), np.copy(ppg_ambient) # remove ambient light - hr_ir = np.clip(hr_ir - hr_ambient, 0, None) - hr_red = np.clip(hr_red - hr_ambient, 0, None) + ppg_ir = np.clip(ppg_ir - ppg_ambient, 0, None) + ppg_red = np.clip(ppg_red - ppg_ambient, 0, None) # detrend and filter down to possible heart rates - DataFilter.perform_bandpass(hr_red, self.ppg_sampling_rate, lowcut, highcut, order, FilterTypes.BUTTERWORTH, 0) - DataFilter.perform_bandpass(hr_ir, self.ppg_sampling_rate, lowcut, highcut, order, FilterTypes.BUTTERWORTH, 0) - DataFilter.detrend(hr_red, DetrendOperations.LINEAR) - DataFilter.detrend(hr_ir, DetrendOperations.LINEAR) + DataFilter.perform_bandpass(ppg_red, self.ppg_sampling_rate, self.lowcut, self.highcut, self.order, FilterTypes.BUTTERWORTH, 0) + DataFilter.perform_bandpass(ppg_ir, self.ppg_sampling_rate, self.lowcut, self.highcut, self.order, FilterTypes.BUTTERWORTH, 0) + DataFilter.detrend(ppg_red, DetrendOperations.LINEAR) + DataFilter.detrend(ppg_ir, DetrendOperations.LINEAR) # find peaks in signal - red_peaks, _ = find_peaks(hr_red, distance=min_distance) - ir_peaks, _ = find_peaks(hr_ir, distance=min_distance) + ppg_red = DataFilter.detect_peaks_z_score(ppg_red) + ppg_ir = DataFilter.detect_peaks_z_score(ppg_ir) + red_peaks, _ = find_peaks(ppg_red, distance=self.min_distance) + ir_peaks, _ = find_peaks(ppg_ir, distance=self.min_distance) - # get inter-peak intervals - red_ipis = np.diff(red_peaks) / self.ppg_sampling_rate - ir_ipis = np.diff(ir_peaks) / self.ppg_sampling_rate - ipis = np.concatenate((red_ipis, ir_ipis)) + # get inter-peak sample intervals + sample_ipis = np.concatenate((np.diff(red_peaks), np.diff(ir_peaks))) - # get bpm from mean inter-peak interval - average_ipi = np.mean(ipis) + # get bpm from mean inter-peak sample interval + average_ipi = np.mean(sample_ipis) / self.ppg_sampling_rate heart_bpm = 60 / average_ipi return heart_bpm diff --git a/main.py b/main.py index 7e0cd07..640b264 100644 --- a/main.py +++ b/main.py @@ -124,9 +124,7 @@ def BoardInit(args: argparse.Namespace) -> tuple[BoardShim, list[BaseLogic], int ### Logic Modules ### has_muse_ppg = master_board_id in (BoardIds.MUSE_2_BOARD, BoardIds.MUSE_S_BOARD) - - fft_size= 64 * 10 # TODO: Make this configurable - biometrics_logic = Biometrics(board, has_muse_ppg, fft_size=fft_size, ema_decay=ema_decay) + biometrics_logic = Biometrics(board, has_muse_ppg, ema_decay=ema_decay) logics = [ Info(board, window_seconds=window_seconds), @@ -146,9 +144,6 @@ def BoardInit(args: argparse.Namespace) -> tuple[BoardShim, list[BaseLogic], int if args.enable_action: logics.append(MLAction(board, ema_decay = ema_decay * args.action_ema_multiplier)) - ### Adding one second to startup time for adaptive filters ### - startup_time += 1 - BoardShim.log_message(LogLevels.LEVEL_INFO.value, 'Intializing (wait {}s)'.format(startup_time)) board.start_stream(streamer_params=args.streamer_params) time.sleep(startup_time) From abe602012fd4f50bbf1031ebc44c90cd337fdeae Mon Sep 17 00:00:00 2001 From: Chillout Charles Date: Tue, 13 Aug 2024 21:42:57 -0500 Subject: [PATCH 3/6] resampling --- logic/biometrics.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/logic/biometrics.py b/logic/biometrics.py index 6bd76b4..2f81779 100644 --- a/logic/biometrics.py +++ b/logic/biometrics.py @@ -2,7 +2,7 @@ from brainflow.board_shim import BoardShim, BrainFlowPresets from brainflow.data_filter import DataFilter, FilterTypes, DetrendOperations -from scipy.signal import find_peaks +from scipy.signal import find_peaks, resample import numpy as np import utils @@ -32,7 +32,10 @@ def __init__(self, board, supported=True, window_seconds=10, ema_decay=0.025): self.lowcut = 30 / 60 self.highcut = 240 / 60 self.order = 4 - self.min_distance = 1 / self.highcut * self.ppg_sampling_rate + + self.resample_rate = int(self.highcut * 2 + 0.5) # nyquist + self.resample_size = self.resample_rate * self.window_seconds + self.min_distance = 1 / self.highcut * self.resample_rate # ema smoothing variables self.current_values = None @@ -49,12 +52,16 @@ def estimate_heart_rate(self, ppg_ir, ppg_red, ppg_ambient): # detrend and filter down to possible heart rates DataFilter.perform_bandpass(ppg_red, self.ppg_sampling_rate, self.lowcut, self.highcut, self.order, FilterTypes.BUTTERWORTH, 0) DataFilter.perform_bandpass(ppg_ir, self.ppg_sampling_rate, self.lowcut, self.highcut, self.order, FilterTypes.BUTTERWORTH, 0) + + ppg_red = resample(ppg_red, self.resample_size) + ppg_ir = resample(ppg_red, self.resample_size) + DataFilter.detrend(ppg_red, DetrendOperations.LINEAR) DataFilter.detrend(ppg_ir, DetrendOperations.LINEAR) # find peaks in signal - ppg_red = DataFilter.detect_peaks_z_score(ppg_red) - ppg_ir = DataFilter.detect_peaks_z_score(ppg_ir) + ppg_red = DataFilter.detect_peaks_z_score(ppg_red, threshold=3) + ppg_ir = DataFilter.detect_peaks_z_score(ppg_ir, threshold=3) red_peaks, _ = find_peaks(ppg_red, distance=self.min_distance) ir_peaks, _ = find_peaks(ppg_ir, distance=self.min_distance) @@ -62,7 +69,7 @@ def estimate_heart_rate(self, ppg_ir, ppg_red, ppg_ambient): sample_ipis = np.concatenate((np.diff(red_peaks), np.diff(ir_peaks))) # get bpm from mean inter-peak sample interval - average_ipi = np.mean(sample_ipis) / self.ppg_sampling_rate + average_ipi = np.mean(sample_ipis) / self.resample_rate heart_bpm = 60 / average_ipi return heart_bpm From 271b2b9e5927544559fa435b804f0166fb456316 Mon Sep 17 00:00:00 2001 From: Chillout Charles Date: Fri, 16 Aug 2024 11:24:40 -0500 Subject: [PATCH 4/6] no peak fix --- logic/biometrics.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/logic/biometrics.py b/logic/biometrics.py index 2f81779..86c3da4 100644 --- a/logic/biometrics.py +++ b/logic/biometrics.py @@ -70,7 +70,9 @@ def estimate_heart_rate(self, ppg_ir, ppg_red, ppg_ambient): # get bpm from mean inter-peak sample interval average_ipi = np.mean(sample_ipis) / self.resample_rate - heart_bpm = 60 / average_ipi + heart_bpm = 0 + if not np.isnan(average_ipi) and average_ipi != 0: + heart_bpm = 60 / average_ipi return heart_bpm From 64e26e8fe4516b6423ee834d9da20de04d39129c Mon Sep 17 00:00:00 2001 From: Chillout Charles Date: Sat, 24 Aug 2024 00:07:34 -0500 Subject: [PATCH 5/6] wavelet denoising and scipy filtering --- logic/biometrics.py | 48 ++++++++++++++++++--------------------------- 1 file changed, 19 insertions(+), 29 deletions(-) diff --git a/logic/biometrics.py b/logic/biometrics.py index 86c3da4..a88da09 100644 --- a/logic/biometrics.py +++ b/logic/biometrics.py @@ -1,8 +1,8 @@ from logic.base_logic import OptionalBaseLogic from brainflow.board_shim import BoardShim, BrainFlowPresets -from brainflow.data_filter import DataFilter, FilterTypes, DetrendOperations -from scipy.signal import find_peaks, resample +from brainflow.data_filter import DataFilter, WaveletTypes +from scipy.signal import find_peaks, butter, filtfilt import numpy as np import utils @@ -29,13 +29,11 @@ def __init__(self, board, supported=True, window_seconds=10, ema_decay=0.025): self.max_sample_size = self.ppg_sampling_rate * self.window_seconds # heart rate filter params - self.lowcut = 30 / 60 - self.highcut = 240 / 60 - self.order = 4 - - self.resample_rate = int(self.highcut * 2 + 0.5) # nyquist - self.resample_size = self.resample_rate * self.window_seconds - self.min_distance = 1 / self.highcut * self.resample_rate + lowcut = 30 / 60 + highcut = 240 / 60 + order = 2 + b, a = butter(order, (lowcut, highcut), btype="bandpass", fs=self.ppg_sampling_rate) + self.hr_filter = lambda data: filtfilt(b, a, data) # ema smoothing variables self.current_values = None @@ -46,34 +44,26 @@ def estimate_heart_rate(self, ppg_ir, ppg_red, ppg_ambient): ppg_ir, ppg_red, ppg_ambient = np.copy(ppg_ir), np.copy(ppg_red), np.copy(ppg_ambient) # remove ambient light - ppg_ir = np.clip(ppg_ir - ppg_ambient, 0, None) - ppg_red = np.clip(ppg_red - ppg_ambient, 0, None) - - # detrend and filter down to possible heart rates - DataFilter.perform_bandpass(ppg_red, self.ppg_sampling_rate, self.lowcut, self.highcut, self.order, FilterTypes.BUTTERWORTH, 0) - DataFilter.perform_bandpass(ppg_ir, self.ppg_sampling_rate, self.lowcut, self.highcut, self.order, FilterTypes.BUTTERWORTH, 0) + ppg_ir -= ppg_ambient + ppg_red -= ppg_ambient - ppg_red = resample(ppg_red, self.resample_size) - ppg_ir = resample(ppg_red, self.resample_size) + # Denoise and Filter to possible heart rates + DataFilter.perform_wavelet_denoising(ppg_ir, WaveletTypes.DB4, 5) + DataFilter.perform_wavelet_denoising(ppg_red, WaveletTypes.DB4, 5) + ppg_ir = self.hr_filter(ppg_ir) + ppg_red = self.hr_filter(ppg_red) - DataFilter.detrend(ppg_red, DetrendOperations.LINEAR) - DataFilter.detrend(ppg_ir, DetrendOperations.LINEAR) - # find peaks in signal - ppg_red = DataFilter.detect_peaks_z_score(ppg_red, threshold=3) - ppg_ir = DataFilter.detect_peaks_z_score(ppg_ir, threshold=3) - red_peaks, _ = find_peaks(ppg_red, distance=self.min_distance) - ir_peaks, _ = find_peaks(ppg_ir, distance=self.min_distance) + red_peaks, _ = find_peaks(ppg_red) + ir_peaks, _ = find_peaks(ppg_ir) # get inter-peak sample intervals sample_ipis = np.concatenate((np.diff(red_peaks), np.diff(ir_peaks))) # get bpm from mean inter-peak sample interval - average_ipi = np.mean(sample_ipis) / self.resample_rate - heart_bpm = 0 - if not np.isnan(average_ipi) and average_ipi != 0: - heart_bpm = 60 / average_ipi - + average_ipi = np.mean(sample_ipis) / self.ppg_sampling_rate + heart_bpm = 60 / average_ipi + return heart_bpm def calculate_data_dict(self): From f8c8b43cd03e561d08b39eda48ad1ae70d483cdf Mon Sep 17 00:00:00 2001 From: Chillout Charles Date: Mon, 9 Sep 2024 18:52:07 -0500 Subject: [PATCH 6/6] lower highcut, min peak distance, wavelet level edit --- logic/biometrics.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/logic/biometrics.py b/logic/biometrics.py index a88da09..edeb25e 100644 --- a/logic/biometrics.py +++ b/logic/biometrics.py @@ -30,10 +30,11 @@ def __init__(self, board, supported=True, window_seconds=10, ema_decay=0.025): # heart rate filter params lowcut = 30 / 60 - highcut = 240 / 60 + highcut = 150 / 60 order = 2 b, a = butter(order, (lowcut, highcut), btype="bandpass", fs=self.ppg_sampling_rate) self.hr_filter = lambda data: filtfilt(b, a, data) + self.min_distance = self.ppg_sampling_rate / highcut # ema smoothing variables self.current_values = None @@ -48,14 +49,14 @@ def estimate_heart_rate(self, ppg_ir, ppg_red, ppg_ambient): ppg_red -= ppg_ambient # Denoise and Filter to possible heart rates - DataFilter.perform_wavelet_denoising(ppg_ir, WaveletTypes.DB4, 5) - DataFilter.perform_wavelet_denoising(ppg_red, WaveletTypes.DB4, 5) + DataFilter.perform_wavelet_denoising(ppg_ir, WaveletTypes.DB4, 4) + DataFilter.perform_wavelet_denoising(ppg_red, WaveletTypes.DB4, 4) ppg_ir = self.hr_filter(ppg_ir) ppg_red = self.hr_filter(ppg_red) # find peaks in signal - red_peaks, _ = find_peaks(ppg_red) - ir_peaks, _ = find_peaks(ppg_ir) + red_peaks, _ = find_peaks(ppg_red, distance=self.min_distance) + ir_peaks, _ = find_peaks(ppg_ir, distance=self.min_distance) # get inter-peak sample intervals sample_ipis = np.concatenate((np.diff(red_peaks), np.diff(ir_peaks)))