Last active
October 15, 2024 06:08
-
-
Save Svidro/259c8baa9037579828d17e7d65703346 to your computer and use it in GitHub Desktop.
Export Images
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Image exporting in QuPath | |
Note, this is likely a mess, and I won't be able to provide much support since I don't do this much, | |
but requests are common and I have trouble tracking things down each time. | |
Added a script from the forum for importing binary masks as annotations. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// @petebankhead 0.2.3+ | |
// https://forum.image.sc/t/qupath-scripting-1-using-clupath-to-save-smoothed-image-regions/49525/4 | |
import qupath.lib.images.writers.ome.OMEPyramidWriter | |
def tilesize = 512 | |
def outputDownsample = 1 | |
def pyramidscaling = 2 | |
def compression = OMEPyramidWriter.CompressionType.J2K_LOSSY //J2K //UNCOMPRESSED //LZW | |
def imageData = getCurrentImageData() | |
def op = ImageOps.buildImageDataOp() | |
.appendOps(ImageOps.Filters.gaussianBlur(10.0)) | |
def serverSmooth = ImageOps.buildServer(imageData, op, imageData.getServer().getPixelCalibration()) | |
print serverSmooth.getPreferredDownsamples() | |
def pathOutput = buildFilePath(PROJECT_BASE_DIR, "smoothed.ome.tif") | |
new OMEPyramidWriter.Builder(serverSmooth) | |
.compression(compression) | |
.parallelize() | |
.tileSize(tilesize) | |
.channelsInterleaved() // Usually faster | |
.scaledDownsampling(outputDownsample, pyramidscaling) | |
.build() | |
.writePyramid(pathOutput) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//https://forum.image.sc/t/mask-script-doesnt-work-in-qupath-m4/33259 | |
//@thomasleb0n | |
import qupath.lib.regions.* | |
import ij.* | |
import java.awt.Color | |
import java.awt.image.BufferedImage | |
import javax.imageio.ImageIO | |
// Read RGB image & show in ImageJ (won't work for multichannel!) | |
double downsample = 1.0 | |
def server = getCurrentImageData().getServer() | |
int w = (server.getWidth() / downsample) as int | |
int h = (server.getHeight() / downsample) as int | |
def img = new BufferedImage(w, h, BufferedImage.TYPE_BYTE_GRAY) | |
def g2d = img.createGraphics() | |
g2d.scale(1.0/downsample, 1.0/downsample) | |
g2d.setColor(Color.WHITE) | |
for (detection in getDetectionObjects()) { | |
roi = detection.getROI() | |
def shape = roi.getShape() | |
g2d.fill(shape) | |
} | |
g2d.dispose() | |
new ImagePlus("Mask", img).show() | |
def name = getProjectEntry().getImageName() //+ '.tiff' | |
def path = buildFilePath(PROJECT_BASE_DIR, 'mask') | |
mkdirs(path) | |
def fileoutput = new File( path, name+ '-mask.png') | |
ImageIO.write(img, 'PNG', fileoutput) | |
println('Results exporting...') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//M6... may not work in most versions, but the expansions idea should be similar | |
//https://forum.image.sc/t/enlarging-images-around-relevant-masks-during-export/34220/8?u=research_associate | |
//@Pietro_Cicalese | |
/** | |
* Script to export binary masks corresponding to all annotations of an image, | |
* optionally along with extracted image regions. | |
* | |
* Note: Pay attention to the 'downsample' value to control the export resolution! | |
* | |
* @author Pete Bankhead | |
*/ | |
import qupath.lib.images.servers.ImageServer | |
import qupath.lib.objects.PathObject | |
import javax.imageio.ImageIO | |
import java.awt.Color | |
import java.awt.image.BufferedImage | |
// Get the main QuPath data structures | |
def imageData = getCurrentImageData() | |
def hierarchy = imageData.getHierarchy() | |
def server = imageData.getServer() | |
// Request all objects from the hierarchy & filter only the annotations | |
def annotations = hierarchy.getAnnotationObjects() | |
// Define downsample value for export resolution & output directory, creating directory if necessary | |
// The pad variable will add padding to both the image and mask output images (# of pixels) | |
def downsample = 1.0 | |
pad = 75 | |
def pathOutput = buildFilePath(QPEx.PROJECT_BASE_DIR, 'masks') | |
mkdirs(pathOutput) | |
// Define image export type; valid values are JPG, PNG or null (if no image region should be exported with the mask) | |
// Note: masks will always be exported as PNG | |
def imageExportType = 'PNG' | |
// Export each annotation | |
annotations.each { | |
saveImageAndMask(pathOutput, server, it, downsample, imageExportType) | |
} | |
print 'Done!' | |
/** | |
* Save extracted image region & mask corresponding to an object ROI. | |
* | |
* @param pathOutput Directory in which to store the output | |
* @param server ImageServer for the relevant image | |
* @param pathObject The object to export | |
* @param downsample Downsample value for the export of both image region & mask | |
* @param imageExportType Type of image (original pixels, not mask!) to export ('JPG', 'PNG' or null) | |
* @return | |
*/ | |
def saveImageAndMask(String pathOutput, ImageServer server, PathObject pathObject, double downsample, String imageExportType) { | |
// Extract ROI & classification name | |
def roi = pathObject.getROI() | |
def pathClass = pathObject.getPathClass() | |
def classificationName = pathClass == null ? 'None' : pathClass.toString() | |
if (roi == null) { | |
print 'Warning! No ROI for object ' + pathObject + ' - cannot export corresponding region & mask' | |
return | |
} | |
// Create a region from the ROI | |
def region = RegionRequest.createInstance(server.getPath(), downsample, (int)roi.getBoundsX()-pad, (int)roi.getBoundsY()-pad, (int)roi.getBoundsWidth() + pad*2, (int)roi.getBoundsHeight() + pad*2, roi.getZ(), roi.getT()) | |
// Create a name | |
String name = String.format('%s_%s_(%.2f,%d,%d,%d,%d)', | |
server.getMetadata().getName(), | |
classificationName, | |
region.getDownsample(), | |
region.getX(), | |
region.getY(), | |
region.getWidth(), | |
region.getHeight() | |
) | |
// Request the BufferedImage | |
def img = server.readBufferedImage(region) | |
// Create a mask using Java2D functionality | |
// (This involves applying a transform to a graphics object, so that none needs to be applied to the ROI coordinates) | |
def shape = RoiTools.getShape(roi) | |
def imgMask = new BufferedImage(img.getWidth(), img.getHeight(), BufferedImage.TYPE_BYTE_GRAY) | |
def g2d = imgMask.createGraphics() | |
g2d.setColor(Color.WHITE) | |
g2d.scale(1.0/downsample, 1.0/downsample) | |
g2d.translate(-region.getX(), -region.getY()) | |
g2d.fill(shape) | |
g2d.dispose() | |
// Create filename & export | |
if (imageExportType != null) { | |
def fileImage = new File(pathOutput, name + '.' + imageExportType.toLowerCase()) | |
ImageIO.write(img, imageExportType, fileImage) | |
} | |
// Export the mask | |
def fileMask = new File(pathOutput, name + '-mask.png') | |
ImageIO.write(imgMask, 'PNG', fileMask) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//From this post: https://forum.image.sc/t/macro-image-displaying-annotations/30214/14?u=research_associate | |
double downsample = 100.0 | |
def server = getCurrentServer() | |
def request = RegionRequest.createInstance(server, downsample) | |
def img = server.readBufferedImage(request) | |
float thickness = 2 | |
def g2d = img.createGraphics() | |
g2d.setColor(java.awt.Color.BLACK) | |
g2d.scale(1.0/downsample, 1.0/downsample) | |
g2d.setStroke(new java.awt.BasicStroke((float)(thickness * downsample))) | |
getAnnotationObjects().each { g2d.draw(it.getROI().getShape()) } | |
g2d.dispose() | |
def path = buildFilePath(PROJECT_BASE_DIR, 'something.png') | |
writeImage(img, path) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//0.2.2 | |
//https://forum.image.sc/t/script-for-send-region-to-imagej/39554/18 | |
import ij.IJ | |
import qupath.imagej.gui.IJExtension | |
double downsample = 4.0 | |
def server = getCurrentServer() | |
def request = RegionRequest.createInstance(server, downsample) | |
boolean setROI = false | |
def imp = IJExtension.extractROIWithOverlay( | |
getCurrentServer(), | |
null, | |
getCurrentHierarchy(), | |
request, | |
setROI, | |
getCurrentViewer().getOverlayOptions() | |
).getImage() | |
IJ.save(imp, '/path/to/export.tif') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//0.2.3 | |
//https://forum.image.sc/t/exporting-rendered-svg-images-in-batch-mode/52045/2 | |
import qupath.lib.extension.svg.SvgTools.SvgBuilder | |
def imageData = getCurrentImageData() | |
def options = getCurrentViewer().getOverlayOptions() | |
def doc = new SvgBuilder() | |
.imageData(imageData) | |
.options(options) | |
.downsample(1) // Increase if needed | |
.createDocument() | |
def name = GeneralTools.getNameWithoutExtension(getProjectEntry().getImageName()) | |
def path = buildFilePath(PROJECT_BASE_DIR, name + '.svg') | |
new File(path).text = doc |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//Tested for 0.2.3 | |
// Write the full image, displaying objects according to how they are currently shown in the viewer | |
double downsample = 10.0 | |
def server = getCurrentServer() | |
def name = getProjectEntry().getImageName() | |
def viewer = getCurrentViewer() | |
getCurrentHierarchy().getTMAGrid().getTMACoreList().each{ | |
mkdirs(buildFilePath(PROJECT_BASE_DIR,'export')) | |
def path = buildFilePath(PROJECT_BASE_DIR,'export', name+" "+ it.getName()+".tif") | |
def request = RegionRequest.createInstance(server.getPath(), downsample, it.getROI()) | |
writeRenderedImageRegion(viewer,request, path) | |
} | |
print "Done" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//0.2.0 | |
//https://forum.image.sc/t/script-for-send-region-to-imagej/39554/3?u=research_associate | |
//exporting to imageJ with overlay | |
import qupath.imagej.gui.IJExtension | |
double downsample = 2.0 | |
def server = getCurrentServer() | |
def selectedObject = getSelectedObject() | |
def request = RegionRequest.createInstance(server.getPath(), downsample, selectedObject.getROI()) | |
boolean setROI = true | |
def imp = IJExtension.extractROIWithOverlay( | |
getCurrentServer(), | |
selectedObject, | |
getCurrentHierarchy(), | |
request, | |
setROI, | |
getCurrentViewer().getOverlayOptions() | |
).getImage() | |
imp.show() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Script written for QuPath v0.2.3 | |
// Minimal working script to import labelled images | |
// (from the TileExporter) back into QuPath as annotations. | |
import qupath.lib.objects.PathObjects | |
import qupath.lib.regions.ImagePlane | |
import static qupath.lib.gui.scripting.QPEx.* | |
import ij.IJ | |
import ij.process.ColorProcessor | |
import qupath.imagej.processing.RoiLabeling | |
import qupath.imagej.tools.IJTools | |
import java.util.regex.Matcher | |
import java.util.regex.Pattern | |
def directoryPath = 'path/to/your/directory' // TO CHANGE | |
File folder = new File(directoryPath); | |
File[] listOfFiles = folder.listFiles(); | |
listOfFiles.each { file -> | |
def path = file.getPath() | |
def imp = IJ.openImage(path) | |
// Only process the labelled images, not the originals | |
if (!path.endsWith("-labelled.tif")) | |
return | |
print "Now processing: " + path | |
// Parse filename to understand where the tile was located | |
def parsedXY = parseFilename(GeneralTools.getNameWithoutExtension(path)) | |
double downsample = 1 // TO CHANGE (if needed) | |
ImagePlane plane = ImagePlane.getDefaultPlane() | |
// Convert labels to ImageJ ROIs | |
def ip = imp.getProcessor() | |
if (ip instanceof ColorProcessor) { | |
throw new IllegalArgumentException("RGB images are not supported!") | |
} | |
int n = imp.getStatistics().max as int | |
if (n == 0) { | |
print 'No objects found!' | |
return | |
} | |
def roisIJ = RoiLabeling.labelsToConnectedROIs(ip, n) | |
// Convert ImageJ ROIs to QuPath ROIs | |
def rois = roisIJ.collect { | |
if (it == null) | |
return | |
return IJTools.convertToROI(it, -parsedXY[0]/downsample, -parsedXY[1]/downsample, downsample, plane); | |
} | |
// Remove all null values from list | |
rois = rois.findAll{null != it} | |
// Convert QuPath ROIs to objects | |
def pathObjects = rois.collect { | |
return PathObjects.createAnnotationObject(it) | |
} | |
addObjects(pathObjects) | |
} | |
resolveHierarchy() | |
int[] parseFilename(String filename) { | |
def p = Pattern.compile("\\[x=(.+?),y=(.+?),") | |
parsedXY = [] | |
Matcher m = p.matcher(filename) | |
if (!m.find()) | |
throw new IOException("Filename does not contain tile position") | |
parsedXY << (m.group(1) as double) | |
parsedXY << (m.group(2) as double) | |
return parsedXY | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* https://forum.image.sc/t/qupath-0-2-0m11-updated-import-masks-as-annotations-script/37427 | |
* Script to import binary masks & create annotations, adding them to the current object hierarchy. | |
* | |
* It is assumed that each mask is stored in a PNG file in a project subdirectory called 'masks'. | |
* Each file name should be of the form: | |
* [Short original image name]_[Classification name]_([downsample],[x],[y],[width],[height])-mask.png | |
* | |
* Note: It's assumed that the classification is a simple name without underscores, i.e. not a 'derived' classification | |
* (so 'Tumor' is ok, but 'Tumor: Positive' is not) | |
* | |
* The x, y, width & height values should be in terms of coordinates for the full-resolution image. | |
* | |
* By default, the image name stored in the mask filename has to match that of the current image - but this check can be turned off. | |
* | |
* @author Pete Bankhead | |
*/ | |
import ij.measure.Calibration | |
import ij.plugin.filter.ThresholdToSelection | |
import ij.process.ByteProcessor | |
import ij.process.ImageProcessor | |
import qupath.imagej.tools.IJTools | |
import qupath.lib.objects.PathAnnotationObject | |
import qupath.lib.objects.classes.PathClassFactory | |
import static qupath.lib.gui.scripting.QPEx.* | |
import javax.imageio.ImageIO | |
import qupath.lib.regions.ImagePlane | |
import qupath.lib.roi.ROIs | |
import qupath.lib.objects.PathObjects | |
// Get the main QuPath data structures | |
def imageData = QPEx.getCurrentImageData() | |
def hierarchy = imageData.getHierarchy() | |
def server = getCurrentServer() | |
// Only parse files that contain the specified text; set to '' if all files should be included | |
// (This is used to avoid adding masks intended for a different image) | |
def includeText = server.getMetadata().getName() | |
// Get a list of image files, stopping early if none can be found | |
def pathOutput = QPEx.buildFilePath(QPEx.PROJECT_BASE_DIR, 'masks') | |
def dirOutput = new File(pathOutput) | |
if (!dirOutput.isDirectory()) { | |
print dirOutput + ' is not a valid directory!' | |
return | |
} | |
def files = dirOutput.listFiles({f -> f.isFile() && f.getName().contains(includeText) && f.getName().endsWith('-mask.png') } as FileFilter) as List | |
if (files.isEmpty()) { | |
print 'No mask files found in ' + dirOutput | |
return | |
} | |
// Create annotations for all the files | |
def annotations = [] | |
files.each { | |
try { | |
annotations << parseAnnotation(it) | |
} catch (Exception e) { | |
print 'Unable to parse annotation from ' + it.getName() + ': ' + e.getLocalizedMessage() | |
} | |
} | |
// Add annotations to image | |
hierarchy.addPathObjects(annotations) | |
/** | |
* Create a new annotation from a binary image, parsing the classification & region from the file name. | |
* | |
* Note: this code doesn't bother with error checking or handling potential issues with formatting/blank images. | |
* If something is not quite right, it is quite likely to throw an exception. | |
* | |
* @param file File containing the PNG image mask. The image name must be formatted as above. | |
* @return The PathAnnotationObject created based on the mask & file name contents. | |
*/ | |
def parseAnnotation(File file) { | |
// Read the image | |
def img = ImageIO.read(file) | |
// Split the file name into parts: [Image name, Classification, Region] | |
def parts = file.getName().replace('-mask.png', '').split('_') | |
// Discard all but the last 2 parts - it's possible that the original name contained underscores, | |
// so better to work from the end of the list and not the start | |
def classificationString = parts[-2] | |
// Extract region, and trim off parentheses (admittedly in a lazy way...) | |
def regionString = parts[-1].replace('(', '').replace(')', '') | |
// Create a classification, if necessary | |
def pathClass = null | |
if (classificationString != 'None') | |
pathClass = PathClassFactory.getPathClass(classificationString) | |
// Parse the x, y coordinates of the region - width & height not really needed | |
// (but could potentially be used to estimate the downsample value, if we didn't already have it) | |
def regionParts = regionString.split(',') | |
double downsample = regionParts[0] as double | |
int x = regionParts[1] as int | |
int y = regionParts[2] as int | |
// To create the ROI, travel into ImageJ | |
def bp = new ByteProcessor(img) | |
bp.setThreshold(127.5, Double.MAX_VALUE, ImageProcessor.NO_LUT_UPDATE) | |
def roiIJ = new ThresholdToSelection().convert(bp) | |
int z = 0 | |
int t = 0 | |
def plane = ImagePlane.getPlane(z, t) | |
// Convert ImageJ ROI to a QuPath ROI | |
// This assumes we have a single 2D image (no z-stack, time series) | |
// Currently, we need to create an ImageJ Calibration object to store the origin | |
// (this might be simplified in a later version) | |
def cal = new Calibration() | |
cal.xOrigin = -x/downsample | |
cal.yOrigin = -y/downsample | |
def roi = IJTools.convertToROI(roiIJ, cal, downsample,plane) | |
// Create & return the object | |
return new PathAnnotationObject(roi, pathClass) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//0.2.0 | |
//https://forum.image.sc/t/qupath-script-with-pixel-classifier/45597/10?u=research_associate | |
def imageData = getCurrentImageData() | |
def classifier = loadPixelClassifier('Classifier') | |
def predictionServer = PixelClassifierTools.createPixelClassificationServer(imageData, classifier) | |
def path = buildFilePath(PROJECT_BASE_DIR, 'prediction.tif') | |
def downsample = predictionServer.getDownsampleForResolution(0) | |
def request = RegionRequest.createInstance(predictionServer, downsample) | |
writeImageRegion(predictionServer, request, path) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//https://forum.image.sc/t/qupath-measurement-maps-image-export-script/50373/3?u=research_associate | |
import qupath.lib.gui.tools.MeasurementMapper | |
import qupath.lib.gui.images.servers.RenderedImageServer | |
// Define the color map name | |
String colorMapName = 'Magma' | |
// Load a color mapper | |
def colorMapper = MeasurementMapper.loadColorMappers().find {it.name == colorMapName} | |
println colorMapper | |
// Define measurement & display range | |
def name = "Nucleus: Circularity" // Set to null to reset | |
double minValue = 0.0 | |
double maxValue = 1.0 | |
// Request current viewer & objects | |
def viewer = getCurrentViewer() | |
def options = viewer.getOverlayOptions() | |
def detections = getDetectionObjects() | |
// Update the display | |
if (name) { | |
print String.format('Setting measurement map: %s (%.2f - %.2f)', name, minValue, maxValue) | |
def mapper = new MeasurementMapper(colorMapper, name, detections) | |
mapper.setDisplayMinValue(minValue) | |
mapper.setDisplayMaxValue(maxValue) | |
options.setMeasurementMapper(mapper) | |
} else { | |
print 'Resetting measurement map' | |
options.setMeasurementMapper(null) | |
} | |
// Now export the rendered image | |
import qupath.imagej.tools.IJTools | |
import qupath.lib.gui.images.servers.RenderedImageServer | |
import qupath.lib.gui.viewer.overlays.HierarchyOverlay | |
import qupath.lib.regions.RegionRequest | |
// It is important to define the downsample! | |
// This is required to determine annotation line thicknesses | |
double downsample = 10 | |
// Add the output file path here | |
String path = buildFilePath(PROJECT_BASE_DIR, 'rendered') | |
mkdirs(path) | |
// Request the current viewer for settings, and current image (which may be used in batch processing) | |
def imageData = getCurrentImageData() | |
// Create a rendered server that includes a hierarchy overlay using the current display settings | |
def server = new RenderedImageServer.Builder(imageData) | |
.downsamples(downsample) | |
.layers(new HierarchyOverlay(null, options, imageData)) | |
.build() | |
// Write or display the rendered image | |
int count = 0 | |
for (annotation in getAnnotationObjects()) { | |
count++ | |
def imageName = getProjectEntry().getImageName() + count + '.png' | |
def path2 = buildFilePath(path, imageName) | |
def region = RegionRequest.createInstance(server.getPath(), downsample, annotation.getROI()) | |
writeImageRegion(server, region, path2) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//https://forum.image.sc/t/imagej-rois-and-rotated-image-server-issue-on-qupath/47019/3 | |
import java.awt.geom.AffineTransform | |
def server = getCurrentServer() | |
def transform = AffineTransform.getRotateInstance(Math.PI) | |
transform.translate(-server.getWidth(), -server.getHeight()) | |
def annotations = getAnnotationObjects() | |
def transformedAnnotations = annotations.collect {PathObjectTools.transformObject(it, transform, true)} | |
removeObjects(annotations, true) | |
addObjects(transformedAnnotations) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment