RadVeil
Open-source anonymization for head MRI & CT

The face leaves.
The data stays.

RadVeil finds the face in a head scan the way an attacker would — it renders the volume as a portrait and runs a face detector on it — then removes it, and accepts the result only when that detector fails on the output. Ears too, on request. Every voxel outside the mask stays bit-exact.

$pip install radveil
v0.1 · early release Limited validation so far — what that means

Image data only. Metadata (names, IDs, dates in headers, sidecars or DICOM tags) is not touched — scrub it separately.

Surface render of the MRHead sample before anonymization The same head after adaptive anonymization inputadaptive
How it works

Localize like an attacker. Verify like one too.

No atlas, no registration. The tool that finds the face is the same class of tool that has to fail on the result — that symmetry is the whole design.

  1. Render

    A CPU ray caster draws portrait views of the volume, plus a per-pixel map of 3D scanner coordinates. LPS, RAS, oblique or wrong affines are all handled.

  2. Detect

    An off-the-shelf 2D face detector, built in and run in pure Rust, looks at the renders — the attack shared data has to survive.

  3. Lift

    Each 2D landmark becomes a millimetre-accurate 3D point via the position map. Views must agree in 3D, or they are rejected.

  4. Rewrite

    Face-aligned regions, scaled from the measured inter-eye distance, are blurred, cut, blocked or adaptively averaged. Nothing else changes.

  5. Verify

    The result is re-rendered and re-attacked. Still detected? Escalate, up to an opaque fill that ends it by construction.

Measured, not promised

Numbers from the evaluation

On public data. This is an early release: large, diverse validation data was not available, so review outputs before sharing data.

0/201
scans where the face region touched the expert brain mask (median clearance 6.6 mm)
0/201
faces still detected after adaptive anonymization with verification on
106/106
hand-marked pinnae covered (≥95% of the ear surface) on 53 IXI subjects
~1 s
per subject end-to-end, verification included, on a laptop CPU (MacBook Pro, Apple M4 Pro, 14 cores)
bit-exact
every voxel outside the exported mask; datatype, scaling and header kept

Face figures: the public SynthStrip evaluation set (582 scans of many contrasts, with expert brain masks), radveil auto, mode="adaptive". RadVeil processed 201; the rest were refused, mostly scans without a visible face (already defaced, or a field of view that ends above it) and non-structural series. Ear figures: 106 pinnae hand-marked on 53 public IXI scans; the ear cut modified 0 brain voxels on the 50 IXI T1 with expert brain masks.

Anonymization modes

Four ways to take a face apart

Pick a mode to see the surface render and the voxels themselves — a mid-sagittal slice above an axial slice through the orbits, with the region outlined in amber.

Input render of the MRHead sample
input
adaptive result on the MRHead sample
adaptive
Slices after adaptive: gradient-weighted average
voxels · gradient-weighted average

3D Slicer’s public “MRHead” sample, donated by the person pictured for use without restriction, run through radveil auto with default settings. On this head verification escalated blur and adaptive twice before no face was found; verified cut still read as a face and was escalated to a fill, so it is shown with verification off.

Regions

The ear is a fingerprint too

Pinna shape is stable over a lifetime and matchable from a surface reconstruction. Ear removal is opt-in — region defaults to face — and can use its own mode in the same pass (ear_mode="cut" with an adaptive face).

  • Measured, not assumed

    No landmark model emits ear points, so RadVeil turns the volume into a surface height map and finds each pinna as a protrusion above the scalp baseline.

  • Skin-deep at the scalp

    The medial wall follows the measured scalp and stops at the skull — the whole pinna goes, the brain is not reached.

  • Tells you when it can’t

    An ear outside the field of view is reported as detected: false with a flagged fallback box — never silent.

Side render of the MRHead sample: the pinna is clearly visible
input · pinna intact
Side render after face+ears anonymization: the pinna is cut away down to the scalp
face+ears · ears cut
Axial slice through both pinnae of the input
axial, through the pinnae
The same axial slice after the ear cut: both pinnae removed, region outlined
both pinnae removed
Mid-sagittal slice of the MRHead sample: the extracted brain outlined in blue, the anonymization region in amber, separated by several millimetres
brain   region  ·  0 brain voxels touched, 5.0 mm clearance
Brain safety

Takes the face, leaves the brain

The face region follows the measured skin, about 4.5 mm deep, so it stays above the skull. A field-of-view cut that merely looks like a face is refused rather than cut.

  • Brain guard

    --brain-guard extracts the brain from the input and never modifies it or anything within 3 mm of it. On the SynthStrip set: 0 scans touched, closest change 3.6 mm.

  • Brain check

    --brain-check reports how many brain voxels changed and how close the region came; the CLI exits with status 3 if any did.

  • Brain extraction

    radveil deskull: SynthStrip ported to pure Rust (any MRI contrast, CT), a classical model-free method, or your own network. Mean Dice 0.969 on the SynthStrip set.

Everything it does

Built for pipelines that can’t afford surprises

Refuses instead of guessing

No plausible face, an already-defaced scan, or a “face” on a field-of-view cut: it raises and writes nothing. Scans that end at the nose opt in with --partial-face.

Any orientation, MRI and CT

LPS, RAS, oblique and wrong affines (a six-direction sweep finds the face). Air, not 0, fills CT. Datatype, scaling and header are kept.

Auditable mask

Export the exact anonymization mask; every voxel outside it is bit-identical. anonymization_mask returns the region without changing anything.

Two independent detectors

A built-in detector (Apache-2.0 models), optional dlib with its CC0 5-point model as an alternative or fallback_detector, or plug in your own.

Batch and services

deface_batch on threads (~11 scans/s on 14 cores); bytes in, bytes out with Volume.from_bytes for services.

Tunable verification

Re-attacked from every detected yaw and tilted 15°; --verify-effort trades speed against attack angles. Opt-ins for chin, lower lip and untouched background air.

Quickstart

Three lines to a verified deface

One Rust core sits behind every interface, so parameters and reports are the same wherever you call it from.

import radveil

report = radveil.deface_auto("t1.nii.gz", "t1_defaced.nii.gz", mode="adaptive")
# {'views_detected': 3, 'residual_detections': 0, 'eye_dist_mm': 63.1, ...}

# ears are biometric too: opt in per call, face-only stays the default
report = radveil.deface_auto("t1.nii.gz", "t1_defaced.nii.gz", region="face+ears")
# rois: [{'label': 'ear_left', 'detected': True, 'protrusion_mm': 14.0}, ...]
!

Refusal is a feature. On non-head data, already-defaced scans, or anything where no plausible face survives cross-view consensus, RadVeil raises instead of writing output — a defacer that guesses is a defacer that silently corrupts. Treat refusals as items for human review.

i

Only the volumetric image data is anonymized — no metadata. RadVeil changes voxels and nothing else. Patient names, IDs, dates and site details in the NIfTI header and extensions, JSON sidecars or DICOM tags are kept exactly as they were. Remove them with your own de-identification tooling before you share data.

Citation

If it saved you a week, cite it

Defacing is a methods detail reviewers increasingly ask about. Naming the tool and version makes your anonymization reproducible.

@software{radveil,
  author  = {Gródek, Piotr},
  title   = {{RadVeil: CPU-only face and ear anonymization
              for volumetric head imaging}},
  url     = {https://github.com/rAum/radveil},
  version = {0.1.1},
  year    = {2026}
}

A methods sentence you can paste

“Facial and auricular surfaces were removed with RadVeil v0.1.1 (region="face+ears", mode="adaptive"), which verifies each output by re-rendering the volume and re-running the face detector; all voxels outside the anonymization mask are unchanged.”

No install needed

Drop a NIfTI file, watch the face go

The demo runs the same Rust core compiled to WebAssembly. Nothing is uploaded; close the tab and it is gone.