OMERO custom plate import

Hi,
We have a widefield microscope which is also able to automatically image well plates. In my example, its a 384 well plate. The microscope saves OME-TIFF files with lots of metadata. These images import fine into OMERO, but they are imported as individual images, and the plate-layout is lost.
I have uploaded a minimal example here (3 wells, 1 image per well, 1t, 1c, 1z): https://filesender.switch.ch/filesender/?vid=49a0ab4e-bc56-7be8-f2fc-000016a515ee

What I would like to do is import the images into OMERO as “plates”, as discussed e.g. here

I hence tried to create a .companion.ome OME-XML but I struggle with this step. The companion file I managed to create with the code from here SPW and patterns - #5 by s.besson
is posted below (sorry that its all in one line, I did not yet figure out how to change that).
I changed my image file names to match the example mentioned in SPW and patterns - #2 by olatarkowska

<?xml version='1.0' encoding='UTF-8'?>
<OME UUID="urn:uuid:1a48a880-a3f1-4d94-bd6d-35836c2caefb" xmlns="http://www.openmicroscopy.org/Schemas/OME/2016-06" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openmicroscopy.org/Schemas/OME/2016-06 http://www.openmicroscopy.org/Schemas/OME/2016-06/ome.xsd"><Plate ID="Plate:0" Name="test"><Well Column="0" ID="Well:0" Row="0"><WellSample ID="WellSample:0" Index="0"><ImageRef ID="Image:0" /></WellSample></Well><Well Column="1" ID="Well:1" Row="0"><WellSample ID="WellSample:1" Index="0"><ImageRef ID="Image:1" /></WellSample></Well><Well Column="2" ID="Well:2" Row="0"><WellSample ID="WellSample:2" Index="0"><ImageRef ID="Image:2" /></WellSample></Well><Well Column="3" ID="Well:3" Row="0"><WellSample ID="WellSample:3" Index="0"><ImageRef ID="Image:3" /></WellSample></Well></Plate><Image ID="Image:0" Name="A0"><Pixels DimensionOrder="XYZTC" ID="Pixels:0:0" SizeC="1" SizeT="1" SizeX="1200" SizeY="1200" SizeZ="1" Type="uint16"><Channel Color="-1" ID="Channel:0" Name="channel" SamplesPerPixel="1" /><TiffData FirstC="0" FirstT="0" FirstZ="0"><UUID FileName="A0_F1P0T0.tif">urn:uuid:82430c2d-3a34-4c40-bfca-e88f5debe45d</UUID></TiffData></Pixels></Image><Image ID="Image:1" Name="A1"><Pixels DimensionOrder="XYZTC" ID="Pixels:1:1" SizeC="1" SizeT="1" SizeX="1200" SizeY="1200" SizeZ="1" Type="uint16"><Channel Color="-1" ID="Channel:1" Name="channel" SamplesPerPixel="1" /><TiffData FirstC="0" FirstT="0" FirstZ="0"><UUID FileName="A1_F1P0T0.tif">urn:uuid:07fe1bff-10d0-4355-be39-463055f7d7e7</UUID></TiffData></Pixels></Image><Image ID="Image:2" Name="A2"><Pixels DimensionOrder="XYZTC" ID="Pixels:2:2" SizeC="1" SizeT="1" SizeX="1200" SizeY="1200" SizeZ="1" Type="uint16"><Channel Color="-1" ID="Channel:2" Name="channel" SamplesPerPixel="1" /><TiffData FirstC="0" FirstT="0" FirstZ="0"><UUID FileName="A2_F1P0T0.tif">urn:uuid:faad1281-7d39-4299-b8ad-1695b7c89daa</UUID></TiffData></Pixels></Image><Image ID="Image:3" Name="A3"><Pixels DimensionOrder="XYZTC" ID="Pixels:3:3" SizeC="1" SizeT="1" SizeX="1200" SizeY="1200" SizeZ="1" Type="uint16"><Channel Color="-1" ID="Channel:3" Name="channel" SamplesPerPixel="1" /><TiffData FirstC="0" FirstT="0" FirstZ="0"><UUID FileName="A3_F1P0T0.tif">urn:uuid:2660d2fd-1d43-4dee-b673-6179710e5a61</UUID></TiffData></Pixels></Image></OME>

