File size: 8,955 Bytes
35f60b9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
import os
import scipy
import numpy as np
#!pip install wfdb
import pandas as pd
from mne.io import concatenate_raws, read_raw_edf
import xml.etree.ElementTree as ET
import multiprocessing

# BASE_PATH = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'datasets')
BASE_PATH = os.path.abspath('.')  # ํ˜„์žฌ ๋””๋ ‰ํ† ๋ฆฌ ๊ธฐ์ค€์œผ๋กœ ์ €์žฅ
EPOCH_SEC_SIZE = 30
# ALL_CHANNELS = ['SaO2', 'H.R.', 'EEG(sec)', 'ECG', 'EMG', 'EOG(L)', 'EOG(R)', 'EEG', 'THOR RES', 'ABDO RES', 'POSITION', 'LIGHT', 'NEW AIR', 'OX stat']
AIRFLOW_NAME = "Cannula Flow"

SELECT_CHNS = [
    "SaO2",
    ["ECG L-ECG R", ["ECG L", "ECG R"]],         
    ["L Chin-R Chin", ["L Chin", "R Chin"]], 
    ["EOG-Diff", ["LOC", "ROC"]],                   
    ["C3-A2", ["C3", "A2"]],    
    # ["C4-A1", ["C4", "A1"]],      
    "Abdominal", # ABD
    "Thoracic",  # CHEST
    AIRFLOW_NAME # AIRFLOW (์—†์œผ๋ฉด skip)
]

# ์ €์žฅ ํด๋” ๋งคํ•‘(chn_grp / chn_name)
# (์—ฌ๊ธฐ key๋Š” "๋‚ด๋ถ€์—์„œ ๋งŒ๋“  ์‹ ํ˜ธ ์ด๋ฆ„"์ž„)
CHN_SAVE_DICT = {
    "SaO2": ("SaO2", "SaO2"),
    "ECG L-ECG R": ("ECG", "ECG"),
    "L Chin-R Chin": ("EMG", "EMG"),
    "EOG-Diff": ("EOG", "EOG-Diff"),
    "C3-A2": ("EEG", "C3-A2"),
    # "C4-A1": ("EEG", "C4-A1"),
    "Abdominal": ("ABD", "ABD"),
    "Thoracic": ("CHEST", "CHEST"),
    "Cannula Flow": ("AIRFLOW", "AIRFLOW"),
}




import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--data_dir', default="/home/yhna/data3/#_MrOS/polysomnography/edfs/visit1", help='MrOS EDF directory')
parser.add_argument('--annotation_dir', default="/home/yhna/data5/#_Annotation_XML/#_MrOS1_AHI_a0h3a", help='MrOS revised XML directory')
parser.add_argument('--output_dir', default="signals_MAE", help='Output directory under BASE_PATH (default: ./signals_MAE)')
parser.add_argument('--dataset_name', default='mros1', help='Folder name for saving annotations under BASE_PATH')
parser.add_argument('--num_workers', type=int, default=8)
cfg = parser.parse_args()

from tqdm.auto import tqdm

def process_signal(raw, select_idx, chns, fname):
    raw_sampling_rate = int(raw.info['sfreq'])
    raw_df = raw.to_data_frame()
    print(raw_df.columns)
    for chn_spec in chns:
    # chn_spec: str  ๋˜๋Š”  [a, [b,c]] ํ˜•ํƒœ
        if isinstance(chn_spec, (list, tuple)):
            if len(chn_spec) != 2 or not isinstance(chn_spec[0], str):
                continue

            a = chn_spec[0]
            bc = chn_spec[1]

            if a in raw_df.columns:
                raw_ch_df = raw_df[a]
            else:
                if not (isinstance(bc, (list, tuple)) and len(bc) == 2):
                    continue
                b, c = bc[0], bc[1]
                if (b in raw_df.columns) and (c in raw_df.columns):
                    raw_df[a] = raw_df[b] - raw_df[c]
                    raw_ch_df = raw_df[a]
                else:
                    continue

            chn_name = a 

        else:
            chn_name = chn_spec
            if chn_name not in raw_df.columns:
                continue
            raw_ch_df = raw_df[chn_name]

        raw_ch_df = raw_ch_df.to_frame()
        raw_ch_df.index = np.arange(len(raw_ch_df))

        # Remove movement and unknown stages if any
        raw_ch = raw_ch_df.values

        # Verify that we can split into 30-s epochs
        ep_len = EPOCH_SEC_SIZE * raw_sampling_rate
        if len(raw_ch) % ep_len != 0:
            print(f"Length of raw channel ({len(raw_ch)}) is not multiple of ({EPOCH_SEC_SIZE} * {raw_sampling_rate})")
            continue  

        # get (# of epochs)
        n_epochs = len(raw_ch) // ep_len 

        # split epochs
        x = raw_ch.reshape(n_epochs, ep_len).astype(np.float32)

        # select_idx ์ ์šฉ
        x = x[select_idx]
        x = x.reshape((-1, ep_len))

        # ์ €์žฅ ๊ฒฝ๋กœ ๊ฒฐ์ •: chn_name ๊ธฐ์ค€์œผ๋กœ ๋งคํ•‘
        chn_grp, chn_rename = CHN_SAVE_DICT[chn_name] 

        os.makedirs(os.path.join(BASE_PATH, cfg.output_dir, chn_grp, chn_rename), exist_ok=True)
        np.save(os.path.join(BASE_PATH, cfg.output_dir, chn_grp, chn_rename, f"{fname}.npy"), x)


