// // MicromanagerReader.java // /* OME Bio-Formats package for reading and converting biological file formats. Copyright (C) 2005-@year@ UW-Madison LOCI and Glencoe Software, Inc. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package loci.formats.in; import java.io.File; import java.io.IOException; import java.util.Arrays; import java.util.StringTokenizer; import java.util.Vector; import loci.common.DataTools; import loci.common.DateTools; import loci.common.Location; import loci.common.RandomAccessInputStream; import loci.common.xml.XMLTools; 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.PositiveFloat; import org.xml.sax.Attributes; import org.xml.sax.helpers.DefaultHandler; /** * MicromanagerReader is the file format reader for Micro-Manager files. * * <dl><dt><b>Source code:</b></dt> * <dd><a href="http://trac.openmicroscopy.org.uk/ome/browser/bioformats.git/components/bio-formats/src/loci/formats/in/MicromanagerReader.java">Trac</a>, * <a href="http://git.openmicroscopy.org/?p=bioformats.git;a=blob;f=components/bio-formats/src/loci/formats/in/MicromanagerReader.java;hb=HEAD">Gitweb</a></dd></dl> */ public class MicromanagerReader extends FormatReader { // -- Constants -- public static final String DATE_FORMAT = "EEE MMM dd HH:mm:ss zzz yyyy"; /** File containing extra metadata. */ private static final String METADATA = "metadata.txt"; /** * Optional file containing additional acquisition parameters. * (And yes, the spelling is correct.) */ private static final String XML = "Acqusition.xml"; // -- Fields -- /** Helper reader for TIFF files. */ private MinimalTiffReader tiffReader; /** List of TIFF files to open. */ private Vector<String> tiffs; private String metadataFile; private String xmlFile; private String[] channels; private String comment, time; private Double exposureTime, sliceThickness, pixelSize; private Double[] timestamps; private int gain; private String binning, detectorID, detectorModel, detectorManufacturer; private double temperature; private Vector<Double> voltage; private String cameraRef; private String cameraMode; // -- Constructor -- /** Constructs a new Micromanager reader. */ public MicromanagerReader() { super("Micro-Manager", new String[] {"tif", "tiff", "txt", "xml"}); domains = new String[] {FormatTools.LM_DOMAIN}; hasCompanionFiles = true; datasetDescription = "A 'metadata.txt' file plus or or more .tif files"; } // -- IFormatReader API methods -- /* @see loci.formats.IFormatReader#isSingleFile(String) */ public boolean isSingleFile(String id) throws FormatException, IOException { return false; } /* @see loci.formats.IFormatReader#isThisType(String, boolean) */ public boolean isThisType(String name, boolean open) { if (!open) return false; // not allowed to touch the file system if (name.equals(METADATA) || name.endsWith(File.separator + METADATA) || name.equals(XML) || name.endsWith(File.separator + XML)) { try { RandomAccessInputStream stream = new RandomAccessInputStream(name); long length = stream.length(); stream.close(); return length > 0; } catch (IOException e) { return false; } } try { Location parent = new Location(name).getAbsoluteFile().getParentFile(); Location metaFile = new Location(parent, METADATA); RandomAccessInputStream s = new RandomAccessInputStream(name); boolean validTIFF = isThisType(s); s.close(); return metaFile.exists() && metaFile.length() > 0 && validTIFF; } catch (NullPointerException e) { } catch (IOException e) { } return false; } /* @see loci.formats.IFormatReader#fileGroupOption(String) */ public int fileGroupOption(String id) throws FormatException, IOException { return FormatTools.MUST_GROUP; } /* @see loci.formats.IFormatReader#isThisType(RandomAccessInputStream) */ public boolean isThisType(RandomAccessInputStream stream) throws IOException { if (tiffReader == null) tiffReader = new MinimalTiffReader(); return tiffReader.isThisType(stream); } /* @see loci.formats.IFormatReader#getSeriesUsedFiles(boolean) */ public String[] getSeriesUsedFiles(boolean noPixels) { FormatTools.assertId(currentId, true, 1); Vector<String> files = new Vector<String>(); files.add(metadataFile); if (xmlFile != null) { files.add(xmlFile); } if (!noPixels) files.addAll(tiffs); return files.toArray(new String[files.size()]); } /** * @see loci.formats.IFormatReader#openBytes(int, byte[], int, int, int, int) */ 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); if (new Location(tiffs.get(no)).exists()) { tiffReader.setId(tiffs.get(no)); return tiffReader.openBytes(0, buf, x, y, w, h); } LOGGER.warn("File for image #{} ({}) is missing.", no, tiffs.get(no)); return buf; } /* @see loci.formats.IFormatReader#close(boolean) */ public void close(boolean fileOnly) throws IOException { super.close(fileOnly); if (tiffReader != null) tiffReader.close(fileOnly); if (!fileOnly) { tiffReader = null; tiffs = null; comment = time = null; exposureTime = sliceThickness = pixelSize = null; timestamps = null; metadataFile = null; channels = null; gain = 0; binning = detectorID = detectorModel = detectorManufacturer = null; temperature = 0; voltage = null; cameraRef = cameraMode = null; xmlFile = null; } } /* @see loci.formats.IFormatReader#getOptimalTileWidth() */ public int getOptimalTileWidth() { FormatTools.assertId(currentId, true, 1); return tiffReader.getOptimalTileWidth(); } /* @see loci.formats.IFormatReader#getOptimalTileHeight() */ public int getOptimalTileHeight() { FormatTools.assertId(currentId, true, 1); return tiffReader.getOptimalTileWidth(); } // -- Internal FormatReader API methods -- /* @see loci.formats.FormatReader#initFile(String) */ public void initFile(String id) throws FormatException, IOException { super.initFile(id); tiffReader = new MinimalTiffReader(); LOGGER.info("Reading metadata file"); // find metadata.txt Location file = new Location(currentId).getAbsoluteFile(); Location parentFile = file.getParentFile(); metadataFile = METADATA; String parent = ""; if (file.exists()) { metadataFile = new Location(parentFile, METADATA).getAbsolutePath(); parent = parentFile.getAbsolutePath() + File.separator; } in = new RandomAccessInputStream(metadataFile); // usually a small file, so we can afford to read it into memory String s = DataTools.readFile(metadataFile); LOGGER.info("Finding image file names"); // find the name of a TIFF file String baseTiff = null; tiffs = new Vector<String>(); int pos = 0; while (true) { pos = s.indexOf("FileName", pos); if (pos == -1 || pos >= in.length()) break; String name = s.substring(s.indexOf(":", pos), s.indexOf(",", pos)); baseTiff = parent + name.substring(3, name.length() - 1); pos++; } // now parse the rest of the metadata // metadata.txt looks something like this: // // { // "Section Name": { // "Key": "Value", // "Array key": [ // first array value, second array value // ] // } // // } LOGGER.info("Populating metadata"); Vector<Double> stamps = new Vector<Double>(); voltage = new Vector<Double>(); StringTokenizer st = new StringTokenizer(s, "\n"); int[] slice = new int[3]; while (st.hasMoreTokens()) { String token = st.nextToken().trim(); boolean open = token.indexOf("[") != -1; boolean closed = token.indexOf("]") != -1; if (open || (!open && !closed && !token.equals("{") && !token.startsWith("}"))) { int quote = token.indexOf("\"") + 1; String key = token.substring(quote, token.indexOf("\"", quote)); String value = null; if (open == closed) { value = token.substring(token.indexOf(":") + 1); } else if (!closed) { StringBuffer valueBuffer = new StringBuffer(); while (!closed) { token = st.nextToken(); closed = token.indexOf("]") != -1; valueBuffer.append(token); } value = valueBuffer.toString(); value = value.replaceAll("\n", ""); } if (value == null) continue; int startIndex = value.indexOf("["); int endIndex = value.indexOf("]"); if (endIndex == -1) endIndex = value.length(); value = value.substring(startIndex + 1, endIndex).trim(); if (value.length() == 0) { continue; } value = value.substring(0, value.length() - 1); value = value.replaceAll("\"", ""); if (value.endsWith(",")) value = value.substring(0, value.length() - 1); addGlobalMeta(key, value); if (key.equals("Channels")) core[0].sizeC = Integer.parseInt(value); else if (key.equals("ChNames")) { channels = value.split(","); for (int q=0; q<channels.length; q++) { channels[q] = channels[q].replaceAll("\"", "").trim(); } } else if (key.equals("Frames")) { core[0].sizeT = Integer.parseInt(value); } else if (key.equals("Slices")) { core[0].sizeZ = Integer.parseInt(value); } else if (key.equals("PixelSize_um")) { pixelSize = new Double(value); } else if (key.equals("z-step_um")) { sliceThickness = new Double(value); } else if (key.equals("Time")) time = value; else if (key.equals("Comment")) comment = value; } if (token.startsWith("\"FrameKey")) { int dash = token.indexOf("-") + 1; int nextDash = token.indexOf("-", dash); slice[2] = Integer.parseInt(token.substring(dash, nextDash)); dash = nextDash + 1; nextDash = token.indexOf("-", dash); slice[1] = Integer.parseInt(token.substring(dash, nextDash)); dash = nextDash + 1; slice[0] = Integer.parseInt(token.substring(dash, token.indexOf("\"", dash))); token = st.nextToken().trim(); String key = "", value = ""; boolean valueArray = false; while (!token.startsWith("}")) { if (valueArray) { if (token.trim().equals("],")) { valueArray = false; } else { value += token.trim().replaceAll("\"", ""); token = st.nextToken().trim(); continue; } } else { int colon = token.indexOf(":"); key = token.substring(1, colon).trim(); value = token.substring(colon + 1, token.length() - 1).trim(); key = key.replaceAll("\"", ""); value = value.replaceAll("\"", ""); if (token.trim().endsWith("[")) { valueArray = true; token = st.nextToken().trim(); continue; } } addGlobalMeta(key, value); if (key.equals("Exposure-ms")) { double t = Double.parseDouble(value); exposureTime = new Double(t / 1000); } else if (key.equals("ElapsedTime-ms")) { double t = Double.parseDouble(value); stamps.add(new Double(t / 1000)); } else if (key.equals("Core-Camera")) cameraRef = value; else if (key.equals(cameraRef + "-Binning")) { if (value.indexOf("x") != -1) binning = value; else binning = value + "x" + value; } else if (key.equals(cameraRef + "-CameraID")) detectorID = value; else if (key.equals(cameraRef + "-CameraName")) detectorModel = value; else if (key.equals(cameraRef + "-Gain")) { gain = (int) Double.parseDouble(value); } else if (key.equals(cameraRef + "-Name")) { detectorManufacturer = value; } else if (key.equals(cameraRef + "-Temperature")) { temperature = Double.parseDouble(value); } else if (key.equals(cameraRef + "-CCDMode")) { cameraMode = value; } else if (key.startsWith("DAC-") && key.endsWith("-Volts")) { voltage.add(new Double(value)); } token = st.nextToken().trim(); } } } timestamps = stamps.toArray(new Double[stamps.size()]); Arrays.sort(timestamps); // look for the optional companion XML file parentFile = new Location(currentId).getAbsoluteFile().getParentFile(); if (new Location(parentFile, XML).exists()) { xmlFile = new Location(parent, XML).getAbsolutePath(); parseXMLFile(); } // build list of TIFF files buildTIFFList(baseTiff); if (tiffs.size() == 0) { Vector<String> uniqueZ = new Vector<String>(); Vector<String> uniqueC = new Vector<String>(); Vector<String> uniqueT = new Vector<String>(); Location dir = new Location(currentId).getAbsoluteFile().getParentFile(); String[] files = dir.list(true); Arrays.sort(files); for (String f : files) { if (checkSuffix(f, "tif") || checkSuffix(f, "tiff")) { String[] blocks = f.split("_"); if (!uniqueT.contains(blocks[1])) uniqueT.add(blocks[1]); if (!uniqueC.contains(blocks[2])) uniqueC.add(blocks[2]); if (!uniqueZ.contains(blocks[3])) uniqueZ.add(blocks[3]); tiffs.add(new Location(dir, f).getAbsolutePath()); } } core[0].sizeZ = uniqueZ.size(); core[0].sizeC = uniqueC.size(); core[0].sizeT = uniqueT.size(); if (tiffs.size() == 0) { throw new FormatException("Could not find TIFF files."); } } tiffReader.setId(tiffs.get(0)); if (getSizeZ() == 0) core[0].sizeZ = 1; if (getSizeT() == 0) core[0].sizeT = tiffs.size() / getSizeC(); core[0].sizeX = tiffReader.getSizeX(); core[0].sizeY = tiffReader.getSizeY(); core[0].dimensionOrder = "XYZCT"; core[0].pixelType = tiffReader.getPixelType(); core[0].rgb = tiffReader.isRGB(); core[0].interleaved = false; core[0].littleEndian = tiffReader.isLittleEndian(); core[0].imageCount = getSizeZ() * getSizeC() * getSizeT(); core[0].indexed = false; core[0].falseColor = false; core[0].metadataComplete = true; MetadataStore store = makeFilterMetadata(); MetadataTools.populatePixels(store, this, true); if (time != null) { String date = DateTools.formatDate(time, DATE_FORMAT); store.setImageAcquiredDate(date, 0); } else MetadataTools.setDefaultCreationDate(store, id, 0); if (getMetadataOptions().getMetadataLevel() != MetadataLevel.MINIMUM) { store.setImageDescription(comment, 0); // link Instrument and Image String instrumentID = MetadataTools.createLSID("Instrument", 0); store.setInstrumentID(instrumentID, 0); store.setImageInstrumentRef(instrumentID, 0); for (int i=0; i<channels.length; i++) { store.setChannelName(channels[i], 0, i); } if (pixelSize != null && pixelSize > 0) { store.setPixelsPhysicalSizeX(new PositiveFloat(pixelSize), 0); store.setPixelsPhysicalSizeY(new PositiveFloat(pixelSize), 0); } if (sliceThickness != null && sliceThickness > 0) { store.setPixelsPhysicalSizeZ(new PositiveFloat(sliceThickness), 0); } for (int i=0; i<getImageCount(); i++) { store.setPlaneExposureTime(exposureTime, 0, i); if (i < timestamps.length) { store.setPlaneDeltaT(timestamps[i], 0, i); } } String serialNumber = detectorID; detectorID = MetadataTools.createLSID("Detector", 0, 0); for (int i=0; i<channels.length; i++) { store.setDetectorSettingsBinning(getBinning(binning), 0, i); store.setDetectorSettingsGain(new Double(gain), 0, i); if (i < voltage.size()) { store.setDetectorSettingsVoltage(voltage.get(i), 0, i); } store.setDetectorSettingsID(detectorID, 0, i); } store.setDetectorID(detectorID, 0, 0); if (detectorModel != null) { store.setDetectorModel(detectorModel, 0, 0); } if (serialNumber != null) { store.setDetectorSerialNumber(serialNumber, 0, 0); } if (detectorManufacturer != null) { store.setDetectorManufacturer(detectorManufacturer, 0, 0); } if (cameraMode == null) cameraMode = "Other"; store.setDetectorType(getDetectorType(cameraMode), 0, 0); store.setImagingEnvironmentTemperature(temperature, 0); } } // -- Helper methods -- /** * Populate the list of TIFF files using the given file name as a pattern. */ private void buildTIFFList(String baseTiff) { LOGGER.info("Building list of TIFFs"); String prefix = ""; if (baseTiff.indexOf(File.separator) != -1) { prefix = baseTiff.substring(0, baseTiff.lastIndexOf(File.separator) + 1); baseTiff = baseTiff.substring(baseTiff.lastIndexOf(File.separator) + 1); } String[] blocks = baseTiff.split("_"); StringBuffer filename = new StringBuffer(); for (int t=0; t<getSizeT(); t++) { for (int c=0; c<getSizeC(); c++) { for (int z=0; z<getSizeZ(); z++) { // file names are of format: // img_<T>_<channel name>_<T>.tif filename.append(prefix); filename.append(blocks[0]); filename.append("_"); int zeros = blocks[1].length() - String.valueOf(t).length(); for (int q=0; q<zeros; q++) { filename.append("0"); } filename.append(t); filename.append("_"); filename.append(channels[c]); filename.append("_"); zeros = blocks[3].length() - String.valueOf(z).length() - 4; for (int q=0; q<zeros; q++) { filename.append("0"); } filename.append(z); filename.append(".tif"); tiffs.add(filename.toString()); filename.delete(0, filename.length()); } } } } /** Parse metadata values from the Acqusition.xml file. */ private void parseXMLFile() throws IOException { String xmlData = DataTools.readFile(xmlFile); xmlData = XMLTools.sanitizeXML(xmlData); DefaultHandler handler = new MicromanagerHandler(); XMLTools.parseXML(xmlData, handler); } // -- Helper classes -- /** SAX handler for parsing Acqusition.xml. */ class MicromanagerHandler extends DefaultHandler { public void startElement(String uri, String localName, String qName, Attributes attributes) { if (qName.equals("entry")) { String key = attributes.getValue("key"); String value = attributes.getValue("value"); addGlobalMeta(key, value); } } } }