From b78a93fd0d35a828345d04761cd2b4b0f9fda194 Mon Sep 17 00:00:00 2001 From: Melissa Linkert Date: Fri, 12 Apr 2024 10:58:50 -0500 Subject: [PATCH] DICOM writer: allow different tile sizes for each resolution This provides better precompressed tile support for input formats that don't have a constant tile size across all resolutions. --- .../src/loci/formats/out/DicomWriter.java | 98 ++++++++++++++++--- 1 file changed, 86 insertions(+), 12 deletions(-) diff --git a/components/formats-bsd/src/loci/formats/out/DicomWriter.java b/components/formats-bsd/src/loci/formats/out/DicomWriter.java index b3f25f64dd4..04c0c867444 100644 --- a/components/formats-bsd/src/loci/formats/out/DicomWriter.java +++ b/components/formats-bsd/src/loci/formats/out/DicomWriter.java @@ -113,6 +113,9 @@ public class DicomWriter extends FormatWriter implements IExtraMetadataWriter { private int baseTileHeight = 256; private int[] tileWidth; private int[] tileHeight; + private long[] tileWidthPointer; + private long[] tileHeightPointer; + private long[] tileCountPointer; private PlaneOffset[][] planeOffsets; private Integer currentPlane = null; private UIDCreator uids; @@ -279,6 +282,13 @@ public void saveCompressedBytes(int no, byte[] buf, int x, int y, int w, int h) boolean first = x == 0 && y == 0; boolean last = x + w == getSizeX() && y + h == getSizeY(); + int width = getSizeX(); + int height = getSizeY(); + int sizeZ = r.getPixelsSizeZ(series).getValue().intValue(); + + int tileCountX = (int) Math.ceil((double) width / tileWidth[resolutionIndex]); + int tileCountY = (int) Math.ceil((double) height / tileHeight[resolutionIndex]); + // the compression type isn't supplied to the writer until // after setId is called, so metadata that indicates or // depends on the compression type needs to be set in @@ -296,6 +306,15 @@ public void saveCompressedBytes(int no, byte[] buf, int x, int y, int w, int h) if (getTIFFCompression() == TiffCompression.JPEG) { ifds[resolutionIndex][no].put(IFD.PHOTOMETRIC_INTERPRETATION, PhotoInterp.Y_CB_CR.getCode()); } + + out.seek(tileWidthPointer[resolutionIndex]); + out.writeShort((short) getTileSizeX()); + out.seek(tileHeightPointer[resolutionIndex]); + out.writeShort((short) getTileSizeY()); + out.seek(tileCountPointer[resolutionIndex]); + + out.writeBytes(padString(String.valueOf( + tileCountX * tileCountY * sizeZ * r.getChannelCount(series)))); } out.seek(out.length()); @@ -334,6 +353,17 @@ public void saveCompressedBytes(int no, byte[] buf, int x, int y, int w, int h) if (ifds[resolutionIndex][no] != null) { tileByteCounts = (long[]) ifds[resolutionIndex][no].getIFDValue(IFD.TILE_BYTE_COUNTS); tileOffsets = (long[]) ifds[resolutionIndex][no].getIFDValue(IFD.TILE_OFFSETS); + + if (tileByteCounts.length < tileCountX * tileCountY) { + long[] newTileByteCounts = new long[tileCountX * tileCountY]; + long[] newTileOffsets = new long[tileCountX * tileCountY]; + System.arraycopy(tileByteCounts, 0, newTileByteCounts, 0, tileByteCounts.length); + System.arraycopy(tileOffsets, 0, newTileOffsets, 0, tileOffsets.length); + tileByteCounts = newTileByteCounts; + tileOffsets = newTileOffsets; + ifds[resolutionIndex][no].put(IFD.TILE_BYTE_COUNTS, tileByteCounts); + ifds[resolutionIndex][no].put(IFD.TILE_OFFSETS, tileOffsets); + } } if (tileByteCounts != null) { @@ -640,6 +670,9 @@ public void setId(String id) throws FormatException, IOException { planeOffsets = new PlaneOffset[totalFiles][]; tileWidth = new int[totalFiles]; tileHeight = new int[totalFiles]; + tileWidthPointer = new long[totalFiles]; + tileHeightPointer = new long[totalFiles]; + tileCountPointer = new long[totalFiles]; // create UIDs that must be consistent across all files in the dataset String specimenUIDValue = uids.getUID(); @@ -739,8 +772,9 @@ public void setId(String id) throws FormatException, IOException { int tileCountX = (int) Math.ceil((double) width / tileWidth[resolutionIndex]); int tileCountY = (int) Math.ceil((double) height / tileHeight[resolutionIndex]); DicomTag numberOfFrames = new DicomTag(NUMBER_OF_FRAMES, IS); + // save space for up to 10 digits numberOfFrames.value = padString(String.valueOf( - tileCountX * tileCountY * sizeZ * r.getChannelCount(pyramid))); + tileCountX * tileCountY * sizeZ * r.getChannelCount(pyramid)), " ", 10); tags.add(numberOfFrames); DicomTag matrixFrames = new DicomTag(TOTAL_PIXEL_MATRIX_FOCAL_PLANES, UL); @@ -1374,6 +1408,9 @@ public void close() throws IOException { ifds = null; tiffSaver = null; validPixelCount = null; + tileWidthPointer = null; + tileHeightPointer = null; + tileCountPointer = null; tagProviders.clear(); @@ -1382,33 +1419,46 @@ public void close() throws IOException { @Override public int setTileSizeX(int tileSize) throws FormatException { - // TODO: this currently enforces the same tile size across all resolutions - // since the tile size is written during setId - // the tile size should probably be configurable per resolution, - // for better pre-compressed tile support if (currentId == null) { baseTileWidth = tileSize; + return baseTileWidth; } - return baseTileWidth; + + int resolutionIndex = getIndex(series, resolution); + tileWidth[resolutionIndex] = tileSize; + return tileWidth[resolutionIndex]; } @Override public int getTileSizeX() { - return baseTileWidth; + if (currentId == null) { + return baseTileWidth; + } + + int resolutionIndex = getIndex(series, resolution); + return tileWidth[resolutionIndex]; } @Override public int setTileSizeY(int tileSize) throws FormatException { - // TODO: see note in setTileSizeX above if (currentId == null) { baseTileHeight = tileSize; + return baseTileHeight; } - return baseTileHeight; + + int resolutionIndex = getIndex(series, resolution); + tileHeight[resolutionIndex] = tileSize; + return tileHeight[resolutionIndex]; } @Override public int getTileSizeY() { - return baseTileHeight; + if (currentId == null) { + return baseTileHeight; + } + + int resolutionIndex = getIndex(series, resolution); + return tileHeight[resolutionIndex]; } // -- DicomWriter-specific methods -- @@ -1468,15 +1518,25 @@ private void writeTag(DicomTag tag) throws IOException { out.writeShort((short) getStoredLength(tag)); } + int resolutionIndex = getIndex(series, resolution); if (tag.attribute == TRANSFER_SYNTAX_UID) { - transferSyntaxPointer[getIndex(series, resolution)] = out.getFilePointer(); + transferSyntaxPointer[resolutionIndex] = out.getFilePointer(); } else if (tag.attribute == LOSSY_IMAGE_COMPRESSION_METHOD) { - compressionMethodPointer[getIndex(series, resolution)] = out.getFilePointer(); + compressionMethodPointer[resolutionIndex] = out.getFilePointer(); } else if (tag.attribute == FILE_META_INFO_GROUP_LENGTH) { fileMetaLengthPointer = out.getFilePointer(); } + else if (tag.attribute == ROWS) { + tileHeightPointer[resolutionIndex] = out.getFilePointer(); + } + else if (tag.attribute == COLUMNS) { + tileWidthPointer[resolutionIndex] = out.getFilePointer(); + } + else if (tag.attribute == NUMBER_OF_FRAMES) { + tileCountPointer[resolutionIndex] = out.getFilePointer(); + } // sequences with no items still need to write a SequenceDelimitationItem below if (tag.children.size() == 0 && tag.value == null && tag.vr != SQ) { @@ -1665,6 +1725,17 @@ private String padString(String value, String append) { return value + append; } + private String padString(String value, String append, int length) { + String rtn = ""; + if (value != null) { + rtn += value; + } + while (rtn.length() < length) { + rtn += append; + } + return rtn; + } + /** * @return transfer syntax UID corresponding to the current compression type */ @@ -1918,6 +1989,9 @@ private void writeIFDs(int resIndex) throws IOException { out.seek(ifdStart); for (int no=0; no