PyAnatomist tutorial

Programming with Anatomist in Python language

Anatomist is written in C++ language but has also a python API: PyAnatomist. This python API enables to drive Anatomist application through python scripts : using Anatomist, loading volumes, meshes, graphs, viewing them in windows, merging objects, changing color palettes, etc… It can be useful in order to automate a repetitive visualization task or to add features to Anatomist application by developping a python plugin, which is much more easier than developping directly in Anatomist C++ library. Actually, several features in Anatomist have been added via a python plugin like the gradient palette for example. The python API is also used by several viewers in BrainVISA.

Description of the API

The main entry point is the Anatomist class which must be instantiated before any operation can be performed. It represents Anatomist application. This class contains a number of nested classes: AObject, AWindow… that represent the elements of Anatomist application.

This notebook is using Qt gui, so first of all let’s do:

[1]:
# test if we are inside a notebook: if yes, use the nbanatomist implementation
import sys
if 'ipywidgets' in sys.modules:
    import anatomist
    anatomist.setDefaultImplementation('notebook')
else:
    import os
    os.environ["QT_API"] = "pyqt5"
    %gui qt

The entry point of this API is the module anatomist.api, you can import it as below:

[2]:
import anatomist.api as anatomist

And then create an instance of Anatomist:

[3]:
a = anatomist.Anatomist()
VirtualGL found.
VirtualGL should work.
Running through VirtualGL + Xvfb: this is optimal.
global modules: /casa/host/build/build_files/anatomist-gpl/pyanatomist/../../../share/anatomist-5.1/python_plugins
home   modules: /casa/home/.anatomist/python_plugins
loading module meshsplit
loading module modelGraphs
loading module histogram
loading module bundles_split_by_cortical_regions
loading module palettecontrols
loading module paletteViewer
loading module ana_image_math
Anatomist started.
loading module simple_controls
loading module bsa_proba
loading module selection
loading module bundles_small_brains
loading module anacontrolmenu
loading module gradientpalette
loading module foldsplit
loading module profilewindow
loading module save_resampled
loading module volumepalettes
loading module gltf_io
all python modules loaded
Anatomist started.

So you can send commands to Anatomist application, for example creating a window:

[4]:
window = a.createWindow('3D')

Implementation

Several means of driving Anatomist in python scripts exist :

  • Python bindings for the C++ library (SIP bindings). In this mode, all bound features are available. You handle directly the Anatomist objects. But Anatomist is loaded directly in your python program, it is not an independent program in this mode. So the errors that occur in Anatomist are reported in the python script.

  • Sending commands via a network socket. Anatomist is run in server mode and listen for commands on the socket connection. In this mode, the available features are limited to what can be expressed with Anatomist commands system, so a limited set of features. A list of these commands can be found here (main page in french, sorry). You cannot handle directly the Anatomist objects, they are represented by a key. But Anatomist application runs in a separate process so potential errors in Anatomist don’t crash the application that uses the API.

Behind a general interface, this api provides several implementations, distinguished by their method of communication with Anatomist, or with the rendering modes (GUI, headless, notebook).

The implementation that uses the python bindings is called direct implementation and is in the module anatomist.direct.api. The implementation that uses socket communication is called socket implementation and is in the module anatomist.socket.api anatomist.socket.api. By default, the implementation used when you import anatomist.api is the direct implementation.

A third implementation is a thread-safe variant of the direct module: anatomist.threaded.api module. It redirects every call to the API to be actually called from the main thread. It is obviously slower.

Another specific implementation for Brainvisa also exists: brainvisa.anatomist module. It enables to use brainvisa database information on loaded objects to automatically load associated referentials and transformations. It uses the same api, so it is possible to switch from one implementation to the other.

By default, the brainvisa module uses the threaded implementation. But it is possible to switch to the socket implementation in the configuration options.

Using the API

In this part, we will use the same story as in the anatomist tutorial and see how to do the same with a python script. The data for the examples in this section are the same: https://brainvisa.info/download/data/demo_data.zip.

The following examples use the general API functions that are available in both direct and socket implementations. To run the following examples, we will use an interactive python shell IPython. It is much more practical than the classic python shell because it offers useful features like automatic completion. This program is available in the BrainVISA package with all other executable programs in the bin directory of the BrainVISA package directory. IPython should be run with the option –gui=qt, which runs a Qt event loop, needed for the graphical interface.

<brainvisa_installation_directory>/bin/ipython --gui=qt

The following code is to work in a temporary directory, download and uncompress the demo data.

[5]:
from __future__ import print_function
from six.moves.urllib.request import urlopen
from soma import aims
import zipfile
import os
import os.path
import tempfile
# let's work in a temporary directory
install_dir = os.path.join(aims.carto.Paths.globalShared(), 'brainvisa_demo')
demo_data = os.path.join(install_dir, 'demo_data.zip')
dl_url = 'https://brainvisa.info/download/data/demo_data.zip'
temp_dirs = []
older_cwd = os.getcwd()
if os.path.exists(install_dir):
    print('the data directory already exists. Assuming it is OK.')
else:
    try:
        os.makedirs(install_dir)
    except:
        # maybe we don't have write permission in the directory
        # try again in a temp directory
        install_dir = tempfile.mkdtemp(prefix='pyanatomist_tutorial_')
        temp_dirs.append(install_dir)

    # download demo data
    with urlopen(dl_url) as f:
        with open(demo_data, 'wb') as g:
            g.write(f.read())
    # extract the archive
    os.chdir(install_dir)
    with zipfile.ZipFile(demo_data) as f:
        f.extractall()
    del f
print('we are working in:', install_dir)
os.chdir(install_dir)
the data directory already exists. Assuming it is OK.
we are working in: /casa/host/build/build_files/anatomist-gpl/pyanatomist/../../../share/brainvisa_demo

Run Anatomist

First of all, we need to import anatomist API module. Here, in a Python shell, the default implementation is the direct one (Python bindings).

Then we can create an instance of Anatomist class.

[6]:
import anatomist.api as ana

a = ana.Anatomist()

Load an object

In this example, we will put the path to the data_for_anatomist directory in a variable named src. We will use this variable in all the next examples. We will also load a python module named os which has useful functions to handle files and paths.

[7]:
import os
src = install_dir
# Load an object
t1mri = a.loadObject(os.path.join(src, "data_for_anatomist", "subject01", "subject01.nii"))

See the corresponding actions in the graphical interface.

View an object

We open an axial window and add the volume loaded in the previous example in this window.

[8]:
# view an object
axial1 = a.createWindow("Axial")
axial1.addObjects(t1mri)

See the corresponding actions in the graphical interface.

When opening a new window, it is possible to change its initial position and size with the geometry parameter. This parameter is a list of 4 values (in pixels) : [x, y, width, height].

[9]:
# customizing the position and size of a new window:
# the windows will be displayed at position (100, 150)
# with a size of (300, 300)
w = a.createWindow("Axial", geometry=[100, 150, 300, 300])

Windows block

We create 4 views in the same windows block and add the image in each views.

[10]:
# view an object in a 4 views block
block = a.createWindowsBlock(2) # 2 columns
w1 = a.createWindow("Axial", block=block)
w2 = a.createWindow("Sagittal", block=block)
w3 = a.createWindow("Coronal", block=block)
w4 = a.createWindow("3D", block=block)
t1mri.addInWindows([w1, w2, w3, w4])

Move the cursor

[11]:
# show the cursor position
print("\nCursor at : ", a.linkCursorLastClickedPosition())
# move the linked cursor
w1.moveLinkedCursor([150, 100, 60])
print("\nCursor at : ", a.linkCursorLastClickedPosition())

Cursor at :  <soma.aims.AimsVector_FLOAT_3 object at 0x7f940c0d6710>
[0.0, 0.0, 0.0]

Cursor at :  <soma.aims.AimsVector_FLOAT_3 object at 0x7f940c0d6710>
[150.0, 100.0, 60.0]

Camera

This method enables to change the camera parameters of a window. In this example, we change the zoom.

[12]:
w4.camera(zoom=2)

You can also change the rotation of the view by changing the view_quaternion parameters. The rotation is represented by a quaternion which is a vector with 4 parameters.

As the interpretation of quaternions is not easy at first time, it is useful to look at the current value of the parameter in a window with the method getInfos.

[13]:
w4.getInfos()
w4.camera(view_quaternion=[0.9, -0.2, -0.2, 0.3])

It is possible to change the orientation of the slice plane with the parameter slice_quaternion.

[14]:
w4.camera(slice_quaternion=[0.3, 0.3, 0, 0.9])

See the corresponding actions in the graphical interface.

View header information

[15]:
# view header
browser = a.createWindow("Browser")
browser.addObjects(t1mri)

See the corresponding actions in the graphical interface.

Change the color palette

[16]:
# color palette
palette = a.getPalette("Blue-Green-Red-Yellow")
t1mri.setPalette(palette)

See the corresponding actions in the graphical interface.

Custom palette

You can create a new palette by giving the list of RGB parameters of the palette colors.

[17]:
# custom palette
customPalette = a.createPalette("customPalette")
colors = []
for x in range(255):
    colors.extend([0, 0, x])

customPalette.setColors(colors=colors)
t1mri.setPalette(customPalette)

View meshes

[18]:
# view meshes
lwhite = a.loadObject(os.path.join(src, "data_for_anatomist", "subject01", "subject01_Lwhite.mesh"))
rwhite = a.loadObject(os.path.join(src, "data_for_anatomist", "subject01", "subject01_Rwhite.mesh"))
w3d = a.createWindow("3D")
w3d.addObjects([lwhite, rwhite])

See the corresponding actions in the graphical interface.

Superimposing

[19]:
# superimposing
w3d.addObjects(t1mri)
w3d.camera(view_quaternion=[0.403016120195389, 0.20057387650013, 0.316507339477539, 0.834967792034149])

