__author__ = 'David Tadres'
__project__ = 'PiVR'
__version__ = '1.8.0'
__date__ = '1st of December, 2023'
#####################################################
# This seems to be necessary in order to run on MacOS
import matplotlib
matplotlib.use('TkAgg')
#####################################################
# general imports
import json
import os
import time
import tkinter as tk
from tkinter import font as tkfont
from tkinter import filedialog, simpledialog
from glob import glob
import numpy as np
import pandas as pd
from PIL import Image, ImageTk
from matplotlib.patches import Circle
import calendar
import subprocess
import pathlib
# Wrap the undistort functionality to not break older installations
# when they update. # < Todo this can probably become a function
try:
import cv2
CV2_INSTALLED = True
except ModuleNotFoundError:
CV2_INSTALLED = False
# local modules_recording
#import control_file # search through file and replace in docs!
import modules_GUI.GUI_common_subframes as GUI_common_subframes
import modules_GUI.GUI_common_variables as GUI_common_variables
import modules_GUI.GUI_multi_animal_tracking as GUI_multi_animal_tracking
import modules_offline.VR_drawing_board as VR_drawing_board
import modules_common.system_utils as system_utils
import modules_common.px_per_mm_configuration as px_per_mm_configuration
import modules_common.RPi_camera as RPi_camera
import modules_common.git_version as git_version
import modules_common.output_channels as output_channels
import modules_common.enter_setup_name as enter_setup_name
import modules_common.animal_color_selection as animal_color_selection
import modules_common.LED_functions as LED_functions
import modules_recording.macro_editor as macro_editor
# Check where the code is run (Raspberry Pi, Windows, Mac)
RASPBERRY, LINUX, DIRECTORY_INDICATOR = system_utils.check_system()
# SOME CONSTANTS:
# depending on the sampling frequency (set when starting the program
# in the terminal when starting the pigpio daemon)
# you need to comment/uncomment the correct variable
# MAX_PWM_FREQ = 40000 # sampling freq = 1us < 24/8/13 now defined in PiVR_settings.json
# MAX_PWM_FREQ = 20000 # sampling freq = 2us
# MAX_PWM_FREQ = 10000 # sampling freq = 4us
# MAX_PWM_FREQ = 8000 # sampling freq = 5us
# MAX_PWM_FREQ = 5000 # sampling freq = 8us
# MAX_PWM_FREQ = 4000 # sampling freq = 10us
# The numbers between 0 and the number defined here are used to
# define the dutycyle of the GPIO. The maximum value for the
# PWM_RANGE is 40000:
# http://abyz.me.uk/rpi/pigpio/python.html#set_PWM_range
# PWM_RANGE = 100 < 24/8/13 now defined in PiVR_settings.json
# Here is a switch for simulating the Raspberry Pi experience on
# another computer
#virtual_raspberry = False
# Keep a reference to the path of the PiVR software.
PiVR_ROOT_PATH = os.path.abspath(os.getcwd())
# load PiVR settings, such as pwm range and settings used when
# last run such as recording time and camera resolution
PiVR_SETTINGS = system_utils.load_GUI_variables()
# IMPORT SPECIFIC MODULES DEPENDING ON WHERE THIS APP IS RUN
if RASPBERRY or system_utils.is_virtual_raspberry():
import modules_GUI.GUI_frame_tracking as GUI_frame_tracking
import modules_GUI.GUI_frame_VR_tracking as GUI_frame_VR_tracking
import modules_GUI.GUI_frame_dynamic_VR_tracking as GUI_frame_dynamic_VR_tracking
import modules_GUI.GUI_frame_video as GUI_frame_video
import modules_GUI.GUI_frame_full_frame_images as GUI_frame_full_frame_images
import modules_GUI.GUI_frame_timelapse as GUI_frame_timelapse
else:
import modules_GUI.GUI_frame_analysis as GUI_frame_analysis
import modules_GUI.GUI_frame_image_handling as GUI_frame_image_handling
import modules_GUI.GUI_frame_display_experiment as GUI_frame_display_experiment
import modules_GUI.GUI_frame_simulate_realtime_tracking as GUI_frame_simulate_realtime_tracking
# Load the 'list_of_available_organism.json'
ORGANISM_HEURISTICS = system_utils.organisms_and_heuristics()
# Grab current git branch and git hash to save in experiment_settings.json
VERSION_INFO, CURRENT_GIT_BRANCH, CURRENT_GIT_HASH = git_version.get_PiVR_version_info(__version__)
# do happen
[docs]
class PiVR(tk.Tk):
"""
This class initializes the GUI the user will see. There
are several different frames (e.g. "Tracking" vs "Virtual
Arena") that are all created differently.
To do this, the "PiVR" class calls a number of
other classes. To help with this the following three "helper"
classes are important:
#) "CommonVariables" contains variables that are true
between frames,
#) "SubFrames" helps with the creation of
the different frames and finally
#) "CommonFunction"
which contains functions that are called in different
frames.
The actual frames (e.g. "TrackingFrame") are then created by
"constructor" classes which call different components of the
three classes described
above.
The "helper" classes are necessary as they can save variables
and functions between different frames (similar to global
variables). For example, if the user would select at
particular folder to save all the experimental data,
the "CommonVariables" class saves this folder when the user
is then switching from, for example, the "Tracking" frame to the
"Virtual Arena" frame.
"""
def __init__(self, *args, **kwargs):
"""
"""
tk.Tk.__init__(self, *args, **kwargs)
self.title_font = tkfont.Font(family='Helvetica', size=18)
if LINUX:
try:
self.wm_iconbitmap(bitmap = '@pics/VRLarvaBanana32px.xbm')
except:
# it's a tkinter.TclError when opening on the Pi
# with the shortcut
pass
else:
self.wm_iconbitmap(pathlib.Path(PiVR_ROOT_PATH, 'pics', 'VRLarvaBanana32px.ico'))
if not CV2_INSTALLED:
self.wm_title('PiVR ' + __version__ + ', noCV2')
else:
self.wm_title('PiVR ' + __version__ + " - "
+ PiVR_SETTINGS["setup name"])
#self.wm_title('PiVR ' + __version__)
# the container is where we'll stack a bunch of frames on top
# of each other, then the one we want visible will be raised
# above the others
container = tk.Frame(self)
container.grid(row=0, column=0)
container.grid_rowconfigure(0, weight=1)
container.grid_columnconfigure(0, weight=1)
# Initialize the camera instance - this is the only time this
# happens!
# If error, e.g. camera not connected/not turned on the
# script fails here. So if we want to give a more meaningful
# error message, this would be the place to raise tkinter
# exception and let the user know what to do!
camera_class_pointer = RPi_camera.Camera()
# Call the modules_common classes first - every class that wants to
# use any of those variables will get to work with this
# variable which has the whole class of variables used by the
# GUI.
# After initalization, these can be called from inside other
# classes.
self.all_common_variables = \
GUI_common_variables.CommonVariables(camera_class=camera_class_pointer)
self.all_common_functions = \
CommonFunctions(camera_class=camera_class_pointer,
controller=self)
self.sub_frames = \
GUI_common_subframes.SubFrames(camera_class=camera_class_pointer, controller=self)
# figure out where the script itself is.
if LINUX:
# todo - test with a mac!
self.path_of_program = \
(os.path.realpath(__file__)[0:-len(
os.path.realpath(__file__).split('/')[-1])])
#print(self.path_of_program)
else: # Assume it's Windows
self.path_of_program = \
(os.path.realpath(__file__)[0:-len(
os.path.realpath(__file__).split('\\')[-1])])
print(self.path_of_program)
#print(pathlib.Path.cwd()) # Todo - for readability can
# try to change to a pathlib.Path - but need to change a
# lot of strings that use self.path_of_program
# Now create an empty dictionary. This dictionary will be
# used to address the newly created tkinter frames
self.frames = {}
# This loop takes the list of the "constructor" classes
if RASPBERRY or system_utils.is_virtual_raspberry():
for F in (GUI_frame_tracking.TrackingFrame,
GUI_frame_VR_tracking.VirtualRealityFrame,
GUI_frame_dynamic_VR_tracking.DynamicVirtualRealityFrame,
GUI_frame_video.VideoFrame,
GUI_frame_full_frame_images.FullFrameImagesRecording,
GUI_frame_timelapse.TimelapseRecording,
#FixMetadata,
#GUI_simulate_online_analysis.SimulateOnlineExperiment,
):
# previously included: , TryFastCam, FastDownscaleSave,
# AnalyzeVideoExperiment
# This will save the name of the class (e.g.
# TrackingFrame) so that it can be used as a
# dictionary key later on
page_name = F.__name__
# Now construct the different frames using the
# constructor classes. All frames are constructed in
# the "container" tk.Frame().
frame = F(parent=container,
controller=self,
camera_class=camera_class_pointer,
version_info=VERSION_INFO)
# Here the "frames" dictionary is filled with the
# different frame instances
self.frames[page_name] = frame
# put all of the pages in the same location;
# the one on the top of the stacking order
# will be the one that is visible.
frame.grid(row=0, column=0, sticky="nsew")
# In case the software is started not on the PiVR (and the
# virtual_raspberry boolean is turned to False) a slightly
# different frame dictionary is created.
elif not system_utils.is_virtual_raspberry():
for F in (GUI_frame_analysis.TrackedAnalysisFrame,
GUI_frame_image_handling.ImageDataHandling,
#FixMetadata,
GUI_frame_display_experiment.DisplayTrackedImage,
GUI_multi_animal_tracking.MultiAnimalTracking,
GUI_frame_simulate_realtime_tracking.SimulateOnlineExperiment, ):
# This will save the name of the class (e.g.
# TrackingFrame) so that it can be used as a
# dictionary key later on
page_name = F.__name__
# Now construct the different frames using the
# constructor classes. All frames are constructed in
# the "container" tk.Frame().
frame = F(parent=container,
controller=self,
camera_class=camera_class_pointer)
# Here the "frames" dictionary is filled with the
# different frame instances
self.frames[page_name] = frame
# put all of the pages in the same location;
# the one on the top of the stacking order
# will be the one that is visible.
frame.grid(row=0, column=0, sticky="nsew")
# After constructing all the individual frames, an immutable
# menubar on top of the window is created
self.menubar = tk.Menu(self)
# The creation of the menubar goes from left to right
# We start with the "File" menu that enables the user to do
# globally valid actions, such as closing the window or
# updating the software!
menu_file = tk.Menu(self.menubar, tearoff=0)
self.menubar.add_cascade(label="File", menu=menu_file)
# To this Menu-object, a number of different options
# attached to functions are created:
menu_file.add_command(
label="Save settings",
command=lambda: self.all_common_functions.quit_sequence(
save=True, exit=False))
menu_file.add_command(
label="Save and exit",
command=lambda: self.all_common_functions.quit_sequence(
save=True, exit=True))
menu_file.add_command(
label='Discard changes and exit',
command=lambda: self.all_common_functions.quit_sequence(
save=False, exit=True))
menu_file.add_command(
label='About/Version',
command=self.all_common_functions.software_info)
menu_file.add_command(
label='Update software',
command=self.all_common_functions.update_software)
# In the "Recording" menu, the user can choose between the
# different recording options!
menu_recording = tk.Menu(self.menubar, tearoff=0)
self.menubar.add_cascade(label="Recording", menu=menu_recording)
menu_recording.add_command(
label="Tracking",
command=lambda: self.show_frame('TrackingFrame'))
menu_recording.add_command(
label="VR Arena",
command=lambda: self.show_frame('VirtualRealityFrame'))
menu_recording.add_command(
label="Dynamic VR Arena",
command=lambda: self.show_frame(
'DynamicVirtualRealityFrame'))
menu_recording.add_command(
label='Full Frame Recording',
command=lambda: self.show_frame('FullFrameImagesRecording'))
menu_recording.add_command(
label='Timelapse Recording',
command=lambda: self.show_frame('TimelapseRecording')
)
menu_recording.add_command(
label="Video",
command=lambda: self.show_frame('VideoFrame'))
# In the "Analysis" menu, the user can choose different
# tools option
menu_tools = tk.Menu(self.menubar, tearoff=0)
self.menubar.add_cascade(label="Tools", menu=menu_tools)
menu_tools.add_command(
label='Analysis',
command=lambda: self.show_frame('TrackedAnalysisFrame'))
menu_tools.add_command(
label='Image data handling',
command=lambda: self.show_frame('ImageDataHandling')
)
#menu_tools.add_command(
# label='Fix Metadata',
# command=lambda: self.show_frame('FixMetadata'))
menu_tools.add_command(
label='Display tracked experiment',
command=lambda: self.show_frame('DisplayTrackedImage'))
menu_tools.add_command(
label='Multi-Animal Tracking',
command=lambda: self.show_frame('MultiAnimalTracking'))
menu_tools.add_command(
label='Draw VR arena',
command=self.all_common_functions.draw_VR_arena)
menu_tools.add_command(
label='Undistort, new lens',
command=self.all_common_functions.undistort_new_lens_menu)
# Here goes the "Debug" menu - currently the only option is
# to simulate an online experiment
menu_simulate = tk.Menu(self.menubar, tearoff=0)
self.menubar.add_cascade(label='Debug', menu=menu_simulate)
menu_simulate.add_command(
label='Simulate Online Tracking',
command=lambda : self.show_frame('SimulateOnlineExperiment'))
# Finally, the "Options' Menu is defined. Quite a few
# essential options are in here such as
menu_options = tk.Menu(self.menubar, tearoff=0)
self.menubar.add_cascade(label='Options', menu=menu_options)
# Add a menu to select organism using a dropdown menu which
# allows for maximum flexibility as the end user can and
# should change the list of organisms!
organism_menu = tk.Menu(self, tearoff=0)
menu_options.add_cascade(label='Select Organism',
menu=organism_menu)
# As the content of the menu option are dependent on what was
# read in "available_organisms.json", use a loop to populate it
for organism_names in ORGANISM_HEURISTICS:
organism_menu.add_radiobutton(
label=organism_names,
value=organism_names,
variable=self.all_common_variables.model_organism_variable)
# add radiobutton to turn debug mode on or off
debug_menu = tk.Menu(self, tearoff=0)
menu_options.add_cascade(
label='Turn Debug Mode...', menu = debug_menu)
debug_options = ['OFF', 'ON']
for debug_option_names in debug_options:
debug_menu.add_radiobutton(
label=debug_option_names,
value=debug_option_names,
variable=self.all_common_variables.debug_mode_var)
# The following menu items call functions found in the
# all_common_functions class
menu_options.add_command(
label='Define Pixel/mm',
command=self.all_common_functions.distance_configuration_func)
menu_options.add_command(
label='Optimize Image',
command=self.all_common_functions.camera_controls_func)
menu_options.add_command(
label='Select Output Channels',
command=self.all_common_functions.select_output_channels)
menu_options.add_command(
label='High Power LEDs',
command=self.all_common_functions.high_power_LED)
menu_options.add_command(
label='Animal Detection Method',
command=self.all_common_functions.animal_detection_method_func)
menu_options.add_command(
label='VR Stimulation Point',
command=self.all_common_functions.vr_body_part_stim_func)
menu_options.add_command(
label='Animal Color',
command=self.all_common_functions.select_signal)
menu_options.add_command(
label='Output Files',
command=self.all_common_functions.output_files_func)
menu_options.add_command(
label='Undistort Options',
command=self.all_common_functions.undistort_online_func)
menu_options.add_command(
label='Setup Name',
command=self.all_common_functions.call_enter_setup_name)
# And finally, this menu allows the user to design their own
# virtual arena using the drawing board.
#menu_VR_options = tk.Menu(self.menubar, tearoff=0)
#self.menubar.add_cascade(label='VR', menu=menu_VR_options)
#menu_VR_options.add_command(
# label='Draw VR arena',
# command=self.all_common_functions.draw_VR_arena)
top = self.winfo_toplevel()
top.config(menu=self.menubar)
# Here, a number of the just created menu options are
# disabled depending on the platform the PiVR software is
# being used.
if RASPBERRY or system_utils.is_virtual_raspberry():
menu_tools.entryconfig(
"Analysis", state="disabled")
menu_tools.entryconfig(
"Display tracked experiment", state="disabled")
#menu_tools.entryconfig(
# "Post-Hoc Single Animal Analysis", state="disabled")
menu_tools.entryconfig(
"Multi-Animal Tracking", state="disabled")
menu_tools.entryconfig(
"Draw VR arena", state="disabled")
menu_tools.entryconfig(
"Image data handling", state="disabled")
#menu_tools.entryconfig(
# 'Undistort, new lens', state='disabled')
menu_simulate.entryconfig(
"Simulate Online Tracking", state="disabled")
menu_options.entryconfig(
"Turn Debug Mode...", state="disabled")
elif not system_utils.is_virtual_raspberry():
#menu_file.entryconfig(
# "Update software", state="disabled")
menu_recording.entryconfig(
"Tracking", state="disabled")
menu_recording.entryconfig(
"VR Arena", state="disabled")
menu_recording.entryconfig(
"Dynamic VR Arena", state="disabled")
menu_recording.entryconfig(
"Full Frame Recording", state="disabled")
menu_recording.entryconfig(
"Timelapse Recording", state="disabled")
menu_recording.entryconfig(
"Video", state="disabled")
menu_options.entryconfig(
"Select Organism", state="disabled")
menu_options.entryconfig(
"Define Pixel/mm", state="disabled")
menu_options.entryconfig(
"Optimize Image", state="disabled")
menu_options.entryconfig(
"Select Output Channels", state="disabled")
menu_options.entryconfig(
"High Power LEDs", state="disabled")
#menu_options.entryconfig(
# "Animal Color", state="disabled")
#menu_options.entryconfig(
# "Output Files", state="disabled")
if not CV2_INSTALLED:
menu_options.entryconfig(
"Undistort Options", state="disabled")
if not CV2_INSTALLED:
menu_tools.entryconfig(
'Undistort, new lens', state='disabled')
if PiVR_SETTINGS is not None and 'opencv_information_box' in PiVR_SETTINGS:
self.all_common_variables.opencv_information_box = \
PiVR_SETTINGS['opencv_information_box']
else:
self.all_common_variables.opencv_information_box = True
if self.all_common_variables.opencv_information_box:
self.all_common_variables.opencv_information_box = \
tk.messagebox.askyesno(title='OpenCV not installed',
message='With v1.7.0 released in October 2021 '
'PiVR can use OpenCV to undistort '
'the image during tracking.\n'
'To take advantage of this feature '
'please install opencv.\n'
'The easiest way to install opencv is '
'to wipe the SD '
'card and re-install everything, '
'including the OS from scratch.\n'
'For more information, please visit '
'www.pivr.org and go to "Advanced Topics"'
'--> "Create your own undistort files".\n\n'
'Please press "YES" if you want to see '
'this message again.\n'
'Please press "NO" if you do not want '
'to see this message anymore' )
# Whenever the software starts, display the last shown frame
if PiVR_SETTINGS is not None and 'Window' in PiVR_SETTINGS:
self.show_window = PiVR_SETTINGS['Window']
else:
if RASPBERRY or system_utils.is_virtual_raspberry():
self.show_window ='TrackingFrame'
else:
self.show_window = 'TrackedAnalysisFrame'
try:
self.show_frame(self.show_window)
except KeyError:
# # this happens when the user opens the software for the
# first time on a PC (non-virtual RPi)
if RASPBERRY or system_utils.is_virtual_raspberry():
self.show_window = 'TrackingFrame'
else:
self.show_window ='TrackedAnalysisFrame'
self.show_frame(self.show_window)
# Here, the term protocol refers to the interaction between
# the application and the window manager. The most commonly
# used protocol is called WM_DELETE_WINDOW, and is used to
# define what happens when the user explicitly closes a
# window using the window manager.
# (http://effbot.org/tkinterbook/tkinter-events-and-bindings.htm#protocols)
def on_closing():
if tk.messagebox.askokcancel("Quit without "
"\n saving",
"Do you want to quit "
"\nwithout saving your "
"settings?"):
self.destroy()
# root.destroy()
self.protocol("WM_DELETE_WINDOW", on_closing)
[docs]
def show_frame(self, page_name):
"""
The function above is called when the user presses on a different
frame. It takes the selected frame and raises it to the top.
In addition, it saves the current page name which is needed
to pass around as a reference to the currently active frame
when calling functions that are generally called, such as
starting an experiment!
"""
frame = self.frames[page_name]
frame.tkraise()
# also save which window we currently have up - needed for referencing
self.current_window = page_name
print(self.current_window)
# finally, for user friendliness - if the user has been
# presenting a virtual reality it is shown in the preview
# window.
# If user now goes to, e.g. 'Tracking' tab, this is not
# intuitive as this would not be presented.
# Therefore check here if the current "page_name" is
# "VirtualRealityFrame" - if yes, check if an arena has been
# chosen and present it in the preview window.
# If the current page_name is NOT "VirtualRealityFrame",
# remove any preview
if page_name == "VirtualRealityFrame":
# check if arena has been selected
if self.all_common_variables.overlay is not None:
self.all_common_functions.preview_overlay_func()
else:
# # Remove any overlay on the preview window!
if self.all_common_variables.overlay_image is not None:
try:
self.all_common_variables.cam.remove_overlay(
self.all_common_variables.overlay)
except Exception as ex:
# This happens when selecting a VR arena,
# then selecting e.g. Video and then selecting
# tracking or some other window that is not
# VirtualRealityFrame.
#print('Tried removing overlay after removing it '
# 'already:')
#print(ex)
pass
[docs]
def call_start_experiment_function(self, page_name):
"""
The function above will be called by the button that says
'start experiment'. It will look in the currently active
frame for a function called 'start_experiment_function'.
"""
self.frames[page_name].start_experiment_function()
[docs]
def access_subframes(self, page_name):
"""
The function above returns the instance of the currently
active (=in foreground) window
"""
return (self.frames[page_name])
class CommonFunctions():
def __init__(self, camera_class, controller, private=False):
self.controller = controller
self.camera_class = camera_class
self.cam = self.camera_class.cam
def update_framerate_func(self, new_framerate):
if RASPBERRY:
# The camera can only support a given framerate at a given resolution. See full docs here:
# https://picamera.readthedocs.io/en/release-1.13/fov.html
# I'm pretty sure that the Camera (F) from Waveshare is v1 module as 5MP are indicated (which is pretty much
# 2592x1944
if self.controller.all_common_variables.resolution == '640x480':
if new_framerate <= 90 and new_framerate > 0:
self.cam.framerate = new_framerate
else:
tk.messagebox.showerror("Error",
"'With resolution of 640x480 the \nframerate must be between 1 and 90")
print('With resolution of 640x480 the framerate must be between 1 and 90')
elif self.controller.all_common_variables.resolution == '1024x768':
if new_framerate <= 42 and new_framerate > 0:
self.cam.framerate = new_framerate
else:
tk.messagebox.showerror("Error",
"'With resolution of 1024x768 the \nframerate must be between 1 and 42")
print('With resolution of 1024x768 the framerate must be between 1 and 42')
elif self.controller.all_common_variables.resolution == '1296x972':
if new_framerate <= 42 and new_framerate > 0:
self.cam.framerate = new_framerate
else:
tk.messagebox.showerror("Error",
"'With resolution of 1296x972 the \nframerate must be between 1 and 42")
print('With resolution of 1296x972 the framerate must be between 1 and 42')
elif self.controller.all_common_variables.resolution == '1920x1080':
if new_framerate <= 30 and new_framerate > 0:
self.cam.framerate = new_framerate
else:
tk.messagebox.showerror("Error",
"'With resolution of 1920x1080 the \nframerate must be between 1 and 15")
print('With resolution of 1920x1080 the framerate must be between 1 and 30')
elif self.controller.all_common_variables.resolution == '2592x1944':
if new_framerate <= 15 and new_framerate > 0:
self.cam.framerate = new_framerate
else:
tk.messagebox.showerror("Error",
"'With resolution of 2592x1944 the \nframerate must be between 1 and 15")
print('With resolution of 2592x1944 the framerate must be between 1 and 15')
else:
tk.messagebox.showwarning("Information",
"With resolution of 640x480 \nthe framerate must be between 1 and 90")
print('you have pressed the update framerate button')
def menu_callback_shared(self, cam, backlight,
analog_output_one): # , backlight, analog_output_one):#, backlight,analog_output_one):
self.controller.after(700, lambda: self.menu_callback_shared(cam, backlight, analog_output_one))
if cam:
if RASPBERRY:
if self.controller.all_common_variables.resolution != self.controller.all_common_variables.resolution_variable.get():
if self.controller.all_common_variables.resolution_variable.get() == '640x480':
self.controller.all_common_variables.resolution = '640x480'
self.cam.resolution = (640, 480)
self.controller.all_common_variables.pixel_per_mm_var.set(0)
print('Changed resolution to 640x480')
if self.controller.all_common_variables.resolution_variable.get() == '1024x768':
self.controller.all_common_variables.resolution = '1024x768'
self.cam.resolution = (1024, 768)
self.controller.all_common_variables.pixel_per_mm_var.set(0)
print('Changed resolution to 1024x768')
if self.controller.all_common_variables.resolution_variable.get() == '1296x972':
self.controller.all_common_variables.resolution = '1296x972'
self.cam.resolution = (1296, 972)
self.controller.all_common_variables.pixel_per_mm_var.set(0)
print('Changed resolution to 1296x730')
if self.controller.all_common_variables.resolution_variable.get() == '1920x1080':
self.controller.all_common_variables.resolution = '1920x1080'
self.cam.resolution = (1920, 1080)
self.controller.all_common_variables.pixel_per_mm_var.set(0)
print('Changed resolution to 1920x1080')
# if self.controller.all_common_variables.resolution_variable.get() == '2592x1944':
# self.controller.all_common_variables.resolution = '2592x1944'
# self.cam.resolution = (2592, 1944)
# self.controller.all_common_variables.pixel_per_mm_var.set(0)
# print('Changed resolution to 2592x1944')
# In case the arena has been defined it needs to be
# removed as different resolutions require different arenas!
if self.controller.all_common_variables.overlay is not None:
# self.controller.all_common_functions.preview_overlay_func()
try:
self.controller.all_common_variables.cam.remove_overlay(
self.controller.all_common_variables.overlay)
except Exception as ex:
print('Tried removing overlay. This happened:')
print(ex)
# then set the overlay_image...
self.controller.all_common_variables.overlay_image = None
# ... and the overlay variable back to None
self.controller.all_common_variables.overlay = None
self.controller.all_common_variables.vr_arena = None
self.controller.all_common_variables.vr_arena_name = None
# always remove the animal that was placed before
try:
self.controller.all_common_variables.animal_annotation.remove()
except (AttributeError, ValueError):
pass
# Finally, remove the arena in the GUI
if self.controller.all_common_variables.resolution == '640x480':
width, height = 640, 480
elif self.controller.all_common_variables.resolution == '1024x768':
width, height = 1024, 768
elif self.controller.all_common_variables.resolution == '1296x972':
width, height = 1296, 972
self.controller.access_subframes(
self.controller.current_window).sub_frames.image_of_arena.set_data(
np.zeros((height, width)))
self.controller.access_subframes(
self.controller.current_window).sub_frames.ax_vr_arena.figure.canvas.draw()
# Check whether the user has used the scale to change preview window size by comparing the variable that
# the preview window size is currently set to, to the value that the variable that is bound to the scale
if self.controller.all_common_variables.preview_window_size_value != \
self.controller.all_common_variables.preview_window_size_variable.get():
# If the current preview window size value is different to the variable that is bound to the scale first
# change the preview window size value to the value defined by the variable
self.controller.all_common_variables.preview_window_size_value = \
self.controller.all_common_variables.preview_window_size_variable.get()
# To be a bit more concise, define the size variable
size = self.controller.all_common_variables.preview_window_size_value
# then change the preview window size to the requested size
self.cam.preview_window = (0, 0,
int(size),
int(size)
)
# After changing the preview window size, the script checks if a VR overlay is present. If yes
# it's size needs to be adjusted as well!
if self.controller.all_common_variables.overlay is not None:
self.controller.all_common_variables.overlay.window = \
(0, 0,
int(size),
int(size)
)
if backlight:
if RASPBERRY:
###
# BACKGROUND #1
###
# Check whether the user has used the scale to change backlight intensity by comparing the variable that
# the background intensity is currently set to, to the value that the variable that is bound to the scale
if self.controller.all_common_variables.backlight_intensity_value != \
self.controller.all_common_variables.backlight_intensity_variable.get():
# If the current backlight intensity value is different to the variable that is bound to the scale
self.controller.all_common_variables.backlight_intensity_value = \
self.controller.all_common_variables.backlight_intensity_variable.get()
'''
#### New in v1.8.0 - removed option to use hardware PWM
_ = LED_functions.update_PWM_dutycycle(pwm_object=self.controller.all_common_variables.pwm_object,
channel=self.controller.all_common_variables.background,
current_stim=self.controller.all_common_variables.backlight_intensity_value,
high_power_LED_bool=False,
HP_LED_lookup_table=self.controller.all_common_variables.HP_LED_lookup_table,
pwm_range=self.controller.all_common_variables.pwm_range,
just_calculate=False,
legacy_PWM_control_bool=False,
background=True
)
###
'''
# set the PWM dutycycle to value chosen by user.
# Manual: Starts hardware PWM on a GPIO at the specified frequency and dutycycle.
# http://abyz.me.uk/rpi/pigpio/python.html#hardware_PWM
for i in range(len(self.controller.all_common_variables.background)):
"""print("PiVR_SETTINGS['max_pwm_freq'] " + repr(PiVR_SETTINGS['max_pwm_freq']))
print("self.controller.all_common_variables.background[i][1] " + repr(
self.controller.all_common_variables.background[i][1]))
print(
"int(round(self.controller.all_common_variables.backlight_intensity_value*(1000000/100)))" + repr(
int(round(self.controller.all_common_variables.backlight_intensity_value * (
1000000 / 100)))))
if self.controller.all_common_variables.background[i][1] > PiVR_SETTINGS['max_pwm_freq']:
# if we use hardware_PWM, the pwm_range is irrelevant
self.controller.all_common_variables.pwm_object.hardware_PWM(
self.controller.all_common_variables.background[i][0],
self.controller.all_common_variables.background[i][1],
int(round(self.controller.all_common_variables.backlight_intensity_value*(1000000/100)))
) # This is a bit convoluted
print("HARDWARE PWM int(round(self.controller.all_common_variables.backlight_intensity_value*(1000000/100)))" +
repr(int(round(self.controller.all_common_variables.backlight_intensity_value*(1000000/100)))
))
# but hopefully easier for the end user: the scale the user sees goes from 0 to 100.
# However, internally, 1 Mio is 100% light intensity, hence we need to scale accordingly.
else:"""
# If we use set_PWM_dutycycle, the pwm_range variable is relevant.
self.controller.all_common_variables.pwm_object.set_PWM_dutycycle(
self.controller.all_common_variables.background[i][0],
int(round(self.controller.all_common_variables.backlight_intensity_value *
(self.controller.all_common_variables.pwm_range / 100))))
###
# BACKGROUND #2
###
# Check whether the user has used the scale to change backlight intensity by comparing the variable that
# the background intensity is currently set to, to the value that the variable that is bound to the scale
if self.controller.all_common_variables.backlight_two_intensity_value != \
self.controller.all_common_variables.backlight_two_intensity_variable.get():
# If the current backlight intensity value is different to the variable that is bound to the scale
self.controller.all_common_variables.backlight_two_intensity_value = \
self.controller.all_common_variables.backlight_two_intensity_variable.get()
'''
#### New in v1.8.0 - removed option to use hardware PWM
_ = LED_functions.update_PWM_dutycycle(pwm_object=self.controller.all_common_variables.pwm_object,
channel=self.controller.all_common_variables.background_two,
current_stim=self.controller.all_common_variables.backlight_two_intensity_value,
high_power_LED_bool=False,
HP_LED_lookup_table=self.controller.all_common_variables.HP_LED_lookup_table,
pwm_range=self.controller.all_common_variables.pwm_range,
just_calculate=False,
legacy_PWM_control_bool=False,
background=True
)
###
'''
# set the PWM dutycycle to value chosen by user.
# Manual: Starts hardware PWM on a GPIO at the specified frequency and dutycycle.
# http://abyz.me.uk/rpi/pigpio/python.html#hardware_PWM
for i in range(len(self.controller.all_common_variables.background_two)):
'''
if self.controller.all_common_variables.background_two[i][1] > PiVR_SETTINGS['max_pwm_freq']:
self.controller.all_common_variables.pwm_object.hardware_PWM(
self.controller.all_common_variables.background_two[i][0],
self.controller.all_common_variables.background_two[i][1],
int(round(self.controller.all_common_variables.backlight_two_intensity_value*(1000000/100)))
) # This is a bit convoluted
# but hopefully easier for the end user: the scale the user sees goes from 0 to 100.
# However, internally, 1 Mio is 100% light intensity, hence we need to scale accordingly.
else:'''
self.controller.all_common_variables.pwm_object.set_PWM_dutycycle(
self.controller.all_common_variables.background_two[i][0],
int(round(self.controller.all_common_variables.backlight_two_intensity_value *
(self.controller.all_common_variables.pwm_range / 100))))
print('backlight 2 intensity value updated to: ' + repr
(self.controller.all_common_variables.backlight_two_intensity_value))
if analog_output_one:
# v1.8.0 - change this to call LED_functions!
if RASPBERRY:
#####
# CH1
#####
# Check whether the user has used the scale to change analog output intensity by comparing the variable that
# the analog output intensity is currently set to, to the value that the variable that is bound to the scale
if self.controller.all_common_variables.channel_one_dutycycle != \
self.controller.all_common_variables.channel_one_variable.get():
# If the current analog output intensity value is different to the variable that is bound to the scale
self.controller.all_common_variables.channel_one_dutycycle = \
self.controller.all_common_variables.channel_one_variable.get()
_ = LED_functions.update_PWM_dutycycle(pwm_object=self.controller.all_common_variables.pwm_object,
channel=self.controller.all_common_variables.channel_one,
current_stim=self.controller.all_common_variables.channel_one_variable.get(),
high_power_LED_bool=self.controller.all_common_variables.high_power_LEDs_bool.get(),
HP_LED_lookup_table=self.controller.all_common_variables.HP_LED_lookup_table,
pwm_range=self.controller.all_common_variables.pwm_range,
just_calculate=False,
legacy_PWM_control_bool=self.controller.all_common_variables.legacy_PWM_control_bool
)
'''
# Todo - delete after testing
for i in range(len(self.controller.all_common_variables.channel_one)):
if self.controller.all_common_variables.channel_one[i][1] > PiVR_SETTINGS['max_pwm_freq']:
self.controller.all_common_variables.pwm_object.hardware_PWM(
gpio=self.controller.all_common_variables.channel_one[i][0],
PWMfreq=self.controller.all_common_variables.channel_one[i][1],
PWMduty=int(round(self.controller.all_common_variables.channel_one_dutycycle *
(1000000 / self.controller.all_common_variables.pwm_range))))
# NOTE: Not sure if this is the best way as it might mask problems for users!
else:
if self.controller.all_common_variables.high_power_LEDs_bool.get():
self.controller.all_common_variables.pwm_object.set_PWM_dutycycle(
user_gpio=self.controller.all_common_variables.channel_one[i][0],
dutycycle=int(round(
(self.controller.all_common_variables.pwm_range -
(self.controller.all_common_variables.channel_one_dutycycle*(
self.controller.all_common_variables.pwm_range / 100)))))) # The assumption here is
# that user input is provided from 0 (off) to 100 (fully on)
else:
self.controller.all_common_variables.pwm_object.set_PWM_dutycycle(
user_gpio=self.controller.all_common_variables.channel_one[i][0],
dutycycle=int(round(self.controller.all_common_variables.channel_one_dutycycle * (
self.controller.all_common_variables.pwm_range / 100)))) # The assumption here is
# that user input is provided from 0 (off) to 100 (fully on)'''
#####
# CH2
#####
if self.controller.all_common_variables.channel_two_dutycycle != \
self.controller.all_common_variables.channel_two_variable.get():
# If the current analog output intensity value is different to the variable that is bound to the scale
self.controller.all_common_variables.channel_two_dutycycle = \
self.controller.all_common_variables.channel_two_variable.get()
_ = LED_functions.update_PWM_dutycycle(pwm_object=self.controller.all_common_variables.pwm_object,
channel=self.controller.all_common_variables.channel_two,
current_stim=self.controller.all_common_variables.channel_two_variable.get(),
high_power_LED_bool=self.controller.all_common_variables.high_power_LEDs_bool.get(),
HP_LED_lookup_table=self.controller.all_common_variables.HP_LED_lookup_table,
pwm_range=self.controller.all_common_variables.pwm_range,
just_calculate=False,
legacy_PWM_control_bool=self.controller.all_common_variables.legacy_PWM_control_bool
)
'''
# Todo - delete after testing
for i in range(len(self.controller.all_common_variables.channel_two)):
if self.controller.all_common_variables.channel_two[i][1] > PiVR_SETTINGS['max_pwm_freq']:
self.controller.all_common_variables.pwm_object.hardware_PWM(
gpio=self.controller.all_common_variables.channel_two[i][0],
PWMfreq=self.controller.all_common_variables.channel_two[i][1],
PWMduty=int(round(self.controller.all_common_variables.channel_two_dutycycle *
(1000000 / self.controller.all_common_variables.pwm_range))))
# NOTE: Not sure if this is the best way as it might mask problems for users!
else:
if self.controller.all_common_variables.high_power_LEDs_bool.get():
self.controller.all_common_variables.pwm_object.set_PWM_dutycycle(
user_gpio=self.controller.all_common_variables.channel_two[i][0],
dutycycle=int(round(
(self.controller.all_common_variables.pwm_range -
(self.controller.all_common_variables.channel_two_dutycycle * (
self.controller.all_common_variables.pwm_range / 100)))))) # The assumption here is
else:
self.controller.all_common_variables.pwm_object.set_PWM_dutycycle(
user_gpio=self.controller.all_common_variables.channel_two[i][0],
dutycycle=int(round(self.controller.all_common_variables.channel_two_dutycycle * (
self.controller.all_common_variables.pwm_range / 100)))) # The assumption here is
# that user input is provided from 0 (off) to 100 (fully on)'''
#####
# CH3
#####
if self.controller.all_common_variables.channel_three_dutycycle != \
self.controller.all_common_variables.channel_three_variable.get():
# If the current analog output intensity value is different to the variable that is bound to the scale
self.controller.all_common_variables.channel_three_dutycycle = \
self.controller.all_common_variables.channel_three_variable.get()
_ = LED_functions.update_PWM_dutycycle(pwm_object=self.controller.all_common_variables.pwm_object,
channel=self.controller.all_common_variables.channel_three,
current_stim=self.controller.all_common_variables.channel_three_variable.get(),
high_power_LED_bool=self.controller.all_common_variables.high_power_LEDs_bool.get(),
HP_LED_lookup_table=self.controller.all_common_variables.HP_LED_lookup_table,
pwm_range=self.controller.all_common_variables.pwm_range,
just_calculate=False,
legacy_PWM_control_bool=self.controller.all_common_variables.legacy_PWM_control_bool
)
'''
# Todo - delete after testing
for i in range(len(self.controller.all_common_variables.channel_three)):
if self.controller.all_common_variables.channel_three[i][1] > PiVR_SETTINGS['max_pwm_freq']:
self.controller.all_common_variables.pwm_object.hardware_PWM(
gpio=self.controller.all_common_variables.channel_three[i][0],
PWMfreq=self.controller.all_common_variables.channel_three[i][1],
PWMduty=int(round(self.controller.all_common_variables.channel_three_dutycycle *
(1000000 / self.controller.all_common_variables.pwm_range))))
# NOTE: Not sure if this is the best way as it might mask problems for users!
# NOTE: Not sure if this is the best way as it might mask problems for users!
else:
if self.controller.all_common_variables.high_power_LEDs_bool.get():
self.controller.all_common_variables.pwm_object.set_PWM_dutycycle(
user_gpio=self.controller.all_common_variables.channel_three[i][0],
dutycycle=int(round(
(self.controller.all_common_variables.pwm_range -
(self.controller.all_common_variables.channel_three_dutycycle * (
self.controller.all_common_variables.pwm_range / 100)))))) # The assumption here is
# that user input is provided from 0 (off) to 100 (fully on)
else:
self.controller.all_common_variables.pwm_object.set_PWM_dutycycle(
user_gpio=self.controller.all_common_variables.channel_three[i][0],
dutycycle=int(round(self.controller.all_common_variables.channel_three_dutycycle * (
self.controller.all_common_variables.pwm_range / 100)))) # The assumption here is
# that user input is provided from 0 (off) to 100 (fully on)'''
#####
# CH4
#####
if self.controller.all_common_variables.channel_four_dutycycle != \
self.controller.all_common_variables.channel_four_variable.get():
# If the current analog output intensity value is different to the variable that is bound to the scale
self.controller.all_common_variables.channel_four_dutycycle = \
self.controller.all_common_variables.channel_four_variable.get()
_ = LED_functions.update_PWM_dutycycle(pwm_object=self.controller.all_common_variables.pwm_object,
channel=self.controller.all_common_variables.channel_four,
current_stim=self.controller.all_common_variables.channel_four_variable.get(),
high_power_LED_bool=self.controller.all_common_variables.high_power_LEDs_bool.get(),
HP_LED_lookup_table=self.controller.all_common_variables.HP_LED_lookup_table,
pwm_range=self.controller.all_common_variables.pwm_range,
just_calculate=False,
legacy_PWM_control_bool=self.controller.all_common_variables.legacy_PWM_control_bool
)
'''
# Todo - delete after testing
for i in range(len(self.controller.all_common_variables.channel_four)):
if self.controller.all_common_variables.channel_four[i][1] > PiVR_SETTINGS['max_pwm_freq']:
self.controller.all_common_variables.pwm_object.hardware_PWM(
gpio=self.controller.all_common_variables.channel_four[i][0],
PWMfreq=self.controller.all_common_variables.channel_four[i][1],
PWMduty=int(round(self.controller.all_common_variables.channel_four_dutycycle *
(1000000 / self.controller.all_common_variables.pwm_range))))
# NOTE: Not sure if this is the best way as it might mask problems for users!
else:
if self.controller.all_common_variables.high_power_LEDs_bool.get():
self.controller.all_common_variables.pwm_object.set_PWM_dutycycle(
user_gpio=self.controller.all_common_variables.channel_four[i][0],
dutycycle=int(round(
(self.controller.all_common_variables.pwm_range -
(self.controller.all_common_variables.channel_four_dutycycle * (
self.controller.all_common_variables.pwm_range / 100)))))) # The assumption here is
# that user input is provided from 0 (off) to 100 (fully on)
else:
self.controller.all_common_variables.pwm_object.set_PWM_dutycycle(
user_gpio=self.controller.all_common_variables.channel_four[i][0],
dutycycle=int(round(self.controller.all_common_variables.channel_four_dutycycle * (
self.controller.all_common_variables.pwm_range / 100)))) # The assumption here is
# that user input is provided from 0 (off) to 100 (fully on)'''
if self.controller.all_common_variables.model_organism_value != \
self.controller.all_common_variables.model_organism_variable.get():
self.controller.all_common_variables.model_organism_value = \
self.controller.all_common_variables.model_organism_variable.get()
print('Model organism: ' + repr(self.controller.all_common_variables.model_organism_value) + ' selected!')
# Update available diskspace
if RASPBERRY:
free_diskpace_mb = system_utils.free_disk_space_MB(self.controller.all_common_variables.path_entry_variable.get())
else:
free_diskpace_mb = 500 # For testing purposes
if free_diskpace_mb < 1000:
# Print in Mb
self.controller.all_common_variables.freespace_text_variable.set(repr(free_diskpace_mb) + 'Mb')
# Mark text in red to make experimenter aware of potentially low space
self.controller.all_common_variables.free_space_label.config(fg='red')
else:
free_diskpace_gb = round(free_diskpace_mb/1024,1)
if free_diskpace_gb < 1000:
self.controller.all_common_variables.freespace_text_variable.set(repr(free_diskpace_gb) + 'Gb')
self.controller.all_common_variables.free_space_label.config(fg='black')
else:
free_diskpace_tb = round(free_diskpace_gb/1024,1)
if free_diskpace_tb < 1000:
self.controller.all_common_variables.freespace_text_variable.set(repr(free_diskpace_tb) + 'Tb')
else:
self.controller.all_common_variables.freespace_text_variable.set('wow')
self.controller.all_common_variables.free_space_label.config(fg='black')
def cam_on_func(self):
print('cam on')
# todo - delete if in final version
if RASPBERRY:
self.controller.all_common_variables.preview_bool = True
size = int(self.controller.all_common_variables.preview_window_size_value)
self.cam.resolution = self.controller.all_common_variables.resolution
self.cam.preview_window = (0, 0, size, size)
self.cam.zoom = (0, 0, 1, 1)
self.cam.start_preview()
self.cam.preview.fullscreen = False
# if overlay was previously defined, re-create it
if self.controller.all_common_variables.overlay is not None:
self.controller.all_common_functions.preview_overlay_func()
# self.controller.all_common_variables.overlay.layer = 3
# wait a second so the camera adjusts before the user has
# the chance to change anything
# TODO check if necessary!
time.sleep(1)
def cam_off_func(self):
print("cam off")
# todo - delete in final version
if RASPBERRY:
self.controller.all_common_variables.preview_bool = False
self.cam.stop_preview()
# if overlay is active remove it.
if self.controller.all_common_variables.overlay is not None:
try:
self.controller.all_common_variables.cam.remove_overlay(
self.controller.all_common_variables.overlay)
except Exception as ex:
# This happens when the user presses the "cam
# off" button more than once...
pass
def distance_configuration_func(self):
if RASPBERRY:
if self.controller.all_common_variables.resolution == '640x480':
resolution = [640, 480]
if self.controller.all_common_variables.resolution == '1024x768':
resolution = [1024, 768]
if self.controller.all_common_variables.resolution == '1296x972':
resolution = [1296, 972]
if self.controller.all_common_variables.resolution == '1920x1080':
resolution = [1920, 1080]
if self.controller.all_common_variables.resolution == '2592x1944':
resolution = [2592, 1944]
# make sure to have the undistort files ready if needed
if self.controller.all_common_variables.online_undistort_bool:
self.controller.all_common_functions.grab_undistort_files()
distance_factor = \
px_per_mm_configuration.DistanceConfigurationLive(cam=self.cam,
resolution=resolution,
known_distance=self.controller.all_common_variables.known_distance,
undistort_bool =self.controller.all_common_variables.online_undistort_bool.get(),
undistort_dst = self.controller.all_common_variables.undistort_dst_file,
undistort_mtx = self.controller.all_common_variables.undistort_mtx_file,
undistort_newcameramtx = self.controller.all_common_variables.newcameramtx
)
else:
# This is for developmental purposes only, user will never see this!
distance_factor = px_per_mm_configuration.DistanceConfigurationStatic(
known_distance=self.controller.all_common_variables.known_distance,
undistort_bool=self.controller.all_common_variables.online_undistort_bool.get(),
undistort_dst=self.controller.all_common_variables.undistort_dst_file,
undistort_mtx=self.controller.all_common_variables.undistort_mtx_file,
undistort_newcameramtx=self.controller.all_common_variables.newcameramtx
)
# This variable is the number that is used internally
self.controller.all_common_variables.pixel_per_mm_var.set(round(distance_factor.distance_factor ,3))
# Check what exactly this variable is doing and write in comments
self.controller.all_common_variables.known_distance = distance_factor.known_distance
self.controller.all_common_variables.display_pixel_per_mm_variable.set(
repr(distance_factor.distance_factor)[0:10])
print('There are ' + repr(self.controller.all_common_variables.pixel_per_mm_var.get()) + ' pixel per mm ')
try:
# todo - probably don't need that!
rounded_value = str(round(self.controller.all_common_variables.pixel_per_mm_var.get() ,1))
self.controller.access_subframes(
self.controller.current_window).sub_frames.pixel_per_mm_label.configure(
text='Pixel/mm: ' + rounded_value)
# todo - check if this is the best way to update the pixel_per_mm in the fix_metadata without breaking
# Todo - online pixel_per_mm
except:
pass
def select_folder_to_save_exp_func(self):
self.controller.all_common_variables.path_entry_variable.set(
filedialog.askdirectory(initialdir=self.controller.all_common_variables.path_entry_variable.get()))
def select_images(self, display_settings=False):
self.controller.all_common_variables.data_path = filedialog.askdirectory()
self.controller.all_common_variables.image_path.set(self.controller.all_common_variables.data_path)
# See if commenting this breaks everything...
if display_settings:
self.controller.all_common_variables.path = self.controller.all_common_variables.data_path
path_to_show = '...' + self.controller.all_common_variables.data_path[-30:]
self.controller.access_subframes(
self.controller.current_window).sub_frames.pathchosen.configure(text=path_to_show)
try:
with open(pathlib.Path(self.controller.all_common_variables.data_path, 'experiment_settings.json'), 'r') as file:
self.controller.all_common_variables.experimental_metadata = json.load(file)
# Comment on why I use try..except instead of if..else. It seems that in Python it is prefered to use
# the try..except style if one does not expect too many excepts to come true.
try:
self.controller.access_subframes(
self.controller.current_window).sub_frames.experiment_date_n_time_label.configure(
text=self.controller.all_common_variables.experimental_metadata['Experiment Date and Time'])
except KeyError:
self.controller.access_subframes(
self.controller.current_window).sub_frames.experiment_date_n_time_label.configure(
text='No Data')
try:
self.controller.access_subframes(
self.controller.current_window).sub_frames.framerate_label.configure(
text=repr(self.controller.all_common_variables.experimental_metadata['Framerate']) + 'fps')
self.controller.all_common_variables.framerate_read_from_experiment_settings = \
self.controller.all_common_variables.experimental_metadata['Framerate']
except KeyError:
self.controller.access_subframes(
self.controller.current_window).sub_frames.framerate_labelconfigure(text='No Data')
self.controller.all_common_variables.framerate_read_from_experiment_settings = None
try:
self.controller.access_subframes(
self.controller.current_window).sub_frames.recording_time_label.configure(
text=repr(
self.controller.all_common_variables.experimental_metadata['Recording time']) + 's')
except KeyError:
self.controller.access_subframes(
self.controller.current_window).sub_frames.recording_time_label.configure(tex='No Data')
try:
self.controller.access_subframes(
self.controller.current_window).sub_frames.resolution_label.configure(
text=self.controller.all_common_variables.experimental_metadata['Resolution'])
self.controller.all_common_variables.resolution_read_from_experiment_settings = \
self.controller.all_common_variables.experimental_metadata['Resolution']
except KeyError:
self.controller.access_subframes(
self.controller.current_window).sub_frames.resolution_label.configure(text='No Data')
self.controller.all_common_variables.resolution_read_from_experiment_settings = None
try:
self.controller.access_subframes(
self.controller.current_window).sub_frames.species_label.configure(
text=self.controller.all_common_variables.experimental_metadata['Model Organism'])
self.controller.all_common_variables.organism_read_from_experiment_settings = \
self.controller.all_common_variables.experimental_metadata['Model Organism']
except KeyError:
self.controller.access_subframes(
self.controller.current_window).sub_frames.species_label.configure(text='No Data')
self.controller.all_common_variables.organism_read_from_experiment_settings = None
# changed name of 'genotype' to 'exp. group' to be more generic. Reflect that change here and
# make sure not to break backwards compatibility
if 'Exp. group' in self.controller.all_common_variables.experimental_metadata:
self.controller.access_subframes(
self.controller.current_window).sub_frames.genotype_label.configure(
text=self.controller.all_common_variables.experimental_metadata['Exp. group'])
self.controller.all_common_variables.genotype_read_from_experiment_settings = \
self.controller.all_common_variables.experimental_metadata['Exp. group']
elif 'Genotype' in self.controller.all_common_variables.experimental_metadata:
self.controller.access_subframes(
self.controller.current_window).sub_frames.genotype_label.configure(
text=self.controller.all_common_variables.experimental_metadata['Genotype'])
self.controller.all_common_variables.genotype_read_from_experiment_settings = \
self.controller.all_common_variables.experimental_metadata['Genotype']
# try:
# self.controller.access_subframes(
# self.controller.current_window).sub_frames.genotype_label.configure(
# text=self.controller.all_common_variables.experimental_metadata['Genotype'])
# self.controller.all_common_variables.genotype_read_from_experiment_settings = \
# self.controller.all_common_variables.experimental_metadata['Genotype']
# except KeyError:
# self.controller.access_subframes(
# self.controller.current_window).sub_frames.genotype_label.configure(text='No Data')
# self.controller.all_common_variables.genotype_read_from_experiment_settings = None
try:
self.controller.access_subframes(
self.controller.current_window).sub_frames.pixel_per_mm_label.configure(
text=round(self.controller.all_common_variables.experimental_metadata['Pixel per mm'] ,2))
self.controller.all_common_variables.pixel_per_mm_read_from_experiment_settings = \
round(self.controller.all_common_variables.experimental_metadata['Pixel per mm'] ,2)
except KeyError:
self.controller.access_subframes(
self.controller.current_window).sub_frames.pixel_per_mm_label.configure(text='No Data')
self.controller.all_common_variables.pixel_per_mm_read_from_experiment_settings = None
except IOError:
self.controller.access_subframes(
self.controller.current_window).sub_frames.experiment_date_n_time_label.configure(text='No Data')
self.controller.access_subframes(
self.controller.current_window).sub_frames.framerate_label.configure(text='No Data')
self.controller.access_subframes(
self.controller.current_window).sub_frames.recording_time_label.configure(text='No Data')
self.controller.access_subframes(
self.controller.current_window).sub_frames.resolution_label.configure(text='No Data')
self.controller.access_subframes(
self.controller.current_window).sub_frames.species_label.configure(text='No Data')
self.controller.access_subframes(
self.controller.current_window).sub_frames.genotype_label.configure(text='No Data')
self.controller.access_subframes(
self.controller.current_window).sub_frames.pixel_per_mm_label.configure(text='No Data')
try:
self.timestamps = np.load('timestamps.npy')
self.controller.access_subframes(
self.controller.current_window).sub_frames.timestamps_label.configure(text='Found')
except IOError:
self.controller.access_subframes(
self.controller.current_window).sub_frames.timestamps_label.configure(text='No Data')
def common_str_update_func(self):
self.controller.all_common_variables.entered_text = self.controller.access_subframes(
self.controller.current_window).sub_frames.common_str.get()
files_and_folders_common = [p.replace('\\', '') for p in
glob('*' + self.controller.all_common_variables.entered_text + '*')]
self.controller.access_subframes(self.controller.current_window).sub_frames.common_str_counted.configure(
text=len(files_and_folders_common))
'''
# DONT DELETE YET
# function is called when user wants to explicitly convert jpgs to a npy array
def convert_single_images_func(self):
if self.controller.all_common_variables.path is None:
tk.messagebox.showerror("Error", "You have to select a\n"
"path in order to convert images")
elif self.controller.all_common_variables.entered_text is None:
user_said_its_ok = tk.messagebox.askokcancel("Warning", "Do you only have images in the folder?\n"
"If yes, press 'OK', otherwise 'Cancel'\n"
"and enter a string for the extension of\n"
"of the images!")
if self.controller.all_common_variables.path is not None and \
(self.controller.all_common_variables.entered_text is not None or user_said_its_ok):
# Figure out how many files must be read
files_to_read = [p.replace('\\', '') for p in glob('*' + self.controller.access_subframes(
self.controller.current_window).sub_frames.common_str.get("1.0", 'end-1c') + '*')]
sorted_filed_to_read = natsorted(files_to_read)
temp = imread(files_to_read[0])
height, width = temp.shape[0], temp.shape[1]
"""
# Todo!! Write a correction algorithm for missing frames (especially if very fast recording!)
if self.timestamps is not None:
# first calculate the time it took to take each frame
# Note: The timestamps are not the true time the picture was taken - after the picture is taken the
# time is written down before the next frame is taken. So the timestamps are not 'true' but give us a good
# hint when the frame was captured
timestamps_subtracted = self.timestamps[1:,0] - np.roll(self.timestamps[:,0],1)[1:]
if timestamps_subtracted.any() < (1/self.framerate)*2:
"""
if self.controller.all_common_variables.convert_image_output_variable.get() == '.npy':
"""
saving as mmep as the advantage that one can build arrays that are larger than the avaiable memory
The drawback is that how it's currently implemented it will take much more SD space than a normal
numpy array
"""
array = np.zeros((height, width, len(files_to_read)), dtype=temp.dtype)
# with open("All_images.npy", 'w') as image_file:
# mmapData = np.memmap("all_images.npy", mode='w+', shape=(height, width, len(files_to_read)),
# dtype=temp.dtype)
for i in range(len(sorted_filed_to_read)):
# print(i)
temp = imread(sorted_filed_to_read[i])
try:
array[:, :, i] = temp[:, :, 0]
# mmapData[:, :, i] = temp[:, :, 0]
except IndexError:
# in case the image is only grayscale
array[:, :, i] = temp
np.save('all_images.npy', array)
elif self.controller.all_common_variables.convert_image_output_variable.get() == '.avi':
"""
not trivial - still needs to be implemented!!
fig = Figure()
ax = fig.add_subplot(111)
ax.set_aspect('equal')
ax.get_xaxis().set_visible(False)
ax.get_yaxis().set_visible(False)
image = ax.imshow(temp, cmap='gray', interpolation='nearest')
#im.set_clim([0, 1])
#fig.set_size_inches([5, 5])
#tight_layout()
def update_img(i):
tmp = np.random(300, 300)
image.set_data(tmp)
#temp = imread(sorted_filed_to_read[i])
#image.set_data(temp)
return image
# legend(loc=0)
ani = animation.FuncAnimation(fig, update_img, 300, interval=30)
writer = animation.writers['ffmpeg'](fps=30)
ani.save('demo.mp4', writer=writer, dpi=100)
return ani
"""
print('Still needs to be implemented!')
'''
def quit_sequence(self, save=True, exit=True):
if RASPBERRY or system_utils.is_virtual_raspberry():
if save:
system_utils.save_GUI_variables(controller=self.controller,
PiVR_settings=PiVR_SETTINGS,
raspberry_or_virtual_raspberry=True,
cam=self.cam)
"""variables = {'Path': self.controller.all_common_variables.path_entry_variable.get(),
'Recording framerate': int
(self.controller.all_common_variables.framerate_entry_variable.get()),
'Resolution': self.controller.all_common_variables.resolution,
'Recording length': int
(self.controller.all_common_variables.recording_time_variable.get()),
'Background dutycycle': self.controller.all_common_variables.backlight_intensity_value,
'Background 2 dutycycle' : self.controller.all_common_variables.backlight_two_intensity_value,
'Exp. group' : self.controller.all_common_variables.genotype_entry_variable.get(),
# 'Observation mode': self.controller.all_common_variables.experiment_observation_mode_output,
'Window': self.controller.current_window,
'Organism': self.controller.all_common_variables.model_organism_value,
'Background': self.controller.all_common_variables.background,
'Background 2' : self.controller.all_common_variables.background_two,
'Channel 1': self.controller.all_common_variables.channel_one,
'Channel 2': self.controller.all_common_variables.channel_two,
'Channel 3': self.controller.all_common_variables.channel_three,
'Channel 4': self.controller.all_common_variables.channel_four,
'Channel 1 dutycycle': self.controller.all_common_variables.channel_one_dutycycle,
'Channel 2 dutycycle': self.controller.all_common_variables.channel_two_dutycycle,
'Channel 3 dutycycle': self.controller.all_common_variables.channel_three_dutycycle,
'Channel 4 dutycycle': self.controller.all_common_variables.channel_four_dutycycle,
'High Power LEDs': self.controller.all_common_variables.high_power_LEDs_bool.get(),
'Animal Detection Mode': self.controller.all_common_variables.animal_detection_method_var.get(),
'VR Body Part Stimulation': self.controller.all_common_variables.vr_stim_loc_var.get(),
'Animal Signal': self.controller.all_common_variables.signal,
'save centroids npy': self.controller.all_common_variables.save_centroids_npy.get(),
'save heads npy': self.controller.all_common_variables.save_heads_npy.get(),
'save tails npy': self.controller.all_common_variables.save_tails_npy.get(),
'save midpoints npy': self.controller.all_common_variables.save_midpoints_npy.get(),
'save bbox npy': self.controller.all_common_variables.save_bbox_npy.get(),
'save stim npy': self.controller.all_common_variables.save_stim_npy.get(),
'save sm_thresh npy': self.controller.all_common_variables.save_thresh_npy.get(),
'save sm_skeletons npy': self.controller.all_common_variables.save_skel_npy.get(),
'online undistort bool ': self.controller.all_common_variables.online_undistort_bool.get(),
'undistort radiobutton var': self.controller.all_common_variables.undistort_radiobutton_var.get(),
'opencv_information_box': self.controller.all_common_variables.opencv_information_box
}
try:
if self.cam.exposure_mode == 'off':
variables['exposure time'] = self.cam.exposure_speed
else:
# if autoexposure is on set the shutter speed to zero which allows it to float
variables['exposure time'] = 0
except AttributeError:
# if not on Raspberry an Attribute Error indicates that the cam object which in that case is
# a string does not have exposure speed as a attribute
pass
if self.controller.all_common_variables.pixel_per_mm_var.get() != 0:
variables['distance factor'] = self.controller.all_common_variables.pixel_per_mm_var.get()
variables['known distance'] = self.controller.all_common_variables.known_distance
# This doesn't do anything I believe (24/8/9)
# try:
# variables['bbox size'] = int(self.boxsize.get("1.0", 'end-1c'))
# except:
# pass
with open((self.controller.path_of_program + '/PiVR_settings.json'), 'w') as file:
json.dump(variables, file, sort_keys=True, indent=4)"""
if not RASPBERRY and not system_utils.is_virtual_raspberry():
if save:
system_utils.save_GUI_variables(controller=self.controller,
PiVR_settings=PiVR_SETTINGS,
raspberry_or_virtual_raspberry=False)
if exit:
if RASPBERRY:
# Try turning off all GPIOs
for i in range(len(self.controller.all_common_variables.background)):
# If we use set_PWM_dutycycle, the pwm_range variable is relevant.
self.controller.all_common_variables.pwm_object.set_PWM_dutycycle(
self.controller.all_common_variables.background[i][0],
int(round(0 * (self.controller.all_common_variables.pwm_range / 100))))
for i in range(len(self.controller.all_common_variables.background_two)):
self.controller.all_common_variables.pwm_object.set_PWM_dutycycle(
self.controller.all_common_variables.background_two[i][0],
int(round(0 * (self.controller.all_common_variables.pwm_range / 100))))
LED_functions.turn_GPIO_fully_off(pwm_object=self.controller.all_common_variables.pwm_object,
output_channel_one=self.controller.all_common_variables.channel_one,
output_channel_two=self.controller.all_common_variables.channel_two,
output_channel_three=self.controller.all_common_variables.channel_three,
output_channel_four=self.controller.all_common_variables.channel_four,
high_power_LED_bool=self.controller.all_common_variables.high_power_LEDs_bool.get(),
pwm_range=self.controller.all_common_variables.pwm_range)
# quit causes mainloop to stop, but keeps interpreter intact.
PiVR_app.quit()
def update_software(self):
"""
This function calls the shell script "update_PiVR" located in the folder
"Installation_update".
This shell script will:
1) sudo apt-get update the Raspberry Pi
2) cd into the PiVR directory and git pull the newest version of the software
from gitlab
"""
if RASPBERRY:
if tk.messagebox.askokcancel('Update?',
'This will stop the PiVR software to update! You '
'will have to restart the software after the '
'update.\nDo you want to continue?'):
# Safe the current path temporarilly
current_path = os.path.abspath(os.getcwd())
# cd to the software path
os.chdir(PiVR_ROOT_PATH)
result = subprocess.run(
['/bin/bash', '-c',
'/'.join(os.path.realpath(__file__).split('/')[:-1])
+ '/Installation_update/update_PiVR.sh'])
if result.returncode == 0:
tk.messagebox.showinfo(
'Success!',
'Was able to connect to gitlab to download '
'newest version!\n'
'Please restart software for update to take effect!')
elif result.returncode == 1:
tk.messagebox.showinfo(
'Failed!',
'Unable to connect to https://gitlab.com'
'\nPlease make sure you are connected to the '
'Internet!')
else:
print(result.returncode)
# cd back to the previously used path.
os.chdir(current_path)
else:
if tk.messagebox.askokcancel(
'Update?',
'This will stop the PiVR software to update! You '
'will have to restart the software after the '
'update.\nDo you want to continue?'):
# Safe the current path temporarilly
current_path = os.path.abspath(os.getcwd())
# cd to the software path
os.chdir(PiVR_ROOT_PATH)
process = subprocess.Popen(["git", "pull"],
stdout=subprocess.PIPE)
output = process.communicate()
print(output[0])
tk.messagebox.showinfo('Update Information',
output[0])
tk.messagebox.showinfo(
'Reminder',
'If successfully downloaded newest version, please '
'remember to restart the software for updates to '
'take effect!')
# cd back to the previously used path.
os.chdir(current_path)
def software_info(self):
"""
Show version (and in future the license) of the software
"""
child_window_info = tk.Toplevel()
child_window_info.wm_title('About')
child_window_info.geometry("%dx%d%+d%+d" % (320, 420, 0, 0))
child_window_info.attributes("-topmost", True) # force on top
# disable main window
child_window_info.grab_set()
logo = Image.open( self.controller.path_of_program +
'/pics/PiVRLogo.ico').resize((200, 200))
logo_as_photo = ImageTk.PhotoImage(logo)
display_logo = tk.Label(child_window_info, image=logo_as_photo)
display_logo.image = logo_as_photo
display_logo.grid(row=1, column=0, columnspan=2)
title = tk.Label(child_window_info, text=__project__)
title.config(font=("Arial", 30, "bold"))
title.grid(row=0, column=0, columnspan=2)
some_description = tk.Label(child_window_info,
text='The software powering your '
'home built RPi tracker')
some_description.config(font=('Arial', 10 ,'italic'))
some_description.grid(row=2, column=0, columnspan=2)
author_label = tk.Label(child_window_info, text='Written by:')
author_label.grid(row=3, column=0)
author_read = tk.Label(child_window_info, text=__author__)
author_read.grid(row=3 ,column=1)
version_label = tk.Label(child_window_info, text='Version:')
version_label.grid(row=4, column=0)
version_read = tk.Label(child_window_info, text=__version__)
version_read.grid(row=4, column=1)
version_date = tk.Label(child_window_info, text='Release Date:')
version_date.grid(row=5, column =0)
version_data_read = tk.Label(child_window_info, text=__date__)
version_data_read.grid(row=5, column=1)
git_branch_label = tk.Label(child_window_info, text='Git branch:')
git_branch_label.grid(row=6, column=0)
git_branch = tk.Label(child_window_info, text=CURRENT_GIT_BRANCH)
git_branch.grid(row=6, column=1)
git_hash_label = tk.Label(child_window_info, text='Git hash:')
git_hash_label.grid(row=7, column=0)
git_hash = tk.Label(child_window_info, text = CURRENT_GIT_HASH[0:8])
git_hash.grid(row=7, column=1)
license_label = tk.Label(child_window_info, text='License:')
license_label.grid(row=8, column = 0)
license_BSD = tk.Label(child_window_info, text='BSD-3-Clause')
license_BSD.grid(row=8, column=1)
"""
def autoexp_callback_old(self):
'''
The callback menu for the autoexposure button.
When the user clicks on the autoexposure button this function will change both what is displayed in the GUI
as well as changing the exposure_mode of the camera.
NOTE: This has been retired 10th of October 2018 as the button moved from a the main frame to a popup window
:return:
'''
if self.controller.access_subframes(
self.controller.current_window).sub_frames.auto_exposure_button['text'] == 'autoexp. on':
self.controller.access_subframes(
self.controller.current_window).sub_frames.auto_exposure_button['text'] = 'autoexp. off'
# todo - delete if line below in final version
if RASPBERRY:
self.cam.exposure_mode = 'off'
print(self.cam.exposure_speed)
print('autoexposure: off')
else:
self.controller.access_subframes(
self.controller.current_window).sub_frames.auto_exposure_button['text'] = 'autoexp. on'
# todo - delete if line below in final version
if RASPBERRY:
self.cam.exposure_mode = 'auto'
print(self.cam.exposure_speed)
print('autoexposure: auto')
"""
def autoexp_callback(self):
"""
The callback menu for the autoexposure button.
When the user clicks on the autoexposure button this
function will change both what is displayed in the GUI
as well as changing the exposure_mode of the camera.
"""
if self.controller.all_common_variables.auto_exposure_button['text'] == 'autoexp. on':
self.controller.all_common_variables.auto_exposure_button['text'] = 'autoexp. off'
# todo - delete if line below in final version
if RASPBERRY:
self.cam.exposure_mode = 'off'
self.cam.shutter_speed = self.cam.exposure_speed
print(self.cam.exposure_speed)
print(self.cam.shutter_speed)
else:
self.controller.all_common_variables.auto_exposure_button['text'] = 'autoexp. on'
# todo - delete if line below in final version
if RASPBERRY:
self.cam.exposure_mode = 'auto'
self.cam.shutter_speed = 0
print(self.cam.exposure_speed)
print(self.cam.shutter_speed)
def select_signal(self):
"""
This function gets called when the user presses the 'Animal
Color' option in the menubar.
It will display an example of both dark and white signal and
lets the user choose which of two options are correct.
"""
animal_color_selection.AnimalColorSelection(
path=self.controller.path_of_program,
linux=LINUX,
controller=self.controller)
print(self.controller.all_common_variables.signal)
def output_files_func(self):
"""
Creates the "Options->Output File" menu
"""
# create a new window and name it
self.child = tk.Toplevel()
self.child.wm_title('Output files to be saved')
self.child.attributes("-topmost", True) # force on top
# disable main window
self.child.grab_set()
explanation_output_files = tk.Label(
self.child,
justify=tk.LEFT,
wraplength=300,
text='If you want to have the same output files as the '
'original version of PiVR you may select the relevant '
'files here.'
)
explanation_output_files.grid(row=0, column=0)
##################################################
# Labelframe Data.csv
data_csv_labelframe = tk.LabelFrame(
self.child,
text='Part of data.csv')
data_csv_labelframe.grid(row=1, column=0, sticky='W')
info_label = tk.Label(
data_csv_labelframe,
justify=tk.LEFT,
wraplength=300,
text='Note that no additional information is saved. '
'This is purely for your convenience.'
)
info_label.grid(row=0, column=0, columnspan=2)
# Centroids.npy
centroids_checkbutton = tk.Checkbutton(
data_csv_labelframe,
text='',
variable=self.controller.all_common_variables.save_centroids_npy,
onvalue=True,
offvalue=False)
centroids_checkbutton.grid(row=1, column=0)
centroids_label = tk.Label(
data_csv_labelframe,
text='Save "centroids.npy" file')
centroids_label.grid(row=1, column=1, sticky='W')
# Heads.npy
heads_checkbutton = tk.Checkbutton(
data_csv_labelframe,
text='',
variable=self.controller.all_common_variables.save_heads_npy,
onvalue=True,
offvalue=False)
heads_checkbutton.grid(row=2, column=0)
heads_label = tk.Label(
data_csv_labelframe,
text='Save "heads.npy" file')
heads_label.grid(row=2, column=1, sticky='W')
# Tails.npy
tails_checkbutton = tk.Checkbutton(
data_csv_labelframe,
text='',
variable=self.controller.all_common_variables.save_tails_npy,
onvalue=True,
offvalue=False)
tails_checkbutton.grid(row=3, column=0)
tails_label = tk.Label(
data_csv_labelframe,
text='Save "tails.npy" file')
tails_label.grid(row=3, column=1, sticky='W')
# Midpoint.npy
midpoint_checkbutton = tk.Checkbutton(
data_csv_labelframe,
text='',
variable=self.controller.all_common_variables.save_midpoints_npy,
onvalue=True,
offvalue=False)
midpoint_checkbutton.grid(row=4, column=0)
midpoint_label = tk.Label(
data_csv_labelframe,
text='Save "midpoints.npy" file')
midpoint_label.grid(row=4, column=1, sticky='W')
# bounding_boxes.npy
bboxes_checkbutton = tk.Checkbutton(
data_csv_labelframe,
text='',
variable=self.controller.all_common_variables.save_bbox_npy,
onvalue=True,
offvalue=False)
bboxes_checkbutton.grid(row=5 ,column=0)
bbox_label = tk.Label(
data_csv_labelframe,
text='Save "bounding_boxes.npy" file')
bbox_label.grid(row=5, column=1, sticky='W')
# stimulation.npy
stim_checkbutton = tk.Checkbutton(
data_csv_labelframe,
text='',
variable=self.controller.all_common_variables.save_stim_npy,
onvalue=True,
offvalue=False)
stim_checkbutton.grid(row=6, column=0)
stim_label = tk.Label(
data_csv_labelframe,
text='Save "stimulation.npy" file')
stim_label.grid(row=6, column=1, sticky='W')
##################################################
# Labelframe binary image and skeleton
image_labelframe = tk.LabelFrame(
self.child,
text='Extra Image Files')
image_labelframe.grid(row=2, column=0, sticky='W')
warning_label = tk.Label(
image_labelframe,
justify=tk.LEFT,
wraplength=300,
text='WARNING! \n'
'To save space it might make sense to not save '
'the binary and skeleton images.\n'
'Be aware that there is currently no guaranteed '
'way to reconstruct the binary images and skeletons.',
fg = 'red'
)
warning_label.grid(row=0, column=0, columnspan=2)
# sm_thresh.npy
binary_checkbutton = tk.Checkbutton(
image_labelframe,
text='',
variable=self.controller.all_common_variables.save_thresh_npy,
onvalue=True,
offvalue=False)
binary_checkbutton.grid(row=1, column=0)
binary_label = tk.Label(
image_labelframe,
text='Save "sm_thresh.npy" file')
binary_label.grid(row=1, column=1, sticky='W')
# sm_skeletons.npy
skeleton_checkbutton = tk.Checkbutton(
image_labelframe,
text='',
variable=self.controller.all_common_variables.save_skel_npy,
onvalue=True,
offvalue=False)
skeleton_checkbutton.grid(row=2, column=0)
skeleton_label = tk.Label(
image_labelframe,
text='Save "sm_skeletons.npy" file')
skeleton_label.grid(row=2, column=1, sticky='W')
def undistort_online_func(self):
"""
This function creates the 'Option->Undistort Online' menu.
Will only be created if CV2_INSTALLED == True
"""
# create a new window and name it
self.child = tk.Toplevel()
self.child.wm_title('Undistort Options')
self.child.attributes("-topmost", True) # force on top
# disable main window
self.child.grab_set()
explanation_undistort = tk.Label(
self.child,
justify=tk.LEFT,
wraplength=300,
text='The images can have visible distortion, depending '
'on the lens used. The PiVR tracking algorithm can '
'correct the extracted coordinates (e.g. centroid) '
'during tracking. If you present a virtual reality '
'the undistorted points will be used as position '
'for the animal'
)
explanation_undistort.grid(row=0, column=0)
# Labelframe yes_no.csv
yes_no_labelframe = tk.LabelFrame(
self.child,
text='')
yes_no_labelframe.grid(row=1, column=0, sticky='W')
# yes_no
undistort_yes_no = tk.Checkbutton(
yes_no_labelframe,
text='',
variable=self.controller.all_common_variables.online_undistort_bool,
onvalue=True,
offvalue=False)
undistort_yes_no.grid(row=2, column=0)
undistort_yes_no_label = tk.Label(
yes_no_labelframe,
text='Perform Undistort?')
undistort_yes_no_label.grid(row=2, column=1, sticky='W')
# Labelframe undistort files
files_labelframe = tk.LabelFrame(
self.child,
text='Undistort Files')
files_labelframe.grid(row=2, column=0, sticky='W')
def undistort_path_func():
"""
Function is called when radiobutton is clicked
Note - easy to expand to add, e.g. HQ camera!
Just add one elif below and copy paste the Radiobutton
lines below and adapt the value.
Also make sure to adapt the undistort_path initializiation
in all_common_variables with then new value
"""
if self.controller.all_common_variables.undistort_radiobutton_var.get() == 0:
self.controller.all_common_variables.undistort_path = \
pathlib.Path(pathlib.Path(os.path.realpath(PiVR_ROOT_PATH)), 'undistort_matrices', 'standard')
elif self.controller.all_common_variables.undistort_radiobutton_var.get() == 1:
self.controller.all_common_variables.undistort_path = \
pathlib.Path(pathlib.Path(os.path.realpath(PiVR_ROOT_PATH)), 'undistort_matrices', 'user_provided')
# New in v1.8.0: set pixel/mm to None, forcing the user to re-define them. See #114
self.controller.all_common_variables.pixel_per_mm_var.set(0)
# standard files
standard_files_radiobutton = tk.Radiobutton(
files_labelframe,
justify=tk.LEFT,
text="Use undistort files for standard lens",
padx=20,
variable=self.controller.all_common_variables.undistort_radiobutton_var,
value=0,
command=undistort_path_func)
standard_files_radiobutton.grid(row=1, column=0 ,columnspan=1, sticky='W')
user_files_radiobutton = tk.Radiobutton(
files_labelframe,
justify=tk.LEFT,
text="Use your own undistort files",
padx=20,
variable=self.controller.all_common_variables.undistort_radiobutton_var,
value=1,
command=undistort_path_func)
user_files_radiobutton.grid(row=2, column=0 ,columnspan=1, sticky='W')
def select_output_channels(self):
"""
This function will let the user select which output
channel (GPIO#x) corresponds to STIM_1 etc
:return:
"""
output_channels.DefineOutputChannels(path=self.controller.path_of_program,
controller=self.controller)
def draw_VR_arena(self):
"""
Calls classes in 'VR_drawing_board.py' to allow user to draw
virtual realities at different resolutions.
It is probably best to just ask the user for the desired
resolution.
This is done in 'VR_drawing_board.py'
"""
VR_drawing_board.SelectResolution(
path_of_program=self.controller.path_of_program)
def select_time_dependent_stim_func(self):
"""
This function gets called when the user clicks on the
'Select Time Dependent Stim File' button a recording setting.
The user has to choose an csv file. I don't think there will
be enough space to actually display it so the only way to
display it will be in a special menu where one can create the
stimulation file.
Todo Actually - it might be better to show the stimulus and
the time...to be discussed!
"""
try:
self.controller.all_common_variables.time_dependent_stim_file_name = filedialog.askopenfilename(
initialdir=self.controller.path_of_program + '/time_dependent_stim',
title="Select file",
filetypes=(("csv files", "*.csv"), ("all files", "*.*")))
# read the file
self.controller.all_common_variables.time_dependent_stimulation_file = \
pd.read_csv(self.controller.all_common_variables.time_dependent_stim_file_name , delimiter=',')
# New in v1.8.0
cancel_loading_stim_file = False
if self.controller.all_common_variables.high_power_LEDs_bool.get():
for column in self.controller.all_common_variables.time_dependent_stimulation_file:
if 'Channel' in column:
if np.logical_and(98 < self.controller.all_common_variables.time_dependent_stimulation_file[column],
self.controller.all_common_variables.time_dependent_stimulation_file[column] < 100).any():
cancel_loading_stim_file = True
if cancel_loading_stim_file:
tk.messagebox.showerror('Forbidden values',
'The HP setup does not allow the presentation\n'
'of stimulus intensities between\n'
'98.01% and 99.99% (and between 0.01 and 4.99%).\n'
'Please adjust the stimulus file accordingly.'
)
if not cancel_loading_stim_file:
for column in self.controller.all_common_variables.time_dependent_stimulation_file:
if 'Channel' in column:
if np.logical_and(0 < self.controller.all_common_variables.time_dependent_stimulation_file[column],
self.controller.all_common_variables.time_dependent_stimulation_file[column] < 5).any():
cancel_loading_stim_file = True
if cancel_loading_stim_file:
tk.messagebox.showerror('Forbidden values',
'The HP setup does not allow the presentation\n'
'of stimulus intensities between\n'
'0.01% and 4.99% (and between 98.01 and 99.99%).\n'
'Please adjust the stimulus file accordingly.'
)
if cancel_loading_stim_file:
# Remove file from memory
self.controller.all_common_variables.time_dependent_stimulation_file = None
self.controller.all_common_variables.chosen_timedep_file_display_var.set(None)
elif not cancel_loading_stim_file:
# update the button label
self.controller.all_common_variables.chosen_timedep_file_display_var.set(
pathlib.Path(self.controller.all_common_variables.time_dependent_stim_file_name).name)
# todo if want to display the stimulation file, put the code here
except OSError:
# if nothing has been selected
self.controller.all_common_variables.time_dependent_stimulation_file = None
self.controller.all_common_variables.chosen_timedep_file_display_var.set(None)
pass
def select_vr_arena_func(self, dynamic_VR=False):
"""
This function gets called when the user clicks on the
'Select VR Arena' button in the VR experiment tab.
The user has to choose a arena and it will be
displayed as an extra window.
Different arenas can be chosen:
#) A static arena that will just be displayed "as is".
Such an arena is identified using its name: "XX.csv"
#) A static arena that will be displayed relative to
starting position of the animal without taking the
movement of the animal into account. Such an arena is
identified using its name: "XX[320,240].csv". 320 and
240 stand for the x and y coordinates. For example,
if the animal is at exactly 320,240 at the start of the
experiment the arena will not be translated. If the
animal is at 300, 240, the virtual arena will be
translated by 20 pixels.
#) A static arena that is both translated and rotated
relative to the starting position of the animal. Such
an arena is defined using its name:
"XX.[320,240,1.5].csv". The first two number again
define the translation while the third number, 1.5 in
this case, defines the angle the arena is oriented
relative to the movement of the animal.
#) A dynamic arena. This arena can **not** be rotated nor
translated. Dynamic arenas are defined as 3
dimensional numpy arrays with the third dimension
encoding time. The dynamic arena is also defined by
its name: "XXHz[Y].np" with Y being a floating point
number between 0 and pi.
This function handles these different types of arenas by
saving the path to the chosen arena which will then be iven
to the tracking class if the user starts an experiment.
The function also handles the presention: if a static arena
is chosen, the arena is presented in the small preview plot
in the PiVR software.
If no rotation nor translation is desired, an overlay is put
over the preview window to help the user place the animal at
the correct place. Obviously, this doesn't make sense if the
arena is anyway translated/rotated afterwards, so no overlay
is presented.
"""
# As soon as user clicks, reset everything. This enables the
# user to "unselect" any arena and have a "clean slate".
self.controller.all_common_variables.vr_arena_name = None
if dynamic_VR:
self.controller.all_common_variables.vr_arena_name = \
filedialog.askopenfilename(
initialdir=pathlib.Path(self.controller.path_of_program, 'VR_arenas'),
title="Select file",
filetypes=(("dynamic", "*.npy"),
))
else:
self.controller.all_common_variables.vr_arena_name = \
filedialog.askopenfilename(
initialdir=pathlib.Path(self.controller.path_of_program, 'VR_arenas'),
title="Select file",
filetypes=(("csv files", "*.csv"),
))
# always remove the animal that was placed before
try:
self.controller.all_common_variables.animal_annotation.remove()
except (AttributeError, ValueError):
pass
# check if overlay image has been defined. If yes, it means
# that the user has selected a VR arena since starting up the
# software.
# If yes, remove the previous overlay
if self.controller.all_common_variables.overlay_image is not \
None:
try:
self.controller.all_common_variables.cam.remove_overlay(
self.controller.all_common_variables.overlay)
except Exception as ex:
print('Tried removing overlay. This happened:')
print(ex)
# then set the overlay_image...
self.controller.all_common_variables.overlay_image = None
# ... and the overlay variable back to None
self.controller.all_common_variables.overlay = None
# The if..elif below just collects the resolution in int
if self.controller.all_common_variables.resolution == '640x480':
width, height = 640, 480
elif self.controller.all_common_variables.resolution == '1024x768':
width, height = 1024, 768
elif self.controller.all_common_variables.resolution == '1296x972':
width, height = 1296, 972
# Now check what the user has provided...start with checking
# if it's a static arena
if 'csv' in self.controller.all_common_variables.vr_arena_name:
# print('csv in name')
self.controller.all_common_variables.vr_arena_multidimensional = \
False
# collect the string after animal_pos. If no animal_pos
# is defined this will just take the whole string.
animal_defined = \
self.controller.all_common_variables.vr_arena_name.split(
'animal_pos')[-1]
# load the arena into memory
self.controller.all_common_variables.vr_arena = \
np.genfromtxt(
self.controller.all_common_variables.vr_arena_name,
delimiter=',')
# new in v1.8.0
cancel_loading_VR_arena = False
# If high power LED there are some limitations on the way a VR arena can
# be presented:
if self.controller.all_common_variables.high_power_LEDs_bool.get():
# Check if there are values between 98 and 100 in addition to 100:
if np.logical_and(98 < self.controller.all_common_variables.vr_arena,
self.controller.all_common_variables.vr_arena < 100).any():
tk.messagebox.showerror('Forbidden values',
'The HP setup does not allow the presentation\n'
'of stimulus intensities between\n'
'98.01% and 99.99% (and between 0.01 and 4.99%).\n'
'Please adjust the VR arena accordingly.'
)
cancel_loading_VR_arena = True
elif np.logical_and(0 < self.controller.all_common_variables.vr_arena,
self.controller.all_common_variables.vr_arena < 5).any():
tk.messagebox.showerror('Forbidden values',
'The HP setup does not allow the presentation\n'
'of stimulus intensities between\n'
'0.01% and 4.99% (and between 98.01 and 99.99%).\n'
'Please adjust the VR arena accordingly.'
)
cancel_loading_VR_arena = True
if cancel_loading_VR_arena:
self.controller.all_common_variables.vr_arena = None # Remove VR arena from memory
# clean up the plot. The
# overlay, filename variables etc. have already been
# reset to zero.
if self.controller.all_common_variables.resolution == '640x480':
width, height = 640, 480
elif self.controller.all_common_variables.resolution == '1024x768':
width, height = 1024, 768
elif self.controller.all_common_variables.resolution == '1296x972':
width, height = 1296, 972
self.controller.access_subframes(
self.controller.current_window).sub_frames.image_of_arena.set_data(
np.zeros((height, width)))
self.controller.access_subframes(
self.controller.current_window).sub_frames.ax_vr_arena.figure.canvas.draw()
elif not cancel_loading_VR_arena:
# after loading arena into memory check if arena size fits with
# current resolution
if self.controller.all_common_variables.vr_arena.shape == (height ,width):
# Now check if the animal is placed..for this it's
# necessary to do some string acrobatics
try:
# This will check if there's a "," and a "[" in the
# string. As there's indexing here, this will fail (
# IndexError) if none of the two characters are
# present and this will lead to no animal placed!
self.controller.all_common_variables.placed_animal = \
[int(animal_defined.split(',')[0].split('[')[1])]
# Now check after how many "," a "]" is present. If
# after only one, no orientation has been defined.
if ']' in animal_defined.split(',')[1]:
print('no orientation')
self.controller.all_common_variables.placed_animal.append(
int(animal_defined.split(',')[1].split(']')[0]))
# Else the desired orientation theta has been defined.
elif ']' in animal_defined.split(',')[2]:
self.controller.all_common_variables.placed_animal.append(
int(animal_defined.split(',')[1]))
self.controller.all_common_variables.placed_animal.append(
float(animal_defined.split(',')[2].split(']')[0]))
print('animal with orientation')
except IndexError:
# When analyzing the string and finding that no
# animal position has been defined, the code
# continues here.
# print('no animal placed')
self.controller.all_common_variables.placed_animal = None
# only display overlay if **no** animal is placed
# Create an empty array which will be used to create
# the RGBA image
RGB_arena = \
np.zeros((
self.controller.all_common_variables.vr_arena.shape[0],
self.controller.all_common_variables.vr_arena.shape[1],
4), dtype=np.uint8)
# The image only takes values between 0...255 (uint8)
normalized_values = \
self.controller.all_common_variables.vr_arena \
* (255 / 100) # 100 as we expect input from 0 - 100%
# Now fill the empty array with the normalized values
RGB_arena[:, :, 0] = np.around(a=(normalized_values))
RGB_arena[:, :, 3] = np.around(a=(normalized_values))
# For resolution other than 640x480 the overlay image must
# be padded!
padded_image = Image.new(
'RGB', (
((RGB_arena.shape[1] + 31 )//32) * 32,
((RGB_arena.shape[0] + 15 )//16) * 16
))
padded_image.paste(Image.fromarray(RGB_arena, 'RGBA'),
(0 ,0))
# And convert the array to an image object
self.controller.all_common_variables.overlay_image = padded_image
# call the function that puts the overlay over the
# preview
self.preview_overlay_func()
print(self.controller.all_common_variables.placed_animal)
# Here the plotting in the small figure in the center of
# the PiVR software is done. First, the arena is set
self.controller.access_subframes(
self.controller.current_window).sub_frames.image_of_arena.set_data(
self.controller.all_common_variables.vr_arena)
if self.controller.all_common_variables.placed_animal is \
not None:
# if only animal is placed, no orientation information
# is given - therefore a circle is drawn at the given
# position - the animal annotation is a modules_common
# variable which enables us to always delete it when
# loading a new arena
if len(self.controller.all_common_variables.placed_animal) == 2:
circle = Circle((
self.controller.all_common_variables.placed_animal[0],
self.controller.all_common_variables.placed_animal[1]),
10)
self.controller.all_common_variables.animal_annotation = \
self.controller.access_subframes(
self.controller.current_window).sub_frames.ax_vr_arena.add_artist(circle)
else:
# if theta is given (here it doesn't check for
# values between -pi and +pi) first the x and y
# position of the thin end of the arrow needs to
# be calculated (the inverse of arctan2 with
# length = 10)
x_before = \
self.controller.all_common_variables.placed_animal[0] \
+ 10 \
* np.cos(self.controller.all_common_variables.placed_animal[2])
y_before = \
self.controller.all_common_variables.placed_animal[1] \
+ 10 \
* np.sin(self.controller.all_common_variables.placed_animal[2])
# This is where the arrow is plotted
self.controller.all_common_variables.animal_annotation = \
self.controller.access_subframes(
self.controller.current_window).sub_frames.ax_vr_arena.arrow(
x_before, # x
y_before, # y
self.controller.all_common_variables.placed_animal[0] - x_before, # dx
self.controller.all_common_variables.placed_animal[1] - y_before, # dy
head_width=50,
head_length=-25,
fc='r',
ec='r'
)
# and finally the plot is updated
self.controller.access_subframes(
self.controller.current_window).sub_frames.ax_vr_arena.figure.canvas.draw()
else:
tk.messagebox.showerror(
'Invalid Resolution',
'The arena you selected has resolution ' +
repr(self.controller.all_common_variables.vr_arena.shape[1]) +
'x' + repr(self.controller.all_common_variables.vr_arena.shape[0]) +
'\nwhile the camera resolution is set to ' +
repr(width) + 'x' + repr(height) +
'\nPlease select arena with same resolution as camera!')
# remove arena from memory
self.controller.all_common_variables.vr_arena = None
# and remove previously selected arena (if there was one)
self.controller.access_subframes(
self.controller.current_window).sub_frames.image_of_arena.set_data(
np.zeros((height, width)))
self.controller.access_subframes(
self.controller.current_window).sub_frames.ax_vr_arena.figure.canvas.draw()
# TODO: Shouldn't be necessary in here, right?
if self.controller.all_common_variables.high_power_LEDs_bool:
self.controller.all_common_variables.invert_arena_bool = True
elif 'npy' in self.controller.all_common_variables.vr_arena_name:
# This is checking whether the user selected a dynamic
# virtual reality arnea
# read the file:
temp = np.load(self.controller.all_common_variables.vr_arena_name)
if temp.shape[0] == 480 and temp.shape[1] == 640:
try:
self.controller.all_common_variables.vr_update_rate = \
int(self.controller.all_common_variables.vr_arena_name.split('Hz[')[-1].split('].npy')[0])
except ValueError:
tk.messagebox.showerror(
'Please define update rate',
'The time-dependent file you have selected does '
'not specify the update rate.'
'The update rate is defined in the name as'
' the follwing string: '
'\nHz[xxxx].npy '
'\nAn example file name would be: "test_Hz[15].npy"')
self.controller.all_common_variables.vr_arena = temp
self.controller.all_common_variables.vr_arena_multidimensional = True
# TODO: Shouldn't be necessary in here, right?
if self.controller.all_common_variables.high_power_LEDs_bool:
self.controller.all_common_variables.invert_arena_bool = True
# Create an empty array which will be used to create
# the RGBA image used to display the overlay over the
# preview window
RGB_arena = \
np.zeros((self.controller.all_common_variables.vr_arena.shape[0],
self.controller.all_common_variables.vr_arena.shape[1],
4), dtype=np.uint8)
values = self.controller.all_common_variables.vr_arena[: ,: ,0]
# Now fill the empty array with the values
RGB_arena[:, :, 0] = np.around(a=(values))
RGB_arena[:, :, 3] = np.around(a=(values))
# And convert the array to an image object
self.controller.all_common_variables.overlay_image = \
Image.fromarray(RGB_arena, 'RGBA')
# call the function that puts the overlay over the
# preview
self.preview_overlay_func()
# Here the plotting in the small figure in the center of
# the PiVR software is done. First, the arena is set
self.controller.access_subframes(
self.controller.current_window).sub_frames.image_of_arena.set_data(
self.controller.all_common_variables.vr_arena[: ,: ,0])
# and finally the plot is updated
self.controller.access_subframes(
self.controller.current_window).sub_frames.ax_vr_arena.figure.canvas.draw()
else:
tk.messagebox.showerror(
'Wrong spatial dimensions',
'For a virtual arena that changes over time please'
'only use numpy arrays with the size of '
'480x640xtime.'
'\nThe arena you selected has the following '
'dimensions: ' + repr(temp.shape[0])
+ 'x' + repr(temp.shape[1]))
else:
# If nothing has been selected, clean up the plot. The
# overlay, filename variables etc. have already been
# reset to zero.
if self.controller.all_common_variables.resolution == '640x480':
width ,height = 640, 480
elif self.controller.all_common_variables.resolution == '1024x768':
width, height = 1024, 768
elif self.controller.all_common_variables.resolution == '1296x972':
width, height = 1296, 972
self.controller.access_subframes(
self.controller.current_window).sub_frames.image_of_arena.set_data(
np.zeros((height ,width)))
self.controller.access_subframes(
self.controller.current_window).sub_frames.ax_vr_arena.figure.canvas.draw()
# TODO: Shouldn't be necessary in here, right?
if self.controller.all_common_variables.high_power_LEDs_bool:
self.controller.all_common_variables.invert_arena_bool = False
def preview_overlay_func(self):
"""
This function is called whenever the virtual arena is
supposed to be put on top of the overlay.
It adds the overlay_image in all_common_variables to cam.
It then adjusts the size of the overlay to the size of the
preview window.
"""
if RASPBERRY:
if self.controller.all_common_variables.preview_bool:
# Problem: If cam already on and user clicks a second time
# a new overlay is added.
# To mitigate this, try to remove an hypothetical
# overlay
try:
self.controller.all_common_variables.cam.remove_overlay(
self.controller.all_common_variables.overlay)
except Exception as ex:
# This happens when the user presses the "cam
# off" button more than once...
pass
self.controller.all_common_variables.overlay = \
self.cam.add_overlay(
source=self.controller.all_common_variables.overlay_image.tobytes(),
layer=3,
alpha=150)
size = int(self.controller.all_common_variables.preview_window_size_value)
self.controller.all_common_variables.overlay.fullscreen = False
self.controller.all_common_variables.overlay.window = \
(0, 0, size, size)
else:
# If user selects an arena while the preview window
# is turned off, change the overlay variable from
# None to True so that when the preview window is
# turned on the overlay is presented!
self.controller.all_common_variables.overlay = True
elif RASPBERRY and \
self.controller.all_common_variables.resolution != '640x480':
tk.messagebox.showerror(
'Wrong spatial dimension',
'For a virtual arena experiment the resolution must '
'be set to'
'\n640x480'
'\n'
'\nIt is currently set to: '
'\n' + self.controller.all_common_variables.resolution)
def overwrite_metadata(self):
"""
This function is called when the user wants to update the
metadata that is saved in each experimental folder
in the file experiment_settings.json
"""
datetime = time.strftime("%d.%m.%Y_%H-%M-%S")
try:
# open the file with the history of the changed metadata
with open((self.controller.all_common_variables.data_path + '/old_experiment_settings.json'), 'r') as file:
all_old_experiment_settings = json.load(file)
# also open the file that holds the metadata until now
with open((self.controller.all_common_variables.data_path + '/experiment_settings.json'), 'r') as file:
old_experiment_settings = json.load(file)
# extract the metadata of the metadata up to now
metadata_before_change = {}
try:
metadata_before_change['Experiment Date and Time'] = old_experiment_settings['Experiment Date and Time']
except KeyError:
pass
try:
metadata_before_change['Framerate'] = old_experiment_settings['Framerate']
except KeyError:
pass
try:
metadata_before_change['Pixel per mm'] = old_experiment_settings['Pixel per mm']
except KeyError:
pass
# to keep backwards compatibility after changing the 'genotype' to exp. control
if 'Exp. group' in metadata_before_change:
metadata_before_change['Exp. group'] = old_experiment_settings['Exp. group']
elif 'Genotype' in metadata_before_change:
metadata_before_change['Genotype'] = old_experiment_settings['Genotype']
# try:
# metadata_before_change['Genotype'] = old_experiment_settings['Genotype']
# except KeyError:
# pass
try:
metadata_before_change['Resolution'] = old_experiment_settings['Resolution']
except KeyError:
pass
try:
metadata_before_change['Recording time'] = old_experiment_settings['Recording time']
except KeyError:
pass
# try:
# metadata_before_change['Bounding box size'] =
# old_experiment_settings['Bounding box size']
# except KeyError:
# pass
try:
metadata_before_change['Search box size'] = \
old_experiment_settings['Search box size']
except KeyError:
pass
try:
metadata_before_change['Preview'] = old_experiment_settings['Preview']
except KeyError:
pass
try:
metadata_before_change['Preview size'] = old_experiment_settings['Preview size']
except KeyError:
pass
try:
metadata_before_change['Model Organism'] = old_experiment_settings['Model Organism']
except KeyError:
pass
# update the json file with all the old metadata
all_old_experiment_settings.update(metadata_before_change)
# and save it
with open((self.controller.all_common_variables.data_path + '/old_experiment_settings.json'), 'w') as file:
json.dump(all_old_experiment_settings, file, indent=4)
except FileNotFoundError:
print(self.controller.all_common_variables.data_path)
with open((self.controller.all_common_variables.data_path + '/experiment_settings.json'), 'r') as file:
old_experiment_settings = json.load(file)
metadata_before_change = {}
try:
metadata_before_change['Experiment Date and Time'] = old_experiment_settings['Experiment Date and Time']
except KeyError:
pass
try:
metadata_before_change['Framerate'] = old_experiment_settings['Framerate']
except KeyError:
pass
try:
metadata_before_change['Pixel per mm'] = old_experiment_settings['Pixel per mm']
except KeyError:
pass
# to keep backwards compatibility after changing the 'genotype' to exp. control
if 'Exp. group' in metadata_before_change:
metadata_before_change['Exp. group'] = old_experiment_settings['Exp. group']
elif 'Genotype' in metadata_before_change:
metadata_before_change['Genotype'] = old_experiment_settings['Genotype']
# try:
# metadata_before_change['Genotype'] = old_experiment_settings['Genotype']
# except KeyError:
# pass
try:
metadata_before_change['Resolution'] = old_experiment_settings['Resolution']
except KeyError:
pass
try:
metadata_before_change['Recording time'] = old_experiment_settings['Recording time']
except KeyError:
pass
# try:
# metadata_before_change['Bounding box size'] =
# old_experiment_settings['Bounding box size']
# except KeyError:
# pass
try:
metadata_before_change['Search box size'] = \
old_experiment_settings['Search box size']
except KeyError:
pass
try:
metadata_before_change['Preview'] = old_experiment_settings['Preview']
except KeyError:
pass
try:
metadata_before_change['Preview size'] = old_experiment_settings['Preview size']
except KeyError:
pass
try:
metadata_before_change['Model Organism'] = old_experiment_settings['Model Organism']
except KeyError:
pass
with open((self.controller.all_common_variables.data_path + '/old_experiment_settings.json'), 'w') as file:
json.dump(metadata_before_change, file, indent=4)
# open the original file with all the experimental settings
with open((self.controller.all_common_variables.data_path + '/experiment_settings.json'), 'r') as file:
experiment_settings = json.load(file)
# update the old values with the new values
experiment_settings['Pixel per mm'] = self.controller.all_common_variables.pixel_per_mm_var.get()
experiment_settings['Model Organism'] = self.controller.all_common_variables.experimental_metadata[
'Model Organism']
experiment_settings['Experiment Date and Time'] = \
self.controller.all_common_variables.experimental_metadata['Experiment Date and Time']
# overwrite the original file with the experimental settings
with open((self.controller.all_common_variables.data_path + '/experiment_settings.json'), 'w') as file:
json.dump(experiment_settings, file, indent=4)
def metadata_fix_date_time(self):
"""
Function currently not in use.
Can be used to allow user interface to change experiment
metadata using the GUI.
"""
# create a new window and name it
self.child = tk.Toplevel()
self.child.wm_title('Fix Date and Time')
self.child.attributes("-topmost", True) # force on top
# disable main window
self.child.grab_set()
# create a frame in the window
self.child_frame = tk.Frame(self.child)
self.day_label = tk.Label(self.child_frame, text='Day')
self.day_label.grid(row=0, column=0)
self.day_entry = tk.Entry(self.child_frame, width=5)
self.day_entry.grid(row=1, column=0)
# enter the original day
self.day_entry.insert(tk.END,
self.controller.all_common_variables.experimental_metadata['Experiment Date and Time'].
split('.')[0])
self.month_label = tk.Label(self.child_frame, text='Month')
self.month_label.grid(row=0, column=1)
self.month_entry = tk.Entry(self.child_frame, width=5)
self.month_entry.grid(row=1, column=1)
# enter the original month
self.month_entry.insert(tk.END,
self.controller.all_common_variables.experimental_metadata['Experiment Date and Time'].
split('.')[1])
self.year_label = tk.Label(self.child_frame, text='Year')
self.year_label.grid(row=0, column=2)
self.year_entry = tk.Entry(self.child_frame, width=5)
self.year_entry.grid(row=1, column=2)
# enter the original year
self.year_entry.insert(tk.END,
self.controller.all_common_variables.experimental_metadata['Experiment Date and Time'].
split('.')[2].split('_')[0])
self.hour_label = tk.Label(self.child_frame, text='Hour')
self.hour_label.grid(row=0, column=3)
self.hour_entry = tk.Entry(self.child_frame, width=5)
self.hour_entry.grid(row=1, column=3)
# enter the original hour
self.hour_entry.insert(tk.END,
self.controller.all_common_variables.experimental_metadata['Experiment Date and Time'].
split('-')[0].split('_')[1])
self.minutes_label = tk.Label(self.child_frame, text='Minutes')
self.minutes_label.grid(row=0, column=4)
self.minutes_entry = tk.Entry(self.child_frame, width=5)
self.minutes_entry.grid(row=1, column=4)
# enter the original hour
self.minutes_entry.insert(tk.END,
self.controller.all_common_variables.experimental_metadata[
'Experiment Date and Time'].
split('-')[1])
self.seconds_label = tk.Label(self.child_frame, text='Seconds')
self.seconds_label.grid(row=0, column=5)
self.seconds_entry = tk.Entry(self.child_frame, width=5)
self.seconds_entry.grid(row=1, column=5)
# enter the original hour
self.seconds_entry.insert(tk.END,
self.controller.all_common_variables.experimental_metadata[
'Experiment Date and Time'].
split('-')[2])
self.accept_changes = tk.Button(self.child_frame, text='Accept Changes',
command=self.metadata_accept_datetime_changes)
self.accept_changes.grid(row=2, column=0, columnspan=3)
self.discard_changes = tk.Button(self.child_frame, text='Discard Changes',
command=self.metadata_discard_changes)
self.discard_changes.grid(row=2, column=3, columnspan=3)
self.child_frame.pack()
def metadata_accept_datetime_changes(self):
"""
Function is called to update the date and time metadata.
It first checks for valid input and prompts the user
to fix the input if it fails this test
"""
try:
if type(int(self.day_entry.get())) and type(int(self.month_entry.get())) and type(
int(self.year_entry.get())) \
and type(int(self.hour_entry.get())) and type(int(self.minutes_entry.get())) and \
type(int(self.seconds_entry.get())) is int:
# first check if the input is all integer and no text
if 0 < int(self.day_entry.get()) <= 31 and \
0 < int(self.month_entry.get()) <= 12 and \
1900 < int(self.year_entry.get()) < 2050 and \
0 <= int(self.hour_entry.get()) < 24 and \
0 <= int(self.minutes_entry.get()) < 60 and \
0 <= int(self.seconds_entry.get()) < 60:
# then check if the input is generally valid
if 0 < int(self.day_entry.get()) <= \
calendar.monthrange(int(self.year_entry.get()), int(self.month_entry.get()))[1]:
# then check if the month has as many days as is being claimed'
# if all good, assign the new time/date to the main variable. Note - small caveat, this will
# remove trailing zeros (e.g. 01 for January will become just 1)
self.controller.all_common_variables.experimental_metadata['Experiment Date and Time'] = \
str(int(self.day_entry.get())) + '.' + str(int(self.month_entry.get())) + '.' + \
str(int(self.year_entry.get())) + '_' + str(int(self.hour_entry.get())) + '-' + \
str(int(self.minutes_entry.get())) + '-' + str(int(self.seconds_entry.get()))
# set main window active again
self.child.grab_release()
# close the child window
self.child.after(0, self.child.destroy())
# update the date and time in the main window
self.controller.access_subframes(
self.controller.current_window).sub_frames.experiment_date_n_time_label.configure(
text=self.controller.all_common_variables.experimental_metadata['Experiment Date and Time'])
else:
# the user entered an invalid day relative to the month and year chosen
tk.messagebox.showerror('Invalid Entry', 'Please enter a valid date')
else:
# the user entered an invalid anything - could be day, month, year, hour, minute or second (or all)
tk.messagebox.showerror('Invalid Entry', 'Please enter valid numbers')
else:
# unsure how this could happen, but let's keep it to not have silent errors
tk.messagebox.showerror('Invalid Entry',
'Please enter valid numbers in the format\n 01 01 2018 12 12 12\n'
'Also, let me know how you got this error!')
except ValueError:
# this error happens when the user enters a string
tk.messagebox.showerror('Invalid Entry', 'Please enter valid numbers in the format\n 01 01 2018 12 12 12')
def metadata_discard_changes(self):
# set main window active again
self.child.grab_release()
# close the child window
self.child.after(0, self.child.destroy())
def metadata_accept_organism_changes(self):
self.controller.all_common_variables.experimental_metadata['Model Organism'] = self.organism_entry.get()
# set main window active again
self.child.grab_release()
# close the child window
self.child.after(0, self.child.destroy())
# update the model organism in the main window
self.controller.access_subframes(
self.controller.current_window).sub_frames.species_label.configure(
text=self.controller.all_common_variables.experimental_metadata['Model Organism'])
def metadata_fix_organism(self):
# create a new window and name it
self.child = tk.Toplevel()
self.child.wm_title('Fix Organism')
self.child.attributes("-topmost", True) # force on top
# disable main window
self.child.grab_set()
# create a frame in the window
self.child_frame = tk.Frame(self.child)
self.organism_label = tk.Label(self.child_frame, text='Enter name of Organims')
self.organism_label.grid(row=0, column=0)
self.organism_entry = tk.Entry(self.child_frame, width=20)
self.organism_entry.grid(row=1, column=0, columnspan=2)
self.organism_entry.insert(tk.END,
self.controller.all_common_variables.experimental_metadata['Model Organism'])
self.accept_changes = tk.Button(self.child_frame, text='Accept Changes',
command=self.metadata_accept_organism_changes)
self.accept_changes.grid(row=2, column=0)
self.discard_changes = tk.Button(self.child_frame, text='Discard Changes',
command=self.metadata_discard_changes)
self.discard_changes.grid(row=2, column=1)
self.child_frame.pack()
def high_power_LED(self):
"""
If the software is run on a configuration using the miniBuck
the PWM signal needs to be inverted as the current controller
is on if no control voltage is provided. The standard
configuration with the transistor isinverse, as the transistor
only conducts current if the gate is opened using the control voltage
*New in v1.8.0
Due to the change how the GPIOs are controlled (see #144) it is necessary to
load a lookup table if
"""
# create new window and name it
self.controller.all_common_variables.child_high_power_LED = tk.Toplevel()
self.controller.all_common_variables.child_high_power_LED.wm_title('High Power LED selection')
self.controller.all_common_variables.child_high_power_LED.attributes("-topmost", True) # force on top
# disable main window
self.controller.all_common_variables.child_high_power_LED.grab_set()
# create a temporary variable that can be saved or thrown away depending on what the user does
temp_high_power_LED_bool = tk.IntVar()
temp_high_power_LED_bool.set(self.controller.all_common_variables.high_power_LEDs_bool.get())
# todo once name is finalized > change name here!
explanation = tk.Label(self.controller.all_common_variables.child_high_power_LED, justify=tk.LEFT,
text='PiVR comes in two different versions:\n'
'1) the standard version with LED strips\n'
'2) the high LED power version with high power LEDs\n'
'Please select which version you have:')
explanation.grid(row=0, column=0, columnspan=2, sticky='W')
standard_radiobutton = tk.Radiobutton(self.controller.all_common_variables.child_high_power_LED, justify=tk.LEFT,
text="Standard LED configuration (Strips)",
padx=20,
variable=temp_high_power_LED_bool,
value=0)
standard_radiobutton.grid(row=1, column=0 ,columnspan=2, sticky='W')
high_power_radiobutton = tk.Radiobutton(self.controller.all_common_variables.child_high_power_LED, justify=tk.LEFT,
text="High Powered LED configuration (Stars)",
padx=20,
variable=temp_high_power_LED_bool,
value=1)
high_power_radiobutton.grid(row=2, column=0 ,columnspan=2, sticky='W')
quit_window_save = tk.Button(self.controller.all_common_variables.child_high_power_LED,
text='Exit and save changes',
command=lambda : self.exit_toplevel(popup_name='High Powered LED',
save=True,
savevalue=temp_high_power_LED_bool))
quit_window_save.grid(row=3, column=0, sticky='W')
quit_window_discard = tk.Button(self.controller.all_common_variables.child_high_power_LED,
text='Exit and discard changes',
command=lambda : self.exit_toplevel(popup_name='High Powered LED',
save=False))
quit_window_discard.grid(row=3, column=1, sticky='W')
def exit_toplevel(self, popup_name ,save=False ,savevalue=None):
"""
Function is called when user presses the quit button on
one of the toplevel (popup) windows
"""
if popup_name =='High Powered LED':
if save:
# only save if user presses the correct button
self.controller.all_common_variables.high_power_LEDs_bool.set(savevalue.get())
# let the user know that the program needs to be restarted
tk.messagebox.showinfo('Notification',
'For the changes to take effect\n'
'please save and exit and restart\n'
'the program')
# set main window active again
self.controller.all_common_variables.child_high_power_LED.grab_release()
# close the child window
self.controller.all_common_variables.child_high_power_LED.after(0,
self.controller.all_common_variables.child_high_power_LED.destroy())
def animal_detection_method_func(self):
"""
User can choose between three animal detection/background
reconstruction modes:
Mode 1 is the standard mode. It waits until something moves
and reconstructs the the background from that image. Most of
the time the background image of this mode will have a little
bit of the animal left. But it seems to be robust.
Mode 2, or pre-define background, is the mode where the user
takes a picture of the arena without the animal and then adds
the animal. The first image is the background for the whole
experiment. This can be the optimal mode if the experiment
has no lid that has to be opened to put the animal, such as
a zebrafish experiment.
Mode 3, or background reconstruction by stitching, attempts
to give a close to perfect background reconstruction without
user intervention. It waits for the animal to move,
does local thresholding to identify the animal, then the
algorithm waits until the animal is gone from the original
position and then takes a picture of only the rectangle that
was occupied by the animal before and 'stiches' it into the
first image. This works well with slow moving animal such as
Drosophila larvae on a evenly illuminated background without
any high-contrast image features
"""
# create a new window and name it
self.child = tk.Toplevel()
self.child.wm_title('Select Detection Method')
self.child.attributes("-topmost", True) # force on top
# disable main window
self.child.grab_set()
# create a frame in the window
self.child_frame = tk.Frame(self.child)
explanation_animal_detection_method = tk.Label(
self.child,
justify=tk.LEFT,
wraplength=500,
text='Before the animal can be tracked it needs to be '
'identified. '
'In the current implementation three options are '
'available. ')
# 'Please refer to the publication/webpage for more details.')
explanation_animal_detection_method.grid(row=0, column=0 ,sticky='W')
standard_method_button = tk.Radiobutton(
self.child,
justify=tk.LEFT,
wraplength=500,
text='Standard - Mode 1\n'
'This method will work for a wide array of animals and '
'experimental setups.\n'
'It detects the animal as soon as it moves. It then proceeds '
'with a lazy background reconstruction that will usually not '
'completely get rid of the animal in the background image\n',
variable=self.controller.all_common_variables.animal_detection_method_var,
value='Mode 1')
standard_method_button.grid(row=1, column=0 ,sticky='W')
pre_define_background_button = tk.Radiobutton(
self.child, justify=tk.LEFT, wraplength=500,
text='Pre-Define background - Mode 2\n'
'This method works well if the user is able to insert the animal '
'into the Field of View of the camera without changing '
'anything else in the image.\n'
'The user takes a background image without the animal. '
'Then the animal (and only the animal) is introduced.\n'
'This works well if no lid is used on the arena.\n',
variable=self.controller.all_common_variables.animal_detection_method_var,
value='Mode 2')
pre_define_background_button.grid(row=2, column=0 ,sticky='W')
background_reconstruction_by_stitching = tk.Radiobutton(
self.child,
justify=tk.LEFT,
wraplength=500,
text='Reconstruct background by stitching - Mode 3\n'
'Similar to Standard. It only works if the animal clearly '
'contrasts with the background.\n'
'This method detects the animal as soon as it moves. It then '
'identies the animal using the parameters defined in'
'"list_of_available_organisms.json" file. '
"It then waits for the animal to leave it's original position "
"and then reconstructs the whole background image automatically\n"
'This method is recommended for slow animals/animals that do '
'not reach the edge of the arena during detection.\n',
variable=self.controller.all_common_variables.animal_detection_method_var,
value='Mode 3')
background_reconstruction_by_stitching.grid(row=3,
column=0,
sticky='W')
def camera_controls_func(self):
"""
Popup that user can call to define optimal image quality.
Contains Cam ON/Cam OFF button, Scales for the different GPIO channels, button for autoexposure, and scales
for brightness and contrast
:return:
"""
# create a new window and name it
child_window_optimize_image = tk.Toplevel()
child_window_optimize_image.wm_title('Set up optimal image')
child_window_optimize_image.attributes("-topmost", True) # force on top
# disable main window
child_window_optimize_image.grab_set()
# create a Labeled Frame in the window for all the camera controls
child_frame_cam = tk.LabelFrame(child_window_optimize_image,
text='Camera Control')
child_frame_cam.grid(row=0, column=0)
# create a second Labeled Frame in the window for all the LED controls
child_frame_led = tk.LabelFrame(child_window_optimize_image,
text='LED Control')
child_frame_led.grid(row=1, column=0)
# if RASPBERRY or PiVR_settings['virtual raspberry']:
self.cam_on_button = tk.Button(child_frame_cam, text='Cam On', fg='green',
command=self.controller.all_common_functions.cam_on_func)
self.cam_on_button.grid(row=0, column=0)
self.cam_off_button = tk.Button(child_frame_cam, text='Cam Off', fg='red',
command=self.controller.all_common_functions.cam_off_func)
self.cam_off_button.grid(row=0, column=1)
self.controller.all_common_variables.auto_exposure_button = tk.Button(child_frame_cam, text='autoexp. on',
command=self.controller.all_common_functions.autoexp_callback)
self.controller.all_common_variables.auto_exposure_button.grid(row=1, column=0)
# turn autoexposure off only if it was turned off before. In fresh out-of-the box software will
# be turned on
if RASPBERRY:
if PiVR_SETTINGS is not None and self.cam.shutter_speed != 0 :
self.controller.all_common_variables.auto_exposure_button['text'] = 'autoexp. off'
# to keep things tidy, class the resolution widgets together in one frame: the resolution_frame
resolution_frame = tk.Frame(child_frame_cam)
resolution_frame.grid(row=2, column=3, rowspan=2)
resolution_label = tk.Label(resolution_frame,
text='Camera Resolution')
resolution_label.grid(row=0, column=0)
self.cam_resolution_menu = tk.OptionMenu(resolution_frame,
self.controller.all_common_variables.resolution_variable,
*self.controller.all_common_variables.available_resolution)
self.cam_resolution_menu.grid(row=1, column=0)
self.preview_size_scale = tk.Scale(child_frame_cam, from_=180, to=1000, resolution=20,
label='Video preview window size',
variable=self.controller.all_common_variables.preview_window_size_variable,
orient='horizontal', len=180)
self.preview_size_scale.grid(row=2, column=0, columnspan=2, rowspan=2)
# to keep things tidy, class the framerate widgets together into one frame: the recording_framerate_frame
recording_framerate_frame = tk.Frame(child_frame_cam)
recording_framerate_frame.grid(row=0, column=3, rowspan=2)
# the recording framerate entry and update button - keep in here as this will change the image
self.recording_frameratelabel = tk.Label(recording_framerate_frame, text='Recording framerate')
self.recording_frameratelabel.grid(row=0, column=0)
self.recording_framerate = tk.Entry(recording_framerate_frame,
width=5,
textvariable=self.controller.all_common_variables.framerate_entry_variable)
self.recording_framerate.grid(row=0, column=1)
self.update_framerate = tk.Button(recording_framerate_frame, text='Update Preview Framerate',
command=lambda: self.controller.all_common_functions.update_framerate_func(
new_framerate=int
(self.controller.all_common_variables.framerate_entry_variable.get())
))
self.update_framerate.grid(row=1, column=0, columnspan=2)
self.backlight_intensity_scale = tk.Scale(child_frame_led, from_=0, to=100, resolution=1,
label='Backlight Intensity (%)',
variable=self.controller.all_common_variables.backlight_intensity_variable,
orient='horizontal',
len=180)
self.backlight_intensity_scale.grid(row=2, column=0)
self.backlight_intensity_scale.set(self.controller.all_common_variables.backlight_intensity_variable.get())
self.backlight_two_intensity_scale = tk.Scale(child_frame_led, from_=0, to=100, resolution=1,
label='Backlight 2 Intensity',
variable=self.controller.all_common_variables.backlight_two_intensity_variable,
orient='horizontal',
len=180)
self.backlight_two_intensity_scale.grid(row=2, column=1)
self.backlight_two_intensity_scale.set \
(self.controller.all_common_variables.backlight_two_intensity_variable.get())
#if RASPBERRY:
# resolution_of_analog_output_scale = int(self.controller.all_common_variables.pwm_range / 20)
# range_of_analog_output_scale = self.controller.all_common_variables.pwm_range
#else:
# range_of_analog_output_scale = PiVR_SETTINGS['pwm_range']
# resolution_of_analog_output_scale = PiVR_SETTINGS['pwm_range'] / 20
# todo - put in ifs so that high speed PWM can be controlled as well!!!!!!
self.analog_output_channel_one_scale = tk.Scale(child_frame_led, from_=0,
to=100, #range_of_analog_output_scale,
resolution=1, #resolution_of_analog_output_scale,
label='Channel 1',
variable=self.controller.all_common_variables.channel_one_variable,
orient='horizontal',
len=180)
self.analog_output_channel_one_scale.grid(row=3, column=0)
self.analog_output_channel_one_scale.set(self.controller.all_common_variables.channel_one_dutycycle)
self.analog_output_channel_two_scale = tk.Scale(child_frame_led, from_=0,
to=100, # range_of_analog_output_scale,
resolution=1, #resolution_of_analog_output_scale,
label='Channel 2',
variable=self.controller.all_common_variables.channel_two_variable,
orient='horizontal',
len=180)
self.analog_output_channel_two_scale.grid(row=3, column=1)
self.analog_output_channel_two_scale.set(self.controller.all_common_variables.channel_two_dutycycle)
self.analog_output_channel_three_scale = tk.Scale(child_frame_led, from_=0,
to=100, # range_of_analog_output_scale,
resolution=1, #resolution_of_analog_output_scale,
label='Channel 3',
variable=self.controller.all_common_variables.channel_three_variable,
orient='horizontal',
len=180)
self.analog_output_channel_three_scale.grid(row=4, column=0)
self.analog_output_channel_three_scale.set(self.controller.all_common_variables.channel_three_dutycycle)
self.analog_output_channel_four_scale = tk.Scale(child_frame_led, from_=0,
to=100, #range_of_analog_output_scale,
resolution=1, #resolution_of_analog_output_scale,
label='Channel 4',
variable=self.controller.all_common_variables.channel_four_variable,
orient='horizontal',
len=180)
self.analog_output_channel_four_scale.grid(row=4, column=1)
self.analog_output_channel_four_scale.set(self.controller.all_common_variables.channel_four_dutycycle)
width =400
height =350
xoffset =400
yoffset =0
child_window_optimize_image.geometry("%dx%d%+d%+d" % (width, height, xoffset, yoffset))
'''
# v1.8.0 Doesn't seem to be used
def turn_GPIO_fully_off(self,
output_channel_one,
output_channel_two,
output_channel_three,
output_channel_four):
"""
This function turns off all the Channels (*not* the
background) by turning the dutycylce to 0 (or the PWM_RANGE (
should be 100) in the high power version).
This is achieved by using a list comprehension and cycling
through the GPIOs of each channel.
"""
# turn off GPIOs of Channel one - keep in loop instead of just
# saying GPIO17 off for forward compatibility!!
# list comprehension of Channel 1
if self.controller.all_common_variables.high_power_LEDs_bool.get():
off_value = self.controller.all_common_variables.pwm_range
else:
off_value = 0
[self.controller.all_common_variables.pwm_object.set_PWM_dutycycle(
user_gpio=output_channel_one[i_stim][0],
dutycycle=off_value) for i_stim in range(len(output_channel_one))]
# list comprehension of Channel 2
[self.controller.all_common_variables.pwm_object.set_PWM_dutycycle(
user_gpio=output_channel_two[i_stim][0],
dutycycle=off_value) for i_stim in range(len(output_channel_two))]
# list comprehension of Channel 3
[self.controller.all_common_variables.pwm_object.set_PWM_dutycycle(
user_gpio=output_channel_three[i_stim][0],
dutycycle=off_value) for i_stim in range(len(output_channel_three))]
# list comprehension of Channel 4
[self.controller.all_common_variables.pwm_object.set_PWM_dutycycle(
user_gpio=output_channel_four[i_stim][0],
dutycycle=off_value) for i_stim in range(len(output_channel_four))]'''
def vr_body_part_stim_func(self):
"""
This function creates a tkinter window which allows the user
to choose which point of the animal to use for virtual reality
reference: Head, Midpoint, Centroid or Tail.
"""
child_window_vr_body_stim = tk.Toplevel()
child_window_vr_body_stim.wm_title('Select body part')
child_window_vr_body_stim.attributes("-topmost", True) # force on top
# disable the main window
child_window_vr_body_stim.grab_set()
explanation_vr_body_stim = tk.Label(
child_window_vr_body_stim,
justify=tk.LEFT,
wraplength=300,
text='When presenting the animal with a virtual reality, '
'the sensory organs that are being probed can be '
'located at different parts of the body. PiVR allows* '
'the presentation of the virtual reality depending on '
'the position of the following body parts:'
)
explanation_vr_body_stim.grid(row=0, column=0 ,columnspan=2)
vr_stim_head_button = tk.Radiobutton(
child_window_vr_body_stim,
justify=tk.LEFT,
text='Head',
variable=self.controller.all_common_variables.vr_stim_loc_var,
value='Head'
)
vr_stim_head_button.grid(row=1, column=0, sticky='W')
vr_stim_centroid_button = tk.Radiobutton(
child_window_vr_body_stim,
justify=tk.LEFT,
text='Centroid',
variable=self.controller.all_common_variables.vr_stim_loc_var,
value='Centroid'
)
vr_stim_centroid_button.grid(row=2, column=0, sticky='W')
vr_stim_midpoint_button = tk.Radiobutton(
child_window_vr_body_stim,
justify=tk.LEFT,
text='Midpoint',
variable=self.controller.all_common_variables.vr_stim_loc_var,
value='Midpoint'
)
vr_stim_midpoint_button.grid(row=3, column=0, sticky='W')
vr_stim_tail_button = tk.Radiobutton(
child_window_vr_body_stim,
justify=tk.LEFT,
text='Tail',
variable=self.controller.all_common_variables.vr_stim_loc_var,
value='Tail'
)
vr_stim_tail_button.grid(row=4, column=0, sticky='W')
disclaimer = tk.Label(
child_window_vr_body_stim,
justify=tk.LEFT,
wraplength=300,
text='*Please note that the head/tail classification '
'algorithm is not perfect! You must ensure that the '
'algorithm works reasonably well for you experiment.'
)
disclaimer.grid(row=5, column=0, columnspan=2)
def grab_undistort_files(self, resolution = None):
"""
This functions is called just before an experiment. It loads the
mtx and dst files used for undistort into memory.
It also prepares the newcameramtx needed to perform undistort
User can provide own files. Different files are used
for different resolution. This function makes sure the correct
files are loaded
"""
# self.controller.all_common_variables.undistort_path # path to undistort files, e.g. user defined files
# self.controller.all_common_variables.resolution # resolution as a string, e.g. '640x480'
# Extra safety call: If for some reason the bool is set to True
# do another check here
if not CV2_INSTALLED:
self.controller.all_common_variables.online_undistort_bool.set(0)
print('cv2 not installed, cannot perform undistort.')
if self.controller.all_common_variables.online_undistort_bool.get():
if resolution == None:
dst_path = pathlib.Path(self.controller.all_common_variables.undistort_path,
self.controller.all_common_variables.resolution + \
'_dist.npy')
mtx_path = pathlib.Path(self.controller.all_common_variables.undistort_path,
self.controller.all_common_variables.resolution + \
'_mtx.npy')
# Also prepare the cameramatrix so that this doesn't have
# to be done afterwards
# extract width and height from resolution variable
width = int(self.controller.all_common_variables.resolution.split('x')[0])
height = int(self.controller.all_common_variables.resolution.split('x')[-1])
else:
dst_path = pathlib.Path(self.controller.all_common_variables.undistort_path,
resolution + '_dist.npy')
mtx_path = pathlib.Path(self.controller.all_common_variables.undistort_path,
resolution + '_mtx.npy')
width = int(resolution.split('x')[0])
height = int(resolution.split('x')[-1])
self.controller.all_common_variables.undistort_dst_file = np.load(dst_path)
self.controller.all_common_variables.undistort_mtx_file = np.load(mtx_path)
# print("width" + repr(width))
# print("height"+ repr(height))
self.controller.all_common_variables.newcameramtx, roi = \
cv2.getOptimalNewCameraMatrix(
self.controller.all_common_variables.undistort_mtx_file,
self.controller.all_common_variables.undistort_dst_file,
(width, height), 1, (width, height))
else:
# if user elects to NOT do undistort make sure the dst and mtx
# variable are None
self.controller.all_common_variables.undistort_dst_file = None
self.controller.all_common_variables.undistort_mtx_file = None
self.controller.all_common_variables.newcameramtx = None
def undistort_new_lens_menu(self):
"""
This functions creates a tkinter window which instructions on
how to create new mtx/dist files for undistort if user wants to
use a different lens/camera or resolution
"""
child_undistort_new_lens = tk.Toplevel()
child_undistort_new_lens.wm_title('Undistort, new lens')
child_undistort_new_lens.attributes("-topmost", True) # force on top
child_undistort_new_lens.grab_set() # disable main window
wraplength = 350
explanation_undistort_new_lens = tk.Label(
child_undistort_new_lens,
justify=tk.LEFT,
wraplength=wraplength + 10,
text='PiVR comes with files to undistort a variety of '
'resolutions for the standard lens.\n\n'
'If you use a different resolution or a different lens '
'you may use these instructions to create files for '
'your specific setup.\n\n'
'Note: For detailed instructions please visit pivr.org')
explanation_undistort_new_lens.grid(row=0, column=0)
instructions_labelframe = tk.LabelFrame(
child_undistort_new_lens,
text='Instructions')
instructions_labelframe.grid(row=1, column=0)
if RASPBERRY or PiVR_SETTINGS['virtual raspberry']:
# On the Raspberry monito there'not a lot of space
# therefore I only display part that is necessary
# without too many explanations
step_3_select_folder_label = tk.Label(
instructions_labelframe,
justify=tk.LEFT,
wraplength=wraplength,
text='Select the folder with the images of the chessboard '
'at different angles. Once selected, the algorithm to '
'extract the calibration files will run automatically. '
'Make sure you have at least 10 jpg images in the '
'folder you are selecting.')
step_3_select_folder_label.grid(row=2, column=0)
step_3_select_folder_button = tk.Button(
instructions_labelframe,
text='Chessboard images',
command=self.controller.all_common_functions.create_mtx_and_dist)
step_3_select_folder_button.grid(row=3, column=0)
else:
explanation_undistort_new_lens = tk.Label(
child_undistort_new_lens,
justify=tk.LEFT,
wraplength=wraplength + 10,
text='PiVR comes with files to undistort a variety of '
'resolutions for the standard lens.\n\n'
'If you use a different resolution or a different lens '
'you may use these instructions to create files for '
'your specific setup.\n\n'
'Note: For detailed instructions please visit pivr.org')
explanation_undistort_new_lens.grid(row=0 ,column=0)
instructions_labelframe = tk.LabelFrame(
child_undistort_new_lens,
text='Instructions')
instructions_labelframe.grid(row=1, column=0)
step_1_print = tk.Label(
instructions_labelframe,
# anchor='w',
justify=tk.LEFT,
# width=wraplength,
wraplength=wraplength,
text='Step 1\n\n'
'Print the Chessboard.jpg. The file can be found in '
'the following folder: "PiVR/undistort_matrices".\n')
step_1_print.grid(row=0, column=0)
step_2_instructions = tk.Label(
instructions_labelframe,
justify=tk.LEFT,
wraplength=wraplength,
text='Step 2\n\n'
'On your PiVR setup, use the "Timelapse Recording" option. '
'Set the image format to "jpg". You will need around 15 '
'pictures of the printed chessboard from different angles. '
'Copy these images from your PiVR to this computer.\n'
)
step_2_instructions.grid(row=1 ,column=0)
step_3_select_folder_label = tk.Label(
instructions_labelframe,
justify=tk.LEFT,
wraplength=wraplength,
text='Step 3\n\n'
'Select the folder with the images of the chessboard '
'at different angles. Once selected, the algorithm to '
'extract the calibration files will run automatically.')
step_3_select_folder_label.grid(row=2, column=0)
step_3_select_folder_button = tk.Button(
instructions_labelframe,
text='Chessboard images',
command=self.controller.all_common_functions.create_mtx_and_dist)
step_3_select_folder_button.grid(row=3, column=0)
step_3_whitespace = tk.Label(
instructions_labelframe,
text='')
step_3_whitespace.grid(row=4, column=0)
step_4_copy_files_label = tk.Label(
instructions_labelframe,
justify=tk.LEFT,
wraplength=wraplength,
text='Step 4\n\n'
'If you are only doing post hoc analysis skip this step.\n'
'Copy the new files in "PiVR/undistort_matrices/user_provided" '
'created on this computer to the same folder on your '
'PiVR setup.\n')
step_4_copy_files_label.grid(row=5, column=0)
step_5_select_correct_undistort = tk.Label(
instructions_labelframe,
justify=tk.LEFT,
wraplength=wraplength,
text='Step 5\n\n'
'In the PiVR GUI go to "Options"->"Undistort Options". '
'Confirm that "Perform Undistort?" is selected. \n'
'In "Undistort Files", select "Use your own undistort '
'files" and save the settings.\n'
'From now on PiVR will use the undistort files specific'
' to your setup.')
step_5_select_correct_undistort.grid(row=6, column=0)
def create_mtx_and_dist(self):
"""
This function asks the user to select a folder with images of
the chessboard at different angles.
It then uses the cv2 functionality to create the mtx and dist
numpy file needed for the undistort functionality.
This function could be transfered to another file if this file
gets too bloated
"""
# Note - this should only be accessible if CV2 is installed
# at the menu is off if not.
path = pathlib.Path(filedialog.askdirectory())
try:
# This fits the provided chessboard image
chessboard_rows = 6
chessboard_columns = 9
chessboard_size = (chessboard_columns, chessboard_rows)
max_number_of_images = 15
# termination criteria
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
# prepare object points, like (0,0,0), (1,0,0), (2,0,0) ....,(6,5,0)
objp = np.zeros((chessboard_rows * chessboard_columns, 3),
np.float32)
objp[:, :2] = np.mgrid[0:chessboard_columns,
0:chessboard_rows].T.reshape(-1, 2)
# Arrays to store object points and image points from all the images.
objpoints = [] # 3d point in real world space
imgpoints = [] # 2d points in image plane.
# images = glob.glob('*.jpg')
image_width = None
image_height = None
# it is unecessary to use more than the max_number_of_images - subsample
# if len(images) > max_number_of_images:
# temp = []
# for i in range(max_number_of_images):
# temp.append(images[int(i * len(images) / max_number_of_images)])
# images = temp
good_images = 0
for counter, current_file in enumerate(path.iterdir()):
if 'jpg' in current_file.name and counter <= max_number_of_images:
img = cv2.imread(str(current_file))
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
if image_height is None:
image_height = gray.shape[1]
image_width = gray.shape[0]
# Find the chess board corners
ret, corners = cv2.findChessboardCorners(gray,
chessboard_size,
None)
# If found, add object points, image points (after refining them)
if ret:
good_images += 1
objpoints.append(objp)
corners2 = cv2.cornerSubPix(gray, corners, (11, 11),
(-1, -1), criteria)
imgpoints.append(corners)
# Draw and display the corners
cv2.drawChessboardCorners(img, (7, 6), corners2, ret)
cv2.imshow('img', img)
cv2.waitKey(500)
cv2.destroyAllWindows()
if good_images > 5:
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(objpoints,
imgpoints,
gray.shape[::-1],
None,
None)
resolution_as_string = repr(image_height) + 'x' + repr(image_width)
path_to_save = pathlib.Path(PiVR_ROOT_PATH, 'undistort_matrices' ,'user_provided')
# Make sure the folder exists before trying to save there.
pathlib.Path(path_to_save).mkdir(parents=True, exist_ok=True)
np.save(pathlib.Path(path_to_save, resolution_as_string + '_dist.npy'), dist)
np.save(pathlib.Path(path_to_save, resolution_as_string + '_mtx.npy'), mtx)
print('Used ' + repr(good_images + 1) + ' images for calibration.')
print('Calibration successful.')
else:
tk.messagebox.showerror('Not enough good images',
'The provided folder does not contain enough\n'
'good images for calibration.\n'
'Please provide at least 5 good images of the\n'
'checkerboard in different angles.')
print('No jpg images in folder')
except Exception as e:
print("Unable to perform calibration. ")
print(e)
def call_macro_editor(self):
"""
This function calls the macro editor which allows a user to run
more than one experiment consecutively
"""
if not RASPBERRY:
pwm_object = None
else:
pwm_object = self.controller.all_common_variables.pwm_object
macro_editor.MacroEditor(pivr_path = self.controller.path_of_program,
pwm_range = PiVR_SETTINGS['pwm_range'],
high_power_LEDs_bool=self.controller.all_common_variables.high_power_LEDs_bool.get(),
HP_LED_lookup_table=self.controller.all_common_variables.HP_LED_lookup_table,
rec_framerate=int(self.controller.all_common_variables.framerate_entry_variable.get()),
resolution=self.controller.all_common_variables.resolution,
cam=self.cam,
base_path=self.controller.all_common_variables.path_entry_variable.get(),
pwm_object=pwm_object,
model_organism=self.controller.all_common_variables.model_organism_value,
pixel_per_mm=self.controller.all_common_variables.pixel_per_mm_var.get(),
output_channel_one=self.controller.all_common_variables.channel_one,
output_channel_two=self.controller.all_common_variables.channel_two,
output_channel_three=self.controller.all_common_variables.channel_three,
output_channel_four=self.controller.all_common_variables.channel_four,
version_info=VERSION_INFO,
setup_name=self.controller.all_common_variables.setup_name
) #
def call_enter_setup_name(self):
"""
This function calls the popup allowing the user to change the PiVR setup
"""
enter_setup_name.SetName(controller=self.controller)
'''
# KEEP - might be implemented in a future version
class FixMetadata(tk.Frame):
def __init__(self, parent, controller, camera_class=None):
tk.Frame.__init__(self, parent)
# reference the camera and the controller
self.camera_class = camera_class
self.cam = self.camera_class.cam
self.controller = controller
self.entered_text = None
self.timestamps = None
self.framerate = None # can't use a textvariable as we'll use both strings and numbers here!
self.species = None
self.genotype = None
# Here the frame that will hold all the camera controls is being prepared.
self.subframe_preexperiment = tk.LabelFrame(self, text='Fix Metadata')
self.subframe_preexperiment.grid(row=0, column=0)
# construct the window by calling the SubFrames class with whatever we need. This instance is then bound to the
# variable self.sub_frames to make it's variables etc available
self.sub_frames = SubFrames(self.camera_class, self.controller,
cam_frame=False,
subframe_preexperiment=self.subframe_preexperiment,
misc_frame=True,
observation_mode=False, distance_configuration=False, model_organism=False,
update_metadata=True,
exp_ctr_frame=False,
VR_arena=False,
frame=self,
quit_frame=False,
display_experiment_settings=True,
fix_metadata=True,
convert_images_frame=False
)
'''
def place_window(the_window):
"""
This function places the Window on the far right of the screen. this helps with the fact that when the preview
window is opened it's always on the top left
"""
the_window.update_idletasks() # this became necessary after installing everything with pip
width_window = the_window.winfo_width() # width for the Tk root
height_window = the_window.winfo_height() # height for the Tk root
# get screen width and height
width_screen = the_window.winfo_screenwidth() # width of the screen
height_screen = the_window.winfo_screenheight() # height of the screen
# calculate x and y coordinates for the Tk root window
x_position_window = width_screen - width_window
y_position_window = 0# hs - h
# set the dimensions of the screen
# and where it is placed
# UNCLEAR why this broke when installing everything with pip and opencv.
# Try to fix in the future. For now, just remove the placement call
the_window.geometry('%dx%d+%d+%d' % (width_window, height_window+30, x_position_window, y_position_window))
if __name__ == "__main__":
PiVR_app = PiVR()
if RASPBERRY:
place_window(PiVR_app)
PiVR_app.mainloop()