Qupath unable to write image

I am using a slightly modified version of one of Pete’s scripts for exporting annotations in QuPath 0.2.3 and it usuallt works as expected, but now one particular image results in the script throwing an IOException. The image is a regular .ndpi image, just like all the other images I’m working on. It’s simply just one in a bunch, but it’s the only one giving me an error. The only thing I could think of was that maybe it was suddenly deemed too large. This doesn’t make much sense as I’ve exported this particular image using the Export training regions dialog that was found in the Extension->AI menu in previous versions of QuPath. Nevertheless, I tried too export just a small region of the image, but I get the same error no matter how small a region I select (except that I managed to export once by first creating a file with the same name and path and then deleting said file after running the script, but I have no idea why that made it work). The complete error message is shown below along with the script.

The commented part at the end (with try/catch) was only added to test exporting smaller regions because of the error I received.

I can share the image in question upon request.

import qupath.lib.images.servers.LabeledImageServer

def imageData = getCurrentImageData()

// Define output path (relative to project)
def outputDir = buildFilePath(PROJECT_BASE_DIR, 'export')
mkdirs(outputDir)
def name = GeneralTools.getNameWithoutExtension(imageData.getServer().getMetadata().getName())
def path = buildFilePath(outputDir, name + ".png")

// Define how much to downsample during export (may be required for large images)
double downsample = 1

// Create an ImageServer where the pixels are derived from annotations
def labelServer = new LabeledImageServer.Builder(imageData)
  .backgroundLabel(0, ColorTools.makeRGB(180, 180, 180)) // Specify background label (usually 0 or 255)
  .downsample(downsample)    // Choose server resolution; this should match the resolution at which tiles are exported
  .addLabel('Ignore*', 1)      // Choose output labels (the order matters!)
  .addLabel('Zone 1', 2)
  .addLabel('Zone 2', 3)
  .addLabel('Thrombus', 4)
  .addLabel('Background', 5)
  .multichannelOutput(false) // If true, each label refers to the channel of a multichannel binary image (required for multiclass probability)
  .build()

// Write the image
//try {
    writeImage(labelServer, path)
//} catch (IOException) {
//    def roi = getSelectedROI()
//    def requestROI = RegionRequest.createInstance(labelServer.getPath(), 0, roi)
//    writeImageRegion(labelServer, requestROI, path)
//}
WARN: Unable to write image
ERROR: IOException at line 28: Unable to write C:\Users\bthorsted\ZoneDetection2.3.0\export\3330.2V Weigert.png!  No compatible writer found.

ERROR: qupath.lib.images.writers.ImageWriterTools.writeImage(ImageWriterTools.java:174)
    qupath.lib.scripting.QP.writeImage(QP.java:2443)
    qupath.lib.scripting.QP$writeImage$1.callStatic(Unknown Source)
    org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCallStatic(CallSiteArray.java:55)
    org.codehaus.groovy.runtime.callsite.AbstractCallSite.callStatic(AbstractCallSite.java:217)
    org.codehaus.groovy.runtime.callsite.AbstractCallSite.callStatic(AbstractCallSite.java:240)
    Script23.run(Script23.groovy:29)
    org.codehaus.groovy.jsr223.GroovyScriptEngineImpl.eval(GroovyScriptEngineImpl.java:317)
    org.codehaus.groovy.jsr223.GroovyScriptEngineImpl.eval(GroovyScriptEngineImpl.java:155)
    qupath.lib.gui.scripting.DefaultScriptEditor.executeScript(DefaultScriptEditor.java:926)
    qupath.lib.gui.scripting.DefaultScriptEditor.executeScript(DefaultScriptEditor.java:859)
    qupath.lib.gui.scripting.DefaultScriptEditor.executeScript(DefaultScriptEditor.java:782)
    qupath.lib.gui.scripting.DefaultScriptEditor$2.run(DefaultScriptEditor.java:1271)
    java.base/java.util.concurrent.Executors$RunnableAdapter.call(Unknown Source)
    java.base/java.util.concurrent.FutureTask.run(Unknown Source)
    java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
    java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
    java.base/java.lang.Thread.run(Unknown Source)

It sounds similar to the issue here: A Rare Error Writing Tile - No compatible writer found

If you or @CCIPD_Yijiang are able to share an image with me that fails I can investigate what could be going wrong.

2 Likes

Are there any news about the issue so far?

@BThorsted From the image you sent me, it seems simply to be too big.

The dimensions are 94208 x 47104 = 4,437,573,632

This is bigger than 2^31 = 2,147,483,648, which is the maximum size of a Java array. All the pixels need to fit into an array to write it as a PNG with ImageIO.

Downsampling by a factor of 2 works, or writing a (tiled, multiresolution) OME-TIFF works for me by using

def path = buildFilePath(outputDir, name + ".ome.tiff")

