def __init__(self, num_samples, fs, data=None): self.__steps = 0 self.__num_samples = num_samples self.__fs = fs self.__l1 = CircularList(data, num_samples) self.__filtered = CircularList([], num_samples) self.__b, self.__a = filt.create_filter(3, 1.2, "lowpass", fs) self.__peak_arr = []
def __init__(self, num_samples, fs, times=[], data=[]): self.__hr = 0 self.__num_samples = num_samples self.__fs = fs self.__time = CircularList(times, num_samples) self.__ppg = CircularList(data, num_samples) self.__filtered = CircularList([], num_samples)
def collect_samples(): num_samples = 500 # 10 seconds of data @ 50Hz times = CircularList([], num_samples) ppg = CircularList([], num_samples) comms = Communication("/dev/cu.usbserial-1420", 115200) try: comms.clear() # just in case any junk is in the pipes # wait for user and then count down input("Ready to collect data? Press [ENTER] to begin.\n") print("Start measuring in...") for k in range(3,0,-1): print(k) sleep(1) print("Begin!") comms.send_message("wearable") # begin sending data sample = 0 while(sample < num_samples): message = comms.receive_message() if(message != None): try: (m1, _, _, _, m2) = message.split(',') except ValueError: # if corrupted data, skip the sample continue # add the new values to the circular lists times.add(int(m1)) ppg.add(int(m2)) sample += 1 print("Collected {:d} samples".format(sample)) # a single ndarray for all samples for easy file I/O data = np.column_stack([times, ppg]) except(Exception, KeyboardInterrupt) as e: print(e) # exiting the program due to exception finally: comms.send_message("sleep") # stop sending data comms.close() return data
from matplotlib import pyplot as plt from time import time from time import sleep import numpy as np if __name__ == "__main__": fs = 50 # sampling rate num_samples = 500 # 10 seconds of data @ 50Hz process_time = 1 # compute the heart beat every second MAX_HEART_RATE = 200 hr = HRMonitor(num_samples, 50) comms = Communication("/dev/cu.ag-ESP32SPP", 115200) comms.clear() # just in case any junk is in the pipes comms.send_message("wearable") # begin sending data print("Ready!") hr_plot = CircularList([], num_samples) t = CircularList([], num_samples) # Live data try: previous_time = time() while(True): message = comms.receive_message() if (message != None): try: (m1, _, _, _, m2) = message.split(',') except ValueError: # if corrupted data, skip the sample continue # Collect data in the pedometer hr.add(int(m1)/1e3, int(m2)) t.add(int(m1)/1e3) t_plot = np.array(t)
if __name__ == "__main__": num_samples = 100 # 2 seconds of data @ 50Hz refresh_time = 0.1 # update the plot every 0.1s (10 FPS) # This is a predefined dictionary I will use to decide what transformation method to call transform_dict = { 0: average_value, 1: sample_difference, 2: l2_norm_calculation, 3: l1_norm_calculation, 4: max_acceleration } transform_method = transform_dict[0] times = CircularList([], num_samples) ax = CircularList([], num_samples) ay = CircularList([], num_samples) az = CircularList([], num_samples) # These will contain data for the transformation data transform_x = CircularList([], num_samples) transform_y = CircularList([], num_samples) transform_z = CircularList([], num_samples) fig = plt.figure() ax1 = fig.add_subplot(311) ax2 = fig.add_subplot(312) ax3 = fig.add_subplot(313) comms = Communication("/dev/cu.AkshayBluetooth-ESP32SPP", 115200)
class Pedometer: """ Encapsulated class attributes (with default values) """ __steps = 0 # the current step count __l1 = None # CircularList containing L1-norm __filtered = None # CircularList containing filtered signal __num_samples = 0 # The length of data maintained __new_samples = 0 # How many new samples exist to process __fs = 0 # Sampling rate in Hz __b = None # Low-pass coefficients __a = None # Low-pass coefficients __thresh_low = 1.5 # Threshold from Tutorial 2 __thresh_high = 15 # Threshold from Tutorial 2 """ Initialize the class instance """ def __init__(self, num_samples, fs, data=None): self.__steps = 0 self.__num_samples = num_samples self.__fs = fs self.__l1 = CircularList(data, num_samples) self.__filtered = CircularList([], num_samples) self.__b, self.__a = filt.create_filter(3, 1.2, "lowpass", fs) self.__peak_arr = [] """ Sets the upper and threshold for detecting steps """ def set_thresholds(self, low_thresh, high_thresh): self.__thresh_low = low_thresh self.__thresh_high = high_thresh """ Add new samples to the data buffer Handles both integers and vectors! """ def add(self, ax, ay, az): l1 = filt.l1_norm(ax, ay, az) if isinstance(ax, int): num_add = 1 else: num_add = len(ax) l1 = l1.tolist() self.__l1.add(l1) self.__new_samples += num_add """ Process the new data to update step count """ def process(self): # Grab only the new samples into a NumPy array x = np.array(self.__l1[-self.__new_samples:]) # Filter the signal (detrend, LP, MA, etc…) # ... # ... # ... # Store the filtered data ma = filt.moving_average(x, 20) # Compute Moving Average dt = filt.detrend(ma) # Detrend the Signal lp = filt.filter(self.__b, self.__a, dt) # Low-pass Filter Signal grad = filt.gradient(lp) # Compute the gradient x = filt.moving_average( grad, 20) # Compute the moving average of the gradient self.__filtered.add(x.tolist()) # Count the number of peaks in the filtered data count, peaks = filt.count_peaks(x, self.__thresh_low, self.__thresh_high) # Update the step count and reset the new sample count self.__steps += count self.__new_samples = 0 # Return the step count, peak locations, and filtered data return self.__steps, peaks, np.array(self.__filtered) """ Clear the data buffers and step count """ def reset(self): self.__steps = 0
def collect_samples(): num_samples = 500 # 10 seconds of data @ 50Hz times = CircularList([], num_samples) ax = CircularList([], num_samples) ay = CircularList([], num_samples) az = CircularList([], num_samples) comms = Communication("/dev/cu.ag-ESP32SPP", 115200) try: comms.clear() # just in case any junk is in the pipes # wait for user to start walking before starting to collect data input( "Walk for 10 seconds to calibrate the pedometer. Press [ENTER] to begin.\n" ) sleep(3) comms.send_message("wearable") # begin sending data sample = 0 while sample < num_samples: message = comms.receive_message() if (message != None): try: (m1, m2, m3, m4) = message.split(',') except ValueError: # if corrupted data, skip the sample continue # add the new values to the circular lists times.add(int(m1)) ax.add(int(m2)) ay.add(int(m3)) az.add(int(m4)) sample += 1 print("Collected {:d} samples".format(sample)) # a single ndarray for all samples for easy file I/O data = np.column_stack([times, ax, ay, az]) except (Exception, KeyboardInterrupt) as e: print(e) # exiting the program due to exception finally: comms.send_message("sleep") # stop sending data comms.close() return data
num_samples = 500 # 10 seconds of data @ 50Hz process_time = 1 # compute the heart beat every second MAX_HEART_RATE = 200 # the maximum heart rate possible initial_period = num_samples / fs # set initial period in seconds hr = HRMonitor(num_samples, fs) hr.train() input("Ready to start monitoring? Press [ENTER] to begin.\n") comms = Communication("/dev/cu.ag-ESP32SPP", 115200) comms.clear() # just in case any junk is in the pipes comms.send_message("wearable") # begin sending data # Used for plotting data t = CircularList([], num_samples) peaks = CircularList([], num_samples) # Live data try: previous_time = time() while True: message = comms.receive_message() if message != None: try: (m1, _, _, _, m2) = message.split(',') except ValueError: # if corrupted data, skip the sample continue # Collect data in the pedometer hr.add(int(m1) / 1e3, int(m2)) t.add(int(m1) / 1e3)
class Pedometer: """ Encapsulated class attributes (with default values) """ __steps = 0 # the current step count __l1 = None # CircularList containing L1-norm __filtered = None # CircularList containing filtered signal __num_samples = 0 # The length of data maintained __new_samples = 0 # How many new samples exist to process __fs = 0 # Sampling rate in Hz __b = None # Low-pass coefficients __a = None # Low-pass coefficients __thresh_low = 3 # Threshold from Tutorial 2 __thresh_high = 15 # Threshold from Tutorial 2 __xi = None # Initial conditions for filter __yi = None # Initial conditions for filter """ Initialize the class instance """ def __init__(self, num_samples, fs, data=None): self.__steps = 0 self.__num_samples = num_samples self.__fs = fs self.__l1 = CircularList(data, num_samples) self.__filtered = CircularList([], num_samples) self.__b, self.__a = filt.create_filter(3, 1.2, "lowpass", fs) self.__xi = np.zeros(4) self.__yi = np.zeros(4) """ Add new samples to the data buffer Handles both integers and vectors! """ def add(self, ax, ay, az): l1 = filt.l1_norm(ax, ay, az) if isinstance(ax, int): num_add = 1 else: num_add = len(ax) l1 = l1.tolist() self.__l1.add(l1) self.__new_samples += num_add """ Process the new data to update step count """ def process(self): # Grab only the new samples into a NumPy array x = np.array(self.__l1[-self.__new_samples:]) # Filter the signal (detrend, LP, MA, etc…) x = filt.detrend(x) # x = filt.filter(self.__b, self.__a, x) x, self.__xi, self.__yi = filt.filter_ic(self.__b, self.__a, x, self.__xi, self.__yi) x = filt.gradient(x) x = filt.moving_average(x, 25) # Store the filtered data self.__filtered.add(x.tolist()) # Count the number of peaks in the filtered data count, peaks = filt.count_peaks(x, self.__thresh_low, self.__thresh_high) # Update the step count and reset the new sample count self.__steps += count self.__new_samples = 0 # Return the step count, peak locations, and filtered data return self.__steps, peaks, np.array(self.__filtered) """ Clear the data buffers and step count """ def reset(self): self.__steps = 0 self.__l1.clear() self.__filtered = np.zeros(self.__num_samples)
class Processing(): __num_samples = 250 # 2 seconds of data @ 50Hz __refresh_time = 0.5 # update the plot every 0.1s (10 FPS) # Thresholds for idle detection __ax_idle_threshold = 1935 __ay_idle_threshold = 1918 __az_idle_threshold = 2430 """ Initializes the processing object and sets the field variables @:param transformation_method: the type of transformation that will be plotted and computed @:param port_name: The Serial port name used for Serial communication """ def __init__(self, transformation_method, port_name): self.comms = Communication(port_name, 115200) __transform_dict = { "average acceleration": self.__average_value, "sample difference": self.__sample_difference, "L2 norm": self.__l2_norm_calculation, "L1 norm": self.__l1_norm_calculation, "Maximum acceleration:": self.__max_acceleration } # This will keep track of the transformation method to be called self.__transformation_method = __transform_dict[transformation_method] self.transform_x = CircularList([], self.__num_samples) self.transform_y = CircularList([], self.__num_samples) self.transform_z = CircularList([], self.__num_samples) # These will contain the acceleration values read from the accelerometer self.times = CircularList([], self.__num_samples) self.ax = CircularList([], self.__num_samples) self.ay = CircularList([], self.__num_samples) self.az = CircularList([], self.__num_samples) # Set up the plotting fig = plt.figure() self.ax1 = fig.add_subplot(311) self.ax2 = fig.add_subplot(312) self.ax3 = fig.add_subplot(313) self.graph_type = transformation_method # times to keep track of when self.__last_idlecheck_time = 0 self.__idle_state = False self.__last_active_time = 0 """ Updates the plot, displaying the acceeleration values as well as the transformation values """ def __plot(self): # Clears the plots and resets them self.ax1.cla() self.ax2.cla() self.ax3.cla() # Sets the title of the plot self.ax1.set_title("X acceleration (Red) and " + self.graph_type + " (Blue)") self.ax2.set_title("Y acceleration (Red) and " + self.graph_type + " (Blue)") self.ax3.set_title("X acceleration (Red) and " + self.graph_type + " (Blue)") # Plots the acceleration values along with the transformation self.ax1.plot(self.transform_x, 'b', self.ax, 'r') self.ax2.plot(self.transform_y, 'b', self.ay, 'r') self.ax3.plot(self.transform_z, 'b', self.az, 'r') plt.show(block=False) plt.pause(0.0001) """ Determines whether the device is inactive or not @:param average_x: The average x-acceleration @:param average_y: The average y-acceleration @:param average_z: The average z-acceleration @:return: a boolean representing whether the device is inactive or not """ def __is_inactive(self, average_x, average_y, average_z): return average_x <= self.__ax_idle_threshold and average_y <= self.__ay_idle_threshold and average_z <= self.__az_idle_threshold """ Computes the average value for an axis @:param acceleration_list: The acceleration over 5 seconds in either x,y,z direction @:return: A scalar which is the average from the acceleration list """ def __average_value(self, acceleration_list): return np.average(np.array(acceleration_list)) """ Computes the difference between each adjacent indexes @:param acceleration_list: The acceleration over 5 seconds in either x,y,z direction @:return: a numpy array containing the differences between each point """ def __sample_difference(self, acceleration_list): np.diff(np.array(acceleration_list)) return np.diff(np.array(acceleration_list)) """ Computes the euclidean distance for the acceleration list @:param x_acceleration: one sample of the x-acceleration @:param y_acceleration: one sample of the y-acceleration @:param z_acceleration: one sample of the z-acceleration @:return: A scalar which is the square root of the sum of each number in the """ def __l2_norm_calculation(self, x_acceleration, y_acceleration, z_acceleration): return np.linalg.norm( np.array([x_acceleration, y_acceleration, z_acceleration])) """ Computes the L1 norm for the acceleration lsit @:param x_acceleration: one sample of the x-acceleration @:param y_acceleration: one sample of the y-acceleration @:param z_acceleration: one sample of the z-acceleration @:return: The sum of the absolute value of each acceleration value """ def __l1_norm_calculation(self, x_acceleration, y_acceleration, z_acceleration): return np.linalg.norm(np.array( [x_acceleration, y_acceleration, z_acceleration]), ord=1) """ Finds the max acceleration in one of the accelerations @:param: acceleration_list: The acceleration over 5 seconds in either x,y,z direction @:return: The maximum value in the acceleration list """ def __max_acceleration(self, acceleration_list): return int(np.max(np.array(acceleration_list))) """ Records the acceleration and times from the accelerometer @:param time: the time from the Serial monitor @:param x_acceleration: the x-acceleration @:param y_acceleration: the y-acceleration @:param z_acceleration: the z-acceleration """ def __record_acceleration(self, time, x_acceleration, y_acceleration, z_acceleration): # add the new values to the circular lists self.times.add(int(time)) self.ax.add(int(x_acceleration)) self.ay.add(int(y_acceleration)) self.az.add(int(z_acceleration)) """ Records the transformation value based on what transformation type the instance uses @:param x_acceleration: one sample of the x-acceleration @:param y_acceleration: one sample of the y-acceleration @:param z_acceleration: one sample of the z-acceleration """ def __record_transformation(self, x_acceleration, y_acceleration, z_acceleration): # These if statements are used because some of these methods have different parameters and return types if self.__transformation_method == self.__l1_norm_calculation or self.__transformation_method == self.__l2_norm_calculation: norm_number = self.__transformation_method() self.transform_x.add(norm_number) self.transform_y.add(norm_number) self.transform_z.add(norm_number) # sets each transformation method to the sample difference array elif self.__transformation_method == self.__sample_difference: self.transform_x = self.__transformation_method(self.ax) self.transform_y = self.__transformation_method(self.ay) self.transform_z = self.__transformation_method(self.az) # This will either call average_value or maximum_acceleration else: self.transform_x.add(self.__transformation_method(self.ax)) self.transform_y.add(self.__transformation_method(self.ay)) self.transform_z.add(self.__transformation_method(self.az)) """ Checks if the device has been idle for 5 seconds or if it's been active for 1 second. This will either cause the motor to buzz or another message displaying that the person has been active. @:param current_time: The current time the program is at """ def __check_idle(self, current_time): # If it's been 5 seconds since the last time the person has been inactive if current_time - self.__last_idlecheck_time >= 5: # get the average acceleration over 5 seconds average_x = self.__average_value(self.ax) average_y = self.__average_value(self.ay) average_z = self.__average_value(self.az) print(average_x, ",", average_y, ",", average_z) self.__last_idlecheck_time = current_time # if the device has been idle for 5 seconds, buzz the motor if self.__is_inactive(average_x, average_y, average_z): self.__idle_state = True self.comms.send_message("Buzz motor") else: self.__idle_state = False # If the person has been inactive but has become active for 1 second if self.__idle_state and current_time - self.__last_active_time >= 1: self.__last_active_time = current_time # get the average values for the last 1 second average_x = self.__average_value(self.ax[200:]) average_y = self.__average_value(self.ay[200:]) average_z = self.__average_value(self.az[200:]) if not self.__is_inactive(average_x, average_y, average_z): print("Active accelerations: ", average_x, average_y, average_z) self.__last_idlecheck_time = current_time # this ensures that the person must be inactive for 5 seconds after their activity self.comms.send_message("Keep it up!") """ Runs all the processing for the Serial communication, including plotting the acceleration values and checking whether the device has been inactive or not. """ def run(self): self.comms.clear() # just in case any junk is in the pipes self.comms.send_message("wearable") # begin sending data try: previous_time = 0 while (True): message = self.comms.receive_message() if (message != None): try: (m1, m2, m3, m4) = message.split(',') except ValueError: # if corrupted data, skip the sample continue # Record the acceleration and transformation self.__record_acceleration(m1, m2, m3, m4) self.__record_transformation(m2, m3, m4) current_time = time() if current_time - previous_time > self.__refresh_time: previous_time = current_time self.__plot() self.__check_idle(current_time) except (Exception, KeyboardInterrupt) as e: print(e) # Exiting the program due to exception finally: print("Closing Connection") self.comms.send_message("sleep") # stop sending data self.comms.close() sleep(1)
def __init__(self, transformation_method, port_name): self.comms = Communication(port_name, 115200) __transform_dict = { "average acceleration": self.__average_value, "sample difference": self.__sample_difference, "L2 norm": self.__l2_norm_calculation, "L1 norm": self.__l1_norm_calculation, "Maximum acceleration:": self.__max_acceleration } # This will keep track of the transformation method to be called self.__transformation_method = __transform_dict[transformation_method] self.transform_x = CircularList([], self.__num_samples) self.transform_y = CircularList([], self.__num_samples) self.transform_z = CircularList([], self.__num_samples) # These will contain the acceleration values read from the accelerometer self.times = CircularList([], self.__num_samples) self.ax = CircularList([], self.__num_samples) self.ay = CircularList([], self.__num_samples) self.az = CircularList([], self.__num_samples) # Set up the plotting fig = plt.figure() self.ax1 = fig.add_subplot(311) self.ax2 = fig.add_subplot(312) self.ax3 = fig.add_subplot(313) self.graph_type = transformation_method # times to keep track of when self.__last_idlecheck_time = 0 self.__idle_state = False self.__last_active_time = 0
from ECE16Lib.Communication import Communication from ECE16Lib.CircularList import CircularList from matplotlib import pyplot as plt from time import time if __name__ == "__main__": num_samples = 100 # 2 seconds of data @ 50Hz refresh_time = 0.1 # update the plot every 0.1s (10 FPS) times = CircularList([], num_samples) ax = CircularList([], num_samples) ay = CircularList([], num_samples) az = CircularList([], num_samples) comms = Communication("/dev/cu.ag-ESP32SPP", 115200) comms.clear() # just in case any junk is in the pipes comms.send_message("wearable") # begin sending data fig = plt.figure() ax1 = fig.add_subplot(311) ax2 = fig.add_subplot(312) ax3 = fig.add_subplot(313) try: previous_time = 0 while(True): message = comms.receive_message() if(message != None): try: (m1, m2, m3, m4) = message.split(',') except ValueError: # if corrupted data, skip the sample continue
class HRMonitor: """ Encapsulated class attributes (with default values) """ __hr = 0 # the current heart rate __time = None # CircularList containing the time vector __ppg = None # CircularList containing the raw signal __filtered = None # CircularList containing filtered signal __num_samples = 0 # The length of data maintained __new_samples = 0 # How many new samples exist to process __fs = 0 # Sampling rate in Hz __thresh = 0.6 # Threshold from Tutorial 2 """ Initialize the class instance """ def __init__(self, num_samples, fs, times=[], data=[]): self.__hr = 0 self.__num_samples = num_samples self.__fs = fs self.__time = CircularList(times, num_samples) self.__ppg = CircularList(data, num_samples) self.__filtered = CircularList([], num_samples) """ Add new samples to the data buffer Handles both integers and vectors! """ def add(self, t, x): if isinstance(t, np.ndarray): t = t.tolist() if isinstance(x, np.ndarray): x = x.tolist() self.__time.add(t) self.__ppg.add(x) self.__new_samples += len(x) """ Compute the average heart rate over the peaks """ def compute_heart_rate(self, peaks): t = np.array(self.__time) return 60 / np.mean(np.diff(t[peaks])) """ Process the new data to update step count """ def process(self): # Grab only the new samples into a NumPy array x = np.array(self.__ppg[-self.__new_samples:]) # Filter the signal (feel free to customize!) x = filt.detrend(x, 25) x = filt.moving_average(x, 5) x = filt.gradient(x) x = filt.normalize(x) # Store the filtered data self.__filtered.add(x.tolist()) # Find the peaks in the filtered data _, peaks = filt.count_peaks(x, self.__thresh, 1) # Update the step count and reset the new sample count self.__hr = self.compute_heart_rate(peaks) self.__new_samples = 0 # Return the heart rate, peak locations, and filtered data return self.__hr, peaks, np.array(self.__filtered) """ Clear the data buffers and step count """ def reset(self): self.__steps = 0 self.__time.clear() self.__ppg.clear() self.__filtered = np.zeros(self.__num_samples)
class HRMonitor: """ Encapsulated class attributes (with default values) """ __hr = 0 # the current heart rate __time = None # CircularList containing the time vector __ppg = None # CircularList containing the raw signal __filtered = None # CircularList containing filtered signal __num_samples = 0 # The length of data maintained __new_samples = 0 # How many new samples exist to process __fs = 0 # Sampling rate in Hz __thresh = 0.6 # Threshold from Tutorial 2 __directory = "/Users/akshaygopalkrishnan/Desktop/ECE 16/Python/Lab 7/data/data" """ Initialize the class instance """ def __init__(self, num_samples, fs, times=[], data=[]): self.__hr = 0 self.__num_samples = num_samples self.__fs = fs self.__time = CircularList(data, num_samples) self.__ppg = CircularList(data, num_samples) self.__filtered = CircularList([], num_samples) self.__gmm = GMM(n_components=2) """ Add new samples to the data buffer Handles both integers and vectors! """ def add(self, t, x): if isinstance(t, np.ndarray): t = t.tolist() if isinstance(x, np.ndarray): x = x.tolist() if isinstance(x, int): self.__new_samples += 1 else: self.__new_samples += len(x) self.__time.add(t) self.__ppg.add(x) """ Compute the average heart rate over the peaks """ def compute_heart_rate(self, peaks): t = np.array(self.__time) if len(np.diff(t[peaks])) > 0: return 60 / np.mean(np.diff(t[peaks])) else: return 0 """ Removes outlier peaks from the filtered data @:param peaks: The location of each peak @:return the peaks with outliers removed """ def remove_outliers(self, peaks): if len(peaks) > 1: t = np.array(self.__time) # Calculate peak difference and average/standard deviation peak_diff = np.diff(t[peaks]) avg_peak_diff = np.mean(peak_diff) std_peak_diff = np.std(peak_diff) for i in range(len(peak_diff)): # If the peak difference is less than 2 deviations from the average, remove the peak (outlier) if peak_diff[i] < (avg_peak_diff - (1 * std_peak_diff)): peaks.pop(i + 1) return peaks # Filter the signal (as in the prior lab) def train_process(self, x): x = filt.detrend(x, 25) x = filt.moving_average(x, 5) x = filt.gradient(x) return filt.normalize(x) # Retrieve a list of the names of the subjects def get_subjects(self, directory): filepaths = glob.glob(directory + "/*") return [filepath.split("/")[-1] for filepath in filepaths] # Estimate the heart rate from the user-reported peak count def get_hr(self, filepath, num_samples, fs): count = int(filepath.split("_")[-1].split(".")[0]) seconds = num_samples / fs return count / seconds * 60 # 60s in a minute # Estimate the sampling rate from the time vector def estimate_fs(self, times): return 1 / np.mean(np.diff(times)) # Retrieve a data file, verifying its FS is reasonable def get_data(self, directory, subject, trial, fs): search_key = "%s/%s/%s_%02d_*.csv" % (directory, subject, subject, trial) filepath = glob.glob(search_key)[0] t, ppg = np.loadtxt(filepath, delimiter=',', unpack=True) t = (t - t[0]) / 1e3 hr = self.get_hr(filepath, len(ppg), fs) fs_est = self.estimate_fs(t) if (fs_est < fs - 1 or fs_est > fs): print("Bad data! FS=%.2f. Consider discarding: %s" % (fs_est, filepath)) return t, ppg, hr, fs_est """ Trains the GMM model on offline data @:return: the trained GMM model """ def train(self): print("Training GMM model... ") subjects = self.get_subjects(self.__directory) train_data = np.array([]) for subject in subjects: for trial in range(1, 11): t, ppg, hr, fs_est = self.get_data(self.__directory, subject, trial, self.__fs) train_data = np.append(train_data, self.train_process(ppg)) # Train the GMM train_data = train_data.reshape(-1, 1) # convert from (N,1) to (N,) vector self.__gmm = GMM(n_components=2).fit(train_data) """ Estimate the heart rate given GMM output labels """ def estimate_hr(self, labels, num_samples, fs): peaks = np.diff(labels, prepend=0) == 1 count = sum(peaks) seconds = num_samples / fs hr = count / seconds * 60 # 60s in a minute return hr, peaks """ Uses the GMM model to estimate the heart rate @:param filtered: the filtered data @:param fs: the sampling frequency @:return: the estimated heart rate and estimated time of each peak """ def predict(self): # Grab only the new samples into a NumPy array x = np.array(self.__ppg[-self.__new_samples:]) filtered_arr = self.train_process(x) self.__filtered.add(filtered_arr.tolist()) labels = self.__gmm.predict(np.array(self.__filtered).reshape(-1, 1)) self.__new_samples = 0 hr_est, est_peaks = self.estimate_hr(labels, len(self.__filtered), self.__fs) return hr_est, est_peaks, np.array(self.__filtered) """ Process the new data to update step count """ def process(self): # Grab only the new samples into a NumPy array x = np.array(self.__ppg[-self.__new_samples:]) # Filter the signal (feel free to customize!) x = filt.detrend(x, 25) x = filt.moving_average(x, 5) x = filt.gradient(x) x = filt.normalize(x) # Store the filtered data self.__filtered.add(x.tolist()) # Find the peaks in the filtered data _, peaks = filt.count_peaks(self.__filtered, self.__thresh, 1) peaks = self.remove_outliers(peaks) # Update the step count and reset the new sample count self.__hr = self.compute_heart_rate(peaks) self.__new_samples = 0 # Return the heart rate, peak locations, and filtered data return self.__hr, peaks, np.array(self.__filtered) """ Clear the data buffers and step count """ def reset(self): self.__steps = 0 self.__time.clear() self.__ppg.clear() self.__filtered = np.zeros(self.__num_samples)