Search in sources :

Example 1 with Histogram

use of qupath.lib.analysis.stats.Histogram in project qupath by qupath.

the class BrightnessContrastCommand method createDialog.

protected Stage createDialog() {
    if (!isInitialized())
        initializeSliders();
    initializePopup();
    changed(null, null, qupath.getImageData());
    BorderPane pane = new BorderPane();
    GridPane box = new GridPane();
    String blank = "      ";
    Label labelMin = new Label("Min display");
    // labelMin.setTooltip(new Tooltip("Set minimum lookup table value - double-click to edit manually"));
    Label labelMinValue = new Label(blank);
    labelMinValue.setTooltip(new Tooltip("Set minimum lookup table value - double-click to edit manually"));
    labelMinValue.textProperty().bind(Bindings.createStringBinding(() -> {
        // If value < 10, return 2 dp, otherwise 1 dp. Should be more useful for 16-/32-bit images.
        return sliderMin.getValue() < 10 ? String.format("%.2f", sliderMin.getValue()) : String.format("%.1f", sliderMin.getValue());
    }, table.getSelectionModel().selectedItemProperty(), sliderMin.valueProperty()));
    box.add(labelMin, 0, 0);
    box.add(sliderMin, 1, 0);
    box.add(labelMinValue, 2, 0);
    Label labelMax = new Label("Max display");
    labelMax.setTooltip(new Tooltip("Set maximum lookup table value - double-click to edit manually"));
    Label labelMaxValue = new Label(blank);
    labelMaxValue.setTooltip(new Tooltip("Set maximum lookup table value - double-click to edit manually"));
    labelMaxValue.textProperty().bind(Bindings.createStringBinding(() -> {
        // If value < 10, return 2 dp, otherwise 1 dp. Should be more useful for 16-/32-bit images.
        return sliderMax.getValue() < 10 ? String.format("%.2f", sliderMax.getValue()) : String.format("%.1f", sliderMax.getValue());
    }, table.getSelectionModel().selectedItemProperty(), sliderMax.valueProperty()));
    box.add(labelMax, 0, 1);
    box.add(sliderMax, 1, 1);
    box.add(labelMaxValue, 2, 1);
    box.setVgap(5);
    GridPane.setFillWidth(sliderMin, Boolean.TRUE);
    GridPane.setFillWidth(sliderMax, Boolean.TRUE);
    box.prefWidthProperty().bind(pane.widthProperty());
    box.setPadding(new Insets(5, 0, 5, 0));
    GridPane.setHgrow(sliderMin, Priority.ALWAYS);
    GridPane.setHgrow(sliderMax, Priority.ALWAYS);
    // In the absence of a better way, make it possible to enter display range values
    // manually by double-clicking on the corresponding label
    labelMinValue.setOnMouseClicked(e -> {
        if (e.getClickCount() == 2) {
            ChannelDisplayInfo infoVisible = getCurrentInfo();
            if (infoVisible == null)
                return;
            Double value = Dialogs.showInputDialog("Display range", "Set display range minimum", (double) infoVisible.getMinDisplay());
            if (value != null && !Double.isNaN(value)) {
                sliderMin.setValue(value);
                // Update display directly if out of slider range
                if (value < sliderMin.getMin() || value > sliderMin.getMax()) {
                    imageDisplay.setMinMaxDisplay(infoVisible, (float) value.floatValue(), (float) infoVisible.getMaxDisplay());
                    // infoVisible.setMinDisplay(value.floatValue());
                    // viewer.updateThumbnail();
                    updateSliders();
                    viewer.repaintEntireImage();
                }
            }
        }
    });
    labelMaxValue.setOnMouseClicked(e -> {
        if (e.getClickCount() == 2) {
            ChannelDisplayInfo infoVisible = getCurrentInfo();
            if (infoVisible == null)
                return;
            Double value = Dialogs.showInputDialog("Display range", "Set display range maximum", (double) infoVisible.getMaxDisplay());
            if (value != null && !Double.isNaN(value)) {
                sliderMax.setValue(value);
                // Update display directly if out of slider range
                if (value < sliderMax.getMin() || value > sliderMax.getMax()) {
                    imageDisplay.setMinMaxDisplay(infoVisible, (float) infoVisible.getMinDisplay(), (float) value.floatValue());
                    // infoVisible.setMaxDisplay(value.floatValue());
                    // viewer.updateThumbnail();
                    updateSliders();
                    viewer.repaintEntireImage();
                }
            }
        }
    });
    Button btnAuto = new Button("Auto");
    btnAuto.setOnAction(e -> {
        if (imageDisplay == null)
            return;
        // if (histogram == null)
        // return;
        // //				setSliders((float)histogram.getEdgeMin(), (float)histogram.getEdgeMax());
        ChannelDisplayInfo info = getCurrentInfo();
        double saturation = PathPrefs.autoBrightnessContrastSaturationPercentProperty().get() / 100.0;
        imageDisplay.autoSetDisplayRange(info, saturation);
        for (ChannelDisplayInfo info2 : table.getSelectionModel().getSelectedItems()) {
            imageDisplay.autoSetDisplayRange(info2, saturation);
        }
        updateSliders();
        handleSliderChange();
    });
    Button btnReset = new Button("Reset");
    btnReset.setOnAction(e -> {
        for (ChannelDisplayInfo info : table.getSelectionModel().getSelectedItems()) {
            imageDisplay.setMinMaxDisplay(info, info.getMinAllowed(), info.getMaxAllowed());
        }
        sliderMin.setValue(sliderMin.getMin());
        sliderMax.setValue(sliderMax.getMax());
    });
    Stage dialog = new Stage();
    dialog.initOwner(qupath.getStage());
    dialog.setTitle("Brightness & contrast");
    // Create color/channel display table
    table = new TableView<>(imageDisplay == null ? FXCollections.observableArrayList() : imageDisplay.availableChannels());
    table.setPlaceholder(new Text("No channels available"));
    table.addEventHandler(KeyEvent.KEY_PRESSED, new CopyTableListener());
    table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
    table.getSelectionModel().selectedItemProperty().addListener((v, o, n) -> {
        updateHistogram();
        updateSliders();
    });
    TableColumn<ChannelDisplayInfo, ChannelDisplayInfo> col1 = new TableColumn<>("Color");
    col1.setCellValueFactory(new Callback<CellDataFeatures<ChannelDisplayInfo, ChannelDisplayInfo>, ObservableValue<ChannelDisplayInfo>>() {

        @Override
        public ObservableValue<ChannelDisplayInfo> call(CellDataFeatures<ChannelDisplayInfo, ChannelDisplayInfo> p) {
            return new SimpleObjectProperty<ChannelDisplayInfo>(p.getValue());
        }
    });
    col1.setCellFactory(column -> {
        return new TableCell<ChannelDisplayInfo, ChannelDisplayInfo>() {

            @Override
            protected void updateItem(ChannelDisplayInfo item, boolean empty) {
                super.updateItem(item, empty);
                if (item == null || empty) {
                    setText(null);
                    setGraphic(null);
                    return;
                }
                setText(item.getName());
                Rectangle square = new Rectangle(0, 0, 10, 10);
                Integer rgb = item.getColor();
                if (rgb == null)
                    square.setFill(Color.TRANSPARENT);
                else
                    square.setFill(ColorToolsFX.getCachedColor(rgb));
                setGraphic(square);
            }
        };
    });
    col1.setSortable(false);
    TableColumn<ChannelDisplayInfo, Boolean> col2 = new TableColumn<>("Selected");
    col2.setCellValueFactory(new Callback<CellDataFeatures<ChannelDisplayInfo, Boolean>, ObservableValue<Boolean>>() {

        @Override
        public ObservableValue<Boolean> call(CellDataFeatures<ChannelDisplayInfo, Boolean> item) {
            SimpleBooleanProperty property = new SimpleBooleanProperty(imageDisplay.selectedChannels().contains(item.getValue()));
            // Remove repaint code here - now handled by table selection changes
            property.addListener((v, o, n) -> {
                imageDisplay.setChannelSelected(item.getValue(), n);
                table.refresh();
                Platform.runLater(() -> viewer.repaintEntireImage());
            });
            return property;
        }
    });
    col2.setCellFactory(column -> {
        CheckBoxTableCell<ChannelDisplayInfo, Boolean> cell = new CheckBoxTableCell<>();
        // Select cells when clicked - means a click anywhere within the row forces selection.
        // Previously, clicking within the checkbox didn't select the row.
        cell.addEventFilter(MouseEvent.MOUSE_CLICKED, e -> {
            if (e.isPopupTrigger())
                return;
            int ind = cell.getIndex();
            var tableView = cell.getTableView();
            if (ind < tableView.getItems().size()) {
                if (e.isShiftDown())
                    tableView.getSelectionModel().select(ind);
                else
                    tableView.getSelectionModel().clearAndSelect(ind);
                var channel = cell.getTableRow().getItem();
                // Handle clicks within the cell but outside the checkbox
                if (e.getTarget() == cell && channel != null && imageDisplay != null) {
                    updateDisplay(channel, !imageDisplay.selectedChannels().contains(channel));
                }
                e.consume();
            }
        });
        return cell;
    });
    col2.setSortable(false);
    col2.setEditable(true);
    col2.setResizable(false);
    // Handle color change requests when an appropriate row is double-clicked
    table.setRowFactory(tableView -> {
        TableRow<ChannelDisplayInfo> row = new TableRow<>();
        row.setOnMouseClicked(e -> {
            if (e.getClickCount() == 2) {
                ChannelDisplayInfo info = row.getItem();
                var imageData = viewer.getImageData();
                if (info instanceof DirectServerChannelInfo && imageData != null) {
                    DirectServerChannelInfo multiInfo = (DirectServerChannelInfo) info;
                    int c = multiInfo.getChannel();
                    var channel = imageData.getServer().getMetadata().getChannel(c);
                    Color color = ColorToolsFX.getCachedColor(multiInfo.getColor());
                    picker.setValue(color);
                    Dialog<ButtonType> colorDialog = new Dialog<>();
                    colorDialog.setTitle("Channel properties");
                    colorDialog.getDialogPane().getButtonTypes().setAll(ButtonType.APPLY, ButtonType.CANCEL);
                    var paneColor = new GridPane();
                    int r = 0;
                    var labelName = new Label("Channel name");
                    var tfName = new TextField(channel.getName());
                    labelName.setLabelFor(tfName);
                    PaneTools.addGridRow(paneColor, r++, 0, "Enter a name for the current channel", labelName, tfName);
                    var labelColor = new Label("Channel color");
                    labelColor.setLabelFor(picker);
                    PaneTools.addGridRow(paneColor, r++, 0, "Choose the color for the current channel", labelColor, picker);
                    paneColor.setVgap(5.0);
                    paneColor.setHgap(5.0);
                    colorDialog.getDialogPane().setContent(paneColor);
                    Optional<ButtonType> result = colorDialog.showAndWait();
                    if (result.orElse(ButtonType.CANCEL) == ButtonType.APPLY) {
                        // if (!DisplayHelpers.showMessageDialog("Choose channel color", picker))
                        // return;
                        String name = tfName.getText().trim();
                        if (name.isEmpty()) {
                            Dialogs.showErrorMessage("Set channel name", "The channel name must not be empty!");
                            return;
                        }
                        Color color2 = picker.getValue();
                        if (color == color2 && name.equals(channel.getName()))
                            return;
                        // Update the server metadata
                        int colorUpdated = ColorToolsFX.getRGB(color2);
                        if (imageData != null) {
                            var server = imageData.getServer();
                            var metadata = server.getMetadata();
                            var channels = new ArrayList<>(metadata.getChannels());
                            channels.set(c, ImageChannel.getInstance(name, colorUpdated));
                            var metadata2 = new ImageServerMetadata.Builder(metadata).channels(channels).build();
                            imageData.updateServerMetadata(metadata2);
                        }
                        // Update the display
                        multiInfo.setLUTColor((int) (color2.getRed() * 255), (int) (color2.getGreen() * 255), (int) (color2.getBlue() * 255));
                        // Add color property
                        imageDisplay.saveChannelColorProperties();
                        viewer.repaintEntireImage();
                        updateHistogram();
                        table.refresh();
                    }
                }
            }
        });
        return row;
    });
    table.getColumns().add(col1);
    table.getColumns().add(col2);
    table.setEditable(true);
    table.setColumnResizePolicy(TableView.UNCONSTRAINED_RESIZE_POLICY);
    // Hack... space for a scrollbar
    col1.prefWidthProperty().bind(table.widthProperty().subtract(col2.widthProperty()).subtract(25));
    // col2.setResizable(false);
    // col2.setMaxWidth(100);
    // col2.setPrefWidth(50);
    // col2.setResizable(false);
    // col1.prefWidthProperty().bind(table.widthProperty().multiply(0.7));
    // col2.prefWidthProperty().bind(table.widthProperty().multiply(0.2));
    // table.getColumnModel().getColumn(0).setCellRenderer(new ChannelCellRenderer());
    // table.getColumnModel().getColumn(1).setMaxWidth(table.getColumnModel().getColumn(1).getPreferredWidth());
    BorderPane panelColor = new BorderPane();
    // panelColor.setBorder(BorderFactory.createTitledBorder("Color display"));
    BorderPane paneTableAndFilter = new BorderPane(table);
    TextField tfFilter = new TextField("");
    tfFilter.textProperty().bindBidirectional(filterText);
    tfFilter.setTooltip(new Tooltip("Enter text to find specific channels by name"));
    tfFilter.setPromptText("Filter channels by name");
    paneTableAndFilter.setBottom(tfFilter);
    predicate.addListener((v, o, n) -> updatePredicate());
    panelColor.setCenter(paneTableAndFilter);
    CheckBox cbShowGrayscale = new CheckBox("Show grayscale");
    cbShowGrayscale.selectedProperty().bindBidirectional(showGrayscale);
    cbShowGrayscale.setTooltip(new Tooltip("Show single channel with grayscale lookup table"));
    if (imageDisplay != null)
        cbShowGrayscale.setSelected(!imageDisplay.useColorLUTs());
    showGrayscale.addListener(o -> {
        if (imageDisplay == null)
            return;
        Platform.runLater(() -> viewer.repaintEntireImage());
        table.refresh();
    });
    CheckBox cbKeepDisplaySettings = new CheckBox("Keep settings");
    cbKeepDisplaySettings.selectedProperty().bindBidirectional(PathPrefs.keepDisplaySettingsProperty());
    cbKeepDisplaySettings.setTooltip(new Tooltip("Retain same display settings where possible when opening similar images"));
    FlowPane paneCheck = new FlowPane();
    paneCheck.getChildren().add(cbShowGrayscale);
    paneCheck.getChildren().add(cbKeepDisplaySettings);
    paneCheck.setHgap(10);
    paneCheck.setPadding(new Insets(5, 0, 0, 0));
    panelColor.setBottom(paneCheck);
    pane.setCenter(panelColor);
    // Create brightness/contrast panel
    BorderPane panelSliders = new BorderPane();
    panelSliders.setTop(box);
    GridPane panelButtons = PaneTools.createColumnGridControls(btnAuto, btnReset);
    panelSliders.setBottom(panelButtons);
    panelSliders.setPadding(new Insets(5, 0, 5, 0));
    BorderPane panelMinMax = new BorderPane();
    // panelMinMax.setBorder(BorderFactory.createTitledBorder("Brightness/Contrast"));
    panelMinMax.setTop(panelSliders);
    histogramPanel.setShowTickLabels(false);
    histogramPanel.getChart().setAnimated(false);
    // histogramPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
    // panelMinMax.setCenter(histogramPanel.getChart());
    panelMinMax.setCenter(chartWrapper.getPane());
    chartWrapper.getPane().setPrefSize(200, 200);
    // histogramPanel.getChart().setPrefSize(200, 200);
    // histogramPanel.setPreferredSize(new Dimension(200, 120));
    pane.setBottom(panelMinMax);
    pane.setPadding(new Insets(10, 10, 10, 10));
    Scene scene = new Scene(pane, 350, 500);
    dialog.setScene(scene);
    dialog.setMinWidth(300);
    dialog.setMinHeight(400);
    dialog.setMaxWidth(600);
    dialog.setMaxHeight(800);
    updateTable();
    if (!table.getItems().isEmpty())
        table.getSelectionModel().select(0);
    updateDisplay(getCurrentInfo(), true);
    updateHistogram();
    updateSliders();
    // Update sliders when receiving focus - in case the display has been updated elsewhere
    dialog.focusedProperty().addListener((v, o, n) -> {
        if (n)
            updateSliders();
    });
    return dialog;
}
Also used : EventHandler(javafx.event.EventHandler) Button(javafx.scene.control.Button) ImageServer(qupath.lib.images.servers.ImageServer) LoggerFactory(org.slf4j.LoggerFactory) KeyCombination(javafx.scene.input.KeyCombination) ContextMenu(javafx.scene.control.ContextMenu) DirectServerChannelInfo(qupath.lib.display.DirectServerChannelInfo) TableView(javafx.scene.control.TableView) QuPathGUI(qupath.lib.gui.QuPathGUI) HistogramData(qupath.lib.gui.charts.HistogramPanelFX.HistogramData) TextField(javafx.scene.control.TextField) MenuItem(javafx.scene.control.MenuItem) BufferedImage(java.awt.image.BufferedImage) Predicate(java.util.function.Predicate) ObjectBinding(javafx.beans.binding.ObjectBinding) FilteredList(javafx.collections.transformation.FilteredList) Set(java.util.Set) Rectangle(javafx.scene.shape.Rectangle) ChannelDisplayInfo(qupath.lib.display.ChannelDisplayInfo) KeyEvent(javafx.scene.input.KeyEvent) Collectors(java.util.stream.Collectors) CellDataFeatures(javafx.scene.control.TableColumn.CellDataFeatures) Platform(javafx.application.Platform) Text(javafx.scene.text.Text) QuPathViewer(qupath.lib.gui.viewer.QuPathViewer) Priority(javafx.scene.layout.Priority) List(java.util.List) BooleanProperty(javafx.beans.property.BooleanProperty) PropertyChangeListener(java.beans.PropertyChangeListener) Clipboard(javafx.scene.input.Clipboard) FlowPane(javafx.scene.layout.FlowPane) CheckBoxTableCell(javafx.scene.control.cell.CheckBoxTableCell) ColorToolsFX(qupath.lib.gui.tools.ColorToolsFX) Optional(java.util.Optional) NumberAxis(javafx.scene.chart.NumberAxis) ClipboardContent(javafx.scene.input.ClipboardContent) BorderPane(javafx.scene.layout.BorderPane) StringProperty(javafx.beans.property.StringProperty) Scene(javafx.scene.Scene) TextArea(javafx.scene.control.TextArea) SimpleStringProperty(javafx.beans.property.SimpleStringProperty) ButtonType(javafx.scene.control.ButtonType) MouseEvent(javafx.scene.input.MouseEvent) FXCollections(javafx.collections.FXCollections) ImageChannel(qupath.lib.images.servers.ImageChannel) Bindings(javafx.beans.binding.Bindings) ImageDisplay(qupath.lib.display.ImageDisplay) ArrayList(java.util.ArrayList) TableColumn(javafx.scene.control.TableColumn) HashSet(java.util.HashSet) Dialogs(qupath.lib.gui.dialogs.Dialogs) TableCell(javafx.scene.control.TableCell) Insets(javafx.geometry.Insets) Slider(javafx.scene.control.Slider) HistogramPanelFX(qupath.lib.gui.charts.HistogramPanelFX) Callback(javafx.util.Callback) Tooltip(javafx.scene.control.Tooltip) PropertyChangeEvent(java.beans.PropertyChangeEvent) LinkedHashSet(java.util.LinkedHashSet) GridPane(javafx.scene.layout.GridPane) ColorPicker(javafx.scene.control.ColorPicker) ImageData(qupath.lib.images.ImageData) KeyCode(javafx.scene.input.KeyCode) Color(javafx.scene.paint.Color) Logger(org.slf4j.Logger) Dialog(javafx.scene.control.Dialog) Label(javafx.scene.control.Label) TableRow(javafx.scene.control.TableRow) DecimalFormat(java.text.DecimalFormat) CheckBox(javafx.scene.control.CheckBox) ThresholdedChartWrapper(qupath.lib.gui.charts.HistogramPanelFX.ThresholdedChartWrapper) KeyCodeCombination(javafx.scene.input.KeyCodeCombination) SimpleBooleanProperty(javafx.beans.property.SimpleBooleanProperty) SelectionMode(javafx.scene.control.SelectionMode) Stage(javafx.stage.Stage) SimpleObjectProperty(javafx.beans.property.SimpleObjectProperty) ObservableValue(javafx.beans.value.ObservableValue) Histogram(qupath.lib.analysis.stats.Histogram) ChangeListener(javafx.beans.value.ChangeListener) PathPrefs(qupath.lib.gui.prefs.PathPrefs) ImageServerMetadata(qupath.lib.images.servers.ImageServerMetadata) PaneTools(qupath.lib.gui.tools.PaneTools) CellDataFeatures(javafx.scene.control.TableColumn.CellDataFeatures) Label(javafx.scene.control.Label) ObservableValue(javafx.beans.value.ObservableValue) Rectangle(javafx.scene.shape.Rectangle) ArrayList(java.util.ArrayList) DirectServerChannelInfo(qupath.lib.display.DirectServerChannelInfo) Button(javafx.scene.control.Button) Dialog(javafx.scene.control.Dialog) Stage(javafx.stage.Stage) TextField(javafx.scene.control.TextField) FlowPane(javafx.scene.layout.FlowPane) ButtonType(javafx.scene.control.ButtonType) GridPane(javafx.scene.layout.GridPane) Tooltip(javafx.scene.control.Tooltip) Color(javafx.scene.paint.Color) Scene(javafx.scene.Scene) TableColumn(javafx.scene.control.TableColumn) CheckBox(javafx.scene.control.CheckBox) TableRow(javafx.scene.control.TableRow) BorderPane(javafx.scene.layout.BorderPane) SimpleBooleanProperty(javafx.beans.property.SimpleBooleanProperty) Insets(javafx.geometry.Insets) CheckBoxTableCell(javafx.scene.control.cell.CheckBoxTableCell) TableCell(javafx.scene.control.TableCell) CheckBoxTableCell(javafx.scene.control.cell.CheckBoxTableCell) Text(javafx.scene.text.Text) ChannelDisplayInfo(qupath.lib.display.ChannelDisplayInfo)

