Skip to content

Instantly share code, notes, and snippets.

@Svidro
Last active August 4, 2023 20:43
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Svidro/259c8baa9037579828d17e7d65703346 to your computer and use it in GitHub Desktop.
Save Svidro/259c8baa9037579828d17e7d65703346 to your computer and use it in GitHub Desktop.
Export Images
Image exporting in QuPath
Note, this is likely a mess, and I won't be able to provide much support since I don't do this much,
but requests are common and I have trouble tracking things down each time.
Added a script from the forum for importing binary masks as annotations.
// @petebankhead 0.2.3+
// https://forum.image.sc/t/qupath-scripting-1-using-clupath-to-save-smoothed-image-regions/49525/4
import qupath.lib.images.writers.ome.OMEPyramidWriter
def tilesize = 512
def outputDownsample = 1
def pyramidscaling = 2
def compression = OMEPyramidWriter.CompressionType.J2K_LOSSY //J2K //UNCOMPRESSED //LZW
def imageData = getCurrentImageData()
def op = ImageOps.buildImageDataOp()
.appendOps(ImageOps.Filters.gaussianBlur(10.0))
def serverSmooth = ImageOps.buildServer(imageData, op, imageData.getServer().getPixelCalibration())
print serverSmooth.getPreferredDownsamples()
def pathOutput = buildFilePath(PROJECT_BASE_DIR, "smoothed.ome.tif")
new OMEPyramidWriter.Builder(serverSmooth)
.compression(compression)
.parallelize()
.tileSize(tilesize)
.channelsInterleaved() // Usually faster
.scaledDownsampling(outputDownsample, pyramidscaling)
.build()
.writePyramid(pathOutput)
//https://forum.image.sc/t/mask-script-doesnt-work-in-qupath-m4/33259
//@thomasleb0n
import qupath.lib.regions.*
import ij.*
import java.awt.Color
import java.awt.image.BufferedImage
import javax.imageio.ImageIO
// Read RGB image & show in ImageJ (won't work for multichannel!)
double downsample = 1.0
def server = getCurrentImageData().getServer()
int w = (server.getWidth() / downsample) as int
int h = (server.getHeight() / downsample) as int
def img = new BufferedImage(w, h, BufferedImage.TYPE_BYTE_GRAY)
def g2d = img.createGraphics()
g2d.scale(1.0/downsample, 1.0/downsample)
g2d.setColor(Color.WHITE)
for (detection in getDetectionObjects()) {
roi = detection.getROI()
def shape = roi.getShape()
g2d.fill(shape)
}
g2d.dispose()
new ImagePlus("Mask", img).show()
def name = getProjectEntry().getImageName() //+ '.tiff'
def path = buildFilePath(PROJECT_BASE_DIR, 'mask')
mkdirs(path)
def fileoutput = new File( path, name+ '-mask.png')
ImageIO.write(img, 'PNG', fileoutput)
println('Results exporting...')
//M6... may not work in most versions, but the expansions idea should be similar
//https://forum.image.sc/t/enlarging-images-around-relevant-masks-during-export/34220/8?u=research_associate
//@Pietro_Cicalese
/**
* Script to export binary masks corresponding to all annotations of an image,
* optionally along with extracted image regions.
*
* Note: Pay attention to the 'downsample' value to control the export resolution!
*
* @author Pete Bankhead
*/
import qupath.lib.images.servers.ImageServer
import qupath.lib.objects.PathObject
import javax.imageio.ImageIO
import java.awt.Color
import java.awt.image.BufferedImage
// Get the main QuPath data structures
def imageData = getCurrentImageData()
def hierarchy = imageData.getHierarchy()
def server = imageData.getServer()
// Request all objects from the hierarchy & filter only the annotations
def annotations = hierarchy.getAnnotationObjects()
// Define downsample value for export resolution & output directory, creating directory if necessary
// The pad variable will add padding to both the image and mask output images (# of pixels)
def downsample = 1.0
pad = 75
def pathOutput = buildFilePath(QPEx.PROJECT_BASE_DIR, 'masks')
mkdirs(pathOutput)
// Define image export type; valid values are JPG, PNG or null (if no image region should be exported with the mask)
// Note: masks will always be exported as PNG
def imageExportType = 'PNG'
// Export each annotation
annotations.each {
saveImageAndMask(pathOutput, server, it, downsample, imageExportType)
}
print 'Done!'
/**
* Save extracted image region & mask corresponding to an object ROI.
*
* @param pathOutput Directory in which to store the output
* @param server ImageServer for the relevant image
* @param pathObject The object to export
* @param downsample Downsample value for the export of both image region & mask
* @param imageExportType Type of image (original pixels, not mask!) to export ('JPG', 'PNG' or null)
* @return
*/
def saveImageAndMask(String pathOutput, ImageServer server, PathObject pathObject, double downsample, String imageExportType) {
// Extract ROI & classification name
def roi = pathObject.getROI()
def pathClass = pathObject.getPathClass()
def classificationName = pathClass == null ? 'None' : pathClass.toString()
if (roi == null) {
print 'Warning! No ROI for object ' + pathObject + ' - cannot export corresponding region & mask'
return
}
// Create a region from the ROI
def region = RegionRequest.createInstance(server.getPath(), downsample, (int)roi.getBoundsX()-pad, (int)roi.getBoundsY()-pad, (int)roi.getBoundsWidth() + pad*2, (int)roi.getBoundsHeight() + pad*2, roi.getZ(), roi.getT())
// Create a name
String name = String.format('%s_%s_(%.2f,%d,%d,%d,%d)',
server.getMetadata().getName(),
classificationName,
region.getDownsample(),
region.getX(),
region.getY(),
region.getWidth(),
region.getHeight()
)
// Request the BufferedImage
def img = server.readBufferedImage(region)
// Create a mask using Java2D functionality
// (This involves applying a transform to a graphics object, so that none needs to be applied to the ROI coordinates)
def shape = RoiTools.getShape(roi)
def imgMask = new BufferedImage(img.getWidth(), img.getHeight(), BufferedImage.TYPE_BYTE_GRAY)
def g2d = imgMask.createGraphics()
g2d.setColor(Color.WHITE)
g2d.scale(1.0/downsample, 1.0/downsample)
g2d.translate(-region.getX(), -region.getY())
g2d.fill(shape)
g2d.dispose()
// Create filename & export
if (imageExportType != null) {
def fileImage = new File(pathOutput, name + '.' + imageExportType.toLowerCase())
ImageIO.write(img, imageExportType, fileImage)
}
// Export the mask
def fileMask = new File(pathOutput, name + '-mask.png')
ImageIO.write(imgMask, 'PNG', fileMask)
}
//From this post: https://forum.image.sc/t/macro-image-displaying-annotations/30214/14?u=research_associate
double downsample = 100.0
def server = getCurrentServer()
def request = RegionRequest.createInstance(server, downsample)
def img = server.readBufferedImage(request)
float thickness = 2
def g2d = img.createGraphics()
g2d.setColor(java.awt.Color.BLACK)
g2d.scale(1.0/downsample, 1.0/downsample)
g2d.setStroke(new java.awt.BasicStroke((float)(thickness * downsample)))
getAnnotationObjects().each { g2d.draw(it.getROI().getShape()) }
g2d.dispose()
def path = buildFilePath(PROJECT_BASE_DIR, 'something.png')
writeImage(img, path)
//0.2.2
//https://forum.image.sc/t/script-for-send-region-to-imagej/39554/18
import ij.IJ
import qupath.imagej.gui.IJExtension
double downsample = 4.0
def server = getCurrentServer()
def request = RegionRequest.createInstance(server, downsample)
boolean setROI = false
def imp = IJExtension.extractROIWithOverlay(
getCurrentServer(),
null,
getCurrentHierarchy(),
request,
setROI,
getCurrentViewer().getOverlayOptions()
).getImage()
IJ.save(imp, '/path/to/export.tif')
//0.2.3
//https://forum.image.sc/t/exporting-rendered-svg-images-in-batch-mode/52045/2
import qupath.lib.extension.svg.SvgTools.SvgBuilder
def imageData = getCurrentImageData()
def options = getCurrentViewer().getOverlayOptions()
def doc = new SvgBuilder()
.imageData(imageData)
.options(options)
.downsample(1) // Increase if needed
.createDocument()
def name = GeneralTools.getNameWithoutExtension(getProjectEntry().getImageName())
def path = buildFilePath(PROJECT_BASE_DIR, name + '.svg')
new File(path).text = doc
//Tested for 0.2.3
// Write the full image, displaying objects according to how they are currently shown in the viewer
double downsample = 10.0
def server = getCurrentServer()
def name = getProjectEntry().getImageName()
def viewer = getCurrentViewer()
getCurrentHierarchy().getTMAGrid().getTMACoreList().each{
mkdirs(buildFilePath(PROJECT_BASE_DIR,'export'))
def path = buildFilePath(PROJECT_BASE_DIR,'export', name+" "+ it.getName()+".tif")
def request = RegionRequest.createInstance(server.getPath(), downsample, it.getROI())
writeRenderedImageRegion(viewer,request, path)
}
print "Done"
//0.2.0
//https://forum.image.sc/t/script-for-send-region-to-imagej/39554/3?u=research_associate
//exporting to imageJ with overlay
import qupath.imagej.gui.IJExtension
double downsample = 2.0
def server = getCurrentServer()
def selectedObject = getSelectedObject()
def request = RegionRequest.createInstance(server.getPath(), downsample, selectedObject.getROI())
boolean setROI = true
def imp = IJExtension.extractROIWithOverlay(
getCurrentServer(),
selectedObject,
getCurrentHierarchy(),
request,
setROI,
getCurrentViewer().getOverlayOptions()
).getImage()
imp.show()
// 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
}
/**
* https://forum.image.sc/t/qupath-0-2-0m11-updated-import-masks-as-annotations-script/37427
* Script to import binary masks & create annotations, adding them to the current object hierarchy.
*
* It is assumed that each mask is stored in a PNG file in a project subdirectory called 'masks'.
* Each file name should be of the form:
* [Short original image name]_[Classification name]_([downsample],[x],[y],[width],[height])-mask.png
*
* Note: It's assumed that the classification is a simple name without underscores, i.e. not a 'derived' classification
* (so 'Tumor' is ok, but 'Tumor: Positive' is not)
*
* The x, y, width & height values should be in terms of coordinates for the full-resolution image.
*
* By default, the image name stored in the mask filename has to match that of the current image - but this check can be turned off.
*
* @author Pete Bankhead
*/
import ij.measure.Calibration
import ij.plugin.filter.ThresholdToSelection
import ij.process.ByteProcessor
import ij.process.ImageProcessor
import qupath.imagej.tools.IJTools
import qupath.lib.objects.PathAnnotationObject
import qupath.lib.objects.classes.PathClassFactory
import static qupath.lib.gui.scripting.QPEx.*
import javax.imageio.ImageIO
import qupath.lib.regions.ImagePlane
import qupath.lib.roi.ROIs
import qupath.lib.objects.PathObjects
// Get the main QuPath data structures
def imageData = QPEx.getCurrentImageData()
def hierarchy = imageData.getHierarchy()
def server = getCurrentServer()
// Only parse files that contain the specified text; set to '' if all files should be included
// (This is used to avoid adding masks intended for a different image)
def includeText = server.getMetadata().getName()
// Get a list of image files, stopping early if none can be found
def pathOutput = QPEx.buildFilePath(QPEx.PROJECT_BASE_DIR, 'masks')
def dirOutput = new File(pathOutput)
if (!dirOutput.isDirectory()) {
print dirOutput + ' is not a valid directory!'
return
}
def files = dirOutput.listFiles({f -> f.isFile() && f.getName().contains(includeText) && f.getName().endsWith('-mask.png') } as FileFilter) as List
if (files.isEmpty()) {
print 'No mask files found in ' + dirOutput
return
}
// Create annotations for all the files
def annotations = []
files.each {
try {
annotations << parseAnnotation(it)
} catch (Exception e) {
print 'Unable to parse annotation from ' + it.getName() + ': ' + e.getLocalizedMessage()
}
}
// Add annotations to image
hierarchy.addPathObjects(annotations)
/**
* Create a new annotation from a binary image, parsing the classification & region from the file name.
*
* Note: this code doesn't bother with error checking or handling potential issues with formatting/blank images.
* If something is not quite right, it is quite likely to throw an exception.
*
* @param file File containing the PNG image mask. The image name must be formatted as above.
* @return The PathAnnotationObject created based on the mask & file name contents.
*/
def parseAnnotation(File file) {
// Read the image
def img = ImageIO.read(file)
// Split the file name into parts: [Image name, Classification, Region]
def parts = file.getName().replace('-mask.png', '').split('_')
// Discard all but the last 2 parts - it's possible that the original name contained underscores,
// so better to work from the end of the list and not the start
def classificationString = parts[-2]
// Extract region, and trim off parentheses (admittedly in a lazy way...)
def regionString = parts[-1].replace('(', '').replace(')', '')
// Create a classification, if necessary
def pathClass = null
if (classificationString != 'None')
pathClass = PathClassFactory.getPathClass(classificationString)
// Parse the x, y coordinates of the region - width & height not really needed
// (but could potentially be used to estimate the downsample value, if we didn't already have it)
def regionParts = regionString.split(',')
double downsample = regionParts[0] as double
int x = regionParts[1] as int
int y = regionParts[2] as int
// To create the ROI, travel into ImageJ
def bp = new ByteProcessor(img)
bp.setThreshold(127.5, Double.MAX_VALUE, ImageProcessor.NO_LUT_UPDATE)
def roiIJ = new ThresholdToSelection().convert(bp)
int z = 0
int t = 0
def plane = ImagePlane.getPlane(z, t)
// Convert ImageJ ROI to a QuPath ROI
// This assumes we have a single 2D image (no z-stack, time series)
// Currently, we need to create an ImageJ Calibration object to store the origin
// (this might be simplified in a later version)
def cal = new Calibration()
cal.xOrigin = -x/downsample
cal.yOrigin = -y/downsample
def roi = IJTools.convertToROI(roiIJ, cal, downsample,plane)
// Create & return the object
return new PathAnnotationObject(roi, pathClass)
}
//0.2.0
//https://forum.image.sc/t/qupath-script-with-pixel-classifier/45597/10?u=research_associate
def imageData = getCurrentImageData()
def classifier = loadPixelClassifier('Classifier')
def predictionServer = PixelClassifierTools.createPixelClassificationServer(imageData, classifier)
def path = buildFilePath(PROJECT_BASE_DIR, 'prediction.tif')
def downsample = predictionServer.getDownsampleForResolution(0)
def request = RegionRequest.createInstance(predictionServer, downsample)
writeImageRegion(predictionServer, request, path)
//https://forum.image.sc/t/qupath-measurement-maps-image-export-script/50373/3?u=research_associate
import qupath.lib.gui.tools.MeasurementMapper
import qupath.lib.gui.images.servers.RenderedImageServer
// Define the color map name
String colorMapName = 'Magma'
// Load a color mapper
def colorMapper = MeasurementMapper.loadColorMappers().find {it.name == colorMapName}
println colorMapper
// Define measurement & display range
def name = "Nucleus: Circularity" // Set to null to reset
double minValue = 0.0
double maxValue = 1.0
// Request current viewer & objects
def viewer = getCurrentViewer()
def options = viewer.getOverlayOptions()
def detections = getDetectionObjects()
// Update the display
if (name) {
print String.format('Setting measurement map: %s (%.2f - %.2f)', name, minValue, maxValue)
def mapper = new MeasurementMapper(colorMapper, name, detections)
mapper.setDisplayMinValue(minValue)
mapper.setDisplayMaxValue(maxValue)
options.setMeasurementMapper(mapper)
} else {
print 'Resetting measurement map'
options.setMeasurementMapper(null)
}
// Now export the rendered image
import qupath.imagej.tools.IJTools
import qupath.lib.gui.images.servers.RenderedImageServer
import qupath.lib.gui.viewer.overlays.HierarchyOverlay
import qupath.lib.regions.RegionRequest
// It is important to define the downsample!
// This is required to determine annotation line thicknesses
double downsample = 10
// Add the output file path here
String path = buildFilePath(PROJECT_BASE_DIR, 'rendered')
mkdirs(path)
// Request the current viewer for settings, and current image (which may be used in batch processing)
def imageData = getCurrentImageData()
// Create a rendered server that includes a hierarchy overlay using the current display settings
def server = new RenderedImageServer.Builder(imageData)
.downsamples(downsample)
.layers(new HierarchyOverlay(null, options, imageData))
.build()
// Write or display the rendered image
int count = 0
for (annotation in getAnnotationObjects()) {
count++
def imageName = getProjectEntry().getImageName() + count + '.png'
def path2 = buildFilePath(path, imageName)
def region = RegionRequest.createInstance(server.getPath(), downsample, annotation.getROI())
writeImageRegion(server, region, path2)
}
//https://forum.image.sc/t/imagej-rois-and-rotated-image-server-issue-on-qupath/47019/3
import java.awt.geom.AffineTransform
def server = getCurrentServer()
def transform = AffineTransform.getRotateInstance(Math.PI)
transform.translate(-server.getWidth(), -server.getHeight())
def annotations = getAnnotationObjects()
def transformedAnnotations = annotations.collect {PathObjectTools.transformObject(it, transform, true)}
removeObjects(annotations, true)
addObjects(transformedAnnotations)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment