Skip to content

Instantly share code, notes, and snippets.

@romainGuiet
Created March 12, 2026 07:44
Show Gist options
  • Select an option

  • Save romainGuiet/1bb1c9cf51589531b79ca0a9c733815a to your computer and use it in GitHub Desktop.

Select an option

Save romainGuiet/1bb1c9cf51589531b79ca0a9c733815a to your computer and use it in GitHub Desktop.
Allow to display a Guided Tour of a WSI set using tjhe script WSI_GuidedTour_Store.groovy
// === UI Components ===
TextArea textDisplay = new TextArea()
textDisplay.setEditable(false)
textDisplay.setWrapText(true)
textDisplay.setPrefRowCount(5)
VBox channelInfoBox = new VBox(5)
channelInfoBox.setStyle("-fx-padding: 5; -fx-border-color: lightgray; -fx-border-width: 1;")
Label channelInfoLabel = new Label("Displayed Channels")
channelInfoLabel.setStyle("-fx-font-weight: bold;")
channelInfoBox.getChildren().add(channelInfoLabel)
Button displayButton = new Button("Display")
Label waitLabel = new Label("Wait (sec)")
Spinner<Integer> waitSpinner = new Spinner<>(0, 60, 10)
waitSpinner.setEditable(true)
Button playButton = new Button("\u25B6")
HBox playControls = new HBox(10)
playControls.getChildren().addAll(waitLabel, waitSpinner, playButton)
// Layout
VBox rightPane = new VBox(10)
rightPane.getChildren().addAll(textDisplay, channelInfoBox, displayButton, playControls)
def applyDisplaySettings(annotation) {
def description = annotation.getDescription()
if (!description?.startsWith("[")) {
println "No saved settings found!"
return
}
def shell = new GroovyShell()
def savedMap
try {
savedMap = shell.evaluate("def dummy = ${description}; dummy")
} catch (Exception e) {
println "Parsing error: $e"
return
}
if (!(savedMap instanceof Map) || !savedMap.channels) {
println "Parsed object is not valid!"
return
}
def savedChannels = savedMap.channels
def savedNote = savedMap.note
def savedZoom = savedMap.zoom
//def channelNames = getCurrentServer().getMetadata().getChannels().collect { it.name.replaceAll("\\s", "") }
def channelNames = getCurrentServer().getMetadata().getChannels().collect { it.name }
def colors = [], mins = [], maxs = []
channelNames.each { channel ->
def match = savedChannels.find { it.name =~ channel }
if (match) {
def matcher = (match.color =~ /Color\[r=(\d+),g=(\d+),b=(\d+)\]/)
if (matcher.matches()) {
def (r, g, b) = [matcher[0][1], matcher[0][2], matcher[0][3]].collect { it as int }
colors.add(makeRGB(r, g, b))
mins.add(match.min instanceof Number ? match.min : 0)
maxs.add(match.max instanceof Number ? match.max : 1)
} else {
println "⚠ Color format error for '${match.name}'"
colors.add(Color.WHITE)
mins.add(0)
maxs.add(1)
}
} else {
println "⚠ No saved data for '${channel}'"
colors.add(Color.WHITE)
mins.add(0)
maxs.add(1)
}
}
// Apply display ranges
[mins, maxs].transpose().eachWithIndex { mima, i ->
setChannelDisplayRange(i, mima[0], mima[1])
}
def viewer = getCurrentViewer()
def display = viewer.getImageDisplay()
def available = display.availableChannels()
def selected = available.findAll { ac ->
savedChannels.any { sc -> sc.toString().contains( ac.getName() ) }
}
display.selectedChannels.setAll(selected)
viewer.repaintEntireImage()
if (savedZoom instanceof Number)
viewer.setDownsampleFactor(savedZoom)
return [savedNote, savedChannels]
}
displayButton.setOnAction {
def annotation = getSelectedObject()
if (!(annotation instanceof PathAnnotationObject)) {
println "No annotation selected!"
return
}
def stored = applyDisplaySettings(annotation)
if (!stored) return
def (savedNote, savedChannels) = stored
textDisplay.setText(savedNote)
channelInfoBox.getChildren().remove(1, channelInfoBox.getChildren().size())
savedChannels.each { ch ->
def matcher = (ch.color =~ /Color\[r=(\d+),g=(\d+),b=(\d+)\]/)
def rgb = matcher ? [matcher[0][1], matcher[0][2], matcher[0][3]].collect { it as int } : [255, 255, 255]
Rectangle colorBox = new Rectangle(20, 20)
colorBox.setFill(FxColor.rgb(*rgb))
colorBox.setStroke(FxColor.BLACK)
channelInfoBox.getChildren().add(new HBox(10, colorBox, new Label(ch.name)))
}
Platform.runLater {
def viewer = getCurrentViewer()
viewer.centerROI(annotation.getROI())
}
}
playButton.setOnAction {
def SLEEP_TIME = waitSpinner.getValue() * 1000
def annotations = getAnnotationObjects()
def viewer = getCurrentViewer()
Thread.start {
// Sort annotations by prefix in description if available
def sortedAnnotations = annotations.sort { a ->
def name = a.getName()
def matcher = (name =~ /^(\d+)-/)
return matcher ? matcher[0][1] as int : Integer.MAX_VALUE
}
println "Sorted annotation names: " + sortedAnnotations.collect { it.getName() }
sortedAnnotations.each { annotation ->
Platform.runLater {
def stored = applyDisplaySettings(annotation)
if (!stored) return
def (savedNote, savedChannels) = stored
textDisplay.setText(savedNote)
channelInfoBox.getChildren().remove(1, channelInfoBox.getChildren().size())
savedChannels.each { ch ->
def matcher = (ch.color =~ /Color\[r=(\d+),g=(\d+),b=(\d+)\]/)
def rgb = matcher ? [matcher[0][1], matcher[0][2], matcher[0][3]].collect { it as int } : [255, 255, 255]
Rectangle colorBox = new Rectangle(20, 20)
colorBox.setFill(FxColor.rgb(*rgb))
colorBox.setStroke(FxColor.BLACK)
channelInfoBox.getChildren().add(new HBox(10, colorBox, new Label(ch.name)))
}
viewer.centerROI(annotation.getROI())
}
Thread.sleep(SLEEP_TIME)
}
}
}
// === Show in a Stage ===
Platform.runLater {
def stage = new Stage()
stage.initOwner(QuPathGUI.getInstance().getStage())
stage.setTitle("Display Description")
stage.setScene(new Scene(rightPane, 500, 500))
stage.show()
if ( getSelectedObject() != null ) {
displayButton.fire()
} else if ( getAnnotationObjects() != null) {
playButton.fire()
}
}
// Imports
import qupath.lib.objects.PathAnnotationObject
import java.awt.Color
import javafx.application.Platform
import javafx.scene.Scene
import javafx.scene.control.*
import javafx.scene.layout.*
import javafx.stage.Stage
import javafx.scene.shape.Rectangle
import javafx.scene.paint.Color as FxColor
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment