Exporting Stardist2D nuclei mask within annotations

Hi,

I have tried to export nuclei masks using

def labelServer = new LabeledImageServer.Builder(imageData)
    .backgroundLabel(0, ColorTools.BLACK) // Specify background label (usually 0 or 255)
    .downsample(downsample)    // Choose server resolution; this should match the resolution at which tiles are exported
    .addLabel('Nuclei', 1)
    .multichannelOutput(false)  // If true, each label is a different channel (required for multiclass probability)
    .useDetections()
    .build()

// Create an exporter that requests corresponding tiles from the original & labeled 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(256)              // 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
    .includePartialTiles(false)
    .writeTiles(pathOutput)     // Write tiles to the specified directory

The problem is that tiles/masks exported are crossing the annotation borders. Is it possible to get the tiles restricted within the annotation borders?

Best regards,

Kai

Fixed the code formatting.

You are currently only exporting tiles with labels on them. If you want to make the area outside of the annotation black/zero/null, that is different:

It sounds like what you want is a downstream Python step.

Otherwise, you could delete all of the nuclei you do not want to export prior to the export step, which might be simpler.

You might not want the tile exporter at all. See the sample script for exporting annotations:

Rather than using

for (annotation in getAnnotationObjects()) {
   ...

to export all annotations individually as in that script, you could draw a rectangle around the area in which nuclei are annotated and assign a class to that rectangle, e.g. Region*.

Then you can loop through those classified rectangles with

def annotatedRegions = getAnnotationObjects().findAll {it.getPathClass() == getPathClass('Region*')}
for (annotation in annotatedRegions) {
   ...

and export only the contents of these.

I have used this and the binary nuclei mask gets exported but I need the RGB image data too:

import qupath.lib.images.servers.LabeledImageServer

def imageData = getCurrentImageData()
selectDetections()
classifySelected('Nuclei')
resetSelection()

// 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 = 2.0

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


// Create an ImageServer where the pixels are derived from annotations
def labelServer = new LabeledImageServer.Builder(imageData)
    .backgroundLabel(0, ColorTools.BLACK) // 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)
    .addLabel('Nuclei', 1)
    //.lineThickness(2)
    //.setBoundaryLabel('Boundary', 12)
    .multichannelOutput(false)  // If true, each label is a different channel (required for multiclass probability)
    .useDetections()
    .build()

// Export each region
def annotatedRegions = getAnnotationObjects().findAll {it.getPathClass() == getPathClass('Region*')}

int i = 0
for (annotation in annotatedRegions) {
    def region = RegionRequest.createInstance(
        labelServer.getPath(), downsample, annotation.getROI())
    i++
    def outputPath = buildFilePath(pathOutput, 'Region ' + i + '.png')
    writeImageRegion(labelServer, region, outputPath)
}


println 'Done!'

I edited your post to add script formatting with ```.

If you need to write the original image, I think adding something like this inside your loop should work

def region2 = RegionRequest.createInstance(
        imageData.getServer().getPath(), downsample, annotation.getROI())
def outputPath2 = buildFilePath(pathOutput, 'Region ' + i + '-original.png')
writeImageRegion(imageData.getServer(), region2, outputPath2)

(Technically, QuPath will probably accept it if you don’t create region2 and just use region in both cases. I haven’t tested any of this, so might be misremembering / have made a typo or two.)

writeImageRegion(imageData.getServer(), region, outputPath2) save the RGB data

Thank you!

Best regards,

Kai