3333import ij .process .ShortProcessor ;
3434import ij3d .*;
3535import io .scif .services .DatasetIOService ;
36+ import mpicbg .spim .data .generic .AbstractSpimData ;
3637import net .imagej .Dataset ;
3738import net .imagej .DatasetService ;
3839import net .imagej .ImgPlus ;
8081import sc .fiji .snt .filter .Tubeness ;
8182import sc .fiji .snt .gui .GuiUtils ;
8283import sc .fiji .snt .hyperpanes .MultiDThreePanes ;
84+ import sc .fiji .snt .io .SpimDataUtils ;
8385import sc .fiji .snt .plugin .ShollAnalysisTreeCmd ;
8486import sc .fiji .snt .seed .SeedOverlay ;
8587import 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.
0 commit comments