Skip to content

Instantly share code, notes, and snippets.

What would you like to do?
Coding helper scripts for QuPath
Collections of scripts harvested mainly from Pete, but also picked up from the forums
Coding-List methods for object.groovy - Select an object, see what functions work for it.
Edit metadata.groovy - Add pixel values and maginification (and possibly other things). Shouldn't be necessary after m3
Efficiently collecting objects.groovy - More efficient scripting
Email - Python script to send you an email when a long analysis run is completed.
Email script ending.groovy - How to use the Python script above. Also good as a general reference for running things through the command
line from within QuPath. Could be generalized to any Python script.
Export Specific Fields.groovy - When you do not want allllll of those detection measurements.
Export annotations as single file.groovy - Good for collecting data
Export detections per annotation.groovy - Exports just detections within each annotation as separate files. For downstream analysis in
other programs like R.
Export file by image name.groovy - A method for naming export files when using "Run for all"
Export images automatically for project.groovy - Export a thumbnail image, with and without an overlay, using QuPath.
Export polygons.groovy - Exporting objects to a file (similar to transfer annotations between images script in Manipulating Objects)
Get current image name without extension.groovy
ImageJ plugin macros in a script.groovy - For another example, look in the Workflow enhancers section under Tissue detection.
Import Export annotations.groovy - Duplicate of Export Polygons and version in Manipulating Objects.
Import TMA information.groovy - Pete's script from forums for importing TMA maps
Memory monitoring with tile cache.groovy - Fun to use to check memory, and a good example of creating GUI elements in a script.
Merging training object files.groovy -
Print a list of detection object measurements.groovy - When you can't quite get the number of whitespaces right in your measurement
names, this is priceless.
Project Directory.groovy - Folder name stuff.
Set number of processors.groovy - Altering preferences for command line scripts.
Variable into ImageJ macro.groovy - Cycle through annotations and export TIFF files that include the annotation name in their
file name.
Variable into string commands.groovy - It took me a long time to remember this, so keeping it here as a reference. Good for more
complex and dynamic scripts.
script to load settings.cmd - Registry manipulation script for use outside QuPath, but interesting for setting up new users in facilities.
//Helpful list of functions when learning groovy scripting in QuPath
// Select an object... any object
def myObject = getSelectedROI()
// An alternative below; this would show what's available with Ctrl+space in the script editor
// def myObject = new qupath.lib.scripting.QPEx()
// Print the methods you can have access to
for (m in myObject.getClass().getMethods()){
//Note that this only applies while the current image is active. Reopening or switching images resets to the image's own metadata.
// Set the magnification & pixel size (be cautious!!!)
def metadata = getCurrentImageData().getServer().getOriginalMetadata()
metadata.magnification = 40
metadata.pixelWidthMicrons = 0.25
metadata.pixelHeightMicrons = 0.25
// If you want to trigger the 'Image' tab on the left to update, try setting a property to something different (and perhaps back again)
type = getCurrentImageData().getImageType()
//A processor efficient way of collecting all objects that do not meet a particular criteria
//In this case, only cells with Nucleus: Area or 30 or less are retained as cells_others
HashSet cells_others = getCellObjects()
cells_some = cells_others.findAll {it.getMeasurementList().getMeasurementValue("Nucleus: Area") > 30}
println("cells_others now contains the remaining cells with nuclear area greater than 30")
#This script was largely taken from Stackoverflow and is intended to be called from within
#QuPath in order to alert the "recipient" email address that a particular slide had finished processing
import smtplib
import sys
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
#free to use spare email account
gmailUser = ''
gmailPassword = 'bizwfscsdbsuildy'
recipient = ''
message = 'QuPath script has completed on '+sys.argv[1]
msg = MIMEMultipart()
msg['From'] = gmailUser
msg['To'] = recipient
msg['Subject'] = "QuPath Alert"
mailServer = smtplib.SMTP('', 587)
mailServer.login(gmailUser, gmailPassword)
mailServer.sendmail(gmailUser, recipient, msg.as_string())
//This script can be added to the end of very slow QuPath runs in order to alert the user by email.
//Email address is set in the python file
// Get the imageData & server
String path = getCurrentImageData().getServer().getPath()
//As long as python files can be run at the command line interface using "python", the following should work for the location of a python script
def cmdArray = ["python", "c:\\Python\\Test1\\Email", path]
* Script to combine results tables exported by QuPath.
* This is particularly intended to deal with the fact that results tables of annotations can produce results
* with different column names, numbers and orders - making them awkward to combine later manually.
* It prompts for a directory containing exported text files, and then writes a new file in the same directory.
* The name of the new file can be modified - see the first lines below.
* Note: This hasn't been tested very extensively - please check the results carefully, and report any problems so they
* can be fixed!
* @author Pete Bankhead
import qupath.lib.gui.QuPathGUI
// Some parameters you might want to change...
String ext = '.txt' // File extension to search for
String delimiter = '\t' // Use tab-delimiter (this is for the *input*, not the output)
String outputName = 'Combined_results.txt' // Name to use for output; use .csv if you really want comma separators
// Prompt for directory containing the results
def dirResults = QuPathGUI.getSharedDialogHelper().promptForDirectory()
//Use the following line instead to prevent the popup. Should work in V 0.1.2 and 0.1.3
//def dirResults = new File(buildFilePath(PROJECT_BASE_DIR, 'annotation results'))
if (dirResults == null)
def fileResults = new File(dirResults, outputName)
// Get a list of all the files to merge
def files = dirResults.listFiles({
File f -> f.isFile() &&
f.getName().toLowerCase().endsWith(ext) &&
f.getName() != outputName} as FileFilter)
if (files.size() <= 1) {
print 'At least two results files needed to merge!'
} else
print 'Will try to merge ' + files.size() + ' files'
// Represent final results as a 'list of maps'
def results = new ArrayList<Map<String, String>>()
// Store all column names that we see - not all files necessarily have all columns
def allColumns = new LinkedHashSet<String>()
allColumns.add('File name')
// Loop through the files
for (file in files) {
// Check if we have anything to read
def lines = file.readLines()
if (lines.size() <= 1) {
print 'No results found in ' + file
// Get the header columns
def iter = lines.iterator()
def columns =
// Create the entries
while (iter.hasNext()) {
def line =
if (line.isEmpty())
def map = ['File name': file.getName()]
def values = line.split(delimiter)
// Check if we have the expected number of columns
if (values.size() != columns.size()) {
print String.format('Number of entries (%d) does not match the number of columns (%d)!', columns.size(), values.size())
print('I will stop processing ' + file.getName())
// Store the results
for (int i = 0; i < columns.size(); i++)
map[columns[i]] = values[i]
// Create a new results file - using a comma delimiter if the extension is csv
if (outputName.toLowerCase().endsWith('.csv'))
delimiter = ','
int count = 0
fileResults.withPrintWriter {
def header = String.join(delimiter, allColumns)
// Add each of the results, with blank columns for missing values
for (result in results) {
for (column in allColumns) {
it.print(result.getOrDefault(column, ''))
// Success! Hopefully...
print 'Done! ' + count + ' result(s) written to ' + fileResults.getAbsolutePath()
//Make sure you replace the default cell (or whatever) detection script with your own.
import qupath.lib.gui.QuPathGUI
//Use either "project" OR "outputFolder" to determine where your detection files will go
def project = QuPathGUI.getInstance().getProject().getBaseDirectory()
project = project.toString()+"\\detectionMeasurements\\"
//Make sure the output folder exists
//def outputFolder = "D:\\Results\\"
hierarchy = getCurrentHierarchy()
def annotations = getAnnotationObjects()
int i = 1
clearDetections() //Just in case, so that the first detection list won't end up with extra stuff that was laying around
for (annotation in annotations)
selectObjects{p -> p == annotation}
runPlugin('qupath.imagej.detect.nuclei.WatershedCellDetection', '{"detectionImageBrightfield": "Hematoxylin OD", "requestedPixelSizeMicrons": 0.5, "backgroundRadiusMicrons": 8.0, "medianRadiusMicrons": 0.0, "sigmaMicrons": 1.5, "minAreaMicrons": 10.0, "maxAreaMicrons": 400.0, "threshold": 0.1, "maxBackground": 2.0, "watershedPostProcess": true, "excludeDAB": false, "cellExpansionMicrons": 5.0, "includeNuclei": true, "smoothBoundaries": true, "makeMeasurements": true}');
saveDetectionMeasurements(project+" "+i+"detections.txt",)
//Potentially replace all of the detections for viewing, after finishing the export
//runPlugin('qupath.imagej.detect.nuclei.WatershedCellDetection', '{"detectionImageBrightfield": "Hematoxylin OD", "requestedPixelSizeMicrons": 0.5, "backgroundRadiusMicrons": 8.0, "medianRadiusMicrons": 0.0, "sigmaMicrons": 1.5, "minAreaMicrons": 10.0, "maxAreaMicrons": 400.0, "threshold": 0.1, "maxBackground": 2.0, "watershedPostProcess": true, "excludeDAB": false, "cellExpansionMicrons": 5.0, "includeNuclei": true, "smoothBoundaries": true, "makeMeasurements": true}');
import qupath.lib.gui.QuPathGUI
def project = QuPathGUI.getInstance().getProject().getBaseDirectory()
outputFolder = "D:\\Results\\"
String imageLocation = getCurrentImageData().getServer().getPath()
fileNameWithNoExtension = imageLocation.split("[^A-Za-z0-9_ ]")[-2]
//also possible to use: def name = getCurrentImageData().getServer().getShortServerName()
//alternatively, change the outputFolder to "project" if you wish to save the files into your project folder, wherever it is.
saveAnnotationMeasurements(outputFolder+fileNameWithNoExtension+".txt", )
* Export a thumbnail image, with and without an overlay, using QuPath.
* For tissue microarrays, the scripting code written by the 'File -> Export TMA data'
* command is probably more appropriate.
* However, for all other kinds of images where batch export is needed this script can be used.
* @author Pete Bankhead
import qupath.lib.gui.ImageWriterTools
import qupath.lib.gui.QuPathGUI
import qupath.lib.gui.viewer.OverlayOptions
import qupath.lib.regions.RegionRequest
import qupath.lib.scripting.QPEx
// Aim for an output resolution of approx 0.5 um/pixel
double requestedPixelSize = 0.5
// Define format
def formatExtensions = [
'PNG': '.png',
'JPEG': '.jpg'
def format = 'PNG'
// Create the output directory, if required
def path = QPEx.buildFilePath(QPEx.PROJECT_BASE_DIR, "screenshots")
// Get the imageData & server
def imageData = QPEx.getCurrentImageData()
def server = imageData.getServer()
def viewer = QPEx.getCurrentViewer()
// Get the file name from the current server
def name = server.getShortServerName()
// We need to get the display settings (colors, line thicknesses, opacity etc.) from the current viewer, if available
def overlayOptions = QuPathGUI.getInstance() == null ? new OverlayOptions() : viewer.getOverlayOptions()
// Calculate downsample factor depending on the requested pixel size
double downsample = requestedPixelSize / server.getAveragedPixelSizeMicrons()
def request = RegionRequest.createInstance(imageData.getServerPath(), downsample, 0, 0, server.getWidth(), server.getHeight())
// Write output image, with and without overlay
def dir = new File(path)
def fileImage = new File(dir, name + formatExtensions[format])
def img = server.readBufferedImage(request)
img = viewer.getImageDisplay().applyTransforms(img, null)
javax.imageio.ImageIO.write(img, format, fileImage)
def fileImageWithOverlay = new File(dir, name + "-overlay" + formatExtensions[format])
ImageWriterTools.writeImageRegionWithOverlay(img, imageData, overlayOptions, request, fileImageWithOverlay.getAbsolutePath())
// Create an empty text file
def path = buildFilePath(PROJECT_BASE_DIR, 'polygons.txt')
def file = new File(path)
file.text = ''
// Loop through all annotations & write the points to the file
for (pathObject in getAnnotationObjects()) {
// Check for interrupt (Run -> Kill running script)
if (Thread.interrupted())
// Get the ROI
def roi = pathObject.getROI()
if (roi == null)
// Write the points; but beware areas, and also ellipses!
file << roi.getPolygonPoints() << System.lineSeparator()
print 'Done!'
// see
saveDetectionMeasurements('/path/to/exported/file.txt', "Measurement", "another measurement")
// or if path is a variable that is a String to your export location, an example would be
saveDetectionMeasurements(path, "Nucleus: Channel 1 mean", "Nucleus: Channel 2 mean")
//Which would limit the giant detection export file to only two columns
//this has changed in 0.2.0m2
imageData = getCurrentImageData();
if (imageData == null) {
print("No image open!");
def currentImageName = imageData.getServer().getShortServerName()
print("Current image name: " + currentImageName);
import qupath.imagej.plugins.ImageJMacroRunner
import qupath.lib.plugins.parameters.ParameterList
// Create a macro runner so we can check what the parameter list contains
def params = new ImageJMacroRunner(getQuPath()).getParameterList()
print ParameterList.getParameterListJSON(params, ' ')
// Change the value of a parameter, using the JSON to identify the key
params.getParameters().get('downsampleFactor').setValue(4.0 as double)
print ParameterList.getParameterListJSON(params, ' ')
// Get the macro text and other required variables
def macro = 'print("Overlay size: " + Overlay.size)'
def imageData = getCurrentImageData()
def annotations = getAnnotationObjects()
// Loop through the annotations and run the macro
for (annotation in annotations) {
ImageJMacroRunner.runMacro(params, imageData, null, annotation, macro)
print 'Done!'
//Two scripts to save annotations to a file, then restore them.
//Useful for redoing cell detection and keeping annotations for a classifier.
//Take from:!topic/qupath-users/UvkNb54fYco
def path = buildFilePath(PROJECT_BASE_DIR, 'annotations')
def annotations = getAnnotationObjects().collect {new qupath.lib.objects.PathAnnotationObject(it.getROI(), it.getPathClass())}
new File(path).withObjectOutputStream {
print 'Done!'
//****** SECOND SCRIPT***********//
def path = buildFilePath(PROJECT_BASE_DIR, 'annotations')
def annotations = null
new File(path).withObjectInputStream {
annotations = it.readObject()
print 'Added ' + annotations
// From
import static qupath.lib.gui.commands.TMAScoreImportCommand.*
def imageName = getProjectEntry().getImageName()
def path = buildFilePath(PROJECT_BASE_DIR, imageName + '.qpmap')
def file = new File(path)
def text = file.text
def hierarchy = getCurrentHierarchy()
handleImportGrid(hierarchy.getTMAGrid(), text)
* A basic GUI to help monitor memory usage in QuPath.
* This helps both to find & address out-of-memory troubles by
* 1. Showing how much memory is in use over time
* 2. Giving a button to clear the tile cache - which can be
* using up precious memory
* 3. Giving quick access to control the number of threads used
* for parallel processing
* You can run this command in the background while going about your
* normal analysis, and check in to see how it is doing.
* If you find QuPath crashing/freezing, look to see if the memory
* use is especially high.
* If it crashes when running memory-hungry commands like cell detection
* across a large image or TMA, try reducing the number of parallel threads.
* @author Pete Bankhead
import javafx.application.Platform
import javafx.beans.binding.Bindings
import javafx.beans.value.ChangeListener
import javafx.geometry.Insets
import javafx.geometry.Side
import javafx.scene.Scene
import javafx.scene.chart.AreaChart
import javafx.scene.chart.NumberAxis
import javafx.scene.chart.XYChart
import javafx.scene.control.Button
import javafx.scene.control.Label
import javafx.scene.control.TextField
import javafx.scene.layout.BorderPane
import javafx.scene.layout.GridPane
import javafx.stage.Stage
import qupath.lib.gui.QuPathGUI
import qupath.lib.gui.prefs.PathPrefs
// Create a timer to poll for memory status once per second
def timer = new Timer("QuPath memory monitor", true)
long sampleFrequency = 1000L
// Observable properties to store memory values
def maxMemory = new SimpleLongProperty()
def totalMemory = new SimpleLongProperty()
def usedMemory = new SimpleLongProperty()
def tileMemory = new SimpleLongProperty()
// Let's sometimes scale to MB, sometimes to GB
double scaleMB = 1.0/1024.0/1024.0
double scaleGB = scaleMB/1024.0
// Create a chart to show how memory use evolves over time
def xAxis = new NumberAxis()
xAxis.setLabel("Time (samples)")
def yAxis = new NumberAxis()
yAxis.setLabel("Memory (GB)")
def chart = new AreaChart(xAxis, yAxis)
def seriesTotal = new XYChart.Series()
def seriesUsed = new XYChart.Series()
def seriesTiles = new XYChart.Series()
yAxis.setUpperBound(Math.ceil(Runtime.getRuntime().maxMemory() * scaleGB))
// Bind the series names to the latest values, in MB
{-> String.format("Total memory (%.1f MB)", totalMemory.get() * scaleMB)}, totalMemory))
{-> String.format("Used memory (%.1f MB)", usedMemory.get() * scaleMB)}, usedMemory))
{-> String.format("Tile memory (%.1f MB)", tileMemory.get() * scaleMB)}, tileMemory))
chart.getData().addAll(seriesTotal, seriesUsed, seriesTiles)
// Add it button to make it possible to clear the tile cache
// This is a bit of a hack, since there is no clean way to do it yet
def btnClearCache = new Button("Clear tile cache")
btnClearCache.setOnAction {e ->
try {
print "Clearing cache..."
} catch (Exception e2) {
// Add a button to run the garbage collector
def btnGarbageCollector = new Button("Reclaim memory")
btnGarbageCollector.setOnAction {e ->
// Add a text field to adjust the number of parallel threads
// This is handy to scale back memory use when running things like cell detection
def runtime = Runtime.getRuntime()
def labThreads = new Label("Parallel threads")
def tfThreads = new TextField(Integer.toString(PathPrefs.getNumCommandThreads()))
PathPrefs.numCommandThreadsProperty().addListener({ v, o, n ->
def text = Integer.toString(n)
if (!text.trim().equals(tfThreads.getText().trim()))
} as ChangeListener)
tfThreads.textProperty().addListener({ v, o, n ->
try {
} catch (Exception e) {}
} as ChangeListener)
// Create a pane to show it all
def paneBottom = new GridPane()
int col = 0
int row = 0
paneBottom.add(new Label("Num processors: " + runtime.availableProcessors()), col, row++, 1, 1)
paneBottom.add(labThreads, col, row, 1, 1)
paneBottom.add(tfThreads, col+1, row++, 1, 1)
paneBottom.add(btnClearCache, col, row++, 2, 1)
paneBottom.add(btnGarbageCollector, col, row++, 2, 1)
paneBottom.add(new Label("Max tile memory: " + QuPathGUI.getInstance().getViewer().getImageRegionStore().cache.maxMemoryBytes/1024.0/1024.0),col, row++, 1, 1)
paneBottom.setPadding(new Insets(10))
def pane = new BorderPane(chart)
// Add a data point for the current memory usage
def snapshot = { ->
def time = seriesUsed.getData().size() + 1
seriesUsed.getData().add(new XYChart.Data<Number, Number>(time, usedMemory.get()*scaleGB))
seriesTotal.getData().add(new XYChart.Data<Number, Number>(time, totalMemory.get()*scaleGB))
seriesTiles.getData().add(new XYChart.Data<Number, Number>(time, tileMemory.get() * scaleGB))
// Switch to the application thread...
Platform.runLater {
// Create a timer that will snapshot the current memory usage & update the chart
timer.schedule({ ->
Platform.runLater {
usedMemory.set(runtime.totalMemory() - runtime.freeMemory())
}, 0L, sampleFrequency)
// Show the GUI
def stage = new Stage()
stage.setScene(new Scene(pane))
stage.setTitle("Memory monitor")
stage.setOnHiding {timer.cancel()}
// Paths to training files (here, both relative to the current project)
paths = [
buildFilePath(PROJECT_BASE_DIR, 'training', 'my_training.qptrain'),
buildFilePath(PROJECT_BASE_DIR, 'training', 'my_training2.qptrain'),
// Path to output training file
pathOutput = buildFilePath(PROJECT_BASE_DIR, 'training', 'merged.qptrain')
// Count mostly helps to ensure we're adding with unique keys
count = 0
// Loop through training files
def result = null
for (path in paths) {
// .qptrain files just have one object but class isn't public, so
// we take the first one that is deserialized
new File(path).withObjectInputStream {
saved = it.readObject()
// Add the training objects, appending an extra number which
// (probably, unless very unfortunate with image names?) means they are unique
map = new HashMap<>(saved.getMap())
if (result == null) {
result = saved
for (entry in map.entrySet())
result.put(entry.getKey() + '-' + count, entry.getValue())
// Check how big the map is & what it contains
print result.size()
print result.getMap().keySet().each { println it }
// Write out a new training file
new File(pathOutput).withObjectOutputStream {
//Also useful to see how to create a list of all measurements quickly
qupath.lib.classifiers.PathClassificationLabellingHelper.getAvailableFeatures(getDetectionObjects()).each { println(it) }
//Find the project directory from within your groovy script, useful if you have placed other files you want to access within the folder
import qupath.lib.gui.QuPathGUI
def path = QuPathGUI.getInstance().getProject().getBaseDirectory()
//Useful for setting up QuPath by checking whether Preferences exist in the registry during first run. This allows new
//users to start with access to all extensions and other settings rather than setting up each user individually.
//Replace the shortcut to QuPath in the Public folder with a shortcut to the following script.
@echo off
SET mykey="HKEY_CURRENT_USER\SOFTWARE\JavaSoft\Prefs\io.github.qupath"
reg query %mykey% >nul
if %errorlevel% equ 0 (
echo "key exists - do nothing"
) else (
echo "qupath key missing - importing default settings"
reg import C:\Tools\QuPath\qupath-imcf-default-settings.reg
start C:\Tools\QuPath\QuPath.exe
//For command line scripts, you may want to set the maximum number of threads in order to not hamper performance
//especially if there is a python script watching for file changes in the background
import qupath.lib.gui.prefs.PathPrefs
//Note you will want to edit the path within the macro, line 26
import qupath.imagej.plugins.ImageJMacroRunner
import qupath.lib.plugins.parameters.ParameterList
// Create a macro runner so we can check what the parameter list contains
def params = new ImageJMacroRunner(getQuPath()).getParameterList()
print ParameterList.getParameterListJSON(params, ' ')
// Change the value of a parameter, using the JSON to identify the key
params.getParameters().get('downsampleFactor').setValue(1.0 as double)
print ParameterList.getParameterListJSON(params, ' ')
def imageData = getCurrentImageData()
String name = it.getName()
macro = 'test="'+name+'"; saveAs("tif", "C:/OAD/" + getTitle()+"_"+test)'
ImageJMacroRunner.runMacro(params, imageData, null, it, macro)
print 'Done!'
//Thresholds for cell detection can be adjusted by using a variable for the threshold in your script, rather than a single value
//Script does not work as written! Only intended as an example
def GREEN_MEAN = average_Intensity+2*stdDev //or some other value based on other statistics from the image
//Scrolllllll =>>>>>>>>
runPlugin('qupath.imagej.detect.nuclei.WatershedCellDetection', '{"detectionImageFluorescence": 1, "requestedPixelSizeMicrons": 0.31, "backgroundRadiusMicrons": 0.0, "medianRadiusMicrons": 0.0, "sigmaMicrons": 1.5, "minAreaMicrons": 10.0, "maxAreaMicrons": 50.0, "threshold": '+GREEN_MEAN+', "watershedPostProcess": true, "cellExpansionMicrons": 0.5, "includeNuclei": true, "smoothBoundaries": true, "makeMeasurements": true}');

This comment has been minimized.

Copy link
Owner Author

commented Jan 25, 2018

More scripts to assist when coding, some mostly as a reference. Almost all are purely ripped from the forums or communication with Pete.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.