Export masks also for images without annotations

Hi! I have a project of 1484x1484 images with zero, one or multiple small circular annotations. I can run the script below to export masks for all the images with annotations, but it won’t write anything for the image files without annotations (I want just a white 1484x1484 square without black holes for those as well). Ideally I want a script that makes a full image annotation, then subtracts the small circular annotations (or make no subtractions for images without annotations) - I think that would solve the problem, but can’t figure out or find a suitable script. Can anyone help create a preprocessing script or modify the export script to also produce empty mask images for images without annotations?

Important: It must be able to run as a script/batch processing for the entire project. A preprosessing script that creates a full image annotation, then subtracts the small circular annotations would probably do it, but can’t get any of the existing “subtract annotations” scripts to work.

Script to export masks (creates masks for annotated images, but unfortunately produce no output for unannotated images):

import qupath.lib.images.servers.LabeledImageServer

def imageData = getCurrentImageData()

// Define output path (relative to project)
def name = GeneralTools.getNameWithoutExtension(imageData.getServer().getMetadata().getName())
def pathOutput = buildFilePath(PROJECT_BASE_DIR, 'tiles', name)
mkdirs(pathOutput)

// Define output resolution
double requestedPixelSize = 1

// Convert to downsample
double downsample = requestedPixelSize / imageData.getServer().getPixelCalibration().getAveragedPixelSize()

// Create an ImageServer where the pixels are derived from annotations
def labelServer = new LabeledImageServer.Builder(imageData)
    .backgroundLabel(0, ColorTools.WHITE) // Specify background label (usually 0 or 255)
    .downsample(downsample)    // Choose server resolution; this should match the resolution at which tiles are exported
    .addLabel('Tumor', 1)      // Choose output labels (the order matters!)
    .addLabel('Stroma', 2)
    .multichannelOutput(false)  // If true, each label is a different channel (required for multiclass probability)
    .build()

// Create an exporter that requests corresponding tiles from the original & labeled image servers
new TileExporter(imageData)
    .downsample(downsample)     // Define export resolution
    .imageExtension('.tif')     // Define file extension for original pixels (often .tif, .jpg, '.png' or '.ome.tif')
    .tileSize(1484)              // Define size of each tile, in pixels
    .labeledServer(labelServer) // Define the labeled image server to use (i.e. the one we just built)
    .annotatedTilesOnly(false)  // If true, only export tiles if there is a (labeled) annotation present
    .overlap(0)                // Define overlap, in pixel units at the export resolution
    .writeTiles(pathOutput)     // Write tiles to the specified directory

print 'Done!'

Caveat: I haven’t tested this… but it looks like the issue may be that you need to include

   includePartialTiles(true)

amidst your options in TileExporter.

Reasoning: your tile size is defined in terms of the export size… but if you’re downsampling, and the image is 1484 pixels in size, then there won’t be any complete tiles to export.

If that isn’t the problem, you can try adding

addUnclassifiedLabel(0)

to the LabelImageServer.Builder and just create a full image annotation. There should be no need to subtract anything from it: the order in which the labels are specified matters; any later classified annotations will be drawn later – see the ‘Tip’ at https://qupath.readthedocs.io/en/latest/docs/advanced/exporting_annotations.html#individual-annotations

1 Like

Neither worked (gave various error messages), but figured it out anyway, thanks to your comment on downsampling - just removed this line from the LabeledImageServer.Builder part:

    .downsample(downsample)    // Choose server resolution; this should match the resolution at which tiles are exported

Here is the final working script:

import qupath.lib.images.servers.LabeledImageServer

def imageData = getCurrentImageData()

// Define output path (relative to project)
def name = GeneralTools.getNameWithoutExtension(imageData.getServer().getMetadata().getName())
def pathOutput = buildFilePath(PROJECT_BASE_DIR, 'tiles')
mkdirs(pathOutput)

// Define output resolution
double requestedPixelSize = 1

// Convert to downsample
double downsample = requestedPixelSize / imageData.getServer().getPixelCalibration().getAveragedPixelSize()

