""" @author:Yosa @file:ecg_label_check.py @email:2023025086@m.scnu.edu.cn @time:2025/2/5 """ import sys from logging import NOTSET, getLogger, FileHandler, Formatter, StreamHandler, info, error, debug from time import time, strftime, localtime import pandas as pd import numpy as np from PyQt5 import QtGui from PyQt5.QtGui import QFont, QDoubleValidator, QIntValidator from matplotlib.ticker import FuncFormatter from numpy import append from matplotlib import use from matplotlib import pyplot as plt, gridspec from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg, NavigationToolbar2QT from datetime import datetime from pathlib import Path from PyQt5.QtCore import QCoreApplication, QTimer from PyQt5.QtWidgets import QFileDialog, QMainWindow, QMessageBox, QButtonGroup, QApplication, QTableWidgetItem, \ QLineEdit, QAction, QTableWidget, QDialog, QVBoxLayout, QLabel, QDialogButtonBox from scipy.signal import butter, filtfilt, find_peaks import append from MainWindow import Ui_MainWindow use("Qt5Agg") # 声明使用QT5 # 设置日志 logger = getLogger() logger.setLevel(NOTSET) realtime = strftime('%Y%m%d', localtime(time())) if not Path("logs").exists(): Path("logs").mkdir(exist_ok=True) fh = FileHandler(Path("logs") / (realtime + ".log"), mode='a') fh.setLevel(NOTSET) fh.setFormatter(Formatter("%(asctime)s: %(message)s")) logger.addHandler(fh) ch = StreamHandler() ch.setLevel(NOTSET) ch.setFormatter(Formatter("%(asctime)s: %(message)s")) logger.addHandler(ch) getLogger('matplotlib.font_manager').disabled = True info("------------------------------------") info("---------ecg_label_check.py---------") class MainWindow(QMainWindow, Ui_MainWindow): # 全局变量初始化 data1 = np.array([]) data1 = data1.astype(np.float64) data2 = np.array([]) data2 = data2.astype(np.float64) label1 = np.array([]) label1 = label1.astype(np.int64) label2 = np.array([]) label2 = label2.astype(np.int64) # 程序初始化操作 def __init__(self): super(MainWindow, self).__init__() self.setupUi(self) # 设置画框 self.figure = plt.figure(figsize=(12, 6), dpi=150) self.canvas = FigureCanvasQTAgg(self.figure) self.figToolbar = CustomNavigationToolbar(self.canvas, self) self.verticalLayout_canvas.addWidget(self.canvas) self.verticalLayout_canvas.addWidget(self.figToolbar) self.figToolbar._actions['home'].triggered.connect(self.toggle_home) self.figToolbar.action_Label_Single.triggered.connect(self.toggle_changeLabel_Single_mode) self.figToolbar.action_Label_Multiple.triggered.connect(self.toggle_changeLabel_Multiple_mode) # 画框子图初始化 self.gs = gridspec.GridSpec(2, 1, height_ratios=[1, 1]) self.figure.subplots_adjust(top=1, bottom=0, right=1, left=0, hspace=0, wspace=0) plt.margins(0, 0) plt.tight_layout() plt.xticks([]) plt.yticks([]) # 单选框组件分组 self.buttonGroup_inputMode = QButtonGroup() self.buttonGroup_inputMode.addButton(self.radioButton_inputMode_auto) self.buttonGroup_inputMode.addButton(self.radioButton_inputMode_manual) self.buttonGroup_data1_preprocess = QButtonGroup() self.buttonGroup_data1_preprocess.addButton(self.radioButton_data1_fillterMode) self.buttonGroup_data2_preprocess = QButtonGroup() self.buttonGroup_data2_preprocess.addButton(self.radioButton_data2_fillterMode) self.buttonGroup_move_preset = QButtonGroup() self.buttonGroup_move_preset.addButton(self.radioButton_move_preset_1) self.buttonGroup_move_preset.addButton(self.radioButton_move_preset_2) self.buttonGroup_move_preset.addButton(self.radioButton_move_preset_3) self.buttonGroup_move_preset.addButton(self.radioButton_move_custom) # 设置表格属性 self.tableWidget_label1.setHorizontalHeaderLabels(['label1']) self.tableWidget_label2.setHorizontalHeaderLabels(['label2']) self.tableWidget_label1.setEditTriggers(QTableWidget.NoEditTriggers) self.tableWidget_label2.setEditTriggers(QTableWidget.NoEditTriggers) self.tableWidget_label1.horizontalHeader().setStretchLastSection(True) self.tableWidget_label1.horizontalHeader().setSectionResizeMode(1) self.tableWidget_label2.horizontalHeader().setStretchLastSection(True) self.tableWidget_label2.horizontalHeader().setSectionResizeMode(1) # 槽函数连接控件初始化 self.pushButton_savepath_open.clicked.connect(self.slot_btn_selectPath) self.pushButton_rootpath_open.clicked.connect(self.slot_btn_selectPath) self.pushButton_data1path_open.clicked.connect(self.slot_btn_selectPath) self.pushButton_data2path_open.clicked.connect(self.slot_btn_selectPath) self.pushButton_label1path_open.clicked.connect(self.slot_btn_selectPath) self.pushButton_label2path_open.clicked.connect(self.slot_btn_selectPath) self.pushButton_dataInput.clicked.connect(self.slot_btn_dataInput) self.pushButton_append.clicked.connect(self.slot_btn_append) self.pushButton_outputLabel.clicked.connect(self.slot_btn_outputLabel) self.comboBox_choose.currentTextChanged.connect(self.slot_comboBox_textChanged) self.radioButton_inputMode_auto.clicked.connect(self.slot_radioBtn_inputMode_auto) self.radioButton_inputMode_manual.clicked.connect(self.slot_radioBtn_inputMode_manual) self.pushButton_left_move.clicked.connect(self.slot_btn_left_move) self.pushButton_pause.clicked.connect(self.slot_btn_pause) self.pushButton_right_move.clicked.connect(self.slot_btn_right_move) self.radioButton_move_preset_1.clicked.connect(self.slot_radioBtn_move_preset_1) self.radioButton_move_preset_2.clicked.connect(self.slot_radioBtn_move_preset_2) self.radioButton_move_preset_3.clicked.connect(self.slot_radioBtn_move_preset_3) self.radioButton_move_custom.clicked.connect(self.slot_radioBtn_move_custom) self.tableWidget_label1.cellDoubleClicked.connect(self.slot_tableWidget_1_on_cell_double_clicked) self.tableWidget_label2.cellDoubleClicked.connect(self.slot_tableWidget_2_on_cell_double_clicked) # 初始化按钮快捷键 self.pushButton_left_move.setShortcut(QCoreApplication.translate("MainWindow", "A")) self.pushButton_pause.setShortcut(QCoreApplication.translate("MainWindow", "S")) self.pushButton_right_move.setShortcut(QCoreApplication.translate("MainWindow", "D")) # 界面控件状态初始化 self.lineEdit_rootpath.setEnabled(False) self.lineEdit_savepath.setEnabled(False) self.lineEdit_data1path.setEnabled(False) self.lineEdit_data2path.setEnabled(False) self.lineEdit_label1path.setEnabled(False) self.lineEdit_label2path.setEnabled(False) self.label_choose.setEnabled(False) self.comboBox_choose.setEnabled(False) self.groupBox_save.setEnabled(False) self.groupBox_data1.setEnabled(False) self.groupBox_data2.setEnabled(False) self.groupBox_label1.setEnabled(False) self.groupBox_label2.setEnabled(False) self.groupBox_autoplay.setEnabled(False) self.groupBox_labelDisplay.setEnabled(False) self.radioButton_inputMode_auto.setChecked(True) self.radioButton_data1_fillterMode.setChecked(True) self.radioButton_data2_fillterMode.setChecked(True) self.radioButton_move_preset_1.setChecked(True) self.pushButton_outputLabel.setEnabled(False) self.figToolbar.action_Label_Single.setEnabled(False) self.figToolbar.action_Label_Multiple.setEnabled(False) for action in self.figToolbar._actions.values(): action.setEnabled(False) # 消息弹窗初始化 self.msgBox = QMessageBox() self.msgBox.setWindowTitle("消息") # 自动播放参数初始化 self.autoplay_mode = "pause" self.autoplay_moveLength = int(self.label_moveLength_preset_1.text()) self.autoplay_maxRange = int(self.label_maxRange_preset_1.text()) self.autoplay_moveSpeed = int(self.label_moveSpeed_preset_1.text()) # 初始化自动播放定时器 self.timer_autoplay = QTimer() self.timer_autoplay.timeout.connect(self.autoplay_move_xlim) # 绘图工具初始化 self.points_y = np.array([]) self.temp_point = None self.is_left_button_pressed = False self.is_right_button_pressed = False # 定义验证器,用于规范lineEdit的输入内容 validator_double = QDoubleValidator(-1e100, 1e100, 10) validator_integer = QIntValidator(-2**31, 2**31 - 1) self.lineEdit_data1_fillterNum.setValidator(validator_integer) self.lineEdit_data2_fillterNum.setValidator(validator_integer) self.lineEdit_data1_fillterFrequency_min.setValidator(validator_double) self.lineEdit_data1_fillterFrequency_max.setValidator(validator_double) self.lineEdit_data2_fillterFrequency_min.setValidator(validator_double) self.lineEdit_data2_fillterFrequency_max.setValidator(validator_double) self.lineEdit_frequency.setValidator(validator_double) self.lineEdit_moveLength.setValidator(validator_integer) self.lineEdit_maxRange.setValidator(validator_double) self.lineEdit_moveSpeed.setValidator(validator_integer) self.lineEdit_findpeaks_min_interval.setValidator(validator_integer) self.lineEdit_findpeaks_min_height.setValidator(validator_double) self.textBrowser_update("程序启动成功,导入数据以开始任务") info("Program Started.") def slot_btn_selectPath(self): fileDialog = QFileDialog() if self.sender() == self.pushButton_rootpath_open: fileDialog.setFileMode(QFileDialog.Directory) fileDialog.setOption(QFileDialog.ShowDirsOnly, True) if fileDialog.exec_() == QFileDialog.Accepted: file_path = fileDialog.selectedFiles()[0] if not file_path: error("Root Path not Exist...") self.textBrowser_update("操作:根目录路径输入错误") self.msgBox.setText("根目录路径输入错误") self.msgBox.setIcon(QMessageBox.Critical) self.msgBox.exec() return self.lineEdit_rootpath.setText(file_path) file_path = Path(file_path) info("Loading Root Path...") self.data_and_label_Path = file_path / "label" if self.data_and_label_Path.exists(): # 检查路径下的hecg.txt和hRpeak.txt文件是否一一对应 data_files = sorted(self.data_and_label_Path.glob('*ecg.txt')) label_files = sorted(self.data_and_label_Path.glob('*Rpeak.txt')) if len(data_files) != len(label_files): error("Data Quantity not Match...") self.textBrowser_update(f"操作:数据导入错误,ecg.txt和Rpeak.txt文件数量不匹配,分别是{str(len(data_files))}和{str(len(label_files))}") self.msgBox.setText(f"数据导入错误,ecg.txt和Rpeak.txt文件数量不匹配,分别是{str(len(data_files))}和{str(len(label_files))}") self.msgBox.setIcon(QMessageBox.Critical) self.msgBox.exec() return for data_file, label_file in zip(data_files, label_files): if data_file.stem.replace('ecg', '') != label_file.stem.replace('Rpeak', ''): self.textBrowser_update(f"操作:数据导入错误,文件名称存在不匹配,分别是{str(data_file.stem)}和{str(label_file.stem)}") self.msgBox.setText(f"操作:数据导入错误,文件名称存在不匹配,分别是{str(data_file.stem)}和{str(label_file.stem)}") self.msgBox.setIcon(QMessageBox.Critical) self.msgBox.exec() return nums_for_comboBox = np.array([]) for data_file in data_files: prefix = data_file.stem.replace('ecg', '') nums_for_comboBox = np.append(nums_for_comboBox, prefix) nums_for_comboBox = nums_for_comboBox.astype(np.int8) nums_for_comboBox.sort() nums_for_comboBox = nums_for_comboBox.astype(str) self.comboBox_choose.clear() self.comboBox_choose.addItems(nums_for_comboBox) self.label_choose.setEnabled(True) self.comboBox_choose.setEnabled(True) self.groupBox_save.setEnabled(True) self.groupBox_data1.setEnabled(True) self.groupBox_data2.setEnabled(True) self.groupBox_label1.setEnabled(True) self.groupBox_label2.setEnabled(True) # 更新数据路径文本框 self.lineEdit_savepath.setText( str(self.data_and_label_Path) + "\\" + self.comboBox_choose.currentText() + "location_J.txt") self.lineEdit_data1path.setText( str(self.data_and_label_Path) + "\\" + self.comboBox_choose.currentText() + "ecg.txt") self.lineEdit_data2path.setText( str(self.data_and_label_Path) + "\\" + self.comboBox_choose.currentText() + "ecg.txt") self.lineEdit_label1path.setText( str(self.data_and_label_Path) + "\\" + self.comboBox_choose.currentText() + "Rpeak.txt") self.lineEdit_label2path.setText( str(self.data_and_label_Path) + "\\" + self.comboBox_choose.currentText() + "Rpeak.txt") info("Successfully Loaded Root Path.") self.textBrowser_update("操作:根目录路径选择成功") else: info("Failed to Load Root Path.") self.textBrowser_update("操作:根目录路径选择错误,缺乏必要数据文件夹") self.msgBox.setText("根目录路径选择错误,缺乏必要数据文件夹") self.msgBox.setIcon(QMessageBox.Critical) self.msgBox.exec() return else: info("Canceled Loading Root Path.") self.textBrowser_update("提示:根目录路径选择取消") self.msgBox.setText("根目录路径选择取消") self.msgBox.setIcon(QMessageBox.Warning) self.msgBox.exec() elif self.sender() == self.pushButton_savepath_open: fileDialog.setFileMode(QFileDialog.Directory) fileDialog.setOption(QFileDialog.ShowDirsOnly, True) if fileDialog.exec_() == QFileDialog.Accepted: file_path = fileDialog.selectedFiles()[0] self.lineEdit_savepath.setText(str(Path(file_path) / "newlocation_J.txt")) info("Successfully Loaded Save Path.") self.textBrowser_update("操作:保存路径选择成功") else: info("Canceled Loading Save Path.") self.textBrowser_update("提示:保存路径选择取消") self.msgBox.setText("保存路径选择取消") self.msgBox.setIcon(QMessageBox.Warning) self.msgBox.exec() else: fileDialog.setFileMode(QFileDialog.ExistingFile) fileDialog.setOption(QFileDialog.ReadOnly, True) fileDialog.setNameFilter("Text Files (*.txt)") if fileDialog.exec_() == QFileDialog.Accepted: file_path = fileDialog.selectedFiles()[0] if self.sender() == self.pushButton_data1path_open: self.lineEdit_data1path.setText(file_path) elif self.sender() == self.pushButton_data2path_open: self.lineEdit_data2path.setText(file_path) elif self.sender() == self.pushButton_label1path_open: self.lineEdit_label1path.setText(file_path) elif self.sender() == self.pushButton_label2path_open: self.lineEdit_label2path.setText(file_path) info("Successfully Loaded Data Path.") self.textBrowser_update("操作:数据路径选择成功") else: info("Canceled Loading Data Path.") self.textBrowser_update("提示:数据路径选择取消") self.msgBox.setText("数据路径选择取消") self.msgBox.setIcon(QMessageBox.Warning) self.msgBox.exec() def slot_btn_dataInput(self): if self.lineEdit_data1path.text() != "" or self.lineEdit_data2path.text() != "" or self.lineEdit_label1path.text() != "" or self.lineEdit_label2path.text() != "" or self.lineEdit_savepath.text() != "": info("Inputing Data...") self.textBrowser_update("提示:开始导入数据") # 导入数据 self.label1 = np.array([]) self.label2 = np.array([]) file = open(str(self.lineEdit_data1path.text()), 'r') self.data1 = file.readlines() self.data1 = list(map(float, self.data1)) self.data2 = self.data1 self.data1 = self.data_preprocess(self.data1, int(self.lineEdit_data1_fillterNum.text()), float(self.lineEdit_data1_fillterFrequency_min.text()), float(self.lineEdit_data1_fillterFrequency_max.text()), float(self.lineEdit_frequency.text())) self.data2 = self.data_preprocess(self.data2, int(self.lineEdit_data2_fillterNum.text()), float(self.lineEdit_data2_fillterFrequency_min.text()), float(self.lineEdit_data2_fillterFrequency_max.text()), float(self.lineEdit_frequency.text())) file = open(str(self.lineEdit_label1path.text()), 'r') self.label1 = np.array(file.readlines()) self.label2 = self.label1 self.data1 = self.data1.astype(np.float64) self.data2 = self.data2.astype(np.float64) self.label1 = self.label1.astype(np.int64) self.label2 = self.label2.astype(np.int64) self.points_y_1 = [self.data1[x] for x in self.label1] self.points_y_2 = [self.data2[x] for x in self.label2] # 更新tableWidget self.tableWidget_label1.setRowCount(len(self.label1)) for row, value in enumerate(self.label1): item = QTableWidgetItem(str(value).strip()) self.tableWidget_label1.setItem(row, 0, item) self.tableWidget_label2.setRowCount(self.label2.__len__()) for row, value in enumerate(self.label2): item = QTableWidgetItem(str(value).strip()) self.tableWidget_label2.setItem(row, 0, item) self.groupBox_labelDisplay.setEnabled(True) # 更新界面 self.groupBox_inputSetting.setEnabled(False) self.groupBox_autoplay.setEnabled(True) self.pushButton_outputLabel.setEnabled(True) self.figToolbar.action_Label_Single.setEnabled(True) self.figToolbar.action_Label_Multiple.setEnabled(True) MainWindow.setWindowTitle(self, QCoreApplication.translate("MainWindow", "ECG_Label_Check - Data Path: " + self.lineEdit_rootpath.text())) for action in self.figToolbar._actions.values(): action.setEnabled(True) # 更新信息 self.label_data1_length.setText(str(len(self.data1))) self.label_data2_length.setText(str(len(self.data2))) self.label_label1_length.setText(str(len(self.label1))) self.label_label2_length.setText(str(len(self.label2))) # 更新画框 self.figure.clear() self.plot_data_and_label() # 连接画框中的槽函数 # Connect mouse events self.canvas.mpl_connect('motion_notify_event', self.on_motion) # 保存路径文件是否存在的检查 if not Path(self.lineEdit_savepath.text()).exists(): Path(self.lineEdit_savepath.text()).touch() np.savetxt(Path(self.lineEdit_savepath.text()), self.label2, fmt='%d', newline='\n') info("Finished Input Data.") self.textBrowser_update("提示:导入数据完成") else: info("Failed to Input Data!") self.textBrowser_update("操作:导入数据失败") self.msgBox.setText("导入失败,请正确输入路径") self.msgBox.setIcon(QMessageBox.Critical) self.msgBox.exec() def slot_radioBtn_inputMode_auto(self): self.pushButton_rootpath_open.setEnabled(True) self.label_choose.setEnabled(False) self.comboBox_choose.setEnabled(False) self.groupBox_save.setEnabled(False) self.groupBox_data1.setEnabled(False) self.groupBox_data2.setEnabled(False) self.groupBox_label1.setEnabled(False) self.groupBox_label2.setEnabled(False) self.lineEdit_savepath.setText("") self.lineEdit_rootpath.setText("") self.lineEdit_data1path.setText("") self.lineEdit_data2path.setText("") self.lineEdit_label1path.setText("") self.lineEdit_label2path.setText("") self.comboBox_choose.clear() info("Switched to inputMode_auto.") self.textBrowser_update("操作:切换到输入模式-自动识别") def slot_radioBtn_inputMode_manual(self): self.pushButton_rootpath_open.setEnabled(False) self.label_choose.setEnabled(False) self.comboBox_choose.setEnabled(False) self.groupBox_save.setEnabled(True) self.groupBox_data1.setEnabled(True) self.groupBox_data2.setEnabled(True) self.groupBox_label1.setEnabled(True) self.groupBox_label2.setEnabled(True) self.lineEdit_rootpath.setText("") self.comboBox_choose.clear() info("Switched to inputMode_manual.") self.textBrowser_update("操作:切换到输入模式-手动输入") def slot_comboBox_textChanged(self): # 更新数据路径文本框 self.lineEdit_savepath.setText( str(self.data_and_label_Path) + "\\" + self.comboBox_choose.currentText() + "location_J.txt") self.lineEdit_data1path.setText( str(self.data_and_label_Path) + "\\" + self.comboBox_choose.currentText() + "ecg.txt") self.lineEdit_data2path.setText( str(self.data_and_label_Path) + "\\" + self.comboBox_choose.currentText() + "ecg.txt") self.lineEdit_label1path.setText( str(self.data_and_label_Path) + "\\" + self.comboBox_choose.currentText() + "Rpeak.txt") self.lineEdit_label2path.setText( str(self.data_and_label_Path) + "\\" + self.comboBox_choose.currentText() + "Rpeak.txt") def slot_btn_left_move(self): self.autoplay_mode = "left" self.autoplay_xlim_start = int(self.ax0.get_xlim()[1] - self.autoplay_maxRange) self.autoplay_xlim_end = int(self.ax0.get_xlim()[1]) if self.autoplay_xlim_end > self.data2.__len__(): self.autoplay_xlim_start = int(self.data2.__len__() - self.autoplay_maxRange) self.autoplay_xlim_end = int(self.data2.__len__()) self.ax0.set_xlim(self.autoplay_xlim_start, self.autoplay_xlim_end) self.canvas.draw() self.timer_autoplay.start(self.autoplay_moveSpeed) info("Started Autoplay left_mode.") self.textBrowser_update("操作:开始自动播放-向左") def slot_btn_pause(self): self.autoplay_mode = "pause" self.timer_autoplay.stop() info("Paused Autoplay.") self.textBrowser_update("操作:暂停自动播放") def slot_btn_right_move(self): self.autoplay_mode = "right" self.autoplay_xlim_start = int(self.ax0.get_xlim()[0]) self.autoplay_xlim_end = int(self.ax0.get_xlim()[0] + self.autoplay_maxRange) if self.autoplay_xlim_start < 0: self.autoplay_xlim_start = 0 self.autoplay_xlim_end = 0 + self.autoplay_maxRange self.ax0.set_xlim(self.autoplay_xlim_start, self.autoplay_xlim_end) self.canvas.draw() self.timer_autoplay.start(self.autoplay_moveSpeed) info("Started Autoplay right_mode.") self.textBrowser_update("操作:开始自动播放-向右") def slot_radioBtn_move_preset_1(self): self.autoplay_moveLength = int(self.label_moveLength_preset_1.text()) self.autoplay_maxRange = int(self.label_maxRange_preset_1.text()) self.autoplay_moveSpeed = int(self.label_moveSpeed_preset_1.text()) if self.autoplay_mode != "pause": if self.autoplay_mode == "right": self.autoplay_xlim_start = int(self.ax0.get_xlim()[0]) self.autoplay_xlim_end = int(self.ax0.get_xlim()[0] + self.autoplay_maxRange) self.ax0.set_xlim(self.autoplay_xlim_start, self.autoplay_xlim_end) self.canvas.draw() elif self.autoplay_mode == "left": self.autoplay_xlim_start = int(self.ax0.get_xlim()[1] - self.autoplay_maxRange) self.autoplay_xlim_end = int(self.ax0.get_xlim()[1]) self.ax0.set_xlim(self.autoplay_xlim_start, self.autoplay_xlim_end) self.canvas.draw() self.timer_autoplay.start(self.autoplay_moveSpeed) info("Switched to Autoplay preset_1.") self.textBrowser_update("操作:切换到自动播放-预设1") def slot_radioBtn_move_preset_2(self): self.autoplay_moveLength = int(self.label_moveLength_preset_2.text()) self.autoplay_maxRange = int(self.label_maxRange_preset_2.text()) self.autoplay_moveSpeed = int(self.label_moveSpeed_preset_2.text()) if self.autoplay_mode != "pause": if self.autoplay_mode == "right": self.autoplay_xlim_start = int(self.ax0.get_xlim()[0]) self.autoplay_xlim_end = int(self.ax0.get_xlim()[0] + self.autoplay_maxRange) self.ax0.set_xlim(self.autoplay_xlim_start, self.autoplay_xlim_end) self.canvas.draw() elif self.autoplay_mode == "left": self.autoplay_xlim_start = int(self.ax0.get_xlim()[1] - self.autoplay_maxRange) self.autoplay_xlim_end = int(self.ax0.get_xlim()[1]) self.ax0.set_xlim(self.autoplay_xlim_start, self.autoplay_xlim_end) self.canvas.draw() self.timer_autoplay.start(self.autoplay_moveSpeed) info("Switched to Autoplay preset_2.") self.textBrowser_update("操作:切换到自动播放-预设2") def slot_radioBtn_move_preset_3(self): self.autoplay_moveLength = int(self.label_moveLength_preset_3.text()) self.autoplay_maxRange = int(self.label_maxRange_preset_3.text()) self.autoplay_moveSpeed = int(self.label_moveSpeed_preset_3.text()) if self.autoplay_mode != "pause": if self.autoplay_mode == "right": self.autoplay_xlim_start = int(self.ax0.get_xlim()[0]) self.autoplay_xlim_end = int(self.ax0.get_xlim()[0] + self.autoplay_maxRange) self.ax0.set_xlim(self.autoplay_xlim_start, self.autoplay_xlim_end) self.canvas.draw() elif self.autoplay_mode == "left": self.autoplay_xlim_start = int(self.ax0.get_xlim()[1] - self.autoplay_maxRange) self.autoplay_xlim_end = int(self.ax0.get_xlim()[1]) self.ax0.set_xlim(self.autoplay_xlim_start, self.autoplay_xlim_end) self.canvas.draw() self.timer_autoplay.start(self.autoplay_moveSpeed) info("Switched to Autoplay preset_3.") self.textBrowser_update("操作:切换到自动播放-预设3") def slot_radioBtn_move_custom(self): self.autoplay_moveLength = int(self.lineEdit_moveLength.text()) self.autoplay_maxRange = int(self.lineEdit_maxRange.text()) self.autoplay_moveSpeed = int(self.lineEdit_moveSpeed.text()) if self.autoplay_mode != "pause": if self.autoplay_mode == "right": self.autoplay_xlim_start = int(self.ax0.get_xlim()[0]) self.autoplay_xlim_end = int(self.ax0.get_xlim()[0] + self.autoplay_maxRange) self.ax0.set_xlim(self.autoplay_xlim_start, self.autoplay_xlim_end) self.canvas.draw() elif self.autoplay_mode == "left": self.autoplay_xlim_start = int(self.ax0.get_xlim()[1] - self.autoplay_maxRange) self.autoplay_xlim_end = int(self.ax0.get_xlim()[1]) self.ax0.set_xlim(self.autoplay_xlim_start, self.autoplay_xlim_end) self.canvas.draw() self.timer_autoplay.start(self.autoplay_moveSpeed) info("Switched to Autoplay custom.") self.textBrowser_update("操作:切换到自动播放-自定义") self.msgBox.setText("自定义的输入参数未做任何检查,请斟酌输入参数,否则可能会导致程序异常") self.msgBox.setIcon(QMessageBox.Warning) self.msgBox.exec() def slot_tableWidget_1_on_cell_double_clicked(self, row, column): x = float(self.tableWidget_label1.item(row, column).text()) self.ax0.set_xlim(x - 5000, x + 5000) self.annotation_tableWidget = self.ax0.annotate(f'x={int(x)}', xy=(int(x), self.ax0.get_ylim()[0]), xytext=(int(x), self.ax0.get_ylim()[0] + (self.ax0.get_ylim()[1] - self.ax0.get_ylim()[0]) * 0.1), arrowprops=dict(facecolor='black', shrink=0.1)) self.canvas.draw() info(f"Jumped to x_axis: {str(int(x))}.") self.textBrowser_update(f"操作:跳转到x坐标: {str(int(x))}") def slot_tableWidget_2_on_cell_double_clicked(self, row, column): x = float(self.tableWidget_label2.item(row, column).text()) self.ax0.set_xlim(x - 5000, x + 5000) self.annotation_tableWidget = self.ax0.annotate(f'x={int(x)}', xy=(int(x), self.ax0.get_ylim()[0]), xytext=(int(x), self.ax0.get_ylim()[0] + (self.ax0.get_ylim()[1] - self.ax0.get_ylim()[0]) * 0.1), arrowprops=dict(facecolor='black', shrink=0.1)) self.canvas.draw() info(f"Jumped to x_axis: {str(int(x))}.") self.textBrowser_update(f"操作:跳转到x坐标: {str(int(x))}") def slot_btn_append(self): dialog = CustomMessageBox(self) reply = dialog.exec_() if reply == QDialog.Accepted: if self.lineEdit_rootpath.text() == "" or len(list((Path(self.lineEdit_rootpath.text()) / "label").rglob("*location_J.txt"))) == 0: info(f"*location_J.txt Files not Exist in the Directory.") self.textBrowser_update(f"错误:无法进行<片段合并>") self.msgBox.setText(f"目录下不存在*location_J.txt文件") self.msgBox.setIcon(QMessageBox.Warning) self.msgBox.exec() return final_Rpeak = append.Rpeak_Detection(self.lineEdit_rootpath.text(), int(dialog.lineEdit_fs.text()), int(dialog.lineEdit_th1.text())) pd.DataFrame(final_Rpeak).to_csv(Path(self.lineEdit_rootpath.text()) / "final_Rpeak.txt", index=False, header=None) info(f"Saved final_Rpeak to Directory {self.lineEdit_rootpath.text()}.") self.textBrowser_update(f"提示:保存final_Rpeak.txt成功至文件夹{self.lineEdit_rootpath.text()}") self.msgBox.setText(f"保存成功至{self.lineEdit_rootpath.text()}") self.msgBox.setIcon(QMessageBox.Information) self.msgBox.exec() else: self.textBrowser_update("提示:操作取消") def slot_btn_outputLabel(self): np.savetxt(Path(self.lineEdit_savepath.text()), self.label2, fmt='%d', newline='\n') info(f"Manually Saved Data to: {str(Path(self.lineEdit_savepath.text()))}.") self.textBrowser_update(f"操作:手动保存数据至{str(Path(self.lineEdit_savepath.text()))}") def data_preprocess(self, data, n, f1, f2, fs): f1 = f1 / (fs / 2.0) f2 = f2 / (fs / 2.0) b, a = butter(n, [f1, f2], btype = 'bandpass') data = np.array(filtfilt(b, a, data)) return data def update_tableWidget_and_info(self): self.tableWidget_label2.setRowCount(self.label2.__len__()) for row, value in enumerate(self.label2): item = QTableWidgetItem(str(value).strip()) self.tableWidget_label2.setItem(row, 0, item) self.groupBox_labelDisplay.setEnabled(True) self.label_label2_length.setText(str(len(self.label2))) def textBrowser_update(self, message): self.textBrowser_infoOutput.append(str(datetime.now().strftime("%H:%M:%S")) + ": " + message) self.textBrowser_infoOutput.verticalScrollBar().setValue(self.textBrowser_infoOutput.verticalScrollBar().maximum()) def autoplay_move_xlim(self): if self.autoplay_mode == "left" and self.autoplay_xlim_start < 0: self.autoplay_mode = "pause" self.timer_autoplay.stop() info("Paused Autoplay.") self.textBrowser_update("操作:暂停自动播放") elif self.autoplay_mode == "right" and self.autoplay_xlim_end > self.data2.__len__(): self.autoplay_mode = "pause" self.timer_autoplay.stop() info("Paused Autoplay.") self.textBrowser_update("操作:暂停自动播放") else: if self.autoplay_mode == "right": self.autoplay_xlim_start += self.autoplay_moveLength self.autoplay_xlim_end += self.autoplay_moveLength elif self.autoplay_mode == "left": self.autoplay_xlim_start -= self.autoplay_moveLength self.autoplay_xlim_end -= self.autoplay_moveLength self.ax0.set_xlim(self.autoplay_xlim_start, self.autoplay_xlim_end) self.canvas.draw() def plot_data_and_label(self, ax_top=True, ax_bottom=True, ax_left=True, ax_right=True): self.ax0 = self.figure.add_subplot(self.gs[0]) self.ax0.plot(self.data1, label="data1", color='b') self.ax0.plot(self.label1, self.points_y_1, 'ro', label='label1') self.ax0 = plt.gca() self.ax0.grid(True) self.ax0.spines["top"].set_visible(ax_top) self.ax0.spines["right"].set_visible(ax_right) self.ax0.spines["bottom"].set_visible(ax_bottom) self.ax0.spines["left"].set_visible(ax_left) self.ax0.tick_params(axis='x', colors='white') self.ax0.xaxis.set_major_formatter(FuncFormatter(lambda x, p: f"{x:.0f}")) self.ax1 = self.figure.add_subplot(self.gs[1], sharex=self.ax0, sharey=self.ax0) self.line_data2, = self.ax1.plot(self.data2, label="data2", color='b') self.point_label2, = self.ax1.plot(self.label2, self.points_y_2, 'ro', label='label2') self.ax1 = plt.gca() self.ax1.grid(True) self.ax1.spines["top"].set_visible(ax_top) self.ax1.spines["right"].set_visible(ax_right) self.ax1.spines["bottom"].set_visible(ax_bottom) self.ax1.spines["left"].set_visible(ax_left) self.ax0.autoscale(False) self.ax0.callbacks.connect('xlim_changed', lambda ax: self.on_xlim_change(ax)) def toggle_home(self): if self.timer_autoplay.isActive() == True: self.autoplay_mode = "pause" self.timer_autoplay.stop() info("Paused Autoplay.") self.textBrowser_update("操作:暂停自动播放") self.ax0.autoscale(True) self.ax0.relim() self.ax0.autoscale_view() self.canvas.draw() self.ax0.autoscale(False) self.textBrowser_update("操作:尺度恢复") def toggle_changeLabel_Single_mode(self, state): if state: self.deactivate_figToolbar_buttons() self.figToolbar.action_Label_Single.setChecked(True) self.figToolbar.action_Label_Multiple.setChecked(False) self.figToolbar.cid_mouse_press = self.canvas.mpl_connect('button_press_event', self.on_click) self.figToolbar.cid_mouse_release = self.canvas.mpl_connect('button_release_event', self.on_release) else: if self.figToolbar.cid_mouse_press is not None: self.canvas.mpl_disconnect(self.figToolbar.cid_mouse_press) self.figToolbar.cid_mouse_press = None if self.figToolbar.cid_mouse_release is not None: self.canvas.mpl_disconnect(self.figToolbar.cid_mouse_release) self.figToolbar.cid_mouse_release = None def toggle_changeLabel_Multiple_mode(self, state): if state: self.deactivate_figToolbar_buttons() self.figToolbar.action_Label_Single.setChecked(False) self.figToolbar.action_Label_Multiple.setChecked(True) self.figToolbar.cid_mouse_press = self.canvas.mpl_connect('button_press_event', self.on_click) self.figToolbar.cid_mouse_release = self.canvas.mpl_connect('button_release_event', self.on_release) self.figToolbar.cid_mouse_hold = self.canvas.mpl_connect('motion_notify_event', self.on_hold) else: if self.figToolbar.cid_mouse_press is not None: self.canvas.mpl_disconnect(self.figToolbar.cid_mouse_press) self.figToolbar.cid_mouse_press = None if self.figToolbar.cid_mouse_release is not None: self.canvas.mpl_disconnect(self.figToolbar.cid_mouse_release) self.figToolbar.cid_mouse_release = None if self.figToolbar.cid_mouse_hold: self.canvas.mpl_disconnect(self.figToolbar.cid_mouse_hold) self.figToolbar.cid_mouse_hold = None def deactivate_figToolbar_buttons(self): for action in self.figToolbar._actions.values(): if action.isChecked() == True: if action == self.figToolbar._actions['pan']: self.figToolbar.pan() if action == self.figToolbar._actions['zoom']: self.figToolbar.zoom() def on_click(self, event): if self.figToolbar.action_Label_Single.isChecked(): if event.button == 1: self.is_left_button_pressed = True self.add_temp_point(event) elif event.button == 3: self.is_right_button_pressed = True self.remove_temp_point(event) elif self.figToolbar.action_Label_Multiple.isChecked(): if event.button == 1 or event.button == 3: # 左键或右键 if event.button == 1: self.is_left_button_pressed = True elif event.button == 3: self.is_right_button_pressed = True self.figToolbar.rect_start_x = event.xdata # 如果矩形patch已存在,先移除 if self.figToolbar.rect_patch is not None: self.figToolbar.rect_patch.remove() self.figToolbar.rect_patch = None self.canvas.draw() def on_release(self, event): if self.figToolbar.action_Label_Single.isChecked(): if event.button == 1 and self.is_left_button_pressed: self.is_left_button_pressed = False if self.temp_point: self.label2 = np.append(self.label2, self.temp_point[0]) self.points_y_2 = np.append(self.points_y_2, self.temp_point[1]) info(f"Added point: ({str(self.temp_point[0])},{str(self.temp_point[1])}).") self.textBrowser_update(f"操作:新增点:({str(self.temp_point[0])},{str(self.temp_point[1])})") self.temp_point = None self.redraw_plot() elif event.button == 3 and self.is_right_button_pressed: self.is_right_button_pressed = False if self.temp_point: distances = np.abs(np.array(self.label2) - self.temp_point[0]) if len(distances) > 0: idx = distances.argmin() info(f"Removed point {idx + 1}: ({str(self.label2[idx])},{str(self.points_y_2[idx])}).") self.textBrowser_update(f"操作:删除第{idx + 1}点:({str(self.label2[idx])},{str(self.points_y_2[idx])})") self.label2 = np.delete(self.label2, idx) self.points_y_2 = np.delete(self.points_y_2, idx) self.temp_point = None self.redraw_plot() self.label2.sort() self.points_y_2 = [self.data2[x] for x in self.label2] self.update_tableWidget_and_info() np.savetxt(Path(self.lineEdit_savepath.text()), self.label2, fmt='%d', newline='\n') elif self.figToolbar.action_Label_Multiple.isChecked(): if self.figToolbar.rect_start_x is not None: self.figToolbar.rect_end_x = event.xdata if self.figToolbar.rect_start_x is not None and self.figToolbar.rect_end_x is not None: if self.figToolbar.rect_start_x < self.figToolbar.rect_end_x: rect_left = self.figToolbar.rect_start_x rect_right = self.figToolbar.rect_end_x elif self.figToolbar.rect_start_x > self.figToolbar.rect_end_x: rect_left = self.figToolbar.rect_end_x rect_right = self.figToolbar.rect_start_x else: rect_left = self.figToolbar.rect_start_x rect_right = self.figToolbar.rect_start_x else: rect_left = self.figToolbar.rect_start_x rect_right = self.figToolbar.rect_start_x if event.button == 1 and self.is_left_button_pressed: self.is_left_button_pressed = False if rect_left < 0: rect_left = 0 if rect_right >= len(self.data2): rect_right = len(self.data2) - 1 selected_area_for_add_points = self.data2[int(rect_left):int(rect_right)] peaks_idx, _ = find_peaks(selected_area_for_add_points, height=float(self.lineEdit_findpeaks_min_height.text()), distance=float(self.lineEdit_findpeaks_min_interval.text())) peaks_idx = peaks_idx + int(rect_left) if len(peaks_idx) != 0: info(f"Added {len(peaks_idx)} points.") self.textBrowser_update(f"操作:新增{len(peaks_idx)}个点") else: info(f"No point added in selected area.") self.textBrowser_update(f"操作:所选区间内无新增点") self.label2 = np.append(self.label2, peaks_idx) self.points_y_2 = np.append(self.points_y_2, self.data2[peaks_idx]) self.redraw_plot() elif event.button == 3 and self.is_right_button_pressed: self.is_right_button_pressed = False left_label2_to_delete = self.label2 - rect_left right_label2_to_delete = self.label2 - rect_right self.left_label2_to_delete_idx = len(left_label2_to_delete[left_label2_to_delete < 0]) self.right_label2_to_delete_idx = len(right_label2_to_delete[right_label2_to_delete < 0]) if self.left_label2_to_delete_idx != self.right_label2_to_delete_idx: info(f"Removed points from {self.left_label2_to_delete_idx + 1} to {self.right_label2_to_delete_idx}.") self.textBrowser_update(f"操作:删除第{self.left_label2_to_delete_idx + 1}到{self.right_label2_to_delete_idx}点") else: info(f"No point to delete in selected area.") self.textBrowser_update(f"操作:所选区间内无删除点") self.label2 = np.delete(self.label2, np.arange(self.left_label2_to_delete_idx, self.right_label2_to_delete_idx)) self.points_y_2 = np.delete(self.points_y_2, np.arange(self.left_label2_to_delete_idx, self.right_label2_to_delete_idx)) self.redraw_plot() self.figToolbar.rect_start_x = None self.figToolbar.rect_end_x = None self.label2.sort() self.points_y_2 = [self.data2[x] for x in self.label2] self.update_tableWidget_and_info() np.savetxt(Path(self.lineEdit_savepath.text()), self.label2, fmt='%d', newline='\n') # 移除矩形patch if self.figToolbar.rect_patch is not None: self.figToolbar.rect_patch.remove() self.figToolbar.rect_patch = None self.canvas.draw() def on_hold(self, event): if self.figToolbar.rect_start_x is not None and event.xdata is not None: self.figToolbar.rect_end_x = event.xdata # 如果矩形patch不存在,则创建一个新的 if self.figToolbar.rect_patch is None: if self.is_left_button_pressed: self.figToolbar.rect_patch = plt.Rectangle((0, 0), 1, 1, fill=True, alpha=0.2, color='#ff00ff') elif self.is_right_button_pressed: self.figToolbar.rect_patch = plt.Rectangle((0, 0), 1, 1, fill=True, alpha=0.2, color='r') self.ax1.add_patch(self.figToolbar.rect_patch) # 更新矩形patch的位置和大小 x_start = self.figToolbar.rect_start_x x_end = self.figToolbar.rect_end_x y_min, y_max = self.ax1.get_ylim() self.figToolbar.rect_patch.set_xy((min(x_start, x_end), y_min)) self.figToolbar.rect_patch.set_width(abs(x_end - x_start)) self.figToolbar.rect_patch.set_height(y_max - y_min) self.canvas.draw() def on_motion(self, event): if event.inaxes: # Clear previous reference lines and temporary points for line in self.ax0.lines[1:]: if line.get_label() == 'vline' or line.get_label() == 'hline' or line.get_label() == 'temp_point': line.remove() for line in self.ax1.lines[1:]: if line.get_label() == 'vline' or line.get_label() == 'hline' or line.get_label() == 'temp_point': line.remove() # Draw vertical and horizontal reference lines self.ax0.axvline(event.xdata, color='gray', linestyle='--', label='vline') self.ax0.axhline(event.ydata, color='gray', linestyle='--', label='hline') self.ax1.axvline(event.xdata, color='gray', linestyle='--', label='vline') self.ax1.axhline(event.ydata, color='gray', linestyle='--', label='hline') # Draw temporary point if left button is pressed if self.is_left_button_pressed: self.add_temp_point(event) elif self.is_right_button_pressed: self.remove_temp_point(event) self.canvas.draw() def add_temp_point(self, event): if self.figToolbar.action_Label_Single.isChecked(): # Find the closest x value on the curve idx = np.abs(np.arange(len(self.data2)) - event.xdata).argmin() x_point = np.arange(len(self.data2))[idx] y_point = self.data2[idx] # Store the temporary point self.temp_point = (x_point, y_point) # Plot the temporary point self.ax1.plot(x_point, y_point, marker='o', color='#ff00ff', label='temp_point') self.ax1.plot(x_point, y_point, marker='x', color='#ff00ff', markersize=30, label='temp_point') def remove_temp_point(self, event): if self.figToolbar.action_Label_Single.isChecked(): if self.label2.any(): # Find the closest point to the current x coordinate distances = np.abs(np.array(self.label2) - event.xdata) idx = distances.argmin() x_point = self.label2[idx] y_point = self.points_y_2[idx] # Store the temporary point to be removed self.temp_point = (x_point, y_point) # Plot the temporary point to be removed self.ax1.plot(x_point, y_point, marker='x', color='r', markersize=30, label='temp_point') def redraw_plot(self): self.point_label2.remove() self.point_label2, = self.ax1.plot(self.label2, self.points_y_2, 'ro', label='label2') self.canvas.draw() def on_xlim_change(self, event_ax): try: if self.annotation_tableWidget: self.annotation_tableWidget.remove() except AttributeError: pass self.annotation_tableWidget = None self.canvas.draw() class CustomNavigationToolbar(NavigationToolbar2QT): def __init__(self, canvas, parent): super().__init__(canvas, parent) # 初始化画框工具栏 self.action_Label_Single = QAction('逐一更改标签(Z)', self) self.action_Label_Single.setFont(QFont("黑体", 14)) self.action_Label_Single.setCheckable(True) self.action_Label_Single.setShortcut(QCoreApplication.translate("MainWindow", "Z")) self.action_Label_Multiple = QAction('批量更改标签(X)', self) self.action_Label_Multiple.setFont(QFont("黑体", 14)) self.action_Label_Multiple.setCheckable(True) self.action_Label_Multiple.setShortcut(QCoreApplication.translate("MainWindow", "X")) self.insertAction(self._actions['pan'], self.action_Label_Single) self.insertAction(self._actions['pan'], self.action_Label_Multiple) self._actions['pan'].setShortcut(QCoreApplication.translate("MainWindow", "C")) # 用于存储事件连接ID self.cid_mouse_press = None self.cid_mouse_release = None self.cid_mouse_hold = None # 初始化矩形选择区域 self.rect_start_x = None self.rect_end_x = None self.rect_patch = None # 用于绘制矩形的patch def home(self, *args): pass def zoom(self, *args): super().zoom(*args) self.deactivate_figToorbar_changeLabel_mode() def pan(self, *args): super().pan(*args) self.deactivate_figToorbar_changeLabel_mode() def deactivate_figToorbar_changeLabel_mode(self): if self.action_Label_Single.isChecked(): self.action_Label_Single.setChecked(False) if self.cid_mouse_press is not None: self.canvas.mpl_disconnect(self.cid_mouse_press) self.cid_mouse_press = None if self.cid_mouse_release is not None: self.canvas.mpl_disconnect(self.cid_mouse_release) self.cid_mouse_release = None elif self.action_Label_Multiple.isChecked(): self.action_Label_Multiple.setChecked(False) if self.cid_mouse_press is not None: self.canvas.mpl_disconnect(self.cid_mouse_press) self.cid_mouse_press = None if self.cid_mouse_release is not None: self.canvas.mpl_disconnect(self.cid_mouse_release) self.cid_mouse_release = None if self.cid_mouse_hold is not None: self.canvas.mpl_disconnect(self.cid_mouse_hold) self.cid_mouse_hold = None class CustomMessageBox(QDialog): def __init__(self, parent=None): super().__init__(parent) self.resize(300, 300) self.setWindowTitle("警告:确认操作") layout = QVBoxLayout() self.label = QLabel("是否执行<片段合并>?请在执行前确保此份数据的所有片段都已被打标!请输入相应参数后执行任务。") self.label1 = QLabel(" ") self.label_fs = QLabel("信号采样率(Hz):") self.label_th1 = QLabel("寻峰阈值(个):") self.lineEdit_fs = QLineEdit() self.lineEdit_th1 = QLineEdit() font = QtGui.QFont() font.setFamily("黑体") font.setPointSize(14) self.label.setFont(font) self.label1.setFont(font) self.label_fs.setFont(font) self.label_th1.setFont(font) self.lineEdit_fs.setFont(font) self.lineEdit_th1.setFont(font) self.label.setWordWrap(True) self.label.setFixedWidth(300) self.lineEdit_fs.setText("1000") self.lineEdit_th1.setText("130") layout.addWidget(self.label) layout.addWidget(self.label1) layout.addWidget(self.label_fs) layout.addWidget(self.lineEdit_fs) layout.addWidget(self.label_th1) layout.addWidget(self.lineEdit_th1) self.button_box = QDialogButtonBox(QDialogButtonBox.Yes | QDialogButtonBox.No) self.button_box.setFont(font) self.button_box.accepted.connect(self.accept) self.button_box.rejected.connect(self.reject) layout.addWidget(self.button_box) self.setLayout(layout) # 主函数 if __name__ == '__main__': app = QApplication(sys.argv) mainWindow = MainWindow() mainWindow.show() sys.exit(app.exec_())