class Controller(): def __init__(self, aConfigFilePath, aDebugFlag=False): # Config parameters default values # Timelapse interval hours, minutes and seconds used to calculate the # interval in seconds self.timelapseIntervalH = 1.0 self.timelapseIntervalM = 0.0 self.timelapseIntervalS = 0.0 # Number of images to take when motion is detected by the PIR self.motionSequenceCount = 5 # Number of images that are used by a motion detection algorithm before # up-to-date results are indicated by the produced difference image self.motionSequencePreload = 2 # Delay between each image taken when triggered by the PIR self.motionSequenceDelay = 2.0 # Percentage difference image area that needs to be covered by a # minimum number of blobs in order to evaluate that motion is happening self.motionAreaPercent = 60 self.motionMaxBlobs = 10 # Where timelapse images are saved self.timelapseFilePath = "./timelapse/" # Where motion images/diffs are saved self.motionFilePath = "./motion/" # Where temporary files are put, for example to get the current # brightness in the scene self.tempFilePath = "./temp/" # Path where hourly and motion logs are placed self.logFilePath = "./logs/" # TM: path to the queue folder self.queuePath = "./queue/" # The name if the most recently captured timelaps image self.timelapseLatestImage = "timelapse.jpg" #IR: Folder to save the true sequences self.miniSequencePath = "./miniSequence/" # Threshold (0-255) for the scene brightness when the IR led is # switched on self.brightnessThreshold = 25 # Timeout (seconds) for when the brightness in the scene needs to be # re-evaluated self.brightnessTimeout = 60.0 # The resolution of the camera self.resolutionX = 640 self.resolutionY = 480 # Flag to produce debug output to the terminal while running self.debug = aDebugFlag self.FOVMaxDistance = 20 self.FOVMinAreaDetected = 1 self.FOVVertical = 54 self.FOVHorizontal = 41 # IR: Defines downsampling level self.downLevel = 0 # TM: ROI self.ROI = None #TM: enable/disable SMS feature self.sendSMS = False # TM: SMS recipient self.textMessageRecipient = None # TM: queue implementation self.processingImage = False self.imgSetsInQ = 0 self.queueIndex = 0 self.currentImageSetToProcess = 0 self.numberOfImgSetsProcessed = 0 self.lastBackgroundRefrestTime = 0 self.backgroundRefreshWaitTime = 60.0 #60.0 * 5 self.previousMotionImgTime = 0 self.timeToWaitForNextMotionImg = 10.0 self._savingImageSequence = False self.sequenceTimeStampList = list() self.imageBrightnessList = list() self.PIRLastCountList = list() # Load config file parameters to override the default values self.parser = SafeConfigParser() self.setConfig(aConfigFilePath) # Paths setup, create the paths if they do not exist if not self._directoryExists(self.tempFilePath): self._createDirectory(self.tempFilePath) if not self._directoryExists(self.motionFilePath): self._createDirectory(self.motionFilePath) if not self._directoryExists(self.logFilePath): self._createDirectory(self.logFilePath) if not self._directoryExists(self.timelapseFilePath): self._createDirectory(self.timelapseFilePath) # TM; queue folder if not self._directoryExists(self.queuePath): self._createDirectory(self.queuePath) # IR: true sequence folder if not self._directoryExists(self.miniSequencePath): self._createDirectory(self.miniSequencePath) # Timelapse setup self.timelapseInterval = (self.timelapseIntervalH * 60.0 * 60.0 + self.timelapseIntervalM * 60.0 + self.timelapseIntervalS) self.timelapseLastTime = 0.0 # Brightness measurements setup self.brightnessLastTime = 0.0 self.brightness = 0 # Inputs/outputs sensor.initGPIO() self.PIR = sensor.PIR(sensor.PIR_PIN, sensor.GPIO.PUD_DOWN) self.PIRLastCount = 0 self.DHTType = Adafruit_DHT.DHT22 self.DHTPin = 23 #TM: Queue implementation self.PIR.enableInterrupt(self._takeImageCallbackPIR) self.PIRTriggered = False self.enableCallbackPIR = False # Camera setup self.resolution = (self.resolutionX, self.resolutionY) self.cam = picamera.PiCamera() self.cam.resolution = self.resolution # TODO(geir): Create a function to switch off camera LED in sensor.py sensor.GPIO.output(sensor.CAM_LED, False) # Difference image area that is considered too small to evaluate the # movement in the scene as true self.motionMinArea = \ motion.getCutoffObjectArea(self.resolutionX * self.resolutionY, self.FOVMinAreaDetected, self.FOVVertical, self.FOVHorizontal, self.FOVMaxDistance) # IR: downsampling self.originalResolutionX = self.resolutionX self.originalResolutionY = self.resolutionY self.defaultMotionMinArea = self.motionMinArea # TODO(geir): Consider wrapping the below in a helper function to # reduce init clutter # Data logging setup hourlyDataDescription = [ "AliveCount", "Timestamp", "Brightness", "ExternalTemp", "Humidity" ] hourlyDataFilePath = os.path.join(self.logFilePath, "hourlydata.csv") self.hourlyDataRecorder = DataWriter(hourlyDataFilePath, hourlyDataDescription) self.hourlyLastTime = 0.0 self.hourlyAliveCount = 1 # TMorton # Added processing time data self.processingEndTime = 0 self.processingStartTime = 0 motionDataDescription = [ "AliveCount", "Timestamp", "Brightness", "Threshold", # Do we need this? "BlobCount", "MaxBlobSize", "Verdict", "ProcessingTime" ] motionDataFilePath = os.path.join(self.logFilePath, "motiondata.csv") self.motionDataRecorder = DataWriter(motionDataFilePath, motionDataDescription) self.motionAliveCount = 1 # Image processor(s) self.processor = ThreeBlur() self.strProcessor = "ThreeBlur" #self.processor = backSub() #self.strProcessor = "backSub" self.processor.setConfig(aConfigFilePath) # do this if backSub self._doBackgroundRefresh() #Overwrites the ROI image from the processor object #if the ROI is downsampled (if the ROI is used) self.downSampleROI('ROI.jpg', self.downLevel) #Send starting SMS if self.textMessageRecipient is not None: self.sms = TextMessage() self.trueMotionCount = 0 self.lastTextMessageSendTime = time.time() self.sms.connectPhone() self.sms.setRecipient(str(self.textMessageRecipient)) self.sms.setContent("FoxBox On") self.sms.sendMessage() self.sms.disconnectPhone() def run(self): self.enableCallbackPIR = True while True: if self.textMessageRecipient is not None: self._sendTextMessage() if self._timeForTimelapse(): self.enableCallbackPIR = False while self._savingImageSequence == True: pass self._doTimelapse() self.enableCallbackPIR = True if self._timeForHourlyData(): self.enableCallbackPIR = False while self._savingImageSequence == True: pass self._doHourlyData() self.enableCallbackPIR = True if self.PIRTriggered == False: self.enableCallbackPIR = False while self._savingImageSequence == True: pass self._doBackgroundRefresh() self.enableCallbackPIR = True self._processImagesFromQ() time.sleep(1) def _timeForTimelapse(self): currentTime = time.time() if (currentTime - self.timelapseLastTime) > self.timelapseInterval: self.timelapseLastTime = currentTime return True else: return False def _doTimelapse(self): if self.debug: print("Performing timelapse") fileName = time.strftime("%Y%m%d-%H%M%S.jpg") savePath = os.path.join(self.timelapseFilePath, fileName) # tempFilePath = os.path.join(self.tempFilePath, # self.timelapseLatestImage) if self._getCurrentBrightness() < self.brightnessThreshold: sensor.activateIRLED() self.cam.capture(savePath) # TODO(geir): Evaluate if we should apply a visual indication of the # current ROI in the image before saving it? # Ensure LED is off sensor.deactivateIRLED() # T Morton def _doBackgroundRefresh(self): if self.strProcessor == "backSub": tempFilePath = os.path.join(self.tempFilePath, self.timelapseLatestImage) backgroundCurrentTime = time.time() if (backgroundCurrentTime - self.lastBackgroundRefrestTime ) > self.backgroundRefreshWaitTime: self.lastBackgroundRefrestTime = backgroundCurrentTime if self.debug: print("Refreshing background") if self._getCurrentBrightness() < self.brightnessThreshold: sensor.activateIRLED() self.cam.capture(tempFilePath) # IR: Downsample background image self.backgroundDownSample(self.downLevel) sensor.deactivateIRLED() # IR: Reset variables to defaults self.resolutionX = self.originalResolutionX self.resolutionY = self.originalResolutionY self.motionMinArea = self.defaultMotionMinArea def _timeForHourlyData(self): currentTime = time.time() if (currentTime - self.hourlyLastTime) > 3600.0: self.hourlyLastTime = currentTime return True else: return False def _doHourlyData(self): if self.debug: print("Performing hourly data") timestamp = time.strftime("%Y%m%d-%H%M%S") brightness = self._getCurrentBrightness() DHTData = Adafruit_DHT.read_retry(self.DHTType, self.DHTPin) #DHTData = Adafruit_DHT.read(self.DHTType, self.DHTPin) if DHTData[0] is None: print "Humidity is None" humidity = 0 else: humidity = round(DHTData[0], 2) if DHTData[1] is None: print "temperature is None" temperature = 0 else: temperature = round(DHTData[1], 2) dataList = [ self.hourlyAliveCount, timestamp, brightness, temperature, humidity ] self.hourlyDataRecorder.writeData(dataList) self.hourlyAliveCount += 1 if self.debug: print("Got hourly data: ") print(dataList) # T Morton: SMS def _sendTextMessage(self): currentTime = time.time() if (currentTime - self.lastTextMessageSendTime) > (60 * 30): self.lastTextMessageSendTime = currentTime self.sms.connectPhone() self.sms.setRecipient(str(self.textMessageRecipient)) self.sms.setContent( "FoxBox Hourly Motion Report. Number of True Detections: " + str(self.trueMotionCount)) self.sms.sendMessage() self.sms.disconnectPhone() print "message sent successfully" self.trueMotionCount = 0 def _takeImageCallbackPIR(self, channel): #print("Interrupt Triggered") self.PIRTriggered = True if self.enableCallbackPIR == True: currentTime = time.time() if (currentTime - self.previousMotionImgTime ) > self.timeToWaitForNextMotionImg: self.previousMotionImgTime = currentTime print("Saving image sequence to queue") self._saveImageSequenceToQ(self.motionSequenceCount, self.motionSequenceDelay) self.PIRTriggered = False def _getQueueFilename(self, aQueueIndex): if aQueueIndex < 10: rQueueIndex = '000' + str(aQueueIndex) elif aQueueIndex > 9 & aQueueIndex < 100: rQueueIndex = '00' + str(aQueueIndex) elif aQueueIndex > 99 & aQueueIndex < 1000: rQueueIndex = '0' + strre(aQueueIndex) return rQueueIndex def _saveImageSequenceToQ(self, aCount, aDeltaTime): #TM: Boolean indicates that images are being saved (used by run loop) self._savingImageSequence = True if self._getCurrentBrightness() < self.brightnessThreshold: sensor.activateIRLED() queueFilename = self._getQueueFilename(self.queueIndex) captureCount = 0 sequenceTimeStamp = time.strftime("%Y%m%d-%H%M%S") while (captureCount < aCount): imgPath = self.queuePath + queueFilename + \ "_" + str(captureCount) + ".jpg" pictureCurrentTime = time.time() self.cam.capture(imgPath) afterPictureCurrentTime = time.time() PictureDeltaTime = afterPictureCurrentTime - pictureCurrentTime #print PictureDeltaTime if (PictureDeltaTime < aDeltaTime): time.sleep(aDeltaTime - PictureDeltaTime) captureCount += 1 #TM: records time image taken self.sequenceTimeStampList.append(sequenceTimeStamp) #TM: records brightness at time image taken imageBrightness = self._getCurrentBrightness() self.imageBrightnessList.append(imageBrightness) # Increments the index used for filename convention ##CircularQ if self.queueIndex < 999: self.queueIndex += 1 else: self.queueIndex = 0 # Increments the actual number of image sets in the queue self.imgSetsInQ += 1 sensor.deactivateIRLED() self._savingImageSequence = False def _loadImageSequenceFromQ(self, aImgSequence): imgList = list() #IR: Saving the full size images fullSizeImgList = list() queueFilename = self._getQueueFilename(aImgSequence) sequence = queueFilename + '_?' for filename in sorted(glob.glob(self.queuePath + sequence + "*.jpg")): PathAndFile = os.path.splitext(filename)[0] latestFilename = ntpath.basename(PathAndFile) # IR: Downsample images and append to list self.resolutionX = self.originalResolutionX self.resolutionY = self.originalResolutionY img = cv2.imread(self.queuePath + latestFilename + ".jpg", cv2.CV_LOAD_IMAGE_COLOR) #IR: Saving the full size images fullSizeImgList.append(img) if (self.downLevel > 0): img = self.imageDownsampling(img, self.downLevel) imgList.append(img) #IR: Return both the full size and downsampled lists return imgList, fullSizeImgList def _processImagesFromQ(self): if self.imgSetsInQ > 0: self.processingImage = True self._doMotionDetectionFromQ(self.currentImageSetToProcess) self.processingImage = False self._deleteProcessedImgsFromQ(self.currentImageSetToProcess) ##CircularQ if self.currentImageSetToProcess < 999: self.currentImageSetToProcess += 1 else: self.currentImageSetToProcess = 0 ## elif os.listdir(self.queuePath) == []: self.queueIndex = 0 self.currentImageSetToProcess = 0 self.sequenceTimeStampList = list() self.imageBrightnessList = list() self.PIRLastCountList = list() def _deleteProcessedImgsFromQ(self, aImgSequence): queueFilename = self._getQueueFilename(aImgSequence) filenameSearch = queueFilename + '_?' for filename in glob.glob(self.queuePath + filenameSearch + ".jpg"): # Calling os.system breaks the parallel thread. #cmd = 'sudo rm ' + filename # os.system(cmd) os.remove(filename) self.imgSetsInQ -= 1 def _doMotionDetectionFromQ(self, aImageSet): if self.debug: print("Performing motion detection") self.processingStartTime = time.time() #IR: self.changeDownsamplingLevel() imgList, fullSizeImgList = self._loadImageSequenceFromQ(aImageSet) diffList = self._getDiffSequence(imgList) if self.motionSequencePreload <= 0: startIndex = 0 else: startIndex = self.motionSequencePreload - 1 largestAreaIndex = None largestAreaBlobs = None largestAreaBlobCount = None largestArea = -1.0 for diffIndex in xrange(startIndex, self.motionSequenceCount): blobData = motion.getBlobs(diffList[diffIndex]) blobs = blobData[0] num_blobs = blobData[1] blobAreaList = motion.getBlobAreaList(blobs, num_blobs) largestBlobArea = motion.getMaxAreaFromBlobs(blobAreaList) if largestBlobArea > largestArea: largestArea = largestBlobArea largestAreaIndex = diffIndex largestAreaBlobs = blobs largestAreaBlobCount = num_blobs self.processingEndTime = time.time() aliveCount = self.motionAliveCount #TM : get timestamp from list for when image was taken timestamp = self.sequenceTimeStampList[aImageSet] #timestamp = time.strftime("%Y%m%d-%H%M%S") #TM : get brightness from list for when image was taken brightness = self.imageBrightnessList[aImageSet] #brightness = self._getCurrentBrightness() threshold = 0 # TODO(geir): Need it? blobCount = largestAreaBlobCount maxBlobSize = largestBlobArea verdict = motion.evalMotionPropotionalBlobArea(largestAreaBlobs, largestAreaBlobCount, self.motionAreaPercent, self.motionMaxBlobs, self.motionMinArea) # TMorton: SMS #if self.sendSMS == True: if self.textMessageRecipient is not None: if verdict == True: self.trueMotionCount += 1 # TM: records the time it takes to process an image processingTime = round(self.processingEndTime - self.processingStartTime) # Save statistics in CSV dataList = [ aliveCount, timestamp, brightness, threshold, blobCount, maxBlobSize, verdict, processingTime ] self.motionDataRecorder.writeData(dataList) imageName = timestamp + ".jpg" diffName = timestamp + "-" + str(verdict) + "-diff.jpg" imagePath = os.path.join(self.motionFilePath, imageName) diffPath = os.path.join(self.motionFilePath, diffName) cv2.imwrite(imagePath, imgList[largestAreaIndex]) cv2.imwrite(diffPath, diffList[largestAreaIndex]) #IR: Save true image sequence to miniSequence Folder # at the original size of the images if verdict == True: for listIndex in range(len(fullSizeImgList)): imgName = timestamp + "-" + str(listIndex) + ".jpg" imgFullPath = os.path.join(self.miniSequencePath, imgName) cv2.imwrite(imgFullPath, fullSizeImgList[listIndex]) self.motionAliveCount += 1 if self.debug: print("Got motion data: ") print(dataList) # TM: Keeps track of the number of image sets that have been processed self.numberOfImgSetsProcessed += 1 # IR: Reset variables to defaults self.resolutionX = self.originalResolutionX self.resolutionY = self.originalResolutionY self.motionMinArea = self.defaultMotionMinArea # IR: downsampling function def imageDownsampling(self, aImage, aDownLevel): # aImage= the input array image # downLevel = how many levels of sampling # perform the downsampling tempImg = aImage for n in range(aDownLevel): imgDownsampled = cv2.pyrDown(tempImg) #print 'downsampling' tempImg = imgDownsampled # change the global resolution parameters of the image self.resolutionX = self.resolutionX / (2**aDownLevel) self.resolutionY = self.resolutionY / (2**aDownLevel) # recalculate the motion min area with the new resolution self.motionMinArea = \ motion.getCutoffObjectArea(self.resolutionX * self.resolutionY, self.FOVMinAreaDetected, self.FOVVertical, self.FOVHorizontal, self.FOVMaxDistance) return tempImg def _getCurrentBrightness(self): currentTime = time.time() if (currentTime - self.brightnessLastTime) > self.brightnessTimeout: self.brightnessLastTime = currentTime imagePath = os.path.join(self.tempFilePath, "brightness.jpg") self.cam.capture(imagePath) image = cv2.imread(imagePath, 0) brightness = motion.getBrightness(image) self.brightness = round(brightness[0], 2) #if self.debug: # print("Got brightness: " + str(self.brightness)) return self.brightness def _getDiffSequence(self, aImgList): diffList = list() for image in aImgList: diff = self.processor.getDiff(image) diffList.append(diff) return diffList def _directoryExists(self, aDirectory): return os.path.exists(aDirectory) def _createDirectory(self, aDirectory): os.makedirs(aDirectory) def setConfig(self, aConfig): if self.parser.read(aConfig): # General variables self.timelapseFilePath = self.setParam(self.timelapseFilePath, 'str', 'General', 'timelapseFilePath') self.motionFilePath = self.setParam(self.motionFilePath, 'str', 'General', 'motionFilePath') self.tempFilePath = self.setParam(self.tempFilePath, 'str', 'General', 'tempFilePath') self.logFilePath = self.setParam(self.logFilePath, 'str', 'General', 'logFilePath') self.timelapseLatestImage = self.setParam(self.logFilePath, 'str', 'General', 'timelapseLatestImage') # TM: loads queue path self.queuePath = self.setParam(self.queuePath, 'str', 'General', 'queuePath') # IR: loads miniSequence path (where true images are saved) self.miniSequencePath = self.setParam(self.miniSequencePath, 'str', 'General', 'miniSequencePath') # Controller variables self.timelapseIntervalH = self.setParam(self.timelapseIntervalH, 'float', 'Controller', 'timelapseIntervalH') self.timelapseIntervalM = self.setParam(self.timelapseIntervalM, 'float', 'Controller', 'timelapseIntervalM') self.timelapseIntervalS = self.setParam(self.timelapseIntervalS, 'float', 'Controller', 'timelapseIntervalS') self.motionSequenceCount = self.setParam(self.motionSequenceCount, 'int', 'Controller', 'motionSequenceCount') self.motionSequencePreload = \ self.setParam(self.motionSequencePreload, 'int', 'Controller', 'motionSequencePreload') self.motionSequenceDelay = self.setParam(self.motionSequenceDelay, 'float', 'Controller', 'motionSequenceDelay') self.motionAreaPercent = self.setParam(self.motionAreaPercent, 'int', 'Controller', 'motionAreaPercent') self.motionMaxBlobs = self.setParam(self.motionMaxBlobs, 'int', 'Controller', 'motionMaxBlobs') self.brightnessThreshold = self.setParam(self.brightnessThreshold, 'int', 'Controller', 'brightnessThreshold') self.brightnessTimeout = self.setParam(self.brightnessTimeout, 'float', 'Controller', 'brightnessTimeout') self.resolutionX = self.setParam(self.resolutionX, 'int', 'Controller', 'resolutionX') self.resolutionY = self.setParam(self.resolutionY, 'int', 'Controller', 'resolutionY') self.FOVMaxDistance = self.setParam(self.FOVMaxDistance, 'int', 'Controller', 'FOVMaxDistance') self.FOVMinAreaDetected = self.setParam(self.FOVMinAreaDetected, 'int', 'Controller', 'FOVMinAreaDetected') self.FOVVertical = self.setParam(self.FOVVertical, 'int', 'Controller', 'FOVVertical') self.FOVHorizontal = self.setParam(self.FOVHorizontal, 'int', 'Controller', 'FOVHorizontal') # TM: added for down-sampling self.downLevel = self.setParam(self.downLevel, 'int', 'Controller', 'downLevel') # TM: added for queue self.backgroundRefreshWaitTime = self.setParam( self.backgroundRefreshWaitTime, 'float', 'Controller', 'backgroundRefreshWaitTime') # TM: added for queue self.timeToWaitForNextMotionImg = self.setParam( self.timeToWaitForNextMotionImg, 'float', 'Controller', 'timeToWaitForNextMotionImg') #TM: Added for ROI down-sampling self.ROI = self.setParam(self.ROI, 'str', 'General', 'ROI') #TM: Added for SMS feature #self.sendSMS = self.setParam(self.sendSMS, 'bool', 'General', 'sendSMS') #TM: SMS recipient self.textMessageRecipient = self.setParam( self.textMessageRecipient, 'str', 'General', 'textMessageRecipient') self.debug = self.setParam(self.debug, 'bool', 'Controller', 'debug') def setParam(self, aParam, aParamType, aSection, aOption): param = None if self.parser.has_option(aSection, aOption): if aParamType == 'int': param = self.parser.getint(aSection, aOption) elif aParamType == 'float': param = self.parser.getfloat(aSection, aOption) elif aParamType == 'bool': param = self.parser.getboolean(aSection, aOption) elif aParamType == 'str': param = self.parser.get(aSection, aOption) if param is None: print(aOption + " in " + aSection + " was not found. Using" + " default value.") param = aParam return param # IR: ROI downsampling function def downSampleROI(self, aROIimageName, aDownLevel): #read the ROI image to an array #perform downsampling on the image #send the downsampled ROI to the processor object #this way the original ROI can be downsampled as many times #at different levels if self.ROI is not None: imgROI = cv2.imread(aROIimageName, 0) #TM: Saving a copy of original ROI might be needed in the future for n in range(aDownLevel): downsampledROI = cv2.pyrDown(imgROI) imgROI = downsampledROI print 'ROI downsampling' #remove the blur regions between from the ROI downsampled image retval, imgROI = cv2.threshold(imgROI, 127, 255, cv2.THRESH_BINARY) self.processor.loadROIfromControl(imgROI) def backgroundDownSample(self, aDownLevel): if self.strProcessor == "backSub": backgroundPath = os.path.join(self.tempFilePath, self.timelapseLatestImage) backgroundImg = cv2.imread(backgroundPath, cv2.CV_LOAD_IMAGE_COLOR) for n in range(aDownLevel): downsampledBackground = cv2.pyrDown(backgroundImg) backgroundImg = downsampledBackground print 'backgound downsampling' self.processor.loadBackgroundFromControl(backgroundImg) #IR : two levels of downsampling including ROI def changeDownsamplingLevel(self): #checks the number of image sets in the Q #if the number is above the threshold #increases the downsampling level to reduce the queue length #maximum time to wait in the cue cueThreshold = 5 # assumed max number of images in Q upperCueThreshold = 8 lowerCueThreshold = 1 #if self.downLevel == 0 and self.imgSetsInQ > cueThreshold: if self.downLevel == 0 and self.imgSetsInQ > cueThreshold and self.imgSetsInQ < upperCueThreshold: self.downLevel += 1 self.downSampleROI("ROI.jpg", self.downLevel) self.backgroundDownSample(self.downLevel) elif self.downLevel == 1 and self.imgSetsInQ <= lowerCueThreshold: self.downLevel -= 1 self.downSampleROI("ROI.jpg", self.downLevel) self.backgroundDownSample(self.downLevel) elif self.downLevel < 2 and self.imgSetsInQ >= upperCueThreshold: #self.downLevel +=1 self.downLevel = 2 self.downSampleROI("ROI.jpg", self.downLevel) self.backgroundDownSample(self.downLevel) elif self.downLevel == 2 and self.imgSetsInQ < cueThreshold: self.downLevel -= 1 self.downSampleROI("ROI.jpg", self.downLevel) self.backgroundDownSample(self.downLevel) else: pass