001/* ***** BEGIN LICENSE BLOCK ***** 002 * Version: MPL 1.1/GPL 2.0/LGPL 2.1 003 * 004 * The contents of this file are subject to the Mozilla Public License Version 005 * 1.1 (the "License"); you may not use this file except in compliance with 006 * the License. You may obtain a copy of the License at 007 * http://www.mozilla.org/MPL/ 008 * 009 * Software distributed under the License is distributed on an "AS IS" basis, 010 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License 011 * for the specific language governing rights and limitations under the 012 * License. 013 * 014 * The Original Code is part of dcm4che, an implementation of DICOM(TM) in 015 * Java(TM), hosted at https://github.com/gunterze/dcm4che. 016 * 017 * The Initial Developer of the Original Code is 018 * Agfa Healthcare. 019 * Portions created by the Initial Developer are Copyright (C) 2013 020 * the Initial Developer. All Rights Reserved. 021 * 022 * Contributor(s): 023 * See @authors listed below 024 * 025 * Alternatively, the contents of this file may be used under the terms of 026 * either the GNU General Public License Version 2 or later (the "GPL"), or 027 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), 028 * in which case the provisions of the GPL or the LGPL are applicable instead 029 * of those above. If you wish to allow use of your version of this file only 030 * under the terms of either the GPL or the LGPL, and not to allow others to 031 * use your version of this file under the terms of the MPL, indicate your 032 * decision by deleting the provisions above and replace them with the notice 033 * and other provisions required by the GPL or the LGPL. If you do not delete 034 * the provisions above, a recipient may use your version of this file under 035 * the terms of any one of the MPL, the GPL or the LGPL. 036 * 037 * ***** END LICENSE BLOCK ***** */ 038 039package org.dcm4che3.image; 040 041import java.awt.image.ComponentSampleModel; 042import java.awt.image.DataBuffer; 043import java.awt.image.DataBufferByte; 044import java.awt.image.DataBufferShort; 045import java.awt.image.DataBufferUShort; 046import java.awt.image.Raster; 047 048import org.dcm4che3.data.Tag; 049import org.dcm4che3.data.Attributes; 050import org.dcm4che3.util.ByteUtils; 051 052/** 053 * @author Gunter Zeilinger <gunterze@gmail.com> 054 * 055 */ 056public class LookupTableFactory { 057 058 private final StoredValue storedValue; 059 private float rescaleSlope = 1; 060 private float rescaleIntercept = 0; 061 private LookupTable modalityLUT; 062 private float windowCenter; 063 private float windowWidth; 064 private String voiLUTFunction; // not yet implemented 065 private LookupTable voiLUT; 066 private LookupTable presentationLUT; 067 private boolean inverse; 068 069 public LookupTableFactory(StoredValue storedValue) { 070 this.storedValue = storedValue; 071 } 072 073 public void setModalityLUT(Attributes attrs) { 074 rescaleIntercept = attrs.getFloat(Tag.RescaleIntercept, 0); 075 rescaleSlope = attrs.getFloat(Tag.RescaleSlope, 1); 076 modalityLUT = createLUT(storedValue, 077 attrs.getNestedDataset(Tag.ModalityLUTSequence)); 078 } 079 080 public void setPresentationLUT(Attributes attrs) { 081 Attributes pLUT = attrs.getNestedDataset(Tag.PresentationLUTSequence); 082 if (pLUT != null) { 083 int[] desc = pLUT.getInts(Tag.LUTDescriptor); 084 if (desc != null && desc.length == 3) { 085 int len = desc[0] == 0 ? 0x10000 : desc[0]; 086 presentationLUT = createLUT(new StoredValue.Unsigned(log2(len)), 087 resetOffset(desc), 088 pLUT.getSafeBytes(Tag.LUTData), pLUT.bigEndian()); 089 } 090 } else { 091 String pShape = attrs.getString(Tag.PresentationLUTShape); 092 inverse = (pShape != null 093 ? "INVERSE".equals(pShape) 094 : "MONOCHROME1".equals( 095 attrs.getString(Tag.PhotometricInterpretation))); 096 } 097 } 098 099 private int[] resetOffset(int[] desc) { 100 if (desc[1] == 0) 101 return desc; 102 103 int[] copy = desc.clone(); 104 copy[1] = 0; 105 return copy; 106 } 107 108 public void setWindowCenter(float windowCenter) { 109 this.windowCenter = windowCenter; 110 } 111 112 public void setWindowWidth(float windowWidth) { 113 this.windowWidth = windowWidth; 114 } 115 116 public void setVOI(Attributes img, int windowIndex, int voiLUTIndex, 117 boolean preferWindow) { 118 if (img == null) 119 return; 120 121 Attributes vLUT = img.getNestedDataset(Tag.VOILUTSequence, voiLUTIndex); 122 if (preferWindow || vLUT == null) { 123 float[] wcs = img.getFloats(Tag.WindowCenter); 124 float[] wws = img.getFloats(Tag.WindowWidth); 125 if (wcs != null && wcs.length != 0 126 && wws != null && wws.length != 0) { 127 int index = windowIndex < Math.min(wcs.length, wws.length) 128 ? windowIndex 129 : 0; 130 windowCenter = wcs[index]; 131 windowWidth = wws[index]; 132 return; 133 } 134 } 135 if (vLUT != null) 136 voiLUT = createLUT(modalityLUT != null 137 ? new StoredValue.Unsigned(modalityLUT.outBits) 138 : storedValue, 139 vLUT); 140 } 141 142 private LookupTable createLUT(StoredValue inBits, Attributes attrs) { 143 if (attrs == null) 144 return null; 145 146 return createLUT(inBits, attrs.getInts(Tag.LUTDescriptor), 147 attrs.getSafeBytes(Tag.LUTData), attrs.bigEndian()); 148 } 149 150 private LookupTable createLUT(StoredValue inBits, int[] desc, byte[] data, 151 boolean bigEndian) { 152 153 if (desc == null) 154 return null; 155 156 if (desc.length != 3) 157 return null; 158 159 int len = desc[0] == 0 ? 0x10000 : desc[0]; 160 int offset = (short) desc[1]; 161 int outBits = desc[2]; 162 if (data == null) 163 return null; 164 165 if (data.length == len << 1) { 166 if (outBits > 8) { 167 if (outBits > 16) 168 return null; 169 170 short[] ss = new short[len]; 171 if (bigEndian) 172 for (int i = 0; i < ss.length; i++) 173 ss[i] = (short) ByteUtils.bytesToShortBE(data, i << 1); 174 else 175 for (int i = 0; i < ss.length; i++) 176 ss[i] = (short) ByteUtils.bytesToShortLE(data, i << 1); 177 178 return new ShortLookupTable(inBits, outBits, offset, ss); 179 } 180 // padded high bits -> use low bits 181 data = halfLength(data, bigEndian ? 1 : 0); 182 } 183 if (data.length != len) 184 return null; 185 186 if (outBits > 8) 187 return null; 188 189 return new ByteLookupTable(inBits, outBits, offset, data); 190 } 191 192 static byte[] halfLength(byte[] data, int hilo) { 193 byte[] bs = new byte[data.length >> 1]; 194 for (int i = 0; i < bs.length; i++) 195 bs[i] = data[(i<<1)|hilo]; 196 197 return bs; 198 } 199 200 public LookupTable createLUT(int outBits) { 201 LookupTable lut = combineModalityVOILUT(presentationLUT != null 202 ? log2(presentationLUT.length()) 203 : outBits); 204 if (presentationLUT != null) { 205 lut = lut.combine(presentationLUT.adjustOutBits(outBits)); 206 } else if (inverse) 207 lut.inverse(); 208 return lut; 209 } 210 211 private static int log2(int value) { 212 int i = 0; 213 while ((value>>>i) != 0) 214 ++i; 215 return i-1; 216 } 217 218 private LookupTable combineModalityVOILUT(int outBits) { 219 float m = rescaleSlope; 220 float b = rescaleIntercept; 221 LookupTable modalityLUT = this.modalityLUT; 222 LookupTable lut = this.voiLUT; 223 if (lut == null) { 224 float c = windowCenter; 225 float w = windowWidth; 226 227 if (w == 0 && modalityLUT != null) 228 return modalityLUT.adjustOutBits(outBits); 229 230 int size, offset; 231 StoredValue inBits = modalityLUT != null 232 ? new StoredValue.Unsigned(modalityLUT.outBits) 233 : storedValue; 234 if (w != 0) { 235 size = Math.max(2,Math.abs(Math.round(w/m))); 236 offset = Math.round(c/m-b) - size/2; 237 } else { 238 offset = inBits.minValue(); 239 size = inBits.maxValue() - inBits.minValue() + 1; 240 } 241 lut = outBits > 8 242 ? new ShortLookupTable(inBits, outBits, offset, size, m < 0) 243 : new ByteLookupTable(inBits, outBits, offset, size, m < 0); 244 } else { 245 //TODO consider m+b 246 lut = lut.adjustOutBits(outBits); 247 } 248 return modalityLUT != null ? modalityLUT.combine(lut) : lut; 249 } 250 251 public boolean autoWindowing(Attributes img, Raster raster) { 252 if (modalityLUT != null || voiLUT != null || windowWidth != 0) 253 return false; 254 255 int min = img.getInt(Tag.SmallestImagePixelValue, 0); 256 int max = img.getInt(Tag.LargestImagePixelValue, 0); 257 if (max == 0) { 258 int[] min_max; 259 ComponentSampleModel sm = (ComponentSampleModel) raster.getSampleModel(); 260 DataBuffer dataBuffer = raster.getDataBuffer(); 261 switch (dataBuffer.getDataType()) { 262 case DataBuffer.TYPE_BYTE: 263 min_max = calcMinMax(storedValue, sm, 264 ((DataBufferByte) dataBuffer).getData()); 265 break; 266 case DataBuffer.TYPE_USHORT: 267 min_max = calcMinMax(storedValue, sm, 268 ((DataBufferUShort) dataBuffer).getData()); 269 break; 270 case DataBuffer.TYPE_SHORT: 271 min_max = calcMinMax(storedValue, sm, 272 ((DataBufferShort) dataBuffer).getData()); 273 break; 274 default: 275 throw new UnsupportedOperationException( 276 "DataBuffer: "+ dataBuffer.getClass() + " not supported"); 277 } 278 min = min_max[0]; 279 max = min_max[1]; 280 } 281 windowCenter = (min + max + 1) / 2 * rescaleSlope + rescaleIntercept; 282 windowWidth = Math.abs((max + 1 - min) * rescaleSlope); 283 return true; 284 } 285 286 private int[] calcMinMax(StoredValue storedValue, ComponentSampleModel sm, 287 byte[] data) { 288 int min = Integer.MAX_VALUE; 289 int max = Integer.MIN_VALUE; 290 int w = sm.getWidth(); 291 int h = sm.getHeight(); 292 int stride = sm.getScanlineStride(); 293 for (int y = 0; y < h; y++) 294 for (int i = y * stride, end = i + w; i < end;) { 295 int val = storedValue.valueOf(data[i++]); 296 if (val < min) min = val; 297 if (val > max) max = val; 298 } 299 return new int[] { min, max }; 300 } 301 302 private int[] calcMinMax(StoredValue storedValue, ComponentSampleModel sm, 303 short[] data) { 304 int min = Integer.MAX_VALUE; 305 int max = Integer.MIN_VALUE; 306 int w = sm.getWidth(); 307 int h = sm.getHeight(); 308 int stride = sm.getScanlineStride(); 309 for (int y = 0; y < h; y++) 310 for (int i = y * stride, end = i + w; i < end;) { 311 int val = storedValue.valueOf(data[i++]); 312 if (val < min) min = val; 313 if (val > max) max = val; 314 } 315 return new int[] { min, max }; 316 } 317 318}