Source code for VR_drawing_board

__author__ = 'David Tadres'
__project__ = 'PiVR'

import os
# general imports
import tkinter as tk
from tkinter import messagebox

import matplotlib.backends.backend_tkagg as tkagg
import numpy as np
from matplotlib.figure import Figure
from matplotlib.patches import Circle
from scipy import signal
from pathlib import Path

# this try-except statement checks if the processor is a ARM processor
# (used by the Raspberry Pi) or not.
# Since this command only works in Linux it is caught using
# try-except otherwise it's throw an error in a Windows system.
try:
    if os.uname()[4][:3] == 'arm':
        # This will yield True for both a Raspberry and for M1 Chip
        # Apple devices.
        # Use this code snippet
        # (from https://raspberrypi.stackexchange.com/questions/5100/detect-that-a-python-program-is-running-on-the-pi)
        import re
        CPUINFO_PATH = Path("/proc/cpuinfo")
        if CPUINFO_PATH.exists():
            with open(CPUINFO_PATH) as f:
                cpuinfo = f.read()
            if re.search(r"^Model\s*:\s*Raspberry Pi", cpuinfo, flags=re.M) is not None:
                # if True, is Raspberry Pi
                RASPBERRY = True
                LINUX = True
        else: # Test if one more intendation necessary or not. On Varun's computer
            # is Apple M1 chip (or other Arm CPU device).
            RASPBERRY = False
            LINUX = True
    else:
        # is either Mac or Linux
        RASPBERRY = False
        LINUX = True

    DIRECTORY_INDICATOR = '/'
except AttributeError:
    # is Windows
    RASPBERRY = False
    LINUX = False
    DIRECTORY_INDICATOR = '\\'

# Todo: Find a way to make the inverted functionality more intuitive!
#  It's a bit hard to always do the inversion in ones head. One
#  possibility is to immediately change the buttons and labels when
#  the user click on 'invert' to be very clear what happens - or
#  change the text in the buttons itself. Ideas welcome!

class SelectResolution():
    """
    In v1.5.0 the option to perform online tracking at different
    resolutions was introduced.
    To enable virtual realities at these different resolutions one
    needs to be able to produce a desired virtual reality. This class
    enables the user to select a given resolution to then draw a
    virtual reality.
    """

    def __init__(self, path_of_program):
        self.path_of_program = path_of_program

        # create a new window and name it
        self.child = tk.Toplevel()
        self.child.wm_title('Select resolution')
        self.child.attributes("-topmost", True)  # force on top
        # disable main window
        self.child.grab_set()

        instruction_label = tk.Label(self.child,
                                     text='Select desired resolution '
                                          'for your virtual arena.')
        instruction_label.grid(row=0, column=0, sticky='W',
                               columnspan=2)

        # resolution variable
        self.resolution = tk.StringVar()
        self.resolution.set('640x480')

        # each radiobutton
        res_640x480_button = tk.Radiobutton(
            self.child,
            text='640x480',
            variable=self.resolution ,
            value='640x480')
        res_640x480_button.grid(row=1, column=0, sticky='W')

        res_1024x768_button = tk.Radiobutton(
            self.child,
            text='1024x768',
            variable=self.resolution ,
            value='1024x768')
        res_1024x768_button.grid(row=2, column=0, sticky='W')

        res_1296x972_button = tk.Radiobutton(
            self.child,
            text='1296x972',
            variable=self.resolution ,
            value='1296x972')
        res_1296x972_button.grid(row=3, column=0, sticky='W')

        res_1920x1080_button = tk.Radiobutton(
            self.child,
            text='1920x1080',
            variable=self.resolution ,
            value='1920x1080')
        res_1920x1080_button.grid(row=4, column=0, sticky='W')

        cancel_button = tk.Button(self.child,
                                  text='Cancel',
                                  command=self.cancel_res_selection_func)
        cancel_button.grid(row=5, column=1)

        continue_button = tk.Button(self.child,
                                    text='Draw VR arena',
                                    command=self.continue_with_draw_func)
        continue_button.grid(row=5, column=0)

    def continue_with_draw_func(self):
        self.child.destroy()
        # Had the keep the variable as string to keep two values.
        # Therefore I have to unpack and cast as int.
        resolution = [int(self.resolution.get().split('x')[0]),
                      int(self.resolution.get().split('x')[1])]
        VRArena(resolution=resolution,
                path_of_program=self.path_of_program)

    def cancel_res_selection_func(self):
        self.child.destroy()


