Search in sources :

Example 1 with ImageType

use of qupath.lib.images.ImageData.ImageType 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;
}
Also used : Button(javafx.scene.control.Button) ImageServer(qupath.lib.images.servers.ImageServer) ServerTools(qupath.lib.images.servers.ServerTools) PathObjectReader(qupath.lib.objects.PathObjectReader) ListCell(javafx.scene.control.ListCell) LoggerFactory(org.slf4j.LoggerFactory) Scanner(java.util.Scanner) RenderingHints(java.awt.RenderingHints) ProjectBrowser(qupath.lib.gui.panes.ProjectBrowser) Future(java.util.concurrent.Future) Task(javafx.concurrent.Task) ScrollPane(javafx.scene.control.ScrollPane) ComboBox(javafx.scene.control.ComboBox) URI(java.net.URI) ImageServers(qupath.lib.images.servers.ImageServers) ImageType(qupath.lib.images.ImageData.ImageType) QuPathGUI(qupath.lib.gui.QuPathGUI) TextField(javafx.scene.control.TextField) BufferedImage(java.awt.image.BufferedImage) ImageServerProvider(qupath.lib.images.servers.ImageServerProvider) Collection(java.util.Collection) ChannelDisplayInfo(qupath.lib.display.ChannelDisplayInfo) ImageServerBuilder(qupath.lib.images.servers.ImageServerBuilder) Collectors(java.util.stream.Collectors) FileNotFoundException(java.io.FileNotFoundException) Executors(java.util.concurrent.Executors) PathObject(qupath.lib.objects.PathObject) Priority(javafx.scene.layout.Priority) List(java.util.List) BooleanProperty(javafx.beans.property.BooleanProperty) Project(qupath.lib.projects.Project) ProjectIO(qupath.lib.projects.ProjectIO) Clipboard(javafx.scene.input.Clipboard) Optional(java.util.Optional) ThreadTools(qupath.lib.common.ThreadTools) BorderPane(javafx.scene.layout.BorderPane) ButtonData(javafx.scene.control.ButtonBar.ButtonData) ListView(javafx.scene.control.ListView) TextArea(javafx.scene.control.TextArea) ButtonType(javafx.scene.control.ButtonType) ProgressDialog(org.controlsfx.dialog.ProgressDialog) ImageDisplay(qupath.lib.display.ImageDisplay) UriImageSupport(qupath.lib.images.servers.ImageServerBuilder.UriImageSupport) TransferMode(javafx.scene.input.TransferMode) ArrayList(java.util.ArrayList) Dialogs(qupath.lib.gui.dialogs.Dialogs) Dragboard(javafx.scene.input.Dragboard) Insets(javafx.geometry.Insets) Graphics2D(java.awt.Graphics2D) GridPane(javafx.scene.layout.GridPane) ImageData(qupath.lib.images.ImageData) Logger(org.slf4j.Logger) Dialog(javafx.scene.control.Dialog) Label(javafx.scene.control.Label) TitledPane(javafx.scene.control.TitledPane) ProjectImageEntry(qupath.lib.projects.ProjectImageEntry) GeneralTools(qupath.lib.common.GeneralTools) CheckBox(javafx.scene.control.CheckBox) IOException(java.io.IOException) ServerBuilder(qupath.lib.images.servers.ImageServerBuilder.ServerBuilder) File(java.io.File) ExecutionException(java.util.concurrent.ExecutionException) TimeUnit(java.util.concurrent.TimeUnit) AtomicLong(java.util.concurrent.atomic.AtomicLong) WrappedBufferedImageServer(qupath.lib.images.servers.WrappedBufferedImageServer) Collections(java.util.Collections) PathPrefs(qupath.lib.gui.prefs.PathPrefs) Rotation(qupath.lib.images.servers.RotatedImageServer.Rotation) PaneTools(qupath.lib.gui.tools.PaneTools) Task(javafx.concurrent.Task) TextArea(javafx.scene.control.TextArea) Label(javafx.scene.control.Label) ArrayList(java.util.ArrayList) ProgressDialog(org.controlsfx.dialog.ProgressDialog) ImageType(qupath.lib.images.ImageData.ImageType) ListView(javafx.scene.control.ListView) Button(javafx.scene.control.Button) ProgressDialog(org.controlsfx.dialog.ProgressDialog) Dialog(javafx.scene.control.Dialog) TextField(javafx.scene.control.TextField) ButtonType(javafx.scene.control.ButtonType) Dragboard(javafx.scene.input.Dragboard) ImageServerBuilder(qupath.lib.images.servers.ImageServerBuilder) UriImageSupport(qupath.lib.images.servers.ImageServerBuilder.UriImageSupport) GridPane(javafx.scene.layout.GridPane) ComboBox(javafx.scene.control.ComboBox) CheckBox(javafx.scene.control.CheckBox) ScrollPane(javafx.scene.control.ScrollPane) Collection(java.util.Collection) ImageServerBuilder(qupath.lib.images.servers.ImageServerBuilder) ServerBuilder(qupath.lib.images.servers.ImageServerBuilder.ServerBuilder) BorderPane(javafx.scene.layout.BorderPane) Insets(javafx.geometry.Insets) ListCell(javafx.scene.control.ListCell) BufferedImage(java.awt.image.BufferedImage) ProjectImageEntry(qupath.lib.projects.ProjectImageEntry) ExecutionException(java.util.concurrent.ExecutionException) TitledPane(javafx.scene.control.TitledPane) IOException(java.io.IOException) Rotation(qupath.lib.images.servers.RotatedImageServer.Rotation) FileNotFoundException(java.io.FileNotFoundException) IOException(java.io.IOException) ExecutionException(java.util.concurrent.ExecutionException) AtomicLong(java.util.concurrent.atomic.AtomicLong) Future(java.util.concurrent.Future)

