Skip to content

Commit 678fd01

Browse files
authored
Merge pull request #4181 from melissalinkert/ndpi-precompressed
Add support for reading compressed NDPI tiles
2 parents 16a7bf5 + 4f2a96e commit 678fd01

File tree

7 files changed

+521
-170
lines changed

7 files changed

+521
-170
lines changed

components/bio-formats-tools/src/loci/formats/tools/ImageConverter.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1263,6 +1263,14 @@ private boolean doTileConversion(IFormatWriter writer, String outputFile)
12631263
if (writer instanceof DicomWriter ||
12641264
(writer instanceof ImageWriter && ((ImageWriter) writer).getWriter(outputFile) instanceof DicomWriter))
12651265
{
1266+
// if we asked to try a precompressed conversion,
1267+
// then the writer's tile sizes will have been set automatically
1268+
// according to the input data
1269+
// the conversion must then be performed tile-wise to match the tile sizes,
1270+
// even if precompression doesn't end up being possible
1271+
if (precompressed) {
1272+
return true;
1273+
}
12661274
MetadataStore r = reader.getMetadataStore();
12671275
return !(r instanceof IPyramidStore) || ((IPyramidStore) r).getResolutionCount(reader.getSeries()) > 1;
12681276
}

components/formats-bsd/src/loci/formats/in/MinimalTiffReader.java

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -705,4 +705,73 @@ protected void initTiffParser() {
705705
tiffParser.setUse64BitOffsets(use64Bit);
706706
}
707707

708+
/**
709+
* Get the index of the tile corresponding to given IFD (plane)
710+
* and tile XY indexes.
711+
*
712+
* @param ifd IFD for the requested tile's plane
713+
* @param x tile X index
714+
* @param y tile Y index
715+
* @return corresponding tile index
716+
*/
717+
protected int getTileIndex(IFD ifd, int x, int y) throws FormatException {
718+
int rows = (int) ifd.getTilesPerColumn();
719+
int cols = (int) ifd.getTilesPerRow();
720+
721+
if (x < 0 || x >= cols) {
722+
throw new IllegalArgumentException("X index " + x + " not in range [0, " + cols + ")");
723+
}
724+
if (y < 0 || y >= rows) {
725+
throw new IllegalArgumentException("Y index " + y + " not in range [0, " + rows + ")");
726+
}
727+
728+
return (cols * y) + x;
729+
}
730+
731+
protected long getCompressedByteCount(IFD ifd, int x, int y) throws FormatException, IOException {
732+
long[] byteCounts = ifd.getStripByteCounts();
733+
int tileIndex = getTileIndex(ifd, x, y);
734+
byte[] jpegTable = (byte[]) ifd.getIFDValue(IFD.JPEG_TABLES);
735+
int jpegTableBytes = jpegTable == null ? 0 : jpegTable.length - 2;
736+
long expectedBytes = byteCounts[tileIndex];
737+
if (expectedBytes > 0) {
738+
expectedBytes += jpegTableBytes;
739+
}
740+
if (expectedBytes < 0 || expectedBytes > Integer.MAX_VALUE) {
741+
throw new IOException("Invalid compressed tile size: " + expectedBytes);
742+
}
743+
return expectedBytes;
744+
}
745+
746+
protected byte[] copyTile(IFD ifd, byte[] buf, int x, int y) throws FormatException, IOException {
747+
long[] offsets = ifd.getStripOffsets();
748+
long[] byteCounts = ifd.getStripByteCounts();
749+
750+
int tileIndex = getTileIndex(ifd, x, y);
751+
752+
byte[] jpegTable = (byte[]) ifd.getIFDValue(IFD.JPEG_TABLES);
753+
int jpegTableBytes = jpegTable == null ? 0 : jpegTable.length - 2;
754+
long expectedBytes = getCompressedByteCount(ifd, x, y);
755+
756+
if (buf.length < expectedBytes) {
757+
throw new IllegalArgumentException("Tile buffer too small: expected >=" +
758+
expectedBytes + ", got " + buf.length);
759+
}
760+
else if (expectedBytes < 0 || expectedBytes > Integer.MAX_VALUE) {
761+
throw new IOException("Invalid compressed tile size: " + expectedBytes);
762+
}
763+
764+
if (jpegTable != null && expectedBytes > 0) {
765+
System.arraycopy(jpegTable, 0, buf, 0, jpegTable.length - 2);
766+
// skip over the duplicate SOI marker
767+
tiffParser.getStream().seek(offsets[tileIndex] + 2);
768+
tiffParser.getStream().readFully(buf, jpegTable.length - 2, (int) byteCounts[tileIndex]);
769+
}
770+
else if (byteCounts[tileIndex] > 0) {
771+
tiffParser.getStream().seek(offsets[tileIndex]);
772+
tiffParser.getStream().readFully(buf, 0, (int) byteCounts[tileIndex]);
773+
}
774+
return buf;
775+
}
776+
708777
}

