@@ -1197,32 +1197,46 @@ def _maybe_float(v):
11971197 # ------------------------------------------------------------------
11981198 import napari as _napari
11991199
1200+ def _label_centroid_world (layer , label_val ):
1201+ import numpy as _np
1202+ data = layer .data
1203+ if hasattr (data , 'compute' ):
1204+ data = data .compute ()
1205+ data = _np .asarray (data )
1206+ coords = _np .argwhere (data == int (label_val ))
1207+ if len (coords ) == 0 :
1208+ return None
1209+ centroid_idx = coords .mean (axis = 0 )
1210+ return tuple (layer .data_to_world (centroid_idx ))
1211+
12001212 def _gcamp_label_at_pos (pos ):
12011213 for layer in viewer .layers :
12021214 if isinstance (layer , _napari .layers .Labels ) and layer .name .startswith (gcamp_layer_prefix ):
12031215 val = layer .get_value (pos , world = True )
1204- return val , None
1205- return None , f"No Labels layer with prefix '{ gcamp_layer_prefix } ' found."
1216+ return val , layer , None
1217+ return None , None , f"No Labels layer with prefix '{ gcamp_layer_prefix } ' found."
12061218
12071219 def _xenium_label_at_pos (pos ):
1208- """Returns (hits, warning) where hits is a list of (cell_id, layer_name).
1220+ """Returns (hits, hit_layers, warning) where hits is a list of (cell_id, layer_name).
12091221 If multiple layers overlap, all are returned with a warning string."""
12101222 hits = []
1223+ hit_layers = []
12111224 found_any = False
12121225 for layer in viewer .layers :
12131226 if isinstance (layer , _napari .layers .Labels ) and layer .name .startswith (xenium_layer_prefix ):
12141227 found_any = True
12151228 val = layer .get_value (pos , world = True )
12161229 if val is not None and val != 0 :
12171230 hits .append ((int (val ), layer .name ))
1231+ hit_layers .append (layer )
12181232 if not found_any :
1219- return [], f"No Labels layer with prefix '{ xenium_layer_prefix } ' found."
1233+ return [], [], f"No Labels layer with prefix '{ xenium_layer_prefix } ' found."
12201234 if not hits :
1221- return [], "Clicked background in all Xenium layers. Try again."
1235+ return [], [], "Clicked background in all Xenium layers. Try again."
12221236 if len (hits ) > 1 :
12231237 names = ', ' .join (f"{ n } (id={ v } )" for v , n in hits )
1224- return hits , f"Warning: overlapping sections — will save { len (hits )} rows: { names } "
1225- return hits , None
1238+ return hits , hit_layers , f"Warning: overlapping sections — will save { len (hits )} rows: { names } "
1239+ return hits , hit_layers , None
12261240
12271241 # ------------------------------------------------------------------
12281242 # UI
@@ -1272,7 +1286,7 @@ def _xenium_label_at_pos(pos):
12721286 btn_row .addWidget (b )
12731287 layout .addLayout (btn_row )
12741288
1275- status = QLabel ("Click 'Start Matching' to begin. " )
1289+ status = QLabel ("" )
12761290 status .setWordWrap (True )
12771291 layout .addWidget (status )
12781292
@@ -1430,8 +1444,33 @@ def _navigate_to(row, col):
14301444 if any (p is None for p in pos ):
14311445 status .setText ("No coordinates stored for this row (loaded from older CSV)." )
14321446 return
1433- viewer .camera .center = pos
1434- status .setText (f"Navigated to { 'GCaMP' if col == 0 else 'Xenium' } cell position." )
1447+ z , y , x = pos
1448+ # Move z-slider to the cell's plane
1449+ viewer .dims .set_point (0 , z )
1450+ viewer .camera .center = (z , y , x )
1451+ label = 'GCaMP' if col == 0 else 'Xenium'
1452+ status .setText (f"Navigated to { label } cell — z={ z :.2f} , y={ y :.2f} , x={ x :.2f} " )
1453+
1454+ def _show_row_coords ():
1455+ selected = table .selectedItems ()
1456+ if not selected :
1457+ return
1458+ row = selected [0 ].row ()
1459+ item = table .item (row , 0 )
1460+ if item is None :
1461+ return
1462+ orig_idx = item .data (Qt .UserRole )
1463+ if orig_idx is None or orig_idx >= len (matches ):
1464+ return
1465+ m = matches [orig_idx ]
1466+ gz , gy , gx = m [3 ], m [4 ], m [5 ]
1467+ xz , xy , xx = m [6 ], m [7 ], m [8 ]
1468+ def _fmt (v ):
1469+ return f"{ v :.2f} " if v is not None else "?"
1470+ status .setText (
1471+ f"GCaMP { m [0 ]} : z={ _fmt (gz )} , y={ _fmt (gy )} , x={ _fmt (gx )} | "
1472+ f"Xenium { m [1 ]} : z={ _fmt (xz )} , y={ _fmt (xy )} , x={ _fmt (xx )} "
1473+ )
14351474
14361475 # Canvas click handler — fires on every left-click while armed.
14371476 # napari mouse_drag_callbacks must be generator functions.
@@ -1442,24 +1481,25 @@ def _on_canvas_click(viewer_obj, event):
14421481 state ['armed' ] = None
14431482 _refresh_armed ()
14441483 if armed == 'gcamp' :
1445- val , err = _gcamp_label_at_pos (pos )
1484+ val , layer , err = _gcamp_label_at_pos (pos )
14461485 if err :
14471486 status .setText (err )
14481487 elif val is None or val == 0 :
14491488 status .setText ("Clicked background in GCaMP layer. Try again." )
14501489 else :
14511490 state ['gcamp_id' ] = int (val )
1452- state ['gcamp_pos' ] = tuple ( pos )
1491+ state ['gcamp_pos' ] = _label_centroid_world ( layer , val )
14531492 _refresh_pending ()
14541493 _check_dupes ()
14551494 status .setText (f"GCaMP cell { val } picked." )
14561495 else :
1457- hits , warn = _xenium_label_at_pos (pos )
1496+ hits , hit_layers , warn = _xenium_label_at_pos (pos )
14581497 if not hits :
14591498 status .setText (warn )
14601499 else :
14611500 state ['xenium_hits' ] = hits
1462- state ['xenium_pos' ] = tuple (pos )
1501+ # Use centroid of first hit layer for navigation (representative position)
1502+ state ['xenium_pos' ] = _label_centroid_world (hit_layers [0 ], hits [0 ][0 ])
14631503 _refresh_pending ()
14641504 _check_dupes ()
14651505 if warn :
@@ -1472,7 +1512,31 @@ def _on_canvas_click(viewer_obj, event):
14721512
14731513 viewer .mouse_drag_callbacks .append (_on_canvas_click )
14741514
1515+ # # ------------------------------------------------------------------
1516+ # # Z-slider section boundary notifications
1517+ # # ------------------------------------------------------------------
1518+ # _z_section_state = {'last_sections': set()}
1519+
1520+ # def _on_z_change():
1521+ # current_z = viewer.dims.point[0]
1522+ # active_now = set()
1523+ # for layer in viewer.layers:
1524+ # if isinstance(layer, _napari.layers.Labels) and layer.name.startswith(xenium_layer_prefix):
1525+ # zmin, zmax = layer.extent.world[0][0], layer.extent.world[1][0]
1526+ # if zmin <= current_z <= zmax:
1527+ # active_now.add(layer.name)
1528+ # entered = active_now - _z_section_state['last_sections']
1529+ # exited = _z_section_state['last_sections'] - active_now
1530+ # if entered or exited:
1531+ # msgs = [f"Entering {n}" for n in sorted(entered)] + \
1532+ # [f"Leaving {n}" for n in sorted(exited)]
1533+ # status.setText(" | ".join(msgs))
1534+ # _z_section_state['last_sections'] = active_now
1535+
1536+ # viewer.dims.events.current_step.connect(_on_z_change)
1537+
14751538 table .cellDoubleClicked .connect (_navigate_to )
1539+ table .itemSelectionChanged .connect (_show_row_coords )
14761540
14771541 pick_gcamp_btn .clicked .connect (lambda : _arm ('gcamp' ))
14781542 pick_xenium_btn .clicked .connect (lambda : _arm ('xenium' ))
0 commit comments