Anonymisation of DICOM Objects
DicomObjects is commonly and successfully used for Anonymisation and all the required data changes are easy using DicomObjects - the real problem is working out what needs changing! This subject is a real “Pandora’s box”, and whilst changing IDs, names etc. is easy, the more you look into it, the more “Hidden” patient information you find
e.g.:
- dates and times of examinations and accession numbers buried within UIDs
- private data [who knows what’s in there?] - see Removing Private Attributes
- Study date and time + institution + referring doctor…would that be enough to identify someone?
UIDs
If you change UIDs, then to keep studies and series together, you need to do so consistently, which may require a database of previously “seen” UIDs together with their replacements. You have to consider whether you’d need to change referenced image lists (e.g. localisers) to match.
DicomObjects.NET version
Below is the sample anonymiser routine that can be found in our sample viewer program.
private static string HashNameAndID(string name, string id)
{
MD5 md5 = MD5.Create();
string s = name.Trim() + id.Trim();
byte[] inputBytes = Encoding.ASCII.GetBytes(s);
byte[] hash = md5.ComputeHash(inputBytes);
// convert byte array to hex string
StringBuilder sb = new StringBuilder();
for (int i = 0; i < hash.Length; i++)
sb.Append(hash[i].ToString("X2"));
return sb.ToString().Substring(0, 8);
}
static List<Keyword> ItemsToAnonymise = new List<Keyword>()
{
Keyword.OtherPatientIDs, Keyword.PatientAddress, Keyword.CountryOfResidence,
Keyword.PatientTelephoneNumbers ,Keyword.MilitaryRank, Keyword.BranchOfService
};
public static DicomDataSetCollection Anonymise(string[] filesToAnonymise)
{
Dictionary<string, string> UIDCache = new Dictionary<string, string>();
DicomDataSetCollection results = new DicomDataSetCollection();
Random random = new Random();
string AccessionNumber = random.Next(1000000).ToString();
string ReplacementValue = "Anonymised";
// offset all dates by fixed amount (this leaves relative dates and ages correct
TimeSpan dateoffset = new TimeSpan(random.Next(730) - 365, 0, 0, 0);
foreach (string file in filesToAnonymise)
{
if (!DicomGlobal.IsDICOM(file))
continue;
DicomDataSet ds = new DicomDataSet(file);
string hash = HashNameAndID(ds.Name, ds.PatientID);
// Remove all private attributes
ds.RemovePrivateAttributes();
// replace all UIDs, except for known UIDs (i.e SOP Class UIDs)
foreach (var attr in ds.Where(a => a.VR == "UI"
&& a.ExistsWithValue
&& !(a.Value.ToString().StartsWith("1.2.840.10008."))).ToList())
{
string UID = attr.Value as string;
if (!UIDCache.ContainsKey(UID))
UIDCache.Add(UID, DicomGlobal.NewUID());
ds.Add(attr.KeywordCode, UIDCache[UID]);
}
// Replace all Names
foreach (var attr in ds.Where(a => a.VR == "PN" && a.ExistsWithValue).ToList())
ds.Add(attr.KeywordCode, ReplacementValue);
// Replace all dates
foreach (var attr in ds.Where(a => a.VR == "DA" && a.ExistsWithValue).ToList())
ds.Add(attr.KeywordCode, (attr.Value as DateTime?)?.Add(dateoffset));
// Anonymise some Patient Level Attributes
ds.Add(Keyword.PatientName, hash);
ds.Add(Keyword.PatientID, hash);
ds.Add(Keyword.AccessionNumber, AccessionNumber);
foreach (Keyword k in ItemsToAnonymise)
if (ds[k].ExistsWithValue)
ds.Add(k, ReplacementValue);
// Try to add 4 white blocks around the image corners by modifying the pixel data
// Designed to work with single frame images
if (ds[Keyword.PixelData].Exists)
{
ushort bits = (ushort)ds[Keyword.BitsAllocated].Value;
ushort rows = (ushort)ds[Keyword.Rows].Value;
ushort columns = (ushort)ds[Keyword.Columns].Value;
// set value to mid-scale
int whiteValue = (1 << (bits - 1));
float blockSize = 0.2F; // 20% of the image
var pixels = ds[Keyword.PixelData].Value;
if (pixels is ushort[,,])
WriteBlockForAllFormats(ds, rows, columns,
(ushort)whiteValue, blockSize, (ushort[,,])pixels);
else if (pixels is byte[,,])
WriteBlockForAllFormats(ds, rows, columns,
(byte)whiteValue, blockSize, (byte[,,])pixels);
ds.Add(Keyword.PixelData, pixels);
}
results.Add(ds);
}
return results;
}
private static void WriteBlockForAllFormats<T>(DicomDataSet ds, int rows, int columns,
T whiteValue, float blockSize, T[,,] pixel)
{
//colour
if ((ushort)ds[Keyword.SamplesPerPixel].Value == 3)
{
// by colour
if ((ushort)ds[Keyword.PlanarConfiguration].Value == 0)
{
MakeBlocks(rows, columns * 3, whiteValue, blockSize, pixel, 0);
}
else // by plane
{
// like 3 mono images concatenated
MakeBlocks(rows, columns, whiteValue, blockSize, pixel, 0);
MakeBlocks(rows, columns, whiteValue, blockSize, pixel, rows);
MakeBlocks(rows, columns, whiteValue, blockSize, pixel, rows * 2);
}
}
else
{
// mono or palette (palette will give arbitrary colour, but will still be obscured)
MakeBlocks(rows, columns, whiteValue, blockSize, pixel, 0);
}
}
private static void MakeBlocks<T>(int rows, int columns,
T whiteValue, float blockThickness, T[,,] temp, int rowoffset)
{
// top left
for (int x = 0; x < columns * blockThickness; x++)
for (int y = 0; y < rows * blockThickness; y++)
temp[x, y + rowoffset, 0] = whiteValue;
//top right
for (int x = columns - 1; x > columns * (1 - blockThickness); x--)
for (int y = 0; y < rows * blockThickness; y++)
temp[x, y + rowoffset, 0] = whiteValue;
//bottom left
for (int x = 0; x < columns * blockThickness; x++)
for (int y = rows - 1; y > rows * (1 - blockThickness); y--)
temp[x, y + rowoffset, 0] = whiteValue;
// bottom right
for (int x = columns - 1; x > columns * (1 - blockThickness); x--)
for (int y = rows - 1; y > rows * (1 - blockThickness); y--)
temp[x, y + rowoffset, 0] = whiteValue;
}