Example 2 with ImageType

use of qupath.lib.images.ImageData.ImageType in project qupath by qupath.

the class ImageDetailsPane method promptToSetImageType.

// /**
// * Prompt the user to set the {@link ImageType} for the image.
// * @param imageData
// * @return
// */
// public static boolean promptToSetImageType(ImageData<BufferedImage> imageData) {
// List<ImageType> values = Arrays.asList(ImageType.values());
// if (!imageData.getServer().isRGB()) {
// values = new ArrayList<>(values);
// values.remove(ImageType.BRIGHTFIELD_H_DAB);
// values.remove(ImageType.BRIGHTFIELD_H_E);
// values.remove(ImageType.BRIGHTFIELD_OTHER);
// }
// 
// var dialog = new ChoiceDialog<>(imageData.getImageType(), values);
// dialog.setTitle("Image type");
// if (QuPathGUI.getInstance() != null)
// dialog.initOwner(QuPathGUI.getInstance().getStage());
// dialog.getDialogPane().setHeaderText(null);
// dialog.getDialogPane().setContentText("Set image type");
// dialog.getDialogPane().setPrefWidth(400);
// 
// var labelExplain = new Label("The image type influences some commands (e.g. cell detection) and should be set for every image. "
// + "\n\nSelect an option below or in the preferences to customize how QuPath handles setting the image type when opening an image."
// + "\n\n'Auto-estimate' is convenient to reduce annoying prompts, but the estimates are sometimes wrong. "
// + "When this happens you can correct them by double-clicking "
// + "the type under the 'Image' tab.");
// labelExplain.setWrapText(true);
// labelExplain.setPrefWidth(400);
// labelExplain.setMinHeight(Label.USE_PREF_SIZE);
// 
// var comboSetType = new ComboBox<ImageTypeSetting>();
// comboSetType.getItems().setAll(ImageTypeSetting.values());
// comboSetType.getSelectionModel().select(PathPrefs.imageTypeSettingProperty().get());
// comboSetType.setMaxWidth(Double.MAX_VALUE);
// labelExplain.setPadding(new Insets(0, 0, 10, 0));
// var expandablePane = new BorderPane(labelExplain);
// expandablePane.setBottom(comboSetType);
// 
// dialog.getDialogPane().setExpandableContent(expandablePane);
// 
// var result = dialog.showAndWait();
// ImageType type = result.orElse(null);
// if (type == null)
// return false;
// 
// if (comboSetType.getSelectionModel().getSelectedItem() != null)
// PathPrefs.imageTypeSettingProperty().set(comboSetType.getSelectionModel().getSelectedItem());
// 
// if (type != imageData.getImageType()) {
// imageData.setImageType(type);
// return true;
// }
// return false;
// }
/**
 * Prompt the user to set the {@link ImageType} for the image.
 * @param imageData the image data for which the type should be set
 * @param defaultType the default type (selected when the dialog is shown)
 * @return true if the type was changed, false otherwise
 */
