Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save petebankhead/0847f339fe5e415fd1402a3b0bfabcda to your computer and use it in GitHub Desktop.
Save petebankhead/0847f339fe5e415fd1402a3b0bfabcda to your computer and use it in GitHub Desktop.
Script to export pixels & annotations for tissue microarrays
/**
* Script to export pixels & annotations for TMA images.
*
* The downsample value and coordinates are encoded in each image file name.
*
* The annotations are exported as 8-bit labelled images.
* These labels depend upon annotation classifications; a text file giving the key is written for reference.
*
* The labelled image can also optionally use indexed colors to depict the colors of the
* original classifications within QuPath for easier visualization & comparison.
*
* @author Pete Bankhead
*/
import qupath.lib.common.ColorTools
import qupath.lib.objects.classes.PathClass
import qupath.lib.regions.RegionRequest
import qupath.lib.roi.PathROIToolsAwt
import qupath.lib.scripting.QPEx
import javax.imageio.ImageIO
import java.awt.Color
import java.awt.image.BufferedImage
import java.awt.image.DataBufferByte
import java.awt.image.IndexColorModel
// Requested pixel size - used to define output resolution
// Set <= 0 to use the full resolution (whatever that may be)
double requestedPixelSizeMicrons = 1.0
// Maximum size of an image tile when exporting
int maxTileSize = 1000
// Export the original pixels (assumed ot be RGB) for each tile
boolean exportOriginalPixels = true
// Export a labelled image for each tile containing annotations
boolean exportAnnotationLabelledImage = true
// Don't export any cores marked as 'missing'
boolean skipMissingCores = true
// NOTE: The following parameters only matter if exportAnnotationLabelledImage == true
// Ignore annotations that don't have a classification set
boolean skipUnclassifiedAnnotations = true
// Skip tiles without annotations (only applies to label exports - all image tiles will be written)
boolean skipUnannotatedTiles = true
// Create an 8-bit indexed image
// This is very useful for display/previewing - although need to be careful when importing into other software,
// which can prefer to replaces labels with the RGB colors they refer to
boolean createIndexedImageLabels = true
// NOTE: The following parameter only matters if exportOriginalPixels == true
// Define the format for the export image
def imageFormat = 'JPG'
// Output directory for storing the tiles
def pathOutput = QPEx.buildFilePath(QPEx.PROJECT_BASE_DIR, 'exported_tiles')
QPEx.mkdirs(pathOutput)
//------------------------------------------------------------------------------------
// Get the main QuPath data structures
def imageData = QPEx.getCurrentImageData()
def hierarchy = imageData.getHierarchy()
def server = imageData.getServer()
// Check we have a TMA grid
if (hierarchy.getTMAGrid() == null) {
print 'No TMA grid found!'
return
}
// Get the annotations that have ROIs & are have classifications (if required)
def annotations = hierarchy.getFlattenedObjectList(null).findAll {
it.isAnnotation() && it.hasROI() && (!skipUnclassifiedAnnotations || it.getPathClass() != null) }
// Get all the represented classifications
def pathClasses = annotations.collect({it.getPathClass()}) as Set
// We can't handle more than 255 classes (because of 8-bit representation)
if (pathClasses.size() > 255) {
print 'Sorry! Cannot handle > 255 classications - number here is ' + pathClasses.size()
return
}
// For each classification, create an integer label >= 1 & cache a color for drawing it
// Also, create a LUT for visualizing more easily
def labelKey = new StringBuilder()
def pathClassColors = [:]
int n = pathClasses.size() + 1
def r = ([(byte)0] * n) as byte[]
def g = r.clone()
def b = r.clone()
def a = r.clone()
pathClasses.eachWithIndex{ PathClass entry, int i ->
// Record integer label for key
int label = i+1
String name = entry == null ? 'None' : entry.toString()
labelKey << name << '\t' << label << System.lineSeparator()
// Store Color based on label - needed for painting with Graphics2D
pathClassColors.put(entry, new Color(label, label, label))
// Update RGB values for IndexColorModel
// Use gray as the default color indicating no classification
int rgb = entry == null ? ColorTools.makeRGB(127, 127, 127) : entry.getColor()
r[label] = ColorTools.red(rgb)
g[label] = ColorTools.green(rgb)
b[label] = ColorTools.blue(rgb)
a[label] = 255
}
// Check if we've anything to do
if (!exportAnnotationLabelledImage && !exportOriginalPixels) {
print 'Nothing to export!'
return
}
// Calculate the downsample value
double downsample = 1
if (requestedPixelSizeMicrons > 0)
downsample = requestedPixelSizeMicrons / server.getAveragedPixelSizeMicrons()
// Calculate the tile spacing in full resolution pixels
int spacing = (int)(maxTileSize * downsample)
// Get the cores
def cores = hierarchy.getTMAGrid().getTMACoreList()
if (skipMissingCores)
cores = cores.findAll {!it.isMissing()}
// Write the label 'key'
println labelKey
def keyName = String.format('%s_(downsample=%.3f,tiles=%d)-key.txt', server.getShortServerName(), downsample, maxTileSize)
def fileLabels = new File(pathOutput, keyName)
fileLabels.text = labelKey.toString()
// Handle the cores in parallel
cores.parallelStream().forEach { core ->
def request = RegionRequest.createInstance(server.getPath(), downsample, core.getROI())
// Create a suitable base image name
String name = String.format('%s_%s_(%.2f,%d,%d,%d,%d)',
server.getShortServerName(),
core.getName(),
request.getDownsample(),
request.getX(),
request.getY(),
request.getWidth(),
request.getHeight()
)
// Export the raw image pixels if necessary
// If we do this, store the width & height - to make sure we have an exact match
int width = -1
int height = -1
if (exportOriginalPixels) {
def img = server.readBufferedImage(request)
width = img.getWidth()
height = img.getHeight()
def fileOutput = new File(pathOutput, name + '.' + imageFormat.toLowerCase())
ImageIO.write(img, imageFormat, fileOutput)
}
// Export the labelled tiles if necessary
if (exportAnnotationLabelledImage) {
// Calculate dimensions if we don't know them already
if (width < 0 || height < 0) {
width = Math.round(request.getWidth() / downsample)
height = Math.round(request.getHeight() / downsample)
}
// Fill the annotations with the appropriate label
def imgMask = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY)
def g2d = imgMask.createGraphics()
g2d.setClip(0, 0, width, height)
g2d.scale(1.0/downsample, 1.0/downsample)
g2d.translate(-request.getX(), -request.getY())
int count = 0
for (annotation in annotations) {
def roi = annotation.getROI()
if (!request.intersects(roi.getBoundsX(), roi.getBoundsY(), roi.getBoundsWidth(), roi.getBoundsHeight()))
continue
def shape = PathROIToolsAwt.getShape(roi)
def color = pathClassColors.get(annotation.getPathClass())
g2d.setColor(color)
g2d.fill(shape)
count++
}
g2d.dispose()
if (count > 0 || !skipUnannotatedTiles) {
// Extract the bytes from the image
def buf = imgMask.getRaster().getDataBuffer() as DataBufferByte
def bytes = buf.getData()
// Check if we actually have any non-zero pixels, if necessary -
// we might not if the annotation bounding box intersected the region, but the annotation itself does not
if (skipUnannotatedTiles && !bytes.any { it != (byte)0 })
return
// If we want an indexed color image, create one using the data buffer & LUT
if (createIndexedImageLabels) {
def colorModel = new IndexColorModel(8, n, r, g, b, a)
def imgMaskColor = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED, colorModel)
System.arraycopy(bytes, 0, imgMaskColor.getRaster().getDataBuffer().getData(), 0, width*height)
imgMask = imgMaskColor
}
// Write the mask
def fileOutput = new File(pathOutput, name + '-labels.png')
ImageIO.write(imgMask, 'PNG', fileOutput)
}
}
}
print 'Done!'
@dan1elR
Copy link

dan1elR commented Aug 17, 2020

I'm trying to export mask tiles without alpha layer and with the background/non labeled regions in white as there's already one black mask label and transparency yields bad results in further processing.
Which further modifications have to be done to achieve that apart from erasing , a in line 201?

Thanks for your help!

@petebankhead
Copy link
Author

This script is really old and not maintained... I'd recommend https://qupath.readthedocs.io/en/latest/docs/advanced/exporting_annotations.html for a more up-to-date way to export with lots of customization options, and https://forum.image.sc/tag/qupath is best for questions (there are lots of annotation export-related discussions there).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment