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
}
5 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!
4 Likes

Thank you so much! It works really well, except it’s not image spesific. The prediction scores are run on the original images, not the labelled images, exported with the tile exporter (that I export all of them to the same folder (i’ve removed the “name” part of this line:

def directoryPath = buildFilePath(PROJECT_BASE_DIR, 'tiles', name)

to be able run batch prection on all of them in one run from the deep learning part. Hence, they must be processed as one big batch of images from one folder, then processed and saved back to a single folder. Is there any way the script can only read the masks/annotations if the file name matches the file name of the currently open image in Qupath (even if all the other mask images from the project are in the same folder)?

I’m not sure I understand exactly the structure of your directory(/ies?)
Is this what you mean?:

project_dir
    |________ tiles
                |___ img1_tile1.tif
                |___ img1_tile1-labelled.tif
                |___ img1_tile2.tif
                |___ img1_tile2-labelled.tif
                |___ ...
                |___ img2_tile1.tif
                |___ img2_tile1-labelled.tif
                |___ img2_tile2.tif
                |___ img2_tile2-labelled.tif
                |___ ...

Rather than this (what I originally thought):

project_dir
    |________ tiles
                |___ img1
                      |______tile1.tif
                      |______tile1-labelled.tif
                      |______ ...
                |___ img2
                      |______tile1.tif
                      |______tile1-labelled.tif
                      |______ ...
  1. If it’s like the first option, the following lines should also be changed:
def directoryPath = 'path/to/project_dir'
File folder = new File(directoryPath);
File[] listOfFiles = folder.listFiles();
def name = GeneralTools.getNameWithoutExtension(getProjectEntry().getImageName())
listOfFiles.each { file ->
    def path = file.getPath()
    def imp = IJ.openImage(path)
    
    // Only process the labelled images for the currently-opened entry, not the originals
    if (!path.contains(name) && !path.endsWith("-labelled.tif"))
        return

  1. If it’s like the second option, I think the original script that I posted before should work. If not then I probably misunderstood something in your explanation :slight_smile:
1 Like

Yes, it’s like the first option, but really it’s without the “-labelled” tag, since it’s the exported image tiles and not the labels I’m processing with the pretrained network. The modified script still imports all the tiles from all the images unfortunately. The final file-structure after processing the image tiles with deep learning is:

project_dir\tiles\

                            |___ Score_img1_tile1.tif
                            |___ Score_img1_tile2.tif
                            |___ ...
                            |___ Score_img2_tile1.tif
                            |___ Score_img2_tile2.tif
                            |___ ...
                            |___ Score_img3_tile1.tif
                            |___ Score_img3_tile2.tif
                            |___ ...

Attached a screenshot. It’s only showing tiles from one image name, but there are tiles from all images in the same folder, in order to be able to batch process them with the deep learning part without having to accsess several different folders when making predictions.


The following logic

if (!path.contains(name))
    return;

should be triggered whenever a tile name (e.g. Score_15479_delta [x=2048,y=4096,w=4096,h=4096].tif) does not contain the entry’s image name (which I assume in this case would be Score_15479).
For instance, if your second entry’s image name is Score_15480, all the tiles starting with Score_15479 should not be included. The code seems to make sense to me, so I’m not sure where it fails? The only thing I can think of is if you have another image called Score_15480X where X is any other digit, in which case the tiles would also be (wrongly) processed.

You can maybe add something like this:

print "Now processing entry with image name: " + name

right after the line defining the name variable. Then add this:

print "Process " + path + "? " + path.contains(name)

right before the if statement (if (!path.contains(name) && ...). So you can see where the code fails.

1 Like

When I add that line this happens (Thinks it’s processing the right image “15480_delta”, but getting the annotations from “15479_delta” (I’m now trying with a directory containing ONLY the “15479_delta” scores, and no files with the name “15480_delta”).

When I add this, it says
“INFO: Process C:\QuPath\Qupath prosjekt …[hidden]…QuPath\scores021020\15479_delta [x=8192,y=8192,w=4096,h=4096].tif? false”, but still add the “15479_delta” annotations to the “15480_delta” image.

Does 15479_delta appear anywhere else in your tiles path?
Also it’s probably better to change && by ||:

if (!path.contains(name) || !path.endsWith("].tif"))

3 Likes

Haven’t tried it fully yet, but I think that “&&” replacement did the trick actually (only imports annotations for 15479_delta and not when I open other images now) - thank you so much!

2 Likes

@melvingelbard I just wanted to say thank you for this, as it has made importing CellPose results very clean. I need to maybe work on some overlap issues, but I can now tile and save, process in CellPose, which saves the results as png files in the same folder, then cycle through the png results in the folder to create objects, and voila, brightfield segmentation that is not completely terrible!


And in QuPath, I can color code them by circularity.

Also thanks to @petebankhead for the thread here : Transferring segmentation predictions from Custom Masks to QuPath which will no doubt be useful in the future for IF related processing.

1 Like