Python Bindings

Elsa also comes with python bindings for almost all aspects of the framework. This short guide aims to give an introduction into some simple cases and explain how one can easily translate C++ code into python code for faster prototyping and experimenting. One major benefit that comes with the python bindings is that we are able to natively use numpy arrays with our elsa data containers making it easy to work with other tools such as matplotlib.

Setup the python bindings

In case you installed elsa via a make install the bindings should work out of the box.

If you do not want to install elsa to your system or are developing locally make sure to add the path to your build folder to the PYTHONPATH for python to be able to find and import the binding code.

Once everything is set up simply

import pyelsa as elsa

2D example

To give a short outline into the python usage of elsa we will recreate the 2D example of the Quickstart section in python.

import pyelsa as elsa
import numpy as np
import matplotlib.pyplot as plt

size = np.array([128, 128])
phantom = elsa.PhantomGenerator.createModifiedSheppLogan(size)
volume_descriptor = phantom.getDataDescriptor()

# generate circular trajectory
num_angles = 180
arc = 360

sino_descriptor = elsa.CircleTrajectoryGenerator.createTrajectory(
    num_angles, phantom.getDataDescriptor(), arc, size[0] * 100, size[0])

# setup operator for 2d X-ray transform
projector = elsa.SiddonsMethod(volume_descriptor, sino_descriptor)

# simulate the sinogram
sinogram = projector.apply(phantom)

# setup reconstruction problem
problem = elsa.WLSProblem(projector, sinogram)

# solve the problem
solver = elsa.CG(problem)
n_iterations = 20
reconstruction = solver.solve(n_iterations)

# plot the reconstruction
plt.imshow(np.array(reconstruction))
plt.show()

As you can see the code is essentially equivalent to the C++ code shown in the Quickstart guide. All the top level elsa modules normally available through

#include "elsa.h"

are available under the top level pyelsa module. All C++ functions and classes essentially have the same signatures. One important aspect of the python bindings is, however, that in places where the C++ code would expect Eigen vectors or matrices we can natively use numpy arrays as well as convert elsa DataContainer back to numpy via

data_container: elsa.DataContainer = ...
back_to_numpy = np.array(data_container)

This is important if we e.g. want to show a reconstruction image using matplotlib as it does not support the elsa data containers.

3D reconstruction viewing

The tight integration with numpy and matplotlib also enables us to directly implement a 3D reconstruction viewer within our experimentation code. Let’s take a simple 3D phantom reconstruction example using a CUDA projector to be more performant

import pyelsa as elsa
import numpy as np
import matplotlib.pyplot as plt

size = np.array([64, 64, 64])  # 3D now
phantom = elsa.PhantomGenerator.createModifiedSheppLogan(size)
volume_descriptor = phantom.getDataDescriptor()

# generate circular trajectory
num_angles = 180
arc = 360

sino_descriptor = elsa.CircleTrajectoryGenerator.createTrajectory(
    num_angles, phantom.getDataDescriptor(), arc, size[0] * 100, size[0])

# setup operator for 2d X-ray transform
projector = elsa.JosephsMethodCUDA(volume_descriptor, sino_descriptor)

# simulate the sinogram
sinogram = projector.apply(phantom)

# setup reconstruction problem
problem = elsa.WLSProblem(projector, sinogram)

# solve the problem
solver = elsa.CG(problem)
n_iterations = 20
reconstruction = np.array(solver.solve(n_iterations))

We can now implement a simple matlab viewer plugin to scroll through our 3D reconstruction using the mouse wheel as shown in the matplotlib documentation

class IndexTracker:
    def __init__(self, ax, X):
        self.ax = ax
        ax.set_title('use scroll wheel to navigate images')

        self.X = X
        rows, cols, self.slices = X.shape
        self.ind = self.slices // 2

        self.im = ax.imshow(self.X[:, :, self.ind])
        self.update()

    def on_scroll(self, event):
        if event.button == 'up':
            self.ind = (self.ind + 1) % self.slices
        else:
            self.ind = (self.ind - 1) % self.slices
        self.update()

    def update(self):
        self.im.set_data(self.X[:, :, self.ind])
        self.ax.set_ylabel('slice %s' % self.ind)
        self.im.axes.figure.canvas.draw()

we then simply set up our viewer

fig, ax = plt.subplots(1, 1)
tracker = IndexTracker(ax, reconstruction)
fig.canvas.mpl_connect('scroll_event', tracker.on_scroll)
plt.show()

and scroll through our 3D reconstruction.