Skip to content

Commit ec805b1

Browse files
committed
Improve Bvv tracing
- Add progress bar for interactive searches - Fix RAI not being loaded from AbstractSpimData or SpimDataUtils.N5Sources sources - Implement a 'Big data' mode in SNTUI - Misc fixes
1 parent e270bea commit ec805b1

13 files changed

Lines changed: 577 additions & 150 deletions

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -705,9 +705,29 @@ private void recordComment(final String comment) {
705705
sntui.getRecorder(false).recordComment(comment);
706706
}
707707

708+
Action loadBookmarksAction(final AbstractBigViewer viewer) {
709+
return new AbstractAction("From Bookmarks", IconFactory.menuIcon(IconFactory.GLYPH.BOOKMARK)) {
710+
@Override
711+
public void actionPerformed(final java.awt.event.ActionEvent e) {
712+
try {
713+
final java.util.List<SNTPoint> pos = viewer.getSNT().getUI().getBookmarkManager().getPositions(false);
714+
if (pos.isEmpty()) {
715+
new GuiUtils(viewer.getMarkerManager().getViewerDialogPanel()).error("The Bookmarks table is empty.");
716+
} else {
717+
viewer.getMarkerManager().add("BM", pos, 1, 1, SNTColor.colorToString(viewer.getRenderingOptions().fallbackColor));
718+
viewer.showViewerMessage(String.format("Imported %d bookmarks", pos.size()));
719+
}
720+
} catch (final NullPointerException ex) {
721+
viewer.showViewerMessage("Bookmark Manager unavailable");
722+
}
723+
}
724+
};
725+
}
726+
708727
private JPopupMenu importMenu() {
709728
final JPopupMenu menu = new JPopupMenu();
710729
GuiUtils.addSeparator(menu, "Import:");
730+
if (viewer != null) menu.add(new JMenuItem(loadBookmarksAction(viewer)));
711731
JMenuItem jmi = new JMenuItem("From CSV File...", IconFactory.menuIcon(IconFactory.GLYPH.TABLE));
712732
menu.add(jmi);
713733
jmi.addActionListener(e -> {

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3606,6 +3606,7 @@ public boolean canExecute(List<Path> selectedPaths) {
36063606
private class TimeProfilerCommand implements PathCommand {
36073607
@Override
36083608
public void execute(List<Path> selectedPaths, String cmd) {
3609+
if (noValidImageDataError()) return;
36093610
final HashMap<String, Object> inputs = new HashMap<>();
36103611
inputs.put("tree", new Tree(selectedPaths));
36113612
inputs.put("imp", plugin.getImagePlus());
@@ -5371,8 +5372,11 @@ private void rebuildRelationShips() {
53715372

53725373
private boolean noValidImageDataError() {
53735374
final boolean invalidImage = !plugin.getUI().accessToValidImagePlus();
5374-
if (invalidImage)
5375-
guiUtils.error("This option requires valid image data to be loaded.");
5375+
if (invalidImage) {
5376+
guiUtils.error((getSNT().getUI() != null && getSNT().getUI().getState() == SNTUI.BVV_TRACING)
5377+
? "This option requires the entire image to be loaded into memory (RAM)."
5378+
: "This option requires valid image data to be loaded.");
5379+
}
53765380
return invalidImage;
53775381
}
53785382

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

Lines changed: 110 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@
102102
import java.util.*;
103103
import java.util.concurrent.*;
104104
import java.util.function.BiFunction;
105+
import java.util.function.Consumer;
105106

106107

107108
/**
@@ -962,21 +963,25 @@ private void loadDatasetFromImagePlus(final ImagePlus imp) {
962963
}
963964

964965
public void startUI() {
966+
startUI(false);
967+
}
968+
969+
public void startUI(final boolean bigDataMode) {
965970
if (SwingUtilities.isEventDispatchThread()) {
966-
startUIOnEDT();
971+
startUIOnEDT(bigDataMode);
967972
} else {
968973
try {
969-
SwingUtilities.invokeAndWait(this::startUIOnEDT);
974+
SwingUtilities.invokeAndWait(() -> startUIOnEDT(bigDataMode));
970975
} catch (final Throwable e) {
971976
throw new RuntimeException("Failed to start UI", e);
972977
}
973978
}
974979
}
975980

976-
private void startUIOnEDT() {
981+
private void startUIOnEDT(final boolean bigDataMode) {
977982
GuiUtils.setLookAndFeel();
978983
final SNT thisPlugin = this;
979-
ui = new SNTUI(thisPlugin);
984+
ui = new SNTUI(thisPlugin, bigDataMode);
980985
guiUtils = new GuiUtils(ui);
981986
ui.displayOnStarting();
982987
}
@@ -2380,6 +2385,32 @@ public Path manualTrace(final SNTPoint start, final SNTPoint end, final PointInI
23802385
return manualTraceHeadless(List.of(start, end), forkPoint);
23812386
}
23822387

2388+
/**
2389+
* Headless auto-trace variant that additionally reports progress and exposes the underlying
2390+
* {@link Future} for cancellation. Always runs headless (regardless of {@link
2391+
* GraphicsEnvironment#isHeadless()}); intended for callers (e.g., BVV's tracing toggle) that
2392+
* need live feedback and/or the ability to cancel a slow search, e.g., over a lazily-loaded,
2393+
* network-backed image where a single segment can take a long time to converge (or never
2394+
* converge, if the goal is unreachable).
2395+
*
2396+
* @param start the start point
2397+
* @param end the end point
2398+
* @param forkPoint the fork point of the parent {@link Path} from which the path should
2399+
* branch off, or null if the path should not have any parent
2400+
* @param progressCallback receives periodic {@code pointsInSearch}/{@code finished}/{@code
2401+
* threadStatus} updates (roughly once a second; see {@code reportEveryMilliseconds}),
2402+
* or null to skip progress reporting
2403+
* @param onSubmit called with the search's {@link Future} immediately after it is submitted to
2404+
* the tracer thread pool, so the caller can cancel it later (e.g. {@code
2405+
* future.cancel(true)}); the search loop already checks for interruption, so this
2406+
* actually stops the search rather than merely abandoning it. May be null.
2407+
* @return the path, or null if the search failed or was cancelled
2408+
*/
2409+
public Path autoTrace(final SNTPoint start, final SNTPoint end, final PointInImage forkPoint,
2410+
final SearchProgressCallback progressCallback, final Consumer<Future<?>> onSubmit) {
2411+
return autoTraceHeadless(List.of(start, end), forkPoint, progressCallback, onSubmit);
2412+
}
2413+
23832414
/**
23842415
* Automatically traces a path from a list of points and adds it to the active
23852416
* {@link PathAndFillManager} instance. Note that this method still requires
@@ -2480,9 +2511,14 @@ public Path autoTrace(final List<SNTPoint> pointList, final PointInImage forkPoi
24802511
}
24812512

24822513
private Path autoTraceHeadless(final List<SNTPoint> pointList, final PointInImage forkPoint) {
2514+
return autoTraceHeadless(pointList, forkPoint, null, null);
2515+
}
2516+
2517+
private Path autoTraceHeadless(final List<SNTPoint> pointList, final PointInImage forkPoint,
2518+
final SearchProgressCallback progressCallback, final Consumer<Future<?>> onSubmit) {
24832519
return runHeadlessTrace(pointList, forkPoint, (start, end) -> createSearch(
24842520
start.getX(), start.getY(), start.getZ(),
2485-
end.getX(), end.getY(), end.getZ()));
2521+
end.getX(), end.getY(), end.getZ()), progressCallback, onSubmit);
24862522
}
24872523

24882524
/**
@@ -2494,7 +2530,7 @@ private Path autoTraceHeadless(final List<SNTPoint> pointList, final PointInImag
24942530
private Path manualTraceHeadless(final List<SNTPoint> pointList, final PointInImage forkPoint) {
24952531
return runHeadlessTrace(pointList, forkPoint, (start, end) -> new ManualTracerThread(this,
24962532
start.getX() / x_spacing, start.getY() / y_spacing, start.getZ() / z_spacing,
2497-
end.getX() / x_spacing, end.getY() / y_spacing, end.getZ() / z_spacing));
2533+
end.getX() / x_spacing, end.getY() / y_spacing, end.getZ() / z_spacing), null, null);
24982534
}
24992535

25002536
/**
@@ -2514,10 +2550,15 @@ private Path manualTraceHeadless(final List<SNTPoint> pointList, final PointInIm
25142550
* @param pointList the nodes to connect, start to end
25152551
* @param forkPoint optional fork point of the parent Path, or null
25162552
* @param searchFactory builds the per-segment search for a given start/end pair
2553+
* @param progressCallback registered on each per-segment search via {@link
2554+
* SearchInterface#addProgressListener}, or null to skip progress reporting
2555+
* @param onSubmit called with each per-segment search's {@link Future} immediately after
2556+
* submission (so a caller can cancel it later), or null
25172557
* @return the stitched path, or null if any segment failed to produce a result
25182558
*/
25192559
private <S extends Runnable & SearchInterface> Path runHeadlessTrace(final List<SNTPoint> pointList,
2520-
final PointInImage forkPoint, final BiFunction<SNTPoint, SNTPoint, S> searchFactory)
2560+
final PointInImage forkPoint, final BiFunction<SNTPoint, SNTPoint, S> searchFactory,
2561+
final SearchProgressCallback progressCallback, final Consumer<Future<?>> onSubmit)
25212562
{
25222563
if (pointList == null || pointList.isEmpty())
25232564
throw new IllegalArgumentException("pointList cannot be null or empty");
@@ -2536,10 +2577,13 @@ private <S extends Runnable & SearchInterface> Path runHeadlessTrace(final List<
25362577
Path pathResult = null;
25372578
try {
25382579
final S search = searchFactory.apply(start, end);
2580+
if (progressCallback != null) search.addProgressListener(progressCallback);
25392581
final Future<?> result = tracerThreadPool.submit(search);
2582+
if (onSubmit != null) onSubmit.accept(result);
25402583
result.get();
25412584
pathResult = search.getResult();
2542-
} catch (InterruptedException | ExecutionException | IllegalArgumentException e) {
2585+
} catch (InterruptedException | ExecutionException | IllegalArgumentException
2586+
| CancellationException e) {
25432587
SNTUtils.error("Error during trace", e);
25442588
}
25452589
if (pathResult == null) {
@@ -3492,6 +3536,9 @@ public boolean isOnlySelectedPathsVisible() {
34923536

34933537
protected void updateTracingViewers(final boolean includeLegacy3Dviewer) {
34943538
repaintAllPanes();
3539+
if (getUI() != null && getUI().bvvSNT != null) {
3540+
new Thread(() -> getUI().bvvSNT.syncPathManagerList()).start();
3541+
}
34953542
if (includeLegacy3Dviewer) update3DViewerContents();
34963543
}
34973544

@@ -3503,6 +3550,9 @@ protected void updateNonTracingViewers() {
35033550
if (getUI().sciViewSNT != null) {
35043551
new Thread(() -> getUI().sciViewSNT.syncPathManagerList()).start();
35053552
}
3553+
if (getUI().bdvSNT != null) {
3554+
new Thread(() -> getUI().bdvSNT.syncPathManagerList()).start();
3555+
}
35063556
}
35073557

35083558
public void updateAllViewers() {
@@ -4204,6 +4254,58 @@ public int getDepth() {
42044254
return depth;
42054255
}
42064256

4257+
/**
4258+
* Sets image dimensions and calibration without providing actual pixel data. Useful when the
4259+
* source image is not directly accessible as random-access data (e.g., a BigDataViewer
4260+
* {@code AbstractSpimData}/IMS source, or an ambiguous N5/Zarr layout) but its metadata (size,
4261+
* voxel spacing) is still known. This allows features that only need bounds/scale (e.g., manual
4262+
* tracing, bounds/distance checks) to work correctly with the image's real dimensions, even
4263+
* though {@link #accessToValidImageData()} keeps reporting no data is available (this does NOT
4264+
* set {@code ctSlice3d}, so A* search remains unavailable).
4265+
*
4266+
* @param width width in pixels (ignored if &le;0)
4267+
* @param height height in pixels (ignored if &le;0)
4268+
* @param depth depth (number of slices) in pixels (ignored if &le;0)
4269+
* @param xSpacing pixel width (ignored if &le;0)
4270+
* @param ySpacing pixel height (ignored if &le;0)
4271+
* @param zSpacing pixel depth (ignored if &le;0)
4272+
* @param units spatial calibration units (ignored if null/blank)
4273+
*/
4274+
public void setImageMetadata(final int width, final int height, final int depth, final double xSpacing,
4275+
final double ySpacing, final double zSpacing, final String units) {
4276+
if (width > 0) this.width = width;
4277+
if (height > 0) this.height = height;
4278+
if (depth > 0) {
4279+
this.depth = depth;
4280+
this.singleSlice = depth == 1;
4281+
}
4282+
if (xSpacing > 0) this.x_spacing = xSpacing;
4283+
if (ySpacing > 0) this.y_spacing = ySpacing;
4284+
if (zSpacing > 0) this.z_spacing = zSpacing;
4285+
if (units != null && !units.isBlank()) this.spacing_units = SNTUtils.getSanitizedUnit(units);
4286+
}
4287+
4288+
/**
4289+
* Directly sets the image data backing A* search, without requiring an {@link ImgPlus} wrapper.
4290+
* Useful when pixel data comes from a source whose {@link RandomAccessibleInterval} is already
4291+
* resolved elsewhere (e.g., a BigDataViewer/SpimData {@code ImgLoader}, or a BDV/BVV
4292+
* {@code Source}), and building/discarding a full {@code ImgPlus} just to satisfy
4293+
* {@link #SNT(ImgPlus)} would be wasteful, or would not preserve lazy/chunked access the way the
4294+
* original object does.
4295+
* <p>
4296+
* Callers remain responsible for also calling {@link #setImageMetadata} (or equivalent) with matching
4297+
* dimensions/calibration; this method does not attempt to infer them from {@code data}.
4298+
* <p>
4299+
* {@code data}'s pixel type must be a {@link RealType}; other types (e.g. {@code ARGBType}) will
4300+
* throw a {@code ClassCastException} later, e.g., the first time A* search accesses a pixel.
4301+
*
4302+
* @param data the (possibly lazily-backed) image data, in the same pixel/voxel grid implied by
4303+
* the dimensions/calibration set via {@link #setImageMetadata}
4304+
*/
4305+
public void setImageData(final RandomAccessibleInterval<?> data) {
4306+
this.ctSlice3d = data;
4307+
}
4308+
42074309
public double getPixelWidth() {
42084310
return x_spacing;
42094311
}

0 commit comments

Comments
 (0)