โปรเจกต์ จาก KY-039 สู่ Heart Rate
รับค่า Heart Rate ของคุณ ไม่ใช่เพียงแค่การอ่านค่าจาก IR Sensor
รับค่า Heart Rate ของคุณ ไม่ใช่เพียงแค่การอ่านค่าจาก IR Sensor
▶ กดเพื่อดูวิดีโอสาธิตโปรเจกต์
Project Supporter Team
โพสต์โดย
ในชุดเซนเซอร์ 37 ชนิดสำหรับ Arduino, มี heartbeat sensor รวมอยู่ด้วย แต่ชื่อของมันอาจจะดูเกินจริงไปหน่อย เพราะคนส่วนใหญ่มักจะคิดว่ามันจะให้ค่าเป็นตัวเลข Digital ผ่าน I2C หรืออะไรที่คล้ายกัน ซึ่งเป็นตัวเลขอัตราการเต้นของหัวใจโดยตรง แต่สิ่งที่ Sensor ตัวนี้ให้มาจริงๆ เป็นเพียงค่า "Analog" ตั้งแต่ 0 ถึง 1023 ซึ่งบอกว่าตัวรับแสงได้รับแสง Infra red มากน้อยเพียงใด หรือพูดง่ายๆ คือ มีอะไรมาบดบังตัวรับแสงมากแค่ไหน ยิ่งค่าสูงขึ้น แสดงว่าได้รับแสง Infra red น้อยลง
สรุปสั้นๆ คือ: ให้วางนิ้วของคุณไว้ระหว่าง IR LED และ Light transistor ของ Sensor เมื่อหัวใจเต้น หลอดเลือดในนิ้วจะขยายตัวและไปกรองแสง IR ทำให้เกิด pulsating signal หรือสัญญาณที่มีลักษณะเป็นจังหวะตุบๆ
ในโปรเจกต์นี้ ผมจะอธิบายวิธีการแปลงสัญญาณดังกล่าวให้เป็นอัตราการเต้นของหัวใจ เช่น 66 BPM (Beats per minute)
หากคุณแค่อ่านค่าและ Plot ค่าจาก Sensor KY-039 คุณจะได้ผลลัพธ์ประมาณนี้:

ค่าที่ได้จะเป็นเลข Integer ซึ่งไม่ค่อยแม่นยำนัก ให้ลองคำนวณหาค่าเฉลี่ยจากกลุ่มข้อมูลหลายๆ ค่าแทน แล้วลอง Plot ค่าเฉลี่ยดู คุณจะได้แบบนี้:

จากตรงนี้คุณจะเริ่มเห็นจังหวะชีพจรของหัวใจแล้ว ให้หาผลต่างของเวลาระหว่างช่วงที่สัญญาณพุ่งสูงขึ้นอย่างชัดเจน จากจุดนั้นคุณจะสามารถคำนวณอัตราการเต้นของหัวใจเป็น BPM ได้
(รูปแบบซิกแซกเล็กๆ ในภาพด้านบนเกิดจากแสงประดิษฐ์หรือหลอดไฟที่มีความถี่ 50 Hz ซึ่งเป็นอีกเรื่องหนึ่งที่เราต้องจัดการ)
นี่คือ Code ง่ายๆ สำหรับแสดงผลค่าที่คุณอ่านได้จาก Sensor KY-039:
// Pulse Monitor Test Script
int sensorPin = 0;
void setup() {
Serial.begin(9600);
}
void loop ()
{
while(1)
{
Serial.print(analogRead(sensorPin));
Serial.print('\
');
}
}
สิ่งที่คุณจะได้อาจจะออกมาหน้าตาประมาณนี้:

เนื่องจากนี่คือภาพจากหน้าต่าง Serial monitor ที่อ่านค่า Serial output จาก Arduino ของคุณที่ 9600 Baud กระบวนการทั้งหมดจึงถูกกำหนดจังหวะด้วยฟังก์ชัน Serial.print() ซึ่งจะเป็นตัวกำหนดอัตราการอ่านและ Plot ค่า อย่างไรก็ตาม เส้นกราฟที่ได้จะมีความขรุขระมาก เพราะค่าแกว่งอยู่ระหว่าง 360 ถึง 383 และมีแต่ค่า Integer เท่านั้น
เพื่อให้ได้ผลลัพธ์ที่เรียบเนียนขึ้น ให้หาค่าเฉลี่ยจากการอ่านค่า 20 ครั้งล่าสุดจาก Sensor นี่คือวิธีที่ผมทำ โดยเริ่มจากการกำหนดค่าคงที่เพื่อระบุจำนวนครั้งที่ต้องการอ่าน:
#define samp_siz 20
จากนั้นผมจะมี Array เพื่อเก็บจำนวนค่าที่อ่านมา:
int reads[samp_siz];
ในการอ่านค่าใหม่แต่ละครั้ง ผมจะหักค่าที่เก่าที่สุดออกจากผลรวมและบวกค่าใหม่ล่าสุดเข้าไป ใน Array ผมจะแทนที่ค่าที่เก่าที่สุดด้วยค่าใหม่ล่าสุด
reader = analogRead (sensorPin); // read the sensor
sum -= reads[ptr]; // subtract the oldest reading from sum
sum += reader; // add the newest reading to the sum
reads[ptr] = reader; // save the newest reading in the array
last = float(sum) / samp_siz; // calculate the average now
ptr++; // update the index for the array, have
ptr %= samp_siz; // it restart at 0 when needed
ด้วยขนาด Array 20 และ Baud rate ที่ 9600 ใน Serial monitor ผมอาจจะได้กราฟดังนี้:

ตรงนี้คุณจะเห็นการเต้นของหัวใจจริงๆ เป็นเส้นโค้งที่พุ่งขึ้นอย่างรวดเร็ว แต่คุณก็จะเห็นรูปแบบซิกแซกเล็กๆ ด้วย ซึ่งซิกแซกที่เล็กกว่านั้นมาจากไฟในห้องครัวของผม ซึ่งใช้หลอดไฟ LED สามดวง ไฟบ้านของผมคือ 240 V, 50 Hz AC ดังนั้น 50 ครั้งต่อวินาทีจะมีความเข้มของแสงเพิ่มขึ้น ซึ่งเห็นได้ชัดว่ารวมถึงในย่าน IR ด้วย ผมต้องการกำจัดสัญญาณรบกวน 50 Hz นั้นออกไป ซึ่งน่าจะทำได้โดยการอ่านค่าจาก Sensor ในช่วงเวลา 20 ms แล้วหาค่าเฉลี่ยของค่าทั้งหมด มาดูกัน...
n = 0;
start = millis();
reader = 0.;
do
{
reader += analogRead (sensorPin); // read and add values...
n++;
now = millis();
}
while (now < start + 20); // ...until 20 ms have elapsed
reader /= n; // and take an average of the values
ด้วย Code ส่วนนี้ ผมจะอ่านค่าจาก Sensor เป็นช่วงๆ ละ 20 ms ซึ่งจะช่วยลดการกะพริบ 50 Hz ที่เกิดจากแสงประดิษฐ์ หากคุณอยู่ในประเทศที่ใช้ไฟ 60 Hz ให้ใช้ช่วงเวลา 16.67 ms แทน หรือ 16667µs
เนื่องจากผมจัดการทำให้กราฟเรียบเนียนในช่วง 20 ms ไปแล้ว จริงๆ ผมก็ไม่จำเป็นต้องใช้ Array ที่เคยใช้ก่อนหน้านี้ แต่ในเมื่อมีมันอยู่แล้วและปรับขนาดได้ง่าย ผมเลยปล่อยมันไว้ และการใช้ Array ขนาด 5 ดูเหมือนจะช่วยกำจัดสัญญาณรบกวนสุดท้ายที่น่ารำคาญออกไปได้ นี่คือผลลัพธ์ที่ได้ตอนนี้:

