Skip to content

🆕 Add DeepSpot-M model (H&E → virtual spatial transcriptomics) - #1103

Open
KalinNonchev wants to merge 6 commits into
TissueImageAnalytics:developfrom
KalinNonchev:feat/deepspotm-virtual-spatial-transcriptomics
Open

🆕 Add DeepSpot-M model (H&E → virtual spatial transcriptomics)#1103
KalinNonchev wants to merge 6 commits into
TissueImageAnalytics:developfrom
KalinNonchev:feat/deepspotm-virtual-spatial-transcriptomics

Conversation

@KalinNonchev

Copy link
Copy Markdown

What

Adds DeepSpot-M (Nonchev et al., medRxiv 2026) as a ModelABC in
tiatoolbox.models.architecture.deepspotm. DeepSpot-M maps a 224×224 H&E tile to
transcriptome-wide spatial gene expression via a LoRA-adapted Midnight backbone and
a cross-attention gene decoder, so one model predicts a ~19k-gene panel (and
zero-shot genes) from histology.

It runs through the existing DeepFeatureExtractor engine — each output "feature"
column is a predicted gene, so a whole-slide image yields an (n_tiles, n_genes)
expression matrix with matching tile coordinates:

from tiatoolbox.models.architecture.deepspotm import DeepSpotM
from tiatoolbox.models.engine.deep_feature_extractor import DeepFeatureExtractor

# A marker panel keeps the output lean; omit `genes` for the full ~19k-gene panel.
model = DeepSpotM(source="scgpt", genes=["EPCAM", "CD3D", "PTPRC"])
extractor = DeepFeatureExtractor(model=model, batch_size=32)
output = extractor.run(
    ["slide.svs"],
    patch_mode=False,
    patch_input_shape=(224, 224),
    input_resolutions=[{"units": "mpp", "resolution": 0.5}],
    save_dir="deepspotm_output",
)

Why DeepFeatureExtractor

DeepSpot-M is image-in and produces a per-tile vector, so it fits the
feature-extractor contract: forward returns the expression tensor and
infer_batch applies DeepSpot-M's own image transform to the NHWC patches and
returns [features]. A genes=[...] subset computes only those gene queries
(faster, and far fewer columns to store). This mirrors how the foundation tile
encoders (UNI / Virchow / H-optimus via TimmBackbone) are wired as models passed
to the engine.

