Skip to main content

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

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:

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

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((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

# 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:

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((2, 240, 240))

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.

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: Any positive integer labels work — non-contiguous IDs (e.g. [1, 3, 4]) are relabelled to 1..N internally without modifying your array, and results are reported against your original IDs. featurize and the bulk get_* registries sanitize by default (sanitize=False to opt out); raw measurement functions assume contiguous 1..N, so wrap them with cp_measure._sanitize.sanitize if you call them directly with gapped IDs.
  • Fidelity: 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. For the four intensity quantile features (LowerQuartileIntensity, MedianIntensity, UpperQuartileIntensity, MADIntensity) you additionally need legacy=True — see below.
  • Speed: v0.2.0 substantially speeds up the default NumPy/SciPy implementation without adding required runtime dependencies; see the v0.2.0 performance report for release-wide and incremental CI benchmarks. Future performance work will pursue two optional acceleration paths, Numba and JAX; both are still under development and are not yet supported backends.
  • Legacy percentile convention: get_intensity, get_core_measurements, get_core_measurements_3d, and make_featurizer_config accept legacy: bool = False. The default uses numpy.percentile 'linear' ((n-1)*q) quartiles and the textbook median(|x - median(x)|) MAD. Pass legacy=True to reproduce the original cp_measure / CellProfiler behavior: n*q quartiles and the (1/ndim)-quantile MAD (which returns the 33rd percentile in 3D rather than the median). The experimental Numba intensity path mirrors this flag but remains unsupported; all other intensity features are identical either way.
  • Radial-distribution centers: If several pixels are tied for the greatest distance from an object's edge, cp_measure uses the first in C order. This fixes label-dependent results from SciPy's previous unstable tie-break (issue #22); symmetric objects may therefore differ from older cp_measure releases.
  • Minor floating-point discrepancy: Some features produce a minor (1e-16) discrepancy when using one vs multiple masks. The issue lies upstream in centrosome and does not significantly impact my use-cases.

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).
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.

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.2.0.tar.gz (73.0 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.2.0-py3-none-any.whl (78.7 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: cp_measure-0.2.0.tar.gz
  • Upload date:
  • Size: 73.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for cp_measure-0.2.0.tar.gz
Algorithm Hash digest
SHA256 d801de512eb380b30139190077ef43e500b04142fd75df2a3f9160f518c1395c
MD5 dc2db9feef9f0624fab7714046d74880
BLAKE2b-256 20df9647590e7342bfb647cd4d3e3cdf2f67543484d55d8083ee1b8565eabbf5

See more details on using hashes here.

File details

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

File metadata

  • Download URL: cp_measure-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 78.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for cp_measure-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 efbbd3b7f006dabd73ef452abc231dc87a249512140fb4652cab4ca304daf07d
MD5 5acd7bd78004c721034dbbf2eaec7f0e
BLAKE2b-256 2fc593e75d32d468eff9a4a8966ab4dcdd6d5d241303076cf0b87ac71436698a

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 files

0.1.19

2 files

0.1.18

2 files

0.1.17

2 files

0.1.16

2 files

0.1.15

2 files

0.1.14

2 files

0.1.13

2 files

0.1.12

2 files

0.1.11

2 files

0.1.10

2 files

0.1.9

2 files

0.1.8

2 files

0.1.7

2 files

0.1.6

2 files

0.1.5

2 files

0.1.4

2 files

0.1.3

2 files

0.1.2

2 files

0.1.1

2 files

0.0.0

2 files

Supported by

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