Skip to content

Commit b042606

Browse files
committed
Fix: Image parsers parsing RAI/ImgPlus but initializing on ImagePlus...
... This makes this commands available in stream mode
1 parent d65e3b6 commit b042606

7 files changed

Lines changed: 125 additions & 47 deletions

File tree

‎src/main/java/sc/fiji/snt/DelineationsManager.java‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -647,17 +647,17 @@ JPopupMenu importMenu() {
647647
if (sntui.noPathsError()) return;
648648
final ImagePlus labelImp = getLabelImage();
649649
if (labelImp == null) return;
650-
final ImagePlus tracingImp = sntui.plugin.getImagePlus();
651-
if (tracingImp != null) {
652-
final boolean dimMatch = labelImp.getWidth() == tracingImp.getWidth()
653-
&& labelImp.getHeight() == tracingImp.getHeight()
654-
&& labelImp.getNSlices() == tracingImp.getNSlices();
650+
// NB: not plugin.getImagePlus() != null -- that is null in Stream mode!
651+
if (sntui.plugin.accessToValidImageData()) {
652+
final int[] fullDims = sntui.plugin.getFullImageDimensions();
653+
final boolean dimMatch = labelImp.getWidth() == fullDims[0]
654+
&& labelImp.getHeight() == fullDims[1]
655+
&& labelImp.getNSlices() == fullDims[2];
655656
if (!dimMatch && !sntui.guiUtils.getConfirmation(
656657
"The label image dimensions (" + labelImp.getWidth() + "×"
657658
+ labelImp.getHeight() + "×" + labelImp.getNSlices()
658659
+ ") do not match the tracing image ("
659-
+ tracingImp.getWidth() + "×" + tracingImp.getHeight()
660-
+ "×" + tracingImp.getNSlices()
660+
+ fullDims[0] + "×" + fullDims[1] + "×" + fullDims[2]
661661
+ "). Assignments may be incorrect. Proceed anyway?",
662662
"Dimension Mismatch")) {
663663
return;

‎src/main/java/sc/fiji/snt/SNT.java‎

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4415,22 +4415,28 @@ public void loadSecondaryImage(final File file) throws IOException, IllegalArgum
44154415
// Falls back to the conventional path for anything it can't resolve
44164416
// (resolvePathToSource() itself ends in a conventional ImgPlus-open fallback too, but keeping
44174417
// our own fallback here avoids any behavior change for formats that already worked before).
4418-
RandomAccessibleInterval<?> data = null;
4418+
// Resolution and dimension-checking are deliberately two separate steps here: only a failure to
4419+
// resolve/parse the source at all should fall back to the conventional (RAM-unsafe-for-big-data)
4420+
// path below. A dimension mismatch on an already-resolved big-data source must NOT fall back to it
4421+
Object resolvedSource = null;
44194422
try {
4420-
data = extractSecondaryData(SpimDataUtils.resolvePathToSource(file.getAbsolutePath()));
4423+
resolvedSource = SpimDataUtils.resolvePathToSource(file.getAbsolutePath());
44214424
} catch (final Exception e) {
44224425
SNTUtils.log("Secondary layer: '" + file.getName() + "' could not be resolved as a big-data "
44234426
+ "container (" + e.getMessage() + "); falling back to conventional image opening");
44244427
}
4425-
if (data != null) {
4426-
// computeStatistics=false: data here may be a lazily-backed (N5/Zarr/BDV) source, and eager
4427-
// min/max/mean/stdDev via generic OpService calls is 3 separate full-volume passes -- real
4428-
// disk I/O per chunk, done 3x over, with no caching between passes. That may work for the
4429-
// classic ImagePlus but not for lazy data.
4430-
//noinspection unchecked,rawtypes
4431-
loadSecondaryImage((RandomAccessibleInterval) data, true, false);
4432-
setSecondaryImage(isSecondaryDataAvailable() ? file : null);
4433-
return;
4428+
if (resolvedSource != null) {
4429+
final RandomAccessibleInterval<?> data = extractSecondaryData(resolvedSource);
4430+
if (data != null) {
4431+
// computeStatistics=false: data here may be a lazily-backed (N5/Zarr/BDV) source, and eager
4432+
// min/max/mean/stdDev via generic OpService calls is 3 separate full-volume passes -- real
4433+
// disk I/O per chunk, done 3x over, with no caching between passes. That may work for the
4434+
// classic ImagePlus but not for lazy data.
4435+
//noinspection unchecked,rawtypes
4436+
loadSecondaryImage((RandomAccessibleInterval) data, true, false);
4437+
setSecondaryImage(isSecondaryDataAvailable() ? file : null);
4438+
return;
4439+
}
44344440
}
44354441
final ImagePlus imp = openCachedDataImage(file);
44364442
loadSecondaryImage(imp, true);
@@ -4440,14 +4446,24 @@ public void loadSecondaryImage(final File file) throws IOException, IllegalArgum
44404446
/**
44414447
* Extracts a {@link RandomAccessibleInterval} for the currently active channel/frame from a
44424448
* resolved big-data source (see {@link SpimDataUtils#resolvePathToSource(String)}), or
4443-
* {@code null} if {@code source} isn't one of the recognized types, or its XYZ dimensions don't
4444-
* match the data currently being traced (see {@link #sameXYZDimensionsAsTracingData}).
4449+
* {@code null} if {@code source} isn't one of the recognized types (the caller, {@link
4450+
* #loadSecondaryImage(File)}, then falls back to conventional image opening).
4451+
* <p>
4452+
* If {@code source} <i>is</i> one of the recognized types but its XYZ dimensions don't match the
4453+
* data currently being traced (see {@link #sameXYZDimensionsAsTracingData}), this throws instead
4454+
* of returning {@code null}: unlike a genuinely unresolved source, this data was already
4455+
* successfully resolved as big data, so silently falling back to the conventional path would mean
4456+
* fully loading a potentially huge dataset into RAM via SCIFIO/Bio-Formats just to discover its
4457+
* dimensions are wrong. Failing fast here, with the same message {@link #openCachedDataImage(File)}
4458+
* gives for the same condition, avoids that.
44454459
* <p>
44464460
* For {@link AbstractSpimData} (BDV .xml/IMS) and {@link SpimDataUtils.N5Sources} (ambiguous
44474461
* N5/Zarr layouts), only the first setup/source and timepoint 0 are used. This is the same
44484462
* simplification {@code BigDataLoaderCmd#applyFallbackCalibration} already makes for the
44494463
* <i>primary</i> volume in this same situation. A genuinely multichannel secondary layer of
44504464
* either of these types would thus only expose its first channel.
4465+
*
4466+
* @throws IllegalArgumentException if the resolved source's dimensions don't match the data currently being traced
44514467
*/
44524468
private RandomAccessibleInterval<?> extractSecondaryData(final Object source) {
44534469
final RandomAccessibleInterval<?> data;
@@ -4473,9 +4489,10 @@ private RandomAccessibleInterval<?> extractSecondaryData(final Object source) {
44734489
return null; // unresolved / unrecognized type
44744490
}
44754491
}
4476-
if (!sameXYZDimensionsAsTracingData(data)) {
4477-
SNTUtils.log("Secondary layer: dimensions do not match those of the data being traced");
4478-
return null;
4492+
if (!sameXYZDimensionsAsTracingData(data)) { // NB: throws rather than returning null
4493+
final String msg = (isStreamMode())
4494+
? "" : " If this is unexpected, check under 'Image>Properties...' that CZT axes are not swapped.";
4495+
throw new IllegalArgumentException("Dimensions do not match those of the image being traced." + msg);
44794496
}
44804497
return data;
44814498
}
@@ -4673,9 +4690,9 @@ private ImagePlus openCachedDataImage(final File file) throws IOException {
46734690
if (!sameXYZDimensionsAsTracingData(imp)) {
46744691
// We are imposing only XYZ dimensions to e.g., allow for loading of single-channel
46754692
// p-maps. Hopefully, being lax about CT dimensions won't cause issues downstream
4676-
final String refName = (xy != null) ? xy.getTitle() : "the image being traced";
4677-
throw new IllegalArgumentException("Dimensions do not match those of " + refName
4678-
+ ". If this is unexpected, check under 'Image>Properties...' that CZT axes are not swapped.");
4693+
final String msg = (isStreamMode())
4694+
? "" : " If this is unexpected, check under 'Image>Properties...' that CZT axes are not swapped.";
4695+
throw new IllegalArgumentException("Dimensions do not match those of the image being traced." + msg);
46794696
}
46804697
return imp;
46814698
}

‎src/main/java/sc/fiji/snt/SNTUI.java‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3257,7 +3257,6 @@ private JPanel secondaryDataPanel() {
32573257
commandFinder.register(mi2, "Main tab", "Interactive tracing (II Layer)");
32583258
final JMenuItem mi3 = GuiUtils.MenuItems.fromFileImage();
32593259
mi3.addActionListener(e -> loadSecondaryImage(false));
3260-
mi2.setEnabled(!plugin.isStreamMode()); // ChooseDatasetCmd is not yet ready for OME-ZARR/N5 etc. disable for now
32613260
commandFinder.register(mi3, "Main tab", "Interactive tracing (II Layer)");
32623261
final JMenuItem mi4 = new JMenuItem("Flush Current Layer...", IconFactory.menuIcon(IconFactory.GLYPH.TOILET));
32633262
registerInCommandFinder(mi4, "Flush Secondary Layer", "Main tab", "Interactive tracing");
@@ -3346,7 +3345,12 @@ private void loadSecondaryImage(final boolean fromAlreadyOpenImageOtherwiseFile)
33463345
warnOnAutoCTcompatibilityOthers();
33473346
final File proposedFile = (plugin.getFilteredImageFile() == null) ? plugin.getPrefs().getRecentDir()
33483347
: plugin.getFilteredImageFile();
3349-
final File file = guiUtils.getOpenFile("Choose Secondary Image", proposedFile);
3348+
// An N5/OME-Zarr secondary layer is a directory, not a single file, and SNT#loadSecondaryImage(File)
3349+
// (via SpimDataUtils#resolvePathToSource) supports resolving one
3350+
final File file = (plugin.isStreamMode())
3351+
? guiUtils.getOpenFileOrDirectory("Choose Secondary Image", proposedFile)
3352+
: guiUtils.getOpenFile("Choose Secondary Image", proposedFile);
3353+
33503354
if (file != null)
33513355
loadSecondaryImageFile(file);
33523356
}

‎src/main/java/sc/fiji/snt/gui/GuiUtils.java‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1271,6 +1271,22 @@ public File getOpenFile(final String title, final File file, final String... all
12711271
return (File) showOpenDialog(title, JFileChooser.FILES_ONLY, false, file, false, filter);
12721272
}
12731273

1274+
/**
1275+
* As {@link #getOpenFile(String, File, String...)}, but the dialog also allows selecting a
1276+
* directory directly. Needed for pointing directly at an N5/OME-Zarr container; a metadata file
1277+
* *inside* such a container (e.g. {@code zarr.json}, {@code .zattrs}) can still be selected too.
1278+
* Both resolve to the same container root,
1279+
* <p>
1280+
* NB: unlike most {@code getXxxFile(s)} methods above, this always uses the classic chooser and
1281+
* ignores {@link SNTPrefs#getUseNativeFileChooser()} -- FlatLaf's {@link SystemFileChooser} (the
1282+
* native alternative) does not support a mixed files-and-directories selection mode at all.
1283+
*/
1284+
public File getOpenFileOrDirectory(final String title, final File file) {
1285+
final JFileChooser chooser = fileChooser(title, file, JFileChooser.OPEN_DIALOG,
1286+
JFileChooser.FILES_AND_DIRECTORIES);
1287+
return (File) getOpenFileChooserResult(chooser);
1288+
}
1289+
12741290
public File getSaveFile(final String title, final File file, final String... allowedExtensions) {
12751291
File chosenFile = null;
12761292
final JFileChooser chooser = fileChooser(title, file, JFileChooser.SAVE_DIALOG, JFileChooser.FILES_ONLY);

‎src/main/java/sc/fiji/snt/gui/cmds/AlongPathDetectorCmd.java‎

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
import ij.ImagePlus;
2626
import ij.plugin.frame.RoiManager;
27+
import net.imagej.Dataset;
2728
import net.imglib2.RandomAccessibleInterval;
2829
import net.imglib2.type.numeric.RealType;
2930
import org.scijava.command.Command;
@@ -110,16 +111,17 @@ private void init() {
110111

111112
channel = snt.getChannel();
112113

113-
final boolean hasImage = snt.accessToValidImageData()
114-
&& snt.getImagePlus() != null;
114+
// NB: not snt.getImagePlus() != null -- that is null in Stream mode unless a materialized crop is active
115+
final Dataset dataset = snt.accessToValidImageData() ? snt.getDataset() : null;
116+
final boolean hasImage = dataset != null;
115117

116118
if (hasImage) {
117-
if (snt.getImagePlus().getNChannels() == 1) {
119+
if (dataset.getChannels() == 1) {
118120
resolveInput("channel");
119121
} else {
120122
final MutableModuleItem<Integer> mItem = getInfo()
121123
.getMutableInput("channel", Integer.class);
122-
mItem.setMaximumValue(snt.getImagePlus().getNChannels());
124+
mItem.setMaximumValue((int) dataset.getChannels());
123125
}
124126
} else {
125127
// No image: force intensity filtering off and hide related params
@@ -161,7 +163,8 @@ public void run() {
161163
final RandomAccessibleInterval<? extends RealType<?>> intensityImg;
162164
double resolvedMinIntensity = minIntensity;
163165
if (useIntensity) {
164-
if (snt == null || !snt.accessToValidImageData() || snt.getImagePlus() == null) {
166+
// NB: not snt.getImagePlus() == null -- see the same note in init() above
167+
if (snt == null || !snt.accessToValidImageData()) {
165168
error("Intensity filtering requires valid image data to be loaded.");
166169
return;
167170
}
@@ -175,8 +178,15 @@ public void run() {
175178
}
176179
if (snt.getChannel() == channel) {
177180
intensityImg = snt.getLoadedData();
178-
} else {
181+
} else if (snt.getImagePlus() != null) {
179182
intensityImg = ImgUtils.getCtSlice3d(snt.getImagePlus(), channel, snt.getFrame());
183+
} else {
184+
// Stream mode without a materialized crop: only the active channel's data is resident
185+
// (see SNT#getDataset()), so a different channel genuinely cannot be sampled here
186+
error("Intensity filtering on a channel other than the active one (" + snt.getChannel() + ")"
187+
+ " requires either a materialized crop or Classic mode. Set 'Intensity channel' to "
188+
+ snt.getChannel() + ", or filter using radius only (Min. intensity = 0).");
189+
return;
180190
}
181191
} else {
182192
intensityImg = null;

‎src/main/java/sc/fiji/snt/gui/cmds/LabelProximityDetectorCmd.java‎

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -99,12 +99,15 @@ private void init() {
9999
continue; // skip binary and hyperstacks; only first C/T would be used
100100

101101
// Quick dimension check: label image should match tracing image spatially
102-
final ImagePlus tracingImp = snt.getImagePlus();
103-
if (tracingImp != null) {
104-
if (imp.getWidth() != tracingImp.getWidth() || imp.getHeight() != tracingImp.getHeight()) {
102+
// NB: not snt.getImagePlus() != null -- that is null in Stream mode unless a materialized
103+
// crop is active, which would let every open image through unfiltered in that case.
104+
// getFullImageDimensions() resolves correctly in every mode (Classic, Stream, materialized crop)
105+
if (snt.accessToValidImageData()) {
106+
final int[] fullDims = snt.getFullImageDimensions();
107+
if (imp.getWidth() != fullDims[0] || imp.getHeight() != fullDims[1]) {
105108
continue;
106109
}
107-
if (tracingImp.getNSlices() > 1 && imp.getNSlices() != tracingImp.getNSlices()) {
110+
if (fullDims[2] > 1 && imp.getNSlices() != fullDims[2]) {
108111
continue;
109112
}
110113
}
@@ -151,10 +154,16 @@ public void run() {
151154
}
152155

153156
// Dimension check against tracing data
154-
final ImagePlus tracingImp = snt.getImagePlus();
155-
if (tracingImp != null && !ImgUtils.haveSameSpatialDimensions(tracingImp, labelImg)) {
156-
msg("Label image dimensions do not match the tracing image.\n"
157-
+ "Results may be inaccurate.", "Dimension Mismatch");
157+
// NB: not snt.getImagePlus() != null -- see the same note in init() above
158+
if (snt.accessToValidImageData()) {
159+
final int[] fullDims = snt.getFullImageDimensions();
160+
final boolean dimMatch = labelImg.dimension(0) == fullDims[0]
161+
&& labelImg.dimension(1) == fullDims[1]
162+
&& (labelImg.numDimensions() <= 2 || labelImg.dimension(2) == fullDims[2]);
163+
if (!dimMatch) {
164+
msg("Label image dimensions do not match the tracing image.\n"
165+
+ "Results may be inaccurate.", "Dimension Mismatch");
166+
}
158167
}
159168

160169
// Build config

‎src/main/java/sc/fiji/snt/gui/cmds/PeripathDetectorCmd.java‎

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
import ij.ImagePlus;
2626
import ij.plugin.frame.RoiManager;
27+
import net.imagej.Dataset;
2728
import net.imglib2.RandomAccessibleInterval;
2829
import net.imglib2.type.numeric.RealType;
2930
import org.scijava.command.Command;
@@ -124,14 +125,18 @@ private void init() {
124125
? p.getFitted() : p)
125126
.collect(Collectors.toList());
126127
channel = snt.getChannel();
127-
if (snt.accessToValidImageData()) {
128-
if (snt.getImagePlus() != null && snt.getImagePlus().getNChannels() == 1) {
128+
// NB: not snt.getImagePlus() != null -- that is null in Stream mode unless a materialized crop
129+
// is active. snt.getDataset() already handles that case: it falls back to a Dataset built from
130+
// the currently-streamed data when no resident ImagePlus exists
131+
final Dataset dataset = snt.accessToValidImageData() ? snt.getDataset() : null;
132+
if (dataset != null) {
133+
if (dataset.getChannels() == 1) {
129134
// Only 1 channel available
130135
resolveInput("channel");
131136
} else {
132137
// Set channel max from loaded image
133138
final MutableModuleItem<Integer> mItem = getInfo().getMutableInput("channel", Integer.class);
134-
mItem.setMaximumValue((snt.getImagePlus() != null) ? snt.getImagePlus().getNChannels() : 1);
139+
mItem.setMaximumValue((int) dataset.getChannels());
135140
}
136141
// Pre-fill prominence from image stats if available
137142
final double[] stats = new double[]{snt.getStats().min, snt.getStats().max};
@@ -173,7 +178,10 @@ public void run() {
173178
error("No paths selected for analysis.");
174179
return;
175180
}
176-
if (snt == null || !snt.accessToValidImageData() || snt.getImagePlus() == null) {
181+
// NB: not snt.getImagePlus() == null -- see the same note in init() above. Requiring only
182+
// accessToValidImageData() lets detection work in Stream mode for the case of the currently active channel,
183+
// via snt.getLoadedData() below
184+
if (snt == null || !snt.accessToValidImageData()) {
177185
error("Maxima detection requires valid image data to be loaded.");
178186
return;
179187
}
@@ -182,8 +190,15 @@ public void run() {
182190
final RandomAccessibleInterval<? extends RealType<?>> detectionImg;
183191
if (snt.getChannel() == channel) {
184192
detectionImg = snt.getLoadedData();
185-
} else {
193+
} else if (snt.getImagePlus() != null) {
186194
detectionImg = ImgUtils.getCtSlice3d(snt.getImagePlus(), channel, snt.getFrame());
195+
} else {
196+
// Stream mode without a materialized crop: only the active channel's data is resident
197+
// (see SNT#getDataset() javadoc), so a different channel genuinely cannot be sampled here
198+
error("Detection on a channel other than the active one (" + snt.getChannel() + ")"
199+
+ " requires either a materialized crop or Classic mode. Set 'Detection channel' to "
200+
+ snt.getChannel() + ".");
201+
return;
187202
}
188203

189204
// Build config
@@ -252,6 +267,13 @@ public void run() {
252267
} else {
253268
// Add to ROI Manager: one grouped PointRoi per path
254269
final ImagePlus imp = snt.getImagePlus();
270+
if (imp == null) {
271+
// Stream mode without a materialized crop: no classic canvas for RoiManager/PointRoi to
272+
// attach to (unlike detection itself above, this really has no Stream-mode equivalent)
273+
error("ROI output requires an image canvas to be available (Classic mode, or a "
274+
+ "materialized crop in Stream mode). Use 'Bookmarked locations' output instead.");
275+
return;
276+
}
255277
RoiManager rm = RoiManager.getInstance2();
256278
if (rm == null) rm = new RoiManager();
257279
for (final Map.Entry<Path, List<Detection>> entry : resultsByPath.entrySet()) {

0 commit comments

Comments
 (0)