Skip to content

Commit

Permalink
feat: Delete and Replace Labels
Browse files Browse the repository at this point in the history
  • Loading branch information
cmhulbert committed Dec 24, 2024
1 parent 4335fd1 commit a5e4d59
Show file tree
Hide file tree
Showing 8 changed files with 747 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ public enum LabelActionType implements ActionType {
Lock(true),
Merge,
Split,
SelectAll(true);
SelectAll(true),
Replace,
Delete;

//TODO Caleb: consider moving this to ActionType. Maybe others too
private final boolean readOnly;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
import net.imglib2.view.ExtendedRealRandomAccessibleRealInterval;
import net.imglib2.view.IntervalView;
import net.imglib2.view.Views;
import org.checkerframework.common.reflection.qual.Invoke;
import org.janelia.saalfeldlab.fx.Tasks;
import org.janelia.saalfeldlab.fx.UtilityTask;
import org.janelia.saalfeldlab.fx.util.InvokeOnJavaFXApplicationThread;
Expand Down Expand Up @@ -637,10 +638,10 @@ public void applyMaskOverIntervals(
intervalOverCanvas,
acceptAsPainted);

synchronized (progressBinding) {
var progress = completedTasks.incrementAndGet() / (double)expectedTasks;
InvokeOnJavaFXApplicationThread.invoke(() -> {
final double progress = completedTasks.incrementAndGet() / (double)expectedTasks;
progressBinding.set(progress);
}
});

final Map<Long, TLongHashSet> blocksByLabelByLevel = this.affectedBlocksByLabel[maskInfo.level];
synchronized (blocksByLabelByLevel) {
Expand Down Expand Up @@ -669,10 +670,10 @@ public void applyMaskOverIntervals(
acceptAsPainted,
propagationExecutor)
).get();
synchronized (progressBinding) {
var progress = completedTasks.incrementAndGet() / (double)expectedTasks;
InvokeOnJavaFXApplicationThread.invoke(() -> {
final double progress = completedTasks.incrementAndGet() / (double)expectedTasks;
progressBinding.set(progress);
}
});
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
Expand All @@ -687,9 +688,7 @@ public void applyMaskOverIntervals(
throw new RuntimeException(e);
}
}
synchronized (progressBinding) {
progressBinding.set(1.0);
}
InvokeOnJavaFXApplicationThread.invoke(() -> progressBinding.set(1.0));

synchronized (this) {
setCurrentMask(null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ enum class LabelSourceStateKeys(lateInitNamedKeyCombo : LateInitNamedKeyCombinat
SELECT_ALL_IN_CURRENT_VIEW ( CONTROL_DOWN + SHIFT_DOWN + A),
LOCK_SEGMENT ( L),
NEXT_ID ( N),
DELETE_ID ( SHIFT_DOWN + BACK_SPACE),
COMMIT_DIALOG ( C + CONTROL_DOWN),
MERGE_ALL_SELECTED ( ENTER + CONTROL_DOWN),
ARGB_STREAM__INCREMENT_SEED ( C),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
package org.janelia.saalfeldlab.paintera.control.actions.paint

import io.github.oshai.kotlinlogging.KotlinLogging
import javafx.beans.property.SimpleDoubleProperty
import javafx.beans.property.SimpleStringProperty
import javafx.event.ActionEvent
import javafx.scene.control.ButtonType
import javafx.scene.control.Dialog
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import net.imglib2.FinalInterval
import net.imglib2.Interval
import net.imglib2.type.Type
import net.imglib2.type.numeric.IntegerType
import net.imglib2.type.numeric.integer.UnsignedLongType
import net.imglib2.type.volatiles.VolatileUnsignedLongType
import net.imglib2.util.Intervals
import net.imglib2.view.IntervalView
import org.janelia.saalfeldlab.fx.actions.verifyPermission
import org.janelia.saalfeldlab.fx.extensions.createObservableBinding
import org.janelia.saalfeldlab.fx.extensions.nonnull
import org.janelia.saalfeldlab.fx.extensions.nonnullVal
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.control.actions.MenuAction
import org.janelia.saalfeldlab.paintera.control.actions.PaintActionType
import org.janelia.saalfeldlab.paintera.control.actions.onAction
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.n5.N5DataSource
import org.janelia.saalfeldlab.paintera.paintera
import org.janelia.saalfeldlab.paintera.state.label.ConnectomicsLabelState
import org.janelia.saalfeldlab.util.convert
import org.janelia.saalfeldlab.util.grids.LabelBlockLookupAllBlocks
import org.janelia.saalfeldlab.util.interval
import kotlin.math.nextUp
import net.imglib2.type.label.Label as Imglib2Label

object ReplaceLabel : MenuAction("_Replace or Delete Label...") {

private val LOG = KotlinLogging.logger { }

private val state = ReplaceLabelState()

private val replacementLabelProperty = state.replacementLabel
private val replacementLabel by replacementLabelProperty.nonnullVal()

private val activateReplacementProperty = state.activeReplacementLabel.apply {
subscribe { _, activate ->
if (activate)
activateReplacementLabel(0L, replacementLabel)
else
activateReplacementLabel(replacementLabel, 0L)
}
}
private val activateReplacement by activateReplacementProperty.nonnull()

private val progressProperty = SimpleDoubleProperty()
private val progressTextProperty = SimpleStringProperty()

init {
verifyPermission(PaintActionType.Erase, PaintActionType.Background, PaintActionType.Fill)
onAction(state) {
showDialog()
}
}

private fun <T : IntegerType<T>> ReplaceLabelState<T>.generateReplaceLabelMask(newLabel: Long, vararg fragments: Long) = with(maskedSource) {
val dataSource = getDataSource(0, 0)

val fragmentsSet = fragments.toHashSet()
dataSource.convert(UnsignedLongType(Imglib2Label.INVALID)) { src, target ->
val value = if (src.integerLong in fragmentsSet) newLabel else Imglib2Label.INVALID
target.set(value)
}.interval(dataSource)
}


private fun <T : IntegerType<T>> ReplaceLabelState<T>.deleteActiveFragment() = replaceActiveFragment(0L)
private fun <T : IntegerType<T>> ReplaceLabelState<T>.deleteActiveSegment() = replaceActiveSegment(0L)
private fun <T : IntegerType<T>> ReplaceLabelState<T>.deleteAllActiveFragments() = replaceAllActiveFragments(0L)
private fun <T : IntegerType<T>> ReplaceLabelState<T>.deleteAllActiveSegments() = replaceAllActiveSegments(0L)
private fun <T : IntegerType<T>> ReplaceLabelState<T>.replaceActiveFragment(newLabel: Long) = replaceLabels(newLabel, activeFragment)
private fun <T : IntegerType<T>> ReplaceLabelState<T>.replaceActiveSegment(newLabel: Long) = replaceLabels(newLabel, *fragmentsForActiveSegment)
private fun <T : IntegerType<T>> ReplaceLabelState<T>.replaceAllActiveFragments(newLabel: Long) = replaceLabels(newLabel, *allActiveFragments)
private fun <T : IntegerType<T>> ReplaceLabelState<T>.replaceAllActiveSegments(newLabel: Long) = replaceLabels(newLabel, *fragmentsForActiveSegment)


private fun <T : IntegerType<T>> ReplaceLabelState<T>.replaceLabels(newLabel: Long, vararg oldLabels: Long) = with(maskedSource) {
val blocks = blocksForLabels(0, *oldLabels)
val replacedLabelMask = generateReplaceLabelMask(newLabel, *oldLabels)

val sourceMask = SourceMask(
MaskInfo(0, 0),
replacedLabelMask,
replacedLabelMask.convert(VolatileUnsignedLongType(Imglib2Label.INVALID)) { input, output ->
output.set(input.integerLong)
output.isValid = true
}.interval(replacedLabelMask),
null,
null
) {}


val numBlocks = blocks.size
val progressTextBinding = progressProperty.createObservableBinding {
val blocksDone = (it.doubleValue() * numBlocks).nextUp().toLong().coerceAtMost(numBlocks.toLong())
"Blocks: $blocksDone / $numBlocks"
}
InvokeOnJavaFXApplicationThread {
progressTextProperty.unbind()
progressTextProperty.bind(progressTextBinding)
}

setMask(sourceMask) { it == newLabel }
applyMaskOverIntervals(
sourceMask,
blocks,
progressProperty
) { it == newLabel }

requestRepaintOverIntervals(blocks)
sourceState.refreshMeshes()
}

private fun activateReplacementLabel(current: Long, next: Long) {
val selectedIds = state.paintContext.selectedIds
if (current != selectedIds.lastSelection) {
selectedIds.deactivate(current)
}
if (activateReplacement && next > 0L) {
selectedIds.activateAlso(selectedIds.lastSelection, next)
}
}

fun showDialog() = state.showDialog()

private fun ReplaceLabelState<*>.showDialog() {
Dialog<Boolean>().apply {
isResizable = true
Paintera.registerStylesheets(dialogPane)
dialogPane.buttonTypes += ButtonType.APPLY
dialogPane.buttonTypes += ButtonType.CANCEL
title = name?.replace("_", "")

progressTextProperty.unbind()
progressTextProperty.set("")

progressProperty.unbind()
progressProperty.set(0.0)

val replaceLabelUI = ReplaceLabelUI(state)
replaceLabelUI.progressBarProperty.bind(progressProperty)
replaceLabelUI.progressLabelText.bind(progressTextProperty)

dialogPane.content = replaceLabelUI
dialogPane.lookupButton(ButtonType.APPLY).also { applyButton ->
applyButton.disableProperty().bind(paintera.baseView.isDisabledProperty)
applyButton.cursorProperty().bind(paintera.baseView.node.cursorProperty())
applyButton.addEventFilter(ActionEvent.ACTION) { event ->
event.consume()
val replacementLabel = state.replacementLabel.value
val fragmentsToReplace = state.fragmentsToReplace.toLongArray()
if (replacementLabel != null && replacementLabel >= 0 && fragmentsToReplace.isNotEmpty()) {
CoroutineScope(Dispatchers.Default).async {
replaceLabels(replacementLabel, *fragmentsToReplace)
if (activateReplacement)
state.sourceState.selectedIds.activate(replacementLabel)
}
}
}
}
dialogPane.lookupButton(ButtonType.CANCEL).apply {
disableProperty().bind(paintContext.dataSource.isApplyingMaskProperty())
}
dialogPane.scene.window.setOnCloseRequest {
if (paintContext.dataSource.isApplyingMaskProperty().get())
it.consume()
}
}.show()
}

private fun ReplaceLabelState<*>.requestRepaintOverIntervals(sourceIntervals: List<Interval>? = null) {
val globalInterval = sourceIntervals
?.reduce(Intervals::union)
?.let { maskedSource.getSourceTransformForMask(MaskInfo(0, 0)).estimateBounds(it) }

paintera.baseView.orthogonalViews().requestRepaint(globalInterval)
}

fun ReplaceLabelState<*>.blocksForLabels(scale0: Int, vararg labels: Long): List<Interval> = with(maskedSource) {
val blocksFromSource = labels.flatMap { sourceState.labelBlockLookup.read(LabelBlockLookupKey(scale0, it)).toList() }

/* 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))
}
}

return blocksFromSource + blocksFromCanvas
}

}

private fun <T : IntegerType<T>> MaskedSource<T, out Type<*>>.addReplaceMaskAsSource(
replacedFragmentMask: IntervalView<UnsignedLongType>
): ConnectomicsLabelState<UnsignedLongType, VolatileUnsignedLongType> {
val metadataState = (underlyingSource() as? N5DataSource)?.getMetadataState()!!
return paintera.baseView.addConnectomicsLabelSource<UnsignedLongType, VolatileUnsignedLongType>(
replacedFragmentMask,
metadataState.resolution,
metadataState.translation,
1L,
"fragmentMask",
LabelBlockLookupAllBlocks.fromSource(underlyingSource())
)!!
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package org.janelia.saalfeldlab.paintera.control.actions.paint

import javafx.beans.property.BooleanProperty
import javafx.beans.property.LongProperty
import javafx.beans.property.SimpleBooleanProperty
import javafx.beans.property.SimpleLongProperty
import javafx.collections.FXCollections
import javafx.collections.ObservableList
import javafx.event.Event
import net.imglib2.type.numeric.IntegerType
import org.janelia.saalfeldlab.fx.actions.Action
import org.janelia.saalfeldlab.paintera.control.actions.ActionState
import org.janelia.saalfeldlab.paintera.control.actions.verify
import org.janelia.saalfeldlab.paintera.control.modes.PaintLabelMode
import org.janelia.saalfeldlab.paintera.control.tools.paint.StatePaintContext
import org.janelia.saalfeldlab.paintera.paintera
import org.janelia.saalfeldlab.paintera.state.label.ConnectomicsLabelState

interface ReplaceLabelUIState {

val activeFragment: Long
val activeSegment: Long
val allActiveFragments: LongArray
val allActiveSegments: LongArray
val fragmentsForActiveSegment: LongArray
val fragmentsForAllActiveSegments: LongArray

val fragmentsToReplace: ObservableList<Long>
val replacementLabel: LongProperty
val activeReplacementLabel: BooleanProperty

fun fragmentsForSegment(segment: Long): LongArray
fun nextId(): Long

}

class ReplaceLabelState<T>() : ActionState(), ReplaceLabelUIState
where T : IntegerType<T> {
internal lateinit var sourceState: ConnectomicsLabelState<*, *>
internal lateinit var paintContext: StatePaintContext<T, *>

internal val maskedSource
get() = paintContext.dataSource

internal val assignment
get() = paintContext.assignment

private val selectedIds
get() = paintContext.selectedIds

override val activeFragment
get() = selectedIds.lastSelection

override val activeSegment
get() = assignment.getSegment(activeFragment)

override val fragmentsForActiveSegment: LongArray
get() = assignment.getFragments(activeSegment).toArray()

override val allActiveFragments: LongArray
get() = selectedIds.activeIds.toArray()

override val allActiveSegments
get() = allActiveFragments.asSequence()
.map { assignment.getSegment(it) }
.toSet()
.toLongArray()

override val fragmentsForAllActiveSegments
get() = allActiveSegments.asSequence()
.flatMap { assignment.getFragments(it).toArray().asSequence() }
.toSet()
.toLongArray()

override fun fragmentsForSegment(segment: Long): LongArray {
return assignment.getFragments(segment).toArray()
}

override val fragmentsToReplace: ObservableList<Long> = FXCollections.observableArrayList()
override val replacementLabel: LongProperty = SimpleLongProperty(0L)
override val activeReplacementLabel = SimpleBooleanProperty(false)

override fun nextId() = sourceState.nextId()

override fun <E : Event> Action<E>.verifyState() {
verify(::sourceState, "Label Source is Active") { paintera.currentSource as? ConnectomicsLabelState<*, *> }
verify(::paintContext, "Paint Label Mode has StatePaintContext") { PaintLabelMode.statePaintContext as StatePaintContext<T, *> }

verify("Paint Label Mode is Active") { paintera.currentMode is PaintLabelMode }
verify("Paintera is not disabled") { !paintera.baseView.isDisabledProperty.get() }
verify("Mask not in use") { !paintContext.dataSource.isMaskInUseBinding().get() }
}
}
Loading

0 comments on commit a5e4d59

Please sign in to comment.