[Feature]: Add MONAILabel to OHIF viewer plugin XNAT - #945
Draft
atriaybagur wants to merge 28 commits into
Draft
Conversation
Ports the MONAI Label / OHIF / XNAT work from the legacy nhsflame repo onto FLIP's current trust layout, as an opt-in service. MONAI Label adds AI-assisted annotation to the XNAT OHIF viewer. It needs an NVIDIA GPU and a large image, so it must not be forced on every trust: it ships as a compose overlay applied only when MONAI_LABEL=true, following the existing GPU_OVERRIDE / TRUST_OVERRIDE pattern rather than introducing Compose profiles. With the flag unset the rendered trust stack is byte-for-byte unchanged. Notes on the port: - MONAI Label is pinned to git commit 3806289, not PyPI. The latest release (0.8.5) omits auth on XNAT datastore GETs, so the viewer's MONAI Label panel fails with HTTP 500 over a 401 (Project-MONAI/MONAILabel#1837). The fix (#1838) merged in June 2025 and is still unreleased. The build asserts the fix is present rather than trusting the pin. - The XNAT archive is mounted read-only at the archive itself, not its parent: the datastore resolves files as <asset path> + <URI minus /data/xnat/archive/>, so a parent path makes every lookup miss and silently fall back to HTTP. MONAI_LABEL_XNAT_ARCHIVE_DIR anchors the kit's XNAT_DATA_DIR at xnat/, since that value is written relative to trust/xnat/. - The URL registered with XNAT is configurable and defaults to localhost. XNAT's ohifaiaa xapi only stores the string — the clinician's browser calls it directly — so a Docker-internal name or 0.0.0.0 cannot work. - The entrypoint polls XNAT, the service account and the server's own readiness instead of sleeping, and fails if registration is rejected; an unregistered server otherwise looks healthy while the viewer panel is simply missing. - up-trust refuses MONAI_LABEL=true without a GPU, and under PROD, where no monailabel image is published yet. Signed-off-by: at24_bioeng625-pc <alexandre.triay_bagur@kcl.ac.uk>
… setup (#55) Second half of the MONAI Label port: the return leg that turns a segmentation saved in the OHIF viewer into the NIfTI that FL training consumes, plus the manual steps the viewer needs. export_mask: - Command and event JSON derived from the canonical definition in londonaicentre/xnat_ohif_monailabel rather than the legacy nhsflame copy. - The subscription is site-wide: a DICOM-SEG can be created in any project and there is no per-project opt-in for it, unlike dcm2niix's dicom_to_nifti flag. - Unlike dcm2niix, the converter talks to XNAT (pyxnat) to fetch the session's NIfTI resources and upload its output, so it needs a network. The trust network name is now exported into xnat-web and pinned onto the command; configure-export-mask.sh refuses to run without it, since the container would otherwise start with no route to XNAT and fail at upload time. configure-dcm2niix.sh deleted EVERY site-wide event subscription it found, not just its own. That silently removed anything else registered on the site — an operator's own subscription, or export_mask's. It is now scoped by name to the two historical dcm2niix spellings ("DICOM-NifTi Conversion" from the retired site-wide JSON, "DICOM-NIfTI Conversion" from imaging-api). Dockerfile: install torch from the cu128 index with --index-url, not --extra-index-url. With PyPI still in the resolver its default torch is a CUDA 13 build that outranks the cu128 wheel, which pulled nvidia-*-cu13 and produced an image that cannot run on the trust hosts' 575.x driver (cu13 needs >=580). The resolved versions are frozen into a constraints file so MONAI Label's own dependency tree cannot swap torch back, and the build now asserts CUDA 12.x — sm_120 alone does not distinguish cu128 from cu13. Docs: MONAI Label needs the OHIF viewer plugin, which FLIP deliberately does not install (#662) — that tension is now stated up front. Registering the server is automatic; the per-user feature flag is not, and cannot be defaulted from the server, so the one-time per-browser step is documented along with where the viewer persists it and why the alternatives were rejected. Signed-off-by: at24_bioeng625-pc <alexandre.triay_bagur@kcl.ac.uk>
The previous version expanded the one-time viewer toggle into two sections with a full rationale for why it cannot be defaulted server-side and a four-item troubleshooting list. That detail belongs in the issue, not in a setup README — the actionable content is the toggle itself, that it is once per browser, and what to check first when the panel is missing. Signed-off-by: at24_bioeng625-pc <alexandre.triay_bagur@kcl.ac.uk>
Both found by actually building and running the image. curl: entrypoint.sh probes XNAT, the service account and the server's own readiness with curl, but python:slim does not ship it — the NVIDIA base this image was originally built on did. The container started and then logged "curl: command not found" on every probe. The build assert used torch.cuda.get_arch_list() to require Blackwell (sm_120) kernels. That call returns [] whenever torch.cuda.is_available() is false, which it always is inside a GPU-less build, so it failed a perfectly good image: the log read "torch 2.11.0+cu128 cuda 12.8 []". The wheel's own metadata is the right build-time signal, so it now asserts CUDA 12.x and a +cu local version tag instead, which is what actually distinguishes the cu128 wheel from PyPI's cu13 default. Signed-off-by: at24_bioeng625-pc <alexandre.triay_bagur@kcl.ac.uk>
The radiology app registers sam_2d and sam_3d whenever the sam2 package is importable (radiology/main.py gates on conf 'sam2', which defaults to true), so MONAI_LABEL_MODELS does not describe the full set of loaded models and the image cannot be slimmed by dropping sam2 without losing interactive segmentation. Also notes the extra ~900MB checkpoint fetched on first start. Signed-off-by: at24_bioeng625-pc <alexandre.triay_bagur@kcl.ac.uk>
Diagnosed live on the dev GSTT stack: every DEEPGROW-type model (sam_2d, deepgrow_*) returned 'Empty mask was returned by the model run' in a long-lived viewer tab, across CR and CT data and regardless of segment selection, while deepedit_seg rendered correctly. The server returned valid non-empty masks for every failing run (verified by replaying the exact infer calls and counting voxels: 139k on CR, 97k on CT). The viewer's DEEPGROW path remaps the returned mask through the active segment index ([0, I]); when the tab's segment state wedges, that index goes bad and every voxel remaps to zero. SEGMENTATION-type models use the model's own label map and are immune. A plain reload of the tab restores stock behaviour — confirmed by console line-offsets proving the working run executed the unmodified 3.8.0 bundle. Signed-off-by: at24_bioeng625-pc <alexandre.triay_bagur@kcl.ac.uk>
Characterised live: DeepGrow-type results (sam_2d/sam_3d/deepgrow_*) always write into segment index 1 regardless of the selected segment; selection and panel refresh do not change it. Same family as the stale-state empty-mask failure — the viewer routes DEEPGROW masks through an active-segment index that the segments panel does not drive in this build. deepedit_seg (SEGMENTATION type) routes by the model's own label map and is unaffected. Upstream (ICR viewer frontend 3.7.2); the 3.7.0 frontend predates the segment-store rework and was the configuration the legacy repo validated. Signed-off-by: at24_bioeng625-pc <alexandre.triay_bagur@kcl.ac.uk>
…ailabel-to-ohif-viewer-plugin-xnat
3 tasks
|
✅ Acceptance criteria have been automatically imported from the linked issue(s) and added to the PR description. |
This was referenced Aug 12, 2026
…mpose (#947) docker_build_monailabel.yml follows the orthanc pattern (direct push + workflow_dispatch + PR build, no test-workflow gate) with two additions: the job reclaims ~25GB of preinstalled runner toolchains before building — the image is ~10.4GB against ubuntu-latest's ~14GB free — and an import smoke (monailabel/torch/sam2/XNATDatastore + curl) runs between build and push, so a broken runtime stack never publishes. The Dockerfile's own build asserts already cover the unpatched-XNAT-auth and non-CUDA-12-torch states. compose_trust.production.monailabel.yml is image-only like the rest of the production compose; the Makefile's $(__DCKR_SUFFIX) machinery picks it up with no new wiring. The hard PROD refusal in require_monailabel_supported becomes an explicit warning: enabling MONAI Label re-introduces the OHIF viewer plugin the trust XNAT deliberately excludes (FLIP#662 bulk-import livelock), which is a per-trust judgement call, not a build limitation — intended for hybrid/on-prem annotation trusts. The GPU refusal stays hard: a GPU-less host cannot run it at all. Dev keeps building locally: pointing the dev overlay at ghcr :stag before a first publish exists would fail every `make up-trust` on pull. Signed-off-by: at24_bioeng625-pc <alexandre.triay_bagur@kcl.ac.uk>
6 tasks
Mirrors the compose overlay: Deployment + Service + weights PVC, gated on monailabel.enabled (default false — the default render is proven unchanged, comment-only delta). The chart's XNAT already ships the ohif-viewer plugin in its roster, so unlike compose there is no plugin step and no FLIP#662 exclusion to weigh here. Chart-specific decisions: - The XNAT archive is read straight off xnat-web's data PVC (read-only `archive` subPath). That PVC is ReadWriteOnce by default, so the pod is pinned to xnat-web's node via required podAffinity (monailabel.coScheduleWithXnat, default true); RWX storage classes can disable it. - monailabel.publicUrl is `required` when enabled rather than defaulted: XNAT hands it to the OHIF viewer, which calls it from the clinician's browser — the chart cannot derive an outside-the-cluster URL, and a wrong default fails only at click time. The helm-template CI job asserts the guard fires. - Exposed as NodePort (default 30030) plus a scoped NetworkPolicy opening exactly that port on exactly this pod through the namespace's default-deny ingress (service.allowExternalIngress). - Startup probe allows 30 minutes: first start on a cold volume downloads pretrained weights including the ~900MB SAM checkpoint. - Render coverage lives in the helm-template job, NOT ci/test-values.yaml: the kind install deploys those values and waits for pod readiness, which a ~10.4GB GPU image can never satisfy there. Signed-off-by: at24_bioeng625-pc <alexandre.triay_bagur@kcl.ac.uk>
7 tasks
…ment [Feature]: MONAI Label deployment in the Kubernetes trust chart
…age-prod-compose [Feature]: Publish monailabel image and support MONAI Label in production compose
a943c06 taught the cleanup to tell site-wide subscriptions from per-project ones. Correct, but beside the point: nothing has created a site-wide dcm2niix subscription since 88adb78 (2026-03-24), when dcm2niix moved to per-project subscriptions and dcm2niix_event.json was deleted. Confirmed by grep — the only remaining references were the cleanup's own name list — and by four running trusts, none of which carries one. So the step had nothing legitimate left to find, while retaining every ability to delete the wrong thing. A cleanup for something nothing creates can only misfire; keeping a smarter version of it just narrows the window. Removed, with the reasoning left in place so it is not reintroduced, and the test repointed from 'is the filter correct' to 'this script deletes no subscription at all' — which is the property that actually matters and is far harder to get wrong. Mutation-checked: re-adding a delete in any form fails two of its cases. Net 202 lines lighter. Signed-off-by: at24_bioeng625-pc <alexandre.triay_bagur@kcl.ac.uk>
atriaybagur
added a commit
that referenced
this pull request
Aug 17, 2026
…HCR image
Docker Hub's mutable xnat/dcm2niix:latest resolves to dcm2niix v1.0.20211006 —
a 2021 build upstream no longer updates — whose slice-ordering heuristic
mis-fires on some of the seeded spleen series ("Slices skipped: image position
not sequential" on clean, uniformly-spaced 0-based-InstanceNumber series) and
silently drops one slice. The converted input_*.nii.gz then has N-1 slices
while the enrichment label has N, so the segmentation tutorials' shape QC
rejects 6 of GSTT's 21 spleen studies. The current upstream release
(v1.0.20260724) converts the same series in full.
- New trust/xnat/dcm2niix/Dockerfile: debian-slim + the upstream release
binary, pinned by version AND sha256, with a build-time version smoke check.
- New docker_build_xnat_dcm2niix.yml workflow publishing
ghcr.io/londonaicentre/xnat-dcm2niix with the usual sha/branch/stag/prod
tags plus the tool-version tag (read from the Dockerfile ARG) that all
deploy configs reference — also resolves the PR #945 review note about the
converter still coming from Docker Hub.
- dcm2niix_command.json (Compose), the K8s init-job's inline copy, and
imaging-api (new Settings.DCM2NIIX_IMAGE, used by the per-project event
subscription lookup) now reference the immutable version tag; command
version bumped 1.6 -> 1.7.
- Verified: the built image converts the previously slice-dropping series
(FAK76374103) 60/60 and a control series 92/92 with the Container Service's
exact command line; imaging-api ruff+mypy+274 tests and trust/xnat's 174
tests pass.
Closes #980
Signed-off-by: at24_bioeng625-pc <alexandre.triay_bagur@kcl.ac.uk>
…plugin-xnat All four conflicts were independent additions colliding at the same anchor. - trust/.env.example, .env.GSTT/.KCH.development.example: kept BOTH the MONAI Label block and develop's FL_SITE_PRIVACY_* block (FLIP#851). Both sit above the hub-shared sentinel that scripts/trust_kit_lib.py matches, which is intact. - trust/Makefile: combined both edits — this branch's $(MONAILABEL_OVERRIDE) (load-bearing: the debug targets run 'up --remove-orphans', so omitting the overlay deletes a running monailabel container) with develop's rename of the debug override to deploy/compose_trust.development.debug.override.yml, which is the only such file that now exists. Signed-off-by: Alex Triay Bagur <alexandre.triay_bagur@kcl.ac.uk>
Verified against real S3: ohif-viewer is not published under the version-keyed prefix xnat-plugins-download syncs (s3://flipdev-artifacts/xnat-1.10.0/plugins/ holds only batch-launch, container-service and dicom-query-retrieve). The only copy in the bucket is under the legacy xnat/plugins/ prefix at 3.7.1 — too old for XNAT 1.10, which is why the k8s chart pins 3.8.0. So MONAI_LABEL=true on compose now fails at plugin download rather than crash- looping later in the monailabel container, which is the right place to fail, but the message was a bare 'missing plugin family'. Say what is actually missing, that it is a publishing gap rather than a mistake, and give the two commands that close it — plus the escape hatch of MONAI_LABEL=false. Also correct a comment: the XNAT config dir is baked into the image in stag/prod only. docker-compose-stack.development.yml bind-mounts it from the host, so the 'script does not exist on an older image' case is a stag/prod one. Signed-off-by: at24_bioeng625-pc <alexandre.triay_bagur@kcl.ac.uk>
…plugin-xnat One conflict, in trust/xnat/Makefile, right after the kit-file `export` block: this branch adds the `MONAI_LABEL ?= false` default there (gates export_mask registration in xnat-configure) and develop (#958, FLIP_INSTANCE) adds `include ../../deploy/instance.mk` at the same spot for INSTANCE_PREFIX. Both kept, develop's include first: the instance-scoped XNAT_STACK / XNAT_NETWORK / TRUST_PROJECT derivations and the check_network_matches_trust guard land unchanged, and the MONAI Label default follows them. The other four files the earlier probe flagged (trust/.env.example, the two dev kit examples, trust/Makefile) merged cleanly once the branch was rebased onto its own develop-synced tip. Verified on the merged tree: trust/xnat unit suite (199 passed), the xnat-configure- export-mask / xnat-plugins-download / create-xnat-network dry-runs in both MONAI_LABEL states, up-trust and build dry-runs with the overlay, and `docker compose config` for the dev and prod stacks with the monailabel overlay (service present in both). Signed-off-by: at24_bioeng625-pc <alexandre.triay_bagur@kcl.ac.uk>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Adds an optional MONAI Label server to the trust stack: AI-assisted annotation (interactive
SAM click-to-segment, DeepEdit multi-organ segmentation) in the XNAT OHIF viewer, reading DICOM
straight off the trust's XNAT archive and writing labels back as DICOM-SEG assessors. Ports the
exploration from the legacy
nhsflamebranch (255-feature-add-monailabel-to-ohif-viewer-plugin-xnat)and the
xnat_ohif_monailabelrepo onto FLIP's current trust layout.Off by default and opt-in per trust — it needs an NVIDIA GPU and a large image, so a trust
that does not enable it is byte-for-byte unaffected (verified: identical rendered compose without
the flag). Follows the existing
GPU_OVERRIDE/TRUST_OVERRIDEoverlay pattern rather thanintroducing Compose profiles:
Key decisions (details in
trust/README.md#monai-label-optionaland commit messages):XNAT datastore GETs — the viewer panel 500s over a 401 (Monailabel-Plugin in XNAT OHIF Viewer status code 500 Project-MONAI/MONAILabel#1837; fix
#1838 merged Jun 2025, never released). The image build asserts the fix is present.
python:3.12-slim+ torch from the cu128 wheelindex (same approach and reasoning as
fl-services/flower/fl-base; cu13 wheels need driver≥ 580, trust hosts run 575.x). Build asserts CUDA 12.x so a wrong wheel fails the build, not
first inference.
<asset path> + <URI minus /data/xnat/archive/>, so a parent path silently misses and fallsback to per-scan HTTP downloads. Labels go back via the XNAT REST API, never the filesystem.
MONAI_LABEL_PUBLIC_URL): XNAT'sohifaiaaxapionly stores the URL — the clinician's browser calls it directly, so it must be
browser-resolvable (and same-origin/TLS-fronted on an HTTPS XNAT).
export_maskcontainer-service command (site-wide event subscription): converts OHIF'sDICOM-SEG assessors to NIfTI for training.
configure-dcm2niix.sh's legacy cleanup previouslydeleted every site-wide subscription it found; now scoped by name to its own.
up-trustrefusesMONAI_LABEL=truewithout a GPU; underPRODitproceeds with an explicit trust XNAT: bulk image-pull wedges permanently — Reactor EventBus ring-buffer livelock (OHIF listener) + restart strands directArchive #662 warning (see the absorbed follow-ups below).
Linked Issues
Fixes #55
Fixes #947
Fixes #948
Checklist
Type of Change
make -C docs/ docs.Testing
No new automated tests — the change is compose/Makefile/shell config plus a new Docker image;
verification was live on the dev GSTT stack (XNAT 1.10.0, OHIF plugin 3.8.0, RTX 5090):
MONAI_LABEL=trueis unchanged(
docker compose configservice-set comparison); both Make guards exercised (no-GPU refusal,PRODrefusal).per-trust XNAT archive (
xnat/xnat-data-<CODE>/xnat-data/archive, read-only).images off disk (
Exists in asset store);deepedit_segproduced correct multi-organ maskson abdominal CT in the viewer;
sam_2d/sam_3dreturned valid masks server-side on everyrun (voxel-counted by replaying the exact infer calls).
non-cu12 torch).
Additional Notes
README states this trade-off up front; enabling MONAI Label means accepting that plugin back
onto the trust's XNAT.
remedies in the README — both client-side, server masks verified valid throughout:
stale-tab state → "Empty mask" (reload the tab); DeepGrow results always land in the first
segment (treat SAM as single-target until fixed upstream;
deepedit_segunaffected).docker_build_monailabel.yml(orthancpattern + runner disk reclaim — the image is ~10.4 GB vs ubuntu-latest's ~14 GB free — and
an import smoke between build and push), image-only production overlay, and the hard
PRODrefusal becomes an explicit trust XNAT: bulk image-pull wedges permanently — Reactor EventBus ring-buffer livelock (OHIF listener) + restart strands directArchive #662 warning. Dev keeps building locally until
:stagfirstexists. The publish path proves itself on its first run (
workflow_dispatch, or thedevelop merge).
templates/monailabel.yaml(Deployment/Service/weightsPVC/scoped NetworkPolicy) gated on
monailabel.enabled(default off; default renderproven to add zero resources). Archive read via xnat-web's data PVC (read-only
archivesubPath, podAffinity co-scheduling for RWO storage);
publicUrlisrequired— thebrowser calls it, the chart can't derive it; render coverage in the
helm-templateCI job(deliberately NOT in
ci/test-values.yaml, which kind installs and waits on podreadiness). Not yet live-tested on a GPU cluster — needs the published image first.
export_maskconverter image from the personalatriaybagur/...Docker Hub tag to the org GHCR with an immutable tag; upstream bug reportto ICR for the two viewer defects.
Acceptance Criteria
From issue #55
From issue #947
trust/monailabel/**to develop publishesghcr.io/londonaicentre/monailabel:stag(+ immutablesha-<short7>); main publishes:prod.make up-trust KIT=<CODE> MONAI_LABEL=true PROD=stagstarts the server from the published image on a GPU host, with a visible trust XNAT: bulk image-pull wedges permanently — Reactor EventBus ring-buffer livelock (OHIF listener) + restart strands directArchive #662 warning.MONAI_LABELunset, dev and prod stacks are unchanged.Follow-up to #55 / PR #945.
From issue #948
helm lintandhelm templatepass withmonailabel.enabled=true(CI values) and default values.monailabel.enabled=false(default) the rendered chart is unchanged.deepedit_segandsam_2dproduce masks in the OHIF viewer against archive data.Follow-up to #55 / PR #945. Depends on the published image from the image-publishing issue.