Licensing / access

  • Weights live in the gated ratschlab/DeepSpotM Hugging Face repo (accept the
    terms and authenticate) — consistent with the existing gated encoders (UNI,
    Virchow, …).
  • License: PolyForm-Noncommercial-1.0.0 (code) + CC-BY-NC-SA-4.0 (weights) —
    research use only.
  • deepspotm is an optional dependency, imported lazily with a clear install
    hint (pip install git+https://github.com/ratschlab/DeepSpotM.git). It is not yet
    on PyPI; happy to add an extras entry once it is.

Testing

  • tests/models/test_arch_deepspotm.py injects a fake deepspotm module, so the
    wrapper (gene subsetting, tile preprocessing, infer_batch shape/contract, and the
    ImportError path) is exercised in CI without gated weights or the optional
    dependency.
  • ruff check and ruff format are clean against the repo config.

Follow-ups (happy to add)

  • An example notebook / docs entry.
  • A dedicated ModelTask.spatial_transcriptomics if the maintainers prefer it over
    the feature-extractor path.

🤖 Generated with Claude Code

DeepSpot-M (Nonchev et al., medRxiv 2026) maps a 224x224 H&E tile to
transcriptome-wide spatial gene expression via a LoRA-adapted Midnight
backbone and a cross-attention gene decoder. It is wrapped as a ModelABC
and runs through the existing DeepFeatureExtractor engine, so a whole-slide
image yields an (n_tiles, n_genes) expression matrix with tile coordinates.
A `genes=[...]` subset computes only those gene queries (faster, lean output).

The `deepspotm` package is an optional, lazily imported dependency; the tests
inject a fake module so CI exercises the wrapper (gene subsetting, tile
preprocessing, infer_batch contract, ImportError path) without the gated
ratschlab/DeepSpotM weights.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@shaneahmed
shaneahmed requested a review from Jiaqi-Lv July 24, 2026 15:15
@codecov

codecov Bot commented Jul 24, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.88%. Comparing base (9a27bbb) to head (b97ca85).

Additional details and impacted files
@@           Coverage Diff            @@
##           develop    #1103   +/-   ##
========================================
  Coverage    99.88%   99.88%           
========================================
  Files           86       87    +1     
  Lines        11670    11710   +40     
  Branches      1533     1537    +4     
========================================
+ Hits         11657    11697   +40     
  Misses           7        7           
  Partials         6        6           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

KalinNonchev pushed a commit to KalinNonchev/tiatoolbox that referenced this pull request Jul 24, 2026
isinstance(batch_data, torch.Tensor) -> .cpu().numpy() branch is
exercised, restoring 100% patch coverage flagged by Codecov on TissueImageAnalytics#1103.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a torch.Tensor input case for infer_batch so the
isinstance(batch_data, torch.Tensor) -> .cpu().numpy() branch is
exercised, restoring 100% patch coverage flagged by Codecov on TissueImageAnalytics#1103.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@KalinNonchev
KalinNonchev force-pushed the feat/deepspotm-virtual-spatial-transcriptomics branch from afe4934 to d566604 Compare July 24, 2026 15:51
@shaneahmed shaneahmed changed the title Add DeepSpot-M model (H&E → virtual spatial transcriptomics) 🆕 Add DeepSpot-M model (H&E → virtual spatial transcriptomics) Jul 24, 2026
@shaneahmed

Copy link
Copy Markdown
Member

Thanks @KalinNonchev Please can you also add a Jupyter Notebook? You can use one of the existing inference pipelines (https://github.com/TissueImageAnalytics/tiatoolbox/tree/develop/examples/inference-pipelines) Jupyter Notebooks as a template.

@shaneahmed shaneahmed added the enhancement New feature or request label Jul 27, 2026
@shaneahmed shaneahmed added this to the Release v2.2.0 milestone Jul 27, 2026
Adds examples/inference-pipelines/deepspotm.ipynb, following the layout of the
existing inference-pipeline notebooks: Colab badge, `remove-cell` setup and
environment cells, device selection, `tmp/` cleanup, and the shared TCGA sample
slide.

The notebook builds a DeepSpotM model restricted to three marker genes, runs it
over the whole slide through DeepFeatureExtractor with an IOPatchPredictorConfig
at 224x224 and 0.5 mpp, reads the (n_tiles, n_genes) matrix back out of the zarr
output, and overlays the predicted expression on the slide thumbnail. It closes
with the `genes=None` full-panel form and the citation and licence terms.

EPCAM, PTPRC and COL1A1 were chosen so each marker reports on a different tissue
compartment, which makes the overlay readable as a sanity check rather than three
similar-looking maps.

Outputs are from a real run: 6,467 tissue tiles scored on one GPU, about 16
minutes end to end including the slide download and the model load.

Also adds the notebook to the Inference Pipelines section of examples/README.md
with a thumbnail at docs/images/deepspotm.png, matching the other entries.
@review-notebook-app

Copy link
Copy Markdown

Check out this pull request on  ReviewNB

See visual diffs & provide feedback on Jupyter Notebooks.


Powered by ReviewNB

@KalinNonchev

Copy link
Copy Markdown
Author

Thanks @shaneahmed, added in 3ba95fb.

examples/inference-pipelines/deepspotm.ipynb, using idars.ipynb as the template: Colab badge, remove-cell setup and environment cells, device selection, tmp/ cleanup, and the same TCGA sample slide the other inference-pipeline notebooks download.

It builds a DeepSpotM restricted to three marker genes, runs it over the WSI through DeepFeatureExtractor with an IOPatchPredictorConfig at 224x224 and 0.5 mpp, reads the (n_tiles, n_genes) matrix back out of the zarr output, and overlays the predicted expression on the slide thumbnail. EPCAM, PTPRC and COL1A1 each report on a different compartment (epithelium, immune infiltrate, stroma), so the overlay reads as a sanity check rather than three similar-looking maps. It closes with the genes=None full-panel form plus the citation and licence terms.

The committed outputs are from a real run rather than a cleared notebook: 6,467 tissue tiles scored on one GPU, roughly 16 minutes end to end including the slide download and the gated-weight load.

I also added the entry to the Inference Pipelines section of examples/README.md with a thumbnail at docs/images/deepspotm.png, matching the format of the other two.

ruff check, ruff format and the notebook-markdown-format pre-commit hook are all clean against the repo config.

Two notes in case they matter for review:

  • The Colab bootstrap cell installs deepspotm from git, since it is not on PyPI yet. I have a PR open on our side to fix the packaging and add a release workflow, so once it is published I am happy to switch that cell to pip install deepspotm and add the extras entry I mentioned in the PR description.
  • Running the notebook needs access to the gated ratschlab/DeepSpotM weights, so it cannot execute unattended in CI. That matches the other gated encoders, but flagging it explicitly since this notebook downloads weights rather than using a TIAToolbox-hosted checkpoint.

`deepspotm` 1.0.0 was published to PyPI, so the optional dependency no longer
needs a git URL.

- setup.py declares `extras_require={"deepspotm": ["deepspotm>=1.0.0"]}`, so
  `pip install tiatoolbox[deepspotm]` pulls the optional model dependency.
- The module docstring and the lazy-import ImportError point at the extra,
  keeping `pip install deepspotm` as a fallback for anyone on a released
  tiatoolbox that predates the extra.
- The notebook's Colab bootstrap installs from PyPI instead of the git URL.

Note that setup.py had no extras_require before; this adds the first one. The
blank line after the module docstring is what ruff-format requires on the
touched file.
@KalinNonchev

Copy link
Copy Markdown
Author

@Jiaqi-Lv just approved your HF request. Please let me know if you need anything else.

@Jiaqi-Lv

Copy link
Copy Markdown
Collaborator

Looks great. I'm reviewing this PR.

@Jiaqi-Lv just approved your HF request. Please let me know if you need anything else.

Thank you! I'll review this soon.

@Jiaqi-Lv

Copy link
Copy Markdown
Collaborator

I've made some minor changes:

  • Switch the demo slide from the large WSI to wsi4_12k_12k.svs.
  • Reorder setup so Hugging Face auth comes before sample download, and note that auth can be skipped when using local weights.
  • Replace scatter-point overlays with a tile-grid heatmap.

@Jiaqi-Lv Jiaqi-Lv left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, works well for me.

@KalinNonchev

Copy link
Copy Markdown
Author

Merge? @Jiaqi-Lv

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants