88 * it under the terms of the GNU General Public License as
99 * published by the Free Software Foundation, either version 3 of the
1010 * License, or (at your option) any later version.
11- *
11+ *
1212 * This program is distributed in the hope that it will be useful,
1313 * but WITHOUT ANY WARRANTY; without even the implied warranty of
1414 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1515 * GNU General Public License for more details.
16- *
16+ *
1717 * You should have received a copy of the GNU General Public
1818 * License along with this program. If not, see
1919 * <http://www.gnu.org/licenses/gpl-3.0.html>.
2222
2323package sc .fiji .snt .viewer ;
2424
25+ import bdv .img .imaris .Imaris ;
26+ import bdv .spimdata .SpimDataMinimal ;
27+ import bdv .spimdata .XmlIoSpimDataMinimal ;
2528import bdv .tools .HelpDialog ;
2629import bdv .tools .InitializeViewerState ;
2730import bdv .tools .brightness .ConverterSetup ;
2831import bdv .util .AxisOrder ;
32+ import mpicbg .spim .data .SpimDataException ;
33+ import mpicbg .spim .data .generic .AbstractSpimData ;
2934import bdv .viewer .Source ;
3035import bdv .viewer .animate .SimilarityTransformAnimator ;
3136import bdv .viewer .state .SourceGroup ;
@@ -144,6 +149,7 @@ public <T extends RealType<T>> BvvSource show(final ImgPlus<T> imgPlus) {
144149 for (int d = 0 ; d < cal .length ; d ++) {
145150 cal [d ] = imgPlus .averageScale (d );
146151 }
152+ checkVolumeSize (dims [0 ], dims [1 ], dims [2 ]);
147153 // Bypass show(RAI, cal) to preserve the image name and assign the source to a named group
148154 final BvvOptions opt = (bvvHandle != null ? bvv .vistools .Bvv .options ().addTo (bvvHandle ) : options )
149155 .sourceTransform (cal );
@@ -175,11 +181,12 @@ private <T extends RealType<T>> BvvMultiSource showImgPlusMultiChannel(final Img
175181 final long nZ = zDimIdx >= 0 ? imgPlus .dimension (zDimIdx ) : 1 ;
176182 dims = new long []{imgPlus .dimension (imgPlus .dimensionIndex (Axes .X )),
177183 imgPlus .dimension (imgPlus .dimensionIndex (Axes .Y )), nZ };
184+ checkVolumeSize (dims [0 ], dims [1 ], nZ );
178185
179186 final String imageName = (imgPlus .getName () != null && !imgPlus .getName ().isBlank ())
180- ? imgPlus .getName () : "Volume " ;
187+ ? imgPlus .getName () : "SNT Bvv " ;
181188
182- // Shared option builder: produces the log and handles prefs/blockSize
189+ // Shared option builder — produces the log and handles prefs/blockSize
183190 final BvvOptions baseOpt = configureBvvOptionsForImage (nZ , nC )
184191 .axisOrder (AxisOrder .XYZ )
185192 .sourceTransform (cal );
@@ -189,7 +196,7 @@ private <T extends RealType<T>> BvvMultiSource showImgPlusMultiChannel(final Img
189196 final List <BvvStackSource <?>> followerSources = new ArrayList <>();
190197
191198 for (int c = 0 ; c < nC ; c ++) {
192- // Extract this channel as a pure 3D XYZ RAI, to avoid dimension-order ambiguity
199+ // Extract this channel as a pure 3D XYZ RAI — no dimension-order ambiguity
193200 final RandomAccessibleInterval <T > channelRai = Views .hyperSlice (imgPlus , chDim , c );
194201 final String chTitle = imageName + " (Ch" + (c + 1 ) + ")" ;
195202 final BvvOptions chOpt = (!hasExistingWindow && leaderSource == null )
@@ -268,8 +275,8 @@ private <T extends RealType<T>> List<BvvMultiSource> showImgPlus(final List<ImgP
268275 // BvvFunctions.show() always returns BvvStackSource; cast is safe
269276 final BvvStackSource <?> src = (BvvStackSource <?>) show (img );
270277 if (multiSources .size () > sizeBefore ) {
271- // show(img) routed through showImagePlusMultiChannel: group assignment and
272- // multiSources entry were already handled, we just need to collect the result
278+ // show(img) routed through showImagePlusMultiChannel: group assignment
279+ // and multiSources entry were already handled — just collect the result
273280 results .add (multiSources .getLast ());
274281 } else {
275282 // Single-channel path: manage group and multiSources here
@@ -364,28 +371,36 @@ private void applyChannelColors(final BvvMultiSource group, final Color... color
364371 }
365372
366373 /**
367- * Sets per-channel colors, applying the supplied color pattern to
368- * <em>every</em> source group. Colors are applied by channel index within
369- * each group; extra colors beyond a group's channel count are ignored for
370- * that group, and channels without a corresponding color are left unchanged.
374+ * Sets per-channel colors across all source groups, assigning colors
375+ * sequentially by global channel order across all groups. For example,
376+ * with two 3-channel images and colors [R, G, B, C, M, Y], image 1 gets
377+ * R/G/B and image 2 gets C/M/Y. With a single multi-channel image or a
378+ * dataset where each setup is its own group (e.g. IMS), colors are assigned
379+ * one-to-one in load order.
371380 * <p>
372- * This is the most convenient entry point for scripts: when all loaded images
373- * share the same channel semantics .
381+ * To apply the same color pattern to every group independently, call
382+ * {@link #setChannelColors(BvvMultiSource, Color...)} per group .
374383 * </p>
375384 * <p>
376385 * Typical Groovy/PySNT usage:
377386 * <pre>
378- * def bvv = Bvv.open(reference, moving) // two 3-channel images
379- * bvv.setChannelColors(Color.RED, Color.GREEN, Color.BLUE) // applied to both
387+ * def bvv = Bvv.open(img)
388+ * bvv.setChannelColors(Color.RED, Color.GREEN, Color.BLUE)
380389 * </pre>
381390 * </p>
382391 *
383- * @param colors one color per channel position (applied to every group)
392+ * @param colors colors assigned sequentially across all channels in all groups
384393 */
385394 public void setChannelColors (final Color ... colors ) {
386395 if (colors == null || colors .length == 0 || bvvHandle == null ) return ;
387- for (final BvvMultiSource group : multiSources )
388- applyChannelColors (group , colors );
396+ int colorIdx = 0 ;
397+ for (final BvvMultiSource group : multiSources ) {
398+ final List <ConverterSetup > setups = getConverterSetups (group );
399+ for (int i = 0 ; i < setups .size () && colorIdx < colors .length ; i ++, colorIdx ++) {
400+ if (colors [colorIdx ] != null )
401+ setups .get (i ).setColor (new ARGBType (colors [colorIdx ].getRGB ()));
402+ }
403+ }
389404 repaint ();
390405 }
391406
@@ -554,6 +569,142 @@ public static Bvv open(final SNT snt) {
554569 return bvv ;
555570 }
556571
572+ /**
573+ * Convenience factory: creates a standalone BVV viewer and displays a
574+ * {@link AbstractSpimData} dataset using BVV's pyramid-aware GPU cache manager.
575+ * Unlike the {@link ImgPlus}/{@link ImagePlus} paths, this does <em>not</em>
576+ * attempt to upload the entire volume at once — data is streamed as tiles on
577+ * demand, making it suitable for out-of-core datasets.
578+ * <p>
579+ * The caller is responsible for constructing the {@link AbstractSpimData}
580+ * object, typically via an XML/HDF5 loader:
581+ * <pre>
582+ * import bdv.spimdata.XmlIoSpimDataMinimal
583+ * spimData = new XmlIoSpimDataMinimal().load("/path/to/dataset.xml")
584+ * def bvv = Bvv.open(spimData)
585+ * </pre>
586+ * or via Bio-Formats/IMS loaders that produce a {@link AbstractSpimData} with
587+ * a multiresolution pyramid.
588+ * </p>
589+ *
590+ * @param spimData the dataset to display
591+ * @return the fully initialised {@link Bvv} instance
592+ */
593+ public static Bvv open (final AbstractSpimData <?> spimData ) {
594+ final Bvv bvv = new Bvv ();
595+ bvv .show (spimData );
596+ return bvv ;
597+ }
598+
599+ /**
600+ * Convenience factory: opens a file path into a BVV viewer, choosing the
601+ * most appropriate loading strategy based on the file extension:
602+ * <ul>
603+ * <li><b>.ims</b> — Imaris HDF5: creates a BDV XML sidecar file next to
604+ * the {@code .ims} file (using the same base name, e.g.
605+ * {@code dataset.xml}), then loads it via {@link XmlIoSpimDataMinimal}.
606+ * BVV's pyramid-aware GPU cache manager is used, so the full volume is
607+ * never loaded into RAM. If the directory is not writable, an
608+ * {@link IllegalArgumentException} is thrown with instructions to
609+ * create the XML manually using
610+ * {@code Plugins > BigDataViewer > Create XML for Imaris file}.</li>
611+ * <li><b>.xml</b> — BDV XML/HDF5: loaded directly via
612+ * {@link XmlIoSpimDataMinimal} and displayed using BVV's cache
613+ * manager.</li>
614+ * <li><b>anything else</b> — delegated to {@link ImgUtils#open(String)}
615+ * and displayed via the standard {@link ImgPlus} path. Very large
616+ * flat volumes will fail with a descriptive error rather than a GL
617+ * crash.</li>
618+ * </ul>
619+ *
620+ * @param filePathOrUrl the file path or URL to open
621+ * @return the fully initialised {@link Bvv} instance
622+ * @throws IllegalArgumentException if an {@code .ims} file's directory is
623+ * not writable, or if the file cannot be opened
624+ */
625+ public static Bvv open (final String filePathOrUrl ) {
626+ final File file = new File (filePathOrUrl );
627+ final String lower = file .getName ().toLowerCase ();
628+
629+ if (lower .endsWith (".ims" )) {
630+ // Derive the canonical sidecar path (same base name, .xml extension)
631+ final String basePath = file .getAbsolutePath ();
632+ final String xmlPath = basePath .substring (0 , basePath .length () - 4 ) + ".xml" ;
633+ try {
634+ if (new File (xmlPath ).exists ()) {
635+ // Sidecar already present — load it directly without re-creating
636+ SNTUtils .log ("BVV: reusing existing XML sidecar: " + xmlPath );
637+ final AbstractSpimData <?> spimData = new XmlIoSpimDataMinimal ().load (xmlPath );
638+ return open (spimData );
639+ }
640+ // No sidecar yet — check write permission before attempting to create one
641+ final File dir = file .getParentFile ();
642+ if (dir != null && !dir .canWrite ()) {
643+ throw new IllegalArgumentException (
644+ "Cannot write to directory: " + dir .getAbsolutePath () + "\n " +
645+ "Create the BDV XML file manually via " +
646+ "Plugins > BigDataViewer > Create XML for Imaris file, " +
647+ "then use Bvv.open(\" /path/to/dataset.xml\" )." );
648+ }
649+ final SpimDataMinimal spimData = Imaris .openIms (file .getAbsolutePath ());
650+ new XmlIoSpimDataMinimal ().save (spimData , xmlPath );
651+ SNTUtils .log ("BVV: created XML sidecar: " + xmlPath );
652+ return open (spimData );
653+ } catch (final IOException | SpimDataException e ) {
654+ throw new IllegalArgumentException ("Could not open IMS file: " + e .getMessage (), e );
655+ }
656+ }
657+
658+ if (lower .endsWith (".xml" )) {
659+ try {
660+ final AbstractSpimData <?> spimData = new XmlIoSpimDataMinimal ().load (filePathOrUrl );
661+ return open (spimData );
662+ } catch (final SpimDataException e ) {
663+ throw new IllegalArgumentException ("Could not open XML file: " + e .getMessage (), e );
664+ }
665+ }
666+
667+ // Fallback: open as ImgPlus (includes size check before reaching BVV)
668+ final ImgPlus <?> img = ImgUtils .open (filePathOrUrl );
669+ if (img == null )
670+ throw new IllegalArgumentException ("Could not open file: " + filePathOrUrl );
671+ final Bvv bvv = new Bvv ();
672+ //noinspection unchecked,rawtypes
673+ bvv .show ((ImgPlus ) img );
674+ return bvv ;
675+ }
676+
677+ /**
678+ * Displays a {@link AbstractSpimData} dataset using BVV's pyramid-aware GPU
679+ * cache manager. Each setup (channel/angle) is added as a source and wrapped
680+ * in a {@link BvvMultiSource}. Unlike the {@link ImgPlus}/{@link ImagePlus}
681+ * paths, the full volume is never loaded into RAM or GPU at once.
682+ *
683+ * @param spimData the dataset to display
684+ * @return list of {@link BvvMultiSource}, one per BDV setup, in setup order
685+ */
686+ @ SuppressWarnings ("unchecked" )
687+ public List <BvvMultiSource > show (final AbstractSpimData <?> spimData ) {
688+ final List <BvvStackSource <?>> sources = BvvFunctions .show (spimData , options );
689+ final List <BvvMultiSource > results = new ArrayList <>(sources .size ());
690+ for (int i = 0 ; i < sources .size (); i ++) {
691+ final BvvStackSource <?> src = sources .get (i );
692+ if (bvvHandle == null ) {
693+ bvvHandle = src .getBvvHandle ();
694+ attachControlPanel (src );
695+ }
696+ final int groupIdx = multiSources .size ();
697+ final int srcIdx = bvvHandle .getViewerPanel ().state ().getSources ().size () - (sources .size () - i );
698+ final String name = src .getSources ().isEmpty () ? "Setup " + (i + 1 )
699+ : src .getSources ().getFirst ().getSpimSource ().getName ();
700+ assignToNamedGroup (name , groupIdx , srcIdx , 1 , bvvHandle .getViewerPanel ());
701+ final BvvMultiSource multi = new BvvMultiSource (src );
702+ multiSources .add (multi );
703+ results .add (multi );
704+ }
705+ return results ;
706+ }
707+
557708 private static AxisOrder getAxisOrder (final ImagePlus imp ) {
558709 final boolean hasZ = imp .getNSlices () > 1 ;
559710 final boolean hasC = imp .getNChannels () > 1 ;
@@ -896,6 +1047,27 @@ private double[] computeCamParams(final double sx, final double sy, final double
8961047 return new double []{dCam , dClip , dClip };
8971048 }
8981049
1050+ /**
1051+ * Checks whether a volume's per-channel voxel count is within BVV's texture
1052+ * capacity. BVV's {@code DefaultSimpleStackManager} computes the texture buffer
1053+ * size as {@code width * height * depth * 2} using a 32-bit signed int; values
1054+ * beyond ~1 billion voxels cause integer overflow and a fatal GL crash.
1055+ *
1056+ * @throws IllegalArgumentException if the volume exceeds ~1 Gvox/channel
1057+ */
1058+ private static void checkVolumeSize (final long width , final long height , final long depth ) {
1059+ // BVV allocates width*height*depth*2 bytes as a signed int; max safe value is
1060+ // Integer.MAX_VALUE = 2^31-1, so the voxel limit is (2^31-1)/2 ≈ 1.07 Gvox.
1061+ final long voxels = width * height * depth ;
1062+ if (voxels * 2L > Integer .MAX_VALUE ) {
1063+ throw new IllegalArgumentException (String .format (
1064+ "Volume too large for BVV's texture manager: %dx%dx%d = %.2f Gvox/channel " +
1065+ "(limit ~1.07 Gvox). For tiled datasets, open the native " +
1066+ "BDV/HDF5 or IMS source directly to use BVV's pyramid-aware cache." ,
1067+ width , height , depth , voxels / 1e9 ));
1068+ }
1069+ }
1070+
8991071 /** Initializes the path overlay system for drawing traced paths. */
9001072 private void initializePathOverlay (final BigVolumeViewer bvv ) {
9011073 if (pathOverlay != null ) {
@@ -2437,7 +2609,7 @@ Action loadSettingsAction() {
24372609 return new AbstractAction ("Load Settings..." , IconFactory .menuIcon (IconFactory .GLYPH .IMPORT )) {
24382610 @ Override
24392611 public void actionPerformed (final java .awt .event .ActionEvent e ) {
2440- final File f = guiUtils .getFile (snt . getPrefs (). getRecentDir (), "xml" );
2612+ final File f = guiUtils .getFile (getDefaultDir (), "xml" );
24412613 if (SNTUtils .fileAvailable (f )) {
24422614 try {
24432615 bvv .loadSettings (f .getAbsolutePath ());
@@ -2454,7 +2626,7 @@ Action saveSettingsAction() {
24542626 return new AbstractAction ("Save Settings..." , IconFactory .menuIcon (IconFactory .GLYPH .EXPORT )) {
24552627 @ Override
24562628 public void actionPerformed (final java .awt .event .ActionEvent e ) {
2457- final File f = guiUtils .getSaveFile ("Save BVV Settings..." , snt . getPrefs (). getRecentDir (), "xml" );
2629+ final File f = guiUtils .getSaveFile ("Save BVV Settings..." , getDefaultDir (), "xml" );
24582630 if (SNTUtils .fileAvailable (f )) {
24592631 try {
24602632 bvv .saveSettings (f .getAbsolutePath ());
@@ -2497,7 +2669,7 @@ Action importAction() {
24972669 return new AbstractAction ("Import Reconstructions..." , IconFactory .menuIcon (IconFactory .GLYPH .IMPORT )) {
24982670 @ Override
24992671 public void actionPerformed (final java .awt .event .ActionEvent e ) {
2500- final File [] files = guiUtils .getReconstructionFiles (snt . getPrefs (). getRecentDir ());
2672+ final File [] files = guiUtils .getReconstructionFiles (getDefaultDir ());
25012673 if (files == null || files .length == 0 ) return ;
25022674 int failureCounter = 0 ;
25032675 final ColorRGB [] colors = SNTColor .getDistinctColors (files .length );
0 commit comments