Created
March 12, 2026 07:44
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // === 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