|
101 | 101 | import java.util.List; |
102 | 102 | import java.util.*; |
103 | 103 | import java.util.concurrent.*; |
| 104 | +import java.util.function.BiFunction; |
104 | 105 |
|
105 | 106 |
|
106 | 107 | /** |
@@ -415,16 +416,23 @@ public <T extends RealType<T>> SNT(final ImgPlus<T> sourceImage, final int chann |
415 | 416 | // Log what was extracted for debugging |
416 | 417 | if (sliceResult.channelIndex() >= 0) { |
417 | 418 | SNTUtils.log("Using channel " + sliceResult.channelIndex()); |
| 419 | + this.channel = sliceResult.channelIndex() + 1; // 1-based index |
| 420 | + } else { |
| 421 | + this.channel = 1; |
418 | 422 | } |
419 | 423 | if (sliceResult.timeIndex() >= 0) { |
420 | 424 | SNTUtils.log("Using timepoint " + sliceResult.timeIndex()); |
| 425 | + this.frame = sliceResult.timeIndex() + 1; // 1-based index |
| 426 | + } else { |
| 427 | + this.frame = 1; |
421 | 428 | } |
422 | 429 | SNTUtils.getContext().inject(this); |
423 | 430 | SNTUtils.setPlugin(this); |
424 | 431 | pathAndFillManager = new PathAndFillManager(this); |
425 | 432 | prefs = new SNTPrefs(this); |
426 | 433 | setFieldsFromImgPlus(processedImage); |
427 | 434 | prefs.loadPluginPrefs(); |
| 435 | + single_pane = true; // avoid NPE on side panes logic |
428 | 436 | } |
429 | 437 |
|
430 | 438 | /** |
@@ -2032,6 +2040,7 @@ private <T extends RealType<T>> AbstractSearch createSearch(final int x_start, |
2032 | 2040 | final int y_end, |
2033 | 2041 | final int z_end) |
2034 | 2042 | { |
| 2043 | + |
2035 | 2044 | final boolean useSecondary = isTracingOnSecondaryImageActive(); |
2036 | 2045 |
|
2037 | 2046 | final RandomAccessibleInterval<T> img = useSecondary ? getSecondaryData() : getLoadedData(); |
@@ -2348,6 +2357,29 @@ public Path autoTrace(final SNTPoint start, final SNTPoint end, final PointInIma |
2348 | 2357 | return autoTrace(List.of(start, end), forkPoint, headless); |
2349 | 2358 | } |
2350 | 2359 |
|
| 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 | + |
2351 | 2383 | /** |
2352 | 2384 | * Automatically traces a path from a list of points and adds it to the active |
2353 | 2385 | * {@link PathAndFillManager} instance. Note that this method still requires |
@@ -2448,37 +2480,70 @@ public Path autoTrace(final List<SNTPoint> pointList, final PointInImage forkPoi |
2448 | 2480 | } |
2449 | 2481 |
|
2450 | 2482 | 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 | + { |
2451 | 2522 | if (pointList == null || pointList.isEmpty()) |
2452 | 2523 | throw new IllegalArgumentException("pointList cannot be null or empty"); |
2453 | 2524 |
|
2454 | 2525 | if (tracerThreadPool == null || tracerThreadPool.isShutdown()) { |
2455 | 2526 | tracerThreadPool = Executors.newSingleThreadExecutor(); |
2456 | 2527 | } |
2457 | 2528 |
|
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); |
2459 | 2530 |
|
2460 | 2531 | // Now keep appending nodes to temporary path |
2461 | 2532 | for (int i = 0; i < pointList.size() - 1; i++) { |
2462 | 2533 | // Append node and wait for search to be finished |
2463 | 2534 | final SNTPoint start = pointList.get(i); |
2464 | 2535 | 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); |
2473 | 2536 | Path pathResult = null; |
2474 | 2537 | try { |
| 2538 | + final S search = searchFactory.apply(start, end); |
| 2539 | + final Future<?> result = tracerThreadPool.submit(search); |
2475 | 2540 | 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); |
2479 | 2544 | } |
2480 | 2545 | if (pathResult == null) { |
2481 | | - SNTUtils.log("Auto-trace result was null."); |
| 2546 | + SNTUtils.log("Trace result was null."); |
2482 | 2547 | return null; |
2483 | 2548 | } |
2484 | 2549 | fullPath.add(pathResult); |
@@ -3937,6 +4002,10 @@ public void enableAutoSelectionOfFinishedPath(final boolean enable) { |
3937 | 4002 | activateFinishedPath = enable; |
3938 | 4003 | } |
3939 | 4004 |
|
| 4005 | + public boolean isAutoSelectionOfFinishedPathEnabled() { |
| 4006 | + return activateFinishedPath; |
| 4007 | + } |
| 4008 | + |
3940 | 4009 | public boolean isTracingOnSecondaryImageActive() { |
3941 | 4010 | return doSearchOnSecondaryData && isSecondaryDataAvailable(); |
3942 | 4011 | } |
|
0 commit comments