예제 #1
0
    def __init__(self, tk_parent, core, callback, **kwargs):
        '''
        *kwargs to core.list_specimens
        '''
        tk.Frame.__init__(self, tk_parent)

        self.tk_parent = tk_parent
        self.core = core
        self.callback = callback
        self._separator = ';'

        specimens = core.list_specimens(**kwargs)
        self.specimens_listbox = Listbox(self, specimens,
                                         self.on_specimen_selection)
        self.specimens_listbox.grid(row=0, column=0, sticky='NSWE')

        self.imagefolders_listbox = Listbox(self, [''], None)
        self.imagefolders_listbox.grid(row=0, column=1, sticky='NSWE')

        self.buttons_frame = ButtonsFrame(self,
                                          button_names=['Add', 'Remove', 'Ok'],
                                          horizontal=False,
                                          button_commands=[
                                              self.on_add_press,
                                              self.on_remove_press, self.on_ok
                                          ])
        self.buttons_frame.grid(row=0, column=2)

        self.selected_listbox = Listbox(self, [], None)
        self.selected_listbox.grid(row=0, column=3, sticky='NSWE')

        for i in [0, 1, 3]:
            self.grid_columnconfigure(i, weight=1)
        self.grid_rowconfigure(0, weight=1)
예제 #2
0
    def __init__(self,
                 parent,
                 fields=[],
                 start_data={},
                 selection_callback=None,
                 postchange_callback=None,
                 save_callback=None,
                 cancel_callback=None):
        '''
        parent : object
            Tkinter parent widget
        start_data : dict of dicts OR dict of lists
            Initial entries in form {item_name: {field1: string1, ...}}
            or {item_name: [string1, string2, ...]}
        '''

        tk.Frame.__init__(self, parent)

        self.current = None
        self.fields = fields
        self.data = start_data
        self.selection_callback = selection_callback
        self.postchange_callback = postchange_callback

        # Left side entries

        self.listbox = Listbox(self, list(self.data.keys()),
                               self._on_listbox_selection)
        self.listbox.grid(row=1, column=1, sticky='NSWE')

        # Right side buttons frame
        button_names = ['Add', 'Remove']
        button_commands = [self.add, self.remove]

        self.buttons = ButtonsFrame(self,
                                    button_names,
                                    button_commands,
                                    horizontal=False)
        self.buttons.grid(row=1, column=2)

        if callable(save_callback):
            tk.Button(self, text='Save', command=save_callback).grid(row=2,
                                                                     column=1)
        if callable(cancel_callback):
            tk.Button(self, text='Cancel',
                      command=cancel_callback).grid(row=2, column=2)

        # Specify stretching
        self.columnconfigure(1, weight=1)
        self.rowconfigure(1, weight=1)
예제 #3
0
    def __init__(self,
                 tk_parent,
                 axes,
                 rotations,
                 callback=None,
                 label='',
                 hide_none=True,
                 rotation_offset=(0, 0)):
        '''
        tk_parent : object
            Tkinter parent object
        axes : list
            List of matplotlib axes
        rotations : list of tuples
            Rotations [(elev, azim), ...]. If any None, keeps the corresponding
            rotation as it is.
        callback : None or callable
            Callback after each rotation update
        hide_none : bool
            When one of the rotations is None, hide the None from
            button text
        rotation_offset : tuple
            Offset in elevation and azitmuth, respectively in degrees
        '''
        tk.Frame.__init__(self, tk_parent)

        self.axes = axes
        self.rotation_offset = rotation_offset

        if hide_none:
            names = []
            for rotation in rotations:
                if None in rotation:
                    for r in rotation:
                        if r is not None:
                            names.append(r)
                else:
                    names.append(rotation)
        else:
            names = rotations

        commands = [
            lambda rot=rot: self.set_rotation(*rot) for rot in rotations
        ]
        self.buttons_frame = ButtonsFrame(self, names, commands, label=label)
        self.buttons_frame.grid(row=1, column=2)

        self.callback = callback
예제 #4
0
class RotationButtons(tk.Frame):
    '''
    Create buttons to set a matplotlib 3D plot rotations
    
    Attrubutes
    ----------
    axes : list
        Associated matplotlib axes that get rotated
    buttons_frame : object
        Buttons frame object containing the tkinter under the buttons attribute
    rotation_offset : tuple
        (elev, azim) offset in rotations
    callback : callable or None
        Additional callback to be called after changing rotation.
    '''
    def __init__(self,
                 tk_parent,
                 axes,
                 rotations,
                 callback=None,
                 label='',
                 hide_none=True,
                 rotation_offset=(0, 0)):
        '''
        tk_parent : object
            Tkinter parent object
        axes : list
            List of matplotlib axes
        rotations : list of tuples
            Rotations [(elev, azim), ...]. If any None, keeps the corresponding
            rotation as it is.
        callback : None or callable
            Callback after each rotation update
        hide_none : bool
            When one of the rotations is None, hide the None from
            button text
        rotation_offset : tuple
            Offset in elevation and azitmuth, respectively in degrees
        '''
        tk.Frame.__init__(self, tk_parent)

        self.axes = axes
        self.rotation_offset = rotation_offset

        if hide_none:
            names = []
            for rotation in rotations:
                if None in rotation:
                    for r in rotation:
                        if r is not None:
                            names.append(r)
                else:
                    names.append(rotation)
        else:
            names = rotations

        commands = [
            lambda rot=rot: self.set_rotation(*rot) for rot in rotations
        ]
        self.buttons_frame = ButtonsFrame(self, names, commands, label=label)
        self.buttons_frame.grid(row=1, column=2)

        self.callback = callback

    def set_rotation(self, elev, azim):
        for ax in self.axes:
            if elev is None:
                uelev = ax.elev
            else:
                uelev = elev + self.rotation_offset[0]

            if azim is None:
                uazim = ax.azim
            else:
                uazim = azim + self.rotation_offset[1]

            ax.view_init(uelev, uazim)

        if callable(self.callback):
            self.callback()
예제 #5
0
class ImagefolderMultisel(tk.Frame):
    '''
    Widget to select image folders from the specimens
    
    Attributes
    ----------
    core
    specimens_listbox
    imagefolders_listbox
    buttons_frame
    '''
    def __init__(self, tk_parent, core, callback, **kwargs):
        '''
        *kwargs to core.list_specimens
        '''
        tk.Frame.__init__(self, tk_parent)

        self.tk_parent = tk_parent
        self.core = core
        self.callback = callback
        self._separator = ';'

        specimens = core.list_specimens(**kwargs)
        self.specimens_listbox = Listbox(self, specimens,
                                         self.on_specimen_selection)
        self.specimens_listbox.grid(row=0, column=0, sticky='NSWE')

        self.imagefolders_listbox = Listbox(self, [''], None)
        self.imagefolders_listbox.grid(row=0, column=1, sticky='NSWE')

        self.buttons_frame = ButtonsFrame(self,
                                          button_names=['Add', 'Remove', 'Ok'],
                                          horizontal=False,
                                          button_commands=[
                                              self.on_add_press,
                                              self.on_remove_press, self.on_ok
                                          ])
        self.buttons_frame.grid(row=0, column=2)

        self.selected_listbox = Listbox(self, [], None)
        self.selected_listbox.grid(row=0, column=3, sticky='NSWE')

        for i in [0, 1, 3]:
            self.grid_columnconfigure(i, weight=1)
        self.grid_rowconfigure(0, weight=1)

    def on_specimen_selection(self, name):
        analyser = self.core.get_manalyser(name)
        image_folders = analyser.list_imagefolders()
        self.imagefolders_listbox.set_selections(image_folders)

    def on_add_press(self):
        image_folder = self.imagefolders_listbox.current
        if image_folder:
            sel = self.specimens_listbox.current + self._separator + image_folder

            selections = self.selected_listbox.selections + [sel]
            self.selected_listbox.set_selections(selections)

    def on_remove_press(self):
        to_remove = self.selected_listbox.current
        if to_remove:
            selections = self.selected_listbox.selections
            selections.remove(to_remove)

            self.selected_listbox.set_selections(selections)

    def on_ok(self):
        image_folders = {}
        for z in self.selected_listbox.selections:
            s, i = z.split(self._separator)
            if s not in image_folders:
                image_folders[s] = []
            image_folders[s].append(i)

        self.callback(image_folders)
        self.tk_parent.destroy()
예제 #6
0
class ItemManager(tk.Frame):
    '''
    Adding, removing, renaming items in a list.
    
    Attributes
    ----------
    current : string or None
        Name of the currently selected item (item_name)
    data : dict of dicts
        {item_name: {field1: string1, ...}}
    fields : list of strings
        Names of the fields user can input data (in addition to the
        name of the entry)
    listbox : object
        tk_steroids Listbox (tkinter Listbox object by self.listbox.listbox)
    buttons : object
        tk_steroids ButtonsFrame (self.buttons.buttons for underlying tkinter
        button objects)
    
    selection_callback : callable
        Called when an item is selected
    postchange_callback : callable
        Called after an item is removed, renamed or added
    save_callback : callable
        Callable when pressing save button
    cancel_callback : callable
        Callable when pressing close button
    '''
    def __init__(self,
                 parent,
                 fields=[],
                 start_data={},
                 selection_callback=None,
                 postchange_callback=None,
                 save_callback=None,
                 cancel_callback=None):
        '''
        parent : object
            Tkinter parent widget
        start_data : dict of dicts OR dict of lists
            Initial entries in form {item_name: {field1: string1, ...}}
            or {item_name: [string1, string2, ...]}
        '''

        tk.Frame.__init__(self, parent)

        self.current = None
        self.fields = fields
        self.data = start_data
        self.selection_callback = selection_callback
        self.postchange_callback = postchange_callback

        # Left side entries

        self.listbox = Listbox(self, list(self.data.keys()),
                               self._on_listbox_selection)
        self.listbox.grid(row=1, column=1, sticky='NSWE')

        # Right side buttons frame
        button_names = ['Add', 'Remove']
        button_commands = [self.add, self.remove]

        self.buttons = ButtonsFrame(self,
                                    button_names,
                                    button_commands,
                                    horizontal=False)
        self.buttons.grid(row=1, column=2)

        if callable(save_callback):
            tk.Button(self, text='Save', command=save_callback).grid(row=2,
                                                                     column=1)
        if callable(cancel_callback):
            tk.Button(self, text='Cancel',
                      command=cancel_callback).grid(row=2, column=2)

        # Specify stretching
        self.columnconfigure(1, weight=1)
        self.rowconfigure(1, weight=1)

    def add(self):
        name = tkinter.simpledialog.askstring('New entry', 'Item name')
        if name:
            self.data[name] = {}

        self.listbox.set_selections(list(self.data.keys()))

        if self.postchange_callback:
            self.postchange_callback(self.data)

    def remove(self):

        del self.data[self.current]

        self.listbox.set_selections(list(self.data.keys()))

        if self.postchange_callback:
            self.postchange_callback(self.data)

    def _on_listbox_selection(self, selection):
        self.current = selection

        if self.selection_callback:
            self.selection_callback(self.data[selection])

    def set_data(self, data):
        '''
        data : list or dict
            [item1, item2, ...] or {item1: {}, item2: {}}
            where item_i is a string.
        '''
        if isinstance(data, list):
            self.data = {key: {} for key in data}
        else:
            self.data = data

        self.listbox.set_selections(list(self.data.keys()))
