// // AxisGuesser.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; import java.io.IOException; import java.math.BigInteger; import loci.common.Location; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * AxisGuesser guesses which blocks in a file pattern correspond to which * dimensional axes (Z, T or C), potentially recommending an adjustment in * dimension order within the files, depending on the confidence of each guess. * * <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/AxisGuesser.java">Trac</a>, * <a href="http://git.openmicroscopy.org/?p=bioformats.git;a=blob;f=components/bio-formats/src/loci/formats/AxisGuesser.java;hb=HEAD">Gitweb</a></dd></dl> * * @author Curtis Rueden ctrueden at wisc.edu */ public class AxisGuesser { // -- Constants -- private static final Logger LOGGER = LoggerFactory.getLogger(AxisGuesser.class); /** Axis type for unclassified axes. */ public static final int UNKNOWN_AXIS = 0; /** Axis type for focal planes. */ public static final int Z_AXIS = 1; /** Axis type for time points. */ public static final int T_AXIS = 2; /** Axis type for channels. */ public static final int C_AXIS = 3; /** Axis type for series. */ public static final int S_AXIS = 4; /** Prefix endings indicating space dimension. */ protected static final String[] Z = { "fp", "sec", "z", "zs", "focal", "focalplane" }; /** Prefix endings indicating time dimension. */ protected static final String[] T = {"t", "tl", "tp", "time"}; /** Prefix endings indicating channel dimension. */ protected static final String[] C = {"c", "ch", "w", "wavelength"}; /** Prefix endings indicating series dimension. */ protected static final String[] S = {"s", "series", "sp"}; protected static final String ONE = "1"; protected static final String TWO = "2"; protected static final String THREE = "3"; // -- Fields -- /** File pattern identifying dimensional axis blocks. */ protected FilePattern fp; /** Original ordering of internal dimensional axes. */ protected String dimOrder; /** Adjusted ordering of internal dimensional axes. */ protected String newOrder; /** Guessed axis types. */ protected int[] axisTypes; /** Whether the guesser is confident that all axis types are correct. */ protected boolean certain; // -- Constructor -- /** * Guesses dimensional axis assignments corresponding to the given * file pattern, using the specified dimensional information from * within each file as a guide. * * @param fp The file pattern of the files * @param dimOrder The dimension order (e.g., XYZTC) within each file * @param sizeZ The number of Z positions within each file * @param sizeT The number of T positions within each file * @param sizeC The number of C positions within each file * @param isCertain Whether the dimension order given is known to be good, * or merely a guess * * @see FilePattern */ public AxisGuesser(FilePattern fp, String dimOrder, int sizeZ, int sizeT, int sizeC, boolean isCertain) { this.fp = fp; this.dimOrder = dimOrder; newOrder = dimOrder; String[] prefixes = fp.getPrefixes(); String suffix = fp.getSuffix(); String[][] elements = fp.getElements(); axisTypes = new int[elements.length]; boolean foundZ = false, foundT = false, foundC = false; // -- 1) fill in "known" axes based on known patterns and conventions -- for (int i=0; i<axisTypes.length; i++) { String p = prefixes[i].toLowerCase(); // strip trailing digits and divider characters char[] ch = p.toCharArray(); int l = ch.length - 1; while (l >= 0 && (ch[l] >= '0' && ch[l] <= '9' || ch[l] == ' ' || ch[l] == '-' || ch[l] == '_' || ch[l] == '.')) { l--; } // useful prefix segment consists of trailing alphanumeric characters int f = l; while (f >= 0 && ch[f] >= 'a' && ch[f] <= 'z') f--; p = p.substring(f + 1, l + 1); // check against known Z prefixes for (int j=0; j<Z.length; j++) { if (p.equals(Z[j])) { axisTypes[i] = Z_AXIS; foundZ = true; break; } } if (axisTypes[i] != UNKNOWN_AXIS) continue; // check against known T prefixes for (int j=0; j<T.length; j++) { if (p.equals(T[j])) { axisTypes[i] = T_AXIS; foundT = true; break; } } if (axisTypes[i] != UNKNOWN_AXIS) continue; // check against known C prefixes for (int j=0; j<C.length; j++) { if (p.equals(C[j])) { axisTypes[i] = C_AXIS; foundC = true; break; } } if (axisTypes[i] != UNKNOWN_AXIS) continue; // check against known series prefixes for (int j=0; j<S.length; j++) { if (p.equals(S[j])) { axisTypes[i] = S_AXIS; break; } } if (axisTypes[i] != UNKNOWN_AXIS) continue; // check special case: <2-3>, <1-3> (Bio-Rad PIC) if (suffix.equalsIgnoreCase(".pic") && i == axisTypes.length - 1 && ((elements[i].length == 2 && (elements[i][0].equals(ONE) || elements[i][0].equals(TWO)) && (elements[i][1].equals(TWO) || elements[i][1].equals(THREE))) || (elements[i].length == 3 && elements[i][0].equals(ONE) && elements[i][1].equals(TWO) && elements[i][2].equals(THREE)))) { axisTypes[i] = C_AXIS; continue; } else if (elements[i].length == 2 || elements[i].length == 3) { char first = elements[i][0].toLowerCase().charAt(0); char second = elements[i][1].toLowerCase().charAt(0); char third = elements[i].length == 2 ? 'b' : elements[i][2].toLowerCase().charAt(0); if ((first == 'r' || second == 'r' || third == 'r') && (first == 'g' || second == 'g' || third == 'g') && (first == 'b' || second == 'b' || third == 'b')) { axisTypes[i] = C_AXIS; continue; } } } // -- 2) check for special cases where dimension order should be swapped -- if (!isCertain) { // only switch if dimension order is uncertain if (foundZ && !foundT && sizeZ > 1 && sizeT == 1 || foundT && !foundZ && sizeT > 1 && sizeZ == 1) { // swap Z and T dimensions int indexZ = newOrder.indexOf('Z'); int indexT = newOrder.indexOf('T'); char[] ch = newOrder.toCharArray(); ch[indexZ] = 'T'; ch[indexT] = 'Z'; newOrder = new String(ch); int sz = sizeT; sizeT = sizeZ; sizeZ = sz; } } // -- 3) fill in remaining axis types -- boolean canBeZ = !foundZ && sizeZ == 1; boolean canBeT = !foundT && sizeT == 1; boolean canBeC = !foundC && sizeC == 1; certain = isCertain; for (int i=0; i<axisTypes.length; i++) { if (axisTypes[i] != UNKNOWN_AXIS) continue; certain = false; if (canBeZ) { axisTypes[i] = Z_AXIS; canBeZ = false; } else if (canBeT) { axisTypes[i] = T_AXIS; canBeT = false; } else if (canBeC) { axisTypes[i] = C_AXIS; canBeC = false; } else { char lastAxis = newOrder.charAt(newOrder.length() - 1); if (lastAxis == 'C') { axisTypes[i] = C_AXIS; } else if (lastAxis == 'Z') { axisTypes[i] = Z_AXIS; } else axisTypes[i] = T_AXIS; } } } // -- AxisGuesser API methods -- /** Gets the file pattern. */ public FilePattern getFilePattern() { return fp; } /** Gets the original dimension order. */ public String getOriginalOrder() { return dimOrder; } /** Gets the adjusted dimension order. */ public String getAdjustedOrder() { return newOrder; } /** Gets whether the guesser is confident that all axes are correct. */ public boolean isCertain() { return certain; } /** * Gets the guessed axis type for each dimensional block. * @return An array containing values from the enumeration: * <ul> * <li>Z_AXIS: focal planes</li> * <li>T_AXIS: time points</li> * <li>C_AXIS: channels</li> * <li>S_AXIS: series</li> * </ul> */ public int[] getAxisTypes() { return axisTypes; } /** * Sets the axis type for each dimensional block. * @param axes An array containing values from the enumeration: * <ul> * <li>Z_AXIS: focal planes</li> * <li>T_AXIS: time points</li> * <li>C_AXIS: channels</li> * <li>S_AXIS: series</li> * </ul> */ public void setAxisTypes(int[] axes) { axisTypes = axes; } /** Gets the number of Z axes in the pattern. */ public int getAxisCountZ() { return getAxisCount(Z_AXIS); } /** Gets the number of T axes in the pattern. */ public int getAxisCountT() { return getAxisCount(T_AXIS); } /** Gets the number of C axes in the pattern. */ public int getAxisCountC() { return getAxisCount(C_AXIS); } /** Gets the number of S axes in the pattern. */ public int getAxisCountS() { return getAxisCount(S_AXIS); } /** Gets the number of axes in the pattern of the given type. * @param axisType One of: * <ul> * <li>Z_AXIS: focal planes</li> * <li>T_AXIS: time points</li> * <li>C_AXIS: channels</li> * <li>S_AXIS: series</li> * </ul> */ public int getAxisCount(int axisType) { int num = 0; for (int i=0; i<axisTypes.length; i++) { if (axisTypes[i] == axisType) num++; } return num; } // -- Static API methods -- /** Returns a best guess of the given label's axis type. */ public static int getAxisType(String label) { String lowerLabel = label.toLowerCase(); for (String p : Z) { if (p.equals(lowerLabel) || lowerLabel.endsWith(p)) return Z_AXIS; } for (String p : C) { if (p.equals(lowerLabel) || lowerLabel.endsWith(p)) return C_AXIS; } for (String p : T) { if (p.equals(lowerLabel) || lowerLabel.endsWith(p)) return T_AXIS; } for (String p : S) { if (p.equals(lowerLabel) || lowerLabel.endsWith(p)) return S_AXIS; } return UNKNOWN_AXIS; } // -- Main method -- /** Method for testing pattern guessing logic. */ public static void main(String[] args) throws FormatException, IOException { Location file = args.length < 1 ? new Location(System.getProperty("user.dir")).listFiles()[0] : new Location(args[0]); LOGGER.info("File = {}", file.getAbsoluteFile()); String pat = FilePattern.findPattern(file); if (pat == null) LOGGER.info("No pattern found."); else { LOGGER.info("Pattern = {}", pat); FilePattern fp = new FilePattern(pat); if (fp.isValid()) { LOGGER.info("Pattern is valid."); String id = fp.getFiles()[0]; if (!new Location(id).exists()) { LOGGER.info("File '{}' does not exist.", id); } else { // read dimensional information from first file LOGGER.info("Reading first file "); ImageReader reader = new ImageReader(); reader.setId(id); String dimOrder = reader.getDimensionOrder(); int sizeZ = reader.getSizeZ(); int sizeT = reader.getSizeT(); int sizeC = reader.getSizeC(); boolean certain = reader.isOrderCertain(); reader.close(); LOGGER.info("[done]"); LOGGER.info("\tdimOrder = {} ({})", dimOrder, certain ? "certain" : "uncertain"); LOGGER.info("\tsizeZ = {}", sizeZ); LOGGER.info("\tsizeT = {}", sizeT); LOGGER.info("\tsizeC = {}", sizeC); // guess axes AxisGuesser ag = new AxisGuesser(fp, dimOrder, sizeZ, sizeT, sizeC, certain); // output results String[] blocks = fp.getBlocks(); String[] prefixes = fp.getPrefixes(); int[] axes = ag.getAxisTypes(); String newOrder = ag.getAdjustedOrder(); boolean isCertain = ag.isCertain(); LOGGER.info("Axis types:"); for (int i=0; i<blocks.length; i++) { String axis; switch (axes[i]) { case Z_AXIS: axis = "Z"; break; case T_AXIS: axis = "T"; break; case C_AXIS: axis = "C"; break; default: axis = "?"; } LOGGER.info("\t{}\t{} (prefix = {})", new Object[] {blocks[i], axis, prefixes[i]}); } if (!dimOrder.equals(newOrder)) { LOGGER.info("Adjusted dimension order = {} ({})", newOrder, isCertain ? "certain" : "uncertain"); } } } else LOGGER.info("Pattern is invalid: {}", fp.getErrorMessage()); } } } // -- Notes -- // INPUTS: file pattern, dimOrder, sizeZ, sizeT, sizeC, isCertain // // 1) Fill in all "known" dimensional axes based on known patterns and // conventions // * known internal axes (ZCT) have isCertain == true // * known dimensional axes have a known pattern or convention // After that, we are left with only unknown slots, which we must guess. // // 2) First, we decide whether we really "believe" the reader. There is a // special case where we may decide that it got Z and T mixed up: // * if a Z block was found, but not a T block: // if !isOrderCertain, and sizeZ > 1, and sizeT == 1, swap 'em // * else if a T block was found, but not a Z block: // if !isOrderCertain and sizeT > 1, and sizeZ == 1, swap 'em // At this point, we can (have to) trust the internal ordering, and use it // to decide how to fill in the remaining dimensional blocks. // // 3) Set canBeZ to true iff no Z block is assigned and sizeZ == 1. // Set canBeT to true iff no T block is assigned and sizeT == 1. // Go through the blocks in order from left to right: // * If canBeZ, assign Z and set canBeZ to false. // * If canBeT, assign T and set canBeT to false. // * Otherwise, assign C. // // OUTPUTS: list of axis assignments, new dimOrder