Skip to main content

patchworks logo

patchworks

PyPI Python versions License: GPLv3 Docs

Tiled processing of arbitrarily large images — any image, any function.

┌──────┬──────┬──────┐     fn(tile) → labels      ┌──────┬──────┬──────┐
│ tile │ tile │ tile │  ─────────────────────►    │  1   │  2   │  3   │
├──────┼──────┼──────┤                            ├──────┼──────┼──────┤
│ tile │ tile │ tile │                            │  4   │  5   │  6   │   globally
├──────┼──────┼──────┤                            ├──────┼──────┼──────┤   consistent
│ tile │ tile │ tile │                            │  7   │  8   │  9   │   labels
└──────┴──────┴──────┘                            └──────┴──────┴──────┘

patchworks splits a large image into tiles, runs any callable on each tile in parallel, and merges the results into a globally consistent label array. It handles terabyte-scale images without loading them into memory.


Installation

pip install patchworks

Optional extras:

pip install "patchworks[gpu]"       # GPU VRAM querying (nvidia-ml-py)
pip install "patchworks[distributed]" # Dask cluster + live dashboard
pip install "patchworks[cellpose]"  # Cellpose plugin (>=3.0, v3 or v4)
pip install "patchworks[cellpose3]" # Cellpose plugin, pinned to v3.x
pip install "patchworks[cellpose4]" # Cellpose plugin, pinned to v4+
pip install "patchworks[dog]"       # deconvolution + DoG plugin (pycudadecon)
pip install "patchworks[bioio]"     # convert any image format to OME-ZARR
pip install "patchworks[imaris]"    # convert Imaris .ims files to OME-ZARR
pip install "patchworks[napari]"    # interactive napari viewer plugin
pip install "patchworks[all]"       # Everything, incl. the napari viewer

bioio reads CZI/LIF/ND2/OME-TIFF/… The [bioio] extra bundles the common native readers (bioio-nd2, bioio-ome-tiff, bioio-czi, bioio-tifffile, bioio-lif) plus bioio-bioformats, the Bio-Formats catch-all reader (JVM). [imaris] adds native .ims support (HDF5, no JVM). Physical pixel calibration is read from the input and written into the OME-ZARR.

cupy is never installed automatically, unlike Cellpose's GPU support (which comes for free via PyTorch's self-contained CUDA wheels). Any use_gpu=True/dilate_gpu: true option (the dog plugin, dilate_labels) needs cupy installed separately, matching your CUDA version — e.g. pip install cupy-cuda12x. Not bundled because cupy ships one wheel per CUDA major version; a generic pin would resolve to the wrong build (or fail to resolve) depending on the machine.


Quick start — 5 lines

from patchworks import tile_process


def my_fn(tile):
    from skimage.filters import threshold_otsu
    from skimage.measure import label

    return label(tile > threshold_otsu(tile)).astype("int32")


result = tile_process("image.zarr", my_fn)

Done. result is a lazy dask array of integer labels (call .compute() for a NumPy array), same spatial shape as the input, with globally unique IDs across all tiles. By default the labels are also written into the input store at image.zarr/labels/labels/ as a multi-scale pyramid, so the image and its segmentation live in one OME-ZARR. Pass write_to="labels.zarr" to write a separate store instead.


With Cellpose

from patchworks import tile_process
from patchworks.plugins.cellpose import cellpose_fn

fn = cellpose_fn("cyto3", gpu=True, diameter=30)

tile_process(
    "image.zarr",
    fn,
    tile_shape=(1, 2048, 2048),  # one z-slice per tile
    overlap=20,  # gives boundary cells enough context
    write_to="labels.zarr",  # stream directly to disk — no RAM accumulation
    progress=True,
)

With StarDist

from stardist.models import StarDist2D
from patchworks import tile_process

model = StarDist2D.from_pretrained("2D_versatile_fluo")


def stardist_fn(tile):
    img = tile[0] if tile.ndim == 3 and tile.shape[0] == 1 else tile
    norm = img.astype("float32") / (img.max() or 1)
    labels, _ = model.predict_instances(norm)
    return labels.astype("int32")[None] if tile.ndim == 3 else labels.astype("int32")


tile_process(
    "image.zarr",
    stardist_fn,
    tile_shape=(1, 1024, 1024),
    overlap=32,
    write_to="labels.zarr",
    progress=True,
)

With any function

import numpy as np
from scipy.ndimage import gaussian_filter
from skimage.measure import label
from patchworks import tile_process