예제 #7
0
    def __init__(self, parent):

        self.last_saveplotter_dir = GONIODIR

        tk.Frame.__init__(self, parent)

        self.core = Core()
        self.core.update_gui = self.update_specimen

        self.root = self.winfo_toplevel()

        # Make canvas plotter to stretch
        self.grid_rowconfigure(0, weight=1)
        self.grid_columnconfigure(1, weight=3)
        self.grid_columnconfigure(0, weight=1)
        self.grid_columnconfigure(0, minsize=400)

        #tk.Button(self, text='Set data directory...', command=self.set_data_directory).grid(row=0, column=0)

        # Uncomment to have the menubar
        self.menu = ExamineMenubar(self)

        # LEFTSIDE frame
        self.leftside_frame = tk.Frame(self)
        self.leftside_frame.grid(row=0, column=0, sticky='NSWE')
        self.leftside_frame.grid_rowconfigure(4, weight=1)
        self.leftside_frame.grid_columnconfigure(0, weight=1)
        self.leftside_frame.grid_columnconfigure(1, weight=1)

        # The 1st buttons frame, selecting root data directory
        self.buttons_frame_1 = ButtonsFrame(
            self.leftside_frame, ['Set data directory'], [
                self.menu.file_commands.set_data_directory,
                self.menu.file_commands.set_data_directory
            ])
        self.buttons_frame_1.grid(row=0, column=0, sticky='NW', columnspan=2)

        self.specimen_control_frame = tk.LabelFrame(self.leftside_frame,
                                                    text='Specimen')
        self.specimen_control_frame.grid(row=1,
                                         column=0,
                                         sticky='NWES',
                                         columnspan=2)

        # The 2nd buttons frame, ROIs and movements
        self.buttons_frame_2 = ButtonsFrame(
            self.specimen_control_frame, ['Select ROIs', 'Measure movement'], [
                self.menu.specimen_commands.select_ROIs,
                self.menu.specimen_commands.measure_movement
            ])
        self.buttons_frame_2.grid(row=1, column=0, sticky='NW', columnspan=2)
        self.button_rois, self.button_measure = self.buttons_frame_2.get_buttons(
        )

        # Subframe for 2nd buttons frame
        #self.status_frame = tk.Frame(self.leftside_frame)
        #self.status_frame.grid(row=2)

        self.status_rois = tk.Label(self.specimen_control_frame,
                                    text='ROIs selected 0/0',
                                    font=('system', 8))
        self.status_rois.grid(row=2, column=0, sticky='W')

        self.status_antenna_level = tk.Label(self.specimen_control_frame,
                                             text='Zero correcter N/A',
                                             font=('system', 8))
        #self.status_antenna_level.grid(row=3, column=0, sticky='W')

        self.status_active_analysis = tk.Label(self.specimen_control_frame,
                                               text='Active analysis: default',
                                               font=('system', 8),
                                               justify=tk.LEFT)
        self.status_active_analysis.grid(row=4, column=0, sticky='W')

        self.tickbox_analyses = TickboxFrame(self.specimen_control_frame, [],
                                             ncols=4)
        self.tickbox_analyses.grid(row=5, column=0, sticky='W')

        # Image folder manipulations
        self.folder_control_frame = tk.LabelFrame(self.leftside_frame,
                                                  text='Image folder')
        self.folder_control_frame.grid(row=2,
                                       column=0,
                                       sticky='NWES',
                                       columnspan=2)

        self.buttons_frame_3 = ButtonsFrame(
            self.folder_control_frame, ['Reselect ROI', 'Remeasure'], [
                self.menu.imagefolder_commands.select_ROIs,
                self.menu.imagefolder_commands.measure_movement
            ])
        self.buttons_frame_3.grid(row=1, column=0, sticky='NW', columnspan=2)
        self.button_one_roi = self.buttons_frame_2.get_buttons()[0]

        self.status_horizontal = tk.Label(self.folder_control_frame,
                                          text='Horizontal angle N/A',
                                          font=('system', 8))
        self.status_horizontal.grid(row=2, column=0, sticky='W')

        self.status_vertical = tk.Label(self.folder_control_frame,
                                        text='Vertical angle N/A',
                                        font=('system', 8))
        self.status_vertical.grid(row=3, column=0, sticky='W')

        # Selecting the specimen
        tk.Label(self.leftside_frame, text='Specimens').grid(row=3, column=0)
        self.specimen_box = Listbox(self.leftside_frame,
                                    ['(select directory)'],
                                    self.on_specimen_selection)
        self.specimen_box.grid(row=4, column=0, sticky='NSEW')

        # Selecting the recording
        tk.Label(self.leftside_frame, text='Image folders').grid(row=3,
                                                                 column=1)
        self.recording_box = Listbox(self.leftside_frame, [''],
                                     self.on_recording_selection)
        self.recording_box.grid(row=4, column=1, sticky='NSEW')

        # Add color explanation frame in the bottom
        ColorExplanation(self.leftside_frame, ['white', 'green', 'yellow'],
                         ['Movements measured', 'ROIs selected', 'No ROIs'
                          ]).grid(row=5, column=0, sticky='NW')

        # RIGHTSIDE frame
        self.rightside_frame = tk.Frame(self)
        self.rightside_frame.grid(row=0, column=1, sticky='NWES')

        tab_kwargs = [{}, {}, {}, {'projection': '3d'}]
        tab_names = ['ROI', 'Displacement', 'XY', '3D']
        canvas_constructors = [
            lambda parent, kwargs=kwargs: CanvasPlotter(
                parent, visibility_button=False, **kwargs)
            for kwargs in tab_kwargs
        ]
        self.tabs = Tabs(self.rightside_frame,
                         tab_names,
                         canvas_constructors,
                         on_select_callback=self.update_plot)

        self.tabs.grid(row=0, column=0, sticky='NWES')

        # Make canvas plotter to stretch
        self.rightside_frame.grid_rowconfigure(0, weight=1)
        self.rightside_frame.grid_columnconfigure(0, weight=1)

        self.canvases = self.tabs.get_elements()

        # Controls for displacement plot (means etc)
        displacementplot_options, displacementplot_defaults = inspect_booleans(
            plot_1d_magnitude, exclude_keywords=['mean_imagefolders'])
        self.displacement_ticks = TickboxFrame(
            self.canvases[1],
            displacementplot_options,
            defaults=displacementplot_defaults,
            callback=lambda: self.update_plot(1))
        self.displacement_ticks.grid()

        xyplot_options, xyplot_defaults = inspect_booleans(plot_xy_trajectory)
        self.xy_ticks = TickboxFrame(self.canvases[2],
                                     xyplot_options,
                                     defaults=xyplot_defaults,
                                     callback=lambda: self.update_plot(2))
        self.xy_ticks.grid()

        # Controls for the vector plot
        # Controls for displacement plot (means etc)
        vectorplot_options, vectorplot_defaults = inspect_booleans(
            plot_3d_vectormap, exclude_keywords=[])
        self.vectorplot_ticks = TickboxFrame(
            self.canvases[3],
            vectorplot_options,
            defaults=vectorplot_defaults,
            callback=lambda: self.update_plot(3))
        self.vectorplot_ticks.grid()

        tk.Button(self.canvases[3],
                  text='Save animation',
                  command=self.save_3d_animation).grid()

        self.default_button_bg = self.button_rois.cget('bg')

        self.plotter = RecordingPlotter(self.core)

        # Add buttons for selecting single repeats from a recording
        self.repetition_selector = RepetitionSelector(
            self.rightside_frame,
            self.plotter,
            self.core,
            update_command=lambda: self.on_recording_selection('current'))
        self.repetition_selector.grid(row=1, column=0)

        tk.Button(self.repetition_selector,
                  text='Copy data',
                  command=self.copy_plotter_to_clipboard).grid(row=0, column=5)

        tk.Button(self.repetition_selector,
                  text='Save view...',
                  command=self.save_plotter_view).grid(row=0, column=6)
예제 #8
0
class ExamineView(tk.Frame):
    '''
    The examine frame. Selection of
    - data directory
    - specimen
    - recording
    and plotting the intemediate result for each recording.
    
    '''
    def __init__(self, parent):

        self.last_saveplotter_dir = GONIODIR

        tk.Frame.__init__(self, parent)

        self.core = Core()
        self.core.update_gui = self.update_specimen

        self.root = self.winfo_toplevel()

        # Make canvas plotter to stretch
        self.grid_rowconfigure(0, weight=1)
        self.grid_columnconfigure(1, weight=3)
        self.grid_columnconfigure(0, weight=1)
        self.grid_columnconfigure(0, minsize=400)

        #tk.Button(self, text='Set data directory...', command=self.set_data_directory).grid(row=0, column=0)

        # Uncomment to have the menubar
        self.menu = ExamineMenubar(self)

        # LEFTSIDE frame
        self.leftside_frame = tk.Frame(self)
        self.leftside_frame.grid(row=0, column=0, sticky='NSWE')
        self.leftside_frame.grid_rowconfigure(4, weight=1)
        self.leftside_frame.grid_columnconfigure(0, weight=1)
        self.leftside_frame.grid_columnconfigure(1, weight=1)

        # The 1st buttons frame, selecting root data directory
        self.buttons_frame_1 = ButtonsFrame(
            self.leftside_frame, ['Set data directory'], [
                self.menu.file_commands.set_data_directory,
                self.menu.file_commands.set_data_directory
            ])
        self.buttons_frame_1.grid(row=0, column=0, sticky='NW', columnspan=2)

        self.specimen_control_frame = tk.LabelFrame(self.leftside_frame,
                                                    text='Specimen')
        self.specimen_control_frame.grid(row=1,
                                         column=0,
                                         sticky='NWES',
                                         columnspan=2)

        # The 2nd buttons frame, ROIs and movements
        self.buttons_frame_2 = ButtonsFrame(
            self.specimen_control_frame, ['Select ROIs', 'Measure movement'], [
                self.menu.specimen_commands.select_ROIs,
                self.menu.specimen_commands.measure_movement
            ])
        self.buttons_frame_2.grid(row=1, column=0, sticky='NW', columnspan=2)
        self.button_rois, self.button_measure = self.buttons_frame_2.get_buttons(
        )

        # Subframe for 2nd buttons frame
        #self.status_frame = tk.Frame(self.leftside_frame)
        #self.status_frame.grid(row=2)

        self.status_rois = tk.Label(self.specimen_control_frame,
                                    text='ROIs selected 0/0',
                                    font=('system', 8))
        self.status_rois.grid(row=2, column=0, sticky='W')

        self.status_antenna_level = tk.Label(self.specimen_control_frame,
                                             text='Zero correcter N/A',
                                             font=('system', 8))
        #self.status_antenna_level.grid(row=3, column=0, sticky='W')

        self.status_active_analysis = tk.Label(self.specimen_control_frame,
                                               text='Active analysis: default',
                                               font=('system', 8),
                                               justify=tk.LEFT)
        self.status_active_analysis.grid(row=4, column=0, sticky='W')

        self.tickbox_analyses = TickboxFrame(self.specimen_control_frame, [],
                                             ncols=4)
        self.tickbox_analyses.grid(row=5, column=0, sticky='W')

        # Image folder manipulations
        self.folder_control_frame = tk.LabelFrame(self.leftside_frame,
                                                  text='Image folder')
        self.folder_control_frame.grid(row=2,
                                       column=0,
                                       sticky='NWES',
                                       columnspan=2)

        self.buttons_frame_3 = ButtonsFrame(
            self.folder_control_frame, ['Reselect ROI', 'Remeasure'], [
                self.menu.imagefolder_commands.select_ROIs,
                self.menu.imagefolder_commands.measure_movement
            ])
        self.buttons_frame_3.grid(row=1, column=0, sticky='NW', columnspan=2)
        self.button_one_roi = self.buttons_frame_2.get_buttons()[0]

        self.status_horizontal = tk.Label(self.folder_control_frame,
                                          text='Horizontal angle N/A',
                                          font=('system', 8))
        self.status_horizontal.grid(row=2, column=0, sticky='W')

        self.status_vertical = tk.Label(self.folder_control_frame,
                                        text='Vertical angle N/A',
                                        font=('system', 8))
        self.status_vertical.grid(row=3, column=0, sticky='W')

        # Selecting the specimen
        tk.Label(self.leftside_frame, text='Specimens').grid(row=3, column=0)
        self.specimen_box = Listbox(self.leftside_frame,
                                    ['(select directory)'],
                                    self.on_specimen_selection)
        self.specimen_box.grid(row=4, column=0, sticky='NSEW')

        # Selecting the recording
        tk.Label(self.leftside_frame, text='Image folders').grid(row=3,
                                                                 column=1)
        self.recording_box = Listbox(self.leftside_frame, [''],
                                     self.on_recording_selection)
        self.recording_box.grid(row=4, column=1, sticky='NSEW')

        # Add color explanation frame in the bottom
        ColorExplanation(self.leftside_frame, ['white', 'green', 'yellow'],
                         ['Movements measured', 'ROIs selected', 'No ROIs'
                          ]).grid(row=5, column=0, sticky='NW')

        # RIGHTSIDE frame
        self.rightside_frame = tk.Frame(self)
        self.rightside_frame.grid(row=0, column=1, sticky='NWES')

        tab_kwargs = [{}, {}, {}, {'projection': '3d'}]
        tab_names = ['ROI', 'Displacement', 'XY', '3D']
        canvas_constructors = [
            lambda parent, kwargs=kwargs: CanvasPlotter(
                parent, visibility_button=False, **kwargs)
            for kwargs in tab_kwargs
        ]
        self.tabs = Tabs(self.rightside_frame,
                         tab_names,
                         canvas_constructors,
                         on_select_callback=self.update_plot)

        self.tabs.grid(row=0, column=0, sticky='NWES')

        # Make canvas plotter to stretch
        self.rightside_frame.grid_rowconfigure(0, weight=1)
        self.rightside_frame.grid_columnconfigure(0, weight=1)

        self.canvases = self.tabs.get_elements()

        # Controls for displacement plot (means etc)
        displacementplot_options, displacementplot_defaults = inspect_booleans(
            plot_1d_magnitude, exclude_keywords=['mean_imagefolders'])
        self.displacement_ticks = TickboxFrame(
            self.canvases[1],
            displacementplot_options,
            defaults=displacementplot_defaults,
            callback=lambda: self.update_plot(1))
        self.displacement_ticks.grid()

        xyplot_options, xyplot_defaults = inspect_booleans(plot_xy_trajectory)
        self.xy_ticks = TickboxFrame(self.canvases[2],
                                     xyplot_options,
                                     defaults=xyplot_defaults,
                                     callback=lambda: self.update_plot(2))
        self.xy_ticks.grid()

        # Controls for the vector plot
        # Controls for displacement plot (means etc)
        vectorplot_options, vectorplot_defaults = inspect_booleans(
            plot_3d_vectormap, exclude_keywords=[])
        self.vectorplot_ticks = TickboxFrame(
            self.canvases[3],
            vectorplot_options,
            defaults=vectorplot_defaults,
            callback=lambda: self.update_plot(3))
        self.vectorplot_ticks.grid()

        tk.Button(self.canvases[3],
                  text='Save animation',
                  command=self.save_3d_animation).grid()

        self.default_button_bg = self.button_rois.cget('bg')

        self.plotter = RecordingPlotter(self.core)

        # Add buttons for selecting single repeats from a recording
        self.repetition_selector = RepetitionSelector(
            self.rightside_frame,
            self.plotter,
            self.core,
            update_command=lambda: self.on_recording_selection('current'))
        self.repetition_selector.grid(row=1, column=0)

        tk.Button(self.repetition_selector,
                  text='Copy data',
                  command=self.copy_plotter_to_clipboard).grid(row=0, column=5)

        tk.Button(self.repetition_selector,
                  text='Save view...',
                  command=self.save_plotter_view).grid(row=0, column=6)

    def _color_specimens(self, specimens):
        '''
        See _color_recording for reference.
        '''
        colors = []
        for specimen in specimens:
            analyser = self.core.get_manalyser(specimen, no_data_load=True)
            color = 'yellow'
            if analyser.are_rois_selected():
                color = 'green'
                if analyser.is_measured():
                    color = 'white'

            colors.append(color)
        return colors

    def save_3d_animation(self):
        def callback():
            self.canvases[3].update()

        fig, ax = self.canvases[3].get_figax()
        save_3d_animation(self.core.analyser,
                          ax=ax,
                          interframe_callback=callback)

    def copy_to_csv(self, formatted):
        with open(os.path.join(GONIODIR, 'clipboard.csv'), 'w') as fp:
            fp.write(formatted.replace('\t', ','))

    def specimen_traces_to_clipboard(self, mean=False):
        '''
        If mean==True, copy only the average trace.
        Otherwise, copy all the traces of the fly.
        '''

        formatted = ''

        # Always first clear clipboard; If something goes wrong, the user
        # doesn't want to keep pasting old data thinking it's new.
        self.root.clipboard_clear()

        if self.core.selected_recording is None:
            return None

        data = []

        self.root.clipboard_append(formatted)

        for pos_folder in self.core.analyser.list_imagefolders():
            all_movements = self.core.analyser.get_movements_from_folder(
                pos_folder)

            for eye, movements in all_movements.items():
                for repetition in range(len(movements)):
                    mag = np.sqrt(
                        np.array(movements[repetition]['x'])**2 +
                        np.array(movements[repetition]['y'])**2)
                    data.append(mag)

        if mean:
            data = [np.mean(data, axis=0)]

        for i_frame in range(len(data[0])):
            formatted += '\t'.join([
                str(data[i_repeat][i_frame]) for i_repeat in range(len(data))
            ]) + '\n'

        self.root.clipboard_append(formatted)
        self.copy_to_csv(formatted)

    def copy_plotter_to_clipboard(self, force_i_tab=None):
        '''
        Copies data currently visible on theopen plotter tab to the clipboard.
        
        force_i_tab         Copy from the specified tab index, instead of
                            the currently opened tab
        '''
        formatted = ''

        # Always first clear clipboard; If something goes wrong, the user
        # doesn't want to keep pasting old data thinking it's new.
        self.root.clipboard_clear()

        if self.core.selected_recording is None:
            return None

        if force_i_tab is not None:
            i_tab = int(force_i_tab)
        else:
            i_tab = self.tabs.i_current

        # Make sure we have the correct data in the plot by reissuing
        # the plotting command
        self.update_plot(i_tab)

        # Select data based on where we want to copy
        if i_tab == 0:
            data = self.plotter.image
        elif i_tab == 1:
            data = self.plotter.magnitudes
        elif i_tab == 2:
            data = self.plotter.xys
            data = list(itertools.chain(*data))
        elif i_tab == 3:
            raise NotImplementedError('Cannot yet cliboard vectormap data')

        # Format the data for tkinter clipboard copy
        for i_frame in range(len(data[0])):
            formatted += '\t'.join([
                str(data[i_repeat][i_frame]) for i_repeat in range(len(data))
            ]) + '\n'

        self.root.clipboard_append(formatted)
        self.copy_to_csv(formatted)

    def save_plotter_view(self):
        '''
        Launches a save dialog for the current plotter view.
        '''
        fig, ax = self.canvases[self.tabs.i_current].get_figax()

        dformats = fig.canvas.get_supported_filetypes()
        formats = [(value, '*.' + key)
                   for key, value in sorted(dformats.items())]

        # Make png first
        if 'png' in dformats.keys():
            i = formats.index((dformats['png'], '*.png'))
            formats.insert(0, formats.pop(i))

        fn = filedialog.asksaveasfilename(title='Save current view',
                                          initialdir=self.last_saveplotter_dir,
                                          filetypes=formats)

        if fn:
            self.last_saveplotter_dir = os.path.dirname(fn)

            fig.savefig(fn, dpi=1200)

    def _color_recordings(self, recordings):
        '''
        Returns a list of colours, each corresponding to a recording
        in recordings, based on wheter the ROIs have been selected or
        movements measured for the recording.

        yellow      No ROIs, no movements
        green       ROIs, no movements
        white       ROIs and movements
        '''
        colors = []
        for recording in recordings:
            color = 'yellow'
            if self.core.analyser.folder_has_rois(recording):
                color = 'green'
                if self.core.analyser.folder_has_movements(recording):
                    color = 'white'

            colors.append(color)
        return colors

    def on_specimen_selection(self, specimen):
        '''
        When a selection happens in the specimens listbox.
        '''
        self.specimen_control_frame.config(text=specimen)

        self.core.set_current_specimen(specimen)

        # Recordings box
        recordings = self.core.analyser.list_imagefolders()
        self.recording_box.enable()
        self.recording_box.set_selections(
            recordings, colors=self._color_recordings(recordings))

        # Logick to set buttons inactive/active and their texts
        if self.core.analyser.are_rois_selected():

            self.button_rois.config(text='Reselect ROIs')
            self.button_rois.config(bg=self.default_button_bg)

            self.button_measure.config(state=tk.NORMAL)

            self.button_one_roi.config(state=tk.NORMAL)

            # Enable image_folder buttons
            for button in self.buttons_frame_3.get_buttons():
                button.config(state=tk.NORMAL)

            if self.core.analyser.is_measured():
                self.button_measure.config(text='Remeasure movement')
                self.button_measure.config(bg=self.default_button_bg)
            else:
                self.button_measure.config(text='Measure movement')
                self.button_measure.config(bg='green')
        else:
            #self.recording_box.disable()

            self.button_rois.config(text='Select ROIs')
            self.button_rois.config(bg='yellow')

            self.button_measure.config(state=tk.DISABLED)
            self.button_measure.config(text='Measure movement')
            self.button_measure.config(bg=self.default_button_bg)

            self.button_one_roi.config(state=tk.DISABLED)

            # Disable image_folder buttons
            for button in self.buttons_frame_3.get_buttons():
                button.config(state=tk.DISABLED)

        if self.core.analyser.are_rois_selected():
            self.core.analyser.load_ROIs()

        # Loading cached analyses and setting the recordings listbox

        if self.core.analyser.is_measured():
            self.core.analyser.load_analysed_movements()
            #self.recording_box.enable()

        N_rois = self.core.analyser.count_roi_selected_folders()
        N_image_folders = len(self.core.analyser.list_imagefolders())
        self.status_rois.config(
            text='ROIs selected {}/{}'.format(N_rois, N_image_folders))

        try:
            self.correction = self.core.analyser.get_antenna_level_correction()
        except:
            self.correction = False
        if self.correction is not False:
            self.status_antenna_level.config(
                text='Zero corrected, {:.2f} degrees'.format(self.correction))
        else:
            self.status_antenna_level.config(text='Zero corrected FALSE')

        self.status_active_analysis.config(text='Active analysis: {}'.format(
            self.core.analyser.active_analysis))

        # FIXME Instead of destroyign tickbox, make changes to tk_steroids
        # so that the selections can be reset
        self.tickbox_analyses.grid_forget()
        self.tickbox_analyses.destroy()
        self.tickbox_analyses = TickboxFrame(
            self.specimen_control_frame,
            self.core.analyser.list_analyses(),
            defaults=[
                self.core.analyser.active_analysis == an
                for an in self.core.analyser.list_analyses()
            ],
            ncols=4,
            callback=lambda: self.update_plot(None))
        self.tickbox_analyses.grid(row=5, column=0, sticky='W')

        self.button_rois.config(state=tk.NORMAL)

    def on_recording_selection(self, selected_recording):
        '''
        When a selection happens in the recordings listbox.
        
        selected_recording      Name of the recording. If 'current', keeps the current
        '''
        if selected_recording == 'current':
            selected_recording = self.core.selected_recording
        else:
            self.core.set_selected_recording(selected_recording)

        print(self.core.analyser.get_recording_time(selected_recording))

        angles = [list(angles_from_fn(selected_recording))]
        to_degrees(angles)
        horizontal, vertical = angles[0]
        self.status_horizontal.config(
            text='Horizontal angle {:.2f} degrees'.format(horizontal))
        self.status_vertical.config(
            text='Vertical angle {:.2f} degrees'.format(vertical))

        # Plotting only the view we have currently open
        self.update_plot(self.tabs.i_current)

    def update_plot(self, i_plot):
        '''
        i_plot : int or None
            Index of the plot (from 0 to N-1 tabs) or None just to update
        '''
        if self.core.selected_recording is None:
            return None

        if i_plot is None:
            i_plot = self.tabs.i_current

        fig, ax = self.canvases[i_plot].get_figax()

        if i_plot == 0:
            self.plotter.ROI(ax)
        else:

            ax.clear()

            remember_analysis = self.core.analyser.active_analysis

            for analysis in [
                    name
                    for name, state in self.tickbox_analyses.states.items()
                    if state == True
            ]:

                self.core.analyser.active_analysis = analysis

                if i_plot == 1:
                    self.plotter.magnitude(ax,
                                           **self.displacement_ticks.states)
                elif i_plot == 2:
                    self.plotter.xy(ax, **self.xy_ticks.states)
                elif i_plot == 3:
                    self.plotter.vectormap(ax, **self.vectorplot_ticks.states)

            self.core.active_analysis = remember_analysis

        self.canvases[i_plot].update()

        self.repetition_selector.update_text()

    def update_specimen(self, changed_specimens=False):
        '''
        Updates GUI colors, button states etc. to right values.
        
        Call this if there has been changes to specimens/image_folders by an
        external process or similar.
        '''
        if changed_specimens:
            specimens = self.core.list_specimens()
            self.specimen_box.set_selections(specimens,
                                             self._color_specimens(specimens))

        if self.core.current_specimen is not None:
            self.on_specimen_selection(self.core.current_specimen)
