Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

InCell: allow mix of 2D and 3D channels to be treated as a Z stack #4270

Merged
merged 3 commits into from
Feb 26, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 60 additions & 4 deletions components/formats-gpl/src/loci/formats/in/InCellReader.java
Original file line number Diff line number Diff line change
@@ -67,6 +67,9 @@ public class InCellReader extends FormatReader {

// -- Constants --

public static final String DUPLICATE_PLANES_KEY = "incell.duplicate_missing_planes";
public static final boolean DUPLICATE_PLANES_DEFAULT = true;

public static final String INCELL_MAGIC_STRING = "IN Cell Analyzer";
public static final String CYTELL_MAGIC_STRING = "Cytell";

@@ -112,6 +115,8 @@ public class InCellReader extends FormatReader {
private List<String> exFilters = new ArrayList<String>();
private List<String> emFilters = new ArrayList<String>();

private transient boolean variableZ = false;

// -- Constructor --

/** Constructs a new InCell 1000/2000 reader. */
@@ -125,6 +130,17 @@ public InCellReader() {
".im file";
}

// -- InCellReader API methods --

public boolean duplicatePlanes() {
MetadataOptions options = getMetadataOptions();
if (options instanceof DynamicMetadataOptions) {
return ((DynamicMetadataOptions) options).getBoolean(
DUPLICATE_PLANES_KEY, DUPLICATE_PLANES_DEFAULT);
}
return DUPLICATE_PLANES_DEFAULT;
}

// -- IFormatReader API methods --

/* @see loci.formats.IFormatReader#isThisType(String, boolean) */
@@ -189,9 +205,18 @@ public byte[] openBytes(int no, byte[] buf, int x, int y, int w, int h)
getSeries() % channelsPerTimepoint.size() : coordinates[2];
int image = getIndex(coordinates[0], coordinates[1], 0);

if (imageFiles[well][field][timepoint][image] == null) return buf;
if (imageFiles[well][field][timepoint][image] == null) {
// unless otherwise configured, copy the first Z section
// to any other planes in the Z stack that are missing
if (duplicatePlanes() && coordinates[0] > 0) {
return openBytes(getIndex(0, coordinates[1], coordinates[2]), buf, x, y, w, h);
}
return buf;
}
String filename = imageFiles[well][field][timepoint][image].filename;
if (filename == null || !(new Location(filename).exists())) return buf;
if (filename == null || !(new Location(filename).exists())) {
return buf;
}

if (imageFiles[well][field][timepoint][image].isTiff) {
try {
@@ -280,6 +305,7 @@ public void close(boolean fileOnly) throws IOException {
refractive = null;
emFilters.clear();
exFilters.clear();
variableZ = false;
}
}

@@ -327,6 +353,14 @@ public int getOptimalTileHeight() {

// -- Internal FormatReader API methods --

/* @see loci.formats.FormatReader#getAvailableOptions() */
@Override
protected ArrayList<String> getAvailableOptions() {
ArrayList<String> optionsList = super.getAvailableOptions();
optionsList.add(DUPLICATE_PLANES_KEY);
return optionsList;
}

/* @see loci.formats.FormatReader#initFile(String) */
@Override
protected void initFile(String id) throws FormatException, IOException {
@@ -464,7 +498,18 @@ protected void initFile(String id) throws FormatException, IOException {
}
int expectedSeries = totalImages / (getSizeZ() * getSizeC() * getSizeT());
if (expectedSeries > 0) {
seriesCount = (int) Math.min(seriesCount, expectedSeries);
if (variableZ) {
// we know that different channels may have different Z sizes,
// so don't try to recalculate the series count based on
// SizeZ * SizeC
LOGGER.warn("Using series count {} but plane count indicates {}",
seriesCount, expectedSeries);
}
else {
// we expect all channels to have the same Z size,
// so recalculate the series count based on the expected number of planes
seriesCount = (int) Math.min(seriesCount, expectedSeries);
}
}
}
else seriesCount = totalImages / (getSizeZ() * getSizeC() * getSizeT());
@@ -908,8 +953,19 @@ else if (qName.equals("Wavelength")) {
String fusion = attributes.getValue("fusion_wave");
if (fusion.equals("false")) ms0.sizeC++;
String mode = attributes.getValue("imaging_mode");

// different wavelengths (channels) may have different imaging modes
// we want to allow a Z stack if one or more "3-D" imaging modes
// are encountered, even if some are variations on "2-D"
if (mode != null) {
doZ = mode.equals("3-D");
boolean is3D = mode.equals("3-D");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An interesting realisation while browsing the xdce files that we have received is that every Wavelength element that includes some imaging_mode attribute also includes z_section/z_slice/z_index attributes.

At first look, these would seem to be a more reliable way of identifying images with varying acquired Z-section. However, looking specifically at the XML of the public dataset uploaded to Zenodo

        <Wavelengths>
            <Wavelength aperture_rows="50" dof_factor="8.00" edge_confocal_mode="false" edge_confocal_width="-1" fusion_wave="false" imaging_mode="2-D Deconvolution" index="0" label="" laser_power="100" name="FITC_511" num_frames_to_avg="1" open_aperture="true" show="false" z_section="0.000" z_slice="1" z_start_index="0" z_step="0.000">
            </Wavelength>
            <Wavelength aperture_rows="50" dof_factor="8.00" edge_confocal_mode="false" edge_confocal_width="-1" fusion_wave="false" imaging_mode="3-D" index="1" label="" laser_power="100" name="DAPI" num_frames_to_avg="1" open_aperture="true" show="false" z_section="64.000" z_slice="16" z_start_index="-4" z_step="4.000">
            </Wavelength>
            <Wavelength aperture_rows="50" dof_factor="8.00" edge_confocal_mode="false" edge_confocal_width="-1" fusion_wave="false" imaging_mode="2-D" index="2" label="" laser_power="100" name="Cy3" num_frames_to_avg="1" open_aperture="true" show="false" z_section="4.000" z_slice="3" z_start_index="-1" z_step="4.000">
            </Wavelength>
            <Wavelength aperture_rows="50" dof_factor="8.00" edge_confocal_mode="false" edge_confocal_width="-1" fusion_wave="false" imaging_mode="2-D" index="3" label="" laser_power="100" name="Brightfield" num_frames_to_avg="1" open_aperture="true" show="false" z_section="0.000" z_slice="1" z_start_index="0" z_step="0.000">
            </Wavelength>
        </Wavelengths>

the Cy3 channel is marked as imaging_mode="2-D" with z_slice="3"

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, the z_* attributes here aren't really usable for setting the Z size for the channel. Maybe z_start_index could be used when the duplicate planes option is turned off, to pick where the single plane sits within the stack? But I don't know if that's actually useful, or just adds confusion.

// record if there were different imaging modes found
if (!variableZ && is3D != doZ && ms0.sizeC > 1) {
variableZ = true;
}
if (ms0.sizeC == 1 || !doZ) {
doZ = is3D;
}
}
}
else if (qName.equals("AcqWave")) {