Exemple #1
0
    def openNew(self):
        '''
        Select and open a video file from the system file manager for analysis.
        '''

        self.filename = QtWidgets.QFileDialog.getOpenFileName(self, 'Open Video', self.vid_dir)
        self.filename = str(self.filename[0])
        # We get a zero division error on self.length if the filename is empty.
        try:
            # Don't even try if user pressed 'Cancel' in file dialog
            if not self.filename == '':
                print(self.filename+'\n')
                self.vid_dir = path.dirname(path.realpath(self.filename))
                self.writeConfig()
                self.capture = cv2.VideoCapture(self.filename)
                self.capture.open(self.filename)
                self.length = int(self.capture.get(7) / self.capture.get(5))
                self.endTimemsec = self.length * 1000
                self.slide.setMinimum(0)
                self.slide.setMaximum(self.length)
                self.slide.setTracking(0)
                self.ui.lcdNumber.setNumDigits(8)
                self.ui.lcdNumber.display('00:00:00')
                self.slide.setSliderPosition(0)
                self.cont = 0
                self.captureNextFrame()

        except ZeroDivisionError as excpt:
            print('type is: ', excpt.__class__.__name__)
            print_exc()
            errorNotif(self, '<br>Not a recognized video format.</br>')
            self.filename = 0
Exemple #2
0
 def fileCheck(self):
     """Make sure user-selected file is actually a video."""
     msg_video = '<br>Not a recognized video format.</br>'
     # Test if the file was encoded in a recognizable video codec type. If
     # not, it's not a (useable) video.
     codec = self.capture.get(6)
     if codec == 0.0:
         errorNotif(self, msg_video)
         return 'error'
Exemple #3
0
    def timeCheck(self, tsec):
        """
        Check the following:
         - Start time input is only integers (no letters or special characters)
         - Start time is in correct format (hh:mm:ss)
         - Second and minute input don't go above 59
        """
        tsec = str(tsec)
        time_check = tsec.split(':')
        msg_time = '<br>Time must be in hh:mm:ss format</br>.'

        # Only integers
        for num in time_check:
            if num.isdigit() is False:
                print('false')
                errorNotif(self, msg_time)
                return 'error'

        # Correct format
        if len(tsec) != 8:
            errorNotif(self, msg_time)
            return 'error'
        elif tsec[2] != ':' or tsec[5] != ':':
            errorNotif(self, msg_time)
            return 'error'

        # Second and minute are not too high
        elif int(time_check[-1]) > 59 or int(time_check[-2]) > 59:
            errorNotif(self, msg_time)
            return 'error'
Exemple #4
0
    def captureNextFrame(self):
        '''capture frame and reverse RGB BGR and return opencv image'''
        # Some file types, like .ico files, make it through the try-except in
        # self.openNew since OpenCV can read them. This should catch them.
        try:
            if self.cont == 0:
                ret, readFrame = self.capture.read()
                self.currentFrame = cv2.cvtColor(readFrame, cv2.COLOR_BGR2RGB)
                height,width = self.currentFrame.shape[:2]
                self.img = QtGui.QImage(self.currentFrame,
                                  width,
                                  height,
                                  QtGui.QImage.Format_RGB888)
                self.img = QtGui.QPixmap(self.img)
                self.ui.videoFrame.setPixmap(self.img.scaled(self.ui.videoFrame.size(), QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation))
                if self.capture.get(0) > self.endTimemsec:
                    self.capture.set(0, self.startTimemsec)
                self._timer.timeout.connect(self.tick)
                self._timer.start()

            elif self.cont == 1:
                ret, readFrame = self.capture.read()
                self.currentFrame = cv2.cvtColor(readFrame,cv2.COLOR_BGR2RGB)
                height,width = self.currentFrame.shape[:2]
                self.img = QtGui.QImage(self.currentFrame,
                                  width,
                                  height,
                                  QtGui.QImage.Format_RGB888)
                self.img = QtGui.QPixmap(self.img)
                self.ui.videoFrame.setPixmap(self.img.scaled(self.ui.videoFrame.size(),
                    QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation))
                if self.capture.get(0) >= self.endTimemsec:
                    self.capture.set(0, self.startTimemsec)
                self._timer.timeout.connect(self.tick)
                self._timer.start()

            elif self.cont == 2:
                self._timer.stop()

        except cv2.error as excpt:
            print('type is: ', excpt.__class__.__name__)
            print_exc()
            self.capture.release()
            self._timer.stop()
            errorNotif(self, '<br>Not a recognized video format.</br>')
            return
Exemple #5
0
 def measLen(self):
     """Make sure the measurement length makes sense."""
     if str(self.lenOfMeas).isdigit() is False:
         msg_len = '<br>The given length of measurement cannot be understood.</br>'
         errorNotif(self, msg_len)
         return 'error'
Exemple #6
0
 def nameCheck(self):
     """Check that the video has been assigned."""
     if self.filename == 0 or self.filename == u'':
         msg_video = '<br>You have not selected a video!</br>'
         errorNotif(self, msg_video)
         return 'error'
Exemple #7
0
    def contourPressed(self):
        """Contour button has been pressed - begin analysis."""
        self.cont = 1
        # Get the number of mice, start time, and length of measurement.
        mice = self.ui.lineEdit_mouseID.text()
        tsec = self.ui.lineEdit_startT.text()
        self.lenOfMeas = self.ui.lineEdit_lenMeasure.text()

        # Do a quick scan for errors and halt execution if necessary:
        # Was a video loaded before 'Contour' was pressed?
        if self.test.nameCheck(self) == 'error':
            return
        elif self.test.fileCheck(self) == 'error':
            return
        # Has the time been typed incorrectly?
        elif self.test.timeCheck(self, tsec) == 'error':
            return
        # Is the given length of measurement an integer?
        elif self.test.measLen(self) == 'error':
            return

        # Number of mice we are using.
        self.numberOfMice = len(mice.split(','))
        startTimehhmmss = [int(x) for x in tsec.split(':')]
        # Time (in seconds) that we begin the measurement
        startTimeSec = (3600 * startTimehhmmss[0]) + (
            60 * startTimehhmmss[1]) + startTimehhmmss[2]
        self.startTimemsec = startTimeSec * 1000  # start-time in miliseconds
        lenOfMeasmsec = int(self.lenOfMeas) * 1000
        self.endTimemsec = self.startTimemsec + lenOfMeasmsec  # ending time
        endTime = self.endTimemsec
        self.capture.set(0, self.endTimemsec)
        self.lastframe = self.capture.get(1)
        self.capture.set(0, self.startTimemsec)
        self.firstframe = self.capture.get(1)
        self.length = int(self.lenOfMeas)
        # Reset slide to show length of analysis time.
        self.slide.setMinimum(startTimeSec)
        self.slide.setMaximum(self.length + startTimeSec)
        self.slide.setSliderPosition(startTimeSec)
        self._timer.start()

        incontours = []
        mouseNumList = []
        respRates = []
        bRespRates = []
        minstdevs = []

        try:
            # Get a unique mouse ID for each mouse.
            for numba in range(0, self.numberOfMice):
                vid = mv.frameReader(self.filename)
                img = vid.getFrame(self.firstframe)
                conty = mv.contour(img)

                mouseNum, ok = QtWidgets.QInputDialog.getText(
                    self, 'Mouse ID', 'Please enter the Mouse #')
                if ok and mouseNum:
                    print('Mouse ID: ', mouseNum)
                    mouseNumList.append(mouseNum)
                    inconty = mv.insideContour(conty, img)
                    incontours.append(inconty)
                else:
                    # If the user presses Cancel, close the contour window
                    cv2.destroyWindow('Click the outline of your ROI:')
                    return

            self.cont = 2
            # Parameters for Shi-Tomasi corner detection
            feature_params = dict(maxCorners=10,
                                  qualityLevel=0.3,
                                  minDistance=7,
                                  blockSize=7)
            # Parameters for Lucas-Kanade optical flow
            lk_params = dict(winSize=(15, 15),
                             maxLevel=2,
                             criteria=(cv2.TERM_CRITERIA_EPS
                                       | cv2.TERM_CRITERIA_COUNT, 10, 0.03))

            # Create some random colors
            color = np.random.randint(0, 255, (100, 3))
            self.capture.set(1, self.firstframe)

            # Take first frame and find corners in it
            _, old_frame = self.capture.read()
            old_gray = cv2.cvtColor(old_frame, cv2.COLOR_BGR2GRAY)
            p0s = mf.ListOfLists(self.numberOfMice)
            for numba in range(0, self.numberOfMice):
                p0s[numba] = cv2.goodFeaturesToTrack(old_gray,
                                                     mask=incontours[numba],
                                                     **feature_params)

            # Create a mask image for drawing purposes
            mask = np.zeros_like(old_frame)
            xPoints = mf.ListOfLists(self.numberOfMice)
            yPoints = mf.ListOfLists(self.numberOfMice)
            p1s = mf.ListOfLists(self.numberOfMice)
            sts = mf.ListOfLists(self.numberOfMice)
            errs = mf.ListOfLists(self.numberOfMice)
            good_news = mf.ListOfLists(self.numberOfMice)
            good_olds = mf.ListOfLists(self.numberOfMice)
            dists = mf.ListOfLists(self.numberOfMice)
            pointPos = mf.ListOfLists(self.numberOfMice)
            peakPos = mf.ListOfLists(self.numberOfMice)

            for numba in range(0, self.numberOfMice):
                for i in range(len(p0s[numba])):
                    xPoints[numba].append([])
                    yPoints[numba].append([])

            l = 0

            for num in range(np.int(self.firstframe),
                             np.int(self.lastframe + 1)):
                _, frame = self.capture.read()
                frame_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
                stzeros = []
                for numba in range(0, self.numberOfMice):
                    stzeros.append(np.array([]))
                for numba in range(0, self.numberOfMice):
                    # Calculate optical flow
                    p1s[numba], sts[numba], errs[
                        numba] = cv2.calcOpticalFlowPyrLK(
                            old_gray, frame_gray, p0s[numba], None,
                            **lk_params)
                    # Select good points
                    if np.any(sts[numba]) == 0:
                        break
                    good_news[numba] = p1s[numba][sts[numba] == 1]
                    good_olds[numba] = p0s[numba][sts[numba] == 1]
                    if len(p0s[numba]) != len(good_olds[numba]):
                        stzeros[numba] = np.where(sts[numba] == 0)[0]
                        for j in stzeros[numba][::-1]:
                            for i in range(len(p0s[numba])):
                                if i == j:
                                    del xPoints[numba][i]
                                    del yPoints[numba][i]

                    # Draw the tracks
                    for i, (new, old) in enumerate(
                            list(zip(good_news[numba], good_olds[numba]))):
                        a, b = new.ravel()
                        c, d = old.ravel()
                        xPoints[numba][i].append([])
                        yPoints[numba][i].append([])
                        xPoints[numba][i][l].append(c)
                        yPoints[numba][i][l].append(d)
                        mask = cv2.line(mask, (a, b), (c, d),
                                        color[i].tolist(), 2)
                        frame = cv2.circle(frame, (a, b), 5, color[i].tolist(),
                                           -1)
                    p0s[numba] = good_news[numba].reshape(
                        -1, 1, 2)  # Update the previous points
                if np.any(sts[numba]) == 0:
                    endTime = self.capture.get(0)
                    break

                # Show the respiratory motion in a new window
                img = cv2.add(frame, mask)
                cv2.imshow('flow', img)
                k = cv2.waitKey(30) & 0xff
                if k == 27:
                    endTime = self.capture.get(0)
                    break

                # Now update the previous frame
                old_gray = frame_gray.copy()
                l = l + 1

            for numba in range(0, self.numberOfMice):
                pointx = mf.ListOfLists(len(p0s[numba]))
                pointy = mf.ListOfLists(len(p0s[numba]))
                peaksx = mf.ListOfLists(len(p0s[numba]))
                peaksy = mf.ListOfLists(len(p0s[numba]))
                distBTpeaksxt = mf.ListOfLists(len(p0s[numba]))
                distBTpeaksxb = mf.ListOfLists(len(p0s[numba]))
                distBTpeaksyt = mf.ListOfLists(len(p0s[numba]))
                distBTpeaksyb = mf.ListOfLists(len(p0s[numba]))
                avgs = mf.ListOfLists(len(p0s[numba]))
                stdevs = mf.ListOfLists(len(p0s[numba]))

                for i in range(len(p0s[numba])):
                    pointx[i] = xPoints[numba][i]
                    peaksx[i] = pd.peakdetect(pointx[i], None, 4,
                                              0)  # 3 for mice, 20 for human
                    pointy[i] = yPoints[numba][i]
                    peaksy[i] = pd.peakdetect(pointy[i], None, 4, 0)

                for ii in range(len(p0s[numba])):
                    for i in range(len(peaksx[ii][0]) - 1):
                        distBTpeaksxt[ii].append(peaksx[ii][0][i + 1][0] -
                                                 peaksx[ii][0][i][0])
                    for i in range(len(peaksx[ii][1]) - 1):
                        distBTpeaksxb[ii].append(peaksx[ii][1][i + 1][0] -
                                                 peaksx[ii][1][i][0])
                    for i in range(len(peaksy[ii][0]) - 1):
                        distBTpeaksyt[ii].append(peaksy[ii][0][i + 1][0] -
                                                 peaksy[ii][0][i][0])
                    for i in range(len(peaksy[ii][1]) - 1):
                        distBTpeaksyb[ii].append(peaksy[ii][1][i + 1][0] -
                                                 peaksy[ii][1][i][0])

                for i in range(len(p0s[numba])):
                    avgs[i].append(
                        sum(distBTpeaksxt[i]) / len(distBTpeaksxt[i]))  # xtop
                    stdevs[i].append(np.std(distBTpeaksxt[i]) /
                                     avgs[i][0])  # xtop
                    avgs[i].append(
                        sum(distBTpeaksxb[i]) /
                        len(distBTpeaksxb[i]))  # xbottom
                    stdevs[i].append(np.std(distBTpeaksxb[i]) /
                                     avgs[i][1])  # xbottom
                    avgs[i].append(
                        sum(distBTpeaksyt[i]) / len(distBTpeaksyt[i]))  # ytop
                    stdevs[i].append(np.std(distBTpeaksyt[i]) /
                                     avgs[i][2])  # ytop
                    avgs[i].append(
                        sum(distBTpeaksyb[i]) /
                        len(distBTpeaksyb[i]))  # ybottom
                    stdevs[i].append(np.std(distBTpeaksyb[i]) /
                                     avgs[i][3])  # ybottom

                merged = list(itertools.chain.from_iterable(stdevs))
                minst = min(merged)
                matches = [match for match in mf.find(stdevs, minst)]
                bestPoints = list(zip(*matches))[0]
                distInd = list(zip(*matches))[1]
                best = mf.ListOfLists(len(p0s[numba]))
                beststdev = mf.ListOfLists(len(p0s[numba]))
                for ii in range(len(p0s[numba])):
                    minimummy = min(stdevs[ii])
                    mins = [
                        i for i, j in enumerate(stdevs[ii]) if j == minimummy
                    ]
                    for i in range(len(mins)):
                        best[ii] = avgs[ii][mins[i]]
                        beststdev[ii] = stdevs[ii][mins[i]]

                minstdev = min(beststdev)
                indMinStdev = [
                    i for i, j in enumerate(beststdev) if j == minstdev
                ]
                bavg = [best[i] for i in indMinStdev]
                avgOfbest = sum(best) / len(best)
                avgOfbavg = sum(bavg) / len(bavg)

                font = {
                    'family': 'sans-serif',
                    'color': 'black',
                    'weight': 'normal',
                    'size': 12
                }

                # Plotting
                print('distInd', distInd)
                xaxis = range(0, len(pointx[numba]))
                xaxisYtop = list(zip(*peaksy[numba][0]))[0]
                xaxisYbottom = list(zip(*peaksy[numba][1]))[0]
                xaxisT = [x / 30 for x in xaxis]
                xaxisYtopT = [x / 30 for x in xaxisYtop]
                xaxisYbottomT = [x / 30 for x in xaxisYbottom]

                # Close open figures with same name to prevent bad formatting
                if plt.fignum_exists('Mouse %s' % mouseNumList[numba]):
                    plt.close('Mouse %s' % mouseNumList[numba])

                plt.ion()  # cosmetic, QCoreApplication error without this
                plt.figure('Mouse %s' % mouseNumList[numba])  # Window name
                plt.plot(xaxisT, pointy[numba], color='#3399FF')  # Curve
                plt.plot(xaxisYtopT,
                         list(zip(*peaksy[numba][0]))[1],
                         'o',
                         color='red',
                         markersize=4)  # Peak
                plt.plot(xaxisYbottomT,
                         list(zip(*peaksy[numba][1]))[1],
                         'o',
                         color='#FF9966',
                         markersize=4)  # Valley
                plt.title('Mouse ' + str(mouseNumList[numba]), fontdict=font)

                pointPos[numba].append(pointy[numba])
                peakPos[numba].append(peaksy[numba])
                plt.xlabel('Time (s)', fontdict=font)
                frame1 = plt.gca()
                frame1.axes.get_yaxis().set_ticks([])
                plt.tight_layout()  # Don't cut off the title and x-label
                plt.show()

                respRate = (60 * 30) / avgOfbest
                bRespRate = (60 * 30) / avgOfbavg

                # Output respiratory rate and stdev to console; don't show if
                # running standalone
                print('\nBest: ' + str([round(elem, 3) for elem in best]))
                print('Best Stdev: ' +
                      str([round(elem, 4) for elem in beststdev]))
                print('Best Respiratory Rate: ', bRespRate, '\n')

                respRates.append(respRate)
                bRespRates.append(bRespRate)
                minstdevs.append(minstdev)
                for i, (bp, ind) in enumerate(list(zip(bestPoints, distInd))):
                    if ind == 0:
                        dists[numba].append(distBTpeaksxt[bp])
                    if ind == 1:
                        dists[numba].append(distBTpeaksxb[bp])
                    if ind == 2:
                        dists[numba].append(distBTpeaksyt[bp])
                    if ind == 3:
                        dists[numba].append(distBTpeaksyb[bp])

            # Show results in main window
            for numba in range(0, self.numberOfMice):
                toPrint1 = mouseNumList[numba]
                toPrint2 = round(bRespRates[numba], 2)
                toPrint3 = round(minstdevs[numba], 4)
                out = ' {:<18}  {:^21.2f}  {:>21.4f} '.format(
                    toPrint1, toPrint2, toPrint3)
                self.ui.textBrowser_Output.append(out)

            # Measurement terminated before full run time
            if endTime != self.endTimemsec:
                errorNotif(
                    self,
                    '<br>Measurement did not run for entire set length.</br>')

            # Ask if we want to export data to a spreadsheet
            export = askQuestion(self, 'RespiRate',
                                 '<br>Export data to spreadsheet?</br>')
            if export == 'yes':
                for numba in range(0, self.numberOfMice):
                    videoName = path.splitext(path.basename(self.filename))[0]
                    toPrintList = [
                        str(videoName),
                        str(mouseNumList[numba]),
                        str(startTimeSec),
                        str(endTime / 1000),
                        str((endTime / 1000) - (startTimeSec)),
                        str(round(bRespRates[numba], 2)),
                        str(round(minstdevs[numba], 4))
                    ]
                    workBook = 'output1.xls'
                    sheetName = 'Sheet1'
                    mf.xOutput(self, toPrintList, workBook, sheetName)

        # If the measurement field on the mouse is out of focus or the mouse
        # moves too much and all trackers are dropped, we get various errors.
        # See TODO for more info
        except (TypeError, ZeroDivisionError, IndexError) as excpt:
            print('type is: ', excpt.__class__.__name__)
            print_exc()
            msg = ('<br>The selected region on ' + str(mouseNumList[numba]) +
                   ' is not suitable for respiration measurements.</br>')
            errorNotif(self, msg)

        # Close the separate video windows.
        cv2.destroyAllWindows()
