Skip to content

Commit 210c927

Browse files
committed
fix: restore TIFF axis-order heuristic + wrap plot_manual_landmark_transforms in _MATPLOTLIB_LOCK
1 parent aaf6f30 commit 210c927

1 file changed

Lines changed: 67 additions & 41 deletions

File tree

src/xenium_analysis_tools/alignment/process_landmarks.py

Lines changed: 67 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,14 @@
1919
from spatialdata._io._utils import _resolve_zarr_store
2020
from tqdm.notebook import tqdm
2121
import json
22+
import threading
2223
from concurrent.futures import ThreadPoolExecutor, as_completed
2324

25+
# Matplotlib's pyplot interface is NOT thread-safe. All plot functions that
26+
# may be called from worker threads must acquire this lock before touching any
27+
# plt.* state.
28+
_MATPLOTLIB_LOCK = threading.Lock()
29+
2430
from spatialdata.transformations import (
2531
get_transformation,
2632
set_transformation,
@@ -225,6 +231,14 @@ def plot_img_landmark_transforms(sdata_img,
225231
in a background thread (matplotlib is not thread-safe) or in batch mode
226232
to avoid blocking; the figure is saved to ``save_path`` and then closed.
227233
"""
234+
with _MATPLOTLIB_LOCK:
235+
_plot_img_landmark_transforms_inner(
236+
sdata_img, lm_img, best_transformed_img, transform_info,
237+
landmarks, landmarks_out, section, save_path, show)
238+
239+
def _plot_img_landmark_transforms_inner(sdata_img, lm_img, best_transformed_img,
240+
transform_info, landmarks, landmarks_out,
241+
section, save_path, show):
228242
fig, axes = plt.subplots(1, 3, figsize=(18, 5))
229243
axes[0].imshow(lm_img, cmap='gray')
230244
if landmarks is not None:
@@ -260,6 +274,7 @@ def plot_manual_landmark_transforms(landmarks_before,
260274
landmarks_after,
261275
landmarked_image_path,
262276
sdata,
277+
ch=0,
263278
landmarks_tf_info=None,
264279
section=None,
265280
save_path=None,
@@ -301,14 +316,16 @@ def plot_manual_landmark_transforms(landmarks_before,
301316
# means the first axis is height, i.e. (H, W) or (H, W, C).
302317
with tifffile.TiffFile(landmarked_image_path) as tif:
303318
lm_stack = tif.asarray()
319+
# Disambiguate (C, H, W) vs (H, W, C): a small leading dimension (≤8)
320+
# means channel-first; a large one means height is axis 0.
304321
if lm_stack.ndim == 2:
305322
lm_img = lm_stack
306323
elif lm_stack.ndim == 3 and lm_stack.shape[0] <= 8: # (C, H, W)
307-
lm_img = lm_stack[min(1, lm_stack.shape[0] - 1)]
324+
lm_img = lm_stack[min(ch, lm_stack.shape[0] - 1)]
308325
elif lm_stack.ndim == 3: # (H, W, C)
309-
lm_img = lm_stack[:, :, min(1, lm_stack.shape[2] - 1)]
310-
else: # unexpected shape — take first plane
311-
lm_img = lm_stack.reshape(-1, lm_stack.shape[-2], lm_stack.shape[-1])[0]
326+
lm_img = lm_stack[:, :, min(ch, lm_stack.shape[2] - 1)]
327+
else:
328+
lm_img = lm_stack.reshape(-1, lm_stack.shape[-2], lm_stack.shape[-1])[ch]
312329

313330
# ── Load sdata morphology at a downsampled level for speed ────────────
314331
# Full-res (n=0) can be 4k–8k px and is slow to materialise; a lower
@@ -326,41 +343,42 @@ def plot_manual_landmark_transforms(landmarks_before,
326343
scale_y = sdata_img.shape[0] / full_res_shape[0]
327344
scale_x = sdata_img.shape[1] / full_res_shape[1]
328345

329-
fig, axes = plt.subplots(1, 2, figsize=(14, 6))
330-
331-
# ── Left: landmarked image + original landmarks ───────────────────────
332-
axes[0].imshow(lm_img, cmap='gray')
333-
if 'xenium_x' in landmarks_before.columns:
334-
axes[0].scatter(landmarks_before['xenium_x'], landmarks_before['xenium_y'],
346+
with _MATPLOTLIB_LOCK:
347+
fig, axes = plt.subplots(1, 2, figsize=(14, 6))
348+
349+
# ── Left: landmarked image + original landmarks ───────────────────
350+
axes[0].imshow(lm_img, cmap='gray')
351+
if 'xenium_x' in landmarks_before.columns:
352+
axes[0].scatter(landmarks_before['xenium_x'], landmarks_before['xenium_y'],
353+
c='red', s=15, zorder=5)
354+
axes[0].set_title('Landmarked image + original landmarks (image pixel space)')
355+
356+
# ── Right: sdata image + transformed landmarks ────────────────────
357+
# Scale landmark coordinates from full-res pixel space to display level.
358+
axes[1].imshow(sdata_img, cmap='gray')
359+
axes[1].scatter(landmarks_after['xenium_x'] * scale_x,
360+
landmarks_after['xenium_y'] * scale_y,
335361
c='red', s=15, zorder=5)
336-
axes[0].set_title('Landmarked image + original landmarks (image pixel space)')
337-
338-
# ── Right: sdata image + transformed landmarks ────────────────────────
339-
# Scale landmark coordinates from full-res pixel space to display level.
340-
axes[1].imshow(sdata_img, cmap='gray')
341-
axes[1].scatter(landmarks_after['xenium_x'] * scale_x,
342-
landmarks_after['xenium_y'] * scale_y,
343-
c='red', s=15, zorder=5)
344-
subtitle = 'sdata morphology_focus (scale0) + transformed landmarks'
345-
if landmarks_tf_info is not None:
346-
sx = landmarks_tf_info.get('scale_factor_x')
347-
sy = landmarks_tf_info.get('scale_factor_y')
348-
bbox = landmarks_tf_info.get('bbox')
349-
if sx is not None and sy is not None:
350-
subtitle += f'\nscale (x, y): ({sx:.3f}, {sy:.3f})'
351-
if bbox is not None:
352-
subtitle += f' | bbox offset: ({bbox["x_min"]}, {bbox["y_min"]})'
353-
axes[1].set_title(subtitle)
354-
355-
plt.tight_layout()
356-
if section is not None:
357-
plt.suptitle(f'Section: {section}', y=1.01)
358-
if save_path is not None:
359-
plt.savefig(save_path, bbox_inches='tight')
360-
if show:
361-
plt.show()
362-
else:
363-
plt.close(fig)
362+
subtitle = 'sdata morphology_focus (scale0) + transformed landmarks'
363+
if landmarks_tf_info is not None:
364+
sx = landmarks_tf_info.get('scale_factor_x')
365+
sy = landmarks_tf_info.get('scale_factor_y')
366+
bbox = landmarks_tf_info.get('bbox')
367+
if sx is not None and sy is not None:
368+
subtitle += f'\nscale (x, y): ({sx:.3f}, {sy:.3f})'
369+
if bbox is not None:
370+
subtitle += f' | bbox offset: ({bbox["x_min"]}, {bbox["y_min"]})'
371+
axes[1].set_title(subtitle)
372+
373+
plt.tight_layout()
374+
if section is not None:
375+
plt.suptitle(f'Section: {section}', y=1.01)
376+
if save_path is not None:
377+
plt.savefig(save_path, bbox_inches='tight')
378+
if show:
379+
plt.show()
380+
else:
381+
plt.close(fig)
364382

365383
def find_landmarked_img_transforms(landmarked_image_path,
366384
sdata_path,
@@ -644,11 +662,19 @@ def get_section_landmarks_threads(xenium_section_ns, paths, alignment_params, n_
644662
pool.submit(_process_section, s_n, paths, alignment_params): s_n
645663
for s_n in xenium_section_ns
646664
}
665+
failed = []
647666
for fut in tqdm(as_completed(futures), total=len(futures),
648667
desc='Processing sections', unit='section'):
649-
s_n, lm = fut.result()
650-
if lm is not None:
651-
sections_landmarks[s_n] = lm
668+
s_n = futures[fut]
669+
try:
670+
s_n_result, lm = fut.result()
671+
if lm is not None:
672+
sections_landmarks[s_n_result] = lm
673+
except Exception as exc:
674+
failed.append(s_n)
675+
print(f" Section {s_n}: FAILED with {type(exc).__name__}: {exc}")
676+
if failed:
677+
print(f"\nWarning: {len(failed)} section(s) failed: {sorted(failed)}")
652678
return sections_landmarks
653679

654680
# ── Affine comparison helpers ─────────────────────────────────────────────────

0 commit comments

Comments
 (0)