Example #1
0
class AvoidanceController(ArenaController.ArenaController):
    def __init__(self, parent, arenaMain):        
        super(AvoidanceController, self).__init__(parent, arenaMain)

        #calibration
        self.arenaCamCorners = []
        self.arenaMidLine = []
        self.arenaSide1Sign = 1
        self.arenaProjCorners = []
        self.fishImg = None #image of fish

        #tracking
        self.arenaCvMask = None   

        #state
        self.mutex = Lock()
        self.pulseMutex = Lock()
        self.nDispMode = 0
        self.bRunning = False
        self.currState = State.BETWEEN
        self.fishPosUpdate = False
        self.fishPos = (0,0)
        self.fishSize = None
        self.escapeLine= []
        self.escapeSign = []

        #shock pulse state - only if using labjack
        self.shockSide1_t = 0 #0 if not shocking, otherwise time of next shock
        self.bPulseHigh1 = False #true if shock_pulse in progress
        self.shockSide1_ndx = None
        self.shockSide2_t = 0 #0 if not shocking, otherwise time of next shock
        self.bPulseHigh2 = False #true if shock_pulse in progress
        self.shockSide2_ndx = None

        #tracking related images
        self.currCvFrame = None

        #init UI
        #arena info group box
        self.arenaGroup = QtGui.QGroupBox(self)
        self.arenaGroup.setTitle('Arena Info')
        self.arenaLayout = QtGui.QGridLayout()
        self.arenaLayout.setHorizontalSpacing(3)
        self.arenaLayout.setVerticalSpacing(3)
        self.cntrUI = QtGui.QWidget(self)
        self.cntrLayout = QtGui.QGridLayout()
        self.cntrLayout.setHorizontalSpacing(3)
        self.cntrLayout.setVerticalSpacing(3)
        l1 = QtGui.QLabel('Side1 Control Channel:')
        l2 = QtGui.QLabel('Side2 Control Channel:')
        l3 = QtGui.QLabel('Current Input Channel:')
        self.cntrSide1Channel = QtGui.QSpinBox()
        self.cntrSide1Channel.setValue(8)
        self.cntrSide1Channel.setMaximumWidth(50)
        self.cntrSide1Channel.setRange(0,8000)
        self.cntrSide2Channel = QtGui.QSpinBox()
        self.cntrSide2Channel.setValue(9)
        self.cntrSide2Channel.setMaximumWidth(50)
        self.cntrSide2Channel.setRange(0,8000)
        self.currInputChannel = QtGui.QSpinBox()
        self.currInputChannel.setValue(10)
        self.currInputChannel.setMaximumWidth(50)
        self.currInputChannel.setRange(0,8000)
        self.cntrLayout.addWidget(l1,0,0)
        self.cntrLayout.addWidget(self.cntrSide1Channel,0,1)
        self.cntrLayout.addWidget(l2,1,0)
        self.cntrLayout.addWidget(self.cntrSide2Channel,1,1)
        self.cntrLayout.addWidget(l3,2,0)
        self.cntrLayout.addWidget(self.currInputChannel,2,1)
        self.cntrLayout.setColumnStretch(2,1)
        self.cntrUI.setLayout(self.cntrLayout)
        self.camCalibButton = QtGui.QPushButton('Set Cam Position')
        self.camCalibButton.setMaximumWidth(150)
        self.camCalibButton.clicked.connect(self.getArenaCameraPosition)      
        self.projGroup = QtGui.QGroupBox(self.arenaGroup)
        self.projGroup.setTitle('Projector Position')
        self.projLayout = QtGui.QGridLayout(self.projGroup)
        self.projLayout.setHorizontalSpacing(3)
        self.projLayout.setVerticalSpacing(3)
        self.projCalibButton = QtGui.QPushButton('Calibrate Projector Position')
        self.projCalibButton.setCheckable(True)
        self.projCalibButton.clicked.connect(self.updateProjectorDisplay)
        self.projLayout.addWidget(self.projCalibButton,0,0,1,6)
        self.proj1L = QtGui.QLabel('C1')
        self.proj1X = QtGui.QSpinBox()
        self.proj1X.setValue(0)
        self.proj1X.setMaximumWidth(50)
        self.proj1X.setRange(0,2000)
        self.proj1Y = QtGui.QSpinBox()
        self.proj1Y.setValue(0)
        self.proj1Y.setMaximumWidth(50)
        self.proj1Y.setRange(0,2000)
        self.projLayout.addWidget(self.proj1L,1,0)
        self.projLayout.addWidget(self.proj1X,1,1)
        self.projLayout.addWidget(self.proj1Y,1,2)
        self.proj1X.valueChanged.connect(self.updateProjectorDisplay)
        self.proj1Y.valueChanged.connect(self.updateProjectorDisplay)
        self.proj2L = QtGui.QLabel('C2')
        self.proj2X = QtGui.QSpinBox()
        self.proj2X.setValue(0)
        self.proj2X.setMaximumWidth(50)
        self.proj2X.setRange(0,2000)
        self.proj2Y = QtGui.QSpinBox()
        self.proj2Y.setValue(100)
        self.proj2Y.setMaximumWidth(50)
        self.proj2Y.setRange(0,2000)
        self.proj2X.valueChanged.connect(self.updateProjectorDisplay)
        self.proj2Y.valueChanged.connect(self.updateProjectorDisplay)
        self.projLayout.addWidget(self.proj2L,2,0)
        self.projLayout.addWidget(self.proj2X,2,1)
        self.projLayout.addWidget(self.proj2Y,2,2)
        self.proj3L = QtGui.QLabel('C3')
        self.proj3X = QtGui.QSpinBox()
        self.proj3X.setValue(200)
        self.proj3X.setMaximumWidth(50)
        self.proj3X.setRange(0,2000)
        self.proj3Y = QtGui.QSpinBox()
        self.proj3Y.setValue(100)
        self.proj3Y.setMaximumWidth(50)
        self.proj3Y.setRange(0,2000)
        self.projLayout.addWidget(self.proj3L,2,3)
        self.projLayout.addWidget(self.proj3X,2,4)
        self.projLayout.addWidget(self.proj3Y,2,5)
        self.proj3X.valueChanged.connect(self.updateProjectorDisplay)
        self.proj3Y.valueChanged.connect(self.updateProjectorDisplay)
        self.proj4L = QtGui.QLabel('C4')
        self.proj4X = QtGui.QSpinBox()
        self.proj4X.setValue(200)
        self.proj4X.setMaximumWidth(50)
        self.proj4X.setRange(0,2000)
        self.proj4Y = QtGui.QSpinBox()
        self.proj4Y.setValue(0)
        self.proj4Y.setMaximumWidth(50)
        self.proj4Y.setRange(0,2000)
        self.projLayout.addWidget(self.proj4L,1,3)
        self.projLayout.addWidget(self.proj4X,1,4)
        self.projLayout.addWidget(self.proj4Y,1,5)
        self.proj4X.valueChanged.connect(self.updateProjectorDisplay)
        self.proj4Y.valueChanged.connect(self.updateProjectorDisplay)
        self.projLayout.setColumnStretch(6,1)
        self.projGroup.setLayout(self.projLayout)
        self.arenaLayout.addWidget(self.camCalibButton, 0,0)
        self.arenaLayout.addWidget(self.cntrUI,1,0)
        self.arenaLayout.addWidget(self.projGroup,2,0)
        self.arenaLayout.setColumnStretch(1,1)
        self.arenaGroup.setLayout(self.arenaLayout)

        #tracking group box
        self.trackWidget = FishTrackerWidget(self, self.arenaMain.ftDisp)

        #experimental parameters groupbox
        self.paramGroup = QtGui.QGroupBox()
        self.paramGroup.setTitle('Exprimental Parameters')
        self.paramLayout = QtGui.QGridLayout()
        self.paramLayout.setHorizontalSpacing(2)
        self.paramLayout.setVerticalSpacing(2)

        self.paramNumTrials = LabeledSpinBox(None,'trials',1,999,40,60)
        self.paramLayout.addWidget(self.paramNumTrials,1,0,1,2)
        self.paramAcc = LabeledSpinBox(None,'acclimation (s)',1,3600,900,60)
        self.paramLayout.addWidget(self.paramAcc,1,2,1,2)
        self.paramITIMin = LabeledSpinBox(None,'iti min (s)',1,300,15,60)
        self.paramLayout.addWidget(self.paramITIMin,2,0,1,2)
        self.paramITIMax = LabeledSpinBox(None,'iti max (s)',1,300,30,60)
        self.paramLayout.addWidget(self.paramITIMax,2,2,1,2)
        self.paramEscapeT = LabeledSpinBox(None,'escape (s)',1,60,10,60)
        self.paramLayout.addWidget(self.paramEscapeT,3,0,1,2)
        self.paramShockT = LabeledSpinBox(None,'shock (s)',1,100,30,60)
        self.paramLayout.addWidget(self.paramShockT,3,2,1,2)
        self.paramV = LabeledSpinBox(None,'volts',1,50,12,60)
        self.paramLayout.addWidget(self.paramV,4,0,1,2)
        self.paramShockPeriod = LabeledSpinBox(None,'shock period (ms)',1,5000,1000,60)
        self.paramLayout.addWidget(self.paramShockPeriod,4,2,1,2)
        self.paramShockDura = LabeledSpinBox(None,'shock t (ms)',1,1000,100,60)
        self.paramLayout.addWidget(self.paramShockDura,5,0,1,2)

        self.paramNeutral = QtGui.QComboBox()
        self.paramNeutral.addItem('White')
        self.paramNeutral.addItem('Red')
        self.paramNeutral.addItem('Blue')
        self.paramNeutral.addItem('Gray')
        self.paramNeutral.setCurrentIndex(0)
        self.labelNeutral = QtGui.QLabel('Neutral')
        self.paramLayout.addWidget(self.paramNeutral,5,2)
        self.paramLayout.addWidget(self.labelNeutral,5,3)

        self.paramAvoid = QtGui.QComboBox()
        self.paramAvoid.addItem('White')
        self.paramAvoid.addItem('Red')
        self.paramAvoid.addItem('Blue')
        self.paramAvoid.addItem('Gray')
        self.paramAvoid.setCurrentIndex(1)
        self.labelAvoid = QtGui.QLabel('Avoid')
        self.paramLayout.addWidget(self.paramAvoid,6,0)
        self.paramLayout.addWidget(self.labelAvoid,6,1)

        self.paramEscape = QtGui.QComboBox()
        self.paramEscape.addItem('White')
        self.paramEscape.addItem('Red')
        self.paramEscape.addItem('Blue')
        self.paramEscape.addItem('Gray')
        self.paramEscape.setCurrentIndex(2)
        self.labelEscape = QtGui.QLabel('Escape')
        self.paramLayout.addWidget(self.paramEscape,6,2)
        self.paramLayout.addWidget(self.labelEscape,6,3)

        self.paramDynamic= QtGui.QCheckBox('Dynamic Escape')
        self.paramLayout.addWidget(self.paramDynamic,7,0,1,2)
        
        self.paramEscapePos = QtGui.QDoubleSpinBox()
        self.paramEscapePos.setRange(0,1)
        self.paramEscapePos.setValue(.5)
        self.labelEscapePos = QtGui.QLabel('escape pos (0-1)')
        self.paramLayout.addWidget(self.paramEscapePos,7,2)
        self.paramLayout.addWidget(self.labelEscapePos,7,3)

        self.paramDynamicDraw = QtGui.QCheckBox('Dynamic Draw')
        self.paramDynamicDraw.setChecked(True)
        self.paramLayout.addWidget(self.paramDynamicDraw,8,0,1,2)

        self.paramDebug = QtGui.QPushButton('Debug')
        self.paramDebug.setMaximumWidth(150) 
        self.paramDebug.setCheckable(True)
        self.paramLayout.addWidget(self.paramDebug,8,2,1,2)
        self.paramDebug.clicked.connect(self.useDebugParams)

        self.paramGroup.setLayout(self.paramLayout)

        #Experimental info group
        self.infoGroup = QtGui.QGroupBox()
        self.infoGroup.setTitle('Experiment Info')
        self.infoLayout = QtGui.QGridLayout()
        self.infoLayout.setHorizontalSpacing(3)
        self.infoLayout.setVerticalSpacing(3)

        self.labelDir = QtGui.QLabel('Dir: ')
        self.infoDir = PathSelectorWidget(browseCaption='Experimental Data Directory')
        self.infoLayout.addWidget(self.labelDir,0,0)
        self.infoLayout.addWidget(self.infoDir,0,1)

        self.labelDOB = QtGui.QLabel('DOB: ')
        self.infoDOB = QtGui.QDateEdit(QtCore.QDate.currentDate())
        self.infoLayout.addWidget(self.labelDOB,1,0)
        self.infoLayout.addWidget(self.infoDOB,1,1)

        self.labelType = QtGui.QLabel('Line: ')
        self.infoType = QtGui.QLineEdit('Nacre')
        self.infoLayout.addWidget(self.labelType,2,0)
        self.infoLayout.addWidget(self.infoType,2,1)     

        self.infoFish = QtGui.QPushButton('Snap Fish Image')
        self.infoFish.clicked.connect(self.getFishSize)
        self.infoLayout.addWidget(self.infoFish,3,0,1,2)

        self.infoGroup.setLayout(self.infoLayout)

        self.settingsLayout = QtGui.QGridLayout()
        self.settingsLayout.addWidget(self.arenaGroup,0,0)
        self.settingsLayout.addWidget(self.trackWidget,1,0)
        self.settingsLayout.addWidget(self.paramGroup,2,0)
        self.settingsLayout.addWidget(self.infoGroup,3,0)
        self.setLayout(self.settingsLayout)


    #---------------------------------------------------
    # OVERLOADED METHODS
    #---------------------------------------------------

    #update to overload an return image based on dispmode.
    #def getArenaView(self):
    #    return self.arenaView

    def onNewFrame(self, frame, time):       
        self.mutex.acquire()
        try:
            self.frameTime = time
            self.currCvFrame = frame # may need to make a deep copy

            (found, pos, view, allFish) = self.trackWidget.findFish(self.currCvFrame)
            if found:
                self.fishPosUpdate = found
                self.fishPos = pos

            if self.bRunning:
                self.avoidData['video'].append((self.frameTime, None)) 
                self.avoidData['tracking'].append((self.frameTime, self.fishPos[0], self.fishPos[1]))

            self.arenaView = view
        except:
            print 'AvoidanceController:onNewFrame failed'
            print "Unexpected error:", sys.exc_info()[0]
        finally:
            self.mutex.release()

  
    def updateState(self):
        self.updateExperimentalState()
        self.handlePulsedShocks()

    def updateExperimentalState(self):
        if self.bRunning:
            self.mutex.acquire()
            try:
                t = time.time()
                #only update status bar if arena is selected.
                if self.isCurrent():
                    if self.nTrial>-1:
                        #print('NextTrial# %d TimeTilNextTrial %f TrialTime %f' % (self.nTrial+1, self.timeOfNextTrial - t, t - self.avoidData['trials'][self.nTrial]['startT']))
                        self.arenaMain.statusBar().showMessage('NextTrial# %d TimeTilNextTrial %f TrialTime %f' % (self.nTrial+1, self.timeOfNextTrial - t, t - self.avoidData['trials'][self.nTrial]['startT']))
                    else:
                        #print('NextTrial# %d TimeTilNextTrial %f' % (self.nTrial+1, self.timeOfNextTrial - t))                        
                        self.arenaMain.statusBar().showMessage('NextTrial# %d TimeTilNextTrial %f' % (self.nTrial+1, self.timeOfNextTrial - t))

                #check if fish escaped
                bDidEscape = False
                if self.fishPosUpdate:
                    self.fishPosUpdate = False
                    if self.escapeLine and (self.currState == State.LED or self.currState == State.SHOCK):
                        bDidEscape = self.isOnSide(self.fishPos, self.escapeLine, self.escapeSign)

                #handle State Changes
                ###BETWEEN###
                if self.currState==State.BETWEEN and t >= self.timeOfNextTrial: 
                    print 'Start Trial'
                    self.nTrial+=1
                    if self.nTrial < self.paramNumTrials.value():
                        self.currState = State.LED;
                        self.timeState = t;

                        #determine relative fish position (0->Side1 1->Side2) (by projecting onto 1 side)
                        ac = self.arenaCamCorners
                        lengC = ((self.fishPos[1]-ac[0][1])**2 + (self.fishPos[0] - ac[0][0])**2)**.5 #length of fish vector
                        lengB = ((ac[3][1]-ac[0][1])**2 + (ac[3][0]-ac[0][0])**2)**.5  #length of tank side
                        lengA = ((self.fishPos[1]-ac[3][1])**2 + (self.fishPos[0]-ac[3][0])**2)**.5  #length of connecting vector
                        #dot product = fish vector length * cos(angle between fish vector and tank side)/normalized by side length
                        relPos = lengC * (lengB**2 + lengC**2 - lengA**2)/(2*lengC*lengB)/lengB

                        #determine side of fish -- not quite the same as relpos<0.5 because corners 2 and 3 not considered in relpos
                        if self.isOnSide(self.fishPos, self.arenaMidLine, self.arenaSide1Sign):
                            self.trialSide = Side.S1
                        else:
                            self.trialSide = Side.S2

                        #determine relative escape position
                        if not self.paramDynamic.isChecked():
                            if self.trialSide == Side.S1:
                                self.escapePos = self.paramEscapePos.value()
                            else:
                                self.escapePos = 1 - self.paramEscapePos.value()
                        else:
                            if self.trialSide == Side.S1:
                                self.escapePos = relPos + self.paramEscapePos.value()
                            else:
                                self.escapePos = relPos - self.paramEscapePos.value()                            
 
                        #convert relative escape position into a line
                        (self.escapeLine, side1Sign) = self.processArenaCorners(self.arenaCamCorners, self.escapePos)
                        self.escapeSign = -1 * self.trialSide * self.arenaSide1Sign

                        self.updateProjectorDisplay()
                        self.avoidData['trials'].append({'trialNum':self.nTrial,
                                                         'startT':self.timeState,
                                                         'side':self.trialSide,
                                                         'escape': (self.escapeLine, self.escapeSign),
                                                         'endT':-1})
                    else:
                        #experiment is complete
                        self.stopController()
                ###LED###
                elif self.currState== State.LED:  
                    if bDidEscape:
                        print 'Avoided Shocks'
                        self.timeState = t
                        self.currState=State.BETWEEN;
                        self.timeOfNextTrial = self.timeState + random.randint(self.paramITIMin.value(),self.paramITIMax.value())
                        self.escapeLine = []
                        self.avoidData['trials'][self.nTrial]['endT'] = self.timeState
                        self.avoidData['trials'][self.nTrial]['bAvoidedShock'] = True
                        self.saveResults()
                    elif (t - self.timeState) > self.paramEscapeT.value(): 
                        print 'Starting Shocks'
                        self.timeState = t
                        self.currState= State.SHOCK;
                        self.avoidData['trials'][self.nTrial]['bAvoidedShock'] = False                    
                        self.setShockState(self.trialSide == Side.S1, self.trialSide == Side.S2)
                        self.avoidData['trials'][self.nTrial]['Shock_current_start'] = self.readCurrentInput()
                    self.updateProjectorDisplay()
                ###SHOCK###
                elif self.currState == State.SHOCK:
                    if bDidEscape or t - self.timeState > self.paramShockT.value():
                        print 'Ending Shocks'
                        self.currState=State.BETWEEN
                        self.timeOfNextTrial = self.timeState + random.randint(self.paramITIMin.value(),self.paramITIMax.value())
                        self.timeState = t;
                        self.escapeLine = []
                        self.setShockState(False, False)
                        self.updateProjectorDisplay()     
                        self.avoidData['trials'][self.nTrial]['endT'] = self.timeState
                        self.saveResults()
                #########
            except:

                print 'AvoidanceController:updateState failed'
                traceback.print_exc()
                QtCore.pyqtRemoveInputHook() 
                ipdb.set_trace()
            finally:
                self.mutex.release()

    def isReadyToStart(self):
        bReady =  os.path.exists(self.infoDir.text()) and self.trackWidget.getBackgroundImage() and self.fishImg and self.arenaCamCorners
        print bReady
        return bReady

    def drawProjectorDisplay(self, painter):
        if not self.bRunning and self.projCalibButton.isChecked():
            brush = QtGui.QBrush(QtCore.Qt.blue)
            pen = QtGui.QPen(QtCore.Qt.black)
            painter.setBrush(brush)
            painter.setPen(pen)
            poly = QtGui.QPolygonF()
            poly.append(QtCore.QPointF(self.proj1X.value(), self.proj1Y.value()))
            poly.append(QtCore.QPointF(self.proj2X.value(), self.proj2Y.value()))
            poly.append(QtCore.QPointF(self.proj3X.value(), self.proj3Y.value()))
            poly.append(QtCore.QPointF(self.proj4X.value(), self.proj4Y.value()))
            painter.drawPolygon(poly)
        else:
            if self.currState == State.BETWEEN:
                #Draw whole tank
                pen = QtGui.QPen(QtCore.Qt.NoPen)
                print self.paramNeutral.currentIndex()
                brush = self.getBrush(self.paramNeutral.currentIndex())
                painter.setBrush(brush)
                painter.setPen(pen)
                poly = QtGui.QPolygonF()
                poly.append(QtCore.QPointF(self.proj1X.value(), self.proj1Y.value()))
                poly.append(QtCore.QPointF(self.proj2X.value(), self.proj2Y.value()))
                poly.append(QtCore.QPointF(self.proj3X.value(), self.proj3Y.value()))
                poly.append(QtCore.QPointF(self.proj4X.value(), self.proj4Y.value()))
                painter.drawPolygon(poly)
            else:
                side1Color = side2Color = self.paramEscape.currentIndex()
                if self.trialSide == Side.S1:
                    side1Color = self.paramAvoid.currentIndex()
                else:
                    side2Color = self.paramAvoid.currentIndex()

                if self.paramDynamicDraw.isChecked():
                    a = float(1-self.escapePos)
                    b = float(self.escapePos)
                else:
                    a = 0.5
                    b = 0.5

                #Draw side one
                pen = QtGui.QPen(QtCore.Qt.NoPen)
                brush = self.getBrush(side1Color)
                painter.setBrush(brush)
                painter.setPen(pen)
                poly = QtGui.QPolygonF()
                poly.append(QtCore.QPointF(self.proj1X.value(), self.proj1Y.value()))
                poly.append(QtCore.QPointF(self.proj2X.value(), self.proj2Y.value()))
                poly.append(QtCore.QPointF(a*self.proj2X.value() + b*self.proj3X.value(), a*self.proj2Y.value() + b*self.proj3Y.value()))
                poly.append(QtCore.QPointF(a*self.proj1X.value() + b*self.proj4X.value(), a*self.proj1Y.value() + b*self.proj4Y.value()))
                painter.drawPolygon(poly)
                #Draw side two
                pen = QtGui.QPen(QtCore.Qt.NoPen)
                brush = self.getBrush(side2Color)
                painter.setBrush(brush)
                painter.setPen(pen)
                poly = QtGui.QPolygonF()
                poly.append(QtCore.QPointF(a*self.proj1X.value() + b*self.proj4X.value(), a*self.proj1Y.value() + b*self.proj4Y.value()))
                poly.append(QtCore.QPointF(a*self.proj2X.value() + b*self.proj3X.value(), a*self.proj2Y.value() + b*self.proj3Y.value()))
                poly.append(QtCore.QPointF(self.proj3X.value(), self.proj3Y.value()))
                poly.append(QtCore.QPointF(self.proj4X.value(), self.proj4Y.value()))
                painter.drawPolygon(poly)
                
                
    def drawDisplayOverlay(self, painter):
        #draw the fish position
        if self.fishPos and self.fishPos[0] > 0:
            brush = QtGui.QBrush(QtCore.Qt.red)
            painter.setBrush(brush)
            painter.setPen(QtCore.Qt.NoPen)
            painter.drawEllipse(QtCore.QPointF(self.fishPos[0],self.fishPos[1]), 3,3)

        #draw the arena overlay
        if self.arenaCamCorners:
            painter.setBrush(QtCore.Qt.NoBrush)
            if self.bIsCurrentArena:
                pen = QtGui.QPen(QtCore.Qt.green)
            else:
                pen = QtGui.QPen(QtCore.Qt.blue)
            pen.setWidth(3)
            painter.setPen(pen)
            poly = QtGui.QPolygonF()
            for p in self.arenaCamCorners:
                poly.append(QtCore.QPointF(p[0],p[1]))
            painter.drawPolygon(poly)

        #draw escape line
        if self.escapeLine:
            pen = QtGui.QPen()
            pen.setColor(QtCore.Qt.red)
            pen.setWidth(1)
            l = self.escapeLine
            painter.drawLine(QtCore.QPointF(l[0][0],l[0][1]), QtCore.QPointF(l[1][0],l[1][1]))            

    def start(self):
        self.mutex.acquire()
        try:
            self.paramGroup.setDisabled(True)
            self.infoGroup.setDisabled(True)
            self.initResults()
            
            #initialize experimental state
            self.nTrial = -1
            self.currState = State.BETWEEN
            self.setShockState(False,False)
            self.updateProjectorDisplay()
            self.timeState = time.time()
            self.timeOfNextTrial = self.timeState + self.paramAcc.value()
            self.trialSide = Side.S1
            self.escapeLine = []
            self.bRunning = True
        finally:
            self.mutex.release()

    def stop(self):
        self.mutex.acquire()
        try:
            self.stopController()
        finally:
            self.mutex.release()

    def stopController(self):
        if self.bRunning:
            self.bRunning = False
            self.saveResults()
            print 'Experiment stopped successfully.  Saved experimental data.'    
        self.currState = State.BETWEEN
        self.setShockState(False,False)
        self.nTrial = -1
        self.escapeLine = []
        self.updateProjectorDisplay()
        self.paramGroup.setDisabled(False)
        self.infoGroup.setDisabled(False)       

    #---------------------------------------------------
    # CALLBACK METHODS
    #---------------------------------------------------
    def getArenaCameraPosition(self):
        self.arenaMain.statusBar().showMessage('Click on the corners of the arena on side 1.')
        self.currArenaClick = 0
        self.arenaCamCorners = []
        self.arenaMain.ftDisp.clicked.connect(self.handleArenaClicks) 

    @QtCore.pyqtSlot(int, int) #not critical but could practice to specify which functions are slots.
    def handleArenaClicks(self, x, y):
        self.currArenaClick+=1
        if self.currArenaClick<5:
            self.arenaCamCorners.append((x,y))
            if self.currArenaClick==1:
                self.arenaMain.statusBar().showMessage('Click on the other corner of the arena on side 1.')
            elif self.currArenaClick==2:
                self.arenaMain.statusBar().showMessage('Now, click on the corners of the arena on side 2.')
            elif self.currArenaClick==3:
                self.arenaMain.statusBar().showMessage('Click on the other corner of the arena on side 2.')	
            elif self.currArenaClick==4:
                self.arenaMain.ftDisp.clicked.disconnect(self.handleArenaClicks)
                self.arenaMain.statusBar().showMessage('')
                [self.arenaMidLine, self.arenaSide1Sign] = self.processArenaCorners(self.arenaCamCorners, .5)
                self.getArenaMask()

    def getFishSize(self):
        self.arenaMain.statusBar().showMessage('Click on the tip of the fish tail.')
        self.currFishClick = 0
        self.fishSize = []
        self.arenaMain.ftDisp.clicked.connect(self.handleFishClicks) 		

    @QtCore.pyqtSlot(int, int) #not critical but could practice to specify which functions are slots.
    def handleFishClicks(self, x, y):
        self.currFishClick+=1
        if self.currFishClick == 1:
            self.fishSize.append((x,y))
            self.arenaMain.statusBar().showMessage('Click on the tip of the fish head.')
        elif self.currFishClick == 2:
            self.arenaMain.ftDisp.clicked.disconnect(self.handleFishClicks)
            self.fishSize.append((x,y))
            self.fishImg = cv.CloneImage(self.currCvFrame)
            self.arenaMain.statusBar().showMessage('')

    def useDebugParams(self):
        if self.paramDebug.isChecked():
            self.debugVals = {}
            self.debugVals['dn'] = self.paramNumTrials.value()
            self.debugVals['da'] = self.paramAcc.value()
            self.debugVals['ds'] = self.paramITIMin.value()
            self.debugVals['dl'] = self.paramITIMax.value()
            self.debugVals['de'] = self.paramEscapeT.value()
            self.debugVals['dt'] = self.paramShockT.value()
            self.debugVals['dv'] = self.paramV.value()
            self.paramNumTrials.setValue(5)
            self.paramAcc.setValue(5)
            self.paramITIMin.setValue(3)
            self.paramITIMax.setValue(3)
            self.paramEscapeT.setValue(5)
            self.paramShockT.setValue(5)
            self.paramV.setValue(0)              
        else:
            self.paramNumTrials.setValue(self.debugVals['dn'])
            self.paramAcc.setValue(self.debugVals['da'])
            self.paramITIMin.setValue(self.debugVals['ds'])
            self.paramITIMax.setValue(self.debugVals['dl'])
            self.paramEscapeT.setValue(self.debugVals['de'])
            self.paramShockT.setValue(self.debugVals['dt'])
            self.paramV.setValue(self.debugVals['dv'])            

    #---------------------------------------------------
    # HELPER METHODS
    #---------------------------------------------------

