程序项目代做,有需求私信(小程序、网站、爬虫、电路板设计、驱动、应用程序开发、毕设疑难问题处理等)

MIT-BIT检测QRS,P,T 波以及分类识别

一、课程设计要求

1.1 预处理

  • 降噪:对信号进行降噪处理,可能需要使用滤波器来滤掉低频噪声;
  • 滤波:使用滤波器对信号进行处理,以去除不需要的频率成分,保留干净的信号。

1.2 心率识别

识别心率:通过算法识别心率,可能需要使用阈值识别、斜率差分或其他方法来定位心率的峰值。

1.3 QRS波群定位

识别QRS波群的位置:QRS波群是心电图中的一个重要特征,可以通过特定的算法来定位其位置。

1.4 P波和T波定位

识别P波和T波的位置:这些也是心电图中的关键波形,需要通过相应的算法进行识别和定位。

1.5 用户界面设计

设计交互界面:创建一个用户友好的界面,允许用户导入心电信号,进行滤波、心率识别等操作,并能够实时显示结果。

1.6 异常心电波形识别

识别异常波形:如心率过快、心率过慢、周期不稳定等,并进行报警。

1.7 信号导入和实时呈现

确保信号能够实时导入程序,并正确呈现,这可能涉及到设置合适的采样率。

1.8 代码注释和文档

编写清晰的代码注释,以便于理解和debug

1.9 结题报告撰写

准备结题报告,总结所做的工作,包括目标、方法、过程、结果、讨论以及遇到的问题和解决方案。

二、ECG概述

2.1 ECG介绍

ECG英文全称Electrogastrogram,即心电图,指的是心脏在每个心动周期中,由起搏点、心房、心室相继兴奋,伴随着心电图生物电的变化,通过心电描记器从体表引出多种形式的电位变化的图形。

心电图是心脏兴奋的发生、传播及恢复过程的客观指标。用于对各种心律失常、心室心房肥大、心肌梗死、心律失常、心肌缺血等病症检查。

心电图是由一系列的波组所构成,每个波组代表着每一个心动周期(即,一个完整的心拍)。一个波组包括P波、QRS波群、T波及U波。看心电图首先要了解每个波所代表的意义。

2.1.1 P

心脏的兴奋发源于窦房结,最先传至心房,故心电图各波中最先出现的是代表左右两心房兴奋过程的P波。兴奋在向两心房传播过程中,其心电去极化的综合向量先指向左下肢,然后逐渐转向左上肢。如将各瞬间心房去极的综合向量连结起来,便形成一个代表心房去极的空间向量环,简称P环。

P环在各导联轴上的投影即得出各导联上不同的P波。P波形小而圆钝,随各导联而稍有不同。P波的宽度一般不超过0.11秒,电压(高度)不超过0.25毫伏。

2.1.2 P-R

是从P波终点到QRS波起点之间的曲线,通常与基线同一水平。P-R段由电活动经房室交界传向心室所产生的电位变化极弱,在体表难于记录出。 

2.1.3 P-R间期

是从P波起点到QRS波群起点的时间距离,代表心房开始兴奋到心室开始兴奋所需的时间,一般成人约为0.12~0.20秒,小儿稍短。超过0.21秒为房室传导时间延长。  

2.1.4 QRS复合波

代表两个心室兴奋传播过程的电位变化。由窦房结发生的兴奋波经传导系统首先到达室间隔的左侧面,以后按一定路线和方向,并由内层向外层依次传播。随着心室各部位先后去极化形成多个瞬间综合心电向量,在额面的导联轴上的投影,便是心电图肢体导联的QRS复合波。

典型的QRS复合波包括三个相连的波动。第一个向下的波为Q波,继Q波后一个狭高向上的波为R波,与R波相连接的又一个向下的波为S波。由于这三个波紧密相连且总时间不超过0.10秒,故合称QRS复合波。QRS复合波所占时间代表心室肌兴奋传播所需时间,正常人在0.06~0.10秒间。  

2.1.5 S-T

QRS波群结束到T波开始的平线,反映心室各部均在兴奋而各部处于去极化状态,故无电位差。

正常时接近于等电位线,向下偏移不应超过0.05毫伏,向上偏移在肢体导联不超过0.1毫伏,在单极心前导程中V1,V2,V3中可达0.2~0.3毫伏;V4,V5导联中很少高于0.1毫伏。任何正常心前导联中,ST段下降不应低于0.05毫伏。偏高或降低超出上述范围,便属异常心电图。  

2.1.6 T

是继QRS波群后的一个波幅较低而波宽较长的电波,反映心室兴奋后再极化过程。

心室再极化的顺序与去极化过程相反,它缓慢地从外层向内层进行,在外层已去极化部分的负电位首先恢复到静息时的正电位,使外层为正,内层为负,因此与去极化时向量的方向基本相同。连接心室复极各瞬间向量所形成的轨迹,就是心室再极化心电向量环,简称T环。

T环的投影即为T波。再极化过程同心肌代谢有关,因而较去极化过程缓慢,占时较长。T波与S-T段同样具有重要的诊断意义。  

2.1.7 U

T波后0.02~0.04秒出现宽而低的波,波高多在0.05毫伏以下,波宽约0.20秒。一般认为可能由心舒张时各部产生的负后电位形成,也有人认为是浦肯野氏纤维再极化的结果。血钾不足,甲状腺功能亢进和强心药等都会使U波加大。

2.2 心电图记录方法

心电图描记方法在体表任何两处安放电极板,用导线接到心电图机的正负两极,即形成导联,可借以记录人体两处的心电电位差。

首先我们要先知道怎么叫做导联,在心电图的专业术语中,导联就是记录心电图时电极在人体体表的放置位置及电极与放大器的连接方式。简单来说导联包含两个方面,一个是位置,一个是连接方式,比如正极接哪个,负极接哪个。

有关心电图的导联方式可以参考:《生物医学基础--讲不明白12导联算我输》。

2.3 数据库介绍

心电图可以客观反映心脏各部位的生理状况和工作状态,是诊断心律失常疾病的重要手段和主要依据。然而,即使是训练有素的心脏病专家,对这些心律失常的大量误诊仍然很常见。医生超负荷的工作进一步加剧了对突发心律失常的疏忽。

心律失常的自动分类是筛选心律失常、帮助医生做出准确诊断和减少工作时间的具有重大价值的研究。为了更好验证分类效果的有效性、准确性,还需要标注好的心电数据库。

