引言:
前面我利用树莓派pico+max30102对原始数据进行了提取,并且还实现了对其进行数据处理,计算出了血氧浓度以及心率。那么这篇文章,便是要通过数据可视化,形象的展示脉搏与心跳。并且利用波形图展示完毕后,通过分析与处理对我的健康状态进行一个检测。
硬件环境:
树莓派pico+max30102+OLED。附上pico的引脚图,顺便自己忘了的时候来翻一翻。OLED与max30102都是利用I2C通讯进行驱动。
数据选择:
首先我们要考虑的一个问题便是对于波形图的展示到底是用红光或者红外光的原始数据,还是用处理过后得到的心率????起初我用的是处理过后的心率来表示我的心跳或者说是脉搏,但是其实这是不正确的。他们三者之前确实有关联,但是他们还是有着不同的地方。心率、心跳和脉搏这三个概念在医学上有所区别:
心率:心率是指心脏每分钟跳动的次数,通常以每分钟的脉搏数表示。心率可以通过心电图或脉搏测量来测量。
心跳:心跳是指心脏每次搏动时的动作,即心脏的收缩和舒张。每次心跳产生的脉搏波可以在动脉中感知到,通常用于脉搏测量。
脉搏:脉搏是血液通过动脉时产生的周期性的压力波。通过测量动脉脉搏的频率和规律性,可以了解心跳的情况和心率的大致范围。
也就是说反射回来的红光与红外光即原始数据代表着你的心跳和脉搏的振幅。换一个角度来讲,一般测量仪器比较准确人的身体健康是正常的情况下,我们的心率大多保持在一个范围,基本上变化不大,甚至可能会有一段时间维持在一个稳定的值,那么用波形图展示就是基本趋近于一条平坦的直线。而真是情况下,当一个人的心电图呈现出一条直线的时候通常证明这死亡。所以,要用波形图来模拟心电图的话我们所要选择的数据是红光与红外光的原始数据。
画图部分代码:
#从 machine 模块导入 I2C、Pin 子模块
from machine import SoftI2C,Pin,ADC
from ssd1306 import SSD1306_I2C #从 ssd1306 模块中导入 SSD1306_I2C 子模块
import random
import time
import math
window_size = 100
data = [1100] * window_size
x = list(range(window_size))
#SoftI2C 初始化:scl--> 10, sda --> 11
i2c = SoftI2C(scl=Pin(10), sda=Pin(11))
#OLED 显示屏初始化:128*64 分辨率,OLED 的 I2C 地址是 0x3c
oled = SSD1306_I2C(128, 64, i2c, addr=0x3c)
WIDTH = 128
HEIGHT = 54
def update_data(new_value):
global data
global average
data.append(new_value)
data = data[1:]
def plot_data(hr,spo2,k):
global oled, data, x
oled.fill(0) # 清屏显示黑色背景
# 计算Y轴范围
y_min = min(data)
y_max = max(data)
y_range = y_max - y_min
# 横轴
oled.hline(0,HEIGHT -1 ,WIDTH - 1,1)
#oled.hline(0,(HEIGHT -1)//2 ,WIDTH - 1,1)
# 纵轴
oled.vline(0, 0, HEIGHT - 5, 1)
oled.text('hr:'+str(hr), 0, 55)
#oled.text('spo2:'+str(int(spo2)), 55, 55)
oled.text('K:'+str(int(k)), 55, 55)
#oled.text(str(spo2),100,HEIGHT-1)
# 绘制波形图
for i in range(window_size - 1):
x0 = int(i * (WIDTH - 1) / (window_size - 1))
y0 = int((data[i] - y_min) * (HEIGHT - 1) / y_range)
x1 = int((i + 1) * (WIDTH - 1) / (window_size - 1))
y1 = int((data[i + 1] - y_min) * (HEIGHT - 1) / y_range)
oled.line(x0, HEIGHT - 1 - y0, x1, HEIGHT - 1 - y1, 1)
oled.show()
实现过程:
画线方面可以展示MicroPython自带的库,自己调好参数带入即可。但是对于参数如何调节呢?主要便是要将原始数据映射成为OLED屏幕上面的点,相邻两点连接即可。 映射值到屏幕上的店首要的是确定x坐标与y坐标,对于y坐标可以直接采用原始数据值,因为对于它的y坐标范围是可以自己设定的。对于x坐标,我是采用的下标。因为OLED的屏幕尺寸为128*64所以我建议在备份数组长度的时候最好不好超过128不然波形显示会有很拥挤导致显示不明显。
接下来便是如何以波形图为依据得到心率不齐或者心率没问题的结论。我是根据波峰与波谷之间的斜率,斜率的绝对值的大小通常代表着变化的快慢。也就是说,当我某一个波峰与波谷之间的连线越陡峭也就代表着它由高到低或者由低到高的变化速率快,这也就意味着你的某一次心跳的剧烈程度比较大;如果一个人心率比较正常,那么它的斜率的绝对值的大小一般不会很大,同时斜率的变化范围也不是很大。如果一个人心率不齐,那么它的每一次心跳的幅度,或者脉搏的振幅会有一定的不同,对应到心电图就是波峰与波谷的斜率。
但是,我所选取的样本容量是100个原始数据。也就还说它的波形显示会有很多个波峰与波谷,那么斜率也会有很多个。所以,如何选取一个斜率来代表一个比较准确的值呢?此时就不得不提到一个数学方法——最小二乘法。
最小二乘法是一种常用的数学方法,用于拟合数据或者估计模型参数。它的核心思想是通过最小化观测数据与模型预测值之间的误差的平方和来确定模型参数的值。换句话说也就是,我们会经常用一组数据去估计模型参数,通过一些数据分析或者一些离散的点来最佳拟合一条直线y=kx+b,使得所有的离散点到这条直线的距离和最小,如果大家对这个最小二乘法还是感到很生涩难懂,也可以到网上搜一下其它资料。通过Python的matplotlib和具体的某一次原始数据进行拟合出来的图像如下图所示:
对于该图形的代码也展示给大家可以选择用numpy的库polyfit直接进行拟合也可以选择自己根据最小二乘法的原理去实现。
import numpy as np
import matplotlib.pyplot as plt
# 准备波形数据
x = np.array([i for i in range(100)])
y = np.array([117081, 117120, 117047, 116853, 116744, 116740, 116768, 116804, 116835, 116842, 116829, 116830, 116862, 116871, 116901, 116926, 116937, 116963, 116976, 116987, 116987, 116985, 116789, 115421, 114374, 115521, 117015, 117400, 117497, 117677, 117699, 117693, 117772, 117842, 117893, 117983, 118007, 118036, 118082, 118119, 118154, 118093, 118036, 118060, 118067, 117948, 117710, 117562, 117523, 117566, 117585, 117593, 117579, 117546, 117542, 117551, 117572, 117587, 117611, 117625, 117646, 117662, 117667, 117692, 117717, 117744, 117722, 117522, 117317, 117222, 117199, 117217, 117228, 117209, 117183, 117146, 117147, 117176, 117206, 117232, 117257, 117278, 117293, 117315, 117338, 117376, 117412, 117382, 117157, 116912, 116778, 116744, 116763, 116800, 116809, 116800, 116780, 116811, 116856, 116905])
# 使用最小二乘法进行线性拟合
al = np.polyfit(x, y, 1)# 1 表示线性拟合,即拟合一条直线
k = al[0]
b = al[1]
# 绘制原始数据点和拟合直线
plt.scatter(x, y, label='Data points') # 原始数据点
plt.plot(x, k*x + b, color='red', label='Fitted line') # 拟合直线
plt.xlabel('x')
plt.ylabel('y')
plt.legend()
plt.show()
"""
# 也可以根据原理用数学公式实现,两种方式的结果是一样的。
def least_squares(x, y):
n = len(x)
sum_x = sum(x)
sum_y = sum(y)
sum_xx = 0
sum_xy = 0
for i in range(n):
sum_xx += x[i]*x[i]
sum_xy += x[i]*y[i]
# 计算斜率和截距
slop = (n * sum_xy - sum_x * sum_y) / (n * sum_xx - sum_x**2)
intercept = (sum_y - slop * sum_x) / n
return slop, intercept
"""
结果展示及代码:
该部分代码为主文件代码其它文件的代码可以在之前两篇文章中找到,同时我还加了一个led灯当心率异常时便会发光,你也可以更换成蜂鸣器或者其它外设,我加它主要是为了模拟一个提示或者警报的作用。另外特别说明一下,以上所有的观点与方法都是我自己的思考,如有错误还希望您指出。
from machine import Pin,I2C
import time
import OLED as draw
import data_deal
led = Pin(16,Pin.OUT)
# 定义Max30102传感器的I2C地址
MAX30102_I2C_ADDR = 0x57
# 定义Max30102传感器的寄存器地址
# 中断状态寄存器
REG_INTR_STATUS_1 = 0x00
REG_INTR_STATUS_2 = 0x01
#中断使能寄存器
REG_INTR_ENABLE_1 = 0x02
REG_INTR_ENABLE_2 = 0x03
#FIFO写指针
REG_FIFO_WR_PTR = 0x04
#溢出计数寄存器,记录FIFO寄存器溢出次数
REG_OVF_COUNTER = 0x05
#FIFO读指针寄存器
REG_FIFO_RD_PTR = 0x06
# FIFO数据寄存器
REG_FIFO_DATA = 0x07
# FIFO配置寄存器
REG_FIFO_CONFIG = 0x08
# 模式配置寄存器
REG_MODE_CONFIG = 0x09
#血氧配置寄存器
REG_SPO2_CONFIG = 0x0A
#LED1配置寄存器,调节发光强度
REG_LED1_PA = 0x0C
#LED2配置寄存器
REG_LED2_PA = 0x0D
# 载波幅度调节寄存器
REG_PILOT_PA = 0x10
#多LED配置寄存器
REG_MULTI_LED_CTRL1 = 0x11
REG_MULTI_LED_CTRL2 = 0x12
#温度中断寄存器
REG_TEMP_INTR = 0x1F
# 温度数据寄存器
REG_TEMP_FRAC = 0x20
# 温度配置寄存器
REG_TEMP_CONFIG = 0x21
#近距离中断配置寄存器
REG_PROX_INT_THRESH = 0x30
# 版本表示寄存器
REG_REV_ID = 0xFE
#部件标记寄存器
REG_PART_ID = 0xFF
i2c = I2C(0,sda=Pin(8),scl=Pin(9),freq=400000)
i2c.scan() # 扫描从设备
flag = False
# 初始化Max3002传感器
write_data = ([REG_INTR_ENABLE_1,0xc0],[REG_INTR_STATUS_2,0x00],[REG_FIFO_WR_PTR,0x00],
[REG_OVF_COUNTER,0x00],[REG_FIFO_RD_PTR,0x00],[REG_FIFO_CONFIG,0x4f],
[REG_MODE_CONFIG,0x03],[REG_SPO2_CONFIG,0x27],[REG_LED1_PA,0x024],
[REG_LED2_PA,0x024],[REG_PILOT_PA,0x7f])
def writ_to_register():
# 势能寄存器
i2c.writeto(0x57,bytearray([REG_INTR_ENABLE_1,0xc0]))
i2c.writeto(0x57,bytearray([REG_INTR_STATUS_2,0x00]))
#
i2c.writeto(0x57,bytearray([REG_FIFO_WR_PTR,0x00]))
i2c.writeto(0x57,bytearray([REG_OVF_COUNTER,0x00]))
i2c.writeto(0x57,bytearray([REG_FIFO_RD_PTR,0x00]))
i2c.writeto(0x57,bytearray([REG_FIFO_CONFIG,0x4f]))
i2c.writeto(0x57,bytearray([REG_MODE_CONFIG,0x03]))
i2c.writeto(0x57,bytearray([REG_SPO2_CONFIG,0x27]))
i2c.writeto(0x57,bytearray([REG_LED1_PA,0x024]))
i2c.writeto(0x57,bytearray([REG_LED2_PA,0x024]))
i2c.writeto(0x57,bytearray([REG_PILOT_PA,0x7f]))
# i2c.writeto(0x57,bytearray(write_data))
# 数据存储区
def data_arae():
read_ptr = i2c.readfrom_mem(0x57,REG_FIFO_RD_PTR,1) # 读指针的位置
write_ptr = i2c.readfrom_mem(0x57,REG_FIFO_WR_PTR,1) # 写指针的位置
#read_p_int = int.from_bytes(read_ptr) # 将字节流转为整数,字节序为大端序
#write_p_int = int.from_bytes(write_ptr)
if read_ptr == write_ptr:
return 0
else:
num_samples = int.from_bytes(write_ptr,'big') - int.from_bytes(read_ptr,'big')
# account for pointer wrap around
if num_samples < 0:
num_samples += 32
return num_samples
# 读取数据
def read_fifo(address=0x57):
"""
This function will read the data register.
"""
red_led = None
ir_led = None
# read 1 byte from registers (values are discarded)
reg_INTR1 = i2c.readfrom_mem(address, REG_INTR_STATUS_1, 1)
reg_INTR2 = i2c.readfrom_mem(address, REG_INTR_STATUS_2, 1)
# read 6-byte data from the device
d = i2c.readfrom_mem(address, REG_FIFO_DATA, 6)
# mask MSB [23:18]
red_led = (d[0] << 16 | d[1] << 8 | d[2]) & 0x03FFFF
ir_led = (d[3] << 16 | d[4] << 8 | d[5]) & 0x03FFFF
return red_led, ir_led
# 将数据写进数组长度为100
def buf(amount=100):
"""
This function will read the red-led and ir-led `amount` times.
This works as blocking function.
"""
count = amount
num_bytes = data_arae()
while num_bytes > 0:
red, ir = read_fifo()
red_buf.append(red)
ir_buf.append(ir)
num_bytes -= 1
return red_buf, ir_buf
if __name__ == "__main__":
writ_to_register()
count = 0
r_buf,i_buf = [],[]
x = [i for i in range(100)]
k = 0
hr,spo2 = 0,0
while True:
red, ir = read_fifo()
draw.update_data(ir) # 更新数据
draw.plot_data(hr,spo2,k) # 绘制折线图
r_buf.append(red)
i_buf.append(ir)
count += 1
if count >= 100:
count = 0
hr, hr_valid, spo2, spo2_valid = data_deal.answer(r_buf,i_buf)
k,_ = data_deal.least_squares(x,i_buf)
if hr >100 or hr < 50:
led.low()
else:
led.high()
print(hr,spo2)
r_buf.clear()
i_buf.clear()
结果由于内存限制,仅能上传几秒: