-
-
Save petebankhead/b5a86caa333de1fdcff6bdee72a20abe to your computer and use it in GitHub Desktop.
| /** | |
| * Convert TIFF fields of view to a pyramidal OME-TIFF. | |
| * | |
| * Locations are parsed from the baseline TIFF tags, therefore these need to be set. | |
| * | |
| * One application of this script is to combine spectrally-unmixed images. | |
| * Be sure to read the script and see where default settings could be changed, e.g. | |
| * - Prompting the user to select files (or using the one currently open the viewer) | |
| * - Using lossy or lossless compression | |
| * | |
| * Note: This version has been updated for QuPath v0.5.x. | |
| * Check the 'Revisions' tab on GitHub to see previous versions. | |
| * | |
| * @author Pete Bankhead | |
| */ | |
| import qupath.fx.dialogs.FileChoosers | |
| import qupath.lib.common.GeneralTools | |
| import qupath.lib.images.servers.ImageServerProvider | |
| import qupath.lib.images.servers.ImageServers | |
| import qupath.lib.images.servers.SparseImageServer | |
| import qupath.lib.images.writers.ome.OMEPyramidWriter | |
| import qupath.lib.regions.ImageRegion | |
| import javax.imageio.ImageIO | |
| import javax.imageio.plugins.tiff.BaselineTIFFTagSet | |
| import javax.imageio.plugins.tiff.TIFFDirectory | |
| import java.awt.image.BufferedImage | |
| import static qupath.lib.gui.scripting.QPEx.* | |
| boolean promptForFiles = true | |
| File dir | |
| List<File> files | |
| String baseName = 'Merged image' | |
| if (promptForFiles) { | |
| files = FileChoosers.promptForMultipleFiles("Choose input files", | |
| FileChoosers.createExtensionFilter("TIFF files", ".tif", ".tiff")) | |
| } else { | |
| // Try to get the URI of the current image that is open | |
| def currentFile = new File(getCurrentServer().getURIs()[0]) | |
| dir = currentFile.getParentFile() | |
| // This naming scheme works for me... | |
| String name = currentFile.getName() | |
| int ind = name.indexOf("_[") | |
| if (ind < 0) | |
| ind = name.toLowerCase().lastIndexOf('.tif') | |
| if (ind >= 0) | |
| baseName = currentFile.getName().substring(0, ind) | |
| // Get all the non-OME TIFF files in the same directory | |
| files = dir.listFiles().findAll { | |
| return it.isFile() && | |
| !it.getName().endsWith('.ome.tif') && | |
| (baseName == null || it.getName().startsWith(baseName)) | |
| } | |
| } | |
| if (!files) { | |
| print 'No TIFF files selected' | |
| return | |
| } | |
| File fileOutput | |
| if (promptForFiles) { | |
| fileOutput = FileChoosers.promptToSaveFile("Output file", null, | |
| FileChoosers.createExtensionFilter("OME-TIFF", ".ome.tif")) | |
| } else { | |
| // Ensure we have a unique output name | |
| fileOutput = new File(dir, baseName+'.ome.tif') | |
| int count = 1 | |
| while (fileOutput.exists()) { | |
| fileOutput = new File(dir, baseName+'-'+count+'.ome.tif') | |
| } | |
| } | |
| if (fileOutput == null) | |
| return | |
| // Parse image regions & create a sparse server | |
| print 'Parsing regions from ' + files.size() + ' files...' | |
| def builder = new SparseImageServer.Builder() | |
| files.parallelStream().forEach { f -> | |
| def region = parseRegion(f) | |
| if (region == null) { | |
| print 'WARN: Could not parse region for ' + f | |
| return | |
| } | |
| def serverBuilder = ImageServerProvider.getPreferredUriImageSupport(BufferedImage.class, f.toURI().toString()).getBuilders().get(0) | |
| builder.jsonRegion(region, 1.0, serverBuilder) | |
| } | |
| print 'Building server...' | |
| def server = builder.build() | |
| server = ImageServers.pyramidalize(server) | |
| long startTime = System.currentTimeMillis() | |
| String pathOutput = fileOutput.getAbsolutePath() | |
| new OMEPyramidWriter.Builder(server) | |
| .downsamples(server.getPreferredDownsamples()) // Use pyramid levels calculated in the ImageServers.pyramidalize(server) method | |
| .tileSize(512) // Requested tile size | |
| .channelsInterleaved() // Because SparseImageServer returns all channels in a BufferedImage, it's more efficient to write them interleaved | |
| .parallelize() // Attempt to parallelize requesting tiles (need to write sequentially) | |
| .losslessCompression() // Use lossless compression (often best for fluorescence, by lossy compression may be ok for brightfield) | |
| .build() | |
| .writePyramid(pathOutput) | |
| long endTime = System.currentTimeMillis() | |
| print('Image written to ' + pathOutput + ' in ' + GeneralTools.formatNumber((endTime - startTime)/1000.0, 1) + ' s') | |
| server.close() | |
| static ImageRegion parseRegion(File file, int z = 0, int t = 0) { | |
| if (checkTIFF(file)) { | |
| try { | |
| return parseRegionFromTIFF(file, z, t) | |
| } catch (Exception e) { | |
| print e.getLocalizedMessage() | |
| } | |
| } | |
| } | |
| /** | |
| * Check for TIFF 'magic number'. | |
| * @param file | |
| * @return | |
| */ | |
| static boolean checkTIFF(File file) { | |
| file.withInputStream { | |
| def bytes = it.readNBytes(4) | |
| short byteOrder = toShort(bytes[0], bytes[1]) | |
| int val | |
| if (byteOrder == 0x4949) { | |
| // Little-endian | |
| val = toShort(bytes[3], bytes[2]) | |
| } else if (byteOrder == 0x4d4d) { | |
| val = toShort(bytes[2], bytes[3]) | |
| } else | |
| return false | |
| return val == 42 || val == 43 | |
| } | |
| } | |
| /** | |
| * Combine two bytes to create a short, in the given order | |
| * @param b1 | |
| * @param b2 | |
| * @return | |
| */ | |
| static short toShort(byte b1, byte b2) { | |
| return (b1 << 8) + (b2 << 0) | |
| } | |
| /** | |
| * Parse an ImageRegion from a TIFF image, using the metadata. | |
| * @param file image file | |
| * @param z index of z plane | |
| * @param t index of timepoint | |
| * @return | |
| */ | |
| static ImageRegion parseRegionFromTIFF(File file, int z = 0, int t = 0) { | |
| int x, y, width, height | |
| file.withInputStream { | |
| def reader = ImageIO.getImageReadersByFormatName("TIFF").next() | |
| reader.setInput(ImageIO.createImageInputStream(it)) | |
| def metadata = reader.getImageMetadata(0) | |
| def tiffDir = TIFFDirectory.createFromMetadata(metadata) | |
| double xRes = getRational(tiffDir, BaselineTIFFTagSet.TAG_X_RESOLUTION) | |
| double yRes = getRational(tiffDir, BaselineTIFFTagSet.TAG_Y_RESOLUTION) | |
| double xPos = getRational(tiffDir, BaselineTIFFTagSet.TAG_X_POSITION) | |
| double yPos = getRational(tiffDir, BaselineTIFFTagSet.TAG_Y_POSITION) | |
| width = tiffDir.getTIFFField(BaselineTIFFTagSet.TAG_IMAGE_WIDTH).getAsLong(0) as int | |
| height = tiffDir.getTIFFField(BaselineTIFFTagSet.TAG_IMAGE_LENGTH).getAsLong(0) as int | |
| x = Math.round(xRes * xPos) as int | |
| y = Math.round(yRes * yPos) as int | |
| } | |
| return ImageRegion.createInstance(x, y, width, height, z, t) | |
| } | |
| /** | |
| * Helper for parsing rational from TIFF metadata. | |
| * @param tiffDir | |
| * @param tag | |
| * @return | |
| */ | |
| static double getRational(TIFFDirectory tiffDir, int tag) { | |
| long[] rational = tiffDir.getTIFFField(tag).getAsRational(0); | |
| return rational[0] / (double)rational[1]; | |
| } |
Hi @petebankhead ,When we are working with Qupath V0.5.0, it showed a method that doesn't exist as followed, I was wondering if there is anything wrong here.
"ERROR: It looks like you've tried to access a method that doesn't exist.
ERROR: No signature of method: static qupath.fx.dialogs.Dialogs.promptForMultipleFiles() is applicable for argument types: (String, null, String, String, String) values: [Choose input files, null, TIFF files, .tif, .tiff] in QuPathScript at line number 35
groovy.lang.MetaClassImpl.invokeStaticMissingMethod(MetaClassImpl.java:1642)
groovy.lang.MetaClassImpl.invokeStaticMethod(MetaClassImpl.java:1628)
org.codehaus.groovy.vmplugin.v8.IndyInterface.fromCache(IndyInterface.java:321)
QuPathScript.run(QuPathScript:35)
org.codehaus.groovy.jsr223.GroovyScriptEngineImpl.eval(GroovyScriptEngineImpl.java:331)
org.codehaus.groovy.jsr223.GroovyScriptEngineImpl.eval(GroovyScriptEngineImpl.java:161)
qupath.lib.gui.scripting.languages.DefaultScriptLanguage.execute(DefaultScriptLanguage.java:234)
qupath.lib.gui.scripting.DefaultScriptEditor.executeScript(DefaultScriptEditor.java:1166)
qupath.lib.gui.scripting.DefaultScriptEditor$3.run(DefaultScriptEditor.java:1534)
java.base/java.util.concurrent.Executors$RunnableAdapter.call(Unknown Source)
java.base/java.util.concurrent.FutureTask.run(Unknown Source)
java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
java.base/java.lang.Thread.run(Unknown Source)"
@NJ1989 I've updated the script - but I have no images to test if it works. If you'd like to try it, please let me know the outcome.
(The relevant discussion on the forum is at https://forum.image.sc/t/merge-tiffs-via-script-via-command-line-arguments/90099 )
@petebankhead Hi,the first problem is fine, thanks! But when I try the second apprearing prompt , which ask to name the resulting stitched OME.TIF file and too select where to save it to. An error showed as followed:
ERROR: Cannot invoke "qupath.lib.images.servers.ImageServerMetadata.duplicate()" because "metadata" is null in QuPathScript at line number 92
qupath.lib.images.servers.ImageServerMetadata$Builder.(ImageServerMetadata.java:165)
qupath.lib.images.servers.SparseImageServer.(SparseImageServer.java:152)
qupath.lib.images.servers.SparseImageServer$Builder.build(SparseImageServer.java:333)
org.codehaus.groovy.vmplugin.v8.IndyInterface.fromCache(IndyInterface.java:321)
QuPathScript.run(QuPathScript:92)
org.codehaus.groovy.jsr223.GroovyScriptEngineImpl.eval(GroovyScriptEngineImpl.java:331)
org.codehaus.groovy.jsr223.GroovyScriptEngineImpl.eval(GroovyScriptEngineImpl.java:161)
qupath.lib.gui.scripting.languages.DefaultScriptLanguage.execute(DefaultScriptLanguage.java:234)
qupath.lib.gui.scripting.DefaultScriptEditor.executeScript(DefaultScriptEditor.java:1166)
qupath.lib.gui.scripting.DefaultScriptEditor$3.run(DefaultScriptEditor.java:1534)
java.base/java.util.concurrent.Executors$RunnableAdapter.call(Unknown Source)
java.base/java.util.concurrent.FutureTask.run(Unknown Source)
java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
java.base/java.lang.Thread.run(Unknown Source)
For help interpreting this error, please search the forum at https://forum.image.sc/tag/qupath
You can also start a new discussion there, including both your script & the messages in this log.
I'm afraid I have no data to test this, and there are too many things I don't know about your images and how you're using the script. I'd suggest posting on the forum (link at the bottom of the error message).
Hi @chrysanthiiliadi can you post your question on https://forum.image.sc/tag/qupath and include the exact error message place?
I know others are using variations of this script, but I haven't used it myself in years - and I don't even have any suitable data to test it with.