Skip to content

Commit d744044

Browse files
committed
[feat] Use CoM for FM fiducial refinement
1 parent 66819f8 commit d744044

6 files changed

Lines changed: 169 additions & 65 deletions

File tree

src/odemis/gui/cont/acquisition/cryo_z_localization.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ def _on_target_focus_pos(self, target_coordinates: List[float]) -> None:
243243
"""
244244
# Set the target Z ctrl with the focus position
245245
self._panel.ctrl_target_z.SetValue(target_coordinates[2])
246-
save_project(self._tab_data_model.main)
246+
save_project(self._tab_data.main)
247247

248248
def _on_ctrl_target_z_change(self) -> List[float]:
249249
"""

src/odemis/gui/cont/multi_point_correlation.py

Lines changed: 79 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
# This is not related to any particular wxPython version and is most likely permanent.
3636

3737
from odemis import model, util
38-
from odemis.acq.align.tdct import get_optimized_z_gauss, _convert_das_to_numpy_stack, run_tdct_correlation
38+
from odemis.acq.align.tdct import _convert_das_to_numpy_stack, run_tdct_correlation
3939
from odemis.acq.feature import FIBFMCorrelationData, Target, TargetType
4040
from odemis.acq.stream import StaticFluoStream, StaticSEMStream, StaticFIBStream, FluoStream
4141
from odemis.gui import conf
@@ -44,7 +44,7 @@
4444
from odemis.gui.util import call_in_wx_main
4545
from odemis.model import ListVA
4646
from odemis.util.dataio import data_to_static_streams
47-
from odemis.util.interpolation import interpolate_z_stack
47+
from odemis.util.img import get_brightest_channel, compute_center_of_mass
4848
from odemis.util.units import readable_str
4949

5050
# create an enum with column labels and position
@@ -62,12 +62,14 @@ class GridColumns(Enum):
6262
FIDUCIAL_PATTERN = r"^[^-]+-"
6363
RIM_COR_DEFAULT = 0.495 # See MD_RIM_COR. This value works fine for 50x objectives, which are common
6464

65-
# Both functions getPixel3DCoordinates(args*, kwargs*) and getPhysical3DCoordinates(args*, kwargs*) need special
65+
# Both functions get_pixel_3d_coordinates(args*, kwargs*) and get_physical_3d_coordinates(args*, kwargs*) need special
6666
# conditions to convert between physical and pixel coordinate systems in order for multipoint correlation to operate.
6767
# For coordinate conversions, we assume the pixels in 3D are isosymmetric
6868
# i.e. size in pixel[0]=pixel[1]=pixel[2].
69+
COM_ROI_PADDING = 12 # Padding (pixels) for center of mass ROI extraction
70+
# TODO: Could adapt padding based on pixel spacing for more flexibility if needed
6971

