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