Skip to content

Commit

Permalink
Merge pull request #554 from saalfeldlab/feat/toggleBoxPrompt
Browse files Browse the repository at this point in the history
Feat/toggle box prompt
  • Loading branch information
cmhulbert authored Dec 13, 2024
2 parents 6542d35 + 9d88c89 commit aa0cf47
Show file tree
Hide file tree
Showing 9 changed files with 151 additions and 62 deletions.
4 changes: 2 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

<groupId>org.janelia.saalfeldlab</groupId>
<artifactId>paintera</artifactId>
<version>1.7.1-SNAPSHOT</version>
<version>1.8.0-SNAPSHOT</version>

<name>Paintera</name>
<description>New Era Painting and annotation tool</description>
Expand Down Expand Up @@ -68,7 +68,7 @@
<main-class>org.janelia.saalfeldlab.paintera.Paintera</main-class>
<app.name>Paintera</app.name>
<app.package>paintera</app.package>
<app.version>1.7.0</app.version>
<app.version>1.8.0</app.version>

<jvm.modules>javafx.base,javafx.controls,javafx.fxml,javafx.media,javafx.swing,javafx.web,javafx.graphics,java.naming,java.management,java.sql</jvm.modules>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,11 @@ public static Alert alert(final Alert.AlertType type) {
public static Alert alert(final Alert.AlertType type, boolean isResizable) {

final AtomicReference<Alert> alertRef = new AtomicReference<>();
PlatformImpl.runAndWait(() -> alertRef.set(new Alert(type)));
try {
InvokeOnJavaFXApplicationThread.invokeAndWait(() -> alertRef.set(new Alert(type)));
} catch (InterruptedException e) {
LOG.error("Could not create alert", e);
}
final Alert alert = alertRef.get();
alert.setTitle(Constants.NAME);
alert.setResizable(isResizable);
Expand Down
8 changes: 6 additions & 2 deletions src/main/kotlin/org/janelia/saalfeldlab/paintera/Paintera.kt
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ class Paintera : Application() {
private lateinit var painteraArgs: PainteraCommandLineArgs
private var projectDir: String? = null

internal lateinit var mainWindow : PainteraMainWindow

init {
application = this
/* add window listener for scenes */
Expand All @@ -59,15 +61,17 @@ class Paintera : Application() {
override fun init() {
paintable = false
if (!::commandlineArguments.isInitialized) {
commandlineArguments = parameters.raw.toTypedArray()
commandlineArguments = parameters?.raw?.toTypedArray() ?: emptyArray()
}
painteraArgs = PainteraCommandLineArgs()
if (commandlineArguments.isNotEmpty() && !parsePainteraCommandLine(*commandlineArguments)) {
Platform.exit()
return
}
Platform.setImplicitExit(true)
paintera = PainteraMainWindow()
paintera = PainteraMainWindow().also {
mainWindow = it
}

projectDir = painteraArgs.project()
val projectPath = projectDir?.let { File(it).absoluteFile }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,11 +189,13 @@ class PainteraMainWindow(val gateway: PainteraGateway = PainteraGateway()) {
}

private fun showSaveCompleteNotification(owner: Any = baseView.node.scene.window) {
Notifications.create()
val saveNotification = Notifications.create()
.graphic(FontAwesome[FontAwesomeIcon.CHECK_CIRCLE])
.title("Save Project")
.text("Save Complete")
.owner(owner)
saveNotification
.threshold(1, saveNotification)
.show()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ import net.imglib2.type.numeric.RealType
import net.imglib2.type.numeric.integer.UnsignedLongType
import net.imglib2.type.numeric.real.FloatType
import net.imglib2.type.volatiles.VolatileUnsignedLongType
import net.imglib2.util.*
import net.imglib2.util.ConstantUtils
import net.imglib2.util.Intervals
import net.imglib2.util.Util
import net.imglib2.view.ExtendedRealRandomAccessibleRealInterval
import net.imglib2.view.IntervalView
import net.imglib2.view.Views
Expand Down Expand Up @@ -64,7 +66,6 @@ import java.math.RoundingMode
import java.util.concurrent.CancellationException
import java.util.concurrent.atomic.AtomicReference
import java.util.function.Supplier
import kotlin.Pair
import kotlin.math.absoluteValue
import kotlin.math.sqrt

Expand Down Expand Up @@ -473,21 +474,16 @@ class ShapeInterpolationController<D : IntegerType<D>>(
if (freezeInterpolation) return
synchronized(source) {
source.resetMasks(false)
/* If preview is on, hide all except the first and last fill mask */
val fillMasks: MutableList<RealRandomAccessibleRealInterval<UnsignedLongType>> = mutableListOf()
val slices = slicesAndInterpolants.slices
slices.forEachIndexed { idx, slice ->
if (idx == 0 || idx == slices.size - 1 || !includeInterpolant) {
fillMasks += slice.mask.run {
viewerImg
.expandborder(0, 0, 1)
.extendValue(Label.INVALID)
.interpolateNearestNeighbor()
.affineReal(initialGlobalToMaskTransform.inverse())
.realInterval(slice.globalBoundingBox!!)
}


fillMasks += slice.mask.run {
viewerImg
.expandborder(0, 0, 1)
.extendValue(Label.INVALID)
.interpolateNearestNeighbor()
.affineReal(initialGlobalToMaskTransform.inverse())
.realInterval(slice.globalBoundingBox!!)
}
}
val invalidLabel = UnsignedLongType(Label.INVALID)
Expand Down Expand Up @@ -586,7 +582,7 @@ class ShapeInterpolationController<D : IntegerType<D>>(
try {
setCompositeMask()
} catch (e: MaskInUse) {
LOG.error { "Label source already has an active mask" }
LOG.error(e) { "Label source already has an active mask" }
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
package org.janelia.saalfeldlab.paintera.control.modes

import bdv.fx.viewer.render.RenderUnitState
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView
import io.github.oshai.kotlinlogging.KotlinLogging
import javafx.beans.property.SimpleBooleanProperty
import javafx.beans.value.ChangeListener
import javafx.collections.FXCollections
import javafx.collections.ObservableList
import javafx.event.Event
import javafx.scene.input.KeyCode
import javafx.scene.input.KeyEvent.KEY_PRESSED
import javafx.scene.input.KeyEvent.KEY_RELEASED
import javafx.util.Subscription
import kotlinx.coroutines.runBlocking
import net.imglib2.Interval
import net.imglib2.algorithm.labeling.ConnectedComponents
import net.imglib2.algorithm.morphology.distance.DistanceTransform
Expand All @@ -21,16 +26,12 @@ import net.imglib2.type.numeric.integer.UnsignedLongType
import net.imglib2.util.Intervals
import net.imglib2.view.IntervalView
import net.imglib2.view.Views
import org.controlsfx.control.Notifications
import org.janelia.saalfeldlab.bdv.fx.viewer.getDataSourceAndConverter
import org.janelia.saalfeldlab.control.mcu.MCUButtonControl
import org.janelia.saalfeldlab.fx.actions.ActionSet
import org.janelia.saalfeldlab.fx.actions.*
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
import org.janelia.saalfeldlab.fx.midi.ToggleAction
Expand All @@ -57,6 +58,7 @@ import org.janelia.saalfeldlab.paintera.control.tools.Tool
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.SamPredictor
import org.janelia.saalfeldlab.paintera.control.tools.paint.SamPredictor.SparseLabel
import org.janelia.saalfeldlab.paintera.control.tools.paint.SamTool
import org.janelia.saalfeldlab.paintera.control.tools.shapeinterpolation.ShapeInterpolationFillTool
import org.janelia.saalfeldlab.paintera.control.tools.shapeinterpolation.ShapeInterpolationPaintBrushTool
Expand All @@ -65,8 +67,9 @@ import org.janelia.saalfeldlab.paintera.control.tools.shapeinterpolation.ShapeIn
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.ui.FontAwesome
import org.janelia.saalfeldlab.util.*
import kotlin.collections.set
import kotlin.math.roundToLong

class ShapeInterpolationMode<D : IntegerType<D>>(val controller: ShapeInterpolationController<D>, private val previousMode: ControlMode) : AbstractToolMode() {

Expand Down Expand Up @@ -161,6 +164,8 @@ class ShapeInterpolationMode<D : IntegerType<D>>(val controller: ShapeInterpolat
converter.activeFragmentAlphaProperty().set((activeSelectionAlpha * 255).toInt())
}

internal val samStyleBoxToggle = SimpleBooleanProperty(true)

private fun modeActions(): List<ActionSet> {
return mutableListOf(
painteraActionSet(CANCEL, ignoreDisable = true) {
Expand Down Expand Up @@ -225,6 +230,7 @@ class ShapeInterpolationMode<D : IntegerType<D>>(val controller: ShapeInterpolat
},
painteraDragActionSet("drag activate SAM mode with box", PaintActionType.Paint, ignoreDisable = true, consumeMouseClicked = true) {
onDragDetected {
verify("primary click drag only ") { it.isPrimaryButtonDown && !it.isSecondaryButtonDown && !it.isMiddleButtonDown }
verify("can't trigger box prompt with active tool") { activeTool in listOf(NavigationTool, shapeInterpolationTool, samTool) }
switchTool(samTool)
}
Expand All @@ -234,6 +240,29 @@ class ShapeInterpolationMode<D : IntegerType<D>>(val controller: ShapeInterpolat
}
}
},
painteraActionSet("change auto sam style") {
KEY_PRESSED(KeyCode.B) {
onAction {
samStyleBoxToggle.set(!samStyleBoxToggle.get())
data class SamStyleToggle(val icon: FontAwesomeIcon, val title: String, val text: String)
val (toggle, title, text) = if (samStyleBoxToggle.get())
SamStyleToggle(FontAwesomeIcon.TOGGLE_RIGHT, "Toggle Sam Style", "Style: Interpolant Interval")
else
SamStyleToggle(FontAwesomeIcon.TOGGLE_LEFT, "Toggle Sam Style", "Style: Interpolant Distance Point")

InvokeOnJavaFXApplicationThread {
val notification = Notifications.create()
.graphic(FontAwesome[toggle])
.title(title)
.text(text)
.owner(paintera.baseView.node)
notification
.threshold(1, notification)
.show()
}
}
}
},
DeviceManager.xTouchMini?.let { device ->
activeViewerProperty.get()?.viewer()?.let { viewer ->
painteraMidiActionSet("midi paint tool switch actions", device, viewer, PaintActionType.Paint) {
Expand Down Expand Up @@ -331,13 +360,32 @@ class ShapeInterpolationMode<D : IntegerType<D>>(val controller: ShapeInterpolat
).filterNotNull()
}

internal fun applyShapeInterpolationAndExitMode() {
with(controller) {
var applyMaskTriggered = false
var selfReference: Subscription? = null
val subscription = source.isApplyingMaskProperty.subscribe { applyingMask ->
if (applyMaskTriggered && !applyingMask) {
selfReference?.unsubscribe()
InvokeOnJavaFXApplicationThread {
paintera.baseView.changeMode(previousMode)
}
}

}
selfReference = subscription
applyMaskTriggered = true
if (!applyMask())
subscription?.unsubscribe()
}
}

fun switchAndApplyShapeInterpolationActions(toolActions: ActionSet) {
with(toolActions) {
KEY_PRESSED(CANCEL) {
name = "cancel_to_shape_interpolation_tool"
onAction {
switchTool(shapeInterpolationTool)
runBlocking { switchTool(shapeInterpolationTool)?.join() }
controller.setMaskOverlay(replaceExistingInterpolants = true)
}
handleException {
Expand All @@ -346,11 +394,13 @@ class ShapeInterpolationMode<D : IntegerType<D>>(val controller: ShapeInterpolat
}
KEY_PRESSED(SHAPE_INTERPOLATION__ACCEPT_INTERPOLATION) {
onAction {
switchTool(shapeInterpolationTool)
if (controller.applyMask())
paintera.baseView.changeMode(previousMode)
runBlocking { switchTool(shapeInterpolationTool)?.join() }
applyShapeInterpolationAndExitMode()
}
handleException {
LOG.error(it) {}
paintera.baseView.changeMode(previousMode)
}
handleException { paintera.baseView.changeMode(previousMode) }
}
}
}
Expand Down Expand Up @@ -384,7 +434,7 @@ class ShapeInterpolationMode<D : IntegerType<D>>(val controller: ShapeInterpolat
}
}

internal fun cacheLoadSamSliceInfo(depth: Double, translate: Boolean = depth != controller.currentDepth, provideGlobalToViewerTransform : AffineTransform3D? = null): SamSliceInfo {
internal fun cacheLoadSamSliceInfo(depth: Double, translate: Boolean = depth != controller.currentDepth, provideGlobalToViewerTransform: AffineTransform3D? = null): SamSliceInfo {
return samSliceCache[depth] ?: with(controller) {
val viewerAndTransforms = this@ShapeInterpolationMode.activeViewerProperty.value!!
val viewer = viewerAndTransforms.viewer()!!
Expand All @@ -397,11 +447,12 @@ class ShapeInterpolationMode<D : IntegerType<D>>(val controller: ShapeInterpolat
else -> AffineTransform3D().also { viewerAndTransforms.viewer().state.getViewerTransform(it) }
}

val predictionPositions = provideGlobalToViewerTransform?.let { listOf(doubleArrayOf(width / 2.0, height / 2.0, 0.0)) } ?: let {
controller.getInterpolationImg(globalToViewerTransform, closest = true)?.let {
val interpolantInViewer = if (translate) alignTransformAndViewCenter(it, globalToViewerTransform, width, height) else it
interpolantInViewer.getComponentMaxDistancePosition()
} ?: listOf(doubleArrayOf(width / 2.0, height / 2.0, 0.0))
val fallbackPrompt = listOf(doubleArrayOf(width / 2.0, height / 2.0, 0.0) to SparseLabel.IN)
val predictionPositions = provideGlobalToViewerTransform?.let { fallbackPrompt } ?: let {
controller.getInterpolationImg(globalToViewerTransform, closest = true)?.let {
val interpolantInViewer = if (translate) alignTransformAndViewCenter(it, globalToViewerTransform, width, height) else it
interpolantInViewer.getInterpolantPrompt(samStyleBoxToggle.get())
} ?: fallbackPrompt
}


Expand All @@ -411,12 +462,11 @@ class ShapeInterpolationMode<D : IntegerType<D>>(val controller: ShapeInterpolat
val activeSource = activeSourceStateProperty.value!!.sourceAndConverter!!.spimSource
val sources = mask.viewer.state.sources
.filter { it.spimSource !== activeSource }
.map { sac -> getDataSourceAndConverter<Any> (sac) } // to ensure non-volatile
.map { sac -> getDataSourceAndConverter<Any>(sac) } // to ensure non-volatile
.toList()

val renderState = RenderUnitState(mask.initialGlobalToViewerTransform.copy(), mask.info.time, sources, width.toLong(), height.toLong())
val predictionRequest = SamPredictor.SparsePrediction(predictionPositions.map { (x, y) -> renderState.getSamPoint(x, y, SamPredictor.SparseLabel.IN) })

val predictionRequest = SamPredictor.SparsePrediction(predictionPositions.map { (pos, label) -> renderState.getSamPoint(pos[0], pos[1], label) })
SamSliceInfo(renderState, mask, predictionRequest, null, false).also {
SamEmbeddingLoaderCache.load(renderState)
samSliceCache[depth] = it
Expand Down Expand Up @@ -523,11 +573,26 @@ class ShapeInterpolationMode<D : IntegerType<D>>(val controller: ShapeInterpolat
}
}

internal fun RenderUnitState.getSamPoint(screenX: Double, screenY: Double, label: SamPredictor.SparseLabel): SamPredictor.SamPoint {
internal fun RenderUnitState.getSamPoint(screenX: Double, screenY: Double, label: SparseLabel): SamPredictor.SamPoint {
val screenScaleFactor = calculateTargetSamScreenScaleFactor()
return SamPredictor.SamPoint(screenX * screenScaleFactor, screenY * screenScaleFactor, label)
}

internal fun IntervalView<UnsignedLongType>.getInterpolantPrompt(box: Boolean): List<Pair<DoubleArray, SparseLabel>> {
return if (box)
getMinMaxIntervalPositions().mapIndexed { idx, it -> it to if (idx == 0) SparseLabel.TOP_LEFT_BOX else SparseLabel.BOTTOM_RIGHT_BOX }
else
getComponentMaxDistancePosition().map { it to SparseLabel.IN }
}

internal fun IntervalView<UnsignedLongType>.getMinMaxIntervalPositions(): List<DoubleArray> {
val interval = Intervals.expand(this, *dimensionsAsLongArray().map { (it * .1).roundToLong() }.toLongArray())
return listOf(
interval.minAsDoubleArray(),
interval.maxAsDoubleArray()
)
}

internal fun IntervalView<UnsignedLongType>.getComponentMaxDistancePosition(): List<DoubleArray> {
/* find the max point to initialize with */
val invalidBorderRai = extendValue(Label.INVALID).interval(Intervals.expand(this, 1, 1, 0))
Expand Down Expand Up @@ -614,11 +679,16 @@ internal data class SamSliceInfo(val renderState: RenderUnitState, val mask: Vie
val preGenerated get() = sliceInfo == null
val globalToViewerTransform get() = renderState.transform

fun updatePrediction(viewerX: Double, viewerY: Double, label: SamPredictor.SparseLabel = SamPredictor.SparseLabel.IN) {
fun updatePrediction(viewerX: Double, viewerY: Double, label: SparseLabel = SparseLabel.IN) {
prediction = SamPredictor.SparsePrediction(listOf(renderState.getSamPoint(viewerX, viewerY, label)))
}

fun updatePrediction(viewerPositions: List<DoubleArray>, label: SamPredictor.SparseLabel = SamPredictor.SparseLabel.IN) {
fun updatePrediction(viewerPositions: List<DoubleArray>, label: SparseLabel = SparseLabel.IN) {
prediction = SamPredictor.SparsePrediction(viewerPositions.map { (x, y) -> renderState.getSamPoint(x, y, label) })
}

fun updatePrediction(viewerPositionsAndLabels: List<Pair<DoubleArray, SparseLabel>>) {
prediction = SamPredictor.SparsePrediction(viewerPositionsAndLabels.map { (pos, label) -> renderState.getSamPoint(pos[0], pos[1], label) })

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -725,7 +725,7 @@ open class SamTool(activeSourceStateProperty: SimpleObjectProperty<SourceState<*
}
}

private fun SamPredictor.PredictionRequest.drawPrompt() {
internal fun SamPredictor.PredictionRequest.drawPrompt() {
clearPromptDrawings()
when (this) {
is SparsePrediction -> drawPromptPoints(points)
Expand Down
Loading

0 comments on commit aa0cf47

Please sign in to comment.