Tilling issue with lif file

Dear @OMETeam,

We encountered an issue with stitching of lif files, somehow the coordinates seems to be wrong.

Please find below the stitching by LASX (Leica), the one by Bio-formats importer with the option “stitch tiles” and one obtained using a script that swapped the x and y coordinates plus flips the individual images vertically and horizontally before stitching.

download dataset


Thank you in advance for your help,


Hi @romainGuiet, from the dataset provided I was able to reproduce the issue as reported. The tile stitching that occurs as part of the plugin is based on the plane position X and Y values. For the given file it appears that these positions in the metadata do not match with the tiles.

Do the coordinates in the OME Metadata look correct to you or does it look as though the X and Y metadata positions are flipped?

1 Like

Hi @dgault,

  • I looked at the lif file metadata opened with BioFormats and the position in written in Units of ‘reference frame’. Is this normal ? Maybe it should be millimeters (there’s a shift of ~ 10 um for each tile so 0.01 mm) ?

  • I also looked at the metadata, directly exported from the leica LAS sofware: TileScan_002.xml (159.4 KB)

At the beginning of this xml file, there’s an interesting line, which may explain the image fliping:

    <Attachment Name="TileScanInfo" Application="LAS AF" FlipX="1" FlipY="1" SwapXY="1" LastUsedTestScanRotationAngle="0" LastUsedTestScanEdgeFactor="1">

Hope that helps

So, I believe bioformats ignores these FlipX, FlipY and SwapXY fields.

We have a widefield system where these fields are set to zero -> the tiling works nicely with bioformats.

We have a confocal system where these settings are set to one -> the tiling fails.

I have a plugin where I modified the bioformats metadata input like that:

// Swap XY
pXmm = omeMeta.getPlanePositionY(image_index, 0).value().doubleValue()*1000;
pYmm = omeMeta.getPlanePositionX(image_index, 0).value().doubleValue()*1000;
// FlipX
dXmm = - omeMeta.getPixelsSizeX(image_index).getNumberValue().doubleValue();
// FlipY
dYmm = - omeMeta.getPixelsSizeY(image_index).getNumberValue().doubleValue();

And with this fix, the images are displayed correctly.

@dgault do you think there’s a possibility to correct this directly within Bioformats ? It should be fixed somewhere here I guess : https://github.com/openmicroscopy/bioformats/blob/7638d6ce946f3f272c5d851913b36609b0aa7aad/components/formats-gpl/src/loci/formats/in/LIFReader.java#L1985

Hi @NicoKiaru, yeah these values are currently not being parsed in the LIFReader, so this is something that should be able to be fixed in Bio-Formats

I have opened a Trello card to track the issue: https://trello.com/c/3eJGDfHh/374-lif-tilescan-flip-and-swap-values-not-being-parsed

1 Like

Hi all,
Rekindling this discusion as unfortunately, this issue is not really solved for us here.

Here is a file Lif.zip (1.3 MB)

Opening the file in LAS and using their Mosaic Merge yields a correct image, notice that no option is checked (No FlipX, FlipY, SwapXY)

Unfortunately when we try to be smart in Fiji (And we need a script for that because Grid/Collection Stitching does not parse the positions properly)

In this script I had to add the FlipXY, and Swap, like @NicoKiaru did, and if I do not I get something incorrect.

So what is happening here? Is it parsing the information in the LIF file, which indeed says that there is no need to Flip? Is it ingoring the information? I am on an updated Fiji.

Second point, I have to correct the coordiantes of the stage by a factor of 1e9 to get microns (Look for corr_factor. How do I avoid doing that my using the calibrated units that BioFormats uses?

Hopefully with the file provided, we can find what the issue is.

Thank you all for your time

#@ File one_lif

exportAndStitch( one_lif, true, false )

void exportAndStitch(File image_file, boolean is_do_stitch, boolean is_compute ) {
	// Get the base directory for the multiposition file
	def base_dir = image_file.getParent()
	// Use it to create  a save directory
	def saveDir = new File(base_dir+File.separator+image_file.getName().replaceFirst("[.][^.]+\$", "")+File.separator)
	// Get the full path of the file. 
	def theFile = image_file.getPath()
	// lif files have their coordiante metadata stored in another unit... 
	// or at least bioformats has issues with them...
	def corr_factor = 1.0

	if(theFile.endsWith(".lif")) corr_factor= 1e6
	def opts = new ImporterOptions()
	//set up import process
	def process = new ImportProcess(opts)
	nseries = process.getSeriesCount()
	//reader belonging to the import process
	def i_reader = process.getReader()
	def impReader = new ImagePlusReader(process)
	// Get all the metadata
	def factory = new ServiceFactory()
	def service = factory.getInstance( OMEXMLService.class )
	// And the retreive function will help us get all the metadata back
	MetadataRetrieve retrieve = service.asRetrieve(i_reader.getMetadataStore())
	double[] posX  = new double[i_reader.getSeriesCount()]
	double[] posY  = new double[i_reader.getSeriesCount()]
	String[] names = new String[i_reader.getSeriesCount()]
	vx = retrieve.getPixelsPhysicalSizeX(0).value()
	all_dims = null
	for (int i=0; i<nseries; i++) {
		// for (int i=0; i<2; i++) {
		posX[i] = (retrieve.getPlanePositionX( i, 0 ).value())
		posY[i] = (retrieve.getPlanePositionY( i, 0 ).value())
		// print("X:"+posX[i])
		// print("Y:"+posY[i])
		// Test Flip X
		posX[i] *= -1
		posY[i] *= -1
		// Test swap
		def temp = posX[i]
		posX[i] = posY[i]
		posY[i] = temp
		names[i] = retrieve.getImageName(i)
		//read and process all images in series
		imps = impReader.openImagePlus()
		//IJ.log("Length "+imps.length);
		bd= imp.getBitDepth()

		// TEST Rotate Right
		//IJ.run(small_ff_imp, "Rotate 90 Degrees Left", "");
		// Save image as TIFF
		IJ.saveAs(imp, "Tiff", saveDir.getAbsolutePath()+File.separator+names[i]+"_"+(i+1)+".tif")
		// Some cleanup	  
		all_dims = imp.getDimensions()
		opts.setSeriesOn(i, false)
	// Get min in X and Y
	List lX = Arrays.asList(ArrayUtils.toObject(posX));
	List lY = Arrays.asList(ArrayUtils.toObject(posY));
	minX = Collections.min(lX)
	minY = Collections.min(lY)
	z = ""
	if(all_dims[3] > 1) {
		dim = 3
		z   = ", 0.0"
	PrintWriter out = new PrintWriter(saveDir.getAbsolutePath()+File.separator+"positions.txt")
	out.println("#Define the number of dimensions we are working on:")
	out.println("dim = "+dim)
	out.println("# Define the image coordinates")

	for (i=0; i<posX.length; i++) {
		if (names[i].contains("Merging")) continue
		fposX = Math.round(((posX[i]-minX)*corr_factor/vx))
		fposY = Math.round(((posY[i]-minY)*corr_factor/vx))		
		out.println(names[i]+"_"+(i+1)+".tif;	;	("+fposX+", "+fposY + z+")")
		//println( ""+posX[i]+", "+posY[i] )
	compute = ""
	if(is_compute) { compute = "compute overlap " }
	if(is_do_stitch) {
		// run the stitcher
		IJ.run("Grid/Collection stitching", "type=[Positions from file] "+
		"order=[Defined by TileConfiguration] "+
		"directory=["+saveDir.getAbsolutePath()+"] "+
		"layout_file=positions.txt "+
		"fusion_method=[Linear Blending] "+
		"regression_threshold=0.30 "+
		"max/avg_displacement_threshold=2.50 "+
		"absolute_displacement_threshold=3.50 "+
		"computation_parameters=[Save memory (but be slower)] "+
		"image_output=[Fuse and display]")
		final_image = IJ.getImage()
		IJ.saveAs(final_image, "Tiff", saveDir.getAbsolutePath()+File.separator+image_file.getName()+"-fused.tif")

import loci.plugins.in.*
import loci.common.services.*
import loci.formats.meta.*
import loci.formats.services.*
import ij.IJ
import org.apache.commons.lang.ArrayUtils

About the 1e9 part: to get a common unit for the voxel size and plane position, you need to specify a Unit before getting the value. So for instance to get values in micrometers (with import ome.units.UNITS):

vx = retrieve.getPixelsPhysicalSizeX(0).value(UNITS.MICROMETER).doubleValue();

and the same for the plane position:

retrieve.getPlanePositionX( i, 0 ).value(UNITS.MICROMETER).doubleValue()
1 Like

Hi @oburri, Bio-Formats is currently not parsing the metadata tags for FLIPX, FLIPY and SWAPXY within the LIF. So are you getting the correct result from your script once you have added the FlipXY and Swap to it?