From 0b80678b022bd1be5d1b8be26961232ae025e9b9 Mon Sep 17 00:00:00 2001 From: Melissa Linkert Date: Fri, 11 Aug 2023 10:26:00 -0500 Subject: [PATCH] Add support for INI files representing a Metaxpress plate --- .../bioformats2raw/Converter.java | 3 +- .../bioformats2raw/MetaxpressReader.java | 376 ++++++++++++++++++ .../bioformats2raw/MetaxpressSite.java | 107 +++++ 3 files changed, 485 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/glencoesoftware/bioformats2raw/MetaxpressReader.java create mode 100644 src/main/java/com/glencoesoftware/bioformats2raw/MetaxpressSite.java diff --git a/src/main/java/com/glencoesoftware/bioformats2raw/Converter.java b/src/main/java/com/glencoesoftware/bioformats2raw/Converter.java index d3fb5e16..7a10ef5a 100644 --- a/src/main/java/com/glencoesoftware/bioformats2raw/Converter.java +++ b/src/main/java/com/glencoesoftware/bioformats2raw/Converter.java @@ -150,7 +150,8 @@ public class Converter implements Callable { new HashMap();; private volatile Class[] extraReaders = new Class[] { PyramidTiffReader.class, MiraxReader.class, - BioTekReader.class, ND2PlateReader.class + BioTekReader.class, ND2PlateReader.class, + MetaxpressReader.class }; private volatile boolean omeroMetadata = true; private volatile boolean nested = true; diff --git a/src/main/java/com/glencoesoftware/bioformats2raw/MetaxpressReader.java b/src/main/java/com/glencoesoftware/bioformats2raw/MetaxpressReader.java new file mode 100644 index 00000000..e82bf510 --- /dev/null +++ b/src/main/java/com/glencoesoftware/bioformats2raw/MetaxpressReader.java @@ -0,0 +1,376 @@ +/** + * Copyright (c) 2023 Glencoe Software, Inc. All rights reserved. + * + * This software is distributed under the terms described by the LICENSE.txt + * file you can find at the root of the distribution bundle. If the file is + * missing please request a copy by contacting info@glencoesoftware.com + */ +package com.glencoesoftware.bioformats2raw; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashMap; + +import loci.common.IniList; +import loci.common.IniParser; +import loci.common.IniTable; +import loci.common.RandomAccessInputStream; +import loci.formats.CoreMetadata; +import loci.formats.FormatException; +import loci.formats.FormatReader; +import loci.formats.FormatTools; +import loci.formats.MetadataTools; +import loci.formats.in.DynamicMetadataOptions; +import loci.formats.in.MetadataOptions; +import loci.formats.in.MetamorphReader; +import loci.formats.in.MinimalTiffReader; +import loci.formats.meta.MetadataStore; +import loci.formats.ome.OMEXMLMetadata; +import ome.units.quantity.Length; +import ome.units.quantity.Time; +import ome.units.UNITS; +import ome.xml.model.primitives.NonNegativeInteger; +import ome.xml.model.primitives.PositiveInteger; +import ome.xml.model.primitives.Timestamp; + +import org.perf4j.StopWatch; +import org.perf4j.slf4j.Slf4JStopWatch; + +/** + * MetaxpressReader is the file format reader for MetaXpress plates. + */ +public class MetaxpressReader extends FormatReader { + + // -- Constants -- + + public static final String INCLUDE_TIFFS_KEY = "metaxpress.include_tiffs"; + public static final boolean INCLUDE_TIFFS_DEFAULT = false; + + public static final String MAGIC_STRING = "#METAXPRESS FILE"; + + // -- Fields -- + + private ArrayList sites = new ArrayList(); + + private transient MinimalTiffReader planeReader = new MinimalTiffReader(); + + // -- Constructor -- + + /** Constructs a new MetaXpress reader. */ + public MetaxpressReader() { + super("MetaXpress", "metaxpress"); + domains = new String[] {FormatTools.HCS_DOMAIN}; + } + + // -- Metaxpress-specific methods -- + + /** + * Check reader options to determine if TIFFs + * should be included in used files. + * + * @return true if TIFFs should be added to used files list + */ + public boolean canIncludeTIFFs() { + MetadataOptions options = getMetadataOptions(); + if (options instanceof DynamicMetadataOptions) { + return ((DynamicMetadataOptions) options).getBoolean( + INCLUDE_TIFFS_KEY, INCLUDE_TIFFS_DEFAULT); + } + return INCLUDE_TIFFS_DEFAULT; + } + + // -- IFormatReader API methods -- + + @Override + public boolean isThisType(RandomAccessInputStream stream) throws IOException { + final int blockLen = 16; + if (!FormatTools.validStream(stream, blockLen, false)) { + return false; + } + return MAGIC_STRING.equals(stream.readString(blockLen)); + } + + @Override + public byte[] openBytes(int no, byte[] buf, int x, int y, int w, int h) + throws FormatException, IOException + { + FormatTools.checkPlaneParameters(this, no, buf.length, x, y, w, h); + + StopWatch s = stopWatch(); + String file = sites.get(getSeries()).files.get(no); + s.stop("file lookup for [" + getSeries() + ", " + no + "]"); + if (file != null) { + s = stopWatch(); + if (planeReader == null) { + planeReader = new MinimalTiffReader(); + } + try { + planeReader.setId(file); + s.stop("setId on " + file); + s = stopWatch(); + planeReader.openBytes(0, buf, x, y, w, h); + } + catch (IOException e) { + Arrays.fill(buf, (byte) 0); + } + s.stop("openBytes(0) on " + file); + } + else { + Arrays.fill(buf, (byte) 0); + } + + return buf; + } + + @Override + public String[] getSeriesUsedFiles(boolean noPixels) { + ArrayList files = new ArrayList(); + files.add(currentId); + if (canIncludeTIFFs() && !noPixels) { + files.addAll(sites.get(getSeries()).files); + } + return files.toArray(new String[files.size()]); + } + + @Override + public void close(boolean fileOnly) throws IOException { + super.close(fileOnly); + planeReader.close(fileOnly); + if (!fileOnly) { + sites.clear(); + } + } + + // -- Internal FormatReader API methods -- + + @Override + protected void initFile(String id) throws FormatException, IOException { + super.initFile(id); + + LOGGER.info("Parsing metadata file"); + StopWatch watch = stopWatch(); + + IniList plateMetadata = new IniParser().parseINI(new File(currentId)); + watch.stop("parsed metadata file"); + + watch = stopWatch(); + IniTable plate = plateMetadata.getTable("Plate"); + + for (IniTable table : plateMetadata) { + String tableName = table.get(IniTable.HEADER_KEY); + if (tableName.startsWith("Site")) { + sites.add(new MetaxpressSite(table)); + } + } + sites.sort(new Comparator() { + public int compare(MetaxpressSite a, MetaxpressSite b) { + if (a.y != b.y) { + return a.y - b.y; + } + if (a.x != b.x) { + return a.x - b.x; + } + return a.id.compareTo(b.id); + } + }); + + core = new ArrayList(); + + Length physicalSizeX = null; + Length physicalSizeY = null; + Length[] wavelengths = null; + Time[] exposureTimes = null; + + MetamorphReader reader = new MetamorphReader(); + reader.setGroupFiles(false); + reader.setOriginalMetadataPopulated(isOriginalMetadataPopulated()); + reader.setMetadataFiltered(isMetadataFiltered()); + watch.stop("set up reader and site list"); + + watch = stopWatch(); + for (int i=0; i validWells = new HashMap(); + for (MetaxpressSite s : sites) { + int well = s.y * wellsX + s.x; + if (!validWells.containsKey(well)) { + validWells.put(well, 1); + } + else { + validWells.put(well, validWells.get(well) + 1); + } + } + LOGGER.trace("validWells = {}", validWells); + + // the field count may vary between wells, so find the + // maximum field count across all wells + int nFields = 0; + for (Integer f : validWells.values()) { + if (f > nFields) { + nFields = f; + } + } + LOGGER.debug("field count = {}", nFields); + + store.setPlateAcquisitionMaximumFieldCount( + new PositiveInteger(nFields), 0, 0); + store.setPlateAcquisitionStartTime(new Timestamp(plate.get("Date")), 0, 0); + + watch.stop("populated plate metadata"); + watch = stopWatch(); + + int image = 0; + int well = 0; + for (int row=0; row= sites.size()) { + break; + } + MetaxpressSite site = sites.get(image); + if (site.x != col || site.y != row) { + // make sure that this site's well lines up with the + // well that we're processing + // this should catch the case when a well has fewer + // fields than expected + break; + } + + LOGGER.debug("Using site {} for row = {}, col = {}, field = {}", + image, row, col, field); + + String wellSampleID = + MetadataTools.createLSID("WellSample", 0, well, field); + store.setWellSampleID(wellSampleID, 0, well, field); + + String imageID = MetadataTools.createLSID("Image", image); + store.setImageID(imageID, image); + store.setImageName("Well " + FormatTools.getWellName(row, col) + + ", Field #" + (field + 1), image); + store.setImageAcquisitionDate( + new Timestamp(plate.get("Date")), image); + + store.setWellSampleImageRef(imageID, 0, well, field); + store.setWellSampleIndex( + new NonNegativeInteger(image), 0, well, field); + + store.setWellSamplePositionX( + new Length(site.xpos, UNITS.REFERENCEFRAME), 0, well, field); + store.setWellSamplePositionY( + new Length(site.ypos, UNITS.REFERENCEFRAME), 0, well, field); + + store.setPlateAcquisitionWellSampleRef(wellSampleID, 0, 0, image); + + store.setPixelsPhysicalSizeX(physicalSizeX, image); + store.setPixelsPhysicalSizeY(physicalSizeY, image); + + for (int c=0; c files = new ArrayList(); + public ArrayList channelNames = new ArrayList(); + public double xpos; + public double ypos; + public int z = 1; + public int c = 1; + public int t = 1; + + public int minZ = Integer.MAX_VALUE; + public int maxZ = 0; + public int minT = Integer.MAX_VALUE; + public int maxT = 0; + + /** + * Create empty site. + */ + public MetaxpressSite() { + } + + /** + * Populate a site from INI data. + * + * @param table INI table representing this site + */ + public MetaxpressSite(IniTable table) { + id = table.get("ID"); + x = Integer.parseInt(table.get("X")); + y = Integer.parseInt(table.get("Y")); + z = Integer.parseInt(table.get("Z")); + c = Integer.parseInt(table.get("C")); + t = Integer.parseInt(table.get("T")); + xpos = DataTools.parseDouble(table.get("XPosition")); + ypos = DataTools.parseDouble(table.get("YPosition")); + + // min/max Z and T values do not need to be stored + // since they are only used for calculating z and t + + for (String key : table.keySet()) { + boolean file = key.startsWith("File_"); + boolean channel = key.startsWith("ChannelName_"); + if (file || channel) { + int index = Integer.parseInt(key.substring(key.indexOf("_") + 1)); + if (file) { + while (index >= files.size()) { + files.add(null); + } + files.set(index, table.get(key)); + } + else if (channel) { + while (index >= channelNames.size()) { + channelNames.add(null); + } + channelNames.set(index, table.get(key)); + } + } + } + } + + @Override + public String toString() { + return "id=" + id + ", x=" + x + ", y=" + y + + ", z=" + z + ", c=" + c + ", t=" + t + + ", files.size=" + files.size(); + } + + /** + * @return an INI table representing this site + */ + public IniTable getIniTable() { + IniTable table = new IniTable(); + table.put(IniTable.HEADER_KEY, "Site " + id); + table.put("ID", id); + table.put("X", String.valueOf(x)); + table.put("Y", String.valueOf(y)); + table.put("Z", String.valueOf(z)); + table.put("C", String.valueOf(c)); + table.put("T", String.valueOf(t)); + table.put("XPosition", String.valueOf(xpos)); + table.put("YPosition", String.valueOf(ypos)); + for (int i=0; i