Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wip/polish code #86

Merged
merged 11 commits into from
Sep 25, 2024
Merged
27 changes: 27 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ on:
- main

jobs:
# Job 1: Linters for Pull Requests
linters:
runs-on: windows-latest
strategy:
Expand All @@ -27,3 +28,29 @@ jobs:
run: flake8 parallax tests
continue-on-error: true

# Job 2: Build Documentation for Pushes to Main
build-docs:
runs-on: ubuntu-latest
steps:
# Step 1: Checkout the repository
- name: Checkout Repository
uses: actions/checkout@v3

# Step 2: Set up Python
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.8'

# Step 3: Install dependencies (including Sphinx)
- name: Install Dependencies
run: |
python -m pip install --upgrade pip
pip install -r docs/requirements.txt

# Step 4: Build the documentation
- name: Build Documentation
run: |
sphinx-build -b html docs/source docs/_build
# Fail the workflow if documentation build fails
continue-on-error: false
2 changes: 1 addition & 1 deletion parallax/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import os

__version__ = "0.37.27"
__version__ = "0.37.29"

# allow multiple OpenMP instances
os.environ["KMP_DUPLICATE_LIB_OK"] = "True"
51 changes: 45 additions & 6 deletions parallax/calculator.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def __init__(self, model, reticle_selector, stage_controller):
self.reticle = None
self.stage_controller = stage_controller

self.ui = loadUi(os.path.join(ui_dir, "calc_move.ui"), self)
self.ui = loadUi(os.path.join(ui_dir, "calc.ui"), self)
self.setWindowTitle(f"Calculator")
self.setWindowFlags(Qt.Window | Qt.WindowMinimizeButtonHint | \
Qt.WindowMaximizeButtonHint | Qt.WindowCloseButtonHint)
Expand Down Expand Up @@ -236,7 +236,7 @@ def _disable(self, sn):
group_box = self.findChild(QGroupBox, f"groupBox_{sn}")
group_box.setEnabled(False)
group_box.setStyleSheet("background-color: #333333;")
group_box.setTitle(f"{sn} (Uncalibrated)")
group_box.setTitle(f"(Uncalibrated) {sn}")

def _enable(self, sn):
# Find the QGroupBox for the stage
Expand All @@ -251,11 +251,11 @@ def _create_stage_groupboxes(self):
for sn in self.model.stages.keys():
# Load the QGroupBox from the calc_QGroupBox.ui file
group_box = QGroupBox(self)
#loadUi(os.path.join(ui_dir, "calc_QGroupBox.ui"), group_box) # TODO
loadUi(os.path.join(ui_dir, "calc_QGroupBox_move.ui"), group_box)
loadUi(os.path.join(ui_dir, "calc_QGroupBox.ui"), group_box)

# Set the visible title of the QGroupBox to sn
group_box.setTitle(f"{sn}")
group_box.setAlignment(Qt.AlignRight) # title alignment to the right

# Append _{sn} to the QGroupBox object name
group_box.setObjectName(f"groupBox_{sn}")
Expand All @@ -274,7 +274,6 @@ def _create_stage_groupboxes(self):
# Add the newly created QGroupBox to the layout
widget_count = self.ui.verticalLayout_QBox.count()
self.ui.verticalLayout_QBox.insertWidget(widget_count - 1, group_box)
#self.ui.verticalLayout_QBox.addWidget(group_box)

def _connect_move_stage_buttons(self):
stop_button = self.ui.findChild(QPushButton, f"stopAllStages")
Expand Down Expand Up @@ -302,11 +301,16 @@ def _move_stage(self, stage_sn, move_type):
# Convert the text to float, round it, then cast to int
x = float(self.findChild(QLineEdit, f"localX_{stage_sn}").text())/1000
y = float(self.findChild(QLineEdit, f"localY_{stage_sn}").text())/1000
z = 15.0
z = 15.0 # Z is inverted in the server.
except ValueError as e:
logger.warning(f"Invalid input for stage {stage_sn}: {e}")
return # Optionally handle the error gracefully (e.g., show a message to the user)

# Safety Check: Check z=15 is high position of stage.
if not self._is_z_safe_pos(stage_sn, x, y, z):
logger.warning(f"Invalid z position for stage {stage_sn}")
return

# Use the confirm_move_stage function to ask for confirmation
if self._confirm_move_stage(x, y):
# If the user confirms, proceed with moving the stage
Expand All @@ -323,6 +327,41 @@ def _move_stage(self, stage_sn, move_type):
# If the user cancels, do nothing
print("Stage move canceled by user.")

def _is_z_safe_pos(self, stage_sn, x, y, z):
"""
Check if the Z=15 position is safe for the stage. (z=15 is the top of the stage)

Args:
stage_sn (str): The serial number of the stage.
x (float): The x-coordinate of the stage.
y (float): The y-coordinate of the stage.
z (float): The z-coordinate (set to 15.0).

Returns:
bool: True if the Z position is safe, False otherwise.
"""
# Z is inverted in the server
local_pts_z15 = [float(x)*1000, float(y)*1000, float(15.0 - z)*1000] # Should be top of the stage
local_pts_z0 = [float(x)*1000, float(y)*1000, 15.0*1000] # Should be bottom
for sn, item in self.model.transforms.items():
if sn != stage_sn:
continue

transM, scale = item[0], item[1]
if transM is not None:
try:
# Apply transformations to get global points for Z=15 and Z=0
global_pts_z15 = self._apply_transformation(local_pts_z15, transM, scale)
global_pts_z0 = self._apply_transformation(local_pts_z0, transM, scale)

# Ensure that Z=15 is higher than Z=0 and Z=15 is positive
if global_pts_z15[2] > global_pts_z0[2] and global_pts_z15[2] > 0:
return True
except Exception as e:
logger.error(f"Error applying transformation for stage {stage_sn}: {e}")
return False
return False

def _confirm_move_stage(self, x, y):
"""
Displays a confirmation dialog asking the user if they are sure about moving the stage.
Expand Down
110 changes: 61 additions & 49 deletions parallax/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@
import PySpin
except ImportError:
PySpin = None
logger.warn("Could not import PySpin.")

logger.warning("Could not import PySpin.")

def list_cameras(dummy=False, version="V1"):
"""
Expand Down Expand Up @@ -254,22 +253,23 @@ def set_wb(self, channel, wb=1.2):
Args:
- wb (float): The desired white balance value. min:1.8, max:2.5
"""
if self.device_color_type == "Color":
self.node_wbauto_mode.SetIntValue(
self.node_wbauto_mode_off.GetValue()
)
if channel == "Red":
self.node_balanceratio_mode.SetIntValue(
self.node_balanceratio_mode_red.GetValue()
)
self.node_wb.SetValue(wb)
elif channel == "Blue":
self.node_balanceratio_mode.SetIntValue(
self.node_balanceratio_mode_blue.GetValue()
try:
if self.device_color_type == "Color":
self.node_wbauto_mode.SetIntValue(
self.node_wbauto_mode_off.GetValue()
)
self.node_wb.SetValue(wb)
else:
pass
if channel == "Red":
self.node_balanceratio_mode.SetIntValue(
self.node_balanceratio_mode_red.GetValue()
)
self.node_wb.SetValue(wb)
elif channel == "Blue":
self.node_balanceratio_mode.SetIntValue(
self.node_balanceratio_mode_blue.GetValue()
)
self.node_wb.SetValue(wb)
except Exception as e:
logger.error(f"An error occurred while setting the white balance: {e}")

def get_wb(self, channel):
"""
Expand Down Expand Up @@ -302,8 +302,11 @@ def set_gamma(self, gamma=1.0):
Args:
- gamma (float): The desired gamma value. min:0.25 max:1.25
"""
self.node_gammaenable_mode.SetValue(True)
self.node_gamma.SetValue(gamma)
try:
self.node_gammaenable_mode.SetValue(True)
self.node_gamma.SetValue(gamma)
except Exception as e:
logger.error(f"An error occurred while setting the gamma: {e}")

def disable_gamma(self):
"""
Expand All @@ -318,10 +321,13 @@ def set_gain(self, gain=20.0):
Args:
- gain (float): The desired gain value. min:0, max:27.0
"""
self.node_gainauto_mode.SetIntValue(
self.node_gainauto_mode_off.GetValue()
)
self.node_gain.SetValue(gain)
try:
self.node_gainauto_mode.SetIntValue(
self.node_gainauto_mode_off.GetValue()
)
self.node_gain.SetValue(gain)
except Exception as e:
logger.error(f"An error occurred while setting the gain: {e}")

def get_gain(self):
"""
Expand All @@ -346,10 +352,13 @@ def set_exposure(self, expTime=16000):
Args:
- expTime (int): The desired exposure time in microseconds.
"""
self.node_expauto_mode.SetIntValue(
self.node_expauto_mode_off.GetValue()
) # Return back to manual mode
self.node_exptime.SetValue(expTime)
try:
self.node_expauto_mode.SetIntValue(
self.node_expauto_mode_off.GetValue()
) # Return back to manual mode
self.node_exptime.SetValue(expTime)
except Exception as e:
logger.error(f"An error occurred while setting the exposure: {e}")

def get_exposure(self):
"""
Expand Down Expand Up @@ -434,26 +443,30 @@ def begin_continuous_acquisition(self):
print("Error: camera is already running")
return -1

# set acquisition mode continuous (continuous stream of images)
node_acquisition_mode = PySpin.CEnumerationPtr(
self.node_map.GetNode("AcquisitionMode")
)
node_acquisition_mode_continuous = node_acquisition_mode.GetEntryByName(
"Continuous"
)
acquisition_mode_continuous = (
node_acquisition_mode_continuous.GetValue()
)
node_acquisition_mode.SetIntValue(acquisition_mode_continuous)

