use of qupath.lib.images.servers.ImageServerBuilder.UriImageSupport 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.UriImageSupport in project qupath by qupath.
the class ImageServerProvider method buildServer.
/**
* Attempt to create {@code ImageServer<T>} from the specified path and arguments.
* <p>
* In general, since v0.3 {@link ImageServers#buildServer(URI, String...)} is usually preferable to this method because it parses
* and applies known args (e.g. to reorder RGB channels or rotate the image).
* Here, all args apart from {@code --classname} are passed to builders - which may or may not do anything with them.
*
* @param path path to an image - typically a URI
* @param cls desired generic type for the ImageServer, e.g. BufferedImage.class
* @param args optional arguments, which may be used by some builders
* @return
* @throws IOException
*/
public static <T> ImageServer<T> buildServer(final String path, final Class<T> cls, String... args) throws IOException {
List<UriImageSupport<T>> supports = getServerBuilders(cls, path, args);
Exception firstException = null;
if (!supports.isEmpty()) {
for (UriImageSupport<T> support : supports) {
try {
if (!support.getBuilders().isEmpty()) {
var server = support.getBuilders().get(0).build();
return checkServerSize(server);
}
} catch (Exception e) {
if (firstException == null)
firstException = e;
logger.warn("Unable to build server: {}", e.getLocalizedMessage());
logger.debug(e.getLocalizedMessage(), e);
}
}
}
if (supports.isEmpty()) {
logger.error("Unable to build whole slide server - check your classpath for a suitable library (e.g. OpenSlide, BioFormats)\n\t");
logger.error(System.getProperty("java.class.path"));
}
String message = firstException == null ? "No supports found" : firstException.getLocalizedMessage();
if (!message.isBlank())
message = " (" + message + ")";
throw new IOException("Unable to build a whole slide server for " + path + message, firstException);
}
use of qupath.lib.images.servers.ImageServerBuilder.UriImageSupport in project qupath by qupath.
the class ImageServerProvider method getServerBuilders.
private static <T> List<UriImageSupport<T>> getServerBuilders(final Class<T> cls, final String path, String... args) throws IOException {
URI uri = legacyPathToURI(path);
if ("file".equals(uri.getScheme()) && !new File(uri).exists()) {
throw new IOException(uri.toString() + " does not exist!");
}
Collection<String> requestedClassnames = new HashSet<>();
String key = "--classname";
if (args.length > 0) {
int i = key.equals(args[0]) ? 1 : 0;
while (i < args.length && !args[i].startsWith("-")) {
requestedClassnames.add(args[i]);
i++;
}
// Remove classname-related args
args = Arrays.copyOfRange(args, i, args.length);
}
// Check which providers we can use
List<UriImageSupport<T>> supports = new ArrayList<>();
synchronized (serviceLoader) {
for (ImageServerBuilder<?> provider : serviceLoader) {
try {
if (!cls.isAssignableFrom(provider.getImageType()))
continue;
// Check classnames
if (!requestedClassnames.isEmpty() && !requestedClassnames.contains(provider.getClass().getName()) && !requestedClassnames.contains(provider.getClass().getSimpleName()))
continue;
UriImageSupport<T> support = (UriImageSupport<T>) provider.checkImageSupport(uri, args);
if (support != null && support.getSupportLevel() > 0f)
supports.add(support);
} catch (Exception e) {
logger.error("Error testing provider " + provider, e.getLocalizedMessage());
}
}
}
Comparator<UriImageSupport<T>> comparator = Collections.reverseOrder(new UriImageSupportComparator<>());
supports.sort(comparator);
return supports;
}
Aggregations