Importing Binary Masks with Annotation Classes

Hi All - I am using the the wonderful code (made by @melvingelbard, here Does anyone have an importing masks script corresponding to the working export tiles script - #3 by melvingelbard) below to import my binary masks (generated by a ML algorithm) but it does not appear to give the annotations a class. Is there a way to modify the code in order to set the class of the imported object given a part of the file name?

My file name currently looks like “79-L2.vsi - 40x Cartilage [x=12288,y=4096,w=4096,h=4096]-labelled.tif” with the “Cartilage” being the class of the annotation. But i can rename the file in anyway to make the string parsing easier.

THanks

// 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_masks/masks’ // 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 = 8 // 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
}

Hi Richard,

Would this work?

// 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 parsing = parseFilename(GeneralTools.getNameWithoutExtension(path))
    def classification = parsing[0]
    def x = parsing[1] as double
    def y = parsing[2] as double

    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, -x/downsample, -y/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, getPathClass(classification))
    }
    addObjects(pathObjects)
}

resolveHierarchy()

String[] parseFilename(String filename) {
    def p = Pattern.compile("40x (.*) \\[x=(.+?),y=(.+?),")
    parsed = []
    Matcher m = p.matcher(filename)
    if (!m.find())
        throw new IOException("Filename does not contain classification and/or tile position")
    
    parsed << m.group(1)
    parsed << (m.group(2) as double)
    parsed << (m.group(3) as double)

    return parsed
}

This snippet makes the assumption that your classification is between 40x and [ in your filename.

1 Like

Melvin - I get the below error. Thanks for your help

INFO: Now processing: …
\masks\79-L2.vsi - 40x Artifact [x=12288,y=12288,w=4096,h=4096]->labelled.tif
ERROR: MissingMethodException at line 20: No signature of method: java.lang.String.negative() is >applicable for argument types: () values:
Possible solutions: notify(), next(), normalize()

ERROR: org.codehaus.groovy.runtime.ScriptBytecodeAdapter.unwrap(ScriptBytecodeAdapter.java:70)

Yes I edited my previous post, you can try again!
Unfortunately I can’t test it right now, so it might throw you some error still.

1 Like

Thanks for your help - This project has asked me to learn python, some linux and now Java/groovy is just a bit to much at the moment so all help is very appreciated.

I am now getting this error

ERROR: NumberFormatException at line 20: For input string: “[”

ERROR: java.base/jdk.internal.math.FloatingDecimal.readJavaFormatString(Unknown Source)

Ah yes, I corrected the script again. The parsing was returning a big String, hence the indexing giving you [ instead of the actual double.

1 Like

Amazing, It works now.

Do you know if there is a way to merge annotations that are the same class?

Using the workflow manager i could only find the mergeSelectedAnnotations()

Can first automatically select annotations of the same class and then run this command?

Yes, right-click the classification in the list under the ‘Annotations’ tab - you should see a Select objects by classification option.

That will log the scripting line in the workflow, which is

selectObjectsByClassification("Name of classification");
2 Likes