/* Walter O'Dell PhD 9/22/03 this plugin acts to change the DICOM header information to remove identifying info: patient name, patient birth date, patient medical ID number This is known to some as the 'anonymous-ing' operation. The patient name field will be replaced by the argument passed in by the GUI. This is intended so that you can replace the identifying patient name field with a generic, yet useful value such as 'patient 1', patient 2', ... Requirements(^*): 1. EndsWithFilter.class (*.java file included with package) Installation Procedure: 1. put the EndsWithFilter.java and DicomRewriter_.java files into the source/plugins folder (or whereever you have your plugins) 2. run ImageJ and in the plugins menu first compile the EndsWithFilter.java and then the DicomRewriter_.java Execution Procedures: 1. Run ImageJ and you can then directly run the DicomRewriter from the plugins menu. 2. go to the directory holding your dicom image files and click on any of the files in there that has the correct extension for your dicom images (note: the EndsWithFilter acts to select from the directory only the files in there there with the same extension as the one first selected, in case there are other extraneous files that you do not want corrupted or that would give the program a hard time.) 3. A GUI pops up to allow you to change the patient Name, Birth Date, and ID# fields. I did not bother to make the program be able to add additional bytes to the Dicom header (nor remove them), thus if the string you enter is longer than the original string value for the field of interest, the trailing characters of that string will be truncated to fit the space limits in the original header. Thus, if in the original images the field contained no data, then putting in values into the Gui text field will have no effect. 4. The corrected files are rewritten over the original image files. I have not had any problems with the test cases I have tried these on, but to be extra cautious, you really want to not loss the data perhaps you should backup the files before writing over them. 5. If you find that you made a mistake/typo in the name, birthdate and id# fields, you can always rerun the program on the same image files. Note that the field sizes were not changed from those of the original fields, thus if the first time you used a shorter name than the original, then the second time you can go back to a longer name (you are still limited by the original field size though). Coding notes: 1. the EndsWIthFilter is a good thing. On my home system I modified the FolderOpener class to always use the EndsWithFilter when I read in a series of images. 2. This set of functions and classes obviously comes from the DicomDecoder class and related components. Much of this code could be omitted if in future versions of ImageJ the DicomDecoder and DicomDictionary classes had as 'public' rather than 'private' many of their attributes. On my home system the DicomRewriter class is hardwired into the main ImageJ code (i.e. not coded as a plugin) and defined as an extension of the DicomDecoder class and the code made much more succinct. */ import java.io.*; import java.util.*; import ij.*; import ij.io.*; import ij.gui.*; import ij.util.Tools; import ij.measure.Calibration; import ij.plugin.*; public class Dicom_Rewriter implements PlugIn { private static final int TRANSFER_SYNTAX_UID = 0x00020010; private static final int PIXEL_DATA = 0x7FE00010; private static final int AE=0x4145, AS=0x4153, AT=0x4154, CS=0x4353, DA=0x4441, DS=0x4453, DT=0x4454, FD=0x4644, FL=0x464C, IS=0x4953, LO=0x4C4F, LT=0x4C54, PN=0x504E, SH=0x5348, SL=0x534C, SS=0x5353, ST=0x5354, TM=0x544D, UI=0x5549, UL=0x554C, US=0x5553, UT=0x5554, OB=0x4F42, OW=0x4F57, SQ=0x5351, UN=0x554E, QQ=0x3F3F; private static Properties dictionary; protected String directory, fileName; // WO was private protected static final int ID_OFFSET = 128; //location of "DICM"; WO was private private static final String DICM = "DICM"; protected BufferedInputStream f; // WO was private protected int location = 0; // WO was private private boolean littleEndian = true; protected int elementLength; // WO was private private int vr; // Value Representation private static final int IMPLICIT_VR = 0x2D2D; // '--' private byte[] vrLetters = new byte[2]; private int previousGroup; private StringBuffer dicomInfo = new StringBuffer(1000); private boolean dicmFound; // "DICM" found at offset 128 protected boolean oddLocations; // WO was: private // one or more tags at odd locations protected boolean bigEndianTransferSyntax = false; // WO was: private // WO these added private static final int PATIENTS_NAME = 0x00100010; private static final int PATIENT_ID = 0x00100020; private static final int PATIENTS_BIRTH_DATE = 0x00100030; String curName, curBD, curID; int patientName_loc = 0, patientID_loc = 0, patientBD_loc = 0; int patientName_len = 0, patientID_len = 0, patientBD_len = 0; public void run(String arg) { // invoke open folder dialog OpenDialog od = new OpenDialog("Open Dicom...", arg); String directory = od.getDirectory(); String fileName = od.getFileName(); if (fileName==null) return; IJ.showStatus("Opening: " + directory + fileName); initDicomRewriter(directory, fileName); } // end run() public void initDicomRewriter(String directory, String fileName) { this.directory = directory; this.fileName = fileName; if (dictionary==null) { dictionary = getDictionary(); } //IJ.register(DICOM.class); curName = new String(" "); curBD = new String(" "); curID = new String(" "); runGui(); } public void runGui() { // get curName, curID, curBD values from first image header FileInfo localFI = getNcatchFileInfo(); int dotIndex = fileName.lastIndexOf("."); if (dotIndex<0) { IJ.showMessage("DICOM Rewriter", "The selected file does not have an extension."); return; } String selected_file_ext = fileName.substring(fileName.lastIndexOf(".")); // file ext includes the '.', else use substring(IndexOf(".") +1); GenericDialog gd = new GenericDialog( "Dicom Header Adjuster", IJ.getInstance()); gd.addMessage("Enter new values for patient name, birth date, and medical ID:"); gd.addStringField("prev Name: "+curName, "patient 1"); gd.addStringField("prev birthdate: "+curBD, ""); gd.addStringField("prev ID: "+curID, ""); gd.addCheckbox("change for all *."+selected_file_ext+" files ",true); gd.showDialog(); while (gd.wasCanceled()) { //IJ.write(" action canceled in dicomRewriter -- no changes written"); return; } String newName = gd.getNextString(); String newBD = gd.getNextString(); String newID = gd.getNextString(); FilenameFilter extfilter = new EndsWithFilter(selected_file_ext); // clever way to only list the files with the proper extension String[] list = new File(directory).list(extfilter); if (list==null) return; ij.util.StringSorter.sort(list); if (IJ.debugMode) IJ.log("FolderOpener: "+directory+" ("+list.length+" files)"); if (gd.getNextBoolean()) for (int i=0; i>>8); return lut; } int getLength() throws IOException { int b0 = getByte(); int b1 = getByte(); int b2 = getByte(); int b3 = getByte(); // We cannot know whether the VR is implicit or explicit // without the full DICOM Data Dictionary for public and // private groups. // We will assume the VR is explicit if the two bytes // match the known codes. It is possible that these two // bytes are part of a 32-bit length for an implicit VR. vr = (b0<<8) + b1; switch (vr) { case OB: case OW: case SQ: case UN: // Explicit VR with 32-bit length if other two bytes are zero if ( (b2 == 0) || (b3 == 0) ) return getInt(); // Implicit VR with 32-bit length vr = IMPLICIT_VR; if (littleEndian) return ((b3<<24) + (b2<<16) + (b1<<8) + b0); else return ((b0<<24) + (b1<<16) + (b2<<8) + b3); case AE: case AS: case AT: case CS: case DA: case DS: case DT: case FD: case FL: case IS: case LO: case LT: case PN: case SH: case SL: case SS: case ST: case TM:case UI: case UL: case US: case UT: case QQ: // Explicit vr with 16-bit length if (littleEndian) return ((b3<<8) + b2); else return ((b2<<8) + b3); default: // Implicit VR with 32-bit length... vr = IMPLICIT_VR; if (littleEndian) return ((b3<<24) + (b2<<16) + (b1<<8) + b0); else return ((b0<<24) + (b1<<16) + (b2<<8) + b3); } } int getNextTag() throws IOException { int groupWord = getShort(); if (groupWord==0x0800 && bigEndianTransferSyntax) { littleEndian = false; groupWord = 0x0008; } int elementWord = getShort(); int tag = groupWord<<16 | elementWord; elementLength = getLength(); // hack needed to read some GE files // The element length must be even! if (elementLength==13 && !oddLocations) elementLength = 10; // "Undefined" element length. // This is a sort of bracket that encloses a sequence of elements. if (elementLength==-1) elementLength = 0; return tag; } // WO made 'public' public FileInfo getNcatchFileInfo() { FileInfo localFI = null; try { localFI = getFileInfo(); } catch (Exception e) { IJ.showMessage("Dicom Rewriter", "Error opening "+fileName+"\n \n\""+e.getMessage()+"\""); } return localFI; } public FileInfo getFileInfo() throws IOException { long skipCount; location = 0; // WO reset for each pass through getFileInfo FileInfo fi = new FileInfo(); int bitsAllocated = 16; fi.fileFormat = fi.RAW; fi.fileName = fileName; fi.directory = directory; fi.width = 0; fi.height = 0; fi.offset = 0; fi.intelByteOrder = true; fi.fileType = FileInfo.GRAY16_UNSIGNED; int samplesPerPixel = 1; int planarConfiguration = 0; String photoInterpretation = ""; f = new BufferedInputStream(new FileInputStream(directory + fileName)); if (IJ.debugMode) { IJ.log(""); IJ.log("DicomDecoder: decoding "+fileName); } skipCount = (long)ID_OFFSET; while (skipCount > 0) skipCount -= f.skip( skipCount ); location += ID_OFFSET; if (!getString(4).equals(DICM)) { f.close(); f = new BufferedInputStream(new FileInputStream(directory + fileName)); location = 0; if (IJ.debugMode) IJ.log(DICM + " not found at offset "+ID_OFFSET+"; reseting to offset 0"); } else { dicmFound = true; if (IJ.debugMode) IJ.log(DICM + " found at offset " + ID_OFFSET); } boolean inSequence = true; boolean decodingTags = true; while (decodingTags) { int tag = getNextTag(); if ((location&1)!=0) // DICOM tags must be at even locations oddLocations = true; String s; switch (tag) { case TRANSFER_SYNTAX_UID: s = getString(elementLength); if (s.indexOf("1.2.4")>-1||s.indexOf("1.2.5")>-1) { f.close(); String msg = "ImageJ cannot open compressed DICOM images.\n \n"; msg += "Transfer Syntax UID = "+s; throw new IOException(msg); } if (s.indexOf("1.2.840.10008.1.2.2")>=0) bigEndianTransferSyntax = true; break; case PATIENTS_NAME: // elementLength was reset in previous call to getNextTag() curName = getString(elementLength); patientName_len = elementLength; patientName_loc = location - elementLength; break; case PATIENT_ID: curID = getString(elementLength); patientID_len = elementLength; patientID_loc = location - elementLength; break; case PATIENTS_BIRTH_DATE: curBD = getString(elementLength); patientBD_len = elementLength; patientBD_loc = location - elementLength; break; case PIXEL_DATA: // Start of image data... if (elementLength!=0) { fi.offset = location; decodingTags = false; } break; default: // Not used, skip over it... addInfo(tag, null); } // end switch } // while(decodingTags) f.close(); return fi; } // WO 10/28/02 made 'public' public String getDicomInfo() { return new String(dicomInfo); } void addInfo(int tag, String value) throws IOException { String info = getHeaderInfo(tag, value); if (info!=null) { int group = tag>>>16; if (group!=previousGroup) dicomInfo.append("\n"); previousGroup = group; dicomInfo.append(tag2hex(tag)+info+"\n"); } } void addInfo(int tag, int value) throws IOException { addInfo(tag, Integer.toString(value)); } String getHeaderInfo(int tag, String value) throws IOException { String key = i2hex(tag); //while (key.length()<8) // key = '0' + key; String id = (String)dictionary.get(key); if (id!=null) { if (vr==IMPLICIT_VR && id!=null) vr = (id.charAt(0)<<8) + id.charAt(1); id = id.substring(2); } if (value!=null) return id+": "+value; switch (vr) { case AE: case AS: case AT: case CS: case DA: case DS: case DT: case IS: case LO: case LT: case PN: case SH: case ST: case TM: case UI: value = getString(elementLength); break; case US: if (elementLength==2) value = Integer.toString(getShort()); else { value = ""; int n = elementLength/2; for (int i=0; i 0) skipCount -= f.skip(skipCount); location += elementLength; value = ""; } if (id==null) return null; else return id+": "+value; } static char[] buf8 = new char[8]; /** Converts an int to an 8 byte hex string. */ String i2hex(int i) { for (int pos=7; pos>=0; pos--) { buf8[pos] = Tools.hexDigits[i&0xf]; i >>>= 4; } return new String(buf8); } char[] buf10; String tag2hex(int tag) { if (buf10==null) { buf10 = new char[11]; buf10[4] = ','; buf10[9] = ' '; } int pos = 8; while (pos>=0) { buf10[pos] = Tools.hexDigits[tag&0xf]; tag >>>= 4; pos--; if (pos==4) pos--; // skip coma } return new String(buf10); } double s2d(String s) { Double d; try {d = new Double(s);} catch (NumberFormatException e) {d = null;} if (d!=null) return(d.doubleValue()); else return(0.0); } boolean dicmFound() { return dicmFound; } void getSpatialScale(FileInfo fi, String scale) { double xscale=0, yscale=0; int i = scale.indexOf('\\'); if (i>0) { xscale = s2d(scale.substring(0, i)); yscale = s2d(scale.substring(i+1)); } if (xscale!=0.0 && yscale!=0.0) { fi.pixelWidth = xscale; fi.pixelHeight = yscale; fi.unit = "mm"; } } // WO 9/19/03 alter Dicom header fields and rewrite file public void changeDicomInfo(String directory, String fileName, String curName, String curBD, String curID) { byte[] namebuf = curName.getBytes(); byte[] BDbuf = curBD.getBytes(); byte[] IDbuf = curID.getBytes(); try { f = new BufferedInputStream(new FileInputStream(directory + fileName)); int totalFileLen = f.available(); byte[] fBufCopy = new byte[f.available()]; f.read(fBufCopy, 0, f.available()); // get copy of entire file as byte[] f.close(); // IJ.log(" fileName "+fileName+" patientName_loc:"+patientName_loc); for (int i=0; i< patientBD_len; i++) fBufCopy[i+patientBD_loc] = 0; for (int i=0; i< patientID_len; i++) fBufCopy[i+patientID_loc] = 0; for (int i=0; i< patientName_len; i++) fBufCopy[i+patientName_loc] = 0; for (int i=0; i< Math.min(patientName_len, curName.length()); i++) fBufCopy[i+patientName_loc] = namebuf[i]; for (int i=0; i< Math.min(patientBD_len, curBD.length()); i++) fBufCopy[i+patientBD_loc] = BDbuf[i]; for (int i=0; i< Math.min(patientID_len, curID.length()); i++) fBufCopy[i+patientID_loc] = IDbuf[i]; // write back into same filename BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(directory + fileName)); bos.write(fBufCopy, 0, totalFileLen); bos.close(); } // end try catch (Exception e) { IJ.showMessage("Dicom Rewriter", "Error opening "+fileName+"\n \n\""+e.getMessage()+"\""); } } // end function changeDicomInfo() Properties getDictionary() { Properties p = new Properties(); for (int i=0; i