private void timer1_Tick(object sender, EventArgs e) { string line; if (!m_bPaused && m_bInited && m_nStep >= 0 && m_srLog != null) { ++m_nStep; label7.Text = m_nStep.ToString(); int count = 0; int nTempSize = 0; while ((line = m_srLog.ReadLine()) != null) { Match match = Regex.Match(line, @"HR=(\d+), SP=(\d+)"); int time = -1, red = -1, ir = -1, hr = -1, sp = -1; bool found_rawdata = false, found_hrsp = false; if (match.Success && match.Groups.Count == 3) { found_hrsp = true; hr = Convert.ToInt32(match.Groups[1].Value); sp = Convert.ToInt32(match.Groups[2].Value); chart1.Series["HR"].Points.AddXY(m_nLastTimeStamp, hr); chart1.Series["SP"].Points.AddXY(m_nLastTimeStamp, sp); // Original values label4.Text = "HR = " + hr.ToString() + ", SP = " + sp.ToString(); } else { match = Regex.Match(line, @"time=(\d+), red=(\d+), ir=(\d+)"); if (match.Success && match.Groups.Count == 4) { found_rawdata = true; } else { // CSV type match = Regex.Match(line, @"\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)"); if (match.Success && match.Groups.Count == 4) { found_rawdata = true; } } if (found_rawdata) { time = Convert.ToInt32(match.Groups[1].Value); red = Convert.ToInt32(match.Groups[2].Value); ir = Convert.ToInt32(match.Groups[3].Value); if (count < MAX_TEMP_SIZE) { m_temp_red_buffer[count] = red; m_temp_ir_buffer[count] = ir; m_temp_time_buffer[count] = time; nTempSize = count + 1; chart1.Series["Red"].Points.AddXY(time, red); chart1.Series["IR"].Points.AddXY(time, ir); } m_nLastTimeStamp = time; ++count; } } if (found_hrsp || count > 100) { int nTotalSize = m_nBufferSize + nTempSize; if (nTempSize >= Algorithm30102.BUFFER_SIZE) { // Temp is enough int nLeftShift = nTempSize - Algorithm30102.BUFFER_SIZE; for (int i = 0; i < Algorithm30102.BUFFER_SIZE; ++i) { m_red_buffer[i] = m_temp_red_buffer[i + nLeftShift]; m_ir_buffer[i] = m_temp_ir_buffer[i + nLeftShift]; m_time_buffer[i] = m_temp_time_buffer[i + nLeftShift]; } m_nBufferSize = Algorithm30102.BUFFER_SIZE; } else if (nTotalSize > Algorithm30102.BUFFER_SIZE) { // Need to left-shift the most recent data on the system buffer int nKeep = Algorithm30102.BUFFER_SIZE - nTempSize; int nLeftShift = m_nBufferSize - nKeep; for (int i = 0; i < nKeep; ++i) { m_red_buffer[i] = m_red_buffer[i + nLeftShift]; m_ir_buffer[i] = m_ir_buffer[i + nLeftShift]; m_time_buffer[i] = m_time_buffer[i + nLeftShift]; } // Then copy the whole temp to fill the system buffer for (int i = nKeep; i < Algorithm30102.BUFFER_SIZE; ++i) { m_red_buffer[i] = m_temp_red_buffer[i - nKeep]; m_ir_buffer[i] = m_temp_ir_buffer[i - nKeep]; m_time_buffer[i] = m_temp_time_buffer[i - nKeep]; } m_nBufferSize = Algorithm30102.BUFFER_SIZE; } else { // Just add the temp to the system buffer for (int i = m_nBufferSize; i < nTotalSize; ++i) { m_red_buffer[i] = m_temp_red_buffer[i - m_nBufferSize]; m_ir_buffer[i] = m_temp_ir_buffer[i - m_nBufferSize]; m_time_buffer[i] = m_temp_time_buffer[i - m_nBufferSize]; } m_nBufferSize = nTotalSize; } int window = Convert.ToInt32(textBox1.Text); if (window < 100) { window = 100; } else if (window > Algorithm30102.BUFFER_SIZE) { window = Algorithm30102.BUFFER_SIZE; } textBox1.Text = window.ToString(); if (m_nBufferSize >= window) { Debug.Assert(m_nBufferSize <= Algorithm30102.BUFFER_SIZE); int nNewHR = -1, nNewSP = -1; bool bHRValid, bSPValid; try { int nStart = m_nBufferSize - window; int nSelected = comboBox1.SelectedIndex, sr = 100; if (nSelected == 0) { sr = 50; } else if (nSelected == 1) { sr = 100; } else if (nSelected == 2) { sr = 200; } RData rdata = new RData(); m_alg.maxim_heart_rate_and_oxygen_saturation(sr, m_ir_buffer.Skip(nStart), window, m_red_buffer.Skip(nStart), out nNewSP, out bSPValid, out nNewHR, out bHRValid, rdata); for (int i = 0; i < rdata.m_size; ++i) { chart1.Series["AD for Red"].Points.AddXY(m_time_buffer[nStart + rdata.m_red_indices[i]], rdata.m_red_ratios[i]); chart1.Series["AD for IR"].Points.AddXY(m_time_buffer[nStart + rdata.m_ir_indices[i]], rdata.m_ir_ratios[i]); } } catch { bHRValid = bSPValid = false; } if (!bHRValid) { nNewHR = -1; } if (!bSPValid) { nNewSP = -1; } if (nNewHR > 20 && nNewHR <= 200) { if (m_hr_filter.AddPoint((double)nNewHR)) { m_bHasHRData = true; } else { nNewHR = -1; } } else { nNewHR = -1; } if (m_bHasHRData) { nNewHR = (int)Math.Round(m_hr_filter.GetValue()); } if (nNewSP > 50 && nNewSP <= 100) { if (m_sp_filter.AddPoint((double)nNewSP)) { m_bHasSPData = true; } else { nNewSP = -1; } } else { nNewSP = -1; } if (m_bHasSPData) { nNewSP = (int)Math.Round(m_sp_filter.GetValue()); } label3.Text = "HR = " + nNewHR.ToString() + ", SP = " + nNewSP.ToString(); if (nNewHR > 0) { chart1.Series["newHR"].Points.AddXY(m_nLastTimeStamp, nNewHR); } if (nNewSP > 0) { chart1.Series["newSP"].Points.AddXY(m_nLastTimeStamp, nNewSP); } } else { label3.Text = "Not enough raw data"; } break; } } if (line == null) // EOF { button2.Enabled = true; button3.Enabled = false; button4.Enabled = false; m_srLog.Close(); m_srLog = null; m_nStep = -1; timer1.Stop(); } } }
int[] an_y = new int [BUFFER_SIZE]; //red /** * \brief Calculate the heart rate and SpO2 level * \par Details * By detecting peaks of PPG cycle and corresponding AC/DC of red/infra-red signal, the ratio for the SPO2 is computed. * Since this algorithm is aiming for Arm M0/M3. formaula for SPO2 did not achieve the accuracy due to register overflow. * Thus, accurate SPO2 is precalculated and save longo uch_spo2_table[] per each ratio. * * \param[in] sr - Sample rate * \param[in] *pun_ir_buffer - IR sensor data buffer * \param[in] n_ir_buffer_length - IR sensor data buffer length * \param[in] *pun_red_buffer - Red sensor data buffer * \param[out] *pn_spo2 - Calculated SpO2 value * \param[out] *pch_spo2_valid - 1 if the calculated SpO2 value is valid * \param[out] *pn_heart_rate - Calculated heart rate value * \param[out] *pch_hr_valid - 1 if the calculated heart rate value is valid * * \retval None */ public void maxim_heart_rate_and_oxygen_saturation(int sr, IEnumerable <int> pun_ir_buffer_IEnum, int n_ir_buffer_length, IEnumerable <int> pun_red_buffer_IEnum, out int pn_spo2, out bool pch_spo2_valid, out int pn_heart_rate, out bool pch_hr_valid, RData rdata) { int[] pun_ir_buffer = pun_ir_buffer_IEnum.ToArray(); int[] pun_red_buffer = pun_red_buffer_IEnum.ToArray(); int un_ir_mean, un_only_once; int k, n_i_ratio_count; int i, s, m, n_exact_ir_valley_locs_count, n_middle_idx; int n_th1, n_npks, n_c_min; int[] an_ir_valley_locs = new int[15]; int[] an_exact_ir_valley_locs = new int[15]; int[] an_dx_peak_locs = new int[15]; int n_peak_interval_sum; int n_y_ac, n_x_ac; int n_spo2_calc; int n_y_dc_max, n_x_dc_max; int n_y_dc_max_idx = -1, n_x_dc_max_idx = -1; int[] an_ratio = new int[5]; int n_ratio_average, n_nume, n_denom; Debug.Assert(pun_red_buffer.Length <= pun_ir_buffer.Length); Debug.Assert(n_ir_buffer_length <= pun_ir_buffer.Length); // Remove DC of ir signal un_ir_mean = 0; for (k = 0; k < n_ir_buffer_length; k++) { un_ir_mean += pun_ir_buffer[k]; } un_ir_mean = un_ir_mean / n_ir_buffer_length; for (k = 0; k < n_ir_buffer_length; k++) { an_x[k] = pun_ir_buffer[k] - un_ir_mean; } // 4 pt Moving Average for (k = 0; k < n_ir_buffer_length - MA4_SIZE; k++) { n_denom = (an_x[k] + an_x[k + 1] + an_x[k + 2] + an_x[k + 3]); an_x[k] = n_denom / (int)4; } // Get difference of smoothed IR signal for (k = 0; k < n_ir_buffer_length - MA4_SIZE - 1; k++) { an_dx[k] = (an_x[k + 1] - an_x[k]); } // 2-pt Moving Average to an_dx for (k = 0; k < n_ir_buffer_length - MA4_SIZE - 2; k++) { an_dx[k] = (an_dx[k] + an_dx[k + 1]) / 2; } // Hamming window: flip wave form so that we can detect valley with peak detector for (i = 0; i < n_ir_buffer_length - HAMMING_SIZE - MA4_SIZE - 2; i++) { s = 0; for (k = i; k < i + HAMMING_SIZE; k++) { s -= an_dx[k] * auw_hamm[k - i]; } an_dx[i] = s / (int)1146; // divide by sum of auw_hamm } n_th1 = 0; // threshold calculation for (k = 0; k < n_ir_buffer_length - HAMMING_SIZE; k++) { n_th1 += ((an_dx[k] > 0) ? an_dx[k] : ((int)0 - an_dx[k])); } n_th1 = n_th1 / (n_ir_buffer_length - HAMMING_SIZE); // Peak location is acutally index for sharpest location of raw signal since we flipped the signal maxim_find_peaks(an_dx_peak_locs, out n_npks, an_dx, n_ir_buffer_length - HAMMING_SIZE, n_th1, 8, 5);//peak_height, peak_distance, max_num_peaks n_peak_interval_sum = 0; if (n_npks >= 2) { for (k = 1; k < n_npks; k++) { n_peak_interval_sum += (an_dx_peak_locs[k] - an_dx_peak_locs[k - 1]); } n_peak_interval_sum = n_peak_interval_sum / (n_npks - 1); // Each data point represent 1/sr second in time so peak internal in seconds is // n_peak_interval_sum * (1 / sr). // The heart rate is 60 / peak interval = 60 * sr / n_peak_interal_sum pn_heart_rate = (int)(60 * sr / n_peak_interval_sum); // Beats per minutes pch_hr_valid = true; } else { pn_heart_rate = -999; pch_hr_valid = false; } for (k = 0; k < n_npks; k++) { an_ir_valley_locs[k] = an_dx_peak_locs[k] + HAMMING_SIZE / 2; } // Raw value : RED(=y) and IR(=X) // We need to assess DC and AC value of ir and red PPG. for (k = 0; k < n_ir_buffer_length; k++) { an_x[k] = pun_ir_buffer[k]; an_y[k] = pun_red_buffer[k]; } // Find precise min near an_ir_valley_locs n_exact_ir_valley_locs_count = 0; for (k = 0; k < n_npks; k++) { un_only_once = 1; m = an_ir_valley_locs[k]; n_c_min = 16777216; //2^24; if (m + 5 < n_ir_buffer_length - HAMMING_SIZE && m - 5 > 0) { for (i = m - 5; i < m + 5; i++) { if (an_x[i] < n_c_min) { if (un_only_once > 0) { un_only_once = 0; } n_c_min = an_x[i]; an_exact_ir_valley_locs[k] = i; } } if (un_only_once == 0) { n_exact_ir_valley_locs_count++; } } } if (n_exact_ir_valley_locs_count < 2) { pn_spo2 = -999; // do not use SPO2 since signal ratio is out of range pch_spo2_valid = false; return; } // 4 pt MA for (k = 0; k < n_ir_buffer_length - MA4_SIZE; k++) { an_x[k] = (an_x[k] + an_x[k + 1] + an_x[k + 2] + an_x[k + 3]) / (int)4; an_y[k] = (an_y[k] + an_y[k + 1] + an_y[k + 2] + an_y[k + 3]) / (int)4; } // Using an_exact_ir_valley_locs , find ir-red DC andir-red AC for SPO2 calibration ratio // Finding AC/DC maximum of raw ir * red between two valley locations n_ratio_average = 0; n_i_ratio_count = 0; for (k = 0; k < 5; k++) { an_ratio[k] = 0; } for (k = 0; k < n_exact_ir_valley_locs_count; k++) { if (an_exact_ir_valley_locs[k] > n_ir_buffer_length) { pn_spo2 = -999; // do not use SPO2 since valley loc is out of range pch_spo2_valid = false; return; } } // Find max between two valley locations // and use ratio betwen AC compoent of Ir & Red and DC compoent of Ir & Red for SPO2 for (k = 0; k < n_exact_ir_valley_locs_count - 1; k++) { n_y_dc_max = -16777216; n_x_dc_max = -16777216; if (an_exact_ir_valley_locs[k + 1] - an_exact_ir_valley_locs[k] > 10) { for (i = an_exact_ir_valley_locs[k]; i < an_exact_ir_valley_locs[k + 1]; i++) { if (an_x[i] > n_x_dc_max) { n_x_dc_max = an_x[i]; n_x_dc_max_idx = i; } if (an_y[i] > n_y_dc_max) { n_y_dc_max = an_y[i]; n_y_dc_max_idx = i; } } n_y_ac = (an_y[an_exact_ir_valley_locs[k + 1]] - an_y[an_exact_ir_valley_locs[k]]) * (n_y_dc_max_idx - an_exact_ir_valley_locs[k]); //red n_y_ac = an_y[an_exact_ir_valley_locs[k]] + n_y_ac / (an_exact_ir_valley_locs[k + 1] - an_exact_ir_valley_locs[k]); n_y_ac = an_y[n_y_dc_max_idx] - n_y_ac; // subracting linear DC compoenents from raw n_x_ac = (an_x[an_exact_ir_valley_locs[k + 1]] - an_x[an_exact_ir_valley_locs[k]]) * (n_x_dc_max_idx - an_exact_ir_valley_locs[k]); // ir n_x_ac = an_x[an_exact_ir_valley_locs[k]] + n_x_ac / (an_exact_ir_valley_locs[k + 1] - an_exact_ir_valley_locs[k]); n_x_ac = an_x[n_y_dc_max_idx] - n_x_ac; // subracting linear DC compoenents from raw n_nume = (n_y_ac * n_x_dc_max) >> 7; //prepare X100 to preserve floating value n_denom = (n_x_ac * n_y_dc_max) >> 7; if (n_denom > 0 && n_i_ratio_count < 5 && n_nume != 0) { rdata.m_red_ratios[rdata.m_size] = 10000 * n_y_ac / n_y_dc_max; rdata.m_red_indices[rdata.m_size] = n_y_dc_max_idx; rdata.m_ir_ratios[rdata.m_size] = 10000 * n_x_ac / n_x_dc_max; rdata.m_ir_indices[rdata.m_size] = n_x_dc_max_idx; ++rdata.m_size; an_ratio[n_i_ratio_count] = (n_nume * 100) / n_denom; //formular is ( n_y_ac *n_x_dc_max) / ( n_x_ac *n_y_dc_max) ; n_i_ratio_count++; } } } maxim_sort_ascend(an_ratio, n_i_ratio_count); n_middle_idx = n_i_ratio_count / 2; if (n_middle_idx > 1) { n_ratio_average = (an_ratio[n_middle_idx - 1] + an_ratio[n_middle_idx]) / 2; // use median } else { n_ratio_average = an_ratio[n_middle_idx]; } if (n_ratio_average > 2 && n_ratio_average < 184) { n_spo2_calc = uch_spo2_table[n_ratio_average]; pn_spo2 = n_spo2_calc; pch_spo2_valid = true;// float_SPO2 = -45.060*n_ratio_average* n_ratio_average/10000 + 30.354 *n_ratio_average/100 + 94.845 ; // for comparison with table } else { pn_spo2 = -999; // do not use SPO2 since signal ratio is out of range pch_spo2_valid = false; } }