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.imageio.plugins.dcm; 040 041import java.awt.image.BufferedImage; 042import java.awt.image.ColorModel; 043import java.awt.image.DataBuffer; 044import java.awt.image.DataBufferByte; 045import java.awt.image.DataBufferUShort; 046import java.awt.image.Raster; 047import java.awt.image.SampleModel; 048import java.awt.image.WritableRaster; 049import java.io.ByteArrayInputStream; 050import java.io.File; 051import java.io.FileNotFoundException; 052import java.io.IOException; 053import java.nio.ByteOrder; 054import java.util.Collections; 055import java.util.Iterator; 056 057import javax.imageio.ImageReadParam; 058import javax.imageio.ImageReader; 059import javax.imageio.ImageTypeSpecifier; 060import javax.imageio.metadata.IIOMetadata; 061import javax.imageio.spi.ImageReaderSpi; 062import javax.imageio.stream.FileImageInputStream; 063import javax.imageio.stream.ImageInputStream; 064import javax.imageio.stream.ImageInputStreamImpl; 065import javax.imageio.stream.MemoryCacheImageInputStream; 066 067import org.dcm4che3.data.Tag; 068import org.dcm4che3.data.UID; 069import org.dcm4che3.data.Attributes; 070import org.dcm4che3.data.BulkData; 071import org.dcm4che3.data.Fragments; 072import org.dcm4che3.data.Sequence; 073import org.dcm4che3.data.VR; 074import org.dcm4che3.image.LookupTable; 075import org.dcm4che3.image.LookupTableFactory; 076import org.dcm4che3.image.Overlays; 077import org.dcm4che3.image.PhotometricInterpretation; 078import org.dcm4che3.image.StoredValue; 079import org.dcm4che3.imageio.codec.ImageReaderFactory; 080import org.dcm4che3.imageio.codec.ImageReaderFactory.ImageReaderParam; 081import org.dcm4che3.imageio.codec.jpeg.PatchJPEGLS; 082import org.dcm4che3.imageio.codec.jpeg.PatchJPEGLSImageInputStream; 083import org.dcm4che3.imageio.stream.ImageInputStreamAdapter; 084import org.dcm4che3.imageio.stream.SegmentedImageInputStream; 085import org.dcm4che3.io.BulkDataDescriptor; 086import org.dcm4che3.io.DicomInputStream; 087import org.dcm4che3.io.DicomInputStream.IncludeBulkData; 088import org.dcm4che3.util.ByteUtils; 089import org.slf4j.Logger; 090import org.slf4j.LoggerFactory; 091 092/** 093 * @author Gunter Zeilinger <gunterze@gmail.com> 094 * @since Feb 2013 095 * 096 */ 097public class DicomImageReader extends ImageReader { 098 099 private static final Logger LOG = LoggerFactory.getLogger(DicomImageReader.class); 100 101 private ImageInputStream iis; 102 103 private Attributes ds; 104 105 private DicomMetaData metadata; 106 107 private int frames; 108 109 private int width; 110 111 private int height; 112 113 private BulkData pixelBulkData; 114 115 private final VR.Holder pixeldataVR = new VR.Holder(); 116 117 private Fragments pixeldataFragments; 118 119 private File pixeldataFile; 120 121 private byte[] pixeldataBytes; 122 123 private ImageReader decompressor; 124 125 private boolean rle; 126 127 private PatchJPEGLS patchJpegLS; 128 129 private int samples; 130 131 private boolean banded; 132 133 private int bitsStored; 134 135 private int bitsAllocated; 136 137 private int dataType; 138 139 private int frameLength; 140 141 private PhotometricInterpretation pmi; 142 143 public DicomImageReader(ImageReaderSpi originatingProvider) { 144 super(originatingProvider); 145 } 146 147 @Override 148 public void setInput(Object input, boolean seekForwardOnly, 149 boolean ignoreMetadata) { 150 super.setInput(input, seekForwardOnly, ignoreMetadata); 151 resetInternalState(); 152 if (input instanceof DicomMetaData) { 153 setMetadata((DicomMetaData) input); 154 if (pixelBulkData != null) { 155 pixeldataFile = pixelBulkData.getFile(); 156 } else if (pixeldataFragments != null && pixeldataFragments.size() > 1) { 157 Object fragment = pixeldataFragments.get(1); 158 if (fragment instanceof BulkData) { 159 pixeldataFile = ((BulkData) fragment).getFile(); 160 } else if (!(fragment instanceof byte[])) { 161 throw new IllegalArgumentException("Fragments is neither BulkData nor byte[]; instead it is " + fragment.getClass()); 162 } 163 } 164 } else { 165 iis = (ImageInputStream) input; 166 } 167 } 168 169 @Override 170 public int getNumImages(boolean allowSearch) throws IOException { 171 readMetadata(); 172 return frames; 173 } 174 175 @Override 176 public int getWidth(int frameIndex) throws IOException { 177 readMetadata(); 178 checkIndex(frameIndex); 179 return width; 180 } 181 182 @Override 183 public int getHeight(int frameIndex) throws IOException { 184 readMetadata(); 185 checkIndex(frameIndex); 186 return height; 187 } 188 189 190 @Override 191 public ImageTypeSpecifier getRawImageType(int frameIndex) 192 throws IOException { 193 readMetadata(); 194 checkIndex(frameIndex); 195 196 if (decompressor == null) 197 return createImageType(bitsStored, dataType, banded); 198 199 if (rle) 200 return createImageType(bitsStored, dataType, true); 201 202 openiis(); 203 try { 204 decompressor.setInput(iisOfFrame(0)); 205 return decompressor.getRawImageType(0); 206 } finally { 207 closeiis(); 208 } 209 } 210 211 @Override 212 public Iterator<ImageTypeSpecifier> getImageTypes(int frameIndex) 213 throws IOException { 214 readMetadata(); 215 checkIndex(frameIndex); 216 217 ImageTypeSpecifier imageType; 218 if (pmi.isMonochrome()) 219 imageType = createImageType(8, DataBuffer.TYPE_BYTE, false); 220 else if (decompressor == null) 221 imageType = createImageType(bitsStored, dataType, banded); 222 else if (rle) 223 imageType = createImageType(bitsStored, dataType, true); 224 else { 225 openiis(); 226 try { 227 decompressor.setInput(iisOfFrame(0)); 228 return decompressor.getImageTypes(0); 229 } finally { 230 closeiis(); 231 } 232 } 233 234 return Collections.singletonList(imageType).iterator(); 235 } 236 237 private void openiis() throws FileNotFoundException, IOException { 238 if (iis == null) { 239 if (pixeldataFile != null) { 240 iis = new FileImageInputStream(pixeldataFile); 241 } else if (pixeldataBytes != null) { 242 iis = new MemoryCacheImageInputStream(new ByteArrayInputStream(pixeldataBytes)); 243 } else if (pixeldataFragments != null) { 244 iis = new MemoryCacheImageInputStream( 245 new ByteArrayInputStream((byte[]) pixeldataFragments.get(1))); 246 } 247 } 248 } 249 250 private void closeiis() throws IOException { 251 if (pixeldataFile != null && iis != null) { 252 iis.close(); 253 iis = null; 254 } 255 } 256 257 @Override 258 public ImageReadParam getDefaultReadParam() { 259 return new DicomImageReadParam(); 260 } 261 262 @Override 263 public IIOMetadata getStreamMetadata() throws IOException { 264 readMetadata(); 265 return metadata; 266 } 267 268 @Override 269 public IIOMetadata getImageMetadata(int frameIndex) throws IOException { 270 return null; 271 } 272 273 @Override 274 public boolean canReadRaster() { 275 return true; 276 } 277 278 @Override 279 public Raster readRaster(int frameIndex, ImageReadParam param) 280 throws IOException { 281 readMetadata(); 282 checkIndex(frameIndex); 283 284 openiis(); 285 try { 286 if (decompressor != null) { 287 decompressor.setInput(iisOfFrame(frameIndex)); 288 289 if (LOG.isDebugEnabled()) 290 LOG.debug("Start decompressing frame #" + (frameIndex + 1)); 291 Raster wr = pmi.decompress() == pmi && decompressor.canReadRaster() 292 ? decompressor.readRaster(0, decompressParam(param)) 293 : decompressor.read(0, decompressParam(param)).getRaster(); 294 if (LOG.isDebugEnabled()) 295 LOG.debug("Finished decompressing frame #" + (frameIndex + 1)); 296 return wr; 297 } 298 iis.setByteOrder(ds.bigEndian() 299 ? ByteOrder.BIG_ENDIAN 300 : ByteOrder.LITTLE_ENDIAN); 301 if (pixelBulkData != null) { 302 iis.seek(pixelBulkData.offset() + frameIndex * frameLength); 303 } else if (pixeldataBytes != null) { 304 iis.seek(frameIndex * frameLength); 305 } else if (pixeldataFragments != null && pixeldataFile == null) { 306 throw new RuntimeException("fix this?"); 307 } 308 309 WritableRaster wr = Raster.createWritableRaster( 310 createSampleModel(dataType, banded), null); 311 DataBuffer buf = wr.getDataBuffer(); 312 if (buf instanceof DataBufferByte) { 313 byte[][] data = ((DataBufferByte) buf).getBankData(); 314 for (byte[] bs : data) 315 iis.readFully(bs); 316 if (pixelBulkData.bigEndian && pixeldataVR.vr == VR.OW) 317 ByteUtils.swapShorts(data); 318 } else { 319 short[] data = ((DataBufferUShort) buf).getData(); 320 iis.readFully(data, 0, data.length); 321 } 322 return wr; 323 } finally { 324 closeiis(); 325 } 326 } 327 328 private ImageReadParam decompressParam(ImageReadParam param) { 329 ImageReadParam decompressParam = decompressor.getDefaultReadParam(); 330 ImageTypeSpecifier imageType = null; 331 BufferedImage dest = null; 332 if (param != null) { 333 imageType = param.getDestinationType(); 334 dest = param.getDestination(); 335 } 336 if (rle && imageType == null && dest == null) 337 imageType = createImageType(bitsStored, dataType, true); 338 decompressParam.setDestinationType(imageType); 339 decompressParam.setDestination(dest); 340 return decompressParam; 341 } 342 343 @Override 344 public BufferedImage read(int frameIndex, ImageReadParam param) 345 throws IOException { 346 readMetadata(); 347 checkIndex(frameIndex); 348 349 WritableRaster raster; 350 if (decompressor != null) { 351 openiis(); 352 try { 353 decompressor.setInput(iisOfFrame(frameIndex)); 354 if (LOG.isDebugEnabled()) 355 LOG.debug("Start decompressing frame #" + (frameIndex + 1)); 356 BufferedImage bi = decompressor.read(0, decompressParam(param)); 357 if (LOG.isDebugEnabled()) 358 LOG.debug("Finished decompressing frame #" + (frameIndex + 1)); 359 if (samples > 1) 360 return bi; 361 362 raster = bi.getRaster(); 363 } finally { 364 closeiis(); 365 } 366 } else 367 raster = (WritableRaster) readRaster(frameIndex, param); 368 369 ColorModel cm; 370 if (pmi.isMonochrome()) { 371 int[] overlayGroupOffsets = getActiveOverlayGroupOffsets(param); 372 byte[][] overlayData = new byte[overlayGroupOffsets.length][]; 373 for (int i = 0; i < overlayGroupOffsets.length; i++) { 374 overlayData[i] = extractOverlay(overlayGroupOffsets[i], raster); 375 } 376 cm = createColorModel(8, DataBuffer.TYPE_BYTE); 377 SampleModel sm = createSampleModel(DataBuffer.TYPE_BYTE, false); 378 raster = applyLUTs(raster, frameIndex, param, sm, 8); 379 for (int i = 0; i < overlayGroupOffsets.length; i++) { 380 applyOverlay(overlayGroupOffsets[i], 381 raster, frameIndex, param, 8, overlayData[i]); 382 } 383 } else { 384 cm = createColorModel(bitsStored, dataType); 385 } 386 return new BufferedImage(cm, raster , false, null); 387 } 388 389 private byte[] extractOverlay(int gg0000, WritableRaster raster) { 390 Attributes attrs = metadata.getAttributes(); 391 392 if (attrs.getInt(Tag.OverlayBitsAllocated | gg0000, 1) == 1) 393 return null; 394 395 int ovlyRows = attrs.getInt(Tag.OverlayRows | gg0000, 0); 396 int ovlyColumns = attrs.getInt(Tag.OverlayColumns | gg0000, 0); 397 int bitPosition = attrs.getInt(Tag.OverlayBitPosition | gg0000, 0); 398 399 int mask = 1<<bitPosition; 400 int length = ovlyRows * ovlyColumns; 401 402 byte[] ovlyData = new byte[(((length+7)>>>3)+1)&(~1)] ; 403 Overlays.extractFromPixeldata(raster, mask, ovlyData, 0, length); 404 return ovlyData; 405 } 406 407 @SuppressWarnings("resource") 408 private ImageInputStreamImpl iisOfFrame(int frameIndex) 409 throws IOException { 410 SegmentedImageInputStream siis = SegmentedImageInputStream.ofFrame( 411 iis, pixeldataFragments, frameIndex, frames); 412 return patchJpegLS != null 413 ? new PatchJPEGLSImageInputStream(siis, patchJpegLS) 414 : siis; 415 } 416 417 private void applyOverlay(int gg0000, WritableRaster raster, 418 int frameIndex, ImageReadParam param, int outBits, byte[] ovlyData) { 419 Attributes ovlyAttrs = metadata.getAttributes(); 420 int grayscaleValue = 0xffff; 421 if (param instanceof DicomImageReadParam) { 422 DicomImageReadParam dParam = (DicomImageReadParam) param; 423 Attributes psAttrs = dParam.getPresentationState(); 424 if (psAttrs != null) { 425 if (psAttrs.containsValue(Tag.OverlayData | gg0000)) 426 ovlyAttrs = psAttrs; 427 grayscaleValue = Overlays.getRecommendedDisplayGrayscaleValue( 428 psAttrs, gg0000); 429 } else 430 grayscaleValue = dParam.getOverlayGrayscaleValue(); 431 } 432 Overlays.applyOverlay(ovlyData != null ? 0 : frameIndex, raster, 433 ovlyAttrs, gg0000, grayscaleValue >>> (16-outBits), ovlyData); 434 } 435 436 private int[] getActiveOverlayGroupOffsets(ImageReadParam param) { 437 if (param instanceof DicomImageReadParam) { 438 DicomImageReadParam dParam = (DicomImageReadParam) param; 439 Attributes psAttrs = dParam.getPresentationState(); 440 if (psAttrs != null) 441 return Overlays.getActiveOverlayGroupOffsets(psAttrs); 442 else 443 return Overlays.getActiveOverlayGroupOffsets( 444 metadata.getAttributes(), 445 dParam.getOverlayActivationMask()); 446 } 447 return Overlays.getActiveOverlayGroupOffsets( 448 metadata.getAttributes(), 449 0xffff); 450 } 451 452 private WritableRaster applyLUTs(WritableRaster raster, 453 int frameIndex, ImageReadParam param, SampleModel sm, int outBits) { 454 WritableRaster destRaster = 455 sm.getDataType() == raster.getSampleModel().getDataType() 456 ? raster 457 : Raster.createWritableRaster(sm, null); 458 Attributes imgAttrs = metadata.getAttributes(); 459 StoredValue sv = StoredValue.valueOf(imgAttrs); 460 LookupTableFactory lutParam = new LookupTableFactory(sv); 461 DicomImageReadParam dParam = param instanceof DicomImageReadParam 462 ? (DicomImageReadParam) param 463 : new DicomImageReadParam(); 464 Attributes psAttrs = dParam.getPresentationState(); 465 if (psAttrs != null) { 466 lutParam.setModalityLUT(psAttrs); 467 lutParam.setVOI( 468 selectVOILUT(psAttrs, 469 imgAttrs.getString(Tag.SOPInstanceUID), 470 frameIndex+1), 471 0, 0, false); 472 lutParam.setPresentationLUT(psAttrs); 473 } else { 474 Attributes sharedFctGroups = imgAttrs.getNestedDataset( 475 Tag.SharedFunctionalGroupsSequence); 476 Attributes frameFctGroups = imgAttrs.getNestedDataset( 477 Tag.PerFrameFunctionalGroupsSequence, frameIndex); 478 lutParam.setModalityLUT( 479 selectFctGroup(imgAttrs, sharedFctGroups, frameFctGroups, 480 Tag.PixelValueTransformationSequence)); 481 if (dParam.getWindowWidth() != 0) { 482 lutParam.setWindowCenter(dParam.getWindowCenter()); 483 lutParam.setWindowWidth(dParam.getWindowWidth()); 484 } else 485 lutParam.setVOI( 486 selectFctGroup(imgAttrs, sharedFctGroups, frameFctGroups, 487 Tag.FrameVOILUTSequence), 488 dParam.getWindowIndex(), 489 dParam.getVOILUTIndex(), 490 dParam.isPreferWindow()); 491 if (dParam.isAutoWindowing()) 492 lutParam.autoWindowing(imgAttrs, raster); 493 lutParam.setPresentationLUT(imgAttrs); 494 } 495 LookupTable lut = lutParam.createLUT(outBits); 496 lut.lookup(raster, destRaster); 497 return destRaster; 498 } 499 500 private Attributes selectFctGroup(Attributes imgAttrs, 501 Attributes sharedFctGroups, 502 Attributes frameFctGroups, 503 int tag) { 504 if (frameFctGroups == null) { 505 return imgAttrs; 506 } 507 Attributes group = frameFctGroups.getNestedDataset(tag); 508 if (group == null && sharedFctGroups != null) { 509 group = sharedFctGroups.getNestedDataset(tag); 510 } 511 return group != null ? group : imgAttrs; 512 } 513 514 private Attributes selectVOILUT(Attributes psAttrs, String iuid, int frame) { 515 Sequence voiLUTs = psAttrs.getSequence(Tag.SoftcopyVOILUTSequence); 516 if (voiLUTs != null) 517 for (Attributes voiLUT : voiLUTs) { 518 Sequence refImgs = voiLUT.getSequence(Tag.ReferencedImageSequence); 519 if (refImgs == null || refImgs.isEmpty()) 520 return voiLUT; 521 for (Attributes refImg : refImgs) { 522 if (iuid.equals(refImg.getString(Tag.ReferencedSOPInstanceUID))) { 523 int[] refFrames = refImg.getInts(Tag.ReferencedFrameNumber); 524 if (refFrames == null) 525 return voiLUT; 526 527 for (int refFrame : refFrames) 528 if (refFrame == frame) 529 return voiLUT; 530 } 531 } 532 } 533 return null; 534 } 535 536 private void readMetadata() throws IOException { 537 if (metadata != null) 538 return; 539 540 if (iis == null) 541 throw new IllegalStateException("Input not set"); 542 543 @SuppressWarnings("resource") 544 DicomInputStream dis = new DicomInputStream(new ImageInputStreamAdapter(iis)); 545 dis.setIncludeBulkData(IncludeBulkData.URI); 546 dis.setBulkDataDescriptor(BulkDataDescriptor.PIXELDATA); 547 dis.setURI("java:iis"); // avoid copy of pixeldata to temporary file 548 Attributes fmi = dis.readFileMetaInformation(); 549 Attributes ds = dis.readDataset(-1, -1); 550 setMetadata(new DicomMetaData(fmi, ds)); 551 } 552 553 private void setMetadata(DicomMetaData metadata) { 554 this.metadata = metadata; 555 this.ds = metadata.getAttributes(); 556 Object pixeldata = ds.getValue(Tag.PixelData, pixeldataVR ); 557 if (pixeldata != null) { 558 frames = ds.getInt(Tag.NumberOfFrames, 1); 559 width = ds.getInt(Tag.Columns, 0); 560 height = ds.getInt(Tag.Rows, 0); 561 samples = ds.getInt(Tag.SamplesPerPixel, 1); 562 banded = samples > 1 && ds.getInt(Tag.PlanarConfiguration, 0) != 0; 563 bitsAllocated = ds.getInt(Tag.BitsAllocated, 8); 564 bitsStored = ds.getInt(Tag.BitsStored, bitsAllocated); 565 dataType = bitsAllocated <= 8 ? DataBuffer.TYPE_BYTE 566 : DataBuffer.TYPE_USHORT; 567 pmi = PhotometricInterpretation.fromString( 568 ds.getString(Tag.PhotometricInterpretation, "MONOCHROME2")); 569 if (pixeldata instanceof BulkData) { 570 this.frameLength = pmi.frameLength(width, height, samples, bitsAllocated); 571 this.pixelBulkData = (BulkData) pixeldata; 572 } else if (pixeldata instanceof byte[]) { 573 this.frameLength = pmi.frameLength(width, height, samples, bitsAllocated); 574 this.pixeldataBytes = (byte[]) pixeldata; 575 } else { // Fragments 576 Attributes fmi = metadata.getFileMetaInformation(); 577 if (fmi == null) 578 throw new IllegalArgumentException("Missing File Meta Information for Data Set with compressed Pixel Data"); 579 580 String tsuid = fmi.getString(Tag.TransferSyntaxUID); 581 ImageReaderParam param = 582 ImageReaderFactory.getImageReaderParam(tsuid); 583 if (param == null) 584 throw new UnsupportedOperationException("Unsupported Transfer Syntax: " + tsuid); 585 this.rle = tsuid.equals(UID.RLELossless); 586 this.decompressor = ImageReaderFactory.getImageReader(param); 587 this.patchJpegLS = param.patchJPEGLS; 588 this.pixeldataFragments = (Fragments) pixeldata; 589 } 590 } 591 } 592 593 private SampleModel createSampleModel(int dataType, boolean banded) { 594 return pmi.createSampleModel(dataType, width, height, samples, banded); 595 } 596 597 private ImageTypeSpecifier createImageType(int bits, int dataType, boolean banded) { 598 return new ImageTypeSpecifier( 599 createColorModel(bits, dataType), 600 createSampleModel(dataType, banded)); 601 } 602 603 private ColorModel createColorModel(int bits, int dataType) { 604 return pmi.createColorModel(bits, dataType, metadata.getAttributes()); 605 } 606 607 private void resetInternalState() { 608 metadata = null; 609 ds = null; 610 pixeldataFile = null; 611 frames = 0; 612 width = 0; 613 height = 0; 614 pixelBulkData = null; 615 frameLength = 0; 616 pixeldataFragments = null; 617 pixeldataBytes = null; 618 if (decompressor != null) { 619 decompressor.dispose(); 620 decompressor = null; 621 } 622 patchJpegLS = null; 623 pmi = null; 624 } 625 626 private void checkIndex(int frameIndex) { 627 if (frames == 0) 628 throw new IllegalStateException("Missing Pixel Data"); 629 630 if (frameIndex < 0 || frameIndex >= frames) 631 throw new IndexOutOfBoundsException("imageIndex: " + frameIndex); 632 } 633 634 @Override 635 public void dispose() { 636 resetInternalState(); 637 } 638 639}