diff --git a/docs/source/_autosummary/parallax.axis_filter.rst b/docs/source/_autosummary/parallax.axis_filter.rst index 0d05944..de252b3 100644 --- a/docs/source/_autosummary/parallax.axis_filter.rst +++ b/docs/source/_autosummary/parallax.axis_filter.rst @@ -1,4 +1,4 @@ -parallax.axis\_filter +parallax.axis\_filter ===================== .. automodule:: parallax.axis_filter diff --git a/docs/source/_autosummary/parallax.bundle_adjustment.rst b/docs/source/_autosummary/parallax.bundle_adjustment.rst index a80d856..6705681 100644 --- a/docs/source/_autosummary/parallax.bundle_adjustment.rst +++ b/docs/source/_autosummary/parallax.bundle_adjustment.rst @@ -1,4 +1,4 @@ -parallax.bundle\_adjustment +parallax.bundle\_adjustment =========================== .. automodule:: parallax.bundle_adjustment diff --git a/docs/source/_autosummary/parallax.calculator.rst b/docs/source/_autosummary/parallax.calculator.rst index a6b7164..0ca46f1 100644 --- a/docs/source/_autosummary/parallax.calculator.rst +++ b/docs/source/_autosummary/parallax.calculator.rst @@ -1,4 +1,4 @@ -parallax.calculator +parallax.calculator =================== .. automodule:: parallax.calculator diff --git a/docs/source/_autosummary/parallax.calibration_camera.rst b/docs/source/_autosummary/parallax.calibration_camera.rst index 8f251e1..286af06 100644 --- a/docs/source/_autosummary/parallax.calibration_camera.rst +++ b/docs/source/_autosummary/parallax.calibration_camera.rst @@ -1,4 +1,4 @@ -parallax.calibration\_camera +parallax.calibration\_camera ============================ .. automodule:: parallax.calibration_camera diff --git a/docs/source/_autosummary/parallax.camera.rst b/docs/source/_autosummary/parallax.camera.rst index ebc1909..c036603 100644 --- a/docs/source/_autosummary/parallax.camera.rst +++ b/docs/source/_autosummary/parallax.camera.rst @@ -1,4 +1,4 @@ -parallax.camera +parallax.camera =============== .. automodule:: parallax.camera diff --git a/docs/source/_autosummary/parallax.coords_transformation.rst b/docs/source/_autosummary/parallax.coords_transformation.rst index 522bd98..0a6c1d1 100644 --- a/docs/source/_autosummary/parallax.coords_transformation.rst +++ b/docs/source/_autosummary/parallax.coords_transformation.rst @@ -1,4 +1,4 @@ -parallax.coords\_transformation +parallax.coords\_transformation =============================== .. automodule:: parallax.coords_transformation diff --git a/docs/source/_autosummary/parallax.curr_bg_cmp_processor.rst b/docs/source/_autosummary/parallax.curr_bg_cmp_processor.rst index e77a258..830102c 100644 --- a/docs/source/_autosummary/parallax.curr_bg_cmp_processor.rst +++ b/docs/source/_autosummary/parallax.curr_bg_cmp_processor.rst @@ -1,4 +1,4 @@ -parallax.curr\_bg\_cmp\_processor +parallax.curr\_bg\_cmp\_processor ================================= .. automodule:: parallax.curr_bg_cmp_processor diff --git a/docs/source/_autosummary/parallax.curr_prev_cmp_processor.rst b/docs/source/_autosummary/parallax.curr_prev_cmp_processor.rst index dd914d4..bdf2244 100644 --- a/docs/source/_autosummary/parallax.curr_prev_cmp_processor.rst +++ b/docs/source/_autosummary/parallax.curr_prev_cmp_processor.rst @@ -1,4 +1,4 @@ -parallax.curr\_prev\_cmp\_processor +parallax.curr\_prev\_cmp\_processor =================================== .. automodule:: parallax.curr_prev_cmp_processor diff --git a/docs/source/_autosummary/parallax.main_window_wip.rst b/docs/source/_autosummary/parallax.main_window_wip.rst index 899c998..90915e9 100644 --- a/docs/source/_autosummary/parallax.main_window_wip.rst +++ b/docs/source/_autosummary/parallax.main_window_wip.rst @@ -1,4 +1,4 @@ -parallax.main\_window\_wip +parallax.main\_window\_wip ========================== .. automodule:: parallax.main_window_wip diff --git a/docs/source/_autosummary/parallax.mask_generator.rst b/docs/source/_autosummary/parallax.mask_generator.rst index 5144ac9..9d7b6e1 100644 --- a/docs/source/_autosummary/parallax.mask_generator.rst +++ b/docs/source/_autosummary/parallax.mask_generator.rst @@ -1,4 +1,4 @@ -parallax.mask\_generator +parallax.mask\_generator ======================== .. automodule:: parallax.mask_generator diff --git a/docs/source/_autosummary/parallax.model.rst b/docs/source/_autosummary/parallax.model.rst index 3f5354a..f86ef83 100644 --- a/docs/source/_autosummary/parallax.model.rst +++ b/docs/source/_autosummary/parallax.model.rst @@ -1,4 +1,4 @@ -parallax.model +parallax.model ============== .. automodule:: parallax.model diff --git a/docs/source/_autosummary/parallax.no_filter.rst b/docs/source/_autosummary/parallax.no_filter.rst index c40c26e..78fa979 100644 --- a/docs/source/_autosummary/parallax.no_filter.rst +++ b/docs/source/_autosummary/parallax.no_filter.rst @@ -1,4 +1,4 @@ -parallax.no\_filter +parallax.no\_filter =================== .. automodule:: parallax.no_filter diff --git a/docs/source/_autosummary/parallax.point_mesh.rst b/docs/source/_autosummary/parallax.point_mesh.rst index 45a8455..e9f2b50 100644 --- a/docs/source/_autosummary/parallax.point_mesh.rst +++ b/docs/source/_autosummary/parallax.point_mesh.rst @@ -1,4 +1,4 @@ -parallax.point\_mesh +parallax.point\_mesh ==================== .. automodule:: parallax.point_mesh diff --git a/docs/source/_autosummary/parallax.probe_calibration.rst b/docs/source/_autosummary/parallax.probe_calibration.rst index 6b0844f..4cbaa5e 100644 --- a/docs/source/_autosummary/parallax.probe_calibration.rst +++ b/docs/source/_autosummary/parallax.probe_calibration.rst @@ -1,4 +1,4 @@ -parallax.probe\_calibration +parallax.probe\_calibration =========================== .. automodule:: parallax.probe_calibration diff --git a/docs/source/_autosummary/parallax.probe_detect_manager.rst b/docs/source/_autosummary/parallax.probe_detect_manager.rst index fe6aa7d..c7b7f13 100644 --- a/docs/source/_autosummary/parallax.probe_detect_manager.rst +++ b/docs/source/_autosummary/parallax.probe_detect_manager.rst @@ -1,4 +1,4 @@ -parallax.probe\_detect\_manager +parallax.probe\_detect\_manager =============================== .. automodule:: parallax.probe_detect_manager diff --git a/docs/source/_autosummary/parallax.probe_detector.rst b/docs/source/_autosummary/parallax.probe_detector.rst index 7788058..c1b0adc 100644 --- a/docs/source/_autosummary/parallax.probe_detector.rst +++ b/docs/source/_autosummary/parallax.probe_detector.rst @@ -1,4 +1,4 @@ -parallax.probe\_detector +parallax.probe\_detector ======================== .. automodule:: parallax.probe_detector diff --git a/docs/source/_autosummary/parallax.probe_fine_tip_detector.rst b/docs/source/_autosummary/parallax.probe_fine_tip_detector.rst index b012923..50460d7 100644 --- a/docs/source/_autosummary/parallax.probe_fine_tip_detector.rst +++ b/docs/source/_autosummary/parallax.probe_fine_tip_detector.rst @@ -1,4 +1,4 @@ -parallax.probe\_fine\_tip\_detector +parallax.probe\_fine\_tip\_detector =================================== .. automodule:: parallax.probe_fine_tip_detector diff --git a/docs/source/_autosummary/parallax.recording_manager.rst b/docs/source/_autosummary/parallax.recording_manager.rst index f69155f..609895a 100644 --- a/docs/source/_autosummary/parallax.recording_manager.rst +++ b/docs/source/_autosummary/parallax.recording_manager.rst @@ -1,4 +1,4 @@ -parallax.recording\_manager +parallax.recording\_manager =========================== .. automodule:: parallax.recording_manager diff --git a/docs/source/_autosummary/parallax.reticle_detect_manager.rst b/docs/source/_autosummary/parallax.reticle_detect_manager.rst index fc5812a..6f78cdc 100644 --- a/docs/source/_autosummary/parallax.reticle_detect_manager.rst +++ b/docs/source/_autosummary/parallax.reticle_detect_manager.rst @@ -1,4 +1,4 @@ -parallax.reticle\_detect\_manager +parallax.reticle\_detect\_manager ================================= .. automodule:: parallax.reticle_detect_manager diff --git a/docs/source/_autosummary/parallax.reticle_detection.rst b/docs/source/_autosummary/parallax.reticle_detection.rst index 291abbc..b52f747 100644 --- a/docs/source/_autosummary/parallax.reticle_detection.rst +++ b/docs/source/_autosummary/parallax.reticle_detection.rst @@ -1,4 +1,4 @@ -parallax.reticle\_detection +parallax.reticle\_detection =========================== .. automodule:: parallax.reticle_detection diff --git a/docs/source/_autosummary/parallax.reticle_detection_coords_interests.rst b/docs/source/_autosummary/parallax.reticle_detection_coords_interests.rst index c5becb4..6a3f729 100644 --- a/docs/source/_autosummary/parallax.reticle_detection_coords_interests.rst +++ b/docs/source/_autosummary/parallax.reticle_detection_coords_interests.rst @@ -1,4 +1,4 @@ -parallax.reticle\_detection\_coords\_interests +parallax.reticle\_detection\_coords\_interests ============================================== .. automodule:: parallax.reticle_detection_coords_interests diff --git a/docs/source/_autosummary/parallax.reticle_metadata.rst b/docs/source/_autosummary/parallax.reticle_metadata.rst index 728ac86..ce661d2 100644 --- a/docs/source/_autosummary/parallax.reticle_metadata.rst +++ b/docs/source/_autosummary/parallax.reticle_metadata.rst @@ -1,4 +1,4 @@ -parallax.reticle\_metadata +parallax.reticle\_metadata ========================== .. automodule:: parallax.reticle_metadata diff --git a/docs/source/_autosummary/parallax.rst b/docs/source/_autosummary/parallax.rst deleted file mode 100644 index 5abe361..0000000 --- a/docs/source/_autosummary/parallax.rst +++ /dev/null @@ -1,60 +0,0 @@ -parallax -======== - -.. automodule:: parallax - - - - - - - - - - - - - - - - - - - -.. rubric:: Modules - -.. autosummary:: - :toctree: - :recursive: - - parallax.axis_filter - parallax.bundle_adjustment - parallax.calculator - parallax.calibration_camera - parallax.camera - parallax.coords_transformation - parallax.curr_bg_cmp_processor - parallax.curr_prev_cmp_processor - parallax.main_window_wip - parallax.mask_generator - parallax.model - parallax.no_filter - parallax.point_mesh - parallax.probe_calibration - parallax.probe_detect_manager - parallax.probe_detector - parallax.probe_fine_tip_detector - parallax.recording_manager - parallax.reticle_detect_manager - parallax.reticle_detection - parallax.reticle_detection_coords_interests - parallax.reticle_metadata - parallax.screen_coords_mapper - parallax.screen_widget - parallax.stage_controller - parallax.stage_listener - parallax.stage_ui - parallax.stage_widget - parallax.user_setting_manager - parallax.utils - diff --git a/docs/source/_autosummary/parallax.screen_coords_mapper.rst b/docs/source/_autosummary/parallax.screen_coords_mapper.rst index de947da..ba7547d 100644 --- a/docs/source/_autosummary/parallax.screen_coords_mapper.rst +++ b/docs/source/_autosummary/parallax.screen_coords_mapper.rst @@ -1,4 +1,4 @@ -parallax.screen\_coords\_mapper +parallax.screen\_coords\_mapper =============================== .. automodule:: parallax.screen_coords_mapper diff --git a/docs/source/_autosummary/parallax.screen_widget.rst b/docs/source/_autosummary/parallax.screen_widget.rst index b9c10d3..4cfc6d1 100644 --- a/docs/source/_autosummary/parallax.screen_widget.rst +++ b/docs/source/_autosummary/parallax.screen_widget.rst @@ -1,4 +1,4 @@ -parallax.screen\_widget +parallax.screen\_widget ======================= .. automodule:: parallax.screen_widget diff --git a/docs/source/_autosummary/parallax.stage_controller.rst b/docs/source/_autosummary/parallax.stage_controller.rst index b17ca61..ed7b80a 100644 --- a/docs/source/_autosummary/parallax.stage_controller.rst +++ b/docs/source/_autosummary/parallax.stage_controller.rst @@ -1,4 +1,4 @@ -parallax.stage\_controller +parallax.stage\_controller ========================== .. automodule:: parallax.stage_controller diff --git a/docs/source/_autosummary/parallax.stage_listener.rst b/docs/source/_autosummary/parallax.stage_listener.rst index 88227c7..f82386f 100644 --- a/docs/source/_autosummary/parallax.stage_listener.rst +++ b/docs/source/_autosummary/parallax.stage_listener.rst @@ -1,4 +1,4 @@ -parallax.stage\_listener +parallax.stage\_listener ======================== .. automodule:: parallax.stage_listener diff --git a/docs/source/_autosummary/parallax.stage_ui.rst b/docs/source/_autosummary/parallax.stage_ui.rst index 477411e..9f6eb56 100644 --- a/docs/source/_autosummary/parallax.stage_ui.rst +++ b/docs/source/_autosummary/parallax.stage_ui.rst @@ -1,4 +1,4 @@ -parallax.stage\_ui +parallax.stage\_ui ================== .. automodule:: parallax.stage_ui diff --git a/docs/source/_autosummary/parallax.stage_widget.rst b/docs/source/_autosummary/parallax.stage_widget.rst index 10a3d60..2ebec1b 100644 --- a/docs/source/_autosummary/parallax.stage_widget.rst +++ b/docs/source/_autosummary/parallax.stage_widget.rst @@ -1,4 +1,4 @@ -parallax.stage\_widget +parallax.stage\_widget ====================== .. automodule:: parallax.stage_widget diff --git a/docs/source/_autosummary/parallax.user_setting_manager.rst b/docs/source/_autosummary/parallax.user_setting_manager.rst index 61c4bd9..ad62923 100644 --- a/docs/source/_autosummary/parallax.user_setting_manager.rst +++ b/docs/source/_autosummary/parallax.user_setting_manager.rst @@ -1,4 +1,4 @@ -parallax.user\_setting\_manager +parallax.user\_setting\_manager =============================== .. automodule:: parallax.user_setting_manager diff --git a/docs/source/_autosummary/parallax.utils.rst b/docs/source/_autosummary/parallax.utils.rst index 8028c2d..50a78da 100644 --- a/docs/source/_autosummary/parallax.utils.rst +++ b/docs/source/_autosummary/parallax.utils.rst @@ -1,4 +1,4 @@ -parallax.utils +parallax.utils ============== .. automodule:: parallax.utils diff --git a/docs/source/conf.py b/docs/source/conf.py index 9d17941..337d8bc 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -20,7 +20,12 @@ copyright = f"{current_year}, {INSTITUTE_NAME}" author = INSTITUTE_NAME release = "0.0.1" # Automatically set version from parallax package -autosummary_generate = True + +autosummary_generate = True # Automatically generate stub files for autosummary +autoclass_content = "both" +autodoc_default_options = { + 'private-members': True, # Include private members +} # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration diff --git a/docs/source/modules.rst b/docs/source/modules.rst index 820ae2b..7913321 100644 --- a/docs/source/modules.rst +++ b/docs/source/modules.rst @@ -8,12 +8,42 @@ Summary :toctree: _autosummary :recursive: - parallax + parallax.calculator + parallax.calibration_camera + parallax.camera + parallax.coords_transformation + parallax.curr_bg_cmp_processor + parallax.curr_prev_cmp_processor + parallax.main_window_wip + parallax.mask_generator + parallax.model + parallax.no_filter + parallax.axis_filter + parallax.bundle_adjustment + parallax.probe_calibration + parallax.probe_detect_manager + parallax.probe_detector + parallax.probe_fine_tip_detector + parallax.recording_manager + parallax.reticle_detect_manager + parallax.reticle_detection + parallax.reticle_detection_coords_interests + parallax.screen_widget + parallax.stage_listener + parallax.stage_ui + parallax.stage_widget + parallax.utils + parallax.point_mesh + parallax.reticle_metadata + parallax.screen_coords_mapper + parallax.stage_controller + parallax.user_setting_manager Detailed Modules ---------------- - + .. toctree:: :maxdepth: 2 - - parallax + + parallaxModules + \ No newline at end of file diff --git a/docs/source/parallax.rst b/docs/source/parallax.rst deleted file mode 100644 index 844dc73..0000000 --- a/docs/source/parallax.rst +++ /dev/null @@ -1,276 +0,0 @@ -Module Description -================ - -Calculator ----------- - -This module contains a calculator for various mathematical functions needed in the system. - -.. automodule:: parallax.calculator - :members: - :undoc-members: - :special-members: __init__, calculate - - -Calibration Camera ------------------- - -This module provides camera calibration functionality. - -.. automodule:: parallax.calibration_camera - :members: - :undoc-members: - :special-members: __init__, calibrate - - -Camera ------- - -This module manages camera interactions, such as capturing frames and adjusting settings. - -.. automodule:: parallax.camera - :members: - :undoc-members: - :special-members: __init__, capture_frame, adjust_exposure - - -Coordinate Transformation -------------------------- - -This module handles transformations between different coordinate systems. - -.. automodule:: parallax.coords_transformation - :members: - :undoc-members: - :special-members: __init__, transform_to_global, transform_to_local - - -Current Background Comparison Processor ---------------------------------------- - -This module compares the current background frame with previous frames to detect changes. - -.. automodule:: parallax.curr_bg_cmp_processor - :members: - :undoc-members: - :special-members: __init__, compare_frames - - -Current Previous Comparison Processor -------------------------------------- - -This module compares current frames with previous ones to track changes over time. - -.. automodule:: parallax.curr_prev_cmp_processor - :members: - :undoc-members: - :special-members: __init__, process_comparison - - -Main Window WIP ---------------- - -This module manages the main window user interface components. - -.. automodule:: parallax.main_window_wip - :members: - :undoc-members: - :special-members: __init__, show_window - - -Mask Generator --------------- - -This module generates masks for image processing tasks. - -.. automodule:: parallax.mask_generator - :members: - :undoc-members: - :special-members: __init__, generate_mask - - -Model ------ - -This module defines the core model for the application. - -.. automodule:: parallax.model - :members: - :undoc-members: - :special-members: __init__, update_state - - -No Filter ---------- - -This module defines a pass-through filter with no changes. - -.. automodule:: parallax.no_filter - :members: - :undoc-members: - :special-members: __init__, apply_filter - - -Axis Filter ------------ - -This module handles the filtering of axis data. - -.. automodule:: parallax.axis_filter - :members: - :undoc-members: - :special-members: __init__, filter_data - - -Bundle Adjustment ------------------ - -This module performs bundle adjustment to refine the 3D structure. - -.. automodule:: parallax.bundle_adjustment - :members: - :undoc-members: - :special-members: __init__, adjust - - -Probe Calibration ------------------ - -This module manages the calibration of the probe. - -.. automodule:: parallax.probe_calibration - :members: - :undoc-members: - :special-members: __init__, calibrate_probe - - -Probe Detect Manager --------------------- - -This module manages the detection of probes in images. - -.. automodule:: parallax.probe_detect_manager - :members: - :undoc-members: - :special-members: __init__, detect_probe - - -Probe Detector --------------- - -This module contains methods for detecting probes in the environment. - -.. automodule:: parallax.probe_detector - :members: - :undoc-members: - :special-members: __init__, detect_tip - - -Probe Fine Tip Detector ------------------------ - -This module detects fine tips of probes. - -.. automodule:: parallax.probe_fine_tip_detector - :members: - :undoc-members: - :special-members: __init__, detect_fine_tip - - -Recording Manager ------------------ - -This module manages the recording of data during the session. - -.. automodule:: parallax.recording_manager - :members: - :undoc-members: - :special-members: __init__, record_data - - -Reticle Detect Manager ----------------------- - -This module handles the detection and tracking of reticles. - -.. automodule:: parallax.reticle_detect_manager - :members: - :undoc-members: - :special-members: __init__, detect_reticle - - -Reticle Detection ------------------ - -This module provides the core methods for detecting reticles. - -.. automodule:: parallax.reticle_detection - :members: - :undoc-members: - :special-members: __init__, detect_shape - - -Reticle Detection Coordinates Interests ---------------------------------------- - -This module deals with detecting specific points of interest in reticle coordinates. - -.. automodule:: parallax.reticle_detection_coords_interests - :members: - :undoc-members: - :special-members: __init__, find_interests - - -Screen Widget -------------- - -This module provides screen interaction components. - -.. automodule:: parallax.screen_widget - :members: - :undoc-members: - :special-members: __init__, update_screen - - -Stage Listener --------------- - -This module listens for changes in the stage and processes events. - -.. automodule:: parallax.stage_listener - :members: - :undoc-members: - :special-members: __init__, listen - - -Stage UI --------- - -This module provides UI components for controlling the stage. - -.. automodule:: parallax.stage_ui - :members: - :undoc-members: - :special-members: __init__, update_ui - - -Stage Widget ------------- - -This module contains the UI components for the stage. - -.. automodule:: parallax.stage_widget - :members: - :undoc-members: - :special-members: __init__, update_widget - - -Utils ------ - -This module contains utility functions used across the project. - -.. automodule:: parallax.utils - :members: - :undoc-members: - :special-members: __init__, helper_method diff --git a/docs/source/parallaxModules.rst b/docs/source/parallaxModules.rst new file mode 100644 index 0000000..8e69032 --- /dev/null +++ b/docs/source/parallaxModules.rst @@ -0,0 +1,268 @@ +Module Description +=================== + +Calculator +---------- + +.. automodule:: parallax.calculator + :members: + :undoc-members: + :private-members: + + +Calibration Camera +------------------ + +.. automodule:: parallax.calibration_camera + :members: + :undoc-members: + :private-members: + + +Camera +------ + +.. automodule:: parallax.camera + :members: + :undoc-members: + :private-members: + + +Coordinate Transformation +------------------------- + +.. automodule:: parallax.coords_transformation + :members: + :undoc-members: + :private-members: + + +Current Background Comparison Processor +--------------------------------------- + +.. automodule:: parallax.curr_bg_cmp_processor + :members: + :undoc-members: + :private-members: + + +Current Previous Comparison Processor +------------------------------------- + +.. automodule:: parallax.curr_prev_cmp_processor + :members: + :undoc-members: + :private-members: + + +Main Window +------------ + +.. automodule:: parallax.main_window_wip + :members: + :undoc-members: + :private-members: + + +Mask Generator +-------------- + +.. automodule:: parallax.mask_generator + :members: + :undoc-members: + :private-members: + + +Model +----- + +.. automodule:: parallax.model + :members: + :undoc-members: + :private-members: + + +No Filter +--------- + +.. automodule:: parallax.no_filter + :members: + :undoc-members: + :private-members: + + +Axis Filter +----------- + +.. automodule:: parallax.axis_filter + :members: + :undoc-members: + :private-members: + + +Bundle Adjustment +----------------- + +.. automodule:: parallax.bundle_adjustment + :members: + :undoc-members: + :private-members: + + +Probe Calibration +----------------- + +.. automodule:: parallax.probe_calibration + :members: + :undoc-members: + :private-members: + + +Probe Detect Manager +-------------------- + +.. automodule:: parallax.probe_detect_manager + :members: + :undoc-members: + :private-members: + + +Probe Detector +-------------- + +.. automodule:: parallax.probe_detector + :members: + :undoc-members: + :private-members: + + +Probe Fine Tip Detector +----------------------- + +.. automodule:: parallax.probe_fine_tip_detector + :members: + :undoc-members: + :private-members: + + +Recording Manager +----------------- + +.. automodule:: parallax.recording_manager + :members: + :undoc-members: + :private-members: + + +Reticle Detect Manager +---------------------- + +.. automodule:: parallax.reticle_detect_manager + :members: + :undoc-members: + :private-members: + + +Reticle Detection +----------------- + +.. automodule:: parallax.reticle_detection + :members: + :undoc-members: + :private-members: + + +Reticle Detection Coordinates Interests +--------------------------------------- + +.. automodule:: parallax.reticle_detection_coords_interests + :members: + :undoc-members: + :private-members: + + +Screen Widget +------------- + +.. automodule:: parallax.screen_widget + :members: + :undoc-members: + :private-members: + + +Stage Listener +-------------- + +.. automodule:: parallax.stage_listener + :members: + :undoc-members: + :private-members: + + +Stage UI +-------- + +.. automodule:: parallax.stage_ui + :members: + :undoc-members: + :private-members: + + +Stage Widget +------------ + +.. automodule:: parallax.stage_widget + :members: + :undoc-members: + :private-members: + +Point Mesh +------------ + +.. automodule:: parallax.point_mesh + :members: + :undoc-members: + :private-members: + +Reticle Metadata +------------------ + +.. automodule:: parallax.reticle_metadata + :members: + :undoc-members: + :private-members: + +Screen Coordinate +------------------ + +.. automodule:: parallax.screen_coords_mapper + :members: + :undoc-members: + :private-members: + +Screen controller +------------------ + +.. automodule:: parallax.stage_controller + :members: + :undoc-members: + :private-members: + +User Setting Manager +--------------------- + +.. automodule:: parallax.user_setting_manager + :members: + :undoc-members: + :private-members: + + +Utils +----- + +.. automodule:: parallax.utils + :members: + :undoc-members: + :private-members: + + diff --git a/parallax/__init__.py b/parallax/__init__.py index 5429efb..7cb612e 100644 --- a/parallax/__init__.py +++ b/parallax/__init__.py @@ -4,7 +4,7 @@ import os -__version__ = "0.38.15" +__version__ = "0.38.16" # allow multiple OpenMP instances os.environ["KMP_DUPLICATE_LIB_OK"] = "True" diff --git a/parallax/axis_filter.py b/parallax/axis_filter.py index b55ca27..2f89f31 100644 --- a/parallax/axis_filter.py +++ b/parallax/axis_filter.py @@ -1,7 +1,8 @@ """ -NoFilter serves as a pass-through component in a frame processing pipeline, -employing a worker-thread model to asynchronously handle frames without modification, -facilitating integration and optional processing steps. +This class is used during the reticle calibration process to handle the selection of the positive x-axis. +The system detects the reticle and requests the user to select the positive x-axis point. AxisFilter manages +the visualization of reticle points and processing of the two points along the x-axis and y-axis, retrieving points clicked by +the user on the screen. """ import logging @@ -17,14 +18,24 @@ logger.setLevel(logging.WARNING) class AxisFilter(QObject): - """Class representing no filter.""" + """ + AxisFilter class is used during the reticle calibration process. + + After detecting the reticle, the system prompts the user to select the positive x-axis. AxisFilter displays + the detected reticle points on the x-axis and y-axis and processes user input (clicked points) for calibration. + """ name = "None" frame_processed = pyqtSignal(object) found_coords = pyqtSignal(np.ndarray, np.ndarray, np.ndarray, np.ndarray, tuple, tuple) class Worker(QObject): - """Worker class for processing frames in a separate thread.""" + """ + Worker class for processing frames and handling user interactions in a separate thread. + + This class processes frames by displaying reticle coordinates and handles user clicks to select + the positive x-axis point. It also performs calibration based on selected points. + """ finished = pyqtSignal() frame_processed = pyqtSignal(object) @@ -33,7 +44,13 @@ class Worker(QObject): ) def __init__(self, name, model): - """Initialize the worker object.""" + """ + Initialize the worker object. + + Args: + name (str): The name of the camera (e.g., serial number). + model: The data model associated with the worker. + """ QObject.__init__(self) self.model = model self.name = name @@ -71,9 +88,20 @@ def process(self): self.frame_processed.emit(self.frame) def squared_distance(self, p1, p2): + """Calculate the squared distance between two points. + + Args: + p1, p2 (tuple): Points between which the squared distance is calculated. + """ return (p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2 def sort_reticle_points(self): + """ + Sort the reticle points based on the current position of the camera. + + This method sorts the reticle points in the correct order based on the + selected positive x-axis point and the detected coordinates. + """ if self.pos_x is None or self.reticle_coords is None: return @@ -128,6 +156,7 @@ def clicked_position(self, input_pt): self.model.add_pos_x(self.name, self.pos_x) def reset_pos_x(self): + """Reset the position of the x-axis (pos_x) in the model.""" self.pos_x = None self.model.reset_pos_x() logger.debug("reset pos_x") diff --git a/parallax/bundle_adjustment.py b/parallax/bundle_adjustment.py index 340c244..93e2329 100644 --- a/parallax/bundle_adjustment.py +++ b/parallax/bundle_adjustment.py @@ -1,3 +1,13 @@ +""" +This module implements the Bundle Adjustment (BA) problem and optimization process. + +The BALProblem class is responsible for loading and parsing the input data +from a CSV file, managing the reticle calibration data, and setting up camera parameters and observations. + +The BALOptimizer class performs optimization on the Bundle Adjustment problem, using the observations to minimize +the reprojection error through optimization of camera parameters and 3D points. +""" + import numpy as np import pandas as pd import cv2 @@ -9,7 +19,31 @@ logger.setLevel(logging.WARNING) class BALProblem: + """ + Class representing the Bundle Adjustment problem (BAL). + + The BALProblem class is responsible for parsing input data from a CSV file and setting up the necessary + observations, 3D points, and camera parameters for the bundle adjustment problem. It manages reticle + calibration data and provides access to the cameras and points for optimization. + + Attributes: + model: A reference to the data model. + file_path (str): Path to the CSV file containing the camera and points data. + df (pd.DataFrame): The parsed data stored as a Pandas DataFrame. + list_cameras (list): List of camera names involved in the bundle adjustment. + points (np.ndarray): Array of unique 3D points. + local_pts (np.ndarray): Array of local coordinates for the points. + cameras_params (list): List of camera parameters. + observations (np.ndarray): Array of observations containing camera index, point index, and image coordinates. + """ def __init__(self, model, file_path): + """ + Initialize the BALProblem class by parsing the CSV file and setting camera parameters. + + Args: + model: The data model used in the bundle adjustment. + file_path (str): Path to the CSV file containing the camera, point, and observation data. + """ self.list_cameras = None self.observations = None self.points = None @@ -23,24 +57,31 @@ def __init__(self, model, file_path): self._set_camera_params() def _parse_csv(self): + """Parse the input CSV file to extract relevant data and setup cameras, points, and observations.""" self.df = pd.read_csv(self.file_path) - #self._remove_duplicates() self._average_3D_points() - self._set_camera_list() self._set_points() self._set_observations() def _set_camera_list(self): + """Set the list of cameras from the parsed data.""" cameras = pd.concat([self.df['cam0'], self.df['cam1']]).unique() self.list_cameras = [str(camera) for camera in cameras] def _set_points(self): + """Set the unique 3D points from the parsed data.""" unique_df = self.df.drop_duplicates(subset=['m_global_x', 'm_global_y', 'm_global_z']) self.points = np.array(unique_df[['m_global_x', 'm_global_y', 'm_global_z']].values) self.local_pts = np.array(unique_df[['local_x', 'local_y', 'local_z']].values) def _set_observations(self): + """ + Set the observations from the parsed data. + + The observations consist of camera indices, point indices, and corresponding image coordinates + in the format (camera_index, point_index, x_image_coord, y_image_coord). + """ # Initialize the list to store observations self.observations = [] @@ -74,6 +115,11 @@ def _set_observations(self): self.observations = np.array(self.observations) def _average_3D_points(self): + """ + Calculate the average 3D points for each local coordinate set. + + The global 3D coordinates are averaged for each unique local coordinate set and stored in the DataFrame. + """ # Group by 'ts_local_coords' and calculate the mean for 'global_x', 'global_y', and 'global_z' grouped = self.df.groupby('ts_local_coords')[['global_x', 'global_y', 'global_z']].mean() grouped = grouped.rename(columns={'global_x': 'm_global_x', 'global_y': 'm_global_y', 'global_z': 'm_global_z'}) @@ -88,12 +134,19 @@ def _average_3D_points(self): self.df.to_csv(self.file_path, index=False) def _remove_duplicates(self): + """ + Remove duplicate rows from the DataFrame. + + This method removes duplicate rows based on the combination of the columns + 'ts_local_coords', 'global_x', 'global_y', and 'global_z'. + """ # Drop duplicate rows based on 'ts_local_coords', 'global_x', 'global_y', 'global_z' columns logger.debug(f"Original rows: {self.df.shape[0]}") self.df = self.df.drop_duplicates(subset=['ts_local_coords', 'global_x', 'global_y', 'global_z']) logger.debug(f"Unique rows: {self.df.shape[0]}") def _set_camera_params(self): + """Set the intrinsic and extrinsic parameters for each camera.""" if not self.list_cameras: return @@ -126,18 +179,49 @@ def _set_camera_params(self): self.cameras_params.append(camera_param) def get_camera_params(self, i): + """Retrieve the parameters for camera `i`.""" return self.cameras_params[i] def get_point(self, i): + """Retrieve the 3D point at index `i`.""" return self.points[i] class BALOptimizer: + """ + Class for performing Bundle Adjustment optimization. + + The BALOptimizer uses the observations from the BALProblem class to optimize the camera parameters + and 3D points, minimizing the reprojection error. + + Attributes: + bal_problem: An instance of the BALProblem class. + opt_camera_params (np.ndarray): Optimized camera parameters. + opt_points (np.ndarray): Optimized 3D points. + """ def __init__(self, bal_problem): + """ + Initialize the optimizer with the given BALProblem instance. + + Args: + bal_problem (BALProblem): The BALProblem instance containing the data to be optimized. + """ self.bal_problem = bal_problem self.opt_camera_params = None self.opt_points = None def residuals(self, params): + """ + Compute the residuals for the current parameters. + + The residuals represent the difference between the observed image points and the + projected points based on the current camera parameters and 3D points. + + Args: + params (np.ndarray): Flattened array of camera parameters and 3D points. + + Returns: + np.ndarray: Array of residuals (reprojection errors). + """ residuals = [] n_cams = len(self.bal_problem.list_cameras) n_pts = len(self.bal_problem.points) @@ -170,6 +254,16 @@ def residuals(self, params): return np.array(residuals) def optimize(self, print_result=True): + """ + Optimize the camera parameters and 3D points using the Bundle Adjustment method. + + This method uses the Levenberg-Marquardt algorithm (via `scipy.optimize.leastsq`) to minimize the + reprojection error based on the observations. The optimized camera parameters and 3D points are saved, + and optionally, the residuals before and after optimization are printed. + + Args: + print_result (bool): If True, print the optimization results and residuals before and after the optimization. + """ # Initial parameters vector initial_params = np.hstack([param.ravel() for param in self.bal_problem.cameras_params] + [self.bal_problem.points.ravel()]) diff --git a/parallax/calculator.py b/parallax/calculator.py index 9697b9f..ca723e4 100644 --- a/parallax/calculator.py +++ b/parallax/calculator.py @@ -1,3 +1,9 @@ +""" +This module implements the Calculator widget, which is used to transform local and global coordinates +and control stage movements. It includes functionality for managing stage interactions, applying +reticle adjustments, and issuing commands for stage movement. +""" + import os import logging import numpy as np @@ -129,7 +135,6 @@ def _convert(self, sn, transM, scale): transM (ndarray): The transformation matrix. scale (ndarray): The scale factors applied to the coordinates. """ - # Enable the groupBox for the stage globalX = self.findChild(QLineEdit, f"globalX_{sn}").text() globalY = self.findChild(QLineEdit, f"globalY_{sn}").text() @@ -257,7 +262,7 @@ def _apply_transformation(self, local_point_, transM_LR, scale): Applies the transformation to convert local coordinates to global coordinates. Args: - local_point_ (ndarray): The local coordinates. + local_point (ndarray): The local coordinates to be transformed. transM_LR (ndarray): The transformation matrix. scale (ndarray): The scale factors for the coordinates. diff --git a/parallax/calibration_camera.py b/parallax/calibration_camera.py index b559f9c..5cc37cd 100644 --- a/parallax/calibration_camera.py +++ b/parallax/calibration_camera.py @@ -2,13 +2,13 @@ Module for camera calibration and stereo calibration. This module provides classes for intrinsic camera calibration (`CalibrationCamera`) and stereo camera calibration (`CalibrationStereo`). + Classes: -CalibrationCamera: Class for intrinsic camera calibration. -CalibrationStereo: Class for stereo camera calibration. """ import logging - import cv2 import numpy as np @@ -68,7 +68,12 @@ class CalibrationCamera: """Class for intrinsic calibration.""" def __init__(self, camera_name): - """Initialize the CalibrationCamera object""" + """ + Initialize the CalibrationCamera object. + + Args: + camera_name (str): The name or serial number of the camera. + """ self.name = camera_name self.n_interest_pixels = X_COORDS_HALF self.imgpoints = None @@ -97,9 +102,11 @@ def _get_changed_data_format(self, x_axis, y_axis): def _process_reticle_points(self, x_axis, y_axis): """ Process reticle points for calibration. + Args: x_axis (list): X-axis coordinates. y_axis (list): Y-axis coordinates. + Returns: tuple: Image points and object points. """ @@ -229,7 +236,18 @@ class CalibrationStereo(CalibrationCamera): def __init__( self, model, camA, imgpointsA, intrinsicA, camB, imgpointsB, intrinsicB): - """Initialize the CalibrationStereo object""" + """ + Initialize the CalibrationStereo object. + + Args: + model (object): The model containing stage and transformation data. + camA (str): Camera A identifier. + imgpointsA (list): Image points for camera A. + intrinsicA (tuple): Intrinsic parameters for camera A. + camB (str): Camera B identifier. + imgpointsB (list): Image points for camera B. + intrinsicB (tuple): Intrinsic parameters for camera B. + """ self.model = model self.n_interest_pixels = X_COORDS_HALF self.camA = camA @@ -413,6 +431,16 @@ def get_global_coords(self, camA, coordA, camB, coordB): return points_3d_G def test_x_y_z_performance(self, points_3d_G): + """ + Evaluates the performance of the stereo calibration by comparing the + predicted 3D points with the original object points. + + Args: + points_3d_G (numpy.ndarray): The predicted 3D points in global coordinates. + + Prints: + The L2 norm (Euclidean distance) for the x, y, and z dimensions in micrometers (µm³). + """ # Calculate the differences for each dimension differences_x = points_3d_G[:, 0] - self.objpoints[0, :, 0] differences_y = points_3d_G[:, 1] - self.objpoints[0, :, 1] @@ -519,6 +547,18 @@ def test_pixel_error(self): print(f"(Reprojection error) Pixel L2 diff B: {total_err} pixels") def register_debug_points(self, camA, camB): + """ + Registers pixel coordinates of custom object points for debugging purposes. + + Args: + camA (str): The serial number or identifier of camera A. + camB (str): The serial number or identifier of camera B. + + This method: + 1. Defines a custom grid of object points (without scaling). + 2. Projects these 3D object points into 2D pixel coordinates for both camera A and camera B. + 3. Registers the computed pixel coordinates to the model for debugging. + """ # Define the custom object points directly without scaling x = np.arange(-4, 5) # from -4 to 4 y = np.arange(-4, 5) # from -4 to 4 diff --git a/parallax/coords_transformation.py b/parallax/coords_transformation.py index 6a464da..0b17c5c 100644 --- a/parallax/coords_transformation.py +++ b/parallax/coords_transformation.py @@ -1,37 +1,102 @@ +""" +This module provides functionality for performing 3D transformations, specifically roll, pitch, +and yaw rotations. It also includes methods for fitting transformation parameters to align measured +points to global points using least squares optimization. + +Classes: + - RotationTransformation: Handles 3D rotations and optimization of transformation parameters + (rotation, translation, and scaling) to fit measured points to global points. +""" import numpy as np from scipy.optimize import leastsq class RotationTransformation: + """ + This class provides methods for performing 3D rotations (roll, pitch, and yaw), + extracting angles from a rotation matrix, combining angles into a rotation matrix, + and fitting parameters for transforming measured points to global points through + optimization. + """ def __init__(self): + """Initialize the RotationTransformation class.""" pass def roll(self, inputMat, g): # rotation around x axis (bank angle) + """ + Performs a rotation around the x-axis (roll or bank angle). + + Args: + inputMat (numpy.ndarray): The input matrix to be rotated. + g (float): The roll angle in radians. + + Returns: + numpy.ndarray: The resulting matrix after applying the roll rotation. + """ rollMat = np.array([[1, 0, 0], [0, np.cos(g), -np.sin(g)], [0, np.sin(g), np.cos(g)]]) return np.dot(inputMat, rollMat) def pitch(self, inputMat, b): # rotation around y axis (elevation angle) + """ + Performs a rotation around the y-axis (pitch or elevation angle). + + Args: + inputMat (numpy.ndarray): The input matrix to be rotated. + b (float): The pitch angle in radians. + + Returns: + numpy.ndarray: The resulting matrix after applying the pitch rotation. + """ pitchMat = np.array([[np.cos(b), 0, np.sin(b)], [0, 1, 0], [-np.sin(b), 0, np.cos(b)]]) return np.dot(inputMat, pitchMat) def yaw(self, inputMat, a): # rotation around z axis (heading angle) + """ + Performs a rotation around the z-axis (yaw or heading angle). + + Args: + inputMat (numpy.ndarray): The input matrix to be rotated. + a (float): The yaw angle in radians. + + Returns: + numpy.ndarray: The resulting matrix after applying the yaw rotation. + """ yawMat = np.array([[np.cos(a), -np.sin(a), 0], [np.sin(a), np.cos(a), 0], [0, 0, 1]]) return np.dot(inputMat, yawMat) def extractAngles(self, mat): - """Extracts roll, pitch, and yaw angles from a rotation matrix mat""" + """ + Extracts roll, pitch, and yaw angles from a given rotation matrix. + + Args: + mat (numpy.ndarray): A 3x3 rotation matrix. + + Returns: + tuple: The roll (x), pitch (y), and yaw (z) angles in radians. + """ x = np.arctan2(mat[2, 1], mat[2, 2]) y = np.arctan2(-mat[2, 0], np.sqrt(pow(mat[2, 1], 2) + pow(mat[2, 2], 2))) z = np.arctan2(mat[1, 0], mat[0, 0]) return x, y, z def combineAngles(self, x, y, z, reflect_z=False): - """Combines separate roll, pitch, and yaw angles into a single rotation matrix.""" + """ + Combines roll, pitch, and yaw angles into a single rotation matrix. + + Args: + x (float): Roll angle in radians. + y (float): Pitch angle in radians. + z (float): Yaw angle in radians. + reflect_z (bool, optional): If True, applies a reflection along the z-axis. Defaults to False. + + Returns: + numpy.ndarray: The combined 3x3 rotation matrix. + """ eye = np.identity(3) R = self.roll( self.pitch( @@ -45,9 +110,19 @@ def combineAngles(self, x, y, z, reflect_z=False): return R def func(self, x, measured_pts, global_pts, reflect_z=False): - """Defines an error function for the optimization, - which calculates the difference - between transformed global points and measured points.""" + """ + Defines an error function for optimization, calculating the difference between transformed + global points and measured points. + + Args: + x (numpy.ndarray): The parameters to optimize (angles, translation, and scaling factors). + measured_pts (numpy.ndarray): The measured points (local coordinates). + global_pts (numpy.ndarray): The global points (target coordinates). + reflect_z (bool, optional): If True, applies a reflection along the z-axis. Defaults to False. + + Returns: + numpy.ndarray: The error values for each point. + """ R = self.combineAngles(x[2], x[1], x[0], reflect_z=reflect_z) origin = np.array([x[3], x[4], x[5]]).T scale = np.array([x[6], x[7], x[8]]) # scaling factors for x, y, z axes @@ -62,7 +137,18 @@ def func(self, x, measured_pts, global_pts, reflect_z=False): return error_values def avg_error(self, x, measured_pts, global_pts, reflect_z=False): - """Calculates the total error for the optimization.""" + """ + Calculates the total error (L2 norm) for the optimization. + + Args: + x (numpy.ndarray): The parameters to optimize. + measured_pts (numpy.ndarray): The measured points (local coordinates). + global_pts (numpy.ndarray): The global points (target coordinates). + reflect_z (bool, optional): If True, applies a reflection along the z-axis. Defaults to False. + + Returns: + float: The average L2 error across all points. + """ error_values = self.func(x, measured_pts, global_pts, reflect_z) # Calculate the L2 error for each point @@ -77,7 +163,18 @@ def avg_error(self, x, measured_pts, global_pts, reflect_z=False): return average_l2_error def fit_params(self, measured_pts, global_pts): - """Fits parameters to minimize the error defined in func""" + """ + Fits the transformation parameters (angles, translation, and scaling) to minimize the error + between measured points and global points using least squares optimization. + + Args: + measured_pts (numpy.ndarray): The measured points (local coordinates). + global_pts (numpy.ndarray): The global points (target coordinates). + + Returns: + tuple: A tuple containing the translation vector (origin), rotation matrix (R), + scaling factors (scale), and the average error (avg_err). + """ x0 = np.array([0, 0, 0, 0, 0, 0, 1, 1, 1]) # initial guess: (x, y, z, x_t, y_t, z_t, s_x, s_y, s_z) if len(measured_pts) <= 3 or len(global_pts) <= 3: diff --git a/parallax/curr_bg_cmp_processor.py b/parallax/curr_bg_cmp_processor.py index a633366..554d439 100644 --- a/parallax/curr_bg_cmp_processor.py +++ b/parallax/curr_bg_cmp_processor.py @@ -274,6 +274,7 @@ def _is_point_in_reticle_region(self, image, point): def _get_precise_tip(self, org_img): """Get precise probe tip on original size image + Args: org_img (numpy.ndarray): Original image. @@ -326,8 +327,10 @@ def get_fine_tip_boundary(self): def _get_binary(self, curr_img): """Get binary image. + Args: curr_img (numpy.ndarray): Current image. + Returns: numpy.ndarray: Binary image. """ diff --git a/parallax/main_window_wip.py b/parallax/main_window_wip.py index a7731f4..ec490ca 100644 --- a/parallax/main_window_wip.py +++ b/parallax/main_window_wip.py @@ -775,6 +775,15 @@ def save_user_configs(self): self.user_setting.save_user_configs(nColumn, directory, width, height) def closeEvent(self, event): + """ + Handles the widget's close event by performing cleanup actions for the model instances. + + This method ensures that all PointMesh widgets, Calculator instances, and ReticleMetadata + instances managed by the model are closed before the widget itself is closed. + + Args: + event (QCloseEvent): The close event triggered when the widget is closed. + """ self.model.close_all_point_meshes() self.model.close_clac_instance() self.model.close_reticle_metadata_instance() diff --git a/parallax/mask_generator.py b/parallax/mask_generator.py index e203809..b00cea9 100644 --- a/parallax/mask_generator.py +++ b/parallax/mask_generator.py @@ -19,7 +19,11 @@ class MaskGenerator: """Class for generating a mask from an image.""" def __init__(self, initial_detect=False): - """Initialize mask generator object""" + """Initialize the MaskGenerator object. + + Args: + initial_detect (bool, optional): Whether to perform initial detection with different settings. + """ self.img = None self.original_size = (None, None) self.is_reticle_exist = None @@ -42,6 +46,15 @@ def _apply_threshold(self): ) def _homomorphic_filter(self, gamma_high=1.5, gamma_low=0.5, c=1, d0=30): + """ + Apply a homomorphic filter to the image to enhance contrast and remove shadows. + + Args: + gamma_high (float, optional): The high gamma value for contrast adjustment. Default is 1.5. + gamma_low (float, optional): The low gamma value for contrast adjustment. Default is 0.5. + c (int, optional): Constant to adjust the filter strength. Default is 1. + d0 (int, optional): Cutoff frequency for the high-pass filter. Default is 30. + """ # Apply the log transform img_log = np.log1p(np.array(self.img, dtype="float")) @@ -102,7 +115,7 @@ def _apply_morphological_operations(self): self.img = cv2.bitwise_not(self.img) # Re-invert image back def _remove_small_contours(self): - """Remove small contours from the image.""" + """Remove small contours from the image based on a threshold size.""" contours, _ = cv2.findContours( self.img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE ) @@ -119,7 +132,7 @@ def _remove_small_contours(self): ) def _finalize_image(self): - """Resize the image back to its original size.""" + """Resize the image back to its original size and adjust the scale.""" self.img = cv2.resize(self.img, self.original_size) self.img = cv2.convertScaleAbs(self.img) @@ -162,6 +175,11 @@ def _is_reticle_frame(self, threshold = 0.5): return self.is_reticle_exist def _reticle_exist_check(self, threshold): + """Check if the reticle exists based on the threshold. + + Args: + threshold (float): The threshold percentage for determining reticle existence. + """ if self.is_reticle_exist is None: self._is_reticle_frame(threshold = threshold) diff --git a/parallax/model.py b/parallax/model.py index 6d73658..c77bbd9 100755 --- a/parallax/model.py +++ b/parallax/model.py @@ -1,12 +1,16 @@ """ The Model class is the core component for managing cameras, stages, and calibration data. +It provides methods for scanning and initializing cameras and stages, managing calibration data, +and handling point mesh instances for 3D visualization. + +This class integrates various hardware components such as cameras and stages and handles +their initialization, configuration, and transformations between local and global coordinates. """ from collections import OrderedDict from PyQt5.QtCore import QObject, pyqtSignal from .camera import MockCamera, PySpinCamera, close_cameras, list_cameras from .stage_listener import Stage, StageInfo - class Model(QObject): """Model class to handle cameras, stages, and calibration data.""" @@ -14,7 +18,12 @@ class Model(QObject): accutest_point_reached = pyqtSignal() def __init__(self, version="V1", bundle_adjustment=False): - """Initialize model object""" + """Initialize the Model object. + + Args: + version (str): The version of the model, typically used for camera setup. + bundle_adjustment (bool): Whether to enable bundle adjustment for calibration. + """ QObject.__init__(self) self.version = version self.bundle_adjustment = bundle_adjustment @@ -64,33 +73,50 @@ def __init__(self, version="V1", bundle_adjustment=False): self.clicked_pts = OrderedDict() def add_calibration(self, cal): - """Add a calibration.""" + """Add a calibration. + + Args: + cal: Calibration object to be added to the calibrations dictionary. + """ self.calibrations[cal.name] = cal def set_calibration(self, calibration): - """Set the calibration.""" + """Set the current calibration object. + + Args: + calibration: Calibration object to set as the active calibration. + """ self.calibration = calibration def init_stages(self): - """Initialize stages.""" + """Initialize stages by clearing the current stages and calibration data.""" self.stages = {} self.stages_calib = {} def init_transforms(self): + """Initialize the transformation matrices for all stages.""" for stage_sn in self.stages.keys(): self.transforms[stage_sn] = [None, None] def add_video_source(self, video_source): - """Add a video source.""" + """Add a video source (camera). + + Args: + video_source: The camera object to add to the model's camera list. + """ self.cameras.append(video_source) def add_mock_cameras(self, n=1): - """Add mock cameras.""" + """Add mock cameras for testing purposes. + + Args: + n (int): The number of mock cameras to add. + """ for i in range(n): self.cameras.append(MockCamera()) def scan_for_cameras(self): - """Scan for cameras.""" + """Scan and detect all available cameras.""" self.cameras = list_cameras(version=self.version) + self.cameras self.cameras_sn = [camera.name(sn_only=True) for camera in self.cameras] self.nMockCameras = len( @@ -109,7 +135,7 @@ def scan_for_cameras(self): ) def scan_for_usb_stages(self): - """Scan for USB stages.""" + """Scan for all USB-connected stages and initialize them.""" stage_info = StageInfo(self.stage_listener_url) instances = stage_info.get_instances() self.init_stages() @@ -119,28 +145,51 @@ def scan_for_usb_stages(self): self.nStages = len(self.stages) def add_stage(self, stage): - """Add a stage.""" + """Add a stage to the model. + + Args: + stage: Stage object to add to the model. + """ self.stages[stage.sn] = stage def get_stage(self, stage_sn): - """Get a stage.""" + """Retrieve a stage by its serial number. + + Args: + stage_sn (str): The serial number of the stage. + + Returns: + Stage: The stage object corresponding to the given serial number. + """ return self.stages.get(stage_sn) def add_stage_calib_info(self, stage_sn, info): - """Add a stage. - info['detection_status'] - info['transM'] - info['L2_err'] - info['scale'] - info['dist_traveled'] - info['status_x'] - info['status_y'] - info['status_z'] + """Add calibration information for a specific stage. + + Args: + stage_sn (str): The serial number of the stage. + info (dict): Calibration information for the stage. + + info['detection_status'] + info['transM'] + info['L2_err'] + info['scale'] + info['dist_traveled'] + info['status_x'] + info['status_y'] + info['status_z'] """ self.stages_calib[stage_sn] = info def get_stage_calib_info(self, stage_sn): - """Get a stage.""" + """Get calibration information for a specific stage. + + Args: + stage_sn (str): The serial number of the stage. + + Returns: + dict: Calibration information for the given stage. + """ return self.stages_calib.get(stage_sn) def reset_stage_calib_info(self): @@ -148,42 +197,87 @@ def reset_stage_calib_info(self): self.stages_calib = {} def add_pts(self, camera_name, pts): - """Add points. If a new camera is added and the size exceeds 2, remove the oldest.""" + """Add detected points for a camera. + + Args: + camera_name (str): The name of the camera. + pts (tuple): The detected points. + """ if len(self.clicked_pts) == 2 and camera_name not in self.clicked_pts: # Remove the oldest entry (first added item) self.clicked_pts.popitem(last=False) self.clicked_pts[camera_name] = pts def get_pts(self, camera_name): - """Get points.""" + """Retrieve points for a specific camera. + + Args: + camera_name (str): The name of the camera. + + Returns: + tuple: The points detected by the camera. + """ return self.clicked_pts.get(camera_name) def get_cameras_detected_pts(self): - """Get cameras that detected the points.""" + """Get the cameras that detected points. + + Returns: + OrderedDict: Cameras and their corresponding detected points. + """ return self.clicked_pts def reset_pts(self): - """Reset points.""" + """Reset all detected points.""" self.clicked_pts = OrderedDict() def add_transform(self, stage_sn, transform, scale): - """Add transformation matrix between local to global coordinates.""" + """Add transformation matrix for a stage to convert local coordinates to global coordinates. + + Args: + stage_sn (str): The serial number of the stage. + transform (numpy.ndarray): The transformation matrix. + scale (numpy.ndarray): The scale factors for the transformation. + """ self.transforms[stage_sn] = [transform, scale] def get_transform(self, stage_sn): - """Get transformation matrix between local to global coordinates.""" + """Get the transformation matrix for a specific stage. + + Args: + stage_sn (str): The serial number of the stage. + + Returns: + tuple: The transformation matrix and scale factors. + """ return self.transforms.get(stage_sn) def add_reticle_metadata(self, reticle_name, metadata): - """Add transformation matrix between local to global coordinates.""" + """Add reticle metadata. + + Args: + reticle_name (str): The name of the reticle. + metadata (dict): Metadata information for the reticle. + """ self.reticle_metadata[reticle_name] = metadata def get_reticle_metadata(self, reticle_name): - """Get transformation matrix between local to global coordinates.""" + """Get metadata for a specific reticle. + + Args: + reticle_name (str): The name of the reticle. + + Returns: + dict: Metadata information for the reticle. + """ return self.reticle_metadata.get(reticle_name) def remove_reticle_metadata(self, reticle_name): - """Remove transformation matrix between local to global coordinates.""" + """Remove reticle metadata. + + Args: + reticle_name (str): The name of the reticle to remove. + """ if reticle_name in self.reticle_metadata.keys(): self.reticle_metadata.pop(reticle_name, None) @@ -192,7 +286,11 @@ def reset_reticle_metadata(self): self.reticle_metadata = {} def add_probe_detector(self, probeDetector): - """Add a probe detector.""" + """Add a probe detector. + + Args: + probeDetector: The probe detector object to add. + """ self.probeDetectors.append(probeDetector) def reset_coords_intrinsic_extrinsic(self): @@ -202,70 +300,154 @@ def reset_coords_intrinsic_extrinsic(self): self.camera_extrinsic = {} def add_pos_x(self, camera_name, pt): - """Add position x.""" + """Add position for the x-axis for a specific camera. + + Args: + camera_name (str): The name of the camera. + pt: The position of the x-axis. + """ self.pos_x[camera_name] = pt def get_pos_x(self, camera_name): - """Add position x.""" + """Get the position for the x-axis of a specific camera. + + Args: + camera_name (str): The name of the camera. + + Returns: + The position of the x-axis for the camera. + """ return self.pos_x.get(camera_name) def reset_pos_x(self): - """Reset position x.""" + """Reset all x-axis positions.""" self.pos_x = {} def add_coords_axis(self, camera_name, coords): - """Add coordinates axis.""" + """Add axis coordinates for a specific camera. + + Args: + camera_name (str): The name of the camera. + coords (list): The axis coordinates to be added. + """ self.coords_axis[camera_name] = coords def get_coords_axis(self, camera_name): - """Get coordinates axis.""" + """Get axis coordinates for a specific camera. + + Args: + camera_name (str): The name of the camera. + + Returns: + list: The axis coordinates for the given camera. + """ return self.coords_axis.get(camera_name) def add_coords_for_debug(self, camera_name, coords): - """Add coordinates axis.""" + """Add debug coordinates for a specific camera. + + Args: + camera_name (str): The name of the camera. + coords (list): The coordinates used for debugging. + """ self.coords_debug[camera_name] = coords def get_coords_for_debug(self, camera_name): - """Get coordinates axis.""" + """Get debug coordinates for a specific camera. + + Args: + camera_name (str): The name of the camera. + + Returns: + list: The debug coordinates for the given camera. + """ return self.coords_debug.get(camera_name) def add_camera_intrinsic(self, camera_name, mtx, dist, rvec, tvec): - """Add camera intrinsic parameters.""" + """Add intrinsic camera parameters for a specific camera. + + Args: + camera_name (str): The name of the camera. + mtx (numpy.ndarray): The camera matrix. + dist (numpy.ndarray): The distortion coefficients. + rvec (numpy.ndarray): The rotation vector. + tvec (numpy.ndarray): The translation vector. + """ self.camera_intrinsic[camera_name] = [mtx, dist, rvec, tvec] def get_camera_intrinsic(self, camera_name): - """Get camera intrinsic parameters.""" + """Get intrinsic camera parameters for a specific camera. + + Args: + camera_name (str): The name of the camera. + + Returns: + list: The intrinsic parameters [mtx, dist, rvec, tvec] for the camera. + """ return self.camera_intrinsic.get(camera_name) def add_stereo_calib_instance(self, sorted_key, instance): + """Add stereo calibration instance. + + Args: + sorted_key (str): The sorted key that identifies the stereo calibration pair. + instance (object): The stereo calibration instance to add. + """ self.stereo_calib_instance[sorted_key] = instance def get_stereo_calib_instance(self, sorted_key): + """Get stereo calibration instance. + + Args: + sorted_key (str): The key identifying the stereo calibration instance. + + Returns: + object: The stereo calibration instance. + """ return self.stereo_calib_instance.get(sorted_key) def reset_stereo_calib_instance(self): + """Reset all stereo calibration instances.""" self.stereo_calib_instance = {} def add_camera_extrinsic(self, name1, name2, retVal, R, T, E, F): - """Add camera extrinsic parameters.""" + """Add extrinsic camera parameters for a camera pair. + + Args: + name1 (str): Name of the first camera. + name2 (str): Name of the second camera. + retVal (float): Return value of the stereo calibration. + R (numpy.ndarray): The rotation matrix between the two cameras. + T (numpy.ndarray): The translation vector between the two cameras. + E (numpy.ndarray): The essential matrix. + F (numpy.ndarray): The fundamental matrix. + """ self.best_camera_pair = [name1, name2] self.camera_extrinsic[name1 + "-" + name2] = [retVal, R, T, E, F] def get_camera_extrinsic(self, name1, name2): - """Get camera extrinsic parameters.""" + """Get extrinsic camera parameters for a specific camera pair. + + Args: + name1 (str): Name of the first camera. + name2 (str): Name of the second camera. + + Returns: + list: The extrinsic parameters [retVal, R, T, E, F] for the camera pair. + """ return self.camera_extrinsic.get(name1 + "-" + name2) def reset_camera_extrinsic(self): - """Add camera extrinsic parameters.""" + """Reset all extrinsic camera parameters and clear the best camera pair.""" self.best_camera_pair = None self.camera_extrinsic = {} def clean(self): - """Clean up.""" + """Clean up and close all camera connections.""" close_cameras() def save_all_camera_frames(self): - """Save all camera frames.""" + """Save the current frames from all cameras.""" for i, camera in enumerate(self.cameras): if camera.last_image: filename = 'camera%d_%s.png' % (i, camera.get_last_capture_time()) @@ -273,28 +455,46 @@ def save_all_camera_frames(self): self.msg_log.post("Saved camera frame: %s" % filename) def add_point_mesh_instance(self, instance): + """Add a point mesh instance for a specific stage or object. + + Args: + instance (object): The point mesh instance to add. + """ 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): + """Close all point mesh instances and clear them from the model.""" for instance in self.point_mesh_instances.values(): instance.close() self.point_mesh_instances.clear() def add_calc_instance(self, instance): + """Add a calculator instance. + + Args: + instance (object): The calculator instance to add. + """ self.calc_instance = instance def close_clac_instance(self): + """Close the calculator instance.""" if self.calc_instance is not None: self.calc_instance.close() self.calc_instance = None def add_reticle_metadata_instance(self, instance): + """Add a reticle metadata instance. + + Args: + instance (object): The reticle metadata instance to add. + """ self.reticle_metadata_instance = instance def close_reticle_metadata_instance(self): + """Close the reticle metadata instance.""" 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 503a252..14f1c3a 100644 --- a/parallax/probe_calibration.py +++ b/parallax/probe_calibration.py @@ -1,7 +1,6 @@ """ -ProbeCalibration transforms probe coordinates from local to global space -local space: Stage coordinates -global space: Reticle coordinates +ProbeCalibration: Handles the transformation of probe coordinates from local (stage) to global (reticle) space. +It supports calibrating the transformation between local and global coordinates through various techniques. """ import csv @@ -24,7 +23,14 @@ class ProbeCalibration(QObject): """ - Handles the transformation of probe coordinates from local (stage) to global (reticle) space. + A class responsible for calibrating probe positions by transforming local stage coordinates to global reticle coordinates. + + Signals: + calib_complete_x (str): Signal emitted when calibration for the X-axis is complete. + calib_complete_y (str): Signal emitted when calibration for the Y-axis is complete. + calib_complete_z (str): Signal emitted when calibration for the Z-axis is complete. + calib_complete (str, object, np.ndarray): Signal emitted when the full calibration is complete. + transM_info (str, object, np.ndarray, float, object): Signal emitted with transformation matrix information. """ calib_complete_x = pyqtSignal(str) calib_complete_y = pyqtSignal(str) @@ -34,11 +40,11 @@ class ProbeCalibration(QObject): def __init__(self, model, stage_listener): """ - Initializes the ProbeCalibration object with a given stage listener. + Initialize the ProbeCalibration object. Args: - model (object): The model that contains the stage information. - stage_listener (QObject): The object responsible for listening to stage signals. + model (object): The model object containing stage information. + stage_listener (QObject): The stage listener object for receiving stage-related events. """ super().__init__() self.transformer = RotationTransformation() @@ -135,7 +141,10 @@ def _create_file(self): def clear(self, sn = None): """ - Clears all stored data and resets the transformation matrix to its default state. + Clear calibration data and reset transformation matrices. + + Args: + sn (str, optional): The serial number of the stage to clear. If None, clears all stages. """ self.model_LR, self.transM_LR, self.transM_LR_prev = None, None, None self.scale = np.array([1, 1, 1]) @@ -149,7 +158,15 @@ def clear(self, sn = None): self.model.add_transform(sn, self.transM_LR, self.scale) def _remove_duplicates(self, df): - # Drop duplicate rows based on 'ts_local_coords', 'global_x', 'global_y', 'global_z' columns + """ + Remove duplicate entries from a DataFrame based on unique local and global coordinates. + + Args: + df (pd.DataFrame): The DataFrame containing the calibration points. + + Returns: + pd.DataFrame: The DataFrame without duplicates. + """ logger.debug(f"Original rows: {self.df.shape[0]}") df.drop_duplicates(subset=['sn', 'ts_local_coords', 'global_x', 'global_y', 'global_z']) logger.debug(f"Unique rows: {self.df.shape[0]}") @@ -157,16 +174,27 @@ def _remove_duplicates(self, df): return df def _filter_df_by_sn(self, sn): + """ + Filters the calibration points in the CSV file by stage serial number. + + Args: + sn (str): The serial number of the stage. + + Returns: + pd.DataFrame: Filtered DataFrame containing only the rows for the specified stage. + """ self.df = pd.read_csv(self.csv_file) - return self.df[self.df["sn"] == sn] def _get_local_global_points(self, df): """ - Retrieves local and global points from the CSV file as numpy arrays. + Retrieve local and global points from the DataFrame. + + Args: + df (pd.DataFrame): The DataFrame containing the points. Returns: - tuple: A tuple containing arrays of local points and global points. + tuple: Arrays of local points and global points. """ # Extract local and global points local_points = df[["local_x", "local_y", "local_z"]].values @@ -176,10 +204,10 @@ def _get_local_global_points(self, df): def _get_df(self): """ - Retrieves local and global points from the CSV file as numpy arrays. + Retrieve the CSV file data and filter it by the current stage. Returns: - tuple: A tuple containing arrays of local points and global points. + pd.DataFrame: Filtered DataFrame for the current stage. """ self.df = pd.read_csv(self.csv_file) # Filter the DataFrame based on self.stage.sn @@ -188,6 +216,16 @@ def _get_df(self): return filtered_df def _get_l2_distance(self, local_points, global_points): + """ + Compute the L2 distance between the expected global points and the actual global points. + + Args: + local_points (numpy.ndarray): The local points. + global_points (numpy.ndarray): The global points. + + Returns: + numpy.ndarray: The L2 distance between the points. + """ R, t, s = self.R, self.origin, self.scale # Apply the scaling factors obtained from fit_params @@ -204,6 +242,17 @@ def _get_l2_distance(self, local_points, global_points): return l2_distance def _remove_outliers(self, local_points, global_points, threshold=30): + """ + Remove outliers based on L2 distance threshold. + + Args: + local_points (numpy.ndarray): The local points. + global_points (numpy.ndarray): The global points. + threshold (float): The L2 distance threshold for outlier removal. + + Returns: + tuple: Filtered local points, global points, and valid indices. + """ # Get the l2 distance l2_distance = self._get_l2_distance(local_points, global_points) @@ -227,9 +276,11 @@ def _remove_outliers(self, local_points, global_points, threshold=30): def _get_transM_LR_orthogonal(self, local_points, global_points, remove_noise=True): """ Computes the transformation matrix from local to global coordinates using orthogonal distance regression. + Args: local_points (np.array): Array of local points. global_points (np.array): Array of global points. + Returns: tuple: Linear regression model and transformation matrix. """ @@ -249,7 +300,27 @@ def _get_transM_LR_orthogonal(self, local_points, global_points, remove_noise=Tr return transformation_matrix def _get_transM(self, df, remove_noise=True, save_to_csv=False, file_name=None, noise_threshold=40): + """ + Computes the transformation matrix from local coordinates (stage) to global coordinates (reticle). + + Args: + df (pd.DataFrame): DataFrame containing local and global points. + remove_noise (bool, optional): Whether to remove noisy points based on an L2 distance threshold. Defaults to True. + save_to_csv (bool, optional): Whether to save the filtered points to a CSV file. Defaults to False. + file_name (str, optional): The name of the file to save the filtered points. Required if `save_to_csv` is True. + noise_threshold (int, optional): The threshold for filtering out noisy points based on L2 distance. Defaults to 40. + + Returns: + np.ndarray: A 4x4 transformation matrix if successful, or None if there are insufficient points for calibration. + Workflow: + 1. Retrieves local and global points from the DataFrame. + 2. Optionally removes noisy points based on L2 distance if `remove_noise` is enabled. + 3. If there are fewer than 3 local or global points, it logs a warning and returns None. + 4. Uses the `fit_params` method from the `RotationTransformation` class to compute the origin, rotation matrix, and scaling factors. + 5. Constructs the 4x4 transformation matrix using the rotation matrix and translation vector. + 6. Optionally saves the filtered points to a CSV file if `save_to_csv` is True. + """ local_points, global_points = self._get_local_global_points(df) valid_indices = np.ones(len(local_points), dtype=bool) # Initialize valid_indices as a mask with all True values @@ -341,12 +412,24 @@ def _is_criteria_met_transM(self): return False def _is_criteria_avg_error_threshold(self): + """ + Checks if the average error is below the defined threshold. + + Returns: + bool: True if the average error is below the threshold, otherwise False. + """ if self.avg_err < self.threshold_avg_error: return True else: return False def _update_min_max_x_y_z(self): + """ + Updates the minimum and maximum x, y, z coordinates for the current stage. + + This method tracks the range of movement for the x, y, and z axes for a given stage + and updates the corresponding minimum and maximum values. + """ sn = self.stage.sn if sn not in self.stages: self.stages[sn] = { @@ -365,6 +448,15 @@ def _update_min_max_x_y_z(self): self.stages[sn]['max_z'] = max(self.stages[sn]['max_z'], self.stage.stage_z) def _is_criteria_met_points_min_max(self): + """ + Checks if the stage movement has exceeded predefined thresholds for x, y, and z axes. + + If any of the axis ranges exceed the threshold, it emits the appropriate signal indicating + calibration is complete for that axis. + + Returns: + bool: True if all axis movements exceed their respective thresholds, otherwise False. + """ sn = self.stage.sn if sn is not None and sn in self.stages: stage_data = self.stages[sn] @@ -401,6 +493,12 @@ def _apply_transformation(self): return global_point[:3] def _l2_error_current_point(self): + """ + Computes the L2 error between the transformed local point and the global point. + + Returns: + float: The L2 error. + """ transformed_point = self._apply_transformation() global_point = np.array( [ @@ -456,10 +554,16 @@ def _enough_points_emit_signal(self): self.stages[sn] = stage_data def _is_enough_points(self): - """Check if there are enough points for calibration. + """ + Determines whether enough points have been collected for calibration. + + The criteria include: + - Minimum range of movement in x, y, z directions. + - Acceptable L2 error between local and global points. + - Stable transformation matrix across iterations. Returns: - bool: True if there are enough points, False otherwise. + bool: True if enough points have been collected for calibration, otherwise False. """ # End Criteria: # 1. distance maxX-minX, maxY-minY, maxZ-minZ @@ -477,6 +581,14 @@ def _is_enough_points(self): return False def _update_info_ui(self, disp_avg_error=False, save_to_csv=False, file_name=None): + """ + Updates the UI with calibration information, such as transformation matrix, scale, and error. + + Args: + disp_avg_error (bool): Whether to display the average error or the L2 error. + save_to_csv (bool): Whether to save the information to a CSV file. + file_name (str, optional): The name of the CSV file to save to. + """ sn = self.stage.sn if sn is not None and sn in self.stages: stage_data = self.stages[sn] @@ -559,6 +671,31 @@ def reshape_array(self): return local_points.reshape(-1, 1, 3), global_points.reshape(-1, 1, 3) def _print_formatted_transM(self): + """ + Prints the transformation matrix in a formatted way, including the rotation matrix, + translation vector, and scale factors. This helps visualize the relationship + between local stage coordinates and global reticle coordinates after calibration. + + The output includes: + - The rotation matrix (R): Describes the orientation transformation. + - The translation vector (T): Represents the offset in global coordinates. + - The scaling factors (S): Represent the scaling applied along the x, y, and z local coordinates. + - The average L2 error between stage and global coordinates. + + This function outputs the results to the console. + + Example output: + stage sn: + Rotation matrix: + [[, , ], + [, , ], + [, , ]] + Translation vector: + [, , ] + Scale: + [, , ] + ==> Average L2 between stage and global: + """ R = self.transM_LR[:3, :3] # Extract the translation vector (top 3 elements of the last column) T = self.transM_LR[:3, 3] @@ -602,6 +739,21 @@ def update(self, stage, debug_info=None): self.complete_calibration(filtered_df) def complete_calibration(self, filtered_df): + """ + Completes the probe calibration process by saving the filtered points, updating the + transformation matrix, and applying bundle adjustment if necessary. + + Args: + filtered_df (pd.DataFrame): A DataFrame containing filtered local and global points. + + Workflow: + 1. Saves the filtered points to a new CSV file. + 2. Updates the transformation matrix based on the filtered points. + 3. If bundle adjustment is enabled, optimizes the transformation matrix. + 4. Registers the transformation matrix and scaling factors into the model. + 5. Emits a signal indicating that calibration is complete. + 6. Initializes the PointMesh instance for 3D visualization. + """ # save the filtered points to a new file print("ProbeCalibration: complete_calibration") timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") @@ -650,6 +802,16 @@ def complete_calibration(self, filtered_df): self.stages[self.stage.sn]['calib_completed'] = True def view_3d_trajectory(self, sn): + """ + Displays the 3D trajectory of the probe based on the calibration data. + + Args: + sn (str): Serial number of the stage for which the trajectory is to be displayed. + + Behavior: + - If calibration is incomplete, it shows the trajectory for the current stage. + - If calibration is complete, it displays the PointMesh instance for the 3D trajectory. + """ if not self.stages.get(sn, {}).get('calib_completed', False): if sn == self.stage.sn: if self.transM_LR is None: @@ -663,6 +825,21 @@ def view_3d_trajectory(self, sn): self.point_mesh[sn].show() def run_bundle_adjustment(self, file_path): + """ + Runs bundle adjustment to optimize the 3D points and camera parameters for better calibration accuracy. + + Args: + file_path (str): Path to the CSV file containing the initial local and global points. + + Returns: + bool: True if bundle adjustment was successful, False otherwise. + + Workflow: + 1. Initializes a BALProblem with the provided file data. + 2. Runs the optimization using BALOptimizer. + 3. Retrieves the optimized points and updates the transformation matrix. + 4. Logs the results of the bundle adjustment. + """ bal_problem = BALProblem(self.model, file_path) optimizer = BALOptimizer(bal_problem) optimizer.optimize() diff --git a/parallax/probe_detect_manager.py b/parallax/probe_detect_manager.py index e67eb72..b1e4c34 100644 --- a/parallax/probe_detect_manager.py +++ b/parallax/probe_detect_manager.py @@ -25,21 +25,32 @@ logging.getLogger("PyQt5.uic.properties").setLevel(logging.WARNING) class ProbeDetectManager(QObject): - """Manager class for probe detection.""" - + """ + Manager class for probe detection. It handles frame processing, probe detection, + reticle zone detection, and result communication through signals. + """ name = "None" frame_processed = pyqtSignal(object) found_coords = pyqtSignal(str, str, tuple, tuple) class Worker(QObject): - """Worker class for probe detection.""" - + """ + Worker class for performing probe detection in a separate thread. This class handles + image processing, probe detection, and reticle detection, and communicates results + through PyQt signals. + """ finished = pyqtSignal() frame_processed = pyqtSignal(object) found_coords = pyqtSignal(str, str, tuple) def __init__(self, name, model): - """Initialize Worker object""" + """ + Initialize the Worker object with camera and model data. + + Args: + name (str): Camera serial number. + model (object): The main model containing stage and camera data. + """ QObject.__init__(self) self.model = model self.name = name # Camera serial number @@ -61,7 +72,7 @@ def __init__(self, name, model): self.sn = None self.IMG_SIZE = (1000, 750) - self.IMG_SIZE_ORIGINAL = (4000, 3000) # TODO + self.IMG_SIZE_ORIGINAL = (4000, 3000) self.CROP_INIT = 50 self.mask_detect = MaskGenerator() @@ -221,12 +232,23 @@ def stop_detection(self): self.is_detection_on = False def enable_calib(self): + """Enable calibration mode.""" self.is_calib = True def disable_calib(self): + """Disable calibration mode.""" self.is_calib = False def process_draw_reticle(self, frame): + """ + Draw reticle coordinates on the frame for visualization. + + Args: + frame (numpy.ndarray): Input frame. + + Returns: + numpy.ndarray: Frame with reticle coordinates drawn. + """ if self.reticle_coords is not None: for coords in self.reticle_coords: for point_idx, (x, y) in enumerate(coords): @@ -241,6 +263,7 @@ def process_draw_reticle(self, frame): return frame def register_colormap(self): + """Register a colormap for visualizing reticle coordinates.""" if self.reticle_coords is not None: for idx, coords in enumerate(self.reticle_coords): # Normalize indices to 0-255 for colormap application. @@ -282,6 +305,20 @@ def set_name(self, name): self.reticle_coords_debug = self.model.get_coords_for_debug(self.name) def debug_draw_boundary(self, frame, is_first_detect, ret_crop, ret_tip, is_curr_prev_comp, is_curr_bg_comp): + """ + Draw debug boundaries and detection results on the frame. + + Args: + frame (numpy.ndarray): The input frame where boundaries will be drawn. + is_first_detect (bool): Whether this is the first detection attempt. + ret_crop (bool): Whether the crop region detection was successful. + ret_tip (bool): Whether the fine tip detection was successful. + is_curr_prev_comp (bool): Whether current-previous frame comparison succeeded. + is_curr_bg_comp (bool): Whether current-background frame comparison succeeded. + + Returns: + numpy.ndarray: Frame with boundary rectangles and other debug information drawn. + """ # Display text on the frame if is_first_detect: text = "first detection" @@ -346,7 +383,13 @@ def debug_draw_boundary(self, frame, is_first_detect, ret_crop, ret_tip, is_curr return frame def __init__(self, model, camera_name): - """Initialize ProbeDetectManager object""" + """ + Initialize the ProbeDetectManager object. + + Args: + model (object): The main model containing stage and camera data. + camera_name (str): Name of the camera being managed for probe detection. + """ super().__init__() self.model = model self.worker = None @@ -354,7 +397,9 @@ def __init__(self, model, camera_name): self.thread = None def init_thread(self): - """Initialize the worker thread.""" + """ + Initialize the worker thread and set up signal connections. + """ if self.thread is not None: self.clean() # Clean up existing thread and worker before reinitializing self.thread = QThread() @@ -374,7 +419,8 @@ def init_thread(self): logger.debug(f"{self.name} init camera name") def process(self, frame, timestamp): - """Process the frame using the worker. + """ + Process the frame using the worker. Args: frame (numpy.ndarray): Input frame. @@ -384,12 +430,13 @@ def process(self, frame, timestamp): self.worker.update_frame(frame, timestamp) def found_coords_print(self, timestamp, sn, pixel_coords): - """Emit the found coordinates signal. + """ + Emit the found coordinates signal after detection. Args: timestamp (str): Timestamp of the frame. - sn (str): Serial number. - pixel_coords (tuple): Pixel coordinates of the probe tip. + sn (str): Serial number of the device. + pixel_coords (tuple): Pixel coordinates of the detected probe tip. """ moving_stage = self.model.get_stage(sn) if moving_stage is not None: @@ -398,28 +445,35 @@ def found_coords_print(self, timestamp, sn, pixel_coords): moving_stage.stage_y, moving_stage.stage_z, ) - # print(timestamp, sn, stage_info, pixel_coords) self.found_coords.emit(timestamp, sn, stage_info, pixel_coords) def start(self): - """Start the probe detection manager.""" + """ + Start the probe detection manager by initializing the worker thread and running it. + """ logger.debug(f" {self.name} Starting thread") self.init_thread() # Reinitialize and start the worker and thread self.worker.start_running() self.thread.start() def stop(self): - """Stop the probe detection manager.""" + """ + Stop the probe detection manager by halting the worker thread. + """ logger.debug(f" {self.name} Stopping thread") if self.worker is not None: self.worker.stop_running() def onWorkerDestroyed(self): - """Cleanup after worker finishes.""" + """ + Cleanup function to handle when the worker is destroyed. + """ logger.debug(f"{self.name} worker destroyed") def onThreadDestroyed(self): - """Flag if thread is deleted""" + """ + Callback function when the thread is destroyed. + """ logger.debug(f"{self.name} thread destroyed") self.threadDeleted = True self.thread = None @@ -444,22 +498,41 @@ def stop_detection(self, sn): # Call from stage listener. self.worker.stop_detection() def enable_calibration(self, sn): # Call from stage listener. + """ + Enable calibration mode for the worker. + + Args: + sn (str): Serial number of the device. + """ if self.worker is not None: self.worker.enable_calib() def disable_calibration(self, sn): # Call from stage listener. + """ + Disable calibration mode for the worker. + + Args: + sn (str): Serial number of the device. + """ if self.worker is not None: self.worker.disable_calib() def set_name(self, camera_name): - """Set camera name.""" + """ + Set the camera name for the worker. + + Args: + camera_name (str): Name of the camera. + """ self.name = camera_name if self.worker is not None: self.worker.set_name(self.name) logger.debug(f"{self.name} set camera name") def clean(self): - """Clean up the probe detection manager.""" + """ + Clean up the worker and thread resources. + """ logger.debug(f"{self.name} Cleaning the thread") if self.worker is not None: self.worker.stop_running() @@ -472,5 +545,7 @@ def clean(self): logger.debug(f"{self.name} Cleaned the thread") def __del__(self): - """Destructor for the probe detection manager.""" + """ + Destructor to ensure proper cleanup when the object is deleted. + """ self.clean() \ No newline at end of file diff --git a/parallax/probe_detector.py b/parallax/probe_detector.py index 586ae1c..3c073cf 100644 --- a/parallax/probe_detector.py +++ b/parallax/probe_detector.py @@ -45,6 +45,7 @@ def _init_gradient_bins(self): def _find_represent_gradient(self, gradient=0): """Find the representative gradient. + Returns: float: Representative gradient value. """ @@ -53,8 +54,10 @@ def _find_represent_gradient(self, gradient=0): def _find_neighboring_gradients(self, target_angle): """Find the neighboring gradients. + Args: target_angle (float): Target angle. + Returns: numpy.ndarray: Neighboring gradients. """ @@ -68,11 +71,13 @@ def _contour_preprocessing( self, img, thresh=20, remove_noise=True, noise_threshold=1 ): """Preprocess the image using contour detection. + Args: img (numpy.ndarray): Input image. thresh (int): Threshold for contour area. Defaults to 20. remove_noise (bool): Flag to remove noise contours. Defaults to True. noise_threshold (int): Threshold for noise contour area. Defaults to 1. + Returns: numpy.ndarray: Preprocessed image. """ @@ -98,9 +103,11 @@ def _contour_preprocessing( def _get_probe_direction(self, probe_tip, probe_base): """Get the direction of the probe. + Args: probe_tip (tuple): Coordinates of the probe tip. probe_base (tuple): Coordinates of the probe base. + Returns: str: Direction of the probe (N, NE, E, SE, S, SW, W, NW, Unknown). """ @@ -204,6 +211,7 @@ def _hough_line_first_detection( def _hough_line_update(self, img, minLineLength=50, maxLineGap=9): """Update the Hough line detection. + Args: img (numpy.ndarray): Input image. minLineLength (int): Minimum length of the line. Defaults to 50. @@ -322,13 +330,6 @@ def _get_probe_point(self, mask, p1, p2, img_fname=None): mask = cv2.copyMakeBorder( mask, 1, 1, 1, 1, cv2.BORDER_CONSTANT, value=[0, 0, 0] ) - """ - # TODO Draw a border inside the mask with the specified thickness - border_size = 200 - cv2.rectangle(mask, (border_size, border_size), - (mask.shape[1] - border_size - 1, mask.shape[0] - border_size - 1), - 0, border_size) - """ dist_transform = cv2.distanceTransform(mask, cv2.DIST_L2, 3) dist_p1 = dist_transform[p1[1], p1[0]] # [y, x] diff --git a/parallax/probe_fine_tip_detector.py b/parallax/probe_fine_tip_detector.py index b3b9ec5..8507e4b 100644 --- a/parallax/probe_fine_tip_detector.py +++ b/parallax/probe_fine_tip_detector.py @@ -1,8 +1,19 @@ +""" +Module for detecting the fine tip of a probe in an image. + +This module includes the `ProbeFineTipDetector` class, which provides several methods for detecting +the fine tip of a probe based on image processing techniques. The class preprocesses the image, +validates the input, detects the closest centroid for tip detection, and refines the detected tip +by applying an offset to ensure accuracy. + +Logging is used to track the progress of tip detection, and debug images can be saved when logging +is set to DEBUG level. +""" + import logging import os import cv2 import numpy as np -from datetime import datetime # Set logger name logger = logging.getLogger(__name__) @@ -31,6 +42,7 @@ def _preprocess_image(cls, img): @classmethod def _is_valid(cls, img): """Check if the image is valid for tip detection. + Returns: bool: True if the image is valid, False otherwise. """ @@ -46,13 +58,7 @@ def _is_valid(cls, img): f"get_probe_precise_tip fail. N of contours_boundary :{len(contours_boundary)}" ) return False - """ - boundary_img = np.zeros_like(img) - boundary_img[0, 0] = 255 - boundary_img[width - 1, 0] = 255 - boundary_img[0, height - 1] = 255 - boundary_img[width - 1, height - 1] = 255 - """ + boundary_img[0, 0] = 255 boundary_img[0, width - 1] = 255 boundary_img[height - 1, 0] = 255 diff --git a/parallax/reticle_detect_manager.py b/parallax/reticle_detect_manager.py index ddfbb11..06170a2 100644 --- a/parallax/reticle_detect_manager.py +++ b/parallax/reticle_detect_manager.py @@ -47,7 +47,7 @@ def __init__(self, name): self.is_detection_on = False self.new = False self.frame = None - self.IMG_SIZE_ORIGINAL = (4000, 3000) # TODO + self.IMG_SIZE_ORIGINAL = (4000, 3000) self.frame_success = None self.mask_detect = MaskGenerator(initial_detect = True) @@ -176,9 +176,6 @@ def process(self, frame): if not ret: logger.debug(f"{ self.name} get_coords_interest fails ") else: - # TODO - #ret, mtx, dist = self.calibrationCamera.get_predefined_intrinsic(x_axis_coords, y_axis_coords) - #if not ret: ret, mtx, dist, rvecs, tvecs = self.calibrationCamera.calibrate_camera( x_axis_coords, y_axis_coords ) diff --git a/parallax/reticle_detection.py b/parallax/reticle_detection.py index 6f90cd2..14bc104 100644 --- a/parallax/reticle_detection.py +++ b/parallax/reticle_detection.py @@ -151,7 +151,7 @@ def _ransac_detect_lines(self, img): residual_threshold += 1 continue - # Draw the centroids + # Draw the centroids for debug """ for points in inlier_pixels: for point in points: @@ -234,8 +234,10 @@ def _get_pixels_interest(self, center, coords, dist=10): def _find_reticle_coords(self, pixels_in_lines): """Find the reticle coordinates from the pixels in lines. + Args: pixels_in_lines (list): List of pixel coordinates for each line. + Returns: list: List of pixel coordinates of interest for each line. """ @@ -298,11 +300,6 @@ def _get_centroid(self, contours): centroids.append([cX, cY]) return centroids - def __del__(self): - """Delete the instance""" - # print("ReticleDetection Object destroyed") - pass - def _get_median_distance_x_y(self, points): """Get the median distance for x and y components of the points. @@ -330,7 +327,6 @@ def _sort_points(self, points): Returns: numpy.ndarray: Sorted points. """ - median_x_diff, median_y_diff = self._get_median_distance_x_y(points) # Determine which dimension has greater median distance @@ -379,13 +375,6 @@ def _estimate_missing_points(self, points, threshold_factor=1.5): ) missing_points.append(np.round(missing_point)) - """ - logger.debug(f"start_point: {start_point},\ - end_point: {end_point},\ - num_missing: {num_missing},\ - Missing points Interpolated: {missing_points}") - """ - return np.array(missing_points) def _add_missing_pixels(self, bg, lines, line_pixels): @@ -541,7 +530,6 @@ def coords_detect_morph(self, img): img = cv2.medianBlur(img, 5) img = cv2.bitwise_not(img, mask=self.mask) kernel_ellipse_5 = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) - kernel_ellipse_3 = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)) img = cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel_ellipse_5) img = self._eroding(img) @@ -559,6 +547,17 @@ def coords_detect_morph(self, img): return ret, img, inliner_lines, inliner_lines_pixels def _draw_debug(self, img, pixels_in_lines, filename): + """ + Draw debug visualization of detected reticle lines and save the image to a file. + + This method draws circles at the pixel coordinates of the detected lines for debugging purposes. + If the logging level is set to DEBUG, the processed image is saved as a file. + + Args: + img (numpy.ndarray): The input image. + pixels_in_lines (list): List of pixel coordinates for the detected lines. + filename (str): The name of the file to save the debug image. + """ if logger.getEffectiveLevel() == logging.DEBUG: if img.ndim == 2: img_ = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR) @@ -606,14 +605,4 @@ def get_coords(self, img): logger.debug(f"{self.name} interpolate: {len(pixels_in_lines[0])} {len(pixels_in_lines[1])}") self._draw_debug(bg, pixels_in_lines, "4_add_missing_pixels") - return ret, bg, inliner_lines, pixels_in_lines - - """ - def is_distance_tip_reticle_threshold(self, probe, reticle, thresh=10): - tip_x, tip_y = probe["tip_coords"] - dist_transform = cv2.distanceTransform(reticle, cv2.DIST_L2, cv2.DIST_MASK_PRECISE) - # Get the distance at the tip location - distance_at_tip = dist_transform[int(tip_y), int(tip_x)] - print("distance_at_tip: ", distance_at_tip) - return distance_at_tip > thresh - """ + return ret, bg, inliner_lines, pixels_in_lines \ No newline at end of file diff --git a/parallax/reticle_metadata.py b/parallax/reticle_metadata.py index 041824f..366f6b3 100644 --- a/parallax/reticle_metadata.py +++ b/parallax/reticle_metadata.py @@ -1,3 +1,18 @@ +""" +This module provides a ReticleMetadata widget for managing and visualizing reticle metadata +in a microscopy setup. The widget allows users to dynamically create, modify, and delete +reticle information. The metadata includes rotation, offsets, and names, and is saved to a +JSON file for persistence. The reticles can be displayed as group boxes in a PyQt UI, and +are associated with their respective metadata such as rotation and offsets. + +Key features: +- Add, update, and remove reticles with alphabetically assigned names. +- Save reticle metadata to a JSON file and load it dynamically. +- Dynamically update reticle rotation, offsets, and other parameters in the UI. +- Manage reticles using QGroupBox objects and connect them to a reticle selector dropdown menu. +- Provides methods for retrieving global coordinates with specific reticle adjustments. +""" + import os import logging import json @@ -28,6 +43,15 @@ class ReticleMetadata(QWidget): - Interact with a reticle selector to display the reticle choices in a dropdown. """ def __init__(self, model, reticle_selector): + """ + Initializes the ReticleMetadata widget. The widget allows users to manage + reticle metadata, including dynamically creating groupboxes for each reticle + and handling the interaction with a reticle selector dropdown. + + Args: + model (object): The main application model that holds reticle data. + reticle_selector (QComboBox): The reticle selector dropdown menu where reticles will be listed. + """ super().__init__() self.model = model self.reticle_selector = reticle_selector @@ -48,6 +72,17 @@ def __init__(self, model, reticle_selector): self.model.add_reticle_metadata_instance(self) def load_metadata_from_file(self): + """ + Load reticle metadata from a JSON file and populate the UI. + + This method attempts to read the reticle metadata from a JSON file. If the file does not exist, + it logs a message and starts fresh with no preloaded data. If the file exists and contains valid + data, it creates reticle group boxes based on the metadata, updates the internal reticle structure, + and refreshes the reticle selector dropdown. + + Raises: + Exception: If there is an error reading the metadata file, logs the error. + """ 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.") @@ -91,7 +126,13 @@ def _add_groupbox(self): self._populate_groupbox(alphabet, reticle_info) def _populate_groupbox(self, name, reticle_info): - """Helper method to set up a groupbox.""" + """ + Helper method to set up a groupbox for a reticle with the given name and reticle info. + + Args: + name (str): The name of the reticle (typically a single letter). + reticle_info (dict): A dictionary containing metadata for the reticle. + """ group_box = QGroupBox(self) loadUi(os.path.join(ui_dir, "reticle_QGroupBox.ui"), group_box) @@ -133,7 +174,14 @@ def _populate_groupbox(self, name, reticle_info): 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.""" + """ + Update the title and object name of the group box when the reticle name is changed. + + Args: + group_box (QGroupBox): The QGroupBox representing the reticle. + new_name (str): The new name for the reticle. + alphabet (str): The original alphabet used for the reticle. + """ if alphabet == group_box.objectName(): self.alphabet_status[alphabet] = 0 @@ -146,6 +194,12 @@ def _update_groupbox_name(self, group_box, new_name, alphabet): self.alphabet_status[new_name] = 1 def _remove_specific_groupbox(self, group_box): + """ + Remove a specific reticle groupbox from the layout and metadata. + + Args: + group_box (QGroupBox): The groupbox to remove. + """ name = group_box.findChild(QLineEdit, "lineEditName").text() if name in self.groupboxes.keys(): @@ -164,18 +218,30 @@ def _remove_specific_groupbox(self, group_box): self.alphabet_status[name.upper()] = 0 def _find_next_available_alphabet(self): + """ + Find the next available alphabet letter for naming a new reticle. + + Returns: + str or None: The next available alphabet letter, or None if all letters are taken. + """ for alphabet, status in self.alphabet_status.items(): if status == 0: return alphabet return None def _update_reticle_info(self): + """ + Update reticle information in the UI and save it to the metadata file. + """ 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): + """ + Update the reticle selector dropdown with the latest reticle names. + """ self.reticle_selector.clear() self.reticle_selector.addItem(f"Global coords") @@ -189,6 +255,12 @@ def _update_to_reticle_selector(self): self.reticle_selector.addItem(f"Proj Global coords ({name})") def default_reticle_selector(self, reticle_detection_status): + """ + Reset the reticle selector to its default state and clear all reticles. + + Args: + reticle_detection_status (str): Status of reticle detection to determine which options to add. + """ # 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) @@ -208,6 +280,12 @@ def default_reticle_selector(self, reticle_detection_status): self.reticle_selector.addItem(f"Proj Global coords") def _update_to_file(self): + """ + Save the current reticle information to the metadata JSON file. + + Raises: + ValueError: If there are empty fields or duplicate reticle names. + """ reticle_info_list = [] names_seen = set() duplicates = False @@ -265,6 +343,15 @@ def _update_to_file(self): print(f"Error saving file: {e}") def _is_valid_number(self, value): + """ + Validate if a string value is a valid number. + + Args: + value (str): The string to validate. + + Returns: + bool: True if the value is a valid number, False otherwise. + """ try: float(value) return True @@ -272,6 +359,12 @@ def _is_valid_number(self, value): return False def _update_reticles(self, group_box): + """ + Update the reticle information stored in the `self.reticles` dictionary based on user input. + + Args: + group_box (QGroupBox): The QGroupBox containing the reticle data. + """ if not group_box: print(f"Error: No groupbox found for reticle '{group_box}'.") return @@ -310,6 +403,16 @@ def _update_reticles(self, group_box): self.model.add_reticle_metadata(name, self.reticles[name]) def get_global_coords_with_offset(self, reticle_name, global_pts): + """ + Get the global coordinates of a point after applying the reticle's rotation and offsets. + + Args: + reticle_name (str): The name of the reticle. + global_pts (numpy.ndarray): The original global coordinates (3D point). + + Returns: + tuple: The transformed global coordinates (global_x, global_y, global_z). + """ if reticle_name not in self.reticles.keys(): raise ValueError(f"Reticle '{reticle_name}' not found in reticles dictionary.") diff --git a/parallax/screen_coords_mapper.py b/parallax/screen_coords_mapper.py index 26824ab..675135a 100644 --- a/parallax/screen_coords_mapper.py +++ b/parallax/screen_coords_mapper.py @@ -1,3 +1,18 @@ +""" +This module defines the ScreenCoordsMapper class, which is responsible for converting +clicked screen coordinates from a camera view into corresponding global coordinates. +It handles the interaction with stereo calibration or bundle adjustment (BA) to +calculate the global position of clicked points on screen widgets, and applies +reticle-specific adjustments if needed. + +The ScreenCoordsMapper class includes functionality to: +- Register clicked positions on camera screen widgets. +- Calculate global coordinates based on stereo calibration or bundle adjustment. +- Apply reticle-specific metadata, such as rotation and offset, to adjust the global coordinates. +- Update the UI fields with the calculated global coordinates. +- Manage interaction between screen widgets, the main model, and a reticle selector dropdown. +""" + import logging import numpy as np diff --git a/parallax/screen_widget.py b/parallax/screen_widget.py index 836f337..335a99d 100755 --- a/parallax/screen_widget.py +++ b/parallax/screen_widget.py @@ -1,4 +1,3 @@ - """ Provides ScreenWidget for image interaction in microscopy apps, supporting image display, point selection, and zooming. It integrates with probe and reticle detection managers @@ -6,7 +5,6 @@ """ import logging - import cv2 import pyqtgraph as pg from PyQt5 import QtCore @@ -24,7 +22,6 @@ logging.getLogger("PyQt5.uic.uiparser").setLevel(logging.WARNING) logging.getLogger("PyQt5.uic.properties").setLevel(logging.WARNING) - class ScreenWidget(pg.GraphicsView): """Screens Class""" @@ -332,7 +329,6 @@ def run_probe_detection(self): def run_no_filter(self): """Run without any filter by stopping the reticle detector and probe detector.""" - self.reticleDetector.stop() self.probeDetector.stop() self.axisFilter.stop() diff --git a/parallax/stage_controller.py b/parallax/stage_controller.py index 0a0118c..ad0fa7d 100644 --- a/parallax/stage_controller.py +++ b/parallax/stage_controller.py @@ -1,3 +1,19 @@ +""" +This module provides the `StageController` class for managing the movement and control +of stages (probes) used in microscopy instruments. The class interacts with an external +stage controller system via HTTP requests to move the stages, stop them, and retrieve +their status. + +The key functionalities include: +- Stopping the movement of all stages (probes). +- Moving stages along the X, Y, and Z axes, with specific coordination of Z-axis movement before X and Y movements. +- Handling requests to update the stage positions dynamically. +- Sending and receiving data over HTTP to an external stage controller software. + +Classes: + StageController: Manages the stage movement, status retrieval, and interaction with + external systems through HTTP requests. +""" import logging import requests import json @@ -85,14 +101,6 @@ def move_request(self, command): Args: command (dict): A dictionary containing the stage serial number, move type, and coordinates. - Example: - { - "stage_sn": stage_sn, - "move_type": "moveXY", - "x": x, - "y": y, - "z": z - } """ move_type = command["move_type"] stage_sn = command["stage_sn"] diff --git a/parallax/stage_listener.py b/parallax/stage_listener.py index 6ca5d8f..b236808 100644 --- a/parallax/stage_listener.py +++ b/parallax/stage_listener.py @@ -90,7 +90,7 @@ def __init__(self, url): self.last_bigmove_stage_info = None self.last_bigmove_detected_time = None self._low_freq_interval = 1000 - self._high_freq_interval = 20 # TODO + self._high_freq_interval = 20 self.curr_interval = self._low_freq_interval self._idle_time = 0.3 # 0.3s self.is_error_log_printed = False @@ -308,12 +308,10 @@ def _updateGlobalDataTransformM(self, sn, moving_stage, transM, scale): Args: sn (str): The serial number of the moving stage. moving_stage (Stage): An object representing the moving stage, with attributes for its local and global coordinates. - transM (np.ndarray): A 4x4 numpy array representing the transformation matrix used to convert local coordinates - to global coordinates. + transM (np.ndarray): A 4x4 numpy array representing the transformation matrix used to convert local coordinates to global coordinates. Effects: - - Updates the moving_stage object's `stage_x_global`, `stage_y_global`, and `stage_z_global` attributes with the - transformed global coordinates. + - Updates the moving_stage object's `stage_x_global`, `stage_y_global`, and `stage_z_global` attributes with the transformed global coordinates. - If the moving stage is the currently selected stage in the UI, triggers an update of the global coordinates display. """ # Transform diff --git a/parallax/stage_ui.py b/parallax/stage_ui.py index 967290e..a750d85 100644 --- a/parallax/stage_ui.py +++ b/parallax/stage_ui.py @@ -64,7 +64,17 @@ def get_current_stage_id(self): return stage_id def update_stage_widget(self, prev_stage_id, curr_stage_id): - # signal + """ + Emit a signal to notify other widgets or components about a change in the selected stage. + + This method emits the `prev_curr_stages` signal, passing the previous and current stage IDs + to allow other components (like a stage widget) to update their displayed information + based on the selected stage change. + + Args: + prev_stage_id (str): The ID of the previously selected stage. + curr_stage_id (str): The ID of the currently selected stage. + """ self.prev_curr_stages.emit(prev_stage_id, curr_stage_id) def sendInfoToStageWidget(self): @@ -93,11 +103,30 @@ def updateStageLocalCoords(self): self.ui.local_coords_z.setText(str(self.selected_stage.stage_z)) def updateCurrentReticle(self): + """ + Update the currently selected reticle and refresh the global coordinates display. + + This method calls `setCurrentReticle` to update the currently selected reticle based on + the user's selection from the reticle dropdown. If the reticle is successfully updated, + it refreshes the displayed global coordinates for the selected stage using + `updateStageGlobalCoords`. + """ ret = self.setCurrentReticle() if ret: self.updateStageGlobalCoords() def setCurrentReticle(self): + """ + Set the current reticle based on the user's selection in the reticle dropdown. + + This method retrieves the selected reticle from the reticle selector UI component. If the + reticle name contains "Proj", it sets the reticle to "Proj" and resets the global coordinates + display by calling `updateStageGlobalCoords_default`. Otherwise, it extracts the reticle + letter from the reticle name (e.g., "Global coords (A)") and sets it as the current reticle. + + Returns: + bool: True if a valid reticle was set, False otherwise. + """ reticle_name = self.ui.reticle_selector.currentText() if not reticle_name: return False diff --git a/parallax/stage_widget.py b/parallax/stage_widget.py index 4920d60..b934e93 100644 --- a/parallax/stage_widget.py +++ b/parallax/stage_widget.py @@ -28,15 +28,16 @@ logger.setLevel(logging.WARNING) class StageWidget(QWidget): - """Widget for stage control and calibration.""" + """A widget for stage control and calibration in a microscopy system.""" def __init__(self, model, ui_dir, screen_widgets): - """Initializes the StageWidget instance. + """ + Initializes the StageWidget instance with model, UI directory, and screen widgets. - Parameters: - model: The data model used for storing calibration and stage information. + Args: + model (object): The data model used for storing calibration and stage information. ui_dir (str): The directory path where UI files are located. - screen_widgets (list): A list of ScreenWidget instances for reticle and probe detection. + screen_widgets (list): A list of screen widgets for reticle and probe detection. """ super().__init__() self.model = model @@ -319,7 +320,6 @@ def reticle_detect_process_status(self): screen.reticle_coords_detected.connect( self.reticle_detect_two_screens ) - #TODO implement camera calib for mono camera if screen.get_camera_color_type() == "Color": screen.run_reticle_detection() self.filter = "reticle_detection" @@ -353,12 +353,35 @@ def reticle_detect_detected_status(self): ) def select_positive_x_popup_window(self): + """ + Displays a popup window instructing the user to click the positive x-axis + on each screen during the calibration process. + + This method is typically called when calibrating the positive x-axis of the reticle + or probe in a stereo setup. The user is expected to select a point along the positive + x-axis on each camera screen for accurate calibration. + + A warning message box will appear, showing the instruction to the user. + + Returns: + None + """ message = ( f"Click positive x-axis on each screen" ) QMessageBox.warning(self, "Calibration", message) def get_coords_detected_screens(self): + """ + Retrieves the list of camera names where reticle coordinates have been detected. + + This method iterates over all the available screen widgets and checks if + coordinates are detected for each camera. If coordinates are detected, + the camera name is added to the result list. + + Returns: + list: A list of camera names where reticle coordinates have been detected. + """ coords_detected_cam_name = [] for screen in self.screen_widgets: cam_name = screen.get_camera_name() @@ -369,6 +392,16 @@ def get_coords_detected_screens(self): return coords_detected_cam_name def is_positive_x_axis_detected(self): + """ + Checks whether the positive x-axis has been detected on all screens. + + This method compares the detected coordinates for each camera with the list of cameras + that have positive x-axis coordinates available. It returns True if the positive x-axis + has been detected on all cameras, otherwise returns False. + + Returns: + bool: True if the positive x-axis has been detected on all screens, False otherwise. + """ pos_x_detected_screens = [] for cam_name in self.coords_detected_screens: pos_x = self.model.get_pos_x(cam_name) @@ -379,6 +412,16 @@ def is_positive_x_axis_detected(self): return set(self.coords_detected_screens) == set(pos_x_detected_screens) def check_positive_x_axis(self): + """ + Checks for the detection of the positive x-axis on all screens and proceeds with calibration if detected. + + This method periodically checks if the positive x-axis has been detected on all screens. + If detected, the stereo calibration is initiated, reticle and probe calibration buttons + are enabled, and the UI is updated. If not yet detected, the method continues checking. + + Returns: + None + """ if self.is_positive_x_axis_detected(): self.get_pos_x_from_user_timer.stop() # Stop the timer if the positive x-axis has been detected @@ -429,7 +472,7 @@ def reticle_detect_accept_detected_status(self): logger.debug(f"2 self.filter: {self.filter}") def start_calibrate(self): - # Perform stereo calibration + """Perform stereo calibration""" result = self.calibrate_cameras() if result: self.reticleCalibrationLabel.setText( @@ -438,6 +481,18 @@ def start_calibrate(self): ) def enable_reticle_probe_calibration_buttons(self): + """ + Enables the reticle and probe calibration buttons in the UI. + + This method checks if the reticle calibration and probe calibration buttons + are disabled, and if so, enables them. This allows the user to start or continue + the reticle and probe calibration process. + + It also logs the current reticle detection status for debugging purposes. + + Returns: + None + """ # Enable reticle_calibration_btn button if not self.reticle_calibration_btn.isEnabled(): self.reticle_calibration_btn.setEnabled(True) @@ -460,6 +515,16 @@ def get_results_calibrate_stereo(self, camA, coordsA, itmxA, camB, coordsB, itmx return err, calibrationStereo, retval, R_AB, T_AB, E_AB, F_AB def get_cameras_lists(self): + """ + Retrieves a list of camera names, intrinsic parameters, and image coordinates + for each screen widget in the system. + + Returns: + tuple: A tuple containing: + - cam_names (list): List of camera names. + - intrinsics (list): List of intrinsic camera parameters. + - img_coords (list): List of reticle coordinates detected on each camera. + """ cam_names = [] intrinsics = [] img_coords = [] @@ -477,6 +542,17 @@ def get_cameras_lists(self): return cam_names, intrinsics, img_coords def calibrate_stereo(self, cam_names, intrinsics, img_coords): + """ + Performs stereo camera calibration between pairs of cameras. + + Args: + cam_names (list): List of camera names. + intrinsics (list): List of intrinsic camera parameters. + img_coords (list): List of reticle coordinates detected on each camera. + + Returns: + float: The minimum reprojection error from the stereo calibration process. + """ # Streo Camera Calibration min_err = math.inf self.calibrationStereo = None @@ -514,16 +590,23 @@ def calibrate_stereo(self, cam_names, intrinsics, img_coords): self.camA_best, self.camB_best, min_err, R_AB_best, T_AB_best, E_AB_best, F_AB_best ) - #print("\n== intrinsics ==") - #print(f" cam {self.camA_best}:\n {itmxA_best}") - #print(f" cam {self.camB_best}:\n {itmxB_best}") - #self.calibrationStereo.print_calibrate_stereo_results(self.camA_best, self.camB_best) err = self.calibrationStereo.test_performance( self.camA_best, coordsA_best, self.camB_best, coordsB_best, print_results=True ) return err def calibrate_all_cameras(self, cam_names, intrinsics, img_coords): + """ + Performs stereo calibration for all pairs of cameras, selecting the pair with the lowest error. + + Args: + cam_names (list): List of camera names. + intrinsics (list): List of intrinsic camera parameters. + img_coords (list): List of reticle coordinates detected on each camera. + + Returns: + float: The minimum reprojection error across all camera pairs. + """ min_err = math.inf # Stereo Camera Calibration calibrationStereo = None @@ -558,14 +641,26 @@ def calibrate_all_cameras(self, cam_names, intrinsics, img_coords): return min_err - # Example of how to retrieve the instance with either (camA, camB) or (camB, camA) def get_calibration_instance(self, camA, camB): + """ + Retrieves the stereo calibration instance for a given pair of cameras. + + Args: + camA (str): The first camera in the pair. + camB (str): The second camera in the pair. + + Returns: + object: The stereo calibration instance for the given camera pair, or None if not found. + """ sorted_key = tuple(sorted((camA, camB))) return self.model.get_stereo_calib_instance(sorted_key) def calibrate_cameras(self): """ Performs stereo calibration using the detected reticle positions and updates the model with the calibration data. + + Returns: + float or None: The reprojection error from the calibration, or None if calibration could not be performed. """ if len(self.model.coords_axis) < 2 and len(self.model.camera_intrinsic) < 2: return None @@ -587,10 +682,16 @@ def calibrate_cameras(self): def reticle_detect_all_screen(self): """ - Checks all screens for reticle detection results and updates the status based on whether the reticle - has been detected on all screens. + Detects the reticle coordinates on all screens for Bundle Adjustment. This method checks each screen widget + for reticle detection results and updates the detection status if the reticle has been + successfully detected on all screens. + + The method proceeds with the detection process by calling the `reticle_detect_detected_status` + method to update the UI and status. Additionally, it registers the detected reticle coordinates + and intrinsic parameters into the model using the `register_reticle_coords_intrinsic_to_model` method. + + If any screen does not have detected reticle coordinates, the method returns without further processing. """ - """Detect reticle coordinates on all screens.""" for screen in self.screen_widgets: coords = screen.get_reticle_coords() if coords is None: @@ -600,7 +701,18 @@ def reticle_detect_all_screen(self): self.register_reticle_coords_intrinsic_to_model() def reticle_detect_two_screens(self): - """Detect reticle coordinates on two screens.""" + """ + Detects the reticle coordinates on two screens for a stereo pair. This method checks each screen widget + for reticle detection results and counts the number of screens with detected reticle + coordinates. If the reticle is detected on at least two screens, it updates the UI + and detection status by calling the `reticle_detect_detected_status` method. + + After detecting the reticle on two screens, it registers the detected reticle + coordinates and intrinsic parameters into the model using the + `register_reticle_coords_intrinsic_to_model` method. + + If fewer than two screens have detected reticle coordinates, the method exits early. + """ reticle_detected_screen_cnt = 0 for screen in self.screen_widgets: coords = screen.get_reticle_coords() @@ -615,6 +727,16 @@ def reticle_detect_two_screens(self): self.register_reticle_coords_intrinsic_to_model() def register_reticle_coords_intrinsic_to_model(self): + """ + Registers the detected reticle coordinates and corresponding intrinsic camera parameters + into the model. For each screen widget, it retrieves the reticle coordinates, the intrinsic + matrix (mtx), distortion coefficients (dist), rotation vectors (rvec), and translation vectors (tvec). + + This method stores the reticle coordinates and intrinsic camera parameters in the model for + each screen where the reticle coordinates are detected. + + If no reticle coordinates are found on a screen, the method skips that screen. + """ # Register into the model for screen in self.screen_widgets: coords = screen.get_reticle_coords() @@ -755,6 +877,21 @@ def probe_detection_button_handler(self): self.probe_calibration_btn.setChecked(True) def probe_detect_default_status_ui(self, sn = None): + """ + Resets the probe detection UI and clears the calibration status. + + If a stage serial number (`sn`) is provided, it resets the calibration for that specific stage. + Otherwise, it resets the calibration for all stages. + + Key actions: + 1. Resets button styles and hides calibration-related buttons. + 2. Disconnects signals and stops probe detection on the screens. + 3. Clears the calibration status and updates the global coordinates on the UI. + 4. Sets calculator functions as "uncalibrated." + + Args: + sn (str, optional): The serial number of the stage to reset. If None, resets all stages. + """ self.probe_calibration_btn.setStyleSheet(""" QPushButton { color: white; @@ -907,6 +1044,13 @@ def probe_detect_accepted_status(self, stage_sn, transformation_matrix, scale, s self.reticle_metadata.load_metadata_from_file() def set_default_x_y_z_style(self): + """ + Resets the style of the X, Y, and Z calibration buttons to their default appearance. + + This method sets the color of the text to white and the background to black for each + of the calibration buttons (X, Y, Z), indicating that they are ready for the next + calibration process. + """ self.calib_x.setStyleSheet( "color: white;" "background-color: black;" @@ -938,14 +1082,23 @@ def hide_x_y_z(self): self.set_default_x_y_z_style() def hide_trajectory_btn(self): + """ + Hides the trajectory view button if it is currently visible. + """ if self.viewTrajectory_btn.isVisible(): self.viewTrajectory_btn.hide() def hide_calculation_btn(self): + """ + Hides the calculation button if it is currently visible. + """ if self.calculation_btn.isVisible(): self.calculation_btn.hide() def hide_reticle_metadata_btn(self): + """ + Hides the reticle metadata button if it is currently visible. + """ if self.reticle_metadata_btn.isVisible(): self.reticle_metadata_btn.hide() @@ -995,6 +1148,11 @@ def calib_z_complete(self, switch_probe = False): self.calib_status_z = True def update_probe_calib_status_transM(self, transformation_matrix, scale): + """ + Updates the probe calibration status with the transformation matrix and scale. + Extracts the rotation matrix (R), translation vector (T), and scale (S) and formats + them into a string to be displayed in the UI. + """ # Extract the rotation matrix (top-left 3x3) R = transformation_matrix[:3, :3] # Extract the translation vector (top 3 elements of the last column) @@ -1018,6 +1176,9 @@ def update_probe_calib_status_transM(self, transformation_matrix, scale): return content def update_probe_calib_status_L2(self, L2_err): + """ + Formats the L2 error value for display in the UI. + """ content = ( f"[L2 distance]
" f" {L2_err:.3f}
" @@ -1026,6 +1187,9 @@ def update_probe_calib_status_L2(self, L2_err): return content def update_probe_calib_status_distance_traveled(self, dist_traveled): + """ + Formats the distance traveled in the X, Y, and Z directions for display in the UI. + """ x, y, z = dist_traveled[0], dist_traveled[1], dist_traveled[2] content = ( f"[Distance traveled (µm)]
" @@ -1036,6 +1200,11 @@ def update_probe_calib_status_distance_traveled(self, dist_traveled): return content def display_probe_calib_status(self, transM, scale, L2_err, dist_traveled): + """ + Displays the full probe calibration status, including the transformation matrix, L2 error, + and distance traveled. It combines the formatted content for each of these elements and + updates the UI label. + """ content_transM = self.update_probe_calib_status_transM(transM, scale) content_L2 = self.update_probe_calib_status_L2(L2_err) content_L2_travel = self.update_probe_calib_status_distance_traveled(dist_traveled) @@ -1044,6 +1213,10 @@ def display_probe_calib_status(self, transM, scale, L2_err, dist_traveled): self.probeCalibrationLabel.setText(full_content) def update_probe_calib_status(self, moving_stage_id, transM, scale, L2_err, dist_traveled): + """ + Updates the probe calibration status based on the moving stage ID and the provided calibration data. + If the selected stage matches the moving stage, the calibration data is displayed on the UI. + """ self.transM, self.L2_err, self.dist_travled = transM, L2_err, dist_traveled self.scale = scale self.moving_stage_id = moving_stage_id @@ -1057,6 +1230,10 @@ def update_probe_calib_status(self, moving_stage_id, transM, scale, L2_err, dist logger.debug(f"Update probe calib status: {self.moving_stage_id}, {self.selected_stage_id}") def get_stage_info(self): + """ + Retrieves the current probe calibration information, including the detection status, + transformation matrix, L2 error, scale, and distance traveled. + """ info = {} info['detection_status'] = self.probe_detection_status info['transM'] = self.transM @@ -1069,7 +1246,9 @@ def get_stage_info(self): return info def update_stage_info(self, info): - #self.probe_detection_status = info['detection_status'] + """ + Updates the stage information with the provided probe calibration data. + """ self.transM = info['transM'] self.L2_err = info['L2_err'] self.scale = info['scale'] @@ -1079,6 +1258,17 @@ def update_stage_info(self, info): self.calib_status_z = info['status_z'] def update_stages(self, prev_stage_id, curr_stage_id): + """ + Updates the stage calibration information when switching between stages. + + This method saves the calibration information for the previous stage and loads the + calibration data for the current stage. Based on the loaded information, it updates + the probe detection status and the corresponding UI elements (e.g., X, Y, Z calibration buttons). + + Args: + prev_stage_id (str): The ID of the previous stage. + curr_stage_id (str): The ID of the current stage being switched to. + """ logger.debug(f"update_stages, prev:{prev_stage_id}, curr:{curr_stage_id}") self.selected_stage_id = curr_stage_id if prev_stage_id is None or curr_stage_id is None: @@ -1123,10 +1313,26 @@ def update_stages(self, prev_stage_id, curr_stage_id): self.probe_detection_status = probe_detection_status def view_trajectory_button_handler(self): + """ + Handles the event when the user clicks the "View Trajectory" button. + + This method triggers the display of the 3D trajectory for the selected stage + using the `probeCalibration` object. + """ self.probeCalibration.view_3d_trajectory(self.selected_stage_id) def calculation_button_handler(self): + """ + Handles the event when the user clicks the "Calculation" button. + + This method displays the calculator widget using the `calculator` object. + """ self.calculator.show() def reticle_button_handler(self): + """ + Handles the event when the user clicks the "Reticle" button. + + This method displays the reticle metadata widget using the `reticle_metadata` object. + """ self.reticle_metadata.show() \ No newline at end of file diff --git a/parallax/utils.py b/parallax/utils.py index a6fb8e3..076c258 100644 --- a/parallax/utils.py +++ b/parallax/utils.py @@ -5,22 +5,25 @@ - UtilsCrops: calculating crop regions based on specified criteria. """ - class UtilsCoords: - """Utility class for coordinate scaling.""" + """Utility class for scaling coordinates between original and resized images.""" def __init__(self): + """init""" pass @classmethod def scale_coords_to_original(self, tip, original_size, resized_size): - """Scale coordinates from resized image to original image. + """ + Scale coordinates from a resized image back to the original image dimensions. Args: - tip (tuple): Coordinates of the tip (x, y) in the resized image. + tip (tuple): The (x, y) coordinates of the tip in the resized image. + original_size (tuple): The (width, height) of the original image. + resized_size (tuple): The (width, height) of the resized image. Returns: - tuple: Scaled coordinates of the tip (x, y) in the original image. + tuple: The scaled (x, y) coordinates of the tip in the original image. """ x, y = tip original_width, original_height = original_size @@ -36,15 +39,16 @@ def scale_coords_to_original(self, tip, original_size, resized_size): @classmethod def scale_coords_to_resized_img(self, tip, original_size, resized_size): - """Scale coordinates from original image to resized image. + """ + Scale coordinates from the original image to a resized image. Args: - tip (tuple): Coordinates of the tip (x, y) in the original image. - original_size (tuple): Original size of the image (width, height). - resized_size (tuple): Resized size of the image (width, height). + tip (tuple): The (x, y) coordinates of the tip in the original image. + original_size (tuple): The (width, height) of the original image. + resized_size (tuple): The (width, height) of the resized image. Returns: - tuple: Scaled coordinates of the tip (x, y) in the resized image. + tuple: The scaled (x, y) coordinates of the tip in the resized image. """ x, y = tip original_width, original_height = original_size @@ -60,7 +64,7 @@ def scale_coords_to_resized_img(self, tip, original_size, resized_size): class UtilsCrops: - """Utility class for calculating crop regions.""" + """Utility class for calculating crop regions based on tip and base coordinates.""" def __init__(self): """Initialize the UtilsCrops object."""