comparison stack_buildXml.groovy @ 0:e1cba36becb2 draft

planemo upload for repository https://github.com/lldelisle/tools-lldelisle/tree/master/tools/incucyte_stack_and_upload_omero commit 4ac9b1d66ba6857357867c8eccb6c9d1ad603364
author lldelisle
date Tue, 19 Dec 2023 15:02:41 +0000
parents
children 3c942429f610
comparison
equal deleted inserted replaced
-1:000000000000 0:e1cba36becb2
1 /*
2 ****************************************************
3 * Relative to the generation of the .companion.ome *
4 ****************************************************
5 * #%L
6 * BSD implementations of Bio-Formats readers and writers
7 * %%
8 * The functions buildXML, makeImage, makePlate, postProcess and asString has been modified and adapted from
9 * https://github.com/ome/bioformats/blob/master/components/formats-bsd/test/loci/formats/utests/SPWModelMock.java
10 *
11 * Copyright (C) 2005 - 2015 Open Microscopy Environment:
12 * - Board of Regents of the University of Wisconsin-Madison
13 * - Glencoe Software, Inc.
14 * - University of Dundee
15 *
16 * @author Chris Allan <callan at blackcat dot ca>
17 * %%
18 *
19 ****************************************************
20 * Relative to the rest of the script *
21 ****************************************************
22 *
23 * * = AUTHOR INFORMATION =
24 * Code written by Rémy Dornier, EPFL - SV - PTECH - BIOP
25 * and Romain Guiet, EPFL - SV - PTECH - BIOP
26 * and Lucille Delisle, EPFL - SV - UPDUB
27 * and Pierre Osteil, EPFL - SV - UPDUB
28 *
29 * Last modification: 2023-12-19
30 *
31 * = COPYRIGHT =
32 * © All rights reserved. ECOLE POLYTECHNIQUE FEDERALE DE LAUSANNE, Switzerland, BioImaging And Optics Platform (BIOP), 2023
33 *
34 * Licensed under the BSD-3-Clause License:
35 * Redistribution and use in source and binary forms, with or without modification, are permitted provided
36 * that the following conditions are met:
37 * 1. Redistributions of source code must retain the above copyright notice,
38 * this list of conditions and the following disclaimer.
39 * 2. Redistributions in binary form must reproduce the above copyright notice,
40 * this list of conditions and the following disclaimer
41 * in the documentation and/or other materials provided with the distribution.
42 * 3. Neither the name of the copyright holder nor the names of its contributors
43 * may be used to endorse or promote products
44 * derived from this software without specific prior written permission.
45 *
46 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
47 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
48 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
49 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
50 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
51 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
52 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
53 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
54 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
55 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
56 * POSSIBILITY OF SUCH DAMAGE.
57 *
58 */
59
60 /**
61 *
62 * The purpose of this script is to combine a series of time-lapse images into
63 * one file per well/field with possibly multiple channels and multiple time points
64 * and in addition create a .companion.ome file to create an OMERO plate object,
65 * with a single image per well/field. This .companion.ome file can be directly uploaded on OMERO via
66 * OMERO.insight software or CLI, with screen import option.
67 *
68 * To make the script run
69 * 1. Create a parent folder (base_dir) and a output folder (output_dir)
70 * 2. Create a dir *Phase, *Green, *Red with the corresponding channels
71 * 3. The image names must contains a prefix followed by '_', the name of the well (no 0-pad) followed by '_', followed by the field id followed by '_', the date of the acquisition in YYYYyMMmDDdHHhMMm and the extension '.tif'
72 * 4. The images can be either regular tif or the raw tif from Incucyte which contains multiple series.
73 * 5. You must provide the path of the Incucyte XML file to populate key values
74 *
75 * The expected outputs are:
76 * 1. In the output_dir one tiff per well/field (multi-T and potentially multi-C)
77 * 2. In the output_dir a .companion.ome
78 */
79
80 #@ File(style="directory", label="Directory with up to 3 subdirectories ending by Green, Phase and/or Red") base_dir
81 #@ File(label="Incucyte XML File (plateMap)") incucyteXMLFile
82 #@ File(style="directory", label="Output directory (must exist)") output_dir
83 #@ String(label="Final XML file name", value="Test") xmlName
84 #@ String(label="Number of well in plate", choices={"96", "384"}, value="96") nWells
85 #@ Integer(label="Maximum number of images per well", value=1, min=1) n_images_per_well
86 #@ String(label="Objective", choices={"4x","10x","20x"}) objectiveChoice
87 #@ String(label="Plate name", value="Experiment:0") plateName
88 #@ String(label="common Key Values formatted as key1=value1;key2=value2", value="") commonKeyValues
89 #@ Boolean(label="Ignore Compound concentration from plateMap", value=true) ignoreConcentration
90 #@ Boolean(label="Ignore Cell passage number from plateMap", value=true) ignorePassage
91 #@ Boolean(label="Ignore Cell seeding concentration from plateMap", value=true) ignoreSeeding
92
93
94 /**
95 * *****************************************************************************************************************
96 * ********************************************* Final Variables **************************************************
97 * ********************************************* DO NOT MODIFY ****************************************************
98 * ****************************************************************************************************************
99 */
100
101 /** objectives and respective pixel sizes */
102 objective = 0
103 objectives = new String[]{"4x", "10x", "20x"}
104 pixelSizes = new double[]{2.82, 1.24, 0.62}
105
106 /** pattern for date */
107 REGEX_FOR_DATE = ".*_([0-9]{4})y([0-9]{2})m([0-9]{2})d_([0-9]{2})h([0-9]{2})m.tif"
108
109 ALTERNATIVE_REGEX_FOR_DATE = ".*_([0-9]{2})d([0-9]{2})h([0-9]{2})m.tif"
110
111 /** Image properties keys */
112 DIMENSION_ORDER = "dimension_order"
113 FILE_NAME = "file_name"
114 IMG_POS_IN_WELL = "img_pos_in_well"
115 FIRST_ACQUISITION_DATE = "acquisition_date"
116 FIRST_ACQUISITION_TIME = "acquisition_time"
117 RELATIVE_ACQUISITION_HOUR = "relative_acquisition_hour"
118
119 /** global variable for index to letter conversion */
120 LETTERS = new String("ABCDEFGHIJKLMNOP")
121
122 // Version number = date of last modif
123 VERSION = "20231219"
124
125 /** Key-Value pairs namespace */
126 GENERAL_ANNOTATION_NAMESPACE = "openmicroscopy.org/omero/client/mapAnnotation"
127 annotations = new StructuredAnnotations()
128
129 /** Plate details and conventions */
130 PLATE_ID = "Plate:0"
131 PLATE_NAME = plateName
132
133 if (nWells == "96") {
134 nRows = 8
135 nCols = 12
136 } else if (nWells == "384") {
137 nRows = 16
138 nCols = 24
139 }
140
141 WELL_ROWS = new PositiveInteger(nRows)
142 WELL_COLS = new PositiveInteger(nCols)
143 WELL_ROW = NamingConvention.LETTER
144 WELL_COL = NamingConvention.NUMBER
145
146 /** XML namespace. */
147 XML_NS = "http://www.openmicroscopy.org/Schemas/OME/2010-06"
148
149 /** XSI namespace. */
150 XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
151
152 /** XML schema location. */
153 SCHEMA_LOCATION = "http://www.openmicroscopy.org/Schemas/OME/2010-06/ome.xsd"
154
155
156 /**
157 * *****************************************************************************************************************
158 * **************************************** Beginning of the script ***********************************************
159 * ****************************************************************************************************************
160 */
161
162 try {
163
164 println "Beginning of the script"
165
166 /**
167 * Prepare list of wells name
168 */
169 String[] well = []
170
171 well = [(0..(nRows - 1)),(0..(nCols - 1))].combinations().collect{ r,c -> LETTERS.substring(r, r + 1) +""+ (c+ 1).toString() }
172
173 IJ.run("Close All", "")
174
175 // loop for all the wells
176
177 // Store all merged ImagePlus into a HashMap where
178 // keys are well name (A1, A10)
179 // values are a list of ImagePlus corresponding to different field of view
180 Map<String, List<ImagePlus>> wellSamplesMap = new HashMap<>()
181
182 well.each{ input ->
183 IJ.run("Close All", "")
184
185 List<ImagePlus> final_imp_list = process_well(base_dir, input, n_images_per_well) //, perform_bc, mediaChangeTime )
186 if (!final_imp_list.isEmpty()) {
187 wellSamplesMap.put(input, final_imp_list)
188 for(ImagePlus final_imp : final_imp_list){
189 final_imp.setTitle(input+"_"+final_imp.getProperty(IMG_POS_IN_WELL))
190 //final_imp.show()
191
192 def fs = new FileSaver(final_imp)
193 File output_path = new File (output_dir ,final_imp.getTitle()+"_merge.tif" )
194 fs.saveAsTiff(output_path.toString() )
195 final_imp.setProperty(FILE_NAME, output_path.getName())
196
197 IJ.run("Close All", "")
198 }
199 } else {
200 println "No match for " + input
201 }
202 }
203
204
205 // get folder and xml file path
206 output_dir_abs = output_dir.getAbsolutePath()
207 incucyteXMLFilePath = incucyteXMLFile.getAbsolutePath()
208
209 if (! new File(incucyteXMLFilePath).exists()) {
210 println "The incucyte file does not exists"
211 return
212 }
213
214 // select the right objective
215 switch (objectiveChoice){
216 case "4x":
217 objective = 0
218 break
219 case "10x":
220 objective = 1
221 break
222 case "20x":
223 objective = 2
224 break
225 }
226
227 // get plate scheme as key-values
228 Map<String, List<MapPair>> keyValuesPerWell = parseIncucyteXML(incucyteXMLFilePath, ignoreConcentration, ignorePassage, ignoreSeeding)
229
230 // get global key-values
231 List<MapPair> globalKeyValues = getGlobalKeyValues(objective, commonKeyValues)
232 double pixelSize = pixelSizes[objective]
233
234 // generate OME-XML metadata file
235 OME ome = buildXMLFile(wellSamplesMap, keyValuesPerWell, globalKeyValues, pixelSize)
236
237 // create XML document
238 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance()
239 DocumentBuilder parser = factory.newDocumentBuilder()
240 Document document = parser.newDocument()
241
242 // Produce a valid OME DOM element hierarchy
243 Element root = ome.asXMLElement(document)
244 postProcess(root, document)
245
246 // Produce string XML
247 try(OutputStream outputStream = new FileOutputStream(output_dir_abs + File.separator + xmlName + ".companion.ome")){
248 outputStream.write(asString(document).getBytes())
249 } catch(Exception e){
250 e.printStackTrace()
251 }
252 println "End of the script"
253
254 } catch (Throwable e) {
255 println("Something went wrong: " + e)
256 e.printStackTrace()
257 throw e
258
259 if (GraphicsEnvironment.isHeadless()){
260 // Force to give exit signal of error
261 System.exit(1)
262 }
263
264 }
265
266 return
267
268 /**
269 * ****************************************************************************************************************
270 * ******************************************* End of the script **************************************************
271 *
272 * ****************************************************************************************************************
273 *
274 * *********************************** Helpers and processing methods *********************************************
275 * ***************************************************************************************************************
276 */
277
278 def process_well(baseDir, input_wellId, n_image_per_well){ //, perform_bc, mediaChangeTime){
279 File bf_dir = baseDir.listFiles().find{ it =~ /.*Phase.*/}
280 File green_dir = baseDir.listFiles().find{ it =~ /.*Green.*/}
281 File red_dir = baseDir.listFiles().find{ it =~ /.*Red.*/}
282 //if (verbose) println bf_dir
283 //if (verbose) println green_dir
284
285 // The images are stored in a TreeMap where
286 // keys are wellSampleId = field identifier
287 // values are a TreeMap that we call channelMap where:
288 // keys are colors (Green, Grays, Red)
289 // values are an ImagePlus (T-stack)
290 Map<Integer, Map<String, ImagePlus>> sampleToChannelMap = new TreeMap<>()
291
292 List<File> folder_list = [bf_dir, green_dir, red_dir]
293 List<String> channels_list = ["Grays", "Green", "Red"]
294
295 // loop over the field and open images
296 for(int wellSampleId = 1; wellSampleId <= n_image_per_well; wellSampleId++) {
297 // nT is the number of time-points for the well input_wellId
298 int nT = 0
299 String first_channel = ""
300 String first_acq_date = ""
301 String first_acq_time = ""
302 String rel_acq_hour = ""
303
304 // Initiate a channel map for the wellSampleId
305 Map<String, ImagePlus> channelMap = new TreeMap<>()
306
307 // Checking if there are images in the corresponding dir
308 // which corresponds to the input_wellId
309 // and to the wellSampleId
310 // The image name should be:
311 // Prefix + "_" + input_wellId + "_" + wellSampleId + "_" + year (4 digits) + "y" + month (2 digits) + "m" + day + "d_" + hour + "h" + minute + "m.tif"
312 FileFilter fileFilter = new WildcardFileFilter("*_" + input_wellId + "_" + wellSampleId + "_*")
313 for(int i = 0; i < folder_list.size(); i++){
314 if (folder_list.get(i) != null) {
315 File[] files_matching = folder_list.get(i).listFiles(fileFilter as FileFilter).sort()
316 if (files_matching.size() != 0) {
317 // In order to deal with raw images from Incucyte which are
318 // Multi series images
319 // We open the first image
320 ImagePlus first_imp = Opener.openUsingBioFormats(files_matching[0].getAbsolutePath())
321 // We check if we can read the infos
322 first_image_infos = Opener.getTiffFileInfo(files_matching[0].getAbsolutePath())
323 // We define the imageplus object
324 ImagePlus single_channel_imp
325 if (first_image_infos == null) {
326 // They are raw from incucyte
327 // We need to open images one by one and add them to the stack
328 ImageStack stack = new ImageStack(first_imp.width, first_imp.height);
329 files_matching.each{
330 ImagePlus single_imp = (new Opener()).openUsingBioFormats(it.getAbsolutePath())
331 String new_title = single_imp.getTitle().split(" - ")[0]
332 stack.addSlice(new_title, single_imp.getProcessor())
333 }
334 single_channel_imp = new ImagePlus(FilenameUtils.getBaseName(folder_list.get(i).getAbsolutePath()), stack);
335 } else {
336 // They are regular tif file
337 // We can use FolderOpener to create the stack at once
338 single_channel_imp = FolderOpener.open(folder_list.get(i).getAbsolutePath(), " filter=_"+ input_wellId + "_"+wellSampleId+"_")
339 }
340 // Phase are 8-bit and need to be changed to 16-bit
341 // Other are already 16-bit but it does not hurt
342 IJ.run(single_channel_imp, "16-bit", "")
343
344 // check frame size
345 if (nT == 0) {
346 // This is the first channel with images
347 nT = single_channel_imp.getNSlices()
348 first_channel = channels_list.get(i)
349 // Process all dates:
350 Pattern date_pattern = Pattern.compile(REGEX_FOR_DATE)
351 ImageStack stack = single_channel_imp.getStack()
352 // Go to the first time (which is slice)
353 single_channel_imp.setSlice(1)
354 int currentSlice = single_channel_imp.getCurrentSlice()
355 String label = stack.getSliceLabel(currentSlice)
356 LocalDateTime dateTime_ref = getDate(label, date_pattern)
357 if (dateTime_ref == null) {
358 date_pattern = Pattern.compile(ALTERNATIVE_REGEX_FOR_DATE)
359 dateTime_ref = getDate(label, date_pattern)
360 }
361 if (dateTime_ref != null) {
362 first_acq_date = dateTime_ref.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))
363 first_acq_time = dateTime_ref.format(DateTimeFormatter.ofPattern("HH:mm:ss"))
364 for (int ti = 2; ti<= nT; ti++) {
365 // Process each frame starting at 2
366 single_channel_imp.setSlice(ti)
367 int hours = getHoursFromImp(single_channel_imp, stack, dateTime_ref, date_pattern)
368 if (rel_acq_hour == "") {
369 rel_acq_hour = "" + hours
370 } else {
371 rel_acq_hour += "," + hours
372 }
373 }
374 } else {
375 first_acq_date = "NA"
376 first_acq_time = "NA"
377 }
378 } else {
379 assert single_channel_imp.getNSlices() == nT : "The number of "+channels_list.get(i)+" images for well "+input_wellId+" and field " + wellSampleId + " does not match the number of images in " + first_channel + "."
380 }
381 // Set acquisition properties
382 single_channel_imp.setProperty(FIRST_ACQUISITION_DATE, first_acq_date)
383 single_channel_imp.setProperty(FIRST_ACQUISITION_TIME, first_acq_time)
384 single_channel_imp.setProperty(RELATIVE_ACQUISITION_HOUR, rel_acq_hour)
385 println single_channel_imp.getProperty(FIRST_ACQUISITION_DATE)
386 println single_channel_imp.getProperty(FIRST_ACQUISITION_TIME)
387 // add the image stack to the channel map for the corresponding color
388 channelMap.put(channels_list.get(i), single_channel_imp)
389 }
390 }
391 }
392 if (nT != 0) {
393 // add the channelmap to the sampleToChannelMap using the wellSampleId as key
394 sampleToChannelMap.put(wellSampleId, channelMap)
395 }
396 }
397
398 ArrayList<ImagePlus> final_imp_list = []
399
400 // Now loop over the wellSampleId which have images
401 for(Integer wellSampleId : sampleToChannelMap.keySet()){
402 // get the channel map
403 Map<String, ImagePlus> channelsMap = sampleToChannelMap.get(wellSampleId)
404 ArrayList<String> channels = []
405 ArrayList<ImagePlus> current_images = []
406
407 for(String channel : channelsMap.keySet()){
408 channels.add(channel)
409 current_images.add(channelsMap.get(channel))
410 }
411 // Get number of time:
412 int nT = current_images[0].nSlices
413
414 // Merge all
415 ImagePlus merged_imps = Concatenator.run(current_images as ImagePlus[])
416 // Re-order to make a multi-channel, time-lapse image
417 ImagePlus final_imp
418 if (channels.size() == 1 && nT == 1) {
419 final_imp = merged_imps
420 } else {
421 final_imp = HyperStackConverter.toHyperStack(merged_imps, channels.size() , 1, nT, "xytcz", "Color")
422 }
423 // add properties to the image
424 final_imp.setProperty(DIMENSION_ORDER, DimensionOrder.XYCZT)
425 final_imp.setProperty(IMG_POS_IN_WELL, wellSampleId)
426 final_imp.setProperty(FIRST_ACQUISITION_DATE, current_images[0].getProperty(FIRST_ACQUISITION_DATE))
427 final_imp.setProperty(FIRST_ACQUISITION_TIME, current_images[0].getProperty(FIRST_ACQUISITION_TIME))
428 final_imp.setProperty(RELATIVE_ACQUISITION_HOUR, current_images[0].getProperty(RELATIVE_ACQUISITION_HOUR))
429
430 // set LUTs
431 (0..channels.size()-1).each{
432 final_imp.setC(it + 1)
433 IJ.run(final_imp, channels[it], "")
434 final_imp.resetDisplayRange()
435 }
436
437 final_imp_list.add(final_imp)
438 }
439
440 return final_imp_list
441 }
442
443
444 /**
445 * create the full XML metadata (plate, images, channels, annotations....)
446 *
447 * @param imagesName
448 * @param keyValuesPerWell
449 * @param globalKeyValues
450 * @param pixelSize
451 * @return OME-XML metadata instance
452 */
453 def buildXMLFile(Map<String,List<ImagePlus>> wellToImagesMap, Map<String, List<MapPair>> keyValuesPerWell, List<MapPair> globalKeyValues, double pixelSize) {
454 // create a new OME-XML metadata instance
455 OME ome = new OME()
456
457 Map<String, Integer> imgInWellPosToListMap = new HashMap<>()
458 int imgCmp = 0
459 for (String wellId: wellToImagesMap.keySet()) {
460 // get well position from image name
461 List<ImagePlus> imagesWithinWell = wellToImagesMap.get(wellId)
462
463 for (ImagePlus image : imagesWithinWell) {
464 // get KVP corresponding to the current well
465 // Initiate a list of keyValues for the wellId
466 // (or use the existing one)
467 List<MapPair> keyValues = []
468 if(keyValuesPerWell.containsKey(wellId))
469 keyValues = keyValuesPerWell.get(wellId)
470 keyValues.addAll(globalKeyValues)
471
472 // create an Image node in the ome-xml
473 imgInWellPosToListMap.put(wellId+ "_" +image.getProperty(IMG_POS_IN_WELL),imgCmp)
474 ome.addImage(makeImage(imgCmp++, image, keyValues, pixelSize))
475 }
476 }
477
478 // create Plate node
479 ome.addPlate(makePlate(wellToImagesMap, imgInWellPosToListMap, pixelSize, ome))
480
481 // add annotation nodes
482 ome.setStructuredAnnotations(annotations)
483
484 return ome
485 }
486
487 /**
488 * create an image xml-element, populated with annotations, channel, pixels and path elements
489 *
490 * @param index image ID
491 * @param imageName
492 * @param keyValues
493 * @param pixelSize
494 * @return an image xml-element
495 */
496 def makeImage(int index, ImagePlus imagePlus, List<MapPair> keyValues, double pixelSize) {
497 // Create <Image/>
498 Image image = new Image()
499 image.setID("Image:" + index)
500 // The image name is the name of the file without extension
501 image.setName(((String)imagePlus.getProperty(FILE_NAME)).split("\\.")[0])
502 // Set the acquisitionDate:
503 if (imagePlus.getProperty(FIRST_ACQUISITION_DATE) != "NA") {
504 image.setAcquisitionDate(new Timestamp(imagePlus.getProperty(FIRST_ACQUISITION_DATE) + "T" + imagePlus.getProperty(FIRST_ACQUISITION_TIME)))
505 // Also add it to the key values:
506 keyValues.add(new MapPair("acquisition.day", (String)imagePlus.getProperty(FIRST_ACQUISITION_DATE)))
507 keyValues.add(new MapPair("acquisition.time", (String)imagePlus.getProperty(FIRST_ACQUISITION_TIME)))
508 keyValues.add(new MapPair("relative.acquisition.hours", (String)imagePlus.getProperty(RELATIVE_ACQUISITION_HOUR)))
509 }
510 // Create <MapAnnotations/>
511 MapAnnotation mapAnnotation = new MapAnnotation()
512 mapAnnotation.setID("ImageKeyValueAnnotation:" + index)
513 mapAnnotation.setNamespace(GENERAL_ANNOTATION_NAMESPACE)
514 mapAnnotation.setValue(keyValues)
515 annotations.addMapAnnotation(mapAnnotation); // add the KeyValues to the general structured annotation element
516 image.linkAnnotation(mapAnnotation)
517
518 // Create <Pixels/>
519 Pixels pixels = new Pixels()
520 pixels.setID("Pixels:" + index)
521 pixels.setSizeX(new PositiveInteger(imagePlus.getWidth()))
522 pixels.setSizeY(new PositiveInteger(imagePlus.getHeight()))
523 pixels.setSizeZ(new PositiveInteger(imagePlus.getNSlices()))
524 pixels.setSizeC(new PositiveInteger(imagePlus.getNChannels()))
525 pixels.setSizeT(new PositiveInteger(imagePlus.getNFrames()))
526 pixels.setDimensionOrder((DimensionOrder) imagePlus.getProperty(DIMENSION_ORDER))
527 pixels.setType(getPixelType(imagePlus))
528 pixels.setPhysicalSizeX(new Length(pixelSize, UNITS.MICROMETER))
529 pixels.setPhysicalSizeY(new Length(pixelSize, UNITS.MICROMETER))
530
531 // Create <TiffData/> under <Pixels/>
532 TiffData tiffData = new TiffData()
533 tiffData.setFirstC(new NonNegativeInteger(0))
534 tiffData.setFirstT(new NonNegativeInteger(0))
535 tiffData.setFirstZ(new NonNegativeInteger(0))
536 tiffData.setPlaneCount(new NonNegativeInteger(imagePlus.getNSlices()*imagePlus.getNChannels()*imagePlus.getNFrames()))
537
538 // Create <UUID/> under <TiffData/>
539 UUID uuid = new UUID()
540 uuid.setFileName((String)imagePlus.getProperty(FILE_NAME))
541 uuid.setValue(java.util.UUID.randomUUID().toString())
542 tiffData.setUUID(uuid)
543
544 // Put <TiffData/> under <Pixels/>
545 pixels.addTiffData(tiffData)
546
547 // Create <Channel/> under <Pixels/>
548 LUT[] luts = imagePlus.getLuts()
549 for (int i = 0; i < luts.length; i++) {
550 Channel channel = new Channel()
551 channel.setID("Channel:" + i)
552 channel.setColor(new Color(luts[i].getRed(255),luts[i].getGreen(255), luts[i].getBlue(255),255))
553 pixels.addChannel(channel)
554 }
555
556 // Put <Pixels/> under <Image/>
557 image.setPixels(pixels)
558
559 return image
560 }
561
562
563 /**
564 * get pixel type based on the imagePlus type
565 * @param imp
566 * @return pixel type
567 */
568 def getPixelType(ImagePlus imp){
569 switch (imp.getType()) {
570 case ImagePlus.GRAY8:
571 return PixelType.UINT8
572 case ImagePlus.GRAY16:
573 return PixelType.UINT16
574 case ImagePlus.GRAY32:
575 return PixelType.FLOAT
576 default:
577 return PixelType.FLOAT
578 }
579 }
580
581 /**
582 * create a Plate xml-element, populated with wells and their attributes
583 * @param imagesName
584 * @return Plate xml-element
585 */
586 def makePlate(Map<String, List<ImagePlus>> wellToImagesMap, Map<String, Integer> imgPosInListMap, double pixelSize, OME ome) {
587 // Create <Plate/>
588 Plate plate = new Plate()
589 plate.setName(PLATE_NAME)
590 plate.setID(PLATE_ID)
591 plate.setRows(WELL_ROWS)
592 plate.setColumns(WELL_COLS)
593 plate.setRowNamingConvention(WELL_ROW)
594 plate.setColumnNamingConvention(WELL_COL)
595
596 // for each image (one image per well)
597 for (String wellId: wellToImagesMap.keySet()) {
598 // get well position from image name
599 List<ImagePlus> imagesWithinWell = wellToImagesMap.get(wellId)
600
601 // get well position from image name
602 int row = convertLetterToNumber(wellId.substring(0, 1))
603 int col = Integer.parseInt(wellId.substring(1)) - 1
604
605 // row and col should correspond to a real well
606 if(row >= 0 && col >= 0 && col < 12) {
607 // Create <Well/> under <Plate/>
608 Well well = new Well()
609 well.setID(String.format("Well:%d_%d", row, col))
610 well.setRow(new NonNegativeInteger(row))
611 well.setColumn(new NonNegativeInteger(col))
612
613 for (ImagePlus imagePlus : imagesWithinWell) {
614 int wellSampleIndex = imgPosInListMap.get(wellId + "_" + imagePlus.getProperty(IMG_POS_IN_WELL))
615
616 // Create <WellSample/> under <Well/>
617 WellSample sample = new WellSample()
618 sample.setID(String.format("WellSample:%d", wellSampleIndex))
619 sample.setIndex(new NonNegativeInteger(wellSampleIndex))
620 if (imagePlus.getCalibration() != null) {
621 sample.setPositionX(new Length(imagePlus.getCalibration().xOrigin * pixelSize, UNITS.MICROMETER))
622 sample.setPositionY(new Length(imagePlus.getCalibration().yOrigin * pixelSize, UNITS.MICROMETER))
623 }
624 sample.linkImage(ome.getImage(wellSampleIndex))
625
626 // Put <WellSample/> under <Well/>
627 well.addWellSample(sample)
628 }
629
630 // Put <Well/> under <Plate/>
631 plate.addWell(well)
632 }
633 }
634 return plate
635 }
636
637 /**
638 * convert the XML metadata document into string
639 *
640 * @param document
641 * @return
642 * @throws TransformerException
643 * @throws UnsupportedEncodingException
644 */
645 def asString(Document document) throws TransformerException, UnsupportedEncodingException {
646 TransformerFactory transformerFactory = TransformerFactory.newInstance()
647 Transformer transformer = transformerFactory.newTransformer()
648
649 //Setup indenting to "pretty print"
650 transformer.setOutputProperty(OutputKeys.INDENT, "yes")
651 transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4")
652
653 Source source = new DOMSource(document)
654 ByteArrayOutputStream os = new ByteArrayOutputStream()
655 Result result = new StreamResult(new OutputStreamWriter(os, "utf-8"))
656 transformer.transform(source, result)
657
658 return os.toString()
659 }
660
661 /**
662 * add document header
663 *
664 * @param root
665 * @param document
666 */
667 def postProcess(Element root, Document document) {
668 root.setAttribute("xmlns", XML_NS)
669 root.setAttribute("xmlns:xsi", XSI_NS)
670 root.setAttribute("xsi:schemaLocation", XML_NS + " " + SCHEMA_LOCATION)
671 root.setAttribute("UUID", java.util.UUID.randomUUID().toString())
672 document.appendChild(root)
673 }
674
675
676 /**
677 * read Incucyte plate-scheme XML file, extract attributes per well and convert attributes to OMERO-compatible
678 * keys-value pairs XML elements.
679 *
680 * @param path Incucyte plate-scheme XML file path
681 * @return Map of OME-XML compatible key-values per well
682 */
683 def parseIncucyteXML(String path, Boolean ignoreConcentration, Boolean ignorePassage, Boolean ignoreSeeding) {
684 Map<String, List<MapPair>> keyValuesPerWell = new HashMap<>()
685
686 final String rowAttribute = "row"
687 final String columnAttribute = "col"
688
689 final String wellNode = "well"
690 final String itemsNode = "items"
691 final String wellItemNode = "wellItem"
692 final String referenceItemNode = "referenceItem"
693
694 // There are 3 types of referenceItem: Compound, CellType, GrowthCondition
695 //
696 // For the Compound, each well can have a concentration and a concentrationUnits
697 // the referenceItem has a displayName
698 // The key should be: displayName (concentrationUnits)
699 // The value should be: concentration
700 // However, if ignoreConcentration is set to true:
701 // The key should be: displayName (NA)
702 // The value should be: 1
703 //
704 // For the CellType, each well can have a passage, a seedingDensity and a seedingDensityUnits
705 // the referenceItem has a displayName
706 // The passage key should be: displayName_passage
707 // The value should be: passage
708 // However, if ignorePassage is set to true no key value should be stored for this
709 // Then:
710 // The seeding key should be: displayName_seedingDensity (seedingDensityUnits)
711 // The value should be: seedingDensity
712 // However, if ignoreSeeding is set to true:
713 // The key should be: displayName
714 // The value should be: "yes"
715 //
716 // For the GrowthCondition, the referenceItem has a displayName
717 // The key should be: displayName
718 // The value should be: "yes"
719
720 try {
721 // create an document
722 DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance()
723 DocumentBuilder dBuilder = dbFactory.newDocumentBuilder()
724
725 // read the xml file
726 Document doc = dBuilder.parse(new File(path))
727 doc.getDocumentElement().normalize()
728
729 // get all "well" nodes
730 NodeList wellList = doc.getElementsByTagName(wellNode)
731
732 for(int i = 0; i < wellList.getLength(); i++) {
733 Node well = wellList.item(i)
734
735 // extract well attributes
736 String row = well.getAttributes().getNamedItem(rowAttribute).getTextContent()
737 int r = row as int
738 String col = well.getAttributes().getNamedItem(columnAttribute).getTextContent()
739 String wellNumber = LETTERS.substring(r, r + 1) + (Integer.parseInt(col)+ 1)
740
741 // extract items node, under well node
742 Node items = ((Element)well).getElementsByTagName(itemsNode).item(0)
743 if (items != null) {
744 // get all "wellItem" nodes, under items node
745 NodeList wellItemList = ((Element)items).getElementsByTagName(wellItemNode)
746
747 // read referenceItem node's attributes and convert them into key-values
748 List<MapPair> keyValues = new ArrayList<>()
749 for (int j = 0; j < wellItemList.getLength(); j++) {
750 Node wellItem = wellItemList.item(j)
751 // extract referenceItem node, under wellItem node
752 Node referenceItem = ((Element)wellItem).getElementsByTagName(referenceItemNode).item(0)
753 String wellType = wellItem.getAttributes().getNamedItem("type").getTextContent()
754
755 // select the right key-values
756 switch (wellType){
757 case "Compound":
758 String compound_name = referenceItem.getAttributes().getNamedItem("displayName").getTextContent()
759 if (ignoreConcentration) {
760 keyValues.add(new MapPair(compound_name + " (NA)", "1"))
761 } else {
762 String unit = wellItem.getAttributes().getNamedItem("concentrationUnits").getTextContent()
763 unit = unit.replace("\u00B5", "u")
764 String value = wellItem.getAttributes().getNamedItem("concentration").getTextContent()
765 keyValues.add(new MapPair(compound_name + " (" + unit + ")", value))
766 }
767 break
768 case "CellType":
769 String cell_name = referenceItem.getAttributes().getNamedItem("displayName").getTextContent()
770 if (!ignorePassage) {
771 String passage = wellItem.getAttributes().getNamedItem("passage").getTextContent()
772 keyValues.add(new MapPair(cell_name + "_passage", passage))
773 }
774 if (ignoreSeeding) {
775 keyValues.add(new MapPair(cell_name, "yes"))
776 } else {
777 String unit = wellItem.getAttributes().getNamedItem("seedingDensityUnits").getTextContent()
778 unit = unit.replace("\u00B5", "u")
779 String value = wellItem.getAttributes().getNamedItem("seedingDensity").getTextContent()
780 keyValues.add(new MapPair(cell_name + "_seedingDensity (" + unit + ")", value))
781 }
782 break
783 case "GrowthCondition":
784 String growth_condition = referenceItem.getAttributes().getNamedItem("displayName").getTextContent()
785 keyValues.add(new MapPair(growth_condition, "yes"))
786 break
787 }
788 }
789 keyValuesPerWell.put(wellNumber,keyValues)
790
791 }
792 }
793 return keyValuesPerWell
794 } catch (Exception e) {
795 println "XML platemap could not be parsed"
796 e.printStackTrace()
797 return new HashMap<>()
798 }
799 }
800
801 /**
802 * make a list of all key-values that are common to all images
803 *
804 * @param objective
805 * @param commonKeyValues (a String with the following format: key1=value1;key2=value2)
806 * @return a list of OME-XML key-values
807 */
808 def getGlobalKeyValues(int objective, String commonKeyValues){
809 List<MapPair> keyValues = new ArrayList<>()
810 keyValues.add(new MapPair("groovy_version", VERSION))
811 keyValues.add(new MapPair("objective", objectives[objective]))
812 if (commonKeyValues != "") {
813 String[] keyValList = commonKeyValues.split(';')
814 for (int i = 0; i < keyValList.size(); i ++) {
815 String keyval = keyValList[i]
816 String[] keyvalsplit = keyval.split('=')
817 int nPieces = keyvalsplit.size()
818 String value = keyvalsplit[nPieces - 1]
819 String key = keyvalsplit[0]
820 // In case there are '=' in key
821 for (int j = 1; j < nPieces - 1; j++) {
822 key += '=' + keyvalsplit[j]
823 }
824 keyValues.add(new MapPair(key, value))
825 }
826 }
827 return keyValues
828 }
829
830 /**
831 * convert alphanumeric well position to numeric position
832 *
833 * @param letter
834 * @return
835 */
836 def convertLetterToNumber(String letter){
837 for (int i = 0; i < LETTERS.size(); i++) {
838 if (LETTERS.substring(i, i + 1) == letter) {
839 return i
840 }
841 }
842 return -1
843 }
844
845 // Returns a date from a label and a date_pattern
846 def getDate(String label, Pattern date_pattern){
847 // println "Trying to get date from " + label
848 Matcher date_m = date_pattern.matcher(label)
849 LocalDateTime dateTime
850 if (date_m.matches()) {
851 if (date_m.groupCount() == 5) {
852 dateTime = LocalDateTime.parse(date_m.group(1) + "-" + date_m.group(2) + "-" + date_m.group(3) + "T" + date_m.group(4) + ":" + date_m.group(5))
853 } else {
854 dateTime = LocalDateTime.parse("1970-01-" + 1 + (date_m.group(1) as int) + "T" + date_m.group(2) + ":" + date_m.group(3))
855 }
856 }
857 // println "Found " + dateTime
858 return dateTime
859 }
860
861 // Returns the number of hours
862 def getHoursFromImp(ImagePlus imp, ImageStack stack, LocalDateTime dateTime_ref, Pattern date_pattern){
863 int currentSlice = imp.getCurrentSlice()
864 String label = stack.getSliceLabel(currentSlice)
865 LocalDateTime dateTime = getDate(label, date_pattern)
866 if (dateTime != null) {
867 return ChronoUnit.HOURS.between(dateTime_ref, dateTime) as int
868 } else {
869 return -1
870 }
871 }
872
873
874 /**
875 * *****************************************************************************************************************
876 * ************************************************* Imports ****************************************************
877 * ****************************************************************************************************************
878 */
879
880
881 import ij.IJ
882 import ij.ImagePlus
883 import ij.ImageStack
884 import ij.io.FileSaver
885 import ij.io.Opener
886 import ij.plugin.Concatenator
887 import ij.plugin.FolderOpener
888 import ij.plugin.HyperStackConverter
889 import ij.process.LUT
890
891 import java.awt.GraphicsEnvironment
892 import java.io.File
893 import java.time.format.DateTimeFormatter
894 import java.time.LocalDateTime
895 import java.time.temporal.ChronoUnit
896 import java.util.stream.Collectors
897 import java.util.stream.IntStream
898 import java.util.regex.*
899
900 import javax.xml.parsers.DocumentBuilder
901 import javax.xml.parsers.DocumentBuilderFactory
902 import javax.xml.transform.OutputKeys
903 import javax.xml.transform.Result
904 import javax.xml.transform.Source
905 import javax.xml.transform.Transformer
906 import javax.xml.transform.TransformerException
907 import javax.xml.transform.TransformerFactory
908 import javax.xml.transform.dom.DOMSource
909 import javax.xml.transform.stream.StreamResult
910
911 import ome.units.UNITS
912 import ome.units.quantity.Length
913
914 import ome.xml.model.Channel
915 import ome.xml.model.Image
916 import ome.xml.model.MapAnnotation
917 import ome.xml.model.MapPair
918 import ome.xml.model.OME
919 import ome.xml.model.Pixels
920 import ome.xml.model.Plate
921 import ome.xml.model.StructuredAnnotations
922 import ome.xml.model.TiffData
923 import ome.xml.model.UUID
924 import ome.xml.model.Well
925 import ome.xml.model.WellSample
926 import ome.xml.model.enums.DimensionOrder
927 import ome.xml.model.enums.NamingConvention
928 import ome.xml.model.enums.PixelType
929 import ome.xml.model.primitives.Color
930 import ome.xml.model.primitives.NonNegativeInteger
931 import ome.xml.model.primitives.PositiveInteger
932 import ome.xml.model.primitives.Timestamp
933
934 import org.apache.commons.io.filefilter.WildcardFileFilter
935 import org.apache.commons.io.FilenameUtils
936
937 import org.w3c.dom.Document
938 import org.w3c.dom.Element
939 import org.w3c.dom.Node
940 import org.w3c.dom.NodeList