Skip to content

Commit

Permalink
Merge pull request #64 from petebankhead/ui
Browse files Browse the repository at this point in the history
Support specifying output channels
  • Loading branch information
petebankhead authored Sep 5, 2024
2 parents 5cd0395 + c98dd6e commit 3388e23
Show file tree
Hide file tree
Showing 4 changed files with 160 additions and 32 deletions.
106 changes: 100 additions & 6 deletions src/main/java/qupath/ext/instanseg/ui/InstanSegController.java
Original file line number Diff line number Diff line change
Expand Up @@ -116,10 +116,12 @@ public class InstanSegController extends BorderPane {
@FXML
private ToggleButton selectAllTMACoresButton;
@FXML
private CheckBox nucleiOnlyCheckBox;
private CheckComboBox<InstanSegOutput> checkComboOutputs;
@FXML
private CheckBox makeMeasurementsCheckBox;
@FXML
private CheckBox randomColorsCheckBox;
@FXML
private Button infoButton;
@FXML
private Label modelDirLabel;
Expand Down Expand Up @@ -178,6 +180,29 @@ private InstanSegController(QuPathGUI qupath) throws IOException {
modelChoiceBox.getSelectionModel().selectedItemProperty().isNull())
);
configureChannelPicker();
configureOutputChannelCombo();
configureDefaultValues();
}

private void configureOutputChannelCombo() {
// Quick way to match widths...
checkComboOutputs.prefWidthProperty().bind(comboChannels.widthProperty());
// Show a better title than the text of all selections
checkComboOutputs.getCheckModel().getCheckedItems().addListener((ListChangeListener<InstanSegOutput>) c -> {
var list = c.getList();
if (list.isEmpty() || list.size() == checkComboOutputs.getItems().size())
checkComboOutputs.setTitle("All available");
else if (list.size() == 1) {
checkComboOutputs.setTitle(list.getFirst().toString());
} else {
checkComboOutputs.setTitle(list.size() + " selected");
}
});
}

private void configureDefaultValues() {
makeMeasurementsCheckBox.selectedProperty().bindBidirectional(InstanSegPreferences.makeMeasurementsProperty());
randomColorsCheckBox.selectedProperty().bindBidirectional(InstanSegPreferences.randomColorsProperty());
}

