-
-
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]; | |
} |
@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 @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)"