/*
* #%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.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 loci.formats.tiff.IFD;
import loci.formats.tiff.TiffParser;
import ome.xml.model.enums.NamingConvention;
import ome.xml.model.primitives.NonNegativeInteger;
import ome.xml.model.primitives.PositiveFloat;
import ome.xml.model.primitives.Timestamp;
import ome.units.quantity.Length;
import ome.units.quantity.Temperature;
import ome.units.quantity.Time;
import ome.units.UNITS;
/**
* MetamorphTiffReader is the file format reader for TIFF files produced by
* Metamorph software version 7.5 and above.
*
* @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";
// -- 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) */
@Override
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() */
@Override
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) */
@Override
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)
*/
@Override
public byte[] openBytes(int no, byte[] buf, int x, int y, int w, int h)
throws FormatException, IOException
{
if (getSeriesCount() == 1 && files.length == 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) */
@Override
protected void initFile(String id) throws FormatException, IOException {
super.initFile(id);
Vector<String> uniqueChannels = new Vector<String>();
Vector<Double> uniqueZs = new Vector<Double>();
Vector<Length> stageX = new Vector<Length>();
Vector<Length> stageY = new Vector<Length>();
CoreMetadata m = core.get(0);
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;
m.sizeC = uniqueChannels.size();
m.sizeZ = uniqueZs.size();
}
else {
files = new String[] {id};
wellCount = 1;
fieldRowCount = 1;
fieldColumnCount = 1;
m.sizeC = 0;
}
// parse XML comment
MetamorphHandler handler = new MetamorphHandler(getGlobalMetadata());
final Vector<Length> xPositions = new Vector<Length>();
final Vector<Length> yPositions = new Vector<Length>();
for (IFD ifd : ifds) {
String xml = XMLTools.sanitizeXML(ifd.getComment());
XMLTools.parseXML(xml, handler);
final Length x = handler.getStagePositionX();
final Length y = handler.getStagePositionY();
if (xPositions.size() == 0) {
xPositions.add(x);
yPositions.add(y);
}
else {
final Length previousX = xPositions.get(xPositions.size() - 1);
final Length previousY = yPositions.get(yPositions.size() - 1);
final double x1 = x.value(UNITS.REFERENCEFRAME).doubleValue();
final double x2 = previousX.value(UNITS.REFERENCEFRAME).doubleValue();
final double y1 = y.value(UNITS.REFERENCEFRAME).doubleValue();
final double y2 = previousY.value(UNITS.REFERENCEFRAME).doubleValue();
if (Math.abs(x1 - x2) > 0.21 || Math.abs(y1 - y2) > 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) m.sizeC = 1;
int samples = ifds.get(0).getSamplesPerPixel();
m.sizeC *= effectiveC * samples;
Vector<Double> uniqueZ = new Vector<Double>();
for (Double z : zPositions) {
if (!uniqueZ.contains(z)) uniqueZ.add(z);
}
if (getSizeZ() == 0) m.sizeZ = 1;
m.sizeZ *= uniqueZ.size();
Double zRange = zPositions.get(zPositions.size() - 1) - zPositions.get(0);
Double physicalSizeZ = Math.abs(zRange);
if (m.sizeZ > 1) {
physicalSizeZ /= (m.sizeZ - 1);
}
int totalPlanes = files.length * ifds.size();
effectiveC = getSizeC() / samples;
m.sizeT = totalPlanes /
(wellCount * fieldRowCount * fieldColumnCount * getSizeZ() * effectiveC);
if (getSizeT() == 0) m.sizeT = 1;
int seriesCount = wellCount * fieldRowCount * fieldColumnCount;
if (seriesCount > 1 && getSizeZ() > totalPlanes / seriesCount ||
totalPlanes > getSizeZ() * getSizeT() * effectiveC)
{
m.sizeZ = 1;
m.sizeT = totalPlanes / (seriesCount * effectiveC);
}
m.imageCount = getSizeZ() * getSizeT() * effectiveC;
if (seriesCount > 1) {
core.clear();
for (int i=0; i<seriesCount; i++) {
core.add(m);
}
}
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);
// Only populate Plate/Well metadata if there are multiple wells.
// The presence of just one well suggests that we don't actually have
// an HCS dataset (and even if we did, the Plate/Well metadata would be
// effectively useless).
if (wellCount > 1) {
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, ".");
if (date != null) {
store.setImageAcquisitionDate(new Timestamp(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, ".");
addSeriesMetaList("timestamp", timestamp);
}
for (int i=0; i<exposures.size(); i++) {
addSeriesMetaList("exposure time (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(new Time((ms - startDate) / 1000.0, UNITS.S), s, image);
}
if (image < exposures.size() && exposures.get(image) != null) {
store.setPlaneExposureTime(new Time(exposures.get(image), UNITS.S), 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(
new Temperature(handler.getTemperature(), UNITS.DEGREEC), s);
Length sizeX =
FormatTools.getPhysicalSizeX(handler.getPixelSizeX());
Length sizeY =
FormatTools.getPhysicalSizeY(handler.getPixelSizeY());
Length sizeZ = FormatTools.getPhysicalSizeZ(physicalSizeZ);
if (sizeX != null) {
store.setPixelsPhysicalSizeX(sizeX, s);
}
if (sizeY != null) {
store.setPixelsPhysicalSizeY(sizeY, s);
}
if (sizeZ != null) {
store.setPixelsPhysicalSizeZ(sizeZ, 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;
}
if (File.separator.equals("\\")) {
tiff = tiff.replaceAll("\\\\\\\\", "\\\\");
}
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;
}
@Override
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;
}
@Override
public int hashCode() {
return (well << 16) | (fieldRow << 8) | fieldCol;
}
}
class NumericComparator implements Comparator<String> {
@Override
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;
}
}
}