2.3.1 数据库

当前世界上公认的心电信号的数据库:

  • MIT-BIH数据库:包含48个半小时两通道动态心电图记录,记录以每通道每秒360个样本的速度(采样率)在10mV范围内以11位分辨率数字化。两位心脏病专家分别对每条记录进行注释(总共约110,000个注释),完全公开免费;
  • AHA数据库:一共含有155个心电数据,每个心电数据的时长为3个小时,采样率250Hz。心电信号只有最后三十分钟被人工标注。AHA数据库为非公开数据库,下载不仅收费并且价格比较昂贵。由美国心脏协会和华盛顿大学联合(American Heart Association)开发;
  • QT数据库:来源于MIT-BIH为同一家医院,但是QT数据库中一个含有105条数据,每条数据是两导联数据,采样频率为250 Hz,仅部分心拍被标记;
  • CSE数据库:是12导联的心电数据库,共有1000条心电数据,每条记录的时长仅为10秒,采样率是500Hz。标记了P-QRS-T波开始于结束的位置,但是并没有心拍的诊断结果,所以不能用于研究心拍的分类。主要用来测试大型心电数据采集机的性能。欧盟的CSE心电数据库(Common Standards for Quantitative ECG);
  • 欧洲ST-T数据库:该数据库由79名患有心肌缺血疾病的受试者的90份带注释的动态心电图记录样本组成。受试者为70名年龄在3084岁之间的男性,以及一些年龄在5571岁之间的女性。建立了额外的选择标准,以获得数据库中心电图异常的代表性选择,包括由高血压、心室运动障碍和药物作用等条件引起的基线ST段位移。每条记录持续2小时,包含两个信号。每个采样速率为每秒250个采样,标称输入范围为20mV,分辨率为12位。

其中,麻省理工学院的MIT-BIH心电数据库是目前使用最多的实验用心电数据库,公开免费使用,并且其中的心电数据已经被人工标记,所以现在的许多心电数据自动分析算法都是在MIT-BIH数据库上进行验证的。

2.3.2 对比

这里我们比较一下MIT-BIH数据库、AHA数据库、QT数据库、CSE数据库;

数据库 MIT-BIH AHA QT CSE
记录时间 30min 3h 15min 10s
导联数目 2 2 2 12
记录数量 48 155 105 1000
采样频率 360Hz 250Hz 250Hz 500Hz
导联数量 2 2 2 12
价格 免费 昂贵 免费 昂贵
标注范围 所有 最后三十分钟 部分节拍 所有

其中:

  • 采样率:采样频率,定义了每秒从连续信号中提取并组成离散信号的采样个数,用Hz表示,计算机每秒采集多少个信号样本。

当前使用最广泛且被学术界普遍认可的据库为MIT-BIH心律失常数据库。此数据库中囊括了所有类型的心电信号并且数量丰富,为本文关于心电信号的研究提供了实验数据。

三、MIT-BIH数据读取

3.1 数据介绍

MT-BIH心律失常数据库拥有48条心电记录,且每个记录的时长是30分钟。这些记录是从1975年至1979年间由BIH心律失常实验室对47名受试者进行的,这些研究对象包括25名男性和22名女性,其年龄介于2389岁(其中记录201202来自于同一个人)。

3.1.1 数据描述

这些记录以每通道每秒360个采样率和10mV范围内的11位分辨率进行数字化。对于每条记录来说,均包含两个通道的信号:

  • 第一个通道一般为MLⅡ导联(记录102104V5导联);
  • 第二个通道一般为V1导联(有些为V2导联或V5导联,其中记录124号为Ⅴ4导联);

为了保持导联的一致性,往往在研究中采用MLⅡ导联。本文选取MLⅡ导联心电信号进行研究分析。

3.1.2 数据获取

官方下载地址:http://www.physionet.org/physiobank/database/mitdb/

下载完成,解压目录结构如下:

关于MIT-BIH数据库的一些常用网站:

  • MIT-BIH数据库的官方网站:https://ecg.mit.edu/
  • 官网上关于该数据库的详细介绍网址:https://archive.physionet.org/physiobank/database/html/mitdbdir/intro.htm#symbols
  • 每条信号的基础信息可以查询:https://www.physionet.org/physiobank/database/html/mitdbdir/mitdbdir.htm
  • 数据库中每条心电信号中的心拍类型表:https://archive.physionet.org/physiobank/database/html/mitdbdir/tables.htm#allbeats
  • MIT-BIH心电数据可视化网站:https://www.physionet.org/lightwave/?db=mitdb/1.0.0
  • 官方网站的可视化工具读取展示MIT-BIH数据:https://archive.physionet.org/cgi-bin/atm/ATM

3.2 数据格式

MIT为了节省文件长度和存储空间,使用了自定义的格式,所以没有通用的读取方式。

一个心电记录由三个部分组成:

  • 头文件[.hea],存储方式ASCII码字符,记录信号的采样频率、采样频率、数据格式使用的导联信息、采样频率、研究者的性别、年龄以及疾病种类等;
  • 数据文件[.dat],按二进制存储,每三个字节存储两个数,一个数12bit
  • 注释文件[.art],按二进制存储,是由专家对信号进行人工标注,并且根据二进制格式进行数据的存储;
3.2.1 头文件

这里我们查看100.hea文件,文件内容如下:

100 2 360 650000
100.dat 212 200 11 1024 995 -22131 0 MLII
100.dat 212 200 11 1024 1011 20052 0 V5
# 69 M 1085 1629 x1
# Aldomet, Inderal

其中:

  • 第一行为记录行,包含两个采样率为360Hz的信号,每一信号的长度为65万个采样点;
  • 后面紧跟的两行是信号技术规范说明行,从中可以看出:
    • 文件名:两个信号都包含在文件格式100.dat中;
    • 数据格式:每一信号都是以12位的位压缩格式(即212格式)进行存储的;
    • 增益:两个信号的增益都是每200ADC uints/mV
    • 分辨率:ADC的分辨率为11位;
    • 零值:ADC零值为1024;
    • 第一个采样点值:两个信号的第一采样点的值分别为9951011
    • 校验数:65万个采样点的校验数分别为-2213120052
    • 输入输出可以以任何尺寸的块来执行,因为文件内容说明了这两个信号的该值都为0;
    • 导联:信号描述字段说明了这两个信号分别采自MLII导联和V5导联;

文件的最后两行包含了注释字符串,其中第一行说明了患者的年龄和性别以及记录数据,第二行列出了患者的用药情况。

