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

Cam rotation and stats #631

Merged
merged 13 commits into from
Jan 30, 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
3 changes: 3 additions & 0 deletions src/main/kotlin/graphics/scenery/RichNode.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ package graphics.scenery
import graphics.scenery.attribute.renderable.HasRenderable
import graphics.scenery.attribute.material.HasMaterial
import graphics.scenery.attribute.spatial.HasSpatial
import graphics.scenery.volumes.Volume
import graphics.scenery.volumes.Volume.Companion.fromPathRawSplit
import org.joml.Vector3f

open class RichNode(override var name: String = "Node") : DefaultNode (name), HasRenderable, HasMaterial, HasSpatial {
init {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package graphics.scenery.attribute.spatial

import graphics.scenery.Camera
import graphics.scenery.DefaultNode
import graphics.scenery.Node
import graphics.scenery.Scene
Expand All @@ -12,6 +13,7 @@ import net.imglib2.RealLocalizable
import org.joml.*
import java.lang.Float.max
import java.lang.Float.min
import java.lang.Math
import kotlin.reflect.KClass
import kotlin.reflect.KProperty

Expand Down Expand Up @@ -178,6 +180,35 @@ open class DefaultSpatial(@Transient protected var node: Node = DefaultNode()) :
return center
}

/**
* This function rotates this spatial by a fixed [yaw] and [pitch] about the [target]
*
* @param[yaw] yaw in degrees
* @param[pitch] pitch in degrees
* @param[target] the target position
*/
override fun rotateAround(yaw: Float, pitch: Float, target: Vector3f) {
val frameYaw = (yaw) / 180.0f * Math.PI.toFloat()
val framePitch = pitch / 180.0f * Math.PI.toFloat()

// first calculate the total rotation quaternion to be applied to the camera
val yawQ = Quaternionf().rotateXYZ(0.0f, frameYaw, 0.0f).normalize()
val pitchQ = Quaternionf().rotateXYZ(framePitch, 0.0f, 0.0f).normalize()

node.ifSpatial {
val distance = (target - position).length()
rotation = pitchQ.mul(rotation).mul(yawQ).normalize()
if(node is Camera) {
position = target + (node as Camera).forward * distance * (-1.0f)
(node as Camera).target = target
} else {
val forward = world.transform(Vector4f(1.0f, 0.0f, 0.0f, 1.0f)).xyz()
position = target + forward * distance * (-1.0f)
}
}
}


override fun putAbove(position: Vector3f): Vector3f {
val center = centerOn(position)

Expand Down
9 changes: 9 additions & 0 deletions src/main/kotlin/graphics/scenery/attribute/spatial/Spatial.kt
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,15 @@ interface Spatial: RealLocalizable, RealPositionable, Networkable {
*/
fun centerOn(position: Vector3f): Vector3f

/**
* This function rotates this spatial by a fixed [yaw] and [pitch] about the [target]
*
* @param[yaw] yaw in degrees
* @param[pitch] pitch in degrees
* @param[target] the target position
*/
fun rotateAround(yaw: Float, pitch: Float, target: Vector3f)

/**
* Orients the Node between points [p1] and [p2], and optionally
* [rescale]s and [reposition]s it.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,20 +114,28 @@ open class ArcballCameraControl(private val name: String, camera: () -> Camera?,
lastX = x
lastY = y

val frameYaw = (xoffset) / 180.0f * Math.PI.toFloat()
val framePitch = yoffset / 180.0f * Math.PI.toFloat()

// first calculate the total rotation quaternion to be applied to the camera
val yawQ = Quaternionf().rotateXYZ(0.0f, frameYaw, 0.0f).normalize()
val pitchQ = Quaternionf().rotateXYZ(framePitch, 0.0f, 0.0f).normalize()

node.ifSpatial {
distance = (target.invoke() - position).length()
node.target = target.invoke()
rotation = pitchQ.mul(rotation).mul(yawQ).normalize()
position = target.invoke() + node.forward * distance * (-1.0f)
node.spatial().rotateAround(xoffset, yoffset, target.invoke())

node.lock.unlock()
}
}


/**
* This function rotates the camera controlled by this behaviour by a fixed [yaw]
* and [pitch] about the [target]
*
* @param[yaw] yaw in degrees
* @param[pitch] pitch in degrees
*/
fun rotateDegrees(yaw: Float, pitch: Float) {
cam?.let { node ->
if (!node.lock.tryLock()) {
return
}

node.spatial().rotateAround(yaw, pitch, target.invoke())

node.lock.unlock()
}
}
Expand Down
8 changes: 8 additions & 0 deletions src/main/kotlin/graphics/scenery/utils/Statistics.kt
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,14 @@ class Statistics(override var hub: Hub?) : Hubable {
}
}

/**
* Remove stat [name] from [stats]. Can be used, e.g., to reset
* the stat.
*/
fun clear(name: String) {
stats.remove(name)
}

/**
* Adds a new datum to the statistic about [name] with [value].
* Accepts all types of numbers.
Expand Down
25 changes: 25 additions & 0 deletions src/main/kotlin/graphics/scenery/utils/extensions/VolumeUtils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package graphics.scenery.utils.extensions

import graphics.scenery.RichNode
import graphics.scenery.volumes.Volume
import graphics.scenery.volumes.Volume.Companion.fromPathRawSplit
import org.joml.Vector3f

/**
* Positions [volumes] back-to-back without gaps, using their pixel-to-world ratio. Can, e.g., be used
* with [fromPathRawSplit] to load volume files greater than 2 GiB into sliced partitions and place
* the partitions back-to-back, emulating a single large volume in the scene.
*/
fun RichNode.positionVolumeSlices(volumes: List<Volume>) {
val pixelToWorld = volumes.first().pixelToWorldRatio

var sliceIndex = 0
volumes.forEach { volume ->
val currentSlices = volume.getDimensions().z
logger.debug("Volume partition with z slices: $currentSlices")
volume.pixelToWorldRatio = pixelToWorld

volume.spatial().position = Vector3f(0f, 0f, 1.0f * (sliceIndex) * pixelToWorld)
sliceIndex += currentSlices
}
}
161 changes: 136 additions & 25 deletions src/main/kotlin/graphics/scenery/volumes/Volume.kt
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import graphics.scenery.numerics.Random
import graphics.scenery.utils.lazyLogger
import graphics.scenery.utils.extensions.times
import graphics.scenery.utils.forEachIndexedAsync
import graphics.scenery.volumes.Volume.Companion.fromPathRawSplit
import graphics.scenery.volumes.Volume.VolumeDataSource.SpimDataMinimalSource
import io.scif.SCIFIO
import io.scif.filters.ReaderFilter
Expand Down Expand Up @@ -67,13 +68,10 @@ import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.atomic.AtomicInteger
import kotlin.io.path.name
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
import kotlin.math.sqrt
import kotlin.properties.Delegates
import kotlin.streams.toList
import net.imglib2.type.numeric.RealType
import kotlin.math.*

@Suppress("DEPRECATION")
open class Volume(
Expand Down Expand Up @@ -789,13 +787,62 @@ open class Volume(
return buffer
}

private fun readRawFile(path: Path, dimensions: Vector3i, bytesPerVoxel: Int, offsets: Pair<Long, Long>? = null): ByteBuffer {
val buffer: ByteBuffer by lazy {

val buffer = ByteArray(1024 * 1024)
val stream = FileInputStream(path.toFile())
if(offsets != null) {
stream.skip(offsets.first)
}

val imageData: ByteBuffer = MemoryUtil.memAlloc((bytesPerVoxel * dimensions.x * dimensions.y * dimensions.z))

logger.debug(
"{}: Allocated {} bytes for image of {} containing {} per voxel",
path.fileName,
imageData.capacity(),
dimensions,
bytesPerVoxel
)

val start = System.nanoTime()
var bytesRead = 0
var total = 0
while (true) {
var maxReadSize = minOf(buffer.size, imageData.capacity() - total)
maxReadSize = maxOf(maxReadSize, 1)
bytesRead = stream.read(buffer, 0, maxReadSize)

if(bytesRead < 0) {
break
}

imageData.put(buffer, 0, bytesRead)

total += bytesRead

if(offsets != null && total >= (offsets.second - offsets.first)) {
break
}
}
val duration = (System.nanoTime() - start) / 10e5
logger.debug("Reading took $duration ms")

imageData.flip()
imageData
}

return buffer
}

/**
* Reads a volume from the given [file].
*/
@JvmStatic @JvmOverloads
fun fromPath(file: Path, hub: Hub, onlyLoadFirst: Int? = null): BufferedVolume {
if(file.normalize().toString().endsWith("raw")) {
return fromPathRaw(file, hub)
return fromPathRaw(file, hub, UnsignedByteType())
}
var volumeFiles: List<Path>
if(Files.isDirectory(file)) {
Expand Down Expand Up @@ -936,11 +983,31 @@ open class Volume(
}

/**
* Reads raw volumetric data from a [file].
* Reads raw volumetric data from a [file], assuming the input
* data is 16bit Unsigned Int.
*
* Returns the new volume.
*/
@JvmStatic fun fromPathRaw(file: Path, hub: Hub): BufferedVolume {
@JvmStatic
fun <T: RealType<T>> fromPathRaw(
file: Path,
hub: Hub
): BufferedVolume {
return fromPathRaw(file, hub, UnsignedShortType())
}

/**
* Reads raw volumetric data from a [file], with the [type] being
* explicitly specified.
*
* Returns the new volume.
*/
@JvmStatic
fun <T: RealType<T>> fromPathRaw(
file: Path,
hub: Hub,
type: T
): BufferedVolume {

val infoFile: Path
val volumeFiles: List<Path>
Expand All @@ -963,32 +1030,75 @@ open class Volume(
val volumes = CopyOnWriteArrayList<BufferedVolume.Timepoint>()
volumeFiles.forEach { v ->
val id = v.fileName.toString()
val buffer: ByteBuffer by lazy {
logger.debug("Loading $id from disk")

logger.debug("Loading $id from disk")
val buffer = ByteArray(1024 * 1024)
val stream = FileInputStream(v.toFile())
val imageData: ByteBuffer = MemoryUtil.memAlloc((2 * dimensions.x * dimensions.y * dimensions.z))
val bytesPerVoxel = type.bitsPerPixel/8
val buffer = readRawFile(v, dimensions, bytesPerVoxel)

volumes.add(BufferedVolume.Timepoint(id, buffer))
}

logger.debug("${v.fileName}: Allocated ${imageData.capacity()} bytes for UINT16 image of $dimensions")
return fromBuffer(volumes, dimensions.x, dimensions.y, dimensions.z, type, hub)
}

val start = System.nanoTime()
var bytesRead = stream.read(buffer, 0, buffer.size)
while (bytesRead > -1) {
imageData.put(buffer, 0, bytesRead)
bytesRead = stream.read(buffer, 0, buffer.size)
}
val duration = (System.nanoTime() - start) / 10e5
logger.debug("Reading took $duration ms")
/**
* Reads raw volumetric data from a [file], splits it into buffers of at most, and as close as possible to,
* [sizeLimit] bytes and creates a volume from each buffer.
*
* Returns the list of volumes.
*/
@JvmStatic
fun <T: RealType<T>> fromPathRawSplit(
file: Path,
type: T,
sizeLimit: Long = 2000000000L,
hub: Hub
): Pair<Node, List<Volume>> {

imageData.flip()
imageData
val infoFile = file.resolveSibling("stacks.info")

val lines = Files.lines(infoFile).toList()

logger.debug("reading stacks.info (${lines.joinToString()}) (${lines.size} lines)")
val dimensions = Vector3i(lines.get(0).split(",").map { it.toInt() }.toIntArray())
val bytesPerVoxel = type.bitsPerPixel/8

var slicesRemaining = dimensions.z
var bytesRead = 0L
var numPartitions = 0

val slicesPerPartition = floor(sizeLimit.toFloat()/(bytesPerVoxel * dimensions.x * dimensions.y)).toInt()

val children = ArrayList<Volume>()

while (slicesRemaining > 0) {
val slices = if(slicesRemaining > slicesPerPartition) {
slicesPerPartition
} else {
slicesRemaining
}

volumes.add(BufferedVolume.Timepoint(id, buffer))
val partitionDims = Vector3i(dimensions.x, dimensions.y, slices)
val size = bytesPerVoxel * dimensions.x * dimensions.y * slices

val window = bytesRead to bytesRead+size-1

logger.debug("Reading raw file with offsets: $window")
val buffer = readRawFile(file, partitionDims, bytesPerVoxel, window)

val volume = ArrayList<BufferedVolume.Timepoint>()
volume.add(BufferedVolume.Timepoint(file.fileName.toString(), buffer))
children.add(fromBuffer(volume, partitionDims.x, partitionDims.y, partitionDims.z, type, hub))

slicesRemaining -= slices
numPartitions += 1
bytesRead += size
}

return fromBuffer(volumes, dimensions.x, dimensions.y, dimensions.z, UnsignedShortType(), hub)
val parent = RichNode()
children.forEach { parent.addChild(it) }

return parent to children
}

/** Amount of supported slicing planes per volume, see also sampling shader segments */
Expand All @@ -1015,3 +1125,4 @@ open class Volume(
}
}


Loading
Loading