Skip to content

Commit

Permalink
Merge pull request #553 from saalfeldlab/feat/1.7.0
Browse files Browse the repository at this point in the history
Feat/1.7.0
  • Loading branch information
cmhulbert authored Dec 6, 2024
2 parents cf63e7c + 0cffa24 commit c927f8b
Show file tree
Hide file tree
Showing 11 changed files with 215 additions and 114 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

public enum LabelActionType implements ActionType {
Toggle(true),
Append,
Append(true ),
CreateNew,
Lock(true),
Merge,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import javafx.util.Duration;
import javafx.util.Pair;
import mpicbg.spim.data.sequence.VoxelDimensions;
import net.imglib2.AbstractInterval;
import net.imglib2.FinalInterval;
import net.imglib2.FinalRealInterval;
import net.imglib2.Interval;
Expand All @@ -66,6 +67,7 @@
import net.imglib2.img.cell.AbstractCellImg;
import net.imglib2.img.cell.CellGrid;
import net.imglib2.interpolation.randomaccess.NearestNeighborInterpolatorFactory;
import net.imglib2.iterator.LocalizingIntervalIterator;
import net.imglib2.loops.LoopBuilder;
import net.imglib2.parallel.TaskExecutor;
import net.imglib2.parallel.TaskExecutors;
Expand Down Expand Up @@ -105,6 +107,8 @@
import org.janelia.saalfeldlab.paintera.data.n5.BlockSpec;
import org.janelia.saalfeldlab.paintera.ui.PainteraAlerts;
import org.janelia.saalfeldlab.paintera.util.IntervalHelpers;
import org.janelia.saalfeldlab.paintera.util.IntervalIterable;
import org.janelia.saalfeldlab.paintera.util.ReusableIntervalIterator;
import org.janelia.saalfeldlab.util.TmpVolatileHelpers;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -1187,36 +1191,51 @@ private static <T extends IntegerType<T>> Set<Long> downsample(
steps
);

/* Views.tiles doesn't preserve intervals, so zeroMin prior to tiling */
final var zeroMinTarget = Views.zeroMin(target);
final var sourceInterval = IntervalHelpers.scale(target, steps, true);
final IntervalView<T> zeroMinSource = Views.zeroMin(Views.interval(source, sourceInterval));
final var tiledSource = Views.tiles(zeroMinSource, steps);

final HashSet<Long> labels = new HashSet<>();
LoopBuilder.setImages(tiledSource, zeroMinTarget)
.forEachChunk(chunk -> {
final TLongLongHashMap maxCounts = new TLongLongHashMap();
chunk.forEachPixel((sourceTile, lowResTarget) -> {
var maxCount = -1L;
var maxId = Label.INVALID;
for (T t : Views.iterable(sourceTile)) {
final long id = t.getIntegerLong();
if (id == Label.INVALID)
continue;
final var curCount = maxCounts.adjustOrPutValue(id, 1, 1);
if (curCount > maxCount) {
maxCount = curCount;
maxId = id;
}
}
lowResTarget.setInteger(maxId);
if (maxId != Label.INVALID)
labels.add(maxId);
maxCounts.clear();
});
return null;
});

final RandomAccess<T> zeroMinSourceRA = zeroMinSource.randomAccess();
final RandomAccess<T> zeroMinTargetRA = zeroMinTarget.randomAccess();

final ReusableIntervalIterator sourceIntervalIterator = new ReusableIntervalIterator(sourceInterval);
final long[] sourcePosMin = new long[zeroMinSource.numDimensions()];
final long[] sourcePosMax = new long[zeroMinSource.numDimensions()];
final var sourceTileInterval = new AbstractInterval(sourcePosMin, sourcePosMax, false) {

};

final TLongLongHashMap maxCounts = new TLongLongHashMap();

var sourceTileIterable = new IntervalIterable(sourceIntervalIterator);
for (long[] targetPos : new IntervalIterable(new LocalizingIntervalIterator(zeroMinTarget))) {
for (int i = 0; i < sourcePosMin.length; i++) {
sourcePosMin[i] = targetPos[i] * steps[i];
sourcePosMax[i] = sourcePosMin[i] + steps[i] - 1;
}
sourceIntervalIterator.resetInterval(sourceTileInterval);

var maxCount = -1L;
var maxId = Label.INVALID;

for (long[] sourcePos : sourceTileIterable) {
final long sourceVal = zeroMinSourceRA.setPositionAndGet(sourcePos).getIntegerLong();
if (sourceVal != Label.INVALID) {
var curCount = maxCounts.adjustOrPutValue(sourceVal, 1, 1);
if (curCount > maxCount) {
maxCount++;
maxId = sourceVal;
}
}
}

final T targetVal = zeroMinTargetRA.setPositionAndGet(targetPos);
targetVal.setInteger(maxId);
labels.add(maxId);
maxCounts.clear();
}
labels.remove(Label.INVALID);
return labels;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,6 @@ public Cell<VolatileLabelMultisetArray> createInvalid(final Long key) throws Exc
}
}