def process_data(args):

    ann_fname, edf_fname, file_id, shhs2 = args
    ann_filename = os.path.basename(edf_fname).replace(".edf", "_ann.npy")
    labels = []


    # Read annotation and its header
    if ann_fname is None: 
        print(f"None of annfname: {edf_fname}")
        return -1

    # annotations-events-profusion -> annotations-events ๋ณ€๊ฒฝ์œผ๋กœ ์ธํ•œ 
    t = ET.parse(ann_fname)
    r = t.getroot()
    faulty_File = 0

    use_r4 = False
    if len(list(r)) > 4:
        try:
            if len(r[4]) > 0 and (r[4][0].text is not None) and r[4][0].text.strip().isdigit():
                use_r4 = True
        except Exception:
            use_r4 = False

    if use_r4:
        for i in range(len(r[4])):
            lbl = int(r[4][i].text)
            if lbl == 4:
                labels.append(3)
            elif lbl == 5:
                labels.append(4)
            else:
                labels.append(lbl)
            if lbl > 5:
                faulty_File = 1

        if faulty_File == 1:
            print("Faulty labels found!!")
            return -1

        y = np.asarray(labels, dtype=np.int32)

    # --- (2) NSRR ํƒ€์ž…(MrOS/SHHS nsrr): ScoredEvent(Stages|Stages)์—์„œ ๊ตฌ๊ฐ„์„ epoch๋กœ ํŽผ์นจ ---
    else:
        epoch_len = float(r.findtext(".//EpochLength") or EPOCH_SEC_SIZE)

        stage_events = []
        for ev in r.findall(".//ScoredEvent"):
            if (ev.findtext("EventType") or "").strip() != "Stages|Stages":
                continue
            concept = (ev.findtext("EventConcept") or "").strip()
            # concept: "Wake|0", "Stage 2 sleep|2", "REM sleep|5" ํ˜•ํƒœ
            try:
                lbl = int(concept.split("|")[-1])
            except Exception:
                continue

            start = float(ev.findtext("Start") or 0.0)
            dur = float(ev.findtext("Duration") or 0.0)
            stage_events.append((start, dur, lbl))

        if len(stage_events) == 0:
            print(f"[SKIP] No stage events in XML: {ann_fname}")
            return -1

        stage_events.sort(key=lambda x: x[0])
        max_end = max(s + d for s, d, _ in stage_events)
        n_epochs = int(round(max_end / epoch_len))

        y = np.full(n_epochs, -1, dtype=np.int32)

        for start, dur, lbl in stage_events:
            # ์›๋ณธ ๋งคํ•‘ ์œ ์ง€
            if lbl == 4:
                v = 3
            elif lbl == 5:
                v = 4
            else:
                v = lbl

            s_idx = int(round(start / epoch_len))
            n = int(round(dur / epoch_len))
            if n <= 0:
                continue
            y[s_idx:s_idx + n] = v

        # ๋นˆ ๊ตฌ๊ฐ„ ์ฑ„์šฐ๊ธฐ (forward fill)
        last = 0
        for i in range(len(y)):
            if y[i] == -1:
                y[i] = last
            else:
                last = y[i]

    # Select on sleep periods
    w_edge_mins = 30
    nw_idx = np.where(y != 0)[0]                          # get index of first not WAKE
    start_idx = nw_idx[0] - (w_edge_mins * 2)             # start from 30min before first WAKE
    end_idx = nw_idx[-1] + (w_edge_mins * 2)              # end at 30min after first WAKE
    if start_idx < 0: start_idx = 0
    if end_idx >= len(y): end_idx = len(y) - 1
    select_idx = np.arange(start_idx, end_idx + 1)

    # only selected labels
    y = y[select_idx]
    raw = read_raw_edf(edf_fname, preload=True, stim_channel=None, verbose=None)    
    
    process_signal(raw, select_idx, SELECT_CHNS, os.path.basename(edf_fname).split('.')[0])

    
    ann_dir = os.path.join(BASE_PATH, cfg.dataset_name, "annotations")
    os.makedirs(ann_dir, exist_ok=True)
    np.save(os.path.join(ann_dir, ann_filename), arr=y)


if __name__ == '__main__':
    START_IDX = 2300  # ์ด๋ฏธ ์ฒ˜๋ฆฌํ•œ ๊ฐœ์ˆ˜

    edf_dir = cfg.data_dir
    xml_dir = cfg.annotation_dir

    # 1) EDF ๋ชฉ๋ก
    edf_fnames = sorted([
        os.path.join(edf_dir, f)
        for f in os.listdir(edf_dir)
        if f.lower().endswith(".edf")
    ])

    # 2) EDF -> XML ๋งค์นญ: {base}.edf  <->  {base}-nsrr-revised.xml
    ann_fnames = []
    for edf_path in edf_fnames:
        base = os.path.splitext(os.path.basename(edf_path))[0]
        xml_path = os.path.join(xml_dir, f"{base}-nsrr-revised.xml")
        if not os.path.exists(xml_path):
            print(f"[SKIP] XML not found for {base}: {xml_path}")
            ann_fnames.append(None)
        else:
            ann_fnames.append(xml_path)

    # 3) input_list (XML ์—†๋Š” ๊ฑด ์Šคํ‚ต)
    input_list = [
        (ann_fnames[i], edf_fnames[i], i, False)
        for i in range(len(edf_fnames))
        if ann_fnames[i] is not None
    ]

    input_list = input_list[START_IDX:] 


    with multiprocessing.Pool(cfg.num_workers) as pool:
        list(tqdm(pool.imap(process_data, input_list), total=len(input_list)))