/******************************************************************************* * Copyright (c) 2016 Weasis Team and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Nicolas Roduit - initial API and implementation *******************************************************************************/ package org.weasis.dicom.codec.utils; import java.awt.Color; import java.awt.Polygon; import java.awt.geom.Area; import java.awt.geom.Ellipse2D; import java.awt.geom.Rectangle2D; import java.io.IOException; import java.io.InputStream; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.time.temporal.ChronoUnit; import java.time.temporal.TemporalAccessor; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; import java.util.Set; import java.util.TreeSet; import javax.media.jai.LookupTableJAI; import javax.xml.stream.XMLInputFactory; import javax.xml.stream.XMLStreamConstants; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamReader; import org.dcm4che3.data.Attributes; import org.dcm4che3.data.ElementDictionary; import org.dcm4che3.data.Sequence; import org.dcm4che3.data.Tag; import org.dcm4che3.data.UID; import org.dcm4che3.data.VR; import org.dcm4che3.util.ByteUtils; import org.dcm4che3.util.TagUtils; import org.dcm4che3.util.UIDUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.weasis.core.api.gui.util.MathUtil; import org.weasis.core.api.image.util.CIELab; import org.weasis.core.api.media.data.MediaSeriesGroup; import org.weasis.core.api.media.data.TagReadable; import org.weasis.core.api.media.data.TagUtil; import org.weasis.core.api.media.data.TagW; import org.weasis.core.api.media.data.TagW.TagType; import org.weasis.core.api.media.data.Tagable; import org.weasis.core.api.util.FileUtil; import org.weasis.core.api.util.StringUtil; import org.weasis.dicom.codec.DicomMediaIO; import org.weasis.dicom.codec.PresentationStateReader; import org.weasis.dicom.codec.TagD; import org.weasis.dicom.codec.TagD.Level; import org.weasis.dicom.codec.TagSeq; import org.weasis.dicom.codec.geometry.ImageOrientation; /** * @author Nicolas Roduit * @author Benoit Jacquemoud * @version $Rev$ $Date$ */ public class DicomMediaUtils { private static final Logger LOGGER = LoggerFactory.getLogger(DicomMediaUtils.class); private static final int[] modalityLutAttributes = new int[] { Tag.RescaleIntercept, Tag.RescaleSlope }; private static final int[] VOILUTWindowLevelAttributes = new int[] { Tag.WindowCenter, Tag.WindowWidth }; private static final int[] LUTAttributes = new int[] { Tag.LUTDescriptor, Tag.LUTData }; public static synchronized void enableAnonymizationProfile(boolean activate) { // Default anonymization profile /* * Other Patient tags to activate if there are accessible 1052673=Other Patient Names (0010,1001) 1052672=Other * Patient IDs (0010,1000) 1052704=Patient's Size (0010,1020) 1052688=Patient's Age (0010,1010) * 1052736=Patient's Address (0010,1040) 1057108=Patient's Telephone Numbers (0010,2154) 1057120=Ethnic Group * (0010,2160) */ /* * Other tags to activate if there are accessible 524417=Institution Address (0008,0081) 528456=Physician(s) of * Record (0008,1048) 524436=Referring Physician's Telephone Numbers (0008,0094) 524434=Referring Physician's * Address (0008,0092) 528480=Name of Physician(s) Reading Study (0008,1060) 3280946=Requesting Physician * (0032,1032) 528464=Performing Physician's Name (0008,1050) 528496=Operators' Name (0008,1070) * 1057152=Occupation (0010,2180) 1577008=*Protocol Name (0018,1030) 4194900=*Performed Procedure Step * Description (0040,0254) 3280992=*Requested Procedure Description (0032,1060) 4237104=Content Sequence * (0040,A730) 532753=Derivation Description (0008,2111) 1576960=Device Serial Number (0018,1000) * 1052816=Medical Record Locator (0010,1090) 528512=Admitting Diagnoses Description (0008,1080) * 1057200=Additional Patient History (0010,21B0) */ int[] list = { Tag.PatientName, Tag.PatientID, Tag.PatientSex, Tag.PatientBirthDate, Tag.PatientBirthTime, Tag.PatientAge, Tag.PatientComments, Tag.PatientWeight, Tag.AccessionNumber, Tag.StudyID, Tag.InstitutionalDepartmentName, Tag.InstitutionName, Tag.ReferringPhysicianName, Tag.StudyDescription, Tag.SeriesDescription, Tag.StationName, Tag.ImageComments }; int type = activate ? 1 : 0; for (int id : list) { TagW t = TagD.getNullable(id); if (t != null) { t.setAnonymizationType(type); } } TagW.PatientPseudoUID.setAnonymizationType(type); } /** * @return false if either an argument is null or if at least one tag value is empty in the given dicomObject */ public static boolean containsRequiredAttributes(Attributes dcmItems, int... requiredTags) { if (dcmItems == null || requiredTags == null || requiredTags.length == 0) { return false; } int countValues = 0; List<String> missingTagList = null; for (int tag : requiredTags) { if (dcmItems.containsValue(tag)) { countValues++; } else { if (missingTagList == null) { missingTagList = new ArrayList<>(requiredTags.length); } missingTagList.add(TagUtils.toString(tag)); } } return countValues == requiredTags.length; } /** * Either a Modality LUT Sequence containing a single Item or Rescale Slope and Intercept values shall be present * but not both.<br> * This requirement for only a single transformation makes it possible to unambiguously define the input of * succeeding stages of the grayscale pipeline such as the VOI LUT * * @return True if the specified object contains some type of Modality LUT attributes at the current level. <br> * * @see - Dicom Standard 2011 - PS 3.3 § C.11.1 Modality LUT Module */ public static boolean containsRequiredModalityLUTAttributes(Attributes dcmItems) { return containsRequiredAttributes(dcmItems, modalityLutAttributes); } public static boolean containsRequiredModalityLUTDataAttributes(Attributes dcmItems) { return containsRequiredAttributes(dcmItems, Tag.ModalityLUTType) && containsLUTAttributes(dcmItems); } /** * * If any VOI LUT Table is included by an Image, a Window Width and Window Center or the VOI LUT Table, but not * both, may be applied to the Image for display. Inclusion of both indicates that multiple alternative views may be * presented. <br> * If multiple items are present in VOI LUT Sequence, only one may be applied to the Image for display. Multiple * items indicate that multiple alternative views may be presented. * * @return True if the specified object contains some type of VOI LUT attributes at the current level (ie:Window * Level or VOI LUT Sequence). * * @see - Dicom Standard 2011 - PS 3.3 § C.11.2 VOI LUT Module */ public static boolean containsRequiredVOILUTWindowLevelAttributes(Attributes dcmItems) { return containsRequiredAttributes(dcmItems, VOILUTWindowLevelAttributes); } public static boolean containsLUTAttributes(Attributes dcmItems) { return containsRequiredAttributes(dcmItems, LUTAttributes); } /** * * @param dicomLutObject * defines LUT data dicom structure * * @return LookupTableJAI object if Data Element and Descriptors are consistent * * @see - Dicom Standard 2011 - PS 3.3 § C.11 LOOK UP TABLES AND PRESENTATION STATES */ public static LookupTableJAI createLut(Attributes dicomLutObject) { if (dicomLutObject == null || dicomLutObject.isEmpty()) { return null; } LookupTableJAI lookupTable = null; // Three values of the LUT Descriptor describe the format of the LUT Data in the corresponding Data Element int[] descriptor = DicomMediaUtils.getIntAyrrayFromDicomElement(dicomLutObject, Tag.LUTDescriptor, null); if (descriptor == null) { LOGGER.debug("Missing LUT Descriptor"); //$NON-NLS-1$ } else if (descriptor.length != 3) { LOGGER.debug("Illegal number of LUT Descriptor values \"{}\"", descriptor.length); //$NON-NLS-1$ } else { // First value is the number of entries in the lookup table. // When this value is 0 the number of table entries is equal to 65536 <=> 0x10000. int numEntries = (descriptor[0] == 0) ? 65536 : descriptor[0]; // Second value is mapped to the first entry in the LUT. int offset = (short) descriptor[1]; // necessary to cast in order to get negative value when present // Third value specifies the number of bits for each entry in the LUT Data. int numBits = descriptor[2]; int dataLength = 0; // number of entry values in the LUT Data. // LUT Data contains the LUT entry values, assuming data is always unsigned data byte[] bData = null; try { bData = dicomLutObject.getBytes(Tag.LUTData); } catch (IOException e) { LOGGER.error("Cannot get byte[] of {}: {} ", TagUtils.toString(Tag.LUTData), e); //$NON-NLS-1$ return null; } if (numBits <= 8) { // LUT Data should be stored in 8 bits allocated format if (numEntries <= 256 && (bData.length == (numEntries << 1))) { // Some implementations have encoded 8 bit entries with 16 bits allocated, padding the high bits byte[] bDataNew = new byte[numEntries]; int byteShift = (dicomLutObject.bigEndian() ? 1 : 0); for (int i = 0; i < bDataNew.length; i++) { bDataNew[i] = bData[(i << 1) | byteShift]; } dataLength = bDataNew.length; lookupTable = new LookupTableJAI(bDataNew, offset); } else { dataLength = bData.length; lookupTable = new LookupTableJAI(bData, offset); // LUT entry value range should be [0,255] } } else if (numBits <= 16) { // LUT Data should be stored in 16 bits allocated format // LUT Data contains the LUT entry values, assuming data is always unsigned data short[] sData = new short[numEntries]; ByteUtils.bytesToShorts(bData, sData, 0, sData.length, dicomLutObject.bigEndian()); if (numEntries <= 256) { // Some implementations have encoded 8 bit entries with 16 bits allocated, padding the high bits int maxIn = (1 << numBits) - 1; int maxOut = numEntries - 1; byte[] bDataNew = new byte[numEntries]; for (int i = 0; i < numEntries; i++) { bDataNew[i] = (byte) ((sData[i] & 0xffff) * maxOut / maxIn); } dataLength = bDataNew.length; lookupTable = new LookupTableJAI(bDataNew, offset); } else { // LUT Data contains the LUT entry values, assuming data is always unsigned data dataLength = sData.length; lookupTable = new LookupTableJAI(sData, offset, true); } } else { LOGGER.debug("Illegal number of bits for each entry in the LUT Data"); //$NON-NLS-1$ } if (lookupTable != null) { if (dataLength != numEntries) { LOGGER.debug("LUT Data length \"{}\" mismatch number of entries \"{}\" in LUT Descriptor ", //$NON-NLS-1$ dataLength, numEntries); } if (dataLength > (1 << numBits)) { LOGGER.debug( "Illegal LUT Data length \"{}\" with respect to the number of bits in LUT descriptor \"{}\"", //$NON-NLS-1$ dataLength, numBits); } } } return lookupTable; } public static String getStringFromDicomElement(Attributes dicom, int tag) { if (dicom == null || !dicom.containsValue(tag)) { return null; } String[] s = dicom.getStrings(tag); if (s == null || s.length == 0) { return null; } if (s.length == 1) { return s[0]; } StringBuilder sb = new StringBuilder(s[0]); for (int i = 1; i < s.length; i++) { sb.append("\\" + s[i]); //$NON-NLS-1$ } return sb.toString(); } public static String[] getStringArrayFromDicomElement(Attributes dicom, int tag) { return getStringArrayFromDicomElement(dicom, tag, (String) null); } public static String[] getStringArrayFromDicomElement(Attributes dicom, int tag, String privateCreatorID) { if (dicom == null || !dicom.containsValue(tag)) { return null; } return dicom.getStrings(privateCreatorID, tag); } public static String[] getStringArrayFromDicomElement(Attributes dicom, int tag, String[] defaultValue) { return getStringArrayFromDicomElement(dicom, tag, null, defaultValue); } public static String[] getStringArrayFromDicomElement(Attributes dicom, int tag, String privateCreatorID, String[] defaultValue) { if (dicom == null || !dicom.containsValue(tag)) { return defaultValue; } String[] val = dicom.getStrings(privateCreatorID, tag); if (val == null || val.length == 0) { return defaultValue; } return val; } public static Date getDateFromDicomElement(Attributes dicom, int tag, Date defaultValue) { if (dicom == null || !dicom.containsValue(tag)) { return defaultValue; } return dicom.getDate(tag, defaultValue); } public static Date[] getDatesFromDicomElement(Attributes dicom, int tag, String privateCreatorID, Date[] defaultValue) { if (dicom == null || !dicom.containsValue(tag)) { return defaultValue; } Date[] val = dicom.getDates(privateCreatorID, tag); if (val == null || val.length == 0) { return defaultValue; } return val; } public static String getPatientAgeInPeriod(Attributes dicom, int tag, boolean computeIfNull) { return getPatientAgeInPeriod(dicom, tag, null, null, computeIfNull); } public static String getPatientAgeInPeriod(Attributes dicom, int tag, String privateCreatorID, String defaultValue, boolean computeIfNull) { if (dicom == null) { return defaultValue; } String s = dicom.getString(privateCreatorID, tag, defaultValue); if (StringUtil.hasText(s) || !computeIfNull) { return s; } Date date = getDate(dicom, new int[] { Tag.ContentDate, Tag.AcquisitionDate, Tag.DateOfSecondaryCapture, Tag.SeriesDate, Tag.StudyDate }); if (date != null) { Date bithdate = dicom.getDate(Tag.PatientBirthDate); if (bithdate != null) { return getPeriod(TagUtil.toLocalDate(bithdate), TagUtil.toLocalDate(date)); } } return null; } private static Date getDate(Attributes dicom, int... tagID) { Date date = null; for (int i : tagID) { date = dicom.getDate(i); if (date != null) { return date; } } return date; } public static String getPeriod(LocalDate first, LocalDate last) { Objects.requireNonNull(first); Objects.requireNonNull(last); long years = ChronoUnit.YEARS.between(first, last); if (years < 2) { long months = ChronoUnit.MONTHS.between(first, last); if (months < 2) { return String.format("%03dD", ChronoUnit.DAYS.between(first, last)); //$NON-NLS-1$ } return String.format("%03dM", months); //$NON-NLS-1$ } return String.format("%03dY", years); //$NON-NLS-1$ } public static Float getFloatFromDicomElement(Attributes dicom, int tag, Float defaultValue) { return getFloatFromDicomElement(dicom, tag, null, defaultValue); } public static Float getFloatFromDicomElement(Attributes dicom, int tag, String privateCreatorID, Float defaultValue) { if (dicom == null || !dicom.containsValue(tag)) { return defaultValue; } try { return dicom.getFloat(privateCreatorID, tag, defaultValue == null ? 0.0F : defaultValue); } catch (NumberFormatException e) { LOGGER.error("Cannot parse Float of {}: {} ", TagUtils.toString(tag), e.getMessage()); //$NON-NLS-1$ } return defaultValue; } public static Integer getIntegerFromDicomElement(Attributes dicom, int tag, Integer defaultValue) { return getIntegerFromDicomElement(dicom, tag, null, defaultValue); } public static Integer getIntegerFromDicomElement(Attributes dicom, int tag, String privateCreatorID, Integer defaultValue) { if (dicom == null || !dicom.containsValue(tag)) { return defaultValue; } try { return dicom.getInt(privateCreatorID, tag, defaultValue == null ? 0 : defaultValue); } catch (NumberFormatException e) { LOGGER.error("Cannot parse Integer of {}: {} ", TagUtils.toString(tag), e.getMessage()); //$NON-NLS-1$ } return defaultValue; } public static Double getDoubleFromDicomElement(Attributes dicom, int tag, Double defaultValue) { return getDoubleFromDicomElement(dicom, tag, null, defaultValue); } public static Double getDoubleFromDicomElement(Attributes dicom, int tag, String privateCreatorID, Double defaultValue) { if (dicom == null || !dicom.containsValue(tag)) { return defaultValue; } try { return dicom.getDouble(privateCreatorID, tag, defaultValue == null ? 0.0 : defaultValue); } catch (NumberFormatException e) { LOGGER.error("Cannot parse Double of {}: {} ", TagUtils.toString(tag), e.getMessage()); //$NON-NLS-1$ } return defaultValue; } public static int[] getIntAyrrayFromDicomElement(Attributes dicom, int tag, int[] defaultValue) { return getIntArrayFromDicomElement(dicom, tag, null, defaultValue); } public static int[] getIntArrayFromDicomElement(Attributes dicom, int tag, String privateCreatorID, int[] defaultValue) { if (dicom == null || !dicom.containsValue(tag)) { return defaultValue; } try { return dicom.getInts(privateCreatorID, tag); } catch (NumberFormatException e) { LOGGER.error("Cannot parse int[] of {}: {} ", TagUtils.toString(tag), e.getMessage()); //$NON-NLS-1$ } return defaultValue; } public static float[] getFloatArrayFromDicomElement(Attributes dicom, int tag, float[] defaultValue) { return getFloatArrayFromDicomElement(dicom, tag, null, defaultValue); } public static float[] getFloatArrayFromDicomElement(Attributes dicom, int tag, String privateCreatorID, float[] defaultValue) { if (dicom == null || !dicom.containsValue(tag)) { return defaultValue; } try { return dicom.getFloats(privateCreatorID, tag); } catch (NumberFormatException e) { LOGGER.error("Cannot parse float[] of {}: {} ", TagUtils.toString(tag), e.getMessage()); //$NON-NLS-1$ } return defaultValue; } public static double[] getDoubleArrayFromDicomElement(Attributes dicom, int tag, double[] defaultValue) { return getDoubleArrayFromDicomElement(dicom, tag, null, defaultValue); } public static double[] getDoubleArrayFromDicomElement(Attributes dicom, int tag, String privateCreatorID, double[] defaultValue) { if (dicom == null || !dicom.containsValue(tag)) { return defaultValue; } try { return dicom.getDoubles(privateCreatorID, tag); } catch (NumberFormatException e) { LOGGER.error("Cannot parse double[] of {}: {} ", TagUtils.toString(tag), e.getMessage()); //$NON-NLS-1$ } return defaultValue; } public static boolean hasOverlay(Attributes attrs) { if (attrs != null) { for (int i = 0; i < 16; i++) { int gg0000 = i << 17; if ((0xffff & (1 << i)) != 0 && attrs.containsValue(Tag.OverlayRows | gg0000)) { return true; } } } return false; } public static Integer getIntPixelValue(Attributes ds, int tag, boolean signed, int stored) { VR vr = ds.getVR(tag); if (vr == null) { return null; } int result = 0; // Bug fix: http://www.dcm4che.org/jira/browse/DCM-460 if (vr == VR.OB || vr == VR.OW) { try { result = ByteUtils.bytesToUShortLE(ds.getBytes(tag), 0); } catch (IOException e) { LOGGER.error("Cannot read {} ", TagUtils.toString(tag), e); //$NON-NLS-1$ } if (signed && (result & (1 << (stored - 1))) != 0) { int andmask = (1 << stored) - 1; int ormask = ~andmask; result |= ormask; } } else if ((!signed && vr != VR.US) || (signed && vr != VR.SS)) { vr = signed ? VR.SS : VR.US; result = ds.getInt(null, tag, vr, 0); } else { result = ds.getInt(tag, 0); } // Unsigned Short (0 to 65535) and Signed Short (-32768 to +32767) int minInValue = signed ? -(1 << (stored - 1)) : 0; int maxInValue = signed ? (1 << (stored - 1)) - 1 : (1 << stored) - 1; return result < minInValue ? minInValue : result > maxInValue ? maxInValue : result; } public static String buildPatientPseudoUID(TagReadable tagable) { String patientID = TagD.getTagValue(tagable, Tag.PatientID, String.class); String issuerOfPatientID = TagD.getTagValue(tagable, Tag.IssuerOfPatientID, String.class); String patientName = TagD.getTagValue(tagable, Tag.PatientName, String.class); return buildPatientPseudoUID(patientID, issuerOfPatientID, patientName); } public static String buildPatientPseudoUID(String patientID, String issuerOfPatientID, String patientName) { /* * IHE RAD TF-­‐2: 4.16.4.2.2.5.3 * * The Image Display shall not display FrameSets for multiple patients simultaneously. Only images with exactly * the same value for Patient’s ID (0010,0020) and Patient’s Name (0010,0010) shall be displayed at the same * time (other Patient-level attributes may be different, empty or absent). Though it is possible that the same * patient may have slightly different identifying attributes in different DICOM images performed at different * sites or on different occasions, it is expected that such differences will have been reconciled prior to the * images being provided to the Image Display (e.g., in the Image Manager/Archive or by the Portable Media * Creator). */ // Build a global identifier for the patient. StringBuilder buffer = new StringBuilder(patientID == null ? TagW.NO_VALUE : patientID); if (StringUtil.hasText(issuerOfPatientID)) { // patientID + issuerOfPatientID => should be unique globally buffer.append(issuerOfPatientID); } if (patientName != null) { buffer.append(patientName.toUpperCase()); } return buffer.toString(); } public static void setTag(Map<TagW, Object> tags, TagW tag, Object value) { if (tag != null) { if (value instanceof Sequence) { Sequence seq = (Sequence) value; Attributes[] list = new Attributes[seq.size()]; for (int i = 0; i < list.length; i++) { Attributes attributes = seq.get(i); list[i] = attributes.getParent() == null ? attributes : new Attributes(attributes); } tags.put(tag, list); } else { tags.put(tag, value); } } } public static void setTagNoNull(Map<TagW, Object> tags, TagW tag, Object value) { if (value != null) { setTag(tags, tag, value); } } public static void writeMetaData(MediaSeriesGroup group, Attributes header) { if (group == null || header == null) { return; } // Patient Group if (TagD.getUID(Level.PATIENT).equals(group.getTagID())) { DicomMediaIO.tagManager.readTags(Level.PATIENT, header, group); // Build patient age if not present group.setTagNoNull(TagD.get(Tag.PatientAge), getPatientAgeInPeriod(header, Tag.PatientAge, true)); } // Study Group else if (TagD.getUID(Level.STUDY).equals(group.getTagID())) { DicomMediaIO.tagManager.readTags(Level.STUDY, header, group); } // Series Group else if (TagD.getUID(Level.SERIES).equals(group.getTagID())) { DicomMediaIO.tagManager.readTags(Level.SERIES, header, group); } } public static void computeSlicePositionVector(Tagable tagable) { if (tagable != null) { double[] patientPos = TagD.getTagValue(tagable, Tag.ImagePositionPatient, double[].class); if (patientPos != null && patientPos.length == 3) { double[] imgOrientation = ImageOrientation .computeNormalVectorOfPlan(TagD.getTagValue(tagable, Tag.ImageOrientationPatient, double[].class)); if (imgOrientation != null) { double[] slicePosition = new double[3]; slicePosition[0] = imgOrientation[0] * patientPos[0]; slicePosition[1] = imgOrientation[1] * patientPos[1]; slicePosition[2] = imgOrientation[2] * patientPos[2]; tagable.setTag(TagW.SlicePosition, slicePosition); } } } } public static void buildSeriesReferences(Tagable tagable, Attributes attributes) { Sequence seq = attributes.getSequence(Tag.ReferencedSeriesSequence); if (Objects.nonNull(seq)) { Attributes[] ref = new Attributes[seq.size()]; for (int i = 0; i < ref.length; i++) { ref[i] = new Attributes(seq.get(i)); } tagable.setTagNoNull(TagD.get(Tag.ReferencedSeriesSequence), ref); } } public static void setShutterColor(Tagable tagable, Attributes attributes) { Integer psVal = (Integer) TagD.get(Tag.ShutterPresentationValue).getValue(attributes); tagable.setTagNoNull(TagW.ShutterPSValue, TagD.get(Tag.ShutterPresentationValue).getValue(attributes)); float[] rgb = CIELab.convertToFloatLab((int[]) TagD.get(Tag.ShutterPresentationColorCIELabValue).getValue(attributes)); Color color = rgb == null ? null : PresentationStateReader.getRGBColor(psVal == null ? 0 : psVal, rgb, (int[]) null); tagable.setTagNoNull(TagW.ShutterRGBColor, color); } /** * Build the shape from DICOM Shutter * * @see <a href="http://dicom.nema.org/MEDICAL/DICOM/current/output/chtml/part03/sect_C.7.6.11.html">C.7.6.11 * Display Shutter Module</a> * @see <a href="http://dicom.nema.org/MEDICAL/DICOM/current/output/chtml/part03/sect_C.7.6.15.html">C.7.6.15 Bitmap * Display Shutter Module</a> * */ public static void setShutter(Tagable tagable, Attributes dcmObject) { Area shape = null; String shutterShape = getStringFromDicomElement(dcmObject, Tag.ShutterShape); if (shutterShape != null) { if (shutterShape.contains("RECTANGULAR") || shutterShape.contains("RECTANGLE")) { //$NON-NLS-1$ //$NON-NLS-2$ Rectangle2D rect = new Rectangle2D.Double(); rect.setFrameFromDiagonal(getIntegerFromDicomElement(dcmObject, Tag.ShutterLeftVerticalEdge, 0), getIntegerFromDicomElement(dcmObject, Tag.ShutterUpperHorizontalEdge, 0), getIntegerFromDicomElement(dcmObject, Tag.ShutterRightVerticalEdge, 0), getIntegerFromDicomElement(dcmObject, Tag.ShutterLowerHorizontalEdge, 0)); shape = new Area(rect); } if (shutterShape.contains("CIRCULAR")) { //$NON-NLS-1$ int[] centerOfCircularShutter = DicomMediaUtils.getIntAyrrayFromDicomElement(dcmObject, Tag.CenterOfCircularShutter, null); if (centerOfCircularShutter != null && centerOfCircularShutter.length >= 2) { Ellipse2D ellipse = new Ellipse2D.Double(); int radius = getIntegerFromDicomElement(dcmObject, Tag.RadiusOfCircularShutter, 0); // Thanks DICOM for reversing x,y by row,column ellipse.setFrameFromCenter(centerOfCircularShutter[1], centerOfCircularShutter[0], centerOfCircularShutter[1] + radius, centerOfCircularShutter[0] + radius); if (shape == null) { shape = new Area(ellipse); } else { shape.intersect(new Area(ellipse)); } } } if (shutterShape.contains("POLYGONAL")) { //$NON-NLS-1$ int[] points = DicomMediaUtils.getIntAyrrayFromDicomElement(dcmObject, Tag.VerticesOfThePolygonalShutter, null); if (points != null) { Polygon polygon = new Polygon(); for (int i = 0; i < points.length / 2; i++) { // Thanks DICOM for reversing x,y by row,column polygon.addPoint(points[i * 2 + 1], points[i * 2]); } if (shape == null) { shape = new Area(polygon); } else { shape.intersect(new Area(polygon)); } } } if (shape != null) { tagable.setTagNoNull(TagW.ShutterFinalShape, shape); } // Set color also for BITMAP shape (bitmap is extracted in overlay class) setShutterColor(tagable, dcmObject); } } public static void writeFunctionalGroupsSequence(Tagable tagable, Attributes dcm) { if (dcm != null && tagable != null) { /** * @see - Dicom Standard 2011 - PS 3.3 §C.7.6.16.2.1 Pixel Measures Macro */ TagSeq.MacroSeqData data = new TagSeq.MacroSeqData(dcm, TagD.getTagFromIDs(Tag.PixelSpacing, Tag.SliceThickness)); TagD.get(Tag.PixelMeasuresSequence).readValue(data, tagable); /** * @see - Dicom Standard 2011 - PS 3.3 §C.7.6.16.2.2 Frame Content Macro */ data = new TagSeq.MacroSeqData(dcm, TagD.getTagFromIDs(Tag.FrameAcquisitionNumber, Tag.StackID, Tag.InStackPositionNumber, Tag.TemporalPositionIndex)); TagD.get(Tag.FrameContentSequence).readValue(data, tagable); // If not null override instance number for a better image sorting. tagable.setTagNoNull(TagD.get(Tag.InstanceNumber), tagable.getTagValue(TagD.get(Tag.InStackPositionNumber))); /** * @see - Dicom Standard 2011 - PS 3.3 § C.7.6.16.2.3 Plane Position (Patient) Macro */ data = new TagSeq.MacroSeqData(dcm, TagD.getTagFromIDs(Tag.ImagePositionPatient)); TagD.get(Tag.PlanePositionSequence).readValue(data, tagable); /** * @see - Dicom Standard 2011 - PS 3.3 § C.7.6.16.2.4 Plane Orientation (Patient) Macro */ data = new TagSeq.MacroSeqData(dcm, TagD.getTagFromIDs(Tag.ImageOrientationPatient)); TagD.get(Tag.PlaneOrientationSequence).readValue(data, tagable); // If not null add ImageOrientationPlane for getting a orientation label. tagable.setTagNoNull(TagW.ImageOrientationPlane, ImageOrientation.makeImageOrientationLabelFromImageOrientationPatient( TagD.getTagValue(tagable, Tag.ImageOrientationPatient, double[].class))); /** * @see - Dicom Standard 2011 - PS 3.3 § C.7.6.16.2.8 Frame Anatomy Macro */ data = new TagSeq.MacroSeqData(dcm, TagD.getTagFromIDs(Tag.FrameLaterality)); TagD.get(Tag.FrameAnatomySequence).readValue(data, tagable); /** * Specifies the attributes of the Pixel Value Transformation Functional Group. This is equivalent with the * Modality LUT transformation in non Multi-frame IODs. It constrains the Modality LUT transformation step * in the grayscale rendering pipeline to be an identity transformation. * * @see - Dicom Standard 2011 - PS 3.3 § C.7.6.16.2.9-b Pixel Value Transformation */ Attributes mLutItems = dcm.getNestedDataset(Tag.PixelValueTransformationSequence); applyModalityLutModule(mLutItems, tagable, Tag.PixelValueTransformationSequence); /** * Specifies the attributes of the Frame VOI LUT Functional Group. It contains one or more sets of linear or * sigmoid window values and/or one or more sets of lookup tables * * @see - Dicom Standard 2011 - PS 3.3 § C.7.6.16.2.10b Frame VOI LUT With LUT Macro */ applyVoiLutModule(dcm.getNestedDataset(Tag.FrameVOILUTSequence), mLutItems, tagable, Tag.FrameVOILUTSequence); // TODO implement: Frame Pixel Shift, Pixel Intensity Relationship LUT (C.7.6.16-14), // Real World Value Mapping (C.7.6.16-12) // This transformation should be applied in in the pixel value (add a list of transformation for pixel // statistics) /** * Display Shutter Macro Table C.7-17A in PS 3.3 * * @see - Dicom Standard 2011 - PS 3.3 § C.7.6.16.2.16 Frame Display Shutter Macro */ Attributes macroFrameDisplayShutter = dcm.getNestedDataset(Tag.FrameDisplayShutterSequence); if (macroFrameDisplayShutter != null) { setShutter(tagable, macroFrameDisplayShutter); } /** * @see - Dicom Standard 2011 - PS 3.3 §C.8 Frame Type Macro */ // Type of Frame. A multi-valued attribute analogous to the Image Type (0008,0008). // Enumerated Values and Defined Terms are the same as those for the four values of the Image Type // (0008,0008) attribute, except that the value MIXED is not allowed. See C.8.16.1 and C.8.13.3.1.1. data = new TagSeq.MacroSeqData(dcm, TagD.getTagFromIDs(Tag.FrameType)); // C.8.13.5.1 MR Image Frame Type Macro TagD.get(Tag.MRImageFrameTypeSequence).readValue(data, tagable); // // C.8.15.3.1 CT Image Frame Type Macro TagD.get(Tag.CTImageFrameTypeSequence).readValue(data, tagable); // C.8.14.3.1 MR Spectroscopy Frame Type Macro TagD.get(Tag.MRSpectroscopyFrameTypeSequence).readValue(data, tagable); // C.8.22.5.1 PET Frame Type Macro TagD.get(Tag.PETFrameTypeSequence).readValue(data, tagable); } } public static boolean writePerFrameFunctionalGroupsSequence(Tagable tagable, Attributes header, int index) { if (header != null && tagable != null) { /* * C.7.6.16 The number of Items shall be the same as the number of frames in the Multi-frame image. */ Attributes a = header.getNestedDataset(Tag.PerFrameFunctionalGroupsSequence, index); if (a != null) { DicomMediaUtils.writeFunctionalGroupsSequence(tagable, a); return true; } } return false; } public static void applyModalityLutModule(Attributes mLutItems, Tagable tagable, Integer seqParentTag) { if (mLutItems != null && tagable != null) { // Overrides Modality LUT Transformation attributes only if sequence is consistent if (containsRequiredModalityLUTAttributes(mLutItems)) { String modlality = TagD.getTagValue(tagable, Tag.Modality, String.class); if ("MR".equals(modlality) || "XA".equals(modlality) || "XRF".equals(modlality) //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ || "PT".equals(modlality)) { //$NON-NLS-1$ /* * IHE BIR: 4.16.4.2.2.5.4 * * The grayscale rendering pipeline shall be appropriate to the SOP Class and modality. If Rescale * Slope and Rescale Intercept are present in the image for MR and PET and XA/XRF images, they shall * be ignored from the perspective of applying window values, and for those SOP Classes, window * values shall be applied directly to the stored pixel values without rescaling. */ LOGGER.trace("Do not apply RescaleSlope and RescaleIntercept to {}", modlality);//$NON-NLS-1$ } else { TagD.get(Tag.RescaleSlope).readValue(mLutItems, tagable); TagD.get(Tag.RescaleIntercept).readValue(mLutItems, tagable); TagD.get(Tag.RescaleType).readValue(mLutItems, tagable); } } else if (seqParentTag != null) { LOGGER.warn("Cannot apply Modality LUT from {} with inconsistent attributes", //$NON-NLS-1$ TagUtils.toString(seqParentTag)); } // Should exist only in root DICOM (when seqParentTag == null) buildMoalityLUT(mLutItems.getNestedDataset(Tag.ModalityLUTSequence), tagable); } } public static void buildMoalityLUT(Attributes mLutItems, Tagable tagable) { if (tagable != null) { // NOTE : Either a Modality LUT Sequence containing a single Item or Rescale Slope and Intercept values // shall be present but not both (@see Dicom Standard 2011 - PS 3.3 § C.11.1 Modality LUT Module) if (mLutItems != null && containsRequiredModalityLUTDataAttributes(mLutItems)) { boolean canApplyMLUT = true; String modlality = TagD.getTagValue(tagable, Tag.Modality, String.class); if ("XA".equals(modlality) || "XRF".equals(modlality)) { //$NON-NLS-1$ //$NON-NLS-2$ // See PS 3.4 N.2.1.2. String pixRel = mLutItems.getParent() == null ? null : mLutItems.getParent().getString(Tag.PixelIntensityRelationship); if (pixRel != null && ("LOG".equalsIgnoreCase(pixRel) || "DISP".equalsIgnoreCase(pixRel))) { //$NON-NLS-1$ //$NON-NLS-2$ canApplyMLUT = false; LOGGER.debug( "Modality LUT Sequence shall NOT be applied according to PixelIntensityRelationship"); //$NON-NLS-1$ } } if (canApplyMLUT) { tagable.setTagNoNull(TagW.ModalityLUTData, createLut(mLutItems)); tagable.setTagNoNull(TagW.ModalityLUTType, TagD.get(Tag.ModalityLUTType).getValue(mLutItems)); tagable.setTagNoNull(TagW.ModalityLUTExplanation, TagD.get(Tag.LUTExplanation).getValue(mLutItems)); } } if (LOGGER.isTraceEnabled()) { // The output range of the Modality LUT Module depends on whether or not Rescale Slope and Rescale // Intercept or the Modality LUT Sequence are used. // In the case where Rescale Slope and Rescale Intercept are used, the output ranges from // (minimum pixel value*Rescale Slope+Rescale Intercept) to // (maximum pixel value*Rescale Slope+Rescale Intercept), // where the minimum and maximum pixel values are determined by Bits Stored and Pixel Representation. // In the case where the Modality LUT Sequence is used, the output range is from 0 to 2n-1 where n // is the third value of LUT Descriptor. This range is always unsigned. // The third value specifies the number of bits for each entry in the LUT Data. It shall take the value // 8 or 16. The LUT Data shall be stored in a format equivalent to 8 bits allocated when the number // of bits for each entry is 8, and 16 bits allocated when the number of bits for each entry is 16 if (tagable.getTagValue(TagW.ModalityLUTData) != null) { if (TagD.getTagValue(tagable, Tag.RescaleIntercept) != null) { LOGGER.trace("Modality LUT Sequence shall NOT be present if Rescale Intercept is present"); //$NON-NLS-1$ } if (TagD.getTagValue(tagable, Tag.ModalityLUTType) == null) { LOGGER.trace("Modality Type is required if Modality LUT Sequence is present. "); //$NON-NLS-1$ } } else if (TagD.getTagValue(tagable, Tag.RescaleIntercept) != null) { if (TagD.getTagValue(tagable, Tag.RescaleSlope) == null) { LOGGER.debug("Modality Rescale Slope is required if Rescale Intercept is present."); //$NON-NLS-1$ } } else { String modlality = TagD.getTagValue(tagable, Tag.Modality, String.class); if ("MR".equals(modlality) || "XA".equals(modlality) || "XRF".equals(modlality) //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ || !"PT".equals(modlality)) { //$NON-NLS-1$ LOGGER .trace("Modality Rescale Intercept is required if Modality LUT Sequence is not present. "); //$NON-NLS-1$ } } } } } public static void applyVoiLutModule(Attributes voiItems, Attributes mLutItems, Tagable tagable, Integer seqParentTag) { if (voiItems != null && tagable != null) { // Overrides VOI LUT Transformation attributes only if sequence is consistent if (containsRequiredVOILUTWindowLevelAttributes(voiItems)) { TagD.get(Tag.WindowWidth).readValue(voiItems, tagable); TagD.get(Tag.WindowCenter).readValue(voiItems, tagable); double[] ww = TagD.getTagValue(tagable, Tag.WindowWidth, double[].class); double[] wc = TagD.getTagValue(tagable, Tag.WindowCenter, double[].class); if (mLutItems != null) { /* * IHE BIR: 4.16.4.2.2.5.4 * * If Rescale Slope and Rescale Intercept has been removed in applyModalityLutModule() then the * Window Center and Window Width must be adapted * * see https://groups.google.com/forum/#!topic/comp.protocols.dicom/iTCxWcsqjnM */ Double rs = getDoubleFromDicomElement(mLutItems, Tag.RescaleSlope, null); Double ri = getDoubleFromDicomElement(mLutItems, Tag.RescaleIntercept, null); String modality = TagD.getTagValue(tagable, Tag.Modality, String.class); if (ww != null && wc != null && rs != null && ri != null && ("MR".equals(modality) || "XA".equals(modality) || "XRF".equals(modality) //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ || "PT".equals(modality))) { //$NON-NLS-1$ int windowLevelDefaultCount = (ww.length == wc.length) ? ww.length : 0; for (int i = 0; i < windowLevelDefaultCount; i++) { ww[i] = (ww[i] - ri) / rs; wc[i] = (wc[i] - ri) / rs; } } } TagD.get(Tag.WindowCenterWidthExplanation).readValue(voiItems, tagable); TagD.get(Tag.VOILUTFunction).readValue(voiItems, tagable); } buildVoiLUTs(voiItems.getSequence(Tag.VOILUTSequence), tagable); } } public static void buildVoiLUTs(Sequence voiLUTSequence, Tagable tagable) { if (tagable != null) { // NOTE : If any VOI LUT Table is included by an Image, a Window Width and Window Center or the VOI LUT // Table, but not both, may be applied to the Image for display. Inclusion of both indicates that multiple // alternative views may be presented. (@see Dicom Standard 2011 - PS 3.3 § C.11.2 VOI LUT Module) if (voiLUTSequence != null && !voiLUTSequence.isEmpty()) { LookupTableJAI[] voiLUTsData = new LookupTableJAI[voiLUTSequence.size()]; String[] voiLUTsExplanation = new String[voiLUTsData.length]; for (int i = 0; i < voiLUTsData.length; i++) { Attributes voiLUTobj = voiLUTSequence.get(i); if (containsLUTAttributes(voiLUTobj)) { voiLUTsData[i] = createLut(voiLUTobj); voiLUTsExplanation[i] = getStringFromDicomElement(voiLUTobj, Tag.LUTExplanation); } else { LOGGER.info("Cannot read VOI LUT Data [{}]", i); //$NON-NLS-1$ } } tagable.setTag(TagW.VOILUTsData, voiLUTsData); tagable.setTag(TagW.VOILUTsExplanation, voiLUTsExplanation); // Optional Tag } if (LOGGER.isDebugEnabled()) { // If multiple items are present in VOI LUT Sequence, only one may be applied to the // Image for display. Multiple items indicate that multiple alternative views may be presented. // If multiple Window center and window width values are present, both Attributes shall have the same // number of values and shall be considered as pairs. Multiple values indicate that multiple alternative // views may be presented double[] windowCenter = TagD.getTagValue(tagable, Tag.WindowCenter, double[].class); double[] windowWidth = TagD.getTagValue(tagable, Tag.WindowWidth, double[].class); if (windowCenter == null && windowWidth == null) { return; } else if (windowCenter == null) { LOGGER.debug("VOI Window Center is required if Window Width is present"); //$NON-NLS-1$ } else if (windowWidth == null) { LOGGER.debug("VOI Window Width is required if Window Center is present"); //$NON-NLS-1$ } else if (windowWidth.length != windowCenter.length) { LOGGER.debug("VOI Window Center and Width attributes have different number of values : {} // {}", //$NON-NLS-1$ windowCenter, windowWidth); } } } } /** * @see <a href="http://dicom.nema.org/medical/Dicom/current/output/chtml/part03/sect_C.11.6.html">C.11.6 Softcopy * Presentation LUT Module</a> */ public static void applyPrLutModule(Attributes dcmItems, Tagable tagable) { if (dcmItems != null && tagable != null) { // TODO implement 1.2.840.10008.5.1.4.1.1.11.2 -5 color and xray if ("1.2.840.10008.5.1.4.1.1.11.1".equals(dcmItems.getString(Tag.SOPClassUID))) { //$NON-NLS-1$ Attributes presentationLUT = dcmItems.getNestedDataset(Tag.PresentationLUTSequence); if (presentationLUT != null) { /** * Presentation LUT Module is always implicitly specified to apply over the full range of output of * the preceding transformation, and it never selects a subset or superset of the that range (unlike * the VOI LUT). */ tagable.setTag(TagW.PRLUTsData, createLut(presentationLUT)); tagable.setTag(TagW.PRLUTsExplanation, getStringFromDicomElement(presentationLUT, Tag.LUTExplanation)); tagable.setTagNoNull(TagD.get(Tag.PresentationLUTShape), "IDENTITY"); //$NON-NLS-1$ } else { // value: INVERSE, IDENTITY // INVERSE => must inverse values (same as monochrome 1) TagD.get(Tag.PresentationLUTShape).readValue(dcmItems, tagable); } } } } public static void readPRLUTsModule(Attributes dcmItems, Tagable tagable) { if (dcmItems != null && tagable != null) { // Modality LUT Module applyModalityLutModule(dcmItems, tagable, null); // VOI LUT Module applyVoiLutModule(dcmItems.getNestedDataset(Tag.SoftcopyVOILUTSequence), dcmItems, tagable, Tag.SoftcopyVOILUTSequence); // Presentation LUT Module applyPrLutModule(dcmItems, tagable); } } public static void computeSUVFactor(Attributes dicomObject, Tagable tagable, int index) { // From vendor neutral code at http://qibawiki.rsna.org/index.php?title=Standardized_Uptake_Value_%28SUV%29 String modlality = TagD.getTagValue(tagable, Tag.Modality, String.class); if ("PT".equals(modlality)) { //$NON-NLS-1$ String correctedImage = getStringFromDicomElement(dicomObject, Tag.CorrectedImage); if (correctedImage != null && correctedImage.contains("ATTN") && correctedImage.contains("DECY")) { //$NON-NLS-1$ //$NON-NLS-2$ double suvFactor = 0.0; String units = dicomObject.getString(Tag.Units); // DICOM $C.8.9.1.1.3 Units // The units of the pixel values obtained after conversion from the stored pixel values (SV) (Pixel // Data (7FE0,0010)) to pixel value units (U), as defined by Rescale Intercept (0028,1052) and // Rescale Slope (0028,1053). Defined Terms: // CNTS = counts // NONE = unitless // CM2 = centimeter**2 // PCNT = percent // CPS = counts/second // BQML = Becquerels/milliliter // MGMINML = milligram/minute/milliliter // UMOLMINML = micromole/minute/milliliter // MLMING = milliliter/minute/gram // MLG = milliliter/gram // 1CM = 1/centimeter // UMOLML = micromole/milliliter // PROPCNTS = proportional to counts // PROPCPS = proportional to counts/sec // MLMINML = milliliter/minute/milliliter // MLML = milliliter/milliliter // GML = grams/milliliter // STDDEV = standard deviations if ("BQML".equals(units)) { //$NON-NLS-1$ Float weight = getFloatFromDicomElement(dicomObject, Tag.PatientWeight, 0.0f); // in Kg if (MathUtil.isDifferentFromZero(weight)) { Attributes dcm = dicomObject.getNestedDataset(Tag.RadiopharmaceuticalInformationSequence, index); if (dcm != null) { Float totalDose = getFloatFromDicomElement(dcm, Tag.RadionuclideTotalDose, null); Float halfLife = getFloatFromDicomElement(dcm, Tag.RadionuclideHalfLife, null); Date injectTime = getDateFromDicomElement(dcm, Tag.RadiopharmaceuticalStartTime, null); Date injectDateTime = getDateFromDicomElement(dcm, Tag.RadiopharmaceuticalStartDateTime, null); Date acquisitionDateTime = TagUtil.dateTime(getDateFromDicomElement(dicomObject, Tag.AcquisitionDate, null), getDateFromDicomElement(dicomObject, Tag.AcquisitionTime, null)); Date scanDate = getDateFromDicomElement(dicomObject, Tag.SeriesDate, null); if ("START".equals(dicomObject.getString(Tag.DecayCorrection)) && totalDose != null //$NON-NLS-1$ && halfLife != null && acquisitionDateTime != null && (injectDateTime != null || (scanDate != null && injectTime != null))) { double time = 0.0; long scanDateTime = TagUtil .dateTime(scanDate, getDateFromDicomElement(dicomObject, Tag.SeriesTime, null)) .getTime(); if (injectDateTime == null) { if (scanDateTime > acquisitionDateTime.getTime()) { // per GE docs, may have been updated during post-processing into new series String privateCreator = dicomObject.getString(0x00090010); Date privateScanDateTime = getDateFromDicomElement(dcm, 0x0009100d, null); if ("GEMS_PETD_01".equals(privateCreator) && privateScanDateTime != null) { //$NON-NLS-1$ scanDate = privateScanDateTime; } else { scanDate = null; } } if (scanDate != null) { injectDateTime = TagUtil.dateTime(scanDate, injectTime); time = scanDateTime - injectDateTime.getTime(); } } else { time = scanDateTime - injectDateTime.getTime(); } // Exclude negative value (case over midnight) if (time > 0) { double correctedDose = totalDose * Math.pow(2, -time / (1000.0 * halfLife)); // Weight convert in kg to g suvFactor = weight * 1000.0 / correctedDose; } } } } } else if ("CNTS".equals(units)) { //$NON-NLS-1$ String privateTagCreator = dicomObject.getString(0x70530010); double privateSUVFactor = dicomObject.getDouble(0x70531000, 0.0); if ("Philips PET Private Group".equals(privateTagCreator) //$NON-NLS-1$ && MathUtil.isDifferentFromZero(privateSUVFactor)) { suvFactor = privateSUVFactor; // units => "g/ml" } } else if ("GML".equals(units)) { //$NON-NLS-1$ suvFactor = 1.0; } if (MathUtil.isDifferentFromZero(suvFactor)) { tagable.setTag(TagW.SuvFactor, suvFactor); } } } } public static Attributes createDicomPR(Attributes dicomSourceAttribute, String seriesInstanceUID, String sopInstanceUID) { final int[] patientStudyAttributes = { Tag.SpecificCharacterSet, Tag.StudyDate, Tag.StudyTime, Tag.StudyDescription, Tag.AccessionNumber, Tag.IssuerOfAccessionNumberSequence, Tag.ReferringPhysicianName, Tag.PatientName, Tag.PatientID, Tag.IssuerOfPatientID, Tag.PatientBirthDate, Tag.PatientSex, Tag.AdditionalPatientHistory, Tag.StudyInstanceUID, Tag.StudyID }; Arrays.sort(patientStudyAttributes); Attributes pr = new Attributes(dicomSourceAttribute, patientStudyAttributes); // TODO implement other ColorSoftcopyPresentationStateStorageSOPClass... pr.setString(Tag.SOPClassUID, VR.UI, UID.GrayscaleSoftcopyPresentationStateStorageSOPClass); pr.setString(Tag.SOPInstanceUID, VR.UI, StringUtil.hasText(sopInstanceUID) ? sopInstanceUID : UIDUtils.createUID()); Date now = new Date(); pr.setDate(Tag.PresentationCreationDateAndTime, now); pr.setDate(Tag.ContentDateAndTime, now); pr.setString(Tag.Modality, VR.CS, "PR"); //$NON-NLS-1$ pr.setString(Tag.SeriesInstanceUID, VR.UI, StringUtil.hasText(seriesInstanceUID) ? seriesInstanceUID : UIDUtils.createUID()); return pr; } // /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /** * Creates a dicomKeyObjectSelection Attributes from another SOP Instance keeping it's patient and study * informations. For instance it can be can an IMAGE or a previously build dicomKOS Document. * * @param dicomSourceAttribute * : Must be valid * @param keyObjectDescription * : Optional, can be null * @param seriesInstanceUID * is supposed to be valid and won't be verified, it's the user responsibility to manage this value. If * null a randomly new one will be generated instead * * @return new dicomKeyObjectSelection Document Attributes * @throws IOException */ public static Attributes createDicomKeyObject(Attributes dicomSourceAttribute, String keyObjectDescription, String seriesInstanceUID) { /** * @see DICOM standard PS 3.3 - § C.17.6.1 Key Object Document Series Module * * @note Series of Key Object Selection Documents are separate from Series of Images or other Composite SOP * Instances. Key Object Documents do not reside in a Series of Images or other Composite SOP Instances. */ /** * @note Loads properties that reference all "Key Object Codes" defined in the following resource : * KeyObjectSelectionCodes.xml * * @see These Codes are up to date regarding Dicom Conformance : <br> * PS 3.16 - § Context ID 7010 Key Object Selection Document Title <br> * PS 3.16 - § Context ID 7011 Rejected for Quality Reasons - <br> * PS 3.16 - § Context ID 7012 Best In Set<br> * Correction Proposal - § CP 1152 Parts 16 (Additional document titles for Key Object Selection Document) */ Map<String, KeyObjectSelectionCode> codeByValue = getKeyObjectSelectionMappingResources(); Map<String, Set<KeyObjectSelectionCode>> resourcesByContextID = new HashMap<>(); for (KeyObjectSelectionCode code : codeByValue.values()) { Set<KeyObjectSelectionCode> resourceSet = resourcesByContextID.get(code.contextGroupID); if (resourceSet == null) { resourceSet = new TreeSet<>(); resourcesByContextID.put(code.contextGroupID, resourceSet); } resourceSet.add(code); } /** * Document Title of created KOS - must be one of the values specified by "Context ID 7010" in * KeyObjectSelectionCodes.xml<br> * * @note Default is code [DCM-113000] with following attributes : <br> * Tag.CodingSchemeDesignator = "DCM" <br> * Tag.CodeValue = 113000 <br> * Tag.CodeMeaning = "Of Interest" */ final Attributes documentTitle = codeByValue.get("113000").toCodeItem(); //$NON-NLS-1$ // TODO - the user or some preferences should be able to set this title value from a predefined list of code /** * @note "Document Title Modifier" should be set when "Document Title" meets one of the following case : <br> * - Concept Name = (113001, DCM, "Rejected for Quality Reasons") <br> * - Concept Name = (113010, DCM," Quality Issue") <br> * - Concept Name = (113013, DCM, "Best In Set") * * @see PS 3.16 - Structured Reporting Templates § TID 2010 Key Object Selection */ // TODO - add ability to set "Optional Document Title Modifier" for created KOS from the predefined list of code // final Attributes documentTitleModifier = null; final String seriesNumber = "999"; // A number that identifies the Series. (default: 999) //$NON-NLS-1$ final String instanceNumber = "1"; // A number that identifies the Document. (default: 1) //$NON-NLS-1$ // TODO - add ability to override default instanceNumber and seriesNumber from given parameters in case many // KEY OBJECT DOCUMENT SERIES and KEY OBJECT DOCUMENT are build for the same Study in the same context final int[] patientStudyAttributes = { Tag.SpecificCharacterSet, Tag.StudyDate, Tag.StudyTime, Tag.AccessionNumber, Tag.IssuerOfAccessionNumberSequence, Tag.ReferringPhysicianName, Tag.PatientName, Tag.PatientID, Tag.IssuerOfPatientID, Tag.PatientBirthDate, Tag.PatientSex, Tag.StudyInstanceUID, Tag.StudyID }; Arrays.sort(patientStudyAttributes); /** * @note : Add selected attributes from another Attributes object to this. The specified array of tag values * must be sorted (as by the {@link java.util.Arrays#sort(int[])} method) prior to making this call. */ Attributes dKOS = new Attributes(dicomSourceAttribute, patientStudyAttributes); dKOS.setString(Tag.SOPClassUID, VR.UI, UID.KeyObjectSelectionDocumentStorage); dKOS.setString(Tag.SOPInstanceUID, VR.UI, UIDUtils.createUID()); dKOS.setDate(Tag.ContentDateAndTime, new Date()); dKOS.setString(Tag.Modality, VR.CS, "KO"); //$NON-NLS-1$ dKOS.setNull(Tag.ReferencedPerformedProcedureStepSequence, VR.SQ); dKOS.setString(Tag.SeriesInstanceUID, VR.UI, StringUtil.hasText(seriesInstanceUID) ? seriesInstanceUID : UIDUtils.createUID()); dKOS.setString(Tag.SeriesNumber, VR.IS, seriesNumber); dKOS.setString(Tag.InstanceNumber, VR.IS, instanceNumber); dKOS.setString(Tag.ValueType, VR.CS, "CONTAINER"); //$NON-NLS-1$ dKOS.setString(Tag.ContinuityOfContent, VR.CS, "SEPARATE"); //$NON-NLS-1$ dKOS.newSequence(Tag.ConceptNameCodeSequence, 1).add(documentTitle); dKOS.newSequence(Tag.CurrentRequestedProcedureEvidenceSequence, 1); Attributes templateIdentifier = new Attributes(2); templateIdentifier.setString(Tag.MappingResource, VR.CS, "DCMR"); //$NON-NLS-1$ templateIdentifier.setString(Tag.TemplateIdentifier, VR.CS, "2010"); //$NON-NLS-1$ dKOS.newSequence(Tag.ContentTemplateSequence, 1).add(templateIdentifier); Sequence contentSeq = dKOS.newSequence(Tag.ContentSequence, 1); // !! Dead Code !! uncomment this when documentTitleModifier will be handled (see above) // if (documentTitleModifier != null) { // // Attributes documentTitleModifierSequence = new Attributes(4); // documentTitleModifierSequence.setString(Tag.RelationshipType, VR.CS, "HAS CONCEPT MOD"); // documentTitleModifierSequence.setString(Tag.ValueType, VR.CS, "CODE"); // documentTitleModifierSequence.newSequence(Tag.ConceptNameCodeSequence, 1).add( // makeKOS.toCodeItem("DCM-113011")); // documentTitleModifierSequence.newSequence(Tag.ConceptCodeSequence, 1).add(documentTitleModifier); // // contentSeq.add(documentTitleModifierSequence); // } if (StringUtil.hasText(keyObjectDescription)) { Attributes keyObjectDescriptionSequence = new Attributes(4); keyObjectDescriptionSequence.setString(Tag.RelationshipType, VR.CS, "CONTAINS"); //$NON-NLS-1$ keyObjectDescriptionSequence.setString(Tag.ValueType, VR.CS, "TEXT"); //$NON-NLS-1$ keyObjectDescriptionSequence.newSequence(Tag.ConceptNameCodeSequence, 1) .add(codeByValue.get("113012").toCodeItem()); //$NON-NLS-1$ keyObjectDescriptionSequence.setString(Tag.TextValue, VR.UT, keyObjectDescription); contentSeq.add(keyObjectDescriptionSequence); dKOS.setString(Tag.SeriesDescription, VR.LO, keyObjectDescription); } // TODO - Handle Identical Documents Sequence (see below) /** * @see DICOM standard PS 3.3 - § C.17.6 Key Object Selection Modules && § C.17.6.2.1 Identical Documents * * @note The Unique identifier for the Study (studyInstanceUID) is supposed to be the same as to one of the * referenced image but it's not necessary. Standard says that if the Current Requested Procedure Evidence * Sequence (0040,A375) references SOP Instances both in the current study and in one or more other * studies, this document shall be duplicated into each of those other studies, and the duplicates shall * be referenced in the Identical Documents Sequence (0040,A525). */ return dKOS; } static Map<String, KeyObjectSelectionCode> getKeyObjectSelectionMappingResources() { Map<String, KeyObjectSelectionCode> codeByValue = new HashMap<>(); XMLStreamReader xmler = null; InputStream stream = null; try { XMLInputFactory xmlif = XMLInputFactory.newInstance(); stream = DicomMediaUtils.class.getResourceAsStream("/config/KeyObjectSelectionCodes.xml"); //$NON-NLS-1$ xmler = xmlif.createXMLStreamReader(stream); while (xmler.hasNext()) { switch (xmler.next()) { case XMLStreamConstants.START_ELEMENT: String key = xmler.getName().getLocalPart(); if ("resources".equals(key)) { //$NON-NLS-1$ while (xmler.hasNext()) { switch (xmler.next()) { case XMLStreamConstants.START_ELEMENT: readCodeResource(xmler, codeByValue); break; default: break; } } } break; default: break; } } } catch (XMLStreamException e) { LOGGER.error("Reading KO Codes", e); //$NON-NLS-1$ codeByValue = null; } finally { FileUtil.safeClose(xmler); FileUtil.safeClose(stream); } return codeByValue; } private static void readCodeResource(XMLStreamReader xmler, Map<String, KeyObjectSelectionCode> codeByValue) throws XMLStreamException { String key = xmler.getName().getLocalPart(); if ("resource".equals(key)) { //$NON-NLS-1$ String resourceName = xmler.getAttributeValue(null, "name"); //$NON-NLS-1$ String contextGroupID = xmler.getAttributeValue(null, "contextId"); //$NON-NLS-1$ while (xmler.hasNext()) { int eventType = xmler.next(); switch (eventType) { case XMLStreamConstants.START_ELEMENT: key = xmler.getName().getLocalPart(); if ("code".equals(key)) { //$NON-NLS-1$ String codingSchemeDesignator = xmler.getAttributeValue(null, "scheme"); //$NON-NLS-1$ String codeValue = xmler.getAttributeValue(null, "value"); //$NON-NLS-1$ String codeMeaning = xmler.getAttributeValue(null, "meaning"); //$NON-NLS-1$ String conceptNameCodeModifier = xmler.getAttributeValue(null, "conceptMod"); //$NON-NLS-1$ String contexGroupIdModifier = xmler.getAttributeValue(null, "contexId"); //$NON-NLS-1$ codeByValue.put(codeValue, new DicomMediaUtils.KeyObjectSelectionCode(resourceName, contextGroupID, codingSchemeDesignator, codeValue, codeMeaning, conceptNameCodeModifier, contexGroupIdModifier)); } break; default: break; } } } } public static class KeyObjectSelectionCode implements Comparable<KeyObjectSelectionCode> { final String resourceName; final String contextGroupID; final String codingSchemeDesignator; final String codeValue; final String codeMeaning; final String conceptNameCodeModifier; final String contexGroupIdModifier; public KeyObjectSelectionCode(String resourceName, String contextGroupID, String codingSchemeDesignator, String codeValue, String codeMeaning, String conceptNameCodeModifier, String contexGroupIdModifier) { this.resourceName = resourceName; this.contextGroupID = contextGroupID; this.codingSchemeDesignator = codingSchemeDesignator; this.codeValue = codeValue; this.codeMeaning = codeMeaning; this.conceptNameCodeModifier = conceptNameCodeModifier; this.contexGroupIdModifier = contexGroupIdModifier; } final Boolean hasConceptModifier() { return conceptNameCodeModifier != null; } @Override public int compareTo(KeyObjectSelectionCode o) { return this.codeValue.compareToIgnoreCase(o.codeValue); } public Attributes toCodeItem() { Attributes attrs = new Attributes(3); attrs.setString(Tag.CodeValue, VR.SH, codeValue); attrs.setString(Tag.CodingSchemeDesignator, VR.SH, codingSchemeDesignator); attrs.setString(Tag.CodeMeaning, VR.LO, codeMeaning); return attrs; } } public static TemporalAccessor getDateFromDicomElement(TagType type, Attributes dicom, int tag, String privateCreatorID, TemporalAccessor defaultValue) { if (dicom == null || !dicom.containsValue(tag)) { return defaultValue; } Date date = dicom.getDate(privateCreatorID, tag); if (date == null) { return defaultValue; } if (TagType.DICOM_DATE == type) { return TagUtil.toLocalDate(date); } else if (TagType.DICOM_TIME == type) { return TagUtil.toLocalTime(date); } return TagUtil.toLocalDateTime(date); } public static TemporalAccessor[] getDatesFromDicomElement(TagType type, Attributes dicom, int tag, String privateCreatorID, TemporalAccessor[] defaultValue) { if (dicom == null || !dicom.containsValue(tag)) { return defaultValue; } Date[] dates = dicom.getDates(privateCreatorID, tag); if (dates == null || dates.length == 0) { return defaultValue; } TemporalAccessor[] vals; if (TagType.DICOM_DATE == type) { vals = new LocalDate[dates.length]; for (int i = 0; i < vals.length; i++) { vals[i] = TagUtil.toLocalDate(dates[i]); } } else if (TagType.DICOM_TIME == type) { vals = new LocalTime[dates.length]; for (int i = 0; i < vals.length; i++) { vals[i] = TagUtil.toLocalTime(dates[i]); } } vals = new LocalDateTime[dates.length]; for (int i = 0; i < vals.length; i++) { vals[i] = TagUtil.toLocalDateTime(dates[i]); } return vals; } public static TemporalAccessor getDateFromDicomElement(XMLStreamReader xmler, String attribute, TagType type, TemporalAccessor defaultValue) { if (attribute != null) { String val = xmler.getAttributeValue(null, attribute); if (val != null) { if (TagType.DICOM_TIME.equals(type)) { return TagD.getDicomTime(val); } else if (TagType.DICOM_DATETIME.equals(type)) { return TagD.getDicomDateTime(null, val); } else { return TagD.getDicomDate(val); } } } return defaultValue; } public static TemporalAccessor[] getDatesFromDicomElement(XMLStreamReader xmler, String attribute, TagType type, TemporalAccessor[] defaultValue) { return getDatesFromDicomElement(xmler, attribute, type, defaultValue, "\\"); //$NON-NLS-1$ } public static TemporalAccessor[] getDatesFromDicomElement(XMLStreamReader xmler, String attribute, TagType type, TemporalAccessor[] defaultValue, String separator) { if (attribute != null) { String val = xmler.getAttributeValue(null, attribute); if (val != null) { String[] strs = val.split(separator); TemporalAccessor[] vals = new TemporalAccessor[strs.length]; for (int i = 0; i < strs.length; i++) { if (TagType.DICOM_TIME.equals(type)) { vals[i] = TagD.getDicomTime(strs[i]); } else if (TagType.DICOM_DATETIME.equals(type)) { vals[i] = TagD.getDicomDateTime(null, strs[i]); } else { vals[i] = TagD.getDicomDate(strs[i]); } } return vals; } } return defaultValue; } public static void fillAttributes(Map<TagW, Object> tags, Attributes dataset) { if (tags != null && dataset != null) { ElementDictionary dic = ElementDictionary.getStandardElementDictionary(); for (Entry<TagW, Object> entry : tags.entrySet()) { fillAttributes(dataset, entry.getKey(), entry.getValue(), dic); } } } public static void fillAttributes(Iterator<Entry<TagW, Object>> iter, Attributes dataset) { if (iter != null && dataset != null) { ElementDictionary dic = ElementDictionary.getStandardElementDictionary(); while (iter.hasNext()) { Entry<TagW, Object> entry = iter.next(); fillAttributes(dataset, entry.getKey(), entry.getValue(), dic); } } } public static void fillAttributes(Attributes dataset, final TagW tag, final Object val, ElementDictionary dic) { if (dataset != null) { TagType type = tag.getType(); int id = tag.getId(); String key = dic.keywordOf(id); if (val == null || !StringUtil.hasLength(key)) { return; } if (tag.isStringFamilyType()) { if (val instanceof String[]) { dataset.setString(id, dic.vrOf(id), (String[]) val); } else { dataset.setString(id, dic.vrOf(id), val.toString()); } } else if (TagType.DICOM_DATE.equals(type) || TagType.DICOM_TIME.equals(type) || TagType.DICOM_DATETIME.equals(type)) { if (val instanceof TemporalAccessor) { dataset.setDate(id, dic.vrOf(id), TagUtil.toLocalDate((TemporalAccessor) val)); } else if (val.getClass().isArray()) { dataset.setDate(id, dic.vrOf(id), TagUtil.toLocalDates(val)); } } else if (TagType.INTEGER.equals(type)) { if (val instanceof Integer) { dataset.setInt(id, dic.vrOf(id), (Integer) val); } else if (val instanceof int[]) { dataset.setInt(id, dic.vrOf(id), (int[]) val); } } else if (TagType.FLOAT.equals(type)) { if (val instanceof Float) { dataset.setFloat(id, dic.vrOf(id), (Float) val); } else if (val instanceof float[]) { dataset.setFloat(id, dic.vrOf(id), (float[]) val); } } else if (TagType.DOUBLE.equals(type)) { if (val instanceof Double) { dataset.setDouble(id, dic.vrOf(id), (Double) val); } else if (val instanceof double[]) { dataset.setDouble(id, dic.vrOf(id), (double[]) val); } } else if (TagType.DICOM_SEQUENCE.equals(type) && val instanceof Attributes[]) { Attributes[] sIn = (Attributes[]) val; Sequence sOut = dataset.newSequence(id, sIn.length); for (Attributes attributes : sIn) { sOut.add(new Attributes(attributes)); } } } } }