private static final VolatileLabelMultisetArray EMPTY_ACCESS = new VolatileLabelMultisetArray(0, true, new long[]{Label.INVALID});
private static final VolatileLabelMultisetArray EMPTY_ACCESS = new VolatileLabelMultisetArray(0, false, new long[]{Label.INVALID});

}
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,8 @@ class PainteraMainWindow(val gateway: PainteraGateway = PainteraGateway()) {
{ projectDirectory.actualDirectory.absolutePath },
{ indexToState[it] })
val gson = builder.create()
val json = projectDirectory.actualDirectory.absolutePath
.let { Paintera.n5Factory.openReader(it).getAttribute("/", PAINTERA_KEY, JsonElement::class.java) }
val json = projectDirectory.actualDirectory.toURI()
.let { Paintera.n5Factory.openReader(it.toString()).getAttribute("/", PAINTERA_KEY, JsonElement::class.java) }
?.takeIf { it.isJsonObject }
?.asJsonObject
Paintera.n5Factory.gsonBuilder(builder)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,9 @@ object SamEmbeddingLoaderCache : AsyncCacheWithLoader<RenderUnitState, OnnxTenso

private class NavigationBasedRequestTimer(val viewerAndTransforms: ViewerAndTransforms) : AnimationTimer() {

private val REQUEST_COUNTDOWN = 30 // Unit is pulses of the Animation timer; roughly targets 60 FPS.
companion object {
private const val REQUEST_COUNTDOWN = 15 // Unit is pulses of the Animation timer; roughly targets 60 FPS.
}

private val viewer
get() = viewerAndTransforms.viewer()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ class OrthoSliceConfig(private val baseConfig: OrthoSliceConfigBase) {
bottomLeftSlice.isVisibleProperty.bind(baseConfig.showBottomLeftProperty().and(enable).and(hasSources).and(isBottomLeftVisible))

listOf(topLeftSlice, topRightSlice, bottomLeftSlice).forEach { slice ->
slice.opacityProperty().bind(baseConfig.opacityProperty())
slice.shadingProperty().bind(baseConfig.shadingProperty())
slice.opacityProperty().bindBidirectional(baseConfig.opacityProperty())
slice.shadingProperty().bindBidirectional(baseConfig.shadingProperty())
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@ import org.janelia.saalfeldlab.control.mcu.MCUButtonControl
import org.janelia.saalfeldlab.fx.actions.ActionSet
import org.janelia.saalfeldlab.fx.actions.ActionSet.Companion.installActionSet
import org.janelia.saalfeldlab.fx.actions.ActionSet.Companion.removeActionSet
import org.janelia.saalfeldlab.fx.actions.DragActionSet
import org.janelia.saalfeldlab.fx.actions.NamedKeyBinding
import org.janelia.saalfeldlab.fx.actions.painteraActionSet
import org.janelia.saalfeldlab.fx.actions.painteraDragActionSet
import org.janelia.saalfeldlab.fx.actions.painteraMidiActionSet
import org.janelia.saalfeldlab.fx.midi.MidiButtonEvent
import org.janelia.saalfeldlab.fx.midi.MidiToggleEvent
Expand Down Expand Up @@ -89,7 +91,7 @@ class ShapeInterpolationMode<D : IntegerType<D>>(val controller: ShapeInterpolat
old?.viewer()?.apply { modeActions.forEach { removeActionSet(it) } }
}

private val samNavigationRequestListener = ChangeListener { _, _, curViewer ->
private val samNavigationRequestListener = ChangeListener<OrthogonalViews.ViewerAndTransforms?> { _, _, curViewer ->
SamEmbeddingLoaderCache.stopNavigationBasedRequests()
curViewer?.let {
SamEmbeddingLoaderCache.startNavigationBasedRequests(curViewer)
Expand Down Expand Up @@ -221,6 +223,17 @@ class ShapeInterpolationMode<D : IntegerType<D>>(val controller: ShapeInterpolat
keyPressEditSelectionAction(EditSelectionChoice.Previous, SHAPE_INTERPOLATION__SELECT_PREVIOUS_SLICE)
keyPressEditSelectionAction(EditSelectionChoice.Next, SHAPE_INTERPOLATION__SELECT_NEXT_SLICE)
},
painteraDragActionSet("drag activate SAM mode with box", PaintActionType.Paint, ignoreDisable = true, consumeMouseClicked = true) {
onDragDetected {
verify("can't trigger box prompt with active tool") { activeTool in listOf(NavigationTool, shapeInterpolationTool, samTool) }
switchTool(samTool)
}
onDrag {
(activeTool as? SamTool)?.apply {
requestBoxPromptPrediction(it)
}
}
},
DeviceManager.xTouchMini?.let { device ->
activeViewerProperty.get()?.viewer()?.let { viewer ->
painteraMidiActionSet("midi paint tool switch actions", device, viewer, PaintActionType.Paint) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,22 +27,23 @@ import org.janelia.saalfeldlab.paintera.util.IntervalHelpers.Companion.smallestC
import org.janelia.saalfeldlab.util.extendValue
import java.util.concurrent.CancellationException
import java.util.concurrent.atomic.AtomicBoolean
import java.util.function.*
import java.util.function.BooleanSupplier
import java.util.function.Consumer
import java.util.stream.Collectors

class FloodFill<T : IntegerType<T>>(
private val activeViewerProperty: ObservableValue<ViewerPanelFX>,
private val source: MaskedSource<T, *>,
val source: MaskedSource<T, *>,
private val assignment: FragmentSegmentAssignment,
private val requestRepaint: Consumer<Interval>,
val requestRepaint: Consumer<Interval?>,
private val isVisible: BooleanSupplier
) {

fun fillAt(x: Double, y: Double, fillSupplier: (() -> Long?)?): Job {
val fill = fillSupplier?.invoke() ?: let {
return Job().apply {
val reason = CancellationException("Received invalid label -- will not fill.")
LOG.debug(reason) { }
LOG.debug(reason) { }
completeExceptionally(reason)
}
}
Expand All @@ -55,7 +56,7 @@ class FloodFill<T : IntegerType<T>>(
if (!isVisible.asBoolean) {
return Job().apply {
val reason = CancellationException("Selected source is not visible -- will not fill")
LOG.debug(reason) { }
LOG.debug(reason) { }
completeExceptionally(reason)
}
}
Expand Down Expand Up @@ -96,7 +97,7 @@ class FloodFill<T : IntegerType<T>>(
val seedLabel = assignment?.getSegment(seedValue!!.integerLong) ?: seedValue!!.integerLong
if (!Label.regular(seedLabel)) {
val reason = CancellationException("Cannot fill at irregular label: $seedLabel (${Point(seed)})")
LOG.debug(reason) { }
LOG.debug(reason) { }
return Job().apply { completeExceptionally(reason) }
}

Expand All @@ -115,11 +116,13 @@ class FloodFill<T : IntegerType<T>>(
.collect(Collectors.toList<RealInterval>())

val triggerRefresh = AtomicBoolean(false)
var floodFillJob: Job? = null
val accessTracker: AccessBoxRandomAccessible<UnsignedLongType> = object : AccessBoxRandomAccessible<UnsignedLongType>(mask.rai.extendValue(UnsignedLongType(1))) {
val position: Point = Point(sourceAccess.numDimensions())

override fun get(): UnsignedLongType {
if (Thread.currentThread().isInterrupted) throw RuntimeException("Flood Fill Interrupted")
if (floodFillJob?.isCancelled == true || Thread.currentThread().isInterrupted)
throw CancellationException("Flood Fill Canceled")
synchronized(this) {
updateAccessBox()
}
Expand All @@ -136,18 +139,17 @@ class FloodFill<T : IntegerType<T>>(
}
}

val floodFillJob = CoroutineScope(Dispatchers.Default).launch {
floodFillJob = CoroutineScope(Dispatchers.Default).launch {
val fillContext = coroutineContext
InvokeOnJavaFXApplicationThread {
delay(1000)
while (fillContext.isActive) {
awaitPulse()
if (triggerRefresh.get()) {
val repaintInterval = globalToSource.inverse().estimateBounds(accessTracker.createAccessInterval())
requestRepaint.accept(repaintInterval.smallestContainingInterval)
triggerRefresh.set(false)
}
awaitPulse()
awaitPulse()
}
}

Expand All @@ -156,32 +158,16 @@ class FloodFill<T : IntegerType<T>>(
} else {
fillPrimitiveType(data, accessTracker, seed, seedLabel, fill, assignment)
}
}.also {
it.invokeOnCompletion { cause ->
val sourceInterval = accessTracker.createAccessInterval()
val globalInterval = globalToSource.inverse().estimateBounds(sourceInterval).smallestContainingInterval

when (cause) {
null -> {
LOG.trace { "FloodFill has been completed" }
LOG.trace {
"Applying mask for interval ${Intervals.minAsLongArray(sourceInterval).contentToString()} ${Intervals.maxAsLongArray(sourceInterval).contentToString()}"
}
requestRepaint.accept(globalInterval)
source.applyMask(mask, sourceInterval, MaskedSource.VALID_LABEL_CHECK)
}

is CancellationException -> try {
LOG.debug { "FloodFill has been interrupted" }
source.resetMasks()
requestRepaint.accept(globalInterval)
} catch (e: MaskInUse) {
LOG.error(e) {}
}
val sourceInterval = accessTracker.createAccessInterval()
val globalInterval = globalToSource.inverse().estimateBounds(sourceInterval).smallestContainingInterval

else -> requestRepaint.accept(globalInterval)
}
LOG.trace { "FloodFill has been completed" }
LOG.trace {
"Applying mask for interval ${Intervals.minAsLongArray(sourceInterval).contentToString()} ${Intervals.maxAsLongArray(sourceInterval).contentToString()}"
}
requestRepaint.accept(globalInterval)
source.applyMask(mask, sourceInterval, MaskedSource.VALID_LABEL_CHECK)
}
return floodFillJob
}
Expand Down Expand Up @@ -231,7 +217,7 @@ class FloodFill<T : IntegerType<T>>(
) { source, target: UnsignedLongType -> predicate(source, target) }
}

private suspend fun <T : IntegerType<T>> fillPrimitiveType(
private fun <T : IntegerType<T>> fillPrimitiveType(
input: RandomAccessibleInterval<T>,
output: RandomAccessible<UnsignedLongType>,
seed: Localizable,
Expand All @@ -249,7 +235,7 @@ class FloodFill<T : IntegerType<T>>(
seed,
UnsignedLongType(fillLabel),
DiamondShape(1)
) { source, target: UnsignedLongType -> runBlocking { predicate(source, target) } }
) { source, target: UnsignedLongType -> predicate(source, target) }
}

private fun <T : IntegerType<T>> makePredicate(seedLabel: Long, assignment: FragmentSegmentAssignment?): (T, UnsignedLongType) -> Boolean {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.janelia.saalfeldlab.paintera.control.tools.paint

import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView
import io.github.oshai.kotlinlogging.KotlinLogging
import javafx.beans.property.SimpleBooleanProperty
import javafx.beans.property.SimpleObjectProperty
import javafx.beans.value.ChangeListener
Expand All @@ -22,6 +23,7 @@ import org.janelia.saalfeldlab.fx.ui.GlyphScaleView
import org.janelia.saalfeldlab.fx.ui.ScaleView
import org.janelia.saalfeldlab.fx.util.InvokeOnJavaFXApplicationThread
import org.janelia.saalfeldlab.paintera.LabelSourceStateKeys
import org.janelia.saalfeldlab.paintera.Paintera
import org.janelia.saalfeldlab.paintera.control.ControlUtils
import org.janelia.saalfeldlab.paintera.control.actions.PaintActionType
import org.janelia.saalfeldlab.paintera.control.modes.ToolMode
Expand All @@ -32,6 +34,10 @@ import org.janelia.saalfeldlab.paintera.ui.overlays.CursorOverlayWithText

class Fill3DTool(activeSourceStateProperty: SimpleObjectProperty<SourceState<*, *>?>, mode: ToolMode? = null) : PaintTool(activeSourceStateProperty, mode) {

companion object {
private val LOG = KotlinLogging.logger { }
}

override val graphic = { ScaleView().also { it.styleClass += "fill-3d" } }

override val name = "Fill 3D"
Expand All @@ -48,7 +54,7 @@ class Fill3DTool(activeSourceStateProperty: SimpleObjectProperty<SourceState<*,
dataSource,
assignment,
{ interval -> paintera.baseView.orthogonalViews().requestRepaint(interval) },
{ activeSourceStateProperty.value?.isVisibleProperty?.get() ?: false }
{ activeSourceStateProperty.value?.isVisibleProperty?.get() == true }
)
}
}
Expand Down Expand Up @@ -101,16 +107,23 @@ class Fill3DTool(activeSourceStateProperty: SimpleObjectProperty<SourceState<*,

fillIsRunningProperty.set(true)
fill.fillAt(it!!.x, it.y, statePaintContext?.paintSelection).also { task ->
floodFillTask = task

paintera.baseView.isDisabledProperty.addListener(setFalseAndRemoveListener)
paintera.baseView.disabledPropertyBindings[this] = fillIsRunningProperty

task.invokeOnCompletion { cause ->
fillIsRunningProperty.set(false)
paintera.baseView.disabledPropertyBindings -= this
statePaintContext?.refreshMeshes?.invoke()
floodFillTask = null
floodFillTask = task.apply {
invokeOnCompletion { cause ->
fillIsRunningProperty.set(false)
paintera.baseView.disabledPropertyBindings -= this
cause?.let {
LOG.debug(cause) { "Fill 3D cancelled, resetting mask. " }
statePaintContext?.dataSource?.resetMasks(true)
fill.source.resetMasks()
fill.requestRepaint.accept(null)
}
statePaintContext?.refreshMeshes?.invoke()
floodFillTask = null
}
}
}
}
Expand Down
Loading

0 comments on commit c927f8b

Please sign in to comment.