Skip to content

Commit

Permalink
delay scan grid generation
Browse files Browse the repository at this point in the history
  • Loading branch information
soham1202 committed Dec 6, 2024
1 parent b45b5f3 commit beaa304
Show file tree
Hide file tree
Showing 4 changed files with 126 additions and 83 deletions.
2 changes: 1 addition & 1 deletion software/control/_def.py
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,7 @@ class SOFTWARE_POS_LIMIT:
DEFAULT_MULTIPOINT_NY=1

ENABLE_FLEXIBLE_MULTIPOINT = True
USE_OVERLAP_FOR_FLEXIBLE = False
USE_OVERLAP_FOR_FLEXIBLE = True
ENABLE_WELLPLATE_MULTIPOINT = True
ENABLE_RECORDING = False

Expand Down
135 changes: 80 additions & 55 deletions software/control/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -726,6 +726,7 @@ class NavigationController(QObject):
zPos = Signal(float)
thetaPos = Signal(float)
xyPos = Signal(float,float)
scanGridPos = Signal(float,float)
signal_joystick_button_pressed = Signal()

# x y z axis pid enable flag
Expand Down Expand Up @@ -754,10 +755,11 @@ def __init__(self,microcontroller, objectivestore, parent=None):
# to be moved to gui for transparency
self.microcontroller.set_callback(self.update_pos)

# self.timer_read_pos = QTimer()
# self.timer_read_pos.setInterval(PosUpdate.INTERVAL_MS)
# self.timer_read_pos.timeout.connect(self.update_pos)
# self.timer_read_pos.start()
self.movement_timer = QTimer(self)
self.movement_timer.setInterval(500) # Single 500ms check
self.movement_timer.timeout.connect(self.check_movement_status)
self.movement_timer.setSingleShot(True) # Timer only fires once
self.movement_threshold = 0.0001 # mm

# scan start position (obsolete? only for TiledDisplay)
self.scan_begin_position_x = 0
Expand All @@ -778,7 +780,70 @@ def set_flag_click_to_move(self, flag):
def get_flag_click_to_move(self):
return self.click_to_move

def scan_preview_move_from_click(self, click_x, click_y, image_width, image_height, Nx=1, Ny=1, dx_mm=0.9, dy_mm=0.9):
def update_pos(self,microcontroller):
# get position from the microcontroller
last_x_pos = self.x_pos_mm
last_y_pos = self.y_pos_mm
x_pos, y_pos, z_pos, theta_pos = microcontroller.get_pos()
self.z_pos = z_pos
# calculate position in mm or rad
if USE_ENCODER_X:
self.x_pos_mm = x_pos*ENCODER_POS_SIGN_X*ENCODER_STEP_SIZE_X_MM
else:
self.x_pos_mm = x_pos*STAGE_POS_SIGN_X*self.get_mm_per_ustep_X()
if USE_ENCODER_Y:
self.y_pos_mm = y_pos*ENCODER_POS_SIGN_Y*ENCODER_STEP_SIZE_Y_MM
else:
self.y_pos_mm = y_pos*STAGE_POS_SIGN_Y*self.get_mm_per_ustep_Y()
if USE_ENCODER_Z:
self.z_pos_mm = z_pos*ENCODER_POS_SIGN_Z*ENCODER_STEP_SIZE_Z_MM
else:
self.z_pos_mm = z_pos*STAGE_POS_SIGN_Z*self.get_mm_per_ustep_Z()
if USE_ENCODER_THETA:
self.theta_pos_rad = theta_pos*ENCODER_POS_SIGN_THETA*ENCODER_STEP_SIZE_THETA
else:
self.theta_pos_rad = theta_pos*STAGE_POS_SIGN_THETA*(2*math.pi/(self.theta_microstepping*FULLSTEPS_PER_REV_THETA))

# emit the updated position
self.xPos.emit(self.x_pos_mm)
self.yPos.emit(self.y_pos_mm)
self.zPos.emit(self.z_pos_mm*1000)
self.thetaPos.emit(self.theta_pos_rad*360/(2*math.pi))
self.xyPos.emit(self.x_pos_mm,self.y_pos_mm)

if microcontroller.signal_joystick_button_pressed_event:
if self.enable_joystick_button_action:
self.signal_joystick_button_pressed.emit()
print('joystick button pressed')
microcontroller.signal_joystick_button_pressed_event = False

# Check if position has changed
if last_x_pos != self.x_pos_mm or last_y_pos != self.y_pos_mm:
# restart movement timer
QMetaObject.invokeMethod(self.movement_timer, "start", Qt.QueuedConnection)

def check_movement_status(self):
"""Check if stage has stopped moving after timer delay"""
x_pos, y_pos, z_pos, theta_pos = self.microcontroller.get_pos()
# calculate position in mm or rad
if USE_ENCODER_X:
x_pos_mm = x_pos*ENCODER_POS_SIGN_X*ENCODER_STEP_SIZE_X_MM
else:
x_pos_mm = x_pos*STAGE_POS_SIGN_X*self.get_mm_per_ustep_X()
if USE_ENCODER_Y:
y_pos_mm = y_pos*ENCODER_POS_SIGN_Y*ENCODER_STEP_SIZE_Y_MM
else:
y_pos_mm = y_pos*STAGE_POS_SIGN_Y*self.get_mm_per_ustep_Y()

delta_x = abs(self.x_pos_mm - x_pos_mm)
delta_y = abs(self.y_pos_mm - y_pos_mm)

# check if movement less than thresshold (i.e. stopped moving)
if delta_x < self.movement_threshold and delta_y < self.movement_threshold and not self.microcontroller.is_busy():
# emit pos to draw scan grid
self.scanGridPos.emit(self.x_pos_mm, self.y_pos_mm)

def scan_preview_move_from_click(self, click_x, click_y, image_width, image_height, Nx=1, Ny=1, dx_mm=0.9, dy_mm=0.9): # obsolete (only for tiled display)
"""
napariTiledDisplay uses the Nx, Ny, dx_mm, dy_mm fields to move to the correct fov first
imageArrayDisplayWindow assumes only a single fov (default values do not impact calculation but this is less correct)
Expand Down Expand Up @@ -892,40 +957,6 @@ def move_y_to_usteps(self,usteps):
def move_z_to_usteps(self,usteps):
self.microcontroller.move_z_to_usteps(usteps)

def update_pos(self,microcontroller):
# get position from the microcontroller
x_pos, y_pos, z_pos, theta_pos = microcontroller.get_pos()
self.z_pos = z_pos
# calculate position in mm or rad
if USE_ENCODER_X:
self.x_pos_mm = x_pos*ENCODER_POS_SIGN_X*ENCODER_STEP_SIZE_X_MM
else:
self.x_pos_mm = x_pos*STAGE_POS_SIGN_X*self.get_mm_per_ustep_X()
if USE_ENCODER_Y:
self.y_pos_mm = y_pos*ENCODER_POS_SIGN_Y*ENCODER_STEP_SIZE_Y_MM
else:
self.y_pos_mm = y_pos*STAGE_POS_SIGN_Y*self.get_mm_per_ustep_Y()
if USE_ENCODER_Z:
self.z_pos_mm = z_pos*ENCODER_POS_SIGN_Z*ENCODER_STEP_SIZE_Z_MM
else:
self.z_pos_mm = z_pos*STAGE_POS_SIGN_Z*self.get_mm_per_ustep_Z()
if USE_ENCODER_THETA:
self.theta_pos_rad = theta_pos*ENCODER_POS_SIGN_THETA*ENCODER_STEP_SIZE_THETA
else:
self.theta_pos_rad = theta_pos*STAGE_POS_SIGN_THETA*(2*math.pi/(self.theta_microstepping*FULLSTEPS_PER_REV_THETA))
# emit the updated position
self.xPos.emit(self.x_pos_mm)
self.yPos.emit(self.y_pos_mm)
self.zPos.emit(self.z_pos_mm*1000)
self.thetaPos.emit(self.theta_pos_rad*360/(2*math.pi))
self.xyPos.emit(self.x_pos_mm,self.y_pos_mm)

if microcontroller.signal_joystick_button_pressed_event:
if self.enable_joystick_button_action:
self.signal_joystick_button_pressed.emit()
print('joystick button pressed')
microcontroller.signal_joystick_button_pressed_event = False

def home_x(self):
self.microcontroller.home_x()

Expand Down Expand Up @@ -3524,6 +3555,10 @@ def create_layers(self):
self.view.addItem(self.scan_overlay_item)
self.view.addItem(self.fov_overlay_item)

self.background_item.setZValue(-1) # Background layer at the bottom
self.scan_overlay_item.setZValue(0) # Scan overlay in the middle
self.fov_overlay_item.setZValue(1) # FOV overlay on top

def update_display_properties(self, sample):
if sample == 'glass slide':
self.location_update_threshold_mm = 0.2
Expand Down Expand Up @@ -3601,29 +3636,19 @@ def update_wellplate_settings(self, sample_format, a1_x_mm, a1_y_mm, a1_x_pixel,
self.update_display_properties(sample)
self.draw_current_fov(self.x_mm, self.y_mm)

def update_current_location(self, x_mm=None, y_mm=None):
def draw_fov_current_location(self, x_mm=None, y_mm=None):
if x_mm is None and y_mm is None:
if self.x_mm is None and self.y_mm is None:
return
else:
self.draw_current_fov(self.x_mm, self.y_mm)

elif self.x_mm is not None and self.y_mm is not None:
# update only when the displacement has exceeded certain value
if abs(x_mm - self.x_mm) > self.location_update_threshold_mm or abs(y_mm - self.y_mm) > self.location_update_threshold_mm:
self.draw_current_fov(x_mm, y_mm)
self.x_mm = x_mm
self.y_mm = y_mm
# update_live_scan_grid
if 'glass slide' in self.sample and not self.acquisition_started:
self.signal_update_live_scan_grid.emit(x_mm, y_mm)
self.draw_current_fov(self.x_mm, self.y_mm)
else:
self.draw_current_fov(x_mm, y_mm)
self.x_mm = x_mm
self.y_mm = y_mm
# update_live_scan_grid
if 'glass slide' in self.sample and not self.acquisition_started:
self.signal_update_live_scan_grid.emit(x_mm, y_mm)

def draw_scan_grid(self, x_mm, y_mm):
if 'glass slide' in self.sample and not self.acquisition_started:
self.signal_update_live_scan_grid.emit(x_mm, y_mm)

def get_FOV_pixel_coordinates(self, x_mm, y_mm):
if self.sample == 'glass slide':
Expand Down
8 changes: 6 additions & 2 deletions software/control/gui_hcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -663,7 +663,9 @@ def makeConnections(self):
self.objectivesWidget.signal_objective_changed.connect(self.navigationViewer.on_objective_changed)
if ENABLE_FLEXIBLE_MULTIPOINT:
self.objectivesWidget.signal_objective_changed.connect(self.flexibleMultiPointWidget.update_fov_positions)
self.navigationController.xyPos.connect(self.navigationViewer.update_current_location)
self.navigationController.xyPos.connect(self.navigationViewer.draw_fov_current_location)
if WELLPLATE_FORMAT == 'glass slide':
self.navigationController.scanGridPos.connect(self.navigationViewer.draw_scan_grid)
self.multipointController.signal_register_current_fov.connect(self.navigationViewer.register_fov)
self.multipointController.signal_current_configuration.connect(self.liveControlWidget.set_microscope_mode)
self.multipointController.signal_z_piezo_um.connect(self.piezoWidget.update_displacement_um_display)
Expand Down Expand Up @@ -704,7 +706,7 @@ def makeConnections(self):
if ENABLE_WELLPLATE_MULTIPOINT:
self.wellSelectionWidget.signal_wellSelected.connect(self.wellplateMultiPointWidget.set_well_coordinates)
self.objectivesWidget.signal_objective_changed.connect(self.wellplateMultiPointWidget.update_coordinates)
self.wellplateMultiPointWidget.signal_update_navigation_viewer.connect(self.navigationViewer.update_current_location)
self.wellplateMultiPointWidget.signal_update_navigation_viewer.connect(self.navigationViewer.draw_fov_current_location)

if SUPPORT_LASER_AUTOFOCUS:
self.liveControlWidget_focus_camera.signal_newExposureTime.connect(self.cameraSettingWidget_focus_camera.set_exposure_time)
Expand Down Expand Up @@ -916,11 +918,13 @@ def onWellplateChanged(self, format_):
self.toggleWellSelector(False)
self.multipointController.inverted_objective = False
self.navigationController.inverted_objective = False
self.navigationController.scanGridPos.connect(self.navigationViewer.draw_scan_grid)
self.setupSlidePositionController(is_for_wellplate=False)
else:
self.toggleWellSelector(True)
self.multipointController.inverted_objective = True
self.navigationController.inverted_objective = True
self.navigationController.scanGridPos.disconnect(self.navigationViewer.draw_scan_grid)
self.setupSlidePositionController(is_for_wellplate=True)

if format_ == '1536 well plate':
Expand Down
64 changes: 39 additions & 25 deletions software/control/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -3123,6 +3123,10 @@ def add_location(self):

# Check for duplicates using rounded values for comparison
if not np.any(np.all(self.location_list[:, :2] == [round(x,3), round(y,3)], axis=1)):
# Block signals to prevent triggering cell_was_changed
self.table_location_list.blockSignals(True)
self.dropdown_location_list.blockSignals(True)

# Store actual values in location_list
self.location_list = np.vstack((self.location_list, [[x, y, z]]))
self.location_ids = np.append(self.location_ids, name)
Expand All @@ -3148,58 +3152,67 @@ def add_location(self):
self.region_fov_coordinates_dict[name] = scan_coordinates

print(f"Added Region: {name} - x={x}, y={y}, z={z}")

# Set the current index to the newly added location
self.dropdown_location_list.setCurrentIndex(len(self.location_ids) - 1)
self.table_location_list.selectRow(row)

# Re-enable signals after the addition is complete
self.table_location_list.blockSignals(False)
self.dropdown_location_list.blockSignals(False)
else:
print("Duplicate location not added.")

def remove_location(self):
index = self.dropdown_location_list.currentIndex()
if index >= 0:
# Get the region ID
# Remove region ID and associated data
region_id = self.location_ids[index]
print("Before Removal:")
print(f"Location IDs: {self.location_ids}")
print(f"Region FOV Coordinates Dict Keys: {list(self.region_fov_coordinates_dict.keys())}")
print(f"Removing region: {region_id}")

# Remove overlays using actual stored coordinates
if region_id in self.region_fov_coordinates_dict:
for coord in self.region_fov_coordinates_dict[region_id]:
self.navigationViewer.deregister_fov_to_image(coord[0], coord[1])
del self.region_fov_coordinates_dict[region_id]
# Block signals to prevent unintended UI updates
self.table_location_list.blockSignals(True)
self.dropdown_location_list.blockSignals(True)

# Remove overlays and dictionaries
for coord in self.region_fov_coordinates_dict.pop(region_id, []):
self.navigationViewer.deregister_fov_to_image(coord[0], coord[1])

# Remove from data structures
self.region_coordinates.pop(region_id, None)
self.location_list = np.delete(self.location_list, index, axis=0)
self.location_ids = np.delete(self.location_ids, index)
if region_id in self.region_coordinates:
del self.region_coordinates[region_id]

# Update remaining IDs and UI
# Update UI
self.dropdown_location_list.removeItem(index)
self.table_location_list.removeRow(index)

# Reindex remaining regions and update UI
for i in range(index, len(self.location_ids)):
old_id = self.location_ids[i]
new_id = f'R{i}'
self.location_ids[i] = new_id

# Update dictionaries
self.region_coordinates[new_id] = self.region_coordinates.pop(old_id)
self.region_fov_coordinates_dict[new_id] = self.region_fov_coordinates_dict.pop(old_id)
self.region_coordinates[new_id] = self.region_coordinates.pop(old_id, None)
self.region_fov_coordinates_dict[new_id] = self.region_fov_coordinates_dict.pop(old_id, [])

# Update UI with rounded display values
self.table_location_list.setItem(i, 3, QTableWidgetItem(new_id))
# Update UI with new ID and coordinates
x, y, z = self.location_list[i]
location_str = f"x:{round(x,3)} mm y:{round(y,3)} mm z:{round(z*1000,1)} μm"
location_str = f"x:{round(x, 3)} mm y:{round(y, 3)} mm z:{round(z * 1000, 1)} μm"
self.dropdown_location_list.setItemText(i, location_str)
self.table_location_list.setItem(i, 3, QTableWidgetItem(new_id))

# Update UI
self.dropdown_location_list.removeItem(index)
self.table_location_list.removeRow(index)

print("After Removal:")
print(f"Location IDs: {self.location_ids}")
print(f"Region FOV Coordinates Dict Keys: {list(self.region_fov_coordinates_dict.keys())}")
# Re-enable signals
self.table_location_list.blockSignals(False)
self.dropdown_location_list.blockSignals(False)

# Clear overlay if no locations remain
if len(self.location_list) == 0:
self.navigationViewer.clear_overlay()

print(f"Remaining location IDs: {self.location_ids}")


def create_point_id(self):
self.scanCoordinates.get_selected_wells()
if len(self.scanCoordinates.name) == 0:
Expand Down Expand Up @@ -3810,6 +3823,7 @@ def set_default_scan_size(self):
self.set_default_shape()
print(self.navigationViewer.sample)
if 'glass slide' in self.navigationViewer.sample:
self.entry_scan_size.setValue(1.0) # init to 1mm when switching to 'glass slide'
self.entry_scan_size.setEnabled(True)
self.entry_well_coverage.setEnabled(False)
else:
Expand Down

0 comments on commit beaa304

Please sign in to comment.