Skip to content

Commit

Permalink
Merge pull request #116 from Cephla-Lab/feature/save-load-coordinates
Browse files Browse the repository at this point in the history
feat: Add save/load scan coordinates to CSV in the GUI
  • Loading branch information
hongquanli authored Feb 24, 2025
2 parents 533807f + 2d2fff5 commit 7e841d6
Showing 1 changed file with 155 additions and 15 deletions.
170 changes: 155 additions & 15 deletions software/control/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -3441,11 +3441,15 @@ def __init__(
self.setFrameStyle(QFrame.Panel | QFrame.Raised)
self.set_default_scan_size()

# Add state tracking for coordinates
self.has_loaded_coordinates = False

def add_components(self):
self.entry_well_coverage = QDoubleSpinBox()
self.entry_well_coverage.setRange(1, 999.99)
self.entry_well_coverage.setValue(100)
self.entry_well_coverage.setSuffix("%")
self.entry_well_coverage.setDecimals(0)
btn_width = self.entry_well_coverage.sizeHint().width()

self.btn_setSavingDir = QPushButton("Browse")
Expand Down Expand Up @@ -3547,6 +3551,9 @@ def add_components(self):
self.combobox_shape.setFixedWidth(btn_width)
# self.combobox_shape.currentTextChanged.connect(self.on_shape_changed)

self.btn_save_scan_coordinates = QPushButton("Save Coordinates")
self.btn_load_scan_coordinates = QPushButton("Load Coordinates")

self.checkbox_genAFMap = QCheckBox("Generate Focus Map")
self.checkbox_genAFMap.setChecked(False)

Expand Down Expand Up @@ -3595,25 +3602,25 @@ def add_components(self):
saving_path_layout.addWidget(self.btn_setSavingDir)
main_layout.addLayout(saving_path_layout)

# Experiment ID and Scan Shape
# Experiment ID
row_1_layout = QHBoxLayout()
row_1_layout.addWidget(QLabel("Experiment ID"))
row_1_layout.addWidget(self.lineEdit_experimentID)
row_1_layout.addWidget(QLabel("Well Shape"))
row_1_layout.addWidget(self.combobox_shape)
main_layout.addLayout(row_1_layout)

# Well Coverage, Scan Size, and Overlap
row_4_layout = QHBoxLayout()
row_4_layout.addWidget(QLabel("Size"))
row_4_layout.addWidget(self.entry_scan_size)
# row_4_layout.addStretch(1)
row_4_layout.addWidget(QLabel("FOV Overlap"))
row_4_layout.addWidget(self.entry_overlap)
# row_4_layout.addStretch(1)
row_4_layout.addWidget(QLabel("Well Coverage"))
row_4_layout.addWidget(self.entry_well_coverage)
main_layout.addLayout(row_4_layout)
# Scan Shape, FOV overlap, and Save / Load Scan Coordinates
row_2_layout = QGridLayout()
row_2_layout.addWidget(QLabel("Scan Shape"), 0, 0)
row_2_layout.addWidget(self.combobox_shape, 0, 1)
row_2_layout.addWidget(QLabel("Scan Size"), 0, 2)
row_2_layout.addWidget(self.entry_scan_size, 0, 3)
row_2_layout.addWidget(QLabel("Coverage"), 0, 4)
row_2_layout.addWidget(self.entry_well_coverage, 0, 5)
row_2_layout.addWidget(QLabel("FOV Overlap"), 1, 0)
row_2_layout.addWidget(self.entry_overlap, 1, 1)
row_2_layout.addWidget(self.btn_save_scan_coordinates, 1, 2, 1, 2)
row_2_layout.addWidget(self.btn_load_scan_coordinates, 1, 4, 1, 2)
main_layout.addLayout(row_2_layout)

grid = QGridLayout()

Expand Down Expand Up @@ -3725,6 +3732,10 @@ def add_components(self):
self.entry_NZ.valueChanged.connect(self.signal_stitcher_z_levels.emit)
# self.combobox_z_stack.currentIndexChanged.connect(self.signal_z_stacking.emit)

# Connect save/clear coordinates button
self.btn_save_scan_coordinates.clicked.connect(self.on_save_or_clear_coordinates_clicked)
self.btn_load_scan_coordinates.clicked.connect(self.on_load_coordinates_clicked)

def enable_manual_ROI(self, enable):
self.combobox_shape.model().item(3).setEnabled(enable)
if not enable:
Expand Down Expand Up @@ -3851,7 +3862,9 @@ def set_default_scan_size(self):
self.set_default_shape()

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.setValue(
0.1
) # init to 0.1mm when switching to 'glass slide' (for imaging a single FOV by default)
self.entry_scan_size.setEnabled(True)
self.entry_well_coverage.setEnabled(False)
else:
Expand Down Expand Up @@ -4169,6 +4182,133 @@ def emit_selected_channels(self):
def display_stitcher_widget(self, checked):
self.signal_stitcher_widget.emit(checked)

def toggle_coordinate_controls(self, has_coordinates: bool):
"""Toggle button text and control states based on whether coordinates are loaded"""
if has_coordinates:
self.btn_save_scan_coordinates.setText("Clear Coordinates")
# Disable scan controls when coordinates are loaded
self.combobox_shape.setEnabled(False)
self.entry_scan_size.setEnabled(False)
self.entry_well_coverage.setEnabled(False)
self.entry_overlap.setEnabled(False)
# Disable well selector
self.parent.wellSelectionWidget.setEnabled(False)
else:
self.btn_save_scan_coordinates.setText("Save Coordinates")
# Re-enable scan controls when coordinates are cleared
self.combobox_shape.setEnabled(True)
self.entry_scan_size.setEnabled(True)
if "glass slide" in self.navigationViewer.sample:
self.entry_well_coverage.setEnabled(False)
else:
self.entry_well_coverage.setEnabled(True)
self.entry_overlap.setEnabled(True)
# Re-enable well selector
self.parent.wellSelectionWidget.setEnabled(True)

self.has_loaded_coordinates = has_coordinates

def on_save_or_clear_coordinates_clicked(self):
"""Handle save/clear coordinates button click"""
if self.has_loaded_coordinates:
# Clear coordinates
self.scanCoordinates.clear_regions()
self.toggle_coordinate_controls(has_coordinates=False)
# Update display/coordinates as needed
self.update_coordinates()
else:
# Save coordinates (existing save functionality)
self.save_coordinates()

def on_load_coordinates_clicked(self):
"""Open file dialog and load coordinates from selected CSV file"""
file_path, _ = QFileDialog.getOpenFileName(
self, "Load Scan Coordinates", "", "CSV Files (*.csv);;All Files (*)" # Default directory
)

if file_path:
print("loading coordinates from", file_path)
self.load_coordinates(file_path)

def load_coordinates(self, file_path: str):
"""Load scan coordinates from a CSV file.
Args:
file_path: Path to CSV file containing coordinates
"""
try:
# Read coordinates from CSV
import pandas as pd

df = pd.read_csv(file_path)

# Validate CSV format
required_columns = ["Region", "X_mm", "Y_mm"]
if not all(col in df.columns for col in required_columns):
raise ValueError("CSV file must contain 'Region', 'X_mm', and 'Y_mm' columns")

# Clear existing coordinates
self.scanCoordinates.clear_regions()

# Load coordinates into scanCoordinates
for region_id in df["Region"].unique():
region_points = df[df["Region"] == region_id]
coords = list(zip(region_points["X_mm"], region_points["Y_mm"]))
self.scanCoordinates.region_fov_coordinates[region_id] = coords

# Calculate and store region center (average of points)
center_x = region_points["X_mm"].mean()
center_y = region_points["Y_mm"].mean()
self.scanCoordinates.region_centers[region_id] = (center_x, center_y)

# Register FOVs with navigation viewer
for x, y in coords:
self.navigationViewer.register_fov_to_image(x, y)

self._log.info(f"Loaded {len(df)} coordinates from {file_path}")

# Update UI state
self.toggle_coordinate_controls(has_coordinates=True)

except Exception as e:
self._log.error(f"Failed to load coordinates: {str(e)}")
QMessageBox.warning(self, "Load Error", f"Failed to load coordinates from {file_path}\nError: {str(e)}")

def save_coordinates(self):
"""Save scan coordinates to a CSV file.
Opens a file dialog for the user to choose save location and filename.
Coordinates are saved in CSV format with headers.
"""
# Open file dialog for user to select save location and filename
file_path, _ = QFileDialog.getSaveFileName(
self, "Save Scan Coordinates", "", "CSV Files (*.csv);;All Files (*)" # Default directory
)

if file_path:
# Add .csv extension if not present
if not file_path.lower().endswith(".csv"):
file_path += ".csv"

try:
# Get coordinates from scanCoordinates
coordinates = []
for region_id, fov_coords in self.scanCoordinates.region_fov_coordinates.items():
for x, y in fov_coords:
coordinates.append([region_id, x, y])

# Save to CSV with headers
import pandas as pd

df = pd.DataFrame(coordinates, columns=["Region", "X_mm", "Y_mm"])
df.to_csv(file_path, index=False)

self._log.info(f"Saved scan coordinates to {file_path}")

except Exception as e:
self._log.error(f"Failed to save coordinates: {str(e)}")
QMessageBox.warning(self, "Save Error", f"Failed to save coordinates to {file_path}\nError: {str(e)}")


class FocusMapWidget(QFrame):
"""Widget for managing focus map points and surface fitting"""
Expand Down

0 comments on commit 7e841d6

Please sign in to comment.