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
-
-
-