3.2.1 数据文件

这里以16进制格式查看100.dat文件;

MIT-BIH数据库中的数据存储格式有Format8Format16Format80Format212Format3108种,心律失常数据库统一采用212格式进行存储。

212格式是针对两个信号的数据库记录,这两个信号的数据交替存储,每三个字节存储两个数据。

100.dat为例,每三个字节(24位)表示两个值,第一组为E3 33 F3,两个值则分别为0x3E30x3F3转换为十进制的9951011,代表的信号幅度分别为-0.145mV-0.065mV,这两个值分别是两个信号的第一采样点,后面以此类推,分别表示了两个信号的采样值。

0x3e3    995    (995 - 1024)/200=-0.145mv
0x3f3    1011   (1011 - 1024/200=-0.065mv
3.2.1 注释文件

这里以16进制格式查看100.atr文件;

记录了心电专家对相应的心电信号的诊断信息,主要有两种格式:MIT格式和AHA格式。若文件的第一字节不为0或第二字节等于0x5B0x5D,则该文件是以MIT格式存储的,否则是按AHA格式存储的。

100.atr为例,从文件中的第一字节不为0可以判断该文件是以MIT格式存储的。

从第一字节开始首先读出16位值0x7012,其高6位的值为0x1C28),低10位的值为0x1218),该类型代码为28,代表意义是节律变化,发生时间在0.05秒(18/360Hz);

接着读出后面的16位值0xFC03,其高6为的值为0x3F63),低10位的值为0x03(3),该类型代码为63,代表的意义是在该16位值后附加了3个字节的辅助信息,若字节个数为奇数,则再附加一个字节的空值,在本例中就是28 4E 00 00

然后再从下一字节读16位值0x043B,其高6位的值为1,低10位的值为0x3B59),该类型码1代表正常心搏,发生时间为0.214秒((18+59)/360Hz);依次类推即可读出所有的注释,当读到的16位值为0时,就表示到了文件尾。

3.3 数据读取

下载数据库到本地后打开,你会发现.dat文件中全部都是乱码,这是由于MIT-BIH数据库采用了自定义的format212格式进行编码。在读取心电数据的时候,可以自行解析,也可以使用Python中的一个工具包:wfdbwfdb依赖包安装:

pip install wfdb -i https://pypi.tuna.tsinghua.edu.cn/simple

Pycharm中新建项目mit-bit-analyze,并将下载好的心电数据集按如图所示的目录结构进行放置;

其中mit-bih-arrhythmia-database-1.0.0为心电数据集的文件夹。

在项目下新建ecg_reader.py文件,定义一个ECGReader类,实现头文件、数据文件、注释文件的读取。

3.3.1 头文件读取

头文件的读取通过load_hea方法实现;

def load_hea(self):
	"""
	读取hea头文件:将源文件按行读取并记录下各参数含义,存入变量。
	:return:
	"""
	print("loading the ecg hea of No." + self.__number)

	with open(self.__hex_file, "r") as f:
		# 读取第一行
		line = f.readline()

		# 默认按照空格分隔
		z = line.split()

		# number of signals, sample rate of data, 比如100.hex文件,num_sig=2  sig_freq=360
		self.num_sig, self.sig_freq = int(z[1]), int(z[2])
		print('\tnumber of signals {}, sample rate of data {}'.format(self.num_sig, self.sig_freq))

		# 解析每个信号信息
		for i in range(self.num_sig):
			# 读取新行
			line = f.readline()
			# 默认按照空格分隔
			z = line.split()
			# format; here only 212 is allowed
			self.dformat.append(int(z[1]))
			# number of integers per mV
			self.gain.append(int(z[2]))
			# bit resolution
			self.bit_res.append(int(z[3]))
			# integer value of ECG zero point
			self.zero_value.append(int(z[4]))
			# first integer value of signal (to test for errors)
			self.first_value.append(int(z[5]))
			# 导联
			self.lead.append(z[8])
	print('\tformat:', self.dformat)
	print('\tnumber of integers per mV:', self.gain)
	print('\tbit resolution:', self.bit_res)
	print('\tinteger value of ECG zero point:', self.zero_value)
	print('\tfirst integer value of signal (to test for errors):', self.first_value)
	print('\tlead:', self.lead)

比如读取100.hea文件的结果如下:

loading the ecg hea of No.100
	number of signals 2, sample rate of data 360
	format: [212, 212]
	number of integers per mV: [200, 200]
	bit resolution: [11, 11]
	integer value of ECG zero point: [1024, 1024]
	first integer value of signal (to test for errors): [995, 1011]
	lead: ['MLII', 'V5']
3.3.2 数据文件读取

数据文件的读取通过load_data方法实现;

def load_data(self):
	"""
	读取data数据文件:三个字节存储两个信号数据,对信号进行处理,步骤如下
		A 将原始数据转换为SAMPLES2READx3矩阵
		data 由SAMPLES2READx3列矩阵转的SAMPLES2READx2列矩阵
		data[:,0] 第一个信号幅度;
		data[:,1] 第二个信号幅度;
		如果信号量num_sig=2,信号幅度减去零点再除以信号增益
		如果信号量num_sig=1,……
	:return:
	"""
	print("loading the ecg dat of No." + self.__number)

	# 以二进制格式读入dat文件
	with open(self.__data_file, "rb") as f:
		# 读取所有数据 比如100.dat 650000*3=1950000
		byte_array = f.read()
		print('\traw data length:', len(byte_array))

		# 根据给定的字节数据和数据类型创建一个Numpy数组,即将读入的二进制文件转化为unit8格式
		raw_data = np.frombuffer(byte_array, dtype=np.uint8)
		# 将原始具转为Nx3矩阵
		A = raw_data.reshape(int(raw_data.shape[0] / 3), 3)[:self.SAMPLES2READ].astype(np.uint32)

		# 创建矩阵M,保存数据
		self.data = np.zeros((self.SAMPLES2READ, 2))
		self.data[:, 0] = ((A[:, 1] & 0x0f) << 8) + A[:, 0]
		self.data[:, 1] = ((A[:, 1] & 0xf0) << 4) + A[:, 2]

		if (self.data[1, :] != self.first_value).any():
			print("inconsistency in the first bit values")
			return

		if self.num_sig == 2:
			self.data[:, 0] = (self.data[:, 0] - self.zero_value[0]) / self.gain[0]
			self.data[:, 1] = (self.data[:, 1] - self.zero_value[1]) / self.gain[1]
			self.time = np.linspace(0, self.SAMPLES2READ - 1, self.SAMPLES2READ) / self.sig_freq
		elif self.num_sig == 1:
			M = []
			self.data[:, 0] = self.data[:, 0] - self.zero_value[0]
			self.data[:, 1] = self.data[:, 1] - self.zero_value[1]
			for i in range(self.data.shape[0]):
				M.append(self.data[:, 0][i])
				M.append(self.data[:, 1][i])
			M.append(0)
			del M[0]
			self.data = np.array(M) / self.gain[0]
			self.time = np.linspace(0, 2 * self.SAMPLES2READ - 1, 2 * self.SAMPLES2READ) / self.sig_freq
		else:
			print("\tSorting algorithm for more than 2 signals not programmed yet!")

		print('\tReal first value {},unit {}'.format(self.data[0, :], 'mV'))

