ALBULA API

Since version 2.0 ALBULA for Linux has included a Python API. The ALBULA API provides a programming interface for both the displaying of images and the performing of operations and calculations on data. Furthermore, the API enables you to easily integrate the viewer functionality into your beamline infrastructure or experimental setup.

The ALBULA API is available for Linux only and documented in detail in the ALBULA Help section. Please note that the ALBULA API has changes in version 3.0 that are not backwards compatible.  These are outlined at the bottom of this document.

GETTING STARTED

The coding examples below show you how to do common tasks with ALBULA.

Accessing an image series

DImageSeries gives you access to series of image file or HDF5 container files. The images themselves are represented by DImage objects.

import sys
sys.path.insert(0,"/usr/local/dectris/albula/3.0/python") # or wherever you've installed ALBULA
import dectris.albula
series = dectris.albula.DImageSeries()
 
#  In order to access a series of CBF or TIF images do open() on an arbitrary image file
series.open("/path/to/dataset/image_01234.cbf")
img = series[12]   # access image_00012.cbf, returns DImage object
 
# same with TIF images
series.open("/path/to/dataset/image_02344.tif")
 
# same with HDF5 containers
series.open("/path/to/hdf_master.h5")

 

Accessing image data

Image data may be accessed via DImage.pixel(x,y), DImage.setPixel(x,y) or as a NumPy array via DImage.data(). Note that NumPy arrays are in row major order i.e. the y (x) position is accessed by the first (second) index. Starting from version 3.0, DImage.data() returns a copy of the image data. Manipulation of the returned numpy array does no longer affect the DImage itself. You may pass a numpy array to the DImage constructor.

img.setPixel(5,8,100) # set pixel at x=5, y=8 to 100 
p = img.pixel(5,8) # p = 100 
n = img.data() # n is a numpy array in row major order i.e. n[8,5] = 100

 

Writing images to files

Manipulated images can be written to files:

# img is an DImage object that has been opened before. See example above.
# write image back to /tmp/img.tif
dectris.albula.DImageWriter.write(img, "/tmp/img.tif")
# write image back to /tmp/img.cbf
dectris.albula.DImageWriter.write(img, "/tmp/img.cbf")

 

Displaying images

The viewer window consists of a DMainFrame that may contain multiple DSubFrames. Create and display a DMainFrame object and open one DSubFrame inside the DMainFrame.

m = dectris.albula.openMainFrame()
s = m.openSubFrame()
# Either pass path to an image to subFrame
s.loadFile("/path/to/dataset/image_01234.cbf")
# Or load a DImage object created before 
s.loadImage(img)
 
# Since version 2.2 DMainFrame and DSubFrame can be created at the same time
mainFrame, subFrame = dectris.albula.display("/path/to/dataset/image_01234.cbf")

 

Slavemode emulation

Former versions of ALBULA could be run in slavemode permanently polling a file and displaying images found there. The more powerful ALBULA API rendered slavemode obsolete as the example below shows.

import re
import sys
import time
 
sys.path.insert(0,"/usr/local/dectris/albula/3.0/python")
import dectris.albula
mainWin = dectris.albula.openMainFrame()
subWin = mainWin.openSubFrame()
linePattern = re.compile(r"\s*([0-9]+)\s+(\S*)")
fileNumber = None
while 1:
    try:
        with open("/tmp/albula.stat") as f:
            line = f.readline()
            m = linePattern.match(line)
            if m:
                newFileNumber = m.group(1)
                fileName = m.group(2)
                if not fileNumber or newFileNumber > fileNumber:
                    fileNumber = newFileNumber
                    subWin.loadFile(fileName)
    except IOError:
        time.sleep(1)
    # mainWin or subWin has been closed by the user    
    except (dectris.albula.DNoObject, dectris.albula.DNoMethod):
        time.sleep(0.1)
        try:
            subWin =  mainWin.openSubFrame()
        # mainWin has been closed by the user
        except dectris.albula.DNoObject:
            mainWin = dectris.albula.openMainFrame()
            subWin = mainWin.openSubFrame()
    time.sleep(0.2)

 

The script above regularly stats the file /tmp/albula.stat. You may create and regularly update /tmp/albula.stat via the command line:

#!/bin/bash
##  Create /tmp/albula.stat via the command line       
i=0; 
while [ $i -lt 720 ]; do
   sleep 0.1; 
   echo "$i /path/to/imageDir/image_$(printf "%05d" $i).cbf" > /tmp/albula.stat; 
   i=$[i+1]; 