예제 #9
0
class MovemeterTkGui(tk.Frame):
    '''
    Class documentation TODO.
    
    
    exclude_images : list of integers and/or strings
        Images to skip by file name or by index
    '''

    def __init__(self, tk_parent):
        tk.Frame.__init__(self, tk_parent)
        self.parent = tk_parent

        self.current_folder = None
        self.folders = []
        self.image_fns = []
        self.images = None
        self.exclude_images = []

        self.selections = []
        self.mask_image = None
        self.roi_groups = []
        self.current_roi_group = 0
        self.roi_patches = []
        self.results = []
        
        self.heatmap_images = []
        
        self.movemeter = None
        self.fs = 100

        self.show_controls = False
        self.use_mask_image = False

        self.batch_name = 'batch_name'

        self.colors = matplotlib.cm.ScalarMappable(cmap=matplotlib.cm.tab10)
        self.colors.set_clim(0,10)
        

        # Top menu
        # --------------------------------
        self.menu = tk.Menu(self)
        
        filemenu = tk.Menu(self)
        filemenu.add_command(label='Add directory...', command=self.open_directory)
        filemenu.add_separator()
        filemenu.add_command(label='Load ROIs',
                command=lambda: self.apply_movzip(rois=True))
        filemenu.add_command(label='Save ROIs',
                command=lambda: self._save_movzip(only=['rois', 'selections']))
        
        filemenu.add_separator()
        filemenu.add_command(label='Save ROI view',
                command=self.save_roiview)
        
        filemenu.add_command(label='Save ROIs only view',
                command=lambda: self.save_roiview(only_rois=True))
        filemenu.add_separator()
        filemenu.add_command(label='Quit', command=self.parent.destroy) 
        
        self.menu.add_cascade(label='File', menu=filemenu)
        

        editmenu = tk.Menu(self)
        editmenu.add_command(label='Undo (latest ROI)', command=self.undo)
        editmenu.add_separator()
        editmenu.add_command(label='Global settings', command=self.open_settings)
        self.menu.add_cascade(label='Edit', menu=editmenu)

        viewmenu = tk.Menu(self)
        viewmenu.add_command(label='Show image controls', command=self.toggle_controls)
        self.menu.add_cascade(label='View', menu=viewmenu)
        
        batchmenu = tk.Menu(self)
        batchmenu.add_command(label='Batch measure & save all', command=self.batch_process)
        batchmenu.add_separator()
        batchmenu.add_command(label='Reprocess old', command=self.recalculate_old)
        batchmenu.add_command(label='Replot heatmap', command=self.replot_heatmap)
 
        self.menu.add_cascade(label='Batch', menu=batchmenu)

        toolmenu = tk.Menu(self)
        toolmenu.add_command(label='Heatmap tool', command=lambda: open_httool(self))
        self.menu.add_cascade(label='Tools', menu=toolmenu)
        


        self.parent.config(menu=self.menu)

        


        # Input folders

        self.folview = tk.LabelFrame(self, text='Input folders')
        self.folview.rowconfigure(2, weight=1)
        self.folview.columnconfigure(1, weight=1)
        self.folview.grid(row=0, column=1, sticky='NSWE')

        self.folders_listbox = Listbox(self.folview, ['No folders selected'], self.folder_selected)
        self.folders_listbox.listbox.config(height=10)
        self.folders_listbox.grid(row=2, column=1, columnspan=2, sticky='NSWE')

        self.imview_buttons = ButtonsFrame(self.folview,
                ['Add...', 'Remove', 'FS'],
                [self.open_directory, self.remove_directory, self.set_fs])
        self.imview_buttons.grid(row=0, column=1) 
        self.fs_button = self.imview_buttons.buttons[2]
        self.set_fs(fs=self.fs)

        # Operations view
        # -------------------------
        self.opview = tk.LabelFrame(self, text='Command center')
        self.opview.grid(row=0, column=2, sticky='NSWE')
        
        self.tabs = Tabs(self.opview,
                ['Style', 'ROI creation', 'Preprocessing', 'Motion analysis'],
                draw_frame = True)
        self.tabs.grid(row=0, column=1, columnspan=2, sticky='NSWE')
        self.tabs.set_page(1) 
       
        self.styleview = self.tabs.tabs[0]
        self.styleview.columnconfigure(2, weight=1)
        
        self.colormap_label = tk.Label(self.styleview, text='Colormap')
        self.colormap_label.grid(row=1, column=1)
        self.colormap_selection = tk.Button(self.styleview, text=self.colors.get_cmap().name,
                command=self.open_colormap_selection)
        self.colormap_selection.grid(row=1, column=2)
        
        tk.Label(self.styleview, text='Line width').grid(row=2, column=1)
        self.patch_lw_slider = tk.Scale(self.styleview, from_=0, to_=10,
                orient=tk.HORIZONTAL)
        self.patch_lw_slider.set(1)
        self.patch_lw_slider.grid(row=2, column=2, sticky='NSWE')
        
        tk.Label(self.styleview, text='Fill strength').grid(row=3, column=1)
        self.patch_fill_slider = tk.Scale(self.styleview, from_=0, to=100,
                orient=tk.HORIZONTAL)
        self.patch_fill_slider.grid(row=3, column=2, sticky='NSWE')
        self.patch_fill_slider.set(40)



        self.roiview = self.tabs.tabs[1]
        self.roiview.columnconfigure(2, weight=1)

        self.roi_drawtypes = {'box': 'box',
                'ellipse': 'ellipse',
                'line': 'line',
                'polygon': 'polygon',
                'arc_from_points': 'polygon',
                'concentric_arcs_from_points': 'polygon',
                'radial_lines_from_points': 'polygon'}

        tk.Label(self.roiview, text='Selection mode').grid(row=1, column=1)
        self.selmode_frame = tk.Frame(self.roiview)
        self.selmode_frame.grid(row=1, column=2)
        
        self.roitype_selection = DropdownList(self.selmode_frame,
                ['box', 'ellipse', 'line', 'polygon', 'arc_from_points',
                    'concentric_arcs_from_points',
                    'radial_lines_from_points'], 
                ['Box', 'Ellipse', 'Line', 'Polygon', 'Arc from points',
                    'Concentric Arcs (++RG)',
                    'Radial lines (++RG)'],
                single_select=True, callback=self.update_roitype_selection)
        self.roitype_selection.grid(row=1, column=2)

        self.drawmode_selection = TickboxFrame(self.selmode_frame,
                ['add', 'remove'], ['Add', 'Remove'],
                single_select=True
                )
        self.drawmode_selection.grid(row=1, column=1)


        tk.Label(self.roiview, text='Block size').grid(row=3, column=1)
        self.blocksize_slider = tk.Scale(self.roiview, from_=16, to=128,
                orient=tk.HORIZONTAL)
        self.blocksize_slider.set(32)
        self.blocksize_slider.grid(row=3, column=2, sticky='NSWE')

        tk.Label(self.roiview, text='Block distance').grid(row=4, column=1)
        self.overlap_slider = tk.Scale(self.roiview, from_=1, to=128,
                orient=tk.HORIZONTAL, resolution=1)
        self.overlap_slider.set(32)
        self.overlap_slider.grid(row=4, column=2, sticky='NSWE')
        
        self.distance_label = tk.Label(self.roiview, text='Line-block distance')
        self.distance_label.grid(row=5, column=1)
        self.distance_slider = tk.Scale(self.roiview, from_=1, to=128,
                orient=tk.HORIZONTAL, resolution=1)
        self.distance_slider.set(32)
        self.distance_slider.grid(row=5, column=2, sticky='NSWE')

        
        self.nroi_label = tk.Label(self.roiview, text='Count')
        self.nroi_label.grid(row=6, column=1)
        self.nroi_label.grid_remove()
        self.nroi_slider = tk.Scale(self.roiview, from_=1, to=128,
                orient=tk.HORIZONTAL, resolution=1,
                command=self.nroi_slider_callback)
        self.nroi_slider.grid(row=6, column=2, sticky='NSWE')
        self.nroi_slider.grid_remove()

        self.radial_len_label = tk.Label(self.roiview, text='Radial line length')
        self.radial_len_label.grid(row=7, column=1)
        self.radial_len_label.grid_remove()
        self.radial_len_slider = tk.Scale(self.roiview, from_=1, to=1024,
                orient=tk.HORIZONTAL, resolution=1)
        self.radial_len_slider.grid(row=7, column=2, sticky='NSWE')
        self.radial_len_slider.grid_remove()





        self.roi_buttons = ButtonsFrame(self.roiview, ['Update', 'Max grid', 'Clear', 'Undo', 'New group'],
                [self.update_grid, self.fill_grid, self.clear_selections, self.undo, self.new_group])

        self.roi_buttons.grid(row=8, column=1, columnspan=2)
        

        self.preview = self.tabs.tabs[2]
        self.preview.columnconfigure(2, weight=1)
        tk.Label(self.preview, text='Gaussian blur').grid(row=2, column=1)
        self.blur_slider = tk.Scale(self.preview, from_=0, to=32,
                orient=tk.HORIZONTAL)
        self.blur_slider.set(0)
        self.blur_slider.grid(row=2, column=2, sticky='NSWE')


        self.parview = self.tabs.tabs[3]
        self.parview.columnconfigure(2, weight=1)


        # Movemeter True/False options; Automatically inspect from Movemeter.__init__
        moveinsp = inspect.getfullargspec(Movemeter.__init__)

        moveargs = []
        movedefaults = []
        for i in range(1, len(moveinsp.args)):
            arg = moveinsp.args[i]
            default = moveinsp.defaults[i-1]
            if isinstance(default, bool) and arg not in ['multiprocess']:
                moveargs.append(arg)
                movedefaults.append(default)
        
        self.movemeter_tickboxes = TickboxFrame(self.parview, moveargs,
                defaults=movedefaults)
        self.movemeter_tickboxes.grid(row=0, column=1, columnspan=2)


        tk.Label(self.parview, text='Maximum movement').grid(row=1, column=1)
        self.maxmovement_slider = tk.Scale(self.parview, from_=1, to=100,
                orient=tk.HORIZONTAL)
        self.maxmovement_slider.set(10)
        self.maxmovement_slider.grid(row=1, column=2, sticky='NSWE')

        tk.Label(self.parview, text='Upscale').grid(row=2, column=1)
        self.upscale_slider = tk.Scale(self.parview, from_=0.1, to=10,
                orient=tk.HORIZONTAL, resolution=0.1)
        self.upscale_slider.set(5)
        self.upscale_slider.grid(row=2, column=2, sticky='NSWE')


        tk.Label(self.parview, text='CPU cores').grid(row=3, column=1)
        self.cores_slider = tk.Scale(self.parview, from_=1, to=os.cpu_count(),
                orient=tk.HORIZONTAL)
        self.cores_slider.set(max(1, int(os.cpu_count()/2)))
        self.cores_slider.grid(row=3, column=2, sticky='NSWE')


        

        self.calculate_button = tk.Button(self.opview, text='Measure movement',
                command=self.measure_movement)
        self.calculate_button.grid(row=1, column=1)

        self.stop_button = tk.Button(self.opview, text='Stop',
                command=self.stop)
        self.stop_button.grid(row=1, column=2)


        self.export_button = tk.Button(self.opview, text='Export results',
                command=self.export_results)
        self.export_button.grid(row=4, column=1)
        
        self.export_name = tk.Entry(self.opview, width=50)
        self.export_name.insert(0, "enter export name")
        self.export_name.grid(row=4, column=2)
        

        # Images view: Image looking and ROI selection
        # -------------------------------------------------
        self.imview = tk.LabelFrame(self, text='Images and ROI')
        self.imview.grid(row=1, column=1, sticky='NSWE')
        
        self.imview.columnconfigure(1, weight=1)
        self.imview.rowconfigure(3, weight=1)

        self.imview_buttons = ButtonsFrame(self.imview,
                ['Exclude image', 'Exclude index'],
                [self.toggle_exclude, lambda: self.toggle_exclude(by_index=True)])
        
        self.imview_buttons.grid(row=1, column=1)


        self.image_slider = tk.Scale(self.imview, from_=0, to=0,
                orient=tk.HORIZONTAL, command=self.change_image)
        
        self.image_slider.grid(row=2, column=1, sticky='NSWE')

        self.images_plotter = CanvasPlotter(self.imview)
        self.images_plotter.grid(row=3, column=1, sticky='NSWE') 
        
        ax = self.images_plotter.ax
        self.excludetext = ax.text(0.5, 0.5, '', transform=ax.transAxes,
                fontsize=24, ha='center', va='center', color='red')



        # Results view: Analysed traces
        # ------------------------------------
        
        self.tabs = Tabs(self, ['Displacement', 'Heatmap'])
        self.tabs.grid(row=1, column=2, sticky='NSWE')
        self.resview = self.tabs.pages[0]
        self.heatview = self.tabs.pages[1]

        #self.resview = tk.LabelFrame(self, text='Results')
        #self.resview.grid(row=1, column=2)
        
        self.resview.rowconfigure(2, weight=1)
        self.resview.columnconfigure(1, weight=1)
        self.heatview.columnconfigure(2, weight=1)
        self.heatview.rowconfigure(2, weight=1)

        self.results_plotter = CanvasPlotter(self.resview)
        self.results_plotter.grid(row=2, column=1, sticky='NSWE')
        
        self.heatmap_plotter = CanvasPlotter(self.heatview)
        self.heatmap_plotter.grid(row=2, column=2, sticky='NSWE') 
        
        self.heatmap_slider = tk.Scale(self.heatview, from_=0, to=0,
            orient=tk.HORIZONTAL, command=self.change_heatmap)
        self.heatmap_slider.grid(row=0, column=1, sticky='NSWE')
        
        self.heatmapcap_slider = tk.Scale(self.heatview, from_=0.1, to=100,
            orient=tk.HORIZONTAL, resolution=0.1, command=self.change_heatmap)
        self.heatmapcap_slider.set(20)
        self.heatmapcap_slider.grid(row=0, column=2, sticky='NSWE') 
        
        self.heatmap_firstcap_slider = tk.Scale(self.heatview, from_=0.1, to=100,
            orient=tk.HORIZONTAL, resolution=0.1, command=self.change_heatmap)
        self.heatmap_firstcap_slider.set(20)
        self.heatmap_firstcap_slider.grid(row=1, column=2, sticky='NSWE') 
       
        
        self.status = tk.Label(self, text='Nothing to do')
        self.status.grid(row=2, column=1, columnspan=2)

        self.columnconfigure(1, weight=1)    
        self.columnconfigure(2, weight=1)
        self.rowconfigure(1, weight=1)


    def stop():
        self.exit=True
        if self.movemeter:
            self.movemeter.stop()

    def set_fs(self, fs=None):

        if fs is None:
            fs = simpledialog.askfloat('Imaging frequency (Hz)', 'How many images were taken per second')

        if fs:
            self.fs = fs
            self.fs_button.configure(text='fs = {} Hz'.format(self.fs))


    def open_settings(self):
        raise NotImplementedError


    def open_directory(self, directory=None):
        
        if directory is None:
            try: 
                with open(os.path.join(MOVEDIR, 'last_directory.txt'), 'r') as fp:
                    previous_directory = fp.read().rstrip('\n')
            except FileNotFoundError:
                previous_directory = os.getcwd()

            print(previous_directory)

            if os.path.exists(previous_directory):
                directory = filedialog.askdirectory(title='Select directory with the images', initialdir=previous_directory)
            else:
                directory = filedialog.askdirectory(title='Select directory with the images')
            
            
        if directory:
            if not os.path.isdir(MOVEDIR):
                os.makedirs(MOVEDIR)
            with open(os.path.join(MOVEDIR, 'last_directory.txt'), 'w') as fp:
                fp.write(directory)
            
            # Check if folder contains any images; If not, append
            # The folders in this folder
            
            if [fn for fn in os.listdir(directory) if fn.endswith('.tif') or fn.endswith('.tiff')] == []:
                directories = [os.path.join(directory, fn) for fn in os.listdir(directory)]
            else:
                directories = [directory]
            
            for directory in directories:
                self.folders.append(directory)
                self.folders_listbox.set_selections(self.folders)
                self.folder_selected(directory)

    
    def remove_directory(self):
        
        self.folders.remove(self.current_folder)
        self.folders_listbox.set_selections(self.folders)


    def folder_selected(self, folder):
        '''
        When the user selects a folder from the self.folders_listbox
        '''
        
        self.current_folder = folder

        print('Selected folder {}'.format(folder))

        self.image_fns = [os.path.join(folder, fn) for fn in os.listdir(folder) if fn.endswith('.tiff') or fn.endswith('.tif')]
        self.image_fns.sort()

        self.images = [None for fn in self.image_fns]
        self.mask_image = None

        self.change_image(slider_value=1)
        N_images = len(self.image_fns)
        self.image_slider.config(from_=1, to=N_images)
       
        self.export_name.delete(0, tk.END)
        self.export_name.insert(0, os.path.basename(folder.rstrip('/')))


    def toggle_exclude(self, by_index=False):
        '''
        by_index  If true, toggle exclude for all images with this index
        '''

        indx = int(self.image_slider.get()) - 1
        if by_index:
            fn = indx
        else:
            fn = self.image_fns[indx]

        if fn not in self.exclude_images:
            self.exclude_images.append(fn)
            self.set_status('Removed image {} from the analysis'.format(fn))
        else:
            self.exclude_images.remove(fn) 
            self.set_status('Added image {} back to the analysis'.format(fn))
        
        self.mask_image = None
        self.change_image(slider_value=self.image_slider.get())
        print(self.exclude_images)
    
    def toggle_controls(self):
        self.show_controls = not(self.show_controls)
        self.change_image()

    def recalculate_old(self, directory=None):
        '''
        Using the current settings, recalculate old data by opening the
        zip file and reading image filenames and ROI limits from there.
        '''

        if directory == None:
            directory = filedialog.askdirectory()
            if not directory:
                return None
        
        if not self._ask_batchname():
            return None
 
        self.exit = False
        for root, dirs, fns in os.walk(directory):
            
            if self.exit:
                break

            movzip = [fn for fn in os.listdir(root) if fn.startswith('movemeter') and fn.endswith('.zip')]
            
            if movzip:
                settings, filenames, selections, rois, movements = self._load_movzip(os.path.join(root, movzip[0]))
                
                self.folder_selected(os.path.dirname(filenames[0]))
                
                x1, y1 = np.min(rois, axis=0)[0:2]
                x2, y2 = np.max(rois, axis=0)[0:2] + rois[0][3]
                self.set_roi(x1,y1,x2,y2)

                self.measure_movement()

                self.export_results(batch_name=self.batch_name)

        self.set_status('Results recalculated :)')


    def replot_heatmap(self, directory=None):
        '''
        Like recalculate old, but relies in the old movement analysis results
        '''
        if directory == None:
            directory = filedialog.askdirectory()
            if not directory:
                return None
        
        if not self._ask_batchname():
            return None
 
        self.exit = False
        for root, dirs, fns in os.walk(directory):
            
            if self.exit:
                break

            movzip = [fn for fn in os.listdir(root) if fn.startswith('movemeter') and fn.endswith('.zip')]
            if movzip:
                settings, filenames, self.selections, self.roi_groups, self.results = self._load_movzip(os.path.join(root, movzip[0])) 
                
                self.folder_selected(os.path.dirname(filenames[0]))
                self.set_settings(settings)

                self.plot_results()
                self.calculate_heatmap()
                self.change_heatmap(1)

                self.export_results(batch_name=self.batch_name)

        self.set_status('Heatmaps replotted :)')

    
    def _ask_batchname(self):
        name = simpledialog.askstring('Batch name', 'Name new folder')
        if name:
            self.batch_name = name
            return True
        else:
            return False


    def batch_process(self, fill_maxgrid=False):
        '''
        fill_maxgrid : bool
            If True, ignore current ROIs and fill a full frame grid
            using the current slider options.
        '''

        if not self._ask_batchname():
            return None
        
        self.exit = False
        for folder in self.folders:
            if self.exit:
                break
            self.folder_selected(folder)
            
            if fill_maxgrid:
                self.fill_grid()
            
            self.measure_movement()
            self.export_results(batch_name=self.batch_name)


    def measure_movement(self):
        if self.image_fns and self.roi_groups:
            print('Started roi measurements')
           
            self.results = []

            cores = int(self.cores_slider.get())
            if cores == 1:
                cores = False
            
            self.movemeter = Movemeter(upscale=float(self.upscale_slider.get()),
                    multiprocess=cores, print_callback=self.set_status, preblur=self.blur_slider.get(),
                    **self.movemeter_tickboxes.states)
           
            for rois in self.roi_groups:
                # Set movemeted data
                images = [self._included_image_fns()]
                self.movemeter.set_data(images, [rois])
                
                self.results.append( self.movemeter.measure_movement(0, max_movement=int(self.maxmovement_slider.get()), optimized=True) )
            
            self.plot_results()

            self.calculate_heatmap()
            self.change_heatmap(1)

            print('Finished roi measurements')
        else:
            print('No rois')
    
    @property
    def image_shape(self):
        slider_value = int(self.image_slider.get())
        image_i = int(slider_value) -1
        if self.images[image_i] is None:
            self.images[image_i] = tifffile.imread(self.image_fns[image_i])
        return self.images[image_i].shape

    
    def open_colormap_selection(self):
        
        top = tk.Toplevel(self)
        top.title('Select colormap')
        sel = ColormapSelector(top, callback=self.apply_colormap,
                startmap=self.colors.get_cmap().name)
        sel.grid(row=0, column=0, sticky='NSWE')
        top.rowconfigure(0, weight=1)
        top.columnconfigure(0, weight=1)
        top.mainloop()

    def apply_colormap(self, colormap):
        
        if hasattr(colormap, 'colors'):
            self.colors.set_clim(0, len(colormap.colors))
        else:
            self.colors.set_clim(0, 10)

        self.colors.set_cmap(colormap)
        self.colormap_selection.config(text=colormap.name)
        self.update_grid()


    def undo(self):
        '''
        Undo a ROI selection made by the user.
        '''
        if len(self.selections) == 0:
            self.set_status('Nothing to undo')
            return None

        # Index of the roigroup to be undone
        i_roigroup = self.selections[-1][-1]['i_roigroup']

        # Clear the previous selection data
        self.selections = self.selections[:-1]
        
        # Clear the corresponding ROI patches
        N_rois_remove = len(self.roi_patches[-1])
        for patch in self.roi_patches[-1]:
            patch.remove()
        self.roi_patches = self.roi_patches[:-1]
        
        # Clear the actual ROIs
        self.roi_groups[i_roigroup] = self.roi_groups[i_roigroup][:-N_rois_remove]
        
        self.images_plotter.update()
        self.set_status('Undone windows {} in ROI group {}'.format(N_rois_remove, i_roigroup))


    def nroi_slider_callback(self, N=None):
        pass

    def update_roitype_selection(self):
        
        selected = self.roitype_selection.ticked[0] 

        if selected in ['concentric_arcs_from_points', 'radial_lines_from_points']:
            self.nroi_label.grid()
            self.nroi_slider.grid()

            if selected == 'radial_lines_from_points':
                self.radial_len_label.grid()
                self.radial_len_slider.grid()

        else:
            self.nroi_label.grid_remove()
            self.nroi_slider.grid_remove()

            self.radial_len_label.grid_remove()
            self.radial_len_slider.grid_remove()

        self.change_image()

    def clear_selections(self):
        self.selections = []
        self.update_grid()

        self.roi_groups = []
        self.current_roi_group = 0

    def update_grid(self, *args):

        # Updating the image also needed now to update the selector
        # type drawn while selecting (box or line)
        self.change_image()
        
        # Clear any previous patches
        for group in self.roi_patches:
            for patch in group:
                patch.remove()
        self.roi_patches = []

        self.roi_groups = []
        
        if self.selections:
            for selection in self.selections:
                self.set_roi(*selection, user_made=False)
        else:
            self.images_plotter.update()



    def fill_grid(self): 
        self.set_roi(0,0,*reversed(self.image_shape))
   

    def new_group(self):
        self.current_roi_group += 1

    def set_roi(self, x1=None,y1=None,x2=None,y2=None, params=None, user_made=True,
            recursion_data=None):
        

        if params is None:
            params = {}
            params['roitype'] = [s for s, b in self.roitype_selection.states.items() if b][0]
            params['blocksize'] = 2*[self.blocksize_slider.get()]
            params['distance'] = self.distance_slider.get()
            params['relstep'] = float(self.overlap_slider.get())/params['blocksize'][0]
            params['count'] = self.nroi_slider.get()
            params['rlen'] = self.radial_len_slider.get()
            params['i_roigroup'] = int(self.current_roi_group)
            params['mode'] = self.drawmode_selection.ticked[0]

       
        roitype, block_size, distance, rel_step, i_roigroup, count, mode, rlen = [
                params[key] for key in ['roitype','blocksize','distance','relstep', 'i_roigroup', 'count', 'mode', 'rlen']]

        if user_made:
            self.selections.append( (x1, y1, x2, y2, params) )   
        
        if roitype in ['polygon', 'arc_from_points', 'concentric_arcs_from_points',
                'radial_lines_from_points']:
            vertices = x1

            if roitype == 'polygon':
                rois = []
                for i_vertex in range(len(vertices)-1):
                    pA, pB = vertices[i_vertex:i_vertex+2]
                    rois.extend( grid_along_line(pA, pB, distance, block_size, step=rel_step) )
            elif roitype == 'arc_from_points':
                rois = grid_arc_from_points((0,0,*reversed(self.image_shape)), block_size, step=rel_step, points=vertices)
            elif roitype in ['concentric_arcs_from_points', 'radial_lines_from_points']:
                if recursion_data is None:
                    recursion_data = _workout_circle(vertices)
                
                if int(self.current_roi_group) < count-1:
                    self.current_roi_group += 1
                    cp, R = recursion_data
                    
                    if roitype == 'concentric_arcs_from_points':
                        new_recursion_data = (cp, R-distance)
                    elif roitype == 'radial_lines_from_points':
                        new_recursion_data = (cp, R)

                    self.set_roi(x1=x1,y1=y1,x2=x2,y2=y2,
                            params={**params, **{'i_roigroup': self.current_roi_group}},
                            user_made=False,
                            recursion_data=new_recursion_data)
                    self.current_roi_group -= 1 
                
                if roitype == 'concentric_arcs_from_points':
                    rois = grid_arc_from_points((0,0,*reversed(self.image_shape)), block_size, step=rel_step,
                            circle=recursion_data, lw=distance)
                elif roitype == 'radial_lines_from_points':
                    rois = grid_radial_line_from_points((0,0,*reversed(self.image_shape)), block_size, step=rel_step,
                            circle=recursion_data, line_len=rlen,
                            i_segment=self.current_roi_group, n_segments=count)

            else:
                raise ValueError('unkown roitype {}'.format(roitype))

        else:
            w = x2-x1
            h = y2-y1
       
            if roitype == 'line':
                rois = grid_along_line((x1, y1), (x2, y2), distance, block_size, step=rel_step)
            elif roitype == 'ellipse':
                rois = grid_along_ellipse((x1,y1,w,h), block_size, step=rel_step)
            else:
                rois = gen_grid((x1,y1,w,h), block_size, step=rel_step)
            
        while len(self.roi_groups) <= i_roigroup:
            self.roi_groups.append([])

        if mode == 'add':
            self.roi_groups[i_roigroup].extend(rois)
     
            # Draw ROIs

            if len(rois) < 3000:
                self.set_status('Plotting all ROIs...')
            else:
                self.set_status('Too many ROIs, plotting only 3 000 first...')
            
            fig, ax = self.images_plotter.get_figax()
            
            color = self.colors.to_rgba(i_roigroup%self.colors.get_clim()[1])
            
            patches = [] 
            lw = self.patch_lw_slider.get()
            fill = self.patch_fill_slider.get()/100
            fcolor = (color[0], color[1], color[2], color[3]*fill)
            for roi in rois[:3000]:

                patch = matplotlib.patches.Rectangle((float(roi[0]), float(roi[1])),
                        float(roi[2]), float(roi[3]), fill=True, edgecolor=color, facecolor=fcolor,
                        lw=lw)
                patches.append(patch)
                ax.add_patch(patch)
            
            self.roi_patches.append(patches)

        elif mode == 'remove':
            
            def _overlaps(a, b):
                return not (a[0]+a[2] < b[0] or b[0]+b[2] < a[0] or a[1]+a[3] < b[1] or b[1]+b[3] < a[1])

            for i_rgroup in range(len(self.roi_groups)) :
                
                # Remove ROIs
                remove_indices = []
                for i_old, old_roi in enumerate(self.roi_groups[i_rgroup]):    
                    for new_roi in rois:
                        if _overlaps(old_roi, new_roi):
                            remove_indices.append(i_old)
                            break
                
                print('removing {} in rg {}'.format(remove_indices, i_rgroup))

                for i_rm in remove_indices[::-1]:
                    self.roi_groups[i_rgroup].pop()

                    #self.roi_patches[i_rgroup].pop()
                
            # Remove patches separetly
            # Potential optimization if needed: Not sure if this is faster or
            #    slower than the own _overlaps
            # Anyway quite risky if rois and patches become unsynced
            #    (should be made in one-to-one correspondence)
            new_bboxes = [matplotlib.transforms.Bbox([[x, y],[x+w,y+h]]) for x,y,w,h in rois]
            for patches, selections in zip(self.roi_patches, self.selections):
                
                remove_indices = []
                for i_patch, patch in enumerate(patches):
                    if patch.get_bbox().count_overlaps(new_bboxes):
                        patch.remove()
                        remove_indices.append(i_patch)

                for i_rm in remove_indices[::-1]:
                    patches.pop(i_rm)


        else:
            raise ValueError('unkown mode {}'.format(mode))
        self.images_plotter.update()
        self.set_status('ROIs plotted :)')




    def change_image(self, slider_value=None):
        
        slider_value = int(self.image_slider.get())

        image_i = int(slider_value) -1
        print(slider_value)

        if not 0 <= image_i < len(self.image_fns):
            return None
        
        if self.use_mask_image:
            if self.mask_image is None:
                for i in range(len(self.images)):
                    self.images[i] = tifffile.imread(self.image_fns[i])
                
                self.mask_image = np.inf * np.ones(self.image_shape)
                
                for image in self.images:
                    self.mask_image = np.min([self.mask_image, image], axis=0)


        if self.images[image_i] is None:
            self.images[image_i] = tifffile.imread(self.image_fns[image_i])
        
        
        if image_i in self.exclude_images or self.image_fns[image_i] in self.exclude_images:
            self.excludetext.set_text('EXCLUDED')
        else: 
            self.excludetext.set_text('')


        if self.use_mask_image:
            showimage = self.images[image_i] - self.mask_image
        else:
            showimage = self.images[image_i]

        self.images_plotter.imshow(showimage, roi_callback=self.set_roi,
                cmap='gray', slider=self.show_controls,
                roi_drawtype=self.roi_drawtypes[self.roitype_selection.ticked[0]])


    @staticmethod
    def get_displacements(results):
        return [np.sqrt(np.array(x)**2+np.array(y)**2) for x,y in results]


    @staticmethod
    def get_destructive_displacement_mean(results):
        x = [x for x,y in results]
        y = [y for x,y in results]
        return np.sqrt(np.mean(x, axis=0)**2 + np.mean(y, axis=0)**2)


    def plot_results(self):
        self.results_plotter.ax.clear()

        for i_roi_group, result in enumerate(self.results):
            color = self.colors.to_rgba(i_roi_group%self.colors.get_clim()[1])
            displacements = [np.sqrt(np.array(x)**2+np.array(y)**2) for x,y in result]
            
            #for d in displacements[0:50]:
            #    self.results_plotter.plot(d, ax_clear=False, color=color, lw=0.5)
            
            self.results_plotter.plot(self.get_destructive_displacement_mean(result), ax_clear=False, color=color, lw=2)


    def _included_image_fns(self):
        return [fn for i_fn, fn in enumerate(self.image_fns) if fn not in self.exclude_images and i_fn not in self.exclude_images]
    

    def calculate_heatmap(self):
        '''
        Produce minimum size heatmap.
        '''
        self.heatmap_images = []
        
        # FIXME Heatmap for ROI groups not implemented properly
        # Currently just take the first nonempty ROI group
        i_roigroup = [i for i, rois in enumerate(self.roi_groups) if len(rois) != 0]
        if not i_roigroup:
            return None
        else:
            i_roigroup = i_roigroup[0]
        rois = self.roi_groups[i_roigroup]
        results = self.results[i_roigroup]

        roi_w, roi_h = rois[0][2:]

        roi_max_x = np.max([z[0] for z in rois])
        roi_min_x = np.min([z[0] for z in rois])
        roi_max_y = np.max([z[1] for z in rois])
        roi_min_y = np.min([z[1] for z in rois])
        

        step = int(self.overlap_slider.get())
        
        max_movement = float(self.maxmovement_slider.get())
        N = len(self._included_image_fns())

        for i_frame in range(N):
            image = np.zeros( (int((roi_max_y-roi_min_y)/step)+1, int((roi_max_x-roi_min_x)/step)+1) )
            for ROI, (x,y) in zip(rois, results):
                values = (np.sqrt(np.array(x)**2+np.array(y)**2))
                
                value = values[i_frame]
               
                cx = int((ROI[0]-roi_min_x)/step)
                cy = int((ROI[1]-roi_min_y)/step)
                
                try:
                    image[cy, cx] = value
                except:
                    print(image.shape)
                    print('cx {} cy {}'.format(cx, cy))
                    raise ValueError
            if np.max(image) < 0.01:
                image[0,0] = 1
            self.heatmap_images.append(image)

        self.heatmap_slider.config(from_=1, to=len(self.heatmap_images))
        self.heatmap_slider.set(1) 


    def change_heatmap(self, slider_value=None, only_return_image=False):
        #if slider_value == None:
        slider_value = int(self.heatmap_slider.get())

        i_image = int(slider_value) - 1
        image = np.copy(self.heatmap_images[i_image])
        
        # Total max value cap
        allframemax = np.max(self.heatmap_images, axis=0)
        image[allframemax > float(self.heatmapcap_slider.get())] = 0
        
        # First value max cap
        firstframemax = np.max(self.heatmap_images[0:3], axis=0)
        image[firstframemax > float(self.heatmap_firstcap_slider.get())] = 0
        
        #image = image / float(self.heatmapcap_slider.get())
        image = image / np.max(image)
    
        if only_return_image:
            return image
        else:
            self.heatmap_plotter.imshow(image, normalize=False)
   

    def set_settings(self, settings):
        for key, value in settings.items():
            if key == 'block_size':
                self.blocksize_slider.set(value)
            elif key == 'block_distance':
                self.overlap_slider.set(value)
            elif key == 'maximum_movement':
                self.maxmovement_slider.set(value)
            elif key == 'upscale':
                self.upscale_slider.set(value)
            elif key == 'cpu_cores':
                self.cores_slider.set(value)
            elif key == 'exclude_images':
                self.exclude_images = value
            elif key == 'measurement_parameters':
                self.movemeter_tickboxes.states = value


    def set_status(self, text):
        self.status.config(text=text)
        self.status.update_idletasks()
    

    def apply_movzip(self, fn=None, rois=False):
        '''
        Load parts of a movzip and apply settings from it
        to the current session.
        '''
        if fn is None:
            fn = filedialog.askopenfilename(parent=self, title='Select a movzip',
                    initialdir=MOVEDIR)

        settings, filenames, selections, roi_groups, movements = self._load_movzip(fn)
        
        if rois:
            self.selections = selections
            self.rois_groups = roi_groups
            
            self.update_grid()


    def _save_movzip(self, fn=None, only=None):
        '''
        only : bool, string or list of strings
        '''

        if isinstance(only, str):
            only = [only]

        if fn is None:
            if only:
                title = 'Save '+','.join(only)
            else:
                title = 'Save movzip'
            fn = filedialog.asksaveasfilename(parent=self, title=title,
                    initialdir=MOVEDIR)
            
            if not fn.endswith('.zip'):
                fn = fn+'.zip'

        # Dump GUI settings
        settings = {}
        settings['block_size'] = self.blocksize_slider.get()
        settings['block_distance'] = self.overlap_slider.get()
        settings['maximum_movement'] = self.maxmovement_slider.get()
        settings['upscale'] = self.upscale_slider.get()
        settings['cpu_cores'] = self.cores_slider.get() 
        settings['export_time'] = str(datetime.datetime.now())
        settings['movemeter_version'] = __version__
        settings['exclude_images'] = self.exclude_images
        settings['measurement_parameters'] = self.movemeter_tickboxes.states

        if self.images:
            settings['images_shape'] = self.image_shape
        
        
        movzip = {'metadata': settings,
                'image_filenames': self._included_image_fns(),
                'selections': self.selections,
                'rois': self.roi_groups,
                'movements': self.results}

        self.set_status('Saving movzip...')
        
        with zipfile.ZipFile(fn, 'w') as savezip:
            for pfn, obj in movzip.items():

                if only and pfn not in only:
                    continue

                with savezip.open(pfn+'.json', 'w') as fp:
                    fp.write(json.dumps(obj).encode('utf-8'))
        
        self.set_status('Mozip saved.')
        


    def _load_movzip(self, fn):
        '''
        Returns settings, image_filenames, selections, rois, movements
        '''

        movzip = []

        with zipfile.ZipFile(fn, 'r') as loadzip:
            
            for pfn in ['metadata', 'image_filenames', 'selections', 'rois', 'movements']:
                try:
                    with loadzip.open(pfn+'.json', 'r') as fp:
                        movzip.append( json.loads(fp.read()) )
        
                except KeyError:
                    movzip.append(None)

        return (*movzip,)

    
    def save_roiview(self, only_rois=False):
        savefn = filedialog.asksaveasfilename()
        if savefn:
            fig = self.images_plotter.figure
            
            if only_rois:
                self.images_plotter.imshow_obj.set_visible(False)
            
            fig.savefig(savefn, dpi=600, transparent=only_rois)
            
            if only_rois:
                self.images_plotter.imshow_obj.set_visible(True)



    def export_results(self, batch_name=None):

        savename = self.export_name.get()
        zipsavename = savename

        save_root = MOVEDIR
        if batch_name is not None:
            save_root = os.path.join(save_root, 'batch', batch_name)
        
        save_directory = os.path.join(save_root, savename)
        os.makedirs(save_directory, exist_ok=True)
    
        self._save_movzip(os.path.join(save_directory, 'movemeter_{}.zip'.format(zipsavename)))
        
        means = []

        for i_roigroup, results in enumerate(self.results):
            fn = os.path.join(save_directory, 'movements_{}_rg{}.csv'.format(zipsavename, i_roigroup))
            
            displacements = self.get_displacements(results)
            
            if not displacements:
                continue

            dm_displacement = self.get_destructive_displacement_mean(results)

            with open(fn, 'w') as fp:
                writer = csv.writer(fp, delimiter=',')
                
                writer.writerow(['time (s)', 'mean displacement (pixels)', 'destructive mean displacement (pixels)'] + ['ROI{} displacement (pixels)'.format(k) for k in range(len(displacements))])

                for i in range(len(displacements[0])):
                    row = [displacements[j][i] for j in range(len(displacements))]
                    row.insert(0, dm_displacement[i])
                    row.insert(0, np.mean(row))
                    row.insert(0, i/self.fs)
                    writer.writerow(row)

                if i_roigroup == 0:
                    N = len(dm_displacement)
                    means.append(np.linspace(0, (N-1)/self.fs, N))
                means.append(dm_displacement)

        with open(os.path.join(save_directory, 'summary_desctructive_{}.csv'.format(zipsavename)), 'w') as fp:
            writer = csv.writer(fp,  delimiter=',')

            writer.writerow(['time (s)'] +['roi group {} (pixels)'.format(i) for i in range(len(means)-1)])

            for i in range(len(means[0])):
                row = [m[i] for m in means]
                writer.writerow(row)


        slider_i = int(self.image_slider.get())
        self.image_slider.set(int(len(self._included_image_fns()))/2)
        #change_image(slider_value=int(len(self._included_image_fns())/2))

        # Image of the ROIs
        self.set_status('Saving the image view')
        fig, ax = self.images_plotter.get_figax()
        fig.savefig(os.path.join(save_directory, 'movemeter_imageview.jpg'), dpi=400, pil_kwargs={'optimize': True})
        
        self.image_slider.set(slider_i)
        #change_image(slider_value=int(len(self._included_image_fns())/2))
        
        # Image of the result traces
        self.set_status('Saving the results view')
        fig, ax = self.results_plotter.get_figax()
        fig.savefig(os.path.join(save_directory, 'movemeter_resultsview.jpg'), dpi=400, pil_kwargs={'optimize': True})

        # Image of the result traces
        #fig, ax = self.heatmap_plotter.get_figax()
        #fig.savefig(os.path.join(save_directory, 'heatmap_view.jpg'), dpi=600, optimize=True)
        
        
        def save_heatmaps(heatmaps, image_fns, savedir):
            
            for fn, image in zip(image_fns, heatmaps):
                tifffile.imsave(os.path.join(savedir, 'ht_{}'.format(os.path.basename(fn))), image.astype('float32'))
            
            # Save mean heatmap image with scale bar using matplotlib
            # FIXME Expose option for how many last images to save the mean for
            meanimage = np.mean(heatmaps[-min(5, len(heatmaps)):], axis=0)
            
            if False:
                # This was used to clip heatmap values
                # FIXME Expose option in the GUI
                if 'musca' in save_directory:
                    meanimage = np.clip(meanimage, 0, 50)
                    if np.max(meanimage) < 50:
                        meanimage[0,0] = 50
                else:
                    meanimage = np.clip(meanimage, 0, 6)
                    if np.max(meanimage) < 6:
                        meanimage[0,0] = 6

            fig, ax = plt.subplots()
            imshow = ax.imshow(meanimage)
            ax.set_axis_off()

            divider = make_axes_locatable(ax)
            cax = divider.append_axes('right', size='5%', pad=0.05)
            fig.colorbar(imshow, cax=cax)
            
            fig.savefig(os.path.join(savedir, 'ht_mean.png'), dpi=800)
            
            plt.show(block=False)
            plt.pause(0.01)
            plt.close(fig)
        
        self.set_status('Saving heatmaps')
        subsavedir = os.path.join(save_directory, 'heatmap_tif')
        os.makedirs(subsavedir, exist_ok=True)
         
        save_heatmaps(self.heatmap_images, self.image_fns, subsavedir)
        
        self.set_status('DONE Saving :)')
