Self-contained folder to run the B-reader CXR stack from pre-built Docker Hub images — no clone of the full training repo required.
GitHub: github.com/sscotti/breader-model-deploy
| Service | Image | Visibility |
|---|---|---|
| CXAS preprocess API | sdscotti/cxr-preprocess-api |
Public |
| Ark inference + Gradio | sdscotti/breader-inference |
Private (Hub login required) |
| Caddy (TLS) + authgate (Basic Auth + lockout) | caddy, authgate |
Public (:8443) |
| Orthanc + ILO plugin | sdscotti/orthanc-breader |
Public |
- DICOM or PNG upload → CXAS anatomy-aware preprocessing → Ark embedding
- Ten classifier heads (Q2A, Q3A, normal vs not, pleural screen/face/diaphragm, profusion 0–3, full ILO category, small-opacity type, large-opacity stage)
- Neighbor retrieval from training dataset .png images. Default Ark (1376-D); optional Google ELIXR / contrastive spaces in the UI (see API.md)
- Orthanc ILO plugin → multi-page PDF report attached to the study
Simplified ILO B-reader Gradio UI
Although the GitHub repo is public, you will need a DOCKER_HUB_TOKEN from the developer to pull the inference image from Docker Hub.
Models, neighbor PNGs, Ark weights, and bundled heads ship inside the inference image.
CXAS UNet weights download on first start (or seed via cxas/weights/).
breader-model-deploy/
docker-compose.yml # pull + run (no build:)
.env.sample # copy → .env
pull.sh # docker login + compose pull
scripts/ # gen_localhost_certs.sh, gen_basicauth_users.sh
caddy/ # Caddyfile + entrypoint (HTTPS gateway)
authgate/ # Basic Auth + 3-strike IP lockout (forward_auth)
certs/ # self-signed TLS (generate locally; gitignored)
auth/ # users.basicauth (generate locally; gitignored)
API.md # predict / heads summary for operators
LICENSE # Apache-2.0 (original glue / docs in this folder)
NOTICE # third-party model / data attributions
TERMS.md # intended use for the assembled Docker stack
images/gui.png # Gradio UI screenshot
images/projections/ # Ark PCA / t-SNE / UMAP thumbs (profusion + opacity type)
cxas/weights/ # optional UNet seed (see README there)
orthanc/
config/ # orthanc.json, ohif.js
python/ # ILO PDF plugin (mounted into Orthanc container)
data/worklists/ # DICOM worklist drop folder
- Docker Engine 24+ and Docker Compose v2
- Docker Desktop → Settings → Resources → Memory: 12 GB+ (inference + CXAS on CPU)
- ~15 GB free disk (images + CXAS weights + Orthanc data)
- Read access to private repo
sdscotti/breader-inference(Hub read token DOCKER_HUB_TOKEN from maintainer) - The Docker Hub images are for linux/amd64 arch. My local dev are for Mac Silicon
cd breader-model-deploy
cp .env.sample .env
# Edit .env: set DOCKER_HUB_TOKEN (read-only PAT)
# TLS cert + optional Basic Auth user (required before first up if CADDY_BASIC_AUTH=true)
EXTRA_DNS=breader.medinformatics.eu ./scripts/gen_localhost_certs.sh
./scripts/gen_basicauth_users.sh breader 'your-long-password'
./pull.sh
docker compose up -dOpen:
- Inference UI / API:
https://breader.medinformatics.eu/(production hostname) orhttps://<host>:8443/locally. IfCADDY_BASIC_AUTH=true, accept the self-signed warning (unless you use a Cloudflare Origin cert) and log in. - Inference HTTP (localhost debug only): http://127.0.0.1:7860/
- CXAS / Orthanc UI / Orthanc DICOM: localhost-only (
8081/8042/4242). For Orthanc from another machine use SSH, e.g.ssh -L 8042:127.0.0.1:8042 user@hostthen open http://127.0.0.1:8042/
Orthanc’s ILO plugin still calls http://inference:7860 on the Docker network (no browser TLS/auth).
First CXAS start may take several minutes while UNet weights download (unless seeded).
Public HTTPS is terminated by Caddy. Production hostname is breader.medinformatics.eu (CADDY_HOSTNAME). Other Host headers (including raw IP) get HTTP 421.
Cloudflare A record
- Point
breader.medinformatics.eu→ VPS public IP. - Start DNS-only (grey cloud) if the origin uses a self-signed cert on
:8443or:443. - If you orange-cloud (proxy): set SSL/TLS to Full (not Flexible). Full (strict) needs a Cloudflare Origin CA cert saved as
certs/localhost.crt+certs/localhost.key. - Open the host port (
CADDY_HTTPS_PORT, default 8443; use443for standard HTTPS:CADDY_HTTPS_PORT=443maps443:8443).
Regenerate the origin cert with the DNS name:
EXTRA_DNS=breader.medinformatics.eu ./scripts/gen_localhost_certs.sh
docker compose up -d --force-recreate caddyBasic Auth (CADDY_BASIC_AUTH, default true) uses the same auth/users.basicauth file (bcrypt hashes). Set CADDY_BASIC_AUTH=false to serve Gradio without a password (authgate stays up but is not consulted). Recreate Caddy after changing it:
# .env
CADDY_BASIC_AUTH=false
docker compose up -d --force-recreate caddy authgate- Users live in
auth/users.basicauth(gitignored). Add more with./scripts/gen_basicauth_users.sh. - Change password → re-run that script →
docker compose up -d --force-recreate authgate caddy. - Lockout (only when auth is on): after
AUTHGATE_MAX_FAILURES(default 3) wrong passwords, that client IP gets HTTP 429 forAUTHGATE_LOCKOUT_SECONDS(default 900). Restartauthgateto clear locks. - Do not commit
.envpasswords orauth/users.basicauth.
docker compose ps
curl -sS http://127.0.0.1:8081/health
# Through Caddy (must auth; -k for self-signed)
curl -sk -u breader:'your-long-password' https://127.0.0.1:8443/healthz \
| jq '{status, embedding_backend, retrieval_kinds}'
# Unauthenticated should be 401
curl -sk -o /dev/null -w '%{http_code}\n' https://127.0.0.1:8443/healthz
curl -sS http://127.0.0.1:8042/systemUpload a DICOM in the Gradio UI or see API.md for curl examples.
docker compose up -d cxas-api inference caddyThe Python plugin under orthanc/python/ calls ILO_PREDICT_BASE_URL (default
http://inference:7860 on the compose network). In Orthanc Explorer2, expand a study →
instance list → ILO Inference button.
Optional plugin defaults: orthanc/python/ilo_plugin.json (env vars override at runtime).
Set tagged images in .env:
INFER_IMAGE=sdscotti/breader-inference:2026-06-17
CXAS_API_IMAGE=sdscotti/cxr-preprocess-api:2026-06-17
ORTHANC_IMAGE=sdscotti/orthanc-breader:2026-06-17Then ./pull.sh && docker compose up -d --force-recreate.
| Symptom | Fix |
|---|---|
pull access denied on inference |
docker login with read token; confirm access to private repo |
| Caddy exits: missing cert / users | Run cert + users scripts (or CADDY_BASIC_AUTH=false); recreate caddy |
| Browser TLS warning | Expected for self-signed; or use Cloudflare Origin CA for Full (strict) |
401 on public HTTPS |
Wrong user/password, or auth still on; re-run gen_basicauth_users.sh or set CADDY_BASIC_AUTH=false |
421 on public HTTPS |
Host is not CADDY_HOSTNAME (use https://breader.medinformatics.eu/, not the raw IP) |
| Gradio “Connection to the server was lost” on Run Inference | Usually OOM (exit 137). Raise Docker Desktop memory to 12 GB+; keep retrieval on Ark; docker compose up -d --force-recreate inference |
| Inference restarts in a loop | docker compose logs inference --tail 50; check OOM in docker events |
| Inference starts before CXAS ready | Wait for cxas-api healthy; restart inference |
| CXAS slow first boot | Normal; or add UNet_ResNet50_default.pth under cxas/weights/ |
| Old model after maintainer push | ./pull.sh && docker compose up -d --force-recreate |
| Port in use | Change *_HOST_PORT / CADDY_HTTPS_PORT in .env |
orthanc-ilo name already in use |
docker rm -f orthanc-ilo then docker compose up -d |
This stack builds on the following public models and tools (weights and packaging are separate; see each project’s license / access terms):
| Resource | Role in this deploy |
|---|---|
| Ark / Ark+ (jlianglab) | Swin-Large CXR foundation encoder used for 1376-D embeddings and the primary classifier / neighbor space |
| Google CXR Foundation (ELIXR) | Optional ELIXR embedding backends and retrieval spaces in the Gradio UI (Health AI Developer Foundations terms apply on Hugging Face) |
| Chest X-Ray Anatomy Segmentation (CXAS) | Anatomy segmentation used by the preprocess API (crop / lung-aware normalization before embed) |
Ark checkpoint used here: Ark6_swinLarge768_ep50 (request / download via the Ark project’s published channels). CXAS UNet weights are fetched on first container start or seeded under cxas/weights/.
Labeled chest radiographs used to train the classifier heads were obtained through the NIOSH B Reader Program, including images associated with the NIOSH Chest Image Repository (CIR). NIOSH does not endorse this software; any classification outputs are research / decision-support only and are not a substitute for a certified B Reader.
Expanded cohort out-of-fold comparison: Google ELIXR vs Ark (primary production encoder is Ark). Regenerated 2026-08-02 (pleural thresholds now max F1).
| Google heads | GoogleCXR/model/<task>/ (dev repo) |
| Ark heads | Ark/model/<task>/ (bundled in inference image) |
| Training cohort | ~1205 films via training_sets/expanded_8bit_vindr_enhanced.txt |
| Labels | Consensus under datasets_png/8bit_robust/labels + VinDr |
| Task | Primary metrics | Ark | Δ (Ark − Google) | Edge | |
|---|---|---|---|---|---|
| q2a_binary | AUROC / F1 | 0.962 / 0.910 | 0.973 / 0.926 | +0.011 / +0.017 | Ark |
| q3a_binary | AUROC / F1 | 0.860 / 0.609 | 0.873 / 0.623 | +0.013 / +0.014 | Ark |
| normal_vs_not | AUROC / F1 | 0.965 / 0.815 | 0.974 / 0.841 | +0.009 / +0.026 | Ark |
| profusion_0-3 | QWK / MAE | 0.855 / 0.282 | 0.887 / 0.229 | +0.032 / −0.053 | Ark |
| profusion_full_category | QWK / Accuracy | 0.897 / 0.452 | 0.921 / 0.481 | +0.024 / +0.028 | Ark |
| small_opacities_multiclass_pure | Macro F1 / Macro AUROC | 0.803 / 0.938 | 0.831 / 0.953 | +0.028 / +0.015 | Ark |
| large_opacity_present | AUROC / F1 | 0.976 / 0.745 | 0.987 / 0.830 | +0.011 / +0.085 | Ark |
| large_opacities | QWK / MAE | 0.744 / 0.111 | 0.813 / 0.086 | +0.069 / −0.025 | Ark |
| pleural_calc_any | AUROC / F1 | 0.874 / 0.487 | 0.899 / 0.590 | +0.025 / +0.102 | Ark |
| pleural_calc_face | AUROC / MAP | 0.884 / 0.470 | 0.933 / 0.682 | +0.049 / +0.211 | Ark |
| pleural_calc_diaphragm | AUROC / F1 | 0.883 / 0.447 | 0.889 / 0.524 | +0.006 / +0.077 | Ark |
Binary / pleural operating points use max F1 on pooled OOF. OOF metrics are the honest generalization estimate for these heads. In-sample / full-cohort rescored numbers look stronger and are not for publication. Not a clinical validation; an independent labeled set is still needed before claiming deploy-ready performance.
| Task | n | Head |
|---|---|---|
| q2a_binary | 1196 | lr |
| q3a_binary | 1196 | lr |
| normal_vs_not | 1196 | lr |
| profusion_0-3 | 1196 | ordinal_logit |
| profusion_full_category | 1196 | ordinal_logit |
| small_opacities_multiclass_pure | 754 | multinomial_logistic_regression |
| large_opacity_present | 1196 | lr |
| large_opacities | 1196 | ordinal_logit |
| pleural_calc_any | 1196 | pleural_calc_multilabel |
| pleural_calc_face | 1196 | pleural_calc_multilabel |
| pleural_calc_diaphragm | 1196 | pleural_calc_multilabel |
- Original code in this folder (compose, Orthanc plugin packaging, scripts, docs): Apache License 2.0 — Copyright 2026 Stephen Douglas Scotti.
- Assembled stack / Docker images (Ark weights, CXAS, optional Google ELIXR, neighbor assets): see NOTICE and TERMS.md. Intended for research and education only; upstream non-commercial and HAI-DEF terms still apply. Not a medical device and not a substitute for a certified B Reader.
From the full HuggingFaceCXR repository root (not this folder):
# 1. Refresh bundled heads + neighbor indices
python inference/scripts/prepare_artifacts.py --profile ark_1376_8bit
# 2. Build all three images
docker compose build
# 3. Tag + push (example)
docker push sdscotti/cxr-preprocess-api:latest
docker push sdscotti/breader-inference:latest
docker push sdscotti/orthanc-breader:latest
# 4. Sync this deploy folder if orthanc plugin or compose changed, then:
cd breader-model-deploy && git add -A && git commit && git pushInference image uses inference/Dockerfile (unified Ark + ELIXR embed backends).
Collaborators only need this folder + Docker + Hub token.
Zip or share breader-model-deploy/ with collaborators; they never need the training tree.
3-D PCA / t-SNE / UMAP of the Ark 1376-D training embeddings, colored by consensus
labels (same expanded cohort as the OOF table above). Small thumbs only; full-res
artifacts live in the training repo under visualizations/out/ark_alllabels/.
PCA
t-SNE
UMAP
Same n = 754 cohort as the deployed small_opacities_multiclass_pure head
(Neither / Round / Interstitial; Mixed dropped; Q3A=N; strict-normal neither).
PCA
t-SNE
UMAP





