Skip to content

Instantly share code, notes, and snippets.

@Svidro
Last active October 15, 2024 06:02
Show Gist options
  • Save Svidro/e00021dff92ea1173e535008854be72e to your computer and use it in GitHub Desktop.
Save Svidro/e00021dff92ea1173e535008854be72e to your computer and use it in GitHub Desktop.
Color changes in QuPath
Collections of scripts to alter object colors harvested mainly from Pete, but also picked up from the forums
TOC
Change IF channel color.groovy - Change the LUT for individual channels. Does not work with OpenSlide servers (check Image tab).
Change colors by subclass.groovy - Detection object color adjustment when using subclasses.
Change subcellular detection color - Hugely useful when working with subcellular detections as, by default, they are a derived class
and cannot be altered directly through the Annotation tab interface.
Density heatmap by class.groovy - Add a measurement to cells that can be used to visually identify hotspots per class
Density heatmap by class with blur.groovy - better looking heatmap than above.
Heatmap for channels.groovy - Apply a LUT to a channel, instead of the default solid colors
Invert LUT of selected channels.groovy
Measurement map buttons.groovy - create some fixed buttons to make viewing consistent measurement maps much easeier.
Measurement Maps color lock.groovy - set fixed values for the measurement maps. Useful when comparing two images to each other and
keeping the same relative color display values.
Measurement Maps in 0.2.0.groovy - a code example for how to use measurement maps in version 0.2.0
Rename and recolor a class.groovy - <-- what it says
Show specific classes of objects v3.groovy - adds in checkboxes for groups of similarly named classes
Specific Object Color changes.groovy - A way of cycling through objects and set each object to a different color
TMA heatmap by color.groovy - Create detection objects in each TMA core, giving them measurements that are summaries of what is
in the core. When Measurement maps are used and one of the summary measurments is selected, the whole TMA turns into a heatmap
1
From Gitter:
If you have a class already created, you can alter the color for that class (replace pathClass with the class)
pathClass.setColor(getColorRGB(0, 200, 0))
This requires having the class as a variable, for example:
stroma = getPathClass('Stroma')
recolored would be:
stroma.setColor(getColorRGB(0, 200, 0))
// https://groups.google.com/forum/#!topic/qupath-users/rBCRysCZEzM
//0.1.2 and 0.2.0
// Access the 'Stroma: Positive' sub-classification
stroma = getPathClass('Stroma')
stromaPositive = getDerivedPathClass(stroma, 'Positive')
// Set the color, using a packed RGB value
color = getColorRGB(200, 0, 0)
stromaPositive.setColor(color)
// Update the GUI
fireHierarchyUpdate()
// Get access to the display info for each channel
//An even better script from https://forum.image.sc/t/qupath-scripts-dont-detect-updated-colormaps/63618/9?u=research_associate
setImageType('FLUORESCENCE')
// You can replace the names with your stainings if you want
// Define minimum display values, maximum display values and channel names in order
def mins = [ 0, 0, 0, 0 ]
def maxs = [ 8000, 750, 2000, 2500 ]
def names = ['DAPI', 'FITC', 'TRITC', 'CY3' ]
// Define colors
def color1 = getColorRGB( 0, 128, 255 )
def color2 = getColorRGB( 0, 255, 128 )
def color3 = getColorRGB( 255, 0, 128 )
def color4 = getColorRGB( 255, 128, 255 )
// Build color array
def colors = [ color1, color2, color3, color4 ]
//Finally set everything for the current image
setChannelNames( *names )
setChannelColors( *colors )
[mins, maxs].transpose().eachWithIndex{ mima, i -> setChannelDisplayRange(i, mima[0], mima[1]) }
////////////////////////////////////////////
///////////////////////////////////////////
//For 0.2.0+ use: https://forum.image.sc/t/a-bug-in-batch-processing/40956/2?u=research_associate
setChannelColors(
getColorRGB(0, 0, 255),
getColorRGB(0, 255, 0),
getColorRGB(255, 0, 255),
getColorRGB(255, 0, 0)
)
//https://forum.image.sc/t/script-to-rename-channels-and-adjust-brightness-contrast/40984/4?u=research_associate
//setChannelColor
//setChannelNames
//setChannelDisplayRange
//This function changed between 0.1.2 and 0.2.0, in 0.2.0 use viewer.getImageDisplay().availableChannels()
def viewer = getCurrentViewer()
def channels = viewer.getImageDisplay().getAvailableChannels()
// Set the LUT color for the first channel & repaint
channels[0].setLUTColor(0, 0, 255)
channels[1].setLUTColor(255, 255, 255)
channels[2].setLUTColor(0, 255, 0)
channels[3].setLUTColor(255, 0, 0)
// Ensure the updates are visible
viewer.repaintEntireImage()
// Usually a good idea to print something, so we know it finished
print 'Done!'
//0.1.2 and 0.2.0
getDerivedPathClass(getPathClass('Subcellular cluster'), 'DAB object').setColor(qupath.lib.common.ColorTools.makeRGB(30, 30, 30))
// https://forum.image.sc/t/cell-density-map/40050/16
/**
* Create a 'counts' image in QuPath that can be used to compute the local density of specific objects.
*
* This implementation uses ImageJ to create and display the image, which can then be filtered as required.
*
* Written for QuPath v0.2.0.
*
* @author Pete Bankhead
*/
// Define the resolution at which the image should be generated
double requestedPixelSizeMicrons = 50
double sigma = 1.5
double accuracy = 0.01
classList = getCurrentHierarchy().getDetectionObjects().collect{it.getPathClass()} as Set
// Get the current image
def imageData = getCurrentImageData()
def server = imageData.getServer()
// Set the downsample directly (without using the requestedPixelSize) if you want; 1.0 indicates the full resolution
double downsample = requestedPixelSizeMicrons / server.getPixelCalibration().getAveragedPixelSizeMicrons()
def request = RegionRequest.createInstance(server, downsample)
def imp = IJTools.convertToImagePlus(server, request).getImage()
// Get the objects you want to count
// Potentially you can add filters for specific objects, e.g. to get only those with a 'Positive' classification
def detections = getDetectionObjects()
classList.each{c->
cellList = getCellObjects().findAll{it.getPathClass() == c}
// Create a counts image in ImageJ, where each pixel corresponds to the number of centroids at that pixel
int width = imp.getWidth()
int height = imp.getHeight()
def fp = new FloatProcessor(width, height)
for (detection in cellList) {
// Get ROI for a detection; this method gets the nucleus if we have a cell object (and the only ROI for anything else)
def roi = PathObjectTools.getROI(detection, true)
int x = (int)(roi.getCentroidX() / downsample)
int y = (int)(roi.getCentroidY() / downsample)
fp.setf(x, y, fp.getf(x, y) + 1 as float)
}
//Here we blur fp. Increase sigma for more blurring.
def g = new GaussianBlur();
g.blurGaussian(fp, sigma, sigma, accuracy);
for (detection in detections) {
// Get ROI for a detection; this method gets the nucleus if we have a cell object (and the only ROI for anything else)
def roi = PathObjectTools.getROI(detection, true)
int x = (int)(roi.getCentroidX() / downsample)
int y = (int)(roi.getCentroidY() / downsample)
detection.getMeasurementList().putMeasurement(c.toString()+' Density', fp.getf(x, y))
}
}
import ij.ImagePlus
import ij.process.FloatProcessor
import ij.plugin.filter.GaussianBlur;
//import ij.plugin.filter.PlugInFilter;
import qupath.imagej.gui.IJExtension
import qupath.lib.objects.PathObjectTools
import qupath.lib.regions.RegionRequest
import static qupath.lib.gui.scripting.QPEx.*
import qupath.imagej.tools.IJTools
// QP 0.2.3
// This script takes the values generated by the Add smoothed features plugin to create a cell density map that can be viewed in Measure->Measurement Maps
// For example, Tumor cells would show up in the Measurement List as "Nearby cells - Tumor"
// Local density will be based on the radius used during Add smoothed features.
//////////CHANGE THIS //////
String smoothedRadius = 15
////////////////////////////
//Alternatively, remove this line and use the Analyze->Calculate Features-> Add smoothed features with Smooth within classes checked.
selectAnnotations()
runPlugin('qupath.lib.plugins.objects.SmoothFeaturesPlugin', '{"fwhmMicrons": '+smoothedRadius+', "smoothWithinClasses": true}');
classList = getCurrentHierarchy().getDetectionObjects().collect{it.getPathClass()} as Set
//Find the smoothed count
classList.each{c->
cellList = getCellObjects().findAll{it.getPathClass() == c}
cellList.each{
it.getMeasurementList().putMeasurement("Nearby cells - "+ c.toString(), measurement(it,"Smoothed: "+smoothedRadius+" µm: Nearby detection counts"))
}
//getCellObjects().findAll{it.getPathClass() != c}.each{it.getMeasurementList().putMeasurement("Nearby cells - "+ c.toString(), 0)}
}
print "Done"
//From https://github.com/qupath/qupath/issues/191
//https://groups.google.com/forum/#!searchin/qupath-users/viewer%7Csort:date/qupath-users/uBMxJ_3JnBM/GkDahJw7EAAJ
// Get access to the display info for each channel
//0.1.2
//For 0.2.0 use viewer.getImageDisplay().availableChannels()
/*def viewer = getCurrentViewer()
def channels = viewer.getImageDisplay().getAvailableChannels()
// Set the range for the first two channels
channels[0].setMinDisplay(0)
channels[0].setMaxDisplay(100)
channels[1].setMinDisplay(0)
channels[1].setMaxDisplay(500)
// Ensure the updates are visible
viewer.repaintEntireImage()*/
// Usually a good idea to print something, so we know it finished
print 'Done!'
//Use a heatmap for a fluorescent channel
//https://forum.image.sc/t/qupath-scripting-3-apply-colorlut-to-channel/50368
import qupath.lib.display.ChannelDisplayInfo
import qupath.lib.display.DirectServerChannelInfo
import qupath.lib.display.ImageDisplay
import qupath.lib.gui.QuPathGUI
import qupath.lib.gui.tools.MeasurementMapper
import qupath.lib.gui.viewer.QuPathViewer
import qupath.lib.common.ColorTools
import java.awt.image.IndexColorModel
byte[] rb = new byte[256];
byte[] gb = new byte[256];
byte[] bb = new byte[256];
// LUT fire
int[][] lutFire = [[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,4,7,10,13,16,19,22,25,28,31,34,37,40,43,46,49,52,55,58,61,64,67,70,73,76,79,82,85,88,91,94,98,101,104,107,110,113,116,119,122,125,128,131,134,137,140,143,146,148,150,152,154,156,158,160,162,163,164,166,167,168,170,171,173,174,175,177,178,179,181,182,184,185,186,188,189,190,192,193,195,196,198,199,201,202,204,205,207,208,209,210,212,213,214,215,217,218,220,221,223,224,226,227,229,230,231,233,234,235,237,238,240,241,243,244,246,247,249,250,252,252,252,253,253,253,254,254,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255],
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,3,5,7,8,10,12,14,16,19,21,24,27,29,32,35,37,40,43,46,48,51,54,57,59,62,65,68,70,73,76,79,81,84,87,90,92,95,98,101,103,105,107,109,111,113,115,117,119,121,123,125,127,129,131,133,134,136,138,140,141,143,145,147,148,150,152,154,155,157,159,161,162,164,166,168,169,171,173,175,176,178,180,182,184,186,188,190,191,193,195,197,199,201,203,205,206,208,210,212,213,215,217,219,220,222,224,226,228,230,232,234,235,237,239,241,242,244,246,248,248,249,250,251,252,253,254,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255],
[0,7,15,22,30,38,45,53,61,65,69,74,78,82,87,91,96,100,104,108,113,117,121,125,130,134,138,143,147,151,156,160,165,168,171,175,178,181,185,188,192,195,199,202,206,209,213,216,220,220,221,222,223,224,225,226,227,224,222,220,218,216,214,212,210,206,202,199,195,191,188,184,181,177,173,169,166,162,158,154,151,147,143,140,136,132,129,125,122,118,114,111,107,103,100,96,93,89,85,82,78,74,71,67,64,60,56,53,49,45,42,38,35,31,27,23,20,16,12,8,5,4,3,3,2,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,8,13,17,21,26,30,35,42,50,58,66,74,82,90,98,105,113,121,129,136,144,152,160,167,175,183,191,199,207,215,223,227,231,235,239,243,247,251,255,255,255,255,255,255,255,255]]
QuPathViewer viewer = QuPathGUI.getInstance().getViewer()
// Get current channel
ImageDisplay display = viewer.getImageDisplay()
ChannelDisplayInfo channelinfoS = display.selectedChannels().get(0)
List<MeasurementMapper.ColorMapper> colormaps = MeasurementMapper.loadColorMappers()
int nColorMaps = 0
colormaps.each { map ->
print nColorMaps + " : " + map.getName() + '\n'
nColorMaps++
}
// Select your ColorMap here (or set to -1 for FireLUT)
// Currently
// 0 : Viridis
// 1 : Svidro2
// 2 : Plasma
// 3 : Magma
// 4 : Inferno
// 5 : Jet
int useColorMap = 1
if (useColorMap < 0 || useColorMap >= nColorMaps) {
// Create FireLUT
for (int i = 0; i < 256; i++) {
rb[i] = (byte) ColorTools.do8BitRangeCheck(lutFire[0][i])
gb[i] = (byte) ColorTools.do8BitRangeCheck(lutFire[1][i])
bb[i] = (byte) ColorTools.do8BitRangeCheck(lutFire[2][i])
}
}
else{
// Read ColorMap
def theMap = colormaps.get(useColorMap)
for (int i = 0; i < 256; i++) {
rb[i] = (byte) ColorTools.do8BitRangeCheck(theMap.r[i])
gb[i] = (byte) ColorTools.do8BitRangeCheck(theMap.g[i])
bb[i] = (byte) ColorTools.do8BitRangeCheck(theMap.b[i])
}
}
if (channelinfoS != null && channelinfoS instanceof DirectServerChannelInfo){
DirectServerChannelInfo directInfo = (DirectServerChannelInfo)channelinfoS
directInfo.cm = new IndexColorModel(8, 256, rb, gb, bb);
// Optional
// Assign histogram color (Brightness&Contrast dialog)
directInfo.rgb = ColorTools.makeRGB(200, 200, 200);
}
viewer.getImageRegionStore().clearCache(false, false)
viewer.repaintEntireImage()
println('Done!')
// https://forum.image.sc/t/qupath-scripting-3-apply-colorlut-or-invertedlut-to-channel/50368/6?u=research_associate
import qupath.lib.display.DirectServerChannelInfo
import qupath.lib.display.ImageDisplay
import qupath.lib.gui.QuPathGUI
import qupath.lib.gui.viewer.QuPathViewer
import java.awt.image.IndexColorModel
byte[] rb = new byte[256];
byte[] gb = new byte[256];
byte[] bb = new byte[256];
byte[] invLUT = [255,254,253,252,251,250,249,248,247,246,245,244,243,242,241,240,239,238,237,236,235,234,233,232,231,230,229,228,227,226,225,224,223,222,221,220,219,218,217,216,215,214,213,212,211,210,209,208,207,206,205,204,203,202,201,200,199,198,197,196,195,194,193,192,191,190,189,188,187,186,185,184,183,182,181,180,179,178,177,176,175,174,173,172,171,170,169,168,167,166,165,164,163,162,161,160,159,158,157,156,155,154,153,152,151,150,149,148,147,146,145,144,143,142,141,140,139,138,137,136,135,134,133,132,131,130,129,128,127,126,125,124,123,122,121,120,119,118,117,116,115,114,113,112,111,110,109,108,107,106,105,104,103,102,101,100,99,98,97,96,95,94,93,92,91,90,89,88,87,86,85,84,83,82,81,80,79,78,77,76,75,74,73,72,71,70,69,68,67,66,65,64,63,62,61,60,59,58,57,56,55,54,53,52,51,50,49,48,47,46,45,44,43,42,41,40,39,38,37,36,35,34,33,32,31,30,29,28,27,26,25,24,23,22,21,20,19,18,17,16,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0]
QuPathViewer viewer = QuPathGUI.getInstance().getViewer()
ImageDisplay display = viewer.getImageDisplay()
display.selectedChannels().each { channel ->
if (channel instanceof DirectServerChannelInfo) {
DirectServerChannelInfo directInfo = (DirectServerChannelInfo) channel
int color = directInfo.getColor()
int r = (color & 0xff0000) >> 16
int g = (color & 0x00ff00) >> 8
int b = (color & 0x0000ff)
// Create invertedLUT (depending on the original channel color)
for (int i = 0; i < 256; i++) {
rb[i] = (byte) invLUT[i]/255*r
gb[i] = (byte) invLUT[i]/255*g
bb[i] = (byte) invLUT[i]/255*b
}
directInfo.cm = new IndexColorModel(8, 256, rb, gb, bb);
}
}
viewer.getImageRegionStore().clearCache(false, false)
viewer.repaintEntireImage()
println('Done!')
guiscript=true
//0.1.2 - Largely obsolete with the Keep settings checkbox in Brightness/contrast.
//Script sets up some buttons to allow easy viewing of a fixed measurement map. Buttons are only removed when closing QuPath
//Keep these labels fairly short, or increase the button size. Be careful about having enough room!
int buttonSize = 40
String[] buttonLabels = ["1C","2C", "3N", "4C", "5C", "6C"] as String[]
String[] names = ["Cytoplasm: Channel 1 mean", "Cytoplasm: Channel 2 mean", "Nucleus: Channel 3 mean", "Cytoplasm: Channel 4 mean", "Cytoplasm: Channel 5 mean", "Cytoplasm: Channel 6 mean"] as String[]
double[] minValue= [0,0,0,0,0,0]
double[] maxValue= [18, 1,6,1,20,10]
//import javafx.application.Platform
import javafx.scene.control.Button
import javafx.scene.control.Tooltip
import qupath.lib.gui.QuPathGUI
import qupath.lib.gui.helpers.MeasurementMapper
def qupath = QuPathGUI.getInstance()
int size = names.size()
buttons = new Button [size]
//check some things
if (minValue.size()!= size || maxValue.size()!= size || buttonLabels.size() != size ) {println("All lists not same size"); return;}
if (getDetectionObjects().size() < 1){println("Detections NEED to be present before running this script"); return;}
maps = new MeasurementMapper [size]
def unColor = new Button('Clear')
unColor.setPrefSize(50, QuPathGUI.iconSize)
unColor.setTooltip(new Tooltip("Remove coloring"));
unColor.setOnAction {
print 'Resetting measurement map'
getCurrentViewer().getOverlayOptions().setMeasurementMapper(null)
}
qupath.addToolbarButton(unColor);
for (i = 0; i<size; i++){
maps[i] = new MeasurementMapper(names[i], getDetectionObjects())
}
//println(maps)
for (i = 0; i<size; i++){
buttons[i] = new Button(buttonLabels[i])
buttons[i].setPrefSize(buttonSize, QuPathGUI.iconSize)
buttons[i].setTooltip(new Tooltip("Measurement Maps "+names[i]));
buttons[i].setOnAction {e->
source = e.getSource().getText()
//println(source)
//println(buttons[0].getText())
//println(buttons[1].getText())
//println(size)
int j= 255
for (k = 0; k<size; k++){
//println("k " + k)
if (source == buttons[k].getText()){j=k; print "please";}else{print "NotA"}
}
// Update the display
//println(j)
//println(buttons[j].getText())
if (names[j]) {
print String.format('Setting measurement map: %s (%.2f - %.2f)', names[j], minValue[j], maxValue[j])
maps[j].setDisplayMinValue(minValue[j])
maps[j].setDisplayMaxValue(maxValue[j])
getCurrentViewer().getOverlayOptions().setMeasurementMapper(maps[j])
} else {
print 'Resetting measurement map'
getCurrentViewer().getOverlayOptions().setMeasurementMapper(null)
}
}
}
for (n = 0; n<size; n++){
qupath.addToolbarButton(buttons[n]);
}
/**
* Set a MeasurementMapper in QuPath v0.1.2 to control the display in a script.
*
* Created for https://groups.google.com/d/msg/qupath-users/9kMNlg4sgAs/dUHQpROLDwAJ
*
* @author Pete Bankhead
*/
import qupath.lib.gui.helpers.MeasurementMapper
import static qupath.lib.scripting.QPEx.*
// Define measurement & display range
def name = "Nucleus/Cell area ratio" // 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(name, detections)
mapper.setDisplayMinValue(minValue)
mapper.setDisplayMaxValue(maxValue)
options.setMeasurementMapper(mapper)
} else {
print 'Resetting measurement map'
options.setMeasurementMapper(null)
}
// from https://forum.image.sc/t/problem-running-script-from-publication/35542/7
//0.2.0
import qupath.lib.gui.tools.*
// Print the names (just to check which you want)
println MeasurementMapper.loadDefaultColorMaps()
// Choose one of them
def colorMapper = MeasurementMapper.loadDefaultColorMaps().find {it.getName() == 'Magma'}
// Create a measurement mapper
def detections = getDetectionObjects()
def measurementMapper = new MeasurementMapper(colorMapper, 'Nucleus: DAB OD mean', detections)
// Show the measurement mapper in the current viewer
def viewer = getCurrentViewer()
def overlayOptions = viewer.getOverlayOptions()
overlayOptions.setMeasurementMapper(measurementMapper)
//0.1.2 - This is much easier to do through the interface or scripting in 0.2.0
//To use in 0.2.0 swap the ColorToolsFX import to import qupath.lib.gui.tools.ColorToolsFX
import javafx.application.Platform
import javafx.beans.property.SimpleLongProperty
import javafx.geometry.Insets
import javafx.scene.Scene
import javafx.geometry.Pos
import javafx.scene.control.Button
import javafx.scene.control.Label
import javafx.scene.control.TextField
import javafx.scene.control.ColorPicker
import javafx.scene.control.ComboBox
import javafx.scene.control.TableColumn
import javafx.scene.layout.BorderPane
import javafx.scene.layout.GridPane
import javafx.scene.control.Tooltip
import javafx.stage.Stage
import qupath.lib.gui.QuPathGUI
import qupath.lib.gui.helpers.ColorToolsFX;
import javafx.scene.paint.Color;
int col = 0
int row = 0
int textFieldWidth = 120
int labelWidth = 150
def gridPane = new GridPane()
gridPane.setPadding(new Insets(10, 10, 10, 10));
gridPane.setVgap(2);
gridPane.setHgap(10);
def titleLabel = new Label("Alter the color and name of a class of objects")
gridPane.add(titleLabel,col, row++, 3, 1)
def requestLabel = new Label("Original Name")
gridPane.add(requestLabel,col++, row, 1, 1)
def requestLabel2 = new Label("New Name")
gridPane.add(requestLabel2,col++, row, 1, 1)
def requestLabel3 = new Label("New Color")
gridPane.add(requestLabel3,col++, row++, 1, 1)
//new row
col = 0
//generate a list of all in-use classes
Set classList = []
for (object in getAllObjects().findAll{it.isDetection() || it.isAnnotation()}) {
classList << object.getPathClass()
}
//place all classes in a combobox
def ComboBox classText = new ComboBox();
classList.each{classText.getItems().add(it)}
gridPane.add(classText, col++, row, 1, 1)
def TextField classText2 = new TextField("MyNewClass");
classText2.setMaxWidth( textFieldWidth);
classText2.setAlignment(Pos.CENTER_RIGHT)
gridPane.add(classText2, col++, row, 1, 1)
def colorPicker = new ColorPicker()
gridPane.add(colorPicker, col, row++, 1, 1)
//ArrayList<Label> channelLabels
Button startButton = new Button()
startButton.setText("Alter Class")
gridPane.add(startButton, 0, row++, 1, 1)
//startButton.setTooltip(new Tooltip("If you need to change the number of classes, re-run the script"));
startButton.setOnAction {
changeList = getAllObjects().findAll{it.getPathClass() == getPathClass(classText.getValue().toString())}
changeList.each{
it.setPathClass(getPathClass(classText2.getText()))
newClass = getPathClass(classText2.getText())
newClass.setColor(ColorToolsFX.getRGBA(colorPicker.getValue()))
}
fireHierarchyUpdate()
}
//Some stuff that controls the dialog box showing up. I don't really understand it but it is needed.
Platform.runLater {
def stage = new Stage()
stage.initOwner(QuPathGUI.getInstance().getStage())
stage.setScene(new Scene( gridPane))
stage.setTitle("Class editor")
stage.setWidth(450);
stage.setHeight(200);
//stage.setResizable(false);
stage.show()
}
//Objective: A quicker way to show only certain classes and hide all others
//ANY GROUP CLASS CHECKING or UNCHECKED OVERWRITE ANY SINGLE CLASS CHANGES
//Written for 0.2.0 and 0.3.0, code fix by @Mark_Zaidi and @petebankhead regarding access to base class names
//Removed tokenization, ending backwards compatability with 0.1.2 multiplexing.
//Find all classifications of detections
/*****************************************
If you have subcellular objects, you may want
to change this to getDetectionObjects() so that
you can turn those on and off as well
*****************************************/
def classifications = new ArrayList<>( getCellObjects().collect {it?.getPathClass()} as Set)
/////////////////////////////////////////////////////////////
List<String> classNames = new ArrayList<String>()
classifications.each{
classNames<< it.toString()
}
Set baseClasses = []
classifications.each{
getCurrentViewer().getOverlayOptions().hiddenClassesProperty().add(it)
PathClassTools.splitNames(it).each{str->
baseClasses << str.trim()
}
}
print baseClasses
baseList = baseClasses
//Find strings with duplicates in baseClasses
//baseList = baseClasses.countBy{it}.grep{it.value > 1}.collect{it.key}
//Set up GUI
int col = 0
int row = 0
int textFieldWidth = 120
int labelWidth = 150
def gridPane = new GridPane()
gridPane.setPadding(new Insets(10, 10, 10, 10));
gridPane.setVgap(2);
gridPane.setHgap(10);
ScrollPane scrollPane = new ScrollPane(gridPane)
scrollPane.setFitToHeight(true);
BorderPane border = new BorderPane(scrollPane)
border.setPadding(new Insets(15));
//Separately set up a checkbox for All classes
allOn = new CheckBox("All")
allOn.setId("All")
gridPane.add( allOn, 1, row++, 1,1)
row = 1
ArrayList<CheckBox> boxes = new ArrayList(classifications.size());
//Create the checkboxes for each class
for (i=0; i<classifications.size();i++){
name = classNames[i].toString()
if (name == "null"){name = "None"}
cb = new CheckBox(name)
cb.setId(name)
boxes.add(cb)
gridPane.add( cb, col, row++, 1,1)
}
//Create checkboxes for base classes, defined as some string that showed up in more than one class entry
ArrayList<CheckBox> baseBoxes = new ArrayList(baseList.size());
row = 2
for (i=0; i<baseList.size();i++){
cb = new CheckBox(baseList[i])
cb.setId(baseList[i])
baseBoxes.add(cb)
gridPane.add( cb, 1, row++, 1,1)
}
//behavior for all single class checkboxes
//I can't seem to check which checkbox is selected when they are created dynamically, so the results are updated for all classes
for (c in boxes){
c.selectedProperty().addListener({o, oldV, newV ->
firstCol = gridPane.getChildren().findAll{gridPane.getColumnIndex(it) == 0}
for (n in firstCol){
if (n.isSelected()){
getCurrentViewer().getOverlayOptions().hiddenClassesProperty().remove(getPathClass(n.getId()))
}else {getCurrentViewer().getOverlayOptions().hiddenClassesProperty().add(getPathClass(n.getId()))}
}
} as ChangeListener)
}
//behavior for base class checkboxes
//I can't easily figure out which checkbox was last checked, so this overwrites any single class checkboxes that were selected or unselected
for (c in baseBoxes){
c.selectedProperty().addListener({o, oldV, newV ->
//verify that we are in the second column, and the nodes are selected
secondColSel = gridPane.getChildren().findAll{gridPane.getColumnIndex(it) == 1 && it.isSelected()}
secondColUnSel = gridPane.getChildren().findAll{gridPane.getColumnIndex(it) == 1 && !it.isSelected()}
for (n in secondColUnSel){
batch = gridPane.getChildren().findAll{gridPane.getColumnIndex(it) == 0 && it.getId().contains(n.getId())}
batch.each{
it.setSelected(false)
getCurrentViewer().getOverlayOptions().hiddenClassesProperty().add(getPathClass(it.getId()))
}
}
for (n in secondColSel){
batch = gridPane.getChildren().findAll{gridPane.getColumnIndex(it) == 0 && it.getId().contains(n.getId())}
batch.each{
it.setSelected(true)
getCurrentViewer().getOverlayOptions().hiddenClassesProperty().remove(getPathClass(it.getId()))
}
}
} as ChangeListener)
}
//Turn all on or off based on the All checkbox
allOn.selectedProperty().addListener({o, oldV, newV ->
if (!allOn.isSelected()){
classifications.each{
getCurrentViewer().getOverlayOptions().hiddenClassesProperty().add(it)
}
gridPane.getChildren().each{
it.setSelected(false)
}
}else {
classifications.each{
getCurrentViewer().getOverlayOptions().hiddenClassesProperty().remove(it)
}
gridPane.getChildren().each{
it.setSelected(true)
}
}
}as ChangeListener)
//Some stuff that controls the dialog box showing up. I don't really understand it but it is needed.
Platform.runLater {
def stage = new Stage()
stage.initOwner(QuPathGUI.getInstance().getStage())
stage.setScene(new Scene( border))
stage.setTitle("Select classes to display")
stage.setWidth(800);
stage.setHeight(500);
stage.setResizable(true);
stage.show()
}
import javafx.application.Platform
import javafx.geometry.Insets
import javafx.scene.Scene
import javafx.geometry.Pos
import javafx.scene.control.TableView
import javafx.scene.control.CheckBox
import javafx.scene.layout.BorderPane
import javafx.scene.layout.GridPane
import javafx.scene.control.ScrollPane
import javafx.scene.layout.BorderPane
import javafx.stage.Stage
import javafx.scene.input.MouseEvent
import javafx.beans.value.ChangeListener
import qupath.lib.gui.QuPathGUI
import static qupath.lib.gui.scripting.QPEx.getCurrentViewer
//The information in this script could be generalized to alter the color for any object list, or any individual object (see Selecting things Gist)
//Another way to use it might be to create a list of names ["blue","green","red"] along with a list/map of groups of three values
// [[0,0,200],[0,200,0],[200,0,0]] and pull from each, which would create a predetermined rainbow of named objects
//0.1.2 and 0.2.0
def annotations = getAnnotationObjects()
for (i = 0; i<annotations.size(); i++){
def j = i.mod(255)
//modulus used to keep the RGB values in the 0-255 range
annotations[i].setColorRGB(getColorRGB(255-j,j, j))
}
print "done"
//For earlier versions of QuPath, tested in 0.1.3
//Create a detection object the same size and shape as the TMA core
//Give it summary measurements for the percentage of cells of each class within the core
//When one of the Class % measurements is selected while viewing Measure->Measurement Maps, all other detections will disappear
//and only the summary detection objects will be visible.
//It may be best to turn off annotation visibility.
import qupath.lib.objects.PathDetectionObject
import qupath.lib.objects.PathCellObject
hierarchy = getCurrentHierarchy()
cores = hierarchy.getTMAGrid().getTMACoreList()
Set list = []
for (object in getAllObjects().findAll{it.isDetection() /*|| it.isAnnotation()*/}) {
list << object.getPathClass().toString()
}
print list
print "before cores"
cores.each {
print "initiating core"
//Find the cell count in this core
total = hierarchy.getDescendantObjects(it, null, PathCellObject).size()
//Prevent divide by zero errors in empty TMA cores
if (total != 0){
for (className in list) {
cellType = hierarchy.getDescendantObjects(it,null, PathCellObject).findAll{it.getPathClass() == getPathClass(className)}.size()
it.getMeasurementList().putMeasurement(className+" cell %", cellType/(total)*100)
}
}
else {
for (className in list) {
it.getMeasurementList().putMeasurement(className+" cell %", 0)
}
}
fireHierarchyUpdate()
print "core complete"
}
cores.each {
print it
roi = it.getROI()
coreName = it.getName()+" - Tile"
def detection = new PathDetectionObject(roi, getPathClass("Tile"))
hierarchy.addPathObject(detection, false)
ml = it.getMeasurementList()
for (i=0;i<ml.size(); i++){
detection.getMeasurementList().putMeasurement(ml.getMeasurementName(i), measurement(it, ml.getMeasurementName(i)))
}
fireHierarchyUpdate()
}
println("Are you done yet?")
// Add percentages by cell class to each TMA core
//Confirmed for 0.2.0
print "Wait for the comment indicating that it is done!"
print "This process is slow."
import qupath.lib.objects.PathCellObject
def metadata = getCurrentImageData().getServer().getOriginalMetadata()
def pixelSize = metadata.pixelCalibration.pixelWidth.value
hierarchy = getCurrentHierarchy()
cores = hierarchy.getTMAGrid().getTMACoreList()
Set list = []
for (object in getAllObjects().findAll{it.isDetection() /*|| it.isAnnotation()*/}) {
list << object.getPathClass()
}
print list
def cellList = []
cores.each {
//Find the cell count in this core
cellList = []
cellList = qupath.lib.objects.PathObjectTools.getDescendantObjects(it, cellList, PathCellObject)
total = cellList.size()
//Prevent divide by zero errors in empty TMA cores
if (total != 0){
annos=it.getChildObjects()[0]
if (annos.isAnnotation()){
for (className in list) {
cellType = cellList.findAll{p->p.getPathClass() == className}.size()
annotationArea = annos.getROI().getArea()*pixelSize*pixelSize/1000000
println(cellType)
println(annotationArea)
it.getMeasurementList().putMeasurement(className.toString()+" cells/mm^2", cellType/(annotationArea))
}
}
for (className in list) {
cellType = cellList.findAll{p->p.getPathClass() == className}.size()
it.getMeasurementList().putMeasurement(className.toString()+" cell %", cellType/(total)*100)
}
}
else {
for (className in list) {
it.getMeasurementList().putMeasurement(className.toString()+" cell %", 0)
}
}
}
import qupath.lib.objects.PathDetectionObject
cores.each {
roi = it.getROI()
coreName = it.getName()+" - Tile"
def detection = new PathDetectionObject(roi, getPathClass("Tile"))
hierarchy.addPathObject(detection)
ml = it.getMeasurementList()
for (i=0;i<ml.size(); i++){
//println(ml.getMeasurementValue(i))
//println(detection)
detection.getMeasurementList().putMeasurement(ml.getMeasurementName(i), measurement(it, ml.getMeasurementName(i)))
}
fireHierarchyUpdate()
}
println("Now it is done.")
//0.2.0
// https://forum.image.sc/t/qupath-toggle-show-hide-detections-annotations-by-scripting/45916/4
def overlayOptions = getCurrentViewer().getOverlayOptions()
overlayOptions.hiddenClassesProperty().addAll(
getPathClass('Tumor'),
getPathClass('Stroma')
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment