Skip to content
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
93 changes: 62 additions & 31 deletions src/main/kotlin/sc/iview/commands/analysis/CellTrackingBase.kt
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ import kotlin.concurrent.thread
* @param [sciview] The [SciView] instance to use
*/
open class CellTrackingBase(
open var sciview: SciView
open var sciview: SciView,
val resolutionScale: Float = 1f
) {
val logger by lazyLogger(System.getProperty("scenery.LogLevel", "info"))

Expand Down Expand Up @@ -130,6 +131,14 @@ open class CellTrackingBase(
var setVolumeVisCallback: ((Boolean) -> Unit)? = null
/** Merges overlapping spots in a given timepoint. */
var mergeOverlapsCallback: ((Int) -> Unit)? = null
/** Merges selected spots. */
var mergeSelectedCallback: (() -> Unit)? = null
/** Deletes the whole graph. */
var deleteGraphCallback: (() -> Unit)? = null
/** Deletes all annotations from this timepoint. */
var deleteTimepointCallback: (() -> Unit)? = null
/** Recenter and set default scaling for volume, then center camera on volume. */
var resetViewCallback: (() -> Unit)? = null

enum class HedgehogVisibility { Hidden, PerTimePoint, Visible }

Expand All @@ -146,12 +155,8 @@ open class CellTrackingBase(

var cursor = CursorTool
var leftElephantColumn: Column? = null
var leftColumnPredict: Column? = null
var leftColumnLink: Column? = null
var leftUndoMenu: Column? = null

var generalMenu: Column? = null
var enableTrackingPreview = true

val leftMenuList = mutableListOf<Column>()
var leftMenuIndex = 0

Expand All @@ -163,7 +168,7 @@ open class CellTrackingBase(
private val observers = mutableListOf<TimepointObserver>()

open fun run() {
sciview.toggleVRRendering()
sciview.toggleVRRendering(resolutionScale = resolutionScale)
hmd = sciview.hub.getWorkingHMD() as? OpenVRHMD ?: throw IllegalStateException("Could not find headset")

// Try to load the correct button mapping corresponding to the controller layout
Expand Down Expand Up @@ -226,6 +231,7 @@ open class CellTrackingBase(
inputSetup()

cellTrackingActive = true
rebuildGeometryCallback?.invoke()
launchUpdaterThread()
}

Expand Down Expand Up @@ -284,7 +290,7 @@ open class CellTrackingBase(
val trackCellsWithController = ClickBehaviour { _, _ ->
if (!controllerTrackingActive) {
controllerTrackingActive = true
cursor.setTrackingColor()
cursor.activateTrackingColor()
// we dont want animation, because we track step by step
playing = false
// Assume the user didn't click on an existing spot to start the track.
Expand All @@ -299,7 +305,7 @@ open class CellTrackingBase(
// If this is the first spot we track, and its a valid existing spot, mark it as such
if (isValidSelection && controllerTrackList.size == 0) {
startWithExistingSpot = selected
logger.info("Set startWithExistingPost to $startWithExistingSpot")
logger.debug("Set startWithExistingPost to $startWithExistingSpot")
} else {
controllerTrackList.add(p)
}
Expand Down Expand Up @@ -361,12 +367,8 @@ open class CellTrackingBase(
color = unpressedColor, touchingColor = touchingColor, pressedColor = pressedColor)

leftElephantColumn =
createWristMenuColumn(stageSpotsButton, name = "Stage Menu")
createWristMenuColumn(stageSpotsButton, trainAllButton, predictTPButton, predictAllButton, linkingButton, name = "Stage Menu")
leftElephantColumn?.visible = false
leftColumnPredict = createWristMenuColumn(trainAllButton, predictTPButton, predictAllButton, name = "Train/Predict Menu")
leftColumnPredict?.visible = false
leftColumnLink = createWristMenuColumn(linkingButton, name = "Linking Menu")
leftColumnLink?.visible = false
}

var lastButtonTime = System.currentTimeMillis()
Expand Down Expand Up @@ -477,14 +479,17 @@ open class CellTrackingBase(
color = color, pressedColor = pressedColor, touchingColor = touchingColor
)

val timeControlRow = Row(goToFirstBtn, playSlowerBtn, togglePlaybackDirBtn, playFasterBtn, goToLastBtn)
val resetViewButton = Button(
"Recenter", command = {
resetViewCallback?.invoke()
}, byTouch = true, depressDelay = 250,
color = color, pressedColor = pressedColor, touchingColor = touchingColor
)

leftUndoMenu = createWristMenuColumn(undoButton, redoButton, name = "Left Undo Menu")
leftUndoMenu?.visible = false
val previewMenu = createWristMenuColumn(toggleTrackingPreviewBtn, name = "Preview Menu")
previewMenu.visible = false
val timeMenu = createWristMenuColumn(timeControlRow, name = "Time Menu")
timeMenu.visible = false
val timeControlRow = Row(goToFirstBtn, playSlowerBtn, togglePlaybackDirBtn, playFasterBtn, goToLastBtn)
val undoRedoRow = Row(undoButton, redoButton, resetViewButton)
generalMenu = createWristMenuColumn(timeControlRow, undoRedoRow, name = "Left Undo Menu")
generalMenu?.visible = false

val toggleVolume = ToggleButton(
"Volume off", "Volume on", command = {
Expand All @@ -509,15 +514,33 @@ open class CellTrackingBase(
},
byTouch = true, color = color, pressedColor = pressedColor, touchingColor = touchingColor, default = true
)
val toggleVisMenu = createWristMenuColumn(toggleVolume, toggleTracks, toggleSpots)
val toggleVisMenu = createWristMenuColumn(toggleVolume, toggleTracks, toggleSpots, toggleTrackingPreviewBtn)
toggleVisMenu.visible = false

val mergeButton = Button(
val mergeOverlapsButton = Button(
"Merge overlaps", command = {
mergeOverlapsCallback?.invoke(volume.currentTimepoint)
}, byTouch = true, depressDelay = 250, color = color, pressedColor = pressedColor, touchingColor = touchingColor
)
val cleanupMenu = createWristMenuColumn(mergeButton)
val mergeSelectedButton = Button(
"Merge selected", command = {
mergeSelectedCallback?.invoke()
}, byTouch = true, depressDelay = 250, color = color, pressedColor = pressedColor, touchingColor = touchingColor
)

val deleteGraphButton = Button(
"Delete Graph", command = {
deleteGraphCallback?.invoke()
}, byTouch = true, depressDelay = 250, color = color, pressedColor = pressedColor, touchingColor = touchingColor
)
val deleteTimepointButton = Button(
"Delete TP", command = {
deleteTimepointCallback?.invoke()
}, byTouch = true, depressDelay = 250, color = color, pressedColor = pressedColor, touchingColor = touchingColor
)
val mergeRow = Row(mergeOverlapsButton, mergeSelectedButton)
val deleteRow = Row(deleteGraphButton, deleteTimepointButton)
val cleanupMenu = createWristMenuColumn(mergeRow, deleteRow)
cleanupMenu.visible = false
}

Expand Down Expand Up @@ -567,7 +590,7 @@ open class CellTrackingBase(
/** Object that represents the 3D cursor in form of a sphere. It needs to be attached to a VR controller via [attachCursor].
* The current cursor position can be obtained with [getPosition]. The current radius is stored in [radius].
* The tool can be scaled up and down with [scaleByFactor].
* [resetColor], [setSelectColor] and [setTrackingColor] allow changing the cursor's color to reflect the currently active operation. */
* [resetColor], [activateSelectColor] and [activateTrackingColor] allow changing the cursor's color to reflect the currently active operation. */
object CursorTool {
private val logger by lazyLogger()
var radius: Float = 0.007f
Expand Down Expand Up @@ -611,11 +634,12 @@ open class CellTrackingBase(
cursor.material().diffuse = Vector3f(0.15f, 0.2f, 1f)
}

fun setSelectColor() {
// TODO rename to activate
fun activateSelectColor() {
cursor.material().diffuse = Vector3f(1f, 0.25f, 0.25f)
}

fun setTrackingColor() {
fun activateTrackingColor() {
cursor.material().diffuse = Vector3f(0.65f, 1f, 0.22f)
}

Expand Down Expand Up @@ -814,7 +838,7 @@ open class CellTrackingBase(
override fun init(x: Int, y: Int) {
time = System.currentTimeMillis()
val p = cursor.getPosition()
cursor.setSelectColor()
cursor.activateSelectColor()
spotSelectCallback?.invoke(p, volume.currentTimepoint, cursor.radius, false)
}
override fun drag(x: Int, y: Int) {
Expand Down Expand Up @@ -1073,19 +1097,26 @@ open class CellTrackingBase(
open fun stop() {
logger.info("Objects in the scene: ${sciview.allSceneNodes.map { it.name }}")
cellTrackingActive = false
lightTetrahedron.forEach { sciview.deleteNode(it) }
if (::lightTetrahedron.isInitialized) {
lightTetrahedron.forEach { sciview.deleteNode(it) }
}
// Try to find and delete possibly existing VR objects
listOf("Shell", "leftHand", "rightHand").forEach {
val n = sciview.find(it)
n?.let { sciview.deleteNode(n) }
}
sciview.deleteNode(rightVRController?.model)
sciview.deleteNode(leftVRController?.model)
rightVRController?.model?.let {
sciview.deleteNode(it)
}
leftVRController?.model?.let {
sciview.deleteNode(it)
}

logger.info("Cleaned up basic VR objects. Objects left: ${sciview.allSceneNodes.map { it.name }}")

sciview.toggleVRRendering()
logger.info("Shut down and disabled VR environment.")
rebuildGeometryCallback?.invoke()
}

}
51 changes: 43 additions & 8 deletions src/main/kotlin/sc/iview/commands/analysis/EyeTracking.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import graphics.scenery.ui.Column
import graphics.scenery.ui.ToggleButton
import graphics.scenery.utils.SystemHelpers
import graphics.scenery.utils.extensions.*
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeoutOrNull
import net.imglib2.type.numeric.integer.UnsignedByteType
import org.apache.commons.math3.ml.clustering.Clusterable
import org.apache.commons.math3.ml.clustering.DBSCANClusterer
Expand All @@ -21,6 +23,9 @@ import java.awt.image.DataBufferByte
import java.io.ByteArrayInputStream
import java.nio.file.Files
import java.nio.file.Paths
import java.util.concurrent.CompletableFuture
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
import javax.imageio.ImageIO
import kotlin.concurrent.thread
import kotlin.math.PI
Expand All @@ -32,13 +37,11 @@ import kotlin.time.TimeSource
* [trackCreationCallback], which is called on every spine graph vertex that is extracted
*/
class EyeTracking(
sciview: SciView
): CellTrackingBase(sciview) {
sciview: SciView,
resolutionScale: Float = 1f,
): CellTrackingBase(sciview, resolutionScale) {

val pupilTracker = PupilEyeTracker(
calibrationType = PupilEyeTracker.CalibrationType.WorldSpace,
port = System.getProperty("PupilPort", "50020").toInt()
)
lateinit var pupilTracker: PupilEyeTracker
val calibrationTarget = Icosphere(0.02f, 2)
val laser = Cylinder(0.005f, 0.2f, 10)

Expand All @@ -52,7 +55,35 @@ class EyeTracking(

private var currentTrackingType = TrackingType.Follow

fun establishEyeTrackerConnection(): Boolean {
return try {
val future = CompletableFuture.supplyAsync {
PupilEyeTracker(
calibrationType = PupilEyeTracker.CalibrationType.WorldSpace,
port = System.getProperty("PupilPort", "50020").toInt()
)
}
pupilTracker = future.get(4, TimeUnit.SECONDS)
true
} catch (e: TimeoutException) {
logger.warn("Eye tracker initialization timed out after 2 seconds. Resuming with default VR.")
false
} catch (e: Exception) {
logger.error("Eye tracker initialization failed with ${e.message}")
false
}
}

override fun run() {
// Check whether we already initialized the pupilTracker or not
if (!::pupilTracker.isInitialized) {
if (!establishEyeTrackerConnection()) {
logger.error("Failed to initialize eye tracker")
// Handle the failure - maybe throw an exception or set a flag
return
}
}

// Do all the things for general VR startup before setting up the eye tracking environment
super.run()

Expand Down Expand Up @@ -471,9 +502,13 @@ class EyeTracking(
n?.let { sciview.deleteNode(it) }
// Delete definitely existing objects
listOf(referenceTarget, calibrationTarget, laser, debugBoard, hedgehogs).forEach {
sciview.deleteNode(it)
try {
sciview.deleteNode(it)
} catch (e: Exception) {
logger.warn("Failed to delete $it")
}
}
logger.info("Successfully cleaned up eye tracking environemt.")
logger.info("Successfully cleaned up eye tracking environment.")
super.stop()
}

Expand Down
Loading