imagebank

A Dockerised Digital Slide Archive deployment — the estate’s only multi-user, browser-based slide platform — whose viewing and annotation layer is upstream software that works, and whose three own-written quality-control tools include one that cannot pass, one that samples background, and two that carry the GrandQC name without containing any GrandQC.

Purpose

This is a Digital Slide Archive (DSA) deployment: Girder as the application server, HistomicsUI as the browser viewer, large_image for pyramidal tile serving, MongoDB for metadata and annotations, RabbitMQ plus a worker for background jobs, and HistomicsTK for analysis — all orchestrated with Docker Compose. On top of that sit MONAI Label for model-assisted annotation on a local RTX A4000, and several analysis plugins written here.

It answers a question nothing else in this estate does. Everything else image-side — QuPath Tooling and the whole prototype cluster under Digital Pathology Drafts — is QuPath on one person’s desktop. imagebank is the only attempt at a server: several people opening the same slides in a browser, permissions per collection, annotations in a shared database, and analyses running as queued jobs rather than in the user’s own session. That capability gap is real and this is the only thing here that fills it — which is why the page exists despite the findings below. See Server-Side Slide Platforms for what that choice costs and buys in general.

It is a private repository (GitHub API, read 2026-07-31: isPrivate: true, no licence file, created 2025-10-25, last push 2025-11-01, 210 KB, five commits). Nine months dormant, so status: paused — the platform was stood up, worked, and then stopped being developed.

Read this page before running anything in qc_tool/ or cli/GrandQC/. The viewing, annotation and MONAI layers are upstream code and are not in question; the QC tools are local and are.

Data used

No cohort in the repository. Slides live outside it, on a mounted volume (/mnt/e/dsa in the deployment configuration), and MONAI Label reads them from that shared mount directly rather than downloading copies — a zero-copy arrangement configured through --conf dsa_asset_store_path.

All 119 tracked files were scanned for accession-format strings and for slide-file extensions, and the root logs/ directory was checked; nothing matched. Stating the method rather than declaring the repository clean is deliberate — this estate’s own gap #8 records two corrections that came from opening a file whose name had misled a pattern scan, so a passing grep is evidence and not a verdict. The only identifier-shaped strings the scan surfaced are a placeholder specimen ID inside an example metadata dictionary in manual-upload-tool.py and a public TCGA slide name in the vendored MONAI sample app. On that evidence, and against the eight instances catalogued in wiki/index.md, this repository does not add a ninth — its problem is credentials, not patient data.

Methods

Read at source level on 2026-07-31 — not from the README, which is why most of what follows is not in the README.

The QC tools do not do what their names say

Three separate quality-control implementations live here, and none uses a trained model. All three compute the same four things: Laplacian variance as a focus proxy, mean grey level as brightness, a fixed grey threshold for tissue, and standard deviation as contrast.

Two of them are called GrandQC and contain no GrandQC. cli/GrandQC/GrandQC.py and grandqc_integration.py are hand-rolled heuristics. The published GrandQC is a trained artefact-segmentation network; these share nothing with it but the name. This is not an inference from reading style — cli/GrandQC/Dockerfile builds FROM python:3.10-slim and pip-installs exactly large-image[sources], opencv-python-headless, numpy and Pillow. There is no PyTorch, no model weights, and no GrandQC package in the image, and a grep across cli/GrandQC/ and grandqc_integration.py for any model, weights or deep-learning import returns nothing at all.

The plugin manifest makes it worse rather than better. cli/GrandQC/GrandQC.xml presents the tool in the HistomicsUI Analyses menu as “GrandQC Quality Assessment” and declares <acknowledgements>Built with GrandQC library</acknowledgements> — a statement that is simply false as the image is built. The same file still carries the scaffold placeholder <documentation-url>https://github.com/yourusername/imagebank</documentation-url>.

Why this specific collision is dangerous here rather than merely untidy. This estate contains the real thing, twice: GrandQC-QuPath wraps the published GrandQC models, and GrandQC Quality Study measured those models against 502 slides of technician quality calls. So “the GrandQC number for this slide” is already an established phrase locally, and there are now two entirely different things that produce one. Anyone comparing this plugin’s output to that study’s figures would be comparing a Laplacian variance against a segmentation network while both are labelled GrandQC.

