ScanTools
A third-party command-line toolkit for WSI provenance and redaction — label/macro extraction, in-place .svs label redaction, barcode decoding, pyramid-level hashing, scan-date extraction, and tissue masking.
Purpose
This is not one of my repositories. It is an external tool, recorded here as a candidate, not as adopted infrastructure. Nothing in it has been run on Memorial data as of this review. There is no local_path because it has not been cloned.
The PathologyDataScience org is the Computational and Integrative Pathology Group at Northwestern University (Lee Cooper’s group, pathdata.io). That is already a known quantity here: HistomicsML and HistomicsML2 come from this same org, and QuPath Tooling already indexes HistomicsML for interactive region labelling. The org also publishes NuCLS, BCSS, MuTILs/PanopTILs, HiPS, SCNN and SurvivalNet — so the provenance is good even though this particular repository is small.
It is on this list because it covers three things the wiki currently records as open questions: capturing scanner metadata before anonymisation strips it (Memorial WSI Share), choosing an anonymisation tool (De-identification), and getting a slide-file-side check on scanner log timestamps (Scanner Log Analysis).
Apache-2.0. Created 2021-08-23, last pushed 2026-01-05. Two stars, one fork — a small, low-adoption toolkit from a credible group. Treat it as code to read and borrow from, not as a maintained dependency.
Data used
None yet. Intended inputs are .svs whole-slide images from the Leica fleet, read from Memorial WSI Share.
Format coverage is narrower than “WSI” suggests, and this bounds everything below:
scandateis Aperio.svsonly.deidentifydescribes itself as covering MRXS, NDPI and SVS, but the README says it was tested only on Leica AT2 and GT450 — both of which write Aperio.svs. The MRXS path is broken (see below), so in practice this is an.svstool.dumpwsi,hashwsiandmaskgo throughlarge_image, so they cover whatever that covers.
Which scanner models are actually in the fleet is not recorded anywhere in this wiki [unverified], so what fraction of the archive these tools would reach is not yet known.
Methods
Six CLIs, installed with pip install ./scantools from the repo root, or run from the provided Docker image. Dependencies are large_image + large_image_source_tiff, tifffile, scikit-image, opencv-python, numpy, tqdm, and — for barcode only — the zxing Python wrapper, which shells out to Java ZXing and therefore needs a JVM on PATH.
Flags below were read from the argparse blocks in the source, not from the README, because the README’s stated defaults are wrong in three places. Where they differ, the code is what runs.
| Tool | What it does | Key flags (verified against source) |
|---|---|---|
dumpwsi |
Writes label / macro / thumbnail images to a mirrored output tree | -e ext (.svs), -l/-t/-m which images, -s skip existing, -f format (jpg), -w workers (16) |
deidentify |
Redacts label + macro in place | -d/--debug only. Uses optparse; takes file arguments, not a folder |
barcode |
Decodes Aztec / Data Matrix from label crops to CSV | -e ext (_label.jpg, not _label.png as the README says), -c calibration (512), -w workers (8) |
hashwsi |
Hashes one pyramid level to CSV | -a algorithm (sha256, not sha1 as the README says), -l level (2), -m min encoded bytes (0), -w workers (16) |
scandate |
Extracts scan date from .svs to CSV |
-w workers (16) |
mask |
Otsu + HSV tissue mask to a mirrored output tree | -m magnification (1.25), -l holes (500 px), -o objects (500 px), -s saturation (0.8), -v value (0.4), -f format (png, not jpg as the README says), -n overwrite, -w workers (16) |
Defaults with no flags: dumpwsi writes only the label image as .jpg, named <original-filename-with-extension>_label.jpg. That matches barcode’s real _label.jpg default, so the two chain correctly out of the box — following the README here would break the chain.
How deidentify actually redacts. It is adapted from anonymize-slide.py (Carnegie Mellon / Benjamin Gilbert), with macro redaction added. For a TIFF-family slide it opens the file r+b, zeroes every strip byte range belonging to the label and macro directories, then unlinks those directories from the IFD chain. Pixel bytes are genuinely overwritten with zeros, so this is real redaction, not a hidden pointer. The file is not truncated, so its size is unchanged.
Current state / open questions
scandate is broken as committed, and the failure is silent. svs_date() returns one value; line 82 of scandate.py unpacks it into two (result, read_level = future.result()), copied from hashwsi.py where the worker really does return a pair. Every file therefore raises, the bare except Exception prints the error and continues, and the run finishes with exit code 0, a full progress bar, and a CSV containing only the header row. Three independent adversarial checks agreed on this at high confidence. The fix is one line.
scandate cannot recover the time of a scan, only the date. It filters the pipe-delimited Aperio ImageDescription for fields containing the substring Date. The real header carries Time and Time Zone as separate fields, which that filter structurally cannot match. This is the ceiling on its usefulness for Scanner Log Analysis — it can cross-check dates, not hour-of-day. Extending it to capture Time/Time Zone is a small change to the same comprehension and would be the higher-value fix.
The date format is also not settled. The docstring claims dd/mm/yyyy, while Aperio ScanScope firmware is generally documented as writing US MM/DD/YY — the tool’s own documentation disagrees with the format it is most likely to meet. (A DD/MM/YYYY header does circulate in public examples, but the one found here came from a Motic-written .svs rather than an Aperio scanner, so it says nothing about Leica output. [unverified]) Both orderings parse without error and differ silently for days 1–12. The format must be confirmed against an actual Memorial file before any extracted date is trusted.
deidentify is destructive with no safety net. It modifies files in place, with no backup, no temp-file-and-rename, no dry-run flag, and no fsync. Three specific hazards:
- MRXS is broken on Python 3 —
_write()and_zero_record()writestrinto binary-mode handles, raisingTypeError. Worse, in the truncate branch the datafile and index are irreversibly modified before the crash, leavingSlidedat.inidesynchronised; a naive re-run can then zero an unrelated image on the same slide. Do not point this at 3DHISTECH files. - SVS partial redaction — the code requires both a label and a macro directory. If only one is present, that one is fully redacted and flushed, and then an
IOErroris raised. The file is left durably half-redacted while the operator sees what looks like a plain failure. - The README’s own caveat: a scan can rarely burn PHI into the image pyramid itself, which this tool cannot detect or remove.
utils.parse_input drops files silently. Text-file input is split on whitespace and the first token taken, so any path containing a space is truncated. Extension matching is case-sensitive in load_source but OS-dependent in the rglob, so .SVS files can be silently skipped. A slide dropped from the list is a slide that never gets redacted or hashed while the run reports success — the count printed by the progress bar must be reconciled against the expected slide count.
barcode fails silently without a JVM. The Java check at import time only prints a message; every decode then fails through a bare except, producing a complete-looking CSV with empty contents. Also, -a and -d are no-ops — both are declared store_true with default=True, so neither symbology can be switched off from the CLI.
hashwsi hashes a re-encoded rendition, not raw bytes. It renders one large_image pyramid level, JPEG-encodes it at quality 95, and hashes that. This is what makes the hash invariant to metadata edits and to label/macro redaction — which is the useful property — but it also means the hash is only reproducible within a fixed library and encoder version, so it is an integrity check within one environment, not a portable checksum. Note also that large_image numbers levels with 0 = most downsampled, the opposite of OpenSlide.
Open questions for a next review: does the fleet write anything other than Aperio .svs; what date format do Memorial .svs headers actually use; and is a fork worth maintaining given that the two fixes wanted (scandate return value, Time/Time Zone capture) are both small.
Related: De-identification — this is a sixth candidate tool for a decision that page records as still unmade, and it is the only one of them that also extracts the metadata stripping would destroy.
Derived from: repository source read at commit-level on 2026-07-25 — scantools/{scandate,deidentify,barcode,hashwsi,mask,dumpwsi,utils}.py and README.md.