예제 #10
0
    def __init__(self, tk_parent):
        tk.Frame.__init__(self, tk_parent)
        self.parent = tk_parent

        self.current_folder = None
        self.folders = []
        self.image_fns = []
        self.images = None
        self.exclude_images = []

        self.selections = []
        self.mask_image = None
        self.roi_groups = []
        self.current_roi_group = 0
        self.roi_patches = []
        self.results = []
        
        self.heatmap_images = []
        
        self.movemeter = None
        self.fs = 100

        self.show_controls = False
        self.use_mask_image = False

        self.batch_name = 'batch_name'

        self.colors = matplotlib.cm.ScalarMappable(cmap=matplotlib.cm.tab10)
        self.colors.set_clim(0,10)
        

        # Top menu
        # --------------------------------
        self.menu = tk.Menu(self)
        
        filemenu = tk.Menu(self)
        filemenu.add_command(label='Add directory...', command=self.open_directory)
        filemenu.add_separator()
        filemenu.add_command(label='Load ROIs',
                command=lambda: self.apply_movzip(rois=True))
        filemenu.add_command(label='Save ROIs',
                command=lambda: self._save_movzip(only=['rois', 'selections']))
        
        filemenu.add_separator()
        filemenu.add_command(label='Save ROI view',
                command=self.save_roiview)
        
        filemenu.add_command(label='Save ROIs only view',
                command=lambda: self.save_roiview(only_rois=True))
        filemenu.add_separator()
        filemenu.add_command(label='Quit', command=self.parent.destroy) 
        
        self.menu.add_cascade(label='File', menu=filemenu)
        

        editmenu = tk.Menu(self)
        editmenu.add_command(label='Undo (latest ROI)', command=self.undo)
        editmenu.add_separator()
        editmenu.add_command(label='Global settings', command=self.open_settings)
        self.menu.add_cascade(label='Edit', menu=editmenu)

        viewmenu = tk.Menu(self)
        viewmenu.add_command(label='Show image controls', command=self.toggle_controls)
        self.menu.add_cascade(label='View', menu=viewmenu)
        
        batchmenu = tk.Menu(self)
        batchmenu.add_command(label='Batch measure & save all', command=self.batch_process)
        batchmenu.add_separator()
        batchmenu.add_command(label='Reprocess old', command=self.recalculate_old)
        batchmenu.add_command(label='Replot heatmap', command=self.replot_heatmap)
 
        self.menu.add_cascade(label='Batch', menu=batchmenu)

        toolmenu = tk.Menu(self)
        toolmenu.add_command(label='Heatmap tool', command=lambda: open_httool(self))
        self.menu.add_cascade(label='Tools', menu=toolmenu)
        


        self.parent.config(menu=self.menu)

        


        # Input folders

        self.folview = tk.LabelFrame(self, text='Input folders')
        self.folview.rowconfigure(2, weight=1)
        self.folview.columnconfigure(1, weight=1)
        self.folview.grid(row=0, column=1, sticky='NSWE')

        self.folders_listbox = Listbox(self.folview, ['No folders selected'], self.folder_selected)
        self.folders_listbox.listbox.config(height=10)
        self.folders_listbox.grid(row=2, column=1, columnspan=2, sticky='NSWE')

        self.imview_buttons = ButtonsFrame(self.folview,
                ['Add...', 'Remove', 'FS'],
                [self.open_directory, self.remove_directory, self.set_fs])
        self.imview_buttons.grid(row=0, column=1) 
        self.fs_button = self.imview_buttons.buttons[2]
        self.set_fs(fs=self.fs)

        # Operations view
        # -------------------------
        self.opview = tk.LabelFrame(self, text='Command center')
        self.opview.grid(row=0, column=2, sticky='NSWE')
        
        self.tabs = Tabs(self.opview,
                ['Style', 'ROI creation', 'Preprocessing', 'Motion analysis'],
                draw_frame = True)
        self.tabs.grid(row=0, column=1, columnspan=2, sticky='NSWE')
        self.tabs.set_page(1) 
       
        self.styleview = self.tabs.tabs[0]
        self.styleview.columnconfigure(2, weight=1)
        
        self.colormap_label = tk.Label(self.styleview, text='Colormap')
        self.colormap_label.grid(row=1, column=1)
        self.colormap_selection = tk.Button(self.styleview, text=self.colors.get_cmap().name,
                command=self.open_colormap_selection)
        self.colormap_selection.grid(row=1, column=2)
        
        tk.Label(self.styleview, text='Line width').grid(row=2, column=1)
        self.patch_lw_slider = tk.Scale(self.styleview, from_=0, to_=10,
                orient=tk.HORIZONTAL)
        self.patch_lw_slider.set(1)
        self.patch_lw_slider.grid(row=2, column=2, sticky='NSWE')
        
        tk.Label(self.styleview, text='Fill strength').grid(row=3, column=1)
        self.patch_fill_slider = tk.Scale(self.styleview, from_=0, to=100,
                orient=tk.HORIZONTAL)
        self.patch_fill_slider.grid(row=3, column=2, sticky='NSWE')
        self.patch_fill_slider.set(40)



        self.roiview = self.tabs.tabs[1]
        self.roiview.columnconfigure(2, weight=1)

        self.roi_drawtypes = {'box': 'box',
                'ellipse': 'ellipse',
                'line': 'line',
                'polygon': 'polygon',
                'arc_from_points': 'polygon',
                'concentric_arcs_from_points': 'polygon',
                'radial_lines_from_points': 'polygon'}

        tk.Label(self.roiview, text='Selection mode').grid(row=1, column=1)
        self.selmode_frame = tk.Frame(self.roiview)
        self.selmode_frame.grid(row=1, column=2)
        
        self.roitype_selection = DropdownList(self.selmode_frame,
                ['box', 'ellipse', 'line', 'polygon', 'arc_from_points',
                    'concentric_arcs_from_points',
                    'radial_lines_from_points'], 
                ['Box', 'Ellipse', 'Line', 'Polygon', 'Arc from points',
                    'Concentric Arcs (++RG)',
                    'Radial lines (++RG)'],
                single_select=True, callback=self.update_roitype_selection)
        self.roitype_selection.grid(row=1, column=2)

        self.drawmode_selection = TickboxFrame(self.selmode_frame,
                ['add', 'remove'], ['Add', 'Remove'],
                single_select=True
                )
        self.drawmode_selection.grid(row=1, column=1)


        tk.Label(self.roiview, text='Block size').grid(row=3, column=1)
        self.blocksize_slider = tk.Scale(self.roiview, from_=16, to=128,
                orient=tk.HORIZONTAL)
        self.blocksize_slider.set(32)
        self.blocksize_slider.grid(row=3, column=2, sticky='NSWE')

        tk.Label(self.roiview, text='Block distance').grid(row=4, column=1)
        self.overlap_slider = tk.Scale(self.roiview, from_=1, to=128,
                orient=tk.HORIZONTAL, resolution=1)
        self.overlap_slider.set(32)
        self.overlap_slider.grid(row=4, column=2, sticky='NSWE')
        
        self.distance_label = tk.Label(self.roiview, text='Line-block distance')
        self.distance_label.grid(row=5, column=1)
        self.distance_slider = tk.Scale(self.roiview, from_=1, to=128,
                orient=tk.HORIZONTAL, resolution=1)
        self.distance_slider.set(32)
        self.distance_slider.grid(row=5, column=2, sticky='NSWE')

        
        self.nroi_label = tk.Label(self.roiview, text='Count')
        self.nroi_label.grid(row=6, column=1)
        self.nroi_label.grid_remove()
        self.nroi_slider = tk.Scale(self.roiview, from_=1, to=128,
                orient=tk.HORIZONTAL, resolution=1,
                command=self.nroi_slider_callback)
        self.nroi_slider.grid(row=6, column=2, sticky='NSWE')
        self.nroi_slider.grid_remove()

        self.radial_len_label = tk.Label(self.roiview, text='Radial line length')
        self.radial_len_label.grid(row=7, column=1)
        self.radial_len_label.grid_remove()
        self.radial_len_slider = tk.Scale(self.roiview, from_=1, to=1024,
                orient=tk.HORIZONTAL, resolution=1)
        self.radial_len_slider.grid(row=7, column=2, sticky='NSWE')
        self.radial_len_slider.grid_remove()





        self.roi_buttons = ButtonsFrame(self.roiview, ['Update', 'Max grid', 'Clear', 'Undo', 'New group'],
                [self.update_grid, self.fill_grid, self.clear_selections, self.undo, self.new_group])

        self.roi_buttons.grid(row=8, column=1, columnspan=2)
        

        self.preview = self.tabs.tabs[2]
        self.preview.columnconfigure(2, weight=1)
        tk.Label(self.preview, text='Gaussian blur').grid(row=2, column=1)
        self.blur_slider = tk.Scale(self.preview, from_=0, to=32,
                orient=tk.HORIZONTAL)
        self.blur_slider.set(0)
        self.blur_slider.grid(row=2, column=2, sticky='NSWE')


        self.parview = self.tabs.tabs[3]
        self.parview.columnconfigure(2, weight=1)


        # Movemeter True/False options; Automatically inspect from Movemeter.__init__
        moveinsp = inspect.getfullargspec(Movemeter.__init__)

        moveargs = []
        movedefaults = []
        for i in range(1, len(moveinsp.args)):
            arg = moveinsp.args[i]
            default = moveinsp.defaults[i-1]
            if isinstance(default, bool) and arg not in ['multiprocess']:
                moveargs.append(arg)
                movedefaults.append(default)
        
        self.movemeter_tickboxes = TickboxFrame(self.parview, moveargs,
                defaults=movedefaults)
        self.movemeter_tickboxes.grid(row=0, column=1, columnspan=2)


        tk.Label(self.parview, text='Maximum movement').grid(row=1, column=1)
        self.maxmovement_slider = tk.Scale(self.parview, from_=1, to=100,
                orient=tk.HORIZONTAL)
        self.maxmovement_slider.set(10)
        self.maxmovement_slider.grid(row=1, column=2, sticky='NSWE')

        tk.Label(self.parview, text='Upscale').grid(row=2, column=1)
        self.upscale_slider = tk.Scale(self.parview, from_=0.1, to=10,
                orient=tk.HORIZONTAL, resolution=0.1)
        self.upscale_slider.set(5)
        self.upscale_slider.grid(row=2, column=2, sticky='NSWE')


        tk.Label(self.parview, text='CPU cores').grid(row=3, column=1)
        self.cores_slider = tk.Scale(self.parview, from_=1, to=os.cpu_count(),
                orient=tk.HORIZONTAL)
        self.cores_slider.set(max(1, int(os.cpu_count()/2)))
        self.cores_slider.grid(row=3, column=2, sticky='NSWE')


        

        self.calculate_button = tk.Button(self.opview, text='Measure movement',
                command=self.measure_movement)
        self.calculate_button.grid(row=1, column=1)

        self.stop_button = tk.Button(self.opview, text='Stop',
                command=self.stop)
        self.stop_button.grid(row=1, column=2)


        self.export_button = tk.Button(self.opview, text='Export results',
                command=self.export_results)
        self.export_button.grid(row=4, column=1)
        
        self.export_name = tk.Entry(self.opview, width=50)
        self.export_name.insert(0, "enter export name")
        self.export_name.grid(row=4, column=2)
        

        # Images view: Image looking and ROI selection
        # -------------------------------------------------
        self.imview = tk.LabelFrame(self, text='Images and ROI')
        self.imview.grid(row=1, column=1, sticky='NSWE')
        
        self.imview.columnconfigure(1, weight=1)
        self.imview.rowconfigure(3, weight=1)

        self.imview_buttons = ButtonsFrame(self.imview,
                ['Exclude image', 'Exclude index'],
                [self.toggle_exclude, lambda: self.toggle_exclude(by_index=True)])
        
        self.imview_buttons.grid(row=1, column=1)


        self.image_slider = tk.Scale(self.imview, from_=0, to=0,
                orient=tk.HORIZONTAL, command=self.change_image)
        
        self.image_slider.grid(row=2, column=1, sticky='NSWE')

        self.images_plotter = CanvasPlotter(self.imview)
        self.images_plotter.grid(row=3, column=1, sticky='NSWE') 
        
        ax = self.images_plotter.ax
        self.excludetext = ax.text(0.5, 0.5, '', transform=ax.transAxes,
                fontsize=24, ha='center', va='center', color='red')



        # Results view: Analysed traces
        # ------------------------------------
        
        self.tabs = Tabs(self, ['Displacement', 'Heatmap'])
        self.tabs.grid(row=1, column=2, sticky='NSWE')
        self.resview = self.tabs.pages[0]
        self.heatview = self.tabs.pages[1]

        #self.resview = tk.LabelFrame(self, text='Results')
        #self.resview.grid(row=1, column=2)
        
        self.resview.rowconfigure(2, weight=1)
        self.resview.columnconfigure(1, weight=1)
        self.heatview.columnconfigure(2, weight=1)
        self.heatview.rowconfigure(2, weight=1)

        self.results_plotter = CanvasPlotter(self.resview)
        self.results_plotter.grid(row=2, column=1, sticky='NSWE')
        
        self.heatmap_plotter = CanvasPlotter(self.heatview)
        self.heatmap_plotter.grid(row=2, column=2, sticky='NSWE') 
        
        self.heatmap_slider = tk.Scale(self.heatview, from_=0, to=0,
            orient=tk.HORIZONTAL, command=self.change_heatmap)
        self.heatmap_slider.grid(row=0, column=1, sticky='NSWE')
        
        self.heatmapcap_slider = tk.Scale(self.heatview, from_=0.1, to=100,
            orient=tk.HORIZONTAL, resolution=0.1, command=self.change_heatmap)
        self.heatmapcap_slider.set(20)
        self.heatmapcap_slider.grid(row=0, column=2, sticky='NSWE') 
        
        self.heatmap_firstcap_slider = tk.Scale(self.heatview, from_=0.1, to=100,
            orient=tk.HORIZONTAL, resolution=0.1, command=self.change_heatmap)
        self.heatmap_firstcap_slider.set(20)
        self.heatmap_firstcap_slider.grid(row=1, column=2, sticky='NSWE') 
       
        
        self.status = tk.Label(self, text='Nothing to do')
        self.status.grid(row=2, column=1, columnspan=2)

        self.columnconfigure(1, weight=1)    
        self.columnconfigure(2, weight=1)
        self.rowconfigure(1, weight=1)