All Downloads are FREE. Search and download functionalities are using the official Maven repository.

scalismo.ui.rendering.actor.ImageActor.scala Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (C) 2016  University of Basel, Graphics and Vision Research Group
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see .
 */

package scalismo.ui.rendering.actor

import scalismo.geometry.Point3D
import scalismo.ui.control.SlicingPosition
import scalismo.ui.model.properties.{NodeProperty, OpacityProperty}
import scalismo.ui.model.{Axis, BoundingBox, ImageNode}
import scalismo.ui.rendering.Caches
import scalismo.ui.rendering.actor.ImageActor2D.InstanceData
import scalismo.ui.rendering.actor.mixin.{ActorOpacity, ActorSceneNode, IsImageActor}
import scalismo.ui.rendering.util.VtkUtil
import scalismo.ui.view.{ScalismoFrame, ViewportPanel, ViewportPanel2D, ViewportPanel3D}
import scalismo.utils.ImageConversion
import vtk._

object ImageActor extends SimpleActorsFactory[ImageNode] {
  override def actorsFor(renderable: ImageNode, viewport: ViewportPanel): Option[Actors] = {
    viewport match {
      case _2d: ViewportPanel2D => Some(ImageActor2D(renderable, _2d))
      case _3d: ViewportPanel3D => Some(new ImageActor3D(renderable, _3d))
    }
  }
}

object ImageActor2D {

  def apply(node: ImageNode, viewport: ViewportPanel2D): ImageActor2D with SingleDataSetActor =
    new ImageActor2D(node, viewport.axis, viewport.frame) with SingleDataSetActor {
      override def boundingBox: BoundingBox = VtkUtil.bounds2BoundingBox(data.points.GetBounds())
    }

  def apply(node: ImageNode, axis: Axis, frame: ScalismoFrame) = new ImageActor2D(node, axis, frame)

  final val OutOfBounds: Int = -1
  final val NotInitialized: Int = -2

  class InstanceData(node: ImageNode, axis: Axis) {
    val points: vtkStructuredPoints =
      Caches.ImageCache.getOrCreate(node.source, ImageConversion.imageToVtkStructuredPoints(node.source))
    lazy val (min, max, exmax, eymax, ezmax) = {
      val b = points.GetBounds()
      val t = points.GetExtent()
      axis match {
        case Axis.X => (b(0), b(1), t(1), t(3), t(5))
        case Axis.Y => (b(2), b(3), t(1), t(3), t(5))
        case Axis.Z => (b(4), b(5), t(1), t(3), t(5))
      }
    }

    // most of the windowLevel logic is stolen from Slicer :-)
    // https://github.com/Slicer/Slicer/blob/121d28f3d03c418e13826a83df1ea1ffc586f0b7/Libs/MRML/DisplayableManager/vtkSliceViewInteractorStyle.cxx#L355-L370
    val windowLevel = new vtkImageMapToWindowLevelColors()
    windowLevel.SetInputData(points)
    windowLevel.SetWindow(node.windowLevel.value.window)
    windowLevel.SetLevel(node.windowLevel.value.level)
    windowLevel.Update()
    windowLevel.SetOutputFormatToLuminance()

    val slice = new vtkImageDataGeometryFilter
    slice.SetInputConnection(windowLevel.GetOutputPort())
    slice.ThresholdValueOff()
    slice.ThresholdCellsOff()
    slice.SetExtent(0, 0, 0, 0, 0, 0)
    slice.Update()

    // Transform used to correct image slice, such that
    // it coincides with the slicing position (which is in general not the case as the slicer slices only
    // at the grid position).
    val slicePositionCorrector = new vtkTransformPolyDataFilter()
    val sliceCorrectionTransform = new vtkTransform()
    sliceCorrectionTransform.Translate(0, 0, 0)
    slicePositionCorrector.SetTransform(sliceCorrectionTransform)
    slicePositionCorrector.SetInputConnection(slice.GetOutputPort())

  }

}

