Docker one-command WSI TIL inference + smoke test#3
Conversation
Provide a single end-to-end entry point (ectil/inference.py) that takes a WSI and ECTIL classifier weights and runs mask -> tiling -> RetCCL -> ECTIL, auto-loading RetCCL. Reuses the existing DLUP tiling/FESI mask, RetCCL encoder, and MeanMIL + GatedAttention components so results match extract.py + eval.py. Per slide it writes the final TIL score (tils_score.json), per-tile TIL and attention scores (tile_predictions.csv), the generated feature dataset (features.h5), thumbnail/mask/mask-overlay images, and attention/TIL heatmaps. Add a Dockerfile (conda-based, mirrors README install), .dockerignore, an example run script, and README usage. Weights are mounted at runtime. https://claude.ai/code/session_018mX7wRvMnm23m4uf44Upq8
--wsi now accepts a directory of slides (recursively globbed by extension, including .mrxs, whose companion data directory is never matched). Slides that fail are skipped and recorded rather than aborting the run. RetCCL and the ECTIL classifier are loaded once and reused across all slides. Each run writes a timestamped <output>/<run_name>/ directory (override with --run-name) containing config.json, an aggregate tils_scores.csv (one row per slide, written incrementally), and a per-slide subdir. Per-slide tils_score.json now embeds the full run config. https://claude.ai/code/session_018mX7wRvMnm23m4uf44Upq8
The editable install failed inside the image because pip 23.3.2 was paired with a setuptools whose _core_metadata calls canonicalize_version with strip_trailing_zero, a kwarg the resolved packaging lacked. Pin setuptools/wheel/packaging as a matched set. Also relax python_requires from ==3.10.9 to >=3.10,<3.11 so conda-resolved 3.10.x patch releases install. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add --overwrite-mpp so TCGA-style SVS that lack an embedded micron-per-pixel can be opened and tiled (dlup otherwise raises UnsupportedSlideError); forwarded to both SlideImage.from_file_path and from_standard_tiling. Decode the slide thumbnail a single time and reuse it for the saved PNG and both heatmap overlays, hoist csv/PIL imports, and derive --mask-function choices from AvailableMaskFunctions. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
run_demo.sh downloads the RetCCL encoder and ECTIL classifier weights and five TCGA-BRCA slides, builds the Docker image, runs both single-slide and directory inference on CPU, and asserts the expected per-slide outputs before printing a SMOKE TEST PASSED/FAILED verdict. Ignore the demo's data/inference_output run dir. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Lead with WSI inference (smoke test, Docker, direct), move the manuscript- reproduction details into a collapsible section, and surface run_demo.sh. Fix broken example commands (missing line-continuation backslashes in the extract and eval snippets), a malformed markdown link, the clone URL (YoniSchirris -> nki-ai, also in setup.py), and note --overwrite-mpp for TCGA slides that lack an embedded spacing. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
continuumio/miniconda3:latest was bumped to conda 26.x on 2026-04-29.
On a clean pull the conda-forge solve for the WSI libs (openslide /
pixman / libvips) could swap python for graalpy, after which
`pip install torch==2.4.1` fails because torch has no GraalPy wheels:
Could not find a version that satisfies the requirement torch==2.4.1
Mac developers didn't see this because they had the old :latest cached.
- Pin the base image to continuumio/miniconda3:24.11.1-0.
- Constrain `python=3.10.9` to the *_cpython build at conda create time
and write `python 3.10.9` to conda-meta/pinned so the second
conda install can't swap CPython out when resolving the WSI deps.
- Add .gitattributes (eol=lf for *.sh / Dockerfile) so shell scripts
don't get CRLF on Windows checkouts and break bash.
|
@NUltee — pushed Root cause: Fix:
Could you do a |
Adds .github/workflows/docker.yml: on push to main and on v* tags, build linux/amd64 and push to ghcr.io/nki-ai/ectil-inference with tags :latest (main), :vX.Y.Z (releases), and :sha-<short> (per commit). PRs that touch the Dockerfile / requirements / workflow trigger a build-only validation (no push). Updates the docs and the runnable wrapper to prefer pulling the published image over building locally, while keeping the local-build path as a one-liner alternative. The local-build smoke test (tools/infer/run_demo.sh) is intentionally left building from source so it keeps validating the Dockerfile end-to-end. Note: the first successful push from main creates the package under the nki-ai org and defaults to private; flip it to public in the GitHub package settings to let unauthenticated users docker pull.
|
Follow-up: pushed What's in it:
Heads-up for whoever merges:
|
Without this the PR build is build-only, which means anyone reviewing this PR still has to docker build locally - defeating the point of publishing the image in the first place. Same-repo PRs now push a :pr-<number> tag so reviewers can `docker pull ghcr.io/nki-ai/ectil-inference:pr-3` to exercise the branch's container without building. Forked PRs stay build-only (no writable GITHUB_TOKEN; we don't want unreviewed fork code pushing to our registry anyway).
|
Good catch from offline — pushed Same-repo PRs now publish a docker pull ghcr.io/nki-ai/ectil-inference:pr-3
docker run --rm \
-v /path/to/slides:/input:ro \
-v /path/to/weights:/weights:ro \
-v /path/to/output:/output \
ghcr.io/nki-ai/ectil-inference:pr-3 \
--wsi /input/slide.svs \
--classifier-weights /weights/ectil_fold_0_weights_only.ckpt \
--retccl-weights /weights/retccl_best_ckpt.pth \
--output /outputForked PRs stay build-only (no writable token from a fork, and we don't want unreviewed fork code pushing to the registry). Two-step caveat for the very first run: the GHCR package doesn't exist yet, so this PR's workflow run will be the one that creates it under |
Summary
tils_score.json,tile_predictions.csv,features.h5, thumbnail/mask/heatmap PNGs) and an aggregatetils_scores.csv.setuptools/wheel/packagingas a matched set withpip==23.3.2(the editable install otherwise died oncanonicalize_version(strip_trailing_zero=)), and relaxespython_requiresfrom==3.10.9to>=3.10,<3.11so conda-resolved 3.10.x patches install.--overwrite-mppso slides without an embedded spacing (many TCGA SVS) can be opened/tiled instead of raising dlup'sUnsupportedSlideError. Off by default (None); when set, it supplies the slide's native base spacing so dlup can resample to the model's target--mpp 0.5(the value used in the extraction experiments — seeconfigs/datamodule/encoder/retccl.yaml). For TCGA 40x diagnostic slides that native spacing is0.25.tools/infer/run_demo.sh: a standalone smoke test (plain bash, no external tooling) that downloads the RetCCL + ECTIL weights and 5 TCGA-BRCA slides, builds the image, runs both single-slide and directory modes on CPU, and asserts the outputs before printingSMOKE TEST PASSED/FAILED.Verification
Smoke test passed end-to-end on CPU (both modes, all assertions green). Real TIL scores on 5 TCGA-BRCA slides:
Test plan
This is a single command. It downloads everything, builds the container, runs both inference modes, and validates the output. No weights, no slides, and no Python deps need to be set up by hand.
1. Clone and enter the repo
git clone https://github.com/NKI-AI/ectil cd ectilYou need Docker running, plus
curlandpython3on the host (the latter only to bootstrapgdownand parse the result JSON — all the heavy C deps live inside the image). The script refuses to start and tells you exactly what's missing if not.2. Run the one command
That's it. Everything below happens automatically, and it's idempotent — re-running skips anything already downloaded.
3. What you should see scroll past
The script narrates 7 stages. Roughly:
The two
[6/7]runs are the point of the test: single-slide (--wsi <one .svs>, expect 1 row) and directory (--wsi data/wsi/demo, expect 5 rows) modes.4. What "pass" looks like
It ends with a per-slide score table and:
and exits
0. On any problem it printsSMOKE TEST FAILEDwith the offending[FAIL]lines and exits1.5. Inspect the artifacts (optional)
ls data/inference_output/demo_dir/ # config.json, tils_scores.csv, 5 per-slide subdirs cat data/inference_output/demo_dir/tils_scores.csvEach per-slide subdir holds:
tils_score.json,tile_predictions.csv,features.h5, andthumbnail.png/mask.png/mask_overlay.png/attention_heatmap.png/til_heatmap.png. Spot-check atil_heatmap.pngagainst the slide to sanity-check the spatial predictions.Everything downloaded (
data/wsi,model_zoo/**) and written (data/inference_output) is gitignored.Requesting @NUltee as reviewer (recent commits on
main).🤖 Generated with Claude Code