Skip to content

Commit

Permalink
Merge pull request #86 from AllenNeuralDynamics/wip/polish_code
Browse files Browse the repository at this point in the history
Wip/polish code
  • Loading branch information
jsiegle authored Sep 25, 2024
2 parents ad4200f + 3d7a6c2 commit 03a7837
Show file tree
Hide file tree
Showing 13 changed files with 314 additions and 627 deletions.
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

0 comments on commit 03a7837

Please sign in to comment.