Napari Headless Visualization
Execute all napari tasks via self-contained Python scripts run with Bash. Use headless mode (napari.Viewer(show=False)) and never leave a GUI visible.
Rules
- Use
napari.Viewer(show=False) — never let the GUI stay visible
- Always wrap in
try/finally with viewer.close()
- Before screenshotting, force a render cycle (show/processEvents/hide) — without this, screenshots are black
- Platform-specific QT_QPA_PLATFORM handling:
- On macOS: Do NOT set
QT_QPA_PLATFORM=offscreen (crashes due to no offscreen OpenGL)
- On Linux: Try WITHOUT setting
QT_QPA_PLATFORM=offscreen first. Only set it if you encounter GUI window issues. In many environments, offscreen mode prevents proper OpenGL rendering, resulting in black screenshots. Rely on show=False + show/hide render cycle for headless operation.
- Prerequisites assumed installed: napari, numpy, tifffile, Pillow, PyQt5
- For visual matching tasks, iterate with: screenshot → assess → adjust → re-screenshot
- After taking a screenshot, use the Read tool to view the image and relying on the model's vision capability to understand whether the code accompolished the goal.
- Use
python (not python3) to run scripts
- General visualization strategy, optimized for the color/opacity mapping first, don't change view or rotation unless really necessary, it need to be well-motivated.
Canonical Script Template
import sys, platform
import napari
import numpy as np
import tifffile
from PIL import Image
from pathlib import Path
from qtpy.QtWidgets import QApplication
import time
# Note: Do NOT set QT_QPA_PLATFORM=offscreen unless absolutely necessary
# In many Linux environments, offscreen mode prevents OpenGL rendering
viewer = napari.Viewer(show=False)
try:
# === Load data ===
# data = tifffile.imread("input.tif")
# viewer.add_image(data, name="my_image", colormap="gray")
# -- or --
# viewer.open("input.tif")
# === Configure layers ===
# (set colormaps, contrast_limits, blending, etc.)
# === Set camera / dims ===
# viewer.reset_view()
# viewer.camera.zoom = 2.0
# === Force render (required for non-black screenshots) ===
app = QApplication.instance()
viewer.window._qt_window.show()
for _ in range(5):
app.processEvents()
time.sleep(0.1)
viewer.window._qt_window.hide()
# === Capture ===
img = viewer.screenshot(canvas_only=True)
Image.fromarray(img).save("output.png")
print("Saved output.png")
finally:
viewer.close()
napari Python API Reference
Layer Types
| Method |
Key Parameters |
viewer.add_image(data) |
name, colormap, contrast_limits, gamma, opacity, blending, rendering, iso_threshold, interpolation2d, interpolation3d, visible, scale, translate, rgb |
viewer.add_labels(data) |
name, opacity, visible, scale |
viewer.add_points(coords) |
name, properties, size, face_color, edge_color, symbol, visible |
viewer.add_shapes(data) |
name, shape_type (rectangle, ellipse, polygon, line, path), edge_color, face_color, edge_width |
viewer.add_surface((verts, faces)) |
name, colormap, opacity |
viewer.add_vectors(data) |
name, edge_color, edge_width |
File I/O
# Read with tifffile (preferred for multi-channel)
data = tifffile.imread("file.tif") # shape: (C, Z, Y, X) or (Z, Y, X) etc.
# Read with viewer (auto-detects format)
viewer.open("file.tif")
# Multi-channel split
for i, cmap in enumerate(["red", "green", "blue"]):
viewer.add_image(data[i], name=f"ch{i}", colormap=cmap, blending="additive")
Colormaps
gray, viridis, plasma, inferno, magma, hot, cool, red, green, blue, cyan, magenta, yellow, turbo, twilight, hsv
Blending Modes
opaque, translucent, translucent_no_depth, additive, minimum
Camera Control
viewer.reset_view()
viewer.camera.center = (z, y, x) # center point
viewer.camera.zoom = 2.0 # zoom level
viewer.camera.angles = (elev, azim, roll) # 3D rotation (degrees)
Rotation notes:
- Rotation only affects 3D renders: set
viewer.dims.ndisplay = 3 and use a 3D-capable layer (volume or surface).
- Use rotation for exploratory viewing / figure framing; avoid it for near-planar volumes (few slices), or when you need quantitative Z localization (use slicing/orthogonal views).
- If voxels are anisotropic, set
layer.scale (physical spacing) before rotating to avoid misleading geometry.
3D Rendering
viewer.dims.ndisplay = 3
layer.rendering = "mip" # maximum intensity projection
# Options: "mip", "iso", "attenuated_mip", "minip", "average"
layer.iso_threshold = 0.5 # for iso-surface rendering
Dimensions & Time Series
viewer.dims.current_step # tuple of current position per dim
viewer.dims.set_current_step(axis, value)
viewer.dims.ndim # number of dimensions
viewer.dims.nsteps # steps per dimension
viewer.dims.axis_labels = ["t", "z", "y", "x"]
Scale Bar
viewer.scale_bar.visible = True
viewer.scale_bar.unit = "um"
Analysis via NumPy
data = layer.data
print(f"Shape: {data.shape}, dtype: {data.dtype}")
print(f"Min: {data.min()}, Max: {data.max()}, Mean: {data.mean():.2f}, Std: {data.std():.2f}")
Common Workflow Patterns
Multi-Channel Overlay
channels = tifffile.imread("multi_ch.tif") # (C, Y, X)
cmaps = ["red", "green", "blue"]
for i, cmap in enumerate(cmaps):
viewer.add_image(channels[i], name=f"ch{i}", colormap=cmap, blending="additive")
3D Volume Exploration (camera + rendering)
vol = tifffile.imread("volume.tif") # (Z, Y, X)
layer = viewer.add_image(vol, name="volume", colormap="inferno", rendering="mip") # or "iso"
# layer.scale = (z_um, y_um, x_um) # set physical spacing if known
viewer.dims.ndisplay = 3
viewer.reset_view()
Notes: for projection renderings (mip/average/minip/attenuated_mip), rotation changes the projection direction; for iso, rotation changes occlusion/shading cues.
Iterative Visual Refinement
# Step 1: Initial render with defaults
img = viewer.screenshot(canvas_only=True)
Image.fromarray(img).save("attempt_1.png")
# Step 2: Read screenshot, assess, adjust contrast/colormap/camera
# Step 3: Re-screenshot, repeat up to 5 times
Debugging & Error Handling
- Blank/black screenshot? → Ensure you included the show/processEvents/hide render cycle before screenshotting. Also call
viewer.reset_view(), check layer.visible, verify contrast_limits bracket actual data range
- Rotation not changing the view? → Ensure
viewer.dims.ndisplay = 3, the layer is actually 3D, and the volume is not effectively 2D (single/few slices).
- macOS OpenGL crash with offscreen? → Do NOT set
QT_QPA_PLATFORM=offscreen on macOS. Use show=False + show/hide render cycle instead
- Dask array? → Call
layer.data.compute() before analysis
- Large data? → Use
tifffile.imread(file, key=slice_range) to load subsets
- Print diagnostics:
for layer in viewer.layers:
print(f"{layer.name}: shape={layer.data.shape}, dtype={layer.data.dtype}, visible={layer.visible}")
if hasattr(layer, 'contrast_limits'):
print(f" contrast_limits={layer.contrast_limits}")
Task Execution
When given $ARGUMENTS:
- Parse the task from the arguments
- Write a self-contained Python script following the template above
- Execute it with Bash
- Read the output image to verify correctness
- If the result needs adjustment, try iterate
- Report the result to the user
1---2name: napari-viz3description: >4 Headless bioimage and microscopy visualization with napari. Use this skill when Claude needs to:5 (1) Visualize 2D/3D/multi-channel microscopy or bioimage data (TIFF, label maps, points/shapes/surfaces),6 (2) Render 3D volumes (MIP, iso, attenuated MIP) and capture viewport-only screenshots,7 (3) Configure colormaps, contrast limits, blending, scale bars, and camera in napari,8 (4) Run napari headlessly (no visible GUI) from self-contained Python scripts.9---101112# Napari Headless Visualization1314Execute all napari tasks via self-contained Python scripts run with Bash. Use headless mode (`napari.Viewer(show=False)`) and never leave a GUI visible.1516## Rules17181. Use `napari.Viewer(show=False)` — never let the GUI stay visible192. Always wrap in `try/finally` with `viewer.close()`203. **Before screenshotting**, force a render cycle (show/processEvents/hide) — without this, screenshots are black214. **Platform-specific QT_QPA_PLATFORM handling:**22 - On **macOS**: Do NOT set `QT_QPA_PLATFORM=offscreen` (crashes due to no offscreen OpenGL)23 - On **Linux**: Try WITHOUT setting `QT_QPA_PLATFORM=offscreen` first. Only set it if you encounter GUI window issues. In many environments, offscreen mode prevents proper OpenGL rendering, resulting in black screenshots. Rely on `show=False` + show/hide render cycle for headless operation.245. Prerequisites assumed installed: napari, numpy, tifffile, Pillow, PyQt5256. For visual matching tasks, iterate with: screenshot → assess → adjust → re-screenshot267. After taking a screenshot, use the Read tool to view the image and relying on the model's vision capability to understand whether the code accompolished the goal.278. Use `python` (not `python3`) to run scripts289. General visualization strategy, optimized for the color/opacity mapping first, don't change view or rotation unless really necessary, it need to be well-motivated.2930## Canonical Script Template3132```python33import sys, platform34import napari35import numpy as np36import tifffile37from PIL import Image38from pathlib import Path39from qtpy.QtWidgets import QApplication40import time4142# Note: Do NOT set QT_QPA_PLATFORM=offscreen unless absolutely necessary43# In many Linux environments, offscreen mode prevents OpenGL rendering4445viewer = napari.Viewer(show=False)46try:47 # === Load data ===48 # data = tifffile.imread("input.tif")49 # viewer.add_image(data, name="my_image", colormap="gray")50 # -- or --51 # viewer.open("input.tif")5253 # === Configure layers ===54 # (set colormaps, contrast_limits, blending, etc.)5556 # === Set camera / dims ===57 # viewer.reset_view()58 # viewer.camera.zoom = 2.05960 # === Force render (required for non-black screenshots) ===61 app = QApplication.instance()62 viewer.window._qt_window.show()63 for _ in range(5):64 app.processEvents()65 time.sleep(0.1)66 viewer.window._qt_window.hide()6768 # === Capture ===69 img = viewer.screenshot(canvas_only=True)70 Image.fromarray(img).save("output.png")71 print("Saved output.png")72finally:73 viewer.close()74```7576## napari Python API Reference7778### Layer Types7980| Method | Key Parameters |81|--------|---------------|82| `viewer.add_image(data)` | `name, colormap, contrast_limits, gamma, opacity, blending, rendering, iso_threshold, interpolation2d, interpolation3d, visible, scale, translate, rgb` |83| `viewer.add_labels(data)` | `name, opacity, visible, scale` |84| `viewer.add_points(coords)` | `name, properties, size, face_color, edge_color, symbol, visible` |85| `viewer.add_shapes(data)` | `name, shape_type` (`rectangle`, `ellipse`, `polygon`, `line`, `path`), `edge_color, face_color, edge_width` |86| `viewer.add_surface((verts, faces))` | `name, colormap, opacity` |87| `viewer.add_vectors(data)` | `name, edge_color, edge_width` |8889### File I/O9091```python92# Read with tifffile (preferred for multi-channel)93data = tifffile.imread("file.tif") # shape: (C, Z, Y, X) or (Z, Y, X) etc.9495# Read with viewer (auto-detects format)96viewer.open("file.tif")9798# Multi-channel split99for i, cmap in enumerate(["red", "green", "blue"]):100 viewer.add_image(data[i], name=f"ch{i}", colormap=cmap, blending="additive")101```102103### Colormaps104105`gray`, `viridis`, `plasma`, `inferno`, `magma`, `hot`, `cool`, `red`, `green`, `blue`, `cyan`, `magenta`, `yellow`, `turbo`, `twilight`, `hsv`106107### Blending Modes108109`opaque`, `translucent`, `translucent_no_depth`, `additive`, `minimum`110111### Camera Control112113```python114viewer.reset_view()115viewer.camera.center = (z, y, x) # center point116viewer.camera.zoom = 2.0 # zoom level117viewer.camera.angles = (elev, azim, roll) # 3D rotation (degrees)118```119120Rotation notes:121- Rotation only affects 3D renders: set `viewer.dims.ndisplay = 3` and use a 3D-capable layer (volume or surface).122- Use rotation for exploratory viewing / figure framing; avoid it for near-planar volumes (few slices), or when you need quantitative Z localization (use slicing/orthogonal views).123- If voxels are anisotropic, set `layer.scale` (physical spacing) before rotating to avoid misleading geometry.124125### 3D Rendering126127```python128viewer.dims.ndisplay = 3129layer.rendering = "mip" # maximum intensity projection130# Options: "mip", "iso", "attenuated_mip", "minip", "average"131layer.iso_threshold = 0.5 # for iso-surface rendering132```133134### Dimensions & Time Series135136```python137viewer.dims.current_step # tuple of current position per dim138viewer.dims.set_current_step(axis, value)139viewer.dims.ndim # number of dimensions140viewer.dims.nsteps # steps per dimension141viewer.dims.axis_labels = ["t", "z", "y", "x"]142```143144### Scale Bar145146```python147viewer.scale_bar.visible = True148viewer.scale_bar.unit = "um"149```150151### Analysis via NumPy152153```python154data = layer.data155print(f"Shape: {data.shape}, dtype: {data.dtype}")156print(f"Min: {data.min()}, Max: {data.max()}, Mean: {data.mean():.2f}, Std: {data.std():.2f}")157```158159## Common Workflow Patterns160161### Multi-Channel Overlay162```python163channels = tifffile.imread("multi_ch.tif") # (C, Y, X)164cmaps = ["red", "green", "blue"]165for i, cmap in enumerate(cmaps):166 viewer.add_image(channels[i], name=f"ch{i}", colormap=cmap, blending="additive")167```168169### 3D Volume Exploration (camera + rendering)170```python171vol = tifffile.imread("volume.tif") # (Z, Y, X)172layer = viewer.add_image(vol, name="volume", colormap="inferno", rendering="mip") # or "iso"173# layer.scale = (z_um, y_um, x_um) # set physical spacing if known174viewer.dims.ndisplay = 3175viewer.reset_view()176```177178Notes: for projection renderings (mip/average/minip/attenuated_mip), rotation changes the projection direction; for iso, rotation changes occlusion/shading cues.179180### Iterative Visual Refinement181```python182# Step 1: Initial render with defaults183img = viewer.screenshot(canvas_only=True)184Image.fromarray(img).save("attempt_1.png")185# Step 2: Read screenshot, assess, adjust contrast/colormap/camera186# Step 3: Re-screenshot, repeat up to 5 times187```188189## Debugging & Error Handling190191- **Blank/black screenshot?** → Ensure you included the show/processEvents/hide render cycle before screenshotting. Also call `viewer.reset_view()`, check `layer.visible`, verify `contrast_limits` bracket actual data range192- **Rotation not changing the view?** → Ensure `viewer.dims.ndisplay = 3`, the layer is actually 3D, and the volume is not effectively 2D (single/few slices).193- **macOS OpenGL crash with offscreen?** → Do NOT set `QT_QPA_PLATFORM=offscreen` on macOS. Use `show=False` + show/hide render cycle instead194- **Dask array?** → Call `layer.data.compute()` before analysis195- **Large data?** → Use `tifffile.imread(file, key=slice_range)` to load subsets196- **Print diagnostics:**197 ```python198 for layer in viewer.layers:199 print(f"{layer.name}: shape={layer.data.shape}, dtype={layer.data.dtype}, visible={layer.visible}")200 if hasattr(layer, 'contrast_limits'):201 print(f" contrast_limits={layer.contrast_limits}")202 ```203204## Task Execution205206When given $ARGUMENTS:2071. Parse the task from the arguments2082. Write a self-contained Python script following the template above2093. Execute it with Bash2104. Read the output image to verify correctness2115. If the result needs adjustment, try iterate2126. Report the result to the user