use of qupath.lib.display.ImageDisplay in project qupath by qupath.
the class ProjectImportImagesCommand method getThumbnailRGB.
// /**
// * Add a single ImageServer to a project, without considering sub-images.
// * <p>
// * This includes an optional attempt to request a thumbnail; if this fails, the image will not be added.
// *
// * @param project the project to which the entry should be added
// * @param server the server to add
// * @param type the ImageType that should be set for each entry being added
// * @return
// */
// public static ProjectImageEntry<BufferedImage> addSingleImageToProject(Project<BufferedImage> project, ImageServer<BufferedImage> server, ImageType type) {
// ProjectImageEntry<BufferedImage> entry = null;
// try {
// var img = getThumbnailRGB(server, null);
// entry = project.addImage(server);
// if (entry != null) {
// // Write a thumbnail if we can
// entry.setThumbnail(img);
// // Initialize an ImageData object with a type, if required
// if (type != null) {
// var imageData = new ImageData<>(server, type);
// entry.saveImageData(imageData);
// }
// }
// } catch (IOException e) {
// logger.warn("Error attempting to add " + server, e);
// }
// return entry;
// }
public static BufferedImage getThumbnailRGB(ImageServer<BufferedImage> server, ImageDisplay imageDisplay) throws IOException {
var img2 = server.getDefaultThumbnail(server.nZSlices() / 2, 0);
// Try to write RGB images directly
boolean success = false;
if (imageDisplay == null && (server.isRGB() || img2.getType() == BufferedImage.TYPE_BYTE_GRAY)) {
return resizeForThumbnail(img2);
}
if (!success) {
// Try with display transforms
if (imageDisplay == null) {
// By wrapping the thumbnail, we avoid slow z-stack/time series requests & determine brightness & contrast just from one plane
var wrappedServer = new WrappedBufferedImageServer("Dummy", img2, server.getMetadata().getChannels());
imageDisplay = new ImageDisplay(new ImageData<>(wrappedServer));
// imageDisplay = new ImageDisplay(new ImageData<>(server));
}
for (ChannelDisplayInfo info : imageDisplay.selectedChannels()) {
imageDisplay.autoSetDisplayRange(info);
}
img2 = imageDisplay.applyTransforms(img2, null);
return resizeForThumbnail(img2);
}
return img2;
}
use of qupath.lib.display.ImageDisplay in project qupath by qupath.
the class BrightnessContrastCommand method createDialog.
protected Stage createDialog() {
if (!isInitialized())
initializeSliders();
initializePopup();
changed(null, null, qupath.getImageData());
BorderPane pane = new BorderPane();
GridPane box = new GridPane();
String blank = " ";
Label labelMin = new Label("Min display");
// labelMin.setTooltip(new Tooltip("Set minimum lookup table value - double-click to edit manually"));
Label labelMinValue = new Label(blank);
labelMinValue.setTooltip(new Tooltip("Set minimum lookup table value - double-click to edit manually"));
labelMinValue.textProperty().bind(Bindings.createStringBinding(() -> {
// If value < 10, return 2 dp, otherwise 1 dp. Should be more useful for 16-/32-bit images.
return sliderMin.getValue() < 10 ? String.format("%.2f", sliderMin.getValue()) : String.format("%.1f", sliderMin.getValue());
}, table.getSelectionModel().selectedItemProperty(), sliderMin.valueProperty()));
box.add(labelMin, 0, 0);
box.add(sliderMin, 1, 0);
box.add(labelMinValue, 2, 0);
Label labelMax = new Label("Max display");
labelMax.setTooltip(new Tooltip("Set maximum lookup table value - double-click to edit manually"));
Label labelMaxValue = new Label(blank);
labelMaxValue.setTooltip(new Tooltip("Set maximum lookup table value - double-click to edit manually"));
labelMaxValue.textProperty().bind(Bindings.createStringBinding(() -> {
// If value < 10, return 2 dp, otherwise 1 dp. Should be more useful for 16-/32-bit images.
return sliderMax.getValue() < 10 ? String.format("%.2f", sliderMax.getValue()) : String.format("%.1f", sliderMax.getValue());
}, table.getSelectionModel().selectedItemProperty(), sliderMax.valueProperty()));
box.add(labelMax, 0, 1);
box.add(sliderMax, 1, 1);
box.add(labelMaxValue, 2, 1);
box.setVgap(5);
GridPane.setFillWidth(sliderMin, Boolean.TRUE);
GridPane.setFillWidth(sliderMax, Boolean.TRUE);
box.prefWidthProperty().bind(pane.widthProperty());
box.setPadding(new Insets(5, 0, 5, 0));
GridPane.setHgrow(sliderMin, Priority.ALWAYS);
GridPane.setHgrow(sliderMax, Priority.ALWAYS);
// In the absence of a better way, make it possible to enter display range values
// manually by double-clicking on the corresponding label
labelMinValue.setOnMouseClicked(e -> {
if (e.getClickCount() == 2) {
ChannelDisplayInfo infoVisible = getCurrentInfo();
if (infoVisible == null)
return;
Double value = Dialogs.showInputDialog("Display range", "Set display range minimum", (double) infoVisible.getMinDisplay());
if (value != null && !Double.isNaN(value)) {
sliderMin.setValue(value);
// Update display directly if out of slider range
if (value < sliderMin.getMin() || value > sliderMin.getMax()) {
imageDisplay.setMinMaxDisplay(infoVisible, (float) value.floatValue(), (float) infoVisible.getMaxDisplay());
// infoVisible.setMinDisplay(value.floatValue());
// viewer.updateThumbnail();
updateSliders();
viewer.repaintEntireImage();
}
}
}
});
labelMaxValue.setOnMouseClicked(e -> {
if (e.getClickCount() == 2) {
ChannelDisplayInfo infoVisible = getCurrentInfo();
if (infoVisible == null)
return;
Double value = Dialogs.showInputDialog("Display range", "Set display range maximum", (double) infoVisible.getMaxDisplay());
if (value != null && !Double.isNaN(value)) {
sliderMax.setValue(value);
// Update display directly if out of slider range
if (value < sliderMax.getMin() || value > sliderMax.getMax()) {
imageDisplay.setMinMaxDisplay(infoVisible, (float) infoVisible.getMinDisplay(), (float) value.floatValue());
// infoVisible.setMaxDisplay(value.floatValue());
// viewer.updateThumbnail();
updateSliders();
viewer.repaintEntireImage();
}
}
}
});
Button btnAuto = new Button("Auto");
btnAuto.setOnAction(e -> {
if (imageDisplay == null)
return;
// if (histogram == null)
// return;
// // setSliders((float)histogram.getEdgeMin(), (float)histogram.getEdgeMax());
ChannelDisplayInfo info = getCurrentInfo();
double saturation = PathPrefs.autoBrightnessContrastSaturationPercentProperty().get() / 100.0;
imageDisplay.autoSetDisplayRange(info, saturation);
for (ChannelDisplayInfo info2 : table.getSelectionModel().getSelectedItems()) {
imageDisplay.autoSetDisplayRange(info2, saturation);
}
updateSliders();
handleSliderChange();
});
Button btnReset = new Button("Reset");
btnReset.setOnAction(e -> {
for (ChannelDisplayInfo info : table.getSelectionModel().getSelectedItems()) {
imageDisplay.setMinMaxDisplay(info, info.getMinAllowed(), info.getMaxAllowed());
}
sliderMin.setValue(sliderMin.getMin());
sliderMax.setValue(sliderMax.getMax());
});
Stage dialog = new Stage();
dialog.initOwner(qupath.getStage());
dialog.setTitle("Brightness & contrast");
// Create color/channel display table
table = new TableView<>(imageDisplay == null ? FXCollections.observableArrayList() : imageDisplay.availableChannels());
table.setPlaceholder(new Text("No channels available"));
table.addEventHandler(KeyEvent.KEY_PRESSED, new CopyTableListener());
table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
table.getSelectionModel().selectedItemProperty().addListener((v, o, n) -> {
updateHistogram();
updateSliders();
});
TableColumn<ChannelDisplayInfo, ChannelDisplayInfo> col1 = new TableColumn<>("Color");
col1.setCellValueFactory(new Callback<CellDataFeatures<ChannelDisplayInfo, ChannelDisplayInfo>, ObservableValue<ChannelDisplayInfo>>() {
@Override
public ObservableValue<ChannelDisplayInfo> call(CellDataFeatures<ChannelDisplayInfo, ChannelDisplayInfo> p) {
return new SimpleObjectProperty<ChannelDisplayInfo>(p.getValue());
}
});
col1.setCellFactory(column -> {
return new TableCell<ChannelDisplayInfo, ChannelDisplayInfo>() {
@Override
protected void updateItem(ChannelDisplayInfo item, boolean empty) {
super.updateItem(item, empty);
if (item == null || empty) {
setText(null);
setGraphic(null);
return;
}
setText(item.getName());
Rectangle square = new Rectangle(0, 0, 10, 10);
Integer rgb = item.getColor();
if (rgb == null)
square.setFill(Color.TRANSPARENT);
else
square.setFill(ColorToolsFX.getCachedColor(rgb));
setGraphic(square);
}
};
});
col1.setSortable(false);
TableColumn<ChannelDisplayInfo, Boolean> col2 = new TableColumn<>("Selected");
col2.setCellValueFactory(new Callback<CellDataFeatures<ChannelDisplayInfo, Boolean>, ObservableValue<Boolean>>() {
@Override
public ObservableValue<Boolean> call(CellDataFeatures<ChannelDisplayInfo, Boolean> item) {
SimpleBooleanProperty property = new SimpleBooleanProperty(imageDisplay.selectedChannels().contains(item.getValue()));
// Remove repaint code here - now handled by table selection changes
property.addListener((v, o, n) -> {
imageDisplay.setChannelSelected(item.getValue(), n);
table.refresh();
Platform.runLater(() -> viewer.repaintEntireImage());
});
return property;
}
});
col2.setCellFactory(column -> {
CheckBoxTableCell<ChannelDisplayInfo, Boolean> cell = new CheckBoxTableCell<>();
// Select cells when clicked - means a click anywhere within the row forces selection.
// Previously, clicking within the checkbox didn't select the row.
cell.addEventFilter(MouseEvent.MOUSE_CLICKED, e -> {
if (e.isPopupTrigger())
return;
int ind = cell.getIndex();
var tableView = cell.getTableView();
if (ind < tableView.getItems().size()) {
if (e.isShiftDown())
tableView.getSelectionModel().select(ind);
else
tableView.getSelectionModel().clearAndSelect(ind);
var channel = cell.getTableRow().getItem();
// Handle clicks within the cell but outside the checkbox
if (e.getTarget() == cell && channel != null && imageDisplay != null) {
updateDisplay(channel, !imageDisplay.selectedChannels().contains(channel));
}
e.consume();
}
});
return cell;
});
col2.setSortable(false);
col2.setEditable(true);
col2.setResizable(false);
// Handle color change requests when an appropriate row is double-clicked
table.setRowFactory(tableView -> {
TableRow<ChannelDisplayInfo> row = new TableRow<>();
row.setOnMouseClicked(e -> {
if (e.getClickCount() == 2) {
ChannelDisplayInfo info = row.getItem();
var imageData = viewer.getImageData();
if (info instanceof DirectServerChannelInfo && imageData != null) {
DirectServerChannelInfo multiInfo = (DirectServerChannelInfo) info;
int c = multiInfo.getChannel();
var channel = imageData.getServer().getMetadata().getChannel(c);
Color color = ColorToolsFX.getCachedColor(multiInfo.getColor());
picker.setValue(color);
Dialog<ButtonType> colorDialog = new Dialog<>();
colorDialog.setTitle("Channel properties");
colorDialog.getDialogPane().getButtonTypes().setAll(ButtonType.APPLY, ButtonType.CANCEL);
var paneColor = new GridPane();
int r = 0;
var labelName = new Label("Channel name");
var tfName = new TextField(channel.getName());
labelName.setLabelFor(tfName);
PaneTools.addGridRow(paneColor, r++, 0, "Enter a name for the current channel", labelName, tfName);
var labelColor = new Label("Channel color");
labelColor.setLabelFor(picker);
PaneTools.addGridRow(paneColor, r++, 0, "Choose the color for the current channel", labelColor, picker);
paneColor.setVgap(5.0);
paneColor.setHgap(5.0);
colorDialog.getDialogPane().setContent(paneColor);
Optional<ButtonType> result = colorDialog.showAndWait();
if (result.orElse(ButtonType.CANCEL) == ButtonType.APPLY) {
// if (!DisplayHelpers.showMessageDialog("Choose channel color", picker))
// return;
String name = tfName.getText().trim();
if (name.isEmpty()) {
Dialogs.showErrorMessage("Set channel name", "The channel name must not be empty!");
return;
}
Color color2 = picker.getValue();
if (color == color2 && name.equals(channel.getName()))
return;
// Update the server metadata
int colorUpdated = ColorToolsFX.getRGB(color2);
if (imageData != null) {
var server = imageData.getServer();
var metadata = server.getMetadata();
var channels = new ArrayList<>(metadata.getChannels());
channels.set(c, ImageChannel.getInstance(name, colorUpdated));
var metadata2 = new ImageServerMetadata.Builder(metadata).channels(channels).build();
imageData.updateServerMetadata(metadata2);
}
// Update the display
multiInfo.setLUTColor((int) (color2.getRed() * 255), (int) (color2.getGreen() * 255), (int) (color2.getBlue() * 255));
// Add color property
imageDisplay.saveChannelColorProperties();
viewer.repaintEntireImage();
updateHistogram();
table.refresh();
}
}
}
});
return row;
});
table.getColumns().add(col1);
table.getColumns().add(col2);
table.setEditable(true);
table.setColumnResizePolicy(TableView.UNCONSTRAINED_RESIZE_POLICY);
// Hack... space for a scrollbar
col1.prefWidthProperty().bind(table.widthProperty().subtract(col2.widthProperty()).subtract(25));
// col2.setResizable(false);
// col2.setMaxWidth(100);
// col2.setPrefWidth(50);
// col2.setResizable(false);
// col1.prefWidthProperty().bind(table.widthProperty().multiply(0.7));
// col2.prefWidthProperty().bind(table.widthProperty().multiply(0.2));
// table.getColumnModel().getColumn(0).setCellRenderer(new ChannelCellRenderer());
// table.getColumnModel().getColumn(1).setMaxWidth(table.getColumnModel().getColumn(1).getPreferredWidth());
BorderPane panelColor = new BorderPane();
// panelColor.setBorder(BorderFactory.createTitledBorder("Color display"));
BorderPane paneTableAndFilter = new BorderPane(table);
TextField tfFilter = new TextField("");
tfFilter.textProperty().bindBidirectional(filterText);
tfFilter.setTooltip(new Tooltip("Enter text to find specific channels by name"));
tfFilter.setPromptText("Filter channels by name");
paneTableAndFilter.setBottom(tfFilter);
predicate.addListener((v, o, n) -> updatePredicate());
panelColor.setCenter(paneTableAndFilter);
CheckBox cbShowGrayscale = new CheckBox("Show grayscale");
cbShowGrayscale.selectedProperty().bindBidirectional(showGrayscale);
cbShowGrayscale.setTooltip(new Tooltip("Show single channel with grayscale lookup table"));
if (imageDisplay != null)
cbShowGrayscale.setSelected(!imageDisplay.useColorLUTs());
showGrayscale.addListener(o -> {
if (imageDisplay == null)
return;
Platform.runLater(() -> viewer.repaintEntireImage());
table.refresh();
});
CheckBox cbKeepDisplaySettings = new CheckBox("Keep settings");
cbKeepDisplaySettings.selectedProperty().bindBidirectional(PathPrefs.keepDisplaySettingsProperty());
cbKeepDisplaySettings.setTooltip(new Tooltip("Retain same display settings where possible when opening similar images"));
FlowPane paneCheck = new FlowPane();
paneCheck.getChildren().add(cbShowGrayscale);
paneCheck.getChildren().add(cbKeepDisplaySettings);
paneCheck.setHgap(10);
paneCheck.setPadding(new Insets(5, 0, 0, 0));
panelColor.setBottom(paneCheck);
pane.setCenter(panelColor);
// Create brightness/contrast panel
BorderPane panelSliders = new BorderPane();
panelSliders.setTop(box);
GridPane panelButtons = PaneTools.createColumnGridControls(btnAuto, btnReset);
panelSliders.setBottom(panelButtons);
panelSliders.setPadding(new Insets(5, 0, 5, 0));
BorderPane panelMinMax = new BorderPane();
// panelMinMax.setBorder(BorderFactory.createTitledBorder("Brightness/Contrast"));
panelMinMax.setTop(panelSliders);
histogramPanel.setShowTickLabels(false);
histogramPanel.getChart().setAnimated(false);
// histogramPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
// panelMinMax.setCenter(histogramPanel.getChart());
panelMinMax.setCenter(chartWrapper.getPane());
chartWrapper.getPane().setPrefSize(200, 200);
// histogramPanel.getChart().setPrefSize(200, 200);
// histogramPanel.setPreferredSize(new Dimension(200, 120));
pane.setBottom(panelMinMax);
pane.setPadding(new Insets(10, 10, 10, 10));
Scene scene = new Scene(pane, 350, 500);
dialog.setScene(scene);
dialog.setMinWidth(300);
dialog.setMinHeight(400);
dialog.setMaxWidth(600);
dialog.setMaxHeight(800);
updateTable();
if (!table.getItems().isEmpty())
table.getSelectionModel().select(0);
updateDisplay(getCurrentInfo(), true);
updateHistogram();
updateSliders();
// Update sliders when receiving focus - in case the display has been updated elsewhere
dialog.focusedProperty().addListener((v, o, n) -> {
if (n)
updateSliders();
});
return dialog;
}
use of qupath.lib.display.ImageDisplay in project qupath by qupath.
the class QuPathViewer method setImageData.
/**
* Set the current image for this viewer.
* @param imageDataNew
*/
public void setImageData(ImageData<BufferedImage> imageDataNew) {
if (this.imageDataProperty.get() == imageDataNew)
return;
imageDataChanging.set(true);
// Remove listeners for previous hierarchy
ImageData<BufferedImage> imageDataOld = this.imageDataProperty.get();
if (imageDataOld != null) {
imageDataOld.getHierarchy().removePathObjectListener(this);
imageDataOld.getHierarchy().getSelectionModel().removePathObjectSelectionListener(this);
}
// Determine if the server has remained the same, so we can avoid shifting the viewer
boolean sameServer = false;
if (imageDataOld != null && imageDataNew != null && imageDataOld.getServerPath().equals(imageDataNew.getServerPath()))
sameServer = true;
this.imageDataProperty.set(imageDataNew);
ImageServer<BufferedImage> server = imageDataNew == null ? null : imageDataNew.getServer();
PathObjectHierarchy hierarchy = imageDataNew == null ? null : imageDataNew.getHierarchy();
long startTime = System.currentTimeMillis();
if (imageDisplay != null) {
boolean keepDisplay = PathPrefs.keepDisplaySettingsProperty().get();
// This is a bit of a hack to avoid calling internal methods for ImageDisplay
// See https://github.com/qupath/qupath/issues/601
boolean displaySet = false;
if (imageDataNew != null && keepDisplay) {
if (imageDisplay.getImageData() != null && serversCompatible(imageDataNew.getServer(), imageDisplay.getImageData().getServer())) {
imageDisplay.setImageData(imageDataNew, keepDisplay);
displaySet = true;
} else {
for (var viewer : QuPathGUI.getInstance().getViewers()) {
if (this == viewer || viewer.getImageData() == null)
continue;
var tempServer = viewer.getServer();
var currentServer = imageDataNew.getServer();
if (serversCompatible(tempServer, currentServer)) {
var json = viewer.getImageDisplay().toJSON(false);
imageDataNew.setProperty(ImageDisplay.class.getName(), json);
imageDisplay.setImageData(imageDataNew, false);
displaySet = true;
break;
}
}
}
}
if (!displaySet)
imageDisplay.setImageData(imageDataNew, keepDisplay);
// See https://github.com/qupath/qupath/issues/843
if (server != null && !server.isRGB()) {
var colors = imageDisplay.availableChannels().stream().filter(c -> c instanceof DirectServerChannelInfo).map(c -> c.getColor()).collect(Collectors.toList());
if (server.nChannels() == colors.size())
updateServerChannels(server, colors);
}
}
long endTime = System.currentTimeMillis();
logger.debug("Setting ImageData time: {} ms", endTime - startTime);
initializeForServer(server);
if (!sameServer) {
setDownsampleFactorImpl(getZoomToFitDownsampleFactor(), -1, -1);
centerImage();
}
fireImageDataChanged(imageDataOld, imageDataNew);
if (imageDataNew != null) {
// hierarchyPainter = new PathHierarchyPainter(hierarchy);
hierarchy.addPathObjectListener(this);
hierarchy.getSelectionModel().addPathObjectSelectionListener(this);
}
setSelectedObject(null);
// TODO: Consider shifting, fixing magnification, repainting etc.
if (isShowing())
repaint();
logger.info("Image data set to {}", imageDataNew);
}
use of qupath.lib.display.ImageDisplay in project qupath by qupath.
the class QPEx method setChannelDisplayRange.
/**
* Set the minimum and maximum display range for the specified {@link ImageData} for a channel identified by number.
* @param imageData
* @param channel channel number (0-based index)
* @param minDisplay
* @param maxDisplay
*/
public static void setChannelDisplayRange(ImageData<BufferedImage> imageData, int channel, double minDisplay, double maxDisplay) {
// Try to get an existing display if the image is currently open
var viewer = getQuPath().getViewers().stream().filter(v -> v.getImageData() == imageData).findFirst().orElse(null);
ImageDisplay display = viewer == null ? new ImageDisplay(imageData) : viewer.getImageDisplay();
var available = display.availableChannels();
if (channel < 0 || channel >= available.size()) {
logger.warn("Channel {} is out of range ({}-{}) - cannot set display range", channel, 0, available.size() - 1);
return;
}
var info = display.availableChannels().get(channel);
display.setMinMaxDisplay(info, (float) minDisplay, (float) maxDisplay);
// Update the viewer is necessary
if (viewer != null)
viewer.repaintEntireImage();
}
use of qupath.lib.display.ImageDisplay in project qupath by qupath.
the class QPEx method setChannelDisplayRange.
/**
* Set the minimum and maximum display range for the specified {@link ImageData} for a channel identified by name.
* @param imageData
* @param channelName
* @param minDisplay
* @param maxDisplay
*/
public static void setChannelDisplayRange(ImageData<BufferedImage> imageData, String channelName, double minDisplay, double maxDisplay) {
// Try to get an existing display if the image is currently open
var viewer = getQuPath().getViewers().stream().filter(v -> v.getImageData() == imageData).findFirst().orElse(null);
ImageDisplay display = viewer == null ? new ImageDisplay(imageData) : viewer.getImageDisplay();
var available = display.availableChannels();
ChannelDisplayInfo info = null;
var serverChannels = imageData.getServer().getMetadata().getChannels();
for (var c : available) {
if (channelName.equals(c.getName())) {
info = c;
break;
}
// We also need to check the channel names, since the info might have adjusted them (e.g. by adding (C1) at the end)
if (c instanceof DirectServerChannelInfo) {
int channelNumber = ((DirectServerChannelInfo) c).getChannel();
if (channelNumber >= 0 && channelNumber < serverChannels.size() && channelName.equals(serverChannels.get(channelNumber).getName())) {
info = c;
break;
}
}
}
if (info == null) {
logger.warn("No channel found with name {} - cannot set display range", channelName);
return;
}
display.setMinMaxDisplay(info, (float) minDisplay, (float) maxDisplay);
// Update the viewer is necessary
if (viewer != null)
viewer.repaintEntireImage();
}
Aggregations