The staining check cannot pass — confirmed

In qc_tool/qc_analysis_cli.py, check_staining() sums the per-channel variances and divides by 255 * 255 * 3; run_qc_analysis() then warns if the result is < 0.5.

By Popoviciu’s inequality a channel bounded in [0, 255] has variance at most 255²/4 = 16256.25, so the score’s maximum possible value is 0.25 — half the threshold it is compared against. The “poor staining quality” warning therefore fires on every image ever analysed, including a theoretically perfect one.

Verified numerically rather than by inspection: an adversarial maximum-contrast image (half pure black, half pure white) scores exactly 0.25; uniform random noise scores 0.084; an H&E-like image scores 0.018; a blank white slide scores 0.0. All four warn. The check carries no information at any input.

It is bounded in one respect worth stating fairly: staining appends a warning but does not set passed = False, so it cannot fail a slide on its own. Only the focus check does that.

The focus score is not comparable between the tool’s own two modes

The same hard-coded threshold — focus_score < 100 — is applied to Laplacian variance computed at two very different resolutions. In whole-slide mode the tool measures a 1024-pixel thumbnail of a slide that may be 100,000 pixels wide; in ROI mode it measures the region at base resolution, downsampling only if the larger side exceeds 2048.

Laplacian variance is strongly scale-dependent, so these are not the same measurement and one constant cannot serve both. A slide judged in-focus by ROI is not necessarily judged in-focus whole-slide, and neither reading is anchored to anything a scanner reports. The threshold’s provenance is not recorded anywhere.

Other source-level findings

  • Random tiles are sampled without regard to tissue. cli/GrandQC/GrandQC.py draws 10 random 2048-pixel tiles from anywhere on the slide (random.seed(42), so at least it is reproducible). On a slide where tissue covers a fraction of the glass most tiles are background, and blank glass has near-zero Laplacian variance — so the blur metric is substantially a measure of how much of the slide is empty. The four metrics are then averaged and combined into a quality_score with undocumented weights (blur 0.4, brightness 0.2, tissue 0.3, contrast 0.1) and a pass mark of 70.
  • That CLI returns exit code 1 when a slide fails QC. return 0 if results['passed'] else 1 conflates “this slide is poor” with “this job errored”. Under Slicer CLI Web a non-zero exit is normally read as job failure, so a correctly-working analysis of a poor slide is likely to surface as a broken job rather than a QC verdict. Not tested against a running deployment — the reasoning is from the exit contract, so [unverified] as an observed behaviour.
  • The ROI parser guesses between two coordinate conventions. parse_roi() decides whether its four numbers mean left,top,right,bottom or x,y,width,height by testing whether the third and fourth are below 10000. A genuine right,bottom pair under 10000 — a small image, or an ROI near the top-left of any slide — is silently read as width and height, giving a region roughly twice as far right and down as the user drew. It prints its interpretation but raises no warning that it guessed.
  • Documentation overstates the model configuration by one. NEWS.md and the MONAI docs say four models are active (segmentation_nuclei, hovernet_nuclei, nuclick, sam_2d), but dsa-deployment/docker-compose.yml passes --conf models ${MONAI_MODELS:-segmentation_nuclei,hovernet_nuclei,nuclick} — three by default. sam_2d is registered conditionally inside the app rather than by that flag. The MONAI pathology app itself is the upstream sample app, not written here.

Secrets: partially remediated, and certified by a check that cannot fail

This corrects wiki/index.md gap #8, which stated that imagebank “commits a MONAI Label API key in plaintext across its whole five-commit history”. That was written from the Digital Pathology Drafts read; this is the first direct read of imagebank, and the true shape is more specific and more interesting.

The key is a Girder API key. Verified by reading the history directly:

  • It is in three of the five commits, not all five — introduced 2025-10-29 (a7d9a99) and present in the two commits after it (f054edb, eleven seconds later, and 5c7af0e). The two earliest commits are clean.
  • The key’s own operational configuration was fixed. dsa-deployment/docker-compose.yml now references an environment variable rather than the literal, both .env.example files ship an empty MONAI_API_KEY= placeholder, and a secrets file was added to .gitignore. A helper script creates a fresh key. For this credential, the running configuration is clean.
  • The literal key is nevertheless still present at HEAD, in exactly three tracked files — nine occurrences — all of them documentation: docs/monai/STATUS.md (4), docs/security/SECURITY_SETUP_SUMMARY.md (4) and docs/security/SYNC_SUMMARY.md (1).
  • A fourth, ungoverned copy sits outside git. dsa-deployment/monailabel/apps/pathology/logs/app.log — untracked and gitignored, so it never reached GitHub — recorded the key in a MONAI Label startup configuration dump on 2025-10-27. It is on local disk, unreachable by any .gitignore-based remedy, and it is the copy a history rewrite would not touch.

The part worth carrying elsewhere is how the remediation was verified. Both security documents assert the key is gone and both demonstrate it with the same shell command: a grep for the literal key, annotated “Should return nothing” in one file and “Returns nothing ✅” in the other.

The command does return nothing — and that is the defect. It greps *.md from the repository root, and that glob is not recursive: it expands to the seven top-level markdown files and never descends into docs/, where all three copies actually live. It cannot even see the file it is printed in. So the check passes, and it would pass no matter how many keys were in the repository. It is mis-scoped rather than dishonest, which is worse, because a check that cannot fail reads exactly like a check that passed.

The prose around it is what is flatly false. SYNC_SUMMARY.md states “No API keys in tracked files” — untrue of three tracked files at HEAD. SECURITY_SETUP_SUMMARY.md carries a ticked checklist item “API keys removed from documentation” — untrue. And the same document prints the key in full under the label “(SECURED)”.

This is De-identification Release Gating’s fourth property failing from an unexpected direction. That page says a gate must re-measure the bytes being released rather than carry an earlier measurement forward; here a measurement was written, run, and passed — against the wrong bytes. It is also the same shape as the warning already on De-identification about OpenMed’s privacy parameters defaulting to values that can never fail: a control that cannot fail is worse than no control, because it manufactures the evidence of its own success. Two unrelated repositories, one pattern.

Bounded, and the bounds matter: the repository is private (0 forks, no CI workflows tracked, so no build-log exposure route), and the key authorises a localhost Girder instance. This is governance, not disclosure. Rotation is still the only remedy — deleting the lines does not undo the history, and would not reach the log file at all. [unverified] whether the key is still active, and whether the repository was ever public; current visibility says nothing about the past.

The admin password is a separate and less-remediated problem, and I first recorded it wrongly. It is not a stock DSA quickstart credential — that distinction matters and I had it backwards. provision.yaml carries the genuine upstream default; the password used everywhere else is bespoke to this deployment, present from the initial commit, and never touched by the October work. It is in 37 tracked files at HEAD, up from 15 in the first commit, including manual-upload-tool.py, batch_export.py, grandqc_integration.py, dsa-deployment/batch_process_monai.py, examples/monai/nuclei_segmentation.py, both setup-config.py files, setup-dsa.sh and create-api-key.sh — several as a fallback when the environment variable is unset. Both .env.example templates ship it as a live default rather than a placeholder, which is the same files that were held up as remediated for the API key. The README presents it as the login to use and nothing prompts for a change.

No licence file, on a repository that vendors and extends Apache-2.0 upstream code while its own plugin manifest declares Apache 2.0. One line to fix, Serdar’s call, not investigated further (AGENTS.md §8).

Current state / open questions

