fillAt(final double x, final double y, final long fill) {
+ private UtilityTask> fillAt(final double x, final double y, final long fill) {
// TODO should this check happen outside?
if (!isVisible.getAsBoolean()) {
@@ -159,7 +167,7 @@ private static P viewerToSourceCo
return location;
}
- private UtilityTask fill(
+ private UtilityTask> fill(
final int time,
final int level,
final long fill,
@@ -181,28 +189,60 @@ private UtilityTask fill(
level
);
final SourceMask mask = source.generateMask(maskInfo, MaskedSource.VALID_LABEL_CHECK);
- final AccessBoxRandomAccessible accessTracker =
- new AccessBoxRandomAccessible<>(Views.extendValue(mask.getRai(), new UnsignedLongType(1)));
+ final AffineTransform3D globalToSource = source.getSourceTransformForMask(maskInfo).inverse();
+
+ final List visibleSourceIntervals = Paintera.getPaintera().getBaseView().orthogonalViews().views().stream()
+ .filter(it -> it.isVisible() && it.getWidth() > 0.0 && it.getHeight() > 0.0)
+ .map(ViewerMask::getGlobalViewerInterval)
+ .map(globalToSource::estimateBounds)
+ .map(Intervals::smallestContainingInterval)
+ .collect(Collectors.toList());
+
+ final AtomicBoolean triggerRefresh = new AtomicBoolean(false);
+ final AccessBoxRandomAccessible accessTracker = new AccessBoxRandomAccessible<>(Views.extendValue(mask.getRai(), new UnsignedLongType(1))) {
+
+ final Point position = new Point(sourceAccess.numDimensions());
- final var floodFillTask = Tasks.createTask((Function, Boolean>)task -> {
+ @Override
+ public UnsignedLongType get() {
+ if (Thread.currentThread().isInterrupted())
+ throw new RuntimeException("Flood Fill Interrupted");
+ synchronized (this) {
+ updateAccessBox();
+ }
+ sourceAccess.localize(position);
+ if (!triggerRefresh.get()) {
+ for (RealInterval interval : visibleSourceIntervals) {
+ if (Intervals.contains(interval, position)) {
+ triggerRefresh.set(true);
+ break;
+ }
+ }
+ }
+ return sourceAccess.get();
+ }
+ };
+
+ final UtilityTask> floodFillTask = Tasks.createTask(task -> {
if (seedValue instanceof LabelMultisetType) {
- fillMultisetType((RandomAccessibleInterval)data, accessTracker, seed, seedLabel, fill, assignment);
+ fillMultisetType((RandomAccessibleInterval) data, accessTracker, seed, seedLabel, fill, assignment);
} else {
fillPrimitiveType(data, accessTracker, seed, seedLabel, fill, assignment);
}
- return true;
- }).onCancelled((state, task) -> {
+ }
+ ).onCancelled((state, task) -> {
try {
source.resetMasks();
} catch (final MaskInUse e) {
e.printStackTrace();
}
- })
- .onFailed((event, task) -> {
+ }
+ ).onFailed((event, task) -> {
if (!Thread.currentThread().isInterrupted() && task.getException() != null && !(task.getException() instanceof CancellationException)) {
throw new RuntimeException(task.getException());
}
- }).onSuccess((state, task) -> {
+ }
+ ).onSuccess((state, task) -> {
LOG.debug(Thread.currentThread().isInterrupted() ? "FloodFill has been interrupted" : "FloodFill has been completed");
final Interval interval = accessTracker.createAccessInterval();
@@ -212,32 +252,38 @@ private UtilityTask fill(
Arrays.toString(Intervals.maxAsLongArray(interval))
);
source.applyMask(mask, interval, MaskedSource.VALID_LABEL_CHECK);
- }).onEnd(task -> requestRepaint.run())
- .submit();
-
- final var floodFillResultCheckerThread = new Thread(() -> {
- while (!floodFillTask.isDone()) {
- try {
- Thread.sleep(100);
- } catch (final InterruptedException e) {
- Thread.currentThread().interrupt(); // restore interrupted status
}
+ );
- if (Thread.currentThread().isInterrupted())
- break;
+ final var refreshAnimation = new AnimationTimer() {
- LOG.trace("Updating View for FloodFill ");
- requestRepaint.run();
- }
+ final static long delay = 2_000_000_000; // 2 second delay before refreshes start
+ final static long REFRESH_RATE = 1_000_000_000;
+ final long start = System.nanoTime();
+ long before = start;
- if (Thread.interrupted()) {
- floodFillTask.cancel();
+ @Override
+ public void handle(long now) {
+ if (now - start < delay || now - before < REFRESH_RATE) return;
+ if (!floodFillTask.isCancelled() && triggerRefresh.get()) {
+ requestRepaint.accept(accessTracker.createAccessInterval());
+ before = now;
+ triggerRefresh.set(false);
+ }
}
- });
+ };
- setFloodFillState(source, new FloodFillState(fill, floodFillResultCheckerThread::interrupt));
- floodFillResultCheckerThread.start();
+ floodFillTask.onEnd(task -> {
+ refreshAnimation.stop();
+ requestRepaint.accept(accessTracker.createAccessInterval());
+ });
+
+ if (floodFillExector.isShutdown()) {
+ floodFillExector = newFloodFillExecutor();
+ }
+ refreshAnimation.start();
+ floodFillTask.submit(floodFillExector);
return floodFillTask;
}
@@ -280,20 +326,28 @@ private static > void fillPrimitiveType(
);
}
- private void setFloodFillState(final Source> source, final FloodFillState state) {
-
- setFloodFillState.accept(state);
- }
+ private static > BiPredicate makePredicate(final long seedLabel, final FragmentSegmentAssignment assignment) {
- private void resetFloodFillState(final Source> source) {
-
- setFloodFillState(source, null);
- }
+ final Long singleFragment;
+ final TLongHashSet seedFragments;
+ if (assignment != null) {
+ seedFragments = assignment.getFragments(seedLabel);
+ singleFragment = seedFragments.size() == 1 ? seedFragments.toArray()[0] : null;
+ } else {
+ singleFragment = seedLabel;
+ seedFragments = null;
+ }
- private static > BiPredicate makePredicate(final long id, final FragmentSegmentAssignment assignment) {
+ return (sourceVal, targetVal) -> {
+ if (Thread.currentThread().isInterrupted()) return false;
+ /* true if sourceFragment is a seedFragment */
+ final long sourceFragment = sourceVal.getIntegerLong();
+ final var shouldFill = singleFragment != null ? singleFragment == sourceFragment : seedFragments.contains(sourceFragment);
+ /* Most target vals are typically invalid, so this is rarely not passed; The sourceMatch filter is likely to
+ * shortcircuit more often */
+ return shouldFill && targetVal.getInteger() == Label.INVALID;
+ };
- return (t, u) -> !Thread.currentThread().isInterrupted() && u.getInteger() == Label.INVALID
- && (assignment != null ? assignment.getSegment(t.getIntegerLong()) : t.getIntegerLong()) == id;
}
public static class RunAll implements Runnable {
@@ -359,7 +413,7 @@ public static Interval trackedFill(
coordinates.add(seed.getLongPosition(d));
}
- final int cleanupThreshold = n * (int)1e5;
+ final int cleanupThreshold = n * (int) 1e5;
final RandomAccessible>> neighborhood = shape.neighborhoodsRandomAccessible(paired);
final RandomAccess>> neighborhoodAccess = neighborhood.randomAccess();
diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill2D.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill2D.java
index 0caba1c2c..7c2278c1d 100644
--- a/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill2D.java
+++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill2D.java
@@ -1,12 +1,12 @@
package org.janelia.saalfeldlab.paintera.control.paint;
import bdv.fx.viewer.ViewerPanelFX;
+import javafx.animation.AnimationTimer;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.value.ObservableValue;
-import javafx.concurrent.Task;
import net.imglib2.FinalInterval;
import net.imglib2.Interval;
import net.imglib2.Point;
@@ -21,7 +21,6 @@
import net.imglib2.type.label.Label;
import net.imglib2.type.logic.BoolType;
import net.imglib2.type.numeric.IntegerType;
-import net.imglib2.type.numeric.RealType;
import net.imglib2.type.numeric.integer.UnsignedLongType;
import net.imglib2.util.AccessBoxRandomAccessibleOnGet;
import net.imglib2.util.Intervals;
@@ -30,13 +29,13 @@
import org.janelia.saalfeldlab.fx.Tasks;
import org.janelia.saalfeldlab.fx.UtilityTask;
import org.janelia.saalfeldlab.fx.ui.Exceptions;
-import org.janelia.saalfeldlab.fx.util.InvokeOnJavaFXApplicationThread;
import org.janelia.saalfeldlab.paintera.Paintera;
import org.janelia.saalfeldlab.paintera.control.assignment.FragmentSegmentAssignment;
import org.janelia.saalfeldlab.paintera.data.mask.MaskInfo;
import org.janelia.saalfeldlab.paintera.data.mask.MaskedSource;
import org.janelia.saalfeldlab.paintera.data.mask.SourceMask;
import org.janelia.saalfeldlab.paintera.data.mask.exception.MaskInUse;
+import org.janelia.saalfeldlab.util.NamedThreadFactory;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
@@ -45,6 +44,9 @@
import java.lang.invoke.MethodHandles;
import java.util.Arrays;
import java.util.Objects;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BooleanSupplier;
import java.util.function.Predicate;
@@ -52,6 +54,14 @@ public class FloodFill2D> {
private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+ private static final Predicate FOREGROUND_CHECK = Label::isForeground;
+
+ private static ExecutorService floodFillExector = newFloodFillExecutor();
+
+ private static ExecutorService newFloodFillExecutor() {
+ return Executors.newFixedThreadPool(Math.min(Runtime.getRuntime().availableProcessors() - 1, 1), new NamedThreadFactory("flood-fill-2d", true, 8));
+ }
+
private final ObservableValue activeViewerProperty;
private final MaskedSource source;
@@ -59,7 +69,6 @@ public class FloodFill2D> {
private final SimpleDoubleProperty fillDepth = new SimpleDoubleProperty(2.0);
- private static final Predicate FOREGROUND_CHECK = it -> Label.isForeground(it.get());
private ViewerMask viewerMask;
@@ -85,6 +94,10 @@ public void provideMask(@NotNull ViewerMask mask) {
this.viewerMask = mask;
}
+ public ViewerMask getMask() {
+ return viewerMask;
+ }
+
public void release() {
this.viewerMask = null;
@@ -101,7 +114,7 @@ public Interval getMaskInterval() {
return this.maskIntervalProperty.get();
}
- public UtilityTask fillViewerAt(final double viewerSeedX, final double viewerSeedY, Long fill, FragmentSegmentAssignment assignment) {
+ public @Nullable UtilityTask> fillViewerAt(final double viewerSeedX, final double viewerSeedY, Long fill, FragmentSegmentAssignment assignment) {
if (fill == null) {
LOG.info("Received invalid label {} -- will not fill.", fill);
@@ -126,10 +139,24 @@ public UtilityTask fillViewerAt(final double viewerSeedX, final double
}
final var maskPos = mask.displayPointToInitialMaskPoint(viewerSeedX, viewerSeedY);
final var filter = getBackgorundLabelMaskForAssignment(maskPos, mask, assignment, fill);
- return fillMaskAt(maskPos, mask, fill, filter);
+ if (filter == null)
+ return null;
+ final UtilityTask> floodFillTask = fillMaskAt(maskPos, mask, fill, filter);
+ if (this.viewerMask == null) {
+ floodFillTask.onCancelled(true, (state, task) -> {
+ try {
+ mask.getSource().resetMasks();
+ mask.requestRepaint();
+ } catch (final MaskInUse e) {
+ e.printStackTrace();
+ }
+ });
+ }
+ floodFillTask.submit(floodFillExector);
+ return floodFillTask;
}
- public UtilityTask fillViewerAt(final double currentViewerX, final double currentViewerY, Long fill, RandomAccessibleInterval filter) {
+ public UtilityTask> fillViewerAt(final double viewerSeedX, final double viewerSeedY, Long fill, RandomAccessibleInterval filter) {
if (fill == null) {
LOG.info("Received invalid label {} -- will not fill.", fill);
@@ -152,135 +179,113 @@ public UtilityTask fillViewerAt(final double currentViewerX, final dou
} else {
mask = viewerMask;
}
- final var maskPos = mask.displayPointToInitialMaskPoint(currentViewerX, currentViewerY);
- return fillMaskAt(maskPos, mask, fill, filter);
+ final var maskPos = mask.displayPointToInitialMaskPoint(viewerSeedX, viewerSeedY);
+ final UtilityTask> floodFillTask = fillMaskAt(maskPos, mask, fill, filter);
+ if (this.viewerMask == null) {
+ floodFillTask.onCancelled(true, (state, task) -> {
+ try {
+ mask.getSource().resetMasks();
+ mask.requestRepaint();
+ } catch (final MaskInUse e) {
+ e.printStackTrace();
+ }
+ });
+ }
+ floodFillTask.submit(floodFillExector);
+ return floodFillTask;
}
+
@NotNull
- private UtilityTask fillMaskAt(Point maskPos, ViewerMask mask, Long fill, RandomAccessibleInterval filter) {
+ private UtilityTask> fillMaskAt(Point maskPos, ViewerMask mask, Long fill, RandomAccessibleInterval filter) {
+
+ final Interval screenInterval = mask.getScreenInterval();
+
+ final AtomicBoolean triggerRefresh = new AtomicBoolean(false);
+ final RandomAccessibleInterval writableViewerImg = mask.getViewerImg().getWritableSource();
+ final var sourceAccessTracker = new AccessBoxRandomAccessibleOnGet<>(Views.extendValue(writableViewerImg, new UnsignedLongType(fill))) {
+ final Point position = new Point(sourceAccess.numDimensions());
+
+ @Override
+ public UnsignedLongType get() {
+ if (Thread.currentThread().isInterrupted())
+ throw new RuntimeException("Flood Fill Interrupted");
+ synchronized (this) {
+ updateAccessBox();
+ }
+ sourceAccess.localize(position);
+ if (!triggerRefresh.get() && Intervals.contains(screenInterval, position)) {
+ triggerRefresh.set(true);
+ }
+ return sourceAccess.get();
+ }
+ };
+ sourceAccessTracker.initAccessBox();
+
final var floodFillTask = createViewerFloodFillTask(
maskPos,
mask,
filter,
- fill,
- this.viewerMask == null
+ sourceAccessTracker,
+ fill
);
- InvokeOnJavaFXApplicationThread.invoke(() -> {
- floodFillTask.valueProperty().addListener((obs, oldv, fillIntervalInMask) -> {
- final Interval curMaskInterval = maskIntervalProperty.get();
- if (fillIntervalInMask != null) {
- if (curMaskInterval == null) {
- maskIntervalProperty.set(fillIntervalInMask);
- } else {
- maskIntervalProperty.set(Intervals.union(fillIntervalInMask, curMaskInterval));
+ final var refreshAnimation = new AnimationTimer() {
+
+ final static long delay = 2_000_000_000; // 2 second delay before refreshes start
+ final static long REFRESH_RATE = 1_000_000_000;
+ final long start = System.nanoTime();
+ long before = start;
+
+ @Override
+ public void handle(long now) {
+ if (now - start < delay || now - before < REFRESH_RATE) return;
+ if (!floodFillTask.isCancelled()) {
+ if (triggerRefresh.get()) {
+ mask.requestRepaint(sourceAccessTracker.createAccessInterval());
+ before = now;
+ triggerRefresh.set(false);
}
}
+ }
+ };
- });
- });
- floodFillTask.submit();
+ if (floodFillExector.isShutdown()) {
+ floodFillExector = newFloodFillExecutor();
+ }
- refreshDuringFloodFill(floodFillTask).start();
+ floodFillTask.onEnd((task) -> {
+ refreshAnimation.stop();
+ /*manually trigger repaint after stop to ensure full interval has been repainted*/
+ mask.requestRepaint(sourceAccessTracker.createAccessInterval());
+ });
+ floodFillTask.onSuccess((state, task) -> {
+ maskIntervalProperty.set(sourceAccessTracker.createAccessInterval());
+ });
+
+ refreshAnimation.start();
return floodFillTask;
}
- public static UtilityTask createViewerFloodFillTask(
+ public static UtilityTask> createViewerFloodFillTask(
Point maskPos,
ViewerMask mask,
- RandomAccessibleInterval floodFillFilter, long fill,
- Boolean apply) {
+ RandomAccessibleInterval floodFillFilter,
+ RandomAccessible target,
+ long fill) {
- return Tasks.createTask(task -> {
+ return Tasks.createTask(task -> {
if (floodFillFilter == null) {
- if (apply) {
- try {
- mask.getSource().resetMasks(true);
- } catch (MaskInUse e) {
- Exceptions.alert(e, Paintera.getPaintera().getBaseView().getNode().getScene().getWindow());
- }
- }
- return new FinalInterval(0, 0, 0);
- } else {
- return viewerMaskFloodFill(maskPos, mask, floodFillFilter, fill, apply);
- }
- }).onCancelled((state, task) -> {
- try {
- mask.getSource().resetMasks();
- } catch (final MaskInUse e) {
- e.printStackTrace();
- }
- });
- }
-
- public static Interval viewerMaskFloodFill(
- final Point maskPos,
- final ViewerMask mask,
- final RandomAccessibleInterval filter,
- final long fill,
- final Boolean apply) {
-
- final var fillIntervalInMask = fillViewerMaskAt(maskPos, mask, filter, fill);
-
- final Interval affectedSourceInterval = Intervals.smallestContainingInterval(
- mask.getCurrentMaskToSourceWithDepthTransform().estimateBounds(fillIntervalInMask));
-
- if (apply) {
- final MaskedSource extends RealType>, ?> source = mask.getSource();
- source.applyMask(source.getCurrentMask(), affectedSourceInterval, FOREGROUND_CHECK);
- }
- mask.requestRepaint(fillIntervalInMask);
- return fillIntervalInMask;
- }
-
- public static Interval viewerMaskFloodFill(
- final Point maskPos,
- final ViewerMask mask,
- final RandomAccessibleInterval filter,
- final long fill,
- final Boolean apply,
- final RandomAccessibleInterval maskImage) {
-
- final var fillIntervalInMask = fillViewerMaskAt(maskPos, maskImage, filter, fill);
-
- final Interval affectedSourceInterval = Intervals.smallestContainingInterval(
- mask.getCurrentMaskToSourceWithDepthTransform().estimateBounds(fillIntervalInMask));
-
- if (apply) {
- final MaskedSource extends RealType>, ?> source = mask.getSource();
- source.applyMask(source.getCurrentMask(), affectedSourceInterval, FOREGROUND_CHECK);
- }
- mask.requestRepaint(fillIntervalInMask);
- return fillIntervalInMask;
- }
-
- public static Thread refreshDuringFloodFill() {
-
- return refreshDuringFloodFill(null);
- }
-
- public static Thread refreshDuringFloodFill(Task task) {
-
- return new Thread(() -> {
- while (task == null || !task.isDone()) {
try {
- Thread.sleep(100);
- } catch (final InterruptedException e) {
- Thread.currentThread().interrupt(); // restore interrupted status
+ mask.getSource().resetMasks(true);
+ } catch (MaskInUse e) {
+ Exceptions.alert(e, Paintera.getPaintera().getBaseView().getNode().getScene().getWindow());
}
-
- if (Thread.currentThread().isInterrupted())
- break;
-
- LOG.trace("Updating View for FloodFill2D");
- Paintera.getPaintera().getBaseView().orthogonalViews().requestRepaint();
- }
-
- if (Thread.interrupted() && task != null) {
- task.cancel();
+ } else {
+ fillAt(maskPos, target, floodFillFilter, fill);
}
});
}
@@ -387,7 +392,7 @@ public static