Compare commits

...

8 Commits
v0.1 ... main

Author SHA1 Message Date
Yorusora
9562eedc02 更新README.md 2025-02-10 18:12:33 +08:00
Yorusora
8370bffdbb 修复PSG_label_2_BCG_label.py脚本中的一个关于路径的小bug 2025-01-14 23:57:49 +08:00
Yorusora
bd026d1895 由于标签、数据和体动都已完成对齐,因此删除了Artifact_Offset的使用。 2025-01-14 19:57:59 +08:00
Yorusora
6efa829dcf 完善了README.md
完善了PSG_label_2_BCG_label.py脚本
2025-01-13 20:48:51 +08:00
hhj
f0297c9148 修改注释 2025-01-12 13:17:09 +08:00
hhj
5a165cb69e 1、修改了之前因将os.path更换成Path后产生的bug
2、由于tdqm库会对exe程序的运行产生崩溃影响,因此删除了tqdm库
3、修改乐requirements.txt的部分内容
2025-01-12 13:14:26 +08:00
marques
1561759f80 添加requirements.txt 2025-01-11 17:16:46 +08:00
marques
537fe94352 添加requirements.txt 2025-01-11 17:16:11 +08:00
7 changed files with 112 additions and 59 deletions

View File

@ -15,7 +15,6 @@ from matplotlib import pyplot as plt, gridspec
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg, NavigationToolbar2QT
from pyedflib import EdfReader
from datetime import datetime
from tqdm import tqdm
from pathlib import Path
from PyQt5.QtCore import QCoreApplication, QThread, pyqtSignal
from PyQt5.QtWidgets import QFileDialog, QMainWindow, QMessageBox, QButtonGroup, QApplication
@ -52,7 +51,7 @@ ch.setFormatter(Formatter("%(asctime)s: %(message)s"))
logger.addHandler(ch)
getLogger('matplotlib.font_manager').disabled = True
info("------------------------------------")
info("-----Main_Quality_Relabel_GUI.py----")
class MainWindow(QMainWindow, Ui_MainWindow):
# 可选择的通道
@ -192,12 +191,11 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self.BCG_Data_Path = self.dir_path / "BCG"
self.BCG_Label_Path = self.dir_path / "BCG_label"
self.Artifact_Label_Path = self.dir_path / "Artifact_label"
self.Artifact_Offset_Path = self.dir_path / "Artifact_label" / "20220421Artifact_offset_value.xlsx"
if self.PSG_Data_Path.exists() and self.BCG_Data_Path.exists() and self.BCG_Label_Path.exists() and self.Artifact_Label_Path.exists() and self.Artifact_Label_Path.exists():
sampIDs = self.BCG_Data_Path.glob()
sampIDs = self.BCG_Data_Path.glob('*.*')
sampID_for_comboBox = []
for sampID in sampIDs:
sampID = sampID.replace("samp.npy", "")
sampID = sampID.name.replace("samp.npy", "")
sampID_for_comboBox.append(sampID)
bcg_path = self.BCG_Data_Path / f"{sampID}samp.npy"
ecg_path = self.PSG_Data_Path / f"A{str(sampID).rjust(7, '0')}.edf"
@ -236,7 +234,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self.pushButton_confirmLabel.setEnabled(True)
self.pushButton_confirmLabel.setText("开始打标")
MainWindow.setWindowTitle(self, QCoreApplication.translate("MainWindow",
"Main_Quality_Relabel_GUI - Data Path: " + self.dir_path))
"Main_Quality_Relabel_GUI - Data Path: " + str(self.dir_path)))
info("Successfully Loaded Data Path.")
self.textBrowser_update("操作:数据路径选择成功")
else:
@ -836,7 +834,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self.bcg_event_label_index_list = df2.index.tolist()
info("Traversaling XinXiao events...")
self.textBrowser_update("提示:正在遍历心晓事件")
for one_data in tqdm(df.index):
for one_data in df.index:
one_data = df.loc[one_data]
SP = one_data["Start"] * self.frequency
EP = one_data["End"] * self.frequency
@ -853,16 +851,14 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self.textBrowser_update("提示:完成遍历心晓事件")
def read_artifact_label(self):
all_offset_length = read_excel(self.Artifact_Offset_Path)
offset_length = all_offset_length[all_offset_length['数据编号'] == self.sampNo]['from_code'].values[0]
artifact_label_path = self.Artifact_Label_Path / f"Artifact_{self.sampNo}.txt"
artifact_label = read_csv(artifact_label_path, header=None).to_numpy().reshape(-1, 4)
self.artifact_event_label = zeros(len(self.bcg_event_label) + self.extend_second * self.frequency)
for i, artifact_type, SP, EP in artifact_label:
SP = (int(SP) + offset_length) // (1000 // self.frequency)
EP = (int(EP) + offset_length) // (1000 // self.frequency)
SP = int(SP) // (1000 // self.frequency)
EP = int(EP) // (1000 // self.frequency)
artifact_type = int(artifact_type) + 5
SP = 0 if SP < 0 else SP

View File

@ -1,4 +1,3 @@
import os
import pandas as pd
import pyedflib
from pathlib import Path
@ -9,38 +8,35 @@ def get_time_to_seconds(time_str):
base_event = ["Hypopnea", "Central apnea", "Obstructive apnea", "Mixed apnea"]
# 输入设置
sampID = 888
dir_path = r"E:\data_annotation\6SleepApnea_annotation_GUI_demo\data"
# 输入设置(每次运行此脚本都必须检查)
dir_path = Path(r"D:\code\data")
PSG_Data_Path = dir_path / "PSG"
PSG_Label_Path = dir_path / "PSG_label"
BCG_Data_Path = dir_path / "BCG"
BCG_Label_Path = dir_path / "BCG_label"
# 数据路径设置
PSG_Data_Path = Path(os.path.join(dir_path, "PSG"))
PSG_Label_Path = Path(os.path.join(dir_path, "PSG_label"))
BCG_Data_Path = Path(os.path.join(dir_path, "BCG"))
BCG_Label_Path = Path(os.path.join(dir_path, "BCG_label"))
sampIDs = PSG_Label_Path.glob('*.*')
for sampID in sampIDs:
sampID = sampID.name.replace("export", "").replace(".csv", "")
# 读取PSG标签
df_PSG_label = pd.read_csv(PSG_Label_Path / (f"export" + str(sampID) + ".csv"), encoding="gbk")
df_PSG_label = df_PSG_label.loc[:, ~df_PSG_label.columns.str.contains('^Unnamed')]
df_PSG_label = df_PSG_label[df_PSG_label["Event type"].isin(base_event)]
df_PSG_label['Duration'] = df_PSG_label['Duration'].str.replace(r' \(.*?\)', '', regex=True)
# 读取PSG标签
df_PSG_label = pd.read_csv(PSG_Label_Path / (f"export" + str(sampID) + ".csv"), encoding="gbk")
df_PSG_label = df_PSG_label.loc[:, ~df_PSG_label.columns.str.contains('^Unnamed')]
df_PSG_label = df_PSG_label[df_PSG_label["Event type"].isin(base_event)]
df_PSG_label['Duration'] = df_PSG_label['Duration'].str.replace(r' \(.*?\)', '', regex=True)
# 读取EDF文件
edf_File = pyedflib.EdfReader(str(PSG_Data_Path / f"A{str(sampID).rjust(7, '0')}.edf"))
# 读取EDF文件
edf_File = pyedflib.EdfReader(str(PSG_Data_Path / f"A{str(sampID).rjust(7, '0')}.edf"))
# 获取PSG记录开始时间
start_time = str(edf_File.getStartdatetime()).split(" ")[1]
start_time_abs = get_time_to_seconds(start_time)
# 获取PSG记录开始时间
start_time = str(edf_File.getStartdatetime()).split(" ")[1]
start_time_abs = get_time_to_seconds(start_time)
# 计算起始时间秒数和终止时间秒数
df_PSG_label['Start'] = (df_PSG_label['Time'].apply(get_time_to_seconds) - start_time_abs).apply(lambda x: x + 24 * 3600 if x < 0 else x).astype(int)
df_PSG_label['End'] = df_PSG_label['Start'] + df_PSG_label['Duration'].astype(float).round(0).astype(int)
# 计算起始时间秒数和终止时间秒数
df_PSG_label['Start'] = (df_PSG_label['Time'].apply(get_time_to_seconds) - start_time_abs).apply(lambda x: x + 24 * 3600 if x < 0 else x).astype(int)
df_PSG_label['End'] = df_PSG_label['Start'] + df_PSG_label['Duration'].astype(float).round(0).astype(int)
# 写入csv文件
df_PSG_label.to_csv(str(dir_path) + r"\BCG_label\export" + str(sampID) + "_all.csv", index=False, encoding="gbk")
# 打印结果
print(df_PSG_label)
# 写入csv文件
df_PSG_label.to_csv(dir_path + r"\BCG_label\export" + str(sampID) + "_all.csv", index=False, encoding="gbk")
# 打印结果
print("sampID_" + str(sampID) + "写入csv成功")
# 打印结果
print("sampID_" + str(sampID) + "写入csv成功")

View File

@ -1,12 +1,24 @@
# SleepApnea_annotation_GUI——睡眠呼吸暂停事件打标程序
# SA_Label——睡眠呼吸暂停事件打标程序
```
项目作者信息
@author:Yosa, Marques
@email:2023025086@m.scnu.edu.cn, 2021022362@m.scnu.edu.cn
@time:2025/1/4
```
本程序提供符合用户直接的操作界面,用于打标睡眠呼吸暂停事件,对医生所做的原始标注进行修改,亦可对医生忽略的片段进行新增标注,对医生打错的片段进行标注删除。
## 0 说明
程序截图:
![0](/img/0.png)
## 0 项目说明
### 0.1 程序说明
本程序的输入是经过对齐处理后的BCG信号和PSG信号BCG事件的类型、事件起始时间和终止时间分别对应于BCG_label文件夹中的`Event type`、`Start`、`End`列。
本程序的输入是经过对齐处理后的BCG信号和PSG信号BCG事件标签包括有BCG事件的类型、事件起始时间和终止时间分别对应于BCG_label文件夹中的`Event type`、`Start`、`End`列。
这里的BCG事件标签通过对齐后的PSG标签直接复制粘贴得到。
本程序的输出是标签类型、备注、修正后起始时间、修正后终止时间、修正后事件类型分别对应于BCG_label文件夹中的`score`、`remark`、`correct_Start`、`correct_End`、`correct_EventTypes`列。
@ -14,40 +26,71 @@
本程序在Python3.9的环境下开发在Intel® Core™ i5-13600K Processor和AMD Ryzen™ 7 8845H平台下都能成功运行。
### 0.3 文件目录说明
```
.../img # 存放README.md中的图片
.../logs # 存放软件运行的日志,命名规则为"年月日.log"
|-(YYYYMMDD.log)
.../utils # 存放主程序需要用到的工具函数
|-Preprocessing.py # 一些信号预处理所用到的函数
|-Quality_Relabel.py # 算是遗产文件,里面被需要的代码已集成到入口程序中,因此删去此文件并不影响程序的运行
.../Main_Quality_Relabel.py # 打标程序入口,主函数在里边
.../MainWindow.py # 对MainWindow.ui执行PyQt的pyuic后生成的ui代码
.../MainWindow.ui # 使用qtdesigner设计的ui文件
.../PSG_label_2_BCG_label.py # 将对齐后的PSG_label转化为符合本打标程序执行的BCG_label文件的脚本
.../README.md # 就是你现在正在查看的说明文档
.../requirements.txt # 执行本程序所需的依赖库
```
## 1 程序安装与启动
### 1.1 直接运行可执行文件
在开发者做出exe可执行文件之前都需要自行搭建环境来运行程序。
点击`版本发布`下载里面的最新版的exe可执行文件双击exe即可直接运行
### 1.2 在开发环境中运行
推荐使用conda环境直接在conda控制台中运行如下指令创建python3.9版本的环境。
推荐使用conda环境直接在conda控制台中运行如下指令创建python3.9版本的环境。或者也可以使用python虚拟环境。
```
conda create -n <env_name> python=3.9
```
激活环境后通过conda install命令手动安装如下依赖库部分需通过pip install命令进行安装。版本不一定要完全对应当默认安装的版本无法启动时可尝试安装对应版本的依赖库。
激活环境后通过`pip install -r requirements.txt`来安装依赖库。下表是`requirements.txt`文件中提及的依赖库和对应的版本
```
Pack Name | Version
-----------|----------
matplotlib | 3.8.3
numpy | 1.26.4
pandas | 2.2.3
matplotlib | 3.8.3
tqdm | 4.66.4
pyyaml | 6.0.1
pillow | 11.1.0
pyEDFlib | 0.1.38
PyQt5 | 5.15.10
pyedflib | 0.1.38
PyQt5-Qt5 | 5.15.2
PyQt5_sip | 12.16.1
PyYAML | 6.0.1
scipy | 1.15.1
six | 1.17.0
openpyxl | 3.1.5
```
**对于开发者来说若通过pyinstaller来build导出exe可执行文件请跳转到第3节查看相关说明。**
## 2 使用方法与流程
### 2.0 界面截图
![1](/img/1.png)
### 2.1 打开数据
### 2.1 将事件标签的时间标准化
此步骤的输入是PSG_label文件夹中的对应样本ID的事件标签输出是BCG_label文件夹中对应样本ID的事件标签其csv文件中再加上`Start`和`End`两列数据。
操作方法修改PSG_label_2_BCG_label.py中的dir_path是数据存放路径然后直接运行程序即可。(注意是指所有数据的存放路径,而不是深入到`PSG_label`文件夹)
**这个步骤只需要操作一遍,建议由睡眠呼吸暂停方向的总负责人执行此步骤后将执行后的数据一同放到对应的数据文件夹中,之后再分发给其他同学进行打标。**
### 2.2 打开数据
点击左上角的`打开`,点击`数据路径选择`。
@ -74,7 +117,7 @@ scipy | 1.15.1
![3](/img/3.png)
### 2.2 开始前的设置
### 2.3 开始前的设置
当正确操作`打开数据`步骤后,界面截图中的`开始前的设置`中的内容将被允许更改。
@ -86,10 +129,12 @@ scipy | 1.15.1
![4](/img/4.png)
### 2.3 打标时的操作
### 2.4 打标时的操作
按需设置好2.2步骤中的内容后,点击`打标操作`中的`开始打标`,在这之后`开始前的设置`将无法被修改,若需修改,则需要直接关闭程序后重新运行。
**!!!若在点击`开始打标`后程序崩溃请检查BCG_label文件夹中该样本ID所对应的csv文件里是否含有`Start`和`End`两列数据若没有那就是你忘记了操作2.1步骤。**
对数据进行初次打标时程序会在BCG_label的csv文件中新起6列分别是`score`、`correct_Start`、`correct_End`、`isLabeled`、`remark`、`correct_EventsType`,对应如下所示,`打标操作`中所做的修改将在用户点击`确定达标参数`后即刻写入到BCG_label对应的csv文件中。
用户无需手动输入`修正后起始时间`和`修正后终止时间`的值,程序提供更为方便的打标方法。在`绘图区域`的下面有一行工具栏,我们需要用到的是工具所对应的图标是`房子`、`十字`和`放大镜``十字`用于挪动绘图区域中的信号,`放大镜`用于放大所选的区域(仅放大横坐标),当通过这两个工具操作了绘图区域中的信号时,程序将获取此时此刻绘图区域的最左边和最右边所对应的横坐标数值,分别填入`修正后起始时间`和`修正后终止时间`中,若不小心操作了信号导致医生所打的时间初始值被修改时,点击`房子`按钮即可将绘图区域中的信号和`修正后起始时间`和`修正后终止时间`复位到初始状态。
@ -112,7 +157,7 @@ correct_EventsType | 事件类型共有4种
导入数据后点击`开始打标`和之后点击`上一个事件`和`下一个事件`,都将读取医生对该事件所做的标注,包括事件类型、起始时间和终止时间,标签类型在事件未被打标的情况下默认为二类,`修正后起始时间`和`修正后终止时间`的初始值都是医生所打的标注,当初始值错误时,用户需对其进行修改。备注默认为空,可根据需要填入内容,点击`快速备注输入`右边的按钮将快速输入对应内容到备注中。`仅检查未确定打标参数的事件`复选框默认未勾选,在勾选的情况下,切换事件时程序将会遍历该事件的`isLabeled`若值为1表明该事件已被标注过程序将会跳过该事件直到程序遍历到未被标注过的事件为止。
### 2.4 逐帧检查功能
### 2.5 逐帧检查功能
此功能的目的是用于检查未被医生标注的片段,当出现医生漏打的情况时,可以通过此功能将对应事件进行标注。
@ -120,10 +165,13 @@ correct_EventsType | 事件类型共有4种
标注事件的方法与2.3步骤类似这里不再赘述。通过此功能标注的事件将被保存到BCG_label的export<xxx>_addNew.csv文件中。
### N 其他说明
## 3 使用pyinstaller构建导出exe可执行文件
本项目的根目录中的PSG_label_2_BCG_label.py用于获取对齐后的PSG标签并生成符合本项目程序运行的格式的BCG标签
在命令行中使用命令`python -m venv <env_name>`在本地创建新的虚拟环境,`<env_name>`是虚拟环境的名字,由于`.gitignore`已经添加了名为`SaLabel`的文件夹,因此虚拟环境名字建议名为`SaLabel`
### TODOLIST
然后使用命令`..\SaLabel\Scripts\activate`切换到这个虚拟环境,通过命令`pip install -r requirements.txt`来安装对应依赖库,获得一个很干净的虚拟环境。
~~令PSG和BCG事件的duration对齐~~
再使用命令`pip install pyinstaller`安装pyinstaller然后使用命令`pyinstaller -F -w Main_Quality_Relabel_GUI.py`就会构建出一个体积比较小的exe文件。
## TODO
添加QValidator组件以规范lineEdit的输入内容

BIN
img/0.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 315 KiB

BIN
img/1.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

After

Width:  |  Height:  |  Size: 81 KiB

BIN
img/5.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 24 KiB

13
requirements.txt Normal file
View File

@ -0,0 +1,13 @@
matplotlib==3.8.3
numpy==1.26.4
pandas==2.2.3
pillow==11.1.0
pyEDFlib==0.1.38
PyQt5==5.15.10
PyQt5-Qt5==5.15.2
PyQt5_sip==12.16.1
PyYAML==6.0.1
scipy==1.15.1
six==1.17.0
openpyxl==3.1.5