Working platform, dormant repository, and a QC layer that should be treated as unwritten. The split verdict is the useful output: keep the deployment, do not use the local QC tools, and rename them regardless.

  • Rename or delete the two GrandQC-named files, and drop the “Built with GrandQC library” acknowledgement. Still the highest-value item: until it is done, this estate has two different things called GrandQC and no way to tell from a result which one produced it.

    This entry previously said the rename “costs minutes”. That was wrong, and the correction is the useful part. The change was scoped properly on 2026-08-01 — attempted, then reverted unchanged — and the name turns out to be load-bearing in three separate ways. Anyone doing this with a find-and-replace will break something silently.

    1. The plugin wiring is a four-file contract. The CLI key, the dispatch branch and the in-container paths all have to move together or the plugin stops working with no error message: cli_list.json names the CLI and gives absolute paths to the .xml and .py; cli_entrypoint.sh dispatches on $1 matching that key and execs a hard-coded path; the Dockerfile creates /opt/GrandQC, COPYs both files into it, chmods one by name, and ends CMD ["GrandQC"]. Miss the entrypoint and the container answers “Unknown CLI”; miss the Dockerfile and the image builds fine but the paths in cli_list.json point at nothing.

    2. grandqc_analysis is a stored data contract, not a name. It is the key this tool writes into each slide’s Girder metadata (grandqc_integration.py:204), and it is read back in two other places — batch_export.py:245 and pdf_report_generator.py:197. Both readers use meta.get(...) guarded by if, so renaming the writer alone does not raise an error: the QC rows silently vanish from the Excel export and the QUALITY CONTROL METRICS section silently disappears from the PDF report, for every slide analysed before the rename. Those results live in MongoDB, not in git, so no amount of care with the repository protects them. A rename needs the readers to accept the old key as well as the new one, indefinitely.

    There is a subtler half. batch_export.py:87 filters the generic metadata flattener with not key.startswith('grandqc'), specifically to stop the QC dictionary being splayed across the ordinary metadata columns because it is handled separately. Rename the key without updating that string and the filter stops matching, so the QC block gets flattened into the general sheet as well as its own — no error, just duplicated and mangled columns.

    3. Not every mention of GrandQC in the repository should be renamed. Roughly twenty tracked files carry the name and they fall into three groups, only one of which is the problem: the falsely-named local tool (rename); genuine references to the real GrandQC, which must survive — TODO.md:8 still lists “GrandQC integration” as an unfulfilled intention, and docs/architecture/APPLICABLE_CONCEPTS_FROM_TODO2.md and docs/architecture/ZERO_COPY_FILE_ACCESS.md discuss it as an external example pattern; and a third instance of the same false-naming problem, not previously recorded hereDockerfile.complex:34–35 carries the comments “Install GrandQC for quality control” and “GrandQC is a digital pathology quality assessment tool” directly above a pip install of opencv-python-headless, pandas and openpyxl. It installs no GrandQC either.

    One practical note for whoever does it: docs/FEATURES_GUIDE.md and docs/architecture/FEATURES_GUIDE.md are byte-identical duplicates, as are docs/INSTALLATION_SUMMARY.md and docs/setup/INSTALLATION_SUMMARY.md, so every edit to those lands twice. docs/GPU_SETUP.md and docs/setup/GPU_SETUP.md are not identical and have to be read separately — checked, because assuming the pattern held would have been wrong.

    Decided 2026-08-01: documented, not fixed. The repository is left exactly as found, and this wiki carries the warning instead — here, on GrandQC-QuPath, on WSI Quality Control and in wiki/index.md. That is a weaker control than a rename and is worth restating as such: it protects a reader who checks the wiki first, and does nothing for anyone who opens the Analyses menu and trusts the label.

  • Rotate the API key, and change the admin password. The key is in three tracked documents, in three commits of the history, and in one gitignored log file on disk — so deleting lines is not sufficient and .gitignore does not reach the fourth copy. The password is the larger job: 37 tracked files, including two .env.example templates that ship it as a working default.

  • Fix or delete the verification command in the two security documents. As written it greps a non-recursive glob that cannot reach the directory the secrets are in, so it certifies success unconditionally. A check that cannot fail is worse than no check, and this one is currently the sole evidence behind two “removed ✅” claims.

  • The staining check should be removed rather than retuned. Rescaling it to its actual 0–0.25 range would produce a working variance measure, but colour variance is a poor proxy for staining quality in the first place — Stain Quality independently arrives at the published UK NEQAS method on the same estate, and that is what a staining check here should be built from.

  • What would a real QC integration look like? The honest answer from GrandQC Quality Study is that an artefact-segmentation model agrees with technicians barely above chance (κ = 0.10) because roughly 70% of recorded quality problems are preparation geometry the scanner never sees — see Preparation Quality Versus Image Quality. So the useful plugin here is probably not a better focus metric at all, and that is a design question nobody has answered.

  • Was the platform ever used on real cases, and by how many people? Nothing in the repository records usage, and the multi-user capability is the entire argument for keeping it. Unanswered and not derivable from the tree. [unverified]

  • The submodule wiring problem is inherited. Digital Pathology Drafts declares imagebank/imagebank_tmp in .gitmodules with no matching gitlink in the index, so a clone --recursive of the umbrella does not reproduce this working tree.

  • MONAI Label’s model-assisted annotation is the most interesting thing here that this reading did not appraise — the app is upstream sample code and the active-learning strategies were read only structurally. Whether click-to-segment and in-loop retraining actually reduce annotation effort on this estate’s material is unmeasured, and no wiki page covers the topic.

