class PlayerMoveSignal(QObject): def __init__(self, parent): super(PlayerMoveSignal, self).__init__() self.game = parent self.sig = Signal(str) self.eventLoop = QEventLoop(self) self.data = None @Slot(str) def stop_waiting(self, data): self.data = data self.eventLoop.exit() return def wait_for_move(self): self.eventLoop.exec_() return self.data
class BatchProcessor(): signals = signal.signalClass() GeneralEventLoop = None SnapshotEventLoop = None SnapshotTaken = False is_active = False well_positioner = None Well_Map = None Well_Targets = None ID = str info = str duration = 0 interleave = 0 start_time = 0 end_time = 0 logging = True ## @brief BatchProcessor()::__init__ sets the batch settings ## @param well_controller is the well positioning class instance used to position the wells under the camera. ## @param well_map is the list of target wells with their positions in mm of both row and column. ## @param well_targets is the list of the target wells with their column and row index positions and ID. ## @param ID is the batch ID. ## @param info is the batch information. ## @param dur is the batch duration. ## @param interl is the time between the photographing of each well. def __init__(self, well_controller, well_map, well_targets, ID, info, path, dur, interl): #super().__init__() self.is_active = False self.well_positioner = well_controller self.Well_Map = well_map self.Well_Targets = well_targets self.batch_id = ID self.batch_info = info self.duration = dur self.interleave = interl self.path = os.path.sep.join([path, ID]) if not os.path.exists(self.path): os.makedirs(self.path) ## @brief BatchProcessor()::msg emits the message signal. This emit will be catched by the logging slot function in main.py. ## @param message is the string message to be emitted. @Slot(str) def msg(self, message): if message: self.signals.mes.emit(self.__class__.__name__ + ": " + str(message)) return ## @brief BatchProcessor()::updateBatchSettings(self, well_data, ID, info, dur, interl) can be called to update the batch settings during runtime. ## @todo well_data update in MainWindow. ## @depricated, BatchProcessor::updateBatchSettings not in use yet. Function might be usefull when updating the batch.ini via the GUI. ## @param well_data is the list of target wells with their positions/indexes. ## @param ID is the batch ID. ## @param info is the batch information. ## @param dur is the batch duration. ## @param interl is the time between the photographing of each well. def updateBatchSettings(self, well_map, well_targets, ID, info, dur, interl): self.is_active = False self.Well_Map = well_map self.Well_Targets = well_targets self.batch_id = ID self.batch_info = info self.duration = dur self.interleave = interl return ## @brief BatchProcessor()::startBatch(self) starts the batch process by setting the current (start)time and the endtime. ## It disables the manual motor control and emits the batch_active signal ## @todo call updateBatchSettings function @Slot() def startBatch(self): self.start_time = current_milli_time() self.end_time = current_milli_time() + (self.duration * 1000) self.well_positioner.reset_current_well() self.signals.batch_active.emit() self.is_active = True self.msg("Batch process initialized and started with:\n\tDuration: " + str(self.duration) + "\n\tInterleave: " + str(self.interleave) + "\n\tStart_time: " + str(self.start_time) + "\n\tEnd_time: " + str(self.end_time)) print("Batch process initialized and started with:\n\tDuration: " + str(self.duration) + "\n\tInterleave: " + str(self.interleave) + "\n\tStart_time: " + str(self.start_time) + "\n\tEnd_time: " + str(self.end_time)) self.runBatch() return ## @brief BatchProcessor()::runBatch(self) runs the batch after it is started. ## @note the interleave is actually the interleave + processingtime of the "for target in self.Well_Targets:" loop def runBatch(self): if self.logging: recording_file_name = os.path.sep.join( [self.path, 'batch_positioning_results.csv']) recording_file = open(recording_file_name, "w") # build csv file heading record_str = "run_start_time, run_time," for target in self.Well_Targets: record_str += ',' + str( self.Well_Map[target[0][1]][1][1]) + ',' + str( self.Well_Map[1][target[0][0]][0]) recording_file.write(record_str + "\n") else: recording_file = None first_run = True while True: print("BatchProcessor thread check: " + str(QThread.currentThread())) ## Run start time run_start_time = current_milli_time() actual_postions = [] # Home first on avery run self.well_positioner.stepper_control.homeXY() for target in self.Well_Targets: if not self.is_active: self.signals.batch_inactive.emit() self.msg("Batch stopped.") print("Batch stopped.") return else: row = target[0][0] column = target[0][1] self.msg("Target: " + str(target[0][2])) ## print("Target: at (" + str(self.Well_Map[column][1][1]) + ", " + str(self.Well_Map[1][row][0]) +")" + ", first run: " + str(first_run)) if (self.well_positioner.goto_well( self.Well_Map[column][1][1], self.Well_Map[1][row][0], first_run)): ## if found well self.snapshot_request( str(self.batch_id) + "/" + str(target[0][2])) (self.Well_Map[1][row][0], self.Well_Map[column][1][1] ) = self.well_positioner.get_current_well() print(" Target adapted to (" + str(self.Well_Map[column][1][1]) + ", " + str(self.Well_Map[1][row][0]) + ")") actual_postions.append( self.well_positioner.get_current_well()) while self.SnapshotTaken is False: if not self.is_active: self.signals.batch_inactive.emit() #self.msg("Batch stopped, breaking out of SnapshotTaken is False while loop.") print( "Batch stopped, breaking out of SnapshotTaken is False while loop." ) return print("Waiting in snapshot loop") self.wait_ms(50) self.msg(str(target) + " finished.") print(str(target) + " finished.") if self.end_time < current_milli_time(): self.msg("batch completed") print("batch completed") self.signals.batch_inactive.emit() self.stopBatch() return else: self.msg("Remaining time " + str(self.end_time - current_milli_time())) print("Remaining time " + str(self.end_time - current_milli_time()) + " ms") # first run is completed, remove this statemant to adapt to well-position in stead of feedforward if actual_postions: first_run = False run_time = current_milli_time() - run_start_time self.msg("Run time: " + str(run_time)) print("Run time: " + str(run_time) + "\nWaiting for " + str(self.interleave * 1000 - run_time) + " ms") if self.interleave * 1000 - run_time < 0: self.msg( "To short interleave, please increase the interleave to at least: " + str(run_time)) print( "To short interleave, please increase the interleave to at least: " + str(run_time)) else: ## Wait for the specified interleave minus the run_time of one run self.msg("Waiting for: " + str(self.interleave * 1000 - run_time) + " ms") print("Waiting for: " + str(self.interleave * 1000 - run_time) + " ms") self.wait_ms(self.interleave * 1000 - run_time) if recording_file: record_str = str(run_start_time) + ',' + str(run_time) for actual_position in actual_postions: record_str += ',' + str(actual_position[0]) + ',' + str( actual_position[1]) print(record_str) recording_file.write(record_str + "\n") if recording_file: recording_file.close() #self.msg("Breaking out batch process") print("Finishing batch process") return ## @brief BatchProcessor()::wait_ms(self, milliseconds) is a delay function. ## @param milliseconds is the number of milliseconds to wait. def wait_ms(self, milliseconds): self.GeneralEventLoop = QEventLoop() QTimer.singleShot(milliseconds, self.GeneralEventLoop.exit) self.GeneralEventLoop.exec_() return ## @brief BatchProcessor()::snapshot_request(self, message) emits the self.signals.snapshot_requested signal. ## @param message is the pathname of the desired snapshot def snapshot_request(self, message): self.msg("Requesting snapshot") self.signals.snapshot_requested.emit(message) self.snapshot_await() return ## @brief StepperWellPositioning()::snapshot_await(self) runs an eventloop while the new snapshot is not stored in self.image yet. def snapshot_await(self): self.SnapshotTaken = False while self.is_active and not self.SnapshotTaken: #self.msg("Waiting for snapshot") self.SnapshotEventLoop = QEventLoop() QTimer.singleShot(10, self.SnapshotEventLoop.exit) self.SnapshotEventLoop.exec_() return @Slot() def snapshot_confirmed(self): self.SnapshotTaken = True if not (self.SnapshotEventLoop is None): self.SnapshotEventLoop.exit() return ## @brief BatchProcessor()::stopBatch is a slot function which is called when the button stopBatch is pressed on the MainWindow GUI. ## It stops the batch process and resets the wait eventloop if running. @Slot() def stopBatch(self): #self.msg("Stopping Batch process") print("Stopping Batch process") self.signals.batch_inactive.emit() self.signals.process_inactive.emit( ) ## Used to stop positioning process of function StepperWellPositioning::goto_target self.is_active = False self.snapshot_confirmed() if not (self.GeneralEventLoop is None): self.GeneralEventLoop.exit() print("Exit BatchProcessor::GeneralEventloop") if not (self.SnapshotEventLoop is None): self.SnapshotEventLoop.exit() print("Exit BatchProcessor::SnapshotEventLoop") return @Slot() def close(self): self.stopBatch() return
class StepperWellPositioning(): signals = signal.signalClass() process_activity = False stepper_control = None ## object StepperControl instance current_well_row = None current_well_column = None Stopped = True GeneralEventLoop = None SnapshotEventLoop = None SnapshotTaken = False image = np.ndarray image_area = None WPE = None WPE_target = None WPE_targetRadius = None Well_Map = None diaphragm_diameter = 12.0 ## mm ## @brief StepperWellPositioning()::__init__ initialises the stepper objects for X and Y axis and initialises the gcodeSerial to the class member variable. ## @param steppers is the StepperControl object representing the X- and Y-axis ## @param Well_data contains the target well specified by the user in the batch.ini file def __init__(self, steppers, Well_data, record_path): self.stepper_control = steppers self.Well_Map = Well_data log_fine_tuning = False if record_path is None else True self.path = record_path return ## @brief StepperWellPositioning()::msg emits the message signal. This emit will be catched by the logging slot function in main.py. ## @param message is the string message to be emitted. @Slot(str) def msg(self, message): if message is not None: self.signals.mes.emit(self.__class__.__name__ + ": " + str(message)) return ## @brief StepperWellPositioning()::reset_current_well(self) sets the current well position (XY) to None def reset_current_well(self): self.set_current_well(None, None) return ## @brief StepperWellPositioning()::set_current_well(self) sets the current well position. ## @param column is the column or x well coordinate ## @param row is the row or y well coordinate def set_current_well(self, column, row): self.current_well_column = column self.current_well_row = row return ## @brief StepperWellPositioning()::get_current_well(self) is the well position getter. ## @return current_well_column (x position in mm from home) and current_well_row (y position in mm from home). def get_current_well(self): if self.current_well_column == None or self.current_well_row == None: self.current_well_column = None self.current_well_row = None return self.current_well_column, self.current_well_row ## @brief StepperWellPositioning()::wait_ms(self, milliseconds) is a delay function. ## @param milliseconds is the number of milliseconds to wait. def wait_ms(self, milliseconds): GeneralEventLoop = QEventLoop() QTimer.singleShot(milliseconds, GeneralEventLoop.exit) GeneralEventLoop.exec_() return ## @brief StepperWellPositioning()::goto_well(self, row, column): ## @author Robin Meekers ## @author Gert van Lagen (ported to new prototype which makes use of the Wrecklab PrintHAT) @Slot() def goto_well(self, row, column, adapt_to_well=False): print("In goto_well function") print(" adapt to well is " + str(adapt_to_well)) ## self.stepper_control.enableMotors() self.signals.process_active.emit() self.Stopped = False self.stepper_control.setLightPWM(1.0) ## If the Well Position Evaluator is not initialized. if self.WPE is None: ## define the resolution of the received images to be entered into the evaluator. self.snapshot_request() self.snapshot_await() self.WPE_target = (int(self.image.shape[1] / 2), int(self.image.shape[0] / 2)) self.WPE = imageProcessor.WellPositionEvaluator( (self.image.shape[0], self.image.shape[1])) self.msg("Current well: " + str(self.get_current_well())) current_row, current_column = self.get_current_well() ## Position unknown if current_row is None or current_column is None: ## Need to go to home position first self.msg("Current position unknown. Moving to home...") self.stepper_control.homeXY() ## If homing is succeeded and confirmed by the STM of the Wrecklab PrintHAT if self.stepper_control.homing_confirmed: self.image = None self.snapshot_request() self.snapshot_await() self.wait_ms( 1000) ## Wait for the snapshot to be stored in self.image ## Arrived at home, move to first well. ## The exact centre of the image can more easily be determined at the home position, ## as there is no distortion in the image by light refracting. self.msg("Looking for light source...") WPE_Error = self.WPE.evaluate(self.image, (int( self.image.shape[1] / 2), int(self.image.shape[0] / 2))) self.msg("Found light-source at: (" + str(WPE_Error[0][0]) + " | " + str(WPE_Error[0][1]) + "). Radius: " + str(WPE_Error[2])) self.WPE_targetRadius = WPE_Error[2] self.WPE_target = (int(self.image.shape[1] / 2) + WPE_Error[0][0], int(self.image.shape[0] / 2) + WPE_Error[0][1]) self.msg("WPE_target: " + str(self.WPE_target[0]) + " | " + str(self.WPE_target[1])) self.signals.target_located.emit( (self.WPE_target[0], self.WPE_target[1], self.WPE_targetRadius)) self.WPE.circle_minRadius = int( self.WPE_targetRadius * 0.8 ) ## Roughly the amount remaining if the target is on the edge between wells. self.WPE.circle_maxRadius = int( self.WPE_targetRadius ) ## The well can never be larger than the diaphragm of the light source. ## ## Homing succeeded, light source found, lets move to the first target of Well_map ## self.set_current_well(column, row) ## self.stepper_control.moveToWell(column, row) self.signals.first_move.emit() else: return False ## position known if self.process_activity: # JV: column and row is probably X and Y in Robin's functions, also in Gert's functions? self.stepper_control.moveToWell(column, row) ## Wait for ms depending on moving distance, JV:why? if self.current_well_column is None or self.current_well_row is None: dist = 60 else: dist = math.sqrt( float( abs(float(column) - float(self.current_well_column)) * abs(float(column) - float(self.current_well_column))) + float( abs(float(row) - float(self.current_well_row)) * abs(float(row) - float(self.current_well_row)))) self.msg("Moving distance: " + str(dist)) if dist < 20: self.msg("Delay: 1999ms | dist: " + str(dist) + "mm") self.wait_ms(1999) elif dist < 50: self.msg("Delay: 3999ms | dist: " + str(dist) + "mm") self.wait_ms(3999) elif dist < 70: self.msg("Delay: 5999ms | dist: " + str(dist) + "mm") self.wait_ms(5999) elif dist < 85: self.msg("Delay: 8999ms | dist: " + str(dist) + "mm") self.wait_ms(8999) elif dist > 85: self.msg("Delay: 11999ms | dist: " + str(dist) + "mm") self.wait_ms(11999) else: self.msg("Delay: 4999ms | Unknown dist: " + str(dist) + "mm") self.wait_ms(4999) self.set_current_well(column, row) print( str(self.WPE_target[0]) + " | " + str(self.WPE_target[1]) + " first run " + str(adapt_to_well)) if adapt_to_well: self.goto_target() else: self.wait_ms( 2000) ## Wait for the snapshot to be stored in self.image self.msg("Well positioning succeeded.") ## Manual confirmation needed. STM is too fast for the software to catch the last confirmation. self.stepper_control.move_confirmed = True ## if not self.goto_target(): ## self.msg("Well positioning not succeeded.") ## self.set_current_well(None, None) ## Never reached reference point ## self.Stopped = True ## self.signals.process_inactive.emit() ## ## ## Manual confirmation needed. STM is too fast for the software to catch the last confirmation. ## self.stepper_control.move_confirmed = True ## return False ## else: ## self.msg("Well positioning succeeded.") ## ## Manual confirmation needed. STM is too fast for the software to catch the last confirmation. ## self.stepper_control.move_confirmed = True else: print( "Positioning process inactive, cannot call goto_target positioning controller function." ) self.msg( "Positioning process inactive, cannot call goto_target positioning controller function." ) ## Different row? if row != self.current_well_row or column != self.current_well_column: self.msg("moving from: (" + str(self.current_well_row) + " | " + str(self.current_well_column) + ") to: (" + str(row) + " | " + str(column) + ")") ## why can we not re-enable motors during steps? Because M17 is not implemented by Klipper ## self.stepper_control.disableMotors() self.stepper_control.setLightPWM(0.0) return True ## @brief StepperWellPositioning()::goto_target(self) evaluates the target circle using class Well_Position_Evaluator, updates the snapshot, ## and corrects the position of the well while not aligned with the light source. def goto_target(self): loops_ = 0 error_count = 0 WPE_Error = None new_column = 0 new_row = 0 error_threshold = 100 #50#120 # higher number means lower error... resolution = self.diaphragm_diameter / self.WPE_targetRadius # [mm/px] run_start_time = current_milli_time() if self.path is not None: column, row = self.get_current_well() log_file_name = '_'.join([ str(run_start_time), 'goto_target', str(round(column)), str(round(row)) ]) recording_file_name = os.path.sep.join([self.path, log_file_name]) recording_file = open(recording_file_name, "w") record_str = "run_time, WPE_target[0], WPE_target[1], WPE_Error[0][0], WPE_Error[0][1]" recording_file.write(record_str + "\n") else: recording_file = None ## Do while the well is not aligned with the light source. while True: if (self.stepper_control.move_confirmed): if not self.process_activity: #self.msg("!Returning from alignment controller loop in StepperWellPositioning::goto_target") print( "!Returning from alignment controller loop in StepperWellPositioning::goto_target" ) return False ## Wait for image to stabilize and request new snapshot self.wait_ms(2000) self.snapshot_request() ## Wait for snapshot to be stored in self.image self.wait_ms(1000) ## Evaluate current wellposition relative to the light source. WPE_Error = self.WPE.evaluate(self.image, self.WPE_target) self.msg("WPE target at (" + str(self.WPE_target[0]) + " | " + str(self.WPE_target[1]) + ")") ## Area error, surface smaller or equal to 0 if WPE_Error[1] <= 0: self.msg( "Possibly something wrong with captured frame, retry with another snapshot" ) error_count = error_count + 1 if (error_count >= 3): self.msg("Error_count = " + str(error_count)) return False continue else: self.msg("Well found at offset (" + str(WPE_Error[0][0]) + " | " + str(WPE_Error[0][1]) + ")") self.signals.well_located.emit( (self.WPE_target[0] + WPE_Error[0][0], self.WPE_target[1] + WPE_Error[0][1], WPE_Error[2])) # ## Some # error_count = 0 # if (abs(WPE_Error[0][0]-30) < (self.image.shape[0] / error_threshold) and abs(WPE_Error[0][1]+30) < (self.image.shape[0] / error_threshold)) or (loops_ >20): ## No more adjustments to make or system is oscilating # if loops_ > 20:loops_ # self.msg("More than 20 correction loops, return False") # return False # self.msg("Returning from positioner controller, loops: " + str(loops_)) # return True ## Define controller variables for column and row | x and y. ## new_column = float(WPE_Error[0][0]) / ((loops_+1)*20.0) ## new_row = float(WPE_Error[0][1]) / ((loops_+1)*15.0) ## column, row = self.get_current_well() run_time = current_milli_time() - run_start_time if recording_file is not None: record_str = str(run_time) + ',' + str( self.WPE_target[0]) + ',' + str( self.WPE_target[1]) + ',' + str( WPE_Error[0][0]) + ',' + str(WPE_Error[0][1]) recording_file.write(record_str + "\n") error = np.sqrt(WPE_Error[0][0]**2 + WPE_Error[0][1]**2) threshold = min(self.image.shape[0:1]) / error_threshold print(" well placement error: {}, while acceptable error:{}". format(error, threshold)) if error > threshold: # Decrease stepsize on each iteration for smooth convergence new_column = float( WPE_Error[0][0]) * resolution / (loops_ + 1) new_row = float( WPE_Error[0][1]) * resolution / (loops_ + 1) column, row = self.get_current_well() new_column += column new_row += row new_column = round(new_column, 1) new_row = round(new_row, 1) self.stepper_control.moveToWell(new_column, new_row) self.set_current_well(new_column, new_row) else: break loops_ += 1 if loops_ > 5: self.msg("Too many correction loops, giving up") break if not self.process_activity: #self.msg("Returning from alignment controller loop in StepperWellPositioning::goto_target") print( "Returning from alignment controller loop in StepperWellPositioning::goto_target" ) return False if recording_file: recording_file.close() return True ## @brief StepperWellPositioning()::snapshot_request(self) emits the snapshot_request signal def snapshot_request(self): self.signals.snapshot_requested.emit(str((1, 1))) ## @brief StepperWellPositioning()::snapshot_awiat(self) runs an eventloop while the new snapshot is not stored in self.image yet. def snapshot_await(self): self.SnapshotTaken = False while not self.Stopped and not self.SnapshotTaken: self.SnapshotEventLoop = QEventLoop() QTimer.singleShot(10, self.SnapshotEventLoop.exit) self.SnapshotEventLoop.exec_() ## @brief StepperWellPositioning()::snapshot_confirmed(self, snapshot) exits the event loop of snapshot_await when the new image is arived. ## It updates the self.image variable with the new variable and defines the image area and WPE_target. @Slot(np.ndarray) def snapshot_confirmed(self, snapshot): self.image = snapshot # self.WPE_target = (int(self.image.shape[1] / 2), int(self.image.shape[0] / 2)) self.image_area = int((self.image.shape[0] * self.image.shape[1])) self.SnapshotTaken = True if not (self.SnapshotEventLoop is None): self.SnapshotEventLoop.exit() return ## @brief StepperWellPositioning()::setProcessInactive(self) disables the positionings process if the signal is emitted. @Slot() def setProcessInactive(self): #self.msg("Disabled positioning process activity") print("Disabled positioning process activity") self.process_activity = False self.Stopped = True if not (self.GeneralEventLoop is None): self.GeneralEventLoop.exit() #self.msg("Exit StepperWellPositioning::GeneralEventloop") print("Exit StepperWellPositioning::GeneralEventloop") if not (self.SnapshotEventLoop is None): self.SnapshotEventLoop.exit() #self.msg("Exit StepperWellPositioning::SnapshotEventLoop") print("Exit StepperWellPositioning::SnapshotEventLoop") return ## @brief StepperWellPositioning()::setProcessActive(self) enables the positionings process if the signal is emitted. @Slot() def setProcessActive(self): self.process_activity = True self.Stopped = False return @Slot() def close(self): self.stepper_control.setLightPWM(0.0) self.stepper_control.setFanPWM(0.0) self.process_activity = False self.Stopped = True return