diff --git a/parallax/__init__.py b/parallax/__init__.py
index 67bd916d..02208d94 100644
--- a/parallax/__init__.py
+++ b/parallax/__init__.py
@@ -4,7 +4,7 @@
import os
-__version__ = "0.37.20"
+__version__ = "0.37.21"
# allow multiple OpenMP instances
os.environ["KMP_DUPLICATE_LIB_OK"] = "True"
diff --git a/parallax/axis_filter.py b/parallax/axis_filter.py
index ffcbd649..b55ca276 100644
--- a/parallax/axis_filter.py
+++ b/parallax/axis_filter.py
@@ -14,7 +14,7 @@
# Set logger name
logger = logging.getLogger(__name__)
-logger.setLevel(logging.DEBUG)
+logger.setLevel(logging.WARNING)
class AxisFilter(QObject):
"""Class representing no filter."""
diff --git a/parallax/bundle_adjustment.py b/parallax/bundle_adjustment.py
index 600c142e..340c2446 100644
--- a/parallax/bundle_adjustment.py
+++ b/parallax/bundle_adjustment.py
@@ -6,7 +6,7 @@
# Set logger name
logger = logging.getLogger(__name__)
-logger.setLevel(logging.DEBUG)
+logger.setLevel(logging.WARNING)
class BALProblem:
def __init__(self, model, file_path):
@@ -184,7 +184,7 @@ def optimize(self, print_result=True):
self.opt_points = opt_params[12 * n_cams:].reshape(n_pts, 3)
if print_result:
- print(f"\n************** Optimization completed. **************************")
+ print(f"\n*********** Optimization completed **************")
# Compute initial residuals
initial_residuals = self.residuals(initial_params)
initial_residuals_sum = np.sum(initial_residuals**2)
@@ -196,7 +196,7 @@ def optimize(self, print_result=True):
opt_residuals_sum = np.sum(opt_residuals**2)
average_residual = opt_residuals_sum / len(self.bal_problem.observations)
print(f"** After BA, Average residual of reproj: {np.round(average_residual, 2)} **")
- print(f"******************************************************************")
+ print(f"****************************************************")
logger.debug(f"Optimized camera parameters: {self.opt_camera_params}")
diff --git a/parallax/coords_transformation.py b/parallax/coords_transformation.py
index 61031451..302b0440 100644
--- a/parallax/coords_transformation.py
+++ b/parallax/coords_transformation.py
@@ -64,9 +64,17 @@ def func(self, x, measured_pts, global_pts, reflect_z=False):
def avg_error(self, x, measured_pts, global_pts, reflect_z=False):
"""Calculates the total error for the optimization."""
error_values = self.func(x, measured_pts, global_pts, reflect_z)
- mean_squared_error = np.mean(error_values**2)
- average_error = np.sqrt(mean_squared_error)
- return average_error
+
+ # Calculate the L2 error for each point
+ l2_errors = np.zeros(len(global_pts))
+ for i in range(len(global_pts)):
+ error_vector = error_values[i * 3: (i + 1) * 3]
+ l2_errors[i] = np.linalg.norm(error_vector)
+
+ # Calculate the average L2 error
+ average_l2_error = np.mean(l2_errors)
+
+ return average_l2_error
def fit_params(self, measured_pts, global_pts):
"""Fits parameters to minimize the error defined in func"""
diff --git a/parallax/main_window_wip.py b/parallax/main_window_wip.py
index 6f1d9d53..a5016e0e 100644
--- a/parallax/main_window_wip.py
+++ b/parallax/main_window_wip.py
@@ -772,3 +772,7 @@ def save_user_configs(self):
width = self.width()
height = self.height()
self.user_setting.save_user_configs(nColumn, directory, width, height)
+
+ def closeEvent(self, event):
+ self.model.close_all_point_meshes()
+ event.accept()
\ No newline at end of file
diff --git a/parallax/model.py b/parallax/model.py
index 9d9fdd49..9c6deaf8 100755
--- a/parallax/model.py
+++ b/parallax/model.py
@@ -3,7 +3,6 @@
"""
from PyQt5.QtCore import QObject, pyqtSignal
-
from .camera import MockCamera, PySpinCamera, close_cameras, list_cameras
from .stage_listener import Stage, StageInfo
@@ -26,6 +25,9 @@ def __init__(self, version="V1", bundle_adjustment=False):
self.nMockCameras = 0
self.focos = []
+ # point mesh
+ self.point_mesh_instances = {}
+
# stage
self.nStages = 0
self.stages = {}
@@ -199,3 +201,14 @@ def save_all_camera_frames(self):
filename = 'camera%d_%s.png' % (i, camera.get_last_capture_time())
camera.save_last_image(filename)
self.msg_log.post("Saved camera frame: %s" % filename)
+
+ def add_point_mesh_instance(self, instance):
+ sn = instance.sn
+ if sn in self.point_mesh_instances.keys():
+ self.point_mesh_instances[sn].close()
+ self.point_mesh_instances[sn] = instance
+
+ def close_all_point_meshes(self):
+ for instance in self.point_mesh_instances.values():
+ instance.close()
+ self.point_mesh_instances.clear()
\ No newline at end of file
diff --git a/parallax/point_mesh.py b/parallax/point_mesh.py
new file mode 100644
index 00000000..ecf0f751
--- /dev/null
+++ b/parallax/point_mesh.py
@@ -0,0 +1,184 @@
+import os
+import pandas as pd
+import plotly.graph_objs as go
+from PyQt5.QtWidgets import QWidget, QPushButton
+from PyQt5.uic import loadUi
+from PyQt5.QtCore import Qt
+from PyQt5.QtWebEngineWidgets import QWebEngineView
+
+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")
+csv_file = os.path.join(debug_dir, "points.csv")
+
+class PointMesh(QWidget):
+ def __init__(self, model, file_path, sn, transM, scale, transM_BA=None, scale_BA=None, calib_completed=False):
+ super().__init__()
+ self.model = model
+ self.file_path = file_path
+ self.sn = sn
+ self.calib_completed = calib_completed
+ self.web_view = None
+
+ self.R, self.R_BA = {}, {}
+ self.T, self.T_BA = {}, {}
+ self.S, self.S_BA = {}, {}
+ self.points_dict = {}
+ self.traces = {} # Plotly trace objects
+ self.colors = {}
+ self.resizeEvent = self._on_resize
+
+ # Register this instance with the model
+ self.model.add_point_mesh_instance(self)
+
+ self.ui = loadUi(os.path.join(ui_dir, "point_mesh.ui"), self)
+ self.setWindowTitle(f"{self.sn} - Trajectory 3D View ")
+ self.setWindowFlags(Qt.Window | Qt.WindowMinimizeButtonHint | \
+ Qt.WindowMaximizeButtonHint | Qt.WindowCloseButtonHint)
+
+ self._set_transM(transM, scale)
+ if transM_BA is not None and scale_BA is not None and \
+ self.model.bundle_adjustment and self.calib_completed:
+ self.set_transM_BA(transM_BA, scale_BA)
+ self._parse_csv()
+ self._init_buttons()
+
+ def show(self):
+ self._init_ui()
+ self._update_canvas()
+ super().show() # Show the widget
+
+ def _init_ui(self):
+ if self.web_view is not None:
+ self.web_view.close()
+ self.web_view = QWebEngineView(self)
+ self.ui.verticalLayout1.addWidget(self.web_view)
+
+ def _set_transM(self, transM, scale):
+ self.R[self.sn] = transM[:3, :3]
+ self.T[self.sn] = transM[:3, 3]
+ self.S[self.sn] = scale[:3]
+
+ def set_transM_BA(self, transM, scale):
+ self.R_BA[self.sn] = transM[:3, :3]
+ self.T_BA[self.sn] = transM[:3, 3]
+ self.S_BA[self.sn] = scale[:3]
+
+ def _parse_csv(self):
+ self.df = pd.read_csv(self.file_path)
+ self.df = self.df[self.df["sn"] == self.sn] # filter by sn
+
+ self.local_pts_org = self.df[['local_x', 'local_y', 'local_z']].values
+ self.local_pts = self._local_to_global(self.local_pts_org, self.R[self.sn], self.T[self.sn], self.S[self.sn])
+ self.points_dict['local_pts'] = self.local_pts
+
+ self.global_pts = self.df[['global_x', 'global_y', 'global_z']].values
+ self.points_dict['global_pts'] = self.global_pts
+
+ if self.model.bundle_adjustment and self.calib_completed:
+ self.m_global_pts = self.df[['m_global_x', 'm_global_y', 'm_global_z']].values
+ self.points_dict['m_global_pts'] = self.m_global_pts
+
+ self.opt_global_pts = self.df[['opt_x', 'opt_y', 'opt_z']].values
+ self.points_dict['opt_global_pts'] = self.opt_global_pts
+
+ self.local_pts_BA = self._local_to_global(self.local_pts_org, self.R_BA[self.sn], self.T_BA[self.sn], self.S_BA[self.sn])
+ self.points_dict['local_pts_BA'] = self.local_pts_BA
+
+ # Assign unique colors to each key
+ color_list = ['red', 'blue', 'green', 'cyan', 'magenta']
+ for i, key in enumerate(self.points_dict.keys()):
+ self.colors[key] = color_list[i % len(color_list)]
+
+ def _local_to_global(self, local_pts, R, t, scale=None):
+ if scale is not None:
+ local_pts = local_pts * scale
+ global_coords_exp = R @ local_pts.T + t.reshape(-1, 1)
+ return global_coords_exp.T
+
+ def _init_buttons(self):
+ self.buttons = {}
+
+ for key in self.points_dict.keys():
+ button_name = self._get_button_name(key)
+ button = QPushButton(f'{button_name}')
+ button.setCheckable(True)
+ button.setMaximumWidth(200)
+ button.clicked.connect(lambda checked, key=key: self._update_plot(key, checked))
+ self.ui.verticalLayout2.addWidget(button)
+ self.buttons[key] = button
+
+ if self.model.bundle_adjustment and self.calib_completed:
+ keys_to_check = ['local_pts_BA', 'opt_global_pts']
+ else:
+ keys_to_check = ['local_pts', 'global_pts']
+
+ for key in keys_to_check:
+ self.buttons[key].setChecked(True)
+ self._draw_specific_points(key)
+
+ def _get_button_name(self, key):
+ if key == 'local_pts':
+ return 'stage'
+ elif key == 'local_pts_BA':
+ return 'stage (BA)'
+ elif key == 'global_pts':
+ return 'global'
+ elif key == 'm_global_pts':
+ return 'global (mean)'
+ elif key == 'opt_global_pts':
+ return 'global (BA)'
+ else:
+ return key # Default to the key if no match
+
+ def _update_plot(self, key, checked):
+ if checked:
+ self._draw_specific_points(key)
+ else:
+ self._remove_points_from_plot(key)
+ self._update_canvas()
+
+ def _remove_points_from_plot(self, key):
+ if key in self.points_dict:
+ del self.traces[key] # Remove from self.traces
+ self._update_canvas()
+
+ def _draw_specific_points(self, key):
+ pts = self.points_dict[key]
+ x_rounded = [round(x, 0) for x in pts[:, 0]]
+ y_rounded = [round(y, 0) for y in pts[:, 1]]
+ z_rounded = [round(z, 0) for z in pts[:, 2]]
+
+ scatter = go.Scatter3d(
+ x=x_rounded, y=y_rounded, z=z_rounded,
+ mode='markers+lines',
+ marker=dict(size=2, color=self.colors[key]),
+ name=self._get_button_name(key),
+ hoverinfo='x+y+z'
+ )
+ self.traces[key] = scatter # Store the trace in self.traces
+
+ def _update_canvas(self):
+ data = list(self.traces.values())
+ layout = go.Layout(
+ scene=dict(
+ xaxis_title='X',
+ yaxis_title='Y',
+ zaxis_title='Z'
+ ),
+ margin=dict(l=0, r=0, b=0, t=0)
+ )
+ fig = go.Figure(data=data, layout=layout)
+ html_content = fig.to_html(include_plotlyjs='cdn')
+ self.web_view.setHtml(html_content)
+
+ def _on_resize(self, event):
+ new_size = event.size()
+ self.web_view.resize(new_size.width(), new_size.height())
+ self._update_canvas()
+
+ # Resize horizontal layout
+ self.ui.horizontalLayoutWidget.resize(new_size.width(), new_size.height())
+
+
+
diff --git a/parallax/probe_calibration.py b/parallax/probe_calibration.py
index 002730c3..d6b0cb5f 100644
--- a/parallax/probe_calibration.py
+++ b/parallax/probe_calibration.py
@@ -13,10 +13,11 @@
from PyQt5.QtCore import QObject, pyqtSignal
from .coords_transformation import RotationTransformation
from .bundle_adjustment import BALProblem, BALOptimizer
+from .point_mesh import PointMesh
# Set logger name
logger = logging.getLogger(__name__)
-logger.setLevel(logging.DEBUG)
+logger.setLevel(logging.WARNING)
# Set the logging level for PyQt5.uic.uiparser/properties to WARNING, to ignore DEBUG messages
logging.getLogger("PyQt5.uic.uiparser").setLevel(logging.WARNING)
logging.getLogger("PyQt5.uic.properties").setLevel(logging.WARNING)
@@ -38,7 +39,6 @@ class ProbeCalibration(QObject):
calib_complete_x = pyqtSignal(str)
calib_complete_y = pyqtSignal(str)
calib_complete_z = pyqtSignal(str)
- #calib_complete = pyqtSignal(str, object)
calib_complete = pyqtSignal(str, object, np.ndarray)
transM_info = pyqtSignal(str, object, np.ndarray, float, object)
@@ -54,14 +54,15 @@ def __init__(self, model, stage_listener):
self.stage_listener = stage_listener
self.stage_listener.probeCalibRequest.connect(self.update)
self.stages = {}
+ self.point_mesh = {}
self.df = None
self.inliers = []
self.stage = None
-
"""
self.threshold_min_max = 250
- self.threshold_min_max_z = 200
+ self.threshold_min_max_z = 0
self.LR_err_L2_threshold = 200
+ self.threshold_avg_error = 500
self.threshold_matrix = np.array(
[
[0.002, 0.002, 0.002, 0.0],
@@ -105,7 +106,8 @@ def reset_calib(self, sn=None):
'max_z': float("-inf"),
'signal_emitted_x': False,
'signal_emitted_y': False,
- 'signal_emitted_z': False
+ 'signal_emitted_z': False,
+ 'calib_completed': False
}
else:
self.stages = {}
@@ -265,7 +267,8 @@ def _get_transM(self, df, remove_noise=True, save_to_csv=False, file_name=None):
local_points, global_points = self._get_local_global_points(df)
if remove_noise:
- if self._is_criteria_met_points_min_max() and len(local_points) > 10 \
+ #if self._is_criteria_met_points_min_max() and len(local_points) > 10 \
+ if self._is_criteria_met_points_min_max() \
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)
@@ -362,7 +365,9 @@ def _update_min_max_x_y_z(self):
self.stages[sn] = {
'min_x': float("inf"), 'max_x': float("-inf"),
'min_y': float("inf"), 'max_y': float("-inf"),
- 'min_z': float("inf"), 'max_z': float("-inf")
+ 'min_z': float("inf"), 'max_z': float("-inf"),
+ 'signal_emitted_x': False, 'signal_emitted_y': False,
+ 'signal_emitted_z': False, 'calib_completed': False
}
self.stages[sn]['min_x'] = min(self.stages[sn]['min_x'], self.stage.stage_x)
@@ -582,7 +587,6 @@ def _print_formatted_transM(self):
print(f" [{S[0]:.5f}, {S[1]:.5f}, {S[2]:.5f}]")
print("==> Average L2 between stage and global: ", self.avg_err)
-
def update(self, stage, debug_info=None):
"""
Main method to update calibration with a new stage position and check if calibration is complete.
@@ -593,11 +597,6 @@ def update(self, stage, debug_info=None):
# update points in the file
self.stage = stage
self._update_local_global_point(debug_info) # Do no update if it is duplicates
-
- # TODO
- # get whole list of local and global points in pd format
- #local_points, global_points = self._get_local_global_points()
- #self.transM_LR = self._get_transM_LR_orthogonal(local_points, global_points) #remove outliers
filtered_df = self._filter_df_by_sn(self.stage.sn)
self.transM_LR = self._get_transM(filtered_df)
@@ -610,35 +609,57 @@ def update(self, stage, debug_info=None):
self._update_info_ui() # update transformation matrix and overall LR in UI
ret = self._is_enough_points() # if ret, send the signal
if ret:
- # save the filtered points to a new file
- self.file_name = f"points_{self.stage.sn}.csv"
- self._get_transM(filtered_df, save_to_csv=True, file_name=self.file_name)
-
- # TODO - Bundle Adjustment
- print("\n\n=========================================================")
- print("Before BA")
- 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")
-
- if self.model.bundle_adjustment:
- ret = self.run_bundle_adjustment(self.file_name)
- if ret:
- print("\n=========================================================")
- print("After BA")
- self._print_formatted_transM()
- print("=========================================================")
- self._update_info_ui(disp_avg_error=True, save_to_csv=True, \
- file_name = f"transM_BA_{self.stage.sn}.csv")
- else:
- return
-
- # Emit the signal to indicate that calibration is complete
- self.calib_complete.emit(self.stage.sn, self.transM_LR, self.scale)
- logger.debug(
- f"complete probe calibration {self.stage.sn}, {self.transM_LR}, {self.scale}"
- )
+ self.complete_calibration(filtered_df)
+
+ def complete_calibration(self, filtered_df):
+ # save the filtered points to a new file
+ self.file_name = f"points_{self.stage.sn}.csv"
+ self._get_transM(filtered_df, save_to_csv=True, file_name=self.file_name)
+
+ print("\n\n=========================================================")
+ 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")
+
+ if self.model.bundle_adjustment:
+ self.old_transM, self.old_scale = self.transM_LR, self.scale
+ ret = self.run_bundle_adjustment(self.file_name)
+ if ret:
+ print("\n=========================================================")
+ print("** After Bundle Adjustment **")
+ self._print_formatted_transM()
+ print("=========================================================")
+ self._update_info_ui(disp_avg_error=True, save_to_csv=True, \
+ file_name = f"transM_BA_{self.stage.sn}.csv")
+ else:
+ return
+
+ # Emit the signal to indicate that calibration is complete
+ self.calib_complete.emit(self.stage.sn, self.transM_LR, self.scale)
+ logger.debug(
+ f"complete probe calibration {self.stage.sn}, {self.transM_LR}, {self.scale}"
+ )
+
+ # Init PointMesh
+ if not self.model.bundle_adjustment:
+ self.point_mesh[self.stage.sn] = PointMesh(self.model, self.file_name, self.stage.sn, \
+ self.transM_LR, self.scale, calib_completed=True)
+ else:
+ self.point_mesh[self.stage.sn] = PointMesh(self.model, self.file_name, self.stage.sn, \
+ self.old_transM, self.old_scale, \
+ self.transM_LR, self.scale, calib_completed=True)
+ self.stages[self.stage.sn]['calib_completed'] = True
+
+ def view_3d_trajectory(self, sn):
+ if not self.stages.get(sn, {}).get('calib_completed', False):
+ if sn == self.stage.sn:
+ self.point_mesh_not_calibrated = PointMesh(self.model, self.csv_file, self.stage.sn, \
+ self.transM_LR, self.scale)
+ self.point_mesh_not_calibrated.show()
+ else:
+ # If calib is completed, show the PointMesh instance.
+ self.point_mesh[sn].show()
def run_bundle_adjustment(self, file_path):
bal_problem = BALProblem(self.model, file_path)
@@ -651,19 +672,6 @@ def run_bundle_adjustment(self, file_path):
if self.transM_LR is None:
return False
- """
- # Save local_pts and optimzied global pts in file_path
- # Save local_pts and optimized global pts in file_path
- df = pd.DataFrame({
- 'local_x': local_pts[:, 0],
- 'local_y': local_pts[:, 1],
- 'local_z': local_pts[:, 2],
- 'global_x': opt_global_pts[:, 0],
- 'global_y': opt_global_pts[:, 1],
- 'opt_global_z': opt_global_pts[:, 2]
- })
- df.to_csv(file_path, index=False)"""
-
logger.debug(f"Number of observations: {len(bal_problem.observations)}")
logger.debug(f"Number of 3d points: {len(bal_problem.points)}")
for i in range(len(bal_problem.list_cameras)):
diff --git a/parallax/probe_detect_manager.py b/parallax/probe_detect_manager.py
index 5e830367..003282c3 100644
--- a/parallax/probe_detect_manager.py
+++ b/parallax/probe_detect_manager.py
@@ -43,7 +43,7 @@ def __init__(self, name, model):
"""Initialize Worker object"""
QObject.__init__(self)
self.model = model
- self.name = name
+ self.name = name # Camera serial number
self.running = False
self.is_detection_on = False
self.is_calib = False
@@ -138,9 +138,8 @@ def process(self, frame, timestamp):
self.currBgCmpProcess.update_reticle_zone(self.reticle_zone)
if self.prev_img is not None:
- if (
- self.probeDetect.angle is None
- ): # Detecting probe for the first time
+ if self.probeDetect.angle is None:
+ # Detecting probe for the first time
ret = self.currPrevCmpProcess.first_cmp(
self.curr_img, self.prev_img, mask, gray_img
)
@@ -163,6 +162,7 @@ def process(self, frame, timestamp):
ret = self.currBgCmpProcess.update_cmp(
self.curr_img, mask, gray_img
)
+ logger.debug(f"cam:{self.name}, ret: {ret}, stopped: {self.is_calib}")
if ret: # Found
if self.is_calib: # If calibaration is enable, use data for calibration
self.found_coords.emit(
@@ -181,7 +181,7 @@ def process(self, frame, timestamp):
(255, 0, 0),
-1,
)
- else: # Otherwise, just draw a tip on the frame
+ else: # Otherwise, just draw a tip on the frame (yellow)
cv2.circle(
frame,
self.probeDetect.probe_tip_org,
@@ -190,9 +190,10 @@ def process(self, frame, timestamp):
-1,
)
self.prev_img = self.curr_img
- logger.debug(f"{self.name} Found")
- else:
- logger.debug(f"{self.name} Not found")
+ #logger.debug(f"{self.name} Found")
+ else:
+ #logger.debug(f"{self.name} Not found")
+ pass
else:
self.prev_img = self.curr_img
diff --git a/parallax/screen_widget.py b/parallax/screen_widget.py
index 5077bc48..82f224ba 100755
--- a/parallax/screen_widget.py
+++ b/parallax/screen_widget.py
@@ -19,7 +19,7 @@
# Set logger name
logger = logging.getLogger(__name__)
-logger.setLevel(logging.DEBUG)
+logger.setLevel(logging.WARNING)
# Set the logging level for PyQt5.uic.uiparser/properties to WARNING, to ignore DEBUG messages
logging.getLogger("PyQt5.uic.uiparser").setLevel(logging.WARNING)
logging.getLogger("PyQt5.uic.properties").setLevel(logging.WARNING)
diff --git a/parallax/stage_widget.py b/parallax/stage_widget.py
index 3ffa4161..8a4b574a 100644
--- a/parallax/stage_widget.py
+++ b/parallax/stage_widget.py
@@ -9,8 +9,6 @@
import logging
import os
import math
-import time
-
import numpy as np
from PyQt5.QtCore import QTimer, Qt
from PyQt5.QtWidgets import (QLabel, QMessageBox, QPushButton, QSizePolicy,
@@ -23,7 +21,7 @@
from .stage_ui import StageUI
logger = logging.getLogger(__name__)
-logger.setLevel(logging.DEBUG)
+logger.setLevel(logging.WARNING)
class StageWidget(QWidget):
"""Widget for stage control and calibration."""
@@ -84,6 +82,12 @@ def __init__(self, model, ui_dir, screen_widgets):
QLabel, "probeCalibrationLabel"
)
self.probeCalibrationLabel.setTextInteractionFlags(Qt.TextSelectableByMouse)
+ self.viewTrajectory_btn = self.probe_calib_widget.findChild(
+ QPushButton, "viewTrajectory_btn"
+ )
+ self.viewTrajectory_btn.clicked.connect(
+ self.view_trajectory_button_handler
+ )
# Reticle Widget
self.reticle_detection_status = (
@@ -127,6 +131,7 @@ def __init__(self, model, ui_dir, screen_widgets):
self.calib_x.hide()
self.calib_y.hide()
self.calib_z.hide()
+ self.viewTrajectory_btn.hide()
self.probeCalibration.calib_complete_x.connect(self.calib_x_complete)
self.probeCalibration.calib_complete_y.connect(self.calib_y_complete)
self.probeCalibration.calib_complete_z.connect(self.calib_z_complete)
@@ -737,6 +742,7 @@ def probe_detect_default_status_ui(self, sn = None):
}
""")
self.hide_x_y_z()
+ self.hide_trajectory_btn()
self.probeCalibrationLabel.setText("")
self.probe_calibration_btn.setChecked(False)
@@ -837,6 +843,8 @@ def probe_detect_accepted_status(self, stage_sn, transformation_matrix, scale, s
self.probe_calibration_btn.setChecked(True)
self.hide_x_y_z()
+ if not self.viewTrajectory_btn.isVisible():
+ self.viewTrajectory_btn.show()
if self.filter == "probe_detection":
for screen in self.screen_widgets:
camera_name = screen.get_camera_name()
@@ -888,6 +896,10 @@ def hide_x_y_z(self):
self.set_default_x_y_z_style()
+ def hide_trajectory_btn(self):
+ if self.viewTrajectory_btn.isVisible():
+ self.viewTrajectory_btn.hide()
+
def calib_x_complete(self, switch_probe = False):
"""
Updates the UI to indicate that the calibration for the X-axis is complete.
@@ -990,7 +1002,8 @@ def update_probe_calib_status(self, moving_stage_id, transM, scale, L2_err, dist
if self.moving_stage_id == self.selected_stage_id:
# If moving stage is the selected stage, update the probe calibration status on UI
self.display_probe_calib_status(transM, scale, L2_err, dist_traveled)
- #self.display_probe_calib_status(transM, L2_err, dist_traveled)
+ if not self.viewTrajectory_btn.isVisible():
+ self.viewTrajectory_btn.show()
else:
# If moving stage is not the selected stage, save the calibration info
content = (
@@ -1062,4 +1075,7 @@ def update_stages(self, prev_stage_id, curr_stage_id):
if self.transM is not None:
self.display_probe_calib_status(self.transM, self.scale, self.L2_err, self.dist_travled)
- self.probe_detection_status = probe_detection_status
\ No newline at end of file
+ self.probe_detection_status = probe_detection_status
+
+ def view_trajectory_button_handler(self):
+ self.probeCalibration.view_3d_trajectory(self.selected_stage_id)
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
index 9881a3e8..a5186ca4 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -26,7 +26,9 @@ dependencies = [
"pandas",
"scikit-image",
"requests",
- "scikit-learn"
+ "scikit-learn",
+ "plotly",
+ "PyQtWebEngine"
]
[tool.setuptools]
diff --git a/ui/point_mesh.ui b/ui/point_mesh.ui
new file mode 100644
index 00000000..42990649
--- /dev/null
+++ b/ui/point_mesh.ui
@@ -0,0 +1,112 @@
+
+
+ plot_widget
+
+
+
+ 0
+ 0
+ 1400
+ 850
+
+
+
+
+ 400
+ 300
+
+
+
+
+ 7680
+ 4320
+
+
+
+ Form
+
+
+ QWidget{
+ background-color: rgb(0,0,0);
+ color: #FFFFFF;
+}
+
+QPushButton{
+ background-color: black;
+}
+
+QPushButton:pressed {
+ background-color: rgb(224, 0, 0);
+}
+
+QPushButton:hover {
+ background-color: rgb(100, 30, 30);
+}
+
+QPushButton:checked {
+ color: gray;
+ background-color: #ffaaaa;
+}
+
+
+
+
+ 10
+ 10
+ 1661
+ 1001
+
+
+
+
+ 0
+
+
+ 0
+
+
+ 20
+
+
+ 20
+
+ -
+
+
-
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+ -
+
+
-
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/probe_calib.ui b/ui/probe_calib.ui
index 09c211a3..53b875f9 100644
--- a/ui/probe_calib.ui
+++ b/ui/probe_calib.ui
@@ -18,29 +18,38 @@
10
10
- 286
- 705
+ 281
+ 715
1
- -
-
-
- Qt::Horizontal
+
-
+
+
+ false
-
+
- 40
- 20
+ 0
+ 30
-
+
+
+ 30
+ 30
+
+
+
+ Y
+
+
- -
-
+
-
+
Qt::Horizontal
@@ -52,35 +61,29 @@
- -
-
-
-
- 0
- 0
-
+
-
+
+
+ false
- 200
- 40
+ 0
+ 30
- 300
- 40
+ 30
+ 30
- Probe Calibration
-
-
- true
+ Z
- -
+
-
false
@@ -102,63 +105,103 @@
- -
-
-
+
-
+
+
+ Qt::Horizontal
+
+
- 0
- 600
+ 40
+ 20
-
-
-
-
- Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop
-
-
+
- -
-
-
- false
+
-
+
+
+
+ 0
+ 0
+
- 0
+ 40
30
- 30
+ 40
30
+
+
+QPushButton{
+ color: white;
+ background-color: black;
+}
+
+QPushButton:pressed {
+ background-color: rgb(224, 0, 0);
+}
+
+QPushButton:hover {
+ background-color: rgb(100, 30, 30);
+}
+
- Y
+ 3D
-
-
- -
-
-
+
false
+
+
+ -
+
0
- 30
+ 600
+
+
+
+
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 200
+ 40
- 30
- 30
+ 300
+ 40
- Z
+ Probe Calibration
+
+
+ true