Skip to content

Commit 4f9bbec

Browse files
committed
Fixed XSpSpotSelectionContext
1 parent dab7458 commit 4f9bbec

3 files changed

Lines changed: 66 additions & 33 deletions

File tree

autoemx/core/em_runtime/particle_finder.py

Lines changed: 57 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -688,6 +688,37 @@ def _is_particle_area_ok(self, area):
688688
return is_par_area_ok
689689

690690

691+
def _capture_ref_frame(self, par_image=None, pixel_size_um=None):
692+
'''
693+
Capture (or accept) the grayscale frame used for spot selection.
694+
695+
At the microscope, a fresh frame is acquired via the driver. Offline, the
696+
provided ``par_image`` is used and image dimensions / pixel size are
697+
updated from it. This is the single place that defines how the reference
698+
frame is obtained, shared by automated and callback spot selection.
699+
700+
Parameters
701+
----------
702+
par_image : ndarray, optional
703+
Grayscale frame to use when not running at the microscope.
704+
pixel_size_um : float, optional
705+
Pixel size in micrometers. Required together with ``par_image`` offline.
706+
707+
Returns
708+
-------
709+
ndarray
710+
The grayscale frame image.
711+
'''
712+
if EM_driver.is_microscope_connected():
713+
self._check_EM_controller_initialization()
714+
return EM_driver.get_image_data(self._im_width, self._im_height, 1)
715+
if par_image is not None and pixel_size_um is not None:
716+
self._im_height, self._im_width = par_image.shape
717+
self.EM.pixel_size_um = pixel_size_um
718+
return par_image
719+
raise ValueError('This function must be run at the microscope, or it needs to be passed both image and its pixel size')
720+
721+
691722
def _get_particle_mask(self, par_image=None, pixel_size_um=None, results_dir=None, centering=False):
692723
'''
693724
Returns a binary mask of the particle at the center of the image, if the particle is large enough.
@@ -719,16 +750,9 @@ def _get_particle_mask(self, par_image=None, pixel_size_um=None, results_dir=Non
719750
- The commented `cv2.imshow` lines can be enabled for debugging visualization.
720751
'''
721752
# Get particle mask
722-
if EM_driver.is_microscope_connected():
723-
self._check_EM_controller_initialization()
724-
par_image = EM_driver.get_image_data(self._im_width, self._im_height, 1)
725-
elif par_image is not None and pixel_size_um is not None:
726-
self._im_height, self._im_width = par_image.shape
727-
self.EM.pixel_size_um = pixel_size_um
728-
if results_dir:
729-
self.results_dir = results_dir
730-
else:
731-
raise ValueError('This function must be run at the microscope, or it needs to be passed both image and its pixel size')
753+
if not EM_driver.is_microscope_connected() and results_dir:
754+
self.results_dir = results_dir
755+
par_image = self._capture_ref_frame(par_image, pixel_size_um)
732756

733757
# Apply the threshold to get a binary image
734758
_, par_mask = cv2.threshold(par_image, self.powder_meas_cfg.par_brightness_thresh, 255, cv2.THRESH_BINARY)
@@ -984,6 +1008,16 @@ def get_XS_acquisition_spots_coord_list(
9841008
- Exclude points that are close to larger particles along the X-ray emission path to the EDS detector,
9851009
as these may absorb emitted X-rays and degrade spectral signal.
9861010
'''
1011+
# In callback mode, spots are supplied by xsp_spot_selector. Skip the
1012+
# particle mask computation entirely and reuse a single reference frame
1013+
# per particle (see _get_callback_xsp_spots).
1014+
if self.powder_meas_cfg.par_spot_selection_mode == 'callback':
1015+
return self._get_callback_xsp_spots(
1016+
n_tot_sp_collected,
1017+
par_image=par_image,
1018+
pixel_size_um=pixel_size_um,
1019+
)
1020+
9871021
# --- 1. Acquire or prepare the particle mask and image ---
9881022
if EM_driver.is_microscope_connected():
9891023
par_mask_return = self._get_particle_mask()
@@ -1006,14 +1040,6 @@ def get_XS_acquisition_spots_coord_list(
10061040
# --- 2. Erode the particle mask to avoid edge effects ---
10071041
margin = max(10, int(self.powder_meas_cfg.par_mask_margin / self.EM.pixel_size_um))
10081042
final_mask = self._erode_particle_mask(par_mask, margin)
1009-
1010-
if self.powder_meas_cfg.par_spot_selection_mode == 'callback':
1011-
return self._get_callback_xsp_spots(
1012-
n_tot_sp_collected=n_tot_sp_collected,
1013-
par_image=par_image,
1014-
par_mask=par_mask,
1015-
usable_mask=final_mask,
1016-
)
10171043

10181044
# --- 3. Find bright points in image, which indicate highest regions on particle---
10191045
thresholded_image, min_area_pixels = self._find_particle_bright_regions(final_mask, par_image)
@@ -1057,21 +1083,27 @@ def get_XS_acquisition_spots_coord_list(
10571083
def _get_callback_xsp_spots(
10581084
self,
10591085
n_tot_sp_collected: int,
1060-
par_image,
1061-
par_mask,
1062-
usable_mask,
1086+
par_image=None,
1087+
pixel_size_um=None,
10631088
) -> List[tuple]:
1064-
"""Invoke xsp_spot_selector and validate returned pixel coordinates."""
1089+
"""Invoke xsp_spot_selector and validate returned pixel coordinates.
1090+
1091+
The reference frame is captured once per particle (when ``self.ref_image``
1092+
is None) and reused for every spot batch on that particle, so the
1093+
drift-correction reference stays fixed across that particle's spectra. No
1094+
particle mask is computed in this mode.
1095+
"""
1096+
if self.ref_image is None:
1097+
self.ref_image = self._capture_ref_frame(par_image, pixel_size_um)
1098+
10651099
frame_label = getattr(self.EM, 'current_frame_label', '')
10661100
if frame_label is None:
10671101
frame_label = ''
10681102

10691103
context = XSpSpotSelectionContext(
10701104
particle_id=self.tot_par_cntr,
10711105
n_tot_sp_collected=n_tot_sp_collected,
1072-
par_image=par_image,
1073-
par_mask=par_mask,
1074-
usable_mask=usable_mask,
1106+
ref_image=self.ref_image,
10751107
pixel_size_um=float(self.EM.pixel_size_um),
10761108
frame_label=str(frame_label),
10771109
im_width=int(self._im_width),
@@ -1085,15 +1117,13 @@ def _get_callback_xsp_spots(
10851117
raise
10861118

10871119
if not raw_spots:
1088-
self.ref_image = par_image
10891120
return []
10901121

10911122
selected_points = validate_xsp_spot_pixels(
10921123
raw_spots,
10931124
im_width=int(self._im_width),
10941125
im_height=int(self._im_height),
10951126
)
1096-
self.ref_image = par_image
10971127
return selected_points
10981128

10991129

autoemx/core/em_runtime/spectrum_acquisition.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,9 @@ def _get_particle_coords(
192192
return False, None, None
193193
self._xsp_callback_particle_active = True
194194
self._xsp_callback_particle_cntr = particle_finder.tot_par_cntr
195+
# Force a fresh reference frame capture for the new particle. It is
196+
# then reused for every spot batch on this particle (no re-capture).
197+
particle_finder.ref_image = None
195198
particle_cntr = self._xsp_callback_particle_cntr
196199
else:
197200
was_particle_found = particle_finder.go_to_next_particle()

autoemx/core/em_runtime/xsp_spot_selection.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,7 @@
1010
from __future__ import annotations
1111

1212
from dataclasses import dataclass
13-
from typing import Callable, List, Sequence, Tuple, TYPE_CHECKING
14-
15-
import numpy as np
13+
from typing import Any, Callable, List, Sequence, Tuple, TYPE_CHECKING
1614

1715
if TYPE_CHECKING:
1816
from autoemx.core.em_runtime.particle_finder import EM_Particle_Finder
@@ -29,13 +27,15 @@ class XSpSpotSelectionContext:
2927
a non-empty list of pixel coordinates to measure, then wait for the next
3028
invocation to supply more spots. Return ``[]`` when finished with this
3129
particle (including to skip it without measuring).
30+
31+
``ref_image`` is the frame captured at spot-selection time. It is the same
32+
image used as the drift-correction reference during subsequent acquisitions
33+
on this particle (see ``EM_Controller.ref_image``).
3234
"""
3335

3436
particle_id: int
3537
n_tot_sp_collected: int
36-
par_image: np.ndarray
37-
par_mask: np.ndarray
38-
usable_mask: np.ndarray
38+
ref_image: Any
3939
pixel_size_um: float
4040
frame_label: str
4141
im_width: int

0 commit comments

Comments
 (0)