public static boolean promptToSetImageType(ImageData<BufferedImage> imageData, ImageType defaultType) {
    double size = 32;
    var group = new ToggleGroup();
    boolean isRGB = imageData.getServer().isRGB();
    if (defaultType == null)
        defaultType = ImageType.UNSET;
    var buttonMap = new LinkedHashMap<ImageType, ToggleButton>();
    // TODO: Create a nicer icon for unspecified type
    var iconUnspecified = (Group) createImageTypeCell(Color.GRAY, null, null, size);
    if (isRGB) {
        buttonMap.put(ImageType.BRIGHTFIELD_H_E, createImageTypeButton(ImageType.BRIGHTFIELD_H_E, "Brightfield\nH&E", createImageTypeCell(Color.WHITE, Color.PINK, Color.DARKBLUE, size), "Brightfield image with hematoylin & eosin stains\n(8-bit RGB only)", isRGB));
        buttonMap.put(ImageType.BRIGHTFIELD_H_DAB, createImageTypeButton(ImageType.BRIGHTFIELD_H_DAB, "Brightfield\nH-DAB", createImageTypeCell(Color.WHITE, Color.rgb(200, 200, 220), Color.rgb(120, 50, 20), size), "Brightfield image with hematoylin & DAB stains\n(8-bit RGB only)", isRGB));
        buttonMap.put(ImageType.BRIGHTFIELD_OTHER, createImageTypeButton(ImageType.BRIGHTFIELD_OTHER, "Brightfield\nOther", createImageTypeCell(Color.WHITE, Color.ORANGE, Color.FIREBRICK, size), "Brightfield image with other chromogenic stains\n(8-bit RGB only)", isRGB));
    }
    buttonMap.put(ImageType.FLUORESCENCE, createImageTypeButton(ImageType.FLUORESCENCE, "Fluorescence", createImageTypeCell(Color.BLACK, Color.LIGHTGREEN, Color.BLUE, size), "Fluorescence or fluorescence-like image with a dark background\n" + "Also suitable for imaging mass cytometry", true));
    buttonMap.put(ImageType.OTHER, createImageTypeButton(ImageType.OTHER, "Other", createImageTypeCell(Color.BLACK, Color.WHITE, Color.GRAY, size), "Any other image type", true));
    buttonMap.put(ImageType.UNSET, createImageTypeButton(ImageType.UNSET, "Unspecified", iconUnspecified, "Do not set the image type (not recommended for analysis)", true));
    var buttons = buttonMap.values().toArray(ToggleButton[]::new);
    for (var btn : buttons) {
        if (btn.isDisabled()) {
            btn.getTooltip().setText("Image type is not supported because image is not RGB");
        }
    }
    var buttonList = Arrays.asList(buttons);
    group.getToggles().setAll(buttons);
    group.selectedToggleProperty().addListener((v, o, n) -> {
        // Ensure that we can't deselect all buttons
        if (n == null)
            o.setSelected(true);
    });
    PaneTools.setMaxWidth(Double.MAX_VALUE, buttons);
    PaneTools.setMaxHeight(Double.MAX_VALUE, buttons);
    var selectedButton = buttonMap.get(defaultType);
    group.selectToggle(selectedButton);
    var grid = new GridPane();
    int nHorizontal = 3;
    int nVertical = (int) Math.ceil(buttons.length / (double) nHorizontal);
    grid.getColumnConstraints().setAll(IntStream.range(0, nHorizontal).mapToObj(i -> {
        var c = new ColumnConstraints();
        c.setPercentWidth(100.0 / nHorizontal);
        return c;
    }).collect(Collectors.toList()));
    grid.getRowConstraints().setAll(IntStream.range(0, nVertical).mapToObj(i -> {
        var c = new RowConstraints();
        c.setPercentHeight(100.0 / nVertical);
        return c;
    }).collect(Collectors.toList()));
    grid.setVgap(5);
    // grid.setHgap(5);
    grid.setMaxWidth(Double.MAX_VALUE);
    for (int i = 0; i < buttons.length; i++) {
        grid.add(buttons[i], i % nHorizontal, i / nHorizontal);
    }
    // grid.getChildren().setAll(buttons);
    var content = new BorderPane(grid);
    var comboOptions = new ComboBox<ImageTypeSetting>();
    comboOptions.getItems().setAll(ImageTypeSetting.values());
    var prompts = Map.of(ImageTypeSetting.AUTO_ESTIMATE, "Always auto-estimate type (don't prompt)", ImageTypeSetting.PROMPT, "Always prompt me to set type", ImageTypeSetting.NONE, "Don't set the image type");
    comboOptions.setButtonCell(GuiTools.createCustomListCell(p -> prompts.get(p)));
    comboOptions.setCellFactory(c -> GuiTools.createCustomListCell(p -> prompts.get(p)));
    comboOptions.setTooltip(new Tooltip("Choose whether you want to see these prompts " + "when opening an image for the first time"));
    comboOptions.setMaxWidth(Double.MAX_VALUE);
    // comboOptions.prefWidthProperty().bind(grid.widthProperty().subtract(100));
    comboOptions.getSelectionModel().select(PathPrefs.imageTypeSettingProperty().get());
    if (nVertical > 1)
        BorderPane.setMargin(comboOptions, new Insets(5, 0, 0, 0));
    else
        BorderPane.setMargin(comboOptions, new Insets(10, 0, 0, 0));
    content.setBottom(comboOptions);
    var labelDetails = new Label("The image type is used for stain separation " + "by some commands, e.g. 'Cell detection'.\n" + "Brightfield types are only available for 8-bit RGB images.");
    // + "For 'Brightfield' images you can set the color stain vectors.");
    labelDetails.setWrapText(true);
    labelDetails.prefWidthProperty().bind(grid.widthProperty().subtract(10));
    labelDetails.setMaxHeight(Double.MAX_VALUE);
    labelDetails.setPrefHeight(Label.USE_COMPUTED_SIZE);
    labelDetails.setPrefHeight(100);
    labelDetails.setAlignment(Pos.CENTER);
    labelDetails.setTextAlignment(TextAlignment.CENTER);
    var dialog = Dialogs.builder().title("Set image type").headerText("What type of image is this?").content(content).buttons(ButtonType.APPLY, ButtonType.CANCEL).expandableContent(labelDetails).build();
    // Try to make it easier to dismiss the dialog in a variety of ways
    var btnApply = dialog.getDialogPane().lookupButton(ButtonType.APPLY);
    Platform.runLater(() -> selectedButton.requestFocus());
    for (var btn : buttons) {
        btn.setOnMouseClicked(e -> {
            if (!btn.isDisabled() && e.getClickCount() == 2) {
                btnApply.fireEvent(new ActionEvent());
                e.consume();
            }
        });
    }
    var enterPressed = new KeyCodeCombination(KeyCode.ENTER);
    var spacePressed = new KeyCodeCombination(KeyCode.SPACE);
    dialog.getDialogPane().addEventFilter(KeyEvent.KEY_PRESSED, e -> {
        if (enterPressed.match(e) || spacePressed.match(e)) {
            btnApply.fireEvent(new ActionEvent());
            e.consume();
        } else if (e.getCode() == KeyCode.UP || e.getCode() == KeyCode.DOWN || e.getCode() == KeyCode.LEFT || e.getCode() == KeyCode.RIGHT) {
            var selected = (ToggleButton) group.getSelectedToggle();
            var ind = buttonList.indexOf(selected);
            var newSelected = selected;
            if (e.getCode() == KeyCode.UP && ind >= nHorizontal) {
                newSelected = buttonList.get(ind - nHorizontal);
            }
            if (e.getCode() == KeyCode.LEFT && ind > 0) {
                newSelected = buttonList.get(ind - 1);
            }
            if (e.getCode() == KeyCode.RIGHT && ind < buttonList.size() - 1) {
                newSelected = buttonList.get(ind + 1);
            }
            if (e.getCode() == KeyCode.DOWN && ind < buttonList.size() - nHorizontal) {
                newSelected = buttonList.get(ind + nHorizontal);
            }
            newSelected.requestFocus();
            group.selectToggle(newSelected);
            e.consume();
        }
    });
    var response = dialog.showAndWait();
    if (response.orElse(ButtonType.CANCEL) == ButtonType.APPLY) {
        PathPrefs.imageTypeSettingProperty().set(comboOptions.getSelectionModel().getSelectedItem());
        var selectedType = (ImageType) group.getSelectedToggle().getUserData();
        if (selectedType != imageData.getImageType()) {
            imageData.setImageType(selectedType);
            return true;
        }
    }
    return false;
}
Also used : Arrays(java.util.Arrays) ServerTools(qupath.lib.images.servers.ServerTools) StackPane(javafx.scene.layout.StackPane) Category(java.util.Locale.Category) ParameterList(qupath.lib.plugins.parameters.ParameterList) MasterDetailPane(org.controlsfx.control.MasterDetailPane) ReadOnlyObjectWrapper(javafx.beans.property.ReadOnlyObjectWrapper) Map(java.util.Map) ColorTools(qupath.lib.common.ColorTools) Rectangle(javafx.scene.shape.Rectangle) KeyEvent(javafx.scene.input.KeyEvent) WorkflowStep(qupath.lib.plugins.workflow.WorkflowStep) Group(javafx.scene.Group) StandardCharsets(java.nio.charset.StandardCharsets) Platform(javafx.application.Platform) PropertyChangeListener(java.beans.PropertyChangeListener) Clipboard(javafx.scene.input.Clipboard) BorderPane(javafx.scene.layout.BorderPane) RectangleROI(qupath.lib.roi.RectangleROI) ColumnConstraints(javafx.scene.layout.ColumnConstraints) Bindings(javafx.beans.binding.Bindings) ArrayList(java.util.ArrayList) LinkedHashMap(java.util.LinkedHashMap) TextAlignment(javafx.scene.text.TextAlignment) GridPane(javafx.scene.layout.GridPane) Color(javafx.scene.paint.Color) GeneralTools(qupath.lib.common.GeneralTools) RegionRequest(qupath.lib.regions.RegionRequest) Node(javafx.scene.Node) IOException(java.io.IOException) Background(javafx.scene.layout.Background) File(java.io.File) Menu(javafx.scene.control.Menu) KeyCodeCombination(javafx.scene.input.KeyCodeCombination) ROI(qupath.lib.roi.interfaces.ROI) ParameterPanelFX(qupath.lib.gui.dialogs.ParameterPanelFX) ImageView(javafx.scene.image.ImageView) PixelCalibration(qupath.lib.images.servers.PixelCalibration) ObservableValue(javafx.beans.value.ObservableValue) QP(qupath.lib.scripting.QP) Image(javafx.scene.image.Image) PathPrefs(qupath.lib.gui.prefs.PathPrefs) ImageServerMetadata(qupath.lib.images.servers.ImageServerMetadata) PaneTools(qupath.lib.gui.tools.PaneTools) Pos(javafx.geometry.Pos) ImageServer(qupath.lib.images.servers.ImageServer) URLDecoder(java.net.URLDecoder) LoggerFactory(org.slf4j.LoggerFactory) Side(javafx.geometry.Side) KeyCombination(javafx.scene.input.KeyCombination) ComboBox(javafx.scene.control.ComboBox) Locale(java.util.Locale) ImageIO(javax.imageio.ImageIO) BufferedImageTools(qupath.lib.awt.common.BufferedImageTools) URI(java.net.URI) TableView(javafx.scene.control.TableView) ImageType(qupath.lib.images.ImageData.ImageType) QuPathGUI(qupath.lib.gui.QuPathGUI) Pane(javafx.scene.layout.Pane) MenuItem(javafx.scene.control.MenuItem) BufferedImage(java.awt.image.BufferedImage) Ellipse(javafx.scene.shape.Ellipse) Collection(java.util.Collection) ChannelDisplayInfo(qupath.lib.display.ChannelDisplayInfo) Collectors(java.util.stream.Collectors) StainVector(qupath.lib.color.StainVector) Objects(java.util.Objects) List(java.util.List) ImageTypeSetting(qupath.lib.gui.prefs.PathPrefs.ImageTypeSetting) DefaultScriptableWorkflowStep(qupath.lib.plugins.workflow.DefaultScriptableWorkflowStep) ToggleButton(javafx.scene.control.ToggleButton) GuiTools(qupath.lib.gui.tools.GuiTools) ClipboardContent(javafx.scene.input.ClipboardContent) IntStream(java.util.stream.IntStream) Scene(javafx.scene.Scene) ListView(javafx.scene.control.ListView) ButtonType(javafx.scene.control.ButtonType) RowConstraints(javafx.scene.layout.RowConstraints) ColorDeconvolutionHelper(qupath.lib.color.ColorDeconvolutionHelper) ImageDisplay(qupath.lib.display.ImageDisplay) TableColumn(javafx.scene.control.TableColumn) Dialogs(qupath.lib.gui.dialogs.Dialogs) ColorDeconvolutionStains(qupath.lib.color.ColorDeconvolutionStains) TableCell(javafx.scene.control.TableCell) Insets(javafx.geometry.Insets) BackgroundFill(javafx.scene.layout.BackgroundFill) Callback(javafx.util.Callback) Tooltip(javafx.scene.control.Tooltip) PropertyChangeEvent(java.beans.PropertyChangeEvent) ImageData(qupath.lib.images.ImageData) KeyCode(javafx.scene.input.KeyCode) Modality(javafx.stage.Modality) Logger(org.slf4j.Logger) Label(javafx.scene.control.Label) MenuBar(javafx.scene.control.MenuBar) ReadOnlyStringWrapper(javafx.beans.property.ReadOnlyStringWrapper) DropShadow(javafx.scene.effect.DropShadow) ActionEvent(javafx.event.ActionEvent) ToggleGroup(javafx.scene.control.ToggleGroup) Stage(javafx.stage.Stage) SwingFXUtils(javafx.embed.swing.SwingFXUtils) WrappedBufferedImageServer(qupath.lib.images.servers.WrappedBufferedImageServer) ChangeListener(javafx.beans.value.ChangeListener) Collections(java.util.Collections) ContentDisplay(javafx.scene.control.ContentDisplay) Group(javafx.scene.Group) ToggleGroup(javafx.scene.control.ToggleGroup) BorderPane(javafx.scene.layout.BorderPane) GridPane(javafx.scene.layout.GridPane) Insets(javafx.geometry.Insets) ColumnConstraints(javafx.scene.layout.ColumnConstraints) ComboBox(javafx.scene.control.ComboBox) ActionEvent(javafx.event.ActionEvent) Tooltip(javafx.scene.control.Tooltip) Label(javafx.scene.control.Label) KeyCodeCombination(javafx.scene.input.KeyCodeCombination) LinkedHashMap(java.util.LinkedHashMap) RowConstraints(javafx.scene.layout.RowConstraints) ImageType(qupath.lib.images.ImageData.ImageType) ToggleGroup(javafx.scene.control.ToggleGroup)

Aggregations

BufferedImage (java.awt.image.BufferedImage)2 File (java.io.File)2 IOException (java.io.IOException)2 URI (java.net.URI)2 ArrayList (java.util.ArrayList)2 Collection (java.util.Collection)2 Collections (java.util.Collections)2 List (java.util.List)2 Collectors (java.util.stream.Collectors)2 Insets (javafx.geometry.Insets)2 ButtonType (javafx.scene.control.ButtonType)2 ComboBox (javafx.scene.control.ComboBox)2 Label (javafx.scene.control.Label)2 ListView (javafx.scene.control.ListView)2 Clipboard (javafx.scene.input.Clipboard)2 BorderPane (javafx.scene.layout.BorderPane)2 GridPane (javafx.scene.layout.GridPane)2 Logger (org.slf4j.Logger)2 LoggerFactory (org.slf4j.LoggerFactory)2 GeneralTools (qupath.lib.common.GeneralTools)2