[docs] class VRArena(): """ Users need to be able to "draw" virtual reality arenas as many users will not be able to just pull up matlab or python to draw a virtual arena as a 2D matrix and save it as csv. This class is intended to let users draw virtual realities painlessly.It opens a blank image with the x/y size of the camera resolution. It lets the user 'draw' a VR arena with the mouse. In general it has two options: One can draw gaussian circles while being able to define sigma. The user also has the option to draw step functions without a gradient at the edge. In both of those geometric objects the user can define the intensity. It is also possible to define a general minimal stimulation. See the subclasses for detailed information It then saves the arena (probably in a subfolder) to be used again. It should also be saved with any experiment that is conducted with it """ def __init__(self, resolution=None, path_of_program=None): self.resolution = resolution self.path_of_program = path_of_program # This variable is the reference to the placed animal self.arrow = None # some more variable we'll need for future use self.animal_draw_selected = False self.animal_x_coordinate = 'NA' self.animal_y_coordinate = 'NA' self.animal_theta = 'NA' self.max_intensity_value = 100 # create a new window and name it self.child = tk.Toplevel() self.child.wm_title('Arena configuration for resolution ' + repr(self.resolution)) self.child.attributes("-topmost", True) # force on top # disable main window self.child.grab_set() ''' FAR LEFT Since there are several options for the user the menu is subdivided into subframes. The first subframe contains all the buttons that are relevant for defining the gaussian circle and drawing it with the mouse pointer. This is the first subframe - it contains all the buttons and text windows that are exclusive to creating the gaussian gradient ''' self.gaussian_subframe = tk.LabelFrame(self.child) self.gaussian_subframe.grid(row=1, column=0, sticky='n') # the button that the user can click to explicitly draw a # gaussian circle. self.draw_gaussian_button = tk.Button( self.gaussian_subframe, text='Draw Gaussian Circle \nwith Mouse', command=self.draw_gaussian_func) self.draw_gaussian_button.grid(row=2, column=0) self.sigma_label = tk.LabelFrame(self.gaussian_subframe, text='Sigma [Number only]') self.sigma_label.grid(row=3, column=0, sticky='W') self.gauss_sigma_var = tk.DoubleVar() self.gauss_sigma_var.set(50) self.sigma_text_box = tk.Entry( self.sigma_label, width=5, textvariable=self.gauss_sigma_var) self.sigma_text_box.grid(row=0, column=0) self.sigma = self.gauss_sigma_var.get() self.size_label = tk.LabelFrame(self.gaussian_subframe, text='Size [Integer only]') self.size_label.grid(row=5, column=0, sticky='W') self.gauss_size_var = tk.IntVar() self.gauss_size_var.set(2000) self.size_text_box = tk.Entry(self.size_label, width=5, textvariable=self.gauss_size_var) self.size_text_box.grid(row=0, column=0) self.size = abs(int(self.gauss_size_var.get())) self.maximum_intensity_gaussian_label = tk.LabelFrame( self.gaussian_subframe, text='Maximum Intensity [%]') self.maximum_intensity_gaussian_label.grid(row=7, column=0, sticky='W') self.gauss_max_intensity_var = tk.DoubleVar() self.gauss_max_intensity_var.set(100) self.maximum_intensity_gaussian_text_box = tk.Entry( self.maximum_intensity_gaussian_label, width=5, textvariable=self.gauss_max_intensity_var) self.maximum_intensity_gaussian_text_box.grid(row=0, column=0) self.maximum_intensity_gaussian = \ self.gauss_max_intensity_var.get() self.precise_gaussian_position_label = tk.LabelFrame( self.gaussian_subframe, text='Coordinates [Integer]') self.precise_gaussian_position_label.grid(row=9, column=0, sticky='W') self.precise_gaussian_x_position_label = tk.Label( self.precise_gaussian_position_label, text='x') self.precise_gaussian_x_position_label.grid(row=0, column=0) self.precise_gaussian_y_position_label = tk.Label( self.precise_gaussian_position_label, text='y') self.precise_gaussian_y_position_label.grid(row=0, column=1) self.gauss_precise_x_position_var = tk.IntVar() self.gauss_precise_x_position_var.set(0) self.precise_gaussian_x_position_text = tk.Entry( self.precise_gaussian_position_label, width=5, textvariable=self.gauss_precise_x_position_var) self.precise_gaussian_x_position_text.grid(row=1, column=0) self.precise_gaussian_x_position = \ self.gauss_precise_x_position_var.get() self.gauss_precise_y_position_var = tk.IntVar() self.gauss_precise_y_position_var.set(0) self.precise_gaussian_y_position_text = tk.Entry( self.precise_gaussian_position_label, width=5, textvariable=self.gauss_precise_y_position_var) self.precise_gaussian_y_position_text.grid(row=1, column=1) self.precise_gaussian_y_position = \ self.gauss_precise_y_position_var.get() self.precise_gaussian_draw_button = tk.Button( self.precise_gaussian_position_label, text='Draw Gaussian Circle\nat defined coordinates', command=self.precise_gaussian_draw_func) self.precise_gaussian_draw_button.grid(row=2, column=0, columnspan=2) ''' LEFT Here the second subframe is defined - contains all fields that are necessary to define rectangular objects. ''' self.rectangle_subframe = tk.LabelFrame(self.child) self.rectangle_subframe.grid(row=1, column=1, sticky='N') self.draw_linear_button = tk.Button( self.rectangle_subframe, text='Draw Rectangle \nwith Mouse', command=self.draw_linear_func) self.draw_linear_button.grid(row=3, column=0) self.size_label = tk.LabelFrame( self.rectangle_subframe, text='Size of Rectangle\n[Integer only]') self.size_label.grid(row=4, column=0, sticky='W') self.size_x_label = tk.Label(self.size_label, text='x') self.size_x_label.grid(row=0, column=0) self.box_size_x_var = tk.IntVar() self.box_size_x_var.set(50) self.size_x_text_box = tk.Entry( self.size_label, width=5, textvariable=self.box_size_x_var) self.size_x_text_box.grid(row=1, column=0) self.size_x = self.box_size_x_var.get() self.size_y_label = tk.Label(self.size_label, text='y') self.size_y_label.grid(row=0, column=1) self.box_size_y_var = tk.IntVar() self.box_size_y_var.set(50) self.size_y_text_box = tk.Entry( self.size_label, width=5, textvariable=self.box_size_y_var) self.size_y_text_box.grid(row=1, column=1) self.size_y = self.size_y_text_box.get() self.rectangle_intensity_label = tk.LabelFrame( self.rectangle_subframe, text='Intensity [%]') self.rectangle_intensity_label.grid(row=5, column=0) self.rect_intensity_var = tk.IntVar() self.rect_intensity_var.set(100) self.rectangle_intensity_text_box = tk.Entry( self.rectangle_intensity_label, width=5, textvariable=self.rect_intensity_var) self.rectangle_intensity_text_box.grid(row=0, column=0) self.rectangle_intensity = self.rect_intensity_var.get() self.precise_rectangle_position_label = tk.LabelFrame( self.rectangle_subframe, text='Coordinates [Integer]') self.precise_rectangle_position_label.grid(row=6, column=0, sticky='W') self.precise_rectangle_x_position_label = tk.Label( self.precise_rectangle_position_label, text='x') self.precise_rectangle_x_position_label.grid(row=0, column=0) self.precise_rectangle_y_position_label = tk.Label( self.precise_rectangle_position_label, text='y') self.precise_rectangle_y_position_label.grid(row=0, column=1) self.rect_precise_x_pos_var = tk.IntVar() self.rect_precise_x_pos_var.set(0) self.precise_rectangle_x_position_text = tk.Entry( self.precise_rectangle_position_label, width=5, textvariable=self.rect_precise_x_pos_var) self.precise_rectangle_x_position_text.grid(row=1, column=0) self.precise_rectangle_x_position = \ self.rect_precise_x_pos_var.get() self.rect_precise_y_pos_var = tk.IntVar() self.rect_precise_y_pos_var.set(0) self.precise_rectangle_y_position_text = tk.Entry( self.precise_rectangle_position_label, width=5, textvariable=self.rect_precise_y_pos_var) self.precise_rectangle_y_position_text.grid(row=1, column=1) self.precise_rectangle_y_position = \ self.rect_precise_y_pos_var.get() self.precise_rectangle_draw_button = tk.Button( self.precise_rectangle_position_label, text='Draw Rectangle\nat defined coordinates', command=self.precise_rectangle_draw_func) self.precise_rectangle_draw_button.grid(row=2, column=0, columnspan=2) self.general_drawing_frame = tk.LabelFrame(self.child, text='General') self.general_drawing_frame.grid(row=2, column=0, columnspan=2, sticky='N') ''' LEFT BOTTOM Now there are several buttons that are involved in drawing but are general such as the overall minimum... ''' self.minimum_intensity_label = tk.Label( self.general_drawing_frame, text='Overall minimum \nintensity [0-' + repr(self.max_intensity_value) + ']') self.minimum_intensity_label.grid(row=0, column=0) self.min_intensity_input_var = tk.IntVar() self.min_intensity_input_var.set(0) self.minimum_intensity_input = tk.Entry( self.general_drawing_frame, width=5, textvariable=self.min_intensity_input_var) self.minimum_intensity_input.grid(row=1, column=0) self.minimum_intensity = self.min_intensity_input_var.get() # ...whether the next geometric object the user draws should be # inverted... self.invert_button = tk.Button(self.general_drawing_frame, text='Invert Off', command=self.invert_func, bg='light grey', foreground='black') self.invert_button.grid(row=2, column=0) self.invert = False # ...and whether the previous arena should be overwritten or # not. self.overwrite_button = tk.Button( self.general_drawing_frame, text='Overwrite Off', command=self.overwrite_func, bg='light grey') self.overwrite_button.grid(row=3, column=0) self.overwrite = False self.addition_of_intensity_button = tk.Button( self.general_drawing_frame, text='Additive Off', command=self.addition_of_intensity_func, bg='light grey') self.addition_of_intensity_button.grid(row=4, column=0) self.addition_of_intensity = False self.dont_draw_button = tk.Button( self.general_drawing_frame, text='Do not draw', command=self.dont_draw_func) self.dont_draw_button.grid(row=5, column=0) ''' RIGHT SIDE The animal placement frame: It's on the right of the arena as it has nothing to do with the arena itself. It's all in the frame 'place_animal_frame) ''' self.place_animal_frame = tk.LabelFrame(self.child) self.place_animal_frame.grid(row=1, column=6, rowspan=2) self.place_animal_button = tk.Button( self.place_animal_frame, text='Press to place\nanimal with mouse', command=self.place_animal_func) self.place_animal_button.grid(row=0, column=0) self.stop_animal_drawing_button = tk.Button( self.place_animal_frame, text='Press to stop\nplacing animal', command=self.stop_animal_drawing_func) self.stop_animal_drawing_button.grid(row=1, column=0) self.place_animal_precise_frame = tk.LabelFrame( self.place_animal_frame, text='Precise Location') self.place_animal_precise_frame.grid(row=2, column=0) self.place_animal_precise_now_label = tk.Label( self.place_animal_precise_frame, text='Where is the animal\nwhen the experiment starts?') self.place_animal_precise_now_label.grid(row=0, column=0, columnspan=2) self.place_animal_precise_x_label = tk.Label( self.place_animal_precise_frame, text='x') self.place_animal_precise_x_label.grid(row=1, column=0) self.place_animal_precise_x_text = tk.Text( self.place_animal_precise_frame, height=1, width=5) self.place_animal_precise_x_text.grid(row=2, column=0) self.place_animal_precise_x_text.insert(tk.END, '') self.place_animal_precise_x = 'NA' self.place_animal_precise_y_label = tk.Label( self.place_animal_precise_frame, text='y') self.place_animal_precise_y_label.grid(row=1, column=1) self.place_animal_precise_y_text = tk.Text( self.place_animal_precise_frame, height=1, width=5) self.place_animal_precise_y_text.grid(row=2, column=1) self.place_animal_precise_y_text.insert(tk.END, '') self.place_animal_precise_y = 'NA' self.place_animal_precise_theta_label = tk.Label( self.place_animal_precise_frame, text='Angle the animal\nis coming from [\u03B8]\nMust be ' '-\u03C0 to +\u03C0: ') self.place_animal_precise_theta_label.grid(row=3, column=0, columnspan=2) self.place_animal_precise_theta_text = tk.Text( self.place_animal_precise_frame, height=1, width=5) self.place_animal_precise_theta_text.grid(row=4, column=0, columnspan=2) self.place_animal_precise_theta_text.insert(tk.END, '') self.place_animal_precise_theta = 'NA' self.place_animal_precise_button = tk.Button( self.place_animal_precise_frame, text='Put animal at\nprecise location', command=self.place_animal_precise_func) self.place_animal_precise_button.grid(row=5, column=0, columnspan=2) self.location_animal_frame = tk.LabelFrame( self.place_animal_frame, text='Location of the animal') self.location_animal_frame.grid(row=3, column=0) self.location_animal_label = tk.Label( self.location_animal_frame, text='Start Location of the animal') self.location_animal_label.grid(row=0, column=0) self.location_animal = tk.Label(self.location_animal_frame, text='x: NA, y: NA') self.location_animal.grid(row=1, column=0) self.angle_of_orientation_label = tk.Label( self.location_animal_frame, text='Angle the animal\nis coming from') self.angle_of_orientation_label.grid(row=2, column=0) self.angle_of_orientation = tk.Label( self.location_animal_frame, text='\u03B8 : ') self.angle_of_orientation.grid(row=3, column=0) self.delete_animal_button = tk.Button( self.location_animal_frame, text='Press to delete animal', command=self.delete_animal_func) self.delete_animal_button.grid(row=4, column=0) ''' TOP CENTER This goes on top of the plot - Essentially asking the user with which name the Arena should be saved (which can be selected below the screen) ''' self.name_text_box_label = tk.Label(self.child, text='Name of the Arena') self.name_text_box_label.grid(row=0, column=2) self.name_var = tk.StringVar() self.name_var.set('my_arena') self.name_text_box = tk.Entry(self.child, width=30, textvariable=self.name_var) self.name_text_box.grid(row=0, column=3) self.name = self.name_var.get() self.save_arena_button = tk.Button(self.child, text='Save Arena', command=self.save_arena) self.save_arena_button.grid(row=0, column=4) # The user also will have the option to select an already # existing arena and modify it before saving it again self.modify_existing_button = tk.Button( self.child, text='Select arena to modify', command=self.modify_exsiting_func) self.modify_existing_button.grid(row=0, column=5) ''' BOTTOM CENTER turn gridlines on an off ''' self.gridlines_button = tk.Button( self.child, text='Turn Gridlines On', command=self.gridlines_func) self.gridlines_button.grid(row=11, column=3) # create the blank image self.arena = np.zeros((resolution[1], resolution[0]), dtype=np.float64) ''' CENTER call the plotting library and plot the empty arena into a figure ''' self.fig = Figure() self.ax = self.fig.add_subplot(111) self.image_of_arena = self.ax.imshow( self.arena, vmin=0, vmax=self.max_intensity_value) # Also plot the colorbar self.fig.colorbar(self.image_of_arena) # make a frame to which will be placed using .grid() because # the NavigationToolbar only accepts .pack() self.canvas_frame = tk.Frame(self.child) self.canvas_frame.grid(row=1, column=2, rowspan=10, columnspan=4) # bind the plot to the GUI self.canvas = tkagg.FigureCanvasTkAgg(self.fig, master=self.canvas_frame) self.canvas.draw() # Add the toolbar toolbar = tkagg.NavigationToolbar2Tk(self.canvas, self.canvas_frame) toolbar.update() self.canvas.get_tk_widget().pack() ''' BOTTOM RIGHT The quit button ''' self.quit_button = tk.Button(self.child, text='Quit', command=self.quit_func, bg='red') self.quit_button.grid(row=11, column=6) ''' Before the user can do anything, they need to explicitly call one of the drawing options ''' self.dont_draw_func() self.stop_animal_drawing_func() self.update_values()
[docs] def update_values(self): """ This funcion runs as a loop in the background after the VR drawing board has been constructed. It listens to changes in the Entry fields by the user and calls appropriate functions. """ # repeat this function every 700ms self.child.after(700, self.update_values) # check if the user changed the input of the size try: if self.size != abs(int(self.gauss_size_var.get())): # This only takes integers as it will be used for # indexing an array which can only be done by integers # it will therefore not take floats. self.size = abs(int(self.gauss_size_var.get())) # after changing the size the drawing class has to be # informed. Here we call another instance of the # class self.draw_gaussian_func() except ValueError: # this happens when the user puts a string, nothing or a # invalid number (e.g. a float) pass # check if user has changed the input of the sigma of the # gaussian plot try: if self.sigma != self.gauss_sigma_var.get(): self.sigma = self.gauss_sigma_var.get() self.draw_gaussian_func() except ValueError: # this happens when the user puts a string or nothing pass # check if user has changed the input on maximum intensity of # the gaussian try: if self.maximum_intensity_gaussian != \ self.gauss_max_intensity_var.get() \ and 0 < self.gauss_max_intensity_var.get() < 100: self.maximum_intensity_gaussian = \ self.gauss_max_intensity_var.get() self.draw_gaussian_func() except ValueError: pass # check if user changed input of the rectangle in x try: if self.size_x != self.box_size_x_var.get(): self.size_x = self.box_size_x_var.get() self.draw_linear_func() except ValueError: pass # check if user changed input of rectangle in y try: if self.size_y != self.box_size_y_var.get(): self.size_y = self.box_size_y_var.get() self.draw_linear_func() except ValueError: pass # check if user changed input of rectangle intensity try: if self.rectangle_intensity != self.rect_intensity_var.get() \ and 0 < self.rect_intensity_var.get() <= 100: self.rectangle_intensity = self.rect_intensity_var.get() self.draw_linear_func() except ValueError: pass # check if user changed the minimum light intensity of the arena try: if self.minimum_intensity != \ self.min_intensity_input_var.get() \ and self.min_intensity_input_var.get() >= 0: self.minimum_intensity = \ self.min_intensity_input_var.get() # after the user changed the input this is # immediatelly reflected in the arena self.arena[np.where(self.arena < self.minimum_intensity)] \ = self.minimum_intensity # and here this updated arena is being plotted self.image_of_arena.set_data(self.arena) # and then it is necessary to explicitly tell # matplotlib to redraw the image - I'm not sure # why this is necessary here but not in the class below self.fig.canvas.draw() except ValueError: pass if self.name != self.name_var.get(): self.name = self.name_var.get() # check if user entered a new gaussian x value try: if 0 < self.gauss_precise_x_position_var.get() \ < self.resolution[0] \ and self.precise_gaussian_x_position != \ self.gauss_precise_x_position_var.get(): self.precise_gaussian_x_position = \ self.gauss_precise_x_position_var.get() except ValueError: pass # check if user entered a new gaussian y value try: if 0 < self.gauss_precise_y_position_var.get()\ < self.resolution[0] \ and self.gauss_precise_y_position_var.get() != \ self.precise_gaussian_y_position: self.precise_gaussian_y_position=\ self.gauss_precise_y_position_var.get() except ValueError: pass # check if user entered a new rectangle x value try: if self.rect_precise_x_pos_var.get() != \ self.precise_rectangle_x_position: self.precise_rectangle_x_position =\ self.rect_precise_x_pos_var.get() except ValueError: pass # check if user entered a new rectangle y value try: if self.rect_precise_y_pos_var.get() != \ self.precise_rectangle_y_position: self.precise_rectangle_y_position = \ self.rect_precise_y_pos_var.get() except ValueError: pass
[docs] def save_arena(self): """ This function is called when the user clicks the 'save' button. The function should work both on Linux based systems and Windows. It checks whether the file already exists and asks the user if it should be overwritten if it exits. Otherwise it just saves, without any confirmation etc. The arenas are always saved with the same resolution as these are not interchangable within a trial. """ if self.animal_x_coordinate is not 'NA': if self.animal_theta is not 'NA': save_animal_placement_position = \ '_animal_pos[' + repr(self.animal_x_coordinate) \ + ',' + repr(self.animal_y_coordinate) + ',' \ + repr(self.animal_theta)[0:5] + ']' else: save_animal_placement_position = \ '_animal_pos[' + repr(self.animal_x_coordinate) \ + ',' + repr(self.animal_y_coordinate) + ']' else: save_animal_placement_position = '' print(self.path_of_program) os.makedirs(self.path_of_program + '/VR_arenas', exist_ok=True) # make the directory if os.path.exists(self.path_of_program + 'VR_arenas/' + repr(self.resolution[0]) + 'x' + repr(self.resolution[1]) + '_' + self.name + save_animal_placement_position + '.csv'): result = messagebox.askokcancel( title="File already exists", message="File already exists. Overwrite?", parent=self.child) if result: np.savetxt(self.path_of_program + 'VR_arenas/' + repr(self.resolution[0]) + 'x' + repr(self.resolution[1]) + '_' + self.name + save_animal_placement_position + '.csv', self.arena, delimiter=',', fmt='%d') else: np.savetxt(self.path_of_program + 'VR_arenas/' + repr(self.resolution[0]) + 'x' + repr(self.resolution[1]) + '_' + self.name + save_animal_placement_position + '.csv', self.arena, delimiter=',', fmt='%d')
[docs] def modify_exsiting_func(self): """ This function first opens a filedialog with the directory that this module saves the arenas normally. It then reads the file and directly draws in on the canvas. It also updates the name with the name of the file selected. """ name_of_file = tk.filedialog.askopenfilename( initialdir=self.path_of_program, title="Select file", filetypes=(("csv files", "*.csv"), ("all files", "*.*")), parent=self.child) # load the arena into a numpy array self.arena = np.genfromtxt(name_of_file, delimiter=',') # check if an animal has been drawn before animal_defined = name_of_file.split('animal_pos')[-1] try: # add the first coordinate to the lisht self.placed_animal = [int(animal_defined.split( ',')[0].split('[')[1])] if ']' in animal_defined.split(',')[1]: print('no orientation') # Add the second coordinate to the list self.placed_animal.append( int(animal_defined.split(',')[1].split(']')[0])) try: self.arrow.arrow_annotation.set_visible(False) except AttributeError: pass self.place_animal_button['bg'] = 'light grey' self.arrow = None self.arrow = PlaceAnimal( ax=self.ax, plot_of_arena=self.image_of_arena, master=self, start_x=self.placed_animal[0], start_y=self.placed_animal[1], theta='NA', precise=True) self.animal_draw_selected = True # add the theta to the list elif ']' in animal_defined.split(',')[2]: self.placed_animal.append(int(animal_defined.split(',')[1])) self.placed_animal.append( float(animal_defined.split(',')[2].split(']')[0])) print('animal with orientation') try: self.arrow.arrow_annotation.set_visible(False) except AttributeError: pass self.place_animal_button['bg'] = 'light grey' self.arrow = None self.arrow = PlaceAnimal( ax=self.ax, plot_of_arena=self.image_of_arena, master=self, start_x=self.placed_animal[0], start_y=self.placed_animal[1], theta=self.placed_animal[2], precise=True) self.animal_draw_selected = True except IndexError: print('no animal placed') # finally, set the data self.image_of_arena.set_data(self.arena) # and then it is necessary to explicitly tell matplotlib to # redraw the image - I'm not sure # why this is necessary here but not in the class below self.fig.canvas.draw() # todo think about a smart way to find this again! # also update the name if self.resolution == [640, 480]: if LINUX: name = name_of_file.split('/')[-1][8:-4] # todo - check if that's correct else: name = name_of_file.split('/')[-1][8:-4] elif self.resolution == [1024, 768]: if LINUX: name = name_of_file.split('/')[-1][9:-4] # todo - check if that's correct else: name = name_of_file.split('/')[-1][9:-4] self.name_var.set(name)
[docs] def draw_gaussian_func(self): """ This function call the GaussianSlope class - just here to save lines in the program. Also changes the last_drawing_call variable to make the user experience more intuitive. """ self.canvas = GaussianSlope( ax=self.ax, arena=self.arena, plot_of_arena=self.image_of_arena, size=self.size, sigma=self.sigma, max_intensity=self.maximum_intensity_gaussian, overwrite=self.overwrite, invert=self.invert, addition_of_intensity=self.addition_of_intensity, mouse_drawing=True ) self.last_drawing_call = 'gauss' self.draw_linear_button['bg'] = 'light grey' self.draw_gaussian_button['bg'] = 'red' self.dont_draw_button['bg'] = 'light grey'
[docs] def draw_linear_func(self): """ This function calls the Step class - just to save some lines in the code. Also changes the last_drawing_call variable to make the user experience more intuitive. """ self.canvas = Step( ax=self.ax, arena=self.arena, plot_of_arena=self.image_of_arena, size_x=self.size_x, size_y=self.size_y, intensity=self.rectangle_intensity, overwrite=self.overwrite, invert=self.invert, addition_of_intensity=self.addition_of_intensity, mouse_drawing=True ) self.last_drawing_call = 'step' self.draw_linear_button['bg'] = 'red' self.draw_gaussian_button['bg'] = 'light grey' self.dont_draw_button['bg'] = 'light grey'
[docs] def update_drawing(self): """ If it is not clear on which geometric form the user is working, this function is called (e.g. when changing the 'overwrite' button or the 'invert' button """ if self.last_drawing_call == 'gauss': self.draw_gaussian_func() elif self.last_drawing_call == 'step': self.draw_linear_func() if self.last_drawing_call == 'none': self.dont_draw_func()
[docs] def overwrite_func(self): """ This function is bound to the overwrite_button. It changes the text on the overwrite button, it also changes the boolean variable 'overwrite' to True or False and it updates the drawing class call """ if self.overwrite_button['text'] == 'Overwrite Off': self.overwrite_button['text'] = 'Overwrite On' self.overwrite_button['bg'] = 'red' self.overwrite = True self.update_drawing() elif self.overwrite_button['text'] == 'Overwrite On': self.overwrite_button['text'] = 'Overwrite Off' self.overwrite_button['bg'] = 'light grey' self.overwrite_button['bg'] = 'light grey' self.overwrite = False self.update_drawing()
[docs] def invert_func(self): """ This function is bound to the invert_button. It changes the text on the invert_button, it also changes the color of both the background and the text of the button for visual help it also changes the boolean invert variable to True or False and it also updates the drawing class call. """ if self.invert_button['text'] == 'Invert Off': self.invert_button['text'] = 'Invert On' self.invert_button['bg'] = 'red' self.invert = True self.update_drawing() elif self.invert_button['text'] == 'Invert On': self.invert_button['text'] = 'Invert Off' self.invert_button['bg'] = 'light grey' self.invert = False self.update_drawing()
[docs] def place_animal_precise_func(self): """ This function calls the PlaceAnimal class which will either draw a circle (if only x and y are given) or an arrow (if x, y and theta are given). Before it does so it tries to set any existing arrows invisible. It also changes a button color and changes the boolean switch 'animal_draw_selected'. """ # check if the user entered a value for the x position of the # animal and collect it try: self.place_animal_precise_x = int( self.place_animal_precise_x_text.get("1.0", 'end-1c')) except ValueError: pass # check if the user entered a value for the y position of the # animal try: self.place_animal_precise_y = int( self.place_animal_precise_y_text.get("1.0", 'end-1c')) except ValueError: pass # check if user entered a value for theta - must be between # -pi and +pi - it can be 'NA' as well try: if -np.pi \ <= float(self.place_animal_precise_theta_text.get( "1.0", 'end-1c')) <= np.pi: self.place_animal_precise_theta = \ float(self.place_animal_precise_theta_text.get( "1.0", 'end-1c')) except ValueError: ''' If this is not here, once the user has chosen to give a theta at any point in the history of the arena they would not be able to change it back to not having a direction ''' if self.place_animal_precise_theta_text.get( "1.0", 'end-1c') == 'NA': self.place_animal_precise_theta = 'NA' else: pass if self.place_animal_precise_x != 'NA' \ and self.place_animal_precise_y != 'NA': if self.animal_draw_selected: try: self.arrow.arrow_annotation.set_visible(False) except AttributeError: pass self.place_animal_button['bg'] = 'light grey' self.arrow = None self.arrow = PlaceAnimal( ax=self.ax, plot_of_arena=self.image_of_arena, master=self, start_x=self.place_animal_precise_x, start_y=self.place_animal_precise_y, theta=self.place_animal_precise_theta, precise=True) self.animal_draw_selected = True else: messagebox.showerror( "Error", "Please enter both x\nand y coordinates!")
[docs] def place_animal_func(self): """ After calling this function by pressing the appropriate button, the user is able to draw in the canvas where the animal shall be located. If mouse is pressed and released immediately (a click), a circle will be drawn. If not, an arrow will be drawn. The (inverted) arrowhead indicates the position of the animal at the beginning of the experiment while the other side indicates the angle the animal was last seen. """ if self.animal_draw_selected: try: self.arrow.arrow_annotation.set_visible(False) except AttributeError: pass self.place_animal_button['bg'] = 'red' self.arrow = None self.arrow = PlaceAnimal(ax=self.ax, plot_of_arena=self.image_of_arena, master=self) self.animal_draw_selected = True
[docs] def addition_of_intensity_func(self): """ A callback function for the button 'additive on/off'. The user can toogle between adding intensities and not """ if self.addition_of_intensity_button['text'] == 'Additive Off': self.addition_of_intensity_button['text'] = 'Additive On' self.addition_of_intensity_button['bg'] = 'red' self.addition_of_intensity = True self.update_drawing() elif self.addition_of_intensity_button['text'] == 'Additive On': self.addition_of_intensity_button['text'] = 'Additive Off' self.addition_of_intensity_button['bg'] = 'light grey' self.addition_of_intensity = False self.update_drawing()
[docs] def dont_draw_func(self): """ If the user first wants to draw and then e.g. place an animal or zoom into a part of the figure, they need to first call this function by pressing the corresponding button. It will disconnect the event handler. """ try: self.canvas.disconnect() except AttributeError: # this is the case if the user has # never pressed on any mouse drawing buttons except # animal placement pass self.last_drawing_call = 'none' self.dont_draw_button['bg'] = 'light grey' self.draw_linear_button['bg'] = 'light grey' self.draw_gaussian_button['bg'] = 'light grey'
[docs] def precise_gaussian_draw_func(self): """ In order to precisely draw a gaussian gradient, the user has the option of defining the x/y coordinate and then pressing a button. It then calls this function which will give the precise x/y coodinates (along with all the other arguments that are called when drawing by mouse) to the GaussianSlope Class. """ self.canvas = GaussianSlope( ax=self.ax, arena=self.arena, plot_of_arena=self.image_of_arena, size=self.size, sigma=self.sigma, #bitdepth=self.bitdepth, max_intensity=self.maximum_intensity_gaussian, overwrite=self.overwrite, invert=self.invert, addition_of_intensity=self.addition_of_intensity, mouse_drawing=False, x_coordinate=self.precise_gaussian_x_position, y_coordinate=self.precise_gaussian_y_position )
[docs] def precise_rectangle_draw_func(self): """ In order to precisely draw a step rectangle, the user has the option of defining the x/y coordinate of the center and then pressing a button. It then calls this function which will give the precise x/y coordinates along with all the other arguments that are called when drawing by mouse) to the Step Class. """ self.canvas = Step( ax=self.ax, arena=self.arena, plot_of_arena=self.image_of_arena, size_x=self.size_x, size_y=self.size_y, intensity=self.rectangle_intensity, overwrite=self.overwrite, invert=self.invert, addition_of_intensity=self.addition_of_intensity, mouse_drawing=False, x_coordinate=self.precise_rectangle_x_position, y_coordinate=self.precise_rectangle_y_position )
[docs] def gridlines_func(self): """ When the user presses the Gridlines button, this function turns them on or off. """ if self.gridlines_button['text'] == 'Turn Gridlines On': self.gridlines_button['text'] = 'Turn Gridlines Off' self.ax.grid(True) self.ax.figure.canvas.draw() else: self.gridlines_button['text'] = 'Turn Gridlines On' self.ax.grid(False) self.ax.figure.canvas.draw()
[docs] def update_animal_position(self, x, y, angle): """ After drawing either a circle or an arrow, the PlaceAnimal Class calls this function to let the main class know what the x/y and theta of the latest animal was :param x: x coordinate of the animal :param y: y coordinate of the animal :param angle: the angle (calculated by arctan2 function) that describes where the animal was before the start of the experiment """ self.animal_x_coordinate = x self.animal_y_coordinate = y self.animal_theta = angle try: self.location_animal['text'] = 'x: ' + x + ' y: ' + y self.angle_of_orientation['text'] = '\u03B8 : ' + angle[0:5] except TypeError: self.location_animal['text'] = 'x: ' + repr(x) + ' y: ' \ + repr(y) self.angle_of_orientation['text'] = '\u03B8 : ' \ + repr(angle)[0:5]
[docs] def stop_animal_drawing_func(self): """ After placing an animal with the mouse the user might want to draw more or just zoom into a part of the figure. Pressing the appropriate button will call this function which disconnects the event handler. """ self.place_animal_button['bg'] = 'light grey' try: self.arrow.disconnect() except AttributeError: # this is the case if the user has # never pressed the 'place animal' button pass
[docs] def delete_animal_func(self): """ If the user wants to get rid of the animal after drawing one,they can press this button. It will just give the string 'NA' to all the animal position variables which tells the program that no animal has been selected. """ self.update_animal_position('NA', 'NA', 'NA') try: self.arrow.arrow_annotation.remove() self.ax.figure.canvas.draw() except (ValueError, AttributeError) as error: pass
[docs] def quit_func(self): """ In order to quit this window and go back to the main GUI, the user needs to press the 'quit' button and this function will be called. """ # set main window active again self.child.grab_release() # close the child window self.child.after(0, self.child.destroy())
[docs] class GaussianSlope(object): """ This class is bound to the canvas. There are two ways this class can behave: #. When the user clicks somewhere on the canvas, the x and y coordinates are collected using the :func:`GaussianSlope.on_press` function. This function then calls the :func:`GaussianSlope.draw_gradient` function. In that function the gaussian gradient with the entered size is created. Then the size of the gaussian gradient arena is matched to the size of the image which is given by the resolution and plotted. This makes for a interactive experience for the user who can 'point and click' on the area where a gaussian gradient should be created. #. If the "Draw Gaussian Circle at defined coordinates" buttons is pressed, the x and y coordinate are collected from the "Coordinates" entry boxes. Then the :func:`GaussianSlope.draw_gradient` is called and the gradient is created identical to the mouse click version. By varying the size of the gaussian gradient it is also possible to have more than one gradient. If only one gradient is to be created the user should just leave the original setting in place (2000) as this is way larger than the resolution used to record the behavior. By varying sigma the user can choose the steepness of the gradient """ def __init__(self, ax, arena, plot_of_arena, size, sigma, max_intensity, overwrite, invert, addition_of_intensity, mouse_drawing, x_coordinate=None, y_coordinate=None): # gca = get current axis, which just means this call gets the # current axis. Could be more explicit by passing the self.ax # from the VRArena class - might to that at one point. self.ax = ax self.sigma = sigma self.size = size self.arena = arena self.image_of_arena = plot_of_arena self.max_intensity_percentage = max_intensity / 100 self.overwrite = overwrite self.invert = invert self.addition_of_intensity = addition_of_intensity self.mouse_drawing = mouse_drawing if self.mouse_drawing: self.x0 = None self.y0 = None self.cidpress = self.ax.figure.canvas.mpl_connect( 'button_press_event', self.on_press) else: self.x0 = x_coordinate self.y0 = y_coordinate self.draw_gradient() def on_press(self, event): print('User pressed a mouse button') # Get the x and y coordinate of the position that the user # has clicked. The values are directly rounded and the # integer taken as those values will be used to index a # matrix (so it can't be a floating point number) self.x0 = int(round(event.xdata)) self.y0 = int(round(event.ydata)) self.draw_gradient()
[docs] def draw_gradient(self): """ This class collects the size and sigma of the gradient and draws it at the coordinates where the user pressed on the canvas. """ if self.size % 2 == 1: # if a uneven number was chosen self.size -= 1 # todo - this is a workaround to keep the # code below clean. half_size = round(self.size / 2) # call the gaussian kernel function gaussian_gradient = self.gkern(kernlen=self.size, std=self.sigma) # Explicit definition of all indexes that will be used to # improve readability of the code. # Those definitions will only work if the size of the # gaussian gradient is smaller than the size of the arena # and it doesn't touch the edges of the arena. image_x_lower = self.x0 - half_size image_x_upper = self.x0 + half_size image_y_lower = self.y0 - half_size image_y_upper = self.y0 + half_size gradient_x_lower = 0 gradient_x_upper = self.size gradient_y_lower = 0 gradient_y_upper = self.size # In case the gaussian gradient goes over the border of the # matrix it becomes necessary to manually define # the indexes if self.x0 - half_size < 0: # This will happen if a small gaussian_gradient is on the # far left of the arena and, e.g. x = 1 or if we have a # huge gaussian gradient image_x_lower = 0 gradient_x_lower = half_size - self.x0 if self.x0 + half_size > self.arena.shape[1]: # same as above but on the right (large x) image_x_upper = self.arena.shape[1] gradient_x_upper = (self.size - ( (self.x0 + half_size) - self.arena.shape[1])) if self.y0 - half_size < 0: # same as above but on top (small y) image_y_lower = 0 gradient_y_lower = half_size - self.y0 if self.y0 + half_size > self.arena.shape[0]: # same as above but on bottom (large y) image_y_upper = self.arena.shape[0] # e.g. arena is on the lower edge, size of VR is 20 and # there y = 399 (of 400). # 399 + 10 = 409 # 409 - 400 = 9 # 20 - 9 = 11 # arena, take 0:11 gradient_y_upper = (self.size - ((self.y0 + half_size) - self.arena.shape[0])) # The gkern defines the kernel in floating point numbers and # only from 0...1. As we need to have either 8bit or 16bit the # numbers need to be converted. # This is trivial as it's just the number * the bitdepth. # At this point it's also easy to directly transform to the # requested maximum intensity # If requested, the inversion of the gaussian kernel also # happens at this point ''' if self.bitdepth == '16': gaussian_gradient *= 65535 * \ self.max_intensity_percentage #TODO gaussian_gradient = gaussian_gradient.astype(np.uint16) if self.invert: gaussian_gradient = 65535 - gaussian_gradient #TODO elif self.bitdepth == '8': gaussian_gradient *= 255 * self.max_intensity_percentage gaussian_gradient = gaussian_gradient.astype(np.uint8) if self.invert: gaussian_gradient = 255 - gaussian_gradient ''' gaussian_gradient *= 100 * self.max_intensity_percentage #gaussian_gradient = gaussian_gradient.astype(np.float64) if self.invert: gaussian_gradient = 100 - gaussian_gradient # Next, the arena is overwritten with the new values at the # right location. if self.overwrite: # overwrite the original arena by using all the variables # that were defined above. self.arena[image_y_lower:image_y_upper, image_x_lower:image_x_upper] = \ gaussian_gradient[int(gradient_y_lower):int(gradient_y_upper), int(gradient_x_lower):int(gradient_x_upper)].copy() else: if self.addition_of_intensity: # input from Matthieu: if we have two odor sources # close by the odor concentration will be larger # Here the original arena and the new arena are # compared and depending on the max value everything # else (than the max point) will be downscaled # first take a copy of the original arena mask = self.arena.copy() # do exactly what would be done if overwrite were # true (above), but on the mask mask[image_y_lower:image_y_upper, image_x_lower:image_x_upper] \ = gaussian_gradient[int(gradient_y_lower):int(gradient_y_upper), int(gradient_x_lower):int(gradient_x_upper)].copy() summed_arena = self.arena.astype(np.float64) \ + mask.astype(np.float64) # find the maximum value max_value = np.amax(summed_arena) self.arena = 100/max_value * summed_arena else: # overwrite only values that are smaller, leave the # larger values alone first take a copy of the # original arena mask = self.arena.copy() # do exactly what would be done if overwrite were # true (above), but on the mask mask[image_y_lower:image_y_upper, image_x_lower:image_x_upper] \ = gaussian_gradient[int(gradient_y_lower):int(gradient_y_upper), int(gradient_x_lower):int(gradient_x_upper)].copy() if self.invert: # if in inverted world, take only values that are # larger in the original compared to the new to be # replaced by the new values self.arena[np.where(mask < self.arena)] \ = mask[np.where(mask < self.arena)] else: # if NOT in inverted world, only take values that # are smaller in the original compared to the new # values to be replaced by the new values self.arena[np.where(mask > self.arena)] \ = mask[np.where(mask > self.arena)] # display the new image first by setting the data self.image_of_arena.set_data(self.arena) # and then redrawing the canvas. self.ax.figure.canvas.draw()
[docs] def gkern(self, kernlen=20, std=3): """ Returns a 2D Gaussian kernel array. Taken from: https://stackoverflow.com/questions/29731726 /how-to-calculate-a-gaussian-kernel-matrix-efficiently-in-numpy """ gkern1d = signal.gaussian(kernlen, std=std).reshape(kernlen, 1) gkern2d = np.outer(gkern1d, gkern1d) return gkern2d
[docs] def disconnect(self): """ Disconnect the mouse button clicks from the canvas """ try: self.ax.figure.canvas.mpl_disconnect(self.cidpress) except AttributeError: pass
[docs] class Step(object): """ This class is bound to the canvas. This class is called to draw precise Rectangles using coordinates *and* when drawing Rectangles with the mouse. Behavior in "precise" mode: Collect the x and y coordinate from the entry field Behavior in "mouse" mode: When the user clicks somewhere on the canvas, the x and y coordinates are collected :func:`Step.on_press`. The rest of the behavior is identical as the function :func:`Step.draw_rectangle` is called This class does essentially the same as the :class:`GaussianSlope` class with the difference of not calling the gkern function. Source code is quite explicit and heavily annotated. """ def __init__(self, ax, arena, plot_of_arena, size_x, size_y, #bitdepth, intensity, overwrite, invert, addition_of_intensity, mouse_drawing, x_coordinate=None, y_coordinate=None): self.ax = ax self.size_y = size_y self.size_x = size_x self.arena = arena self.image_of_arena = plot_of_arena #self.bitdepth = bitdepth self.intensity_percentage = intensity / 100 self.overwrite = overwrite self.invert = invert self.addition_of_intensity = addition_of_intensity self.mouse_drawing = mouse_drawing if mouse_drawing: self.x0 = None self.y0 = None self.cidpress = self.ax.figure.canvas.mpl_connect( 'button_press_event', self.on_press) else: self.x0 = x_coordinate self.y0 = y_coordinate self.draw_rectangle()
[docs] def on_press(self, event): """ This function just collectes the x/y coordinate of where the user pressed the mouse button """ print('press') self.x0 = int(round(event.xdata)) self.y0 = int(round(event.ydata)) self.draw_rectangle()
[docs] def draw_rectangle(self): """ This function is called either after the user pressed the mouse button on the canvas or if the x/y coordinates have been entered manually. """ if self.size_x % 2 == 1: # if a uneven number was chosen self.size_x -= 1 # todo - this is a workaround to keep # the code below clean. if self.size_y % 2 == 1: self.size_y -= 1 half_size_x = int(self.size_x / 2) half_size_y = int(self.size_y / 2) image_x_lower = self.x0 - half_size_x image_x_upper = self.x0 + half_size_x image_y_lower = self.y0 - half_size_y image_y_upper = self.y0 + half_size_y if self.x0 - half_size_x < 0: image_x_lower = 0 if self.x0 + half_size_x > self.arena.shape[1]: image_x_upper = self.arena.shape[1] if self.y0 - half_size_y < 0: image_y_lower = 0 if self.y0 + half_size_y > self.arena.shape[0]: image_y_upper = self.arena.shape[0] ''' if self.bitdepth == '16': step_intensity = 65535 * self.intensity_percentage if self.invert: step_intensity = 65535 - step_intensity elif self.bitdepth == '8': step_intensity = 255 * self.intensity_percentage if self.invert: step_intensity = 255 - step_intensity ''' step_intensity = 100 * self.intensity_percentage if self.invert: step_intensity = 100 - step_intensity # This is different from GaussianSlope class in that not the # gaussian kernel is being used but the scalar # value requested by the user if self.overwrite: # overwrite the original arena self.arena[image_y_lower:image_y_upper, image_x_lower:image_x_upper] = step_intensity else: if self.addition_of_intensity: # first take a copy of the original arena mask = self.arena.copy() # do exactly what would be done if overwrite were # true (above), but on the mask mask[image_y_lower:image_y_upper, image_x_lower:image_x_upper] = step_intensity #print(mask[image_y_lower:image_y_upper, # image_x_lower:image_x_upper]) # then add the new and the old arena ''' if self.bitdepth == '8': summed_arena = self.arena.astype(np.uint16) \ + mask.astype(np.uint16) elif self.bitdepth == '16': summed_arena = self.arena.astype(np.uint32) \ + mask.astype(np.uint32) ''' summed_arena = self.arena.astype(np.float64) \ + mask.astype(np.float64) # find the maximum value (e.g. at 8 bit if two times # 100% we will have 255+255=510) max_value = np.amax(summed_arena) print(max_value) ''' if self.bitdepth == '8': # take bitdepth and divide by the max_value and # multiply by the summed arena self.arena = 255 / max_value * summed_arena elif self.bitdepth == '16': self.arena = 65535 / max_value * summed_arena ''' self.arena = 100 / max_value * summed_arena else: mask = self.arena.copy() mask[image_y_lower:image_y_upper, image_x_lower:image_x_upper] = step_intensity if self.invert: self.arena[np.where(mask < self.arena)] \ = mask[np.where(mask < self.arena)] elif not self.invert: self.arena[np.where(mask > self.arena)] \ = mask[np.where(mask > self.arena)] self.image_of_arena.set_data(self.arena) self.ax.figure.canvas.draw()
[docs] def disconnect(self): """ This function is called directly from the master and disconnects the event handler """ try: self.ax.figure.canvas.mpl_disconnect(self.cidpress) except AttributeError: pass
[docs] class PlaceAnimal(object): """ This class is bound to the image that has been plotted to show the arena. There are three ways this class can behave: #. If no x/y and theta values are passed: When the user clicks somewhere first the x and y coordinates are collected. After the user has released the mouse button, the coordinates for the press and release are compared. If they are identical, the animal will not have a directionality which is displayed as a circle. If they are not identical, the point of release is seen as the point where the animal will be when the experiment starts. The point where the user pressed is the direction where the animal is coming from. #. If x/y but not theta are provided: The class will just draw a circle (no directionality of the animal is assumed). #. If x/y and theta are provided, an arrow is drawn with the given coordinates as the place where the animal will be when the experiment starts and theta as the direction where the animal was before. """ def __init__(self, ax, plot_of_arena, master, start_x=None, start_y=None, theta=None, precise=False): self.ax = ax self.image_of_arena = plot_of_arena self.master = master self.x_release = start_x self.y_release = start_y self.x_press = None self.y_press = None self.theta = theta if precise: if self.theta == 'NA': self.draw_point() else: self.x_press = self.x_release + 10 * np.cos(self.theta) self.y_press = self.y_release + 10 * np.sin(self.theta) self.draw_arrow() else: self.cidpress = self.ax.figure.canvas.mpl_connect( 'button_press_event', self.on_press) self.cidrelease = self.ax.figure.canvas.mpl_connect( 'button_release_event', self.on_release) def on_press(self, event): self.x_press = int(round(event.xdata)) self.y_press = int(round(event.ydata)) def on_release(self, event): self.x_release = int(round(event.xdata)) self.y_release = int(round(event.ydata)) if self.x_release == self.x_press and self.y_release == self.y_press: self.draw_point() else: self.draw_arrow()
[docs] def draw_arrow(self): """ This function is called either after the mouse button is released and x/y at press and release are not identical or if the x/y and theta are provided. First it will try to remove the arrow or circle that is already present, then it'll draw a new arrow. """ try: self.arrow_annotation.remove() except AttributeError: pass # This is suboptimal. # I know there is self.arrow.set_position and # self.arrow.set_text - but how do I set the xytext position??? # I suspect I have to dig a bit deeper # into how text() works, or maybe annotate? self.arrow_annotation = self.ax.arrow( self.x_press, # x self.y_press, # y self.x_release - self.x_press, # dx self.y_release - self.y_press, # dy head_width=50, head_length=-25, fc='r', ec='r') self.master.update_animal_position( self.x_release, self.y_release, np.arctan2(self.y_press - self.y_release, self.x_press - self.x_release)) self.ax.figure.canvas.draw()
[docs] def draw_point(self): """ This function is called either after the mouse button is released and x/y at press and release are identical or if the x/y coordinates (but not theta) are provided. First it will try to remove the arrow or circle that is already present, then it'll draw a new circle. """ try: self.arrow_annotation.remove() except AttributeError: pass circle = Circle((self.x_release, self.y_release), 10) self.arrow_annotation = self.ax.add_artist(circle) self.master.update_animal_position(self.x_release, self.y_release, 'NA') self.ax.figure.canvas.draw()
[docs] def disconnect(self): """ Disconnect all mouse buttons from canvas. Called directly from the master """ self.ax.figure.canvas.mpl_disconnect(self.cidpress) self.ax.figure.canvas.mpl_disconnect(self.cidrelease)
# todo - should I also save all the things the user entered? It # would be easy and not space intensive, but probably a # bit unreadable (think, 1 gaussian at x=50, y=150, # sigma = 20, then 2 step at x1=250, y1=25, x2=260, y2= 400) # I think I should definitely keep the csv file, but the above # would save a lot of space if users don't completely # overdo it with clicking around. Could be saved a json...it # would be like a history of what happened to that file