import numpy as np import random from helper_code import * from tqdm import tqdm import pandas as pd from scipy import signal import warnings def standard_to_cabrera(ecg_data): """ Converts 12-lead ECG from Standard order to Cabrera order. Input shape: (12, N) Input order: I, II, III, aVR, aVL, aVF, V1, V2, V3, V4, V5, V6 Output order: aVL, I, -aVR, II, aVF, III, V1, V2, V3, V4, V5, V6 """ # 1. Define the index mapping from Standard to Cabrera # Standard Indices: # I=0, II=1, III=2, aVR=3, aVL=4, aVF=5, V1=6, V2=7 ... V6=11 # Cabrera Target: aVL, I, aVR, II, aVF, III, V1...V6 cabrera_indices = np.array([ 4, 0, 3, 1, 5, 2, 6, 7, 8, 9, 10, 11]) # 2. Reorder the rows using advanced integer indexing # This creates a copy of the data in the new order cabrera_data = ecg_data[cabrera_indices, :] # 3. Invert aVR to become -aVR # In the NEW Cabrera array, aVR is now at index 2 (the 3rd row) cabrera_data[2, :] = -1 * cabrera_data[2, :] return cabrera_data def get_nsamp(header): return int(header.split('\n')[0].split(' ')[3]) def replace_equivalent_classes(classes, equivalent_classes): for j, x in enumerate(classes): for multiple_classes in equivalent_classes: if x in multiple_classes: classes[j] = multiple_classes[0] # Use the first class as the representative class. return classes class lead_exctractor: """ used to select specific leads or random choice of configurations Twelve leads: I, II, III, aVR, aVL, aVF, V1, V2, V3, V4, V5, V6 Eight leads: I, II, V1, V2, V3, V4, V5, V6 Six leads: I, II, III, aVR, aVL, aVF Four leads: I, II, III, V2 Three leads: I, II, V2 Two leads: I, II """ L2 = np.array([1,1,0,0,0,0,0,0,0,0,0,0]) L3 = np.array([1,1,0,0,0,0,0,1,0,0,0,0]) L4 = np.array([1,1,1,0,0,0,0,1,0,0,0,0]) L6 = np.array([1,1,1,1,1,1,0,0,0,0,0,0]) L8 = np.array([1,1,0,0,0,0,1,1,1,1,1,1]) L12 = np.array([1,1,1,1,1,1,1,1,1,1,1,1]) @staticmethod def get (x, num_leads, lead_indicator): if num_leads==None: # random choice output num_leads = random.choice([12,8,6,4,3,2]) if num_leads==12: # Twelve leads: I, II, III, aVR, aVL, aVF, V1, V2, V3, V4, V5, V6 return x, lead_indicator * lead_exctractor.L12 if num_leads==8: # Six leads: I, II, V1, V2, V3, V4, V5, V6 x = x * lead_exctractor.L8.reshape(12,1) return x,lead_indicator * lead_exctractor.L8 if num_leads==6: # Six leads: I, II, III, aVL, aVR, aVF x = x * lead_exctractor.L6.reshape(12,1) return x,lead_indicator * lead_exctractor.L6 if num_leads==4: # Six leads: I, II, III, V2 x = x * lead_exctractor.L4.reshape(12,1) return x,lead_indicator * lead_exctractor.L4 if num_leads==3: # Three leads: I, II, V2 x = x * lead_exctractor.L3.reshape(12,1) return x,lead_indicator * lead_exctractor.L3 if num_leads==2: # Two leads: II, V5 x = x * lead_exctractor.L2.reshape(12,1) return x,lead_indicator * lead_exctractor.L2 raise Exception("invalid-leads-number") def expand_leads(recording, input_leads): output = np.zeros((12, recording.shape[1])) # recording.shape[1]: 5000 twelve_leads = ('I', 'II', 'III', 'aVR', 'aVL', 'aVF', 'V1', 'V2', 'V3', 'V4', 'V5', 'V6') twelve_leads = [k.lower() for k in twelve_leads] # ['i', 'ii', 'iii', 'avr', 'avl', 'avf', 'v1', 'v2', 'v3', 'v4', 'v5', 'v6'] input_leads = [k.lower() for k in input_leads] # Here we can assume: # input_leads:I, II, V1, V2, V3, V4, V5, V6, # so the new input_leads: ['i', 'ii', 'v1', 'v2', 'v3', 'v4', 'v5', 'v6'] output_leads = np.zeros((12,)) # [0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.] # idx: [0, 1, 6, 7, 8, 9, 10, 11] for i,k in enumerate(input_leads): idx = twelve_leads.index(k) output[idx,:] = recording[i,:] output_leads[idx] = 1 return output, output_leads class dataset: classes = ['164889003','164890007','6374002','426627000','733534002', '713427006','270492004','713426002','39732003','445118002', '164947007','251146004','111975006','698252002','426783006', '284470004','10370003','365413008','427172004','164917005', '47665007','427393009','426177001','427084000','164934002', '59931005'] normal_class = '426783006' equivalent_classes = [['713427006', '59118001'], ['284470004', '63593006'], ['427172004', '17338001'], ['733534002', '164909002']] def __init__(self, header_files): self.files = [] self.sample = True self.num_leads = None for h in tqdm(header_files): tmp = dict() tmp['header'] = h tmp['record'] = h.replace('.hea','.mat') hdr = load_header(h) tmp['nsamp'] = get_nsamp(hdr) tmp['leads'] = get_leads(hdr) tmp['age'] = get_age(hdr) tmp['sex'] = get_sex(hdr) tmp['dx'] = get_labels(hdr) tmp['fs'] = get_frequency(hdr) tmp['target'] = np.zeros((26,)) tmp['dx'] = replace_equivalent_classes(tmp['dx'], dataset.equivalent_classes) for dx in tmp['dx']: # in SNOMED code is in scored classes if dx in dataset.classes: idx = dataset.classes.index(dx) tmp['target'][idx] = 1 self.files.append(tmp) # print("This is the target:", tmp['target']) # set filter parameters # Filtering: Data are filtered using a zero-phase method with 3rd order Butterworth bandpass filter # with frequency band from 1 Hz to 47 Hz. self.b, self.a = signal.butter(3, [1 / 250, 47 / 250], 'bandpass') self.files = pd.DataFrame(self.files) self.current_epoch = 0 # Initialize the current epoch def set_epoch(self, epoch): self.current_epoch = epoch def summary(self, output): if output=='pandas': # print("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@") # print("This is the target:",self.files['target'],type(self.files['target'])) return pd.Series(np.stack(self.files['target'].to_list(),axis=0).sum(axis=0),index=dataset.classes) if output=='numpy': return np.stack(self.files['target'].to_list(),axis=0).sum(axis=0) def Pre_processing(self, fs, leads, data): # 1. Expand to a 12-lead setup if the original signal has fewer channels data, lead_indicator = expand_leads(data, input_leads=leads) data = np.nan_to_num(data) # 2. Resample from 1000 Hz and other rates to 100 Hz if fs == float(1000): data = signal.resample_poly(data, up=1, down=10, axis=-1) # to 500Hz fs = 100 elif fs == float(500): data = signal.resample_poly(data, up=1, down=5, axis=-1) # to 500Hz fs = 100 else: data = signal.resample(data, int(data.shape[1] * 100 / fs), axis=1) fs = 100 # 4. We compute the average and mean for each lead using the code below; this is Z-score normalization mu = np.nanmean(data, axis=-1, keepdims=True) std = np.nanstd(data, axis=-1, keepdims=True) #std = np.nanstd(data.flatten()) with warnings.catch_warnings(): warnings.simplefilter("ignore") data = (data - mu) / std data = np.nan_to_num(data) # 5. Selection of leads for 12-lead ECGs; the default is 12 leads data, lead_indicator = lead_exctractor.get(data, self.num_leads, lead_indicator) each_file_length = 1000 # 6. We filter out samples with a length of 1000 using the code below, similar to random shift windows, and zero-padding as well if self.sample: fs = int(fs) # random sample signal if len > 8192 samples if data.shape[-1] >= each_file_length: idx = data.shape[-1] - each_file_length data = data[:, idx:idx + each_file_length] else: # Apply zero-padding along the second dimension padding_size = each_file_length - data.shape[-1] data = np.pad(data, ((0, 0), (0, padding_size)), mode='constant', constant_values=0) #7 use the orde of cabrera sequence # data =standard_to_cabrera(data) return data def __len__(self): return len(self.files) ''' fs: 500.0 sampling rate target: [0. 0. 0. 0. 1. 0. 0. 0. 0. 0. 0. 0. 0. 1. 0. 0. 0. 0. 0. 0. 0. 0. 1. 0. 0. 0.] leads: ('I', 'II', 'III', 'aVR', 'aVL', 'aVF', 'V1', 'V2', 'V3', 'V4', 'V5', 'V6') self.files.iloc[item]['record']: ../../python-classifier-2021-main/training_data/chapman_shaoxing/g1/JS00518.mat data: shape->(12 X 5000) ''' def __getitem__(self, item): fs = self.files.iloc[item]['fs'] target = self.files.iloc[item]['target'] leads = self.files.iloc[item]['leads'] data = load_recording(self.files.iloc[item]['record']) # the phase of pre-processing: data = self.Pre_processing(fs, leads, data) return data, target