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..25ea24e 100644 --- a/src/main/java/qupath/ext/instanseg/ui/InstanSegController.java +++ b/src/main/java/qupath/ext/instanseg/ui/InstanSegController.java @@ -158,7 +158,6 @@ void promptForModelDirectory() { private void configureChannelPicker() { updateChannelPicker(qupath.getImageData()); qupath.imageDataProperty().addListener((v, o, n) -> updateChannelPicker(n)); - comboChannels.disableProperty().bind(qupath.imageDataProperty().isNull()); comboChannels.setTitle(getCheckComboBoxText(comboChannels)); comboChannels.getItems().addListener((ListChangeListener) c -> { comboChannels.setTitle(getCheckComboBoxText(comboChannels)); @@ -170,13 +169,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 +289,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 +311,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 +378,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 +481,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 +524,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..4056026 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,10 +107,10 @@ private StringBinding createWarningTextBinding() { return Bindings.createStringBinding(this::getWarningText, qupath.imageDataProperty(), modelChoiceBox.getSelectionModel().selectedItemProperty(), + comboChannels.getCheckModel().getCheckedItems(), deviceChoiceBox.getSelectionModel().selectedItemProperty(), selectedObjectCounter.numSelectedAnnotations, - selectedObjectCounter.numSelectedTMACores, - selectedObjectCounter.numSelectedDetections); + selectedObjectCounter.numSelectedTMACores); } private String getWarningText() { @@ -116,11 +119,20 @@ private String getWarningText() { if (modelChoiceBox.getSelectionModel().isEmpty()) return resources.getString("ui.error.no-model"); if (selectedObjectCounter.numSelectedAnnotations.get() == 0 && - selectedObjectCounter.numSelectedDetections.get() == 0 && selectedObjectCounter.numSelectedTMACores.get() == 0) 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..cd54945 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 @@ + - + + + + - + + + + - + + + + - + + + + - + + + + - + + + + - + + + + @@ -175,11 +205,6 @@ - - - - - diff --git a/src/main/resources/qupath/ext/instanseg/ui/strings.properties b/src/main/resources/qupath/ext/instanseg/ui/strings.properties index 568a750..f6ecae1 100644 --- a/src/main/resources/qupath/ext/instanseg/ui/strings.properties +++ b/src/main/resources/qupath/ext/instanseg/ui/strings.properties @@ -33,6 +33,7 @@ ui.error.no-selection = No annotation, TMA core, or detection selected ui.error.no-model = No model selected ui.error.no-device = No device selected ui.error.no-image = No image selected +ui.error.num-channels-dont-match = Model requires %d channels, input has %d ui.selection.annotations-single = 1 annotation selected ui.selection.annotations-multiple = %d annotations selected @@ -45,22 +46,22 @@ ui.selection.empty = No valid objects selected # Hardware tab ui.options.pane = Additional Options -ui.options.device = Preferred device: +ui.options.device = Preferred device ui.options.device.tooltip = Select the preferred device for model running (choose CPU if other options are not available) -ui.options.nuclei-only = Nuclei only: +ui.options.nuclei-only = Nuclei only ui.options.nuclei-only.tooltip = Segment nuclei only, or nuclei and cell boundaries -ui.options.threads = Threads: +ui.options.threads = Threads ui.options.threads.tooltip = Define the preferred number of threads -ui.options.tilesize = Tile Size: +ui.options.tilesize = Tile size ui.options.tilesize.tooltip = Define the approximate tile size -ui.options.channel = Channels: +ui.options.channel = Channels ui.options.channel.tooltip = Define the set of channels to be used in inference ui.options.makeMeasurements = Make measurements ui.options.makeMeasurements.tooltip = Make shape and intensity measurements after detecting objects ui.options.noChannelSelected = No channels selected! ui.options.oneChannelSelected = 1 channel selected ui.options.nChannelSelected = %d channels selected -ui.options.directory = Downloaded model directory: +ui.options.directory = Downloaded model directory ui.options.directory.tooltip = Choose the directory where models should be stored # Model directories