#!/usr/bin/python # -*- coding: UTF-8 -*- """ @author:andrew @file:RespCoarseAlign.py @email:admin@marques22.com @email:2021022362@m.scnu.edu.cn @time:2023/09/20 """ import sys from pathlib import Path import mne import numpy as np import pandas as pd from PySide6.QtWidgets import QApplication, QMainWindow, QFileDialog, QMessageBox from PySide6.QtCore import QFile from scipy import signal from scipy.signal import butter from ui.Mian import Ui_mainWindow as Ui_respCoarseAlign from ui.setings import Ui_MainWindow as Ui_Setting import yaml Conf = { "PSGConfig": { "Path": "./Data/PSG/", "Frequency": 100, "THOChannel": { "auto": True, "Channel": 3, }, "ABDChannel": { "auto": True, "Channel": 4, }, }, "XXConfig": { "Path": "./Data/XX/", "Frequency": 100, }, "RespFilterConfig": { "LowPass": 0.01, "HighPass": 0.7, "order": 4 }, "ApplyFrequency": 5 } class SettingWindow(QMainWindow): def __init__(self): super(SettingWindow, self).__init__() self.ui = Ui_Setting() self.ui.setupUi(self) self.__read_settings__() def __read_settings__(self): with open("./config.yaml", "r") as f: fileConfig = yaml.load(f.read(), Loader=yaml.FullLoader) Conf.update(fileConfig) # print(Conf) self.ui.lineEdit_PSGFilePath.setText(Conf["PSGConfig"]["Path"]) self.ui.lineEdit_XXFilePath.setText(Conf["XXConfig"]["Path"]) self.ui.spinBox_PSGDefaultFreq.setValue(Conf["PSGConfig"]["Frequency"]) self.ui.spinBox_XXDefaultFreq.setValue(Conf["XXConfig"]["Frequency"]) self.ui.QSpinBox_ApplyFre.setValue(Conf["ApplyFrequency"]) autoTHO = Conf["PSGConfig"]["THOChannel"]["auto"] self.ui.checkBox_THOautoChannel.setChecked(2 if autoTHO else 0) autoABD = Conf["PSGConfig"]["ABDChannel"]["auto"] self.ui.checkBox_ABDautoChannel.setChecked(2 if autoABD else 0) self.ui.spinBox_THOcustomChannel.setValue(Conf["PSGConfig"]["THOChannel"]["Channel"]) self.ui.spinBox_ABDcustomChannel.setValue(Conf["PSGConfig"]["ABDChannel"]["Channel"]) self.ui.doubleSpinBox_ButterLowPassFreq.setValue(Conf["RespFilterConfig"]["LowCut"]) self.ui.doubleSpinBox_ButterHighPassFreq.setValue(Conf["RespFilterConfig"]["HighCut"]) self.ui.spinBox_ButterOrder.setValue(Conf["RespFilterConfig"]["Order"]) self.ui.spinBox_THOcustomChannel.setEnabled(not autoTHO) self.ui.spinBox_ABDcustomChannel.setEnabled(not autoABD) # 绑定事件 self.ui.toolButton_PSGFilePath.clicked.connect(self.__select_file__) self.ui.toolButton_XXFilePath.clicked.connect(self.__select_file__) self.ui.pushButton_SaveConfig.clicked.connect(self.__write_settings__) self.ui.pushButton_Cancel.clicked.connect(self.close) # ABD auto checkbox和SpinBox 互斥 self.ui.checkBox_ABDautoChannel.stateChanged.connect(self.__ABDAutoChannel__) self.ui.checkBox_THOautoChannel.stateChanged.connect(self.__THOAutoChannel__) def __ABDAutoChannel__(self, state): if state == 2: self.ui.spinBox_ABDcustomChannel.setEnabled(False) else: self.ui.spinBox_ABDcustomChannel.setEnabled(True) def __THOAutoChannel__(self, state): if state == 2: self.ui.spinBox_THOcustomChannel.setEnabled(False) else: self.ui.spinBox_THOcustomChannel.setEnabled(True) def __select_file__(self, event): sender = self.sender() if sender.objectName() == "toolButton_PSGFilePath": path = QFileDialog.getExistingDirectory(self, "选择PSG数据文件夹", "./Data/PSG/") self.ui.lineEdit_PSGFilePath.setText(path) elif sender.objectName() == "toolButton_XXFilePath": path = QFileDialog.getExistingDirectory(self, "选择XX数据文件夹", "./Data/XX/") self.ui.lineEdit_XXFilePath.setText(path) def __write_settings__(self): # 从界面读取配置 Conf["PSGConfig"]["Path"] = self.ui.lineEdit_PSGFilePath.text() Conf["XXConfig"]["Path"] = self.ui.lineEdit_XXFilePath.text() Conf["PSGConfig"]["Frequency"] = self.ui.spinBox_PSGDefaultFreq.value() Conf["XXConfig"]["Frequency"] = self.ui.spinBox_XXDefaultFreq.value() Conf["ApplyFrequency"] = self.ui.QSpinBox_ApplyFre.value() Conf["PSGConfig"]["THOChannel"]["auto"] = self.ui.checkBox_THOautoChannel.isChecked() Conf["PSGConfig"]["ABDChannel"]["auto"] = self.ui.checkBox_ABDautoChannel.isChecked() Conf["PSGConfig"]["THOChannel"]["Channel"] = self.ui.spinBox_THOcustomChannel.value() Conf["PSGConfig"]["ABDChannel"]["Channel"] = self.ui.spinBox_ABDcustomChannel.value() Conf["RespFilterConfig"]["LowCut"] = self.ui.doubleSpinBox_ButterLowPassFreq.value() Conf["RespFilterConfig"]["HighCut"] = self.ui.doubleSpinBox_ButterHighPassFreq.value() Conf["RespFilterConfig"]["Order"] = self.ui.spinBox_ButterOrder.value() with open("./config.yaml", "w") as f: yaml.dump(Conf, f) self.close() class Data: def __init__(self, PSGDataPath, XXDataPath, sampNo, Config): self.PSGDataPath = PSGDataPath / f"{sampNo}.edf" if (XXDataPath / f"{sampNo}.npy").exists(): self.XXDataPath = XXDataPath / f"{sampNo}.npy" elif (XXDataPath / f"{sampNo}.txt").exists(): self.XXDataPath = XXDataPath / f"{sampNo}.txt" else: self.XXDataPath = None self.Config = Config self.raw_THO = None self.raw_ABD = None self.raw_XX = None self.PSG_minutes = None self.XX_minutes = None def OpenFile(self): # 判断是edf还是npy或txt if self.PSGDataPath.suffix == ".edf": with mne.io.read_raw_edf(self.PSGDataPath) as PSG: # read THO ABD if self.Config["PSGConfig"]["THOChannel"]["auto"]: self.raw_THO = PSG.pick_channels(["THO"]).get_data()[0] else: self.raw_THO = PSG.pick_channels([self.Config["PSGConfig"]["THOChannel"]["Channel"]]).get_data()[0] if self.Config["PSGConfig"]["ABDChannel"]["auto"]: self.raw_ABD = PSG.pick_channels(["ABD"]).get_data()[0] else: self.raw_ABD = PSG.pick_channels([self.Config["PSGConfig"]["ABDChannel"]["Channel"]]).get_data()[0] else: return False, "PSG文件格式错误" if self.XXDataPath.suffix == ".npy": self.raw_XX = np.load(self.XXDataPath) elif self.XXDataPath.suffix == ".txt": self.raw_XX = pd.read_csv(self.XXDataPath, sep="\t", header=None).values else: return False, "XX文件格式错误" # 获取时长 self.PSG_minutes = self.raw_THO.shape[0] / self.Config["PSGConfig"]["Frequency"] / 60 self.XX_minutes = self.raw_XX.shape[0] / self.Config["XXConfig"]["Frequency"] / 60 return True, "读取成功" def __Filter__(self): def butter_bandpass_filter(data, lowCut, highCut, fs, order): low = lowCut / (fs * 0.5) high = highCut / (fs * 0.5) sos = signal.butter(order, [low, high], btype="bandpass", output='sos') return signal.sosfilt(sos, data) # 滤波 self.raw_THO = butter_bandpass_filter(self.raw_THO, self.Config["RespFilterConfig"]["LowCut"], self.Config["RespFilterConfig"]["HighCut"], self.Config["PSGConfig"]["Frequency"], self.Config["RespFilterConfig"]["Order"]) self.raw_ABD = butter_bandpass_filter(self.raw_ABD, self.Config["RespFilterConfig"]["LowCut"], self.Config["RespFilterConfig"]["HighCut"], self.Config["PSGConfig"]["Frequency"], self.Config["RespFilterConfig"]["Order"]) self.raw_XX = butter_bandpass_filter(self.raw_XX, self.Config["RespFilterConfig"]["LowCut"], self.Config["RespFilterConfig"]["HighCut"], self.Config["XXConfig"]["Frequency"], self.Config["RespFilterConfig"]["Order"]) def Standardize(self): if self.Config["RawSignal"]: # 重采样 self.raw_THO = signal.resample(self.raw_THO, int(self.PSG_minutes * 60 * self.Config["ApplyFrequency"])) self.raw_ABD = signal.resample(self.raw_ABD, int(self.PSG_minutes * 60 * self.Config["ApplyFrequency"])) self.raw_XX = signal.resample(self.raw_XX, int(self.XX_minutes * 60 * self.Config["ApplyFrequency"])) else: # 滤波 self.__Filter__() # 重采样 self.raw_THO = signal.resample(self.raw_THO, int(self.PSG_minutes * 60 * self.Config["ApplyFrequency"])) self.raw_ABD = signal.resample(self.raw_ABD, int(self.PSG_minutes * 60 * self.Config["ApplyFrequency"])) self.raw_XX = signal.resample(self.raw_XX, int(self.XX_minutes * 60 * self.Config["ApplyFrequency"])) # 判断是否去基线 # 判断是否标准化 class MainWindow(QMainWindow): def __init__(self): super(MainWindow, self).__init__() self.ui = Ui_respCoarseAlign() self.setting = SettingWindow() self.ui.setupUi(self) self.ui.actionDefault_Configuration.triggered.connect(self.setting.show) # checkbox custom 和SpinBox 互斥 self.ui.checkBox_custom.stateChanged.connect(self.__customChannel__) # 绑定事件 # 刷新键分别获取PSG和XX文件夹里面的数据,获取所有编号显示在下拉框,比对编号同时存在的可选,仅存在一个文件夹的编号不可选 self.ui.pushButton_Refresh.clicked.connect(self.__refresh__) self.ui.pushButton_OpenFile.clicked.connect(self.__openFile__) self.ui.pushButton_Standardize.clicked.connect(self.__standardize__) def __customChannel__(self, state): if state == 2: self.ui.spinBox_custom.setEnabled(True) else: self.ui.spinBox_custom.setEnabled(False) # 刷新键分别获取PSG和XX文件夹里面的数据,获取所有编号显示在下拉框,比对编号同时存在的可选,仅存在一个文件夹的编号不可选 def __refresh__(self): # 检查两文件夹是否存在 PSGPath = Path(self.setting.ui.lineEdit_PSGFilePath.text()) XXPath = Path(self.setting.ui.lineEdit_XXFilePath.text()) if not PSGPath.exists(): QMessageBox.warning(self, "警告", "PSG文件夹不存在") return if not XXPath.exists(): QMessageBox.warning(self, "警告", "XX文件夹不存在") return # 获取两文件夹下所有的txt和npy文件编号 PSGFiles = [file.stem for file in PSGPath.glob("*.edf")] XXFiles = [file.stem for file in XXPath.glob("*.txt")] + [file.stem for file in XXPath.glob("*.npy")] # 获取两文件夹下同时存在的编号 print(PSGFiles, XXFiles) Files = list(set(PSGFiles).intersection(set(XXFiles))) # 获取两文件夹下仅存在一个的编号 FilesOnlyInPSG = list(set(PSGFiles).difference(set(XXFiles))) FilesOnlyInXX = list(set(XXFiles).difference(set(PSGFiles))) print(Files, FilesOnlyInPSG, FilesOnlyInXX) # 均显示到下拉框 self.ui.comboBox_SelectFile.clear() self.ui.comboBox_SelectFile.addItems([file for file in Files]) self.ui.comboBox_SelectFile.addItems([file + " (仅PSG)" for file in FilesOnlyInPSG]) self.ui.comboBox_SelectFile.addItems([file + " (仅XX)" for file in FilesOnlyInXX]) self.ui.comboBox_SelectFile.setCurrentIndex(0) # # 仅存在一个文件夹的编号不可选 for file in FilesOnlyInPSG: self.ui.comboBox_SelectFile.model().item(FilesOnlyInPSG.index(file) + len(Files)).setEnabled(False) for file in FilesOnlyInXX: self.ui.comboBox_SelectFile.model().item( FilesOnlyInXX.index(file) + len(Files) + len(FilesOnlyInPSG)).setEnabled(False) def __openFile__(self): # 获取checkbox状态 self.data = Data(Path(self.setting.ui.lineEdit_PSGFilePath.text()), Path(self.setting.ui.lineEdit_XXFilePath.text()), self.ui.comboBox_SelectFile.currentText().split(" ")[0], Conf) opened, info = self.data.OpenFile() if not opened: QMessageBox.warning(self, "警告", info) self.ui.label_Info.setText(info) return else: self.ui.label_PSGmins.setText(str(self.data.PSG_minutes)) self.ui.label_XXmins.setText(str(self.data.XX_minutes)) self.ui.label_Info.setText(info) def __standardize__(self): Conf2 = self.data.Config.copy() Conf2["RawSignal"] = self.ui.checkBox_RawSignal.isChecked() Conf2["PSGConfig"].update({ "PSGDelBase": self.ui.checkBox_PSGDelBase.isChecked(), "PSGZScore": self.ui.checkBox_PSGZScore.isChecked(), "Frequency": self.ui.spinBox_PSGFreq.value(), }) Conf2["XXConfig"].update({ "XXDelBase": self.ui.checkBox_XXDelBase.isChecked(), "XXZScore": self.ui.checkBox_XXZScore.isChecked(), "Frequency": self.ui.spinBox_XXFreq.value(), }) self.data.Config = Conf2 self.data.Standardize() if __name__ == "__main__": app = QApplication(sys.argv) window = MainWindow() window.show() sys.exit(app.exec())