use of qupath.lib.gui.QuPathGUI in project qupath by qupath.
the class ProjectImportImagesCommand method promptToImportImages.
/**
* Prompt to import images to the current project.
*
* @param qupath QuPath instance, used to access the current project and stage
* @param builder if not null, this will be used to create the servers. If null, a combobox will be shown to choose an installed builder.
* @param defaultPaths URIs to use to prepopulate the list
* @return
*/
static List<ProjectImageEntry<BufferedImage>> promptToImportImages(QuPathGUI qupath, ImageServerBuilder<BufferedImage> builder, String... defaultPaths) {
var project = qupath.getProject();
if (project == null) {
Dialogs.showNoProjectError(commandName);
return Collections.emptyList();
}
ListView<String> listView = new ListView<>();
listView.setPrefWidth(480);
listView.setMinHeight(100);
listView.getItems().addAll(defaultPaths);
listView.setPlaceholder(new Label("Drag & drop image or project files for import, \nor choose from the options below"));
Button btnFile = new Button("Choose files");
btnFile.setOnAction(e -> loadFromFileChooser(listView.getItems()));
Button btnURL = new Button("Input URL");
btnURL.setOnAction(e -> loadFromSingleURL(listView.getItems()));
Button btnClipboard = new Button("From clipboard");
btnClipboard.setOnAction(e -> loadFromClipboard(listView.getItems()));
Button btnFileList = new Button("From path list");
btnFileList.setOnAction(e -> loadFromTextFile(listView.getItems()));
TitledPane paneList = new TitledPane("Image paths", listView);
paneList.setCollapsible(false);
BorderPane paneImages = new BorderPane();
class BuilderListCell extends ListCell<ImageServerBuilder<BufferedImage>> {
@Override
protected void updateItem(ImageServerBuilder<BufferedImage> item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
} else {
if (item == null)
setText("Default (let QuPath decide)");
else
setText(item.getName());
}
}
}
boolean requestBuilder = builder == null;
ComboBox<ImageServerBuilder<BufferedImage>> comboBuilder = new ComboBox<>();
Label labelBuilder = new Label("Image provider");
if (requestBuilder) {
comboBuilder.setCellFactory(p -> new BuilderListCell());
comboBuilder.setButtonCell(new BuilderListCell());
List<ImageServerBuilder<BufferedImage>> availableBuilders = new ArrayList<>(ImageServerProvider.getInstalledImageServerBuilders(BufferedImage.class));
if (!availableBuilders.contains(null))
availableBuilders.add(0, null);
comboBuilder.getItems().setAll(availableBuilders);
comboBuilder.getSelectionModel().selectFirst();
labelBuilder.setLabelFor(comboBuilder);
labelBuilder.setMinWidth(Label.USE_PREF_SIZE);
}
ComboBox<ImageType> comboType = new ComboBox<>();
comboType.getItems().setAll(ImageType.values());
Label labelType = new Label("Set image type");
labelType.setLabelFor(comboType);
labelType.setMinWidth(Label.USE_PREF_SIZE);
ComboBox<Rotation> comboRotate = new ComboBox<>();
comboRotate.getItems().setAll(Rotation.values());
Label labelRotate = new Label("Rotate image");
labelRotate.setLabelFor(comboRotate);
labelRotate.setMinWidth(Label.USE_PREF_SIZE);
TextField tfArgs = new TextField();
Label labelArgs = new Label("Optional args");
labelArgs.setLabelFor(tfArgs);
labelArgs.setMinWidth(Label.USE_PREF_SIZE);
CheckBox cbPyramidalize = new CheckBox("Auto-generate pyramids");
cbPyramidalize.setSelected(pyramidalizeProperty.get());
CheckBox cbImportObjects = new CheckBox("Import objects");
cbImportObjects.setSelected(importObjectsProperty.get());
PaneTools.setMaxWidth(Double.MAX_VALUE, comboBuilder, comboType, comboRotate, cbPyramidalize, cbImportObjects, tfArgs);
PaneTools.setFillWidth(Boolean.TRUE, comboBuilder, comboType, comboRotate, cbPyramidalize, cbImportObjects, tfArgs);
PaneTools.setHGrowPriority(Priority.ALWAYS, comboBuilder, comboType, comboRotate, cbPyramidalize, cbImportObjects, tfArgs);
GridPane paneType = new GridPane();
paneType.setPadding(new Insets(5));
paneType.setHgap(5);
paneType.setVgap(5);
int row = 0;
if (requestBuilder)
PaneTools.addGridRow(paneType, row++, 0, "Specify the library used to open images", labelBuilder, comboBuilder);
PaneTools.addGridRow(paneType, row++, 0, "Specify the default image type for all images being imported (required for analysis, can be changed later under the 'Image' tab)", labelType, comboType);
PaneTools.addGridRow(paneType, row++, 0, "Optionally rotate images on import", labelRotate, comboRotate);
PaneTools.addGridRow(paneType, row++, 0, "Optionally pass reader-specific arguments to the image provider.\nUsually this should just be left empty.", labelArgs, tfArgs);
PaneTools.addGridRow(paneType, row++, 0, "Dynamically create image pyramids for large, single-resolution images", cbPyramidalize, cbPyramidalize);
PaneTools.addGridRow(paneType, row++, 0, "Read and import objects (e.g. annotations) from the image file, if possible", cbImportObjects, cbImportObjects);
paneImages.setCenter(paneList);
paneImages.setBottom(paneType);
// TilePane paneButtons = new TilePane();
// paneButtons.getChildren().addAll(btnFile, btnURL, btnClipboard, btnFileList);
GridPane paneButtons = PaneTools.createColumnGridControls(btnFile, btnURL, btnClipboard, btnFileList);
paneButtons.setHgap(5);
paneButtons.setPadding(new Insets(5));
BorderPane pane = new BorderPane();
pane.setCenter(paneImages);
pane.setBottom(paneButtons);
// Support drag & drop for files
pane.setOnDragOver(e -> {
e.acceptTransferModes(TransferMode.COPY);
e.consume();
});
pane.setOnDragDropped(e -> {
Dragboard dragboard = e.getDragboard();
if (dragboard.hasFiles()) {
logger.trace("Files dragged onto project import dialog");
try {
var paths = dragboard.getFiles().stream().filter(f -> f.isFile() && !f.isHidden()).map(f -> f.getAbsolutePath()).collect(Collectors.toList());
paths.removeAll(listView.getItems());
if (!paths.isEmpty())
listView.getItems().addAll(paths);
} catch (Exception ex) {
Dialogs.showErrorMessage("Drag & Drop", ex);
}
}
e.setDropCompleted(true);
e.consume();
});
Dialog<ButtonType> dialog = new Dialog<>();
dialog.setResizable(true);
dialog.initOwner(qupath.getStage());
dialog.setTitle("Import images to project");
ButtonType typeImport = new ButtonType("Import", ButtonData.OK_DONE);
dialog.getDialogPane().getButtonTypes().addAll(typeImport, ButtonType.CANCEL);
ScrollPane scroll = new ScrollPane(pane);
scroll.setFitToHeight(true);
scroll.setFitToWidth(true);
dialog.getDialogPane().setContent(scroll);
Optional<ButtonType> result = dialog.showAndWait();
if (!result.isPresent() || result.get() != typeImport)
return Collections.emptyList();
// // Do the actual import
// List<String> pathSucceeded = new ArrayList<>();
// List<String> pathFailed = new ArrayList<>();
// for (String path : listView.getItems()) {
// if (qupath.getProject().addImage(path.trim()))
// pathSucceeded.add(path);
// else
// pathFailed.add(path);
// }
ImageType type = comboType.getValue();
Rotation rotation = comboRotate.getValue();
boolean pyramidalize = cbPyramidalize.isSelected();
boolean importObjects = cbImportObjects.isSelected();
pyramidalizeProperty.set(pyramidalize);
importObjectsProperty.set(importObjects);
ImageServerBuilder<BufferedImage> requestedBuilder = requestBuilder ? comboBuilder.getSelectionModel().getSelectedItem() : builder;
List<String> argsList = new ArrayList<>();
String argsString = tfArgs.getText();
// TODO: Use a smarter approach to splitting! Currently we support so few arguments that splitting on spaces should be ok... for now.
String[] argsSplit = argsString == null || argsString.isBlank() ? new String[0] : argsString.split(" ");
for (var a : argsSplit) {
argsList.add(a);
}
if (rotation != null && rotation != Rotation.ROTATE_NONE) {
argsList.add("--rotate");
argsList.add(rotation.toString());
}
if (!argsList.isEmpty())
logger.debug("Args: [{}]", argsList.stream().collect(Collectors.joining(", ")));
String[] args = argsList.toArray(String[]::new);
List<String> pathSucceeded = new ArrayList<>();
List<String> pathFailed = new ArrayList<>();
List<ProjectImageEntry<BufferedImage>> entries = new ArrayList<>();
Task<Collection<ProjectImageEntry<BufferedImage>>> worker = new Task<>() {
@Override
protected Collection<ProjectImageEntry<BufferedImage>> call() throws Exception {
AtomicLong counter = new AtomicLong(0L);
List<String> items = new ArrayList<>(listView.getItems());
updateMessage("Checking for compatible image readers...");
// Limit the size of the thread pool
// The previous use of a cached thread pool caused trouble when importing may large, non-pyramidal images
var pool = Executors.newFixedThreadPool(ThreadTools.getParallelism(), ThreadTools.createThreadFactory("project-import", true));
// var pool = Executors.newCachedThreadPool(ThreadTools.createThreadFactory("project-import", true));
List<Future<List<ServerBuilder<BufferedImage>>>> results = new ArrayList<>();
List<ProjectImageEntry<BufferedImage>> projectImages = new ArrayList<>();
for (var item : items) {
// Try to load items from a project if possible
if (item.toLowerCase().endsWith(ProjectIO.DEFAULT_PROJECT_EXTENSION)) {
try {
var tempProject = ProjectIO.loadProject(GeneralTools.toURI(item), BufferedImage.class);
projectImages.addAll(tempProject.getImageList());
} catch (Exception e) {
logger.error("Unable to add images from {} ({})", item, e.getLocalizedMessage());
}
continue;
}
results.add(pool.submit(() -> {
try {
var uri = GeneralTools.toURI(item);
UriImageSupport<BufferedImage> support;
if (requestedBuilder == null)
support = ImageServers.getImageSupport(uri, args);
else
support = ImageServers.getImageSupport(requestedBuilder, uri, args);
if (support != null)
return support.getBuilders();
} catch (Exception e) {
logger.error("Unable to add {}");
logger.error(e.getLocalizedMessage(), e);
}
return new ArrayList<ServerBuilder<BufferedImage>>();
}));
}
List<ProjectImageEntry<BufferedImage>> failures = Collections.synchronizedList(new ArrayList<>());
// If we have projects, try adding images from these first
if (!projectImages.isEmpty()) {
if (projectImages.size() == 1)
updateMessage("Importing 1 image from existing projects");
else
updateMessage("Importing " + projectImages.size() + " images from existing projects");
for (var temp : projectImages) {
try {
project.addDuplicate(temp, true);
} catch (Exception e) {
failures.add(temp);
}
}
}
// If we have 'standard' image paths, use these next
List<ServerBuilder<BufferedImage>> builders = new ArrayList<>();
for (var result : results) {
try {
builders.addAll(result.get());
} catch (ExecutionException e) {
logger.error("Execution exception importing image", e);
}
}
long max = builders.size();
if (!builders.isEmpty()) {
if (max == 1)
updateMessage("Adding 1 image to project");
else
updateMessage("Adding " + max + " images to project");
// Add everything in order first
List<ProjectImageEntry<BufferedImage>> entries = new ArrayList<>();
for (var builder : builders) {
// if (rotation != null && rotation != Rotation.ROTATE_NONE)
// builder = RotatedImageServer.getRotatedBuilder(builder, rotation);
// if (swapRedBlue)
// builder = RearrangeRGBImageServer.getSwapRedBlueBuilder(builder);
entries.add(project.addImage(builder));
}
// Initialize (the slow bit)
int n = builders.size();
for (var entry : entries) {
pool.submit(() -> {
try {
initializeEntry(entry, type, pyramidalize, importObjects);
} catch (Exception e) {
failures.add(entry);
logger.warn("Exception adding " + entry, e);
} finally {
long i = counter.incrementAndGet();
updateProgress(i, max);
String name = entry.getImageName();
if (name != null) {
updateMessage("Added " + i + "/" + n + " - " + name);
}
}
});
}
}
pool.shutdown();
try {
pool.awaitTermination(60, TimeUnit.MINUTES);
} catch (Exception e) {
logger.error("Exception waiting for project import to complete: " + e.getLocalizedMessage(), e);
}
if (!failures.isEmpty()) {
String message;
if (failures.size() == 1)
message = "Failed to load one image.";
else
message = "Failed to load " + failures.size() + " images.";
if (requestedBuilder != null)
message += "\nThe image type might not be supported by '" + requestedBuilder.getName() + "'";
Dialogs.showErrorMessage("Import images", message);
var toRemove = failures.stream().filter(p -> project.getImageList().contains(p)).collect(Collectors.toList());
project.removeAllImages(toRemove, true);
}
// Now save changes
project.syncChanges();
// builders.parallelStream().forEach(builder -> {
// // builders.parallelStream().forEach(builder -> {
// try (var server = builder.build()) {
// var entry = addSingleImageToProject(project, server);
// updateMessage("Added " + entry.getImageName());
// } catch (Exception e) {
// logger.warn("Exception adding " + builder, e);
// } finally {
// updateProgress(counter.incrementAndGet(), max);
// }
// });
updateProgress(max, max);
return entries;
}
};
ProgressDialog progress = new ProgressDialog(worker);
progress.setTitle("Project import");
qupath.submitShortTask(worker);
progress.showAndWait();
try {
project.syncChanges();
} catch (IOException e1) {
Dialogs.showErrorMessage("Sync project", e1);
}
qupath.refreshProject();
StringBuilder sb = new StringBuilder();
if (!pathSucceeded.isEmpty()) {
sb.append("Successfully imported " + pathSucceeded.size() + " paths:\n");
for (String path : pathSucceeded) sb.append("\t" + path + "\n");
sb.append("\n");
qupath.refreshProject();
ProjectBrowser.syncProject(qupath.getProject());
}
if (!pathFailed.isEmpty()) {
sb.append("Unable to import " + pathFailed.size() + " paths:\n");
for (String path : pathFailed) sb.append("\t" + path + "\n");
sb.append("\n");
TextArea textArea = new TextArea();
textArea.setText(sb.toString());
if (pathSucceeded.isEmpty())
Dialogs.showErrorMessage(commandName, textArea);
else
Dialogs.showMessageDialog(commandName, textArea);
}
// TODO: Add failed and successful paths to pathFailed/pathSucceeded, so the line below prints something
if (sb.length() > 0)
logger.info(sb.toString());
return entries;
}
use of qupath.lib.gui.QuPathGUI in project qupath by qupath.
the class ExportObjectsCommand method runGeoJsonExport.
/**
* Run the path object GeoJSON export command.
* @param qupath
* @return success
* @throws IOException
*/
public static boolean runGeoJsonExport(QuPathGUI qupath) throws IOException {
// Get ImageData
var imageData = qupath.getImageData();
if (imageData == null)
return false;
// Get hierarchy
PathObjectHierarchy hierarchy = imageData.getHierarchy();
String allObjects = "All objects";
String selectedObjects = "Selected objects";
String defaultObjects = hierarchy.getSelectionModel().noSelection() ? allObjects : selectedObjects;
// Params
var parameterList = new ParameterList().addChoiceParameter("exportOptions", "Export ", defaultObjects, Arrays.asList(allObjects, selectedObjects), "Choose which objects to export - run a 'Select annotations/detections' command first if needed").addBooleanParameter("excludeMeasurements", "Exclude measurements", false, "Exclude object measurements during export - for large numbers of detections this can help reduce the file size").addBooleanParameter("doPretty", "Pretty JSON", false, "Pretty GeoJSON is more human-readable but results in larger file sizes").addBooleanParameter("doFeatureCollection", "Export as FeatureCollection", true, "Export as a 'FeatureCollection', which is a standard GeoJSON way to represent multiple objects; if not, a regular JSON object/array will be export").addBooleanParameter("doZip", "Compress data (zip)", false, "Compressed files take less memory");
if (!Dialogs.showParameterDialog("Export objects", parameterList))
return false;
Collection<PathObject> toProcess;
var comboChoice = parameterList.getChoiceParameterValue("exportOptions");
if (comboChoice.equals("Selected objects")) {
if (hierarchy.getSelectionModel().noSelection()) {
Dialogs.showErrorMessage("No selection", "No selection detected!");
return false;
}
toProcess = hierarchy.getSelectionModel().getSelectedObjects();
} else
toProcess = hierarchy.getObjects(null, null);
// Remove PathRootObject
toProcess = toProcess.stream().filter(e -> !e.isRootObject()).collect(Collectors.toList());
// Check if includes ellipse(s), as they will need to be polygonized
var nEllipses = toProcess.stream().filter(ann -> isEllipse(ann)).count();
if (nEllipses > 0) {
String message = nEllipses == 1 ? "1 ellipse will be polygonized, continue?" : String.format("%d ellipses will be polygonized, continue?", nEllipses);
var response = Dialogs.showYesNoDialog("Ellipse polygonization", message);
if (!response)
return false;
}
File outFile;
// Get default name & output directory
var project = qupath.getProject();
String defaultName = imageData.getServer().getMetadata().getName();
if (project != null) {
var entry = project.getEntry(imageData);
if (entry != null)
defaultName = entry.getImageName();
}
defaultName = GeneralTools.getNameWithoutExtension(defaultName);
File defaultDirectory = project == null || project.getPath() == null ? null : project.getPath().toFile();
while (defaultDirectory != null && !defaultDirectory.isDirectory()) defaultDirectory = defaultDirectory.getParentFile();
if (parameterList.getBooleanParameterValue("doZip"))
outFile = Dialogs.promptToSaveFile("Export to file", defaultDirectory, defaultName + ".zip", "ZIP archive", ".zip");
else
outFile = Dialogs.promptToSaveFile("Export to file", defaultDirectory, defaultName + ".geojson", "GeoJSON", ".geojson");
// If user cancels
if (outFile == null)
return false;
List<GeoJsonExportOptions> options = new ArrayList<>();
if (parameterList.getBooleanParameterValue("excludeMeasurements"))
options.add(GeoJsonExportOptions.EXCLUDE_MEASUREMENTS);
if (parameterList.getBooleanParameterValue("doPretty"))
options.add(GeoJsonExportOptions.PRETTY_JSON);
if (parameterList.getBooleanParameterValue("doFeatureCollection"))
options.add(GeoJsonExportOptions.FEATURE_COLLECTION);
// Export
QP.exportObjectsToGeoJson(toProcess, outFile.getAbsolutePath(), options.toArray(GeoJsonExportOptions[]::new));
// Notify user of success
int nObjects = toProcess.size();
String message = nObjects == 1 ? "1 object was exported to " + outFile.getAbsolutePath() : String.format("%d objects were exported to %s", nObjects, outFile.getAbsolutePath());
Dialogs.showInfoNotification("Succesful export", message);
// Get history workflow
var historyWorkflow = imageData.getHistoryWorkflow();
// args for workflow step
Map<String, String> map = new LinkedHashMap<>();
map.put("path", outFile.getPath());
String method = comboChoice.equals(allObjects) ? "exportAllObjectsToGeoJson" : "exportSelectedObjectsToGeoJson";
String methodTitle = comboChoice.equals(allObjects) ? "Export all objects" : "Export selected objects";
String optionsString = options.stream().map(o -> "\"" + o.name() + "\"").collect(Collectors.joining(", "));
map.put("options", optionsString);
if (!optionsString.isEmpty())
optionsString = ", " + optionsString;
String methodString = String.format("%s(%s%s)", method, "\"" + GeneralTools.escapeFilePath(outFile.getPath()) + "\"", optionsString);
historyWorkflow.addStep(new DefaultScriptableWorkflowStep(methodTitle, map, methodString));
return true;
}
use of qupath.lib.gui.QuPathGUI in project qupath by qupath.
the class TMACommands method promptToExportTMAData.
/**
* Prompt to export summary TMA data for a specific image to a directory.
* @param qupath
* @param imageData
*/
public static void promptToExportTMAData(QuPathGUI qupath, ImageData<BufferedImage> imageData) {
String title = "Export TMA data";
if (imageData == null) {
Dialogs.showNoImageError(title);
return;
}
PathObjectHierarchy hierarchy = imageData == null ? null : imageData.getHierarchy();
if (hierarchy == null || hierarchy.isEmpty() || hierarchy.getTMAGrid() == null || hierarchy.getTMAGrid().nCores() == 0) {
Dialogs.showErrorMessage(title, "No TMA data available!");
return;
}
var overlayOptions = qupath.getViewers().stream().filter(v -> v.getImageData() == imageData).map(v -> v.getOverlayOptions()).findFirst().orElse(qupath.getOverlayOptions());
String defaultName = ServerTools.getDisplayableImageName(imageData.getServer());
File file = Dialogs.promptToSaveFile(null, null, defaultName, "TMA data", ".qptma");
if (file != null) {
if (!file.getName().endsWith(".qptma"))
file = new File(file.getParentFile(), file.getName() + ".qptma");
double downsample = PathPrefs.tmaExportDownsampleProperty().get();
TMADataIO.writeTMAData(file, imageData, overlayOptions, downsample);
WorkflowStep step = new DefaultScriptableWorkflowStep("Export TMA data", "exportTMAData(\"" + GeneralTools.escapeFilePath(file.getParentFile().getAbsolutePath()) + "\", " + downsample + ")");
imageData.getHistoryWorkflow().addStep(step);
// PathAwtIO.writeTMAData(file, imageData, viewer.getOverlayOptions(), Double.NaN);
}
}
use of qupath.lib.gui.QuPathGUI in project qupath by qupath.
the class QPEx method copyToClipboard.
/**
* Try to copy an object to the clipboard.
* This will attempt to perform a smart conversion; for example, if a window is provided a snapshot will be taken
* and copied as an image.
* @param o the object to copy
*/
public static void copyToClipboard(Object o) {
if (!Platform.isFxApplicationThread()) {
Object o2 = o;
Platform.runLater(() -> copyToClipboard(o2));
return;
}
ClipboardContent content = new ClipboardContent();
// Handle things that are (or could become) images
if (o instanceof BufferedImage)
o = SwingFXUtils.toFXImage((BufferedImage) o, null);
if (o instanceof QuPathGUI)
o = ((QuPathGUI) o).getStage();
if (o instanceof QuPathViewer)
o = ((QuPathViewer) o).getView();
if (o instanceof Window)
o = ((Window) o).getScene();
if (o instanceof Scene)
o = ((Scene) o).snapshot(null);
if (o instanceof Node)
o = ((Node) o).snapshot(null, null);
if (o instanceof Image)
content.putImage((Image) o);
// Handle files
List<File> files = null;
if (o instanceof File)
files = Arrays.asList((File) o);
else if (o instanceof File[])
files = Arrays.asList((File[]) o);
else if (o instanceof Collection) {
files = new ArrayList<>();
for (var something : (Collection<?>) o) {
if (something instanceof File)
files.add((File) something);
}
}
if (files != null && !files.isEmpty())
content.putFiles(files);
// Handle URLs
if (o instanceof URL)
content.putUrl(((URL) o).toString());
// Always put a String representation
content.putString(o.toString());
Clipboard.getSystemClipboard().setContent(content);
}
use of qupath.lib.gui.QuPathGUI in project qupath by qupath.
the class TMASummaryViewer method setTMAEntriesFromOpenProject.
private void setTMAEntriesFromOpenProject() {
QuPathGUI qupath = QuPathGUI.getInstance();
if (qupath == null || qupath.getProject() == null || qupath.getProject().getImageList().isEmpty()) {
Dialogs.showNoProjectError("Show TMA summary");
return;
}
Project<BufferedImage> project = qupath.getProject();
List<TMAEntry> entries = new ArrayList<>();
for (ProjectImageEntry<BufferedImage> imageEntry : project.getImageList()) {
if (imageEntry.hasImageData()) {
try {
ImageData<BufferedImage> imageData = imageEntry.readImageData();
entries.addAll(getEntriesForTMAData(imageData));
} catch (IOException e) {
logger.error("Unable to read ImageData for {} ({})", imageEntry.getImageName(), e.getLocalizedMessage());
}
}
}
setTMAEntries(entries);
stage.setTitle("TMA Viewer: " + project.getName());
}
Aggregations