Example 2 with Histogram

use of qupath.lib.analysis.stats.Histogram in project qupath by qupath.

the class HistogramDisplay method setHistogram.

void setHistogram(final PathTableData<?> model, final String columnName) {
    if (model != null && model.getMeasurementNames().contains(columnName)) {
        double[] values = model.getDoubleValues(columnName);
        int nBins = params.getIntParameterValue("nBins");
        if (nBins < 2)
            nBins = 2;
        else if (nBins > 1000)
            nBins = 1000;
        // We can have values in the 'wrong' order to facilitate comparison...
        Arrays.sort(values);
        // Check if we've actually changed anything - if not, then abort
        if (nBins == currentBins && currentValues != null && Arrays.equals(currentValues, values))
            return;
        Histogram histogram = new Histogram(values, nBins);
        // histogram.setNormalizeCounts(params.getBooleanParameterValue("normalizeCounts"));
        HistogramData histogramData = HistogramPanelFX.createHistogramData(histogram, false, (Integer) null);
        histogramData.setNormalizeCounts(params.getBooleanParameterValue("normalizeCounts"));
        panelHistogram.getHistogramData().setAll(histogramData);
        AreaChart<Number, Number> chart = panelHistogram.getChart();
        chart.setVerticalGridLinesVisible(true);
        chart.setHorizontalGridLinesVisible(true);
        chart.setLegendVisible(false);
        // Can't stop them being orange...
        chart.setCreateSymbols(false);
        chart.getXAxis().setLabel("Values");
        chart.getYAxis().setLabel("Counts");
        chart.getYAxis().setTickLabelsVisible(true);
        chart.getYAxis().setTickMarkVisible(true);
        chart.getXAxis().setTickLabelsVisible(true);
        chart.getXAxis().setTickMarkVisible(true);
        chart.setAnimated(params.getBooleanParameterValue("animate"));
        updateTable(histogram);
        currentColumn = columnName;
        currentBins = nBins;
        currentValues = values;
        this.model = model;
    } else
        panelHistogram.getHistogramData().clear();
}
Also used : Histogram(qupath.lib.analysis.stats.Histogram) HistogramData(qupath.lib.gui.charts.HistogramPanelFX.HistogramData)

