Does anyone have an importing masks script corresponding to the working export tiles script

Pete’s script for exporting tiles with names and coordinates works well in 0.2.2 when substituting the TileExporter line (see working script below). I’m wondering if anyone has a working script that similarly can import binary masks.
Thats it, a script that imports masks and creates annotations in the corresponding location in qupath after e.g. deep learning prosessing of the exported tiles with corresponding names (e.g. “…-mask.png”) exported with the above script (file name example: “Image15202 [x=0,y=0,w=11129,h=11129].tif”). Much back and forth in a previous discussion about this (Importing binary masks in QuPath), and seems like Pete’s working on it, but also seems like some of you have been successful in doing this already. I would be very greatful if you posted a working script for Qupath 0.2.2 (or for Qupath 0.1.2 with a corresponding export script).

Working export script (need corresponding import script for masks):

/**
 * Script to export image tiles (can be customized in various ways).
 */

import qupath.lib.images.writers.TileExporter

// Get the current image (supports 'Run for project')
def imageData = getCurrentImageData()

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

// Define output resolution in calibrated units (e.g. µm if available)
double requestedPixelSize = 0.6

// Convert output resolution to a downsample factor
double pixelSize = imageData.getServer().getPixelCalibration().getAveragedPixelSize()
double downsample = requestedPixelSize / pixelSize

// Create an exporter that requests corresponding tiles from the original & labelled 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(508)            // Define size of each tile, in pixels
    .annotatedTilesOnly(true) // 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

print 'Done!'

If someone has succeeded in doing this, I would really appreciate if someone could share a script like this.
That is, importing masks into Qupath converting them in to annotations from the same format as the export script above (e.g. “IMAGE_NAME [x=0,y=0,w=1484,h=1484].tif”).

If this script could be run in batch mode, this would allow exporting tiles from qupath, processing them with a deep learning algorithm (like DeepMIB), then reimporting them, essentially making deep learning/CNN available for Qupath without much programming skills.

Hi @HenrikSP,

Here is a script that imports labelled images back into QuPath as annotations.
You have to provide the downsample and the directory where you exported all your tiles (if processing this as a batch, you could do something like: def directoryPath = buildFilePath(PROJECT_BASE_DIR, 'tiles', ...)).

Also, it’s important to bear in mind that your tile export script accounted for some overlap between tiles. This will be reflected in the annotations when importing them back into QuPath. A simple solution for this could be to select the annotations manually (or not?) and merge them.

Finally, the following script doesn’t assign classes to the imported annotations.

It’s probably best to try on one test image first, before processing all of your images at once!

Script:

// Script written for QuPath v0.2.3
// Minimal working script to import labelled images 
// (from the TileExporter) back into QuPath as annotations.

import qupath.lib.objects.PathObjects
import qupath.lib.regions.ImagePlane
import static qupath.lib.gui.scripting.QPEx.*
import ij.IJ
import ij.process.ColorProcessor
import qupath.imagej.processing.RoiLabeling
import qupath.imagej.tools.IJTools
import java.util.regex.Matcher
import java.util.regex.Pattern


def directoryPath = 'path/to/your/directory' // TO CHANGE
File folder = new File(directoryPath);
File[] listOfFiles = folder.listFiles();

listOfFiles.each { file ->
    def path = file.getPath()
    def imp = IJ.openImage(path)
    
    // Only process the labelled images, not the originals
    if (!path.endsWith("-labelled.tif"))
        return
        
    print "Now processing: " + path
    
    // Parse filename to understand where the tile was located
    def parsedXY = parseFilename(GeneralTools.getNameWithoutExtension(path))
   
    double downsample = 1 // TO CHANGE (if needed)
    ImagePlane plane = ImagePlane.getDefaultPlane()
    
    
    // Convert labels to ImageJ ROIs
    def ip = imp.getProcessor()
    if (ip instanceof ColorProcessor) {
        throw new IllegalArgumentException("RGB images are not supported!")
    }
    
    int n = imp.getStatistics().max as int
    if (n == 0) {
        print 'No objects found!'
        return
    }
    def roisIJ = RoiLabeling.labelsToConnectedROIs(ip, n)
    
    
    
    // Convert ImageJ ROIs to QuPath ROIs
    def rois = roisIJ.collect {
        if (it == null)
            return
        return IJTools.convertToROI(it, -parsedXY[0]/downsample, -parsedXY[1]/downsample, downsample, plane);
    }
    
    // Remove all null values from list
    rois = rois.findAll{null != it}
    
    // Convert QuPath ROIs to objects
    def pathObjects = rois.collect {
        return PathObjects.createAnnotationObject(it)
    }
    addObjects(pathObjects)
}

resolveHierarchy()



int[] parseFilename(String filename) {
    def p = Pattern.compile("\\[x=(.+?),y=(.+?),")
    parsedXY = []
    Matcher m = p.matcher(filename)
    if (!m.find())
        throw new IOException("Filename does not contain tile position")
            
    parsedXY << (m.group(1) as double)
    parsedXY << (m.group(2) as double)
    
    return parsedXY
}
3 Likes

Excellent - thank you so much! Tried it now on one tile and works great. Is it image specific - that is, can you use it to import only annotations for the specified image-filename, thus keeping all the mask-image-files in the same folder and run the script in batch mode for a full project?

Great, no worries!
The script imports annotations for the currently opened image, which means that it is (nearly) ‘batch-ready’. The only thing to change to make it work on all your images in your project at once is the following line:

def directoryPath = 'path/to/your/directory' // TO CHANGE

by this (assuming that you originally exported your tiles with the script in your first post):

def name = GeneralTools.getNameWithoutExtension(imageData.getServer().getMetadata().getName())
def directoryPath = buildFilePath(PROJECT_BASE_DIR, 'tiles', name)

And make sure that the downsample is right (assuming it is the same for each image in your project):

double downsample = 1 // Will be the same for all images in your project!
3 Likes