diff --git a/pom.xml b/pom.xml index 4b98a7566..dda35ddf4 100644 --- a/pom.xml +++ b/pom.xml @@ -58,7 +58,7 @@ 22.0.1 - 1.4.1 + 1.4.2 4.0.16-alpha 1.4.1 diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/BindingKeys.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/BindingKeys.kt index b7650e120..3e17f071d 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/BindingKeys.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/BindingKeys.kt @@ -156,6 +156,29 @@ enum class LabelSourceStateKeys(lateInitNamedKeyCombo : LateInitNamedKeyCombinat } } +enum class RawSourceStateKeys(lateInitNamedKeyCombo : LateInitNamedKeyCombination) : NamedKeyBinding by lateInitNamedKeyCombo { + RESET_MIN_MAX_INTENSITY_THRESHOLD ( SHIFT_DOWN + Y, "Reset Min / Max Intensity Threshold"), + AUTO_MIN_MAX_INTENSITY_THRESHOLD ( Y, "Auto Min / Max Intensity Threshold"), + ; + + + private val formattedName = name.lowercase() + .replace("__", ": ") + .replace("_", " ") + + constructor(keys : KeyCombination, name : String? = null) : this(LateInitNamedKeyCombination(keys, name)) + constructor(key : KeyCode, name : String? = null) : this(LateInitNamedKeyCombination(key.asCombination(), name)) + constructor(key : Modifier, name : String? = null) : this(LateInitNamedKeyCombination(key.asCombination(), name)) + + init { + lateInitNamedKeyCombo.setName(formattedName) + } + + companion object { + fun namedCombinationsCopy() = NamedKeyCombination.CombinationMap(*entries.map { it.deepCopy }.toTypedArray()) + } +} + object NavigationKeys { const val BUTTON_TRANSLATE_ALONG_NORMAL_FORWARD = "translate along normal forward" const val BUTTON_TRANSLATE_ALONG_NORMAL_FORWARD_FAST = "translate along normal forward fast" diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/RawSourceMode.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/RawSourceMode.kt index 1811287c5..da012eb3c 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/RawSourceMode.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/RawSourceMode.kt @@ -28,6 +28,7 @@ import org.janelia.saalfeldlab.fx.actions.painteraActionSet import org.janelia.saalfeldlab.fx.ortho.OrthogonalViews import org.janelia.saalfeldlab.fx.ui.ScaleView import org.janelia.saalfeldlab.net.imglib2.converter.ARGBColorConverter +import org.janelia.saalfeldlab.paintera.RawSourceStateKeys import org.janelia.saalfeldlab.paintera.control.actions.AllowedActions import org.janelia.saalfeldlab.paintera.control.tools.Tool import org.janelia.saalfeldlab.paintera.paintera @@ -46,14 +47,14 @@ object RawSourceMode : AbstractToolMode() { private val minMaxIntensityThreshold = painteraActionSet("Min/Max Intensity Threshold") { verifyAll(KEY_PRESSED, "Source State is Raw Source State ") { activeSourceStateProperty.get() is ConnectomicsRawState<*, *> } - KEY_PRESSED(KeyCode.SHIFT, KeyCode.Y) { + KEY_PRESSED(RawSourceStateKeys.RESET_MIN_MAX_INTENSITY_THRESHOLD) { graphic = { ScaleView().apply { styleClass += "intensity-reset-min-max" } } onAction { val rawSource = activeSourceStateProperty.get() as ConnectomicsRawState<*, *> resetIntensityMinMax(rawSource) } } - KEY_PRESSED(KeyCode.Y) { + KEY_PRESSED(RawSourceStateKeys.AUTO_MIN_MAX_INTENSITY_THRESHOLD) { lateinit var viewer: ViewerPanelFX graphic = { ScaleView().apply { styleClass += "intensity-auto-min-max" } } verify("Last focused viewer found") { paintera.baseView.lastFocusHolder.value?.viewer()?.also { viewer = it } != null } 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 5aff3996a..0d9b0daa9 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 @@ -1,7 +1,6 @@ package org.janelia.saalfeldlab.paintera.control.modes import bdv.fx.viewer.render.RenderUnitState -import bdv.util.BdvFunctions import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView import io.github.oshai.kotlinlogging.KotlinLogging import javafx.beans.value.ChangeListener @@ -18,16 +17,18 @@ import net.imglib2.loops.LoopBuilder import net.imglib2.realtransform.AffineTransform3D import net.imglib2.type.logic.BoolType import net.imglib2.type.numeric.IntegerType -import net.imglib2.type.numeric.integer.UnsignedIntType import net.imglib2.type.numeric.integer.UnsignedLongType -import net.imglib2.type.volatiles.VolatileUnsignedIntType import net.imglib2.util.Intervals import net.imglib2.view.IntervalView import net.imglib2.view.Views import org.janelia.saalfeldlab.control.mcu.MCUButtonControl -import org.janelia.saalfeldlab.fx.actions.* +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.NamedKeyBinding +import org.janelia.saalfeldlab.fx.actions.painteraActionSet +import org.janelia.saalfeldlab.fx.actions.painteraMidiActionSet +import org.janelia.saalfeldlab.fx.extensions.addTriggeredWithListener import org.janelia.saalfeldlab.fx.midi.MidiButtonEvent import org.janelia.saalfeldlab.fx.midi.MidiToggleEvent import org.janelia.saalfeldlab.fx.midi.ToggleAction @@ -35,9 +36,12 @@ import org.janelia.saalfeldlab.fx.ortho.OrthogonalViews import org.janelia.saalfeldlab.fx.ui.GlyphScaleView import org.janelia.saalfeldlab.fx.util.InvokeOnJavaFXApplicationThread import org.janelia.saalfeldlab.labels.Label +import org.janelia.saalfeldlab.net.imglib2.view.BundleView import org.janelia.saalfeldlab.paintera.DeviceManager import org.janelia.saalfeldlab.paintera.LabelSourceStateKeys.* import org.janelia.saalfeldlab.paintera.cache.HashableTransform.Companion.hashable +import org.janelia.saalfeldlab.paintera.cache.SamEmbeddingLoaderCache +import org.janelia.saalfeldlab.paintera.cache.SamEmbeddingLoaderCache.calculateTargetSamScreenScaleFactor import org.janelia.saalfeldlab.paintera.control.ShapeInterpolationController import org.janelia.saalfeldlab.paintera.control.ShapeInterpolationController.ControllerState.Moving import org.janelia.saalfeldlab.paintera.control.ShapeInterpolationController.EditSelectionChoice @@ -45,12 +49,13 @@ import org.janelia.saalfeldlab.paintera.control.actions.AllowedActions import org.janelia.saalfeldlab.paintera.control.actions.MenuActionType import org.janelia.saalfeldlab.paintera.control.actions.NavigationActionType import org.janelia.saalfeldlab.paintera.control.actions.PaintActionType -import org.janelia.saalfeldlab.paintera.cache.SamEmbeddingLoaderCache -import org.janelia.saalfeldlab.paintera.cache.SamEmbeddingLoaderCache.calculateTargetSamScreenScaleFactor import org.janelia.saalfeldlab.paintera.control.paint.ViewerMask import org.janelia.saalfeldlab.paintera.control.paint.ViewerMask.Companion.createViewerMask import org.janelia.saalfeldlab.paintera.control.tools.Tool -import org.janelia.saalfeldlab.paintera.control.tools.paint.* +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.SamTool import org.janelia.saalfeldlab.paintera.control.tools.shapeinterpolation.ShapeInterpolationFillTool import org.janelia.saalfeldlab.paintera.control.tools.shapeinterpolation.ShapeInterpolationPaintBrushTool import org.janelia.saalfeldlab.paintera.control.tools.shapeinterpolation.ShapeInterpolationSAMTool @@ -59,8 +64,6 @@ 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.util.* -import org.janelia.saalfeldlab.net.imglib2.view.BundleView -import kotlin.collections.forEach import kotlin.collections.set class ShapeInterpolationMode>(val controller: ShapeInterpolationController, private val previousMode: ControlMode) : AbstractToolMode() { @@ -165,7 +168,7 @@ class ShapeInterpolationMode>(val controller: ShapeInterpolat with(controller) { verifyAll(KEY_PRESSED, "Shape Interpolation Controller is Active ") { isControllerActive } verifyAll(Event.ANY, "Shape Interpolation Tool is Active") { activeTool is ShapeInterpolationTool } - val exitMode = { _ : Event? -> + val exitMode = { _: Event? -> exitShapeInterpolation(false) paintera.baseView.changeMode(previousMode) } @@ -190,7 +193,12 @@ class ShapeInterpolationMode>(val controller: ShapeInterpolat filter = true verify("Fill2DTool is active") { activeTool is Fill2DTool } onAction { - switchTool(shapeInterpolationTool) + fill2DTool.fillIsRunningProperty.addTriggeredWithListener { obs, _, isRunning -> + if (!isRunning) { + switchTool(shapeInterpolationTool) + obs?.removeListener(this) + } + } } } }, @@ -202,7 +210,7 @@ class ShapeInterpolationMode>(val controller: ShapeInterpolat } KEY_PRESSED(fill2DTool.keyTrigger) { name = "switch to fill2d tool" - verify("Active source is MaskedSource") { activeSourceStateProperty.get()?.dataSource is MaskedSource<*, *> } + verify("Active source is MaskedSource") { activeSourceStateProperty.get()?.dataSource is MaskedSource<*, *> } onAction { switchTool(fill2DTool) } } KEY_PRESSED(samTool.keyTrigger) { @@ -343,7 +351,7 @@ class ShapeInterpolationMode>(val controller: ShapeInterpolat private fun ActionSet.keyPressEditSelectionAction(choice: EditSelectionChoice, namedKey: NamedKeyBinding) = with(controller) { - KEY_PRESSED ( namedKey) { + KEY_PRESSED(namedKey) { graphic = when (choice) { EditSelectionChoice.First -> { { GlyphScaleView(FontAwesomeIconView().also { it.styleClass += "interpolation-first-slice" }) } @@ -398,7 +406,7 @@ class ShapeInterpolationMode>(val controller: ShapeInterpolat .toList() val renderState = RenderUnitState(mask.initialGlobalToViewerTransform.copy(), mask.info.time, sources, width.toLong(), height.toLong()) - val predictionRequest = SamPredictor.SparsePrediction(maxDistancePositions.map { (x,y) -> renderState.getSamPoint(x,y, SamPredictor.SparseLabel.IN) }) + val predictionRequest = SamPredictor.SparsePrediction(maxDistancePositions.map { (x, y) -> renderState.getSamPoint(x, y, SamPredictor.SparseLabel.IN) }) SamSliceInfo(renderState, mask, predictionRequest, null, false).also { SamEmbeddingLoaderCache.load(renderState) @@ -465,7 +473,7 @@ class ShapeInterpolationMode>(val controller: ShapeInterpolat selectionIntervalOverMask: Interval, globalTransform: AffineTransform3D = paintera.baseView.manager().transform, viewerMask: ViewerMask = controller.currentViewerMask!!, - replaceExistingSlice : Boolean = false + replaceExistingSlice: Boolean = false ): SamSliceInfo? { val globalToViewerTransform = viewerMask.initialGlobalToMaskTransform val sliceDepth = controller.depthAt(globalToViewerTransform) @@ -527,7 +535,7 @@ internal fun IntervalView.getComponentMaxDistancePosition(): L DistanceTransform.binaryTransform(invertedBinaryImg, distances, DistanceTransform.DISTANCE_TYPE.EUCLIDIAN) - val distancePerComponent = mutableMapOf>() + val distancePerComponent = mutableMapOf>() var backgroundId = -1; @@ -601,7 +609,7 @@ internal data class SamSliceInfo(val renderState: RenderUnitState, val mask: Vie prediction = SamPredictor.SparsePrediction(listOf(renderState.getSamPoint(viewerX, viewerY, label))) } - fun updatePrediction(viewerPositions : List, label: SamPredictor.SparseLabel = SamPredictor.SparseLabel.IN) { - prediction = SamPredictor.SparsePrediction(viewerPositions.map { (x,y) -> renderState.getSamPoint(x,y, label) }) + fun updatePrediction(viewerPositions: List, label: SamPredictor.SparseLabel = SamPredictor.SparseLabel.IN) { + prediction = SamPredictor.SparsePrediction(viewerPositions.map { (x, y) -> renderState.getSamPoint(x, y, label) }) } } \ No newline at end of file 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 87a18086f..d85be0300 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 @@ -8,6 +8,7 @@ import javafx.beans.property.SimpleObjectProperty import javafx.beans.value.ObservableValue import javafx.scene.Cursor import javafx.scene.input.* +import javafx.util.Subscription import net.imglib2.Interval import net.imglib2.util.Intervals import org.janelia.saalfeldlab.fx.UtilityTask @@ -110,14 +111,16 @@ open class Fill2DTool(activeSourceStateProperty: SimpleObjectProperty Unit = {}): UtilityTask<*>? { 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 ada02c540..f3a4b2d3e 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 @@ -26,6 +26,7 @@ import javafx.scene.layout.Pane import javafx.scene.shape.Circle import javafx.scene.shape.Rectangle import kotlinx.coroutines.* +import kotlinx.coroutines.channels.Channel import net.imglib2.FinalInterval import net.imglib2.Interval import net.imglib2.RandomAccessibleInterval @@ -92,7 +93,6 @@ import org.janelia.saalfeldlab.paintera.util.IntervalHelpers.Companion.smallestC import org.janelia.saalfeldlab.paintera.util.algorithms.otsuThresholdPrediction import org.janelia.saalfeldlab.util.* import java.util.concurrent.CancellationException -import java.util.concurrent.LinkedBlockingQueue import kotlin.collections.List import kotlin.collections.MutableList import kotlin.collections.addAll @@ -204,7 +204,7 @@ open class SamTool(activeSourceStateProperty: SimpleObjectProperty(null) var lastPrediction by lastPredictionProperty.nullable() @@ -232,13 +232,13 @@ open class SamTool(activeSourceStateProperty: SimpleObjectProperty() - private val predictionQueue = LinkedBlockingQueue>(1) + private val predictionChannel = Channel>(1) private var currentPredictionRequest: Pair? = null - set(value) = synchronized(predictionQueue) { - predictionQueue.clear() + set(value) = runBlocking { + predictionChannel.tryReceive() /* capacity 1, so this will always either do nothing, or empty the channel */ value?.let { (request, _) -> - predictionQueue.put(value) + predictionChannel.send(value) if (!temporaryPrompt) request.drawPrompt() } @@ -297,6 +297,7 @@ open class SamTool(activeSourceStateProperty: SimpleObjectProperty, estimateThreshold: Boolean = true) { + if (promptPoints.isEmpty()) + temporaryPrompt = true + if (!predictionJob.isActive) { startPredictionJob() } @@ -845,9 +845,24 @@ open class SamTool(activeSourceStateProperty: SimpleObjectProperty? = null private var currentPrediction: SamPredictor.SamPrediction? = null + + private val resetSAMTaskOnException = CoroutineExceptionHandler { _, exception -> + LOG.error(exception) { "Error during SAM Prediction " } + isBusy = false + deactivate() + mode?.apply { + InvokeOnJavaFXApplicationThread { + switchTool(defaultTool) + } + } + SAM_TASK_SCOPE = CoroutineScope(Dispatchers.IO + Job()) + } + + protected open var currentDisplay = false + private fun startPredictionJob() { val maskSource = maskedSource ?: return - predictionJob = SAM_TASK_SCOPE.launch { + predictionJob = SAM_TASK_SCOPE.launch(resetSAMTaskOnException) { val session = createOrtSessionTask.get() val imageEmbedding = try { runBlocking { @@ -863,9 +878,8 @@ open class SamTool(activeSourceStateProperty: SimpleObjectProperty true - !info.preGenerated -> false - else -> temporaryPrompt + if (!info.locked && !info.preGenerated) { + temporaryPrompt = false + requestPrediction(info.prediction) } - requestPrediction(info.prediction) } - override fun cancelAction() { - shapeInterpolationMode.switchTool(shapeInterpolationMode.defaultTool) + override fun deactivate() { + super.deactivate() controller.setMaskOverlay() } diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/raw/ConnectomicsRawState.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/raw/ConnectomicsRawState.kt index 18b8a6679..72b9f231f 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/raw/ConnectomicsRawState.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/raw/ConnectomicsRawState.kt @@ -22,8 +22,10 @@ import org.janelia.saalfeldlab.fx.TitledPanes import org.janelia.saalfeldlab.fx.extensions.TitledPaneExtensions.Companion.graphicsOnly import org.janelia.saalfeldlab.fx.ui.NamedNode import org.janelia.saalfeldlab.paintera.PainteraBaseView +import org.janelia.saalfeldlab.paintera.RawSourceStateKeys import org.janelia.saalfeldlab.paintera.composition.Composite import org.janelia.saalfeldlab.paintera.composition.CompositeCopy +import org.janelia.saalfeldlab.paintera.config.input.KeyAndMouseBindings import org.janelia.saalfeldlab.paintera.control.modes.ControlMode import org.janelia.saalfeldlab.paintera.control.modes.RawSourceMode import org.janelia.saalfeldlab.paintera.data.DataSource @@ -78,6 +80,10 @@ open class ConnectomicsRawState( private val source: DataSource = backend.createSource(queue, priority, name) + override fun createKeyAndMouseBindings(): KeyAndMouseBindings { + return KeyAndMouseBindings(RawSourceStateKeys.namedCombinationsCopy()) + } + override fun getDataSource(): DataSource = source override fun converter(): ARGBColorConverter = converter