diff --git a/.github/workflows/maven-build-all-installer.yml b/.github/workflows/maven-build-all-installer.yml index 3d352bdf6..64fbe1a29 100644 --- a/.github/workflows/maven-build-all-installer.yml +++ b/.github/workflows/maven-build-all-installer.yml @@ -19,7 +19,7 @@ jobs: build: strategy: matrix: - os: [ubuntu-latest, windows-latest, macos-latest] + os: [ubuntu-latest, windows-latest, macos-latest, macOS-ARM64] runs-on: ${{ matrix.os }} env: RELEASE_INSTALLERS: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }} @@ -50,58 +50,14 @@ jobs: java-package: jdk+fx cache: 'maven' - name: "Build with Maven" - if: matrix.os != 'macos-latest' + if: matrix.os != 'macos-latest' && matrix.os != 'macOS-ARM64' run: mvn -B clean install -DskipTests -Pbuild-installer --file pom.xml - name: "Build with Maven (macOS No Signing)" env: MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} MACOS_CERTIFICATE_PWD: ${{ secrets.MACOS_CERTIFICATE_PWD }} - if: ${{ env.MACOS_CERTIFICATE == null && matrix.os == 'macos-latest' }} + if: ${{ env.MACOS_CERTIFICATE == null && (matrix.os == 'macos-latest' || matrix.os == 'macOS-ARM64') }} run: mvn -B clean install -DskipTests -Pbuild-installer --file pom.xml - - name: "Build with Maven (macOS Signed)" - env: - MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} - MACOS_CERTIFICATE_PWD: ${{ secrets.MACOS_CERTIFICATE_PWD }} - if: ${{ env.MACOS_CERTIFICATE != null && matrix.os == 'macos-latest' }} - run: | - echo $MACOS_CERTIFICATE | base64 --decode > certificate.p12 - security create-keychain -p temppass build.keychain - security default-keychain -s build.keychain - security unlock-keychain -p temppass build.keychain - security import certificate.p12 -k build.keychain -P $MACOS_CERTIFICATE_PWD -T /usr/bin/codesign - security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k temppass build.keychain - export IDENTITY=$(security find-identity -v) - echo $(security find-identity -v) - mvn -B clean install -DskipTests -Pbuild-installer -Pmacos-sign --file pom.xml - - name: "Codesign DMG" - env: - MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} - MACOS_CERTIFICATE_PWD: ${{ secrets.MACOS_CERTIFICATE_PWD }} - if: ${{ env.MACOS_CERTIFICATE != null && matrix.os == 'macos-latest' }} - run: | - export DMG_PATH=$(ls ./target/*.dmg) - /usr/bin/codesign --deep --force -s ${{ env.DEV_IDENTITY}} $DMG_PATH -v - echo DMG_PATH=$DMG_PATH >> $GITHUB_ENV - echo ${{ env.DMG_PATH }} - - name: "Notarize DMG" - env: - APP_EMAIL: ${{ secrets.APP_EMAIL }} - APP_PASS: ${{ secrets.APP_PASS }} - if: ${{ env.APP_EMAIL != null && matrix.os == 'macos-latest' }} - uses: devbotsxyz/xcode-notarize@v1 - with: - product-path: ${{ env.DMG_PATH }} - primary-bundle-id: ${{ env.PRIMARY_BUNDLE_ID }} - appstore-connect-username: ${{ secrets.APP_EMAIL }} - appstore-connect-password: ${{ secrets.APP_PASS }} - - name: "Staple DMG" - env: - APP_EMAIL: ${{ secrets.APP_EMAIL }} - APP_PASS: ${{ secrets.APP_PASS }} - if: ${{ env.APP_EMAIL != null && matrix.os == 'macos-latest' }} - uses: devbotsxyz/xcode-staple@v1 - with: - product-path: ${{ env.DMG_PATH }} - name: Update Automatic Release uses: marvinpinto/action-automatic-releases@latest if: ${{ env.RELEASE_INSTALLERS }} diff --git a/pom.xml b/pom.xml index 2ed59ab47..232248a1e 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ org.janelia.saalfeldlab paintera - 0.34.2-SNAPSHOT + 1.0.0-SNAPSHOT Paintera New Era Painting and annotation tool @@ -53,7 +53,7 @@ true ${javadoc.skip} - 0.7.0 + 1.0.0 3.0.7 1.4.0 @@ -75,11 +75,11 @@ - 3.0.0 + 3.0.2 2.0.0 4.0.0 4.0.1 - 1.0.0 + 1.0.1 7.0.0 1.1.0 @@ -88,7 +88,11 @@ - + + dev.dirs + directories + 26 + com.microsoft.onnxruntime onnxruntime @@ -180,7 +184,7 @@ net.imglib2 imglib2-label-multisets - 0.11.5 + 0.12.0 sc.fiji @@ -226,12 +230,12 @@ org.janelia.saalfeldlab label-utilities - 0.5.2-SNAPSHOT + 0.5.1 org.janelia.saalfeldlab label-utilities-n5 - 0.3.3-SNAPSHOT + 0.3.2 org.controlsfx @@ -983,42 +987,6 @@ build-installer - - org.codehaus.mojo - build-helper-maven-plugin - 3.0.0 - - - prepare-package - normalize-app-version1 - - regex-property - - - app._normalVersion - ${project.version} - \.([0-9]+)-SNAPSHOT - .000$1 - false - - - - prepare-package - normalize-app-version2 - - regex-property - - - app.normalVersion - - ${app._normalVersion} - ^0\. - 99. - false - - - - io.github.wiverson @@ -1061,14 +1029,13 @@ - ${app.normalVersion} ${project.build.finalName}.jar jpackage true true true ${project.build.directory}/installer-work - @${project.build.directory}/packaging/${os.detected.name}-jpackage.txt + @${project.build.directory}/packaging/${os.detected.name}/jpackage.txt diff --git a/src/main/java/bdv/fx/viewer/OverlayPane.java b/src/main/java/bdv/fx/viewer/OverlayPane.java index 322cc0ab2..5afb05aae 100644 --- a/src/main/java/bdv/fx/viewer/OverlayPane.java +++ b/src/main/java/bdv/fx/viewer/OverlayPane.java @@ -63,7 +63,7 @@ public class OverlayPane extends StackPane { */ final protected CopyOnWriteArrayList> overlayRenderers; - private final CanvasPane canvasPane = new CanvasPane(1, 1); + private final CanvasPane canvasPane = new CanvasPane(0, 0); private final ObservableList children = FXCollections.unmodifiableObservableList(super.getChildren()); diff --git a/src/main/java/bdv/fx/viewer/ViewerPanelFX.java b/src/main/java/bdv/fx/viewer/ViewerPanelFX.java index d6ba0c4f1..f338c0c1b 100644 --- a/src/main/java/bdv/fx/viewer/ViewerPanelFX.java +++ b/src/main/java/bdv/fx/viewer/ViewerPanelFX.java @@ -37,16 +37,21 @@ import bdv.viewer.SourceAndConverter; import bdv.viewer.TransformListener; import bdv.viewer.ViewerOptions; +import javafx.animation.AnimationTimer; import javafx.beans.binding.Bindings; import javafx.beans.property.ReadOnlyBooleanProperty; import javafx.beans.property.ReadOnlyDoubleProperty; +import javafx.scene.image.Image; +import javafx.scene.layout.Background; import javafx.scene.layout.StackPane; +import javafx.scene.paint.Color; import net.imglib2.Interval; import net.imglib2.Positionable; import net.imglib2.RealInterval; import net.imglib2.RealLocalizable; import net.imglib2.RealPoint; import net.imglib2.RealPositionable; +import net.imglib2.parallel.TaskExecutor; import net.imglib2.realtransform.AffineTransform3D; import net.imglib2.util.Intervals; import org.janelia.saalfeldlab.fx.ObservablePosition; @@ -61,8 +66,6 @@ import java.util.Collection; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; @@ -91,8 +94,6 @@ public class ViewerPanelFX private ThreadGroup threadGroup; - private final ExecutorService renderingExecutorService; - private final CopyOnWriteArrayList> transformListeners; private final ViewerOptions.Values options; @@ -105,9 +106,10 @@ public ViewerPanelFX( final List> sources, final int numTimePoints, final CacheControl cacheControl, - final Function, Interpolation> interpolation) { + final Function, Interpolation> interpolation, + final TaskExecutor taskExecutor) { - this(sources, numTimePoints, cacheControl, ViewerOptions.options(), interpolation); + this(sources, numTimePoints, cacheControl, ViewerOptions.options(), interpolation, taskExecutor); } /** @@ -120,9 +122,10 @@ public ViewerPanelFX( public ViewerPanelFX( final CacheControl cacheControl, final ViewerOptions optional, - final Function, Interpolation> interpolation) { + final Function, Interpolation> interpolation, + final TaskExecutor taskExecutor) { - this(1, cacheControl, optional, interpolation); + this(1, cacheControl, optional, interpolation, taskExecutor); } /** @@ -137,9 +140,10 @@ public ViewerPanelFX( final int numTimepoints, final CacheControl cacheControl, final ViewerOptions optional, - final Function, Interpolation> interpolation) { + final Function, Interpolation> interpolation, + final TaskExecutor taskExecutor) { - this(new ArrayList<>(), numTimepoints, cacheControl, optional, interpolation); + this(new ArrayList<>(), numTimepoints, cacheControl, optional, interpolation, taskExecutor); } /** @@ -156,11 +160,12 @@ public ViewerPanelFX( final int numTimepoints, final CacheControl cacheControl, final ViewerOptions optional, - final Function, Interpolation> interpolation) { + final Function, Interpolation> interpolation, + final TaskExecutor taskExecutor) { super(); + super.setBackground(Background.fill(Color.BLACK)); super.getChildren().setAll(canvasPane, overlayPane); - this.renderingExecutorService = Executors.newFixedThreadPool(optional.values.getNumRenderingThreads(), new RenderThreadFactory()); options = optional.values; threadGroup = new ThreadGroup(this.toString()); @@ -177,15 +182,14 @@ public ViewerPanelFX( options.getAccumulateProjectorFactory(), cacheControl, options.getTargetRenderNanos(), - options.getNumRenderingThreads(), - renderingExecutorService + taskExecutor ); - setRenderedImageListener(); + startRenderAnimator(); setWidth(options.getWidth()); setHeight(options.getHeight()); - this.widthProperty().addListener((obs, oldv, newv) -> this.renderUnit.setDimensions((long)getWidth(), (long)getHeight())); - this.heightProperty().addListener((obs, oldv, newv) -> this.renderUnit.setDimensions((long)getWidth(), (long)getHeight())); + this.widthProperty().addListener((obs, oldv, newv) -> this.renderUnit.setDimensions((long) getWidth(), (long) getHeight())); + this.heightProperty().addListener((obs, oldv, newv) -> this.renderUnit.setDimensions((long) getWidth(), (long) getHeight())); transformListeners.add(tf -> Paintera.whenPaintable(getDisplay()::drawOverlays)); @@ -217,7 +221,8 @@ public void setFocusable(boolean focusable) { this.focusable = focusable; } - @Override public void requestFocus() { + @Override + public void requestFocus() { if (this.focusable) { super.requestFocus(); @@ -280,6 +285,7 @@ public

void displayToSourceCoordi sourceToGlobal.applyInverse(pos, pos); } + /** * Set {@code pos} to the source coordinates (x,y,z)T transformed into the display coordinate system. * @@ -324,8 +330,8 @@ public void getMouseCoordinates(final Positionable p) { assert p.numDimensions() >= 2; synchronized (mouseTracker) { - p.setPosition((long)mouseTracker.getMouseX(), 0); - p.setPosition((long)mouseTracker.getMouseY(), 1); + p.setPosition((long) mouseTracker.getMouseX(), 0); + p.setPosition((long) mouseTracker.getMouseY(), 1); } } @@ -345,6 +351,7 @@ public void requestRepaint(final RealInterval intervalInGlobalSpace) { if (intervalInGlobalSpace == null) { requestRepaint(); + return; } final AffineTransform3D globalToViewerTransform = this.viewerTransform.copy(); @@ -433,16 +440,9 @@ public void removeTransformListener(final TransformListener l } } - /** - * Shutdown the {@link ExecutorService} used for rendering tiles onto the screen. - */ - public void stop() { - - renderingExecutorService.shutdown(); - } - private static final AtomicInteger panelNumber = new AtomicInteger(1); + // TODO: rendering protected class RenderThreadFactory implements ThreadFactory { private final String threadNameFormat; @@ -536,15 +536,16 @@ public void setScreenScales(final double[] screenScales) { this.renderUnit.setScreenScales(screenScales.clone()); } + public double[] getScreenScales() { + final double[] screenScale = renderUnit.getScreenScalesProperty().get(); + return Arrays.copyOf(screenScale, screenScale.length); + } + /** * @return {@link OverlayPane} used for drawing overlays without re-rendering 2D cross-sections */ public OverlayPane getDisplay() { - var pos = new ObservablePosition(0, 0); - pos.getX(); - pos.setX(0.0); - return this.overlayPane; } @@ -553,24 +554,47 @@ public RenderUnit getRenderUnit() { return renderUnit; } - private void setRenderedImageListener() { - - renderUnit.getRenderedImageProperty().addListener((obs, oldv, newv) -> { - if (newv != null && newv.getImage() != null) { - final Interval screenInterval = newv.getScreenInterval(); - final RealInterval renderTargetRealInterval = newv.getRenderTargetRealInterval(); - canvasPane.getCanvas().getGraphicsContext2D().drawImage( - newv.getImage(), // src - renderTargetRealInterval.realMin(0), // src X - renderTargetRealInterval.realMin(1), // src Y - renderTargetRealInterval.realMax(0) - renderTargetRealInterval.realMin(0), // src width - renderTargetRealInterval.realMax(1) - renderTargetRealInterval.realMin(1), // src height - screenInterval.min(0), // dst X - screenInterval.min(1), // dst Y - screenInterval.dimension(0), // dst width - screenInterval.dimension(1) // dst height - ); + private void startRenderAnimator() { + + new AnimationTimer() { + + private RenderUnit.RenderResult renderResult = null; + + @Override + public void handle(long now) { + final var result = renderUnit.getRenderedImageProperty().get(); + if (result != renderResult) { + renderResult = result; + } + + if (renderResult != null) { + final Image image = renderResult.getImage(); + if (image != null) { + final Interval screenInterval = renderResult.getScreenInterval(); + final RealInterval renderTargetRealInterval = renderResult.getRenderTargetRealInterval(); + + canvasPane.getCanvas().getGraphicsContext2D().clearRect( + screenInterval.min(0), // dst X + screenInterval.min(1), // dst Y + screenInterval.dimension(0), // dst width + screenInterval.dimension(1) // dst height + ); + synchronized (image) { + canvasPane.getCanvas().getGraphicsContext2D().drawImage( + image, // src + renderTargetRealInterval.realMin(0), // src X + renderTargetRealInterval.realMin(1), // src Y + renderTargetRealInterval.realMax(0) - renderTargetRealInterval.realMin(0), // src width + renderTargetRealInterval.realMax(1) - renderTargetRealInterval.realMin(1), // src height + screenInterval.min(0) - 1, // dst X + screenInterval.min(1) - 1, // dst Y + screenInterval.dimension(0) + 1, // dst width + screenInterval.dimension(1) + 1 // dst height + ); + } + } + } } - }); + }.start(); } } diff --git a/src/main/java/bdv/fx/viewer/project/VolatileHierarchyProjector.java b/src/main/java/bdv/fx/viewer/project/VolatileHierarchyProjector.java index 22af2ed86..5982942d0 100644 --- a/src/main/java/bdv/fx/viewer/project/VolatileHierarchyProjector.java +++ b/src/main/java/bdv/fx/viewer/project/VolatileHierarchyProjector.java @@ -28,12 +28,9 @@ */ package bdv.fx.viewer.project; -import bdv.viewer.render.ProjectorUtils; import bdv.viewer.render.VolatileProjector; -import net.imglib2.Cursor; import net.imglib2.FinalInterval; import net.imglib2.IterableInterval; -import net.imglib2.RandomAccess; import net.imglib2.RandomAccessible; import net.imglib2.RandomAccessibleInterval; import net.imglib2.Volatile; @@ -42,7 +39,6 @@ import net.imglib2.converter.Converter; import net.imglib2.img.array.ArrayImgs; import net.imglib2.loops.LoopBuilder; -import net.imglib2.parallel.Parallelization; import net.imglib2.parallel.TaskExecutor; import net.imglib2.type.numeric.integer.ByteType; import net.imglib2.type.operators.SetZero; @@ -54,7 +50,6 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.Callable; -import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; @@ -120,15 +115,10 @@ public class VolatileHierarchyProjector, B extends SetZero */ protected final IterableInterval iterableTarget; - /** - * Number of threads to use for rendering - */ - private final int numThreads; - /** * Executor service to be used for rendering */ - private final ExecutorService executorService; + protected final TaskExecutor taskExecutor; /** * Time needed for rendering the last frame, in nano-seconds. @@ -151,16 +141,14 @@ public class VolatileHierarchyProjector, B extends SetZero * Flag to indicate that someone is trying to {@link #cancel()} rendering. */ protected final AtomicBoolean canceled = new AtomicBoolean(); - protected final Object setTargetLock = new Object(); public VolatileHierarchyProjector( final List> sources, final Converter converter, final RandomAccessibleInterval target, - final int numThreads, - final ExecutorService executorService) { + final TaskExecutor taskExecutor) { - this(sources, converter, target, ArrayImgs.bytes(Intervals.dimensionsAsLongArray(target)), numThreads, executorService); + this(sources, converter, target, ArrayImgs.bytes(Intervals.dimensionsAsLongArray(target)), taskExecutor); } public VolatileHierarchyProjector( @@ -168,8 +156,7 @@ public VolatileHierarchyProjector( final Converter converter, final RandomAccessibleInterval target, final RandomAccessibleInterval mask, - final int numThreads, - final ExecutorService executorService) { + final TaskExecutor taskExecutor) { this.converter = converter; this.target = target; @@ -188,8 +175,7 @@ public VolatileHierarchyProjector( max[1] = target.max(1); sourceInterval = new FinalInterval(min, max); - this.numThreads = numThreads; - this.executorService = executorService; + this.taskExecutor = taskExecutor; lastFrameRenderNanoTime = -1; clearMask(); @@ -219,13 +205,12 @@ public boolean isValid() { } /** - * Set all pixels in target to 100% transparent zero, and mask to all - * Integer.MAX_VALUE. + * Set all mask values to Byte.MAX_VALUE. This will ensure all target values are re-render on the next pass. */ public void clearMask() { try { - LoopBuilder.setImages(mask).multiThreaded().forEachPixel(val -> val.set(Byte.MAX_VALUE)); + LoopBuilder.setImages(mask).multiThreaded(taskExecutor).forEachPixel(val -> val.set(Byte.MAX_VALUE)); } catch (RuntimeException e) { if (!e.getMessage().contains("Interrupted")) { throw e; @@ -239,20 +224,16 @@ public void clearMask() { */ private void clearUntouchedTargetPixels() { - final int[] data = ProjectorUtils.getARGBArrayImgData(target); - if (data != null) { - final Cursor maskCursor = Views.iterable(mask).cursor(); - final int size = (int)Intervals.numElements(target); - for (int i = 0; i < size; ++i) { - if (maskCursor.next().get() == Byte.MAX_VALUE) - data[i] = 0; - } - } else { - final Cursor maskCursor = Views.iterable(mask).cursor(); - for (final B t : iterableTarget) { - if (maskCursor.next().get() == Byte.MAX_VALUE) - t.setZero(); - } + try { + LoopBuilder.setImages(Views.interval(new BundleView<>(target), target), mask) + .multiThreaded(taskExecutor) + .forEachPixel((targetRA, maskVal) -> { + if (maskVal.get() == Byte.MAX_VALUE) { + targetRA.get().setZero(); + } + }); + } catch (RuntimeException e) { + Thread.currentThread().interrupt(); } } @@ -268,45 +249,29 @@ public boolean map(final boolean clearUntouchedTargetPixels) { final long startTimeIo = iostat.getIoNanoTime(); final long startTimeIoCumulative = iostat.getCumulativeIoNanoTime(); - final int targetHeight = (int)target.dimension(1); - final int numTasks = 1; //numThreads <= 1 ? 1 : Math.min(numThreads * 10, targetHeight); - final double taskHeight = (double)targetHeight / numTasks; - final int[] taskStartHeights = new int[numTasks + 1]; - for (int i = 0; i < numTasks; ++i) { - taskStartHeights[i] = (int)(i * taskHeight); - } - taskStartHeights[numTasks] = targetHeight; - valid = false; - final boolean createExecutor = (executorService == null); - final ExecutorService ex = createExecutor ? Executors.newFixedThreadPool(numThreads) : executorService; int resolutionLevel; - try { - /* - * After the for loop, resolutionLevel is the highest (coarsest) - * resolution for which all pixels could be filled from valid data. This - * means that in the next pass, i.e., map() call, levels up to - * resolutionLevel have to be re-rendered. - */ - for (resolutionLevel = 0; resolutionLevel < numInvalidLevels && !valid; ++resolutionLevel) { - final List> tasks = new ArrayList<>(numTasks); - valid = true; - numInvalidPixels.set(0); - for (int i = 0; i < numTasks; ++i) { - tasks.add(createMapTask((byte)resolutionLevel, taskStartHeights[i], taskStartHeights[i + 1])); - } - try { - ex.invokeAll(tasks); - } catch (final InterruptedException e) { - Thread.currentThread().interrupt(); - } - if (canceled.get()) - return false; + /* + * After the for loop, resolutionLevel is the highest (coarsest) + * resolution for which all pixels could be filled from valid data. This + * means that in the next pass, i.e., map() call, levels up to + * resolutionLevel have to be re-rendered. + */ + for (resolutionLevel = 0; resolutionLevel < numInvalidLevels && !valid; ++resolutionLevel) { + final List> tasks = new ArrayList<>(); + valid = true; + numInvalidPixels.set(0); + final byte idx = (byte) resolutionLevel; + tasks.add(Executors.callable(() -> map(idx, -1, -1), null)); + + try { + taskExecutor.getExecutorService().invokeAll(tasks); + } catch (final InterruptedException e) { + canceled.set(true); } - } finally { - if (createExecutor) - ex.shutdown(); + if (canceled.get()) + return false; } if (clearUntouchedTargetPixels && !canceled.get()) @@ -314,7 +279,7 @@ public boolean map(final boolean clearUntouchedTargetPixels) { final long lastFrameTime = stopWatch.nanoTime(); lastFrameIoNanoTime = iostat.getIoNanoTime() - startTimeIo; - lastFrameRenderNanoTime = lastFrameTime - (iostat.getCumulativeIoNanoTime() - startTimeIoCumulative) / numThreads; + lastFrameRenderNanoTime = lastFrameTime - (iostat.getCumulativeIoNanoTime() - startTimeIoCumulative) / taskExecutor.getParallelism(); if (valid) numInvalidLevels = resolutionLevel - 1; @@ -323,15 +288,6 @@ public boolean map(final boolean clearUntouchedTargetPixels) { return !canceled.get(); } - /** - * @return a {@code Callable} that runs - * {@code map(resolutionIndex, startHeight, endHeight)} - */ - private Callable createMapTask(final byte resolutionIndex, final int startHeight, final int endHeight) { - - return Executors.callable(() -> map(resolutionIndex, startHeight, endHeight), null); - } - /** * Copy lines from {@code y = startHeight} up to {@code endHeight} * (exclusive) from source {@code resolutionIndex} to target. Check after @@ -354,85 +310,33 @@ protected void map(final byte resolutionIndex, final int startHeight, final int if (canceled.get()) return; - if (true) { - //TODO Caleb: For now this seems to be faster, but some proper benchmarks should be performed. - // The previous logic is left below, in the `else` branch. - final AtomicInteger myNumInvalidPixels = new AtomicInteger(); - - final TaskExecutor taskExecutor = Parallelization.getTaskExecutor(); - LoopBuilder.setImages( - Views.interval(new BundleView<>(target), sourceInterval), - Views.interval(new BundleView<>(sources.get(resolutionIndex)), sourceInterval), - Views.interval(mask, sourceInterval) - ).multiThreaded(taskExecutor) - .forEachChunk(chunk -> { - if (canceled.get()) { - if (!taskExecutor.getExecutorService().isShutdown()) { - taskExecutor.getExecutorService().shutdown(); - } - return null; - } - chunk.forEachPixel((targetVal, sourceVal, maskVal) -> { - if (maskVal.get() > resolutionIndex) { - synchronized (setTargetLock) { - final A a = sourceVal.get(); - final boolean v = a.isValid(); - if (v) { - converter.convert(a, targetVal.get()); - maskVal.set(resolutionIndex); - } else - myNumInvalidPixels.incrementAndGet(); - } - } - }); + final AtomicInteger myNumInvalidPixels = new AtomicInteger(); + + LoopBuilder.setImages( + Views.interval(new BundleView<>(target), sourceInterval), + Views.interval(new BundleView<>(sources.get(resolutionIndex)), sourceInterval), + Views.interval(mask, sourceInterval) + ).multiThreaded(taskExecutor) + .forEachChunk(chunk -> { + if (canceled.get()) { + Thread.currentThread().interrupt(); return null; - }); - numInvalidPixels.addAndGet(myNumInvalidPixels.get()); - if (myNumInvalidPixels.get() != 0) - valid = false; - } else { - final RandomAccess targetRandomAccess = target.randomAccess(target); - final RandomAccess sourceRandomAccess = sources.get(resolutionIndex).randomAccess(sourceInterval); - final int width = (int)target.dimension(0); - final long[] smin = Intervals.minAsLongArray(sourceInterval); - int myNumInvalidPixels = 0; - - final Cursor maskCursor = Views.iterable(mask).cursor(); - maskCursor.jumpFwd((long)startHeight * width); - - final int targetMin = (int)target.min(1); - for (int y = startHeight; y < endHeight; ++y) { - if (canceled.get()) - return; - - smin[1] = y + targetMin; - sourceRandomAccess.setPosition(smin); - targetRandomAccess.setPosition(smin); - - for (int x = 0; x < width; ++x) { - - final ByteType maskByte = maskCursor.next(); - if (maskByte.get() > resolutionIndex) { - - //TODO Caleb: This should be temporary, currently necessary to stop the canvas from flickering. - // Fix underlying issue, so we can remove the lock - synchronized (setTargetLock) { - final A a = sourceRandomAccess.get(); + } + chunk.forEachPixel((targetVal, sourceVal, maskVal) -> { + if (maskVal.get() > resolutionIndex) { + final A a = sourceVal.get(); final boolean v = a.isValid(); if (v) { - converter.convert(a, targetRandomAccess.get()); - maskByte.set(resolutionIndex); + converter.convert(a, targetVal.get()); + maskVal.set(resolutionIndex); } else - ++myNumInvalidPixels; + myNumInvalidPixels.incrementAndGet(); } - } - sourceRandomAccess.fwd(0); - targetRandomAccess.fwd(0); - } - } - numInvalidPixels.addAndGet(myNumInvalidPixels); - if (myNumInvalidPixels != 0) - valid = false; - } + }); + return null; + }); + numInvalidPixels.addAndGet(myNumInvalidPixels.get()); + if (myNumInvalidPixels.get() != 0) + valid = false; } } diff --git a/src/main/java/bdv/fx/viewer/project/VolatileHierarchyProjectorPreMultiply.java b/src/main/java/bdv/fx/viewer/project/VolatileHierarchyProjectorPreMultiply.java index ff9489fe9..2d2142e9c 100644 --- a/src/main/java/bdv/fx/viewer/project/VolatileHierarchyProjectorPreMultiply.java +++ b/src/main/java/bdv/fx/viewer/project/VolatileHierarchyProjectorPreMultiply.java @@ -1,72 +1,68 @@ package bdv.fx.viewer.project; import com.sun.javafx.image.PixelUtils; -import net.imglib2.Cursor; -import net.imglib2.RandomAccess; import net.imglib2.RandomAccessible; import net.imglib2.RandomAccessibleInterval; import net.imglib2.Volatile; import net.imglib2.converter.Converter; +import net.imglib2.loops.LoopBuilder; +import net.imglib2.parallel.TaskExecutor; import net.imglib2.type.numeric.ARGBType; import net.imglib2.type.numeric.integer.ByteType; -import net.imglib2.util.Intervals; +import net.imglib2.view.BundleView; import net.imglib2.view.Views; import java.util.List; -import java.util.concurrent.ExecutorService; +import java.util.concurrent.atomic.AtomicInteger; public class VolatileHierarchyProjectorPreMultiply> extends VolatileHierarchyProjector { - public VolatileHierarchyProjectorPreMultiply(List> sources, Converter converter, - RandomAccessibleInterval target, RandomAccessibleInterval mask, int numThreads, ExecutorService executorService) { + public VolatileHierarchyProjectorPreMultiply( + final List> sources, + final Converter converter, + final RandomAccessibleInterval target, + final RandomAccessibleInterval mask, + final TaskExecutor taskExecutor) { - super(sources, converter, target, mask, numThreads, executorService); + super(sources, converter, target, mask, taskExecutor); } @Override protected void map(byte resolutionIndex, int startHeight, int endHeight) { + if (canceled.get()) return; - final RandomAccess targetRandomAccess = target.randomAccess(target); - final RandomAccess sourceRandomAccess = sources.get(resolutionIndex).randomAccess(sourceInterval); - final int width = (int)target.dimension(0); - final long[] smin = Intervals.minAsLongArray(sourceInterval); - int myNumInvalidPixels = 0; - - final Cursor maskCursor = Views.iterable(mask).cursor(); - maskCursor.jumpFwd((long)startHeight * width); - - final int targetMin = (int)target.min(1); - for (int y = startHeight; y < endHeight; ++y) { - if (canceled.get()) - return; - - smin[1] = y + targetMin; - sourceRandomAccess.setPosition(smin); - targetRandomAccess.setPosition(smin); - - for (int x = 0; x < width; ++x) { + final AtomicInteger myNumInvalidPixels = new AtomicInteger(); - final ByteType maskByte = maskCursor.next(); - if (maskByte.get() > resolutionIndex) { - final A a = sourceRandomAccess.get(); - final boolean v = a.isValid(); - if (v) { - final ARGBType argb = targetRandomAccess.get(); - converter.convert(a, argb); - argb.set(PixelUtils.NonPretoPre(argb.get())); - maskByte.set(resolutionIndex); - } else - ++myNumInvalidPixels; - } - sourceRandomAccess.fwd(0); - targetRandomAccess.fwd(0); - } - } - numInvalidPixels.addAndGet(myNumInvalidPixels); - if (myNumInvalidPixels != 0) + LoopBuilder.setImages( + Views.interval(new BundleView<>(target), sourceInterval), + Views.interval(new BundleView<>(sources.get(resolutionIndex)), sourceInterval), + Views.interval(mask, sourceInterval) + ).multiThreaded(taskExecutor) + .forEachChunk(chunk -> { + if (canceled.get()) { + Thread.currentThread().interrupt(); + return null; + } + chunk.forEachPixel((targetVal, sourceVal, maskVal) -> { + if (maskVal.get() > resolutionIndex) { + final A a = sourceVal.get(); + final boolean v = a.isValid(); + if (v) { + final ARGBType argb = targetVal.get(); + converter.convert(a, argb); + argb.set(PixelUtils.NonPretoPre(argb.get())); + maskVal.set(resolutionIndex); + } else + myNumInvalidPixels.incrementAndGet(); + } + }); + return null; + }); + numInvalidPixels.addAndGet(myNumInvalidPixels.get()); + if (myNumInvalidPixels.get() != 0) valid = false; } } diff --git a/src/main/java/bdv/fx/viewer/render/ImageOverlayRendererFX.java b/src/main/java/bdv/fx/viewer/render/ImageOverlayRendererFX.java index ca60c5546..24af07f48 100644 --- a/src/main/java/bdv/fx/viewer/render/ImageOverlayRendererFX.java +++ b/src/main/java/bdv/fx/viewer/render/ImageOverlayRendererFX.java @@ -51,14 +51,6 @@ public class ImageOverlayRendererFX protected PixelBufferWritableImage bufferedImage; - /** - * An {@link ArrayImg} that has been previously set for painting. Whenever a - * new image is set, this is stored here and marked {@link #pending}. Whenever an image is painted and a new image - * is pending, the new image is painted to the screen. Before doing this, the image previously used for painting is - * swapped into pendingImage. This is used for double-buffering. - */ - protected PixelBufferWritableImage pendingImage; - /** * Whether an image is pending. */ @@ -77,8 +69,6 @@ public class ImageOverlayRendererFX public ImageOverlayRendererFX() { bufferedImage = null; - pendingImage = null; - pending = false; width = 0; height = 0; } @@ -91,10 +81,12 @@ public ImageOverlayRendererFX() { @Override public synchronized PixelBufferWritableImage setBufferedImage(final PixelBufferWritableImage img) { - final PixelBufferWritableImage tmp = pendingImage; - pendingImage = img; - pending = true; - return tmp; + PixelBufferWritableImage oldImage; + synchronized (this) { + oldImage = bufferedImage; + bufferedImage = img; + } + return oldImage; } @Override @@ -109,11 +101,6 @@ public int getHeight() { return height; } - public Image getPendingImage() { - - return pendingImage; - } - public Image getBufferedImage() { return bufferedImage; @@ -122,14 +109,6 @@ public Image getBufferedImage() { @Override public void drawOverlays(final Consumer g) { - synchronized (this) { - if (pending) { - final PixelBufferWritableImage tmp = bufferedImage; - bufferedImage = pendingImage; - pendingImage = tmp; - pending = false; - } - } if (bufferedImage != null) { g.accept(bufferedImage); } diff --git a/src/main/java/bdv/fx/viewer/render/MultiResolutionRendererFX.java b/src/main/java/bdv/fx/viewer/render/MultiResolutionRendererFX.java index 229a87849..d8e3596c5 100644 --- a/src/main/java/bdv/fx/viewer/render/MultiResolutionRendererFX.java +++ b/src/main/java/bdv/fx/viewer/render/MultiResolutionRendererFX.java @@ -34,6 +34,7 @@ import net.imglib2.Interval; import net.imglib2.Volatile; import net.imglib2.img.array.ArrayImg; +import net.imglib2.parallel.TaskExecutor; import net.imglib2.type.numeric.ARGBType; import java.util.concurrent.ExecutorService; @@ -111,8 +112,7 @@ public MultiResolutionRendererFX( final double[] screenScales, final long targetRenderNanos, final boolean doubleBuffered, - final int numRenderingThreads, - final ExecutorService renderingExecutorService, + final TaskExecutor renderingTaskExecutor, final boolean useVolatileIfAvailable, final AccumulateProjectorFactory accumulateProjectorFactory, final CacheControl cacheControl) { @@ -123,8 +123,7 @@ public MultiResolutionRendererFX( screenScales, targetRenderNanos, doubleBuffered, - numRenderingThreads, - renderingExecutorService, + renderingTaskExecutor, useVolatileIfAvailable, accumulateProjectorFactory, cacheControl, diff --git a/src/main/java/bdv/fx/viewer/render/MultiResolutionRendererGeneric.java b/src/main/java/bdv/fx/viewer/render/MultiResolutionRendererGeneric.java index f864b10cc..72c9e4ad2 100644 --- a/src/main/java/bdv/fx/viewer/render/MultiResolutionRendererGeneric.java +++ b/src/main/java/bdv/fx/viewer/render/MultiResolutionRendererGeneric.java @@ -46,6 +46,7 @@ import bdv.viewer.render.Prefetcher; import bdv.viewer.render.SimpleVolatileProjector; import bdv.viewer.render.VolatileProjector; +import javafx.animation.AnimationTimer; import net.imglib2.Dimensions; import net.imglib2.FinalInterval; import net.imglib2.FinalRealInterval; @@ -63,6 +64,7 @@ import net.imglib2.img.array.ArrayImgs; import net.imglib2.img.basictypeaccess.IntAccess; import net.imglib2.img.basictypeaccess.array.IntArray; +import net.imglib2.parallel.TaskExecutor; import net.imglib2.realtransform.AffineTransform3D; import net.imglib2.realtransform.RealViews; import net.imglib2.type.numeric.ARGBType; @@ -78,10 +80,8 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Optional; -import java.util.concurrent.ExecutorService; import java.util.function.Function; import java.util.function.ToIntFunction; @@ -128,14 +128,9 @@ public interface ImageGenerator { private final boolean doubleBuffered; /** - * Double-buffer index of next {@link #screenImages image} to render. + * screen scale index of last buffer sent to render. Used for returning the buffer when done. */ - private final ArrayDeque renderIdQueue; - - /** - * Maps from data store to double-buffer index. Needed for double-buffering. - */ - private final HashMap bufferedImageToRenderId; + private int reuseBufferScreenScale = -1; /** * Used to render an individual source. One image per screen resolution and visible source. First index is screen @@ -151,15 +146,9 @@ public interface ImageGenerator { /** * Used to render the image for display. Three images per screen resolution if double buffering is enabled. First - * index is screen scale, second index is double-buffer. - */ - private List> screenImages; - - /** - * data store wrapping the data in the {@link #screenImages}. First index is screen scale, second index is - * double-buffer. + * index is screen scale, second array is double-buffer. */ - private List> bufferedImages; + private List> screenImages; /** * Scale factors from the {@link #display viewer canvas} to the {@link #screenImages}. @@ -218,14 +207,9 @@ public interface ImageGenerator { private volatile boolean renderingMayBeCancelled; /** - * How many threads to use for rendering. + * {@link TaskExecutor} used for rendering. */ - private final int numRenderingThreads; - - /** - * {@link ExecutorService} used for rendering. - */ - private final ExecutorService renderingExecutorService; + private final TaskExecutor renderingTaskExecutor; /** * TODO @@ -277,9 +261,7 @@ public interface ImageGenerator { * this * threshold. * @param doubleBuffered Whether to use double buffered rendering. - * @param numRenderingThreads How many threads to use for rendering. - * @param renderingExecutorService if non-null, this is used for rendering. Note, that it is still important to supply the numRenderingThreads - * parameter, because that is used to determine into how many sub-tasks rendering is split. + * @param renderingTaskExecutor * @param useVolatileIfAvailable whether volatile versions of sources should be used if available. * @param accumulateProjectorFactory can be used to customize how sources are combined. * @param cacheControl the cache controls IO budgeting and fetcher queue. @@ -290,8 +272,7 @@ public interface ImageGenerator { final double[] screenScales, final long targetRenderNanos, final boolean doubleBuffered, - final int numRenderingThreads, - final ExecutorService renderingExecutorService, + final TaskExecutor renderingTaskExecutor, final boolean useVolatileIfAvailable, final AccumulateProjectorFactory accumulateProjectorFactory, final CacheControl cacheControl, @@ -306,28 +287,31 @@ public interface ImageGenerator { currentScreenScaleIndex = -1; this.screenScales = screenScales.clone(); this.doubleBuffered = doubleBuffered; - renderIdQueue = new ArrayDeque<>(); - bufferedImageToRenderId = new HashMap<>(); createVariables(); this.makeImage = makeImage; - this.width = width; - this.height = height; - this.wrapAsArrayImg = wrapAsArrayImg; - this.targetRenderNanos = targetRenderNanos; renderingMayBeCancelled = true; - this.numRenderingThreads = numRenderingThreads; - this.renderingExecutorService = renderingExecutorService; + this.renderingTaskExecutor = renderingTaskExecutor; this.useVolatileIfAvailable = useVolatileIfAvailable; this.accumulateProjectorFactory = accumulateProjectorFactory; this.cacheControl = cacheControl; newFrameRequest = false; previousTimepoint = -1; + + new AnimationTimer() { + + @Override + public void handle(long now) { + if (requestedScreenScaleIndex >= 0 && requestedScreenScaleIndex < screenScales.length && pendingRepaintRequests[requestedScreenScaleIndex] != null) { + painterThread.requestRepaint(); + } + } + }.start(); } /** @@ -340,31 +324,20 @@ private synchronized boolean checkResize() { final int componentW = display.getWidth(); final int componentH = display.getHeight(); - if (screenImages.get(0).get(0) == null - || width.applyAsInt(screenImages.get(0).get(0)) != (int)Math.ceil(componentW * screenScales[0]) - || height.applyAsInt(screenImages.get(0).get(0)) != (int)Math.ceil(componentH * screenScales[0])) { - renderIdQueue.clear(); - renderIdQueue.addAll(Arrays.asList(0, 1, 2)); - bufferedImageToRenderId.clear(); + final ArrayDeque highestResBuffers = screenImages.get(0); + final T highResBuffer = highestResBuffers.peek(); + if (highResBuffer == null + || width.applyAsInt(highResBuffer) != (int) Math.ceil(componentW * screenScales[0]) + || height.applyAsInt(highResBuffer) != (int) Math.ceil(componentH * screenScales[0])) { + int numBuffers = doubleBuffered ? 2 : 1; for (int i = 0; i < screenScales.length; ++i) { + final ArrayDeque bufferQueue = screenImages.get(i); + bufferQueue.clear(); final double screenToViewerScale = screenScales[i]; - final int w = (int)Math.ceil(screenToViewerScale * componentW); - final int h = (int)Math.ceil(screenToViewerScale * componentH); - if (doubleBuffered) { - for (int b = 0; b < 3; ++b) { - // reuse storage arrays of level 0 (highest resolution) - screenImages.get(i).set(b, i == 0 - ? makeImage.create(w, h) - : makeImage.create(w, h, screenImages.get(0).get(b))); - final T bi = screenImages.get(i).get(b); - // getBufferedImage.apply( screenImages[ i ][ b ] ); - bufferedImages.get(i).set(b, bi); - bufferedImageToRenderId.put(bi, b); - } - } else { - screenImages.get(i).set(0, makeImage.create(w, h)); - bufferedImages.get(i).set(0, screenImages.get(i).get(0)); - // getBufferedImage.apply( screenImages[ i ][ 0 ] ); + final int w = (int) Math.ceil(screenToViewerScale * componentW); + final int h = (int) Math.ceil(screenToViewerScale * componentH); + for (int idx = 0; idx < numBuffers; ++idx) { + bufferQueue.add(makeImage.create(w, h)); } final AffineTransform3D scale = new AffineTransform3D(); final double xScale = screenToViewerScale; @@ -384,38 +357,48 @@ private synchronized boolean checkResize() { @SuppressWarnings("unchecked") private boolean checkRenewRenderImages(final int numVisibleSources) { - final int n = numVisibleSources > 1 ? numVisibleSources : 0; - if (n != renderImages[0].length || - n != 0 && - (renderImages[0][0].dimension(0) != width.applyAsInt(screenImages.get(0).get(0)) || - renderImages[0][0].dimension(1) != height.applyAsInt(screenImages.get(0).get(0)))) { - renderImages = new ArrayImg[screenScales.length][n]; - for (int i = 0; i < screenScales.length; ++i) { - final int w = width.applyAsInt(screenImages.get(i).get(0)); - final int h = height.applyAsInt(screenImages.get(i).get(0)); - for (int j = 0; j < n; ++j) { - renderImages[i][j] = i == 0 - ? ArrayImgs.argbs(w, h) - : ArrayImgs.argbs(renderImages[0][j].update(null), w, h); - } + final int n = Math.max(numVisibleSources, 1); + final T screenImage = screenImages.get(0).peek(); + + final int screenWidth = width.applyAsInt(screenImage); + final int screenHeight = height.applyAsInt(screenImage); + + final boolean correctNumRenderImages = n == renderImages[0].length; + if (correctNumRenderImages) { + final var renderImage = renderImages[0][0]; + final long renderWidth = renderImage.dimension(0); + final long renderHeight = renderImage.dimension(1); + final boolean dimensionsMatch = screenWidth == renderWidth && screenHeight == renderHeight; + if (dimensionsMatch) + return false; + } + + renderImages = new ArrayImg[screenScales.length][n]; + for (int i = 0; i < screenScales.length; ++i) { + + final int w = this.width.applyAsInt(screenImages.get(i).peek()); + final int h = height.applyAsInt(screenImages.get(i).peek()); + for (int j = 0; j < n; ++j) { + renderImages[i][j] = i == 0 + ? ArrayImgs.argbs(screenWidth, screenHeight) + : ArrayImgs.argbs(renderImages[0][j].update(null), screenWidth, screenHeight); } - return true; } - return false; + return true; } private boolean checkRenewMaskArrays(final int numVisibleSources) { - final int size = width.applyAsInt(screenImages.get(0).get(0)) * height.applyAsInt(screenImages.get(0).get(0)); - if (numVisibleSources != renderMaskArrays.length || - numVisibleSources != 0 && renderMaskArrays[0].length < size) { - renderMaskArrays = new byte[numVisibleSources][]; - for (int j = 0; j < numVisibleSources; ++j) { - renderMaskArrays[j] = new byte[size]; - } - return true; - } - return false; + final T screenImage = screenImages.get(0).peek(); + final int size = width.applyAsInt(screenImage) * height.applyAsInt(screenImage); + if (numVisibleSources == renderMaskArrays.length && (numVisibleSources == 0 || renderMaskArrays[0].length == size)) + return false; + + renderMaskArrays = new byte[numVisibleSources][]; + for (int j = 0; j < numVisibleSources; ++j) + renderMaskArrays[j] = new byte[size]; + + return true; } private int[] getImageSize(final T image) { @@ -431,6 +414,8 @@ private static Interval padInterval(final Interval interval, final int[] padding return new FinalInterval(paddedIntervalMin, paddedIntervalMax); } + private T renderTarget = null; + /** * Render image at the {@link #requestedScreenScaleIndex requested screen scale}. * @@ -447,26 +432,13 @@ public int paint( return -1; final boolean resized = checkResize(); - - // the BufferedImage that is rendered to (to paint to the canvas) - final T bufferedImage; - // the projector that paints to the screenImage. final VolatileProjector p; - final boolean clearQueue; - final boolean createProjector; - final Interval repaintScreenInterval; synchronized (this) { - // FIXME: there is a race condition that sometimes may cause an ArrayIndexOutOfBounds exception: - // Screen scales are first initialized with the default setting (see RenderUnit), - // then the project metadata is loaded, and the screen scales are changed to the saved configuration. - // If the project screen scales are [1.0], sometimes the renderer receives a request to re-render the screen at screen scale 1, which results in the exception. - if (requestedScreenScaleIndex >= pendingRepaintRequests.length) - return -1; repaintScreenInterval = pendingRepaintRequests[requestedScreenScaleIndex]; pendingRepaintRequests[requestedScreenScaleIndex] = null; @@ -488,15 +460,10 @@ public int paint( final List> sacs = sources; if (createProjector) { - final int renderId = renderIdQueue.peek(); currentScreenScaleIndex = requestedScreenScaleIndex; - bufferedImage = bufferedImages.get(currentScreenScaleIndex).get(renderId); - final T renderTarget = screenImages.get(currentScreenScaleIndex).get(renderId); + final var buffers = this.screenImages.get(currentScreenScaleIndex); + renderTarget = buffers.peek(); - // Sometimes there is a race condition at startup, and it may happen that at this point - // the screen images list has not been initialized yet and only contains null images. - if (renderTarget == null) - return -1; synchronized (Optional.ofNullable(synchronizationLock).orElse(this)) { final int numSources = sacs.size(); @@ -513,6 +480,7 @@ public int paint( Arrays.setAll(renderTargetRealIntervalMax, d -> repaintScreenInterval.max(d) * renderTargetToScreenPixelRatio[d]); final RealInterval renderTargetRealInterval = new FinalRealInterval(renderTargetRealIntervalMin, renderTargetRealIntervalMax); + //TODO Caleb: I don't think this padding behaves the way it claims to... // apply 1px padding on each side of the render target repaint interval to avoid interpolation artifacts final Interval renderTargetPaddedInterval = padInterval( Intervals.smallestContainingInterval(renderTargetRealInterval), @@ -542,7 +510,6 @@ public int paint( } projector = p; } else { - bufferedImage = null; p = projector; } @@ -550,19 +517,32 @@ public int paint( } // try rendering - final boolean success = p.map(createProjector); - // final long rendertime = p.getLastFrameRenderNanoTime(); + final boolean success; + synchronized (renderTarget) { + success = p.map(createProjector); + } synchronized (this) { // if rendering was not cancelled... if (success) { if (createProjector) { - final T bi = display.setBufferedImageAndTransform(bufferedImage, currentProjectorTransform); + final ArrayDeque buffers = screenImages.get(currentScreenScaleIndex); + final T renderTarget = doubleBuffered ? buffers.pop() : buffers.peek(); + final T unusedBuffer = display.setBufferedImageAndTransform(renderTarget, currentProjectorTransform); if (doubleBuffered) { - renderIdQueue.pop(); - final Integer id = bufferedImageToRenderId.get(bi); - if (id != null) - renderIdQueue.add(id); + if (unusedBuffer != null) { + /* add the buffer back to the correct screen scale*/ + final ArrayDeque reuseBuffers = screenImages.get(reuseBufferScreenScale); + final T otherBuffer = reuseBuffers == buffers ? renderTarget : reuseBuffers.peek(); + if ( + width.applyAsInt(unusedBuffer) == width.applyAsInt(otherBuffer) + && height.applyAsInt(unusedBuffer) == height.applyAsInt(otherBuffer) + ) { + reuseBuffers.add(unusedBuffer); + } + } + /* update the idx to the one we just provided */ + reuseBufferScreenScale = currentScreenScaleIndex; } /** @@ -581,16 +561,6 @@ public int paint( * When the user finishes painting and starts navigating again, there may be a delay in rendering the first few frames because * it starts from the highest available resolution and then gradually decreases the resolution until the rendertime is within the targetRenderNanos threshold. */ - // if (currentScreenScaleIndex == maxScreenScaleIndex) - // { - // if (rendertime > targetRenderNanos && maxScreenScaleIndex < screenScales.length - 1) - // maxScreenScaleIndex++; - // else if (rendertime < targetRenderNanos / 3 && maxScreenScaleIndex > 0) - // maxScreenScaleIndex--; - // } - // else if (currentScreenScaleIndex == maxScreenScaleIndex - 1) - // if (rendertime < targetRenderNanos && maxScreenScaleIndex > 0) - // maxScreenScaleIndex--; } if (currentScreenScaleIndex > 0) @@ -654,22 +624,22 @@ public void requestRepaint(final Interval interval, final int screenScaleIndex) if (renderingMayBeCancelled && projector != null) projector.cancel(); - if (screenScaleIndex > requestedScreenScaleIndex) - requestedScreenScaleIndex = screenScaleIndex; + int newRequestedScaleIdx; + if (screenScaleIndex > maxScreenScaleIndex) { + newRequestedScaleIdx = maxScreenScaleIndex; + } else if (screenScaleIndex < 0) { + newRequestedScaleIdx = 0; + } else { + newRequestedScaleIdx = screenScaleIndex; + } - // FIXME: there is a race condition that sometimes may cause an ArrayIndexOutOfBounds exception: - // Screen scales are first initialized with the default setting (see RenderUnit), - // then the project metadata is loaded, and the screen scales are changed to the saved configuration. - // If the project screen scales are [1.0], sometimes the renderer receives a request to re-render the screen at screen scale 1, which results in the exception. - if (requestedScreenScaleIndex >= pendingRepaintRequests.length) - return; + if (newRequestedScaleIdx > requestedScreenScaleIndex) + requestedScreenScaleIndex = newRequestedScaleIdx; if (pendingRepaintRequests[requestedScreenScaleIndex] == null) pendingRepaintRequests[requestedScreenScaleIndex] = interval; else pendingRepaintRequests[requestedScreenScaleIndex] = Intervals.union(pendingRepaintRequests[requestedScreenScaleIndex], interval); - - painterThread.requestRepaint(); } private VolatileProjector createProjector( @@ -692,7 +662,7 @@ else if (sacs.size() == 1) { LOG.debug("Got only one source, creating pre-multiplying single source projector"); final SourceAndConverter sac = sacs.get(0); final Interpolation interpolation = interpolationForSource.apply(sac.getSpimSource()); - final int[] renderTargetSize = getImageSize(this.screenImages.get(currentScreenScaleIndex).get(0)); + final int[] renderTargetSize = getImageSize(this.screenImages.get(currentScreenScaleIndex).peek()); projector = createSingleSourceProjector( sac, timepoint, @@ -707,14 +677,13 @@ else if (sacs.size() == 1) { LOG.debug("Got {} sources, creating {} non-pre-multiplying single source projectors", sacs.size()); final ArrayList sourceProjectors = new ArrayList<>(); final ArrayList> sourceImages = new ArrayList<>(); - final ArrayList> sources = new ArrayList<>(); int j = 0; for (final SourceAndConverter sac : sacs) { final RandomAccessibleInterval renderImage = Views.interval(renderImages[currentScreenScaleIndex][j], screenImage); final byte[] maskArray = renderMaskArrays[j]; ++j; final Interpolation interpolation = interpolationForSource.apply(sac.getSpimSource()); - final int[] renderTargetSize = getImageSize(this.screenImages.get(currentScreenScaleIndex).get(0)); + final int[] renderTargetSize = getImageSize(this.screenImages.get(currentScreenScaleIndex).peek()); final VolatileProjector p = createSingleSourceProjector( sac, timepoint, @@ -726,16 +695,15 @@ else if (sacs.size() == 1) { false ); sourceProjectors.add(p); - sources.add(sac.getSpimSource()); sourceImages.add(renderImage); } - projector = accumulateProjectorFactory.createAccumulateProjector( + projector = accumulateProjectorFactory.createProjector( sourceProjectors, - sources, + sacs, sourceImages, screenImage, - numRenderingThreads, - renderingExecutorService + -1, + null //TODO: rendering ); } previousTimepoint = timepoint; @@ -778,7 +746,7 @@ private VolatileProjector createSingleSourceProjector( source.getSpimSource().getName() ); @SuppressWarnings("unchecked") final SourceAndConverter> vsource = - (SourceAndConverter>)source; + (SourceAndConverter>) source; return createSingleSourceVolatileProjector( vsource, timepoint, @@ -832,7 +800,7 @@ private > VolatileProjector createSingleSourceVolatileProj LOG.debug("Creating single source volatile projector for type={}", spimSource.getType()); final MipmapOrdering ordering = MipmapOrdering.class.isInstance(spimSource) - ? (MipmapOrdering)spimSource + ? (MipmapOrdering) spimSource : new DefaultMipmapOrdering(spimSource); final AffineTransform3D screenTransform = viewerTransform.copy(); @@ -882,8 +850,7 @@ private > VolatileProjector createSingleSourceVolatileProj source.getConverter(), Views.stack(screenImage), Views.stack(mask), - numRenderingThreads, - renderingExecutorService + renderingTaskExecutor ); else return new VolatileHierarchyProjector<>( @@ -891,8 +858,7 @@ private > VolatileProjector createSingleSourceVolatileProj source.getConverter(), Views.stack(screenImage), Views.stack(mask), - numRenderingThreads, - renderingExecutorService + renderingTaskExecutor ); } @@ -907,7 +873,7 @@ private static RandomAccessible getTransformedSource( final RandomAccessibleInterval img = source.getSource(timepoint, mipmapIndex); if (VolatileCachedCellImg.class.isInstance(img)) - ((VolatileCachedCellImg)img).setCacheHints(cacheHints); + ((VolatileCachedCellImg) img).setCacheHints(cacheHints); final RealRandomAccessible ipimg = source.getInterpolatedSource(timepoint, mipmapIndex, interpolation); @@ -961,8 +927,8 @@ private static String prettyPrintDouble(final double d) { DecimalFormat df = new DecimalFormat(); df.setMaximumFractionDigits(5); - if (d == (long)d) - return String.format("%d", (long)d); + if (d == (long) d) + return String.format("%d", (long) d); else return df.format(d); } @@ -979,7 +945,7 @@ private static void prefetch( final RandomAccessibleInterval img = source.getSource(timepoint, mipmapIndex); if (VolatileCachedCellImg.class.isInstance(img)) { - final VolatileCachedCellImg cellImg = (VolatileCachedCellImg)img; + final VolatileCachedCellImg cellImg = (VolatileCachedCellImg) img; CacheHints hints = prefetchCacheHints; if (hints == null) { @@ -1036,10 +1002,8 @@ private synchronized void createVariables() { renderImages = new ArrayImg[screenScales.length][0]; renderMaskArrays = new byte[0][]; screenImages = new ArrayList<>(); - bufferedImages = new ArrayList<>(); for (int i = 0; i < screenScales.length; ++i) { - screenImages.add(Arrays.asList(null, null, null)); - bufferedImages.add(Arrays.asList(null, null, null)); + screenImages.add(new ArrayDeque<>()); } screenScaleTransforms = new AffineTransform3D[screenScales.length]; pendingRepaintRequests = new Interval[screenScales.length]; diff --git a/src/main/java/bdv/fx/viewer/render/PainterThread.java b/src/main/java/bdv/fx/viewer/render/PainterThread.java index 2dd4a3c1b..349e0d484 100644 --- a/src/main/java/bdv/fx/viewer/render/PainterThread.java +++ b/src/main/java/bdv/fx/viewer/render/PainterThread.java @@ -16,9 +16,12 @@ public final class PainterThread extends Thread { private boolean isRunning; + private long lastUpdate = -1; + private long targetFrameRateMs = 1000 / 60; + public PainterThread(PainterThread.Paintable paintable) { - this((ThreadGroup)null, "PainterThread", paintable); + this(null, "PainterThread", paintable); } public PainterThread(ThreadGroup group, PainterThread.Paintable paintable) { @@ -35,31 +38,31 @@ public PainterThread(ThreadGroup group, String name, PainterThread.Paintable pai this.setDaemon(true); } - @Override public void run() { + @Override + public void run() { + while (this.isRunning) { - if (this.isRunning && !this.isInterrupted()) { - boolean b; - synchronized (this) { - b = this.pleaseRepaint; - this.pleaseRepaint = false; - } + boolean paint; + synchronized (this) { + paint = this.pleaseRepaint; + this.pleaseRepaint = false; + } - if (b) { - try { - this.paintable.paint(); - } catch (RejectedExecutionException var5) { - } + if (paint) { + try { + this.paintable.paint(); + } catch (RejectedExecutionException var5) { } + } - synchronized (this) { - try { - if (this.isRunning && !this.pleaseRepaint) { - this.wait(); - } - continue; - } catch (InterruptedException var7) { + synchronized (this) { + try { + if (this.isRunning && !this.pleaseRepaint) { + this.wait(); } + continue; + } catch (InterruptedException var7) { } } diff --git a/src/main/java/bdv/fx/viewer/render/RenderUnit.java b/src/main/java/bdv/fx/viewer/render/RenderUnit.java index 8d6f1455f..a926c80e6 100644 --- a/src/main/java/bdv/fx/viewer/render/RenderUnit.java +++ b/src/main/java/bdv/fx/viewer/render/RenderUnit.java @@ -13,10 +13,10 @@ import net.imglib2.FinalInterval; import net.imglib2.Interval; import net.imglib2.RealInterval; +import net.imglib2.parallel.TaskExecutor; import net.imglib2.realtransform.AffineTransform3D; import net.imglib2.type.numeric.ARGBType; import net.imglib2.util.Intervals; -import org.janelia.saalfeldlab.paintera.Paintera; import org.janelia.saalfeldlab.paintera.config.ScreenScalesConfig; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -24,7 +24,6 @@ import java.lang.invoke.MethodHandles; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.ExecutorService; import java.util.function.Function; import java.util.function.Supplier; @@ -35,7 +34,7 @@ public class RenderUnit implements PainterThread.Paintable { private static Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - private final long[] dimensions = {1, 1}; + private final long[] dimensions = {0, 0}; private final ObjectProperty screenScalesProperty = new SimpleObjectProperty<>(ScreenScalesConfig.defaultScreenScalesCopy()); @@ -59,9 +58,7 @@ public class RenderUnit implements PainterThread.Paintable { private final long targetRenderNanos; - private final int numRenderingThreads; - - private final ExecutorService renderingExecutorService; + private final TaskExecutor renderingTaskExecutor; private final List updateListeners = new ArrayList<>(); @@ -72,8 +69,7 @@ public RenderUnit( final AccumulateProjectorFactory accumulateProjectorFactory, final CacheControl cacheControl, final long targetRenderNanos, - final int numRenderingThreads, - final ExecutorService renderingExecutorService) { + final TaskExecutor renderingTaskExecutor) { this.threadGroup = threadGroup; this.viewerStateSupplier = viewerStateSupplier; @@ -81,12 +77,7 @@ public RenderUnit( this.accumulateProjectorFactory = accumulateProjectorFactory; this.cacheControl = cacheControl; this.targetRenderNanos = targetRenderNanos; - this.numRenderingThreads = numRenderingThreads; - this.renderingExecutorService = renderingExecutorService; - Paintera.whenPaintable(() -> { - update(); - requestRepaint(); - }); + this.renderingTaskExecutor = renderingTaskExecutor; } /** @@ -109,6 +100,7 @@ public void setDimensions(final long dimX, final long dimY) { */ public synchronized void requestRepaint(final int screenScaleIndex) { + if (renderer == null) return; renderer.requestRepaint(new FinalInterval(dimensions), screenScaleIndex); } @@ -116,7 +108,7 @@ public synchronized void requestRepaint(final int screenScaleIndex) { * Request repaint of the whole screen at highest possible resolution */ public synchronized void requestRepaint() { - + if (renderer == null) return; renderer.requestRepaint(new FinalInterval(dimensions)); } @@ -129,6 +121,7 @@ public synchronized void requestRepaint() { */ public synchronized void requestRepaint(final int screenScaleIndex, final long[] min, final long[] max) { + if (renderer == null) return; renderer.requestRepaint(clampRepaintInterval(new FinalInterval(min, max)), screenScaleIndex); } @@ -140,6 +133,7 @@ public synchronized void requestRepaint(final int screenScaleIndex, final long[] */ public synchronized void requestRepaint(final long[] min, final long[] max) { + if (renderer == null) return; renderer.requestRepaint(clampRepaintInterval(new FinalInterval(min, max))); } @@ -166,17 +160,13 @@ private synchronized void update() { LOG.debug("Updating render unit"); - if (painterThread != null) { - painterThread.stopRendering(); - painterThread.interrupt(); - } - renderTarget = new TransformAwareBufferedImageOverlayRendererFX(); - renderTarget.setCanvasSize((int)dimensions[0], (int)dimensions[1]); + renderTarget.setCanvasSize((int) dimensions[0], (int) dimensions[1]); - painterThread = new PainterThread(threadGroup, "painter-thread", this); - painterThread.setDaemon(true); - painterThread.start(); + if (painterThread == null || !painterThread.isAlive()) { + painterThread = new PainterThread(threadGroup, "painter-thread", this); + painterThread.start(); + } renderer = new MultiResolutionRendererFX( renderTarget, @@ -184,8 +174,7 @@ private synchronized void update() { screenScalesProperty.get(), targetRenderNanos, true, - numRenderingThreads, - renderingExecutorService, + renderingTaskExecutor, true, accumulateProjectorFactory, cacheControl @@ -220,11 +209,12 @@ public synchronized void getScreenScaleTransform(final int screenScaleIndex, fin renderer.getScreenScaleTransform(screenScaleIndex, screenScaleTransform); } + private AffineTransform3D viewerTransform = new AffineTransform3D(); + @Override public void paint() { final List> sacs = new ArrayList<>(); - final AffineTransform3D viewerTransform = new AffineTransform3D(); final int timepoint; synchronized (RenderUnit.this) { final ViewerState viewerState = this.viewerStateSupplier.get(); diff --git a/src/main/java/bdv/fx/viewer/render/TransformAwareBufferedImageOverlayRendererFX.java b/src/main/java/bdv/fx/viewer/render/TransformAwareBufferedImageOverlayRendererFX.java index 5e2b04a36..2003cb374 100644 --- a/src/main/java/bdv/fx/viewer/render/TransformAwareBufferedImageOverlayRendererFX.java +++ b/src/main/java/bdv/fx/viewer/render/TransformAwareBufferedImageOverlayRendererFX.java @@ -32,7 +32,6 @@ import bdv.viewer.TransformListener; import javafx.scene.image.Image; import net.imglib2.realtransform.AffineTransform3D; -import net.imglib2.type.numeric.ARGBType; import org.janelia.saalfeldlab.fx.util.InvokeOnJavaFXApplicationThread; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -72,25 +71,21 @@ public synchronized PixelBufferWritableImage setBufferedImageAndTransform( final PixelBufferWritableImage img, final AffineTransform3D transform) { - pendingTransform.set(transform); - return super.setBufferedImage(img); + synchronized (paintedTransform) { + pendingTransform.set(transform); + return super.setBufferedImage(img); + } } @Override public void drawOverlays(final Consumer g) { - boolean notifyTransformListeners = false; + boolean notifyTransformListeners = !paintedTransform.equals(pendingTransform); + final PixelBufferWritableImage sourceImage; synchronized (this) { - if (pending) { - final PixelBufferWritableImage tmp = bufferedImage; - bufferedImage = pendingImage; - paintedTransform.set(pendingTransform); - pendingImage = tmp; - pending = false; - notifyTransformListeners = true; - } + sourceImage = bufferedImage; + paintedTransform.set(pendingTransform); } - final PixelBufferWritableImage sourceImage = this.bufferedImage; if (sourceImage != null) { final boolean notify = notifyTransformListeners; InvokeOnJavaFXApplicationThread.invoke(() -> { @@ -111,9 +106,11 @@ public void drawOverlays(final Consumer g) { * * https://docs.oracle.com/javase/8/javafx/api/javafx/scene/effect/BlendMode.html */ - for (final ARGBType px : sourceImage.asArrayImg()) { - px.set(px.get() | FULL_OPACITY); - } + //FIXME: Pretty sure we don't want to be iterating over the entire render image AGAIN + // in the ovarlay renderer.... It has huge performance implications + //for (final ARGBType px : sourceImage.asArrayImg()) { + // px.set(px.get() | FULL_OPACITY); + //} sourceImage.setPixelsDirty(); g.accept(sourceImage); @@ -156,7 +153,6 @@ public void addTransformListener(final TransformListener list synchronized (paintedTransformListeners) { final int s = paintedTransformListeners.size(); paintedTransformListeners.add(index < 0 ? 0 : index > s ? s : index, listener); - listener.transformChanged(paintedTransform); } } diff --git a/src/main/java/net/imglib2/converter/ARGBColorConverter.java b/src/main/java/net/imglib2/converter/ARGBColorConverter.java index 47d884181..7d1fc236a 100644 --- a/src/main/java/net/imglib2/converter/ARGBColorConverter.java +++ b/src/main/java/net/imglib2/converter/ARGBColorConverter.java @@ -10,18 +10,17 @@ public abstract class ARGBColorConverter> implements ColorConverter, Converter { - protected final DoubleProperty alpha = new SimpleDoubleProperty(1.0); + /* Reading from properties is slow; use these, they are updated by a listener to the respective properties */ + private double alpha = 1.0; + private double min = 0.0; + private double max = 1.0; - protected final DoubleProperty min = new SimpleDoubleProperty(0.0); + private int color = ARGBType.rgba( 255, 255, 255, 255 ); - protected final DoubleProperty max = new SimpleDoubleProperty(1.0); - - protected final ObjectProperty color = new SimpleObjectProperty<>(new ARGBType(ARGBType.rgba( - 255, - 255, - 255, - 255 - ))); + protected final DoubleProperty alphaProperty = new SimpleDoubleProperty(alpha); + protected final DoubleProperty minProperty = new SimpleDoubleProperty(min); + protected final DoubleProperty maxProperty = new SimpleDoubleProperty(max); + protected final ObjectProperty colorProperty = new SimpleObjectProperty<>(new ARGBType(color)); protected int A; @@ -40,47 +39,61 @@ public ARGBColorConverter() { public ARGBColorConverter(final double min, final double max) { - this.min.set(min); - this.max.set(max); - - this.min.addListener((obs, oldv, newv) -> update()); - this.max.addListener((obs, oldv, newv) -> update()); - this.color.addListener((obs, oldv, newv) -> update()); - this.alpha.addListener((obs, oldv, newv) -> update()); + this.min = min; + this.max = max; + this.minProperty.set(min); + this.maxProperty.set(max); + + this.minProperty.addListener((obs, oldv, newv) -> { + this.min = newv.doubleValue(); + update(); + }); + this.maxProperty.addListener((obs, oldv, newv) -> { + this.max = newv.doubleValue(); + update(); + }); + this.alphaProperty.addListener((obs, oldv, newv) -> { + this.alpha = newv.doubleValue(); + update(); + }); + this.colorProperty.addListener((obs, oldv, newv) -> { + this.color = newv.get(); + update(); + }); update(); } public DoubleProperty minProperty() { - return min; + return minProperty; } public DoubleProperty maxProperty() { - return max; + return maxProperty; } public ObjectProperty colorProperty() { - return color; + return colorProperty; } public DoubleProperty alphaProperty() { - return this.alpha; + return alphaProperty; } @Override public ARGBType getColor() { - return color.get().copy(); + return new ARGBType(color); } @Override public void setColor(final ARGBType c) { - color.set(c); + colorProperty.set(c); } @Override @@ -92,32 +105,32 @@ public boolean supportsColor() { @Override public double getMin() { - return min.get(); + return min; } @Override public double getMax() { - return max.get(); + return max; } @Override public void setMax(final double max) { - this.max.set(max); + this.maxProperty.set(max); } @Override public void setMin(final double min) { - this.min.set(min); + this.minProperty.set(min); } private void update() { - final double scale = 1.0 / (max.get() - min.get()); - final int value = color.get().get(); - A = (int)Math.min(Math.max(Math.round(255 * alphaProperty().get()), 0), 255); + final double scale = 1.0 / (max - min); + final int value = color; + A = (int)Math.min(Math.max(Math.round(255 * alpha), 0), 255); scaleR = ARGBType.red(value) * scale; scaleG = ARGBType.green(value) * scale; scaleB = ARGBType.blue(value) * scale; @@ -139,7 +152,7 @@ public Imp0(final double min, final double max) { @Override public void convert(final R input, final ARGBType output) { - final double v = input.getRealDouble() - min.get(); + final double v = input.getRealDouble() - getMin(); if (v < 0) { output.set(black); } else { @@ -169,7 +182,7 @@ public Imp1(final double min, final double max) { @Override public void convert(final R input, final ARGBType output) { - final double v = input.getRealDouble() - min.get(); + final double v = input.getRealDouble() - getMin(); if (v < 0) { output.set(black); } else { @@ -199,7 +212,7 @@ public InvertingImp0(final double min, final double max) { @Override public void convert(final R input, final ARGBType output) { - final double v = input.getRealDouble() - min.get(); + final double v = input.getRealDouble() - getMin(); final int r0 = (int)(scaleR * v + 0.5); final int g0 = (int)(scaleG * v + 0.5); final int b0 = (int)(scaleB * v + 0.5); @@ -226,7 +239,7 @@ public InvertingImp1(final double min, final double max) { @Override public void convert(final R input, final ARGBType output) { - final double v = input.getRealDouble() - min.get(); + final double v = input.getRealDouble() - getMin(); final int r0 = (int)(scaleR * v + 0.5); final int g0 = (int)(scaleG * v + 0.5); final int b0 = (int)(scaleB * v + 0.5); diff --git a/src/main/java/net/imglib2/converter/RealRandomArrayAccess.java b/src/main/java/net/imglib2/converter/RealRandomArrayAccess.java deleted file mode 100644 index 92e605467..000000000 --- a/src/main/java/net/imglib2/converter/RealRandomArrayAccess.java +++ /dev/null @@ -1,2 +0,0 @@ -package net.imglib2.converter; - diff --git a/src/main/java/net/imglib2/converter/RealRandomArrayAccessible.java b/src/main/java/net/imglib2/converter/RealRandomArrayAccessible.java deleted file mode 100644 index a27638bd4..000000000 --- a/src/main/java/net/imglib2/converter/RealRandomArrayAccessible.java +++ /dev/null @@ -1,301 +0,0 @@ -package net.imglib2.converter; - -import net.imglib2.Localizable; -import net.imglib2.RealInterval; -import net.imglib2.RealLocalizable; -import net.imglib2.RealRandomAccess; -import net.imglib2.RealRandomAccessible; -import net.imglib2.type.Type; - -import java.lang.reflect.Array; -import java.util.List; -import java.util.function.BiConsumer; -import java.util.stream.Collectors; - -public class RealRandomArrayAccessible> implements RealRandomAccessible { - - final protected List> inputs; - private final BiConsumer reducer; - private final T result; - - public RealRandomArrayAccessible(List> inputs, BiConsumer reducer, T result) { - - this.inputs = inputs; - this.reducer = reducer; - this.result = result.copy(); - } - - @Override - public int numDimensions() { - - return inputs.get(0).numDimensions(); - } - - @Override - public RealRandomArrayAccess realRandomAccess() { - - return new RealRandomArrayAccess(realRandomAccessList(), this.reducer, this.result); - } - - private List> realRandomAccessList() { - - return this.inputs.stream().map(RealRandomAccessible::realRandomAccess).collect(Collectors.toList()); - } - - @Override - public RealRandomArrayAccess realRandomAccess(final RealInterval interval) { - - return new RealRandomArrayAccess(realRandomAccessList(), this.reducer, this.result); - } - - public class RealRandomArrayAccess implements net.imglib2.RealRandomAccess { - - final protected List> inputs; - private final BiConsumer reducer; - private final T result; - - public RealRandomArrayAccess(final List> inputs, final BiConsumer reducer, final T result) { - - this.inputs = inputs; - this.reducer = reducer; - this.result = result.copy(); - } - - @Override - public void localize(final float[] position) { - - for (RealRandomAccess input : inputs) { - input.localize(position); - } - } - - @Override - public void localize(final double[] position) { - - for (RealRandomAccess input : inputs) { - input.localize(position); - } - } - - @Override - public float getFloatPosition(final int d) { - - return inputs.get(0).getFloatPosition(d); - } - - @Override - public double getDoublePosition(final int d) { - - return inputs.get(0).getDoublePosition(d); - } - - @Override - public void fwd(final int d) { - - for (RealRandomAccess input : inputs) { - input.fwd(d); - } - } - - @Override - public void bck(final int d) { - - for (RealRandomAccess input : inputs) { - input.bck(d); - } - } - - @Override - public void move(final int distance, final int d) { - - for (RealRandomAccess input : inputs) { - input.move(distance, d); - } - } - - @Override - public void move(final long distance, final int d) { - - for (RealRandomAccess input : inputs) { - input.move(distance, d); - } - } - - @Override - public void move(final float distance, final int d) { - - for (RealRandomAccess input : inputs) { - input.move(distance, d); - } - } - - @Override - public void move(final double distance, final int d) { - - for (RealRandomAccess input : inputs) { - input.move(distance, d); - } - } - - @Override - public void move(final Localizable localizable) { - - for (RealRandomAccess input : inputs) { - input.move(localizable); - } - } - - @Override - public void move(final int[] distance) { - - for (RealRandomAccess input : inputs) { - input.move(distance); - } - } - - @Override - public void move(final long[] distance) { - - for (RealRandomAccess input : inputs) { - input.move(distance); - } - } - - @Override - public void move(final RealLocalizable distance) { - - for (RealRandomAccess input : inputs) { - input.move(distance); - } - } - - @Override - public void move(final float[] distance) { - - for (RealRandomAccess input : inputs) { - input.move(distance); - } - } - - @Override - public void move(final double[] distance) { - - for (RealRandomAccess input : inputs) { - input.move(distance); - } - } - - @Override - public void setPosition(final Localizable localizable) { - - for (RealRandomAccess input : inputs) { - input.setPosition(localizable); - } - } - - @Override - public void setPosition(final int[] position) { - - for (RealRandomAccess input : inputs) { - input.setPosition(position); - } - } - - @Override - public void setPosition(final long[] position) { - - for (RealRandomAccess input : inputs) { - input.setPosition(position); - } - } - - @Override - public void setPosition(final float[] position) { - - for (RealRandomAccess input : inputs) { - input.setPosition(position); - } - } - - @Override - public void setPosition(final double[] position) { - - for (RealRandomAccess input : inputs) { - input.setPosition(position); - } - } - - @Override - public void setPosition(final RealLocalizable position) { - - for (RealRandomAccess input : inputs) { - input.setPosition(position); - } - } - - @Override - public void setPosition(final int position, final int d) { - - for (RealRandomAccess input : inputs) { - input.setPosition(position, d); - } - } - - @Override - public void setPosition(final long position, final int d) { - - for (RealRandomAccess input : inputs) { - input.setPosition(position, d); - } - } - - @Override - public void setPosition(final float position, final int d) { - - for (RealRandomAccess input : inputs) { - input.setPosition(position, d); - } - } - - @Override - public void setPosition(final double position, final int d) { - - for (RealRandomAccess input : inputs) { - input.setPosition(position, d); - } - } - - @Override - public T get() { - - @SuppressWarnings("unchecked") - final T[] tList = (T[]) Array.newInstance(result.getClass(), inputs.size()); - for (int i = 0; i < inputs.size(); i++) { - tList[i] = inputs.get(i).get(); - } - this.reducer.accept(tList, result); - return result; - } - - @Override - public RealRandomArrayAccess copy() { - - final RealRandomArrayAccess copy = new RealRandomArrayAccess(this.inputs, this.reducer, this.result.copy()); - copy.setPosition(this); - return copy; - } - - @Override - public int numDimensions() { - - return inputs.get(0).numDimensions(); - } - - @Override - public RealRandomAccess copyRealRandomAccess() { - - return copy(); - } - } - -} diff --git a/src/main/java/net/imglib2/util/AccessedBlocksRandomAccessible.java b/src/main/java/net/imglib2/util/AccessedBlocksRandomAccessible.java index 7191ca544..da34650b3 100644 --- a/src/main/java/net/imglib2/util/AccessedBlocksRandomAccessible.java +++ b/src/main/java/net/imglib2/util/AccessedBlocksRandomAccessible.java @@ -44,7 +44,7 @@ public AccessedBlocksRandomAccessible(final RandomAccessibleInterval source, this( source, - IntStream.range(0, grid.numDimensions()).map(grid::cellDimension).toArray(), + grid.getCellDimensions(), grid.getGridDimensions() ); } @@ -60,17 +60,22 @@ public AccessedBlocksRandomAccessible(final RandomAccessibleInterval source, public void clear() { - this.visitedBlocks.clear(); + synchronized (visitedBlocks) { + visitedBlocks.clear(); + } } protected void addBlockId(final long id) { - this.visitedBlocks.add(id); + synchronized (visitedBlocks) { + visitedBlocks.add(id); + } } public long[] listBlocks() { - - return visitedBlocks.toArray(); + synchronized (visitedBlocks) { + return visitedBlocks.toArray(); + } } public CellGrid getGrid() { 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 7897d613b..80c242587 100644 --- a/src/main/java/org/janelia/saalfeldlab/fx/ortho/OrthogonalViews.java +++ b/src/main/java/org/janelia/saalfeldlab/fx/ortho/OrthogonalViews.java @@ -12,6 +12,8 @@ import javafx.scene.Node; import javafx.scene.effect.ColorAdjust; import net.imglib2.RealInterval; +import net.imglib2.parallel.TaskExecutor; +import net.imglib2.parallel.TaskExecutors; import net.imglib2.realtransform.AffineTransform3D; import org.janelia.saalfeldlab.paintera.control.navigation.AffineTransformWithListeners; import org.janelia.saalfeldlab.paintera.control.navigation.TransformConcatenator; @@ -22,6 +24,8 @@ import java.lang.invoke.MethodHandles; import java.util.Collection; import java.util.List; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.ForkJoinWorkerThread; import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Collectors; @@ -36,6 +40,12 @@ public class OrthogonalViews
{ private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + private final TaskExecutor renderingTaskExecutor; + + public void stop() { + renderingTaskExecutor.close(); + } + /** * Utility class that holds {@link ViewerPanelFX} and related objects. */ @@ -133,9 +143,20 @@ public OrthogonalViews( final Function, Interpolation> interpolation) { this.manager = manager; - this.topLeft = create(this.manager, cacheControl, optional, ViewerAxis.Z, interpolation); - this.topRight = create(this.manager, cacheControl, optional, ViewerAxis.X, interpolation); - this.bottomLeft = create(this.manager, cacheControl, optional, ViewerAxis.Y, interpolation); + + final ForkJoinPool.ForkJoinWorkerThreadFactory factory = pool -> { + final ForkJoinWorkerThread worker = ForkJoinPool.defaultForkJoinWorkerThreadFactory.newThread(pool); + worker.setDaemon(true); + worker.setPriority(4); + worker.setName("render-thread-" + worker.getPoolIndex()); + return worker; + }; + + final ForkJoinPool rendererService = new ForkJoinPool(optional.values.getNumRenderingThreads(), factory, null, false); + this.renderingTaskExecutor = TaskExecutors.forExecutorService(rendererService); + this.topLeft = create(this.manager, cacheControl, optional, ViewerAxis.Z, interpolation, renderingTaskExecutor); + this.topRight = create(this.manager, cacheControl, optional, ViewerAxis.X, interpolation, renderingTaskExecutor); + this.bottomLeft = create(this.manager, cacheControl, optional, ViewerAxis.Y, interpolation, renderingTaskExecutor); this.bottomRight = bottomRight; this.pane = new DynamicCellPane(); resetPane(); @@ -248,8 +269,7 @@ public void disableView(final ViewerPanelFX viewer) { viewer.setFocusable(false); final var grayedOut = new ColorAdjust(); - grayedOut.setContrast(-0.2); - grayedOut.setBrightness(-0.5); + grayedOut.setBrightness(-0.3); viewer.setEffect(grayedOut); } @@ -292,7 +312,8 @@ private static ViewerAndTransforms create( final CacheControl cacheControl, final ViewerOptions optional, final ViewerAxis axis, - final Function, Interpolation> interpolation) { + final Function, Interpolation> interpolation, + final TaskExecutor taskExecutor) { final AffineTransform3D globalToViewer = ViewerAxis.globalToViewer(axis); LOG.debug("Generating viewer, axis={}, globalToViewer={}", axis, globalToViewer); @@ -300,7 +321,8 @@ private static ViewerAndTransforms create( 1, cacheControl, optional, - interpolation + interpolation, + taskExecutor ); final AffineTransformWithListeners displayTransform = new AffineTransformWithListeners(); final AffineTransformWithListeners globalToViewerTransform = new AffineTransformWithListeners(globalToViewer); diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/PainteraBaseView.java b/src/main/java/org/janelia/saalfeldlab/paintera/PainteraBaseView.java index eb3f2a564..615eae50b 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/PainteraBaseView.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/PainteraBaseView.java @@ -59,6 +59,8 @@ import java.util.Optional; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.ForkJoinWorkerThread; /** * Contains all the things necessary to build a Paintera UI, most importantly: @@ -116,7 +118,20 @@ public class PainteraBaseView { private final ExecutorService paintQueue = Executors.newFixedThreadPool(1); - private final ExecutorService propagationQueue = Executors.newFixedThreadPool(1); + private final ExecutorService propagationQueue; + + { + final ForkJoinPool.ForkJoinWorkerThreadFactory factory = pool -> { + final ForkJoinWorkerThread worker = ForkJoinPool.defaultForkJoinWorkerThreadFactory.newThread(pool); + worker.setDaemon(true); + worker.setPriority(4); + worker.setName("propagation-queue-" + worker.getPoolIndex()); + return worker; + }; + + propagationQueue = new ForkJoinPool(Math.max(1, Runtime.getRuntime().availableProcessors() - 2), factory, null, false); + + } private final SharedQueue sharedQueue; @@ -150,7 +165,7 @@ public PainteraBaseView( this.keyAndMouseBindings = keyAndMouseBindings; this.viewerOptions = viewerOptions .accumulateProjectorFactory(new CompositeProjectorPreMultiply.CompositeProjectorFactory(sourceInfo.composites())) - .numRenderingThreads(Math.min(3, Math.max(1, Runtime.getRuntime().availableProcessors() / 3))); + .numRenderingThreads(Math.max(1, Runtime.getRuntime().availableProcessors() - 2)); this.views = new OrthogonalViews<>( manager, this.sharedQueue, @@ -373,7 +388,7 @@ public & NativeType, T extends AbstractVolatileNativeR ); InvokeOnJavaFXApplicationThread.invoke(() -> addState(state)); state.converter().setMin(min); - state.converter().setMin(max); + state.converter().setMax(max); return state; } @@ -523,9 +538,7 @@ public void stop() { this.paintQueue.shutdown(); this.propagationQueue.shutdown(); - this.orthogonalViews().getTopLeft().viewer().stop(); - this.orthogonalViews().getTopRight().viewer().stop(); - this.orthogonalViews().getBottomLeft().viewer().stop(); + this.orthogonalViews().stop(); } /** diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/PainteraCommandLineArgs.java b/src/main/java/org/janelia/saalfeldlab/paintera/PainteraCommandLineArgs.java index 6e40142e7..8f3a907c3 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/PainteraCommandLineArgs.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/PainteraCommandLineArgs.java @@ -76,8 +76,6 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import static org.janelia.saalfeldlab.util.n5.N5Helpers.getReaderOrWriterIfN5ContainerExists; - @Command(name = "Paintera", showDefaultValues = true, resourceBundle = "org.janelia.saalfeldlab.paintera.PainteraCommandLineArgs", usageHelpWidth = 120, parameterListHeading = "%n@|bold,underline Parameters|@:%n", optionListHeading = "%n@|bold,underline Options|@:%n") @@ -649,7 +647,7 @@ private void addToViewer(final PainteraBaseView viewer, final Supplier p for (final String container : containers) { LOG.debug("Adding datasets for container {}", container); - N5Reader n5Container = getReaderOrWriterIfN5ContainerExists(container); + N5Reader n5Container = Paintera.getN5Factory().openWriterElseOpenReader(container); final Predicate datasetFilter = options.useDataset(); final ExecutorService es = getDiscoveryExecutorService(); diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/ProjectDirectoryNotSpecifiedDialog.java b/src/main/java/org/janelia/saalfeldlab/paintera/ProjectDirectoryNotSpecifiedDialog.java index ce602e83b..a5ee29902 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/ProjectDirectoryNotSpecifiedDialog.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/ProjectDirectoryNotSpecifiedDialog.java @@ -16,6 +16,7 @@ import java.io.File; import java.lang.invoke.MethodHandles; +import java.nio.file.Path; import java.util.Optional; public class ProjectDirectoryNotSpecifiedDialog { @@ -32,8 +33,10 @@ public ProjectDirectoryNotSpecifiedDialog(final boolean defaultToTempDirectory) public Optional showDialog(final String contentText) throws ProjectDirectoryNotSpecified { + final String currentTempDir = Paintera.getPaintera().getProperties().getPainteraDirectoriesConfig().getTmpDir(); + final Path tempProjectPath = Path.of(currentTempDir); if (this.defaultToTempDirectory) { - return Optional.of(tmpDir()); + return Optional.of(temporaryProjectDir(tempProjectPath)); } final StringProperty projectDirectory = new SimpleStringProperty(null); @@ -43,11 +46,12 @@ public Optional showDialog(final String contentText) throws ProjectDirec final Dialog dialog = new Dialog<>(); dialog.setResultConverter(bt -> { - return ButtonType.CANCEL.equals(bt) - ? null - : noProject.equals(bt) - ? tmpDir() - : projectDirectory.get(); + if (ButtonType.CANCEL.equals(bt)) + return null; + else if (noProject.equals(bt)) + return temporaryProjectDir(tempProjectPath); + else + return projectDirectory.get(); }); dialog.getDialogPane().getButtonTypes().setAll(specifyProject, noProject, ButtonType.CANCEL); @@ -95,11 +99,10 @@ public Optional showDialog(final String contentText) throws ProjectDirec } - private static String tmpDir() { - // TODO read tmp directory and prefix from ~/.paintera/config if present - final String tmpDir = new TmpDirectoryCreator(() -> null, "paintera-project-").get(); - LOG.info("Using temporary project directory {}", tmpDir); - return tmpDir; + private static String temporaryProjectDir(final Path projectTempDir) { + final String tempProjectDir = new TmpDirectoryCreator(projectTempDir, "paintera-project-").get(); + LOG.info("Using temporary project directory {}", tempProjectDir); + return tempProjectDir; } } diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/composition/ClearingCompositeProjector.java b/src/main/java/org/janelia/saalfeldlab/paintera/composition/ClearingCompositeProjector.java index 9703a1738..591960e14 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/composition/ClearingCompositeProjector.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/composition/ClearingCompositeProjector.java @@ -1,6 +1,7 @@ package org.janelia.saalfeldlab.paintera.composition; import bdv.viewer.Source; +import bdv.viewer.SourceAndConverter; import bdv.viewer.render.AccumulateProjectorFactory; import bdv.viewer.render.VolatileProjector; import net.imglib2.Cursor; @@ -9,6 +10,7 @@ import net.imglib2.type.Type; import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.concurrent.ExecutorService; @@ -32,26 +34,24 @@ public ClearingCompositeProjectorFactory(final Map, Composite> c } @Override - public VolatileProjector createAccumulateProjector( - final ArrayList sourceProjectors, - final ArrayList> sources, - final ArrayList> sourceScreenImages, - final RandomAccessibleInterval
targetScreenImage, - final int numThreads, - final ExecutorService executorService) { + public VolatileProjector createProjector( + List sourceProjectors, + List> sources, + List> sourceScreenImages, + RandomAccessibleInterval targetScreenImage, + int numThreads, + ExecutorService executorService) { final ClearingCompositeProjector projector = new ClearingCompositeProjector<>( sourceProjectors, sourceScreenImages, targetScreenImage, - clearValue, - numThreads, - executorService + clearValue ); final ArrayList> activeComposites = new ArrayList<>(); - for (final Source activeSource : sources) { - activeComposites.add(composites.get(activeSource)); + for (final var activeSource : sources) { + activeComposites.add(composites.get(activeSource.getSpimSource())); } projector.setComposites(activeComposites); @@ -63,14 +63,12 @@ public VolatileProjector createAccumulateProjector( private final A clearValue; public ClearingCompositeProjector( - final ArrayList sourceProjectors, - final ArrayList> sources, + final List sourceProjectors, + final List> sources, final RandomAccessibleInterval target, - final A clearValue, - final int numThreads, - final ExecutorService executorService) { + final A clearValue) { - super(sourceProjectors, sources, target, numThreads, executorService); + super(sourceProjectors, sources, target); this.clearValue = clearValue; } diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/composition/CompositeProjector.java b/src/main/java/org/janelia/saalfeldlab/paintera/composition/CompositeProjector.java index 436c06212..8b630e6d6 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/composition/CompositeProjector.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/composition/CompositeProjector.java @@ -1,6 +1,7 @@ package org.janelia.saalfeldlab.paintera.composition; import bdv.viewer.Source; +import bdv.viewer.SourceAndConverter; import bdv.viewer.render.AccumulateProjector; import bdv.viewer.render.AccumulateProjectorFactory; import bdv.viewer.render.VolatileProjector; @@ -34,25 +35,23 @@ public CompositeProjectorFactory(final Map, Composite> composite } @Override - public VolatileProjector createAccumulateProjector( - final ArrayList sourceProjectors, - final ArrayList> sources, - final ArrayList> sourceScreenImages, - final RandomAccessibleInterval targetScreenImage, - final int numThreads, - final ExecutorService executorService) { + public VolatileProjector createProjector( + List sourceProjectors, + List> sources, + List> sourceScreenImages, + RandomAccessibleInterval targetScreenImage, + int numThreads, + ExecutorService executorService) { final CompositeProjector projector = new CompositeProjector<>( sourceProjectors, sourceScreenImages, - targetScreenImage, - numThreads, - executorService + targetScreenImage ); final ArrayList> activeComposites = new ArrayList<>(); - for (final Source activeSource : sources) { - activeComposites.add(composites.get(activeSource)); + for (final var activeSource : sources) { + activeComposites.add(composites.get(activeSource.getSpimSource())); } projector.setComposites(activeComposites); @@ -64,13 +63,11 @@ public VolatileProjector createAccumulateProjector( final protected ArrayList> composites = new ArrayList<>(); public CompositeProjector( - final ArrayList sourceProjectors, - final ArrayList> sources, - final RandomAccessibleInterval target, - final int numThreads, - final ExecutorService executorService) { + final List sourceProjectors, + final List> sources, + final RandomAccessibleInterval target) { - super(sourceProjectors, sources, target, numThreads, executorService); + super(sourceProjectors, sources, target); } public void setComposites(final List> composites) { diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/composition/CompositeProjectorPreMultiply.java b/src/main/java/org/janelia/saalfeldlab/paintera/composition/CompositeProjectorPreMultiply.java index ee876a714..5ee059235 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/composition/CompositeProjectorPreMultiply.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/composition/CompositeProjectorPreMultiply.java @@ -1,6 +1,7 @@ package org.janelia.saalfeldlab.paintera.composition; import bdv.viewer.Source; +import bdv.viewer.SourceAndConverter; import bdv.viewer.render.AccumulateProjector; import bdv.viewer.render.AccumulateProjectorFactory; import bdv.viewer.render.VolatileProjector; @@ -42,10 +43,10 @@ public CompositeProjectorFactory(final Map, Composite sourceProjectors, - final ArrayList> sources, - final ArrayList> sourceScreenImages, + public VolatileProjector createProjector( + final List sourceProjectors, + final List> sources, + final List> sourceScreenImages, final RandomAccessibleInterval targetScreenImage, final int numThreads, final ExecutorService executorService) { @@ -53,14 +54,12 @@ public VolatileProjector createAccumulateProjector( final CompositeProjectorPreMultiply projector = new CompositeProjectorPreMultiply( sourceProjectors, sourceScreenImages, - targetScreenImage, - numThreads, - executorService + targetScreenImage ); final ArrayList> activeComposites = new ArrayList<>(); - for (final Source activeSource : sources) { - activeComposites.add(composites.get(activeSource)); + for (final var activeSource : sources) { + activeComposites.add(composites.get(activeSource.getSpimSource())); } projector.setComposites(activeComposites); @@ -72,13 +71,11 @@ public VolatileProjector createAccumulateProjector( final protected ArrayList> composites = new ArrayList<>(); public CompositeProjectorPreMultiply( - final ArrayList sourceProjectors, - final ArrayList> sources, - final RandomAccessibleInterval target, - final int numThreads, - final ExecutorService executorService) { + final List sourceProjectors, + final List> sources, + final RandomAccessibleInterval target) { - super(sourceProjectors, sources, target, numThreads, executorService); + super(sourceProjectors, sources, target); LOG.debug("Creating {}", this.getClass().getName()); } diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/config/BookmarkSelectionDialog.java b/src/main/java/org/janelia/saalfeldlab/paintera/config/BookmarkSelectionDialog.java index 112416e97..fae484a30 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/config/BookmarkSelectionDialog.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/config/BookmarkSelectionDialog.java @@ -167,7 +167,7 @@ private BookmarkWithFuzzyScore( this.bookmark = bookmark; this.fuzzyQuery = fuzzyQuery; - this.score = scorer.apply(fuzzyQuery, bookmark.getNote() == null ? "" : bookmark.getNote());//.replace("\n", " ")); + this.score = scorer.apply(fuzzyQuery, bookmark.getNote() == null ? "" : bookmark.getNote()); } public BookmarkConfig.Bookmark getBookmark() { diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/IdSelector.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/IdSelector.java index fc1379387..163b09c88 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/IdSelector.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/IdSelector.java @@ -96,7 +96,7 @@ private void selectAllLabelMultisetType(final TLongSet allIds) { return; } final LabelMultisetType lmt = cursor.next(); - for (LabelMultisetEntry iterEntry : lmt.entrySetWithRef(entry)) { + for (Entry

P viewerToSourceCo return location; } - private UtilityTask fill( + private UtilityTask fill( final int time, final int level, final long fill, @@ -181,28 +189,60 @@ private UtilityTask fill( level ); final SourceMask mask = source.generateMask(maskInfo, MaskedSource.VALID_LABEL_CHECK); - final AccessBoxRandomAccessible accessTracker = - new AccessBoxRandomAccessible<>(Views.extendValue(mask.getRai(), new UnsignedLongType(1))); + final AffineTransform3D globalToSource = source.getSourceTransformForMask(maskInfo).inverse(); + + final List visibleSourceIntervals = Paintera.getPaintera().getBaseView().orthogonalViews().views().stream() + .filter(it -> it.isVisible() && it.getWidth() > 0.0 && it.getHeight() > 0.0) + .map(ViewerMask::getGlobalViewerInterval) + .map(globalToSource::estimateBounds) + .map(Intervals::smallestContainingInterval) + .collect(Collectors.toList()); + + final AtomicBoolean triggerRefresh = new AtomicBoolean(false); + final AccessBoxRandomAccessible accessTracker = new AccessBoxRandomAccessible<>(Views.extendValue(mask.getRai(), new UnsignedLongType(1))) { + + final Point position = new Point(sourceAccess.numDimensions()); - final var floodFillTask = Tasks.createTask((Function, Boolean>)task -> { + @Override + public UnsignedLongType get() { + if (Thread.currentThread().isInterrupted()) + throw new RuntimeException("Flood Fill Interrupted"); + synchronized (this) { + updateAccessBox(); + } + sourceAccess.localize(position); + if (!triggerRefresh.get()) { + for (RealInterval interval : visibleSourceIntervals) { + if (Intervals.contains(interval, position)) { + triggerRefresh.set(true); + break; + } + } + } + return sourceAccess.get(); + } + }; + + final UtilityTask floodFillTask = Tasks.createTask(task -> { if (seedValue instanceof LabelMultisetType) { - fillMultisetType((RandomAccessibleInterval)data, accessTracker, seed, seedLabel, fill, assignment); + fillMultisetType((RandomAccessibleInterval) data, accessTracker, seed, seedLabel, fill, assignment); } else { fillPrimitiveType(data, accessTracker, seed, seedLabel, fill, assignment); } - return true; - }).onCancelled((state, task) -> { + } + ).onCancelled((state, task) -> { try { source.resetMasks(); } catch (final MaskInUse e) { e.printStackTrace(); } - }) - .onFailed((event, task) -> { + } + ).onFailed((event, task) -> { if (!Thread.currentThread().isInterrupted() && task.getException() != null && !(task.getException() instanceof CancellationException)) { throw new RuntimeException(task.getException()); } - }).onSuccess((state, task) -> { + } + ).onSuccess((state, task) -> { LOG.debug(Thread.currentThread().isInterrupted() ? "FloodFill has been interrupted" : "FloodFill has been completed"); final Interval interval = accessTracker.createAccessInterval(); @@ -212,32 +252,38 @@ private UtilityTask fill( Arrays.toString(Intervals.maxAsLongArray(interval)) ); source.applyMask(mask, interval, MaskedSource.VALID_LABEL_CHECK); - }).onEnd(task -> requestRepaint.run()) - .submit(); - - final var floodFillResultCheckerThread = new Thread(() -> { - while (!floodFillTask.isDone()) { - try { - Thread.sleep(100); - } catch (final InterruptedException e) { - Thread.currentThread().interrupt(); // restore interrupted status } + ); - if (Thread.currentThread().isInterrupted()) - break; + final var refreshAnimation = new AnimationTimer() { - LOG.trace("Updating View for FloodFill "); - requestRepaint.run(); - } + final static long delay = 2_000_000_000; // 2 second delay before refreshes start + final static long REFRESH_RATE = 1_000_000_000; + final long start = System.nanoTime(); + long before = start; - if (Thread.interrupted()) { - floodFillTask.cancel(); + @Override + public void handle(long now) { + if (now - start < delay || now - before < REFRESH_RATE) return; + if (!floodFillTask.isCancelled() && triggerRefresh.get()) { + requestRepaint.accept(accessTracker.createAccessInterval()); + before = now; + triggerRefresh.set(false); + } } - }); + }; - setFloodFillState(source, new FloodFillState(fill, floodFillResultCheckerThread::interrupt)); - floodFillResultCheckerThread.start(); + floodFillTask.onEnd(task -> { + refreshAnimation.stop(); + requestRepaint.accept(accessTracker.createAccessInterval()); + }); + + if (floodFillExector.isShutdown()) { + floodFillExector = newFloodFillExecutor(); + } + refreshAnimation.start(); + floodFillTask.submit(floodFillExector); return floodFillTask; } @@ -280,20 +326,28 @@ private static > void fillPrimitiveType( ); } - private void setFloodFillState(final Source source, final FloodFillState state) { - - setFloodFillState.accept(state); - } + private static > BiPredicate makePredicate(final long seedLabel, final FragmentSegmentAssignment assignment) { - private void resetFloodFillState(final Source source) { - - setFloodFillState(source, null); - } + final Long singleFragment; + final TLongHashSet seedFragments; + if (assignment != null) { + seedFragments = assignment.getFragments(seedLabel); + singleFragment = seedFragments.size() == 1 ? seedFragments.toArray()[0] : null; + } else { + singleFragment = seedLabel; + seedFragments = null; + } - private static > BiPredicate makePredicate(final long id, final FragmentSegmentAssignment assignment) { + return (sourceVal, targetVal) -> { + if (Thread.currentThread().isInterrupted()) return false; + /* true if sourceFragment is a seedFragment */ + final long sourceFragment = sourceVal.getIntegerLong(); + final var shouldFill = singleFragment != null ? singleFragment == sourceFragment : seedFragments.contains(sourceFragment); + /* Most target vals are typically invalid, so this is rarely not passed; The sourceMatch filter is likely to + * shortcircuit more often */ + return shouldFill && targetVal.getInteger() == Label.INVALID; + }; - return (t, u) -> !Thread.currentThread().isInterrupted() && u.getInteger() == Label.INVALID - && (assignment != null ? assignment.getSegment(t.getIntegerLong()) : t.getIntegerLong()) == id; } public static class RunAll implements Runnable { @@ -359,7 +413,7 @@ public static Interval trackedFill( coordinates.add(seed.getLongPosition(d)); } - final int cleanupThreshold = n * (int)1e5; + final int cleanupThreshold = n * (int) 1e5; final RandomAccessible>> neighborhood = shape.neighborhoodsRandomAccessible(paired); final RandomAccess>> neighborhoodAccess = neighborhood.randomAccess(); diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill2D.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill2D.java index 0caba1c2c..7c2278c1d 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill2D.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill2D.java @@ -1,12 +1,12 @@ package org.janelia.saalfeldlab.paintera.control.paint; import bdv.fx.viewer.ViewerPanelFX; +import javafx.animation.AnimationTimer; import javafx.beans.property.DoubleProperty; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.beans.property.SimpleDoubleProperty; import javafx.beans.value.ObservableValue; -import javafx.concurrent.Task; import net.imglib2.FinalInterval; import net.imglib2.Interval; import net.imglib2.Point; @@ -21,7 +21,6 @@ import net.imglib2.type.label.Label; import net.imglib2.type.logic.BoolType; import net.imglib2.type.numeric.IntegerType; -import net.imglib2.type.numeric.RealType; import net.imglib2.type.numeric.integer.UnsignedLongType; import net.imglib2.util.AccessBoxRandomAccessibleOnGet; import net.imglib2.util.Intervals; @@ -30,13 +29,13 @@ import org.janelia.saalfeldlab.fx.Tasks; import org.janelia.saalfeldlab.fx.UtilityTask; import org.janelia.saalfeldlab.fx.ui.Exceptions; -import org.janelia.saalfeldlab.fx.util.InvokeOnJavaFXApplicationThread; import org.janelia.saalfeldlab.paintera.Paintera; import org.janelia.saalfeldlab.paintera.control.assignment.FragmentSegmentAssignment; import org.janelia.saalfeldlab.paintera.data.mask.MaskInfo; import org.janelia.saalfeldlab.paintera.data.mask.MaskedSource; import org.janelia.saalfeldlab.paintera.data.mask.SourceMask; import org.janelia.saalfeldlab.paintera.data.mask.exception.MaskInUse; +import org.janelia.saalfeldlab.util.NamedThreadFactory; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; @@ -45,6 +44,9 @@ import java.lang.invoke.MethodHandles; import java.util.Arrays; import java.util.Objects; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.BooleanSupplier; import java.util.function.Predicate; @@ -52,6 +54,14 @@ public class FloodFill2D> { private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + private static final Predicate FOREGROUND_CHECK = Label::isForeground; + + private static ExecutorService floodFillExector = newFloodFillExecutor(); + + private static ExecutorService newFloodFillExecutor() { + return Executors.newFixedThreadPool(Math.min(Runtime.getRuntime().availableProcessors() - 1, 1), new NamedThreadFactory("flood-fill-2d", true, 8)); + } + private final ObservableValue activeViewerProperty; private final MaskedSource source; @@ -59,7 +69,6 @@ public class FloodFill2D> { private final SimpleDoubleProperty fillDepth = new SimpleDoubleProperty(2.0); - private static final Predicate FOREGROUND_CHECK = it -> Label.isForeground(it.get()); private ViewerMask viewerMask; @@ -85,6 +94,10 @@ public void provideMask(@NotNull ViewerMask mask) { this.viewerMask = mask; } + public ViewerMask getMask() { + return viewerMask; + } + public void release() { this.viewerMask = null; @@ -101,7 +114,7 @@ public Interval getMaskInterval() { return this.maskIntervalProperty.get(); } - public UtilityTask fillViewerAt(final double viewerSeedX, final double viewerSeedY, Long fill, FragmentSegmentAssignment assignment) { + public @Nullable UtilityTask fillViewerAt(final double viewerSeedX, final double viewerSeedY, Long fill, FragmentSegmentAssignment assignment) { if (fill == null) { LOG.info("Received invalid label {} -- will not fill.", fill); @@ -126,10 +139,24 @@ public UtilityTask fillViewerAt(final double viewerSeedX, final double } final var maskPos = mask.displayPointToInitialMaskPoint(viewerSeedX, viewerSeedY); final var filter = getBackgorundLabelMaskForAssignment(maskPos, mask, assignment, fill); - return fillMaskAt(maskPos, mask, fill, filter); + if (filter == null) + return null; + final UtilityTask floodFillTask = fillMaskAt(maskPos, mask, fill, filter); + if (this.viewerMask == null) { + floodFillTask.onCancelled(true, (state, task) -> { + try { + mask.getSource().resetMasks(); + mask.requestRepaint(); + } catch (final MaskInUse e) { + e.printStackTrace(); + } + }); + } + floodFillTask.submit(floodFillExector); + return floodFillTask; } - public UtilityTask fillViewerAt(final double currentViewerX, final double currentViewerY, Long fill, RandomAccessibleInterval filter) { + public UtilityTask fillViewerAt(final double viewerSeedX, final double viewerSeedY, Long fill, RandomAccessibleInterval filter) { if (fill == null) { LOG.info("Received invalid label {} -- will not fill.", fill); @@ -152,135 +179,113 @@ public UtilityTask fillViewerAt(final double currentViewerX, final dou } else { mask = viewerMask; } - final var maskPos = mask.displayPointToInitialMaskPoint(currentViewerX, currentViewerY); - return fillMaskAt(maskPos, mask, fill, filter); + final var maskPos = mask.displayPointToInitialMaskPoint(viewerSeedX, viewerSeedY); + final UtilityTask floodFillTask = fillMaskAt(maskPos, mask, fill, filter); + if (this.viewerMask == null) { + floodFillTask.onCancelled(true, (state, task) -> { + try { + mask.getSource().resetMasks(); + mask.requestRepaint(); + } catch (final MaskInUse e) { + e.printStackTrace(); + } + }); + } + floodFillTask.submit(floodFillExector); + return floodFillTask; } + @NotNull - private UtilityTask fillMaskAt(Point maskPos, ViewerMask mask, Long fill, RandomAccessibleInterval filter) { + private UtilityTask fillMaskAt(Point maskPos, ViewerMask mask, Long fill, RandomAccessibleInterval filter) { + + final Interval screenInterval = mask.getScreenInterval(); + + final AtomicBoolean triggerRefresh = new AtomicBoolean(false); + final RandomAccessibleInterval writableViewerImg = mask.getViewerImg().getWritableSource(); + final var sourceAccessTracker = new AccessBoxRandomAccessibleOnGet<>(Views.extendValue(writableViewerImg, new UnsignedLongType(fill))) { + final Point position = new Point(sourceAccess.numDimensions()); + + @Override + public UnsignedLongType get() { + if (Thread.currentThread().isInterrupted()) + throw new RuntimeException("Flood Fill Interrupted"); + synchronized (this) { + updateAccessBox(); + } + sourceAccess.localize(position); + if (!triggerRefresh.get() && Intervals.contains(screenInterval, position)) { + triggerRefresh.set(true); + } + return sourceAccess.get(); + } + }; + sourceAccessTracker.initAccessBox(); + final var floodFillTask = createViewerFloodFillTask( maskPos, mask, filter, - fill, - this.viewerMask == null + sourceAccessTracker, + fill ); - InvokeOnJavaFXApplicationThread.invoke(() -> { - floodFillTask.valueProperty().addListener((obs, oldv, fillIntervalInMask) -> { - final Interval curMaskInterval = maskIntervalProperty.get(); - if (fillIntervalInMask != null) { - if (curMaskInterval == null) { - maskIntervalProperty.set(fillIntervalInMask); - } else { - maskIntervalProperty.set(Intervals.union(fillIntervalInMask, curMaskInterval)); + final var refreshAnimation = new AnimationTimer() { + + final static long delay = 2_000_000_000; // 2 second delay before refreshes start + final static long REFRESH_RATE = 1_000_000_000; + final long start = System.nanoTime(); + long before = start; + + @Override + public void handle(long now) { + if (now - start < delay || now - before < REFRESH_RATE) return; + if (!floodFillTask.isCancelled()) { + if (triggerRefresh.get()) { + mask.requestRepaint(sourceAccessTracker.createAccessInterval()); + before = now; + triggerRefresh.set(false); } } + } + }; - }); - }); - floodFillTask.submit(); + if (floodFillExector.isShutdown()) { + floodFillExector = newFloodFillExecutor(); + } - refreshDuringFloodFill(floodFillTask).start(); + floodFillTask.onEnd((task) -> { + refreshAnimation.stop(); + /*manually trigger repaint after stop to ensure full interval has been repainted*/ + mask.requestRepaint(sourceAccessTracker.createAccessInterval()); + }); + floodFillTask.onSuccess((state, task) -> { + maskIntervalProperty.set(sourceAccessTracker.createAccessInterval()); + }); + + refreshAnimation.start(); return floodFillTask; } - public static UtilityTask createViewerFloodFillTask( + public static UtilityTask createViewerFloodFillTask( Point maskPos, ViewerMask mask, - RandomAccessibleInterval floodFillFilter, long fill, - Boolean apply) { + RandomAccessibleInterval floodFillFilter, + RandomAccessible target, + long fill) { - return Tasks.createTask(task -> { + return Tasks.createTask(task -> { if (floodFillFilter == null) { - if (apply) { - try { - mask.getSource().resetMasks(true); - } catch (MaskInUse e) { - Exceptions.alert(e, Paintera.getPaintera().getBaseView().getNode().getScene().getWindow()); - } - } - return new FinalInterval(0, 0, 0); - } else { - return viewerMaskFloodFill(maskPos, mask, floodFillFilter, fill, apply); - } - }).onCancelled((state, task) -> { - try { - mask.getSource().resetMasks(); - } catch (final MaskInUse e) { - e.printStackTrace(); - } - }); - } - - public static Interval viewerMaskFloodFill( - final Point maskPos, - final ViewerMask mask, - final RandomAccessibleInterval filter, - final long fill, - final Boolean apply) { - - final var fillIntervalInMask = fillViewerMaskAt(maskPos, mask, filter, fill); - - final Interval affectedSourceInterval = Intervals.smallestContainingInterval( - mask.getCurrentMaskToSourceWithDepthTransform().estimateBounds(fillIntervalInMask)); - - if (apply) { - final MaskedSource, ?> source = mask.getSource(); - source.applyMask(source.getCurrentMask(), affectedSourceInterval, FOREGROUND_CHECK); - } - mask.requestRepaint(fillIntervalInMask); - return fillIntervalInMask; - } - - public static Interval viewerMaskFloodFill( - final Point maskPos, - final ViewerMask mask, - final RandomAccessibleInterval filter, - final long fill, - final Boolean apply, - final RandomAccessibleInterval maskImage) { - - final var fillIntervalInMask = fillViewerMaskAt(maskPos, maskImage, filter, fill); - - final Interval affectedSourceInterval = Intervals.smallestContainingInterval( - mask.getCurrentMaskToSourceWithDepthTransform().estimateBounds(fillIntervalInMask)); - - if (apply) { - final MaskedSource, ?> source = mask.getSource(); - source.applyMask(source.getCurrentMask(), affectedSourceInterval, FOREGROUND_CHECK); - } - mask.requestRepaint(fillIntervalInMask); - return fillIntervalInMask; - } - - public static Thread refreshDuringFloodFill() { - - return refreshDuringFloodFill(null); - } - - public static Thread refreshDuringFloodFill(Task task) { - - return new Thread(() -> { - while (task == null || !task.isDone()) { try { - Thread.sleep(100); - } catch (final InterruptedException e) { - Thread.currentThread().interrupt(); // restore interrupted status + mask.getSource().resetMasks(true); + } catch (MaskInUse e) { + Exceptions.alert(e, Paintera.getPaintera().getBaseView().getNode().getScene().getWindow()); } - - if (Thread.currentThread().isInterrupted()) - break; - - LOG.trace("Updating View for FloodFill2D"); - Paintera.getPaintera().getBaseView().orthogonalViews().requestRepaint(); - } - - if (Thread.interrupted() && task != null) { - task.cancel(); + } else { + fillAt(maskPos, target, floodFillFilter, fill); } }); } @@ -387,7 +392,7 @@ public static > Interval fillMaskAt( fillNormalAxisInLabelCoordinateSystem ); final long slicePos = Math.round(pos.getDoublePosition(fillNormalAxisInLabelCoordinateSystem)); - final long numSlices = Math.max((long)Math.ceil(fillDepth) - 1, 0); + final long numSlices = Math.max((long) Math.ceil(fillDepth) - 1, 0); if (numSlices == 0) { // fill only within the given slice, run 2D flood-fill final long[] seed2D = { @@ -446,11 +451,11 @@ public DoubleProperty fillDepthProperty() { @Nullable public static RandomAccessibleInterval getBackgorundLabelMaskForAssignment(Point initialSeed, ViewerMask mask, - FragmentSegmentAssignment assignment, long fillValue) { + FragmentSegmentAssignment assignment, long fillValue) { final var backgroundViewerRai = ViewerMask.getSourceDataInInitialMaskSpace(mask); - final var id = (long)backgroundViewerRai.getAt(initialSeed).getRealDouble(); + final var id = (long) backgroundViewerRai.getAt(initialSeed).getRealDouble(); final long seedLabel = assignment != null ? assignment.getSegment(id) : id; LOG.debug("Got seed label {}", seedLabel); @@ -461,7 +466,7 @@ public static RandomAccessibleInterval getBackgorundLabelMaskForAssign return Converters.convert( backgroundViewerRai, (src, target) -> { - long segmentId = (long)src.getRealDouble(); + long segmentId = (long) src.getRealDouble(); if (assignment != null) { segmentId = assignment.getSegment(segmentId); } @@ -471,21 +476,17 @@ public static RandomAccessibleInterval getBackgorundLabelMaskForAssign ); } - public static Interval fillViewerMaskAt( + public static void fillViewerMaskAt( final Point initialSeed, - final ViewerMask mask, + final RandomAccessible source, final RandomAccessibleInterval filter, final long fillValue) { final RandomAccessible extendedFilter = Views.extendValue(filter, new BoolType(false)); - final AccessBoxRandomAccessibleOnGet sourceAccessTracker = new - AccessBoxRandomAccessibleOnGet<>( - Views.extendValue(mask.getViewerImg(), new UnsignedLongType(fillValue))); - sourceAccessTracker.initAccessBox(); // fill only within the given slice, run 2D flood-fill - LOG.debug("Flood filling into viewer mask "); + LOG.debug("Flood filling into viewer source "); final RandomAccessible backgroundSlice; if (extendedFilter.numDimensions() == 3) { @@ -493,7 +494,7 @@ public static Interval fillViewerMaskAt( } else { backgroundSlice = extendedFilter; } - final RandomAccessible viewerFillImg = Views.hyperSlice(sourceAccessTracker, 2, 0); + final RandomAccessible viewerFillImg = Views.hyperSlice(source, 2, 0); FloodFill.fill( backgroundSlice, @@ -502,25 +503,18 @@ public static Interval fillViewerMaskAt( new UnsignedLongType(fillValue), new DiamondShape(1) ); - - return new FinalInterval(sourceAccessTracker.getMin(), sourceAccessTracker.getMax()); } - public static Interval fillAt( + public static void fillAt( final Point initialSeed, - final RandomAccessibleInterval target, + final RandomAccessible target, final RandomAccessibleInterval filter, final long fillValue) { final RandomAccessible extendedFilter = Views.extendValue(filter, new BoolType(false)); - final AccessBoxRandomAccessibleOnGet sourceAccessTracker = new - AccessBoxRandomAccessibleOnGet<>( - Views.extendValue(target, new UnsignedLongType(fillValue))); - sourceAccessTracker.initAccessBox(); - // fill only within the given slice, run 2D flood-fill - LOG.debug("Flood filling into viewer mask "); + LOG.debug("Flood filling "); final RandomAccessible backgroundSlice; if (extendedFilter.numDimensions() == 3) { @@ -528,7 +522,7 @@ public static Interval fillAt( } else { backgroundSlice = extendedFilter; } - final RandomAccessible viewerFillImg = Views.hyperSlice(sourceAccessTracker, 2, 0); + final RandomAccessible viewerFillImg = Views.hyperSlice(target, 2, 0); FloodFill.fill( backgroundSlice, @@ -537,8 +531,6 @@ public static Interval fillAt( new UnsignedLongType(fillValue), new DiamondShape(1) ); - - return new FinalInterval(sourceAccessTracker.getMin(), sourceAccessTracker.getMax()); } public static Interval fillViewerMaskAt( diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/data/DelegatingDataSource.java b/src/main/java/org/janelia/saalfeldlab/paintera/data/DelegatingDataSource.java deleted file mode 100644 index 5d114f26e..000000000 --- a/src/main/java/org/janelia/saalfeldlab/paintera/data/DelegatingDataSource.java +++ /dev/null @@ -1,116 +0,0 @@ -package org.janelia.saalfeldlab.paintera.data; - -import bdv.viewer.Interpolation; -import mpicbg.spim.data.sequence.VoxelDimensions; -import net.imglib2.RandomAccessibleInterval; -import net.imglib2.RealRandomAccessible; -import net.imglib2.realtransform.AffineTransform3D; - -import java.util.function.Predicate; - -public class DelegatingDataSource implements DataSource { - - private final DataSource delegate; - - public DelegatingDataSource(final DataSource delegate) { - - super(); - this.delegate = delegate; - } - - @Override - public boolean isPresent(final int t) { - - return this.delegate.isPresent(t); - } - - @Override - public RandomAccessibleInterval getSource(final int t, final int level) { - - return this.delegate.getSource(t, level); - } - - @Override - public RealRandomAccessible getInterpolatedSource(final int t, final int level, final Interpolation method) { - - return this.delegate.getInterpolatedSource(t, level, method); - } - - @Override - public void getSourceTransform(final int t, final int level, final AffineTransform3D transform) { - - this.delegate.getSourceTransform(t, level, transform); - } - - @Override - public T getType() { - - return this.delegate.getType(); - } - - @Override - public String getName() { - - return this.delegate.getName(); - } - - @Override - public VoxelDimensions getVoxelDimensions() { - - return this.delegate.getVoxelDimensions(); - } - - @Override - public int getNumMipmapLevels() { - - return this.delegate.getNumMipmapLevels(); - } - - @Override - public RandomAccessibleInterval getDataSource(final int t, final int level) { - - return this.delegate.getDataSource(t, level); - } - - @Override - public RealRandomAccessible getInterpolatedDataSource(final int t, final int level, final Interpolation method) { - - return this.delegate.getInterpolatedDataSource(t, level, method); - } - - @Override - public D getDataType() { - - return this.delegate.getDataType(); - } - - @Override - public void invalidate(Long key) { - - delegate.invalidate(key); - } - - @Override - public void invalidateIf(long parallelismThreshold, Predicate condition) { - - delegate.invalidateIf(parallelismThreshold, condition); - } - - @Override - public void invalidateIf(Predicate condition) { - - delegate.invalidateIf(condition); - } - - @Override - public void invalidateAll(long parallelismThreshold) { - - delegate.invalidateAll(parallelismThreshold); - } - - @Override - public void invalidateAll() { - - delegate.invalidateAll(); - } -} 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 2c249a35b..3e3287e8d 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 @@ -2,6 +2,7 @@ import bdv.cache.SharedQueue; import bdv.viewer.Interpolation; +import com.google.common.util.concurrent.ThreadFactoryBuilder; import gnu.trove.iterator.TLongIterator; import gnu.trove.map.TLongObjectMap; import gnu.trove.map.hash.TLongLongHashMap; @@ -14,6 +15,7 @@ import javafx.beans.binding.BooleanBinding; import javafx.beans.binding.ObjectBinding; import javafx.beans.property.BooleanProperty; +import javafx.beans.property.DoubleProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyBooleanProperty; import javafx.beans.property.ReadOnlyStringProperty; @@ -67,6 +69,8 @@ import net.imglib2.interpolation.randomaccess.NearestNeighborInterpolatorFactory; import net.imglib2.loops.LoopBuilder; import net.imglib2.outofbounds.RealOutOfBoundsConstantValueFactory; +import net.imglib2.parallel.TaskExecutor; +import net.imglib2.parallel.TaskExecutors; import net.imglib2.realtransform.AffineTransform3D; import net.imglib2.realtransform.RealViews; import net.imglib2.realtransform.Scale3D; @@ -116,7 +120,11 @@ import java.util.Map.Entry; import java.util.Optional; import java.util.Set; -import java.util.concurrent.*; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; @@ -163,7 +171,7 @@ public class MaskedSource, T extends Type> implements D private static final int NUM_DIMENSIONS = 3; - public static final Predicate VALID_LABEL_CHECK = it -> it.get() != Label.INVALID; + public static final Predicate VALID_LABEL_CHECK = it -> it != Label.INVALID; private final UnsignedLongType INVALID = new UnsignedLongType(Label.INVALID); @@ -333,7 +341,7 @@ public SourceMask getCurrentMask() { public synchronized SourceMask generateMask( final MaskInfo maskInfo, - final Predicate isPaintedForeground) + final Predicate isPaintedForeground) throws MaskInUse { LOG.debug("Generating mask: {}", maskInfo); @@ -374,7 +382,7 @@ private boolean isMaskInUse() { public synchronized void setMask( final SourceMask mask, - final Predicate isPaintedForeground) + final Predicate isPaintedForeground) throws MaskInUse { setMask(mask, mask.getRai(), mask.getVolatileRai(), isPaintedForeground); @@ -384,7 +392,7 @@ public synchronized void setMask( final SourceMask mask, RandomAccessibleInterval rai, RandomAccessibleInterval volatileRai, - final Predicate acceptLabel) + final Predicate acceptLabel) throws MaskInUse { if (isMaskInUse()) { @@ -411,7 +419,7 @@ public synchronized void setMask( final SourceMask mask, RealRandomAccessible rai, RealRandomAccessible volatileRai, - final Predicate acceptLabel) + final Predicate acceptLabel) throws MaskInUse { if (isMaskInUse()) { @@ -441,7 +449,7 @@ public void setMask( final Invalidate invalidate, final Invalidate volatileInvalidate, final Runnable shutdown, - final Predicate acceptLabel) + final Predicate acceptLabel) throws MaskInUse { if (isMaskInUse()) { @@ -471,7 +479,7 @@ public void setMask( public void applyMask( final SourceMask mask, final Interval paintedInterval, - final Predicate acceptAsPainted) { + final Predicate acceptAsPainted) { if (mask == null) return; @@ -529,7 +537,8 @@ public void applyMask( affectedBlocks, maskInfo.level, paintedInterval, - acceptAsPainted); + acceptAsPainted, + propagationExecutor); } finally { setMasksConstant(); synchronized (this) { @@ -557,6 +566,136 @@ public void applyMask( } + /** + * This method differs from `applyMask` in a few important ways: + * - It runs over each block in parallel + * - It is a blocking method + * + * @param mask to apply ( should be same as `currentMask`) + * @param intervals to apply mask over, separately. + * @param acceptAsPainted to accept a value + */ + public void applyMaskOverIntervals( + final SourceMask mask, + final List intervals, + final DoubleProperty progressBinding, + final Predicate acceptAsPainted) { + + if (mask == null) + return; + + final ExecutorService applyPool = Executors.newFixedThreadPool( + Runtime.getRuntime().availableProcessors(), + new ThreadFactoryBuilder().setNameFormat("apply-thread-%d").build() + ); + final ArrayList> applies = new ArrayList<>(); + synchronized (this) { + final boolean maskCanBeApplied = !this.isCreatingMask() && this.getCurrentMask() == mask && !this.isApplyingMask.get() && !this.isPersisting(); + if (!maskCanBeApplied) { + LOG.debug("Did not pass valid mask {}, will not do anything", mask); + this.isBusy.set(false); + return; + } + this.isApplyingMask.set(true); + } + var expectedTasks = intervals.size() * 2; + final var completedTasks = new AtomicInteger(); + for (Interval interval : intervals) { + + /* Start as busy, so a new mask isn't generated until we are done applying this one. */ + this.isBusy.set(true); + + var applyFuture = applyPool.submit(() -> { + final MaskInfo maskInfo = mask.getInfo(); + final CachedCellImg canvas = dataCanvases[maskInfo.level]; + final CellGrid grid = canvas.getCellGrid(); + + final int[] blockSize = new int[grid.numDimensions()]; + grid.cellDimensions(blockSize); + + final TLongSet directlyAffectedBlocks = affectedBlocks(mask.getRai(), canvas.getCellGrid(), interval); + + final var labelToBlocks = paintAffectedPixels( + directlyAffectedBlocks, + mask, + canvas, + canvas.getCellGrid(), + interval, + acceptAsPainted); + + synchronized (progressBinding) { + var progress = completedTasks.incrementAndGet() / (double) expectedTasks; + progressBinding.set(progress); + } + + final Map blocksByLabelByLevel = this.affectedBlocksByLabel[maskInfo.level]; + synchronized (blocksByLabelByLevel) { + for (var label : labelToBlocks.entrySet()) { + blocksByLabelByLevel.computeIfAbsent(label.getKey(), k -> new TLongHashSet()).addAll(label.getValue()); + } + } + + final TLongSet paintedBlocksAtHighestResolution = this.scaleBlocksToLevel( + directlyAffectedBlocks, + maskInfo.level, + 0); + + LOG.debug("Added affected block: {}", blocksByLabelByLevel); + synchronized (affectedBlocks) { + affectedBlocks.addAll(paintedBlocksAtHighestResolution); + } + + try { + propagationExecutor.submit(() -> { + propagateMask( + mask.getRai(), + directlyAffectedBlocks, + maskInfo.level, + interval, + acceptAsPainted, + propagationExecutor); + + }).get(); + synchronized (progressBinding) { + var progress = completedTasks.incrementAndGet() / (double) expectedTasks; + progressBinding.set(progress); + } + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + + }); + applies.add(applyFuture); + } + for (Future it : applies) { + try { + it.get(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + } + synchronized (progressBinding) { + progressBinding.set(1.0); + } + + synchronized (this) { + setCurrentMask(null); + setMasksConstant(); + LOG.debug("Done applying mask!"); + this.isApplyingMask.set(false); + } + + if (mask.getShutdown() != null) + mask.getShutdown().run(); + if (mask.getInvalidate() != null) + mask.getInvalidate().invalidateAll(); + if (mask.getInvalidateVolatile() != null) + mask.getInvalidateVolatile().invalidateAll(); + + applyPool.shutdown(); + this.isBusy.set(false); + } + private void setMasksConstant() { for (int level = 0; level < getNumMipmapLevels(); ++level) { @@ -615,7 +754,7 @@ private void scalePositionToLevel(final long[] position, final int intervalLevel toTargetScale.apply(positionDouble, positionDouble); - Arrays.setAll(targetPosition, d -> (long)Math.ceil(positionDouble[d])); + Arrays.setAll(targetPosition, d -> (long) Math.ceil(positionDouble[d])); } public void resetMasks() throws MaskInUse { @@ -685,8 +824,16 @@ public void persistCanvas(final boolean clearCanvas) throws CannotPersist { final ObservableList states = FXCollections.observableArrayList(); - final Consumer nextState = states::add; - final Consumer updateState = state -> states.set(states.size() - 1, state); + final Consumer nextState = (next) -> { + synchronized (states) { + states.add(next); + } + }; + final Consumer updateState = (update) -> { + synchronized (states) { + states.set(states.size() -1, update); + } + }; final Runnable dialogHandler = () -> { LOG.warn("Creating commit status dialog."); @@ -722,9 +869,12 @@ public void persistCanvas(final boolean clearCanvas) throws CannotPersist { VBox.setVgrow(statesText, Priority.ALWAYS); VBox.setVgrow(progressBar, Priority.ALWAYS); InvokeOnJavaFXApplicationThread.invoke(() -> isCommittingDialog.getDialogPane().setContent(content)); - states.addListener((ListChangeListener)change -> - InvokeOnJavaFXApplicationThread.invoke(() -> - statesText.setText(String.join("\n", states)) + states.addListener((ListChangeListener) change -> + InvokeOnJavaFXApplicationThread.invoke(() -> { + synchronized (states) { + statesText.setText(String.join("\n", states)); + } + } ) ); synchronized (this) { @@ -735,6 +885,9 @@ public void persistCanvas(final boolean clearCanvas) throws CannotPersist { isCommittingDialog.show(); }; final Consumer animateProgressBar = progress -> { + if (progressBar.progressProperty().get() > progress) { + progressBar.progressProperty().set(0.0); + } Timeline timeline = new Timeline(); KeyValue keyValue = new KeyValue(progressBar.progressProperty(), progress); KeyFrame keyFrame = new KeyFrame(new Duration(500), keyValue); @@ -936,7 +1089,7 @@ public D getDataType() { public RandomAccessibleInterval getReadOnlyDataCanvas(final int t, final int level) { return Converters.convert( - (RandomAccessibleInterval)this.dataCanvases[level], + (RandomAccessibleInterval) this.dataCanvases[level], new TypeIdentity<>(), new UnsignedLongType() ); @@ -959,14 +1112,18 @@ public RandomAccessibleInterval getReadOnlyDataBackground(final int t, final * @param affectedBlocks * @param steps * @param interval + * @param propagationExecutor * @return map of labels to modified block ids */ - public static Map> downsampleBlocks( + private static Map> downsampleBlocks( final RandomAccessible source, final CachedCellImg img, final TLongSet affectedBlocks, final int[] steps, - final Interval interval) { + final Interval interval, + final ExecutorService propagationExecutor) { + + final TaskExecutor taskExecutor = TaskExecutors.forExecutorService(propagationExecutor); final BlockSpec blockSpec = new BlockSpec(img.getCellGrid()); @@ -975,6 +1132,7 @@ public static Map> downsampleBlocks( final Map> blocksModifiedByLabel = new HashMap<>(); LOG.debug("Initializing affected blocks: {}", affectedBlocks); + final var labelsForBlockFutures = new ArrayList>>>(); for (final TLongIterator it = affectedBlocks.iterator(); it.hasNext(); ) { final long blockId = it.next(); blockSpec.fromLinearIndex(blockId); @@ -986,8 +1144,23 @@ public static Map> downsampleBlocks( if (isNonEmpty(intersectedCellMin, intersectedCellMax)) { LOG.trace("Downsampling for intersected min/max: {} {}", intersectedCellMin, intersectedCellMax); - final var labelsForBlock = downsample(source, Views.interval(img, intersectedCellMin, intersectedCellMax), steps); + final long[] min = new long[intersectedCellMin.length]; + final long[] max = new long[intersectedCellMax.length]; + System.arraycopy(intersectedCellMin, 0, min, 0, min.length); + System.arraycopy(intersectedCellMax, 0, max, 0, max.length); + final var future = propagationExecutor.submit(() -> new Pair<>(blockId, downsample(source, Views.interval(img, min, max), steps, taskExecutor))); + labelsForBlockFutures.add(future); + } + } + for (Future>> labelsForBlockFuture : labelsForBlockFutures) { + try { + final Pair> futurePair = labelsForBlockFuture.get(); + final var blockId = futurePair.getKey(); + final Set labelsForBlock = futurePair.getValue(); labelsForBlock.forEach(label -> blocksModifiedByLabel.computeIfAbsent(label, k -> new HashSet<>()).add(blockId)); + } catch (InterruptedException | ExecutionException e) { + e.printStackTrace(); + throw new RuntimeException(e); } } return blocksModifiedByLabel; @@ -997,11 +1170,13 @@ public static Map> downsampleBlocks( * @param source * @param target * @param steps + * @param taskExecutor */ - public static > Set downsample( + private static > Set downsample( final RandomAccessible source, final RandomAccessibleInterval target, - final int[] steps) { + final int[] steps, + final TaskExecutor taskExecutor) { LOG.debug( "Downsampling ({} {}) with steps {}", @@ -1018,7 +1193,7 @@ public static > Set downsample( final HashSet labels = new HashSet<>(); LoopBuilder.setImages(tiledSource, zeroMinTarget) - .multiThreaded() + .multiThreaded(taskExecutor) .forEachChunk(chunk -> { final TLongLongHashMap maxCounts = new TLongLongHashMap(); chunk.forEachPixel((sourceTile, lowResTarget) -> { @@ -1059,7 +1234,8 @@ private void propagateMask( final TLongSet paintedBlocksAtPaintedScale, final int paintedLevel, final Interval intervalAtPaintedScale, - final Predicate isPaintedForeground) { + final Predicate isPaintedForeground, + final ExecutorService propagationExecutor) { final RandomAccessibleInterval atPaintedLevel = dataCanvases[paintedLevel]; for (int lowerResLevel = paintedLevel + 1; lowerResLevel < getNumMipmapLevels(); ++lowerResLevel) { @@ -1083,14 +1259,15 @@ private void propagateMask( LOG.debug("Interval at lower resolution level: {} {}", Intervals.minAsLongArray(intervalAtLowerRes), Intervals.maxAsLongArray(intervalAtLowerRes)); // downsample - final int[] steps = DoubleStream.of(paintedToLowerScales).mapToInt(d -> (int)d).toArray(); + final int[] steps = DoubleStream.of(paintedToLowerScales).mapToInt(d -> (int) d).toArray(); LOG.debug("Downsample step size: {}", steps); final var blocksModifiedByLabel = downsampleBlocks( Views.extendValue(atPaintedLevel, new UnsignedLongType(Label.INVALID)), lowerResCanvas, affectedBlocksAtLowerRes, steps, - intervalAtLowerRes); + intervalAtLowerRes, + propagationExecutor); for (Entry> entry : blocksModifiedByLabel.entrySet()) { final Long labelId = entry.getKey(); final Set blocks = entry.getValue(); @@ -1155,7 +1332,7 @@ private void propagateMask( ); final IntervalView relevantBlockAtPaintedResolution = Views.interval( - Converters.convert(mask, (s, t) -> t.set(isPaintedForeground.test(s)), new BoolType()), + Converters.convert(mask, (s, t) -> t.set(isPaintedForeground.test(s.get())), new BoolType()), minPainted, maxPainted ); @@ -1172,15 +1349,15 @@ private void propagateMask( ); final Interval interval = new FinalInterval(intersectionMin, intersectionMax); - final RandomAccessibleInterval canvasAtHighResInterval = Views.interval(higherResCanvas, interval); - final RandomAccessibleInterval maskOverInterval = Views.interval(Views.raster(higherResMask), interval); + final RandomAccessibleInterval> canvasAtHighResInterval = Views.interval(new BundleView<>(higherResCanvas), interval); + final RandomAccessibleInterval> maskOverInterval = Views.interval(new BundleView<>(Views.raster(higherResMask)), interval); final HashSet labels = new HashSet<>(); - LoopBuilder.setImages(Views.interval(new BundleView<>(canvasAtHighResInterval), canvasAtHighResInterval), maskOverInterval) + LoopBuilder.setImages(canvasAtHighResInterval, maskOverInterval) .multiThreaded() .forEachPixel((canvasRa, maskVal) -> { - if (maskVal.get() != Label.INVALID) { - final long maskLabel = maskVal.get(); + final long maskLabel = maskVal.get().get(); + if (maskLabel != Label.INVALID) { canvasRa.get().set(maskLabel); labels.add(maskLabel); } @@ -1221,7 +1398,7 @@ public static TLongSet affectedBlocks( final Interval interval) { if (input instanceof AccessedBlocksRandomAccessible) { - final var tracker = (net.imglib2.util.AccessedBlocksRandomAccessible)input; + final var tracker = (net.imglib2.util.AccessedBlocksRandomAccessible) input; if (grid.equals(tracker.getGrid())) { final long[] blocks = tracker.listBlocks(); LOG.debug("Got these blocks from tracker: {}", blocks); @@ -1303,7 +1480,7 @@ public static , C extends IntegerType> Map canvas, final CellGrid grid, final Interval paintedInterval, - final Predicate acceptAsPainted) { + final Predicate acceptAsPainted) { final long[] currentMin = new long[grid.numDimensions()]; final long[] currentMax = new long[grid.numDimensions()]; @@ -1314,7 +1491,8 @@ public static , C extends IntegerType> Map> labelToBlocks = new AtomicReference<>(new HashMap<>()); - final ExecutorService threadPool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); + final ThreadFactory build = new ThreadFactoryBuilder().setNameFormat("paint-affected-pixels-%d").build(); + final ExecutorService threadPool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors(), build); final List> jobs = new ArrayList<>(); for (final TLongIterator blockIt = relevantBlocks.iterator(); blockIt.hasNext(); ) { final long blockId = blockIt.next(); @@ -1339,10 +1517,11 @@ public static , C extends IntegerType> Map canvasOverRestricted = Views.interval(canvas, restrictedInterval); final var labelsForBlock = mask.applyMaskToCanvas(canvasOverRestricted, acceptAsPainted); labelToBlocks.getAndUpdate(map -> { + final HashMap updatedMap = new HashMap<>(map); for (Long label : labelsForBlock) { - map.computeIfAbsent(label, k -> new TLongHashSet()).add(blockId); + updatedMap.computeIfAbsent(label, k -> new TLongHashSet()).add(blockId); } - return map; + return updatedMap; }); }); jobs.add(job); @@ -1354,6 +1533,7 @@ public static , C extends IntegerType> Map observable, final St final CellLoader loader = img -> img.forEach(t -> t.set(Label.INVALID)); final DiskCachedCellImg store = f.create(dimensions[level], loader, o); final TmpVolatileHelpers.RaiWithInvalidate vstore = TmpVolatileHelpers.createVolatileCachedCellImgWithInvalidate(( - DiskCachedCellImg)store, + DiskCachedCellImg) store, queue, new CacheHints(LoadingStrategy.VOLATILE, canvases.length - 1 - level, true)); @@ -1516,7 +1696,7 @@ public PersistCanvas getPersister() { public CellGrid getCellGrid(final int t, final int level) { - return ((AbstractCellImg)underlyingSource().getSource(t, level)).getCellGrid(); + return ((AbstractCellImg) underlyingSource().getSource(t, level)).getCellGrid(); } @Override @@ -1606,7 +1786,7 @@ private DiskCachedCellImgOptions getMaskDiskCachedCellImgOptions(final int level final DiskCachedCellImg store = createMaskStore(maskOpts, level); final TmpVolatileHelpers.RaiWithInvalidate vstore = TmpVolatileHelpers.createVolatileCachedCellImgWithInvalidate( - (CachedCellImg)store, + (CachedCellImg) store, queue, new CacheHints(LoadingStrategy.VOLATILE, 0, true)); return new Pair<>(store, vstore); @@ -1624,7 +1804,7 @@ private DiskCachedCellImgOptions getMaskDiskCachedCellImgOptions(final int level final DiskCachedCellImg store = createMaskStore(maskOpts, imgDimensions, defaultValue); final TmpVolatileHelpers.RaiWithInvalidate vstore = TmpVolatileHelpers.createVolatileCachedCellImgWithInvalidate( - (CachedCellImg)store, + (CachedCellImg) store, queue, new CacheHints(LoadingStrategy.VOLATILE, 0, true)); return new Pair<>(store, vstore); @@ -1634,7 +1814,7 @@ private void setMasks( final RandomAccessibleInterval store, final RandomAccessibleInterval vstore, final int maskLevel, - final Predicate isPaintedForeground) { + final Predicate isPaintedForeground) { setAtMaskLevel(store, vstore, maskLevel, isPaintedForeground); LOG.debug("Created mask at scale level {}", maskLevel); @@ -1645,7 +1825,7 @@ private void setMasks( final RealRandomAccessible mask, final RealRandomAccessible vmask, final int maskLevel, - final Predicate acceptMask) { + final Predicate acceptMask) { setAtMaskLevel(mask, vmask, maskLevel, acceptMask); LOG.debug("Created mask at scale level {}", maskLevel); @@ -1681,7 +1861,7 @@ private void setAtMaskLevel( final RandomAccessibleInterval store, final RandomAccessibleInterval vstore, final int maskLevel, - final Predicate acceptLabel) { + final Predicate acceptLabel) { setAtMaskLevel( Views.interpolate(Views.extendZero(store), new NearestNeighborInterpolatorFactory<>()), @@ -1690,25 +1870,24 @@ private void setAtMaskLevel( acceptLabel); if (store instanceof AccessedBlocksRandomAccessible) { - ((AccessedBlocksRandomAccessible)store).clear(); + ((AccessedBlocksRandomAccessible) store).clear(); } if (vstore instanceof AccessedBlocksRandomAccessible) { - ((AccessedBlocksRandomAccessible)vstore).clear(); + ((AccessedBlocksRandomAccessible) vstore).clear(); } - } private void setAtMaskLevel( final RealRandomAccessible mask, final RealRandomAccessible vmask, final int maskLevel, - final Predicate acceptLabel) { + final Predicate acceptLabel) { this.dMasks[maskLevel] = Converters.convert( mask, (input, output) -> { - if (acceptLabel.test(input)) { - output.set(input); + if (acceptLabel.test(input.get())) { + output.set(input.get()); } else { output.set(INVALID); } @@ -1721,9 +1900,9 @@ private void setAtMaskLevel( final boolean isValid = input.isValid(); output.setValid(isValid); if (isValid) { - final UnsignedLongType inputType = input.get(); - if (acceptLabel.test(inputType)) { - output.get().set(inputType); + final long inputVal = input.get().get(); + if (acceptLabel.test(inputVal)) { + output.get().set(inputVal); } else { output.get().set(INVALID); } @@ -1755,7 +1934,7 @@ private static javafx.scene.control.Label[] asLabels(final List canvasCacheDirUpdate = Masks.canvasTmpDirDirectorySupplier(currentProjectDirectory); + final String appCacheDir = Paintera.getPaintera().getProperties().getPainteraDirectoriesConfig().getAppCacheDir(); + final Supplier canvasDirSupplier = Masks.canvasTmpDirDirectorySupplier(appCacheDir); final String sourceClass = map.get(UNDERLYING_SOURCE_CLASS_KEY).getAsString(); final DataSource source = context.deserialize( @@ -80,18 +83,18 @@ public MaskedSourceDeserializer( Class.forName(persisterClass) ); - final String initialCanvasPath = canvasCacheDirUpdate.get(); + final String initialCanvasPath = canvasDirSupplier.get(); // TODO re-use canvas // Optional // .ofNullable( map.get( CURRENT_CACHE_DIR_KEY ) ) // .map( JsonElement::getAsString ) - // .orElseGet( canvasCacheDirUpdate ); + // .orElseGet( canvasDirSupplier ); final DataSource masked = Masks.maskedSource( source, queue, initialCanvasPath, - canvasCacheDirUpdate, + canvasDirSupplier, mergeCanvasIntoBackground, propagationExecutor); final MaskedSource returnVal = masked instanceof MaskedSource diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/data/mask/Masks.java b/src/main/java/org/janelia/saalfeldlab/paintera/data/mask/Masks.java index e462446de..f36a3735c 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/data/mask/Masks.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/data/mask/Masks.java @@ -230,9 +230,9 @@ public static MaskedSource fromLab ); } - public static Supplier canvasTmpDirDirectorySupplier(final Supplier root) { + public static Supplier canvasTmpDirDirectorySupplier(final String projectCacheDir) { - return new TmpDirectoryCreator(() -> Paths.get(root.get(), "canvases"), "canvas-"); + return new TmpDirectoryCreator(Paths.get(projectCacheDir, "canvases"), "canvas-"); } } diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/data/mask/PickOneLabelMultisetType.java b/src/main/java/org/janelia/saalfeldlab/paintera/data/mask/PickOneLabelMultisetType.java index ff7a6cf35..417e10f86 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/data/mask/PickOneLabelMultisetType.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/data/mask/PickOneLabelMultisetType.java @@ -56,21 +56,19 @@ private PickOneLabelMultisetType( @Override public LabelMultisetType apply(final Triple t) { - final LabelMultisetType a = t.getA(); - final M b = t.getB(); final M c = t.getC(); - if (pickThird.test(c)) { converter.convert(c, scalarValue); return scalarValue; } + final M b = t.getB(); if (pickSecond.test(b, c)) { converter.convert(b, scalarValue); return scalarValue; } - return a; + return t.getA(); } diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/data/mask/SourceMask.java b/src/main/java/org/janelia/saalfeldlab/paintera/data/mask/SourceMask.java index 6354df009..48ab5e90c 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/data/mask/SourceMask.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/data/mask/SourceMask.java @@ -4,6 +4,7 @@ import net.imglib2.RandomAccessibleInterval; import net.imglib2.cache.Invalidate; import net.imglib2.loops.LoopBuilder; +import net.imglib2.type.label.Label; import net.imglib2.type.numeric.IntegerType; import net.imglib2.type.numeric.integer.UnsignedLongType; import net.imglib2.type.volatiles.VolatileUnsignedLongType; @@ -81,21 +82,26 @@ public SourceMask( public > Set applyMaskToCanvas( final RandomAccessibleInterval canvas, - final Predicate acceptAsPainted) { + final Predicate acceptAsPainted) { - final IntervalView maskOverCanvas = Views.interval(getRai(), canvas); + final IntervalView maskOverCanvas = Views.interval(Views.extendValue(getRai(), Label.INVALID), canvas); final IntervalView> bundledCanvas = Views.interval(new BundleView<>(canvas), canvas); - final HashSet labels = new HashSet<>(); - LoopBuilder.setImages(maskOverCanvas, bundledCanvas).multiThreaded().forEachPixel((maskVal, bundledCanvasVal) -> { - if (acceptAsPainted.test(maskVal)) { + final var labels = LoopBuilder.setImages(maskOverCanvas, bundledCanvas).multiThreaded().forEachChunk(chunk -> { + final HashSet labelsForChunk = new HashSet<>(); + chunk.forEachPixel((maskVal, bundledCanvasVal) -> { final long label = maskVal.getIntegerLong(); - bundledCanvasVal.get().setInteger(label); - synchronized (labels) { - labels.add(label); + if (acceptAsPainted.test(label)) { + bundledCanvasVal.get().setInteger(label); + labelsForChunk.add(label); } - } + }); + return labelsForChunk; }); - return labels; + final var allLabels = new HashSet(); + for (HashSet labelSet : labels) { + allLabels.addAll(labelSet); + } + return allLabels; } } diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/data/mask/TmpDirectoryCreator.java b/src/main/java/org/janelia/saalfeldlab/paintera/data/mask/TmpDirectoryCreator.java index 411511329..438e6bac0 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/data/mask/TmpDirectoryCreator.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/data/mask/TmpDirectoryCreator.java @@ -16,17 +16,17 @@ public class TmpDirectoryCreator implements Supplier { private static Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - private final Supplier dir; + private final Path baseTempDir; private final String prefix; private final FileAttribute[] attrs; - public TmpDirectoryCreator(final Supplier dir, final String prefix, final FileAttribute... attrs) { + public TmpDirectoryCreator(final Path baseDir, final String prefix, final FileAttribute... attrs) { super(); - LOG.debug("Creating {} with dir={} prefix={} attrs={}", this.getClass().getSimpleName(), dir, prefix, attrs); - this.dir = dir; + LOG.debug("Creating {} with dir={} prefix={} attrs={}", this.getClass().getSimpleName(), baseDir, prefix, attrs); + this.baseTempDir = baseDir; this.prefix = prefix; this.attrs = attrs; } @@ -34,14 +34,16 @@ public TmpDirectoryCreator(final Supplier dir, final String prefix, final @Override public String get() { - final Path dir = this.dir.get(); try { - Optional.ofNullable(dir).map(Path::toFile).ifPresent(File::mkdirs); - final Path tmpDir = dir == null - ? Files.createTempDirectory(prefix, attrs) - : Files.createTempDirectory(dir, prefix, attrs); + Optional.ofNullable(baseTempDir).map(Path::toFile).ifPresent(File::mkdirs); + final Path tmpDir; + if (baseTempDir == null) + tmpDir = Files.createTempDirectory(prefix, attrs); + else + tmpDir = Files.createTempDirectory(baseTempDir, prefix, attrs); + tmpDir.toFile().deleteOnExit(); //TODO meta ensure this is safe to do. It should be, if they are temporary... - LOG.debug("Created tmp dir {}", tmpDir.toString()); + LOG.debug("Created tmp dir {}", tmpDir); return tmpDir.toString(); } catch (final IOException e) { throw new RuntimeException(e); diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/data/n5/CommitCanvasN5.java b/src/main/java/org/janelia/saalfeldlab/paintera/data/n5/CommitCanvasN5.java index 00719e0c3..eddd5ca1d 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/data/n5/CommitCanvasN5.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/data/n5/CommitCanvasN5.java @@ -1,5 +1,6 @@ package org.janelia.saalfeldlab.paintera.data.n5; +import com.google.common.util.concurrent.ThreadFactoryBuilder; import gnu.trove.iterator.TLongIterator; import gnu.trove.iterator.TLongObjectIterator; import gnu.trove.map.TLongObjectMap; @@ -36,12 +37,7 @@ import org.janelia.saalfeldlab.labels.blocks.LabelBlockLookupKey; import org.janelia.saalfeldlab.labels.blocks.n5.IsRelativeToContainer; import org.janelia.saalfeldlab.labels.downsample.WinnerTakesAll; -import org.janelia.saalfeldlab.n5.ByteArrayDataBlock; -import org.janelia.saalfeldlab.n5.DataBlock; -import org.janelia.saalfeldlab.n5.DatasetAttributes; -import org.janelia.saalfeldlab.n5.LongArrayDataBlock; -import org.janelia.saalfeldlab.n5.N5Reader; -import org.janelia.saalfeldlab.n5.N5Writer; +import org.janelia.saalfeldlab.n5.*; import org.janelia.saalfeldlab.n5.imglib2.N5LabelMultisets; import org.janelia.saalfeldlab.n5.imglib2.N5Utils; import org.janelia.saalfeldlab.paintera.data.mask.persist.PersistCanvas; @@ -56,15 +52,11 @@ import java.io.IOException; import java.lang.invoke.MethodHandles; -import java.nio.file.Paths; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Optional; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; +import java.util.concurrent.*; import java.util.function.Supplier; public class CommitCanvasN5 implements PersistCanvas { @@ -138,7 +130,8 @@ public void updateLabelBlockLookup(final List> blockDi LOG.debug("Found scale datasets {}", (Object)scaleUniqueLabels); for (int level = 0; level < scaleUniqueLabels.length; ++level) { - final DatasetSpec datasetUniqueLabels = DatasetSpec.of(n5Writer, Paths.get(uniqueLabelsPath, scaleUniqueLabels[level]).toString()); + final String uniqueLabelScalePath = N5URI.normalizeGroupPath(uniqueLabelsPath + n5Writer.getGroupSeparator() + scaleUniqueLabels[level]); + final DatasetSpec datasetUniqueLabels = DatasetSpec.of(n5Writer, uniqueLabelScalePath); final TLongObjectMap removedById = new TLongObjectHashMap<>(); final TLongObjectMap addedById = new TLongObjectHashMap<>(); final TLongObjectMap blockDiffs = blockDiffsByLevel.get(level); @@ -264,8 +257,8 @@ public List> persistCanvas(final CachedCellImg blockDiffsAt = new TLongObjectHashMap<>(); blockDiffs.add(blockDiffsAt); - final DatasetSpec targetDataset = DatasetSpec.of(n5Writer, Paths.get(dataset, scaleDatasets[level]).toString()); - final DatasetSpec previousDataset = DatasetSpec.of(n5Writer, Paths.get(dataset, scaleDatasets[level - 1]).toString()); + final DatasetSpec targetDataset = DatasetSpec.of(n5Writer, N5URI.normalizeGroupPath(dataset + n5Writer.getGroupSeparator() + scaleDatasets[level])); + final DatasetSpec previousDataset = DatasetSpec.of(n5Writer, N5URI.normalizeGroupPath(dataset + n5Writer.getGroupSeparator() + scaleDatasets[level - 1])); final double[] targetDownsamplingFactors = N5Helpers.getDownsamplingFactors(n5Writer, targetDataset.dataset); final double[] previousDownsamplingFactors = N5Helpers.getDownsamplingFactors(n5Writer, previousDataset.dataset); @@ -468,13 +461,13 @@ private static BlockDiff createBlockDiffFromCanvas( for (final Pair p : backgroundWithCanvas) { final long newLabel = p.getB().getIntegerLong(); if (newLabel == Label.INVALID) { - for (LabelMultisetEntry iterEntry : p.getA().entrySetWithRef(entry)) { + for (LabelMultisetType.Entry

- * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - *

- * Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - *

- * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - *

- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ -package org.janelia.saalfeldlab.util.n5.universe; - -import org.janelia.saalfeldlab.n5.N5Reader; -import org.janelia.saalfeldlab.n5.N5Writer; -import org.janelia.saalfeldlab.n5.universe.N5Factory; - -import java.io.IOException; -import java.util.HashMap; - -/** - * Factory for various N5 readers and writers. Implementation specific - * parameters can be provided to the factory instance and will be used when - * such implementations are generated and ignored otherwise. Reasonable - * defaults are provided. - * - * @author Stephan Saalfeld - * @author John Bogovic - * @author Igor Pisarev - */ -public class N5FactoryWithCache extends N5Factory { - - private final HashMap writerCache = new HashMap<>(); - private final HashMap readerCache = new HashMap<>(); - - @Override public N5Reader openReader(String url) { - - /* Check if writer is valid (it may have been closed by someone) */ - final N5Reader cachedContainer = readerCache.get(url); - if (cachedContainer != null) { - try { - cachedContainer.getVersion(); - } catch (Exception e) { - readerCache.remove(url).close(); - } - } else { - readerCache.put(url, super.openReader(url)); - } - return readerCache.get(url); - } - - @Override public N5Writer openWriter(String url) { - - /* Check if writer is valid (it may have been closed by someone) */ - final N5Reader cachedContainer = writerCache.get(url); - if (cachedContainer != null) { - try { - cachedContainer.getVersion(); - } catch (Exception e) { - writerCache.remove(url).close(); - if (readerCache.get(url) == cachedContainer) { - readerCache.remove(url); - } - } - } else { - final N5Writer n5Writer = super.openWriter(url); - writerCache.put(url, n5Writer); - if (readerCache.get(url) != null) { - readerCache.remove(url).close(); - } - readerCache.put(url, n5Writer); - } - return writerCache.get(url); - } - - public void clearKey(String url) { - - final var writer = writerCache.remove(url); - if (writer != null) - writer.close(); - - final var reader = readerCache.remove(url); - if (reader != null) - reader.close(); - } - - public void clearCache() { - writerCache.clear(); - readerCache.clear(); - } - - public N5Reader getFromCache(String url) { - return readerCache.get(url); - } -} diff --git a/src/main/java/org/janelia/saalfeldlab/util/n5/universe/N5FactoryWithCache.kt b/src/main/java/org/janelia/saalfeldlab/util/n5/universe/N5FactoryWithCache.kt new file mode 100644 index 000000000..d3b2bca42 --- /dev/null +++ b/src/main/java/org/janelia/saalfeldlab/util/n5/universe/N5FactoryWithCache.kt @@ -0,0 +1,211 @@ +/** + * Copyright (c) 2017-2021, Saalfeld lab, HHMI Janelia + * All rights reserved. + * + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package org.janelia.saalfeldlab.util.n5.universe + +import org.janelia.saalfeldlab.n5.N5Exception +import org.janelia.saalfeldlab.n5.N5Reader +import org.janelia.saalfeldlab.n5.N5URI +import org.janelia.saalfeldlab.n5.N5Writer +import org.janelia.saalfeldlab.n5.hdf5.N5HDF5Reader +import org.janelia.saalfeldlab.n5.universe.N5Factory +import org.slf4j.LoggerFactory +import java.lang.invoke.MethodHandles + +/** + * Factory for various N5 readers and writers. Implementation specific + * parameters can be provided to the factory instance and will be used when + * such implementations are generated and ignored otherwise. Reasonable + * defaults are provided. + * + * @author Stephan Saalfeld + * @author John Bogovic + * @author Igor Pisarev + */ +class N5FactoryWithCache : N5Factory() { + + companion object { + private val LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()) + } + + private val writerCache = HashMap() + private val readerCache = HashMap() + override fun openReader(uri: String): N5Reader { + return getFromReaderCache(uri) ?: getFromWriterCache(uri) ?: super.openReader(uri).let { + if (containerIsReadable(it)) { + readerCache[uri] = it + it + } else { + throw N5ContainerDoesntExist(uri) + } + } + } + + override fun openWriter(uri: String): N5Writer { + return getFromWriterCache(uri) ?: openAndCacheExistingN5Writer(uri) + } + fun createWriter(uri: String): N5Writer { + return getFromWriterCache(uri) ?: createAndCacheN5Writer(uri) + } + + fun openWriterOrNull(uri : String) : N5Writer? = try { + openWriter(uri) + } catch (e : Exception) { + LOG.debug("Unable to open $uri as N5Writer", e) + null + } + + fun openReaderOrNull(uri : String) : N5Reader? = try { + openReader(uri) + } catch (e : Exception) { + LOG.debug("Unable to open $uri as N5Reader", e) + null + } + + fun openWriterElseOpenReader(uri : String) = try { + openWriterOrNull(uri) ?: openReader(uri) + } catch (e : N5Exception) { + if (e.message?.startsWith("No container exists at ") == true) + throw N5ContainerDoesntExist(uri, e) + else throw e + } + + private fun containerIsReadable(reader: N5Reader) = try { + reader.getAttribute("/", "/", String::class.java) + true + } catch (e : Exception ) { + false + } + + private fun containerIsWritable(writer: N5Writer) = try { + val version = writer.getAttribute("/", N5Writer.VERSION_KEY, String::class.java) + if (version == null) + false + else { + writer.setAttribute("/", N5Writer.VERSION_KEY, version) + true + } + } catch (e : Exception ) { + false + } + + /** + * If a cached reader is present, and is valid (i.e., can actually read), return the reader. + * + * + * This has the side effect that if the cached reader is not valid (e.g. it has been closed, + * or can otherwise no longer read) then it will be removed from the cache, and null will be returned. + * Note this can case a removal of a writer from the writer cache, in the case that the reader we are + * removing is also a writer that is present in the writer cache. + * + * @param uri to check the cached reader for + * @return the cached reader, if it exists + */ + private fun getFromReaderCache(uri: String): N5Reader? { + synchronized(readerCache) { + readerCache[uri]?.also { reader -> if (!containerIsReadable(reader)) clearKey(uri) } + return readerCache[uri] + } + } + + /** + * If a cached writer is present, and is valid (i.e., can actually write), return the writer. + * + * + * This has the side effect that if the cached writer is not valid (e.g. it has been closed, + * or can otherwise no longer write) then it will be removed from the cache, and null will be returned. + * Since an N5Writer is a valid N5Reader, when discovering that the cached N5Writer is no longer valid, it + * will also be removed from the reader cache, if it's present. + * + * @param uri to check the cached writer for + * @return the cached writer, if it exists and is valid + */ + private fun getFromWriterCache(uri: String): N5Writer? { + synchronized(writerCache) { + writerCache[uri]?.also { writer -> if (!containerIsWritable(writer)) clearKey(uri) } + return writerCache[uri] + } + } + + private fun openAndCacheExistingN5Writer(uri: String): N5Writer { + val readerWasCached = getFromReaderCache(uri) != null + + val reader = openReader(uri) + if (!containerIsReadable(reader)) + throw N5ContainerDoesntExist(uri) + + /* If we opened explicitly to check, then close now. + * If we are HDF5, we need to close before opening */ + if (!readerWasCached || reader is N5HDF5Reader) + reader.close() + + return createAndCacheN5Writer(uri) + } + + private fun createAndCacheN5Writer(uri: String): N5Writer { + val n5Writer = super.openWriter(uri) + /* See if we have write permissions before we declare success */ + n5Writer.setAttribute("/", N5Reader.VERSION_KEY, n5Writer.version.toString()) + if (readerCache[uri] == null) { + readerCache[uri] = n5Writer + } + writerCache[uri] = n5Writer + return n5Writer + } + + fun clearKey(uri: String) { + val writer = writerCache.remove(uri) + writer?.close() + val reader = readerCache.remove(uri) + reader?.close() + } + + fun clearCache() { + writerCache.clear() + readerCache.clear() + } +} + +class N5DatasetDoesntExist : N5Exception { + + companion object { + private fun displayDataset(dataset: String) = N5URI.normalizeGroupPath(dataset).ifEmpty { "/" } + } + + constructor(uri : String, dataset: String) : super("Dataset \"${displayDataset(dataset)}\" not found in container $uri") + constructor(uri: String, dataset : String, cause: Throwable) : super("Dataset \"${displayDataset(dataset)}\" not found in container $uri", cause) +} + + +class N5ContainerDoesntExist : N5Exception { + + constructor(location: String) : super("Cannot Open $location") + constructor(location: String, cause: Throwable) : super("Cannot Open $location", cause) +} diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/DeviceManager.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/DeviceManager.kt index 3d3a7610f..b6947138d 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/DeviceManager.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/DeviceManager.kt @@ -25,7 +25,7 @@ object DeviceManager { } } - fun closeMidiDevices() { + private fun closeMidiDevices() { activeMidiDevices.removeIf { try { it.close() @@ -37,7 +37,9 @@ object DeviceManager { } } - fun closeDevices() { - closeMidiDevices() - } + fun closeDevices(): Boolean = + if (activeMidiDevices.isNotEmpty()) { + closeMidiDevices() + true + } else false } \ No newline at end of file diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/Paintera.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/Paintera.kt index ab1aebaae..d90d6253e 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/Paintera.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/Paintera.kt @@ -11,6 +11,7 @@ import javafx.event.EventHandler import javafx.scene.Parent import javafx.scene.Scene import javafx.scene.control.Alert +import javafx.scene.control.ButtonType import javafx.scene.input.MouseEvent import javafx.stage.Modality import javafx.stage.Stage @@ -20,6 +21,7 @@ import org.janelia.saalfeldlab.fx.extensions.nonnull import org.janelia.saalfeldlab.fx.ui.Exceptions import org.janelia.saalfeldlab.fx.util.InvokeOnJavaFXApplicationThread import org.janelia.saalfeldlab.paintera.config.ScreenScalesConfig +import org.janelia.saalfeldlab.paintera.state.label.ConnectomicsLabelState import org.janelia.saalfeldlab.paintera.ui.PainteraAlerts import org.janelia.saalfeldlab.paintera.util.logging.LogUtils import org.janelia.saalfeldlab.util.PainteraCache @@ -88,7 +90,7 @@ class Paintera : Application() { notifyPreloader(SplashScreenFinishPreloader()) PlatformImpl.runAndWait { Exceptions.exceptionAlert(Constants.NAME, "Unable to open Paintera project", error).apply { - setOnHidden { exitProcess(Error.UNABLE_TO_DESERIALIZE_PROJECT.code) } + setOnHidden { exitProcess(0); } initModality(Modality.NONE) showAndWait() } @@ -203,6 +205,42 @@ class Paintera : Application() { } fun loadProject(projectDirectory: String? = null) { + projectDirectory?.let { + if (n5Factory.openReaderOrNull(it) == null) { + val alert = PainteraAlerts.alert(Alert.AlertType.WARNING) + alert.headerText = "Paintera Project Not Found" + alert.contentText = """ + No Paintera project at: + ${projectDirectory} + """.trimIndent() + alert.showAndWait() + return + } + } + + paintera.baseView.sourceInfo().apply { + trackSources().forEach { source -> + (getState(source) as? ConnectomicsLabelState)?.let { state -> + val responseButton = state.promptForCommitIfNecessary(paintera.baseView) { index, name -> + """ + Closing current Paintera project. + Uncommitted changes to the canvas will be lost for source $index: $name if skipped. + Uncommitted changes to the fragment-segment-assigment will be stored in the Paintera project (if any) + but can be committed to the data backend, as well + """.trimIndent() + } + + when (responseButton) { + ButtonType.OK -> Unit + ButtonType.NO -> state.skipCommit = true + ButtonType.CANCEL, ButtonType.CLOSE -> return + null -> Unit + } + } + } + } + + if (!paintera.askSaveAndQuit()) { return } @@ -227,7 +265,7 @@ class Paintera : Application() { companion object { @JvmStatic - val n5Factory = N5FactoryWithCache() + val n5Factory = N5FactoryWithCache().apply { cacheAttributes(true) } private val LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()) diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/PainteraMainWindow.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/PainteraMainWindow.kt index 87bf01789..c5d272221 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/PainteraMainWindow.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/PainteraMainWindow.kt @@ -174,7 +174,7 @@ class PainteraMainWindow(val gateway: PainteraGateway = PainteraGateway()) { .setPrettyPrinting() Paintera.n5Factory.gsonBuilder(builder) Paintera.n5Factory.clearKey(projectDirectory.actualDirectory.absolutePath) - Paintera.n5Factory.openWriter(projectDirectory.actualDirectory.absolutePath).use { + Paintera.n5Factory.createWriter(projectDirectory.actualDirectory.absolutePath).use { it.setAttribute("/", PAINTERA_KEY, this) } if (notify) { @@ -291,9 +291,13 @@ class PainteraMainWindow(val gateway: PainteraGateway = PainteraGateway()) { LOG.debug("Quitting!") baseView.stop() projectDirectory.close() - DeviceManager.closeDevices() Platform.exit() - exitProcess(0) + if (DeviceManager.closeDevices()) { + /* due to a bug (https://bugs.openjdk.org/browse/JDK-8232862) when MIDI devices are opened, the thread + * that is created does not exit when closing the devices. If the process is not explicitly exited + * then the application hangs after exiting the window. */ + exitProcess(0) + } } diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/config/LoggingConfig.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/config/LoggingConfig.kt index 767fcb920..a37de627b 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/config/LoggingConfig.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/config/LoggingConfig.kt @@ -5,58 +5,55 @@ import com.google.gson.JsonDeserializationContext import com.google.gson.JsonElement import com.google.gson.JsonObject import com.google.gson.JsonSerializationContext -import javafx.beans.property.BooleanProperty import javafx.beans.property.ObjectProperty import javafx.beans.property.SimpleBooleanProperty import javafx.beans.property.SimpleObjectProperty import javafx.collections.FXCollections +import org.janelia.saalfeldlab.fx.extensions.addTriggeredListener +import org.janelia.saalfeldlab.fx.extensions.nonnull import org.janelia.saalfeldlab.paintera.serialization.GsonExtensions import org.janelia.saalfeldlab.paintera.serialization.GsonExtensions.set import org.janelia.saalfeldlab.paintera.serialization.PainteraSerialization import org.janelia.saalfeldlab.paintera.util.logging.LogUtils -import org.janelia.saalfeldlab.paintera.util.logging.LogUtils.Companion.isRootLoggerName -import org.janelia.saalfeldlab.paintera.util.logging.LogUtils.Logback.Levels.Companion.levels +import org.janelia.saalfeldlab.paintera.util.logging.LogUtils.isRootLoggerName import org.scijava.plugin.Plugin import java.lang.reflect.Type class LoggingConfig { - private val _rootLoggerLevel = SimpleObjectProperty(LogUtils.rootLoggerLevel ?: defaultLogLevel) - .also { it.addListener { _, _, level -> LogUtils.rootLoggerLevel = level } } - var rootLoggerLevel: Level - get() = _rootLoggerLevel.value - set(level) = _rootLoggerLevel.set(level) + private val loggerLevels = FXCollections.observableHashMap>() + val unmodifiableLoggerLevels = FXCollections.unmodifiableObservableMap(loggerLevels)!! - fun rootLoggerLevelProperty(): ObjectProperty = _rootLoggerLevel + val rootLoggerLevelProperty = SimpleObjectProperty(LogUtils.rootLoggerLevel ?: DEFAULT_LOG_LEVEL) + .apply { addTriggeredListener { _, _, level -> LogUtils.rootLoggerLevel = level } } + var rootLoggerLevel: Level by rootLoggerLevelProperty.nonnull() - private val loggerLevels = FXCollections.observableHashMap>() + val isLoggingEnabledProperty = SimpleBooleanProperty(DEFAULT_IS_LOGGING_ENABLED) + .apply { + addTriggeredListener { _, _, new -> + LogUtils.setLoggingEnabled(new) + loggerLevels.forEach { (logger, level) -> LogUtils.setLogLevelFor(logger, level.get()) } + } + } + var isLoggingEnabled: Boolean by isLoggingEnabledProperty.nonnull() + + val isLoggingToConsoleEnabledProperty = SimpleBooleanProperty(DEFAULT_IS_LOGGING_TO_CONSOLE_ENABLED) + .apply { + addTriggeredListener { _, _, new -> + LogUtils.setLoggingToConsoleEnabled(new) + loggerLevels.forEach { (logger, level) -> LogUtils.setLogLevelFor(logger, level.get()) } + } + } + var isLoggingToConsoleEnabled: Boolean by isLoggingEnabledProperty.nonnull() - val unmodifiableLoggerLevels - get() = FXCollections.unmodifiableObservableMap(loggerLevels) - - private val _isLoggingEnabled = SimpleBooleanProperty(defaultIsLoggingEnabled) - .also { it.addListener { _, _, new -> LogUtils.setLoggingEnabled(new) } } - .also { LogUtils.setLoggingEnabled(it.value) } - var isLoggingEnabled: Boolean - get() = _isLoggingEnabled.value - set(enabled) = _isLoggingEnabled.set(enabled) - val loggingEnabledProperty: BooleanProperty = _isLoggingEnabled - - private val _isLoggingToConsoleEnabled = SimpleBooleanProperty(defaultIsLoggingToConsoleEnabled) - .also { it.addListener { _, _, new -> LogUtils.setLoggingToConsoleEnabled(new) } } - .also { LogUtils.setLoggingToConsoleEnabled(it.value) } - var isLoggingToConsoleEnabled: Boolean - get() = _isLoggingToConsoleEnabled.value - set(enabled) = _isLoggingToConsoleEnabled.set(enabled) - val loggingToConsoleEnabledProperty: BooleanProperty = _isLoggingToConsoleEnabled - - private val _isLoggingToFileEnabled = SimpleBooleanProperty(defaultIsLoggingToFileEnabled) - .also { it.addListener { _, _, new -> LogUtils.setLoggingToFileEnabled(new) } } - .also { LogUtils.setLoggingToFileEnabled(it.value) } - var isLoggingToFileEnabled: Boolean - get() = _isLoggingToFileEnabled.value - set(enabled) = _isLoggingToFileEnabled.set(enabled) - val loggingToFileEnabledProperty: BooleanProperty = _isLoggingToFileEnabled + val isLoggingToFileEnabledProperty = SimpleBooleanProperty(DEFAULT_IS_LOGGING_TO_FILE_ENABLED) + .apply { + addTriggeredListener { _, _, new -> + LogUtils.setLoggingToFileEnabled(new) + loggerLevels.forEach { (logger, level) -> LogUtils.setLogLevelFor(logger, level.get()) } + } + } + var isLoggingToFileEnabled: Boolean by isLoggingEnabledProperty.nonnull() fun setLogLevelFor(name: String, level: String) = LogUtils.Logback.Levels[level]?.let { setLogLevelFor(name, it) } @@ -82,19 +79,16 @@ class LoggingConfig { companion object { @JvmStatic - val defaultLogLevel = Level.INFO + val DEFAULT_LOG_LEVEL: Level = Level.INFO - @JvmStatic - val defaultIsLoggingEnabled = true + const val DEFAULT_IS_LOGGING_ENABLED = true - @JvmStatic - val defaultIsLoggingToConsoleEnabled = true + const val DEFAULT_IS_LOGGING_TO_CONSOLE_ENABLED = true - @JvmStatic - val defaultIsLoggingToFileEnabled = true + const val DEFAULT_IS_LOGGING_TO_FILE_ENABLED = true - fun String.toLogbackLevel(defaultLevel: Level = defaultLogLevel) = Level.toLevel(this, defaultLevel) + fun String.toLogbackLevel(defaultLevel: Level = DEFAULT_LOG_LEVEL) = Level.toLevel(this, defaultLevel) } @Plugin(type = PainteraSerialization.PainteraAdapter::class) diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/config/LoggingConfigNode.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/config/LoggingConfigNode.kt index 776ce9b45..e1b2a781f 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/config/LoggingConfigNode.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/config/LoggingConfigNode.kt @@ -31,7 +31,7 @@ class LoggingConfigNode(private val config: LoggingConfig) { val node: Node get() { - val rootLevelChoiceBox = logLevelChoiceBox(config.rootLoggerLevelProperty()) + val rootLevelChoiceBox = logLevelChoiceBox(config.rootLoggerLevelProperty) val loggerLevelGrid = GridPane() loggerLevelGrid.columnConstraints.setAll( ColumnConstraints().also { it.hgrow = Priority.ALWAYS }, @@ -66,27 +66,25 @@ class LoggingConfigNode(private val config: LoggingConfig) { private val toggleLogEnableNode: Node get() { - - val userHome = System.getProperty("user.home") ?: "\$HOME" - val logFilePath = "$userHome/.paintera/logs/paintera.${LogUtils.painteraLogFilenameBase}.log" - - val isEnabledCheckBox = CheckBox("Enable logging") - .also { it.selectedProperty().bindBidirectional(config.loggingEnabledProperty) } - val isLoggingToConsoleEnabled = CheckBox("Log to console") - .also { it.selectedProperty().bindBidirectional(config.loggingToConsoleEnabledProperty) } - .also { it.disableProperty().bind(config.loggingEnabledProperty.not()) } - val isLoggingToFileEnabled = CheckBox("Log to file") - .also { it.selectedProperty().bindBidirectional(config.loggingToFileEnabledProperty) } - .also { it.disableProperty().bind(config.loggingEnabledProperty.not()) } - .also { it.tooltip = Tooltip("Log file located at `$logFilePath'") } - .also { it.contentDisplay = ContentDisplay.RIGHT } - .also { it.graphicTextGap = 25.0 } - .also { - it.graphic = Buttons.withTooltip(null, "Copy log file path (`$logFilePath') to clipboard") { - Clipboard.getSystemClipboard().setContent(ClipboardContent().also { it.putString(logFilePath) }) - }.also { it.graphic = FontAwesome[FontAwesomeIcon.COPY, 2.0] } + val isEnabledCheckBox = CheckBox("Enable logging").also { + it.selectedProperty().bindBidirectional(config.isLoggingEnabledProperty) + } + val isLoggingToConsoleEnabled = CheckBox("Log to console").apply { + selectedProperty().bindBidirectional(config.isLoggingToConsoleEnabledProperty) + disableProperty().bind(config.isLoggingEnabledProperty.not()) + } + val isLoggingToFileEnabled = CheckBox("Log to file").apply { + selectedProperty().bindBidirectional(config.isLoggingToFileEnabledProperty) + disableProperty().bind(config.isLoggingEnabledProperty.not()) + tooltip = Tooltip("Log file located at `${LogUtils.painteraLogFilePath}'") + contentDisplay = ContentDisplay.RIGHT + graphicTextGap = 25.0 + graphic = Buttons.withTooltip(null, "Copy log file path (`${LogUtils.painteraLogFilePath}') to clipboard") { + Clipboard.getSystemClipboard().setContent(ClipboardContent().also { content -> content.putString(LogUtils.painteraLogFilePath) }) + }.also { button -> + button.graphic = FontAwesome[FontAwesomeIcon.COPY, 2.0] } - + } return VBox( isEnabledCheckBox, isLoggingToConsoleEnabled, @@ -96,7 +94,7 @@ class LoggingConfigNode(private val config: LoggingConfig) { private fun logLevelChoiceBox(logLevelProperty: ObjectProperty?): ChoiceBox { val choiceBox = ChoiceBox(FXCollections.observableList(LogUtils.Logback.Levels.levels)) - choiceBox.value = LoggingConfig.defaultLogLevel + choiceBox.value = LoggingConfig.DEFAULT_LOG_LEVEL logLevelProperty?.let { choiceBox.valueProperty().bindBidirectional(it) } return choiceBox } diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/config/PainteraDirectoriesConfig.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/config/PainteraDirectoriesConfig.kt new file mode 100644 index 000000000..a57804c6a --- /dev/null +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/config/PainteraDirectoriesConfig.kt @@ -0,0 +1,186 @@ +package org.janelia.saalfeldlab.paintera.config + +import com.google.gson.* +import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon +import dev.dirs.ProjectDirectories +import javafx.application.Platform +import javafx.beans.property.SimpleStringProperty +import javafx.event.EventHandler +import javafx.geometry.Pos +import javafx.scene.control.* +import javafx.scene.layout.ColumnConstraints +import javafx.scene.layout.GridPane +import javafx.scene.layout.Priority +import javafx.scene.layout.VBox +import org.janelia.saalfeldlab.fx.extensions.nonnull +import org.janelia.saalfeldlab.paintera.config.PainteraDirectoriesConfig.Companion.APPLICATION_DIRECTORIES +import org.janelia.saalfeldlab.paintera.config.PainteraDirectoriesConfig.Companion.TEMP_DIRECTORY +import org.janelia.saalfeldlab.paintera.serialization.GsonExtensions.get +import org.janelia.saalfeldlab.paintera.serialization.GsonExtensions.set +import org.janelia.saalfeldlab.paintera.serialization.PainteraSerialization +import org.janelia.saalfeldlab.paintera.ui.FontAwesome +import org.janelia.saalfeldlab.paintera.ui.PainteraAlerts +import org.scijava.plugin.Plugin +import java.lang.reflect.Type + +class PainteraDirectoriesConfig { + + private val appCacheDirProperty = SimpleStringProperty(APPLICATION_DIRECTORIES.cacheDir).apply { + addListener { _, _, new -> if (new.isBlank()) appCacheDir = APPLICATION_DIRECTORIES.cacheDir } + } + var appCacheDir: String by appCacheDirProperty.nonnull() + + private val tmpDirProperty = SimpleStringProperty(TEMP_DIRECTORY).apply { + addListener { _, _, new -> if (new.isBlank()) tmpDir = TEMP_DIRECTORY } + } + var tmpDir: String by tmpDirProperty.nonnull() + + internal val allDefault + get() = appCacheDir == APPLICATION_DIRECTORIES.cacheDir && tmpDir == TEMP_DIRECTORY + + companion object { + @JvmStatic + internal val APPLICATION_DIRECTORIES = ProjectDirectories.from("org", "janelia", "Paintera") + + @JvmStatic + internal val TEMP_DIRECTORY = System.getProperty("java.io.tmpdir") + } +} + +class PainteraDirectoriesConfigNode(val config: PainteraDirectoriesConfig) : TitledPane() { + + init { + isExpanded = false + text = "Application Directories" + content = createNode() + } + + private fun createNode() = GridPane().apply { + addCacheDirectoryConfigRow(0) + addTempDirectoryConfigRow(1) + + columnConstraints.add(ColumnConstraints().apply { hgrow = Priority.NEVER }) + columnConstraints.add(ColumnConstraints().apply { hgrow = Priority.ALWAYS }) + columnConstraints.add(ColumnConstraints().apply { hgrow = Priority.NEVER }) + columnConstraints.add(ColumnConstraints().apply { hgrow = Priority.NEVER }) + } + + private fun GridPane.addCacheDirectoryConfigRow(row: Int) { + Label("Cache Directory").also { + add(it, 0, row) + it.alignment = Pos.BASELINE_LEFT + it.minWidth = Label.USE_PREF_SIZE + } + val cacheTextField = TextField(config.appCacheDir).also { + VBox.setVgrow(it, Priority.NEVER) + it.maxWidth = Double.MAX_VALUE + it.prefWidth - Double.MAX_VALUE + it.textProperty().addListener { _, _, new -> + if (new.isBlank()) { + it.text = APPLICATION_DIRECTORIES.cacheDir + Platform.runLater { it.positionCaret(0) } + } else { + config.appCacheDir = new + } + } + add(it, 1, row) + } + Button().also { + it.graphic = FontAwesome[FontAwesomeIcon.UNDO] + it.onAction = EventHandler { cacheTextField.text = APPLICATION_DIRECTORIES.cacheDir } + add(it, 2, row) + } + Button().also { + it.graphic = FontAwesome[FontAwesomeIcon.QUESTION] + it.onAction = EventHandler { + PainteraAlerts.information("Ok", true).also { alert -> + alert.title = "Cache Directory" + alert.headerText = alert.title + alert.dialogPane.content = TextArea().also { area -> + area.isWrapText = true + area.isEditable = false + area.text = """ + Directory used for storing potentially large, temporary data files used during the application runtime for caching non-persisted data. + + + By default, the cache directory is shared for all instances of the application, across all projects. + """.trimIndent() + } + }.showAndWait() + } + add(it, 3, row) + } + } + + private fun GridPane.addTempDirectoryConfigRow(row: Int) { + Label("Temp Directory").also { + add(it, 0, row) + it.alignment = Pos.BASELINE_LEFT + it.minWidth = Label.USE_PREF_SIZE + } + val tempDirTextField = TextField(config.tmpDir).also { + VBox.setVgrow(it, Priority.NEVER) + it.maxWidth = Double.MAX_VALUE + it.prefWidth - Double.MAX_VALUE + it.textProperty().addListener { _, _, new -> + if (new.isBlank()) { + it.text = TEMP_DIRECTORY + Platform.runLater { it.positionCaret(0) } + } else { + config.tmpDir = new + } + } + add(it, 1, row) + } + Button().also { + it.graphic = FontAwesome[FontAwesomeIcon.UNDO] + it.onAction = EventHandler { tempDirTextField.text = TEMP_DIRECTORY } + add(it, 2, row) + } + Button().also { + it.graphic = FontAwesome[FontAwesomeIcon.QUESTION] + it.onAction = EventHandler { + PainteraAlerts.information("Ok", true).also { alert -> + alert.title = "Temp Directory" + alert.headerText = alert.title + alert.dialogPane.content = TextArea().also { area -> + area.isEditable = false + area.isWrapText = true + area.text = """ + Directory used for storing temporary non-data files used during the application runtime. + + + By default, the temp directory is project-specific, and will change if a new project is loaded. + """.trimIndent() + } + }.showAndWait() + } + add(it, 3, row) + } + } +} + +@Plugin(type = PainteraSerialization.PainteraAdapter::class) +class PainteraDirectoriesConfigSerializer : PainteraSerialization.PainteraAdapter { + + override fun getTargetClass() = PainteraDirectoriesConfig::class.java + + override fun serialize(src: PainteraDirectoriesConfig, typeOfSrc: Type, context: JsonSerializationContext): JsonElement { + return if (src.allDefault) JsonNull.INSTANCE + else JsonObject().also { + if (src.appCacheDir != APPLICATION_DIRECTORIES.cacheDir) + it[src::appCacheDir.name] = src.appCacheDir + if (src.tmpDir != TEMP_DIRECTORY) + it[src::tmpDir.name] = src.tmpDir + } + } + + override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?): PainteraDirectoriesConfig { + return PainteraDirectoriesConfig().apply { + json?.let { + it[::appCacheDir.name, { model: String -> appCacheDir = model }] + it[::tmpDir.name, { model: String -> tmpDir = model }] + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/config/ScreenScalesConfig.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/config/ScreenScalesConfig.kt index 7a297e2da..8af420afb 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/config/ScreenScalesConfig.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/config/ScreenScalesConfig.kt @@ -55,7 +55,7 @@ class ScreenScalesConfig @JvmOverloads constructor(vararg initialScales: Double companion object { - private val DEFAULT_SCREEN_SCALES = doubleArrayOf(1.0, 0.5, 0.25, 0.125, 0.0625) + private val DEFAULT_SCREEN_SCALES = doubleArrayOf(1.0, 0.5, 0.25, 0.125) @JvmStatic fun defaultScreenScalesCopy(): DoubleArray { diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/config/SegmentAnythingConfig.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/config/SegmentAnythingConfig.kt new file mode 100644 index 000000000..1cb23885b --- /dev/null +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/config/SegmentAnythingConfig.kt @@ -0,0 +1,144 @@ +package org.janelia.saalfeldlab.paintera.config + +import com.google.gson.* +import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon +import javafx.application.Platform +import javafx.beans.property.SimpleStringProperty +import javafx.event.EventHandler +import javafx.geometry.Pos +import javafx.scene.control.Button +import javafx.scene.control.Label +import javafx.scene.control.TextField +import javafx.scene.control.TitledPane +import javafx.scene.layout.ColumnConstraints +import javafx.scene.layout.GridPane +import javafx.scene.layout.Priority +import javafx.scene.layout.VBox +import org.janelia.saalfeldlab.fx.extensions.nonnull +import org.janelia.saalfeldlab.paintera.config.SegmentAnythingConfig.Companion.DEFAULT_MODEL_LOCATION +import org.janelia.saalfeldlab.paintera.config.SegmentAnythingConfig.Companion.DEFAULT_SERVICE_URL +import org.janelia.saalfeldlab.paintera.serialization.GsonExtensions.get +import org.janelia.saalfeldlab.paintera.serialization.GsonExtensions.set +import org.janelia.saalfeldlab.paintera.serialization.PainteraSerialization +import org.janelia.saalfeldlab.paintera.ui.FontAwesome +import org.scijava.plugin.Plugin +import java.lang.reflect.Type + +class SegmentAnythingConfig { + + private val serviceUrlProperty = SimpleStringProperty(System.getenv(SAM_SERVICE_HOST_ENV) ?: DEFAULT_SERVICE_URL).apply { + addListener { _, _, new -> if (new.isBlank()) serviceUrl = DEFAULT_SERVICE_URL } + } + var serviceUrl: String by serviceUrlProperty.nonnull() + + private val modelLocationProperty = SimpleStringProperty(DEFAULT_MODEL_LOCATION).apply { + addListener { _, _, new -> if (new.isBlank()) modelLocation = DEFAULT_MODEL_LOCATION } + } + var modelLocation: String by modelLocationProperty.nonnull() + + internal val allDefault + get() = serviceUrl == DEFAULT_SERVICE_URL && modelLocation == DEFAULT_MODEL_LOCATION + + companion object { + private const val SAM_SERVICE_HOST_ENV = "SAM_SERVICE_HOST" + internal const val DEFAULT_SERVICE_URL = "http://gpu3.saalfeldlab.org/embedded_model" + internal const val DEFAULT_MODEL_LOCATION = "sam/sam_vit_h_4b8939.onnx" + } +} + +class SegmentAnythingConfigNode(val config: SegmentAnythingConfig) : TitledPane() { + + init { + isExpanded = false + text = "SAM Service" + content = createNode() + } + + private fun createNode() = GridPane().apply { + addServiceUrlConfigRow(0) + addModelLocationConfigRow(1) + + columnConstraints.add(ColumnConstraints().apply { hgrow = Priority.NEVER }) + columnConstraints.add(ColumnConstraints().apply { hgrow = Priority.ALWAYS }) + columnConstraints.add(ColumnConstraints().apply { hgrow = Priority.NEVER }) + } + + private fun GridPane.addServiceUrlConfigRow(row : Int) { + Label("Service URL").also { + add(it, 0, row) + it.alignment = Pos.BASELINE_LEFT + it.minWidth = Label.USE_PREF_SIZE + } + val serviceTextField = TextField(config.serviceUrl).also { + VBox.setVgrow(it, Priority.NEVER) + it.maxWidth = Double.MAX_VALUE + it.prefWidth - Double.MAX_VALUE + it.textProperty().addListener { _, _, new -> + if (new.isBlank()) { + it.text = DEFAULT_SERVICE_URL + Platform.runLater { it.positionCaret(0) } + } else { + config.serviceUrl = new + } + } + add(it, 1, row) + } + Button().also { + it.graphic = FontAwesome[FontAwesomeIcon.UNDO] + it.onAction = EventHandler { serviceTextField.text = DEFAULT_SERVICE_URL } + add(it, 2, row) + } + } + + private fun GridPane.addModelLocationConfigRow(row : Int) { + Label("Model Location").also { + add(it, 0, row) + it.alignment = Pos.BASELINE_LEFT + it.minWidth = Label.USE_PREF_SIZE + } + val modelTextField = TextField(config.modelLocation).also { + VBox.setVgrow(it, Priority.NEVER) + it.maxWidth = Double.MAX_VALUE + it.prefWidth - Double.MAX_VALUE + it.textProperty().addListener { _, _, new -> + if (new.isBlank()) { + it.text = DEFAULT_MODEL_LOCATION + Platform.runLater { it.positionCaret(0) } + } else { + config.modelLocation = new + } + } + add(it, 1, row) + } + Button().also { + it.graphic = FontAwesome[FontAwesomeIcon.UNDO] + it.onAction = EventHandler { modelTextField.text = DEFAULT_MODEL_LOCATION } + add(it, 2, row) + } + } +} + +@Plugin(type = PainteraSerialization.PainteraAdapter::class) +class SamServiceConfigSerializer : PainteraSerialization.PainteraAdapter { + + override fun getTargetClass() = SegmentAnythingConfig::class.java + + override fun serialize(src: SegmentAnythingConfig, typeOfSrc: Type, context: JsonSerializationContext): JsonElement { + return if (src.allDefault) JsonNull.INSTANCE + else JsonObject().also { + if (src.serviceUrl != DEFAULT_SERVICE_URL) + it[src::serviceUrl.name] = src.serviceUrl + if (src.modelLocation != DEFAULT_MODEL_LOCATION) + it[src::modelLocation.name] = src.modelLocation + } + } + + override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?): SegmentAnythingConfig { + return SegmentAnythingConfig().apply { + json?.let { + it[::serviceUrl.name, { model: String -> serviceUrl = model }] + it[::modelLocation.name, { model: String -> modelLocation = model }] + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationController.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationController.kt index 3251e74ce..ffc68366e 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationController.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationController.kt @@ -4,6 +4,7 @@ import bdv.fx.viewer.ViewerPanelFX import bdv.viewer.TransformListener import javafx.beans.property.ObjectProperty import javafx.beans.property.SimpleBooleanProperty +import javafx.beans.property.SimpleDoubleProperty import javafx.beans.property.SimpleObjectProperty import javafx.beans.value.ChangeListener import javafx.collections.FXCollections @@ -21,7 +22,6 @@ import net.imglib2.* import net.imglib2.algorithm.morphology.distance.DistanceTransform import net.imglib2.converter.BiConverter import net.imglib2.converter.Converters -import net.imglib2.converter.RealRandomArrayAccessible import net.imglib2.converter.logical.Logical import net.imglib2.converter.read.BiConvertedRealRandomAccessible import net.imglib2.img.array.ArrayImgFactory @@ -88,7 +88,7 @@ class ShapeInterpolationController>( private val slicesAndInterpolants = SlicesAndInterpolants() - var sliceDepthProperty: ObjectProperty = SimpleObjectProperty() + val sliceDepthProperty = SimpleDoubleProperty(0.0) private var sliceDepth: Double by sliceDepthProperty.nonnull() val isBusyProperty = SimpleBooleanProperty(false, "Shape Interpolation Controller is Busy") @@ -99,9 +99,11 @@ class ShapeInterpolationController>( val isControllerActive: Boolean get() = controllerState != ControllerState.Off - private val sliceAtCurrentDepthBinding = sliceDepthProperty.createNonNullValueBinding(slicesAndInterpolants) { slicesAndInterpolants.getSliceAtDepth(it) } + private val sliceAtCurrentDepthBinding = sliceDepthProperty.createNonNullValueBinding(slicesAndInterpolants) { slicesAndInterpolants.getSliceAtDepth(it.toDouble()) } private val sliceAtCurrentDepth by sliceAtCurrentDepthBinding.nullableVal() + val currentSliceMaskInterval get() = sliceAtCurrentDepth?.maskBoundingBox + val numSlices: Int get() = slicesAndInterpolants.slices.size var activeSelectionAlpha = (AbstractHighlightingARGBStream.DEFAULT_ACTIVE_FRAGMENT_ALPHA ushr 24) / 255.0 @@ -172,9 +174,14 @@ class ShapeInterpolationController>( addSelection(maskPaintInterval) } - fun deleteCurrentSliceOrInterpolant() { + /** + * Delete current slice or interpolant + * + * @return the global interval of the mask just removed (useful for repainting after removing) + */ + fun deleteCurrentSliceOrInterpolant(): RealInterval? { slicesAndInterpolants.removeIfInterpolantAt(currentDepth) - slicesAndInterpolants.removeSliceAtDepth(currentDepth) + return slicesAndInterpolants.removeSliceAtDepth(currentDepth)?.globalBoundingBox } fun deleteCurrentSlice() { @@ -480,17 +487,15 @@ class ShapeInterpolationController>( fillMasks += constantInvalid } + var compositeFill: RealRandomAccessible = fillMasks[0] + for ((index, dataMask) in fillMasks.withIndex()) { + if (index == 0) continue - val compositeFillMask = RealRandomArrayAccessible(fillMasks, { sources: Array, output: UnsignedLongType -> - val label: Long = sources - .map { it.get() } - .firstOrNull { it.isInterpolationLabel } - ?: Label.INVALID - - if (output.get() != label) { - output.set(label) + compositeFill = compositeFill.convertWith(dataMask, UnsignedLongType(Label.INVALID)) { composite, mask, result -> + val maskVal = mask.get() + result.setInteger(if (maskVal != Label.INVALID) maskVal else composite.get()) } - }, UnsignedLongType(Label.INVALID)) + } val interpolants = slicesAndInterpolants.interpolants val dataMasks: MutableList> = mutableListOf() @@ -501,18 +506,17 @@ class ShapeInterpolationController>( } } + var compositeInterpolation: RealRandomAccessible = dataMasks.getOrNull(0) ?: ConstantUtils.constantRealRandomAccessible(UnsignedLongType(Label.INVALID), compositeFill.numDimensions()) + for ((index, dataMask) in dataMasks.withIndex()) { + if (index == 0) continue - val interpolatedArrayMask = RealRandomArrayAccessible(dataMasks, { sources: Array, output: UnsignedLongType -> - - val label = sources - .firstOrNull { it.get().isInterpolationLabel }?.get() - ?: Label.INVALID - - if (output.get() != label) { - output.set(label) + compositeInterpolation = compositeInterpolation.convertWith(dataMask, UnsignedLongType(Label.INVALID)) { composite, mask, result -> + val maskVal = mask.get() + result.setInteger(if (maskVal != Label.INVALID) maskVal else composite.get()) } - }, UnsignedLongType(Label.INVALID)) - val compositeMaskInGlobal = BiConvertedRealRandomAccessible(compositeFillMask, interpolatedArrayMask, Supplier { + } + + val compositeMaskInGlobal = BiConvertedRealRandomAccessible(compositeFill, compositeInterpolation, Supplier { BiConverter { fillValue: UnsignedLongType, interpolationValue: UnsignedLongType, compositeValue: UnsignedLongType -> val aVal = fillValue.get() val aOrB = if (aVal.isInterpolationLabel) fillValue else interpolationValue @@ -629,62 +633,31 @@ class ShapeInterpolationController>( .affine(oldToNewMask) .interval(oldIntervalInNew) + + /* We want to use the old mask as the backing mask, and have a new writable one on top. + * So let's re-use the images this mask created, and replace them with the old mask images (transformed) */ val newImg = newMask.viewerImg.wrappedSource val newVolatileImg = newMask.volatileViewerImg.wrappedSource - val compositeMask = Converters.convert( - oldInNew.extendValue(Label.INVALID), - newImg.extendValue(Label.INVALID), - { oldVal, newVal, result -> - val new = newVal.get() - if (new != Label.INVALID) { - result.set(new) - } else result.set(oldVal) - }, - UnsignedLongType(Label.INVALID) - ).interval(newImg) - - val compositeVolatileMask = Converters.convert( - oldInNewVolatile.extendValue(VolatileUnsignedLongType(Label.INVALID)), - newVolatileImg.extendValue(VolatileUnsignedLongType(Label.INVALID)), - { original, overlay, composite -> - - var checkOriginal = false - if (overlay.isValid) { - val overlayVal = overlay.get().get() - if (overlayVal != Label.INVALID) { - composite.get().set(overlayVal) - composite.isValid = true - } else checkOriginal = true - } else checkOriginal = true - if (checkOriginal) { - if (original.isValid) { - composite.set(original) - composite.isValid = true - } else composite.isValid = false - } - composite.isValid = true - }, - VolatileUnsignedLongType(Label.INVALID) - ).interval(newVolatileImg) - newMask.apply { - updateBackingImages( - compositeMask to compositeVolatileMask, - newImg to newVolatileImg - ) - /* Replace old slice info */ - slicesAndInterpolants.removeSlice(oldSlice) - - val newSlice = SliceInfo( - newMask, - paintera().manager().transform, - FinalRealInterval( - oldIntervalInNew.minAsDoubleArray().also { it[2] = 0.0 }, - oldIntervalInNew.maxAsDoubleArray().also { it[2] = 0.0 } - ) + newMask.viewerImg.wrappedSource = oldInNew + newMask.volatileViewerImg.wrappedSource = oldInNewVolatile + + /* then we pop the `newMask` back in front, as a writable layer */ + newMask.pushNewImageLayer(newImg to newVolatileImg) + + /* Replace old slice info */ + slicesAndInterpolants.removeSlice(oldSlice) + + val newSlice = SliceInfo( + newMask, + paintera().manager().transform, + FinalRealInterval( + oldIntervalInNew.minAsDoubleArray().also { it[2] = 0.0 }, + oldIntervalInNew.maxAsDoubleArray().also { it[2] = 0.0 } ) - slicesAndInterpolants.add(currentDepth, newSlice) - } + ) + slicesAndInterpolants.add(currentDepth, newSlice) + newMask } ?: let { val maskInfo = MaskInfo(0, currentLevel) source.createViewerMask(maskInfo, activeViewer!!, paintDepth = null, setMask = false) diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/actions/paint/SmoothAction.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/actions/paint/SmoothAction.kt index 7ecdb7496..cdb0e30a5 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/actions/paint/SmoothAction.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/actions/paint/SmoothAction.kt @@ -1,41 +1,46 @@ package org.janelia.saalfeldlab.paintera.control.actions.paint +import com.google.common.util.concurrent.ThreadFactoryBuilder import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon +import javafx.animation.KeyFrame +import javafx.animation.KeyValue +import javafx.animation.Timeline +import javafx.beans.binding.Bindings import javafx.beans.property.SimpleBooleanProperty import javafx.beans.property.SimpleDoubleProperty -import javafx.beans.property.SimpleIntegerProperty import javafx.beans.property.SimpleLongProperty +import javafx.beans.property.SimpleObjectProperty import javafx.beans.value.ChangeListener +import javafx.concurrent.Worker import javafx.event.ActionEvent import javafx.event.Event import javafx.event.EventHandler +import javafx.geometry.Orientation import javafx.geometry.Pos import javafx.scene.control.* +import javafx.scene.input.KeyCode +import javafx.scene.input.KeyEvent import javafx.scene.layout.HBox import javafx.scene.layout.Priority import javafx.scene.layout.VBox -import kotlinx.coroutines.Job -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking +import javafx.util.Duration +import kotlinx.coroutines.* import net.imglib2.FinalInterval import net.imglib2.FinalRealInterval import net.imglib2.Interval import net.imglib2.RealInterval -import net.imglib2.algorithm.convolution.kernel.Kernel1D -import net.imglib2.algorithm.convolution.kernel.SeparableKernelConvolution -import net.imglib2.algorithm.gauss3.Gauss3 +import net.imglib2.algorithm.convolution.fast_gauss.FastGauss import net.imglib2.algorithm.lazy.Lazy import net.imglib2.cache.img.CachedCellImg -import net.imglib2.converter.Converters import net.imglib2.img.basictypeaccess.AccessFlags import net.imglib2.img.cell.CellGrid import net.imglib2.loops.LoopBuilder +import net.imglib2.parallel.Parallelization import net.imglib2.realtransform.AffineTransform3D import net.imglib2.type.numeric.integer.UnsignedLongType import net.imglib2.type.numeric.real.DoubleType import net.imglib2.util.Intervals -import net.imglib2.view.Views +import net.imglib2.view.BundleView import org.janelia.saalfeldlab.fx.Tasks import org.janelia.saalfeldlab.fx.UtilityTask import org.janelia.saalfeldlab.fx.actions.Action @@ -45,8 +50,11 @@ import org.janelia.saalfeldlab.fx.extensions.nonnull import org.janelia.saalfeldlab.fx.extensions.nonnullVal import org.janelia.saalfeldlab.fx.ui.NumberField import org.janelia.saalfeldlab.fx.ui.ObjectField.SubmitOn +import org.janelia.saalfeldlab.fx.util.InvokeOnJavaFXApplicationThread +import org.janelia.saalfeldlab.labels.blocks.LabelBlockLookupKey import org.janelia.saalfeldlab.paintera.Paintera import org.janelia.saalfeldlab.paintera.Style.ADD_GLYPH +import org.janelia.saalfeldlab.paintera.control.actions.paint.SmoothActionVerifiedState.Companion.verifyState import org.janelia.saalfeldlab.paintera.control.modes.PaintLabelMode import org.janelia.saalfeldlab.paintera.control.modes.PaintLabelMode.statePaintContext import org.janelia.saalfeldlab.paintera.control.tools.paint.StatePaintContext @@ -59,27 +67,21 @@ import org.janelia.saalfeldlab.paintera.state.SourceStateBackendN5 import org.janelia.saalfeldlab.paintera.state.label.ConnectomicsLabelState import org.janelia.saalfeldlab.paintera.state.metadata.MultiScaleMetadataState import org.janelia.saalfeldlab.paintera.ui.FontAwesome -import org.janelia.saalfeldlab.util.extendZero -import org.janelia.saalfeldlab.util.intersect -import org.janelia.saalfeldlab.util.interval -import org.janelia.saalfeldlab.util.union -import kotlin.collections.List -import kotlin.collections.filter -import kotlin.collections.firstOrNull -import kotlin.collections.flatMap -import kotlin.collections.forEach -import kotlin.collections.map -import kotlin.collections.min -import kotlin.collections.minusAssign -import kotlin.collections.mutableListOf -import kotlin.collections.plus -import kotlin.collections.plusAssign +import org.janelia.saalfeldlab.paintera.util.IntervalHelpers.Companion.smallestContainingInterval +import org.janelia.saalfeldlab.util.* +import org.slf4j.LoggerFactory +import java.lang.invoke.MethodHandles +import java.util.concurrent.LinkedBlockingQueue +import java.util.concurrent.RejectedExecutionException +import java.util.concurrent.ThreadPoolExecutor +import java.util.concurrent.TimeUnit import kotlin.collections.set -import kotlin.collections.toList import kotlin.math.floor import kotlin.math.log10 import kotlin.math.pow import kotlin.math.roundToLong +import kotlin.properties.Delegates +import kotlin.reflect.KMutableProperty0 import net.imglib2.type.label.Label as Imglib2Label @@ -100,78 +102,125 @@ open class MenuAction(val label: String) : Action(Event.ANY) { } -object SmoothAction : MenuAction("Smooth") { +class SmoothActionVerifiedState { + internal lateinit var labelSource: ConnectomicsLabelState<*, *> + internal lateinit var paintContext: StatePaintContext<*, *> + internal var mipMapLevel by Delegates.notNull() - private var smoothTask: UtilityTask? = null - private lateinit var updateSmoothMask: ((Boolean) -> Unit) - private var finalizeSmoothing = false; + fun Action.verifyState() { + verify(::labelSource, "Label Source is Active") { paintera.currentSource as? ConnectomicsLabelState<*, *> } + verify(::paintContext, "Paint Label Mode has StatePaintContext") { statePaintContext } + verify(::mipMapLevel, "Viewer is Focused") { paintera.activeViewer.get()?.state?.bestMipMapLevel } + } - private val replacementLabelProperty = SimpleLongProperty(0) + fun Action.verify(property: KMutableProperty0, description: String, stateProvider: () -> T?) { + verify(description) { stateProvider()?.also { state -> property.set(state) } != null } + } + + companion object { + fun Action.verifyState(state: SmoothActionVerifiedState) { + state.run { verifyState() } + } + } +} + +object SmoothAction : MenuAction("_Smooth") { + private fun newConvolutionExecutor(): ThreadPoolExecutor { + val threads = Runtime.getRuntime().availableProcessors() + return ThreadPoolExecutor(threads, threads, 0L, TimeUnit.MILLISECONDS, LinkedBlockingQueue(), ThreadFactoryBuilder().setNameFormat("gaussian-smoothing-%d").build()) + } + + private val LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()) + + private var scopeJob: Job? = null + private var smoothScope: CoroutineScope = CoroutineScope(Dispatchers.Default) + private var smoothTask: UtilityTask>? = null + private var convolutionExecutor = newConvolutionExecutor() + + private val replacementLabelProperty = SimpleLongProperty(0) private val replacementLabel by replacementLabelProperty.nonnullVal() + private val kernelSizeProperty = SimpleDoubleProperty() + private val kernelSize by kernelSizeProperty.nonnullVal() - private val currentMipMapLevelProperty = SimpleIntegerProperty() - private var currentMipMapLevel by currentMipMapLevelProperty.nonnull() + private val progressBarProperty = SimpleDoubleProperty() + private val currentProgressBar by progressBarProperty.nonnull() - private val kernelSize by kernelSizeProperty.nonnullVal() + private val progressProperty = SimpleDoubleProperty() + private var progress by progressProperty.nonnull() + + private val progressStatusProperty = SimpleObjectProperty(ProgressStatus.Empty) + private var progressStatus by progressStatusProperty.nonnull() - private lateinit var labelSource: ConnectomicsLabelState<*, *> - private lateinit var paintLabelMode: PaintLabelMode - private lateinit var paintContext: StatePaintContext<*, *> + private var finalizeSmoothing = false + private var resmooth = false - private var sourceSmoothInterval: Interval? = null - private var globalSmoothInterval: RealInterval? = null + private lateinit var updateSmoothMask: suspend ((Boolean) -> List) + + private val state = SmoothActionVerifiedState() + + private val SmoothActionVerifiedState.defaultKernelSize: Double + get() { + val levelResolution = getLevelResolution(mipMapLevel) + val min = levelResolution.min() + val max = levelResolution.max() + return min + (max - min) / 2.0 + } init { - verify("Label Source is Active") { paintera.currentSource is ConnectomicsLabelState<*, *> } + verifyState(state) verify("Paint Label Mode is Active") { paintera.currentMode is PaintLabelMode } - verify("Paint Label Mode has StatePaintContext") { statePaintContext != null } - verify("Viewer is Focused") { paintera.activeViewer.get()?.state != null } - verify { !paintera.baseView.isDisabledProperty.get() } + verify("Paintera is not disabled") { !paintera.baseView.isDisabledProperty.get() } + verify("Mask is in Use") { !state.paintContext.dataSource.isMaskInUseBinding().get() } onAction { - val updateLevelListener: (transform: AffineTransform3D) -> Unit = { currentMipMapLevel = paintera.activeViewer.get()!!.state!!.bestMipMapLevel } - paintera.baseView.manager().addListener(updateLevelListener) - currentMipMapLevel = paintera.activeViewer.get()!!.state!!.bestMipMapLevel - - labelSource = paintera.currentSource as ConnectomicsLabelState<*, *> - paintLabelMode = paintera.currentMode as PaintLabelMode - paintContext = statePaintContext!! finalizeSmoothing = false - startSmoothTask() - showSmoothDialog() - paintera.baseView.manager().removeListener(updateLevelListener) + /* Set lateinit values */ + with(state) { + kernelSizeProperty.unbind() + kernelSizeProperty.set(defaultKernelSize) + progress = 0.0 + startSmoothTask() + showSmoothDialog() + } } } + private enum class ProgressStatus(val text: String) { + Smoothing("Smoothing... "), + Done(" Done "), + Applying(" Applying... "), + Empty(" ") + } + + private val AffineTransform3D.resolution get() = doubleArrayOf(this[0, 0], this[1, 1], this[2, 2]) - private fun showSmoothDialog() { + private fun SmoothActionVerifiedState.showSmoothDialog() { Dialog().apply { Paintera.registerStylesheets(dialogPane) dialogPane.buttonTypes += ButtonType.APPLY dialogPane.buttonTypes += ButtonType.CANCEL - title = name + title = name?.replace("_", "") - val level = paintera.activeViewer.get()?.state?.bestMipMapLevel!! - val levelResolution: DoubleArray = getLevelResolution(level) + val levelResolution: DoubleArray = getLevelResolution(mipMapLevel) val replacementLabelField = NumberField.longField(Imglib2Label.BACKGROUND, { it >= Imglib2Label.BACKGROUND }, *SubmitOn.values()) - replacementLabelProperty.unbind() replacementLabelProperty.bind(replacementLabelField.valueProperty()) val nextIdButton = Button().apply { styleClass += ADD_GLYPH graphic = FontAwesome[FontAwesomeIcon.PLUS, 2.0] onAction = EventHandler { replacementLabelField.valueProperty().set(labelSource.idService.next()) } + tooltip = Tooltip("Next New ID") } val minRes = levelResolution.min() val minKernelSize = floor(minRes / 2) val maxKernelSize = levelResolution.max() * 10 - val defaultKernelSize = minRes * 4 - val kernelSizeSlider = Slider(log10(minKernelSize), log10(maxKernelSize), log10(defaultKernelSize)) - val kernelSizeField = NumberField.doubleField(defaultKernelSize, { it > 0.0 }, *SubmitOn.values()) + val initialKernelSize = defaultKernelSize + val kernelSizeSlider = Slider(log10(minKernelSize), log10(maxKernelSize), log10(initialKernelSize)) + val kernelSizeField = NumberField.doubleField(initialKernelSize, { it > 0.0 }, *SubmitOn.values()) /* slider sets field */ kernelSizeSlider.valueProperty().addListener { _, _, sliderVal -> @@ -179,41 +228,142 @@ object SmoothAction : MenuAction("Smooth") { } /* field sets slider*/ kernelSizeField.valueProperty().addListener { _, _, fieldVal -> - kernelSizeSlider.valueProperty().set(log10(fieldVal.toDouble())) + /* Let the user go over if they want to explicitly type a larger number in the field. + * otherwise, set the slider */ + if (fieldVal.toDouble() <= maxKernelSize) + kernelSizeSlider.valueProperty().set(log10(fieldVal.toDouble())) } + + val prevStableKernelSize = SimpleDoubleProperty(initialKernelSize) + + val stableKernelSizeBinding = Bindings + .`when`(kernelSizeSlider.valueChangingProperty().not()) + .then(kernelSizeField.valueProperty()) + .otherwise(prevStableKernelSize) + /* size property binds field */ - kernelSizeProperty.unbind() - kernelSizeProperty.bind(kernelSizeField.valueProperty()) + kernelSizeProperty.bind(stableKernelSizeBinding) + val sizeChangeListener = ChangeListener { _, _, size -> prevStableKernelSize.value = size.toDouble() } + kernelSizeProperty.addListener(sizeChangeListener) + + val timeline = Timeline() - dialogPane.scene.cursorProperty().bind(paintera.baseView.node.cursorProperty()) dialogPane.content = VBox(10.0).apply { + isFillWidth = true val replacementIdLabel = Label("Replacement Label") val kernelSizeLabel = Label("Kernel Size (phyiscal units)") replacementIdLabel.alignment = Pos.BOTTOM_RIGHT kernelSizeLabel.alignment = Pos.BOTTOM_RIGHT - children += HBox(10.0, replacementIdLabel, replacementLabelField.textField, nextIdButton) - children += HBox(10.0, kernelSizeLabel, kernelSizeSlider, kernelSizeField.textField) + children += HBox(10.0, replacementIdLabel, nextIdButton, replacementLabelField.textField).also { + it.disableProperty().bind(paintera.baseView.isDisabledProperty) + it.cursorProperty().bind(paintera.baseView.node.cursorProperty()) + } + children += Separator(Orientation.HORIZONTAL) + children += HBox(10.0, kernelSizeLabel, kernelSizeField.textField) + children += HBox(10.0, kernelSizeSlider) + children += HBox(10.0).also { hbox -> + hbox.children += Label().apply { + progressStatusProperty.addListener { _, _, _ -> + InvokeOnJavaFXApplicationThread { + textProperty().set(progressStatus.text) + requestLayout() + } + } + } + hbox.children += ProgressBar().also { progressBar -> + progressBarProperty.unbind() + progressBarProperty.bind(progressBar.progressProperty()) + HBox.setHgrow(progressBar, Priority.ALWAYS) + progressBar.maxWidth = Double.MAX_VALUE + timeline.keyFrames.setAll( + KeyFrame(Duration.ZERO, KeyValue(progressBar.progressProperty(), 0.0)), + KeyFrame(Duration.seconds(1.0), KeyValue(progressBar.progressProperty(), progressProperty.get())) + ) + timeline.play() + progressBar.progressProperty().addListener { _, _, progress -> + val progress = progress.toDouble() + val isApplyMask = paintContext.dataSource.isApplyingMaskProperty() + progressStatus = when { + progress == 0.0 -> ProgressStatus.Empty + progress == 1.0 -> ProgressStatus.Done + isApplyMask.get() -> ProgressStatus.Applying + scopeJob?.isActive == true -> ProgressStatus.Smoothing + else -> ProgressStatus.Empty + } + } + progressProperty.addListener { _, _, progress -> + timeline.stop() + if (progress == 0.0) { + progressBar.progressProperty().set(0.0) + return@addListener + } + + /* Don't move backwards while actively smoothing */ + if (progress.toDouble() <= progressBar.progressProperty().get()) return@addListener + + /* If done, move fast. If not done, move slower if kernel size is larger */ + val duration = if (progress == 1.0) Duration.seconds(.25) else { + val adjustment = (kernelSize - minKernelSize) / (maxKernelSize - minKernelSize) + Duration.seconds(1.0 + adjustment) + } + timeline.keyFrames.setAll( + KeyFrame(Duration.ZERO, KeyValue(progressBar.progressProperty(), progressBar.progressProperty().get())), + KeyFrame(duration, KeyValue(progressBar.progressProperty(), progress.toDouble())) + ) + timeline.play() + } + } + } HBox.setHgrow(kernelSizeLabel, Priority.NEVER) HBox.setHgrow(kernelSizeSlider, Priority.ALWAYS) - HBox.setHgrow(kernelSizeField.textField, Priority.NEVER ) + HBox.setHgrow(kernelSizeField.textField, Priority.ALWAYS) + HBox.setHgrow(replacementLabelField.textField, Priority.ALWAYS) + } + val cleanupOnDialogClose = { + scopeJob?.cancel() + smoothTask?.cancel() + kernelSizeProperty.removeListener(sizeChangeListener) + kernelSizeProperty.unbind() + replacementLabelProperty.unbind() + close() } dialogPane.lookupButton(ButtonType.APPLY).also { applyButton -> applyButton.disableProperty().bind(paintera.baseView.isDisabledProperty) - applyButton.addEventFilter(ActionEvent.ACTION) { _ -> finalizeSmoothing = true } + applyButton.cursorProperty().bind(paintera.baseView.node.cursorProperty()) + applyButton.addEventFilter(ActionEvent.ACTION) { event -> + //So the dialog doesn't close until the smoothing is done + event.consume() + // but listen for when the smoothTask finishes + smoothTask?.stateProperty()?.addListener { _, _, state -> + if (state >= Worker.State.SUCCEEDED) + cleanupOnDialogClose() + } + // indicate the smoothTask should try to apply the current smoothing mask to canvas + progress = 0.0 + finalizeSmoothing = true + } } - dialogPane.lookupButton(ButtonType.CANCEL).addEventFilter(ActionEvent.ACTION) { _ -> - smoothTask?.cancel() - close() + val cancelButton = dialogPane.lookupButton(ButtonType.CANCEL) + cancelButton.disableProperty().bind(paintContext.dataSource.isApplyingMaskProperty()) + cancelButton.addEventFilter(ActionEvent.ACTION) { _ -> cleanupOnDialogClose() } + dialogPane.scene.window.addEventFilter(KeyEvent.KEY_PRESSED) { event -> + if (event.code == KeyCode.ESCAPE && (progressStatus == ProgressStatus.Smoothing || (scopeJob?.isActive == true))) { + /* Cancel if still running */ + event.consume() + cancelActiveSmoothing("Escape Pressed") + } } dialogPane.scene.window.setOnCloseRequest { - smoothTask?.cancel() - close() + if (!paintContext.dataSource.isApplyingMaskProperty().get()) { + cleanupOnDialogClose() + } + } }.show() } - private fun getLevelResolution(level: Int): DoubleArray { + private fun SmoothActionVerifiedState.getLevelResolution(level: Int): DoubleArray { val metadataScales = ((labelSource.backend as? SourceStateBackendN5<*, *>)?.getMetadataState() as? MultiScaleMetadataState)?.scaleTransforms?.get(level) val resFromRai = (labelSource.backend as? RandomAccessibleIntervalBackend<*, *>)?.resolutions return when { @@ -225,121 +375,172 @@ object SmoothAction : MenuAction("Smooth") { } private val smoothingProperty = SimpleBooleanProperty("Smoothing", "Smooth Action is Running", false) + + /** + * Smoothing flag to indicate if a smoothing task is actively running. + * Bound to Paintera.isDisabled, so if set to `true` then Paintera will + * be "busy" until set to `false` again. This is to block unpermitted state + * changes while waiting for smoothing to finish. + */ private var smoothing by smoothingProperty.nonnull() - private fun startSmoothTask() { - var updateSmoothing = false - val updateOnKernelSizeChange: ChangeListener = ChangeListener { _, _, _ -> updateSmoothing = true } + private fun SmoothActionVerifiedState.startSmoothTask() { + val prevScales = paintera.activeViewer.get()!!.screenScales + + val kernelSizeChange: ChangeListener = ChangeListener { _, _, _ -> + if (scopeJob?.isActive == true) + cancelActiveSmoothing("Kernel Size Changed") + resmooth = true + } + smoothTask = Tasks.createTask { task -> paintera.baseView.disabledPropertyBindings[task] = smoothingProperty - kernelSizeProperty.addListener(updateOnKernelSizeChange) + kernelSizeProperty.addListener(kernelSizeChange) + paintera.baseView.orthogonalViews().setScreenScales(doubleArrayOf(prevScales[0])) smoothing = true initializeSmoothLabel() smoothing = false - while (!finalizeSmoothing && !task.isCancelled) { - if (updateSmoothing) { - smoothing = true - updateSmoothMask(true) - smoothing = false - updateSmoothing = false + var intervals: List? = null + while (!task.isCancelled) { + if (resmooth || finalizeSmoothing) { + val preview = !finalizeSmoothing + try { + smoothing = true + intervals = runBlocking { updateSmoothMask(preview).map { it.smallestContainingInterval } } + if (!preview) break + else requestRepaintOverIntervals(intervals) + } catch (c: CancellationException) { + intervals = null + paintContext.dataSource.resetMasks() + paintera.baseView.orthogonalViews().requestRepaint() + } finally { + smoothing = false + /* If any remaine on the queue, shut it down */ + convolutionExecutor.queue.peek()?.let { convolutionExecutor.shutdown() } + /* reset for the next loop */ + resmooth = false + finalizeSmoothing = false + } + } else { + Thread.sleep(100) + } + } + intervals?.also { smoothedIntervals -> + paintContext.dataSource.apply { + val applyProgressProperty = SimpleDoubleProperty() + applyProgressProperty.addListener { _, _, applyProgress -> progress = applyProgress.toDouble() } + applyMaskOverIntervals(currentMask, smoothedIntervals, applyProgressProperty) { it >= 0 } } - Thread.sleep(100) + } ?: let { + task.cancel() + emptyList() } - updateSmoothMask(false) }.onCancelled { _, _ -> + scopeJob?.cancel() paintContext.dataSource.resetMasks() - paintera.baseView.orthogonalViews().requestRepaint(globalSmoothInterval!!) + paintera.baseView.orthogonalViews().requestRepaint() + }.onSuccess { _, task -> + val intervals = task.get() + requestRepaintOverIntervals(intervals) + statePaintContext?.refreshMeshes?.invoke() + }.onEnd { paintera.baseView.disabledPropertyBindings -= smoothTask - }.onSuccess { _, _ -> - var refreshAfterApplyingMask: ChangeListener? = null - refreshAfterApplyingMask = ChangeListener { obs, _, isApplyingMask -> - if (!isApplyingMask) { - paintera.baseView.orthogonalViews().requestRepaint(globalSmoothInterval) - statePaintContext?.refreshMeshes?.invoke() - obs.removeListener(refreshAfterApplyingMask!!) - paintera.baseView.disabledPropertyBindings -= smoothTask - } - } - val maskedSource = paintContext.dataSource - maskedSource.isApplyingMaskProperty.addListener(refreshAfterApplyingMask) - val mask = maskedSource.currentMask - maskedSource.applyMask(mask, sourceSmoothInterval) { it.integerLong >= 0 } + kernelSizeProperty.removeListener(kernelSizeChange) + paintera.baseView.orthogonalViews().setScreenScales(prevScales) + convolutionExecutor.shutdown() }.submit() } - private fun initializeSmoothLabel() { + private fun requestRepaintOverIntervals(intervals: List? = null) { + if (intervals.isNullOrEmpty()) { + paintera.baseView.orthogonalViews().requestRepaint() + return + } + val smoothedInterval = intervals.reduce(Intervals::union) + val globalSmoothedInterval = state.paintContext.dataSource.getSourceTransformForMask(MaskInfo(0, 0)).estimateBounds(smoothedInterval) + paintera.baseView.orthogonalViews().requestRepaint(globalSmoothedInterval) + } + + private fun cancelActiveSmoothing(reason: String) { + convolutionExecutor.shutdown() + scopeJob?.cancel(CancellationException(reason)) + progress = 0.0 + } + + private fun SmoothActionVerifiedState.initializeSmoothLabel() { val maskedSource = paintContext.dataSource as MaskedSource<*, *> - val maskInfo = MaskInfo(0, currentMipMapLevel) val labels = paintContext.selectedIds.activeIds /* Read from the labelBlockLookup (if already persisted) */ - val blocksFromSource = labels.toArray().flatMap { paintContext.getBlocksForLabel(currentMipMapLevel, it).toList() } + val scale0 = 0 - /* Read from canvas access (if in canvas) */ - val cellGrid = maskedSource.getCellGrid(0, currentMipMapLevel) - val cellIntervals = cellGrid.cellIntervals().randomAccess() - val cellPos = LongArray(cellGrid.numDimensions()) - val blocksFromCanvas = let { - labels.toArray().flatMap { - maskedSource.getModifiedBlocks(currentMipMapLevel, it).toArray().map { block -> - cellGrid.getCellGridPositionFlat(block, cellPos) - FinalInterval(cellIntervals.setPositionAndGet(*cellPos)) + val blocksWithLabel= maskedSource.blocksForLabels(scale0, labels.toArray()) + if (blocksWithLabel.isEmpty()) return + + val sourceImg = maskedSource.getReadOnlyDataBackground(0, scale0) + val canvasImg = maskedSource.getReadOnlyDataCanvas(0, scale0) + + val bundleSourceImg = BundleView(sourceImg.convert(UnsignedLongType(Imglib2Label.INVALID)) { input, output -> output.set(input.realDouble.toLong()) }.interval(sourceImg)).interval(sourceImg) + + val cellGrid = maskedSource.getCellGrid(0, scale0) + val labelMask = Lazy.generate(bundleSourceImg, cellGrid.cellDimensions, DoubleType(0.0), AccessFlags.setOf()) { labelMaskChunk -> + val sourceChunk = bundleSourceImg.interval(labelMaskChunk) + val canvasChunk = canvasImg.interval(labelMaskChunk) + + LoopBuilder.setImages(canvasChunk, sourceChunk, labelMaskChunk).multiThreaded().forEachPixel { canvasLabel, sourceBundle, maskVal -> + val hasLabel = canvasLabel.get().let { label -> + if (label != Imglib2Label.INVALID && label in labels) + 1.0 + else null + } ?: sourceBundle.get().get().let { label -> + if (label != Imglib2Label.INVALID && label in labels) + 1.0 + else null } + hasLabel?.let { maskVal.set(it) } } } - val blocksWithLabel = blocksFromSource + blocksFromCanvas - if (blocksWithLabel.isEmpty()) return + var labelRoi: Interval = FinalInterval(blocksWithLabel[0]) + blocksWithLabel.forEach { labelRoi = labelRoi union it } - setNewSourceMask(maskedSource, currentMipMapLevel, maskInfo) + updateSmoothMask = { preview -> smoothMask(labelMask, cellGrid, blocksWithLabel, preview) } + resmooth = true + } - val sourceLabels = Converters.convert( - maskedSource.getReadOnlyDataBackground(0, currentMipMapLevel), - { source, output -> output.set(source.realDouble.toLong()) }, - UnsignedLongType() - ) - val collapsedLabelStack = Views.collapse( - Views.stack(sourceLabels, maskedSource.getReadOnlyDataCanvas(0, currentMipMapLevel)) - ) - val virtualLabelMask = Converters.convert(collapsedLabelStack, { input, output -> - for (i in 1 downTo 0) { - val labelVal = input.get(i.toLong()).get() - if (labelVal != Imglib2Label.INVALID) { - output.set(if (labelVal in labels) 1.0 else 0.0) - break - } - } - }, DoubleType()) + @JvmStatic + fun MaskedSource<*, *>.blocksForLabels(scale0: Int, labels: LongArray): List { + val sourceState = paintera.baseView.sourceInfo().getState(this) as ConnectomicsLabelState + val blocksFromSource = labels.flatMap { sourceState.labelBlockLookup.read(LabelBlockLookupKey(scale0, it)).toList() } - val labelMask = Lazy.generate(virtualLabelMask, cellGrid.cellDimensions, DoubleType(), AccessFlags.setOf(AccessFlags.VOLATILE)) { output -> - val input = virtualLabelMask.interval(output) - LoopBuilder.setImages(input, output).multiThreaded().forEachPixel { inVal, outVal -> if (inVal.get() == 1.0) outVal.set(1.0) } + /* Read from canvas access (if in canvas) */ + val cellGrid = getCellGrid(0, scale0) + val cellIntervals = cellGrid.cellIntervals().randomAccess() + val cellPos = LongArray(cellGrid.numDimensions()) + val blocksFromCanvas = labels.flatMap { + getModifiedBlocks(scale0, it).toArray().map { block -> + cellGrid.getCellGridPositionFlat(block, cellPos) + FinalInterval(cellIntervals.setPositionAndGet(*cellPos)) + } } - var labelRoi: Interval = FinalInterval(blocksWithLabel[0]) - blocksWithLabel.forEach { labelRoi = labelRoi union it } - - updateSmoothMask = { preview -> - val blocksToSmoothOver = if (preview) pruneBlock(blocksWithLabel) else blocksWithLabel - smoothMask(labelMask, cellGrid, blocksToSmoothOver, preview) - } - updateSmoothMask(true) + return blocksFromSource + blocksFromCanvas } - private fun pruneBlock(blocksWithLabel: List): List { + private fun SmoothActionVerifiedState.pruneBlock(blocksWithLabel: List): List { val viewsInSourceSpace = viewerIntervalsInSourceSpace() /* remove any blocks that don't intersect with them*/ return blocksWithLabel - .filter { block -> viewsInSourceSpace.firstOrNull { viewer -> !Intervals.isEmpty(viewer.intersect(block)) } != null } + .mapNotNull { block -> viewsInSourceSpace.firstOrNull { viewer -> !Intervals.isEmpty(viewer.intersect(block)) } } .toList() } - private fun viewerIntervalsInSourceSpace(): Array { + private fun SmoothActionVerifiedState.viewerIntervalsInSourceSpace(): Array { /* get viewer screen intervals for each orthognal view in source space*/ val viewerAndTransforms = paintera.baseView.orthogonalViews().viewerAndTransforms() val viewsInSourceSpace = viewerAndTransforms @@ -350,88 +551,116 @@ object SmoothAction : MenuAction("Smooth") { val height = it.viewer().height val screenInterval = FinalInterval(width.toLong(), height.toLong(), 1L) val sourceToGlobal = AffineTransform3D() - labelSource.getDataSource().getSourceTransform(0, currentMipMapLevel, sourceToGlobal) + labelSource.getDataSource().getSourceTransform(0, 0, sourceToGlobal) val viewerToSource = sourceToGlobal.inverse().copy().concatenate(globalToViewerTransform.inverse()) viewerToSource.estimateBounds(screenInterval) }.toTypedArray() return viewsInSourceSpace } - private fun smoothMask(labelMask: CachedCellImg, cellGrid: CellGrid, blocksWithLabel: List, preview : Boolean = false) { - val levelResolution = getLevelResolution(currentMipMapLevel) + private suspend fun SmoothActionVerifiedState.smoothMask(labelMask: CachedCellImg, cellGrid: CellGrid, blocksWithLabel: List, preview: Boolean = false): List { + + /* Just to show that smoothing has started */ + progress = 0.0 // The listener only always reseting to zero if going backward, so do this first + progress = .05 + + val intervalsToSmoothOver = if (preview) pruneBlock(blocksWithLabel) else blocksWithLabel + + val scale0 = 0 + val levelResolution = getLevelResolution(scale0) val sigma = DoubleArray(3) { kernelSize / levelResolution[it] } - val halfkernels = Gauss3.halfkernels(sigma) - val symmetric = Kernel1D.symmetric(halfkernels) - val smoothedImg = Lazy.generate(labelMask, cellGrid.cellDimensions, DoubleType(), AccessFlags.setOf(AccessFlags.VOLATILE)) { - SeparableKernelConvolution.convolution(*symmetric).process(labelMask.extendZero(), it) + if (convolutionExecutor.isShutdown) { + convolutionExecutor = newConvolutionExecutor() } + val convolution = FastGauss.convolution(sigma) + if (convolutionExecutor.isShutdown) { + convolutionExecutor = newConvolutionExecutor() + } + + + val smoothedImg = Lazy.generate(labelMask, cellGrid.cellDimensions, DoubleType(), AccessFlags.setOf()) {} + + paintContext.dataSource.resetMasks() + setNewSourceMask(paintContext.dataSource, MaskInfo(0, scale0)) val mask = paintContext.dataSource.currentMask - val viewerIntervalsInSource = viewerIntervalsInSourceSpace() - - runBlocking { - coroutineScope { - val smoothJobs = mutableListOf() - for (block in blocksWithLabel) { - if (smoothTask!!.isCancelled) return@coroutineScope - - if (preview) { - for (interval in viewerIntervalsInSource) { - val slice = interval.intersect(block) - if (Intervals.isEmpty(slice)) continue - smoothJobs += launch { - LoopBuilder.setImages( - labelMask.interval(slice), - smoothedImg.interval(slice), - mask.rai.interval(slice) - ).multiThreaded().forEachPixel { original, smooth, mask -> - if (smoothTask!!.isCancelled) return@forEachPixel - - if (smooth.get() < 0.5 && original.get() == 1.0) { - mask.setInteger(replacementLabel) - } else if (mask.get() == replacementLabel) { - mask.setInteger(Imglib2Label.INVALID) - } - } - } - } - } else { - smoothJobs += launch { - LoopBuilder.setImages( - labelMask.interval(block), - smoothedImg.interval(block), - mask.rai.interval(block) - ).multiThreaded().forEachPixel { original, smooth, mask -> - if (smoothTask!!.isCancelled) return@forEachPixel - - if (smooth.get() < 0.5 && original.get() == 1.0) { - mask.setInteger(replacementLabel) - } else if (mask.get() == replacementLabel) { - mask.setInteger(Imglib2Label.INVALID) - } - } - } + + val smoothOverInterval: suspend CoroutineScope.(RealInterval) -> Job = { slice -> + launch { + val smoothedSlice = smoothedImg.interval(slice) + try { + Parallelization.runWithExecutor(convolutionExecutor) { convolution.process(labelMask.extendValue(DoubleType(0.0)), smoothedSlice) } + } catch (e : RejectedExecutionException) { + if (isActive) { + throw e + } + } + + val labels = BundleView(labelMask).interval(slice).cursor() + val smoothed = smoothedSlice.cursor() + val maskBundle = BundleView(mask.rai.extendValue(Imglib2Label.INVALID)).interval(slice).cursor() + + + ensureActive() + while (smoothed.hasNext()) { + //This is the slow one, check cancellation immediately before and after + val smoothness: Double + try { + smoothness = smoothed.next().get() + } catch (e: Exception) { + throw CancellationException("Gaussian Convolution Shutdown", e).also { cancel(it) } + } + val labelVal = labels.next().get() + val maskVal = maskBundle.next().get() + if (smoothness < 0.5 && labelVal.get() == 1.0) { + maskVal.setInteger(replacementLabel) + } else if (maskVal.get() == replacementLabel) { + maskVal.setInteger(Imglib2Label.INVALID) } + } + } + } + /*Start smoothing */ + if (!smoothScope.isActive) + smoothScope = CoroutineScope(Dispatchers.Default) + scopeJob = smoothScope.launch { + intervalsToSmoothOver.forEach { smoothOverInterval(it) } + } + + /* wait and update progress */ + val progressUpdateJob = CoroutineScope(Dispatchers.Default).launch { + while (scopeJob?.isActive == true) { + with(convolutionExecutor) { + progress = currentProgressBar.let { currentProgress -> + val remaining = 1.0 - currentProgress + val numTasksModifier = 1 / log10(taskCount + 10.0) + /* Max quarter remaining increments*/ + currentProgress + (remaining * numTasksModifier).coerceAtMost(.25) + } } - smoothJobs.forEach { it.join() } + delay(200) } } - val maskedSource = paintContext.dataSource - val maskInfo = mask.info - var labelRoi: Interval = FinalInterval(blocksWithLabel[0]) - blocksWithLabel.forEach { labelRoi = labelRoi union it } - sourceSmoothInterval = labelRoi - globalSmoothInterval = maskedSource.getSourceTransformForMask(maskInfo).estimateBounds(sourceSmoothInterval) - paintera.baseView.orthogonalViews().requestRepaint(globalSmoothInterval) + while (scopeJob?.isActive == true) { + delay(50) + if (scopeJob?.isCancelled == true) { + progress = 0.0 + progressUpdateJob.cancelAndJoin() + throw CancellationException("Smoothing Cancelled") + } + } + progressUpdateJob.cancelAndJoin() + progress = 1.0 + return intervalsToSmoothOver } - private fun setNewSourceMask(maskedSource: MaskedSource<*, *>, level: Int, maskInfo: MaskInfo) { - val (store, volatileStore) = maskedSource.createMaskStoreWithVolatile(level) + private fun setNewSourceMask(maskedSource: MaskedSource<*, *>, maskInfo: MaskInfo) { + val (store, volatileStore) = maskedSource.createMaskStoreWithVolatile(maskInfo.level) val mask = SourceMask(maskInfo, store, volatileStore.rai, store.cache, volatileStore.invalidate) { store.shutdown() } - maskedSource.setMask(mask) { it.integerLong >= 0 } + maskedSource.setMask(mask) { it >= 0 } } } diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/ShapeInterpolationMode.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/ShapeInterpolationMode.kt index 4c90ee3cc..a45f91512 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/ShapeInterpolationMode.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/ShapeInterpolationMode.kt @@ -18,6 +18,7 @@ import javafx.scene.input.MouseEvent.* import net.imglib2.Interval import net.imglib2.realtransform.AffineTransform3D import net.imglib2.type.numeric.IntegerType +import net.imglib2.util.Intervals import org.janelia.saalfeldlab.control.mcu.MCUButtonControl import org.janelia.saalfeldlab.fx.UtilityTask import org.janelia.saalfeldlab.fx.actions.* @@ -55,8 +56,8 @@ import org.janelia.saalfeldlab.paintera.control.tools.paint.Fill2DTool import org.janelia.saalfeldlab.paintera.control.tools.paint.PaintBrushTool import org.janelia.saalfeldlab.paintera.control.tools.paint.SamTool import org.janelia.saalfeldlab.paintera.data.mask.MaskedSource -import org.janelia.saalfeldlab.paintera.data.mask.SourceMask import org.janelia.saalfeldlab.paintera.paintera +import org.janelia.saalfeldlab.util.extendValue import org.janelia.saalfeldlab.util.get import org.slf4j.LoggerFactory import java.lang.invoke.MethodHandles @@ -141,10 +142,10 @@ class ShapeInterpolationMode>(val controller: ShapeInterpolat } override fun activate() { - fillLabel = { controller.interpolationId } + super.activate() /* Don't allow filling with depth during shape interpolation */ brushProperties?.brushDepth = 1.0 - super.activate() + fillLabel = { controller.interpolationId } fill2D.maskIntervalProperty.addListener(controllerPaintOnFill) } @@ -291,7 +292,6 @@ class ShapeInterpolationMode>(val controller: ShapeInterpolat filter = true verify { activeTool is Fill2DTool } onAction { - fill2DTool.fill2D.release() switchTool(shapeInterpolationTool) } } @@ -323,9 +323,6 @@ class ShapeInterpolationMode>(val controller: ShapeInterpolat filter = true onAction { InvokeOnJavaFXApplicationThread { - if (activeTool is Fill2DTool) { - fill2DTool.fill2D.release() - } if (activeTool != shapeInterpolationTool) switchTool(shapeInterpolationTool) /* If triggered, ensure toggle is on. Only can be off when switching to another tool */ @@ -356,6 +353,7 @@ class ShapeInterpolationMode>(val controller: ShapeInterpolat switchTool(shapeInterpolationTool) } else { switchTool(fill2DTool) + fill2DTool.enteredWithoutKeyTrigger = true } } } @@ -369,6 +367,7 @@ class ShapeInterpolationMode>(val controller: ShapeInterpolat switchTool(shapeInterpolationTool) } else { switchTool(samTool) + samTool.enteredWithoutKeyTrigger = true } } } @@ -445,7 +444,6 @@ class ShapeInterpolationMode>(val controller: ShapeInterpolat /* On click, generate a new mask, */ (activeSourceStateProperty.get()?.dataSource as? MaskedSource<*, *>)?.let { source -> paintClickOrDrag!!.let { paintController -> - val previousMask: SourceMask? = source.currentMask source.resetMasks(false) paintController.provideMask(controller.getMask()) } @@ -503,11 +501,22 @@ class ShapeInterpolationMode>(val controller: ShapeInterpolat consume = false verify { activeTool == floodFillTool } onAction { - /* On click, provide the mask, */ + /* On click, provide the mask, setup the task listener */ (activeSourceStateProperty.get()?.dataSource as? MaskedSource<*, *>)?.let { source -> - fill2DTool.fill2D.let { fillController -> - source.resetMasks(false) - fillController.provideMask(controller.getMask()) + source.resetMasks(false) + val mask = controller.getMask() + mask.pushNewImageLayer() + fill2DTool.run { + fillTaskProperty.addWithListener { obs, _, task -> + task?.let { + task.onCancelled(true) { _, _ -> + mask.popImageLayer() + mask.requestRepaint() + } + task.onEnd(true) { obs?.removeListener(this) } + } ?: obs?.removeListener(this) + } + fill2D.provideMask(mask) } } } @@ -726,11 +735,28 @@ class ShapeInterpolationTool( verifyNoKeysDown() verifyEventNotNull() verify { !paintera.mouseTracker.isDragging } + verify { mode?.activeTool !is Fill2DTool } verify { it!!.button == MouseButton.PRIMARY } // respond to primary click verify { controllerState != Interpolate } // need to be in the select state + verify("Can't select BACKGROUND or higher MAX_ID ") { event -> + + source.resetMasks(false) + val mask = getMask() + + fill2D.fill2D.provideMask(mask) + val pointInMask = mask.displayPointToInitialMaskPoint(event!!.x, event.y) + val pointInSource = pointInMask.positionAsRealPoint().also { mask.initialMaskToSourceTransform.apply(it, it) } + val info = mask.info + val sourceLabel = source.getInterpolatedDataSource(info.time, info.level, null).getAt(pointInSource).integerLong + return@verify sourceLabel != Label.BACKGROUND && sourceLabel.toULong() <= Label.MAX_ID.toULong() + + } onAction { event -> /* get value at position */ - deleteCurrentSliceOrInterpolant() + deleteCurrentSliceOrInterpolant()?.let { prevSliceGlobalInterval -> + source.resetMasks(true) + paintera.baseView.orthogonalViews().requestRepaint(Intervals.smallestContainingInterval(prevSliceGlobalInterval)) + } currentTask = fillObjectInSlice(event!!) } } @@ -769,11 +795,25 @@ class ShapeInterpolationTool( } } - private fun fillObjectInSlice(event: MouseEvent): UtilityTask? { + private fun fillObjectInSlice(event: MouseEvent): UtilityTask<*>? { with(controller) { source.resetMasks(false) val mask = getMask() + /* If a current slice exists, try to preserve it if cancelled */ + currentSliceMaskInterval?.also { + mask.pushNewImageLayer() + fill2D.fillTaskProperty.addWithListener { obs, _, task -> + task?.let { + task.onCancelled(true) { _, _ -> + mask.popImageLayer() + mask.requestRepaint() + } + task.onEnd(true) { obs?.removeListener(this) } + } ?: obs?.removeListener(this) + } + } + fill2D.fill2D.provideMask(mask) val pointInMask = mask.displayPointToInitialMaskPoint(event.x, event.y) val pointInSource = pointInMask.positionAsRealPoint().also { mask.initialMaskToSourceTransform.apply(it, it) } @@ -783,12 +823,13 @@ class ShapeInterpolationTool( return null } - val maskLabel = mask.rai[pointInMask].get() + val maskLabel = mask.rai.extendValue(Label.INVALID)[pointInMask].get() fill2D.brushProperties?.brushDepth = 1.0 fill2D.fillLabel = { if (maskLabel == interpolationId) Label.TRANSPARENT else interpolationId } return fill2D.executeFill2DAction(event.x, event.y) { paint(it) currentTask = null + fill2D.fill2D.release() } } } diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/paint/PaintClickOrDragController.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/paint/PaintClickOrDragController.kt index 465f443cb..e2a4c5e97 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/paint/PaintClickOrDragController.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/paint/PaintClickOrDragController.kt @@ -28,10 +28,13 @@ import org.janelia.saalfeldlab.paintera.ui.PainteraAlerts import org.janelia.saalfeldlab.paintera.util.IntervalHelpers.Companion.asRealInterval import org.janelia.saalfeldlab.paintera.util.IntervalHelpers.Companion.extendAndTransformBoundingBox import org.janelia.saalfeldlab.paintera.util.IntervalHelpers.Companion.smallestContainingInterval +import org.janelia.saalfeldlab.util.NamedThreadFactory import org.janelia.saalfeldlab.util.extendValue import org.janelia.saalfeldlab.util.union import org.slf4j.LoggerFactory import java.lang.invoke.MethodHandles +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors private data class Postion(var x: Double = 0.0, var y: Double = 0.0) { @@ -149,7 +152,8 @@ class PaintClickOrDragController( private val position = Postion() var viewerMask: ViewerMask? = null - private var submitMask = true + var submitMask: Boolean = true + private set internal fun provideMask(viewerMask: ViewerMask) { @@ -305,6 +309,12 @@ class PaintClickOrDragController( @Synchronized private fun paint(pos: Postion) = pos.run { paint(x, y) } + private var paintService : ExecutorService? = null + get() = if (field == null || field!!.isShutdown) { + field = Executors.newSingleThreadExecutor(NamedThreadFactory("PaintThread",true)) + field + } else field + @Synchronized private fun paint(viewerX: Double, viewerY: Double) { LOG.trace("At {} {}", viewerX, viewerY) @@ -328,7 +338,7 @@ class PaintClickOrDragController( val paintIntervalInMask = task.get() maskInterval = paintIntervalInMask union maskInterval requestRepaint(paintIntervalInMask) - }.submit() + }.submit(paintService!!) } diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/paint/ViewerMask.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/paint/ViewerMask.kt index 84a6b3797..e26a66a48 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/paint/ViewerMask.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/paint/ViewerMask.kt @@ -5,6 +5,7 @@ import bdv.util.Affine3DHelpers import javafx.beans.property.SimpleBooleanProperty import net.imglib2.* import net.imglib2.cache.Invalidate +import net.imglib2.converter.Converters import net.imglib2.loops.LoopBuilder import net.imglib2.realtransform.AffineTransform3D import net.imglib2.realtransform.RealViews @@ -41,7 +42,7 @@ class ViewerMask private constructor( invalidate: Invalidate<*>? = null, invalidateVolatile: Invalidate<*>? = null, shutdown: Runnable? = null, - private inline val paintedLabelIsValid: (UnsignedLongType) -> Boolean = { MaskedSource.VALID_LABEL_CHECK.test(it) }, + private inline val paintedLabelIsValid: (Long) -> Boolean = { MaskedSource.VALID_LABEL_CHECK.test(it) }, val paintDepthFactor: Double? = 1.0, private val maskSize: Interval? = null, private val defaultValue: Long = Label.INVALID @@ -167,7 +168,13 @@ class ViewerMask private constructor( val (cachedCellImg, volatileRaiWithInvalidate) = source.createMaskStoreWithVolatile(CELL_DIMS, imgDims, defaultValue)!! - this.shutdown = Runnable { cachedCellImg.shutdown() } + this.shutdown = let { + val oldShutdown = shutdown + Runnable { + oldShutdown?.run() + cachedCellImg.shutdown() + } + } return WrappedRandomAccessibleInterval(cachedCellImg) to WrappedRandomAccessibleInterval(volatileRaiWithInvalidate.rai) } @@ -202,7 +209,7 @@ class ViewerMask private constructor( viewerCellImg: RandomAccessibleInterval, volatileViewerCellImg: RandomAccessibleInterval ): - Pair, RealRandomAccessible> { + Pair, RealRandomAccessible> { val realViewerImg = viewerCellImg.extendValue(Label.INVALID).interpolateNearestNeighbor() val realVolatileViewerImg = volatileViewerCellImg.extendValue(VolatileUnsignedLongType(Label.INVALID)).interpolateNearestNeighbor() @@ -221,20 +228,83 @@ class ViewerMask private constructor( writableSourceImages: Pair?, RandomAccessibleInterval?>? = newSourceImages ) { (newSourceImages ?: newBackingImages()).let { (img, volatileImg) -> - viewerImg.wrappedSource = (img as? WrappedRandomAccessibleInterval)?.wrappedSource ?: img - volatileViewerImg.wrappedSource = (volatileImg as? WrappedRandomAccessibleInterval)?.wrappedSource ?: volatileImg + viewerImg.wrappedSource = img + volatileViewerImg.wrappedSource = volatileImg - viewerImg.writableSource = (writableSourceImages?.first as? WrappedRandomAccessibleInterval)?.wrappedSource - ?: writableSourceImages?.first - volatileViewerImg.writableSource = (writableSourceImages?.second as? WrappedRandomAccessibleInterval)?.wrappedSource - ?: writableSourceImages?.second + viewerImg.writableSource = writableSourceImages?.first + volatileViewerImg.writableSource = writableSourceImages?.second } } + internal fun pushNewImageLayer(writableSourceImages: Pair, RandomAccessibleInterval>? = newBackingImages()) { + writableSourceImages?.let { (newImg, newVolatileImg) -> + val compositeMask = Converters.convert( + viewerImg.wrappedSource.extendValue(Label.INVALID), + newImg.extendValue(Label.INVALID), + { oldVal, newVal, result -> + val new = newVal.get() + if (new != Label.INVALID) { + result.set(new) + } else result.set(oldVal) + }, + UnsignedLongType(Label.INVALID) + ).interval(newImg) + + val compositeVolatileMask = Converters.convert( + volatileViewerImg.wrappedSource.extendValue(VolatileUnsignedLongType(Label.INVALID)), + newVolatileImg.extendValue(VolatileUnsignedLongType(Label.INVALID)), + { original, overlay, composite -> + + var checkOriginal = false + if (overlay.isValid) { + val overlayVal = overlay.get().get() + if (overlayVal != Label.INVALID) { + composite.get().set(overlayVal) + composite.isValid = true + } else checkOriginal = true + } else checkOriginal = true + if (checkOriginal) { + if (original.isValid) { + composite.set(original) + composite.isValid = true + } else composite.isValid = false + } + composite.isValid = true + }, + VolatileUnsignedLongType(Label.INVALID) + ).interval(newVolatileImg) + + val wrappedCompositeMask = WrappedRandomAccessibleInterval(compositeMask) + wrappedCompositeMask.writableSource = WrappedRandomAccessibleInterval(viewerImg.wrappedSource).apply { writableSource = viewerImg.writableSource } + + val wrappedVolatileCompositeMask = WrappedRandomAccessibleInterval(compositeVolatileMask) + wrappedVolatileCompositeMask.writableSource = WrappedRandomAccessibleInterval(volatileViewerImg.wrappedSource).apply { writableSource = volatileViewerImg.writableSource } + + updateBackingImages( + wrappedCompositeMask to wrappedVolatileCompositeMask, + newImg to newVolatileImg + ) + } + } + + internal fun popImageLayer() : Boolean { + var popped = false + ((viewerImg.wrappedSource as? WrappedRandomAccessibleInterval)?.writableSource as? WrappedRandomAccessibleInterval)?.let { prevImg -> + viewerImg.wrappedSource = prevImg.wrappedSource + viewerImg.writableSource = prevImg.writableSource + popped = true + } + ((volatileViewerImg.wrappedSource as? WrappedRandomAccessibleInterval)?.writableSource as? WrappedRandomAccessibleInterval)?.let { prevImg -> + volatileViewerImg.wrappedSource = prevImg.wrappedSource + volatileViewerImg.writableSource = prevImg.writableSource + } + return popped + } + override fun > applyMaskToCanvas( canvas: RandomAccessibleInterval, - acceptAsPainted: Predicate + acceptAsPainted: Predicate ): Set { val extendedViewerImg = Views.extendValue(viewerImg, Label.INVALID) @@ -294,7 +364,7 @@ class ViewerMask private constructor( val canvasPositionInMask = DoubleArray(3) val canvasPositionInMaskAsRealPoint = RealPoint.wrap(canvasPositionInMask) - chunk.forEachPixel { canvasBundle, viewerVal -> + chunk.forEachPixel { canvasBundle, viewerValType -> canvasBundle.localize(canvasPosition) val sourceDepthInMask = sourceToMaskTransformAsArray.let { it[8] * canvasPosition[0] + it[9] * canvasPosition[1] + it[10] * canvasPosition[2] + it[11] @@ -302,8 +372,9 @@ class ViewerMask private constructor( sourceToMaskTransform.apply(canvasPosition, canvasPositionInMask) if (sourceDepthInMask <= maxSourceDistInMask) { + val viewerVal = viewerValType.get() if (acceptAsPainted.test(viewerVal)) { - val paintVal = viewerVal.get() + val paintVal = viewerVal if (sourceDepthInMask < minSourceDistInMask) { canvasBundle.get().setInteger(paintVal) } else { @@ -359,9 +430,8 @@ class ViewerMask private constructor( val maskCursor = extendedViewerImg.interval(maskInterval).cursor() while (maskCursor.hasNext()) { - val maskLabel = maskCursor.next() - val maskId = maskLabel.integerLong - if (acceptAsPainted.test(maskLabel)) { + val maskId = maskCursor.next().get() + if (acceptAsPainted.test(maskId)) { maskCursor.localize(canvasPositionInMask) sourceToMaskTransform.inverse().apply(canvasPositionInMask, canvasPositionInMask) /* just renaming for clarity. */ @@ -384,6 +454,7 @@ class ViewerMask private constructor( return paintedLabelSet } + @JvmOverloads fun requestRepaint(intervalOverMask: Interval? = null) { intervalOverMask?.let { val globalInterval = IntervalHelpers.extendAndTransformBoundingBox(intervalOverMask, initialGlobalToMaskTransform.inverse(), .5) @@ -391,6 +462,7 @@ class ViewerMask private constructor( } ?: paintera.baseView.orthogonalViews().requestRepaint() } + @JvmOverloads fun getScreenInterval(width: Long = viewer.width.toLong(), height: Long = viewer.height.toLong()): Interval { val (x: Long, y: Long) = displayPointToInitialMaskPoint(0, 0) return Intervals.createMinSize(x, y, 0, width, height, 1) @@ -416,6 +488,13 @@ class ViewerMask private constructor( doubleArrayOf(+.5, -.5, -.5), ) + @JvmStatic + fun ViewerPanelFX.getGlobalViewerInterval(): RealInterval { + val zeroGlobal = doubleArrayOf(0.0, 0.0, 0.0).also { displayToGlobalCoordinates(it) } + val sizeGlobal = doubleArrayOf(width, height, 0.0).also { displayToGlobalCoordinates(it) } + return FinalRealInterval(zeroGlobal, sizeGlobal) + } + @JvmStatic @JvmOverloads fun MaskedSource<*, *>.createViewerMask(maskInfo: MaskInfo, viewer: ViewerPanelFX, paintDepth: Double? = 1.0, maskSize: Interval? = null, defaultValue: Long = Label.INVALID, setMask: Boolean = true): ViewerMask { diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/paint/Fill2DTool.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/paint/Fill2DTool.kt index f9053f74d..fe95b6d7d 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/paint/Fill2DTool.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/paint/Fill2DTool.kt @@ -5,18 +5,15 @@ import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView import javafx.beans.property.SimpleBooleanProperty import javafx.beans.property.SimpleDoubleProperty import javafx.beans.property.SimpleObjectProperty -import javafx.beans.value.ChangeListener import javafx.beans.value.ObservableValue import javafx.scene.Cursor import javafx.scene.input.* import net.imglib2.Interval +import net.imglib2.util.Intervals import org.janelia.saalfeldlab.fx.UtilityTask import org.janelia.saalfeldlab.fx.actions.ActionSet import org.janelia.saalfeldlab.fx.actions.painteraActionSet -import org.janelia.saalfeldlab.fx.extensions.LazyForeignValue -import org.janelia.saalfeldlab.fx.extensions.createNullableValueBinding -import org.janelia.saalfeldlab.fx.extensions.nonnull -import org.janelia.saalfeldlab.fx.extensions.nonnullVal +import org.janelia.saalfeldlab.fx.extensions.* import org.janelia.saalfeldlab.fx.ui.StyleableImageView import org.janelia.saalfeldlab.fx.util.InvokeOnJavaFXApplicationThread import org.janelia.saalfeldlab.labels.Label @@ -25,10 +22,12 @@ import org.janelia.saalfeldlab.paintera.control.ControlUtils import org.janelia.saalfeldlab.paintera.control.actions.PaintActionType import org.janelia.saalfeldlab.paintera.control.modes.ToolMode import org.janelia.saalfeldlab.paintera.control.paint.FloodFill2D +import org.janelia.saalfeldlab.paintera.control.paint.ViewerMask import org.janelia.saalfeldlab.paintera.meshes.MeshSettings import org.janelia.saalfeldlab.paintera.paintera import org.janelia.saalfeldlab.paintera.state.SourceState import org.janelia.saalfeldlab.paintera.ui.overlays.CursorOverlayWithText +import kotlin.collections.set open class Fill2DTool(activeSourceStateProperty: SimpleObjectProperty?>, mode: ToolMode? = null) : PaintTool(activeSourceStateProperty, mode) { @@ -50,7 +49,8 @@ open class Fill2DTool(activeSourceStateProperty: SimpleObjectProperty? = null + val fillTaskProperty: SimpleObjectProperty?> = SimpleObjectProperty(null) + private var fillTask by fillTaskProperty.nullable() private val overlay by lazy { Fill2DOverlay(activeViewerProperty.createNullableValueBinding { it?.viewer() }).apply { @@ -69,12 +69,16 @@ open class Fill2DTool(activeSourceStateProperty: SimpleObjectProperty + if (!isRunning) { + overlay.visible = false + fill2D.release() + obs?.removeListener(this) + super.deactivate() + } + } } override val actionSets: MutableList by LazyForeignValue({ activeViewerAndTransforms }) { @@ -94,7 +98,9 @@ open class Fill2DTool(activeSourceStateProperty: SimpleObjectProperty Unit = {}): UtilityTask { - lateinit var setFalseAndRemoveListener: ChangeListener - setFalseAndRemoveListener = ChangeListener { obs, _, isBusy -> - if (isBusy) { - overlay.cursor = Cursor.WAIT - } else { - overlay.cursor = Cursor.CROSSHAIR - if (!paintera.keyTracker.areKeysDown(*keyTrigger.toTypedArray()) && !enteredWithoutKeyTrigger) { - InvokeOnJavaFXApplicationThread { mode?.switchTool(mode.defaultTool) } - } - obs.removeListener(setFalseAndRemoveListener) - } - } + internal fun executeFill2DAction(x: Double, y: Double, afterFill: (Interval) -> Unit = {}): UtilityTask<*>? { fillIsRunningProperty.set(true) - return fill2D.fillViewerAt(x, y, fillLabel(), statePaintContext!!.assignment).also { task -> - fillTask = task - - paintera.baseView.isDisabledProperty.addListener(setFalseAndRemoveListener) - paintera.baseView.disabledPropertyBindings[this] = fillIsRunningProperty + val applyIfMaskNotProvided = fill2D.mask == null + if (applyIfMaskNotProvided) { + statePaintContext!!.dataSource.resetMasks(true); + } + fillTask = fill2D.fillViewerAt(x, y, fillLabel(), statePaintContext!!.assignment) + if (fillTask == null) { + fillIsRunningProperty.set(false) + } + return fillTask?.also { task -> if (task.isDone) { /* If it's already done, do this now*/ - if (!task.isCancelled) afterFill(task.get()) - fillIsRunningProperty.set(false) - paintera.baseView.disabledPropertyBindings -= this + if (!task.isCancelled) { + val maskFillInterval = fill2D.maskIntervalProperty.value + afterFill(maskFillInterval) + if (applyIfMaskNotProvided) { + /* Then apply when done */ + val source = statePaintContext!!.dataSource + val mask = source.currentMask as ViewerMask + val affectedSourceInterval = Intervals.smallestContainingInterval( + mask.currentMaskToSourceWithDepthTransform.estimateBounds(maskFillInterval)) + source.applyMask(mask, affectedSourceInterval, net.imglib2.type.label.Label::isForeground) + } + + } } else { + paintera.baseView.disabledPropertyBindings[this] = fillIsRunningProperty + paintera.baseView.isDisabledProperty.addTriggeredWithListener { obs, _, isBusy -> + if (isBusy) { + overlay.cursor = Cursor.WAIT + } else { + overlay.cursor = Cursor.CROSSHAIR + if (!paintera.keyTracker.areKeysDown(*keyTrigger.toTypedArray()) && !enteredWithoutKeyTrigger) { + InvokeOnJavaFXApplicationThread { mode?.switchTool(mode.defaultTool) } + } + obs?.removeListener(this) + } + } + /* Otherwise, do it when it's done */ - task.onEnd { + + task.onEnd(append = true) { fillIsRunningProperty.set(false) paintera.baseView.disabledPropertyBindings -= this + fillTask = null } - task.onSuccess { _, _ -> afterFill(task.get()) } + + task.onSuccess(append = true) { _, _ -> + val maskFillInterval = fill2D.maskIntervalProperty.value + afterFill(maskFillInterval) + if (applyIfMaskNotProvided) { + /* Then apply when done */ + val source = statePaintContext!!.dataSource + val mask = source.currentMask as ViewerMask + val affectedSourceInterval = Intervals.smallestContainingInterval( + mask.currentMaskToSourceWithDepthTransform.estimateBounds(maskFillInterval)) + source.applyMask(mask, affectedSourceInterval, net.imglib2.type.label.Label::isForeground) + } + } + } } } diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/paint/Fill3DTool.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/paint/Fill3DTool.kt index 722e3b303..ac7333263 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/paint/Fill3DTool.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/paint/Fill3DTool.kt @@ -12,6 +12,8 @@ import javafx.scene.input.KeyEvent.KEY_PRESSED import javafx.scene.input.MouseButton import javafx.scene.input.MouseEvent import javafx.scene.input.ScrollEvent +import net.imglib2.realtransform.AffineTransform3D +import org.janelia.saalfeldlab.fx.UtilityTask import org.janelia.saalfeldlab.fx.actions.ActionSet import org.janelia.saalfeldlab.fx.actions.painteraActionSet import org.janelia.saalfeldlab.fx.extensions.LazyForeignValue @@ -27,7 +29,6 @@ import org.janelia.saalfeldlab.paintera.control.modes.ToolMode import org.janelia.saalfeldlab.paintera.control.paint.FloodFill import org.janelia.saalfeldlab.paintera.meshes.MeshSettings import org.janelia.saalfeldlab.paintera.paintera -import org.janelia.saalfeldlab.paintera.state.FloodFillState import org.janelia.saalfeldlab.paintera.state.SourceState import org.janelia.saalfeldlab.paintera.ui.overlays.CursorOverlayWithText @@ -39,8 +40,8 @@ class Fill3DTool(activeSourceStateProperty: SimpleObjectProperty() - private var floodFillState: FloodFillState? by floodFillStateProperty.nullable() + private val floodFillTaskProperty = SimpleObjectProperty?>() + private var floodFillTask: UtilityTask<*>? by floodFillTaskProperty.nullable() val fill by LazyForeignValue({ statePaintContext }) { with(it!!) { @@ -48,9 +49,13 @@ class Fill3DTool(activeSourceStateProperty: SimpleObjectProperty vat?.viewer() }, dataSource, assignment, - { paintera.baseView.orthogonalViews().requestRepaint() }, - { MeshSettings.Defaults.Values.isVisible }, - { floodFillState = it } + { interval -> + val sourceToGlobal = AffineTransform3D().also { transform -> + dataSource.getSourceTransform(dataSource.currentMask.info, transform) + } + paintera.baseView.orthogonalViews().requestRepaint(sourceToGlobal.estimateBounds(interval)) + }, + { MeshSettings.Defaults.Values.isVisible } ) } } @@ -103,6 +108,7 @@ class Fill3DTool(activeSourceStateProperty: SimpleObjectProperty + floodFillTask = task paintera.baseView.isDisabledProperty.addListener(setFalseAndRemoveListener) paintera.baseView.disabledPropertyBindings[this] = fillIsRunningProperty @@ -112,9 +118,11 @@ class Fill3DTool(activeSourceStateProperty: SimpleObjectProperty - setFalseAndRemoveListener = ChangeListener { obs, _, isBusy -> - if (isBusy) { - paint2D.setBrushCursor(Cursor.WAIT) - } else { - paint2D.setBrushCursor(Cursor.NONE) - if (!paintera.keyTracker.areKeysDown(*keyTrigger.toTypedArray()) && !enteredWithoutKeyTrigger) { - InvokeOnJavaFXApplicationThread { mode?.switchTool(mode.defaultTool) } + if (submitMask) { + lateinit var setFalseAndRemoveListener: ChangeListener + setFalseAndRemoveListener = ChangeListener { obs, _, isBusy -> + if (isBusy) { + paint2D.setBrushCursor(Cursor.WAIT) + } else { + paint2D.setBrushCursor(Cursor.NONE) + if (!paintera.keyTracker.areKeysDown(*keyTrigger.toTypedArray()) && !enteredWithoutKeyTrigger) { + InvokeOnJavaFXApplicationThread { mode?.switchTool(mode.defaultTool) } + } + obs.removeListener(setFalseAndRemoveListener) } - obs.removeListener(setFalseAndRemoveListener) } + + paintera.baseView.isDisabledProperty.addListener(setFalseAndRemoveListener) } - paintera.baseView.isDisabledProperty.addListener(setFalseAndRemoveListener) submitPaint() } } diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/paint/SamTool.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/paint/SamTool.kt index 3128919a9..f7bcf47a8 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/paint/SamTool.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/paint/SamTool.kt @@ -37,6 +37,7 @@ import net.imglib2.algorithm.labeling.ConnectedComponents.StructuringElement import net.imglib2.converter.Converters import net.imglib2.img.array.ArrayImgs import net.imglib2.loops.LoopBuilder +import net.imglib2.parallel.TaskExecutors import net.imglib2.realtransform.AffineTransform3D import net.imglib2.realtransform.Scale3D import net.imglib2.realtransform.Translation3D @@ -77,6 +78,7 @@ import org.janelia.saalfeldlab.paintera.control.paint.ViewerMask.Companion.creat import org.janelia.saalfeldlab.paintera.data.mask.MaskInfo import org.janelia.saalfeldlab.paintera.data.mask.MaskedSource import org.janelia.saalfeldlab.paintera.paintera +import org.janelia.saalfeldlab.paintera.properties import org.janelia.saalfeldlab.paintera.state.SourceState import org.janelia.saalfeldlab.paintera.util.IntervalHelpers import org.janelia.saalfeldlab.paintera.util.IntervalHelpers.Companion.asRealInterval @@ -89,6 +91,8 @@ import java.io.PipedOutputStream import java.lang.invoke.MethodHandles import java.nio.ByteBuffer import java.nio.FloatBuffer +import java.nio.file.Files +import java.nio.file.Paths import java.util.concurrent.Executors import java.util.concurrent.LinkedBlockingQueue import javax.imageio.ImageIO @@ -101,8 +105,6 @@ import kotlin.math.min import kotlin.math.sign import kotlin.properties.Delegates -private val SAM_SERVICE = "http://${System.getenv("SAM_SERVICE_HOST") ?: "gpu3.saalfeldlab.org"}/embedded_model" - open class SamTool(activeSourceStateProperty: SimpleObjectProperty?>, mode: ToolMode? = null) : PaintTool(activeSourceStateProperty, mode) { override val graphic = { FontAwesomeIconView().also { it.styleClass += listOf("toolbar-tool", "sam-select") } } @@ -467,7 +469,7 @@ open class SamTool(activeSourceStateProperty: SimpleObjectProperty - LOG.error("Failure retrieving image embedding", task.exception); + LOG.error("Failure retrieving image embedding", task.exception) mode?.switchTool(mode.defaultTool) }.also { getImageEmbeddingTask = it @@ -550,11 +552,17 @@ open class SamTool(activeSourceStateProperty: SimpleObjectProperty = ArrayImgs.unsignedLongs(*predictionMask.dimensionsAsLongArray()) - ConnectedComponents.labelAllConnectedComponents( - filter, - connectedComponents, - StructuringElement.FOUR_CONNECTED - ) + try { + ConnectedComponents.labelAllConnectedComponents( + filter, + connectedComponents, + StructuringElement.FOUR_CONNECTED + ) + } catch (e: InterruptedException) { + LOG.debug("Connected Components Interrupted During SAM", e) + task.cancel() + continue + } val previousPredictionInterval = lastPredictionProperty.get()?.maskInterval?.extendBy(1.0)?.smallestContainingInterval if (noneAccepted) { @@ -726,8 +734,7 @@ open class SamTool(activeSourceStateProperty: SimpleObjectProperty + Tasks.createTask { + if (!::ortEnv.isInitialized) + ortEnv = OrtEnvironment.getEnvironment() + val modelArray = try { + Companion::class.java.classLoader.getResourceAsStream(modelLocation)!!.readAllBytes() + } catch (e: Exception) { + Files.readAllBytes(Paths.get(modelLocation)) + } + val session = ortEnv.createSession(modelArray) + session + }.submit() + }.beforeValueChange { + it?.cancel() + } data class SamTaskInfo(val maskedSource: MaskedSource<*, *>, val maskInterval: Interval) diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/data/n5/N5Adapter.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/data/n5/N5Adapter.kt index 7c4520b10..39e40918f 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/data/n5/N5Adapter.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/data/n5/N5Adapter.kt @@ -14,7 +14,6 @@ import org.janelia.saalfeldlab.paintera.Paintera.Companion.n5Factory import org.janelia.saalfeldlab.paintera.serialization.GsonExtensions import org.janelia.saalfeldlab.paintera.serialization.StatefulSerializer import org.janelia.saalfeldlab.paintera.state.SourceState -import org.janelia.saalfeldlab.util.n5.N5Helpers.getWriterIfN5ContainerExists import org.scijava.plugin.Plugin import java.lang.reflect.Type import java.util.function.IntFunction @@ -83,7 +82,7 @@ class N5FSWriterAdapter : StatefulSerializer.SerializerAndDeserializer, dependencyFromIndex: IntFunction>?, ): JsonDeserializer = N5ReaderDeserializer(projectDirectory) { - getWriterIfN5ContainerExists(it) as N5FSWriter + n5Factory.openWriterElseOpenReader(it) as N5FSWriter } override fun getTargetClass() = N5FSWriter::class.java @@ -124,7 +123,7 @@ class N5GoogleCloudWriterAdapter : projectDirectory: Supplier, dependencyFromIndex: IntFunction>?, ): JsonDeserializer = N5ReaderDeserializer(projectDirectory) { - getWriterIfN5ContainerExists(it) as N5GoogleCloudStorageWriter + n5Factory.openWriterElseOpenReader(it) as N5GoogleCloudStorageWriter } override fun getTargetClass() = N5GoogleCloudStorageWriter::class.java @@ -164,7 +163,7 @@ class N5AmazonS3WriterAdapter : projectDirectory: Supplier, dependencyFromIndex: IntFunction>?, ): JsonDeserializer = N5ReaderDeserializer(projectDirectory) { - getWriterIfN5ContainerExists(it) as N5AmazonS3Writer + n5Factory.openWriterElseOpenReader(it) as N5AmazonS3Writer } override fun getTargetClass() = N5AmazonS3Writer::class.java @@ -204,7 +203,7 @@ class N5ZarrWriterAdapter : StatefulSerializer.SerializerAndDeserializer, dependencyFromIndex: IntFunction>?, ): JsonDeserializer = N5ReaderDeserializer(projectDirectory) { - getWriterIfN5ContainerExists(it) as N5ZarrWriter + n5Factory.openWriterElseOpenReader(it) as N5ZarrWriter } override fun getTargetClass() = N5ZarrWriter::class.java diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/data/n5/N5HDF5WriterAdapter.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/data/n5/N5HDF5WriterAdapter.kt index 265ce5102..5c3ebaed7 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/data/n5/N5HDF5WriterAdapter.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/data/n5/N5HDF5WriterAdapter.kt @@ -7,8 +7,6 @@ import org.janelia.saalfeldlab.paintera.Paintera.Companion.n5Factory import org.janelia.saalfeldlab.paintera.serialization.GsonExtensions import org.janelia.saalfeldlab.paintera.serialization.StatefulSerializer import org.janelia.saalfeldlab.paintera.state.SourceState -import org.janelia.saalfeldlab.util.n5.N5Helpers.getReaderOrWriterIfN5ContainerExists -import org.janelia.saalfeldlab.util.n5.N5Helpers.getWriterIfN5ContainerExists import org.scijava.plugin.Plugin import java.lang.reflect.Type import java.nio.file.Path @@ -84,10 +82,14 @@ class N5HDF5ReaderAdapter : StatefulSerializer.SerializerAndDeserializer, dependencyFromIndex: IntFunction>?, - ): JsonDeserializer = HDF5Deserializer(projectDirectory) { file, overrideBlockSize, defaultBlockSize -> - n5Factory.hdf5OverrideBlockSize(overrideBlockSize) - n5Factory.hdf5DefaultBlockSize(*(defaultBlockSize ?: intArrayOf())) - (getReaderOrWriterIfN5ContainerExists(file) as? N5HDF5Reader) ?: throw hdf5OpenError(file) + ): JsonDeserializer = HDF5Deserializer(projectDirectory) { file, overrideBlockSize, blockSize -> + with(n5Factory) { + if (overrideBlockSize && blockSize != null) { + hdf5OverrideBlockSize(true) + hdf5DefaultBlockSize(*blockSize) + } + (openWriterElseOpenReader(file)) as? N5HDF5Reader ?: throw hdf5OpenError(file) + } } override fun getTargetClass() = N5HDF5Reader::class.java @@ -108,7 +110,7 @@ class N5HDF5WriterAdapter : StatefulSerializer.SerializerAndDeserializer = HDF5Deserializer(projectDirectory) { file, _, defaultBlockSize -> //FIXME this should be temporary! we should generify these special adaptors if possible. n5Factory.hdf5DefaultBlockSize(*(defaultBlockSize ?: intArrayOf())) - (getWriterIfN5ContainerExists(file) as? N5HDF5Writer) ?: throw hdf5OpenError(file) + (n5Factory.openWriterElseOpenReader(file) as? N5HDF5Writer) ?: throw hdf5OpenError(file) } override fun getTargetClass() = N5HDF5Writer::class.java diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/meshes/MeshSettings.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/meshes/MeshSettings.kt index 37b6fff53..a6c7a7cc6 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/meshes/MeshSettings.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/meshes/MeshSettings.kt @@ -20,7 +20,6 @@ class MeshSettings @JvmOverloads constructor( val opacity: Double val drawMode: DrawMode val cullFace: CullFace - val inflate: Double val isVisible: Boolean val minLabelRatio: Double val levelOfDetail: Int @@ -45,9 +44,6 @@ class MeshSettings @JvmOverloads constructor( @JvmStatic val cullFace = CullFace.FRONT - @JvmStatic - val inflate = 1.0 - @JvmStatic val isVisible = true @@ -83,7 +79,6 @@ class MeshSettings @JvmOverloads constructor( override var opacity: Double = Defaults.Values.opacity override var drawMode: DrawMode = Defaults.Values.drawMode override var cullFace: CullFace = Defaults.Values.cullFace - override var inflate: Double = Defaults.Values.inflate override var isVisible: Boolean = Defaults.Values.isVisible override var minLabelRatio: Double = Defaults.Values.minLabelRatio override var levelOfDetail: Int = Defaults.Values.levelOfDetail @@ -103,7 +98,6 @@ class MeshSettings @JvmOverloads constructor( val opacityProperty: DoubleProperty = SimpleDoubleProperty(defaults.opacity) val drawModeProperty: ObjectProperty = SimpleObjectProperty(defaults.drawMode) val cullFaceProperty: ObjectProperty = SimpleObjectProperty(defaults.cullFace) - val inflateProperty: DoubleProperty = SimpleDoubleProperty(defaults.inflate) val isVisibleProperty: BooleanProperty = SimpleBooleanProperty(defaults.isVisible) val minLabelRatioProperty: DoubleProperty = SimpleDoubleProperty(defaults.minLabelRatio) val levelOfDetailProperty: IntegerProperty = SimpleIntegerProperty(defaults.levelOfDetail) @@ -116,7 +110,6 @@ class MeshSettings @JvmOverloads constructor( var opacity by opacityProperty.nonnull() var drawMode by drawModeProperty.nonnull() var cullFace by cullFaceProperty.nonnull() - var inflate by inflateProperty.nonnull() var isVisible by isVisibleProperty.nonnull() var minLabelRatio by minLabelRatioProperty.nonnull() var levelOfDetail by levelOfDetailProperty.nonnull() @@ -147,7 +140,6 @@ class MeshSettings @JvmOverloads constructor( opacity = that.opacity drawMode = that.drawMode cullFace = that.cullFace - inflate = that.inflate isVisible = that.isVisible } @@ -171,7 +163,6 @@ class MeshSettings @JvmOverloads constructor( opacityProperty.bind(that.opacityProperty) drawModeProperty.bind(that.drawModeProperty) cullFaceProperty.bind(that.cullFaceProperty) - inflateProperty.bind(that.inflateProperty) isVisibleProperty.bind(that.isVisibleProperty) } @@ -186,7 +177,6 @@ class MeshSettings @JvmOverloads constructor( opacityProperty.unbind() drawModeProperty.unbind() cullFaceProperty.unbind() - inflateProperty.unbind() isVisibleProperty.unbind() } @@ -201,7 +191,6 @@ class MeshSettings @JvmOverloads constructor( opacityProperty.bindBidirectional(that.opacityProperty) drawModeProperty.bindBidirectional(that.drawModeProperty) cullFaceProperty.bindBidirectional(that.cullFaceProperty) - inflateProperty.bindBidirectional(that.inflateProperty) isVisibleProperty.bindBidirectional(that.isVisibleProperty) } @@ -216,7 +205,6 @@ class MeshSettings @JvmOverloads constructor( opacityProperty.unbindBidirectional(that.opacityProperty) drawModeProperty.unbindBidirectional(that.drawModeProperty) cullFaceProperty.unbindBidirectional(that.cullFaceProperty) - inflateProperty.unbindBidirectional(that.inflateProperty) isVisibleProperty.unbindBidirectional(that.isVisibleProperty) } @@ -229,7 +217,6 @@ class MeshSettings @JvmOverloads constructor( && opacity == defaults.opacity && defaults.drawMode == drawMode && defaults.cullFace == cullFace - && inflate == defaults.inflate && isVisible == defaults.isVisible } diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/meshes/ui/MeshSettingsController.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/meshes/ui/MeshSettingsController.kt index 775ab2189..0537b709e 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/meshes/ui/MeshSettingsController.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/meshes/ui/MeshSettingsController.kt @@ -34,7 +34,6 @@ class MeshSettingsController @JvmOverloads constructor( private val smoothingLambda: DoubleProperty, private val smoothingIterations: IntegerProperty, private val minLabelRatio: DoubleProperty, - private val inflate: DoubleProperty, private val drawMode: Property, private val cullFace: Property, private val isVisible: BooleanProperty, @@ -51,7 +50,6 @@ class MeshSettingsController @JvmOverloads constructor( meshSettings.smoothingLambdaProperty, meshSettings.smoothingIterationsProperty, meshSettings.minLabelRatioProperty, - meshSettings.inflateProperty, meshSettings.drawModeProperty, meshSettings.cullFaceProperty, meshSettings.isVisibleProperty, @@ -74,10 +72,9 @@ class MeshSettingsController @JvmOverloads constructor( it.slider.valueProperty().bindBidirectional(coarsestScaleLevel) }, NumericSliderWithField(0, this.numScaleLevels - 1, finestScaleLevel.value).apply { slider.valueProperty().bindBidirectional(finestScaleLevel) }, - NumericSliderWithField(0.0, 1.00, .05).apply { slider.valueProperty().bindBidirectional(smoothingLambda) }, - NumericSliderWithField(0, 10, 5).apply { slider.valueProperty().bindBidirectional(smoothingIterations) }, + NumericSliderWithField(0.0, 1.00, 1.0).apply { slider.valueProperty().bindBidirectional(smoothingLambda) }, + NumericSliderWithField(0, 10, 1).apply { slider.valueProperty().bindBidirectional(smoothingIterations) }, NumericSliderWithField(0.0, 1.0, 0.5).apply { slider.valueProperty().bindBidirectional(minLabelRatio) }, - NumericSliderWithField(0.5, 2.0, inflate.value).apply { slider.valueProperty().bindBidirectional(inflate) }, ComboBox(FXCollections.observableArrayList(*DrawMode.values())).apply { valueProperty().bindBidirectional(drawMode) }, ComboBox(FXCollections.observableArrayList(*CullFace.values())).apply { valueProperty().bindBidirectional(cullFace) }) } @@ -154,7 +151,6 @@ class MeshSettingsController @JvmOverloads constructor( smoothingLambdaSlider: NumericSliderWithField, smoothingIterationsSlider: NumericSliderWithField, minLabelRatioSlider: NumericSliderWithField, - inflateSlider: NumericSliderWithField, drawModeChoice: ComboBox, cullFaceChoice: ComboBox, ): GridPane { @@ -188,7 +184,6 @@ class MeshSettingsController @JvmOverloads constructor( addGridOption("Min label ratio", minLabelRatioSlider, tooltipText) } - addGridOption("Inflate", inflateSlider, "Inflate Meshes by Factor") addGridOption("Draw Mode", drawModeChoice) addGridOption("Cull Face", cullFaceChoice) return this diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/serialization/Properties.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/serialization/Properties.kt index 700803755..063d163ee 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/serialization/Properties.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/serialization/Properties.kt @@ -59,6 +59,12 @@ class Properties : TransformListener { @Expose val multiBoxOverlayConfig = MultiBoxOverlayConfig() + @Expose + val segmentAnythingConfig = SegmentAnythingConfig() + + @Expose + val painteraDirectoriesConfig = PainteraDirectoriesConfig() + @Transient val keyAndMouseConfig = KeyAndMouseConfig() diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/LabelSourceStateMeshPaneNode.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/LabelSourceStateMeshPaneNode.kt index 44b56fb46..feabb2660 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/LabelSourceStateMeshPaneNode.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/LabelSourceStateMeshPaneNode.kt @@ -11,6 +11,7 @@ import javafx.scene.control.* import javafx.scene.layout.HBox import javafx.scene.layout.Priority import javafx.scene.layout.VBox +import javafx.scene.paint.Color import javafx.stage.Modality import net.imglib2.type.label.LabelMultisetType import org.janelia.saalfeldlab.fx.extensions.TitledPaneExtensions @@ -18,6 +19,7 @@ import org.janelia.saalfeldlab.fx.extensions.createNonNullValueBinding import org.janelia.saalfeldlab.fx.util.InvokeOnJavaFXApplicationThread import org.janelia.saalfeldlab.paintera.data.DataSource import org.janelia.saalfeldlab.paintera.meshes.GlobalMeshProgress +import org.janelia.saalfeldlab.paintera.meshes.MeshExporterObj import org.janelia.saalfeldlab.paintera.meshes.SegmentMeshInfo import org.janelia.saalfeldlab.paintera.meshes.SegmentMeshInfos import org.janelia.saalfeldlab.paintera.meshes.managed.MeshManagerWithAssignmentForSegments @@ -28,7 +30,7 @@ import org.janelia.saalfeldlab.paintera.ui.source.mesh.SegmentMeshExporterDialog import org.janelia.saalfeldlab.paintera.ui.source.mesh.SegmentMeshInfoNode import org.slf4j.LoggerFactory import java.lang.invoke.MethodHandles -import java.util.Objects +import java.util.* import java.util.stream.Collectors typealias TPE = TitledPaneExtensions @@ -114,14 +116,23 @@ class LabelSourceStateMeshPaneNode( val exportDialog = SegmentMeshExporterDialog(meshesInfoList.items) val result = exportDialog.showAndWait() if (result.isPresent) { - val parameters = result.get() - parameters.meshExporter.exportMesh( - manager.getBlockListForLongKey, - manager.getMeshForLongKey, - parameters.segmentId.map { it }.toTypedArray(), - parameters.scale, - parameters.filePaths - ) + result.get().run { + val ids = segmentId.map { it }.toTypedArray() + val meshSettings = ids.map { meshInfos.meshSettings().getOrAddMesh(it) }.toTypedArray() + + (meshExporter as? MeshExporterObj<*>)?.run { + val colors : Array = ids.map { meshInfos.getColor(it) }.toTypedArray() + exportMaterial(filePath, ids.toLongArray(), colors) + } + meshExporter.exportMesh( + manager.getBlockListForLongKey, + manager.getMeshForLongKey, + meshSettings, + ids, + scale, + filePath + ) + } } } diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/LabelSourceStatePreferencePaneNode.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/LabelSourceStatePreferencePaneNode.kt index 094c9a3db..f77158120 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/LabelSourceStatePreferencePaneNode.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/LabelSourceStatePreferencePaneNode.kt @@ -1,6 +1,9 @@ package org.janelia.saalfeldlab.paintera.state +import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon +import javafx.application.Platform import javafx.beans.property.ObjectProperty +import javafx.beans.property.SimpleObjectProperty import javafx.event.EventHandler import javafx.geometry.Insets import javafx.geometry.Pos @@ -13,11 +16,12 @@ import net.imglib2.type.numeric.ARGBType import org.janelia.saalfeldlab.fx.Buttons import org.janelia.saalfeldlab.fx.Labels import org.janelia.saalfeldlab.fx.TitledPanes -import org.janelia.saalfeldlab.fx.extensions.TextFieldExtensions import org.janelia.saalfeldlab.fx.extensions.TitledPaneExtensions import org.janelia.saalfeldlab.fx.extensions.addKeyAndScrollHandlers import org.janelia.saalfeldlab.fx.ui.Exceptions import org.janelia.saalfeldlab.fx.ui.NamedNode +import org.janelia.saalfeldlab.fx.ui.NumberField +import org.janelia.saalfeldlab.fx.ui.ObjectField import org.janelia.saalfeldlab.fx.undo.UndoFromEvents import org.janelia.saalfeldlab.paintera.Constants import org.janelia.saalfeldlab.paintera.composition.Composite @@ -36,12 +40,12 @@ import org.janelia.saalfeldlab.paintera.meshes.SegmentMeshInfos import org.janelia.saalfeldlab.paintera.meshes.managed.MeshManagerWithAssignmentForSegments import org.janelia.saalfeldlab.paintera.stream.HighlightingStreamConverter import org.janelia.saalfeldlab.paintera.stream.HighlightingStreamConverterConfigNode +import org.janelia.saalfeldlab.paintera.ui.FontAwesome import org.janelia.saalfeldlab.paintera.ui.PainteraAlerts import org.slf4j.LoggerFactory import java.lang.invoke.MethodHandles import java.text.DecimalFormat - -typealias TFE = TextFieldExtensions +import org.janelia.saalfeldlab.labels.Label.Companion as Imglib2Labels //TODO maybe rename this? Or make it subclass Pane/Node like the name indicates class LabelSourceStatePreferencePaneNode( @@ -81,115 +85,199 @@ class LabelSourceStatePreferencePaneNode( private val selectedSegments: SelectedSegments ) { + class SelectedSegmentsConverter(val selectedSegments: SelectedSegments) : StringConverter() { + override fun toString(obj: SelectedSegments?): String { + return selectedSegments.selectedSegments.toArray().joinToString(",") + } + + override fun fromString(string: String?): SelectedSegments { + val lastFragmentSelection = selectedSegments.selectedIds.lastSelection + /* get fragments from segments and update */ + string?.split(Regex("\\D+"))?.filter { it.isNotBlank() } + ?.map { it.toLong() } + ?.flatMap { selectedSegments.assignment.getFragments(it).toArray().asIterable() } + ?.toLongArray()?.let { fragments -> + if (fragments.isEmpty()) { + selectedSegments.selectedIds.activateAlso(selectedSegments.selectedIds.lastSelection) + return@let + } + selectedSegments.selectedIds.activate(*fragments) + if (selectedSegments.selectedIds.isActive(lastFragmentSelection)) { + selectedSegments.selectedIds.activateAlso(lastFragmentSelection) + } + } + return selectedSegments + } + } + + class SelectedFragmentsConverter(val selectedSegments: SelectedSegments) : StringConverter() { + override fun toString(obj: SelectedSegments?): String { + return selectedSegments.selectedIds.activeIds.toArray().joinToString(",") + } + + override fun fromString(string: String?): SelectedSegments { + val lastFragmentSelection = selectedSegments.selectedIds.lastSelection + string?.split(Regex("\\D+"))?.filter { it.isNotBlank() } + ?.map { it.toLong() } + ?.toLongArray()?.let { fragments -> + if (fragments.isEmpty()) { + selectedSegments.selectedIds.activateAlso(selectedSegments.selectedIds.lastSelection) + return@let + } + selectedSegments.selectedIds.activate(*fragments) + if (selectedSegments.selectedIds.isActive(lastFragmentSelection)) { + selectedSegments.selectedIds.activateAlso(lastFragmentSelection) + } + } + return selectedSegments + } + } + val node: Node get() { if (selectedIds == null || assignment == null) return Region() - val lastSelectionField = TextField() - val selectedIdsField = TextField() - val selectedSegmentsField = TextField() val grid = GridPane().also { it.hgap = 5.0 } - grid.add(lastSelectionField, 1, 0) - grid.add(selectedIdsField, 1, 1) - grid.add(selectedSegmentsField, 1, 2) - val lastSelectionLabel = Label("Last Selection") val fragmentLabel = Label("Fragments") val segmentLabel = Label("Segments") + Platform.runLater { + lastSelectionLabel.maxWidth = Label.USE_COMPUTED_SIZE + fragmentLabel.maxWidth = Label.USE_COMPUTED_SIZE + segmentLabel.maxWidth = Label.USE_COMPUTED_SIZE + } lastSelectionLabel.tooltip = Tooltip("Last selected fragment id") fragmentLabel.tooltip = Tooltip("Active fragment ids") segmentLabel.tooltip = Tooltip("Active segment ids") - val activeFragmentsToolTip = Tooltip() - val activeSegmentsToolTip = Tooltip() - activeFragmentsToolTip.textProperty().bind(selectedIdsField.textProperty()) - activeSegmentsToolTip.textProperty().bind(selectedSegmentsField.textProperty()) - selectedIdsField.tooltip = activeFragmentsToolTip - selectedSegmentsField.tooltip = activeSegmentsToolTip - grid.add(lastSelectionLabel, 0, 0) grid.add(fragmentLabel, 0, 1) grid.add(segmentLabel, 0, 2) + grid.columnConstraints += ColumnConstraints().also { it.hgrow = Priority.NEVER } + + + val selectedSegmentsProperty = SimpleObjectProperty(selectedSegments) + val selectedSegmentsConverter = SelectedSegmentsConverter(selectedSegments) + val segmentsField = ObjectField>( + selectedSegmentsProperty, + selectedSegmentsConverter, + ObjectField.SubmitOn.ENTER_PRESSED, + ObjectField.SubmitOn.FOCUS_LOST, + ) + + val selectedFragmentsConverter = SelectedFragmentsConverter(selectedSegments) + val fragmentsField = ObjectField>( + selectedSegmentsProperty, + selectedFragmentsConverter, + ObjectField.SubmitOn.ENTER_PRESSED, + ObjectField.SubmitOn.FOCUS_LOST + ) + + val lastSelectionField = NumberField.longField( + selectedSegments.selectedIds.lastSelection, + { it >= 0 && it.toULong() < Imglib2Labels.MAX_ID.toULong() }, + ObjectField.SubmitOn.ENTER_PRESSED, + ObjectField.SubmitOn.FOCUS_LOST + ) + lastSelectionField.valueProperty().addListener { _, _, newId -> + val activeFragment = newId.toLong() + if (selectedSegments.selectedIds.isActive(activeFragment)) + selectedSegments.selectedIds.activateAlso(activeFragment) + else + selectedSegments.selectedIds.activate(activeFragment) + } - GridPane.setHgrow(lastSelectionField, Priority.ALWAYS) - GridPane.setHgrow(selectedIdsField, Priority.ALWAYS) - GridPane.setHgrow(selectedSegmentsField, Priority.ALWAYS) - lastSelectionField.isEditable = false - selectedIdsField.isEditable = false - selectedSegmentsField.isEditable = false - - lastSelectionField.setOnMousePressed { event -> - if (event.clickCount == 2) { - event.consume() - val tf = with(TFE) { TextField(lastSelectionField.text).also { it.acceptOnly(LAST_SELECTION_REGEX) } } - val setOnly = ButtonType("_Set", ButtonBar.ButtonData.OK_DONE) - val append = ButtonType("_Append", ButtonBar.ButtonData.OK_DONE) - val bt = PainteraAlerts.confirmation("_Set", "_Cancel", true).apply { - headerText = "Set last selected fragment." - dialogPane.content = VBox( - Label(LAST_SELECTION_DIALOG_DESCRIPTION).apply { isWrapText = true }, - HBox(Label("Fragment:"), tf).apply { - alignment = Pos.CENTER_LEFT - spacing = 5.0 - } - ) - dialogPane.buttonTypes.setAll(append, setOnly, ButtonType.CANCEL) - }.showAndWait() - bt.orElse(null)?.let { b -> - if (setOnly == b) tf.text?.let { selectedIds.activate(it.toLong()) } - else if (append == b) tf.text?.let { selectedIds.activateAlso(it.toLong()) } - else null - } - } + selectedSegments.addListener { _ -> + segmentsField.textField.text = selectedSegmentsConverter.toString(selectedSegments) + fragmentsField.textField.text = selectedFragmentsConverter.toString(selectedSegments) + lastSelectionField.textField.text = selectedSegments.selectedIds.lastSelection.toString() + } + + val activeFragmentsToolTip = Tooltip() + val activeSegmentsToolTip = Tooltip() + activeFragmentsToolTip.textProperty().bind(fragmentsField.textField.textProperty()) + activeSegmentsToolTip.textProperty().bind(segmentsField.textField.textProperty()) + fragmentsField.textField.tooltip = activeFragmentsToolTip + segmentsField.textField.tooltip = activeSegmentsToolTip + + listOf(lastSelectionField, fragmentsField, segmentsField).forEach { it.textField.alignment = Pos.BASELINE_LEFT } + + + Platform.runLater { + lastSelectionField.textField.maxWidth = Region.USE_COMPUTED_SIZE + fragmentsField.textField.maxWidth = Region.USE_COMPUTED_SIZE + segmentsField.textField.maxWidth = Region.USE_COMPUTED_SIZE } - selectedIdsField.setOnMousePressed { event -> - if (event.clickCount == 2) { - event.consume() - val tf = with(TFE) { TextField(selectedIdsField.text).also { it.acceptOnly(SELECTION_REGEX) } } - val bt = PainteraAlerts.confirmation("_Set", "_Cancel", true).apply { - headerText = "Select active fragments." - dialogPane.content = VBox( - Label(SELECTION_DIALOG_DESCRIPTION).apply { isWrapText = true }, - HBox(Label("Fragments:"), tf).apply { - alignment = Pos.CENTER_LEFT - spacing = 5.0 - } - ) + grid.add(lastSelectionField.textField, 1, 0) + grid.add(fragmentsField.textField, 1, 1) + grid.add(segmentsField.textField, 1, 2) + grid.columnConstraints += ColumnConstraints().also { it.hgrow = Priority.ALWAYS } + + + val lastSelectionHelp = Button().apply { + graphic = FontAwesome[FontAwesomeIcon.QUESTION] + onAction = EventHandler { + PainteraAlerts.information("Ok", true).also { + it.title = "Last Selection" + it.headerText = it.title + it.dialogPane.content = TextArea().also { area -> + area.isWrapText = true + area.isEditable = false + area.text = LAST_SELECTION_DIALOG_DESCRIPTION + } }.showAndWait() - bt.filter { ButtonType.OK == it }.orElse(null)?.let { - val selection = (tf.text ?: "").split(",").map { it.trim() }.filter { it.isNotEmpty() }.map { it.toLong() }.toLongArray() - val lastSelected = selectedIds.lastSelection.takeIf { selection.contains(it) } - selectedIds.activate(*selection) - lastSelected?.let { selectedIds.activateAlso(it) } - } } } - - - selectedIds.addListener { - selectedIdsField.text = if (selectedIds.isEmpty) "" else selectedIds.activeIdsCopyAsArray.joinToString(separator = ", ") { it.toString() } - lastSelectionField.text = selectedIds.lastSelection.takeIf(IS_FOREGROUND)?.toString() ?: "" + val fragmentSelectionHelp = Button().apply { + graphic = FontAwesome[FontAwesomeIcon.QUESTION] + onAction = EventHandler { + PainteraAlerts.information("Ok", true).also { + it.title = "Fragment Selection" + it.headerText = it.title + it.dialogPane.content = TextArea().also { area -> + area.isWrapText = true + area.text = FRAGMENT_SELECTION_DIALOG_DESCRIPTION + } + }.showAndWait() + } } - selectedSegments.let { sel -> - sel.addListener { - selectedSegmentsField.text = sel.selectedSegmentsCopyAsArray.joinToString(", ") { it.toString() } + val segmentSelectionHelp = Button().apply { + graphic = FontAwesome[FontAwesomeIcon.QUESTION] + onAction = EventHandler { + PainteraAlerts.information("Ok", true).also { + it.title = "Segment Selection" + it.headerText = it.title + it.dialogPane.content = TextArea().also { area -> + area.isWrapText = true + area.isEditable = false + area.text = SEGMENT_SELECTION_DIALOG_DESCRIPTION + } + }.showAndWait() } } + grid.add(lastSelectionHelp, 2, 0) + grid.add(fragmentSelectionHelp, 2, 1) + grid.add(segmentSelectionHelp, 2, 2) + grid.columnConstraints += ColumnConstraints().also { it.hgrow = Priority.NEVER } val helpDialog = PainteraAlerts.alert(Alert.AlertType.INFORMATION, true).apply { initModality(Modality.NONE) headerText = "Fragment Selection" - contentText = DESCRIPTION + dialogPane.content = TextArea().also { + it.isWrapText = true + it.isEditable = false + it.text = DESCRIPTION + } } val tpGraphics = HBox( Label("Fragment Selection"), NamedNode.bufferNode(), - Button("?").also { bt -> bt.onAction = EventHandler { helpDialog.show() } }) - .also { it.alignment = Pos.CENTER } + Button("?").also { bt -> bt.onAction = EventHandler { helpDialog.show() } } + ).also { it.alignment = Pos.CENTER } return with(TitledPaneExtensions) { TitledPane(null, grid).apply { @@ -203,28 +291,33 @@ class LabelSourceStatePreferencePaneNode( } companion object { - private val IS_FOREGROUND = { id: Long -> net.imglib2.type.label.Label.isForeground(id) } - - private const val LAST_SELECTION_DIALOG_DESCRIPTION = "" + - "The last selected fragment is used for painting, and assignment actions. " + - "The new selection can be appended to the currently active fragments or set as the only active fragment. " + - "If no fragments are currently active, both choices are equivalent." - - private const val SELECTION_DIALOG_DESCRIPTION = "" + - "Active fragments (and the containing segments) will be highlighted in the 2D cross-sections and rendered " + - "in the 3D viewer. If the current last selected fragment is not part of the new selection, an arbitrary fragment " + - "of the new selection will be chosen to be last selected fragment." - - private val LAST_SELECTION_REGEX = "^$|\\d+".toRegex() - - // empty string or one integer followed by any number of commas followed by optional space and integer number - private val SELECTION_REGEX = "^$|\\d+(, *\\d*)*".toRegex() - private const val DESCRIPTION = "" + - "Fragment can be selected with left mouse click. When used with the CTRL key or right mouse click append the " + - "fragment to the set of currently active (selected) fragments. In either case, the last selected fragment will " + - "be used for tasks that require a fragment id, such as painting or merge/split actions. Alternatively, the last " + - "selection and set of currently active fragments can be modified by double clicking the respective text fields." + private val LAST_SELECTION_DIALOG_DESCRIPTION = """ + The last selected fragment is used for painting, and assignment actions. + The new selection can be appended to the currently active fragments or set as the only active fragment. + If no fragments are currently active, both choices are equivalent. + """.trimIndent() + + private val FRAGMENT_SELECTION_DIALOG_DESCRIPTION = """ + Active fragments (and the containing segments) will be highlighted in the 2D cross-sections and rendered + in the 3D viewer. All segments that contain the specified fragments will also be selected. + If the current Last Selection ID is not one of the listed Fragment Selection IDs, then + the last fragment specified will be used for the Last Selection ID. + """.trimIndent() + + private val SEGMENT_SELECTION_DIALOG_DESCRIPTION = """ + Active segments (and all contained fragments) will be highlighted in the 2D cross-sections and rendered + in the 3D viewer. When manually specified, all fragments contained by the selected fragments will also be selected. + If the current last selection is not part of the specified selected segment(s), the resulting last fragment will be used + for the Last Selection ID. + """.trimIndent() + + private val DESCRIPTION = """ + Fragment can be selected with left mouse click. When used with the CTRL key or right mouse click append the + fragment to the set of currently active (selected) fragments. In either case, the last selected fragment will + be used for tasks that require a fragment id, such as painting or merge/split actions. Alternatively, the last + selection and set of currently active fragments can be modified by double clicking the respective text fields. + """.trimIndent() } } diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/label/CommitHandler.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/label/CommitHandler.kt index aae52f489..c1ea6980f 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/label/CommitHandler.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/label/CommitHandler.kt @@ -43,7 +43,7 @@ class CommitHandler>(private val state: S, private val fra cancelButtonText: String = "_Cancel", okButtonText: String = "Commi_t", fragmentSegmentAssignmentState: FragmentSegmentAssignmentState - ) { + ): ButtonType? { val assignmentsCanBeCommitted = fragmentSegmentAssignmentState.hasPersistableData() val canvasCanBeCommitted = state.dataSource.let { it is MaskedSource && it.affectedBlocks.isNotEmpty() } val commitAssignmentCheckbox = CheckBox("Fragment-segment assignment").also { it.isSelected = assignmentsCanBeCommitted } @@ -55,6 +55,12 @@ class CommitHandler>(private val state: S, private val fra if (assignmentsCanBeCommitted) contents.children.add(commitAssignmentCheckbox) if (canvasCanBeCommitted) contents.children.add(commitCanvasCheckbox) PainteraAlerts.confirmation(okButtonText, cancelButtonText, true).also { + (it.dialogPane.lookupButton(ButtonType.CANCEL) as? Button)?.let { closeButton -> + closeButton.isVisible = false + closeButton.isManaged = false + } + it.buttonTypes.add(ButtonType.NO) + (it.dialogPane.lookupButton(ButtonType.NO) as? Button)?.text = cancelButtonText it.headerText = headerText.apply(index, name) it.dialogPane.content = contents } @@ -67,7 +73,8 @@ class CommitHandler>(private val state: S, private val fra else null } - if (dialog?.showAndWait()?.filter { ButtonType.OK == it }?.isPresent == true && anythingToCommit) { + val buttonType = dialog?.showAndWait() + if (buttonType?.filter { ButtonType.OK == it }?.isPresent == true && anythingToCommit) { if (assignmentsCanBeCommitted && commitAssignmentCheckbox.isSelected) fragmentSegmentAssignmentState.persist() state.dataSource.let { if (canvasCanBeCommitted && commitCanvasCheckbox.isSelected && it is MaskedSource) { @@ -75,6 +82,7 @@ class CommitHandler>(private val state: S, private val fra } } } + return buttonType?.get() } } diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/label/ConnectomicsLabelState.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/label/ConnectomicsLabelState.kt index d6f1c2e3d..1d6c5857b 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/label/ConnectomicsLabelState.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/label/ConnectomicsLabelState.kt @@ -32,7 +32,10 @@ import net.imglib2.type.numeric.RealType import org.apache.commons.lang.builder.HashCodeBuilder import org.janelia.saalfeldlab.fx.TitledPanes import org.janelia.saalfeldlab.fx.actions.ActionSet -import org.janelia.saalfeldlab.fx.extensions.* +import org.janelia.saalfeldlab.fx.extensions.TitledPaneExtensions +import org.janelia.saalfeldlab.fx.extensions.createNonNullValueBinding +import org.janelia.saalfeldlab.fx.extensions.createObservableBinding +import org.janelia.saalfeldlab.fx.extensions.nonnull import org.janelia.saalfeldlab.fx.ui.NamedNode import org.janelia.saalfeldlab.fx.util.InvokeOnJavaFXApplicationThread import org.janelia.saalfeldlab.labels.blocks.LabelBlockLookup @@ -76,10 +79,19 @@ import org.slf4j.LoggerFactory import java.lang.invoke.MethodHandles import java.lang.reflect.Type import java.util.concurrent.ExecutorService -import java.util.function.IntFunction -import java.util.function.LongFunction -import java.util.function.Predicate -import java.util.function.Supplier +import java.util.function.* +import kotlin.collections.List +import kotlin.collections.Map +import kotlin.collections.any +import kotlin.collections.asSequence +import kotlin.collections.component1 +import kotlin.collections.component2 +import kotlin.collections.forEach +import kotlin.collections.isEmpty +import kotlin.collections.isNotEmpty +import kotlin.collections.listOf +import kotlin.collections.map +import kotlin.collections.toTypedArray class ConnectomicsLabelState, T>( override val backend: ConnectomicsLabelBackend, @@ -93,7 +105,7 @@ class ConnectomicsLabelState, T>( name: String, labelBlockLookup: LabelBlockLookup? = null, ) : SourceStateWithBackend, IntersectableSourceState - where T : net.imglib2.type.Type, T : Volatile { + where T : net.imglib2.type.Type, T : Volatile { private val source: DataSource = backend.createSource(queue, priority, name) override fun getDataSource(): DataSource = source @@ -118,6 +130,8 @@ class ConnectomicsLabelState, T>( private val converter = HighlightingStreamConverter.forType(stream, dataSource.type) + internal var skipCommit = false + override fun converter(): HighlightingStreamConverter = converter val meshManager = MeshManagerWithAssignmentForSegments.fromBlockLookup( @@ -287,18 +301,25 @@ class ConnectomicsLabelState, T>( } override fun onShutdown(paintera: PainteraBaseView) { - CommitHandler.showCommitDialog( + if (!skipCommit) { + promptForCommitIfNecessary(paintera) { index, name -> + """ + Shutting down Paintera. + Uncommitted changes to the canvas will be lost for source $index: $name if skipped. + Uncommitted changes to the fragment-segment-assigment will be stored in the Paintera project (if any) + but can be committed to the data backend, as well + """.trimIndent() + } + } + skipCommit = false + } + + internal fun promptForCommitIfNecessary(paintera: PainteraBaseView, prompt: BiFunction) : ButtonType? { + return CommitHandler.showCommitDialog( this, paintera.sourceInfo().indexOf(this.dataSource), false, - { index, name -> - """ - Shutting down Paintera. - Uncommitted changes to the canvas will be lost for source $index: $name if skipped. - Uncommitted changes to the fragment-segment-assigment will be stored in the Paintera project (if any) - but can be committed to the data backend, as well - """.trimIndent() - }, + prompt, false, "_Skip", fragmentSegmentAssignmentState = fragmentSegmentAssignment @@ -460,7 +481,7 @@ class ConnectomicsLabelState, T>( @JvmStatic fun labelToBooleanFragmentMaskSource(labelSource: ConnectomicsLabelState): DataSource> - where D : IntegerType, T : Volatile, T : net.imglib2.type.Type { + where D : IntegerType, T : Volatile, T : net.imglib2.type.Type { return with(labelSource) { val fragmentsInSelectedSegments = FragmentsInSelectedSegments(selectedSegments) PredicateDataSource(dataSource, checkForType(dataSource.dataType, fragmentsInSelectedSegments), name) @@ -556,7 +577,7 @@ class ConnectomicsLabelState, T>( @Plugin(type = PainteraSerialization.PainteraSerializer::class) class Serializer, T> : PainteraSerialization.PainteraSerializer> - where T : net.imglib2.type.Type, T : Volatile { + where T : net.imglib2.type.Type, T : Volatile { override fun serialize(state: ConnectomicsLabelState, typeOfSrc: Type, context: JsonSerializationContext): JsonElement { val map = JsonObject() with(SerializationKeys) { @@ -596,12 +617,12 @@ class ConnectomicsLabelState, T>( class Deserializer, T>(val viewer: PainteraBaseView) : JsonDeserializer> - where T : net.imglib2.type.Type, T : Volatile { + where T : net.imglib2.type.Type, T : Volatile { @Plugin(type = StatefulSerializer.DeserializerFactory::class) class Factory, T> : StatefulSerializer.DeserializerFactory, Deserializer> - where T : net.imglib2.type.Type, T : Volatile { + where T : net.imglib2.type.Type, T : Volatile { override fun createDeserializer( arguments: StatefulSerializer.Arguments, projectDirectory: Supplier, diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/label/n5/N5BackendMultiScaleGroup.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/label/n5/N5BackendMultiScaleGroup.kt index 3c4f2c4d5..628358b5f 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/label/n5/N5BackendMultiScaleGroup.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/label/n5/N5BackendMultiScaleGroup.kt @@ -12,6 +12,7 @@ import org.janelia.saalfeldlab.paintera.data.mask.Masks import org.janelia.saalfeldlab.paintera.data.n5.CommitCanvasN5 import org.janelia.saalfeldlab.paintera.data.n5.N5DataSource import org.janelia.saalfeldlab.paintera.id.IdService +import org.janelia.saalfeldlab.paintera.paintera import org.janelia.saalfeldlab.paintera.serialization.GsonExtensions import org.janelia.saalfeldlab.paintera.serialization.GsonExtensions.get import org.janelia.saalfeldlab.paintera.serialization.PainteraSerialization @@ -52,7 +53,6 @@ class N5BackendMultiScaleGroup constructor( queue, priority, name, - projectDirectory, propagationExecutorService ) } @@ -68,13 +68,12 @@ class N5BackendMultiScaleGroup constructor( queue: SharedQueue, priority: Int, name: String, - projectDirectory: Supplier, propagationExecutorService: ExecutorService, ): DataSource where D : NativeType, D : IntegerType, T : net.imglib2.Volatile, T : NativeType { val dataSource = N5DataSource(metadataState, name, queue, priority) return metadataState.writer?.let { - val tmpDir = Masks.canvasTmpDirDirectorySupplier(projectDirectory) - Masks.maskedSource(dataSource, queue, tmpDir.get(), tmpDir, CommitCanvasN5(metadataState), propagationExecutorService) + val canvasDirSupplier = Masks.canvasTmpDirDirectorySupplier(paintera.properties.painteraDirectoriesConfig.appCacheDir) + Masks.maskedSource(dataSource, queue, canvasDirSupplier.get(), canvasDirSupplier, CommitCanvasN5(metadataState), propagationExecutorService) } ?: dataSource } } diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/label/n5/N5BackendPainteraDataset.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/label/n5/N5BackendPainteraDataset.kt index a9b675f15..161d71fc4 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/label/n5/N5BackendPainteraDataset.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/label/n5/N5BackendPainteraDataset.kt @@ -16,6 +16,7 @@ import org.janelia.saalfeldlab.paintera.data.mask.Masks import org.janelia.saalfeldlab.paintera.data.n5.CommitCanvasN5 import org.janelia.saalfeldlab.paintera.data.n5.N5DataSource import org.janelia.saalfeldlab.paintera.id.IdService +import org.janelia.saalfeldlab.paintera.paintera import org.janelia.saalfeldlab.paintera.serialization.GsonExtensions import org.janelia.saalfeldlab.paintera.serialization.GsonExtensions.get import org.janelia.saalfeldlab.paintera.serialization.PainteraSerialization @@ -28,6 +29,7 @@ import org.janelia.saalfeldlab.paintera.state.metadata.N5ContainerState import org.janelia.saalfeldlab.util.grids.LabelBlockLookupNoBlocks import org.janelia.saalfeldlab.util.n5.N5Helpers import org.janelia.saalfeldlab.util.n5.N5Helpers.serializeTo +import org.janelia.saalfeldlab.util.n5.universe.N5DatasetDoesntExist import org.scijava.plugin.Plugin import org.slf4j.LoggerFactory import java.io.File @@ -63,7 +65,6 @@ class N5BackendPainteraDataset( queue, priority, name, - projectDirectory, propagationExecutorService ) } @@ -93,14 +94,13 @@ class N5BackendPainteraDataset( queue: SharedQueue, priority: Int, name: String, - projectDirectory: Supplier, propagationExecutorService: ExecutorService, ): DataSource where D : NativeType, D : IntegerType, T : net.imglib2.Volatile, T : NativeType { val dataSource = N5DataSource(metadataState, name, queue, priority) val containerWriter = metadataState.writer return containerWriter?.let { - val tmpDir = Masks.canvasTmpDirDirectorySupplier(projectDirectory) - Masks.maskedSource(dataSource, queue, tmpDir.get(), tmpDir, CommitCanvasN5(metadataState), propagationExecutorService) + val canvasDirSupplier = Masks.canvasTmpDirDirectorySupplier(paintera.properties.painteraDirectoriesConfig.appCacheDir) + Masks.maskedSource(dataSource, queue, canvasDirSupplier.get(), canvasDirSupplier, CommitCanvasN5(metadataState), propagationExecutorService) } ?: dataSource } @@ -230,10 +230,21 @@ class N5BackendPainteraDataset( ): N5BackendPainteraDataset { return with(SerializationKeys) { with(GsonExtensions) { - val container = N5Helpers.deserializeFrom(json.asJsonObject) + var container = N5Helpers.deserializeFrom(json.asJsonObject) val dataset: String = json[DATASET]!! val n5ContainerState = N5ContainerState(container) - val metadataState = MetadataUtils.createMetadataState(n5ContainerState, dataset).nullable!! + var metadataState = MetadataUtils.createMetadataState(n5ContainerState, dataset).nullable + while (metadataState == null) { + container = N5Helpers.promptForNewLocationOrRemove(container.uri.toString(), N5DatasetDoesntExist(container.uri.toString(), dataset), + "Dataset not found", + """ + Expected dataset "${dataset.ifEmpty { "/" }}" not found at + ${container.uri} + """.trimIndent() + ) + val n5ContainerState = N5ContainerState(container) + metadataState = MetadataUtils.createMetadataState(n5ContainerState, dataset).nullable + } N5BackendPainteraDataset( metadataState, projectDirectory, diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/label/n5/N5BackendSingleScaleDataset.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/label/n5/N5BackendSingleScaleDataset.kt index ab6467366..b2fc5f257 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/label/n5/N5BackendSingleScaleDataset.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/label/n5/N5BackendSingleScaleDataset.kt @@ -12,6 +12,7 @@ import org.janelia.saalfeldlab.paintera.data.mask.Masks import org.janelia.saalfeldlab.paintera.data.n5.CommitCanvasN5 import org.janelia.saalfeldlab.paintera.data.n5.N5DataSource import org.janelia.saalfeldlab.paintera.id.IdService +import org.janelia.saalfeldlab.paintera.paintera import org.janelia.saalfeldlab.paintera.serialization.GsonExtensions import org.janelia.saalfeldlab.paintera.serialization.GsonExtensions.get import org.janelia.saalfeldlab.paintera.serialization.PainteraSerialization @@ -52,7 +53,6 @@ class N5BackendSingleScaleDataset constructor( queue, priority, name, - projectDirectory, propagationExecutorService ) } @@ -84,13 +84,12 @@ class N5BackendSingleScaleDataset constructor( queue: SharedQueue, priority: Int, name: String, - projectDirectory: Supplier, propagationExecutorService: ExecutorService, ): DataSource where D : NativeType, D : IntegerType, T : net.imglib2.Volatile, T : NativeType { val dataSource = N5DataSource(metadataState, name, queue, priority) return metadataState.writer?.let { - val tmpDir = Masks.canvasTmpDirDirectorySupplier(projectDirectory) - Masks.maskedSource(dataSource, queue, tmpDir.get(), tmpDir, CommitCanvasN5(metadataState), propagationExecutorService) + val canvasDirSupplier = Masks.canvasTmpDirDirectorySupplier(paintera.properties.painteraDirectoriesConfig.appCacheDir) + Masks.maskedSource(dataSource, queue, canvasDirSupplier.get(), canvasDirSupplier, CommitCanvasN5(metadataState), propagationExecutorService) } ?: dataSource } } diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/metadata/MetadataState.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/metadata/MetadataState.kt index 501f54b0a..167112e16 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/metadata/MetadataState.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/metadata/MetadataState.kt @@ -14,11 +14,11 @@ import org.janelia.saalfeldlab.n5.universe.metadata.N5Metadata import org.janelia.saalfeldlab.n5.universe.metadata.N5SingleScaleMetadata import org.janelia.saalfeldlab.n5.universe.metadata.N5SpatialDatasetMetadata import org.janelia.saalfeldlab.n5.universe.metadata.SpatialMultiscaleMetadata +import org.janelia.saalfeldlab.paintera.Paintera import org.janelia.saalfeldlab.paintera.state.metadata.MetadataState.Companion.isLabel import org.janelia.saalfeldlab.util.n5.ImagesWithTransform import org.janelia.saalfeldlab.util.n5.N5Data import org.janelia.saalfeldlab.util.n5.N5Helpers -import org.janelia.saalfeldlab.util.n5.N5Helpers.getReaderOrWriterIfN5ContainerExists import org.janelia.saalfeldlab.util.n5.metadata.N5PainteraDataMultiScaleGroup import java.util.Optional @@ -38,7 +38,7 @@ interface MetadataState { var unit: String var reader: N5Reader - var writer: N5Writer? + val writer: N5Writer? var group: String val dataset: String get() = group @@ -71,14 +71,12 @@ interface MetadataState { target.resolution = source.resolution.copyOf() target.translation = source.translation.copyOf() target.unit = source.unit - target.reader = source.reader - target.writer = source.writer target.group = source.group } } } -open class SingleScaleMetadataState constructor( +open class SingleScaleMetadataState( final override var n5ContainerState: N5ContainerState, final override val metadata: N5SingleScaleMetadata ) : @@ -93,7 +91,9 @@ open class SingleScaleMetadataState constructor( override var translation = metadata.offset!! override var unit = metadata.unit()!! override var reader = n5ContainerState.reader - override var writer = n5ContainerState.writer + override val writer :N5Writer? + get() = n5ContainerState.writer + override var group = metadata.path!! override fun copy(): SingleScaleMetadataState { @@ -229,7 +229,10 @@ class MetadataUtils { @JvmStatic fun createMetadataState(n5containerAndDataset: String): Optional { - val reader = getReaderOrWriterIfN5ContainerExists(n5containerAndDataset) ?: return Optional.empty() + + val reader = with(Paintera.n5Factory) { + openWriterOrNull(n5containerAndDataset) ?: openReaderOrNull(n5containerAndDataset)?: return Optional.empty() + } val n5ContainerState = N5ContainerState(reader) return N5Helpers.parseMetadata(reader).map { treeNode -> @@ -243,7 +246,9 @@ class MetadataUtils { @JvmStatic fun createMetadataState(n5container: String, dataset: String?): Optional { - val reader = getReaderOrWriterIfN5ContainerExists(n5container) ?: return Optional.empty() + val reader = with(Paintera.n5Factory) { + openWriterOrNull(n5container) ?: openReaderOrNull(n5container)?: return Optional.empty() + } val n5ContainerState = N5ContainerState(reader) val metadataRoot = N5Helpers.parseMetadata(reader) diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/metadata/N5ContainerState.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/metadata/N5ContainerState.kt index ad4938a06..4cde6ba18 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/metadata/N5ContainerState.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/metadata/N5ContainerState.kt @@ -1,41 +1,25 @@ package org.janelia.saalfeldlab.paintera.state.metadata -import javafx.beans.property.SimpleObjectProperty -import javafx.beans.value.ObservableValue import org.apache.commons.lang.builder.HashCodeBuilder -import org.janelia.saalfeldlab.fx.extensions.nonnullVal -import org.janelia.saalfeldlab.fx.extensions.nullableVal import org.janelia.saalfeldlab.n5.N5Reader import org.janelia.saalfeldlab.n5.N5Writer -import java.net.URI +import org.janelia.saalfeldlab.paintera.Paintera -data class N5ContainerState(private val n5Container: N5Reader) { +data class N5ContainerState(val reader: N5Reader) { - private val readerProperty: ObservableValue by lazy { SimpleObjectProperty(n5Container) } - val reader by readerProperty.nonnullVal() - - private val writerProperty: ObservableValue by lazy { SimpleObjectProperty(n5Container as? N5Writer) } - val writer by writerProperty.nullableVal() - - val uri : URI - get() = reader.uri + val writer by lazy { + (reader as? N5Writer) ?: Paintera.n5Factory.openWriterOrNull(uri.toString()) + } - val isReadOnly: Boolean - get() = writer == null + val uri by lazy { reader.uri!! } override fun equals(other: Any?): Boolean { return if (other is N5ContainerState) { - /* Equal if we are the same url, and we both either have a writer, or have no writer. */ - uri == other.uri && ((writer == null) == (other.writer == null)) + uri == other.uri } else { super.equals(other) } } - override fun hashCode(): Int { - val builder = HashCodeBuilder() - .append(reader.uri) - .append(writer?.uri ?: 0) - return builder.toHashCode() - } + override fun hashCode() = HashCodeBuilder().append(uri).toHashCode() } diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/raw/n5/N5BackendRaw.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/raw/n5/N5BackendRaw.kt index d80edc341..9f30258a3 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/raw/n5/N5BackendRaw.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/raw/n5/N5BackendRaw.kt @@ -24,7 +24,7 @@ import java.lang.reflect.Type class N5BackendRaw(@JvmField val metadataState: MetadataState) : AbstractN5BackendRaw where D : NativeType, D : RealType, T : AbstractVolatileRealType, T : NativeType { - override val container = metadataState.writer ?: metadataState.reader + override val container = metadataState.reader override val dataset = metadataState.dataset override fun createSource(queue: SharedQueue, priority: Int, name: String): DataSource { diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/ui/SettingsView.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/ui/SettingsView.kt index 5f5f72a98..deb2824fa 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/ui/SettingsView.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/ui/SettingsView.kt @@ -10,12 +10,11 @@ import org.janelia.saalfeldlab.paintera.config.* import org.janelia.saalfeldlab.paintera.paintera import org.janelia.saalfeldlab.paintera.viewer3d.OrthoSliceFX -class SettingsView private constructor(val vBox: VBox) : TitledPane("Settings", vBox) { +class SettingsView private constructor(val vBox: VBox ) : TitledPane("Settings", vBox) { constructor() : this(VBox()) - private val navigationConfigNode = - NavigationConfigNode(config = paintera.properties.navigationConfig, coordinateConfig = CoordinateConfigNode(paintera.baseView.manager())) + private val navigationConfigNode = NavigationConfigNode(config = paintera.properties.navigationConfig, coordinateConfig = CoordinateConfigNode(paintera.baseView.manager())) private val multiBoxOverlayConfigNode = MultiBoxOverlayConfigNode(config = paintera.properties.multiBoxOverlayConfig) @@ -39,6 +38,9 @@ class SettingsView private constructor(val vBox: VBox) : TitledPane("Settings", private val loggingConfigNode = LoggingConfigNode(paintera.properties.loggingConfig) + private val segmentAnythingConfigNode = SegmentAnythingConfigNode(paintera.properties.segmentAnythingConfig) + private val painteraDirectoriesConfigNode = PainteraDirectoriesConfigNode(paintera.properties.painteraDirectoriesConfig) + init { vBox.apply { children += navigationConfigNode.getContents() @@ -49,6 +51,8 @@ class SettingsView private constructor(val vBox: VBox) : TitledPane("Settings", children += scaleBarConfigNode children += bookmarkConfigNode children += arbitraryMeshConfigNode + children += segmentAnythingConfigNode + children += painteraDirectoriesConfigNode children += screenScaleConfigNode.contents children += loggingConfigNode.node } diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/ui/dialogs/create/CreateDataset.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/ui/dialogs/create/CreateDataset.kt index 20b474410..92867918b 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/ui/dialogs/create/CreateDataset.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/ui/dialogs/create/CreateDataset.kt @@ -23,6 +23,13 @@ import javafx.stage.Stage import javafx.util.Pair import net.imglib2.img.cell.AbstractCellImg import net.imglib2.realtransform.AffineTransform3D +import org.controlsfx.control.decoration.Decoration +import org.controlsfx.control.decoration.GraphicDecoration +import org.controlsfx.validation.ValidationMessage +import org.controlsfx.validation.ValidationResult +import org.controlsfx.validation.ValidationSupport +import org.controlsfx.validation.Validator +import org.controlsfx.validation.decoration.GraphicValidationDecoration import org.janelia.saalfeldlab.fx.SaalFxStyle import org.janelia.saalfeldlab.fx.ui.DirectoryField import org.janelia.saalfeldlab.fx.ui.Exceptions.Companion.exceptionAlert @@ -57,10 +64,10 @@ import kotlin.streams.toList class CreateDataset(private val currentSource: Source<*>?, vararg allSources: SourceState<*, *>) { + private val mipmapLevels = FXCollections.observableArrayList() private val mipmapLevelsNode by lazy { - createMipMapLevelsNode(mipmapLevels, FIELD_WIDTH, NAME_WIDTH, *SubmitOn.values() - ) + createMipMapLevelsNode(mipmapLevels, FIELD_WIDTH, NAME_WIDTH, *SubmitOn.values()) } private val populateFromCurrentSource = MenuItem("_From Current Source").apply { @@ -124,20 +131,55 @@ class CreateDataset(private val currentSource: Source<*>?, vararg allSources: So reduceBaseScale(mipmapLevels[0]) } else if (it.from < mipmapLevels.size) { adjustSubsequentScale(it.removed[0]!!, mipmapLevels[it.from]) - provideAbsoluteValues(mipmapLevels, resolution) + provideAbsoluteValues(mipmapLevels, resolution, dimensions) } } else if (it.wasAdded()) { - provideAbsoluteValues(mipmapLevels, resolution) + it.addedSubList.forEach { newLevel -> registerLevelDecorations(newLevel) } + provideAbsoluteValues(mipmapLevels, resolution, dimensions) } } }) currentSource?.let { populateFrom(it) } ?: let { val scale0 = MipMapLevel(1, -1, FIELD_WIDTH, NAME_WIDTH, *SubmitOn.values()) - provideAbsoluteValues(listOf(scale0), resolution) + provideAbsoluteValues(listOf(scale0), resolution, dimensions) mipmapLevels += scale0 } } + private class AlignableGraphicValidationDecoration(private val alignment: Pos = Pos.BOTTOM_RIGHT) : GraphicValidationDecoration() { + override fun createValidationDecorations(message: ValidationMessage?): MutableCollection { + return mutableListOf(GraphicDecoration(createDecorationNode(message), alignment)) + } + } + + private fun registerLevelDecorations(level: MipMapLevel) { + val x = level.dimensions.x + val y = level.dimensions.y + val z = level.dimensions.z + + val support = ValidationSupport() + (support.validationDecorator as? GraphicValidationDecoration)?.let { + support.validationDecoratorProperty().set(AlignableGraphicValidationDecoration(Pos.BOTTOM_RIGHT)) + } + + support.registerValidator(x.textField, false, Validator { _, _ -> + ValidationResult.fromErrorIf(x.textField, "Dimension size is zero. Remove this scale level.", x.value.toInt() <= 0) + ?.addWarningIf(x.textField, "Dimension is smaller than block size. Consider removing this scale level.", x.value.toInt() <= blockSize.x.value.toInt()) + }) + support.registerValidator(y.textField, false, Validator { _, _ -> + ValidationResult.fromErrorIf(y.textField, "Dimension size is zero. Remove this scale level.", y.value.toInt() <= 0) + ?.addWarningIf(y.textField, "Dimension is smaller than block size. Consider removing this scale level.", y.value.toInt() <= blockSize.y.value.toInt()) + }) + support.registerValidator(z.textField, false, Validator { _, _ -> + ValidationResult.fromErrorIf(z.textField, "Dimension size is zero. Remove this scale level.", z.value.toInt() <= 0) + ?.addWarningIf(z.textField, "Dimension is smaller than block size. Consider removing this scale level.", z.value.toInt() <= blockSize.z.value.toInt()) + }) + + blockSize.x.valueProperty().addListener { _, _, _ -> support.revalidate() } + blockSize.y.valueProperty().addListener { _, _, _ -> support.revalidate() } + blockSize.z.valueProperty().addListener { _, _, _ -> support.revalidate() } + } + private fun reduceBaseScale(newBaseScale: MipMapLevel) { val relativeFactors = newBaseScale.relativeDownsamplingFactors.asLongArray().toTypedArray() @@ -193,6 +235,12 @@ class CreateDataset(private val currentSource: Source<*>?, vararg allSources: So if (dataset.isNullOrEmpty()) throw IOException("Dataset not specified!") if (name.isNullOrEmpty()) throw IOException("Name not specified!") + /* Remove Scales where downsampling factors are 1 */ + val scaleLevels = mutableListOf() + mipmapLevels.forEach { level -> + if (level.relativeDownsamplingFactors.asDoubleArray().reduce { l, r -> l * r } != 1.0) + scaleLevels.add(level) + } N5Data.createEmptyLabelDataset( container, dataset, @@ -200,12 +248,11 @@ class CreateDataset(private val currentSource: Source<*>?, vararg allSources: So blockSize.asIntArray(), resolution.asDoubleArray(), offset.asDoubleArray(), - mipmapLevels.stream().map { it.downsamplingFactors() }.toList().toTypedArray(), - mipmapLevels.stream().mapToInt { it.maxNumEntries() }.toArray() + scaleLevels.stream().map { it.downsamplingFactors() }.toList().toTypedArray(), + scaleLevels.stream().mapToInt { it.maxNumEntries() }.toArray() ) - val path = Path.of(container).toFile().canonicalPath - val writer = n5Factory.openWriter(path) + val writer = n5Factory.openWriter(container) N5Helpers.parseMetadata(writer, true).ifPresent { _ -> val containerState = N5ContainerState(writer) createMetadataState(containerState, dataset).ifPresent { metadataStateProp.set(it) } @@ -229,7 +276,6 @@ class CreateDataset(private val currentSource: Source<*>?, vararg allSources: So private fun populateFrom(source: Source<*>?) { source?.let { - setMipMapLevels(source) val mdSource = source as? N5DataSource<*, *> ?: (source as? MaskedSource<*, *>)?.let { it.underlyingSource() as? N5DataSource<*, *> } @@ -257,6 +303,7 @@ class CreateDataset(private val currentSource: Source<*>?, vararg allSources: So offset.x.value = transform[0, 3] offset.y.value = transform[1, 3] offset.z.value = transform[2, 3] + setMipMapLevels(it) } } @@ -279,7 +326,7 @@ class CreateDataset(private val currentSource: Source<*>?, vararg allSources: So previousFactors = downsamplingFactors levels.add(level) } - provideAbsoluteValues(levels, resolution) + provideAbsoluteValues(levels, resolution, dimensions) mipmapLevels.clear() mipmapLevels += levels } @@ -303,7 +350,7 @@ class CreateDataset(private val currentSource: Source<*>?, vararg allSources: So MipMapLevel(2, -1, 60.0, 60.0), MipMapLevel(2, -1, 60.0, 60.0), ) - provideAbsoluteValues(levels, SpatialField.doubleField(4.0, { true })) + provideAbsoluteValues(levels, SpatialField.doubleField(4.0, { true }), SpatialField.longField(100, { true })) Platform.runLater { val scene = Scene(createMipMapLevelsNode(obsLevels, 60.0, 60.0)) obsLevels += levels @@ -314,13 +361,13 @@ class CreateDataset(private val currentSource: Source<*>?, vararg allSources: So } } - private fun provideAbsoluteValues(mipmapLevels: List, resolution: SpatialField) { + private fun provideAbsoluteValues(mipmapLevels: List, resolution: SpatialField, baseDimensions: SpatialField) { val baseAbsoluteFactors = SpatialField.intField(1, { true }, FIELD_WIDTH, *SubmitOn.values()).also { it.editable = false }.also { it.x.valueProperty().bind(mipmapLevels[0].relativeDownsamplingFactors.x.valueProperty()) it.y.valueProperty().bind(mipmapLevels[0].relativeDownsamplingFactors.y.valueProperty()) it.z.valueProperty().bind(mipmapLevels[0].relativeDownsamplingFactors.z.valueProperty()) } - mipmapLevels[0].displayAbsoluteValues(resolution, baseAbsoluteFactors) + mipmapLevels[0].displayAbsoluteValues(resolution, baseAbsoluteFactors, baseDimensions) var previousAbsoluteFactors = baseAbsoluteFactors if (mipmapLevels.size > 1) { @@ -329,7 +376,7 @@ class CreateDataset(private val currentSource: Source<*>?, vararg allSources: So absoluteFactors.x.valueProperty().bind(previousAbsoluteFactors.x.valueProperty().multiply(level.relativeDownsamplingFactors.x.valueProperty())) absoluteFactors.y.valueProperty().bind(previousAbsoluteFactors.y.valueProperty().multiply(level.relativeDownsamplingFactors.y.valueProperty())) absoluteFactors.z.valueProperty().bind(previousAbsoluteFactors.z.valueProperty().multiply(level.relativeDownsamplingFactors.z.valueProperty())) - level.displayAbsoluteValues(resolution, absoluteFactors) + level.displayAbsoluteValues(resolution, absoluteFactors, baseDimensions) previousAbsoluteFactors = absoluteFactors } } @@ -347,9 +394,10 @@ class CreateDataset(private val currentSource: Source<*>?, vararg allSources: So } addButton.onAction = EventHandler { event -> event.consume() - val newLevel = MipMapLevel(2, -1, fieldWidth, nameWidth, *submitOn) - levels.last()?.let { it.resolution to it.absoluteDownsamplingFactors }?.let { (res, prevAbs) -> - if (res != null && prevAbs != null) newLevel.displayAbsoluteValues(res, prevAbs) + val newLevel = MipMapLevel(2, -1, fieldWidth, nameWidth, *submitOn).apply { + levels.last()?.also { prevLevel -> + displayAbsoluteValues(prevLevel.resolution, prevLevel.absoluteDownsamplingFactors, prevLevel.baseDimensions) + } } levels.add(newLevel) } diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/util/logging/LogUtils.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/util/logging/LogUtils.kt index a5c8732d7..1630ace8c 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/util/logging/LogUtils.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/util/logging/LogUtils.kt @@ -3,135 +3,132 @@ package org.janelia.saalfeldlab.paintera.util.logging import ch.qos.logback.classic.Level import ch.qos.logback.classic.LoggerContext import ch.qos.logback.classic.util.ContextInitializer +import dev.dirs.ProjectDirectories import org.slf4j.Logger import org.slf4j.LoggerFactory import picocli.CommandLine import java.lang.invoke.MethodHandles -import java.lang.management.ManagementFactory +import java.nio.file.Path import java.text.SimpleDateFormat -import java.util.Date +import java.util.* import ch.qos.logback.classic.Logger as LogbackLogger -class LogUtils { - - companion object { - - // set property "paintera.startup.date" to current time - @JvmStatic - val currentTime = Date() - - @JvmStatic - val currentTimeString = SimpleDateFormat("yyyy-MM-dd_HH-mm-ss").format(currentTime).also { System.setProperty("paintera.startup.date", it) } - - @JvmStatic - val processId = ManagementFactory.getRuntimeMXBean().getName()?.split("@")?.firstOrNull()?.also { System.setProperty("paintera.process.id", it) } - - // set the file name base into which to log if logging to file is enabled - // - // if a process id (PID) is available: - // paintera.yyyy-MM-dd_HH-mm-ss_${PID} - // else: - // paintera.yyyy-MM-dd_HH-mm-ss - // - // logback.xml is configured to store logs in - // $HOME/.paintera/logs/${BASENAME}.log - @JvmStatic - val painteraLogFilenameBase = if (processId == null) { - currentTimeString - } else { - "${currentTimeString}_$processId" - }.also { - System.setProperty("paintera.log.filename.base", "paintera.$it") - resetLoggingConfig() - } +object LogUtils { + private const val PAINTERA_STARTUP_DATE_PROPERTY = "paintera.startup.date" + private const val PAINTERA_PID_PROPERTY = "paintera.pid" + private const val PAINTERA_LOG_DIR_PROPERTY = "paintera.log.dir" + private const val ROOT_LOGGER_LEVEL_PROPERTY = "paintera.log.root.logger.level" + private const val PAINTERA_LOG_FILENAME_BASE_PROPERTY = "paintera.log.filename.base" + private const val PAINTERA_LOGGING_ENABLED_PROPERTY = "paintera.log.enabled" + private const val PAINTERA_LOGGING_TO_CONSOLE_ENABLED_PROPERTY = "paintera.log.console.enabled" + private const val PAINTERA_LOGGING_TO_FILE_ENABLED_PROPERTY = "paintera.log.file.enabled" - @JvmStatic - val rootLogger = LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME) + private val logContextProperties: MutableMap = mutableMapOf() - private val rootLoggerLogback = rootLogger as? LogbackLogger + @JvmStatic + val currentTime = Date() - private const val painteraLogDirProperty = "paintera.log.dir" + private val context + get() = LoggerFactory.getILoggerFactory() as LoggerContext - private const val rootLoggerLevelProperty = "paintera.log.root.logger.level" + init { + resetLoggingConfig() + } - private const val painteraLoggingEnabledProperty = "paintera.log.enabled" - private const val painteraLoggingToConsoleEnabledProperty = "paintera.log.console.enabled" - private const val painteraLoggingToFileEnabledProperty = "paintera.log.file.enabled" + @JvmStatic + val painteraLogDir: String + get() = (context).getProperty(PAINTERA_LOG_DIR_PROPERTY) - @JvmStatic - var rootLoggerLevel: Level? - @Synchronized set(level) { - if (level === null) - System.clearProperty(rootLoggerLevelProperty) - else { - setLogLevelFor(rootLogger, level) - System.setProperty(rootLoggerLevelProperty, level.levelStr) - } - } - @Synchronized get() = rootLoggerLogback?.level + @JvmStatic + val painteraLogFilenameBase: String + get() = (context).getProperty(PAINTERA_LOG_FILENAME_BASE_PROPERTY) - private fun setPropertyAndResetLoggingConfig(property: String, value: String?) { - if (value === null) System.clearProperty(property) else System.setProperty(property, value) - resetLoggingConfig() - } + @JvmStatic + val painteraLogFilePath: String + get() = Path.of(painteraLogDir, "$painteraLogFilenameBase.log").toAbsolutePath().toString() - private fun setBooleanProperty(identifier: String, enabled: Boolean) = setPropertyAndResetLoggingConfig(identifier, "$enabled") + @JvmStatic + val rootLogger = LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME) + private val rootLoggerLogback = rootLogger as? LogbackLogger - @JvmStatic - fun setLoggingEnabled(enabled: Boolean) = setBooleanProperty(painteraLoggingEnabledProperty, enabled) + @JvmStatic + var rootLoggerLevel: Level? + @Synchronized set(level) { + logContextProperties[ROOT_LOGGER_LEVEL_PROPERTY] = level?.also { setLogLevelFor(rootLogger, level) }?.levelStr + } + @Synchronized get() = rootLoggerLogback?.level - @JvmStatic - fun setLoggingToConsoleEnabled(enabled: Boolean) = setBooleanProperty(painteraLoggingToConsoleEnabledProperty, enabled) + private fun setPropertyAndResetLoggingConfig(property: String, value: String?) { + value?.let { logContextProperties[property] = value } ?: logContextProperties.remove(property) + resetLoggingConfig() + } - @JvmStatic - fun setLoggingToFileEnabled(enabled: Boolean) = setBooleanProperty(painteraLoggingToFileEnabledProperty, enabled) + private fun setBooleanProperty(identifier: String, enabled: Boolean) = setPropertyAndResetLoggingConfig(identifier, "$enabled") - @JvmStatic - fun resetLoggingConfig() { - val lc = LoggerFactory.getILoggerFactory() as LoggerContext - val levels = lc.loggerList.associate { Pair(it.name, it.level) } - val ci = ContextInitializer(lc) - lc.reset() - ci.autoConfig() - levels.forEach { (name, level) -> lc.getLogger(name).level = level } - } + @JvmStatic + fun setLoggingEnabled(enabled: Boolean) = setBooleanProperty(PAINTERA_LOGGING_ENABLED_PROPERTY, enabled) - @JvmStatic - fun setLogLevelFor(name: String, level: Level) = setLogLevelFor(LoggerFactory.getLogger(name), level) + @JvmStatic + fun setLoggingToConsoleEnabled(enabled: Boolean) = setBooleanProperty(PAINTERA_LOGGING_TO_CONSOLE_ENABLED_PROPERTY, enabled) - @JvmStatic - fun setLogLevelFor(logger: Logger, level: Level) = if (logger is LogbackLogger) logger.level = level else Unit + @JvmStatic + fun setLoggingToFileEnabled(enabled: Boolean) = setBooleanProperty(PAINTERA_LOGGING_TO_FILE_ENABLED_PROPERTY, enabled) - fun String.isRootLoggerName() = ch.qos.logback.classic.Logger.ROOT_LOGGER_NAME == this + @JvmStatic + fun resetLoggingConfig(clearProperties: Boolean = false) { + if (clearProperties) + logContextProperties.clear() + context.reset() + setLoggingProperties(context) + ContextInitializer(context).autoConfig() + val levels = context.loggerList.associate { Pair(it.name, it.level) } + levels.forEach { (name, level) -> context.getLogger(name).level = level } } - class Logback { - class Loggers { - companion object { - operator fun get(name: String) = LoggerFactory.getLogger(name) as? ch.qos.logback.classic.Logger + @JvmStatic + fun setLoggingProperties(context: LoggerContext) { + context.apply { + logContextProperties.computeIfAbsent(PAINTERA_LOG_DIR_PROPERTY) { + Path.of(ProjectDirectories.from("org", "janelia", "Paintera").dataLocalDir, "logs").toAbsolutePath().toString() } + + logContextProperties.computeIfAbsent(PAINTERA_STARTUP_DATE_PROPERTY) { SimpleDateFormat("yyyy-MM-dd").format(currentTime) } + logContextProperties.computeIfAbsent(PAINTERA_PID_PROPERTY) { "${ProcessHandle.current().pid()}" } + logContextProperties.computeIfAbsent(PAINTERA_LOG_FILENAME_BASE_PROPERTY) { "paintera" } + logContextProperties.forEach { (key, value) -> value?.let { context.putProperty(key, it) } } } + } + + @JvmStatic + fun setLogLevelFor(name: String, level: Level) = setLogLevelFor(LoggerFactory.getLogger(name), level) + + @JvmStatic + fun setLogLevelFor(logger: Logger, level: Level) = if (logger is LogbackLogger) logger.level = level else Unit - class Levels { - companion object { + fun String.isRootLoggerName() = ch.qos.logback.classic.Logger.ROOT_LOGGER_NAME == this - @JvmStatic - val levels = arrayOf( - Level.ALL, - Level.TRACE, - Level.DEBUG, - Level.INFO, - Level.WARN, - Level.ERROR, - Level.OFF - ).sortedBy { it.levelInt }.asReversed() + class Logback { + object Loggers { + operator fun get(name: String) = LoggerFactory.getLogger(name) as? ch.qos.logback.classic.Logger + } - operator fun get(level: String): Level? = levels.firstOrNull { level.equals(it.levelStr, ignoreCase = true) } + object Levels { - operator fun contains(level: String): Boolean = get(level) != null + @JvmStatic + val levels = arrayOf( + Level.ALL, + Level.TRACE, + Level.DEBUG, + Level.INFO, + Level.WARN, + Level.ERROR, + Level.OFF + ).sortedBy { it.levelInt }.asReversed() - } + operator fun get(level: String): Level? = levels.firstOrNull { level.equals(it.levelStr, ignoreCase = true) } + + operator fun contains(level: String): Boolean = get(level) != null class CmdLineConverter : CommandLine.ITypeConverter { override fun convert(value: String): Level? { diff --git a/src/main/kotlin/org/janelia/saalfeldlab/util/Imglib2Extensions.kt b/src/main/kotlin/org/janelia/saalfeldlab/util/Imglib2Extensions.kt index 31727c94e..95aa4bd30 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/util/Imglib2Extensions.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/util/Imglib2Extensions.kt @@ -13,10 +13,12 @@ import net.imglib2.type.numeric.IntegerType import net.imglib2.type.numeric.NumericType import net.imglib2.type.numeric.RealType import net.imglib2.util.Intervals +import net.imglib2.view.ExtendedRandomAccessibleInterval import net.imglib2.view.IntervalView import net.imglib2.view.RandomAccessibleOnRealRandomAccessible import net.imglib2.view.Views import org.janelia.saalfeldlab.paintera.util.IntervalHelpers.Companion.smallestContainingInterval +import tmp.net.imglib2.outofbounds.OutOfBoundsConstantValueFactory import kotlin.math.floor import kotlin.math.roundToLong @@ -47,7 +49,6 @@ fun , F : RandomAccessibleInterval> F.extendValue(extension: fun , F : RandomAccessibleInterval> F.extendValue(extension: Int) = Views.extendValue(this, extension)!! fun , F : RandomAccessibleInterval> F.extendValue(extension: Long) = Views.extendValue(this, extension)!! fun , F : RandomAccessibleInterval> F.extendValue(extension: Boolean) = Views.extendValue(this, extension)!! -fun , F : RandomAccessibleInterval> F.extendZero() = Views.extendZero(this)!! fun > F.expandborder(vararg border: Long) = Views.expandBorder(this, *border)!! fun RandomAccessible.hyperSlice(dimension: Int = this.numDimensions() - 1, position: Long = 0) = Views.hyperSlice(this, dimension, position)!! @@ -62,10 +63,18 @@ fun > RandomAccessible.convert(type: R, converter: (T, R) -> U return Converters.convert(this, converter, type) } +fun > RandomAccessibleInterval.convert(type: R, converter: (T, R) -> Unit): RandomAccessibleInterval { + return Converters.convert(this, converter, type) +} + fun > RandomAccessible.convertWith(other: RandomAccessible, type: C, converter: (A, B, C) -> Unit): RandomAccessible { return Converters.convert(this, other, converter, type) } +fun > RandomAccessibleInterval.convertWith(other: RandomAccessibleInterval, type: C, converter: (A, B, C) -> Unit): RandomAccessibleInterval { + return Converters.convert(this, other, converter, type) +} + fun > RealRandomAccessible.convert(type: R, converter: (T, R) -> Unit): RealRandomAccessible { return ConvertedRealRandomAccessible(this, converter) { type.copy() } } diff --git a/src/main/kotlin/org/janelia/saalfeldlab/util/n5/N5Helpers.kt b/src/main/kotlin/org/janelia/saalfeldlab/util/n5/N5Helpers.kt index 05b1acb64..e9918d403 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/util/n5/N5Helpers.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/util/n5/N5Helpers.kt @@ -5,15 +5,23 @@ import com.google.gson.JsonElement import com.google.gson.JsonObject import javafx.beans.property.BooleanProperty import javafx.beans.value.ChangeListener +import javafx.event.EventHandler +import javafx.scene.control.* +import javafx.scene.layout.HBox +import javafx.scene.layout.Priority +import javafx.scene.layout.VBox +import javafx.stage.DirectoryChooser import net.imglib2.img.cell.CellGrid import net.imglib2.realtransform.AffineTransform3D import net.imglib2.realtransform.ScaleAndTranslation import net.imglib2.realtransform.Translation3D +import org.janelia.saalfeldlab.fx.util.InvokeOnJavaFXApplicationThread import org.janelia.saalfeldlab.labels.blocks.LabelBlockLookup import org.janelia.saalfeldlab.labels.blocks.LabelBlockLookupAdapter import org.janelia.saalfeldlab.labels.blocks.n5.LabelBlockLookupFromN5Relative import org.janelia.saalfeldlab.n5.DatasetAttributes import org.janelia.saalfeldlab.n5.N5Reader +import org.janelia.saalfeldlab.n5.N5URI import org.janelia.saalfeldlab.n5.N5Writer import org.janelia.saalfeldlab.n5.hdf5.N5HDF5Reader import org.janelia.saalfeldlab.n5.universe.N5DatasetDiscoverer @@ -32,11 +40,13 @@ import org.janelia.saalfeldlab.paintera.state.metadata.MetadataState import org.janelia.saalfeldlab.paintera.state.metadata.MetadataUtils.Companion.metadataIsValid import org.janelia.saalfeldlab.paintera.state.metadata.MultiScaleMetadataState import org.janelia.saalfeldlab.paintera.state.raw.n5.SerializationKeys +import org.janelia.saalfeldlab.paintera.ui.PainteraAlerts import org.janelia.saalfeldlab.paintera.util.n5.metadata.LabelBlockLookupGroup import org.janelia.saalfeldlab.util.NamedThreadFactory import org.janelia.saalfeldlab.util.n5.metadata.N5PainteraDataMultiScaleMetadata.PainteraDataMultiScaleParser import org.janelia.saalfeldlab.util.n5.metadata.N5PainteraLabelMultiScaleGroup.PainteraLabelMultiScaleParser import org.janelia.saalfeldlab.util.n5.metadata.N5PainteraRawMultiScaleGroup.PainteraRawMultiScaleParser +import org.janelia.saalfeldlab.util.n5.universe.N5ContainerDoesntExist import org.slf4j.LoggerFactory import java.io.IOException import java.lang.invoke.MethodHandles @@ -45,6 +55,7 @@ import java.util.* import java.util.List import java.util.concurrent.ExecutorService import java.util.concurrent.Executors +import java.util.concurrent.atomic.AtomicReference import java.util.function.* import kotlin.collections.ArrayList import kotlin.collections.HashMap @@ -55,6 +66,7 @@ import kotlin.collections.filter import kotlin.collections.indices import kotlin.collections.isNotEmpty import kotlin.collections.map +import kotlin.collections.plusAssign import kotlin.collections.set import kotlin.collections.sortBy import kotlin.collections.toDoubleArray @@ -318,7 +330,8 @@ object N5Helpers { FragmentSegmentAssignmentOnlyLocal.NO_INITIAL_LUT_AVAILABLE, FragmentSegmentAssignmentOnlyLocal.doesNotPersist(persistError)) } - val dataset = "$group/$PAINTERA_FRAGMENT_SEGMENT_ASSIGNMENT_DATASET " + + val dataset = "$group/$PAINTERA_FRAGMENT_SEGMENT_ASSIGNMENT_DATASET" val initialLut = if (writer.exists(dataset)) { N5FragmentSegmentAssignmentInitialLut(writer, dataset) } else NoInitialLutAvailable() @@ -709,8 +722,9 @@ object N5Helpers { ?.takeIf { it.isJsonObject } ?.let { gson.fromJson(it, LabelBlockLookup::class.java) as LabelBlockLookup } ?: let { - val labelToBlockDataset = Paths.get(group, "label-to-block-mapping").toString() - val relativeLookup = LabelBlockLookupFromN5Relative("label-to-block-mapping/s%d") + val labelToBlockDataset = N5URI.normalizeGroupPath(group + reader.groupSeparator + "label-to-block-mapping"); + val scaleDatasetPattern = N5URI.normalizeGroupPath("label-to-block-mapping" + reader.groupSeparator + "s%d") + val relativeLookup = LabelBlockLookupFromN5Relative(scaleDatasetPattern) val numScales = if (metadataState is MultiScaleMetadataState) metadataState.scaleTransforms.size else 1 val labelBlockLookupMetadata = LabelBlockLookupGroup(labelToBlockDataset, numScales) labelBlockLookupMetadata.write(metadataState.writer!!) @@ -771,74 +785,6 @@ object N5Helpers { class MaxIDNotSpecified(message: String) : PainteraException(message) class NotAPainteraDataset(val container: N5Reader, val group: String) : PainteraException(String.format("Group %s in container %s is not a Paintera dataset.", group, container)) - /** - * If an n5 container exists at [uri], return it as [N5Writer] if possible, and [N5Reader] if not. - * - * @param uri the location of the n5 container - * @return [N5Writer] or [N5Reader] if container exists - */ - @JvmStatic - fun getReaderOrWriterIfN5ContainerExists(uri: String): N5Reader? { - val cachedContainer = getReaderOrWriterIfCached(uri) - return cachedContainer ?: openReaderOrWriterIfContainerExists(uri) - } - - /** - * If an n5 container exists at [uri], return it as [N5Writer] if possible. - * - * @param uri the location of the n5 container - * @return [N5Writer] if container exists and is openable as a writer. - */ - @JvmStatic - fun getWriterIfN5ContainerExists(uri: String): N5Writer? { - return getReaderOrWriterIfN5ContainerExists(uri) as? N5Writer - } - - /** - * Retrieves a reader or writer for the given container if it exists. - * - * If the reader is successfully opened, the container must exist, so we try to open a writer. - * This is to ensure that an N5 container isn't created by opening as a writer if one doesn't exist. - * If opening a writer is not possible it falls back to again as a reader. - * - * @param container the path to the container - * @return a reader or writer as N5Reader, or null if the container does not exist - */ - private fun openReaderOrWriterIfContainerExists(container: String) = try { - n5Factory.openReader(container)?.let { - var reader: N5Reader? = it - if (it is N5HDF5Reader) { - it.close() - reader = null - } - try { - n5Factory.openWriter(container) - } catch (_: Exception) { - reader ?: n5Factory.openReader(container) - } - } - } catch (e: Exception) { - null - } - - /** - * If there is a cached N5Reader for [container] than try to open a writer (also may be cached), - * or fallback to the cached reader - * - * @param container The path to the N5 container. - * @return The N5Reader instance. - */ - private fun getReaderOrWriterIfCached(container: String): N5Reader? { - val reader: N5Reader? = n5Factory.getFromCache(container)?.let { - try { - n5Factory.openWriter(container) - } catch (_: Exception) { - it - } - } - return reader - } - private const val URI = "uri" internal fun N5Reader.serializeTo(json: JsonObject) { if (!uri.equals(paintera.projectDirectory.actualDirectory.toURI())) { @@ -850,9 +796,82 @@ object N5Helpers { /* `fromClassInfo is the old style. Support both when deserializing, at least for now. Should be replaced on next save */ val fromClassInfo = json[SerializationKeys.CONTAINER] ?.asJsonObject?.get("data") - ?.asJsonObject?.get("basePath") + ?.asJsonObject?.let { it.get("basePath") ?: it.get("file") } ?.asString val uri = fromClassInfo ?: json[URI]?.asString ?: paintera.projectDirectory.actualDirectory.toURI().toString() - return N5Helpers.getReaderOrWriterIfN5ContainerExists(uri)!! + return getN5ContainerWithRetryPrompt(uri) + } + + internal fun getN5ContainerWithRetryPrompt(uri: String): N5Reader { + return try { + n5Factory.openWriterElseOpenReader(uri) + } catch (e: N5ContainerDoesntExist) { + promptForNewLocationOrRemove(uri, e, "Container Not Found", + """ + N5 container does not exist at + $uri + + If the container has moved, specify it's new location. + If the container no longer exists, you can attempt to remove this source. + """.trimIndent() + ) + } + } + + internal fun promptForNewLocationOrRemove(uri: String, cause: Throwable, header: String? = null, contentText: String? = null): N5Reader { + var exception: () -> Throwable = { cause } + val n5Container = AtomicReference() + InvokeOnJavaFXApplicationThread.invokeAndWait { + PainteraAlerts.confirmation("Accept", "Quit", true).also { alert -> + alert.headerText = header ?: "Error Opening N5 Container" + alert.buttonTypes.add(ButtonType.FINISH) + (alert.dialogPane.lookupButton(ButtonType.FINISH) as Button).apply { + text = "Remove Source" + onAction = EventHandler { + it.consume() + alert.close() + exception = { RemoveSourceException(uri) } + } + } + + val newLocationField = TextField() + (alert.dialogPane.lookupButton(ButtonType.OK) as Button).apply { + disableProperty().bind(newLocationField.textProperty().isEmpty) + onAction = EventHandler { + it.consume() + alert.close() + n5Container.set(getN5ContainerWithRetryPrompt(newLocationField.textProperty().get())) + } + } + + + alert.dialogPane.content = VBox().apply { + children += HBox().apply { + children += TextArea( contentText ?: "Error accessing container at $uri" ).also { it.editableProperty().set(false) } + } + children += HBox().apply { + children += Label("New Location ").also { HBox.setHgrow(it, Priority.NEVER) } + children += newLocationField + newLocationField.maxWidth = Double.MAX_VALUE + HBox.setHgrow(newLocationField, Priority.ALWAYS) + children += Button("Browse").also { + HBox.setHgrow(it, Priority.NEVER) + it.onAction = EventHandler { + DirectoryChooser().showDialog(alert.owner)?.let { newLocationField.textProperty().set(it.canonicalPath) } + } + } + } + } + alert.showAndWait() + } + } + return n5Container.get() ?: throw exception() + } + + class RemoveSourceException : PainteraException { + + constructor(location: String) : super("Source expected at:\n$location\nshould be removed") + constructor(location: String, cause: Throwable) : super("Source expected at:\n$location\nshould be removed", cause) + } } diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index 995400699..d1ac20a7f 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -11,22 +11,33 @@ - - ${user.home}/.paintera/logs/${paintera.log.filename.base:-paintera}.log - - - ${user.home}/.paintera/logs/${paintera.log.filename.base:-paintera}.%d{yyyy-MM-dd}.log - - 60 - 30GB - - - - UTF-8 - %d %-4relative [%thread] %-5level %logger - %msg%n - - + + + + + + + + ${paintera.log.dir}/${paintera.log.filename.base}.log + + + ${paintera.log.dir}/${paintera.log.filename.base}_%d{yyyy-MM-dd}.log + + 60 + 10GB + + + UTF-8 + %d %-7relative ${paintera.pid} [%thread] %-5level %logger - %msg%n + + + + + + + + @@ -40,7 +51,6 @@ - diff --git a/src/packaging/linux/control b/src/packaging/linux/control new file mode 100644 index 000000000..13d1b0862 --- /dev/null +++ b/src/packaging/linux/control @@ -0,0 +1,10 @@ +Package: paintera +Version: 1.0 +Section: misc +Maintainer: Caleb Hulbert +Priority: optional +Architecture: amd64 +Provides: paintera +Description: Paintera +Depends: libbz2-1.0, libc6, libcap2, libcom-err2, libdbus-1-3, libexpat1, libgcc-s1, libgpg-error0, libkeyutils1, liblzma5, libpcre3, libselinux1, xdg-utils, zlib1g, libblosc1 +Installed-Size: 411870 \ No newline at end of file diff --git a/src/packaging/linux-jpackage.txt b/src/packaging/linux/jpackage.txt similarity index 96% rename from src/packaging/linux-jpackage.txt rename to src/packaging/linux/jpackage.txt index b1d8b4e9f..5b0ca60e6 100644 --- a/src/packaging/linux-jpackage.txt +++ b/src/packaging/linux/jpackage.txt @@ -1,3 +1,4 @@ +--verbose --name ${app.name} --icon "${project.basedir}/img/icons/icon-draft.png" --dest "${project.build.directory}" @@ -5,4 +6,5 @@ --input "${project.build.directory}/dependency" --runtime-image "${project.build.directory}/jvm-image" --temp "${project.build.directory}/installer-work" +--resource-dir "${project.build.directory}/packaging/linux" --java-options "-XX:MaxRAMPercentage=75 --add-opens=javafx.base/javafx.util=ALL-UNNAMED --add-opens=javafx.base/javafx.event=ALL-UNNAMED --add-opens=javafx.base/javafx.beans.property=ALL-UNNAMED --add-opens=javafx.base/com.sun.javafx.binding=ALL-UNNAMED --add-opens=javafx.base/com.sun.javafx.event=ALL-UNNAMED --add-opens=javafx.graphics/javafx.scene=ALL-UNNAMED --add-opens=javafx.graphics/javafx.stage=ALL-UNNAMED --add-opens=javafx.graphics/javafx.geometry=ALL-UNNAMED --add-opens=javafx.graphics/javafx.animation=ALL-UNNAMED --add-opens=javafx.graphics/javafx.scene.input=ALL-UNNAMED --add-opens=javafx.graphics/javafx.scene.image=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.prism=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.application=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.geom=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.image=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.scene=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.stage=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.perf=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.cursor=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.tk=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.scene.traversal=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.geom.transform=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.scenario.animation=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.scenario.animation.shared=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.scenario.effect=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.sg.prism=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.prism.paint=ALL-UNNAMED" diff --git a/src/packaging/osx-jpackage.txt b/src/packaging/osx/jpackage.txt similarity index 100% rename from src/packaging/osx-jpackage.txt rename to src/packaging/osx/jpackage.txt diff --git a/src/packaging/windows-jpackage.txt b/src/packaging/windows/jpackage.txt similarity index 100% rename from src/packaging/windows-jpackage.txt rename to src/packaging/windows/jpackage.txt diff --git a/src/test/java/org/janelia/saalfeldlab/paintera/PainteraBaseViewTest.java b/src/test/java/org/janelia/saalfeldlab/paintera/PainteraBaseViewTest.java index ceb4bbfc7..d100433e8 100644 --- a/src/test/java/org/janelia/saalfeldlab/paintera/PainteraBaseViewTest.java +++ b/src/test/java/org/janelia/saalfeldlab/paintera/PainteraBaseViewTest.java @@ -19,11 +19,9 @@ import net.imglib2.view.IntervalView; import net.imglib2.view.Views; import org.janelia.saalfeldlab.fx.util.InvokeOnJavaFXApplicationThread; -import org.janelia.saalfeldlab.paintera.meshes.MeshSettings; import org.janelia.saalfeldlab.util.grids.LabelBlockLookupAllBlocks; import org.janelia.saalfeldlab.util.grids.LabelBlockLookupNoBlocks; import org.jetbrains.annotations.NotNull; -import org.junit.*; import org.testfx.api.FxRobot; import org.testfx.api.FxToolkit; import org.testfx.framework.junit.ApplicationTest; @@ -31,14 +29,14 @@ import java.util.Arrays; import java.util.Random; import java.util.concurrent.TimeoutException; -import java.util.function.BiConsumer; import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Supplier; + public class PainteraBaseViewTest extends FxRobot { - @BeforeClass + // @BeforeClass public static void setup() throws Exception { System.setProperty("headless.geometry", "1600x1200-32"); @@ -48,7 +46,7 @@ public static void setup() throws Exception { ApplicationTest.launch(Paintera.class, "--log-level=ERROR"); } - @AfterClass + // @AfterClass public static void cleanup() throws InterruptedException, TimeoutException { InvokeOnJavaFXApplicationThread.invokeAndWait(() -> { Paintera.getPaintera().getBaseView().stop(); @@ -57,8 +55,9 @@ public static void cleanup() throws InterruptedException, TimeoutException { FxToolkit.cleanupApplication(Paintera.getApplication()); } - @Test - public void testAddSingleScaleLabelSource() throws Exception { + + // @Test + public void testAddSingleScaleLabelSource() { final RandomAccessibleInterval labels = ArrayImgs.unsignedLongs(10, 15, 20); final PainteraBaseView viewer = Paintera.getPaintera().getBaseView(); viewer.addConnectomicsLabelSource( @@ -69,7 +68,7 @@ public void testAddSingleScaleLabelSource() throws Exception { "singleScaleLabelSource", new LabelBlockLookupNoBlocks()); } - @Test + // @Test public void testAddSingleScaleConnectomicsRawSource() { final Random random = new Random(); final RandomAccessibleInterval rawData = @@ -90,8 +89,8 @@ public void testAddSingleScaleConnectomicsRawSource() { ); } - @Test - public void testAddMultiScaleConnectomicsRawSource() throws Exception { + // @Test + public void testAddMultiScaleConnectomicsRawSource() { var random = new Random(); final double[] center2D = new double[]{500, 500}; final var multiscale = generateMultiscaleLabels( @@ -135,8 +134,8 @@ private static void fillCylinderChunk(double[] center2D, Double scale, LoopBuild }); } - @Test - public void testAddMultiScaleConnectomicsLabelSource() throws Exception { + // @Test + public void testAddMultiScaleConnectomicsLabelSource() { final Random random = new Random(); final var multiscale = generateMultiscaleLabels(4, @@ -271,8 +270,9 @@ public GeneratedMultiscaleImage(RandomAccessibleInterval[] images, double[][] } } - @NotNull private static GeneratedMultiscaleImage generateMultiscaleLabels(int numScales, FinalInterval interval, int[] blockSize, - double[] center, BiFunction>>, ?> fillLabelByChunk) { + @NotNull + private static GeneratedMultiscaleImage generateMultiscaleLabels(int numScales, FinalInterval interval, int[] blockSize, + double[] center, BiFunction>>, ?> fillLabelByChunk) { final CachedCellImg[] multiScaleImages = new CachedCellImg[numScales]; for (int i = 0; i < multiScaleImages.length; i++) { diff --git a/src/test/java/org/janelia/saalfeldlab/paintera/data/n5/CommitCanvasN5Test.java b/src/test/java/org/janelia/saalfeldlab/paintera/data/n5/CommitCanvasN5Test.java index 536d1fcbe..05e73c8a7 100644 --- a/src/test/java/org/janelia/saalfeldlab/paintera/data/n5/CommitCanvasN5Test.java +++ b/src/test/java/org/janelia/saalfeldlab/paintera/data/n5/CommitCanvasN5Test.java @@ -352,10 +352,11 @@ private static void testPainteraData( testCanvasPersistance(container, dataset, s0, canvas, openLabels, asserts); // test highest level block lookups - final String uniqueBlock0 = String.join("/", dataset, "unique-labels", "s0"); + final String groupSeparator = container.getReader().getGroupSeparator(); + final String uniqueBlock0Group = N5URI.normalizeGroupPath(String.join(groupSeparator, dataset, "unique-labels","s0")); final Path mappingPattern = Paths.get(container.getUri().getPath(), dataset, "label-to-block-mapping", "s%d", "%d"); final Path mapping0 = Paths.get(container.getUri().getPath(), dataset, "label-to-block-mapping", "s0"); - final DatasetAttributes uniqueBlockAttributes = writer.getDatasetAttributes(uniqueBlock0); + final DatasetAttributes uniqueBlockAttributes = writer.getDatasetAttributes(uniqueBlock0Group); final List blocks = Grids.collectAllContainedIntervals(dims, blockSize); final TLongObjectMap labelToBLockMapping = new TLongObjectHashMap<>(); for (final Interval block : blocks) { @@ -375,7 +376,7 @@ private static void testPainteraData( } }); - final DataBlock uniqueBlock = writer.readBlock(uniqueBlock0, uniqueBlockAttributes, blockPos); + final DataBlock uniqueBlock = writer.readBlock(uniqueBlock0Group, uniqueBlockAttributes, blockPos); Assert.assertEquals(labels, new TLongHashSet((long[])uniqueBlock.getData())); } @@ -603,10 +604,6 @@ public DummyMetadataState(String labelsDataset, N5Writer container) { } - @Override public void setWriter(@Nullable N5Writer writer) { - - } - @Override public void setGroup(String group) { }