From 7d6f18e25cac4aade5a9180ab66d323e4712e184 Mon Sep 17 00:00:00 2001 From: Alan O'Callaghan Date: Thu, 22 Aug 2024 16:28:52 +0100 Subject: [PATCH 1/4] Improve handling of channel numbers in UI to prevent spurious errors Change the GUI look a bit, making the "extra" options center-aligned with some padding. Hopefully resolve the weirdness when the 3 color deconvolved channels would count as selected, but not show up visually as selected, if switching from multiplex to brightfield. Change the handling of channels to read the channel information from the RDF spec and enable/disable running/error messages appropriately. Running models is disabled and a warning shown if: - the currently selected model doesn't support arbitrary channel numbers - the number of currently selected channels doesn't match the number the model wants This means you can do funny stuff like: - select 3 channels in a multiplexed image and run a brightfield model on it - Run a brightfield model on the color deconvolved channels of an RGB image - Run a fluorescence model on brightfield (color deconvolved or RGB) - Support weird models like the 2 channel tissuenet one --- .../ext/instanseg/core/InstanSegModel.java | 9 ++++ .../ext/instanseg/ui/InstanSegController.java | 47 +++++++++++++--- .../ext/instanseg/ui/MessageTextHelper.java | 16 +++++- .../ext/instanseg/ui/instanseg_control.fxml | 54 ++++++++++++++----- .../ext/instanseg/ui/strings.properties | 13 ++--- 5 files changed, 112 insertions(+), 27 deletions(-) diff --git a/src/main/java/qupath/ext/instanseg/core/InstanSegModel.java b/src/main/java/qupath/ext/instanseg/core/InstanSegModel.java index c01dbe4..2f15328 100644 --- a/src/main/java/qupath/ext/instanseg/core/InstanSegModel.java +++ b/src/main/java/qupath/ext/instanseg/core/InstanSegModel.java @@ -158,6 +158,15 @@ private Map getPixelSize() { return map; } + public int getNumChannels() { + assert getModel().getInputs().getFirst().getAxes().equals("bcyx"); + int numChannels = getModel().getInputs().getFirst().getShape().getShapeMin()[1]; + if (getModel().getInputs().getFirst().getShape().getShapeStep()[1] == 1) { + numChannels = Integer.MAX_VALUE; + } + return numChannels; + } + private void fetchModel() { if (modelURL == null) { throw new NullPointerException("Model URL should not be null for a local model!"); diff --git a/src/main/java/qupath/ext/instanseg/ui/InstanSegController.java b/src/main/java/qupath/ext/instanseg/ui/InstanSegController.java index df26b9b..e763fe7 100644 --- a/src/main/java/qupath/ext/instanseg/ui/InstanSegController.java +++ b/src/main/java/qupath/ext/instanseg/ui/InstanSegController.java @@ -158,7 +158,17 @@ void promptForModelDirectory() { private void configureChannelPicker() { updateChannelPicker(qupath.getImageData()); qupath.imageDataProperty().addListener((v, o, n) -> updateChannelPicker(n)); - comboChannels.disableProperty().bind(qupath.imageDataProperty().isNull()); + // comboChannels.disableProperty().bind(Bindings.createBooleanBinding( + // () -> { + // var model = modelChoiceBox.getSelectionModel().getSelectedItem(); + // var imageData = qupath.getImageData(); + // if (model == null || imageData == null) { + // return false; + // } + // return !(model.getNumChannels() == Integer.MAX_VALUE || model.getNumChannels() == imageData.getServer().nChannels()); + // }, + // qupath.imageDataProperty(), + // modelChoiceBox.getSelectionModel().selectedItemProperty())); comboChannels.setTitle(getCheckComboBoxText(comboChannels)); comboChannels.getItems().addListener((ListChangeListener) c -> { comboChannels.setTitle(getCheckComboBoxText(comboChannels)); @@ -170,13 +180,24 @@ private void configureChannelPicker() { addSetFromVisible(comboChannels); } + private void updateChannelPicker(ImageData imageData) { if (imageData == null) { return; } + comboChannels.getCheckModel().clearChecks(); comboChannels.getItems().clear(); comboChannels.getItems().setAll(getAvailableChannels(imageData)); - comboChannels.getCheckModel().checkIndices(IntStream.range(0, imageData.getServer().nChannels()).toArray()); + if (imageData.isBrightfield()) { + comboChannels.getCheckModel().checkIndices(IntStream.range(0, 3).toArray()); + var model = modelChoiceBox.getSelectionModel().selectedItemProperty().get(); + if (model != null && model.getNumChannels() != Integer.MAX_VALUE) { + comboChannels.getCheckModel().clearChecks(); + comboChannels.getCheckModel().checkIndices(0, 1, 2); + } + } else { + comboChannels.getCheckModel().checkIndices(IntStream.range(0, imageData.getServer().nChannels()).toArray()); + } } private static String getCheckComboBoxText(CheckComboBox comboBox) { @@ -279,6 +300,15 @@ private void configureRunning() { .or(modelChoiceBox.getSelectionModel().selectedItemProperty().isNull()) .or(messageTextHelper.hasWarning()) .or(deviceChoices.getSelectionModel().selectedItemProperty().isNull()) + .or(Bindings.createBooleanBinding(() -> { + var model = modelChoiceBox.getSelectionModel().selectedItemProperty().get(); + if (model == null) { + return true; + } + int numSelected = comboChannels.getCheckModel().getCheckedIndices().size(); + int numAllowed = model.getNumChannels(); + return !(numSelected == numAllowed || numAllowed == Integer.MAX_VALUE); + }, modelChoiceBox.getSelectionModel().selectedItemProperty())) ); pendingTask.addListener((observable, oldValue, newValue) -> { if (newValue != null) { @@ -292,6 +322,13 @@ private void configureModelChoices() { tfModelDirectory.textProperty().bindBidirectional(InstanSegPreferences.modelDirectoryProperty()); handleModelDirectory(tfModelDirectory.getText()); tfModelDirectory.textProperty().addListener((v, o, n) -> handleModelDirectory(n)); + // for brightfield models, we want to disable the picker and set it to use RGB only + modelChoiceBox.getSelectionModel().selectedItemProperty().addListener((v, o, n) -> { + if (qupath.getImageData().isBrightfield() && n != null && n.getNumChannels() != Integer.MAX_VALUE) { + comboChannels.getCheckModel().clearChecks(); + comboChannels.getCheckModel().checkIndices(0, 1, 2); + } + }); } private static void addRemoteModels(ComboBox comboBox) { @@ -352,7 +389,7 @@ private void addDeviceChoices() { } private void configureMessageLabel() { - messageTextHelper = new MessageTextHelper(modelChoiceBox, deviceChoices); + messageTextHelper = new MessageTextHelper(modelChoiceBox, deviceChoices, comboChannels); labelMessage.textProperty().bind(messageTextHelper.messageLabelText()); if (messageTextHelper.hasWarning().get()) { labelMessage.getStyleClass().setAll("warning-message"); @@ -455,7 +492,6 @@ protected Void call() { .build(); instanSeg.detectObjects(selectedObjects); - String cmd = String.format(""" import qupath.ext.instanseg.core.InstanSeg import static qupath.lib.gui.scripting.QPEx.* @@ -499,8 +535,7 @@ protected Void call() { if (model.nFailed() > 0) { var errorMessage = String.format(resources.getString("error.tiles-failed"), model.nFailed()); logger.error(errorMessage); - Dialogs.showErrorMessage(resources.getString("title"), - errorMessage); + Dialogs.showErrorMessage(resources.getString("title"), errorMessage); } return null; } diff --git a/src/main/java/qupath/ext/instanseg/ui/MessageTextHelper.java b/src/main/java/qupath/ext/instanseg/ui/MessageTextHelper.java index 097c5ae..0e827a9 100644 --- a/src/main/java/qupath/ext/instanseg/ui/MessageTextHelper.java +++ b/src/main/java/qupath/ext/instanseg/ui/MessageTextHelper.java @@ -11,6 +11,7 @@ import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.ObservableValue; import javafx.scene.control.ChoiceBox; +import org.controlsfx.control.CheckComboBox; import org.controlsfx.control.SearchableComboBox; import qupath.ext.instanseg.core.InstanSegModel; import qupath.lib.gui.QuPathGUI; @@ -32,6 +33,7 @@ class MessageTextHelper { private final SelectedObjectCounter selectedObjectCounter; private final SearchableComboBox modelChoiceBox; private final ChoiceBox deviceChoiceBox; + private final CheckComboBox comboChannels; /** * Text to display a warning (because inference can't be run) @@ -53,9 +55,10 @@ class MessageTextHelper { */ private BooleanBinding hasWarning; - MessageTextHelper(SearchableComboBox modelChoiceBox, ChoiceBox deviceChoiceBox) { + MessageTextHelper(SearchableComboBox modelChoiceBox, ChoiceBox deviceChoiceBox, CheckComboBox comboChannels) { this.modelChoiceBox = modelChoiceBox; this.deviceChoiceBox = deviceChoiceBox; + this.comboChannels = comboChannels; this.selectedObjectCounter = new SelectedObjectCounter(qupath.imageDataProperty()); configureMessageTextBindings(); } @@ -104,6 +107,7 @@ private StringBinding createWarningTextBinding() { return Bindings.createStringBinding(this::getWarningText, qupath.imageDataProperty(), modelChoiceBox.getSelectionModel().selectedItemProperty(), + comboChannels.getCheckModel().getCheckedItems(), deviceChoiceBox.getSelectionModel().selectedItemProperty(), selectedObjectCounter.numSelectedAnnotations, selectedObjectCounter.numSelectedTMACores, @@ -121,6 +125,16 @@ private String getWarningText() { return resources.getString("ui.error.no-selection"); if (deviceChoiceBox.getSelectionModel().isEmpty()) return resources.getString("ui.error.no-device"); + int modelChannels = modelChoiceBox.getSelectionModel().getSelectedItem().getNumChannels(); + int selectedChannels = comboChannels.getCheckModel().getCheckedItems().size(); + if (modelChannels != Integer.MAX_VALUE) { + if (modelChannels != selectedChannels) { + return String.format( + resources.getString("ui.error.num-channels-dont-match"), + modelChannels, + selectedChannels); + } + } return null; } diff --git a/src/main/resources/qupath/ext/instanseg/ui/instanseg_control.fxml b/src/main/resources/qupath/ext/instanseg/ui/instanseg_control.fxml index 1a3a0f1..54c3626 100644 --- a/src/main/resources/qupath/ext/instanseg/ui/instanseg_control.fxml +++ b/src/main/resources/qupath/ext/instanseg/ui/instanseg_control.fxml @@ -6,6 +6,7 @@ +
@@ -86,19 +87,28 @@ + - + + + + - + + + + - + + + + - + + + + - + + + + - + + + + - + + + + @@ -158,11 +188,12 @@