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
@@ -97,7 +99,7 @@ def getPixel3DCoordinates(stream: FluoStream, p_pos: Tuple[float, float, float],
9799 pixel_pos = (pixel_pos [0 ], pixel_pos [1 ], z )
98100 return pixel_pos
99101
100- def getPhysical3DCoordinates (stream : FluoStream , pixel_pos : Tuple [float , float , float ])\
102+ def get_physical_3d_coordinates (stream : FluoStream , pixel_pos : Tuple [float , float , float ])\
101103 -> Optional [Tuple [float , float , float ]]:
102104 """
103105 Translate 3D pixel coordinates into 3D physical coordinates. The z coordinate is computed assuming iso-voxel
@@ -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,69 @@ 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+ target_x , target_y = int (pixel_coords [0 ]), int (pixel_coords [1 ])
831+
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+
836+ # Get boundary-safe slice & crop
837+ y_start = max (0 , target_y - COM_ROI_PADDING )
838+ y_end = min (shape_y , target_y + COM_ROI_PADDING + 1 )
839+ x_start = max (0 , target_x - COM_ROI_PADDING )
840+ x_end = min (shape_x , target_x + COM_ROI_PADDING + 1 )
841+ roi = numpy .s_ [:, y_start :y_end , x_start :x_end ]
842+
843+ multi_crop = raw_multi [(slice (None ),) + roi ]
844+
845+ # Find best channel and compute COM
846+ best_c = get_brightest_channel (multi_crop )
847+
848+ com = compute_center_of_mass (multi_crop [best_c ], baseline_percentile = 95.0 )
849+ com_z = com [0 ]
850+ com_y_crop = com [1 ] + roi [1 ].start
851+ com_x_crop = com [2 ] + roi [2 ].start
852+ # Account for iso-voxel enforcement using best channel's metadata
853+ # Z array indices must be scaled to match the iso-voxel pixel size
854+ best_stream = streams_projections [best_c ].stream
855+ best_channel_raw = best_stream .raw [0 ]
856+ md = best_stream ._find_metadata (best_channel_raw .metadata )
857+ pxs = md .get (model .MD_PIXEL_SIZE , (1e-6 , 1e-6 ))
858+ com_y = com_z * pxs [2 ] / pxs [0 ]
859+
860+ # Map back to physical coordinates using optimized X, Y, and Z
861+ physical_coords = get_physical_3d_coordinates (
862+ self .correlation_target .fm_streams [0 ],
863+ (com_x_crop , com_y_crop , com_y )
864+ )
865+
866+ # Update the model with the refined 3D coordinates
867+ target_coords = self ._tab_data_model .main .currentTarget .value .coordinates .value
868+ target_coords [0 ] = physical_coords [0 ]
869+ target_coords [1 ] = physical_coords [1 ]
870+ target_coords [2 ] = physical_coords [2 ]
871+
872+ for vp in self ._viewports :
873+ vp .canvas .update_drawing ()
838874
839875 def _reorder_grid (self ) -> None :
840876 """
0 commit comments