/*#%L * OME Bio-Formats package for reading and converting biological file formats. * %% * Copyright (C) 2005 - 2015 Open Microscopy Environment: * - Board of Regents of the University of Wisconsin-Madison * - Glencoe Software, Inc. * - University of Dundee * %% * 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, see * <http://www.gnu.org/licenses/gpl-2.0.html>. * #L% */ package loci.formats.in; import java.io.IOException; import java.nio.ByteOrder; import java.util.ArrayList; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ome.xml.model.enums.DimensionOrder; import loci.common.IRandomAccess; 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; /** * @author Lee Kamentsky * * This is a FormatReader for the Perkin-Elmer Nuance * line of multispectral imaging microscopes' .im3 * file format. The .im3 format stores 63 planes * representing intensity measurements taken on * separate spectral bands. The pixel intensity at * a single band might be used as a proxy for * fluorophore presence, but the best results are * obtained if the whole spectrum is used to * synthesize an intensity. The most efficient * strategy is to use IM3Reader.openRaw() to * fetch the entire image which may then be scanned * sequentially to create one planar image * per desired signal. * * IM3Reader may be run as a Java application to * dump a per-record description of the file to * standard output */ public class IM3Reader extends FormatReader { /** * Logger for outputting summary diagnostics. */ private static final Logger LOGGER = LoggerFactory.getLogger(IM3Reader.class); /** * First 4 bytes of file is "1985" */ private static final int COOKIE = 1985; /** * A record that is a container of * other records. */ private static final int REC_CONTAINER = 0; /** * Image format * int: pixel type? 3 = RGBA * int: width * int: height * int: depth * data bytes follow */ @SuppressWarnings("unused") private static final int REC_IMAGE = 1; /** * A nested record of records */ @SuppressWarnings("unused") private static final int REC_NESTED = 3; /** * 32-bit integer values */ private static final int REC_INT = 6; /* * Floating point values */ private static final int REC_FLOAT = 7; private static final int REC_BOOLEAN = 9; /** * A string * int: ? * int: length * 8-byte characters follow */ private static final int REC_STRING=10; /* * Container fields. */ private static final String FIELD_DATA_SET="DataSet"; @SuppressWarnings("unused") private static final String FIELD_TIMESTAMP="TimeStamp"; @SuppressWarnings("unused") private static final String FIELD_AUX_FLAGS="AuxFlags"; @SuppressWarnings("unused") private static final String FIELD_NUANCE_FLAGS="NuanceFlags"; private static final String FIELD_SPECTRA="Spectra"; private static final String FIELD_VALUES="Values"; @SuppressWarnings("unused") private static final String FIELD_PROTOCOL="Protocol"; @SuppressWarnings("unused") private static final String FIELD_OBJECTIVE="Objective"; @SuppressWarnings("unused") private static final String FIELD_SPECTRAL_BASIS_INFO="SpectralBasisInfo"; @SuppressWarnings("unused") private static final String FIELD_FILTER_PAIR="FilterPair"; @SuppressWarnings("unused") private static final String FIELD_FIXED_FILTER="FixedFilter"; @SuppressWarnings("unused") private static final String FIELD_BANDS="Bands"; private static final String FIELD_SPECTRAL_LIBRARY="SpectralLibrary"; private static final String FIELD_SPECTRUM="Spectrum"; /* * Image fields */ @SuppressWarnings("unused") private static final String FIELD_THUMBNAIL="Thumbnail"; private static final String FIELD_DATA="Data"; /* * Int fields */ @SuppressWarnings("unused") private static final String FIELD_FILE_VERSION="FileVersion"; @SuppressWarnings("unused") private static final String FIELD_CLASS_ID="ClassID"; @SuppressWarnings("unused") private static final String FIELD_TYPE_ID="TypeID"; private static final String FIELD_SHAPE="Shape"; @SuppressWarnings("unused") private static final String FIELD_BAND_INDEX="BandIndex"; /* * String fields */ private static final String FIELD_NAME="Name"; @SuppressWarnings("unused") private static final String FIELD_SAMPLE_ID="SampleID"; @SuppressWarnings("unused") private static final String FIELD_USER_COMMENTS="UserComments"; @SuppressWarnings("unused") private static final String FIELD_SOURCE_FILE_NAME="SourceFileName"; @SuppressWarnings("unused") private static final String FIELD_PROXY_PARENT_FILE_NAME="ProxyParentFileName"; @SuppressWarnings("unused") private static final String FIELD_MANUFACTURER="Manufacturer"; @SuppressWarnings("unused") private static final String FIELD_PART_NUMBER="PartNumber"; /* * Float fields */ @SuppressWarnings("unused") private static final String FIELD_MILLIMETERS_PER_PIXEL="MillimetersPerPixel"; @SuppressWarnings("unused") private static final String FIELD_EXPOSURE="Exposure"; @SuppressWarnings("unused") private static final String FIELD_WAVELENGTH="Wavelength"; private static final String FIELD_WAVELENGTHS="Wavelengths"; private static final String FIELD_MAGNITUDES="Magnitudes"; /* * Misc fields */ @SuppressWarnings("unused") private static final String FIELD_HOMOGENEOUS="Homogeneous"; /* * Records for the current file */ private List<IM3Record> records = new ArrayList<IM3Record>(); /* * Data sets for the current file */ private List<ContainerRecord> dataSets = new ArrayList<ContainerRecord>(); /* * Spectrum records for a spectral library */ private List<Spectrum> spectra = new ArrayList<Spectrum>(); /* * The data from the current series' file. */ private byte [] data; /** * Construct an uninitialized reader of .im3 files. */ public IM3Reader() { super("Perkin-Elmer Nuance IM3", "im3"); } @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); if (data == null) { data = openRaw(); } if (data == null) return null; final int srcWidth = getSizeX(); final int srcChannels = getSizeC(); int idx = 0; int offset = ((x + y * srcWidth) * srcChannels + no) * 2; for (int hidx=0; hidx < h; hidx++) { int roffset = offset + hidx * srcWidth * srcChannels * 2; for (int widx=0; widx < w; widx++) { buf[idx++] = data[roffset]; buf[idx++] = data[roffset+1]; roffset += srcChannels * 2; } } return buf; } /** * Open the current series in raw-mode, returning the * interleaved image bytes. The data format for a pixel is * a run of 63 unsigned short little endian values. * * @return a byte array containing the data organized by * spectral channel, then x, then y. Returns null * if, for some incomprehensible reason, the DATA * block was missing. * @throws IOException */ public byte [] openRaw() throws IOException { IRandomAccess is = Location.getHandle(getCurrentFile(), false); try { is.setOrder(ByteOrder.LITTLE_ENDIAN); final ContainerRecord dataSet = dataSets.get(getSeries()); for (IM3Record subRec:dataSet.parseChunks(is)){ if (subRec.name.equals(FIELD_DATA)) { is.seek(subRec.offset+4); int width = is.readInt(); int height = is.readInt(); int channels = is.readInt(); final byte [] result = new byte [width * height * channels * 2]; is.read(result); return result; } } } finally { is.close(); } return null; } /** * If a Nuance file is a spectral library set (.csl) file, * there is a spectral library inside that contains a profile * of spectral bin magnitudes for each of several measured * fluorophores (or auto fluorescence). This method finds * the spectral library and returns the spectra inside. * * @return a list of the Spectrum records contained in the library */ public List<Spectrum> getSpectra() { return spectra; } /* (non-Javadoc) * @see loci.formats.FormatReader#initFile(java.lang.String) */ @Override protected void initFile(String id) throws FormatException, IOException { super.initFile(id); core.clear(); IRandomAccess is = Location.getHandle(id, false); try { is.setOrder(ByteOrder.LITTLE_ENDIAN); final int cookie = is.readInt(); if (cookie != COOKIE) { throw new FormatException(String.format("Expected file cookie of %d, but got %d.", COOKIE, cookie)); } long fileLength = is.length(); while (is.getFilePointer() < fileLength) { final IM3Record rec = parseRecord(is); if (rec == null) { if (is.getFilePointer() > fileLength-16) break; /* * # of bytes in chunk. */ @SuppressWarnings("unused") final int chunkLength = is.readInt(); /* * Is always zero? Chunk #? */ @SuppressWarnings("unused") final int unknown = is.readInt(); /* * Is always one? Chunk #? */ @SuppressWarnings("unused") final int unknown1 = is.readInt(); /* * # of records to follow */ @SuppressWarnings("unused") final int nRecords = is.readInt(); } else { if (rec instanceof ContainerRecord) { final ContainerRecord bRec = (ContainerRecord)rec; for (IM3Record subDS:bRec.parseChunks(is)) { if ((subDS instanceof ContainerRecord) && (subDS.name.equals(FIELD_DATA_SET))) { final ContainerRecord bSubDS = (ContainerRecord)subDS; for (IM3Record subSubDS:bSubDS.parseChunks(is)) { if (subSubDS instanceof ContainerRecord) { final ContainerRecord bDataSet = (ContainerRecord) subSubDS; dataSets.add(bDataSet); List<IM3Record> subRecs = bDataSet.parseChunks(is); final CoreMetadata cm = new CoreMetadata(); cm.dimensionOrder = DimensionOrder.XYCZT.getValue(); cm.littleEndian = true; // TODO: Detect pixel type cm.pixelType = FormatTools.UINT16; for (IM3Record subRec:subRecs){ if (subRec.name.equals(FIELD_SHAPE) && (subRec instanceof IntIM3Record)) { final IntIM3Record iRec = (IntIM3Record)subRec; cm.sizeX = iRec.getEntry(is, 0); cm.sizeY = iRec.getEntry(is, 1); cm.sizeC = iRec.getEntry(is, 2); cm.sizeZ = 1; cm.sizeT = 1; cm.imageCount = cm.sizeC; cm.metadataComplete = true; } } core.add(cm); } } } else if ((subDS instanceof ContainerRecord) && subDS.name.equals(FIELD_SPECTRAL_LIBRARY)) { /* * SpectralLibrary * (unnamed container record) * Spectra * Keys (integers) * Values * (unnamed container record for spectrum #1) * (unnamed container record for spectrum #2)... * */ for (IM3Record slContainer:((ContainerRecord)subDS).parseChunks(is)) { /* unnamed container */ if (slContainer instanceof ContainerRecord) { for (IM3Record slSpectra:((ContainerRecord)slContainer).parseChunks(is)) { if ((slSpectra instanceof ContainerRecord) && (slSpectra.name.equals(FIELD_SPECTRA))) { for (IM3Record slRec:((ContainerRecord)slSpectra).parseChunks(is)) { if (slRec.name.equals(FIELD_VALUES) && (slRec instanceof ContainerRecord)) { for (IM3Record spectrumRec:((ContainerRecord)slRec).parseChunks(is)) { if (spectrumRec instanceof ContainerRecord) { spectra.add(new Spectrum(is, (ContainerRecord)spectrumRec)); } } } } } } } } } } } records.add(rec); } } } finally { is.close(); } MetadataStore store = makeFilterMetadata(); MetadataTools.populatePixels(store, this); } static private final String EMPTY_STRING = new String(); /** * Parse a string from the IM3 file at the current file pointer loc * * @param is stream to read from * @return parsed string or null for string of zero length * @throws IOException */ static protected String parseString(IRandomAccess is) throws IOException { final int nameLength = is.readInt(); if (nameLength == 0) return EMPTY_STRING; final byte [] buf = new byte [nameLength]; is.read(buf); return new String(buf, loci.common.Constants.ENCODING); } /** * Parse an IM3 record at the current file pointer location * * @param is random access stream, pointing at the record's start * (the length-quadword of the record's tag name) * @return an IM3Record or subclass depending on the record's type * @throws IOException on file misparsing leading to overrun and other */ private static IM3Record parseRecord(IRandomAccess is) throws IOException { final String name = parseString(is); if (name == null) return null; final int recLength = is.readInt()-8; final int recType = is.readInt(); final long offset = is.getFilePointer(); is.skipBytes(recLength); switch(recType) { case REC_CONTAINER: return new ContainerRecord(name, recType, offset, recLength); case REC_STRING: return new StringIM3Record(name, recType, offset, recLength); case REC_INT: return new IntIM3Record(name, recType, offset, recLength); case REC_FLOAT: return new FloatIM3Record(name, recType, offset, recLength); case REC_BOOLEAN: return new BooleanIM3Record(name, recType, offset, recLength); } return new IM3Record(name, recType, offset, recLength); } /* (non-Javadoc) * @see loci.formats.FormatReader#isThisType(loci.common.RandomAccessInputStream) */ @Override public boolean isThisType(RandomAccessInputStream stream) throws IOException { stream.seek(0); return (stream.readInt() == COOKIE); } protected static class IM3Record { final String name; final int type; final long offset; final int length; IM3Record(String name, int type, long offset, int length) { this.name = name; this.type = type; this.offset=offset; this.length=length; } /** * Write a summary of the contents of the record * * @param is * @throws IOException */ public void writeSummary(IRandomAccess is, String indentation) throws IOException { is.seek(offset); LOGGER.info(indentation + toString()); for (int i=0; (i<length) && (i < 256); i+= 32) { StringBuilder msg = new StringBuilder(indentation + String.format("%02x:", i)); for (int j=i;(j < length) &&(j < i+32); j++) { msg.append(String.format(" %02x", is.readByte())); } LOGGER.info(msg.toString()); } } /* (non-Javadoc) * @see java.lang.Object#toString() */ @Override public String toString() { return String.format("[%s: type=%d, offset=%d, length=%d]", name, type, offset, length); } } /** * @author Lee Kamentsky * * A Container3Record is a nesting container for * other records. In the IM3 format, records are often grouped * under a ContainerRecord with a blank tagname. */ protected static class ContainerRecord extends IM3Record { ContainerRecord(String name, int type, long offset, int length) { super(name, type, offset, length); } /** * Parse and return the sub-records for the record container * * @param is * @return a list of sub-records * @throws IOException */ List<IM3Record> parseChunks(IRandomAccess is) throws IOException { long oldOffset = is.getFilePointer(); is.seek(offset+8); long end = offset+length; List<IM3Record> recs = new ArrayList<IM3Record>(); while(is.getFilePointer() < end-8) { final IM3Record rec = parseRecord(is); if (rec != null) recs.add(rec); } is.seek(oldOffset); return recs; } /* (non-Javadoc) * @see loci.formats.in.IM3Reader.IM3Record#writeSummary(loci.common.IRandomAccess, java.lang.String) */ public void writeSummary(IRandomAccess is, String indentation) throws IOException { is.seek(offset); LOGGER.info(indentation + toString()); for (IM3Record rec:parseChunks(is)) { rec.writeSummary(is, indentation + " "); } } } /** * @author Lee Kamentsky * * An integer array record */ protected static class IntIM3Record extends IM3Record { public IntIM3Record(String name, int recType, long offset, int recLength) { super(name, recType, offset, recLength); } /** * Get the # of integer values in this record * @param is * * @return number of integer values contained in the record * @throws IOException */ public int getNumEntries(IRandomAccess is) throws IOException { long oldPos = is.getFilePointer(); try { is.seek(offset); final int code = is.readInt(); if (code == 0) return 1; return is.readInt(); } finally { is.seek(oldPos); } } /** * Get the integer value at the given index * * @param is the stream for the IM3 file * @param index the zero-based index of the entry to retrieve * @return the value stored in the indexed slot of the record * @throws IOException */ public int getEntry(IRandomAccess is, int index) throws IOException { long oldPos = is.getFilePointer(); try { is.seek(offset); if (is.readInt() == 0) return is.readInt(); is.seek(offset+index*4+8); return is.readInt(); } finally { is.seek(oldPos); } } /* (non-Javadoc) * @see loci.formats.in.IM3Reader.IM3Record#writeSummary(loci.common.IRandomAccess, java.lang.String) */ public void writeSummary(IRandomAccess is, String indentation) throws IOException { is.seek(offset); LOGGER.info(indentation + toString()); final int length = getNumEntries(is); for (int i=0; (i < length) && (i < 256); i+=16) { StringBuilder msg = new StringBuilder(indentation + String.format("%02x:", i)); for (int j=i; (j<i+16) && (j<length); j++) { msg.append(String.format(" %7d", getEntry(is, j))); } LOGGER.info(msg.toString()); } } } /** * @author Lee Kamentsky * * A 4-byte floating point array record * * The record format is * int32 - unknown * int32 - # of floats * float32s - values */ protected static class FloatIM3Record extends IM3Record { public FloatIM3Record(String name, int recType, long offset, int recLength) { super(name, recType, offset, recLength); } /** * Get the # of floating-point values in this record * * @return number of integer values contained in the record * @throws IOException */ public int getNumEntries(IRandomAccess is) throws IOException { long oldPos = is.getFilePointer(); try { is.seek(offset); if (is.readInt() == 0) return 1; return is.readInt(); } finally { is.seek(oldPos); } } /** * Get the floating-point value at the given index * * @param is the stream for the IM3 file * @param index the zero-based index of the entry to retrieve * @return the value stored in the indexed slot of the record * @throws IOException */ public float getEntry(IRandomAccess is, int index) throws IOException { long oldPos = is.getFilePointer(); try { is.seek(offset); if (is.readInt() == 0) return is.readFloat(); is.seek(offset+8+index*4); return is.readFloat(); } finally { is.seek(oldPos); } } /** * Return all entries as an array * @param is handle to file * @return an array of the stored values * @throws IOException */ public float [] getEntries(IRandomAccess is) throws IOException { final long oldPos = is.getFilePointer(); try { float [] values = new float[getNumEntries(is)]; is.seek(offset+8); for (int index=0; index < values.length; index++) { values[index] = is.readFloat(); } return values; } finally { is.seek(oldPos); } } /* (non-Javadoc) * @see loci.formats.in.IM3Reader.IM3Record#writeSummary(loci.common.IRandomAccess, java.lang.String) */ public void writeSummary(IRandomAccess is, String indentation) throws IOException { is.seek(offset); LOGGER.info(indentation + toString()); final int length = getNumEntries(is); for (int i=0; (i < length) && (i < 256); i+=16) { StringBuilder msg = new StringBuilder(indentation + String.format("%02x:", i)); for (int j=i; (j<i+16) && (j<length); j++) { msg.append(String.format(" %4.4f", getEntry(is, j))); } LOGGER.info(msg.toString()); } } } /** * A record containing boolean values * * @author Lee Kamentsky * */ protected static class BooleanIM3Record extends IM3Record { public BooleanIM3Record(String name, int recType, long offset, int recLength) { super(name, recType, offset, recLength); } /** * Get the # of boolean values in this record * * @return number of boolean values contained in the record * @throws IOException */ public int getNumEntries(IRandomAccess is) throws IOException { long oldPos = is.getFilePointer(); try { is.seek(offset+4); return is.readInt(); } finally { is.seek(oldPos); } } /** * Get the boolean value at the given index * * @param is the stream for the IM3 file * @param index the zero-based index of the entry to retrieve * @return the value stored in the indexed slot of the record * @throws IOException */ public boolean getEntry(IRandomAccess is, int index) throws IOException { long oldPos = is.getFilePointer(); try { is.seek(offset+8+index); return (is.readByte() != 0); } finally { is.seek(oldPos); } } } /** * @author Lee Kamentsky * * A record whose value is a string. */ protected static class StringIM3Record extends IM3Record { public StringIM3Record(String name, int recType, long offset, int recLength) { super(name, recType, offset, recLength); } /** * Return the string value for this record * * @param is an open handle on the .IM3 file * @return the string value stored in the record * @throws IOException */ public String getValue(IRandomAccess is) throws IOException { final long oldPos = is.getFilePointer(); try { is.seek(offset+4); return parseString(is); } finally { is.seek(oldPos); } } /* (non-Javadoc) * @see loci.formats.in.IM3Reader.IM3Record#writeSummary(loci.common.IRandomAccess, java.lang.String) */ @Override public void writeSummary(IRandomAccess is, String indentation) throws IOException { LOGGER.info(indentation + toString()); LOGGER.info(indentation + String.format("Value = %s", getValue(is))); } } /** * @author Lee Kamentsky * * Represents a Spectrum record within a SpectralLibrary * */ static public class Spectrum { private String name; private float [] wavelengths; private float [] magnitudes; /** * Construct a spectrum by parsing a Spectrum record * * @param is file handle to the file being parsed * @param rec the container record grouping the spectrum's record * @throws IOException */ Spectrum(IRandomAccess is, ContainerRecord rec) throws IOException { /* * The record format is a nested container record containing * * Name - the name of the spectrum * The spectrum container record * A nesting record * Wavelengths - the wavelengths of the spectral components * Magnitudes - the measured signal magnitudes of the fluorophore * more stuff like calibration * Color - a nested record containing the RGB values for display * Selected - a boolean record that tells whether or not the spectrum is selected * more stuff like acquisition settings */ final long oldPos = is.getFilePointer(); try { for (IM3Record subRec:rec.parseChunks(is)) { if (subRec.name.equals(FIELD_NAME) && (subRec instanceof StringIM3Record) ) { name = ((StringIM3Record)subRec).getValue(is); } else if (subRec.name.equals(FIELD_SPECTRUM) && (subRec instanceof ContainerRecord)) { parseSpectrumRecord(is, (ContainerRecord)subRec); } } } finally { is.seek(oldPos); } } /** * @return the name of this spectrum or null if unnamed */ public String getName() { return name; } /** * @return the wavelengths for each of the spectral bins */ public float [] getWavelengths() { return wavelengths; } /** * @return the magnitudes of the signals for each of the spectral bins */ public float [] getMagnitudes() { return magnitudes; } /** * Parse the spectrum record * * @param is the file handle * @param rec the spectrum container record * @throws IOException */ private void parseSpectrumRecord(IRandomAccess is, ContainerRecord rec) throws IOException { for (IM3Record subRec:rec.parseChunks(is)) { if (subRec instanceof ContainerRecord) { for (IM3Record subSubRec:((ContainerRecord)subRec).parseChunks(is)) { if (subSubRec.name.equals(FIELD_WAVELENGTHS) && (subSubRec instanceof FloatIM3Record)) { wavelengths = ((FloatIM3Record)subSubRec).getEntries(is); } else if (subSubRec.name.equals(FIELD_MAGNITUDES) && (subSubRec instanceof FloatIM3Record)) { magnitudes = ((FloatIM3Record)subSubRec).getEntries(is); } } } } } } /* (non-Javadoc) * @see loci.formats.FormatReader#setSeries(int) */ @Override public void setSeries(int no) { super.setSeries(no); data = null; } /* (non-Javadoc) * @see loci.formats.FormatReader#close(boolean) */ @Override public void close(boolean fileOnly) throws IOException { super.close(fileOnly); if (!fileOnly) { data = null; if (records != null) { records.clear(); } if (spectra != null) { spectra.clear(); } if (dataSets != null) { dataSets.clear(); } } } /** * Write a summary of each field in the IM3 file to the writer * @throws IOException */ public void writeSummary() throws IOException { IRandomAccess is = Location.getHandle(getCurrentFile(), false); try { is.setOrder(ByteOrder.LITTLE_ENDIAN); for (IM3Record rec: records) { rec.writeSummary(is, ""); } } finally { is.close(); } } /** * Write a summary of each record to STDOUT * * @param args */ static public void main(String [] args){ final IM3Reader reader = new IM3Reader(); try { reader.setId(args[0]); reader.writeSummary(); } catch (FormatException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } }