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;
}