H&E ↔ Xenium registration pipeline. One CLI invocation per sample. Aligns a hematoxylin-and-eosin whole-slide image to a matched Xenium in-situ transcriptomics run. Warps Xenium cell + nucleus segmentations into H&E pixel space. Renders a per-slide celltype-annotated overlay.
- CLI:
hexenium run …(Python package + console script). - Env: pinned conda + pip layer at
environments/heRegistration.yml. Default env nameheRegistration. - HPC:
scripts/submit_he_registration.sh …self-submits to Slurm. Handles VSI inputs via anafterok:conversion chain. Routes logs into the run tree.
Under the hood: VALIS
(via valis_hest) solves the registration; HEST does the
boundary warp. Five stages
(he_preprocess → register → warp → celltype → viz). Each
stage is guarded by a sentinel file, so reruns pick up where
the last invocation left off.
Contents: Pipeline overview · Installation · Quickstart · Invocation modes · Stage details · Output tree · Configuration · HPC / Slurm · Re-runs & resume · Development & testing · Citation
Deeper docs: docs/install.md
(troubleshooting + env drift), docs/methods.md
(algorithm details), docs/usage.md
(extended examples).
Five stages. When --he-slide is a .vsi file, the HPC driver
runs a VSI → OME-TIFF conversion job first and chains hexenium
after it with --dependency=afterok:. Per-stage outputs
colocate under
<output-root>/<sample>/<sample>_<run-id>/he_registration/ —
the canonical layout that xenium-preprocess-pipeline peers with:
H&E slide hexenium pipeline per-run outputs
(<output-root>/<sample>/
┌────────────────┐ ┌───────────────────┐ <sample>_<run-id>/
│ .vsi or │───────▶│ he_preprocess │──┐ he_registration/)
│ .ome.tif │ │ VSI → OME-TIFF │ │
└────────────────┘ │ (no-op for TIFF) │ │
└───────────────────┘ │
▼
<sample>_he.ome.tif
(location depends on writability;
see he_preprocess stage details)
│
│
Xenium bundle │
┌────────────────┐ ┌───────────────────┐ │
│ output-XETG.. │───────▶│ register │◀─┘
│ + morphology_ │ │ VALIS DAPI ↔ H&E │
│ focus/ │ │ (rigid + non- │
└────────────────┘ │ rigid + micro) │
└───────────────────┘
│
▼
register/<he_job_id>/data/_registrar.pickle
│
│
┌───────────────────┐
│ warp │
│ Xenium cell + │
│ nucleus polygons │
│ → H&E pixel space │
└───────────────────┘
│
▼
warp/<he_job_id>/he_cell_seg.parquet
warp/<he_job_id>/he_nucleus_seg.parquet
│
xenium-ranger │
┌────────────────┐ ┌───────────────────┐
│ xenium_ranger │───────▶│ celltype │
│ .h5ad │ │ read .obs[<col>] │
│ .obs[<col>] = │ │ direct on xenium │
│ celltype label│ │ cells │
└────────────────┘ └───────────────────┘
│
▼
celltyped/<he_job_id>/
<sample>_cells_analysis.geojson
<sample>_nuclei_analysis.geojson
<sample>_cells_qupath.geojson
<sample>_nuclei_qupath.geojson
<sample>_celltyped_wholeslide.parquet
│
▼
┌───────────────────┐
│ viz │
│ H&E overlay PNG │
│ + summary.html │
└───────────────────┘
│
▼
viz/<he_job_id>/<sample>_overlay.png
(only on --set-default-on-success or
hexenium set-default-run:)
output/summary.html
output/{register,warp,celltyped,viz} [symlinks]
Hexenium accepts two H&E input types.
OME-TIFF / TIFF (.ome.tif, .ome.tiff, .tif) — used
as-is. The he_preprocess stage is a no-op.
Olympus .vsi — the HPC driver
(submit_he_registration.sh) auto-converts to OME-TIFF via
BioFormats before running hexenium. Conversion runs as a
separate Slurm job; hexenium waits on
--dependency=afterok:<conv_jobid>. Reruns reuse the
converted file. See HPC usage — VSI
inputs
for install requirements and options.
Every stage writes into its own <he_job_id> shard. Once a
run is promoted (see below), the
output/{register,warp,celltyped,viz} symlinks point at
that run so output/register/ etc. show one canonical run
for browsing.
Auto-promote on success. Add --set-default-on-success
to hexenium run. When the run completes cleanly, the
symlinks are repointed at its fresh <he_job_id>.
Compare N registrations, then promote manually. Run
hexenium N times without --set-default-on-success, review
each register/<he_job_id>/ shard, then promote the winner:
hexenium set-default-run \
--output-root <output_root> \
--sample-id <sample> \
--run-id <upstream_run_id> \
--register-run-id <winning_he_job_id>hexenium set-default-run --help lists the per-stage flags
(--register-run-id, --warp-run-id, --celltyped-run-id,
--viz-run-id) — you can retarget any subset independently.
The celltype stage assigns a cell-type label to every
warped Xenium cell. The default (ranger-direct) reads
labels from --xenium-h5ad's .obs["celltype"]; the
legacy path (--proseg-purified-h5ad, passed in addition
to --xenium-h5ad) does spatial NN-mapping from a proseg
h5ad instead. Missing / empty column, or neither h5ad flag
set → unlabeled fallback.
See the celltype stage docs
for source-mode details, --celltype-col overrides,
column-resolution precedence, and the full set of knobs.
hexenium targets Python 3.10+ (validated on 3.11). It
depends on VALIS, HEST, and their Java/BioFormats bridge
(jpype1, openjdk=11).
Reproducing the working env requires a two-step install:
- A conda-side solve for the system libraries and scientific-Python core.
- A
--no-depspip layer forhest/valis-wsi/valis-hestand their transitive stack.
Both steps are captured in the order they should be run in
environments/heRegistration.yml and
environments/heRegistration-requirements.txt.
--no-deps on the pip step is essential and must live
on the CLI. See
docs/install.md
for the resolver-conflict details.
# 1. Clone the repo
git clone https://github.com/settylab/heRegistration.git
cd heRegistration
# 2. Create the conda env from the pinned spec.
# `heRegistration` is just a readable convention — the sbatch
# wrapper activates envs by resolved absolute prefix (step 8 in
# docs/install.md), not by name.
# scripts/create-env.sh wraps any `micromamba` env-creation call; if
# you set MAMBA_ROOT_PREFIX for an isolated install it also keeps
# the package cache isolated (see docs/install.md § 2).
scripts/create-env.sh env create -n heRegistration -f environments/heRegistration.yml
# or: conda env create -n heRegistration -f environments/heRegistration.yml
# 3. Activate (needs `micromamba shell hook` sourced first — see
# docs/install.md § Prerequisites)
micromamba activate heRegistration
# or: conda activate heRegistration
# 4. Install the pinned pip layer with `--no-deps`.
# Ships hest v1.2.0, valis-wsi 1.1.0, valis_hest 0.0.2, and
# their supporting stack. `--no-deps` is required — see the
# callout below. scripts/pip-install.sh wraps `pip install`; if
# you set MAMBA_ROOT_PREFIX (step 2) it also keeps pip's HTTP +
# wheel cache isolated (see docs/install.md § 4).
scripts/pip-install.sh --no-deps -r environments/heRegistration-requirements.txt
# 5. Install hexenium itself. `--no-deps` here prevents pip
# from re-resolving pyproject.toml's deps and clobbering
# the pinned layer from step 4.
scripts/pip-install.sh --no-deps .
# 6. Sanity-check the install.
python -c "import hest, valis_hest, dask, openslide; print('OK')"
hexenium --version
hexenium run --help
# 7. (Optional — only if you have .vsi H&E input.)
# Download the Glencoe release zips so the pipeline can
# auto-convert VSI to OME-TIFF via
# submit_he_registration.sh (tested versions:
# bioformats2raw 0.12.1 + raw2ometiff 0.9.0):
mkdir -p $HOME/opt/bftools
cd $HOME/opt/bftools
wget https://github.com/glencoesoftware/bioformats2raw/releases/download/v0.12.1/bioformats2raw-0.12.1.zip
wget https://github.com/glencoesoftware/raw2ometiff/releases/download/v0.9.0/raw2ometiff-0.9.0.zip
unzip bioformats2raw-0.12.1.zip
unzip raw2ometiff-0.9.0.zipIf any verification line in step 6 errors, jump to Troubleshooting below.
8. REQUIRED before any sbatch submission — steps 1-7 only cover interactive use. Run this in any shell, even a brand new one (it does not depend on step 7's shell state — see the warning below):
micromamba env list # find heRegistration's absolute prefix
scripts/write-env-config.sh --env-prefix /path/to/micromamba/envs/heRegistration
scripts/env-preflight.sh # verify it resolves end-to-endIf you did step 7 for VSI support, pass those two paths again
explicitly — do not reuse $BFTOOLS_ROOT/$LIBBLOSC_DIR as bare
shell variables here: they only exist in the shell session that ran
step 7, and if that step was skipped (no .vsi input — the common
case) or you're in a fresh terminal, both are unset. An unset,
unquoted $VAR vanishes from the command line entirely rather than
expanding to an empty string, which silently shifts every argument
after it — write-env-config.sh then reports a confusing "does not
exist" error for a path you never intended to pass. Use the literal
paths from step 7 instead:
scripts/write-env-config.sh \
--env-prefix /path/to/micromamba/envs/heRegistration \
--bftools-root $HOME/opt/bftools \
--libblosc-dir $HOME/micromamba/envs/blosc/lib
scripts/env-preflight.shIf you don't already have a conda env with
conda-forge::blosc installed (needed for
LIBBLOSC_DIR), docs/install.md has
a minimal blosc-only env recipe under
### VSI-input prerequisites.
Step 8 is not optional if you plan to submit anything via
scripts/submit_he_registration.sh — see
docs/install.md
for why skipping it produces a submission that looks successful and
fails later, silently, inside the job.
For depth on installation edge cases, see
docs/install.md:
- Why
--no-depsis mandatory — the pandas / pyvips / scikit-image over-cap invalis-wsi 1.1's declared metadata + why the flag must live on the CLI. - Manual install (advanced) — reproduce the working env without the yml.
- Troubleshooting —
the seven most common install-time failures + fixes
(
ResolutionImpossible,ModuleNotFoundError, VIPS warnings,openslide-binsdist, etc.). - Env drift & recovery
—
--force-reinstallgotchas around the pinned numpy / xarray / fastcluster / opencv-contrib layer. - Configure Slurm env activation
— the required post-install step (
write-env-config.sh+env-preflight.sh) before anysbatchsubmission works.
A bare hexenium run … runs the full pipeline under its defaults
(--mode full_with_micro, --use-he-deconvolution true,
--viz-render-boundaries nucleus). The most common shape is
integrated-by-run-id — a single command from an upstream
xenium-preprocess output tree:
micromamba activate heRegistration
hexenium run \
--sample-id SAMPLE1 \
--run-id demo_v1 \
--output-root /data/xenium_runs \
--he-slide /data/SAMPLE1/HE/SAMPLE1_he.ome.tif \
--xenium-bundle /data/SAMPLE1/xenium/output-XETG.../For a standalone run (no upstream xenium-preprocess tree), point at
the celltype source explicitly — pass --xenium-h5ad for the
ranger-direct default:
hexenium run \
--sample-id SAMPLE1 \
--he-slide /data/SAMPLE1/HE/SAMPLE1_he.ome.tif \
--xenium-bundle /data/SAMPLE1/xenium/output-XETG.../ \
--dapi-path /data/SAMPLE1/xenium/output-XETG.../morphology_focus/ch0000_dapi.ome.tif \
--xenium-h5ad /data/SAMPLE1/spatial_adata/SAMPLE1_xenium_ranger.h5ad \
--output-root /data/hexenium_runsFor the legacy proseg-NN mapping instead, add
--proseg-purified-h5ad /data/SAMPLE1/spatial_adata/SAMPLE1_proseg_purified.h5ad
to the invocation above. See celltype — assign cell-type
labels to warped polygons
for when to reach for it.
--dapi-path is only needed for multichannel Xenium bundles where the
DAPI lives at morphology_focus/ch0000_dapi.ome.tif instead of the
standard morphology_focus/morphology_focus_0000.ome.tif.
Common per-run overrides:
--viz-render-boundaries both— draw cell polygons and nucleus outlines together.--use-he-deconvolution false— faint-hematoxylin samples where deconvolution hurts alignment.--mode rigid_only/--mode rigid_nonrigid— diagnostic sweeps.--stages register warp celltype viz— restrict which stages run (he_preprocessis a no-op for OME-TIFF input; skip explicitly when the input is a.vsiyou don't want re-converted).--force-rerun— remove all sentinels and redo every requested stage.--force-preprocess— force VSI → OME-TIFF re-conversion only.
Three ways to point hexenium at a sample. Pick the one that matches how you're driving the pipeline; the on-disk output layout under the top-level run dir is identical in all three modes.
Drive with --sample-id + --he-slide + --xenium-bundle +
--output-root. Outputs land at <output_root>/<sample_id>/.
If the celltype stage is in --stages, pass --xenium-h5ad
so hexenium can read labels directly off its .obs[<col>]
(the ranger-direct default). Add --proseg-purified-h5ad
only if you want to opt in to the legacy proseg-NN mapping
instead. Neither flag is enforced — omitting both doesn't error,
it just labels every cell unlabeled (with a log line), so a
typo'd or missing path here fails silently rather than loudly.
Add --run-id <upstream_run_id> on top of the standalone
set. Hexenium derives the xenium h5ad from the upstream
xenium-preprocess layout at
<output-root>/<sample>/<sample>_<run-id>/spatial_adata/<sample>_xenium_ranger.h5ad.
All H&E outputs colocate under
<output-root>/<sample>/<sample>_<run-id>/he_registration/.
The celltype stage reads its labels directly from this
ranger h5ad's .obs["celltype"] — no proseg lookup by
default. Pass --proseg-purified-h5ad <path> explicitly to
switch to the legacy proseg-NN code path.
Pass --xenium-h5ad <path> directly. Sample identity is
read from .uns['sample_id'] and .uns['run_id'] on that
h5ad. Outputs colocate under <xenium_run_dir>/he_registration/.
Fails LOUD if .uns identity is missing or disagrees with a
passed --sample-id.
Celltype labels still come from this h5ad's
.obs["celltype"]. Same ranger-direct default as mode 2;
same legacy opt-in via explicit --proseg-purified-h5ad.
The subsections below cover user-facing behavior per stage.
For algorithm details and library-level implementation
(OpenSlide / tifffile writer settings, Dask worker + JVM
lifecycle, Shapely polygon cleanup, palette assignment),
see docs/methods.md.
If --he-slide points to an Olympus SlideScanner .vsi,
this stage converts it to a pyramidal OME-TIFF matching
10x's Xenium Explorer specification. If the input is
already an OME-TIFF, the stage returns immediately and
downstream stages consume the input as-is.
The converted OME-TIFF is written next to the source VSI
as <vsi_dir>/<vsi_stem>.ome.tif when that directory is
writable. This lets a single conversion be reused across
every pipeline invocation for that sample. When the source
dir is read-only, the pipeline falls back to
<output_root_he>/converted/<sample_id>_he.ome.tif. Re-runs
auto-detect either location and skip re-conversion.
--force-preprocess forces re-conversion of the VSI without
touching downstream stages.
Registers the H&E to the DAPI morphology image using VALIS via
valis_hest. The default --mode full_with_micro composes
three transforms into one registrar:
- A rigid initial solve.
- A non-rigid B-spline solve at
max_processed_image_dim_px = 1500. - A micro-registration refinement at
max_non_rigid_registration_dim_px = 10000.
This matches stock HEST register_dapi_he(micro_reg=True).
Two diagnostic modes are available for sweeps: rigid_only
(fastest, rigid only) and rigid_nonrigid (rigid + non-rigid,
no micro).
Macenko-style H&E deconvolution runs before feature detection
by default. On faint-hematoxylin samples it can degrade
alignment. Override with --use-he-deconvolution false when a
run shows that failure mode.
Reflection checking is on by default so mirrored slides are
caught cheaply. To sidestep a hardcoded
he_key='aligned_fullres_HE' in valis_hest, hexenium
symlinks the user's H&E to that canonical name inside the
per-sample workdir before invoking VALIS.
Writes:
register/<he_job_id>/data/_registrar.pickle — the composed rigid ×
non-rigid × micro transforms — plus VALIS's per-stage image dumps under
sibling rigid_registration/, non_rigid_registration/,
micro_registration/, etc., and a cross-run
register/manifest.yaml symlink pointing at the
current-default registrar.
Applies the VALIS registrar to the Xenium
cell_boundaries.parquet and nucleus_boundaries.parquet
(and, optionally, transcripts.parquet) via HEST's
warp_and_save_xenium_objects.
Outputs are WKB-encoded GeoPandas parquets in H&E pixel
space. Each row's Xenium cell_id is preserved for
downstream joins.
Writes: warp/<he_job_id>/he_cell_seg.{parquet,geojson} and
warp/<he_job_id>/he_nucleus_seg.{parquet,geojson} (and
he_transcripts.parquet when --include-transcripts is set). Parquet
for programmatic downstream use, GeoJSON for viewers (QuPath,
GeoJSON.io, napari-geojson, …).
Assigns a cell-type label to every warped Xenium cell. Viz
uses these labels to colour the overlay; downstream analysis
uses them to filter by class. Two source modes are available.
The default now reads labels DIRECTLY off the query h5ad
(post rctd-split celltype_writeback), so no NN mapping is
needed in the normal xenium-preprocess → hexenium flow.
Default: ranger-direct. With just --xenium-h5ad (or
--run-id auto-deriving it), hexenium reads
xenium_ranger.h5ad's .obs[<celltype_col>] for every cell.
Default column: celltype (matches
packages/rctd-split/config/default.yaml's
celltype_writeback.celltype_col). Pass --celltype-col <name> to
override for a custom-built ranger h5ad. --celltype-col auto
falls through the historic precedence
celltype > first_type > primary_cell_type > celltype_updated.
No NN mapping in this mode — the label comes straight off
the query h5ad. Hexenium's overlay reads the same
.obs["celltype"] column that the xenium-preprocess
summary reports read, so labels match unless the column
has been rewritten between the two invocations.
Legacy: proseg-NN. Pass --proseg-purified-h5ad <path>
explicitly (in addition to --xenium-h5ad) to switch to the
original NN-mapping path. For every xenium cell centroid,
hexenium looks up the nearest proseg-purified cell and inherits
its celltype label. This mode is kept for two cases:
- Standalone-mode users who don't run xenium-preprocess's
rctd-split celltype_writeback. - Users who want to override the ranger labels with a fresh NN fit against a custom proseg reference.
Missing / all-NaN column → unlabeled. When the requested
column doesn't exist, or exists but is entirely NaN / empty, the
stage logs a WARN and labels every row unlabeled. Viz renders
unlabeled as grey #888888. Per-row NaN / empty values also
fall to unlabeled (only the affected rows). No crash.
Auto-detection knobs:
- celltype column (
--celltype-col, defaultcelltype) — set toautofor precedence walk; set to a literal column name to force a choice; unmatched →unlabeledfallback. - Xenium id column (
--id-col, defaultauto) — every candidate is shape-checked against the Xenium UUID regex (^[a-z]{8}-\d+$) before use, so it can't silently pick Proseg's int64 index masquerading ascell_id. Special value__index__reads from.obs.index. - spatial coords (proseg-NN mode only) — checks
.obsm['spatial']first, then falls back through.obs['centroid_x'/'centroid_y'],.obs['x_centroid'/'y_centroid'],.obs['x'/'y']. Override withcelltype.{proseg,xenium}_{x,y}_colin the config YAML.
After the label join, polygon cleanup drops empty /
non-Polygon geometries and geometries smaller than
area_threshold_px (default 20 sq px). Nuclei without a
label optionally inherit their sibling cell's label
(nuclei_inherit_classification: true).
Writes under celltyped/<he_job_id>/:
<sample>_cells_analysis.geojson— cells, raw-float precision.<sample>_nuclei_analysis.geojson— nuclei, raw-float precision.<sample>_cells_qupath.geojson— cells, rounded (default 2 decimals)- polygon-safety cleanup so QuPath's drag-and-drop importer accepts them without invalid-polygon errors.
<sample>_nuclei_qupath.geojson— nuclei, ditto.<sample>_celltyped_wholeslide.parquet— merged, analysis-precision combined table feeding the viz stage.<sample>_celltype_annotation.parquet— per-cell label table (columns:xenium_cell_id,group,nn_distance), written by both the ranger-direct and legacy proseg-NN branches. In ranger-direct modenn_distanceisNaN(no NN fit occurred); in proseg-NN mode it carries the L2 distance to the winning proseg centroid.
Draws warped boundaries on a downsampled H&E thumbnail.
The default (viz.render_boundaries = nucleus) draws only
nucleus outlines — the cleanest read of registration
quality against H&E's hematoxylin-stained nuclei. Two
alternatives are exposed via --viz-render-boundaries:
cell draws cell polygons (semi-transparent fill) only,
and both draws nucleus outlines on top of the cell
polygons.
Cell-type labels drive a qualitative palette (tab20 by
default). Override colors in viz.classification_palette.
Writes: viz/<he_job_id>/<sample>_overlay.png at
viz.dpi (default 200 DPI), with a legend of the labels
present in the data.
Every mode writes into a top-level H&E-reg output dir — call it
<output_root_he> — with the same shape. Integrated modes place that dir
under an upstream xenium run dir; standalone mode places it under
<output_root>/<sample_id>/.
<output_root_he>/ ← integrated: <run_dir>/he_registration/
│ standalone: <output_root>/<sample_id>/
├── converted/
│ └── <sample>_he.ome.tif ← stage 0 fallback (only when
│ <vsi_dir> was read-only; the
│ default write is next to the VSI)
├── register/
│ ├── manifest.yaml ← symlink to current-default registrar
│ └── <he_job_id>/
│ ├── data/_registrar.pickle ← VALIS composed transform (sentinel)
│ ├── rigid_registration/
│ ├── non_rigid_registration/
│ ├── micro_registration/
│ ├── deformation_fields/
│ ├── masks/
│ ├── overlaps/
│ ├── processed/
│ └── _workdir/ ← canonical-name H&E symlink
├── warp/
│ └── <he_job_id>/
│ ├── he_cell_seg.parquet (+ .geojson)
│ ├── he_nucleus_seg.parquet (+ .geojson)
│ └── he_transcripts.parquet ← only with --include-transcripts
├── celltyped/
│ └── <he_job_id>/
│ ├── <sample>_cells_analysis.geojson ← per-cell polygons, float precision
│ ├── <sample>_nuclei_analysis.geojson
│ ├── <sample>_cells_qupath.geojson ← rounded + polygon-safe
│ ├── <sample>_nuclei_qupath.geojson
│ ├── <sample>_celltyped_wholeslide.parquet ← merged, feeds viz
│ └── <sample>_celltype_annotation.parquet ← label table (both modes; NaN nn_distance in ranger-direct)
└── viz/
└── <he_job_id>/
└── <sample>_overlay.png ← H&E + warped boundaries
<he_job_id> precedence (highest wins):
--he-job-id— explicit CLI override.$SLURM_JOB_ID— set automatically undersbatch/srun.YYYYMMDDTHHMMSS— local-time timestamp (interactive fallback, 1-second resolution).
Logs — per-run logs land at:
- Integrated modes →
<run_dir>/logs/logs_heRegistration/<sample>_<he_job_id>_<stages>/(colocated with any workflow-driver step logs under<run_dir>/logs/). - Standalone →
<output_root_he>/logs/<sample>_<he_job_id>_<stages>/.
The <stages> suffix (e.g. all, register_warp) records which pipeline
stages ran, so re-runs at the same jobid don't clobber each other's logs.
Each per-run logs dir carries the Slurm .out/.err and a
resolved_config.yaml snapshot with every parameter that shaped the run
(defaults + user YAML + CLI overrides, merged).
Every knob is defined in src/hexenium/_defaults/default.yaml
with a comment explaining its effect. Override any subset with
a user YAML (hexenium run --config user.yaml …) or
spot-override on the CLI. CLI overrides win over user YAML.
User YAML wins over the default.
The knobs most runs actually touch:
| Key | Default | Meaning |
|---|---|---|
--mode / registration.mode |
full_with_micro |
rigid_only, rigid_nonrigid, or full_with_micro. rigid_only_micro is invalid — see below. |
--use-he-deconvolution / registration.use_he_deconvolution |
true |
Macenko-style H&E stain deconvolution before registration. Override with false on faint-hematoxylin samples. |
--dapi-path / dapi_path |
derived from xenium_bundle |
Pass explicitly for multichannel bundles (morphology_focus/ch0000_dapi.ome.tif). |
--proseg-purified-h5ad / proseg_purified_h5ad |
(unset) | Legacy proseg-NN mode opt-in. Explicit, and only in addition to --xenium-h5ad → celltype uses the historic NN-mapping code path. Set alone (without --xenium-h5ad) it does nothing — falls through to the same unlabeled fallback as neither flag being set. Not needed in the standard xenium-preprocess → hexenium flow (default ranger-direct). |
--xenium-h5ad / xenium_h5ad |
integrated modes: auto; standalone: (unset) |
Query xenium h5ad. Default source for celltype labels (read directly from .obs[<celltype_col>]). Also the identity source in integrated-by-h5ad mode. Not enforced in standalone mode — if celltype is in --stages and neither this nor --proseg-purified-h5ad is set, every cell is labeled unlabeled (logged, not an error). |
--celltype-col / celltype.celltype_col |
celltype |
Column on the ranger h5ad (or proseg h5ad in legacy mode) that carries per-cell labels. auto walks the precedence celltype > first_type > primary_cell_type > celltype_updated. Missing / all-NaN → unlabeled fallback. |
--id-col / celltype.id_col |
auto |
Force a specific xenium-side id column. __index__ reads from .obs.index. |
--nn-k / celltype.nn_k |
1 |
Neighbours per query in the proseg → xenium NN mapping. |
celltype.nuclei_inherit_classification |
true |
Unlabelled nuclei inherit their sibling cell's label. |
celltype.area_threshold_px |
20 |
Drop polygons smaller than this (in H&E pixels²) as segmentation noise. |
viz.thumbnail_max_dim |
4096 |
Downsample H&E to this max dimension for the overlay. |
viz.dpi |
200 |
Overlay PNG DPI. |
--viz-render-boundaries / viz.render_boundaries |
nucleus |
Which boundaries to draw: nucleus (outlines only), cell (polygons only), or both. |
--include-transcripts / warp.include_transcripts |
false |
Also warp transcripts.parquet. Adds hours. |
The register stage's numeric parameters live under
registration: and parameters: in
src/hexenium/_defaults/default.yaml. Every one is
CLI-overridable (dash-cased flag) and YAML-overridable (dot
path).
| Key | Default | Meaning |
|---|---|---|
--check-for-reflections / registration.check_for_reflections |
true |
Have VALIS test mirrored/flipped candidates during rigid init. Cheap; catches mounted-backwards slides. |
--create-masks / registration.create_masks |
false |
Auto-mask non-tissue regions before registration. Off by default; auto-mask can over-mask small tissue islands. |
--align-to-reference / registration.align_to_reference |
true |
Use H&E as the reference image (rather than DAPI). |
--max-image-dim-px / parameters.max_image_dim_px |
1500 |
Cap on the SAVED image pyramid (VALIS memory guard). Constraint: max_image_dim_px >= max_processed_image_dim_px; if lower, valis_hest silently auto-bumps. |
--max-processed-image-dim-px / parameters.max_processed_image_dim_px |
1500 |
Long-edge pixel cap for feature detection during rigid + first-pass non-rigid. Higher = more accurate + more memory. |
--max-non-rigid-registration-dim-px / parameters.max_non_rigid_registration_dim_px |
10000 |
Long-edge pixel cap for the non-rigid B-spline solve (rigid_nonrigid and full_with_micro). |
parameters.micro_rigid_registrar_cls |
null |
Override VALIS's MicroRigidRegistrar class. Advanced; leave null for the stock class. |
parameters.micro_rigid_registrar_params |
{} |
Extra kwargs forwarded to the micro-rigid registrar. |
--run-name / parameters.name |
null (auto) |
Registrar run name recorded in the manifest for provenance (the on-disk folder is always <he_job_id>). |
Note on rigid_only_micro: removed — register_micro requires
bk_dxdy from an initial non-rigid pass; combining rigid-only init with
micro raises TypeError: 'NoneType' object is not subscriptable in
valis_hest.registration.py:register_micro. Use rigid_only,
rigid_nonrigid, or full_with_micro.
See src/hexenium/_defaults/default.yaml for the full list.
That file covers Dask worker settings, JVM memory, palette
overrides, and per-stage rounding for QuPath vs. analysis
outputs.
- Sentinel-file resume. Each stage checks for its sentinel
output(s) before starting. If present and
--force-rerunisn't set, the stage is skipped. Its outputs are threaded through to the next stage. - Per-invocation sharding.
register/,warp/,celltyped/,viz/all shard under<he_job_id>/. A new run under a new Slurm job id never clobbers a prior run's outputs.celltypeandvizreading an earlier stage's outputs default to the same<he_job_id>. Point them at a prior run with--warp-run-id/--celltype-run-id. - Config snapshot. Every run writes
resolved_config.yamlinto its logs dir (see Output tree). This is the exact merged defaults + user YAML + CLI overrides that shaped the run. Integrated modes also merge ahe_registration:key into the xenium run dir's sharedresolved_config.yamlfor cross-pipeline provenance. - VSI conversion is per-sample, not per-run. The
converted OME-TIFF lives next to the source VSI (or the
pipeline-tree fallback). It is reused by every invocation
for that sample.
--force-preprocesstriggers a re-conversion without touching downstream sentinels.
The bundled wrapper scripts/submit_he_registration.sh is
designed to be called directly (no sbatch prefix). It
self-submits under sbatch and routes its own .out/.err
under the run folder's logs dir.
Under the hood it does two-phase sbatch --hold → mkdir
per-job-id dir → release. This meets Slurm's
log-dir-must-exist-at-job-start requirement.
#SBATCH header in the wrapper (tune to your cluster):
--partition=campus-new --time=2-00:00:00 --mem=196G --cpus-per-task=12
Call:
./scripts/submit_he_registration.sh \
--sample-id SAMPLE1 \
--run-id demo_v1 \
--output-root /data/xenium_runs \
--he-slide /data/SAMPLE1/HE/SAMPLE1_he.ome.tif \
--xenium-bundle /data/SAMPLE1/xenium/output-XETG.../ \
[--stages register warp celltype viz] \
[--force-rerun]OUTPUT_ROOT=/data/xenium_runs in your env is a fallback for
--output-root.
Env activation is by resolved absolute prefix, never by name. The
wrapper does not source ~/.bashrc and has no
micromamba→mamba→conda fallback chain — an earlier version did, and
that $HOME-relative activation chain is what caused real jobs to hang
with 0-byte output (see scripts/lib/env_config.sh's header comment).
It always activates the exact prefix recorded in
scripts/env.local.conf, written once by
scripts/write-env-config.sh — see docs/install.md step
8.
To point the wrapper at a different env, re-run scripts/write-env-config.sh --env-prefix /path/to/other/env — there is no per-invocation flag for
this anymore. --env-name / ENV_NAME are still parsed (so old
invocations don't hit an "unknown flag" error) but now only emit a WARN
and change nothing.
A misconfigured environment does not block submission — it fails
later, silently, inside the job. submit_he_registration.sh's
launcher branch (scripts/submit_he_registration.sh:101-333 — the part
that runs when you invoke the script directly) parses flags and calls
sbatch/scontrol release; it never sources
scripts/lib/env_config.sh. That only happens in the under-sbatch
branch (scripts/submit_he_registration.sh:388-390), once the job
actually starts running on a compute node. So if scripts/env.local.conf
is missing or stale, ./scripts/submit_he_registration.sh ... still
prints a normal Submitted batch job NNNN and exits 0 — the failure
only shows up afterward in that job's own slurm-<jobid>.err. If
you're queueing a run and stepping away, run scripts/env-preflight.sh
first so a bad config fails in your terminal instead.
The wrapper builds the logs dir under either
<output_root>/<sample>/<sample>_<run_id>/logs/logs_heRegistration/
(integrated — submit_he_registration.sh:302) or
<output_root>/<sample>/logs/ (standalone —
submit_he_registration.sh:305), with a leaf directory
<sample>_<jobid>_<stages> (:307, :324) — so re-runs at the same
job id don't clobber each other's logs. Slurm's own .out/.err land
inside that leaf as slurm-<jobid>.out / slurm-<jobid>.err
(:313-314). Concretely, for an integrated run:
<output_root>/<sample>/<sample>_<run_id>/logs/logs_heRegistration/<sample>_<jobid>_<stages>/slurm-<jobid>.{out,err}
Integrated-by-h5ad is the one mode this wrapper doesn't specially
route. The launcher branch has no --xenium-h5ad handling at all,
and still hard-requires --output-root + --sample-id to submit
(:128-137) even though hexenium run --xenium-h5ad ... itself needs
neither — identity and output location come from the h5ad's .uns (see
Invocation modes above). In practice this mode is
more often driven with a direct sbatch/sbatch --wrap call instead of
this wrapper (see "Direct sbatch --wrap" below); in that case Slurm's
own default applies — slurm-<jobid>.out/.err land in the submitting
shell's cwd, wherever that is (commonly the xenium run directory
itself, if that's what an upstream pipeline cds into before
submitting). If you do drive h5ad-mode through
submit_he_registration.sh, you still need --output-root/
--sample-id/--run-id to satisfy the launcher, and the logs land per
the integrated/standalone rule above — not automatically under the
h5ad's own xenium run directory.
Direct sbatch --wrap is also fine when you want to override the
resource envelope per-run:
sbatch --partition=campus-new --time=2-00:00:00 --mem=196G --cpus-per-task=12 \
--job-name=hexenium-SAMPLE1 \
--wrap "micromamba activate heRegistration && hexenium run \
--sample-id SAMPLE1 \
--run-id demo_v1 \
--output-root /data/xenium_runs \
--he-slide /data/SAMPLE1/HE/SAMPLE1_he.ome.tif \
--xenium-bundle /data/SAMPLE1/xenium/output-XETG.../"Slurm's own slurm-<jobid>.out in cwd captures everything;
PYTHONUNBUFFERED=1 (which hexenium sets internally) keeps stdout
line-buffered.
Resource sizing. Under the default --mode full_with_micro, a typical whole-slide run takes 2–2.5 h
wall-clock. Breakdown:
register(rigid + non-rigid): 30–60 min.register_micro: 30 min – 2 h, depending on tissue size.- Warp + celltype + viz: another 30–60 min.
The 2-day --time in the wrapper is the honest ceiling for
full_with_micro on large samples. A --mode rigid_only
diagnostic sweep completes in well under an hour.
196 GB memory is sized for the non-rigid + micro solve at
max_non_rigid_registration_dim_px = 10000 plus the Dask
warp cluster. Drop to 128 GB only if you also drop that cap.
submit_he_registration.sh handles VSI inputs end-to-end
without any extra flags. Pass --he-slide /path/to/foo.vsi
and the launcher:
- Detects the
.vsiextension. - Submits a
bioformats2raw+raw2ometiffconversion job to Slurm as its own record. - Rewrites
--he-slideinternally to the converted OME-TIFF at<run_dir>/he_registration/converted/<sample>_he.ome.tif(the HPC driver always writes to this pipeline-tree location, whether or not the source VSI's directory is writable; interactivehexenium runon a.vsiprefers next-to-source — see thehe_preprocessstage docs). - Submits the hexenium job with
--dependency=afterok:<conv_jobid>so it runs only when the conversion finishes cleanly.
Both jobs are independent slurm records. A hexenium rerun
(different --mode, --warp-run-id, etc.) reuses the
converted OME-TIFF without redoing the ~15-25-min BioFormats
step. Each job's slurm-<jobid>.out/err lives at a stable
log path.
Idempotency. The converted file is reused on subsequent
invocations unless you pass --force-preprocess (analogous to
--force-rerun for the hexenium stages themselves).
Series auto-detect. Olympus VSI files typically carry 4
series (label / overview / 40x brightfield / macro thumbnail).
The launcher auto-picks the largest-on-disk series after
raw2ometiff --split — a robust proxy for pixel count that
avoids the s2-is-always-the-40x assumption. Override with
--vsi-series <N> if the auto-pick ever gets it wrong for
your data.
Prerequisites — see Step 7 of Recommended
install above for the Glencoe-zip
setup + BFTOOLS_ROOT / LIBBLOSC_DIR env vars. Full
deep-dive (Java 11 note, minimal blosc-only env
recipe, release-page links) in
docs/install.md.
Example — a VSI-input invocation looks identical to any
other input, just with a .vsi path:
./scripts/submit_he_registration.sh \
--sample-id SAMPLE1 \
--run-id demo_v1 \
--output-root /data/xenium_runs \
--he-slide /data/SAMPLE1/HE/SAMPLE1.vsi \
--xenium-bundle /data/SAMPLE1/xenium/output-XETG.../ \
--dapi-path /data/SAMPLE1/xenium/output-XETG.../morphology_focus/morphology_focus_0000.ome.tifOutput:
[submit] VSI input detected — submitting conversion job:
Submitted batch job 12344 (conversion)
Submitted batch job 12345 (logs: SAMPLE1_12345_all)
waiting on conversion job 12344 (afterok)
On a rerun with the same --run-id, the launcher notices the
canonical OME-TIFF is already present. It prints
[submit] skipping VSI conversion. Only the hexenium job is
submitted — no --dependency wait, starts immediately.
Two auxiliary launchers cover the rare cases the unified flow above doesn't handle cleanly:
scripts/submit_vsi_to_ometiff.sh— VSI → OME-TIFF only. Use to prime a shared cache of converted files outside a specific--run-id, or to debug conversion parameters (--vsi-series,--max-workers) in isolation. Same install prerequisites as above.scripts/submit_hexenium_from_ometiff.sh— hexenium against an already-converted OME-TIFF at a non-canonical path (e.g. a Trident / QuPath /bfconvertoutput). Thin dispatcher tosubmit_he_registration.sh— forwards every hexenium flag untouched.
Both are called the same way, with --help support and
Submitted batch job <jobid> output routed to a logs/
subdirectory next to their outputs.
The test suite covers:
- CLI parsing and config validation.
- Layout resolution for the three invocation modes.
- Celltyping (auto-detect, NN mapping, polygon cleanup).
- Viz
render_boundariesvariants. - Stage imports.
- An end-to-end pipeline smoke test.
scripts/pip-install.sh -e '.[test]'
pytest tests/Tests are ordinary pytest — no cluster or GPU required. pytest -k layout and friends work for targeted runs. conftest.py in tests/
sets up shared fixtures.
If you use hexenium in a publication, cite via the metadata in
CITATION.cff — GitHub renders that file as a
copy-paste-ready BibTeX / APA / CFF block on the repo landing page.
Methods-section template (fill in the version tag and Zenodo DOI):
Registration of the H&E whole-slide image to the matched Xenium DAPI morphology channel was performed with hexenium v0.2.0 (DOI: ), a Python wrapper around VALIS (Gatenbee et al., 2023) via the
valis_hestadapter and HEST (Jaume et al., 2024) that composes rigid, non-rigid, and micro-registration transforms into a single registrar. Xenium cell and nucleus segmentations were warped into H&E pixel space via HEST'swarp_and_save_xenium_objectsand annotated with per-cell type labels sourced fromxenium_ranger.h5ad's.obs["celltype"](as written by an upstreamrctd-splitcelltype_writebackstep). Annotated boundaries were then rendered as per-slide overlays.
Please also cite VALIS and HEST directly per their upstream requests.
MIT — see LICENSE.
- VALIS — Gatenbee, C. D. et al. VALIS: Virtual Alignment of pathoLogy Image Series. Nature Communications 14, 4502 (2023).
- HEST — Jaume, G. et al. HEST-1k: A Dataset for Spatial
Transcriptomics and Histology Image Analysis. NeurIPS Datasets and
Benchmarks (2024). Code:
mahmoodlab/HEST. - Xenium In Situ platform — 10x Genomics. The Xenium bundle layout
(
morphology_focus/,cell_boundaries.parquet,nucleus_boundaries.parquet,transcripts.parquet) is a 10x-Genomics output specification.
hexenium was adapted from an internal H&E-Xenium registration workflow.