比如读取100.dat文件的结果如下:

loading the ecg dat of No.100
	raw data length: 1950000
	Real first value [-0.145 -0.065],unit mV
3.3.3 注释文件读取

注释文件的读取通过load_atr方法实现;

def load_atr(self):
	"""
	读取atr注释文件
	:return:
	"""
	print("loading the ecg atr of No." + self.__number)

	with open(self.__atr_file, "rb") as f:
		# 读取所有数据 比如100.atr
		byte_array = f.read()
		print('\tatr length:', len(byte_array))

		# 根据给定的字节数据和数据类型创建一个Numpy数组,即将读入的二进制文件转化为unit8格式
		raw_data = np.frombuffer(byte_array, dtype=np.uint8)

		# 转换为 Nx2 矩阵
		A = raw_data.reshape(int(raw_data.shape[0] / 2), 2).astype(np.uint32)

		annot = []
		i = 0
		while i < A.shape[0]:
			# 读取高6位
			annoth = A[i, 1] >> 2
			if annoth == 59:
				annot.append(A[i + 3, 1] >> 2)
				self.atr_time.append(
					A[i + 2, 0] + (A[i + 2, 1] << 8) + (A[i + 1, 0] << 16) + (A[i + 1, 1] << 24))
				i += 3
			elif annoth == 60:
				pass
			elif annoth == 61:
				pass
			elif annoth == 62:
				pass
			elif annoth == 63:
				hilfe = ((A[i, 1] & 3) << 8) + A[i, 0]
				hilfe = hilfe + hilfe % 2
				i += int(hilfe / 2)
			else:
				self.atr_time.append(((A[i, 1] & 3) << 8) + A[i, 0])
				annot.append(A[i, 1] >> 2)
			i += 1

		del annot[len(annot) - 1]
		del self.atr_time[len(self.atr_time) - 1]

		self.atr_time = np.array(self.atr_time)
		self.atr_time = np.cumsum(self.atr_time) / self.sig_freq

		ind = np.where(self.atr_time <= self.time[-1])[0]
		self.atr_time = self.atr_time[ind]

		annot = np.round(annot)
		self.annotd = annot[ind]

比如读取100.atr文件的结果如下:

loading the ecg atr of No.100
	atr length: 4558
3.3.4 绘制ECG波形

波形绘制通过plt_ecg函数实现;

def plt_ecg(self):
	"""
	绘制心电图
	:return:
	"""
	plt.figure(figsize=(12, 8))
	# 调整子图之间的垂直间距
	plt.subplots_adjust(hspace=0.5)

	if self.num_sig == 2:
		plt.subplot(211)
	plt.plot(self.time, self.data[:, 0], linewidth="0.5", c="r")
	plt.xlim(self.time[0], self.time[-1])
	plt.xlabel("Time / s")
	plt.ylabel("Voltage / mV({})".format(self.lead[0]))
	plt.title("ECG signal")
	plt.grid()
	for i in range(len(self.atr_time)):
		plt.text(self.atr_time[i], 0, str(self.annotd[i]))

	if self.num_sig == 2:
		plt.subplot(212)
		plt.plot(self.time, self.data[:, 1], linewidth="0.5", c="b")
		plt.xlim(self.time[0], self.time[-1])
		plt.xlabel("Time / s")
		plt.ylabel("Voltage / mV({})".format(self.lead[1]))
		plt.title("ECG signal")
		plt.grid()
		for i in range(len(self.atr_time)):
			plt.text(self.atr_time[i], 0, str(self.annotd[i]))
	plt.show()

绘制波形如下:

self.data[:, 0]表示信号1,使用红线绘制;横坐标是采样率的时间间隔,一个单位为1/360s,纵坐标是信号相对零点的幅度。

self.data[:, 1]表示信号2,使用蓝线绘制;横坐标是采样率的时间间隔,一个单位为1/360s,纵坐标是信号相对零点的幅度。

plt.text注释内容self.annotd,注释横坐标self.atr_time,纵坐标0

3.4 数据处理

由于心电信号通常十分微弱,采集过程中容易受到各种因素的干扰,心电信号的噪声种类繁多,所以心电图分类识别的首要步骤就是对信号进行预处理。

国内外研究学者针对去噪方面做了很多研究,主要的去噪手段有经典的数字滤波器和基于小波变换的阈值去噪等;

  • 经典的数字滤波器根据频率范围的不同对噪声进行去噪:对于基线漂移使用高通滤波器去噪、对于肌电干扰使用低通滤波器去噪、对于工频干扰使用带通滤波器去噪;
  • 近年来,小波变换技术的快速发展催生出了一系列基于小波阈值去噪技术,该类技术是根据信号和噪声的频率在不同尺度上的分布,先对信号进行小波变换,再根据阈值对各层小波系数进行处理,最后重构信号实现去噪;

小波阈值去噪技术对于非平稳信号具有优秀的处理效果,与传统处理方法相比有显著的优越性。

3.4.1 ECG信号噪声

ECG信号具有微弱、低幅值、低频、随杋性的特点,很容易被噪声干扰,而噪声可能来自生物体内,如呼吸、肌肉颤抖,也可能因为接触不良而引起体外干扰。

ECG信号主要的三种噪声为工频干扰、肌电干扰和基线漂移,也是在滤波过程中急需被抑制去除的噪声干扰;

  • 工频干扰:是由采集心电信号的设备周身的供电环境引起的电磁干扰,幅值低,噪声频率为50Hz左右,其波形很像一个正弦信号,该噪声常常会淹没有用的心电信号,也会影响P波和T波的检测;
