use of qupath.lib.images.servers.ImageServerBuilder.ServerBuilder 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.images.servers.ImageServerBuilder.ServerBuilder 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.images.servers.ImageServerBuilder.ServerBuilder in project qupath by qupath.
the class PathIO method extractServerBuilder.
/**
* Extract a {@link ServerBuilder} from a String.
* This may represent an image path (for v0.2 and earlier) or JSON (from v0.3).
*
* @param <T>
* @param serverString
* @param warnIfInvalid log warnings if the server is a different version
* @return
* @throws IOException
*/
private static <T> ServerBuilder<T> extractServerBuilder(String serverString, boolean warnIfInvalid) throws IOException {
if (serverString.startsWith("Image path: ")) {
String serverPath = serverString.substring("Image path: ".length()).trim();
URI uri = ImageServerProvider.legacyPathToURI(serverPath);
if (warnIfInvalid)
logger.warn("Attempting to extract server from legacy data file - this may result in errors");
return DefaultImageServerBuilder.createInstance(null, uri);
} else {
String json = serverString;
var wrapper = GsonTools.getInstance().fromJson(json, ServerBuilderWrapper.class);
if (warnIfInvalid && !Objects.equals(wrapper.dataVersion, DATA_FILE_VERSION)) {
logger.warn("Attempting to read data file version {} written by QuPath {} (expected data file version {})", wrapper.dataVersion, wrapper.qupathVersion, DATA_FILE_VERSION);
}
return (ServerBuilder<T>) wrapper.server;
}
}
Aggregations