新增Stage同步路径配置,更新界面布局以支持Stage信号输入

This commit is contained in:
marques
2025-09-06 14:11:53 +08:00
parent 4abd6168cd
commit 704513e2bf
5 changed files with 237 additions and 171 deletions

View File

@ -106,6 +106,8 @@ class SettingWindow(QMainWindow):
self.ui.plainTextEdit_file_path_input_signal_SpO2.textChanged.connect(
self.__set_plaintext_font_color_by_path__)
self.ui.plainTextEdit_file_path_input_signal_Tho.textChanged.connect(self.__set_plaintext_font_color_by_path__)
self.ui.plainTextEdit_file_path_input_signal_Stage.textChanged.connect(
self.__set_plaintext_font_color_by_path__)
self.ui.pushButton_confirm.clicked.connect(self.__write_config__)
self.ui.pushButton_cancel.clicked.connect(self.__rollback_config__)
@ -126,12 +128,14 @@ class SettingWindow(QMainWindow):
self.params.update({
"Path": {
"Input_OrgBCG": sync_bcg_path,
"Input_Tho": sync_psg_path,
"Input_Abd": sync_psg_path,
"Input_FlowT": sync_psg_path,
"Input_FlowP": sync_psg_path,
"Input_SpO2": sync_psg_path,
"Input_Stage": sync_psg_path,
"Input_SA_Label": sync_psg_path / f"{Filename.SA_LABEL_SYNC}{Params.ENDSWITH_CSV}",
"Input_Artifact_A": label_path / f"{Filename.ARTIFACT_A}{Params.ENDSWITH_CSV}",
@ -145,6 +149,7 @@ class SettingWindow(QMainWindow):
def __write_config__(self):
# 从界面写入配置
self.params["Path"]["Input_Stage"] = self.ui.plainTextEdit_file_path_input_signal_Stage().toPlainText()
self.params["Path"]["Input_OrgBCG"] = self.ui.plainTextEdit_file_path_input_signal_OrgBCG.toPlainText()
self.params["Path"]["Input_Tho"] = self.ui.plainTextEdit_file_path_input_signal_Tho.toPlainText()
self.params["Path"]["Input_Abd"] = self.ui.plainTextEdit_file_path_input_signal_Abd.toPlainText()
@ -177,6 +182,7 @@ class SettingWindow(QMainWindow):
self.ui.spinBox_input_freq_signal_FlowP.setValue(self.params["Config"]["InputConfig"]["FlowPFreq"])
self.ui.spinBox_input_freq_signal_SpO2.setValue(self.params["Config"]["InputConfig"]["SpO2Freq"])
self.ui.plainTextEdit_file_path_input_signal_Stage.setPlainText(str(self.params["Path"]["Input_Stage"]))
self.ui.plainTextEdit_file_path_input_signal_OrgBCG.setPlainText(str(self.params["Path"]["Input_OrgBCG"]))
self.ui.plainTextEdit_file_path_input_signal_Tho.setPlainText(str(self.params["Path"]["Input_Tho"]))
self.ui.plainTextEdit_file_path_input_signal_Abd.setPlainText(str(self.params["Path"]["Input_Abd"]))
@ -189,7 +195,7 @@ class SettingWindow(QMainWindow):
# self.ui.plainTextEdit_file_path_save_2.setPlainText(str(self.params["Path"]["Save_2"]))
def __auto_find_file__(self):
check_signal_type_list = ["Input_OrgBCG", "Input_Tho", "Input_Abd", "Input_FlowT", "Input_FlowP", "Input_SpO2"]
check_signal_type_list = ["Input_OrgBCG", "Input_Tho", "Input_Abd", "Input_FlowT", "Input_FlowP", "Input_SpO2", "Input_Stage"]
def find_file(file_path: Path, _type, endswith):
if file_path.is_file():
@ -231,12 +237,14 @@ class SettingWindow(QMainWindow):
class Data:
def __init__(self, config):
self.config = config
self.Stage = None
self.OrgBCG = None
self.Tho = None
self.Abd = None
self.FlowT = None
self.FlowP = None
self.SpO2 = None
self.Stage_Resampled = None
self.lowPass20Hz = None
self.lowPassResp = None
self.OrgBCG_Resampled = None
@ -248,6 +256,7 @@ class Data:
self.lowPass20Hz_Resampled = None
self.lowPassResp_Resampled = None
self.channel = {
"Stage": None,
"orgdata": None,
"0.7lowpass_resp": None,
"Effort Tho": None,
@ -264,6 +273,14 @@ class Data:
"Mixed apnea": 4,
}
self.stage_to_value = {
"W": 5,
"N1": 3,
"N2": 2,
"N3": 1,
"R": 4,
}
self.event_label_origin = None
self.event_label_revised = None
self.event_index_origin = None
@ -277,7 +294,7 @@ class Data:
def open_file(self):
# 仅判断文件是否存在
check_file_list = ["Input_OrgBCG", "Input_Tho", "Input_Abd", "Input_FlowT", "Input_FlowP", "Input_SpO2",
"Input_SA_Label"]
"Input_SA_Label", "Input_Stage"]
for file_key in check_file_list:
if (not self.config["Path"][file_key].is_file()) or (not self.config["Path"][file_key].exists()):
@ -286,6 +303,8 @@ class Data:
Constants.FAILURE_REASON["Path_Not_Exist"])
try:
self.Stage = read_csv(self.config["Path"]["Input_Stage"], encoding=Params.UTF8_ENCODING,
header=None).to_numpy().reshape(-1)
self.OrgBCG = read_csv(self.config["Path"]["Input_OrgBCG"], encoding=Params.UTF8_ENCODING,
header=None).to_numpy().reshape(-1)
self.Tho = read_csv(self.config["Path"]["Input_Tho"], encoding=Params.UTF8_ENCODING,
@ -299,6 +318,7 @@ class Data:
self.SpO2 = read_csv(self.config["Path"]["Input_SpO2"], encoding=Params.UTF8_ENCODING,
header=None).to_numpy().reshape(-1)
if self.config["Path"]["Input_Artifact_A"].exists() and self.config["Path"]["Input_Artifact_A"].is_file():
self.Artifact = read_csv(self.config["Path"]["Input_Artifact_A"],
encoding=Params.UTF8_ENCODING,
@ -324,6 +344,12 @@ class Data:
"SignalSecond": int(len(self.OrgBCG) // self.config["Config"]["InputConfig"]["OrgBCGFreq"])
})
# 批量将睡眠分期按照映射转换为数字
for stage_str, stage_val in self.stage_to_value.items():
place(self.Stage, self.Stage == stage_str, stage_val)
self.Stage = self.Stage.astype(int)
# 根据秒数对信号截断
self.OrgBCG = self.OrgBCG[:self.config["SignalSecond"] * self.config["Config"]["InputConfig"]["OrgBCGFreq"]]
self.Tho = self.Tho[:self.config["SignalSecond"] * self.config["Config"]["InputConfig"]["ThoFreq"]]
@ -331,10 +357,14 @@ class Data:
self.FlowT = self.FlowT[:self.config["SignalSecond"] * self.config["Config"]["InputConfig"]["FlowTFreq"]]
self.FlowP = self.FlowP[:self.config["SignalSecond"] * self.config["Config"]["InputConfig"]["FlowPFreq"]]
self.SpO2 = self.SpO2[:self.config["SignalSecond"] * self.config["Config"]["InputConfig"]["SpO2Freq"]]
self.event_label_origin = zeros(len(self.OrgBCG))
self.event_label_revised = zeros(len(self.OrgBCG))
self.event_index_origin = zeros(len(self.OrgBCG))
self.event_index_revised = zeros(len(self.OrgBCG))
self.Stage = self.Stage[:self.config["SignalSecond"]]
plot_freq = self.config["Config"]["InputConfig"]["PlotFreq"]
self.event_label_origin = zeros(self.config["SignalSecond"] * plot_freq)
self.event_label_revised = zeros(self.config["SignalSecond"] * plot_freq)
self.event_index_origin = zeros(self.config["SignalSecond"])
self.event_index_revised = zeros(self.config["SignalSecond"])
return Result().success(info=Constants.INPUT_FINISHED)
@ -474,6 +504,7 @@ class Data:
plot_freq)
self.SpO2_Resampled = _resample_signal(self.SpO2, self.config["Config"]["InputConfig"]["SpO2Freq"],
plot_freq)
self.Stage_Resampled = _resample_signal(self.Stage, 1, plot_freq)
self.channel["orgdata"] = self.lowPass20Hz_Resampled
self.channel["0.7lowpass_resp"] = self.lowPassResp_Resampled
@ -482,6 +513,17 @@ class Data:
self.channel["Flow T"] = self.FlowT_Resampled
self.channel["Flow P"] = self.FlowP_Resampled
self.channel["SpO2"] = self.SpO2_Resampled
self.channel["Stage"] = self.Stage_Resampled
# 从内存中删除原始信号
del self.OrgBCG
del self.Tho
del self.Abd
del self.FlowT
del self.FlowP
del self.SpO2
del self.Stage
except Exception as e:
return Result().failure(info=Constants.RESAMPLE_FAILURE +
Constants.FAILURE_REASON["Resample_Exception"] + "\n" + format_exc())
@ -716,7 +758,7 @@ class MainWindow_SA_label(QMainWindow):
self.ui.verticalLayout_canvas.addWidget(self.canvas)
self.ui.verticalLayout_canvas.addWidget(self.figToolbar)
self.gs = gridspec.GridSpec(7, 1, height_ratios=[1, 1, 1, 1, 1, 3, 2])
self.gs = gridspec.GridSpec(8, 1, height_ratios=[1, 1, 1, 1, 1, 3, 2, 1])
self.fig.subplots_adjust(top=0.98, bottom=0.05, right=0.98, left=0.1, hspace=0, wspace=0)
self.ax0 = self.fig.add_subplot(self.gs[0])
self.ax0.grid(True)
@ -746,6 +788,9 @@ class MainWindow_SA_label(QMainWindow):
self.ax6 = self.fig.add_subplot(self.gs[6], sharex=self.ax0)
self.ax6.grid(True)
self.ax6.xaxis.set_major_formatter(Params.FORMATTER)
self.ax7 = self.fig.add_subplot(self.gs[7], sharex=self.ax0)
self.ax7.grid(True)
self.ax7.xaxis.set_major_formatter(Params.FORMATTER)
self.channel_to_ax = {
"SpO2": self.ax0,
@ -755,10 +800,12 @@ class MainWindow_SA_label(QMainWindow):
"Effort Abd": self.ax4,
"0.7lowpass_resp": self.ax5,
"orgdata": self.ax6,
"Stage": self.ax7,
}
self.channel_to_best_fit_checkbox = {
"SpO2": None,
"Stage": None,
"Flow T": self.ui.checkBox_best_flow,
"Flow P": self.ui.checkBox_best_flow,
"Effort Tho": self.ui.checkBox_best_effort,
@ -815,6 +862,9 @@ class MainWindow_SA_label(QMainWindow):
self.ui.pushButton_confirmLabel.clicked.connect(self.__slot_btn_confirmLabel__)
self.ui.pushButton_reset_event.clicked.connect(self.__reset_event__)
self.ui.pushButton_next_half.setProperty("offset", int(self.ui.comboBox_window_signal_length.lineEdit().text()) // 2)
self.ui.pushButton_previous_half.setProperty("offset", -int(self.ui.comboBox_window_signal_length.lineEdit().text()) // 2)
# 输入防抖
self.debounce_timer1 = QTimer()
self.debounce_timer1.setSingleShot(True)
@ -910,14 +960,19 @@ class MainWindow_SA_label(QMainWindow):
start_point = self.config["WindowStartSecond"] * plot_freq
end_point = start_point + self.config["WindowSignalSecond"] * plot_freq
for channel, ax in self.channel_to_ax.items():
if self.channel_to_best_fit_checkbox[channel] is not None and (
if (self.channel_to_best_fit_checkbox[channel] is not None) and (
not self.channel_to_best_fit_checkbox[channel].isChecked()):
continue
signal_max = self.data.channel[channel][start_point: end_point].max()
signal_min = self.data.channel[channel][start_point: end_point].min()
if channel == "SpO2":
signal_max = 100
signal_min = min(signal_min, 90)
elif channel == "Stage":
continue
else:
# 上限上移2%下限下移2%
delta = abs(signal_max - signal_min) * 0.02
@ -1700,37 +1755,37 @@ class MainWindow_SA_label(QMainWindow):
self.data = Data(self.config)
# 导入数据
PublicFunc.progressbar_update(self, 1, 9, Constants.INPUTTING_DATA, 0)
PublicFunc.progressbar_update(self, 1, 8, Constants.INPUTTING_DATA, 0)
result = self.data.open_file()
if not result.status:
PublicFunc.text_output(self.ui, "(1/9)" + result.info, Constants.TIPS_TYPE_ERROR)
PublicFunc.text_output(self.ui, "(1/8)" + result.info, Constants.TIPS_TYPE_ERROR)
PublicFunc.msgbox_output(self, result.info, Constants.MSGBOX_TYPE_ERROR)
PublicFunc.finish_operation(self, ButtonState)
return
else:
PublicFunc.text_output(self.ui, "(1/9)" + result.info, Constants.TIPS_TYPE_INFO)
PublicFunc.text_output(self.ui, "(1/8)" + result.info, Constants.TIPS_TYPE_INFO)
# 获取存档
PublicFunc.progressbar_update(self, 2, 9, Constants.LOADING_ARCHIVE, 15)
PublicFunc.progressbar_update(self, 2, 8, Constants.LOADING_ARCHIVE, 15)
result = self.data.get_archive()
if not result.status:
PublicFunc.text_output(self.ui, "(2/9)" + result.info, Constants.TIPS_TYPE_ERROR)
PublicFunc.text_output(self.ui, "(2/8)" + result.info, Constants.TIPS_TYPE_ERROR)
PublicFunc.msgbox_output(self, result.info, Constants.MSGBOX_TYPE_ERROR)
PublicFunc.finish_operation(self, ButtonState)
return
else:
PublicFunc.text_output(self.ui, "(2/9)" + result.info, Constants.TIPS_TYPE_INFO)
PublicFunc.text_output(self.ui, "(2/8)" + result.info, Constants.TIPS_TYPE_INFO)
# 保存
PublicFunc.progressbar_update(self, 3, 9, Constants.SAVING_DATA, 20)
PublicFunc.progressbar_update(self, 3, 8, Constants.SAVING_DATA, 20)
result = self.data.save()
if not result.status:
PublicFunc.text_output(self.ui, "(3/9)" + result.info, Constants.TIPS_TYPE_ERROR)
PublicFunc.text_output(self.ui, "(3/8)" + result.info, Constants.TIPS_TYPE_ERROR)
PublicFunc.msgbox_output(self, result.info, Constants.MSGBOX_TYPE_ERROR)
PublicFunc.finish_operation(self, ButtonState)
return
else:
PublicFunc.text_output(self.ui, "(3/9)" + result.info, Constants.TIPS_TYPE_INFO)
PublicFunc.text_output(self.ui, "(3/8)" + result.info, Constants.TIPS_TYPE_INFO)
# 保存
# PublicFunc.progressbar_update(self, 4, 9, Constants.SAVING_DATA, 25)
@ -1744,54 +1799,54 @@ class MainWindow_SA_label(QMainWindow):
# PublicFunc.text_output(self.ui, "(4/9)" + result.info, Constants.TIPS_TYPE_INFO)
# 数据预处理
PublicFunc.progressbar_update(self, 5, 9, Constants.PREPROCESSING_DATA, 30)
PublicFunc.progressbar_update(self, 4, 8, Constants.PREPROCESSING_DATA, 30)
result = self.data.preprocess()
if not result.status:
PublicFunc.text_output(self.ui, "(5/9)" + result.info, Constants.TIPS_TYPE_ERROR)
PublicFunc.text_output(self.ui, "(4/8)" + result.info, Constants.TIPS_TYPE_ERROR)
PublicFunc.msgbox_output(self, result.info, Constants.MSGBOX_TYPE_ERROR)
PublicFunc.finish_operation(self, ButtonState)
return
else:
PublicFunc.text_output(self.ui, "(5/9)" + result.info, Constants.TIPS_TYPE_INFO)
PublicFunc.text_output(self.ui, "(4/8)" + result.info, Constants.TIPS_TYPE_INFO)
# 重采样
PublicFunc.progressbar_update(self, 6, 9, Constants.RESAMPLING_DATA, 50)
PublicFunc.progressbar_update(self, 5, 8, Constants.RESAMPLING_DATA, 50)
result = self.data.resample()
for key, value in self.data.channel.items():
PublicFunc.text_output(self.ui, key + "重采样后的长度:" + str(len(value)), Constants.TIPS_TYPE_INFO)
if not result.status:
PublicFunc.text_output(self.ui, "(6/9)" + result.info, Constants.TIPS_TYPE_ERROR)
PublicFunc.text_output(self.ui, "(5/8)" + result.info, Constants.TIPS_TYPE_ERROR)
PublicFunc.msgbox_output(self, result.info, Constants.MSGBOX_TYPE_ERROR)
PublicFunc.finish_operation(self, ButtonState)
return
else:
PublicFunc.text_output(self.ui, "(6/9)" + result.info, Constants.TIPS_TYPE_INFO)
PublicFunc.text_output(self.ui, "(5/8)" + result.info, Constants.TIPS_TYPE_INFO)
# 绘图
PublicFunc.progressbar_update(self, 7, 9, Constants.DRAWING_DATA, 70)
PublicFunc.progressbar_update(self, 6, 8, Constants.DRAWING_DATA, 70)
# result = self.__plot__()
result = self.draw_signal()
if not result.status:
PublicFunc.text_output(self.ui, "(7/9)" + result.info, Constants.TIPS_TYPE_ERROR)
PublicFunc.text_output(self.ui, "(6/8)" + result.info, Constants.TIPS_TYPE_ERROR)
PublicFunc.msgbox_output(self, result.info, Constants.MSGBOX_TYPE_ERROR)
PublicFunc.finish_operation(self, ButtonState)
return
else:
PublicFunc.text_output(self.ui, "(7/9)" + result.info, Constants.TIPS_TYPE_INFO)
PublicFunc.text_output(self.ui, "(6/8)" + result.info, Constants.TIPS_TYPE_INFO)
# 更新表格
PublicFunc.progressbar_update(self, 8, 9, Constants.UPDATING_TABLEWIDGET, 90)
PublicFunc.progressbar_update(self, 7, 8, Constants.UPDATING_TABLEWIDGET, 90)
result = self.load_data_to_table()
if not result.status:
PublicFunc.text_output(self.ui, "(8/9)" + result.info, Constants.TIPS_TYPE_ERROR)
PublicFunc.text_output(self.ui, "(7/8)" + result.info, Constants.TIPS_TYPE_ERROR)
PublicFunc.msgbox_output(self, result.info, Constants.MSGBOX_TYPE_ERROR)
PublicFunc.finish_operation(self, ButtonState)
return
else:
PublicFunc.text_output(self.ui, "(8/9)" + result.info, Constants.TIPS_TYPE_INFO)
PublicFunc.text_output(self.ui, "(7/8)" + result.info, Constants.TIPS_TYPE_INFO)
# 更新信息
PublicFunc.progressbar_update(self, 9, 9, Constants.UPDATING_INFO, 95)
PublicFunc.progressbar_update(self, 8, 8, Constants.UPDATING_INFO, 95)
# result = self.update_UI_Args()
# if not result.status:
# PublicFunc.text_output(self.ui, "(9/9)" + result.info, Constants.TIPS_TYPE_ERROR)
@ -1931,6 +1986,7 @@ class MainWindow_SA_label(QMainWindow):
self.plt_interactive_event(channel="0.7lowpass_resp", label_df=self.data.df_revised)
self.plt_channel(channel="orgdata", start=0, end=self.config["SignalSecond"],
label_list=self.data.artifact_label, event_code=[6, 7, 8, 9, 10])
self.plt_channel(channel="Stage", start=0, end=self.config["SignalSecond"])
self.ax0.set_xlim(self.check_start_end(self.config["WindowStartSecond"], 0))
self.canvas.draw()
@ -1971,6 +2027,12 @@ class MainWindow_SA_label(QMainWindow):
place(y, y == 0, nan)
plt_.plot(linspace(start, end, (end - start) * plot_freq), y, color=self.color_cycle[j], linestyle="-")
if channel == "Stage":
plt_.set_yticks([1, 2, 3, 4, 5])
plt_.set_yticklabels(["N3", "N2", "N1", "REM", "Awake"])
plt_.set_ylim(0.5, 5.5)
plt_.legend(fontsize=8, loc=1)
def plt_interactive_event(self, channel, label_df):

View File

@ -214,20 +214,20 @@ class Data:
self.freq[key] = freq
except ValueError:
return Result().failure(info=Constants.CUT_PSG_GET_FILE_AND_FREQ_FAILURE +
Constants.FAILURE_REASON["Filename_Format_not_Correct"])
Constants.FAILURE_REASON["Filename_Format_not_Correct"] + f"\n{Config['ChannelInput']}")
for value in self.freq.values():
if value == 0:
return Result().failure(info=Constants.CUT_PSG_GET_FILE_AND_FREQ_FAILURE +
Constants.FAILURE_REASON["Filename_Format_not_Correct"])
Constants.FAILURE_REASON["Filename_Format_not_Correct"] + f"\n{Config['ChannelInput']}")
if not any((Config["LabelInput"]["SA Label"] + Config["EndWith"]["SA Label"]) in str(file) for file in Path(Config["Path"]["InputFolder"]).glob('*')):
return Result().failure(info=Constants.CUT_PSG_GET_FILE_AND_FREQ_FAILURE +
Constants.FAILURE_REASON["File_not_Exist"])
Constants.FAILURE_REASON["File_Not_Exist"])
if not any((Config["StartTime"] + Config["EndWith"]["StartTime"]) in str(file) for file in Path(Config["Path"]["InputFolder"]).glob('*')):
return Result().failure(info=Constants.CUT_PSG_GET_FILE_AND_FREQ_FAILURE +
Constants.FAILURE_REASON["File_not_Exist"])
Constants.FAILURE_REASON["File_Not_Exist"])
if not Path(Config["Path"]["InputAlignInfo"]).exists():
return Result().failure(info=Constants.CUT_PSG_GET_FILE_AND_FREQ_FAILURE +
Constants.FAILURE_REASON["File_not_Exist"])
Constants.FAILURE_REASON["File_Not_Exist"])
except Exception as e:
return Result().failure(info=Constants.CUT_PSG_GET_FILE_AND_FREQ_FAILURE +
Constants.FAILURE_REASON["Get_File_and_Freq_Excepetion"] + "\n" + format_exc())

View File

@ -42,6 +42,7 @@ class Filename:
BCG_SYNC: str = "BCG_Sync_"
# Folder: PSG_Aligned
STAGE_SYNC: str = "5_class_Sync_"
ECG_SYNC: str = "ECG_Sync_"
THO_SYNC: str = "Effort Tho_Sync_"
ABD_SYNC: str = "Effort Abd_Sync_"