Esempio n. 1
0
class CocaineController(ArenaController.ArenaController):
    def __init__(self, parent, arenaMain):        
        super(CocaineController, 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.arenaData = None
        self.currState = State.OFF
        self.currRun = -1 #a run is started each time Start Switches is pressed
        self.currSwitch = 0
        self.lastSwitchTime = 0
        self.fishPosUpdate = False
        self.fishPos = (0,0)
        self.allFish = []
        self.fishSize = 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.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.setRange(0,1000)
        self.proj1X.setValue(0)
        self.proj1X.setMaximumWidth(50)
        self.proj1Y = QtGui.QSpinBox()
        self.proj1Y.setValue(0)
        self.proj1Y.setMaximumWidth(50)
        self.proj1Y.setRange(0,1000)
        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.setRange(0,1000)
        self.proj2X.setValue(0)
        self.proj2X.setMaximumWidth(50)
        self.proj2Y = QtGui.QSpinBox()
        self.proj2Y.setRange(0,1000)
        self.proj2Y.setValue(400)
        self.proj2Y.setMaximumWidth(50)
        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.setRange(0,1000)
        self.proj3X.setValue(848)
        self.proj3X.setMaximumWidth(50)
        self.proj3Y = QtGui.QSpinBox()
        self.proj3Y.setRange(0,1000)
        self.proj3Y.setValue(400)
        self.proj3Y.setMaximumWidth(50)
        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.setRange(0,1000)
        self.proj4X.setValue(848)
        self.proj4X.setMaximumWidth(50)
        self.proj4Y = QtGui.QSpinBox()
        self.proj4Y.setRange(0,1000)
        self.proj4Y.setValue(0)
        self.proj4Y.setMaximumWidth(50)
        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.projGroup,1,0)
        self.arenaLayout.setColumnStretch(1,1)
        self.arenaGroup.setLayout(self.arenaLayout)

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

        self.startButton = QtGui.QPushButton('Start Switches')
        self.startButton.setMaximumWidth(150)
        self.startButton.setCheckable(True)
        self.startButton.clicked.connect(self.startSwitches)      

        #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.paramNumSwitches = LabeledSpinBox(None,'NumSwitches',0,100,1,60)
        self.paramLayout.addWidget(self.paramNumSwitches,1,0,1,2)
        self.paramSwitchTime = LabeledSpinBox(None,'SwitchTime (s)',1,3600,300,60)
        self.paramLayout.addWidget(self.paramSwitchTime,1,2,1,2)

        self.paramCond = QtGui.QComboBox()
        self.paramCond.addItem('Pre Train')
        self.paramCond.addItem('Cocaine')
        self.paramCond.addItem('Neutral')
        self.paramCond.addItem('Post Train')
        self.paramCond.setCurrentIndex(0)
        self.labelCond = QtGui.QLabel('Condition')
        self.paramLayout.addWidget(self.paramCond,2,0)
        self.paramLayout.addWidget(self.labelCond,2,1)

        self.paramColor1 = QtGui.QComboBox()
        self.paramColor1.addItem('White')
        self.paramColor1.addItem('Red')
        self.paramColor1.addItem('Blue')
        self.paramColor1.addItem('Gray')
        self.paramColor1.setCurrentIndex(1)
        self.labelColor1 = QtGui.QLabel('Color1')
        self.paramLayout.addWidget(self.paramColor1,2,2)
        self.paramLayout.addWidget(self.labelColor1,2,3)

        self.paramColor2 = QtGui.QComboBox()
        self.paramColor2.addItem('White')
        self.paramColor2.addItem('Red')
        self.paramColor2.addItem('Blue')
        self.paramColor2.addItem('Gray')
        self.paramColor2.setCurrentIndex(2)
        self.labelColor2 = QtGui.QLabel('Color2')
        self.paramLayout.addWidget(self.paramColor2,3,0)
        self.paramLayout.addWidget(self.labelColor2,3,1)

        self.paramOffColor = QtGui.QComboBox()
        self.paramOffColor.addItem('White')
        self.paramOffColor.addItem('Red')
        self.paramOffColor.addItem('Blue')
        self.paramOffColor.addItem('Gray')
        self.paramOffColor.setCurrentIndex(0)
        self.labelOffColor = QtGui.QLabel('OffColor')
        self.paramLayout.addWidget(self.paramOffColor,3,2)
        self.paramLayout.addWidget(self.labelOffColor,3,3)
        
        self.paramDrugColor = QtGui.QComboBox()
        self.paramDrugColor.addItem('White')
        self.paramDrugColor.addItem('Red')
        self.paramDrugColor.addItem('Blue')
        self.paramDrugColor.addItem('Gray')
        self.paramDrugColor.setCurrentIndex(0)
        self.labelDrugColor = QtGui.QLabel('DrugColor')
        self.paramLayout.addWidget(self.paramDrugColor,4,0)
        self.paramLayout.addWidget(self.labelDrugColor,4,1)
        
        self.paramConc = QtGui.QDoubleSpinBox()
        self.paramConc.setRange(0.0,1000.0)
        self.paramConc.setValue(10.0)
        self.labelConc = QtGui.QLabel('Conc mg/L')
        self.paramLayout.addWidget(self.paramConc,5,0)
        self.paramLayout.addWidget(self.labelConc,5,1)

        self.paramNumFish = LabeledSpinBox(None,'NumFish',1,10,1,60)
        self.paramLayout.addWidget(self.paramNumFish,5,2,1,2)

        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.startButton,2,0)
        self.settingsLayout.addWidget(self.paramGroup,3,0)
        self.settingsLayout.addWidget(self.infoGroup,4,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
                self.allFish = allFish

            if not self.currState == State.OFF:
                self.arenaData['runs'][self.currRun]['video'].append((self.frameTime, None)) 
#                if self.paramNumFish.value() == 1:
#                    self.arenaData['runs'][self.currRun]['tracking'].append((self.frameTime, self.fishPos[0], self.fishPos[1]))
#                else:
                d = [self.frameTime, pos[0], pos[1]]
                for nFish in range(1, min(self.paramNumFish.value(),len(allFish))):
                    d.append(allFish[nFish][0])
                    d.append(allFish[nFish][1])
#                print d
                self.arenaData['runs'][self.currRun]['tracking'].append(tuple(d))

            self.arenaView = view
        except:
            print 'CocaineController:onNewFrame failed'
            traceback.print_exc()
            QtCore.pyqtRemoveInputHook() 
            ipdb.set_trace()
        finally:
            self.mutex.release()

  
    def updateState(self):
        self.updateExperimentalState()

    def updateExperimentalState(self):
        if not self.currState == State.OFF:
            self.mutex.acquire()
            try:
                t = time.time()
                #only update status bar if arena is selected.
                if self.isCurrent():
                    self.arenaMain.statusBar().showMessage('SwitchesCompleted: %d TimeSinceSwitch: %f' % (self.currSwitch, t - self.lastSwitchTime))

                #handle State Changes
                if t >= self.lastSwitchTime + self.paramSwitchTime.value():
                    self.currSwitch += 1
                    self.lastSwitchTime = t
                    if self.currSwitch > self.paramNumSwitches.value():
                        self.currState = State.OFF
                        self.arenaData['runs'][self.currRun]['endTime'] = t
                        self.startButton.setText('Start Switches')
                        self.startButton.setChecked(False)
                        self.paramGroup.setDisabled(False)
                        self.infoGroup.setDisabled(False)
                    self.updateProjectorDisplay()
                    self.arenaData['runs'][self.currRun]['switchTimes'].append(t)
                    self.saveResults()
            except:
                print 'CocaineController:updateState failed'
                traceback.print_exc()
                QtCore.pyqtRemoveInputHook() 
                ipdb.set_trace()
            finally:
                self.mutex.release()

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

    def drawProjectorDisplay(self, painter):
        if self.currState == State.OFF 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.OFF:
                #Draw whole tank
                pen = QtGui.QPen(QtCore.Qt.NoPen)
                brush = self.getBrush(self.paramOffColor.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 = self.paramColor1.currentIndex()
                side2Color = self.paramColor2.currentIndex()
                if self.currSwitch%2 == 1:
                    side1Color = self.paramColor2.currentIndex()
                    side2Color = self.paramColor1.currentIndex()
                a = .5
                b = 1-a
                #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)
            for nFish in range(min(len(self.allFish),self.paramNumFish.value())):
                painter.drawEllipse(QtCore.QPointF(self.allFish[nFish][0],self.allFish[nFish][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)

    def start(self):
        #Global start button not used.
        pass

    def stop(self):
        #Global stop button not used.
        pass

    #---------------------------------------------------
    # CALLBACK METHODS
    #---------------------------------------------------
    def startSwitches(self):
        if self.currState == State.OFF:
            if self.isReadyToStart():
                self.paramGroup.setDisabled(True)
                self.infoGroup.setDisabled(True)
                if self.arenaData == None or not str(self.infoDir.text()) == self.saveLocation:
                    td = datetime.datetime.now()
                    self.saveLocation = str(self.infoDir.text())
                    [p, self.fnResults] = os.path.split(self.saveLocation)
                    self.fnResults = self.fnResults + '_' + td.strftime('%Y-%m-%d-%H-%M-%S')
                    self.jsonFileName = str(self.infoDir.text()) + os.sep + self.fnResults  + '.json'
                    self.arenaData = {}
                    self.arenaData['runs'] = list()
                self.initNewRunData()

                t = time.time()
                self.currState = State.RUNNING
                self.lastSwitchTime = t
                self.arenaData['runs'][self.currRun]['startTime'] = t
                self.currSwitch = 0
                self.startButton.setText('Stop')
            else:
                self.arenaMain.statusBar().showMessage('Arena not ready to start.  Information is missing.')
        else: 
            self.mutex.acquire()
            try:
                t = time.time()
                self.currState = State.OFF
                self.arenaData['runs'][self.currRun]['endTime'] = t
                self.arenaData['runs'][self.currRun]['switchTimes'].append(t)
                self.saveResults()
                self.startButton.setText('Start Switches')
                self.paramGroup.setDisabled(False)
                self.infoGroup.setDisabled(False)
            except:
                print 'stop switches failed'
                print "Unexpected error:", sys.exc_info()[0]
            finally:
                self.mutex.release()            
        self.updateProjectorDisplay()
        
    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('')

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

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

    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  

    #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 initNewRunData(self):
        #prepare output data structure
        self.arenaData['runs'].append({})
        cr = self.currRun = len(self.arenaData['runs']) - 1
        self.arenaData['runs'][cr]['fishbirthday'] = str(self.infoDOB.date().toPyDate())
        self.arenaData['runs'][cr]['fishage'] =  (datetime.date.today() - self.infoDOB.date().toPyDate()).days
        self.arenaData['runs'][cr]['fishstrain'] = str(self.infoType.text())
        self.arenaData['runs'][cr]['fishsize'] = self.fishSize
        self.arenaData['runs'][cr]['parameters'] = { 'numSwitches':self.paramNumSwitches.value(),
                                                     'SwitchDuration':self.paramSwitchTime.value(),
                                                     'Cond':str(self.paramCond.currentText()),
                                                     'Conc':str(self.paramConc.value()),
                                                     'Color1':str(self.paramColor1.currentText()),
                                                     'Color2':str(self.paramColor2.currentText()),
                                                     'OffColor':str(self.paramOffColor.currentText()),
                                                     'DrugColor':str(self.paramDrugColor.currentText()),
                                                     'CodeVersion':None }
        self.arenaData['runs'][cr]['trackingParameters'] = self.trackWidget.getParameterDictionary()
        self.arenaData['runs'][cr]['trackingParameters']['arenaPoly'] = self.arenaCamCorners 
        self.arenaData['runs'][cr]['trackingParameters']['arenaDivideLine'] = self.arenaMidLine
        self.arenaData['runs'][cr]['trackingParameters']['arenaSide1Sign'] = self.arenaSide1Sign
        self.arenaData['runs'][cr]['tracking'] = list() #list of tuples (frametime, posx, posy)
        self.arenaData['runs'][cr]['video'] = list() #list of tuples (frametime, filename)	
        self.arenaData['runs'][cr]['switchTimes'] = list() #list of times at switch stimulus flipped.
        self.arenaData['runs'][cr]['startTime'] = None
        self.arenaData['runs'][cr]['endTime'] = None

        t = datetime.datetime.now()
        #save experiment images
        self.bcvImgFileName = str(self.infoDir.text()) + os.sep + self.fnResults  + '_BackImg_' + t.strftime('%Y-%m-%d-%H-%M-%S') + '.tiff'
        cv.SaveImage(self.bcvImgFileName, self.trackWidget.getBackgroundImage())	
        self.fishImgFileName = str(self.infoDir.text()) + os.sep +  self.fnResults + '_FishImg_' + t.strftime('%Y-%m-%d-%H-%M-%S') + '.tiff'
        cv.SaveImage(self.fishImgFileName, self.fishImg)

    def saveResults(self):
        f = open(name=self.jsonFileName, mode='w')
        json.dump(self.arenaData,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)
class RealTimeShockController(ArenaController.ArenaController):

    def __init__(self, parent, arenaMain):        
        super(RealTimeShockController, 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.arenaData = None
        self.currState = State.OFF
        self.fishPosUpdate = False
        self.fishPos = (0,0)
        self.allFish = []
        self.fishSize = 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.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.setRange(0,1000)
        self.proj1X.setValue(0)
        self.proj1X.setMaximumWidth(50)
        self.proj1Y = QtGui.QSpinBox()
        self.proj1Y.setValue(0)
        self.proj1Y.setMaximumWidth(50)
        self.proj1Y.setRange(0,1000)
        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.setRange(0,1000)
        self.proj2X.setValue(0)
        self.proj2X.setMaximumWidth(50)
        self.proj2Y = QtGui.QSpinBox()
        self.proj2Y.setRange(0,1000)
        self.proj2Y.setValue(400)
        self.proj2Y.setMaximumWidth(50)
        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.setRange(0,1000)
        self.proj3X.setValue(848)
        self.proj3X.setMaximumWidth(50)
        self.proj3Y = QtGui.QSpinBox()
        self.proj3Y.setRange(0,1000)
        self.proj3Y.setValue(400)
        self.proj3Y.setMaximumWidth(50)
        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.setRange(0,1000)
        self.proj4X.setValue(848)
        self.proj4X.setMaximumWidth(50)
        self.proj4Y = QtGui.QSpinBox()
        self.proj4Y.setRange(0,1000)
        self.proj4Y.setValue(0)
        self.proj4Y.setMaximumWidth(50)
        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.projGroup,1,0)
        self.arenaLayout.setColumnStretch(1,1)
        self.arenaGroup.setLayout(self.arenaLayout)

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

        self.startButton = QtGui.QPushButton('Start')
        self.startButton.setMaximumWidth(150)
        self.startButton.setCheckable(True)
        self.startButton.clicked.connect(self.startSwitches)
      
        #experimental parameters groupbox
        self.paramGroup = QtGui.QGroupBox()
        self.paramGroup.setTitle('Exprimental Parameters')
        self.paramLayout = QtGui.QGridLayout()
        self.paramLayout.setHorizontalSpacing(2)
        self.paramLayout.setVerticalSpacing(2)

        #parameters
        self.paramAcclimate = LabeledSpinBox(None, 'Acclimate (m)', 0, 180, 30, 60)
        self.paramLayout.addWidget(self.paramAcclimate, 0,0,1,2)
        self.paramNumShockBlocks = LabeledSpinBox(None,'NumShockBlocks',0,100,4,60) #number of side switches pre test
        self.paramLayout.addWidget(self.paramNumShockBlocks,0,2,1,2)
        self.paramShockingTime = LabeledSpinBox(None,'ShockingTime (s)',1,3600,60,60) #duration before two sides swap colors during pre and post
        self.paramLayout.addWidget(self.paramShockingTime,1,0,1,2)
        self.paramBetweenTime = LabeledSpinBox(None,'BetweenTime (s)',0,3600,30,60) #time between pre , train and post periods
        self.paramLayout.addWidget(self.paramBetweenTime,1,2,1,2)
        self.paramShockPeriod = LabeledSpinBox(None, 'Shock Period (ms)', 0,5000,1000,60)
        self.paramLayout.addWidget(self.paramShockPeriod, 2,0,1,2)
        self.paramShockDuration = LabeledSpinBox(None, 'ShockDuration (ms)', 0,1000,50,60)
        self.paramLayout.addWidget(self.paramShockDuration, 2,2,1,2)
        self.paramShockChan1 = LabeledSpinBox(None, 'ShockChan1', 0,10000,12,60)
        self.paramLayout.addWidget(self.paramShockChan1, 3,0,1,2)
        self.paramShockChan2 = LabeledSpinBox(None, 'ShockChan2', 0,10000,13,60)
        self.paramLayout.addWidget(self.paramShockChan2, 3,2,1,2)
        self.paramCurrChan1 = LabeledSpinBox(None, 'CurrChan Side 1', 0,16,0,60)
        self.paramLayout.addWidget(self.paramCurrChan1,4,0,1,2)
        self.paramCurrChan2 = LabeledSpinBox(None, 'CurrChan Side 2', 0,16,0,60)
        self.paramLayout.addWidget(self.paramCurrChan2,4,2,1,2)
        self.paramShockV = LabeledSpinBox(None, 'ShockV', 0,100, 10,60)
        self.paramLayout.addWidget(self.paramShockV,5,0,1,2)
        self.paramNumFish = LabeledSpinBox(None,'NumFish',1,10,1,60)
        self.paramLayout.addWidget(self.paramNumFish,5,2,1,2)
        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.startButton,0,0,1,1)
        self.settingsLayout.addWidget(self.arenaGroup,2,0,1,2)
        self.settingsLayout.addWidget(self.trackWidget,3,0,1,2)
        self.settingsLayout.addWidget(self.paramGroup,4,0,1,2)
        self.settingsLayout.addWidget(self.infoGroup,5,0,1,2)
        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
                self.allFish = allFish

            if not self.currState == State.OFF:
                self.arenaData['video'].append((self.frameTime, None)) 
                d = [self.frameTime, pos[0], pos[1]]
                for nFish in range(1, min(self.paramNumFish.value(),len(allFish))):
                    d.append(allFish[nFish][0])
                    d.append(allFish[nFish][1])
                self.arenaData['tracking'].append(tuple(d))

            self.arenaView = view
        except:
            print 'onNewFrame failed'
            traceback.print_exc()
            QtCore.pyqtRemoveInputHook() 
            ipdb.set_trace()
        finally:
            self.mutex.release()

  
    def updateState(self):
        self.updateExperimentalState()

    def updateExperimentalState(self):
        if not self.currState == State.OFF:
            self.mutex.acquire()
            try:
                t = time.time()
                #only update status bar if arena is selected.
                if self.isCurrent():
                    self.arenaMain.statusBar().showMessage('Running: Block:%d/%d, CurrState=%d, TimeToNextState=%f, Side=%d'%(self.numBlocks,self.paramNumShockBlocks.value(),self.currState,self.nextStateTime-t, self.currShockSide))
                if t > self.nextStateTime:
                    self.setShockState(False, False)
                    if (self.currState == State.ACCLIMATE or
                        self.currState == State.BETWEEN or
                        self.paramBetweenTime.value() == 0):
                        if self.numBlocks < self.paramNumShockBlocks.value():
                            if self.currShockSide == 2:
                                self.currState = State.SHOCKING
                                self.setShockState(True,False)
                                self.arenaData['stateinfo'].append((t, self.currState, 'On','Off'))
                                self.currShockSide = 1
                                print 'SHOCKING SIDE1'
                            else:
                                self.currState = State.SHOCKING
                                self.setShockState(False,True)
                                self.arenaData['stateinfo'].append((t, self.currState, 'Off','On'))
                                self.currShockSide = 2
                                print 'SHOCKING SIDE2'
                            self.nextStateTime = t + self.paramShockingTime.value()
                            self.numBlocks+=1
                        else:
                            self.currState = State.OFF
                            self.arenaData['stateinfo'].append((t, self.currState, 'Off','Off'))
                            self.startButton.setText('Start Switches')
                            self.startButton.setChecked(False)
                            self.paramGroup.setDisabled(False)
                            self.infoGroup.setDisabled(False)
                            self.saveResults()
                            print 'DONE'
                    elif self.paramBetweenTime.value() > 0:
                        self.currState = State.BETWEEN
                        self.nextStateTime = t + self.paramBetweenTime.value()
                        self.arenaData['stateinfo'].append((t, self.currState, 'Off','Off'))
                        print 'BETWEEN'
                    self.updateProjectorDisplay()
                    self.saveResults()
            except:
                print ':updateState failed'
                traceback.print_exc()
                QtCore.pyqtRemoveInputHook() 
                ipdb.set_trace()
            finally:
                self.mutex.release()

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

    def drawProjectorDisplay(self, painter):
        if self.currState == State.OFF and self.projCalibButton.isChecked() and self.isCurrent():
            a = .5
            b = 1-a
            #Draw side one
            pen = QtGui.QPen(QtCore.Qt.NoPen)
            brush = QtGui.QBrush(QtCore.Qt.red)
            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)
            painter.drawText(self.proj1X.value(),self.proj1Y.value(),'1')
            #Draw side two
            pen = QtGui.QPen(QtCore.Qt.NoPen)
            brush = QtGui.QBrush(QtCore.Qt.blue)
            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)
            for nFish in range(min(len(self.allFish),self.paramNumFish.value())):
                painter.drawEllipse(QtCore.QPointF(self.allFish[nFish][0],self.allFish[nFish][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)
            if len(self.arenaCamCorners) >= 2:
                pen = QtGui.QPen(QtCore.Qt.red)
                pen.setWidth(2)
                painter.setPen(pen)
                painter.drawLine(self.arenaCamCorners[0][0],self.arenaCamCorners[0][1],
                                 self.arenaCamCorners[1][0],self.arenaCamCorners[1][1])

    def start(self):
        #Global start button not used.
        pass

    def stop(self):
        #Global stop button not used.
        pass

    #---------------------------------------------------
    # CALLBACK METHODS
    #---------------------------------------------------
    def startSwitches(self):
        self.mutex.acquire()
        try:
            if self.currState == State.OFF:
                if self.isReadyToStart():
                    self.paramGroup.setDisabled(True)
                    self.infoGroup.setDisabled(True)
                    td = datetime.datetime.now()
                    self.saveLocation = str(self.infoDir.text())
                    [p, self.fnResults] = os.path.split(self.saveLocation)
                    self.fnResults = self.fnResults + '_' + td.strftime('%Y-%m-%d-%H-%M-%S')
                    self.jsonFileName = str(self.infoDir.text()) + os.sep + self.fnResults  + '.json'
                    self.initArenaData()
                    
                    t = time.time()
                    self.numBlocks = 0
                    self.currState = State.ACCLIMATE
                    self.nextStateTime = t + self.paramAcclimate.value()*60
                    self.setShockState(False,False)
                    self.currShockSide = random.choice([1,2])
                    self.arenaData['stateinfo'].append((t, self.currState, 'Off', 'Off'))
                    self.updateProjectorDisplay()
                    self.startButton.setText('Stop')
                    print 'ACCLIMATING'
                else:
                    self.startButton.setChecked(False)
                    self.arenaMain.statusBar().showMessage('Arena not ready to start.  Information is missing.')
            else: 
                t = time.time()
                self.currState = State.OFF
                self.setShockState(False,False)
                self.startButton.setText('Start Switches')
                self.arenaData['stateinfo'].append((t, self.currState, 'Off', 'Off'))
                self.updateProjectorDisplay()
                self.saveResults()
                self.paramGroup.setDisabled(False) 
                self.infoGroup.setDisabled(False)
        except:
            print ':startSwitches failed'
            traceback.print_exc()
            QtCore.pyqtRemoveInputHook() 
            ipdb.set_trace()
        finally:
            self.mutex.release()    

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

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

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

    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  

    #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 initArenaData(self):
        #prepare output data structure
        self.arenaData = {}
        self.arenaData['fishbirthday'] = str(self.infoDOB.date().toPyDate())
        self.arenaData['fishage'] =  (datetime.date.today() - self.infoDOB.date().toPyDate()).days
        self.arenaData['fishstrain'] = str(self.infoType.text())
        self.arenaData['fishsize'] = self.fishSize
        self.arenaData['parameters'] = { 'numShockBlocks':self.paramNumShockBlocks.value(),
                                         'acclimate (m)':self.paramAcclimate.value(),
                                         'between': self.paramBetweenTime.value(),
                                         'shockingTime': self.paramShockingTime.value(),
                                         'shock period (ms)':self.paramShockPeriod.value(),
                                         'shock dura (ms)':self.paramShockDuration.value(),
                                         'shock chan 1':self.paramShockChan1.value(),
                                         'shock chan 2':self.paramShockChan2.value(),
                                         'shock V':self.paramShockV.value(),
                                         'numFish': self.paramNumFish.value(),
                                         'CodeVersion':None }
        self.arenaData['trackingParameters'] = self.trackWidget.getParameterDictionary()
        self.arenaData['trackingParameters']['arenaPoly'] = self.arenaCamCorners 
        self.arenaData['trackingParameters']['arenaDivideLine'] = self.arenaMidLine
        self.arenaData['trackingParameters']['arenaSide1Sign'] = self.arenaSide1Sign
        self.arenaData['tracking'] = list() #list of tuples (frametime, posx, posy)
        self.arenaData['video'] = list() #list of tuples (frametime, filename)	
        self.arenaData['stateinfo'] = list() #list of times at switch stimulus flipped.
        self.arenaData['shockinfo'] = list()
        t = datetime.datetime.now()
      
        #save experiment images
        self.bcvImgFileName = str(self.infoDir.text()) + os.sep + self.fnResults  + '_BackImg_' + t.strftime('%Y-%m-%d-%H-%M-%S') + '.tiff'
        cv.SaveImage(self.bcvImgFileName, self.trackWidget.getBackgroundImage())	
        self.fishImgFileName = str(self.infoDir.text()) + os.sep +  self.fnResults + '_FishImg_' + t.strftime('%Y-%m-%d-%H-%M-%S') + '.tiff'
        cv.SaveImage(self.fishImgFileName, self.fishImg)

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

    def setShockState(self, bSide1, bSide2):
        if not self.arenaMain.ard:
            print 'WARNING: Arduino not connected'
            return

        if bSide1:
            curr1 = self.arenaMain.ard.pinPulse(self.paramShockChan1.value(), 
                                        self.paramShockPeriod.value(), 
                                        self.paramShockDuration.value(),
                                        feedbackPin = self.paramCurrChan1.value())
        else:
            self.arenaMain.ard.pinLow(self.paramShockChan1.value())
            curr1 = 0
        if bSide2:
            curr2 = self.arenaMain.ard.pinPulse(self.paramShockChan2.value(), 
                                           self.paramShockPeriod.value(), 
                                           self.paramShockDuration.value(),
                                           feedbackPin = self.paramCurrChan2.value())
        else:
            self.arenaMain.ard.pinLow(self.paramShockChan2.value()) 
            curr2 = 0
        self.arenaData['shockinfo'].append((time.time(), bSide1, bSide2, curr1, curr2))
class ContextualHelplessnessController(ArenaController.ArenaController):
    def __init__(self, parent, arenaMain):        
        super(ContextualHelplessnessController, 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.arenaData = None
        self.currState = State.OFF
        self.fishPosUpdate = False
        self.fishPos = (0,0)
        self.allFish = []
        self.fishSize = 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.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.projectorPositionChanged)
        self.projLayout.addWidget(self.projCalibButton,0,0,1,6)
        self.projPosL = QtGui.QLabel('Pos')
        self.projX = QtGui.QSpinBox()
        self.projX.setRange(0,1000)
        self.projX.setValue(250)
        self.projX.setMaximumWidth(50)
        self.projY = QtGui.QSpinBox()
        self.projY.setRange(0,1000)
        self.projY.setValue(250)
        self.projY.setMaximumWidth(50)
        self.projLayout.addWidget(self.projPosL,1,0)
        self.projLayout.addWidget(self.projX,1,1)
        self.projLayout.addWidget(self.projY,1,2)
        self.projX.valueChanged.connect(self.projectorPositionChanged)
        self.projY.valueChanged.connect(self.projectorPositionChanged)
        self.projSizeL = QtGui.QLabel('Size L,W')
        self.projLen = QtGui.QSpinBox()
        self.projLen.setRange(0,1000)
        self.projLen.setValue(220)
        self.projLen.setMaximumWidth(50)
        self.projWid = QtGui.QSpinBox()
        self.projWid.setRange(0,1000)
        self.projWid.setValue(115)
        self.projWid.setMaximumWidth(50)
        self.projLen.valueChanged.connect(self.projectorPositionChanged)
        self.projWid.valueChanged.connect(self.projectorPositionChanged)
        self.projLayout.addWidget(self.projSizeL,2,0)
        self.projLayout.addWidget(self.projLen,2,1)
        self.projLayout.addWidget(self.projWid,2,2)
        self.projRotL = QtGui.QLabel('Rotation')
        self.projRot = QtGui.QSpinBox()
        self.projRot.setRange(0,360)
        self.projRot.setValue(270)
        self.projRot.setMaximumWidth(50)
        self.projLayout.addWidget(self.projRotL,2,3)
        self.projLayout.addWidget(self.projRot,2,4)
        self.projRot.valueChanged.connect(self.projectorPositionChanged)
        
        self.tankLength = LabeledSpinBox(None, 'Tank Len (mm)', 0,100,46,60)
        self.projLayout.addWidget(self.tankLength, 3,0,1,2)
        self.tankWidth = LabeledSpinBox(None, 'Tank Wid (mm)', 0,100,21,60)
        self.projLayout.addWidget(self.tankWidth, 3,2,1,2)

        self.projLayout.setColumnStretch(6,1)
        self.projGroup.setLayout(self.projLayout)
        self.arenaLayout.addWidget(self.camCalibButton, 0,0)
        self.arenaLayout.addWidget(self.projGroup,1,0)
        self.arenaLayout.setColumnStretch(1,1)
        self.arenaGroup.setLayout(self.arenaLayout)

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

        self.startButton = QtGui.QPushButton('Start Switches')
        self.startButton.setMaximumWidth(150)
        self.startButton.setCheckable(True)
        self.startButton.clicked.connect(self.startSwitches)
      
        self.pauseButton = QtGui.QPushButton('Pause')
        self.pauseButton.setMaximumWidth(150)
        self.pauseButton.setCheckable(True)
        self.pauseButton.setDisabled(True)
        self.pauseButton.clicked.connect(self.pause)

        self.autoPauseCheckBox = QtGui.QCheckBox('AutoPause')

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

        #high level flow - acclimate -> pretest -> [b/t] -> train -> [b/t] -> posttest 
        self.paramAcclimate = LabeledSpinBox(None, 'Acclimate (m)', 0, 240, 0, 60)
        self.paramLayout.addWidget(self.paramAcclimate, 0,0,1,2)
        self.paramBetweenTime = LabeledSpinBox(None,'BetweenTime (m)',0, 1440, 0, 60) #time between pre , train and post periods
        self.paramLayout.addWidget(self.paramBetweenTime,0,2,1,2)
        self.paramPreDuration = LabeledSpinBox(None,'PreTest (m)',0,120,15,60) #number of side switches pre test
        self.paramLayout.addWidget(self.paramPreDuration,1,0,1,2)
        self.paramPreOMR = QtGui.QCheckBox('PreOMR')
        self.paramPreOMR.setCheckState(2)
        self.paramLayout.addWidget(self.paramPreOMR,1,2,1,2)
        self.paramNumTrain = LabeledSpinBox(None,'NumTrain',0,500,30,60)
        self.paramLayout.addWidget(self.paramNumTrain,2,0,1,2)
        self.paramTrainOMR = QtGui.QCheckBox('TrainOMR')
        self.paramTrainOMR.setCheckState(2)
        self.paramLayout.addWidget(self.paramTrainOMR,2,2,1,2)
        self.paramPostDuration = LabeledSpinBox(None,'PostTest (m)',0,120,5,60) #number of side switches post teset
        self.paramLayout.addWidget(self.paramPostDuration,3,0,1,2)
        self.paramPostOMR = QtGui.QCheckBox('PostOMR')
        self.paramPostOMR.setCheckState(2)
        self.paramLayout.addWidget(self.paramPostOMR,3,2,1,2)

        #omr parameters  
        self.paramOMRInterval = LabeledSpinBox(None,'OMR Interval (s)',0,600,48,60) #time between OMR tests
        self.paramOMRDuration = LabeledSpinBox(None,'OMR Duration (s)',0,600,12,60) #duration of OMR tests
        self.paramOMRPeriod = LabeledSpinBox(None,'OMR Grating Period (mm)',0,50,5,60) #spacing between grating bars
        self.paramOMRDutyCycle = LabeledSpinBox(None, 'OMR DutyCycle %',0,100,50,60) #width of grating bars
        self.paramOMRVelocity = LabeledSpinBox(None, 'OMR Speed (mm/s)',0,50,5,60) #velocity of moving gratings.
        self.paramLayout.addWidget(self.paramOMRInterval,4,0,1,2)
        self.paramLayout.addWidget(self.paramOMRDuration,4,2,1,2)        
        self.paramLayout.addWidget(self.paramOMRPeriod,5,0,1,2) 
        self.paramLayout.addWidget(self.paramOMRDutyCycle,5,2,1,2)
        self.paramLayout.addWidget(self.paramOMRVelocity,6,0,1,2)

        #training parameters
        self.paramUSTime = LabeledSpinBox(None, 'US Time (s)', 1,3600,60,60)
        self.paramLayout.addWidget(self.paramUSTime,7,0,1,2)
        self.paramNeutralMinTime = LabeledSpinBox(None, 'Neutral Min (s)',1,3600,2,60)
        self.paramLayout.addWidget(self.paramNeutralMinTime, 8,0,1,2)
        self.paramNeutralMaxTime = LabeledSpinBox(None, 'Neutral Max (s)',1,3600,5,60)
        self.paramLayout.addWidget(self.paramNeutralMaxTime, 8,2,1,2)
        self.paramShockPeriod = LabeledSpinBox(None, 'Shock Period (ms)', 0,5000,1000,60)
        self.paramLayout.addWidget(self.paramShockPeriod, 9,0,1,2)
        self.paramShockDuration = LabeledSpinBox(None, 'ShockDuration (ms)', 0,1000,50,60)
        self.paramLayout.addWidget(self.paramShockDuration, 9,2,1,2)
        self.paramShockChan1 = LabeledSpinBox(None, 'ShockChan1', 0,10000,53,60)
        self.paramLayout.addWidget(self.paramShockChan1, 10,0,1,2)
        self.paramShockChan2 = LabeledSpinBox(None, 'ShockChan2', 0,10000,52,60)
        self.paramLayout.addWidget(self.paramShockChan2, 10,2,1,2)
        self.paramCurrChan1 = LabeledSpinBox(None, 'CurrChan Side 1', 0,16,15,60)
        self.paramLayout.addWidget(self.paramCurrChan1,11,0,1,2)
        self.paramCurrChan2 = LabeledSpinBox(None, 'CurrChan Side 2', 0,16,14,60)
        self.paramLayout.addWidget(self.paramCurrChan2,11,2,1,2)
        self.paramShockV = LabeledSpinBox(None, 'ShockV', 0,100, 5,60)
        self.paramLayout.addWidget(self.paramShockV, 12,0,1,2)

        #Colors
        (self.paramColorAcclimate,self.labelColorAcclimate) = self.getColorComboBox('AcclimateColor', 0)
        self.paramLayout.addWidget(self.paramColorAcclimate,13,0)
        self.paramLayout.addWidget(self.labelColorAcclimate,13,1)

        (self.paramColorPreTest,self.labelColorPreTest) = self.getColorComboBox('PreTestColor', 0)
        self.paramLayout.addWidget(self.paramColorPreTest,13,2)
        self.paramLayout.addWidget(self.labelColorPreTest,13,3)

        (self.paramColorTrain,self.labelColorTrain) = self.getColorComboBox('TrainColor', 0)
        self.paramLayout.addWidget(self.paramColorTrain,14,0)
        self.paramLayout.addWidget(self.labelColorTrain,14,1)

        (self.paramColorPostTest,self.labelColorPostTest) = self.getColorComboBox('PostTestColor', 0)
        self.paramLayout.addWidget(self.paramColorPostTest,14,2)
        self.paramLayout.addWidget(self.labelColorPostTest,14,3)

        (self.paramColorBetween,self.labelColorBetween) = self.getColorComboBox('BetweenColor', 0)
        self.paramLayout.addWidget(self.paramColorBetween,15,0)
        self.paramLayout.addWidget(self.labelColorBetween,15,1)

        (self.paramColorOMR,self.labelColorOMR) = self.getColorComboBox('OMRColor', 5)
        self.paramLayout.addWidget(self.paramColorOMR,15,2)
        self.paramLayout.addWidget(self.labelColorOMR,15,3)

        self.paramNumFish = LabeledSpinBox(None,'NumFish',1,10,1,60)
        self.paramLayout.addWidget(self.paramNumFish,16,0,1,2)

        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,1,2)
        self.settingsLayout.addWidget(self.trackWidget,1,0,1,2)
        self.settingsLayout.addWidget(self.infoGroup,3,0,1,2)
        self.settingsLayout.addWidget(self.startButton,4,0,1,1)
        self.settingsLayout.addWidget(self.pauseButton,5,0,1,1)
        self.settingsLayout.addWidget(self.autoPauseCheckBox,5,1,1,1)
        self.settingsLayout.addWidget(self.paramGroup,6,0,1,2)
        self.setLayout(self.settingsLayout)

        self.projectorPositionChanged()
        self.t = 0
#        self.omrPhase = 0
#        self.omrDirection = [1,0]

    def getColorComboBox(self, labelName, defaultNdx=1):
        colorBox = QtGui.QComboBox()
        colorBox.addItem('White')
        colorBox.addItem('Red')
        colorBox.addItem('Blue')
        colorBox.addItem('Gray')
        colorBox.addItem('Magenta')
        colorBox.addItem('Black')
        colorBox.setCurrentIndex(defaultNdx)
        colorLabel = QtGui.QLabel(labelName)
        return (colorBox,colorLabel)
    

    #---------------------------------------------------
    # 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
                self.allFish = allFish

            if not self.currState == State.OFF:
                self.arenaData['video'].append((self.frameTime, None)) 
                d = [self.frameTime, pos[0], pos[1]]
                for nFish in range(1, min(self.paramNumFish.value(),len(allFish))):
                    d.append(allFish[nFish][0])
                    d.append(allFish[nFish][1])
                self.arenaData['tracking'].append(tuple(d))

            self.arenaView = view
        except:
            print 'ClassicalConditioningController:onNewFrame failed'
            traceback.print_exc()
            QtCore.pyqtRemoveInputHook() 
            ipdb.set_trace()
        finally:
            self.mutex.release()

  
    def updateState(self):
        self.updateExperimentalState()

    def startOMR(self):
        self.nextOMRTime = self.t + self.paramOMRInterval.value()
        self.bOMR = False
        self.omrDirection = (0,0)
        self.arenaData['OMRinfo'].append((self.t, self.bOMR, self.omrDirection))

    def updateOMR(self):
        if self.t > self.nextOMRTime :
            if self.bOMR:
                self.nextOMRTime = self.t + self.paramOMRInterval.value()
                self.bOMR = False
                self.omrDirection = (0,0)
            else:
                self.nextOMRTime = self.t + self.paramOMRDuration.value()
                self.bOMR = True
                self.omrPhase = 0
                self.omrLastUpdate = self.t
                if self.isOnSide(self.fishPos, self.arenaMidLine, self.arenaSide1Sign):
                    self.omrDirection = (1,0)
                else:
                    self.omrDirection = (-1,0)
            self.arenaData['OMRinfo'].append((self.t, self.bOMR, self.omrDirection))
            self.updateProjectorDisplay()

    def stopOMR(self):
        self.nextOMRTime = float('inf')
        self.bOMR = False
        self.omrDirection = (0,0)
        self.arenaData['OMRinfo'].append((self.t, self.bOMR, self.omrDirection))

    def updateExperimentalState(self):
#        if self.paramPreOMR.isChecked():
#            t = time.time()
#            self.lt = self.t
#            self.t = t
#            print self.t - self.lt
#            self.omrPhase += self.omrDirection[0] * (float(self.paramOMRVelocity.value())/float(self.paramOMRPeriod.value())) * 360.0 * (self.t-self.lt)
#            print self.omrPhase
#            if t - self.omrLastUpdate > 0.010:
#                #print t - self.omrLastUpdate
#                self.updateProjectorDisplay()


        if not self.currState == State.OFF:
            self.mutex.acquire()
            try:
                t = time.time()
                self.lt = self.t
                self.t = t

                #only update status bar if arena is selected.
                if self.isCurrent():
                    self.arenaMain.statusBar().showMessage('State %d'%(self.currState))

                if t > self.nextStateTime:
                    self.setShockState(False, False)
                    self.stopOMR()
                    self.bInShockBout = False
                    self.nextShockBoutTime = float('inf')
                    self.pauseButton.setDisabled(True)
                    if (self.currState <= State.ACCLIMATE
                        and self.paramPreDuration.value() > 0):
                        self.currState = State.PRETEST
                        self.nextStateTime = t + (self.paramPreDuration.value()*60)
                        if self.paramPreOMR.isChecked():
                            self.startOMR()                            
                    elif (self.currState <= State.PRETEST
                        and self.paramBetweenTime.value() > 0):
                        self.currState = State.PREDONE
                        self.nextStateTime = t + self.paramBetweenTime.value()*60
                        self.pauseButton.setDisabled(False)
                        if self.autoPauseCheckBox.isChecked():
                            self.pauseButton.setChecked(True)
                            self.doPause()
                    elif (self.currState <= State.PREDONE
                        and self.paramNumTrain.value() > 0):
                        self.currState = State.TRAIN
                        self.nextStateTime = float("inf")
                        self.numShock = 0; 
                        self.bInShockBout = False
                        self.nextShockBoutTime = t + random.uniform(self.paramNeutralMinTime.value(), self.paramNeutralMaxTime.value())                        
                        if self.paramTrainOMR.isChecked():
                            self.startOMR()                            
                    elif (self.currState <= State.TRAIN
                          and self.paramBetweenTime.value() > 0):
                        self.currState = State.TRAINDONE
                        self.nextStateTime = t + self.paramBetweenTime.value()*60
                        self.pauseButton.setDisabled(False)
                        if self.autoPauseCheckBox.isChecked():
                            self.pauseButton.setChecked(True)
                            self.doPause()
                    elif (self.currState <= State.TRAINDONE
                        and self.paramPostDuration.value() > 0):
                        self.currState = State.POSTTEST
                        self.nextStateTime = t + (self.paramPostDuration.value()*60) 
                        if self.paramPostOMR.isChecked():
                            self.startOMR()                            
                    elif self.currState <= State.POSTTEST:
                        self.currState = State.OFF
                        self.startButton.setText('Start Switches')
                        self.startButton.setChecked(False)
                        self.paramGroup.setDisabled(False)
                        self.infoGroup.setDisabled(False)
                        self.saveResults()
                    self.arenaData['stateinfo'].append((t, self.currState, self.getSide1ColorName(), self.getSide2ColorName()))
                    self.updateProjectorDisplay()
                    self.saveResults()


                #handle omr state changes and projector updates
                self.updateOMR()
                if self.bOMR:
                    self.omrPhase += self.omrDirection[0] * (float(self.paramOMRVelocity.value())/float(self.paramOMRPeriod.value())) * 360.0 * (self.t-self.lt)
                    self.omrPhase = self.omrPhase%360.0
                    if t - self.omrLastUpdate > 0.010:
                        #if t - self.omrLastUpdate > 0.040:
                        #    print 'WARNING: projector slow to update', t - self.omrLastUpdate
                        self.updateProjectorDisplay()
               
                #handle training shock bout state changes
                if t > self.nextShockBoutTime:
                    if self.bInShockBout:
                        self.bInShockBout = False
                        self.setShockState(False,False)
                        self.numShock += 1                        
                        self.nextShockBoutTime = t + random.uniform(self.paramNeutralMinTime.value(),
                                                                    self.paramNeutralMaxTime.value())
                        if self.numShock >= self.paramNumTrain.value():
                            self.nextStateTime = t 
                    else:
                        self.bInShockBout = True
                        self.setShockState(True,True)
                        self.nextShockBoutTime = t + self.paramUSTime.value()
                    self.updateProjectorDisplay()
                    self.arenaData['stateinfo'].append((t, self.currState, self.getSide1ColorName(), self.getSide2ColorName()))
            
            except:
                print 'ContextualHelplessnessController:updateState failed'
                traceback.print_exc()
                QtCore.pyqtRemoveInputHook() 
                ipdb.set_trace()
            finally:
                self.mutex.release()

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

    def projectorPositionChanged(self):
        #the point defining the tank are such that:
        #points 1 and 2 define side1
        #points 2 and 3 define the 'near' connecting edge
        #points 3 and 4 define side2
        #points 4 and 1 define the 'far' connecting edge.
 
        #move this code elsewhere to avoid redundant computation...
        self.pts = np.array([[0                   , 0],
                             [0                   , self.projWid.value()],
                             [self.projLen.value(), self.projWid.value()],
                             [self.projLen.value(), 0]])
        theta = self.projRot.value()
        self.tankRotM = np.array([[cos(radians(theta)), -sin(radians(theta))],
                         [sin(radians(theta)),  cos(radians(theta))]]);
        self.pts = np.dot(self.tankRotM,self.pts.T).T
        self.pts += [self.projX.value(), self.projY.value()]
        self.updateProjectorDisplay()

    def drawProjectorDisplay(self, painter):
        if self.currState == State.OFF and self.projCalibButton.isChecked() and self.isCurrent():
            #DRAW A CALIBRATION IMAGE THAT INDICATES THE PROJECTOR LOCATION AND SIDE1
            side1Color = QtCore.Qt.red
            side2Color = QtCore.Qt.blue
            a = .5
            b = 1-a
            #Draw side one
            pen = QtGui.QPen(QtCore.Qt.NoPen)
            brush = QtGui.QBrush(side1Color)
            painter.setBrush(brush)
            painter.setPen(pen)
            poly = QtGui.QPolygonF()
            poly.append(QtCore.QPointF(self.pts[0,0], self.pts[0,1]))
            poly.append(QtCore.QPointF(self.pts[1,0], self.pts[1,1]))
            poly.append(QtCore.QPointF(a*self.pts[1,0] + b*self.pts[2,0], a*self.pts[1,1] + b*self.pts[2,1]))
            poly.append(QtCore.QPointF(a*self.pts[0,0] + b*self.pts[3,0], a*self.pts[0,1] + b*self.pts[3,1]))
            painter.drawPolygon(poly)
            painter.setPen(QtGui.QColor(168, 34, 3))
            painter.setFont(QtGui.QFont('Decorative',24))
            painter.drawText(self.pts[0,0],self.pts[0,1],'1')
            painter.drawText(self.pts[1,0],self.pts[1,1],'2')
            painter.drawText(self.pts[2,0],self.pts[2,1],'3')
            painter.drawText(self.pts[3,0],self.pts[3,1],'4')
            #Draw side two
            pen = QtGui.QPen(QtCore.Qt.NoPen)
            brush = QtGui.QBrush(side2Color)
            painter.setBrush(brush)
            painter.setPen(pen)
            poly = QtGui.QPolygonF()
            poly.append(QtCore.QPointF(a*self.pts[0,0] + b*self.pts[3,0], a*self.pts[0,1] + b*self.pts[3,1]))
            poly.append(QtCore.QPointF(a*self.pts[1,0] + b*self.pts[2,0], a*self.pts[1,1] + b*self.pts[2,1]))
            poly.append(QtCore.QPointF(self.pts[2,0], self.pts[2,1]))
            poly.append(QtCore.QPointF(self.pts[3,0], self.pts[3,1]))
            painter.drawPolygon(poly)
        else:
            if self.currState == State.OFF:
                #Draw whole tank
                pen = QtGui.QPen(QtCore.Qt.NoPen)
                brush = QtGui.QBrush(QtCore.Qt.white)
                painter.setBrush(brush)
                painter.setPen(pen)
                poly = QtGui.QPolygonF()
                poly.append(QtCore.QPointF(self.pts[0,0], self.pts[0,1]))
                poly.append(QtCore.QPointF(self.pts[1,0], self.pts[1,1]))
                poly.append(QtCore.QPointF(self.pts[2,0], self.pts[2,1]))
                poly.append(QtCore.QPointF(self.pts[3,0], self.pts[3,1]))
                painter.drawPolygon(poly)
            else:
                side1Color = self.getSide1ColorNdx()
                side2Color = self.getSide2ColorNdx()
                a = .5
                b = 1-a
                #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.pts[0,0], self.pts[0,1]))
                poly.append(QtCore.QPointF(self.pts[1,0], self.pts[1,1]))
                poly.append(QtCore.QPointF(a*self.pts[1,0] + b*self.pts[2,0], a*self.pts[1,1] + b*self.pts[2,1]))
                poly.append(QtCore.QPointF(a*self.pts[0,0] + b*self.pts[3,0], a*self.pts[0,1] + b*self.pts[3,1]))
                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.pts[0,0] + b*self.pts[3,0], a*self.pts[0,1] + b*self.pts[3,1]))
                poly.append(QtCore.QPointF(a*self.pts[1,0] + b*self.pts[2,0], a*self.pts[1,1] + b*self.pts[2,1]))
                poly.append(QtCore.QPointF(self.pts[2,0], self.pts[2,1]))
                poly.append(QtCore.QPointF(self.pts[3,0], self.pts[3,1]))
                painter.drawPolygon(poly)
                #Draw OMR grating
                if self.bOMR:
                    self.omrLastUpdate = self.t                    
                    #This stuff doesn't need to computed on every frame
                    mm2pix = math.hypot(self.pts[2,0]-self.pts[1,0], self.pts[2,1] - self.pts[1,1]) / self.tankLength.value()
                    period = self.paramOMRPeriod.value() * mm2pix
                    numBars = int(math.ceil(math.hypot(self.pts[2,0]-self.pts[1,0], self.pts[2,1] - self.pts[1,1]) / period))
                    barWidth = period * (self.paramOMRDutyCycle.value()/100.0)
                    
                    #Draw the bar
                    phaseShift = self.omrPhase/360.0 * period  
                    pen = QtGui.QPen(QtCore.Qt.NoPen)
                    brush = self.getBrush(self.paramColorOMR.currentIndex())
                    painter.setBrush(brush)
                    painter.setPen(pen)
                    for nBar in range(-1,numBars+1):
                        #define bar in unrotated untranslated space.
                        bl = min(max(phaseShift + nBar * period, 0), self.projLen.value()) 
                        br = min(max(phaseShift + nBar * period + barWidth, 0), self.projLen.value()) 
                        bar = np.array([[br,0],
                                        [bl,0],
                                        [bl,self.projWid.value()],
                                        [br,self.projWid.value()]])
                        #rotate and tranlate.
                        bar = np.dot(self.tankRotM, bar.T).T + [self.projX.value(), self.projY.value()]
                        #draw
                        poly = QtGui.QPolygonF()
                        poly.append(QtCore.QPointF(bar[0,0],bar[0,1]))
                        poly.append(QtCore.QPointF(bar[1,0],bar[1,1]))
                        poly.append(QtCore.QPointF(bar[2,0],bar[2,1]))
                        poly.append(QtCore.QPointF(bar[3,0],bar[3,1]))
                        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)
            for nFish in range(min(len(self.allFish),self.paramNumFish.value())):
                painter.drawEllipse(QtCore.QPointF(self.allFish[nFish][0],self.allFish[nFish][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)
            if len(self.arenaCamCorners) >= 2:
                pen = QtGui.QPen(QtCore.Qt.red)
                pen.setWidth(2)
                painter.setPen(pen)
                painter.drawLine(self.arenaCamCorners[0][0],self.arenaCamCorners[0][1],
                                 self.arenaCamCorners[1][0],self.arenaCamCorners[1][1])
                painter.setPen(QtGui.QColor(168, 34, 3))
                painter.setFont(QtGui.QFont('Decorative',14))
                painter.drawText(self.arenaCamCorners[0][0],self.arenaCamCorners[0][1],str(self.currState))

    def start(self):
        #Global start button not used.
        pass

    def stop(self):
        #Global stop button not used.
        pass

    #---------------------------------------------------
    # CALLBACK METHODS
    #---------------------------------------------------
    def startSwitches(self):
        self.mutex.acquire()
        try:
            if self.currState == State.OFF:
                if self.isReadyToStart():
                    self.paramGroup.setDisabled(True)
                    self.infoGroup.setDisabled(True)
                    td = datetime.datetime.now()
                    self.saveLocation = str(self.infoDir.text())
                    [p, self.fnResults] = os.path.split(self.saveLocation)
                    self.fnResults = self.fnResults + '_' + td.strftime('%Y-%m-%d-%H-%M-%S')
                    self.jsonFileName = str(self.infoDir.text()) + os.sep + self.fnResults  + '.json'
                    self.initArenaData()
                    
                    t = time.time()
                    self.currState = State.ACCLIMATE
                    self.nextStateTime = t + self.paramAcclimate.value()*60
                    self.bInShockBout = False
                    self.nextShockBoutTime = float('inf')
                    self.setShockState(False,False)
                    self.stopOMR()
                    self.arenaData['stateinfo'].append((t, self.currState, self.getSide1ColorName(), self.getSide2ColorName()))
                    self.updateProjectorDisplay()
                    self.startButton.setText('Stop')
                else:
                    self.startButton.setChecked(False)
                    self.arenaMain.statusBar().showMessage('Arena not ready to start.  Information is missing.')
            else: 
                t = time.time()
                self.currState = State.OFF
                self.nextStateTime = float('inf')
                self.bInShockBout = False
                self.nextShockBoutTime = float('inf')
                self.setShockState(False,False)
                self.stopOMR()
                self.startButton.setText('Start Switches')
                self.arenaData['stateinfo'].append((t, self.currState, self.getSide1ColorName(), self.getSide2ColorName()))
                self.updateProjectorDisplay()
                self.saveResults()
                self.paramGroup.setDisabled(False) 
                self.infoGroup.setDisabled(False)
                self.pauseButton.setChecked(False)
        except:
            print 'ContextualHelplessnessController:startSwitches failed'
            traceback.print_exc()
            QtCore.pyqtRemoveInputHook() 
            ipdb.set_trace()
        finally:
            self.mutex.release()    

    def doPause(self):   
        t = time.time()
        if self.pauseButton.isChecked():
            self.arenaData['pauseinfo'].append((True, t))
            self.pausedRemainingTime = self.nextStateTime - t
            self.nextStateTime = float('inf')
        else:
            self.arenaData['pauseinfo'].append((False, t))
            self.nextStateTime = t + self.pausedRemainingTime

    def pause(self):
        self.mutex.acquire()
        try:
            self.doPause()
        except:
            print 'ContextualHelplessnessController: pause failed'
            traceback.print_exc()
            QtCore.pyqtRemoveInputHook() 
            ipdb.set_trace()
        finally:
            self.mutex.release()    
                
        
    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('')

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

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

    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  

    #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 initArenaData(self):
        #prepare output data structure
        self.arenaData = {}
        self.arenaData['fishbirthday'] = str(self.infoDOB.date().toPyDate())
        self.arenaData['fishage'] =  (datetime.date.today() - self.infoDOB.date().toPyDate()).days
        self.arenaData['fishstrain'] = str(self.infoType.text())
        self.arenaData['fishsize'] = self.fishSize
        self.arenaData['parameters'] = { 'preDuration':self.paramPreDuration.value(),
                                         'numTrain':self.paramNumTrain.value(),
                                         'postDuration':self.paramPostDuration.value(),
                                         'acclimate (m)':self.paramAcclimate.value(),
                                         'between (m)': self.paramBetweenTime.value(),
                                         'trainUStime': self.paramUSTime.value(),
                                         'trainNeutralMinTime':self.paramNeutralMinTime.value(),
                                         'trainNeutralMaxTime':self.paramNeutralMaxTime.value(),
                                         'shock period (ms)':self.paramShockPeriod.value(),
                                         'shock dura (ms)':self.paramShockDuration.value(),
                                         'shock chan 1':self.paramShockChan1.value(),
                                         'shock chan 2':self.paramShockChan2.value(),
                                         'shock V':self.paramShockV.value(),
                                         'OMR_pre':self.paramPreOMR.isChecked(),
                                         'OMR_train':self.paramTrainOMR.isChecked(),
                                         'OMR_post':self.paramPostOMR.isChecked(),
                                         'OMR_period':self.paramOMRPeriod.value(),
                                         'OMR_dutycycle':self.paramOMRDutyCycle.value(),
                                         'OMR_velocity':self.paramOMRVelocity.value(),
                                         'OMR_interval':self.paramOMRInterval.value(),
                                         'OMR_duration':self.paramOMRDuration.value(),
                                         'OMR_color':str(self.paramColorOMR.currentText()),
                                         'AcclimateColor':str(self.paramColorAcclimate.currentText()),
                                         'BetweenColor':str(self.paramColorBetween.currentText()),
                                         'PreTestColor':str(self.paramColorPreTest.currentText()),
                                         'TrainColor':str(self.paramColorTrain.currentText()),
                                         'PostTestColor':str(self.paramColorPostTest.currentText()),
                                         'numFish': self.paramNumFish.value(),
                                         'CodeVersion':None }
        self.arenaData['trackingParameters'] = self.trackWidget.getParameterDictionary()
        self.arenaData['trackingParameters']['arenaPoly'] = self.arenaCamCorners 
        self.arenaData['trackingParameters']['arenaDivideLine'] = self.arenaMidLine
        self.arenaData['trackingParameters']['arenaSide1Sign'] = self.arenaSide1Sign
        self.arenaData['projectorParameters'] = {'position':[self.projX.value(), self.projY.value()],
                                                 'size':[self.projLen.value(), self.projWid.value()],
                                                 'rotation':self.projRot.value()}
        self.arenaData['tankSize_mm'] = [self.tankLength.value(), self.tankWidth.value()]
        self.arenaData['tracking'] = list() #list of tuples (frametime, posx, posy)
        self.arenaData['video'] = list() #list of tuples (frametime, filename)	
        self.arenaData['stateinfo'] = list() #list of times at switch stimulus flipped.
        self.arenaData['shockinfo'] = list()
        self.arenaData['OMRinfo'] = list()
        self.arenaData['pauseinfo'] = list()
        t = datetime.datetime.now()
      
        #save experiment images
        self.bcvImgFileName = str(self.infoDir.text()) + os.sep + self.fnResults  + '_BackImg_' + t.strftime('%Y-%m-%d-%H-%M-%S') + '.tiff'
        cv.SaveImage(self.bcvImgFileName, self.trackWidget.getBackgroundImage())	
        self.fishImgFileName = str(self.infoDir.text()) + os.sep +  self.fnResults + '_FishImg_' + t.strftime('%Y-%m-%d-%H-%M-%S') + '.tiff'
        cv.SaveImage(self.fishImgFileName, self.fishImg)

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

    def getSideColors(self):
        if self.currState == State.ACCLIMATE:
            return (self.paramColorAcclimate.currentIndex(), self.paramColorAcclimate.currentIndex(),
                    str(self.paramColorAcclimate.currentText()), str(self.paramColorAcclimate.currentText()))
        if self.currState == State.PREDONE or self.currState == State.TRAINDONE:
            return (self.paramColorBetween.currentIndex(), self.paramColorBetween.currentIndex(),
                    str(self.paramColorBetween.currentText()), str(self.paramColorBetween.currentText()))
        elif self.currState == State.PRETEST:
            return (self.paramColorPreTest.currentIndex(), self.paramColorPreTest.currentIndex(),
                    str(self.paramColorPreTest.currentText()), str(self.paramColorPreTest.currentText()))
        elif self.currState == State.TRAIN:
            return (self.paramColorTrain.currentIndex(), self.paramColorTrain.currentIndex(),
                    str(self.paramColorTrain.currentText()), str(self.paramColorTrain.currentText()))
        elif self.currState == State.POSTTEST:
            return (self.paramColorPostTest.currentIndex(), self.paramColorPostTest.currentIndex(),
                    str(self.paramColorPostTest.currentText()), str(self.paramColorPostTest.currentText()))
        else:
            return (-1,-1,'Black','Black')

    def getSide1ColorName(self):
        return self.getSideColors()[2]
    def getSide2ColorName(self):
        return self.getSideColors()[3]
    def getSide1ColorNdx(self):
        return self.getSideColors()[0]
    def getSide2ColorNdx(self):
        return self.getSideColors()[1]

    def setShockState(self, bSide1, bSide2):
        if not self.arenaMain.ard:
            print 'WARNING: Arduino not connected'
            return

        if bSide1:
            curr1 = self.arenaMain.ard.pinPulse(self.paramShockChan1.value(), 
                                        self.paramShockPeriod.value(), 
                                        self.paramShockDuration.value(), 
                                        feedbackPin = self.paramCurrChan1.value())
            print 'Shocking!!!'
        else:
            self.arenaMain.ard.pinLow(self.paramShockChan1.value())
            curr1 = 0
        if bSide2:
            curr2 = self.arenaMain.ard.pinPulse(self.paramShockChan2.value(), 
                                           self.paramShockPeriod.value(), 
                                           self.paramShockDuration.value(), 
                                           feedbackPin = self.paramCurrChan2.value())
            print 'Shocking!!!'
        else:
            self.arenaMain.ard.pinLow(self.paramShockChan2.value()) 
            curr2 = 0
        self.arenaData['shockinfo'].append((time.time(), bSide1, bSide2, curr1, curr2))

    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)) 
        elif colorNdx == 4:
            return QtGui.QBrush(QtCore.Qt.magenta)
        elif colorNdx == 5:
            return QtGui.QBrush(QtCore.Qt.black)
        else:
            return QtGui.QBrush(QtCore.Qt.black)
