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 extends RandomAccessibleInterval> accessibles;
+ private final ImageServerMetadata metadata;
+ private final int numberOfChannelsInAccessibles;
+
+ private ImgLib2ImageServer(List extends RandomAccessibleInterval> 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 extends RandomAccessibleInterval> 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 extends RandomAccessibleInterval> accessibles;
+ private final PixelType pixelType;
+ private ImageServerMetadata metadata;
+
+ private Builder(List extends RandomAccessibleInterval> 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 extends RandomAccessibleInterval> 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 extends RandomAccessibleInterval> 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 extends RandomAccessibleInterval>> 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