If you really want PNG at full resolution, you can use the Tile Exporter to split it up into separate images.

Okay, that makes sense. If it was up to me, the image would be a lot smaller. There are a lot of unnecessary debris in the image that is not of importance, but it apparently has a large enough contrast for the slide scanner software to think it is relevant. If only I knew how to crop .ndpi images, I could avoid this problem entirely.

I will give the tile exporter a try since it is really too late to begin downsampling anything.

1 Like

Well, you can use writeImageRegion as well, if you can define the area you care about. The link shows how to get a region from a ROI and write only that.

1 Like

Actually, for some reason, the TileExporter is not exporting the annotations and the writeImageRegion is failing on me with an IOException.

I’m not sure if that’s a question, but to be able to help I’d need your modified script and more details about what specifically is included in the IOException.

It’s the exact same error message I’m getting for the writeImageRegion, just the trace is a bit different. I don’t get any error from TileExporter, it just doesn’t export the annotations along with the raw image.

So this is the modified code:

import qupath.lib.images.servers.LabeledImageServer

def imageData = getCurrentImageData()

// Define output path (relative to project)
def outputDir = buildFilePath(PROJECT_BASE_DIR, 'export')
mkdirs(outputDir)
def name = GeneralTools.getNameWithoutExtension(imageData.getServer().getMetadata().getName())
def path = buildFilePath(outputDir, name + ".png")

// Define how much to downsample during export (may be required for large images)
double downsample = 1

// Create an ImageServer where the pixels are derived from annotations
def labelServer = new LabeledImageServer.Builder(imageData)
  .backgroundLabel(0, ColorTools.makeRGB(180, 180, 180)) // Specify background label (usually 0 or 255)
  .downsample(downsample)    // Choose server resolution; this should match the resolution at which tiles are exported
  .addLabel('Ignore*', 1)      // Choose output labels (the order matters!)
  .addLabel('Zone 1', 2)
  .addLabel('Zone 2', 3)
  .addLabel('Thrombus', 4)
  .addLabel('Background', 5)
  .multichannelOutput(false)  // If true, each label refers to the channel of a multichannel binary image (required for multiclass probability)
  .build()

// Write the image
try {
    writeImage(labelServer, path)
} catch (IOException e1) {
        def roi = getSelectedROI()
        def requestROI = RegionRequest.createInstance(labelServer.getPath(), 0, roi)
        writeImageRegion(labelServer, requestROI, path)
}

The TileExporter is formatted like this:

...// Same as previous

try {
    writeImage(labelServer, path)
} catch (IOException e1) {
    try {
        def roi = getSelectedROI()
        def requestROI = RegionRequest.createInstance(labelServer.getPath(), 0, roi)
        writeImageRegion(labelServer, requestROI, path)
    } catch (IOException e2) {
        def pathOutput = buildFilePath(outputDir, 'tiles', name)
        mkdirs(pathOutput)
        // Create an exporter that requests corresponding tiles from the original & labelled image servers
        new TileExporter(imageData)
            .downsample(downsample)   // Define export resolution
            .imageExtension('.jpg')   // Define file extension for original pixels (often .tif, .jpg, '.png' or '.ome.tif')
            .tileSize(16384)            // Define size of each tile, in pixels
            .annotatedTilesOnly(false) // If true, only export tiles if there is a (classified) annotation present
            .overlap(64)              // Define overlap, in pixel units at the export resolution
            .writeTiles(pathOutput)   // Write tiles to the specified directory
    }
}

Here’s the trace for the error:

ERROR: IOException at line 33: Unable to write C:\Users\bthorsted\ZoneDetection2.3.0\export\3330.2V Weigert.png!  No compatible writer found.

ERROR: qupath.lib.images.writers.ImageWriterTools.writeImageRegion(ImageWriterTools.java:127)
    qupath.lib.scripting.QP.writeImageRegion(QP.java:2409)
    qupath.lib.scripting.QP$writeImageRegion$3.callStatic(Unknown Source)
    org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCallStatic(CallSiteArray.java:55)
    org.codehaus.groovy.runtime.callsite.AbstractCallSite.callStatic(AbstractCallSite.java:217)
    org.codehaus.groovy.runtime.callsite.AbstractCallSite.callStatic(AbstractCallSite.java:249)
    Script45.run(Script45.groovy:34)
    org.codehaus.groovy.jsr223.GroovyScriptEngineImpl.eval(GroovyScriptEngineImpl.java:317)
    org.codehaus.groovy.jsr223.GroovyScriptEngineImpl.eval(GroovyScriptEngineImpl.java:155)
    qupath.lib.gui.scripting.DefaultScriptEditor.executeScript(DefaultScriptEditor.java:926)
    qupath.lib.gui.scripting.DefaultScriptEditor.executeScript(DefaultScriptEditor.java:859)
    qupath.lib.gui.scripting.DefaultScriptEditor.executeScript(DefaultScriptEditor.java:782)
    qupath.lib.gui.scripting.DefaultScriptEditor$2.run(DefaultScriptEditor.java:1271)
    java.base/java.util.concurrent.Executors$RunnableAdapter.call(Unknown Source)
    java.base/java.util.concurrent.FutureTask.run(Unknown Source)
    java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
    java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
    java.base/java.lang.Thread.run(Unknown Source)

Do you have a selected ROI?

If you have an annotation on the image, and you’re running this in batch, getSelectedROI() won’t work unless you explicitly select the annotation of interest. If you can safely assume you have exactly one annotation, this works:

setSelectedObject(getAnnotationObjects()[0])
print getSelectedROI()

Otherwise the script should really loop through all annotations.

For the TileExporter, I don’t know what corresponds to line 33/34 since I don’t have your entire script in one place but the problem seems to be with writeImageRegion again, so I presume TileExporter isn’t reached.

1 Like

I’m positive that I have a rectangular ROI selected. The trace was for writeImageRegion, so thats why it doesn’t make sense for the TileExporter.

When I run the script for TileExporter it creates tiles only for the original image, but not the annotations, and no error is produced, so I have no trace to show.

How? Did you try print getSelectedROI()?

If this is null, see Error when exporting region of the image in script - #3 by petebankhead

You haven’t passed your labelServer to the TileExporter: Exporting annotations — QuPath 0.2.3 documentation

1 Like

I get this output:

print getSelectedROI()

INFO: Rectangle (27519, 3092, 62765, 41884)

Oh, stupid me, I was wondering why I didn’t have to pass the labelServer to the TileExporter, but apparently I copied from a source that just didn’t include that snippet.

So, related question. I think I managed to export my data as tiles, but I have to stitch it back together for it to work further down the pipeline. I wrote a python script to do the magic using tifffile and bigtiff and even forced it to make tiles and pyramid levels, but QuPath won’t open the image even if Fiji can open it with bio-formats (albeit using the crop option to load only a subsection). Why won’t QuPath open a tiled tif?

Below is the tiffinfo output:

tiffinfo -D 3330.2V_Weigert.tif
TIFF Directory at offset 0x10 (16)
  Image Width: 70672 Image Length: 47104
  Tile Width: 1024 Tile Length: 1024
  Resolution: 1, 1 (unitless)
  Bits/Sample: 8
  Compression Scheme: JPEG
  Photometric Interpretation: YCbCr
  YCbCr Subsampling: 2, 2
  Samples/Pixel: 3
  Planar Configuration: single image plane
  Reference Black/White:
     0:     0   255
     1:   128   255
     2:   128   255
  SubIFD Offsets: 693855922 778445554 785697562
  ImageDescription: {"shape": [47104, 70672, 3]}
  Software: tifffile.py
Cannot display data: th * rowsize > tilesize

I didn’t see if you posted the new error somewhere.

It simply pops up with the message: “Failed to load one image.”

The log has this additional info:

INFO: Image data set to null
WARN: Openslide: Property 'openslide.mpp-x' not available, will return default value NaN
WARN: Openslide: Property 'openslide.mpp-y' not available, will return default value NaN
WARN: Openslide: Property 'openslide.objective-power' not available, will return default value NaN
WARN: Unable to open file:/home/bthorsted/Pictures/3330/3330.2V_Weigert.tif with OpenSlide: -966033408
WARN: Unable to open UriImageSupport (class qupath.lib.images.servers.openslide.OpenslideServerBuilder) support=2.5, builders=1
WARN: Exception adding Image null
ERROR: Import images: Failed to load one image.
INFO: 
1 Like

It looks like you have not chosen BioFormats for the importer, maybe? I only see an Openslide error.

I get the same error for bio-formats:

WARN: Exception adding Image null
ERROR: Import images: Failed to load one image.
The image type might not be supported by 'Bio-Formats builder'
INFO: 
1 Like

I presume the image isn’t pyramidal. Without the crop option, Bio-Formats will still be trying to read it all in one go – and this won’t work.

It should be pyramidal. I followed the tifffile manual, which states that you can “Write a tiled, multi-resolution, pyramidal, OME-TIFF file using JPEG compression. Sub-resolution images are written to SubIFDs” using the following code snippet:

>>> data = numpy.arange(1024*1024*3, dtype='uint8').reshape((1024, 1024, 3))
>>> with TiffWriter('temp.ome.tif', bigtiff=True) as tif:
...     options = dict(tile=(256, 256), compression='jpeg')
...     tif.write(data, subifds=2, **options)
...     # save pyramid levels to the two subifds
...     # in production use resampling to generate sub-resolutions
...     tif.write(data[::2, ::2], subfiletype=1, **options)
...     tif.write(data[::4, ::4], subfiletype=1, **options)

Only difference is that I didn’t add the .ome before the extension and I added a third pyramid level.