Skip to content

Commit e270bea

Browse files
committed
Implement basic support for Bvv tracing
1 parent c87e78e commit e270bea

13 files changed

Lines changed: 654 additions & 106 deletions

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ public void showPanel() {
178178
protected JPanel getPanel() {
179179
if (panel != null) return panel;
180180
panel = (sntui != null)
181-
? SNTUI.InternalUtils.getTab() : new JPanel(new GridBagLayout());
181+
? SNTUI.InternalUtils.initTab() : new JPanel(new GridBagLayout());
182182
final GridBagConstraints gbc = GuiUtils.defaultGbc();
183183
gbc.fill = GridBagConstraints.BOTH;
184184
if (sntui != null) {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,7 @@ private void navigateToWarning(final PlausibilityCheck.Warning warning) {
342342
public JPanel getPanel() {
343343
if (panel != null) return panel;
344344

345-
panel = SNTUI.InternalUtils.getTab();
345+
panel = SNTUI.InternalUtils.initTab();
346346
final GridBagConstraints gbc = GuiUtils.defaultGbc();
347347

348348
// Header & description

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ private Color[] defaultDelineationColors(final int n) {
106106
}
107107

108108
protected JPanel getPanel() {
109-
final JPanel container = SNTUI.InternalUtils.getTab();
109+
final JPanel container = SNTUI.InternalUtils.initTab();
110110
final GridBagConstraints gbc = GuiUtils.defaultGbc();
111111
gbc.fill = GridBagConstraints.BOTH;
112112
SNTUI.InternalUtils.addSeparatorWithURL(container, "Delineations:",

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ public SNTEditorPane getEditor() {
6464
* Returns the assembled <i>Notepad</i> panel.
6565
*/
6666
protected JPanel getPanel() {
67-
final JPanel container = SNTUI.InternalUtils.getTab();
67+
final JPanel container = SNTUI.InternalUtils.initTab();
6868
final GridBagConstraints gbc = GuiUtils.defaultGbc();
6969
gbc.fill = GridBagConstraints.BOTH;
7070
SNTUI.InternalUtils.addSeparatorWithURL(container, "Notepad:", true, gbc);

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,10 @@ public static class PathNode extends PointInImage implements Cloneable {
155155
private Color nodeColor;
156156
private double[] tangent; // [x, y, z]
157157

158+
public PathNode(final double[] xyz) {
159+
super(xyz[0], xyz[1], xyz[2]);
160+
}
161+
158162
public PathNode(final double x, final double y, final double z) {
159163
super(x, y, z);
160164
}

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

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3232,8 +3232,7 @@ synchronized PointInImage nearestJoinPointOnSelectedPaths(final double x,
32323232

32333233
if (0 == p.size()) continue;
32343234

3235-
final int i = p.indexNearestTo(x * x_spacing, y * y_spacing, z *
3236-
z_spacing);
3235+
final int i = p.indexNearestTo(x * x_spacing, y * y_spacing, z * z_spacing);
32373236

32383237
final PointInImage nearestOnPath = p.getNodeWithoutChecks(i);
32393238

@@ -3358,8 +3357,8 @@ public void universeClosed() {
33583357
public NearPoint nearestPointOnAnyPath(final double x, final double y,
33593358
final double z, final double distanceLimit)
33603359
{
3361-
return nearestPointOnAnyPath(allPaths, new PointInImage(x, y, z), Math.sqrt(
3362-
distanceLimit), false);
3360+
return nearestPointOnAnyPath(allPaths, new PointInImage(x, y, z), distanceLimit *
3361+
distanceLimit, false);
33633362
}
33643363

33653364
protected List<Path> getUnSelectedPathsRenderedInViewPort(

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

Lines changed: 82 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@
101101
import java.util.List;
102102
import java.util.*;
103103
import java.util.concurrent.*;
104+
import java.util.function.BiFunction;
104105

105106

106107
/**
@@ -415,16 +416,23 @@ public <T extends RealType<T>> SNT(final ImgPlus<T> sourceImage, final int chann
415416
// Log what was extracted for debugging
416417
if (sliceResult.channelIndex() >= 0) {
417418
SNTUtils.log("Using channel " + sliceResult.channelIndex());
419+
this.channel = sliceResult.channelIndex() + 1; // 1-based index
420+
} else {
421+
this.channel = 1;
418422
}
419423
if (sliceResult.timeIndex() >= 0) {
420424
SNTUtils.log("Using timepoint " + sliceResult.timeIndex());
425+
this.frame = sliceResult.timeIndex() + 1; // 1-based index
426+
} else {
427+
this.frame = 1;
421428
}
422429
SNTUtils.getContext().inject(this);
423430
SNTUtils.setPlugin(this);
424431
pathAndFillManager = new PathAndFillManager(this);
425432
prefs = new SNTPrefs(this);
426433
setFieldsFromImgPlus(processedImage);
427434
prefs.loadPluginPrefs();
435+
single_pane = true; // avoid NPE on side panes logic
428436
}
429437

430438
/**
@@ -2032,6 +2040,7 @@ private <T extends RealType<T>> AbstractSearch createSearch(final int x_start,
20322040
final int y_end,
20332041
final int z_end)
20342042
{
2043+
20352044
final boolean useSecondary = isTracingOnSecondaryImageActive();
20362045

20372046
final RandomAccessibleInterval<T> img = useSecondary ? getSecondaryData() : getLoadedData();
@@ -2348,6 +2357,29 @@ public Path autoTrace(final SNTPoint start, final SNTPoint end, final PointInIma
23482357
return autoTrace(List.of(start, end), forkPoint, headless);
23492358
}
23502359

2360+
/**
2361+
* Manually traces a straight path from a point A to a point B, bypassing A* (or
2362+
* any other configured {@link SearchInterface}) entirely: the two points are
2363+
* simply connected via {@link ManualTracerThread}. Unlike {@link #autoTrace(SNTPoint,
2364+
* SNTPoint, PointInImage, boolean)}, this method does not depend on, or alter,
2365+
* {@link #isAstarEnabled()}. Runs headless (no SNTUI state changes) and, since it does
2366+
* not search the image, is cheap enough to be safely called off the EDT.
2367+
* <p>
2368+
* The returned path is <i>not</i> added to the Path Manager: callers are responsible
2369+
* for that (see {@link PathAndFillManager#addPath(Path, boolean, boolean)}).
2370+
*
2371+
* @param start the {@link SNTPoint} start of the path
2372+
* @param end the {@link SNTPoint} end of the path
2373+
* @param forkPoint the {@link PointInImage} fork point of the parent {@link Path}
2374+
* from which the path should branch off, or null if the path should not
2375+
* have any parent.
2376+
* @return the path a reference to the computed path.
2377+
*/
2378+
@SuppressWarnings("unused") // used for snt scripts
2379+
public Path manualTrace(final SNTPoint start, final SNTPoint end, final PointInImage forkPoint) {
2380+
return manualTraceHeadless(List.of(start, end), forkPoint);
2381+
}
2382+
23512383
/**
23522384
* Automatically traces a path from a list of points and adds it to the active
23532385
* {@link PathAndFillManager} instance. Note that this method still requires
@@ -2448,37 +2480,70 @@ public Path autoTrace(final List<SNTPoint> pointList, final PointInImage forkPoi
24482480
}
24492481

24502482
private Path autoTraceHeadless(final List<SNTPoint> pointList, final PointInImage forkPoint) {
2483+
return runHeadlessTrace(pointList, forkPoint, (start, end) -> createSearch(
2484+
start.getX(), start.getY(), start.getZ(),
2485+
end.getX(), end.getY(), end.getZ()));
2486+
}
2487+
2488+
/**
2489+
* Manual-tracing counterpart of {@link #autoTraceHeadless(List, PointInImage)}: connects
2490+
* each consecutive pair of points with a straight segment via {@link ManualTracerThread}
2491+
* instead of searching for a path with {@link #createSearch(double, double, double, double,
2492+
* double, double)}. See {@link #manualTrace(SNTPoint, SNTPoint, PointInImage)}.
2493+
*/
2494+
private Path manualTraceHeadless(final List<SNTPoint> pointList, final PointInImage forkPoint) {
2495+
return runHeadlessTrace(pointList, forkPoint, (start, end) -> new ManualTracerThread(this,
2496+
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));
2498+
}
2499+
2500+
/**
2501+
* Shared skeleton for headless, point-to-point tracing. For each consecutive pair of
2502+
* points in {@code pointList}, builds a {@link SearchInterface} via {@code searchFactory},
2503+
* submits it to {@link #tracerThreadPool}, blocks until it completes, and stitches the
2504+
* per-segment {@link Path} results into a single path. Used by both {@link
2505+
* #autoTraceHeadless(List, PointInImage)} (A* search, or whichever {@link SearchInterface}
2506+
* is currently configured) and {@link #manualTraceHeadless(List, PointInImage)} (straight
2507+
* segments, ignoring {@link #isAstarEnabled()} entirely).
2508+
* <p>
2509+
* This blocks the calling thread on {@link Future#get()} for each segment: callers running
2510+
* on the EDT should only do so if {@code searchFactory} is guaranteed to be cheap (as
2511+
* {@link ManualTracerThread} is). A real search (A*, Tubular Geodesics, etc.) can be slow,
2512+
* especially against lazily-loaded data, and should be dispatched from a background thread.
2513+
*
2514+
* @param pointList the nodes to connect, start to end
2515+
* @param forkPoint optional fork point of the parent Path, or null
2516+
* @param searchFactory builds the per-segment search for a given start/end pair
2517+
* @return the stitched path, or null if any segment failed to produce a result
2518+
*/
2519+
private <S extends Runnable & SearchInterface> Path runHeadlessTrace(final List<SNTPoint> pointList,
2520+
final PointInImage forkPoint, final BiFunction<SNTPoint, SNTPoint, S> searchFactory)
2521+
{
24512522
if (pointList == null || pointList.isEmpty())
24522523
throw new IllegalArgumentException("pointList cannot be null or empty");
24532524

24542525
if (tracerThreadPool == null || tracerThreadPool.isShutdown()) {
24552526
tracerThreadPool = Executors.newSingleThreadExecutor();
24562527
}
24572528

2458-
Path fullPath = new Path(x_spacing, y_spacing, z_spacing, spacing_units);
2529+
final Path fullPath = new Path(x_spacing, y_spacing, z_spacing, spacing_units);
24592530

24602531
// Now keep appending nodes to temporary path
24612532
for (int i = 0; i < pointList.size() - 1; i++) {
24622533
// Append node and wait for search to be finished
24632534
final SNTPoint start = pointList.get(i);
24642535
final SNTPoint end = pointList.get(i + 1);
2465-
AbstractSearch pathSearch = createSearch(
2466-
start.getX(),
2467-
start.getY(),
2468-
start.getZ(),
2469-
end.getX(),
2470-
end.getY(),
2471-
end.getZ());
2472-
Future<?> result = tracerThreadPool.submit(pathSearch);
24732536
Path pathResult = null;
24742537
try {
2538+
final S search = searchFactory.apply(start, end);
2539+
final Future<?> result = tracerThreadPool.submit(search);
24752540
result.get();
2476-
pathResult = pathSearch.getResult();
2477-
} catch (InterruptedException | ExecutionException e) {
2478-
SNTUtils.error("Error during auto-trace", e);
2541+
pathResult = search.getResult();
2542+
} catch (InterruptedException | ExecutionException | IllegalArgumentException e) {
2543+
SNTUtils.error("Error during trace", e);
24792544
}
24802545
if (pathResult == null) {
2481-
SNTUtils.log("Auto-trace result was null.");
2546+
SNTUtils.log("Trace result was null.");
24822547
return null;
24832548
}
24842549
fullPath.add(pathResult);
@@ -3937,6 +4002,10 @@ public void enableAutoSelectionOfFinishedPath(final boolean enable) {
39374002
activateFinishedPath = enable;
39384003
}
39394004

4005+
public boolean isAutoSelectionOfFinishedPathEnabled() {
4006+
return activateFinishedPath;
4007+
}
4008+
39404009
public boolean isTracingOnSecondaryImageActive() {
39414010
return doSearchOnSecondaryData && isSecondaryDataAvailable();
39424011
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ public class SNTPrefs { // TODO: Adopt PrefService
6363
/** Boolean identifiers */
6464
private static final int DRAW_DIAMETERS = 1;
6565
private static final int SNAP_CURSOR = 2;
66-
private static final int REQUIRE_SHIFT_FOR_FORK = 4;
66+
public static final int REQUIRE_SHIFT_FOR_FORK = 4;
6767
private static final int AUTO_CANVAS_ACTIVATION = 8;
6868
private static final int AUTO_SELECTION_FINISHED_PATH = 16;
6969
private static final int USE_THREE_PANE = 32;
@@ -312,7 +312,7 @@ protected void loadStartupPrefs() {
312312
snt.use3DViewer = getPref(USE_3D_VIEWER);
313313
}
314314

315-
private boolean getPref(final int key) {
315+
public boolean getPref(final int key) {
316316
return (currentBooleans & key) != 0;
317317
}
318318

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

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -288,11 +288,11 @@ public void windowClosing(final WindowEvent e) {
288288
GuiUtils.removeIcon(this);
289289

290290
assert SwingUtilities.isEventDispatchThread();
291-
final JTabbedPane tabbedPane = getTabbedPane();
291+
final JTabbedPane tabbedPane = initTabbedPane();
292292
final GridBagConstraints c1 = GuiUtils.defaultGbc();
293293

294294
// Main tab
295-
final JPanel tab1 = InternalUtils.getTab();
295+
final JPanel tab1 = InternalUtils.initTab();
296296
c1.insets.top = InternalUtils.MARGIN * 2;
297297
c1.anchor = GridBagConstraints.NORTHEAST;
298298
InternalUtils.addSeparatorWithURL(tab1, "Data Source:", false, c1);
@@ -335,7 +335,7 @@ public void windowClosing(final WindowEvent e) {
335335

336336

337337
// Options Tab
338-
final JPanel tab2 = InternalUtils.getTab();
338+
final JPanel tab2 = InternalUtils.initTab();
339339
tab2.setLayout(new GridBagLayout());
340340
final GridBagConstraints c2 = GuiUtils.defaultGbc();
341341
c2.anchor = GridBagConstraints.NORTHEAST;
@@ -360,7 +360,7 @@ public void windowClosing(final WindowEvent e) {
360360

361361

362362
// 3D tab
363-
final JPanel tab3 = InternalUtils.getTab();
363+
final JPanel tab3 = InternalUtils.initTab();
364364
tab3.setLayout(new GridBagLayout());
365365
final GridBagConstraints c3 = GuiUtils.defaultGbc();
366366
c3.insets.bottom = InternalUtils.MARGIN;
@@ -388,8 +388,8 @@ public void windowClosing(final WindowEvent e) {
388388

389389
InternalUtils.addSeparatorWithURL(tab3, "Big Volume Viewer:", false, c3);
390390
++c3.gridy;
391-
final String msg4 = "Big Volume Viewer (BVV) is the 3D counterpart of Big Data Viewer " +
392-
"capable of GPU volume rendering. Discrete graphics card recommended.";
391+
final String msg4 = "Big Volume Viewer (BVV) is a functional tracing canvas and the 3D counterpart " +
392+
"of Big Data Viewer capable of GPU volume rendering. Discrete graphics card recommended.";
393393
tab3.add(GuiUtils.longSmallMsg(msg4, "bdv-logo-light.svg", tab3), c3);
394394
c3.gridy++;
395395
tab3.add(bvvPanel(viewerPanelBuilder), c3);
@@ -431,14 +431,14 @@ public void windowClosing(final WindowEvent e) {
431431
tabbedPane.addTab("Notes", notesui.getPanel());
432432

433433
// set icons: Main, Options, Assistant, Bookmarks, 3D, Delineations, Seeds, Notes
434-
tabbedPane.setIconAt(0, IconFactory.tabbedPaneIcon(tabbedPane, GLYPH.HOME));
435-
tabbedPane.setIconAt(1, IconFactory.tabbedPaneIcon(tabbedPane, GLYPH.TOOL));
436-
tabbedPane.setIconAt(2, IconFactory.tabbedPaneIcon(tabbedPane, GLYPH.USER_DOCTOR));
437-
tabbedPane.setIconAt(3, IconFactory.tabbedPaneIcon(tabbedPane, GLYPH.BOOKMARK));
438-
tabbedPane.setIconAt(4, IconFactory.tabbedPaneIcon(tabbedPane, GLYPH.CUBE));
439-
tabbedPane.setIconAt(5, IconFactory.tabbedPaneIcon(tabbedPane, GLYPH.LINES_LEANING));
440-
tabbedPane.setIconAt(6, IconFactory.tabbedPaneIcon(tabbedPane, GLYPH.SEEDLING));
441-
tabbedPane.setIconAt(7, IconFactory.tabbedPaneIcon(tabbedPane, GLYPH.CLIPBOARD));
434+
IconFactory.assignTabIcon(tabbedPane, 0, GLYPH.HOME);
435+
IconFactory.assignTabIcon(tabbedPane, 1, GLYPH.TOOL);
436+
IconFactory.assignTabIcon(tabbedPane, 2, GLYPH.USER_DOCTOR);
437+
IconFactory.assignTabIcon(tabbedPane, 3, GLYPH.BOOKMARK);
438+
IconFactory.assignTabIcon(tabbedPane, 4, GLYPH.CUBE);
439+
IconFactory.assignTabIcon(tabbedPane, 5, GLYPH.LINES_LEANING);
440+
IconFactory.assignTabIcon(tabbedPane, 6, GLYPH.SEEDLING);
441+
IconFactory.assignTabIcon(tabbedPane, 7, GLYPH.CLIPBOARD);
442442
registerTabCommandFinderAliases(tabbedPane); // must be called after icons have been set
443443

444444
setJMenuBar(createMenuBar(tabbedPane));
@@ -509,7 +509,7 @@ public void windowClosing(final WindowEvent e) {
509509

510510
}
511511

512-
private JTabbedPane getTabbedPane() {
512+
private JTabbedPane initTabbedPane() {
513513
final JTabbedPane tabbedPane = GuiUtils.getTabbedPane();
514514
tabbedPane.addChangeListener(e -> {
515515
final JTabbedPane source = (JTabbedPane) e.getSource();
@@ -1003,6 +1003,16 @@ private void updateRebuildCanvasButton() {
10031003
rebuildCanvasButton.setText(label);
10041004
}
10051005

1006+
public void setBvv(final Bvv bvv) {
1007+
this.bvvSNT = bvv;
1008+
// Disable components not yet tested
1009+
final JTabbedPane tp = getJTabbedPaneAddedToContentPane();
1010+
List.of("Bookmarks", "3D").forEach(tabTitle -> {
1011+
final int idx = InternalUtils.getTabIndex(tp, tabTitle);
1012+
if (idx != -1) tp.setEnabledAt(idx, bvvSNT == null);
1013+
});
1014+
}
1015+
10061016
// State interface for UI state management
10071017
private interface UIState {
10081018
void enter();
@@ -2540,6 +2550,10 @@ private JPanel bvvPanel(final ViewerPanelBuilder viewerPanelBuilder) {
25402550
} else if (plugin.is2D()) {
25412551
error("BVV requires a 3D image: ray-casting cannot render flat (2D) images with no depth.");
25422552
} else {
2553+
if (bvvSNT != null && bvvSNT.getViewerFrame() != null) {
2554+
bvvSNT.getViewerFrame().toFront();
2555+
return;
2556+
}
25432557
try {
25442558
initializeBvvFromPrompt();
25452559
} catch (final Throwable exc) {
@@ -5632,7 +5646,7 @@ static String hotKeyLabel(final String text, final String key) {
56325646
return (text.startsWith("<HTML>")) ? label : "<HTML>" + label;
56335647
}
56345648

5635-
static JPanel getTab() {
5649+
static JPanel initTab() {
56365650
final JPanel tab = new JPanel();
56375651
tab.setBorder(BorderFactory.createEmptyBorder(MARGIN, MARGIN / 2, MARGIN / 2, MARGIN));
56385652
tab.setLayout(new GridBagLayout());

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -297,8 +297,11 @@ public static Icon listIcon(final JList<?> list, final GLYPH entry) {
297297
return new FADerivedIcon(entry.id, list.getFont().getSize() * 0.9f, list.getForeground(), entry.solid);
298298
}
299299

300-
public static Icon tabbedPaneIcon(final JTabbedPane tabbedPane, final GLYPH entry) {
301-
return new FADerivedIcon(entry.id, tabbedPane.getFont().getSize(), tabbedPane.getForeground(), entry.solid);
300+
public static void assignTabIcon(final JTabbedPane tabbedPane, final int tabIndex, final GLYPH entry) {
301+
tabbedPane.setIconAt(tabIndex,
302+
new FADerivedIcon(entry.id, tabbedPane.getFont().getSize(), tabbedPane.getForeground(), entry.solid));
303+
tabbedPane.setDisabledIconAt(tabIndex,
304+
new FADerivedIcon(entry.id, tabbedPane.getFont().getSize(), GuiUtils.getDisabledComponentColor(), entry.solid));
302305
}
303306

304307
public static TitledBorder bottomBorder(final char symbol, final boolean solid) {

0 commit comments

Comments
 (0)