Skip to content

Instantly share code, notes, and snippets.

@Svidro
Last active July 18, 2023 16:18
Show Gist options
  • Star 19 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save Svidro/5e4c29630e8d2ef36988184987d1028f to your computer and use it in GitHub Desktop.
Save Svidro/5e4c29630e8d2ef36988184987d1028f to your computer and use it in GitHub Desktop.
Coding helper scripts for QuPath
Collections of scripts harvested mainly from Pete, but also picked up from the forums
If you are starting with 0.2.0m4+ START HERE: https://petebankhead.github.io/qupath/2019/08/21/scripting-in-v020.html
TOC
Access objects in other project images.groovy - in later versions of 0.2.0M#, access the hierarchies of other images
Access other project images or data.groovy
Access project metadata.groovy - interact with user defined, per image metadata. Useful for sorting projects.
Aperio XML Files - import.groovy
CLI server selection.groovy - select a server other than QuPath's default when opening a file
Coding-List methods for object.groovy - Select an object, see what functions work for it.
Color deconvolution vectors - Print.groovy
Create Histogram through ImageJ.groovy - shows a histogram representing the pixels in selected Annotations - color deconvolved BF
Create project from CLI.groovy
CSV file import.groovy - Script to handle CSV files with coordinates
CSV file write out data.groovy - How to write a csv file out of QuPath
DESCRIBE.groovy - Use describe() to find other functions related to the object you are interested in. Run it last and in small scripts.
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 alert.py - 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.
Error log within functions.groovy - print functions to not normally work within a function, this provides an alternative
Export data about the image.groovy - summary data about the size and type of image
Export Specific Fields.groovy - When you do not want allllll of those detection measurements.
Export Summary of Detection measurements.groovy - Mean+Std Dev for a list of 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
GSON and JSON script examples.groovy - converting between GSON and JSON and some links to examples of code
ImageJ plugin macros in a script.groovy - For another example, look in the Workflow enhancers section under Tissue detection.
ImageOps examples.groovy - using ImageOps in 0.2.0M12 to adjust StarDist function
Import Export annotations.groovy - Duplicate of Export Polygons and version in Manipulating Objects.
Import Export objects.groovy - import and export full objects. backup or transfer function.
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 - https://github.com/qupath/qupath/issues/256
Metadata - collect from image.groovy - get all accessible metadata from the image - may or may not work depending on format.
Print list of source images for composite.groovy - Find out which images were used for a training image montage1
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.
Transfer ROIs to ImageJ.groovy - Send annotations and detections to the ROI manager in ImageJ, possibly with a downsample
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.
//cycle through all images in the project and check the number of annotations. Add it to their metadata in the Project tab
//https://forum.image.sc/t/select-slides-without-annotations/35019/2
for (entry in getProject().getImageList()) {
def hierarchy = entry.readHierarchy()
String nAnnotations = hierarchy.getAnnotationObjects().size()
entry.putMetadataValue('Num annotations', nAnnotations)
}
getQuPath().refreshProject()
//Accessing project image data
//https://forum.image.sc/t/read-an-image-that-is-not-open/34824/3
def path = '/path/to/the/image.ext'
// Read a small image with ImageIO
// ('normal' Java, nothing QuPath-specific)
def img = javax.imageio.ImageIO.read(new File(path))
println img
// Read a small image with ImageJ as an ImagePlus
// ('normal' ImageJ, nothing QuPath-specific)
def imp = ij.IJ.openImage(path)
println imp
// Read any image with QuPath as an ImageServer
def server = qupath.lib.images.servers.ImageServerProvider.buildServer(path, BufferedImage)
println server
// Read another image within a project as an ImageData (from which you can get the server)
def name = 'Image name as shown in Project pane'
def project = getProject()
def entry = project.getImageList().find { it.getImageName() == name }
def imageData = entry.readImageData()
println imageData
//Access object data in other images in a project
//https://forum.image.sc/t/m9-multiplex-classifier-script-updates-cell-summary-measurements-visualization/34663/4
def project = QPEx.getProject()
def fileNames = project.getImageList()
detClasses=[]
fileNames.each{
def objs = it.readHierarchy().getDetectionObjects()
def imageClasses=objs.collect{it.getPathClass()}
detClasses.addAll(imageClasses)
}
//Also potentially written as:
getProject().getImageList().each{
def objs = it.readHierarchy().getDetectionObjects()
classes = objs.collect{it?.getPathClass()?.toString()}
classNames.addAll(classes)
}
// https://forum.image.sc/t/access-project-metadata-by-script/38758/2?u=research_associate
// 0.2.0
def key = "Your key" // Your metadata key
def value = getProjectEntryMetadataValue(key)
print key + ": " + value
// From https://forum.image.sc/t/labelled-annotation-import/37787/11?u=research_associate
// Originally https://gist.github.com/DanaCase/9cfc23912fee48e437af03f97763d78e
import qupath.lib.scripting.QP
import qupath.lib.geom.Point2
import qupath.lib.roi.PolygonROI
import qupath.lib.objects.PathAnnotationObject
import qupath.lib.images.servers.ImageServer
//Aperio Image Scope displays images in a different orientation
def rotated = false
def server = QP.getCurrentImageData().getServer()
def h = server.getHeight()
def w = server.getWidth()
// need to add annotations to hierarchy so qupath sees them
def hierarchy = QP.getCurrentHierarchy()
//Prompt user for exported aperio image scope annotation file
def path = server.getURIs().getAt(0).getPath(); // HERE
path = path.substring(0, path.lastIndexOf(".")) + ".xml" // HERE
def file = new File(path)
def text = file.getText()
def list = new XmlSlurper().parseText(text)
list.Annotation.each {
// Get the class from your XML
def annotationClass = getPathClass(it.@Name.toString())
it.Regions.Region.each { region ->
def tmp_points_list = []
region.Vertices.Vertex.each{ vertex ->
if (rotated) {
X = vertex.@Y.toDouble()
Y = h - vertex.@X.toDouble()
}
else {
X = vertex.@X.toDouble()
Y = vertex.@Y.toDouble()
}
tmp_points_list.add(new Point2(X, Y))
}
def roi = new PolygonROI(tmp_points_list)
def annotation = new PathAnnotationObject(roi)
// Set the class here below
annotation.setPathClass(annotationClass)
hierarchy.addPathObject(annotation, false)
}
}
// Update hierarchy to see changes in QuPath's hierarchy
fireHierarchyUpdate()
print "Done!"
//0.2.0
//https://forum.image.sc/t/specifying-the-imageserver-image-provider-server-type-eg-bio-formats-or-openslide-in-a-qupath-script/45884/2
def server = getCurrentServer()
def uri = server.getURIs()[0]
//def server2 = new qupath.lib.images.servers.bioformats.BioFormatsServerBuilder().buildServer(uri)
def server2 = new qupath.lib.images.servers.openslide.OpenslideServerBuilder().buildServer(uri)
print "Original server: ${server}"
print "New server: ${server2}"
// Create an ImageData if you need one
def imageData = new ImageData(server2)
//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()){
println(m)
}
//0.2.0 Sara McCardle
import static qupath.lib.gui.scripting.QPEx.*
totalString= []
project.getImageList().each {
def ImgString = new StringBuilder()
ImgString.append(it.getImageName())
ImgString.append(',')
def stains = it.readImageData().getColorDeconvolutionStains().getStains(false)
stains.each {
ImgString.append(it.getName()).append(',').append(it.getRed()).append(',').append(it.getGreen()).append(',').append(it.getBlue()).append(',')
}
totalString << ImgString
}
print(totalString)
def path = buildFilePath(PROJECT_BASE_DIR, 'vectors')
mkdirs(path)
String outputName = 'Stain_Vectors.csv'
path2 = buildFilePath(path, outputName)
outFile= new File(path2)
outFile.withPrintWriter {
totalString.each{ img->
it.println(img)
}
}
//0.2.0
// https://forum.image.sc/t/get-pixel-by-pixel-intensities-in-qupath/46626/10?u=research_associate
import qupath.lib.images.servers.TransformedServerBuilder
import qupath.imagej.gui.IJExtension
IJExtension.getImageJInstance()
// Create an ImageServer that applies color deconvolution to the current image, using the current stains
def imageData = getCurrentImageData()
def server = new TransformedServerBuilder(imageData.getServer())
.deconvolveStains(imageData.getColorDeconvolutionStains(), 1, 2)
.build()
for (annotation in getAnnotationObjects()) {
def region = RegionRequest.createInstance(server.getPath(), 1.0, annotation.getROI())
def pathImage = IJTools.convertToImagePlus(server, region)
def imp = pathImage.getImage()
def roiIJ = IJTools.convertToIJRoi(annotation.getROI(), pathImage)
imp.setRoi(roiIJ)
imp.setPosition(1)
new ij.gui.HistogramWindow(imp)
imp.setPosition(2)
new ij.gui.HistogramWindow(imp)
// imp.show() // show image to check if you want
}
//0.2.0
//https://forum.image.sc/t/creating-project-from-command-line/45608/2
import java.awt.image.BufferedImage
import qupath.lib.images.servers.ImageServerProvider
// Paths
def directory = new File("Path/to/your/empty/directory")
def imagePath = "D:/QuPath/data/Capture.PNG"
// Create project
def project = Projects.createProject(directory , BufferedImage.class)
// Get serverBuilder
def support = ImageServerProvider.getPreferredUriImageSupport(BufferedImage.class, imagePath, "")
def builder = support.builders.get(0)
// Make sure we don't have null
if (builder == null) {
print "Image not supported"
return
}
// Add the image as entry to the project
project.addImage(builder)
// Changes should now be reflected in the project directory
project.syncChanges()
//https://forum.image.sc/t/reading-csv-table-in-qupath/32772/3
//From @melvingelbard
import java.io.BufferedReader;
import java.io.FileReader;
import qupath.lib.objects.PathAnnotationObject;
import qupath.lib.roi.RectangleROI;
def imageData = getCurrentImageData();
// Get location of csv
def file = getQuPath().getDialogHelper().promptForFile(null)
// Create BufferedReader
def csvReader = new BufferedReader(new FileReader(file));
int sizePixels = 1000
row = csvReader.readLine() // first row (header)
// Loop through all the rows of the CSV file.
while ((row = csvReader.readLine()) != null) {
def rowContent = row.split(",")
double cx = rowContent[1] as double;
double cy = rowContent[2] as double;
// Create annotation
def roi = new RectangleROI(cx-sizePixels/2, cy-sizePixels/2, sizePixels, sizePixels);
def annotation = new PathAnnotationObject(roi, PathClassFactory.getPathClass("Region"));
imageData.getHierarchy().addPathObject(annotation, true);
}
//Reference sample for writing out some array as a CSV file with a header line and row headers (classes)
//Based off of https://forum.image.sc/t/write-polygon-coordinates-to-text-or-csv-file/44811/2
def header = " , a, b, c, d"
classes = ["a","b","c","d"]
output = "g:/test.csv"
File csvFile = new File(output)
csvFile.createNewFile()
m = [[0,1,2,3],[0,0,0,0],[1,1,1,1],[5,2,4,6]]
new File(output).withWriter { fw ->
fw.writeLine(header)
m.eachWithIndex{l,x->
String line = classes[x]+","+l.join(",")
fw.writeLine(line)
}
}
//0.2.0
// Use describe to access information about a particular object.
// https://forum.image.sc/t/qupath-export-individual-annotation-on-entire-size-image-just-like-the-image-size-of-labeled-image/42966/11
imageData = getCurrentImageData()
def labelServer = new LabeledImageServer.Builder(imageData)
println describe(labelServer)
// https://github.com/qupath/qupath/issues/223
//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()
setImageType(null)
setImageType(type)
//THIS HAS CHANGED IN MORE RECENT VERSIONS. I do not know exactly when, but at least as of M7, adjusting the metada should be done as follows
import qupath.lib.images.servers.ImageServerMetadata
def imageData = getCurrentImageData()
def server = imageData.getServer()
def oldMetadata = server.getMetadata()
def newMetadata = new ImageServerMetadata.Builder(oldMetadata)
.magnification(10.0)
.pixelSizeMicrons(1.25, 1.25)
.build()
imageData.updateServerMetadata(newMetadata)
//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}
cells_others.removeAll(cells_some)
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 = 'qupathemailer@gmail.com'
gmailPassword = 'bizwfscsdbsuildy'
recipient = 'YourEmailHere@domain.com'
message = 'QuPath script has completed on '+sys.argv[1]
msg = MIMEMultipart()
msg['From'] = gmailUser
msg['To'] = recipient
msg['Subject'] = "QuPath Alert"
msg.attach(MIMEText(message))
mailServer = smtplib.SMTP('smtp.gmail.com', 587)
mailServer.ehlo()
mailServer.starttls()
mailServer.ehlo()
mailServer.login(gmailUser, gmailPassword)
mailServer.sendmail(gmailUser, recipient, msg.as_string())
mailServer.close()
//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 alert.py", path]
cmdArray.execute()
//Not a script, but an example from komzy on how to get error information out of functions.
// https://forum.image.sc/t/button-setonaction-does-not-print-anything-when-scripting/25443/5?u=research_associate
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Logger logger = LoggerFactory.getLogger(QuPathGUI.class);
button.setOnAction {
print("Test Print # 2: Button Clicked")
logger.info("Test Print # 3: call logger.info on button click")
}
//https://groups.google.com/forum/#!topic/qupath-users/GEI99RECuAc
/**
* 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)
return
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!'
return
} 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
continue
}
// Get the header columns
def iter = lines.iterator()
def columns = iter.next().split(delimiter)
allColumns.addAll(columns)
// Create the entries
while (iter.hasNext()) {
def line = iter.next()
if (line.isEmpty())
continue
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())
break
}
// Store the results
for (int i = 0; i < columns.size(); i++)
map[columns[i]] = values[i]
results.add(map)
}
}
// 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)
it.println(header)
// Add each of the results, with blank columns for missing values
for (result in results) {
for (column in allColumns) {
it.print(result.getOrDefault(column, ''))
it.print(delimiter)
}
it.println()
count++
}
}
// Success! Hopefully...
print 'Done! ' + count + ' result(s) written to ' + fileResults.getAbsolutePath()
// https://forum.image.sc/t/metadata-batch-export/42668/2
def metadata = []
def delim = '\t'
for (entry in getProject().getImageList()) {
def imageData = entry.readImageData()
def server = imageData.getServer()
metadata << String.join(delim,
[entry.getImageName(),
server.getMetadata().getMagnification(),
server.getPixelCalibration().getAveragedPixelSize(),
server.getWidth(),
server.getHeight()] as String[]
)
}
def output = String.join(System.lineSeparator, metadata)
print output
// To write to a file, use something like this
// new File('/path/to/file.txt').text = output
//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
mkdirs(project)
//def outputFolder = "D:\\Results\\"
//mkdirs(outputFolder)
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)
{
hierarchy.getSelectionModel().clearSelection();
selectObjects{p -> p == annotation}
//**********************************************************************
//REPLACE THIS PART WITH YOUR OWN DETECTION CREATION SCRIPT
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",)
i+=1
clearDetections()
}
//Potentially replace all of the detections for viewing, after finishing the export
//selectAnnotations()
//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()
println(project)
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")
QPEx.mkdirs(path)
// 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())
break
// Get the ROI
def roi = pathObject.getROI()
if (roi == null)
continue
// Write the points; but beware areas, and also ellipses!
file << roi.getPolygonPoints() << System.lineSeparator()
}
print 'Done!'
// see https://github.com/qupath/qupath/issues/25
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
// @melvingelbard - https://forum.image.sc/t/script-for-averaging-detection-measurement-data/48805/9?u=research_associate
// Source: https://forum.image.sc/t/script-for-averaging-detection-measurement-data/48805/9?u=research_associate
columnName = ["CK: Cell: Mean", "CD68: Cell: Mean"] // Change this to your column of interest
imageName = getProjectEntry().getImageName()
//Save to the project folder
var path = buildFilePath(PROJECT_BASE_DIR, "Detection Summary measurements.csv")
//print "Mean: " + avg
//print "Std.Dev: " + Math.sqrt(sd/stats.length)
var pathObjects = getDetectionObjects()
var separator = ","
File file = new File(path)
header = "Image name,"
var exists = file.exists()
file.withWriterAppend { fw ->
if (!exists){
columnName.each{
header =header+[it+" Average", it+" Standard deviation"].join(separator)+separator
}
fw.writeLine(header)
}
dataLine = getProjectEntry().getImageName()+","
columnName.each{
stats = Histogram.getMeasurementValues(pathObjects, it);
var avg = 0
for (int i=0; i < stats.length; i++) {
avg += stats[i]
}
avg /= stats.length
var sd = 0
for (int i=0; i < stats.length; i++) {
sd = sd + Math.pow(stats[i] - avg, 2);
}
sd = Math.sqrt(sd/stats.length)
dataLine = dataLine + [avg, sd].join(separator)+separator
}
fw.append(dataLine)
fw.append(System.getProperty("line.separator"))
}
print "Done!"
import qupath.lib.analysis.stats.Histogram
//this has changed in 0.2.0m2
imageData = getCurrentImageData();
if (imageData == null) {
print("No image open!");
return
}
def currentImageName = imageData.getServer().getShortServerName()
print("Current image name: " + currentImageName);
//https://forum.image.sc/t/labelling-negative-space-in-annotations/32653/3
//https://petebankhead.github.io/qupath/2019/08/21/scripting-in-v020.html#serialization--json
// Create JSON
def annotations = getAnnotationObjects()
def gson = GsonTools.getInstance(true)
def json = gson.toJson(annotations)
///////////////////////////
// Create JSON string
def annotations = getAnnotationObjects()
def gson = GsonTools.getInstance(true)
def json = gson.toJson(annotations)
// Print the string
println json
// Read the annotations
def type = new com.google.gson.reflect.TypeToken<List<qupath.lib.objects.PathObject>>() {}.getType()
def deserializedAnnotations = gson.fromJson(json, type)
// Set the annotations to have a different name (so we can identify them) & add to the current image
deserializedAnnotations.eachWithIndex {annotation, i -> annotation.setName('New annotation ' + (i+1))}
addObjects(deserializedAnnotations)
//https://groups.google.com/forum/#!topic/qupath-users/C3GRBF614N8
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 = new File("myMacroPath/MacroName.ijm").text
//Line above from https://github.com/qupath/qupath/issues/176
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!'
// https://forum.image.sc/t/stardist-extension/37696/6?u=research_associate
// SCRIPTS NOT INTENDED TO BE RUN AS IS. READ THE FORUM POST
def stardist = StarDist2D.builder(pathModel)
.threshold(0.5) // Prediction threshold
.preprocess(
ImageOps.Core.subtract(100),
ImageOps.Core.divide(100)
)
// .normalizePercentiles(1, 99) // Percentile normalization
.pixelSize(0.5) // Resolution for detection
.includeProbability(true) // Include prediction probability as measurement
.build()
////////////////////
// Get current image - assumed to have color deconvolution stains set
def imageData = getCurrentImageData()
def stains = imageData.getColorDeconvolutionStains()
// Set everything up with single-channel fluorescence model
def pathModel = '/path/to/dsb2018_heavy_augment'
def stardist = StarDist2D.builder(pathModel)
.preprocess(
ImageOps.Channels.deconvolve(stains),
ImageOps.Channels.extract(0),
ImageOps.Filters.median(2),
ImageOps.Core.divide(1.5)
) // Optional preprocessing (can chain multiple ops)
.pixelSize(0.5)
.includeProbability(true)
.threshold(0.5)
.build()
//Two scripts to save annotations to a file, then restore them.
//Useful for redoing cell detection and keeping annotations for a classifier.
//Take from: https://groups.google.com/forum/#!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 {
it.writeObject(annotations)
}
print 'Done!'
//****** SECOND SCRIPT***********//
def path = buildFilePath(PROJECT_BASE_DIR, 'annotations')
def annotations = null
new File(path).withObjectInputStream {
annotations = it.readObject()
}
addObjects(annotations)
fireHierarchyUpdate()
print 'Added ' + annotations
// A pair of scripts to be run separately.
//Export objects without measurements.
path = buildFilePath(PROJECT_BASE_DIR, 'objects')
mkdirs(path)
image = getProjectEntry().getImageName()
path = buildFilePath(PROJECT_BASE_DIR, 'objects',image)
def objects = getAllObjects().findAll{it.getLevel() !=0}
//adjust this to getAnnotationObjects() or getDetectionObjects() or getCellObjects() as desired.
new File(path).withObjectOutputStream {
it.writeObject(objects)
}
print 'Done!'
////////////////////////////////////////////////////////////////////////////
//Second script to import objects from the 'objects' folder within a project
////////////////////////////////////////////////////////////////////////////
guiscript = true
image = getProjectEntry().getImageName()
path = buildFilePath(PROJECT_BASE_DIR, 'objects',image)
def objects = null
new File(path).withObjectInputStream {
objects = it.readObject()
}
addObjects(objects)
resolveHierarchy()
fireHierarchyUpdate()
print 'Added ' + objects.size()+' objects'
for (annotation in getAnnotationObjects())
annotation.setLocked(true)
// From https://forum.image.sc/t/qupath-importing-tma-data-within-script/24188
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)
fireHierarchyUpdate()
/**
* 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.property.SimpleLongProperty
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.setAutoRanging(false)
yAxis.setLowerBound(0.0)
yAxis.setTickUnit(1.0)
yAxis.setUpperBound(Math.ceil(Runtime.getRuntime().maxMemory() * scaleGB))
xAxis.setAutoRanging(true)
// Bind the series names to the latest values, in MB
seriesTotal.nameProperty().bind(Bindings.createStringBinding(
{-> String.format("Total memory (%.1f MB)", totalMemory.get() * scaleMB)}, totalMemory))
seriesUsed.nameProperty().bind(Bindings.createStringBinding(
{-> String.format("Used memory (%.1f MB)", usedMemory.get() * scaleMB)}, usedMemory))
seriesTiles.nameProperty().bind(Bindings.createStringBinding(
{-> String.format("Tile memory (%.1f MB)", tileMemory.get() * scaleMB)}, tileMemory))
chart.getData().addAll(seriesTotal, seriesUsed, seriesTiles)
chart.setLegendVisible(true)
chart.setLegendSide(Side.TOP)
chart.setAnimated(false)
chart.setCreateSymbols(false)
// 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..."
QuPathGUI.getInstance().getViewer().getImageRegionStore().cache.clear()
System.gc()
} catch (Exception e2) {
e2.printStackTrace()
}
}
btnClearCache.setMaxWidth(Double.MAX_VALUE)
// Add a button to run the garbage collector
def btnGarbageCollector = new Button("Reclaim memory")
btnGarbageCollector.setOnAction {e ->
System.gc()
}
btnGarbageCollector.setMaxWidth(Double.MAX_VALUE)
// 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()))
tfThreads.setText(text)
} as ChangeListener)
tfThreads.setPrefColumnCount(4)
tfThreads.textProperty().addListener({ v, o, n ->
try {
PathPrefs.setNumCommandThreads(Integer.parseInt(n.trim()))
} catch (Exception e) {}
} as ChangeListener)
labThreads.setLabelFor(tfThreads)
// 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))
paneBottom.setVgap(5)
def pane = new BorderPane(chart)
pane.setRight(paneBottom)
// 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 {
totalMemory.set(runtime.totalMemory())
maxMemory.set(runtime.maxMemory())
usedMemory.set(runtime.totalMemory() - runtime.freeMemory())
tileMemory.set(QuPathGUI.getInstance().getViewer().getImageRegionStore().cache.memoryBytes)
snapshot()
}
}, 0L, sampleFrequency)
// Show the GUI
def stage = new Stage()
stage.initOwner(QuPathGUI.getInstance().getStage())
stage.setScene(new Scene(pane))
stage.setTitle("Memory monitor")
stage.show()
stage.setOnHiding {timer.cancel()}
}
//https://github.com/qupath/qupath/issues/256
// 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
result.clear()
}
for (entry in map.entrySet())
result.put(entry.getKey() + '-' + count, entry.getValue())
count++
}
// 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 {
it.writeObject(result)
}
// https://forum.image.sc/t/qupath-scripting-1-using-clupath-to-save-smoothed-image-regions/49525/9?u=research_associate
getCurrentServer().dumpMetadata()
//Also useful to see how to create a list of all measurements quickly
qupath.lib.classifiers.PathClassificationLabellingHelper.getAvailableFeatures(getDetectionObjects()).each { println(it) }
//After 0.2.0m5
PathClassifierTools.getAvailableFeatures(getDetectionObjects())
// 0.2.0
//https://forum.image.sc/t/training-image-localization-size/46509/3?u=research_associate
//print list of source regions in a montage
def manager = getCurrentServer().getManager()
def regions = manager.getRegions()
for (region in regions) {
def uris = manager.getServer(region, 1).getURIs().collect {it.toString()}
def uriString = String.join("\t", uris)
print region.toString() + "\t" + uriString
}
////////////////////////////////
//Or, print the name of the region that contains a specific object.
def roi = getSelectedROI()
def manager = getCurrentServer().getManager()
def regions = manager.getRegions()
if (roi)
regions = regions.findAll {it.intersects(ImageRegion.createInstance(roi))}
for (region in regions) {
def uris = manager.getServer(region, 1).getURIs().collect {it.toString()}
def uriString = String.join("\t", uris)
print region.toString() + "\t" + uriString
}
//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()
//or
PROJECT_BASE_DIR
//From: https://gist.github.com/kaischleicher/dda2c5a1abb717336bae2ff656cb91dd
//Useful for setting up QuPath (0.1.2?) 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
PathPrefs.setNumCommandThreads(3)
//https://forum.image.sc/t/script-for-send-region-to-imagej/39554/18
//0.2.2
import qupath.imagej.gui.IJExtension
import ij.plugin.frame.RoiManager
double downsample = 4.0
// Define a path to save the ROIs, or null if the ROI Manager should be shown instead
String savePath = "/path/to/rois.zip"
//String savePath = null
boolean showManager = true // If false, just save the ROIs instead
def server = getCurrentServer()
def region = RegionRequest.createInstance(server, downsample)
def options = getCurrentViewer().getOverlayOptions()
def overlay = IJExtension.extractOverlay(
getCurrentHierarchy(),
region,
options,
null
)
RoiManager rm
if (showManager) {
rm = RoiManager.getInstance2()
if (rm == null)
rm = new RoiManager()
} else
rm = new RoiManager(false)
rm.setOverlay(overlay)
if (savePath)
rm.runCommand("save", savePath)
//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()
getAnnotationObjects().each{
String name = it.getName()
println(name)
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}');
@Svidro
Copy link
Author

Svidro 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