/* * #%L * BSD implementations of Bio-Formats readers and writers * %% * Copyright (C) 2005 - 2017 Open Microscopy Environment: * - Board of Regents of the University of Wisconsin-Madison * - Glencoe Software, Inc. * - University of Dundee * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. * #L% */ package loci.formats.in; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Hashtable; import java.util.List; import java.util.StringTokenizer; import java.util.Vector; import java.util.zip.GZIPInputStream; import loci.common.DataTools; import loci.common.DateTools; import loci.common.Location; 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.meta.MetadataStore; import ome.xml.model.primitives.Timestamp; import ome.units.quantity.Frequency; import ome.units.quantity.Length; import ome.units.quantity.Power; import ome.units.quantity.Time; import ome.units.UNITS; /** * ICSReader is the file format reader for ICS (Image Cytometry Standard) * files. More information on ICS can be found at http://libics.sourceforge.net * * TODO : remove sub-C logic once N-dimensional support is in place * see http://dev.loci.wisc.edu/trac/java/ticket/398 * * @author Melissa Linkert melissa at glencoesoftware.com */ public class ICSReader extends FormatReader { // -- Constants -- /** Newline characters. */ public static final String NL = "\r\n"; public static final String[] DATE_FORMATS = { "EEEE, MMMM dd, yyyy HH:mm:ss", "EEE dd MMMM yyyy HH:mm:ss", "EEE MMM dd HH:mm:ss yyyy", "EE dd MMM yyyy HH:mm:ss z", "HH:mm:ss dd\\MM\\yy" }; // key token value matching regexes within the "document" category. // // this table is alphabetized for legibility only. // // however it is important that the most qualified regex list goes first, // e.g. { "a", "b" } must precede { "a" }. private static final String[][] DOCUMENT_KEYS = { { "date" }, // the full key is "document date" { "document", "average" }, { "document" }, { "gmtdate" }, { "label" } }; // key token value matching regexes within the "history" category. // // this table is alphabetized for legibility only. // // however it is important that the most qualified regex list goes first, // e.g. { "a", "b" } must precede { "a" }. private static final String[][] HISTORY_KEYS = { { "a\\d" }, // the full key is "history a1", etc. { "acquisition", "acquire\\..*." }, { "acquisition", "laserbox\\..*." }, { "acquisition", "modules\\(.*." }, { "acquisition", "objective", "position" }, { "adc", "resolution" }, { "atd_hardware", "ver" }, { "atd_libraries", "ver" }, { "atd_microscopy", "ver" }, { "author" }, { "averagecount" }, { "averagequality" }, { "beam", "zoom" }, { "binning" }, { "bits/pixel" }, { "black", "level" }, { "black", "level\\*" }, { "black_level" }, { "camera", "manufacturer" }, { "camera", "model" }, { "camera" }, { "cfd", "holdoff" }, { "cfd", "limit", "high" }, { "cfd", "limit", "low" }, { "cfd", "zc", "level" }, { "channel\\*" }, { "collection", "time" }, { "cols" }, { "company" }, { "count", "increment" }, { "created", "on" }, { "creation", "date" }, { "cube", "descriptio" }, // sic; not found in sample files { "cube", "description" }, // correction; not found in sample files { "cube", "emm", "nm" }, { "cube", "exc", "nm" }, { "cube" }, { "date" }, { "dategmt" }, { "dead", "time", "comp" }, { "desc", "exc", "turret" }, { "desc", "emm", "turret" }, { "detector", "type" }, { "detector" }, { "dimensions" }, { "direct", "turret" }, { "dither", "range" }, { "dwell" }, { "excitationfwhm" }, { "experiment" }, { "experimenter" }, { "expon.", "order" }, { "exposure" }, { "exposure_time" }, { "ext", "latch", "delay" }, { "extents" }, { "filterset", "dichroic", "name" }, { "filterset", "dichroic", "nm" }, { "filterset", "emm", "name" }, { "filterset", "emm", "nm" }, { "filterset", "exc", "name" }, { "filterset", "exc", "nm" }, { "filterset" }, { "filter\\*" }, { "firmware" }, { "fret", "backgr\\d"}, { "frametime" }, { "gain" }, { "gain\\d" }, { "gain\\*" }, { "gamma" }, { "icsviewer", "ver" }, { "ht\\*" }, { "id" }, { "illumination", "mode", "laser" }, { "illumination", "mode" }, { "image", "bigendian" }, { "image", "bpp" }, { "image", "form" }, // not found in sample files { "image", "physical_sizex" }, { "image", "physical_sizey" }, { "image", "sizex" }, { "image", "sizey" }, { "labels" }, { "lamp", "manufacturer" }, { "lamp", "model" }, { "laser", "firmware" }, { "laser", "manufacturer" }, { "laser", "model" }, { "laser", "power" }, { "laser", "rep", "rate" }, { "laser", "type" }, { "laser\\d", "intensity" }, { "laser\\d", "name" }, { "laser\\d", "wavelength" }, { "left" }, { "length" }, { "line", "compressio" }, // sic { "line", "compression" }, // correction; not found in sample files { "linetime" }, { "magnification" }, { "manufacturer" }, { "max", "photon", "coun" }, // sic { "max", "photon", "count" }, // correction; not found in sample files { "memory", "bank" }, { "metadata", "format", "ver" }, { "microscope", "built", "on" }, { "microscope", "name" }, { "microscope" }, { "mirror", "\\d" }, { "mode" }, { "noiseval" }, { "no.", "frames" }, { "objective", "detail" }, { "objective", "immersion" }, { "objective", "mag" }, { "objective", "magnification" }, { "objective", "na" }, { "objective", "type" }, { "objective", "workingdistance" }, { "objective" }, { "offsets" }, { "other", "text" }, { "passcount" }, { "pinhole" }, { "pixel", "clock" }, { "pixel", "time" }, { "pmt" }, { "polarity" }, { "region" }, { "rep", "period" }, { "repeat", "time" }, { "revision" }, { "routing", "chan", "x" }, { "routing", "chan", "y" }, { "rows" }, { "scan", "borders" }, { "scan", "flyback" }, { "scan", "pattern" }, { "scan", "pixels", "x" }, { "scan", "pixels", "y" }, { "scan", "pos", "x" }, { "scan", "pos", "y" }, { "scan", "resolution" }, { "scan", "speed" }, { "scan", "zoom" }, { "scanner", "lag" }, { "scanner", "pixel", "time" }, { "scanner", "resolution" }, { "scanner", "speed" }, { "scanner", "xshift" }, { "scanner", "yshift" }, { "scanner", "zoom" }, { "shutter\\d" }, { "shutter", "type" }, { "software" }, { "spectral", "bin_definition" }, { "spectral", "calibration", "gain", "data" }, { "spectral", "calibration", "gain", "mode" }, { "spectral", "calibration", "offset", "data" }, { "spectral", "calibration", "offset", "mode" }, { "spectral", "calibration", "sensitivity", "mode" }, { "spectral", "central_wavelength" }, { "spectral", "laser_shield" }, { "spectral", "laser_shield_width" }, { "spectral", "resolution" }, { "stage", "controller" }, { "stage", "firmware" }, { "stage", "manufacturer" }, { "stage", "model" }, { "stage", "pos" }, { "stage", "positionx" }, { "stage", "positiony" }, { "stage", "positionz" }, { "stage_xyzum" }, { "step\\d", "channel", "\\d" }, { "step\\d", "gain", "\\d" }, { "step\\d", "laser" }, { "step\\d", "name" }, { "step\\d", "pinhole" }, { "step\\d", "pmt", "ch", "\\d" }, { "step\\d", "shutter", "\\d" }, { "step\\d" }, { "stop", "on", "o'flow" }, { "stop", "on", "time" }, { "study" }, { "sync", "freq", "div" }, { "sync", "holdoff" }, { "sync" }, { "tac", "gain" }, { "tac", "limit", "low" }, { "tac", "offset" }, { "tac", "range" }, { "tau\\d" }, { "tcspc", "adc", "res" }, { "tcspc", "adc", "resolution" }, { "tcspc", "approx", "adc", "rate" }, { "tcspc", "approx", "cfd", "rate" }, { "tcspc", "approx", "tac", "rate" }, { "tcspc", "bh" }, { "tcspc", "cfd", "holdoff" }, { "tcspc", "cfd", "limit", "high" }, { "tcspc", "cfd", "limit", "low" }, { "tcspc", "cfd", "zc", "level" }, { "tcspc", "clock", "polarity" }, { "tcspc", "collection", "time" }, { "tcspc", "count", "increment" }, { "tcspc", "dead", "time", "enabled" }, { "tcspc", "delay" }, { "tcspc", "dither", "range" }, { "tcspc", "left", "border" }, { "tcspc", "line", "compression" }, { "tcspc", "mem", "offset" }, { "tcspc", "operation", "mode" }, { "tcspc", "overflow" }, { "tcspc", "pixel", "clk", "divider" }, { "tcspc", "pixel", "clock" }, { "tcspc", "routing", "x" }, { "tcspc", "routing", "y" }, { "tcspc", "scan", "x" }, { "tcspc", "scan", "y" }, { "tcspc", "sync", "divider" }, { "tcspc", "sync", "holdoff" }, { "tcspc", "sync", "rate" }, { "tcspc", "sync", "threshold" }, { "tcspc", "sync", "zc", "level" }, { "tcspc", "tac", "gain" }, { "tcspc", "tac", "limit", "high" }, { "tcspc", "tac", "limit", "low" }, { "tcspc", "tac", "offset" }, { "tcspc", "tac", "range" }, { "tcspc", "time", "window" }, { "tcspc", "top", "border" }, { "tcspc", "total", "frames" }, { "tcspc", "total", "time" }, { "tcspc", "trigger" }, { "tcspc", "x", "sync", "polarity" }, { "tcspc", "y", "sync", "polarity" }, { "text" }, { "time" }, { "title" }, { "top" }, { "transmission" }, { "trigger" }, { "type" }, { "units" }, { "version" }, { "wavelength\\*" }, { "x", "amplitude" }, { "y", "amplitude" }, { "x", "delay" }, { "y", "delay" }, { "x", "offset" }, { "y", "offset" }, { "z", "\\(background\\)" } }; // key token value matching regexes within the "layout" category. // // this table is alphabetized for legibility only. // // however it is important that the most qualified regex list goes first, // e.g. { "a", "b" } must precede { "a" }. private static final String[][] LAYOUT_KEYS = { { "coordinates" }, // the full key is "layout coordinates" { "order" }, { "parameters" }, { "real_significant_bits" }, { "significant_bits" }, { "significant_channels" }, { "sizes" } }; // key token value matching regexes within the "parameter" category. // // this table is alphabetized for legibility only. // // however it is important that the most qualified regex list goes first, // e.g. { "a", "b" } must precede { "a" }. private static final String[][] PARAMETER_KEYS = { { "allowedlinemodes" }, // the full key is "parameter allowedlinemodes" { "ch" }, { "higher_limit" }, { "labels" }, { "lower_limit" }, { "origin" }, { "range" }, { "sample_width", "ch" }, { "sample_width" }, { "scale" }, { "units", "adc-units", "channels" }, { "units", "adc-units", "nm" }, { "units" } }; // key token value matching regexes within the "representation" category. // // this table is alphabetized for legibility only. // // however it is important that the most qualified regex list goes first, // e.g. { "a", "b" } must precede { "a" }. private static final String[][] REPRESENTATION_KEYS = { { "byte_order" }, // the full key is "representation byte_order" { "compression" }, { "format" }, { "sign" } }; // key token value matching regexes within the "sensor" category. // // this table is alphabetized for legibility only. // // however it is important that the most qualified regex list goes first, // e.g. { "a", "b" } must precede { "a" }. private static final String[][] SENSOR_KEYS = { { "model" }, // the full key is "sensor model" { "s_params", "channels" }, { "s_params", "exphotoncnt" }, { "s_params", "lambdaem" }, { "s_params", "lambdaex" }, { "s_params", "numaperture" }, { "s_params", "pinholeradius" }, { "s_params", "pinholespacing" }, { "s_params", "refinxlensmedium" }, // sic; not found in sample files { "s_params", "refinxmedium" }, // sic; not found in sample files { "s_params", "refrinxlensmedium" }, { "s_params", "refrinxmedium" }, { "type" } }; // key token value matching regexes within the "view" category. // // this table is alphabetized for legibility only. // // however it is important that the most qualified regex list goes first, // e.g. { "a", "b" } must precede { "a" }. private static final String[][] VIEW_KEYS = { { "view", "color", "lib", "lut" }, // the full key is // "view view color lib lut" { "view", "color", "count" }, { "view", "color", "doc", "scale" }, { "view", "color", "mode", "rgb", "set" }, { "view", "color", "mode", "rgb" }, { "view", "color", "schemes" }, { "view", "color", "view", "active" }, { "view", "color" }, { "view\\d", "alpha" }, { "view\\d", "alphastate" }, { "view\\d", "annotation", "annellipse" }, { "view\\d", "annotation", "annpoint" }, { "view\\d", "autoresize" }, { "view\\d", "axis" }, { "view\\d", "blacklevel" }, { "view\\d", "color" }, { "view\\d", "cursor" }, { "view\\d", "dimviewoption" }, { "view\\d", "gamma" }, { "view\\d", "ignoreaspect" }, { "view\\d", "intzoom" }, { "view\\d", "live" }, { "view\\d", "order" }, { "view\\d", "port" }, { "view\\d", "position" }, { "view\\d", "saturation" }, { "view\\d", "scale" }, { "view\\d", "showall" }, { "view\\d", "showcursor" }, { "view\\d", "showindex" }, { "view\\d", "size" }, { "view\\d", "synchronize" }, { "view\\d", "tile" }, { "view\\d", "useunits" }, { "view\\d", "zoom" }, { "view\\d" }, { "view" } }; // These strings appeared in the former metadata field categories but are not // found in the LOCI sample files. // // The former metadata field categories table did not save the context, i.e. // the first token such as "document" or "history" and other intermediate // tokens. The preceding tables such as DOCUMENT_KEYS or HISTORY_KEYS use // this full context. // // In an effort at backward compatibility, these will be used to form key // value pairs if key/value pair not already assigned and they match anywhere // in the input line. // private static String[][] OTHER_KEYS = { { "cube", "descriptio" }, // sic; also listed in HISTORY_KEYS { "cube", "description" }, // correction; also listed in HISTORY_KEYS { "image", "form" }, // also listed in HISTORY_KEYS { "refinxlensmedium" }, // Could be a mispelling of "refrinxlensmedium"; // also listed in SENSOR_KEYS { "refinxmedium" }, // Could be a mispelling of "refinxmedium"; // also listed in SENSOR_KEYS { "scil_type" }, { "source" } }; // -- Fields -- /** Current filename. */ private String currentIcsId; private String currentIdsId; /** Flag indicating whether current file is v2.0. */ private boolean versionTwo; /** Image data. */ private byte[] data; /** Offset to pixel data. */ private long offset; /** Whether or not the pixels are GZIP-compressed. */ private boolean gzip; private GZIPInputStream gzipStream; /** Whether or not the image is inverted along the Y axis. */ private boolean invertY; /** Whether or not the channels represent lifetime histogram bins. */ private boolean lifetime; /** Dimensional reordering for lifetime data */ private String labels; /** The length of each channel axis. */ private Vector channelLengths; /** The type of each channel axis. */ private Vector channelTypes; private int prevImage; private boolean hasInstrumentData = false; private boolean storedRGB = false; // -- Constructor -- /** Constructs a new ICSReader. */ public ICSReader() { super("Image Cytometry Standard", new String[] {"ics", "ids"}); domains = new String[] {FormatTools.LM_DOMAIN, FormatTools.FLIM_DOMAIN, FormatTools.UNKNOWN_DOMAIN}; hasCompanionFiles = true; datasetDescription = "One .ics and possibly one .ids with a similar name"; } // -- IFormatReader API methods -- /* @see loci.formats.IFormatReader#isSingleFile(String) */ @Override public boolean isSingleFile(String id) throws FormatException, IOException { // check if we have a v2 ICS file - means there is no companion IDS file try (RandomAccessInputStream f = new RandomAccessInputStream(id)) { return f.readString(17).trim().equals("ics_version\t2.0"); } } /* @see loci.formats.IFormatReader#getDomains() */ @Override public String[] getDomains() { FormatTools.assertId(currentId, true, 1); String[] domain = new String[] {FormatTools.GRAPHICS_DOMAIN}; if (getModuloC().length() > 1) { domain[0] = FormatTools.FLIM_DOMAIN; } else if (hasInstrumentData) { domain[0] = FormatTools.LM_DOMAIN; } return domain; } /* @see loci.formats.IFormatReader#isInterleaved(int) */ @Override public boolean isInterleaved(int subC) { FormatTools.assertId(currentId, true, 1); return subC == 0 && core.get(0).interleaved; } /* @see loci.formats.IFormatReader#fileGroupOption(String) */ @Override public int fileGroupOption(String id) throws FormatException, IOException { return FormatTools.MUST_GROUP; } /** * @see loci.formats.IFormatReader#openBytes(int, byte[], int, int, int, int) */ @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); int bpp = FormatTools.getBytesPerPixel(getPixelType()); int len = FormatTools.getPlaneSize(this); int rowLen = FormatTools.getPlaneSize(this, w, 1); // Update y value when invertY and reading tiles if (invertY) { y = getSizeY() - y - h; } int[] coordinates = getZCTCoords(no); int[] prevCoordinates = getZCTCoords(prevImage); if (!gzip) { in.seek(offset + no * (long) len); } else { long toSkip = (no - prevImage - 1) * (long) len; if (gzipStream == null || no <= prevImage) { FileInputStream fis = null; toSkip = no * (long) len; if (versionTwo) { fis = new FileInputStream(currentIcsId); fis.skip(offset); } else { fis = new FileInputStream(currentIdsId); toSkip += offset; } try { if (gzipStream != null) { gzipStream.close(); } gzipStream = new GZIPInputStream(fis); } catch (IOException e) { // the 'gzip' flag is set erroneously gzip = false; in.seek(offset + no * (long) len); gzipStream = null; } } if (gzipStream != null) { while (toSkip > 0) { toSkip -= gzipStream.skip(toSkip); } data = new byte[len * (storedRGB ? getSizeC() : 1)]; int toRead = data.length; while (toRead > 0) { toRead -= gzipStream.read(data, data.length - toRead, toRead); } } } int sizeC = lifetime ? 1 : getSizeC(); if (!isRGB() && channelLengths.size() == 1 && storedRGB) { // channels are stored interleaved, but because there are more than we // can display as RGB, we need to separate them in.seek(offset + (long) len * getIndex(coordinates[0], 0, coordinates[2])); if (!gzip && data == null) { data = new byte[len * getSizeC()]; in.read(data); } else if (!gzip && (coordinates[0] != prevCoordinates[0] || coordinates[2] != prevCoordinates[2])) { in.read(data); } for (int row=y; row gains = new Hashtable(); Hashtable pinholes = new Hashtable(); Hashtable wavelengths = new Hashtable(); Hashtable channelNames = new Hashtable(); double[] sizes = null; boolean signed = false; try (RandomAccessInputStream reader = new RandomAccessInputStream(icsId)) { reader.seek(0); reader.readString(NL); String line = reader.readString(NL); while (line != null && !line.trim().equals("end") && reader.getFilePointer() < reader.length() - 1) { line = line.trim(); if (line.length() > 0) { // split the line into tokens String[] tokens = tokenize(line); String token0 = tokens[0].toLowerCase(); String[] keyValue = null; // version category if (token0.equals("ics_version")) { String value = concatenateTokens(tokens, 1, tokens.length); addGlobalMeta(token0, value); } // filename category else if (token0.equals("filename")) { imageName = concatenateTokens(tokens, 1, tokens.length); addGlobalMeta(token0, imageName); } // layout category else if (token0.equals("layout")) { keyValue = findKeyValue(tokens, LAYOUT_KEYS); String key = keyValue[0]; String value = keyValue[1]; addGlobalMeta(key, value); if (key.equalsIgnoreCase("layout sizes")) { StringTokenizer t = new StringTokenizer(value); axisLengths = new int[t.countTokens()]; for (int n=0; n 0) { date = value.substring(0, value.lastIndexOf(" ")); date = DateTools.formatDate(date, DATE_FORMATS); } } else if (key.equalsIgnoreCase("history creation date")) { date = DateTools.formatDate(value, DATE_FORMATS); } else if (key.equalsIgnoreCase("history type")) { // HACK - support for Gray Institute at Oxford's ICS lifetime data if (value.equalsIgnoreCase("time resolved") || value.equalsIgnoreCase("FluorescenceLifetime")) { lifetime = true; } experimentType = value; } else if (key.equalsIgnoreCase("history labels")) { // HACK - support for Gray Institute at Oxford's ICS lifetime data labels = value; } else if (getMetadataOptions().getMetadataLevel() != MetadataLevel.MINIMUM) { if (key.equalsIgnoreCase("history") || key.equalsIgnoreCase("history text")) { textBlock.append(value); textBlock.append("\n"); metadata.remove(key); } else if (key.startsWith("history gain")) { int n = 0; try { n = Integer.parseInt(key.substring(12).trim()); n--; } catch (NumberFormatException e) { } if (doubleValue != null) { gains.put(n, doubleValue); } } else if (key.startsWith("history laser") && key.endsWith("wavelength")) { int laser = Integer.parseInt(key.substring(13, key.indexOf(" ", 13))) - 1; value = value.replaceAll("nm", "").trim(); try { wavelengths.put(laser, DataTools.parseDouble(value)); } catch (NumberFormatException e) { LOGGER.debug("Could not parse wavelength", e); } } else if (key.equalsIgnoreCase("history Wavelength*")) { String[] waves = value.split(" "); for (int i=0; i 0 && redIndex > greenIndex && redIndex > blueIndex) { index = redIndex + "red".length(); } else if (greenIndex > 0 && greenIndex > redIndex && greenIndex > blueIndex) { index = greenIndex + "green".length(); } else if (blueIndex > 0 && blueIndex > redIndex && blueIndex > greenIndex) { index = blueIndex + "blue".length(); } else { index = value.indexOf(' '); } if (index > 0) { key = key + ' ' + value.substring(0, index); value = value.substring(index + 1); } } // handle "view view color mode rgb set Default Colors" and // "view view color mode rgb set blue-green-red", etc. else if (key.equalsIgnoreCase("view view color mode rgb set")) { int index = value.toLowerCase().lastIndexOf("colors"); if (index > 0) { index += "colors".length(); } else { index = value.indexOf(' '); } if (index > 0) { key = key + ' ' + value.substring(0, index); value = value.substring(index + 1); } } addGlobalMeta(key, value); } else { LOGGER.debug("Unknown category " + token0); } } line = reader.readString(NL); } } hasInstrumentData = emWaves != null || exWaves != null || lensNA != null || stagePos != null || magnification != null || workingDistance != null || objectiveModel != null || immersion != null; addGlobalMeta("history text", textBlock.toString()); LOGGER.info("Populating core metadata"); m.rgb = false; m.dimensionOrder = "XY"; // find axis sizes channelLengths = new Vector(); channelTypes = new Vector(); int bitsPerPixel = 0; for (int i=0; i= axisLengths.length) break; if (axes[i].equals("bits")) { bitsPerPixel = axisLengths[i]; while (bitsPerPixel % 8 != 0) bitsPerPixel++; if (bitsPerPixel == 24 || bitsPerPixel == 48) bitsPerPixel /= 3; } else if (axes[i].equals("x")) { m.sizeX = axisLengths[i]; } else if (axes[i].equals("y")) { m.sizeY = axisLengths[i]; } else if (axes[i].equals("z")) { m.sizeZ = axisLengths[i]; if (getDimensionOrder().indexOf('Z') == -1) { m.dimensionOrder += 'Z'; } } else if (axes[i].equals("t")) { if (getSizeT() == 0) m.sizeT = axisLengths[i]; else m.sizeT *= axisLengths[i]; if (getDimensionOrder().indexOf('T') == -1) { m.dimensionOrder += 'T'; } } else { if (m.sizeC == 0) m.sizeC = axisLengths[i]; else m.sizeC *= axisLengths[i]; channelLengths.add(axisLengths[i]); storedRGB = getSizeX() == 0; m.rgb = getSizeX() == 0 && getSizeC() <= 4 && getSizeC() > 1; if (getDimensionOrder().indexOf('C') == -1) { m.dimensionOrder += 'C'; } if (axes[i].startsWith("c")) { channelTypes.add(FormatTools.CHANNEL); } else if (axes[i].equals("p")) { channelTypes.add(FormatTools.PHASE); } else if (axes[i].equals("f")) { channelTypes.add(FormatTools.FREQUENCY); } else channelTypes.add(""); } } if (channelLengths.isEmpty()) { channelLengths.add(1); channelTypes.add(FormatTools.CHANNEL); } if (isRGB() && emWaves != null && emWaves.length == getSizeC()) { m.rgb = false; storedRGB = true; } m.dimensionOrder = MetadataTools.makeSaneDimensionOrder(getDimensionOrder()); if (getSizeZ() == 0) m.sizeZ = 1; if (getSizeC() == 0) m.sizeC = 1; if (getSizeT() == 0) m.sizeT = 1; // Set up ModuloC. It appears that for ICS, different channels // can have different lengths, which isn't supported by ModuloC // which requires all channels have the same length. This could // be rectified by setting SizeC=1 and allowing multiple Modulo // annotations per dimension, but would require a model change. // Here, ModuloC is only set if all channels are of the same // length and type. if (channelLengths.size() > 0) { int clen0 = channelLengths.get(0); String ctype0 = channelTypes.get(0); boolean same = true; for (Integer len : channelLengths) { if (clen0 != len) same = false; } for (String type : channelTypes) { if (!ctype0.equals(type)) same = false; } if (same) { m.moduloC.type = ctype0; if (FormatTools.LIFETIME.equals(ctype0)) { m.moduloC.parentType = FormatTools.SPECTRA; } m.moduloC.typeDescription = "TCSPC"; m.moduloC.start = 0; m.moduloC.step = 1; m.moduloC.end = clen0 - 1; } } m.interleaved = isRGB(); m.indexed = false; m.falseColor = false; m.metadataComplete = true; m.littleEndian = true; // HACK - support for Gray Institute at Oxford's ICS lifetime data if (lifetime && labels != null) { int binCount = 0; String newOrder = null; if (labels.equalsIgnoreCase("t x y")) { // nominal X Y Z is actually C X Y (which is X Y C interleaved) newOrder = "XYCZT"; m.interleaved = true; binCount = m.sizeX; m.sizeX = m.sizeY; m.sizeY = m.sizeZ; m.sizeZ = 1; } else if (labels.equalsIgnoreCase("x y t")) { // nominal X Y Z is actually X Y C newOrder = "XYCZT"; binCount = m.sizeZ; m.sizeZ = 1; } else { LOGGER.debug("Lifetime data, unexpected 'history labels' " + labels); } if (newOrder != null) { m.dimensionOrder = newOrder; m.sizeC = binCount; m.moduloC.parentType = FormatTools.LIFETIME; } } // do not modify the Z, T, or channel counts after this point m.imageCount = getSizeZ() * getSizeT(); if (!isRGB()) m.imageCount *= getSizeC(); if (byteOrder != null) { String firstByte = byteOrder.split(" ")[0]; int first = Integer.parseInt(firstByte); m.littleEndian = rFormat.equals("real") ? first == 1 : first != 1; } gzip = (compression == null) ? false : compression.equals("gzip"); if (versionTwo) { String s = in.readString(NL); while (!s.trim().equals("end")) s = in.readString(NL); } offset = in.getFilePointer(); int bytes = bitsPerPixel / 8; if (bitsPerPixel < 32) m.littleEndian = !isLittleEndian(); boolean fp = rFormat.equals("real"); m.pixelType = FormatTools.pixelTypeFromBytes(bytes, signed, fp); LOGGER.info("Populating OME metadata"); MetadataStore store = makeFilterMetadata(); MetadataTools.populatePixels(store, this, true); // populate Image data if (imageName != null) { imageName = imageName.replace('/', File.separatorChar); imageName = imageName.replace('\\', File.separatorChar); imageName = imageName.substring(imageName.lastIndexOf(File.separator) + 1); store.setImageName(imageName, 0); } if (date != null) store.setImageAcquisitionDate(new Timestamp(date), 0); if (getMetadataOptions().getMetadataLevel() != MetadataLevel.MINIMUM) { store.setImageDescription(description, 0); // link Instrument and Image String instrumentID = MetadataTools.createLSID("Instrument", 0); store.setInstrumentID(instrumentID, 0); store.setMicroscopeModel(microscopeModel, 0); store.setMicroscopeManufacturer(microscopeManufacturer, 0); store.setImageInstrumentRef(instrumentID, 0); store.setExperimentID(MetadataTools.createLSID("Experiment", 0), 0); store.setExperimentType(MetadataTools.getExperimentType(experimentType), 0); // populate Dimensions data if (scales != null) { if (units != null && units.length == scales.length - 1) { // correct for missing units // sometimes, the units for the C axis are missing entirely ArrayList realUnits = new ArrayList(); int unitIndex = 0; for (int i=0; i= units.length) { realUnits.add("nm"); } else { realUnits.add(units[unitIndex++]); } } units = realUnits.toArray(new String[realUnits.size()]); } for (int i=0; i i ? axes[i] : ""; String unit = units != null && units.length > i ? units[i] : ""; if (axis.equals("x")) { if (checkUnit(unit, "um", "microns", "micrometers")) { Length x = FormatTools.getPhysicalSizeX(scale); if (x != null) { store.setPixelsPhysicalSizeX(x, 0); } } } else if (axis.equals("y")) { if (checkUnit(unit, "um", "microns", "micrometers")) { Length y = FormatTools.getPhysicalSizeY(scale); if (y != null) { store.setPixelsPhysicalSizeY(y, 0); } } } else if (axis.equals("z")) { if (checkUnit(unit, "um", "microns", "micrometers")) { Length z = FormatTools.getPhysicalSizeZ(scale); if (z != null) { store.setPixelsPhysicalSizeZ(z, 0); } } } else if (axis.equals("t") && scale != null) { if (checkUnit(unit, "ms")) { store.setPixelsTimeIncrement(new Time(scale, UNITS.MILLISECOND), 0); } else if (checkUnit(unit, "seconds") || checkUnit(unit, "s") ) { store.setPixelsTimeIncrement(new Time(scale, UNITS.SECOND), 0); } } } } else if (sizes != null) { if (sizes.length > 0) { Length x = FormatTools.getPhysicalSizeX(sizes[0]); if (x != null) { store.setPixelsPhysicalSizeX(x, 0); } } if (sizes.length > 1) { sizes[1] /= getSizeY(); Length y = FormatTools.getPhysicalSizeY(sizes[1]); if (y != null) { store.setPixelsPhysicalSizeY(y, 0); } } } // populate Plane data if (timestamps != null) { for (int t=0; t= getSizeT()) break; // ignore superfluous timestamps if (timestamps[t] == null) continue; // ignore missing timestamp Time deltaT = new Time(timestamps[t], UNITS.SECOND); if (Double.isNaN(deltaT.value().doubleValue())) continue; // ignore invalid timestamp // assign timestamp to all relevant planes for (int z=0; z 0) { store.setPlanePositionX(stagePos[0], 0, i); addGlobalMeta("X position for position #1", stagePos[0]); } if (stagePos.length > 1) { store.setPlanePositionY(stagePos[1], 0, i); addGlobalMeta("Y position for position #1", stagePos[1]); } if (stagePos.length > 2) { store.setPlanePositionZ(stagePos[2], 0, i); addGlobalMeta("Z position for position #1", stagePos[2]); } } } if (exposureTime != null) { for (int i=0; i tokens = new ArrayList(); boolean inWhiteSpace = true; boolean withinQuotes = false; StringBuilder token = null; for (int i = 0; i < line.length(); ++i) { char c = line.charAt(i); if (Character.isWhitespace(c) || c == 0x04) { if (withinQuotes) { // retain white space within quotes token.append(c); } else if (!inWhiteSpace) { // put out pending token string inWhiteSpace = true; if (token.length() > 0) { tokens.add(token.toString()); token = null; } } } else { if ('"' == c) { // toggle quotes withinQuotes = !withinQuotes; } if (inWhiteSpace) { inWhiteSpace = false; // start a new token string token = new StringBuilder(); } // build token string token.append(c); } } // put out any pending token strings if (null != token && token.length() > 0) { tokens.add(token.toString()); } return tokens.toArray(new String[0]); } /* Given a list of tokens and an array of lists of regular expressions, tries * to find a match. If no match is found, looks in OTHER_KEYS. */ String[] findKeyValue(String[] tokens, String[][] regexesArray) { String[] keyValue = findKeyValueForCategory(tokens, regexesArray); if (null == keyValue) { keyValue = findKeyValueOther(tokens, OTHER_KEYS); } if (null == keyValue) { String key = tokens[0]; String value = concatenateTokens(tokens, 1, tokens.length); keyValue = new String[] { key, value }; } return keyValue; } /* * Builds a string from a list of tokens. */ private String concatenateTokens(String[] tokens, int start, int stop) { final StringBuilder returnValue = new StringBuilder(); for (int i = start; i < tokens.length && i < stop; ++i) { returnValue.append(tokens[i]); if (i < stop - 1) { returnValue.append(' '); } } return returnValue.toString(); } /* * Given a list of tokens and an array of lists of regular expressions, finds * a match. Returns key/value pair if matched, null otherwise. * * The first element, tokens[0], has already been matched to a category, i.e. * 'history', and the regexesArray is category-specific. */ private String[] findKeyValueForCategory(String[] tokens, String[][] regexesArray) { String[] keyValue = null; for (String[] regexes : regexesArray) { if (compareTokens(tokens, 1, regexes, 0)) { int splitIndex = 1 + regexes.length; // add one for the category String key = concatenateTokens(tokens, 0, splitIndex); String value = concatenateTokens(tokens, splitIndex, tokens.length); keyValue = new String[] { key, value }; break; } } return keyValue; } /* Given a list of tokens and an array of lists of regular expressions, finds * a match. Returns key/value pair if matched, null otherwise. * * The first element, tokens[0], represents a category and is skipped. Look * for a match of a list of regular expressions anywhere in the list of tokens. */ private String[] findKeyValueOther(String[] tokens, String[][] regexesArray) { String[] keyValue = null; for (String[] regexes : regexesArray) { for (int i = 1; i < tokens.length - regexes.length; ++i) { // does token match first regex? if (tokens[i].toLowerCase().matches(regexes[0])) { // do remaining tokens match remaining regexes? if (1 == regexes.length || compareTokens(tokens, i + 1, regexes, 1)) { // if so, return key/value int splitIndex = i + regexes.length; String key = concatenateTokens(tokens, 0, splitIndex); String value = concatenateTokens(tokens, splitIndex, tokens.length); keyValue = new String[] { key, value }; break; } } } if (null != keyValue) { break; } } return keyValue; } /* * Compares a list of tokens with a list of regular expressions. */ private boolean compareTokens(String[] tokens, int tokenIndex, String[] regexes, int regexesIndex) { boolean returnValue = true; int i, j; for (i = tokenIndex, j = regexesIndex; j < regexes.length; ++i, ++j) { if (i >= tokens.length || !tokens[i].toLowerCase().matches(regexes[j])) { returnValue = false; break; } } return returnValue; } /** Splits the given string into a list of {@link Double}s. */ private Double[] splitDoubles(String v) { StringTokenizer t = new StringTokenizer(v); Double[] values = new Double[t.countTokens()]; for (int n=0; n