含工频干扰的心电信号
  • 肌电干扰:在心电图采集过程中,因为人体运动肌肉不自主颤抖造成,这种干扰无规律可言,波形形态会急速变化,频率很高,并且分布很广,范围在0-2000Hz内,能量集中在30-300Hz内,持续时间一般为50ms,肌电干扰与心电信号会重合在一起,这会导致有用的心电信号细微的变化很可能被忽视;
img
  • 基线漂移:属于低频干扰,频率分布在0.15-0.3Hz内,由于电极位置的滑动变化或者人体的呼吸运动造成心电信号随时间缓慢变化而偏离正常基线位置产生基线漂移,幅度和频率都会时刻变化着。心电信号中的PR波段和ST波段非常容易受到影响产生失真;
img
3.4.2 小波变化步骤

小波变换(Wavelet Transform, WT)可以进行时频变换,是对信号进行时域以及频域分析的最为理想工具。

有关小波变换的原理可以参考这篇文章:《形象易懂讲解算法I——小波变换》。

本文对含噪心电信号采用基于小波变换的去噪处理方法,分为以下3个步骤:

  • 由于噪声和信号混杂在一起,首先选择一个小波基函数,由于噪声和信号混杂在一起,所以要用小波变换对含噪心电信号进行某尺度分解得到各尺度上的小波系数;

  • 心电信号经过小波变换尺度分解后,幅值比较大的小波系数就是有用的信号,幅值比较小的小波系数就是噪声,根据心电信号和夹杂噪声的频率分布,对各尺度上的小波系数进行阈值处理,把小于阈值的小波系数置零或用阈值函数处理;

  • 分别处理完小波尺度分解后的低频系数和高频系数,再重构信号;

img

小波变换通常会将信号分解成近似系数(Approximation Coefficients)和细节系数(Detail Coefficients)。这些系数代表了信号在不同频率和尺度上的特征信息。

在小波变换中,一般用A表示近似系数,用D表示细节系数。通常,小波变换会将信号分解成不同层次的近似系数和细节系数,例如A1, D,A2,D2, ...An,Dn,其中A1是第一层的近似系数,D1是第一层的细节系数,以此类推。

小波4尺度分解所得各尺度系数示意图如下,9尺度小波分解可以类比之:

img

小波系数处理的阈值函数有硬阈值和软阈值之分;

  • 硬阈值函数:若分解后的系数绝对值大于阈值,保证其值不变;当其小于给定的阈值时,令其为零。

img

  • 软阈值函数:若分解后的系数绝对值大于阈值,令其值减去λ;当其小于给定的阈值时,令其为零;

img

其中w为原始小波系数,W为处理后的小波系数,λ为给定的阈值,N为信号长度。λ的计算公式为

λ=median|w|2lnN0.6745

由上文分析可知,软阈值去噪法的小波系数在连续性上优于硬阈值法,故本文采取了软阈值法结合小波基对信号进行仿真实验。

3.4.3 代码实现

安装pywt包,pywtPython中一个用于小波变换(Wavelet Transform)的库;

pip install PyWavelets  -i https://pypi.tuna.tsinghua.edu.cn/simple

去噪代码如下:

def denoise(signal, wavelet='db5', level=9):
    """
    wavelet denoise preprocess using mallat algorithm
    :param signal: 输入信号
    :param wavelet: 小波基
    :param level: 尺度
    :return:
    """
    print('denoise....')
    # 将时域信号进行9尺度变换到频域,小波基选用db5,返回值即为各尺度系数
    coefficients = pywt.wavedec(data=signal, wavelet=wavelet, level=level)
    cA9, cD9, cD8, cD7, cD6, cD5, cD4, cD3, cD2, cD1 = coefficients
    print('\tcD1 length {} ,shape {}'.format(len(cD1), cD1.shape))
    print('\tcD2 length {} ,shape {}'.format(len(cD2), cD2.shape))
    print('\tcD3 length {} ,shape {}'.format(len(cD3), cD3.shape))
    print('\tcD4 length {} ,shape {}'.format(len(cD4), cD4.shape))
    print('\tcD5 length {} ,shape {}'.format(len(cD5), cD5.shape))
    print('\tcD6 length {} ,shape {}'.format(len(cD6), cD6.shape))
    print('\tcD7 length {} ,shape {}'.format(len(cD7), cD7.shape))
    print('\tcD8 length {} ,shape {}'.format(len(cD8), cD8.shape))
    print('\tcD9 length {} ,shape {}'.format(len(cD9), cD9.shape))
    print('\tcA9 length {} ,shape {}'.format(len(cA9), cA9.shape))

    # denoise using soft threshold
    threshold = (np.median(np.abs(cD1)) / 0.6745) * (np.sqrt(2 * np.log(len(cD1))))

    # 将高频信号cD1、cD2置零
    cD1.fill(0)
    cD2.fill(0)

    # 去除基线漂移, 9层低频信息
    cA9.fill(0)

    # 将其他中低频信号按软阈值公式滤波  cD9,cD8,cD7,...cD3
    for i in range(1, len(coefficients) - 2):
        coefficients[i] = pywt.threshold(coefficients[i], threshold)

    # 最后对小波系数进行反变换,获得去噪后的信号
    return pywt.waverec(coeffs=coefficients, wavelet=wavelet)

这里我们以编号为100的心电数据记录为例,使用pywt.wavedec将时域信号进行9尺度变换到频域,小波基选用db5,返回值即为各尺度系数;

在对原始信号进行分解之后,我们可以知道,1-2层的细节分量的能量与原始信号的高频干扰保持一致。表明1-2层是高频噪声(工频干扰以及肌电噪声)集中的主要地方。因此我们需要滤除D1层和D2层的细节分量,通过将其置0来达到去除的目的。然后将信号分解得到的3~9层小波系数通过软阈值公式对信号的阈值进行处理。pywt.threshold函数提供了阈值滤波功能,并且默认是mode='soft'的软阈值滤波。

最后对小波系数进行反变换,获得去噪后的信号。对预处理前后的100号信号1截取前1500个信号点进行对比如下,可以看出降噪效果还是不错的:

def plt_compare(signal1, signal2):
    """
    绘制两个信号的对比波形
    :param signal1: 信号1
    :param signal2: 信号2
    :return:
    """
    plt.figure(figsize=(12, 8))

    plt.subplot(211)
    plt.title('ECG signal')
    plt.plot(signal1)
    plt.xlim(0, len(signal1))

    plt.subplot(212)
    plt.title('ECG signal after denoise')
    plt.plot(signal2)
    plt.xlim(0, len(signal2))
    plt.show()

运行结果如下:

denoise....
	cD1 length 2004 ,shape (2004,)
	cD2 length 1006 ,shape (1006,)
	cD3 length 507 ,shape (507,)
	cD4 length 258 ,shape (258,)
	cD5 length 133 ,shape (133,)
	cD6 length 71 ,shape (71,)
	cD7 length 40 ,shape (40,)
	cD8 length 24 ,shape (24,)
	cD9 length 16 ,shape (16,)
	cA9 length 16 ,shape (16,)

第一个波形为100号信号1截取前1500个信号点绘制成的心电图;第二个波形为小波去噪之后的心电图。

四、QRSPT波检测

4.1 QRS检测

QRS检测是处理ECG信号的基础,不管最后实现什么样的功能,QRS波的检测都是前提,所以准确的检测QRS波是特征提取的前提。

(1) 采用基于二进样条4层小波变换,二进样条小波滤波器:

  • 低通滤波器:[1/4 3/4 3/4 1/4]
  • 高通滤波器:[-1/4 -3/4 3/4 1/4]

(2) 在第3层细节系数中找到极大极小值对:在3层的细节系数中利用极大极小值方法能够非常好的检测出R波,选择3层细节系数是基于R波在3层系数下表现的与其它噪声区别最大;

  • 找极大值方法:找出斜率大于0的值,并赋值为1,其余为0,极大值就在序列类似1, 0这种点,即前面一个值比后面的大的值相应的位置点;
  • 找极小值方法:类似极大值,找出斜率<0的值相应的位置,并赋值为1。其余的为0,极小值就在类似1,0的序列中相应的位置。即前面一个值比后面的大的值相应的位置点;

(3) 设置阈值提取出R波:我们能够看出R波的值要明显大于其它位置的值,其在3层细节系数的特点也类似于此;这样我们就能够设置一个可靠的阈值(将全部点分为4部分。求出每部分最大值的平均值T,阈值为T/3)来提取一组相邻的最大最小值对;这样最大最小值间的过0点就是相应于原始信号的R波点;

(4) 补偿R波点:因为在二进样条小波变换的过程中,3层细节系数与原始信号的相应的位置有10个点的漂移。在程序中须要补偿;

(5) 找Q S波:基于R波的位置,在R波位置(在1层细节系数下)的前3个极点为Q波;在R波位置(1细节系数下)的后3个极点为S波;这样我们就将QRS波定位出来;

(6) 因为不同的情况,可能造成R波的漏检和错检(把T波检测为R波),我们依据相邻R波的距离进行检测漏检与错检;

当相邻R波的距离<0.4 mean(RR)平均距离时,这是错检。这样去除值小的R波。当相邻R波的距离>1.6mean(RR)时。在两个RR波间找到一个最大的极值对,定位R波。这是防止漏检。

4.1.1 二进样条4层小波变换

二进样条小波(Biorthogonal wavelets)是一种特殊类型的小波基函数,用于小波变换中。与正交小波相比,二进样条小波具有更灵活的性质,在某些应用中可能更加适用。

在二进样条小波中,分解和重构滤波器是成对的,并且彼此不是正交的。这意味着在分解过程中使用一个滤波器对信号进行高通滤波,而另一个滤波器对信号进行低通滤波。在重构过程中,相反的滤波器被应用于细节和逼近系数。

具体实现代码如下:

def __biorthogonal_wavelets__(self):
	"""
	对输入信号进行二进样条4层小波变换
	:return:
	"""
	# 存储概貌信息
	swa = np.zeros((self.level, self.length))
	# 存储细节信息
	swd = np.zeros((self.level, self.length))

	# 低通滤波器 1/4 3/4 3/4 1/4
	# 高通滤波器 -1/4 -3/4 3/4 1/4
	# 二进样条小波
	for i in range(self.length - 3):
		swa[0, i + 3] = (1 / 4 * self.signal[i + 3]
						 + 3 / 4 * self.signal[i + 2]
						 + 3 / 4 * self.signal[i + 1]
						 + 1 / 4 * self.signal[i])
		swd[0, i + 3] = (-1 / 4 * self.signal[i + 3]
						 - 3 / 4 * self.signal[i + 2]
						 + 3 / 4 * self.signal[i + 1]
						 + 1 / 4 * self.signal[i])

	j = 1
	while j < self.level:
		for i in range(self.length - 24):
			swa[j, i + 24] = (1 / 4 * swa[j - 1, i + 24 - (2 << (j - 1)) * 0]
							  + 3 / 4 * swa[j - 1, i + 24 - (2 << (j - 1)) * 1]
							  + 3 / 4 * swa[j - 1, i + 24 - (2 << (j - 1)) * 2]
							  + 1 / 4 * swa[j - 1, i + 24 - (2 << (j - 1)) * 3])
			swd[j, i + 24] = (-1 / 4 * swa[j - 1, i + 24 - (2 << (j - 1)) * 0]
							  - 3 / 4 * swa[j - 1, i + 24 - (2 << (j - 1)) * 1]
							  + 3 / 4 * swa[j - 1, i + 24 - (2 << (j - 1)) * 2]
							  + 1 / 4 * swa[j - 1, i + 24 - (2 << (j - 1)) * 3])
		j += 1

	if self.plt_flag:
		# 画出原信号和小波变化近似系数、细节系数
		plt.figure(figsize=(16, 10))
		# 调整子图之间的垂直间距
		plt.subplots_adjust(hspace=0.5)

		plt.subplot(self.level + 1, 1, 1)
		plt.plot(self.signal)
		plt.grid()
		plt.axis('tight')
		plt.title(
			'ECG signal along with its approximation coefficients and detail coefficients at scales j=1,2,3,4')

		for i in range(self.level):
			plt.subplot(self.level + 1, 2, 2 * i + 3)
			plt.plot(swa[i, :])
			plt.axis('tight')
			plt.grid()
			plt.xlabel('time')
			plt.ylabel('a ' + str(i + 1))

			plt.subplot(self.level + 1, 2, 2 * i + 4)
			plt.plot(swd[i, :])
			plt.axis('tight')
			plt.grid()
			plt.ylabel('d ' + str(i + 1))
		plt.show()

	return swa, swd

这里我们同样以100号新号1为例进行测试,采样点设置为1500;

