diff --git a/.gitignore b/.gitignore index c2f7e05..2cebdc7 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ docs/_build/ # User-settings ui/settings.json +ui/reticle_metadata.json # Unit test / coverage reports .coverage diff --git a/parallax/__init__.py b/parallax/__init__.py index 88f2bfe..9d1cda6 100644 --- a/parallax/__init__.py +++ b/parallax/__init__.py @@ -4,7 +4,7 @@ import os -__version__ = "0.37.23" +__version__ = "0.37.24" # allow multiple OpenMP instances os.environ["KMP_DUPLICATE_LIB_OK"] = "True" diff --git a/parallax/calculator.py b/parallax/calculator.py index 98b0248..085cc90 100644 --- a/parallax/calculator.py +++ b/parallax/calculator.py @@ -1,7 +1,7 @@ import os import logging import numpy as np -from PyQt5.QtWidgets import QWidget, QGroupBox, QLineEdit, QPushButton +from PyQt5.QtWidgets import QWidget, QGroupBox, QLineEdit, QPushButton, QLabel from PyQt5.uic import loadUi from PyQt5.QtCore import Qt @@ -13,9 +13,11 @@ ui_dir = os.path.join(os.path.dirname(package_dir), "ui") class Calculator(QWidget): - def __init__(self, model): + def __init__(self, model, reticle_selector): super().__init__() self.model = model + self.reticle_selector = reticle_selector + self.reticle = None self.ui = loadUi(os.path.join(ui_dir, "calc.ui"), self) self.setWindowTitle(f"Calculator") @@ -24,14 +26,32 @@ def __init__(self, model): # Create the number of GroupBox for the number of stages self.create_stage_groupboxes() - self.model.add_calc_instance(self) + self.reticle_selector.currentIndexChanged.connect(self.setCurrentReticle) + self.model.add_calc_instance(self) + def show(self): # Refresh the list of stage to show + self.change_global_label() self.set_calc_functions() # Show super().show() # Show the widget + def setCurrentReticle(self): + reticle_name = self.reticle_selector.currentText() + if not reticle_name: + return + # Extract the letter from reticle_name, assuming it has the format "Global coords (A)" + self.reticle = reticle_name.split('(')[-1].strip(')') + self.change_global_label() + + def change_global_label(self): + if self.reticle is None or self.reticle == "Global coords": + self.findChild(QLabel, f"labelGlobal").setText(f" Global") + return + else: + self.findChild(QLabel, f"labelGlobal").setText(f" Global ({self.reticle})") + def set_calc_functions(self): for stage_sn, item in self.model.transforms.items(): transM, scale = item[0], item[1] @@ -120,15 +140,73 @@ def is_valid_number(s): else: return None, None, None + def apply_reticle_adjustments(self, global_pts): + reticle_metadata = self.model.get_reticle_metadata(self.reticle) + reticle_rot = reticle_metadata.get("rot", 0) + reticle_rotmat = reticle_metadata.get("rotmat", np.eye(3)) # Default to identity matrix if not found + reticle_offset = np.array([ + reticle_metadata.get("offset_x", global_pts[0]), + reticle_metadata.get("offset_y", global_pts[1]), + reticle_metadata.get("offset_z", global_pts[2]) + ]) + + if reticle_rot != 0: + # Transpose because points are row vectors + global_pts = global_pts @ reticle_rotmat.T + global_pts = global_pts + reticle_offset + + global_x = np.round(global_pts[0], 1) + global_y = np.round(global_pts[1], 1) + global_z = np.round(global_pts[2], 1) + return global_x, global_y, global_z + def apply_transformation(self, local_point_, transM_LR, scale): + """Apply transformation to convert local to global coordinates.""" local_point = local_point_ * scale local_point = np.append(local_point, 1) global_point = np.dot(transM_LR, local_point) logger.debug(f"local_to_global: {local_point_} -> {global_point[:3]}") logger.debug(f"R: {transM_LR[:3, :3]}\nT: {transM_LR[:3, 3]}") + + # Ensure the reticle is defined and get its metadata + if self.reticle and self.reticle != "Global coords": + # Apply the reticle offset and rotation adjustment + global_x, global_y, global_z = self.apply_reticle_adjustments(global_point[:3]) + # Return the adjusted global coordinates + return np.array([global_x, global_y, global_z]) + return global_point[:3] - + + def apply_reticle_adjustments_inverse(self, global_point): + """Apply reticle offset and inverse rotation to the global point.""" + if self.reticle and self.reticle != "Global coords": + # Convert global_point to numpy array if it's not already + global_point = np.array(global_point) + + # Get the reticle metadata + reticle_metadata = self.model.get_reticle_metadata(self.reticle) + + # Get rotation matrix (default to identity if not found) + reticle_rotmat = reticle_metadata.get("rotmat", np.eye(3)) + + # Get offset values, default to global point coordinates if not found + reticle_offset = np.array([ + reticle_metadata.get("offset_x", 0), # Default to 0 if no offset is provided + reticle_metadata.get("offset_y", 0), + reticle_metadata.get("offset_z", 0) + ]) + + # Subtract the reticle offset + global_point = global_point - reticle_offset + # Undo the rotation + global_point = np.dot(global_point, reticle_rotmat) + + return global_point + def apply_inverse_transformation(self, global_point, transM_LR, scale): + """Apply inverse transformation to convert global to local coordinates.""" + global_point = self.apply_reticle_adjustments_inverse(global_point) + # Transpose the 3x3 rotation part R_T = transM_LR[:3, :3].T local_point = np.dot(R_T, global_point - transM_LR[:3, 3]) diff --git a/parallax/main_window_wip.py b/parallax/main_window_wip.py index 8dda4bb..a7731f4 100644 --- a/parallax/main_window_wip.py +++ b/parallax/main_window_wip.py @@ -777,4 +777,5 @@ def save_user_configs(self): def closeEvent(self, event): self.model.close_all_point_meshes() self.model.close_clac_instance() + self.model.close_reticle_metadata_instance() event.accept() \ No newline at end of file diff --git a/parallax/model.py b/parallax/model.py index 1960e59..9c0cc46 100755 --- a/parallax/model.py +++ b/parallax/model.py @@ -31,6 +31,9 @@ def __init__(self, version="V1", bundle_adjustment=False): # Calculator self.calc_instance = None + # reticle metadata + self.reticle_metadata_instance = None + # stage self.nStages = 0 self.stages = {} @@ -52,6 +55,9 @@ def __init__(self, version="V1", bundle_adjustment=False): # Transformation matrices of stages to global coords self.transforms = {} + # Reticle metadata + self.reticle_metadata = {} + def add_calibration(self, cal): """Add a calibration.""" self.calibrations[cal.name] = cal @@ -140,6 +146,27 @@ def add_transform(self, stage_sn, transform, scale): """Add transformation matrix between local to global coordinates.""" self.transforms[stage_sn] = [transform, scale] + def get_transform(self, stage_sn): + """Get transformation matrix between local to global coordinates.""" + return self.transforms.get(stage_sn) + + def add_reticle_metadata(self, reticle_name, metadata): + """Add transformation matrix between local to global coordinates.""" + self.reticle_metadata[reticle_name] = metadata + + def get_reticle_metadata(self, reticle_name): + """Get transformation matrix between local to global coordinates.""" + return self.reticle_metadata.get(reticle_name) + + def remove_reticle_metadata(self, reticle_name): + """Remove transformation matrix between local to global coordinates.""" + if reticle_name in self.reticle_metadata.keys(): + self.reticle_metadata.pop(reticle_name, None) + + def reset_reticle_metadata(self): + """Reset transformation matrix between local to global coordinates.""" + self.reticle_metadata = {} + def add_probe_detector(self, probeDetector): """Add a probe detector.""" self.probeDetectors.append(probeDetector) @@ -223,4 +250,12 @@ def add_calc_instance(self, instance): def close_clac_instance(self): if self.calc_instance is not None: self.calc_instance.close() + self.calc_instance = None + + def add_reticle_metadata_instance(self, instance): + self.reticle_metadata_instance = instance + + def close_reticle_metadata_instance(self): + if self.reticle_metadata_instance is not None: + self.reticle_metadata_instance.close() self.calc_instance = None \ No newline at end of file diff --git a/parallax/probe_calibration.py b/parallax/probe_calibration.py index c1bd8ad..51d62dc 100644 --- a/parallax/probe_calibration.py +++ b/parallax/probe_calibration.py @@ -84,7 +84,6 @@ def __init__(self, model, stage_listener): [0.0, 0.0, 0.0, 0.0], ] ) - self.model_LR, self.transM_LR, self.transM_LR_prev = None, None, None self.origin, self.R, self.scale = None, None, np.array([1, 1, 1]) self.avg_err = None @@ -272,7 +271,7 @@ def _get_transM(self, df, remove_noise=True, save_to_csv=False, file_name=None, if self._is_criteria_met_points_min_max() and len(local_points) > 10 \ and self.R is not None and self.origin is not None: local_points, global_points, valid_indices = self._remove_outliers( - local_points, global_points, threshold=noise_threshold) + local_points, global_points, threshold=noise_threshold) if len(local_points) <= 3 or len(global_points) <= 3: logger.warning("Not enough points for calibration.") @@ -492,7 +491,7 @@ def _is_enough_points(self): return False def _update_info_ui(self, disp_avg_error=False, save_to_csv=False, file_name=None): - sn = self.stage.sn + sn = self.stage.sn if sn is not None and sn in self.stages: stage_data = self.stages[sn] @@ -504,7 +503,7 @@ def _update_info_ui(self, disp_avg_error=False, save_to_csv=False, file_name=Non error = self.avg_err else: error = self.LR_err_L2_current - + self.transM_info.emit( sn, self.transM_LR, @@ -596,7 +595,7 @@ def update(self, stage, debug_info=None): Args: stage (Stage): The current stage object with new position data. """ - # update points in the file`` + # update points in the file self.stage = stage self._update_local_global_point(debug_info) # Do no update if it is duplicates @@ -618,6 +617,7 @@ def update(self, stage, debug_info=None): def complete_calibration(self, filtered_df): # save the filtered points to a new file + print("ProbeCalibration: complete_calibration") self.file_name = f"points_{self.stage.sn}.csv" self.transM_LR = self._get_transM(filtered_df, save_to_csv=True, file_name=self.file_name, noise_threshold=20) # TODO original #self.transM_LR = self._get_transM(filtered_df, save_to_csv=True, file_name=self.file_name, remove_noise=False) # Test @@ -629,7 +629,7 @@ def complete_calibration(self, filtered_df): self._print_formatted_transM() print("=========================================================") self._update_info_ui(disp_avg_error=True, save_to_csv=True, \ - file_name = f"transM_{self.stage.sn}.csv") + file_name = f"transM_{self.stage.sn}.csv") if self.model.bundle_adjustment: self.old_transM, self.old_scale = self.transM_LR, self.scale @@ -645,7 +645,7 @@ def complete_calibration(self, filtered_df): return # Register into model - self.model.add_transform(self.stage.sn, self.transM_LR, self.scale) + self.model.add_transform(self.stage.sn, self.transM_LR, self.scale) # Emit the signal to indicate that calibration is complete self.calib_complete.emit(self.stage.sn, self.transM_LR, self.scale) diff --git a/parallax/reticle_metadata.py b/parallax/reticle_metadata.py new file mode 100644 index 0000000..0ae75d9 --- /dev/null +++ b/parallax/reticle_metadata.py @@ -0,0 +1,316 @@ +import os +import logging +import json +import numpy as np +from scipy.spatial.transform import Rotation +from PyQt5.QtWidgets import QWidget, QGroupBox, QLineEdit, QPushButton +from PyQt5.uic import loadUi +from PyQt5.QtCore import Qt + +logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) + +package_dir = os.path.dirname(os.path.abspath(__file__)) +debug_dir = os.path.join(os.path.dirname(package_dir), "debug") +ui_dir = os.path.join(os.path.dirname(package_dir), "ui") + +class ReticleMetadata(QWidget): + def __init__(self, model, reticle_selector): + super().__init__() + self.model = model + self.reticle_selector = reticle_selector + + self.ui = loadUi(os.path.join(ui_dir, "reticle_metadata.ui"), self) + self.default_size = self.size() + self.setWindowTitle(f"Reticle Metadata") + self.setWindowFlags(Qt.Window | Qt.WindowMinimizeButtonHint | \ + Qt.WindowMaximizeButtonHint | Qt.WindowCloseButtonHint) + + self.groupboxes = {} # Change from list to dictionary + self.reticles = {} + self.alphabet_status = {chr(i): 0 for i in range(65, 91)} # A-Z with 0 availability status + + self.ui.add_btn.clicked.connect(self.add_groupbox) + self.ui.update_btn.clicked.connect(self.update_reticle_info) + + self.model.add_reticle_metadata_instance(self) + + def load_metadata_from_file(self): + json_path = os.path.join(ui_dir, "reticle_metadata.json") + if not os.path.exists(json_path): + logger.info("No existing metadata file found. Starting fresh.") + return + + try: + with open(json_path, 'r') as json_file: + reticle_data = json.load(json_file) + if reticle_data: + self.create_groupbox_from_metadata(reticle_data) + for group_box in self.groupboxes.values(): + self.update_reticles(group_box) + self.update_to_reticle_selector() + + except Exception as e: + logger.error(f"Error reading metadata file: {e}") + + def create_groupbox_from_metadata(self, reticle_data): + """Create a groupbox from metadata and populate it.""" + for reticle_info in reticle_data: + #name = reticle_info.get("name", "") + name = reticle_info.get("lineEditName", "") + if name in self.groupboxes.keys(): + return # Do not add a new groupbox if it already exists + + self.populate_groupbox(name, reticle_info) + + def add_groupbox(self): + """This method creates new groupboxes with an alphabet name.""" + alphabet = self.find_next_available_alphabet() + if alphabet is None: + logger.warning("No available slot for reticle. All alphabets are assigned.") + print("No available slot for reticle.") + return + + # Mark the alphabet as used + self.alphabet_status[alphabet] = 1 + + # Create an empty metadata dictionary for the new group box + reticle_info = {"lineEditName": alphabet} + self.populate_groupbox(alphabet, reticle_info) + + def populate_groupbox(self, name, reticle_info): + """Helper method to set up a groupbox.""" + group_box = QGroupBox(self) + loadUi(os.path.join(ui_dir, "reticle_QGroupBox.ui"), group_box) + + # Set the visible title and object name of the QGroupBox + group_box.setTitle(f"Reticle '{name}'") + group_box.setObjectName(name) + + # Mark the alphabet as used (if not already used) + if name in self.alphabet_status: + self.alphabet_status[name] = 1 + + # Populate the QLineEdit fields with the values from the metadata + for key, value in reticle_info.items(): + line_edit = group_box.findChild(QLineEdit, key) + if line_edit: + line_edit.setText(value) + + # Find the QLineEdit for the reticle name and connect the signal (group box name) + lineEditName = group_box.findChild(QLineEdit, "lineEditName") + if lineEditName: + lineEditName.setText(name) # Initialize with alphabet if not in metadata + # Connect the textChanged signal to dynamically update the group_box title and object name + lineEditName.textChanged.connect(lambda text, gb=group_box: self.update_groupbox_name(gb, text, name)) + + # Connect the remove button + push_button = group_box.findChild(QPushButton, "remove_btn") + if push_button: + #push_button.clicked.connect(lambda _, gb=group_box: self.remove_specific_groupbox(gb, name)) + push_button.clicked.connect(lambda _, gb=group_box: self.remove_specific_groupbox(gb)) + + # Extend the height of the form by 200 pixels + current_size = self.size() + self.resize(current_size.width(), current_size.height() + 200) + + # Insert the group_box just before the last item (which is the vertical spacer) + count = self.ui.verticalLayout.count() + self.ui.verticalLayout.insertWidget(count - 1, group_box) + + # Store the group_box in a dictionary to track added groupboxes + self.groupboxes[name] = group_box + + def update_groupbox_name(self, group_box, new_name, alphabet): + """Update the title and object name of the group box.""" + if alphabet == group_box.objectName(): + self.alphabet_status[alphabet] = 0 + + # Update the title and object name of the group box + if new_name.strip(): + group_box.setTitle(f"Reticle '{new_name}'") + group_box.setObjectName(new_name) + + if new_name.strip().isalpha() and len(new_name.strip()) == 1 and new_name.strip().upper() in self.alphabet_status: + self.alphabet_status[new_name] = 1 + + def remove_specific_groupbox(self, group_box): + name = group_box.findChild(QLineEdit, "lineEditName").text() + + if name in self.groupboxes.keys(): + group_box = self.groupboxes.pop(name) # Remove from dictionary + if name in self.reticles.keys(): + self.reticles.pop(name) + # Register in the model + self.model.remove_reticle_metadata(name) + self.ui.verticalLayout.removeWidget(group_box) + group_box.deleteLater() + + current_size = self.size() + self.resize(current_size.width(), current_size.height() - 200) + + if name.isalpha() and len(name) == 1 and name.upper() in self.alphabet_status: + self.alphabet_status[name.upper()] = 0 + + def find_next_available_alphabet(self): + for alphabet, status in self.alphabet_status.items(): + if status == 0: + return alphabet + return None + + def update_reticle_info(self): + self.update_to_file() + for group_box in self.groupboxes.values(): + self.update_reticles(group_box) + self.update_to_reticle_selector() + + def update_to_reticle_selector(self): + self.reticle_selector.clear() + self.reticle_selector.addItem(f"Global coords") + + # update dropdown menu with reticle names + for name in self.groupboxes.keys(): + self.reticle_selector.addItem(f"Global coords ({name})") + + def default_reticle_selector(self): + # Iterate over the added sgroup boxes and remove each one from the layout + for name, group_box in self.groupboxes.items(): + self.ui.verticalLayout.removeWidget(group_box) + group_box.deleteLater() # Properly delete the widget + self.resize(self.default_size) + + # Clear the dictionary after removing all group boxes + self.groupboxes.clear() + self.reticles.clear() + # Register in the model + self.model.reset_reticle_metadata() + + # Clear and reset the reticle_selector + self.reticle_selector.clear() + self.reticle_selector.addItem(f"Global coords") + + def update_to_file(self): + reticle_info_list = [] + names_seen = set() + duplicates = False + + # Create a list of original dictionary keys to avoid modification during iteration + original_groupbox_keys = list(self.groupboxes.keys()) + + # Iterate over the copied list of keys + for org_name in original_groupbox_keys: + group_box = self.groupboxes[org_name] + reticle_info = {} + + for line_edit in group_box.findChildren(QLineEdit): + line_edit_value = line_edit.text().strip() + + if not line_edit_value: + print(f"Error: Field {line_edit.objectName()} is empty.") + return + + # Handle reticle name changes + if "lineEditName" in line_edit.objectName(): + if line_edit_value in names_seen: + print(f"Error: Duplicate name found - {line_edit_value}") + duplicates = True + names_seen.add(line_edit_value) + + # Update self.groupboxes with the new name, if different from the original + if org_name != line_edit_value: + self.groupboxes[line_edit_value] = group_box + self.groupboxes.pop(org_name) + + # Validate numeric inputs + if line_edit.objectName() in ["lineEditRot", "lineEditOffsetX", "lineEditOffsetY", "lineEditOffsetZ"]: + if not self.is_valid_number(line_edit_value): + print(f"Error: {line_edit.objectName()} contains an invalid number.") + return + + # Store the data in reticle_info + reticle_info[line_edit.objectName()] = line_edit_value + + # Append the info for this groupbox + reticle_info_list.append(reticle_info) + + if duplicates: + print("Error: Duplicate names detected, aborting file save.") + return + + # Save the updated groupbox information to file + json_path = os.path.join(ui_dir, "reticle_metadata.json") + try: + with open(json_path, 'w') as json_file: + json.dump(reticle_info_list, json_file, indent=4) + print(f"Metadata successfully saved to {json_path}") + except Exception as e: + print(f"Error saving file: {e}") + + def is_valid_number(self, value): + try: + float(value) + return True + except ValueError: + return False + + def update_reticles(self, group_box): + #group_box = self.groupboxes.get(reticle_name) + if not group_box: + print(f"Error: No groupbox found for reticle '{group_box}'.") + return + + name = group_box.findChild(QLineEdit, "lineEditName").text() + offset_rot = group_box.findChild(QLineEdit, "lineEditRot").text() + offset_x = group_box.findChild(QLineEdit, "lineEditOffsetX").text() + offset_y = group_box.findChild(QLineEdit, "lineEditOffsetY").text() + offset_z = group_box.findChild(QLineEdit, "lineEditOffsetZ").text() + + try: + offset_rot = float(offset_rot) + offset_x = float(offset_x) + offset_y = float(offset_y) + offset_z = float(offset_z) + except ValueError: + print("Error: Invalid offset values.") + return + + rotmat = np.eye(3) + if offset_rot != 0: + rotmat = ( + Rotation.from_euler("z", offset_rot, degrees=True) + .as_matrix() + .squeeze() + ) + + self.reticles[name] = { + "rot": offset_rot, + "rotmat": rotmat, + "offset_x": offset_x, + "offset_y": offset_y, + "offset_z": offset_z + } + #Register the reticle in the model + self.model.add_reticle_metadata(name, self.reticles[name]) + + def get_global_coords_with_offset(self, reticle_name, global_pts): + if reticle_name not in self.reticles.keys(): + raise ValueError(f"Reticle '{reticle_name}' not found in reticles dictionary.") + + reticle = self.reticles[reticle_name] + reticle_rot = reticle.get("rot", 0) + reticle_rotmat = reticle.get("rotmat", np.eye(3)) # Default to identity matrix if not found + reticle_offset = np.array([ + reticle.get("offset_x", global_pts[0]), + reticle.get("offset_y", global_pts[1]), + reticle.get("offset_z", global_pts[2]) + ]) + + if reticle_rot != 0: + # Transpose because points are row vectors + global_pts = global_pts @ reticle_rotmat.T + global_pts = global_pts + reticle_offset + + global_x = np.round(global_pts[0], 1) + global_y = np.round(global_pts[1], 1) + global_z = np.round(global_pts[2], 1) + return global_x, global_y, global_z \ No newline at end of file diff --git a/parallax/stage_ui.py b/parallax/stage_ui.py index bbf7a13..a5fc3c9 100644 --- a/parallax/stage_ui.py +++ b/parallax/stage_ui.py @@ -6,24 +6,27 @@ from PyQt5.QtWidgets import QWidget from PyQt5.QtCore import pyqtSignal +import numpy as np class StageUI(QWidget): """User interface for stage control and display.""" prev_curr_stages = pyqtSignal(str, str) - def __init__(self, model, parent=None): + def __init__(self, model, ui=None): """Initialize StageUI object""" - QWidget.__init__(self, parent) + QWidget.__init__(self, ui) self.selected_stage = None self.model = model - self.ui = parent + self.ui = ui self.update_stage_selector() self.updateStageSN() self.updateStageLocalCoords() self.updateStageGlobalCoords() self.previous_stage_id = self.get_current_stage_id() + self.setCurrentReticle() + # Swtich stages self.ui.stage_selector.currentIndexChanged.connect(self.updateStageSN) self.ui.stage_selector.currentIndexChanged.connect( self.updateStageLocalCoords @@ -33,6 +36,10 @@ def __init__(self, model, parent=None): ) self.ui.stage_selector.currentIndexChanged.connect(self.sendInfoToStageWidget) + # Swtich Reticle Coordinates (e.g Reticle + No Offset, Reticle + Offset..) + self.ui.reticle_selector.currentIndexChanged.connect(self.updateCurrentReticle) + + def get_selected_stage_sn(self): """Get the serial number of the selected stage. @@ -88,24 +95,38 @@ def updateStageLocalCoords(self): self.ui.local_coords_y.setText(str(self.selected_stage.stage_y)) self.ui.local_coords_z.setText(str(self.selected_stage.stage_z)) + def updateCurrentReticle(self): + self.setCurrentReticle() + self.updateStageGlobalCoords() + + def setCurrentReticle(self): + reticle_name = self.ui.reticle_selector.currentText() + if not reticle_name: + return + # Extract the letter from reticle_name, assuming it has the format "Global coords (A)" + self.reticle = reticle_name.split('(')[-1].strip(')') + def updateStageGlobalCoords(self): """Update the displayed global coordinates of the selected stage.""" stage_id = self.get_current_stage_id() if stage_id: self.selected_stage = self.model.get_stage(stage_id) if self.selected_stage: - if self.selected_stage.stage_x_global is not None \ - and self.selected_stage.stage_y_global is not None \ - and self.selected_stage.stage_z_global is not None: - self.ui.global_coords_x.setText( - str(self.selected_stage.stage_x_global) - ) - self.ui.global_coords_y.setText( - str(self.selected_stage.stage_y_global) - ) - self.ui.global_coords_z.setText( - str(self.selected_stage.stage_z_global) - ) + x = self.selected_stage.stage_x_global + y = self.selected_stage.stage_y_global + z = self.selected_stage.stage_z_global + if x is not None and y is not None and z is not None: + # If reticle is with offset, get the global coordinates with offset + if self.reticle != "Global coords": + if self.ui.reticle_metadata is not None: + global_pts = np.array([x, y, z]) + x, y, z = self.ui.reticle_metadata.get_global_coords_with_offset(self.reticle, global_pts) + + # Update into UI + if x is not None and y is not None and z is not None: + self.ui.global_coords_x.setText(str(x)) + self.ui.global_coords_y.setText(str(y)) + self.ui.global_coords_z.setText(str(z)) else: self.updateStageGlobalCoords_default() diff --git a/parallax/stage_widget.py b/parallax/stage_widget.py index 3a12c65..0b51b22 100644 --- a/parallax/stage_widget.py +++ b/parallax/stage_widget.py @@ -20,6 +20,7 @@ from .stage_listener import StageListener from .stage_ui import StageUI from .calculator import Calculator +from .reticle_metadata import ReticleMetadata logger = logging.getLogger(__name__) logger.setLevel(logging.WARNING) @@ -98,6 +99,14 @@ def __init__(self, model, ui_dir, screen_widgets): self.calculation_button_handler ) + # Reticle Button + self.reticle_metadata_btn = self.probe_calib_widget.findChild( + QPushButton, "reticle_btn" + ) + self.reticle_metadata_btn.clicked.connect( + self.reticle_button_handler + ) + # Reticle Widget self.reticle_detection_status = ( "default" # options: default, process, detected, accepted, request_axis @@ -160,9 +169,13 @@ def __init__(self, model, ui_dir, screen_widgets): self.filter = "no_filter" logger.debug(f"filter: {self.filter}") - # Calculator + # Calculator Button self.calculation_btn.hide() - self.calculator = Calculator(self.model) + self.calculator = Calculator(self.model, self.reticle_selector) + + # Reticle Button + self.reticle_metadata_btn.hide() + self.reticle_metadata = ReticleMetadata(self.model, self.reticle_selector) def reticle_detection_button_handler(self): """ @@ -752,6 +765,7 @@ def probe_detect_default_status_ui(self, sn = None): self.hide_x_y_z() self.hide_trajectory_btn() self.hide_calculation_btn() + self.hide_reticle_metadata_btn() self.probeCalibrationLabel.setText("") self.probe_calibration_btn.setChecked(False) @@ -797,6 +811,7 @@ def probe_detect_default_status(self, sn = None): self.scale = np.array([1, 1, 1]) self.probeCalibration.reset_calib(sn = sn) self.probe_detect_default_status_ui(sn = sn) + self.reticle_metadata.default_reticle_selector() def probe_detect_process_status(self): """ @@ -864,6 +879,8 @@ def probe_detect_accepted_status(self, stage_sn, transformation_matrix, scale, s self.viewTrajectory_btn.show() if not self.calculation_btn.isVisible(): self.calculation_btn.show() + if not self.reticle_metadata_btn.isVisible(): + self.reticle_metadata_btn.show() if self.filter == "probe_detection": for screen in self.screen_widgets: camera_name = screen.get_camera_name() @@ -884,6 +901,9 @@ def probe_detect_accepted_status(self, stage_sn, transformation_matrix, scale, s stage_sn, transformation_matrix, scale ) + # Update reticle selector + self.reticle_metadata.load_metadata_from_file() + def set_default_x_y_z_style(self): self.calib_x.setStyleSheet( "color: white;" @@ -923,6 +943,10 @@ def hide_calculation_btn(self): if self.calculation_btn.isVisible(): self.calculation_btn.hide() + def hide_reticle_metadata_btn(self): + if self.reticle_metadata_btn.isVisible(): + self.reticle_metadata_btn.hide() + def calib_x_complete(self, switch_probe = False): """ Updates the UI to indicate that the calibration for the X-axis is complete. @@ -1104,4 +1128,7 @@ def view_trajectory_button_handler(self): self.probeCalibration.view_3d_trajectory(self.selected_stage_id) def calculation_button_handler(self): - self.calculator.show() \ No newline at end of file + self.calculator.show() + + def reticle_button_handler(self): + self.reticle_metadata.show() \ No newline at end of file diff --git a/ui/probe_calib.ui b/ui/probe_calib.ui index 35d0833..5ab1ce8 100644 --- a/ui/probe_calib.ui +++ b/ui/probe_calib.ui @@ -13,12 +13,18 @@ Form + + QWidget{ +background-color: rgb(00,00,00); +color: #FFFFFF; +} + 10 10 - 281 + 289 715 @@ -26,53 +32,21 @@ 1 - - - - false - + + 0 - 30 - - - - - 30 - 30 + 600 - Z - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Qt::Horizontal + - - - 40 - 20 - + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop - + @@ -96,8 +70,8 @@ - - + + 0 @@ -106,32 +80,7 @@ - 200 - 40 - - - - - 300 - 40 - - - - Probe Calibration - - - true - - - - - - - false - - - - 0 + 30 30 @@ -141,12 +90,37 @@ 30 + + <html><head/><body><p><span style=" font-size:7pt;">Trajectory</span></p></body></html> + + + +QPushButton{ + color: white; + background-color: black; +} + +QPushButton:pressed { + background-color: rgb(224, 0, 0); +} + +QPushButton:hover { + background-color: rgb(100, 30, 30); +} + - Y + + + + + resources/3D_map.pngresources/3D_map.png + + + false - + @@ -156,13 +130,13 @@ - 40 + 30 30 - 40 + 30 30 @@ -196,24 +170,56 @@ QPushButton:hover { - - + + + + false + 0 - 600 + 30 - - + + + 30 + 30 + - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + Y - - + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + 0 @@ -222,18 +228,62 @@ QPushButton:hover { - 40 + 200 + 40 + + + + + 300 + 40 + + + + Probe Calibration + + + true + + + + + + + false + + + + 0 30 - 40 + 30 + 30 + + + + Z + + + + + + + + 30 + 30 + + + + + 30 30 - <html><head/><body><p><span style=" font-size:7pt;">Trajectory</span></p></body></html> + <html><head/><body><p><span style=" font-size:7pt;">Reticle Metadata</span></p></body></html> @@ -251,14 +301,11 @@ QPushButton:hover { } - + - resources/3D_map.pngresources/3D_map.png - - - false + resources/reticle_gray.pngresources/reticle_gray.png diff --git a/ui/resources/reticle_balck.png b/ui/resources/reticle_balck.png new file mode 100644 index 0000000..47bac8a Binary files /dev/null and b/ui/resources/reticle_balck.png differ diff --git a/ui/resources/reticle_gray.png b/ui/resources/reticle_gray.png new file mode 100644 index 0000000..5cb0fc4 Binary files /dev/null and b/ui/resources/reticle_gray.png differ diff --git a/ui/resources/reticle_white.png b/ui/resources/reticle_white.png new file mode 100644 index 0000000..3766d27 Binary files /dev/null and b/ui/resources/reticle_white.png differ diff --git a/ui/reticle_QGroupBox.ui b/ui/reticle_QGroupBox.ui new file mode 100644 index 0000000..6c91580 --- /dev/null +++ b/ui/reticle_QGroupBox.ui @@ -0,0 +1,275 @@ + + + GroupBox + + + + 0 + 0 + 1109 + 150 + + + + + 700 + 150 + + + + + 16777215 + 150 + + + + GroupBox + + + QWidget{ +background-color: rgb(00,00,00); +color: #FFFFFF; +} +QPushButton{ + background-color: black; +} + QPushButton:pressed { + background-color: rgb(224, 0, 0); +} +QPushButton:hover { + background-color: rgb(100, 30, 30); +} +QPushButton#startButton:disabled:checked { + color: gray; +} +QPushButton#startButton:disabled:checked { + background-color: #ffaaaa; +} +QPushButton#startButton:disabled:!checked { + background-color: lightGreen; +} + + + + + 180 + 70 + 140 + 40 + + + + + 140 + 40 + + + + + 115 + 40 + + + + + + + 330 + 70 + 140 + 40 + + + + + 140 + 40 + + + + + 115 + 40 + + + + + + + 480 + 70 + 140 + 40 + + + + + 140 + 40 + + + + + 115 + 40 + + + + + + + 630 + 70 + 140 + 40 + + + + + 140 + 40 + + + + + + + 180 + 50 + 141 + 20 + + + + + 7 + + + + RotDegrees + + + + + + 330 + 50 + 141 + 20 + + + + + 7 + + + + OffsetX (μm) + + + + + + 30 + 70 + 140 + 40 + + + + + 140 + 40 + + + + + 115 + 40 + + + + + + + 30 + 50 + 141 + 20 + + + + + 7 + + + + ReticleName + + + + + + 480 + 50 + 141 + 20 + + + + + 7 + + + + OffsetY (μm) + + + + + + 630 + 50 + 141 + 20 + + + + + 7 + + + + OffsetZ (μm) + + + + + + 770 + 70 + 30 + 31 + + + + + 30 + 16777215 + + + + - + + + + + lineEditName + lineEditRot + lineEditOffsetX + lineEditOffsetY + lineEditOffsetZ + + + + diff --git a/ui/reticle_metadata.ui b/ui/reticle_metadata.ui new file mode 100644 index 0000000..cd3832e --- /dev/null +++ b/ui/reticle_metadata.ui @@ -0,0 +1,130 @@ + + + Form + + + + 0 + 0 + 890 + 183 + + + + + 800 + 50 + + + + Form + + + + resources/reticle_balck.pngresources/reticle_balck.png + + + QWidget{ +background-color: rgb(00,00,00); +color: #FFFFFF; +} +QPushButton{ + background-color: black; +} + QPushButton:pressed { + background-color: rgb(224, 0, 0); +} +QPushButton:hover { + background-color: rgb(100, 30, 30); +} +QPushButton#startButton:disabled:checked { + color: gray; +} +QPushButton#startButton:disabled:checked { + background-color: #ffaaaa; +} +QPushButton#startButton:disabled:!checked { + background-color: lightGreen; +} + + + + + 10 + 10 + 861 + 831 + + + + + 2 + + + + + 2 + + + 2 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 30 + 16777215 + + + + + + + + + + + + + 100 + 16777215 + + + + Update + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + diff --git a/ui/stage_info.ui b/ui/stage_info.ui index 102a3bf..abe72e3 100644 --- a/ui/stage_info.ui +++ b/ui/stage_info.ui @@ -7,7 +7,7 @@ 0 0 266 - 338 + 356 @@ -45,22 +45,30 @@ 2 - - - - - 0 - 25 - - + + - 16777215 - 25 + 150 + 5 - Z: + + + + + + + + - + + + + + + + μm @@ -83,56 +91,48 @@ - - + + + + + 0 + 25 + + - 40 - 16777215 + 16777215 + 25 - μm - - - - - - - μm + Z: - - + + - - - - + + + - 16777215 - 16777215 + 0 + 30 - - - - - - - - 16777215 - 5 + 30 - + SN @@ -155,19 +155,22 @@ - - - + + + - 25 + 0 25 - - background-color : yellow + + + 16777215 + 25 + - + Z: @@ -178,27 +181,24 @@ - - + + 0 - 25 + 40 16777215 - 25 + 40 - - Z: - - - + + QLabel { color: yellow; @@ -209,27 +209,8 @@ - - - - - 0 - 30 - - - - - 16777215 - 30 - - - - Global coords - - - - - + + 0 @@ -243,36 +224,18 @@ - X: + Y: - - - - - 0 - 30 - - + + 16777215 - 30 + 16777215 - - SN - - - - - - - QLabel { - color: yellow; -} - - @@ -297,49 +260,74 @@ - - - - - 0 - 40 - - + + 16777215 - 40 + 5 + + + - - + + - 150 - 5 + 40 + 16777215 - + μm - - + + μm - - + + + + μm + + + + + + + QLabel { + color: yellow; +} + - + + + + + 25 + 25 + + + + background-color : yellow + + + + + + @@ -359,8 +347,8 @@ - - + + 0 @@ -374,12 +362,33 @@ - Y: + X: - - + + + + + 0 + 30 + + + + + 16777215 + 30 + + + + + Global coords + + + + + + QLabel { color: yellow; @@ -390,13 +399,6 @@ - - - - μm - - -