Esempio n. 4
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())
        """
Esempio n. 5
0
class OMRController(ArenaController.ArenaController, Machine):
    def __init__(self, parent, arenaMain):
        super(OMRController, self).__init__(parent, arenaMain)

        print type(parent)

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

        #tracking
        self.arenaCvMask = None   

        #state
        self.mutex = Lock()
        states = ['off', 'between_session', 'omr_to_s1', 'no_omr_post_s1', 'omr_to_s2', 'no_omr_post_s2'] # off between L-N-R-N-L-N-R between LNRNLNR between off
        Machine.__init__(self, states=states, initial='off', after_state_change='update_state_data')

        self.add_transition('begin', 'off', 'between_session', conditions='isReadyToStart')
        self.add_transition('start_session', 'between_session', 'omr_to_s1')
        self.add_transition('start_omr', 'no_omr_post_s1', 'omr_to_s2')
        self.add_transition('start_omr', 'no_omr_post_s2', 'omr_to_s1')
        self.add_transition('stop_omr', 'omr_to_s1', 'no_omr_post_s1')
        self.add_transition('stop_omr', 'omr_to_s2', 'no_omr_post_s2')
        self.add_transition('stop_session', ['omr_to_s1', 'omr_to_s2', 'no_omr_post_s1', 'no_omr_post_s2'], 'between_session')
        self.add_transition('quit', '*', 'off')

        self.fishPosUpdate = False
        self.fishPos = (0,0)
        self.fishPosBuffer = []
        self.fishPosBufferT = []
        self.currCvFrame = None
        #Note additional state variable will be created during state transitions (like nextStateTime)

        #data results
        self.arenaData = None
        self.fishSize = 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.camCalibButton = QtGui.QPushButton('Set Cam Position')
        self.camCalibButton.setMaximumWidth(150)
        self.camCalibButton.clicked.connect(self.getArenaCameraPosition)
        self.resetCamCorners = None
        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.projectorPositionChanged)
        self.projLayout.addWidget(self.projCalibButton,0,0,1,6)
        self.projPosL = QtGui.QLabel('Pos')
        self.projX = QtGui.QSpinBox()
        self.projX.setRange(0,1000)
        self.projX.setValue(250)
        self.projX.setMaximumWidth(50)
        self.projY = QtGui.QSpinBox()
        self.projY.setRange(0,1000)
        self.projY.setValue(250)
        self.projY.setMaximumWidth(50)
        self.projLayout.addWidget(self.projPosL,1,0)
        self.projLayout.addWidget(self.projX,1,1)
        self.projLayout.addWidget(self.projY,1,2)
        self.projX.valueChanged.connect(self.projectorPositionChanged)
        self.projY.valueChanged.connect(self.projectorPositionChanged)
        self.projSizeL = QtGui.QLabel('Size W,L')
        self.projWid = QtGui.QSpinBox()
        self.projWid.setRange(0,1000)
        self.projWid.setValue(115)
        self.projWid.setMaximumWidth(50)
        self.projLen = QtGui.QSpinBox()
        self.projLen.setRange(0,1000)
        self.projLen.setValue(220)
        self.projLen.setMaximumWidth(50)
        self.projLen.valueChanged.connect(self.projectorPositionChanged)
        self.projWid.valueChanged.connect(self.projectorPositionChanged)
        self.projLayout.addWidget(self.projSizeL,2,0)
        self.projLayout.addWidget(self.projWid,2,1)
        self.projLayout.addWidget(self.projLen,2,2)
        self.projRotL = QtGui.QLabel('Rotation')
        self.projRot = QtGui.QSpinBox()
        self.projRot.setRange(0,360)
        self.projRot.setValue(270)
        self.projRot.setMaximumWidth(50)
        self.projLayout.addWidget(self.projRotL,3,0)
        self.projLayout.addWidget(self.projRot,3,1)
        self.projRot.valueChanged.connect(self.projectorPositionChanged)
        
        self.tankLength = LabeledSpinBox(None, 'Tank Len (mm)', 0,100,46,60)
        self.projLayout.addWidget(self.tankLength, 4,0,1,2)
        self.tankWidth = LabeledSpinBox(None, 'Tank Wid (mm)', 0,100,21,60)
        self.projLayout.addWidget(self.tankWidth, 4,2,1,2)

        self.projLayout.setColumnStretch(6,1)
        self.projGroup.setLayout(self.projLayout)
        self.arenaLayout.addWidget(self.camCalibButton, 0,0)
        self.arenaLayout.addWidget(self.projGroup,1,0)
        self.arenaLayout.setColumnStretch(1,1)
        self.arenaGroup.setLayout(self.arenaLayout)

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

        #start button for individual tank
        self.startButton = QtGui.QPushButton('Start')
        self.startButton.setMaximumWidth(150)
        self.startButton.clicked.connect(self.startstop)
        
        #whether to save movie  
        self.paramSaveMovie = QtGui.QCheckBox('Save Movie')
            
        #experimental parameters groupbox
        self.paramGroup = QtGui.QGroupBox()
        self.paramGroup.setTitle('Exprimental Parameters')
        self.paramLayout = QtGui.QGridLayout()
        self.paramLayout.setHorizontalSpacing(2)
        self.paramLayout.setVerticalSpacing(2)

        #high level flow - Between - OMR Session ( OMRS1 - pause - OMRS2
        self.paramNumSession = LabeledSpinBox(None, 'Number of Sessions', 0, 200, 20, 60) #Number of Sessions (a cluster of OMR tests)
        self.paramLayout.addWidget(self.paramNumSession, 1,0,1,2) 
        self.paramBetween = LabeledSpinBox(None, 'Between Sessions (s)', 0, 1200, 300, 60)
        self.paramLayout.addWidget(self.paramBetween, 1,2,1,2)
        self.paramOMRPerSession = LabeledSpinBox(None,'#OMR Per Session', 0 ,10,4,60) # Number of OMR tests per session
        self.paramLayout.addWidget(self.paramOMRPerSession, 2,0,1,2)        
        self.paramOMRDuration = LabeledSpinBox(None,'OMR Duration (s)',0,60,12,60) #duration of each OMR tests
        self.paramLayout.addWidget(self.paramOMRDuration, 2,2,1,2)
        self.paramOMRInterval = LabeledSpinBox(None,'OMR Interval (s)',0,60 ,3 ,60) #time between OMR tests
        self.paramLayout.addWidget(self.paramOMRInterval,3,0,1,2)
 
        #Vision properities of OMR
        self.paramOMRPeriod = LabeledSpinBox(None,'OMR Grating Period (mm)',0,50,5,60) #spacing between grating bars
        self.paramLayout.addWidget(self.paramOMRPeriod,4,0,1,2)
        self.paramOMRDutyCycle = LabeledSpinBox(None, 'OMR DutyCycle %',0,100,50,60) #width of grating bars
        self.paramLayout.addWidget(self.paramOMRDutyCycle,4,2,1,2)
        self.paramOMRVelocity = LabeledSpinBox(None, 'OMR Speed (mm/s)',0,50,5,60) #velocity of moving gratings.
        self.paramLayout.addWidget(self.paramOMRVelocity,5,0,1,2)

        (self.paramColorNeutral,self.labelColorNeutral) = self.getColorComboBox('Neutral', 0)
        self.paramLayout.addWidget(self.paramColorNeutral,6,0)
        self.paramLayout.addWidget(self.labelColorNeutral,6,1)
        (self.paramColorOMR,self.labelColorOMR) = self.getColorComboBox('OMR', 5)
        self.paramLayout.addWidget(self.paramColorOMR,6,2)
        self.paramLayout.addWidget(self.labelColorOMR,6,3)
        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', default=os.path.expanduser('~/data/test'))
        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,1,2)
        self.settingsLayout.addWidget(self.trackWidget,1,0,1,2)
        self.settingsLayout.addWidget(self.infoGroup,2,0,1,2)
        self.settingsLayout.addWidget(self.startButton,3,0,1,1)
        self.settingsLayout.addWidget(self.paramSaveMovie,3,1,1,1)
        self.settingsLayout.addWidget(self.paramGroup,4,0,1,2)
        self.setLayout(self.settingsLayout)

        #HACK Couldn't quick initialize arena lcation until after frist image arrives, so stuck in onNewFrame

        #initialize projector
        self.projectorPositionChanged()
        self.t = 0

    def getColorComboBox(self, labelName, defaultNdx=1):
        colorBox = QtGui.QComboBox()
        colorBox.addItem('White')
        colorBox.addItem('Red')
        colorBox.addItem('Blue')
        colorBox.addItem('Gray')
        colorBox.addItem('Magenta')
        colorBox.addItem('Black')
        colorBox.setCurrentIndex(defaultNdx)
        colorLabel = QtGui.QLabel(labelName)
        return (colorBox,colorLabel)

    def configTank(self, ardConfig, camPos, projPos):
        #set tank position in camera
        self.resetCamCorners = camPos

        #set tank position in projector
        self.projX.setValue(projPos[0])
        self.projY.setValue(projPos[1])
        self.projWid.setValue(projPos[2])
        self.projLen.setValue(projPos[3])
        self.projRot.setValue(projPos[4])
        

    #---------------------------------------------------
    # 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 experiment is running then keep track of the average fish speed in one minute blocks
                if not self.is_off():
                    self.fishPosBuffer.append(pos)
                    self.fishPosBufferT.append(time)
                    if (self.fishPosBufferT[-1] - self.fishPosBufferT[0]) > 60: #averaging every 60 seconds, must match constant in drawDisplayOverlay               
                        self.fishPosBuffer = [np.sqrt((f1[0]-f0[0])**2 + (f1[1]-f0[1])**2) for f0, f1 in pairwise(self.fishPosBuffer)]
                        self.averageSpeed.append(np.mean(self.fishPosBuffer)/(self.fishPosBufferT[-1] - self.fishPosBufferT[0])) #pixels/sec
                        self.fishPosBuffer = []
                        self.fishPosBufferT = []

            if not self.is_off():
                #record location
                self.arenaData['video'].append((self.frameTime, None)) 
                d = [self.frameTime, pos[0], pos[1]]
                self.arenaData['tracking'].append(tuple(d))
                if self.paramSaveMovie.isChecked():                
                    img = np.array(self.currCvFrame[:,:]) #convert IplImage into numpy array
                    img = img[self.arenaBB[0][1]:self.arenaBB[1][1],self.arenaBB[0][0]:self.arenaBB[1][0]] 
                    self.movie_logger.write_frame(img)

            self.arenaView = view

            #HACK: couldn't quickly initilize cam corner before currCvFrame is set, so I stuck it here.
            #not a great place since conditoin will be checked frequently
            if self.resetCamCorners is not None:
                self.setArenaCamCorners(self.resetCamCorners)
                self.resetCamCorners = None

        except:
            print 'ClassicalConditioningController:onNewFrame failed'
            traceback.print_exc()
            QtCore.pyqtRemoveInputHook() 
            ipdb.set_trace()
        finally:
            self.mutex.release()

  
    def updateState(self):
        self.updateExperimentalState()
        
    def updateExperimentalState(self):
        if not self.is_off():
            self.mutex.acquire()
            try:
                self.t = time.time()

                #Check if it time to transition to next high level state (acclimate->baseline->trials->post->off)
                if self.t > self.nextStateTime:
                    if self.is_between_session():
                        if self.nSession < self.paramNumSession.value():
                            self.nOMR = 0 #TODO move to callback for start session
                            self.nSession+=1 #TODO move to callback for start session
                            self.start_session()
                        else:
                            self.quit()

                    elif self.is_omr_to_s1() or self.is_omr_to_s2():
                        self.stop_omr()
                            
                    elif self.is_no_omr_post_s1() or self.is_no_omr_post_s2():
                        if self.nOMR < self.paramOMRPerSession.value():
                            self.start_omr()
                        else:
                            self.stop_session()

                #handle omr projector updates
                if self.is_omr_to_s1() or self.is_omr_to_s2():
                    if self.t - self.omrLastUpdate > 0.010: #only updated display once sufficient time has passed.
                        if self.t - self.omrLastUpdate > 0.060:
                            print 'WARNING: projector slow to update', self.t - self.omrLastUpdate
                        self.updateProjectorDisplay()

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

    def projectorPositionChanged(self):
        #the point defining the tank are such that:
        #points 1 and 2 define side1
        #points 2 and 3 define the 'near' connecting edge
        #points 3 and 4 define side2
        #points 4 and 1 define the 'far' connecting edge.
 
        #move this code elsewhere to avoid redundant computation...
        self.pts = np.array([[0                   , 0],
                             [0                   , self.projWid.value()],
                             [self.projLen.value(), self.projWid.value()],
                             [self.projLen.value(), 0]])
        theta = self.projRot.value()
        self.tankRotM = np.array([[cos(radians(theta)), -sin(radians(theta))],
                         [sin(radians(theta)),  cos(radians(theta))]]);
        self.pts = np.dot(self.tankRotM,self.pts.T).T
        self.pts += [self.projX.value(), self.projY.value()]
        self.updateProjectorDisplay()

    def drawProjectorDisplay(self, painter):
        if self.is_off() and self.projCalibButton.isChecked() and self.isCurrent():
            #DRAW A CALIBRATION IMAGE THAT INDICATES THE PROJECTOR LOCATION AND SIDE1
            side1Color = QtCore.Qt.red
            side2Color = QtCore.Qt.blue
            a = .5
            b = 1-a
            #Draw side one
            pen = QtGui.QPen(QtCore.Qt.NoPen)
            brush = QtGui.QBrush(side1Color)
            painter.setBrush(brush)
            painter.setPen(pen)
            poly = QtGui.QPolygonF()
            poly.append(QtCore.QPointF(self.pts[0,0], self.pts[0,1]))
            poly.append(QtCore.QPointF(self.pts[1,0], self.pts[1,1]))
            poly.append(QtCore.QPointF(a*self.pts[1,0] + b*self.pts[2,0], a*self.pts[1,1] + b*self.pts[2,1]))
            poly.append(QtCore.QPointF(a*self.pts[0,0] + b*self.pts[3,0], a*self.pts[0,1] + b*self.pts[3,1]))
            painter.drawPolygon(poly)
            painter.setPen(QtGui.QColor(168, 34, 3))
            painter.setFont(QtGui.QFont('Decorative',24))
            painter.drawText(self.pts[0,0],self.pts[0,1],'1')
            painter.drawText(self.pts[1,0],self.pts[1,1],'2')
            painter.drawText(self.pts[2,0],self.pts[2,1],'3')
            painter.drawText(self.pts[3,0],self.pts[3,1],'4')
            #Draw side two
            pen = QtGui.QPen(QtCore.Qt.NoPen)
            brush = QtGui.QBrush(side2Color)
            painter.setBrush(brush)
            painter.setPen(pen)
            poly = QtGui.QPolygonF()
            poly.append(QtCore.QPointF(a*self.pts[0,0] + b*self.pts[3,0], a*self.pts[0,1] + b*self.pts[3,1]))
            poly.append(QtCore.QPointF(a*self.pts[1,0] + b*self.pts[2,0], a*self.pts[1,1] + b*self.pts[2,1]))
            poly.append(QtCore.QPointF(self.pts[2,0], self.pts[2,1]))
            poly.append(QtCore.QPointF(self.pts[3,0], self.pts[3,1]))
            painter.drawPolygon(poly)
        else:
            #Draw Background
            backColor = self.paramColorNeutral.currentIndex()
            pen = QtGui.QPen(QtCore.Qt.NoPen)
            brush = self.getBrush(backColor)
            painter.setBrush(brush)
            painter.setPen(pen)
            poly = QtGui.QPolygonF()
            poly.append(QtCore.QPointF(self.pts[0,0], self.pts[0,1]))
            poly.append(QtCore.QPointF(self.pts[1,0], self.pts[1,1]))
            poly.append(QtCore.QPointF(self.pts[2,0], self.pts[2,1]))
            poly.append(QtCore.QPointF(self.pts[3,0], self.pts[3,1]))          
            painter.drawPolygon(poly)

            #Draw OMR grating
            if self.is_omr_to_s1() or self.is_omr_to_s2():
                #update the possition according to time since last update.
                if self.is_omr_to_s1():
                    self.omrPhase -= (float(self.paramOMRVelocity.value())/float(self.paramOMRPeriod.value())) * 360.0 * (self.t-self.omrLastUpdate)
                else:
                    self.omrPhase += (float(self.paramOMRVelocity.value())/float(self.paramOMRPeriod.value())) * 360.0 * (self.t-self.omrLastUpdate)
                self.omrPhase = self.omrPhase%360.0
                self.omrLastUpdate = self.t                    
                
                #This stuff doesn't need to computed on every frame
                mm2pix = math.hypot(self.pts[2,0]-self.pts[1,0], self.pts[2,1] - self.pts[1,1]) / self.tankLength.value()
                period = self.paramOMRPeriod.value() * mm2pix
                numBars = int(math.ceil(math.hypot(self.pts[2,0]-self.pts[1,0], self.pts[2,1] - self.pts[1,1]) / period))
                barWidth = period * (self.paramOMRDutyCycle.value()/100.0)
                    
                #Draw the bar
                phaseShift = self.omrPhase/360.0 * period  
                pen = QtGui.QPen(QtCore.Qt.NoPen)
                brush = self.getBrush(self.paramColorOMR.currentIndex())
                painter.setBrush(brush)
                painter.setPen(pen)
                for nBar in range(-1,numBars+1):
                    #define bar in unrotated untranslated space.
                    bl = min(max(phaseShift + nBar * period, 0), self.projLen.value()) 
                    br = min(max(phaseShift + nBar * period + barWidth, 0), self.projLen.value()) 
                    bar = np.array([[br,0],
                                    [bl,0],
                                    [bl,self.projWid.value()],
                                    [br,self.projWid.value()]])
                    #rotate and tranlate.
                    bar = np.dot(self.tankRotM, bar.T).T + [self.projX.value(), self.projY.value()]
                    #draw
                    poly = QtGui.QPolygonF()
                    poly.append(QtCore.QPointF(bar[0,0],bar[0,1]))
                    poly.append(QtCore.QPointF(bar[1,0],bar[1,1]))
                    poly.append(QtCore.QPointF(bar[2,0],bar[2,1]))
                    poly.append(QtCore.QPointF(bar[3,0],bar[3,1]))
                    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)
            if len(self.arenaCamCorners) >= 2:
                pen = QtGui.QPen(QtCore.Qt.red)
                pen.setWidth(2)
                painter.setPen(pen)
                painter.drawLine(self.arenaCamCorners[0][0],self.arenaCamCorners[0][1],
                                 self.arenaCamCorners[1][0],self.arenaCamCorners[1][1])
                painter.setPen(QtGui.QColor(168, 34, 3))
                painter.setFont(QtGui.QFont('Decorative',14))

                statustext = ""
                statustext = statustext + self.state
                if not self.is_off():
                    statustext = statustext + ": %d/%d %0.1f"%(self.nSession, self.paramNumSession.value(), self.nextStateTime-self.t)
                if self.is_omr_to_s1() or self.is_omr_to_s2() or self.is_no_omr_post_s1() or self.is_no_omr_post_s2():
                    statustext = statustext + ": %d/%d"%(self.nOMR, self.paramOMRPerSession.value())

                painter.drawText(self.arenaCamCorners[0][0],self.arenaCamCorners[0][1],statustext)

                #Plot average speed of fish overtime
                if not self.is_off() and len(self.averageSpeed)>2:
                    ac = np.array(self.arenaCamCorners)
                    plt_b = np.max(ac[:,1]) #Plot bottom
                    plt_l = np.min(ac[:,0]) #Plot left
                    plt_r = np.max(ac[:,0]) #Plot right

                    totalDuration = self.paramBetween.value() + self.paramNumSession.value()*(self.paramBetween.value()+
                                    self.paramOMRPerSession.value() * (self.paramOMRDuration.value() + self.paramOMRInterval.value()))
                    totalDuration = totalDuration*(60/60.0) #averaging every N seconds (see onnewframe)
                    ndx = np.arange(len(self.averageSpeed))
                    x = (np.arange(len(self.averageSpeed))/totalDuration) 
                    x = x * (plt_r - plt_l) 
                    x = x + plt_l
                    #scale = 75 / np.max(self.averageSpeed)
                    scale = 3000
                    y = plt_b - (scale * np.array(self.averageSpeed))
                    for ((x1,x2),(y1,y2)) in zip(pairwise(x),pairwise(y)):
                        painter.drawLine(x1,y1,x2,y2)


    def start(self):
        self.startstop()

    def stop(self):
        self.startstop()

    #---------------------------------------------------
    # STATE MACHINE CALLBACK METHODS
    #---------------------------------------------------

    def update_state_data(self):
        # runs on every state transition...
        self.updateProjectorDisplay()
        self.arenaData['stateinfo'].append((self.t, 0, 0, 0, self.state))
        #self.saveResults() #just save at end to avoid long interframe intervals

    def on_exit_off(self):
        #experiment has started so disable parts of UI
        self.paramGroup.setDisabled(True)
        self.infoGroup.setDisabled(True)
        self.startButton.setText('Stop')

        #experiment has started so prepare result files and data structures
        td = datetime.datetime.now()
        self.saveLocation = str(self.infoDir.text())
        [p, self.fnResults] = os.path.split(self.saveLocation)
        self.fnResults = self.fnResults + '_' + td.strftime('%Y-%m-%d-%H-%M-%S')
        self.jsonFileName = str(self.infoDir.text()) + os.sep + self.fnResults  + '.json'

        #prepare to write movie
        if self.paramSaveMovie.isChecked():
            self.movieFileName = str(self.infoDir.text()) + os.sep + self.fnResults  + '.mp4'
            from moviepy.video.io.ffmpeg_writer import FFMPEG_VideoWriter as VidWriter
            self.movie_logger = VidWriter(filename=self.movieFileName,
                                          size=(self.arenaBB[1][0] - self.arenaBB[0][0],
                                                self.arenaBB[1][1] - self.arenaBB[0][1]),
                                          fps=15,
                                          codec='mpeg4',
                                          preset='ultrafast')

        self.initArenaData()
        self.nSession = 0
        self.averageSpeed = []

    def on_enter_between_session(self):
        self.nextStateTime = self.t + self.paramBetween.value()

    def on_enter_omr_to_s1(self):
        self.nOMR += 1
        self.nextStateTime = self.t + self.paramOMRDuration.value()
        self.omrPhase = 0
        self.omrLastUpdate = self.t #updates everytime the projectory updates 
        self.updateProjectorDisplay()

    def on_exit_omr_to_s1(self):
        self.updateProjectorDisplay()

    def on_enter_omr_to_s2(self):
        self.nOMR += 1
        self.nextStateTime = self.t + self.paramOMRDuration.value()
        self.omrPhase = 0 
        self.omrLastUpdate = self.t #updates everytime the projectory updates 
        self.updateProjectorDisplay()

    def on_exit_omr_to_s2(self):
        self.updateProjectorDisplay()

    def on_enter_no_omr_post_s1(self):
        self.nextStateTime = self.t + self.paramOMRInterval.value()
        self.updateProjectorDisplay()

    def on_enter_no_omr_post_s2(self):
        self.nextStateTime = self.t + self.paramOMRInterval.value()
        self.updateProjectorDisplay()

    def on_enter_off(self):
        self.nextStateTime = None
        self.updateProjectorDisplay()

        self.saveResults()

        #note: the projector colors will be reset on call to after_state_change
        if self.paramSaveMovie.isChecked():
            self.movie_logger.close()

        #experiment has ended so enable parts of UI
        self.paramGroup.setDisabled(False)
        self.infoGroup.setDisabled(False)
        self.startButton.setText('Start')

    def isReadyToStart(self):
        if not os.path.exists(self.infoDir.text()):
            try:
                os.mkdir(self.infoDir.text())
            except:
                self.arenaMain.statusBar().showMessage('%s arena not ready to start.  Experiment directory does not exist and cannot be created.'%(self.getYokedStr()))
                return False

        if not self.arenaCamCorners:
            self.arenaMain.statusBar().showMessage('%s arena not ready to start.  The arena location has not been specified.'%(self.getYokedStr()))
            return False

        if not self.trackWidget.getBackgroundImage():
            self.arenaMain.statusBar().showMessage('%s arena not ready to start.  Background image has not been taken.'%(self.getYokedStr()))
            return False

        #if not self.fishImg:
        #    self.arenaMain.statusBar().showMessage('%s arena not ready to start.  Fish image has not been taken.'%(self.getYokedStr()))
        #    return False

        return True

    #---------------------------------------------------
    # SLOT CALLBACK METHODS
    #---------------------------------------------------

    def startstop(self):
        self.mutex.acquire()
        try:
            self.t = time.time()

            if self.is_off():
                self.begin()
            else:
                self.quit(); 
        except:
            print 'Start Failed:'
            traceback.print_exc()
            QtCore.pyqtRemoveInputHook()
            ipdb.set_trace()
        finally:
            self.mutex.release()

    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.setArenaCamCorners(self.arenaCamCorners)
               
    def setArenaCamCorners(self, corners):
        #corners must be tuple of tuples (not list or np.array)
        self.currArenaclick=4
        self.arenaCamCorners = corners
        print corners
        [self.arenaMidLine, self.arenaSide1Sign] = self.processArenaCorners(self.arenaCamCorners, .5)
        #compute bounding box with vertical and horizontal sides.
        self.arenaBB = [[min([p[0] for p in corners]), min([p[1] for p in corners])],
                        [max([p[0] for p in corners]), max([p[1] for p in corners])]]
        #bounding box needs to have even heights and widths in order to save as mp4
        if (self.arenaBB[1][0] - self.arenaBB[0][0]) % 2:
            self.arenaBB[1][0] += 1
        if (self.arenaBB[1][1] - self.arenaBB[0][1]) % 2:
            self.arenaBB[1][1] += 1


        self.getArenaMask()
        self.trackWidget.setBackgroundImage()
                                             
    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('')

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

    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  

    #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 initArenaData(self):
        #prepare output data structure
        self.arenaData = {}
        self.arenaData['fishbirthday'] = str(self.infoDOB.date().toPyDate())
        self.arenaData['fishage'] =  (datetime.date.today() - self.infoDOB.date().toPyDate()).days
        self.arenaData['fishstrain'] = str(self.infoType.text())
        self.arenaData['fishsize'] = self.fishSize

        self.arenaData['parameters'] = { 'Num Sessions':self.paramNumSession.value(),
                                         'Between (s)':self.paramBetween.value(),
                                         'OMRPerSession':self.paramOMRPerSession.value(),
                                         'OMR Duration (s)':self.paramOMRDuration.value(),
                                         'OMR Interval (s)':self.paramOMRInterval.value(),
                                         'OMR Period (mm)': self.paramOMRPeriod.value(),
                                         'OMR Duty Cycle': self.paramOMRDutyCycle.value(),
                                         'OMR Velocity':self.paramOMRVelocity.value(),
                                         'NeutralColor':str(self.paramColorNeutral.currentText()),
                                         'OMRColor':str(self.paramColorOMR.currentText()),
                                         'states':self.states.keys(),
                                         'CodeVersion':None }

        self.arenaData['trackingParameters'] = self.trackWidget.getParameterDictionary()
        self.arenaData['trackingParameters']['arenaPoly'] = self.arenaCamCorners 
        self.arenaData['trackingParameters']['arenaDivideLine'] = self.arenaMidLine
        self.arenaData['trackingParameters']['arenaSide1Sign'] = self.arenaSide1Sign
        self.arenaData['projectorParameters'] = {'position':[self.projX.value(), self.projY.value()],
                                                 'size':[self.projLen.value(), self.projWid.value()],
                                                 'rotation':self.projRot.value()}
        self.arenaData['tankSize_mm'] = [self.tankLength.value(), self.tankWidth.value()]
        self.arenaData['trials'] = list() #outcome on each trial
        self.arenaData['tracking'] = list() #list of tuples (frametime, posx, posy)
        self.arenaData['video'] = list() #list of tuples (frametime, filename)	
        self.arenaData['stateinfo'] = list() #list of times at switch stimulus flipped.
        self.arenaData['shockinfo'] = list()

        t = datetime.datetime.now()
      
        #save experiment images
        self.bcvImgFileName = str(self.infoDir.text()) + os.sep + self.fnResults  + '_BackImg_' + t.strftime('%Y-%m-%d-%H-%M-%S') + '.tiff'
        cv.SaveImage(self.bcvImgFileName, self.trackWidget.getBackgroundImage())	
        if self.fishImg:
            self.fishImgFileName = str(self.infoDir.text()) + os.sep +  self.fnResults + '_FishImg_' + t.strftime('%Y-%m-%d-%H-%M-%S') + '.tiff'
            cv.SaveImage(self.fishImgFileName, self.fishImg)

    def saveResults(self):
        f = open(name=self.jsonFileName, mode='w')
        json.dump(self.arenaData,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)) 
        elif colorNdx == 4:
            return QtGui.QBrush(QtCore.Qt.magenta)
        elif colorNdx == 5:
            return QtGui.QBrush(QtCore.Qt.black)
        else:
            return QtGui.QBrush(QtCore.Qt.black)

    def useDebugParams(self):
        pass
Esempio n. 6
0
class FishTrackerWidget(QtGui.QGroupBox):

    def __init__(self, parent, ftDisp):
        #ftDisp is necessary so the FishTrackerWidget can collect mouse clicks
        
        super(FishTrackerWidget, self).__init__(parent)

        self.ftDisp = ftDisp

        self.currCvFrame = None
        self.arenaCvMask = None #track mask
        self.bcvImg = None #background image for subtraction
        self.currG = None
        self.maskG = None
        self.backG = None
        self.backG32 = None
        self.diffG = None
        self.thrsG = None
        self.thrsMG = None
        self.tracEG = None
        self.tracDG = None
        self.fmask = None
        self.bFixBackgroundNow = False

        self.setTitle('Tracking Parameters')
        self.trackLayout = QtGui.QGridLayout(self)
        self.trackLayout.setHorizontalSpacing(3)
        self.trackLayout.setVerticalSpacing(3)

        self.viewLabel = QtGui.QLabel('Track Display Mode:')
        self.viewLabel.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
        self.viewLabel.adjustSize()
        self.selView = QtGui.QComboBox(self)
        self.selView.addItem('Raw')
        self.selView.addItem('Background')
        self.selView.addItem('Subtracted')
        self.selView.addItem('Thresholded')
        self.selView.addItem('Masked')
        self.selView.addItem('Eroded')
        self.selView.addItem('Dilated')
        self.selView.addItem('Mask')
        self.trackLayout.addWidget(self.viewLabel, 0,0,1,2)
        self.trackLayout.addWidget(self.selView, 0,2,1,2)

        self.bgButton = QtGui.QPushButton('Get Background')
        self.bgButton.clicked.connect(self.setBackgroundImage)
        self.trackLayout.addWidget(self.bgButton, 1,0,1,2)

        self.fixButton = QtGui.QPushButton('Fix Background')
        self.fixButton.clicked.connect(self.fixBackground)
        self.trackLayout.addWidget(self.fixButton, 1,2,1,2)

        self.trackThreshold = LabeledSpinBox(None,'Threshold',0,255,3,60)
        self.trackErode = LabeledSpinBox(None,'Erode',0,20,0,60)
        self.trackDilate = LabeledSpinBox(None,'Dilate',0,20,0,60)
        self.trackMinArea = LabeledSpinBox(None,'MinArea',0,600000,0,75)
        self.trackMaxArea = LabeledSpinBox(None,'MaxArea',0,600000,600000,75)
        self.updateCheckbox = QtGui.QCheckBox('Use Updating BG')
        self.updateCheckbox.setChecked(True)
        self.numFish = LabeledSpinBox(None,'NumFish',0,20,1,60)
        self.fishSize = LabeledSpinBox(None,'FG Size',0,1000,30,60)
        self.learningRate = LabeledDoubleSpinBox(None,'Learning rate',0,1,0.1,60)

        self.trackLayout.addWidget(self.trackThreshold,2,0,1,2)
        self.trackLayout.addWidget(self.trackErode, 2,2,1,2)
        self.trackLayout.addWidget(self.trackDilate, 3,0,1,2)        
        self.trackLayout.addWidget(self.trackMinArea, 3,2,1,2)
        self.trackLayout.addWidget(self.trackMaxArea, 4,0,1,2) 
        self.trackLayout.addWidget(self.updateCheckbox,5,0,1,2)
        self.trackLayout.addWidget(self.numFish,5,2,1,2)
        self.trackLayout.addWidget(self.fishSize,6,0,1,2)
        self.trackLayout.addWidget(self.learningRate,6,2,1,2)

        self.setLayout(self.trackLayout)

    def setBackgroundImage(self):
        if self.currCvFrame:
            self.bcvImg = cv.CloneImage(self.currCvFrame)
            if not self.maskG:
                self.backG = cv.CreateImage(cv.GetSize(self.currCvFrame), cv.IPL_DEPTH_8U, 1)
                self.backG32 = cv.CreateImage(cv.GetSize(self.currCvFrame), cv.IPL_DEPTH_32F, 1)
                cv.CvtColor(self.bcvImg, self.backG, cv.CV_BGR2GRAY)
                cv.ConvertScale(self.backG, self.backG32)
            else:
                self.backG = cv.CreateImage(cv.GetSize(self.maskG), cv.IPL_DEPTH_8U, 1)
                self.backG32 = cv.CreateImage(cv.GetSize(self.maskG), cv.IPL_DEPTH_32F, 1)
                cv.CvtColor(self.bcvImg[self.maskBoundedR0:self.maskBoundedR0+self.maskBoundedHeight, 
                                        self.maskBoundedC0:self.maskBoundedC0+self.maskBoundedWidth], self.backG, cv.CV_BGR2GRAY)
                cv.ConvertScale(self.backG, self.backG32)

    def getBackgroundImage(self):
        return self.bcvImg

    def fixBackground(self):
        self.ftDisp.clicked.connect(self.handleFixBackgroundClick)
        self.bFixBackgroundNow = False

    def handleFixBackgroundClick(self,x,y):
        self.correctFish = (x - self.maskBoundedC0, y - self.maskBoundedR0)
        self.ftDisp.clicked.disconnect(self.handleFixBackgroundClick)
        self.bFixBackgroundNow = True

    def setTrackMask(self, img):
        #Convert mask to gray scale.
        self.arenaCvMask = img
        self.maskG = cv.CreateImage((self.arenaCvMask.width, self.arenaCvMask.height), cv.IPL_DEPTH_8U, 1)
        cv.CvtColor(self.arenaCvMask, self.maskG, cv.CV_BGR2GRAY)
        
        #get bounding rectangle of mask and resize it
        maskArray = np.asarray(self.maskG[:,:])
        self.maskBoundedC0 = np.min(np.nonzero(np.max(maskArray,0))) #X = width = cols
        self.maskBoundedR0 = np.min(np.nonzero(np.max(maskArray,1))) #Y = height = rows
        self.maskBoundedWidth = np.max(np.nonzero(np.max(maskArray,0))) - self.maskBoundedC0 + 1
        self.maskBoundedHeight = np.max(np.nonzero(np.max(maskArray,1))) - self.maskBoundedR0 + 1
        self.maskG = cv.CreateImage((self.maskBoundedWidth, self.maskBoundedHeight), cv.IPL_DEPTH_8U, 1)
        cv.CvtColor(self.arenaCvMask[self.maskBoundedR0:self.maskBoundedR0+self.maskBoundedHeight, 
                                     self.maskBoundedC0:self.maskBoundedC0+self.maskBoundedWidth], self.maskG, cv.CV_BGR2GRAY)

        #recrop background image
        if self.bcvImg:
            self.backG = cv.CreateImage(cv.GetSize(self.maskG), cv.IPL_DEPTH_8U, 1)
            self.backG32 = cv.CreateImage(cv.GetSize(self.maskG), cv.IPL_DEPTH_32F, 1)
            cv.CvtColor(self.bcvImg[self.maskBoundedR0:self.maskBoundedR0+self.maskBoundedHeight, 
                                    self.maskBoundedC0:self.maskBoundedC0+self.maskBoundedWidth], self.backG, cv.CV_BGR2GRAY)
            cv.ConvertScale(self.backG, self.backG32)

        #resize temp images to bounded rect size
        self.currG = cv.CreateImage((self.maskBoundedWidth, self.maskBoundedHeight), cv.IPL_DEPTH_8U, 1)	
        self.diffG = cv.CreateImage((self.maskBoundedWidth, self.maskBoundedHeight), cv.IPL_DEPTH_8U, 1)
        self.thrsG = cv.CreateImage((self.maskBoundedWidth, self.maskBoundedHeight), cv.IPL_DEPTH_8U, 1)
        self.thrsMG = cv.CreateImage((self.maskBoundedWidth, self.maskBoundedHeight), cv.IPL_DEPTH_8U, 1)
        self.tracEG = cv.CreateImage((self.maskBoundedWidth, self.maskBoundedHeight), cv.IPL_DEPTH_8U, 1)
        self.tracDG = cv.CreateImage((self.maskBoundedWidth, self.maskBoundedHeight), cv.IPL_DEPTH_8U, 1)
        self.tracG = cv.CreateImage((self.maskBoundedWidth, self.maskBoundedHeight), cv.IPL_DEPTH_8U, 1)
        self.fmask = cv.CreateImage((self.maskBoundedWidth, self.maskBoundedHeight), cv.IPL_DEPTH_8U, 1)

    def getParameterDictionary(self):
        pd = {}
        pd['nDiffThreshold'] = self.trackThreshold.value()
        pd['nErode'] = self.trackErode.value()
        pd['nDilate'] = self.trackDilate.value()
        return pd

    def getTrackDisplay(self):
        if self.currCvFrame == None:
            return None

        dispImg = cv.CreateImage(cv.GetSize(self.currCvFrame), cv.IPL_DEPTH_8U, 1)
        cv.CvtColor(self.currCvFrame, dispImg, cv.CV_BGR2GRAY)
        if self.maskG:
            cv.SetImageROI(dispImg, (self.maskBoundedC0, self.maskBoundedR0, self.maskBoundedWidth, self.maskBoundedHeight))
      
        dispModeNdx = self.selView.currentIndex()
        if dispModeNdx == 1 and self.backG:
            cv.Copy(self.backG, dispImg)
        elif dispModeNdx == 2 and self.diffG:
            cv.Copy(self.diffG, dispImg)
        elif dispModeNdx == 3 and self.thrsG:
            cv.Copy(self.thrsG, dispImg)
        elif dispModeNdx == 4 and self.thrsMG:
            cv.Copy(self.thrsMG, dispImg)
        elif dispModeNdx == 5 and self.tracEG:
            cv.Copy(self.tracEG, dispImg)
        elif dispModeNdx == 6 and self.tracDG:
            cv.Copy(self.tracDG, dispImg)
        elif dispModeNdx == 7 and self.maskG:
            cv.Copy(self.maskG, dispImg)
        else:
            return self.currCvFrame

        cv.ResetImageROI(dispImg)
        return dispImg

    def findFish(self, cvImg):
        ####NEED TO MODIFY TO USE CV2 
        self.currCvFrame = cvImg
        foundFish = False
        fishPos = (0,0)
        allFish = []
        if not self.bcvImg == None and not self.arenaCvMask == None:
            #Background subtract, threshold, mask, erode and dilate
            cv.CvtColor(self.currCvFrame[self.maskBoundedR0:self.maskBoundedR0+self.maskBoundedHeight, 
                                         self.maskBoundedC0:self.maskBoundedC0+self.maskBoundedWidth], self.currG, cv.CV_BGR2GRAY)
            cv.AbsDiff(self.currG, self.backG, self.diffG) #difference
            cv.Threshold ( self.diffG , self.thrsG , self.trackThreshold.value() , 255 , cv.CV_THRESH_BINARY ) #threshold
            cv.And( self.thrsG, self.maskG, self.thrsMG ) #mask
            cv.Erode(self.thrsMG, self.tracEG, erodeKernal, self.trackErode.value()) #erode
            cv.Dilate(self.tracEG, self.tracDG, dilateKernal, self.trackDilate.value()) #dilate
            cv.Copy(self.tracDG, self.tracG)

            #Get List of connected components	
            seq = cv.FindContours(self.tracG, cv.CreateMemStorage(0), cv.CV_RETR_EXTERNAL, cv.CV_CHAIN_APPROX_NONE)		
            #Get each connected components area and center of mass (first moments)
            areas = []
            moments = []	
            if len(seq)>0:
                while seq != None:
                    #bx, by, bwidth, bheight = cv.BoundingRect(seq, 0)
                    areas.append(cv.ContourArea(seq))
                    moments.append(cv.Moments(seq))
                    seq = seq.h_next()

                #get the largest connected component
                ndx = areas.index(max(areas))
                if (moments[ndx].m00 > self.trackMinArea.value() and moments[ndx].m00 < self.trackMaxArea.value()):
                    foundFish = True
                    fishPos = (moments[ndx].m10/moments[ndx].m00 + self.maskBoundedC0, moments[ndx].m01/moments[ndx].m00 + self.maskBoundedR0)
                #get all fish sorted by size.
                sndx = np.argsort(areas)[::-1]
                for bn in sndx:
                    if (moments[bn].m00 > self.trackMinArea.value() and moments[bn].m00 < self.trackMaxArea.value()):
                        allFish.append((moments[bn].m10/moments[bn].m00 + self.maskBoundedC0, moments[bn].m01/moments[bn].m00 + self.maskBoundedR0))
            del seq  

            #update background image:
            if self.bFixBackgroundNow:
                cv.Rectangle(self.fmask,(0,0),cv.GetSize(self.fmask),255,-1)
                cv.Circle(self.fmask, tuple([int(x) for x in self.correctFish]), self.fishSize.value(), 0, -1)
                cv.Copy(self.currG, self.backG, self.fmask)
                cv.ConvertScale(self.backG, self.backG32) 
                self.bFixBackgroundNow = False
            elif self.updateCheckbox.isChecked():
                if foundFish:
                    cv.Rectangle(self.fmask,(0,0),cv.GetSize(self.fmask),255,-1)
                    for nFish in range(min(self.numFish.value(),len(allFish))):
                        pos = (int(allFish[nFish][0] - self.maskBoundedC0), int(allFish[nFish][1] - self.maskBoundedR0))
                        cv.Circle(self.fmask, pos, self.fishSize.value(),0,-1)
                    cv.RunningAvg(self.currG, self.backG32, self.learningRate.value(), self.fmask)
                    cv.ConvertScaleAbs(self.backG32,self.backG)

        return (foundFish, fishPos, self.getTrackDisplay(), allFish)