Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/toggle box prompt #554

Merged
merged 8 commits into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading