Skip to main content

cp_measure implements CellProfiler measurements in an accessible interface.

Project description

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 current version of the preprint.

If you used cp_measure in your project, 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

pip install cp-measure

Usage

Featurizer (Recommended for small datasets)

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

import numpy as np
from cp_measure.featurizer import featurize

# image: (C, H, W) float array, masks: (N_masks, H, W) integer labels
image = np.random.default_rng(42).random((2, 240, 240))
masks = np.zeros((1, 240, 240), dtype=np.int32)
masks[0, 50:100, 50:100] = 1
masks[0, 150:200, 150:200] = 2

data, columns, rows = featurize(image, masks)
# data:    np.ndarray of shape (n_objects, n_features)
# columns: feature names (e.g. "Area", "Intensity_MeanIntensity__ch0", ...)
# rows:    [(None, "object", 1), (None, "object", 2)]  — (image_id, object_name, label) per row

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 first axis 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, ...

from cp_measure.featurizer import make_featurizer_config

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

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

config = make_featurizer_config(["DNA", "ER"], objects=["nuclei", "cells"])

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

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

Volumetric (C, Z, H, W) data is supported. The featurizer automatically skips 2D-only features (radial_distribution, radial_zernikes, zernike, feret). All other features (intensity, sizeshape, texture, granularity, correlations) work for both 2D and 3D.

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.

API (Recommended for large datasets)

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.

There are four 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)
  • Type 3: 2 sets of masks (e.g., number of neighbors)
  • Type 4: 1 image + 2 sets of masks (e.g., skeleton)

IMPORTANT: If you need to match CellProfiler measurements 1:1, you must convert your image arrays to float values between 0 and 1. For instance, if you have an array of data type uint16, you must divide them all by 65535. This is important for radial distribution measurements.

NOTE: The input labels must be sequential (e.g., [1,2,3], not [1,3,4]). You can use skimage.segmentation.relabel_sequential to ensure compliance.

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])}
"""

Call specific measurements

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

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

mask = np.zeros((50, 50))
mask[5:-6, 5:-6] = 1
get_sizeshape(mask, None)

Available Type 1 and 2 functions:

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

For Type 3 functions:

measureobjectoverlap.measureobjectoverlap
measureobjectneghbors.measureobjectneighboors

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.
  • thyme: Rust library to extract a subset of CellProfiler's features efficiently (independent implementation).

Contribute

Please use GitHub issues to report bugs and issues or submit a Pull Request.

Development installation

If you want to install it for development use uv.

git clone git@github.com:afermg/cp_measure.git
cd cp_measure
uv sync --all-groups

Current work

You can follow progress here.

Done

  • Type 1 and 2 in sklearn style (multiple integer labels in one mask array)

Pending

  • Add a wrapper for type 3 measurements
  • Type 4 measurements (ObjectSkeleton). We don't know if it is worth implementing.

Design notes

  • cp_measure is not optimised for efficiency (yet). We aim to reproduce the 'vanilla' results of CellProfiler with minimal code changes. Optimisations will be implemented once we come up with a standard interface for functionally-focused CellProfiler components.
  • The Image-wide functions will not be implemented directly, they were originally implemented independently to the Object (mask) functions. We will adjust the existing functions assume that an image-wide measurement is the same as measuring an object with the same size as the intensity image.
  • The functions do not include guardrails (e.g., checks of type or value). They will fail if provided with empty masks. Not all functions will fail if provided with masks only.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

cp_measure-0.1.18.tar.gz (62.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

cp_measure-0.1.18-py3-none-any.whl (70.8 kB view details)

Uploaded Python 3

File details

Details for the file cp_measure-0.1.18.tar.gz.

File metadata

  • Download URL: cp_measure-0.1.18.tar.gz
  • Upload date:
  • Size: 62.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.10.10 {"installer":{"name":"uv","version":"0.10.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"NixOS","version":"26.05","id":"yarara","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for cp_measure-0.1.18.tar.gz
Algorithm Hash digest
SHA256 21f0253a8b19b4e1c0b7503358766b3cc313c4332d24ca6bbcfd265b3d3409ce
MD5 0e9b14fd61af29c54fce8e51b0baf011
BLAKE2b-256 b84eca61b2ef877829e4334a325713c5549039a6c15279f9f95144d30d7e0e20

See more details on using hashes here.

File details

Details for the file cp_measure-0.1.18-py3-none-any.whl.

File metadata

  • Download URL: cp_measure-0.1.18-py3-none-any.whl
  • Upload date:
  • Size: 70.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.10.10 {"installer":{"name":"uv","version":"0.10.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"NixOS","version":"26.05","id":"yarara","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for cp_measure-0.1.18-py3-none-any.whl
Algorithm Hash digest
SHA256 71e9202fb3e4130aa4136e366df9e64b8af050befdd37909483ced3da1106742
MD5 626531c30d4a939dcff2fd2c047697da
BLAKE2b-256 c724af521b9395d0e0125a9c3e549341b6c08e413581a20839b239bc3ade759f

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page