สิ่งสุดท้ายที่ผมต้องทำคือการตรวจจับส่วนใดส่วนหนึ่งของรูปแบบที่ซ้ำกัน เนื่องจากช่วงขาขึ้น (Rising slope) มีความสม่ำเสมอมากกว่า ผมจึงเลือกใช้ส่วนนี้ สังเกตว่าค่าในแกน Y ของแต่ละกราฟจะแตกต่างกันมาก ผมจึงไม่สามารถพึ่งพาค่าสัมบูรณ์ได้เลย ผมทำได้เพียงพึ่งพาการขึ้นและลงของเส้นกราฟเท่านั้น นักคณิตศาสตร์อาจจะพูดถึงเรื่องของ Derivative แต่สำหรับผม ผมพอใจแล้วถ้าเจอค่าที่เพิ่มขึ้นติดต่อกัน n ครั้ง โดยที่ n สามารถเป็นค่าที่ปรับเปลี่ยนได้ตามความเหมาะสม ผมเริ่มที่ 5 ซึ่งผมได้กำหนดค่าคงที่ rise_threshold ไว้ใน Code เมื่อผมพบค่าที่เพิ่มขึ้นติดต่อกัน 5 ครั้ง ผมจะรู้ว่าผมอยู่ที่จุดต่ำสุดของกราฟที่กำลังจะพุ่งขึ้น ผมจะบันทึกเวลาไว้ จากนั้นรอให้กราฟตกลงมา แล้วรอการเพิ่มขึ้น 5 ครั้งถัดไปเพื่อบันทึกเวลาอีกครั้ง แล้วจึงแสดงค่า BPM ที่คำนวณได้
ผมได้ทำการทดสอบและนับว่ามีค่าที่เพิ่มขึ้นติดต่อกันกี่ครั้งในกราฟ และพบว่ามีอยู่ระหว่าง 10 ถึง 15 ครั้ง ดังนั้นถ้าผมนับถึง 5 ผมก็ค่อนข้างมั่นใจได้ว่าพบจุดเริ่มต้นของการเต้นของหัวใจแล้ว
เนื่องจากผมจะสั่งพิมพ์ค่าหลังจากหัวใจเต้นแต่ละครั้งเท่านั้น จึงไม่มีการพิมพ์บ่อยเกินไป ทำให้มีเวลาเหลือมากขึ้นสำหรับการอ่านค่าจาก Sensor ผมอาจจะเจอ High frequent noise มากขึ้น ซึ่งผมจะไม่เห็นเพราะไม่ได้เปิด Plotter ไว้ มาดูกันว่ามันทำงานอย่างไร
#define samp_siz 4
#define rise_threshold 5
// Pulse Monitor Test Script
int sensorPin = 0;
void setup() {
Serial.begin(9600);
}
void loop ()
{
float reads[samp_siz], sum;
long int now, ptr;
float last, reader, start;
float first, second, third, before, print_value;
bool rising;
int rise_count;
int n;
long int last_beat;
for (int i = 0; i < samp_siz; i++)
reads[i] = 0;
sum = 0;
ptr = 0;
while(1)
{
// calculate an average of the sensor
// during a 20 ms period (this will eliminate
// the 50 Hz noise caused by electric light
n = 0;
start = millis();
reader = 0.;
do
{
reader += analogRead (sensorPin);
n++;
now = millis();
}
while (now < start + 20);
reader /= n; // we got an average
// Add the newest measurement to an array
// and subtract the oldest measurement from the array
// to maintain a sum of last measurements
sum -= reads[ptr];
sum += reader;
reads[ptr] = reader;
last = sum / samp_siz;
// now last holds the average of the values in the array
// check for a rising curve (= a heart beat)
if (last > before)
{
rise_count++;
if (!rising && rise_count > rise_threshold)
{
// Ok, we have detected a rising curve, which implies a heartbeat.
// Record the time since last beat, keep track of the two previous
// times (first, second, third) to get a weighed average.
// The rising flag prevents us from detecting the same rise
// more than once.
rising = true;
first = millis() - last_beat;
last_beat = millis();
// Calculate the weighed average of heartbeat rate
// according to the three last beats
print_value = 60000. / (0.4 * first + 0.3 * second + 0.3 * third);
Serial.print(print_value);
Serial.print('\
');
third = second;
second = first;
}
}
else
{
// Ok, the curve is falling
rising = false;
rise_count = 0;
}
before = last;
ptr++;
ptr %= samp_siz;
}
}
มันทำงานได้ดีทีเดียว นี่คือวิดีโอครับ
สังเกตว่าไฟ LED RX ขนาดเล็กบน Arduino จะกะพริบตามจังหวะหัวใจของผม นั่นเป็นเพราะเมื่อมีการเต้นของหัวใจ อัตราจะถูกคำนวณและแสดงผลไปยัง Serial ซึ่งทำให้ไฟ LED กะพริบ หากนิ้วขยับเล็กน้อย อาจทำให้ค่าที่อ่านได้ผิดพลาด
ในตอนนี้ อัตราการเต้นที่แสดงผลจะถูกคำนวณจากจังหวะการเต้น 3 ครั้งล่าสุด แต่จริงๆ แล้วอาจจะเหมาะสมกว่าหากคำนวณจากช่วงเวลา เช่น 15 วินาที ผมอาจจะเก็บค่าอัตราการเต้นที่ต่อเนื่องกัน 15 ค่า หาค่าเฉลี่ย จากนั้นตัดค่า 5 ค่าที่ห่างจากค่าเฉลี่ยมากที่สุดออก แล้วคำนวณหาค่าเฉลี่ยใหม่อีกครั้ง วิธีนี้จะช่วยให้อัตราการเต้นของหัวใจที่วัดได้มีความน่าเชื่อถือและคงที่มากขึ้น
ผมเพิ่งทดสอบ Sensor นี้กับภรรยาและตัวผมเองเท่านั้น แต่ละขั้นตอนในการปรับปรุงสัญญาณผมทำขึ้นตามข้อมูลที่อ่านได้ก่อนหน้า คนอื่นอาจจะมีจังหวะการเต้นของหัวใจแบบอื่นที่ทำให้รูปร่างของกราฟแตกต่างออกไป ซึ่งอาจต้องใช้วิธีการตรวจจับจังหวะแบบอื่น บางทีช่วงขาลงอาจจะตรวจจับได้ง่ายกว่า หรืออาจจะเป็นจุดสูงสุด และจะเกิดอะไรขึ้นถ้าชีพจรเต้นเร็วถึง 180 - 200 BPM? การตรวจจับช่วงขาขึ้นอาจจะทำได้ยากกว่าเดิม
สนับสนุนเพื่อรับ Source Code หรือแอปพลิเคชันสำหรับโปรเจกต์นี้
ประเมิน Project
เอาฟอร์มยาวออกจากท้ายหน้า Project แล้ว เหลือเป็นปุ่มให้กดไปกรอกหน้าเดียว ตัวใหญ่ เว้นบรรทัดเยอะ อ่านง่ายกว่า
รีวิวจากคนใช้งานจริง
ถ้าเคยสั่งงาน เคยอ่านหน้านี้แล้วได้ประโยชน์ หรือมีข้อเสนอแนะ ฝากรีวิวไว้ได้เลย
ยังไม่มีรีวิวบนหน้านี้ ถ้าเคยใช้งานหรือมีข้อเสนอแนะ เขียนเป็นคนแรกได้เลย