See the corresponding actions in the graphical interface.

Change mesh material

[20]:
# mesh material
head = a.loadObject(os.path.join(src, "data_for_anatomist", "subject01", "subject01_head.mesh"))
head.addInWindows(w3d)
material = a.Material(diffuse=[0.8, 0.6, 0.6, 0.5])
head.setMaterial(material)

See the corresponding actions in the graphical interface.

Fusion between two volumes

[21]:
brain_mask = a.loadObject(os.path.join(src, "data_for_anatomist", "subject01", "brain_subject01.nii"))
brain_mask.setPalette("GREEN-ufusion")
t1mri.setPalette("B-W LINEAR")
# fusion 2D
fusion2d = a.fusionObjects([brain_mask, t1mri], "Fusion2DMethod")
axial2 = a.createWindow("Axial")
axial2.addObjects(fusion2d)
# params of the fusion : linear on non null
a.execute("Fusion2DParams", object=fusion2d, mode="linear_on_defined", rate=0.4)
[21]:
<anatomist.cpp.anatomist.RegularCommand at 0x7f940c0d7e20>

It is possible to do different types of fusions using the same method Anatomist.fusionObjects and changing the list of objects and the type of fusion.

See the corresponding actions in the graphical interface.

Load a transformation

[22]:
t1mri_s2 = a.loadObject(os.path.join(src, "data_for_anatomist", "subject02", "sujet02.ima"))
t1mri_s2.setPalette("Blue-White")
fusion2d = a.fusionObjects([t1mri, t1mri_s2], "Fusion2DMethod")
r1 = a.createReferential()
r2 = a.createReferential()
cr = a.centralRef
t1mri.assignReferential(r1)
t1mri_s2.assignReferential(r2)
# load a transformation
a.loadTransformation(os.path.join(src, "data_for_anatomist", "subject01", "RawT1-subject01_default_acquisition_TO_Talairach-ACPC.trm"), r1, cr)
a.loadTransformation(os.path.join(src, "data_for_anatomist", "subject02", "RawT1-sujet02_200810_TO_Talairach-ACPC.trm"), r2, cr)
axial3 = a.createWindow("Axial")
axial3.addObjects(fusion2d)

See the corresponding actions in the graphical interface.

Load an existing referential

[23]:
lwhite.assignReferential(r1)
axial3.addObjects(lwhite)

See the corresponding actions in the graphical interface.

Load referential information from file header

[24]:
map = a.loadObject(os.path.join(src, "data_for_anatomist", "subject01", "Audio-Video_T_map.nii"))
map.setPalette("tvalues100-200-100")
t1mri.loadReferentialFromHeader()
map.loadReferentialFromHeader()
fusion_map = a.fusionObjects([map, t1mri], "Fusion2DMethod")
axial4 = a.createWindow("Axial")
axial4.addObjects(fusion_map)
a.execute("Fusion2DParams", object=fusion_map, mode="linear_on_defined", rate=0.5)
[24]:
<anatomist.cpp.anatomist.RegularCommand at 0x7f93c8e13250>

See the corresponding actions in the graphical interface.

Display a ROI graph

[25]:
graph = a.loadObject(os.path.join(src, "data_for_anatomist", "roi", "basal_ganglia.arg"))
w5 = a.createWindow('3D')
w5.addObjects(graph, add_graph_nodes=True)

See the corresponding actions in the graphical interface.

Sending a command to Anatomist

A lot of commands that can be processed by Anatomist are encapsulted in Anatomist class methods. But some commands, less commonly used are not available through specific methods. Nevertheless, they can be called through a generic method Anatomist.execute.

The list of available commands is listed in the following page: anatomist commands.

In the previous examples, we use this method to call the Fusion2DParams command which is not encapsulated in a specific method.

[26]:
a.execute("Fusion2DParams", object=fusion_map, mode="linear_on_defined", rate=0.5)
[26]:
<anatomist.cpp.anatomist.RegularCommand at 0x7f93c8e6f910>

Using the direct implementation

When you use the direct implementation of Anatomist Python API, more features are available. Indeed, each C++ class and function that are declared in the SIP bindings are available in Python. Moreover, the Python script and Anatomist objects share memory, so lower level manipulations are possible.

See the Anatomist doxygen documentation about the C++ API to have more information about the classes and functions that may be available in Python (if the bindings are done).

In this mode, it is also possible to interact on objects directly and interactively with Python Aims API. See more details about that in PyAnatomist / PyAIMS tutorial: mixing Anatomist and AIMS in Python language..

Amongst the noticeable benefits of the direct implementation is the possibility to use Anatomist views as Qt widgets to build custom interfaces. This is what has been used to program the AnaSimpleViewer application. See the Simple Viewer example.

At the end, cleanup the temporary working directory

[27]:
# cleanup data
import shutil
os.chdir(older_cwd)
for d in temp_dirs:
    shutil.rmtree(d)