6464 lambda x , y , H , W : (W - 1 - y , H - 1 - x )),
6565]
6666
67- def get_bigwarp_params (bigwarp_json_path ):
68- with open (bigwarp_json_path , 'r' ) as f :
69- bw_json = json .load (f )
70- for source , source_data in bw_json ['Sources' ].items ():
71- if source_data ['isMoving' ]:
72- if 'confocal' in source_data ['uri' ].lower ():
73- continue
74- moving_image_path = Path (source_data ['uri' ])
75- if moving_image_path .name == '?' :
76- moving_image_path = moving_image_path .parent
77- else :
78- reference_image_path = Path (source_data ['uri' ])
79- if reference_image_path .name == '?' :
80- reference_image_path = reference_image_path .parent
81- if 'zstack' in moving_image_path .name .lower () or 'z_stack' in moving_image_path .name .lower ():
82- moving_image = 'czstack'
83- reference_image = 'xenium_section'
84- transform_type = bw_json ['Transform' ]['type' ]
85- bigwarp_params = {'moving_image' : moving_image ,
86- 'reference_image' : reference_image ,
87- 'moving_image_path' : moving_image_path ,
88- 'reference_image_path' : reference_image_path ,
89- 'transform_type' : transform_type }
90- return bigwarp_params
91-
9267def invert_xenium_y_landmarks (landmarks , landmarked_image_path ):
9368 with tifffile .TiffFile (landmarked_image_path ) as tif :
9469 landmarked_image_shape = tif .pages [0 ].shape
@@ -107,28 +82,6 @@ def remove_landmark_buffer(landmarks, czstack_buffer=None, xenium_buffer=None):
10782 landmarks ['xenium_z' ] = landmarks ['xenium_z' ] - xenium_buffer .get ('z' , 0 )
10883 return landmarks
10984
110- def get_section_landmarks (landmarks_path = None , dims_order = ['x' ,'y' ,'z' ], bigwarp_project_path = None , moving_img = None ):
111- if landmarks_path is not None :
112- if bigwarp_project_path is not None :
113- bigwarp_params = get_bigwarp_params (bigwarp_project_path )
114- elif moving_img is not None :
115- bigwarp_params = {'moving_image' : moving_img }
116- else :
117- print ("No BigWarp project path or moving image specified - assuming czstack was moving image" )
118- bigwarp_params = {'moving_image' : 'czstack' }
119- print (f"Loading landmarks from: { landmarks_path } " )
120- landmarks = pd .read_csv (landmarks_path , header = None )
121- if bigwarp_params ['moving_image' ]== 'czstack' :
122- # Flatten the lists using + operator to concatenate them
123- landmarks .columns = ['landmark_name' , 'active' ] + [f'czstack_{ dim } ' for dim in dims_order ] + [f'xenium_{ dim } ' for dim in dims_order ]
124- else :
125- landmarks .columns = ['landmark_name' , 'active' ] + [f'xenium_{ dim } ' for dim in dims_order ] + [f'czstack_{ dim } ' for dim in dims_order ]
126- else :
127- landmarks = None
128- bigwarp_params = None
129-
130- return landmarks , bigwarp_params
131-
13285def _normalize_bw_uri (uri ):
13386 """Normalize a BigWarp source URI to a Path.
13487
@@ -302,6 +255,90 @@ def plot_img_landmark_transforms(sdata_img,
302255 else :
303256 plt .close (fig )
304257
258+
259+ def plot_manual_landmark_transforms (landmarks_before ,
260+ landmarks_after ,
261+ landmarked_image_path ,
262+ sdata ,
263+ landmarks_tf_info = None ,
264+ section = None ,
265+ save_path = None ,
266+ show = True ):
267+ """Visualise the manual landmark coordinate transform.
268+
269+ Two-panel figure:
270+ Left — landmarked TIFF with original (xenium_x, xenium_y) landmarks in
271+ landmarked-image pixel space.
272+ Right — sdata ``morphology_focus`` (full resolution) with transformed
273+ (xenium_x, xenium_y) landmarks mapped into sdata pixel space,
274+ after all scaling / inversion / bbox-offset corrections.
275+
276+ Parameters
277+ ----------
278+ landmarks_before : pd.DataFrame
279+ CSV-loaded landmark table with ``xenium_x``, ``xenium_y`` columns in
280+ landmarked-image pixel space (before any coordinate correction).
281+ landmarks_after : pd.DataFrame
282+ Same table after coordinate corrections (scaling, inversion, offset)
283+ but *before* the column rename to ``x``/``y``. Still has
284+ ``xenium_x``/``xenium_y`` columns, now in sdata pixel space.
285+ landmarked_image_path : Path or str
286+ TIFF that was used for BigWarp landmarking.
287+ sdata : SpatialData
288+ Section SpatialData object (already loaded).
289+ landmarks_tf_info : dict or None
290+ Dict returned by ``get_landmarked_image_props``. Used to annotate
291+ scale factors and bbox offset on the right panel.
292+ section : int or str, optional
293+ Section number used as figure suptitle.
294+ save_path : Path or str, optional
295+ show : bool
296+ Display inline. Set ``False`` when called from a worker thread.
297+ """
298+ # ── Load landmarked TIFF ──────────────────────────────────────────────
299+ with tifffile .TiffFile (landmarked_image_path ) as tif :
300+ lm_stack = tif .asarray ()
301+ lm_img = lm_stack if lm_stack .ndim == 2 else lm_stack [1 ]
302+
303+ # ── Load sdata morphology at full resolution ──────────────────────────
304+ morph = sdata ['morphology_focus' ]
305+ sdata_img = np .asarray (sd .get_pyramid_levels (morph , n = 0 )[1 ])
306+
307+ fig , axes = plt .subplots (1 , 2 , figsize = (14 , 6 ))
308+
309+ # ── Left: landmarked image + original landmarks ───────────────────────
310+ axes [0 ].imshow (lm_img , cmap = 'gray' )
311+ if 'xenium_x' in landmarks_before .columns :
312+ axes [0 ].scatter (landmarks_before ['xenium_x' ], landmarks_before ['xenium_y' ],
313+ c = 'red' , s = 15 , zorder = 5 )
314+ axes [0 ].set_title ('Landmarked image + original landmarks (image pixel space)' )
315+
316+ # ── Right: sdata image + transformed landmarks ────────────────────────
317+ axes [1 ].imshow (sdata_img , cmap = 'gray' )
318+ axes [1 ].scatter (landmarks_after ['xenium_x' ], landmarks_after ['xenium_y' ],
319+ c = 'red' , s = 15 , zorder = 5 )
320+ subtitle = 'sdata morphology_focus (scale0) + transformed landmarks'
321+ if landmarks_tf_info is not None :
322+ sx = landmarks_tf_info .get ('scale_factor_x' )
323+ sy = landmarks_tf_info .get ('scale_factor_y' )
324+ bbox = landmarks_tf_info .get ('bbox' )
325+ if sx is not None and sy is not None :
326+ subtitle += f'\n scale (x, y): ({ sx :.3f} , { sy :.3f} )'
327+ if bbox is not None :
328+ subtitle += f' | bbox offset: ({ bbox ["x_min" ]} , { bbox ["y_min" ]} )'
329+ axes [1 ].set_title (subtitle )
330+
331+ plt .tight_layout ()
332+ if section is not None :
333+ plt .suptitle (f'Section: { section } ' , y = 1.01 )
334+ if save_path is not None :
335+ plt .savefig (save_path , bbox_inches = 'tight' )
336+ if show :
337+ plt .show ()
338+ else :
339+ plt .close (fig )
340+
341+
305342def find_landmarked_img_transforms (landmarked_image_path ,
306343 sdata_path ,
307344 landmarks ,
@@ -486,13 +523,20 @@ def _process_section(s_n, paths, alignment_params):
486523 print (f" Section { s_n } : landmarks not found at { landmarks_path } , skipping" )
487524 return s_n , None
488525 print (f" Manually transforming landmarks..." )
489- landmarked_image_path = paths ['data_root' ] / alignment_params ['landmarked_images_folder' ] / alignment_params ['landmarked_images_names_fn' ](s_n )
490- formatted_landmarks = manual_landmarks_transform (s_n = s_n ,
491- sdata_path = sdata_path ,
492- landmarks_path = landmarks_path ,
493- landmarked_image_path = landmarked_image_path ,
494- alignment_params = alignment_params ,
495- bigwarp_params = bigwarp_params )
526+ landmarked_image_path = (paths ['data_root' ]
527+ / alignment_params ['landmarked_images_folder' ]
528+ / alignment_params ['landmarked_images_names_fn' ](s_n ))
529+ val_folder = alignment_params .get ('validation_images_folder' )
530+ save_imgs_path = val_folder / f"section_{ s_n } _manual.png" if val_folder is not None else None
531+ formatted_landmarks = manual_landmarks_transform (
532+ s_n = s_n ,
533+ sdata_path = sdata_path ,
534+ landmarks_path = landmarks_path ,
535+ landmarked_image_path = landmarked_image_path ,
536+ alignment_params = alignment_params ,
537+ bigwarp_params = bigwarp_params ,
538+ save_imgs_path = save_imgs_path ,
539+ )
496540
497541 else :
498542 print (f"Formatting section { s_n } landmarks" )
@@ -511,36 +555,59 @@ def _process_section(s_n, paths, alignment_params):
511555 return s_n , formatted_landmarks
512556
513557
514- def manual_landmarks_transform (s_n , sdata_path , landmarks_path , landmarked_image_path , alignment_params , bigwarp_params , dims_order = ['x' ,'y' ,'z' ]):
558+ def manual_landmarks_transform (s_n , sdata_path , landmarks_path , landmarked_image_path ,
559+ alignment_params , bigwarp_params ,
560+ dims_order = ['x' , 'y' , 'z' ],
561+ save_imgs_path = None ):
562+ # ── Read sdata once — needed for pixel size and get_landmarked_image_props ──
563+ sdata = sd .read_zarr (sdata_path )
564+ full_scale_pixel_size = sdata ['table' ].uns ['section_metadata' ]['pixel_size' ]
565+
515566 starting_lm_df = pd .read_csv (landmarks_path , header = None )
516- if bigwarp_params ['moving_image_dataset' ]== 'czstack' :
517- starting_lm_df .columns = ['landmark_name' , 'active' ] + [f'czstack_{ dim } ' for dim in dims_order ] + [f'xenium_{ dim } ' for dim in dims_order ]
567+ if bigwarp_params ['moving_image_dataset' ] == 'czstack' :
568+ starting_lm_df .columns = (['landmark_name' , 'active' ]
569+ + [f'czstack_{ d } ' for d in dims_order ]
570+ + [f'xenium_{ d } ' for d in dims_order ])
518571 else :
519- starting_lm_df .columns = ['landmark_name' , 'active' ] + [f'xenium_{ dim } ' for dim in dims_order ] + [f'czstack_{ dim } ' for dim in dims_order ]
520-
521- landmarks_out = starting_lm_df .copy ()
522- if alignment_params .get ('czstack_buffer' , None ) is not None :
523- landmarks_out = remove_landmark_buffer (landmarks_out , czstack_buffer = alignment_params ['czstack_buffer' ])
524-
525- if alignment_params .get ('fix_cropped_landmarks' , None ) and landmarked_image_path is not None :
526- landmarks_out , landmarks_info = get_landmarked_image_props (
527- landmarked_image_path ,
528- sdata_path ,
529- landmarks_out ,
530- s_n ,
531- invert_y = alignment_params .get ('invert_lm_y' , None ))
532- elif alignment_params .get ('invert_lm_y' , None ) and landmarked_image_path is not None :
572+ starting_lm_df .columns = (['landmark_name' , 'active' ]
573+ + [f'xenium_{ d } ' for d in dims_order ]
574+ + [f'czstack_{ d } ' for d in dims_order ])
575+
576+ landmarks_out = starting_lm_df .copy ()
577+ landmarks_tf_info = None
578+
579+ if alignment_params .get ('czstack_buffer' ) is not None :
580+ landmarks_out = remove_landmark_buffer (
581+ landmarks_out , czstack_buffer = alignment_params ['czstack_buffer' ])
582+
583+ if alignment_params .get ('fix_cropped_landmarks' , False ) and landmarked_image_path is not None :
584+ landmarks_out , landmarks_tf_info = get_landmarked_image_props (
585+ landmarked_image_path , sdata , landmarks_out , s_n ,
586+ invert_y = alignment_params .get ('invert_lm_y' , False ))
587+ elif alignment_params .get ('invert_lm_y' , False ) and landmarked_image_path is not None :
533588 landmarks_out = invert_xenium_y_landmarks (landmarks_out , landmarked_image_path )
534- landmarks_out = landmarks_out .rename (columns = {'xenium_x' : 'x' , 'xenium_y' : 'y' , 'xenium_z' : 'z' })
535- full_scale_pixel_size = landmarks_info .get ('full_scale_pixel_size' )
589+
590+ if save_imgs_path is not None :
591+ plot_manual_landmark_transforms (
592+ landmarks_before = starting_lm_df ,
593+ landmarks_after = landmarks_out ,
594+ landmarked_image_path = landmarked_image_path ,
595+ sdata = sdata ,
596+ landmarks_tf_info = landmarks_tf_info ,
597+ section = s_n ,
598+ save_path = save_imgs_path ,
599+ show = False ,
600+ )
601+
602+ landmarks_out = landmarks_out .rename (
603+ columns = {'xenium_x' : 'x' , 'xenium_y' : 'y' , 'xenium_z' : 'z' })
536604 parsed_lm = PointsModel .parse (landmarks_out )
537605 set_transformation (parsed_lm , Identity (), to_coordinate_system = 'global' )
538606 set_transformation (
539607 parsed_lm ,
540608 Scale ([full_scale_pixel_size , full_scale_pixel_size ], axes = ('x' , 'y' )),
541- to_coordinate_system = 'microns'
609+ to_coordinate_system = 'microns' ,
542610 )
543-
544611 return parsed_lm
545612
546613def get_section_landmarks_threads (xenium_section_ns , paths , alignment_params , n_workers = None ):
@@ -742,48 +809,3 @@ def get_landmarked_image_props(landmarked_image_path, sdata, landmarks,
742809
743810 return landmarks , landmarks_tf_info
744811
745- def format_landmarks (sdata_path ,
746- landmarks_path ,
747- section_n ,
748- bigwarp_project_paths = None ,
749- moving_img = None ,
750- czstack_buffer = None ,
751- invert_lm_y = False ,
752- fix_cropped_landmarks = False ,
753- landmarked_image_path = None ,
754- dims_order = ['x' ,'y' ,'z' ]):
755- sdata = sd .read_zarr (sdata_path )
756- landmarks , bigwarp_params = get_section_landmarks (
757- landmarks_path = landmarks_path ,
758- bigwarp_project_path = bigwarp_project_paths ,
759- moving_img = moving_img ,
760- dims_order = dims_order )
761-
762- if czstack_buffer is not None :
763- landmarks = remove_landmark_buffer (landmarks ,
764- czstack_buffer = czstack_buffer )
765-
766- # if fix_cropped_landmarks and landmarked_image_path is not None:
767- # invert_y is handled inside get_landmarked_image_props at the right point
768- landmarks , landmarks_tf_info = get_landmarked_image_props (
769- landmarked_image_path ,
770- sdata ,
771- landmarks ,
772- section_n ,
773- invert_y = invert_lm_y )
774- # elif invert_lm_y and landmarked_image_path is not None:
775- # # sections where fix_cropped_landmarks=False
776- # landmarks = invert_xenium_y_landmarks(landmarks, landmarked_image_path)
777-
778- # Adjust landmarks resolutions
779- full_scale_pixel_size = landmarks_tf_info ['fullres_pixel_size' ]
780- landmarks = landmarks .rename (columns = {'xenium_x' : 'x' , 'xenium_y' : 'y' , 'xenium_z' : 'z' })
781- landmarks = PointsModel .parse (landmarks )
782- set_transformation (landmarks , Identity (), to_coordinate_system = 'global' )
783- set_transformation (
784- landmarks ,
785- Scale ([full_scale_pixel_size , full_scale_pixel_size ], axes = ('x' , 'y' )),
786- to_coordinate_system = 'microns'
787- )
788- return landmarks
789-
0 commit comments