Reversible anonymization: keep a way back
Some studies need a way back. A collaborator finds something in patient
ANON_... that the treating team must hear about, and someone has to be
able to say who that patient is. Reversible anonymization gives you
that: before de-identifying, Isocenter encrypts each patient's original
identity under a key you hold and writes it into the files.
That way back travels with the data, so this tutorial also covers what you are handing out. It runs a real session over one CT file: lock, then de-identify, export and read the disclosure. It then recovers the identity from the pseudonym, and shows what happens without the key.
Run it yourself
Every Python block on this page runs, in order, as part of Isocenter's test suite, and every output shown is checked.
- Start in a new, empty folder. Each tutorial creates its own
tutorial.dband export folders, and running one in another tutorial's folder changes what it prints. The first block below copies the input file from pydicom intoinput/. - Paste the blocks into a Python prompt or a notebook. In a
.pyscript, put them underif __name__ == "__main__":(why). - In a block with
>>>, type what follows each>>>; the lines under it are what Python prints. A...inside a printed value stands for a part that differs on every run, such as a pseudonym or a UID. - The session also prints progress bars, status lines and
WARNINGlines as it works. They are not shown here.ISOCENTER_SHOW_PROGRESS=0turns the bars off.
import shutil
from pathlib import Path
import pydicom.data
Path("input").mkdir(exist_ok=True)
shutil.copy(pydicom.data.get_testdata_file("CT_small.dcm"), "input")
1. Ingest one patient
>>> summary
IngestSummary(ingested=1, failures=[], declined=0, skipped=0)
>>> [(p.patient_id, p.patient_name) for p in session.store.patients]
[('1CT1', 'CompressedSamples^CT1')]
These are the identifiers the export will remove, and the ones you want a way back to.
2. Load a configuration
This page keeps the policy simple, the DICOM standard's Basic Profile
(PS3.15 Annex E Table E.1-1, 2026c edition) with no changes. Save it as
config.yaml:
De-identify a cohort and read the grade shows how to change the rules and read the grade they give.
3. Lock the identities, then anonymize
Locking has to come before anonymize(): the lock copies each
file's original values into an encrypted token, and after anonymize()
there is no original value left to copy.
session.enable_reversible_anonymization("isocenter.key")
report = session.audit()
locked = session.lock_identities(report)
enable_reversible_anonymization("isocenter.key")names the key file. It does not create one.audit()scans the patients against the policy. Its report is also the list of patients to lock.lock_identities(report)writes an identity token into every instance of those patients: Patient's Name, Patient ID, Birth Date, Sex and Accession Number, encrypted, in the Encrypted Attributes Sequence(0400,0500). The first lock creates the key file, readable only by you (mode 0600).
Check the count: a lock after anonymize() secures nothing
len(locked) is the number of instances that now carry a token.
Called after anonymize(), lock_identities(report) does not
raise. It looks for the report's patients by their original Patient
IDs, and after anonymize() no patient holds them, so it logs one
ERROR line saying the IDs matched no patient, returns an empty
result, and still creates the key file. The count is 0, and the key
opens nothing. (Passing one patient's new ANON_ ID instead raises
RuntimeError.)
The key is a file of its own. Here it sits next to the store; step 6 moves it where it belongs. Keep its contents to yourself: anyone who holds it can read every locked identity. This page never prints it.
Now de-identify and export as usual, with the report last:
session.anonymize(report)
session.export("export", use_compression=False)
session.generate_report("report.md")
use_compression=False writes the pixels as ingested, so this page does
not depend on the JPEG 2000 encoder.
4. What you are handing out
The exported file looks de-identified:
import pydicom
from pathlib import Path
exported = pydicom.dcmread(next(Path("export").rglob("*.dcm")))
It still carries the original identity, encrypted:
That is the point of the lock, and it is also a disclosure. Anyone who
holds this file and the key can recover who the patient is. So
export() says so twice.
First, a warning, printed to the console as the export runs and
written to the log file. By default that is isocenter.log in the
working directory; ISOCENTER_LOG_FILE moves it, and
ISOCENTER_LOG_LEVEL=ERROR leaves this warning out of both
(Environment variables):
def logged_warning(text):
"""The first WARNING line in isocenter.log that contains `text`."""
with open("isocenter.log", encoding="utf-8") as log:
return next(line.split(" - ", 1)[1].strip() for line in log
if " - WARNING - " in line and text in line)
>>> print(logged_warning("re-identifiable"))
WARNING - 1 of 1 exported instances carry encrypted original identities (0400,0500). They are recoverable with the session key; treat the export as re-identifiable by any holder of it.
Each new Session overwrites the log file; copy it if you need it.
Second, a REVERSIBLE_EXPORT row in the store's audit log. The row
is kept in the store for good, and the report counts it in section 2.
These helpers print one row of section 2, and the grade:
def audit_row(path, action):
with open(path, encoding="utf-8") as report_file:
return next(line.strip() for line in report_file
if line.startswith(f"| {action} |"))
def grade_line(path):
with open(path, encoding="utf-8") as report_file:
return next(line.strip() for line in report_file
if "Validation Status" in line)
>>> print(audit_row("report.md", "REVERSIBLE_EXPORT"))
| REVERSIBLE_EXPORT | 1 |
>>> print(grade_line("report.md"))
| **Validation Status** | **PASS** |
The grade is PASS, and it does not contradict the disclosure. The grade
says the run did what your policy asked. The REVERSIBLE_EXPORT row says
who can undo it. Read both before this export leaves. If the recipient
must not be able to re-identify, the key must never reach them.
These helpers read the report's layout, which can change
The action name REVERSIBLE_EXPORT and the grade values PASS and
REVIEW_REQUIRED are frozen for 1.x. The report's layout, the log's
line format, the warning's wording, and a new session overwriting the
log are not
(API stability). If a 1.x release changes it,
this page goes red in Isocenter's own tests and is updated with it.
Keep the pseudonym, as a collaborator would quote it back to you, and close the session:
5. Recover the identity
Weeks later, the collaborator asks about ANON_.... Reopen the store,
enable reversible anonymization with the key the data was locked with,
and recover:
session = Session("tutorial.db")
session.load_config("config.yaml")
session.enable_reversible_anonymization("isocenter.key")
identity = session.recover_patient_identity(pseudonym, restore=False)
A Session opened in a folder that holds a file named isocenter.key
enables reversible anonymization with it on its own. Don't rely on that:
a key kept in the working directory sits beside the store and the
exports, which is where it should not be. The explicit call says which
key you mean, and it is the call you need once the key lives somewhere
safer (step 6).
recover_patient_identity() returns a dict. Each key is the SOP Instance
UID of an instance that carries a token, and each value holds what that
token holds. The first entry speaks for the patient:
>>> len(identity)
1
>>> next(iter(identity)).startswith("2.25.")
True
>>> first = next(iter(identity.values()))
>>> sorted(first)
['0008,0050', '0010,0010', '0010,0020', '0010,0030', '0010,0040']
>>> first["0010,0010"], first["0010,0020"]
('CompressedSamples^CT1', '1CT1')
restore=False only reads. Nothing in the store changes, and the patient
still carries the pseudonym:
With restore=True (the default) the call returns the same dict and
also writes the original values back onto the patient in memory. A later
save() stores them. Use it when you need the identified data back in
the store, not just the answer. The
Quick Start covers what a
restore puts back and what it leaves shifted.
6. Without the key, there is no way back
Store the key apart from the store, for example on another machine or
in a password manager. Here, a vault folder stands in for that place:
Reopen the store and try to recover with no key at the path you give:
session = Session("tutorial.db")
session.load_config("config.yaml")
session.enable_reversible_anonymization("isocenter.key")
>>> session.recover_patient_identity(pseudonym, restore=False)
Traceback (most recent call last):
...
FileNotFoundError: no key file at ...isocenter.key; recovery needs the key the identities were locked with, and does not create one
The store holds the token but not the key, so it cannot open the token.
Recovery never creates a key, because a new key could not open a token
locked under the old one: a mistyped path would only make a useless key.
A key file that exists but is the wrong key raises RuntimeError
instead. Either way, nothing is written.
Point the session at the key's real place, and recovery works again:
session.enable_reversible_anonymization("vault/isocenter.key")
identity = session.recover_patient_identity(pseudonym, restore=False)
What to keep
Reversible anonymization adds a third thing to keep. The three are separate files, and each one does a different job:
| Keep | Because | If you lose it |
|---|---|---|
isocenter.key, apart from the store and never with an export |
It is the only thing that opens the identity tokens. | Nobody can recover any identity locked under it, and no new key helps. |
The store: tutorial.db and tutorial_pixels.bin, together |
It holds the project secret that made the pseudonyms and date offsets, and the patients you recover from. Never send it with an export. | Later exports of the same patients get new pseudonyms, new date offsets and new UIDs, and will not link to this one. The tokens in files already exported can still be opened with the key: ingest those files into a new store, and recover as in step 5. |
config.yaml, under version control |
It is your policy. Reload it every time you reopen the store. | Nothing you cannot write again, but a rewritten file must be the same policy. If it is not, an export before a new audit() writes a WARNING row that stays in the store, and every later report of this store grades REVIEW_REQUIRED. |
Keep them apart. The key opens the tokens in the exported files, so an export shipped with its key is not de-identified at all. The store holds the secret behind every date shift, so it must stay with you as well. The configuration holds no secret, so it is the one of the three you can show a reviewer. The Configuration guide's What to keep is the full table.