use of qupath.lib.projects.ProjectImageEntry 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.projects.ProjectImageEntry in project qupath by qupath.
the class ProjectMetadataEditorCommand method showProjectMetadataEditor.
public static void showProjectMetadataEditor(Project<?> project) {
if (project == null) {
logger.warn("No project available!");
return;
}
Set<String> metadataNameSet = new TreeSet<>();
List<ImageEntryWrapper> entries = new ArrayList<>();
for (ProjectImageEntry<?> entry : project.getImageList()) {
entries.add(new ImageEntryWrapper(entry));
metadataNameSet.addAll(entry.getMetadataKeys());
}
TableView<ImageEntryWrapper> table = new TableView<>();
TableColumn<ImageEntryWrapper, String> colName = new TableColumn<>(IMAGE_NAME);
colName.setCellValueFactory(v -> v.getValue().getNameBinding());
table.getColumns().add(colName);
table.setEditable(true);
for (String metadataName : metadataNameSet) {
TableColumn<ImageEntryWrapper, String> col = new TableColumn<>(metadataName);
col.setCellFactory(TextFieldTableCell.<ImageEntryWrapper>forTableColumn());
col.setOnEditCommit(e -> {
ImageEntryWrapper entry = e.getRowValue();
String n = e.getNewValue();
if (n == null || n.isEmpty())
entry.removeMetadataValue(e.getTableColumn().getText());
else
entry.putMetadataValue(e.getTableColumn().getText(), n);
});
col.setCellValueFactory(v -> v.getValue().getProperty(metadataName));
col.setEditable(true);
table.getColumns().add(col);
}
table.getItems().setAll(entries);
table.getSelectionModel().setCellSelectionEnabled(true);
table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
// Handle deleting entries
table.addEventHandler(KeyEvent.KEY_RELEASED, e -> {
if (e.getCode() == KeyCode.BACK_SPACE || e.getCode() == KeyCode.DELETE) {
var positions = table.getSelectionModel().getSelectedCells().stream().filter(p -> !IMAGE_NAME.equals(p.getTableColumn().getText())).collect(Collectors.toList());
if (positions.isEmpty())
return;
if (positions.size() == 1) {
setTextForSelectedCells(positions, null);
} else {
if (Dialogs.showConfirmDialog("Project metadata", "Clear metadata for " + positions.size() + " selected cells?")) {
setTextForSelectedCells(positions, null);
}
}
table.refresh();
}
});
BooleanBinding selectedCells = Bindings.createBooleanBinding(() -> table.getSelectionModel().selectedItemProperty() == null, table.getSelectionModel().selectedItemProperty());
MenuBar menubar = new MenuBar();
Menu menuEdit = new Menu("Edit");
MenuItem miCopy = new MenuItem("Copy selected cells");
miCopy.disableProperty().bind(selectedCells);
miCopy.setAccelerator(new KeyCodeCombination(KeyCode.C, KeyCombination.SHORTCUT_DOWN));
miCopy.setOnAction(e -> copySelectedCellsToClipboard(table, true));
MenuItem miCopyFull = new MenuItem("Copy full table");
miCopyFull.setOnAction(e -> copyEntireTableToClipboard(table));
MenuItem miPaste = new MenuItem("Paste");
miPaste.setAccelerator(new KeyCodeCombination(KeyCode.V, KeyCombination.SHORTCUT_DOWN));
miPaste.disableProperty().bind(selectedCells);
miPaste.setOnAction(e -> pasteClipboardContentsToTable(table));
MenuItem miSet = new MenuItem("Set cell contents");
miSet.disableProperty().bind(selectedCells);
miSet.setOnAction(e -> {
String input = Dialogs.showInputDialog("Set metadata cells", "Metadata text", "");
if (input == null)
return;
setTextForSelectedCells(table.getSelectionModel().getSelectedCells(), input);
table.refresh();
});
menuEdit.getItems().addAll(miCopy, miCopyFull, miPaste, miSet);
menubar.getMenus().add(menuEdit);
BorderPane pane = new BorderPane();
pane.setTop(menubar);
pane.setCenter(table);
// menubar.setUseSystemMenuBar(true);
menubar.useSystemMenuBarProperty().bindBidirectional(PathPrefs.useSystemMenubarProperty());
Dialog<ButtonType> dialog = new Dialog<>();
var qupath = QuPathGUI.getInstance();
if (qupath != null)
dialog.initOwner(qupath.getStage());
dialog.setTitle("Project metadata");
dialog.setHeaderText(null);
dialog.setResizable(true);
dialog.getDialogPane().setContent(pane);
dialog.getDialogPane().getButtonTypes().setAll(ButtonType.OK, ButtonType.CANCEL);
dialog.getDialogPane().setPrefWidth(500);
Optional<ButtonType> result = dialog.showAndWait();
if (result.isPresent() && result.get().getButtonData() == ButtonData.OK_DONE) {
// Make the changes
for (ImageEntryWrapper wrapper : entries) {
wrapper.commitChanges();
}
// Write the project
ProjectBrowser.syncProject(project);
}
// Stage stage = new Stage();
// stage.initOwner(qupath.getStage());
// stage.setTitle("Project metadata");
// stage.setScene(new Scene(pane, 500, 400));
// stage.showAndWait();
}
use of qupath.lib.projects.ProjectImageEntry in project qupath by qupath.
the class QuPathGUI method openImage.
/**
* Open a new whole slide image server, or ImageData.
* If the path is the same as a currently-open server, do nothing.
*
* @param viewer the viewer into which the image should be opened
* @param pathNew
* @param prompt if true, give the user the opportunity to cancel opening if a whole slide server is already set
* @param includeURLs if true, any prompt should support URL input and not only a file chooser
* @return true if the server was set for this GUI, false otherwise
* @throws IOException
*/
public boolean openImage(QuPathViewer viewer, String pathNew, boolean prompt, boolean includeURLs) throws IOException {
if (viewer == null) {
if (getViewers().size() == 1)
viewer = getViewer();
else {
Dialogs.showErrorMessage("Open image", "Please specify the viewer where the image should be opened!");
return false;
}
}
ImageServer<BufferedImage> server = viewer.getServer();
String pathOld = null;
File fileBase = null;
if (server != null) {
var uris = server.getURIs();
if (uris.size() == 1) {
var uri = uris.iterator().next();
pathOld = uri.toString();
try {
var path = GeneralTools.toPath(uri);
if (path != null)
fileBase = path.toFile().getParentFile();
} catch (Exception e) {
}
;
}
// pathOld = server.getPath();
// try {
// fileBase = new File(pathOld).getParentFile();
// } catch (Exception e) {};
}
// Prompt for a path, if required
File fileNew = null;
if (pathNew == null) {
if (includeURLs) {
pathNew = Dialogs.promptForFilePathOrURL("Choose path", pathOld, fileBase, null);
if (pathNew == null)
return false;
fileNew = new File(pathNew);
} else {
fileNew = Dialogs.promptForFile(null, fileBase, null);
if (fileNew == null)
return false;
pathNew = fileNew.getAbsolutePath();
}
} else
fileNew = new File(pathNew);
// If we have a file, check if it is a data file - if so, handle differently
if (fileNew.isFile() && GeneralTools.checkExtensions(pathNew, PathPrefs.getSerializationExtension()))
return openSavedData(viewer, fileNew, false, true);
// Check for project file
if (fileNew.isFile() && GeneralTools.checkExtensions(pathNew, ProjectIO.getProjectExtension())) {
logger.info("Trying to load project {}", fileNew.getAbsolutePath());
try {
Project<BufferedImage> project = ProjectIO.loadProject(fileNew, BufferedImage.class);
if (project != null) {
setProject(project);
return true;
}
} catch (Exception e) {
Dialogs.showErrorMessage("Open project", e);
logger.error("Error opening project " + fileNew.getAbsolutePath(), e);
return false;
}
}
// Try opening an image, unless it's the same as the image currently open
if (!pathNew.equals(pathOld)) {
// If we have a project, show the import dialog
if (getProject() != null) {
List<ProjectImageEntry<BufferedImage>> entries = ProjectCommands.promptToImportImages(this, pathNew);
if (entries.isEmpty())
return false;
return openImageEntry(entries.get(0));
}
ImageServer<BufferedImage> serverNew = null;
UriImageSupport<BufferedImage> support = ImageServerProvider.getPreferredUriImageSupport(BufferedImage.class, pathNew);
List<ServerBuilder<BufferedImage>> builders = support == null ? Collections.emptyList() : support.getBuilders();
if (builders.isEmpty()) {
String message = "Unable to build ImageServer for " + pathNew + ".\nSee View > Show log for more details";
Dialogs.showErrorMessage("Unable to build server", message);
return false;
} else if (builders.size() == 1) {
try {
serverNew = builders.get(0).build();
} catch (Exception e) {
logger.error("Error building server: " + e.getLocalizedMessage(), e);
}
} else {
var selector = new ServerSelector(builders);
serverNew = selector.promptToSelectServer();
if (serverNew == null)
return false;
}
if (serverNew != null) {
if (pathOld != null && prompt && !viewer.getHierarchy().isEmpty()) {
if (!Dialogs.showYesNoDialog("Replace open image", "Close " + ServerTools.getDisplayableImageName(server) + "?"))
return false;
}
ImageData<BufferedImage> imageData = null;
if (serverNew != null) {
int minSize = PathPrefs.minPyramidDimensionProperty().get();
if (serverNew.nResolutions() == 1 && Math.max(serverNew.getWidth(), serverNew.getHeight()) > minSize) {
// Check if we have any hope at all with the current settings
long estimatedBytes = (long) serverNew.getWidth() * (long) serverNew.getHeight() * (long) serverNew.nChannels() * (long) serverNew.getPixelType().getBytesPerPixel();
double requiredBytes = estimatedBytes * (4.0 / 3.0);
if (prompt && imageRegionStore != null && requiredBytes >= imageRegionStore.getTileCacheSize()) {
logger.warn("Selected image is {} x {} x {} pixels ({})", serverNew.getWidth(), serverNew.getHeight(), serverNew.nChannels(), serverNew.getPixelType());
Dialogs.showErrorMessage("Image too large", "Non-pyramidal image is too large for the available tile cache!\n" + "Try converting the image to a pyramidal file format, or increasing the memory available to QuPath.");
return false;
}
// Offer to pyramidalize
var serverWrapped = ImageServers.pyramidalize(serverNew);
if (serverWrapped.nResolutions() > 1) {
if (prompt) {
var response = Dialogs.showYesNoCancelDialog("Auto pyramidalize", "QuPath works best with large images saved in a pyramidal format.\n\n" + "Do you want to generate a pyramid dynamically from " + ServerTools.getDisplayableImageName(serverNew) + "?" + "\n(This requires more memory, but is usually worth it)");
if (response == DialogButton.CANCEL)
return false;
if (response == DialogButton.YES)
serverNew = serverWrapped;
}
}
}
imageData = createNewImageData(serverNew);
}
viewer.setImageData(imageData);
if (imageData.getImageType() == ImageType.UNSET && PathPrefs.imageTypeSettingProperty().get() == ImageTypeSetting.PROMPT) {
var type = GuiTools.estimateImageType(serverNew, imageRegionStore.getThumbnail(serverNew, 0, 0, true));
ImageDetailsPane.promptToSetImageType(imageData, type);
}
return true;
} else {
// Show an error message if we can't open the file
Dialogs.showErrorNotification("Open image", "Sorry, I can't open " + pathNew);
// logger.error("Unable to build whole slide server for path '{}'", pathNew);
}
}
return false;
}
use of qupath.lib.projects.ProjectImageEntry in project qupath by qupath.
the class MeasurementExporter method exportMeasurements.
/**
* Exports the measurements of one or more entries in the project.
* This function first opens all the images in the project to store
* all the column names and values of the measurements.
* Then, it loops through the maps containing the values to write
* them to the given output stream.
* @param stream
*/
public void exportMeasurements(OutputStream stream) {
long startTime = System.currentTimeMillis();
Map<ProjectImageEntry<?>, String[]> imageCols = new HashMap<>();
Map<ProjectImageEntry<?>, Integer> nImageEntries = new HashMap<>();
List<String> allColumns = new ArrayList<>();
Multimap<String, String> valueMap = LinkedListMultimap.create();
String pattern = "(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)";
for (ProjectImageEntry<?> entry : imageList) {
try {
ImageData<?> imageData = entry.readImageData();
ObservableMeasurementTableData model = new ObservableMeasurementTableData();
Collection<PathObject> pathObjects = imageData == null ? Collections.emptyList() : imageData.getHierarchy().getObjects(null, type);
if (filter != null)
pathObjects = pathObjects.stream().filter(filter).collect(Collectors.toList());
model.setImageData(imageData, pathObjects);
List<String> data = SummaryMeasurementTableCommand.getTableModelStrings(model, separator, excludeColumns);
// Get header
String[] header;
String headerString = data.get(0);
if (headerString.chars().filter(e -> e == '"').count() > 1)
header = headerString.split(separator.equals("\t") ? "\\" + separator : separator + pattern, -1);
else
header = headerString.split(separator);
imageCols.put(entry, header);
nImageEntries.put(entry, data.size() - 1);
for (String col : header) {
if (!allColumns.contains(col) && !excludeColumns.contains(col))
allColumns.add(col);
}
// To keep the same column order, just delete non-relevant columns
if (!includeOnlyColumns.isEmpty())
allColumns.removeIf(n -> !includeOnlyColumns.contains(n));
for (int i = 1; i < data.size(); i++) {
String[] row;
String rowString = data.get(i);
// Check if some values in the row are escaped
if (rowString.chars().filter(e -> e == '"').count() > 1)
row = rowString.split(separator.equals("\t") ? "\\" + separator : separator + pattern, -1);
else
row = rowString.split(separator);
// Put value in map
for (int elem = 0; elem < row.length; elem++) {
if (allColumns.contains(header[elem]))
valueMap.put(header[elem], row[elem]);
}
}
} catch (Exception e) {
logger.error(e.getLocalizedMessage(), e);
}
}
try (PrintWriter writer = new PrintWriter(new OutputStreamWriter(stream, StandardCharsets.UTF_8))) {
writer.write(String.join(separator, allColumns));
writer.write(System.lineSeparator());
Iterator[] its = new Iterator[allColumns.size()];
for (int col = 0; col < allColumns.size(); col++) {
its[col] = valueMap.get(allColumns.get(col)).iterator();
}
for (ProjectImageEntry<?> entry : imageList) {
for (int nObject = 0; nObject < nImageEntries.get(entry); nObject++) {
for (int nCol = 0; nCol < allColumns.size(); nCol++) {
if (Arrays.stream(imageCols.get(entry)).anyMatch(allColumns.get(nCol)::equals)) {
String val = (String) its[nCol].next();
// NaN values -> blank
if (val.equals("NaN"))
val = "";
writer.write(val);
}
if (nCol < allColumns.size() - 1)
writer.write(separator);
}
writer.write(System.lineSeparator());
}
}
} catch (Exception e) {
logger.error("Error writing to file: " + e.getLocalizedMessage(), e);
}
long endTime = System.currentTimeMillis();
long timeMillis = endTime - startTime;
String time = null;
if (timeMillis > 1000 * 60)
time = String.format("Total processing time: %.2f minutes", timeMillis / (1000.0 * 60.0));
else if (timeMillis > 1000)
time = String.format("Total processing time: %.2f seconds", timeMillis / (1000.0));
else
time = String.format("Total processing time: %d milliseconds", timeMillis);
logger.info("Processed {} images", imageList.size());
logger.info(time);
}
use of qupath.lib.projects.ProjectImageEntry in project qupath by qupath.
the class ProjectBrowser method getPopup.
ContextMenu getPopup() {
Action actionOpenImage = new Action("Open image", e -> qupath.openImageEntry(getSelectedEntry()));
Action actionRemoveImage = new Action("Remove image(s)", e -> {
Collection<ImageRow> imageRows = getSelectedImageRowsRecursive();
Collection<ProjectImageEntry<BufferedImage>> entries = ProjectTreeRow.getEntries(imageRows);
if (entries.isEmpty())
return;
// Don't allow us to remove any entries that are currently open (in any viewer)
for (var viewer : qupath.getViewers()) {
var imageData = viewer.getImageData();
var entry = imageData == null ? null : getProject().getEntry(imageData);
if (entry != null && entries.contains(entry)) {
Dialogs.showErrorMessage("Remove project entries", "Please close all images you want to remove!");
return;
}
}
if (entries.size() == 1) {
if (!Dialogs.showConfirmDialog("Remove project entry", "Remove " + entries.iterator().next().getImageName() + " from project?"))
return;
} else if (!Dialogs.showYesNoDialog("Remove project entries", String.format("Remove %d entries?", entries.size())))
return;
var result = Dialogs.showYesNoCancelDialog("Remove project entries", "Delete all associated data?");
if (result == DialogButton.CANCEL)
return;
project.removeAllImages(entries, result == DialogButton.YES);
refreshTree(null);
syncProject(project);
if (tree != null) {
boolean isExpanded = tree.getRoot() != null && tree.getRoot().isExpanded();
tree.setRoot(model.getRoot());
tree.getRoot().setExpanded(isExpanded);
}
});
Action actionDuplicateImages = new Action("Duplicate image(s)", e -> {
Collection<ImageRow> imageRows = getSelectedImageRowsRecursive();
if (imageRows.isEmpty()) {
logger.debug("Nothing to duplicate - no entries selected");
return;
}
boolean singleImage = false;
String name = "";
String title = "Duplicate images";
String namePrompt = "Append to image name";
String nameHelp = "Specify text to append to the image name to distinguish duplicated images";
if (imageRows.size() == 1) {
title = "Duplicate image";
namePrompt = "Duplicate image name";
nameHelp = "Specify name for the duplicated image";
singleImage = true;
name = imageRows.iterator().next().getDisplayableString();
name = GeneralTools.generateDistinctName(name, project.getImageList().stream().map(p -> p.getImageName()).collect(Collectors.toSet()));
}
var params = new ParameterList().addStringParameter("name", namePrompt, name, nameHelp).addBooleanParameter("copyData", "Also duplicate data files", true, "Duplicate any associated data files along with the image");
if (!Dialogs.showParameterDialog(title, params))
return;
boolean copyData = params.getBooleanParameterValue("copyData");
name = params.getStringParameterValue("name");
// Ensure we have a single space and then the text to append, with extra whitespace removed
if (!singleImage && !name.isBlank())
name = " " + name.strip();
for (var imageRow : imageRows) {
try {
var newEntry = project.addDuplicate(ProjectTreeRow.getEntry(imageRow), copyData);
if (newEntry != null && !name.isBlank()) {
if (singleImage)
newEntry.setImageName(name);
else
newEntry.setImageName(newEntry.getImageName() + name);
}
} catch (Exception ex) {
Dialogs.showErrorNotification("Duplicating image", "Error duplicating " + ProjectTreeRow.getEntry(imageRow).getImageName());
logger.error(ex.getLocalizedMessage(), ex);
}
}
try {
project.syncChanges();
} catch (Exception ex) {
logger.error("Error synchronizing project changes: " + ex.getLocalizedMessage(), ex);
}
refreshProject();
if (imageRows.size() == 1)
logger.debug("Duplicated 1 image entry");
else
logger.debug("Duplicated {} image entries");
});
Action actionSetImageName = new Action("Rename image", e -> {
TreeItem<ProjectTreeRow> path = tree.getSelectionModel().getSelectedItem();
if (path == null)
return;
if (path.getValue().getType() == ProjectTreeRow.Type.IMAGE) {
if (setProjectEntryImageName(ProjectTreeRow.getEntry(path.getValue())) && project != null)
syncProject(project);
}
});
// Add a metadata value
Action actionAddMetadataValue = new Action("Add metadata", e -> {
Project<BufferedImage> project = getProject();
Collection<ImageRow> imageRows = getSelectedImageRowsRecursive();
if (project != null && !imageRows.isEmpty()) {
TextField tfMetadataKey = new TextField();
var suggestions = project.getImageList().stream().map(p -> p.getMetadataKeys()).flatMap(Collection::stream).distinct().sorted().collect(Collectors.toList());
TextFields.bindAutoCompletion(tfMetadataKey, suggestions);
TextField tfMetadataValue = new TextField();
Label labKey = new Label("New key");
Label labValue = new Label("New value");
labKey.setLabelFor(tfMetadataKey);
labValue.setLabelFor(tfMetadataValue);
tfMetadataKey.setTooltip(new Tooltip("Enter the name for the metadata entry"));
tfMetadataValue.setTooltip(new Tooltip("Enter the value for the metadata entry"));
ProjectImageEntry<BufferedImage> entry = imageRows.size() == 1 ? ProjectTreeRow.getEntry(imageRows.iterator().next()) : null;
int nMetadataValues = entry == null ? 0 : entry.getMetadataKeys().size();
GridPane pane = new GridPane();
pane.setVgap(5);
pane.setHgap(5);
pane.add(labKey, 0, 0);
pane.add(tfMetadataKey, 1, 0);
pane.add(labValue, 0, 1);
pane.add(tfMetadataValue, 1, 1);
String name = imageRows.size() + " images";
if (entry != null) {
name = entry.getImageName();
if (nMetadataValues > 0) {
Label labelCurrent = new Label("Current metadata");
TextArea textAreaCurrent = new TextArea();
textAreaCurrent.setEditable(false);
String keyString = entry.getMetadataSummaryString();
if (keyString.isEmpty())
textAreaCurrent.setText("No metadata entries yet");
else
textAreaCurrent.setText(keyString);
textAreaCurrent.setPrefRowCount(3);
labelCurrent.setLabelFor(textAreaCurrent);
pane.add(labelCurrent, 0, 2);
pane.add(textAreaCurrent, 1, 2);
}
}
Dialog<ButtonType> dialog = new Dialog<>();
dialog.setTitle("Metadata");
dialog.getDialogPane().getButtonTypes().setAll(ButtonType.OK, ButtonType.CANCEL);
dialog.getDialogPane().setHeaderText("Set metadata for " + name);
dialog.getDialogPane().setContent(pane);
Optional<ButtonType> result = dialog.showAndWait();
if (result.isPresent() && result.get() == ButtonType.OK) {
String key = tfMetadataKey.getText().trim();
String value = tfMetadataValue.getText();
if (key.isEmpty()) {
logger.warn("Attempted to set metadata value for {}, but key was empty!", name);
} else {
// Set metadata for all entries
for (var temp : imageRows) ProjectTreeRow.getEntry(temp).putMetadataValue(key, value);
syncProject(project);
tree.refresh();
}
}
} else {
Dialogs.showErrorMessage("Edit image description", "No entry is selected!");
}
});
// Edit the description for the image
Action actionEditDescription = new Action("Edit description", e -> {
Project<?> project = getProject();
ProjectImageEntry<?> entry = getSelectedEntry();
if (project != null && entry != null) {
if (showDescriptionEditor(entry)) {
descriptionText.set(entry.getDescription());
syncProject(project);
}
} else {
Dialogs.showErrorMessage("Edit image description", "No entry is selected!");
}
});
// Mask the name of the images and shuffle the entry
Action actionMaskImageNames = ActionTools.createSelectableAction(PathPrefs.maskImageNamesProperty(), "Mask image names");
// Refresh thumbnail according to current display settings
Action actionRefreshThumbnail = new Action("Refresh thumbnail", e -> {
TreeItem<ProjectTreeRow> path = tree.getSelectionModel().getSelectedItem();
if (path == null)
return;
if (path.getValue().getType() == ProjectTreeRow.Type.IMAGE) {
ProjectImageEntry<BufferedImage> entry = ProjectTreeRow.getEntry(path.getValue());
if (!isCurrentImage(entry)) {
logger.warn("Cannot refresh entry for image that is not open!");
return;
}
BufferedImage imgThumbnail = qupath.getViewer().getRGBThumbnail();
imgThumbnail = resizeForThumbnail(imgThumbnail);
try {
entry.setThumbnail(imgThumbnail);
} catch (IOException e1) {
logger.error("Error writing thumbnail", e1);
}
tree.refresh();
}
});
// Open the project directory using Explorer/Finder etc.
Action actionOpenProjectDirectory = createBrowsePathAction("Project...", () -> getProjectPath());
Action actionOpenProjectEntryDirectory = createBrowsePathAction("Project entry...", () -> getProjectEntryPath());
Action actionOpenImageServerDirectory = createBrowsePathAction("Image server...", () -> getImageServerPath());
Menu menuSort = new Menu("Sort by...");
ContextMenu menu = new ContextMenu();
var hasProjectBinding = qupath.projectProperty().isNotNull();
var menuOpenDirectories = MenuTools.createMenu("Open directory...", actionOpenProjectDirectory, actionOpenProjectEntryDirectory, actionOpenImageServerDirectory);
menuOpenDirectories.visibleProperty().bind(hasProjectBinding);
// MenuItem miOpenProjectDirectory = ActionUtils.createMenuItem(actionOpenProjectDirectory);
MenuItem miOpenImage = ActionUtils.createMenuItem(actionOpenImage);
MenuItem miRemoveImage = ActionUtils.createMenuItem(actionRemoveImage);
MenuItem miDuplicateImage = ActionUtils.createMenuItem(actionDuplicateImages);
MenuItem miSetImageName = ActionUtils.createMenuItem(actionSetImageName);
MenuItem miRefreshThumbnail = ActionUtils.createMenuItem(actionRefreshThumbnail);
MenuItem miEditDescription = ActionUtils.createMenuItem(actionEditDescription);
MenuItem miAddMetadata = ActionUtils.createMenuItem(actionAddMetadataValue);
MenuItem miMaskImages = ActionUtils.createCheckMenuItem(actionMaskImageNames);
// Set visibility as menu being displayed
menu.setOnShowing(e -> {
TreeItem<ProjectTreeRow> selected = tree.getSelectionModel().getSelectedItem();
ProjectImageEntry<BufferedImage> selectedEntry = selected == null ? null : ProjectTreeRow.getEntry(selected.getValue());
var entries = getSelectedImageRowsRecursive();
boolean isImageEntry = selectedEntry != null;
// miOpenProjectDirectory.setVisible(project != null && project.getBaseDirectory().exists());
miOpenImage.setVisible(isImageEntry);
miDuplicateImage.setVisible(isImageEntry);
miSetImageName.setVisible(isImageEntry);
miAddMetadata.setVisible(!entries.isEmpty());
miEditDescription.setVisible(isImageEntry);
miRefreshThumbnail.setVisible(isImageEntry && isCurrentImage(selectedEntry));
miRemoveImage.setVisible(selected != null && project != null && !project.getImageList().isEmpty());
if (project == null) {
menuSort.setVisible(false);
return;
}
Map<String, MenuItem> newItems = new TreeMap<>();
for (ProjectImageEntry<?> entry : project.getImageList()) {
// Add all entry metadata keys
for (String key : entry.getMetadataKeys()) {
if (!newItems.containsKey(key))
newItems.put(key, ActionUtils.createMenuItem(createSortByKeyAction(key, key)));
}
// Add all additional keys
for (String key : baseMetadataKeys) {
if (!newItems.containsKey(key))
newItems.put(key, ActionUtils.createMenuItem(createSortByKeyAction(key, key)));
}
}
menuSort.getItems().setAll(newItems.values());
menuSort.getItems().add(0, ActionUtils.createMenuItem(createSortByKeyAction("None", null)));
menuSort.getItems().add(1, new SeparatorMenuItem());
menuSort.setVisible(true);
if (menu.getItems().isEmpty())
e.consume();
});
SeparatorMenuItem separator = new SeparatorMenuItem();
separator.visibleProperty().bind(menuSort.visibleProperty());
menu.getItems().addAll(miOpenImage, miRemoveImage, miDuplicateImage, new SeparatorMenuItem(), miSetImageName, miAddMetadata, miEditDescription, miMaskImages, miRefreshThumbnail, separator, menuSort);
separator = new SeparatorMenuItem();
separator.visibleProperty().bind(menuOpenDirectories.visibleProperty());
if (Desktop.isDesktopSupported()) {
menu.getItems().addAll(separator, menuOpenDirectories);
}
return menu;
}
Aggregations