# 1. 二进样条小波变换
swa, swd = self.__biorthogonal_wavelets__()

运行结果如下:

最上面的波形为编号100信号1的波形图,左侧a1a2a3a4为小波变换近似系数波形图,右侧d1d2d3d4为小波变换细节系数波形图。

4.1.2 查找极大/小值

找极大值方法:找出斜率大于0的值,并赋值为1,其余为0,极大值就在序列类似1, 0这种点,即前面一个值比后面的大的值相应的位置点;

找极小值方法:类似极大值,找出斜率<0的值相应的位置,并赋值为1。其余的为0,极小值就在类似1,0的序列中相应的位置,即前面一个值比后面的大的值相应的位置点;

代码实现如下:

def __find_extrema__(self, swd):
	"""
	获取正负极大值
	:param swd: 二进样条小波变换细节系数
	:return: 返回各尺度小细节系数的极大值、极小值位置
			如果当前采样点不是极值点,将其设置为0
	"""
	print('find extrema')
	# 创建一个与示例数组形状相同的全零数组
	ddw = np.zeros_like(swd, dtype=np.int32)
	pddw = np.copy(ddw)
	nddw = np.copy(ddw)

	# swd中大于0的元素为True,否则为False。然后,将这个布尔数组与swd相乘,由于布尔数组会被自动转换为0和1,所以相乘的结果就是将小于等于0的元素置为0
	posw = swd * (swd > 0)

	# 斜率大于0
	pdw = ((posw[:, :self.length - 1] - posw[:, 1:self.length]) < 0).astype(int)

	# 正极大值点
	pddw[:, 1:self.length - 1] = (pdw[:, :self.length - 2] - pdw[:, 1:self.length - 1]) > 0

	# 细节系数小于0的点
	negw = swd * (swd < 0)

	# 斜率小于0
	ndw = ((negw[:, :self.length - 1] - negw[:, 1:self.length]) > 0).astype(int)

	# 负极大值点
	nddw[:, 1:self.length - 1] = (ndw[:, :self.length - 2] - ndw[:, 1:self.length - 1]) > 0

	# 或运算
	ddw = pddw | nddw
	ddw[:, 0] = 1
	ddw[:, self.length - 1] = 1

	# 求出极值点的值,其它点置0
	wpeak = ddw * swd
	wpeak[:, 0] += 0
	wpeak[:, self.length - 1] += 0

	if self.plt_flag:
		# 画出原信号和各尺度下的极值点
		plt.figure(figsize=(16, 10))
		# 调整子图之间的垂直间距
		plt.subplots_adjust(hspace=0.5)

		plt.subplot(self.level + 1, 1, 1)
		plt.plot(self.signal)
		plt.grid()
		plt.axis('tight')
		plt.title('ECG signal extreme points of detail coefficients at scales j=1,2,3,4')

		for i in range(self.level):
			plt.subplot(self.level + 1, 1, i + 2)
			# 细节系数波形图
			plt.plot(swd[i, :], color='blue', linewidth=1)
			# 极值波形图
			plt.plot(wpeak[i, :], color='red', linewidth=0.5)
			plt.axis('tight')
			plt.grid()
			plt.xlabel('time')
			plt.ylabel('d ' + str(i + 1) + ' extrema')
		plt.show()

	return wpeak

这里我们同样以100号新号1为例进行测试,采样点设置为1500;

# 2. 获取正负极大值
wpeak = self.__find_extrema__(swd)

运行结果如下:

最上面的波形为编号100信号1的波形图,下面四个波形依次为细节系数d1d2d3d4极值点(图中红色波形)波形图。

这里为了直观的判断极值点位置查找的是否正确,我们同时绘制了细节系数的波形。

4.1.3 设置阈值筛选R

设置阈值:我们能够看出R波的值要明显大于其它位置的值,其在3层细节系数的特点也类似于此;这样我们就能够设置一个可靠的阈值(将全部点分为4部分。求出每部分最大值的平均值T,阈值为T/3)来提取一组相邻的最大最小值对;这样最大最小值间的过0点就是相应于原始信号的R波点;

def __filter_r__(self, coefficients):
	"""
	我们能够看出R波的值要明显大于其它位置的值,这样我们就能够设置一个可靠的阈值(将全部点分为4部分。求出每部分最大值的平均值T,阈值为T/3)来提取一组相邻的最大最小值对;这样最大最小值间的过0点就是相应于原始信号的R波点
	:param coefficients: 二进样条小波变换某种尺度下细节系数
	:return: interva, thnega, thposi
		interva: 如果采样点不满足阈值过滤条件,将其设置为0
		thnega: 正极大值的平均
		thposi: 负极大值的平均
	"""
	posi = coefficients * (coefficients > 0)
	nega = coefficients * (coefficients < 0)
	part_len = round(self.length / 4)
	# 求正极大值的平均
	thposi = (np.max(posi[:part_len])
			  + np.max(posi[part_len:2 * part_len])
			  + np.max(posi[2 * part_len:3 * part_len])
			  + np.max(posi[3 * part_len:4 * part_len])) / 4
	# 筛选R波
	posi = posi > (thposi / 3)
	# 求负极大值的平均
	thnega = (np.min(nega[:part_len])
			  + np.min(nega[part_len:2 * part_len])
			  + np.min(nega[2 * part_len:3 * part_len])
			  + np.min(nega[3 * part_len:4 * part_len])) / 4
	# 筛选R波
	nega = -1 * (nega < (thnega / 4))
	# 找到数组sum中非零元素的索引
	sum = posi + nega
	loca = np.where(sum)[0]
	print('\t loca {}'.format(loca))
	# 计算数组loca中相邻元素之间的差异,并将结果存储在数组diff中
	diff = np.zeros(len(loca) - 1)
	for i in range(len(loca) - 1):
		# 如果相邻元素之间的距离小于80,说明它们足够接近,可以计算差值,否则将差值设为0
		if abs(loca[i] - loca[i + 1]) < 80:
			diff[i] = sum[loca[i]] - sum[loca[i + 1]]
		else:
			diff[i] = 0
	# 找到极值对的索引
	loca2 = np.where(diff == -2)[0]
	print('\t loca2 {}'.format(loca2))
	# 负极大值点
	interva = np.zeros(len(sum))
	interva[loca[loca2]] = sum[loca[loca2]]
	# 正极大值点
	interva[loca[loca2 + 1]] = sum[loca[loca2 + 1]]
	if self.plt_flag:
		# 画出原信号和某种尺度下细节系数极值波形图
		plt.figure(figsize=(16, 10))
		# 调整子图之间的垂直间距
		plt.subplots_adjust(hspace=0.5)

		plt.subplot(2, 1, 1)
		plt.title('ECG signal')
		plt.plot(self.signal)

		plt.subplot(212)
		plt.title('coefficients and interva')
		# 某种尺度下细节系数极值波形图
		plt.plot(coefficients, color='blue', linewidth=1)
		# 处理后的极值波形图
		plt.plot(interva, color='red', linewidth=0.5)
		plt.show()
	return interva, thnega, thposi

这里我们同样以100号信号1为例进行测试,采样点设置为1500;

# 3. 设置阈值,筛选R波
Mj3 = wpeak[2, :]
interva, thnega, thposi = self.__filter_r__(Mj3)

运行结果如下:

最上面的波形为编号100信号1的波形图,下面的为极值波形图(蓝色为尺度3下细节系数极值Mj3波形图,红色为阈值处理后的极值interva波形图)。

4.1.4 补偿R波点/查找QS

上一步我们已经提取了一组相邻的最大最小值对,这样最大最小值间的过0点就是相应于原始信号的R波点;

补偿R波点:因为在二进样条小波变换的过程中,3层细节系数与原始信号的相应的位置有10个点的漂移,在程序中须要补偿;

Q S波:基于R波的位置,在R波位置(在1层细节系数下)的前3个极点为Q波;在R波位置(1细节系数下)的后3个极点为S波;这样我们就将QRS波定位出来;

代码如下:

# 4. 求正负极值对过零,即R波峰值,并检测出QRS波起点及终点
Mj1 = wpeak[0, :]
q_loca = np.zeros(self.length)  # QRS起点
r_loca = np.zeros(self.length)  # R波波峰
s_loca = np.zeros(self.length)  # QRS终点
# 保存R点所在的索引
r_index = np.zeros(self.length)
# R点个数
r_count = 0
i = 0
j = 0
while i < self.length:
	# 如果是负极值点
	if interva[i] == -1:
		# 标记负极值点
		mark1 = i
		i = i + 1
		# 查找正极值点
		while i < self.length and interva[i] == 0:
			i = i + 1
		# 标记正极值点
		mark2 = i

		# 求极大值对的过零点 已知两个点(mark1,Mj3[mark1])、(mark2,Mj3[mark2]),计算y=0时的x点坐标  (x2y1 - x1y2)/(y2-y1)
		mark_r = round(
			(abs(Mj3[mark2]) * mark1 + mark2 * abs(Mj3[mark1])) / (abs(Mj3[mark2]) + abs(Mj3[mark1])))
		# 为何 - 10?经验值吧
		mark_r = mark_r - 10
		# R波位置
		r_index[j] = mark_r
		r_loca[mark_r] = 1

		# 求出QRS波起点
		kqs = mark_r
		mark_q = 0
		while kqs > 1 and mark_q < 3:
			if Mj1[kqs] != 0:
				mark_q = mark_q + 1
			kqs = kqs - 1
		q_loca[kqs] = -1

		# 求出QRS波终点
		kqs = mark_r
		mark_s = 0
		while kqs < self.length and mark_s < 3:
			if Mj1[kqs] != 0:
				mark_s = mark_s + 1
			kqs = kqs + 1
		s_loca[kqs] = -1

		i = i + 60
		j = j + 1
		r_count = r_count + 1
	i = i + 1

我们将原始信号和QRS波形起始位置标记出来;

# 画出原信号和QRS所在位置波形图
plt.figure(figsize=(16, 6))
plt.title('ECG signal and QPS')

# ECG信息
plt.plot(self.signal)
# Q点
plt.plot(q_loca, color='black', linewidth=0.5)
# R点
plt.plot(r_loca, color='red', linewidth=0.5)
# S点
plt.plot(s_loca, color='black', linewidth=0.5)
plt.show()

结果如下:

亲爱的读者和支持者们,自动博客加入了打赏功能,陆陆续续收到了各位老铁的打赏。在此,我想由衷地感谢每一位对我们博客的支持和打赏。你们的慷慨与支持,是我们前行的动力与源泉。

日期姓名金额
2023-09-06*源19
2023-09-11*朝科88
2023-09-21*号5
2023-09-16*真60
2023-10-26*通9.9
2023-11-04*慎0.66
2023-11-24*恩0.01
2023-12-30I*B1
2024-01-28*兴20
2024-02-01QYing20
2024-02-11*督6
2024-02-18一*x1
2024-02-20c*l18.88
2024-01-01*I5
2024-04-08*程150
2024-04-18*超20
2024-04-26.*V30
2024-05-08D*W5
2024-05-29*辉20
2024-05-30*雄10
2024-06-08*:10
2024-06-23小狮子666
2024-06-28*s6.66
2024-06-29*炼1
2024-06-30*!1
2024-07-08*方20
2024-07-18A*16.66
2024-07-31*北12
2024-08-13*基1
2024-08-23n*s2
2024-09-02*源50
2024-09-04*J2
2024-09-06*强8.8
2024-09-09*波1
2024-09-10*口1
2024-09-10*波1
2024-09-12*波10
2024-09-18*明1.68
2024-09-26B*h10
2024-09-3010
2024-10-02M*i1
2024-10-14*朋10
2024-10-22*海10
2024-10-23*南10
2024-10-26*节6.66
2024-10-27*o5
2024-10-28W*F6.66
2024-10-29R*n6.66
2024-11-02*球6
2024-11-021*鑫6.66
2024-11-25*沙5
2024-11-29C*n2.88
posted @   大奥特曼打小怪兽  阅读(88)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
历史上的今天:
2018-03-24 第六节,Neural Networks and Deep Learning 一书小节(下)
2018-03-24 第五节,Neural Networks and Deep Learning 一书小节(中)
2018-03-24 第四节,Neural Networks and Deep Learning 一书小节(上)
2018-03-24 第三节,如何直观理解卷积神经网络的工作原理
2018-03-24 数据处理中模块之numpy
如果有任何技术小问题,欢迎大家交流沟通,共同进步

公告 & 打赏

>>

欢迎打赏支持我 ^_^

最新公告

程序项目代做,有需求私信(小程序、网站、爬虫、电路板设计、驱动、应用程序开发、毕设疑难问题处理等)。

了解更多

点击右上角即可分享
微信分享提示