But when I import as plate in a screen in OMERO.insight, the error is:

org.openmicroscopy.shoola.env.data.ImportException: File Not Valid
	at org.openmicroscopy.shoola.env.data.OmeroImageServiceImpl.importFile(OmeroImageServiceImpl.java:1076)
	at org.openmicroscopy.shoola.env.data.views.calls.ImagesImporter.importFile(ImagesImporter.java:73)
	at org.openmicroscopy.shoola.env.data.views.calls.ImagesImporter.access$000(ImagesImporter.java:48)
	at org.openmicroscopy.shoola.env.data.views.calls.ImagesImporter$1.doCall(ImagesImporter.java:97)
	at org.openmicroscopy.shoola.env.data.views.BatchCall.doStep(BatchCall.java:144)
	at org.openmicroscopy.shoola.util.concur.tasks.CompositeTask.doStep(CompositeTask.java:226)
	at org.openmicroscopy.shoola.env.data.views.CompositeBatchCall.doStep(CompositeBatchCall.java:126)
	at org.openmicroscopy.shoola.util.concur.tasks.ExecCommand.exec(ExecCommand.java:165)
	at org.openmicroscopy.shoola.util.concur.tasks.ExecCommand.run(ExecCommand.java:276)
	at org.openmicroscopy.shoola.util.concur.tasks.AsyncProcessor$Runner.run(AsyncProcessor.java:91)
	at java.lang.Thread.run(Thread.java:748

@s.besson @sukunis ,

It would be amazing if you could help me adapt the code and create a companion file for my use case. Any help is highly appreciated :slight_smile: Thank you!

Hi @CellKai thanks for sharing an example.

I don’t have access to the exact binary data that was aimed to be part of the fileset example. I downloaded the minimal example that you shared above, copy one of the TIFF files to use the filenames from the metadata file

cp 21.05.05_10XAir_test_plate_1_C3_1.tif A0_F1P0T0.tif
cp 21.05.05_10XAir_test_plate_1_C3_1.tif A1_F1P0T0.tif
cp 21.05.05_10XAir_test_plate_1_C3_1.tif A2_F1P0T0.tif
cp 21.05.05_10XAir_test_plate_1_C3_1.tif A3_F1P0T0.tif

and saved the content of your OME-XML into a file called imagesc_52381.companion.ome. With this I was able to import this fileset without issue using OMERO.insight into the OME demo server and this created a plate as I was expecting

Screenshot 2021-05-06 at 16.46.23

So from my side, it looks like your logic works as expected. Back to your import issue, which OMERO.insight version are you using?

2 Likes

Hi @CellKai , the approach with .companion.ome file is clearly the elegant and appropriate way to do it and I hope that ends up working for you. However, if it doesn’t, you could try a much less elegant approach that I use: import the individual images into a Dataset first and then use the Dataset_To_Plate script from the util_scripts to move the images into a plate layout of your choosing.
Cheers,
Damir

2 Likes

thank you both so much for your answers!

@s.besson the OMERO.insight version I tested was 5.4.10. Since we are currently upgrading OMERO, we have a dev server at 5.6, so I could also test OMERO.insight 5.5.17, and it was indeed working like you posted! :star_struck:
To reproduce:

#!/usr/bin/env python3
from ome_model.experimental import Plate, Image, create_companion
import os

os.chdir("/home/kai/Downloads/MDimagexpress-omero/2021-05-05_MORE_test_plate")

rows = 1
columns = 4
plate = Plate("test", rows, columns)
for row in range(rows):
    for column in range(columns):
        well = plate.add_well(row, column)
        basename = "%s%s" % (chr(row + 65), column)
        image = Image(basename, 1200, 1200, 1, 1, 1)
        image.add_tiff('%s_F1P0T0.tif' % basename, c=0, z=0, t=0)
        image.add_channel("channel", -1)
        well.add_wellsample(0, image)
create_companion(plates=[plate], out='plate.companion.ome')

so far, so awesome :smiley:

Some follow up questions:

  • I plan my images to be hyperstacks (still ome.tiff) with several c, z t, rather then individual tiffs
  • I have the well encoded in the filename, e.g. 21.05.05_10XAir_test_plate_1_C3_1.tif
  • I have several hyperstacks per well, also encoded in the filename

Is it possible to adjust the companion file writer accommodate these 3 points?
This might be obvious and already there, it is very possible that I am oblivious and don’t “see the Forrest because of all the trees” :wink:

@dsudar Thank you for your answer as well, I really appreciate the input! I have indeed tested Dataset_To_Plate, and this also worked for me to some degree. Do you happen to know by chance, if/how

  • the plate format can be altered (for example, my plates are 384 wells)
  • the well ID could be extracted from the image file name (e.g. 21.05.05_10XAir_test_plate_1_C3_1.tif)
  • several images per well are also possible? I think I read somewhere that this as not supported, but the info might also be old
1 Like

Hi @CellKai

  • The Dataset_To_Plate script does support multiple Images Per Well: There is an Images Per Well input.
  • There isn’t currently a way to use the image name to determine the position.
  • The script essentially takes all the input images, ordered by name and assigns them into columns, rows and fields, filling every column, row and field fully (sparse layouts not supported). You can choose to either iterate through ‘column’ or ‘row’ as the ‘first axis’ and choose the size of that first axis.

So if you have 384 Images (or a multiple of that for multi-field plate) and these are named all the same except for e.g. C3 and C is the row identifier then you can choose the ‘row’ as the first axis and First Axis Count = 16 then this should work. Although you’ll probably need names like C03 so that this comes before C11 when sorted.

Will.

1 Like

Hi @will-moore , thanks for the reply!

In my use case, there won’t be images for all wells. One reason is for example that the microscope can’t actually reach the outer wells, another reason is that the researchers often only use a couple of wells.

concerning the file naming, I think its quite flexible in terms of introducing patterns.

ultimately, I don’t mind which solution to use, and I am happy to invest time helping to adapt code to make it work, if that’s feasible :slight_smile:
I had a look at omero-scripts/Dataset_To_Plate.py at 68c7505e62115e9c086a8e5a1d3edc1d4aff35f3 · ome/omero-scripts · GitHub
Do you think it would be reasonable to try and add changes so the images would be added to the well position corresponding to a string in their name while leaving the other wells empty? Its hard for me to judge if this is the right way to go, or if going for the companion file might be more suitable to achieve this.

1 Like

Great new @CellKai (and another incentive to get your server upgraded :slight_smile: )

Some follow up questions:

I plan my images to be hyperstacks (still ome.tiff) with several c, z t, rather then individual tiffs

Absolutely. The TiffData element can generally refer to any multi-page TIFF. It might be worth also passing planeCount to set the number of planes that should be read - see idr0092-ostrop-organoid/create_companions.py at 216beab0142aa4e4b44cc02a2d55dda96e0dc9eb · IDR/idr0092-ostrop-organoid · GitHub. For multi-dimensional images, another important metadata element is the DimensionOrder value as it determines how to map the linear planes 0…n read from a multi-page TIFF into (z,c,t) coordinates.

I have the well encoded in the filename, e.g. 21.05.05_10XAir_test_plate_1_C3_1.tif

I suspect in that case you will want to convert the row and column variables into a string representation and use that to determine the filename.

I have several hyperstacks per well, also encoded in the filename

Understood, in that case you will need to create multiple well samples (fields of view) per well and add another loop to the example above i.e

for row in range(rows):
    for column in range(columns):
        well = plate.add_well(row, column)
        for wellsample_index in range(wellsamples):
          ...
          well.add_wellsample(wellsample_index, image)

Feel free to push your conversion code to a GitHub repository and point it to us. We have a few examples of such generation scripts in a few IDR study repositories but more use cases is better for the community.

1 Like

Hi,
I imagine that this might be possible to add an “Image Name” input, in which the user could enter a template string e.g.

21.05.05_10XAir_test_plate_1_{row}{column:02d}_1.tif

Then in the script, you could format this with the row, column (according to the row and column naming convention that the user has chosen) along with the field index.
If you don’t need to use field index, you can omit it as in the above example.

This would give you the image name, and if that image is found in the collection of input images, then you place that image in the appropriate row, column, and field.

E.g. some examples:

>>> "21.05.05_10XAir_test_plate_1_{row}{column:d}_1.tif".format(row="C", column=3, field=0)
>>> "21.05.05_10XAir_test_plate_1_{row}{column:02d}_{field}.tif".format(row="C", column=3, field=0)
'21.05.05_10XAir_test_plate_1_C03_0.tif'

pseudo code…

images_by_name = {}
for image in images:
    images_by_name[image.name] = image

for row in range(row_count):
    for column in range(col_count):
        # if label is 'letter' format to A, B etc, else keep as number
        row_name = format_row(row)
        col_name = format_col(column)
        index=0
        image_name = name_template.format(row=row_name, column=col_name, index=index
        image = images_by_name.get(image_name)
        # if image is not None, add to plate, increment index and check for image at next index, until None

Will

A post was split to a new topic: OMERO custom plate import: well positions

Hello again @will-moore @s.besson ,
thank you once again for the excellent examples and help.

I now continued with the companion file approach:

  • I altered the use case for a bit more complexity and introduced a minimal example with several hyperstacks per well (download, until 08-06-2021)

together with @ehrenfeu, we

This way, the classes can be used in a Fiji jython script. This was useful to us since it allows to use Fiji Bio-Formats to access metadata fields in the original images, like for example the pixel calibration.

This is used in below Fiji jython script which matches the dataset above in this post.

#@ File (style=directory) source

from ome_model.experimental import Plate, Image, create_companion
from loci.formats import ImageReader
from loci.formats import MetadataTools
import os
import re
import glob
import string


def letter_position(letter):
    """convert a letter to its position in the alphabet

    Parameters
    ----------
    letter :string
        the letter you wish to convert to an integer

    Returns
    -------
    integer
        the position of the given letter in the alphabet
    """
    ucase = string.uppercase
    pos = ucase.find(letter.upper()) + 1
    
    return pos


def atoi(text):
    return int(text) if text.isdigit() else text


def natural_keys(text):
    '''
    alist.sort(key=natural_keys) sorts in human order
    http://nedbatchelder.com/blog/200712/human_sorting.html
    (See Toothy's implementation in the comments)
    '''
    return [ atoi(c) for c in re.split(r'(\d+)', text)]


source = str(source).replace("\\", "/") + "/"

# get all file in the working dir -----------------------------------------------------------
os.chdir(source)
all_filenames= glob.glob("*.tif")
all_filenames.sort(key=natural_keys) # is sorted C9 before C10 etc..
print(all_filenames)

# regex for the well position and file name---------------------------------------------------
expression = "(.+)_([A-Z])(\d+)_(\d+).tif"
# match.group(1) = experiment name
# match.group(2) = row # transform to row index somehow, A = 1, B = 2 etc
# match.group(3) = column
# match.group(4) = nothing really, its the index of total images
plate_row = []
plate_column = []
filename_index = []
for filename in all_filenames:
    match = re.search(expression, filename)
    if match:
        plate_row.append( letter_position( match.group(2) ) -1 ) # python starts from 0
        plate_column.append( int( match.group(3) ) - 1 )
        plate_name = match.group(1)

### get the metadata from the first image ------------------------------------------------------
reader = ImageReader()
omeMeta = MetadataTools.createOMEXMLMetadata()
reader.setMetadataStore(omeMeta)
reader.setId( source + all_filenames[0] )

physSizeX = omeMeta.getPixelsPhysicalSizeX(0)
physSizeY = omeMeta.getPixelsPhysicalSizeY(0)
physSizeZ = omeMeta.getPixelsPhysicalSizeZ(0)
pixel_type = str( omeMeta.getPixelsType(0) )
image_calibration = [physSizeX.value(), physSizeY.value(), physSizeZ.value()]
calibration_unit = physSizeX.unit().getSymbol()

dimension_order = str( omeMeta.getPixelsDimensionOrder(0) )
frame_size_x = reader.getSizeX() # returns an integer
frame_size_y = reader.getSizeY()
frame_size_z = reader.getSizeZ()
frame_size_c = reader.getSizeC()
frame_size_t = reader.getSizeT()

rows = omeMeta.getPlateRows(0).getNumberValue() # returns an integer
columns = omeMeta.getPlateColumns(0).getNumberValue()

# write the companion file --------------------------------------------------------------------
plate = Plate(plate_name, rows, columns)
counter = 0

print("plate_rows with images ", plate_row)
print("plate_columns with images ", plate_column)

for row in range(rows):
     for column in range(columns):
        well = plate.add_well(row, column)
        well_samples = []
        if counter < len(all_filenames) and row == plate_row[counter] and column == plate_column[counter]:
            print("found images for well: ", plate_row[counter], plate_column[counter])
            well_samples.append(all_filenames[counter])
            for i in range( len(all_filenames) - ( counter +1 ) ):
                current_well = [ plate_row[counter], plate_column[counter] ]
                next_well = [ plate_row[counter + 1], plate_column[counter + 1] ]
                if next_well == current_well:
                    counter += 1
                    well_samples.append(all_filenames[counter])

            for wellsample_index, wellsample_filename in enumerate(well_samples):
                print("adding: ", wellsample_filename)
                print("at position: ", wellsample_index)
                image = Image(
                    wellsample_filename,
                    frame_size_x, frame_size_y, frame_size_z, frame_size_c,frame_size_t,
                    [],
                    dimension_order,
                    pixel_type,
                    image_calibration[0], image_calibration[1], image_calibration[2],
                    calibration_unit, calibration_unit, calibration_unit
                    )
                image.add_tiff(str(wellsample_filename), c=0, z=0, t=0)
                # image.add_channel("channel", -1) # add channel-specific metadata
                well.add_wellsample(wellsample_index, image)
            
            counter += 1
print("all images added: ", counter == len(all_filenames))
create_companion(plates=[plate], out= plate_name + '.companion.ome')

This is a really great approach and we are very happy with it!

Since my original raw data contains plenty of metadata in the OME-XML, I was wondering:
Is it possible to keep this per-image-metadata somehow, maybe “merge” it with the companion file?

I noticed that after import, “only” the metadata from the companion file is shown in OMERO, but not the metadata inside the images ome-xml.

Thanks again for your help!

2 Likes

In case you want to give it a try, here’s the JAR for the current HEAD in “our” populate-missing-fields branch (simply to be dropped into Fiji’s jars/ folder):

jython-ome-model-6.2.3-SNAPSHOT.jar (7.9 KB)

1 Like

@CellKai @ehrenfeu very nice! Very happy the original API was easy enough to extend and many thanks for sharing the code and the distribution.

Looking more specifically at Add physical sizes and units to Image class · imcf/ome-model@d9a55c7 · GitHub, I completely support the additions of extra metadata. Another similar example recently mentioned was the WellSample.PositionX discussed in OMERO custom plate import: well positions.

One consideration would be to backport this new feature to the upstream repository so that it becomes part of the upstream releases. I have a few API considerations that would probable best be discussed as a GitHub issue. Let me know if that’s something that would be useful or you would consider.

Should we just turn it into a PR and discuss there?

That was our goal anyway, but we wanted to get some feedback from you guys here first, as this is where the discussion took off :slight_smile:

@ehrenfeu: that’s great. Yes let’s turn this into a PR and have this discussion as part of the review.

Voilà: Add physical sizes and units to Image class by ehrenfeu · Pull Request #143 · ome/ome-model · GitHub

I’d have a follow-up question concerning the companion file approach:

I noticed that after import, “only” the metadata from the companion file is shown in OMERO, but not the metadata from the original raw images.
I my case, the raw image already contains metadata fields that I would like to keep

Is it possible to keep this per-image-metadata somehow, maybe “merge” it with the companion file?

Thanks again for your help!

@CellKai unfortunately not, the OME-TIFF specification does not have the ability to distribute the metadata across files. As you noticed, only the metadata defined in the master file (the companion file in your case) will be read and parsed by the library.
To preserve image-level metadata in your case, you will need to extract it from the individual files and store it in the appropriate location of the top-level metadata file.