anonym-qupath
A QuPath menu for anonymising whole-slide images that, as written, does not remove the slide label, does not remove the identifying values it targets, and deletes the original file whether or not any of it worked — do not run it on clinical material.
Purpose
The intent is a one-click Extensions → Anonymization Tools command inside QuPath that strips PHI from a project’s slides, renames them, and keeps a lookup table. That is a real need — De-identification records that the group’s anonymisation SOP is still a v0.8 draft and that tool choice is unsettled.
The verdict after reading the source on 2026-07-26 is that this repository does not meet that need and should not be pointed at anything irreplaceable. It is three days of drafting (three commits, 9–11 June 2025, nothing since) that reached a state where the menus look finished and the operations underneath them do not work. Everything below was confirmed twice, the second time by a reader instructed to refute it.
The repository is private, and QuPath scripts only run when a person opens the Script Editor and clicks Run, so nothing here has harmed anything yet. The severity is in what happens the first time someone follows the guide.
Data used
project.qpproj carries 18 image entries and none of them are patient material: nine kontrol*/KONTROL*.svs QC slides from an E:/KONTROLHE_2025/ staining-control series, seven already-pseudonymised ANON*.svs annotation-demo slides, and two single diagnostic test slides (ONC1.svs, RCC1.svs). Worth stating explicitly because a committed .qpproj is exactly what QuPath Tooling’s convention warns about — here the convention was not broken.
Methods
The pipeline is Groovy for the UI and Python for the work, communicating by subprocess and files. Five overlapping scripts implement four divergent versions of the same workflow, which is itself the finding: nothing was ever chosen.
What the installer actually wires up. InstallAnonymizationMenu.groovy — the step ANONYMIZATION_GUIDE.md tells users to run — installs exactly AnonymizeWorkflow.groovy and registers it as the menu command. So the defects below are not in dead code; they are in the one path the documentation leads to. CLAUDE.md separately recommends AnonymizeWorkflow_Compatible.groovy, which the installer never touches and which carries the same defect anyway, so both plausible routes fail the same way.
The redaction removes the wrong half of the string. anonymize_wsi.py is 27 lines and its operative loop is description.replace(field, "REDACTED") over ["Filename", "Title", "Patient", "Case", "Barcode", "ID"]. Those are field names. Applied to a TIFF ImageDescription reading Patient: John Doe, it produces REDACTED: John Doe — the label is destroyed and the identifier survives. This is not a gap in coverage, it is the operation inverted, and it is the single most important line on this page.
The label image is never removed. The same script does an unconditional for page in tiff.pages: out.write(page.asarray(), ...) with no page filtering of any kind, so the label and macro images are copied through intact. ANONYMIZATION_GUIDE.md line 11 advertises “Label Image Removal: Removes embedded slide label images containing handwritten/printed PHI” as core functionality. Label-stripping code does exist, in enhanced_anonymize_wsi.py behind an _is_label_page() heuristic that calls any page past index 2 or under 2000×2000 a label — but the installed workflow never reaches it, and AnonymizeWorkflow.groovy’s anonymizationScript field defaults to null with no dialog that can set it, so it always falls back to inline Python with the same no-filter behaviour. That matters for a reason De-identification states directly: the label image is a photograph of the accession number, and renaming the file does not touch it.
It fails open, by explicit code, and then reports success. In AnonymizeWorkflow.groovy, when metadata stripping fails the handler is Files.copy(originalFile.toPath(), tempFile.toPath(), REPLACE_EXISTING) — the unmodified original becomes the file that is renamed into place as the anonymised output, the true original is deleted, and processingLog[...] = "Success" is written. The audit trail states the opposite of what happened. AnonymizeWorkflow_Compatible.groovy is identical. AnonymizeWorkflow_MIA.groovy shares the copy-fallback and the false “Success” but, by default, renames the original to original_<name> instead of deleting it.
One path risks losing the slide entirely. AnonymizeProject.groovy starts the Python subprocess, never reads the exit code from proc.waitFor(), then deletes the original. Since anonymize_wsi.py contains no try/except at all, any error is an uncaught traceback with no output file written — and the original is deleted anyway. Its default pyScriptPath = "/path/to/anonymize_wsi.py" is an unedited placeholder, so out of the box this path fails every time and deletes every time.
The reversibility feature does not exist. MIA_INTEGRATION.md documents a de-anonymise workflow; _restore_metadata() logs “Metadata restoration not yet implemented”, copies the still-anonymised file to the output path, and returns True. The generated restore_anonymization_*.py is a print() stub. SIMILAR_WORKS_ANALYSIS.md is a survey of other tools ending in a block of proposed SafeAnonymizer/ReversibleAnonymizer code that exists nowhere in the repository — readable as implemented behaviour if skimmed.
Nothing checks any of it. No tests, no CI, no assertion anywhere that anonymisation removed anything. There is no LICENSE file, and GitHub reports none.
Current state / open questions
Paused rather than archived: the need is real and the QuPath menu scaffolding is reusable. What is not reusable is the anonymisation logic, which should be replaced wholesale by a tool that already works — De-identification lists the candidates, and QuPath Atölye Extension’s wizard is the group’s own working version of the metadata half, notable for stating its own ceiling honestly instead of overclaiming.
Three things to fix before this is touched again, in order: make failure abort instead of copying the original; delete the original only after verifying the replacement exists and differs; and redact values rather than field names.
- Has this ever been run against a real slide? The
.qpprojsays no and the commit history is three days long, but the tool deletes originals and nothing would record it if it had.[unverified]— only the user can settle this, and it is worth settling before anything else on this page. - Does any other group tool inherit this code? The redaction list here differs from QuPath Atölye Extension’s, so they appear to be independent drafts rather than a shared lineage.
[unverified]
Related: De-identification — this is the third fail-open de-identification control found in the estate, and the first where the redaction step is also inverted. Related: QuPath Atölye Extension — its anonymisation wizard is the working counterpart and the code that should have been reused here. Related: Digital Pathology Drafts — the umbrella repository this sits in as a submodule. Related: QuPath Tooling — the index that should have caught the duplicate effort.
Derived from: repository source read 2026-07-26 — anonymize_wsi.py, scripts/AnonymizeProject.groovy, scripts/AnonymizeWorkflow.groovy, scripts/AnonymizeWorkflow_Compatible.groovy, scripts/AnonymizeWorkflow_MIA.groovy, scripts/InstallAnonymizationMenu.groovy, scripts/enhanced_anonymize_wsi.py, scripts/enhanced_anonymize_mia.py, ANONYMIZATION_GUIDE.md, MIA_INTEGRATION.md, SIMILAR_WORKS_ANALYSIS.md, CLAUDE.md, project.qpproj; GitHub API for visibility and licence state, read 2026-07-26. Every code finding independently re-verified by a second reading instructed to refute it.