components/formats-bsd/src/loci/formats/out/DicomWriter.java

Lines changed: 111 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,9 @@ public class DicomWriter extends FormatWriter implements IExtraMetadataWriter {
113113
private int baseTileHeight = 256;
114114
private int[] tileWidth;
115115
private int[] tileHeight;
116+
private long[] tileWidthPointer;
117+
private long[] tileHeightPointer;
118+
private long[] tileCountPointer;
116119
private PlaneOffset[][] planeOffsets;
117120
private Integer currentPlane = null;
118121
private UIDCreator uids;
@@ -232,14 +235,7 @@ public void saveCompressedBytes(int no, byte[] buf, int x, int y, int w, int h)
232235
LOGGER.debug("savePrecompressedBytes(series={}, resolution={}, no={}, x={}, y={})",
233236
series, resolution, no, x, y);
234237

235-
// TODO: may want better handling of non-tiled "extra" images (e.g. label, macro)
236238
MetadataRetrieve r = getMetadataRetrieve();
237-
if ((!(r instanceof IPyramidStore) ||
238-
((IPyramidStore) r).getResolutionCount(series) == 1) &&
239-
!isFullPlane(x, y, w, h))
240-
{
241-
throw new FormatException("DicomWriter does not allow tiles for non-pyramid images");
242-
}
243239

244240
int bytesPerPixel = FormatTools.getBytesPerPixel(
245241
FormatTools.pixelTypeFromString(
@@ -279,6 +275,13 @@ public void saveCompressedBytes(int no, byte[] buf, int x, int y, int w, int h)
279275
boolean first = x == 0 && y == 0;
280276
boolean last = x + w == getSizeX() && y + h == getSizeY();
281277

278+
int width = getSizeX();
279+
int height = getSizeY();
280+
int sizeZ = r.getPixelsSizeZ(series).getValue().intValue();
281+
282+
int tileCountX = (int) Math.ceil((double) width / tileWidth[resolutionIndex]);
283+
int tileCountY = (int) Math.ceil((double) height / tileHeight[resolutionIndex]);
284+
282285
// the compression type isn't supplied to the writer until
283286
// after setId is called, so metadata that indicates or
284287
// depends on the compression type needs to be set in
@@ -296,6 +299,15 @@ public void saveCompressedBytes(int no, byte[] buf, int x, int y, int w, int h)
296299
if (getTIFFCompression() == TiffCompression.JPEG) {
297300
ifds[resolutionIndex][no].put(IFD.PHOTOMETRIC_INTERPRETATION, PhotoInterp.Y_CB_CR.getCode());
298301
}
302+
303+
out.seek(tileWidthPointer[resolutionIndex]);
304+
out.writeShort((short) getTileSizeX());
305+
out.seek(tileHeightPointer[resolutionIndex]);
306+
out.writeShort((short) getTileSizeY());
307+
out.seek(tileCountPointer[resolutionIndex]);
308+
309+
out.writeBytes(padString(String.valueOf(
310+
tileCountX * tileCountY * sizeZ * r.getChannelCount(series))));
299311
}
300312

301313
out.seek(out.length());
@@ -334,6 +346,17 @@ public void saveCompressedBytes(int no, byte[] buf, int x, int y, int w, int h)
334346
if (ifds[resolutionIndex][no] != null) {
335347
tileByteCounts = (long[]) ifds[resolutionIndex][no].getIFDValue(IFD.TILE_BYTE_COUNTS);
336348
tileOffsets = (long[]) ifds[resolutionIndex][no].getIFDValue(IFD.TILE_OFFSETS);
349+
350+
if (tileByteCounts.length < tileCountX * tileCountY) {
351+
long[] newTileByteCounts = new long[tileCountX * tileCountY];
352+
long[] newTileOffsets = new long[tileCountX * tileCountY];
353+
System.arraycopy(tileByteCounts, 0, newTileByteCounts, 0, tileByteCounts.length);
354+
System.arraycopy(tileOffsets, 0, newTileOffsets, 0, tileOffsets.length);
355+
tileByteCounts = newTileByteCounts;
356+
tileOffsets = newTileOffsets;
357+
ifds[resolutionIndex][no].put(IFD.TILE_BYTE_COUNTS, tileByteCounts);
358+
ifds[resolutionIndex][no].put(IFD.TILE_OFFSETS, tileOffsets);
359+
}
337360
}
338361

339362
if (tileByteCounts != null) {
@@ -367,13 +390,7 @@ public void saveBytes(int no, byte[] buf, int x, int y, int w, int h)
367390
int thisTileHeight = tileHeight[resolutionIndex];
368391

369392
MetadataRetrieve r = getMetadataRetrieve();
370-
if ((!(r instanceof IPyramidStore) ||
371-
((IPyramidStore) r).getResolutionCount(series) == 1) &&
372-
!isFullPlane(x, y, w, h))
373-
{
374-
throw new FormatException("DicomWriter does not allow tiles for non-pyramid images");
375-
}
376-
else if (x % thisTileWidth != 0 || y % thisTileHeight != 0 ||
393+
if (x % thisTileWidth != 0 || y % thisTileHeight != 0 ||
377394
(w != thisTileWidth && x + w != getSizeX()) ||
378395
(h != thisTileHeight && y + h != getSizeY()))
379396
{
@@ -385,6 +402,10 @@ else if (x % thisTileWidth != 0 || y % thisTileHeight != 0 ||
385402
boolean first = x == 0 && y == 0;
386403
boolean last = x + w == getSizeX() && y + h == getSizeY();
387404

405+
int xTiles = (int) Math.ceil((double) getSizeX() / thisTileWidth);
406+
int yTiles = (int) Math.ceil((double) getSizeY() / thisTileHeight);
407+
int sizeZ = r.getPixelsSizeZ(series).getValue().intValue();
408+
388409
// the compression type isn't supplied to the writer until
389410
// after setId is called, so metadata that indicates or
390411
// depends on the compression type needs to be set in
@@ -406,6 +427,15 @@ else if (x % thisTileWidth != 0 || y % thisTileHeight != 0 ||
406427
ifds[resolutionIndex][no].put(IFD.PHOTOMETRIC_INTERPRETATION, PhotoInterp.Y_CB_CR.getCode());
407428
}
408429
}
430+
431+
out.seek(tileWidthPointer[resolutionIndex]);
432+
out.writeShort((short) getTileSizeX());
433+
out.seek(tileHeightPointer[resolutionIndex]);
434+
out.writeShort((short) getTileSizeY());
435+
out.seek(tileCountPointer[resolutionIndex]);
436+
437+
out.writeBytes(padString(String.valueOf(
438+
xTiles * yTiles * sizeZ * r.getChannelCount(series))));
409439
}
410440

411441
// TILED_SPARSE, so the tile coordinates must be written
@@ -498,7 +528,6 @@ else if (x % thisTileWidth != 0 || y % thisTileHeight != 0 ||
498528
// in the IFD
499529
// this tries to calculate the index without assuming sequential tile
500530
// writing, but maybe there is a better way to calculate this?
501-
int xTiles = (int) Math.ceil((double) getSizeX() / tileWidth[resolutionIndex]);
502531
int xTile = x / tileWidth[resolutionIndex];
503532
int yTile = y / tileHeight[resolutionIndex];
504533
int tileIndex = (yTile * xTiles) + xTile;
@@ -508,6 +537,17 @@ else if (x % thisTileWidth != 0 || y % thisTileHeight != 0 ||
508537
if (ifds[resolutionIndex][no] != null) {
509538
tileByteCounts = (long[]) ifds[resolutionIndex][no].getIFDValue(IFD.TILE_BYTE_COUNTS);
510539
tileOffsets = (long[]) ifds[resolutionIndex][no].getIFDValue(IFD.TILE_OFFSETS);
540+
541+
if (tileByteCounts.length < xTiles * yTiles) {
542+
long[] newTileByteCounts = new long[xTiles * yTiles];
543+
long[] newTileOffsets = new long[xTiles * yTiles];
544+
System.arraycopy(tileByteCounts, 0, newTileByteCounts, 0, tileByteCounts.length);
545+
System.arraycopy(tileOffsets, 0, newTileOffsets, 0, tileOffsets.length);
546+
tileByteCounts = newTileByteCounts;
547+
tileOffsets = newTileOffsets;
548+
ifds[resolutionIndex][no].put(IFD.TILE_BYTE_COUNTS, tileByteCounts);
549+
ifds[resolutionIndex][no].put(IFD.TILE_OFFSETS, tileOffsets);
550+
}
511551
}
512552

513553
if (compression == null || compression.equals(CompressionType.UNCOMPRESSED.getCompression())) {
@@ -640,6 +680,9 @@ public void setId(String id) throws FormatException, IOException {
640680
planeOffsets = new PlaneOffset[totalFiles][];
641681
tileWidth = new int[totalFiles];
642682
tileHeight = new int[totalFiles];
683+
tileWidthPointer = new long[totalFiles];
684+
tileHeightPointer = new long[totalFiles];
685+
tileCountPointer = new long[totalFiles];
643686

644687
// create UIDs that must be consistent across all files in the dataset
645688
String specimenUIDValue = uids.getUID();
@@ -739,8 +782,9 @@ public void setId(String id) throws FormatException, IOException {
739782
int tileCountX = (int) Math.ceil((double) width / tileWidth[resolutionIndex]);
740783
int tileCountY = (int) Math.ceil((double) height / tileHeight[resolutionIndex]);
741784
DicomTag numberOfFrames = new DicomTag(NUMBER_OF_FRAMES, IS);
785+
// save space for up to 10 digits
742786
numberOfFrames.value = padString(String.valueOf(
743-
tileCountX * tileCountY * sizeZ * r.getChannelCount(pyramid)));
787+
tileCountX * tileCountY * sizeZ * r.getChannelCount(pyramid)), " ", 10);
744788
tags.add(numberOfFrames);
745789

746790
DicomTag matrixFrames = new DicomTag(TOTAL_PIXEL_MATRIX_FOCAL_PLANES, UL);
@@ -1374,6 +1418,9 @@ public void close() throws IOException {
13741418
ifds = null;
13751419
tiffSaver = null;
13761420
validPixelCount = null;
1421+
tileWidthPointer = null;
1422+
tileHeightPointer = null;
1423+
tileCountPointer = null;
13771424

13781425
tagProviders.clear();
13791426

@@ -1382,33 +1429,46 @@ public void close() throws IOException {
13821429

13831430
@Override
13841431
public int setTileSizeX(int tileSize) throws FormatException {
1385-
// TODO: this currently enforces the same tile size across all resolutions
1386-
// since the tile size is written during setId
1387-
// the tile size should probably be configurable per resolution,
1388-
// for better pre-compressed tile support
13891432
if (currentId == null) {
13901433
baseTileWidth = tileSize;
1434+
return baseTileWidth;
13911435
}
1392-
return baseTileWidth;
1436+
1437+
int resolutionIndex = getIndex(series, resolution);
1438+
tileWidth[resolutionIndex] = tileSize;
1439+
return tileWidth[resolutionIndex];
13931440
}
13941441

13951442
@Override
13961443
public int getTileSizeX() {
1397-
return baseTileWidth;
1444+
if (currentId == null) {
1445+
return baseTileWidth;
1446+
}
1447+
1448+
int resolutionIndex = getIndex(series, resolution);
1449+
return tileWidth[resolutionIndex];
13981450
}
13991451

14001452
@Override
14011453
public int setTileSizeY(int tileSize) throws FormatException {
1402-
// TODO: see note in setTileSizeX above
14031454
if (currentId == null) {
14041455
baseTileHeight = tileSize;
1456+
return baseTileHeight;
14051457
}
1406-
return baseTileHeight;
1458+
1459+
int resolutionIndex = getIndex(series, resolution);
1460+
tileHeight[resolutionIndex] = tileSize;
1461+
return tileHeight[resolutionIndex];
14071462
}
14081463

14091464
@Override
14101465
public int getTileSizeY() {
1411-
return baseTileHeight;
1466+
if (currentId == null) {
1467+
return baseTileHeight;
1468+
}
1469+
1470+
int resolutionIndex = getIndex(series, resolution);
1471+
return tileHeight[resolutionIndex];
14121472
}
14131473

14141474
// -- DicomWriter-specific methods --
@@ -1468,15 +1528,25 @@ private void writeTag(DicomTag tag) throws IOException {
14681528
out.writeShort((short) getStoredLength(tag));
14691529
}
14701530

1531+
int resolutionIndex = getIndex(series, resolution);
14711532
if (tag.attribute == TRANSFER_SYNTAX_UID) {
1472-
transferSyntaxPointer[getIndex(series, resolution)] = out.getFilePointer();
1533+
transferSyntaxPointer[resolutionIndex] = out.getFilePointer();
14731534
}
14741535
else if (tag.attribute == LOSSY_IMAGE_COMPRESSION_METHOD) {
1475-
compressionMethodPointer[getIndex(series, resolution)] = out.getFilePointer();
1536+
compressionMethodPointer[resolutionIndex] = out.getFilePointer();
14761537
}
14771538
else if (tag.attribute == FILE_META_INFO_GROUP_LENGTH) {
14781539
fileMetaLengthPointer = out.getFilePointer();
14791540
}
1541+
else if (tag.attribute == ROWS) {
1542+
tileHeightPointer[resolutionIndex] = out.getFilePointer();
1543+
}
1544+
else if (tag.attribute == COLUMNS) {
1545+
tileWidthPointer[resolutionIndex] = out.getFilePointer();
1546+
}
1547+
else if (tag.attribute == NUMBER_OF_FRAMES) {
1548+
tileCountPointer[resolutionIndex] = out.getFilePointer();
1549+
}
14801550

14811551
// sequences with no items still need to write a SequenceDelimitationItem below
14821552
if (tag.children.size() == 0 && tag.value == null && tag.vr != SQ) {
@@ -1665,6 +1735,17 @@ private String padString(String value, String append) {
16651735
return value + append;
16661736
}
16671737

1738+
private String padString(String value, String append, int length) {
1739+
String rtn = "";
1740+
if (value != null) {
1741+
rtn += value;
1742+
}
1743+
while (rtn.length() < length) {
1744+
rtn += append;
1745+
}
1746+
return rtn;
1747+
}
1748+
16681749
/**
16691750
* @return transfer syntax UID corresponding to the current compression type
16701751
*/
@@ -1919,6 +2000,9 @@ private void writeIFDs(int resIndex) throws IOException {
19192000
out.seek(ifdStart);
19202001

19212002
for (int no=0; no<ifds[resIndex].length; no++) {
2003+
ifds[resIndex][no].put(IFD.TILE_WIDTH, tileWidth[resIndex]);
2004+
ifds[resIndex][no].put(IFD.TILE_LENGTH, tileHeight[resIndex]);
2005+
19222006
try {
19232007
tiffSaver.writeIFD(ifds[resIndex][no], 0, no < ifds[resIndex].length - 1);
19242008
}

0 commit comments

Comments
 (0)