Related: Digital Pathology Drafts — the private umbrella this is a submodule of; its broken submodule declarations include this one, so read its clone warning before setting up elsewhere. Related: GrandQC-QuPath — the group’s real GrandQC wrapper, and the reason the naming collision here is a hazard rather than a cosmetic problem. Related: GrandQC Quality Study — the measurement that says what a QC model can and cannot substitute for, and therefore what this platform’s QC plugin should have been aiming at. Related: WSI Quality Control — the method page these tools belong to, now amended to say which QC implementations in the estate rest on trained models and which do not. Related: Server-Side Slide Platforms — the architectural choice this repository is the estate’s only instance of. Related: De-identification Release Gating — the pattern the secrets remediation here fails in a new way: the check was written but never run against the released bytes. Related: QuPath Tooling — the desktop hub this is the server-side alternative to.

Derived from: repository working tree and full git history read 2026-07-31 at D:/DigitalPathologyDrafts/imagebank (5 commits, 5c7af0e, 119 tracked files) — qc_tool/qc_analysis_cli.py, cli/GrandQC/GrandQC.py, cli/GrandQC/GrandQC.xml, cli/GrandQC/Dockerfile, cli/GrandQC/cli_list.json, grandqc_integration.py, dsa-deployment/docker-compose.yml, dsa-deployment/monailabel/apps/pathology/main.py, docs/security/SECURITY_SETUP_SUMMARY.md, docs/security/SYNC_SUMMARY.md, docs/monai/STATUS.md, README.md, NEWS.md, TODO.md, env.example; git log, git ls-files, git rev-list and per-commit git grep for the credential history; gh repo view and gh api for visibility, forks, licence and push dates. The staining bound was confirmed by running the extracted function against adversarial and realistic inputs rather than by inspection.

Extended 2026-08-01 by scoping the rename against the same tree: cli/GrandQC/ in full (cli_list.json, cli_entrypoint.sh, Dockerfile), the metadata write and read sites in grandqc_integration.py, batch_export.py and pdf_report_generator.py, Dockerfile.complex, TODO.md, and a git grep of every GrandQC mention across all 119 tracked files; the duplicate status of the three docs/ pairs was checked with cmp rather than assumed. The rename was applied and then fully revertedgit status in that repository is empty and every claim on this page describes the tree as it currently stands.

The credential findings were independently refuted-then-confirmed: a reviewer given only the instruction to disprove them corrected two things this page originally got wrong. The verification command in the security documents does not return matches as I first claimed — it is mis-scoped and returns nothing, which is a stronger finding, not a weaker one. And the admin password is not a stock DSA quickstart credential but one bespoke to this deployment, present in 37 tracked files rather than the ten I counted, and never remediated. The reviewer also found the fourth copy of the API key in the untracked MONAI log, which I had missed entirely. Claims about the commit range, the three tracked files, and the repository’s private status survived unchanged.