use of qupath.lib.plugins.workflow.DefaultScriptableWorkflowStep in project qupath by qupath.
the class GuiTools method promptToClearAllSelectedObjects.
/**
* Prompt user to select all currently-selected objects (except TMA core objects).
*
* @param imageData
* @return
*/
public static boolean promptToClearAllSelectedObjects(final ImageData<?> imageData) {
// Get all non-TMA core objects
PathObjectHierarchy hierarchy = imageData.getHierarchy();
Collection<PathObject> selectedRaw = hierarchy.getSelectionModel().getSelectedObjects();
List<PathObject> selected = selectedRaw.stream().filter(p -> !(p instanceof TMACoreObject)).collect(Collectors.toList());
if (selected.isEmpty()) {
if (selectedRaw.size() > selected.size())
Dialogs.showErrorMessage("Delete selected objects", "No valid objects selected! \n\nNote: Individual TMA cores cannot be deleted with this method.");
else
Dialogs.showErrorMessage("Delete selected objects", "No objects selected!");
return false;
}
int n = selected.size();
String message;
if (n == 1)
message = "Delete selected object?";
else
message = "Delete " + n + " selected objects?";
if (Dialogs.showYesNoDialog("Delete objects", message)) {
// Check for descendants
List<PathObject> children = new ArrayList<>();
for (PathObject temp : selected) {
children.addAll(temp.getChildObjects());
}
children.removeAll(selected);
boolean keepChildren = true;
if (!children.isEmpty()) {
Dialogs.DialogButton response = Dialogs.showYesNoCancelDialog("Delete objects", "Keep descendant objects?");
if (response == Dialogs.DialogButton.CANCEL)
return false;
keepChildren = response == Dialogs.DialogButton.YES;
}
hierarchy.removeObjects(selected, keepChildren);
hierarchy.getSelectionModel().clearSelection();
imageData.getHistoryWorkflow().addStep(new DefaultScriptableWorkflowStep("Delete selected objects", "clearSelectedObjects(" + keepChildren + ");"));
if (keepChildren)
logger.info(selected.size() + " object(s) deleted");
else
logger.info(selected.size() + " object(s) deleted with descendants");
imageData.getHistoryWorkflow().addStep(new DefaultScriptableWorkflowStep("Delete selected objects", "clearSelectedObjects();"));
logger.info(selected.size() + " object(s) deleted");
return true;
} else
return false;
}
use of qupath.lib.plugins.workflow.DefaultScriptableWorkflowStep in project qupath by qupath.
the class ImageData method setImageType.
/**
* Set the image type.
* @param type
*/
public void setImageType(final ImageType type) {
if (this.type == type)
return;
logger.trace("Setting image type to {}", type);
ImageType oldType = this.type;
this.type = type;
// Log the step
getHistoryWorkflow().addStep(new DefaultScriptableWorkflowStep("Set image type", Collections.singletonMap("Image type", type), "setImageType(\'" + type.name() + "');"));
if (isBrightfield())
addColorDeconvolutionStainsToWorkflow(this);
// TODO: REINTRODUCE LOGGING!
// // Log the step
// getWorkflow().addStep(
// new DefaultScriptableWorkflowStep("Set image type",
// Collections.singletonMap("Image type", type),
// QP.class.getSimpleName() + ".setImageType(\'" + type.toString() + "');")
// );
// if (isBrightfield())
// addColorDeconvolutionStainsToWorkflow(this);
pcs.firePropertyChange("imageType", oldType, type);
changes = true;
}
use of qupath.lib.plugins.workflow.DefaultScriptableWorkflowStep in project qupath by qupath.
the class PixelClassifierUI method promptToSavePredictionImage.
// public static Pane createSaveObjectClassifierPane(ObjectExpression<Project<BufferedImage>> project, ObjectExpression<ObjectClassifier<BufferedImage>> classifier, StringProperty savedName) {
// return new SaveResourcePaneBuilder<>(ObjectClassifier.class, classifier)
// .project(project)
// .savedName(savedName)
// .title("Object classifier")
// .build();
// }
private static boolean promptToSavePredictionImage(ImageData<BufferedImage> imageData, PixelClassifier classifier, String classifierName) {
Objects.requireNonNull(imageData);
Objects.requireNonNull(classifier);
var server = PixelClassifierTools.createPixelClassificationServer(imageData, classifier);
ImageWriter<BufferedImage> writer;
var allWriters = ImageWriterTools.getCompatibleWriters(server, "ome.tif");
if (allWriters == null || allWriters.isEmpty()) {
allWriters = ImageWriterTools.getCompatibleWriters(server, null);
}
if (allWriters.isEmpty()) {
Dialogs.showErrorMessage("Save prediction", "Sorry, I could not find any compatible image writers!");
return false;
} else if (allWriters.size() > 1) {
Map<String, ImageWriter<BufferedImage>> map = new LinkedHashMap<>();
for (var w : allWriters) map.put(w.getName(), w);
var choice = Dialogs.showChoiceDialog("Save prediction", "Choose image writer", map.keySet(), map.keySet().iterator().next());
writer = choice == null ? null : map.get(choice);
if (writer == null)
return false;
} else
writer = allWriters.iterator().next();
var file = Dialogs.promptToSaveFile("Save prediction", null, classifierName, writer.getName(), writer.getDefaultExtension());
if (file == null)
return false;
try {
var path = file.getAbsolutePath();
writer.writeImage(server, path);
if (classifierName != null && !classifierName.isBlank()) {
imageData.getHistoryWorkflow().addStep(new DefaultScriptableWorkflowStep("Write prediction image", String.format("writePredictionImage(\"%s\", \"%s\")", classifierName, path)));
}
} catch (IOException e) {
Dialogs.showErrorMessage("Save prediction", e);
}
return true;
}
use of qupath.lib.plugins.workflow.DefaultScriptableWorkflowStep in project qupath by qupath.
the class PixelClassifierUI method promptToCreateObjects.
/**
* Prompt the user to create objects directly from the pixels of an {@link ImageServer}.
* Often, the {@link ImageServer} has been created by applying a {@link PixelClassifier}.
*
* @param imageData the {@link ImageData} to which objects should be added
* @param classifier the {@link ImageServer} used to generate objects
* @param classifierName the name of the classifier; if not null and the command runs to completion, it will be logged in the history
* workflow of the {@link ImageData} for later scripting.
* @return true if changes were made, false otherwise
*/
public static boolean promptToCreateObjects(ImageData<BufferedImage> imageData, PixelClassifier classifier, String classifierName) {
Objects.requireNonNull(imageData);
Objects.requireNonNull(classifier);
// Check what is selected
List<SelectionChoice> choices = buildChoiceList(imageData.getHierarchy(), SelectionChoice.FULL_IMAGE, SelectionChoice.CURRENT_SELECTION, SelectionChoice.ANNOTATIONS, SelectionChoice.TMA);
SelectionChoice defaultChoice;
if (choices.contains(SelectionChoice.CURRENT_SELECTION))
defaultChoice = SelectionChoice.CURRENT_SELECTION;
else if (choices.contains(SelectionChoice.ANNOTATIONS))
defaultChoice = SelectionChoice.ANNOTATIONS;
else
defaultChoice = choices.get(0);
var parentChoice = Dialogs.showChoiceDialog("Pixel classifier", "Choose parent objects", choices, defaultChoice);
if (parentChoice == null)
return false;
var outputObjectTypes = Arrays.asList("Annotation", "Detection");
// To avoid confusing the user unnecessarily, if we *only* have ignored classes then set default for includeIgnored to true
var labels = classifier.getMetadata().getClassificationLabels();
boolean allIgnored = !labels.isEmpty() && labels.values().stream().allMatch(p -> p == null || PathClassTools.isIgnoredClass(p));
boolean includeIgnored = allIgnored;
var cal = imageData.getServer().getPixelCalibration();
var units = cal.unitsMatch2D() ? cal.getPixelWidthUnit() + "^2" : cal.getPixelWidthUnit() + "x" + cal.getPixelHeightUnit();
ParameterList params;
if (lastCreateObjectParams != null) {
params = lastCreateObjectParams.duplicate();
params.setHiddenParameters(false, params.getKeyValueParameters(true).keySet().toArray(String[]::new));
((BooleanParameter) params.getParameters().get("includeIgnored")).setValue(includeIgnored);
} else {
params = new ParameterList().addChoiceParameter("objectType", "New object type", "Annotation", outputObjectTypes, "Define the type of objects that will be created").addDoubleParameter("minSize", "Minimum object size", 0, units, "Minimum size of a region to keep (smaller regions will be dropped)").addDoubleParameter("minHoleSize", "Minimum hole size", 0, units, "Minimum size of a hole to keep (smaller holes will be filled)").addBooleanParameter("doSplit", "Split objects", false, "Split multi-part regions into separate objects").addBooleanParameter("clearExisting", "Delete existing objects", false, "Delete any existing objects within the selected object before adding new objects (or entire image if no object is selected)").addBooleanParameter("includeIgnored", "Create objects for ignored classes", includeIgnored, "Create objects for classifications that are usually ignored (e.g. \"Ignore*\", \"Region*\")").addBooleanParameter("selectNew", "Set new objects to selected", false, "Set the newly-created objects to be selected");
}
if (!Dialogs.showParameterDialog("Create objects", params))
return false;
boolean createDetections = params.getChoiceParameterValue("objectType").equals("Detection");
boolean doSplit = params.getBooleanParameterValue("doSplit");
includeIgnored = params.getBooleanParameterValue("includeIgnored");
double minSize = params.getDoubleParameterValue("minSize");
double minHoleSize = params.getDoubleParameterValue("minHoleSize");
boolean clearExisting = params.getBooleanParameterValue("clearExisting");
boolean selectNew = params.getBooleanParameterValue("selectNew");
lastCreateObjectParams = params;
parentChoice.handleSelection(imageData);
List<CreateObjectOptions> options = new ArrayList<>();
if (doSplit)
options.add(CreateObjectOptions.SPLIT);
if (clearExisting)
options.add(CreateObjectOptions.DELETE_EXISTING);
if (includeIgnored)
options.add(CreateObjectOptions.INCLUDE_IGNORED);
else if (allIgnored) {
Dialogs.showErrorMessage(title, "Cannot create objects - all class names have an asterisk to show they should be 'ignored'!");
return false;
}
if (selectNew)
options.add(CreateObjectOptions.SELECT_NEW);
var optionsArray = options.toArray(CreateObjectOptions[]::new);
String optionsString = "";
if (!options.isEmpty())
optionsString = ", " + options.stream().map(o -> "\"" + o.name() + "\"").collect(Collectors.joining(", "));
try {
if (createDetections) {
if (PixelClassifierTools.createDetectionsFromPixelClassifier(imageData, classifier, minSize, minHoleSize, optionsArray)) {
if (classifierName != null) {
imageData.getHistoryWorkflow().addStep(new DefaultScriptableWorkflowStep("Pixel classifier create detections", String.format("createDetectionsFromPixelClassifier(\"%s\", %s, %s)", classifierName, minSize, minHoleSize + optionsString)));
}
return true;
}
} else {
if (PixelClassifierTools.createAnnotationsFromPixelClassifier(imageData, classifier, minSize, minHoleSize, optionsArray)) {
if (classifierName != null) {
imageData.getHistoryWorkflow().addStep(new DefaultScriptableWorkflowStep("Pixel classifier create annotations", String.format("createAnnotationsFromPixelClassifier(\"%s\", %s, %s)", classifierName, minSize, minHoleSize + optionsString)));
}
return true;
}
}
} catch (IOException e) {
Dialogs.showErrorMessage(title, e);
}
return false;
}
use of qupath.lib.plugins.workflow.DefaultScriptableWorkflowStep 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);
}
}
Aggregations