Install any skill in seconds. Free to start, no credit card required.
Get Started Free →DL cell/nucleus segmentation for fluorescence and brightfield microscopy. Pre-trained models (cyto3, nuclei, tissuenet) and a generalist flow-based algorithm segment cells without retraining. Outputs label masks for morphology and tracking. Use scikit-image watershed for rule-based; Cellpose when DL generalization across staining is needed.
.claude/skills/jaechang-hits-cellpose-cell-segmentation/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 36% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 178% | 0% |
| case-11 | ✗→✓ | ▲ Improved | 168% | 0% |
| case-18 | ✗→✓ | ▲ Improved | 163% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 184% | 0% |
Cellpose uses a flow-based neural network to segment individual cells or nuclei in fluorescence microscopy images without manual parameter tuning. Pre-trained models (cyto3, nuclei, tissuenet) generalize across cell types, magnifications, and staining conditions — eliminating the need for manual threshold selection or watershed parameter optimization. Cellpose outputs integer label masks (each cell = unique integer) compatible with scikit-image regionprops for morphology measurement and with TrackPy for tracking. A built-in diameter estimator removes the need to specify cell size, though providing an approximate diameter improves accuracy.
do_3D=Truecellpose, numpy, matplotlibpip install cellpose[gui] for GUI)bash# Install Cellpose pip install cellpose # Install with GUI support pip install cellpose[gui] # Install with GPU (PyTorch CUDA) pip install cellpose torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118 # Verify python -c "from cellpose import models; print('Cellpose ready')"
pythonfrom cellpose import models import numpy as np from skimage import io # Load image (grayscale or 2D array) img = io.imread("cells.tif") # shape: (H, W) or (H, W, C) # Initialize model and segment model = models.Cellpose(model_type="cyto3", gpu=False) masks, flows, styles, diams = model.eval(img, diameter=0, channels=[0, 0]) print(f"Cells segmented: {masks.max()}") # number of cells print(f"Estimated diameter: {diams:.1f} px") print(f"Mask shape: {masks.shape}")
Load microscopy images and inspect channel layout before segmentation.
pythonimport numpy as np from skimage import io import matplotlib.pyplot as plt # Load single-channel fluorescence image img_gray = io.imread("nucleus_dapi.tif") # shape: (H, W) img_rgb = io.imread("cells_multichannel.tif") # shape: (H, W, C) print(f"Grayscale shape: {img_gray.shape}, dtype: {img_gray.dtype}") print(f"Multichannel shape: {img_rgb.shape}") # Preview fig, axes = plt.subplots(1, 2, figsize=(12, 5)) axes[0].imshow(img_gray, cmap="gray") axes[0].set_title("DAPI (nuclei)") axes[1].imshow(img_rgb[..., 0], cmap="green") axes[1].set_title("GFP channel") plt.tight_layout() plt.savefig("image_preview.png", dpi=100) print("Saved: image_preview.png")
Run Cellpose with the appropriate pre-trained model.
pythonfrom cellpose import models import numpy as np from skimage import io # Available models: 'cyto3' (cells), 'nuclei', 'tissuenet', 'cyto2', 'CP' model = models.Cellpose(model_type="cyto3", gpu=False) img = io.imread("cells.tif") # channels=[cytoplasm_channel, nucleus_channel] # Use [0, 0] for grayscale; [1, 3] for green cytoplasm + blue nucleus (1-indexed) masks, flows, styles, diams = model.eval( img, diameter=0, # 0 = auto-estimate; or provide px estimate channels=[0, 0], # grayscale flow_threshold=0.4, # lower = fewer false positives; range 0.1-1.0 cellprob_threshold=0.0, # lower = more cells detected; range -6 to 6 ) print(f"Cells found: {masks.max()}") print(f"Estimated cell diameter: {diams:.1f} pixels") np.save("masks.npy", masks)
Use the nuclei model for DAPI-stained nuclei.
pythonfrom cellpose import models from skimage import io import numpy as np model = models.Cellpose(model_type="nuclei", gpu=False) dapi = io.imread("dapi.tif") # Nucleus-only segmentation: channels=[0, 0] (single channel) masks, flows, styles, diams = model.eval( dapi, diameter=30, # approximate nucleus diameter in pixels channels=[0, 0], flow_threshold=0.4, cellprob_threshold=0.0, ) print(f"Nuclei segmented: {masks.max()}") # Save label mask as TIFF for ImageJ/FIJI compatibility from skimage import io as skio skio.imsave("nuclei_masks.tif", masks.astype(np.uint16)) print("Saved: nuclei_masks.tif")
Overlay masks on original images for quality control.
pythonfrom cellpose import plot as cpplot import matplotlib.pyplot as plt import numpy as np from skimage import io img = io.imread("cells.tif") masks = np.load("masks.npy") flows_data = None # load if you saved them: flows = np.load("flows.npy", allow_pickle=True) # Cellpose built-in visualization fig, axes = plt.subplots(1, 3, figsize=(15, 5)) # Original image axes[0].imshow(img, cmap="gray") axes[0].set_title(f"Original image") # Label mask (each cell = unique color) axes[1].imshow(masks, cmap="tab20") axes[1].set_title(f"Segmentation masks ({masks.max()} cells)") # Overlay: outline on original from skimage.segmentation import find_boundaries boundaries = find_boundaries(masks, mode="inner") overlay = np.stack([img / img.max()] * 3, axis=-1) overlay[boundaries] = [1, 0, 0] # red outlines axes[2].imshow(overlay) axes[2].set_title("Outlines overlay") plt.tight_layout() plt.savefig("segmentation_result.png", dpi=150) print("Saved: segmentation_result.png")
Extract morphology and intensity measurements using scikit-image regionprops.
pythonimport numpy as np import pandas as pd from skimage.measure import regionprops_table from skimage import io masks = np.load("masks.npy") img = io.imread("cells.tif") # Measure morphology and intensity per cell props = regionprops_table( masks, intensity_image=img, properties=["label", "area", "centroid", "eccentricity", "mean_intensity", "max_intensity", "perimeter", "equivalent_diameter_area"] ) df = pd.DataFrame(props) df.columns = ["cell_id", "area_px", "centroid_y", "centroid_x", "eccentricity", "mean_intensity", "max_intensity", "perimeter", "diameter_px"] print(f"Cells measured: {len(df)}") print(f"Median area: {df['area_px'].median():.0f} px²") print(f"Median diameter: {df['diameter_px'].median():.1f} px") print(df.head()) df.to_csv("cell_measurements.csv", index=False)
Process a directory of images and aggregate results.
pythonfrom cellpose import models from skimage import io from skimage.measure import regionprops_table import pandas as pd import numpy as np from pathlib import Path model = models.Cellpose(model_type="cyto3", gpu=False) image_dir = Path("images/") output_dir = Path("results/") output_dir.mkdir(exist_ok=True) all_stats = [] for img_path in sorted(image_dir.glob("*.tif")): img = io.imread(img_path) masks, _, _, diams = model.eval(img, diameter=0, channels=[0, 0]) # Save mask np.save(output_dir / f"{img_path.stem}_masks.npy", masks) # Measure if masks.max() > 0: props = regionprops_table(masks, intensity_image=img, properties=["label", "area", "mean_intensity"]) df = pd.DataFrame(props) df["image"] = img_path.name df["est_diameter"] = diams all_stats.append(df) print(f"{img_path.name}: {masks.max()} cells, diameter={diams:.0f}px") summary = pd.concat(all_stats, ignore_index=True) summary.to_csv(output_dir / "all_cells.csv", index=False) print(f"\nTotal cells: {len(summary)} across {summary['image'].nunique()} images")
| Parameter | Default | Range/Options | Effect | |-----------|---------|---------------|--------| | model_type | "cyto3" | "cyto3", "cyto2", "nuclei", "tissuenet", "CP", custom path | Pre-trained model; cyto3 is most general; nuclei for DAPI-only | | diameter | 30 | 0–500 px | Approximate cell diameter in pixels; 0 = auto-estimate from image | | channels | [0, 0] | [cyto, nucleus] (0=gray, 1=R, 2=G, 3=B) | Channel indices for cytoplasm and nuclear stain | | flow_threshold | 0.4 | 0.1–1.0 | Cell probability threshold from flow field; lower = stricter | | cellprob_threshold | 0.0 | −6 to 6 | Cell probability cutoff; increase to find more cells | | gpu | False | True, False | Enable GPU inference (requires CUDA PyTorch) | | do_3D | False | True, False | Enable 3D volumetric segmentation of z-stacks | | min_size | 15 | integer px² | Minimum object size in pixels²; smaller objects discarded | | batch_size | 8 | integer | Number of image tiles processed per GPU batch | | normalize | True | True, False | Normalize image intensity before segmentation |
pythonfrom cellpose import models from skimage import io import numpy as np model = models.Cellpose(model_type="cyto3", gpu=False) # Multichannel image: channel 1 = GFP (cytoplasm), channel 3 = DAPI (nucleus) img_multi = io.imread("cells_gfp_dapi.tif") # shape: (H, W, 3) # channels=[cytoplasm_channel, nucleus_channel] (1-indexed for multichannel) masks, flows, styles, diams = model.eval( img_multi, diameter=0, channels=[2, 3], # GFP=channel2, DAPI=channel3 (1-indexed) flow_threshold=0.4, ) print(f"Cells segmented: {masks.max()}, diameter: {diams:.0f}px") np.save("masks_multichannel.npy", masks)
bash# CLI batch segmentation of all TIFFs in a directory cellpose \ --image_path images/ \ --pretrained_model cyto3 \ --diameter 0 \ --chan 0 \ --save_tif \ --no_npy # With GPU cellpose \ --image_path images/ \ --pretrained_model nuclei \ --diameter 30 \ --chan 0 \ --use_gpu \ --save_tif # Results saved as: images/*_cp_masks.tif echo "Done. Masks saved in images/ directory."
pythonfrom cellpose import models, train import numpy as np from skimage import io # Prepare training data: list of images and corresponding masks train_images = [io.imread(f"train/img_{i}.tif") for i in range(10)] train_masks = [np.load(f"train/mask_{i}.npy") for i in range(10)] # Fine-tune starting from cyto3 model = models.CellposeModel(model_type="cyto3") # Train: saves model to models/ directory model_path = train.train_seg( model.net, train_data=train_images, train_labels=train_masks, channels=[0, 0], save_path="models/", n_epochs=100, learning_rate=0.2, weight_decay=1e-5, ) print(f"Fine-tuned model saved: {model_path}")
| Output | Format | Description | |--------|--------|-------------| | masks array | numpy int32 | Label mask: 0=background, 1..N=unique cell IDs | | flows list | numpy arrays | Flow field components: XY flows, cell prob, gradient] | | styles array | numpy float | Style vector embedding (used for model similarity) | | diams float | scalar | Estimated average cell diameter in pixels | | *_masks.npy | NumPy | Saved mask array (from np.save) | | *_cp_masks.tif | TIFF uint16 | Mask TIFF (from CLI --save_tif); compatible with FIJI/ImageJ |
| Problem | Cause | Solution | |---------|-------|----------| | All cells merged into one mask | Diameter too large or cells too close | Reduce diameter; increase flow_threshold to 0.6–0.8 | | Very few cells detected | Diameter too small or cellprob_threshold too high | Increase cellprob_threshold to −2; use diameter=0 for auto | | Many false positives (background labeled) | Low flow_threshold | Increase flow_threshold to 0.6–0.9; increase min_size | | GPU out of memory | Image too large for GPU batch | Process in tiles; reduce batch_size; crop image | | Poor generalization on new cell type | Model not trained on similar cells | Try all pre-trained models; fine-tune with 10-20 annotated images | | 3D segmentation very slow | Large z-stack on CPU | Enable GPU; reduce z-stack depth; use anisotropy parameter | | Mask values overflow uint8 | More than 255 cells in image | Save with dtype=np.uint16 or np.int32 | | Import error: No module named 'cellpose' | Package not installed | pip install cellpose or conda install -c conda-forge cellpose |
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 23,528 | 13,605 | -42% | 1 | 1 | 0% | 4,438 | 6,039 | +36% | 0 | 0 | — |
case-02 | fail→fail | 21,449 | 14,658 | -32% | 1 | 1 | 0% | 4,113 | 7,045 | +71% | 0 | 0 | — |
case-03 | pass→pass | 11,594 | 9,398 | -19% | 1 | 1 | 0% | 2,063 | 5,862 | +184% | 0 | 0 | — |
case-04 | pass→pass | 8,419 | 7,766 | -8% | 1 | 1 | 0% | 1,496 | 5,709 | +282% | 0 | 0 | — |
case-05 | fail→fail | 13,296 | 9,697 | -27% | 1 | 1 | 0% | 2,063 | 5,841 | +183% | 0 | 0 | — |
case-06 | fail→pass | 37,615 | 8,374 | -78% | 1 | 1 | 0% | 2,106 | 5,857 | +178% | 0 | 0 | — |
case-07 | pass→pass | 9,190 | 4,412 | -52% | 1 | 1 | 0% | 1,608 | 5,024 | +212% | 0 | 0 | — |
case-08 | pass→pass | 6,692 | 4,151 | -38% | 1 | 1 | 0% | 1,099 | 4,893 | +345% | 0 | 0 | — |
case-09 | pass→pass | 8,093 | 5,459 | -33% | 1 | 1 | 0% | 1,542 | 5,214 | +238% | 0 | 0 | — |
case-10 | pass→pass | 11,126 | 3,631 | -67% | 1 | 1 | 0% | 1,783 | 4,801 | +169% | 0 | 0 | — |
case-11 | fail→pass | 11,753 | 3,071 | -74% | 1 | 1 | 0% | 1,737 | 4,660 | +168% | 0 | 0 | — |
case-12 | pass→pass | 13,927 | 11,707 | -16% | 1 | 1 | 0% | 2,346 | 6,218 | +165% | 0 | 0 | — |
case-13 | pass→pass | 6,611 | 3,442 | -48% | 1 | 1 | 0% | 1,033 | 4,789 | +364% | 0 | 0 | — |
case-14 | pass→pass | 13,726 | 12,710 | -7% | 1 | 1 | 0% | 2,255 | 6,500 | +188% | 0 | 0 | — |
case-15 | pass→pass | 4,060 | 3,709 | -9% | 1 | 1 | 0% | 704 | 4,861 | +590% | 0 | 0 | — |
case-16 | pass→pass | 11,415 | 12,150 | +6% | 1 | 1 | 0% | 1,983 | 6,522 | +229% | 0 | 0 | — |
case-17 | pass→pass | 10,016 | 3,451 | -66% | 1 | 1 | 0% | 1,570 | 4,742 | +202% | 0 | 0 | — |
case-18 | fail→pass | 13,247 | 7,205 | -46% | 1 | 1 | 0% | 2,023 | 5,317 | +163% | 0 | 0 | — |
case-19 | pass→pass | 14,602 | 9,422 | -35% | 1 | 1 | 0% | 2,515 | 5,865 | +133% | 0 | 0 | — |
case-20 | pass→pass | 9,703 | 10,336 | +7% | 1 | 1 | 0% | 1,902 | 6,258 | +229% | 0 | 0 | — |
case-21 | pass→pass | 11,515 | 10,637 | -8% | 1 | 1 | 0% | 2,068 | 6,166 | +198% | 0 | 0 | — |
case-22 | pass→pass | 5,903 | 2,017 | -66% | 1 | 1 | 0% | 995 | 4,586 | +361% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted, and 21 counted toward the lift figure. The other 1 produced results that are not comparable between the two arms, so they are excluded from the headline rather than averaged into it. The headline lift of +18 percentage points is the difference between those two pass rates over the 21 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.