Skip to content

Commit aa5a3a8

Browse files
authored
Merge pull request #111 from sebi06/improve_stability
Fix threading issues with aicspylibczi on Linux + Napari
2 parents f19c098 + cd5b704 commit aa5a3a8

19 files changed

+2476
-131
lines changed

README.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,61 @@
99

1010
This repository provides a collection of tools to simplify reading CZI (Carl Zeiss Image) pixel and metadata in Python. It is available as a [Python Package on PyPi](https://pypi.org/project/czitools/)
1111

12+
## ⚠️ Important: Using czitools with Napari on Linux
13+
14+
## ⚠️ Important: Using czitools with Napari on Linux
15+
16+
If you use **Napari** on **Linux** and need `get_planetable()` or `read_tiles()`:
17+
18+
📖 **[Linux + Napari + Planetable Guide](docs/LINUX_NAPARI_PLANETABLE.md)****READ THIS**
19+
20+
**The Solution: Sequential Execution Pattern**
21+
22+
Extract planetable **BEFORE** starting Napari to avoid threading conflicts:
23+
24+
```python
25+
# Step 1: Get planetable FIRST (before Napari)
26+
from czitools.utils.planetable import get_planetable
27+
df, _ = get_planetable("file.czi")
28+
29+
# Step 2: Load image (thread-safe)
30+
from czitools.read_tools import read_tools
31+
array, _ = read_tools.read_6darray("file.czi", use_dask=True)
32+
33+
# Step 3: NOW start Napari (safe - no conflicts!)
34+
import napari
35+
viewer = napari.Viewer()
36+
viewer.add_image(array)
37+
napari.run()
38+
```
39+
40+
**Full planetable functionality on Linux**
41+
**No crashes**
42+
**No performance loss**
43+
44+
**Alternative: Safe Mode** (simpler, but no planetable/tiles):
45+
46+
```python
47+
import os
48+
os.environ["CZITOOLS_DISABLE_AICSPYLIBCZI"] = "1"
49+
50+
from czitools.read_tools import read_tools
51+
# Use read_6darray() instead of read_tiles()
52+
array, mdata = read_tools.read_6darray("file.czi", use_dask=True)
53+
```
54+
55+
**Why?** `aicspylibczi` has threading conflicts with PyQt on Linux.
56+
**Solution:** Extract planetable before PyQt event loop starts (sequential execution).
57+
58+
📄 **Documentation:**
59+
- [Linux + Napari + Planetable Guide](docs/LINUX_NAPARI_PLANETABLE.md) - Complete examples
60+
- [Threading Considerations](docs/threading_considerations.md) - Technical details
61+
- [Quick Fix Guide](docs/NAPARI_FIX.md) - Emergency fixes
62+
63+
---
64+
65+
See [demo/scripts/napari_with_process_isolation.py](demo/scripts/napari_with_process_isolation.py) for complete examples and [docs/threading_considerations.md](docs/threading_considerations.md) for detailed information.
66+
1267
## Installation
1368

1469
To install czitools (core functionality) use:
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
"""
2+
Example: Platform-aware CZI loading for napari-czitools plugin.
3+
4+
This module shows how to handle Linux threading issues when czitools
5+
is used as part of a Napari plugin.
6+
"""
7+
8+
import platform
9+
from pathlib import Path
10+
from typing import Optional, Tuple
11+
import pandas as pd
12+
import numpy as np
13+
from czitools.read_tools import read_tools
14+
from czitools.utils.planetable import get_planetable
15+
from czitools.metadata_tools.czi_metadata import CziMetadata
16+
17+
18+
class NapariCziLoader:
19+
"""
20+
Platform-aware CZI loader for Napari plugins.
21+
22+
Handles threading issues on Linux automatically.
23+
"""
24+
25+
def __init__(self, enable_planetable_on_linux: bool = False):
26+
"""
27+
Initialize loader.
28+
29+
Args:
30+
enable_planetable_on_linux: If True, attempt planetable extraction
31+
on Linux (may crash). If False, skip planetable on Linux.
32+
"""
33+
self.enable_planetable_on_linux = enable_planetable_on_linux
34+
self.is_linux = platform.system() == "Linux"
35+
36+
def load_czi(
37+
self, filepath: Path, extract_planetable: bool = True
38+
) -> Tuple[np.ndarray, CziMetadata, Optional[pd.DataFrame]]:
39+
"""
40+
Load CZI file with platform-aware handling.
41+
42+
Args:
43+
filepath: Path to CZI file
44+
extract_planetable: Whether to attempt planetable extraction
45+
46+
Returns:
47+
Tuple of (array, metadata, planetable_df)
48+
planetable_df may be None on Linux or if extraction fails
49+
"""
50+
print(f"Loading CZI: {filepath}")
51+
52+
# Always load image data (thread-safe)
53+
array, metadata = read_tools.read_6darray(filepath, use_dask=True, use_xarray=True, chunk_zyx=True)
54+
print(f"✅ Image loaded: {array.shape}")
55+
56+
# Handle planetable based on platform
57+
planetable_df = None
58+
59+
if extract_planetable:
60+
if self.is_linux and not self.enable_planetable_on_linux:
61+
# Skip on Linux by default
62+
print("ℹ️ Planetable extraction disabled on Linux (threading safety)")
63+
print(" To enable: set enable_planetable_on_linux=True")
64+
print(" Warning: May cause crashes with Napari on Linux")
65+
else:
66+
# Try extraction
67+
planetable_df = self._extract_planetable_safe(filepath)
68+
69+
return array, metadata, planetable_df
70+
71+
def _extract_planetable_safe(self, filepath: Path) -> Optional[pd.DataFrame]:
72+
"""
73+
Safely attempt planetable extraction with error handling.
74+
75+
Args:
76+
filepath: Path to CZI file
77+
78+
Returns:
79+
DataFrame or None if extraction fails
80+
"""
81+
if self.is_linux:
82+
print("⚠️ WARNING: Attempting planetable extraction on Linux")
83+
print(" This may cause Napari to crash due to threading conflicts")
84+
print(" If crashes occur, set CZITOOLS_DISABLE_AICSPYLIBCZI=1")
85+
86+
try:
87+
df, _ = get_planetable(filepath, norm_time=True)
88+
print(f"✅ Planetable extracted: {len(df)} rows")
89+
return df
90+
91+
except RuntimeError as e:
92+
if "CZITOOLS_DISABLE_AICSPYLIBCZI" in str(e):
93+
print("ℹ️ Planetable disabled (safe mode active)")
94+
else:
95+
print(f"❌ Planetable extraction failed: {e}")
96+
return None
97+
98+
except Exception as e:
99+
print(f"❌ Planetable extraction error: {e}")
100+
if self.is_linux:
101+
print(" This may be a threading conflict on Linux")
102+
print(" Restart Napari with: export CZITOOLS_DISABLE_AICSPYLIBCZI=1")
103+
return None
104+
105+
106+
# Example usage in a napari plugin widget
107+
def example_napari_plugin_widget():
108+
"""
109+
Example of how to use NapariCziLoader in a napari plugin.
110+
"""
111+
from magicgui import magic_factory
112+
from napari.types import LayerDataTuple
113+
114+
@magic_factory(
115+
call_button="Load CZI",
116+
filepath={"mode": "r", "filter": "*.czi"},
117+
enable_planetable={"label": "Extract Planetable (may crash on Linux)"},
118+
)
119+
def load_czi_widget(filepath: Path, enable_planetable: bool = False) -> LayerDataTuple:
120+
"""
121+
Napari widget to load CZI files.
122+
123+
Args:
124+
filepath: CZI file to load
125+
enable_planetable: Extract planetable (risky on Linux)
126+
127+
Returns:
128+
Layer data tuple for Napari
129+
"""
130+
# Create loader with platform awareness
131+
loader = NapariCziLoader(enable_planetable_on_linux=enable_planetable)
132+
133+
# Load CZI
134+
array, metadata, planetable_df = loader.load_czi(filepath, extract_planetable=enable_planetable)
135+
136+
# Prepare metadata for Napari
137+
layer_metadata = {
138+
"czi_metadata": metadata.info,
139+
"planetable": planetable_df.to_dict() if planetable_df is not None else None,
140+
}
141+
142+
# Show planetable summary if available
143+
if planetable_df is not None:
144+
print(f"\n📊 Planetable Summary:")
145+
print(f" Total planes: {len(planetable_df)}")
146+
if "Time[s]" in planetable_df.columns:
147+
print(f" Time range: {planetable_df['Time[s]'].min():.2f} - {planetable_df['Time[s]'].max():.2f} s")
148+
149+
# Return layer data tuple
150+
return (array, {"name": filepath.name, "metadata": layer_metadata}, "image")
151+
152+
return load_czi_widget
153+
154+
155+
# Example: Manual usage
156+
if __name__ == "__main__":
157+
from pathlib import Path
158+
159+
# Create loader
160+
loader = NapariCziLoader(enable_planetable_on_linux=False) # Safe default for Linux
161+
162+
# Load CZI
163+
filepath = Path("data/CellDivision_T3_Z5_CH2_X240_Y170.czi")
164+
array, metadata, planetable_df = loader.load_czi(filepath)
165+
166+
print(f"\n✅ Loaded successfully:")
167+
print(f" Shape: {array.shape}")
168+
print(f" Planetable: {'Available' if planetable_df is not None else 'Not extracted'}")
169+
170+
# If running in Napari, could display now:
171+
# import napari
172+
# viewer = napari.current_viewer()
173+
# viewer.add_image(array, name=filepath.name)
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
"""
2+
Safe CZI Loading for Napari on Linux
3+
4+
This script demonstrates how to safely load CZI files with Napari on Linux
5+
to avoid crashes related to aicspylibczi threading issues.
6+
7+
Author: sebi06
8+
"""
9+
10+
import os
11+
12+
# CRITICAL: Set this BEFORE importing czitools or napari
13+
# This prevents aicspylibczi from being used, avoiding threading conflicts
14+
os.environ["CZITOOLS_DISABLE_AICSPYLIBCZI"] = "1"
15+
16+
from pathlib import Path
17+
from czitools.read_tools import read_tools
18+
from czitools.utils import misc
19+
import napari
20+
21+
# Define base directory for test data
22+
basedir = Path(__file__).resolve().parents[2] / "data"
23+
os.chdir(basedir)
24+
25+
# Select a CZI file
26+
filepath = misc.openfile(
27+
directory=str(basedir),
28+
title="Open CZI Image File",
29+
ftypename="CZI Files",
30+
extension="*.czi",
31+
)
32+
33+
if filepath is None:
34+
print("No file selected. Exiting.")
35+
exit()
36+
37+
print(f"Loading: {filepath}")
38+
print("\n⚠️ Using thread-safe mode (aicspylibczi disabled)")
39+
print("This is the recommended approach for Napari on Linux\n")
40+
41+
# Read CZI with thread-safe settings
42+
# This uses ONLY pylibCZIrw which is confirmed thread-safe
43+
array6d, mdata = read_tools.read_6darray(
44+
filepath,
45+
use_dask=True, # Lazy loading - efficient for large files
46+
use_xarray=True, # Labeled dimensions - easy to work with
47+
chunk_zyx=True, # Optimize chunking for performance
48+
)
49+
50+
print(f"Image dimensions: {array6d.dims}")
51+
print(f"Image shape: {array6d.shape}")
52+
print(f"Number of channels: {mdata.image.SizeC}")
53+
print(f"Pixel type: {mdata.pixeltypes}")
54+
55+
# Create Napari viewer
56+
viewer = napari.Viewer()
57+
58+
# Add each channel to Napari
59+
for ch in range(mdata.image.SizeC):
60+
# Extract channel
61+
channel_data = array6d.sel(C=ch)
62+
63+
# Get channel name if available
64+
channel_name = (
65+
mdata.channelinfo.names[ch]
66+
if mdata.channelinfo.names and ch < len(mdata.channelinfo.names)
67+
else f"Channel {ch}"
68+
)
69+
70+
# Get scaling information
71+
scale = [1.0] # For Scene dimension if present
72+
if mdata.image.SizeT and mdata.image.SizeT > 1:
73+
scale.append(1.0) # Time dimension
74+
scale.extend(
75+
[
76+
mdata.scale.Z if mdata.scale.Z else 1.0,
77+
mdata.scale.Y if mdata.scale.Y else 1.0,
78+
mdata.scale.X if mdata.scale.X else 1.0,
79+
]
80+
)
81+
82+
# Add to viewer
83+
viewer.add_image(
84+
channel_data,
85+
name=channel_name,
86+
scale=scale,
87+
colormap="gray" if not mdata.isRGB.get(ch, False) else "viridis",
88+
blending="additive",
89+
)
90+
91+
print("\n✅ CZI loaded successfully in thread-safe mode!")
92+
print("No aicspylibczi was used - only pylibCZIrw (thread-safe)\n")
93+
94+
# Run Napari
95+
napari.run()
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Safe pattern for using get_planetable() with Napari on Linux.
4+
5+
The key: Extract planetable BEFORE starting Napari's event loop.
6+
This avoids threading conflicts between aicspylibczi and PyQt.
7+
"""
8+
9+
from pathlib import Path
10+
from czitools.utils.planetable import get_planetable
11+
from czitools.read_tools import read_tools
12+
13+
# Step 1: Extract planetable FIRST (before Napari)
14+
print("Extracting planetable...")
15+
czi_file = Path("data/CellDivision_T3_Z5_CH2_X240_Y170.czi")
16+
17+
# Get planetable - aicspylibczi runs here, before Napari
18+
df_planetable, csv_path = get_planetable(czi_file, norm_time=True, save_table=False)
19+
20+
print(f"✅ Planetable extracted: {len(df_planetable)} rows")
21+
print(f" Columns: {list(df_planetable.columns)}")
22+
23+
# Step 2: Load image data (thread-safe, uses only pylibCZIrw)
24+
print("\nLoading image data...")
25+
array, metadata = read_tools.read_6darray(czi_file, use_dask=True, use_xarray=True, chunk_zyx=True)
26+
27+
print(f"✅ Image loaded: {array.shape}")
28+
29+
# Step 3: NOW start Napari (planetable already extracted)
30+
print("\nStarting Napari...")
31+
import napari
32+
33+
viewer = napari.Viewer()
34+
35+
# Add image
36+
viewer.add_image(array, name="CZI Image", metadata={"czi_metadata": metadata.info})
37+
38+
# Use planetable data for visualization
39+
# Example: Color timepoints differently based on planetable timestamps
40+
if "Time[s]" in df_planetable.columns:
41+
time_info = df_planetable.groupby("T")["Time[s]"].first()
42+
print(f"\n📊 Time points from planetable:")
43+
for t, time_s in time_info.items():
44+
print(f" T={t}: {time_s:.2f} seconds")
45+
46+
# Example: Show planetable as table in console
47+
print(f"\n📊 Planetable preview:")
48+
print(df_planetable.head(10))
49+
50+
print("\n✅ Safe to use Napari now - planetable extracted before event loop started")
51+
print(" Close Napari viewer to exit.")
52+
53+
napari.run()

0 commit comments

Comments
 (0)