Commit 22f5dee1 authored by Stephen Thompson's avatar Stephen Thompson

Merge branch '1-get-started'

parents 4406659f 12566229
Pipeline #3820 passed with stages
in 7 minutes and 18 seconds
......@@ -5,7 +5,6 @@ variables:
stages:
- build
- installer
- test
- deploy
......@@ -19,34 +18,6 @@ stages:
variables:
- $CI_COMMIT_MESSAGE =~ /\[skip[ _-]build?\]/i
.build-install-template: &build-install-template
script:
- tox -e installer
artifacts:
paths:
- dist/
expire_in: 1 week
# The win/mac/linux build stages inherit settings
# from build-install-template
build linux installer:
<<: *build-skip-template
<<: *build-install-template
tags:
- shared-linux
build mac installer:
<<: *build-skip-template
<<: *build-install-template
tags:
- shared-mac
build windows installer:
<<: *build-skip-template
<<: *build-install-template
tags:
- shared-win
build docs:
<<: *build-skip-template
script:
......
......@@ -23,18 +23,17 @@ scikit-surgery-evaluation
Author: Stephen Thompson
scikit-surgery-evaluation provides an application to evaluate surgical skills. You can provide a set of unstructured grids representing a set of locations that the user is then expected to target using a tracked pointer, utilising a SNAPPY tracking library (scikit-surgeryarucotracker, or scikit-surgerynditracker). You can specify paths for the user to follow, or let the system select target meshes automatically.
scikit-surgery-evaluation is part of the `SNAPPY`_ software project, developed at the `Wellcome EPSRC Centre for Interventional and Surgical Sciences`_, part of `University College London (UCL)`_.
scikit-surgery-evaluation supports Python 2.7 and Python 3.6.
scikit-surgery-evaluation supports Python 3.6.
scikit-surgery-evaluation is currently a demo project, which will add/multiply two numbers. Example usage:
::
python sksurgeryeval.py 5 8
python sksurgeryeval.py 3 6 --multiply
python sksurgeryeval.py -c configuration.json
Please explore the project structure, and implement your own functionality.
Developing
----------
......
566.222 0 311.281
0 565.696 245.08
0 0 1
0.137847 -0.058787 0.00508153 -0.00782094
{
"tracker config" :
{
"tracker type" : "aruco",
"debug" : true,
"calibration" : "configuration/calibration.txt"
},
"camera" : {
"origin" : [0.0, 0.0, 0.0],
"normal" : [0.0, 0.0, 1.0],
"bounding box" : [0.0, 640.0, 0.0, 480.0, -100.0, 0.0]
},
"target" : "data/patches",
"map" : "data/full",
"model to world" : "configuration/model_to_world.txt",
"logo" : false,
"search radius" : 1000
}
1. 0. 0. -616.
0. 1. 0. -202.
0. 0. 1. 1558.
0. 0. 0. 1.
......@@ -2,6 +2,13 @@
# It is used by pip to manage software dependencies. It is not to be
# confused with the software requirements, which are listed in
# doc/requirements.rst
numpy
numpy>=1.11
ipykernel
nbsphinx
vtk
PySide2
scikit-surgeryvtk>=0.12.3
scikit-surgerycore
scikit-surgeryutils
scikit-surgerynditracker
scikit-surgeryarucotracker>=0.0.4
......@@ -47,12 +47,21 @@ setup(
exclude=[
'doc',
'tests',
'data',
]
),
install_requires=[
'six>=1.10',
'numpy>=1.11',
'ipykernel',
'nbsphinx',
'vtk',
'PySide2',
'scikit-surgeryvtk>=0.12.3',
'scikit-surgerycore',
'scikit-surgeryutils',
'scikit-surgerynditracker',
'scikit-surgeryarucotracker>=0.0.4'
],
entry_points={
......
# coding=utf-8
""" Module for adding numbers. """
def add_two_numbers(input_x, input_y):
""" Add two numbers """
return input_x + input_y
# coding=utf-8
""" Algorithms for the surgery evaluation application """
from random import shuffle
import vtk
from numpy import inf, eye, loadtxt, float32
from sksurgerynditracker.nditracker import NDITracker
from sksurgeryarucotracker.arucotracker import ArUcoTracker
from sksurgeryvtk.models. vtk_surface_model_directory_loader \
import VTKSurfaceModelDirectoryLoader
#from sksurgeryeval.shapes.cone import VTKConeModel
def point_in_locator(point, point_locators, radius=1.0):
"""
Tests whether a point is within a set distance of any of a
list of point locators.
:param point: the point to test, in 3D (x,y,z)
:param point_locators: a list of vtkPointLocators
:param radius: optional search radius in mm (default=1.0)
:return locator: the index of the nearest point locator,
-1 if no locators within radius)
:return distance: distance to nearest point_locator
:raises: delegates to vtk
"""
minumum_distance = inf
locator_index = -1
for index, locator in enumerate(point_locators):
distance = vtk.mutable(0.0)
if locator.FindClosestPointWithinRadius(radius, point, distance) == -1:
continue
if distance > minumum_distance:
continue
minumum_distance = distance
locator_index = index
return locator_index, minumum_distance
def np2vtk(mat):
"""
Converts a Numpy array to a vtk matrix
:param: the number array, should be 4x4
:return: a vtk 4x4 matrix
:raises: ValueError when matrix is not 4x4
"""
if mat.shape == (4, 4):
obj = vtk.vtkMatrix4x4()
for i in range(4):
for j in range(4):
obj.SetElement(i, j, mat[i, j])
return obj
raise ValueError('Array must be 4x4')
def configure_tracker(config):
"""
Configures the tracking system.
:param: A dictionary containing configuration data
:return: The tracker object
:raises: KeyError if no tracker entry in config
"""
if "tracker type" not in config:
raise KeyError('Tracker configuration requires tracker type')
tracker_type = config.get("tracker type")
tracker = None
if tracker_type in ("vega", "polaris", "aurora", "dummy"):
tracker = NDITracker(config)
if tracker_type in "aruco":
tracker = ArUcoTracker(config)
tracker.start_tracking()
return tracker
def populate_models(config):
"""
Loads vtk models from a directory and returns
a list of vtk actors and associated vtkPointLocators
:param: configuration, should contain a target value
:param: model_to_world: 4x4 matrix, of dtype float32
:return: locators
:return: actors
:raises: KeyError if target not in config
"""
models = []
if "target" not in config:
raise KeyError("Config must contain target key")
path_name = config.get("target")
loader = VTKSurfaceModelDirectoryLoader(path_name)
models = loader.models
locators = []
model_to_world = _set_model_to_world(config)
transform = vtk.vtkTransform()
transform.SetMatrix(np2vtk(model_to_world))
for model in models:
print(model.source.GetCenter())
transformer = vtk.vtkTransformPolyDataFilter()
transformer.SetTransform(transform)
transformer.SetInputData(model.source)
target = vtk.vtkPolyData()
transformer.SetOutput(target)
transformer.Update()
model.source = target
transformer.SetInputConnection(model.normals.GetOutputPort())
model.mapper = vtk.vtkPolyDataMapper()
model.mapper.SetInputConnection(transformer.GetOutputPort())
model.mapper.Update()
model.actor.SetMapper(model.mapper)
print("after trans", model.source.GetCenter())
point_locator = vtk.vtkPointLocator()
point_locator.SetDataSet(model.source)
point_locator.Update()
locators.append(point_locator)
return models, locators
def add_map(config):
"""
Loads vtk models from a directory and returns
a list of vtk actors, with mesh visualisation
:param: configuration, may contain a "map" key
:param: model_to_world: 4x4 matrix, of dtype float32
:return: actors, None if no "map" key
"""
models = []
if "map" not in config:
return None
path_name = config.get("map")
loader = VTKSurfaceModelDirectoryLoader(path_name)
models = loader.models
model_to_world = _set_model_to_world(config)
transform = vtk.vtkTransform()
transform.SetMatrix(np2vtk(model_to_world))
for model in models:
transformer = vtk.vtkTransformPolyDataFilter()
transformer.SetTransform(transform)
transformer.SetInputData(model.source)
target = vtk.vtkPolyData()
transformer.SetOutput(target)
transformer.Update()
model.source = target
transformer.SetInputConnection(model.normals.GetOutputPort())
model.mapper = vtk.vtkPolyDataMapper()
model.mapper.SetInputConnection(transformer.GetOutputPort())
model.mapper.Update()
model.actor.SetMapper(model.mapper)
model.actor.GetProperty().SetRepresentationToWireframe()
model.actor.GetProperty().SetColor(0.7, 0.7, 0.7)
model.actor.GetProperty().SetOpacity(1.0)
model.actor.GetProperty().SetBackfaceCulling(False)
return models
def _set_model_to_world(config):
"""
Creates a 4x4 model to world matrix
:param: the configuration, if model to world is defined it will load, if
not will set a identity model to world
:return the model to world matrix
:raises: ValueError if file does not contain a 4x4 matrix
"""
model_to_world = eye(4, dtype=float32)
if "model to world" in config:
model_to_world = loadtxt(config.get("model to world"), dtype=float32)
if (model_to_world.shape == (4, 4) and model_to_world.dtype == float32):
return model_to_world
raise ValueError(('model to world should be a 4x4 matrix of type float32'),
model_to_world.shape, model_to_world.dtype)
def random_targets(count):
"""
Create a list of targets
"""
list_a = []
for i in range(count):
list_a.append(i)
shuffle(list_a)
return list_a
"""
A class to provide the background image
"""
from numpy import zeros, uint8
from sksurgeryimage.utilities.weisslogo import WeissLogo
class OverlayBackground():
"""
Provides the background image for the overlay
window.
"""
def __init__(self, config):
"""
Initialises and configures class to provide a background image.
Image can be a WEISS logo, or blank.
:param: A configuration dictionary
:raises: RunTimeError, KeyError
"""
self._logo_maker = None
self._blank_image = None
if config.get("logo"):
self._logo_maker = WeissLogo()
else:
self._blank_image = zeros(shape=[512, 512, 3], dtype=uint8)
def next_image(self):
"""
Returns a background image.
The behaviour is determined by the configuration
dictionary used at init.
"""
if self._logo_maker is not None:
image = self._logo_maker.get_noisy_logo()
else:
image = self._blank_image
return image
# coding=utf-8
"""Main loop for surgery evaluation"""
from sksurgeryvtk.text.text_overlay import VTKCornerAnnotation
from sksurgeryeval.algorithms.algorithms import (
populate_models, point_in_locator, random_targets)
class Locators():
"""stores a list of vtk models and corresponding locators,
and handles associated logic
"""
def __init__(self, config):
"""Overides overlay base app's init, to initialise the
external tracking system. Together with a video source"""
self.models, self._locators = populate_models(config)
for model in range(len(self.models)):
self._set_target_inactive(model)
self._search_radius = 10.0
if "search radius" in config:
self._search_radius = config.get("search radius")
self._targets = random_targets(len(self._locators))
self._target_index = 0
self._set_target_active(self._targets[self._target_index])
self._text = VTKCornerAnnotation()
self._text.set_text(["Hello World", "", "",
str(self._targets[self._target_index])])
def _set_target_active(self, index):
self.models[index].actor.GetProperty().SetColor(1.0, 0.0, 0.0)
def _set_target_inactive(self, index):
self.models[index].actor.GetProperty().SetColor(1.0, 1.0, 1.0)
def is_hit(self, tracking):
"""
Checks whether a target has been hit
:param: the tracking data (3D point)
"""
index, distance = point_in_locator(tracking[0:3, 3],
self._locators,
self._search_radius)
if self._target_index < len(self._locators):
self._text.set_text([str(index), str(distance),
str(tracking),
str(self._targets[self._target_index])])
if index == self._targets[self._target_index]:
print("hit")
self._set_target_inactive(self._targets[self._target_index])
self._target_index = self._target_index + 1
if self._target_index < len(self._locators):
self._set_target_active(self._targets[self._target_index])
else:
self._text.set_text([str(index), str(distance),
str(tracking),
str("Finished")])
else:
self._text.set_text([str(index), str(distance),
str(tracking),
str("Finished")])
# coding=utf-8
""" Module for multiplying numbers. """
def multiply_two_numbers(input_x, input_y):
""" Multiply two numbers """
return input_x * input_y
# -*- coding: utf-8 -*-
"""
VTK pipeline to represent a surface model via a vtkPolyData.
"""
import vtk
import sksurgeryvtk.models.vtk_surface_model as vbs
# pylint: disable=no-member
class VTKConeModel(vbs.VTKSurfaceModel):
"""
Class to create a VTK surface model of a cone.
"""
def __init__(self, height, radius, colour, name, visibility=True,
opacity=1.0):
"""
Creates a new surface model.
:param height: the height of the cone
:param diameter: the radius of the cone
:param name: a name for the model
:param colour: (R,G,B) where each are floats [0-1]
:param visibility: boolean, True|False
:param opacity: float [0,1]
"""
super(VTKConeModel, self).__init__(None, colour, visibility,
opacity)
self.name = name
cone = vtk.vtkConeSource()
cone.SetResolution(88)
cone.SetRadius(radius)
cone.SetHeight(height)
cone.Update()
self.source = cone.GetOutput()
#this is from super init, have to redo as we now have data
self.normals = None
self.normals = vtk.vtkPolyDataNormals()
self.normals.SetInputData(self.source)
self.normals.SetAutoOrientNormals(True)
self.normals.SetFlipNormals(False)
self.transform = vtk.vtkTransform()
self.transform.Identity()
self.transform_filter = vtk.vtkTransformPolyDataFilter()
self.transform_filter.SetInputConnection(self.normals.GetOutputPort())
self.transform_filter.SetTransform(self.transform)
self.mapper = vtk.vtkPolyDataMapper()
self.mapper.SetInputConnection(self.transform_filter.GetOutputPort())
self.mapper.Update()
self.actor.SetMapper(self.mapper)
......@@ -14,19 +14,13 @@ def main(args=None):
parser = argparse.ArgumentParser(description='scikit-surgery-evaluation')
## ADD POSITIONAL ARGUMENTS
parser.add_argument("x",
type=int,
help="1st number")
parser.add_argument("-c", "--config",
type=str,
help="A configuration file",
required=True)
parser.add_argument("y",
type=int,
help="2nd number")
# ADD OPTINAL ARGUMENTS
parser.add_argument("-m", "--multiply",
action="store_true",
help="Enable multiplication of inputs."
)
parser.add_argument("-v", "--verbose",
action="store_true",
......@@ -42,4 +36,4 @@ def main(args=None):
args = parser.parse_args(args)
run_demo(args.x, args.y, args.multiply, args.verbose)
run_demo(args.config, args.verbose)
# coding=utf-8
"""Hello world demo module"""
from sksurgeryeval.algorithms import addition, multiplication
def run_demo(input_x, input_y, multiply, verbose):
import sys
from PySide2.QtWidgets import QApplication
from sksurgerycore.configuration.configuration_manager import (
ConfigurationManager
)
from sksurgeryeval.widgets.overlay import OverlayApp
def run_demo(configfile, verbose):
""" Run the application """
if multiply:
result = multiplication.multiply_two_numbers(input_x, input_y)
configurer = ConfigurationManager(configfile)
else:
result = addition.add_two_numbers(input_x, input_y)
app = QApplication([])
configuration = configurer.get_copy()
if verbose:
if multiply:
print("Calculating {} * {}".format(input_x, input_y))
else:
print("Calculating {} + {}".format(input_x, input_y))
print("Starting overlay app")
print("Result is {}".format(result))
overlay = OverlayApp(configuration)
overlay.start()
return result
sys.exit(app.exec_())
# coding=utf-8
"""Main loop for surgery evaluation"""
from math import isnan
from sksurgeryutils.common_overlay_apps import OverlayBaseApp
from sksurgeryeval.algorithms.algorithms import (
configure_tracker, np2vtk, add_map)
from sksurgeryeval.algorithms.background_image import \
OverlayBackground
from sksurgeryeval.shapes.cone import VTKConeModel
from sksurgeryeval.algorithms.locators import Locators
class OverlayApp(OverlayBaseApp):
"""Inherits from OverlayBaseApp, adding code to test the
proximity of a tracked object to a set of vtk objects"""
def __init__(self, config):
"""Overides overlay base app's init, to initialise the
external tracking system. Together with a video source"""
try:
super().__init__(None)
except RuntimeError:
self.update_rate = 30
self.img = None
self.timer = None
self.save_frame = None
if "logo" in config:
self.bg_image = OverlayBackground(config)
else:
default_config = {"logo" : True}
self.bg_image = OverlayBackground(default_config)
self._tracker = None
if "tracker config" in config:
self._tracker = configure_tracker(config.get("tracker config"))
self._tracker_handle = 0
if "tracker handle" in config:
self._tracker_handle = config.get("tracker handle")
self._locator = Locators(config)
maps = add_map(config)
self._pointer = VTKConeModel(10.0, 5.0, (1.0, 1.0, 1.0), "pointer")
self.vtk_overlay_window.add_vtk_actor(self._pointer.actor)
self.vtk_overlay_window.add_vtk_models(self._locator.models)
if maps is not None:
self.vtk_overlay_window.add_vtk_models(maps)
if "camera" in config:
camera_config = config.get("camera")
if "bounding box" in camera_config:
self.vtk_overlay_window.foreground_renderer.ResetCamera(
camera_config.get("bounding box"))
else:
self.vtk_overlay_window.foreground_renderer.ResetCamera(
-300, 300, -300, 300, -200, 0)
self.vtk_overlay_window.add_vtk_actor(self._locator._text.text_actor)
def update(self):
"""Update the background renderer with a new frame,
move the model(s) and render"""
image = self.bg_image.next_image()
#add a method to move the pointer
self._update_tracking()
self.vtk_overlay_window.set_video_image(image)
self.vtk_overlay_window.Render()
def _update_tracking(self):
"""Internal method to move the pointer,
and check it's distance to the various
polydata
"""
port_handles, _, _, tracking, quality = self._tracker.get_frame()
for ph_index, port_handle in enumerate(port_handles):
if port_handle != self._tracker_handle:
continue
if not isnan(quality[ph_index]):
self._pointer.actor.SetUserMatrix(np2vtk(tracking[ph_index]))
self._locator.is_hit(tracking[ph_index])
......@@ -32,7 +32,7 @@ unsafe-load-any-extension=no
# A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code
extension-pkg-whitelist=numpy
extension-pkg-whitelist=numpy, PySide2, vtk
# Allow optimization of some AST trees. This will activate a peephole AST
# optimizer, which will apply various small optimizations. For instance, it can
......
# coding=utf-8
"""scikit-surgery-evaluation tests"""
# Pytest style