// Create an ImageServer where the pixels are derived from annotations
def labelServer = new LabeledImageServer.Builder(imageData)
    .backgroundLabel(0, ColorTools.WHITE) // Specify background label (usually 0 or 255)
    .addLabel('Tumor', 1)      // Choose output labels (the order matters!)
    .addLabel('Stroma', 2)
    .multichannelOutput(false)  // If true, each label is a different channel (required for multiclass probability)
    .build()

// Create an exporter that requests corresponding tiles from the original & labeled image servers
new TileExporter(imageData)
    .downsample(downsample)     // Define export resolution
    .imageExtension('.tif')     // Define file extension for original pixels (often .tif, .jpg, '.png' or '.ome.tif')
    .tileSize(1484)              // Define size of each tile, in pixels
    .labeledServer(labelServer) // Define the labeled image server to use (i.e. the one we just built)
    .annotatedTilesOnly(false)  // If true, only export tiles if there is a (labeled) annotation present
    .overlap(0)                // Define overlap, in pixel units at the export resolution
    .writeTiles(pathOutput)     // Write tiles to the specified directory

print 'Done!'```
1 Like

The scripts gives a warning e.g. “WARN: Resizing tile from 511x512 to 512x512” for about 10-50% of the tiles it creates and writes these tiles as 511x511 or 511x512 or 512x511, and not the requested 512x512. Is there any way to correct this forcing only 512x512 tiles to be written?

It’s not really possible to answer that without access to the same images. I understand from your original post that your images are all 1484x1484 pixels in size, but I don’t know the pixel size, annotations, script changes or any other potential sources of variation.

The easiest thing may be to export everything at the full resolution and then resize elsewhere (e.g. in Python, if that is the next step).

The pixel size is 0.2300 um per pixel and the script is set to that resolution as well (so not really downsampling anyway). What would the script above look like if I wanted to export in full resolution (no downsampling)? Tried removing this part from the “new TileExporter(imageData)” part, but didn’t work.

    .downsample(downsample)     // Define export resolution

Also tried setting this to 0, but doesn’t really make sense, so didn’t expect that to work:)

// Define output resolution
double requestedPixelSize = 1

Set

double downsample = 1

and then you can remove the requestedPixelSize altogether.

Perfect - thank you! That work perfectly and solved the resolution (512x511… etc) problem - thank you so much again, Pete, you’re brilliant! Now I just need a corresponding working import masks to annotations script for the corresponding exported file-names (“IMAGENAME [x=3072,y=10240,w=512,h=512].tif”) and I can use DeepMIB and Qupath together. Still no replies on that post unfortunately and can’t the old import mask script to work.

Final working export tiles and corresponding masks script:

import qupath.lib.images.servers.LabeledImageServer

def imageData = getCurrentImageData()

// Define output path (relative to project)
def name = GeneralTools.getNameWithoutExtension(imageData.getServer().getMetadata().getName())
def pathOutput = buildFilePath(PROJECT_BASE_DIR, 'exportedTILESandMASKS')
mkdirs(pathOutput)

// Convert to downsample
double downsample = 1

// Create an ImageServer where the pixels are derived from annotations
def labelServer = new LabeledImageServer.Builder(imageData)
    .backgroundLabel(0, ColorTools.WHITE) // Specify background label (usually 0 or 255)
    .addLabel('Tumor', 1)      // Choose output labels (the order matters!)
    .addLabel('Stroma', 2)
    .multichannelOutput(false)  // If true, each label is a different channel (required for multiclass probability)
    .build()

// Create an exporter that requests corresponding tiles from the original & labeled image servers
new TileExporter(imageData)
    .downsample(downsample)     // Define export resolution
    .imageExtension('.tif')     // Define file extension for original pixels (often .tif, .jpg, '.png' or '.ome.tif')
    .tileSize(512)              // Define size of each tile, in pixels
    .labeledServer(labelServer) // Define the labeled image server to use (i.e. the one we just built)
    .annotatedTilesOnly(true)  // If true, only export tiles if there is a (labeled) annotation present
    .overlap(0)                // Define overlap, in pixel units at the export resolution
    .writeTiles(pathOutput)     // Write tiles to the specified directory

print 'Done!'

@melvingelbard and I discussed it this morning, hope to get something to you soon… :slight_smile:

It will be have to be temporary for v0.2 – labelled image tracing has been rewritten for v0.3, with the plan of having a more built-in and generic solution.

3 Likes