Example 3 with Histogram

use of qupath.lib.analysis.stats.Histogram in project qupath by qupath.

the class KaplanMeierDisplay method generatePlot.

@SuppressWarnings("unchecked")
private void generatePlot() {
    KaplanMeierDisplay.ScoreData newScoreData = scoreData;
    // If we have a hierarchy, update the scores with the most recent data
    if (hierarchy != null) {
        List<TMACoreObject> cores = PathObjectTools.getTMACoreObjects(hierarchy, false);
        double[] survival = new double[cores.size()];
        boolean[] censored = new boolean[cores.size()];
        double[] scores = new double[cores.size()];
        // scoreColumn = "RoughScore";
        for (int i = 0; i < cores.size(); i++) {
            TMACoreObject core = cores.get(i);
            MeasurementList ml = core.getMeasurementList();
            survival[i] = core.getMeasurementList().getMeasurementValue(survivalColumn);
            double censoredValue = core.getMeasurementList().getMeasurementValue(censoredColumn);
            boolean hasCensoredValue = !Double.isNaN(censoredValue) && (censoredValue == 0 || censoredValue == 1);
            censored[i] = censoredValue != 0;
            if (!hasCensoredValue) {
                // If we don't have a censored value, ensure we mask out everything else
                scores[i] = Double.NaN;
                survival[i] = Double.NaN;
            } else if (ml.containsNamedMeasurement(scoreColumn))
                // Get the score if we can
                scores[i] = ml.getMeasurementValue(scoreColumn);
            else {
                // // Try to compute score if we need to
                // Map<String, Number> map = ROIMeaningfulMeasurements.getPathClassSummaryMeasurements(core.getChildObjects(), true);
                // Number value = map.get(scoreColumn);
                // if (value == null)
                scores[i] = Double.NaN;
            // else
            // scores[i] = value.doubleValue();
            }
        }
        // Mask out any scores that don't have associated survival data
        for (int i = 0; i < survival.length; i++) {
            if (Double.isNaN(survival[i]))
                scores[i] = Double.NaN;
        }
        newScoreData = new ScoreData(scores, survival, censored);
    }
    if (newScoreData == null || newScoreData.scores.length == 0)
        return;
    // KaplanMeier kmHigh = new KaplanMeier("Above threshold");
    // KaplanMeier kmLow = new KaplanMeier("Below threshold");
    double[] quartiles = StatisticsHelper.getQuartiles(newScoreData.scores);
    double q1 = quartiles[0];
    double median = quartiles[1];
    double q3 = quartiles[2];
    double[] thresholds;
    if (params != null) {
        Object thresholdMethod = params.getChoiceParameterValue("scoreThresholdMethod");
        if (thresholdMethod.equals("Median")) {
            // panelParams.setNumericParameterValue("scoreThreshold", median);
            // ((DoubleParameter)params.getParameters().get("scoreThreshold")).setValue(median); // TODO: UPDATE DIALOG!
            thresholds = new double[] { median };
        } else if (thresholdMethod.equals("Tertiles")) {
            // ((DoubleParameter)params.getParameters().get("scoreThreshold")).setValue(median); // TODO: UPDATE DIALOG!
            thresholds = StatisticsHelper.getTertiles(newScoreData.scores);
        } else if (thresholdMethod.equals("Quartiles")) {
            // ((DoubleParameter)params.getParameters().get("scoreThreshold")).setValue(median); // TODO: UPDATE DIALOG!
            thresholds = new double[] { q1, median, q3 };
        } else if (thresholdMethod.equals("Manual (1)")) {
            thresholds = new double[] { params.getDoubleParameterValue("threshold1") };
        } else if (thresholdMethod.equals("Manual (2)")) {
            thresholds = new double[] { params.getDoubleParameterValue("threshold1"), params.getDoubleParameterValue("threshold2") };
        } else
            // if (thresholdMethod.equals("Manual (3)")) {
            thresholds = new double[] { params.getDoubleParameterValue("threshold1"), params.getDoubleParameterValue("threshold2"), params.getDoubleParameterValue("threshold3") };
    } else
        thresholds = new double[] { median };
    double minVal = Double.POSITIVE_INFINITY;
    double maxVal = Double.NEGATIVE_INFINITY;
    int numNonNaN = 0;
    for (double d : newScoreData.scores) {
        if (Double.isNaN(d))
            continue;
        if (d < minVal)
            minVal = d;
        if (d > maxVal)
            maxVal = d;
        numNonNaN++;
    }
    // If not this, we don't have valid scores that we can work with
    boolean scoresValid = maxVal > minVal;
    double maxTimePoint = 0;
    for (double d : newScoreData.survival) {
        if (Double.isNaN(d))
            continue;
        if (d > maxTimePoint)
            maxTimePoint = d;
    }
    if (panelParams != null && maxTimePoint > ((IntParameter) params.getParameters().get("censorTimePoints")).getUpperBound()) {
        panelParams.setNumericParameterValueRange("censorTimePoints", 0, Math.ceil(maxTimePoint));
    }
    // Optionally censor at specified time
    double censorThreshold = params == null ? maxTimePoint : params.getIntParameterValue("censorTimePoints");
    // Compute log-rank p-values for *all* possible thresholds
    // Simultaneously determine the threshold that yields the lowest p-value,
    // resolving ties in favour of a more even split between high/low numbers of events
    boolean pValuesChanged = false;
    if (calculateAllPValues) {
        if (!(pValues != null && pValueThresholds != null && newScoreData.equals(scoreData) && censorThreshold == lastPValueCensorThreshold)) {
            Map<Double, Double> mapLogRank = new TreeMap<>();
            Set<Double> setObserved = new HashSet<>();
            for (int i = 0; i < newScoreData.scores.length; i++) {
                Double d = newScoreData.scores[i];
                boolean observed = !newScoreData.censored[i] && newScoreData.survival[i] < censorThreshold;
                if (observed)
                    setObserved.add(d);
                if (mapLogRank.containsKey(d))
                    continue;
                List<KaplanMeierData> kmsTemp = splitByThresholds(newScoreData, new double[] { d }, censorThreshold, false);
                // if (kmsTemp.get(1).nObserved() == 0 || kmsTemp.get(1).nObserved() == 0)
                // continue;
                LogRankResult test = LogRankTest.computeLogRankTest(kmsTemp.get(0), kmsTemp.get(1));
                double pValue = test.getPValue();
                // double pValue = test.hazardRatio < 1 ? test.hazardRatio : 1.0/test.hazardRatio; // Checking usefulness of Hazard ratios...
                if (!Double.isFinite(pValue))
                    continue;
                // if (!Double.isFinite(test.getHazardRatio())) {
                // //						continue;
                // pValue = Double.NaN;
                // }
                mapLogRank.put(d, pValue);
            }
            pValueThresholds = new double[mapLogRank.size()];
            pValues = new double[mapLogRank.size()];
            pValueThresholdsObserved = new boolean[mapLogRank.size()];
            int count = 0;
            for (Entry<Double, Double> entry : mapLogRank.entrySet()) {
                pValueThresholds[count] = entry.getKey();
                pValues[count] = entry.getValue();
                if (setObserved.contains(entry.getKey()))
                    pValueThresholdsObserved[count] = true;
                count++;
            }
            // Find the longest 'significant' stretch
            int maxSigCount = 0;
            int maxSigInd = -1;
            int sigCurrent = 0;
            int[] sigCount = new int[pValues.length];
            for (int i = 0; i < pValues.length; i++) {
                if (pValues[i] < 0.05) {
                    sigCurrent++;
                    sigCount[i] = sigCurrent;
                    if (sigCurrent > maxSigCount) {
                        maxSigCount = sigCurrent;
                        maxSigInd = i;
                    }
                } else
                    sigCurrent = 0;
            }
            if (maxSigCount == 0) {
                logger.info("No p-values < 0.05");
            } else {
                double minThresh = maxSigInd - maxSigCount < 0 ? pValueThresholds[0] - 0.0000001 : pValueThresholds[maxSigInd - maxSigCount];
                double maxThresh = pValueThresholds[maxSigInd];
                int nBetween = 0;
                int nBetweenObserved = 0;
                for (int i = 0; i < newScoreData.scores.length; i++) {
                    if (newScoreData.scores[i] > minThresh && newScoreData.scores[i] <= maxThresh) {
                        nBetween++;
                        if (newScoreData.survival[i] < censorThreshold && !newScoreData.censored[i])
                            nBetweenObserved++;
                    }
                }
                logger.info("Longest stretch of p-values < 0.05: {} - {} ({} entries, {} observed)", minThresh, maxThresh, nBetween, nBetweenObserved);
            }
            pValuesSmoothed = new double[pValues.length];
            Arrays.fill(pValuesSmoothed, Double.NaN);
            int n = (pValues.length / 20) * 2 + 1;
            logger.info("Smoothing log-rank test p-values by " + n);
            for (int i = n / 2; i < pValues.length - n / 2; i++) {
                double sum = 0;
                for (int k = i - n / 2; k < i - n / 2 + n; k++) {
                    sum += pValues[k];
                }
                pValuesSmoothed[i] = sum / n;
            }
            // for (int i = 0; i < pValues.length; i++) {
            // double sum = 0;
            // for (int k = Math.max(0, i-n/2); k < Math.min(pValues.length, i-n/2+n); k++) {
            // sum += pValues[k];
            // }
            // pValuesSmoothed[i] = sum/n;
            // }
            // pValues = pValuesSmoothed;
            lastPValueCensorThreshold = censorThreshold;
            pValuesChanged = true;
        }
    } else {
        lastPValueCensorThreshold = Double.NaN;
        pValueThresholds = null;
        pValues = null;
    }
    // if (params != null && !Double.isNaN(bestThreshold) && (params.getChoiceParameterValue("scoreThresholdMethod").equals("Lowest p-value")))
    if (params != null && (params.getChoiceParameterValue("scoreThresholdMethod").equals("Lowest p-value"))) {
        int bestIdx = -1;
        double bestPValue = Double.POSITIVE_INFINITY;
        for (int i = pValueThresholds.length / 10; i < pValueThresholds.length * 9 / 10; i++) {
            if (pValues[i] < bestPValue) {
                bestIdx = i;
                bestPValue = pValues[i];
            }
        }
        thresholds = bestIdx >= 0 ? new double[] { pValueThresholds[bestIdx] } : new double[0];
    } else if (params != null && (params.getChoiceParameterValue("scoreThresholdMethod").equals("Lowest smoothed p-value"))) {
        int bestIdx = -1;
        double bestPValue = Double.POSITIVE_INFINITY;
        for (int i = pValueThresholds.length / 10; i < pValueThresholds.length * 9 / 10; i++) {
            if (pValuesSmoothed[i] < bestPValue) {
                bestIdx = i;
                bestPValue = pValuesSmoothed[i];
            }
        }
        thresholds = bestIdx >= 0 ? new double[] { pValueThresholds[bestIdx] } : new double[0];
    }
    // Split into different curves using the provided thresholds
    List<KaplanMeierData> kms = splitByThresholds(newScoreData, thresholds, censorThreshold, params != null && "Quartiles".equals(params.getChoiceParameterValue("scoreThresholdMethod")));
    if (plotter == null) {
        plotter = new KaplanMeierChartWrapper(survivalColumn + " time");
    // plotter.setBorder(BorderFactory.createTitledBorder("Survival plot"));
    // plotter.getCanvas().setWidth(300);
    // plotter.getCanvas().setHeight(300);
    }
    KaplanMeierData[] kmArray = new KaplanMeierData[kms.size()];
    plotter.setKaplanMeierCurves(survivalColumn + " time", kms.toArray(kmArray));
    tableModel.setSurvivalCurves(thresholds, params != null && params.getChoiceParameterValue("scoreThresholdMethod").equals("Lowest p-value"), kmArray);
    // Bar width determined using 'Freedman and Diaconis' rule' (but overridden if this gives < 16 bins...)
    double barWidth = (2 * q3 - q1) * Math.pow(numNonNaN, -1.0 / 3.0);
    int nBins = 100;
    if (!Double.isNaN(barWidth))
        barWidth = (int) Math.max(16, Math.ceil((maxVal - minVal) / barWidth));
    Histogram histogram = scoresValid ? new Histogram(newScoreData.scores, nBins) : null;
    if (histogramPanel == null) {
        GridPane paneHistogram = new GridPane();
        histogramPanel = new HistogramPanelFX();
        histogramPanel.getChart().setAnimated(false);
        histogramWrapper = new ThresholdedChartWrapper(histogramPanel.getChart());
        for (ObservableNumberValue val : threshProperties) histogramWrapper.addThreshold(val, ColorToolsFX.getCachedColor(240, 0, 0, 128));
        histogramWrapper.getPane().setPrefHeight(150);
        paneHistogram.add(histogramWrapper.getPane(), 0, 0);
        Tooltip.install(histogramPanel.getChart(), new Tooltip("Distribution of scores"));
        GridPane.setHgrow(histogramWrapper.getPane(), Priority.ALWAYS);
        GridPane.setVgrow(histogramWrapper.getPane(), Priority.ALWAYS);
        NumberAxis xAxis = new NumberAxis();
        xAxis.setLabel("Score threshold");
        NumberAxis yAxis = new NumberAxis();
        yAxis.setLowerBound(0);
        yAxis.setUpperBound(1);
        yAxis.setTickUnit(0.1);
        yAxis.setAutoRanging(false);
        yAxis.setLabel("P-value");
        chartPValues = new LineChart<>(xAxis, yAxis);
        chartPValues.setAnimated(false);
        chartPValues.setLegendVisible(false);
        // Make chart so it can be navigated
        ChartTools.makeChartInteractive(chartPValues, xAxis, yAxis);
        pValuesChanged = true;
        Tooltip.install(chartPValues, new Tooltip("Distribution of p-values (log-rank test) comparing low vs. high for all possible score thresholds"));
        // chartPValues.getYAxis().setAutoRanging(false);
        pValuesWrapper = new ThresholdedChartWrapper(chartPValues);
        for (ObservableNumberValue val : threshProperties) pValuesWrapper.addThreshold(val, ColorToolsFX.getCachedColor(240, 0, 0, 128));
        pValuesWrapper.getPane().setPrefHeight(150);
        paneHistogram.add(pValuesWrapper.getPane(), 0, 1);
        GridPane.setHgrow(pValuesWrapper.getPane(), Priority.ALWAYS);
        GridPane.setVgrow(pValuesWrapper.getPane(), Priority.ALWAYS);
        ContextMenu popup = new ContextMenu();
        ChartTools.addChartExportMenu(chartPValues, popup);
        RadioMenuItem miZoomY1 = new RadioMenuItem("0-1");
        miZoomY1.setOnAction(e -> {
            yAxis.setAutoRanging(false);
            yAxis.setUpperBound(1);
            yAxis.setTickUnit(0.2);
        });
        RadioMenuItem miZoomY05 = new RadioMenuItem("0-0.5");
        miZoomY05.setOnAction(e -> {
            yAxis.setAutoRanging(false);
            yAxis.setUpperBound(0.5);
            yAxis.setTickUnit(0.1);
        });
        RadioMenuItem miZoomY02 = new RadioMenuItem("0-0.2");
        miZoomY02.setOnAction(e -> {
            yAxis.setAutoRanging(false);
            yAxis.setUpperBound(0.2);
            yAxis.setTickUnit(0.05);
        });
        RadioMenuItem miZoomY01 = new RadioMenuItem("0-0.1");
        miZoomY01.setOnAction(e -> {
            yAxis.setAutoRanging(false);
            yAxis.setUpperBound(0.1);
            yAxis.setTickUnit(0.05);
        });
        RadioMenuItem miZoomY005 = new RadioMenuItem("0-0.05");
        miZoomY005.setOnAction(e -> {
            yAxis.setAutoRanging(false);
            yAxis.setUpperBound(0.05);
            yAxis.setTickUnit(0.01);
        });
        RadioMenuItem miZoomY001 = new RadioMenuItem("0-0.01");
        miZoomY001.setOnAction(e -> {
            yAxis.setAutoRanging(false);
            yAxis.setUpperBound(0.01);
            yAxis.setTickUnit(0.005);
        });
        ToggleGroup tgZoom = new ToggleGroup();
        miZoomY1.setToggleGroup(tgZoom);
        miZoomY05.setToggleGroup(tgZoom);
        miZoomY02.setToggleGroup(tgZoom);
        miZoomY01.setToggleGroup(tgZoom);
        miZoomY005.setToggleGroup(tgZoom);
        miZoomY001.setToggleGroup(tgZoom);
        Menu menuZoomY = new Menu("Set y-axis range");
        menuZoomY.getItems().addAll(miZoomY1, miZoomY05, miZoomY02, miZoomY01, miZoomY005, miZoomY001);
        MenuItem miCopyData = new MenuItem("Copy chart data");
        miCopyData.setOnAction(e -> {
            String dataString = ChartTools.getChartDataAsString(chartPValues);
            ClipboardContent content = new ClipboardContent();
            content.putString(dataString);
            Clipboard.getSystemClipboard().setContent(content);
        });
        popup.getItems().addAll(miCopyData, menuZoomY);
        chartPValues.setOnContextMenuRequested(e -> {
            popup.show(chartPValues, e.getScreenX(), e.getScreenY());
        });
        for (int col = 0; col < tableModel.getColumnCount(); col++) {
            TableColumn<Integer, String> column = new TableColumn<>(tableModel.getColumnName(col));
            int colNumber = col;
            column.setCellValueFactory(new Callback<CellDataFeatures<Integer, String>, ObservableValue<String>>() {

                @Override
                public ObservableValue<String> call(CellDataFeatures<Integer, String> p) {
                    return new SimpleStringProperty((String) tableModel.getValueAt(p.getValue(), colNumber));
                }
            });
            column.setCellFactory(new Callback<TableColumn<Integer, String>, TableCell<Integer, String>>() {

                @Override
                public TableCell<Integer, String> call(TableColumn<Integer, String> param) {
                    TableCell<Integer, String> cell = new TableCell<Integer, String>() {

                        @Override
                        protected void updateItem(String item, boolean empty) {
                            super.updateItem(item, empty);
                            setText(item);
                            setTooltip(new Tooltip(item));
                        }
                    };
                    return cell;
                }
            });
            table.getColumns().add(column);
        }
        table.setPrefHeight(250);
        table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
        table.maxHeightProperty().bind(table.prefHeightProperty());
        params = new ParameterList();
        // maxTimePoint = 0;
        // for (TMACoreObject core : hierarchy.getTMAGrid().getTMACoreList()) {
        // double os = core.getMeasurementList().getMeasurementValue(TMACoreObject.KEY_OVERALL_SURVIVAL);
        // double rfs = core.getMeasurementList().getMeasurementValue(TMACoreObject.KEY_RECURRENCE_FREE_SURVIVAL);
        // if (os > maxTimePoint)
        // maxTimePoint = os;
        // if (rfs > maxTimePoint)
        // maxTimePoint = rfs;
        // }
        params.addIntParameter("censorTimePoints", "Max censored time", (int) (censorThreshold + 0.5), null, 0, (int) Math.ceil(maxTimePoint), "Latest time point beyond which data will be censored");
        // params.addChoiceParameter("scoreThresholdMethod", "Threshold method", "Manual", Arrays.asList("Manual", "Median", "Log-rank test"));
        if (calculateAllPValues)
            // Don't include "Lowest smoothed p-value" - it's not an established method and open to misinterpretation...
            params.addChoiceParameter("scoreThresholdMethod", "Threshold method", "Median", Arrays.asList("Manual (1)", "Manual (2)", "Manual (3)", "Median", "Tertiles", "Quartiles", "Lowest p-value"));
        else
            // params.addChoiceParameter("scoreThresholdMethod", "Threshold method", "Median", Arrays.asList("Manual (1)", "Manual (2)", "Manual (3)", "Median", "Tertiles", "Quartiles", "Lowest p-value", "Lowest smoothed p-value"));
            params.addChoiceParameter("scoreThresholdMethod", "Threshold method", "Median", Arrays.asList("Manual (1)", "Manual (2)", "Manual (3)", "Median", "Tertiles", "Quartiles"));
        params.addDoubleParameter("threshold1", "Threshold 1", thresholds.length > 0 ? thresholds[0] : (minVal + maxVal) / 2, null, "Threshold to distinguish between patient groups");
        params.addDoubleParameter("threshold2", "Threshold 2", thresholds.length > 1 ? thresholds[1] : (minVal + maxVal) / 2, null, "Threshold to distinguish between patient groups");
        params.addDoubleParameter("threshold3", "Threshold 3", thresholds.length > 2 ? thresholds[2] : (minVal + maxVal) / 2, null, "Threshold to distinguish between patient groups");
        params.addBooleanParameter("showAtRisk", "Show at risk", plotter.getShowAtRisk(), "Show number of patients at risk below the plot");
        params.addBooleanParameter("showTicks", "Show censored ticks", plotter.getShowCensoredTicks(), "Show ticks to indicate censored data");
        params.addBooleanParameter("showKey", "Show key", plotter.getShowKey(), "Show key indicating display of each curve");
        // Hide threshold parameters if threshold can't be used
        if (!scoresValid) {
            // params.setHiddenParameters(true, "scoreThresholdMethod", "scoreThreshold");
            histogramPanel.getChart().setVisible(false);
        }
        panelParams = new ParameterPanelFX(params);
        panelParams.addParameterChangeListener(this);
        updateThresholdsEnabled();
        for (int i = 0; i < threshProperties.length; i++) {
            String p = "threshold" + (i + 1);
            threshProperties[i].addListener((v, o, n) -> {
                if (interactiveThresholds()) {
                    // Need to do a decent double check with tolerance to text field value changing while typing
                    if (!GeneralTools.almostTheSame(params.getDoubleParameterValue(p), n.doubleValue(), 0.0001))
                        panelParams.setNumericParameterValue(p, n);
                }
            });
        }
        BorderPane paneBottom = new BorderPane();
        TitledPane paneOptions = new TitledPane("Options", panelParams.getPane());
        // paneOptions.setCollapsible(false);
        Pane paneCanvas = new StackPane();
        paneCanvas.getChildren().add(plotter.getCanvas());
        GridPane paneLeft = new GridPane();
        paneLeft.add(paneOptions, 0, 0);
        paneLeft.add(table, 0, 1);
        GridPane.setHgrow(paneOptions, Priority.ALWAYS);
        GridPane.setHgrow(table, Priority.ALWAYS);
        paneBottom.setLeft(paneLeft);
        paneBottom.setCenter(paneHistogram);
        paneMain.setCenter(paneCanvas);
        paneMain.setBottom(paneBottom);
        paneMain.setPadding(new Insets(10, 10, 10, 10));
    } else if (thresholds.length > 0) {
        // Ensure the sliders/text fields are set sensibly
        if (!GeneralTools.almostTheSame(thresholds[0], params.getDoubleParameterValue("threshold1"), 0.0001)) {
            panelParams.setNumericParameterValue("threshold1", thresholds[0]);
        }
        if (thresholds.length > 1 && !GeneralTools.almostTheSame(thresholds[1], params.getDoubleParameterValue("threshold2"), 0.0001)) {
            panelParams.setNumericParameterValue("threshold2", thresholds[1]);
        }
        if (thresholds.length > 2 && !GeneralTools.almostTheSame(thresholds[2], params.getDoubleParameterValue("threshold3"), 0.0001)) {
            panelParams.setNumericParameterValue("threshold3", thresholds[2]);
        }
    }
    if (histogram != null) {
        histogramPanel.getHistogramData().setAll(HistogramPanelFX.createHistogramData(histogram, false, (Color) null));
        histogramPanel.getChart().getXAxis().setLabel(scoreColumn);
        histogramPanel.getChart().getYAxis().setLabel("Count");
        ChartTools.addChartExportMenu(histogramPanel.getChart(), null);
    // histogramWrapper.setVerticalLines(thresholds, ColorToolsFX.getCachedColor(240, 0, 0, 128));
    // Deal with threshold adjustment
    // histogramWrapper.getThresholds().addListener((Observable o) -> generatePlot());
    }
    if (pValues != null) {
        // TODO: Raise earlier where p-value calculation is
        if (pValuesChanged) {
            ObservableList<XYChart.Data<Number, Number>> data = FXCollections.observableArrayList();
            for (int i = 0; i < pValueThresholds.length; i++) {
                double pValue = pValues[i];
                if (Double.isNaN(pValue))
                    continue;
                data.add(new XYChart.Data<>(pValueThresholds[i], pValue, pValueThresholdsObserved[i]));
            }
            ObservableList<XYChart.Data<Number, Number>> dataSmoothed = null;
            if (pValuesSmoothed != null) {
                dataSmoothed = FXCollections.observableArrayList();
                for (int i = 0; i < pValueThresholds.length; i++) {
                    double pValueSmoothed = pValuesSmoothed[i];
                    if (Double.isNaN(pValueSmoothed))
                        continue;
                    dataSmoothed.add(new XYChart.Data<>(pValueThresholds[i], pValueSmoothed));
                }
            }
            // Don't bother showing the smoothed data... it tends to get in the way...
            // if (dataSmoothed != null)
            // chartPValues.getData().setAll(new XYChart.Series<>("P-values", data), new XYChart.Series<>("Smoothed P-values", dataSmoothed));
            // else
            chartPValues.getData().setAll(new XYChart.Series<>("P-values", data));
            // Add line to show 0.05 significance threshold
            if (pValueThresholds.length > 1) {
                Data<Number, Number> sigData1 = new Data<>(pValueThresholds[0], 0.05);
                Data<Number, Number> sigData2 = new Data<>(pValueThresholds[pValueThresholds.length - 1], 0.05);
                XYChart.Series<Number, Number> dataSignificant = new XYChart.Series<>("Significance 0.05", FXCollections.observableArrayList(sigData1, sigData2));
                chartPValues.getData().add(dataSignificant);
                sigData1.getNode().setVisible(false);
                sigData2.getNode().setVisible(false);
            }
            // pValuesWrapper.clearThresholds();
            for (XYChart.Data<Number, Number> dataPoint : data) {
                if (!Boolean.TRUE.equals(dataPoint.getExtraValue()))
                    dataPoint.getNode().setVisible(false);
            }
        // if (dataSmoothed != null) {
        // for (XYChart.Data<Number, Number> dataPoint : dataSmoothed) {
        // dataPoint.getNode().setVisible(false);
        // }
        // chartPValues.getData().get(1).getNode().setOpacity(0.5);
        // }
        // int count = 0;
        // for (int i = 0; i < pValueThresholds.length; i++) {
        // double pValue = pValues[i];
        // if (Double.isNaN(pValue))
        // continue;
        // boolean observed = pValueThresholdsObserved[i];
        // //						if (observed)
        // //							pValuesWrapper.addThreshold(new ReadOnlyDoubleWrapper(pValueThresholds[i]), Color.rgb(0, 0, 0, 0.05));
        // 
        // if (!observed) {
        // //							StackPane pane = (StackPane)data.get(count).getNode();
        // //							pane.setEffect(new DropShadow());
        // data.get(count).getNode().setVisible(false);
        // }
        // count++;
        // }
        }
        for (int i = 0; i < threshProperties.length; i++) {
            if (i < thresholds.length)
                threshProperties[i].set(thresholds[i]);
            else
                threshProperties[i].set(Double.NaN);
        }
        boolean isInteractive = interactiveThresholds();
        histogramWrapper.setIsInteractive(isInteractive);
        pValuesWrapper.setIsInteractive(isInteractive);
        chartPValues.setVisible(true);
    }
    // else
    // chartPValues.setVisible(false);
    // Store values for next time
    scoreData = newScoreData;
}
Also used : Histogram(qupath.lib.analysis.stats.Histogram) CellDataFeatures(javafx.scene.control.TableColumn.CellDataFeatures) ObservableValue(javafx.beans.value.ObservableValue) ThresholdedChartWrapper(qupath.lib.gui.charts.HistogramPanelFX.ThresholdedChartWrapper) StackPane(javafx.scene.layout.StackPane) HashSet(java.util.HashSet) ObservableNumberValue(javafx.beans.value.ObservableNumberValue) GridPane(javafx.scene.layout.GridPane) TMACoreObject(qupath.lib.objects.TMACoreObject) Tooltip(javafx.scene.control.Tooltip) Color(javafx.scene.paint.Color) SimpleStringProperty(javafx.beans.property.SimpleStringProperty) TableColumn(javafx.scene.control.TableColumn) StackPane(javafx.scene.layout.StackPane) Pane(javafx.scene.layout.Pane) BorderPane(javafx.scene.layout.BorderPane) GridPane(javafx.scene.layout.GridPane) TitledPane(javafx.scene.control.TitledPane) ParameterList(qupath.lib.plugins.parameters.ParameterList) XYChart(javafx.scene.chart.XYChart) TMACoreObject(qupath.lib.objects.TMACoreObject) HistogramPanelFX(qupath.lib.gui.charts.HistogramPanelFX) LogRankResult(qupath.lib.analysis.stats.survival.LogRankTest.LogRankResult) IntParameter(qupath.lib.plugins.parameters.IntParameter) BorderPane(javafx.scene.layout.BorderPane) NumberAxis(javafx.scene.chart.NumberAxis) Insets(javafx.geometry.Insets) ClipboardContent(javafx.scene.input.ClipboardContent) MeasurementList(qupath.lib.measurements.MeasurementList) ContextMenu(javafx.scene.control.ContextMenu) RadioMenuItem(javafx.scene.control.RadioMenuItem) KaplanMeierData(qupath.lib.analysis.stats.survival.KaplanMeierData) ParameterPanelFX(qupath.lib.gui.dialogs.ParameterPanelFX) TableCell(javafx.scene.control.TableCell) ContextMenu(javafx.scene.control.ContextMenu) Menu(javafx.scene.control.Menu) TitledPane(javafx.scene.control.TitledPane) MenuItem(javafx.scene.control.MenuItem) RadioMenuItem(javafx.scene.control.RadioMenuItem) KaplanMeierData(qupath.lib.analysis.stats.survival.KaplanMeierData) Data(javafx.scene.chart.XYChart.Data) TreeMap(java.util.TreeMap) ToggleGroup(javafx.scene.control.ToggleGroup)

Example 4 with Histogram

use of qupath.lib.analysis.stats.Histogram in project qupath by qupath.

the class CellIntensityClassificationCommand method run.

@Override
public void run() {
    // Dialogs.showErrorNotification(title, "Not implemented yet!");
    var imageData = qupath.getImageData();
    if (imageData == null) {
        Dialogs.showNoImageError(title);
        return;
    }
    var hierarchy = imageData.getHierarchy();
    // Try to operate on cells, but accept operating on all detections if necessary
    var cells = imageData.getHierarchy().getCellObjects();
    boolean allDetections = cells.isEmpty();
    if (allDetections)
        logger.debug("No cells found - will try using all detections");
    var detections = allDetections ? imageData.getHierarchy().getDetectionObjects() : cells;
    if (detections.isEmpty()) {
        Dialogs.showErrorMessage(title, "No cells found in the current image!");
        return;
    }
    var measurements = PathClassifierTools.getAvailableFeatures(detections);
    if (measurements.isEmpty()) {
        Dialogs.showErrorMessage(title, "No cell measurements found in the current image!");
        return;
    }
    var currentClassifications = PathClassifierTools.createClassificationMap(detections);
    var comboMeasurements = new ComboBox<String>();
    comboMeasurements.getItems().setAll(measurements);
    PaneTools.setToExpandGridPaneWidth(comboMeasurements);
    var selectedMeasurement = comboMeasurements.getSelectionModel().selectedItemProperty();
    var cbSingleThreshold = new CheckBox("Single threshold");
    cbSingleThreshold.setSelected(true);
    var singleThreshold = cbSingleThreshold.selectedProperty();
    var sliders = new ArrayList<Slider>();
    var textFields = new ArrayList<TextField>();
    for (int i = 0; i < 3; i++) {
        var slider = new Slider();
        var tf = new TextField();
        tf.setPrefColumnCount(6);
        textFields.add(tf);
        GuiTools.bindSliderAndTextField(slider, tf, true);
        GuiTools.installRangePrompt(slider);
        slider.valueProperty().addListener((v, o, n) -> {
            updateClassifications(hierarchy, allDetections, selectedMeasurement.get(), parseValues(sliders, singleThreshold.get()));
        });
        PaneTools.setToExpandGridPaneWidth(slider);
        sliders.add(slider);
    }
    var map = new HashMap<String, double[]>();
    var histogramPanel = new HistogramPanelFX();
    var chartWrapper = new ThresholdedChartWrapper(histogramPanel.getChart());
    chartWrapper.setIsInteractive(true);
    singleThreshold.addListener((v, o, n) -> {
        chartWrapper.clearThresholds();
        Color color = Color.rgb(0, 0, 0, 0.2);
        if (!n) {
            for (int i = 0; i < sliders.size(); i++) {
                chartWrapper.addThreshold(sliders.get(i).valueProperty(), color);
            }
        } else
            chartWrapper.addThreshold(sliders.get(0).valueProperty(), color);
    });
    selectedMeasurement.addListener((v, o, n) -> {
        if (o != null)
            map.put(o, parseValues(sliders));
        double[] measurementValues = detections.stream().mapToDouble(p -> p.getMeasurementList().getMeasurementValue(n)).filter(d -> Double.isFinite(d)).toArray();
        var stats = new DescriptiveStatistics(measurementValues);
        var histogram = new Histogram(measurementValues, 100, stats.getMin(), stats.getMax());
        histogramPanel.getHistogramData().setAll(HistogramPanelFX.createHistogramData(histogram, false, ColorTools.packARGB(100, 200, 20, 20)));
        double[] values = map.get(n);
        for (int i = 0; i < sliders.size(); i++) {
            var slider = sliders.get(i);
            slider.setMin(stats.getMin());
            slider.setMax(stats.getMax());
            double val = values == null ? stats.getMean() + stats.getStandardDeviation() * i : values[i];
            slider.setValue(val);
            // Add first threshold to histogram
            if (i == 0) {
                Color color = Color.rgb(0, 0, 0, 0.2);
                chartWrapper.addThreshold(sliders.get(i).valueProperty(), color);
            }
        }
    });
    selectedMeasurement.addListener((v, o, n) -> updateClassifications(hierarchy, allDetections, n, parseValues(sliders, singleThreshold.get())));
    singleThreshold.addListener((v, o, n) -> updateClassifications(hierarchy, allDetections, selectedMeasurement.get(), parseValues(sliders, singleThreshold.get())));
    var pane = new GridPane();
    int row = 0;
    var labelMeasurements = new Label("Measurement");
    PaneTools.addGridRow(pane, row++, 0, "Select measurement to threshold", labelMeasurements, comboMeasurements, comboMeasurements);
    for (int i = 0; i < sliders.size(); i++) {
        var labelThreshold = new Label("Threshold " + (i + 1) + "+");
        var slider = sliders.get(i);
        var tf = textFields.get(i);
        if (i > 0) {
            slider.disableProperty().bind(singleThreshold);
            tf.disableProperty().bind(singleThreshold);
        }
        PaneTools.addGridRow(pane, row++, 0, "Select threshold value", labelThreshold, slider, tf);
    }
    PaneTools.addGridRow(pane, row++, 0, "Toggle between using a single threshold (Negative/Positive) or three threshold Negative/1+/2+/3+)", cbSingleThreshold, cbSingleThreshold, cbSingleThreshold);
    pane.setHgap(5.0);
    pane.setVgap(5.0);
    PaneTools.setToExpandGridPaneHeight(chartWrapper.getPane());
    PaneTools.setToExpandGridPaneWidth(chartWrapper.getPane());
    histogramPanel.getChart().getYAxis().setTickLabelsVisible(false);
    histogramPanel.getChart().setAnimated(false);
    chartWrapper.getPane().setPrefSize(200, 80);
    pane.add(chartWrapper.getPane(), pane.getColumnCount(), 0, 1, pane.getRowCount());
    var dialog = new Dialog<ButtonType>();
    dialog.initOwner(qupath.getStage());
    dialog.setTitle(title);
    dialog.getDialogPane().setContent(pane);
    dialog.getDialogPane().getButtonTypes().setAll(ButtonType.APPLY, ButtonType.CANCEL);
    var response = dialog.showAndWait().orElse(ButtonType.CANCEL);
    if (pool != null) {
        pool.shutdown();
        try {
            pool.awaitTermination(5000L, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            logger.debug("Exception waiting for classification to complete: " + e.getLocalizedMessage(), e);
        }
    }
    // Check if we did anything, if not return
    if (nextRequest == null)
        return;
    if (ButtonType.APPLY.equals(response)) {
        // Make sure we ran the last command, then log it in the workflow
        if (!nextRequest.isComplete())
            nextRequest.doClassification();
        imageData.getHistoryWorkflow().addStep(nextRequest.toWorkflowStep());
    } else {
        // Restore classifications if the user cancelled
        var changed = PathClassifierTools.restoreClassificationsFromMap(currentClassifications);
        if (!changed.isEmpty())
            hierarchy.fireObjectClassificationsChangedEvent(this, changed);
    }
}
Also used : Arrays(java.util.Arrays) ButtonType(javafx.scene.control.ButtonType) LoggerFactory(org.slf4j.LoggerFactory) HashMap(java.util.HashMap) PathObjectHierarchy(qupath.lib.objects.hierarchy.PathObjectHierarchy) ArrayList(java.util.ArrayList) Dialogs(qupath.lib.gui.dialogs.Dialogs) Slider(javafx.scene.control.Slider) ComboBox(javafx.scene.control.ComboBox) HistogramPanelFX(qupath.lib.gui.charts.HistogramPanelFX) ExecutorService(java.util.concurrent.ExecutorService) GridPane(javafx.scene.layout.GridPane) QuPathGUI(qupath.lib.gui.QuPathGUI) Color(javafx.scene.paint.Color) PathClassifierTools(qupath.lib.classifiers.PathClassifierTools) TextField(javafx.scene.control.TextField) Logger(org.slf4j.Logger) Dialog(javafx.scene.control.Dialog) Label(javafx.scene.control.Label) ColorTools(qupath.lib.common.ColorTools) DecimalFormat(java.text.DecimalFormat) CheckBox(javafx.scene.control.CheckBox) ThresholdedChartWrapper(qupath.lib.gui.charts.HistogramPanelFX.ThresholdedChartWrapper) WorkflowStep(qupath.lib.plugins.workflow.WorkflowStep) Collectors(java.util.stream.Collectors) Executors(java.util.concurrent.Executors) TimeUnit(java.util.concurrent.TimeUnit) List(java.util.List) DefaultScriptableWorkflowStep(qupath.lib.plugins.workflow.DefaultScriptableWorkflowStep) GuiTools(qupath.lib.gui.tools.GuiTools) DescriptiveStatistics(org.apache.commons.math3.stat.descriptive.DescriptiveStatistics) ThreadTools(qupath.lib.common.ThreadTools) Histogram(qupath.lib.analysis.stats.Histogram) QP(qupath.lib.scripting.QP) PaneTools(qupath.lib.gui.tools.PaneTools) DescriptiveStatistics(org.apache.commons.math3.stat.descriptive.DescriptiveStatistics) Histogram(qupath.lib.analysis.stats.Histogram) GridPane(javafx.scene.layout.GridPane) Slider(javafx.scene.control.Slider) HashMap(java.util.HashMap) ComboBox(javafx.scene.control.ComboBox) Color(javafx.scene.paint.Color) ArrayList(java.util.ArrayList) Label(javafx.scene.control.Label) CheckBox(javafx.scene.control.CheckBox) Dialog(javafx.scene.control.Dialog) TextField(javafx.scene.control.TextField) HistogramPanelFX(qupath.lib.gui.charts.HistogramPanelFX) ThresholdedChartWrapper(qupath.lib.gui.charts.HistogramPanelFX.ThresholdedChartWrapper)

Example 5 with Histogram

use of qupath.lib.analysis.stats.Histogram in project qupath by qupath.

the class PathIntensityClassifierPane method updateIntensityHistogram.

private void updateIntensityHistogram() {
    String selected = comboIntensities.getSelectionModel().getSelectedItem();
    PathObjectHierarchy hierarchy = getHierarchy();
    // if (!"None".equals(selected) || hierarchy == null)
    if ("None".equals(selected) || hierarchy == null) {
        if (panelHistogram != null)
            panelHistogram.getHistogramData().clear();
        return;
    }
    // Try to make a histogram & set it in the panel
    // PathObject pathObjectSelected = hierarchy.getSelectionModel().getSelectedPathObject();
    // For now, always use all objects (not direct descendants only)
    Collection<PathObject> pathObjects = null;
    // if (pathObjectSelected == null || !pathObjectSelected.hasChildren())
    pathObjects = hierarchy.getDetectionObjects();
    // else
    // pathObjects = hierarchy.getDescendantObjects(pathObjectSelected, pathObjects, PathDetectionObject.class);
    // Histogram histogram = Histogram.makeMeasurementHistogram(pathObjects, (String)selected, 256);
    double[] values = Histogram.getMeasurementValues(pathObjects, (String) selected);
    Histogram histogram = new Histogram(values, 128);
    // Compute quartile values
    Arrays.sort(values);
    int nNaNs = 0;
    // NaNs should be at the end of the list
    for (int i = values.length - 1; i >= 0; i--) {
        if (Double.isNaN(values[i]))
            nNaNs++;
        else
            break;
    }
    // Should be same as histogram.getCountSum() ?
    int nValues = values.length - nNaNs;
    assert nValues == histogram.getCountSum();
    if (nValues > 0) {
        double median = values[nValues / 2];
        double quartile1 = values[(int) (nValues / 4 + .5)];
        double quartile3 = values[(int) (nValues * 3 / 4 + .5)];
        logger.info(String.format("%s Quartile 1: %.4f", selected, quartile1));
        logger.info(String.format("%s Median: %.4f", selected, median));
        logger.info(String.format("%s Quartile 3: %.4f", selected, quartile3));
        RunningStatistics stats = StatisticsHelper.computeRunningStatistics(values);
        logger.info(String.format("%s Mean: %.4f", selected, stats.getMean()));
        logger.info(String.format("%s Std.Dev.: %.4f", selected, stats.getStdDev()));
        panelHistogram.getHistogramData().setAll(HistogramPanelFX.createHistogramData(histogram, true, (Integer) null));
    } else
        panelHistogram.getHistogramData().clear();
    updateHistogramThresholdLines();
}
Also used : PathObjectHierarchy(qupath.lib.objects.hierarchy.PathObjectHierarchy) Histogram(qupath.lib.analysis.stats.Histogram) PathObject(qupath.lib.objects.PathObject) RunningStatistics(qupath.lib.analysis.stats.RunningStatistics)

Aggregations

Histogram (qupath.lib.analysis.stats.Histogram)6 GridPane (javafx.scene.layout.GridPane)4 Color (javafx.scene.paint.Color)4 NumberAxis (javafx.scene.chart.NumberAxis)3 Label (javafx.scene.control.Label)3 HistogramPanelFX (qupath.lib.gui.charts.HistogramPanelFX)3 HistogramData (qupath.lib.gui.charts.HistogramPanelFX.HistogramData)3 ThresholdedChartWrapper (qupath.lib.gui.charts.HistogramPanelFX.ThresholdedChartWrapper)3 DecimalFormat (java.text.DecimalFormat)2 ArrayList (java.util.ArrayList)2 HashSet (java.util.HashSet)2 List (java.util.List)2 Collectors (java.util.stream.Collectors)2 SimpleStringProperty (javafx.beans.property.SimpleStringProperty)2 ObservableValue (javafx.beans.value.ObservableValue)2 Insets (javafx.geometry.Insets)2 ButtonType (javafx.scene.control.ButtonType)2 CheckBox (javafx.scene.control.CheckBox)2 ContextMenu (javafx.scene.control.ContextMenu)2 Dialog (javafx.scene.control.Dialog)2