예제 #1
0
 def _set_TB(self, specs):
     TB = Div(text=specs['title'])
     TB.width = specs['width']
     TB.height = specs['height']
     if 'style' in specs:
         TB.style = specs['style']
     return TB
예제 #2
0
파일: main.py 프로젝트: CONDUITlab/Afib
def main():
    #path variables
    #in_path = '/mnt/data04/Conduit/afib/new_files/1hour/'
    #af_outpath = '/mnt/data04/Conduit/afib/AF_annotations/af_ann4/'
    in_path = join(dirname(__file__), "data_in/")
    af_outpath = join(dirname(__file__), "data_out/")
    #Miscellaneous variables
    colours = {
        '': 'black',
        'O': 'blue',
        'N': 'green',
        '~': 'purple',
        'A': 'red'
    }
    #wf_classes = ['AF','Normal','Other','Noise','No Signal']
    wf_classes = ['AF', 'Not AF']
    #global variables remove if you can
    newECG = pd.DataFrame()
    df_ann = pd.DataFrame(columns=[0])

    #widgets to be used as necessary
    rdo_btn_wave_lbls = RadioButtonGroup(labels=wf_classes, active=2)
    date_range_slider = Slider(title="Date Range", step=10)
    table_source, table_columns = load_file_source(in_path, af_outpath)
    file_table = DataTable(source=table_source,
                           columns=table_columns,
                           width=800,
                           height=600)
    pgph_file_loaded = Div(text="Select file from table.",
                           width=1000,
                           height=50)
    pgph_file_loaded.style = {
        "font-size": '1.2em',
        'font-weight': 'bold',
        'color': 'SteelBlue'
    }
    txt_processing = Div(
        text=
        "Use slider to select segments, label by selecting the wave type and pressing 'Label'. Use the save to save when finished all annotations.",
        width=1000,
        height=30)
    txt_processing.style = {
        "font-size": '1.2em',
        'font-weight': 'bold',
        'color': 'SteelBlue',
        'white-space': 'pre'
    }

    btn_lbl = Button(label='Label', button_type='success')
    btn_save = Button(label='Save', button_type='danger')
    btn_load_annotated_graph = Button(label='Load Annotated Graph',
                                      button_type='warning')

    #----------------------------- Functions ---------------------------------#
    ''' Callback function for the data table ColumnDataSource (table_source).
        Load the hd5 file based on the selected row in the data_table.
        Generate the output file path.
        Update the date range slider according to the new file (date_range_slider).
        Generate a plot of the hd5 ECG lead II data (wave_graph).
        Then combine all into a layout (graph_layout)
        Update the Tab Pane with this new layout.
        Returns: Nothing'''
    def callback_select_file_table(attr, old, new):
        global newECG, out_file
        #clear tabs graphs to reduce overhead
        output_tab.child.children = []
        sel_id = new
        txt_processing.text = "Use slider to select segments, label by selecting the wave type and pressing 'Label'. Use the save button to when done."
        txt_processing.style = {"font-size": '1.2em', 'color': 'SteelBlue'}
        wave_file_path = load_selected_file(table_source, sel_id)
        out_file = load_annotation_file(
            table_source, sel_id, af_outpath)  #to be used for saving the file
        pgph_file_loaded.text = "Processing..."
        wave_hdfs, hdfs_keys = load_hd5_file(wave_file_path)
        print(hdfs_keys)
        newECG = (wave_hdfs.select(
            key='Waveforms').II).to_frame()  #get lead II from waveforms)
        newECG.reset_index(
            inplace=True)  #move datetime index to column and reset the index
        newECG.columns = ['date', 'II']
        wave_hdfs.close()
        #enable the buttons if disabled
        btn_save.disabled = False
        btn_lbl.disabled = False
        btn_load_annotated_graph.disabled = True
        #check if the file has already been annotated
        annotated = check_if_annotated(table_source, sel_id)
        if annotated == 'Yes':
            txt_processing.text = 'This file has already been annotated. If you save this file again you will overwrite previous data.'
            txt_processing.style = {"font-size": '1.2em', 'color': 'Red'}
            btn_load_annotated_graph.disabled = False
        #create figure
        print("*********************Creating Figure (in Callback File Select)")
        get_next_graph(0, newECG)
        pgph_file_loaded.text = "File loaded, navigate Label Data Tab to annotate lead II."

    ''' Using the AF annotation file, create a Bokeh figure with annotated data.
        Update the output_tab to show this figure.
        Disable some buttons to keep users on track. '''

    def load_output_graph():
        global newECG
        btn_save.disabled = True
        btn_lbl.disabled = True
        btn_load_annotated_graph.disabled = True
        txt_processing.text = 'Loading Plot...'
        df_AF = pd.read_hdf(out_file)
        noise, normal, other, af, nosig, notAF = load_annotations(
            0, df_AF.shape[0] - 1, newECG, df_AF)
        output_graph = get_graph_annotated(noise, normal, other, af, nosig,
                                           notAF)
        output_tab.child.children = [output_graph]
        txt_processing.text = 'Plot loaded, navigate to "Final Annotated Graph" tab to view. &#10 If you save this file again you will overwrite previous data.'
        btn_save.disabled = False
        btn_lbl.disabled = False

    def get_next_graph(sind, df):
        length = 20000  #7200
        eind = sind + length
        if eind <= list(df.index)[-1] and sind < list(
                df.index
        )[-1]:  #as long as the end and beginning are less than the end of the dataframe
            sub_df = df.iloc[sind:eind]
            sub_df = sub_df.set_index('date')
            del sub_df.index.name
            source = ColumnDataSource(sub_df)
            wave_graph = get_graph(source)
            start_span, end_span = add_span(source)
            wave_graph.add_layout(start_span)
            wave_graph.add_layout(end_span)
            start_rng = start_span.location
            end_rng = end_span.location
        elif eind < list(df.index)[-1] and sind < list(
                df.index
        )[-1]:  #if the start is before but the end is after then just use the end of the dataframe
            sub_df = df.iloc[sind:list(df.index)[-1]]
            sub_df = sub_df.set_index('date')
            del sub_df.index.name
            source = ColumnDataSource(sub_df)
            wave_graph = get_graph(source)
            start_span, end_span = add_span(source)
            wave_graph.add_layout(start_span)
            wave_graph.add_layout(end_span)
            start_rng = start_span.location
            end_rng = end_span.location
        else:
            txt_processing.text = 'You have finished annotating this file.'
            return
        end_span.location = start_span.location
        #slider to change date range
        date_range_slider.start = start_rng
        date_range_slider.end = end_rng
        date_range_slider.value = start_rng
        date_range_slider.on_change('value', callback_date_time_slider)
        graph_layout = column(
            widgetbox([txt_processing, btn_load_annotated_graph, btn_save],
                      width=250),
            widgetbox(Div(text="""<hr/>""",
                          style={
                              'display': 'block',
                              'height': '1px',
                              'border': '0',
                              'border-top': '1px solid #css',
                              'margin': '1em 0',
                              'padding': '0'
                          }),
                      width=1400),
            widgetbox([rdo_btn_wave_lbls, btn_lbl], width=300),
            widgetbox(date_range_slider, width=1350),
            widgetbox(Div(text="""<hr/>""",
                          style={
                              'display': 'block',
                              'height': '1px',
                              'border': '0',
                              'border-top': '1px solid #css',
                              'margin': '1em 0',
                              'padding': '0'
                          }),
                      width=1400), wave_graph)
        wf_tab.child.children = [graph_layout]

    ''' Use a ColumnDataSource to plot the ECG lead II waveform data in a line plot.
        Parameters: source a ColumnDataSource
        Returns: p a Bokeh figure '''

    def get_graph(source):
        p = figure(plot_width=1400,
                   plot_height=500,
                   x_axis_type='datetime',
                   tools=['zoom_in', 'zoom_out', 'xpan', 'ypan'])
        date_range = source.data['II'][0:20000]
        p.y_range = Range1d(start=min(date_range) - 1, end=max(date_range) + 1)
        dt_axis_format = ["%d-%m-%Y %H:%M"]
        wf_x_axis = DatetimeTickFormatter(
            hours=dt_axis_format,
            days=dt_axis_format,
            months=dt_axis_format,
            years=dt_axis_format,
        )
        p.xaxis.formatter = wf_x_axis
        p.line(x='index',
               y='II',
               source=source,
               line_color='black',
               line_width=1)
        return p

    ''' Get the first and last time points from the ColumnDataSource (source).
        Utlizes tzlocal to add an offset which modifies the data.
        Returns integer timetuple value for the dates found: start, end '''

    def get_time(source):
        start = pd.to_datetime(min(source.data['index'])).timestamp() * 1000
        end = pd.to_datetime(max(source.data['index'])).timestamp() * 1000
        return start, end

    ''' Generate two Bokeh Spans based on the ColumnDataSource given (source).
        Spans are at the first and last datetimes in the source data.
        Parameters: source a ColumnDataSource
        Returns: Span, Span '''

    def add_span(source):
        start, end = get_time(source)
        # Start span represents the start of the area of interest
        start_span = Span(location=start,
                          dimension='height',
                          line_color='green',
                          line_dash='dashed',
                          line_width=3)
        # End span represents the end of the area of interest
        end_span = Span(location=end,
                        dimension='height',
                        line_color='red',
                        line_dash='dashed',
                        line_width=3)
        return start_span, end_span

    ''' Callback function for Bokeh Slider, move the Spans on the specified graph (generated in callback_file_table) to the location specified by the Span.
        Returns: Nothing '''

    def callback_date_time_slider(attr, old, new):
        inds = get_spans()
        wf_tab.child.children[0].children[5].renderers[inds[1]].location = new

    ''' Navigate through the widgets on the wave_graph to find the spans
        Returns the widget indexes of the spans'''

    def get_spans():
        inds = []
        for x in range(len(wf_tab.child.children[0].children[5].renderers)):
            if isinstance(wf_tab.child.children[0].children[5].renderers[x],
                          Span):
                inds.append(x)
        return inds

    ''' Callback function for btn_lbl.
        Get the location of the spans from the wave_graph, then call segment_and_label
        Return: Nothing '''

    def callback_btn_lbl():
        inds = get_spans()
        active = rdo_btn_wave_lbls.labels[rdo_btn_wave_lbls.active]
        start_span = wf_tab.child.children[0].children[5].renderers[inds[0]]
        end_span = wf_tab.child.children[0].children[5].renderers[inds[1]]
        segment_and_label(active, start_span.location, end_span.location)

    ''' Function to get ECG data between two Spans (after modifying the timetuple to timestamp).
        Call apply_annotations using start and end indexes found.
        Modify the global df_ann variable.
        Update slider position (start to end), (end to start).
        Parameters: label a string (AF, Normal, Noise, Other), start a timpletuple integer, end a timetuple integer
        Return: nothing '''

    def segment_and_label(label, start, end):
        global newECG
        print("*********************Segmenting and Labelling")
        try:
            txt_processing.text = "Use slider to select segments, label by selecting the wave type and pressing 'Label'. Use the save button to when done."
            start_dt = pd.Timestamp(start / 1000, unit='s')
            end_dt = pd.Timestamp(end / 1000, unit='s')
            mask = (newECG['date'] > start_dt) & (newECG['date'] <= end_dt)
            df_sub = newECG.loc[mask]  #apply mask
            indexes = list(df_sub.index)  #get indexes
            s_ind = indexes[0]  #get first index
            e_ind = indexes[-1]  #get last index
            apply_annotations(label, s_ind, e_ind,
                              df_ann)  #concatenate dataframe of annotations
            get_next_graph(e_ind, newECG)
        except IndexError:
            txt_processing.text = 'Indexing error. Advance slider.'

    ''' Function to apply annotations to a dataframe structured like that of Computing in Cardiology AF algorithm.
        Parameters: label a string (AF, Normal, Noise, Other), s_ind the index of the start datetime,
                    e_ind the index of the end datetime, df a pandas database to be appended to, columns= 'AF'
        Return: Nothing '''

    def apply_annotations(label, s_ind, e_ind, df):
        #data frame structure like this
        if label == 'AF':
            df.loc[s_ind, 0] = 'A'
        elif label == 'Not AF':
            df.loc[s_ind, 0] = 'nAF'
        elif label == 'Noise':
            df.loc[s_ind, 0] = '~'
        elif label == 'Normal':
            df.loc[s_ind, 0] = 'N'
        elif label == 'Other':
            df.loc[s_ind, 0] = 'O'
        elif label == 'No Signal':
            df.loc[s_ind, 0] = '-'

    ''' Stream update to ColumnDataSource that the file has been annotated'''

    def mark_as_done(file_path):
        print("*********************Marking as Done")
        table_source = file_table.source
        name = os.path.splitext(os.path.basename(file_path))[0]  #get file name
        ind = list(table_source.data['name']).index(
            name)  #get index within the source
        patches = {
            'annotated': [(ind, 'Yes')]
        }  #new data to update ColumnDataSource with
        table_source.patch(patches)  #update - ***** THROWING ERROR - CHECK!!

    ''' Callback function for btn_save.
        Utilizes the out_file global variable for the path.
        Appends dataframe to output file. '''

    def callback_save_annotations():
        print("*********************Saving Annotations")
        txt_processing.style = {"font-size": '1.2em', 'color': 'Red'}
        print("Writting: ", out_file)
        txt_processing.text = 'Saving Annotations...'
        df_ann.to_hdf(out_file, key='AF', format='t')
        print("success!")
        btn_save.disabled = True
        btn_lbl.disabled = True
        mark_as_done(out_file)
        nrows = df_ann.shape[0]
        df_ann.drop(df_ann.index[:nrows],
                    inplace=True)  #clear dataframe for a new file to be loaded
        btn_load_annotated_graph.disabled = False
        txt_processing.style = {"font-size": '1.2em', 'color': 'SteelBlue'}
        txt_processing.text = '''Done. Click 'Load Annotated Graph' to view annotations or to "File Management" to select new file to anntoate. You will need to reload this file to make changes.'''

    ''' Load annotations from AF formatted hdf file. '''

    def load_annotations(start, end, df, df_AF):
        # Initialize empty dataframes
        noise = pd.DataFrame()
        normal = pd.DataFrame()
        other = pd.DataFrame()
        af = pd.DataFrame()
        nosig = pd.DataFrame()
        notAF = pd.DataFrame()
        # Read annotations
        for n in range(start, end):
            df_temp = df.iloc[df_AF.index[n]:df_AF.index[n + 1]]
            df_temp.index = list(df_temp['date'])
            df_temp.drop(columns='date')
            value = df_AF.iloc[n, 0]
            if value == '~':
                noise = noise.append(df_temp)
            elif value == 'N':
                normal = normal.append(df_temp)
            elif value == 'O':
                other = other.append(df_temp)
            elif value == 'A':
                af = af.append(df_temp)
            elif value == '-':
                nosig = nosig.append(df_temp)
            elif value == 'nAF':
                notAF = notAF.append(df_temp)
        #add the last labelled section (end+1) to end of df
        df_temp = df.iloc[df_AF.index[end]:df.index[-1]]
        df_temp.index = list(df_temp['date'])
        df_temp.drop(columns='date')
        value = df_AF.iloc[end, 0]
        if value == '~':
            noise = noise.append(df_temp)
        elif value == 'N':
            normal = normal.append(df_temp)
        elif value == 'O':
            other = other.append(df_temp)
        elif value == 'A':
            af = af.append(df_temp)
        elif value == '-':
            nosig = nosig.append(df_temp)
        elif value == 'nAF':
            notAF = notAF.append(df_temp)
        return noise, normal, other, af, nosig, notAF

    ''' Create Bokeh figure from dataframes af, normal, other and noise. '''

    def get_graph_annotated(noise, normal, other, af, nosig, notAF):
        p = figure(
            plot_width=1400,
            plot_height=500,
            x_axis_type='datetime',
            tools=['box_zoom', 'wheel_zoom', 'pan', 'reset', 'crosshair'])
        # plot color coded waves (if they exist)
        if noise.empty is False:
            p.line(x='index',
                   y='II',
                   source=noise,
                   color='blue',
                   legend='Noise')
        if normal.empty is False:
            p.line(x='index',
                   y='II',
                   source=normal,
                   color='green',
                   legend='Normal')
        if other.empty is False:
            p.line(x='index',
                   y='II',
                   source=other,
                   color='purple',
                   legend='Other')
        if af.empty is False:
            p.line(x='index', y='II', source=af, color='red', legend='AF')
        if nosig.empty is False:
            p.line(x='index',
                   y='II',
                   source=nosig,
                   color='black',
                   legend='No Signal')
        if notAF.empty is False:
            p.line(x='index',
                   y='II',
                   source=notAF,
                   color='grey',
                   legend='No Signal')
        dt_axis_format = ["%d-%m-%Y %H:%M"]
        wf_x_axis = DatetimeTickFormatter(
            hours=dt_axis_format,
            days=dt_axis_format,
            months=dt_axis_format,
            years=dt_axis_format,
        )
        p.xaxis.formatter = wf_x_axis
        return p

    ############################## Assign Callbacks ##########################################
    #table_source.on_change('selected', callback_select_file_table) #assign callback
    table_source.selected.on_change('indices', callback_select_file_table)
    btn_lbl.on_click(callback_btn_lbl)
    btn_save.on_click(callback_save_annotations)
    btn_load_annotated_graph.on_click(load_output_graph)

    ################################## Load Document ##########################################
    ####layouts####
    file_layout = column(widgetbox(pgph_file_loaded, file_table, width=1000))
    col_waveforms = column(name="figures", sizing_mode='scale_width')
    col_output = column(name="output", sizing_mode='scale_width')

    ###tabs###
    wf_tab = Panel(child=col_waveforms, title='Label Data')
    file_tab = Panel(child=file_layout, title='File Management')
    output_tab = Panel(child=col_output, title='Final Annotated Graph')
    tab_pane = Tabs(tabs=[file_tab, wf_tab, output_tab], width=1000)

    ###combine into document####
    curdoc().add_root(column(tab_pane))
    curdoc().title = "AF Annotator 4"
예제 #3
0
 def _set_TB(self, specs):
     TB = Div(text=specs.text)
     TB.width = specs.width
     TB.height = specs.height
     TB.style = specs.style
     return TB