def xOutput(self, toPrintList, workBook, sheetName):
    '''Export data to spreadsheet for easy review and analysis.'''
    if workBook == 0 and sheetName == 0:
        notifiCat.errorNotif('<br>Data was not saved to spreadsheet!</br>')
    else:
        try:
            # Check if the target file already exists.
            dir_path = path.join(path.expanduser('~'), 'RespiRate')
            file_path = path.join(dir_path, workBook)
            if path.isfile(file_path) == False:
                q = (
                    '<p>A suitable spreadsheet was not found.'
                    '<br>Would you like to generate one automatically?</br></p>'
                )
                new = notifiCat.askQuestion(self, 'No spreadsheet.', q)
                if new == 'yes':
                    # Test if the folder exists. It might even if the file does not.
                    if not path.exists(dir_path):
                        mkdir(dir_path)
                    # Set up the spreadsheet
                    book = xlwt.Workbook()
                    sheet1 = book.add_sheet('Sheet1')
                    sheet1.write(0, 0, 'Video #')
                    sheet1.write(0, 1, 'Mouse #')
                    sheet1.write(0, 2, 'Start Time')
                    sheet1.write(0, 3, 'End Time')
                    sheet1.write(0, 4, 'Total Time')
                    sheet1.write(0, 5, 'Best RR')
                    sheet1.write(0, 6, 'stdev')
                    book.save(file_path)
                    created_msg = (
                        'Spreadsheet was created as `output1.xls` in'
                        ' the RespiRate folder.')
                    notifiCat.infoNotif(self, 'Success!', created_msg)
                else:
                    return
            # The file exists (or was just created) - now write output.
            wb = xlrd.open_workbook(file_path)  #output1.xls
            sheet = wb.sheet_by_name(sheetName)  #Sheet1
            rb = copy(wb)
            ws = rb.get_sheet(0)
            row = 0
            col = 0
            cell = sheet.cell(row, 0)
            try:
                while cell.ctype != 6:
                    row = row + 1
                    cell = sheet.cell(row, 0)
            except IndexError:
                print('index error')  #TODO: Why do we get this for every run?
            for i in range(0, len(toPrintList)):
                ws.write(row, col, toPrintList[i])
                col = col + 1
            rb.save(file_path)
        except IOError:
            # If we get this far, the spreadsheet cannot be opened (most likely
            # it is already opened in Excel or another program). Check it, close
            # it, and rerun it. Working now?
            notifiCat.errorNotif(
                self, 'Data cannot be exported!\n'
                '<br>Please check if the spreadsheet is already opened.</br>')
Exemple #9
0
    def errorCheck(self, tsec):
        '''Check for common mistakes and return a notification.'''
        msg_video = '<br>You have not selected a video!</br>'
        msg_time = '<br>Time must be in hh:mm:ss format</br>.'
        msg_len = '<br>The given length of measurement cannot be understood.</br>'
        tsec = str(tsec)
        time_check = tsec.split(':')
        len_measure = str(self.lenOFMeas)

        # A video hasn't been selected. Note that this doesn't detect if the
        # selected file is actually a video, but only if a file (any type) has
        # been chosen.
        if self.filename == 0 or self.filename == u'':
            errorNotif(self, msg_video)
            return 'error'

        # Check if measurement length makes sense
        elif len_measure.isdigit() == False:
            errorNotif(self, msg_len)
            return 'error'

        # Check the following:
        # - Start time is in correct format (hh:mm:ss)
        # - Second and minute input don't go above 59
        # - Start time input is only integers (no letters or special characters)
        elif len(tsec) != 8:
            errorNotif(self, msg_time)
            return 'error'
        elif tsec[2] != ':' or tsec[5] != ':':
            errorNotif(self, msg_time)
            return 'error'
        elif 1 == 1: # Bit of a ugly hack
            for num in time_check:
                if num.isdigit() == False:
                    errorNotif(self, msg_time)
                    return 'error'
        elif int(time_check[-1]) > 59 or int(time_check[-2]) > 59:
            errorNotif(self, msg_time)
            return 'error'