#    def getBackgroundImage(self):
#        if self.currCvFrame:
#            self.bcvImg = cv.CloneImage(self.currCvFrame) 
#            self.trackWidget.setBackgroundImage(self.bcvImg)

    #convert the arena corners into a color mask image (arena=255, not=0)    
    def getArenaMask(self): 
        if self.arenaView:
            cvImg = self.currCvFrame
            self.arenaCvMask = cv.CreateImage((cvImg.width,cvImg.height), cvImg.depth, cvImg.channels) 
            cv.SetZero(self.arenaCvMask)
            cv.FillConvexPoly(self.arenaCvMask, self.arenaCamCorners, (255,)*cvImg.channels)	
            self.trackWidget.setTrackMask(self.arenaCvMask)

    def initResults(self):
        #prepare output data structure
        self.avoidData = {}
        self.avoidData['fishbirthday'] = str(self.infoDOB.date().toPyDate())
        self.avoidData['fishage'] =  (datetime.date.today() - self.infoDOB.date().toPyDate()).days
        self.avoidData['fishstrain'] = str(self.infoType.text())
        self.avoidData['fishsize'] = self.fishSize
        self.avoidData['parameters'] = { 'numtrials':self.paramNumTrials.value(),
                                         'LEDTimeMS':self.paramEscapeT.value(),
                                         'ShockTimeMS':self.paramShockT.value(),
                                         'ShockV':self.paramV.value(),
                                         'AcclimationTime':self.paramAcc.value(),
                                         'MinTrialInterval':self.paramITIMin.value(),
                                         'MaxTrialInterval':self.paramITIMax.value(),
                                         'ShockPeriodT':self.paramShockPeriod.value(),
                                         'ShockDurationT':self.paramShockDura.value(),
                                         'NeuralStimulus':str(self.paramNeutral.currentText()),
                                         'AvoidStimulus':str(self.paramAvoid.currentText()),
                                         'EscapeStimulus':str(self.paramEscape.currentText()),
                                         'isDynamicEscape':self.paramDynamic.isChecked(),
                                         'inDynamicDraw':self.paramDynamicDraw.isChecked(),
                                         'fEscapePosition':self.paramEscapePos.value(),
                                         'CodeVersion':None }
        self.avoidData['trackingParameters'] = self.trackWidget.getParameterDictionary()
        self.avoidData['trackingParameters']['arenaPoly'] = self.arenaCamCorners 
        self.avoidData['trackingParameters']['arenaDivideLine'] = self.arenaMidLine
        self.avoidData['trackingParameters']['arenaSide1Sign'] = self.arenaSide1Sign
        self.avoidData['trials'] = list() #outcome on each trial
        self.avoidData['tracking'] = list() #list of tuples (frametime, posx, posy)
        self.avoidData['video'] = list() #list of tuples (frametime, filename)	
        self.avoidData['current'] = list() #list of tuples (time, currentValue)
        self.avoidData['shockPulses'] = list() #list of tuples (side, target_start, actual_start, target_end, actual_end)

        #get experiment filename
        [p, self.fnResults] = os.path.split(str(self.infoDir.text()))
        self.fnResults = self.fnResults + '_' + datetime.datetime.now().strftime('%Y-%m-%d-%H-%M-%S')
        self.jsonFileName = str(self.infoDir.text()) + os.sep + self.fnResults  + '.json'

        #save experiment images
        self.bcvImgFileName = str(self.infoDir.text()) + os.sep + self.fnResults  + '_BackImg.tiff'
        cv.SaveImage(self.bcvImgFileName, self.trackWidget.getBackgroundImage())	
        self.fishImgFileName = str(self.infoDir.text()) + os.sep +  self.fnResults + '_FishImg.tiff'
        cv.SaveImage(self.fishImgFileName, self.fishImg)


    def processArenaCorners(self, arenaCorners, linePosition):
        #return the line dividing the center of the arena, and a definition of side 1.
        a = 1-linePosition
        b = linePosition
        ac = np.array(arenaCorners)
        #arenaDivideLine = [tuple(np.mean(ac[(0,3),:],axis = 0)),tuple(np.mean(ac[(1,2),:],axis = 0))]
        arenaDivideLine = [(a*ac[0,0]+b*ac[3,0], a*ac[0,1]+b*ac[3,1]),(a*ac[1,0]+b*ac[2,0], a*ac[1,1]+b*ac[2,1])]
        side1Sign = 1
        if not self.isOnSide(arenaCorners[1], arenaDivideLine, side1Sign):
            side1Sign = -1	
        return (arenaDivideLine, side1Sign)

    def isOnSide(self, point, line, sideSign):
        #return if the fish is on side1 of the arena.   
        side = (line[1][0] - line[0][0]) * (point[1] - line[0][1]) - (line[1][1] - line[0][1]) * (point[0] - line[0][0])
        return cmp(side,0)==sideSign  

    def saveResults(self):
        f = open(name=self.jsonFileName, mode='w')
        json.dump(self.avoidData,f)
        f.close()

    def getBrush(self, colorNdx):
        if colorNdx == 0:
            return QtGui.QBrush(QtCore.Qt.white)
        elif colorNdx == 1:
             return QtGui.QBrush(QtCore.Qt.red)           
        elif colorNdx == 2:
             return QtGui.QBrush(QtCore.Qt.blue)           
        elif colorNdx == 3:
             return QtGui.QBrush(QtGui.QColor(128,128,128))  
        else:
            return QtGui.QBrush(QtCore.Qt.black)

    def setShockState(self, bSide1, bSide2):
        if bSide1:
            self.arenaMain.ard.pinPulse(self.cntrSide1Channel.value(), 
                                        self.paramShockPeriod.value(), 
                                        self.paramShockDura.value())
        else:
            self.arenaMain.ard.pinLow(self.cntrSide1Channel.value())
        if bSide2:
            self.arenaMain.ard.pinPulse(self.cntrSide2Channel.value(), 
                                           self.paramShockPeriod.value(), 
                                           self.paramShockDura.value())
        else:
            self.arenaMain.ard.pinLow(self.cntrSide2Channel.value())                   
        """
        elif bLabJack:
            self.pulseMutex.acquire()
            try:
                if bSide1 and not self.shockSide1_t:
                    self.shockSide1_t = time.time()
                    self.bPulseHigh1 = False
                elif not bSide1 and self.shockSide1_t:
                    if self.bPulseHigh1:
                        self.arenaMain.labjack.writeRegister(self.cntrSide1Channel.value(), 0)
                        if self.bRunning: self.avoidData['shockPulses'][self.shockSide1_ndx][3] = self.shockSide1_t + self.paramShockDura.value()/1000
                        if self.bRunning: self.avoidData['shockPulses'][self.shockSide1_ndx][4] = time.time()
                    self.bPulseHigh1 = False
                    self.shockSide1_ndx = None
                    self.shockSide1_t = 0  

                if bSide2 and not self.shockSide2_t:
                    self.shockSide2_t = time.time()
                    self.bPulseHigh2 = False
                elif not bSide2 and self.shockSide2_t:
                    if self.bPulseHigh2:
                        self.arenaMain.labjack.writeRegister(self.cntrSide2Channel.value(), 0)
                        if self.bRunning: self.avoidData['shockPulses'][self.shockSide2_ndx][3] = self.shockSide2_t + self.paramShockDura.value()/1000
                        if self.bRunning: self.avoidData['shockPulses'][self.shockSide2_ndx][4] = time.time()
                    self.bPulseHigh2 = False
                    self.shockSide2_ndx = None
                    self.shockSide2_t = 0               
            finally:
                self.pulseMutex.release()
        """

    def handlePulsedShocks(self):
        pass
        """
        self.pulseMutex.acquire()
        try:
            t = time.time()
            if self.shockSide1_t:
                if not self.bPulseHigh1 and t > self.shockSide1_t:
                    self.arenaMain.labjack.writeRegister(self.cntrSide1Channel.value(), 5)
                    if self.bRunning: self.avoidData['shockPulses'].append([1, self.shockSide1_s, t, -1, -1])
                    self.bPulseHigh1 = True
                    self.shockSide1_ndx = len(self.avoidData['shockPulses'])-1
                elif t > self.shockSide1_t + self.paramShockDura.value()/1000:
                    self.arenaMain.labjack.writeRegister(self.cntrSide1Channel.value(), 0)
                    if self.bRunning: self.avoidData['shockPulses'][self.shockSide1_ndx][3] = self.shockSide1_t + self.paramShockDura.value()/1000
                    if self.bRunning: self.avoidData['shockPulses'][self.shockSide1_ndx][4] = t
                    self.shockSide1_t = self.shockSide1_t + self.paramShockPeriod.value()/1000
                    self.bPulseHigh1 = False
                    self.shockSide1_ndx = None
            if self.shockSide2_t:
                if not self.bPulseHigh2 and t > self.shockSide2_t:
                    self.arenaMain.labjack.writeRegister(self.cntrSide2Channel.value(), 5)
                    if self.bRunning: self.avoidData['shockPulses'].append([2, self.shockSide2_s, t, -1, -1])
                    self.bPulseHigh2 = True
                    self.shockSide2_ndx = len(self.avoidData['shockPulses'])-1
                elif t > self.shockSide2_t + self.paramShockDura.value()/1000:
                    self.arenaMain.labjack.writeRegister(self.cntrSide2Channel.value(), 0)
                    if self.bRunning: self.avoidData['shockPulses'][self.shockSide2_ndx][3] = self.shockSide2_t + self.paramShockDura.value()/1000
                    if self.bRunning: self.avoidData['shockPulses'][self.shockSide2_ndx][4] = t
                    self.shockSide2_t = self.shockSide2_t + self.paramShockPeriod.value()/1000
                    self.bPulseHigh2 = False
                    self.shockSide2_ndx = None
        finally:
            self.pulseMutex.release()
        """

    def readCurrentInput(self):
        return None
        #self.arenaMain.ard.analogRead(self.currInputChannel.value())
        """