def my_custom_fn(tile: np.ndarray) -> np.ndarray:
    smoothed = gaussian_filter(tile.astype("float32"), sigma=1.5)
    binary = smoothed > smoothed.mean()
    return label(binary).astype("int32")


tile_process("image.zarr", my_custom_fn, tile_shape=(1, 512, 512))

Convert to OME-ZARR & view in napari

Optional plugins close the loop: convert any image (Imaris .ims, CZI, LIF, ND2, OME-TIFF, … via bioio) to a pyramidal, calibrated OME-ZARR, then view the image and its labels in napari.

from patchworks.plugins.ome_zarr import to_ome_zarr
from patchworks.plugins.napari import view_in_napari

to_ome_zarr("scan.ims", "scan.zarr")          # lazy, OOM-safe, keeps µm calibration
view_in_napari("scan.zarr", labels="scan.zarr/labels/labels")

Pyramids downsample X/Y only (Z kept full-res) and are built level-by-level from disk, so terabyte volumes convert in bounded RAM. See the OME-ZARR & napari guide.


From the command line

patchworks convert scan.czi scan.zarr
patchworks segment scan.zarr --method cellpose --model cyto3 --diameter 30 --gpu
patchworks info scan.zarr
patchworks view scan.zarr

See Command line for every method and flag.

Common patterns

Auto-size tiles from available memory

from patchworks import tile_process

tile_process("image.zarr", fn, tile_shape="auto", use_gpu=True)

Skip empty tiles (sparse volumes)

from patchworks import estimate_empty_tiles, tile_process

info = estimate_empty_tiles("image.zarr", tile_shape=(120, 697, 697))
print(f"{info['empty_fraction']:.0%} tiles are background — will be skipped")

tile_process(
    "image.zarr",
    fn,
    tile_shape=(120, 697, 697),
    skip_empty=True,
    empty_threshold=info["threshold"],
    write_to="labels.zarr",
)

Distributed cluster for GPU

from patchworks import make_local_cluster, tile_process

client, cluster = make_local_cluster(use_gpu=True)
try:
    tile_process("image.zarr", fn, write_to="labels.zarr", progress=True)
finally:
    client.close()
    cluster.close()

Contiguous label numbering

# Labels are globally unique by default, but may be gappy (block-encoded IDs).
# sequential_labels=True does a linear relabel O(voxels) — not O(n_tiles²).
tile_process("image.zarr", fn, write_to="labels.zarr", sequential_labels=True)

Use only the merge step (bring your own tiling)

If you already have per-tile labels from your own pipeline, just call the merge step directly:

import dask.array as da
import numpy as np
from patchworks import merge_tile_labels

# Your own tiling + segmentation
image = da.from_zarr("image.zarr").rechunk((1, 1024, 1024))
labeled = image.map_blocks(
    my_segment_fn, dtype="int32", meta=np.empty((0,) * image.ndim, dtype="int32")
)

merged = merge_tile_labels(labeled, write_to="labels.zarr", progress=True)

Or merge from a zarr store your pipeline already wrote:

from patchworks import merge_tile_labels

merged = merge_tile_labels(
    "my_staged_labels.zarr",
    input_component="raw_labels",
    write_to="merged.zarr",
    sequential_labels=True,
)

How tiling and merging work

See the Merging labels guide for a full explanation. Short version:

  1. Image is split into tiles (with optional overlap for boundary context).
  2. Your function is called independently on each tile. Dask handles parallelism and streaming — tiles are never all in memory at once.
  3. Each tile's labels are written to a temp zarr exactly once (the staging step — this prevents your function being called 3-4× per tile during merge).
  4. Thin slabs at each tile boundary are scanned for touching label pairs.
  5. scipy connected components on the pairs → relabeling lookup table.
  6. LUT applied to every tile in parallel → globally consistent labels.

The merge is zarr-native (no dask task graph), so it scales to thousands of tiles where the dask-image approach stalls.


Known pitfalls (and how patchworks avoids them)

