Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Color changes in QuPath
Collections of scripts to alter object colors harvested mainly from Pete, but also picked up from the forums
TOC
Adjust fluorescence contrast.groovy - Adjusts the display channels. Only works if the image is currently visible. Version 0.1.3 saves
these changes once made, but this can keep consistent display coloring between images.
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.
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.
Rename and recolor a class.groovy - <-- what it says
Show specific classes of objects.groovy - Only checked classes will be displayed
Show specific classes of objects v2.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
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))
//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
def viewer = getCurrentViewer()
def channels = viewer.getImageDisplay().getAvailableChannels()
//Use viewer.getImageDisplay().availableChannels() in more recent versions of QuPath.
// 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!'
// https://groups.google.com/forum/#!topic/qupath-users/rBCRysCZEzM
// 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
def viewer = getCurrentViewer()
//This function changed between 0.1.2 and 0.2.0, in 0.2.0 use viewer.getImageDisplay().availableChannels()
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!'
getDerivedPathClass(getPathClass('Subcellular cluster'), 'DAB object').setColor(qupath.lib.common.ColorTools.makeRGB(30, 30, 30))
guiscript=true
//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)
}
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 in 0.2.0m7
separatorsForBaseClass = "[.-_,]+" //add an extra symbol between the brackets if you need to split on a different character
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
//Find all classifications of detections
/*****************************************
If you have subcellular objects, you may want
to change this to getCellObjects() rather than
getDetectionObjects()
*****************************************/
def classifications = new ArrayList<>(getDetectionObjects().collect {it.getPathClass()?.getBaseClass()} as Set)
List<String> classNames = new ArrayList<String>()
classifications.each{
classNames<< it.getName().toString()
}
List baseClasses = []
classifications.each{
getCurrentViewer().getOverlayOptions().hiddenClassesProperty().add(it)
it.getName().tokenize("[.-_, ]+").each{str->
baseClasses << str
}
}
//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++){
cb = new CheckBox(classNames[i])
cb.setId(classNames[i].toString())
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(400);
stage.setHeight(500);
stage.setResizable(true);
stage.show()
}
//Objective: A quicker way to show only certain classes and hide all others
//Written in 0.2.0m7
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
def classifications = new ArrayList<>(getDetectionObjects().collect {it.getPathClass()?.getBaseClass()} as Set)
List<String> classNames = new ArrayList<String>()
classifications.each{
classNames<< it.getName().toString()
}
classifications.each{
getCurrentViewer().getOverlayOptions().hiddenClassesProperty().add(it)
}
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));
allOn = new CheckBox("All")
allOn.setId("All")
gridPane.add( allOn, col, row++, 1,1)
row = 3
ArrayList<CheckBox> boxes = new ArrayList(classifications.size());
//Create the checkboxes for each class
for (i=0; i<classifications.size();i++){
cb = new CheckBox(classNames[i])
cb.setId(classNames[i].toString())
boxes.add(cb)
gridPane.add( cb, col, row++, 1,1)
}
for (c in boxes){
c.selectedProperty().addListener({o, oldV, newV ->
print(c.getId())
for (n in gridPane.getChildren()){
if (n.isSelected()){
getCurrentViewer().getOverlayOptions().hiddenClassesProperty().remove(getPathClass(n.getId()))
}else {getCurrentViewer().getOverlayOptions().hiddenClassesProperty().add(getPathClass(n.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(400);
stage.setHeight(500);
stage.setResizable(true);
stage.show()
}
//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
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
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.")
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.