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
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("Ready to collect data? 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
 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 enough time has elapsed, process the data and plot it
             current_time = time()
             if (current_time - previous_time > process_time):
                 previous_time = current_time
                 try:
                     heart_rate, peaks, filtered = hr.process()
                 except:
                     continue
                 print("Heart Rate: " + str(heart_rate))
                 plt.cla()
                 plt.plot(t_plot, filtered)
                 plt.plot(t_plot[peaks], filtered[peaks], 'rx')
                 plt.plot(t_plot, [0.6]*len(filtered), 'b--')
                 # If the heart is too large, then it should be displayed as an inaccurate reading
    comms = Communication("/dev/cu.AkshayBluetooth-ESP32SPP", 115200)
    comms.clear()  # just in case any junk is in the pipes
    comms.send_message("wearable")  # begin sending data
    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
                    print("bad")
                    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))

                if transform_method == l2_norm_calculation or transform_method == l1_norm_calculation:
                    transform_x.add(transform_method(int(m1), int(m2),
                                                     int(m3)))
                elif transform_method != sample_difference:
                    transform_x.add(transform_method(ax))
                    transform_y.add(transform_method(ay))
                    transform_z.add(transform_method(az))
                else:
                    transform_x = sample_difference(ax)
                    transform_y = sample_difference(ay)
                    transform_z = sample_difference(az)
Пример #5
0
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
Пример #6
0
    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)
                t_arr = np.array(t)

                # if enough time has elapsed, process the data and plot it
                current_time = time()
                if current_time - previous_time > process_time:
                    previous_time = current_time
                    try:
                        # with the processed data make a prediction using GMM model
                        hr_est, est_peaks, filtered = hr.predict()
                        peaks.add(list(est_peaks))
                    except:
                        continue
                    # If enough time has passed for clean data
                    if initial_period <= 0:
                        # If the heart is too large, then it should be displayed as an inaccurate reading
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)
Пример #8
0
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)
  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


        # add the new values to the circular lists
        times.add(int(m1))
        ax.add(int(m2))
        ay.add(int(m3))
        az.add(int(m4))


        # if enough time has elapsed, clear the axis, and plot az
        current_time = time()
        if (current_time - previous_time > refresh_time):
            previous_time = current_time
            ax1.cla()
            ax2.cla()
            ax3.cla()
            ax1.plot(ax)
            ax2.plot(ay)
            ax3.plot(az)
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)