Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/odemis/acq/feature.py
Original file line number Diff line number Diff line change
Expand Up @@ -811,7 +811,8 @@ def acquire_at_features(


class Target:
def __init__(self, x: float, y: float, z: float, name: str, type: TargetType, index: int, fm_focus_position: float, superz_focused: Optional[bool] = None):
def __init__(self, x: float, y: float, z: float, name: str, type: TargetType, index: int, fm_focus_position: float,
superz_focused: Optional[bool] = None, needs_refinement: Optional[bool] = False) -> None:
"""
Target class to store the target information for multipoint correlation. The target can be a fiducial, point of
interest, surface fiducial, projected fiducial or projected point of interest. The target information is used to
Expand All @@ -826,13 +827,16 @@ def __init__(self, x: float, y: float, z: float, name: str, type: TargetType, in
:param fm_focus_position: position of the focus (objective in cased of Meteor) in meters
:param superz_focused: True if super z focus has high accuracy, False otherwise. If None, it means that the
super z focus was not used.
:param needs_refinement: Whether target needs further algorithmic refinement.
"""
self.coordinates = model.ListVA((x, y, z), unit="m")
self.type = model.VAEnumerated(type, choices={t for t in TargetType})
self.name = model.StringVA(name)
self.index = model.IntContinuous(index, range=(1, 99))
self.fm_focus_position = model.FloatVA(fm_focus_position, unit="m")
self.superz_focused = superz_focused
# No need to store this on disk, so leaving it out of to/from_dict methods
self.needs_refinement = model.BooleanVA(needs_refinement)

def to_dict(self) -> dict:
return {
Expand Down
1 change: 1 addition & 0 deletions src/odemis/gui/comp/overlay/cryo_feature.py
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,7 @@ def on_left_down(self, evt):
self.cnvs.set_dynamic_cursor(gui.DRAG_CURSOR)
else:
self.tab_data.add_new_target(p_pos[0], p_pos[1], type=TargetType.PointOfInterest)

elif self._mode == MODE_EDIT_REFRACTIVE_INDEX and TargetType.SurfaceFiducial in self.allowed_targets:
# Check for existing surface fiducial in order to change the position or introduce a new one.
existing_surface = next((target for target in self.tab_data.main.targets.value if target.type.value == TargetType.SurfaceFiducial), None)
Expand Down
2 changes: 1 addition & 1 deletion src/odemis/gui/cont/acquisition/cryo_z_localization.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ def _on_target_focus_pos(self, target_coordinates: List[float]) -> None:
"""
# Set the target Z ctrl with the focus position
self._panel.ctrl_target_z.SetValue(target_coordinates[2])
save_project(self._tab_data_model.main)
save_project(self._tab_data.main)

def _on_ctrl_target_z_change(self) -> List[float]:
"""
Expand Down
185 changes: 112 additions & 73 deletions src/odemis/gui/cont/multi_point_correlation.py

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions src/odemis/gui/main_xrc.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,8 @@ def __init__(self, parent):
self.fp_correlation_panel = xrc.XRCCTRL(self, "fp_correlation_panel")
self.pnl_correlation = xrc.XRCCTRL(self, "pnl_correlation")
self.btn_delete_row = xrc.XRCCTRL(self, "btn_delete_row")
self.btn_z_targeting = xrc.XRCCTRL(self, "btn_z_targeting")
self.txt_refinez_active = xrc.XRCCTRL(self, "txt_refinez_active")
self.btn_xyz_targeting = xrc.XRCCTRL(self, "btn_xyz_targeting")
self.txt_refine_xyz_active = xrc.XRCCTRL(self, "txt_refine_xyz_active")
self.table_grid = xrc.XRCCTRL(self, "table_grid")
self.txt_correlation_rms = xrc.XRCCTRL(self, "txt_correlation_rms")
self.fp_correlation_streams = xrc.XRCCTRL(self, "fp_correlation_streams")
Expand Down Expand Up @@ -2010,14 +2010,14 @@ def __init_resources():
<flag>wxALL|wxEXPAND</flag>
<border>10</border>
</object>
<!-- Z-targeting button -->
<!-- XYZ-targeting button -->
<object class="sizeritem">
<object class="wxButton" name="btn_z_targeting">
<label>Refine Z</label>
<object class="wxButton" name="btn_xyz_targeting">
<label>Refine</label>
</object>
</object>
<object class="sizeritem">
<object class="wxStaticText" name="txt_refinez_active">
<object class="wxStaticText" name="txt_refine_xyz_active">
<label> </label>
<fg>#E5E5E5</fg>
<hidden>1</hidden>
Expand Down
4 changes: 2 additions & 2 deletions src/odemis/gui/model/tab_gui_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,11 +294,11 @@ def add_new_target(self, x: float, y: float, type: TargetType, z: Optional[float
t_name = make_unique_name("FM-1", existing_names)
index = int(re.search(TARGET_INDEX, t_name).group(1))
target = Target(x, y, z=z_val, name=t_name, type=type,
index=index, fm_focus_position=fm_focus_position)
index=index, fm_focus_position=fm_focus_position, needs_refinement=True)

elif type == TargetType.PointOfInterest:
target = Target(x, y, z=z_val, name="POI-1", type=type,
index=1, fm_focus_position=fm_focus_position)
index=1, fm_focus_position=fm_focus_position, needs_refinement=True)

elif type == TargetType.SurfaceFiducial:
target = Target(x, y, z=0, name="FIB_surface", type=type, index=1,
Expand Down
8 changes: 4 additions & 4 deletions src/odemis/gui/xmlh/resources/dialog_correlation_tdct.xrc
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,14 @@
<flag>wxALL|wxEXPAND</flag>
<border>10</border>
</object>
<!-- Z-targeting button -->
<!-- XYZ-targeting button -->
<object class="sizeritem">
<object class="wxButton" name="btn_z_targeting">
<label>Refine Z</label>
<object class="wxButton" name="btn_xyz_targeting">
<label>Refine</label>
</object>
</object>
<object class="sizeritem">
<object class="wxStaticText" name="txt_refinez_active">
<object class="wxStaticText" name="txt_refine_xyz_active">
<label> </label>
<fg>#E5E5E5</fg>
<hidden>1</hidden>
Expand Down
30 changes: 30 additions & 0 deletions src/odemis/util/img.py
Original file line number Diff line number Diff line change
Expand Up @@ -1441,3 +1441,33 @@ def apply_zoom_on_image_coordinates(rect: List[int], z:int) -> List[int]:
:return: (list of int) the rectangle in image pixel coordinates at the given zoom level
"""
return [px // (2 ** z) for px in rect]


Comment thread
tmoerkerken marked this conversation as resolved.
def get_brightest_channel(sub_image_multi: numpy.ndarray) -> int:
"""
Finds the index of the channel with the highest maximum intensity.
Comment thread
tmoerkerken marked this conversation as resolved.
Useful for multi-channel imaging to select the brightest channel for analysis.

:param sub_image_multi: Multi-channel sub-image as (C, Z, Y, X) array
:returns: Index of the channel with the highest maximum intensity.
"""
channel_maxes = numpy.max(sub_image_multi, axis=(1, 2, 3))
return int(numpy.argmax(channel_maxes))


Comment thread
tmoerkerken marked this conversation as resolved.
def compute_center_of_mass(image: numpy.ndarray,
baseline_ratio: float = 0.95, roi_slice: tuple = None) -> Tuple[float, float, float]:
"""
Computes center of mass with baselining.
Uses the brightest pixels (above the baseline percentile) as weights for center of mass calculation,
effectively filtering background noise.

:param image: Single-channel 3D sub-image as (Z, Y, X) array
:param baseline_ratio: Ratio for background separation (0-1). Values lower than the baseline
will be discarded.
:returns: Tuple of (global_z, global_y, global_x) in pixel coordinates maintaining input order.
"""
baseline = numpy.percentile(image, baseline_ratio * 100)
image_weights = numpy.where(image > baseline, image - baseline, 0)
com = scipy.ndimage.center_of_mass(numpy.asarray(image_weights))
return com
Comment thread
tmoerkerken marked this conversation as resolved.
49 changes: 49 additions & 0 deletions src/odemis/util/test/img_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2318,5 +2318,54 @@ def test_tint_rgba(self):
# red becomes black (255*0, 0*1, 0*0), alpha preserved
numpy.testing.assert_array_equal(out[1, 1], [0, 0, 0, 255])


class TestCenterOfMassTargeting(unittest.TestCase):
"""Test 3D Center of Mass methods."""

def test_brightest_channel(self):
"""Test channel selection by maximum intensity."""
# 4D array: (C, Z, Y, X)
multi_channel = numpy.random.rand(3, 10, 20, 20)
multi_channel[1] = numpy.ones((10, 20, 20)) * 100 # Make channel 1 bright
best_c = img.get_brightest_channel(multi_channel)
self.assertEqual(best_c, 1)

def test_compute_local_center_of_mass_with_noise(self):
"""Test COM computation correctly filters background noise."""
# Create 3D image with strong peak and weak noise
sub_image = numpy.zeros((10, 20, 20))
# Add signal (strong)
for z in range(4, 7):
for y in range(8, 13):
for x in range(8, 13):
sub_image[z, y, x] = 100.0
# Add weak noise (much smaller than signal)
sub_image += numpy.random.rand(10, 20, 20) * 2.0

Comment on lines +2328 to +2344
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests rely on numpy.random without seeding. While failures may be unlikely with the current ranges, unseeded randomness can still produce flaky tests and makes failures hard to reproduce. Prefer a fixed seed via numpy.random.default_rng(seed) (or fully deterministic arrays) for both test_brightest_channel and the COM noise test.

Copilot uses AI. Check for mistakes.
target_y = 5
target_x = 10
pad_y = 5
pad_x = 10
shape_y = 20
shape_x = 20
# Get boundary-safe slice & crop
y_start = max(0, target_y - pad_y)
y_end = min(shape_y, target_y + pad_y + 1)
x_start = max(0, target_x - pad_x)
x_end = min(shape_x, target_x + pad_x + 1)
roi = numpy.s_[:, y_start:y_end, x_start:x_end]

com = img.compute_center_of_mass(sub_image, baseline_ratio=0.95)
com_y_crop = com[1] + roi[1].start
com_x_crop = com[2] + roi[2].start

# COM z should be near peak (z=4-6, center at 5)
self.assertAlmostEqual(com[0], 5.0, delta=1.0)
# COM should be within the extracted ROI [5:16] range
self.assertGreaterEqual(com_x_crop, 5)
self.assertLess(com_x_crop, 16)
self.assertGreaterEqual(com_y_crop, 5)
self.assertLess(com_y_crop, 16)

if __name__ == "__main__":
unittest.main()
Loading