This tutorial is for biologists who know their way around a pipette and a microscope but have limited coding experience. By the end of one weekend you will have a working Python script that takes a microscopy image, sends it to a hosted segmentation API, and produces a clean CSV of cell counts and per-cell intensity statistics. No GPU, no heavy installation, no deep-learning background required.
The whole thing runs against the SciRouter imaging API, which hosts Cellpose-SAM and related segmentation models on managed GPUs. You call it from Python, which you can install on a MacBook or Windows laptop in about ten minutes.
Step 1 — install the tools
Open a terminal and install the few libraries you will need:
pip install requests numpy pillow pandas
That is the entire install. requests handles the API call, numpy handles arrays, pillow reads the image, and pandas writes the output CSV.
Step 2 — encode the image
The imaging endpoint takes images as base64-encoded strings in the JSON payload. Here is the helper to convert a PNG or TIFF file to a base64 string:
import base64
from pathlib import Path
def encode_image(path: str) -> str:
data = Path(path).read_bytes()
return base64.b64encode(data).decode("ascii")
Step 3 — call the segmentation endpoint
Now post the encoded image to the Cellpose endpoint with a couple of parameters:
import requests
API_URL = "https://scirouter-gateway-production.up.railway.app/v1/imaging/cellpose"
API_KEY = "sk-sci-your-api-key-here"
def segment(image_path: str, model: str = "cyto3", diameter: int = 30):
payload = {
"model": model,
"diameter": diameter,
"image_base64": encode_image(image_path),
"return_masks": True,
"return_stats": True,
}
headers = {"Authorization": f"Bearer {API_KEY}"}
r = requests.post(API_URL, json=payload, headers=headers, timeout=300)
r.raise_for_status()
return r.json()
The response contains a masks array (a 2D array of instance labels, one label per cell) and a stats list with per-cell area, centroid, and intensity values.
Step 4 — count cells
Cell counting is the simplest thing you can do with the output:
import numpy as np
def count_cells(result: dict) -> int:
masks = np.array(result["masks"])
# mask value 0 is background, every other value is one cell
return int(masks.max())
result = segment("field_01.tif", diameter=30)
print(f"cells detected: {count_cells(result)}")
Step 5 — build a CSV of per-cell metrics
The per-cell stats in the response map neatly into a pandas DataFrame. Write it out as a CSV and you have something you can open in Excel, R, or GraphPad:
import pandas as pd
def stats_to_csv(result: dict, out_path: str):
df = pd.DataFrame(result["stats"])
df.to_csv(out_path, index=False)
print(f"wrote {len(df)} rows to {out_path}")
stats_to_csv(result, "field_01_stats.csv")
The CSV columns typically include cell id, centroid x and y, area in pixels, perimeter, mean intensity, and a few shape descriptors. The exact list depends on the endpoint version — check the Cellpose tool page for the current schema.
Step 6 — process a whole folder
Looping over a directory lets you batch-process an entire experiment:
from pathlib import Path
def process_folder(folder: str, out_dir: str):
Path(out_dir).mkdir(parents=True, exist_ok=True)
summary = []
for img_path in sorted(Path(folder).glob("*.tif")):
try:
result = segment(str(img_path))
n_cells = count_cells(result)
out_csv = Path(out_dir) / f"{img_path.stem}_stats.csv"
stats_to_csv(result, str(out_csv))
summary.append({"image": img_path.name, "n_cells": n_cells})
print(f"{img_path.name}: {n_cells} cells")
except Exception as e:
print(f"{img_path.name}: FAILED — {e}")
pd.DataFrame(summary).to_csv(Path(out_dir) / "summary.csv", index=False)
process_folder("raw_images/", "results/")
This prints a per-image count, writes a CSV per image, and drops a summary CSV at the top. For a typical 96-well plate with one image per well, the whole batch runs in a few minutes.
Choosing the right parameters
Two parameters matter most:
- model — pick
cyto3for whole-cell segmentation,nucleifor DAPI or Hoechst, ortissuenetfor multiplexed tissue imaging. - diameter — the approximate cell size in pixels. Measure once on a representative image and use that value for the whole experiment.
Visual sanity check
Before trusting the batch output, overlay the masks on one image and eyeball the result:
from PIL import Image
import numpy as np
def overlay_masks(image_path: str, result: dict, out_path: str):
img = np.array(Image.open(image_path).convert("RGB"))
masks = np.array(result["masks"])
# tint detected cells red
overlay = img.copy()
overlay[masks > 0] = [255, 0, 0]
blended = (0.6 * img + 0.4 * overlay).astype(np.uint8)
Image.fromarray(blended).save(out_path)
overlay_masks("field_01.tif", result, "field_01_overlay.png")
Troubleshooting
Masks come back mostly empty
Your diameter is probably too small, so the model rejects anything larger. Double the diameter and try again.
Cells merged into one big blob
The diameter is probably too large. Halve it and try again.
Request times out
Your image is too large. Downsample to 1024x1024 or tile it manually, then segment each tile and stitch.
When to switch to the workspace UI
Scripting is great for batches. For parameter tuning on a new dataset, the browser imaging workspace is faster. Drag and drop an image, drag sliders for diameter and thresholds, and see masks update in real time. Once you find parameters that work, copy them into your script.
Bottom line
In about a weekend, a biologist with light Python skills can go from raw microscopy images to clean per-cell statistics, with reproducible code and no local GPU. The hosted API handles the hard parts — model weights, GPU scheduling, mask post-processing — and you focus on your biology.