# Begin Acquisition: Image acquisition must be ended when no more images are needed.
self.camera.BeginAcquisition()
logger.debug(f"BeginAcquisition {self.name(sn_only=True)} ")
self.running = True
self.capture_thread = threading.Thread(
target=self.capture_loop, daemon=True
)
self.capture_thread.start()
try:
# set acquisition mode continuous (continuous stream of images)
node_acquisition_mode = PySpin.CEnumerationPtr(
self.node_map.GetNode("AcquisitionMode")
)
node_acquisition_mode_continuous = node_acquisition_mode.GetEntryByName(
"Continuous"
)
acquisition_mode_continuous = (
node_acquisition_mode_continuous.GetValue()
)
node_acquisition_mode.SetIntValue(acquisition_mode_continuous)

# Begin Acquisition: Image acquisition must be ended when no more images are needed.
self.camera.BeginAcquisition()
logger.debug(f"BeginAcquisition {self.name(sn_only=True)} ")
self.running = True
self.capture_thread = threading.Thread(
target=self.capture_loop, daemon=True
)
self.capture_thread.start()
except Exception as e:
logger.error(f"An error occurred while starting the camera: {e}")
print(f"Error: An error occurred while starting the camera {e}")

def capture_loop(self):
"""
Expand Down Expand Up @@ -537,8 +550,7 @@ def get_last_capture_time(self, millisecond=False):
)

def save_last_image(
self, filepath, isTimestamp=False, custom_name="Microscope_"
):
self, filepath, isTimestamp=False, custom_name="Microscope_"):
"""
Saves the last captured image to the specified file path.

Expand Down
30 changes: 18 additions & 12 deletions parallax/probe_calibration.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import csv
import logging
import os

import datetime
import numpy as np
import pandas as pd
from PyQt5.QtCore import QObject, pyqtSignal
Expand Down Expand Up @@ -89,8 +89,11 @@ def __init__(self, model, stage_listener):
self.origin, self.R, self.scale = None, None, np.array([1, 1, 1])
self.avg_err = None
self.last_row = None

# create file for points.csv
self.log_dir = None
self._create_file()

def reset_calib(self, sn=None):
"""
Resets calibration to its initial state, clearing any stored min and max values.
Expand Down Expand Up @@ -119,9 +122,10 @@ def _create_file(self):
Creates or clears the CSV file used to store local and global points during calibration.
"""
package_dir = os.path.dirname(os.path.abspath(__file__))
debug_dir = os.path.join(os.path.dirname(package_dir), "debug")
os.makedirs(debug_dir, exist_ok=True)
self.csv_file = os.path.join(debug_dir, "points.csv")
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
self.log_dir = os.path.join(os.path.dirname(package_dir), f"debug/log_{timestamp}")
os.makedirs(self.log_dir, exist_ok=True) # Create the log directory if it doesn't exist
self.csv_file = os.path.join(self.log_dir, "points.csv")

# Check if the file exists and remove it if it does
if os.path.exists(self.csv_file):
Expand Down Expand Up @@ -523,11 +527,12 @@ def _save_df_to_csv(self, df, file_name):
Args:
filtered_df (pd.DataFrame): DataFrame containing filtered local and global points.
"""
if self.log_dir is None:
logger.error("log_dir is not initialized.")
return

# Save the updated DataFrame back to the CSV file
package_dir = os.path.dirname(os.path.abspath(__file__))
debug_dir = os.path.join(os.path.dirname(package_dir), "debug")
os.makedirs(debug_dir, exist_ok=True)
csv_file = os.path.join(debug_dir, file_name)
csv_file = os.path.join(self.log_dir, file_name)
df.to_csv(csv_file, index=False)

return csv_file
Expand Down Expand Up @@ -618,7 +623,8 @@ 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"
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
self.file_name = f"points_{self.stage.sn}_{timestamp}.csv"
self.transM_LR = self._get_transM(filtered_df, save_to_csv=True, file_name=self.file_name, noise_threshold=20)

if self.transM_LR is None:
Expand All @@ -628,7 +634,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}_{timestamp}.csv")

if self.model.bundle_adjustment:
self.old_transM, self.old_scale = self.transM_LR, self.scale
Expand All @@ -639,7 +645,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_BA_{self.stage.sn}.csv")
file_name = f"transM_BA_{self.stage.sn}_{timestamp}.csv")
else:
return

Expand Down
2 changes: 1 addition & 1 deletion parallax/screen_coords_mapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def _clicked_position(self, camera_name, pos):
global_coords = self._get_global_coords_BA(camera_name, pos)
if global_coords is None:
return

global_coords = np.round(global_coords*1000, decimals=1)
reticle_name = self.reticle_selector.currentText()
if "Proj" not in reticle_name:
Expand Down
Loading
Loading