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 __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 __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
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()
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()
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()))
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)
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)
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 :)')
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)