# 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

# 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':
RASPBERRY = True
LINUX = True
else:
RASPBERRY = False
LINUX = True
except AttributeError:
RASPBERRY = False
LINUX = False

# 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!

[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
'''
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.general_drawing_frame,

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.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()

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.
if it should be overwritten if it exits.
Otherwise it just saves, without any confirmation etc.
The areas are always saved with the resolution as they are
not interchangable.
"""

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'):
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.
"""
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, 784]:
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,
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,
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 press and release are at the same position, 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 expriment 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

"""
A callback function for the button 'additive on/off'.
The user can toogle between adding intensities and not
"""
self.update_drawing()
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 so called 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,
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,
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

#. 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
"""

def __init__(self, ax, arena, plot_of_arena, size, sigma,
max_intensity, overwrite, invert,
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.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

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))

"""
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
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

# 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
image_x_lower = 0

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]
(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

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':
self.max_intensity_percentage #TODO
if self.invert:
elif self.bitdepth == '8':
if self.invert:
'''
if self.invert:

# 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] = \
else:
# 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
# do exactly what would be done if overwrite were
# true (above), but on the mask
summed_arena = self.arena.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
# do exactly what would be done if overwrite were
# true (above), but on the mask
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
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

# 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,
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.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:
# first take a copy of the original arena
# do exactly what would be done if overwrite were
# true (above), but on the mask
image_x_lower:image_x_upper] = step_intensity
#      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) \
elif self.bitdepth == '16':
summed_arena = self.arena.astype(np.uint32) \
'''
summed_arena = self.arena.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:
image_x_lower:image_x_upper] = step_intensity
if self.invert:
elif not self.invert:

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
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.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