Skip to content
Proyectos

afermg/cp_measure has 53 GitHub stars and 11 forks. GitHot measured 0 stars in 24 hours. Last checked 2026-11-11 08:39 UTC. Last push 2026-11-08.

Cómo se miden estas cifras

Morphological features from images and masks made easy.

Crecimiento de estrellas medido

in 24h
0
in 7 days
Recopilando historial
in 15 days
Recopilando historial
in 1 month
Recopilando historial

Cambios netos en períodos que se solapan, no totales diarios. Los datos no disponibles se dejan en blanco. Las estrellas indican interés, no calidad.

README.md

Original

cp_measure: Morphological features for imaging data

Do you need to use CellProfiler features, but you want to do it in a programmatic way? Look no more, this package was developed by and for the click-a-phobic scientists.

Preprint

Here is the preprint. Published as a workshop paper for ICML 2025's CODEML.

Please cite using the following .bib entry
@article{munoz2025cpmeasure,
  title={cp\_measure: API-first feature extraction for image-based profiling workflows},
  author={Mu{\~n}oz, Al{\'a}n F and Treis, Tim and Kalinin, Alexandr A and Dasgupta, Shatavisha and Theis, Fabian and Carpenter, Anne E and Singh, Shantanu},
  journal={arXiv preprint arXiv:2507.01163},
  year={2025}
}

Quick overview

Installation

Requires Python 3.11 or newer.

pip install cp-measure

Usage

We provide three entry points: (Featurizer) We orchestrate the images x combinations, (Bulk API) we give you all features and you orchestrate, and (Low-level) you directly import the functions.

Featurizer (Recommended for small datasets)

The simplest way to extract all features from an image and its masks:

Image axes are (Batch, Channel, Y, X); mask axes are (Batch, Mask type, Y, X). Keep the batch axis even for one image (Batch=1).

import numpy as np
from cp_measure.featurizer import featurize, make_featurizer_config

# B = batch size, C = channels; a single image has B=1.
# Dimensionality is declared with is_3d (default False), never inferred from shape.
image = np.random.default_rng(42).random((1, 2, 240, 240))
masks = np.zeros((1, 1, 240, 240), dtype=np.int32)
masks[0, 0, 50:100, 50:100] = 1
masks[0, 0, 150:200, 150:200] = 2

# Choose a 255-step threshold grid for this synthetic example.
# For real normalized images, use their acquisition scale instead.
config = make_featurizer_config(correlation_costes_params={"scale_max": 255})
data, columns, rows = featurize(image, masks, config)  # is_3d=False
# data:    np.ndarray of shape (n_objects, n_features)
# columns: feature names (e.g. "Area", "Intensity_MeanIntensity__ch0", ...)
# rows:    [(0, "object", 1), (0, "object", 2)]  — (image_id, object_name, label) per row
#          image_id defaults to the batch index; pass image_ids=[...] for your own ids

To customise which features are extracted, or to name your channels and masks, use make_featurizer_config. Channel names are matched positionally to the image's channel axis (axis 1) and control how per-channel features are labeled in the output columns (e.g. "Intensity_MeanIntensity__DNA"). If omitted, channels are auto-named ch0, ch1, ...

import numpy as np
from cp_measure.featurizer import featurize, make_featurizer_config

# Recreate variables from previous examples for this block to run in isolation
image = np.random.default_rng(42).random((1, 2, 240, 240))
masks = np.zeros((1, 1, 240, 240), dtype=np.int32)
masks[0, 0, 50:100, 50:100] = 1
masks[0, 0, 150:200, 150:200] = 2

# Disable texture features, name channels explicitly
config = make_featurizer_config(
    ["DNA", "ER"], texture=False,
    correlation_costes_params={"scale_max": 255},
)
data, columns, rows = featurize(image, masks, config)

Multiple mask types (e.g. nuclei and cells) are supported by stacking them along the mask-type axis (axis 1):

import numpy as np
from cp_measure.featurizer import featurize, make_featurizer_config

# Recreate variables from previous examples for this block to run in isolation
image = np.random.default_rng(42).random((1, 2, 240, 240))

config = make_featurizer_config(
    ["DNA", "ER"], objects=["nuclei", "cells"],
    correlation_costes_params={"scale_max": 255},
)

masks = np.zeros((1, 2, 240, 240), dtype=np.int32)
masks[0, 0, 50:100, 50:100] = 1    # nucleus 1
masks[0, 1, 40:110, 40:110] = 1    # cell 1
masks[0, 1, 150:200, 150:200] = 2  # cell 2
masks[0, 1, 175:180, 180:210] = 2  # Minor asymmetries on bottom right edge of cells

data, columns, rows = featurize(image, masks, config)
# rows: [(0, "nuclei", 1), (0, "cells", 1), (0, "cells", 2)]

Single- and multichannel volumes are supported: pass images shaped (B, C, Z, Y, X) and masks shaped (B, M, Z, Y, X) with is_3d=True. Here M is the number of mask types (e.g. nuclei and cells), not the number of labelled objects. Batch size and all spatial dimensions must match exactly. The featurizer does not repeat or broadcast masks automatically, including singleton Z axes. Inputs are not modified.

If you want a 2D label map to span a volume, repeat it yourself before calling featurize, as below. Each repeated label then represents one 3D object, not one object per slice. The featurizer skips 2D-only features (radial_distribution, radial_zernikes, zernike, feret) with a warning. See the single-slice size/shape limitation.

import numpy as np
from cp_measure.featurizer import featurize, make_featurizer_config

volume = np.random.default_rng(42).random((1, 1, 4, 32, 32))  # B, C, Z, Y, X
masks_2d = np.zeros((1, 1, 32, 32), dtype=np.int32)  # B, M, Y, X
masks_2d[0, 0, 8:24, 12:20] = 42
# Explicit caller-side conversion; featurize does not add or repeat this axis.
masks_3d = np.repeat(masks_2d[:, :, None], volume.shape[2], axis=2)
config = make_featurizer_config(["DNA"])
data, columns, rows = featurize(volume, masks_3d, config, is_3d=True)
assert rows == [(0, "object", 42)]  # one object spanning all four slices

Migrating from the single-image API: add a batch axis to both arrays: featurize(image[None], masks[None], config). For volumes, also pass is_3d=True. Replace image_id="sample" with image_ids=["sample"]. Previously, omitting image_id produced None in row metadata; now image_ids=None produces zero-based batch indices, so a single-image batch produces 0.

The output is plain numpy + lists, so converting to a DataFrame is straightforward:

import pandas as pd
row_names = [f"{img}__{obj}__{label}" for img, obj, label in rows]
df = pd.DataFrame(data, index=row_names, columns=columns)

Note: DataFrame libraries must be installed independently, to keep the dependency tree low.

Bulk API (Access all measurements at once)

For more control over individual measurements, or to call specific functions directly, use the bulk API. It operates on single images and masks following the scikit-image convention.

cp_measure currently provides two types of measurements based on their inputs:

  • Type 1: 1 image + 1 set of masks (e.g., intensity)
  • Type 2: 2 images + 1 set of masks (e.g., colocalization)
import numpy as np
from cp_measure.bulk import get_core_measurements

measurements = get_core_measurements()
# print(measurements.keys())
# dict_keys(['radial_distribution', 'radial_zernikes', 'intensity', 'sizeshape', 'zernike', 'feret', 'texture', 'granularity'])

# Create synthetic data
size = 240
rng = np.random.default_rng(42)
pixels = rng.integers(low=1, high=255, size=(size, size))

# Create two similar-sized objects
masks = np.zeros_like(pixels)
masks[50:100, 50:100] = 1
masks[150:200, 150:200] = 2

measurements = get_core_measurements()
results = {}
for name, func in measurements.items():
    results = {**results, **func(masks, pixels)}

"""
{'RadialDistribution_FracAtD_1of4': array([0.03673493, 0.05640786]),
 'RadialDistribution_MeanFrac_1of4': array([1.02857809, 1.15072037]),
 'RadialDistribution_RadialCV_1of4': array([0.05539421, 0.04635982]),
 ...
 'Granularity_16': array([97.65759629, 97.64371833])
}
"""

Low-level access

Individual measurement functions can be imported directly. Each returns a dictionary of arrays.

import numpy as np
from cp_measure.core.measureobjectsizeshape import get_sizeshape

mask = np.zeros((50, 50), dtype=np.int32)
mask[5:-6, 5:-6] = 1
get_sizeshape(mask, None)
measureobjectintensitydistribution.get_radial_zernikes
measureobjectintensity.get_intensity
measureobjectsizeshape.get_zernike
measureobjectsizeshape.get_feret
measuregranularity.get_granularity
measuretexture.get_texture
measurecolocalization.get_correlation_pearson
measurecolocalization.get_correlation_manders_fold
measurecolocalization.get_correlation_rwc
measurecolocalization.get_correlation_costes
measurecolocalization.get_correlation_overlap

Important notes

  • Labels: Use positive integers for objects and 0 for background. featurize and the bulk registries relabel non-contiguous IDs internally without modifying your array; featurize reports the original IDs. Raw measurement functions require contiguous 1..N labels; wrap them with cp_measure._sanitize.sanitize when needed.
  • Image shapes: featurize requires dense (B, C, *spatial) images and (B, M, *spatial) masks. A single image still needs B=1. Ragged (differently-sized) batches are not supported; normalise to a common shape and stack first.
  • Fidelity: Use float intensities in [0, 1] to match CellProfiler's input convention (e.g. divide uint16 values by 65535). Matching the original intensity quantile measurements also requires legacy=True; see the legacy percentile convention.
  • Costes scale (breaking correction): Float inputs to get_correlation_costes and get_correlation_costes_ind must be normalized to [0, 1] and specify scale_max (e.g. 65535 for normalized 16-bit data). It controls the threshold grid; it is not the observed image maximum. Both matching unsigned-integer dtypes infer their storage maximum and are normalized internally. For 12-bit data stored in uint16, pass scale_max=4095 explicitly; signed or mixed-dtype arrays also require a scale. Through the featurizer, use correlation_costes_params={"scale_max": 65535} or disable Costes. cp_measure fits thresholds per object, whereas CellProfiler's object workflow fits across the image.
  • Speed: v0.2.0 speeds up the default NumPy/SciPy implementation without adding required runtime dependencies; see the performance report. Optional Numba and JAX backends remain under development and are not yet supported.

See Measurement notes for texture quantization, percentile formulas, radial-distribution centers, and floating-point caveats.

Similar projects

  • spacr: Library to analyse screens, it provides measurements (independent implementation) and a GUI.
  • ScaleFEX: Python pipeline that includes measurements, designed for the cloud.
  • CharmFeatures: Library, Python module, and command-line utility for extracting Wnd-Charm image features from large TIFF collections.
  • thyme: Rust library to extract a subset of CellProfiler's features efficiently (independent implementation).
  • CellProfiler Library: WIP library that isolates CellProfiler image-processing functions from its frontend.
Current work

You can follow progress here.

Most features are implemented, but Type 3 measurements (e.g., ObjectNeighbors) does not have a wrapper. We do not plan to implement ObjectSkeleton.

Contributing

See CONTRIBUTING.md for details.

Explorar proyectos relacionados