Skip to content

Commit 2d2cd10

Browse files
committed
Extend tracing on secondary layer to stream mode
1 parent e0a59d2 commit 2d2cd10

7 files changed

Lines changed: 339 additions & 18 deletions

File tree

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

Lines changed: 157 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import ij.process.ShortProcessor;
3434
import ij3d.*;
3535
import io.scif.services.DatasetIOService;
36+
import mpicbg.spim.data.generic.AbstractSpimData;
3637
import net.imagej.Dataset;
3738
import net.imagej.DatasetService;
3839
import net.imagej.ImgPlus;
@@ -80,6 +81,7 @@
8081
import sc.fiji.snt.filter.Tubeness;
8182
import sc.fiji.snt.gui.GuiUtils;
8283
import sc.fiji.snt.hyperpanes.MultiDThreePanes;
84+
import sc.fiji.snt.io.SpimDataUtils;
8385
import sc.fiji.snt.plugin.ShollAnalysisTreeCmd;
8486
import sc.fiji.snt.seed.SeedOverlay;
8587
import sc.fiji.snt.seed.SeedOverlayCanvasHandler;
@@ -3363,11 +3365,84 @@ public void setSecondaryImage(final File file) {
33633365
* @see #getSecondaryDataAsImp()
33643366
*/
33653367
public void loadSecondaryImage(final File file) throws IOException, IllegalArgumentException {
3368+
if (!SNTUtils.fileAvailable(file)) {
3369+
throw new IllegalArgumentException("File path of input data unknown");
3370+
}
3371+
// Big-data formats (N5, Zarr, BDV .xml/.h5, IMS, remote URLs) aren't openable via the classic
3372+
// SCIFIO/Bio-Formats path openCachedDataImage() relies on, e.g.m a BDV .xml companion of a
3373+
// large .h5 dataset fails there with "Could not find H5 file location in XML" (Bio-Formats
3374+
// trying, and failing, to parse it as its own HDF5-companion XML dialect). Try SpimDataUtils'
3375+
// resolution first (the same one BigDataLoaderCmd's own volume fields use) and, on success,
3376+
// route through the RAI-based loadSecondaryImage() overload below, which also sidesteps
3377+
// openCachedDataImage()'s need for a classic tracing canvas (xy) entirely.
3378+
// Falls back to the conventional path for anything it can't resolve
3379+
// (resolvePathToSource() itself ends in a conventional ImgPlus-open fallback too, but keeping
3380+
// our own fallback here avoids any behavior change for formats that already worked before).
3381+
RandomAccessibleInterval<?> data = null;
3382+
try {
3383+
data = extractSecondaryData(SpimDataUtils.resolvePathToSource(file.getAbsolutePath()));
3384+
} catch (final Exception e) {
3385+
SNTUtils.log("Secondary layer: '" + file.getName() + "' could not be resolved as a big-data "
3386+
+ "container (" + e.getMessage() + "); falling back to conventional image opening");
3387+
}
3388+
if (data != null) {
3389+
// computeStatistics=false: data here may be a lazily-backed (N5/Zarr/BDV) source, and eager
3390+
// min/max/mean/stdDev via generic OpService calls is 3 separate full-volume passes -- real
3391+
// disk I/O per chunk, done 3x over, with no caching between passes. That may work for the
3392+
// classic ImagePlus but not for lazy data.
3393+
//noinspection unchecked,rawtypes
3394+
loadSecondaryImage((RandomAccessibleInterval) data, true, false);
3395+
setSecondaryImage(isSecondaryDataAvailable() ? file : null);
3396+
return;
3397+
}
33663398
final ImagePlus imp = openCachedDataImage(file);
33673399
loadSecondaryImage(imp, true);
33683400
setSecondaryImage(isSecondaryDataAvailable() && isSecondaryImageFileLoaded() ? file : null);
33693401
}
33703402

3403+
/**
3404+
* Extracts a {@link RandomAccessibleInterval} for the currently active channel/frame from a
3405+
* resolved big-data source (see {@link SpimDataUtils#resolvePathToSource(String)}), or
3406+
* {@code null} if {@code source} isn't one of the recognized types, or its XYZ dimensions don't
3407+
* match the data currently being traced (see {@link #sameXYZDimensionsAsTracingData}).
3408+
* <p>
3409+
* For {@link AbstractSpimData} (BDV .xml/IMS) and {@link SpimDataUtils.N5Sources} (ambiguous
3410+
* N5/Zarr layouts), only the first setup/source and timepoint 0 are used. This is the same
3411+
* simplification {@code BigDataLoaderCmd#applyFallbackCalibration} already makes for the
3412+
* <i>primary</i> volume in this same situation. A genuinely multichannel secondary layer of
3413+
* either of these types would thus only expose its first channel.
3414+
*/
3415+
private RandomAccessibleInterval<?> extractSecondaryData(final Object source) {
3416+
final RandomAccessibleInterval<?> data;
3417+
switch (source) {
3418+
case ImgPlus<?> img ->
3419+
data = ImgUtils.getCtSlice((Dataset) img, Math.max(0, channel - 1), Math.max(0, frame - 1));
3420+
case AbstractSpimData<?> spimData -> {
3421+
final var setups = spimData.getSequenceDescription().getViewSetupsOrdered();
3422+
if (setups.isEmpty()) return null;
3423+
final var setup = setups.getFirst();
3424+
try {
3425+
data = spimData.getSequenceDescription().getImgLoader()
3426+
.getSetupImgLoader(setup.getId()).getImage(0); // timepoint 0
3427+
} catch (final Exception e) {
3428+
SNTUtils.log("Secondary layer: could not access pixel data from SpimData ImgLoader ("
3429+
+ e.getMessage() + ")");
3430+
return null;
3431+
}
3432+
}
3433+
case SpimDataUtils.N5Sources n5 when !n5.sources.isEmpty() ->
3434+
data = n5.sources.getFirst().getSpimSource().getSource(0, 0); // timepoint 0, full-resolution level 0
3435+
case null, default -> {
3436+
return null; // unresolved / unrecognized type
3437+
}
3438+
}
3439+
if (!sameXYZDimensionsAsTracingData(data)) {
3440+
SNTUtils.log("Secondary layer: dimensions do not match those of the data being traced");
3441+
return null;
3442+
}
3443+
return data;
3444+
}
3445+
33713446
public void loadSecondaryImage(final ImagePlus imp) throws IllegalArgumentException {
33723447
loadSecondaryImage(imp, true);
33733448
}
@@ -3396,7 +3471,10 @@ protected void loadSecondaryImage(final ImagePlus imp, final boolean changeUISta
33963471
return;
33973472
}
33983473
if (changeUIState) changeUIState(SNTUI.CACHING_DATA);
3399-
imp.setPosition( channel, xy.getSlice(), frame ); // important! sets the channel/frame to be imported. Does nothing if image is not a hyperstack
3474+
// important! sets the channel/frame to be imported. Does nothing if image is not a hyperstack.
3475+
// The 2ng arg Z position is irrelevant to ImgUtils#getCtSlice(ImagePlus) below, so any in-range
3476+
// value works. In stream mode xy is null so we fallback to 1 rather than dereferencing xy.getSlice()
3477+
imp.setPosition( channel, (xy != null) ? xy.getSlice() : 1, frame );
34003478
secondaryData = ImgUtils.getCtSlice(imp);
34013479
SNTUtils.log("Secondary data dimensions: " +
34023480
Arrays.toString(Intervals.dimensionsAsLongArray(secondaryData)));
@@ -3466,6 +3544,25 @@ public void enableSecondaryLayerTracing(final boolean enable) {
34663544
doSearchOnSecondaryData = false;
34673545
} else {
34683546
doSearchOnSecondaryData = true;
3547+
// In Stream mode there is no classic canvas/MIP-overlay to give visual feedback that a
3548+
// secondary layer was loaded (see SNTUI#secondaryDataPanel()'s load actions), so push it
3549+
// into the tethered viewer as an additional source instead. displayData(true)/showSecondaryData()
3550+
// on Bdv/Bvv will replace any previously-shown secondary layer.
3551+
// NB: enableSecondaryLayerTracing() is reached from several off-EDT callers but showSecondaryData()
3552+
// does BVV/BDV Swing/OpenGL setup that. Calling it directly from a worker thread is a silent EDT/GL
3553+
// threading violation (no exception, just a hang), hence the explicit invokeLater() below
3554+
if (getUI() != null && getUI().isStreamMode()) {
3555+
final var viewer = getUI().getActiveBigViewer();
3556+
if (viewer != null) {
3557+
SwingUtilities.invokeLater(() -> {
3558+
try {
3559+
viewer.showSecondaryData();
3560+
} catch (final Exception e) {
3561+
SNTUtils.log("Could not display secondary layer in Stream viewer: " + e.getMessage());
3562+
}
3563+
});
3564+
}
3565+
}
34693566
}
34703567
} else {
34713568
doSearchOnSecondaryData = false;
@@ -3489,11 +3586,27 @@ public void flushSecondaryData() {
34893586
setSecondaryImage(null);
34903587
if (getUI() != null) {
34913588
getUI().disableSecondaryLayerComponents();
3589+
// In Stream mode the secondary layer is also displayed as an extra source in the tethered
3590+
// Bdv/Bvv viewer (see enableSecondaryLayerTracing()'s showSecondaryData() hook); flushing it
3591+
// here should remove that source toot. Same invokeLater() rationale as that hook:
3592+
// hideSecondaryData() touches Swing/OpenGL state and may be reached from off-EDT callers.
3593+
if (getUI().isStreamMode()) {
3594+
final var viewer = getUI().getActiveBigViewer();
3595+
if (viewer != null) {
3596+
SwingUtilities.invokeLater(() -> {
3597+
try {
3598+
viewer.hideSecondaryData();
3599+
} catch (final Exception e) {
3600+
SNTUtils.log("Could not remove secondary layer from Stream viewer: " + e.getMessage());
3601+
}
3602+
});
3603+
}
3604+
}
34923605
}
34933606
}
34943607

34953608
private ImagePlus openCachedDataImage(final File file) throws IOException {
3496-
if (xy == null) throw new IllegalArgumentException(
3609+
if (!accessToValidImageData()) throw new IllegalArgumentException(
34973610
"Data can only be loaded after main tracing image is known");
34983611
if (!SNTUtils.fileAvailable(file)) {
34993612
throw new IllegalArgumentException("File path of input data unknown");
@@ -3505,15 +3618,55 @@ private ImagePlus openCachedDataImage(final File file) throws IOException {
35053618
throw new IllegalArgumentException("Image could not be loaded by IJ.");
35063619
imp = convertService.convert(ds, ImagePlus.class);
35073620
}
3508-
if (!ImpUtils.sameXYZDimensions(imp, xy)) {
3621+
if (!sameXYZDimensionsAsTracingData(imp)) {
35093622
// We are imposing only XYZ dimensions to e.g., allow for loading of single-channel
35103623
// p-maps. Hopefully, being lax about CT dimensions won't cause issues downstream
3511-
throw new IllegalArgumentException("Dimensions do not match those of " + xy.getTitle()
3624+
final String refName = (xy != null) ? xy.getTitle() : "the image being traced";
3625+
throw new IllegalArgumentException("Dimensions do not match those of " + refName
35123626
+ ". If this is unexpected, check under 'Image>Properties...' that CZT axes are not swapped.");
35133627
}
35143628
return imp;
35153629
}
35163630

3631+
/**
3632+
* Checks if {@code imp} shares the same XYZ voxel dimensions as the image currently being traced. Compares against
3633+
* the classic tracing {@link ImagePlus} ({@link #xy}) when one exists; in Stream mode compares against the streamed
3634+
* primary volume's own dimensions ({@link #getLoadedData()}) instead. Used to validate a candidate secondary layer
3635+
* before loading it.
3636+
*
3637+
* @param imp the candidate image
3638+
* @return true if dimensions match; false if they don't, or if no tracing data is loaded to compare against
3639+
*/
3640+
public boolean sameXYZDimensionsAsTracingData(final ImagePlus imp) {
3641+
if (xy != null) {
3642+
return ImpUtils.sameXYZDimensions(imp, xy);
3643+
}
3644+
final RandomAccessibleInterval<?> loadedData = getLoadedData();
3645+
if (loadedData == null) return false;
3646+
final long z = (loadedData.numDimensions() > 2) ? loadedData.dimension(2) : 1;
3647+
return loadedData.dimension(0) == imp.getWidth() && loadedData.dimension(1) == imp.getHeight()
3648+
&& z == imp.getNSlices();
3649+
}
3650+
3651+
/**
3652+
* RAI-based overload of {@link #sameXYZDimensionsAsTracingData(ImagePlus)}, for candidate
3653+
* sources that never go through an {@link ImagePlus} at all (e.g. a big-data secondary layer
3654+
* resolved via {@link SpimDataUtils#resolvePathToSource(String)}). Compares XYZ dimensions
3655+
* directly against {@link #getLoadedData()}, so unlike the {@link ImagePlus} overload this
3656+
* works the same regardless of whether {@link #xy} exists.
3657+
*
3658+
* @param data the candidate image data
3659+
* @return true if dimensions match; false if they don't, or if no tracing data is loaded to compare against
3660+
*/
3661+
public boolean sameXYZDimensionsAsTracingData(final RandomAccessibleInterval<?> data) {
3662+
final RandomAccessibleInterval<?> loadedData = getLoadedData();
3663+
if (loadedData == null || data == null) return false;
3664+
final long dataZ = (data.numDimensions() > 2) ? data.dimension(2) : 1;
3665+
final long refZ = (loadedData.numDimensions() > 2) ? loadedData.dimension(2) : 1;
3666+
return loadedData.dimension(0) == data.dimension(0) && loadedData.dimension(1) == data.dimension(1)
3667+
&& refZ == dataZ;
3668+
}
3669+
35173670
/**
35183671
* Retrieves the 'secondary image' data currently loaded in memory as an
35193672
* ImagePlus object. Returned image is always of 32-bit type.

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

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ public class SNTUI extends JDialog {
128128

129129
// UI controls for loading on 'secondary layer'
130130
private JCheckBox secLayerActivateCheckbox;
131+
private JButton secLayerActionButton;
131132
private CheckboxSpinner secLayerImgOverlayCSpinner;
132133

133134
// UI controls promoted from Options tab for quick-toggle access
@@ -164,6 +165,14 @@ public class SNTUI extends JDialog {
164165
protected Bvv bvvSNT;
165166
protected Bdv bdvSNT;
166167

168+
/**
169+
* Returns the {@link AbstractBigViewer} (BVV or BDV) currently tethered to this UI, in
170+
* particular, Stream mode's own viewer or {@code null} if neither is open.
171+
*/
172+
public AbstractBigViewer getActiveBigViewer() {
173+
return (bvvSNT != null) ? bvvSNT : bdvSNT;
174+
}
175+
167176
private final GuiListener listener;
168177

169178
/* These are the states that the UI can be in: */
@@ -703,7 +712,10 @@ public void runCommand(final Class<? extends Command> cmd, final HashMap<String,
703712
try {
704713
(new DynamicCmdRunner(cmd, inputs)).run();
705714
} catch (final RuntimeException ignored) {
706-
new CmdRunner(cmd, inputs, getState()).run();
715+
// .execute(), not .run(): CmdRunner extends ActiveWorker (a SwingWorker). Calling .run()
716+
// directly, from what is typically the EDT here (this is reached from menu actions/scripts),
717+
// self-deadlocks: See the identical fix and explanation at loadImageData()'s activeWorker.execute()
718+
new CmdRunner(cmd, inputs, getState()).execute();
707719
}
708720
}
709721

@@ -782,7 +794,10 @@ public void runSecondaryLayerWizard() {
782794
private void runSecondaryLayerWizard(final boolean autoCTwarning) {
783795
if (!okToReplaceSecLayer())
784796
return;
785-
if (!accessToValidImagePlus()) { // wizard requires image to be open
797+
// NB: accessToValidImageData(), not accessToValidImagePlus(): the latter requires a classic
798+
// ImagePlus and would always reject Stream mode. ComputeSecondaryImg only needs
799+
// plugin.getLoadedData() (works from either an ImagePlus or a streamed ImgPlus)
800+
if (!plugin.accessToValidImageData()) {
786801
noValidImageDataError();
787802
return;
788803
}
@@ -2826,7 +2841,11 @@ private JPanel bdvPanel(final ViewerPanelBuilder viewerPanelBuilder) {
28262841
openBDV.putClientProperty("cmdFinder-icon",
28272842
new FlatSVGIcon("gui/bdv-logo-dark.svg", IconFactory.defaultSize(), IconFactory.defaultSize()));
28282843
openBDV.addActionListener(e -> {
2829-
if (!plugin.accessToValidImageData() || plugin.getImagePlus() == null) {
2844+
// NB: accessToValidImageData() alone (matching bvvPanel()'s "Open BVV" handler above),
2845+
// not also requiring plugin.getImagePlus() != null: the latter would always reject Stream
2846+
// mode, even though "Full image" is the only one of the three choices in
2847+
// initializeBigViewerFromPrompt() that actually needs an ImagePlus
2848+
if (!plugin.accessToValidImageData()) {
28302849
noValidImageDataError();
28312850
} else {
28322851
if (bdvSNT != null && bdvSNT.getViewerFrame() != null) {
@@ -2987,7 +3006,7 @@ private JPanel secondaryDataPanel() {
29873006
registerInCommandFinder(secLayerActivateCheckbox, "Toggle Secondary Layer", "Main Tab");
29883007
// Options for externalImagePanel
29893008
final JPopupMenu secLayerMenu = new JPopupMenu();
2990-
final JButton secLayerActionButton = GuiUtils.Buttons.OptionsButton(IconFactory.GLYPH.LAYERS, 1f, secLayerMenu);
3009+
secLayerActionButton = GuiUtils.Buttons.OptionsButton(IconFactory.GLYPH.LAYERS, 1f, secLayerMenu);
29913010
GuiUtils.addTooltip(secLayerActionButton, "Actions for handling secondary layer imagery");
29923011
final JMenuItem mi1 = new JMenuItem("Secondary Layer Creation Wizard...",
29933012
IconFactory.menuIcon(IconFactory.GLYPH.WIZARD));
@@ -3220,7 +3239,15 @@ protected void done() {
32203239
showStatus(null, false);
32213240
}
32223241
};
3223-
activeWorker.run();
3242+
// .execute(), not .run(): this is called from the EDT (a menu click). SwingWorker.run() is meant
3243+
// to be invoked by the worker's own background-thread machinery (which .execute() sets up), not
3244+
// called directly by application code. Doing so from the EDT makes SwingWorker's completion path
3245+
// (doneEDT()) detect it's already on the EDT and invoke done() synchronously, nested inside the
3246+
// same call stack that's still completing the task -- done()'s call to get() then blocks forever
3247+
// waiting for a completion state that can only be reached by returning from that very call stack.
3248+
// .execute() properly runs doInBackground() on a background thread and schedules done() via
3249+
// invokeLater() afterward, avoiding the self-deadlock entirely.
3250+
activeWorker.execute();
32243251
}
32253252

32263253
protected void updateSecLayerWidgets() {
@@ -4930,7 +4957,7 @@ private void arrangeStreamViewer(final DialogLayout layout) {
49304957
imp.getWindow().toFront();
49314958
viewerY += placeholderHeight + InternalUtils.MARGIN;
49324959
}
4933-
final AbstractBigViewer viewer = (bvvSNT != null) ? bvvSNT : bdvSNT;
4960+
final AbstractBigViewer viewer = getActiveBigViewer();
49344961
if (viewer == null) return;
49354962
final JFrame viewerFrame = viewer.getViewerFrame();
49364963
if (viewerFrame == null) return;
@@ -5512,6 +5539,22 @@ protected void setSecondaryLayerTracingSelected(final boolean enable) {
55125539
secLayerActivateCheckbox.setSelected(enable);
55135540
updateSettingsString();
55145541
showStatus("Tracing on secondary layer enabled", true);
5542+
updateSecLayerActionButtonIcon(enable);
5543+
if (isStreamMode()) {
5544+
final AbstractBigViewer viewer = getActiveBigViewer();
5545+
if (viewer != null) viewer.updateSecondaryLayerIndicator();
5546+
}
5547+
}
5548+
5549+
/**
5550+
* Recolors {@link #secLayerActionButton}'s icon to indicate whether tracing/filling is currently
5551+
* being computed on the secondary layer. Classic mode already gets this feedback from
5552+
* {@link #secLayerActivateCheckbox} becoming checked, but that's easy to overlook in Stream mode
5553+
*/
5554+
private void updateSecLayerActionButtonIcon(final boolean active) {
5555+
if (secLayerActionButton == null) return;
5556+
secLayerActionButton.setIcon(IconFactory.dropdownMenuIcon(IconFactory.GLYPH.LAYERS, 1f,
5557+
(active) ? GuiUtils.getSelectionColor() : IconFactory.defaultColor()));
55155558
}
55165559

55175560
boolean noSecondaryDataAvailableError() {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ public void run() {
8282
return;
8383
}
8484
if (secondaryLayer) {
85-
if (!ImpUtils.sameXYZDimensions(snt.getImagePlus(), chosenImp)) {
85+
if (!snt.sameXYZDimensionsAsTracingData(chosenImp)) { // stream mode safe
8686
error("Dimensions of chosen image differ from those of image being traced.");
8787
} else {
8888
snt.flushSecondaryData();

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -703,7 +703,8 @@ private RandomAccessibleInterval<T> getInputData() {
703703
}
704704

705705
private String getImageName() {
706-
final String basename = SNTUtils.stripExtension(sntService.getInstance().getImagePlus().getTitle());
706+
final ImagePlus tracingImp = sntService.getInstance().getImagePlus();
707+
final String basename = SNTUtils.stripExtension((tracingImp != null) ? tracingImp.getTitle() : "SNT_Data"); // stream mode safe
707708
final String sfx;
708709
if (NONE.equals(filter)) {
709710
sfx = "DUP";

0 commit comments

Comments
 (0)