class ImageActor2D private[ImageActor2D] (override val sceneNode: ImageNode, axis: Axis, frame: ScalismoFrame)
    extends DataSetActor
    with IsImageActor
    with ActorOpacity
    with ActorEvents
    with ActorSceneNode {

  override def opacity: OpacityProperty = sceneNode.opacity

  val data = new InstanceData(sceneNode, axis)

  // This method computes the closest into the image for the given slicing position (point) for a given axis.
  def point3DToSliceIndex(p: Point3D, axis: Axis): Int = {
    val (fmin, fmax, fval, tmax) = axis match {
      case Axis.X => (data.min, data.max, p.x, data.exmax)
      case Axis.Y => (data.min, data.max, p.y, data.eymax)
      case Axis.Z => (data.min, data.max, p.z, data.ezmax)
    }

    if (fval < fmin || fval > fmax) ImageActor2D.OutOfBounds
    else {
      val (nval, nmax) = (fval - fmin, fmax - fmin)
      val idx = Math.round(nval * tmax / nmax).toInt
      idx
    }

  }

  def update(slicingPoint: Point3D, geometryChanged: Boolean): Unit = {
    val sliceIndex = point3DToSliceIndex(slicingPoint, axis)

    if (sliceIndex == ImageActor2D.OutOfBounds) {
      SetVisibility(0)
    } else {
      SetVisibility(1)

      // since the vtkImageDataGeometryFilter that is currently used for slicing does only
      // support slicing at grid position, the slicing position is in general different from the
      // indicated slicing position. We correct this by computing an additional translation.
      val (origin, spacing) = (data.points.GetOrigin(), data.points.GetSpacing())
      data.sliceCorrectionTransform.Identity()

      def computeOffset(component: Int): Double = {
        val pointComponentForIndex = origin(component) + sliceIndex * spacing(component)
        val offset = slicingPoint(component) - pointComponentForIndex
        offset
      }

      axis match {
        case Axis.X =>
          data.slice.SetExtent(sliceIndex, sliceIndex, 0, data.eymax, 0, data.ezmax)
          val offset = computeOffset(0)
          data.sliceCorrectionTransform.Translate(+offset, 0, 0)
        case Axis.Y =>
          data.slice.SetExtent(0, data.exmax, sliceIndex, sliceIndex, 0, data.ezmax)
          val offset = computeOffset(1)
          data.sliceCorrectionTransform.Translate(0, +offset, 0)
        case Axis.Z =>
          data.slice.SetExtent(0, data.exmax, 0, data.eymax, sliceIndex, sliceIndex)
          val offset = computeOffset(2)
          data.sliceCorrectionTransform.Translate(0, 0, +offset)
      }
      data.sliceCorrectionTransform.Modified()
      data.slicePositionCorrector.Modified()
      data.slice.Modified()
      mapper.Modified()
    }
    actorChanged(geometryChanged)

  }

  def updateWindowLevel(): Unit = {
    val wl = sceneNode.windowLevel.value
    if (data.windowLevel.GetWindow() != wl.window || data.windowLevel.GetLevel() != wl.level) {
      data.windowLevel.SetWindow(wl.window)
      data.windowLevel.SetLevel(wl.level)
      data.windowLevel.Modified()
      actorChanged()
    }
  }

  listenTo(frame.sceneControl.slicingPosition, sceneNode.windowLevel)

  mapper.SetInputConnection(data.slicePositionCorrector.GetOutputPort())
  update(frame.sceneControl.slicingPosition.point, geometryChanged = true)

  reactions += {
    case SlicingPosition.event.PointChanged(_, _, current) => update(current, geometryChanged = false)
    case NodeProperty.event.PropertyChanged(_)             => updateWindowLevel()
  }
}

class ImageActor3D(node: ImageNode, viewport: ViewportPanel3D) extends Actors {
  override val vtkActors: List[ImageActor2D] = Axis.All.map { axis =>
    ImageActor2D(node, axis, viewport.frame)
  }

  // the actors all return the same bounding box, so we just take the first
  override def boundingBox: BoundingBox =
    vtkActors.headOption.map(a => VtkUtil.bounds2BoundingBox(a.data.points.GetBounds())).getOrElse(BoundingBox.Invalid)
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy