use of qupath.lib.regions.RegionRequest 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.regions.RegionRequest 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.regions.RegionRequest in project qupath by qupath.
the class ContourTracing method traceGeometries.
/**
* Trace one or more geometries in an image.
* @param server
* @param regionRequest optional region defining the area within which geometries should be traced
* @param clipArea optional clip region, intersected with the created geometries (may be null)
* @param thresholds min/max thresholds (inclusive) to apply to each channel to generate objects
* @return
* @throws IOException
*/
public static Map<Integer, Geometry> traceGeometries(ImageServer<BufferedImage> server, RegionRequest regionRequest, Geometry clipArea, ChannelThreshold... thresholds) throws IOException {
RegionRequest region = regionRequest;
if (region == null) {
if (clipArea == null) {
region = RegionRequest.createInstance(server, server.getDownsampleForResolution(0));
} else {
var env = clipArea.getEnvelopeInternal();
region = RegionRequest.createInstance(server.getPath(), server.getDownsampleForResolution(0), GeometryTools.envelopToRegion(env, 0, 0));
}
} else if (clipArea != null) {
// Ensure we don't compute more than we need to
var env = clipArea.getEnvelopeInternal();
region = region.intersect2D(GeometryTools.envelopToRegion(env, region.getZ(), region.getT()));
}
Collection<TileRequest> tiles = server.getTileRequestManager().getTileRequests(region);
if (thresholds.length == 0 || tiles.isEmpty())
return Collections.emptyMap();
// If the region downsample doesn't match the tile requests, the scaling may be off
// One way to resolve that (without requiring the region to be read in one go) is to generate new tile requests for a pyramidalized server at the correct resolution
double downsample = region.getDownsample();
if (Math.abs(tiles.iterator().next().getDownsample() - downsample) > 1e-3) {
server = ImageServers.pyramidalize(server, downsample);
tiles = server.getTileRequestManager().getTileRequests(region);
}
return traceGeometriesImpl(server, tiles, clipArea, thresholds);
// TODO: Consider restricting parallelization
// int nThreads = Math.min(Math.max(1, Math.max(thresholds.length, tiles.size())), Runtime.getRuntime().availableProcessors());
// var pool = new ForkJoinPool(nThreads);
// var task = pool.submit(() -> traceGeometriesImpl(server, tiles, clipArea, thresholds));
// pool.shutdown();
// try {
// return task.get();
// } catch (InterruptedException e) {
// // TODO Auto-generated catch block
// e.printStackTrace();
// } catch (ExecutionException e) {
// // TODO Auto-generated catch block
// e.printStackTrace();
// }
}
use of qupath.lib.regions.RegionRequest 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.regions.RegionRequest in project qupath by qupath.
the class TMADataIO method writeTMAData.
/**
* Write TMA data in a human-readable (and viewable) way, with JPEGs and TXT/CSV files.
*
* @param file
* @param imageData
* @param overlayOptions
* @param downsampleFactor The downsample factor used for the TMA cores. If NaN, an automatic downsample value will be selected (>= 1). If <= 0, no cores are exported.
*/
public static void writeTMAData(File file, final ImageData<BufferedImage> imageData, OverlayOptions overlayOptions, final double downsampleFactor) {
if (imageData == null || imageData.getHierarchy() == null || imageData.getHierarchy().getTMAGrid() == null) {
logger.error("No TMA data available to save!");
return;
}
final ImageServer<BufferedImage> server = imageData.getServer();
String coreExt = imageData.getServer().isRGB() ? ".jpg" : ".tif";
if (file == null) {
file = Dialogs.promptToSaveFile("Save TMA data", null, ServerTools.getDisplayableImageName(server), "TMA data", "qptma");
if (file == null)
return;
} else if (file.isDirectory() || (!file.exists() && file.getAbsolutePath().endsWith(File.pathSeparator))) {
// Put inside the specified directory
file = new File(file, ServerTools.getDisplayableImageName(server) + TMA_DEARRAYING_DATA_EXTENSION);
if (!file.getParentFile().exists())
file.getParentFile().mkdirs();
}
final File dirData = new File(file + ".data");
if (!dirData.exists())
dirData.mkdir();
// Write basic file info
String delimiter = "\t";
TMAGrid tmaGrid = imageData.getHierarchy().getTMAGrid();
try {
PrintWriter writer = new PrintWriter(file);
writer.println(server.getPath());
writer.println(ServerTools.getDisplayableImageName(server));
writer.println();
writer.println("TMA grid width: " + tmaGrid.getGridWidth());
writer.println("TMA grid height: " + tmaGrid.getGridHeight());
writer.println("Core name" + delimiter + "X" + delimiter + "Y" + delimiter + "Width" + delimiter + "Height" + delimiter + "Present" + delimiter + TMACoreObject.KEY_UNIQUE_ID);
for (int row = 0; row < tmaGrid.getGridHeight(); row++) {
for (int col = 0; col < tmaGrid.getGridWidth(); col++) {
TMACoreObject core = tmaGrid.getTMACore(row, col);
if (!core.hasROI()) {
writer.println(core.getName() + delimiter + delimiter + delimiter + delimiter);
continue;
}
ROI pathROI = core.getROI();
int x = (int) pathROI.getBoundsX();
int y = (int) pathROI.getBoundsY();
int w = (int) Math.ceil(pathROI.getBoundsWidth());
int h = (int) Math.ceil(pathROI.getBoundsHeight());
String id = core.getUniqueID() == null ? "" : core.getUniqueID();
writer.println(core.getName() + delimiter + x + delimiter + y + delimiter + w + delimiter + h + delimiter + !core.isMissing() + delimiter + id);
}
}
writer.close();
} catch (Exception e) {
logger.error("Error writing TMA data: " + e.getLocalizedMessage(), e);
return;
}
// Save the summary results
ObservableMeasurementTableData tableData = new ObservableMeasurementTableData();
tableData.setImageData(imageData, tmaGrid.getTMACoreList());
SummaryMeasurementTableCommand.saveTableModel(tableData, new File(dirData, "TMA results - " + ServerTools.getDisplayableImageName(server) + ".txt"), Collections.emptyList());
boolean outputCoreImages = Double.isNaN(downsampleFactor) || downsampleFactor > 0;
if (outputCoreImages) {
// Create new overlay options, if we don't have some already
if (overlayOptions == null) {
overlayOptions = new OverlayOptions();
overlayOptions.setFillDetections(true);
}
final OverlayOptions options = overlayOptions;
// Write an overall TMA map (for quickly checking if the dearraying is ok)
File fileTMAMap = new File(dirData, "TMA map - " + ServerTools.getDisplayableImageName(server) + ".jpg");
double downsampleThumbnail = Math.max(1, (double) Math.max(server.getWidth(), server.getHeight()) / 1024);
RegionRequest request = RegionRequest.createInstance(server.getPath(), downsampleThumbnail, 0, 0, server.getWidth(), server.getHeight());
OverlayOptions optionsThumbnail = new OverlayOptions();
optionsThumbnail.setShowTMAGrid(true);
optionsThumbnail.setShowGrid(false);
optionsThumbnail.setShowAnnotations(false);
optionsThumbnail.setShowDetections(false);
try {
var renderedServer = new RenderedImageServer.Builder(imageData).layers(new TMAGridOverlay(overlayOptions)).downsamples(downsampleThumbnail).build();
ImageWriterTools.writeImageRegion(renderedServer, request, fileTMAMap.getAbsolutePath());
// ImageWriters.writeImageRegionWithOverlay(imageData.getServer(), Collections.singletonList(new TMAGridOverlay(overlayOptions, imageData)), request, fileTMAMap.getAbsolutePath());
} catch (IOException e) {
logger.warn("Unable to write image overview: " + e.getLocalizedMessage(), e);
}
final double downsample = Double.isNaN(downsampleFactor) ? (server.getPixelCalibration().hasPixelSizeMicrons() ? ServerTools.getDownsampleFactor(server, preferredExportPixelSizeMicrons) : 1) : downsampleFactor;
// Creating a plugin makes it possible to parallelize & show progress easily
var renderedImageServer = new RenderedImageServer.Builder(imageData).layers(new HierarchyOverlay(null, options, imageData)).downsamples(downsample).build();
ExportCoresPlugin plugin = new ExportCoresPlugin(dirData, renderedImageServer, downsample, coreExt);
PluginRunner<BufferedImage> runner;
var qupath = QuPathGUI.getInstance();
if (qupath == null || qupath.getImageData() != imageData) {
runner = new CommandLinePluginRunner<>(imageData);
plugin.runPlugin(runner, null);
} else {
try {
qupath.runPlugin(plugin, null, false);
} catch (Exception e) {
logger.error("Error writing TMA data: " + e.getLocalizedMessage(), e);
}
// new Thread(() -> qupath.runPlugin(plugin, null, false)).start();
// runner = new PluginRunnerFX(QuPathGUI.getInstance());
// new Thread(() -> plugin.runPlugin(runner, null)).start();
}
}
}
Aggregations