Pitfall Symptom How patchworks handles it
In-process Dask client FutureCancelledError: lost dependencies Detected at startup, raises immediately with fix instructions
3-4× fn recompute during merge Cellpose runs 3× per tile Staging writes labels once, merge reads from disk
O(n²) sequential relabelling Graph construction hangs at 1000+ tiles Folded into the merge's own LUT — no extra pass over the volume
Wrong overlap boundary Output shape mismatch Always uses boundary="none"
Persisting large arrays Worker OOM Never persists; keeps dask graph lazy and streams
Sizing work to the whole node Job OOM-killed on a shared cluster node Reads the SLURM/cgroup allocation, not os.cpu_count()
Rechunking a pyramid level Threaded scheduler stockpiles intermediates Levels stream one source chunk per task, bounded by construction
Isotropic halo on flat tiles 5× the voxels read and segmented, then trimmed overlap takes one width per axis

Documentation

Full docs, guides and tutorials: https://imcf.one/patchworks/


Requirements

  • Python ≥ 3.11
  • dask[array], numpy, zarr ≥ 3.1.3, scipy, psutil (RAM-aware tile and worker sizing)

Optional:

  • nvidia-ml-py — accurate GPU VRAM sizing
  • tqdm — progress bars
  • cellpose — Cellpose plugin, v3 or v4 (patchworks[cellpose]); pin with [cellpose3] or [cellpose4]
  • pycudadecon — deconvolution step of the dog plugin (patchworks[dog])
  • bioio + readers — convert CZI/LIF/ND2/OME-TIFF/… to OME-ZARR (patchworks[bioio])
  • imaris-ims-file-reader — convert Imaris .ims (patchworks[imaris])
  • napari — interactive viewer plugin (patchworks[napari])
  • cupy — install manually, matching your CUDA version (e.g. pip install cupy-cuda12x); not offered as an extra since it isn't one generic pin. Needed for use_gpu=True/dilate_gpu: true.

License

GNU General Public License v3.0 (GPL-3.0). See LICENSE.

Release files for patchworks 3.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for patchworks 3.0.0
File Size Uploaded
patchworks-3.0.0.tar.gz 627.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for patchworks 3.0.0
File Interpreter ABI Platform
patchworks-3.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 783.1 kB

Release files / patchworks-3.0.0.tar.gz

Download URL patchworks-3.0.0.tar.gz
Size 627.4 kB
Tags Source
SHA-256 checksum
How to use checksums
309b18abeaa89ae9220eb50e7ae5a9c083149eae2efedd96507d41c3d2fe9df0
BLAKE2b-256 checksum
How to use checksums
f99b3546bf84d02ad1fdf92a3f2f28e1de0ed0f76fd4204d1cbf516d68b80f98
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 25, 2026.

Transparency log

Release files / patchworks-3.0.0-py3-none-any.whl

Download URL patchworks-3.0.0-py3-none-any.whl
Size 155.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f0476dfbfced081a0aa14d4fd7ba6a88c26851f50ac637c4541e35c8364b3c35
BLAKE2b-256 checksum
How to use checksums
9888bbbf08ecd508840fd143953add9f39c3c18e7b0ecc12ab2e69cfd666e461
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 25, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

3.0.0 This release

2 release files

2.8.0

2 release files

2.7.0

2 release files

2.6.12

2 release files

2.6.11

2 release files

2.6.9

2 release files

2.6.8

2 release files

2.6.7

2 release files

2.6.6

2 release files

2.6.5

2 release files

2.6.4

2 release files

2.6.3

2 release files

2.6.2

2 release files

2.6.1

2 release files

2.6.0

2 release files

2.5.2

2 release files

2.5.1

2 release files

2.5.0

2 release files

2.4.0

2 release files

2.3.0

2 release files

2.2.0

2 release files

2.1.2

2 release files

2.1.1

2 release files

2.1.0

2 release files

2.0.0

2 release files

1.4.4

2 release files

1.4.3

2 release files

1.4.2

2 release files

1.4.1

2 release files

1.4.0

2 release files

1.3.4

2 release files

1.3.3

2 release files

1.3.2

2 release files

1.3.1

2 release files

1.3.0

2 release files

1.2.2

2 release files

1.2.1

2 release files

1.2.0

2 release files

1.1.1

2 release files

1.1.0

2 release files

1.0.0

2 release files

0.11.9

2 release files

0.11.8

2 release files

0.11.7

2 release files

0.11.6

2 release files

0.11.5

2 release files

0.11.4

2 release files

0.11.3

2 release files

0.11.2

2 release files

0.11.1

2 release files

0.11.0

2 release files

0.10.0

2 release files

0.9.1

2 release files

0.9.0

2 release files

0.8.1

2 release files

0.8.0

2 release files

0.7.0

2 release files

0.6.0

2 release files

0.5.0

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page