| name | napari-viz |
| description | Headless bioimage and microscopy visualization with napari. Use this skill when Claude needs to: (1) Visualize 2D/3D/multi-channel microscopy or bioimage data (TIFF, label maps, points/shapes/surfaces), (2) Render 3D volumes (MIP, iso, attenuated MIP) and capture viewport-only screenshots, (3) Configure colormaps, contrast limits, blending, scale bars, and camera in napari, (4) Run napari headlessly (no visible GUI) from self-contained Python scripts.
|
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
viewer = napari.Viewer(show=False)
try:
app = QApplication.instance()
viewer.window._qt_window.show()
for _ in range(5):
app.processEvents()
time.sleep(0.1)
viewer.window._qt_window.hide()
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
data = tifffile.imread("file.tif")
viewer.open("file.tif")
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)
viewer.camera.zoom = 2.0
viewer.camera.angles = (elev, azim, roll)
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"
layer.iso_threshold = 0.5
Dimensions & Time Series
viewer.dims.current_step
viewer.dims.set_current_step(axis, value)
viewer.dims.ndim
viewer.dims.nsteps
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")
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")
layer = viewer.add_image(vol, name="volume", colormap="inferno", rendering="mip")
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
img = viewer.screenshot(canvas_only=True)
Image.fromarray(img).save("attempt_1.png")
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