//
// MetamorphTiffReader.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.Comparator;
import java.util.HashMap;
import java.util.Vector;
import loci.common.DateTools;
import loci.common.Location;
import loci.common.RandomAccessInputStream;
import loci.common.xml.XMLTools;
import loci.formats.CoreMetadata;
import loci.formats.FilePattern;
import loci.formats.FormatException;
import loci.formats.FormatTools;
import loci.formats.MetadataTools;
import loci.formats.meta.MetadataStore;
import ome.xml.model.primitives.PositiveFloat;
import loci.formats.tiff.IFD;
import loci.formats.tiff.TiffParser;
import ome.xml.model.enums.NamingConvention;
import ome.xml.model.primitives.NonNegativeInteger;
/**
* MetamorphTiffReader is the file format reader for TIFF files produced by
* Metamorph software version 7.5 and above.
*
* <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/MetamorphTiffReader.java">Trac</a>,
* <a href="http://git.openmicroscopy.org/?p=bioformats.git;a=blob;f=components/bio-formats/src/loci/formats/in/MetamorphTiffReader.java;hb=HEAD">Gitweb</a></dd></dl>
*
* @author Melissa Linkert melissa at glencoesoftware.com
* @author Thomas Caswell tcaswell at uchicago.edu
*/
public class MetamorphTiffReader extends BaseTiffReader {
// -- Constants --
private static final String DATE_FORMAT = "yyyyMMdd HH:mm:ss.SSS";
// -- Fields --
private String[] files;
private int wellCount = 0;
private int fieldRowCount = 0;
private int fieldColumnCount = 0;
// -- Constructor --
/** Constructs a new Metamorph TIFF reader. */
public MetamorphTiffReader() {
super("Metamorph TIFF", new String[] {"tif", "tiff"});
suffixSufficient = false;
domains = new String[] {FormatTools.LM_DOMAIN, FormatTools.HCS_DOMAIN};
datasetDescription = "One or more .tif/.tiff files";
}
// -- IFormatReader API methods --
/* @see loci.formats.IFormatReader#isThisType(RandomAccessInputStream) */
public boolean isThisType(RandomAccessInputStream stream) throws IOException {
TiffParser tp = new TiffParser(stream);
String comment = tp.getComment();
if (comment == null) return false;
comment = comment.trim();
return comment.startsWith("<MetaData>") && comment.endsWith("</MetaData>");
}
/* @see loci.formats.IFormatReader#getDomains() */
public String[] getDomains() {
FormatTools.assertId(currentId, true, 1);
String[] domain = new String[1];
domain[0] =
files.length == 1 ? FormatTools.LM_DOMAIN : FormatTools.HCS_DOMAIN;
return domain;
}
/* @see loci.formats.IFormatReader#getSeriesUsedFiles(boolean) */
public String[] getUsedFiles(boolean noPixels) {
FormatTools.assertId(currentId, true, 1);
return noPixels ? new String[0] : files;
}
/**
* @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
{
if (getSeriesCount() == 1) return super.openBytes(no, buf, x, y, w, h);
int[] lengths = new int[] {getSizeZ(), getEffectiveSizeC(),
fieldColumnCount, fieldRowCount, wellCount, getSizeT()};
int[] zct = getZCTCoords(no);
Well well = getWell(getSeries());
int[] position = new int[] {zct[0], zct[1], well.fieldCol,
well.fieldRow, well.well, zct[2]};
int fileIndex = FormatTools.positionToRaster(lengths, position);
RandomAccessInputStream s = null;
if (fileIndex < files.length) {
s = new RandomAccessInputStream(files[fileIndex]);
}
else {
s = new RandomAccessInputStream(files[0]);
}
TiffParser parser = new TiffParser(s);
IFD ifd = files.length == 1 ?
ifds.get(getSeries() * getImageCount() + no) : parser.getFirstIFD();
parser.getSamples(ifd, buf, x, y, w, h);
s.close();
return buf;
}
// -- Internal FormatReader API methods --
/* @see loci.formats.FormatReader#initFile(String) */
protected void initFile(String id) throws FormatException, IOException {
super.initFile(id);
Vector<String> uniqueChannels = new Vector<String>();
Vector<Double> uniqueZs = new Vector<Double>();
Vector<Double> stageX = new Vector<Double>();
Vector<Double> stageY = new Vector<Double>();
String filename = id.substring(id.lastIndexOf(File.separator) + 1);
filename = filename.substring(0, filename.indexOf("."));
boolean integerFilename = true;
try {
Integer.parseInt(filename);
}
catch (NumberFormatException e) {
integerFilename = false;
}
if (integerFilename && ifds.size() == 1 &&
ifds.get(0).getIFDIntValue(IFD.NEW_SUBFILE_TYPE) == 2)
{
// look for other files in the dataset
findTIFFs();
String stageLabel = null;
for (String tiff : files) {
MetamorphHandler handler = new MetamorphHandler();
parseFile(tiff, handler);
String label = handler.getStageLabel();
if (stageLabel == null) {
stageLabel = label;
}
else if (!label.equals(stageLabel)) {
break;
}
if (!uniqueChannels.contains(handler.getChannelName())) {
uniqueChannels.add(handler.getChannelName());
}
Vector<Double> zPositions = handler.getZPositions();
Double pos = Math.rint(zPositions.get(0));
if (!uniqueZs.contains(pos)) {
uniqueZs.add(pos);
}
}
MetamorphHandler handler = new MetamorphHandler();
parseFile(files[files.length - 1], handler);
String lastStageLabel = handler.getStageLabel();
int lastField = getField(lastStageLabel);
int lastWellRow = getWellRow(lastStageLabel);
int lastWellColumn = getWellColumn(lastStageLabel);
int field = getField(stageLabel);
int fieldRow = getWellRow(stageLabel);
int fieldColumn = getWellColumn(stageLabel);
wellCount = lastField - field + 1;
fieldRowCount = lastWellRow - fieldRow + 1;
fieldColumnCount = lastWellColumn - fieldColumn + 1;
core[0].sizeC = uniqueChannels.size();
core[0].sizeZ = uniqueZs.size();
}
else {
files = new String[] {id};
wellCount = 1;
fieldRowCount = 1;
fieldColumnCount = 1;
core[0].sizeC = 0;
}
// parse XML comment
MetamorphHandler handler = new MetamorphHandler(getGlobalMetadata());
Vector<Double> xPositions = new Vector<Double>();
Vector<Double> yPositions = new Vector<Double>();
for (IFD ifd : ifds) {
String xml = XMLTools.sanitizeXML(ifd.getComment());
XMLTools.parseXML(xml, handler);
double x = handler.getStagePositionX();
double y = handler.getStagePositionY();
if (xPositions.size() == 0) {
xPositions.add(x);
yPositions.add(y);
}
else {
double previousX = xPositions.get(xPositions.size() - 1);
double previousY = yPositions.get(yPositions.size() - 1);
if (Math.abs(previousX - x) > 0.21 || Math.abs(previousY - y) > 0.21) {
xPositions.add(x);
yPositions.add(y);
}
}
}
if (xPositions.size() > 1) {
fieldRowCount = xPositions.size();
}
Vector<Integer> wavelengths = handler.getWavelengths();
Vector<Double> zPositions = handler.getZPositions();
// calculate axis sizes
Vector<Integer> uniqueC = new Vector<Integer>();
for (Integer c : wavelengths) {
if (!uniqueC.contains(c)) {
uniqueC.add(c);
}
}
int effectiveC = uniqueC.size();
if (effectiveC == 0) effectiveC = 1;
if (getSizeC() == 0) core[0].sizeC = 1;
int samples = ifds.get(0).getSamplesPerPixel();
core[0].sizeC *= effectiveC * samples;
Vector<Double> uniqueZ = new Vector<Double>();
for (Double z : zPositions) {
if (!uniqueZ.contains(z)) uniqueZ.add(z);
}
if (getSizeZ() == 0) core[0].sizeZ = 1;
core[0].sizeZ *= uniqueZ.size();
int totalPlanes = files.length * ifds.size();
effectiveC = getSizeC() / samples;
core[0].sizeT = totalPlanes /
(wellCount * fieldRowCount * fieldColumnCount * getSizeZ() * effectiveC);
if (getSizeT() == 0) core[0].sizeT = 1;
int seriesCount = wellCount * fieldRowCount * fieldColumnCount;
if (seriesCount > 1 && getSizeZ() > totalPlanes / seriesCount) {
core[0].sizeZ = 1;
core[0].sizeT = totalPlanes / (seriesCount * getSizeT() * effectiveC);
}
core[0].imageCount = getSizeZ() * getSizeT() * effectiveC;
if (seriesCount > 1) {
CoreMetadata oldCore = core[0];
core = new CoreMetadata[seriesCount];
for (int i=0; i<seriesCount; i++) {
core[i] = oldCore;
}
}
for (int s=0; s<wellCount * fieldRowCount * fieldColumnCount; s++) {
if (files.length > 1) {
int[] lengths = new int[] {getSizeZ(), getEffectiveSizeC(),
fieldColumnCount, fieldRowCount, wellCount, getSizeT()};
Well well = getWell(s);
int[] position =
new int[] {0, 0, well.fieldCol, well.fieldRow, well.well, 0};
int fileIndex = FormatTools.positionToRaster(lengths, position);
parseFile(files[fileIndex], handler);
stageX.add(handler.getStagePositionX());
stageY.add(handler.getStagePositionY());
}
else {
stageX.add(xPositions.get(s));
stageY.add(yPositions.get(s));
}
}
MetadataStore store = makeFilterMetadata();
MetadataTools.populatePixels(store, this);
store.setPlateID(MetadataTools.createLSID("Plate", 0), 0);
store.setPlateRowNamingConvention(NamingConvention.LETTER, 0);
store.setPlateColumnNamingConvention(NamingConvention.NUMBER, 0);
for (int well=0; well<wellCount; well++) {
store.setWellID(MetadataTools.createLSID("Well", 0, well), 0, well);
store.setWellRow(new NonNegativeInteger(0), 0, well);
store.setWellColumn(new NonNegativeInteger(well), 0, well);
for (int row=0; row<fieldRowCount; row++) {
for (int col=0; col<fieldColumnCount; col++) {
int field = row * fieldColumnCount + col;
String wellSampleID =
MetadataTools.createLSID("WellSample", 0, well, field);
store.setWellSampleID(wellSampleID, 0, well, field);
int seriesIndex = getSeriesIndex(row, col, well);
String imageID = MetadataTools.createLSID("Image", seriesIndex);
store.setImageID(imageID, seriesIndex);
store.setWellSampleImageRef(imageID, 0, well, field);
store.setWellSampleIndex(
new NonNegativeInteger(seriesIndex), 0, well, field);
}
}
}
for (int s=0; s<seriesCount; s++) {
setSeries(s);
Well well = getWell(s);
String name = handler.getImageName();
if (seriesCount > 1) {
name = "Field " + (char) (well.fieldRow + 'A') + (well.fieldCol + 1) +
", Well " + (well.well + 1) + ": " + name;
}
store.setImageName(name, s);
String date =
DateTools.formatDate(handler.getDate(), DateTools.ISO8601_FORMAT);
store.setImageAcquiredDate(date, s);
if (getMetadataOptions().getMetadataLevel() != MetadataLevel.MINIMUM) {
Vector<String> timestamps = handler.getTimestamps();
Vector<Double> exposures = handler.getExposures();
for (int i=0; i<timestamps.size(); i++) {
long timestamp = DateTools.getTime(timestamps.get(i), DATE_FORMAT);
addSeriesMeta("timestamp " + i, timestamp);
}
for (int i=0; i<exposures.size(); i++) {
addSeriesMeta("exposure time " + i + " (ms)",
exposures.get(i).floatValue() * 1000);
}
long startDate = 0;
if (timestamps.size() > 0) {
startDate = DateTools.getTime(timestamps.get(0), DATE_FORMAT);
}
store.setImageDescription("", s);
int image = 0;
for (int c=0; c<getEffectiveSizeC(); c++) {
for (int t=0; t<getSizeT(); t++) {
store.setPlaneTheZ(new NonNegativeInteger(0), s, image);
store.setPlaneTheC(new NonNegativeInteger(c), s, image);
store.setPlaneTheT(new NonNegativeInteger(t), s, image);
if (t < timestamps.size()) {
String stamp = timestamps.get(t);
long ms = DateTools.getTime(stamp, DATE_FORMAT);
store.setPlaneDeltaT((ms - startDate) / 1000.0, s, image);
}
if (image < exposures.size()) {
store.setPlaneExposureTime(exposures.get(image), s, image);
}
if (s < stageX.size()) {
store.setPlanePositionX(stageX.get(s), s, image);
}
if (s < stageY.size()) {
store.setPlanePositionY(stageY.get(s), s, image);
}
image++;
}
}
store.setImagingEnvironmentTemperature(handler.getTemperature(), s);
store.setPixelsPhysicalSizeX(
new PositiveFloat(handler.getPixelSizeX()), s);
store.setPixelsPhysicalSizeY(
new PositiveFloat(handler.getPixelSizeY()), s);
for (int c=0; c<getEffectiveSizeC(); c++) {
if (uniqueChannels.size() > c) {
store.setChannelName(uniqueChannels.get(c), s, c);
}
else store.setChannelName(handler.getChannelName(), s, c);
}
}
}
}
// -- Helper methods --
private int getSeriesIndex(int fieldRow, int fieldColumn, int well) {
return FormatTools.positionToRaster(
new int[] {fieldColumnCount, fieldRowCount, wellCount},
new int[] {fieldColumn, fieldRow, well});
}
private Well getWell(int seriesIndex) {
int[] coordinates = FormatTools.rasterToPosition(
new int[] {fieldColumnCount, fieldRowCount, wellCount}, seriesIndex);
return new Well(coordinates[1], coordinates[0], coordinates[2]);
}
private int getField(String stageLabel) {
if (stageLabel.indexOf("Scan") < 0) return 0;
String index = stageLabel.substring(0, stageLabel.indexOf(":")).trim();
return Integer.parseInt(index) - 1;
}
private int getWellRow(String stageLabel) {
int scanIndex = stageLabel.indexOf("Scan");
if (scanIndex < 0) return 0;
scanIndex = stageLabel.indexOf(" ", scanIndex) + 1;
return (int) (stageLabel.charAt(scanIndex) - 'A');
}
private int getWellColumn(String stageLabel) {
int scanIndex = stageLabel.indexOf("Scan");
if (scanIndex < 0) return 0;
scanIndex = stageLabel.indexOf(" ", scanIndex) + 2;
return Integer.parseInt(stageLabel.substring(scanIndex)) - 1;
}
private void findTIFFs() throws IOException {
Location baseFile = new Location(currentId).getAbsoluteFile();
Location parent = baseFile.getParentFile();
FilePattern pattern = new FilePattern(baseFile);
String[] tiffs = pattern.getFiles();
NumericComparator comparator = new NumericComparator();
Arrays.sort(tiffs, comparator);
Vector<String> validTIFFs = new Vector<String>();
for (String tiff : tiffs) {
if (!new Location(tiff).exists()) {
String base = tiff.substring(tiff.lastIndexOf(File.separator) + 1);
base = base.substring(0, base.indexOf("."));
String suffix = tiff.substring(tiff.lastIndexOf("."));
while (base.length() < 3) {
base = "0" + base;
}
Location test = new Location(parent, base + suffix);
if (test.exists()) {
tiff = test.getAbsolutePath();
}
else continue;
}
validTIFFs.add(tiff);
}
files = validTIFFs.toArray(new String[validTIFFs.size()]);
}
private void parseFile(String tiff, MetamorphHandler handler)
throws IOException
{
RandomAccessInputStream s = new RandomAccessInputStream(tiff);
TiffParser parser = new TiffParser(s);
IFD firstIFD = parser.getFirstIFD();
String xml = XMLTools.sanitizeXML(firstIFD.getComment());
XMLTools.parseXML(xml, handler);
s.close();
}
// -- Helper classes --
class Well {
public int well;
public int fieldRow;
public int fieldCol;
public Well(int fieldRow, int fieldCol, int well) {
this.fieldRow = fieldRow;
this.fieldCol = fieldCol;
this.well = well;
}
public boolean equals(Object o) {
if (!(o instanceof Well)) return false;
Well w = (Well) o;
return w.well == this.well && w.fieldRow == this.fieldRow &&
w.fieldCol == this.fieldCol;
}
public int hashCode() {
return (well << 16) | (fieldRow << 8) | fieldCol;
}
}
class NumericComparator implements Comparator<String> {
public int compare(String s1, String s2) {
if (s1.equals(s2)) return 0;
String base1 = s1.substring(s1.lastIndexOf(File.separator) + 1);
String base2 = s2.substring(s2.lastIndexOf(File.separator) + 1);
base1 = base1.substring(0, base1.indexOf("."));
base2 = base2.substring(0, base2.indexOf("."));
try {
int num1 = Integer.parseInt(base1);
int num2 = Integer.parseInt(base2);
if (num1 == num2) return 0;
return num1 < num2 ? -1 : 1;
}
catch (NumberFormatException e) { }
return 0;
}
}
}