use of qupath.lib.gui.measure.ObservableMeasurementTableData in project qupath by qupath.
the class SummaryMeasurementTableCommand method showTable.
/**
* Show a measurement table for the specified image data.
* @param imageData the image data
* @param type the object type to show
*/
public void showTable(ImageData<BufferedImage> imageData, Class<? extends PathObject> type) {
if (imageData == null) {
Dialogs.showNoImageError("Show measurement table");
return;
}
final PathObjectHierarchy hierarchy = imageData.getHierarchy();
ObservableMeasurementTableData model = new ObservableMeasurementTableData();
model.setImageData(imageData, imageData == null ? Collections.emptyList() : imageData.getHierarchy().getObjects(null, type));
SplitPane splitPane = new SplitPane();
HistogramDisplay histogramDisplay = new HistogramDisplay(model, true);
// table.setTableMenuButtonVisible(true);
TableView<PathObject> table = new TableView<>();
table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
table.getSelectionModel().getSelectedItems().addListener(new ListChangeListener<PathObject>() {
@Override
public void onChanged(ListChangeListener.Change<? extends PathObject> c) {
synchronizeSelectionModelToTable(hierarchy, c, table);
}
});
StringProperty displayedName = new SimpleStringProperty(ServerTools.getDisplayableImageName(imageData.getServer()));
var title = Bindings.createStringBinding(() -> {
if (type == null)
return "Results " + displayedName.get();
else
return PathObjectTools.getSuitableName(type, false) + " results - " + displayedName.get();
}, displayedName);
// Handle double-click as a way to center on a ROI
// var enter = new KeyCodeCombination(KeyCode.ENTER);
table.setRowFactory(params -> {
var row = new TableRow<PathObject>();
row.setOnMouseClicked(e -> {
if (e.getClickCount() == 2) {
maybeCenterROI(row.getItem());
}
});
// });
return row;
});
// Create columns according to the table model
// for (int i = 0; i < model.getColumnCount(); i++) {
// // Add string column
// if (model.getColumnClass(i).equals(String.class)) {
// TableColumn<PathObject, String> col = null;
// col = new TableColumn<>(model.getColumnName(i));
// col.setCellValueFactory(new Callback<CellDataFeatures<PathObject, String>, ObservableValue<String>>() {
// public ObservableValue<String> call(CellDataFeatures<PathObject, String> val) {
// return new SimpleStringProperty(val.getValue().getDisplayedName());
// }
// });
// col.setCellFactory(column -> new BasicTableCell<String>());
// table.getColumns().add(col);
// }
// }
boolean tmaCoreList = TMACoreObject.class.isAssignableFrom(type);
// Add TMA core columns, if suitable
if (tmaCoreList) {
TableColumn<PathObject, ROI> col = new TableColumn<>("Image");
col.setCellValueFactory(val -> new SimpleObjectProperty<>(val.getValue().getROI()));
double maxWidth = maxDimForTMACore;
double padding = 10;
col.setCellFactory(column -> new TMACoreTableCell(table, imageData.getServer(), maxWidth, padding));
col.widthProperty().addListener((v, o, n) -> table.refresh());
col.setMaxWidth(maxWidth + padding * 2);
table.getColumns().add(col);
// While here, make sure we have fewer bins - don't usually have all that many cores
histogramDisplay.setNumBins(10);
}
// Create numeric columns
for (String columnName : model.getAllNames()) {
// Add column
if (model.isStringMeasurement(columnName)) {
TableColumn<PathObject, String> col = new TableColumn<>(columnName);
col.setCellValueFactory(column -> model.createStringMeasurement(column.getValue(), column.getTableColumn().getText()));
col.setCellFactory(column -> new BasicTableCell<>());
table.getColumns().add(col);
} else {
TableColumn<PathObject, Number> col = new TableColumn<>(columnName);
col.setCellValueFactory(column -> model.createNumericMeasurement(column.getValue(), column.getTableColumn().getText()));
col.setCellFactory(column -> new NumericTableCell<PathObject>(histogramDisplay));
table.getColumns().add(col);
}
}
// Set the PathObjects - need to deal with sorting, since a FilteredList won't handle it directly
SortedList<PathObject> items = new SortedList<>(model.getItems());
items.comparatorProperty().bind(table.comparatorProperty());
table.setItems(items);
List<ButtonBase> buttons = new ArrayList<>();
ToggleButton btnHistogram = new ToggleButton("Show histograms");
btnHistogram.selectedProperty().addListener((v, o, n) -> {
if (n) {
Pane paneHistograms = histogramDisplay.getPane();
splitPane.getItems().add(paneHistograms);
} else if (histogramDisplay != null)
splitPane.getItems().remove(histogramDisplay.getPane());
});
buttons.add(btnHistogram);
// Button btnScatterplot = new Button("Show scatterplots");
// btnScatterplot.setOnAction(e -> {
// SwingUtilities.invokeLater(() -> {
// JDialog dialog = new ScatterplotDisplay(null, "Scatterplots: " + displayedName, model).getDialog();
// dialog.setLocationRelativeTo(null);
// dialog.setVisible(true);
// });
// });
// buttons.add(btnScatterplot);
Button btnCopy = new Button("Copy to clipboard");
btnCopy.setOnAction(e -> {
// TODO: Deal with repetition immediately below...
Set<String> excludeColumns = new HashSet<>();
for (TableColumn<?, ?> col : table.getColumns()) {
if (!col.isVisible())
excludeColumns.add(col.getText());
}
copyTableContentsToClipboard(model, excludeColumns);
});
buttons.add(btnCopy);
Button btnSave = new Button("Save");
btnSave.setOnAction(e -> {
Set<String> excludeColumns = new HashSet<>();
for (TableColumn<?, ?> col : table.getColumns()) {
if (!col.isVisible())
excludeColumns.add(col.getText());
}
File fileOutput = promptForOutputFile();
if (fileOutput == null)
return;
if (saveTableModel(model, fileOutput, excludeColumns)) {
WorkflowStep step;
String includeColumns;
if (excludeColumns.isEmpty())
includeColumns = "";
else {
List<String> includeColumnList = new ArrayList<>(model.getAllNames());
includeColumnList.removeAll(excludeColumns);
includeColumns = ", " + includeColumnList.stream().map(s -> "'" + s + "'").collect(Collectors.joining(", "));
}
String path = qupath.getProject() == null ? fileOutput.toURI().getPath() : fileOutput.getParentFile().toURI().getPath();
if (type == TMACoreObject.class) {
step = new DefaultScriptableWorkflowStep("Save TMA measurements", String.format("saveTMAMeasurements('%s'%s)", path, includeColumns));
} else if (type == PathAnnotationObject.class) {
step = new DefaultScriptableWorkflowStep("Save annotation measurements", String.format("saveAnnotationMeasurements('%s\'%s)", path, includeColumns));
} else if (type == PathDetectionObject.class) {
step = new DefaultScriptableWorkflowStep("Save detection measurements", String.format("saveDetectionMeasurements('%s'%s)", path, includeColumns));
} else {
step = new DefaultScriptableWorkflowStep("Save measurements", String.format("saveMeasurements('%s', %s%s)", path, type == null ? null : type.getName(), includeColumns));
}
imageData.getHistoryWorkflow().addStep(step);
}
});
buttons.add(btnSave);
Stage frame = new Stage();
frame.initOwner(qupath.getStage());
frame.titleProperty().bind(title);
BorderPane paneTable = new BorderPane();
paneTable.setCenter(table);
// Add text field to filter visible columns
TextField tfColumnFilter = new TextField();
GridPane paneFilter = new GridPane();
paneFilter.add(new Label("Column filter"), 0, 0);
paneFilter.add(tfColumnFilter, 1, 0);
GridPane.setHgrow(tfColumnFilter, Priority.ALWAYS);
paneFilter.setHgap(5);
if (tmaCoreList) {
CheckBox cbHideMissing = new CheckBox("Hide missing cores");
paneFilter.add(cbHideMissing, 2, 0);
cbHideMissing.selectedProperty().addListener((v, o, n) -> {
if (n) {
model.setPredicate(p -> (!(p instanceof TMACoreObject)) || !((TMACoreObject) p).isMissing());
} else
model.setPredicate(null);
});
cbHideMissing.setSelected(true);
}
paneFilter.setPadding(new Insets(2, 5, 2, 5));
paneTable.setBottom(paneFilter);
StringProperty columnFilter = tfColumnFilter.textProperty();
columnFilter.addListener((v, o, n) -> {
String val = n.toLowerCase().trim();
if (val.isEmpty()) {
for (TableColumn<?, ?> col : table.getColumns()) {
if (!col.isVisible())
col.setVisible(true);
}
return;
}
for (TableColumn<?, ?> col : table.getColumns()) {
col.setVisible(col.getText().toLowerCase().contains(val));
}
});
BorderPane pane = new BorderPane();
// pane.setCenter(table);
splitPane.getItems().add(paneTable);
pane.setCenter(splitPane);
GridPane panelButtons = PaneTools.createColumnGridControls(buttons.toArray(new ButtonBase[0]));
pane.setBottom(panelButtons);
PathObjectHierarchyListener listener = new PathObjectHierarchyListener() {
@Override
public void hierarchyChanged(PathObjectHierarchyEvent event) {
if (event.isChanging())
return;
if (!Platform.isFxApplicationThread()) {
Platform.runLater(() -> hierarchyChanged(event));
return;
}
if (imageData != null)
displayedName.set(ServerTools.getDisplayableImageName(imageData.getServer()));
model.refreshEntries();
table.refresh();
if (histogramDisplay != null)
histogramDisplay.refreshHistogram();
}
};
QuPathViewer viewer = qupath.getViewer();
TableViewerListener tableViewerListener = new TableViewerListener(viewer, table);
frame.setOnShowing(e -> {
hierarchy.addPathObjectListener(listener);
viewer.addViewerListener(tableViewerListener);
});
frame.setOnHiding(e -> {
hierarchy.removePathObjectListener(listener);
viewer.removeViewerListener(tableViewerListener);
});
Scene scene = new Scene(pane, 600, 500);
frame.setScene(scene);
frame.show();
// Add ability to remove entries from table
ContextMenu menu = new ContextMenu();
Menu menuLimitClasses = new Menu("Show classes");
menu.setOnShowing(e -> {
Set<PathClass> representedClasses = model.getBackingListEntries().stream().map(p -> p.getPathClass() == null ? null : p.getPathClass().getBaseClass()).collect(Collectors.toCollection(() -> new HashSet<>()));
representedClasses.remove(null);
if (representedClasses.isEmpty()) {
menuLimitClasses.setVisible(false);
} else {
menuLimitClasses.setVisible(true);
}
menuLimitClasses.getItems().clear();
List<PathClass> sortedClasses = new ArrayList<>(representedClasses);
Collections.sort(sortedClasses);
MenuItem miClass = new MenuItem("All");
miClass.setOnAction(e2 -> {
model.setPredicate(null);
histogramDisplay.refreshHistogram();
});
menuLimitClasses.getItems().add(miClass);
for (PathClass pathClass : sortedClasses) {
miClass = new MenuItem(pathClass.getName());
miClass.setOnAction(e2 -> {
model.setPredicate(p -> pathClass.isAncestorOf(p.getPathClass()));
histogramDisplay.refreshHistogram();
});
menuLimitClasses.getItems().add(miClass);
}
});
if (type != TMACoreObject.class) {
menu.getItems().add(menuLimitClasses);
table.setContextMenu(menu);
}
}
use of qupath.lib.gui.measure.ObservableMeasurementTableData in project qupath by qupath.
the class MeasurementExportCommand method createAndShowDialog.
private void createAndShowDialog() {
project = qupath.getProject();
if (project == null) {
Dialogs.showNoProjectError("Export measurements");
return;
}
BorderPane mainPane = new BorderPane();
BorderPane imageEntryPane = new BorderPane();
GridPane optionPane = new GridPane();
optionPane.setHgap(5.0);
optionPane.setVgap(5.0);
// TOP PANE (SELECT PROJECT ENTRY FOR EXPORT)
project = qupath.getProject();
pathObjectCombo = new ComboBox<>();
separatorCombo = new ComboBox<>();
includeCombo = new CheckComboBox<String>();
String sameImageWarning = "A selected image is open in the viewer!\nData should be saved before exporting.";
var listSelectionView = ProjectDialogs.createImageChoicePane(qupath, project.getImageList(), previousImages, sameImageWarning);
// BOTTOM PANE (OPTIONS)
int row = 0;
Label pathOutputLabel = new Label("Output file");
var btnChooseFile = new Button("Choose");
btnChooseFile.setOnAction(e -> {
String extSelected = separatorCombo.getSelectionModel().getSelectedItem();
String ext = extSelected.equals("Tab (.tsv)") ? ".tsv" : ".csv";
String extDesc = ext.equals(".tsv") ? "TSV (Tab delimited)" : "CSV (Comma delimited)";
File pathOut = Dialogs.promptToSaveFile("Output file", Projects.getBaseDirectory(project), "measurements" + ext, extDesc, ext);
if (pathOut != null) {
if (pathOut.isDirectory())
pathOut = new File(pathOut.getAbsolutePath() + File.separator + "measurements" + ext);
outputText.setText(pathOut.getAbsolutePath());
}
});
pathOutputLabel.setLabelFor(outputText);
PaneTools.addGridRow(optionPane, row++, 0, "Choose output file", pathOutputLabel, outputText, outputText, btnChooseFile, btnChooseFile);
outputText.setMaxWidth(Double.MAX_VALUE);
btnChooseFile.setMaxWidth(Double.MAX_VALUE);
Label pathObjectLabel = new Label("Export type");
pathObjectLabel.setLabelFor(pathObjectCombo);
pathObjectCombo.getItems().setAll("Image", "Annotations", "Detections", "Cells", "TMA cores");
pathObjectCombo.getSelectionModel().selectFirst();
pathObjectCombo.valueProperty().addListener((v, o, n) -> {
if (n != null)
setType(n);
});
PaneTools.addGridRow(optionPane, row++, 0, "Choose the export type", pathObjectLabel, pathObjectCombo, pathObjectCombo, pathObjectCombo, pathObjectCombo);
Label separatorLabel = new Label("Separator");
separatorLabel.setLabelFor(separatorCombo);
separatorCombo.getItems().setAll("Tab (.tsv)", "Comma (.csv)", "Semicolon (.csv)");
separatorCombo.getSelectionModel().selectFirst();
PaneTools.addGridRow(optionPane, row++, 0, "Choose a value separator", separatorLabel, separatorCombo, separatorCombo, separatorCombo, separatorCombo);
Label includeLabel = new Label("Columns to include (Optional)");
includeLabel.setLabelFor(includeCombo);
GuiTools.installSelectAllOrNoneMenu(includeCombo);
Button btnPopulateColumns = new Button("Populate\t");
ProgressIndicator progressIndicator = new ProgressIndicator();
progressIndicator.setPrefSize(20, 20);
progressIndicator.setMinSize(20, 20);
progressIndicator.setOpacity(0);
Button btnResetColumns = new Button("Reset");
PaneTools.addGridRow(optionPane, row++, 0, "Choose the specific column(s) to include (default: all)", includeLabel, includeCombo, btnPopulateColumns, progressIndicator, btnResetColumns);
btnPopulateColumns.setOnAction(e -> {
includeCombo.setDisable(true);
Set<String> allColumnsForCombo = Collections.synchronizedSet(new LinkedHashSet<>());
setType(pathObjectCombo.getSelectionModel().getSelectedItem());
for (int i = 0; i < ProjectDialogs.getTargetItems(listSelectionView).size(); i++) {
ProjectImageEntry<BufferedImage> entry = ProjectDialogs.getTargetItems(listSelectionView).get(i);
int updatedEntries = i;
executor.submit(() -> {
try {
progressIndicator.setOpacity(100);
ImageData<?> imageData = entry.readImageData();
ObservableMeasurementTableData model = new ObservableMeasurementTableData();
model.setImageData(imageData, imageData == null ? Collections.emptyList() : imageData.getHierarchy().getObjects(null, type));
allColumnsForCombo.addAll(model.getAllNames());
imageData.getServer().close();
if (updatedEntries == ProjectDialogs.getTargetItems(listSelectionView).size() - 1) {
Platform.runLater(() -> {
allColumnsForCombo.removeIf(n -> n == null);
includeCombo.getItems().setAll(allColumnsForCombo);
includeCombo.getCheckModel().clearChecks();
includeCombo.setDisable(false);
});
progressIndicator.setOpacity(0);
}
} catch (Exception ex) {
logger.warn("Error loading columns for entry " + entry.getImageName() + ": " + ex.getLocalizedMessage());
}
});
}
btnResetColumns.fire();
});
btnPopulateColumns.disableProperty().addListener((v, o, n) -> {
if (n != null && n == true)
includeCombo.setDisable(true);
});
var targetItemBinding = Bindings.size(listSelectionView.getTargetItems()).isEqualTo(0);
btnPopulateColumns.disableProperty().bind(targetItemBinding);
btnResetColumns.disableProperty().bind(targetItemBinding);
btnResetColumns.setOnAction(e -> includeCombo.getCheckModel().clearChecks());
// Add listener to separatorCombo
separatorCombo.getSelectionModel().selectedItemProperty().addListener((v, o, n) -> {
if (outputText == null || n == null)
return;
String currentOut = outputText.getText();
if (n.equals("Tab (.tsv)") && currentOut.endsWith(".csv"))
outputText.setText(currentOut.replace(".csv", ".tsv"));
else if ((n.equals("Comma (.csv)") || n.equals("Semicolon (.csv)")) && currentOut.endsWith(".tsv"))
outputText.setText(currentOut.replace(".tsv", ".csv"));
});
PaneTools.getContentsOfType(optionPane, Label.class, false).forEach(e -> e.setMinWidth(160));
PaneTools.setToExpandGridPaneWidth(outputText, pathObjectCombo, separatorCombo, includeCombo);
btnPopulateColumns.setMinWidth(100);
btnResetColumns.setMinWidth(75);
dialog = Dialogs.builder().title("Export measurements").buttons(btnExport, ButtonType.CANCEL).content(mainPane).build();
dialog.getDialogPane().setPrefSize(600, 400);
imageEntryPane.setCenter(listSelectionView);
// Set the disabledProperty according to (1) targetItems.size() > 0 and (2) outputText.isEmpty()
var emptyOutputTextBinding = outputText.textProperty().isEqualTo("");
dialog.getDialogPane().lookupButton(btnExport).disableProperty().bind(Bindings.or(emptyOutputTextBinding, targetItemBinding));
mainPane.setTop(imageEntryPane);
mainPane.setBottom(optionPane);
Optional<ButtonType> result = dialog.showAndWait();
if (!result.isPresent() || result.get() != btnExport || result.get() == ButtonType.CANCEL)
return;
String curExt = Files.getFileExtension(outputText.getText());
if (curExt.equals("") || (!curExt.equals("csv") && !curExt.equals("tsv"))) {
curExt = curExt.length() > 1 ? "." + curExt : curExt;
String extSelected = separatorCombo.getSelectionModel().getSelectedItem();
String ext = extSelected.equals("Tab (.tsv)") ? ".tsv" : ".csv";
outputText.setText(outputText.getText().substring(0, outputText.getText().length() - curExt.length()) + ext);
}
if (new File(outputText.getText()).getParent() == null) {
String ext = Files.getFileExtension(outputText.getText()).equals("tsv") ? ".tsv" : ".csv";
String extDesc = ext.equals(".tsv") ? "TSV (Tab delimited)" : "CSV (Comma delimited)";
File pathOut = Dialogs.promptToSaveFile("Output file", Projects.getBaseDirectory(project), outputText.getText(), extDesc, ext);
if (pathOut == null)
return;
else
outputText.setText(pathOut.getAbsolutePath());
}
var checkedItems = includeCombo.getCheckModel().getCheckedItems();
String[] include = checkedItems.stream().collect(Collectors.toList()).toArray(new String[checkedItems.size()]);
String separator = defSep;
switch(separatorCombo.getSelectionModel().getSelectedItem()) {
case "Tab (.tsv)":
separator = "\t";
break;
case "Comma (.csv)":
separator = ",";
break;
case "Semicolon (.csv)":
separator = ";";
break;
}
;
MeasurementExporter exporter;
exporter = new MeasurementExporter().imageList(ProjectDialogs.getTargetItems(listSelectionView)).separator(separator).includeOnlyColumns(include).exportType(type);
ExportTask worker = new ExportTask(exporter, outputText.getText());
ProgressDialog progress = new ProgressDialog(worker);
progress.setWidth(600);
progress.initOwner(qupath.getStage());
progress.setTitle("Export measurements...");
progress.getDialogPane().setHeaderText("Export measurements");
progress.getDialogPane().setGraphic(null);
progress.getDialogPane().getButtonTypes().add(ButtonType.CANCEL);
progress.getDialogPane().lookupButton(ButtonType.CANCEL).addEventFilter(ActionEvent.ACTION, e -> {
if (Dialogs.showYesNoDialog("Cancel export", "Are you sure you want to stop the export after the current image?")) {
worker.quietCancel();
progress.setHeaderText("Cancelling...");
// worker.cancel(false);
progress.getDialogPane().lookupButton(ButtonType.CANCEL).setDisable(true);
}
e.consume();
});
// Create & run task
runningTask.set(qupath.createSingleThreadExecutor(this).submit(worker));
progress.show();
}
Aggregations