diff --git a/README.md b/README.md index e80e045..87240c0 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Here is a sample script that shows how to use the library from QuPath: ```groovy import qupath.ext.imglib2.ImgCreator -import qupath.ext.imglib2.Dimension +import qupath.ext.imglib2.ImgLib2ImageServer import net.imglib2.type.numeric.ARGBType @@ -45,10 +45,15 @@ println safeImg // For example, to read the pixel located at [x:1, y:2; c:0; z:0; t:0]: var randomAccess = randomAccessible.randomAccess() -var position = new long[Dimension.values().length] -position[ImgCreator.getIndexOfDimension(Dimension.X)] = 1 -position[ImgCreator.getIndexOfDimension(Dimension.Y)] = 2 +var position = new long[ImgCreator.NUMBER_OF_AXES] +position[ImgCreator.AXIS_X] = 1 +position[ImgCreator.AXIS_Y] = 2 var pixel = randomAccess.setPositionAndGet(position) println pixel + + +// It is also possible to create an ImageServer from a RandomAccessible or Img. +var newServer = ImgLib2ImageServer.builder(List.of(randomAccessible)).build() +println newServer ``` diff --git a/src/main/java/qupath/ext/imglib2/ImgLib2ImageServer.java b/src/main/java/qupath/ext/imglib2/ImgLib2ImageServer.java new file mode 100644 index 0000000..9dcd295 --- /dev/null +++ b/src/main/java/qupath/ext/imglib2/ImgLib2ImageServer.java @@ -0,0 +1,605 @@ +package qupath.ext.imglib2; + +import net.imglib2.Cursor; +import net.imglib2.RandomAccessibleInterval; +import net.imglib2.type.NativeType; +import net.imglib2.type.numeric.ARGBType; +import net.imglib2.type.numeric.NumericType; +import net.imglib2.type.numeric.integer.ByteType; +import net.imglib2.type.numeric.integer.IntType; +import net.imglib2.type.numeric.integer.ShortType; +import net.imglib2.type.numeric.integer.UnsignedByteType; +import net.imglib2.type.numeric.integer.UnsignedIntType; +import net.imglib2.type.numeric.integer.UnsignedShortType; +import net.imglib2.type.numeric.real.DoubleType; +import net.imglib2.type.numeric.real.FloatType; +import net.imglib2.view.Views; +import qupath.lib.color.ColorModelFactory; +import qupath.lib.images.servers.AbstractTileableImageServer; +import qupath.lib.images.servers.ImageChannel; +import qupath.lib.images.servers.ImageServerBuilder; +import qupath.lib.images.servers.ImageServerMetadata; +import qupath.lib.images.servers.PixelCalibration; +import qupath.lib.images.servers.PixelType; +import qupath.lib.images.servers.TileRequest; + +import java.awt.image.BandedSampleModel; +import java.awt.image.BufferedImage; +import java.awt.image.DataBuffer; +import java.awt.image.DataBufferByte; +import java.awt.image.DataBufferDouble; +import java.awt.image.DataBufferFloat; +import java.awt.image.DataBufferInt; +import java.awt.image.DataBufferShort; +import java.awt.image.DataBufferUShort; +import java.awt.image.WritableRaster; +import java.net.URI; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.IntStream; + +/** + * An {@link qupath.lib.images.servers.ImageServer} whose pixel values come from {@link RandomAccessibleInterval}. + *

+ * Use a {@link #builder(List)} to create an instance of this class. + *

+ * This server doesn't support JSON serialization. + * + * @param the pixel type of the underlying {@link RandomAccessibleInterval} + */ +public class ImgLib2ImageServer & NumericType> extends AbstractTileableImageServer { + + private static final AtomicInteger counter = new AtomicInteger(); + private final List> accessibles; + private final ImageServerMetadata metadata; + private final int numberOfChannelsInAccessibles; + + private ImgLib2ImageServer(List> accessibles, PixelType pixelType, ImageServerMetadata metadata) { + this.accessibles = accessibles; + + RandomAccessibleInterval firstAccessible = accessibles.getFirst(); + T value = firstAccessible.firstElement(); + this.metadata = new ImageServerMetadata.Builder(metadata) + .width((int) firstAccessible.dimension(ImgCreator.AXIS_X)) + .height((int) firstAccessible.dimension(ImgCreator.AXIS_Y)) + .rgb(value instanceof ARGBType) + .pixelType(pixelType) + .levels(createResolutionLevels(accessibles)) + .sizeZ((int) firstAccessible.dimension(ImgCreator.AXIS_Z)) + .sizeT((int) firstAccessible.dimension(ImgCreator.AXIS_TIME)) + .build(); + + this.numberOfChannelsInAccessibles = (int) firstAccessible.dimension(ImgCreator.AXIS_CHANNEL); + } + + /** + * Create a {@link ImgLib2ImageServer} builder. + *

+ * The provided accessibles must correspond to the ones returned by functions of {@link ImgCreator}: they must have + * {@link ImgCreator#NUMBER_OF_AXES} dimensions, the X-axes must correspond to {@link ImgCreator#AXIS_X}, and so on. + *

+ * All dimensions of the provided accessibles must contain {@link Integer#MAX_VALUE} pixels or less. + *

+ * The type of the provided accessibles must be {@link ARGBType}, {@link UnsignedByteType}, {@link ByteType}, + * {@link UnsignedShortType}, {@link ShortType}, {@link UnsignedIntType}, {@link IntType}, {@link FloatType}, or + * {@link DoubleType}. If the type is {@link ARGBType}, the provided accessibles must have one channel + * + * @param accessibles one accessible for each resolution level the image server should have, from highest to lowest + * resolution. Must not be empty. Each accessible must have the same number of channels, z-stacks, + * and timepoints + * @throws NullPointerException if the provided list is null or contain a null element + * @throws IllegalArgumentException if the provided list is empty, if the accessible type is not among the list + * mentioned above, if a dimension of a provided accessible contain more than {@link Integer#MAX_VALUE} pixels, + * if the provided accessibles do not have {@link ImgCreator#NUMBER_OF_AXES} axes, if the provided accessibles + * do not have the same number of channels, z-stacks, or timepoints, or if the accessible type is {@link ARGBType} + * and the number of channels of the accessibles is not 1 + */ + public static & NumericType> Builder builder(List> accessibles) { + return new Builder<>(accessibles); + } + + @Override + protected BufferedImage readTile(TileRequest tileRequest) { + RandomAccessibleInterval tile = getImgLib2Tile(tileRequest); + int minTileX = Math.toIntExact(tile.min(ImgCreator.AXIS_X)); + int minTileY = Math.toIntExact(tile.min(ImgCreator.AXIS_Y)); + int minTileC = Math.toIntExact(tile.min(ImgCreator.AXIS_CHANNEL)); + + Cursor cursor = tile.localizingCursor(); + + if (isRGB()) { + return createArgbImage(tileRequest, cursor, minTileX, minTileY); + } else { + int xyPlaneSize = Math.toIntExact(tile.dimension(ImgCreator.AXIS_X) * tile.dimension(ImgCreator.AXIS_Y)); + + DataBuffer dataBuffer = switch (metadata.getPixelType()) { + case UINT8 -> createUint8DataBuffer(cursor, xyPlaneSize, tileRequest.getTileWidth(), minTileX, minTileY, minTileC); + case INT8 -> createInt8DataBuffer(cursor, xyPlaneSize, tileRequest.getTileWidth(), minTileX, minTileY, minTileC); + case UINT16 -> createUint16DataBuffer(cursor, xyPlaneSize, tileRequest.getTileWidth(), minTileX, minTileY, minTileC); + case INT16 -> createInt16DataBuffer(cursor, xyPlaneSize, tileRequest.getTileWidth(), minTileX, minTileY, minTileC); + case UINT32 -> createUint32DataBuffer(cursor, xyPlaneSize, tileRequest.getTileWidth(), minTileX, minTileY, minTileC); + case INT32 -> createInt32DataBuffer(cursor, xyPlaneSize, tileRequest.getTileWidth(), minTileX, minTileY, minTileC); + case FLOAT32 -> createFloat32DataBuffer(cursor, xyPlaneSize, tileRequest.getTileWidth(), minTileX, minTileY, minTileC); + case FLOAT64 -> createFloat64DataBuffer(cursor, xyPlaneSize, tileRequest.getTileWidth(), minTileX, minTileY, minTileC); + }; + + return new BufferedImage( + ColorModelFactory.createColorModel(metadata.getPixelType(), metadata.getChannels()), + WritableRaster.createWritableRaster( + new BandedSampleModel( + dataBuffer.getDataType(), + tileRequest.getTileWidth(), + tileRequest.getTileHeight(), + numberOfChannelsInAccessibles + ), + dataBuffer, + null + ), + false, + null + ); + } + } + + @Override + protected ImageServerBuilder.ServerBuilder createServerBuilder() { + return null; + } + + @Override + protected String createID() { + return String.valueOf(counter.incrementAndGet()); + } + + @Override + public Collection getURIs() { + return List.of(); + } + + @Override + public String getServerType() { + return "ImgLib2"; + } + + @Override + public ImageServerMetadata getOriginalMetadata() { + return metadata; + } + + @Override + protected BufferedImage createDefaultRGBImage(int width, int height) { + return new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + } + + /** + * A builder to create an instance of {@link ImgLib2ImageServer}. + * + * @param the pixel type of the {@link ImgLib2ImageServer} to create + */ + public static class Builder & NumericType> { + + private static final int DEFAULT_TILE_SIZE = 1024; + private final List> accessibles; + private final PixelType pixelType; + private ImageServerMetadata metadata; + + private Builder(List> accessibles) { + checkAccessibles(accessibles); + + RandomAccessibleInterval firstAccessible = accessibles.getFirst(); + T value = firstAccessible.firstElement(); + + this.accessibles = accessibles; + this.pixelType = switch (value) { + case ARGBType ignored -> PixelType.UINT8; + case UnsignedByteType ignored -> PixelType.UINT8; + case ByteType ignored -> PixelType.INT8; + case UnsignedShortType ignored -> PixelType.UINT16; + case ShortType ignored -> PixelType.INT16; + case UnsignedIntType ignored -> PixelType.UINT32; + case IntType ignored -> PixelType.INT32; + case FloatType ignored -> PixelType.FLOAT32; + case DoubleType ignored -> PixelType.FLOAT64; + default -> throw new IllegalArgumentException(String.format("Unexpected accessible type %s", value)); + }; + this.metadata = new ImageServerMetadata.Builder() + .width(1) // the width will be ignored, but it must be > 0 to avoid an exception when calling build() + .height(1) // the height will be ignored, but it must be > 0 to avoid an exception when calling build() + .channels(value instanceof ARGBType ? + ImageChannel.getDefaultRGBChannels() : + ImageChannel.getDefaultChannelList((int) firstAccessible.dimension(ImgCreator.AXIS_CHANNEL)) + ) + .preferredTileSize(DEFAULT_TILE_SIZE, DEFAULT_TILE_SIZE) + .build(); + } + + /** + * Set the name of the {@link ImgLib2ImageServer} to build. + * + * @param name the name the image should have + * @return this builder + */ + public Builder name(String name) { + this.metadata = new ImageServerMetadata.Builder(metadata).name(name).build(); + return this; + } + + /** + * Set the channels of the {@link ImgLib2ImageServer} to build. + *

+ * If not provided here or with {@link #metadata(ImageServerMetadata)}, the channels of the output image will be + * {@link ImageChannel#getDefaultRGBChannels()} or {@link ImageChannel#getDefaultChannelList(int)} depending on + * whether the accessible type is {@link ARGBType}. + * + * @param channels the channels to set. Must be {@link ImageChannel#getDefaultRGBChannels()} if the type of the + * current accessibles is {@link ARGBType}, or must match the number of channels of the current + * accessibles else + * @return this builder + * @throws NullPointerException if the provided list is null or contain a null element + * @throws IllegalArgumentException if the current accessibles have the {@link ARGBType} and the provided channels + * are not {@link ImageChannel#getDefaultRGBChannels()}, or if the current accessibles don't have the {@link ARGBType} + * and the provided number of channels doesn't match the number of channels of the current accessibles + */ + public Builder channels(Collection channels) { + checkChannels(accessibles, channels); + + this.metadata = new ImageServerMetadata.Builder(metadata).channels(channels).build(); + return this; + } + + /** + * Set the tile size of the {@link ImgLib2ImageServer} to build. + *

+ * If not provided here or with {@link #metadata(ImageServerMetadata)}, the tile width and height is set to 1024. + * + * @param tileWidth the tile width in pixels to set + * @param tileHeight the tile height in pixels to set + * @return this builder + */ + public Builder preferredTileSize(int tileWidth, int tileHeight) { + this.metadata = new ImageServerMetadata.Builder(metadata) + .preferredTileSize(tileWidth, tileHeight) + .build(); + return this; + } + + /** + * Set the pixel calibration of the {@link ImgLib2ImageServer} to build. + * + * @param pixelCalibration the pixel calibration to set + * @return this builder + * @throws NullPointerException if the provided pixel calibration is null + */ + public Builder pixelCalibration(PixelCalibration pixelCalibration) { + this.metadata = new ImageServerMetadata.Builder(metadata) + .pixelSizeMicrons(pixelCalibration.getPixelWidthMicrons(), pixelCalibration.getPixelHeightMicrons()) + .zSpacingMicrons(pixelCalibration.getZSpacingMicrons()) + .timepoints( + pixelCalibration.getTimeUnit(), + IntStream.range(0, pixelCalibration.nTimepoints()).mapToDouble(pixelCalibration::getTimepoint).toArray() + ) + .build(); + return this; + } + + /** + * Set metadata parameters of the {@link ImgLib2ImageServer} to build. + *

+ * If not provided here or with {@link #channels(Collection)}, the channels of the output image will be + * {@link ImageChannel#getDefaultRGBChannels()} or {@link ImageChannel#getDefaultChannelList(int)} depending on + * whether the accessible type is {@link ARGBType}. + *

+ * If not provided here or with {@link #preferredTileSize(int, int)}, the tile width and height is set to 1024. + * + * @param metadata the metadata the image server should have. The width, height, number of z-stacks, number of + * time points, whether the image is RGB, pixel type, and resolution level are not taken from + * this metadata but determined from the provided accessibles. The channels of the provided + * metadata must be {@link ImageChannel#getDefaultRGBChannels()} if the type of the current + * accessibles is {@link ARGBType}, or must match the number of channels of the current accessibles + * else + * @return this builder + * @throws NullPointerException if the provided metadata is null or if the channels of the provided metadata are + * null or contain a null element + * @throws IllegalArgumentException if the current accessibles have the {@link ARGBType} and the channels of the + * provided metadata are not {@link ImageChannel#getDefaultRGBChannels()}, or if the current accessibles don't + * have the {@link ARGBType} and the number of channels of the provided metadata doesn't match the number of + * channels of the current accessibles + */ + public Builder metadata(ImageServerMetadata metadata) { + checkChannels(accessibles, metadata.getChannels()); + + this.metadata = metadata; + return this; + } + + /** + * Create an {@link ImgLib2ImageServer} from this builder. + * + * @return a new {@link ImgLib2ImageServer} whose parameters are determined from this builder + */ + public ImgLib2ImageServer build() { + return new ImgLib2ImageServer<>(accessibles, pixelType, metadata); + } + + private static & NumericType> void checkAccessibles(List> accessibles) { + if (accessibles == null) { + throw new NullPointerException("The provided list of accessibles is null"); + } + if (accessibles.stream().anyMatch(Objects::isNull)) { + throw new NullPointerException(String.format("One of the provided accessibles %s is null", accessibles)); + } + if (accessibles.isEmpty()) { + throw new IllegalArgumentException("The provided list of accessibles is empty"); + } + + for (RandomAccessibleInterval accessible: accessibles) { + for (int dimension=0; dimension accessible: accessibles) { + if (accessible.numDimensions() != ImgCreator.NUMBER_OF_AXES) { + throw new IllegalArgumentException(String.format( + "The provided accessible %s does not have %d dimensions", + accessible, + ImgCreator.NUMBER_OF_AXES + )); + } + } + + Map axes = Map.of( + ImgCreator.AXIS_CHANNEL, "number of channels", + ImgCreator.AXIS_Z, "number of z-stacks", + ImgCreator.AXIS_TIME, "number of timepoints" + ); + for (var axis: axes.entrySet()) { + List numberOfElements = accessibles.stream() + .map(accessible -> accessible.dimension(axis.getKey())) + .distinct() + .toList(); + if (numberOfElements.size() > 1) { + throw new IllegalArgumentException(String.format( + "The provided accessibles %s do not contain the same %s (found %s)", + accessibles, + axis.getValue(), + numberOfElements + )); + } + } + + RandomAccessibleInterval firstAccessible = accessibles.getFirst(); + if (firstAccessible.firstElement() instanceof ARGBType && firstAccessible.dimension(ImgCreator.AXIS_CHANNEL) != 1) { + throw new IllegalArgumentException(String.format( + "The provided accessibles %s have the ARGB type, but not one channel (found %d)", + accessibles, + firstAccessible.dimension(ImgCreator.AXIS_CHANNEL) + )); + } + } + + private static & NumericType> void checkChannels( + List> accessibles, + Collection channels + ) { + for (ImageChannel channel: channels) { + Objects.requireNonNull(channel); + } + + if (accessibles.getFirst().firstElement() instanceof ARGBType) { + if (!channels.equals(ImageChannel.getDefaultRGBChannels())) { + throw new IllegalArgumentException(String.format( + "The current accessibles %s have the ARGB type, but the provided channels %s are not the default RGB channels %s", + accessibles, + channels, + ImageChannel.getDefaultRGBChannels() + )); + } + } else { + if (accessibles.getFirst().dimension(ImgCreator.AXIS_CHANNEL) != channels.size()) { + throw new IllegalArgumentException(String.format( + "There are %d provided channels, but the current accessibles %s contain %s channels", + channels.size(), + accessibles, + accessibles.getFirst().dimension(ImgCreator.AXIS_CHANNEL) + )); + } + } + } + } + + private static List createResolutionLevels(List> accessibles) { + ImageServerMetadata.ImageResolutionLevel.Builder builder = new ImageServerMetadata.ImageResolutionLevel.Builder( + (int) accessibles.getFirst().dimension(ImgCreator.AXIS_X), + (int) accessibles.getFirst().dimension(ImgCreator.AXIS_Y) + ); + + for (RandomAccessibleInterval accessible: accessibles) { + builder.addLevel( + (int) accessible.dimension(ImgCreator.AXIS_X), + (int) accessible.dimension(ImgCreator.AXIS_Y) + ); + } + + return builder.build(); + } + + private RandomAccessibleInterval getImgLib2Tile(TileRequest tileRequest) { + RandomAccessibleInterval wholeLevel = accessibles.get(tileRequest.getLevel()); + + long[] minWholeLevel = new long[ImgCreator.NUMBER_OF_AXES]; + wholeLevel.min(minWholeLevel); + + long[] min = new long[ImgCreator.NUMBER_OF_AXES]; + min[ImgCreator.AXIS_X] = minWholeLevel[ImgCreator.AXIS_X] + tileRequest.getTileX(); + min[ImgCreator.AXIS_Y] = minWholeLevel[ImgCreator.AXIS_Y] + tileRequest.getTileY(); + min[ImgCreator.AXIS_CHANNEL] = minWholeLevel[ImgCreator.AXIS_CHANNEL]; + min[ImgCreator.AXIS_Z] = minWholeLevel[ImgCreator.AXIS_Z] + tileRequest.getZ(); + min[ImgCreator.AXIS_TIME] = minWholeLevel[ImgCreator.AXIS_TIME] + tileRequest.getT(); + + long[] max = new long[ImgCreator.NUMBER_OF_AXES]; // max is inclusive, hence the -1 + max[ImgCreator.AXIS_X] = min[ImgCreator.AXIS_X] + tileRequest.getTileWidth() - 1; + max[ImgCreator.AXIS_Y] = min[ImgCreator.AXIS_Y] + tileRequest.getTileHeight() - 1; + max[ImgCreator.AXIS_CHANNEL] = min[ImgCreator.AXIS_CHANNEL] + numberOfChannelsInAccessibles - 1; + max[ImgCreator.AXIS_Z] = min[ImgCreator.AXIS_Z]; + max[ImgCreator.AXIS_TIME] = min[ImgCreator.AXIS_TIME]; + + return Views.interval(wholeLevel, min, max); + } + + private BufferedImage createArgbImage(TileRequest tileRequest, Cursor cursor, int minTileX, int minTileY) { + BufferedImage image = new BufferedImage(tileRequest.getTileWidth(), tileRequest.getTileHeight(), BufferedImage.TYPE_INT_ARGB); + DataBufferInt buffer = (DataBufferInt) image.getRaster().getDataBuffer(); + + while (cursor.hasNext()) { + ARGBType value = (ARGBType) cursor.next(); + + int xy = cursor.getIntPosition(ImgCreator.AXIS_X) - minTileX + + (cursor.getIntPosition(ImgCreator.AXIS_Y) - minTileY) * tileRequest.getTileWidth(); + + buffer.setElem(xy, value.get()); + } + + return image; + } + + private DataBuffer createUint8DataBuffer(Cursor cursor, int xyPlaneSize, int tileWidth, int minTileX, int minTileY, int minTileC) { + byte[][] pixels = new byte[numberOfChannelsInAccessibles][xyPlaneSize]; + + while (cursor.hasNext()) { + UnsignedByteType value = (UnsignedByteType) cursor.next(); + + int c = cursor.getIntPosition(ImgCreator.AXIS_CHANNEL) - minTileC; + int xy = cursor.getIntPosition(ImgCreator.AXIS_X) - minTileX + + (cursor.getIntPosition(ImgCreator.AXIS_Y) - minTileY) * tileWidth; + + pixels[c][xy] = value.getByte(); + } + + return new DataBufferByte(pixels, xyPlaneSize); + } + + private DataBuffer createInt8DataBuffer(Cursor cursor, int xyPlaneSize, int tileWidth, int minTileX, int minTileY, int minTileC) { + byte[][] pixels = new byte[numberOfChannelsInAccessibles][xyPlaneSize]; + + while (cursor.hasNext()) { + ByteType value = (ByteType) cursor.next(); + + int c = cursor.getIntPosition(ImgCreator.AXIS_CHANNEL) - minTileC; + int xy = cursor.getIntPosition(ImgCreator.AXIS_X) - minTileX + + (cursor.getIntPosition(ImgCreator.AXIS_Y) - minTileY) * tileWidth; + + pixels[c][xy] = value.getByte(); + } + + return new DataBufferByte(pixels, xyPlaneSize); + } + + private DataBuffer createUint16DataBuffer(Cursor cursor, int xyPlaneSize, int tileWidth, int minTileX, int minTileY, int minTileC) { + short[][] pixels = new short[numberOfChannelsInAccessibles][xyPlaneSize]; + + while (cursor.hasNext()) { + UnsignedShortType value = (UnsignedShortType) cursor.next(); + + int c = cursor.getIntPosition(ImgCreator.AXIS_CHANNEL) - minTileC; + int xy = cursor.getIntPosition(ImgCreator.AXIS_X) - minTileX + + (cursor.getIntPosition(ImgCreator.AXIS_Y) - minTileY) * tileWidth; + + pixels[c][xy] = value.getShort(); + } + + return new DataBufferUShort(pixels, xyPlaneSize); + } + + private DataBuffer createInt16DataBuffer(Cursor cursor, int xyPlaneSize, int tileWidth, int minTileX, int minTileY, int minTileC) { + short[][] pixels = new short[numberOfChannelsInAccessibles][xyPlaneSize]; + + while (cursor.hasNext()) { + ShortType value = (ShortType) cursor.next(); + + int c = cursor.getIntPosition(ImgCreator.AXIS_CHANNEL) - minTileC; + int xy = cursor.getIntPosition(ImgCreator.AXIS_X) - minTileX + + (cursor.getIntPosition(ImgCreator.AXIS_Y) - minTileY) * tileWidth; + + pixels[c][xy] = value.getShort(); + } + + return new DataBufferShort(pixels, xyPlaneSize); + } + + private DataBuffer createUint32DataBuffer(Cursor cursor, int xyPlaneSize, int tileWidth, int minTileX, int minTileY, int minTileC) { + int[][] pixels = new int[numberOfChannelsInAccessibles][xyPlaneSize]; + + while (cursor.hasNext()) { + UnsignedIntType value = (UnsignedIntType) cursor.next(); + + int c = cursor.getIntPosition(ImgCreator.AXIS_CHANNEL) - minTileC; + int xy = cursor.getIntPosition(ImgCreator.AXIS_X) - minTileX + + (cursor.getIntPosition(ImgCreator.AXIS_Y) - minTileY) * tileWidth; + + pixels[c][xy] = value.getInt(); + } + + return new DataBufferInt(pixels, xyPlaneSize); + } + + private DataBuffer createInt32DataBuffer(Cursor cursor, int xyPlaneSize, int tileWidth, int minTileX, int minTileY, int minTileC) { + int[][] pixels = new int[numberOfChannelsInAccessibles][xyPlaneSize]; + + while (cursor.hasNext()) { + IntType value = (IntType) cursor.next(); + + int c = cursor.getIntPosition(ImgCreator.AXIS_CHANNEL) - minTileC; + int xy = cursor.getIntPosition(ImgCreator.AXIS_X) - minTileX + + (cursor.getIntPosition(ImgCreator.AXIS_Y) - minTileY) * tileWidth; + + pixels[c][xy] = value.getInt(); + } + + return new DataBufferInt(pixels, xyPlaneSize); + } + + private DataBuffer createFloat32DataBuffer(Cursor cursor, int xyPlaneSize, int tileWidth, int minTileX, int minTileY, int minTileC) { + float[][] pixels = new float[numberOfChannelsInAccessibles][xyPlaneSize]; + + while (cursor.hasNext()) { + FloatType value = (FloatType) cursor.next(); + + int c = cursor.getIntPosition(ImgCreator.AXIS_CHANNEL) - minTileC; + int xy = cursor.getIntPosition(ImgCreator.AXIS_X) - minTileX + + (cursor.getIntPosition(ImgCreator.AXIS_Y) - minTileY) * tileWidth; + + pixels[c][xy] = value.get(); + } + + return new DataBufferFloat(pixels, xyPlaneSize); + } + + private DataBuffer createFloat64DataBuffer(Cursor cursor, int xyPlaneSize, int tileWidth, int minTileX, int minTileY, int minTileC) { + double[][] pixels = new double[numberOfChannelsInAccessibles][xyPlaneSize]; + + while (cursor.hasNext()) { + DoubleType value = (DoubleType) cursor.next(); + + int c = cursor.getIntPosition(ImgCreator.AXIS_CHANNEL) - minTileC; + int xy = cursor.getIntPosition(ImgCreator.AXIS_X) - minTileX + + (cursor.getIntPosition(ImgCreator.AXIS_Y) - minTileY) * tileWidth; + + pixels[c][xy] = value.get(); + } + + return new DataBufferDouble(pixels, xyPlaneSize); + } +} diff --git a/src/main/java/qupath/ext/imglib2/bufferedimageaccesses/ArgbBufferedImageAccess.java b/src/main/java/qupath/ext/imglib2/bufferedimageaccesses/ArgbBufferedImageAccess.java index 5d30e95..ab2f980 100644 --- a/src/main/java/qupath/ext/imglib2/bufferedimageaccesses/ArgbBufferedImageAccess.java +++ b/src/main/java/qupath/ext/imglib2/bufferedimageaccesses/ArgbBufferedImageAccess.java @@ -2,6 +2,7 @@ import net.imglib2.img.basictypeaccess.IntAccess; import qupath.ext.imglib2.SizableDataAccess; +import qupath.lib.common.ColorTools; import java.awt.image.BufferedImage; import java.awt.image.DataBuffer; @@ -11,6 +12,9 @@ /** * An {@link IntAccess} whose elements are computed from an (A)RGB {@link BufferedImage}. *

+ * If the alpha component is not provided (e.g. if the {@link BufferedImage} has the {@link BufferedImage#TYPE_INT_RGB} type), + * then the alpha component of each pixel is considered to be 255. + *

* This {@link IntAccess} is immutable; any attempt to changes its values will result in a * {@link UnsupportedOperationException}. */ @@ -21,6 +25,7 @@ public class ArgbBufferedImageAccess implements IntAccess, SizableDataAccess { private final int width; private final int planeSize; private final boolean canUseDataBuffer; + private final boolean alphaProvided; private final int size; /** @@ -38,6 +43,7 @@ public ArgbBufferedImageAccess(BufferedImage image) { this.canUseDataBuffer = image.getRaster().getDataBuffer() instanceof DataBufferInt && image.getRaster().getSampleModel() instanceof SinglePixelPackedSampleModel; + this.alphaProvided = image.getType() == BufferedImage.TYPE_INT_ARGB; this.size = AccessTools.getSizeOfDataBufferInBytes(this.dataBuffer); } @@ -48,7 +54,18 @@ public int getValue(int index) { int xyIndex = index % planeSize; if (canUseDataBuffer) { - return dataBuffer.getElem(b, xyIndex); + int pixel = dataBuffer.getElem(b, xyIndex); + + if (alphaProvided) { + return pixel; + } else { + return ColorTools.packARGB( + 255, + ColorTools.red(pixel), + ColorTools.green(pixel), + ColorTools.blue(pixel) + ); + } } else { return image.getRGB(xyIndex % width, xyIndex / width); } diff --git a/src/test/java/qupath/ext/imglib2/TestImgLib2ImageServer.java b/src/test/java/qupath/ext/imglib2/TestImgLib2ImageServer.java new file mode 100644 index 0000000..6b11f40 --- /dev/null +++ b/src/test/java/qupath/ext/imglib2/TestImgLib2ImageServer.java @@ -0,0 +1,1475 @@ +package qupath.ext.imglib2; + +import net.imglib2.RandomAccessibleInterval; +import net.imglib2.img.array.ArrayImgFactory; +import net.imglib2.img.basictypeaccess.array.ByteArray; +import net.imglib2.img.cell.Cell; +import net.imglib2.img.cell.CellGrid; +import net.imglib2.img.cell.LazyCellImg; +import net.imglib2.type.NativeType; +import net.imglib2.type.logic.BitType; +import net.imglib2.type.numeric.ARGBType; +import net.imglib2.type.numeric.NumericType; +import net.imglib2.type.numeric.integer.ByteType; +import net.imglib2.type.numeric.integer.IntType; +import net.imglib2.type.numeric.integer.ShortType; +import net.imglib2.type.numeric.integer.UnsignedByteType; +import net.imglib2.type.numeric.integer.UnsignedIntType; +import net.imglib2.type.numeric.integer.UnsignedShortType; +import net.imglib2.type.numeric.real.DoubleType; +import net.imglib2.type.numeric.real.FloatType; +import net.imglib2.view.Views; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import qupath.lib.images.servers.ImageChannel; +import qupath.lib.images.servers.ImageServer; +import qupath.lib.images.servers.ImageServerMetadata; +import qupath.lib.images.servers.PixelCalibration; +import qupath.lib.images.servers.PixelType; +import qupath.lib.objects.classes.PathClass; +import qupath.lib.regions.RegionRequest; + +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferByte; +import java.awt.image.DataBufferDouble; +import java.awt.image.DataBufferFloat; +import java.awt.image.DataBufferInt; +import java.awt.image.DataBufferShort; +import java.awt.image.DataBufferUShort; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.stream.IntStream; + +public class TestImgLib2ImageServer { + + @Test + void Check_Null_Accessible() { + List> accessibles = null; + + Assertions.assertThrows( + NullPointerException.class, + () -> ImgLib2ImageServer.builder(accessibles) + ); + } + + @Test + void Check_List_Contain_Null_Accessible() { + List> accessibles = new ArrayList<>(); + accessibles.add(new ArrayImgFactory<>(new ByteType()).create(1, 1, 1, 1, 1)); + accessibles.add(null); + accessibles.add(new ArrayImgFactory<>(new ByteType()).create(1, 1, 1, 1, 1)); + + Assertions.assertThrows( + NullPointerException.class, + () -> ImgLib2ImageServer.builder(accessibles) + ); + } + + @Test + void Check_Empty_List() { + List> accessibles = List.of(); + + Assertions.assertThrows( + IllegalArgumentException.class, + () -> ImgLib2ImageServer.builder(accessibles) + ); + } + + @Test + void Check_Invalid_Type() { + List> accessibles = List.of(new ArrayImgFactory<>(new BitType(false)).create(1, 1, 1, 1, 1)); + + Assertions.assertThrows( + IllegalArgumentException.class, + () -> ImgLib2ImageServer.builder(accessibles) + ); + } + + @Test + void Check_Dimension_Too_High() { + List> accessibles = List.of(new LazyCellImg<>( + new CellGrid(new long[] {Integer.MAX_VALUE + 1L, 1, 1, 1, 1}, new int[] {1, 1, 1, 1, 1}), + new ByteType(), + cellIndex -> new Cell<>( + new int[]{ 1, 1, 1, 1, 1 }, + new long[]{ 0, 0, 0, 0, 0}, + new ByteArray(5) + ) + )); // LazyCellImg instead of ArrayImgFactory because an ArrayImgFactory of this size cannot be created + + Assertions.assertThrows( + IllegalArgumentException.class, + () -> ImgLib2ImageServer.builder(accessibles) + ); + } + + @Test + void Check_Invalid_Number_Of_Axes() { + List> accessibles = List.of(new ArrayImgFactory<>(new ByteType()).create(1, 1, 1)); + + Assertions.assertThrows( + IllegalArgumentException.class, + () -> ImgLib2ImageServer.builder(accessibles) + ); + } + + @Test + void Check_Different_Number_Of_Channels_Between_Accessibles() { + List> accessibles = List.of( + new ArrayImgFactory<>(new ByteType()).create(1, 1, 1, 1, 1), + new ArrayImgFactory<>(new ByteType()).create(1, 1, 2, 1, 1) + ); + + Assertions.assertThrows( + IllegalArgumentException.class, + () -> ImgLib2ImageServer.builder(accessibles) + ); + } + + @Test + void Check_Different_Number_Of_Z_Stacks_Between_Accessibles() { + List> accessibles = List.of( + new ArrayImgFactory<>(new ByteType()).create(1, 1, 1, 1, 1), + new ArrayImgFactory<>(new ByteType()).create(1, 1, 1, 2, 1) + ); + + Assertions.assertThrows( + IllegalArgumentException.class, + () -> ImgLib2ImageServer.builder(accessibles) + ); + } + + @Test + void Check_Different_Number_Of_Timepoints_Between_Accessibles() { + List> accessibles = List.of( + new ArrayImgFactory<>(new ByteType()).create(1, 1, 1, 1, 1), + new ArrayImgFactory<>(new ByteType()).create(1, 1, 1, 1, 2) + ); + + Assertions.assertThrows( + IllegalArgumentException.class, + () -> ImgLib2ImageServer.builder(accessibles) + ); + } + + @Test + void Check_Not_One_Channel_In_Accessibles_When_Argb() { + List> accessibles = List.of(new ArrayImgFactory<>(new ARGBType()).create(1, 1, 2, 1, 1)); + + Assertions.assertThrows( + IllegalArgumentException.class, + () -> ImgLib2ImageServer.builder(accessibles) + ); + } + + @Test + void Check_Metadata_When_Nothing_Provided() throws Exception { + List> accessibles = List.of( + new ArrayImgFactory<>(new FloatType()).create(100, 200, 2, 12, 7), + new ArrayImgFactory<>(new FloatType()).create(25, 50, 2, 12, 7) + ); + ImageServerMetadata expectedMetadata = new ImageServerMetadata.Builder() + .width(100) + .height(200) + .rgb(false) + .pixelType(PixelType.FLOAT32) + .levelsFromDownsamples(1, 4) + .sizeZ(12) + .sizeT(7) + .channels(ImageChannel.getDefaultChannelList(2)) + .preferredTileSize(1024, 1024) + .build(); + ImageServer server = ImgLib2ImageServer.builder(accessibles).build(); + + ImageServerMetadata metadata = server.getMetadata(); + + Assertions.assertEquals(expectedMetadata, metadata); + + server.close(); + } + + @Test + void Check_Metadata_When_Name_Provided() throws Exception { + List> accessibles = List.of( + new ArrayImgFactory<>(new FloatType()).create(100, 200, 2, 12, 7), + new ArrayImgFactory<>(new FloatType()).create(25, 50, 2, 12, 7) + ); + String name = "Some name"; + ImageServerMetadata expectedMetadata = new ImageServerMetadata.Builder() + .width(100) + .height(200) + .rgb(false) + .pixelType(PixelType.FLOAT32) + .levelsFromDownsamples(1, 4) + .sizeZ(12) + .sizeT(7) + .channels(ImageChannel.getDefaultChannelList(2)) + .preferredTileSize(1024, 1024) + .name(name) + .build(); + ImageServer server = ImgLib2ImageServer.builder(accessibles).name(name).build(); + + ImageServerMetadata metadata = server.getMetadata(); + + Assertions.assertEquals(expectedMetadata, metadata); + + server.close(); + } + + @Test + void Check_Null_Channels() { + List> accessibles = List.of(new ArrayImgFactory<>(new ByteType()).create(1, 1, 1, 1, 1)); + List channels = null; + + Assertions.assertThrows( + NullPointerException.class, + () -> ImgLib2ImageServer.builder(accessibles).channels(channels) + ); + } + + @Test + void Check_Non_Rgb_Channels_When_Argb() { + List> accessibles = List.of(new ArrayImgFactory<>(new ARGBType()).create(1, 1, 1, 1, 1)); + List channels = List.of(ImageChannel.getInstance("Channel", 0)); + + Assertions.assertThrows( + IllegalArgumentException.class, + () -> ImgLib2ImageServer.builder(accessibles).channels(channels) + ); + } + + @Test + void Check_Different_Number_Of_Channels_With_Accessibles() { + List> accessibles = List.of(new ArrayImgFactory<>(new ByteType()).create(1, 1, 2, 1, 1)); + List channels = List.of(ImageChannel.getInstance("Channel", 0)); + + Assertions.assertThrows( + IllegalArgumentException.class, + () -> ImgLib2ImageServer.builder(accessibles).channels(channels) + ); + } + + @Test + void Check_Metadata_When_Channels_Provided() throws Exception { + List> accessibles = List.of( + new ArrayImgFactory<>(new FloatType()).create(100, 200, 2, 12, 7), + new ArrayImgFactory<>(new FloatType()).create(25, 50, 2, 12, 7) + ); + List channels = List.of( + ImageChannel.getInstance("Some channel 1", 1), + ImageChannel.getInstance("Some channel 2", 2) + ); + ImageServerMetadata expectedMetadata = new ImageServerMetadata.Builder() + .width(100) + .height(200) + .rgb(false) + .pixelType(PixelType.FLOAT32) + .levelsFromDownsamples(1, 4) + .sizeZ(12) + .sizeT(7) + .channels(channels) + .preferredTileSize(1024, 1024) + .build(); + ImageServer server = ImgLib2ImageServer.builder(accessibles).channels(channels).build(); + + ImageServerMetadata metadata = server.getMetadata(); + + Assertions.assertEquals(expectedMetadata, metadata); + + server.close(); + } + + @Test + void Check_Metadata_When_Tile_Size_Provided() throws Exception { + List> accessibles = List.of( + new ArrayImgFactory<>(new FloatType()).create(100, 200, 2, 12, 7), + new ArrayImgFactory<>(new FloatType()).create(25, 50, 2, 12, 7) + ); + int tileWidth = 22; + int tileHeight = 54; + ImageServerMetadata expectedMetadata = new ImageServerMetadata.Builder() + .width(100) + .height(200) + .rgb(false) + .pixelType(PixelType.FLOAT32) + .levelsFromDownsamples(1, 4) + .sizeZ(12) + .sizeT(7) + .channels(ImageChannel.getDefaultChannelList(2)) + .preferredTileSize(tileWidth, tileHeight) + .build(); + ImageServer server = ImgLib2ImageServer.builder(accessibles) + .preferredTileSize(tileWidth, tileHeight) + .build(); + + ImageServerMetadata metadata = server.getMetadata(); + + Assertions.assertEquals(expectedMetadata, metadata); + + server.close(); + } + + @Test + void Check_Null_Pixel_Calibration() { + List> accessibles = List.of(new ArrayImgFactory<>(new ByteType()).create(1, 1, 1, 1, 1)); + PixelCalibration pixelCalibration = null; + + Assertions.assertThrows( + NullPointerException.class, + () -> ImgLib2ImageServer.builder(accessibles).pixelCalibration(pixelCalibration) + ); + } + + @Test + void Check_Metadata_When_Pixel_Calibration_Provided() throws Exception { + List> accessibles = List.of( + new ArrayImgFactory<>(new FloatType()).create(100, 200, 2, 12, 7), + new ArrayImgFactory<>(new FloatType()).create(25, 50, 2, 12, 7) + ); + PixelCalibration pixelCalibration = new PixelCalibration.Builder() + .pixelSizeMicrons(4.4, 4) + .zSpacingMicrons(.5) + .timepoints(TimeUnit.DAYS, 1, 5.6) + .build(); + ImageServerMetadata expectedMetadata = new ImageServerMetadata.Builder() + .width(100) + .height(200) + .rgb(false) + .pixelType(PixelType.FLOAT32) + .levelsFromDownsamples(1, 4) + .sizeZ(12) + .sizeT(7) + .channels(ImageChannel.getDefaultChannelList(2)) + .preferredTileSize(1024, 1024) + .pixelSizeMicrons(4.4, 4) + .zSpacingMicrons(.5) + .timepoints(TimeUnit.DAYS, 1, 5.6) + .build(); + ImageServer server = ImgLib2ImageServer.builder(accessibles).pixelCalibration(pixelCalibration).build(); + + ImageServerMetadata metadata = server.getMetadata(); + + Assertions.assertEquals(expectedMetadata, metadata); + + server.close(); + } + + @Test + void Check_Null_Metadata() { + List> accessibles = List.of(new ArrayImgFactory<>(new ByteType()).create(1, 1, 1, 1, 1)); + ImageServerMetadata metadata = null; + + Assertions.assertThrows( + NullPointerException.class, + () -> ImgLib2ImageServer.builder(accessibles).metadata(metadata) + ); + } + + @Test + void Check_Null_Channels_In_Metadata() { + List> accessibles = List.of(new ArrayImgFactory<>(new ByteType()).create(1, 1, 1, 1, 1)); + List channels = new ArrayList<>(); + channels.add(ImageChannel.getInstance("Channel", 0)); + channels.add(null); + ImageServerMetadata metadata = new ImageServerMetadata.Builder().width(1).height(1).channels(channels).build(); + + Assertions.assertThrows( + NullPointerException.class, + () -> ImgLib2ImageServer.builder(accessibles).metadata(metadata) + ); + } + + @Test + void Check_Non_Rgb_Channels_In_Metadata_When_Argb() { + List> accessibles = List.of(new ArrayImgFactory<>(new ARGBType()).create(1, 1, 1, 1, 1)); + List channels = List.of(ImageChannel.getInstance("Channel", 0)); + ImageServerMetadata metadata = new ImageServerMetadata.Builder().width(1).height(1).channels(channels).build(); + + Assertions.assertThrows( + IllegalArgumentException.class, + () -> ImgLib2ImageServer.builder(accessibles).metadata(metadata) + ); + } + + @Test + void Check_Different_Number_Of_Channels_In_Metadata_With_Accessibles() { + List> accessibles = List.of(new ArrayImgFactory<>(new ByteType()).create(1, 1, 2, 1, 1)); + List channels = List.of(ImageChannel.getInstance("Channel", 0)); + ImageServerMetadata metadata = new ImageServerMetadata.Builder().width(1).height(1).channels(channels).build(); + + Assertions.assertThrows( + IllegalArgumentException.class, + () -> ImgLib2ImageServer.builder(accessibles).metadata(metadata) + ); + } + + @Test + void Check_Metadata_When_Metadata_Provided() throws Exception { + List> accessibles = List.of( + new ArrayImgFactory<>(new FloatType()).create(100, 200, 2, 12, 7), + new ArrayImgFactory<>(new FloatType()).create(25, 50, 2, 12, 7) + ); + ImageServerMetadata providedMetadata = new ImageServerMetadata.Builder() + .width(400) + .height(5465) + .minValue(-23.23) + .maxValue(10345) + .channelType(ImageServerMetadata.ChannelType.CLASSIFICATION) + .classificationLabels(Map.of( + 1, PathClass.fromString("Class 1"), + 2, PathClass.fromString("Class 2") + )) + .rgb(true) + .pixelType(PixelType.INT8) + .levelsFromDownsamples(1) + .sizeZ(4) + .sizeT(56) + .pixelSizeMicrons(4.4, 4) + .zSpacingMicrons(.5) + .timepoints(TimeUnit.DAYS, 1, 5.6) + .magnification(4.324) + .preferredTileSize(23, 54) + .channels(List.of( + ImageChannel.getInstance("Channel 1", 1), + ImageChannel.getInstance("Channel 2", 2) + )) + .name("Image name") + .build(); + ImageServerMetadata expectedMetadata = new ImageServerMetadata.Builder() // same as metadata, except for values mentioned in ImgLib2ImageServer.Builder.metadata + .width(100) + .height(200) + .minValue(-23.23) + .maxValue(10345) + .channelType(ImageServerMetadata.ChannelType.CLASSIFICATION) + .classificationLabels(Map.of( + 1, PathClass.fromString("Class 1"), + 2, PathClass.fromString("Class 2") + )) + .rgb(false) + .pixelType(PixelType.FLOAT32) + .levelsFromDownsamples(1, 4) + .sizeZ(12) + .sizeT(7) + .pixelSizeMicrons(4.4, 4) + .zSpacingMicrons(.5) + .timepoints(TimeUnit.DAYS, 1, 5.6) + .magnification(4.324) + .preferredTileSize(23, 54) + .channels(List.of( + ImageChannel.getInstance("Channel 1", 1), + ImageChannel.getInstance("Channel 2", 2) + )) + .name("Image name") + .build(); + ImageServer server = ImgLib2ImageServer.builder(accessibles).metadata(providedMetadata).build(); + + ImageServerMetadata metadata = server.getMetadata(); + + Assertions.assertEquals(expectedMetadata, metadata); + + server.close(); + } + + abstract static class GenericImage & NumericType> { + + @Test + void Check_Full_Resolution_Pixels() throws Exception { + List> accessibles = getAccessibles(); + BufferedImage expectedImage = getExpectedFullResolutionImage(); + ImageServer server = ImgLib2ImageServer.builder(accessibles).build(); + + BufferedImage image = server.readRegion(RegionRequest.createInstance(server).updateT(1)); + + Utils.assertBufferedImagesEqual(expectedImage, image, 0.00001); + + server.close(); + } + + @Test + void Check_Lowest_Resolution_Pixels() throws Exception { + List> accessibles = getAccessibles(); + BufferedImage expectedImage = getExpectedLowestResolutionImage(); + ImageServer server = ImgLib2ImageServer.builder(accessibles).build(); + + BufferedImage image = server.readRegion(RegionRequest.createInstance(server, 2)); + + Utils.assertBufferedImagesEqual(expectedImage, image, 0.00001); + + server.close(); + } + + @Test + void Check_Pixels_On_View() throws Exception { + List> accessibles = List.of(Views.interval( + getAccessibles().getFirst(), + new long[] {1, 0, 0, 0, 1}, + new long[] {1, 1, 0, 0, 1} + )); + BufferedImage expectedImage = getExpectedViewOfImage(); + ImageServer server = ImgLib2ImageServer.builder(accessibles).build(); + + BufferedImage image = server.readRegion(RegionRequest.createInstance(server)); + + Utils.assertBufferedImagesEqual(expectedImage, image, 0.00001); + + server.close(); + } + + @Test + void Check_Pixels_On_Big_Image() throws Exception { + List> accessibles = getBigAccessibles(); + BufferedImage expectedImage = getExpectedBigImage(); + ImageServer server = ImgLib2ImageServer.builder(accessibles).build(); + + BufferedImage image = server.readRegion(RegionRequest.createInstance(server)); + + Utils.assertBufferedImagesEqual(expectedImage, image, 0.00001); + + server.close(); + } + + abstract protected List> getAccessibles(); + + abstract protected List> getBigAccessibles(); + + abstract protected BufferedImage getExpectedFullResolutionImage(); + + abstract protected BufferedImage getExpectedLowestResolutionImage(); + + abstract protected BufferedImage getExpectedViewOfImage(); + + abstract protected BufferedImage getExpectedBigImage(); + } + + @Nested + class ArgbImage extends GenericImage { + + @Override + protected List> getAccessibles() { + return List.of( + Utils.createArgbImg( + new long[] {2, 2, 1, 1, 2}, + new int[] { + ARGBType.rgba(43, 65, 33, 0), ARGBType.rgba(45, 5, 133, 255), + ARGBType.rgba(37, 5, 223, 2), ARGBType.rgba(4, 33, 66, 87), + + ARGBType.rgba(43, 65, 33, 0), ARGBType.rgba(45, 5, 133, 255), + ARGBType.rgba(37, 5, 223, 2), ARGBType.rgba(4, 33, 66, 87) + } + ), + Utils.createArgbImg( + new long[] {1, 1, 1, 1, 2}, + new int[] { + ARGBType.rgba(32, 165, 233, 30), + + ARGBType.rgba(3, 16, 255, 3) + } + ) + ); + } + + @Override + protected List> getBigAccessibles() { + int width = 1000; + int height = 1000; + + return List.of(Utils.createArgbImg( + new long[] {width, height, 1, 1, 1}, + IntStream.range(0, width * height) + .map(i -> ARGBType.rgba(120, i % 255, 0, 255)) + .toArray() + )); + } + + @Override + protected BufferedImage getExpectedFullResolutionImage() { + return Utils.createArgbBufferedImage( + 2, + 2, + new int[] { + ARGBType.rgba(43, 65, 33, 0), ARGBType.rgba(45, 5, 133, 255), + ARGBType.rgba(37, 5, 223, 2), ARGBType.rgba(4, 33, 66, 87) + } + ); + } + + @Override + protected BufferedImage getExpectedLowestResolutionImage() { + return Utils.createArgbBufferedImage( + 1, + 1, + new int[] { + ARGBType.rgba(32, 165, 233, 30) + } + ); + } + + @Override + protected BufferedImage getExpectedViewOfImage() { + return Utils.createArgbBufferedImage( + 1, + 2, + new int[] { + ARGBType.rgba(45, 5, 133, 255), + ARGBType.rgba(4, 33, 66, 87) + } + ); + } + + @Override + protected BufferedImage getExpectedBigImage() { + int width = 1000; + int height = 1000; + + return Utils.createArgbBufferedImage( + width, + height, + IntStream.range(0, width * height) + .map(i -> ARGBType.rgba(120, i % 255, 0, 255)) + .toArray() + ); + } + } + + @Nested + class Uint8Image extends GenericImage { + + @Override + protected List> getAccessibles() { + return List.of( + Utils.createImg( + new long[] {2, 2, 1, 1, 2}, + new double[] { + 23, 23, + 4, 3, + + 75, 7, + 0, 1 + }, + new UnsignedByteType() + ), + Utils.createImg( + new long[] {1, 1, 1, 1, 2}, + new double[] { + 45, + + 3 + }, + new UnsignedByteType() + ) + ); + } + + @Override + protected List> getBigAccessibles() { + int width = 1000; + int height = 1000; + + return List.of(Utils.createImg( + new long[] {width, height, 1, 1, 1}, + IntStream.range(0, width * height) + .mapToDouble(i -> i % 255) + .toArray(), + new UnsignedByteType() + )); + } + + @Override + protected BufferedImage getExpectedFullResolutionImage() { + return Utils.createBufferedImage( + new DataBufferByte(new byte[][] { new byte[] { + 75, 7, + 0, 1 + }}, 4), + 2, + 2, + 1, + PixelType.UINT8 + ); + } + + @Override + protected BufferedImage getExpectedLowestResolutionImage() { + return Utils.createBufferedImage( + new DataBufferByte(new byte[][] { new byte[] { + 45 + }}, 4), + 1, + 1, + 1, + PixelType.UINT8 + ); + } + + @Override + protected BufferedImage getExpectedViewOfImage() { + return Utils.createBufferedImage( + new DataBufferByte(new byte[][] { new byte[] { + 7, + 1 + }}, 4), + 1, + 2, + 1, + PixelType.UINT8 + ); + } + + @Override + protected BufferedImage getExpectedBigImage() { + int width = 1000; + int height = 1000; + byte[] pixels = new byte[width * height]; + for (int i=0; i { + + @Override + protected List> getAccessibles() { + return List.of( + Utils.createImg( + new long[] {2, 2, 1, 1, 2}, + new double[] { + 23, -23, + 4, 3, + + 75, 7, + 0, -1 + }, + new ByteType() + ), + Utils.createImg( + new long[] {1, 1, 1, 1, 2}, + new double[] { + 45, + + -3 + }, + new ByteType() + ) + ); + } + + @Override + protected List> getBigAccessibles() { + int width = 1000; + int height = 1000; + + return List.of(Utils.createImg( + new long[] {width, height, 1, 1, 1}, + IntStream.range(0, width * height) + .mapToDouble(i -> i % 255 - 128) + .toArray(), + new ByteType() + )); + } + + @Override + protected BufferedImage getExpectedFullResolutionImage() { + return Utils.createBufferedImage( + new DataBufferByte(new byte[][] { new byte[] { + 75, 7, + 0, -1 + }}, 4), + 2, + 2, + 1, + PixelType.INT8 + ); + } + + @Override + protected BufferedImage getExpectedLowestResolutionImage() { + return Utils.createBufferedImage( + new DataBufferByte(new byte[][] { new byte[] { + 45 + }}, 4), + 1, + 1, + 1, + PixelType.INT8 + ); + } + + @Override + protected BufferedImage getExpectedViewOfImage() { + return Utils.createBufferedImage( + new DataBufferByte(new byte[][] { new byte[] { + 7, + -1 + }}, 4), + 1, + 2, + 1, + PixelType.INT8 + ); + } + + @Override + protected BufferedImage getExpectedBigImage() { + int width = 1000; + int height = 1000; + byte[] pixels = new byte[width * height]; + for (int i=0; i { + + @Override + protected List> getAccessibles() { + return List.of( + Utils.createImg( + new long[] {2, 2, 1, 1, 2}, + new double[] { + 23, 23, + 4, 3, + + 75, 7, + 0, 1 + }, + new UnsignedShortType() + ), + Utils.createImg( + new long[] {1, 1, 1, 1, 2}, + new double[] { + 45, + + 3 + }, + new UnsignedShortType() + ) + ); + } + + @Override + protected List> getBigAccessibles() { + int width = 1000; + int height = 1000; + + return List.of(Utils.createImg( + new long[] {width, height, 1, 1, 1}, + IntStream.range(0, width * height) + .mapToDouble(i -> i) + .toArray(), + new UnsignedShortType() + )); + } + + @Override + protected BufferedImage getExpectedFullResolutionImage() { + return Utils.createBufferedImage( + new DataBufferUShort(new short[][] { new short[] { + 75, 7, + 0, 1 + }}, 4), + 2, + 2, + 1, + PixelType.UINT16 + ); + } + + @Override + protected BufferedImage getExpectedLowestResolutionImage() { + return Utils.createBufferedImage( + new DataBufferUShort(new short[][] { new short[] { + 45 + }}, 4), + 1, + 1, + 1, + PixelType.UINT16 + ); + } + + @Override + protected BufferedImage getExpectedViewOfImage() { + return Utils.createBufferedImage( + new DataBufferUShort(new short[][] { new short[] { + 7, + 1 + }}, 4), + 1, + 2, + 1, + PixelType.UINT16 + ); + } + + @Override + protected BufferedImage getExpectedBigImage() { + int width = 1000; + int height = 1000; + short[] pixels = new short[width * height]; + for (int i=0; i { + + @Override + protected List> getAccessibles() { + return List.of( + Utils.createImg( + new long[] {2, 2, 1, 1, 2}, + new double[] { + 23, -23, + 4, 3, + + 75, 7, + 0, -1 + }, + new ShortType() + ), + Utils.createImg( + new long[] {1, 1, 1, 1, 2}, + new double[] { + 45, + + -3 + }, + new ShortType() + ) + ); + } + + @Override + protected List> getBigAccessibles() { + int width = 1000; + int height = 1000; + + return List.of(Utils.createImg( + new long[] {width, height, 1, 1, 1}, + IntStream.range(0, width * height) + .mapToDouble(i -> i) + .toArray(), + new ShortType() + )); + } + + @Override + protected BufferedImage getExpectedFullResolutionImage() { + return Utils.createBufferedImage( + new DataBufferShort(new short[][] { new short[] { + 75, 7, + 0, -1 + }}, 4), + 2, + 2, + 1, + PixelType.INT16 + ); + } + + @Override + protected BufferedImage getExpectedLowestResolutionImage() { + return Utils.createBufferedImage( + new DataBufferShort(new short[][] { new short[] { + 45 + }}, 4), + 1, + 1, + 1, + PixelType.INT16 + ); + } + + @Override + protected BufferedImage getExpectedViewOfImage() { + return Utils.createBufferedImage( + new DataBufferShort(new short[][] { new short[] { + 7, + -1 + }}, 4), + 1, + 2, + 1, + PixelType.INT16 + ); + } + + @Override + protected BufferedImage getExpectedBigImage() { + int width = 1000; + int height = 1000; + short[] pixels = new short[width * height]; + for (int i=0; i { + + @Override + protected List> getAccessibles() { + return List.of( + Utils.createImg( + new long[] {2, 2, 1, 1, 2}, + new double[] { + 23, 23, + 4, 3, + + 75, 7, + 0, 1 + }, + new UnsignedIntType() + ), + Utils.createImg( + new long[] {1, 1, 1, 1, 2}, + new double[] { + 45, + + 3 + }, + new UnsignedIntType() + ) + ); + } + + @Override + protected List> getBigAccessibles() { + int width = 1000; + int height = 1000; + + return List.of(Utils.createImg( + new long[] {width, height, 1, 1, 1}, + IntStream.range(0, width * height) + .mapToDouble(i -> i) + .toArray(), + new UnsignedIntType() + )); + } + + @Override + protected BufferedImage getExpectedFullResolutionImage() { + return Utils.createBufferedImage( + new DataBufferInt(new int[][] { new int[] { + 75, 7, + 0, 1 + }}, 4), + 2, + 2, + 1, + PixelType.UINT32 + ); + } + + @Override + protected BufferedImage getExpectedLowestResolutionImage() { + return Utils.createBufferedImage( + new DataBufferInt(new int[][] { new int[] { + 45 + }}, 4), + 1, + 1, + 1, + PixelType.UINT32 + ); + } + + @Override + protected BufferedImage getExpectedViewOfImage() { + return Utils.createBufferedImage( + new DataBufferInt(new int[][] { new int[] { + 7, + 1 + }}, 4), + 1, + 2, + 1, + PixelType.UINT32 + ); + } + + @Override + protected BufferedImage getExpectedBigImage() { + int width = 1000; + int height = 1000; + + return Utils.createBufferedImage( + new DataBufferInt( + new int[][] { IntStream.range(0, width * height).toArray() }, + width*height + ), + width, + height, + 1, + PixelType.UINT32 + ); + } + } + + @Nested + class Int32Image extends GenericImage { + + @Override + protected List> getAccessibles() { + return List.of( + Utils.createImg( + new long[] {2, 2, 1, 1, 2}, + new double[] { + 23, -23, + 4, 3, + + 75, 7, + 0, -1 + }, + new IntType() + ), + Utils.createImg( + new long[] {1, 1, 1, 1, 2}, + new double[] { + 45, + + -3 + }, + new IntType() + ) + ); + } + + @Override + protected List> getBigAccessibles() { + int width = 1000; + int height = 1000; + + return List.of(Utils.createImg( + new long[] {width, height, 1, 1, 1}, + IntStream.range(0, width * height) + .mapToDouble(i -> i) + .toArray(), + new IntType() + )); + } + + @Override + protected BufferedImage getExpectedFullResolutionImage() { + return Utils.createBufferedImage( + new DataBufferInt(new int[][] { new int[] { + 75, 7, + 0, -1 + }}, 4), + 2, + 2, + 1, + PixelType.INT32 + ); + } + + @Override + protected BufferedImage getExpectedLowestResolutionImage() { + return Utils.createBufferedImage( + new DataBufferInt(new int[][] { new int[] { + 45 + }}, 4), + 1, + 1, + 1, + PixelType.INT32 + ); + } + + @Override + protected BufferedImage getExpectedViewOfImage() { + return Utils.createBufferedImage( + new DataBufferInt(new int[][] { new int[] { + 7, + -1 + }}, 4), + 1, + 2, + 1, + PixelType.INT32 + ); + } + + @Override + protected BufferedImage getExpectedBigImage() { + int width = 1000; + int height = 1000; + + return Utils.createBufferedImage( + new DataBufferInt( + new int[][] { IntStream.range(0, width * height).toArray() }, + width*height + ), + width, + height, + 1, + PixelType.INT32 + ); + } + } + + @Nested + class FloatImage extends GenericImage { + + @Override + protected List> getAccessibles() { + return List.of( + Utils.createImg( + new long[] {2, 2, 1, 1, 2}, + new double[] { + 23, -23.4, + .4, 3, + + 75, 7.6, + 0, -1 + }, + new FloatType() + ), + Utils.createImg( + new long[] {1, 1, 1, 1, 2}, + new double[] { + 45.4, + + -1.3 + }, + new FloatType() + ) + ); + } + + @Override + protected List> getBigAccessibles() { + int width = 1000; + int height = 1000; + + return List.of(Utils.createImg( + new long[] {width, height, 1, 1, 1}, + IntStream.range(0, width * height) + .mapToDouble(i -> i) + .toArray(), + new FloatType() + )); + } + + @Override + protected BufferedImage getExpectedFullResolutionImage() { + return Utils.createBufferedImage( + new DataBufferFloat(new float[][] { new float[] { + 75, 7.6f, + 0, -1 + }}, 4), + 2, + 2, + 1, + PixelType.FLOAT32 + ); + } + + @Override + protected BufferedImage getExpectedLowestResolutionImage() { + return Utils.createBufferedImage( + new DataBufferFloat(new float[][] { new float[] { + 45.4f + }}, 4), + 1, + 1, + 1, + PixelType.FLOAT32 + ); + } + + @Override + protected BufferedImage getExpectedViewOfImage() { + return Utils.createBufferedImage( + new DataBufferFloat(new float[][] { new float[] { + 7.6f, + -1 + }}, 4), + 1, + 2, + 1, + PixelType.FLOAT32 + ); + } + + @Override + protected BufferedImage getExpectedBigImage() { + int width = 1000; + int height = 1000; + float[] pixels = new float[width * height]; + for (int i=0; i { + + @Override + protected List> getAccessibles() { + return List.of( + Utils.createImg( + new long[] {2, 2, 1, 1, 2}, + new double[] { + 23, -23.4, + .4, 3, + + 75, 7.6, + 0, -1 + }, + new DoubleType() + ), + Utils.createImg( + new long[] {1, 1, 1, 1, 2}, + new double[] { + 45.4, + + -1.3 + }, + new DoubleType() + ) + ); + } + + @Override + protected List> getBigAccessibles() { + int width = 1000; + int height = 1000; + + return List.of(Utils.createImg( + new long[] {width, height, 1, 1, 1}, + IntStream.range(0, width * height) + .mapToDouble(i -> i) + .toArray(), + new DoubleType() + )); + } + + @Override + protected BufferedImage getExpectedFullResolutionImage() { + return Utils.createBufferedImage( + new DataBufferDouble(new double[][] { new double[] { + 75, 7.6, + 0, -1 + }}, 4), + 2, + 2, + 1, + PixelType.FLOAT64 + ); + } + + @Override + protected BufferedImage getExpectedLowestResolutionImage() { + return Utils.createBufferedImage( + new DataBufferDouble(new double[][] { new double[] { + 45.4 + }}, 4), + 1, + 1, + 1, + PixelType.FLOAT64 + ); + } + + @Override + protected BufferedImage getExpectedViewOfImage() { + return Utils.createBufferedImage( + new DataBufferDouble(new double[][] { new double[] { + 7.6, + -1 + }}, 4), + 1, + 2, + 1, + PixelType.FLOAT64 + ); + } + + @Override + protected BufferedImage getExpectedBigImage() { + int width = 1000; + int height = 1000; + + return Utils.createBufferedImage( + new DataBufferDouble( + new double[][] { IntStream.range(0, width * height) + .mapToDouble(i -> i) + .toArray() + }, + width*height + ), + width, + height, + 1, + PixelType.FLOAT64 + ); + } + } +} diff --git a/src/test/java/qupath/ext/imglib2/Utils.java b/src/test/java/qupath/ext/imglib2/Utils.java index 8b91157..c7511eb 100644 --- a/src/test/java/qupath/ext/imglib2/Utils.java +++ b/src/test/java/qupath/ext/imglib2/Utils.java @@ -2,6 +2,9 @@ import net.imglib2.Cursor; import net.imglib2.RandomAccessibleInterval; +import net.imglib2.img.Img; +import net.imglib2.img.array.ArrayImgFactory; +import net.imglib2.type.NativeType; import net.imglib2.type.numeric.ARGBType; import net.imglib2.type.numeric.RealType; import org.junit.jupiter.api.Assertions; @@ -43,6 +46,58 @@ public static BufferedImage createBufferedImage(DataBuffer dataBuffer, int width ); } + public static BufferedImage createArgbBufferedImage(int width, int height, int[] argb) { + BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + image.setRGB(0, 0, width, height, argb, 0, width); + return image; + } + + public static & RealType> Img createImg(long[] dimensions, double[] pixels, T type) { + Img img = new ArrayImgFactory<>(type).create(dimensions); + + Cursor cursor = img.localizingCursor(); + int[] position = new int[dimensions.length]; + while (cursor.hasNext()) { + T value = cursor.next(); + + cursor.localize(position); + int index = Math.toIntExact( + position[ImgCreator.AXIS_X] + + position[ImgCreator.AXIS_Y] * dimensions[ImgCreator.AXIS_X] + + position[ImgCreator.AXIS_CHANNEL] * dimensions[ImgCreator.AXIS_X] * dimensions[ImgCreator.AXIS_Y] + + position[ImgCreator.AXIS_Z] * dimensions[ImgCreator.AXIS_X] * dimensions[ImgCreator.AXIS_Y] * dimensions[ImgCreator.AXIS_CHANNEL] + + position[ImgCreator.AXIS_TIME] * dimensions[ImgCreator.AXIS_X] * dimensions[ImgCreator.AXIS_Y] * dimensions[ImgCreator.AXIS_CHANNEL] * dimensions[ImgCreator.AXIS_Z] + ); + + value.setReal(pixels[index]); + } + + return img; + } + + public static Img createArgbImg(long[] dimensions, int[] pixels) { + Img img = new ArrayImgFactory<>(new ARGBType()).create(dimensions); + + Cursor cursor = img.localizingCursor(); + int[] position = new int[dimensions.length]; + while (cursor.hasNext()) { + ARGBType value = cursor.next(); + + cursor.localize(position); + int index = Math.toIntExact( + position[ImgCreator.AXIS_X] + + position[ImgCreator.AXIS_Y] * dimensions[ImgCreator.AXIS_X] + + position[ImgCreator.AXIS_CHANNEL] * dimensions[ImgCreator.AXIS_X] * dimensions[ImgCreator.AXIS_Y] + + position[ImgCreator.AXIS_Z] * dimensions[ImgCreator.AXIS_X] * dimensions[ImgCreator.AXIS_Y] * dimensions[ImgCreator.AXIS_CHANNEL] + + position[ImgCreator.AXIS_TIME] * dimensions[ImgCreator.AXIS_X] * dimensions[ImgCreator.AXIS_Y] * dimensions[ImgCreator.AXIS_CHANNEL] * dimensions[ImgCreator.AXIS_Z] + ); + + value.set(pixels[index]); + } + + return img; + } + public static > void assertRandomAccessibleEquals(RandomAccessibleInterval accessible, PixelGetter pixelGetter, double downsample) { int[] position = new int[accessible.numDimensions()]; Cursor cursor = accessible.localizingCursor(); @@ -105,4 +160,52 @@ public static > void assertRandomAccessibleEquals(RandomAc ); } } + + public static void assertBufferedImagesEqual(BufferedImage expectedImage, BufferedImage actualImage, double delta) { + Assertions.assertEquals(expectedImage.getWidth(), actualImage.getWidth()); + Assertions.assertEquals(expectedImage.getHeight(), actualImage.getHeight()); + + if (expectedImage.getType() == BufferedImage.TYPE_INT_ARGB && actualImage.getType() == BufferedImage.TYPE_INT_ARGB) { + int[] expectedRgb = expectedImage.getRGB( + 0, + 0, + expectedImage.getWidth(), + expectedImage.getHeight(), + null, + 0, + expectedImage.getWidth() + ); + int[] actualRgb = actualImage.getRGB( + 0, + 0, + actualImage.getWidth(), + actualImage.getHeight(), + null, + 0, + actualImage.getWidth() + ); + + for (int i=0; i