Skip to content

Commit 28a02a2

Browse files
committed
Bvv: Add wrapper for ims/xml files
1 parent e2b50aa commit 28a02a2

3 files changed

Lines changed: 302 additions & 24 deletions

File tree

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,8 @@ private Icon getIcon(final String menuName) {
208208
return switch (menuName) {
209209
case "Analysis" -> IconFactory.menuIcon(GLYPH.CHART);
210210
case "Batch" -> IconFactory.menuIcon(GLYPH.COG);
211-
case "Demos" -> IconFactory.menuIcon(GLYPH.GRADUATION_CAP);
211+
case "Big Data" -> IconFactory.menuIcon(GLYPH.DATABASE);
212+
case "Demos" -> IconFactory.menuIcon(GLYPH.GRADUATION_CAP);
212213
case "Full List" -> IconFactory.menuIcon(GLYPH.LIST);
213214
case "Misc" -> IconFactory.menuIcon(GLYPH.ELLIPSIS);
214215
case "Render" -> IconFactory.menuIcon(GLYPH.CUBE);
@@ -268,8 +269,8 @@ public JMenu getBatchScriptsMenu() {
268269

269270
/** Returns a UI list with all the bundled non-demo SNT scripts **/
270271
public JMenu getScriptsMenu() {
271-
final JMenu menus = getScriptsMenu(DEMO_SCRIPT, "Analysis", "Batch", "Misc", "Render", "Skeletons_and_ROIs", "Tracing", "Time-lapses");
272-
menus.insert(getDemosMenu(), 2);
272+
final JMenu menus = getScriptsMenu(DEMO_SCRIPT, "Analysis", "Batch", "Big_Data", "Misc", "Render", "Skeletons_and_ROIs", "Tracing", "Time-lapses");
273+
menus.insert(getDemosMenu(), 3);
273274
return menus;
274275
}
275276

‎src/main/java/sc/fiji/snt/viewer/Bvv.java‎

Lines changed: 193 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@
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>.
@@ -22,10 +22,15 @@
2222

2323
package sc.fiji.snt.viewer;
2424

25+
import bdv.img.imaris.Imaris;
26+
import bdv.spimdata.SpimDataMinimal;
27+
import bdv.spimdata.XmlIoSpimDataMinimal;
2528
import bdv.tools.HelpDialog;
2629
import bdv.tools.InitializeViewerState;
2730
import bdv.tools.brightness.ConverterSetup;
2831
import bdv.util.AxisOrder;
32+
import mpicbg.spim.data.SpimDataException;
33+
import mpicbg.spim.data.generic.AbstractSpimData;
2934
import bdv.viewer.Source;
3035
import bdv.viewer.animate.SimilarityTransformAnimator;
3136
import 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

Comments
 (0)