diff --git a/src/main/java/com/glencoesoftware/bioformats2raw/BioTekReader.java b/src/main/java/com/glencoesoftware/bioformats2raw/BioTekReader.java index b201a6d4..79628f58 100644 --- a/src/main/java/com/glencoesoftware/bioformats2raw/BioTekReader.java +++ b/src/main/java/com/glencoesoftware/bioformats2raw/BioTekReader.java @@ -70,6 +70,9 @@ public class BioTekReader extends FormatReader { private static final String TIFF_REGEX_Z = WELL_REGEX + "_.+\\[(.+)_" + ALPHANUM + "\\]_(\\d+)_(\\d+)_([0-9-]+)?" + SUFFIX; + private static final String TIFF_REGEX_ROI = + "([A-Z]{1,2})(\\d{1,2})ROI(\\d+)_(\\d+)_(\\d+)_(\\d+)(Z(\\d+))?_" + + ALPHANUM + "_(-?\\d+)" + SUFFIX; private static final String DATE_FORMAT = "MM/dd/yy HH:mm:ss"; // -- Fields -- @@ -211,22 +214,81 @@ protected void initFile(String id) throws FormatException, IOException { findXPTFiles(parent); String[] files = parent.list(true); + for (int i=0; i 0) { + String wellCheck = files[0].substring(0, endIndex); + LOGGER.debug("well check string = {}", wellCheck); + for (int i=0; i allFiles = new ArrayList(); + boolean multipleDirs = true; + for (String well : wellDirs) { + Location wellDir = new Location(plateDir, well).getAbsoluteFile(); + LOGGER.debug("looking in well directory = {}", wellDir); + if (wellDir == null) { + multipleDirs = false; + } + else { + String[] f = wellDir.list(true); + if (f == null) { + multipleDirs = false; + } + else { + for (String file : f) { + LOGGER.debug(" adding well file {}", file); + allFiles.add(new Location(wellDir, file).getAbsolutePath()); + } + } + } + } + if (multipleDirs) { + LOGGER.debug("found files = {}", allFiles); + files = allFiles.toArray(new String[allFiles.size()]); + Arrays.sort(files); + } + } + Pattern regexA = Pattern.compile(TIFF_REGEX_A); Pattern regexB = Pattern.compile(TIFF_REGEX_B); Pattern regexZ = Pattern.compile(TIFF_REGEX_Z); + Pattern regexROI = Pattern.compile(TIFF_REGEX_ROI); + ArrayList validWellRowCol = new ArrayList(); int maxRow = 0; - int minRow = Integer.MAX_VALUE; int maxCol = 0; - int minCol = Integer.MAX_VALUE; int maxPlateAcq = 0; Map maxField = new HashMap(); - for (String f : files) { + String matchingPath = new Location(currentId).getAbsolutePath(); + LOGGER.trace("matching path = {}", matchingPath); + + ArrayList foundWellSamples = new ArrayList(); + + for (String absolutePath : files) { + String f = new Location(absolutePath).getName(); Matcher m = regexA.matcher(f); int rowIndex = -1; int colIndex = -1; int fieldIndex = -1; + int roiIndex = -1; int z = 0; int t = 0; String channelName = ""; @@ -261,28 +323,68 @@ protected void initFile(String id) throws FormatException, IOException { t = (int) Math.max(0, Integer.parseInt(m.group(8)) - 1); channelName += m.group(6); } + else { + m = regexROI.matcher(f); + if (m.matches()) { + rowIndex = getWellRow(m.group(1)); + colIndex = Integer.parseInt(m.group(2)) - 1; + roiIndex = Integer.parseInt(m.group(3)) - 1; + + LOGGER.trace("absolutePath = {}, roiIndex = {}", + absolutePath, roiIndex); + + int channelIndex = Integer.parseInt(m.group(5)) - 1; + fieldIndex = Integer.parseInt(m.group(6)) - 1; + try { + z = Integer.parseInt(m.group(8)); + // can have two channels with same name + // one with Z stack and one without + channelName = "Z"; + } + catch (NumberFormatException e) { + } + channelName += m.group(9); + // recorded T index may be negative if no timepoints + t = (int) Math.max(0, Integer.parseInt(m.group(10)) - 1); + } + } } - if (rowIndex >= 0 && colIndex >= 0 && fieldIndex >= 0) { + if (rowIndex >= 0 && colIndex >= 0 && + (fieldIndex >= 0 || roiIndex >= 0)) + { + // collapse field and ROI index into single field index + if (roiIndex >= 0) { + if (fieldIndex < 0) { + // only ROIs, no field + fieldIndex = roiIndex; + } + else { + // both fields and ROIs + String key = fieldIndex + "," + roiIndex; + if (!foundWellSamples.contains(key)) { + foundWellSamples.add(key); + } + fieldIndex = foundWellSamples.indexOf(key); + } + } + BioTekWell well = lookupWell(0, rowIndex, colIndex); if (fieldIndex >= well.getFieldCount()) { well.setFieldCount(fieldIndex + 1); } int c = well.addChannelName(fieldIndex, channelName); - well.addFile(new PlaneIndex(fieldIndex, z, c, t), - new Location(parent, f).getAbsolutePath()); + well.addFile(new PlaneIndex(fieldIndex, z, c, t), absolutePath); if (rowIndex > maxRow) { maxRow = rowIndex; } - if (rowIndex < minRow) { - minRow = rowIndex; - } if (colIndex > maxCol) { maxCol = colIndex; } - if (colIndex < minCol) { - minCol = colIndex; + WellIndex rowColPair = new WellIndex(rowIndex, colIndex); + if (!validWellRowCol.contains(rowColPair)) { + validWellRowCol.add(rowColPair); } Integer maxFieldIndex = maxField.get(0); if (maxFieldIndex == null) { @@ -292,6 +394,7 @@ protected void initFile(String id) throws FormatException, IOException { } } wells.sort(null); + validWellRowCol.sort(null); // split brightfield channels into a separate plate acquisition maxField.put(1, -1); @@ -303,6 +406,8 @@ protected void initFile(String id) throws FormatException, IOException { for (int f=0; f channels = well.getChannels(field - 1); + String fieldName = " #" + (field + 1); + if (field < foundWellSamples.size()) { + // unpack the field/ROI indexes and store both + // in the image name + + String[] indexes = foundWellSamples.get(field).split(","); + if (indexes.length == 2) { + fieldName = " Field #" + (Integer.parseInt(indexes[0]) + 1) + + ", ROI #" + (Integer.parseInt(indexes[1]) + 1); + } + } + + store.setImageName(getWellName(row, column) + fieldName, s); + + List channels = well.getChannels(field); for (int c=0; c { + public int row; + public int col; + + public WellIndex(int r, int c) { + this.row = r; + this.col = c; + } + + @Override + public int compareTo(WellIndex w) { + if (this.row != w.row) { + return this.row - w.row; + } + return this.col - w.col; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof WellIndex)) { + return false; + } + return compareTo((WellIndex) o) == 0; + } + + @Override + public int hashCode() { + // this would need fixing if we had more than 65535 rows or columns + return (row & 0xffff) << 16 | (col & 0xffff); + } + } + } diff --git a/src/test/java/com/glencoesoftware/bioformats2raw/test/BioTekTest.java b/src/test/java/com/glencoesoftware/bioformats2raw/test/BioTekTest.java index cae226ef..2f96b1ba 100644 --- a/src/test/java/com/glencoesoftware/bioformats2raw/test/BioTekTest.java +++ b/src/test/java/com/glencoesoftware/bioformats2raw/test/BioTekTest.java @@ -71,7 +71,12 @@ void createPlate(String[] filenames) throws IOException { Path testTiff = getTestFile("test.tiff"); for (String f : filenames) { - Files.copy(testTiff, input.resolve(f)); + Path copyLocation = input.resolve(f); + if (!input.equals(copyLocation.getParent())) { + // supplied file path includes a subdirectory + copyLocation.getParent().toFile().mkdirs(); + } + Files.copy(testTiff, copyLocation); } // reset the input path to the first file in the list @@ -80,16 +85,15 @@ void createPlate(String[] filenames) throws IOException { } /** - * Create an artificial single well BioTek plate with the given - * list of file names. + * Create an artificial BioTek plate with the given list of file names. * Checks that the correct well row/column and ZCT sizes are detected. * * This test will be run once for each Arguments object * returned by getTestCases below. * * @param paths path to each file in the plate - * @param wellRow well row index (from 0) - * @param wellColumn well column index (from 0) + * @param wellRow row index for each well (from 0) + * @param wellColumn column index for each well (from 0) * @param fields number of fields in the well * @param sizeZ number of Z sections * @param sizeC number of channels @@ -97,7 +101,7 @@ void createPlate(String[] filenames) throws IOException { */ @ParameterizedTest @MethodSource("getTestCases") - public void testBioTek(String[] paths, int wellRow, int wellColumn, + public void testBioTek(String[] paths, int[] wellRow, int[] wellColumn, int fields, int sizeZ, int sizeC, int sizeT) throws Exception { // set up the artificial plate @@ -112,29 +116,33 @@ public void testBioTek(String[] paths, int wellRow, int wellColumn, // an exception which would fail the test reader.setId(input.toString()); - // the number of OME Images should match the expected field count + // the number of OME Images should match the expected well * field count // this should be the same as the series count - assertEquals(metadata.getImageCount(), fields); + assertEquals(metadata.getImageCount(), fields * wellRow.length); assertEquals(metadata.getImageCount(), reader.getSeriesCount()); - // there should be exactly one plate, with exactly one well + // there should be exactly one plate assertEquals(metadata.getPlateCount(), 1); - assertEquals(metadata.getWellCount(0), 1); + assertEquals(metadata.getWellCount(0), wellRow.length); // the well's row and column indexes should match expectations // this is especially important for "sparse" plates where the first // row and/or column in the plate are missing - assertEquals(metadata.getWellRow(0, 0).getValue(), wellRow); - assertEquals(metadata.getWellColumn(0, 0).getValue(), wellColumn); - // all of the Images should be linked to the well - assertEquals(metadata.getWellSampleCount(0, 0), fields); - for (int f=0; f getTestCases() { return Stream.of( Arguments.of(new String[] { "A1_-1_1_1_Tsf[Phase Contrast]_001.tif" - }, 0, 0, 1, 1, 1, 1), + }, new int[] {0}, new int[] {0}, 1, 1, 1, 1), Arguments.of(new String[] { "A1_01_1_1_Phase Contrast_001.tif", "A1_01_1_2_Phase Contrast_001.tif", @@ -186,33 +194,111 @@ static Stream getTestCases() { "A1_01_1_7_Phase Contrast_001.tif", "A1_01_1_8_Phase Contrast_001.tif", "A1_01_1_9_Phase Contrast_001.tif", - }, 0, 0, 9, 1, 1, 1), + }, new int[] {0}, new int[] {0}, 9, 1, 1, 1), Arguments.of(new String[] { "P24_1_Bright Field_1_001_02.tif" - }, 15, 23, 1, 1, 1, 1), + }, new int[] {15}, new int[] {23}, 1, 1, 1, 1), Arguments.of(new String[] { "B2_1_Bright Field_1_001_02.tif" - }, 1, 1, 1, 1, 1, 1), + }, new int[] {1}, new int[] {1}, 1, 1, 1, 1), Arguments.of(new String[] { "A1_1_Stitched[AandB_Phase Contrast]_1_001_-1.tif" - }, 0, 0, 1, 1, 1, 1), + }, new int[] {0}, new int[] {0}, 1, 1, 1, 1), Arguments.of(new String[] { "A1_1Z0_DAPI_1_001_.tif", "A1_1Z1_DAPI_1_001_.tif", "A1_1Z2_DAPI_1_001_.tif", "A1_1Z3_DAPI_1_001_.tif", "A1_1Z4_DAPI_1_001_.tif" - }, 0, 0, 1, 5, 1, 1), + }, new int[] {0}, new int[] {0}, 1, 5, 1, 1), Arguments.of(new String[] { "A1_-1_1_1_Stitched[Channel1 300,400]_001.tif", "A1_-1_2_1_Stitched[Channel2 500,600]_001.tif", "A1_-1_3_1_Stitched[Channel 3 600,650]_001.tif" - }, 0, 0, 1, 1, 3, 1), + }, new int[] {0}, new int[] {0}, 1, 1, 3, 1), Arguments.of(new String[] { "A1_-2_1_1_Tsf[Stitched[Channel1 300,400]]_001.tif", "A1_-2_2_1_Tsf[Stitched[Channel2 500,600]]_001.tif", "A1_-2_3_1_Tsf[Stitched[Channel 3 600,650]]_001.tif" - }, 0, 0, 1, 1, 3, 1) + }, new int[] {0}, new int[] {0}, 1, 1, 3, 1), + Arguments.of(new String[] { + "B2/B2_1_Bright Field_1_001_02.tif", + "D3/D3_1_Bright Field_1_001_02.tif", + }, new int[] {1, 3}, new int[] {1, 2}, 1, 1, 1, 1), + Arguments.of(new String[] { + "ID_A9 info/A9_-1_1_1_Phase Contrast_001.tif", + "ID_A9 info/A9_-1_2_1_GFP_001.tif", + "ID_A9 info/A9_-1_3_1_DAPI_001.tif", + "ID_A10 info/A10_-1_1_1_Phase Contrast_001.tif", + "ID_A10 info/A10_-1_2_1_GFP_001.tif", + "ID_A10 info/A10_-1_3_1_DAPI_001.tif", + "ID_E9 info/E9_-1_1_1_Phase Contrast_001.tif", + "ID_E9 info/E9_-1_2_1_GFP_001.tif", + "ID_E9 info/E9_-1_3_1_DAPI_001.tif", + "ID_E10 info/E10_-1_1_1_Phase Contrast_001.tif", + "ID_E10 info/E10_-1_2_1_GFP_001.tif", + "ID_E10 info/E10_-1_3_1_DAPI_001.tif" + }, new int[] {0, 0, 4, 4}, new int[] {8, 9, 8, 9}, 1, 1, 3, 1), + Arguments.of(new String[] { + "A1ROI1_01_1_1Z0_Confocal DAPI_001.tif", + "A1ROI1_01_1_1Z1_Confocal DAPI_001.tif", + "A1ROI1_01_1_1Z2_Confocal DAPI_001.tif", + "A1ROI1_01_1_1Z0_Confocal RFP_001.tif", + "A1ROI1_01_1_1Z1_Confocal RFP_001.tif", + "A1ROI1_01_1_1Z2_Confocal RFP_001.tif", + "A1ROI1_01_1_2Z0_Confocal DAPI_001.tif", + "A1ROI1_01_1_2Z1_Confocal DAPI_001.tif", + "A1ROI1_01_1_2Z2_Confocal DAPI_001.tif", + "A1ROI1_01_1_2Z0_Confocal RFP_001.tif", + "A1ROI1_01_1_2Z1_Confocal RFP_001.tif", + "A1ROI1_01_1_2Z2_Confocal RFP_001.tif", + "A1ROI2_01_1_1Z0_Confocal DAPI_001.tif", + "A1ROI2_01_1_1Z1_Confocal DAPI_001.tif", + "A1ROI2_01_1_1Z2_Confocal DAPI_001.tif", + "A1ROI2_01_1_1Z0_Confocal RFP_001.tif", + "A1ROI2_01_1_1Z1_Confocal RFP_001.tif", + "A1ROI2_01_1_1Z2_Confocal RFP_001.tif", + "A1ROI2_01_1_2Z0_Confocal DAPI_001.tif", + "A1ROI2_01_1_2Z1_Confocal DAPI_001.tif", + "A1ROI2_01_1_2Z2_Confocal DAPI_001.tif", + "A1ROI2_01_1_2Z0_Confocal RFP_001.tif", + "A1ROI2_01_1_2Z1_Confocal RFP_001.tif", + "A1ROI2_01_1_2Z2_Confocal RFP_001.tif" + }, new int[] {0}, new int[] {0}, 4, 3, 2, 1), + Arguments.of(new String[] { + "A1ROI1_01_1_1Z0_Confocal DAPI_001.tif", + "A1ROI1_01_1_1Z1_Confocal DAPI_001.tif", + "A1ROI1_01_1_1Z2_Confocal DAPI_001.tif", + "A1ROI1_01_1_1Z0_Confocal RFP_001.tif", + "A1ROI1_01_1_1Z1_Confocal RFP_001.tif", + "A1ROI1_01_1_1Z2_Confocal RFP_001.tif", + "A1ROI2_01_1_1Z0_Confocal DAPI_001.tif", + "A1ROI2_01_1_1Z1_Confocal DAPI_001.tif", + "A1ROI2_01_1_1Z2_Confocal DAPI_001.tif", + "A1ROI2_01_1_1Z0_Confocal RFP_001.tif", + "A1ROI2_01_1_1Z1_Confocal RFP_001.tif", + "A1ROI2_01_1_1Z2_Confocal RFP_001.tif", + "A1ROI3_01_1_1Z0_Confocal DAPI_001.tif", + "A1ROI3_01_1_1Z1_Confocal DAPI_001.tif", + "A1ROI3_01_1_1Z2_Confocal DAPI_001.tif", + "A1ROI3_01_1_1Z0_Confocal RFP_001.tif", + "A1ROI3_01_1_1Z1_Confocal RFP_001.tif", + "A1ROI3_01_1_1Z2_Confocal RFP_001.tif", + }, new int[] {0}, new int[] {0}, 3, 3, 2, 1), + Arguments.of(new String[] { + "A1_01_3_4_Blue_001.tif", + "A1_01_3_3_Blue_001.tif", + "A1_01_3_2_Blue_001.tif", + "A1_01_3_1_Blue_001.tif", + "A1_01_2_4_Green_001.tif", + "A1_01_2_3_Green_001.tif", + "A1_01_2_2_Green_001.tif", + "A1_01_2_1_Green_001.tif", + "A1_01_1_4_Red_001.tif", + "A1_01_1_3_Red_001.tif", + "A1_01_1_2_Red_001.tif", + "A1_01_1_1_Red_001.tif" + }, new int[] {0}, new int[] {0}, 4, 1, 3, 1) ); }