use of qupath.lib.images.ImageData 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.images.ImageData in project qupath by qupath.
the class ScriptCommand method run.
@Override
public void run() {
try {
if (projectPath != null && !projectPath.toLowerCase().endsWith(ProjectIO.getProjectExtension()))
throw new IOException("Project file must end with '.qpproj'");
if (scriptCommand == null) {
if (scriptFile == null || scriptFile.equals("") || !scriptFile.endsWith(".groovy"))
throw new IOException("File must be a valid script file (.groovy): " + scriptFile);
} else if (scriptFile != null) {
throw new IllegalArgumentException("Either a script file or a script command may be provided, but not both!");
}
// Ensure we have a tile cache set
createTileCache();
// Set classloader to include any available extensions
var extensionClassLoader = new ExtensionClassLoader();
extensionClassLoader.refresh();
ImageServerProvider.setServiceLoader(ServiceLoader.load(ImageServerBuilder.class, extensionClassLoader));
Thread.currentThread().setContextClassLoader(extensionClassLoader);
// Unfortunately necessary to force initialization (including GsonTools registration of some classes)
QP.getCoreClasses();
ImageData<BufferedImage> imageData;
if (projectPath != null && !projectPath.equals("")) {
String path = QuPath.getEncodedPath(projectPath);
Project<BufferedImage> project = ProjectIO.loadProject(new File(path), BufferedImage.class);
for (var entry : project.getImageList()) {
if (imagePath != null && !imagePath.equals("") && !imagePath.equals(entry.getImageName()))
continue;
logger.info("Running script for {}", entry.getImageName());
imageData = entry.readImageData();
try {
Object result = runScript(project, imageData);
if (result != null)
logger.info("Script result: {}", result);
if (save)
entry.saveImageData(imageData);
} catch (Exception e) {
logger.error("Error running script for image: " + entry.getImageName(), e);
// Otherwise, try to recover and continue processing images
if (imagePath != null && imagePath.equals(entry.getImageName()))
throw new RuntimeException(e);
} finally {
imageData.getServer().close();
}
}
} else if (imagePath != null && !imagePath.equals("")) {
String path = QuPath.getEncodedPath(imagePath);
URI uri = GeneralTools.toURI(path);
ImageServer<BufferedImage> server = ImageServers.buildServer(uri, parseArgs(serverArgs));
imageData = new ImageData<>(server);
Object result = runScript(null, imageData);
if (result != null)
logger.info("Script result: {}", result);
server.close();
} else {
Object result = runScript(null, null);
if (result != null)
logger.info("Script result: {}", result);
}
} catch (Exception e) {
logger.error(e.getLocalizedMessage(), e);
throw new RuntimeException(e);
}
}
use of qupath.lib.images.ImageData in project qupath by qupath.
the class PathClassifierPane method runClassifier.
void runClassifier() {
QuPathViewer viewer = viewerValue.getValue();
if (viewer == null)
return;
ImageData<BufferedImage> imageData = viewer.getImageData();
if (classifier == null || imageData == null || !classifier.isValid())
return;
Collection<PathObject> pathObjects = imageData.getHierarchy().getDetectionObjects();
var availableFeatures = PathClassifierTools.getAvailableFeatures(pathObjects);
var requiredFeatures = classifier.getRequiredMeasurements();
String missingFeatures = requiredFeatures.stream().filter(p -> !availableFeatures.contains(p)).collect(Collectors.joining("\n\t"));
if (!missingFeatures.isEmpty())
logger.warn("Detection objects are missing the following feature(s):\n\t" + missingFeatures + "\nWill proceed with classification anyway..");
// SwingUtilities.getRoot(viewer).setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
try {
PathClassifierTools.runClassifier(imageData.getHierarchy(), classifier);
// Log the classifier
if (pathClassifier != null) {
imageData.getHistoryWorkflow().addStep(new RunSavedClassifierWorkflowStep(pathClassifier));
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// SwingUtilities.getRoot(viewer).setCursor(cursor);
// Update displayed list - names may have changed - and classifier summary
updateClassifierSummary();
}
}
use of qupath.lib.images.ImageData in project qupath by qupath.
the class DensityMapDataOp method apply.
@Override
public Mat apply(ImageData<BufferedImage> imageData, RegionRequest request) throws IOException {
ensureInitialized();
logger.trace("Applying density map op for {}", request);
// Calculate how much padding we need
var padding = op.getPadding();
if (!padding.isEmpty()) {
// Add padding to the request
double downsample = request.getDownsample();
var padding2 = Padding.getPadding((int) Math.round(padding.getX1() * downsample), (int) Math.round(padding.getX2() * downsample), (int) Math.round(padding.getY1() * downsample), (int) Math.round(padding.getY2() * downsample));
request = request.pad2D(padding2);
}
// Get all objects within the padded region
var allPathObjects = imageData.getHierarchy().getObjectsForRegion(null, request, null).stream().filter(allObjects).collect(Collectors.toList());
if (allPathObjects.size() == 1)
logger.trace("Generating counts tile for 1 object");
else
logger.trace("Generating counts tile for {} objects", allPathObjects.size());
// Create an output mat
int nChannels = getChannelCount();
int width = (int) Math.round(request.getWidth() / request.getDownsample());
int height = (int) Math.round(request.getHeight() / request.getDownsample());
var mat = new Mat(height, width, opencv_core.CV_64FC(nChannels), Scalar.ZERO);
DoubleIndexer idx = mat.createIndexer();
// Get points representing all the centroids of each subpopulation of object
// Use these to increment pixel values in a counts image
int c = 0;
for (var entry : primaryObjects.entrySet()) {
var predicate = entry.getValue();
var primaryROIs = allPathObjects.stream().filter(predicate).map(p -> PathObjectTools.getROI(p, true)).collect(Collectors.toList());
var points = objectsToPoints(primaryROIs);
incrementCounts(idx, points, request, width, height, c);
c++;
}
// Get points for all objects, if we need them
if (c < getChannelCount()) {
var allObjectROIs = allPathObjects.stream().map(p -> PathObjectTools.getROI(p, true)).collect(Collectors.toList());
var points = objectsToPoints(allObjectROIs);
incrementCounts(idx, points, request, width, height, c);
c++;
}
idx.close();
// Now apply the op
var output = this.op.apply(mat);
return output;
}
use of qupath.lib.images.ImageData in project qupath by qupath.
the class OMEPyramidWriterCommand method run.
@Override
public void run() {
if (currentTask != null && !currentTask.isDone()) {
if (!Dialogs.showConfirmDialog("OME Pyramid writer", "Do you want to stop the current export?"))
// TODO: Delete exporting file?
return;
else {
currentTask.cancel(true);
}
}
QuPathViewer viewer = qupath.getViewer();
int zPos = viewer.getZPosition();
int tPos = viewer.getTPosition();
ImageData<BufferedImage> imageData = viewer.getImageData();
if (imageData == null) {
Dialogs.showNoImageError("OME Pyramid writer");
return;
}
ImageServer<BufferedImage> server = imageData.getServer();
// Region
PathObject selected = imageData.getHierarchy().getSelectionModel().getSelectedObject();
ImageRegion region = null;
int width, height;
if (selected == null || !selected.hasROI() || !selected.getROI().isArea()) {
width = server.getWidth();
height = server.getHeight();
} else {
region = ImageRegion.createInstance(selected.getROI());
width = region.getWidth();
height = region.getHeight();
}
// Set compression - with a sanity check for validity, defaulting to another comparable method if necessary
CompressionType compression = getDefaultPyramidCompression();
List<String> compatibleCompression = Arrays.stream(CompressionType.values()).filter(c -> c.supportsImage(server)).map(c -> c.toFriendlyString()).collect(Collectors.toList());
if (!compatibleCompression.contains(compression.toFriendlyString()))
compression = CompressionType.DEFAULT;
var params = new ParameterList().addChoiceParameter("compression", "Compression type", compression.toFriendlyString(), compatibleCompression).addIntParameter("scaledDownsample", "Pyramidal downsample", scaledDownsample.get(), "", 1, 8, "Amount to downsample each consecutive pyramidal level; use 1 to indicate the image should not be pyramidal").addIntParameter("tileSize", "Tile size", getDefaultTileSize(), "px", "Tile size for export (should be between 128 and 8192)").addBooleanParameter("parallelize", "Parallelize export", parallelizeTiling.get(), "Export image tiles in parallel - " + "this should be faster, best keep it on unless you encounter export problems").addBooleanParameter("allZ", "All z-slices", allZ.get(), "Include all z-slices in the stack").addBooleanParameter("allT", "All timepoints", allT.get(), "Include all timepoints in the time-series");
boolean singleTile = server.getTileRequestManager().getTileRequests(RegionRequest.createInstance(server)).size() == 1;
params.setHiddenParameters(server.nZSlices() == 1, "allZ");
params.setHiddenParameters(server.nTimepoints() == 1, "allT");
params.setHiddenParameters(singleTile, "tileSize", "parallelize");
if (!Dialogs.showParameterDialog("Export OME-TIFF", params))
return;
compression = CompressionType.fromFriendlyString((String) params.getChoiceParameterValue("compression"));
defaultPyramidCompression.set(compression);
int downsampleScale = params.getIntParameterValue("scaledDownsample");
scaledDownsample.set(downsampleScale);
int tileSize = params.getIntParameterValue("tileSize");
boolean parallelize = params.getBooleanParameterValue("parallelize");
if (!singleTile) {
tileSize = GeneralTools.clipValue(tileSize, 128, 8192);
defaultTileSize.set(tileSize);
parallelizeTiling.set(parallelize);
}
boolean doAllZ = false;
boolean doAllT = false;
if (server.nZSlices() > 1) {
doAllZ = params.getBooleanParameterValue("allZ");
allZ.set(doAllZ);
}
if (server.nTimepoints() > 1) {
doAllT = params.getBooleanParameterValue("allT");
allT.set(doAllT);
}
OMEPyramidWriter.Builder builder = new OMEPyramidWriter.Builder(server);
if (region != null) {
builder = builder.region(region);
} else {
if (server.nZSlices() > 1 && !doAllZ)
builder.zSlice(zPos);
if (server.nTimepoints() > 1 && !doAllT)
builder.timePoint(tPos);
}
builder.compression(compression);
if (downsampleScale <= 1 || Math.max(width, height) / server.getDownsampleForResolution(0) < minSizeForTiling.get())
builder.downsamples(server.getDownsampleForResolution(0));
else
builder.scaledDownsampling(server.getDownsampleForResolution(0), downsampleScale);
// Set tile size; if we just have one tile, use the image width & height
if (singleTile)
builder = builder.tileSize(width, height);
else
builder = builder.tileSize(tileSize).parallelize(parallelize);
if (server.nZSlices() > 1 && doAllZ)
builder.allZSlices();
if (server.nTimepoints() > 1 && doAllT)
builder.allTimePoints();
// Prompt for file
File fileOutput = Dialogs.promptToSaveFile("Write pyramid", null, null, "OME TIFF pyramid", ".ome.tif");
if (fileOutput == null)
return;
String name = fileOutput.getName();
// We can have trouble with only the '.tif' part of the name being included
if (name.endsWith(".tif") && !name.endsWith(".ome.tif"))
fileOutput = new File(fileOutput.getParentFile(), name.substring(0, name.length() - 4) + ".ome.tif");
OMEPyramidSeries writer = builder.build();
if (pool == null) {
pool = Executors.newSingleThreadExecutor(ThreadTools.createThreadFactory("ome-pyramid-export", false));
}
currentTask = pool.submit(new WriterTask(OMEPyramidWriter.createWriter(writer), fileOutput.getAbsolutePath()));
}
Aggregations