diff --git a/pyproject.toml b/pyproject.toml index e1da4c0..41eaf1b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,6 @@ requires-python = ">=3.11" license = {file = "LICENSE.txt"} dependencies = [ "sci-agent@git+https://github.com/mdw771/sci-agent", - "opencv-contrib-python", "matplotlib", "numpy", "scikit-image", diff --git a/src/eaa/task_manager/tuning/analytical_focusing.py b/src/eaa/task_manager/tuning/analytical_focusing.py index 65c0aef..33a2d6e 100644 --- a/src/eaa/task_manager/tuning/analytical_focusing.py +++ b/src/eaa/task_manager/tuning/analytical_focusing.py @@ -43,6 +43,7 @@ def __init__( line_scan_tool_y_coordinate_args: Tuple[str, ...] = ("y_center",), image_acquisition_tool_x_coordinate_args: Tuple[str, ...] = ("x_center",), image_acquisition_tool_y_coordinate_args: Tuple[str, ...] = ("y_center",), + use_feature_tracking_subtask: bool = True, *args, **kwargs ): """Analytical scanning microscope focusing task manager driven @@ -106,6 +107,9 @@ def __init__( See `line_scan_tool_x_coordinate_args`. image_acquisition_tool_y_coordinate_args: Tuple[str, ...] See `line_scan_tool_y_coordinate_args`. + use_feature_tracking_subtask: bool + If True, feature tracking will be run if the line scan feature has drifted + out of the FOV. """ if acquisition_tool is None: raise ValueError("`acquisition_tool` must be provided.") @@ -127,6 +131,7 @@ def __init__( self.last_acquisition_count_registered = 0 self.last_acquisition_count_stitched = 0 + self.use_feature_tracking_subtask: bool = use_feature_tracking_subtask self.feature_tracking_task_manager: Optional[AnalyticalFeatureTrackingTaskManager] = None self.line_scan_tool_x_coordinate_args = line_scan_tool_x_coordinate_args @@ -168,7 +173,7 @@ def create_image_registration_tool(self, acquisition_tool: AcquireImage): reference_image=None, reference_pixel_size=1.0, image_coordinates_origin="top_left", - registration_method="sift", + registration_method="phase_correlation", ) return image_registration_tool @@ -292,6 +297,8 @@ def initialize_kwargs_buffers( ): self.line_scan_kwargs = copy.deepcopy(initial_line_scan_kwargs) self.image_acquisition_kwargs = copy.deepcopy(initial_2d_scan_kwargs) + + self.line_scan_kwargs["view_scan_line_in_image"] = True def run_line_scan(self) -> float: """Run a line scan and return the FWHM of the Gaussian fit. @@ -301,6 +308,7 @@ def run_line_scan(self) -> float: float The FWHM of the Gaussian fit. """ + self.record_system_message(f"Acquiring line scan with {self.line_scan_kwargs}.") res = self.acquisition_tool.acquire_line_scan(**self.line_scan_kwargs) try: res = json.loads(res) @@ -327,6 +335,7 @@ def update_optimization_model(self, fwhm: float): self.optimization_tool.update(x, -np.array([[fwhm]])) def run_2d_scan(self): + self.record_system_message(f"Acquiring 2D scan with {self.image_acquisition_kwargs}.") image_path = self.acquisition_tool.acquire_image(**self.image_acquisition_kwargs) content = f"Acquired 2D scan with kwargs: {self.image_acquisition_kwargs}" if isinstance(image_path, str): @@ -457,16 +466,18 @@ def run_tuning_iteration(self, x: np.ndarray): f"but got {len(x)} and {len(self.parameter_names)}." ) x = np.array(x) + self.record_system_message(f"Setting parameters to {x}.") self.param_setting_tool.set_parameters(x) self.run_2d_scan() offset, is_present = self.find_offset_and_feature_presence() - if not is_present: + if not is_present and self.use_feature_tracking_subtask: msg = "Feature is not present in the current image. Running feature tracking sub-task." logger.info(msg) self.record_system_message(msg) offset = self.run_feature_tracking_subtask() if np.any(np.isnan(offset)): raise RuntimeError("Offset is NaN. Please set offset manually.") + self.record_system_message(f"Applying offset {offset}.") self.apply_offset_to_kwargs_buffers(offset) fwhm = self.run_line_scan() if np.isnan(fwhm): diff --git a/src/eaa/tool/imaging/aps_mic/acquisition.py b/src/eaa/tool/imaging/aps_mic/acquisition.py index 8a882a8..9831dac 100644 --- a/src/eaa/tool/imaging/aps_mic/acquisition.py +++ b/src/eaa/tool/imaging/aps_mic/acquisition.py @@ -55,6 +55,7 @@ def __init__( show_colorbar_in_image: bool = False, require_approval: bool = False, line_scan_return_gaussian_fit: bool = False, + scan_samy: bool = False, *args, **kwargs ): """Image acquisition tool with Bluesky. @@ -86,7 +87,9 @@ def __init__( line_scan_return_gaussian_fit: bool, optional If True, the function returns a stringified JSON object containing the image path and the Gaussian fit FWHM. - + scan_samy: bool, optional + If True, the line_scan is generated by moving sample-y motor + Raises ------ ImportError @@ -106,6 +109,7 @@ def __init__( self.plot_image_in_log_scale = plot_image_in_log_scale self.show_colorbar_in_image = show_colorbar_in_image self.line_scan_return_gaussian_fit = line_scan_return_gaussian_fit + self.scan_samy = scan_samy super().__init__(*args, require_approval=require_approval, **kwargs) @@ -294,7 +298,7 @@ def acquire_line_scan( img_path, [_, _, _, fwhm] = save_xrf_line_scan( mda_file_path, png_output_dir, roi_num=self.xrf_roi_num, - return_line_array=True + return_line_array=True, scan_samy=self.scan_samy, ) wait_for_file(img_path, duration=5) diff --git a/src/eaa/tool/imaging/aps_mic/param_tuning.py b/src/eaa/tool/imaging/aps_mic/param_tuning.py index 901d952..059461a 100644 --- a/src/eaa/tool/imaging/aps_mic/param_tuning.py +++ b/src/eaa/tool/imaging/aps_mic/param_tuning.py @@ -86,6 +86,7 @@ def set_parameters( self.RE(self.bps.mv(self.zp_z_motor, parameters[0])) msg = f"Moved Zone Plate z position to position: {parameters[0]}" logger.info(msg) + self.update_parameter_history(parameters) return msg else: raise ValueError("parameter_ranges is not set") diff --git a/src/eaa/tool/imaging/aps_mic/scan_control.py b/src/eaa/tool/imaging/aps_mic/scan_control.py index b4b2f5d..75993b4 100644 --- a/src/eaa/tool/imaging/aps_mic/scan_control.py +++ b/src/eaa/tool/imaging/aps_mic/scan_control.py @@ -35,6 +35,7 @@ def __init__(self, require_approval: bool = False, *args, **kwargs): self.acquire_image_tool.scan2d_plan = fly2d_scanrecord self.acquire_image_tool.scan1d_plan = step1d_scanrecord self.acquire_image_tool.samy_motor = oregistry["samy"] + self.acquire_image_tool.scan_samy = True self.param_tuning_tool = BlueskySetParameters() self.param_tuning_tool.RE = RE diff --git a/src/eaa/tool/imaging/aps_mic/util.py b/src/eaa/tool/imaging/aps_mic/util.py index 6dcf17c..83768ff 100644 --- a/src/eaa/tool/imaging/aps_mic/util.py +++ b/src/eaa/tool/imaging/aps_mic/util.py @@ -147,7 +147,7 @@ def process_xrfdata( return None -def plot_xrf_line_scan(x, y, val_gauss, fwhm, scan_name, roi_num): +def plot_xrf_line_scan(x, y, val_gauss, fwhm, scan_name, roi_num, scan_samy=False): """ Plot the XRF line scan data. """ @@ -165,7 +165,7 @@ def plot_xrf_line_scan(x, y, val_gauss, fwhm, scan_name, roi_num): ) ax.legend() - ax.set_xlabel("X-axis Position") + ax.set_xlabel("X-axis Position" if not scan_samy else "Y-axis Position") ax.set_ylabel("Intensity") ax.set_title(f"{scan_name}-{roi_num}") ax.grid(True) @@ -178,7 +178,8 @@ def save_xrf_line_scan( output_dir: str, roi_num: int, y_threshold: float = 0.0, - return_line_array: bool = False + return_line_array: bool = False, + scan_samy: bool = False, ) -> str | None: """ @@ -196,6 +197,8 @@ def save_xrf_line_scan( The threshold for the Gaussian fit. return_line_array : bool If True, the line array will be returned. + scan_samy : bool + If True, line profile is generated using sample-y motor, scanning sample-y Returns ------- @@ -233,7 +236,7 @@ def save_xrf_line_scan( # Plot the data and the fit scan_name = os.path.basename(mda_path) - fig = plot_xrf_line_scan(x, y, val_gauss, fwhm, scan_name, roi_num) + fig = plot_xrf_line_scan(x, y, val_gauss, fwhm, scan_name, roi_num, scan_samy=scan_samy) sc = scan_name.replace(".mda", "") fname = f"{output_dir}/{sc}_ROI{roi_num}.png" fig.savefig(fname) diff --git a/src/eaa/tool/imaging/registration.py b/src/eaa/tool/imaging/registration.py index 856424d..796dba0 100644 --- a/src/eaa/tool/imaging/registration.py +++ b/src/eaa/tool/imaging/registration.py @@ -2,9 +2,9 @@ import logging import json -import cv2 import numpy as np import scipy.ndimage as ndi +from skimage import feature from sciagent.tool.base import BaseTool, check, ToolReturnType, tool from eaa.tool.imaging.acquisition import AcquireImage @@ -261,7 +261,7 @@ def prepare_image_for_feature_matching(self, image: np.ndarray) -> np.ndarray: image = (image - min_val) / (max_val - min_val) else: image = np.zeros_like(image, dtype=np.float32) - return (image * 255).astype(np.uint8) + return image def adjust_points_for_origin( self, @@ -282,30 +282,39 @@ def feature_based_registration( image_t: np.ndarray, image_r: np.ndarray, ) -> np.ndarray: - image_t_uint8 = self.prepare_image_for_feature_matching(image_t) - image_r_uint8 = self.prepare_image_for_feature_matching(image_r) - sift = cv2.SIFT_create() - keypoints_t, descriptors_t = sift.detectAndCompute(image_t_uint8, None) - keypoints_r, descriptors_r = sift.detectAndCompute(image_r_uint8, None) + image_t_float = self.prepare_image_for_feature_matching(image_t) + image_r_float = self.prepare_image_for_feature_matching(image_r) + sift_t = feature.SIFT() + sift_r = feature.SIFT() + sift_t.detect_and_extract(image_t_float) + sift_r.detect_and_extract(image_r_float) + + descriptors_t = sift_t.descriptors + descriptors_r = sift_r.descriptors + keypoints_t = sift_t.keypoints + keypoints_r = sift_r.keypoints - if descriptors_t is None or descriptors_r is None: + if ( + descriptors_t is None + or descriptors_r is None + or descriptors_t.size == 0 + or descriptors_r.size == 0 + ): raise RuntimeError("SIFT feature detection failed to find descriptors.") - matcher = cv2.BFMatcher(cv2.NORM_L2) - raw_matches = matcher.knnMatch(descriptors_t, descriptors_r, k=2) - good_matches = [] - for match_pair in raw_matches: - if len(match_pair) != 2: - continue - m, n = match_pair - if m.distance < 0.75 * n.distance: - good_matches.append(m) + matches = feature.match_descriptors( + descriptors_t, + descriptors_r, + metric="euclidean", + cross_check=True, + max_ratio=0.75, + ) - if len(good_matches) < 3: + if matches.shape[0] < 3: raise RuntimeError("Not enough SIFT matches to estimate translation.") - pts_t = np.array([keypoints_t[m.queryIdx].pt for m in good_matches]) - pts_r = np.array([keypoints_r[m.trainIdx].pt for m in good_matches]) + pts_t = keypoints_t[matches[:, 0]][:, ::-1] + pts_r = keypoints_r[matches[:, 1]][:, ::-1] pts_t = self.adjust_points_for_origin(pts_t, image_t.shape) pts_r = self.adjust_points_for_origin(pts_r, image_r.shape) deltas = pts_r - pts_t