Skip to content

Commit

Permalink
Allow BioTek plates to be split across multiple subdirectories
Browse files Browse the repository at this point in the history
  • Loading branch information
melissalinkert committed Oct 26, 2023
1 parent d61c97d commit cee34a2
Show file tree
Hide file tree
Showing 2 changed files with 125 additions and 45 deletions.
96 changes: 82 additions & 14 deletions src/main/java/com/glencoesoftware/bioformats2raw/BioTekReader.java
Original file line number Diff line number Diff line change
Expand Up @@ -212,17 +212,53 @@ protected void initFile(String id) throws FormatException, IOException {

String[] files = parent.list(true);
Arrays.sort(files);

// is there only one well in the directory?
// compare the well identifiers (relative file name up to first _)
boolean sameWell = true;
int endIndex = files[0].indexOf("_");
if (endIndex > 0) {
String wellCheck = files[0].substring(0, endIndex);
LOGGER.debug("well check string = {}", wellCheck);
for (int i=0; i<files.length; i++) {
if (!files[i].startsWith(wellCheck)) {
sameWell = false;
break;
}
}
}
LOGGER.debug("single well in {}: {}", parent, sameWell);
// if only one well exists, look in other subdirectories of the parent
if (sameWell) {
Location plateDir = parent.getParentFile();
LOGGER.debug("plate directory = {}", plateDir);
String[] wellDirs = plateDir.list(true);
ArrayList<String> allFiles = new ArrayList<String>();
for (String well : wellDirs) {
Location wellDir = new Location(plateDir, well).getAbsoluteFile();
LOGGER.debug("looking in well directory = {}", wellDir);
String[] f = wellDir.list(true);
for (String file : f) {
LOGGER.debug(" adding well file {}", file);
allFiles.add(new Location(wellDir, file).getAbsolutePath());
}
}
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);
ArrayList<WellIndex> validWellRowCol = new ArrayList<WellIndex>();
int maxRow = 0;
int minRow = Integer.MAX_VALUE;
int maxCol = 0;
int minCol = Integer.MAX_VALUE;
int maxPlateAcq = 0;
Map<Integer, Integer> maxField = new HashMap<Integer, Integer>();

for (String f : files) {
for (String absolutePath : files) {
String f = new Location(absolutePath).getName();
Matcher m = regexA.matcher(f);
int rowIndex = -1;
int colIndex = -1;
Expand Down Expand Up @@ -269,20 +305,17 @@ protected void initFile(String id) throws FormatException, IOException {
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) {
Expand All @@ -292,6 +325,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);
Expand Down Expand Up @@ -443,12 +477,14 @@ else if (pa == 1) {

int nextImage = 0;
int[] nextWellSample = new int[(maxRow + 1) * (maxCol + 1)];
int totalColumns = (maxCol - minCol) + 1;
for (int w=0; w<wells.size(); w++) {
BioTekWell well = wells.get(w);
int effectiveRow = well.getRowIndex() - minRow;
int effectiveColumn = well.getColumnIndex() - minCol;
int wellIndex = effectiveRow * totalColumns + effectiveColumn;
int wellIndex = validWellRowCol.indexOf(
new WellIndex(well.getRowIndex(), well.getColumnIndex()));
LOGGER.debug(
"well #{}, row = {}, col = {}, index = {}",
w, well.getRowIndex(), well.getColumnIndex(), wellIndex);

well.fillMetadataStore(store, 0, well.getPlateAcquisition(), wellIndex,
nextWellSample[wellIndex], nextImage);

Expand Down Expand Up @@ -976,4 +1012,36 @@ public Channel(String name) {
}
}

class WellIndex implements Comparable<WellIndex> {
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);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -80,24 +85,23 @@ 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
* @param sizeT number of timepoints
*/
@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
Expand All @@ -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<fields; f++) {
// sanity check that the Images are linked to the
// well in the correct order
assertEquals(metadata.getWellSampleImageRef(0, 0, f), "Image:" + f);
// check that the number of Z sections, channels, and timepoints
// all match expectations
assertEquals(metadata.getPixelsSizeZ(f).getValue(), sizeZ);
assertEquals(metadata.getPixelsSizeC(f).getValue(), sizeC);
assertEquals(metadata.getPixelsSizeT(f).getValue(), sizeT);
for (int w=0; w<wellRow.length; w++) {
assertEquals(metadata.getWellRow(0, w).getValue(), wellRow[w]);
assertEquals(metadata.getWellColumn(0, w).getValue(), wellColumn[w]);
// all of the fields should be linked to the well
assertEquals(metadata.getWellSampleCount(0, w), fields);
for (int f=0; f<fields; f++) {
// sanity check that the Images are linked to the
// well in the correct order
int imageIndex = (fields * w) + f;
assertEquals(metadata.getWellSampleImageRef(0, w, f),
"Image:" + imageIndex);
// check that the number of Z sections, channels, and timepoints
// all match expectations
assertEquals(metadata.getPixelsSizeZ(imageIndex).getValue(), sizeZ);
assertEquals(metadata.getPixelsSizeC(imageIndex).getValue(), sizeC);
assertEquals(metadata.getPixelsSizeT(imageIndex).getValue(), sizeT);
}
}
}
}
Expand Down Expand Up @@ -175,7 +183,7 @@ static Stream<Arguments> 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",
Expand All @@ -186,33 +194,37 @@ static Stream<Arguments> 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)
);
}

Expand Down

0 comments on commit cee34a2

Please sign in to comment.