Mercurial > repos > mvdbeek > incucyte_stack_and_upload_omero_debug
comparison stack_buildXml.groovy @ 0:a599551e800d draft
planemo upload for repository https://github.com/lldelisle/tools-lldelisle/tree/master/tools/incucyte_stack_and_upload_omero commit 4ac9b1d66ba6857357867c8eccb6c9d1ad603364-dirty
author | mvdbeek |
---|---|
date | Tue, 06 Aug 2024 15:28:26 +0000 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:a599551e800d |
---|---|
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 |