Hi all
I know this has come up before in a few places, but I was creating a quick importer to pair with the TileExporter function when it became apparent that the exporter was exporting a lot more tiles than I expected (full scripts below).
new TileExporter(imageData)
.downsample(downsample) // Define export resolution
.imageExtension('.png') // Define file extension for original pixels (often .tif, .jpg, '.png' or '.ome.tif')
.tileSize(224) // 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
//.includePartialTiles(false)
.overlap(0) // Define overlap, in pixel units at the export resolution
.writeTiles(pathOutput) // Write tiles to the specified directory
removeObject(it, true)
print 'Done!'
For an object like the above, I would have expected the tiles to roughly follow the outline of the annotation rather than the bounding box. That is not what happens, as when I re-create tile detections based off of the exported images, I get the following.

I think the last I recall, the recommendation was to use Python and a labeled image to verify the tiles overlapped significantly enough with the annotation, but I was expecting to only have to deal with X% overlap (the group is fine with any overlap, but there needs to be some overlap), not the 0% overlap in many of the tiles.
I wanted to make sure I had not missed anything new that could streamline the process or remove tiles with zero annotation overlap.
I also noticed that even with the annotatedTilesOnly
set to true
, I was getting tiles for my tumor, stroma, and unclassified annotations. I was able to work around that fairly easily, though, by first removing them, and then re-adding such annotations after exporting the tiles.
Script to export tiles to sub-folders based on the class of the annotation.
import qupath.lib.images.servers.LabeledImageServer
def imageData = getCurrentImageData()
// Define output path (relative to project)
def name = GeneralTools.getNameWithoutExtension(imageData.getServer().getMetadata().getName())
// Define output resolution
double requestedPixelSize = 10.0
// Convert to downsample
double downsample = 1.0 //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('Other', 1) // Choose output labels (the order matters!)
.addLabel('Islet', 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
annotations = getAnnotationObjects()
print annotations
removeObjects(annotations, true)
annotations.each{
if(it.getPathClass() == null){return}
addObject(it)
className = it.getPathClass().toString()
pathOutput = buildFilePath(PROJECT_BASE_DIR, 'tiles', name, className)
mkdirs(pathOutput)
new TileExporter(imageData)
.downsample(downsample) // Define export resolution
.imageExtension('.png') // Define file extension for original pixels (often .tif, .jpg, '.png' or '.ome.tif')
.tileSize(224) // 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
//.includePartialTiles(false)
.overlap(0) // Define overlap, in pixel units at the export resolution
.writeTiles(pathOutput) // Write tiles to the specified directory
removeObject(it, true)
print 'Done!'
}
addObjects(annotations)
fireHierarchyUpdate()
Script to create Detection tiles based on the exported images. Classification taken from folder name
def imageData = getCurrentImageData()
// Define output path (relative to project)
def name = GeneralTools.getNameWithoutExtension(imageData.getServer().getMetadata().getName())
basePath = buildFilePath(PROJECT_BASE_DIR, 'tiles', name)
baseDir = new File(basePath);
files = baseDir.listFiles();
classes = []
newTiles = []
files.each{ classes<<it.getName().toString()}
print classes
classes.each{c->
tempPath= buildFilePath(PROJECT_BASE_DIR, 'tiles', name, c)
dir = new File(tempPath);
files = dir.listFiles();
files.each{f->
toParse = f.getName()
subString = (toParse =~ /\[(.+)\]/).findAll()*.first()
//extract the X and Y coordinates and tile size
s = (subString =~ /\d+/).findAll()
x = Double.parseDouble(s[0])
y = Double.parseDouble(s[1])
size = Double.parseDouble(s[2])
def roi = new RectangleROI(x,y,size,size, ImagePlane.getDefaultPlane())
newTiles << PathObjects.createDetectionObject(roi, getPathClass(c))
}
}
//pathOutput = buildFilePath(PROJECT_BASE_DIR, 'tiles', name, className)
/*
double x = Double.parseDouble(xy[0])
double y = Double.parseDouble(xy[1])
def roi = new RectangleROI(x-size/2,y-size/2,size,size, ImagePlane.getDefaultPlane())
newTiles << PathObjects.createDetectionObject(roi)
*/
addObjects(newTiles)
resolveHierarchy()
import qupath.lib.regions.ImagePlane
import qupath.lib.roi.RectangleROI;