private BooleanBinding createModelDownloadedBinding() {
Expand Down Expand Up @@ -356,7 +381,7 @@ private static Collection<ChannelSelectItem> getAvailableChannels(ImageData<?> i
private void configureThreadSpinner() {
SpinnerValueFactory.IntegerSpinnerValueFactory factory = (SpinnerValueFactory.IntegerSpinnerValueFactory) threadSpinner.getValueFactory();
factory.setMax(Runtime.getRuntime().availableProcessors());
threadSpinner.getValueFactory().valueProperty().bindBidirectional(InstanSegPreferences.numThreadsProperty());
threadSpinner.getValueFactory().valueProperty().bindBidirectional(InstanSegPreferences.numThreadsProperty().asObject());
}

private void configureRunning() {
Expand Down Expand Up @@ -425,6 +450,10 @@ private void configureModelChoices() {
comboChannels.getCheckModel().clearChecks();
comboChannels.getCheckModel().checkIndices(0, 1, 2);
}
// Handle output channels
var nOutputs = n.getOutputChannels().orElse(1);
checkComboOutputs.getItems().setAll(InstanSegOutput.getOutputsForChannelCount(nOutputs));
checkComboOutputs.getCheckModel().checkAll();
});
downloadButton.setOnAction(e -> downloadModel());
WebView webView = WebViews.create(true);
Expand Down Expand Up @@ -735,7 +764,19 @@ protected Void call() {
Dialogs.showErrorNotification(resources.getString("title"), resources.getString("error.querying-local"));
return null;
}
var outputChannels = nucleiOnlyCheckBox.isSelected() ? new int[]{0} : new int[]{};
// TODO: HANDLE OUTPUT CHANNELS!
int nOutputs = model.getOutputChannels().orElse(1);
int[] outputChannels = new int[0];
if (nOutputs <= 0) {
logger.warn("Unknown output channels for {}", model);
nOutputs = 1;
}
int nChecked = checkComboOutputs.getCheckModel().getCheckedIndices().size();
if (nChecked > 0 && nChecked < nOutputs) {
outputChannels = checkComboOutputs.getCheckModel().getCheckedIndices().stream().mapToInt(Integer::intValue).toArray();
}
boolean makeMeasurements = makeMeasurementsCheckBox.isSelected();
boolean randomColors = randomColorsCheckBox.isSelected();

var instanSeg = InstanSeg.builder()
.model(model)
Expand All @@ -744,10 +785,10 @@ protected Void call() {
.outputChannels(outputChannels)
.tileDims(InstanSegPreferences.tileSizeProperty().get())
.taskRunner(taskRunner)
.makeMeasurements(makeMeasurementsCheckBox.isSelected())
.makeMeasurements(makeMeasurements)
.randomColors(randomColors)
.build();

boolean makeMeasurements = makeMeasurementsCheckBox.isSelected();
String cmd = String.format("""
qupath.ext.instanseg.core.InstanSeg.builder()
.modelPath("%s")
Expand All @@ -757,6 +798,7 @@ protected Void call() {
.tileDims(%d)
.nThreads(%d)
.makeMeasurements(%s)
.randomColors(%s)
.build()
.detectObjects()
""",
Expand All @@ -768,7 +810,8 @@ protected Void call() {
.collect(Collectors.joining(", ")),
InstanSegPreferences.tileSizeProperty().get(),
InstanSegPreferences.numThreadsProperty().getValue(),
makeMeasurements
makeMeasurements,
randomColors
).strip();
InstanSegResults results = instanSeg.detectObjects(imageData, selectedObjects);
imageData.getHierarchy().fireHierarchyChangedEvent(this);
Expand Down Expand Up @@ -940,4 +983,55 @@ private static List<GitHubAsset> getAssets(GitHubRelease release) {
}
return assets;
}

/**
* Helper class to manage the display of an output channel.
*/
private static class InstanSegOutput {

private final int index;
private final String name;

private static final List<InstanSegOutput> SINGLE_CHANNEL = List.of(
new InstanSegOutput(0, "Only channel")
);

// We have no way to query channel names currently - b
// but the first InstanSeg models with two channels are always in this order
private static final List<InstanSegOutput> TWO_CHANNEL = List.of(
new InstanSegOutput(0, "Channel 1 (Nuclei)"),
new InstanSegOutput(0, "Channel 2 (Cells)")
);

InstanSegOutput(int index, String name) {
this.index = index;
this.name = name;
}

private static List<InstanSegOutput> getOutputsForChannelCount(int nChannels) {
return switch (nChannels) {
case 0 -> List.of();
case 1 -> SINGLE_CHANNEL;
case 2 -> TWO_CHANNEL;
default ->
IntStream.range(0, nChannels).mapToObj(i -> new InstanSegOutput(i, "Channel " + (i + 1))).toList();
};
}

/**
* Get the index of the output channel.
* @return
*/
public int getIndex() {
return index;
}

@Override
public String toString() {
return name;
}

}


}
24 changes: 21 additions & 3 deletions src/main/java/qupath/ext/instanseg/ui/InstanSegPreferences.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package qupath.ext.instanseg.ui;

import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.Property;
Expand Down Expand Up @@ -32,14 +33,22 @@ enum OnlinePermission {
"instanseg.pref.device",
getDefaultDevice());

private static final Property<Integer> numThreadsProperty = PathPrefs.createPersistentPreference(
private static final IntegerProperty numThreadsProperty = PathPrefs.createPersistentPreference(
"instanseg.num.threads",
Math.min(4, Runtime.getRuntime().availableProcessors())).asObject();
Math.min(4, Runtime.getRuntime().availableProcessors()));

private static final IntegerProperty tileSizeProperty = PathPrefs.createPersistentPreference(
"intanseg.tile.size",
512);

private static final BooleanProperty makeMeasurementsProperty = PathPrefs.createPersistentPreference(
"intanseg.measurements",
true);

private static final BooleanProperty randomColorsProperty = PathPrefs.createPersistentPreference(
"intanseg.random.colors",
true);

/**
* MPS should work reliably (and much faster) on Apple Silicon, so set as default.
* Everywhere else, use CPU as we can't count on a GPU/CUDA being available.
Expand All @@ -65,11 +74,20 @@ static StringProperty preferredDeviceProperty() {
return preferredDeviceProperty;
}

static Property<Integer> numThreadsProperty() {
static IntegerProperty numThreadsProperty() {
return numThreadsProperty;
}

static IntegerProperty tileSizeProperty() {
return tileSizeProperty;
}

static BooleanProperty makeMeasurementsProperty() {
return makeMeasurementsProperty;
}

static BooleanProperty randomColorsProperty() {
return randomColorsProperty;
}

}
52 changes: 33 additions & 19 deletions src/main/resources/qupath/ext/instanseg/ui/instanseg_control.fxml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
<?import org.controlsfx.control.SearchableComboBox?>
<?import org.controlsfx.control.SegmentedButton?>

<fx:root maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefWidth="330" stylesheets="@instanseg.css" type="BorderPane" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0" xmlns="http://javafx.com/javafx/20.0.1" xmlns:fx="http://javafx.com/fxml/1">
<fx:root maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefWidth="330" stylesheets="@instanseg.css" type="BorderPane" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0" xmlns="http://javafx.com/javafx/22" xmlns:fx="http://javafx.com/fxml/1">
<center>
<VBox fx:id="vBox">
<TitledPane fx:id="pane1" animated="false" collapsible="false" prefHeight="262.0" prefWidth="330.0" styleClass="uncollapsible-titled-pane" text="%ui.processing.pane" VBox.vgrow="NEVER">
Expand Down Expand Up @@ -112,7 +112,7 @@
<children>
<Label styleClass="regular" text="%ui.options.device" />
<Pane minWidth="5" HBox.hgrow="ALWAYS" />
<ChoiceBox fx:id="deviceChoices">
<ChoiceBox fx:id="deviceChoices" prefWidth="75.0">
<tooltip><Tooltip text="%ui.options.device.tooltip" /></tooltip>
</ChoiceBox>
</children>
Expand All @@ -135,20 +135,6 @@
</Spinner>
</children>
</HBox>
<HBox alignment="CENTER_RIGHT" styleClass="standard-spacing">
<padding>
<Insets left="10" right="10" />
</padding>
<children>
<Label styleClass="regular" text="%ui.options.nuclei-only" />
<Pane minWidth="5" HBox.hgrow="ALWAYS" />
<CheckBox fx:id="nucleiOnlyCheckBox">
<tooltip>
<Tooltip text="%ui.options.nuclei-only.tooltip" />
</tooltip>
</CheckBox>
</children>
</HBox>
<!-- Tile Size Selection-->
<HBox alignment="CENTER_RIGHT" styleClass="standard-spacing">
<padding>
Expand All @@ -170,18 +156,31 @@
<Insets left="10" right="10" />
</padding>
<children>
<Label styleClass="regular" text="%ui.options.channel" />
<Label styleClass="regular" text="%ui.options.input-channels" />
<Pane minWidth="5" HBox.hgrow="ALWAYS" />
<CheckComboBox fx:id="comboChannels">
<tooltip>
<Tooltip text="%ui.options.channel.tooltip" />
<Tooltip text="%ui.options.input-channels.tooltip" />
</tooltip>
</CheckComboBox>
</children>
</HBox>
<HBox alignment="CENTER_RIGHT" styleClass="standard-spacing">
<padding>
<Insets bottom="10" left="10" right="10" />
<Insets left="10" right="10" />
</padding>
<children>
<Label styleClass="regular" text="%ui.options.output-channels" />
<Pane minWidth="5" HBox.hgrow="ALWAYS" />
<CheckComboBox id="checkComboOutputs" fx:id="checkComboOutputs">
<tooltip>
<Tooltip text="%ui.options.output-channels.tooltip" />
</tooltip></CheckComboBox>
</children>
</HBox>
<HBox alignment="CENTER_RIGHT" styleClass="standard-spacing">
<padding>
<Insets left="10.0" right="10.0" />
</padding>
<children>
<Label text="%ui.options.makeMeasurements" />
Expand All @@ -193,6 +192,21 @@
</CheckBox>
</children>
</HBox>
<HBox alignment="CENTER_RIGHT" styleClass="standard-spacing">
<padding>
<Insets bottom="5.0" left="10.0" right="10.0" />
</padding>
<children>
<Label text="%ui.options.randomColors">
</Label>
<Pane minWidth="5" HBox.hgrow="ALWAYS" />
<CheckBox fx:id="randomColorsCheckBox" styleClass="regular">
<tooltip>
<Tooltip text="%ui.options.randomColors.tooltip" />
</tooltip>
</CheckBox>
</children>
</HBox>



Expand Down
10 changes: 6 additions & 4 deletions src/main/resources/qupath/ext/instanseg/ui/strings.properties
Original file line number Diff line number Diff line change
Expand Up @@ -49,16 +49,18 @@ ui.selection.empty = No valid objects selected
ui.options.pane = Additional Options
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.tooltip = Segment nuclei only, or nuclei and cell boundaries
ui.options.output-channels = Outputs
ui.options.output-channels.tooltip = Choose which output channels to use, for InstanSeg models trained to give different outputs
ui.options.threads = Threads
ui.options.threads.tooltip = Define the preferred number of threads
ui.options.tilesize = Tile size
ui.options.tilesize.tooltip = Define the approximate tile size
ui.options.channel = Channels
ui.options.channel.tooltip = Define the set of channels to be used in inference
ui.options.input-channels = Input channels
ui.options.input-channels.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.randomColors = Random colors
ui.options.randomColors.tooltip = Assign random colors to detected objects
ui.options.noChannelSelected = No channels selected!
ui.options.oneChannelSelected = 1 channel selected
ui.options.nChannelSelected = %d channels selected
Expand Down

0 comments on commit 3388e23

Please sign in to comment.