3535# This is not related to any particular wxPython version and is most likely permanent.
3636
3737from 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
3939from odemis .acq .feature import FIBFMCorrelationData , Target , TargetType
4040from odemis .acq .stream import StaticFluoStream , StaticSEMStream , StaticFIBStream , FluoStream
4141from odemis .gui import conf
4444from odemis .gui .util import call_in_wx_main
4545from odemis .model import ListVA
4646from 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
4848from odemis .util .units import readable_str
4949
5050# create an enum with column labels and position
@@ -62,12 +62,14 @@ class GridColumns(Enum):
6262FIDUCIAL_PATTERN = r"^[^-]+-"
6363RIM_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
118120def 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 """
0 commit comments