70-
def getPixel3DCoordinates(stream: FluoStream, p_pos: Tuple[float, float, float], check_bbox: bool = False) \
72+
def get_pixel_3d_coordinates(stream: FluoStream, p_pos: Tuple[float, float, float], check_bbox: bool = False) \
7173
-> Optional[Tuple[float, float, float]]:
7274
"""
7375
Translate 3D physical coordinates into 3D pixel coordinates. The z coordinate is computed assuming iso-voxel
@@ -84,20 +86,19 @@ def getPixel3DCoordinates(stream: FluoStream, p_pos: Tuple[float, float, float],
8486

8587
raw = stream.raw[0]
8688
md = stream._find_metadata(raw.metadata)
87-
pxs = md.get(model.MD_PIXEL_SIZE, (1e-6, 1e-6))
89+
pxs = md.get(model.MD_PIXEL_SIZE, (1e-6, 1e-6, 1e-6))
8890
# For multipoint correlation, we assume that the pixel size in x is the same as in y
8991
if not util.almost_equal(pxs[0], pxs[1], atol=1e-9):
9092
logging.warning("Pixel size in x and y are not equal while computing pixel coordinates")
9193

92-
# Z position is found by taking into account MD_POS and subtracting it from the physical coordinates.
93-
# Pixel value used for Z enforces the iso-voxel condition between x, y and z. It is not the real pixel value in z.
9494
tpos = md.get(model.MD_POS, (0, 0, 0))
9595
tpos_z = tpos[2] if len(tpos) >= 3 else 0.0
96-
z = (p_pos[2] - tpos_z) / pxs[1]
96+
z = (p_pos[2] - tpos_z) / pxs[2]
9797
pixel_pos = (pixel_pos[0], pixel_pos[1], z)
98+
9899
return pixel_pos
99100

100-
def getPhysical3DCoordinates(stream: FluoStream, pixel_pos: Tuple[float, float, float])\
101+
def get_physical_3d_coordinates(stream: FluoStream, pixel_pos: Tuple[float, float, float])\
101102
-> Optional[Tuple[float, float, float]]:
102103
"""
103104
Translate 3D pixel coordinates into 3D physical coordinates. The z coordinate is computed assuming iso-voxel
@@ -109,10 +110,11 @@ def getPhysical3DCoordinates(stream: FluoStream, pixel_pos: Tuple[float, float,
109110
p_pos = stream.getPhysicalCoordinates(pixel_pos[:2])
110111
raw = stream.raw[0]
111112
md = stream._find_metadata(raw.metadata)
112-
pxs = md.get(model.MD_PIXEL_SIZE, (1e-6, 1e-6))[0:2]
113+
pxs = md.get(model.MD_PIXEL_SIZE, (1e-6, 1e-6, 1e-6))
113114
tpos = md.get(model.MD_POS, (0, 0, 0))
114115
tpos_z = tpos[2] if len(tpos) >= 3 else 0.0
115-
p_pos_z = pixel_pos[2] * pxs[1] + tpos_z
116+
# Account for slice thickness, aka, z distance between slices
117+
p_pos_z = pixel_pos[2] * pxs[2] + tpos_z
116118
return (p_pos[0], p_pos[1], p_pos_z)
117119

118120
def update_feature_correlation_target(correlation_target: FIBFMCorrelationData,
@@ -146,8 +148,7 @@ def update_feature_correlation_target(correlation_target: FIBFMCorrelationData,
146148
fm_fiducials.sort(key=lambda x: x.index.value)
147149
correlation_target.fm_fiducials = fm_fiducials
148150

149-
acq_conf = conf.get_acqui_conf()
150-
save_project(acq_conf.pj_last_path, tab_data.main.features.value, tab_data.main.overviews.value)
151+
save_project(tab_data.main)
151152

152153
return correlation_target
153154

@@ -176,20 +177,20 @@ def __init__(self, frame):
176177
# Access the correlation points table (wxListCtrl)
177178
self.grid = self._panel.table_grid
178179

179-
# Access the Refine Z text (to check if refine_z is working or not)
180-
self.txt_refinez_active = self._panel.txt_refinez_active
181-
self.txt_refinez_active.Show(True)
180+
# Access the Refine XYZ status text (to check if XYZ targeting is working or not)
181+
self.txt_refine_xyz_active = self._panel.txt_refine_xyz_active
182+
self.txt_refine_xyz_active.Show(True)
182183

183-
# Access the Z-targeting button
184-
self.z_targeting_btn = self._panel.btn_z_targeting
185-
self.z_targeting_btn.Bind(wx.EVT_BUTTON, self._on_z_targeting)
186-
self.z_targeting_btn.Enable(False)
187-
# Disable Z-targeting button if super z stream is available as Z-targeting is not required in that case
188-
self.refinez_active = True
184+
# Access the XYZ-targeting button
185+
self.xyz_targeting_btn = self._panel.btn_xyz_targeting
186+
self.xyz_targeting_btn.Bind(wx.EVT_BUTTON, self._on_xyz_targeting)
187+
self.xyz_targeting_btn.Enable(False)
188+
# Disable XYZ-targeting button if super z stream is available as XYZ-targeting is not required in that case
189+
self.refine_xyz_active = True
189190
if self._tab_data_model.main.currentFeature.value.superz_stream_name:
190-
self.z_targeting_btn.SetToolTip("Super Z information available, Refine Z disabled")
191-
self.txt_refinez_active.SetLabel("Super Z information in use")
192-
self.refinez_active = False
191+
self.xyz_targeting_btn.SetToolTip("Super Z information available, Refine XYZ disabled")
192+
self.txt_refine_xyz_active.SetLabel("Super Z information in use")
193+
self.refine_xyz_active = False
193194

194195
self._panel.btn_delete_row.Bind(wx.EVT_BUTTON, self._on_delete_row)
195196

@@ -478,11 +479,11 @@ def _do_correlation(self):
478479
fib_coords.append(fib_coord)
479480
fib_coords = numpy.array(fib_coords, dtype=numpy.float32)
480481
for fm_coord in self.correlation_target.fm_fiducials:
481-
fm_coord_px = getPixel3DCoordinates(self.correlation_target.fm_streams[0], fm_coord.coordinates.value)
482+
fm_coord_px = get_pixel_3d_coordinates(self.correlation_target.fm_streams[0], fm_coord.coordinates.value)
482483
fm_coords.append(fm_coord_px)
483484
fm_coords = numpy.array(fm_coords, dtype=numpy.float32)
484485
poi_coord = self.correlation_target.fm_pois[0]
485-
poi_coord_px = getPixel3DCoordinates(self.correlation_target.fm_streams[0], poi_coord.coordinates.value)
486+
poi_coord_px = get_pixel_3d_coordinates(self.correlation_target.fm_streams[0], poi_coord.coordinates.value)
486487
poi_coords.append(poi_coord_px)
487488
poi_coords = numpy.array(poi_coords, dtype=numpy.float32)
488489
# Run the correlation
@@ -663,7 +664,7 @@ def _on_cell_changing(self, event) -> None:
663664
elif col_name == GridColumns.Z.name and (
664665
self._tab_data_model.main.currentTarget.value.type.value != TargetType.FibFiducial):
665666
self._tab_data_model.main.currentTarget.value.coordinates.value[2] = \
666-
getPhysical3DCoordinates(self.correlation_target.fm_streams[0], (x, y, float(new_value)))[2]
667+
get_physical_3d_coordinates(self.correlation_target.fm_streams[0], (x, y, float(new_value)))[2]
667668
except ValueError:
668669
wx.MessageBox("X, Y, Z values must be a float!", "Invalid Input", wx.OK | wx.ICON_ERROR)
669670
event.Veto() # Prevent the change
@@ -695,19 +696,19 @@ def _on_current_target_changes(self, target: Target) -> None:
695696
# For new targets, automatically perform Z targeting if MIP is checked for at least one FM stream
696697
if not target:
697698
self.grid.ClearSelection()
698-
self.z_targeting_btn.Enable(False)
699+
self.xyz_targeting_btn.Enable(False)
699700
return
700701

701702
mip_enabled = any([stream.max_projection.value for stream in self.correlation_target.fm_streams])
702703

703-
# Refine z should be disabled if the the Z information was obtained using SuperZ
704-
if self.refinez_active and (target.type.value in self.grid_targets):
704+
# Refine xyz should be disabled if the the Z information was obtained using SuperZ
705+
if self.refine_xyz_active and (target.type.value in self.grid_targets):
705706
if TargetType.FibFiducial == target.type.value:
706-
self.z_targeting_btn.Enable(False)
707+
self.xyz_targeting_btn.Enable(False)
707708
else:
708-
self.z_targeting_btn.Enable(True)
709+
self.xyz_targeting_btn.Enable(True)
709710
if mip_enabled:
710-
self._on_z_targeting(None)
711+
self._on_xyz_targeting(None)
711712

712713
for row in range(self.grid.GetNumberRows()):
713714
if self._selected_target_in_grid(target, row):
@@ -739,7 +740,7 @@ def _on_current_coordinates_changes(self, coordinates: ListVA) -> None:
739740
pixel_coords = self.correlation_target.fib_stream.getPixelCoordinates(
740741
(target.coordinates.value[0], target.coordinates.value[1]), check_bbox=False)
741742
else:
742-
pixel_coords = getPixel3DCoordinates(self.correlation_target.fm_streams[0], target.coordinates.value)
743+
pixel_coords = get_pixel_3d_coordinates(self.correlation_target.fm_streams[0], target.coordinates.value)
743744
if (self.grid.GetCellValue(row,
744745
GridColumns.Z.value)) != f"{pixel_coords[2]:.{GRID_PRECISION}f}":
745746
temp_check = True
@@ -783,7 +784,7 @@ def _on_target_changes(self, targets: List[Target]) -> None:
783784
(target.coordinates.value[0], target.coordinates.value[1]), check_bbox=False)
784785
self.grid.SetCellValue(current_row_count, GridColumns.Z.value, "")
785786
else:
786-
pixel_coords = getPixel3DCoordinates(self.correlation_target.fm_streams[0], target.coordinates.value)
787+
pixel_coords = get_pixel_3d_coordinates(self.correlation_target.fm_streams[0], target.coordinates.value)
787788
self.grid.SetCellValue(current_row_count, GridColumns.Z.value,
788789
f"{pixel_coords[2]:.{GRID_PRECISION}f}")
789790
# Set x and y position in the grid
@@ -807,34 +808,58 @@ def _on_target_changes(self, targets: List[Target]) -> None:
807808
if self.check_correlation_conditions():
808809
self._need_reprocessing()
809810

810-
def _on_z_targeting(self, evt) -> None:
811+
def _on_xyz_targeting(self, evt) -> None:
811812
"""
812-
Handle Z-targeting when the Z-targeting button is clicked.
813+
Handle targeting when the targeting button is clicked.
814+
Refactored to perform full 3D Center of Mass targeting (X, Y, Z).
813815
"""
814816
if self._tab_data_model.main.currentTarget.value:
815817

816-
# Select the streams which are visible in the view for Z-targeting
818+
# Select the streams which are visible in the view for targeting
817819
streams_projections = self._tab_data_model.views.value[0].stream_tree.flat.value
818820
if not streams_projections:
819-
wx.MessageBox("FM streams are not available for refining Z", "Error", wx.OK | wx.ICON_ERROR)
821+
wx.MessageBox("FM streams are not available for refining targets", "Error", wx.OK | wx.ICON_ERROR)
820822
return
821823

822-
self.txt_refinez_active.SetLabel("active ...")
823-
wx.CallLater(1000, self.txt_refinez_active.SetLabel, "")
824+
self.txt_refine_xyz_active.SetLabel("active ...")
825+
wx.CallLater(1000, self.txt_refine_xyz_active.SetLabel, "")
824826

825827
coords = self._tab_data_model.main.currentTarget.value.coordinates.value
826-
pixel_coords = getPixel3DCoordinates(self.correlation_target.fm_streams[0], coords)
827-
das = [interpolate_z_stack(da=stream_projection.stream.raw[0]
828-
[:,
829-
int(pixel_coords[1]):int(pixel_coords[1])+1,
830-
int(pixel_coords[0]):int(pixel_coords[0])+1],
831-
method="linear")
832-
for stream_projection in streams_projections]
833-
834-
z = float(get_optimized_z_gauss(das, int(0), int(0), int(pixel_coords[2])))
835-
z_p = getPhysical3DCoordinates(self.correlation_target.fm_streams[0],
836-
(pixel_coords[0],pixel_coords[1], z))[2]
837-
self._tab_data_model.main.currentTarget.value.coordinates.value[2] = z_p
828+
pixel_coords = get_pixel_3d_coordinates(self.correlation_target.fm_streams[0], coords)
829+
830+
# We are going to refine around the clicked position
831+
target_x, target_y = int(pixel_coords[0]), int(pixel_coords[1])
832+
# Ensure multi-channel compatibility
833+
raw_multi = numpy.asarray([d.stream.raw[0] for d in streams_projections])
834+
shape_y, shape_x = raw_multi.shape[-2], raw_multi.shape[-1]
835+
# Get boundary-safe slice & crop
836+
y_start = max(0, target_y - COM_ROI_PADDING)
837+
y_end = min(shape_y, target_y + COM_ROI_PADDING + 1)
838+
x_start = max(0, target_x - COM_ROI_PADDING)
839+
x_end = min(shape_x, target_x + COM_ROI_PADDING + 1)
840+
roi = numpy.s_[:, y_start:y_end, x_start:x_end]
841+
multi_crop = raw_multi[(slice(None),) + roi] # We search along all stack slices (first axis)
842+
# Find best channel and compute COM
843+
best_c = get_brightest_channel(multi_crop)
844+
com = compute_center_of_mass(multi_crop[best_c], baseline_percentile=95.0)
845+
com_z = com[0]
846+
com_y_crop = com[1] + roi[1].start
847+
com_x_crop = com[2] + roi[2].start
848+
849+
# Map back to physical coordinates using optimized X, Y, and Z
850+
physical_coords = get_physical_3d_coordinates(
851+
self.correlation_target.fm_streams[0],
852+
(com_x_crop, com_y_crop, com_z)
853+
)
854+
855+
# Update the model with the refined 3D coordinates
856+
target_coords = self._tab_data_model.main.currentTarget.value.coordinates.value
857+
target_coords[0] = physical_coords[0]
858+
target_coords[1] = physical_coords[1]
859+
target_coords[2] = physical_coords[2]
860+
861+
for vp in self._viewports:
862+
vp.canvas.update_drawing()
838863

839864
def _reorder_grid(self) -> None:
840865
"""

src/odemis/gui/main_xrc.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,8 @@ def __init__(self, parent):
118118
self.fp_correlation_panel = xrc.XRCCTRL(self, "fp_correlation_panel")
119119
self.pnl_correlation = xrc.XRCCTRL(self, "pnl_correlation")
120120
self.btn_delete_row = xrc.XRCCTRL(self, "btn_delete_row")
121-
self.btn_z_targeting = xrc.XRCCTRL(self, "btn_z_targeting")
122-
self.txt_refinez_active = xrc.XRCCTRL(self, "txt_refinez_active")
121+
self.btn_xyz_targeting = xrc.XRCCTRL(self, "btn_xyz_targeting")
122+
self.txt_refine_xyz_active = xrc.XRCCTRL(self, "txt_refine_xyz_active")
123123
self.table_grid = xrc.XRCCTRL(self, "table_grid")
124124
self.txt_correlation_rms = xrc.XRCCTRL(self, "txt_correlation_rms")
125125
self.fp_correlation_streams = xrc.XRCCTRL(self, "fp_correlation_streams")
@@ -2010,14 +2010,14 @@ def __init_resources():
20102010
<flag>wxALL|wxEXPAND</flag>
20112011
<border>10</border>
20122012
</object>
2013-
<!-- Z-targeting button -->
2013+
<!-- XYZ-targeting button -->
20142014
<object class="sizeritem">
2015-
<object class="wxButton" name="btn_z_targeting">
2016-
<label>Refine Z</label>
2015+
<object class="wxButton" name="btn_xyz_targeting">
2016+
<label>Refine</label>
20172017
</object>
20182018
</object>
20192019
<object class="sizeritem">
2020-
<object class="wxStaticText" name="txt_refinez_active">
2020+
<object class="wxStaticText" name="txt_refine_xyz_active">
20212021
<label> </label>
20222022
<fg>#E5E5E5</fg>
20232023
<hidden>1</hidden>

src/odemis/gui/xmlh/resources/dialog_correlation_tdct.xrc

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,14 +86,14 @@
8686
<flag>wxALL|wxEXPAND</flag>
8787
<border>10</border>
8888
</object>
89-
<!-- Z-targeting button -->
89+
<!-- XYZ-targeting button -->
9090
<object class="sizeritem">
91-
<object class="wxButton" name="btn_z_targeting">
92-
<label>Refine Z</label>
91+
<object class="wxButton" name="btn_xyz_targeting">
92+
<label>Refine</label>
9393
</object>
9494
</object>
9595
<object class="sizeritem">
96-
<object class="wxStaticText" name="txt_refinez_active">
96+
<object class="wxStaticText" name="txt_refine_xyz_active">
9797
<label> </label>
9898
<fg>#E5E5E5</fg>
9999
<hidden>1</hidden>

0 commit comments

Comments
 (0)