done

 

Working with HDF5 files

Starting from 3.0 there is just one class that gives you access to both HDF5 container files and series of single image files. Class DImageSeries turns the single frames in HDF5 files into DImage objects. Let's say you have in your current working directory the master file "series_16_master.h5" and mulitple data files "series_16_data_000000.h5", "series_16_data_000001.h5",... The only thing you have to do is to open the master file.

h5cont = dectris.albula.DImageSeries("series_16_master.h5")
# loop over the frames in the image series to display them in albula
for i in range(h5cont.first(), h5cont.first() + h5cont.size()): 
    img = h5cont[i]
# All images of the series provide access to the full HDF5 header data, e.g.
img = h5cont[h5cont.first()]
# read header items using convenience functions 
optData = img.optionalData() # DImageOptionalData object
# e.g. wavelength
wavelength = optData.wavelength() 
# threshold energy
threshold_energy = optData.threshold_energy()

 

A common pitfall is to merge lines 6 and 8 of the example above into one expression:

optData_sf = h5cont[0].optionalData() # not valid code -> segmentation fault

 

Accessing the variable optData_sf in the example above leads to a segmentation fault. The reason is that optionalData() returns a reference to an internal DImageOptionalData object that is destroyed as soon as the temporary DImage returned by h5Cont[0] is garbage collected.

 

You may also access the NeXus tree from the DImageSeries:

# Define convenience function to iterate over the NeXus tree
def iterateChildren(node,nodeList=[]):
    """ iterates over the children of a neXus node """
    if node.type() == dectris.albula.DNeXusNode.GROUP: 
        for kid in node.children(): 
            nodeList = iterateChildren(kid,nodeList) 
    else: 
        nodeList.append(node) 
    return nodeList
  
# Read the header item directly from the DImageSeries
neXusHeader = h5cont.neXus()  # NeXus tree of a HDF5 container
neXusRoot = neXusHeader.root()  # root element of the NeXus tree 
# print all header item names with path 
for kid in iterateChildren(neXusRoot): 
    print kid.path() 
 
# extract wavelength 
wavelength = neXusRoot.childElement('/entry/instrument/beam/incident_wavelength') 
print "wavelength value: ",wavelength.value()

 

The following lines let you write image data to an HDF5 file

# write the (uncompressed) images and the neXus header to a new HDF5 file
hdf5Writer = dectris.albula.DHdf5Writer("testContainer.h5",1000, neXusHeader) 

for i in range(h5cont.first(), h5cont.first() + h5cont.size()): 
    img = h5cont[i] 
    hdf5Writer.write(img) 

# flushing closes the master and the data files 
hdf5Writer.flush()

 

Having the examples above in mind, it is a simple exercise to write HDF5 data to a series of CBF files:

# Write the images in the CBF format. 
# Warning: Header information will be lost! 

for i in range(h5cont.first(), h5cont.first() + h5cont.size()): 
    img = h5cont[i] 
    dectris.albula.DImageWriter.write(img, "testImage_{0:05d}.cbf".format(i))
    
# Write the images in the tif format. 
# Be careful: Header information will be lost! 

for i in range(h5cont.first(), h5cont.first() + h5cont.size()): 
    img = h5cont[i] 
    dectris.albula.DImageWriter.write(img, "testImage_{0:05d}.tif".format(i))

 

Changes in Albula API

The following changes are not compatible with version 2 of the ALBULA API:

  • class DImage replaces the classes DImageInt and DImageFloat. class DImage accomodates both integer and float type data. The datatype may be defined by passing to the constructor one of the strings: "int8", "int16", "int32", "int64", "uint8", "uint16", "uint32", "uint64", "float32", "float64". dataType() returns the datatype string.
  • data() returns a copy of the image data as a numpy array. In earlier versions data() used to return a reference to its data. Passing a numeric numpy array to the constructor is the preferred way create a DImage from a numeric numpy array.
  • class DImageSeries replaces the classes DImageIntContainer, DImageFloatContainer, DHdf5IntContainer and DHdf5FloatContainer. DImageSeries determines automatically the image type (e.g. CBF or HDF5) and data type (e.g. 32-bit integer or 32-bit float).
  • classes DNeXusGroup, DNeXusData and DNeXusAttribute replace the generic class DNeXusNode. You will face no changes if you gained accesses to header data only via class DImageOptionalData.