From 6e812fbe63097e1ec6ff74cf54e4285118d9d084 Mon Sep 17 00:00:00 2001 From: lleplat Date: Fri, 28 Nov 2025 11:35:19 +0000 Subject: [PATCH 01/12] Add Realtype builders --- .../java/qupath/ext/imglib2/ImgBuilder.java | 321 ++++++++++++++++-- .../accesses/ArgbBufferedImageAccess.java | 3 +- .../accesses/ByteBufferedImageAccess.java | 92 +++++ 3 files changed, 394 insertions(+), 22 deletions(-) create mode 100644 src/main/java/qupath/ext/imglib2/accesses/ByteBufferedImageAccess.java diff --git a/src/main/java/qupath/ext/imglib2/ImgBuilder.java b/src/main/java/qupath/ext/imglib2/ImgBuilder.java index 17325cc..ece62a5 100644 --- a/src/main/java/qupath/ext/imglib2/ImgBuilder.java +++ b/src/main/java/qupath/ext/imglib2/ImgBuilder.java @@ -8,6 +8,7 @@ import net.imglib2.type.NativeType; import net.imglib2.type.numeric.ARGBType; import net.imglib2.type.numeric.NumericType; +import net.imglib2.type.numeric.RealType; import net.imglib2.type.numeric.integer.ByteType; import net.imglib2.type.numeric.integer.IntType; import net.imglib2.type.numeric.integer.ShortType; @@ -17,6 +18,7 @@ import net.imglib2.type.numeric.real.DoubleType; import net.imglib2.type.numeric.real.FloatType; import qupath.ext.imglib2.accesses.ArgbBufferedImageAccess; +import qupath.ext.imglib2.accesses.ByteBufferedImageAccess; import qupath.ext.imglib2.accesses.ByteRasterAccess; import qupath.ext.imglib2.accesses.DoubleRasterAccess; import qupath.ext.imglib2.accesses.FloatRasterAccess; @@ -79,13 +81,13 @@ public class ImgBuilder & NumericType, A extends Siza private final T type; private CellCache cellCache = DEFAULT_CELL_CACHE; - private ImgBuilder(ImageServer server, T type, Function cellCreator) { + private ImgBuilder(ImageServer server, T type, Function cellCreator, int numberOfChannels) { if (server.nChannels() <= 0) { throw new IllegalArgumentException(String.format("The provided image has less than one channel (%d)", server.nChannels())); } this.server = server; - this.numberOfChannels = server.isRGB() ? 1 : server.nChannels(); + this.numberOfChannels = numberOfChannels; this.cellCreator = cellCreator; this.type = type; } @@ -95,6 +97,7 @@ private ImgBuilder(ImageServer server, T type, Function * The type of the output image is not checked, which might lead to problems later when accessing pixel values of the * returned accessibles of this class. It is recommended to use {@link #createBuilder(ImageServer, NativeType)} instead. + * See also this function to know which pixel type is used. * * @param server the input image * @return a builder to create an instance of this class @@ -102,48 +105,56 @@ private ImgBuilder(ImageServer server, T type, Function createBuilder(ImageServer server) { if (server.isRGB()) { - return new ImgBuilder<>(server, new ARGBType(), ArgbBufferedImageAccess::new); + return new ImgBuilder<>(server, new ARGBType(), ArgbBufferedImageAccess::new, 1); } else { return switch (server.getPixelType()) { case UINT8 -> new ImgBuilder<>( server, new UnsignedByteType(), - image -> new ByteRasterAccess(image.getRaster()) + image -> new ByteRasterAccess(image.getRaster()), + server.nChannels() ); case INT8 -> new ImgBuilder<>( server, new ByteType(), - image -> new ByteRasterAccess(image.getRaster()) + image -> new ByteRasterAccess(image.getRaster()), + server.nChannels() ); case UINT16 -> new ImgBuilder<>( server, new UnsignedShortType(), - image -> new ShortRasterAccess(image.getRaster()) + image -> new ShortRasterAccess(image.getRaster()), + server.nChannels() ); case INT16 -> new ImgBuilder<>( server, new ShortType(), - image -> new ShortRasterAccess(image.getRaster()) + image -> new ShortRasterAccess(image.getRaster()), + server.nChannels() ); case UINT32 -> new ImgBuilder<>( server, new UnsignedIntType(), - image -> new IntRasterAccess(image.getRaster()) + image -> new IntRasterAccess(image.getRaster()), + server.nChannels() ); case INT32 -> new ImgBuilder<>( server, new IntType(), - image -> new IntRasterAccess(image.getRaster()) + image -> new IntRasterAccess(image.getRaster()), + server.nChannels() ); case FLOAT32 -> new ImgBuilder<>( server, new FloatType(), - image -> new FloatRasterAccess(image.getRaster()) + image -> new FloatRasterAccess(image.getRaster()), + server.nChannels() ); case FLOAT64 -> new ImgBuilder<>( server, new DoubleType(), - image -> new DoubleRasterAccess(image.getRaster()) + image -> new DoubleRasterAccess(image.getRaster()), + server.nChannels() ); }; } @@ -154,7 +165,10 @@ private ImgBuilder(ImageServer server, T type, Function * The provided type must be compatible with the input image: *
    - *
  • If the input image is {@link ImageServer#isRGB() RGB}, the type must be {@link ARGBType}.
  • + *
  • + * If the input image is {@link ImageServer#isRGB() RGB}, the type must be {@link ARGBType}. Images created + * by the returned builder will have one channel. + *
  • *
  • * Else: *
      @@ -205,14 +219,203 @@ private ImgBuilder(ImageServer server, T type, Function(server, type, ArgbBufferedImageAccess::new); + return new ImgBuilder<>(server, type, ArgbBufferedImageAccess::new, 1); + } else { + return switch (server.getPixelType()) { + case UINT8, INT8 -> new ImgBuilder<>( + server, + type, + image -> new ByteRasterAccess(image.getRaster()), + server.nChannels() + ); + case UINT16, INT16 -> new ImgBuilder<>( + server, + type, + image -> new ShortRasterAccess(image.getRaster()), + server.nChannels() + ); + case UINT32, INT32 -> new ImgBuilder<>( + server, + type, + image -> new IntRasterAccess(image.getRaster()), + server.nChannels() + ); + case FLOAT32 -> new ImgBuilder<>( + server, + type, + image -> new FloatRasterAccess(image.getRaster()), + server.nChannels() + ); + case FLOAT64 -> new ImgBuilder<>( + server, + type, + image -> new DoubleRasterAccess(image.getRaster()), + server.nChannels() + ); + }; + } + } + + /** + * Create a builder from an {@link ImageServer}. This doesn't create any accessibles yet. + *

      + * The type of the output image is not checked, which might lead to problems later when accessing pixel values of the + * returned accessibles of this class. It is recommended to use {@link #createRealBuilder(ImageServer, NativeType)} + * instead. See also this function to know which pixel type is used. + * + * @param server the input image + * @return a builder to create an instance of this class + * @throws IllegalArgumentException if the provided image has less than one channel + */ + public static ImgBuilder createRealBuilder(ImageServer server) { + if (server.isRGB()) { + return new ImgBuilder<>(server, new ARGBType(), ByteBufferedImageAccess::new, 4); + } else { + return switch (server.getPixelType()) { + case UINT8 -> new ImgBuilder<>( + server, + new UnsignedByteType(), + image -> new ByteRasterAccess(image.getRaster()), + server.nChannels() + ); + case INT8 -> new ImgBuilder<>( + server, + new ByteType(), + image -> new ByteRasterAccess(image.getRaster()), + server.nChannels() + ); + case UINT16 -> new ImgBuilder<>( + server, + new UnsignedShortType(), + image -> new ShortRasterAccess(image.getRaster()), + server.nChannels() + ); + case INT16 -> new ImgBuilder<>( + server, + new ShortType(), + image -> new ShortRasterAccess(image.getRaster()), + server.nChannels() + ); + case UINT32 -> new ImgBuilder<>( + server, + new UnsignedIntType(), + image -> new IntRasterAccess(image.getRaster()), + server.nChannels() + ); + case INT32 -> new ImgBuilder<>( + server, + new IntType(), + image -> new IntRasterAccess(image.getRaster()), + server.nChannels() + ); + case FLOAT32 -> new ImgBuilder<>( + server, + new FloatType(), + image -> new FloatRasterAccess(image.getRaster()), + server.nChannels() + ); + case FLOAT64 -> new ImgBuilder<>( + server, + new DoubleType(), + image -> new DoubleRasterAccess(image.getRaster()), + server.nChannels() + ); + }; + } + } + + /** + * Create a builder from an {@link ImageServer}. This doesn't create any accessibles yet. + *

      + * The provided type must be compatible with the input image: + *

        + *
      • + * If the input image is {@link ImageServer#isRGB() RGB}, the type must be {@link UnsignedByteType}. Images + * created by the returned builder will have 4 channels (RGBA). + *
      • + *
      • + * Else: + *
          + *
        • + * If the input image has the {@link PixelType#UINT8} {@link ImageServer#getPixelType() pixel type}, + * the type must be {@link UnsignedByteType}. + *
        • + *
        • + * If the input image has the {@link PixelType#INT8} {@link ImageServer#getPixelType() pixel type}, + * the type must be {@link ByteType}. + *
        • + *
        • + * If the input image has the {@link PixelType#UINT16} {@link ImageServer#getPixelType() pixel type}, + * the type must be {@link UnsignedShortType}. + *
        • + *
        • + * If the input image has the {@link PixelType#INT16} {@link ImageServer#getPixelType() pixel type}, + * the type must be {@link ShortType}. + *
        • + *
        • + * If the input image has the {@link PixelType#UINT32} {@link ImageServer#getPixelType() pixel type}, + * the type must be {@link UnsignedIntType}. + *
        • + *
        • + * If the input image has the {@link PixelType#INT32} {@link ImageServer#getPixelType() pixel type}, + * the type must be {@link IntType}. + *
        • + *
        • + * If the input image has the {@link PixelType#FLOAT32} {@link ImageServer#getPixelType() pixel type}, + * the type must be {@link FloatType}. + *
        • + *
        • + * If the input image has the {@link PixelType#FLOAT64} {@link ImageServer#getPixelType() pixel type}, + * the type must be {@link DoubleType}. + *
        • + *
        + *
      • + *
      + * + * @param server the input image + * @param type the expected type of the output image + * @return a builder to create an instance of this class + * @param the type corresponding to the provided image + * @throws IllegalArgumentException if the provided type is not compatible with the input image (see above), or if + * the provided image has less than one channel + */ + public static & RealType> ImgBuilder createRealBuilder(ImageServer server, T type) { + checkRealType(server, type); + + if (server.isRGB()) { + return new ImgBuilder<>(server, type, ByteBufferedImageAccess::new, 4); } else { return switch (server.getPixelType()) { - case UINT8, INT8 -> new ImgBuilder<>(server, type, image -> new ByteRasterAccess(image.getRaster())); - case UINT16, INT16 -> new ImgBuilder<>(server, type, image -> new ShortRasterAccess(image.getRaster())); - case UINT32, INT32 -> new ImgBuilder<>(server, type, image -> new IntRasterAccess(image.getRaster())); - case FLOAT32 -> new ImgBuilder<>(server, type, image -> new FloatRasterAccess(image.getRaster())); - case FLOAT64 -> new ImgBuilder<>(server, type, image -> new DoubleRasterAccess(image.getRaster())); + case UINT8, INT8 -> new ImgBuilder<>( + server, + type, + image -> new ByteRasterAccess(image.getRaster()), + server.nChannels() + ); + case UINT16, INT16 -> new ImgBuilder<>( + server, + type, + image -> new ShortRasterAccess(image.getRaster()), + server.nChannels() + ); + case UINT32, INT32 -> new ImgBuilder<>( + server, + type, + image -> new IntRasterAccess(image.getRaster()), + server.nChannels() + ); + case FLOAT32 -> new ImgBuilder<>( + server, + type, + image -> new FloatRasterAccess(image.getRaster()), + server.nChannels() + ); + case FLOAT64 -> new ImgBuilder<>( + server, + type, + image -> new DoubleRasterAccess(image.getRaster()), + server.nChannels() + ); }; } } @@ -376,7 +579,85 @@ private static void checkType(ImageServer server, T type) { case UINT8 -> { if (!(type instanceof UnsignedByteType)) { throw new IllegalArgumentException(String.format( - "The provided type %s is not a ByteType, which is the one expected for non-RGB UINT8 images", + "The provided type %s is not a UnsignedByteType, which is the one expected for non-RGB UINT8 images", + type.getClass() + )); + } + } + case INT8 -> { + if (!(type instanceof ByteType)) { + throw new IllegalArgumentException(String.format( + "The provided type %s is not a ByteType, which is the one expected for non-RGB INT8 images", + type.getClass() + )); + } + } + case UINT16 -> { + if (!(type instanceof UnsignedShortType)) { + throw new IllegalArgumentException(String.format( + "The provided type %s is not a UnsignedShortType, which is the one expected for non-RGB UINT16 images", + type.getClass() + )); + } + } + case INT16 -> { + if (!(type instanceof ShortType)) { + throw new IllegalArgumentException(String.format( + "The provided type %s is not a ShortType, which is the one expected for non-RGB INT16 images", + type.getClass() + )); + } + } + case UINT32 -> { + if (!(type instanceof UnsignedIntType)) { + throw new IllegalArgumentException(String.format( + "The provided type %s is not a UnsignedIntType, which is the one expected for non-RGB UINT32 images", + type.getClass() + )); + } + } + case INT32 -> { + if (!(type instanceof IntType)) { + throw new IllegalArgumentException(String.format( + "The provided type %s is not a IntType, which is the one expected for non-RGB INT32 images", + type.getClass() + )); + } + } + case FLOAT32 -> { + if (!(type instanceof FloatType)) { + throw new IllegalArgumentException(String.format( + "The provided type %s is not a FloatType, which is the one expected for non-RGB FLOAT32 images", + type.getClass() + )); + } + } + case FLOAT64 -> { + if (!(type instanceof DoubleType)) { + throw new IllegalArgumentException(String.format( + "The provided type %s is not a DoubleType, which is the one expected for non-RGB FLOAT64 images", + type.getClass() + )); + } + } + } + } + } + + private static void checkRealType(ImageServer server, T type) { + if (server.isRGB()) { + if (!(type instanceof UnsignedByteType)) { + throw new IllegalArgumentException(String.format( + "The provided type %s is not an UnsignedByteType, which is the one expected for RGB images", + type.getClass() + )); + } + } else { + switch (server.getPixelType()) { + case UINT8 -> { + if (!(type instanceof UnsignedByteType)) { + throw new IllegalArgumentException(String.format( + "The provided type %s is not a UnsignedByteType, which is the one expected for non-RGB UINT8 images", type.getClass() )); } @@ -384,7 +665,7 @@ private static void checkType(ImageServer server, T type) { case INT8 -> { if (!(type instanceof ByteType)) { throw new IllegalArgumentException(String.format( - "The provided type %s is not a UnsignedByteType, which is the one expected for non-RGB INT8 images", + "The provided type %s is not a ByteType, which is the one expected for non-RGB INT8 images", type.getClass() )); } diff --git a/src/main/java/qupath/ext/imglib2/accesses/ArgbBufferedImageAccess.java b/src/main/java/qupath/ext/imglib2/accesses/ArgbBufferedImageAccess.java index 7150526..6c359ea 100644 --- a/src/main/java/qupath/ext/imglib2/accesses/ArgbBufferedImageAccess.java +++ b/src/main/java/qupath/ext/imglib2/accesses/ArgbBufferedImageAccess.java @@ -53,11 +53,10 @@ public ArgbBufferedImageAccess(BufferedImage image) { @Override public int getValue(int index) { - int b = index / planeSize; int xyIndex = index % planeSize; if (canUseDataBuffer) { - int pixel = dataBuffer.getElem(b, xyIndex); + int pixel = dataBuffer.getElem(0, xyIndex); if (alphaProvided) { return pixel; diff --git a/src/main/java/qupath/ext/imglib2/accesses/ByteBufferedImageAccess.java b/src/main/java/qupath/ext/imglib2/accesses/ByteBufferedImageAccess.java new file mode 100644 index 0000000..c702b97 --- /dev/null +++ b/src/main/java/qupath/ext/imglib2/accesses/ByteBufferedImageAccess.java @@ -0,0 +1,92 @@ +package qupath.ext.imglib2.accesses; + +import net.imglib2.img.basictypeaccess.ByteAccess; +import net.imglib2.img.basictypeaccess.volatiles.VolatileAccess; +import qupath.ext.imglib2.SizableDataAccess; +import qupath.lib.common.ColorTools; + +import java.awt.image.BufferedImage; +import java.awt.image.DataBuffer; +import java.awt.image.DataBufferInt; +import java.awt.image.SinglePixelPackedSampleModel; + +/** + * An {@link ByteAccess} 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 ByteAccess} is immutable; any attempt to changes its values will result in a + * {@link UnsupportedOperationException}. + *

      + * This data access is marked as volatile but always contain valid data. + */ +public class ByteBufferedImageAccess implements ByteAccess, SizableDataAccess, VolatileAccess { + + private final BufferedImage image; + private final DataBuffer dataBuffer; + private final int width; + private final int planeSize; + private final boolean canUseDataBuffer; + private final boolean alphaProvided; + private final int size; + + /** + * Create the byte buffered image access. + * + * @param image the image containing the values to return. It is expected to be (A)RGB + * @throws NullPointerException if the provided image is null + */ + public ByteBufferedImageAccess(BufferedImage image) { + this.image = image; + this.dataBuffer = this.image.getRaster().getDataBuffer(); + + this.width = this.image.getWidth(); + this.planeSize = width * this.image.getHeight(); + + 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); + } + + @Override + public byte getValue(int index) { + int channel = index / planeSize; + int xyIndex = index % planeSize; + + int pixel = canUseDataBuffer ? + dataBuffer.getElem(0, xyIndex) : + image.getRGB(xyIndex % width, xyIndex / width); + + return switch (channel) { + case 0 -> (byte) ColorTools.red(pixel); + case 1 -> (byte) ColorTools.green(pixel); + case 2 -> (byte) ColorTools.blue(pixel); + case 3 -> { + if (alphaProvided) { + yield (byte) ColorTools.alpha(pixel); + } else { + yield (byte) 255; + } + } + default -> throw new IllegalArgumentException(String.format("The provided index %d is out of bounds", index)); + }; + } + + @Override + public void setValue(int index, byte value) { + throw new UnsupportedOperationException("This access is not mutable"); + } + + @Override + public int getSizeBytes() { + return size; + } + + @Override + public boolean isValid() { + return true; + } +} From 277ccc1c523049826e47f3e52dffb0dac1b1122e Mon Sep 17 00:00:00 2001 From: lleplat Date: Fri, 28 Nov 2025 14:32:15 +0000 Subject: [PATCH 02/12] Remove alpha component --- src/main/java/qupath/ext/imglib2/ImgBuilder.java | 8 ++++---- .../imglib2/accesses/ByteBufferedImageAccess.java | 14 ++------------ 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/src/main/java/qupath/ext/imglib2/ImgBuilder.java b/src/main/java/qupath/ext/imglib2/ImgBuilder.java index ece62a5..8885d9d 100644 --- a/src/main/java/qupath/ext/imglib2/ImgBuilder.java +++ b/src/main/java/qupath/ext/imglib2/ImgBuilder.java @@ -103,7 +103,7 @@ private ImgBuilder(ImageServer server, T type, Function createBuilder(ImageServer server) { + public static ImgBuilder, ?> createBuilder(ImageServer server) { if (server.isRGB()) { return new ImgBuilder<>(server, new ARGBType(), ArgbBufferedImageAccess::new, 1); } else { @@ -267,9 +267,9 @@ private ImgBuilder(ImageServer server, T type, Function createRealBuilder(ImageServer server) { + public static ImgBuilder, ?> createRealBuilder(ImageServer server) { if (server.isRGB()) { - return new ImgBuilder<>(server, new ARGBType(), ByteBufferedImageAccess::new, 4); + return new ImgBuilder<>(server, new UnsignedByteType(), ByteBufferedImageAccess::new, 3); } else { return switch (server.getPixelType()) { case UINT8 -> new ImgBuilder<>( @@ -383,7 +383,7 @@ private ImgBuilder(ImageServer server, T type, Function(server, type, ByteBufferedImageAccess::new, 4); + return new ImgBuilder<>(server, type, ByteBufferedImageAccess::new, 3); } else { return switch (server.getPixelType()) { case UINT8, INT8 -> new ImgBuilder<>( diff --git a/src/main/java/qupath/ext/imglib2/accesses/ByteBufferedImageAccess.java b/src/main/java/qupath/ext/imglib2/accesses/ByteBufferedImageAccess.java index c702b97..67d33d7 100644 --- a/src/main/java/qupath/ext/imglib2/accesses/ByteBufferedImageAccess.java +++ b/src/main/java/qupath/ext/imglib2/accesses/ByteBufferedImageAccess.java @@ -11,10 +11,9 @@ import java.awt.image.SinglePixelPackedSampleModel; /** - * An {@link ByteAccess} whose elements are computed from an (A)RGB {@link BufferedImage}. + * An {@link ByteAccess} whose elements are computed from an 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. + * The alpha component is not taken into account. *

      * This {@link ByteAccess} is immutable; any attempt to changes its values will result in a * {@link UnsupportedOperationException}. @@ -28,7 +27,6 @@ public class ByteBufferedImageAccess implements ByteAccess, SizableDataAccess, V private final int width; private final int planeSize; private final boolean canUseDataBuffer; - private final boolean alphaProvided; private final int size; /** @@ -46,7 +44,6 @@ public ByteBufferedImageAccess(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); } @@ -64,13 +61,6 @@ public byte getValue(int index) { case 0 -> (byte) ColorTools.red(pixel); case 1 -> (byte) ColorTools.green(pixel); case 2 -> (byte) ColorTools.blue(pixel); - case 3 -> { - if (alphaProvided) { - yield (byte) ColorTools.alpha(pixel); - } else { - yield (byte) 255; - } - } default -> throw new IllegalArgumentException(String.format("The provided index %d is out of bounds", index)); }; } From f81de53ea09fb40149f8cea219f4b5ebd257decc Mon Sep 17 00:00:00 2001 From: lleplat Date: Fri, 28 Nov 2025 14:58:24 +0000 Subject: [PATCH 03/12] Remove duplicate code --- .../java/qupath/ext/imglib2/ImgBuilder.java | 262 ++++-------------- 1 file changed, 58 insertions(+), 204 deletions(-) diff --git a/src/main/java/qupath/ext/imglib2/ImgBuilder.java b/src/main/java/qupath/ext/imglib2/ImgBuilder.java index 8885d9d..472925d 100644 --- a/src/main/java/qupath/ext/imglib2/ImgBuilder.java +++ b/src/main/java/qupath/ext/imglib2/ImgBuilder.java @@ -107,56 +107,7 @@ private ImgBuilder(ImageServer server, T type, Function(server, new ARGBType(), ArgbBufferedImageAccess::new, 1); } else { - return switch (server.getPixelType()) { - case UINT8 -> new ImgBuilder<>( - server, - new UnsignedByteType(), - image -> new ByteRasterAccess(image.getRaster()), - server.nChannels() - ); - case INT8 -> new ImgBuilder<>( - server, - new ByteType(), - image -> new ByteRasterAccess(image.getRaster()), - server.nChannels() - ); - case UINT16 -> new ImgBuilder<>( - server, - new UnsignedShortType(), - image -> new ShortRasterAccess(image.getRaster()), - server.nChannels() - ); - case INT16 -> new ImgBuilder<>( - server, - new ShortType(), - image -> new ShortRasterAccess(image.getRaster()), - server.nChannels() - ); - case UINT32 -> new ImgBuilder<>( - server, - new UnsignedIntType(), - image -> new IntRasterAccess(image.getRaster()), - server.nChannels() - ); - case INT32 -> new ImgBuilder<>( - server, - new IntType(), - image -> new IntRasterAccess(image.getRaster()), - server.nChannels() - ); - case FLOAT32 -> new ImgBuilder<>( - server, - new FloatType(), - image -> new FloatRasterAccess(image.getRaster()), - server.nChannels() - ); - case FLOAT64 -> new ImgBuilder<>( - server, - new DoubleType(), - image -> new DoubleRasterAccess(image.getRaster()), - server.nChannels() - ); - }; + return createRealBuilderFromNonRgbServer(server); } } @@ -271,56 +222,7 @@ private ImgBuilder(ImageServer server, T type, Function(server, new UnsignedByteType(), ByteBufferedImageAccess::new, 3); } else { - return switch (server.getPixelType()) { - case UINT8 -> new ImgBuilder<>( - server, - new UnsignedByteType(), - image -> new ByteRasterAccess(image.getRaster()), - server.nChannels() - ); - case INT8 -> new ImgBuilder<>( - server, - new ByteType(), - image -> new ByteRasterAccess(image.getRaster()), - server.nChannels() - ); - case UINT16 -> new ImgBuilder<>( - server, - new UnsignedShortType(), - image -> new ShortRasterAccess(image.getRaster()), - server.nChannels() - ); - case INT16 -> new ImgBuilder<>( - server, - new ShortType(), - image -> new ShortRasterAccess(image.getRaster()), - server.nChannels() - ); - case UINT32 -> new ImgBuilder<>( - server, - new UnsignedIntType(), - image -> new IntRasterAccess(image.getRaster()), - server.nChannels() - ); - case INT32 -> new ImgBuilder<>( - server, - new IntType(), - image -> new IntRasterAccess(image.getRaster()), - server.nChannels() - ); - case FLOAT32 -> new ImgBuilder<>( - server, - new FloatType(), - image -> new FloatRasterAccess(image.getRaster()), - server.nChannels() - ); - case FLOAT64 -> new ImgBuilder<>( - server, - new DoubleType(), - image -> new DoubleRasterAccess(image.getRaster()), - server.nChannels() - ); - }; + return createRealBuilderFromNonRgbServer(server); } } @@ -331,45 +233,9 @@ private ImgBuilder(ImageServer server, T type, Function *

    • * If the input image is {@link ImageServer#isRGB() RGB}, the type must be {@link UnsignedByteType}. Images - * created by the returned builder will have 4 channels (RGBA). - *
    • - *
    • - * Else: - *
        - *
      • - * If the input image has the {@link PixelType#UINT8} {@link ImageServer#getPixelType() pixel type}, - * the type must be {@link UnsignedByteType}. - *
      • - *
      • - * If the input image has the {@link PixelType#INT8} {@link ImageServer#getPixelType() pixel type}, - * the type must be {@link ByteType}. - *
      • - *
      • - * If the input image has the {@link PixelType#UINT16} {@link ImageServer#getPixelType() pixel type}, - * the type must be {@link UnsignedShortType}. - *
      • - *
      • - * If the input image has the {@link PixelType#INT16} {@link ImageServer#getPixelType() pixel type}, - * the type must be {@link ShortType}. - *
      • - *
      • - * If the input image has the {@link PixelType#UINT32} {@link ImageServer#getPixelType() pixel type}, - * the type must be {@link UnsignedIntType}. - *
      • - *
      • - * If the input image has the {@link PixelType#INT32} {@link ImageServer#getPixelType() pixel type}, - * the type must be {@link IntType}. - *
      • - *
      • - * If the input image has the {@link PixelType#FLOAT32} {@link ImageServer#getPixelType() pixel type}, - * the type must be {@link FloatType}. - *
      • - *
      • - * If the input image has the {@link PixelType#FLOAT64} {@link ImageServer#getPixelType() pixel type}, - * the type must be {@link DoubleType}. - *
      • - *
      + * created by the returned builder will have 3 channels (RGB). *
    • + *
    • Else, see {@link #createBuilder(ImageServer, NativeType)}.
    • *
    * * @param server the input image @@ -566,6 +432,59 @@ public RandomAccessibleInterval buildForDownsample(double downsample) { } } + private static ImgBuilder, ?> createRealBuilderFromNonRgbServer(ImageServer server) { + return switch (server.getPixelType()) { + case UINT8 -> new ImgBuilder<>( + server, + new UnsignedByteType(), + image -> new ByteRasterAccess(image.getRaster()), + server.nChannels() + ); + case INT8 -> new ImgBuilder<>( + server, + new ByteType(), + image -> new ByteRasterAccess(image.getRaster()), + server.nChannels() + ); + case UINT16 -> new ImgBuilder<>( + server, + new UnsignedShortType(), + image -> new ShortRasterAccess(image.getRaster()), + server.nChannels() + ); + case INT16 -> new ImgBuilder<>( + server, + new ShortType(), + image -> new ShortRasterAccess(image.getRaster()), + server.nChannels() + ); + case UINT32 -> new ImgBuilder<>( + server, + new UnsignedIntType(), + image -> new IntRasterAccess(image.getRaster()), + server.nChannels() + ); + case INT32 -> new ImgBuilder<>( + server, + new IntType(), + image -> new IntRasterAccess(image.getRaster()), + server.nChannels() + ); + case FLOAT32 -> new ImgBuilder<>( + server, + new FloatType(), + image -> new FloatRasterAccess(image.getRaster()), + server.nChannels() + ); + case FLOAT64 -> new ImgBuilder<>( + server, + new DoubleType(), + image -> new DoubleRasterAccess(image.getRaster()), + server.nChannels() + ); + }; + } + private static void checkType(ImageServer server, T type) { if (server.isRGB()) { if (!(type instanceof ARGBType)) { @@ -653,72 +572,7 @@ private static void checkRealType(ImageServer server, T type) { )); } } else { - switch (server.getPixelType()) { - case UINT8 -> { - if (!(type instanceof UnsignedByteType)) { - throw new IllegalArgumentException(String.format( - "The provided type %s is not a UnsignedByteType, which is the one expected for non-RGB UINT8 images", - type.getClass() - )); - } - } - case INT8 -> { - if (!(type instanceof ByteType)) { - throw new IllegalArgumentException(String.format( - "The provided type %s is not a ByteType, which is the one expected for non-RGB INT8 images", - type.getClass() - )); - } - } - case UINT16 -> { - if (!(type instanceof UnsignedShortType)) { - throw new IllegalArgumentException(String.format( - "The provided type %s is not a UnsignedShortType, which is the one expected for non-RGB UINT16 images", - type.getClass() - )); - } - } - case INT16 -> { - if (!(type instanceof ShortType)) { - throw new IllegalArgumentException(String.format( - "The provided type %s is not a ShortType, which is the one expected for non-RGB INT16 images", - type.getClass() - )); - } - } - case UINT32 -> { - if (!(type instanceof UnsignedIntType)) { - throw new IllegalArgumentException(String.format( - "The provided type %s is not a UnsignedIntType, which is the one expected for non-RGB UINT32 images", - type.getClass() - )); - } - } - case INT32 -> { - if (!(type instanceof IntType)) { - throw new IllegalArgumentException(String.format( - "The provided type %s is not a IntType, which is the one expected for non-RGB INT32 images", - type.getClass() - )); - } - } - case FLOAT32 -> { - if (!(type instanceof FloatType)) { - throw new IllegalArgumentException(String.format( - "The provided type %s is not a FloatType, which is the one expected for non-RGB FLOAT32 images", - type.getClass() - )); - } - } - case FLOAT64 -> { - if (!(type instanceof DoubleType)) { - throw new IllegalArgumentException(String.format( - "The provided type %s is not a DoubleType, which is the one expected for non-RGB FLOAT64 images", - type.getClass() - )); - } - } - } + checkType(server, type); } } From 65c1343b5ae26969b486cd394d2fa7d96a1e1f23 Mon Sep 17 00:00:00 2001 From: lleplat Date: Fri, 28 Nov 2025 15:02:44 +0000 Subject: [PATCH 04/12] Change order of types --- src/main/java/qupath/ext/imglib2/ImgBuilder.java | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/main/java/qupath/ext/imglib2/ImgBuilder.java b/src/main/java/qupath/ext/imglib2/ImgBuilder.java index 472925d..a71ef18 100644 --- a/src/main/java/qupath/ext/imglib2/ImgBuilder.java +++ b/src/main/java/qupath/ext/imglib2/ImgBuilder.java @@ -41,14 +41,16 @@ /** * A class to create {@link Img} or {@link RandomAccessibleInterval} from an {@link ImageServer}. *

    - * Use a {@link #createBuilder(ImageServer)} or {@link #createBuilder(ImageServer, NativeType)} to create an instance of this class. + * Use {@link #createBuilder(ImageServer)}, {@link #createBuilder(ImageServer, NumericType)}, + * {@link #createRealBuilder(ImageServer)} or {@link #createRealBuilder(ImageServer, RealType)} to create an instance + * of this class. *

    * This class is thread-safe. * * @param the type of the returned accessibles * @param the type contained in the input image */ -public class ImgBuilder & NumericType, A extends SizableDataAccess> { +public class ImgBuilder & NativeType, A extends SizableDataAccess> { /** * The index of the X axis of accessibles returned by functions of this class @@ -96,7 +98,7 @@ private ImgBuilder(ImageServer server, T type, Function * The type of the output image is not checked, which might lead to problems later when accessing pixel values of the - * returned accessibles of this class. It is recommended to use {@link #createBuilder(ImageServer, NativeType)} instead. + * returned accessibles of this class. It is recommended to use {@link #createBuilder(ImageServer, NumericType)} instead. * See also this function to know which pixel type is used. * * @param server the input image @@ -166,7 +168,7 @@ private ImgBuilder(ImageServer server, T type, Function & NumericType> ImgBuilder createBuilder(ImageServer server, T type) { + public static & NativeType> ImgBuilder createBuilder(ImageServer server, T type) { checkType(server, type); if (server.isRGB()) { @@ -211,7 +213,7 @@ private ImgBuilder(ImageServer server, T type, Function * The type of the output image is not checked, which might lead to problems later when accessing pixel values of the - * returned accessibles of this class. It is recommended to use {@link #createRealBuilder(ImageServer, NativeType)} + * returned accessibles of this class. It is recommended to use {@link #createRealBuilder(ImageServer, RealType)} * instead. See also this function to know which pixel type is used. * * @param server the input image @@ -235,7 +237,7 @@ private ImgBuilder(ImageServer server, T type, Function - *

  • Else, see {@link #createBuilder(ImageServer, NativeType)}.
  • + *
  • Else, see {@link #createBuilder(ImageServer, NumericType)}.
  • *
* * @param server the input image @@ -245,7 +247,7 @@ private ImgBuilder(ImageServer server, T type, Function & RealType> ImgBuilder createRealBuilder(ImageServer server, T type) { + public static & NativeType> ImgBuilder createRealBuilder(ImageServer server, T type) { checkRealType(server, type); if (server.isRGB()) { From 48bb2492559fab4d80f2e8bc2f965f5842e3efad Mon Sep 17 00:00:00 2001 From: Pete Date: Mon, 1 Dec 2025 15:33:40 +0000 Subject: [PATCH 05/12] Attempting to simplify things --- .../qupath/ext/imglib2/AccessibleScaler.java | 9 +-- .../java/qupath/ext/imglib2/ImgBuilder.java | 78 +++++++++++++++---- 2 files changed, 65 insertions(+), 22 deletions(-) diff --git a/src/main/java/qupath/ext/imglib2/AccessibleScaler.java b/src/main/java/qupath/ext/imglib2/AccessibleScaler.java index d33280a..db56c40 100644 --- a/src/main/java/qupath/ext/imglib2/AccessibleScaler.java +++ b/src/main/java/qupath/ext/imglib2/AccessibleScaler.java @@ -9,7 +9,6 @@ import net.imglib2.realtransform.RealViews; import net.imglib2.realtransform.Scale2D; import net.imglib2.realtransform.Translation2D; -import net.imglib2.type.NativeType; import net.imglib2.type.numeric.NumericType; import net.imglib2.view.Views; @@ -40,7 +39,7 @@ private AccessibleScaler() { * @throws IllegalArgumentException if the input interval has at least one minimum different from 0, if the provided scale is less * than or equal to 0, or if the input interval has less than two dimensions */ - public static & NumericType> RandomAccessibleInterval scaleWithLinearInterpolation( + public static > RandomAccessibleInterval scaleWithLinearInterpolation( RandomAccessibleInterval input, double scale ) { @@ -59,7 +58,7 @@ public static & NumericType> RandomAccessibleInterva * @throws IllegalArgumentException if the input interval has at least one minimum different from 0, if the provided scale is less * than or equal to 0, or if the input interval has less than two dimensions */ - public static & NumericType> RandomAccessibleInterval scaleWithNearestNeighborInterpolation( + public static RandomAccessibleInterval scaleWithNearestNeighborInterpolation( RandomAccessibleInterval input, double scale ) { @@ -79,7 +78,7 @@ public static & NumericType> RandomAccessibleInterva * @throws IllegalArgumentException if the input interval has at least one minimum different from 0, if the provided scale is less * than or equal to 0, or if the input interval has less than two dimensions */ - public static & NumericType> RandomAccessibleInterval scale( + public static RandomAccessibleInterval scale( RandomAccessibleInterval input, double scale, InterpolatorFactory> interpolatorFactory @@ -104,7 +103,7 @@ public static & NumericType> RandomAccessibleInterva } } - private static & NumericType> RandomAccessibleInterval scaleWithoutChecks( + private static RandomAccessibleInterval scaleWithoutChecks( RandomAccessibleInterval input, double scale, Scale2D scale2D, diff --git a/src/main/java/qupath/ext/imglib2/ImgBuilder.java b/src/main/java/qupath/ext/imglib2/ImgBuilder.java index a71ef18..25445c1 100644 --- a/src/main/java/qupath/ext/imglib2/ImgBuilder.java +++ b/src/main/java/qupath/ext/imglib2/ImgBuilder.java @@ -17,6 +17,8 @@ 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 net.imglib2.view.composite.RealComposite; import qupath.ext.imglib2.accesses.ArgbBufferedImageAccess; import qupath.ext.imglib2.accesses.ByteBufferedImageAccess; import qupath.ext.imglib2.accesses.ByteRasterAccess; @@ -26,6 +28,7 @@ import qupath.ext.imglib2.accesses.ShortRasterAccess; import qupath.lib.images.servers.ImageServer; import qupath.lib.images.servers.ImageServerMetadata; +import qupath.lib.images.servers.ImageServers; import qupath.lib.images.servers.PixelType; import qupath.lib.images.servers.ServerTools; import qupath.lib.images.servers.TileRequest; @@ -48,9 +51,8 @@ * This class is thread-safe. * * @param the type of the returned accessibles - * @param the type contained in the input image */ -public class ImgBuilder & NativeType, A extends SizableDataAccess> { +public class ImgBuilder { /** * The index of the X axis of accessibles returned by functions of this class @@ -78,12 +80,12 @@ public class ImgBuilder & NativeType, A extends Siza public static final int NUMBER_OF_AXES = 5; private static final CellCache DEFAULT_CELL_CACHE = new CellCache((int) (Runtime.getRuntime().maxMemory() * 0.5 / (1024 * 1024))); private final ImageServer server; - private final Function cellCreator; + private final Function cellCreator; private final int numberOfChannels; private final T type; private CellCache cellCache = DEFAULT_CELL_CACHE; - private ImgBuilder(ImageServer server, T type, Function cellCreator, int numberOfChannels) { + private ImgBuilder(ImageServer server, T type, Function cellCreator, int numberOfChannels) { if (server.nChannels() <= 0) { throw new IllegalArgumentException(String.format("The provided image has less than one channel (%d)", server.nChannels())); } @@ -105,7 +107,7 @@ private ImgBuilder(ImageServer server, T type, Function, ?> createBuilder(ImageServer server) { + public static ImgBuilder> createBuilder(ImageServer server) { if (server.isRGB()) { return new ImgBuilder<>(server, new ARGBType(), ArgbBufferedImageAccess::new, 1); } else { @@ -168,7 +170,7 @@ private ImgBuilder(ImageServer server, T type, Function & NativeType> ImgBuilder createBuilder(ImageServer server, T type) { + public static > ImgBuilder createBuilder(ImageServer server, T type) { checkType(server, type); if (server.isRGB()) { @@ -220,7 +222,7 @@ private ImgBuilder(ImageServer server, T type, Function, ?> createRealBuilder(ImageServer server) { + public static ImgBuilder> createRealBuilder(ImageServer server) { if (server.isRGB()) { return new ImgBuilder<>(server, new UnsignedByteType(), ByteBufferedImageAccess::new, 3); } else { @@ -247,7 +249,7 @@ private ImgBuilder(ImageServer server, T type, Function & NativeType> ImgBuilder createRealBuilder(ImageServer server, T type) { + public static & NativeType> ImgBuilder createRealBuilder(ImageServer server, T type) { checkRealType(server, type); if (server.isRGB()) { @@ -297,7 +299,7 @@ private ImgBuilder(ImageServer server, T type, Function cellCache(CellCache cellCache) { + public ImgBuilder cellCache(CellCache cellCache) { this.cellCache = Objects.requireNonNull(cellCache); return this; } @@ -345,8 +347,15 @@ public RandomAccessibleInterval buildForLevel(int level) { )); } - List tiles = new ArrayList<>(server.getTileRequestManager().getTileRequestsForLevel(level)); + if (type instanceof NativeType) { + return createLazyImage((NativeType)type, level); + } else { + throw new IllegalArgumentException(type + " is not an instanceof NativeType"); + } + } + private > LazyCellImg createLazyImage(S type, int level) { + List tiles = new ArrayList<>(server.getTileRequestManager().getTileRequestsForLevel(level)); return new LazyCellImg<>( new CellGrid( new long[] { @@ -420,21 +429,23 @@ public RandomAccessibleInterval buildForDownsample(double downsample) { } int level = ServerTools.getPreferredResolutionLevel(server, downsample); + RandomAccessibleInterval imgLevel = buildForLevel(level); - if (server.getMetadata().getChannelType() == ImageServerMetadata.ChannelType.CLASSIFICATION) { - return AccessibleScaler.scaleWithNearestNeighborInterpolation( - buildForLevel(level), + if (server.getMetadata().getChannelType() != ImageServerMetadata.ChannelType.CLASSIFICATION && + imgLevel.getType() instanceof NumericType) { + return AccessibleScaler.scaleWithLinearInterpolation( + (RandomAccessibleInterval)imgLevel, server.getDownsampleForResolution(level) / downsample ); } else { - return AccessibleScaler.scaleWithLinearInterpolation( - buildForLevel(level), + return AccessibleScaler.scaleWithNearestNeighborInterpolation( + imgLevel, server.getDownsampleForResolution(level) / downsample ); } } - private static ImgBuilder, ?> createRealBuilderFromNonRgbServer(ImageServer server) { + private static ImgBuilder> createRealBuilderFromNonRgbServer(ImageServer server) { return switch (server.getPixelType()) { case UINT8 -> new ImgBuilder<>( server, @@ -578,7 +589,7 @@ private static void checkRealType(ImageServer server, T type) { } } - private Cell createCell(TileRequest tile) { + private Cell createCell(TileRequest tile) { BufferedImage image; try { image = server.readRegion(tile.getRegionRequest()); @@ -592,4 +603,37 @@ private Cell createCell(TileRequest tile) { cellCreator.apply(image) ); } + + + @SuppressWarnings("unchecked") // Class only used internally + private static & NativeType> T getRealType(PixelType type) { + Objects.requireNonNull(type, "Pixel type must not be null"); + return switch (type) { + case UINT8 -> (T)new UnsignedByteType(); + case INT8 -> (T)new ByteType(); + case UINT16 -> (T)new UnsignedShortType(); + case INT16 -> (T)new ShortType(); + case UINT32 -> (T)new UnsignedIntType(); + case INT32 -> (T)new IntType(); + case FLOAT32 -> (T)new FloatType(); + case FLOAT64 -> (T)new DoubleType(); + }; + } + + + public static > void anything(String[] args) { + try (var server = ImageServers.buildServer("")) { + T t = getRealType(server.getPixelType()); +// RandomAccessibleInterval> img = ImgBuilder.createRealBuilder(server).buildForLevel(0); + RandomAccessibleInterval img = ImgBuilder.createBuilder(server, t).buildForLevel(0); + Views.collapseReal(img); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private static > RandomAccessibleInterval> collapse(RandomAccessibleInterval img) { + return Views.collapseReal(img); + } + } From 9dd727b5149a9f895781924d8af264db26724937 Mon Sep 17 00:00:00 2001 From: Pete Date: Mon, 1 Dec 2025 18:30:22 +0000 Subject: [PATCH 06/12] Reduce code --- .../java/qupath/ext/imglib2/ImgBuilder.java | 378 ++++++------------ 1 file changed, 122 insertions(+), 256 deletions(-) diff --git a/src/main/java/qupath/ext/imglib2/ImgBuilder.java b/src/main/java/qupath/ext/imglib2/ImgBuilder.java index 25445c1..4f62177 100644 --- a/src/main/java/qupath/ext/imglib2/ImgBuilder.java +++ b/src/main/java/qupath/ext/imglib2/ImgBuilder.java @@ -17,8 +17,6 @@ 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 net.imglib2.view.composite.RealComposite; import qupath.ext.imglib2.accesses.ArgbBufferedImageAccess; import qupath.ext.imglib2.accesses.ByteBufferedImageAccess; import qupath.ext.imglib2.accesses.ByteRasterAccess; @@ -28,7 +26,6 @@ import qupath.ext.imglib2.accesses.ShortRasterAccess; import qupath.lib.images.servers.ImageServer; import qupath.lib.images.servers.ImageServerMetadata; -import qupath.lib.images.servers.ImageServers; import qupath.lib.images.servers.PixelType; import qupath.lib.images.servers.ServerTools; import qupath.lib.images.servers.TileRequest; @@ -38,7 +35,6 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; -import java.util.function.Function; import java.util.stream.IntStream; /** @@ -80,20 +76,23 @@ public class ImgBuilder { public static final int NUMBER_OF_AXES = 5; private static final CellCache DEFAULT_CELL_CACHE = new CellCache((int) (Runtime.getRuntime().maxMemory() * 0.5 / (1024 * 1024))); private final ImageServer server; - private final Function cellCreator; private final int numberOfChannels; private final T type; private CellCache cellCache = DEFAULT_CELL_CACHE; - private ImgBuilder(ImageServer server, T type, Function cellCreator, int numberOfChannels) { + private ImgBuilder(ImageServer server, T type, int numberOfChannels) { + Objects.requireNonNull(server, "Server must not be null"); + Objects.requireNonNull(type, "Type must not be null"); if (server.nChannels() <= 0) { throw new IllegalArgumentException(String.format("The provided image has less than one channel (%d)", server.nChannels())); } - this.server = server; this.numberOfChannels = numberOfChannels; - this.cellCreator = cellCreator; this.type = type; + if (server.isRGB() && numberOfChannels == 1) + checkType(server, type); + else + checkRealType(server.getPixelType(), type); } /** @@ -107,14 +106,16 @@ private ImgBuilder(ImageServer server, T type, Function> createBuilder(ImageServer server) { + public static > ImgBuilder> createBuilder(ImageServer server) { if (server.isRGB()) { - return new ImgBuilder<>(server, new ARGBType(), ArgbBufferedImageAccess::new, 1); + return createRgbBuilder(server); } else { - return createRealBuilderFromNonRgbServer(server); + T type = getRealType(server.getPixelType()); + return createBuilder(server, type); } } + /** * Create a builder from an {@link ImageServer}. This doesn't create any accessibles yet. *

@@ -170,126 +171,63 @@ public static ImgBuilder> createBuilder(ImageServer> ImgBuilder createBuilder(ImageServer server, T type) { checkType(server, type); - - if (server.isRGB()) { - return new ImgBuilder<>(server, type, ArgbBufferedImageAccess::new, 1); + if (server.isRGB() && type instanceof ARGBType) { + return new ImgBuilder<>(server, type, 1); + } else if (type instanceof RealType) { + return createRealBuilder(server, (RealType)type); } else { - return switch (server.getPixelType()) { - case UINT8, INT8 -> new ImgBuilder<>( - server, - type, - image -> new ByteRasterAccess(image.getRaster()), - server.nChannels() - ); - case UINT16, INT16 -> new ImgBuilder<>( - server, - type, - image -> new ShortRasterAccess(image.getRaster()), - server.nChannels() - ); - case UINT32, INT32 -> new ImgBuilder<>( - server, - type, - image -> new IntRasterAccess(image.getRaster()), - server.nChannels() - ); - case FLOAT32 -> new ImgBuilder<>( - server, - type, - image -> new FloatRasterAccess(image.getRaster()), - server.nChannels() - ); - case FLOAT64 -> new ImgBuilder<>( - server, - type, - image -> new DoubleRasterAccess(image.getRaster()), - server.nChannels() - ); - }; + throw new IllegalArgumentException(type + " is not an instanceof RealType"); } } /** - * Create a builder from an {@link ImageServer}. This doesn't create any accessibles yet. + * Create a builder from an {@link ImageServer} representing an RGB image using {@link ARGBType}. + * + * @param server the input image + * @return a builder to create an instance of this class + * @throws IllegalArgumentException if the provided image is not RGB + */ + public static ImgBuilder createRgbBuilder(ImageServer server) throws IllegalArgumentException { + return new ImgBuilder<>(server, new ARGBType(), 1); + } + + /** + * Create a builder from an {@link ImageServer}, where the type is known to be an instance of {@link RealType}. + * An RGB image is returned so that the channels are accessed separately, not packed into a single type. *

* The type of the output image is not checked, which might lead to problems later when accessing pixel values of the - * returned accessibles of this class. It is recommended to use {@link #createRealBuilder(ImageServer, RealType)} - * instead. See also this function to know which pixel type is used. + * returned accessibles of this class. It is recommended to use {@link #createRealBuilder(ImageServer, RealType)} instead. + * See also this function to know which pixel type is used. * * @param server the input image * @return a builder to create an instance of this class * @throws IllegalArgumentException if the provided image has less than one channel */ - public static ImgBuilder> createRealBuilder(ImageServer server) { - if (server.isRGB()) { - return new ImgBuilder<>(server, new UnsignedByteType(), ByteBufferedImageAccess::new, 3); - } else { - return createRealBuilderFromNonRgbServer(server); - } + public static > ImgBuilder> createRealBuilder(ImageServer server) { + T type = getRealType(server.getPixelType()); + return createRealBuilder(server, type); } /** - * Create a builder from an {@link ImageServer}. This doesn't create any accessibles yet. + * Create a builder from an {@link ImageServer}, where the type is specified and an instance of {@link RealType}. *

- * The provided type must be compatible with the input image: - *

    - *
  • - * If the input image is {@link ImageServer#isRGB() RGB}, the type must be {@link UnsignedByteType}. Images - * created by the returned builder will have 3 channels (RGB). - *
  • - *
  • Else, see {@link #createBuilder(ImageServer, NumericType)}.
  • - *
+ * Note that the type must be compatible for this to work; see {@link #createBuilder(ImageServer, NumericType)} + * for more information, and/or check the type against the type returned by {@link #getRealType(PixelType)}. * * @param server the input image - * @param type the expected type of the output image * @return a builder to create an instance of this class - * @param the type corresponding to the provided image * @throws IllegalArgumentException if the provided type is not compatible with the input image (see above), or if - * the provided image has less than one channel + * * the provided image has less than one channel */ - public static & NativeType> ImgBuilder createRealBuilder(ImageServer server, T type) { - checkRealType(server, type); - - if (server.isRGB()) { - return new ImgBuilder<>(server, type, ByteBufferedImageAccess::new, 3); - } else { - return switch (server.getPixelType()) { - case UINT8, INT8 -> new ImgBuilder<>( - server, - type, - image -> new ByteRasterAccess(image.getRaster()), - server.nChannels() - ); - case UINT16, INT16 -> new ImgBuilder<>( - server, - type, - image -> new ShortRasterAccess(image.getRaster()), - server.nChannels() - ); - case UINT32, INT32 -> new ImgBuilder<>( - server, - type, - image -> new IntRasterAccess(image.getRaster()), - server.nChannels() - ); - case FLOAT32 -> new ImgBuilder<>( - server, - type, - image -> new FloatRasterAccess(image.getRaster()), - server.nChannels() - ); - case FLOAT64 -> new ImgBuilder<>( - server, - type, - image -> new DoubleRasterAccess(image.getRaster()), - server.nChannels() - ); - }; - } + public static > ImgBuilder createRealBuilder(ImageServer server, T type) { + return new ImgBuilder<>(server, type, server.nChannels()); } + + /** * Accessibles returned by this class will be divided into cells, which will be cached to gain performance. This * function sets the cache to use. By default, a static cache of maximal size half the amount of the @@ -445,59 +383,6 @@ public RandomAccessibleInterval buildForDownsample(double downsample) { } } - private static ImgBuilder> createRealBuilderFromNonRgbServer(ImageServer server) { - return switch (server.getPixelType()) { - case UINT8 -> new ImgBuilder<>( - server, - new UnsignedByteType(), - image -> new ByteRasterAccess(image.getRaster()), - server.nChannels() - ); - case INT8 -> new ImgBuilder<>( - server, - new ByteType(), - image -> new ByteRasterAccess(image.getRaster()), - server.nChannels() - ); - case UINT16 -> new ImgBuilder<>( - server, - new UnsignedShortType(), - image -> new ShortRasterAccess(image.getRaster()), - server.nChannels() - ); - case INT16 -> new ImgBuilder<>( - server, - new ShortType(), - image -> new ShortRasterAccess(image.getRaster()), - server.nChannels() - ); - case UINT32 -> new ImgBuilder<>( - server, - new UnsignedIntType(), - image -> new IntRasterAccess(image.getRaster()), - server.nChannels() - ); - case INT32 -> new ImgBuilder<>( - server, - new IntType(), - image -> new IntRasterAccess(image.getRaster()), - server.nChannels() - ); - case FLOAT32 -> new ImgBuilder<>( - server, - new FloatType(), - image -> new FloatRasterAccess(image.getRaster()), - server.nChannels() - ); - case FLOAT64 -> new ImgBuilder<>( - server, - new DoubleType(), - image -> new DoubleRasterAccess(image.getRaster()), - server.nChannels() - ); - }; - } - private static void checkType(ImageServer server, T type) { if (server.isRGB()) { if (!(type instanceof ARGBType)) { @@ -507,85 +392,19 @@ private static void checkType(ImageServer server, T type) { )); } } else { - switch (server.getPixelType()) { - case UINT8 -> { - if (!(type instanceof UnsignedByteType)) { - throw new IllegalArgumentException(String.format( - "The provided type %s is not a UnsignedByteType, which is the one expected for non-RGB UINT8 images", - type.getClass() - )); - } - } - case INT8 -> { - if (!(type instanceof ByteType)) { - throw new IllegalArgumentException(String.format( - "The provided type %s is not a ByteType, which is the one expected for non-RGB INT8 images", - type.getClass() - )); - } - } - case UINT16 -> { - if (!(type instanceof UnsignedShortType)) { - throw new IllegalArgumentException(String.format( - "The provided type %s is not a UnsignedShortType, which is the one expected for non-RGB UINT16 images", - type.getClass() - )); - } - } - case INT16 -> { - if (!(type instanceof ShortType)) { - throw new IllegalArgumentException(String.format( - "The provided type %s is not a ShortType, which is the one expected for non-RGB INT16 images", - type.getClass() - )); - } - } - case UINT32 -> { - if (!(type instanceof UnsignedIntType)) { - throw new IllegalArgumentException(String.format( - "The provided type %s is not a UnsignedIntType, which is the one expected for non-RGB UINT32 images", - type.getClass() - )); - } - } - case INT32 -> { - if (!(type instanceof IntType)) { - throw new IllegalArgumentException(String.format( - "The provided type %s is not a IntType, which is the one expected for non-RGB INT32 images", - type.getClass() - )); - } - } - case FLOAT32 -> { - if (!(type instanceof FloatType)) { - throw new IllegalArgumentException(String.format( - "The provided type %s is not a FloatType, which is the one expected for non-RGB FLOAT32 images", - type.getClass() - )); - } - } - case FLOAT64 -> { - if (!(type instanceof DoubleType)) { - throw new IllegalArgumentException(String.format( - "The provided type %s is not a DoubleType, which is the one expected for non-RGB FLOAT64 images", - type.getClass() - )); - } - } - } + checkRealType(server.getPixelType(), type); } } - private static void checkRealType(ImageServer server, T type) { - if (server.isRGB()) { - if (!(type instanceof UnsignedByteType)) { - throw new IllegalArgumentException(String.format( - "The provided type %s is not an UnsignedByteType, which is the one expected for RGB images", - type.getClass() - )); - } - } else { - checkType(server, type); + private static void checkRealType(PixelType pixelType, T type) { + var expectedType = getRealType(pixelType); + if (!expectedType.getClass().isInstance(type)) { + throw new IllegalArgumentException(String.format( + "The provided type %s is not %s, which is the one expected for %s images", + type.getClass(), + expectedType.getClass().getSimpleName(), + pixelType + )); } } @@ -600,15 +419,78 @@ private Cell createCell(TileRequest tile) { return new Cell<>( new int[]{ image.getWidth(), image.getHeight(), numberOfChannels, 1, 1 }, new long[]{ tile.getTileX(), tile.getTileY(), 0, tile.getZ(), tile.getT()}, - cellCreator.apply(image) + createAccess(image) ); } + private SizableDataAccess createAccess(BufferedImage img) { + if (server.isRGB()) { + if (numberOfChannels == 1) { + return new ArgbBufferedImageAccess(img); + } else { + return new ByteBufferedImageAccess(img); + } + } else { + var raster = img.getRaster(); + return switch (server.getPixelType()) { + case UINT8, INT8 -> new ByteRasterAccess(raster); + case UINT16, INT16 -> new ShortRasterAccess(raster); + case UINT32, INT32 -> new IntRasterAccess(raster); + case FLOAT32 -> new FloatRasterAccess(raster); + case FLOAT64 -> new DoubleRasterAccess(raster); + }; + } + } + + /** + * Create an instance of the default type for a server. + * If {@code server.isRGB()} returns true, this will call {@link #getRgbType()}, otherwise it will call + * {@link #getRealType(ImageServer)}. + * @param server the image server + * @return the default numeric type to create images for this server + */ + public static NumericType getDefaultType(ImageServer server) { + if (server.isRGB()) + return getRgbType(); + else + return getRealType(server.getPixelType()); + } - @SuppressWarnings("unchecked") // Class only used internally - private static & NativeType> T getRealType(PixelType type) { - Objects.requireNonNull(type, "Pixel type must not be null"); - return switch (type) { + /** + * Create an instance of the default type for an RGB image. + * @return a new instance of {@link ARGBType}. + */ + public static ARGBType getRgbType() { + return new ARGBType(); + } + + /** + * Create an instance of the default {@link RealType} for an image server. + * If the image is RGB, this will return {@link UnsignedByteType} - indicating that channels should be + * treated separately. + * @param server the image server + * @return the default real type to create images for this server + */ + public static RealType getRealType(ImageServer server) { + return getRealType(server.getPixelType()); + } + + /** + * Create the default {@link RealType} for a pixel type. + *

+ * Warning! The return value is unchecked. It is possible to write code that compiles but fails with + * a {@link ClassCastException} at runtime because the caller assigns the wrong class, e.g. + * + * FloatType type = createRealType(PixelType.UINT8); // Compiler cannot check return type! + * + * @param pixelType the pixel type of the image server + * @return an instanceof {@link RealType} suitable to represent the given pixel type + * @param generic parameter for the type + */ + @SuppressWarnings("unchecked") + private static > T getRealType(PixelType pixelType) { + Objects.requireNonNull(pixelType, "Pixel type must not be null"); + return switch (pixelType) { case UINT8 -> (T)new UnsignedByteType(); case INT8 -> (T)new ByteType(); case UINT16 -> (T)new UnsignedShortType(); @@ -620,20 +502,4 @@ private static & NativeType> T getRealType(PixelType t }; } - - public static > void anything(String[] args) { - try (var server = ImageServers.buildServer("")) { - T t = getRealType(server.getPixelType()); -// RandomAccessibleInterval> img = ImgBuilder.createRealBuilder(server).buildForLevel(0); - RandomAccessibleInterval img = ImgBuilder.createBuilder(server, t).buildForLevel(0); - Views.collapseReal(img); - } catch (Exception e) { - e.printStackTrace(); - } - } - - private static > RandomAccessibleInterval> collapse(RandomAccessibleInterval img) { - return Views.collapseReal(img); - } - } From 55810f5c4637446313416e6ea3538e76613b6179 Mon Sep 17 00:00:00 2001 From: Pete Date: Mon, 1 Dec 2025 18:41:14 +0000 Subject: [PATCH 07/12] Add tests, make method public --- .../java/qupath/ext/imglib2/ImgBuilder.java | 3 ++- .../qupath/ext/imglib2/TestImgBuilder.java | 23 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/main/java/qupath/ext/imglib2/ImgBuilder.java b/src/main/java/qupath/ext/imglib2/ImgBuilder.java index 4f62177..900b03c 100644 --- a/src/main/java/qupath/ext/imglib2/ImgBuilder.java +++ b/src/main/java/qupath/ext/imglib2/ImgBuilder.java @@ -488,7 +488,7 @@ public static RealType getRealType(ImageServer server) { * @param generic parameter for the type */ @SuppressWarnings("unchecked") - private static > T getRealType(PixelType pixelType) { + public static > T getRealType(PixelType pixelType) { Objects.requireNonNull(pixelType, "Pixel type must not be null"); return switch (pixelType) { case UINT8 -> (T)new UnsignedByteType(); @@ -501,5 +501,6 @@ private static > T getRealType(PixelType pixelType) { case FLOAT64 -> (T)new DoubleType(); }; } + } diff --git a/src/test/java/qupath/ext/imglib2/TestImgBuilder.java b/src/test/java/qupath/ext/imglib2/TestImgBuilder.java index 5a489c1..8f0e467 100644 --- a/src/test/java/qupath/ext/imglib2/TestImgBuilder.java +++ b/src/test/java/qupath/ext/imglib2/TestImgBuilder.java @@ -37,6 +37,29 @@ public class TestImgBuilder { + @Test + void Check_Fail_Fast() throws Exception { + boolean isRgb = false; + PixelType pixelType = PixelType.UINT8; + ImageServer imageServer = new GenericImageServer(isRgb, pixelType); + Assertions.assertThrows(IllegalArgumentException.class, + () -> ImgBuilder.createBuilder(imageServer, new FloatType()).buildForLevel(0)); + imageServer.close(); + } + + @Test + void Check_Fail_With_Wrong_Type() throws Exception { + boolean isRgb = false; + PixelType pixelType = PixelType.UINT8; + ImageServer imageServer = new GenericImageServer(isRgb, pixelType); + Assertions.assertThrows(ClassCastException.class, + () -> { + FloatType myType = ImgBuilder.getRealType(imageServer.getPixelType()); + ImgBuilder.createBuilder(imageServer, myType).buildForLevel(0); + }); + imageServer.close(); + } + @Test void Check_Rgb_Server() throws Exception { boolean isRgb = true; From a54d959b44497db730895ac955189be717e0f8a5 Mon Sep 17 00:00:00 2001 From: Pete Date: Tue, 2 Dec 2025 11:22:56 +0000 Subject: [PATCH 08/12] Quick fixes --- .../java/qupath/ext/imglib2/ImgBuilder.java | 20 +++++++++---------- .../qupath/ext/imglib2/TestImgBuilder.java | 4 ++++ 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/main/java/qupath/ext/imglib2/ImgBuilder.java b/src/main/java/qupath/ext/imglib2/ImgBuilder.java index 900b03c..6863bdf 100644 --- a/src/main/java/qupath/ext/imglib2/ImgBuilder.java +++ b/src/main/java/qupath/ext/imglib2/ImgBuilder.java @@ -83,16 +83,17 @@ public class ImgBuilder { private ImgBuilder(ImageServer server, T type, int numberOfChannels) { Objects.requireNonNull(server, "Server must not be null"); Objects.requireNonNull(type, "Type must not be null"); - if (server.nChannels() <= 0) { + if (numberOfChannels <= 0) { throw new IllegalArgumentException(String.format("The provided image has less than one channel (%d)", server.nChannels())); } - this.server = server; - this.numberOfChannels = numberOfChannels; - this.type = type; if (server.isRGB() && numberOfChannels == 1) checkType(server, type); else checkRealType(server.getPixelType(), type); + + this.server = server; + this.numberOfChannels = numberOfChannels; + this.type = type; } /** @@ -171,15 +172,11 @@ public static > ImgBuilder> creat * @throws IllegalArgumentException if the provided type is not compatible with the input image (see above), or if * the provided image has less than one channel */ - @SuppressWarnings({"unchecked", "rawtypes"}) public static > ImgBuilder createBuilder(ImageServer server, T type) { - checkType(server, type); if (server.isRGB() && type instanceof ARGBType) { return new ImgBuilder<>(server, type, 1); - } else if (type instanceof RealType) { - return createRealBuilder(server, (RealType)type); } else { - throw new IllegalArgumentException(type + " is not an instanceof RealType"); + return new ImgBuilder<>(server, type, server.nChannels()); } } @@ -216,18 +213,19 @@ public static > ImgBuilder> createRe *

* Note that the type must be compatible for this to work; see {@link #createBuilder(ImageServer, NumericType)} * for more information, and/or check the type against the type returned by {@link #getRealType(PixelType)}. + *

+ * Note that for an RGB image, this will give 3 channels with {@link UnsignedByteType}, rather than using {@link ARGBType}. * * @param server the input image * @return a builder to create an instance of this class * @throws IllegalArgumentException if the provided type is not compatible with the input image (see above), or if * * the provided image has less than one channel + * @see #createRgbBuilder(ImageServer) */ public static > ImgBuilder createRealBuilder(ImageServer server, T type) { return new ImgBuilder<>(server, type, server.nChannels()); } - - /** * Accessibles returned by this class will be divided into cells, which will be cached to gain performance. This * function sets the cache to use. By default, a static cache of maximal size half the amount of the diff --git a/src/test/java/qupath/ext/imglib2/TestImgBuilder.java b/src/test/java/qupath/ext/imglib2/TestImgBuilder.java index 8f0e467..282147e 100644 --- a/src/test/java/qupath/ext/imglib2/TestImgBuilder.java +++ b/src/test/java/qupath/ext/imglib2/TestImgBuilder.java @@ -42,8 +42,10 @@ void Check_Fail_Fast() throws Exception { boolean isRgb = false; PixelType pixelType = PixelType.UINT8; ImageServer imageServer = new GenericImageServer(isRgb, pixelType); + Assertions.assertThrows(IllegalArgumentException.class, () -> ImgBuilder.createBuilder(imageServer, new FloatType()).buildForLevel(0)); + imageServer.close(); } @@ -52,11 +54,13 @@ void Check_Fail_With_Wrong_Type() throws Exception { boolean isRgb = false; PixelType pixelType = PixelType.UINT8; ImageServer imageServer = new GenericImageServer(isRgb, pixelType); + Assertions.assertThrows(ClassCastException.class, () -> { FloatType myType = ImgBuilder.getRealType(imageServer.getPixelType()); ImgBuilder.createBuilder(imageServer, myType).buildForLevel(0); }); + imageServer.close(); } From c44d7028647f993404b60bec2c851a71923fb7ef Mon Sep 17 00:00:00 2001 From: Pete Date: Tue, 2 Dec 2025 11:26:10 +0000 Subject: [PATCH 09/12] Addressing more comments --- src/main/java/qupath/ext/imglib2/ImgBuilder.java | 8 ++++---- src/test/java/qupath/ext/imglib2/TestImgBuilder.java | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/qupath/ext/imglib2/ImgBuilder.java b/src/main/java/qupath/ext/imglib2/ImgBuilder.java index 6863bdf..5b02597 100644 --- a/src/main/java/qupath/ext/imglib2/ImgBuilder.java +++ b/src/main/java/qupath/ext/imglib2/ImgBuilder.java @@ -48,7 +48,7 @@ * * @param the type of the returned accessibles */ -public class ImgBuilder { +public class ImgBuilder> { /** * The index of the X axis of accessibles returned by functions of this class @@ -286,6 +286,7 @@ public RandomAccessibleInterval buildForLevel(int level) { if (type instanceof NativeType) { return createLazyImage((NativeType)type, level); } else { + // This code should not be reached, since we checked the type in the constructor throw new IllegalArgumentException(type + " is not an instanceof NativeType"); } } @@ -367,10 +368,9 @@ public RandomAccessibleInterval buildForDownsample(double downsample) { int level = ServerTools.getPreferredResolutionLevel(server, downsample); RandomAccessibleInterval imgLevel = buildForLevel(level); - if (server.getMetadata().getChannelType() != ImageServerMetadata.ChannelType.CLASSIFICATION && - imgLevel.getType() instanceof NumericType) { + if (server.getMetadata().getChannelType() != ImageServerMetadata.ChannelType.CLASSIFICATION) { return AccessibleScaler.scaleWithLinearInterpolation( - (RandomAccessibleInterval)imgLevel, + imgLevel, server.getDownsampleForResolution(level) / downsample ); } else { diff --git a/src/test/java/qupath/ext/imglib2/TestImgBuilder.java b/src/test/java/qupath/ext/imglib2/TestImgBuilder.java index 282147e..969342f 100644 --- a/src/test/java/qupath/ext/imglib2/TestImgBuilder.java +++ b/src/test/java/qupath/ext/imglib2/TestImgBuilder.java @@ -45,7 +45,7 @@ void Check_Fail_Fast() throws Exception { Assertions.assertThrows(IllegalArgumentException.class, () -> ImgBuilder.createBuilder(imageServer, new FloatType()).buildForLevel(0)); - + imageServer.close(); } From 54bc71dd67b5f70d9b2b4ffd4d1bb03e8f97b461 Mon Sep 17 00:00:00 2001 From: Pete Date: Tue, 2 Dec 2025 12:33:06 +0000 Subject: [PATCH 10/12] More cleanup, addressing issues --- .../java/qupath/ext/imglib2/ImgBuilder.java | 99 +++++++++---------- 1 file changed, 49 insertions(+), 50 deletions(-) diff --git a/src/main/java/qupath/ext/imglib2/ImgBuilder.java b/src/main/java/qupath/ext/imglib2/ImgBuilder.java index 5b02597..c7d8e58 100644 --- a/src/main/java/qupath/ext/imglib2/ImgBuilder.java +++ b/src/main/java/qupath/ext/imglib2/ImgBuilder.java @@ -86,10 +86,7 @@ private ImgBuilder(ImageServer server, T type, int numberOfChanne if (numberOfChannels <= 0) { throw new IllegalArgumentException(String.format("The provided image has less than one channel (%d)", server.nChannels())); } - if (server.isRGB() && numberOfChannels == 1) - checkType(server, type); - else - checkRealType(server.getPixelType(), type); + checkType(server, type, numberOfChannels); this.server = server; this.numberOfChannels = numberOfChannels; @@ -101,19 +98,16 @@ private ImgBuilder(ImageServer server, T type, int numberOfChanne *

* The type of the output image is not checked, which might lead to problems later when accessing pixel values of the * returned accessibles of this class. It is recommended to use {@link #createBuilder(ImageServer, NumericType)} instead. - * See also this function to know which pixel type is used. + *

+ * To query the pixel type that will be used, see {@link #getDefaultType(ImageServer)}. * * @param server the input image * @return a builder to create an instance of this class * @throws IllegalArgumentException if the provided image has less than one channel */ - public static > ImgBuilder> createBuilder(ImageServer server) { - if (server.isRGB()) { - return createRgbBuilder(server); - } else { - T type = getRealType(server.getPixelType()); - return createBuilder(server, type); - } + public static > ImgBuilder> createBuilder(ImageServer server) { + T type = getDefaultTypeUnsafe(server); + return createBuilder(server, type); } @@ -188,7 +182,7 @@ public static > ImgBuilder createBuilder(ImageServer * @throws IllegalArgumentException if the provided image is not RGB */ public static ImgBuilder createRgbBuilder(ImageServer server) throws IllegalArgumentException { - return new ImgBuilder<>(server, new ARGBType(), 1); + return createBuilder(server, new ARGBType()); } /** @@ -204,7 +198,7 @@ public static ImgBuilder createRgbBuilder(ImageServer s * @throws IllegalArgumentException if the provided image has less than one channel */ public static > ImgBuilder> createRealBuilder(ImageServer server) { - T type = getRealType(server.getPixelType()); + T type = getRealTypeUnsafe(server.getPixelType()); return createRealBuilder(server, type); } @@ -223,7 +217,7 @@ public static > ImgBuilder> createRe * @see #createRgbBuilder(ImageServer) */ public static > ImgBuilder createRealBuilder(ImageServer server, T type) { - return new ImgBuilder<>(server, type, server.nChannels()); + return createBuilder(server, type); } /** @@ -283,8 +277,10 @@ public RandomAccessibleInterval buildForLevel(int level) { )); } - if (type instanceof NativeType) { - return createLazyImage((NativeType)type, level); + if (type instanceof NativeType) { + @SuppressWarnings({"unchecked", "rawtypes"}) // Type checked in constructor + RandomAccessibleInterval img = createLazyImage((NativeType)type, level); + return img; } else { // This code should not be reached, since we checked the type in the constructor throw new IllegalArgumentException(type + " is not an instanceof NativeType"); @@ -335,9 +331,10 @@ public RandomAccessibleInterval buildForLevel(int level) { * applied. The ith returned {@link RandomAccessibleInterval} corresponds to the ith provided downsample * @throws IllegalArgumentException if one of the provided downsamples is not greater than 0 */ - public List> buildForDownsamples(List downsamples) { + public List> buildForDownsamples(List downsamples) { return downsamples.stream() - .map(this::buildForDownsample) + .mapToDouble(Number::doubleValue) + .mapToObj(this::buildForDownsample) .toList(); } @@ -368,21 +365,16 @@ public RandomAccessibleInterval buildForDownsample(double downsample) { int level = ServerTools.getPreferredResolutionLevel(server, downsample); RandomAccessibleInterval imgLevel = buildForLevel(level); + double scale = server.getDownsampleForResolution(level) / downsample; if (server.getMetadata().getChannelType() != ImageServerMetadata.ChannelType.CLASSIFICATION) { - return AccessibleScaler.scaleWithLinearInterpolation( - imgLevel, - server.getDownsampleForResolution(level) / downsample - ); + return AccessibleScaler.scaleWithLinearInterpolation(imgLevel, scale); } else { - return AccessibleScaler.scaleWithNearestNeighborInterpolation( - imgLevel, - server.getDownsampleForResolution(level) / downsample - ); + return AccessibleScaler.scaleWithNearestNeighborInterpolation(imgLevel, scale); } } - private static void checkType(ImageServer server, T type) { - if (server.isRGB()) { + private static void checkType(ImageServer server, T type, int nChannels) { + if (server.isRGB() && nChannels == 1) { if (!(type instanceof ARGBType)) { throw new IllegalArgumentException(String.format( "The provided type %s is not an ARGBType, which is the one expected for RGB images", @@ -390,21 +382,19 @@ private static void checkType(ImageServer server, T type) { )); } } else { - checkRealType(server.getPixelType(), type); + var pixelType = server.getPixelType(); + var expectedType = getRealType(pixelType); + if (!expectedType.getClass().isInstance(type)) { + throw new IllegalArgumentException(String.format( + "The provided type %s is not %s, which is the one expected for %s images", + type.getClass(), + expectedType.getClass().getSimpleName(), + pixelType + )); + } } } - private static void checkRealType(PixelType pixelType, T type) { - var expectedType = getRealType(pixelType); - if (!expectedType.getClass().isInstance(type)) { - throw new IllegalArgumentException(String.format( - "The provided type %s is not %s, which is the one expected for %s images", - type.getClass(), - expectedType.getClass().getSimpleName(), - pixelType - )); - } - } private Cell createCell(TileRequest tile) { BufferedImage image; @@ -441,21 +431,27 @@ private SizableDataAccess createAccess(BufferedImage img) { } /** - * Create an instance of the default type for a server. - * If {@code server.isRGB()} returns true, this will call {@link #getRgbType()}, otherwise it will call - * {@link #getRealType(ImageServer)}. + * Create an instance of the default type for a server when using {@link #createBuilder(ImageServer)}. + * If {@code server.isRGB()} returns true, this will return the value of {@link #getRgbType()}, + * otherwise it will return the value of {@link #getRealType(ImageServer)}. * @param server the image server * @return the default numeric type to create images for this server */ public static NumericType getDefaultType(ImageServer server) { + return getDefaultTypeUnsafe(server); + } + + @SuppressWarnings("unchecked") // Private method, used internally + private static > T getDefaultTypeUnsafe(ImageServer server) { if (server.isRGB()) - return getRgbType(); + return (T)getRgbType(); else - return getRealType(server.getPixelType()); + return (T)getRealType(server.getPixelType()); } /** - * Create an instance of the default type for an RGB image. + * Create an instance of the default type for an RGB image, using a packed int representation so + * that all RGB values can be contained in a single 'channel'. * @return a new instance of {@link ARGBType}. */ public static ARGBType getRgbType() { @@ -465,7 +461,7 @@ public static ARGBType getRgbType() { /** * Create an instance of the default {@link RealType} for an image server. * If the image is RGB, this will return {@link UnsignedByteType} - indicating that channels should be - * treated separately. + * treated separately, and not used a packed representation as with {@link #getRgbType()}. * @param server the image server * @return the default real type to create images for this server */ @@ -485,8 +481,12 @@ public static RealType getRealType(ImageServer server) { * @return an instanceof {@link RealType} suitable to represent the given pixel type * @param generic parameter for the type */ - @SuppressWarnings("unchecked") public static > T getRealType(PixelType pixelType) { + return getRealTypeUnsafe(pixelType); + } + + @SuppressWarnings("unchecked") // Private method + private static > T getRealTypeUnsafe(PixelType pixelType) { Objects.requireNonNull(pixelType, "Pixel type must not be null"); return switch (pixelType) { case UINT8 -> (T)new UnsignedByteType(); @@ -499,6 +499,5 @@ public static > T getRealType(PixelType pixelType) { case FLOAT64 -> (T)new DoubleType(); }; } - } From 1cc760b29753d7b78a3fd77d0fff9e8832b15357 Mon Sep 17 00:00:00 2001 From: Pete Date: Tue, 2 Dec 2025 14:57:26 +0000 Subject: [PATCH 11/12] Minor fixes --- src/main/java/qupath/ext/imglib2/ImgBuilder.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/main/java/qupath/ext/imglib2/ImgBuilder.java b/src/main/java/qupath/ext/imglib2/ImgBuilder.java index c7d8e58..5e2c3c9 100644 --- a/src/main/java/qupath/ext/imglib2/ImgBuilder.java +++ b/src/main/java/qupath/ext/imglib2/ImgBuilder.java @@ -117,8 +117,9 @@ public static > ImgBuilder> cr * The provided type must be compatible with the input image: *

    *
  • - * If the input image is {@link ImageServer#isRGB() RGB}, the type must be {@link ARGBType}. Images created - * by the returned builder will have one channel. + * If the input image is {@link ImageServer#isRGB() RGB}, the type may be {@link ARGBType}, in which case + * images created by the returned builder will have one channel. + * Alternatively, the type may be {@link UnsignedByteType} and the image will have three channels. *
  • *
  • * Else: @@ -213,7 +214,7 @@ public static > ImgBuilder> createRe * @param server the input image * @return a builder to create an instance of this class * @throws IllegalArgumentException if the provided type is not compatible with the input image (see above), or if - * * the provided image has less than one channel + * the provided image has less than one channel * @see #createRgbBuilder(ImageServer) */ public static > ImgBuilder createRealBuilder(ImageServer server, T type) { @@ -441,7 +442,7 @@ public static NumericType getDefaultType(ImageServer server) { return getDefaultTypeUnsafe(server); } - @SuppressWarnings("unchecked") // Private method, used internally + @SuppressWarnings("unchecked") // Private method - must only be used internally with appropriate return value private static > T getDefaultTypeUnsafe(ImageServer server) { if (server.isRGB()) return (T)getRgbType(); @@ -485,7 +486,7 @@ public static > T getRealType(PixelType pixelType) { return getRealTypeUnsafe(pixelType); } - @SuppressWarnings("unchecked") // Private method + @SuppressWarnings("unchecked") // Private method - must only be used internally with appropriate return value private static > T getRealTypeUnsafe(PixelType pixelType) { Objects.requireNonNull(pixelType, "Pixel type must not be null"); return switch (pixelType) { From d630f0dec757c9d735cf449040b19c4bb6e6ecfd Mon Sep 17 00:00:00 2001 From: Pete Date: Tue, 2 Dec 2025 15:01:19 +0000 Subject: [PATCH 12/12] Unsafety first --- src/main/java/qupath/ext/imglib2/ImgBuilder.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/qupath/ext/imglib2/ImgBuilder.java b/src/main/java/qupath/ext/imglib2/ImgBuilder.java index 5e2c3c9..6b7871f 100644 --- a/src/main/java/qupath/ext/imglib2/ImgBuilder.java +++ b/src/main/java/qupath/ext/imglib2/ImgBuilder.java @@ -384,7 +384,7 @@ private static void checkType(ImageServer server, T type, int nChannels) } } else { var pixelType = server.getPixelType(); - var expectedType = getRealType(pixelType); + var expectedType = getRealTypeUnsafe(pixelType); if (!expectedType.getClass().isInstance(type)) { throw new IllegalArgumentException(String.format( "The provided type %s is not %s, which is the one expected for %s images", @@ -447,7 +447,7 @@ private static > T getDefaultTypeUnsafe(ImageServer if (server.isRGB()) return (T)getRgbType(); else - return (T)getRealType(server.getPixelType()); + return (T)getRealTypeUnsafe(server.getPixelType()); } /** @@ -467,7 +467,7 @@ public static ARGBType getRgbType() { * @return the default real type to create images for this server */ public static RealType getRealType(ImageServer server) { - return getRealType(server.getPixelType()); + return getRealTypeUnsafe(server.getPixelType()); } /**