From bad728d8959e2159e78ea80b079c67395d25ec0e Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Fri, 17 May 2019 14:57:55 -0400 Subject: [PATCH 01/84] add viewer-specific handler for entering shape interpolation mode --- .../control/ShapeInterpolationMode.java | 62 +++++++++++++++++++ .../paintera/state/LabelSourceState.java | 9 ++- 2 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java new file mode 100644 index 000000000..b669ff417 --- /dev/null +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java @@ -0,0 +1,62 @@ +package org.janelia.saalfeldlab.paintera.control; + +import org.janelia.saalfeldlab.fx.event.DelegateEventHandlers; +import org.janelia.saalfeldlab.fx.event.EventFX; +import org.janelia.saalfeldlab.fx.event.KeyTracker; +import org.janelia.saalfeldlab.paintera.PainteraBaseView; +import org.janelia.saalfeldlab.paintera.control.selection.SelectedIds; + +import bdv.fx.viewer.ViewerPanelFX; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.event.Event; +import javafx.event.EventHandler; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; + +public class ShapeInterpolationMode +{ + private final ObjectProperty<ViewerPanelFX> activeViewer = new SimpleObjectProperty<>(); + + private final SelectedIds selectedIds; + + public ShapeInterpolationMode(final SelectedIds selectedIds) + { + this.selectedIds = selectedIds; + } + + public ObjectProperty<ViewerPanelFX> activeViewerProperty() + { + return this.activeViewer; + } + + public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final KeyTracker keyTracker) + { + final DelegateEventHandlers.AnyHandler filter = DelegateEventHandlers.handleAny(); + filter.addEventHandler( + KeyEvent.KEY_PRESSED, + EventFX.KEY_PRESSED( + "enter shape interpolation mode", + e -> enterMode((ViewerPanelFX) e.getTarget()), + e -> e.getTarget() instanceof ViewerPanelFX && + selectedIds.isLastSelectionValid() && + keyTracker.areOnlyTheseKeysDown(KeyCode.S) + ) + ); + return filter; + } + + public void enterMode(final ViewerPanelFX viewer) + { + assert this.activeViewer.get() == null; + activeViewer.set(viewer); + // ... + } + + public void exitMode() + { + assert this.activeViewer.get() != null; + // ... + this.activeViewer.set(null); + } +} diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java index b25b315b2..64a5ad090 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java @@ -52,6 +52,7 @@ import org.janelia.saalfeldlab.paintera.cache.global.GlobalCache; import org.janelia.saalfeldlab.paintera.composition.ARGBCompositeAlphaYCbCr; import org.janelia.saalfeldlab.paintera.composition.Composite; +import org.janelia.saalfeldlab.paintera.control.ShapeInterpolationMode; import org.janelia.saalfeldlab.paintera.control.assignment.FragmentSegmentAssignmentOnlyLocal; import org.janelia.saalfeldlab.paintera.control.assignment.FragmentSegmentAssignmentState; import org.janelia.saalfeldlab.paintera.control.lock.LockedSegmentsOnlyLocal; @@ -124,6 +125,8 @@ public class LabelSourceState<D extends IntegerType<D>, T> private final LabelSourceStateMergeDetachHandler mergeDetachHandler; + private final ShapeInterpolationMode shapeInterpolationMode; + private final ObjectProperty<FloodFillState> floodFillState = new SimpleObjectProperty<>(); private final HBox displayStatus; @@ -152,6 +155,7 @@ public LabelSourceState( this.paintHandler = new LabelSourceStatePaintHandler(selectedIds); this.idSelectorHandler = new LabelSourceStateIdSelectorHandler(dataSource, selectedIds, assignment, lockedSegments); this.mergeDetachHandler = new LabelSourceStateMergeDetachHandler(dataSource, selectedIds, assignment, idService); + this.shapeInterpolationMode = new ShapeInterpolationMode(selectedIds); this.displayStatus = createDisplayStatus(); assignment.addListener(obs -> stain()); selectedIds.addListener(obs -> stain()); @@ -478,7 +482,10 @@ public EventHandler<Event> stateSpecificViewerEventHandler(PainteraBaseView pain @Override public EventHandler<Event> stateSpecificViewerEventFilter(PainteraBaseView paintera, KeyTracker keyTracker) { LOG.info("Returning {}-specific filter", getClass().getSimpleName()); - return paintHandler.viewerFilter(paintera, keyTracker); + final DelegateEventHandlers.ListDelegateEventHandler<Event> filter = DelegateEventHandlers.listHandler(); + filter.addHandler(paintHandler.viewerFilter(paintera, keyTracker)); + filter.addHandler(shapeInterpolationMode.modeHandler(paintera, keyTracker)); + return filter; } @Override From a5b7aa7c1ba5f8d267d430e1c13d13afec6f1855 Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Fri, 17 May 2019 14:58:25 -0400 Subject: [PATCH 02/84] block user input in other viewers upon entering shape interpolation mode --- .../control/ShapeInterpolationMode.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java index b669ff417..3abf7feb8 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java @@ -11,6 +11,8 @@ import javafx.beans.property.SimpleObjectProperty; import javafx.event.Event; import javafx.event.EventHandler; +import javafx.scene.Node; +import javafx.scene.Parent; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; @@ -50,13 +52,28 @@ public void enterMode(final ViewerPanelFX viewer) { assert this.activeViewer.get() == null; activeViewer.set(viewer); + setDisableOtherViewers(true); // ... } public void exitMode() { assert this.activeViewer.get() != null; + setDisableOtherViewers(false); // ... this.activeViewer.set(null); } + + private void setDisableOtherViewers(final boolean disable) + { + final Parent parent = this.activeViewer.get().getParent(); + for (final Node child : parent.getChildrenUnmodifiable()) + { + if (child instanceof ViewerPanelFX && child != this.activeViewer.get()) + { + final ViewerPanelFX viewer = (ViewerPanelFX) child; + viewer.setDisable(disable); + } + } + } } From 81fda9f85fd1ac328e75a8cb7d7c6fd76ad1be49 Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Fri, 17 May 2019 15:26:36 -0400 Subject: [PATCH 03/84] add grayed out effect to other viewers in shape interpolation mode --- .../paintera/control/ShapeInterpolationMode.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java index 3abf7feb8..bb37178dc 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java @@ -5,7 +5,6 @@ import org.janelia.saalfeldlab.fx.event.KeyTracker; import org.janelia.saalfeldlab.paintera.PainteraBaseView; import org.janelia.saalfeldlab.paintera.control.selection.SelectedIds; - import bdv.fx.viewer.ViewerPanelFX; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; @@ -13,6 +12,7 @@ import javafx.event.EventHandler; import javafx.scene.Node; import javafx.scene.Parent; +import javafx.scene.effect.ColorAdjust; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; @@ -73,6 +73,17 @@ private void setDisableOtherViewers(final boolean disable) { final ViewerPanelFX viewer = (ViewerPanelFX) child; viewer.setDisable(disable); + if (disable) + { + final ColorAdjust grayedOutEffect = new ColorAdjust(); + grayedOutEffect.setContrast(-0.2); + grayedOutEffect.setBrightness(-0.5); + viewer.setEffect(grayedOutEffect); + } + else + { + viewer.setEffect(null); + } } } } From 7f5e75e480d81aa5e44fe13bf4a43432798f2bd5 Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Fri, 17 May 2019 15:32:54 -0400 Subject: [PATCH 04/84] add handler to exit shape interpolation mode --- .../paintera/control/ShapeInterpolationMode.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java index bb37178dc..c5ba974b8 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java @@ -41,10 +41,20 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke "enter shape interpolation mode", e -> enterMode((ViewerPanelFX) e.getTarget()), e -> e.getTarget() instanceof ViewerPanelFX && + this.activeViewer.get() == null && selectedIds.isLastSelectionValid() && keyTracker.areOnlyTheseKeysDown(KeyCode.S) ) ); + filter.addEventHandler( + KeyEvent.KEY_PRESSED, + EventFX.KEY_PRESSED( + "exit shape interpolation mode", + e -> exitMode(), + e -> this.activeViewer.get() != null && + keyTracker.areOnlyTheseKeysDown(KeyCode.ESCAPE) + ) + ); return filter; } From 81f354cd8967b24acf3ed18ad6e08e46ea19fd6b Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Fri, 17 May 2019 15:33:37 -0400 Subject: [PATCH 05/84] add basic logging for shape interpolation mode --- .../paintera/control/ShapeInterpolationMode.java | 9 +++++++++ .../saalfeldlab/paintera/state/LabelSourceState.java | 2 ++ 2 files changed, 11 insertions(+) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java index c5ba974b8..0b61e7643 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java @@ -1,10 +1,15 @@ package org.janelia.saalfeldlab.paintera.control; +import java.lang.invoke.MethodHandles; + import org.janelia.saalfeldlab.fx.event.DelegateEventHandlers; import org.janelia.saalfeldlab.fx.event.EventFX; import org.janelia.saalfeldlab.fx.event.KeyTracker; import org.janelia.saalfeldlab.paintera.PainteraBaseView; import org.janelia.saalfeldlab.paintera.control.selection.SelectedIds; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import bdv.fx.viewer.ViewerPanelFX; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; @@ -18,6 +23,8 @@ public class ShapeInterpolationMode { + private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + private final ObjectProperty<ViewerPanelFX> activeViewer = new SimpleObjectProperty<>(); private final SelectedIds selectedIds; @@ -60,6 +67,7 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke public void enterMode(final ViewerPanelFX viewer) { + LOG.info("Entering shape interpolation mode"); assert this.activeViewer.get() == null; activeViewer.set(viewer); setDisableOtherViewers(true); @@ -68,6 +76,7 @@ public void enterMode(final ViewerPanelFX viewer) public void exitMode() { + LOG.info("Exiting shape interpolation mode"); assert this.activeViewer.get() != null; setDisableOtherViewers(false); // ... diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java index 64a5ad090..dd283cbd8 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java @@ -472,6 +472,7 @@ public EventHandler<Event> stateSpecificGlobalEventHandler(PainteraBaseView pain @Override public EventHandler<Event> stateSpecificViewerEventHandler(PainteraBaseView paintera, KeyTracker keyTracker) { LOG.info("Returning {}-specific handler", getClass().getSimpleName()); + LOG.debug("Returning {}-specific handler", getClass().getSimpleName()); final DelegateEventHandlers.ListDelegateEventHandler<Event> handler = DelegateEventHandlers.listHandler(); handler.addHandler(paintHandler.viewerHandler(paintera, keyTracker)); handler.addHandler(idSelectorHandler.viewerHandler(paintera, keyTracker)); @@ -482,6 +483,7 @@ public EventHandler<Event> stateSpecificViewerEventHandler(PainteraBaseView pain @Override public EventHandler<Event> stateSpecificViewerEventFilter(PainteraBaseView paintera, KeyTracker keyTracker) { LOG.info("Returning {}-specific filter", getClass().getSimpleName()); + LOG.debug("Returning {}-specific filter", getClass().getSimpleName()); final DelegateEventHandlers.ListDelegateEventHandler<Event> filter = DelegateEventHandlers.listHandler(); filter.addHandler(paintHandler.viewerFilter(paintera, keyTracker)); filter.addHandler(shapeInterpolationMode.modeHandler(paintera, keyTracker)); From 6b00ba1b406d66a287cca2284482b1c1ef1894ef Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Fri, 17 May 2019 16:57:24 -0400 Subject: [PATCH 06/84] add basic structure for specifying allowed UI actions --- .../paintera/PainteraBaseView.java | 15 ++++++ .../control/actions/AllowedActions.java | 47 +++++++++++++++++++ .../control/actions/NavigationAction.java | 26 ++++++++++ .../paintera/control/actions/PaintAction.java | 28 +++++++++++ .../control/actions/SelectIdAction.java | 26 ++++++++++ 5 files changed, 142 insertions(+) create mode 100644 src/main/java/org/janelia/saalfeldlab/paintera/control/actions/AllowedActions.java create mode 100644 src/main/java/org/janelia/saalfeldlab/paintera/control/actions/NavigationAction.java create mode 100644 src/main/java/org/janelia/saalfeldlab/paintera/control/actions/PaintAction.java create mode 100644 src/main/java/org/janelia/saalfeldlab/paintera/control/actions/SelectIdAction.java diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/PainteraBaseView.java b/src/main/java/org/janelia/saalfeldlab/paintera/PainteraBaseView.java index 73df8cba9..9edf5c31d 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/PainteraBaseView.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/PainteraBaseView.java @@ -4,6 +4,7 @@ import bdv.viewer.SourceAndConverter; import bdv.viewer.ViewerOptions; import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.event.Event; @@ -37,6 +38,7 @@ import org.janelia.saalfeldlab.paintera.config.OrthoSliceConfig; import org.janelia.saalfeldlab.paintera.config.OrthoSliceConfigBase; import org.janelia.saalfeldlab.paintera.config.Viewer3DConfig; +import org.janelia.saalfeldlab.paintera.control.actions.AllowedActions; import org.janelia.saalfeldlab.paintera.data.axisorder.AxisOrder; import org.janelia.saalfeldlab.paintera.data.axisorder.AxisOrderNotSupported; import org.janelia.saalfeldlab.paintera.data.mask.MaskedSource; @@ -72,6 +74,7 @@ * <li>{@link SourceInfo source state management}</li> * <li>{@link GlobalCache global cache management}</li> * <li>{@link ExecutorService thread management} for number crunching</li> + * <li>{@link AllowedActions UI mode}</li> * </ul><p> */ public class PainteraBaseView @@ -101,6 +104,8 @@ public class PainteraBaseView private final OrthogonalViews<Viewer3DFX> views; + private final ObjectProperty<AllowedActions> allowedActionsProperty; + private final ObservableList<SourceAndConverter<?>> visibleSourcesAndConverters = sourceInfo .trackVisibleSourcesAndConverters(); @@ -157,6 +162,7 @@ public PainteraBaseView( s -> Optional.ofNullable(sourceInfo.getState(s)).map(SourceState::interpolationProperty).map(ObjectProperty::get).orElse(Interpolation.NLINEAR), s -> Optional.ofNullable(sourceInfo.getState(s)).map(SourceState::getAxisOrder).orElse(null) ); + this.allowedActionsProperty = new SimpleObjectProperty<>(AllowedActions.all()); this.vsacUpdate = change -> views.setAllSources(visibleSourcesAndConverters); visibleSourcesAndConverters.addListener(vsacUpdate); LOG.debug("Meshes group={}", viewer3D.meshesGroup()); @@ -207,6 +213,15 @@ public GlobalTransformManager manager() return this.manager; } + /** + * + * @return {@link AllowedActions} that describe the user interface in the current application mode + */ + public ObjectProperty<AllowedActions> allowedActionsProperty() + { + return this.allowedActionsProperty; + } + /** * * Add a source and state to the viewer diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/AllowedActions.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/AllowedActions.java new file mode 100644 index 000000000..b16d66f45 --- /dev/null +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/AllowedActions.java @@ -0,0 +1,47 @@ +package org.janelia.saalfeldlab.paintera.control.actions; + +import java.util.EnumSet; + +/** + * Describes what actions in the UI are allowed in the current application mode. + */ +public final class AllowedActions +{ + private final EnumSet<NavigationAction> navigationAllowedActions; + private final EnumSet<SelectIdAction> selectIdAllowedActions; + private final EnumSet<PaintAction> paintAllowedActions; + + public AllowedActions( + final EnumSet<NavigationAction> navigationAllowedActions, + final EnumSet<SelectIdAction> selectIdAllowedActions, + final EnumSet<PaintAction> paintAllowedActions) + { + this.navigationAllowedActions = navigationAllowedActions; + this.selectIdAllowedActions = selectIdAllowedActions; + this.paintAllowedActions = paintAllowedActions; + } + + public boolean isAllowed(final NavigationAction navigationAction) + { + return this.navigationAllowedActions.contains(navigationAction); + } + + public boolean isAllowed(final SelectIdAction selectIdAction) + { + return this.selectIdAllowedActions.contains(selectIdAction); + } + + public boolean isAllowed(final PaintAction paintAction) + { + return this.paintAllowedActions.contains(paintAction); + } + + public static AllowedActions all() + { + return new AllowedActions( + NavigationAction.all(), + SelectIdAction.all(), + PaintAction.all() + ); + } +} diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/NavigationAction.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/NavigationAction.java new file mode 100644 index 000000000..57788a4e8 --- /dev/null +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/NavigationAction.java @@ -0,0 +1,26 @@ +package org.janelia.saalfeldlab.paintera.control.actions; + +import java.util.EnumSet; + +public enum NavigationAction +{ + Drag, + Scroll, + Zoom, + Rotate; + + public static EnumSet<NavigationAction> of(final NavigationAction first, final NavigationAction... rest) + { + return EnumSet.of(first, rest); + } + + public static EnumSet<NavigationAction> all() + { + return EnumSet.allOf(NavigationAction.class); + } + + public static EnumSet<NavigationAction> none() + { + return EnumSet.noneOf(NavigationAction.class); + } +} diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/PaintAction.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/PaintAction.java new file mode 100644 index 000000000..dc0436c7a --- /dev/null +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/PaintAction.java @@ -0,0 +1,28 @@ +package org.janelia.saalfeldlab.paintera.control.actions; + +import java.util.EnumSet; + +public enum PaintAction +{ + Paint, + Erase, + Background, + Fill, + Restrict, + SetBrush; + + public static EnumSet<PaintAction> of(final PaintAction first, final PaintAction... rest) + { + return EnumSet.of(first, rest); + } + + public static EnumSet<PaintAction> all() + { + return EnumSet.allOf(PaintAction.class); + } + + public static EnumSet<PaintAction> none() + { + return EnumSet.noneOf(PaintAction.class); + } +} diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/SelectIdAction.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/SelectIdAction.java new file mode 100644 index 000000000..06d0e826e --- /dev/null +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/SelectIdAction.java @@ -0,0 +1,26 @@ +package org.janelia.saalfeldlab.paintera.control.actions; + +import java.util.EnumSet; + +public enum SelectIdAction +{ + Toggle, + Append, + Create, + Lock; + + public static EnumSet<SelectIdAction> of(final SelectIdAction first, final SelectIdAction... rest) + { + return EnumSet.of(first, rest); + } + + public static EnumSet<SelectIdAction> all() + { + return EnumSet.allOf(SelectIdAction.class); + } + + public static EnumSet<SelectIdAction> none() + { + return EnumSet.noneOf(SelectIdAction.class); + } +} From 68eedbb6362a24786b0a1055d3df0ed1309d3fb6 Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Fri, 17 May 2019 16:58:22 -0400 Subject: [PATCH 07/84] set appropriate allowed UI actions while in shape interpolation mode --- .../control/ShapeInterpolationMode.java | 34 ++++++++++++++++--- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java index 0b61e7643..2c82550f7 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java @@ -1,11 +1,14 @@ package org.janelia.saalfeldlab.paintera.control; import java.lang.invoke.MethodHandles; - import org.janelia.saalfeldlab.fx.event.DelegateEventHandlers; import org.janelia.saalfeldlab.fx.event.EventFX; import org.janelia.saalfeldlab.fx.event.KeyTracker; import org.janelia.saalfeldlab.paintera.PainteraBaseView; +import org.janelia.saalfeldlab.paintera.control.actions.AllowedActions; +import org.janelia.saalfeldlab.paintera.control.actions.NavigationAction; +import org.janelia.saalfeldlab.paintera.control.actions.PaintAction; +import org.janelia.saalfeldlab.paintera.control.actions.SelectIdAction; import org.janelia.saalfeldlab.paintera.control.selection.SelectedIds; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -25,10 +28,22 @@ public class ShapeInterpolationMode { private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + private static final AllowedActions allowedActionsInShapeInterpolationMode; + static + { + allowedActionsInShapeInterpolationMode = new AllowedActions( + NavigationAction.of(NavigationAction.Drag, NavigationAction.Zoom, NavigationAction.Scroll), + SelectIdAction.none(), + PaintAction.of(PaintAction.Paint) + ); + } + private final ObjectProperty<ViewerPanelFX> activeViewer = new SimpleObjectProperty<>(); private final SelectedIds selectedIds; + private AllowedActions lastAllowedActions; + public ShapeInterpolationMode(final SelectedIds selectedIds) { this.selectedIds = selectedIds; @@ -46,7 +61,7 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke KeyEvent.KEY_PRESSED, EventFX.KEY_PRESSED( "enter shape interpolation mode", - e -> enterMode((ViewerPanelFX) e.getTarget()), + e -> enterMode(paintera, (ViewerPanelFX) e.getTarget()), e -> e.getTarget() instanceof ViewerPanelFX && this.activeViewer.get() == null && selectedIds.isLastSelectionValid() && @@ -57,7 +72,7 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke KeyEvent.KEY_PRESSED, EventFX.KEY_PRESSED( "exit shape interpolation mode", - e -> exitMode(), + e -> exitMode(paintera), e -> this.activeViewer.get() != null && keyTracker.areOnlyTheseKeysDown(KeyCode.ESCAPE) ) @@ -65,21 +80,30 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke return filter; } - public void enterMode(final ViewerPanelFX viewer) + public void enterMode(final PainteraBaseView paintera, final ViewerPanelFX viewer) { LOG.info("Entering shape interpolation mode"); assert this.activeViewer.get() == null; activeViewer.set(viewer); setDisableOtherViewers(true); + + this.lastAllowedActions = paintera.allowedActionsProperty().get(); + paintera.allowedActionsProperty().set(allowedActionsInShapeInterpolationMode); + // ... } - public void exitMode() + public void exitMode(final PainteraBaseView paintera) { LOG.info("Exiting shape interpolation mode"); assert this.activeViewer.get() != null; setDisableOtherViewers(false); + + paintera.allowedActionsProperty().set(this.lastAllowedActions); + this.lastAllowedActions = null; + // ... + this.activeViewer.set(null); } From cf90564f23950c74b1927c2bd365ac4c693177a3 Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Fri, 17 May 2019 18:11:51 -0400 Subject: [PATCH 08/84] check if label selection actions are allowed --- .../control/actions/SelectIdAction.java | 2 +- .../LabelSourceStateIdSelectorHandler.java | 17 +++++++++++++---- .../state/LabelSourceStatePaintHandler.java | 7 ------- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/SelectIdAction.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/SelectIdAction.java index 06d0e826e..a92f0dd5f 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/SelectIdAction.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/SelectIdAction.java @@ -6,7 +6,7 @@ public enum SelectIdAction { Toggle, Append, - Create, + CreateNewLabel, Lock; public static EnumSet<SelectIdAction> of(final SelectIdAction first, final SelectIdAction... rest) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStateIdSelectorHandler.java b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStateIdSelectorHandler.java index c280a4bfd..cf8504e3b 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStateIdSelectorHandler.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStateIdSelectorHandler.java @@ -13,8 +13,10 @@ import org.janelia.saalfeldlab.fx.event.KeyTracker; import org.janelia.saalfeldlab.paintera.PainteraBaseView; import org.janelia.saalfeldlab.paintera.control.IdSelector; +import org.janelia.saalfeldlab.paintera.control.actions.SelectIdAction; import org.janelia.saalfeldlab.paintera.control.assignment.FragmentSegmentAssignment; import org.janelia.saalfeldlab.paintera.control.lock.LockedSegments; +import org.janelia.saalfeldlab.paintera.control.paint.SelectNextId; import org.janelia.saalfeldlab.paintera.control.selection.SelectedIds; import org.janelia.saalfeldlab.paintera.data.DataSource; import org.slf4j.Logger; @@ -66,20 +68,27 @@ public EventHandler<Event> viewerHandler(final PainteraBaseView paintera, final }; } - private EventHandler<Event> makeHandler(PainteraBaseView paintera, KeyTracker keyTracker, ViewerPanelFX vp) { + private EventHandler<Event> makeHandler(final PainteraBaseView paintera, final KeyTracker keyTracker, final ViewerPanelFX vp) { final IdSelector selector = new IdSelector(source, selectedIds, vp); final DelegateEventHandlers.AnyHandler handler = DelegateEventHandlers.handleAny(); // TODO event handlers should probably not be on ANY/RELEASED but on PRESSED handler.addEventHandler(MouseEvent.ANY, selector.selectFragmentWithMaximumCount( "toggle single id", - event -> event.isPrimaryButtonDown() && keyTracker.noKeysActive()).handler()); + event -> paintera.allowedActionsProperty().get().isAllowed(SelectIdAction.Toggle) && event.isPrimaryButtonDown() && keyTracker.noKeysActive()).handler()); handler.addEventHandler(MouseEvent.ANY, selector.appendFragmentWithMaximumCount( "append id", - event -> event.isSecondaryButtonDown() && keyTracker.noKeysActive()).handler()); + event -> paintera.allowedActionsProperty().get().isAllowed(SelectIdAction.Append) && event.isSecondaryButtonDown() && keyTracker.noKeysActive()).handler()); handler.addOnKeyPressed(EventFX.KEY_PRESSED( "lock segment", e -> selector.toggleLock(selectedIds, assignment, lockedSegments), - e -> keyTracker.areOnlyTheseKeysDown(KeyCode.L))); + e -> paintera.allowedActionsProperty().get().isAllowed(SelectIdAction.Lock) && keyTracker.areOnlyTheseKeysDown(KeyCode.L))); + + final SourceInfo sourceInfo = paintera.sourceInfo(); + final SelectNextId nextId = new SelectNextId(sourceInfo); + handler.addOnKeyPressed(EventFX.KEY_PRESSED( + "next id", + event -> nextId.getNextId(), + event -> paintera.allowedActionsProperty().get().isAllowed(SelectIdAction.CreateNewLabel) && keyTracker.areOnlyTheseKeysDown(KeyCode.N))); return handler; } } diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStatePaintHandler.java b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStatePaintHandler.java index d5219484a..31094ba57 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStatePaintHandler.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStatePaintHandler.java @@ -24,7 +24,6 @@ import org.janelia.saalfeldlab.paintera.control.paint.PaintActions2D; import org.janelia.saalfeldlab.paintera.control.paint.PaintClickOrDrag; import org.janelia.saalfeldlab.paintera.control.paint.RestrictPainting; -import org.janelia.saalfeldlab.paintera.control.paint.SelectNextId; import org.janelia.saalfeldlab.paintera.control.selection.SelectedIds; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -217,12 +216,6 @@ private EventHandler<Event> makeHandler(PainteraBaseView paintera, KeyTracker ke KeyCode.SHIFT, KeyCode.R))); - final SelectNextId nextId = new SelectNextId(sourceInfo); - handler.addOnKeyPressed(EventFX.KEY_PRESSED( - "next id", - event -> nextId.getNextId(), - event -> keyTracker.areOnlyTheseKeysDown(KeyCode.N))); - return handler; } From 23e6affb970281750144e2423960b1070a92ec7e Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Fri, 17 May 2019 18:28:43 -0400 Subject: [PATCH 09/84] check if navigation actions are allowed --- .../paintera/PainteraDefaultHandlers.java | 3 +- .../paintera/control/Navigation.java | 69 +++++++++++-------- 2 files changed, 41 insertions(+), 31 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/PainteraDefaultHandlers.java b/src/main/java/org/janelia/saalfeldlab/paintera/PainteraDefaultHandlers.java index 573461c47..46037f7b4 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/PainteraDefaultHandlers.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/PainteraDefaultHandlers.java @@ -178,7 +178,8 @@ public PainteraDefaultHandlers( baseView.manager(), v -> viewerToTransforms.get(v).displayTransform(), v -> viewerToTransforms.get(v).globalToViewerTransform(), - keyTracker + keyTracker, + baseView.allowedActionsProperty() ); this.onEnterOnExit = createOnEnterOnExit(paneWithStatus.currentFocusHolder()); diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/Navigation.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/Navigation.java index 82c206bb1..43bc865a5 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/Navigation.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/Navigation.java @@ -33,6 +33,8 @@ import org.janelia.saalfeldlab.fx.event.InstallAndRemove; import org.janelia.saalfeldlab.fx.event.KeyTracker; import org.janelia.saalfeldlab.fx.event.MouseDragFX; +import org.janelia.saalfeldlab.paintera.control.actions.AllowedActions; +import org.janelia.saalfeldlab.paintera.control.actions.NavigationAction; import org.janelia.saalfeldlab.paintera.control.navigation.AffineTransformWithListeners; import org.janelia.saalfeldlab.paintera.control.navigation.ButtonRotationSpeedConfig; import org.janelia.saalfeldlab.paintera.control.navigation.KeyRotate; @@ -62,6 +64,8 @@ public class Navigation implements ToOnEnterOnExit private final KeyTracker keyTracker; + private final ObjectProperty<AllowedActions> allowedActionsProperty; + private final HashMap<ViewerPanelFX, Collection<InstallAndRemove<Node>>> mouseAndKeyHandlers = new HashMap<>(); private final Function<ViewerPanelFX, AffineTransformWithListeners> displayTransform; @@ -72,13 +76,15 @@ public Navigation( final GlobalTransformManager manager, final Function<ViewerPanelFX, AffineTransformWithListeners> displayTransform, final Function<ViewerPanelFX, AffineTransformWithListeners> globalToViewerTransform, - final KeyTracker keyTracker) + final KeyTracker keyTracker, + final ObjectProperty<AllowedActions> allowedActionsProperty) { super(); this.manager = manager; this.displayTransform = displayTransform; this.globalToViewerTransform = globalToViewerTransform; this.keyTracker = keyTracker; + this.allowedActionsProperty = allowedActionsProperty; } @Override @@ -160,55 +166,55 @@ public Consumer<ViewerPanelFX> getOnEnter() iars.add(EventFX.SCROLL( "translate along normal", e -> scrollDefault.scroll(-ControlUtils.getBiggestScroll(e)), - event -> keyTracker.noKeysActive() + event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Scroll) && keyTracker.noKeysActive() )); iars.add(EventFX.SCROLL( "translate along normal fast", e -> scrollFast.scroll(-ControlUtils.getBiggestScroll(e)), - event -> keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT) + event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Scroll) && keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT) )); iars.add(EventFX.SCROLL( "translate along normal slow", e -> scrollSlow.scroll(-ControlUtils.getBiggestScroll(e)), - event -> keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL) + event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Scroll) && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL) )); iars.add(EventFX.KEY_PRESSED( "button translate along normal bck", e -> scrollDefault.scroll(+1), - event -> keyTracker.areOnlyTheseKeysDown(KeyCode.COMMA) + event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Scroll) && keyTracker.areOnlyTheseKeysDown(KeyCode.COMMA) )); iars.add(EventFX.KEY_PRESSED( "button translate along normal fwd", e -> scrollDefault.scroll(-1), - event -> keyTracker.areOnlyTheseKeysDown(KeyCode.PERIOD) + event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Scroll) && keyTracker.areOnlyTheseKeysDown(KeyCode.PERIOD) )); iars.add(EventFX.KEY_PRESSED( "button translate along normal fast bck", e -> scrollFast.scroll(+1), - event -> keyTracker.areOnlyTheseKeysDown(KeyCode.COMMA, KeyCode.SHIFT) + event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Scroll) && keyTracker.areOnlyTheseKeysDown(KeyCode.COMMA, KeyCode.SHIFT) )); iars.add(EventFX.KEY_PRESSED( "button translate along normal fast fwd", e -> scrollFast.scroll(-1), - event -> keyTracker.areOnlyTheseKeysDown(KeyCode.PERIOD, KeyCode.SHIFT) + event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Scroll) && keyTracker.areOnlyTheseKeysDown(KeyCode.PERIOD, KeyCode.SHIFT) )); iars.add(EventFX.KEY_PRESSED( "button translate along normal slow bck", e -> scrollSlow.scroll(+1), - event -> keyTracker.areOnlyTheseKeysDown(KeyCode.COMMA, KeyCode.CONTROL) + event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Scroll) && keyTracker.areOnlyTheseKeysDown(KeyCode.COMMA, KeyCode.CONTROL) )); iars.add(EventFX.KEY_PRESSED( "button translate along normal slow fwd", e -> scrollSlow.scroll(-1), - event -> keyTracker.areOnlyTheseKeysDown(KeyCode.PERIOD, KeyCode.CONTROL) + event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Scroll) && keyTracker.areOnlyTheseKeysDown(KeyCode.PERIOD, KeyCode.CONTROL) )); iars.add(MouseDragFX.createDrag( "translate xy", - e -> e.isSecondaryButtonDown() && keyTracker.noKeysActive(), + e -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Drag) && e.isSecondaryButtonDown() && keyTracker.noKeysActive(), true, manager, e -> translateXY.init(), @@ -220,8 +226,9 @@ public Consumer<ViewerPanelFX> getOnEnter() iars.add(EventFX.SCROLL( "zoom", event -> zoom.zoomCenteredAt(-ControlUtils.getBiggestScroll(event), event.getX(), event.getY()), - event -> keyTracker.areOnlyTheseKeysDown(KeyCode.META) || - keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL, KeyCode.SHIFT) + event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Zoom) && + (keyTracker.areOnlyTheseKeysDown(KeyCode.META) || + keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL, KeyCode.SHIFT)) )); iars.add(EventFX.KEY_PRESSED( @@ -231,9 +238,10 @@ public Consumer<ViewerPanelFX> getOnEnter() mouseXIfInsideElseCenterX.get(), mouseYIfInsideElseCenterY.get() ), - event -> keyTracker.areOnlyTheseKeysDown(KeyCode.MINUS) + event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Zoom) && + (keyTracker.areOnlyTheseKeysDown(KeyCode.MINUS) || keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT, KeyCode.MINUS) - || keyTracker.areOnlyTheseKeysDown(KeyCode.DOWN) + || keyTracker.areOnlyTheseKeysDown(KeyCode.DOWN)) )); iars.add(EventFX.KEY_PRESSED( @@ -243,9 +251,10 @@ public Consumer<ViewerPanelFX> getOnEnter() mouseXIfInsideElseCenterX.get(), mouseYIfInsideElseCenterY.get() ), - event -> keyTracker.areOnlyTheseKeysDown(KeyCode.EQUALS) + event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Zoom) && + (keyTracker.areOnlyTheseKeysDown(KeyCode.EQUALS) || keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT, KeyCode.EQUALS) - || keyTracker.areOnlyTheseKeysDown(KeyCode.UP) + || keyTracker.areOnlyTheseKeysDown(KeyCode.UP)) )); iars.add(rotationHandler( @@ -257,7 +266,7 @@ public Consumer<ViewerPanelFX> getOnEnter() globalToViewerTransform, manager::setTransform, manager, - event -> keyTracker.noKeysActive() && event.getButton().equals(MouseButton.PRIMARY) + event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Rotate) && keyTracker.noKeysActive() && event.getButton().equals(MouseButton.PRIMARY) )); iars.add(rotationHandler( @@ -269,7 +278,7 @@ public Consumer<ViewerPanelFX> getOnEnter() globalToViewerTransform, manager::setTransform, manager, - event -> keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT) && event.getButton().equals + event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT) && event.getButton().equals (MouseButton.PRIMARY) )); @@ -282,7 +291,7 @@ public Consumer<ViewerPanelFX> getOnEnter() globalToViewerTransform, manager::setTransform, manager, - event -> keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL) && event.getButton().equals( + event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL) && event.getButton().equals( MouseButton.PRIMARY) )); @@ -290,17 +299,17 @@ public Consumer<ViewerPanelFX> getOnEnter() iars.add(EventFX.KEY_PRESSED( "set key rotation axis x", e -> keyRotationAxis.set(KeyRotate.Axis.X), - e -> keyTracker.areOnlyTheseKeysDown(KeyCode.X) + e -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.X) )); iars.add(EventFX.KEY_PRESSED( "set key rotation axis y", e -> keyRotationAxis.set(KeyRotate.Axis.Y), - e -> keyTracker.areOnlyTheseKeysDown(KeyCode.Y) + e -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.Y) )); iars.add(EventFX.KEY_PRESSED( "set key rotation axis z", e -> keyRotationAxis.set(KeyRotate.Axis.Z), - e -> keyTracker.areOnlyTheseKeysDown(KeyCode.Z) + e -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.Z) )); iars.add(keyRotationHandler( @@ -315,7 +324,7 @@ public Consumer<ViewerPanelFX> getOnEnter() globalTransform, manager::setTransform, manager, - event -> keyTracker.areOnlyTheseKeysDown(KeyCode.LEFT) + event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.LEFT) )); iars.add(keyRotationHandler( @@ -330,7 +339,7 @@ public Consumer<ViewerPanelFX> getOnEnter() globalTransform, manager::setTransform, manager, - event -> keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL, KeyCode.LEFT) + event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL, KeyCode.LEFT) )); iars.add(keyRotationHandler( @@ -345,7 +354,7 @@ public Consumer<ViewerPanelFX> getOnEnter() globalTransform, manager::setTransform, manager, - event -> keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT, KeyCode.LEFT) + event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT, KeyCode.LEFT) )); iars.add(keyRotationHandler( @@ -360,7 +369,7 @@ public Consumer<ViewerPanelFX> getOnEnter() globalTransform, manager::setTransform, manager, - event -> keyTracker.areOnlyTheseKeysDown(KeyCode.RIGHT) + event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.RIGHT) )); iars.add(keyRotationHandler( @@ -375,7 +384,7 @@ public Consumer<ViewerPanelFX> getOnEnter() globalTransform, manager::setTransform, manager, - event -> keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL, KeyCode.RIGHT) + event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL, KeyCode.RIGHT) )); iars.add(keyRotationHandler( @@ -390,7 +399,7 @@ public Consumer<ViewerPanelFX> getOnEnter() globalTransform, manager::setTransform, manager, - event -> keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT, KeyCode.RIGHT) + event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT, KeyCode.RIGHT) )); final RemoveRotation removeRotation = new RemoveRotation( @@ -405,7 +414,7 @@ public Consumer<ViewerPanelFX> getOnEnter() mouseXIfInsideElseCenterX.get(), mouseYIfInsideElseCenterY.get() ), - e -> keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT, KeyCode.Z) + e -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT, KeyCode.Z) )); this.mouseAndKeyHandlers.put(t, iars); From 1aa05a82fcc8d56319dd9612d18e52ca1d8329a9 Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Fri, 17 May 2019 18:43:22 -0400 Subject: [PATCH 10/84] check if merge/split actions are allowed --- .../control/ShapeInterpolationMode.java | 4 +-- .../control/actions/AllowedActions.java | 12 ++++---- .../paintera/control/actions/LabelAction.java | 28 +++++++++++++++++++ .../control/actions/SelectIdAction.java | 26 ----------------- .../LabelSourceStateIdSelectorHandler.java | 10 +++---- .../LabelSourceStateMergeDetachHandler.java | 7 +++-- 6 files changed, 45 insertions(+), 42 deletions(-) create mode 100644 src/main/java/org/janelia/saalfeldlab/paintera/control/actions/LabelAction.java delete mode 100644 src/main/java/org/janelia/saalfeldlab/paintera/control/actions/SelectIdAction.java diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java index 2c82550f7..2aa5a53b0 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java @@ -6,9 +6,9 @@ import org.janelia.saalfeldlab.fx.event.KeyTracker; import org.janelia.saalfeldlab.paintera.PainteraBaseView; import org.janelia.saalfeldlab.paintera.control.actions.AllowedActions; +import org.janelia.saalfeldlab.paintera.control.actions.LabelAction; import org.janelia.saalfeldlab.paintera.control.actions.NavigationAction; import org.janelia.saalfeldlab.paintera.control.actions.PaintAction; -import org.janelia.saalfeldlab.paintera.control.actions.SelectIdAction; import org.janelia.saalfeldlab.paintera.control.selection.SelectedIds; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -33,7 +33,7 @@ public class ShapeInterpolationMode { allowedActionsInShapeInterpolationMode = new AllowedActions( NavigationAction.of(NavigationAction.Drag, NavigationAction.Zoom, NavigationAction.Scroll), - SelectIdAction.none(), + LabelAction.none(), PaintAction.of(PaintAction.Paint) ); } diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/AllowedActions.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/AllowedActions.java index b16d66f45..9a1d899aa 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/AllowedActions.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/AllowedActions.java @@ -8,16 +8,16 @@ public final class AllowedActions { private final EnumSet<NavigationAction> navigationAllowedActions; - private final EnumSet<SelectIdAction> selectIdAllowedActions; + private final EnumSet<LabelAction> labelAllowedActions; private final EnumSet<PaintAction> paintAllowedActions; public AllowedActions( final EnumSet<NavigationAction> navigationAllowedActions, - final EnumSet<SelectIdAction> selectIdAllowedActions, + final EnumSet<LabelAction> labelAllowedActions, final EnumSet<PaintAction> paintAllowedActions) { this.navigationAllowedActions = navigationAllowedActions; - this.selectIdAllowedActions = selectIdAllowedActions; + this.labelAllowedActions = labelAllowedActions; this.paintAllowedActions = paintAllowedActions; } @@ -26,9 +26,9 @@ public boolean isAllowed(final NavigationAction navigationAction) return this.navigationAllowedActions.contains(navigationAction); } - public boolean isAllowed(final SelectIdAction selectIdAction) + public boolean isAllowed(final LabelAction labelAction) { - return this.selectIdAllowedActions.contains(selectIdAction); + return this.labelAllowedActions.contains(labelAction); } public boolean isAllowed(final PaintAction paintAction) @@ -40,7 +40,7 @@ public static AllowedActions all() { return new AllowedActions( NavigationAction.all(), - SelectIdAction.all(), + LabelAction.all(), PaintAction.all() ); } diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/LabelAction.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/LabelAction.java new file mode 100644 index 000000000..190d5ba4b --- /dev/null +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/LabelAction.java @@ -0,0 +1,28 @@ +package org.janelia.saalfeldlab.paintera.control.actions; + +import java.util.EnumSet; + +public enum LabelAction +{ + Toggle, + Append, + CreateNew, + Lock, + Merge, + Split; + + public static EnumSet<LabelAction> of(final LabelAction first, final LabelAction... rest) + { + return EnumSet.of(first, rest); + } + + public static EnumSet<LabelAction> all() + { + return EnumSet.allOf(LabelAction.class); + } + + public static EnumSet<LabelAction> none() + { + return EnumSet.noneOf(LabelAction.class); + } +} diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/SelectIdAction.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/SelectIdAction.java deleted file mode 100644 index a92f0dd5f..000000000 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/SelectIdAction.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.janelia.saalfeldlab.paintera.control.actions; - -import java.util.EnumSet; - -public enum SelectIdAction -{ - Toggle, - Append, - CreateNewLabel, - Lock; - - public static EnumSet<SelectIdAction> of(final SelectIdAction first, final SelectIdAction... rest) - { - return EnumSet.of(first, rest); - } - - public static EnumSet<SelectIdAction> all() - { - return EnumSet.allOf(SelectIdAction.class); - } - - public static EnumSet<SelectIdAction> none() - { - return EnumSet.noneOf(SelectIdAction.class); - } -} diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStateIdSelectorHandler.java b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStateIdSelectorHandler.java index cf8504e3b..ca70893be 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStateIdSelectorHandler.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStateIdSelectorHandler.java @@ -13,7 +13,7 @@ import org.janelia.saalfeldlab.fx.event.KeyTracker; import org.janelia.saalfeldlab.paintera.PainteraBaseView; import org.janelia.saalfeldlab.paintera.control.IdSelector; -import org.janelia.saalfeldlab.paintera.control.actions.SelectIdAction; +import org.janelia.saalfeldlab.paintera.control.actions.LabelAction; import org.janelia.saalfeldlab.paintera.control.assignment.FragmentSegmentAssignment; import org.janelia.saalfeldlab.paintera.control.lock.LockedSegments; import org.janelia.saalfeldlab.paintera.control.paint.SelectNextId; @@ -74,21 +74,21 @@ private EventHandler<Event> makeHandler(final PainteraBaseView paintera, final K // TODO event handlers should probably not be on ANY/RELEASED but on PRESSED handler.addEventHandler(MouseEvent.ANY, selector.selectFragmentWithMaximumCount( "toggle single id", - event -> paintera.allowedActionsProperty().get().isAllowed(SelectIdAction.Toggle) && event.isPrimaryButtonDown() && keyTracker.noKeysActive()).handler()); + event -> paintera.allowedActionsProperty().get().isAllowed(LabelAction.Toggle) && event.isPrimaryButtonDown() && keyTracker.noKeysActive()).handler()); handler.addEventHandler(MouseEvent.ANY, selector.appendFragmentWithMaximumCount( "append id", - event -> paintera.allowedActionsProperty().get().isAllowed(SelectIdAction.Append) && event.isSecondaryButtonDown() && keyTracker.noKeysActive()).handler()); + event -> paintera.allowedActionsProperty().get().isAllowed(LabelAction.Append) && event.isSecondaryButtonDown() && keyTracker.noKeysActive()).handler()); handler.addOnKeyPressed(EventFX.KEY_PRESSED( "lock segment", e -> selector.toggleLock(selectedIds, assignment, lockedSegments), - e -> paintera.allowedActionsProperty().get().isAllowed(SelectIdAction.Lock) && keyTracker.areOnlyTheseKeysDown(KeyCode.L))); + e -> paintera.allowedActionsProperty().get().isAllowed(LabelAction.Lock) && keyTracker.areOnlyTheseKeysDown(KeyCode.L))); final SourceInfo sourceInfo = paintera.sourceInfo(); final SelectNextId nextId = new SelectNextId(sourceInfo); handler.addOnKeyPressed(EventFX.KEY_PRESSED( "next id", event -> nextId.getNextId(), - event -> paintera.allowedActionsProperty().get().isAllowed(SelectIdAction.CreateNewLabel) && keyTracker.areOnlyTheseKeysDown(KeyCode.N))); + event -> paintera.allowedActionsProperty().get().isAllowed(LabelAction.CreateNew) && keyTracker.areOnlyTheseKeysDown(KeyCode.N))); return handler; } } diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStateMergeDetachHandler.java b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStateMergeDetachHandler.java index 4f7608147..4c968b18e 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStateMergeDetachHandler.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStateMergeDetachHandler.java @@ -28,6 +28,7 @@ import org.janelia.saalfeldlab.fx.event.EventFX; import org.janelia.saalfeldlab.fx.event.KeyTracker; import org.janelia.saalfeldlab.paintera.PainteraBaseView; +import org.janelia.saalfeldlab.paintera.control.actions.LabelAction; import org.janelia.saalfeldlab.paintera.control.assignment.FragmentSegmentAssignment; import org.janelia.saalfeldlab.paintera.control.assignment.action.AssignmentAction; import org.janelia.saalfeldlab.paintera.control.assignment.action.Detach; @@ -92,15 +93,15 @@ private EventHandler<Event> makeHandler(PainteraBaseView paintera, KeyTracker ke handler.addOnMousePressed(EventFX.MOUSE_PRESSED( "merge fragments", new MergeFragments(vp), - e -> e.isPrimaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT))); + e -> paintera.allowedActionsProperty().get().isAllowed(LabelAction.Merge) && e.isPrimaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT))); handler.addOnMousePressed(EventFX.MOUSE_PRESSED( "detach fragment", new DetachFragment(vp), - e -> e.isSecondaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT))); + e -> paintera.allowedActionsProperty().get().isAllowed(LabelAction.Split) && e.isSecondaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT))); handler.addOnMousePressed(EventFX.MOUSE_PRESSED( "detach fragment", new ConfirmSelection(vp), - e -> e.isSecondaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT, KeyCode.CONTROL))); + e -> paintera.allowedActionsProperty().get().isAllowed(LabelAction.Split) && e.isSecondaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT, KeyCode.CONTROL))); return handler; } From 0519dcf7db7e08c1e25710da5c4f1b940f77de34 Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Fri, 17 May 2019 19:02:01 -0400 Subject: [PATCH 11/84] check if paint actions are allowed --- .../control/ShapeInterpolationMode.java | 2 +- .../state/LabelSourceStatePaintHandler.java | 40 ++++++++++--------- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java index 2aa5a53b0..dc3673b99 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java @@ -34,7 +34,7 @@ public class ShapeInterpolationMode allowedActionsInShapeInterpolationMode = new AllowedActions( NavigationAction.of(NavigationAction.Drag, NavigationAction.Zoom, NavigationAction.Scroll), LabelAction.none(), - PaintAction.of(PaintAction.Paint) + PaintAction.of(PaintAction.Paint, PaintAction.SetBrush) ); } diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStatePaintHandler.java b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStatePaintHandler.java index 31094ba57..483a60dfa 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStatePaintHandler.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStatePaintHandler.java @@ -17,6 +17,7 @@ import org.janelia.saalfeldlab.fx.event.KeyTracker; import org.janelia.saalfeldlab.paintera.PainteraBaseView; import org.janelia.saalfeldlab.paintera.control.ControlUtils; +import org.janelia.saalfeldlab.paintera.control.actions.PaintAction; import org.janelia.saalfeldlab.paintera.control.paint.Fill2DOverlay; import org.janelia.saalfeldlab.paintera.control.paint.FillOverlay; import org.janelia.saalfeldlab.paintera.control.paint.FloodFill; @@ -124,47 +125,48 @@ private EventHandler<Event> makeHandler(PainteraBaseView paintera, KeyTracker ke handler.addEventHandler(KeyEvent.KEY_PRESSED, EventFX.KEY_PRESSED( "show brush overlay", event -> {LOG.trace("Showing brush overlay!"); paint2D.showBrushOverlay();}, - event -> keyTracker.areKeysDown(KeyCode.SPACE))); + event -> paintera.allowedActionsProperty().get().isAllowed(PaintAction.Paint) && keyTracker.areKeysDown(KeyCode.SPACE))); handler.addEventHandler(KeyEvent.KEY_RELEASED, EventFX.KEY_RELEASED( "hide brush overlay", event -> {LOG.trace("Hiding brush overlay!"); paint2D.hideBrushOverlay();}, - event -> event.getCode().equals(KeyCode.SPACE) && !keyTracker.areKeysDown(KeyCode.SPACE))); + event -> paintera.allowedActionsProperty().get().isAllowed(PaintAction.Paint) && event.getCode().equals(KeyCode.SPACE) && !keyTracker.areKeysDown(KeyCode.SPACE))); handler.addOnScroll(EventFX.SCROLL( "change brush size", event -> paint2D.changeBrushRadius(event.getDeltaY()), - event -> keyTracker.areOnlyTheseKeysDown(KeyCode.SPACE))); + event -> paintera.allowedActionsProperty().get().isAllowed(PaintAction.SetBrush) && keyTracker.areOnlyTheseKeysDown(KeyCode.SPACE))); handler.addOnScroll(EventFX.SCROLL( "change brush depth", event -> paint2D.changeBrushDepth(-ControlUtils.getBiggestScroll(event)), - event -> keyTracker.areOnlyTheseKeysDown( - KeyCode.SPACE, - KeyCode.SHIFT - ) || keyTracker.areOnlyTheseKeysDown(KeyCode.F) || - keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT,KeyCode.F))); + event -> paintera.allowedActionsProperty().get().isAllowed(PaintAction.SetBrush) && + (keyTracker.areOnlyTheseKeysDown(KeyCode.SPACE, KeyCode.SHIFT) || + keyTracker.areOnlyTheseKeysDown(KeyCode.F) || + keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT,KeyCode.F)))); handler.addOnKeyPressed(EventFX.KEY_PRESSED("show fill 2D overlay", event -> { fill2DOverlay.setVisible(true); fillOverlay.setVisible(false); - }, event -> keyTracker.areOnlyTheseKeysDown(KeyCode.F))); + }, event -> paintera.allowedActionsProperty().get().isAllowed(PaintAction.Fill) && keyTracker.areOnlyTheseKeysDown(KeyCode.F))); handler.addOnKeyReleased(EventFX.KEY_RELEASED( "show fill 2D overlay", event -> fill2DOverlay.setVisible(false), - event -> event.getCode().equals(KeyCode.F) && keyTracker.noKeysActive())); + event -> paintera.allowedActionsProperty().get().isAllowed(PaintAction.Fill) && event.getCode().equals(KeyCode.F) && keyTracker.noKeysActive())); handler.addOnKeyPressed(EventFX.KEY_PRESSED("show fill overlay", event -> { fillOverlay.setVisible(true); fill2DOverlay.setVisible(false); - }, event -> keyTracker.areOnlyTheseKeysDown(KeyCode.F, KeyCode.SHIFT))); + }, event -> paintera.allowedActionsProperty().get().isAllowed(PaintAction.Fill) && keyTracker.areOnlyTheseKeysDown(KeyCode.F, KeyCode.SHIFT))); handler.addOnKeyReleased(EventFX.KEY_RELEASED( "show fill overlay", event -> fillOverlay.setVisible(false), - event -> event.getCode().equals(KeyCode.F) && keyTracker.areOnlyTheseKeysDown(KeyCode - .SHIFT) || event.isShiftDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.F))); + event -> paintera.allowedActionsProperty().get().isAllowed(PaintAction.Fill) && + ((event.getCode().equals(KeyCode.F) && keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT)) || + (event.getCode().equals(KeyCode.SHIFT) && keyTracker.areOnlyTheseKeysDown(KeyCode.F)) + ))); // paint final PaintClickOrDrag paintDrag = new PaintClickOrDrag( @@ -173,7 +175,7 @@ private EventHandler<Event> makeHandler(PainteraBaseView paintera, KeyTracker ke paintSelection, brushRadius::get, brushDepth::get, - event -> event.isPrimaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.SPACE)); + event -> paintera.allowedActionsProperty().get().isAllowed(PaintAction.Paint) && event.isPrimaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.SPACE)); handler.addEventHandler(MouseEvent.ANY, paintDrag.singleEventHandler()); // erase @@ -183,7 +185,7 @@ private EventHandler<Event> makeHandler(PainteraBaseView paintera, KeyTracker ke () -> Label.TRANSPARENT, brushRadius::get, brushDepth::get, - event -> event.isSecondaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.SPACE)); + event -> paintera.allowedActionsProperty().get().isAllowed(PaintAction.Erase) && event.isSecondaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.SPACE)); handler.addEventHandler(MouseEvent.ANY, eraseDrag.singleEventHandler()); // background @@ -193,26 +195,26 @@ private EventHandler<Event> makeHandler(PainteraBaseView paintera, KeyTracker ke () -> Label.BACKGROUND, brushRadius::get, brushDepth::get, - event -> event.isSecondaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.SPACE, KeyCode.SHIFT)); + event -> paintera.allowedActionsProperty().get().isAllowed(PaintAction.Background) && event.isSecondaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.SPACE, KeyCode.SHIFT)); handler.addEventHandler(MouseEvent.ANY, backgroundDrag.singleEventHandler()); // advanced paint stuff handler.addOnMousePressed((EventFX.MOUSE_PRESSED( "fill", event -> fill.fillAt(event.getX(), event.getY(), paintSelection::get), - event -> event.isPrimaryButtonDown() && keyTracker.areOnlyTheseKeysDown( + event -> paintera.allowedActionsProperty().get().isAllowed(PaintAction.Fill) && event.isPrimaryButtonDown() && keyTracker.areOnlyTheseKeysDown( KeyCode.SHIFT, KeyCode.F)))); handler.addOnMousePressed(EventFX.MOUSE_PRESSED( "fill 2D", event -> fill2D.fillAt(event.getX(), event.getY(), paintSelection::get), - event -> event.isPrimaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.F))); + event -> paintera.allowedActionsProperty().get().isAllowed(PaintAction.Fill) && event.isPrimaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.F))); handler.addOnMousePressed(EventFX.MOUSE_PRESSED( "restrict", event -> restrictor.restrictTo(event.getX(), event.getY()), - event -> event.isPrimaryButtonDown() && keyTracker.areOnlyTheseKeysDown( + event -> paintera.allowedActionsProperty().get().isAllowed(PaintAction.Restrict) && event.isPrimaryButtonDown() && keyTracker.areOnlyTheseKeysDown( KeyCode.SHIFT, KeyCode.R))); From fd2e9471df25cf74ae78cfc07935fc4f717226c4 Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Tue, 21 May 2019 18:13:07 -0400 Subject: [PATCH 12/84] use 2d flood-filling in shape interpolation mode instead of painting --- .../control/ShapeInterpolationMode.java | 125 +++++++++-- .../paintera/control/paint/FloodFill2D.java | 200 ++++++++++-------- .../paintera/state/LabelSourceState.java | 21 +- 3 files changed, 236 insertions(+), 110 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java index dc3673b99..771cbd056 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java @@ -1,6 +1,8 @@ package org.janelia.saalfeldlab.paintera.control; import java.lang.invoke.MethodHandles; +import java.util.function.Predicate; + import org.janelia.saalfeldlab.fx.event.DelegateEventHandlers; import org.janelia.saalfeldlab.fx.event.EventFX; import org.janelia.saalfeldlab.fx.event.KeyTracker; @@ -9,11 +11,19 @@ import org.janelia.saalfeldlab.paintera.control.actions.LabelAction; import org.janelia.saalfeldlab.paintera.control.actions.NavigationAction; import org.janelia.saalfeldlab.paintera.control.actions.PaintAction; +import org.janelia.saalfeldlab.paintera.control.paint.FloodFill2D; import org.janelia.saalfeldlab.paintera.control.selection.SelectedIds; +import org.janelia.saalfeldlab.paintera.data.mask.Mask; +import org.janelia.saalfeldlab.paintera.data.mask.MaskInfo; +import org.janelia.saalfeldlab.paintera.data.mask.MaskedSource; +import org.janelia.saalfeldlab.paintera.data.mask.exception.MaskInUse; +import org.janelia.saalfeldlab.paintera.id.IdService; +import org.janelia.saalfeldlab.paintera.state.SourceInfo; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import bdv.fx.viewer.ViewerPanelFX; +import bdv.fx.viewer.ViewerState; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.event.Event; @@ -23,8 +33,12 @@ import javafx.scene.effect.ColorAdjust; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; +import net.imglib2.realtransform.AffineTransform3D; +import net.imglib2.type.label.Label; +import net.imglib2.type.numeric.IntegerType; +import net.imglib2.type.numeric.integer.UnsignedLongType; -public class ShapeInterpolationMode +public class ShapeInterpolationMode<D extends IntegerType<D>> { private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); @@ -34,24 +48,44 @@ public class ShapeInterpolationMode allowedActionsInShapeInterpolationMode = new AllowedActions( NavigationAction.of(NavigationAction.Drag, NavigationAction.Zoom, NavigationAction.Scroll), LabelAction.none(), - PaintAction.of(PaintAction.Paint, PaintAction.SetBrush) + PaintAction.none() ); } + private static final class ForegroundCheck implements Predicate<UnsignedLongType> + { + + @Override + public boolean test(final UnsignedLongType t) + { + return t.getIntegerLong() == 1; + } + + } + + private static final ForegroundCheck FOREGROUND_CHECK = new ForegroundCheck(); + private final ObjectProperty<ViewerPanelFX> activeViewer = new SimpleObjectProperty<>(); + private final MaskedSource<D, ?> source; + private final SelectedIds selectedIds; + private final IdService idService; private AllowedActions lastAllowedActions; - public ShapeInterpolationMode(final SelectedIds selectedIds) + private Mask<UnsignedLongType> mask; + + public ShapeInterpolationMode(final MaskedSource<D, ?> source, final SelectedIds selectedIds, final IdService idService) { + this.source = source; this.selectedIds = selectedIds; + this.idService = idService; } public ObjectProperty<ViewerPanelFX> activeViewerProperty() { - return this.activeViewer; + return activeViewer; } public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final KeyTracker keyTracker) @@ -63,8 +97,8 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke "enter shape interpolation mode", e -> enterMode(paintera, (ViewerPanelFX) e.getTarget()), e -> e.getTarget() instanceof ViewerPanelFX && - this.activeViewer.get() == null && - selectedIds.isLastSelectionValid() && + !isModeOn() && + !source.isApplyingMaskProperty().get() && keyTracker.areOnlyTheseKeysDown(KeyCode.S) ) ); @@ -73,46 +107,95 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke EventFX.KEY_PRESSED( "exit shape interpolation mode", e -> exitMode(paintera), - e -> this.activeViewer.get() != null && - keyTracker.areOnlyTheseKeysDown(KeyCode.ESCAPE) + e -> isModeOn() && keyTracker.areOnlyTheseKeysDown(KeyCode.ESCAPE) ) ); + filter.addOnMousePressed(EventFX.MOUSE_PRESSED( + "select object in current section", + e -> selectObjectSection(paintera.sourceInfo(), e.getX(), e.getY()), + e -> isModeOn() && e.isPrimaryButtonDown() && keyTracker.noKeysActive()) + ); return filter; } public void enterMode(final PainteraBaseView paintera, final ViewerPanelFX viewer) { + if (isModeOn()) + { + LOG.info("Already in shape interpolation mode"); + return; + } LOG.info("Entering shape interpolation mode"); - assert this.activeViewer.get() == null; activeViewer.set(viewer); setDisableOtherViewers(true); - this.lastAllowedActions = paintera.allowedActionsProperty().get(); + lastAllowedActions = paintera.allowedActionsProperty().get(); paintera.allowedActionsProperty().set(allowedActionsInShapeInterpolationMode); - // ... + try + { + createMask(); + } + catch (final MaskInUse e) + { + e.printStackTrace(); + } } public void exitMode(final PainteraBaseView paintera) { + if (!isModeOn()) + { + LOG.info("Not in shape interpolation mode"); + return; + } LOG.info("Exiting shape interpolation mode"); - assert this.activeViewer.get() != null; setDisableOtherViewers(false); - paintera.allowedActionsProperty().set(this.lastAllowedActions); - this.lastAllowedActions = null; + paintera.allowedActionsProperty().set(lastAllowedActions); + lastAllowedActions = null; + + forgetMask(); + activeViewer.get().requestRepaint(); + + activeViewer.set(null); + } + + public boolean isModeOn() + { + return activeViewer.get() != null; + } - // ... + private void createMask() throws MaskInUse + { + final ViewerState viewerState = activeViewer.get().getState(); + final int time = viewerState.timepointProperty().get(); + final int level = 0; + final long labelId = idService.next(); + + final AffineTransform3D labelTransform = new AffineTransform3D(); + source.getSourceTransform(time, level, labelTransform); + final AffineTransform3D viewerTransform = new AffineTransform3D(); + viewerState.getViewerTransform(viewerTransform); + final AffineTransform3D labelToViewerTransform = viewerTransform.copy(); + labelToViewerTransform.concatenate(labelTransform); + + final MaskInfo<UnsignedLongType> maskInfo = new MaskInfo<>(time, level, new UnsignedLongType(labelId)); + mask = source.generateMask(maskInfo, FOREGROUND_CHECK); + } - this.activeViewer.set(null); + private void forgetMask() + { + mask = null; + source.resetMasks(); } private void setDisableOtherViewers(final boolean disable) { - final Parent parent = this.activeViewer.get().getParent(); + final Parent parent = activeViewer.get().getParent(); for (final Node child : parent.getChildrenUnmodifiable()) { - if (child instanceof ViewerPanelFX && child != this.activeViewer.get()) + if (child instanceof ViewerPanelFX && child != activeViewer.get()) { final ViewerPanelFX viewer = (ViewerPanelFX) child; viewer.setDisable(disable); @@ -130,4 +213,10 @@ private void setDisableOtherViewers(final boolean disable) } } } + + private void selectObjectSection(final SourceInfo sourceInfo, final double x, final double y) + { + FloodFill2D.fillMaskAt(x, y, activeViewer.get(), mask, source, 1.0); + activeViewer.get().requestRepaint(); + } } 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 b960512b0..bf6da8d95 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 @@ -14,6 +14,7 @@ import javafx.scene.Cursor; import javafx.scene.Scene; import net.imglib2.FinalInterval; +import net.imglib2.Interval; import net.imglib2.Point; import net.imglib2.RandomAccess; import net.imglib2.RandomAccessible; @@ -26,20 +27,18 @@ import net.imglib2.converter.Converter; import net.imglib2.converter.Converters; import net.imglib2.realtransform.AffineTransform3D; -import net.imglib2.type.label.LabelMultisetType; 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.view.MixedTransformView; import net.imglib2.view.Views; + import org.janelia.saalfeldlab.paintera.data.mask.Mask; import org.janelia.saalfeldlab.paintera.data.mask.exception.MaskInUse; import org.janelia.saalfeldlab.paintera.data.mask.MaskInfo; import org.janelia.saalfeldlab.paintera.data.mask.MaskedSource; import org.janelia.saalfeldlab.paintera.state.HasMaskForLabel; -import org.janelia.saalfeldlab.paintera.state.LabelSourceState; import org.janelia.saalfeldlab.paintera.state.SourceInfo; import org.janelia.saalfeldlab.paintera.state.SourceState; import org.slf4j.Logger; @@ -172,89 +171,9 @@ public <T extends IntegerType<T>> void fillAt(final double x, final double y, fi try { final Mask<UnsignedLongType> mask = source.generateMask(maskInfo, FOREGROUND_CHECK); - final long seedLabel = access.get().getIntegerLong(); - LOG.debug("Got seed label {}", seedLabel); - final RandomAccessibleInterval<BoolType> relevantBackground = Converters.convert( - background, - (src, tgt) -> tgt.set(src.getIntegerLong() == seedLabel), - new BoolType() - ); - final RandomAccessible<BoolType> extended = Views.extendValue(relevantBackground, new BoolType(false)); - - final int fillNormalAxisInLabelCoordinateSystem = PaintUtils.labelAxisCorrespondingToViewerAxis(labelTransform, viewerTransform, 2); - final AccessBoxRandomAccessibleOnGet<UnsignedLongType> accessTracker = new - AccessBoxRandomAccessibleOnGet<>( - Views.extendValue(mask.mask, new UnsignedLongType(1l))); - accessTracker.initAccessBox(); - - if (fillNormalAxisInLabelCoordinateSystem < 0) - - { - - FloodFillTransformedPlane.fill( - labelToViewerTransform, - (0.5 + this.fillDepth.get() - 1.0) * PaintUtils.maximumVoxelDiagonalLengthPerDimension( - labelTransform, - viewerTransform - )[2], - extended.randomAccess(), - accessTracker.randomAccess(), - new RealPoint(x, y, 0), - 1l - ); - - requestRepaint.run(); - source.applyMask( - mask, - new FinalInterval(accessTracker.getMin(), accessTracker.getMax()), - FOREGROUND_CHECK - ); - } - - else - { - LOG.debug( - "Flood filling axis aligned. Corressponding viewer axis={}", - fillNormalAxisInLabelCoordinateSystem - ); - final long slicePos = access.getLongPosition(fillNormalAxisInLabelCoordinateSystem); - final long numSlices = Math.max((long) Math.ceil(this.fillDepth.get()) - 1, 0); - final long[] seed2D = { - access.getLongPosition(fillNormalAxisInLabelCoordinateSystem == 0 ? 1 : 0), - access.getLongPosition(fillNormalAxisInLabelCoordinateSystem != 2 ? 2 : 1) - }; - accessTracker.initAccessBox(); - final long[] min = {Long.MAX_VALUE, Long.MAX_VALUE, Long.MAX_VALUE}; - final long[] max = {Long.MIN_VALUE, Long.MIN_VALUE, Long.MIN_VALUE}; - for (long i = slicePos - numSlices; i <= slicePos + numSlices; ++i) - { - final MixedTransformView<BoolType> relevantBackgroundSlice = Views.hyperSlice( - extended, - fillNormalAxisInLabelCoordinateSystem, - i - ); - final MixedTransformView<UnsignedLongType> relevantAccessTracker = Views.hyperSlice( - accessTracker, - fillNormalAxisInLabelCoordinateSystem, - i - ); - - FloodFill.fill( - relevantBackgroundSlice, - relevantAccessTracker, - new Point(seed2D), - new UnsignedLongType(1l), - new DiamondShape(1) - ); - Arrays.setAll(min, d -> Math.min(accessTracker.getMin()[d], min[d])); - Arrays.setAll(max, d -> Math.max(accessTracker.getMax()[d], max[d])); - } - - requestRepaint.run(); - source.applyMask(mask, new FinalInterval(min, max), FOREGROUND_CHECK); - - } - + final Interval affectedInterval = fillMaskAt(x, y, this.viewer, mask, source, this.fillDepth.get()); + requestRepaint.run(); + source.applyMask(mask, affectedInterval, FOREGROUND_CHECK); } catch (final MaskInUse e) { LOG.debug(e.getMessage()); @@ -263,7 +182,116 @@ public <T extends IntegerType<T>> void fillAt(final double x, final double y, fi { scene.setCursor(previousCursor); } + } + + /** + * Flood-fills the given mask starting at the specified 2D location in the viewer. + * Returns the affected interval in source coordinates. + * + * @param x + * @param y + * @param viewer + * @param mask + * @param source + * @param fillDepth + * @return affected interval + */ + public static <T extends IntegerType<T>> Interval fillMaskAt( + final double x, + final double y, + final ViewerPanelFX viewer, + final Mask<UnsignedLongType> mask, + final MaskedSource<T, ?> source, + final double fillDepth) + { + final int time = mask.info.t; + final int level = mask.info.level; + + final AffineTransform3D labelTransform = new AffineTransform3D(); + source.getSourceTransform(time, level, labelTransform); + final AffineTransform3D viewerTransform = new AffineTransform3D(); + viewer.getState().getViewerTransform(viewerTransform); + final AffineTransform3D labelToViewerTransform = viewerTransform.copy().concatenate(labelTransform); + + final RealPoint rp = setCoordinates(x, y, viewer, labelTransform); + final RandomAccessibleInterval<T> background = source.getDataSource(time, level); + final RandomAccess<T> access = background.randomAccess(); + for (int d = 0; d < access.numDimensions(); ++d) + access.setPosition(Math.round(rp.getDoublePosition(d)), d); + final long seedLabel = access.get().getIntegerLong(); + LOG.debug("Got seed label {}", seedLabel); + final RandomAccessibleInterval<BoolType> relevantBackground = Converters.convert( + background, + (src, tgt) -> tgt.set(src.getIntegerLong() == seedLabel), + new BoolType() + ); + final RandomAccessible<BoolType> extended = Views.extendValue(relevantBackground, new BoolType(false)); + + final int fillNormalAxisInLabelCoordinateSystem = PaintUtils.labelAxisCorrespondingToViewerAxis(labelTransform, viewerTransform, 2); + final AccessBoxRandomAccessibleOnGet<UnsignedLongType> accessTracker = new + AccessBoxRandomAccessibleOnGet<>( + Views.extendValue(mask.mask, new UnsignedLongType(1l))); + accessTracker.initAccessBox(); + + final Interval affectedInterval; + + if (fillNormalAxisInLabelCoordinateSystem < 0) + { + FloodFillTransformedPlane.fill( + labelToViewerTransform, + (0.5 + fillDepth - 1.0) * PaintUtils.maximumVoxelDiagonalLengthPerDimension( + labelTransform, + viewerTransform + )[2], + extended.randomAccess(), + accessTracker.randomAccess(), + new RealPoint(x, y, 0), + 1l + ); + affectedInterval = new FinalInterval(accessTracker.getMin(), accessTracker.getMax()); + } + else + { + LOG.debug( + "Flood filling axis aligned. Corressponding viewer axis={}", + fillNormalAxisInLabelCoordinateSystem + ); + final long slicePos = access.getLongPosition(fillNormalAxisInLabelCoordinateSystem); + final long numSlices = Math.max((long) Math.ceil(fillDepth) - 1, 0); + final long[] seed2D = { + access.getLongPosition(fillNormalAxisInLabelCoordinateSystem == 0 ? 1 : 0), + access.getLongPosition(fillNormalAxisInLabelCoordinateSystem != 2 ? 2 : 1) + }; + accessTracker.initAccessBox(); + final long[] min = {Long.MAX_VALUE, Long.MAX_VALUE, Long.MAX_VALUE}; + final long[] max = {Long.MIN_VALUE, Long.MIN_VALUE, Long.MIN_VALUE}; + for (long i = slicePos - numSlices; i <= slicePos + numSlices; ++i) + { + final MixedTransformView<BoolType> relevantBackgroundSlice = Views.hyperSlice( + extended, + fillNormalAxisInLabelCoordinateSystem, + i + ); + final MixedTransformView<UnsignedLongType> relevantAccessTracker = Views.hyperSlice( + accessTracker, + fillNormalAxisInLabelCoordinateSystem, + i + ); + + FloodFill.fill( + relevantBackgroundSlice, + relevantAccessTracker, + new Point(seed2D), + new UnsignedLongType(1l), + new DiamondShape(1) + ); + Arrays.setAll(min, d -> Math.min(accessTracker.getMin()[d], min[d])); + Arrays.setAll(max, d -> Math.max(accessTracker.getMax()[d], max[d])); + } + affectedInterval = new FinalInterval(min, max); + } + return affectedInterval; } private static RealPoint setCoordinates( diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java index dd283cbd8..00649d705 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java @@ -125,7 +125,7 @@ public class LabelSourceState<D extends IntegerType<D>, T> private final LabelSourceStateMergeDetachHandler mergeDetachHandler; - private final ShapeInterpolationMode shapeInterpolationMode; + private final ShapeInterpolationMode<D> shapeInterpolationMode; private final ObjectProperty<FloodFillState> floodFillState = new SimpleObjectProperty<>(); @@ -155,7 +155,10 @@ public LabelSourceState( this.paintHandler = new LabelSourceStatePaintHandler(selectedIds); this.idSelectorHandler = new LabelSourceStateIdSelectorHandler(dataSource, selectedIds, assignment, lockedSegments); this.mergeDetachHandler = new LabelSourceStateMergeDetachHandler(dataSource, selectedIds, assignment, idService); - this.shapeInterpolationMode = new ShapeInterpolationMode(selectedIds); + if (dataSource instanceof MaskedSource<?, ?>) + this.shapeInterpolationMode = new ShapeInterpolationMode<>((MaskedSource<D, ?>) dataSource, selectedIds, idService); + else + this.shapeInterpolationMode = null; this.displayStatus = createDisplayStatus(); assignment.addListener(obs -> stain()); selectedIds.addListener(obs -> stain()); @@ -166,6 +169,7 @@ public LabelBlockLookup labelBlockLookup() { return this.labelBlockLookup; } + @Override public LongFunction<Converter<D, BoolType>> maskForLabel() { return this.maskForLabel; @@ -183,16 +187,19 @@ public ManagedMeshSettings managedMeshSettings() return this.meshManager.managedMeshSettings(); } + @Override public FragmentSegmentAssignmentState assignment() { return this.assignment; } + @Override public IdService idService() { return this.idService; } + @Override public SelectedIds selectedIds() { return this.selectedIds; @@ -210,6 +217,7 @@ public void invalidateAllMeshCaches() this.meshManager.invalidateMeshCaches(); } + @Override public LockedSegmentsState lockedSegments() { return this.lockedSegments; @@ -453,7 +461,7 @@ private static <D extends RealType<D>> LongFunction<Converter<D, BoolType>> equa } @Override - public EventHandler<Event> stateSpecificGlobalEventHandler(PainteraBaseView paintera, KeyTracker keyTracker) { + public EventHandler<Event> stateSpecificGlobalEventHandler(final PainteraBaseView paintera, final KeyTracker keyTracker) { LOG.debug("Returning {}-specific global handler", getClass().getSimpleName()); final DelegateEventHandlers.AnyHandler handler = DelegateEventHandlers.handleAny(); handler.addEventHandler( @@ -470,7 +478,7 @@ public EventHandler<Event> stateSpecificGlobalEventHandler(PainteraBaseView pain // } @Override - public EventHandler<Event> stateSpecificViewerEventHandler(PainteraBaseView paintera, KeyTracker keyTracker) { + public EventHandler<Event> stateSpecificViewerEventHandler(final PainteraBaseView paintera, final KeyTracker keyTracker) { LOG.info("Returning {}-specific handler", getClass().getSimpleName()); LOG.debug("Returning {}-specific handler", getClass().getSimpleName()); final DelegateEventHandlers.ListDelegateEventHandler<Event> handler = DelegateEventHandlers.listHandler(); @@ -481,12 +489,13 @@ public EventHandler<Event> stateSpecificViewerEventHandler(PainteraBaseView pain } @Override - public EventHandler<Event> stateSpecificViewerEventFilter(PainteraBaseView paintera, KeyTracker keyTracker) { + public EventHandler<Event> stateSpecificViewerEventFilter(final PainteraBaseView paintera, final KeyTracker keyTracker) { LOG.info("Returning {}-specific filter", getClass().getSimpleName()); LOG.debug("Returning {}-specific filter", getClass().getSimpleName()); final DelegateEventHandlers.ListDelegateEventHandler<Event> filter = DelegateEventHandlers.listHandler(); filter.addHandler(paintHandler.viewerFilter(paintera, keyTracker)); - filter.addHandler(shapeInterpolationMode.modeHandler(paintera, keyTracker)); + if (shapeInterpolationMode != null) + filter.addHandler(shapeInterpolationMode.modeHandler(paintera, keyTracker)); return filter; } From cf73327d4625d5c44501bd2ce9f570d0ca1a2ecc Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Wed, 22 May 2019 10:50:30 -0400 Subject: [PATCH 13/84] activate new label id in interpolation and restore old selection on exit --- .../paintera/control/ShapeInterpolationMode.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java index 771cbd056..6154bf4d4 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java @@ -73,6 +73,8 @@ public boolean test(final UnsignedLongType t) private final IdService idService; private AllowedActions lastAllowedActions; + private long lastSelectedId; + private long[] lastActiveIds; private Mask<UnsignedLongType> mask; @@ -135,6 +137,10 @@ public void enterMode(final PainteraBaseView paintera, final ViewerPanelFX viewe try { createMask(); + lastSelectedId = selectedIds.getLastSelection(); + lastActiveIds = selectedIds.getActiveIds(); + final long newLabelId = mask.info.value.get(); + selectedIds.activate(newLabelId); } catch (final MaskInUse e) { @@ -155,6 +161,11 @@ public void exitMode(final PainteraBaseView paintera) paintera.allowedActionsProperty().set(lastAllowedActions); lastAllowedActions = null; + final long newLabelId = mask.info.value.get(); + selectedIds.activate(lastActiveIds); + selectedIds.activateAlso(lastSelectedId); + lastSelectedId = Label.INVALID; + lastActiveIds = null; forgetMask(); activeViewer.get().requestRepaint(); From 339fa615b60c36b2179ad677f2252e1aeb98d49f Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Wed, 22 May 2019 10:52:05 -0400 Subject: [PATCH 14/84] slightly increase floodfill depth in shape interpolation mode with 1.0 we can still see bleedthrough from adjacent sections in a rotated view --- .../paintera/control/ShapeInterpolationMode.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java index 6154bf4d4..cdcf84b5d 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java @@ -54,17 +54,17 @@ public class ShapeInterpolationMode<D extends IntegerType<D>> private static final class ForegroundCheck implements Predicate<UnsignedLongType> { - @Override public boolean test(final UnsignedLongType t) { return t.getIntegerLong() == 1; } - } private static final ForegroundCheck FOREGROUND_CHECK = new ForegroundCheck(); + private static final double FILL_DEPTH = 2.0; + private final ObjectProperty<ViewerPanelFX> activeViewer = new SimpleObjectProperty<>(); private final MaskedSource<D, ?> source; @@ -227,7 +227,7 @@ private void setDisableOtherViewers(final boolean disable) private void selectObjectSection(final SourceInfo sourceInfo, final double x, final double y) { - FloodFill2D.fillMaskAt(x, y, activeViewer.get(), mask, source, 1.0); + FloodFill2D.fillMaskAt(x, y, activeViewer.get(), mask, source, FILL_DEPTH); activeViewer.get().requestRepaint(); } } From f4f7a3b1a88e407db54ffef4e119fc0ce0e9bdd0 Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Wed, 22 May 2019 10:53:22 -0400 Subject: [PATCH 15/84] use predefined color to highlight selection in shape interpolation mode --- .../paintera/control/ShapeInterpolationMode.java | 15 +++++++++++++-- .../paintera/state/LabelSourceState.java | 2 +- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java index cdcf84b5d..f2f6e5092 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java @@ -19,6 +19,7 @@ import org.janelia.saalfeldlab.paintera.data.mask.exception.MaskInUse; import org.janelia.saalfeldlab.paintera.id.IdService; import org.janelia.saalfeldlab.paintera.state.SourceInfo; +import org.janelia.saalfeldlab.paintera.stream.HighlightingStreamConverter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -33,6 +34,7 @@ import javafx.scene.effect.ColorAdjust; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; +import javafx.scene.paint.Color; import net.imglib2.realtransform.AffineTransform3D; import net.imglib2.type.label.Label; import net.imglib2.type.numeric.IntegerType; @@ -65,12 +67,14 @@ public boolean test(final UnsignedLongType t) private static final double FILL_DEPTH = 2.0; + private static final Color MASK_COLOR = Color.web("00CCFF"); + private final ObjectProperty<ViewerPanelFX> activeViewer = new SimpleObjectProperty<>(); private final MaskedSource<D, ?> source; - private final SelectedIds selectedIds; private final IdService idService; + private final HighlightingStreamConverter<?> converter; private AllowedActions lastAllowedActions; private long lastSelectedId; @@ -78,11 +82,16 @@ public boolean test(final UnsignedLongType t) private Mask<UnsignedLongType> mask; - public ShapeInterpolationMode(final MaskedSource<D, ?> source, final SelectedIds selectedIds, final IdService idService) + public ShapeInterpolationMode( + final MaskedSource<D, ?> source, + final SelectedIds selectedIds, + final IdService idService, + final HighlightingStreamConverter<?> converter) { this.source = source; this.selectedIds = selectedIds; this.idService = idService; + this.converter = converter; } public ObjectProperty<ViewerPanelFX> activeViewerProperty() @@ -140,6 +149,7 @@ public void enterMode(final PainteraBaseView paintera, final ViewerPanelFX viewe lastSelectedId = selectedIds.getLastSelection(); lastActiveIds = selectedIds.getActiveIds(); final long newLabelId = mask.info.value.get(); + converter.setColor(newLabelId, MASK_COLOR); selectedIds.activate(newLabelId); } catch (final MaskInUse e) @@ -162,6 +172,7 @@ public void exitMode(final PainteraBaseView paintera) lastAllowedActions = null; final long newLabelId = mask.info.value.get(); + converter.removeColor(newLabelId); selectedIds.activate(lastActiveIds); selectedIds.activateAlso(lastSelectedId); lastSelectedId = Label.INVALID; diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java index 00649d705..97ecdd837 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java @@ -156,7 +156,7 @@ public LabelSourceState( this.idSelectorHandler = new LabelSourceStateIdSelectorHandler(dataSource, selectedIds, assignment, lockedSegments); this.mergeDetachHandler = new LabelSourceStateMergeDetachHandler(dataSource, selectedIds, assignment, idService); if (dataSource instanceof MaskedSource<?, ?>) - this.shapeInterpolationMode = new ShapeInterpolationMode<>((MaskedSource<D, ?>) dataSource, selectedIds, idService); + this.shapeInterpolationMode = new ShapeInterpolationMode<>((MaskedSource<D, ?>) dataSource, selectedIds, idService, converter); else this.shapeInterpolationMode = null; this.displayStatus = createDisplayStatus(); From 08c013e9ac12c803a882f4bc5b0a0d0af4d51c09 Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Wed, 22 May 2019 10:54:07 -0400 Subject: [PATCH 16/84] remove duplicate method for generating and caching colors from stream --- ...nAngleSaturatedHighlightingARGBStream.java | 49 ------------------- 1 file changed, 49 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/stream/ModalGoldenAngleSaturatedHighlightingARGBStream.java b/src/main/java/org/janelia/saalfeldlab/paintera/stream/ModalGoldenAngleSaturatedHighlightingARGBStream.java index 8c66c1cb0..a978c212a 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/stream/ModalGoldenAngleSaturatedHighlightingARGBStream.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/stream/ModalGoldenAngleSaturatedHighlightingARGBStream.java @@ -13,7 +13,6 @@ */ package org.janelia.saalfeldlab.paintera.stream; -import net.imglib2.type.label.Label; import org.janelia.saalfeldlab.paintera.control.assignment.FragmentSegmentAssignmentOnlyLocal; import org.janelia.saalfeldlab.paintera.control.assignment.FragmentSegmentAssignmentState; import org.janelia.saalfeldlab.paintera.control.lock.LockedSegments; @@ -55,52 +54,4 @@ public ModalGoldenAngleSaturatedHighlightingARGBStream( super(highlights, assignment, lockedSegments); seed = 1; } - - @Override - protected int argbImpl(final long fragmentId, final boolean colorFromSegmentId) - { - - final long assigned = colorFromSegmentId ? assignment.getSegment(fragmentId) : fragmentId; - - if (!argbCache.contains(assigned)) - { - double x = getDouble(seed + assigned); - x *= 6.0; - final int k = (int) x; - final int l = k + 1; - final double u = x - k; - final double v = 1.0 - u; - - final int r = interpolate(rs, k, l, u, v); - final int g = interpolate(gs, k, l, u, v); - final int b = interpolate(bs, k, l, u, v); - - final int argb = argb(r, g, b, alpha); - - synchronized (argbCache) - { - argbCache.put(assigned, argb); - } - } - - int argb = argbCache.get(assigned); - - if (Label.INVALID == fragmentId) - { - argb = argb & 0x00ffffff | invalidSegmentAlpha; - } - else if (isLockedSegment(fragmentId) && hideLockedSegments) - { - argb = argb & 0x00ffffff; - } - else - { - final boolean isActiveSegment = isActiveSegment(fragmentId); - argb = argb & 0x00ffffff | (isActiveSegment ? isActiveFragment(fragmentId) - ? activeFragmentAlpha - : activeSegmentAlpha : alpha); - } - - return argb; - } } From 6f8d1c50878b3dfedede277716d9a9481dc5d7cf Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Wed, 22 May 2019 13:53:27 -0400 Subject: [PATCH 17/84] block scrolling when selecting objects in section until selection is fixed --- .../control/ShapeInterpolationMode.java | 33 +++++++++++++++++-- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java index f2f6e5092..f7c195794 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java @@ -18,7 +18,6 @@ import org.janelia.saalfeldlab.paintera.data.mask.MaskedSource; import org.janelia.saalfeldlab.paintera.data.mask.exception.MaskInUse; import org.janelia.saalfeldlab.paintera.id.IdService; -import org.janelia.saalfeldlab.paintera.state.SourceInfo; import org.janelia.saalfeldlab.paintera.stream.HighlightingStreamConverter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -45,6 +44,7 @@ public class ShapeInterpolationMode<D extends IntegerType<D>> private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); private static final AllowedActions allowedActionsInShapeInterpolationMode; + private static final AllowedActions allowedActionsInShapeInterpolationModeWhenSelected; static { allowedActionsInShapeInterpolationMode = new AllowedActions( @@ -52,6 +52,11 @@ public class ShapeInterpolationMode<D extends IntegerType<D>> LabelAction.none(), PaintAction.none() ); + allowedActionsInShapeInterpolationModeWhenSelected = new AllowedActions( + NavigationAction.of(NavigationAction.Drag, NavigationAction.Zoom), + LabelAction.none(), + PaintAction.none() + ); } private static final class ForegroundCheck implements Predicate<UnsignedLongType> @@ -81,6 +86,7 @@ public boolean test(final UnsignedLongType t) private long[] lastActiveIds; private Mask<UnsignedLongType> mask; + private boolean hasActiveSelection; public ShapeInterpolationMode( final MaskedSource<D, ?> source, @@ -113,6 +119,16 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke keyTracker.areOnlyTheseKeysDown(KeyCode.S) ) ); + filter.addEventHandler( + KeyEvent.KEY_PRESSED, + EventFX.KEY_PRESSED( + "fix selection", + e -> fixSelection(paintera), + e -> isModeOn() && + hasActiveSelection && + keyTracker.areOnlyTheseKeysDown(KeyCode.S) + ) + ); filter.addEventHandler( KeyEvent.KEY_PRESSED, EventFX.KEY_PRESSED( @@ -123,7 +139,7 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke ); filter.addOnMousePressed(EventFX.MOUSE_PRESSED( "select object in current section", - e -> selectObjectSection(paintera.sourceInfo(), e.getX(), e.getY()), + e -> selectObject(paintera, e.getX(), e.getY()), e -> isModeOn() && e.isPrimaryButtonDown() && keyTracker.noKeysActive()) ); return filter; @@ -151,6 +167,7 @@ public void enterMode(final PainteraBaseView paintera, final ViewerPanelFX viewe final long newLabelId = mask.info.value.get(); converter.setColor(newLabelId, MASK_COLOR); selectedIds.activate(newLabelId); + hasActiveSelection = false; } catch (final MaskInUse e) { @@ -177,6 +194,7 @@ public void exitMode(final PainteraBaseView paintera) selectedIds.activateAlso(lastSelectedId); lastSelectedId = Label.INVALID; lastActiveIds = null; + hasActiveSelection = false; forgetMask(); activeViewer.get().requestRepaint(); @@ -194,6 +212,7 @@ private void createMask() throws MaskInUse final int time = viewerState.timepointProperty().get(); final int level = 0; final long labelId = idService.next(); + LOG.info("Created new label ID for shape interpolation: {}", labelId); final AffineTransform3D labelTransform = new AffineTransform3D(); source.getSourceTransform(time, level, labelTransform); @@ -236,9 +255,17 @@ private void setDisableOtherViewers(final boolean disable) } } - private void selectObjectSection(final SourceInfo sourceInfo, final double x, final double y) + private void fixSelection(final PainteraBaseView paintera) + { + hasActiveSelection = false; + paintera.allowedActionsProperty().set(allowedActionsInShapeInterpolationMode); + } + + private void selectObject(final PainteraBaseView paintera, final double x, final double y) { FloodFill2D.fillMaskAt(x, y, activeViewer.get(), mask, source, FILL_DEPTH); activeViewer.get().requestRepaint(); + hasActiveSelection = true; + paintera.allowedActionsProperty().set(allowedActionsInShapeInterpolationModeWhenSelected); } } From 5be37721b9c2d7c47ba458055b9deebe245e4bb7 Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Wed, 22 May 2019 13:54:21 -0400 Subject: [PATCH 18/84] allow 2d flood-filling with provided label id --- .../java/bdv/fx/viewer/ViewerPanelFX.java | 38 ++++++----- .../control/ShapeInterpolationMode.java | 35 ++++++---- .../paintera/control/paint/FloodFill2D.java | 66 ++++--------------- 3 files changed, 57 insertions(+), 82 deletions(-) diff --git a/src/main/java/bdv/fx/viewer/ViewerPanelFX.java b/src/main/java/bdv/fx/viewer/ViewerPanelFX.java index d87f39aae..a461e0417 100644 --- a/src/main/java/bdv/fx/viewer/ViewerPanelFX.java +++ b/src/main/java/bdv/fx/viewer/ViewerPanelFX.java @@ -36,43 +36,26 @@ import bdv.viewer.Source; import bdv.viewer.SourceAndConverter; import bdv.viewer.ViewerOptions; -import gnu.trove.list.array.TIntArrayList; -import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyBooleanProperty; import javafx.beans.property.ReadOnlyDoubleProperty; -import javafx.beans.property.ReadOnlyObjectProperty; -import javafx.beans.property.SimpleObjectProperty; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.scene.Node; -import javafx.scene.canvas.Canvas; import javafx.scene.layout.StackPane; import net.imglib2.Interval; -import net.imglib2.Point; import net.imglib2.Positionable; import net.imglib2.RealInterval; import net.imglib2.RealLocalizable; import net.imglib2.RealPoint; import net.imglib2.RealPositionable; -import net.imglib2.algorithm.fill.FloodFill; -import net.imglib2.algorithm.neighborhood.DiamondShape; -import net.imglib2.img.array.ArrayImg; -import net.imglib2.img.array.ArrayImgs; -import net.imglib2.img.basictypeaccess.array.IntArray; -import net.imglib2.img.cell.CellGrid; import net.imglib2.realtransform.AffineTransform3D; -import net.imglib2.type.numeric.integer.IntType; import net.imglib2.ui.TransformListener; -import net.imglib2.util.Intervals; -import net.imglib2.view.Views; - import org.janelia.saalfeldlab.paintera.data.axisorder.AxisOrder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.lang.invoke.MethodHandles; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; @@ -287,6 +270,25 @@ public void displayToGlobalCoordinates(final double x, final double y, final Rea viewerTransform.applyInverse(gPos, lPos); } + /** + * Set {@code pos} to the display coordinates (x,y,0)<sup>T</sup> transformed into the source coordinate system. + * + * @param pos + * is set to the source coordinates at display (x,y,0)<sup>T</sup>. + */ + public <P extends RealLocalizable & RealPositionable> void displayToSourceCoordinates( + final double x, + final double y, + final AffineTransform3D sourceTransform, + final P pos) + { + pos.setPosition(x, 0); + pos.setPosition(y, 1); + pos.setPosition(0, 2); + displayToGlobalCoordinates(pos); + sourceTransform.applyInverse(pos, pos); + } + /** * Set {@code gPos} to the current mouse coordinates transformed into the global coordinate system. * @@ -499,7 +501,7 @@ public ReadOnlyDoubleProperty mouseYProperty() * 1. {@code 0 < sceenScales[i] <= 1} for all {@code i} * 2. {@code screenScales[i] < screenScales[i - 1]} for all {@code i > 0} */ - public void setScreenScales(double[] screenScales) + public void setScreenScales(final double[] screenScales) { LOG.debug("Setting screen scales to {}", screenScales); this.renderUnit.setScreenScales(screenScales.clone()); diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java index f7c195794..98a061fe8 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java @@ -34,6 +34,8 @@ import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; import javafx.scene.paint.Color; +import net.imglib2.RandomAccess; +import net.imglib2.RealPoint; import net.imglib2.realtransform.AffineTransform3D; import net.imglib2.type.label.Label; import net.imglib2.type.numeric.IntegerType; @@ -59,21 +61,12 @@ public class ShapeInterpolationMode<D extends IntegerType<D>> ); } - private static final class ForegroundCheck implements Predicate<UnsignedLongType> - { - @Override - public boolean test(final UnsignedLongType t) - { - return t.getIntegerLong() == 1; - } - } - - private static final ForegroundCheck FOREGROUND_CHECK = new ForegroundCheck(); - private static final double FILL_DEPTH = 2.0; private static final Color MASK_COLOR = Color.web("00CCFF"); + private static final Predicate<UnsignedLongType> FOREGROUND_CHECK = t -> t.get() > 0; + private final ObjectProperty<ViewerPanelFX> activeViewer = new SimpleObjectProperty<>(); private final MaskedSource<D, ?> source; @@ -86,6 +79,7 @@ public boolean test(final UnsignedLongType t) private long[] lastActiveIds; private Mask<UnsignedLongType> mask; + private int currentFillValue; private boolean hasActiveSelection; public ShapeInterpolationMode( @@ -168,6 +162,7 @@ public void enterMode(final PainteraBaseView paintera, final ViewerPanelFX viewe converter.setColor(newLabelId, MASK_COLOR); selectedIds.activate(newLabelId); hasActiveSelection = false; + currentFillValue = 0; } catch (final MaskInUse e) { @@ -195,6 +190,7 @@ public void exitMode(final PainteraBaseView paintera) lastSelectedId = Label.INVALID; lastActiveIds = null; hasActiveSelection = false; + currentFillValue = 0; forgetMask(); activeViewer.get().requestRepaint(); @@ -263,9 +259,24 @@ private void fixSelection(final PainteraBaseView paintera) private void selectObject(final PainteraBaseView paintera, final double x, final double y) { - FloodFill2D.fillMaskAt(x, y, activeViewer.get(), mask, source, FILL_DEPTH); + final long fill = isSelected(x, y) ? Label.BACKGROUND : ++currentFillValue; + FloodFill2D.fillMaskAt(x, y, activeViewer.get(), mask, source, fill, FILL_DEPTH); activeViewer.get().requestRepaint(); hasActiveSelection = true; paintera.allowedActionsProperty().set(allowedActionsInShapeInterpolationModeWhenSelected); } + + private boolean isSelected(final double x, final double y) + { + final AffineTransform3D labelTransform = new AffineTransform3D(); + source.getSourceTransform(mask.info.t, mask.info.level, labelTransform); + final RealPoint pos = new RealPoint(labelTransform.numDimensions()); + activeViewer.get().displayToSourceCoordinates(x, y, labelTransform, pos); + + final RandomAccess<UnsignedLongType> maskAccess = mask.mask.randomAccess(); + for (int d = 0; d < pos.numDimensions(); ++d) + maskAccess.setPosition(Math.round(pos.getDoublePosition(d)), d); + + return FOREGROUND_CHECK.test(maskAccess.get()); + } } 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 bf6da8d95..c2fdde69f 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 @@ -19,9 +19,7 @@ import net.imglib2.RandomAccess; import net.imglib2.RandomAccessible; import net.imglib2.RandomAccessibleInterval; -import net.imglib2.RealLocalizable; import net.imglib2.RealPoint; -import net.imglib2.RealPositionable; import net.imglib2.algorithm.fill.FloodFill; import net.imglib2.algorithm.neighborhood.DiamondShape; import net.imglib2.converter.Converter; @@ -59,15 +57,15 @@ public class FloodFill2D private final SimpleDoubleProperty fillDepth = new SimpleDoubleProperty(1.0); + private static final long FILL_VALUE = 1l; + private static final class ForegroundCheck implements Predicate<UnsignedLongType> { - @Override public boolean test(final UnsignedLongType t) { - return t.getIntegerLong() == 1; + return t.getIntegerLong() == FILL_VALUE; } - } private static final ForegroundCheck FOREGROUND_CHECK = new ForegroundCheck(); @@ -148,21 +146,8 @@ public <T extends IntegerType<T>> void fillAt(final double x, final double y, fi return; } - final int level = 0; - final AffineTransform3D labelTransform = new AffineTransform3D(); - final int time = viewerState.timepointProperty().get(); - source.getSourceTransform(time, level, labelTransform); - final AffineTransform3D viewerTransform = this.viewerTransform.copy(); - final AffineTransform3D labelToViewerTransform = this.viewerTransform.copy().concatenate(labelTransform); - - final RealPoint rp = setCoordinates(x, y, viewer, labelTransform); - final RandomAccessibleInterval<T> background = source.getDataSource(time, level); - final RandomAccess<T> access = background.randomAccess(); - for (int d = 0; d < access.numDimensions(); ++d) - { - access.setPosition(Math.round(rp.getDoublePosition(d)), d); - } - + final int level = 0; + final int time = viewerState.timepointProperty().get(); final MaskInfo<UnsignedLongType> maskInfo = new MaskInfo<>(time, level, new UnsignedLongType(fill)); final Scene scene = viewer.getScene(); @@ -171,7 +156,7 @@ public <T extends IntegerType<T>> void fillAt(final double x, final double y, fi try { final Mask<UnsignedLongType> mask = source.generateMask(maskInfo, FOREGROUND_CHECK); - final Interval affectedInterval = fillMaskAt(x, y, this.viewer, mask, source, this.fillDepth.get()); + final Interval affectedInterval = fillMaskAt(x, y, this.viewer, mask, source, FILL_VALUE, this.fillDepth.get()); requestRepaint.run(); source.applyMask(mask, affectedInterval, FOREGROUND_CHECK); } catch (final MaskInUse e) @@ -193,6 +178,7 @@ public <T extends IntegerType<T>> void fillAt(final double x, final double y, fi * @param viewer * @param mask * @param source + * @param fillValue * @param fillDepth * @return affected interval */ @@ -202,6 +188,7 @@ public static <T extends IntegerType<T>> Interval fillMaskAt( final ViewerPanelFX viewer, final Mask<UnsignedLongType> mask, final MaskedSource<T, ?> source, + final long fillValue, final double fillDepth) { final int time = mask.info.t; @@ -213,11 +200,12 @@ public static <T extends IntegerType<T>> Interval fillMaskAt( viewer.getState().getViewerTransform(viewerTransform); final AffineTransform3D labelToViewerTransform = viewerTransform.copy().concatenate(labelTransform); - final RealPoint rp = setCoordinates(x, y, viewer, labelTransform); final RandomAccessibleInterval<T> background = source.getDataSource(time, level); final RandomAccess<T> access = background.randomAccess(); + final RealPoint pos = new RealPoint(access.numDimensions()); + viewer.displayToSourceCoordinates(x, y, labelTransform, pos); for (int d = 0; d < access.numDimensions(); ++d) - access.setPosition(Math.round(rp.getDoublePosition(d)), d); + access.setPosition(Math.round(pos.getDoublePosition(d)), d); final long seedLabel = access.get().getIntegerLong(); LOG.debug("Got seed label {}", seedLabel); final RandomAccessibleInterval<BoolType> relevantBackground = Converters.convert( @@ -230,7 +218,7 @@ public static <T extends IntegerType<T>> Interval fillMaskAt( final int fillNormalAxisInLabelCoordinateSystem = PaintUtils.labelAxisCorrespondingToViewerAxis(labelTransform, viewerTransform, 2); final AccessBoxRandomAccessibleOnGet<UnsignedLongType> accessTracker = new AccessBoxRandomAccessibleOnGet<>( - Views.extendValue(mask.mask, new UnsignedLongType(1l))); + Views.extendValue(mask.mask, new UnsignedLongType(fillValue))); accessTracker.initAccessBox(); final Interval affectedInterval; @@ -246,7 +234,7 @@ public static <T extends IntegerType<T>> Interval fillMaskAt( extended.randomAccess(), accessTracker.randomAccess(), new RealPoint(x, y, 0), - 1l + fillValue ); affectedInterval = new FinalInterval(accessTracker.getMin(), accessTracker.getMax()); } @@ -282,7 +270,7 @@ public static <T extends IntegerType<T>> Interval fillMaskAt( relevantBackgroundSlice, relevantAccessTracker, new Point(seed2D), - new UnsignedLongType(1l), + new UnsignedLongType(fillValue), new DiamondShape(1) ); Arrays.setAll(min, d -> Math.min(accessTracker.getMin()[d], min[d])); @@ -294,32 +282,6 @@ public static <T extends IntegerType<T>> Interval fillMaskAt( return affectedInterval; } - private static RealPoint setCoordinates( - final double x, - final double y, - final ViewerPanelFX viewer, - final AffineTransform3D labelTransform) - { - return setCoordinates(x, y, new RealPoint(labelTransform.numDimensions()), viewer, labelTransform); - } - - private static <P extends RealLocalizable & RealPositionable> P setCoordinates( - final double x, - final double y, - final P location, - final ViewerPanelFX viewer, - final AffineTransform3D labelTransform) - { - location.setPosition(x, 0); - location.setPosition(y, 1); - location.setPosition(0, 2); - - viewer.displayToGlobalCoordinates(location); - labelTransform.applyInverse(location, location); - - return location; - } - public DoubleProperty fillDepthProperty() { return this.fillDepth; From e18af82b1e0d77dfdd23c6ab2a56f1a46a86d5fd Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Wed, 22 May 2019 14:40:34 -0400 Subject: [PATCH 19/84] allow specifying custom predicate for 2d flood-filling --- .../paintera/control/paint/FloodFill2D.java | 58 +++++++++++++++---- 1 file changed, 46 insertions(+), 12 deletions(-) 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 c2fdde69f..b40673065 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 @@ -95,6 +95,7 @@ public void fillAt(final double x, final double y, final Supplier<Long> fillSupp fillAt(x, y, fill); } + @SuppressWarnings("unchecked") public <T extends IntegerType<T>> void fillAt(final double x, final double y, final long fill) { final Source<?> currentSource = sourceInfo.currentSourceProperty().get(); @@ -105,7 +106,7 @@ public <T extends IntegerType<T>> void fillAt(final double x, final double y, fi return; } - @SuppressWarnings("unchecked") final SourceState<T, ?> currentSourceState = (SourceState<T, ?>) sourceInfo + final SourceState<T, ?> currentSourceState = (SourceState<T, ?>) sourceInfo .getState( currentSource); @@ -136,7 +137,7 @@ public <T extends IntegerType<T>> void fillAt(final double x, final double y, fi return; } - @SuppressWarnings("unchecked") final MaskedSource<T, ?> source = (MaskedSource<T, ?>) currentSource; + final MaskedSource<T, ?> source = (MaskedSource<T, ?>) currentSource; final T t = source.getDataType(); @@ -196,9 +197,6 @@ public static <T extends IntegerType<T>> Interval fillMaskAt( final AffineTransform3D labelTransform = new AffineTransform3D(); source.getSourceTransform(time, level, labelTransform); - final AffineTransform3D viewerTransform = new AffineTransform3D(); - viewer.getState().getViewerTransform(viewerTransform); - final AffineTransform3D labelToViewerTransform = viewerTransform.copy().concatenate(labelTransform); final RandomAccessibleInterval<T> background = source.getDataSource(time, level); final RandomAccess<T> access = background.randomAccess(); @@ -212,8 +210,44 @@ public static <T extends IntegerType<T>> Interval fillMaskAt( background, (src, tgt) -> tgt.set(src.getIntegerLong() == seedLabel), new BoolType() - ); - final RandomAccessible<BoolType> extended = Views.extendValue(relevantBackground, new BoolType(false)); + ); + + return fillMaskAt(x, y, viewer, mask, relevantBackground, labelTransform, fillValue, fillDepth); + } + + /** + * Flood-fills the given mask starting at the specified 2D location in the viewer + * based on the given boolean filter. + * Returns the affected interval in source coordinates. + * + * @param x + * @param y + * @param viewer + * @param mask + * @param filter + * @param labelTransform + * @param fillValue + * @param fillDepth + * @return affected interval + */ + public static <T extends IntegerType<T>> Interval fillMaskAt( + final double x, + final double y, + final ViewerPanelFX viewer, + final Mask<UnsignedLongType> mask, + final RandomAccessibleInterval<BoolType> filter, + final AffineTransform3D labelTransform, + final long fillValue, + final double fillDepth) + { + final AffineTransform3D viewerTransform = new AffineTransform3D(); + viewer.getState().getViewerTransform(viewerTransform); + final AffineTransform3D labelToViewerTransform = viewerTransform.copy().concatenate(labelTransform); + + final RealPoint pos = new RealPoint(labelTransform.numDimensions()); + viewer.displayToSourceCoordinates(x, y, labelTransform, pos); + + final RandomAccessible<BoolType> extendedFilter = Views.extendValue(filter, new BoolType(false)); final int fillNormalAxisInLabelCoordinateSystem = PaintUtils.labelAxisCorrespondingToViewerAxis(labelTransform, viewerTransform, 2); final AccessBoxRandomAccessibleOnGet<UnsignedLongType> accessTracker = new @@ -231,7 +265,7 @@ public static <T extends IntegerType<T>> Interval fillMaskAt( labelTransform, viewerTransform )[2], - extended.randomAccess(), + extendedFilter.randomAccess(), accessTracker.randomAccess(), new RealPoint(x, y, 0), fillValue @@ -244,11 +278,11 @@ public static <T extends IntegerType<T>> Interval fillMaskAt( "Flood filling axis aligned. Corressponding viewer axis={}", fillNormalAxisInLabelCoordinateSystem ); - final long slicePos = access.getLongPosition(fillNormalAxisInLabelCoordinateSystem); + final long slicePos = Math.round(pos.getDoublePosition(fillNormalAxisInLabelCoordinateSystem)); final long numSlices = Math.max((long) Math.ceil(fillDepth) - 1, 0); final long[] seed2D = { - access.getLongPosition(fillNormalAxisInLabelCoordinateSystem == 0 ? 1 : 0), - access.getLongPosition(fillNormalAxisInLabelCoordinateSystem != 2 ? 2 : 1) + Math.round(pos.getDoublePosition(fillNormalAxisInLabelCoordinateSystem == 0 ? 1 : 0)), + Math.round(pos.getDoublePosition(fillNormalAxisInLabelCoordinateSystem != 2 ? 2 : 1)) }; accessTracker.initAccessBox(); final long[] min = {Long.MAX_VALUE, Long.MAX_VALUE, Long.MAX_VALUE}; @@ -256,7 +290,7 @@ public static <T extends IntegerType<T>> Interval fillMaskAt( for (long i = slicePos - numSlices; i <= slicePos + numSlices; ++i) { final MixedTransformView<BoolType> relevantBackgroundSlice = Views.hyperSlice( - extended, + extendedFilter, fillNormalAxisInLabelCoordinateSystem, i ); From d8eee22b6c1d715cee2ab632fc632ad4e84222a8 Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Wed, 22 May 2019 14:42:02 -0400 Subject: [PATCH 20/84] fix de-selecting objects in shape interpolation mode Create a custom predicate for flood-filling with the background label to only accept the current fill value. Without it the flood-filling would also deselect adjacent objects if they were active. --- .../control/ShapeInterpolationMode.java | 66 +++++++++++++------ 1 file changed, 45 insertions(+), 21 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java index 98a061fe8..65d49f06c 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java @@ -23,7 +23,6 @@ import org.slf4j.LoggerFactory; import bdv.fx.viewer.ViewerPanelFX; -import bdv.fx.viewer.ViewerState; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.event.Event; @@ -35,9 +34,12 @@ import javafx.scene.input.KeyEvent; import javafx.scene.paint.Color; import net.imglib2.RandomAccess; +import net.imglib2.RandomAccessibleInterval; import net.imglib2.RealPoint; +import net.imglib2.converter.Converters; import net.imglib2.realtransform.AffineTransform3D; import net.imglib2.type.label.Label; +import net.imglib2.type.logic.BoolType; import net.imglib2.type.numeric.IntegerType; import net.imglib2.type.numeric.integer.UnsignedLongType; @@ -136,6 +138,11 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke e -> selectObject(paintera, e.getX(), e.getY()), e -> isModeOn() && e.isPrimaryButtonDown() && keyTracker.noKeysActive()) ); + filter.addOnMousePressed(EventFX.MOUSE_PRESSED( + "toggle object in current section", + e -> selectObject(paintera, e.getX(), e.getY()), + e -> isModeOn() && e.isSecondaryButtonDown() && keyTracker.noKeysActive()) + ); return filter; } @@ -204,21 +211,12 @@ public boolean isModeOn() private void createMask() throws MaskInUse { - final ViewerState viewerState = activeViewer.get().getState(); - final int time = viewerState.timepointProperty().get(); + final int time = activeViewer.get().getState().timepointProperty().get(); final int level = 0; - final long labelId = idService.next(); - LOG.info("Created new label ID for shape interpolation: {}", labelId); - - final AffineTransform3D labelTransform = new AffineTransform3D(); - source.getSourceTransform(time, level, labelTransform); - final AffineTransform3D viewerTransform = new AffineTransform3D(); - viewerState.getViewerTransform(viewerTransform); - final AffineTransform3D labelToViewerTransform = viewerTransform.copy(); - labelToViewerTransform.concatenate(labelTransform); - - final MaskInfo<UnsignedLongType> maskInfo = new MaskInfo<>(time, level, new UnsignedLongType(labelId)); + final long newLabelId = idService.next(); + final MaskInfo<UnsignedLongType> maskInfo = new MaskInfo<>(time, level, new UnsignedLongType(newLabelId)); mask = source.generateMask(maskInfo, FOREGROUND_CHECK); + LOG.info("Generated mask for shape interpolation using new label ID {}", newLabelId); } private void forgetMask() @@ -259,8 +257,23 @@ private void fixSelection(final PainteraBaseView paintera) private void selectObject(final PainteraBaseView paintera, final double x, final double y) { - final long fill = isSelected(x, y) ? Label.BACKGROUND : ++currentFillValue; - FloodFill2D.fillMaskAt(x, y, activeViewer.get(), mask, source, fill, FILL_DEPTH); + if (!isSelected(x, y)) + { + // Flood-fill using new fill value. + FloodFill2D.fillMaskAt(x, y, activeViewer.get(), mask, source, ++currentFillValue, FILL_DEPTH); + } + else + { + // Flood-fill using background value. + // The predicate is set to accept only the fill value at the clicked location to avoid deselecting adjacent objects. + final long maskValue = getMaskValue(x, y).get(); + final RandomAccessibleInterval<BoolType> predicate = Converters.convert( + mask.mask, + (in, out) -> out.set(in.getIntegerLong() == maskValue), + new BoolType() + ); + FloodFill2D.fillMaskAt(x, y, activeViewer.get(), mask, predicate, getMaskTransform(), Label.BACKGROUND, FILL_DEPTH); + } activeViewer.get().requestRepaint(); hasActiveSelection = true; paintera.allowedActionsProperty().set(allowedActionsInShapeInterpolationModeWhenSelected); @@ -268,15 +281,26 @@ private void selectObject(final PainteraBaseView paintera, final double x, final private boolean isSelected(final double x, final double y) { - final AffineTransform3D labelTransform = new AffineTransform3D(); - source.getSourceTransform(mask.info.t, mask.info.level, labelTransform); - final RealPoint pos = new RealPoint(labelTransform.numDimensions()); - activeViewer.get().displayToSourceCoordinates(x, y, labelTransform, pos); + return FOREGROUND_CHECK.test(getMaskValue(x, y)); + } + + private UnsignedLongType getMaskValue(final double x, final double y) + { + final AffineTransform3D maskTransform = getMaskTransform(); + final RealPoint pos = new RealPoint(maskTransform.numDimensions()); + activeViewer.get().displayToSourceCoordinates(x, y, maskTransform, pos); final RandomAccess<UnsignedLongType> maskAccess = mask.mask.randomAccess(); for (int d = 0; d < pos.numDimensions(); ++d) maskAccess.setPosition(Math.round(pos.getDoublePosition(d)), d); - return FOREGROUND_CHECK.test(maskAccess.get()); + return maskAccess.get(); + } + + private AffineTransform3D getMaskTransform() + { + final AffineTransform3D labelTransform = new AffineTransform3D(); + source.getSourceTransform(mask.info.t, mask.info.level, labelTransform); + return labelTransform; } } From 9ff5b4d7ce1ed74e1e861e67c1b2ddb5a6c140e4 Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Wed, 22 May 2019 16:02:27 -0400 Subject: [PATCH 21/84] fix click vs. drag mouse event detection in shape interpolation mode --- .../paintera/control/ShapeInterpolationMode.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java index 65d49f06c..67397e9a5 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java @@ -6,6 +6,7 @@ import org.janelia.saalfeldlab.fx.event.DelegateEventHandlers; import org.janelia.saalfeldlab.fx.event.EventFX; import org.janelia.saalfeldlab.fx.event.KeyTracker; +import org.janelia.saalfeldlab.fx.event.MouseClickFX; import org.janelia.saalfeldlab.paintera.PainteraBaseView; import org.janelia.saalfeldlab.paintera.control.actions.AllowedActions; import org.janelia.saalfeldlab.paintera.control.actions.LabelAction; @@ -32,6 +33,7 @@ import javafx.scene.effect.ColorAdjust; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; +import javafx.scene.input.MouseEvent; import javafx.scene.paint.Color; import net.imglib2.RandomAccess; import net.imglib2.RandomAccessibleInterval; @@ -133,16 +135,16 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke e -> isModeOn() && keyTracker.areOnlyTheseKeysDown(KeyCode.ESCAPE) ) ); - filter.addOnMousePressed(EventFX.MOUSE_PRESSED( + filter.addEventHandler(MouseEvent.ANY, new MouseClickFX( "select object in current section", e -> selectObject(paintera, e.getX(), e.getY()), e -> isModeOn() && e.isPrimaryButtonDown() && keyTracker.noKeysActive()) - ); - filter.addOnMousePressed(EventFX.MOUSE_PRESSED( + .handler()); + filter.addEventHandler(MouseEvent.ANY, new MouseClickFX( "toggle object in current section", e -> selectObject(paintera, e.getX(), e.getY()), e -> isModeOn() && e.isSecondaryButtonDown() && keyTracker.noKeysActive()) - ); + .handler()); return filter; } From f9db064d9995a5b4635ff118952d0f93e0c08c51 Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Wed, 22 May 2019 17:14:01 -0400 Subject: [PATCH 22/84] fix select/toggle/deselect objects in shape interpolation mode --- .../control/ShapeInterpolationMode.java | 139 +++++++++++++----- 1 file changed, 101 insertions(+), 38 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java index 67397e9a5..766f2c0ae 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java @@ -24,6 +24,9 @@ import org.slf4j.LoggerFactory; import bdv.fx.viewer.ViewerPanelFX; +import gnu.trove.iterator.TLongObjectIterator; +import gnu.trove.map.TLongObjectMap; +import gnu.trove.map.hash.TLongObjectHashMap; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.event.Event; @@ -44,21 +47,22 @@ import net.imglib2.type.logic.BoolType; import net.imglib2.type.numeric.IntegerType; import net.imglib2.type.numeric.integer.UnsignedLongType; +import net.imglib2.util.Util; public class ShapeInterpolationMode<D extends IntegerType<D>> { private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - private static final AllowedActions allowedActionsInShapeInterpolationMode; - private static final AllowedActions allowedActionsInShapeInterpolationModeWhenSelected; + private static final AllowedActions allowedActions; + private static final AllowedActions allowedActionsWhenSelected; static { - allowedActionsInShapeInterpolationMode = new AllowedActions( + allowedActions = new AllowedActions( NavigationAction.of(NavigationAction.Drag, NavigationAction.Zoom, NavigationAction.Scroll), LabelAction.none(), PaintAction.none() ); - allowedActionsInShapeInterpolationModeWhenSelected = new AllowedActions( + allowedActionsWhenSelected = new AllowedActions( NavigationAction.of(NavigationAction.Drag, NavigationAction.Zoom), LabelAction.none(), PaintAction.none() @@ -84,7 +88,8 @@ public class ShapeInterpolationMode<D extends IntegerType<D>> private Mask<UnsignedLongType> mask; private int currentFillValue; - private boolean hasActiveSelection; + + private final TLongObjectMap<RealPoint> selectedObjects = new TLongObjectHashMap<>(); public ShapeInterpolationMode( final MaskedSource<D, ?> source, @@ -123,7 +128,7 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke "fix selection", e -> fixSelection(paintera), e -> isModeOn() && - hasActiveSelection && + !selectedObjects.isEmpty() && keyTracker.areOnlyTheseKeysDown(KeyCode.S) ) ); @@ -137,12 +142,12 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke ); filter.addEventHandler(MouseEvent.ANY, new MouseClickFX( "select object in current section", - e -> selectObject(paintera, e.getX(), e.getY()), + e -> {e.consume(); selectObject(paintera, e.getX(), e.getY(), true);}, e -> isModeOn() && e.isPrimaryButtonDown() && keyTracker.noKeysActive()) .handler()); filter.addEventHandler(MouseEvent.ANY, new MouseClickFX( "toggle object in current section", - e -> selectObject(paintera, e.getX(), e.getY()), + e -> {e.consume(); selectObject(paintera, e.getX(), e.getY(), false);}, e -> isModeOn() && e.isSecondaryButtonDown() && keyTracker.noKeysActive()) .handler()); return filter; @@ -160,7 +165,7 @@ public void enterMode(final PainteraBaseView paintera, final ViewerPanelFX viewe setDisableOtherViewers(true); lastAllowedActions = paintera.allowedActionsProperty().get(); - paintera.allowedActionsProperty().set(allowedActionsInShapeInterpolationMode); + paintera.allowedActionsProperty().set(allowedActions); try { @@ -170,8 +175,8 @@ public void enterMode(final PainteraBaseView paintera, final ViewerPanelFX viewe final long newLabelId = mask.info.value.get(); converter.setColor(newLabelId, MASK_COLOR); selectedIds.activate(newLabelId); - hasActiveSelection = false; currentFillValue = 0; + selectedObjects.clear(); } catch (final MaskInUse e) { @@ -198,8 +203,8 @@ public void exitMode(final PainteraBaseView paintera) selectedIds.activateAlso(lastSelectedId); lastSelectedId = Label.INVALID; lastActiveIds = null; - hasActiveSelection = false; currentFillValue = 0; + selectedObjects.clear(); forgetMask(); activeViewer.get().requestRepaint(); @@ -253,32 +258,74 @@ private void setDisableOtherViewers(final boolean disable) private void fixSelection(final PainteraBaseView paintera) { - hasActiveSelection = false; - paintera.allowedActionsProperty().set(allowedActionsInShapeInterpolationMode); + selectedObjects.clear(); + paintera.allowedActionsProperty().set(allowedActions); } - private void selectObject(final PainteraBaseView paintera, final double x, final double y) + private void selectObject(final PainteraBaseView paintera, final double x, final double y, final boolean deactivateOthers) { - if (!isSelected(x, y)) + final boolean wasSelected = isSelected(x, y); + final int numSelectedObjects = selectedObjects.size(); + + LOG.debug("Object was clicked: deactivateOthers={}, wasSelected={}, numSelectedObjects", deactivateOthers, wasSelected, numSelectedObjects); + + if (deactivateOthers) + { + for (final TLongObjectIterator<RealPoint> it = selectedObjects.iterator(); it.hasNext();) + { + it.advance(); + final double[] deselectDisplayPos = getDisplayCoordinates(it.value()); + runFloodFillToDeselect(deselectDisplayPos[0], deselectDisplayPos[1]); + } + selectedObjects.clear(); + } + + if (!wasSelected || (deactivateOthers && numSelectedObjects > 1)) { - // Flood-fill using new fill value. - FloodFill2D.fillMaskAt(x, y, activeViewer.get(), mask, source, ++currentFillValue, FILL_DEPTH); + final long newFillValue = runFloodFillToSelect(x, y); + selectedObjects.put(newFillValue, getSourceCoordinates(x, y)); } else { - // Flood-fill using background value. - // The predicate is set to accept only the fill value at the clicked location to avoid deselecting adjacent objects. - final long maskValue = getMaskValue(x, y).get(); - final RandomAccessibleInterval<BoolType> predicate = Converters.convert( - mask.mask, - (in, out) -> out.set(in.getIntegerLong() == maskValue), - new BoolType() - ); - FloodFill2D.fillMaskAt(x, y, activeViewer.get(), mask, predicate, getMaskTransform(), Label.BACKGROUND, FILL_DEPTH); + final long oldFillValue = runFloodFillToDeselect(x, y); + selectedObjects.remove(oldFillValue); } + activeViewer.get().requestRepaint(); - hasActiveSelection = true; - paintera.allowedActionsProperty().set(allowedActionsInShapeInterpolationModeWhenSelected); + paintera.allowedActionsProperty().set(selectedObjects.isEmpty() ? allowedActions : allowedActionsWhenSelected); + } + + /** + * Flood-fills the mask using a new fill value to mark the object as selected. + * + * @param x + * @param y + * @return the fill value of the selected object + */ + private long runFloodFillToSelect(final double x, final double y) + { + FloodFill2D.fillMaskAt(x, y, activeViewer.get(), mask, source, ++currentFillValue, FILL_DEPTH); + return currentFillValue; + } + + /** + * Flood-fills the mask using a background value to remove the object from the selection. + * + * @param x + * @param y + * @return the fill value of the deselected object + */ + private long runFloodFillToDeselect(final double x, final double y) + { + // set the predicate to accept only the fill value at the clicked location to avoid deselecting adjacent objects. + final long maskValue = getMaskValue(x, y).get(); + final RandomAccessibleInterval<BoolType> predicate = Converters.convert( + mask.mask, + (in, out) -> out.set(in.getIntegerLong() == maskValue), + new BoolType() + ); + FloodFill2D.fillMaskAt(x, y, activeViewer.get(), mask, predicate, getMaskTransform(), Label.BACKGROUND, FILL_DEPTH); + return maskValue; } private boolean isSelected(final double x, final double y) @@ -288,21 +335,37 @@ private boolean isSelected(final double x, final double y) private UnsignedLongType getMaskValue(final double x, final double y) { - final AffineTransform3D maskTransform = getMaskTransform(); - final RealPoint pos = new RealPoint(maskTransform.numDimensions()); - activeViewer.get().displayToSourceCoordinates(x, y, maskTransform, pos); - + final RealPoint sourcePos = getSourceCoordinates(x, y); final RandomAccess<UnsignedLongType> maskAccess = mask.mask.randomAccess(); - for (int d = 0; d < pos.numDimensions(); ++d) - maskAccess.setPosition(Math.round(pos.getDoublePosition(d)), d); - + for (int d = 0; d < sourcePos.numDimensions(); ++d) + maskAccess.setPosition(Math.round(sourcePos.getDoublePosition(d)), d); return maskAccess.get(); } private AffineTransform3D getMaskTransform() { - final AffineTransform3D labelTransform = new AffineTransform3D(); - source.getSourceTransform(mask.info.t, mask.info.level, labelTransform); - return labelTransform; + final AffineTransform3D maskTransform = new AffineTransform3D(); + source.getSourceTransform(mask.info.t, mask.info.level, maskTransform); + return maskTransform; + } + + private RealPoint getSourceCoordinates(final double x, final double y) + { + final AffineTransform3D maskTransform = getMaskTransform(); + final RealPoint sourcePos = new RealPoint(maskTransform.numDimensions()); + activeViewer.get().displayToSourceCoordinates(x, y, maskTransform, sourcePos); + return sourcePos; + } + + private double[] getDisplayCoordinates(final RealPoint sourcePos) + { + final AffineTransform3D maskTransform = getMaskTransform(); + final RealPoint displayPos = new RealPoint(maskTransform.numDimensions()); + final AffineTransform3D viewerTransform = new AffineTransform3D(); + activeViewer.get().getState().getViewerTransform(viewerTransform); + final AffineTransform3D maskToViewerTransform = viewerTransform.copy().concatenate(maskTransform); + maskToViewerTransform.apply(sourcePos, displayPos); + assert Util.isApproxEqual(displayPos.getDoublePosition(2), 0.0, 1e-10); + return new double[] {displayPos.getDoublePosition(0), displayPos.getDoublePosition(1)}; } } From 118d3f2407c8046070a6f82713fb0fba8c1fe50a Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Thu, 23 May 2019 11:11:46 -0400 Subject: [PATCH 23/84] allow ctrl+left click to toggle objects in shape interpolation mode --- .../saalfeldlab/paintera/control/ShapeInterpolationMode.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java index 766f2c0ae..47f68c104 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java @@ -148,7 +148,9 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke filter.addEventHandler(MouseEvent.ANY, new MouseClickFX( "toggle object in current section", e -> {e.consume(); selectObject(paintera, e.getX(), e.getY(), false);}, - e -> isModeOn() && e.isSecondaryButtonDown() && keyTracker.noKeysActive()) + e -> isModeOn() && + ((e.isSecondaryButtonDown() && keyTracker.noKeysActive()) || + (e.isPrimaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL)))) .handler()); return filter; } From d652a3773b7308e45e0e1e17599661402aec3b6c Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Fri, 24 May 2019 09:52:19 -0400 Subject: [PATCH 24/84] fix section by copying object selection from the mask in the current view --- .../control/ShapeInterpolationMode.java | 116 ++++++++++++++++-- 1 file changed, 105 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java index 47f68c104..300744fc9 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java @@ -1,6 +1,7 @@ package org.janelia.saalfeldlab.paintera.control; import java.lang.invoke.MethodHandles; +import java.util.Arrays; import java.util.function.Predicate; import org.janelia.saalfeldlab.fx.event.DelegateEventHandlers; @@ -14,6 +15,7 @@ import org.janelia.saalfeldlab.paintera.control.actions.PaintAction; import org.janelia.saalfeldlab.paintera.control.paint.FloodFill2D; import org.janelia.saalfeldlab.paintera.control.selection.SelectedIds; +import org.janelia.saalfeldlab.paintera.data.DataSource; import org.janelia.saalfeldlab.paintera.data.mask.Mask; import org.janelia.saalfeldlab.paintera.data.mask.MaskInfo; import org.janelia.saalfeldlab.paintera.data.mask.MaskedSource; @@ -24,6 +26,7 @@ import org.slf4j.LoggerFactory; import bdv.fx.viewer.ViewerPanelFX; +import bdv.util.Affine3DHelpers; import gnu.trove.iterator.TLongObjectIterator; import gnu.trove.map.TLongObjectMap; import gnu.trove.map.hash.TLongObjectHashMap; @@ -38,16 +41,26 @@ import javafx.scene.input.KeyEvent; import javafx.scene.input.MouseEvent; import javafx.scene.paint.Color; +import net.imglib2.Cursor; +import net.imglib2.Interval; import net.imglib2.RandomAccess; import net.imglib2.RandomAccessibleInterval; import net.imglib2.RealPoint; +import net.imglib2.RealRandomAccessible; import net.imglib2.converter.Converters; +import net.imglib2.img.array.ArrayImgs; +import net.imglib2.interpolation.randomaccess.NearestNeighborInterpolatorFactory; import net.imglib2.realtransform.AffineTransform3D; +import net.imglib2.realtransform.RealViews; +import net.imglib2.realtransform.Scale3D; import net.imglib2.type.label.Label; import net.imglib2.type.logic.BoolType; +import net.imglib2.type.logic.NativeBoolType; import net.imglib2.type.numeric.IntegerType; import net.imglib2.type.numeric.integer.UnsignedLongType; +import net.imglib2.util.Intervals; import net.imglib2.util.Util; +import net.imglib2.view.Views; public class ShapeInterpolationMode<D extends IntegerType<D>> { @@ -71,6 +84,10 @@ public class ShapeInterpolationMode<D extends IntegerType<D>> private static final double FILL_DEPTH = 2.0; + private static final int MASK_SCALE_LEVEL = 0; // NOTE: getMaskScaledDisplayTransform() would need to change if it is needed to support masks created at arbitrary scale level + + private static final int SHAPE_INTERPOLATION_SCALE_LEVEL = 0; + private static final Color MASK_COLOR = Color.web("00CCFF"); private static final Predicate<UnsignedLongType> FOREGROUND_CHECK = t -> t.get() > 0; @@ -91,6 +108,9 @@ public class ShapeInterpolationMode<D extends IntegerType<D>> private final TLongObjectMap<RealPoint> selectedObjects = new TLongObjectHashMap<>(); + private RandomAccessibleInterval<NativeBoolType> section1; + private RandomAccessibleInterval<NativeBoolType> section2; + public ShapeInterpolationMode( final MaskedSource<D, ?> source, final SelectedIds selectedIds, @@ -177,8 +197,6 @@ public void enterMode(final PainteraBaseView paintera, final ViewerPanelFX viewe final long newLabelId = mask.info.value.get(); converter.setColor(newLabelId, MASK_COLOR); selectedIds.activate(newLabelId); - currentFillValue = 0; - selectedObjects.clear(); } catch (final MaskInUse e) { @@ -207,6 +225,8 @@ public void exitMode(final PainteraBaseView paintera) lastActiveIds = null; currentFillValue = 0; selectedObjects.clear(); + section1 = null; + section2 = null; forgetMask(); activeViewer.get().requestRepaint(); @@ -221,7 +241,7 @@ public boolean isModeOn() private void createMask() throws MaskInUse { final int time = activeViewer.get().getState().timepointProperty().get(); - final int level = 0; + final int level = MASK_SCALE_LEVEL; final long newLabelId = idService.next(); final MaskInfo<UnsignedLongType> maskInfo = new MaskInfo<>(time, level, new UnsignedLongType(newLabelId)); mask = source.generateMask(maskInfo, FOREGROUND_CHECK); @@ -260,8 +280,52 @@ private void setDisableOtherViewers(final boolean disable) private void fixSelection(final PainteraBaseView paintera) { - selectedObjects.clear(); - paintera.allowedActionsProperty().set(allowedActions); + if (section1 == null) + { + section1 = writeCurrentSelectionToImage(); + selectedObjects.clear(); + paintera.allowedActionsProperty().set(allowedActions); + } + else + { + section2 = writeCurrentSelectionToImage(); + interpolateBetweenSections(paintera); + } + } + + private RandomAccessibleInterval<NativeBoolType> writeCurrentSelectionToImage() + { + final AffineTransform3D maskScaledDisplayTransform = getMaskScaledDisplayTransform(SHAPE_INTERPOLATION_SCALE_LEVEL); + + final RandomAccessibleInterval<BoolType> selectionMask = Converters.convert( + mask.mask, + (in, out) -> out.set(in.getIntegerLong() != 0 && in.getIntegerLong() <= currentFillValue), + new BoolType() + ); + final RealRandomAccessible<BoolType> realSelectionMask = Views.interpolate( + Views.extendValue(selectionMask, new BoolType()), + new NearestNeighborInterpolatorFactory<>() + ); + final RealRandomAccessible<BoolType> transformedSelectionMask = RealViews.affine(realSelectionMask, maskScaledDisplayTransform ); + final Interval displayBoundingBox = Intervals.smallestContainingInterval(maskScaledDisplayTransform.estimateBounds(mask.mask)); + final RandomAccessibleInterval<BoolType> transformedSelectionMaskInterval = Views.interval(Views.raster(transformedSelectionMask), displayBoundingBox); + final RandomAccessibleInterval<BoolType> src = Views.hyperSlice(transformedSelectionMaskInterval, 2, 0l); + + final RandomAccessibleInterval<NativeBoolType> dst = ArrayImgs.booleans(Intervals.dimensionsAsLongArray(src)); + LOG.debug("Copying the current selection into an image of size {}", Intervals.dimensionsAsLongArray(dst)); + System.out.println("Copying the current selection into an image of size " + Arrays.toString(Intervals.dimensionsAsLongArray(dst))); + final Cursor<BoolType> srcCursor = Views.flatIterable(src).cursor(); + final Cursor<NativeBoolType> dstCursor = Views.flatIterable(dst).cursor(); + while (dstCursor.hasNext() || srcCursor.hasNext()) + dstCursor.next().set(srcCursor.next().get()); + + return dst; + } + + private void interpolateBetweenSections(final PainteraBaseView paintera) + { + assert section1 != null && section2 != null; + throw new RuntimeException("TODO"); } private void selectObject(final PainteraBaseView paintera, final double x, final double y, final boolean deactivateOthers) @@ -351,6 +415,39 @@ private AffineTransform3D getMaskTransform() return maskTransform; } + private AffineTransform3D getMaskDisplayTransform() + { + final AffineTransform3D viewerTransform = new AffineTransform3D(); + activeViewer.get().getState().getViewerTransform(viewerTransform); + return viewerTransform.concatenate(getMaskTransform()); + } + + /** + * Returns the transformation to map the mask at full resolution to the viewer plane. + * Ignores the current viewer scaling and instead can downscale the resulting image with respect to the requested mipmap level. + * + * NOTE: only works when the mask is created at scale level 0 (full resolution)! + * The way the viewer scaling is ignored would need to change if it is needed to support masks created at arbitrary scale level. + * + * @param level + * @return + */ + private AffineTransform3D getMaskScaledDisplayTransform(final int level) + { + final AffineTransform3D viewerTransform = new AffineTransform3D(); + activeViewer.get().getState().getViewerTransform(viewerTransform); + // undo the scaling in the viewer + final double[] viewerScale = new double[viewerTransform.numDimensions()]; + Arrays.setAll(viewerScale, d -> Affine3DHelpers.extractScale(viewerTransform, d)); + final Scale3D viewerScaleTransform = new Scale3D(viewerScale); + viewerTransform.preConcatenate(viewerScaleTransform.inverse()); + // build the scaling transform to account for the given scale level + final Scale3D maskScaleTransform = new Scale3D(DataSource.getRelativeScales(source, mask.info.t, mask.info.level, level)); + viewerTransform.preConcatenate(maskScaleTransform.inverse()); + // build the resulting transform + return viewerTransform.concatenate(getMaskTransform()); + } + private RealPoint getSourceCoordinates(final double x, final double y) { final AffineTransform3D maskTransform = getMaskTransform(); @@ -361,12 +458,9 @@ private RealPoint getSourceCoordinates(final double x, final double y) private double[] getDisplayCoordinates(final RealPoint sourcePos) { - final AffineTransform3D maskTransform = getMaskTransform(); - final RealPoint displayPos = new RealPoint(maskTransform.numDimensions()); - final AffineTransform3D viewerTransform = new AffineTransform3D(); - activeViewer.get().getState().getViewerTransform(viewerTransform); - final AffineTransform3D maskToViewerTransform = viewerTransform.copy().concatenate(maskTransform); - maskToViewerTransform.apply(sourcePos, displayPos); + final AffineTransform3D maskDisplayTransform = getMaskDisplayTransform(); + final RealPoint displayPos = new RealPoint(maskDisplayTransform.numDimensions()); + maskDisplayTransform.apply(sourcePos, displayPos); assert Util.isApproxEqual(displayPos.getDoublePosition(2), 0.0, 1e-10); return new double[] {displayPos.getDoublePosition(0), displayPos.getDoublePosition(1)}; } From 652c8d30547595ccff49f282bea47ab12da4527e Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Tue, 28 May 2019 14:49:21 -0400 Subject: [PATCH 25/84] keep track of the bounding box of a selection in shape interpolation mode --- .../control/ShapeInterpolationMode.java | 89 ++++++++++++++----- 1 file changed, 67 insertions(+), 22 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java index 300744fc9..d38ca2936 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java @@ -59,13 +59,39 @@ import net.imglib2.type.numeric.IntegerType; import net.imglib2.type.numeric.integer.UnsignedLongType; import net.imglib2.util.Intervals; +import net.imglib2.util.Pair; import net.imglib2.util.Util; +import net.imglib2.util.ValuePair; import net.imglib2.view.Views; public class ShapeInterpolationMode<D extends IntegerType<D>> { private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + private static final class SelectedObjectInfo + { + final RealPoint sourceClickPosition; + final Interval sourceBoundingBox; + + SelectedObjectInfo(final RealPoint sourceClickPosition, final Interval sourceBoundingBox) + { + this.sourceClickPosition = sourceClickPosition; + this.sourceBoundingBox = sourceBoundingBox; + } + } + + private static final class SectionInfo + { + final AffineTransform3D sourceToDisplayTransform; + final Interval sourceBoundingBox; + + SectionInfo(final AffineTransform3D sourceToDisplayTransform, final Interval sourceBoundingBox) + { + this.sourceToDisplayTransform = sourceToDisplayTransform; + this.sourceBoundingBox = sourceBoundingBox; + } + } + private static final AllowedActions allowedActions; private static final AllowedActions allowedActionsWhenSelected; static @@ -104,12 +130,11 @@ public class ShapeInterpolationMode<D extends IntegerType<D>> private long[] lastActiveIds; private Mask<UnsignedLongType> mask; - private int currentFillValue; + private long currentFillValue; - private final TLongObjectMap<RealPoint> selectedObjects = new TLongObjectHashMap<>(); + private final TLongObjectMap<SelectedObjectInfo> selectedObjects = new TLongObjectHashMap<>(); - private RandomAccessibleInterval<NativeBoolType> section1; - private RandomAccessibleInterval<NativeBoolType> section2; + private SectionInfo section1, section2; public ShapeInterpolationMode( final MaskedSource<D, ?> source, @@ -225,8 +250,7 @@ public void exitMode(final PainteraBaseView paintera) lastActiveIds = null; currentFillValue = 0; selectedObjects.clear(); - section1 = null; - section2 = null; + section1 = section2 = null; forgetMask(); activeViewer.get().requestRepaint(); @@ -282,32 +306,53 @@ private void fixSelection(final PainteraBaseView paintera) { if (section1 == null) { - section1 = writeCurrentSelectionToImage(); + LOG.debug("Fix selection in the first section"); + section1 = createSectionInfo(); selectedObjects.clear(); paintera.allowedActionsProperty().set(allowedActions); } else { - section2 = writeCurrentSelectionToImage(); + LOG.debug("Fix selection in the second section"); + section2 = createSectionInfo(); interpolateBetweenSections(paintera); } } - private RandomAccessibleInterval<NativeBoolType> writeCurrentSelectionToImage() + private SectionInfo createSectionInfo() { - final AffineTransform3D maskScaledDisplayTransform = getMaskScaledDisplayTransform(SHAPE_INTERPOLATION_SCALE_LEVEL); + Interval selectionSourceBoundingBox = null; + for (final TLongObjectIterator<SelectedObjectInfo> it = selectedObjects.iterator(); it.hasNext();) + { + it.advance(); + if (selectionSourceBoundingBox == null) + selectionSourceBoundingBox = it.value().sourceBoundingBox; + else + selectionSourceBoundingBox = Intervals.union(selectionSourceBoundingBox, it.value().sourceBoundingBox); + } + return new SectionInfo( + getMaskScaledDisplayTransform(SHAPE_INTERPOLATION_SCALE_LEVEL), + selectionSourceBoundingBox + ); + } + + private RandomAccessibleInterval<NativeBoolType> writeSectionToImage(final SectionInfo sectionInfo) + { + final AffineTransform3D maskScaledDisplayTransform = sectionInfo.sourceToDisplayTransform; + final Interval sourceSelectionBoundingBox = sectionInfo.sourceBoundingBox; + final RandomAccessibleInterval<UnsignedLongType> maskInterval = Views.interval(mask.mask, sourceSelectionBoundingBox); final RandomAccessibleInterval<BoolType> selectionMask = Converters.convert( - mask.mask, + maskInterval, (in, out) -> out.set(in.getIntegerLong() != 0 && in.getIntegerLong() <= currentFillValue), new BoolType() ); final RealRandomAccessible<BoolType> realSelectionMask = Views.interpolate( - Views.extendValue(selectionMask, new BoolType()), + Views.extendZero(selectionMask), new NearestNeighborInterpolatorFactory<>() ); - final RealRandomAccessible<BoolType> transformedSelectionMask = RealViews.affine(realSelectionMask, maskScaledDisplayTransform ); - final Interval displayBoundingBox = Intervals.smallestContainingInterval(maskScaledDisplayTransform.estimateBounds(mask.mask)); + final RealRandomAccessible<BoolType> transformedSelectionMask = RealViews.affine(realSelectionMask, maskScaledDisplayTransform); + final Interval displayBoundingBox = Intervals.smallestContainingInterval(maskScaledDisplayTransform.estimateBounds(sourceSelectionBoundingBox)); final RandomAccessibleInterval<BoolType> transformedSelectionMaskInterval = Views.interval(Views.raster(transformedSelectionMask), displayBoundingBox); final RandomAccessibleInterval<BoolType> src = Views.hyperSlice(transformedSelectionMaskInterval, 2, 0l); @@ -337,10 +382,10 @@ private void selectObject(final PainteraBaseView paintera, final double x, final if (deactivateOthers) { - for (final TLongObjectIterator<RealPoint> it = selectedObjects.iterator(); it.hasNext();) + for (final TLongObjectIterator<SelectedObjectInfo> it = selectedObjects.iterator(); it.hasNext();) { it.advance(); - final double[] deselectDisplayPos = getDisplayCoordinates(it.value()); + final double[] deselectDisplayPos = getDisplayCoordinates(it.value().sourceClickPosition); runFloodFillToDeselect(deselectDisplayPos[0], deselectDisplayPos[1]); } selectedObjects.clear(); @@ -348,8 +393,8 @@ private void selectObject(final PainteraBaseView paintera, final double x, final if (!wasSelected || (deactivateOthers && numSelectedObjects > 1)) { - final long newFillValue = runFloodFillToSelect(x, y); - selectedObjects.put(newFillValue, getSourceCoordinates(x, y)); + final Pair<Long, Interval> fillValueAndInterval = runFloodFillToSelect(x, y); + selectedObjects.put(fillValueAndInterval.getA(), new SelectedObjectInfo(getSourceCoordinates(x, y), fillValueAndInterval.getB())); } else { @@ -366,12 +411,12 @@ private void selectObject(final PainteraBaseView paintera, final double x, final * * @param x * @param y - * @return the fill value of the selected object + * @return the fill value of the selected object and the affected interval in source coordinates */ - private long runFloodFillToSelect(final double x, final double y) + private Pair<Long, Interval> runFloodFillToSelect(final double x, final double y) { - FloodFill2D.fillMaskAt(x, y, activeViewer.get(), mask, source, ++currentFillValue, FILL_DEPTH); - return currentFillValue; + final Interval affectedInterval = FloodFill2D.fillMaskAt(x, y, activeViewer.get(), mask, source, ++currentFillValue, FILL_DEPTH); + return new ValuePair<>(currentFillValue, affectedInterval); } /** From 63ba0df7166a4e3310167f764859c86af2a5cfa3 Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Tue, 28 May 2019 14:54:33 -0400 Subject: [PATCH 26/84] transform mask section as is without any unwanted scaling For example, when the transform of a masked source is 4,4,40 at scale level 0, the same scaling was applied to the mask. Now the mask image is transformed directly without any extra scaling. (the mask is scaled only if a different mipmap level is requested) --- .../control/ShapeInterpolationMode.java | 57 ++++++++++++------- 1 file changed, 37 insertions(+), 20 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java index d38ca2936..9c49478a0 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java @@ -110,9 +110,9 @@ private static final class SectionInfo private static final double FILL_DEPTH = 2.0; - private static final int MASK_SCALE_LEVEL = 0; // NOTE: getMaskScaledDisplayTransform() would need to change if it is needed to support masks created at arbitrary scale level + private static final int MASK_SCALE_LEVEL = 0; - private static final int SHAPE_INTERPOLATION_SCALE_LEVEL = 0; + private static final int SHAPE_INTERPOLATION_SCALE_LEVEL = MASK_SCALE_LEVEL; private static final Color MASK_COLOR = Color.web("00CCFF"); @@ -331,7 +331,7 @@ private SectionInfo createSectionInfo() selectionSourceBoundingBox = Intervals.union(selectionSourceBoundingBox, it.value().sourceBoundingBox); } return new SectionInfo( - getMaskScaledDisplayTransform(SHAPE_INTERPOLATION_SCALE_LEVEL), + getMaskDisplayTransformIgnoreScaling(SHAPE_INTERPOLATION_SCALE_LEVEL), selectionSourceBoundingBox ); } @@ -460,37 +460,54 @@ private AffineTransform3D getMaskTransform() return maskTransform; } - private AffineTransform3D getMaskDisplayTransform() + private AffineTransform3D getDisplayTransform() { final AffineTransform3D viewerTransform = new AffineTransform3D(); activeViewer.get().getState().getViewerTransform(viewerTransform); - return viewerTransform.concatenate(getMaskTransform()); + return viewerTransform; + } + + private AffineTransform3D getMaskDisplayTransform() + { + return getDisplayTransform().concatenate(getMaskTransform()); } /** - * Returns the transformation to map the mask at full resolution to the viewer plane. - * Ignores the current viewer scaling and instead can downscale the resulting image with respect to the requested mipmap level. - * - * NOTE: only works when the mask is created at scale level 0 (full resolution)! - * The way the viewer scaling is ignored would need to change if it is needed to support masks created at arbitrary scale level. + * Returns the transformation to bring the mask to the current viewer plane at the requested mipmap level. + * Ignores the scaling in the viewer and in the mask and instead uses the requested mipmap level for scaling. * * @param level * @return */ - private AffineTransform3D getMaskScaledDisplayTransform(final int level) + private AffineTransform3D getMaskDisplayTransformIgnoreScaling(final int level) { - final AffineTransform3D viewerTransform = new AffineTransform3D(); - activeViewer.get().getState().getViewerTransform(viewerTransform); - // undo the scaling in the viewer + final AffineTransform3D maskMipmapDisplayTransform = getMaskDisplayTransformIgnoreScaling(); + if (level != mask.info.level) + { + // scale with respect to the given mipmap level + final Scale3D relativeScaleTransform = new Scale3D(DataSource.getRelativeScales(source, mask.info.t, level, mask.info.level)); + maskMipmapDisplayTransform.preConcatenate(relativeScaleTransform); + } + return maskMipmapDisplayTransform; + } + + /** + * Returns the transformation to bring the mask to the current viewer plane. + * Ignores the scaling in the viewer and in the mask. + * + * @return + */ + private AffineTransform3D getMaskDisplayTransformIgnoreScaling() + { + final AffineTransform3D viewerTransform = getDisplayTransform(); + // undo scaling in the viewer final double[] viewerScale = new double[viewerTransform.numDimensions()]; Arrays.setAll(viewerScale, d -> Affine3DHelpers.extractScale(viewerTransform, d)); - final Scale3D viewerScaleTransform = new Scale3D(viewerScale); - viewerTransform.preConcatenate(viewerScaleTransform.inverse()); - // build the scaling transform to account for the given scale level - final Scale3D maskScaleTransform = new Scale3D(DataSource.getRelativeScales(source, mask.info.t, mask.info.level, level)); - viewerTransform.preConcatenate(maskScaleTransform.inverse()); + final Scale3D scalingTransform = new Scale3D(viewerScale); + // neutralize mask scaling if there is any + scalingTransform.concatenate(new Scale3D(DataSource.getScale(source, mask.info.t, mask.info.level))); // build the resulting transform - return viewerTransform.concatenate(getMaskTransform()); + return viewerTransform.preConcatenate(scalingTransform.inverse()).concatenate(getMaskTransform()); } private RealPoint getSourceCoordinates(final double x, final double y) From 1e5b7e8c864d4ac1e4fa3885104da34affe4f73f Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Wed, 29 May 2019 13:38:18 -0400 Subject: [PATCH 27/84] first working version of shape interpolation --- pom.xml | 2 +- .../control/ShapeInterpolationMode.java | 223 ++++++++++++++---- 2 files changed, 181 insertions(+), 44 deletions(-) diff --git a/pom.xml b/pom.xml index ccdfe4fe5..917e8ba4c 100644 --- a/pom.xml +++ b/pom.xml @@ -119,7 +119,7 @@ <dependency> <groupId>org.janelia.saalfeldlab</groupId> <artifactId>label-utilities</artifactId> - <version>0.2.1</version> + <version>0.4.1</version> </dependency> <dependency> <groupId>org.janelia.saalfeldlab</groupId> diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java index 9c49478a0..89a7c5c90 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java @@ -1,13 +1,20 @@ package org.janelia.saalfeldlab.paintera.control; +import java.io.IOException; import java.lang.invoke.MethodHandles; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import java.util.function.Predicate; import org.janelia.saalfeldlab.fx.event.DelegateEventHandlers; import org.janelia.saalfeldlab.fx.event.EventFX; import org.janelia.saalfeldlab.fx.event.KeyTracker; import org.janelia.saalfeldlab.fx.event.MouseClickFX; +import org.janelia.saalfeldlab.labels.InterpolateBetweenSections; +import org.janelia.saalfeldlab.n5.GzipCompression; +import org.janelia.saalfeldlab.n5.N5FSWriter; +import org.janelia.saalfeldlab.n5.imglib2.N5Utils; import org.janelia.saalfeldlab.paintera.PainteraBaseView; import org.janelia.saalfeldlab.paintera.control.actions.AllowedActions; import org.janelia.saalfeldlab.paintera.control.actions.LabelAction; @@ -41,23 +48,27 @@ import javafx.scene.input.KeyEvent; import javafx.scene.input.MouseEvent; import javafx.scene.paint.Color; -import net.imglib2.Cursor; +import net.imglib2.FinalInterval; import net.imglib2.Interval; import net.imglib2.RandomAccess; import net.imglib2.RandomAccessibleInterval; +import net.imglib2.RealInterval; import net.imglib2.RealPoint; import net.imglib2.RealRandomAccessible; +import net.imglib2.algorithm.morphology.distance.DistanceTransform.DISTANCE_TYPE; import net.imglib2.converter.Converters; -import net.imglib2.img.array.ArrayImgs; +import net.imglib2.img.array.ArrayImgFactory; import net.imglib2.interpolation.randomaccess.NearestNeighborInterpolatorFactory; import net.imglib2.realtransform.AffineTransform3D; import net.imglib2.realtransform.RealViews; import net.imglib2.realtransform.Scale3D; +import net.imglib2.realtransform.Translation3D; import net.imglib2.type.label.Label; import net.imglib2.type.logic.BoolType; -import net.imglib2.type.logic.NativeBoolType; import net.imglib2.type.numeric.IntegerType; import net.imglib2.type.numeric.integer.UnsignedLongType; +import net.imglib2.type.numeric.integer.UnsignedShortType; +import net.imglib2.type.numeric.real.DoubleType; import net.imglib2.util.Intervals; import net.imglib2.util.Pair; import net.imglib2.util.Util; @@ -68,6 +79,13 @@ public class ShapeInterpolationMode<D extends IntegerType<D>> { private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + private static enum ModeState + { + Selecting, + Interpolating, + Review + } + private static final class SelectedObjectInfo { final RealPoint sourceClickPosition; @@ -118,6 +136,8 @@ private static final class SectionInfo private static final Predicate<UnsignedLongType> FOREGROUND_CHECK = t -> t.get() > 0; + private static final long INTERPOLATION_FILL_VALUE = 1; + private final ObjectProperty<ViewerPanelFX> activeViewer = new SimpleObjectProperty<>(); private final MaskedSource<D, ?> source; @@ -134,7 +154,9 @@ private static final class SectionInfo private final TLongObjectMap<SelectedObjectInfo> selectedObjects = new TLongObjectHashMap<>(); - private SectionInfo section1, section2; + private SectionInfo sectionInfo1, sectionInfo2; + + private ModeState modeState; public ShapeInterpolationMode( final MaskedSource<D, ?> source, @@ -160,7 +182,7 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke KeyEvent.KEY_PRESSED, EventFX.KEY_PRESSED( "enter shape interpolation mode", - e -> enterMode(paintera, (ViewerPanelFX) e.getTarget()), + e -> {e.consume(); enterMode(paintera, (ViewerPanelFX) e.getTarget());}, e -> e.getTarget() instanceof ViewerPanelFX && !isModeOn() && !source.isApplyingMaskProperty().get() && @@ -171,29 +193,38 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke KeyEvent.KEY_PRESSED, EventFX.KEY_PRESSED( "fix selection", - e -> fixSelection(paintera), - e -> isModeOn() && + e -> {e.consume(); fixSelection(paintera);}, + e -> modeState == ModeState.Selecting && !selectedObjects.isEmpty() && keyTracker.areOnlyTheseKeysDown(KeyCode.S) ) ); + filter.addEventHandler( + KeyEvent.KEY_PRESSED, + EventFX.KEY_PRESSED( + "apply mask", + e -> {e.consume(); applyMask(paintera);}, + e -> modeState == ModeState.Review && + keyTracker.areOnlyTheseKeysDown(KeyCode.S) + ) + ); filter.addEventHandler( KeyEvent.KEY_PRESSED, EventFX.KEY_PRESSED( "exit shape interpolation mode", - e -> exitMode(paintera), + e -> {e.consume(); exitMode(paintera);}, e -> isModeOn() && keyTracker.areOnlyTheseKeysDown(KeyCode.ESCAPE) ) ); filter.addEventHandler(MouseEvent.ANY, new MouseClickFX( "select object in current section", e -> {e.consume(); selectObject(paintera, e.getX(), e.getY(), true);}, - e -> isModeOn() && e.isPrimaryButtonDown() && keyTracker.noKeysActive()) + e -> modeState == ModeState.Selecting && e.isPrimaryButtonDown() && keyTracker.noKeysActive()) .handler()); filter.addEventHandler(MouseEvent.ANY, new MouseClickFX( "toggle object in current section", e -> {e.consume(); selectObject(paintera, e.getX(), e.getY(), false);}, - e -> isModeOn() && + e -> modeState == ModeState.Selecting && ((e.isSecondaryButtonDown() && keyTracker.noKeysActive()) || (e.isPrimaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL)))) .handler()); @@ -227,6 +258,8 @@ public void enterMode(final PainteraBaseView paintera, final ViewerPanelFX viewe { e.printStackTrace(); } + + modeState = ModeState.Selecting; } public void exitMode(final PainteraBaseView paintera) @@ -250,7 +283,8 @@ public void exitMode(final PainteraBaseView paintera) lastActiveIds = null; currentFillValue = 0; selectedObjects.clear(); - section1 = section2 = null; + sectionInfo1 = sectionInfo2 = null; + modeState = null; forgetMask(); activeViewer.get().requestRepaint(); @@ -259,7 +293,7 @@ public void exitMode(final PainteraBaseView paintera) public boolean isModeOn() { - return activeViewer.get() != null; + return modeState != null; } private void createMask() throws MaskInUse @@ -304,21 +338,35 @@ private void setDisableOtherViewers(final boolean disable) private void fixSelection(final PainteraBaseView paintera) { - if (section1 == null) + if (sectionInfo1 == null) { LOG.debug("Fix selection in the first section"); - section1 = createSectionInfo(); + sectionInfo1 = createSectionInfo(); selectedObjects.clear(); paintera.allowedActionsProperty().set(allowedActions); } else { LOG.debug("Fix selection in the second section"); - section2 = createSectionInfo(); - interpolateBetweenSections(paintera); + sectionInfo2 = createSectionInfo(); + modeState = ModeState.Interpolating; + interpolateBetweenSections(); + modeState = ModeState.Review; + paintera.allowedActionsProperty().set(allowedActions); } } + private void applyMask(final PainteraBaseView paintera) + { + final Interval sectionUnionSourceInterval = Intervals.union( + sectionInfo1.sourceBoundingBox, + sectionInfo2.sourceBoundingBox + ); + System.out.println("applying mask using affected interval of size " + Arrays.toString(Intervals.dimensionsAsLongArray(sectionUnionSourceInterval))); + source.applyMask(mask, sectionUnionSourceInterval, FOREGROUND_CHECK); + exitMode(paintera); + } + private SectionInfo createSectionInfo() { Interval selectionSourceBoundingBox = null; @@ -336,41 +384,130 @@ private SectionInfo createSectionInfo() ); } - private RandomAccessibleInterval<NativeBoolType> writeSectionToImage(final SectionInfo sectionInfo) + private void testOutput(final RandomAccessibleInterval<UnsignedLongType> mask, final String dataset) { - final AffineTransform3D maskScaledDisplayTransform = sectionInfo.sourceToDisplayTransform; - final Interval sourceSelectionBoundingBox = sectionInfo.sourceBoundingBox; + try + { + N5Utils.save( + Converters.convert(mask, (in, out) -> out.setInteger(in.get()), new UnsignedShortType()), + new N5FSWriter("/groups/saalfeld/home/pisarevi/data/paintera-test/test.n5"), + dataset, + mask.numDimensions() == 2 ? new int[] {4096, 4096} : new int[] {256, 256, 256}, + new GzipCompression() + ); + } + catch (final IOException e) + { + e.printStackTrace(); + } + } - final RandomAccessibleInterval<UnsignedLongType> maskInterval = Views.interval(mask.mask, sourceSelectionBoundingBox); - final RandomAccessibleInterval<BoolType> selectionMask = Converters.convert( - maskInterval, - (in, out) -> out.set(in.getIntegerLong() != 0 && in.getIntegerLong() <= currentFillValue), - new BoolType() + @SuppressWarnings("unchecked") + private void interpolateBetweenSections() + { + final SectionInfo[] sectionInfoPair = {sectionInfo1, sectionInfo2}; + + // find the interval that encompasses both of the sections + final Interval sectionsUnionInterval = Intervals.union( + getSectionInterval(sectionInfoPair[0]), + getSectionInterval(sectionInfoPair[1]) ); - final RealRandomAccessible<BoolType> realSelectionMask = Views.interpolate( - Views.extendZero(selectionMask), - new NearestNeighborInterpolatorFactory<>() + + // extract the sections using the union interval + final RandomAccessibleInterval<UnsignedLongType>[] sectionPair = new RandomAccessibleInterval[2]; + for (int i = 0; i < 2; ++i) + sectionPair[i] = getMaskSection(sectionInfoPair[i].sourceToDisplayTransform, sectionsUnionInterval); + + // replace fill values in the sections with a single value because we do not want to interpolate between multiple labels + for (final RandomAccessibleInterval<UnsignedLongType> section : sectionPair) + for (final UnsignedLongType val : Views.iterable(section)) + if (FOREGROUND_CHECK.test(val)) + val.set(INTERPOLATION_FILL_VALUE); + + +// testOutput(sectionPair[0], "section1"); +// testOutput(sectionPair[1], "section2"); + + + // find the distance between the sections and generate the filler sections between them + final double distanceBetweenSections = computeDistanceBetweenSections(); + final int numFillers = (int) Math.round(Math.abs(distanceBetweenSections)) - 1; + final double fillerStep = distanceBetweenSections / (numFillers + 1); + final RandomAccessibleInterval<UnsignedLongType>[] fillers = new RandomAccessibleInterval[numFillers]; + System.out.println("Distance between the sections: " + distanceBetweenSections); + System.out.println("Creating " + numFillers + " fillers of size " + Arrays.toString(Intervals.dimensionsAsLongArray(sectionsUnionInterval)) + ":"); + for (int i = 0; i < numFillers; ++i) + { + final double fillerShift = fillerStep * (i + 1); + final AffineTransform3D fillerTransform = sectionInfo1.sourceToDisplayTransform.copy(); + fillerTransform.preConcatenate(new Translation3D(0, 0, fillerShift)); + fillers[i] = getMaskSection(fillerTransform, sectionsUnionInterval); + System.out.println(" " + fillerShift); + } + + + final List<RandomAccessibleInterval<UnsignedLongType>> sectionsList = new ArrayList<>(); + sectionsList.add(sectionPair[0]); + sectionsList.addAll(Arrays.asList(fillers)); + sectionsList.add(sectionPair[1]); + + System.out.println("writing debug output before interpolating..."); + testOutput(Views.stack(sectionsList), "sections-before"); + System.out.println("done"); + + System.out.println("Running interpolation..."); + InterpolateBetweenSections.interpolateBetweenSections( + sectionPair[0], + sectionPair[1], + new ArrayImgFactory<>(new DoubleType()), + fillers, + DISTANCE_TYPE.EUCLIDIAN, + new double[] {1}, + Label.BACKGROUND ); - final RealRandomAccessible<BoolType> transformedSelectionMask = RealViews.affine(realSelectionMask, maskScaledDisplayTransform); - final Interval displayBoundingBox = Intervals.smallestContainingInterval(maskScaledDisplayTransform.estimateBounds(sourceSelectionBoundingBox)); - final RandomAccessibleInterval<BoolType> transformedSelectionMaskInterval = Views.interval(Views.raster(transformedSelectionMask), displayBoundingBox); - final RandomAccessibleInterval<BoolType> src = Views.hyperSlice(transformedSelectionMaskInterval, 2, 0l); + System.out.println("Done"); - final RandomAccessibleInterval<NativeBoolType> dst = ArrayImgs.booleans(Intervals.dimensionsAsLongArray(src)); - LOG.debug("Copying the current selection into an image of size {}", Intervals.dimensionsAsLongArray(dst)); - System.out.println("Copying the current selection into an image of size " + Arrays.toString(Intervals.dimensionsAsLongArray(dst))); - final Cursor<BoolType> srcCursor = Views.flatIterable(src).cursor(); - final Cursor<NativeBoolType> dstCursor = Views.flatIterable(dst).cursor(); - while (dstCursor.hasNext() || srcCursor.hasNext()) - dstCursor.next().set(srcCursor.next().get()); + System.out.println("writing debug output after interpolating..."); + testOutput(Views.stack(sectionsList), "sections-after"); + System.out.println("done"); + } + + private Interval getSectionInterval(final SectionInfo sectionInfo) + { + final RealInterval sectionBounds = sectionInfo.sourceToDisplayTransform.estimateBounds(sectionInfo.sourceBoundingBox); + final Interval sectionInterval3D = Intervals.smallestContainingInterval(sectionBounds); + return new FinalInterval( + new long[] {sectionInterval3D.min(0), sectionInterval3D.min(1)}, + new long[] {sectionInterval3D.max(0), sectionInterval3D.max(1)} + ); + } + + private RandomAccessibleInterval<UnsignedLongType> getMaskSection(final AffineTransform3D transform, final Interval sectionInterval) + { + final Interval sectionInterval3D = sectionInterval.numDimensions() == 3 ? sectionInterval : new FinalInterval( + new long[] {sectionInterval.min(0), sectionInterval.min(1), 0l}, + new long[] {sectionInterval.max(0), sectionInterval.max(1), 0l} + ); + final RealRandomAccessible<UnsignedLongType> transformedMask = getTransformedMask(transform); + final RandomAccessibleInterval<UnsignedLongType> transformedMaskInterval = Views.interval(Views.raster(transformedMask), sectionInterval3D); + return Views.hyperSlice(transformedMaskInterval, 2, 0l); + } - return dst; + private RealRandomAccessible<UnsignedLongType> getTransformedMask(final AffineTransform3D transform) + { + final RealRandomAccessible<UnsignedLongType> interpolatedMask = Views.interpolate( + Views.extendValue(mask.mask, new UnsignedLongType(Label.BACKGROUND)), + new NearestNeighborInterpolatorFactory<>() + ); + return RealViews.affine(interpolatedMask, transform); } - private void interpolateBetweenSections(final PainteraBaseView paintera) + private double computeDistanceBetweenSections() { - assert section1 != null && section2 != null; - throw new RuntimeException("TODO"); + final double[] pos1 = new double[3], pos2 = new double[3]; + sectionInfo1.sourceToDisplayTransform.apply(pos1, pos1); + sectionInfo2.sourceToDisplayTransform.apply(pos2, pos2); + return pos2[2] - pos1[2]; // We care only about the shift between the sections (Z distance in the viewer) } private void selectObject(final PainteraBaseView paintera, final double x, final double y, final boolean deactivateOthers) @@ -420,7 +557,7 @@ private Pair<Long, Interval> runFloodFillToSelect(final double x, final double y } /** - * Flood-fills the mask using a background value to remove the object from the selection. + * Flood-fills the mask using the background value to remove the object from the selection. * * @param x * @param y From 61e5a6e821317c1361cbc18ae91adbfb3a49cb22 Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Fri, 31 May 2019 11:30:25 -0400 Subject: [PATCH 28/84] allow setting mask from outside in MaskedSource the setMask() method takes a RealRandomAccessible to make it easy to use virtual masks --- .../paintera/data/mask/MaskedSource.java | 95 +++++++++++++++---- 1 file changed, 79 insertions(+), 16 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/data/mask/MaskedSource.java b/src/main/java/org/janelia/saalfeldlab/paintera/data/mask/MaskedSource.java index 159027150..1c2568f70 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/data/mask/MaskedSource.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/data/mask/MaskedSource.java @@ -279,12 +279,12 @@ public Mask<UnsignedLongType> getCurrentMask() } public Mask<UnsignedLongType> generateMask( - final MaskInfo<UnsignedLongType> mask, + final MaskInfo<UnsignedLongType> maskInfo, final Predicate<UnsignedLongType> isPaintedForeground) throws MaskInUse { - LOG.debug("Asking for mask: {}", mask); + LOG.debug("Asking for mask: {}", maskInfo); synchronized (this) { final boolean canGenerateMask = !isCreatingMask && currentMask == null && !isApplyingMask.get() && !isPersisting; @@ -300,24 +300,55 @@ public Mask<UnsignedLongType> generateMask( } this.isCreatingMask = true; } - LOG.debug("Generating mask: {}", mask); + LOG.debug("Generating mask: {}", maskInfo); - Pair<RandomAccessibleInterval<UnsignedLongType>, RandomAccessibleInterval<VolatileUnsignedLongType>> - storeWithVolatile = createMaskStoreWithVolatile(mask.level); + final Pair<RandomAccessibleInterval<UnsignedLongType>, RandomAccessibleInterval<VolatileUnsignedLongType>> + storeWithVolatile = createMaskStoreWithVolatile(maskInfo.level); final RandomAccessibleInterval<UnsignedLongType> store = storeWithVolatile.getKey(); final RandomAccessibleInterval<VolatileUnsignedLongType> vstore = storeWithVolatile.getValue(); - setMasks(store, vstore, mask.level, mask.value, isPaintedForeground); + setMasks(store, vstore, maskInfo.level, maskInfo.value, isPaintedForeground); final AccessedBlocksRandomAccessible<UnsignedLongType> trackingStore = new AccessedBlocksRandomAccessible<>( store, ((AbstractCellImg<?,?,?,?>)store).getCellGrid() ); - final Mask<UnsignedLongType> m = new Mask<>(mask, trackingStore); + final Mask<UnsignedLongType> mask = new Mask<>(maskInfo, trackingStore); synchronized(this) { this.isCreatingMask = false; - this.currentMask = m; + this.currentMask = mask; + } + return mask; + } + + public void setMask( + final MaskInfo<UnsignedLongType> maskInfo, + final RealRandomAccessible<UnsignedLongType> mask, + final RealRandomAccessible<VolatileUnsignedLongType> vmask, + final Predicate<UnsignedLongType> isPaintedForeground) + throws MaskInUse + { + synchronized (this) + { + final boolean canSetMask = !isCreatingMask && currentMask == null && !isApplyingMask.get() && !isPersisting; + LOG.debug("Can set mask? {}", canSetMask); + if (!canSetMask) + { + LOG.error( + "Currently processing, cannot set new mask: persisting? {} mask in use? {}", + isPersisting, + currentMask + ); + throw new MaskInUse("Busy, cannot set new mask."); + } + } + + setMasks(mask, vmask, maskInfo.level, maskInfo.value, isPaintedForeground); + + synchronized(this) + { + final RandomAccessibleInterval<UnsignedLongType> rasteredMask = Views.interval(Views.raster(mask), source.getSource(0, maskInfo.level)); + this.currentMask = new Mask<>(maskInfo, rasteredMask); } - return m; } public void applyMask( @@ -495,7 +526,7 @@ public void persistCanvas() throws CannotPersist this.affectedBlocks.clear(); final MaskedSource<D, T> thiz = this; final BooleanProperty proxy = new SimpleBooleanProperty(this.isPersisting); - ObservableList<String> states = FXCollections.observableArrayList(); + final ObservableList<String> states = FXCollections.observableArrayList(); final Runnable dialogHandler = () -> { LOG.warn("Creating commit status dialog."); final Alert isCommittingDialog = PainteraAlerts.alert(Alert.AlertType.INFORMATION); @@ -522,7 +553,7 @@ public void persistCanvas() throws CannotPersist } try { states.add("Persisting painted labels..."); - List<TLongObjectMap<PersistCanvas.BlockDiff>> blockDiffs = this.persistCanvas.persistCanvas(canvas, affectedBlocks); + final List<TLongObjectMap<PersistCanvas.BlockDiff>> blockDiffs = this.persistCanvas.persistCanvas(canvas, affectedBlocks); states.set(states.size() - 1, "Persisting painted labels... Done"); if (this.persistCanvas.supportsLabelBlockLookupUpdate()) { states.add("Updating label-to-block lookup..."); @@ -539,7 +570,7 @@ public void persistCanvas() throws CannotPersist caughtException = e; throw new RuntimeException("Error while trying to persist.", e); } - catch (RuntimeException e) + catch (final RuntimeException e) { caughtException = e; throw e; @@ -1350,7 +1381,7 @@ void affectBlocks( } - private DiskCachedCellImgOptions getMaskDiskCachedCellImgOptions(int level) + private DiskCachedCellImgOptions getMaskDiskCachedCellImgOptions(final int level) { return DiskCachedCellImgOptions .options() @@ -1385,7 +1416,23 @@ private void setMasks( { setAtMaskLevel(store, vstore, maskLevel, value, isPaintedForeground); LOG.debug("Created mask at scale level {}", maskLevel); + setMaskScaleLevels(maskLevel); + } + private void setMasks( + final RealRandomAccessible<UnsignedLongType> mask, + final RealRandomAccessible<VolatileUnsignedLongType> vmask, + final int maskLevel, + final UnsignedLongType value, + final Predicate<UnsignedLongType> isPaintedForeground) + { + setAtMaskLevel(mask, vmask, maskLevel, value, isPaintedForeground); + LOG.debug("Created mask at scale level {}", maskLevel); + setMaskScaleLevels(maskLevel); + } + + private void setMaskScaleLevels(final int maskLevel) + { final RealRandomAccessible<UnsignedLongType> dMask = this.dMasks[maskLevel]; final RealRandomAccessible<VolatileUnsignedLongType> tMask = this.tMasks[maskLevel]; @@ -1419,13 +1466,29 @@ private void setAtMaskLevel( final UnsignedLongType value, final Predicate<UnsignedLongType> isPaintedForeground) { - this.dMasks[maskLevel] = Converters.convert( + setAtMaskLevel( interpolateNearestNeighbor(Views.extendZero(store)), + interpolateNearestNeighbor(Views.extendZero(vstore)), + maskLevel, + value, + isPaintedForeground + ); + } + + private void setAtMaskLevel( + final RealRandomAccessible<UnsignedLongType> mask, + final RealRandomAccessible<VolatileUnsignedLongType> vmask, + final int maskLevel, + final UnsignedLongType value, + final Predicate<UnsignedLongType> isPaintedForeground) + { + this.dMasks[maskLevel] = Converters.convert( + mask, (input, output) -> output.set(isPaintedForeground.test(input) ? value : INVALID), new UnsignedLongType()); this.tMasks[maskLevel] = Converters.convert( - interpolateNearestNeighbor(Views.extendZero(vstore)), + vmask, (input, output) -> { final boolean isValid = input.isValid(); output.setValid(isValid); @@ -1437,7 +1500,7 @@ private void setAtMaskLevel( new VolatileUnsignedLongType()); } - private static <T> RealRandomAccessible<T> interpolateNearestNeighbor(RandomAccessible<T> ra) + private static <T> RealRandomAccessible<T> interpolateNearestNeighbor(final RandomAccessible<T> ra) { return Views.interpolate(ra, new NearestNeighborInterpolatorFactory<>()); } From 8c14b241faf0c1683975315a7316e2d4316fcfa6 Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Fri, 31 May 2019 11:31:33 -0400 Subject: [PATCH 29/84] use virtual mask for interpolation between two distance transforms --- .../control/ShapeInterpolationMode.java | 341 +++++++++++------- 1 file changed, 202 insertions(+), 139 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java index 89a7c5c90..7c0ff6a95 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java @@ -1,20 +1,14 @@ package org.janelia.saalfeldlab.paintera.control; -import java.io.IOException; import java.lang.invoke.MethodHandles; -import java.util.ArrayList; import java.util.Arrays; -import java.util.List; import java.util.function.Predicate; import org.janelia.saalfeldlab.fx.event.DelegateEventHandlers; import org.janelia.saalfeldlab.fx.event.EventFX; import org.janelia.saalfeldlab.fx.event.KeyTracker; import org.janelia.saalfeldlab.fx.event.MouseClickFX; -import org.janelia.saalfeldlab.labels.InterpolateBetweenSections; -import org.janelia.saalfeldlab.n5.GzipCompression; -import org.janelia.saalfeldlab.n5.N5FSWriter; -import org.janelia.saalfeldlab.n5.imglib2.N5Utils; +import org.janelia.saalfeldlab.fx.util.InvokeOnJavaFXApplicationThread; import org.janelia.saalfeldlab.paintera.PainteraBaseView; import org.janelia.saalfeldlab.paintera.control.actions.AllowedActions; import org.janelia.saalfeldlab.paintera.control.actions.LabelAction; @@ -23,6 +17,7 @@ import org.janelia.saalfeldlab.paintera.control.paint.FloodFill2D; import org.janelia.saalfeldlab.paintera.control.selection.SelectedIds; import org.janelia.saalfeldlab.paintera.data.DataSource; +import org.janelia.saalfeldlab.paintera.data.PredicateDataSource.PredicateConverter; import org.janelia.saalfeldlab.paintera.data.mask.Mask; import org.janelia.saalfeldlab.paintera.data.mask.MaskInfo; import org.janelia.saalfeldlab.paintera.data.mask.MaskedSource; @@ -48,6 +43,7 @@ import javafx.scene.input.KeyEvent; import javafx.scene.input.MouseEvent; import javafx.scene.paint.Color; +import net.imglib2.Cursor; import net.imglib2.FinalInterval; import net.imglib2.Interval; import net.imglib2.RandomAccess; @@ -55,20 +51,27 @@ import net.imglib2.RealInterval; import net.imglib2.RealPoint; import net.imglib2.RealRandomAccessible; +import net.imglib2.algorithm.morphology.distance.DistanceTransform; import net.imglib2.algorithm.morphology.distance.DistanceTransform.DISTANCE_TYPE; import net.imglib2.converter.Converters; +import net.imglib2.converter.logical.Logical; import net.imglib2.img.array.ArrayImgFactory; +import net.imglib2.interpolation.randomaccess.NLinearInterpolatorFactory; import net.imglib2.interpolation.randomaccess.NearestNeighborInterpolatorFactory; +import net.imglib2.loops.LoopBuilder; import net.imglib2.realtransform.AffineTransform3D; import net.imglib2.realtransform.RealViews; import net.imglib2.realtransform.Scale3D; import net.imglib2.realtransform.Translation3D; +import net.imglib2.type.BooleanType; +import net.imglib2.type.NativeType; 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.type.numeric.integer.UnsignedShortType; -import net.imglib2.type.numeric.real.DoubleType; +import net.imglib2.type.numeric.real.FloatType; +import net.imglib2.type.volatiles.VolatileUnsignedLongType; import net.imglib2.util.Intervals; import net.imglib2.util.Pair; import net.imglib2.util.Util; @@ -136,8 +139,6 @@ private static final class SectionInfo private static final Predicate<UnsignedLongType> FOREGROUND_CHECK = t -> t.get() > 0; - private static final long INTERPOLATION_FILL_VALUE = 1; - private final ObjectProperty<ViewerPanelFX> activeViewer = new SimpleObjectProperty<>(); private final MaskedSource<D, ?> source; @@ -156,7 +157,9 @@ private static final class SectionInfo private SectionInfo sectionInfo1, sectionInfo2; - private ModeState modeState; + private ObjectProperty<ModeState> modeState = new SimpleObjectProperty<>(); + + private Thread workerThread; public ShapeInterpolationMode( final MaskedSource<D, ?> source, @@ -194,7 +197,7 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke EventFX.KEY_PRESSED( "fix selection", e -> {e.consume(); fixSelection(paintera);}, - e -> modeState == ModeState.Selecting && + e -> modeState.get() == ModeState.Selecting && !selectedObjects.isEmpty() && keyTracker.areOnlyTheseKeysDown(KeyCode.S) ) @@ -204,7 +207,7 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke EventFX.KEY_PRESSED( "apply mask", e -> {e.consume(); applyMask(paintera);}, - e -> modeState == ModeState.Review && + e -> modeState.get() == ModeState.Review && keyTracker.areOnlyTheseKeysDown(KeyCode.S) ) ); @@ -212,19 +215,19 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke KeyEvent.KEY_PRESSED, EventFX.KEY_PRESSED( "exit shape interpolation mode", - e -> {e.consume(); exitMode(paintera);}, + e -> {e.consume(); exitMode(paintera, false);}, e -> isModeOn() && keyTracker.areOnlyTheseKeysDown(KeyCode.ESCAPE) ) ); filter.addEventHandler(MouseEvent.ANY, new MouseClickFX( "select object in current section", e -> {e.consume(); selectObject(paintera, e.getX(), e.getY(), true);}, - e -> modeState == ModeState.Selecting && e.isPrimaryButtonDown() && keyTracker.noKeysActive()) + e -> modeState.get() == ModeState.Selecting && e.isPrimaryButtonDown() && keyTracker.noKeysActive()) .handler()); filter.addEventHandler(MouseEvent.ANY, new MouseClickFX( "toggle object in current section", e -> {e.consume(); selectObject(paintera, e.getX(), e.getY(), false);}, - e -> modeState == ModeState.Selecting && + e -> modeState.get() == ModeState.Selecting && ((e.isSecondaryButtonDown() && keyTracker.noKeysActive()) || (e.isPrimaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL)))) .handler()); @@ -259,10 +262,10 @@ public void enterMode(final PainteraBaseView paintera, final ViewerPanelFX viewe e.printStackTrace(); } - modeState = ModeState.Selecting; + modeState.set(ModeState.Selecting); } - public void exitMode(final PainteraBaseView paintera) + public void exitMode(final PainteraBaseView paintera, final boolean completed) { if (!isModeOn()) { @@ -272,28 +275,47 @@ public void exitMode(final PainteraBaseView paintera) LOG.info("Exiting shape interpolation mode"); setDisableOtherViewers(false); - paintera.allowedActionsProperty().set(lastAllowedActions); - lastAllowedActions = null; + if (!completed) // extra cleanup if the mode is aborted + { + if (workerThread != null) + { + workerThread.interrupt(); + try { + workerThread.join(); + } catch (final InterruptedException e) { + e.printStackTrace(); + } + } + + selectedIds.activate(lastActiveIds); + selectedIds.activateAlso(lastSelectedId); + + source.resetMasks(); + } final long newLabelId = mask.info.value.get(); converter.removeColor(newLabelId); - selectedIds.activate(lastActiveIds); - selectedIds.activateAlso(lastSelectedId); - lastSelectedId = Label.INVALID; - lastActiveIds = null; + + paintera.allowedActionsProperty().set(lastAllowedActions); + lastAllowedActions = null; + currentFillValue = 0; selectedObjects.clear(); sectionInfo1 = sectionInfo2 = null; - modeState = null; - forgetMask(); - activeViewer.get().requestRepaint(); + modeState.set(null); + workerThread = null; + lastSelectedId = Label.INVALID; + lastActiveIds = null; + mask = null; + + activeViewer.get().requestRepaint(); activeViewer.set(null); } public boolean isModeOn() { - return modeState != null; + return modeState.get() != null; } private void createMask() throws MaskInUse @@ -306,12 +328,6 @@ private void createMask() throws MaskInUse LOG.info("Generated mask for shape interpolation using new label ID {}", newLabelId); } - private void forgetMask() - { - mask = null; - source.resetMasks(); - } - private void setDisableOtherViewers(final boolean disable) { final Parent parent = activeViewer.get().getParent(); @@ -349,22 +365,19 @@ private void fixSelection(final PainteraBaseView paintera) { LOG.debug("Fix selection in the second section"); sectionInfo2 = createSectionInfo(); - modeState = ModeState.Interpolating; - interpolateBetweenSections(); - modeState = ModeState.Review; - paintera.allowedActionsProperty().set(allowedActions); + interpolateBetweenSections(paintera); } } private void applyMask(final PainteraBaseView paintera) { - final Interval sectionUnionSourceInterval = Intervals.union( + final Interval sectionsUnionSourceInterval = Intervals.union( sectionInfo1.sourceBoundingBox, sectionInfo2.sourceBoundingBox ); - System.out.println("applying mask using affected interval of size " + Arrays.toString(Intervals.dimensionsAsLongArray(sectionUnionSourceInterval))); - source.applyMask(mask, sectionUnionSourceInterval, FOREGROUND_CHECK); - exitMode(paintera); + LOG.info("Applying interpolated mask using bounding box of size {}", Intervals.dimensionsAsLongArray(sectionsUnionSourceInterval)); + source.applyMask(source.getCurrentMask(), sectionsUnionSourceInterval, FOREGROUND_CHECK); + exitMode(paintera, true); } private SectionInfo createSectionInfo() @@ -384,112 +397,156 @@ private SectionInfo createSectionInfo() ); } - private void testOutput(final RandomAccessibleInterval<UnsignedLongType> mask, final String dataset) + @SuppressWarnings("unchecked") + private void interpolateBetweenSections(final PainteraBaseView paintera) { - try + modeState.set(ModeState.Interpolating); + + workerThread = new Thread(() -> { - N5Utils.save( - Converters.convert(mask, (in, out) -> out.setInteger(in.get()), new UnsignedShortType()), - new N5FSWriter("/groups/saalfeld/home/pisarevi/data/paintera-test/test.n5"), - dataset, - mask.numDimensions() == 2 ? new int[] {4096, 4096} : new int[] {256, 256, 256}, - new GzipCompression() + final SectionInfo[] sectionInfoPair = {sectionInfo1, sectionInfo2}; + + final Interval affectedUnionSourceInterval = Intervals.union( + sectionInfoPair[0].sourceBoundingBox, + sectionInfoPair[1].sourceBoundingBox ); - } - catch (final IOException e) - { - e.printStackTrace(); - } - } - @SuppressWarnings("unchecked") - private void interpolateBetweenSections() - { - final SectionInfo[] sectionInfoPair = {sectionInfo1, sectionInfo2}; + final Interval[] displaySectionIntervalPair = new Interval[2]; + final RandomAccessibleInterval<UnsignedLongType>[] sectionPair = new RandomAccessibleInterval[2]; + for (int i = 0; i < 2; ++i) + { + final SectionInfo newSectionInfo = new SectionInfo(sectionInfoPair[i].sourceToDisplayTransform, affectedUnionSourceInterval); + final RandomAccessibleInterval<UnsignedLongType> section = getTransformedMaskSection(newSectionInfo); + displaySectionIntervalPair[i] = new FinalInterval(section); + sectionPair[i] = new ArrayImgFactory<>(new UnsignedLongType()).create(section); + final Cursor<UnsignedLongType> srcCursor = Views.flatIterable(section).cursor(); + final Cursor<UnsignedLongType> dstCursor = Views.flatIterable(sectionPair[i]).cursor(); + while (dstCursor.hasNext() || srcCursor.hasNext()) + dstCursor.next().set(srcCursor.next()); + } - // find the interval that encompasses both of the sections - final Interval sectionsUnionInterval = Intervals.union( - getSectionInterval(sectionInfoPair[0]), - getSectionInterval(sectionInfoPair[1]) - ); + // compute distance transform on both sections + final RandomAccessibleInterval<FloatType>[] distanceTransformPair = new RandomAccessibleInterval[2]; + for (int i = 0; i < 2; ++i) + { + distanceTransformPair[i] = new ArrayImgFactory<>(new FloatType()).create(sectionPair[i]); + final RandomAccessibleInterval<BoolType> binarySection = Converters.convert(sectionPair[i], new PredicateConverter<>(FOREGROUND_CHECK), new BoolType()); + computeSignedDistanceTransform(binarySection, distanceTransformPair[i], DISTANCE_TYPE.EUCLIDIAN); + } - // extract the sections using the union interval - final RandomAccessibleInterval<UnsignedLongType>[] sectionPair = new RandomAccessibleInterval[2]; - for (int i = 0; i < 2; ++i) - sectionPair[i] = getMaskSection(sectionInfoPair[i].sourceToDisplayTransform, sectionsUnionInterval); + final double distanceBetweenSections = computeDistanceBetweenSections(sectionInfo1, sectionInfo2); + final AffineTransform3D transformToSource = new AffineTransform3D(); + transformToSource + .preConcatenate(new Translation3D(displaySectionIntervalPair[0].min(0), displaySectionIntervalPair[0].min(1), 0)) + .preConcatenate(sectionInfo1.sourceToDisplayTransform.inverse()); + + final RealRandomAccessible<UnsignedLongType> interpolatedShapeMask = getInterpolatedDistanceTransformMask( + distanceTransformPair[0], + distanceTransformPair[1], + distanceBetweenSections, + new UnsignedLongType(1), + transformToSource + ); - // replace fill values in the sections with a single value because we do not want to interpolate between multiple labels - for (final RandomAccessibleInterval<UnsignedLongType> section : sectionPair) - for (final UnsignedLongType val : Views.iterable(section)) - if (FOREGROUND_CHECK.test(val)) - val.set(INTERPOLATION_FILL_VALUE); + final RealRandomAccessible<VolatileUnsignedLongType> volatileInterpolatedShapeMask = getInterpolatedDistanceTransformMask( + distanceTransformPair[0], + distanceTransformPair[1], + distanceBetweenSections, + new VolatileUnsignedLongType(1), + transformToSource + ); + try + { + synchronized (source) + { + source.resetMasks(); + source.setMask( + mask.info, + interpolatedShapeMask, + volatileInterpolatedShapeMask, + FOREGROUND_CHECK + ); + } + activeViewer.get().requestRepaint(); + } + catch (final MaskInUse e) + { + LOG.error("Label source already has an active mask"); + } -// testOutput(sectionPair[0], "section1"); -// testOutput(sectionPair[1], "section2"); + InvokeOnJavaFXApplicationThread.invoke(() -> { + modeState.set(ModeState.Review); + paintera.allowedActionsProperty().set(allowedActions); + }); + }); + workerThread.start(); + } + + private static <R extends RealType<R> & NativeType<R>, B extends BooleanType<B>> void computeSignedDistanceTransform( + final RandomAccessibleInterval<B> mask, + final RandomAccessibleInterval<R> target, + final DISTANCE_TYPE distanceType, + final double... weights) + { + final RandomAccessibleInterval<R> distanceOutside = target; + final RandomAccessibleInterval<R> distanceInside = new ArrayImgFactory<>(Util.getTypeFromInterval(target)).create(target); + DistanceTransform.binaryTransform(mask, distanceOutside, distanceType, weights); + DistanceTransform.binaryTransform(Logical.complement(mask), distanceInside, distanceType, weights); + LoopBuilder.setImages(distanceOutside, distanceInside, target).forEachPixel((outside, inside, result) -> { + switch (distanceType) + { + case EUCLIDIAN: + result.setReal(Math.sqrt(outside.getRealDouble()) - Math.sqrt(inside.getRealDouble())); + break; + case L1: + result.setReal(outside.getRealDouble() - inside.getRealDouble()); + break; + } + }); + } + private static <R extends RealType<R>, T extends NativeType<T> & RealType<T>> RealRandomAccessible<T> getInterpolatedDistanceTransformMask( + final RandomAccessibleInterval<R> dt1, + final RandomAccessibleInterval<R> dt2, + final double distance, + final T targetValue, + final AffineTransform3D transformToSource) + { + final RandomAccessibleInterval<R> distanceTransformStack = Views.stack(dt1, dt2); - // find the distance between the sections and generate the filler sections between them - final double distanceBetweenSections = computeDistanceBetweenSections(); - final int numFillers = (int) Math.round(Math.abs(distanceBetweenSections)) - 1; - final double fillerStep = distanceBetweenSections / (numFillers + 1); - final RandomAccessibleInterval<UnsignedLongType>[] fillers = new RandomAccessibleInterval[numFillers]; - System.out.println("Distance between the sections: " + distanceBetweenSections); - System.out.println("Creating " + numFillers + " fillers of size " + Arrays.toString(Intervals.dimensionsAsLongArray(sectionsUnionInterval)) + ":"); - for (int i = 0; i < numFillers; ++i) - { - final double fillerShift = fillerStep * (i + 1); - final AffineTransform3D fillerTransform = sectionInfo1.sourceToDisplayTransform.copy(); - fillerTransform.preConcatenate(new Translation3D(0, 0, fillerShift)); - fillers[i] = getMaskSection(fillerTransform, sectionsUnionInterval); - System.out.println(" " + fillerShift); - } + final R extendValue = Util.getTypeFromInterval(distanceTransformStack).createVariable(); + extendValue.setReal(extendValue.getMaxValue()); + final RealRandomAccessible<R> interpolatedDistanceTransform = Views.interpolate( + Views.extendValue(distanceTransformStack, extendValue), + new NLinearInterpolatorFactory<>() + ); + final double distanceSign = Math.signum(distance); + final double padding = 0.5; // slightly stretches the mask past the end sections to ensure that it's visible in the current plane + final RealRandomAccessible<R> scaledInterpolatedDistanceTransform = RealViews.affineReal( + interpolatedDistanceTransform, + new AffineTransform3D() + .preConcatenate(new Scale3D(1, 1, -(distance + padding * distanceSign))) + .preConcatenate(new Translation3D(0, 0, padding * 0.5 * distanceSign)) + ); - final List<RandomAccessibleInterval<UnsignedLongType>> sectionsList = new ArrayList<>(); - sectionsList.add(sectionPair[0]); - sectionsList.addAll(Arrays.asList(fillers)); - sectionsList.add(sectionPair[1]); - - System.out.println("writing debug output before interpolating..."); - testOutput(Views.stack(sectionsList), "sections-before"); - System.out.println("done"); - - System.out.println("Running interpolation..."); - InterpolateBetweenSections.interpolateBetweenSections( - sectionPair[0], - sectionPair[1], - new ArrayImgFactory<>(new DoubleType()), - fillers, - DISTANCE_TYPE.EUCLIDIAN, - new double[] {1}, - Label.BACKGROUND + final T emptyValue = targetValue.createVariable(); + final RealRandomAccessible<T> interpolatedShape = Converters.convert( + scaledInterpolatedDistanceTransform, + (in, out) -> out.set(in.getRealDouble() <= 0 ? targetValue : emptyValue), + emptyValue.createVariable() ); - System.out.println("Done"); - System.out.println("writing debug output after interpolating..."); - testOutput(Views.stack(sectionsList), "sections-after"); - System.out.println("done"); + return RealViews.affineReal(interpolatedShape, transformToSource); } - private Interval getSectionInterval(final SectionInfo sectionInfo) + private RandomAccessibleInterval<UnsignedLongType> getTransformedMaskSection(final SectionInfo sectionInfo) { final RealInterval sectionBounds = sectionInfo.sourceToDisplayTransform.estimateBounds(sectionInfo.sourceBoundingBox); - final Interval sectionInterval3D = Intervals.smallestContainingInterval(sectionBounds); - return new FinalInterval( - new long[] {sectionInterval3D.min(0), sectionInterval3D.min(1)}, - new long[] {sectionInterval3D.max(0), sectionInterval3D.max(1)} - ); - } - - private RandomAccessibleInterval<UnsignedLongType> getMaskSection(final AffineTransform3D transform, final Interval sectionInterval) - { - final Interval sectionInterval3D = sectionInterval.numDimensions() == 3 ? sectionInterval : new FinalInterval( - new long[] {sectionInterval.min(0), sectionInterval.min(1), 0l}, - new long[] {sectionInterval.max(0), sectionInterval.max(1), 0l} - ); - final RealRandomAccessible<UnsignedLongType> transformedMask = getTransformedMask(transform); - final RandomAccessibleInterval<UnsignedLongType> transformedMaskInterval = Views.interval(Views.raster(transformedMask), sectionInterval3D); + final Interval sectionInterval = Intervals.smallestContainingInterval(sectionBounds); + final RealRandomAccessible<UnsignedLongType> transformedMask = getTransformedMask(sectionInfo.sourceToDisplayTransform); + final RandomAccessibleInterval<UnsignedLongType> transformedMaskInterval = Views.interval(Views.raster(transformedMask), sectionInterval); return Views.hyperSlice(transformedMaskInterval, 2, 0l); } @@ -502,11 +559,11 @@ private RealRandomAccessible<UnsignedLongType> getTransformedMask(final AffineTr return RealViews.affine(interpolatedMask, transform); } - private double computeDistanceBetweenSections() + private static double computeDistanceBetweenSections(final SectionInfo s1, final SectionInfo s2) { final double[] pos1 = new double[3], pos2 = new double[3]; - sectionInfo1.sourceToDisplayTransform.apply(pos1, pos1); - sectionInfo2.sourceToDisplayTransform.apply(pos2, pos2); + s1.sourceToDisplayTransform.apply(pos1, pos1); + s2.sourceToDisplayTransform.apply(pos2, pos2); return pos2[2] - pos1[2]; // We care only about the shift between the sections (Z distance in the viewer) } @@ -593,7 +650,9 @@ private UnsignedLongType getMaskValue(final double x, final double y) private AffineTransform3D getMaskTransform() { final AffineTransform3D maskTransform = new AffineTransform3D(); - source.getSourceTransform(mask.info.t, mask.info.level, maskTransform); + final int time = activeViewer.get().getState().timepointProperty().get(); + final int level = MASK_SCALE_LEVEL; + source.getSourceTransform(time, level, maskTransform); return maskTransform; } @@ -616,14 +675,16 @@ private AffineTransform3D getMaskDisplayTransform() * @param level * @return */ - private AffineTransform3D getMaskDisplayTransformIgnoreScaling(final int level) + private AffineTransform3D getMaskDisplayTransformIgnoreScaling(final int targetLevel) { final AffineTransform3D maskMipmapDisplayTransform = getMaskDisplayTransformIgnoreScaling(); - if (level != mask.info.level) + final int maskLevel = MASK_SCALE_LEVEL; + if (targetLevel != maskLevel) { // scale with respect to the given mipmap level - final Scale3D relativeScaleTransform = new Scale3D(DataSource.getRelativeScales(source, mask.info.t, level, mask.info.level)); - maskMipmapDisplayTransform.preConcatenate(relativeScaleTransform); + final int time = activeViewer.get().getState().timepointProperty().get(); + final Scale3D relativeScaleTransform = new Scale3D(DataSource.getRelativeScales(source, time, maskLevel, targetLevel)); + maskMipmapDisplayTransform.preConcatenate(relativeScaleTransform.inverse()); } return maskMipmapDisplayTransform; } @@ -642,7 +703,9 @@ private AffineTransform3D getMaskDisplayTransformIgnoreScaling() Arrays.setAll(viewerScale, d -> Affine3DHelpers.extractScale(viewerTransform, d)); final Scale3D scalingTransform = new Scale3D(viewerScale); // neutralize mask scaling if there is any - scalingTransform.concatenate(new Scale3D(DataSource.getScale(source, mask.info.t, mask.info.level))); + final int time = activeViewer.get().getState().timepointProperty().get(); + final int level = MASK_SCALE_LEVEL; + scalingTransform.concatenate(new Scale3D(DataSource.getScale(source, time, level))); // build the resulting transform return viewerTransform.preConcatenate(scalingTransform.inverse()).concatenate(getMaskTransform()); } From bb4031716001b512af2183e7403c34788ae2b49c Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Fri, 31 May 2019 11:31:58 -0400 Subject: [PATCH 30/84] display progress indicator when running shape interpolation --- .../paintera/control/ShapeInterpolationMode.java | 7 ++++++- .../saalfeldlab/paintera/state/LabelSourceState.java | 9 +++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java index 7c0ff6a95..b48e51840 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java @@ -82,7 +82,7 @@ public class ShapeInterpolationMode<D extends IntegerType<D>> { private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - private static enum ModeState + public static enum ModeState { Selecting, Interpolating, @@ -178,6 +178,11 @@ public ObjectProperty<ViewerPanelFX> activeViewerProperty() return activeViewer; } + public ObjectProperty<ModeState> modeStateProperty() + { + return modeState; + } + public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final KeyTracker keyTracker) { final DelegateEventHandlers.AnyHandler filter = DelegateEventHandlers.handleAny(); diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java index 97ecdd837..719e11f39 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java @@ -53,6 +53,7 @@ import org.janelia.saalfeldlab.paintera.composition.ARGBCompositeAlphaYCbCr; import org.janelia.saalfeldlab.paintera.composition.Composite; import org.janelia.saalfeldlab.paintera.control.ShapeInterpolationMode; +import org.janelia.saalfeldlab.paintera.control.ShapeInterpolationMode.ModeState; import org.janelia.saalfeldlab.paintera.control.assignment.FragmentSegmentAssignmentOnlyLocal; import org.janelia.saalfeldlab.paintera.control.assignment.FragmentSegmentAssignmentState; import org.janelia.saalfeldlab.paintera.control.lock.LockedSegmentsOnlyLocal; @@ -606,6 +607,14 @@ private HBox createDisplayStatus() }); }); + this.shapeInterpolationMode.modeStateProperty().addListener((obs, oldv, newv) -> { + InvokeOnJavaFXApplicationThread.invoke(() -> { + paintingProgressIndicator.setVisible(newv == ModeState.Interpolating); + if (newv == ModeState.Interpolating) + paintingProgressIndicatorTooltip.setText("Interpolating between sections..."); + }); + }); + final HBox displayStatus = new HBox(5, lastSelectedLabelColorRect, paintingProgressIndicator From ee86f22833250242d1ffbe227503436450ab23a1 Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Fri, 31 May 2019 12:35:28 -0400 Subject: [PATCH 31/84] check if shape interpolation thread was interrupted for early exit --- .../saalfeldlab/paintera/control/ShapeInterpolationMode.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java index b48e51840..d3810469e 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java @@ -434,6 +434,8 @@ private void interpolateBetweenSections(final PainteraBaseView paintera) final RandomAccessibleInterval<FloatType>[] distanceTransformPair = new RandomAccessibleInterval[2]; for (int i = 0; i < 2; ++i) { + if (Thread.currentThread().isInterrupted()) + return; distanceTransformPair[i] = new ArrayImgFactory<>(new FloatType()).create(sectionPair[i]); final RandomAccessibleInterval<BoolType> binarySection = Converters.convert(sectionPair[i], new PredicateConverter<>(FOREGROUND_CHECK), new BoolType()); computeSignedDistanceTransform(binarySection, distanceTransformPair[i], DISTANCE_TYPE.EUCLIDIAN); @@ -461,6 +463,9 @@ private void interpolateBetweenSections(final PainteraBaseView paintera) transformToSource ); + if (Thread.currentThread().isInterrupted()) + return; + try { synchronized (source) From 4357e44c71b853c13d58f9d113f25ed69f1723dd Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Fri, 31 May 2019 12:37:45 -0400 Subject: [PATCH 32/84] disable menu actions in shape interpolation mode except viewer maximize/minimize --- .../paintera/BorderPaneWithStatusBars.java | 5 +++ .../paintera/PainteraDefaultHandlers.java | 40 +++++++++++-------- .../control/ShapeInterpolationMode.java | 7 +++- .../control/actions/AllowedActions.java | 13 +++++- .../paintera/control/actions/MenuAction.java | 29 ++++++++++++++ 5 files changed, 73 insertions(+), 21 deletions(-) create mode 100644 src/main/java/org/janelia/saalfeldlab/paintera/control/actions/MenuAction.java diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/BorderPaneWithStatusBars.java b/src/main/java/org/janelia/saalfeldlab/paintera/BorderPaneWithStatusBars.java index cab1b2a7a..b1c0c9f53 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/BorderPaneWithStatusBars.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/BorderPaneWithStatusBars.java @@ -279,6 +279,11 @@ public BorderPaneWithStatusBars( resizeSideBar = new ResizeOnLeftSide(sideBar, sideBar.prefWidthProperty(), dist -> Math.abs(dist) < 5); } + public boolean isSideBarActive() + { + return pane.getRight() != null; + } + public void toggleSideBar() { if (pane.getRight() == null) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/PainteraDefaultHandlers.java b/src/main/java/org/janelia/saalfeldlab/paintera/PainteraDefaultHandlers.java index 46037f7b4..28e5a8c88 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/PainteraDefaultHandlers.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/PainteraDefaultHandlers.java @@ -46,6 +46,7 @@ import org.janelia.saalfeldlab.paintera.control.OrthogonalViewsValueDisplayListener; import org.janelia.saalfeldlab.paintera.control.RunWhenFirstElementIsAdded; import org.janelia.saalfeldlab.paintera.control.ShowOnlySelectedInStreamToggle; +import org.janelia.saalfeldlab.paintera.control.actions.MenuAction; import org.janelia.saalfeldlab.paintera.control.navigation.AffineTransformWithListeners; import org.janelia.saalfeldlab.paintera.control.navigation.DisplayTransformUpdateOnResize; import org.janelia.saalfeldlab.paintera.state.SourceInfo; @@ -248,12 +249,17 @@ public PainteraDefaultHandlers( final EventFX<KeyEvent> toggleSideBar = EventFX.KEY_RELEASED( "toggle sidebar", e -> paneWithStatus.toggleSideBar(), - e -> keyTracker.areOnlyTheseKeysDown(KeyCode.P) - ); + e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.SidePanel) && keyTracker.areOnlyTheseKeysDown(KeyCode.P) + ); borderPane.sceneProperty().addListener((obs, oldv, newv) -> newv.addEventHandler( KeyEvent.KEY_PRESSED, toggleSideBar)); + baseView.allowedActionsProperty().addListener((obs, oldv, newv) -> { + if (!newv.isAllowed(MenuAction.SidePanel) && paneWithStatus.isSideBarActive()) + paneWithStatus.toggleSideBar(); + }); + EventFX.KEY_PRESSED( "toggle interpolation", e -> toggleInterpolation(), @@ -261,11 +267,11 @@ public PainteraDefaultHandlers( EventFX.KEY_PRESSED( "cycle current source", e -> sourceInfo.incrementCurrentSourceIndex(), - e -> keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL, KeyCode.TAB)).installInto(borderPane); + e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.ChangeActiveSource) && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL, KeyCode.TAB)).installInto(borderPane); EventFX.KEY_PRESSED( "backwards cycle current source", e -> sourceInfo.decrementCurrentSourceIndex(), - e -> keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL, KeyCode.SHIFT, KeyCode.TAB)).installInto(borderPane); + e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.ChangeActiveSource) && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL, KeyCode.SHIFT, KeyCode.TAB)).installInto(borderPane); this.resizer = new GridResizer(gridConstraintsManager, 5, baseView.pane(), keyTracker); this.resizer.installInto(baseView.pane()); @@ -296,19 +302,19 @@ public PainteraDefaultHandlers( EventFX.KEY_PRESSED( "maximize", e -> toggleMaximizeTopLeft.toggleFullScreen(), - e -> keyTracker.areOnlyTheseKeysDown(KeyCode.M)).installInto(orthogonalViews.topLeft().viewer()); + e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.ToggleViewerMaximizedMinimized) && keyTracker.areOnlyTheseKeysDown(KeyCode.M)).installInto(orthogonalViews.topLeft().viewer()); EventFX.KEY_PRESSED( "maximize", e -> toggleMaximizeTopRight.toggleFullScreen(), - e -> keyTracker.areOnlyTheseKeysDown(KeyCode.M)).installInto(orthogonalViews.topRight().viewer()); + e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.ToggleViewerMaximizedMinimized) && keyTracker.areOnlyTheseKeysDown(KeyCode.M)).installInto(orthogonalViews.topRight().viewer()); EventFX.KEY_PRESSED( "maximize", e -> toggleMaximizeBottomLeft.toggleFullScreen(), - e -> keyTracker.areOnlyTheseKeysDown(KeyCode.M)).installInto(orthogonalViews.bottomLeft().viewer()); + e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.ToggleViewerMaximizedMinimized) && keyTracker.areOnlyTheseKeysDown(KeyCode.M)).installInto(orthogonalViews.bottomLeft().viewer()); EventFX.KEY_PRESSED( "maximize", e -> toggleMaximizeBottomRight.toggleFullScreen(), - e -> keyTracker.areOnlyTheseKeysDown(KeyCode.M)).installInto(baseView.viewer3D()); + e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.ToggleViewerMaximizedMinimized) && keyTracker.areOnlyTheseKeysDown(KeyCode.M)).installInto(baseView.viewer3D()); final CurrentSourceVisibilityToggle csv = new CurrentSourceVisibilityToggle(sourceInfo.currentState()); EventFX.KEY_PRESSED( @@ -326,10 +332,8 @@ public PainteraDefaultHandlers( EventFX.KEY_PRESSED( "toggle maximize bottom row", - e -> { - gridConstraintsManager.maximize(MaximizedRow.BOTTOM, 0); - }, - e -> keyTracker.areOnlyTheseKeysDown(KeyCode.M, KeyCode.SHIFT)).installInto(paneWithStatus.getPane()); + e -> gridConstraintsManager.maximize(MaximizedRow.BOTTOM, 0), + e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.ToggleViewerAndOrthoslicesView) && keyTracker.areOnlyTheseKeysDown(KeyCode.M, KeyCode.SHIFT)).installInto(paneWithStatus.getPane()); bottomLeftNeedsZNormal = Bindings.createBooleanBinding( () -> MaximizedColumn.NONE.equals(gridConstraintsManager.getMaximizedColumn()) && MaximizedRow.BOTTOM @@ -363,8 +367,10 @@ public PainteraDefaultHandlers( MouseEvent.MOUSE_CLICKED, e -> { LOG.debug("Handling event {}", e); - if (MouseButton.SECONDARY.equals(e.getButton()) && e.getClickCount() == 1 && !mouseTracker - .isDragging()) + if (baseView.allowedActionsProperty().get().isAllowed(MenuAction.OrthoslicesContextMenu) && + MouseButton.SECONDARY.equals(e.getButton()) && + e.getClickCount() == 1 && + !mouseTracker.isDragging()) { LOG.debug("Check passed for event {}", e); e.consume(); @@ -380,7 +386,7 @@ public PainteraDefaultHandlers( projectDirectory, Exceptions.handler("Paintera", "Unable to create new Dataset"), baseView.sourceInfo().currentSourceProperty().get()), - e -> keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL, KeyCode.SHIFT, KeyCode.N)).installInto(paneWithStatus.getPane()); + e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.CreateNewLabelSource) && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL, KeyCode.SHIFT, KeyCode.N)).installInto(paneWithStatus.getPane()); } private final Map<ViewerPanelFX, ViewerAndTransforms> viewerToTransforms = new HashMap<>(); @@ -510,10 +516,10 @@ public static EventHandler<KeyEvent> addOpenDatasetContextMenuHandler( assert triggers.length > 0; - EventHandler<KeyEvent> handler = OpenDialogMenu.keyPressedHandler( + final EventHandler<KeyEvent> handler = OpenDialogMenu.keyPressedHandler( target, exception -> Exceptions.exceptionAlert(Paintera.NAME, "Unable to show open dataset menu", exception), - e -> keyTracker.areOnlyTheseKeysDown(triggers), + e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.AddSource) && keyTracker.areOnlyTheseKeysDown(triggers), "Open dataset", baseView, projectDirectory, diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java index d3810469e..0087dcb4f 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java @@ -12,6 +12,7 @@ import org.janelia.saalfeldlab.paintera.PainteraBaseView; import org.janelia.saalfeldlab.paintera.control.actions.AllowedActions; import org.janelia.saalfeldlab.paintera.control.actions.LabelAction; +import org.janelia.saalfeldlab.paintera.control.actions.MenuAction; import org.janelia.saalfeldlab.paintera.control.actions.NavigationAction; import org.janelia.saalfeldlab.paintera.control.actions.PaintAction; import org.janelia.saalfeldlab.paintera.control.paint.FloodFill2D; @@ -120,12 +121,14 @@ private static final class SectionInfo allowedActions = new AllowedActions( NavigationAction.of(NavigationAction.Drag, NavigationAction.Zoom, NavigationAction.Scroll), LabelAction.none(), - PaintAction.none() + PaintAction.none(), + MenuAction.of(MenuAction.ToggleViewerMaximizedMinimized) ); allowedActionsWhenSelected = new AllowedActions( NavigationAction.of(NavigationAction.Drag, NavigationAction.Zoom), LabelAction.none(), - PaintAction.none() + PaintAction.none(), + MenuAction.of(MenuAction.ToggleViewerMaximizedMinimized) ); } diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/AllowedActions.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/AllowedActions.java index 9a1d899aa..aca57c552 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/AllowedActions.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/AllowedActions.java @@ -10,15 +10,18 @@ public final class AllowedActions private final EnumSet<NavigationAction> navigationAllowedActions; private final EnumSet<LabelAction> labelAllowedActions; private final EnumSet<PaintAction> paintAllowedActions; + private final EnumSet<MenuAction> menuAllowedActions; public AllowedActions( final EnumSet<NavigationAction> navigationAllowedActions, final EnumSet<LabelAction> labelAllowedActions, - final EnumSet<PaintAction> paintAllowedActions) + final EnumSet<PaintAction> paintAllowedActions, + final EnumSet<MenuAction> menuAllowedActions) { this.navigationAllowedActions = navigationAllowedActions; this.labelAllowedActions = labelAllowedActions; this.paintAllowedActions = paintAllowedActions; + this.menuAllowedActions = menuAllowedActions; } public boolean isAllowed(final NavigationAction navigationAction) @@ -36,12 +39,18 @@ public boolean isAllowed(final PaintAction paintAction) return this.paintAllowedActions.contains(paintAction); } + public boolean isAllowed(final MenuAction menuAction) + { + return this.menuAllowedActions.contains(menuAction); + } + public static AllowedActions all() { return new AllowedActions( NavigationAction.all(), LabelAction.all(), - PaintAction.all() + PaintAction.all(), + MenuAction.all() ); } } diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/MenuAction.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/MenuAction.java new file mode 100644 index 000000000..d1c1f6d5c --- /dev/null +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/MenuAction.java @@ -0,0 +1,29 @@ +package org.janelia.saalfeldlab.paintera.control.actions; + +import java.util.EnumSet; + +public enum MenuAction +{ + AddSource, + CreateNewLabelSource, + ChangeActiveSource, + SidePanel, + ToggleViewerMaximizedMinimized, + ToggleViewerAndOrthoslicesView, + OrthoslicesContextMenu; + + public static EnumSet<MenuAction> of(final MenuAction first, final MenuAction... rest) + { + return EnumSet.of(first, rest); + } + + public static EnumSet<MenuAction> all() + { + return EnumSet.allOf(MenuAction.class); + } + + public static EnumSet<MenuAction> none() + { + return EnumSet.noneOf(MenuAction.class); + } +} From 83c3b5e889d0044fdd4948771f001a26b907f931 Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Fri, 31 May 2019 13:15:32 -0400 Subject: [PATCH 33/84] properly ignore clicks outside the mask in shape interpolation mode --- .../paintera/control/ShapeInterpolationMode.java | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java index 0087dcb4f..dc435172a 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java @@ -566,7 +566,7 @@ private RandomAccessibleInterval<UnsignedLongType> getTransformedMaskSection(fin private RealRandomAccessible<UnsignedLongType> getTransformedMask(final AffineTransform3D transform) { final RealRandomAccessible<UnsignedLongType> interpolatedMask = Views.interpolate( - Views.extendValue(mask.mask, new UnsignedLongType(Label.BACKGROUND)), + Views.extendValue(mask.mask, new UnsignedLongType(Label.OUTSIDE)), new NearestNeighborInterpolatorFactory<>() ); return RealViews.affine(interpolatedMask, transform); @@ -582,7 +582,11 @@ private static double computeDistanceBetweenSections(final SectionInfo s1, final private void selectObject(final PainteraBaseView paintera, final double x, final double y, final boolean deactivateOthers) { - final boolean wasSelected = isSelected(x, y); + final UnsignedLongType maskValue = getMaskValue(x, y); + if (maskValue.get() == Label.OUTSIDE) + return; + + final boolean wasSelected = FOREGROUND_CHECK.test(maskValue); final int numSelectedObjects = selectedObjects.size(); LOG.debug("Object was clicked: deactivateOthers={}, wasSelected={}, numSelectedObjects", deactivateOthers, wasSelected, numSelectedObjects); @@ -646,15 +650,10 @@ private long runFloodFillToDeselect(final double x, final double y) return maskValue; } - private boolean isSelected(final double x, final double y) - { - return FOREGROUND_CHECK.test(getMaskValue(x, y)); - } - private UnsignedLongType getMaskValue(final double x, final double y) { final RealPoint sourcePos = getSourceCoordinates(x, y); - final RandomAccess<UnsignedLongType> maskAccess = mask.mask.randomAccess(); + final RandomAccess<UnsignedLongType> maskAccess = Views.extendValue(mask.mask, new UnsignedLongType(Label.OUTSIDE)).randomAccess(); for (int d = 0; d < sourcePos.numDimensions(); ++d) maskAccess.setPosition(Math.round(sourcePos.getDoublePosition(d)), d); return maskAccess.get(); From 3a1782079ef8b1c52bd2ca3d3c5dd2db95467b63 Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Fri, 31 May 2019 13:39:21 -0400 Subject: [PATCH 34/84] use different color for fixed selection in shape interpolation mode --- .../saalfeldlab/paintera/control/ShapeInterpolationMode.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java index dc435172a..5936ff15f 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java @@ -139,6 +139,7 @@ private static final class SectionInfo private static final int SHAPE_INTERPOLATION_SCALE_LEVEL = MASK_SCALE_LEVEL; private static final Color MASK_COLOR = Color.web("00CCFF"); + private static final Color MASK_COLOR_FIXED_SELECTION = Color.web("44AACC"); private static final Predicate<UnsignedLongType> FOREGROUND_CHECK = t -> t.get() > 0; @@ -367,6 +368,8 @@ private void fixSelection(final PainteraBaseView paintera) LOG.debug("Fix selection in the first section"); sectionInfo1 = createSectionInfo(); selectedObjects.clear(); + converter.setColor(mask.info.value.get(), MASK_COLOR_FIXED_SELECTION); + activeViewerProperty().get().requestRepaint(); paintera.allowedActionsProperty().set(allowedActions); } else @@ -588,6 +591,7 @@ private void selectObject(final PainteraBaseView paintera, final double x, final final boolean wasSelected = FOREGROUND_CHECK.test(maskValue); final int numSelectedObjects = selectedObjects.size(); + converter.setColor(mask.info.value.get(), MASK_COLOR); LOG.debug("Object was clicked: deactivateOthers={}, wasSelected={}, numSelectedObjects", deactivateOthers, wasSelected, numSelectedObjects); From bd6e2e4550d430ed5986dea212c3889b9c647d56 Mon Sep 17 00:00:00 2001 From: Igor Pisarev <igorpisarev@users.noreply.github.com> Date: Fri, 31 May 2019 15:35:39 -0400 Subject: [PATCH 35/84] add shape interpolation mode shortcuts to readme --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index a165cce34..e4573aa67 100644 --- a/README.md +++ b/README.md @@ -162,6 +162,8 @@ Usage: Paintera [-h] [--default-to-temp-directory] [--print-error-codes] | `F` + left click | 2D Flood-fill in current viewer plane with id that was last toggled active (if any) | | `Shift` + `F` + left click | Flood-fill with id that was last toggled active (if any) | | `N` | Select new, previously unused id | +| `S` | Enter/advance shape interpolation mode | +| `ESC` | Exit shape interpolation mode | | `Ctrl` + `C` | Show dialog to commit canvas and/or assignments | | `C` | Increment ARGB stream seed by one | | `Shift` + `C` | Decrement ARGB stream seed by one | From a970c085800d55289e7fba70bd3cea62df6c6e7b Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Fri, 31 May 2019 16:18:52 -0400 Subject: [PATCH 36/84] use two separate masks for selecting objects in shape interpolation mode --- .../control/ShapeInterpolationMode.java | 62 +++++++++++-------- 1 file changed, 36 insertions(+), 26 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java index 5936ff15f..7e2168e7e 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java @@ -104,11 +104,13 @@ private static final class SelectedObjectInfo private static final class SectionInfo { + final Mask<UnsignedLongType> mask; final AffineTransform3D sourceToDisplayTransform; final Interval sourceBoundingBox; - SectionInfo(final AffineTransform3D sourceToDisplayTransform, final Interval sourceBoundingBox) + SectionInfo(final Mask<UnsignedLongType> mask, final AffineTransform3D sourceToDisplayTransform, final Interval sourceBoundingBox) { + this.mask = mask; this.sourceToDisplayTransform = sourceToDisplayTransform; this.sourceBoundingBox = sourceBoundingBox; } @@ -139,7 +141,6 @@ private static final class SectionInfo private static final int SHAPE_INTERPOLATION_SCALE_LEVEL = MASK_SCALE_LEVEL; private static final Color MASK_COLOR = Color.web("00CCFF"); - private static final Color MASK_COLOR_FIXED_SELECTION = Color.web("44AACC"); private static final Predicate<UnsignedLongType> FOREGROUND_CHECK = t -> t.get() > 0; @@ -154,14 +155,14 @@ private static final class SectionInfo private long lastSelectedId; private long[] lastActiveIds; - private Mask<UnsignedLongType> mask; - private long currentFillValue; - private final TLongObjectMap<SelectedObjectInfo> selectedObjects = new TLongObjectHashMap<>(); + private long currentFillValue; private SectionInfo sectionInfo1, sectionInfo2; + private Mask<UnsignedLongType> mask; private ObjectProperty<ModeState> modeState = new SimpleObjectProperty<>(); + private long newLabelId; private Thread workerThread; @@ -257,19 +258,11 @@ public void enterMode(final PainteraBaseView paintera, final ViewerPanelFX viewe lastAllowedActions = paintera.allowedActionsProperty().get(); paintera.allowedActionsProperty().set(allowedActions); - try - { - createMask(); - lastSelectedId = selectedIds.getLastSelection(); - lastActiveIds = selectedIds.getActiveIds(); - final long newLabelId = mask.info.value.get(); - converter.setColor(newLabelId, MASK_COLOR); - selectedIds.activate(newLabelId); - } - catch (final MaskInUse e) - { - e.printStackTrace(); - } + lastSelectedId = selectedIds.getLastSelection(); + lastActiveIds = selectedIds.getActiveIds(); + newLabelId = idService.next(); + converter.setColor(newLabelId, MASK_COLOR); + selectedIds.activate(newLabelId); modeState.set(ModeState.Selecting); } @@ -302,8 +295,8 @@ public void exitMode(final PainteraBaseView paintera, final boolean completed) source.resetMasks(); } - final long newLabelId = mask.info.value.get(); converter.removeColor(newLabelId); + newLabelId = Label.INVALID; paintera.allowedActionsProperty().set(lastAllowedActions); lastAllowedActions = null; @@ -311,12 +304,12 @@ public void exitMode(final PainteraBaseView paintera, final boolean completed) currentFillValue = 0; selectedObjects.clear(); sectionInfo1 = sectionInfo2 = null; + mask = null; modeState.set(null); workerThread = null; lastSelectedId = Label.INVALID; lastActiveIds = null; - mask = null; activeViewer.get().requestRepaint(); activeViewer.set(null); @@ -331,10 +324,8 @@ private void createMask() throws MaskInUse { final int time = activeViewer.get().getState().timepointProperty().get(); final int level = MASK_SCALE_LEVEL; - final long newLabelId = idService.next(); final MaskInfo<UnsignedLongType> maskInfo = new MaskInfo<>(time, level, new UnsignedLongType(newLabelId)); mask = source.generateMask(maskInfo, FOREGROUND_CHECK); - LOG.info("Generated mask for shape interpolation using new label ID {}", newLabelId); } private void setDisableOtherViewers(final boolean disable) @@ -368,7 +359,8 @@ private void fixSelection(final PainteraBaseView paintera) LOG.debug("Fix selection in the first section"); sectionInfo1 = createSectionInfo(); selectedObjects.clear(); - converter.setColor(mask.info.value.get(), MASK_COLOR_FIXED_SELECTION); + source.resetMasks(); + mask = null; activeViewerProperty().get().requestRepaint(); paintera.allowedActionsProperty().set(allowedActions); } @@ -403,6 +395,7 @@ private SectionInfo createSectionInfo() selectionSourceBoundingBox = Intervals.union(selectionSourceBoundingBox, it.value().sourceBoundingBox); } return new SectionInfo( + mask, getMaskDisplayTransformIgnoreScaling(SHAPE_INTERPOLATION_SCALE_LEVEL), selectionSourceBoundingBox ); @@ -426,7 +419,7 @@ private void interpolateBetweenSections(final PainteraBaseView paintera) final RandomAccessibleInterval<UnsignedLongType>[] sectionPair = new RandomAccessibleInterval[2]; for (int i = 0; i < 2; ++i) { - final SectionInfo newSectionInfo = new SectionInfo(sectionInfoPair[i].sourceToDisplayTransform, affectedUnionSourceInterval); + final SectionInfo newSectionInfo = new SectionInfo(sectionInfoPair[i].mask, sectionInfoPair[i].sourceToDisplayTransform, affectedUnionSourceInterval); final RandomAccessibleInterval<UnsignedLongType> section = getTransformedMaskSection(newSectionInfo); displaySectionIntervalPair[i] = new FinalInterval(section); sectionPair[i] = new ArrayImgFactory<>(new UnsignedLongType()).create(section); @@ -561,12 +554,12 @@ private RandomAccessibleInterval<UnsignedLongType> getTransformedMaskSection(fin { final RealInterval sectionBounds = sectionInfo.sourceToDisplayTransform.estimateBounds(sectionInfo.sourceBoundingBox); final Interval sectionInterval = Intervals.smallestContainingInterval(sectionBounds); - final RealRandomAccessible<UnsignedLongType> transformedMask = getTransformedMask(sectionInfo.sourceToDisplayTransform); + final RealRandomAccessible<UnsignedLongType> transformedMask = getTransformedMask(sectionInfo.mask, sectionInfo.sourceToDisplayTransform); final RandomAccessibleInterval<UnsignedLongType> transformedMaskInterval = Views.interval(Views.raster(transformedMask), sectionInterval); return Views.hyperSlice(transformedMaskInterval, 2, 0l); } - private RealRandomAccessible<UnsignedLongType> getTransformedMask(final AffineTransform3D transform) + private static RealRandomAccessible<UnsignedLongType> getTransformedMask(final Mask<UnsignedLongType> mask, final AffineTransform3D transform) { final RealRandomAccessible<UnsignedLongType> interpolatedMask = Views.interpolate( Views.extendValue(mask.mask, new UnsignedLongType(Label.OUTSIDE)), @@ -585,6 +578,16 @@ private static double computeDistanceBetweenSections(final SectionInfo s1, final private void selectObject(final PainteraBaseView paintera, final double x, final double y, final boolean deactivateOthers) { + // create the mask if needed + if (mask == null) + { + try { + createMask(); + } catch (final MaskInUse e) { + e.printStackTrace(); + } + } + final UnsignedLongType maskValue = getMaskValue(x, y); if (maskValue.get() == Label.OUTSIDE) return; @@ -617,6 +620,13 @@ private void selectObject(final PainteraBaseView paintera, final double x, final selectedObjects.remove(oldFillValue); } + // free the mask if there are no selected objects + if (selectedObjects.isEmpty()) + { + source.resetMasks(); + mask = null; + } + activeViewer.get().requestRepaint(); paintera.allowedActionsProperty().set(selectedObjects.isEmpty() ? allowedActions : allowedActionsWhenSelected); } From 42954b0b8b9f8b2dd9d34595170c500df0208390 Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Mon, 3 Jun 2019 10:32:48 -0400 Subject: [PATCH 37/84] disable commiting canvas and saving project in shape interpolation mode --- src/main/java/org/janelia/saalfeldlab/paintera/Paintera.java | 4 ++-- .../saalfeldlab/paintera/control/actions/MenuAction.java | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/Paintera.java b/src/main/java/org/janelia/saalfeldlab/paintera/Paintera.java index 509d59104..5538bb6f5 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/Paintera.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/Paintera.java @@ -288,7 +288,7 @@ public void startImpl(final Stage stage) throws Exception LOG.error("Project undefined"); } }, - e -> keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL, KeyCode.S) + e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.SaveProject) && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL, KeyCode.S) ).installInto(paneWithStatus.getPane()); EventFX.KEY_PRESSED("commit", e -> { @@ -305,7 +305,7 @@ public void startImpl(final Stage stage) throws Exception LOG.error("Unable to persist fragment-segment-assignment: {}", e1.getMessage()); } }, - e -> keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL, KeyCode.C) + e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.CommitCanvas) && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL, KeyCode.C) ).installInto(paneWithStatus.getPane()); keyTracker.installInto(scene); diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/MenuAction.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/MenuAction.java index d1c1f6d5c..9dc81b202 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/MenuAction.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/MenuAction.java @@ -10,7 +10,9 @@ public enum MenuAction SidePanel, ToggleViewerMaximizedMinimized, ToggleViewerAndOrthoslicesView, - OrthoslicesContextMenu; + OrthoslicesContextMenu, + SaveProject, + CommitCanvas; public static EnumSet<MenuAction> of(final MenuAction first, final MenuAction... rest) { From 252279b4d6260f188a555eb5f6641b329866c241 Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Mon, 3 Jun 2019 10:34:41 -0400 Subject: [PATCH 38/84] show message if attempted to save project in shape interpolation mode better than silently ignoring the action --- .../saalfeldlab/paintera/Paintera.java | 54 +++++++++++-------- 1 file changed, 33 insertions(+), 21 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/Paintera.java b/src/main/java/org/janelia/saalfeldlab/paintera/Paintera.java index 5538bb6f5..c65f1dbbf 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/Paintera.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/Paintera.java @@ -1,50 +1,52 @@ package org.janelia.saalfeldlab.paintera; -import bdv.viewer.ViewerOptions; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonObject; -import javafx.application.Application; -import javafx.application.Platform; -import javafx.scene.Scene; -import javafx.scene.input.KeyCode; -import javafx.scene.input.MouseEvent; -import javafx.stage.Stage; -import javafx.stage.WindowEvent; +import java.io.File; +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + import org.janelia.saalfeldlab.fx.event.EventFX; import org.janelia.saalfeldlab.fx.event.KeyTracker; import org.janelia.saalfeldlab.fx.event.MouseTracker; import org.janelia.saalfeldlab.fx.ortho.GridConstraintsManager; import org.janelia.saalfeldlab.fx.ortho.OrthogonalViews; -import org.janelia.saalfeldlab.n5.N5Writer; import org.janelia.saalfeldlab.paintera.SaveProject.ProjectUndefined; import org.janelia.saalfeldlab.paintera.config.CoordinateConfigNode; import org.janelia.saalfeldlab.paintera.config.NavigationConfigNode; import org.janelia.saalfeldlab.paintera.config.OrthoSliceConfig; import org.janelia.saalfeldlab.paintera.config.ScreenScalesConfig; import org.janelia.saalfeldlab.paintera.control.CommitChanges; -import org.janelia.saalfeldlab.paintera.control.assignment.FragmentSegmentAssignmentState; +import org.janelia.saalfeldlab.paintera.control.actions.MenuAction; import org.janelia.saalfeldlab.paintera.control.assignment.UnableToPersist; import org.janelia.saalfeldlab.paintera.data.mask.exception.CannotPersist; -import org.janelia.saalfeldlab.paintera.id.IdService; import org.janelia.saalfeldlab.paintera.serialization.GsonHelpers; import org.janelia.saalfeldlab.paintera.serialization.Properties; import org.janelia.saalfeldlab.paintera.state.HasSelectedIds; import org.janelia.saalfeldlab.paintera.state.SourceState; +import org.janelia.saalfeldlab.paintera.ui.PainteraAlerts; import org.janelia.saalfeldlab.paintera.viewer3d.Viewer3DFX; import org.janelia.saalfeldlab.util.n5.N5Helpers; import org.slf4j.Logger; import org.slf4j.LoggerFactory; + +import com.google.gson.GsonBuilder; +import com.google.gson.JsonObject; + +import bdv.viewer.ViewerOptions; +import javafx.application.Application; +import javafx.application.Platform; +import javafx.scene.Scene; +import javafx.scene.control.Alert; +import javafx.scene.input.KeyCode; +import javafx.scene.input.MouseEvent; +import javafx.stage.Stage; +import javafx.stage.WindowEvent; import picocli.CommandLine; import pl.touk.throwing.ThrowingFunction; import pl.touk.throwing.ThrowingSupplier; -import java.io.File; -import java.io.IOException; -import java.lang.invoke.MethodHandles; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; - public class Paintera extends Application { @@ -270,6 +272,16 @@ public void startImpl(final Stage stage) throws Exception "save project", e -> { e.consume(); + + if (!baseView.allowedActionsProperty().get().isAllowed(MenuAction.SaveProject)) + { + final Alert cannotSaveProjectDialog = PainteraAlerts.alert(Alert.AlertType.WARNING); + cannotSaveProjectDialog.setHeaderText("Cannot currently save the project."); + cannotSaveProjectDialog.setContentText("Please return to the normal application mode."); + cannotSaveProjectDialog.show(); + return; + } + try { SaveProject.persistProperties( @@ -288,7 +300,7 @@ public void startImpl(final Stage stage) throws Exception LOG.error("Project undefined"); } }, - e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.SaveProject) && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL, KeyCode.S) + e -> keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL, KeyCode.S) ).installInto(paneWithStatus.getPane()); EventFX.KEY_PRESSED("commit", e -> { From 98f15e992318e85d8da1c4ea6978ad6d426e799f Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Mon, 3 Jun 2019 10:46:34 -0400 Subject: [PATCH 39/84] return to normal mode if requested to exit in shape interpolation mode --- .../paintera/SaveOnExitDialog.java | 14 ++++--- .../control/ShapeInterpolationMode.java | 38 ++++++++++--------- .../control/actions/AllowedActions.java | 18 ++++++++- 3 files changed, 45 insertions(+), 25 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/SaveOnExitDialog.java b/src/main/java/org/janelia/saalfeldlab/paintera/SaveOnExitDialog.java index 901e411fb..62e0d831b 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/SaveOnExitDialog.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/SaveOnExitDialog.java @@ -4,11 +4,6 @@ import java.lang.invoke.MethodHandles; import java.util.Optional; -import javafx.event.EventHandler; -import javafx.scene.control.ButtonBar.ButtonData; -import javafx.scene.control.ButtonType; -import javafx.scene.control.Dialog; -import javafx.stage.WindowEvent; import org.janelia.saalfeldlab.paintera.SaveProject.ProjectUndefined; import org.janelia.saalfeldlab.paintera.control.CommitChanges; import org.janelia.saalfeldlab.paintera.control.CommitChanges.Commitable; @@ -17,6 +12,12 @@ import org.janelia.saalfeldlab.paintera.serialization.Properties; import org.slf4j.Logger; import org.slf4j.LoggerFactory; + +import javafx.event.EventHandler; +import javafx.scene.control.ButtonBar.ButtonData; +import javafx.scene.control.ButtonType; +import javafx.scene.control.Dialog; +import javafx.stage.WindowEvent; import pl.touk.throwing.ThrowingConsumer; public class SaveOnExitDialog implements EventHandler<WindowEvent> @@ -62,6 +63,9 @@ public void handle(final WindowEvent event) return; } + // run cleanup if the application is not currently in the normal mode + baseView.allowedActionsProperty().get().cleanup(baseView); + if (saveButton.equals(response)) { LOG.debug("Saving project before exit"); diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java index 7e2168e7e..7e1a4983f 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java @@ -2,6 +2,7 @@ import java.lang.invoke.MethodHandles; import java.util.Arrays; +import java.util.function.Consumer; import java.util.function.Predicate; import org.janelia.saalfeldlab.fx.event.DelegateEventHandlers; @@ -116,24 +117,6 @@ private static final class SectionInfo } } - private static final AllowedActions allowedActions; - private static final AllowedActions allowedActionsWhenSelected; - static - { - allowedActions = new AllowedActions( - NavigationAction.of(NavigationAction.Drag, NavigationAction.Zoom, NavigationAction.Scroll), - LabelAction.none(), - PaintAction.none(), - MenuAction.of(MenuAction.ToggleViewerMaximizedMinimized) - ); - allowedActionsWhenSelected = new AllowedActions( - NavigationAction.of(NavigationAction.Drag, NavigationAction.Zoom), - LabelAction.none(), - PaintAction.none(), - MenuAction.of(MenuAction.ToggleViewerMaximizedMinimized) - ); - } - private static final double FILL_DEPTH = 2.0; private static final int MASK_SCALE_LEVEL = 0; @@ -151,6 +134,9 @@ private static final class SectionInfo private final IdService idService; private final HighlightingStreamConverter<?> converter; + private final AllowedActions allowedActions; + private final AllowedActions allowedActionsWhenSelected; + private AllowedActions lastAllowedActions; private long lastSelectedId; private long[] lastActiveIds; @@ -176,6 +162,22 @@ public ShapeInterpolationMode( this.selectedIds = selectedIds; this.idService = idService; this.converter = converter; + + final Consumer<PainteraBaseView> cleanup = baseView -> exitMode(baseView, false); + this.allowedActions = new AllowedActions( + NavigationAction.of(NavigationAction.Drag, NavigationAction.Zoom, NavigationAction.Scroll), + LabelAction.none(), + PaintAction.none(), + MenuAction.of(MenuAction.ToggleViewerMaximizedMinimized), + cleanup + ); + this.allowedActionsWhenSelected = new AllowedActions( + NavigationAction.of(NavigationAction.Drag, NavigationAction.Zoom), + LabelAction.none(), + PaintAction.none(), + MenuAction.of(MenuAction.ToggleViewerMaximizedMinimized), + cleanup + ); } public ObjectProperty<ViewerPanelFX> activeViewerProperty() diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/AllowedActions.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/AllowedActions.java index aca57c552..906040bb5 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/AllowedActions.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/AllowedActions.java @@ -1,6 +1,9 @@ package org.janelia.saalfeldlab.paintera.control.actions; import java.util.EnumSet; +import java.util.function.Consumer; + +import org.janelia.saalfeldlab.paintera.PainteraBaseView; /** * Describes what actions in the UI are allowed in the current application mode. @@ -12,16 +15,20 @@ public final class AllowedActions private final EnumSet<PaintAction> paintAllowedActions; private final EnumSet<MenuAction> menuAllowedActions; + private final Consumer<PainteraBaseView> cleanup; + public AllowedActions( final EnumSet<NavigationAction> navigationAllowedActions, final EnumSet<LabelAction> labelAllowedActions, final EnumSet<PaintAction> paintAllowedActions, - final EnumSet<MenuAction> menuAllowedActions) + final EnumSet<MenuAction> menuAllowedActions, + final Consumer<PainteraBaseView> cleanup) { this.navigationAllowedActions = navigationAllowedActions; this.labelAllowedActions = labelAllowedActions; this.paintAllowedActions = paintAllowedActions; this.menuAllowedActions = menuAllowedActions; + this.cleanup = cleanup; } public boolean isAllowed(final NavigationAction navigationAction) @@ -44,13 +51,20 @@ public boolean isAllowed(final MenuAction menuAction) return this.menuAllowedActions.contains(menuAction); } + public void cleanup(final PainteraBaseView baseView) + { + if (this.cleanup != null) + this.cleanup.accept(baseView); + } + public static AllowedActions all() { return new AllowedActions( NavigationAction.all(), LabelAction.all(), PaintAction.all(), - MenuAction.all() + MenuAction.all(), + null ); } } From 322db3473bbcc07ff9c13f0a4c3119c9d070721a Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Mon, 3 Jun 2019 12:29:17 -0400 Subject: [PATCH 40/84] use slightly larger depth for flood filling in rotated view extra 0.5 help to prevent displaying flood-filled sections as stripes instead of a solid object --- .../paintera/control/ShapeInterpolationMode.java | 2 +- .../saalfeldlab/paintera/control/paint/FloodFill2D.java | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java index 7e1a4983f..1d7c534ba 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java @@ -117,7 +117,7 @@ private static final class SectionInfo } } - private static final double FILL_DEPTH = 2.0; + private static final double FILL_DEPTH = 1.0; private static final int MASK_SCALE_LEVEL = 0; 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 b40673065..4f57592ab 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 @@ -261,15 +261,15 @@ public static <T extends IntegerType<T>> Interval fillMaskAt( { FloodFillTransformedPlane.fill( labelToViewerTransform, - (0.5 + fillDepth - 1.0) * PaintUtils.maximumVoxelDiagonalLengthPerDimension( + fillDepth * PaintUtils.maximumVoxelDiagonalLengthPerDimension( labelTransform, viewerTransform - )[2], + )[2], extendedFilter.randomAccess(), accessTracker.randomAccess(), new RealPoint(x, y, 0), fillValue - ); + ); affectedInterval = new FinalInterval(accessTracker.getMin(), accessTracker.getMax()); } else From e513f8b446c7044f5fcc63e27c467d4910875679 Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Mon, 3 Jun 2019 15:23:15 -0400 Subject: [PATCH 41/84] make maximized+3d view viewer-specific and allow it in interpolation mode --- .../saalfeldlab/fx/ortho/OrthogonalViews.java | 18 +- .../fx/ortho/ResizableGridPane2x2.java | 12 +- .../paintera/PainteraDefaultHandlers.java | 158 ++++++++---------- .../control/ShapeInterpolationMode.java | 14 +- .../paintera/control/actions/MenuAction.java | 3 +- .../paintera/ui/ToggleMaximize.java | 37 +++- 6 files changed, 124 insertions(+), 118 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/fx/ortho/OrthogonalViews.java b/src/main/java/org/janelia/saalfeldlab/fx/ortho/OrthogonalViews.java index b087f5665..b13714a5f 100644 --- a/src/main/java/org/janelia/saalfeldlab/fx/ortho/OrthogonalViews.java +++ b/src/main/java/org/janelia/saalfeldlab/fx/ortho/OrthogonalViews.java @@ -5,9 +5,15 @@ import java.util.function.Consumer; import java.util.function.Function; +import org.janelia.saalfeldlab.paintera.control.navigation.AffineTransformWithListeners; +import org.janelia.saalfeldlab.paintera.control.navigation.TransformConcatenator; +import org.janelia.saalfeldlab.paintera.data.axisorder.AxisOrder; +import org.janelia.saalfeldlab.paintera.state.GlobalTransformManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import bdv.cache.CacheControl; import bdv.fx.viewer.ViewerPanelFX; -import bdv.util.volatiles.SharedQueue; import bdv.viewer.Interpolation; import bdv.viewer.Source; import bdv.viewer.SourceAndConverter; @@ -16,14 +22,8 @@ import javafx.event.EventHandler; import javafx.event.EventType; import javafx.scene.Node; -import javafx.scene.layout.Pane; +import javafx.scene.layout.GridPane; import net.imglib2.realtransform.AffineTransform3D; -import org.janelia.saalfeldlab.paintera.control.navigation.AffineTransformWithListeners; -import org.janelia.saalfeldlab.paintera.control.navigation.TransformConcatenator; -import org.janelia.saalfeldlab.paintera.data.axisorder.AxisOrder; -import org.janelia.saalfeldlab.paintera.state.GlobalTransformManager; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * Wrap a {@link ResizableGridPane2x2} with {@link ViewerPanelFX viewer panels} at top left, top right, and bottom left. Bottom right @@ -157,7 +157,7 @@ public ResizableGridPane2x2<ViewerPanelFX, ViewerPanelFX, ViewerPanelFX, BR> gri * * @return {@link ResizableGridPane2x2#pane()} for the underlying {@link ResizableGridPane2x2} */ - public Pane pane() + public GridPane pane() { return grid().pane(); } diff --git a/src/main/java/org/janelia/saalfeldlab/fx/ortho/ResizableGridPane2x2.java b/src/main/java/org/janelia/saalfeldlab/fx/ortho/ResizableGridPane2x2.java index a815dfc9a..6bd74c828 100644 --- a/src/main/java/org/janelia/saalfeldlab/fx/ortho/ResizableGridPane2x2.java +++ b/src/main/java/org/janelia/saalfeldlab/fx/ortho/ResizableGridPane2x2.java @@ -4,7 +4,6 @@ import javafx.beans.property.SimpleObjectProperty; import javafx.scene.Node; import javafx.scene.layout.GridPane; -import javafx.scene.layout.Pane; /** * A wrapper around {@link GridPane} that holds for children organized in a 2x2 grid. The underlying @@ -62,7 +61,7 @@ public ResizableGridPane2x2( * * @return underlying {@link GridPane} */ - public Pane pane() + public GridPane pane() { return this.grid; } @@ -149,11 +148,18 @@ public void manage(final GridConstraintsManager manager) manager.manageGrid(this.grid); } + public Node getNodeAt(final int col, final int row) + { + for (final Node child : grid.getChildren()) + if (GridPane.getColumnIndex(child) == col && GridPane.getRowIndex(child) == row) + return child; + return null; + } + private static void replace(final GridPane grid, final Node oldValue, final Node newValue, final int col, final int row) { grid.getChildren().remove(oldValue); grid.add(newValue, col, row); } - } diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/PainteraDefaultHandlers.java b/src/main/java/org/janelia/saalfeldlab/paintera/PainteraDefaultHandlers.java index 28e5a8c88..a4e0708dd 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/PainteraDefaultHandlers.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/PainteraDefaultHandlers.java @@ -1,31 +1,16 @@ package org.janelia.saalfeldlab.paintera; -import bdv.fx.viewer.ViewerPanelFX; -import bdv.fx.viewer.multibox.MultiBoxOverlayRendererFX; -import bdv.viewer.Interpolation; -import bdv.viewer.Source; -import javafx.beans.binding.Bindings; -import javafx.beans.binding.BooleanBinding; -import javafx.beans.binding.IntegerBinding; -import javafx.beans.binding.ObjectBinding; -import javafx.beans.property.ObjectProperty; -import javafx.beans.property.ReadOnlyBooleanProperty; -import javafx.beans.value.ChangeListener; -import javafx.beans.value.ObservableBooleanValue; -import javafx.beans.value.ObservableObjectValue; -import javafx.event.Event; -import javafx.event.EventHandler; -import javafx.scene.Node; -import javafx.scene.control.ContextMenu; -import javafx.scene.input.KeyCode; -import javafx.scene.input.KeyEvent; -import javafx.scene.input.MouseButton; -import javafx.scene.input.MouseEvent; -import javafx.scene.layout.Pane; -import net.imglib2.FinalRealInterval; -import net.imglib2.Interval; -import net.imglib2.realtransform.AffineTransform3D; -import net.imglib2.util.Intervals; +import java.lang.invoke.MethodHandles; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.DoubleSupplier; + import org.janelia.saalfeldlab.fx.event.DelegateEventHandlers; import org.janelia.saalfeldlab.fx.event.EventFX; import org.janelia.saalfeldlab.fx.event.KeyTracker; @@ -37,7 +22,6 @@ import org.janelia.saalfeldlab.fx.ortho.OnEnterOnExit; import org.janelia.saalfeldlab.fx.ortho.OrthogonalViews; import org.janelia.saalfeldlab.fx.ortho.OrthogonalViews.ViewerAndTransforms; -import org.janelia.saalfeldlab.fx.ortho.ViewerAxis; import org.janelia.saalfeldlab.fx.ui.Exceptions; import org.janelia.saalfeldlab.paintera.control.CurrentSourceVisibilityToggle; import org.janelia.saalfeldlab.paintera.control.FitToInterval; @@ -59,16 +43,31 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.lang.invoke.MethodHandles; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.function.Consumer; -import java.util.function.DoubleSupplier; +import bdv.fx.viewer.ViewerPanelFX; +import bdv.fx.viewer.multibox.MultiBoxOverlayRendererFX; +import bdv.viewer.Interpolation; +import bdv.viewer.Source; +import javafx.beans.binding.Bindings; +import javafx.beans.binding.BooleanBinding; +import javafx.beans.binding.IntegerBinding; +import javafx.beans.binding.ObjectBinding; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.ReadOnlyBooleanProperty; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableObjectValue; +import javafx.event.Event; +import javafx.event.EventHandler; +import javafx.scene.Node; +import javafx.scene.control.ContextMenu; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; +import javafx.scene.input.MouseButton; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.Pane; +import net.imglib2.FinalRealInterval; +import net.imglib2.Interval; +import net.imglib2.realtransform.AffineTransform3D; +import net.imglib2.util.Intervals; public class PainteraDefaultHandlers { @@ -97,28 +96,16 @@ public class PainteraDefaultHandlers private final Navigation navigation; -// private final Merges merges; -// -// private final Paint paint; -// -// private final Selection selection; - private final Consumer<OnEnterOnExit> onEnterOnExit; private final ToggleMaximize toggleMaximizeTopLeft; - private final ToggleMaximize toggleMaximizeTopRight; - private final ToggleMaximize toggleMaximizeBottomLeft; - private final ToggleMaximize toggleMaximizeBottomRight; - private final MultiBoxOverlayRendererFX[] multiBoxes; private final GridResizer resizer; - private final ObservableBooleanValue bottomLeftNeedsZNormal; - private final EventHandler<KeyEvent> openDatasetContextMenuHandler; private final ObjectBinding<EventHandler<Event>> sourceSpecificGlobalEventHandler; @@ -211,10 +198,9 @@ public PainteraDefaultHandlers( KeyCode.CONTROL, KeyCode.O); - this.toggleMaximizeTopLeft = toggleMaximizeNode(gridConstraintsManager, 0, 0); - this.toggleMaximizeTopRight = toggleMaximizeNode(gridConstraintsManager, 1, 0); - this.toggleMaximizeBottomLeft = toggleMaximizeNode(gridConstraintsManager, 0, 1); - this.toggleMaximizeBottomRight = toggleMaximizeNode(gridConstraintsManager, 1, 1); + this.toggleMaximizeTopLeft = toggleMaximizeNode(orthogonalViews, gridConstraintsManager, 0, 0); + this.toggleMaximizeTopRight = toggleMaximizeNode(orthogonalViews, gridConstraintsManager, 1, 0); + this.toggleMaximizeBottomLeft = toggleMaximizeNode(orthogonalViews, gridConstraintsManager, 0, 1); viewerToTransforms.put(orthogonalViews.topLeft().viewer(), orthogonalViews.topLeft()); viewerToTransforms.put(orthogonalViews.topRight().viewer(), orthogonalViews.topRight()); @@ -299,22 +285,35 @@ public PainteraDefaultHandlers( .setInitialTransformToInterval( sourceIntervalInWorldSpace(c.getAddedSubList().get(0))))); + + EventFX.KEY_PRESSED( + "toggle maximize viewer", + e -> toggleMaximizeTopLeft.toggleMaximizeViewer(), + e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.ToggleMaximizeViewer) && keyTracker.areOnlyTheseKeysDown(KeyCode.M)).installInto(orthogonalViews.topLeft().viewer()); + EventFX.KEY_PRESSED( + "toggle maximize viewer", + e -> toggleMaximizeTopRight.toggleMaximizeViewer(), + e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.ToggleMaximizeViewer) && keyTracker.areOnlyTheseKeysDown(KeyCode.M)).installInto(orthogonalViews.topRight().viewer()); EventFX.KEY_PRESSED( - "maximize", - e -> toggleMaximizeTopLeft.toggleFullScreen(), - e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.ToggleViewerMaximizedMinimized) && keyTracker.areOnlyTheseKeysDown(KeyCode.M)).installInto(orthogonalViews.topLeft().viewer()); + "toggle maximize viewer", + e -> toggleMaximizeBottomLeft.toggleMaximizeViewer(), + e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.ToggleMaximizeViewer) && keyTracker.areOnlyTheseKeysDown(KeyCode.M)).installInto(orthogonalViews.bottomLeft().viewer()); + EventFX.KEY_PRESSED( - "maximize", - e -> toggleMaximizeTopRight.toggleFullScreen(), - e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.ToggleViewerMaximizedMinimized) && keyTracker.areOnlyTheseKeysDown(KeyCode.M)).installInto(orthogonalViews.topRight().viewer()); + "toggle maximize viewer and orthoslice", + e -> toggleMaximizeTopLeft.toggleMaximizeViewerAndOrthoslice(), + e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.ToggleMaximizeViewer) && keyTracker.areOnlyTheseKeysDown(KeyCode.M, KeyCode.SHIFT)).installInto(orthogonalViews.topLeft().viewer()); + EventFX.KEY_PRESSED( - "maximize", - e -> toggleMaximizeBottomLeft.toggleFullScreen(), - e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.ToggleViewerMaximizedMinimized) && keyTracker.areOnlyTheseKeysDown(KeyCode.M)).installInto(orthogonalViews.bottomLeft().viewer()); + "toggle maximize viewer and orthoslice", + e -> toggleMaximizeTopRight.toggleMaximizeViewerAndOrthoslice(), + e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.ToggleMaximizeViewer) && keyTracker.areOnlyTheseKeysDown(KeyCode.M, KeyCode.SHIFT)).installInto(orthogonalViews.topRight().viewer()); + EventFX.KEY_PRESSED( - "maximize", - e -> toggleMaximizeBottomRight.toggleFullScreen(), - e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.ToggleViewerMaximizedMinimized) && keyTracker.areOnlyTheseKeysDown(KeyCode.M)).installInto(baseView.viewer3D()); + "toggle maximize viewer and orthoslice", + e -> toggleMaximizeBottomLeft.toggleMaximizeViewerAndOrthoslice(), + e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.ToggleMaximizeViewer) && keyTracker.areOnlyTheseKeysDown(KeyCode.M, KeyCode.SHIFT)).installInto(orthogonalViews.bottomLeft().viewer()); + final CurrentSourceVisibilityToggle csv = new CurrentSourceVisibilityToggle(sourceInfo.currentState()); EventFX.KEY_PRESSED( @@ -330,33 +329,6 @@ public PainteraDefaultHandlers( e -> sosist.toggleNonSelectionVisibility(), e -> keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT, KeyCode.V)).installInto(borderPane); - EventFX.KEY_PRESSED( - "toggle maximize bottom row", - e -> gridConstraintsManager.maximize(MaximizedRow.BOTTOM, 0), - e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.ToggleViewerAndOrthoslicesView) && keyTracker.areOnlyTheseKeysDown(KeyCode.M, KeyCode.SHIFT)).installInto(paneWithStatus.getPane()); - - bottomLeftNeedsZNormal = Bindings.createBooleanBinding( - () -> MaximizedColumn.NONE.equals(gridConstraintsManager.getMaximizedColumn()) && MaximizedRow.BOTTOM - .equals( - gridConstraintsManager.getMaximizedRow()), - gridConstraintsManager.observeMaximizedColumn(), - gridConstraintsManager.observeMaximizedRow()); - - final AffineTransformWithListeners bottomLeftGlobalToViewer = orthogonalViews.bottomLeft() - .globalToViewerTransform(); - bottomLeftNeedsZNormal.addListener((obs, oldv, newv) -> bottomLeftGlobalToViewer.setTransform(ViewerAxis - .globalToViewer( - newv ? ViewerAxis.Z : ViewerAxis.Y))); - - if (gridConstraintsManager.isFullScreen() && GridConstraintsManager.MaximizedRow.BOTTOM.equals( - gridConstraintsManager.getMaximizedRow()) && GridConstraintsManager.MaximizedColumn.NONE.equals( - gridConstraintsManager.getMaximizedColumn())) - { - orthogonalViews - .bottomLeft() - .globalToViewerTransform() - .setTransform(ViewerAxis.globalToViewer(ViewerAxis.Z)); - } // TODO does MouseEvent.getPickResult make the coordinate tracker // TODO obsolete? @@ -531,11 +503,13 @@ public static EventHandler<KeyEvent> addOpenDatasetContextMenuHandler( } public static ToggleMaximize toggleMaximizeNode( + final OrthogonalViews<? extends Node> orthogonalViews, final GridConstraintsManager manager, final int column, final int row) { return new ToggleMaximize( + orthogonalViews, manager, MaximizedColumn.fromIndex(column), MaximizedRow.fromIndex(row)); diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java index 1d7c534ba..048a690fd 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java @@ -165,17 +165,17 @@ public ShapeInterpolationMode( final Consumer<PainteraBaseView> cleanup = baseView -> exitMode(baseView, false); this.allowedActions = new AllowedActions( - NavigationAction.of(NavigationAction.Drag, NavigationAction.Zoom, NavigationAction.Scroll), - LabelAction.none(), - PaintAction.none(), - MenuAction.of(MenuAction.ToggleViewerMaximizedMinimized), - cleanup - ); + NavigationAction.of(NavigationAction.Drag, NavigationAction.Zoom, NavigationAction.Scroll), + LabelAction.none(), + PaintAction.none(), + MenuAction.of(MenuAction.ToggleMaximizeViewer), + cleanup + ); this.allowedActionsWhenSelected = new AllowedActions( NavigationAction.of(NavigationAction.Drag, NavigationAction.Zoom), LabelAction.none(), PaintAction.none(), - MenuAction.of(MenuAction.ToggleViewerMaximizedMinimized), + MenuAction.of(MenuAction.ToggleMaximizeViewer), cleanup ); } diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/MenuAction.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/MenuAction.java index 9dc81b202..63b0108c7 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/MenuAction.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/MenuAction.java @@ -8,8 +8,7 @@ public enum MenuAction CreateNewLabelSource, ChangeActiveSource, SidePanel, - ToggleViewerMaximizedMinimized, - ToggleViewerAndOrthoslicesView, + ToggleMaximizeViewer, OrthoslicesContextMenu, SaveProject, CommitCanvas; diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/ui/ToggleMaximize.java b/src/main/java/org/janelia/saalfeldlab/paintera/ui/ToggleMaximize.java index a9f45421c..7cc59f13e 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/ui/ToggleMaximize.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/ui/ToggleMaximize.java @@ -3,30 +3,57 @@ import org.janelia.saalfeldlab.fx.ortho.GridConstraintsManager; import org.janelia.saalfeldlab.fx.ortho.GridConstraintsManager.MaximizedColumn; import org.janelia.saalfeldlab.fx.ortho.GridConstraintsManager.MaximizedRow; +import org.janelia.saalfeldlab.fx.ortho.OrthogonalViews; + +import javafx.scene.Node; public class ToggleMaximize { - + private final OrthogonalViews<? extends Node> orthogonalViews; private final GridConstraintsManager manager; private final MaximizedColumn col; - private final MaximizedRow row; public ToggleMaximize( + final OrthogonalViews<? extends Node> orthogonalViews, final GridConstraintsManager manager, final MaximizedColumn col, final MaximizedRow row) { - super(); + this.orthogonalViews = orthogonalViews; this.manager = manager; this.col = col; this.row = row; } - public void toggleFullScreen() + public void toggleMaximizeViewer() { - this.manager.maximize(row, col, 200); + if (manager.getMaximizedColumn() == MaximizedColumn.NONE && manager.getMaximizedRow() == MaximizedRow.BOTTOM) + toggleMaximizeViewerAndOrthoslice(); + else + manager.maximize(row, col, 0); } + public void toggleMaximizeViewerAndOrthoslice() + { + if (manager.getMaximizedColumn() != MaximizedColumn.NONE && manager.getMaximizedRow() != MaximizedRow.NONE) + { + toggleMaximizeViewer(); + return; + } + + if (col != MaximizedColumn.LEFT || row != MaximizedRow.BOTTOM) + { + final Node swappedNode = orthogonalViews.grid().getNodeAt(col.asIndex(), row.asIndex()); + final Node bottomLeftNode = orthogonalViews.grid().getNodeAt(MaximizedColumn.LEFT.asIndex(), MaximizedRow.BOTTOM.asIndex()); + + orthogonalViews.pane().getChildren().remove(swappedNode); + orthogonalViews.pane().getChildren().remove(bottomLeftNode); + + orthogonalViews.pane().add(swappedNode, MaximizedColumn.LEFT.asIndex(), MaximizedRow.BOTTOM.asIndex()); + orthogonalViews.pane().add(bottomLeftNode, col.asIndex(), row.asIndex()); + } + manager.maximize(MaximizedRow.BOTTOM, 0); + } } From f899c8661fad4fa7e9c56fe9c988f136c1ea591d Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Mon, 3 Jun 2019 15:30:59 -0400 Subject: [PATCH 42/84] todo: optimize keeping track of union bounding box for distance transform Currently it's being tracked in the source coordinate space, which may make it unnecessarily large in the viewer space because of the rotating transform from source to display coordinates --- .../saalfeldlab/paintera/control/ShapeInterpolationMode.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java index 048a690fd..29243cc24 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java @@ -103,6 +103,8 @@ private static final class SelectedObjectInfo } } + // TODO: instead of keeping track of the bounding box in the source coordinate space, track affected interval in the current viewer plane. + // This would make the union bounding box smaller. Currently it may be larger than necessary because of the rotating transform from source to display coordinates. private static final class SectionInfo { final Mask<UnsignedLongType> mask; From 25f5952bf27136077c10afa26ba18443aba1933f Mon Sep 17 00:00:00 2001 From: Igor Pisarev <igorpisarev@users.noreply.github.com> Date: Mon, 3 Jun 2019 15:55:08 -0400 Subject: [PATCH 43/84] add usage of shape interpolation mode to readme --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index e4573aa67..2a93e6247 100644 --- a/README.md +++ b/README.md @@ -176,6 +176,14 @@ Usage: Paintera [-h] [--default-to-temp-directory] [--print-error-codes] | `Ctrl` + `Shift` + `N` | Create new label dataset | | `Ctrl` + `T` | Threshold raw source (only available if current source is raw source) | +### Shape interpolation mode + +The mode is activated by pressing the `S` key when the current source is a label source. Then, you can select the objects in the first section by left/right clicking and hit `S` to fix the selection and switch to the second section (scrolling through sections works only when there are no selected objects). + +When you're done with selecting the objects in the second section, hit `S` again to interpolate between the sections. The interpolated preview will be displayed, where you can hit `S` again to initiate committing the results into the canvas using a new label ID and return back to normal mode. + +While in the shape interpolation mode, at any point in time you can hit `ESC` to discard the current state and exit the mode. + ## Data In [#61](https://github.com/saalfeldlab/paintera/issues/61) we introduced a specification for the data format that Paintera can load through the opener dialog (`Ctrl O`). From 2c2b5564f712b88ade6ddafb27cac0649d703a23 Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Tue, 4 Jun 2019 10:44:15 -0400 Subject: [PATCH 44/84] get all viewer panels directly from base view instead of parent node --- .../control/ShapeInterpolationMode.java | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java index 29243cc24..b0e2e448e 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java @@ -38,8 +38,6 @@ import javafx.beans.property.SimpleObjectProperty; import javafx.event.Event; import javafx.event.EventHandler; -import javafx.scene.Node; -import javafx.scene.Parent; import javafx.scene.effect.ColorAdjust; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; @@ -257,7 +255,7 @@ public void enterMode(final PainteraBaseView paintera, final ViewerPanelFX viewe } LOG.info("Entering shape interpolation mode"); activeViewer.set(viewer); - setDisableOtherViewers(true); + setDisableOtherViewers(paintera, true); lastAllowedActions = paintera.allowedActionsProperty().get(); paintera.allowedActionsProperty().set(allowedActions); @@ -279,7 +277,7 @@ public void exitMode(final PainteraBaseView paintera, final boolean completed) return; } LOG.info("Exiting shape interpolation mode"); - setDisableOtherViewers(false); + setDisableOtherViewers(paintera, false); if (!completed) // extra cleanup if the mode is aborted { @@ -332,14 +330,12 @@ private void createMask() throws MaskInUse mask = source.generateMask(maskInfo, FOREGROUND_CHECK); } - private void setDisableOtherViewers(final boolean disable) + private void setDisableOtherViewers(final PainteraBaseView paintera, final boolean disable) { - final Parent parent = activeViewer.get().getParent(); - for (final Node child : parent.getChildrenUnmodifiable()) + for (final ViewerPanelFX viewer : getViewerPanels(paintera)) { - if (child instanceof ViewerPanelFX && child != activeViewer.get()) + if (viewer != activeViewer.get()) { - final ViewerPanelFX viewer = (ViewerPanelFX) child; viewer.setDisable(disable); if (disable) { @@ -756,4 +752,13 @@ private double[] getDisplayCoordinates(final RealPoint sourcePos) assert Util.isApproxEqual(displayPos.getDoublePosition(2), 0.0, 1e-10); return new double[] {displayPos.getDoublePosition(0), displayPos.getDoublePosition(1)}; } + + private static ViewerPanelFX[] getViewerPanels(final PainteraBaseView paintera) + { + return new ViewerPanelFX[] { + paintera.orthogonalViews().topLeft().viewer(), + paintera.orthogonalViews().topRight().viewer(), + paintera.orthogonalViews().bottomLeft().viewer() + }; + } } From 3b6c84bb20241412b66845b20a880613f62e5fc8 Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Tue, 4 Jun 2019 10:44:40 -0400 Subject: [PATCH 45/84] repaint in all viewer panels to properly see 3d interpolated shape --- .../saalfeldlab/paintera/control/ShapeInterpolationMode.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java index b0e2e448e..ef00c96cf 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java @@ -477,7 +477,9 @@ private void interpolateBetweenSections(final PainteraBaseView paintera) FOREGROUND_CHECK ); } - activeViewer.get().requestRepaint(); + + for (final ViewerPanelFX viewer : getViewerPanels(paintera)) + viewer.requestRepaint(); } catch (final MaskInUse e) { From 41812373072abb860c5e1dd7abfca0da994d68c3 Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Tue, 4 Jun 2019 12:19:11 -0400 Subject: [PATCH 46/84] revert increased 2d floodfill thickness in arbitrary orientation remove extra 0.5 px so that it only affets the clicked section, which may paint only part of the displayed view if it's rotated and encompasses several sections at once --- .../janelia/saalfeldlab/paintera/control/paint/FloodFill2D.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 4f57592ab..63d0e0f9a 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 @@ -261,7 +261,7 @@ public static <T extends IntegerType<T>> Interval fillMaskAt( { FloodFillTransformedPlane.fill( labelToViewerTransform, - fillDepth * PaintUtils.maximumVoxelDiagonalLengthPerDimension( + (fillDepth - 0.5) * PaintUtils.maximumVoxelDiagonalLengthPerDimension( labelTransform, viewerTransform )[2], From 323bfd1b0cf6fad6839c417d290a29e2cb0bd587 Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Tue, 4 Jun 2019 12:23:31 -0400 Subject: [PATCH 47/84] do not stretch virtual mask after interpolation because of side effects The extra 0.5 px on each side was to make sure that the interpolated shape is fully visible in the end sections and doesn't disappear or look like discontinuous stripes. However, this has undesired side effects when painting in an orthogonal view (or very close to it) because it affects adjacent sections outside the defined span. --- .../paintera/control/ShapeInterpolationMode.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java index ef00c96cf..ee67a6ecb 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java @@ -533,13 +533,9 @@ private static <R extends RealType<R>, T extends NativeType<T> & RealType<T>> Re new NLinearInterpolatorFactory<>() ); - final double distanceSign = Math.signum(distance); - final double padding = 0.5; // slightly stretches the mask past the end sections to ensure that it's visible in the current plane final RealRandomAccessible<R> scaledInterpolatedDistanceTransform = RealViews.affineReal( interpolatedDistanceTransform, - new AffineTransform3D() - .preConcatenate(new Scale3D(1, 1, -(distance + padding * distanceSign))) - .preConcatenate(new Translation3D(0, 0, padding * 0.5 * distanceSign)) + new Scale3D(1, 1, -distance) ); final T emptyValue = targetValue.createVariable(); From 221659d9bb6f368548fa05df8f715467c60f79da Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Tue, 4 Jun 2019 15:39:46 -0400 Subject: [PATCH 48/84] more consistent naming of shape interpolation mode states --- .../paintera/control/ShapeInterpolationMode.java | 14 +++++++------- .../paintera/state/LabelSourceState.java | 6 +++--- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java index ee67a6ecb..b6c6959f9 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java @@ -84,8 +84,8 @@ public class ShapeInterpolationMode<D extends IntegerType<D>> public static enum ModeState { - Selecting, - Interpolating, + Select, + Interpolate, Review } @@ -209,7 +209,7 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke EventFX.KEY_PRESSED( "fix selection", e -> {e.consume(); fixSelection(paintera);}, - e -> modeState.get() == ModeState.Selecting && + e -> modeState.get() == ModeState.Select && !selectedObjects.isEmpty() && keyTracker.areOnlyTheseKeysDown(KeyCode.S) ) @@ -234,12 +234,12 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke filter.addEventHandler(MouseEvent.ANY, new MouseClickFX( "select object in current section", e -> {e.consume(); selectObject(paintera, e.getX(), e.getY(), true);}, - e -> modeState.get() == ModeState.Selecting && e.isPrimaryButtonDown() && keyTracker.noKeysActive()) + e -> modeState.get() == ModeState.Select && e.isPrimaryButtonDown() && keyTracker.noKeysActive()) .handler()); filter.addEventHandler(MouseEvent.ANY, new MouseClickFX( "toggle object in current section", e -> {e.consume(); selectObject(paintera, e.getX(), e.getY(), false);}, - e -> modeState.get() == ModeState.Selecting && + e -> modeState.get() == ModeState.Select && ((e.isSecondaryButtonDown() && keyTracker.noKeysActive()) || (e.isPrimaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL)))) .handler()); @@ -266,7 +266,7 @@ public void enterMode(final PainteraBaseView paintera, final ViewerPanelFX viewe converter.setColor(newLabelId, MASK_COLOR); selectedIds.activate(newLabelId); - modeState.set(ModeState.Selecting); + modeState.set(ModeState.Select); } public void exitMode(final PainteraBaseView paintera, final boolean completed) @@ -404,7 +404,7 @@ private SectionInfo createSectionInfo() @SuppressWarnings("unchecked") private void interpolateBetweenSections(final PainteraBaseView paintera) { - modeState.set(ModeState.Interpolating); + modeState.set(ModeState.Interpolate); workerThread = new Thread(() -> { diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java index 719e11f39..99ea369e7 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java @@ -609,9 +609,9 @@ private HBox createDisplayStatus() this.shapeInterpolationMode.modeStateProperty().addListener((obs, oldv, newv) -> { InvokeOnJavaFXApplicationThread.invoke(() -> { - paintingProgressIndicator.setVisible(newv == ModeState.Interpolating); - if (newv == ModeState.Interpolating) - paintingProgressIndicatorTooltip.setText("Interpolating between sections..."); + final boolean showProgressIndicator = newv == ModeState.Interpolate; + paintingProgressIndicator.setVisible(showProgressIndicator); + paintingProgressIndicatorTooltip.setText(showProgressIndicator ? "Interpolating between sections..." : ""); }); }); From 59dde9370652ab52015f76dd5b18814cfb04250e Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Tue, 4 Jun 2019 15:41:26 -0400 Subject: [PATCH 49/84] allow to set mask created by generateMask() earlier in the label source --- .../paintera/data/mask/MaskedSource.java | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/data/mask/MaskedSource.java b/src/main/java/org/janelia/saalfeldlab/paintera/data/mask/MaskedSource.java index 1c2568f70..b07a4406a 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/data/mask/MaskedSource.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/data/mask/MaskedSource.java @@ -320,6 +320,41 @@ public Mask<UnsignedLongType> generateMask( return mask; } + public void setMask( + final Mask<UnsignedLongType> mask, + final Predicate<UnsignedLongType> isPaintedForeground) + throws MaskInUse + { + synchronized (this) + { + final boolean canSetMask = !isCreatingMask && currentMask == null && !isApplyingMask.get() && !isPersisting; + LOG.debug("Can set mask? {}", canSetMask); + if (!canSetMask) + { + LOG.error( + "Currently processing, cannot set new mask: persisting? {} mask in use? {}", + isPersisting, + currentMask + ); + throw new MaskInUse("Busy, cannot set new mask."); + } + } + + final RandomAccessibleInterval<UnsignedLongType> store; + if (mask.mask instanceof AccessedBlocksRandomAccessible<?>) + store = ((AccessedBlocksRandomAccessible<UnsignedLongType>) mask.mask).getSource(); + else + store = mask.mask; + final RandomAccessibleInterval<VolatileUnsignedLongType> vstore = VolatileViews.wrapAsVolatile(store); + + setMasks(store, vstore, mask.info.level, mask.info.value, isPaintedForeground); + + synchronized(this) + { + this.currentMask = mask; + } + } + public void setMask( final MaskInfo<UnsignedLongType> maskInfo, final RealRandomAccessible<UnsignedLongType> mask, From 11f1fd2215e9fe0391e1b066e1aa4ddff774b773 Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Tue, 4 Jun 2019 15:45:42 -0400 Subject: [PATCH 50/84] allow to edit selection when previewing shape interpolation results --- .../control/ShapeInterpolationMode.java | 145 ++++++++++++++---- 1 file changed, 115 insertions(+), 30 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java index b6c6959f9..c1b44ae6c 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java @@ -106,14 +106,23 @@ private static final class SelectedObjectInfo private static final class SectionInfo { final Mask<UnsignedLongType> mask; + final AffineTransform3D globalTransform; final AffineTransform3D sourceToDisplayTransform; final Interval sourceBoundingBox; - - SectionInfo(final Mask<UnsignedLongType> mask, final AffineTransform3D sourceToDisplayTransform, final Interval sourceBoundingBox) + final TLongObjectMap<SelectedObjectInfo> selectedObjects; + + SectionInfo( + final Mask<UnsignedLongType> mask, + final AffineTransform3D globalTransform, + final AffineTransform3D sourceToDisplayTransform, + final Interval sourceBoundingBox, + final TLongObjectMap<SelectedObjectInfo> selectedObjects) { this.mask = mask; + this.globalTransform = globalTransform; this.sourceToDisplayTransform = sourceToDisplayTransform; this.sourceBoundingBox = sourceBoundingBox; + this.selectedObjects = selectedObjects; } } @@ -141,16 +150,19 @@ private static final class SectionInfo private long lastSelectedId; private long[] lastActiveIds; - private final TLongObjectMap<SelectedObjectInfo> selectedObjects = new TLongObjectHashMap<>(); + private Mask<UnsignedLongType> mask; + private long newLabelId; private long currentFillValue; - private SectionInfo sectionInfo1, sectionInfo2; - private Mask<UnsignedLongType> mask; + private final TLongObjectMap<SelectedObjectInfo> selectedObjects = new TLongObjectHashMap<>(); - private ObjectProperty<ModeState> modeState = new SimpleObjectProperty<>(); - private long newLabelId; + private final ObjectProperty<SectionInfo> sectionInfo1 = new SimpleObjectProperty<>(); + private final ObjectProperty<SectionInfo> sectionInfo2 = new SimpleObjectProperty<>(); + + private final ObjectProperty<ModeState> modeState = new SimpleObjectProperty<>(); private Thread workerThread; + private ObjectProperty<SectionInfo> editedSectionInfo = null; public ShapeInterpolationMode( final MaskedSource<D, ?> source, @@ -231,6 +243,25 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke e -> isModeOn() && keyTracker.areOnlyTheseKeysDown(KeyCode.ESCAPE) ) ); + filter.addEventHandler( + KeyEvent.KEY_PRESSED, + EventFX.KEY_PRESSED( + "edit selection 1", + e -> {e.consume(); editSelection(paintera, sectionInfo1);}, + e -> modeState.get() == ModeState.Review && + keyTracker.areOnlyTheseKeysDown(KeyCode.DIGIT1) + ) + ); + filter.addEventHandler( + KeyEvent.KEY_PRESSED, + EventFX.KEY_PRESSED( + "edit selection 2", + e -> {e.consume(); editSelection(paintera, sectionInfo2);}, + e -> modeState.get() == ModeState.Review && + keyTracker.areOnlyTheseKeysDown(KeyCode.DIGIT2) + ) + ); + filter.addEventHandler(MouseEvent.ANY, new MouseClickFX( "select object in current section", e -> {e.consume(); selectObject(paintera, e.getX(), e.getY(), true);}, @@ -243,6 +274,7 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke ((e.isSecondaryButtonDown() && keyTracker.noKeysActive()) || (e.isPrimaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL)))) .handler()); + return filter; } @@ -294,7 +326,7 @@ public void exitMode(final PainteraBaseView paintera, final boolean completed) selectedIds.activate(lastActiveIds); selectedIds.activateAlso(lastSelectedId); - source.resetMasks(); + resetMask(); } converter.removeColor(newLabelId); @@ -305,9 +337,11 @@ public void exitMode(final PainteraBaseView paintera, final boolean completed) currentFillValue = 0; selectedObjects.clear(); - sectionInfo1 = sectionInfo2 = null; - mask = null; + sectionInfo1.set(null); + sectionInfo2.set(null); modeState.set(null); + editedSectionInfo = null; + mask = null; workerThread = null; lastSelectedId = Label.INVALID; @@ -330,6 +364,12 @@ private void createMask() throws MaskInUse mask = source.generateMask(maskInfo, FOREGROUND_CHECK); } + private void resetMask() + { + source.resetMasks(); + mask = null; + } + private void setDisableOtherViewers(final PainteraBaseView paintera, final boolean disable) { for (final ViewerPanelFX viewer : getViewerPanels(paintera)) @@ -354,36 +394,67 @@ private void setDisableOtherViewers(final PainteraBaseView paintera, final boole private void fixSelection(final PainteraBaseView paintera) { - if (sectionInfo1 == null) + final ObjectProperty<SectionInfo> sectionInfoPropertyToSet; + if (editedSectionInfo != null) + sectionInfoPropertyToSet = editedSectionInfo; + else if (sectionInfo1.get() == null) + sectionInfoPropertyToSet = sectionInfo1; + else + sectionInfoPropertyToSet = sectionInfo2; + + LOG.debug("Fix selection"); + sectionInfoPropertyToSet.set(createSectionInfo(paintera)); + selectedObjects.clear(); + editedSectionInfo = null; + + if (sectionInfo2.get() == null) { - LOG.debug("Fix selection in the first section"); - sectionInfo1 = createSectionInfo(); - selectedObjects.clear(); - source.resetMasks(); - mask = null; + // let the user now select the second section + resetMask(); activeViewerProperty().get().requestRepaint(); paintera.allowedActionsProperty().set(allowedActions); } else { - LOG.debug("Fix selection in the second section"); - sectionInfo2 = createSectionInfo(); + // both sections are ready, run interpolation interpolateBetweenSections(paintera); } } + private void editSelection(final PainteraBaseView paintera, final ObjectProperty<SectionInfo> sectionInfoProperty) + { + final SectionInfo sectionInfo = sectionInfoProperty.get(); + + resetMask(); + try { + source.setMask(sectionInfo.mask, FOREGROUND_CHECK); + } catch (final MaskInUse e) { + e.printStackTrace(); + } + mask = sectionInfo.mask; + + paintera.manager().setTransform(sectionInfo.globalTransform); + paintera.allowedActionsProperty().set(allowedActionsWhenSelected); + + selectedObjects.clear(); + selectedObjects.putAll(sectionInfo.selectedObjects); + + editedSectionInfo = sectionInfoProperty; + modeState.set(ModeState.Select); + } + private void applyMask(final PainteraBaseView paintera) { final Interval sectionsUnionSourceInterval = Intervals.union( - sectionInfo1.sourceBoundingBox, - sectionInfo2.sourceBoundingBox + sectionInfo1.get().sourceBoundingBox, + sectionInfo2.get().sourceBoundingBox ); LOG.info("Applying interpolated mask using bounding box of size {}", Intervals.dimensionsAsLongArray(sectionsUnionSourceInterval)); source.applyMask(source.getCurrentMask(), sectionsUnionSourceInterval, FOREGROUND_CHECK); exitMode(paintera, true); } - private SectionInfo createSectionInfo() + private SectionInfo createSectionInfo(final PainteraBaseView paintera) { Interval selectionSourceBoundingBox = null; for (final TLongObjectIterator<SelectedObjectInfo> it = selectedObjects.iterator(); it.hasNext();) @@ -394,10 +465,16 @@ private SectionInfo createSectionInfo() else selectionSourceBoundingBox = Intervals.union(selectionSourceBoundingBox, it.value().sourceBoundingBox); } + + final AffineTransform3D globalTransform = new AffineTransform3D(); + paintera.manager().getTransform(globalTransform); + return new SectionInfo( mask, + globalTransform, getMaskDisplayTransformIgnoreScaling(SHAPE_INTERPOLATION_SCALE_LEVEL), - selectionSourceBoundingBox + selectionSourceBoundingBox, + new TLongObjectHashMap<>(selectedObjects) ); } @@ -408,7 +485,7 @@ private void interpolateBetweenSections(final PainteraBaseView paintera) workerThread = new Thread(() -> { - final SectionInfo[] sectionInfoPair = {sectionInfo1, sectionInfo2}; + final SectionInfo[] sectionInfoPair = {sectionInfo1.get(), sectionInfo2.get()}; final Interval affectedUnionSourceInterval = Intervals.union( sectionInfoPair[0].sourceBoundingBox, @@ -419,7 +496,13 @@ private void interpolateBetweenSections(final PainteraBaseView paintera) final RandomAccessibleInterval<UnsignedLongType>[] sectionPair = new RandomAccessibleInterval[2]; for (int i = 0; i < 2; ++i) { - final SectionInfo newSectionInfo = new SectionInfo(sectionInfoPair[i].mask, sectionInfoPair[i].sourceToDisplayTransform, affectedUnionSourceInterval); + final SectionInfo newSectionInfo = new SectionInfo( + sectionInfoPair[i].mask, + sectionInfoPair[i].globalTransform, + sectionInfoPair[i].sourceToDisplayTransform, + affectedUnionSourceInterval, + sectionInfoPair[i].selectedObjects + ); final RandomAccessibleInterval<UnsignedLongType> section = getTransformedMaskSection(newSectionInfo); displaySectionIntervalPair[i] = new FinalInterval(section); sectionPair[i] = new ArrayImgFactory<>(new UnsignedLongType()).create(section); @@ -440,11 +523,11 @@ private void interpolateBetweenSections(final PainteraBaseView paintera) computeSignedDistanceTransform(binarySection, distanceTransformPair[i], DISTANCE_TYPE.EUCLIDIAN); } - final double distanceBetweenSections = computeDistanceBetweenSections(sectionInfo1, sectionInfo2); + final double distanceBetweenSections = computeDistanceBetweenSections(sectionInfoPair[0], sectionInfoPair[1]); final AffineTransform3D transformToSource = new AffineTransform3D(); transformToSource .preConcatenate(new Translation3D(displaySectionIntervalPair[0].min(0), displaySectionIntervalPair[0].min(1), 0)) - .preConcatenate(sectionInfo1.sourceToDisplayTransform.inverse()); + .preConcatenate(sectionInfoPair[0].sourceToDisplayTransform.inverse()); final RealRandomAccessible<UnsignedLongType> interpolatedShapeMask = getInterpolatedDistanceTransformMask( distanceTransformPair[0], @@ -469,9 +552,10 @@ private void interpolateBetweenSections(final PainteraBaseView paintera) { synchronized (source) { - source.resetMasks(); + final MaskInfo<UnsignedLongType> maskInfo = mask.info; + resetMask(); source.setMask( - mask.info, + maskInfo, interpolatedShapeMask, volatileInterpolatedShapeMask, FOREGROUND_CHECK @@ -579,6 +663,7 @@ private void selectObject(final PainteraBaseView paintera, final double x, final // create the mask if needed if (mask == null) { + LOG.debug("No selected objects yet, create mask"); try { createMask(); } catch (final MaskInUse e) { @@ -621,8 +706,8 @@ private void selectObject(final PainteraBaseView paintera, final double x, final // free the mask if there are no selected objects if (selectedObjects.isEmpty()) { - source.resetMasks(); - mask = null; + LOG.debug("No selected objects, reset mask"); + resetMask(); } activeViewer.get().requestRepaint(); From 8c45b8f01dcc3788271928d40c6faefd9e110b62 Mon Sep 17 00:00:00 2001 From: Igor Pisarev <igorpisarev@users.noreply.github.com> Date: Tue, 4 Jun 2019 16:19:35 -0400 Subject: [PATCH 51/84] describe section editing functionality in readme --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2a93e6247..ddd680b36 100644 --- a/README.md +++ b/README.md @@ -163,6 +163,7 @@ Usage: Paintera [-h] [--default-to-temp-directory] [--print-error-codes] | `Shift` + `F` + left click | Flood-fill with id that was last toggled active (if any) | | `N` | Select new, previously unused id | | `S` | Enter/advance shape interpolation mode | +| `1` / `2` | Edit first/second section when previewing interpolated shape | | `ESC` | Exit shape interpolation mode | | `Ctrl` + `C` | Show dialog to commit canvas and/or assignments | | `C` | Increment ARGB stream seed by one | @@ -180,7 +181,7 @@ Usage: Paintera [-h] [--default-to-temp-directory] [--print-error-codes] The mode is activated by pressing the `S` key when the current source is a label source. Then, you can select the objects in the first section by left/right clicking and hit `S` to fix the selection and switch to the second section (scrolling through sections works only when there are no selected objects). -When you're done with selecting the objects in the second section, hit `S` again to interpolate between the sections. The interpolated preview will be displayed, where you can hit `S` again to initiate committing the results into the canvas using a new label ID and return back to normal mode. +When you're done with selecting the objects in the second section, hit `S` again to interpolate between the sections. The interpolated preview will be displayed, where you can scroll through the sections and make sure that the interpolated shape is correct. If something is not right, you can edit the selection in the first or second section by pressing `1` or `2`, and then press `S` to update the interpolated shape. When the desired result is reached, hit `S` again to initiate committing the results into the canvas using a new label ID and return back to normal mode. While in the shape interpolation mode, at any point in time you can hit `ESC` to discard the current state and exit the mode. From 19d3448a40ec9a95d6a3286ef3fe8378a63e7803 Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Tue, 4 Jun 2019 17:00:36 -0400 Subject: [PATCH 52/84] generate mesh for interpolated shape once committed into canvas --- .../control/ShapeInterpolationMode.java | 20 +++++++++++++++++++ .../paintera/state/LabelSourceState.java | 4 ++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java index c1b44ae6c..f7c27016d 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java @@ -25,6 +25,7 @@ import org.janelia.saalfeldlab.paintera.data.mask.MaskedSource; import org.janelia.saalfeldlab.paintera.data.mask.exception.MaskInUse; import org.janelia.saalfeldlab.paintera.id.IdService; +import org.janelia.saalfeldlab.paintera.state.LabelSourceState; import org.janelia.saalfeldlab.paintera.stream.HighlightingStreamConverter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -36,6 +37,7 @@ import gnu.trove.map.hash.TLongObjectHashMap; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.value.ChangeListener; import javafx.event.Event; import javafx.event.EventHandler; import javafx.scene.effect.ColorAdjust; @@ -139,6 +141,7 @@ private static final class SectionInfo private final ObjectProperty<ViewerPanelFX> activeViewer = new SimpleObjectProperty<>(); private final MaskedSource<D, ?> source; + private final LabelSourceState<D, ?> sourceState; private final SelectedIds selectedIds; private final IdService idService; private final HighlightingStreamConverter<?> converter; @@ -146,6 +149,8 @@ private static final class SectionInfo private final AllowedActions allowedActions; private final AllowedActions allowedActionsWhenSelected; + private final ChangeListener<Boolean> doneApplyingMaskListener; + private AllowedActions lastAllowedActions; private long lastSelectedId; private long[] lastActiveIds; @@ -166,11 +171,13 @@ private static final class SectionInfo public ShapeInterpolationMode( final MaskedSource<D, ?> source, + final LabelSourceState<D, ?> sourceState, final SelectedIds selectedIds, final IdService idService, final HighlightingStreamConverter<?> converter) { this.source = source; + this.sourceState = sourceState; this.selectedIds = selectedIds; this.idService = idService; this.converter = converter; @@ -190,6 +197,11 @@ public ShapeInterpolationMode( MenuAction.of(MenuAction.ToggleMaximizeViewer), cleanup ); + + this.doneApplyingMaskListener = (obs, oldv, newv) -> { + if (!newv) + doneApplyingMask(); + }; } public ObjectProperty<ViewerPanelFX> activeViewerProperty() @@ -450,10 +462,18 @@ private void applyMask(final PainteraBaseView paintera) sectionInfo2.get().sourceBoundingBox ); LOG.info("Applying interpolated mask using bounding box of size {}", Intervals.dimensionsAsLongArray(sectionsUnionSourceInterval)); + source.isApplyingMaskProperty().addListener(doneApplyingMaskListener); source.applyMask(source.getCurrentMask(), sectionsUnionSourceInterval, FOREGROUND_CHECK); exitMode(paintera, true); } + private void doneApplyingMask() + { + // generate mesh for the interpolated shape + source.isApplyingMaskProperty().removeListener(doneApplyingMaskListener); + sourceState.refreshMeshes(); + } + private SectionInfo createSectionInfo(final PainteraBaseView paintera) { Interval selectionSourceBoundingBox = null; diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java index 99ea369e7..46f25103a 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java @@ -157,7 +157,7 @@ public LabelSourceState( this.idSelectorHandler = new LabelSourceStateIdSelectorHandler(dataSource, selectedIds, assignment, lockedSegments); this.mergeDetachHandler = new LabelSourceStateMergeDetachHandler(dataSource, selectedIds, assignment, idService); if (dataSource instanceof MaskedSource<?, ?>) - this.shapeInterpolationMode = new ShapeInterpolationMode<>((MaskedSource<D, ?>) dataSource, selectedIds, idService, converter); + this.shapeInterpolationMode = new ShapeInterpolationMode<>((MaskedSource<D, ?>) dataSource, this, selectedIds, idService, converter); else this.shapeInterpolationMode = null; this.displayStatus = createDisplayStatus(); @@ -468,7 +468,7 @@ public EventHandler<Event> stateSpecificGlobalEventHandler(final PainteraBaseVie handler.addEventHandler( KeyEvent.KEY_PRESSED, EventFX.KEY_PRESSED("refresh meshes", e -> {LOG.debug("Key event triggered refresh meshes"); refreshMeshes();}, e -> keyTracker.areOnlyTheseKeysDown(KeyCode.R))); - return handler; + return handler; } // @Override From b11b35d1cf0b88dc22c23f34b40a6495ad44e23a Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Wed, 5 Jun 2019 12:46:56 -0400 Subject: [PATCH 53/84] do not automatically hide side bar upon entering shape interpolation mode --- .../saalfeldlab/paintera/BorderPaneWithStatusBars.java | 5 ++--- .../saalfeldlab/paintera/PainteraDefaultHandlers.java | 7 ++----- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/BorderPaneWithStatusBars.java b/src/main/java/org/janelia/saalfeldlab/paintera/BorderPaneWithStatusBars.java index b1c0c9f53..987563be9 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/BorderPaneWithStatusBars.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/BorderPaneWithStatusBars.java @@ -279,9 +279,9 @@ public BorderPaneWithStatusBars( resizeSideBar = new ResizeOnLeftSide(sideBar, sideBar.prefWidthProperty(), dist -> Math.abs(dist) < 5); } - public boolean isSideBarActive() + public ScrollPane getSideBar() { - return pane.getRight() != null; + return sideBar; } public void toggleSideBar() @@ -291,7 +291,6 @@ public void toggleSideBar() pane.setRight(sideBar); resizeSideBar.install(); } - else { resizeSideBar.remove(); diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/PainteraDefaultHandlers.java b/src/main/java/org/janelia/saalfeldlab/paintera/PainteraDefaultHandlers.java index a4e0708dd..50b12d360 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/PainteraDefaultHandlers.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/PainteraDefaultHandlers.java @@ -235,16 +235,13 @@ public PainteraDefaultHandlers( final EventFX<KeyEvent> toggleSideBar = EventFX.KEY_RELEASED( "toggle sidebar", e -> paneWithStatus.toggleSideBar(), - e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.SidePanel) && keyTracker.areOnlyTheseKeysDown(KeyCode.P) + e -> keyTracker.areOnlyTheseKeysDown(KeyCode.P) ); borderPane.sceneProperty().addListener((obs, oldv, newv) -> newv.addEventHandler( KeyEvent.KEY_PRESSED, toggleSideBar)); - baseView.allowedActionsProperty().addListener((obs, oldv, newv) -> { - if (!newv.isAllowed(MenuAction.SidePanel) && paneWithStatus.isSideBarActive()) - paneWithStatus.toggleSideBar(); - }); + baseView.allowedActionsProperty().addListener((obs, oldv, newv) -> paneWithStatus.getSideBar().setDisable(!newv.isAllowed(MenuAction.SidePanel))); EventFX.KEY_PRESSED( "toggle interpolation", From 83fd6ec9ccab609949bcb4461f50354749935ef0 Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Wed, 5 Jun 2019 16:24:39 -0400 Subject: [PATCH 54/84] allow source state to set its status text for displaying --- .../paintera/BorderPaneWithStatusBars.java | 87 ++++++++++++------- .../paintera/state/MinimalSourceState.java | 8 ++ .../paintera/state/SourceInfo.java | 10 --- .../paintera/state/SourceState.java | 2 + 4 files changed, 67 insertions(+), 40 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/BorderPaneWithStatusBars.java b/src/main/java/org/janelia/saalfeldlab/paintera/BorderPaneWithStatusBars.java index 987563be9..4e91ee554 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/BorderPaneWithStatusBars.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/BorderPaneWithStatusBars.java @@ -4,34 +4,14 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; -import java.util.Optional; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; -import java.util.function.*; +import java.util.function.BiConsumer; +import java.util.function.LongSupplier; +import java.util.function.LongUnaryOperator; +import java.util.function.Supplier; -import bdv.fx.viewer.ViewerPanelFX; -import bdv.viewer.Source; -import javafx.animation.KeyFrame; -import javafx.animation.Timeline; -import javafx.beans.binding.Bindings; -import javafx.beans.property.*; -import javafx.beans.value.ObservableObjectValue; -import javafx.event.ActionEvent; -import javafx.event.EventHandler; -import javafx.scene.Group; -import javafx.scene.control.*; -import javafx.scene.control.ScrollPane.ScrollBarPolicy; -import javafx.scene.input.MouseEvent; -import javafx.scene.layout.BorderPane; -import javafx.scene.layout.HBox; -import javafx.scene.layout.Priority; -import javafx.scene.layout.Region; -import javafx.scene.layout.VBox; -import javafx.scene.paint.Color; -import javafx.scene.text.Font; -import javafx.util.Duration; -import net.imglib2.RealPoint; import org.janelia.saalfeldlab.fx.TitledPanes; import org.janelia.saalfeldlab.fx.ortho.OrthogonalViews; import org.janelia.saalfeldlab.fx.ortho.OrthogonalViews.ViewerAndTransforms; @@ -57,6 +37,40 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import bdv.fx.viewer.ViewerPanelFX; +import bdv.viewer.Source; +import javafx.animation.KeyFrame; +import javafx.animation.Timeline; +import javafx.beans.binding.Bindings; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.LongProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.ReadOnlyBooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.value.ObservableObjectValue; +import javafx.event.ActionEvent; +import javafx.event.EventHandler; +import javafx.scene.Group; +import javafx.scene.control.Alert; +import javafx.scene.control.Button; +import javafx.scene.control.ButtonType; +import javafx.scene.control.CheckBox; +import javafx.scene.control.Label; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.ScrollPane.ScrollBarPolicy; +import javafx.scene.control.TitledPane; +import javafx.scene.control.Tooltip; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; +import javafx.scene.paint.Color; +import javafx.scene.text.Font; +import javafx.util.Duration; +import net.imglib2.RealPoint; + public class BorderPaneWithStatusBars { @@ -160,16 +174,29 @@ public BorderPaneWithStatusBars( center.orthogonalViews(), center.viewer3D().meshesGroup(), center.sourceInfo() - ); - - center.sourceInfo().currentNameProperty().addListener((obs, oldv, newv) -> { - currentSourceStatus.textProperty().unbind(); - Optional.ofNullable(newv).ifPresent(currentSourceStatus.textProperty()::bind); - }); + ); final SingleChildStackPane sourceDisplayStatus = new SingleChildStackPane(); center.sourceInfo().currentState().addListener((obs, oldv, newv) -> sourceDisplayStatus.setChild(newv.getDisplayStatus())); + // show source name by default, or override it with source status text if any + center.sourceInfo().currentState().addListener((obs, oldv, newv) -> { + sourceDisplayStatus.setChild(newv.getDisplayStatus()); + currentSourceStatus.textProperty().unbind(); + currentSourceStatus.textProperty().bind(Bindings.createStringBinding( + () -> { + if (newv.statusTextProperty() != null && newv.statusTextProperty().get() != null) + return newv.statusTextProperty().get(); + else if (newv.nameProperty().get() != null) + return newv.nameProperty().get(); + else + return null; + }, + newv.nameProperty(), + newv.statusTextProperty() + )); + }); + // for positioning the 'show status bar' checkbox on the right final Region valueStatusSpacing = new Region(); HBox.setHgrow(valueStatusSpacing, Priority.ALWAYS); diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/state/MinimalSourceState.java b/src/main/java/org/janelia/saalfeldlab/paintera/state/MinimalSourceState.java index 966c1e8c7..dbc112fba 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/state/MinimalSourceState.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/state/MinimalSourceState.java @@ -34,6 +34,8 @@ public class MinimalSourceState<D, T, S extends DataSource<D, T>, C extends Conv private final StringProperty name; + private final StringProperty statusText = new SimpleStringProperty(null); + private final BooleanProperty isVisible = new SimpleBooleanProperty(true); private final BooleanProperty isDirty = new SimpleBooleanProperty(false); @@ -100,6 +102,12 @@ public StringProperty nameProperty() return this.name; } + @Override + public StringProperty statusTextProperty() + { + return this.statusText; + } + @Override public BooleanProperty isVisibleProperty() { diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/state/SourceInfo.java b/src/main/java/org/janelia/saalfeldlab/paintera/state/SourceInfo.java index a20676a41..bac6b123d 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/state/SourceInfo.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/state/SourceInfo.java @@ -12,7 +12,6 @@ import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleObjectProperty; -import javafx.beans.property.StringProperty; import javafx.beans.value.ObservableBooleanValue; import javafx.beans.value.ObservableIntegerValue; import javafx.beans.value.ObservableObjectValue; @@ -122,10 +121,6 @@ public class SourceInfo () -> Optional.ofNullable(currentSource.get()).map(this::getState).orElse(null), currentSource); - private final ObservableObjectValue<StringProperty> currentName = Bindings.createObjectBinding( - () -> Optional.ofNullable(currentState.get()).map(SourceState::nameProperty).orElse(null), - currentState); - private final ObservableMap<Source<?>, Composite<ARGBType, ARGBType>> composites = FXCollections .observableHashMap(); @@ -437,9 +432,4 @@ public int indexOf(final Source<?> source) return this.currentState; } - public ObservableObjectValue<StringProperty> currentNameProperty() - { - return this.currentName; - } - } diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/state/SourceState.java b/src/main/java/org/janelia/saalfeldlab/paintera/state/SourceState.java index cf27258bb..a4060bc05 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/state/SourceState.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/state/SourceState.java @@ -34,6 +34,8 @@ public interface SourceState<D, T> extends HasModifiableAxisOrder StringProperty nameProperty(); + StringProperty statusTextProperty(); + BooleanProperty isVisibleProperty(); ObservableBooleanValue isDirtyProperty(); From c0305a660449eacb63805d31e127b927ec4a5af0 Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Wed, 5 Jun 2019 16:25:57 -0400 Subject: [PATCH 55/84] display shape interpolation mode status on each step --- .../control/ShapeInterpolationMode.java | 90 ++++++++----- .../paintera/state/LabelSourceState.java | 122 +++++++++++------- 2 files changed, 131 insertions(+), 81 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java index f7c27016d..ccc13e8ef 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java @@ -88,7 +88,27 @@ public static enum ModeState { Select, Interpolate, - Review + Preview, + Edit + } + + public static enum ActiveSection + { + First("1"), + Second("2"); + + private final String s; + + private ActiveSection(final String s) + { + this.s = s; + } + + @Override + public String toString() + { + return s; + } } private static final class SelectedObjectInfo @@ -165,9 +185,9 @@ private static final class SectionInfo private final ObjectProperty<SectionInfo> sectionInfo2 = new SimpleObjectProperty<>(); private final ObjectProperty<ModeState> modeState = new SimpleObjectProperty<>(); + private final ObjectProperty<ActiveSection> activeSection = new SimpleObjectProperty<>(); private Thread workerThread; - private ObjectProperty<SectionInfo> editedSectionInfo = null; public ShapeInterpolationMode( final MaskedSource<D, ?> source, @@ -214,6 +234,11 @@ public ObjectProperty<ModeState> modeStateProperty() return modeState; } + public ObjectProperty<ActiveSection> activeSectionProperty() + { + return activeSection; + } + public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final KeyTracker keyTracker) { final DelegateEventHandlers.AnyHandler filter = DelegateEventHandlers.handleAny(); @@ -228,12 +253,20 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke keyTracker.areOnlyTheseKeysDown(KeyCode.S) ) ); + filter.addEventHandler( + KeyEvent.KEY_PRESSED, + EventFX.KEY_PRESSED( + "exit shape interpolation mode", + e -> {e.consume(); exitMode(paintera, false);}, + e -> isModeOn() && keyTracker.areOnlyTheseKeysDown(KeyCode.ESCAPE) + ) + ); filter.addEventHandler( KeyEvent.KEY_PRESSED, EventFX.KEY_PRESSED( "fix selection", e -> {e.consume(); fixSelection(paintera);}, - e -> modeState.get() == ModeState.Select && + e -> (modeState.get() == ModeState.Select || modeState.get() == ModeState.Edit) && !selectedObjects.isEmpty() && keyTracker.areOnlyTheseKeysDown(KeyCode.S) ) @@ -243,24 +276,16 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke EventFX.KEY_PRESSED( "apply mask", e -> {e.consume(); applyMask(paintera);}, - e -> modeState.get() == ModeState.Review && + e -> modeState.get() == ModeState.Preview && keyTracker.areOnlyTheseKeysDown(KeyCode.S) ) ); - filter.addEventHandler( - KeyEvent.KEY_PRESSED, - EventFX.KEY_PRESSED( - "exit shape interpolation mode", - e -> {e.consume(); exitMode(paintera, false);}, - e -> isModeOn() && keyTracker.areOnlyTheseKeysDown(KeyCode.ESCAPE) - ) - ); filter.addEventHandler( KeyEvent.KEY_PRESSED, EventFX.KEY_PRESSED( "edit selection 1", - e -> {e.consume(); editSelection(paintera, sectionInfo1);}, - e -> modeState.get() == ModeState.Review && + e -> {e.consume(); editSelection(paintera, ActiveSection.First);}, + e -> modeState.get() == ModeState.Preview && keyTracker.areOnlyTheseKeysDown(KeyCode.DIGIT1) ) ); @@ -268,8 +293,8 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke KeyEvent.KEY_PRESSED, EventFX.KEY_PRESSED( "edit selection 2", - e -> {e.consume(); editSelection(paintera, sectionInfo2);}, - e -> modeState.get() == ModeState.Review && + e -> {e.consume(); editSelection(paintera, ActiveSection.Second);}, + e -> modeState.get() == ModeState.Preview && keyTracker.areOnlyTheseKeysDown(KeyCode.DIGIT2) ) ); @@ -277,12 +302,12 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke filter.addEventHandler(MouseEvent.ANY, new MouseClickFX( "select object in current section", e -> {e.consume(); selectObject(paintera, e.getX(), e.getY(), true);}, - e -> modeState.get() == ModeState.Select && e.isPrimaryButtonDown() && keyTracker.noKeysActive()) + e -> (modeState.get() == ModeState.Select || modeState.get() == ModeState.Edit) && e.isPrimaryButtonDown() && keyTracker.noKeysActive()) .handler()); filter.addEventHandler(MouseEvent.ANY, new MouseClickFX( "toggle object in current section", e -> {e.consume(); selectObject(paintera, e.getX(), e.getY(), false);}, - e -> modeState.get() == ModeState.Select && + e -> (modeState.get() == ModeState.Select || modeState.get() == ModeState.Edit) && ((e.isSecondaryButtonDown() && keyTracker.noKeysActive()) || (e.isPrimaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL)))) .handler()); @@ -310,6 +335,7 @@ public void enterMode(final PainteraBaseView paintera, final ViewerPanelFX viewe converter.setColor(newLabelId, MASK_COLOR); selectedIds.activate(newLabelId); + activeSection.set(ActiveSection.First); modeState.set(ModeState.Select); } @@ -352,7 +378,7 @@ public void exitMode(final PainteraBaseView paintera, final boolean completed) sectionInfo1.set(null); sectionInfo2.set(null); modeState.set(null); - editedSectionInfo = null; + activeSection.set(null); mask = null; workerThread = null; @@ -406,22 +432,15 @@ private void setDisableOtherViewers(final PainteraBaseView paintera, final boole private void fixSelection(final PainteraBaseView paintera) { - final ObjectProperty<SectionInfo> sectionInfoPropertyToSet; - if (editedSectionInfo != null) - sectionInfoPropertyToSet = editedSectionInfo; - else if (sectionInfo1.get() == null) - sectionInfoPropertyToSet = sectionInfo1; - else - sectionInfoPropertyToSet = sectionInfo2; - + final ObjectProperty<SectionInfo> sectionInfoPropertyToSet = activeSection.get() == ActiveSection.First ? sectionInfo1 : sectionInfo2; LOG.debug("Fix selection"); sectionInfoPropertyToSet.set(createSectionInfo(paintera)); selectedObjects.clear(); - editedSectionInfo = null; - if (sectionInfo2.get() == null) + if (modeState.get() != ModeState.Edit && sectionInfo2.get() == null) { // let the user now select the second section + activeSection.set(ActiveSection.Second); resetMask(); activeViewerProperty().get().requestRepaint(); paintera.allowedActionsProperty().set(allowedActions); @@ -429,13 +448,15 @@ else if (sectionInfo1.get() == null) else { // both sections are ready, run interpolation + activeSection.set(null); interpolateBetweenSections(paintera); } } - private void editSelection(final PainteraBaseView paintera, final ObjectProperty<SectionInfo> sectionInfoProperty) + private void editSelection(final PainteraBaseView paintera, final ActiveSection section) { - final SectionInfo sectionInfo = sectionInfoProperty.get(); + final ObjectProperty<SectionInfo> sectionInfoPropertyToEdit = section == ActiveSection.First ? sectionInfo1 : sectionInfo2; + final SectionInfo sectionInfo = sectionInfoPropertyToEdit.get(); resetMask(); try { @@ -451,8 +472,9 @@ private void editSelection(final PainteraBaseView paintera, final ObjectProperty selectedObjects.clear(); selectedObjects.putAll(sectionInfo.selectedObjects); - editedSectionInfo = sectionInfoProperty; - modeState.set(ModeState.Select); + sectionInfoPropertyToEdit.set(null); + activeSection.set(section); + modeState.set(ModeState.Edit); } private void applyMask(final PainteraBaseView paintera) @@ -591,7 +613,7 @@ private void interpolateBetweenSections(final PainteraBaseView paintera) } InvokeOnJavaFXApplicationThread.invoke(() -> { - modeState.set(ModeState.Review); + modeState.set(ModeState.Preview); paintera.allowedActionsProperty().set(allowedActions); }); }); diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java index 46f25103a..7f0bf5123 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java @@ -1,5 +1,51 @@ package org.janelia.saalfeldlab.paintera.state; +import java.lang.invoke.MethodHandles; +import java.util.Arrays; +import java.util.concurrent.ExecutorService; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.LongFunction; +import java.util.function.ToLongFunction; + +import org.janelia.saalfeldlab.fx.event.DelegateEventHandlers; +import org.janelia.saalfeldlab.fx.event.EventFX; +import org.janelia.saalfeldlab.fx.event.KeyTracker; +import org.janelia.saalfeldlab.fx.util.InvokeOnJavaFXApplicationThread; +import org.janelia.saalfeldlab.labels.blocks.LabelBlockLookup; +import org.janelia.saalfeldlab.paintera.PainteraBaseView; +import org.janelia.saalfeldlab.paintera.cache.InvalidateAll; +import org.janelia.saalfeldlab.paintera.cache.global.GlobalCache; +import org.janelia.saalfeldlab.paintera.composition.ARGBCompositeAlphaYCbCr; +import org.janelia.saalfeldlab.paintera.composition.Composite; +import org.janelia.saalfeldlab.paintera.control.ShapeInterpolationMode; +import org.janelia.saalfeldlab.paintera.control.ShapeInterpolationMode.ActiveSection; +import org.janelia.saalfeldlab.paintera.control.ShapeInterpolationMode.ModeState; +import org.janelia.saalfeldlab.paintera.control.assignment.FragmentSegmentAssignmentOnlyLocal; +import org.janelia.saalfeldlab.paintera.control.assignment.FragmentSegmentAssignmentState; +import org.janelia.saalfeldlab.paintera.control.lock.LockedSegmentsOnlyLocal; +import org.janelia.saalfeldlab.paintera.control.lock.LockedSegmentsState; +import org.janelia.saalfeldlab.paintera.control.selection.SelectedIds; +import org.janelia.saalfeldlab.paintera.data.DataSource; +import org.janelia.saalfeldlab.paintera.data.RandomAccessibleIntervalDataSource; +import org.janelia.saalfeldlab.paintera.data.axisorder.AxisOrder; +import org.janelia.saalfeldlab.paintera.data.mask.Mask; +import org.janelia.saalfeldlab.paintera.data.mask.MaskedSource; +import org.janelia.saalfeldlab.paintera.id.IdService; +import org.janelia.saalfeldlab.paintera.id.LocalIdService; +import org.janelia.saalfeldlab.paintera.meshes.InterruptibleFunction; +import org.janelia.saalfeldlab.paintera.meshes.ManagedMeshSettings; +import org.janelia.saalfeldlab.paintera.meshes.MeshManager; +import org.janelia.saalfeldlab.paintera.meshes.MeshManagerWithAssignmentForSegments; +import org.janelia.saalfeldlab.paintera.stream.AbstractHighlightingARGBStream; +import org.janelia.saalfeldlab.paintera.stream.HighlightingStreamConverter; +import org.janelia.saalfeldlab.paintera.stream.HighlightingStreamConverterIntegerType; +import org.janelia.saalfeldlab.paintera.stream.ModalGoldenAngleSaturatedHighlightingARGBStream; +import org.janelia.saalfeldlab.util.Colors; +import org.janelia.saalfeldlab.util.grids.LabelBlockLookupNoBlocks; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import bdv.util.volatiles.VolatileTypeMatcher; import gnu.trove.set.hash.TLongHashSet; import javafx.beans.InvalidationListener; @@ -42,52 +88,8 @@ import net.imglib2.util.Intervals; import net.imglib2.util.Util; import net.imglib2.view.Views; -import org.janelia.saalfeldlab.fx.event.DelegateEventHandlers; -import org.janelia.saalfeldlab.fx.event.EventFX; -import org.janelia.saalfeldlab.fx.event.KeyTracker; -import org.janelia.saalfeldlab.fx.util.InvokeOnJavaFXApplicationThread; -import org.janelia.saalfeldlab.labels.blocks.LabelBlockLookup; -import org.janelia.saalfeldlab.paintera.PainteraBaseView; -import org.janelia.saalfeldlab.paintera.cache.InvalidateAll; -import org.janelia.saalfeldlab.paintera.cache.global.GlobalCache; -import org.janelia.saalfeldlab.paintera.composition.ARGBCompositeAlphaYCbCr; -import org.janelia.saalfeldlab.paintera.composition.Composite; -import org.janelia.saalfeldlab.paintera.control.ShapeInterpolationMode; -import org.janelia.saalfeldlab.paintera.control.ShapeInterpolationMode.ModeState; -import org.janelia.saalfeldlab.paintera.control.assignment.FragmentSegmentAssignmentOnlyLocal; -import org.janelia.saalfeldlab.paintera.control.assignment.FragmentSegmentAssignmentState; -import org.janelia.saalfeldlab.paintera.control.lock.LockedSegmentsOnlyLocal; -import org.janelia.saalfeldlab.paintera.control.lock.LockedSegmentsState; -import org.janelia.saalfeldlab.paintera.control.selection.SelectedIds; -import org.janelia.saalfeldlab.paintera.data.DataSource; -import org.janelia.saalfeldlab.paintera.data.RandomAccessibleIntervalDataSource; -import org.janelia.saalfeldlab.paintera.data.axisorder.AxisOrder; -import org.janelia.saalfeldlab.paintera.data.mask.Mask; -import org.janelia.saalfeldlab.paintera.data.mask.MaskedSource; -import org.janelia.saalfeldlab.paintera.id.IdService; -import org.janelia.saalfeldlab.paintera.id.LocalIdService; -import org.janelia.saalfeldlab.paintera.meshes.InterruptibleFunction; -import org.janelia.saalfeldlab.paintera.meshes.ManagedMeshSettings; -import org.janelia.saalfeldlab.paintera.meshes.MeshManager; -import org.janelia.saalfeldlab.paintera.meshes.MeshManagerWithAssignmentForSegments; -import org.janelia.saalfeldlab.paintera.stream.AbstractHighlightingARGBStream; -import org.janelia.saalfeldlab.paintera.stream.HighlightingStreamConverter; -import org.janelia.saalfeldlab.paintera.stream.HighlightingStreamConverterIntegerType; -import org.janelia.saalfeldlab.paintera.stream.ModalGoldenAngleSaturatedHighlightingARGBStream; -import org.janelia.saalfeldlab.util.Colors; -import org.janelia.saalfeldlab.util.grids.LabelBlockLookupNoBlocks; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import pl.touk.throwing.ThrowingFunction; -import java.lang.invoke.MethodHandles; -import java.util.Arrays; -import java.util.concurrent.ExecutorService; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.LongFunction; -import java.util.function.ToLongFunction; - public class LabelSourceState<D extends IntegerType<D>, T> extends MinimalSourceState<D, T, DataSource<D, T>, HighlightingStreamConverter<T>> @@ -607,13 +609,39 @@ private HBox createDisplayStatus() }); }); - this.shapeInterpolationMode.modeStateProperty().addListener((obs, oldv, newv) -> { + final InvalidationListener shapeInterpolationModeStatusUpdater = obs -> { InvokeOnJavaFXApplicationThread.invoke(() -> { - final boolean showProgressIndicator = newv == ModeState.Interpolate; + final ModeState modeState = this.shapeInterpolationMode.modeStateProperty().get(); + final ActiveSection activeSection = this.shapeInterpolationMode.activeSectionProperty().get(); + if (modeState != null) { + switch (modeState) { + case Select: + statusTextProperty().set("Select #" + activeSection); + break; + case Edit: + statusTextProperty().set("Edit #" + activeSection); + break; + case Interpolate: + statusTextProperty().set("Interpolating"); + break; + case Preview: + statusTextProperty().set("Preview"); + break; + default: + statusTextProperty().set(null); + break; + } + } else { + statusTextProperty().set(null); + } + final boolean showProgressIndicator = modeState == ModeState.Interpolate; paintingProgressIndicator.setVisible(showProgressIndicator); paintingProgressIndicatorTooltip.setText(showProgressIndicator ? "Interpolating between sections..." : ""); }); - }); + }; + + this.shapeInterpolationMode.modeStateProperty().addListener(shapeInterpolationModeStatusUpdater); + this.shapeInterpolationMode.activeSectionProperty().addListener(shapeInterpolationModeStatusUpdater); final HBox displayStatus = new HBox(5, lastSelectedLabelColorRect, From cf532eb71af9b1583b3f6ebc34d43f1987287d18 Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Wed, 5 Jun 2019 17:34:47 -0400 Subject: [PATCH 56/84] improve thread synchronization in MaskedSource previously, applyMask and persistCanvas used to synchronize on this until the process is completed. This was causing the application to freeze when, for example, a new mask is requested while the last one is still being committed into the canvas. Now the application will not block anymore and instead show a dialog suggesting to wait until the process is completed. --- .../control/ShapeInterpolationMode.java | 6 +- .../paintera/control/paint/FloodFill.java | 29 +- .../paintera/data/mask/MaskedSource.java | 338 +++++++++--------- 3 files changed, 193 insertions(+), 180 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java index ccc13e8ef..38f26a0ad 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java @@ -404,7 +404,11 @@ private void createMask() throws MaskInUse private void resetMask() { - source.resetMasks(); + try { + source.resetMasks(); + } catch (final MaskInUse e) { + e.printStackTrace(); + } mask = null; } diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill.java index 6eb77a696..41bbf7da2 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill.java @@ -10,6 +10,18 @@ import java.util.function.Predicate; import java.util.function.Supplier; +import org.janelia.saalfeldlab.paintera.data.mask.Mask; +import org.janelia.saalfeldlab.paintera.data.mask.MaskInfo; +import org.janelia.saalfeldlab.paintera.data.mask.MaskedSource; +import org.janelia.saalfeldlab.paintera.data.mask.exception.MaskInUse; +import org.janelia.saalfeldlab.paintera.state.HasFloodFillState; +import org.janelia.saalfeldlab.paintera.state.HasFloodFillState.FloodFillState; +import org.janelia.saalfeldlab.paintera.state.HasMaskForLabel; +import org.janelia.saalfeldlab.paintera.state.SourceInfo; +import org.janelia.saalfeldlab.paintera.state.SourceState; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import bdv.fx.viewer.ViewerPanelFX; import bdv.fx.viewer.ViewerState; import bdv.viewer.Source; @@ -34,17 +46,6 @@ import net.imglib2.util.Intervals; import net.imglib2.util.Util; import net.imglib2.view.Views; -import org.janelia.saalfeldlab.paintera.data.mask.Mask; -import org.janelia.saalfeldlab.paintera.data.mask.exception.MaskInUse; -import org.janelia.saalfeldlab.paintera.data.mask.MaskInfo; -import org.janelia.saalfeldlab.paintera.data.mask.MaskedSource; -import org.janelia.saalfeldlab.paintera.state.HasFloodFillState; -import org.janelia.saalfeldlab.paintera.state.HasMaskForLabel; -import org.janelia.saalfeldlab.paintera.state.SourceInfo; -import org.janelia.saalfeldlab.paintera.state.SourceState; -import org.janelia.saalfeldlab.paintera.state.HasFloodFillState.FloodFillState; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; public class FloodFill { @@ -270,7 +271,11 @@ private <T extends IntegerType<T>> void fill( if (Thread.interrupted()) { floodFillThread.interrupt(); - source.resetMasks(); + try { + source.resetMasks(); + } catch (final MaskInUse e) { + e.printStackTrace(); + } } else { diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/data/mask/MaskedSource.java b/src/main/java/org/janelia/saalfeldlab/paintera/data/mask/MaskedSource.java index b07a4406a..41cd978b7 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/data/mask/MaskedSource.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/data/mask/MaskedSource.java @@ -1,5 +1,37 @@ package org.janelia.saalfeldlab.paintera.data.mask; +import java.lang.invoke.MethodHandles; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.concurrent.ExecutorService; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.DoubleStream; +import java.util.stream.IntStream; +import java.util.stream.LongStream; +import java.util.stream.Stream; + +import org.janelia.saalfeldlab.fx.util.InvokeOnJavaFXApplicationThread; +import org.janelia.saalfeldlab.paintera.data.DataSource; +import org.janelia.saalfeldlab.paintera.data.mask.PickOne.PickAndConvert; +import org.janelia.saalfeldlab.paintera.data.mask.exception.CannotClearCanvas; +import org.janelia.saalfeldlab.paintera.data.mask.exception.CannotPersist; +import org.janelia.saalfeldlab.paintera.data.mask.exception.MaskInUse; +import org.janelia.saalfeldlab.paintera.data.mask.persist.PersistCanvas; +import org.janelia.saalfeldlab.paintera.data.mask.persist.UnableToPersistCanvas; +import org.janelia.saalfeldlab.paintera.data.mask.persist.UnableToUpdateLabelBlockLookup; +import org.janelia.saalfeldlab.paintera.data.n5.BlockSpec; +import org.janelia.saalfeldlab.paintera.ui.PainteraAlerts; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import bdv.util.volatiles.VolatileViews; import bdv.viewer.Interpolation; import gnu.trove.iterator.TLongIterator; @@ -69,37 +101,6 @@ import net.imglib2.view.IntervalView; import net.imglib2.view.RealRandomAccessibleTriple; import net.imglib2.view.Views; -import org.janelia.saalfeldlab.fx.util.InvokeOnJavaFXApplicationThread; -import org.janelia.saalfeldlab.paintera.data.DataSource; -import org.janelia.saalfeldlab.paintera.data.mask.PickOne.PickAndConvert; -import org.janelia.saalfeldlab.paintera.data.mask.exception.CannotClearCanvas; -import org.janelia.saalfeldlab.paintera.data.mask.exception.CannotPersist; -import org.janelia.saalfeldlab.paintera.data.mask.exception.MaskInUse; -import org.janelia.saalfeldlab.paintera.data.mask.persist.PersistCanvas; -import org.janelia.saalfeldlab.paintera.data.mask.persist.UnableToPersistCanvas; -import org.janelia.saalfeldlab.paintera.data.mask.persist.UnableToUpdateLabelBlockLookup; -import org.janelia.saalfeldlab.paintera.data.n5.BlockSpec; -import org.janelia.saalfeldlab.paintera.ui.PainteraAlerts; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.lang.invoke.MethodHandles; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Optional; -import java.util.concurrent.ExecutorService; -import java.util.function.Predicate; -import java.util.function.Supplier; -import java.util.stream.DoubleStream; -import java.util.stream.IntStream; -import java.util.stream.LongStream; -import java.util.stream.Stream; /** * @@ -283,7 +284,6 @@ public Mask<UnsignedLongType> generateMask( final Predicate<UnsignedLongType> isPaintedForeground) throws MaskInUse { - LOG.debug("Asking for mask: {}", maskInfo); synchronized (this) { @@ -312,10 +312,10 @@ public Mask<UnsignedLongType> generateMask( ((AbstractCellImg<?,?,?,?>)store).getCellGrid() ); final Mask<UnsignedLongType> mask = new Mask<>(maskInfo, trackingStore); - synchronized(this) + synchronized (this) { - this.isCreatingMask = false; this.currentMask = mask; + this.isCreatingMask = false; } return mask; } @@ -338,6 +338,7 @@ public void setMask( ); throw new MaskInUse("Busy, cannot set new mask."); } + this.isCreatingMask = true; } final RandomAccessibleInterval<UnsignedLongType> store; @@ -349,9 +350,10 @@ public void setMask( setMasks(store, vstore, mask.info.level, mask.info.value, isPaintedForeground); - synchronized(this) + synchronized (this) { this.currentMask = mask; + this.isCreatingMask = false; } } @@ -375,14 +377,16 @@ public void setMask( ); throw new MaskInUse("Busy, cannot set new mask."); } + this.isCreatingMask = true; } setMasks(mask, vmask, maskInfo.level, maskInfo.value, isPaintedForeground); - synchronized(this) + synchronized (this) { final RandomAccessibleInterval<UnsignedLongType> rasteredMask = Views.interval(Views.raster(mask), source.getSource(0, maskInfo.level)); this.currentMask = new Mask<>(maskInfo, rasteredMask); + this.isCreatingMask = false; } } @@ -404,60 +408,63 @@ public void applyMask( return; } this.isApplyingMask.set(true); + } - LOG.debug("Applying mask: {}", mask, paintedInterval); - final MaskInfo<UnsignedLongType> maskInfo = mask.info; - final CachedCellImg<UnsignedLongType, ?> canvas = dataCanvases[maskInfo.level]; - final CellGrid grid = canvas.getCellGrid(); + LOG.debug("Applying mask: {}", mask, paintedInterval); + final MaskInfo<UnsignedLongType> maskInfo = mask.info; + final CachedCellImg<UnsignedLongType, ?> canvas = dataCanvases[maskInfo.level]; + final CellGrid grid = canvas.getCellGrid(); - final int[] blockSize = new int[grid.numDimensions()]; - grid.cellDimensions(blockSize); + final int[] blockSize = new int[grid.numDimensions()]; + grid.cellDimensions(blockSize); - final TLongSet affectedBlocks = affectedBlocks(mask.mask, canvas.getCellGrid(), paintedInterval); + final TLongSet affectedBlocks = affectedBlocks(mask.mask, canvas.getCellGrid(), paintedInterval); - paintAffectedPixels( - affectedBlocks, - Converters.convert( - Views.extendZero(mask.mask), - (s, t) -> t.set(acceptAsPainted.test(s)), - new BitType()), - canvas, - maskInfo.value, - canvas.getCellGrid(), - paintedInterval); + paintAffectedPixels( + affectedBlocks, + Converters.convert( + Views.extendZero(mask.mask), + (s, t) -> t.set(acceptAsPainted.test(s)), + new BitType()), + canvas, + maskInfo.value, + canvas.getCellGrid(), + paintedInterval); - forgetMasks(); + synchronized (this) + { + this.currentMask = null; + } - final TLongSet paintedBlocksAtHighestResolution = this.scaleBlocksToLevel( + final TLongSet paintedBlocksAtHighestResolution = this.scaleBlocksToLevel( + affectedBlocks, + maskInfo.level, + 0); + + this.affectedBlocksByLabel[maskInfo.level].computeIfAbsent( + maskInfo.value.getIntegerLong(), + key -> new TLongHashSet() + ).addAll(affectedBlocks); + LOG.debug("Added affected block: {}", affectedBlocksByLabel[maskInfo.level]); + this.affectedBlocks.addAll(paintedBlocksAtHighestResolution); + + propagationExecutor.submit(() -> { + propagateMask( + mask.mask, affectedBlocks, maskInfo.level, - 0); - - this.affectedBlocksByLabel[maskInfo.level].computeIfAbsent( - maskInfo.value.getIntegerLong(), - key -> new TLongHashSet() - ).addAll(affectedBlocks); - LOG.debug("Added affected block: {}", affectedBlocksByLabel[maskInfo.level]); - this.affectedBlocks.addAll(paintedBlocksAtHighestResolution); - - propagationExecutor.submit(() -> { - propagateMask( - mask.mask, - affectedBlocks, - maskInfo.level, - maskInfo.value, - paintedInterval, - acceptAsPainted - ); - setMasksConstant(); - synchronized(MaskedSource.this) - { - LOG.debug("Done applying mask!"); - MaskedSource.this.isApplyingMask.set(false); - } - }); + maskInfo.value, + paintedInterval, + acceptAsPainted + ); + setMasksConstant(); + synchronized (this) + { + LOG.debug("Done applying mask!"); + this.isApplyingMask.set(false); + } + }); - } }).start(); } @@ -518,21 +525,20 @@ private void scalePositionToLevel(final long[] position, final int intervalLevel toTargetScale.apply(positionDouble, positionDouble); Arrays.setAll(targetPosition, d -> (long) Math.ceil(positionDouble[d])); - } - public synchronized void resetMasks() - { - forgetMasks(); - setMasksConstant(); - } - - public void forgetMasks() + public void resetMasks() throws MaskInUse { synchronized (this) { + final boolean canResetMask = !isCreatingMask && !isApplyingMask.get(); + LOG.debug("Can reset mask? {}", canResetMask); + if (!canResetMask) + throw new MaskInUse("Busy, cannot reset mask."); + this.currentMask = null; } + setMasksConstant(); } public void forgetCanvases() throws CannotClearCanvas @@ -540,102 +546,100 @@ public void forgetCanvases() throws CannotClearCanvas synchronized (this) { if (this.isPersisting) - { throw new CannotClearCanvas("Currently persisting canvas -- try again later."); - } this.currentMask = null; - clearCanvases(); } + clearCanvases(); } public void persistCanvas() throws CannotPersist { synchronized (this) { - if (!this.isCreatingMask && this.currentMask == null && !this.isApplyingMask.get() && !this.isPersisting) - { - this.isPersisting = true; - LOG.debug("Merging canvas into background for blocks {}", this.affectedBlocks); - final CachedCellImg<UnsignedLongType, ?> canvas = this.dataCanvases[0]; - final long[] affectedBlocks = this.affectedBlocks.toArray(); - this.affectedBlocks.clear(); - final MaskedSource<D, T> thiz = this; - final BooleanProperty proxy = new SimpleBooleanProperty(this.isPersisting); - final ObservableList<String> states = FXCollections.observableArrayList(); - final Runnable dialogHandler = () -> { - LOG.warn("Creating commit status dialog."); - final Alert isCommittingDialog = PainteraAlerts.alert(Alert.AlertType.INFORMATION); - isCommittingDialog.setHeaderText("Committing canvas."); - isCommittingDialog.getDialogPane().lookupButton(ButtonType.OK).setDisable(true); - isCommittingDialog.initModality(Modality.NONE); - states.addListener((ListChangeListener<? super String>) change -> InvokeOnJavaFXApplicationThread.invoke(() -> isCommittingDialog.getDialogPane().setContent(new VBox(asLabels(states))))); - synchronized(MaskedSource.this) { - isCommittingDialog.getDialogPane().lookupButton(ButtonType.OK).disableProperty().bind(proxy); - } - LOG.info("Will show dialog? {}", proxy.get()); - if(proxy.get()) isCommittingDialog.show(); - }; - new Thread(() -> { - Exception caughtException = null; - try - { - try { - InvokeOnJavaFXApplicationThread.invokeAndWait(dialogHandler); - } - catch (final InterruptedException e) - { - throw new RuntimeException(e); - } - try { - states.add("Persisting painted labels..."); - final List<TLongObjectMap<PersistCanvas.BlockDiff>> blockDiffs = this.persistCanvas.persistCanvas(canvas, affectedBlocks); - states.set(states.size() - 1, "Persisting painted labels... Done"); - if (this.persistCanvas.supportsLabelBlockLookupUpdate()) { - states.add("Updating label-to-block lookup..."); - this.persistCanvas.updateLabelBlockLookup(blockDiffs); - states.set(states.size() - 1, "Updating label-to-block lookup... Done"); - } - states.add("Clearing canvases..."); - clearCanvases(); - states.set(states.size() - 1, "Clearing canvases... Done"); - this.source.invalidateAll(); - } - catch (UnableToPersistCanvas | UnableToUpdateLabelBlockLookup e) - { - caughtException = e; - throw new RuntimeException("Error while trying to persist.", e); - } - catch (final RuntimeException e) - { - caughtException = e; - throw e; - } - } finally - { - synchronized (thiz) - { - thiz.isPersisting = false; - proxy.set(false); - if (caughtException == null) - states.add("Successfully finished committing canvas."); - else - states.add("Unable to commit canvas: " + caughtException.getMessage()); - } - } - }).start(); - } - else + final boolean canPersist = !this.isCreatingMask && this.currentMask == null && !this.isApplyingMask.get() && !this.isPersisting; + if (!canPersist) { LOG.error( "Cannot persist canvas: is persisting? {} has mask? {} is creating mask? {} is applying mask? {}", - isPersisting, + this.isPersisting, this.currentMask != null, this.isCreatingMask, this.isApplyingMask - ); + ); throw new CannotPersist("Can not persist canvas!"); } + this.isPersisting = true; } + + LOG.debug("Merging canvas into background for blocks {}", this.affectedBlocks); + final CachedCellImg<UnsignedLongType, ?> canvas = this.dataCanvases[0]; + final long[] affectedBlocks = this.affectedBlocks.toArray(); + this.affectedBlocks.clear(); + final BooleanProperty proxy = new SimpleBooleanProperty(this.isPersisting); + final ObservableList<String> states = FXCollections.observableArrayList(); + final Runnable dialogHandler = () -> { + LOG.warn("Creating commit status dialog."); + final Alert isCommittingDialog = PainteraAlerts.alert(Alert.AlertType.INFORMATION); + isCommittingDialog.setHeaderText("Committing canvas."); + isCommittingDialog.getDialogPane().lookupButton(ButtonType.OK).setDisable(true); + isCommittingDialog.initModality(Modality.NONE); + states.addListener((ListChangeListener<? super String>) change -> InvokeOnJavaFXApplicationThread.invoke(() -> isCommittingDialog.getDialogPane().setContent(new VBox(asLabels(states))))); + synchronized (this) { + isCommittingDialog.getDialogPane().lookupButton(ButtonType.OK).disableProperty().bind(proxy); + } + LOG.info("Will show dialog? {}", proxy.get()); + if(proxy.get()) isCommittingDialog.show(); + }; + new Thread(() -> { + Exception caughtException = null; + try + { + try { + InvokeOnJavaFXApplicationThread.invokeAndWait(dialogHandler); + } + catch (final InterruptedException e) + { + throw new RuntimeException(e); + } + try { + states.add("Persisting painted labels..."); + final List<TLongObjectMap<PersistCanvas.BlockDiff>> blockDiffs = this.persistCanvas.persistCanvas(canvas, affectedBlocks); + states.set(states.size() - 1, "Persisting painted labels... Done"); + if (this.persistCanvas.supportsLabelBlockLookupUpdate()) { + states.add("Updating label-to-block lookup..."); + this.persistCanvas.updateLabelBlockLookup(blockDiffs); + states.set(states.size() - 1, "Updating label-to-block lookup... Done"); + } + states.add("Clearing canvases..."); + clearCanvases(); + states.set(states.size() - 1, "Clearing canvases... Done"); + this.source.invalidateAll(); + } + catch (UnableToPersistCanvas | UnableToUpdateLabelBlockLookup e) + { + caughtException = e; + throw new RuntimeException("Error while trying to persist.", e); + } + catch (final RuntimeException e) + { + caughtException = e; + throw e; + } + } + finally + { + synchronized (this) + { + this.isPersisting = false; + proxy.set(false); + if (caughtException == null) + states.add("Successfully finished committing canvas."); + else + states.add("Unable to commit canvas: " + caughtException.getMessage()); + } + } + }).start(); + } @Override From 536b9ca92c83a4a714d53a10ff34b06f5e6f1544 Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Wed, 5 Jun 2019 17:39:23 -0400 Subject: [PATCH 57/84] fix bug allowing to enter shape interpolation mode while 3d flood-filling --- .../saalfeldlab/paintera/control/ShapeInterpolationMode.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java index 38f26a0ad..3baa1903b 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java @@ -249,6 +249,7 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke e -> {e.consume(); enterMode(paintera, (ViewerPanelFX) e.getTarget());}, e -> e.getTarget() instanceof ViewerPanelFX && !isModeOn() && + source.getCurrentMask() == null && !source.isApplyingMaskProperty().get() && keyTracker.areOnlyTheseKeysDown(KeyCode.S) ) From 60468ad5911eed282915a7aa74ffa568d8f9869d Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Wed, 5 Jun 2019 18:33:08 -0400 Subject: [PATCH 58/84] attempt to fix race condition when explicitly setting label colors argbCache is accessed concurrently, but the accesses are not fully synchronized due to performance reasons (only write is protected). The clearCache() method changes the collection but was not protected, which sometimes caused the stream to ignore the explicitly specified color. This fix should reduce the chance of this bug, but is not guaranteed to completely solve it. --- .../paintera/stream/AbstractHighlightingARGBStream.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/stream/AbstractHighlightingARGBStream.java b/src/main/java/org/janelia/saalfeldlab/paintera/stream/AbstractHighlightingARGBStream.java index 7e9837bd7..fa6e65f4c 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/stream/AbstractHighlightingARGBStream.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/stream/AbstractHighlightingARGBStream.java @@ -264,8 +264,11 @@ public int getActiveFragmentAlpha() public void clearCache() { LOG.debug("Before clearing cache: {}", argbCache); - argbCache.clear(); - argbCache.putAll(this.explicitlySpecifiedColors); + synchronized (argbCache) + { + argbCache.clear(); + argbCache.putAll(this.explicitlySpecifiedColors); + } LOG.debug("After clearing cache: {}", argbCache); // TODO is this stateChanged bad here? // stateChanged() probably triggers a re-render, which calls clearCache, From 04b035bdfa40ebff2039cd3492f62371cdfd6453 Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Wed, 5 Jun 2019 18:35:36 -0400 Subject: [PATCH 59/84] fix occasional selection color change bug in shape interpolation mode Previously forgot to remove this line of code that would force the ARGB stream to set the explicit color and clear its cache on every object selection/deselection, which sometimes triggered the race condition described in the previous commit. --- .../saalfeldlab/paintera/control/ShapeInterpolationMode.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java index 3baa1903b..55347289d 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java @@ -724,7 +724,6 @@ private void selectObject(final PainteraBaseView paintera, final double x, final final boolean wasSelected = FOREGROUND_CHECK.test(maskValue); final int numSelectedObjects = selectedObjects.size(); - converter.setColor(mask.info.value.get(), MASK_COLOR); LOG.debug("Object was clicked: deactivateOthers={}, wasSelected={}, numSelectedObjects", deactivateOthers, wasSelected, numSelectedObjects); From b47cbdc99765e1fb24ac29d0b2d8e0ecf2aef9ed Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Thu, 6 Jun 2019 12:12:49 -0400 Subject: [PATCH 60/84] increase thickness of 2d flood fill in shape interpolation mode to 2.0 this way it fills 'holes' in a rotated view that actually belong to adjacent sections --- .../saalfeldlab/paintera/control/ShapeInterpolationMode.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java index 55347289d..4aa8cab10 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java @@ -148,7 +148,7 @@ private static final class SectionInfo } } - private static final double FILL_DEPTH = 1.0; + private static final double FILL_DEPTH = 2.0; private static final int MASK_SCALE_LEVEL = 0; From 6ecbe1c0d8566d13467df731bef0a98c01e3791d Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Thu, 6 Jun 2019 12:14:13 -0400 Subject: [PATCH 61/84] consider fragment-segment assignment when flood-filling in 2d/3d --- .../control/ShapeInterpolationMode.java | 8 +++- .../paintera/control/paint/FloodFill.java | 45 ++++++++++++------- .../paintera/control/paint/FloodFill2D.java | 41 +++++++++++------ .../paintera/state/LabelSourceState.java | 2 +- 4 files changed, 64 insertions(+), 32 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java index 4aa8cab10..8b25225e6 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java @@ -16,6 +16,7 @@ import org.janelia.saalfeldlab.paintera.control.actions.MenuAction; import org.janelia.saalfeldlab.paintera.control.actions.NavigationAction; import org.janelia.saalfeldlab.paintera.control.actions.PaintAction; +import org.janelia.saalfeldlab.paintera.control.assignment.FragmentSegmentAssignment; import org.janelia.saalfeldlab.paintera.control.paint.FloodFill2D; import org.janelia.saalfeldlab.paintera.control.selection.SelectedIds; import org.janelia.saalfeldlab.paintera.data.DataSource; @@ -165,6 +166,7 @@ private static final class SectionInfo private final SelectedIds selectedIds; private final IdService idService; private final HighlightingStreamConverter<?> converter; + private final FragmentSegmentAssignment assignment; private final AllowedActions allowedActions; private final AllowedActions allowedActionsWhenSelected; @@ -194,13 +196,15 @@ public ShapeInterpolationMode( final LabelSourceState<D, ?> sourceState, final SelectedIds selectedIds, final IdService idService, - final HighlightingStreamConverter<?> converter) + final HighlightingStreamConverter<?> converter, + final FragmentSegmentAssignment assignment) { this.source = source; this.sourceState = sourceState; this.selectedIds = selectedIds; this.idService = idService; this.converter = converter; + this.assignment = assignment; final Consumer<PainteraBaseView> cleanup = baseView -> exitMode(baseView, false); this.allowedActions = new AllowedActions( @@ -769,7 +773,7 @@ private void selectObject(final PainteraBaseView paintera, final double x, final */ private Pair<Long, Interval> runFloodFillToSelect(final double x, final double y) { - final Interval affectedInterval = FloodFill2D.fillMaskAt(x, y, activeViewer.get(), mask, source, ++currentFillValue, FILL_DEPTH); + final Interval affectedInterval = FloodFill2D.fillMaskAt(x, y, activeViewer.get(), mask, source, assignment, ++currentFillValue, FILL_DEPTH); return new ValuePair<>(currentFillValue, affectedInterval); } diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill.java index 41bbf7da2..a0715b40d 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill.java @@ -10,12 +10,14 @@ import java.util.function.Predicate; import java.util.function.Supplier; +import org.janelia.saalfeldlab.paintera.control.assignment.FragmentSegmentAssignment; import org.janelia.saalfeldlab.paintera.data.mask.Mask; import org.janelia.saalfeldlab.paintera.data.mask.MaskInfo; import org.janelia.saalfeldlab.paintera.data.mask.MaskedSource; import org.janelia.saalfeldlab.paintera.data.mask.exception.MaskInUse; import org.janelia.saalfeldlab.paintera.state.HasFloodFillState; import org.janelia.saalfeldlab.paintera.state.HasFloodFillState.FloodFillState; +import org.janelia.saalfeldlab.paintera.state.HasFragmentSegmentAssignments; import org.janelia.saalfeldlab.paintera.state.HasMaskForLabel; import org.janelia.saalfeldlab.paintera.state.SourceInfo; import org.janelia.saalfeldlab.paintera.state.SourceState; @@ -138,6 +140,17 @@ private void fillAt(final double x, final double y, final long fill) return; } + final FragmentSegmentAssignment assignment; + if (currentSourceState instanceof HasFragmentSegmentAssignments) + { + LOG.info("Selected source has a fragment-segment assignment that will be used for filling"); + assignment = ((HasFragmentSegmentAssignments) currentSourceState).assignment(); + } + else + { + assignment = null; + } + final MaskedSource<?, ?> source = (MaskedSource<?, ?>) currentSource; final Type<?> t = source.getDataType(); @@ -168,7 +181,8 @@ private void fillAt(final double x, final double y, final long fill) time, level, fill, - p + p, + assignment ); } catch (final MaskInUse e) { @@ -209,13 +223,15 @@ private <T extends IntegerType<T>> void fill( final int time, final int level, final long fill, - final Localizable seed) throws MaskInUse + final Localizable seed, + final FragmentSegmentAssignment assignment) throws MaskInUse { final RandomAccessibleInterval<T> data = source.getDataSource(time, level); final RandomAccess<T> dataAccess = data.randomAccess(); dataAccess.setPosition(seed); final T seedValue = dataAccess.get(); - final long seedLabel = seedValue instanceof LabelMultisetType ? getArgMaxLabel((LabelMultisetType) seedValue) : seedValue.getIntegerLong(); + final long seedPrimitiveValue = seedValue instanceof LabelMultisetType ? getArgMaxLabel((LabelMultisetType) seedValue) : seedValue.getIntegerLong(); + final long seedLabel = assignment != null ? assignment.getSegment(seedPrimitiveValue) : seedPrimitiveValue; if (!Label.regular(seedLabel)) { LOG.info("Trying to fill at irregular label: {} ({})", seedLabel, new Point(seed)); @@ -235,9 +251,9 @@ private <T extends IntegerType<T>> void fill( final Thread floodFillThread = new Thread(() -> { try { if (seedValue instanceof LabelMultisetType) { - fillMultisetType((RandomAccessibleInterval<LabelMultisetType>) data, accessTracker, seed, seedLabel); + fillMultisetType((RandomAccessibleInterval<LabelMultisetType>) data, accessTracker, seed, seedLabel, assignment); } else { - fillPrimitiveType(data, accessTracker, seed, seedLabel); + fillPrimitiveType(data, accessTracker, seed, seedLabel, assignment); } } catch (final Exception e) { // got an exception, ignore it if the operation has been canceled, or re-throw otherwise @@ -301,7 +317,8 @@ private static void fillMultisetType( final RandomAccessibleInterval<LabelMultisetType> input, final RandomAccessible<UnsignedLongType> output, final Localizable seed, - final long seedLabel) + final long seedLabel, + final FragmentSegmentAssignment assignment) { net.imglib2.algorithm.fill.FloodFill.fill( Views.extendValue(input, new LabelMultisetType()), @@ -309,7 +326,7 @@ private static void fillMultisetType( seed, new UnsignedLongType(1), new DiamondShape(1), - makePredicateMultisetType(seedLabel) + makePredicate(seedLabel, assignment) ); } @@ -317,7 +334,8 @@ private static <T extends IntegerType<T>> void fillPrimitiveType( final RandomAccessibleInterval<T> input, final RandomAccessible<UnsignedLongType> output, final Localizable seed, - final long seedLabel) + final long seedLabel, + final FragmentSegmentAssignment assignment) { final T extension = Util.getTypeFromInterval(input).createVariable(); extension.setInteger(Label.OUTSIDE); @@ -328,7 +346,7 @@ private static <T extends IntegerType<T>> void fillPrimitiveType( seed, new UnsignedLongType(1), new DiamondShape(1), - makePredicatePrimitiveType(seedLabel) + makePredicate(seedLabel, assignment) ); } @@ -344,14 +362,9 @@ private void resetFloodFillState(final Source<?> source) setFloodFillState(source, null); } - private static BiPredicate<LabelMultisetType, UnsignedLongType> makePredicateMultisetType(final long id) - { - return (l, u) -> !Thread.currentThread().isInterrupted() && u.getInteger() == 0 && l.contains(id); - } - - private static <T extends IntegerType<T>> BiPredicate<T, UnsignedLongType> makePredicatePrimitiveType(final long id) + private static <T extends IntegerType<T>> BiPredicate<T, UnsignedLongType> makePredicate(final long id, final FragmentSegmentAssignment assignment) { - return (t, u) -> !Thread.currentThread().isInterrupted() && u.getInteger() == 0 && t.getIntegerLong() == id; + return (t, u) -> !Thread.currentThread().isInterrupted() && u.getInteger() == 0 && (assignment != null ? assignment.getSegment(t.getIntegerLong()) : t.getIntegerLong()) == id; } public static class RunAll implements Runnable 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 63d0e0f9a..b2d8f6340 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 @@ -6,6 +6,18 @@ import java.util.function.Predicate; import java.util.function.Supplier; +import org.janelia.saalfeldlab.paintera.control.assignment.FragmentSegmentAssignment; +import org.janelia.saalfeldlab.paintera.data.mask.Mask; +import org.janelia.saalfeldlab.paintera.data.mask.MaskInfo; +import org.janelia.saalfeldlab.paintera.data.mask.MaskedSource; +import org.janelia.saalfeldlab.paintera.data.mask.exception.MaskInUse; +import org.janelia.saalfeldlab.paintera.state.HasFragmentSegmentAssignments; +import org.janelia.saalfeldlab.paintera.state.HasMaskForLabel; +import org.janelia.saalfeldlab.paintera.state.SourceInfo; +import org.janelia.saalfeldlab.paintera.state.SourceState; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import bdv.fx.viewer.ViewerPanelFX; import bdv.fx.viewer.ViewerState; import bdv.viewer.Source; @@ -32,16 +44,6 @@ import net.imglib2.view.MixedTransformView; import net.imglib2.view.Views; -import org.janelia.saalfeldlab.paintera.data.mask.Mask; -import org.janelia.saalfeldlab.paintera.data.mask.exception.MaskInUse; -import org.janelia.saalfeldlab.paintera.data.mask.MaskInfo; -import org.janelia.saalfeldlab.paintera.data.mask.MaskedSource; -import org.janelia.saalfeldlab.paintera.state.HasMaskForLabel; -import org.janelia.saalfeldlab.paintera.state.SourceInfo; -import org.janelia.saalfeldlab.paintera.state.SourceState; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - public class FloodFill2D { @@ -137,6 +139,17 @@ public <T extends IntegerType<T>> void fillAt(final double x, final double y, fi return; } + final FragmentSegmentAssignment assignment; + if (currentSourceState instanceof HasFragmentSegmentAssignments) + { + LOG.info("Selected source has a fragment-segment assignment that will be used for filling"); + assignment = ((HasFragmentSegmentAssignments) currentSourceState).assignment(); + } + else + { + assignment = null; + } + final MaskedSource<T, ?> source = (MaskedSource<T, ?>) currentSource; final T t = source.getDataType(); @@ -157,7 +170,7 @@ public <T extends IntegerType<T>> void fillAt(final double x, final double y, fi try { final Mask<UnsignedLongType> mask = source.generateMask(maskInfo, FOREGROUND_CHECK); - final Interval affectedInterval = fillMaskAt(x, y, this.viewer, mask, source, FILL_VALUE, this.fillDepth.get()); + final Interval affectedInterval = fillMaskAt(x, y, this.viewer, mask, source, assignment, FILL_VALUE, this.fillDepth.get()); requestRepaint.run(); source.applyMask(mask, affectedInterval, FOREGROUND_CHECK); } catch (final MaskInUse e) @@ -179,6 +192,7 @@ public <T extends IntegerType<T>> void fillAt(final double x, final double y, fi * @param viewer * @param mask * @param source + * @param assignment * @param fillValue * @param fillDepth * @return affected interval @@ -189,6 +203,7 @@ public static <T extends IntegerType<T>> Interval fillMaskAt( final ViewerPanelFX viewer, final Mask<UnsignedLongType> mask, final MaskedSource<T, ?> source, + final FragmentSegmentAssignment assignment, final long fillValue, final double fillDepth) { @@ -204,11 +219,11 @@ public static <T extends IntegerType<T>> Interval fillMaskAt( viewer.displayToSourceCoordinates(x, y, labelTransform, pos); for (int d = 0; d < access.numDimensions(); ++d) access.setPosition(Math.round(pos.getDoublePosition(d)), d); - final long seedLabel = access.get().getIntegerLong(); + final long seedLabel = assignment != null ? assignment.getSegment(access.get().getIntegerLong()) : access.get().getIntegerLong(); LOG.debug("Got seed label {}", seedLabel); final RandomAccessibleInterval<BoolType> relevantBackground = Converters.convert( background, - (src, tgt) -> tgt.set(src.getIntegerLong() == seedLabel), + (src, tgt) -> tgt.set((assignment != null ? assignment.getSegment(src.getIntegerLong()) : src.getIntegerLong()) == seedLabel), new BoolType() ); diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java index 7f0bf5123..432935abf 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java @@ -159,7 +159,7 @@ public LabelSourceState( this.idSelectorHandler = new LabelSourceStateIdSelectorHandler(dataSource, selectedIds, assignment, lockedSegments); this.mergeDetachHandler = new LabelSourceStateMergeDetachHandler(dataSource, selectedIds, assignment, idService); if (dataSource instanceof MaskedSource<?, ?>) - this.shapeInterpolationMode = new ShapeInterpolationMode<>((MaskedSource<D, ?>) dataSource, this, selectedIds, idService, converter); + this.shapeInterpolationMode = new ShapeInterpolationMode<>((MaskedSource<D, ?>) dataSource, this, selectedIds, idService, converter, assignment); else this.shapeInterpolationMode = null; this.displayStatus = createDisplayStatus(); From cd271e8be873e4f3e9737db02c13b1d50c62dbcf Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Thu, 6 Jun 2019 13:18:06 -0400 Subject: [PATCH 62/84] remove unneeded code in 3d flood filling --- .../paintera/control/paint/FloodFill.java | 21 +------------------ 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill.java index a0715b40d..48473086e 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill.java @@ -41,7 +41,6 @@ import net.imglib2.type.Type; import net.imglib2.type.label.Label; import net.imglib2.type.label.LabelMultisetType; -import net.imglib2.type.label.LabelMultisetType.Entry; import net.imglib2.type.numeric.IntegerType; import net.imglib2.type.numeric.integer.UnsignedLongType; import net.imglib2.util.AccessBoxRandomAccessible; @@ -230,8 +229,7 @@ private <T extends IntegerType<T>> void fill( final RandomAccess<T> dataAccess = data.randomAccess(); dataAccess.setPosition(seed); final T seedValue = dataAccess.get(); - final long seedPrimitiveValue = seedValue instanceof LabelMultisetType ? getArgMaxLabel((LabelMultisetType) seedValue) : seedValue.getIntegerLong(); - final long seedLabel = assignment != null ? assignment.getSegment(seedPrimitiveValue) : seedPrimitiveValue; + final long seedLabel = assignment != null ? assignment.getSegment(seedValue.getIntegerLong()) : seedValue.getIntegerLong(); if (!Label.regular(seedLabel)) { LOG.info("Trying to fill at irregular label: {} ({})", seedLabel, new Point(seed)); @@ -390,21 +388,4 @@ public void run() } } - - public static long getArgMaxLabel(final LabelMultisetType t) - { - long argmax = Label.INVALID; - long max = 0; - for (final Entry<net.imglib2.type.label.Label> e : t.entrySet()) - { - final int count = e.getCount(); - if (count > max) - { - max = count; - argmax = e.getElement().id(); - } - } - return argmax; - } - } From 0043e5c57f0d98a9a972d4e07dea37a7dbb871e7 Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Thu, 6 Jun 2019 14:58:47 -0400 Subject: [PATCH 63/84] use last selected id for interpolated shape, or new id if wasn't selected --- .../control/ShapeInterpolationMode.java | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java index 8b25225e6..4fb0e1e38 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java @@ -190,6 +190,7 @@ private static final class SectionInfo private final ObjectProperty<ActiveSection> activeSection = new SimpleObjectProperty<>(); private Thread workerThread; + private Pair<RealRandomAccessible<UnsignedLongType>, RealRandomAccessible<VolatileUnsignedLongType>> interpolatedMaskImgs; public ShapeInterpolationMode( final MaskedSource<D, ?> source, @@ -365,16 +366,15 @@ public void exitMode(final PainteraBaseView paintera, final boolean completed) e.printStackTrace(); } } - - selectedIds.activate(lastActiveIds); - selectedIds.activateAlso(lastSelectedId); - resetMask(); } converter.removeColor(newLabelId); newLabelId = Label.INVALID; + selectedIds.activate(lastActiveIds); + selectedIds.activateAlso(lastSelectedId); + paintera.allowedActionsProperty().set(lastAllowedActions); lastAllowedActions = null; @@ -387,6 +387,7 @@ public void exitMode(final PainteraBaseView paintera, final boolean completed) mask = null; workerThread = null; + interpolatedMaskImgs = null; lastSelectedId = Label.INVALID; lastActiveIds = null; @@ -493,8 +494,29 @@ private void applyMask(final PainteraBaseView paintera) sectionInfo2.get().sourceBoundingBox ); LOG.info("Applying interpolated mask using bounding box of size {}", Intervals.dimensionsAsLongArray(sectionsUnionSourceInterval)); + + if (Label.regular(lastSelectedId)) + { + final MaskInfo<UnsignedLongType> maskInfoWithLastSelectedLabelId = new MaskInfo<>( + source.getCurrentMask().info.t, + source.getCurrentMask().info.level, + new UnsignedLongType(lastSelectedId) + ); + resetMask(); + try { + source.setMask(maskInfoWithLastSelectedLabelId, interpolatedMaskImgs.getA(), interpolatedMaskImgs.getB(), FOREGROUND_CHECK); + } catch (final MaskInUse e) { + e.printStackTrace(); + } + } + else + { + lastSelectedId = newLabelId; + } + source.isApplyingMaskProperty().addListener(doneApplyingMaskListener); source.applyMask(source.getCurrentMask(), sectionsUnionSourceInterval, FOREGROUND_CHECK); + exitMode(paintera, true); } @@ -611,6 +633,7 @@ private void interpolateBetweenSections(final PainteraBaseView paintera) volatileInterpolatedShapeMask, FOREGROUND_CHECK ); + interpolatedMaskImgs = new ValuePair<>(interpolatedShapeMask, volatileInterpolatedShapeMask); } for (final ViewerPanelFX viewer : getViewerPanels(paintera)) From 291e9f5633b01dd0116f627b06fae9f4064cf412 Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Fri, 7 Jun 2019 11:43:06 -0400 Subject: [PATCH 64/84] improve allowed actions class and add custom predicate for actions --- .../saalfeldlab/paintera/Paintera.java | 6 +- .../paintera/PainteraBaseView.java | 86 +++++----- .../paintera/PainteraDefaultHandlers.java | 26 +-- .../paintera/SaveOnExitDialog.java | 4 +- .../paintera/control/Navigation.java | 83 ++++----- .../control/ShapeInterpolationMode.java | 37 ++-- .../paintera/control/actions/ActionType.java | 8 + .../control/actions/AllowedActions.java | 159 +++++++++++++----- .../paintera/control/actions/LabelAction.java | 28 --- .../control/actions/LabelActionType.java | 28 +++ .../paintera/control/actions/MenuAction.java | 30 ---- .../control/actions/MenuActionType.java | 30 ++++ .../control/actions/NavigationAction.java | 26 --- .../control/actions/NavigationActionType.java | 26 +++ .../paintera/control/actions/PaintAction.java | 28 --- .../control/actions/PaintActionType.java | 28 +++ .../LabelSourceStateIdSelectorHandler.java | 31 ++-- .../LabelSourceStateMergeDetachHandler.java | 58 ++++--- .../state/LabelSourceStatePaintHandler.java | 67 ++++---- 19 files changed, 437 insertions(+), 352 deletions(-) create mode 100644 src/main/java/org/janelia/saalfeldlab/paintera/control/actions/ActionType.java delete mode 100644 src/main/java/org/janelia/saalfeldlab/paintera/control/actions/LabelAction.java create mode 100644 src/main/java/org/janelia/saalfeldlab/paintera/control/actions/LabelActionType.java delete mode 100644 src/main/java/org/janelia/saalfeldlab/paintera/control/actions/MenuAction.java create mode 100644 src/main/java/org/janelia/saalfeldlab/paintera/control/actions/MenuActionType.java delete mode 100644 src/main/java/org/janelia/saalfeldlab/paintera/control/actions/NavigationAction.java create mode 100644 src/main/java/org/janelia/saalfeldlab/paintera/control/actions/NavigationActionType.java delete mode 100644 src/main/java/org/janelia/saalfeldlab/paintera/control/actions/PaintAction.java create mode 100644 src/main/java/org/janelia/saalfeldlab/paintera/control/actions/PaintActionType.java diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/Paintera.java b/src/main/java/org/janelia/saalfeldlab/paintera/Paintera.java index c65f1dbbf..61c4a03aa 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/Paintera.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/Paintera.java @@ -18,7 +18,7 @@ import org.janelia.saalfeldlab.paintera.config.OrthoSliceConfig; import org.janelia.saalfeldlab.paintera.config.ScreenScalesConfig; import org.janelia.saalfeldlab.paintera.control.CommitChanges; -import org.janelia.saalfeldlab.paintera.control.actions.MenuAction; +import org.janelia.saalfeldlab.paintera.control.actions.MenuActionType; import org.janelia.saalfeldlab.paintera.control.assignment.UnableToPersist; import org.janelia.saalfeldlab.paintera.data.mask.exception.CannotPersist; import org.janelia.saalfeldlab.paintera.serialization.GsonHelpers; @@ -273,7 +273,7 @@ public void startImpl(final Stage stage) throws Exception e -> { e.consume(); - if (!baseView.allowedActionsProperty().get().isAllowed(MenuAction.SaveProject)) + if (!baseView.allowedActionsProperty().get().isAllowed(MenuActionType.SaveProject)) { final Alert cannotSaveProjectDialog = PainteraAlerts.alert(Alert.AlertType.WARNING); cannotSaveProjectDialog.setHeaderText("Cannot currently save the project."); @@ -317,7 +317,7 @@ public void startImpl(final Stage stage) throws Exception LOG.error("Unable to persist fragment-segment-assignment: {}", e1.getMessage()); } }, - e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.CommitCanvas) && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL, KeyCode.C) + e -> baseView.allowedActionsProperty().get().isAllowed(MenuActionType.CommitCanvas) && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL, KeyCode.C) ).installInto(paneWithStatus.getPane()); keyTracker.installInto(scene); diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/PainteraBaseView.java b/src/main/java/org/janelia/saalfeldlab/paintera/PainteraBaseView.java index 9edf5c31d..53e622cdf 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/PainteraBaseView.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/PainteraBaseView.java @@ -1,26 +1,12 @@ package org.janelia.saalfeldlab.paintera; -import bdv.viewer.Interpolation; -import bdv.viewer.SourceAndConverter; -import bdv.viewer.ViewerOptions; -import javafx.beans.property.ObjectProperty; -import javafx.beans.property.SimpleObjectProperty; -import javafx.collections.ListChangeListener; -import javafx.collections.ObservableList; -import javafx.event.Event; -import javafx.scene.layout.Pane; -import net.imglib2.RandomAccessibleInterval; -import net.imglib2.Volatile; -import net.imglib2.cache.LoaderCache; -import net.imglib2.converter.ARGBColorConverter; -import net.imglib2.converter.ARGBCompositeColorConverter; -import net.imglib2.type.NativeType; -import net.imglib2.type.Type; -import net.imglib2.type.numeric.IntegerType; -import net.imglib2.type.numeric.RealType; -import net.imglib2.type.volatiles.AbstractVolatileNativeRealType; -import net.imglib2.type.volatiles.AbstractVolatileRealType; -import net.imglib2.view.composite.RealComposite; +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.nio.file.Files; +import java.util.Optional; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + import org.janelia.saalfeldlab.fx.event.KeyTracker; import org.janelia.saalfeldlab.fx.event.MouseTracker; import org.janelia.saalfeldlab.fx.ortho.GridConstraintsManager; @@ -39,16 +25,12 @@ import org.janelia.saalfeldlab.paintera.config.OrthoSliceConfigBase; import org.janelia.saalfeldlab.paintera.config.Viewer3DConfig; import org.janelia.saalfeldlab.paintera.control.actions.AllowedActions; +import org.janelia.saalfeldlab.paintera.control.actions.AllowedActions.AllowedActionsBuilder; import org.janelia.saalfeldlab.paintera.data.axisorder.AxisOrder; import org.janelia.saalfeldlab.paintera.data.axisorder.AxisOrderNotSupported; import org.janelia.saalfeldlab.paintera.data.mask.MaskedSource; import org.janelia.saalfeldlab.paintera.state.ChannelSourceState; import org.janelia.saalfeldlab.paintera.state.GlobalTransformManager; -import org.janelia.saalfeldlab.paintera.state.HasFragmentSegmentAssignments; -import org.janelia.saalfeldlab.paintera.state.HasHighlightingStreamConverter; -import org.janelia.saalfeldlab.paintera.state.HasLockedSegments; -import org.janelia.saalfeldlab.paintera.state.HasMeshes; -import org.janelia.saalfeldlab.paintera.state.HasSelectedIds; import org.janelia.saalfeldlab.paintera.state.LabelSourceState; import org.janelia.saalfeldlab.paintera.state.RawSourceState; import org.janelia.saalfeldlab.paintera.state.SourceInfo; @@ -59,12 +41,26 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.IOException; -import java.lang.invoke.MethodHandles; -import java.nio.file.Files; -import java.util.Optional; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; +import bdv.viewer.Interpolation; +import bdv.viewer.SourceAndConverter; +import bdv.viewer.ViewerOptions; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; +import javafx.scene.layout.Pane; +import net.imglib2.RandomAccessibleInterval; +import net.imglib2.Volatile; +import net.imglib2.cache.LoaderCache; +import net.imglib2.converter.ARGBColorConverter; +import net.imglib2.converter.ARGBCompositeColorConverter; +import net.imglib2.type.NativeType; +import net.imglib2.type.Type; +import net.imglib2.type.numeric.IntegerType; +import net.imglib2.type.numeric.RealType; +import net.imglib2.type.volatiles.AbstractVolatileNativeRealType; +import net.imglib2.type.volatiles.AbstractVolatileRealType; +import net.imglib2.view.composite.RealComposite; /** * Contains all the things necessary to build a Paintera UI, most importantly: @@ -87,6 +83,8 @@ public class PainteraBaseView // set this absurdly high private static final int MAX_NUM_MIPMAP_LEVELS = 100; + private static final AllowedActions DEFAULT_ALLOWED_ACTIONS = AllowedActionsBuilder.all(); + private final SourceInfo sourceInfo = new SourceInfo(); private final GlobalTransformManager manager = new GlobalTransformManager(); @@ -162,7 +160,7 @@ public PainteraBaseView( s -> Optional.ofNullable(sourceInfo.getState(s)).map(SourceState::interpolationProperty).map(ObjectProperty::get).orElse(Interpolation.NLINEAR), s -> Optional.ofNullable(sourceInfo.getState(s)).map(SourceState::getAxisOrder).orElse(null) ); - this.allowedActionsProperty = new SimpleObjectProperty<>(AllowedActions.all()); + this.allowedActionsProperty = new SimpleObjectProperty<>(DEFAULT_ALLOWED_ACTIONS); this.vsacUpdate = change -> views.setAllSources(visibleSourcesAndConverters); visibleSourcesAndConverters.addListener(vsacUpdate); LOG.debug("Meshes group={}", viewer3D.meshesGroup()); @@ -222,6 +220,14 @@ public ObjectProperty<AllowedActions> allowedActionsProperty() return this.allowedActionsProperty; } + /** + * Set allowed actions in the normal application mode. + */ + public void setDefaultAllowedActions() + { + this.allowedActionsProperty.set(DEFAULT_ALLOWED_ACTIONS); + } + /** * * Add a source and state to the viewer @@ -285,12 +291,12 @@ public <D, T> void addGenericState(final SourceState<D, T> state) */ public <D extends RealType<D> & NativeType<D>, T extends AbstractVolatileNativeRealType<D, T>> RawSourceState<D, T> addSingleScaleRawSource( final RandomAccessibleInterval<D> data, - double[] resolution, - double[] offset, - double min, - String name, - double max) throws AxisOrderNotSupported { - RawSourceState<D, T> state = RawSourceState.simpleSourceFromSingleRAI(data, resolution, offset, min, max, + final double[] resolution, + final double[] offset, + final double min, + final String name, + final double max) throws AxisOrderNotSupported { + final RawSourceState<D, T> state = RawSourceState.simpleSourceFromSingleRAI(data, resolution, offset, min, max, name); InvokeOnJavaFXApplicationThread.invoke(() -> addRawSource(state)); return state; @@ -364,7 +370,7 @@ public <T extends RealType<T>, U extends RealType<U>> void addRawSource(final Ra final AxisOrder axisOrder, final long maxId, final String name) throws AxisOrderNotSupported { - LabelSourceState<D, T> state = LabelSourceState.simpleSourceFromSingleRAI( + final LabelSourceState<D, T> state = LabelSourceState.simpleSourceFromSingleRAI( data, resolution, offset, diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/PainteraDefaultHandlers.java b/src/main/java/org/janelia/saalfeldlab/paintera/PainteraDefaultHandlers.java index 50b12d360..2da57a27a 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/PainteraDefaultHandlers.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/PainteraDefaultHandlers.java @@ -30,7 +30,7 @@ import org.janelia.saalfeldlab.paintera.control.OrthogonalViewsValueDisplayListener; import org.janelia.saalfeldlab.paintera.control.RunWhenFirstElementIsAdded; import org.janelia.saalfeldlab.paintera.control.ShowOnlySelectedInStreamToggle; -import org.janelia.saalfeldlab.paintera.control.actions.MenuAction; +import org.janelia.saalfeldlab.paintera.control.actions.MenuActionType; import org.janelia.saalfeldlab.paintera.control.navigation.AffineTransformWithListeners; import org.janelia.saalfeldlab.paintera.control.navigation.DisplayTransformUpdateOnResize; import org.janelia.saalfeldlab.paintera.state.SourceInfo; @@ -241,7 +241,7 @@ public PainteraDefaultHandlers( KeyEvent.KEY_PRESSED, toggleSideBar)); - baseView.allowedActionsProperty().addListener((obs, oldv, newv) -> paneWithStatus.getSideBar().setDisable(!newv.isAllowed(MenuAction.SidePanel))); + baseView.allowedActionsProperty().addListener((obs, oldv, newv) -> paneWithStatus.getSideBar().setDisable(!newv.isAllowed(MenuActionType.SidePanel))); EventFX.KEY_PRESSED( "toggle interpolation", @@ -250,11 +250,11 @@ public PainteraDefaultHandlers( EventFX.KEY_PRESSED( "cycle current source", e -> sourceInfo.incrementCurrentSourceIndex(), - e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.ChangeActiveSource) && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL, KeyCode.TAB)).installInto(borderPane); + e -> baseView.allowedActionsProperty().get().isAllowed(MenuActionType.ChangeActiveSource) && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL, KeyCode.TAB)).installInto(borderPane); EventFX.KEY_PRESSED( "backwards cycle current source", e -> sourceInfo.decrementCurrentSourceIndex(), - e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.ChangeActiveSource) && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL, KeyCode.SHIFT, KeyCode.TAB)).installInto(borderPane); + e -> baseView.allowedActionsProperty().get().isAllowed(MenuActionType.ChangeActiveSource) && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL, KeyCode.SHIFT, KeyCode.TAB)).installInto(borderPane); this.resizer = new GridResizer(gridConstraintsManager, 5, baseView.pane(), keyTracker); this.resizer.installInto(baseView.pane()); @@ -286,30 +286,30 @@ public PainteraDefaultHandlers( EventFX.KEY_PRESSED( "toggle maximize viewer", e -> toggleMaximizeTopLeft.toggleMaximizeViewer(), - e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.ToggleMaximizeViewer) && keyTracker.areOnlyTheseKeysDown(KeyCode.M)).installInto(orthogonalViews.topLeft().viewer()); + e -> baseView.allowedActionsProperty().get().isAllowed(MenuActionType.ToggleMaximizeViewer) && keyTracker.areOnlyTheseKeysDown(KeyCode.M)).installInto(orthogonalViews.topLeft().viewer()); EventFX.KEY_PRESSED( "toggle maximize viewer", e -> toggleMaximizeTopRight.toggleMaximizeViewer(), - e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.ToggleMaximizeViewer) && keyTracker.areOnlyTheseKeysDown(KeyCode.M)).installInto(orthogonalViews.topRight().viewer()); + e -> baseView.allowedActionsProperty().get().isAllowed(MenuActionType.ToggleMaximizeViewer) && keyTracker.areOnlyTheseKeysDown(KeyCode.M)).installInto(orthogonalViews.topRight().viewer()); EventFX.KEY_PRESSED( "toggle maximize viewer", e -> toggleMaximizeBottomLeft.toggleMaximizeViewer(), - e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.ToggleMaximizeViewer) && keyTracker.areOnlyTheseKeysDown(KeyCode.M)).installInto(orthogonalViews.bottomLeft().viewer()); + e -> baseView.allowedActionsProperty().get().isAllowed(MenuActionType.ToggleMaximizeViewer) && keyTracker.areOnlyTheseKeysDown(KeyCode.M)).installInto(orthogonalViews.bottomLeft().viewer()); EventFX.KEY_PRESSED( "toggle maximize viewer and orthoslice", e -> toggleMaximizeTopLeft.toggleMaximizeViewerAndOrthoslice(), - e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.ToggleMaximizeViewer) && keyTracker.areOnlyTheseKeysDown(KeyCode.M, KeyCode.SHIFT)).installInto(orthogonalViews.topLeft().viewer()); + e -> baseView.allowedActionsProperty().get().isAllowed(MenuActionType.ToggleMaximizeViewer) && keyTracker.areOnlyTheseKeysDown(KeyCode.M, KeyCode.SHIFT)).installInto(orthogonalViews.topLeft().viewer()); EventFX.KEY_PRESSED( "toggle maximize viewer and orthoslice", e -> toggleMaximizeTopRight.toggleMaximizeViewerAndOrthoslice(), - e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.ToggleMaximizeViewer) && keyTracker.areOnlyTheseKeysDown(KeyCode.M, KeyCode.SHIFT)).installInto(orthogonalViews.topRight().viewer()); + e -> baseView.allowedActionsProperty().get().isAllowed(MenuActionType.ToggleMaximizeViewer) && keyTracker.areOnlyTheseKeysDown(KeyCode.M, KeyCode.SHIFT)).installInto(orthogonalViews.topRight().viewer()); EventFX.KEY_PRESSED( "toggle maximize viewer and orthoslice", e -> toggleMaximizeBottomLeft.toggleMaximizeViewerAndOrthoslice(), - e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.ToggleMaximizeViewer) && keyTracker.areOnlyTheseKeysDown(KeyCode.M, KeyCode.SHIFT)).installInto(orthogonalViews.bottomLeft().viewer()); + e -> baseView.allowedActionsProperty().get().isAllowed(MenuActionType.ToggleMaximizeViewer) && keyTracker.areOnlyTheseKeysDown(KeyCode.M, KeyCode.SHIFT)).installInto(orthogonalViews.bottomLeft().viewer()); final CurrentSourceVisibilityToggle csv = new CurrentSourceVisibilityToggle(sourceInfo.currentState()); @@ -336,7 +336,7 @@ public PainteraDefaultHandlers( MouseEvent.MOUSE_CLICKED, e -> { LOG.debug("Handling event {}", e); - if (baseView.allowedActionsProperty().get().isAllowed(MenuAction.OrthoslicesContextMenu) && + if (baseView.allowedActionsProperty().get().isAllowed(MenuActionType.OrthoslicesContextMenu) && MouseButton.SECONDARY.equals(e.getButton()) && e.getClickCount() == 1 && !mouseTracker.isDragging()) @@ -355,7 +355,7 @@ public PainteraDefaultHandlers( projectDirectory, Exceptions.handler("Paintera", "Unable to create new Dataset"), baseView.sourceInfo().currentSourceProperty().get()), - e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.CreateNewLabelSource) && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL, KeyCode.SHIFT, KeyCode.N)).installInto(paneWithStatus.getPane()); + e -> baseView.allowedActionsProperty().get().isAllowed(MenuActionType.CreateNewLabelSource) && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL, KeyCode.SHIFT, KeyCode.N)).installInto(paneWithStatus.getPane()); } private final Map<ViewerPanelFX, ViewerAndTransforms> viewerToTransforms = new HashMap<>(); @@ -488,7 +488,7 @@ public static EventHandler<KeyEvent> addOpenDatasetContextMenuHandler( final EventHandler<KeyEvent> handler = OpenDialogMenu.keyPressedHandler( target, exception -> Exceptions.exceptionAlert(Paintera.NAME, "Unable to show open dataset menu", exception), - e -> baseView.allowedActionsProperty().get().isAllowed(MenuAction.AddSource) && keyTracker.areOnlyTheseKeysDown(triggers), + e -> baseView.allowedActionsProperty().get().isAllowed(MenuActionType.AddSource) && keyTracker.areOnlyTheseKeysDown(triggers), "Open dataset", baseView, projectDirectory, diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/SaveOnExitDialog.java b/src/main/java/org/janelia/saalfeldlab/paintera/SaveOnExitDialog.java index 62e0d831b..64c8b346d 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/SaveOnExitDialog.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/SaveOnExitDialog.java @@ -63,8 +63,8 @@ public void handle(final WindowEvent event) return; } - // run cleanup if the application is not currently in the normal mode - baseView.allowedActionsProperty().get().cleanup(baseView); + // ensure that the application is in the normal mode when the project is saved + baseView.setDefaultAllowedActions(); if (saveButton.equals(response)) { diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/Navigation.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/Navigation.java index 43bc865a5..1e49961cb 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/Navigation.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/Navigation.java @@ -12,6 +12,22 @@ import java.util.function.Predicate; import java.util.function.Supplier; +import org.janelia.saalfeldlab.fx.event.EventFX; +import org.janelia.saalfeldlab.fx.event.InstallAndRemove; +import org.janelia.saalfeldlab.fx.event.KeyTracker; +import org.janelia.saalfeldlab.fx.event.MouseDragFX; +import org.janelia.saalfeldlab.paintera.control.actions.AllowedActions; +import org.janelia.saalfeldlab.paintera.control.actions.NavigationActionType; +import org.janelia.saalfeldlab.paintera.control.navigation.AffineTransformWithListeners; +import org.janelia.saalfeldlab.paintera.control.navigation.ButtonRotationSpeedConfig; +import org.janelia.saalfeldlab.paintera.control.navigation.KeyRotate; +import org.janelia.saalfeldlab.paintera.control.navigation.RemoveRotation; +import org.janelia.saalfeldlab.paintera.control.navigation.Rotate; +import org.janelia.saalfeldlab.paintera.control.navigation.TranslateAlongNormal; +import org.janelia.saalfeldlab.paintera.control.navigation.TranslateWithinPlane; +import org.janelia.saalfeldlab.paintera.control.navigation.Zoom; +import org.janelia.saalfeldlab.paintera.state.GlobalTransformManager; + import bdv.fx.viewer.ViewerPanelFX; import javafx.beans.binding.Bindings; import javafx.beans.binding.DoubleBinding; @@ -29,21 +45,6 @@ import javafx.scene.input.MouseButton; import javafx.scene.input.MouseEvent; import net.imglib2.realtransform.AffineTransform3D; -import org.janelia.saalfeldlab.fx.event.EventFX; -import org.janelia.saalfeldlab.fx.event.InstallAndRemove; -import org.janelia.saalfeldlab.fx.event.KeyTracker; -import org.janelia.saalfeldlab.fx.event.MouseDragFX; -import org.janelia.saalfeldlab.paintera.control.actions.AllowedActions; -import org.janelia.saalfeldlab.paintera.control.actions.NavigationAction; -import org.janelia.saalfeldlab.paintera.control.navigation.AffineTransformWithListeners; -import org.janelia.saalfeldlab.paintera.control.navigation.ButtonRotationSpeedConfig; -import org.janelia.saalfeldlab.paintera.control.navigation.KeyRotate; -import org.janelia.saalfeldlab.paintera.control.navigation.RemoveRotation; -import org.janelia.saalfeldlab.paintera.control.navigation.Rotate; -import org.janelia.saalfeldlab.paintera.control.navigation.TranslateAlongNormal; -import org.janelia.saalfeldlab.paintera.control.navigation.TranslateWithinPlane; -import org.janelia.saalfeldlab.paintera.control.navigation.Zoom; -import org.janelia.saalfeldlab.paintera.state.GlobalTransformManager; public class Navigation implements ToOnEnterOnExit { @@ -166,55 +167,55 @@ public Consumer<ViewerPanelFX> getOnEnter() iars.add(EventFX.SCROLL( "translate along normal", e -> scrollDefault.scroll(-ControlUtils.getBiggestScroll(e)), - event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Scroll) && keyTracker.noKeysActive() + event -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Scroll) && keyTracker.noKeysActive() )); iars.add(EventFX.SCROLL( "translate along normal fast", e -> scrollFast.scroll(-ControlUtils.getBiggestScroll(e)), - event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Scroll) && keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT) + event -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Scroll) && keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT) )); iars.add(EventFX.SCROLL( "translate along normal slow", e -> scrollSlow.scroll(-ControlUtils.getBiggestScroll(e)), - event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Scroll) && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL) + event -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Scroll) && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL) )); iars.add(EventFX.KEY_PRESSED( "button translate along normal bck", e -> scrollDefault.scroll(+1), - event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Scroll) && keyTracker.areOnlyTheseKeysDown(KeyCode.COMMA) + event -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Scroll) && keyTracker.areOnlyTheseKeysDown(KeyCode.COMMA) )); iars.add(EventFX.KEY_PRESSED( "button translate along normal fwd", e -> scrollDefault.scroll(-1), - event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Scroll) && keyTracker.areOnlyTheseKeysDown(KeyCode.PERIOD) + event -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Scroll) && keyTracker.areOnlyTheseKeysDown(KeyCode.PERIOD) )); iars.add(EventFX.KEY_PRESSED( "button translate along normal fast bck", e -> scrollFast.scroll(+1), - event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Scroll) && keyTracker.areOnlyTheseKeysDown(KeyCode.COMMA, KeyCode.SHIFT) + event -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Scroll) && keyTracker.areOnlyTheseKeysDown(KeyCode.COMMA, KeyCode.SHIFT) )); iars.add(EventFX.KEY_PRESSED( "button translate along normal fast fwd", e -> scrollFast.scroll(-1), - event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Scroll) && keyTracker.areOnlyTheseKeysDown(KeyCode.PERIOD, KeyCode.SHIFT) + event -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Scroll) && keyTracker.areOnlyTheseKeysDown(KeyCode.PERIOD, KeyCode.SHIFT) )); iars.add(EventFX.KEY_PRESSED( "button translate along normal slow bck", e -> scrollSlow.scroll(+1), - event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Scroll) && keyTracker.areOnlyTheseKeysDown(KeyCode.COMMA, KeyCode.CONTROL) + event -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Scroll) && keyTracker.areOnlyTheseKeysDown(KeyCode.COMMA, KeyCode.CONTROL) )); iars.add(EventFX.KEY_PRESSED( "button translate along normal slow fwd", e -> scrollSlow.scroll(-1), - event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Scroll) && keyTracker.areOnlyTheseKeysDown(KeyCode.PERIOD, KeyCode.CONTROL) + event -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Scroll) && keyTracker.areOnlyTheseKeysDown(KeyCode.PERIOD, KeyCode.CONTROL) )); iars.add(MouseDragFX.createDrag( "translate xy", - e -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Drag) && e.isSecondaryButtonDown() && keyTracker.noKeysActive(), + e -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Drag) && e.isSecondaryButtonDown() && keyTracker.noKeysActive(), true, manager, e -> translateXY.init(), @@ -226,7 +227,7 @@ public Consumer<ViewerPanelFX> getOnEnter() iars.add(EventFX.SCROLL( "zoom", event -> zoom.zoomCenteredAt(-ControlUtils.getBiggestScroll(event), event.getX(), event.getY()), - event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Zoom) && + event -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Zoom) && (keyTracker.areOnlyTheseKeysDown(KeyCode.META) || keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL, KeyCode.SHIFT)) )); @@ -238,7 +239,7 @@ public Consumer<ViewerPanelFX> getOnEnter() mouseXIfInsideElseCenterX.get(), mouseYIfInsideElseCenterY.get() ), - event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Zoom) && + event -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Zoom) && (keyTracker.areOnlyTheseKeysDown(KeyCode.MINUS) || keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT, KeyCode.MINUS) || keyTracker.areOnlyTheseKeysDown(KeyCode.DOWN)) @@ -251,7 +252,7 @@ public Consumer<ViewerPanelFX> getOnEnter() mouseXIfInsideElseCenterX.get(), mouseYIfInsideElseCenterY.get() ), - event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Zoom) && + event -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Zoom) && (keyTracker.areOnlyTheseKeysDown(KeyCode.EQUALS) || keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT, KeyCode.EQUALS) || keyTracker.areOnlyTheseKeysDown(KeyCode.UP)) @@ -266,7 +267,7 @@ public Consumer<ViewerPanelFX> getOnEnter() globalToViewerTransform, manager::setTransform, manager, - event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Rotate) && keyTracker.noKeysActive() && event.getButton().equals(MouseButton.PRIMARY) + event -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Rotate) && keyTracker.noKeysActive() && event.getButton().equals(MouseButton.PRIMARY) )); iars.add(rotationHandler( @@ -278,7 +279,7 @@ public Consumer<ViewerPanelFX> getOnEnter() globalToViewerTransform, manager::setTransform, manager, - event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT) && event.getButton().equals + event -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT) && event.getButton().equals (MouseButton.PRIMARY) )); @@ -291,7 +292,7 @@ public Consumer<ViewerPanelFX> getOnEnter() globalToViewerTransform, manager::setTransform, manager, - event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL) && event.getButton().equals( + event -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL) && event.getButton().equals( MouseButton.PRIMARY) )); @@ -299,17 +300,17 @@ public Consumer<ViewerPanelFX> getOnEnter() iars.add(EventFX.KEY_PRESSED( "set key rotation axis x", e -> keyRotationAxis.set(KeyRotate.Axis.X), - e -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.X) + e -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.X) )); iars.add(EventFX.KEY_PRESSED( "set key rotation axis y", e -> keyRotationAxis.set(KeyRotate.Axis.Y), - e -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.Y) + e -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.Y) )); iars.add(EventFX.KEY_PRESSED( "set key rotation axis z", e -> keyRotationAxis.set(KeyRotate.Axis.Z), - e -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.Z) + e -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.Z) )); iars.add(keyRotationHandler( @@ -324,7 +325,7 @@ public Consumer<ViewerPanelFX> getOnEnter() globalTransform, manager::setTransform, manager, - event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.LEFT) + event -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.LEFT) )); iars.add(keyRotationHandler( @@ -339,7 +340,7 @@ public Consumer<ViewerPanelFX> getOnEnter() globalTransform, manager::setTransform, manager, - event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL, KeyCode.LEFT) + event -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL, KeyCode.LEFT) )); iars.add(keyRotationHandler( @@ -354,7 +355,7 @@ public Consumer<ViewerPanelFX> getOnEnter() globalTransform, manager::setTransform, manager, - event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT, KeyCode.LEFT) + event -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT, KeyCode.LEFT) )); iars.add(keyRotationHandler( @@ -369,7 +370,7 @@ public Consumer<ViewerPanelFX> getOnEnter() globalTransform, manager::setTransform, manager, - event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.RIGHT) + event -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.RIGHT) )); iars.add(keyRotationHandler( @@ -384,7 +385,7 @@ public Consumer<ViewerPanelFX> getOnEnter() globalTransform, manager::setTransform, manager, - event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL, KeyCode.RIGHT) + event -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL, KeyCode.RIGHT) )); iars.add(keyRotationHandler( @@ -399,7 +400,7 @@ public Consumer<ViewerPanelFX> getOnEnter() globalTransform, manager::setTransform, manager, - event -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT, KeyCode.RIGHT) + event -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT, KeyCode.RIGHT) )); final RemoveRotation removeRotation = new RemoveRotation( @@ -414,7 +415,7 @@ public Consumer<ViewerPanelFX> getOnEnter() mouseXIfInsideElseCenterX.get(), mouseYIfInsideElseCenterY.get() ), - e -> this.allowedActionsProperty.get().isAllowed(NavigationAction.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT, KeyCode.Z) + e -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Rotate) && keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT, KeyCode.Z) )); this.mouseAndKeyHandlers.put(t, iars); diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java index 4fb0e1e38..dea7c2368 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java @@ -2,7 +2,6 @@ import java.lang.invoke.MethodHandles; import java.util.Arrays; -import java.util.function.Consumer; import java.util.function.Predicate; import org.janelia.saalfeldlab.fx.event.DelegateEventHandlers; @@ -12,10 +11,9 @@ import org.janelia.saalfeldlab.fx.util.InvokeOnJavaFXApplicationThread; import org.janelia.saalfeldlab.paintera.PainteraBaseView; import org.janelia.saalfeldlab.paintera.control.actions.AllowedActions; -import org.janelia.saalfeldlab.paintera.control.actions.LabelAction; -import org.janelia.saalfeldlab.paintera.control.actions.MenuAction; -import org.janelia.saalfeldlab.paintera.control.actions.NavigationAction; -import org.janelia.saalfeldlab.paintera.control.actions.PaintAction; +import org.janelia.saalfeldlab.paintera.control.actions.AllowedActions.AllowedActionsBuilder; +import org.janelia.saalfeldlab.paintera.control.actions.MenuActionType; +import org.janelia.saalfeldlab.paintera.control.actions.NavigationActionType; import org.janelia.saalfeldlab.paintera.control.assignment.FragmentSegmentAssignment; import org.janelia.saalfeldlab.paintera.control.paint.FloodFill2D; import org.janelia.saalfeldlab.paintera.control.selection.SelectedIds; @@ -36,6 +34,7 @@ import gnu.trove.iterator.TLongObjectIterator; import gnu.trove.map.TLongObjectMap; import gnu.trove.map.hash.TLongObjectHashMap; +import javafx.beans.InvalidationListener; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.ChangeListener; @@ -190,6 +189,8 @@ private static final class SectionInfo private final ObjectProperty<ActiveSection> activeSection = new SimpleObjectProperty<>(); private Thread workerThread; + private InvalidationListener modeSwitchListener; + private Pair<RealRandomAccessible<UnsignedLongType>, RealRandomAccessible<VolatileUnsignedLongType>> interpolatedMaskImgs; public ShapeInterpolationMode( @@ -207,21 +208,10 @@ public ShapeInterpolationMode( this.converter = converter; this.assignment = assignment; - final Consumer<PainteraBaseView> cleanup = baseView -> exitMode(baseView, false); - this.allowedActions = new AllowedActions( - NavigationAction.of(NavigationAction.Drag, NavigationAction.Zoom, NavigationAction.Scroll), - LabelAction.none(), - PaintAction.none(), - MenuAction.of(MenuAction.ToggleMaximizeViewer), - cleanup - ); - this.allowedActionsWhenSelected = new AllowedActions( - NavigationAction.of(NavigationAction.Drag, NavigationAction.Zoom), - LabelAction.none(), - PaintAction.none(), - MenuAction.of(MenuAction.ToggleMaximizeViewer), - cleanup - ); + final AllowedActionsBuilder allowedActionsBuilder = new AllowedActionsBuilder(); + allowedActionsBuilder.add(NavigationActionType.Drag, NavigationActionType.Zoom, MenuActionType.ToggleMaximizeViewer); + this.allowedActionsWhenSelected = allowedActionsBuilder.create(); + this.allowedActions = allowedActionsBuilder.add(NavigationActionType.Scroll).create(); this.doneApplyingMaskListener = (obs, oldv, newv) -> { if (!newv) @@ -335,6 +325,10 @@ public void enterMode(final PainteraBaseView paintera, final ViewerPanelFX viewe lastAllowedActions = paintera.allowedActionsProperty().get(); paintera.allowedActionsProperty().set(allowedActions); + // properly exit the mode if somebody else wants to switch it + modeSwitchListener = obs -> exitMode(paintera, false); + paintera.allowedActionsProperty().addListener(modeSwitchListener); + lastSelectedId = selectedIds.getLastSelection(); lastActiveIds = selectedIds.getActiveIds(); newLabelId = idService.next(); @@ -375,6 +369,9 @@ public void exitMode(final PainteraBaseView paintera, final boolean completed) selectedIds.activate(lastActiveIds); selectedIds.activateAlso(lastSelectedId); + paintera.allowedActionsProperty().removeListener(modeSwitchListener); + modeSwitchListener = null; + paintera.allowedActionsProperty().set(lastAllowedActions); lastAllowedActions = null; diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/ActionType.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/ActionType.java new file mode 100644 index 000000000..36ae084aa --- /dev/null +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/ActionType.java @@ -0,0 +1,8 @@ +package org.janelia.saalfeldlab.paintera.control.actions; + +/** + * Marker interface for all action types. + */ +public interface ActionType +{ +} diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/AllowedActions.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/AllowedActions.java index 906040bb5..7bb12bbe5 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/AllowedActions.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/AllowedActions.java @@ -1,70 +1,137 @@ package org.janelia.saalfeldlab.paintera.control.actions; -import java.util.EnumSet; -import java.util.function.Consumer; - -import org.janelia.saalfeldlab.paintera.PainteraBaseView; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.BooleanSupplier; +import java.util.stream.Collectors; /** * Describes what actions in the UI are allowed in the current application mode. + * An optional custom predicate can be specified for any action type to determine if it is allowed (or do extra handling before the action is performed). */ public final class AllowedActions { - private final EnumSet<NavigationAction> navigationAllowedActions; - private final EnumSet<LabelAction> labelAllowedActions; - private final EnumSet<PaintAction> paintAllowedActions; - private final EnumSet<MenuAction> menuAllowedActions; - - private final Consumer<PainteraBaseView> cleanup; + private final Map<ActionType, BooleanSupplier> actions; - public AllowedActions( - final EnumSet<NavigationAction> navigationAllowedActions, - final EnumSet<LabelAction> labelAllowedActions, - final EnumSet<PaintAction> paintAllowedActions, - final EnumSet<MenuAction> menuAllowedActions, - final Consumer<PainteraBaseView> cleanup) + private AllowedActions(final Map<ActionType, BooleanSupplier> actions) { - this.navigationAllowedActions = navigationAllowedActions; - this.labelAllowedActions = labelAllowedActions; - this.paintAllowedActions = paintAllowedActions; - this.menuAllowedActions = menuAllowedActions; - this.cleanup = cleanup; + this.actions = actions; } - public boolean isAllowed(final NavigationAction navigationAction) + public boolean isAllowed(final ActionType actionType) { - return this.navigationAllowedActions.contains(navigationAction); + return Optional.ofNullable(this.actions.get(actionType)).orElse(() -> false).getAsBoolean(); } - public boolean isAllowed(final LabelAction labelAction) + public void runIfAllowed(final ActionType actionType, final Runnable action) { - return this.labelAllowedActions.contains(labelAction); + if (isAllowed(actionType)) + action.run(); } - public boolean isAllowed(final PaintAction paintAction) + /** + * Used to create an instance of {@link AllowedActions}. + */ + public static class AllowedActionsBuilder { - return this.paintAllowedActions.contains(paintAction); - } + private static final Map<ActionType, BooleanSupplier> ALL; + static + { + final Set<ActionType> actions = new HashSet<>(); + actions.addAll(NavigationActionType.all()); + actions.addAll(LabelActionType.all()); + actions.addAll(PaintActionType.all()); + actions.addAll(MenuActionType.all()); + ALL = toMap(actions); + } + private static Map<ActionType, BooleanSupplier> toMap(final Collection<ActionType> actions) + { + return actions.stream().collect(Collectors.toMap(t -> t, t -> () -> true)); + } - public boolean isAllowed(final MenuAction menuAction) - { - return this.menuAllowedActions.contains(menuAction); - } + /** + * Create a new instance of {@link AllowedActions} with all known actions. + * + * @return + */ + public static AllowedActions all() + { + return new AllowedActions(ALL); + } - public void cleanup(final PainteraBaseView baseView) - { - if (this.cleanup != null) - this.cleanup.accept(baseView); - } + private final Map<ActionType, BooleanSupplier> actions; - public static AllowedActions all() - { - return new AllowedActions( - NavigationAction.all(), - LabelAction.all(), - PaintAction.all(), - MenuAction.all(), - null - ); + public AllowedActionsBuilder() + { + this.actions = new HashMap<>(); + } + + /** + * Add one or more allowed {@link ActionType actions}. + * + * @param first + * @param rest + * @return + */ + public AllowedActionsBuilder add(final ActionType first, final ActionType... rest) + { + final Set<ActionType> set = new HashSet<>(); + set.add(first); + set.addAll(Arrays.asList(rest)); + add(set); + return this; + } + + /** + * Add a collection of allowed {@link ActionType actions}. + * + * @param actions + * @return + */ + public AllowedActionsBuilder add(final Collection<ActionType> actions) + { + this.actions.putAll(toMap(actions)); + return this; + } + + /** + * Add an {@link ActionType action} with a custom predicate to determine if it is allowed. + * + * @param action + * @param predicate + * @return + */ + public AllowedActionsBuilder add(final ActionType action, final BooleanSupplier predicate) + { + this.actions.put(action, predicate); + return this; + } + + /** + * Add a collection of {@link ActionType actions} with a custom predicate to determine if it is allowed. + * + * @param actions + * @return + */ + public AllowedActionsBuilder add(final Map<ActionType, BooleanSupplier> actions) + { + this.actions.putAll(actions); + return this; + } + + /** + * Create a new instance of {@link AllowedActions} with the current set of actions. + * + * @return + */ + public AllowedActions create() + { + return new AllowedActions(new HashMap<>(this.actions)); + } } } diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/LabelAction.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/LabelAction.java deleted file mode 100644 index 190d5ba4b..000000000 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/LabelAction.java +++ /dev/null @@ -1,28 +0,0 @@ -package org.janelia.saalfeldlab.paintera.control.actions; - -import java.util.EnumSet; - -public enum LabelAction -{ - Toggle, - Append, - CreateNew, - Lock, - Merge, - Split; - - public static EnumSet<LabelAction> of(final LabelAction first, final LabelAction... rest) - { - return EnumSet.of(first, rest); - } - - public static EnumSet<LabelAction> all() - { - return EnumSet.allOf(LabelAction.class); - } - - public static EnumSet<LabelAction> none() - { - return EnumSet.noneOf(LabelAction.class); - } -} diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/LabelActionType.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/LabelActionType.java new file mode 100644 index 000000000..a1f8aa163 --- /dev/null +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/LabelActionType.java @@ -0,0 +1,28 @@ +package org.janelia.saalfeldlab.paintera.control.actions; + +import java.util.EnumSet; + +public enum LabelActionType implements ActionType +{ + Toggle, + Append, + CreateNew, + Lock, + Merge, + Split; + + public static EnumSet<LabelActionType> of(final LabelActionType first, final LabelActionType... rest) + { + return EnumSet.of(first, rest); + } + + public static EnumSet<LabelActionType> all() + { + return EnumSet.allOf(LabelActionType.class); + } + + public static EnumSet<LabelActionType> none() + { + return EnumSet.noneOf(LabelActionType.class); + } +} diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/MenuAction.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/MenuAction.java deleted file mode 100644 index 63b0108c7..000000000 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/MenuAction.java +++ /dev/null @@ -1,30 +0,0 @@ -package org.janelia.saalfeldlab.paintera.control.actions; - -import java.util.EnumSet; - -public enum MenuAction -{ - AddSource, - CreateNewLabelSource, - ChangeActiveSource, - SidePanel, - ToggleMaximizeViewer, - OrthoslicesContextMenu, - SaveProject, - CommitCanvas; - - public static EnumSet<MenuAction> of(final MenuAction first, final MenuAction... rest) - { - return EnumSet.of(first, rest); - } - - public static EnumSet<MenuAction> all() - { - return EnumSet.allOf(MenuAction.class); - } - - public static EnumSet<MenuAction> none() - { - return EnumSet.noneOf(MenuAction.class); - } -} diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/MenuActionType.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/MenuActionType.java new file mode 100644 index 000000000..74aa3a923 --- /dev/null +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/MenuActionType.java @@ -0,0 +1,30 @@ +package org.janelia.saalfeldlab.paintera.control.actions; + +import java.util.EnumSet; + +public enum MenuActionType implements ActionType +{ + AddSource, + CreateNewLabelSource, + ChangeActiveSource, + SidePanel, + ToggleMaximizeViewer, + OrthoslicesContextMenu, + SaveProject, + CommitCanvas; + + public static EnumSet<MenuActionType> of(final MenuActionType first, final MenuActionType... rest) + { + return EnumSet.of(first, rest); + } + + public static EnumSet<MenuActionType> all() + { + return EnumSet.allOf(MenuActionType.class); + } + + public static EnumSet<MenuActionType> none() + { + return EnumSet.noneOf(MenuActionType.class); + } +} diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/NavigationAction.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/NavigationAction.java deleted file mode 100644 index 57788a4e8..000000000 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/NavigationAction.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.janelia.saalfeldlab.paintera.control.actions; - -import java.util.EnumSet; - -public enum NavigationAction -{ - Drag, - Scroll, - Zoom, - Rotate; - - public static EnumSet<NavigationAction> of(final NavigationAction first, final NavigationAction... rest) - { - return EnumSet.of(first, rest); - } - - public static EnumSet<NavigationAction> all() - { - return EnumSet.allOf(NavigationAction.class); - } - - public static EnumSet<NavigationAction> none() - { - return EnumSet.noneOf(NavigationAction.class); - } -} diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/NavigationActionType.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/NavigationActionType.java new file mode 100644 index 000000000..d65cb5c3b --- /dev/null +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/NavigationActionType.java @@ -0,0 +1,26 @@ +package org.janelia.saalfeldlab.paintera.control.actions; + +import java.util.EnumSet; + +public enum NavigationActionType implements ActionType +{ + Drag, + Scroll, + Zoom, + Rotate; + + public static EnumSet<NavigationActionType> of(final NavigationActionType first, final NavigationActionType... rest) + { + return EnumSet.of(first, rest); + } + + public static EnumSet<NavigationActionType> all() + { + return EnumSet.allOf(NavigationActionType.class); + } + + public static EnumSet<NavigationActionType> none() + { + return EnumSet.noneOf(NavigationActionType.class); + } +} diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/PaintAction.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/PaintAction.java deleted file mode 100644 index dc0436c7a..000000000 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/PaintAction.java +++ /dev/null @@ -1,28 +0,0 @@ -package org.janelia.saalfeldlab.paintera.control.actions; - -import java.util.EnumSet; - -public enum PaintAction -{ - Paint, - Erase, - Background, - Fill, - Restrict, - SetBrush; - - public static EnumSet<PaintAction> of(final PaintAction first, final PaintAction... rest) - { - return EnumSet.of(first, rest); - } - - public static EnumSet<PaintAction> all() - { - return EnumSet.allOf(PaintAction.class); - } - - public static EnumSet<PaintAction> none() - { - return EnumSet.noneOf(PaintAction.class); - } -} diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/PaintActionType.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/PaintActionType.java new file mode 100644 index 000000000..60e7f194e --- /dev/null +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/PaintActionType.java @@ -0,0 +1,28 @@ +package org.janelia.saalfeldlab.paintera.control.actions; + +import java.util.EnumSet; + +public enum PaintActionType implements ActionType +{ + Paint, + Erase, + Background, + Fill, + Restrict, + SetBrush; + + public static EnumSet<PaintActionType> of(final PaintActionType first, final PaintActionType... rest) + { + return EnumSet.of(first, rest); + } + + public static EnumSet<PaintActionType> all() + { + return EnumSet.allOf(PaintActionType.class); + } + + public static EnumSet<PaintActionType> none() + { + return EnumSet.noneOf(PaintActionType.class); + } +} diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStateIdSelectorHandler.java b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStateIdSelectorHandler.java index 714b00165..f6fbc4540 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStateIdSelectorHandler.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStateIdSelectorHandler.java @@ -1,19 +1,14 @@ package org.janelia.saalfeldlab.paintera.state; -import bdv.fx.viewer.ViewerPanelFX; -import javafx.event.Event; -import javafx.event.EventHandler; -import javafx.event.EventTarget; -import javafx.scene.Node; -import javafx.scene.input.KeyCode; -import javafx.scene.input.MouseEvent; -import net.imglib2.type.numeric.IntegerType; +import java.lang.invoke.MethodHandles; +import java.util.HashMap; + import org.janelia.saalfeldlab.fx.event.DelegateEventHandlers; import org.janelia.saalfeldlab.fx.event.EventFX; import org.janelia.saalfeldlab.fx.event.KeyTracker; import org.janelia.saalfeldlab.paintera.PainteraBaseView; import org.janelia.saalfeldlab.paintera.control.IdSelector; -import org.janelia.saalfeldlab.paintera.control.actions.LabelAction; +import org.janelia.saalfeldlab.paintera.control.actions.LabelActionType; import org.janelia.saalfeldlab.paintera.control.assignment.FragmentSegmentAssignment; import org.janelia.saalfeldlab.paintera.control.lock.LockedSegments; import org.janelia.saalfeldlab.paintera.control.paint.SelectNextId; @@ -22,8 +17,14 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.lang.invoke.MethodHandles; -import java.util.HashMap; +import bdv.fx.viewer.ViewerPanelFX; +import javafx.event.Event; +import javafx.event.EventHandler; +import javafx.event.EventTarget; +import javafx.scene.Node; +import javafx.scene.input.KeyCode; +import javafx.scene.input.MouseEvent; +import net.imglib2.type.numeric.IntegerType; public class LabelSourceStateIdSelectorHandler { @@ -74,24 +75,24 @@ private EventHandler<Event> makeHandler(final PainteraBaseView paintera, final K // TODO event handlers should probably not be on ANY/RELEASED but on PRESSED handler.addEventHandler(MouseEvent.ANY, selector.selectFragmentWithMaximumCount( "toggle single id", - event -> paintera.allowedActionsProperty().get().isAllowed(LabelAction.Toggle) && event.isPrimaryButtonDown() && keyTracker.noKeysActive()).handler()); + event -> paintera.allowedActionsProperty().get().isAllowed(LabelActionType.Toggle) && event.isPrimaryButtonDown() && keyTracker.noKeysActive()).handler()); handler.addEventHandler(MouseEvent.ANY, selector.appendFragmentWithMaximumCount( "append id", - event -> paintera.allowedActionsProperty().get().isAllowed(LabelAction.Append) && + event -> paintera.allowedActionsProperty().get().isAllowed(LabelActionType.Append) && ((event.isSecondaryButtonDown() && keyTracker.noKeysActive()) || (event.isPrimaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL)))) .handler()); handler.addOnKeyPressed(EventFX.KEY_PRESSED( "lock segment", e -> selector.toggleLock(selectedIds, assignment, lockedSegments), - e -> paintera.allowedActionsProperty().get().isAllowed(LabelAction.Lock) && keyTracker.areOnlyTheseKeysDown(KeyCode.L))); + e -> paintera.allowedActionsProperty().get().isAllowed(LabelActionType.Lock) && keyTracker.areOnlyTheseKeysDown(KeyCode.L))); final SourceInfo sourceInfo = paintera.sourceInfo(); final SelectNextId nextId = new SelectNextId(sourceInfo); handler.addOnKeyPressed(EventFX.KEY_PRESSED( "next id", event -> nextId.getNextId(), - event -> paintera.allowedActionsProperty().get().isAllowed(LabelAction.CreateNew) && keyTracker.areOnlyTheseKeysDown(KeyCode.N))); + event -> paintera.allowedActionsProperty().get().isAllowed(LabelActionType.CreateNew) && keyTracker.areOnlyTheseKeysDown(KeyCode.N))); return handler; } } diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStateMergeDetachHandler.java b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStateMergeDetachHandler.java index 4c968b18e..d68610b85 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStateMergeDetachHandler.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStateMergeDetachHandler.java @@ -1,5 +1,26 @@ package org.janelia.saalfeldlab.paintera.state; +import java.lang.invoke.MethodHandles; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Optional; +import java.util.function.Consumer; + +import org.janelia.saalfeldlab.fx.event.DelegateEventHandlers; +import org.janelia.saalfeldlab.fx.event.EventFX; +import org.janelia.saalfeldlab.fx.event.KeyTracker; +import org.janelia.saalfeldlab.paintera.PainteraBaseView; +import org.janelia.saalfeldlab.paintera.control.actions.LabelActionType; +import org.janelia.saalfeldlab.paintera.control.assignment.FragmentSegmentAssignment; +import org.janelia.saalfeldlab.paintera.control.assignment.action.AssignmentAction; +import org.janelia.saalfeldlab.paintera.control.assignment.action.Detach; +import org.janelia.saalfeldlab.paintera.control.assignment.action.Merge; +import org.janelia.saalfeldlab.paintera.control.selection.SelectedIds; +import org.janelia.saalfeldlab.paintera.data.DataSource; +import org.janelia.saalfeldlab.paintera.id.IdService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import bdv.fx.viewer.ViewerPanelFX; import bdv.fx.viewer.ViewerState; import bdv.viewer.Interpolation; @@ -24,26 +45,6 @@ import net.imglib2.type.numeric.IntegerType; import net.imglib2.view.IntervalView; import net.imglib2.view.Views; -import org.janelia.saalfeldlab.fx.event.DelegateEventHandlers; -import org.janelia.saalfeldlab.fx.event.EventFX; -import org.janelia.saalfeldlab.fx.event.KeyTracker; -import org.janelia.saalfeldlab.paintera.PainteraBaseView; -import org.janelia.saalfeldlab.paintera.control.actions.LabelAction; -import org.janelia.saalfeldlab.paintera.control.assignment.FragmentSegmentAssignment; -import org.janelia.saalfeldlab.paintera.control.assignment.action.AssignmentAction; -import org.janelia.saalfeldlab.paintera.control.assignment.action.Detach; -import org.janelia.saalfeldlab.paintera.control.assignment.action.Merge; -import org.janelia.saalfeldlab.paintera.control.selection.SelectedIds; -import org.janelia.saalfeldlab.paintera.data.DataSource; -import org.janelia.saalfeldlab.paintera.id.IdService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.lang.invoke.MethodHandles; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Optional; -import java.util.function.Consumer; public class LabelSourceStateMergeDetachHandler { @@ -88,20 +89,20 @@ public EventHandler<Event> viewerHandler(final PainteraBaseView paintera, final }; } - private EventHandler<Event> makeHandler(PainteraBaseView paintera, KeyTracker keyTracker, ViewerPanelFX vp) { + private EventHandler<Event> makeHandler(final PainteraBaseView paintera, final KeyTracker keyTracker, final ViewerPanelFX vp) { final DelegateEventHandlers.AnyHandler handler = DelegateEventHandlers.handleAny(); handler.addOnMousePressed(EventFX.MOUSE_PRESSED( "merge fragments", new MergeFragments(vp), - e -> paintera.allowedActionsProperty().get().isAllowed(LabelAction.Merge) && e.isPrimaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT))); + e -> paintera.allowedActionsProperty().get().isAllowed(LabelActionType.Merge) && e.isPrimaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT))); handler.addOnMousePressed(EventFX.MOUSE_PRESSED( "detach fragment", new DetachFragment(vp), - e -> paintera.allowedActionsProperty().get().isAllowed(LabelAction.Split) && e.isSecondaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT))); + e -> paintera.allowedActionsProperty().get().isAllowed(LabelActionType.Split) && e.isSecondaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT))); handler.addOnMousePressed(EventFX.MOUSE_PRESSED( "detach fragment", new ConfirmSelection(vp), - e -> paintera.allowedActionsProperty().get().isAllowed(LabelAction.Split) && e.isSecondaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT, KeyCode.CONTROL))); + e -> paintera.allowedActionsProperty().get().isAllowed(LabelActionType.Split) && e.isSecondaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT, KeyCode.CONTROL))); return handler; } @@ -109,10 +110,11 @@ private class MergeFragments implements Consumer<MouseEvent> { private final ViewerPanelFX viewer; - private MergeFragments(ViewerPanelFX viewer) { + private MergeFragments(final ViewerPanelFX viewer) { this.viewer = viewer; } + @Override public void accept(final MouseEvent e) { synchronized (viewer) @@ -156,10 +158,11 @@ private class DetachFragment implements Consumer<MouseEvent> { private final ViewerPanelFX viewer; - private DetachFragment(ViewerPanelFX viewer) { + private DetachFragment(final ViewerPanelFX viewer) { this.viewer = viewer; } + @Override public void accept(final MouseEvent e) { final long lastSelection = selectedIds.getLastSelection(); @@ -196,10 +199,11 @@ private class ConfirmSelection implements Consumer<MouseEvent> private final ViewerPanelFX viewer; - private ConfirmSelection(ViewerPanelFX viewer) { + private ConfirmSelection(final ViewerPanelFX viewer) { this.viewer = viewer; } + @Override public void accept(final MouseEvent e) { final long[] activeFragments = selectedIds.getActiveIds(); diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStatePaintHandler.java b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStatePaintHandler.java index 483a60dfa..43d33b850 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStatePaintHandler.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStatePaintHandler.java @@ -1,23 +1,16 @@ package org.janelia.saalfeldlab.paintera.state; -import bdv.fx.viewer.ViewerPanelFX; -import bdv.viewer.Source; -import javafx.beans.property.ObjectProperty; -import javafx.beans.property.SimpleDoubleProperty; -import javafx.event.Event; -import javafx.event.EventHandler; -import javafx.event.EventTarget; -import javafx.scene.Node; -import javafx.scene.input.KeyCode; -import javafx.scene.input.KeyEvent; -import javafx.scene.input.MouseEvent; -import net.imglib2.type.label.Label; +import java.lang.invoke.MethodHandles; +import java.util.HashMap; +import java.util.Optional; +import java.util.function.Supplier; + import org.janelia.saalfeldlab.fx.event.DelegateEventHandlers; import org.janelia.saalfeldlab.fx.event.EventFX; import org.janelia.saalfeldlab.fx.event.KeyTracker; import org.janelia.saalfeldlab.paintera.PainteraBaseView; import org.janelia.saalfeldlab.paintera.control.ControlUtils; -import org.janelia.saalfeldlab.paintera.control.actions.PaintAction; +import org.janelia.saalfeldlab.paintera.control.actions.PaintActionType; import org.janelia.saalfeldlab.paintera.control.paint.Fill2DOverlay; import org.janelia.saalfeldlab.paintera.control.paint.FillOverlay; import org.janelia.saalfeldlab.paintera.control.paint.FloodFill; @@ -29,10 +22,18 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.lang.invoke.MethodHandles; -import java.util.HashMap; -import java.util.Optional; -import java.util.function.Supplier; +import bdv.fx.viewer.ViewerPanelFX; +import bdv.viewer.Source; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleDoubleProperty; +import javafx.event.Event; +import javafx.event.EventHandler; +import javafx.event.EventTarget; +import javafx.scene.Node; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; +import javafx.scene.input.MouseEvent; +import net.imglib2.type.label.Label; public class LabelSourceStatePaintHandler { @@ -72,7 +73,7 @@ public EventHandler<Event> viewerHandler(final PainteraBaseView paintera, final }; } - public EventHandler<Event> viewerFilter(PainteraBaseView paintera, KeyTracker keyTracker) { + public EventHandler<Event> viewerFilter(final PainteraBaseView paintera, final KeyTracker keyTracker) { return event -> { final EventTarget target = event.getTarget(); if (MouseEvent.MOUSE_EXITED.equals(event.getEventType()) && target instanceof ViewerPanelFX) @@ -80,7 +81,7 @@ public EventHandler<Event> viewerFilter(PainteraBaseView paintera, KeyTracker ke }; } - private EventHandler<Event> makeHandler(PainteraBaseView paintera, KeyTracker keyTracker, final ViewerPanelFX t) + private EventHandler<Event> makeHandler(final PainteraBaseView paintera, final KeyTracker keyTracker, final ViewerPanelFX t) { LOG.debug("Making handler with PainterBaseView {} key Tracker {} and ViewerPanelFX {}", paintera, keyTracker, t); @@ -125,22 +126,22 @@ private EventHandler<Event> makeHandler(PainteraBaseView paintera, KeyTracker ke handler.addEventHandler(KeyEvent.KEY_PRESSED, EventFX.KEY_PRESSED( "show brush overlay", event -> {LOG.trace("Showing brush overlay!"); paint2D.showBrushOverlay();}, - event -> paintera.allowedActionsProperty().get().isAllowed(PaintAction.Paint) && keyTracker.areKeysDown(KeyCode.SPACE))); + event -> paintera.allowedActionsProperty().get().isAllowed(PaintActionType.Paint) && keyTracker.areKeysDown(KeyCode.SPACE))); handler.addEventHandler(KeyEvent.KEY_RELEASED, EventFX.KEY_RELEASED( "hide brush overlay", event -> {LOG.trace("Hiding brush overlay!"); paint2D.hideBrushOverlay();}, - event -> paintera.allowedActionsProperty().get().isAllowed(PaintAction.Paint) && event.getCode().equals(KeyCode.SPACE) && !keyTracker.areKeysDown(KeyCode.SPACE))); + event -> paintera.allowedActionsProperty().get().isAllowed(PaintActionType.Paint) && event.getCode().equals(KeyCode.SPACE) && !keyTracker.areKeysDown(KeyCode.SPACE))); handler.addOnScroll(EventFX.SCROLL( "change brush size", event -> paint2D.changeBrushRadius(event.getDeltaY()), - event -> paintera.allowedActionsProperty().get().isAllowed(PaintAction.SetBrush) && keyTracker.areOnlyTheseKeysDown(KeyCode.SPACE))); + event -> paintera.allowedActionsProperty().get().isAllowed(PaintActionType.SetBrush) && keyTracker.areOnlyTheseKeysDown(KeyCode.SPACE))); handler.addOnScroll(EventFX.SCROLL( "change brush depth", event -> paint2D.changeBrushDepth(-ControlUtils.getBiggestScroll(event)), - event -> paintera.allowedActionsProperty().get().isAllowed(PaintAction.SetBrush) && + event -> paintera.allowedActionsProperty().get().isAllowed(PaintActionType.SetBrush) && (keyTracker.areOnlyTheseKeysDown(KeyCode.SPACE, KeyCode.SHIFT) || keyTracker.areOnlyTheseKeysDown(KeyCode.F) || keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT,KeyCode.F)))); @@ -148,22 +149,22 @@ private EventHandler<Event> makeHandler(PainteraBaseView paintera, KeyTracker ke handler.addOnKeyPressed(EventFX.KEY_PRESSED("show fill 2D overlay", event -> { fill2DOverlay.setVisible(true); fillOverlay.setVisible(false); - }, event -> paintera.allowedActionsProperty().get().isAllowed(PaintAction.Fill) && keyTracker.areOnlyTheseKeysDown(KeyCode.F))); + }, event -> paintera.allowedActionsProperty().get().isAllowed(PaintActionType.Fill) && keyTracker.areOnlyTheseKeysDown(KeyCode.F))); handler.addOnKeyReleased(EventFX.KEY_RELEASED( "show fill 2D overlay", event -> fill2DOverlay.setVisible(false), - event -> paintera.allowedActionsProperty().get().isAllowed(PaintAction.Fill) && event.getCode().equals(KeyCode.F) && keyTracker.noKeysActive())); + event -> paintera.allowedActionsProperty().get().isAllowed(PaintActionType.Fill) && event.getCode().equals(KeyCode.F) && keyTracker.noKeysActive())); handler.addOnKeyPressed(EventFX.KEY_PRESSED("show fill overlay", event -> { fillOverlay.setVisible(true); fill2DOverlay.setVisible(false); - }, event -> paintera.allowedActionsProperty().get().isAllowed(PaintAction.Fill) && keyTracker.areOnlyTheseKeysDown(KeyCode.F, KeyCode.SHIFT))); + }, event -> paintera.allowedActionsProperty().get().isAllowed(PaintActionType.Fill) && keyTracker.areOnlyTheseKeysDown(KeyCode.F, KeyCode.SHIFT))); handler.addOnKeyReleased(EventFX.KEY_RELEASED( "show fill overlay", event -> fillOverlay.setVisible(false), - event -> paintera.allowedActionsProperty().get().isAllowed(PaintAction.Fill) && + event -> paintera.allowedActionsProperty().get().isAllowed(PaintActionType.Fill) && ((event.getCode().equals(KeyCode.F) && keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT)) || (event.getCode().equals(KeyCode.SHIFT) && keyTracker.areOnlyTheseKeysDown(KeyCode.F)) ))); @@ -175,7 +176,7 @@ private EventHandler<Event> makeHandler(PainteraBaseView paintera, KeyTracker ke paintSelection, brushRadius::get, brushDepth::get, - event -> paintera.allowedActionsProperty().get().isAllowed(PaintAction.Paint) && event.isPrimaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.SPACE)); + event -> paintera.allowedActionsProperty().get().isAllowed(PaintActionType.Paint) && event.isPrimaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.SPACE)); handler.addEventHandler(MouseEvent.ANY, paintDrag.singleEventHandler()); // erase @@ -185,7 +186,7 @@ private EventHandler<Event> makeHandler(PainteraBaseView paintera, KeyTracker ke () -> Label.TRANSPARENT, brushRadius::get, brushDepth::get, - event -> paintera.allowedActionsProperty().get().isAllowed(PaintAction.Erase) && event.isSecondaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.SPACE)); + event -> paintera.allowedActionsProperty().get().isAllowed(PaintActionType.Erase) && event.isSecondaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.SPACE)); handler.addEventHandler(MouseEvent.ANY, eraseDrag.singleEventHandler()); // background @@ -195,26 +196,26 @@ private EventHandler<Event> makeHandler(PainteraBaseView paintera, KeyTracker ke () -> Label.BACKGROUND, brushRadius::get, brushDepth::get, - event -> paintera.allowedActionsProperty().get().isAllowed(PaintAction.Background) && event.isSecondaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.SPACE, KeyCode.SHIFT)); + event -> paintera.allowedActionsProperty().get().isAllowed(PaintActionType.Background) && event.isSecondaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.SPACE, KeyCode.SHIFT)); handler.addEventHandler(MouseEvent.ANY, backgroundDrag.singleEventHandler()); // advanced paint stuff handler.addOnMousePressed((EventFX.MOUSE_PRESSED( "fill", event -> fill.fillAt(event.getX(), event.getY(), paintSelection::get), - event -> paintera.allowedActionsProperty().get().isAllowed(PaintAction.Fill) && event.isPrimaryButtonDown() && keyTracker.areOnlyTheseKeysDown( + event -> paintera.allowedActionsProperty().get().isAllowed(PaintActionType.Fill) && event.isPrimaryButtonDown() && keyTracker.areOnlyTheseKeysDown( KeyCode.SHIFT, KeyCode.F)))); handler.addOnMousePressed(EventFX.MOUSE_PRESSED( "fill 2D", event -> fill2D.fillAt(event.getX(), event.getY(), paintSelection::get), - event -> paintera.allowedActionsProperty().get().isAllowed(PaintAction.Fill) && event.isPrimaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.F))); + event -> paintera.allowedActionsProperty().get().isAllowed(PaintActionType.Fill) && event.isPrimaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.F))); handler.addOnMousePressed(EventFX.MOUSE_PRESSED( "restrict", event -> restrictor.restrictTo(event.getX(), event.getY()), - event -> paintera.allowedActionsProperty().get().isAllowed(PaintAction.Restrict) && event.isPrimaryButtonDown() && keyTracker.areOnlyTheseKeysDown( + event -> paintera.allowedActionsProperty().get().isAllowed(PaintActionType.Restrict) && event.isPrimaryButtonDown() && keyTracker.areOnlyTheseKeysDown( KeyCode.SHIFT, KeyCode.R))); From e7f537beb8606c4ea10bdb7fc10d4865307181fb Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Fri, 7 Jun 2019 11:54:28 -0400 Subject: [PATCH 65/84] fix sections on scroll, commit interpolated shape on Enter --- .../paintera/control/Navigation.java | 81 ++++++++++++------- .../control/ShapeInterpolationMode.java | 41 +++------- 2 files changed, 67 insertions(+), 55 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/Navigation.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/Navigation.java index 1e49961cb..b117deb53 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/Navigation.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/Navigation.java @@ -166,52 +166,79 @@ public Consumer<ViewerPanelFX> getOnEnter() iars.add(EventFX.SCROLL( "translate along normal", - e -> scrollDefault.scroll(-ControlUtils.getBiggestScroll(e)), - event -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Scroll) && keyTracker.noKeysActive() - )); + e -> this.allowedActionsProperty.get().runIfAllowed( + NavigationActionType.Scroll, + () -> scrollDefault.scroll(-ControlUtils.getBiggestScroll(e)) + ), + event -> keyTracker.noKeysActive() + )); iars.add(EventFX.SCROLL( "translate along normal fast", - e -> scrollFast.scroll(-ControlUtils.getBiggestScroll(e)), - event -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Scroll) && keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT) - )); + e -> this.allowedActionsProperty.get().runIfAllowed( + NavigationActionType.Scroll, + () -> scrollFast.scroll(-ControlUtils.getBiggestScroll(e)) + ), + event -> keyTracker.areOnlyTheseKeysDown(KeyCode.SHIFT) + )); iars.add(EventFX.SCROLL( "translate along normal slow", - e -> scrollSlow.scroll(-ControlUtils.getBiggestScroll(e)), - event -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Scroll) && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL) - )); + e -> this.allowedActionsProperty.get().runIfAllowed( + NavigationActionType.Scroll, + () -> scrollSlow.scroll(-ControlUtils.getBiggestScroll(e)) + ), + event -> keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL) + )); iars.add(EventFX.KEY_PRESSED( "button translate along normal bck", - e -> scrollDefault.scroll(+1), - event -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Scroll) && keyTracker.areOnlyTheseKeysDown(KeyCode.COMMA) - )); + e -> this.allowedActionsProperty.get().runIfAllowed( + NavigationActionType.Scroll, + () -> scrollDefault.scroll(+1) + ), + event -> keyTracker.areOnlyTheseKeysDown(KeyCode.COMMA) + )); iars.add(EventFX.KEY_PRESSED( "button translate along normal fwd", - e -> scrollDefault.scroll(-1), - event -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Scroll) && keyTracker.areOnlyTheseKeysDown(KeyCode.PERIOD) - )); + e -> this.allowedActionsProperty.get().runIfAllowed( + NavigationActionType.Scroll, + () -> scrollDefault.scroll(-1) + ), + event -> keyTracker.areOnlyTheseKeysDown(KeyCode.PERIOD) + )); iars.add(EventFX.KEY_PRESSED( "button translate along normal fast bck", - e -> scrollFast.scroll(+1), - event -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Scroll) && keyTracker.areOnlyTheseKeysDown(KeyCode.COMMA, KeyCode.SHIFT) - )); + e -> this.allowedActionsProperty.get().runIfAllowed( + NavigationActionType.Scroll, + () -> scrollFast.scroll(+1) + ), + event -> keyTracker.areOnlyTheseKeysDown(KeyCode.COMMA, KeyCode.SHIFT) + )); iars.add(EventFX.KEY_PRESSED( "button translate along normal fast fwd", - e -> scrollFast.scroll(-1), - event -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Scroll) && keyTracker.areOnlyTheseKeysDown(KeyCode.PERIOD, KeyCode.SHIFT) - )); + e -> this.allowedActionsProperty.get().runIfAllowed( + NavigationActionType.Scroll, + () -> scrollFast.scroll(-1) + ), + event -> keyTracker.areOnlyTheseKeysDown(KeyCode.PERIOD, KeyCode.SHIFT) + )); iars.add(EventFX.KEY_PRESSED( "button translate along normal slow bck", - e -> scrollSlow.scroll(+1), - event -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Scroll) && keyTracker.areOnlyTheseKeysDown(KeyCode.COMMA, KeyCode.CONTROL) - )); + e -> this.allowedActionsProperty.get().runIfAllowed( + NavigationActionType.Scroll, + () -> scrollSlow.scroll(+1) + ), + event -> keyTracker.areOnlyTheseKeysDown(KeyCode.COMMA, KeyCode.CONTROL) + )); iars.add(EventFX.KEY_PRESSED( "button translate along normal slow fwd", - e -> scrollSlow.scroll(-1), - event -> this.allowedActionsProperty.get().isAllowed(NavigationActionType.Scroll) && keyTracker.areOnlyTheseKeysDown(KeyCode.PERIOD, KeyCode.CONTROL) - )); + e -> this.allowedActionsProperty.get().runIfAllowed( + NavigationActionType.Scroll, + () -> scrollSlow.scroll(-1) + ), + event -> keyTracker.areOnlyTheseKeysDown(KeyCode.PERIOD, KeyCode.CONTROL) + )); iars.add(MouseDragFX.createDrag( "translate xy", diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java index dea7c2368..419921edf 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java @@ -167,15 +167,12 @@ private static final class SectionInfo private final HighlightingStreamConverter<?> converter; private final FragmentSegmentAssignment assignment; - private final AllowedActions allowedActions; - private final AllowedActions allowedActionsWhenSelected; - - private final ChangeListener<Boolean> doneApplyingMaskListener; - + private InvalidationListener modeSwitchListener; private AllowedActions lastAllowedActions; private long lastSelectedId; private long[] lastActiveIds; + private final ChangeListener<Boolean> doneApplyingMaskListener; private Mask<UnsignedLongType> mask; private long newLabelId; private long currentFillValue; @@ -189,8 +186,6 @@ private static final class SectionInfo private final ObjectProperty<ActiveSection> activeSection = new SimpleObjectProperty<>(); private Thread workerThread; - private InvalidationListener modeSwitchListener; - private Pair<RealRandomAccessible<UnsignedLongType>, RealRandomAccessible<VolatileUnsignedLongType>> interpolatedMaskImgs; public ShapeInterpolationMode( @@ -208,11 +203,6 @@ public ShapeInterpolationMode( this.converter = converter; this.assignment = assignment; - final AllowedActionsBuilder allowedActionsBuilder = new AllowedActionsBuilder(); - allowedActionsBuilder.add(NavigationActionType.Drag, NavigationActionType.Zoom, MenuActionType.ToggleMaximizeViewer); - this.allowedActionsWhenSelected = allowedActionsBuilder.create(); - this.allowedActions = allowedActionsBuilder.add(NavigationActionType.Scroll).create(); - this.doneApplyingMaskListener = (obs, oldv, newv) -> { if (!newv) doneApplyingMask(); @@ -257,23 +247,13 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke e -> isModeOn() && keyTracker.areOnlyTheseKeysDown(KeyCode.ESCAPE) ) ); - filter.addEventHandler( - KeyEvent.KEY_PRESSED, - EventFX.KEY_PRESSED( - "fix selection", - e -> {e.consume(); fixSelection(paintera);}, - e -> (modeState.get() == ModeState.Select || modeState.get() == ModeState.Edit) && - !selectedObjects.isEmpty() && - keyTracker.areOnlyTheseKeysDown(KeyCode.S) - ) - ); filter.addEventHandler( KeyEvent.KEY_PRESSED, EventFX.KEY_PRESSED( "apply mask", e -> {e.consume(); applyMask(paintera);}, e -> modeState.get() == ModeState.Preview && - keyTracker.areOnlyTheseKeysDown(KeyCode.S) + keyTracker.areOnlyTheseKeysDown(KeyCode.ENTER) ) ); filter.addEventHandler( @@ -322,8 +302,17 @@ public void enterMode(final PainteraBaseView paintera, final ViewerPanelFX viewe activeViewer.set(viewer); setDisableOtherViewers(paintera, true); + // set allowed actions in this mode + final AllowedActionsBuilder allowedActionsBuilder = new AllowedActionsBuilder(); + allowedActionsBuilder.add(NavigationActionType.Drag, NavigationActionType.Zoom, MenuActionType.ToggleMaximizeViewer); + allowedActionsBuilder.add(NavigationActionType.Scroll, () -> { + // allow to scroll through sections, but fix the selection first if the object selection is not empty + if ((modeState.get() == ModeState.Select || modeState.get() == ModeState.Edit) && !selectedObjects.isEmpty()) + fixSelection(paintera); + return true; + }); lastAllowedActions = paintera.allowedActionsProperty().get(); - paintera.allowedActionsProperty().set(allowedActions); + paintera.allowedActionsProperty().set(allowedActionsBuilder.create()); // properly exit the mode if somebody else wants to switch it modeSwitchListener = obs -> exitMode(paintera, false); @@ -450,7 +439,6 @@ private void fixSelection(final PainteraBaseView paintera) activeSection.set(ActiveSection.Second); resetMask(); activeViewerProperty().get().requestRepaint(); - paintera.allowedActionsProperty().set(allowedActions); } else { @@ -474,7 +462,6 @@ private void editSelection(final PainteraBaseView paintera, final ActiveSection mask = sectionInfo.mask; paintera.manager().setTransform(sectionInfo.globalTransform); - paintera.allowedActionsProperty().set(allowedActionsWhenSelected); selectedObjects.clear(); selectedObjects.putAll(sectionInfo.selectedObjects); @@ -643,7 +630,6 @@ private void interpolateBetweenSections(final PainteraBaseView paintera) InvokeOnJavaFXApplicationThread.invoke(() -> { modeState.set(ModeState.Preview); - paintera.allowedActionsProperty().set(allowedActions); }); }); workerThread.start(); @@ -781,7 +767,6 @@ private void selectObject(final PainteraBaseView paintera, final double x, final } activeViewer.get().requestRepaint(); - paintera.allowedActionsProperty().set(selectedObjects.isEmpty() ? allowedActions : allowedActionsWhenSelected); } /** From ddeefa4f3f5134d9e28a4c0213f7867009deaad1 Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Fri, 7 Jun 2019 12:10:27 -0400 Subject: [PATCH 66/84] request to repaint in all viewers after object selection/deselection --- .../control/ShapeInterpolationMode.java | 39 +++++++++---------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java index 419921edf..1488d7289 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java @@ -158,8 +158,6 @@ private static final class SectionInfo private static final Predicate<UnsignedLongType> FOREGROUND_CHECK = t -> t.get() > 0; - private final ObjectProperty<ViewerPanelFX> activeViewer = new SimpleObjectProperty<>(); - private final MaskedSource<D, ?> source; private final LabelSourceState<D, ?> sourceState; private final SelectedIds selectedIds; @@ -167,6 +165,7 @@ private static final class SectionInfo private final HighlightingStreamConverter<?> converter; private final FragmentSegmentAssignment assignment; + private ViewerPanelFX activeViewer; private InvalidationListener modeSwitchListener; private AllowedActions lastAllowedActions; private long lastSelectedId; @@ -209,11 +208,6 @@ public ShapeInterpolationMode( }; } - public ObjectProperty<ViewerPanelFX> activeViewerProperty() - { - return activeViewer; - } - public ObjectProperty<ModeState> modeStateProperty() { return modeState; @@ -299,7 +293,7 @@ public void enterMode(final PainteraBaseView paintera, final ViewerPanelFX viewe return; } LOG.info("Entering shape interpolation mode"); - activeViewer.set(viewer); + activeViewer = viewer; setDisableOtherViewers(paintera, true); // set allowed actions in this mode @@ -377,8 +371,9 @@ public void exitMode(final PainteraBaseView paintera, final boolean completed) lastSelectedId = Label.INVALID; lastActiveIds = null; - activeViewer.get().requestRepaint(); - activeViewer.set(null); + for (final ViewerPanelFX viewer : getViewerPanels(paintera)) + viewer.requestRepaint(); + activeViewer = null; } public boolean isModeOn() @@ -388,7 +383,7 @@ public boolean isModeOn() private void createMask() throws MaskInUse { - final int time = activeViewer.get().getState().timepointProperty().get(); + final int time = activeViewer.getState().timepointProperty().get(); final int level = MASK_SCALE_LEVEL; final MaskInfo<UnsignedLongType> maskInfo = new MaskInfo<>(time, level, new UnsignedLongType(newLabelId)); mask = source.generateMask(maskInfo, FOREGROUND_CHECK); @@ -408,7 +403,7 @@ private void setDisableOtherViewers(final PainteraBaseView paintera, final boole { for (final ViewerPanelFX viewer : getViewerPanels(paintera)) { - if (viewer != activeViewer.get()) + if (viewer != activeViewer) { viewer.setDisable(disable); if (disable) @@ -438,7 +433,8 @@ private void fixSelection(final PainteraBaseView paintera) // let the user now select the second section activeSection.set(ActiveSection.Second); resetMask(); - activeViewerProperty().get().requestRepaint(); + for (final ViewerPanelFX viewer : getViewerPanels(paintera)) + viewer.requestRepaint(); } else { @@ -766,7 +762,8 @@ private void selectObject(final PainteraBaseView paintera, final double x, final resetMask(); } - activeViewer.get().requestRepaint(); + for (final ViewerPanelFX viewer : getViewerPanels(paintera)) + viewer.requestRepaint(); } /** @@ -778,7 +775,7 @@ private void selectObject(final PainteraBaseView paintera, final double x, final */ private Pair<Long, Interval> runFloodFillToSelect(final double x, final double y) { - final Interval affectedInterval = FloodFill2D.fillMaskAt(x, y, activeViewer.get(), mask, source, assignment, ++currentFillValue, FILL_DEPTH); + final Interval affectedInterval = FloodFill2D.fillMaskAt(x, y, activeViewer, mask, source, assignment, ++currentFillValue, FILL_DEPTH); return new ValuePair<>(currentFillValue, affectedInterval); } @@ -798,7 +795,7 @@ private long runFloodFillToDeselect(final double x, final double y) (in, out) -> out.set(in.getIntegerLong() == maskValue), new BoolType() ); - FloodFill2D.fillMaskAt(x, y, activeViewer.get(), mask, predicate, getMaskTransform(), Label.BACKGROUND, FILL_DEPTH); + FloodFill2D.fillMaskAt(x, y, activeViewer, mask, predicate, getMaskTransform(), Label.BACKGROUND, FILL_DEPTH); return maskValue; } @@ -814,7 +811,7 @@ private UnsignedLongType getMaskValue(final double x, final double y) private AffineTransform3D getMaskTransform() { final AffineTransform3D maskTransform = new AffineTransform3D(); - final int time = activeViewer.get().getState().timepointProperty().get(); + final int time = activeViewer.getState().timepointProperty().get(); final int level = MASK_SCALE_LEVEL; source.getSourceTransform(time, level, maskTransform); return maskTransform; @@ -823,7 +820,7 @@ private AffineTransform3D getMaskTransform() private AffineTransform3D getDisplayTransform() { final AffineTransform3D viewerTransform = new AffineTransform3D(); - activeViewer.get().getState().getViewerTransform(viewerTransform); + activeViewer.getState().getViewerTransform(viewerTransform); return viewerTransform; } @@ -846,7 +843,7 @@ private AffineTransform3D getMaskDisplayTransformIgnoreScaling(final int targetL if (targetLevel != maskLevel) { // scale with respect to the given mipmap level - final int time = activeViewer.get().getState().timepointProperty().get(); + final int time = activeViewer.getState().timepointProperty().get(); final Scale3D relativeScaleTransform = new Scale3D(DataSource.getRelativeScales(source, time, maskLevel, targetLevel)); maskMipmapDisplayTransform.preConcatenate(relativeScaleTransform.inverse()); } @@ -867,7 +864,7 @@ private AffineTransform3D getMaskDisplayTransformIgnoreScaling() Arrays.setAll(viewerScale, d -> Affine3DHelpers.extractScale(viewerTransform, d)); final Scale3D scalingTransform = new Scale3D(viewerScale); // neutralize mask scaling if there is any - final int time = activeViewer.get().getState().timepointProperty().get(); + final int time = activeViewer.getState().timepointProperty().get(); final int level = MASK_SCALE_LEVEL; scalingTransform.concatenate(new Scale3D(DataSource.getScale(source, time, level))); // build the resulting transform @@ -878,7 +875,7 @@ private RealPoint getSourceCoordinates(final double x, final double y) { final AffineTransform3D maskTransform = getMaskTransform(); final RealPoint sourcePos = new RealPoint(maskTransform.numDimensions()); - activeViewer.get().displayToSourceCoordinates(x, y, maskTransform, sourcePos); + activeViewer.displayToSourceCoordinates(x, y, maskTransform, sourcePos); return sourcePos; } From 1fccf84b6a40bf88fc16632cb879b81b154375dd Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Fri, 7 Jun 2019 13:19:06 -0400 Subject: [PATCH 67/84] allow to immediately switch between editing first and second section --- .../control/ShapeInterpolationMode.java | 40 ++++++++++++++----- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java index 1488d7289..9457babd1 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java @@ -255,7 +255,7 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke EventFX.KEY_PRESSED( "edit selection 1", e -> {e.consume(); editSelection(paintera, ActiveSection.First);}, - e -> modeState.get() == ModeState.Preview && + e -> (modeState.get() == ModeState.Interpolate || modeState.get() == ModeState.Preview || modeState.get() == ModeState.Edit) && keyTracker.areOnlyTheseKeysDown(KeyCode.DIGIT1) ) ); @@ -264,7 +264,7 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke EventFX.KEY_PRESSED( "edit selection 2", e -> {e.consume(); editSelection(paintera, ActiveSection.Second);}, - e -> modeState.get() == ModeState.Preview && + e -> (modeState.get() == ModeState.Interpolate || modeState.get() == ModeState.Preview || modeState.get() == ModeState.Edit) && keyTracker.areOnlyTheseKeysDown(KeyCode.DIGIT2) ) ); @@ -302,7 +302,10 @@ public void enterMode(final PainteraBaseView paintera, final ViewerPanelFX viewe allowedActionsBuilder.add(NavigationActionType.Scroll, () -> { // allow to scroll through sections, but fix the selection first if the object selection is not empty if ((modeState.get() == ModeState.Select || modeState.get() == ModeState.Edit) && !selectedObjects.isEmpty()) + { fixSelection(paintera); + advanceMode(paintera); + } return true; }); lastAllowedActions = paintera.allowedActionsProperty().get(); @@ -334,15 +337,7 @@ public void exitMode(final PainteraBaseView paintera, final boolean completed) if (!completed) // extra cleanup if the mode is aborted { - if (workerThread != null) - { - workerThread.interrupt(); - try { - workerThread.join(); - } catch (final InterruptedException e) { - e.printStackTrace(); - } - } + interruptInterpolation(); resetMask(); } @@ -427,7 +422,10 @@ private void fixSelection(final PainteraBaseView paintera) LOG.debug("Fix selection"); sectionInfoPropertyToSet.set(createSectionInfo(paintera)); selectedObjects.clear(); + } + private void advanceMode(final PainteraBaseView paintera) + { if (modeState.get() != ModeState.Edit && sectionInfo2.get() == null) { // let the user now select the second section @@ -446,6 +444,13 @@ private void fixSelection(final PainteraBaseView paintera) private void editSelection(final PainteraBaseView paintera, final ActiveSection section) { + interruptInterpolation(); + + if (activeSection.get() == section) + return; + else if (activeSection.get() != null) + fixSelection(paintera); + final ObjectProperty<SectionInfo> sectionInfoPropertyToEdit = section == ActiveSection.First ? sectionInfo1 : sectionInfo2; final SectionInfo sectionInfo = sectionInfoPropertyToEdit.get(); @@ -631,6 +636,19 @@ private void interpolateBetweenSections(final PainteraBaseView paintera) workerThread.start(); } + private void interruptInterpolation() + { + if (workerThread != null) + { + workerThread.interrupt(); + try { + workerThread.join(); + } catch (final InterruptedException e) { + e.printStackTrace(); + } + } + } + private static <R extends RealType<R> & NativeType<R>, B extends BooleanType<B>> void computeSignedDistanceTransform( final RandomAccessibleInterval<B> mask, final RandomAccessibleInterval<R> target, From 96ed205684179228a9cac0bd6645c80acfba3c7f Mon Sep 17 00:00:00 2001 From: Igor Pisarev <igorpisarev@users.noreply.github.com> Date: Fri, 7 Jun 2019 13:33:30 -0400 Subject: [PATCH 68/84] update shape interpolation description and shortcuts --- README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index ddd680b36..732e54c7a 100644 --- a/README.md +++ b/README.md @@ -162,9 +162,10 @@ Usage: Paintera [-h] [--default-to-temp-directory] [--print-error-codes] | `F` + left click | 2D Flood-fill in current viewer plane with id that was last toggled active (if any) | | `Shift` + `F` + left click | Flood-fill with id that was last toggled active (if any) | | `N` | Select new, previously unused id | -| `S` | Enter/advance shape interpolation mode | +| `S` | Enter shape interpolation mode | | `1` / `2` | Edit first/second section when previewing interpolated shape | -| `ESC` | Exit shape interpolation mode | +| `Enter` | Commit interpolated shape into canvas | +| `Esc` | Abort shape interpolation mode | | `Ctrl` + `C` | Show dialog to commit canvas and/or assignments | | `C` | Increment ARGB stream seed by one | | `Shift` + `C` | Decrement ARGB stream seed by one | @@ -179,11 +180,11 @@ Usage: Paintera [-h] [--default-to-temp-directory] [--print-error-codes] ### Shape interpolation mode -The mode is activated by pressing the `S` key when the current source is a label source. Then, you can select the objects in the first section by left/right clicking and hit `S` to fix the selection and switch to the second section (scrolling through sections works only when there are no selected objects). +The mode is activated by pressing the `S` key when the current source is a label source. Then, you can select the objects in the sections by left/right clicking (scrolling automatically fixes the selection in the current section). -When you're done with selecting the objects in the second section, hit `S` again to interpolate between the sections. The interpolated preview will be displayed, where you can scroll through the sections and make sure that the interpolated shape is correct. If something is not right, you can edit the selection in the first or second section by pressing `1` or `2`, and then press `S` to update the interpolated shape. When the desired result is reached, hit `S` again to initiate committing the results into the canvas using a new label ID and return back to normal mode. +When you're done with selecting the objects in the second section and initiate scrolling, the preview of the interpolated shape will be displayed. If something is not right, you can edit the selection in the first or second section by pressing `1` or `2`, which will update the preview. When the desired result is reached, hit `Enter` to commit the results into the canvas and return back to normal mode. -While in the shape interpolation mode, at any point in time you can hit `ESC` to discard the current state and exit the mode. +While in the shape interpolation mode, at any point in time you can hit `Esc` to discard the current state and exit the mode. ## Data From fbbbc6a02d5ed1b9ccc3ba46ad68ecb22e93a72a Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Fri, 7 Jun 2019 16:00:41 -0400 Subject: [PATCH 69/84] optimize number of 2d flood-fill operations in shape interpolation mode --- .../control/ShapeInterpolationMode.java | 39 ++++++++++++------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java index 9457babd1..030454914 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java @@ -747,30 +747,43 @@ private void selectObject(final PainteraBaseView paintera, final double x, final return; final boolean wasSelected = FOREGROUND_CHECK.test(maskValue); - final int numSelectedObjects = selectedObjects.size(); - - LOG.debug("Object was clicked: deactivateOthers={}, wasSelected={}, numSelectedObjects", deactivateOthers, wasSelected, numSelectedObjects); + LOG.debug("Object was clicked: deactivateOthers={}, wasSelected={}", deactivateOthers, wasSelected); if (deactivateOthers) { + // If the clicked object is not selected, deselect all other objects and select the clicked object. + // If the clicked object is the only selected object, toggle it. + // If the clicked object is selected along with some other objects, deselect the others and keep the clicked one selected. + final boolean keepClickedObjectSelected = wasSelected && selectedObjects.size() > 1; for (final TLongObjectIterator<SelectedObjectInfo> it = selectedObjects.iterator(); it.hasNext();) { it.advance(); final double[] deselectDisplayPos = getDisplayCoordinates(it.value().sourceClickPosition); - runFloodFillToDeselect(deselectDisplayPos[0], deselectDisplayPos[1]); + if (!keepClickedObjectSelected || !getMaskValue(deselectDisplayPos[0], deselectDisplayPos[1]).valueEquals(maskValue)) + { + runFloodFillToDeselect(deselectDisplayPos[0], deselectDisplayPos[1]); + it.remove(); + } + } + if (!wasSelected) + { + final Pair<Long, Interval> fillValueAndInterval = runFloodFillToSelect(x, y); + selectedObjects.put(fillValueAndInterval.getA(), new SelectedObjectInfo(getSourceCoordinates(x, y), fillValueAndInterval.getB())); } - selectedObjects.clear(); - } - - if (!wasSelected || (deactivateOthers && numSelectedObjects > 1)) - { - final Pair<Long, Interval> fillValueAndInterval = runFloodFillToSelect(x, y); - selectedObjects.put(fillValueAndInterval.getA(), new SelectedObjectInfo(getSourceCoordinates(x, y), fillValueAndInterval.getB())); } else { - final long oldFillValue = runFloodFillToDeselect(x, y); - selectedObjects.remove(oldFillValue); + // Simply toggle the clicked object + if (!wasSelected) + { + final Pair<Long, Interval> fillValueAndInterval = runFloodFillToSelect(x, y); + selectedObjects.put(fillValueAndInterval.getA(), new SelectedObjectInfo(getSourceCoordinates(x, y), fillValueAndInterval.getB())); + } + else + { + final long oldFillValue = runFloodFillToDeselect(x, y); + selectedObjects.remove(oldFillValue); + } } // free the mask if there are no selected objects From 064704ef715eeae78c28b58300b579b8b89b71ae Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Fri, 7 Jun 2019 16:17:29 -0400 Subject: [PATCH 70/84] floodfill 2d bugfix: check if inside clicked object in adjacent sections When 2D flood-filling in an orthogonal view, there is a special case implementation that runs a genuine 2D flood-fill algorithm on a number of sections around the clicked one depending on the specified depth. If the depth is thicker than 1 section, it would run the flood-filling in adjacent sections starting from the same seed point. However, if this location in the adjacent section happens to be outside the clicked object, the outside value is used in the predicate which leads to filling the entire section. --- .../paintera/control/paint/FloodFill2D.java | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) 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 b2d8f6340..e6ecb9e33 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 @@ -308,22 +308,27 @@ public static <T extends IntegerType<T>> Interval fillMaskAt( extendedFilter, fillNormalAxisInLabelCoordinateSystem, i - ); + ); final MixedTransformView<UnsignedLongType> relevantAccessTracker = Views.hyperSlice( accessTracker, fillNormalAxisInLabelCoordinateSystem, i - ); - - FloodFill.fill( - relevantBackgroundSlice, - relevantAccessTracker, - new Point(seed2D), - new UnsignedLongType(fillValue), - new DiamondShape(1) - ); - Arrays.setAll(min, d -> Math.min(accessTracker.getMin()[d], min[d])); - Arrays.setAll(max, d -> Math.max(accessTracker.getMax()[d], max[d])); + ); + + final RandomAccess<BoolType> filterRandomAccess = relevantBackgroundSlice.randomAccess(); + filterRandomAccess.setPosition(seed2D); + if (filterRandomAccess.get().get()) + { + FloodFill.fill( + relevantBackgroundSlice, + relevantAccessTracker, + new Point(seed2D), + new UnsignedLongType(fillValue), + new DiamondShape(1) + ); + Arrays.setAll(min, d -> Math.min(accessTracker.getMin()[d], min[d])); + Arrays.setAll(max, d -> Math.max(accessTracker.getMax()[d], max[d])); + } } affectedInterval = new FinalInterval(min, max); } From c2edfaad51f260be1b74ceb29de5c9805b833da0 Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Fri, 7 Jun 2019 16:33:29 -0400 Subject: [PATCH 71/84] pick fill depth based on orientation to optimize fill in orthogonal view --- .../control/ShapeInterpolationMode.java | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java index 030454914..b1d06be31 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java @@ -16,6 +16,7 @@ import org.janelia.saalfeldlab.paintera.control.actions.NavigationActionType; import org.janelia.saalfeldlab.paintera.control.assignment.FragmentSegmentAssignment; import org.janelia.saalfeldlab.paintera.control.paint.FloodFill2D; +import org.janelia.saalfeldlab.paintera.control.paint.PaintUtils; import org.janelia.saalfeldlab.paintera.control.selection.SelectedIds; import org.janelia.saalfeldlab.paintera.data.DataSource; import org.janelia.saalfeldlab.paintera.data.PredicateDataSource.PredicateConverter; @@ -150,6 +151,8 @@ private static final class SectionInfo private static final double FILL_DEPTH = 2.0; + private static final double FILL_DEPTH_ORTHOGONAL = 1.0; + private static final int MASK_SCALE_LEVEL = 0; private static final int SHAPE_INTERPOLATION_SCALE_LEVEL = MASK_SCALE_LEVEL; @@ -806,8 +809,11 @@ private void selectObject(final PainteraBaseView paintera, final double x, final */ private Pair<Long, Interval> runFloodFillToSelect(final double x, final double y) { - final Interval affectedInterval = FloodFill2D.fillMaskAt(x, y, activeViewer, mask, source, assignment, ++currentFillValue, FILL_DEPTH); - return new ValuePair<>(currentFillValue, affectedInterval); + final long fillValue = ++currentFillValue; + final double fillDepth = determineFillDepth(); + LOG.debug("Flood-filling to select object: fill value={}, depth={}", fillValue, fillDepth); + final Interval affectedInterval = FloodFill2D.fillMaskAt(x, y, activeViewer, mask, source, assignment, fillValue, fillDepth); + return new ValuePair<>(fillValue, affectedInterval); } /** @@ -826,10 +832,18 @@ private long runFloodFillToDeselect(final double x, final double y) (in, out) -> out.set(in.getIntegerLong() == maskValue), new BoolType() ); - FloodFill2D.fillMaskAt(x, y, activeViewer, mask, predicate, getMaskTransform(), Label.BACKGROUND, FILL_DEPTH); + final double fillDepth = determineFillDepth(); + LOG.debug("Flood-filling to deselect object: old value={}, depth={}", maskValue, fillDepth); + FloodFill2D.fillMaskAt(x, y, activeViewer, mask, predicate, getMaskTransform(), Label.BACKGROUND, determineFillDepth()); return maskValue; } + private double determineFillDepth() + { + final int normalAxis = PaintUtils.labelAxisCorrespondingToViewerAxis(getMaskTransform(), getDisplayTransform(), 2); + return normalAxis < 0 ? FILL_DEPTH : FILL_DEPTH_ORTHOGONAL; + } + private UnsignedLongType getMaskValue(final double x, final double y) { final RealPoint sourcePos = getSourceCoordinates(x, y); From b8af372ee7104337cb8fb574416d403cd464d63b Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Fri, 7 Jun 2019 17:09:46 -0400 Subject: [PATCH 72/84] run restricted 3d flood-fill when requested to fill in 2d with depth > 1 --- .../paintera/control/paint/FloodFill2D.java | 70 +++++++++++-------- 1 file changed, 41 insertions(+), 29 deletions(-) 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 e6ecb9e33..dd3f26bf0 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 @@ -41,6 +41,7 @@ import net.imglib2.type.numeric.IntegerType; import net.imglib2.type.numeric.integer.UnsignedLongType; import net.imglib2.util.AccessBoxRandomAccessibleOnGet; +import net.imglib2.util.Intervals; import net.imglib2.view.MixedTransformView; import net.imglib2.view.Views; @@ -270,8 +271,6 @@ public static <T extends IntegerType<T>> Interval fillMaskAt( Views.extendValue(mask.mask, new UnsignedLongType(fillValue))); accessTracker.initAccessBox(); - final Interval affectedInterval; - if (fillNormalAxisInLabelCoordinateSystem < 0) { FloodFillTransformedPlane.fill( @@ -285,7 +284,6 @@ public static <T extends IntegerType<T>> Interval fillMaskAt( new RealPoint(x, y, 0), fillValue ); - affectedInterval = new FinalInterval(accessTracker.getMin(), accessTracker.getMax()); } else { @@ -295,44 +293,58 @@ public static <T extends IntegerType<T>> Interval fillMaskAt( ); final long slicePos = Math.round(pos.getDoublePosition(fillNormalAxisInLabelCoordinateSystem)); final long numSlices = Math.max((long) Math.ceil(fillDepth) - 1, 0); - final long[] seed2D = { - Math.round(pos.getDoublePosition(fillNormalAxisInLabelCoordinateSystem == 0 ? 1 : 0)), - Math.round(pos.getDoublePosition(fillNormalAxisInLabelCoordinateSystem != 2 ? 2 : 1)) - }; - accessTracker.initAccessBox(); - final long[] min = {Long.MAX_VALUE, Long.MAX_VALUE, Long.MAX_VALUE}; - final long[] max = {Long.MIN_VALUE, Long.MIN_VALUE, Long.MIN_VALUE}; - for (long i = slicePos - numSlices; i <= slicePos + numSlices; ++i) + if (numSlices == 0) { + // fill only within the given slice, run 2D flood-fill + final long[] seed2D = { + Math.round(pos.getDoublePosition(fillNormalAxisInLabelCoordinateSystem == 0 ? 1 : 0)), + Math.round(pos.getDoublePosition(fillNormalAxisInLabelCoordinateSystem != 2 ? 2 : 1)) + }; final MixedTransformView<BoolType> relevantBackgroundSlice = Views.hyperSlice( extendedFilter, fillNormalAxisInLabelCoordinateSystem, - i + slicePos ); final MixedTransformView<UnsignedLongType> relevantAccessTracker = Views.hyperSlice( accessTracker, fillNormalAxisInLabelCoordinateSystem, - i + slicePos + ); + FloodFill.fill( + relevantBackgroundSlice, + relevantAccessTracker, + new Point(seed2D), + new UnsignedLongType(fillValue), + new DiamondShape(1) + ); + } + else + { + // fill a range around the given slice, run 3D flood-fill restricted by this range + final long[] seed3D = new long[3]; + Arrays.setAll(seed3D, d -> Math.round(pos.getDoublePosition(d))); + + final long[] rangeMin = Intervals.minAsLongArray(filter); + final long[] rangeMax = Intervals.maxAsLongArray(filter); + rangeMin[fillNormalAxisInLabelCoordinateSystem] = slicePos - numSlices; + rangeMax[fillNormalAxisInLabelCoordinateSystem] = slicePos + numSlices; + final Interval range = new FinalInterval(rangeMin, rangeMax); + + final RandomAccessible<BoolType> extendedBackgroundRange = Views.extendValue( + Views.interval(extendedFilter, range), + new BoolType(false) + ); + FloodFill.fill( + extendedBackgroundRange, + accessTracker, + new Point(seed3D), + new UnsignedLongType(fillValue), + new DiamondShape(1) ); - - final RandomAccess<BoolType> filterRandomAccess = relevantBackgroundSlice.randomAccess(); - filterRandomAccess.setPosition(seed2D); - if (filterRandomAccess.get().get()) - { - FloodFill.fill( - relevantBackgroundSlice, - relevantAccessTracker, - new Point(seed2D), - new UnsignedLongType(fillValue), - new DiamondShape(1) - ); - Arrays.setAll(min, d -> Math.min(accessTracker.getMin()[d], min[d])); - Arrays.setAll(max, d -> Math.max(accessTracker.getMax()[d], max[d])); - } } - affectedInterval = new FinalInterval(min, max); } + final Interval affectedInterval = new FinalInterval(accessTracker.getMin(), accessTracker.getMax()); return affectedInterval; } From 1b4e778172db95e735ab6547d6042d4a403374d9 Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Fri, 7 Jun 2019 17:28:54 -0400 Subject: [PATCH 73/84] fix NPE when deselecting all in edit mode and switching to other section --- .../paintera/control/ShapeInterpolationMode.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java index b1d06be31..bb81004a9 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java @@ -451,8 +451,13 @@ private void editSelection(final PainteraBaseView paintera, final ActiveSection if (activeSection.get() == section) return; - else if (activeSection.get() != null) + + if (activeSection.get() != null) + { + if (selectedObjects.isEmpty()) + return; fixSelection(paintera); + } final ObjectProperty<SectionInfo> sectionInfoPropertyToEdit = section == ActiveSection.First ? sectionInfo1 : sectionInfo2; final SectionInfo sectionInfo = sectionInfoPropertyToEdit.get(); From 5ae2528f94fb07dc0d90d14ab04ae65db7351857 Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Fri, 7 Jun 2019 17:38:35 -0400 Subject: [PATCH 74/84] allow to edit #1 immediately after selecting #2 --- .../paintera/control/ShapeInterpolationMode.java | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java index bb81004a9..1bc82f27d 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java @@ -258,8 +258,7 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke EventFX.KEY_PRESSED( "edit selection 1", e -> {e.consume(); editSelection(paintera, ActiveSection.First);}, - e -> (modeState.get() == ModeState.Interpolate || modeState.get() == ModeState.Preview || modeState.get() == ModeState.Edit) && - keyTracker.areOnlyTheseKeysDown(KeyCode.DIGIT1) + e -> keyTracker.areOnlyTheseKeysDown(KeyCode.DIGIT1) ) ); filter.addEventHandler( @@ -267,8 +266,7 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke EventFX.KEY_PRESSED( "edit selection 2", e -> {e.consume(); editSelection(paintera, ActiveSection.Second);}, - e -> (modeState.get() == ModeState.Interpolate || modeState.get() == ModeState.Preview || modeState.get() == ModeState.Edit) && - keyTracker.areOnlyTheseKeysDown(KeyCode.DIGIT2) + e -> keyTracker.areOnlyTheseKeysDown(KeyCode.DIGIT2) ) ); @@ -462,6 +460,12 @@ private void editSelection(final PainteraBaseView paintera, final ActiveSection final ObjectProperty<SectionInfo> sectionInfoPropertyToEdit = section == ActiveSection.First ? sectionInfo1 : sectionInfo2; final SectionInfo sectionInfo = sectionInfoPropertyToEdit.get(); + if (sectionInfo == null) + { + advanceMode(paintera); + return; + } + resetMask(); try { source.setMask(sectionInfo.mask, FOREGROUND_CHECK); @@ -475,9 +479,10 @@ private void editSelection(final PainteraBaseView paintera, final ActiveSection selectedObjects.clear(); selectedObjects.putAll(sectionInfo.selectedObjects); + modeState.set(sectionInfo1.get() != null && sectionInfo2.get() != null ? ModeState.Edit : ModeState.Select); + sectionInfoPropertyToEdit.set(null); activeSection.set(section); - modeState.set(ModeState.Edit); } private void applyMask(final PainteraBaseView paintera) From 12ec0704439c621c2b8010142f343f38374f1fe0 Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Fri, 7 Jun 2019 17:44:38 -0400 Subject: [PATCH 75/84] remove superfluous edit state of shape interpolation mode --- .../paintera/control/ShapeInterpolationMode.java | 15 +++++++-------- .../paintera/state/LabelSourceState.java | 3 --- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java index 1bc82f27d..e4c63bef4 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java @@ -89,8 +89,7 @@ public static enum ModeState { Select, Interpolate, - Preview, - Edit + Preview } public static enum ActiveSection @@ -273,12 +272,12 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke filter.addEventHandler(MouseEvent.ANY, new MouseClickFX( "select object in current section", e -> {e.consume(); selectObject(paintera, e.getX(), e.getY(), true);}, - e -> (modeState.get() == ModeState.Select || modeState.get() == ModeState.Edit) && e.isPrimaryButtonDown() && keyTracker.noKeysActive()) + e -> modeState.get() == ModeState.Select && e.isPrimaryButtonDown() && keyTracker.noKeysActive()) .handler()); filter.addEventHandler(MouseEvent.ANY, new MouseClickFX( "toggle object in current section", e -> {e.consume(); selectObject(paintera, e.getX(), e.getY(), false);}, - e -> (modeState.get() == ModeState.Select || modeState.get() == ModeState.Edit) && + e -> modeState.get() == ModeState.Select && ((e.isSecondaryButtonDown() && keyTracker.noKeysActive()) || (e.isPrimaryButtonDown() && keyTracker.areOnlyTheseKeysDown(KeyCode.CONTROL)))) .handler()); @@ -302,7 +301,7 @@ public void enterMode(final PainteraBaseView paintera, final ViewerPanelFX viewe allowedActionsBuilder.add(NavigationActionType.Drag, NavigationActionType.Zoom, MenuActionType.ToggleMaximizeViewer); allowedActionsBuilder.add(NavigationActionType.Scroll, () -> { // allow to scroll through sections, but fix the selection first if the object selection is not empty - if ((modeState.get() == ModeState.Select || modeState.get() == ModeState.Edit) && !selectedObjects.isEmpty()) + if (modeState.get() == ModeState.Select && !selectedObjects.isEmpty()) { fixSelection(paintera); advanceMode(paintera); @@ -427,7 +426,7 @@ private void fixSelection(final PainteraBaseView paintera) private void advanceMode(final PainteraBaseView paintera) { - if (modeState.get() != ModeState.Edit && sectionInfo2.get() == null) + if (sectionInfo1.get() == null || sectionInfo2.get() == null) { // let the user now select the second section activeSection.set(ActiveSection.Second); @@ -479,10 +478,10 @@ private void editSelection(final PainteraBaseView paintera, final ActiveSection selectedObjects.clear(); selectedObjects.putAll(sectionInfo.selectedObjects); - modeState.set(sectionInfo1.get() != null && sectionInfo2.get() != null ? ModeState.Edit : ModeState.Select); - sectionInfoPropertyToEdit.set(null); activeSection.set(section); + + modeState.set(ModeState.Select); } private void applyMask(final PainteraBaseView paintera) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java index 432935abf..e90ef7cb6 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java @@ -618,9 +618,6 @@ private HBox createDisplayStatus() case Select: statusTextProperty().set("Select #" + activeSection); break; - case Edit: - statusTextProperty().set("Edit #" + activeSection); - break; case Interpolate: statusTextProperty().set("Interpolating"); break; From aa8efa81eab9ccf9a7bb7545880c4dfadd320b68 Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Fri, 7 Jun 2019 18:48:30 -0400 Subject: [PATCH 76/84] consume events when rotating using hotkeys to avoid triggering side panel --- .../org/janelia/saalfeldlab/paintera/control/Navigation.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/Navigation.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/Navigation.java index b117deb53..3dd7329f8 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/Navigation.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/Navigation.java @@ -548,6 +548,7 @@ private static final EventFX<KeyEvent> keyRotationHandler( event -> { if (allowRotations.getAsBoolean()) { + event.consume(); rotate.rotate(rotationCenterX.getAsDouble(), rotationCenterY.getAsDouble()); } }, From 6ee731a8530fbe1fd264f831186038293e1157f6 Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Mon, 17 Jun 2019 13:25:40 -0400 Subject: [PATCH 77/84] allow to commit interpolated shape without going through preview step --- .../control/ShapeInterpolationMode.java | 47 ++++++++++++++++--- 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java index e4c63bef4..b27af04a9 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java @@ -187,6 +187,7 @@ private static final class SectionInfo private final ObjectProperty<ActiveSection> activeSection = new SimpleObjectProperty<>(); private Thread workerThread; + private Runnable onInterpolationFinished; private Pair<RealRandomAccessible<UnsignedLongType>, RealRandomAccessible<VolatileUnsignedLongType>> interpolatedMaskImgs; public ShapeInterpolationMode( @@ -248,8 +249,7 @@ public EventHandler<Event> modeHandler(final PainteraBaseView paintera, final Ke EventFX.KEY_PRESSED( "apply mask", e -> {e.consume(); applyMask(paintera);}, - e -> modeState.get() == ModeState.Preview && - keyTracker.areOnlyTheseKeysDown(KeyCode.ENTER) + e -> isModeOn() && keyTracker.areOnlyTheseKeysDown(KeyCode.ENTER) ) ); filter.addEventHandler( @@ -362,6 +362,7 @@ public void exitMode(final PainteraBaseView paintera, final boolean completed) mask = null; workerThread = null; + onInterpolationFinished = null; interpolatedMaskImgs = null; lastSelectedId = Label.INVALID; lastActiveIds = null; @@ -438,6 +439,8 @@ private void advanceMode(final PainteraBaseView paintera) { // both sections are ready, run interpolation activeSection.set(null); + modeState.set(ModeState.Interpolate); + onInterpolationFinished = () -> modeState.set(ModeState.Preview); interpolateBetweenSections(paintera); } } @@ -486,6 +489,30 @@ private void editSelection(final PainteraBaseView paintera, final ActiveSection private void applyMask(final PainteraBaseView paintera) { + if (modeState.get() == ModeState.Select) + { + final boolean firstSectionReady = sectionInfo1.get() != null || (activeSection.get() == ActiveSection.First && !selectedObjects.isEmpty()); + final boolean secondSectionReady = sectionInfo2.get() != null || (activeSection.get() == ActiveSection.Second && !selectedObjects.isEmpty()); + if (!firstSectionReady || !secondSectionReady) + return; + + fixSelection(paintera); + advanceMode(paintera); + } + + if (modeState.get() == ModeState.Interpolate) + { + // wait until the interpolation is done + try { + workerThread.join(); + } catch (final InterruptedException e) { + e.printStackTrace(); + }; + runOnInterpolationFinished(); + } + + assert modeState.get() == ModeState.Preview; + final Interval sectionsUnionSourceInterval = Intervals.union( sectionInfo1.get().sourceBoundingBox, sectionInfo2.get().sourceBoundingBox @@ -551,8 +578,6 @@ private SectionInfo createSectionInfo(final PainteraBaseView paintera) @SuppressWarnings("unchecked") private void interpolateBetweenSections(final PainteraBaseView paintera) { - modeState.set(ModeState.Interpolate); - workerThread = new Thread(() -> { final SectionInfo[] sectionInfoPair = {sectionInfo1.get(), sectionInfo2.get()}; @@ -641,13 +666,20 @@ private void interpolateBetweenSections(final PainteraBaseView paintera) LOG.error("Label source already has an active mask"); } - InvokeOnJavaFXApplicationThread.invoke(() -> { - modeState.set(ModeState.Preview); - }); + InvokeOnJavaFXApplicationThread.invoke(this::runOnInterpolationFinished); }); workerThread.start(); } + private void runOnInterpolationFinished() + { + if (onInterpolationFinished != null) + { + onInterpolationFinished.run(); + onInterpolationFinished = null; + } + } + private void interruptInterpolation() { if (workerThread != null) @@ -659,6 +691,7 @@ private void interruptInterpolation() e.printStackTrace(); } } + onInterpolationFinished = null; } private static <R extends RealType<R> & NativeType<R>, B extends BooleanType<B>> void computeSignedDistanceTransform( From 1bb64aff5d062ec1ea616047b7f3567547f0f9ff Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Mon, 17 Jun 2019 13:55:39 -0400 Subject: [PATCH 78/84] allow to interrupt 3d floodfill operation by pressing Esc --- .../paintera/state/LabelSourceState.java | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java index e90ef7cb6..9e4fa27f9 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java @@ -469,7 +469,27 @@ public EventHandler<Event> stateSpecificGlobalEventHandler(final PainteraBaseVie final DelegateEventHandlers.AnyHandler handler = DelegateEventHandlers.handleAny(); handler.addEventHandler( KeyEvent.KEY_PRESSED, - EventFX.KEY_PRESSED("refresh meshes", e -> {LOG.debug("Key event triggered refresh meshes"); refreshMeshes();}, e -> keyTracker.areOnlyTheseKeysDown(KeyCode.R))); + EventFX.KEY_PRESSED( + "refresh meshes", + e -> { + e.consume(); + LOG.debug("Key event triggered refresh meshes"); + refreshMeshes(); + }, + e -> keyTracker.areOnlyTheseKeysDown(KeyCode.R) + )); + handler.addEventHandler( + KeyEvent.KEY_PRESSED, + EventFX.KEY_PRESSED( + "cancel 3d floodfill", + e -> { + e.consume(); + final FloodFillState state = floodFillState.get(); + if (state != null && state.interrupt != null) + state.interrupt.run(); + }, + e -> floodFillState.get() != null && keyTracker.areOnlyTheseKeysDown(KeyCode.ESCAPE) + )); return handler; } From 659e57205d52128132b822a11e228a4217a98d59 Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Mon, 17 Jun 2019 14:41:01 -0400 Subject: [PATCH 79/84] request repaint in all viewers when running 2d/3d floodfill --- .../control/ShapeInterpolationMode.java | 29 +++++++------------ .../state/LabelSourceStatePaintHandler.java | 6 ++-- 2 files changed, 14 insertions(+), 21 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java index b27af04a9..9928ec12a 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java @@ -367,8 +367,7 @@ public void exitMode(final PainteraBaseView paintera, final boolean completed) lastSelectedId = Label.INVALID; lastActiveIds = null; - for (final ViewerPanelFX viewer : getViewerPanels(paintera)) - viewer.requestRepaint(); + paintera.orthogonalViews().requestRepaint(); activeViewer = null; } @@ -397,7 +396,13 @@ private void resetMask() private void setDisableOtherViewers(final PainteraBaseView paintera, final boolean disable) { - for (final ViewerPanelFX viewer : getViewerPanels(paintera)) + final ViewerPanelFX[] viewers = { + paintera.orthogonalViews().topLeft().viewer(), + paintera.orthogonalViews().topRight().viewer(), + paintera.orthogonalViews().bottomLeft().viewer() + }; + + for (final ViewerPanelFX viewer : viewers) { if (viewer != activeViewer) { @@ -432,8 +437,7 @@ private void advanceMode(final PainteraBaseView paintera) // let the user now select the second section activeSection.set(ActiveSection.Second); resetMask(); - for (final ViewerPanelFX viewer : getViewerPanels(paintera)) - viewer.requestRepaint(); + paintera.orthogonalViews().requestRepaint(); } else { @@ -658,8 +662,7 @@ private void interpolateBetweenSections(final PainteraBaseView paintera) interpolatedMaskImgs = new ValuePair<>(interpolatedShapeMask, volatileInterpolatedShapeMask); } - for (final ViewerPanelFX viewer : getViewerPanels(paintera)) - viewer.requestRepaint(); + paintera.orthogonalViews().requestRepaint(); } catch (final MaskInUse e) { @@ -838,8 +841,7 @@ private void selectObject(final PainteraBaseView paintera, final double x, final resetMask(); } - for (final ViewerPanelFX viewer : getViewerPanels(paintera)) - viewer.requestRepaint(); + paintera.orthogonalViews().requestRepaint(); } /** @@ -974,13 +976,4 @@ private double[] getDisplayCoordinates(final RealPoint sourcePos) assert Util.isApproxEqual(displayPos.getDoublePosition(2), 0.0, 1e-10); return new double[] {displayPos.getDoublePosition(0), displayPos.getDoublePosition(1)}; } - - private static ViewerPanelFX[] getViewerPanels(final PainteraBaseView paintera) - { - return new ViewerPanelFX[] { - paintera.orthogonalViews().topLeft().viewer(), - paintera.orthogonalViews().topRight().viewer(), - paintera.orthogonalViews().bottomLeft().viewer() - }; - } } diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStatePaintHandler.java b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStatePaintHandler.java index 43d33b850..4de1b5502 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStatePaintHandler.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStatePaintHandler.java @@ -113,14 +113,14 @@ private EventHandler<Event> makeHandler(final PainteraBaseView paintera, final K painters.put(t, paint2D); - final FloodFill fill = new FloodFill(t, sourceInfo, t::requestRepaint); - final FloodFill2D fill2D = new FloodFill2D(t, sourceInfo, t::requestRepaint); + final FloodFill fill = new FloodFill(t, sourceInfo, paintera.orthogonalViews()::requestRepaint); + final FloodFill2D fill2D = new FloodFill2D(t, sourceInfo, paintera.orthogonalViews()::requestRepaint); fill2D.fillDepthProperty().bindBidirectional(this.brushDepth); final Fill2DOverlay fill2DOverlay = new Fill2DOverlay(t); fill2DOverlay.brushDepthProperty().bindBidirectional(this.brushDepth); final FillOverlay fillOverlay = new FillOverlay(t); - final RestrictPainting restrictor = new RestrictPainting(t, sourceInfo, t::requestRepaint); + final RestrictPainting restrictor = new RestrictPainting(t, sourceInfo, paintera.orthogonalViews()::requestRepaint); // brush handler.addEventHandler(KeyEvent.KEY_PRESSED, EventFX.KEY_PRESSED( From fe9b35f68b93c4d08f2b64c3221d2f84de7de563 Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Mon, 17 Jun 2019 15:44:18 -0400 Subject: [PATCH 80/84] make sidebar wider and adjust mesh settings layout to fit all nodes --- .../paintera/BorderPaneWithStatusBars.java | 2 +- .../paintera/ui/source/mesh/MeshPane.java | 52 +++++++++++-------- 2 files changed, 32 insertions(+), 22 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/BorderPaneWithStatusBars.java b/src/main/java/org/janelia/saalfeldlab/paintera/BorderPaneWithStatusBars.java index 4e91ee554..b10ed4ccd 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/BorderPaneWithStatusBars.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/BorderPaneWithStatusBars.java @@ -299,7 +299,7 @@ else if (newv.nameProperty().get() != null) this.sideBar.setHbarPolicy(ScrollBarPolicy.NEVER); this.sideBar.setVbarPolicy(ScrollBarPolicy.AS_NEEDED); this.sideBar.setVisible(true); - this.sideBar.prefWidthProperty().set(250); + this.sideBar.prefWidthProperty().set(280); sourceTabs.widthProperty().bind(sideBar.prefWidthProperty()); settingsContents.prefWidthProperty().bind(sideBar.prefWidthProperty()); diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/ui/source/mesh/MeshPane.java b/src/main/java/org/janelia/saalfeldlab/paintera/ui/source/mesh/MeshPane.java index d4243e97f..d26963cfb 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/ui/source/mesh/MeshPane.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/ui/source/mesh/MeshPane.java @@ -13,6 +13,7 @@ import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.collections.ObservableMap; +import javafx.geometry.HPos; import javafx.scene.Node; import javafx.scene.control.Button; import javafx.scene.control.CheckBox; @@ -264,66 +265,75 @@ public static int populateGridWithMeshSettings( { int row = initialRow; - final double textFieldWidth = 95; + final double textFieldWidth = 55; + final double choiceWidth = 95; - contents.add(labelWithToolTip("Opacity "), 0, row); + contents.add(labelWithToolTip("Opacity"), 0, row); contents.add(opacitySlider.slider(), 1, row); - contents.add(opacitySlider.textField(), 2, row); + GridPane.setColumnSpan(opacitySlider.slider(), 2); + contents.add(opacitySlider.textField(), 3, row); opacitySlider.slider().setShowTickLabels(true); opacitySlider.slider().setTooltip(new Tooltip("Mesh opacity.")); - opacitySlider.textField().setMinWidth(textFieldWidth); - opacitySlider.textField().setMaxWidth(textFieldWidth); + opacitySlider.textField().setPrefWidth(textFieldWidth); GridPane.setHgrow(opacitySlider.slider(), Priority.ALWAYS); ++row; contents.add(labelWithToolTip("Scale"), 0, row); contents.add(scaleSlider.slider(), 1, row); - contents.add(scaleSlider.textField(), 2, row); + GridPane.setColumnSpan(scaleSlider.slider(), 2); + contents.add(scaleSlider.textField(), 3, row); scaleSlider.slider().setShowTickLabels(true); scaleSlider.slider().setTooltip(new Tooltip("Scale level.")); - scaleSlider.textField().setMinWidth(textFieldWidth); - scaleSlider.textField().setMaxWidth(textFieldWidth); + scaleSlider.textField().setPrefWidth(textFieldWidth); GridPane.setHgrow(scaleSlider.slider(), Priority.ALWAYS); ++row; contents.add(labelWithToolTip("Lambda"), 0, row); contents.add(smoothingLambdaSlider.slider(), 1, row); - contents.add(smoothingLambdaSlider.textField(), 2, row); + GridPane.setColumnSpan(smoothingLambdaSlider.slider(), 2); + contents.add(smoothingLambdaSlider.textField(), 3, row); smoothingLambdaSlider.slider().setShowTickLabels(true); smoothingLambdaSlider.slider().setTooltip(new Tooltip("Smoothing lambda.")); - smoothingLambdaSlider.textField().setMinWidth(textFieldWidth); - smoothingLambdaSlider.textField().setMaxWidth(textFieldWidth); + smoothingLambdaSlider.textField().setPrefWidth(textFieldWidth); GridPane.setHgrow(smoothingLambdaSlider.slider(), Priority.ALWAYS); ++row; contents.add(labelWithToolTip("Iterations"), 0, row); contents.add(smoothingIterationsSlider.slider(), 1, row); - contents.add(smoothingIterationsSlider.textField(), 2, row); + GridPane.setColumnSpan(smoothingIterationsSlider.slider(), 2); + contents.add(smoothingIterationsSlider.textField(), 3, row); smoothingIterationsSlider.slider().setShowTickLabels(true); smoothingIterationsSlider.slider().setTooltip(new Tooltip("Smoothing iterations.")); - smoothingIterationsSlider.textField().setMinWidth(textFieldWidth); - smoothingIterationsSlider.textField().setMaxWidth(textFieldWidth); + smoothingIterationsSlider.textField().setPrefWidth(textFieldWidth); GridPane.setHgrow(smoothingIterationsSlider.slider(), Priority.ALWAYS); ++row; contents.add(labelWithToolTip("Inflate"), 0, row); contents.add(inflateSlider.slider(), 1, row); - contents.add(inflateSlider.textField(), 2, row); + GridPane.setColumnSpan(inflateSlider.slider(), 2); + contents.add(inflateSlider.textField(), 3, row); inflateSlider.slider().setShowTickLabels(true); inflateSlider.slider().setTooltip(new Tooltip("Inflate meshes by factor")); - inflateSlider.textField().setMinWidth(textFieldWidth); - inflateSlider.textField().setMaxWidth(textFieldWidth); + inflateSlider.textField().setPrefWidth(textFieldWidth); GridPane.setHgrow(inflateSlider.slider(), Priority.ALWAYS); ++row; - contents.add(labelWithToolTip("Draw Mode"), 0, row); + final Node drawModeLabel = labelWithToolTip("Draw Mode"); + contents.add(drawModeLabel, 0, row); + GridPane.setColumnSpan(drawModeLabel, 2); contents.add(drawModeChoice, 2, row); - drawModeChoice.setMaxWidth(textFieldWidth); + GridPane.setColumnSpan(drawModeChoice, 2); + GridPane.setHalignment(drawModeChoice, HPos.RIGHT); + drawModeChoice.setPrefWidth(choiceWidth); ++row; - contents.add(labelWithToolTip("CullFace "), 0, row); + final Node cullFaceLabel = labelWithToolTip("Cull Face"); + contents.add(cullFaceLabel, 0, row); + GridPane.setColumnSpan(cullFaceLabel, 2); contents.add(cullFaceChoice, 2, row); - cullFaceChoice.setMaxWidth(textFieldWidth); + GridPane.setColumnSpan(cullFaceChoice, 2); + GridPane.setHalignment(cullFaceChoice, HPos.RIGHT); + cullFaceChoice.setPrefWidth(choiceWidth); ++row; return row; From 9ee43c4071170499b7a431f33224f6f1596a68f4 Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Tue, 18 Jun 2019 14:48:25 -0400 Subject: [PATCH 81/84] do not make an extra copy when computing distance transform on sections --- .../paintera/control/ShapeInterpolationMode.java | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java index 9928ec12a..4cf7cca0c 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java @@ -591,7 +591,7 @@ private void interpolateBetweenSections(final PainteraBaseView paintera) sectionInfoPair[1].sourceBoundingBox ); - final Interval[] displaySectionIntervalPair = new Interval[2]; + // get the two sections as 2D images final RandomAccessibleInterval<UnsignedLongType>[] sectionPair = new RandomAccessibleInterval[2]; for (int i = 0; i < 2; ++i) { @@ -602,13 +602,7 @@ private void interpolateBetweenSections(final PainteraBaseView paintera) affectedUnionSourceInterval, sectionInfoPair[i].selectedObjects ); - final RandomAccessibleInterval<UnsignedLongType> section = getTransformedMaskSection(newSectionInfo); - displaySectionIntervalPair[i] = new FinalInterval(section); - sectionPair[i] = new ArrayImgFactory<>(new UnsignedLongType()).create(section); - final Cursor<UnsignedLongType> srcCursor = Views.flatIterable(section).cursor(); - final Cursor<UnsignedLongType> dstCursor = Views.flatIterable(sectionPair[i]).cursor(); - while (dstCursor.hasNext() || srcCursor.hasNext()) - dstCursor.next().set(srcCursor.next()); + sectionPair[i] = getTransformedMaskSection(newSectionInfo); } // compute distance transform on both sections @@ -619,13 +613,13 @@ private void interpolateBetweenSections(final PainteraBaseView paintera) return; distanceTransformPair[i] = new ArrayImgFactory<>(new FloatType()).create(sectionPair[i]); final RandomAccessibleInterval<BoolType> binarySection = Converters.convert(sectionPair[i], new PredicateConverter<>(FOREGROUND_CHECK), new BoolType()); - computeSignedDistanceTransform(binarySection, distanceTransformPair[i], DISTANCE_TYPE.EUCLIDIAN); + computeSignedDistanceTransform(Views.zeroMin(binarySection), distanceTransformPair[i], DISTANCE_TYPE.EUCLIDIAN); } final double distanceBetweenSections = computeDistanceBetweenSections(sectionInfoPair[0], sectionInfoPair[1]); final AffineTransform3D transformToSource = new AffineTransform3D(); transformToSource - .preConcatenate(new Translation3D(displaySectionIntervalPair[0].min(0), displaySectionIntervalPair[0].min(1), 0)) + .preConcatenate(new Translation3D(sectionPair[0].min(0), sectionPair[0].min(1), 0)) .preConcatenate(sectionInfoPair[0].sourceToDisplayTransform.inverse()); final RealRandomAccessible<UnsignedLongType> interpolatedShapeMask = getInterpolatedDistanceTransformMask( From 9bdd3581c686d83e129cbef205b38df09ec65be4 Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Tue, 18 Jun 2019 14:51:09 -0400 Subject: [PATCH 82/84] narrow bounding box of selected shape before computing distance transform --- .../control/ShapeInterpolationMode.java | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java index 4cf7cca0c..b86bc0c2e 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java @@ -605,12 +605,44 @@ private void interpolateBetweenSections(final PainteraBaseView paintera) sectionPair[i] = getTransformedMaskSection(newSectionInfo); } + // Narrow the bounding box of the two sections in the display space. + // The initial bounding box may be larger because of transforming the source bounding box into the display space and then taking the bounding box of that. + final Interval[] boundingBoxPair = new Interval[2]; + for (int i = 0; i < 2; ++i) + { + if (Thread.currentThread().isInterrupted()) + return; + + final long[] min = new long[2], max = new long[2], position = new long[2]; + Arrays.fill(min, Long.MAX_VALUE); + Arrays.fill(max, Long.MIN_VALUE); + final Cursor<UnsignedLongType> cursor = Views.iterable(sectionPair[i]).localizingCursor(); + while (cursor.hasNext()) + { + if (FOREGROUND_CHECK.test(cursor.next())) + { + cursor.localize(position); + for (int d = 0; d < position.length; ++d) + { + min[d] = Math.min(min[d], position[d]); + max[d] = Math.max(max[d], position[d]); + } + } + } + boundingBoxPair[i] = new FinalInterval(min, max); + } + final Interval boundingBox = Intervals.union(boundingBoxPair[0], boundingBoxPair[1]); + LOG.debug("Narrowed the bounding box of the selected shape in both sections from {} to {}", Intervals.dimensionsAsLongArray(sectionPair[0]), Intervals.dimensionsAsLongArray(boundingBox)); + for (int i = 0; i < 2; ++i) + sectionPair[i] = Views.interval(sectionPair[i], boundingBox); + // compute distance transform on both sections final RandomAccessibleInterval<FloatType>[] distanceTransformPair = new RandomAccessibleInterval[2]; for (int i = 0; i < 2; ++i) { if (Thread.currentThread().isInterrupted()) return; + distanceTransformPair[i] = new ArrayImgFactory<>(new FloatType()).create(sectionPair[i]); final RandomAccessibleInterval<BoolType> binarySection = Converters.convert(sectionPair[i], new PredicateConverter<>(FOREGROUND_CHECK), new BoolType()); computeSignedDistanceTransform(Views.zeroMin(binarySection), distanceTransformPair[i], DISTANCE_TYPE.EUCLIDIAN); @@ -619,7 +651,7 @@ private void interpolateBetweenSections(final PainteraBaseView paintera) final double distanceBetweenSections = computeDistanceBetweenSections(sectionInfoPair[0], sectionInfoPair[1]); final AffineTransform3D transformToSource = new AffineTransform3D(); transformToSource - .preConcatenate(new Translation3D(sectionPair[0].min(0), sectionPair[0].min(1), 0)) + .preConcatenate(new Translation3D(boundingBox.min(0), boundingBox.min(1), 0)) .preConcatenate(sectionInfoPair[0].sourceToDisplayTransform.inverse()); final RealRandomAccessible<UnsignedLongType> interpolatedShapeMask = getInterpolatedDistanceTransformMask( From b6ad3e044a963e4c8045d04baed4857fc542aef5 Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Tue, 18 Jun 2019 16:43:58 -0400 Subject: [PATCH 83/84] bugfix in bounding box adjustment: use translation-invariant transforms --- .../paintera/control/ShapeInterpolationMode.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java index b86bc0c2e..b5fdd7cbb 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationMode.java @@ -592,6 +592,7 @@ private void interpolateBetweenSections(final PainteraBaseView paintera) ); // get the two sections as 2D images + final Interval[] displaySectionIntervalPair = new Interval[2]; final RandomAccessibleInterval<UnsignedLongType>[] sectionPair = new RandomAccessibleInterval[2]; for (int i = 0; i < 2; ++i) { @@ -602,7 +603,9 @@ private void interpolateBetweenSections(final PainteraBaseView paintera) affectedUnionSourceInterval, sectionInfoPair[i].selectedObjects ); - sectionPair[i] = getTransformedMaskSection(newSectionInfo); + final RandomAccessibleInterval<UnsignedLongType> section = getTransformedMaskSection(newSectionInfo); + displaySectionIntervalPair[i] = new FinalInterval(section); + sectionPair[i] = Views.zeroMin(section); } // Narrow the bounding box of the two sections in the display space. @@ -634,7 +637,7 @@ private void interpolateBetweenSections(final PainteraBaseView paintera) final Interval boundingBox = Intervals.union(boundingBoxPair[0], boundingBoxPair[1]); LOG.debug("Narrowed the bounding box of the selected shape in both sections from {} to {}", Intervals.dimensionsAsLongArray(sectionPair[0]), Intervals.dimensionsAsLongArray(boundingBox)); for (int i = 0; i < 2; ++i) - sectionPair[i] = Views.interval(sectionPair[i], boundingBox); + sectionPair[i] = Views.offsetInterval(sectionPair[i], boundingBox); // compute distance transform on both sections final RandomAccessibleInterval<FloatType>[] distanceTransformPair = new RandomAccessibleInterval[2]; @@ -645,13 +648,14 @@ private void interpolateBetweenSections(final PainteraBaseView paintera) distanceTransformPair[i] = new ArrayImgFactory<>(new FloatType()).create(sectionPair[i]); final RandomAccessibleInterval<BoolType> binarySection = Converters.convert(sectionPair[i], new PredicateConverter<>(FOREGROUND_CHECK), new BoolType()); - computeSignedDistanceTransform(Views.zeroMin(binarySection), distanceTransformPair[i], DISTANCE_TYPE.EUCLIDIAN); + computeSignedDistanceTransform(binarySection, distanceTransformPair[i], DISTANCE_TYPE.EUCLIDIAN); } final double distanceBetweenSections = computeDistanceBetweenSections(sectionInfoPair[0], sectionInfoPair[1]); final AffineTransform3D transformToSource = new AffineTransform3D(); transformToSource .preConcatenate(new Translation3D(boundingBox.min(0), boundingBox.min(1), 0)) + .preConcatenate(new Translation3D(displaySectionIntervalPair[0].min(0), displaySectionIntervalPair[0].min(1), 0)) .preConcatenate(sectionInfoPair[0].sourceToDisplayTransform.inverse()); final RealRandomAccessible<UnsignedLongType> interpolatedShapeMask = getInterpolatedDistanceTransformMask( From 32ed7cbe2d1278342d1f81bf128801673071f728 Mon Sep 17 00:00:00 2001 From: Igor Pisarev <pisarevi@janelia.hhmi.org> Date: Tue, 18 Jun 2019 16:45:26 -0400 Subject: [PATCH 84/84] show both segment and fragment ids in selected label indicator tooltip --- .../saalfeldlab/paintera/state/LabelSourceState.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java index 9e4fa27f9..7bb4c25ab 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceState.java @@ -554,7 +554,13 @@ private HBox createDisplayStatus() final Color currSelectedColor = Colors.toColor(colorStream.argb(lastSelectedLabelId)); lastSelectedLabelColorRect.setFill(currSelectedColor); lastSelectedLabelColorRect.setVisible(true); - lastSelectedLabelColorRectTooltip.setText("Selected label ID: " + lastSelectedLabelId); + + final StringBuilder activeIdText = new StringBuilder(); + final long segmentId = assignment.getSegment(lastSelectedLabelId); + if (segmentId != lastSelectedLabelId) + activeIdText.append("Segment: " + segmentId).append(". "); + activeIdText.append("Fragment: " + lastSelectedLabelId); + lastSelectedLabelColorRectTooltip.setText(activeIdText.toString()); } } else { lastSelectedLabelColorRect.setVisible(false); @@ -562,6 +568,7 @@ private HBox createDisplayStatus() }); }; selectedIds.addListener(lastSelectedIdUpdater); + assignment.addListener(lastSelectedIdUpdater); // add the same listener to the color stream (for example, the color should change when a new random seed value is set) final AbstractHighlightingARGBStream colorStream = highlightingStreamConverter().getStream();