diff --git a/AlaskaEx1.csv b/AlaskaEx1.csv new file mode 100644 index 0000000..badd1a2 --- /dev/null +++ b/AlaskaEx1.csv @@ -0,0 +1,3 @@ +file_path_raw_signals,file_path_preprocessed_signals,file_path_manual_state_annot_orig,file_path_manual_state_annotation,file_path_automated_state_annotation,file_path_refined_state_annotation,file_path_review_intervals,sampling_frequency_in_hz,ECG,BP,EEG,EOG,EMG,EMGamp,Resp,Tabd +WorkPath\Bears11_Bear2\Bear20120112a-Bear2-HH.edf,WorkPath\Processed\Bear20120112a-Bear2-HH.npy,WorkPath\Scores\Bear20120112a-Bear2-HH_OT.edf,WorkPath\Scores\Bear20120112a-Bear2-HH_OT_adj.edf,WorkPath\Processed\Bear20120112a-Bear2-HH_annotation_predicted.edf,WorkPath\Processed\Bear20120112a-Bear2-HH_refined_annotation.csv,WorkPath\Processed\Bear20120112a-Bear2-HH_review_intervals.csv,500,ECG,BP,EEG,EOG,EMG,EMGamp,Resp,Tabd +WorkPath\Bears11_Bear2\Bear20120203a-Bear2-HL.edf,WorkPath\Processed\Bear20120203a-Bear2-HL.npy,WorkPath\Scores\Bear20120203a-Bear2-HL_OT.edf,WorkPath\Scores\Bear20120203a-Bear2-HL_OT_adj.edf,WorkPath\Processed\Bear20120203a-Bear2-HL_annotation_predicted.edf,WorkPath\Processed\Bear20120203a-Bear2-HL_refined_annotation.csv,WorkPath\Processed\Bear20120203a-Bear2-HL_review_intervals.csv,500,ECG,BP,EEG,EOG,EMG,EMGamp,Resp,Tabd diff --git a/example_pipeline/00_truncate_edf_annotations.py b/example_pipeline/00_truncate_edf_annotations.py index b4a0013..6cb0c20 100644 --- a/example_pipeline/00_truncate_edf_annotations.py +++ b/example_pipeline/00_truncate_edf_annotations.py @@ -15,6 +15,10 @@ if __name__ == '__main__': + from configuration import ( + time_resolution, + ) + # -------------------------------------------------------------------------------- # parse and check inputs @@ -34,10 +38,12 @@ check_dataframe(datasets, columns = [ 'file_path_raw_signals', + 'file_path_manual_state_annot_orig', 'file_path_manual_state_annotation', ], column_to_dtype = { 'file_path_raw_signals' : str, + 'file_path_manual_state_annot_orig' : str, 'file_path_manual_state_annotation' : str, } ) @@ -46,14 +52,19 @@ datasets = datasets.loc[np.in1d(range(len(datasets)), args.only)] for ii, dataset in datasets.iterrows(): - print("{} ({}/{})".format(dataset['file_path_manual_state_annotation'], ii+1, len(datasets))) + print("{} ({}/{})".format(dataset['file_path_manual_state_annot_orig'], ii+1, len(datasets))) file_path_raw_signals = dataset['file_path_raw_signals'] with EdfReader(file_path_raw_signals) as f: total_time_in_seconds = f.file_duration + #truncate to whole number of epochs + total_time_in_seconds = time_resolution*(total_time_in_seconds // time_resolution) - file_path_manual_state_annotation = dataset['file_path_manual_state_annotation'] - states, intervals = _load_edf_hypnogram(file_path_manual_state_annotation) + + file_path_manual_state_annot_orig = dataset['file_path_manual_state_annot_orig'] + + + states, intervals = _load_edf_hypnogram(file_path_manual_state_annot_orig) start, stop = intervals[-1] while stop > total_time_in_seconds: @@ -63,5 +74,9 @@ elif (start < total_time_in_seconds) and (stop > total_time_in_seconds): intervals[-1] = (start, total_time_in_seconds) start, stop = intervals[-1] - - _export_edf_hypnogram(file_path_manual_state_annotation, states, intervals) + + with EdfReader(file_path_manual_state_annot_orig) as f: + edf_header = f.getHeader() + + file_path_manual_state_annotation = dataset['file_path_manual_state_annotation'] + _export_edf_hypnogram(file_path_manual_state_annotation, states, intervals, edf_header) diff --git a/example_pipeline/02_test_state_annotation.py b/example_pipeline/02_test_state_annotation.py index 2b74b7b..10800ac 100755 --- a/example_pipeline/02_test_state_annotation.py +++ b/example_pipeline/02_test_state_annotation.py @@ -152,15 +152,17 @@ # LDA tranformed signals transformed_signals = annotator.transform(signal_arrays[ii]) - plot_signals(transformed_signals, ax=axes[1]) + plot_signals(transformed_signals, ax=axes[1], sampling_frequency=1/time_resolution) axes[1].set_ylabel("Transformed signals") predicted_state_vector = annotator.predict(signal_arrays[ii]) - predicted_states, predicted_intervals = convert_state_vector_to_state_intervals(predicted_state_vector, mapping=int_to_state) + predicted_states, predicted_intervals = convert_state_vector_to_state_intervals( + predicted_state_vector, mapping=int_to_state, time_resolution=time_resolution) plot_states(predicted_states, predicted_intervals, ax=axes[2]) axes[2].set_ylabel("Automated\nannotation") - states, intervals = convert_state_vector_to_state_intervals(state_vectors[ii], mapping=int_to_state) + states, intervals = convert_state_vector_to_state_intervals( + state_vectors[ii], mapping=int_to_state, time_resolution=time_resolution) plot_states(states, intervals, ax=axes[3]) axes[3].set_ylabel("Manual\nannotation") diff --git a/example_pipeline/04_run_state_annotation.py b/example_pipeline/04_run_state_annotation.py index cb48905..e41a76e 100755 --- a/example_pipeline/04_run_state_annotation.py +++ b/example_pipeline/04_run_state_annotation.py @@ -11,6 +11,7 @@ import numpy as np import matplotlib.pyplot as plt +from pyedflib import EdfReader, EdfWriter from somnotate._automated_state_annotation import StateAnnotator from somnotate._utils import ( convert_state_vector_to_state_intervals, @@ -89,11 +90,13 @@ def _get_score(vec): # check contents of spreadsheet check_dataframe(datasets, columns = [ + 'file_path_raw_signals', 'file_path_preprocessed_signals', 'file_path_automated_state_annotation', 'file_path_review_intervals', ], column_to_dtype = { + 'file_path_raw_signals' : str, 'file_path_preprocessed_signals' : str, 'file_path_automated_state_annotation' : str, 'file_path_review_intervals' : str, @@ -114,7 +117,11 @@ def _get_score(vec): predicted_state_vector = annotator.predict(signal_array) predicted_states, predicted_intervals = convert_state_vector_to_state_intervals( predicted_state_vector, mapping=int_to_state, time_resolution=time_resolution) - export_hypnogram(dataset['file_path_automated_state_annotation'], predicted_states, predicted_intervals) + + with EdfReader(dataset['file_path_raw_signals']) as f: + edf_header = f.getHeader() + + export_hypnogram(dataset['file_path_automated_state_annotation'], predicted_states, predicted_intervals, edf_header) # compute intervals for manual review state_probability = annotator.predict_proba(signal_array) diff --git a/example_pipeline/configuration.py b/example_pipeline/configuration.py index d94758c..ec8e511 100755 --- a/example_pipeline/configuration.py +++ b/example_pipeline/configuration.py @@ -153,7 +153,8 @@ def _chebychev_bandpass(lowcut, highcut, fs, rs, order=5): ('Sleep stage 1 (artefact)', -2), ('Sleep stage R' , 3), ('Sleep stage R (artefact)', -3), - ('undefined' , 0), + ('Sleep stage ?' , 0), # ? = Undefined/unscorable code in Polyman + ('Movement Time' , 0), # Just included in case M code is accidentally added to avoid exception, cannot be deleted in Polyman ]) # Construct the inverse mapping to convert back from state predictions to human readabe labels. @@ -167,7 +168,7 @@ def _chebychev_bandpass(lowcut, highcut, fs, rs, order=5): 'N' : 'Sleep stage 1 (artefact)', 'r' : 'Sleep stage R' , 'R' : 'Sleep stage R (artefact)', - 'x' : 'undefined' , + 'x' : 'Sleep stage ?' , # ? = Undefined/unscorable code in Polyman 'X' : 'undefined (artefact)' , } @@ -179,7 +180,7 @@ def _chebychev_bandpass(lowcut, highcut, fs, rs, order=5): 'Sleep stage 1 (artefact)' : 'cornflowerblue', 'Sleep stage R' : 'gold', 'Sleep stage R (artefact)' : 'yellow', - 'undefined' : 'gray', + 'Sleep stage ?' : 'gray', # ? = Undefined/unscorable code in Polyman 'undefined (artefact)' : 'lightgray', } @@ -190,7 +191,7 @@ def _chebychev_bandpass(lowcut, highcut, fs, rs, order=5): 'Sleep stage 1 (artefact)' , 'Sleep stage R' , 'Sleep stage R (artefact)' , - 'undefined' , + 'Sleep stage ?' , # ? = Undefined/unscorable code in Polyman 'undefined (artefact)' , ] @@ -213,7 +214,7 @@ def _chebychev_bandpass(lowcut, highcut, fs, rs, order=5): # -------------------------------------------------------------------------------- # desired time resolution of the automated annotation (in seconds) -time_resolution = 1 +time_resolution = 10 # default view length when manually annotating states default_view_length = 60. diff --git a/example_pipeline/data_io.py b/example_pipeline/data_io.py index 6be3aed..bbcad3c 100755 --- a/example_pipeline/data_io.py +++ b/example_pipeline/data_io.py @@ -9,7 +9,12 @@ import pandas from argparse import ArgumentParser -from collections import Iterable +try: + # Python <= 3.9 + from collections import Iterable +except ImportError: + # Python > 3.9 + from collections.abc import Iterable from pyedflib import EdfReader, EdfWriter from six import ensure_str @@ -338,7 +343,7 @@ def _load_edf_hypnogram(file_path): return states, intervals -def _export_edf_hypnogram(file_path, states, intervals): +def _export_edf_hypnogram(file_path, states, intervals, header=None): """Export hypnogram as "Time-stamped Annotations Lists (TALs)" in EDF annotations. @@ -361,8 +366,10 @@ def _export_edf_hypnogram(file_path, states, intervals): with EdfWriter(file_path, 0) as writer: for (start, stop), state in zip(intervals, states): writer.writeAnnotation(start, stop-start, state) + if header: + writer.setHeader(header) writer.close() - + # -------------------------------------------------------------------------------- # aliases