use of qupath.lib.roi.interfaces.ROI in project qupath by qupath.
the class SpecifyAnnotationCommand method init.
private void init() {
pane = new GridPane();
int row = 0;
var cbMicrons = new CheckBox("Use " + GeneralTools.micrometerSymbol());
var units = Bindings.createStringBinding(() -> {
if (cbMicrons.isSelected())
return GeneralTools.micrometerSymbol();
return "px";
}, cbMicrons.selectedProperty());
var comboType = new ComboBox<ROI_TYPE>(FXCollections.observableArrayList(ROI_TYPE.values()));
comboType.setMaxWidth(Double.MAX_VALUE);
comboType.getSelectionModel().select(ROI_TYPE.RECTANGLE);
PaneTools.addGridRow(pane, row++, 0, "Type of ROI to create", createLabelFor(comboType, "Type"), comboType, comboType);
var comboClassification = new ComboBox<>(qupath.getAvailablePathClasses());
comboClassification.setMaxWidth(Double.MAX_VALUE);
comboClassification.setCellFactory(o -> {
return new ClassificationCell();
});
// comboClassification.cell;
PaneTools.addGridRow(pane, row++, 0, "Classification for the annotation (may be empty if annotation should be unclassified)", createLabelFor(comboClassification, "Classification"), comboClassification, comboClassification);
var tfX = new TextField("");
var tfY = new TextField("");
var tfWidth = new TextField("1000");
var tfHeight = new TextField("1000");
tfX.setPrefColumnCount(10);
tfY.setPrefColumnCount(10);
tfWidth.setPrefColumnCount(10);
tfHeight.setPrefColumnCount(10);
PaneTools.addGridRow(pane, row++, 0, "X-coordinate of top left of annotation bounding box (if missing or < 0, annotation will be centered in current viewer)", createLabelFor(tfX, "X origin"), tfX, createBoundLabel(units));
PaneTools.addGridRow(pane, row++, 0, "Y-coordinate of top left of annotation bounding box (if missing or < 0, annotation will be centered in current viewer)", createLabelFor(tfY, "Y origin"), tfY, createBoundLabel(units));
PaneTools.addGridRow(pane, row++, 0, "Width of annotation bounding box (must be > 0)", createLabelFor(tfWidth, "Width"), tfWidth, createBoundLabel(units));
PaneTools.addGridRow(pane, row++, 0, "Height of annotation bounding box (must be > 0)", createLabelFor(tfHeight, "Height"), tfHeight, createBoundLabel(units));
cbMicrons.setMaxWidth(Double.MAX_VALUE);
PaneTools.addGridRow(pane, row++, 0, "Specify coordinates in " + GeneralTools.micrometerSymbol() + " - pixel calibration information must be available", cbMicrons, cbMicrons, cbMicrons);
var cbLock = new CheckBox("Set locked");
cbLock.setMaxWidth(Double.MAX_VALUE);
PaneTools.addGridRow(pane, row++, 0, "Set annotation as locked, so that it can't be immediately edited", cbLock, cbLock, cbLock);
var tfName = new TextField("");
PaneTools.addGridRow(pane, row++, 0, "Name of annotation (can be empty)", createLabelFor(tfName, "Name"), tfName, tfName);
var btnAdd = new Button("Add annotation");
btnAdd.setOnAction(e -> {
var viewer = qupath.getViewer();
var imageData = viewer == null ? null : viewer.getImageData();
if (imageData == null) {
Dialogs.showNoImageError("Create annotation");
return;
}
var server = imageData.getServer();
var hierarchy = imageData.getHierarchy();
double xScale = 1;
double yScale = 1;
PixelCalibration cal = server.getPixelCalibration();
if (cbMicrons.isSelected()) {
if (!cal.hasPixelSizeMicrons()) {
Dialogs.showErrorMessage("Create annotation", "No pixel size information available! Try again using pixel units.");
return;
}
xScale = 1.0 / cal.getPixelWidthMicrons();
yScale = 1.0 / cal.getPixelHeightMicrons();
}
var xOrig = tryToParse(tfX.getText());
var yOrig = tryToParse(tfY.getText());
var width = tryToParse(tfWidth.getText());
var height = tryToParse(tfHeight.getText());
if (width == null || width.doubleValue() <= 0 || height == null || height.doubleValue() <= 0) {
Dialogs.showErrorMessage("Create annotation", "Width and height must be specified, and > 0!");
return;
}
double w = width.doubleValue() * xScale;
double h = height.doubleValue() * yScale;
// It helps to start at integer pixels, since otherwise the width can be surprising when exporting regions
// (since when requesting ROIs, Math.ceil is currently applied to ensure that the full ROI is included).
double x;
if (xOrig == null || xOrig.doubleValue() < 0)
x = (int) Math.max(0, viewer.getCenterPixelX() - w / 2.0);
else
x = xOrig.doubleValue() * xScale;
double y;
if (yOrig == null || yOrig.doubleValue() < 0)
y = (int) Math.max(viewer.getCenterPixelY() - h / 2.0, 0);
else
y = yOrig.doubleValue() * yScale;
if (x + w > server.getWidth() || y + h > server.getHeight()) {
Dialogs.showErrorMessage("Create annotation", "Specified annotation is too large for the image bounds!");
return;
}
int z = viewer.getZPosition();
int t = viewer.getTPosition();
ROI roi = null;
switch(comboType.getSelectionModel().getSelectedItem()) {
case ELLIPSE:
roi = ROIs.createEllipseROI(x, y, w, h, ImagePlane.getPlane(z, t));
break;
case RECTANGLE:
roi = ROIs.createRectangleROI(x, y, w, h, ImagePlane.getPlane(z, t));
break;
default:
Dialogs.showErrorMessage("Create annotation", "No ROI type selected!");
return;
}
var pathClass = comboClassification.getSelectionModel().getSelectedItem();
if (pathClass != null && !pathClass.isValid())
pathClass = null;
var annotation = PathObjects.createAnnotationObject(roi, pathClass);
// Set name, if necessary
var name = tfName.getText();
if (name != null && !name.isEmpty())
annotation.setName(name);
if (cbLock.isSelected())
((PathAnnotationObject) annotation).setLocked(true);
hierarchy.addPathObject(annotation);
});
btnAdd.setMaxWidth(Double.MAX_VALUE);
PaneTools.addGridRow(pane, row++, 0, "Create annotation with specified options & add to object hierarchy", btnAdd, btnAdd, btnAdd);
PaneTools.setFillWidth(Boolean.TRUE, tfX, tfY, tfWidth, tfHeight, btnAdd, comboType);
PaneTools.setHGrowPriority(Priority.ALWAYS, tfX, tfY, tfWidth, tfHeight, btnAdd, comboType);
pane.setHgap(5);
pane.setVgap(5);
pane.setPadding(new Insets(10));
}
use of qupath.lib.roi.interfaces.ROI in project qupath by qupath.
the class Commands method promptToExportImageRegion.
/**
* Prompt to export the current image region selected in the viewer.
* @param viewer the viewer containing the image to export
* @param renderedImage if true, export the rendered (RGB) image rather than original pixel values
*/
public static void promptToExportImageRegion(QuPathViewer viewer, boolean renderedImage) {
if (viewer == null || viewer.getServer() == null) {
Dialogs.showErrorMessage("Export image region", "No viewer & image selected!");
return;
}
ImageServer<BufferedImage> server = viewer.getServer();
if (renderedImage)
server = RenderedImageServer.createRenderedServer(viewer);
PathObject pathObject = viewer.getSelectedObject();
ROI roi = pathObject == null ? null : pathObject.getROI();
double regionWidth = roi == null ? server.getWidth() : roi.getBoundsWidth();
double regionHeight = roi == null ? server.getHeight() : roi.getBoundsHeight();
// Create a dialog
GridPane pane = new GridPane();
int row = 0;
pane.add(new Label("Export format"), 0, row);
ComboBox<ImageWriter<BufferedImage>> comboImageType = new ComboBox<>();
Function<ImageWriter<BufferedImage>, String> fun = (ImageWriter<BufferedImage> writer) -> writer.getName();
comboImageType.setCellFactory(p -> GuiTools.createCustomListCell(fun));
comboImageType.setButtonCell(GuiTools.createCustomListCell(fun));
var writers = ImageWriterTools.getCompatibleWriters(server, null);
comboImageType.getItems().setAll(writers);
comboImageType.setTooltip(new Tooltip("Choose export image format"));
if (writers.contains(lastWriter))
comboImageType.getSelectionModel().select(lastWriter);
else
comboImageType.getSelectionModel().selectFirst();
comboImageType.setMaxWidth(Double.MAX_VALUE);
GridPane.setHgrow(comboImageType, Priority.ALWAYS);
pane.add(comboImageType, 1, row++);
TextArea textArea = new TextArea();
textArea.setPrefRowCount(2);
textArea.setEditable(false);
textArea.setWrapText(true);
// textArea.setPadding(new Insets(15, 0, 0, 0));
comboImageType.setOnAction(e -> textArea.setText(((ImageWriter<BufferedImage>) comboImageType.getValue()).getDetails()));
textArea.setText(((ImageWriter<BufferedImage>) comboImageType.getValue()).getDetails());
pane.add(textArea, 0, row++, 2, 1);
var label = new Label("Downsample factor");
pane.add(label, 0, row);
TextField tfDownsample = new TextField();
label.setLabelFor(tfDownsample);
pane.add(tfDownsample, 1, row++);
tfDownsample.setTooltip(new Tooltip("Amount to scale down image - choose 1 to export at full resolution (note: for large images this may not succeed for memory reasons)"));
ObservableDoubleValue downsample = Bindings.createDoubleBinding(() -> {
try {
return Double.parseDouble(tfDownsample.getText());
} catch (NumberFormatException e) {
return Double.NaN;
}
}, tfDownsample.textProperty());
// Define a sensible limit for non-pyramidal images
long maxPixels = 10000 * 10000;
Label labelSize = new Label();
labelSize.setMinWidth(400);
labelSize.setTextAlignment(TextAlignment.CENTER);
labelSize.setContentDisplay(ContentDisplay.CENTER);
labelSize.setAlignment(Pos.CENTER);
labelSize.setMaxWidth(Double.MAX_VALUE);
labelSize.setTooltip(new Tooltip("Estimated size of exported image"));
pane.add(labelSize, 0, row++, 2, 1);
labelSize.textProperty().bind(Bindings.createStringBinding(() -> {
if (!Double.isFinite(downsample.get())) {
labelSize.setStyle("-fx-text-fill: red;");
return "Invalid downsample value! Must be >= 1";
} else {
long w = (long) (regionWidth / downsample.get() + 0.5);
long h = (long) (regionHeight / downsample.get() + 0.5);
String warning = "";
var writer = comboImageType.getSelectionModel().getSelectedItem();
boolean supportsPyramid = writer == null ? false : writer.supportsPyramidal();
if (!supportsPyramid && w * h > maxPixels) {
labelSize.setStyle("-fx-text-fill: red;");
warning = " (too big!)";
} else if (w < 5 || h < 5) {
labelSize.setStyle("-fx-text-fill: red;");
warning = " (too small!)";
} else
labelSize.setStyle(null);
return String.format("Output image size: %d x %d pixels%s", w, h, warning);
}
}, downsample, comboImageType.getSelectionModel().selectedIndexProperty()));
tfDownsample.setText(Double.toString(exportDownsample.get()));
PaneTools.setMaxWidth(Double.MAX_VALUE, labelSize, textArea, tfDownsample, comboImageType);
PaneTools.setHGrowPriority(Priority.ALWAYS, labelSize, textArea, tfDownsample, comboImageType);
pane.setVgap(5);
pane.setHgap(5);
if (!Dialogs.showConfirmDialog("Export image region", pane))
return;
var writer = comboImageType.getSelectionModel().getSelectedItem();
boolean supportsPyramid = writer == null ? false : writer.supportsPyramidal();
int w = (int) (regionWidth / downsample.get() + 0.5);
int h = (int) (regionHeight / downsample.get() + 0.5);
if (!supportsPyramid && w * h > maxPixels) {
Dialogs.showErrorNotification("Export image region", "Requested export region too large - try selecting a smaller region, or applying a higher downsample factor");
return;
}
if (downsample.get() < 1 || !Double.isFinite(downsample.get())) {
Dialogs.showErrorMessage("Export image region", "Downsample factor must be >= 1!");
return;
}
exportDownsample.set(downsample.get());
// Now that we know the output, we can create a new server to ensure it is downsampled as the necessary resolution
if (renderedImage && downsample.get() != server.getDownsampleForResolution(0))
server = new RenderedImageServer.Builder(viewer).downsamples(downsample.get()).build();
// selectedImageType.set(comboImageType.getSelectionModel().getSelectedItem());
// Create RegionRequest
RegionRequest request = null;
if (pathObject != null && pathObject.hasROI())
request = RegionRequest.createInstance(server.getPath(), exportDownsample.get(), roi);
// Create a sensible default file name, and prompt for the actual name
String ext = writer.getDefaultExtension();
String writerName = writer.getName();
String defaultName = GeneralTools.getNameWithoutExtension(new File(ServerTools.getDisplayableImageName(server)));
if (roi != null) {
defaultName = String.format("%s (%s, x=%d, y=%d, w=%d, h=%d)", defaultName, GeneralTools.formatNumber(request.getDownsample(), 2), request.getX(), request.getY(), request.getWidth(), request.getHeight());
}
File fileOutput = Dialogs.promptToSaveFile("Export image region", null, defaultName, writerName, ext);
if (fileOutput == null)
return;
try {
if (request == null) {
if (exportDownsample.get() == 1.0)
writer.writeImage(server, fileOutput.getAbsolutePath());
else
writer.writeImage(ImageServers.pyramidalize(server, exportDownsample.get()), fileOutput.getAbsolutePath());
} else
writer.writeImage(server, request, fileOutput.getAbsolutePath());
lastWriter = writer;
} catch (IOException e) {
Dialogs.showErrorMessage("Export region", e);
}
}
use of qupath.lib.roi.interfaces.ROI in project qupath by qupath.
the class TMAGridView method initializeData.
private void initializeData(ImageData<BufferedImage> imageData) {
if (this.imageData != imageData) {
if (this.imageData != null)
this.imageData.getHierarchy().removePathObjectListener(this);
this.imageData = imageData;
if (imageData != null) {
imageData.getHierarchy().addPathObjectListener(this);
}
}
if (imageData == null || imageData.getHierarchy().getTMAGrid() == null) {
model.setImageData(null, Collections.emptyList());
grid.getItems().clear();
return;
}
// Request all core thumbnails now
List<TMACoreObject> cores = imageData.getHierarchy().getTMAGrid().getTMACoreList();
ImageServer<BufferedImage> server = imageData.getServer();
CountDownLatch latch = new CountDownLatch(cores.size());
for (TMACoreObject core : cores) {
ROI roi = core.getROI();
if (roi != null) {
qupath.submitShortTask(() -> {
RegionRequest request = createRegionRequest(core);
if (cache.containsKey(request)) {
latch.countDown();
return;
}
BufferedImage img;
try {
img = server.readBufferedImage(request);
} catch (IOException e) {
logger.debug("Unable to get tile for " + request, e);
latch.countDown();
return;
}
Image imageNew = SwingFXUtils.toFXImage(img, null);
if (imageNew != null) {
cache.put(request, imageNew);
// Platform.runLater(() -> updateGridDisplay());
}
latch.countDown();
});
} else
latch.countDown();
}
long startTime = System.currentTimeMillis();
try {
latch.await(10, TimeUnit.SECONDS);
} catch (InterruptedException e1) {
if (latch.getCount() > 0)
logger.warn("Loaded {} cores in 10 seconds", cores.size() - latch.getCount());
}
logger.info("Countdown complete in {} seconds", (System.currentTimeMillis() - startTime) / 1000.0);
model.setImageData(imageData, cores);
backingList.setAll(cores);
String m = measurement.getValue();
sortCores(backingList, model, m, descending.get());
filteredList.setPredicate(p -> {
return !(p.isMissing() || Double.isNaN(model.getNumericValue(p, m)));
});
grid.getItems().setAll(filteredList);
}
use of qupath.lib.roi.interfaces.ROI in project qupath by qupath.
the class EstimateStainVectorsCommand method promptToEstimateStainVectors.
public static void promptToEstimateStainVectors(ImageData<BufferedImage> imageData) {
if (imageData == null) {
Dialogs.showNoImageError(TITLE);
return;
}
if (imageData == null || !imageData.isBrightfield() || imageData.getServer() == null || !imageData.getServer().isRGB()) {
Dialogs.showErrorMessage(TITLE, "No brightfield, RGB image selected!");
return;
}
ColorDeconvolutionStains stains = imageData.getColorDeconvolutionStains();
if (stains == null || !stains.getStain(3).isResidual()) {
Dialogs.showErrorMessage(TITLE, "Sorry, stain editing is only possible for brightfield, RGB images with 2 stains");
return;
}
PathObject pathObject = imageData.getHierarchy().getSelectionModel().getSelectedObject();
ROI roi = pathObject == null ? null : pathObject.getROI();
if (roi == null)
roi = ROIs.createRectangleROI(0, 0, imageData.getServer().getWidth(), imageData.getServer().getHeight(), ImagePlane.getDefaultPlane());
double downsample = Math.max(1, Math.sqrt((roi.getBoundsWidth() * roi.getBoundsHeight()) / MAX_PIXELS));
RegionRequest request = RegionRequest.createInstance(imageData.getServerPath(), downsample, roi);
BufferedImage img = null;
try {
img = imageData.getServer().readBufferedImage(request);
} catch (IOException e) {
Dialogs.showErrorMessage("Estimate stain vectors", e);
logger.error("Unable to obtain pixels for " + request.toString(), e);
}
// Apply small amount of smoothing to reduce compression artefacts
img = EstimateStainVectors.smoothImage(img);
// Check modes for background
int[] rgb = img.getRGB(0, 0, img.getWidth(), img.getHeight(), null, 0, img.getWidth());
int[] rgbMode = EstimateStainVectors.getModeRGB(rgb);
int rMax = rgbMode[0];
int gMax = rgbMode[1];
int bMax = rgbMode[2];
// Check if the background values may need to be changed
if (rMax != stains.getMaxRed() || gMax != stains.getMaxGreen() || bMax != stains.getMaxBlue()) {
DialogButton response = Dialogs.showYesNoCancelDialog(TITLE, String.format("Modal RGB values %d, %d, %d do not match current background values - do you want to use the modal values?", rMax, gMax, bMax));
if (response == DialogButton.CANCEL)
return;
else if (response == DialogButton.YES) {
stains = stains.changeMaxValues(rMax, gMax, bMax);
imageData.setColorDeconvolutionStains(stains);
}
}
ColorDeconvolutionStains stainsUpdated = null;
logger.info("Requesting region for stain vector editing: ", request);
try {
stainsUpdated = showStainEditor(img, stains);
} catch (Exception e) {
Dialogs.showErrorMessage(TITLE, "Error with stain estimation: " + e.getLocalizedMessage());
logger.error("{}", e.getLocalizedMessage(), e);
// JOptionPane.showMessageDialog(qupath.getFrame(), "Error with stain estimation: " + e.getLocalizedMessage(), "Estimate stain vectors", JOptionPane.ERROR_MESSAGE, null);
return;
}
if (!stains.equals(stainsUpdated)) {
String suggestedName;
String collectiveNameBefore = stainsUpdated.getName();
if (collectiveNameBefore.endsWith("default"))
suggestedName = collectiveNameBefore.substring(0, collectiveNameBefore.lastIndexOf("default")) + "estimated";
else
suggestedName = collectiveNameBefore;
String newName = Dialogs.showInputDialog(TITLE, "Set name for stain vectors", suggestedName);
if (newName == null)
return;
if (!newName.isBlank())
stainsUpdated = stainsUpdated.changeName(newName);
imageData.setColorDeconvolutionStains(stainsUpdated);
}
}
use of qupath.lib.roi.interfaces.ROI in project qupath by qupath.
the class AbstractTileableDetectionPlugin method addRunnableTasks.
/**
* Intercepts the 'standard' addRunnableTasks to (if necessary) insert ParallelTileObjects along the way,
* thereby breaking an excessively-large parentObject into more manageable pieces.
* <p>
* TODO: Avoid hard-coding what is considered a 'manageable size' or a preferred size for parallel tiles.
*/
@Override
protected void addRunnableTasks(ImageData<T> imageData, PathObject parentObject, List<Runnable> tasks) {
if (imageData == null)
return;
ParameterList params = getParameterList(imageData);
// Determine appropriate sizes
// Note, for v0.1.2 and earlier the downsample was restricted to be a power of 2
double downsampleFactor = ServerTools.getDownsampleFactor(imageData.getServer(), getPreferredPixelSizeMicrons(imageData, params));
int preferred = (int) (PREFERRED_TILE_SIZE * downsampleFactor);
int max = (int) (MAX_TILE_SIZE * downsampleFactor);
ImmutableDimension sizePreferred = ImmutableDimension.getInstance(preferred, preferred);
ImmutableDimension sizeMax = ImmutableDimension.getInstance(max, max);
// parentObject.clearPathObjects();
// Extract (or create) suitable ROI
ROI parentROI = parentObject.getROI();
if (parentROI == null)
parentROI = ROIs.createRectangleROI(0, 0, imageData.getServer().getWidth(), imageData.getServer().getHeight(), ImagePlane.getDefaultPlane());
// Make tiles
Collection<? extends ROI> pathROIs = RoiTools.computeTiledROIs(parentROI, sizePreferred, sizeMax, false, getTileOverlap(imageData, params));
// No tasks to complete
if (pathROIs.isEmpty())
return;
// // Exactly one task to complete
// if (pathROIs.size() == 1 && pathROIs.iterator().next() == parentObject.getROI()) {
// tasks.add(DetectionPluginTools.createRunnableTask(createDetector(imageData, params), getParameterList(imageData), imageData, parentObject));
// return;
// }
ParallelDetectionTileManager manager = new ParallelDetectionTileManager(parentObject);
List<ParallelTileObject> tileList = new ArrayList<>();
AtomicInteger countdown = new AtomicInteger(pathROIs.size());
for (ROI pathROI : pathROIs) {
ParallelTileObject tile = new ParallelTileObject(manager, pathROI, imageData.getHierarchy(), countdown);
parentObject.addPathObject(tile);
for (ParallelTileObject tileTemp : tileList) {
if (tileTemp.suggestNeighbor(tile))
tile.suggestNeighbor(tileTemp);
}
tileList.add(tile);
tasks.add(DetectionPluginTools.createRunnableTask(createDetector(imageData, params), params, imageData, tile));
}
manager.setTiles(tileList);
imageData.getHierarchy().fireHierarchyChangedEvent(this);
}
Aggregations