-
-
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 @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.
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).
Hello! Thank you very much for this. It is extremely helpful for the opal analysis!! I tried running it on QuPath 0.5.0, but it does not work; it only runs in QuPath 0.4. I am using the Stardist extension on 0.5, so now I'm analyzing using two different versions of QuPath. Is there a way to update the latest version so this script runs there?
Thank you again!