Skip to content

Commit

Permalink
DICOM writer: allow different tile sizes for each resolution
Browse files Browse the repository at this point in the history
This provides better precompressed tile support for input formats
that don't have a constant tile size across all resolutions.
  • Loading branch information
melissalinkert committed Apr 12, 2024
1 parent e21ed51 commit b78a93f
Showing 1 changed file with 86 additions and 12 deletions.
98 changes: 86 additions & 12 deletions components/formats-bsd/src/loci/formats/out/DicomWriter.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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());
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -1374,6 +1408,9 @@ public void close() throws IOException {
ifds = null;
tiffSaver = null;
validPixelCount = null;
tileWidthPointer = null;
tileHeightPointer = null;
tileCountPointer = null;

tagProviders.clear();

Expand All @@ -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 --
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -1918,6 +1989,9 @@ private void writeIFDs(int resIndex) throws IOException {
out.seek(ifdStart);

for (int no=0; no<ifds[resIndex].length; no++) {
ifds[resIndex][no].put(IFD.TILE_WIDTH, tileWidth[resIndex]);
ifds[resIndex][no].put(IFD.TILE_LENGTH, tileHeight[resIndex]);

try {
tiffSaver.writeIFD(ifds[resIndex][no], 0, no < ifds[resIndex].length - 1);
}
Expand Down

0 comments on commit b78a93f

Please sign in to comment.