use of qupath.lib.objects.classes.PathClass in project qupath by qupath.
the class TileClassificationsToAnnotationsPlugin method getDefaultParameterList.
@Override
public ParameterList getDefaultParameterList(final ImageData<T> imageData) {
if (!parametersInitialized) {
Set<PathClass> pathClasses = PathClassifierTools.getRepresentedPathClasses(imageData.getHierarchy(), PathTileObject.class);
List<PathClass> choices = new ArrayList<>(pathClasses);
Collections.sort(choices, new Comparator<PathClass>() {
@Override
public int compare(PathClass pc1, PathClass pc2) {
return pc1.getName().compareTo(pc2.getName());
}
});
PathClass allClasses = PathClassFactory.getPathClass("All classes");
PathClass defaultChoice = allClasses;
choices.add(0, allClasses);
// PathClass classTumor = PathClassFactory.getDefaultPathClass(PathClasses.TUMOR); // Tumor is the most likely choice, so default to it if available
// PathClass defaultChoice = choices.contains(classTumor) ? classTumor : choices.get(0);
params = new ParameterList();
params.addChoiceParameter("pathClass", "Choose class", defaultChoice, choices, "Choose PathClass to create annotations from").addBooleanParameter("deleteTiles", "Delete existing child objects", false, "Delete the tiles that were used for creating annotations - further training will not be possible after these are deleted").addBooleanParameter("clearAnnotations", "Clear existing annotations", true, "Remove all existing annotations (often a good idea if they were used to train a classifier, but are no longer needed)").addBooleanParameter("splitAnnotations", "Split new annotations", false, "Split newly-created annotations into distinct regions (rather than have one large, possibly-discontinuous object)");
// .addDoubleParameter("simplify", "Simplify shapes", 0);
}
return params;
}
use of qupath.lib.objects.classes.PathClass in project qupath by qupath.
the class PathClassPane method promptToAddClass.
/**
* Prompt to add a new classification.
* @return true if a new classification was added, false otherwise
*/
boolean promptToAddClass() {
String input = Dialogs.showInputDialog("Add class", "Class name", "");
if (input == null || input.trim().isEmpty())
return false;
PathClass pathClass = PathClassFactory.getPathClass(input);
var list = qupath.getAvailablePathClasses();
if (list.contains(pathClass)) {
Dialogs.showErrorMessage("Add class", "Class '" + input + "' already exists!");
return false;
} else if (input.toLowerCase().equals("null")) {
Dialogs.showErrorMessage("Add class", "Cannot add a 'null' class, try another name!");
return false;
}
list.add(pathClass);
listClasses.getSelectionModel().clearAndSelect(listClasses.getItems().size() - 1);
return true;
}
use of qupath.lib.objects.classes.PathClass in project qupath by qupath.
the class DefaultProject method loadPathClasses.
Collection<PathClass> loadPathClasses() throws IOException {
var path = Paths.get(ensureDirectoryExists(getClassifiersPath()).toString(), "classes.json");
if (!Files.isRegularFile(path))
return Collections.emptyList();
Gson gson = GsonTools.getInstance();
try (var reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
var element = gson.fromJson(reader, JsonObject.class);
JsonElement pathClassesElement = element.get("pathClasses");
if (pathClassesElement != null && pathClassesElement.isJsonArray()) {
JsonArray pathClassesArray = pathClassesElement.getAsJsonArray();
List<PathClass> pathClasses = new ArrayList<>();
for (int i = 0; i < pathClassesArray.size(); i++) {
JsonObject pathClassObject = pathClassesArray.get(i).getAsJsonObject();
if (pathClassObject.has("name")) {
String name = pathClassObject.get("name").getAsString();
Integer color = null;
if (pathClassObject.has("color") && !pathClassObject.get("color").isJsonNull()) {
color = pathClassObject.get("color").getAsInt();
}
PathClass pathClass = PathClassFactory.getPathClass(name, color);
if (color != null)
// Make sure we have the color we want
pathClass.setColor(color);
pathClasses.add(pathClass);
}
}
return pathClasses;
} else
return Collections.emptyList();
}
}
use of qupath.lib.objects.classes.PathClass 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.objects.classes.PathClass in project qupath by qupath.
the class ClassifierBuilderPane method completeClassification.
private void completeClassification(final PathObjectHierarchy hierarchy, final Collection<PathObject> classifiedObjects, final Collection<PathObject> originalObjects, final Map<PathClass, List<PathObject>> mapTest, final boolean testOnTrainingData) {
if (!Platform.isFxApplicationThread())
Platform.runLater(() -> completeClassification(hierarchy, classifiedObjects, originalObjects, mapTest, testOnTrainingData));
else {
// Test the classifications of the test set... which may or may not be the same as the training set
if (mapTest != null) {
int nCorrect = 0;
List<PathClass> pathClasses = new ArrayList<>(mapTest.keySet());
Collections.sort(pathClasses);
ConfusionMatrix<PathClass> confusion = new ConfusionMatrix<>(pathClasses);
int nCorrectTumor = 0;
// int nWrong = 0;
int nUnclassified = 0;
int n = 0;
PathClass tumorClass = PathClassFactory.getPathClass(StandardPathClasses.TUMOR);
// If we have multiple classes, it can be beneficial to see how tumor vs. everything else performs
// Create a tumor vs. everything else classifier
boolean multiclassContainsTumor = mapTest.containsKey(tumorClass) && mapTest.size() > 2;
for (Entry<PathClass, List<PathObject>> entry : mapTest.entrySet()) {
PathClass pathClass = entry.getKey();
boolean isTumor = pathClass.equals(tumorClass);
for (PathObject testObject : entry.getValue()) {
PathClass tempClass = testObject.getPathClass();
if (tempClass == null) {
nUnclassified++;
n++;
continue;
}
// We've probably applied an intensity classifier by now
PathClass resultClass = tempClass.getBaseClass();
confusion.registerClassification(pathClass, resultClass);
if (resultClass.equals(pathClass))
nCorrect++;
if (multiclassContainsTumor && (isTumor && pathClass.equals(resultClass) || (!isTumor && !resultClass.equals(tumorClass))))
nCorrectTumor++;
n++;
}
}
// Log the results
if (testOnTrainingData) {
logger.info(String.format("Percentage of correctly classified objects in TRAINING set: %.2f%% (n=%d)", nCorrect * 100. / n, n));
logger.warn("It is *strongly* advised not to report accuracies based on testing using the training set!");
} else {
logger.info(String.format("Percentage of correctly classified objects in test set: %.2f%% (n=%d)", nCorrect * 100. / n, n));
if (multiclassContainsTumor)
logger.info(String.format("Percentage of correctly classified objects in test set (Tumor vs. everything else): %.2f%% (n=%d)", nCorrectTumor * 100. / n, n));
}
if (nUnclassified > 0)
logger.info(String.format("Number of unclassified objects in the test set: %d (%.2f%%)", nUnclassified, nUnclassified * 100. / n));
logger.info("Confusion matrix\n" + confusion.toString());
// Only interested in changes from now
hierarchyChanged = false;
}
progressIndicator.setVisible(false);
// Update the classification of any proxy objects
int nChanged = classifiedObjects.parallelStream().mapToInt(p -> {
if (p instanceof PathObjectClassificationProxy && ((PathObjectClassificationProxy) p).updateObject())
return 1;
else
return 0;
}).sum();
if (classifiedObjects != originalObjects)
logger.info("Number of reclassified objects: {} of {}", nChanged, classifiedObjects.size());
// Update displayed list - names may have changed - and classifier summary
updateClassifierSummary(null);
btnSaveClassifier.setDisable(!classifier.isValid());
hierarchy.fireObjectClassificationsChangedEvent(this, originalObjects);
lastClassifierCompleted = classifier;
updatingClassification = false;
}
}
Aggregations