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 diff --git a/uv.lock b/uv.lock index fa463e1..f2f200f 100644 --- a/uv.lock +++ b/uv.lock @@ -1538,7 +1538,6 @@ dependencies = [ { name = "matplotlib" }, { name = "numpy" }, { name = "openai" }, - { name = "opencv-contrib-python" }, { name = "sci-agent" }, { name = "scikit-image" }, { name = "scipy" }, @@ -1581,7 +1580,6 @@ requires-dist = [ { name = "mic-vis", marker = "extra == 'aps-mic'", git = "https://github.com/grace227/mic-vis" }, { name = "numpy" }, { name = "openai" }, - { name = "opencv-contrib-python" }, { name = "psycopg", extras = ["binary"], marker = "extra == 'postgresql-vector-store'", specifier = ">=3.2.10" }, { name = "pytest", marker = "extra == 'dev'" }, { name = "sci-agent", git = "https://github.com/mdw771/sci-agent" }, @@ -4279,25 +4277,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/12/cf/03675d8bd8ecbf4445504d8071adab19f5f993676795708e36402ab38263/openapi_pydantic-0.5.1-py3-none-any.whl", hash = "sha256:a3a09ef4586f5bd760a8df7f43028b60cafb6d9f61de2acba9574766255ab146", size = 96381, upload-time = "2025-01-08T19:29:25.275Z" }, ] -[[package]] -name = "opencv-contrib-python" -version = "4.13.0.90" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/2d/8a/6d5723ef4551cd6abe9b4c5cbb51bc48ab452b057d48d58d641c27f83c34/opencv_contrib_python-4.13.0.90.tar.gz", hash = "sha256:177d75b048021df8a2632cb5998b3827a30143f88540be2791cac5497e8592dd", size = 150983550, upload-time = "2026-01-18T13:55:20.9Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4c/31/10a5be5c3f07b066232a0a3c796c705d8c92e9137c30aa041044e974dbe4/opencv_contrib_python-4.13.0.90-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:e000908d262f479042677ef91475cecb1801341ea985af5cb3b9dcde4bb78029", size = 51810049, upload-time = "2026-01-18T08:16:22.443Z" }, - { url = "https://files.pythonhosted.org/packages/e8/29/00c03495a46b22a330c87af0507f4bd8dffd914efc90e492353a0509c659/opencv_contrib_python-4.13.0.90-cp37-abi3-macosx_14_0_x86_64.whl", hash = "sha256:8e7c74869e329636727298f3cab858f015160b2980f4726b538744732d4ab526", size = 38829919, upload-time = "2026-01-18T08:17:48.752Z" }, - { url = "https://files.pythonhosted.org/packages/99/4e/32185a05284e4a804b9fb10aebc816c9d818dddce86488afcf882e1b29d6/opencv_contrib_python-4.13.0.90-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3b95f67bd2539309459057fe6d239603754994155957cee0ce3b82e0f0d8d8a5", size = 53100146, upload-time = "2026-01-18T08:18:22.985Z" }, - { url = "https://files.pythonhosted.org/packages/39/f6/9c3f12778cc7cde48747539bd121e1829e1fed24af6c87fc4144deb8929a/opencv_contrib_python-4.13.0.90-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8ab59de69d1eee39787aa496cfdb2e6e6685733c54c97d416d58e70261836f58", size = 76590796, upload-time = "2026-01-18T08:19:17.471Z" }, - { url = "https://files.pythonhosted.org/packages/01/b2/f53ca77ca5d97a06fc15664bbe813798872ab5d2e4bd639ca0cb35e8c4d5/opencv_contrib_python-4.13.0.90-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c9268888a1592875cb1119f2dc444ff8581a92c27dec65cbe694551b2952d834", size = 52543760, upload-time = "2026-01-18T08:19:58.587Z" }, - { url = "https://files.pythonhosted.org/packages/a9/3d/00071f3a395611a13efca22e3ee65aab25b8bf54128ae5080d8361cbb673/opencv_contrib_python-4.13.0.90-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3c693f1fb7a25eae73eb9bc1c2fdbc08ad3df51c31589f1f9a8377f2a4368b1c", size = 79150484, upload-time = "2026-01-18T08:20:48.248Z" }, - { url = "https://files.pythonhosted.org/packages/78/0f/32d766d139aaa56485099a8e2b514578af608c083e01f29050130a411d7f/opencv_contrib_python-4.13.0.90-cp37-abi3-win32.whl", hash = "sha256:334c10e5b191cac919206f797f9b84a2809ab39db20458b993edf8d204379341", size = 36829547, upload-time = "2026-01-18T08:21:22.909Z" }, - { url = "https://files.pythonhosted.org/packages/de/f8/c94c0ef4b525fc672f3692384aa6d971292b02de8fbf880d9e158890c226/opencv_contrib_python-4.13.0.90-cp37-abi3-win_amd64.whl", hash = "sha256:2f7ac460620f5a03924be96d9770c076e2807366a03941bdf38b29431305bfd4", size = 46485754, upload-time = "2026-01-18T08:21:56.426Z" }, -] - [[package]] name = "openpyxl" version = "3.1.5" @@ -6223,8 +6202,8 @@ wheels = [ [[package]] name = "sci-agent" -version = "0.1.dev25+gf9a6dc97e" -source = { git = "https://github.com/mdw771/sci-agent#f9a6dc97ef0e2154fa995d5ff01279a77334faa2" } +version = "0.1.dev28+ga2d4061a0" +source = { git = "https://github.com/mdw771/sci-agent#a2d4061a004c030b32a463314aac7defecfe1d11" } dependencies = [ { name = "chromadb" }, { name = "fastapi" },