Skip to content

Cell embeddings

encode_cells() returns a CellEmbeddings object with one row per cell. Each row contains a pre-projection image-derived cell embedding, its projected representation in the shared cell space used for annotation, and spatial information when available.

Which representation should I use?

  • Use the pre-projection embedding (X_alhena_backbone) for general downstream analysis such as neighbors, UMAP, and clustering.
  • Use the projected cell-space embedding (X_alhena_cell) to inspect the representation used for reference-based annotation.
cells = encoder.encode_cells(source, nuclei)

print(len(cells), int(cells.valid.sum()))       # cells, and how many can be annotated
print(cells.backbone.shape)                     # (n_cells, 1536)
print(cells.cell_space.shape)                   # (n_cells, 512)
print(cells.centroids_frame, cells.centroids_mpp)

What a cell embedding represents

ALHENA derives each cell representation by pooling image tokens according to the pixels covered by the nucleus. Each token corresponds to approximately 5.95 µm of the image and is itself computed in the context of the surrounding tile.

The resulting cell representation therefore combines information from the nucleus with local tissue context. Cells whose nuclei overlap some of the same image tokens can consequently have correlated representations.

Each cell stores two related embeddings:

Representation Width Role
backbone / X_alhena_backbone 1536 pre-projection image-derived cell embedding used for general downstream analysis
cell_space / X_alhena_cell 512 projection of the cell embedding into the shared space used for reference-based annotation

X_alhena_cell is obtained from the pre-projection cell representation through the learned projection head img_tok_proj.

See The shared cell space and Cell pooling.

Which cell representation should I use?

For general analysis of image-derived cellular variation, use X_alhena_backbone. Despite its API name, this is best understood as ALHENA's pre-projection cell embedding. It is derived from H&E features learned under molecular supervision, but it is not gene expression.

Typical uses include:

  • nearest-neighbor graphs;
  • dimensionality reduction;
  • clustering into cell embedding clusters;
  • analysis of cellular heterogeneity.

Use X_alhena_cell when the analysis specifically concerns the shared representation used for cell-type annotation. This space is where image-derived cells are compared with the single-cell reference.

X_alhena_cell is a projection of X_alhena_backbone; the two representations are therefore related rather than independent.

What CellEmbeddings holds

CellEmbeddings
├── cell_id                  the identifier of each cell
├── backbone                 pre-projection cell embedding
├── cell_space               projected embedding used for annotation
├── valid                    whether the cell can be used for annotation
├── centroids                the position of each nucleus, where it can be placed
├── centroids_frame          the frame of those positions
├── centroids_mpp            the microns one unit of that frame spans
├── tile_index, alpha,       the tile each cell was read from, its nucleus pixels there,
│   n_candidate_tiles        and how many tiles held the nucleus
├── encoder_id,              the encoder and cell space identities
│   cell_space_id
├── backbone_digest          the digest of the UNI2-h model weights
├── geometry                 the geometry the tiles presented to the model
├── membership_source,       how the nuclei were given, and whether their shapes were
│   membership_approximate   approximated
├── n_cells_without_tokens   how many nuclei reached no token
└── tiles                    the embeddings of the tiles the cells were read from
Field Holds
cell_id the identifier of each cell; rows are ordered by it
backbone pre-projection cell embeddings, (n_cells, 1536) float32
cell_space projected embeddings in the shared cell space, (n_cells, 512) float32
valid False where the embedding cannot be annotated: no nucleus pixel was pooled, or it is not finite
centroids (n_cells, 2), x then y, or None
centroids_frame the frame of centroids, such as "slide.svs, level 0"
centroids_mpp microns per unit of that frame
tile_index the tile each cell was read from
alpha the nucleus pixels supporting the cell in that tile
n_candidate_tiles how many tiles held the nucleus
encoder_id, cell_space_id the identities that pair the cells with a reference
backbone_digest the digest of the UNI2-h model weights the cells were encoded with, or None
geometry the geometry the tiles presented, as describe_geometry reports it; cells.mpp reads its resolution
membership_source how the nuclei were given: labels, centroids, polygons, or unknown
membership_approximate whether the nucleus shapes were approximated, as centroid disks are
n_cells_without_tokens how many nuclei reached no token
tiles a TileEmbeddings with purpose="cell_context"

Cells omitted or marked invalid

A nucleus that cannot be associated with any image token is omitted from the returned rows. cells.n_cells_without_tokens records how many nuclei were excluded for this reason.

A cell may also remain present while being marked valid=False, for example when its embedding cannot be used safely for annotation. Keeping the row allows the cell and its provenance to remain inspectable.

See Some of my nuclei have no row.

Cell positions

When the nucleus can be located in a shared image coordinate system, CellEmbeddings stores its position in centroids.

Depending on the nucleus representation, this position corresponds to the centroid of the segmented shape, the centroid of the labeled pixels, or the supplied centroid itself. centroids_frame identifies the coordinate system and centroids_mpp gives its physical scale.

For label maps and centroids, spatial positions require tiles with known origins. See Where positions end up.

Tile context retained during cell encoding

cells.tiles contains the tile embeddings computed during the same forward pass as the cell embeddings, with purpose="cell_context".

By default, these are only the tiles needed for the supplied nuclei, so they may not provide systematic coverage of the full image. For an independent tile-level analysis, use encode_tiles().

If encode_cells(..., all_tiles=True) is used, all tiles are processed during the cell pass, avoiding a second encoder pass while retaining purpose="cell_context" as provenance. At the same batch_size, these tile embeddings equal those of encode_tiles(). See Dedicated tiles and cell-context tiles.

As an AnnData table

cells.to_anndata() returns the cells as an AnnData table. One observation is one cell.

Where Key Holds
obsm X_alhena_backbone the pre-projection cell embeddings, (n_cells, 1536)
obsm X_alhena_cell the projected embeddings in the shared cell space, (n_cells, 512)
obsm spatial the position of each nucleus, x then y, where recorded
obs cell_id the identifiers, as integers or text
obs alhena_tile_index the tile each cell was read from
obs alhena_nucleus_px the nucleus pixels behind the cell there
obs alhena_n_candidate_tiles how many tiles held the nucleus
obs alhena_embedding_valid whether the embedding can be annotated
uns alhena_cells the provenance, including centroids_frame and centroids_mpp
X nothing: zero columns wide

The index holds the identifiers as text, which AnnData requires. As for tiles, the embeddings sit in obsm, not in X: they are representation features rather than measured variables such as genes. The tile embeddings on cells.tiles are not part of this table.

cells.to_spatialdata(sdata, region="nuclei") writes this table into a SpatialData object that holds the nuclei, tied to their element, before any annotation. See Writing into a SpatialData object.

Encode once, annotate many times

Cell encoding depends on the H&E image and nuclei, not on the single-cell reference. The resulting CellEmbeddings can therefore be saved once and reused with different compatible references or annotation axes without running the image encoder again:

cells.write_h5ad("cells.h5ad")

cells = alhena.io.read_cells("cells.h5ad")
result = alhena.load_celltyper(tissue="Breast").annotate(cells)

read_cells() also restores the cell_space_id, allowing ALHENA to verify that the saved cells and the selected reference use the same shared cell space.


Next: Cell annotations