Table of Contents
List of Figures
ThresholdGadget
experimentTable of Contents
The Gadgetron is a streaming data processing framework for medical image reconstruction. It has been developed to enable easy prototyping, testing, and deployment of new image reconstruction algorithms. The framework contains a wide range of toolboxes with common data structures and algorithms that can be used within the streaming framework itself or as shared libraries in standalone/3rd party applications.
This document serves as an introduction to the Gadgetron framework and provides some "getting started" examples of using it. A scientific paper is also available [HANSEN12].
Although the Gadgetron is a generic, multi-modality image reconstruction framework, it was initially developed to support the work of the authors in the field of advanced MRI reconstruction. Specifically to support work on fast image reconstruction, not only on traditional CPU architectures, but also using commodity graphics hardware (GPUs). Some examples that are made publicly available through the Gadgetron release include fast regridding on the GPU [SANGILD08], Cartesian parallel imaging on the GPU [HANSEN08], and non-Cartesian parallel imaging on the GPU [SANGILD09].
The Gadgetron is available as a cross-platform source code
distribution, which compiles and has been tested to run on Linux, Mac OS
X, and Windows 7. Compilation instructions for these platforms are
provided below. Generally speaking, the Gadgetron is easiest set up on
Linux since all dependencies are readily available. If you want to get
started quickly with the Gadgetron and happen to not be using Linux, it
is easy to install Ubuntu (our preferred Linux distribution) in a
virtual machine (e.g. VirtualBox, https://www.virtualbox.org/)
and follow the Linux compilation instructions below.
The Gadgetron is available from the project Sourceforge website:
http://sourceforge.net/projects/gadgetron
This manual is available in HTML form at:
http://gadgetron.sourceforge.net/latest/manual/gadgetron_manual.html
Or in PDF form at:
http://gadgetron.sourceforge.net/latest/manual/gadgetron_manual.pdf
API documentation (generated with Doxygen) is available from:
http://gadgetron.sourceforge.net/latest/api/
The Gadgetron depends on a number of libraries that can either be downloaded for free or that may already be part of the installation on your workstation. If you are working on a Linux platform you should be able to install all dependencies without compiling anything. The following is a list of the components that you will need. Some are optional.
To install these components please follow the platform specific installation instructions provided below (Section 1.3, “Compiling and Installing Gadgetron”).
CMake. The Gadgetron uses
cmake for cross platform building.
Available from http://www.cmake.org/cmake/resources/software.html.
ADAPTIVE Computing Environment (ACE).
Available from http://www.cs.wustl.edu/~schmidt/ACE.html.
Boost C++ Libraries. Available from
http://www.boost.org/.
FFT3W Library for Fast Fourier
Transforms. Available from http://www.fftw.org/.
BLAS and LAPACK (optional). Most Linux distributions come with these libraries and they are included on Mac OS X as well, but the vendor depends on your distribution and platform. See specific instructions below for Windows.
Python (optional). Python is included
with Mac OS X and Linux. On Windows you will need to install
Python in order to use Python prototyping with the Gadgetron.
Python is available from http://www.python.org/.
Along with Python you need numpy (and you probably want to have
SciPy) as well.
CUDA (optional). For GPU support, you
need CUDA from Nvidia which can de downloaded from http://developer.nvidia.com/cuda-toolkit-40.
You will need a CUDA driver for your graphics card too, which is
available from the same website. On Ubuntu 11.10 (or newer) the
driver is included with your distribution. See the specific
installation guide for your platform.
CULA (optional). We use CULA for LAPACK
routines on the GPU. This is the only dependency which is not Open
Source. You can however download a free version of CULA from http://www.culatools.com/downloads/dense/.
Registration is required.
QT4 (optional). A few standalone and
Gadgetron client example applications use QT for creating user
interfaces. It comes packaged with most Linux distributions, but
can otherwise be obtained from http://qt.nokia.com/ for
all platforms.
Doxygen (optional). If you would like
to build the API documentation you need Doxygen. It is included in
most Linux distributions. It is available from http://www.stack.nl/~dimitri/doxygen/.
Docbook (optional). If you would like
to build the manual (this document), you need Docbook. A number of
tools are needed such as xsltproc and
fop (for the PDF version of the
library). Additionally, you need the Docbook stylesheets, which
are included in most Linux distributions and otherwise available
at http://docbook.sourceforge.net/.
Git (optional). We use
git to manage our source code archives.
You can use any source code management system you prefer (or none
at all), but if you would like to stay in line with the Gadgetron
team, use git. Available from http://git-scm.com/.
Linux is preferred operating system to get started with the
Gadgetron. All of the required dependencies are included in most major
Linux distributions and can be installed easily and without having to
compile anything. In the following sections we walk you through the
required steps to set up a full Gadgetron installation. We assume that
you are starting with a freshly installed Ubuntu 11.10 available from
the Ubuntu website (http://www.ubuntu.com). If
you don't have a machine available for installing Ubuntu, you can
always try it out in a virtual machine using virtualization software
such as VirtualBox (https://www.virtualbox.org/).
If you would like to use the GPU components included in the Gadgetron and you have a GPU available on your system, please complete the CUDA/CULA installations as described in Section 1.3.1.1, “Installing GPU components (CUDA and CULA) on Linux”.
First install all dependencies for Gadgetron. The following will install everything you need:
user@mycomputer:~$sudo apt-get install doxygen cmake \ libqt4-dev libglew1.6-dev \ docbook5-xml docbook-xsl-doc-pdf \ docbook-xsl-doc-html docbook-xsl-ns xsltproc \ fop git-core libboost-dev libboost-python-dev \ libfftw3-dev libace-dev python-dev python-numpy \ freeglut3-dev libxi-dev liblapack-dev build-essential
Now download the Gadgetron archive and unpack it somewhere. If you have access to a git repository, you can get the code with:
user@mycomputer:~$git clone ssh://user@hostname/path/gadgetron.git
Configure and build the Gadgetron:
user@mycomputer:~$cd gadgetronuser@mycomputer:~/gadgetron$mkdir builduser@mycomputer:~/gadgetron$cd builduser@mycomputer:~/gadgetron/build$cmake ../user@mycomputer:~/gadgetron/build$make
Install (default location is
/usr/local/gadgetron):
user@mycomputer:~/gadgetron/build$sudo make install
The final step is to add/modify a few environment variables in
your ~/.bashrc file.
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/gadgetron/lib export PATH=$PATH:/usr/local/gadgetron/bin export GADGETRON_HOME=/usr/local/gadgetron
You are now set up to run a simple example reconstruction as outlined in Section 1.4, “Hello Gadgetron: Your First Image Reconstruction”.
First install your Nvidia driver. As of Ubuntu 10.11, it is no longer necessary to install the developers driver from Nvidia. The one included with Ubuntu supports CUDA. To install type:
user@mycomputer:~$sudo apt-get install nvidia-current \ nvidia-current-dev nvidia-current-updates \ nvidia-current-updates-dev
We need to install gcc 4.4 since Ubuntu comes preconfigured with gcc 4.6, which is not compatible with the current versions of the CUDA nvcc compiler.
user@mycomputer:~$sudo apt-get install \ gcc-4.4 g++-4.4 build-essential
Set up alternative systems to allow easy switching between the two versions of gcc/g++
user@mycomputer:~$sudo update-alternatives \ --install /usr/bin/gcc gcc /usr/bin/gcc-4.6 40 \ --slave /usr/bin/g++ g++ /usr/bin/g++-4.6user@mycomputer:~$sudo update-alternatives \ --install /usr/bin/gcc gcc /usr/bin/gcc-4.4 60 \ --slave /usr/bin/g++ g++ /usr/bin/g++-4.4
Check your gcc compiler (should now be version 4.4.6):
user@mycomputer:~$gcc -v
When you want to switch between the two compiler versions:
user@mycomputer:~$sudo update-alternatives --config gcc
The final step is to actually install CUDA and CULA. Download the following files:
cudatoolkit_4.0.17_linux_64_ubuntu10.10.run
from http://developer.nvidia.com/cuda-toolkit-40
cula_dense_free_R13a-linux64.run from
http://www.culatools.com/downloads/dense/
(free registration requited)
Go to the folder where the files were downloaded and type:
user@c:~$chmod +x cudatoolkit_4.0.17_linux_64_ubuntu10.10.runuser@c:~$sudo ./cudatoolkit_4.0.17_linux_64_ubuntu10.10.runuser@c:~$chmod +x cula_dense_free_R13a-linux64.runuser@c:~$sudo ./cula_dense_free_R13a-linux64.run
Follow the instructions. When you are done with the
installation you may want to add the following to your
~/.bashrc file.
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/cuda/lib64 export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/cuda/lib export CULA_ROOT="/usr/local/cula" export CULA_INC_PATH="$CULA_ROOT/include" export CULA_BIN_PATH_32="$CULA_ROOT/bin" export CULA_BIN_PATH_64="$CULA_ROOT/bin64" export CULA_LIB_PATH_32="$CULA_ROOT/lib" export CULA_LIB_PATH_64="$CULA_ROOT/lib64" export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$CULA_LIB_PATH_64
You are now ready to compile and run CUDA (and CULA) applications. You may want to download the CUDA SDK from Nvidia to validate your installation but this is not required.
The following instructions assume that you are starting on a Mac with OS X 10.6.8 (Snow Leopard) Installed. Additionally it assumes that you have Xcode (3.2.6) installed. If you have upgraded to Lion or are on an older release, you should still be able to make it all compile, but you may have to make some adjustments.
We will use MacPorts (http://www.macports.org/)
to install the required dependencies. You may use a different package
management system or prefer to install packages manually. In that
case, please look at the list of dependencies (Section 1.2.1, “Dependencies”) and install the required dependencies
for the components you would like to use.
MacPorts is not the fastest way to install packages. We use this methods here to make it easier to follow the instructions. Please be patient when running the port commands.
Install MacPorts.
Download MacPorts-2.0.3.pkg from http://www.macports.org/.
Run sudo port -v selfupdate to make sure you up to date.
Get your Python installation up to date. Mac OS X ships with Python installed, but it is not a complete distribution and if you would like to do Python development with the Gadgetron, you need to update. If you are already using Python for signal processing, you may already have the required components installed. If you already have numpy and SciPy installed, you may be able to skip this step. If you do not wish to use Python, you can also skip this step.
$ sudo port install python27 py27-numpy py27-scipyThis should install Python 2.7. Now select Python 2.7 as as the active Python installation:
$ sudo port select python python27Now to make sure the build system finds the right version of Python we need to edit a couple of symbolic links manually:
$cd /System/Library/Frameworks/Python.framework/Versions$sudo ln -s /opt/local/Library/Frameworks/Python.framework/Versions/2.7$sudo rm Current$sudo ln -s 2.7 Current
Install Boost. Boost gets special treatment here. Depending on whether you would like to do Python development, you need to install Boost with or without boost_python. If you would like Python:
$ sudo port install boost +python27If you don't need Python support:
$ sudo port install boostNow we can install the rest of the packages:
$ sudo port install git-core cmake libACE \
fftw-3-single fftw-3 qt4-mac-develThis may take quite a long time (hours).
Install CUDA and CULA. If you would like to use the GPU components, you need to install the following:
The Nvidia development driver
(devdriver_4.0.50_macos.dmg) from http://developer.nvidia.com/cuda-toolkit-40/.
The CUDA Toolkit
(cudatoolkit_4.0.17_macos.pkg) from http://developer.nvidia.com/cuda-toolkit-40/.
The CULA Dense Libraries
(cula_dense_free_R13a-osx.dmg) from http://www.culatools.com/downloads/dense/.
Compiling the Gadgetron:
$cd gadgetron$mkdir build$cd build$cmake -DPYTHON_NUMPY_INCLUDE_DIR= \ /opt/local/Library/Frameworks/Python.framework \ /Versions/2.7/lib/python2.7/site-packages/numpy/core/include ../$make$sudo make install
The long path for the numpy header files is only needed if you want Python support. You can avoid this by creating a symbolic link:
$cd /opt/local/Library/Frameworks/Python.framework$cd Versions/2.7/include/python2.7$sudo ln -s ../../lib/python2.7/site-packages/numpy/core/include/numpy
After creating this link you should be able to compile with the following:
$cd gadgetron$mkdir build$cd build$cmake ../$make$sudo make install
Set environment variables:
$export GADGETRON_HOME=/usr/local/gadgetron$export PATH=$PATH:/usr/local/gadgetron/bin$export DYLD_LIBRARY_PATH=$DYLD_LIBRARY_PATH:/usr/local/gadgetron/lib
You may wish to add these lines to
~/.bash_profile, You may also want to add
paths to CUDA and CULA libraries if you are using those:
$export DYLD_LIBRARY_PATH=$DYLD_LIBRARY_PATH:/usr/local/cula/lib64$export DYLD_LIBRARY_PATH=$DYLD_LIBRARY_PATH:/usr/local/cuda/lib
Test your Gadgetron by following the instructions in Section 1.4, “Hello Gadgetron: Your First Image Reconstruction”.
It is probably appropriate to start this section with a warning: Windows is not the easiest environment in which to work with the Gadgetron. As indicated in Section 1.2.1, “Dependencies”, the Gadgetron relies on multiple external libraries. Many of those libraries are not available as easy install packages and must be compiled separately. If you are uncomfortable setting up development tools on Windows, or if you are just looking for a fast and easy way to get started with the Gadgetron, we recommend installing on Ubuntu Linux - possibly using a virtual machine inside Windows (see Section 1.3.1, “Linux Installation Instructions”).
The following is a list of steps we have used to install the Gadgetron on a clean Windows 7 (64-bit) machine:
Install Visual Studio 2010 (with Service Pack 1)
Install CUDA/CULA (optional, but required for GPU support).
Download from http://developer.nvidia.com/cuda-toolkit-40):
Install Nvidia Developer Driver (Version 270.81)
Install Nvdia Toolkit (4.0)
Install gpucomputingsdk (Install ad Asministrator)
Install CUDA4_0BuildCustomizationFix
Install cula_R12-win64.exe (as
Administrator). Download from
http://www.culatools.com/downloads/dense/. R13 (dense)
should work too, but has not been tested on Windows. Assuming CULA
was installed in C:\Program Files\CULA\R12,
add C:\Program Files\CULA\R12\bin64 to your
PATH environment variable.
Create a folder for external libraries, say
C:\Libraries.
Install FFTW3 (http://www.fftw.org/install/windows.html)
Copy FFTW3 binaries to
C:\Libraries\FFTW3
Create *.lib files, on the command line type:
c:\Libraries\FFTW3>lib /machine:x64 /def:libfftw3f-3.def c:\Libraries\FFTW3>lib /machine:x64 /def:libfftw3-3.def c:\Libraries\FFTW3>lib /machine:x64 /def:libfftw3l-3.def
Add C:\Libraries\FFTW3 to path.
Install ACE (http://download.dre.vanderbilt.edu/)
Unpack ACE into C:\Libraries\ACE-6.0.5\ACE_wrappers
Add config.h in
ACE_ROOT/ace/ with the following
content:
//We are on Windows #include "ace/config-win32.h" //This ensured that INLINE settings //do not vary between Debug and Release modes #define ACE_NO_INLINE
Open the VS 2010 project in the source code archive
Set build type to Release/x64
Build (this takes a while)
Add to PATH:
C:\Libraries\ACE-6.0.5\ACE_wrappers\lib
Install Python (optional). Regrettably, the off-the-shelf Python header files cannot be compiled in debug mode on Windows. This enforces the Gadgetron to be compiled in release mode.
Install python-2.7.2.amd64 (http://www.python.org)
Add install folder (e.g. C:\Python27)
to PATH environment variable
Add PYTHON_ROOT environment
variable
From http://www.lfd.uci.edu/~gohlke/pythonlibs/
download and install the following:
numpy-MKL-1.6.1.win-amd64-py2.7
scipy-0.10.0.win-amd64-py2.7
matplotlib-1.1.0.win-amd64-py2.7
(not strictly necessary)
ipython-0.11.win-amd64-py2.7 (not
strictly necessary)
Install ACML (BLAS and LAPACK)
Download acml4.4.0-win64.exe From: http://developer.amd.com/libraries/acml/downloads/pages/
Install Library in say C:\Libraries\C:\Libraries\acml4.4.0
Add
C:\Libraries\acml4.4.0\win64\lib;C:\Libraries\acml4.4.0\win64_mp\lib
to PATH
Install Boost (http://www.boost.org)
Unpack boost to
C:\Libraries\boost_1_48_0
Open a command line as Administrator (type cmd and hit "shift+ctrl + enter")
From C:\Libraries\boost_1_48_0 type
"bootstrap" (not bootstrap.bat)
Type .\b2 --with-python --build-type=complete address-model=64 (This only builds the boost_python library, add others as needed)
Add C:\Libraries\boost_1_48_0\stage\lib
to PATH
Install git (if you are using source code management):
Run installation package Git-1.7.7.1-preview20111027 (As
Administrator), download from http://code.google.com/p/msysgit/
Use run in git bash only option
Use checkout Windows LF and commit Unix Line feeds
Install CMake (http://www.cmake.org/cmake/resources/software.html)
Run cmake-2.8.6-win32-x86 (As Administrator)
Download and unpack Gadgetron source code.
Create Visual Studio project (your process may vary):
Start cmake-gui
Select source and target directories
Hit configure (first time)
Add variable BOOST_ROOT to point to BOOST folder
Hit configure (again)
Specify location of FFTW and FFTWf libraries
Hit configure (again)
Specify location of CULA (include and library)
Specify location of ACE include dir
Hit configure (again)
Specify PYTHON_NUMPY_INCLUDE_DIR =
C:/Python27/Lib/site-packages/numpy/core/include
Hit configure (again)
Specify the following environment variables:
BLAS_CALBLAS= \ C:/Libraries/acml4.4.0/win64/lib/libacml.lib BLAS_acml_LIBRARY= \ C:/Libraries/acml4.4.0/win64/lib/libacml_dll.lib BLAS_acml_mp_LIBRARY= \ C:/Libraries/acml4.4.0/win64_mp/lib/libacml_mp_dll.lib BLAS_acml_mv_LIBRARY= \ C:/Libraries/acml4.4.0/win64/lib/libacml_mv_dll.lib
Hit configure
Hit "generate"
You should now have a visual studio project that you can open and build (try Release/x64 mode and try the install target).
You now have a working installation of the Gadgetron in Windows. Follow the instructions below to run a simple reconstruction example (Section 1.4, “Hello Gadgetron: Your First Image Reconstruction”).
Before attempting to run any reconstructions, please set the
environment variable GADGETRON_HOME to point to the
installation folder of your Gadgetron installation and make sure that
the paths of all dependencies are in your PATH
environment variable.
Some basic sample datasets are available from the Sourceforge website:
https://sourceforge.net/projects/gadgetron/files/testdata/
In this example we choose the
simple_gre.tar.gz dataset from the
mri section.
Start by downloading the dataset and unpacking it. On Linux or Mac you would type:
user@mycomputer:~/temp/test_data$tar -xzvf simple_gre.tar.gz
There are two files in the dataset;
simple_gre.gmr and
simple_gre.xml.
Open two terminal windows to observe both client and Gadgetron communication output. In the Gadgetron terminal window simply type:
user@mycomputer:~/temp/gadgetron_out$gadgetron
In the client window (in the folder where you just unpacked your data) type:
user@mycomputer:~/temp/test_data$mriclient \ -d simple_gre.gmr \ -x simple_gre.xml \ -c default.xml
You should now see some logging information both in the Gadgetron window and in the client window. Specifically, you should see that images are being returned from the Gadgetron:
user@mycomputer:~/temp/test_data$ mriclient \
-d simple_gre.gmr \
-x simple_gre.xml \
-c default.xml
Gadgetron MRI Data Sender
-- host: localhost
-- port: 9002
-- data: simple_gre.gmr
-- parm: simple_gre.xml
-- conf: default.xml
-- loop: 1
(2815|140119753992000) Connection from 127.0.0.1:9002
Image Writer writing image
Image Writer writing image
Image Writer writing image
Image Writer writing image
Image Writer writing image
Image Writer writing image
Image Writer writing image
Image Writer writing image
Image Writer writing image
Image Writer writing image
2815, 81, GadgetronConnector, Close Message received
(2815|140119714780928) Handling close...
(2815|140119714780928) svc done...
(2815|140119714780928) Handling close... The images are saved in the folder in which you started the
mriclient. The images are stored in a single
precision format as specified by the default.xml
configuration file. The output files are saved in a simple array format
used throughout the Gadgetron framework (see Appendix A, Simple Array File Format for details). To read the first image into
Matlab you can use the following code:
f = fopen('out_00000.real');
ndims = fread(f,1,'int32');
dims = fread(f,ndims,'int32');
data = fread(f,prod(dims),'float');
fclose(f);
data = reshape(data,dims');
The image is then contained in the data
variable.
Table of Contents
The Gadgetron consists of a streaming processing architecture and a set of toolboxes. The toolboxes are used within the streaming components but comes as individual shared libraries and can thus also be used in standalone applications. The architecture is outlined in Figure 2.1, “Gadgetron Architecture”.
The Gadgetron receives connections from clients through a TCP/IP connection. A client can be any application from which you can open a TCP/IP socket and send data. Once a connection to a client has been established (see Section 2.1.4, “Communication Sequence”), the Gadgetron will read data from the socket and pass it on down a chain of processing steps. The responsibility of reading and writing packages on the socket is dispatched to a set of Readers and Writers (see Section 2.1.2, “Readers and Writers”). Each step in the processing chain is implemented in a module or Gadget (see Section 2.1.1, “Gadgets”). A reconstruction process is defined by defining a chain of Gadgets. The assembly of Gadgets is done dynamically at run-time (see Section 2.1.3, “Stream Configuration”).
A Gadget is the functional unit of the Gadgetron. You can think of the Gadget as a device with an input and output. Data passes through the device and is modified and/or transformed between input and output. By wiring multiple Gadgets together you create a reconstruction program. A schematic outline of a Gadget is seen in Figure 2.2, “Gadget”
The Gadget is an active object based on the
ACE_Task from the ACE library. It has its own
thread (or threads) of execution and an input queue where data is
placed for processing by either the Gadgetron framework or an upstream
Gadget.
The active thread(s) in the Gadget will pick up a data package
from the queue, and then pass it on to a virtual
process. An abbreviated version of the header
Gadget.h is seen below:
class Gadget : public ACE_Task<ACE_MT_SYNCH>
{
public:
virtual int svc(void)
{
//Pick up package from queue
//Call process
if (this->process(m) == -1) {
//Handle error
}
return 0;
}
//More function (left out for simplicity)
protected:
virtual int process(ACE_Message_Block * m) = 0;
virtual int process_config(ACE_Message_Block * m) {
return 0;
}
};The data package used by the ACE_Task is
the ACE_Message_Block, which is a very basic
block of data (essentially just a byte array). To allow the Gadgets to
check if the data blocks on the message queue are of the expected
type, the Gadgetron uses a modified
ACE_Message_Block called
GadgetContainerMessage, which can contain any
class with a no-argument constructor. It is possible to check if the
GadgetContainerMessage contains a specific type
of data, and if so, access that object. Suppose we want to store a
class named MyClass:
GadgetContainerMessage<MyClass>* m = new GadgetContainerMessage<MyClass>(); MyClass* mc = m->getObjectPtr(); //Do something with mc m->release(); //Delete the message block and containing data
When a function receives an
ACE_Message_Block it is possible to check if it
is of a certain type:
int process(ACE_Message_Block* mb)
{
GadgetContainerMessage<MyClass>* m =
AsContainerMessage<MyClass>(mb);
if (m) {
MyClass* mc = m->getObjectPtr();
//Do something with mc
} else {
//Something went wrong, deal with error
return -1;
}
mb->release();
return 0;
}It is possible to chain more than one
ACE_Message_Block together using the
cont function. This effectively provides a way to
pass multiple arguments into a Gadget and checking if they have the
appropriate types:
int process(ACE_Message_Block* mb)
{
GadgetContainerMessage<MyClass>* m1 =
AsContainerMessage<MyClass>(mb);
GadgetContainerMessage<MyOtherClass>* m2 =
AsContainerMessage<MyOtherClass>(mb->cont());
if (m1 && m2) {
MyClass* mc = m1->getObjectPtr();
MyOtherClass* moc = m2->getObjectPtr();
//Do something with mc
} else {
//Something went wrong, deal with error
return -1;
}
mb->release(); //This deletes both message blocks
return 0;
}It gets a bit tedious and error prone to repeat code like the
above in every Gadget. To overcome this, the Gadgetron comes with a
set of templated classes to automate the steps. Say we would like to
make a Gadget which takes a single input argument, we would inherit
from Gadget1. If you need two arguments, you
inherit from Gadget2:
template <class P1, class P2> class Gadget2 : public Gadget
{
protected:
int process(ACE_Message_Block* mb)
{
//Do type checking
}
virtual int process(GadgetContainerMessage<P1>* m1,
GadgetContainerMessage<P2>* m2) = 0;
};The base class performs the type checking for you and only when
the arguments have been verified, it will call the virtual
process above. So, all you need to do in order to
implement a Gadget that takes two arguments is to implement this
function. As an example, let's look at a very simple Gadget, which
receives an image header and some image data and does a Fourier
transform of the first 3 dimensions. First the header file
FFTGadget.h
#include "gadgetroncore_export.h"
#include "Gadget.h"
#include "GadgetMRIHeaders.h"
#include "hoNDArray.h"
#include <complex>
class EXPORTGADGETSCORE FFTGadget :
public Gadget2<GadgetMessageImage, hoNDArray< std::complex<float> > >
{
public:
GADGET_DECLARE(FFTGadget)
protected:
virtual int process(
GadgetContainerMessage< GadgetMessageImage>* m1,
GadgetContainerMessage< hoNDArray< std::complex<float> > >* m2);
};Let us walk through the code step by step. The Gadget takes two
arguments: 1) GadgetMessageImage, which is just
a struct with some image header information (it is defined in
GadgetMRIHeaders.h), and 2) a
hoNDArray, which is a multidimensional array
(see Section 2.2.1, “NDArray”) storage container. In this case
the hoNDArray contains complex floating point
data.
There are a couple of other things to notice. One is the
EXPORTGADGETSCORE macro in the class definition.
This is needed to make things work properly on Windows. It is defined
in gadgetroncore_export.h and is used on Windows
to indicate if the class is being imported or exported from a DLL. It
translates into __declspec(dllexport) or
__declspec(dllimport) in Windows and is empty in
Linux/OSX. It is beyond the scope of this manual to go into why such a
declaration is needed, but keep this in mind when you start creating
your own Gadgets. Each shared library (DLL) has its own export
declaration macro.
The other thing to notice is the
GADGET_DECLARE(FFTGadget) macro. This macro is
required for Windows to correctly handle shared libraries and is
needed whenever you create a new Gadget to make things work properly
on Windows.
The actual implementation looks like this:
#include "FFTGadget.h"
#include "FFT.h"
int FFTGadget::process(
GadgetContainerMessage< GadgetMessageImage>* m1,
GadgetContainerMessage< hoNDArray< std::complex<float> > >* m2)
{
FFT<float>::instance()->ifft(m2->getObjectPtr(),0);
FFT<float>::instance()->ifft(m2->getObjectPtr(),1);
FFT<float>::instance()->ifft(m2->getObjectPtr(),2);
if (this->next()->putq(m1) < 0) {
return GADGET_FAIL;
}
return GADGET_OK;
}
GADGET_FACTORY_DECLARE(FFTGadget)Once we are inside the process function,
the data has already been converted to the appropriate container
messages and we can start processing the data. This function uses an
FFT toolbox (more on toolboxes in Section 2.2, “Gadgetron Toolboxes”).
After the data has been Fourier transformed along the first 3
dimensions it is placed on the next Gadgets queue. Remember the two
GadgetContainerMessage objects were originally
picked up from the message queue as a chain of
ACE_Message_Block objects. They are still
chained together, i.e. when passing m1 on to the
next Gadget we are effectively passing on both arguments.
Another couple of macros to notice are the
GADGET_OK and GADGET_FAIL. They
are defined as 0 and -1 respectively. The convention in the Gadgetron
is to return 0 when a function succeeds and < 0 when it fails -
unless the function returns a pointer.
Last thing to notice is the
GADGET_FACTORY_DECLARE(FFTGadget) statement. This
is a macro which declares functions for loading a Gadget of this type
out of a shared library (DLL) and destroying it again when we are
done. It ensures that we can load the Gadget on all platforms. When
you create your own gadgets you should use this macro to declare the
factory function for the Gadget.
For a tutorial on how to make your own Gadget library see Section 2.3.3, “Making a new Gadget Library”.
In addition to defining a Gadget's behavior in response to a
data package, it is also possible for the Gadgets to receive
configuration information or parameters. The user can define the
Gadgets behavior in response to configuration information by
implementing the process_config function in the
Gadget header file. The configuration information or parameters is
typically transmitted in the beginning of the reconstruction process
from the client (see Section 2.1.4, “Communication Sequence”).
The configuration information can in principle be in any format (a
given application can use a binary format or a text format defined
for the specific purpose), but conventionally the parameters are
transmitted in XML format and the Gadgetron comes with toolbox
functionality in GadgetXml.h, which allow easy
access to parameters in XML format (see the examples throughout the
source code on how to interpret parameters).
An example of a parameter XML file for an MRI reconstruction is shown here:
<?xml version="1.0" ?>
<gadgetron>
<encoding>
<kspace>
<base_resolution>
<value>128</value>
</base_resolution>
<readout_length>
<value>256</value>
</readout_length>
<phase_encoding_lines>
<value>128</value>
</phase_encoding_lines>
<partitions>
<value>64</value>
</partitions>
<phase_resolution>
<value>1</value>
</phase_resolution>
<slice_resolution>
<value>1</value>
</slice_resolution>
<matrix_size>
<value>128</value>
<value>128</value>
</matrix_size>
</kspace>
<trajectory>
<value>1</value>
</trajectory>
<slices>
<value>1</value>
</slices>
<acquisition_dwell_time_ns>
<value>7800</value>
</acquisition_dwell_time_ns>
<channels>
<value>32</value>
</channels>
</encoding>
<timing>
<TR>
<value>5860</value>
</TR>
<TE>
<value>2960</value>
</TE>
</timing>
</gadgetron>The parameter scripts can have any format, but the convention
is to have one root element called gadgetron and
enclose all parameters in a hierarchical structure. The Gadgetron
comes with functions and classes for parsing this format. Firstly,
the TinyXML library is included with the
Gadgetron and can be used to parse the XML script, but we also
include some wrapper classes to make the parsing easier, the file
GadgetXml.h includes the
GadgetXMLNode:
class GadgetXMLNode
{
public:
GadgetXMLNode(TiXmlNode* anchor);
//Other functions
template<typename T> std::vector<T> get(std::string name);
}This class can be used to access the XML parameters in a convenient way. There are multiple examples of this throughout the Gadgetron archive, but one example is given here:
int MyGadget::process_config(ACE_Message_Block* mb)
{
//Use TinyXML to parse the doucment
TiXmlDocument doc;
doc.Parse(mb->rd_ptr());
std::string path("gadgetron.encoding.kspace.matrix_size.value");
//Get a vector with all the values
std::vector<long> dims =
n.get<long>(path);
//Do something with the dimensions
return GADGET_OK;
}In the case of parsing the XML parameters listed above,
dims would contain two values ([128,
128]).
As illustrated in Figure 2.1, “Gadgetron Architecture”
the Gadgetron uses a set of Readers and Writers to deal with the
incoming communication on the TCP/IP socket. Readers are responsible
for deserialization of packages and Writers are responsible for
serialization of packages. All packages that arrive on the socket will
start with a message ID. Based on this ID, the Gadgetron delegates the
responsibility of reading the package of the socket to a particular
instance of a GadgetMessageReader defined by
the following abstract class:
class GadgetMessageReader
{
public:
virtual ACE_Message_Block* read(ACE_SOCK_Stream* stream) = 0;
};In order to be able to read a specific type of data, the
read function must be implemented for that data
type. As an example here is the
GadgetMessageReader, which reads an MRI data
acquisition from the socket.
class EXPORTGADGETSCORE MRIAcquisitionReader
: public GadgetMessageReader
{
public:
GADGETRON_READER_DECLARE(MRIAcquisitionReader);
virtual ACE_Message_Block* read(ACE_SOCK_Stream* socket);
};Note the
GADGETRON_READER_DECLARE(MRIAcquisitionReader)
declaration. This is equivalent to the declaration needed for the
Gadgets (see Section 2.1.1, “Gadgets”) in order to make them
load properly from shared libraries.
The implementation of this particular reader is as follows (this is an abbreviated version without error checking, etc.):
ACE_Message_Block* MRIAcquisitionReader::read(ACE_SOCK_Stream* sock)
{
GadgetContainerMessage<GadgetMessageAcquisition>* m1 =
new GadgetContainerMessage<GadgetMessageAcquisition>();
GadgetContainerMessage<hoNDArray< std::complex<float> > >* m2 =
new GadgetContainerMessage< hoNDArray< std::complex<float> > >();
m1->cont(m2);
sock->recv_n(m1->getObjectPtr(), sizeof(GadgetMessageAcquisition);
std::vector<unsigned int> adims;
adims.push_back(m1->getObjectPtr()->samples);
adims.push_back(m1->getObjectPtr()->channels);
m2->getObjectPtr()->create(&adims)
long data_length = sizeof(std::complex<float>)*
m1->getObjectPtr()->samples*m1->getObjectPtr()->channels;
sock->recv_n (m2->getObjectPtr()->get_data_ptr(), data_length);
return m1;
}
GADGETRON_READER_FACTORY_DECLARE(MRIAcquisitionReader)The Reader allocates two
GadgetContainerMessage data blocks to contain
the incoming data. First an MRI acquisition header (defined in
GadgetMRIHeaders.h) is read. Based hereon the
length of each acquisition (number of samples) and the number of
acquisition channels are determined. A
hoNDArray is allocated to store the data read
from the socket. Notice that the two
GadgetContainerMessage are chained together
using the cont function.
A final important statement to notice is:
GADGETRON_READER_FACTORY_DECLARE(MRIAcquisitionReader)
This macro declares create and destroy functions to load the reader from a shared library on all platforms supported.
Whereas the Readers are responsible for deserialization, The
GadgetMessageWriter is responsible for the
opposite operation (serialization). In practice, Gadgets that produce
an output for the client application can hand that data back to the
Gadgetron framework where it is placed on the output queue along with
a message ID. This is for instance done in this (abbreviated) code
from an ImageFinishGadget:
int ImageFinishGadget
::process(GadgetContainerMessage<GadgetMessageImage>* m1,
GadgetContainerMessage< hoNDArray< float > >* m2)
{
GadgetContainerMessage<GadgetMessageIdentifier>* mb =
new GadgetContainerMessage<GadgetMessageIdentifier>();
mb->getObjectPtr()->id = GADGET_MESSAGE_IMAGE_REAL_FLOAT;
mb->cont(m1);
int ret = this->controller_->output_ready(mb);
if ( (ret < 0) ) {
return GADGET_FAIL;
}
return GADGET_OK;
}Notice that the Gadget has a reference to the Gadgetron
framework through the controller_ member variable,
which is set during initialization.
In the framework (more specifically in the
GadgetStreamController) there is an active
thread responsible for writing messages that are put on to the output
queue. This is done by investigating the message ID and then picking
the GadgetMessageWriter associated with this
ID. A Writer must implement the following abstract class:
class GadgetMessageWriter
{
public:
virtual int write(ACE_SOCK_Stream* stream,
ACE_Message_Block* mb) = 0;
};The Writer is handed control of the socket along with the message block. A Writer declaration could look like:
class MRIImageWriter : public GadgetMessageWriter
{
public:
GADGETRON_WRITER_DECLARE(GadgetMessageWriter);
virtual int write(ACE_SOCK_Stream* sock,
ACE_Message_Block* mb);
};Notice again the
GADGETRON_WRITER_DECLARE(GadgetMessageWriterFLOAT)
which ensures proper run-time linking behavior. The implementation
could look like (abbreviated with no error checking, etc.):
int MRIImageWriter::write(ACE_SOCK_Stream* sock,
ACE_Message_Block* mb)
{
GadgetContainerMessage<GadgetMessageImage>* imagemb =
AsContainerMessage<GadgetMessageImage>(mb);
GadgetContainerMessage< hoNDArray< float > >* datamb =
AsContainerMessage< hoNDArray< float > >(imagemb->cont());
if (!datamb || !imagemb) {
//Deal with errors
}
GadgetMessageIdentifier id;
id.id = GADGET_MESSAGE_IMAGE_REAL_FLOAT;
sock->send_n (&id, sizeof(GadgetMessageIdentifier));
sock->send_n (imagemb->getObjectPtr(), sizeof(GadgetMessageImage));
sock->send_n (datamb->getObjectPtr()->get_data_ptr(),
sizeof(float)*datamb->getObjectPtr()->get_number_of_elements());
return 0;
}
GADGETRON_WRITER_FACTORY_DECLARE(MRIImageWriter)Once again notice the required
GADGETRON_WRITER_FACTORY_DECLARE(MRIImageWriter)
macro. Also notice that the message ID is transmitted to the client.
The client is expected to follow the same communication model as the
Reader, but it is determined entirely by the Writer implementation how
the message is transmitted.
Readers and Writers are loaded dynamically at run-time along with the Gadgets (see Section 2.1.3, “Stream Configuration”). The input and output behaviour can be adapted by manipulating which Readers and Writers are associated with which message IDs.
A Gadgetron reconstruction is made up of modules, i.e. Readers, Writers, and Gadgets. New reconstruction programs can be created by simply assembling existing components in a new way. The configuration of the Gadgetron stream is done at run-time and new configuration chains can be created without recompiling any of the underlying Gadgets. More specifically, the configuration is specified in an XML file that the Gadgetron will read before receiving data. The best way to explain the format is by looking at a (simplified) example:
<?xml version="1.0" ?>
<gadgetron>
<readers>
<reader>
<slot>1001</slot>
<dll>gadgetroncore</dll>
<class>MRIAcquisitionReader</class>
</reader>
</readers>
<writers>
<writer>
<slot>1005</slot>
<dll>gadgetroncore</dll>
<class>MRIImageWriterFLOAT</class>
</writer>
<writer>
<slot>1006</slot>
<dll>gadgetroncore</dll>
<class>MRIImageWriterUSHORT</class>
</writer>
</writers>
<stream>
<gadget>
<name>Accumulator</name>
<dll>gadgetroncore</dll>
<class>AccumulatorGadget</class>
</gadget>
<gadget>
<name>FFT</name>
<dll>gadgetroncore</dll>
<class>FFTGadget</class>
</gadget>
<gadget>
<name>ExtractMAG</name>
<dll>gadgetroncore</dll>
<class>ExtractGadget</class>
</gadget>
<gadget>
<name>ImageFinishFLOAT</name>
<dll>gadgetroncore</dll>
<class>ImageFinishGadgetFLOAT</class>
</gadget>
</stream>
</gadgetron>The configuration file format contains 3 sections: 1) Readers, 2) Writers, 3) Stream (with Gadgets) corresponding to the 3 different types of components that can be assembled in the Gadgetron.
In the example above, the Readers section contains only one
reader, which is the MRIAcquisitionReader
mentioned previously. The message ID associated with this Reader is
1001. Every time a message with ID 1001 arrives on the socket,
responsibility for reading the message will be delegated to the
MRIAcquisitionReader. When the Gadgetron
configuration is loaded, the framework will load the
MRIAcquisitionReader from the DLL (shared
library) gadgetroncore. On the Linux platform
this would be a shared library called
libgadgetroncore.so and on the Windows platform
it would be called gadgetroncore.dll.
The Gadgetron framework knows how to load the components from the DLLs assuming that they have been declared properly as described in Section 2.1.2, “Readers and Writers” and Section 2.1.1, “Gadgets”.
The example Gadgetron configuration has two Writers, i.e. it is
capable of outputting two different types of data. Again the
declarations cause the Gadgetron framework to load specific instances
of GadgetMessageWriter and associate them with
specific ID numbers.
There are certain built-in Readers and Writers in addition to those specified in the configuration file. As an example, there are Readers for receiving configurations to be used by the Gadgetron and for receiving the parameters that will be passed to all Gadgets (see Section 2.1.4, “Communication Sequence”). If the Gadgetron receives a message with an ID for which there is no associated Reader or encounters a message on the output queue for which there is no associated Writer an error will be generated, the Gadgetron stream shuts down, and the connection to the client will be closed.
The <stream> section of the
configuration specifies which Gadgets to load. In the example above,
we have 4 Gadgets. In this specific case the reconstruction is an MRI
reconstruction and the first Gadget is an
AccumulatorGadget, which collects individual
lines and inserts them in k-space. When the k-space image is complete
it is sent to the next Gadget in the chain, the
FFTGadget, which is responsible for Fourier
transforming the data into image space. The next Gadget
(ExtractGadget) will extract the magnitude of
the complex image. Finally the last Gadget in the chain
(ImageFinishGadgetFLOAT) sends the
reconstructed image back to the Gadgetron framework where it is added
to the output queue.
It is also possible to send configuration parameters to Gadgets using the XML file. For example, to set a parameter in a Gadget, one could write:
<gadget> <name>Accumulator</name> <dll>gadgetroncore</dll> <class>AccumulatorGadget</class> <property><name>MyTestProperty</name> <value>Blah Blah</value></property> <property><name>MyTestProperty2</name> <value>98776.862187</value></property> </gadget>
The two properties will now be accessible inside the Gadget
using the parameter access functions defined in
Gadget.h:
class Gadget : public ACE_Task<ACE_MT_SYNCH>
{
//Other definitions
int get_bool_value(const char* name);
int get_int_value(const char* name);
double get_double_value(const char* name);
};Additionally it is also possible to specify how many active threads there should be in a Gadget. This is specified with:
<gadget> <name>Accumulator</name> <dll>gadgetroncore</dll> <class>AccumulatorGadget</class> <property><name>threads</name><value>5</value></property> </gadget>
Which would make the AccumulatorGadget
have 5 threads.
Communication between a client and the Gadgetron follows a straightforward communication protocol. When the Gadgetron is started it will be expecting a connection on a specific port (port 9002 is the default). The communication sequence is as follows:
The client makes connection
The Gadgetron accepts the connection and creates a new
instance of a GadgetStreamController (see
Figure 2.1, “Gadgetron Architecture”). After creating the
GadgetStreamController the Gadgetron
returns to accept connections on the socket such that multiple
clients can be connected simultaneously.
The GadgetStreamController takes
control of the socket and expects to read a specific type of
message, which either contains the filename of a specific stream
configuration (see Section 2.1.3, “Stream Configuration”) or
alternatively it can receive the actual XML stream specification
directly on the socket. These two types of messages are read with
Readers that are always registered for the Gadgetron (see Section 2.1.2, “Readers and Writers”). If the Gadgetron receives the
filename of a Gadget stream it expects to be able to find that
configuration file in the gadegtron/config
folder (see Section 2.1.5, “File Organization”).
The GadgetStreamController is then
expecting to receive parameters that will be transmitted to each
individual Gadget. In principle the "parameters" is just a raw
buffer of characters that will be transmitted as such to each
individual Gadget. It is the convention however to send the
parameters in an XML format. It is up to each individual Gadget to
interpret the parameters. The user can implement any behavior in
response to the parameters by implementing the
process_config function (see Section 2.1.1, “Gadgets”). The client can send parameters at any
time during a reconstruction and they will always be transmitted
to all Gadgets through the process_config
function.
The client will then start transmitting data packages that the Gadgetron processes. Images are returned to the client.
When the client has no more data it will send a closure package. This package causes all Gadgets (in order) to process all remaining data on their input queue and then shut down.
Once the final Gadget has shut down, the connection with the client is terminated.
To make it easier to create a new client, the Gadgetron comes
with a GadgetronConnector class:
class GadgetronConnector:
public ACE_Svc_Handler<ACE_SOCK_STREAM, ACE_MT_SYNCH> {
public:
int open (std::string hostname, std::string port);
int putq (ACE_Message_Block * mb ,
ACE_Time_Value * timeout = 0);
int register_reader(unsigned int slot,
GadgetMessageReader* reader);
int register_writer(unsigned int slot,
GadgetMessageWriter* writer);
int send_gadgetron_configuration_file(std::string config_xml_name);
int send_gadgetron_configuration_script(std::string config_xml_name);
int send_gadgetron_parameters(std::string xml_string);
};This class can be used to create simple clients that open a
connection with the Gadgetron using the open
function and then communicate with the Gadgetron through the Readers
and Writers registered with the connector. See the
mriclient example application
(gagetron/apps/clients/mriclient in the source
code archive) for a simple example of how to build a Gadgetron
client.
This section provides a brief overview of the file organization
in the Gadgetron installation. Once you have compiled the Gadgetron
and installed it (see Section 1.3, “Compiling and Installing Gadgetron”), it will
reside in its designated installation folder
(GADGETRON_HOME). For the purposes of this
description, we will assume that the Gadgetron was installed in
/usr/local/gadgetron.
In GADGETRON_HOME you should find the
following folders:
bin: Contains all executables from
the Gadgetron framework including the
gadgetron executable itself and all
clients and standalone applications.
config: Contains Gadgetron XML
configuration files (see Section 2.1.3, “Stream Configuration”). This is where the
Gadgetron searches for the configurations requested by the
clients during initialization of the Gadget chain (see Section 2.1.4, “Communication Sequence”).
lib:Contains all shared libraries
(Gadgets and toolboxes). Additionally, this is the default path
where Python Gadgets look for Python modules.
include: Contains all header files
for the Gadgets and Toolboxes in order that they can be linked
into external applications and Gadget libraries compiled outside
the Gadgetron source tree.
cmake: Contains a set of helpful
CMake scripts that can be used if you wish to build applications
or Gadget libraries outside the Gadgetron source tree. Among
other things it contains a
FindGadgetron.cmake script, which can be
used to localize and set paths for the Gadgetron using
CMake.
The core reconstruction data structures and algorithms are made available through a set of toolboxes in shared libraries. The toolboxes implement the functionality of the various Gadgets, but they can also be used in standalone applications. A non-exhaustive overview of key functionality is covered in the following sections.
Most image processing operations involve multi-dimensional
arrays. Although the Gadgetron framework does not impose any specific
array structure on the user, it does come with an abstract
multi-dimensional array used throughout, the
NDArray. It has a specific implementation for
the CPU (hoNDArray) and GPU
(cuNDArray). The abstract class definition
looks like (abbreviated version):
template <class T> class NDArray
{
public:
NDArray ();
virtual ~NDArray();
virtual T* create(std::vector<unsigned int> *dimensions);
virtual T* create(std::vector<unsigned int> *dimensions,
T* data, bool delete_data_on_destruct = false);
virtual int permute(std::vector<unsigned int> *dim_order,
NDArray<T> *out = 0, int shift_mode = 0);
inline unsigned int get_number_of_dimensions() const {
return dimensions_->size();
}
unsigned int get_size(unsigned int dimension);
boost::shared_ptr< std::vector<unsigned int> > get_dimensions();
inline T* get_data_ptr() const {
return data_;
}
inline unsigned long int get_number_of_elements() const {
return elements_;
}
//Other public functions....such as permute, etc.
protected:
virtual int allocate_memory() = 0;
virtual int deallocate_memory() = 0;
//Other private functions
};The CPU (host) definition would look like (abbreviated):
template <class T> class hoNDArray : public NDArray<T>
{
public:
//Public functions...
protected:
virtual int allocate_memory();
virtual int deallocate_memory();
};As is seen from the NDArray header file,
this class has a no-argument constructor, which makes it suited for
encapsulating in the GadgetContainerMessage
mentioned in Section 2.1.1, “Gadgets”. The procedure for
creating an array with complex float values would look something like
this:
#include <hoNDArray.h>
#include <complex>
hoNDArray< std::complex<float> > myArray;
std::vector<unsigned int> dimensions;
dimensions.push_back(128);
dimensions.push_back(128);
if(!myArray.create(&dimensions)) {
//Deal with errors
}
//process dataTo create an NDArray contained in a
GadgetContainerMessage would look something
like this:
GadgetContainerMessage< hoNDArray< std::complex<float> > >* m =
new GadgetContainerMessage< hoNDArray< std::complex<float> > >();
std::vector<unsigned int> dimensions;
dimensions.push_back(128);
dimensions.push_back(128);
if(!m->getObjectPtr()->create(&dimensions)) {
//Deal with errors
}
//Process data or pass on to other Gadget, etc.
m->release(); //Delete the message block and containing data
As mentioned in Section 2.1.1, “Gadgets”, the
GadgetContainerMessage is a specialized version
of the ACE_Message_Block class from the ACE
framework. Data is passed between Gadgets in the form of
ACE_Message_Blocks and Gadgets have access to
utility functions that allow them to test if a given
ACE_Message_Block is in fact a partcular type
of GagetContainerMessage.
The NDArray data structure also has a
GPU implementation (abbreviated version of header below):
template <class T> class cuNDArray : public NDArray<T>
{
public:
cuNDArray();
cuNDArray(const cuNDArray<T>& a);
// Constructor from hoNDArray
cuNDArray(hoNDArray<T> *a);
// Assignment operator
cuNDArray& operator=(const cuNDArray<T>& rhs);
virtual ~cuNDArray();
virtual T* create(std::vector<unsigned int> *dimensions);
virtual T* create(std::vector<unsigned int> *dimensions,
int device_no);
virtual T* create(std::vector<unsigned int> *dimensions,
T* data, bool delete_data_on_destruct = false);
virtual boost::shared_ptr< hoNDArray<T> > to_host() const;
virtual int set_device(int device_no);
inline int get_device() { return device_; }
protected:
int device_;
virtual int allocate_memory();
virtual int deallocate_memory();
};It has a few extra create functions
compared to the host (CPU) version of this array. Specifically, it
is possible to provide the array with the device number that the
array should be allocated on. This is important when working on
systems with multiple GPU processors. The default is to allocate it
on the first GPU device (device 0). It is possible to query which
device the data is allocated and to effectively move the data from
one device to another through operators. Similarly, the copy
constructor takes a hoNDArray and
transparently copies the host data to the GPU.
The class vector_td provides a basic
representation of complex numbers as well as one-, two-, three-, or
four-dimensional vectors (positions). It is templetized with the
datatype T and dimensionality D.
Also we provide a set of typedefs to commonly encountered instances. A
subset of the definitions provided in vector_td.h
is provided here:
template<class T, unsigned int D> struct vector_td
{
T vec[D];
};
// Some typedefs for convenience
template< class REAL, unsigned int D > struct reald{
typedef vector_td< REAL, D > Type;
};
template< unsigned int D > struct intd{
typedef vector_td< int, D > Type;
};
template< unsigned int D > struct uintd{
typedef vector_td< unsigned int, D > Type;
};
template< unsigned int D > struct floatd{
typedef typename reald< float, D >::Type Type;
};
template< unsigned int D > struct doubled{
typedef typename reald< double, D >::Type Type;
};
template< class T > struct complext{
typedef vector_td< T, 2 > Type;
};
struct float_complext : complext<float>::Type {
typedef complext<float>::Type Type;
float_complext(){}
float_complext( float r, float i ) { vec[0] = r; vec[1] = i; }
};
struct double_complext : complext<double>::Type {
typedef complext<double>::Type Type;
double_complext(){}
double_complext( double r, double i ) { vec[0] = r; vec[1] = i; }
}; A substancial subset of the C++ arithmetic operators on the
vector_td are defined in
vector_td_operators.h. They are convienient when
performing algebra on complex numbers or spatial vectors. Similarly,
the header vector_td_utilities.h wraps common
math functionality for the vector_td class.
Many common operations that takes one of more
cuNDArray instances with element type
vector_td are defined in
ndarray_vector_utilities.h. We leave it for the
readers to explore these utilities on their own.
The vector_td can be used in both host
and device code. As an example of use it is contained in the interface
of the non-Cartesian FFT described in Section 2.2.3.2, “Non-Cartesian FFT”.
The Gadgetron uses the FFTW library for Fourier transform of
hoNDArray structures. The users can call
the FFTW directly from their code, but to make things a little
easier, we provide a simple wrapper class defined in
toolboxes/ndarray/FFT.h. This wrapper is
defined as (abbreviated):
template <typename T> class EXPORTNDARRAY FFT
{
public:
static FFT<T>* instance();
void fft(hoNDArray< std::complex<T> >* input,
unsigned int dim_to_transform);
void ifft(hoNDArray< std::complex<T> >* input,
unsigned int dim_to_transform);
void fft(hoNDArray< std::complex<T> >* input);
void ifft(hoNDArray< std::complex<T> >* input);
protected:
FFT();
virtual ~FFT();
};This class provides simple wrapper function to perform FFTs
of hoNDArrays along a specific dimension or
along all dimensions. The wrapper will perform in
place FFTs and works on complex arrays of single and
double precision.
An important feature of this class is that it is a process
wide singleton for the Gadgetron. As outlined in the definition
above, the constructor and destructor are protected and it is not
possible to allocate a new FFT object. The
way to use the class is through the instance
function:
#include "FFT.h" FFT<float>::instance()->fft(...);
The reason for this is that the FFTW planning routines are
not thread safe. Multiple Gadgets (that each have their own thread
of execution) may need to use FFTs and consequently the planning
routines need to be protected with a mutex. All of this is handled
inside the FFT class and since it is a
singleton only one thread can run the planning routines at any
given time.
As mentioned it is possible for the users to call FFTW routines directly, and there may be some performance reasons for doing so (as opposed to using this wrapper), but please be aware of this thread safety issue when you design your Gadgets. If you want to be on the safe side, use the wrapper.
Cartesian Fast Fourier Transform on the GPU is supported by
wrapping Cuda's FFT routines and defined in
cuNDFFT.h.
template<class T> class EXPORTGPUCORE cuNDFFT
{
public:
cuNDFFT() {}
virtual ~cuNDFFT() {}
int fft ( cuNDArray<T> *input,
std::vector<unsigned int> *dims_to_transform );
int ifft( cuNDArray<T> *input,
std::vector<unsigned int> *dims_to_transform,
bool do_scale = true );
int fft ( cuNDArray<T> *input,
unsigned int dim_to_transform);
int ifft( cuNDArray<T> *input,
unsigned int dim_to_transform,
bool do_scale = true );
int fft ( cuNDArray<T> *input );
int ifft( cuNDArray<T> *input,
bool do_scale = true );
protected:
int fft_int( cuNDArray<T> *input,
std::vector<unsigned int> *dims_to_transform,
int direction,
bool do_scale = true );
};
The interface defines forwards and inverse transforms of a single array dimension, all dimensions of the array, or a subset of dimensions.
A dedicated GPU-implementation of the NUFFT - often referred
to a gridding - is provided. The interface is defined in
NFFT.h provided below in abbreviated
form
template<class REAL, unsigned int D> class EXPORTGPUNFFT NFFT_plan
{
public:
// Constructors
NFFT_plan();
NFFT_plan( typename uintd<D>::Type matrix_size,
typename uintd<D>::Type matrix_size_os,
REAL W, int device = -1 );
// Destructor
virtual ~NFFT_plan();
// Replan
bool setup( typename uintd<D>::Type matrix_size,
typename uintd<D>::Type matrix_size_os,
REAL W, int device = -1 );
// Preprocess trajectory
enum NFFT_prep_mode { NFFT_PREP_ALL,
NFFT_PREP_FORWARDS,
NFFT_PREP_BACKWARDS };
bool preprocess( cuNDArray<typename reald<REAL,D>::Type> *trajectory,
NFFT_prep_mode mode );
// Execute NFFT
enum NFFT_comp_mode { NFFT_FORWARDS, NFFT_BACKWARDS };
bool compute( cuNDArray<typename complext<REAL>::Type> *samples,
cuNDArray<typename complext<REAL>::Type> *image,
cuNDArray<REAL> *weights, NFFT_comp_mode mode );
bool compute_iteration(
cuNDArray<typename complext<REAL>::Type> *samples,
cuNDArray<typename complext<REAL>::Type> *image,
cuNDArray<REAL> *weights, NFFT_comp_mode mode );
public: // Access to underlying building blocks
// NFFT convolution
bool convolve( cuNDArray<typename complext<REAL>::Type> *samples,
cuNDArray<typename complext<REAL>::Type> *image,
cuNDArray<REAL> *weights, NFFT_comp_mode mode,
bool accumulate = false );
// NFFT FFT
bool fft( cuNDArray<typename complext<REAL>::Type> *data,
NFFT_comp_mode mode, bool do_scale = true );
// NFFT deapodization
bool deapodize( cuNDArray<typename complext<REAL>::Type> *image );
}After a NFFT_plan is constructed the
preprocess function should be called with the
desired trajectory. In the special case of radial sampling the
header radial_utilities.h defines some
convienient functions to compute radial trajectories and
corresponding density compensation weights. After preprocessing the
NFFT can be executed through the compute
function. The individial building blocks of the NFFT - convolution,
FFT, and deapodization - are exposed in the public interface and
hence available for use in custom algorithms.
Please note. The NFFT performs significantly better on GPUs supporting Cuda's shader model 2.0 or newer compared to devices supporting only shader models 1.x. The reason being that we rely on the inherent caching of global memory - available only on hardware supporting at least shader model 2.0.
A fundamental building block of most image reconstruction
algorithms is the abstract matrix_operator. A
range of linear imaging and regularization operators are inherited
from this pure virtual base class:
template <class REAL, class ARRAY_TYPE> class matrixOperator
{
public:
matrixOperator() { weight_ = get_one<REAL>(); }
virtual ~matrixOperator() {}
inline void set_weight( REAL weight ){ weight_ = weight; }
inline REAL get_weight(){ return weight_; }
virtual int mult_M( ARRAY_TYPE* in, ARRAY_TYPE* out,
bool accumulate = false) = 0;
virtual int mult_MH( ARRAY_TYPE* in, ARRAY_TYPE* out,
bool accumulate = false) = 0;
virtual int mult_MH_M( ARRAY_TYPE* in, ARRAY_TYPE* out,
bool accumulate = false) = 0;
private:
REAL weight_;
}The MatrixOperator is templated by
two arguments: 1) the basic precision REAL
(e.g. float or double)
and 2) the ARRAY_TYPE (e.g.
NDArray, hoNDArray, or
cuNDArray) representing the expected vector
format for the matrix-vector multiplication the operator
implements.
Every MatrixOperator has an associated
weight that is used to balance multiple matrix terms when added to a
cost function (see Section 2.2.5, “Linear Solvers”).
The main functionality is provided in the three pure virtual
functions, mult_M, mult_MH,
and mult_MH_M, denoting multiplication with the
matrix operator (M), multiplication with the
adjoint (i.e. conjugate transpose) of the matrix operator
(MH), and an "iteration"
of the two respectively
(MHM).
The MatrixOperator is used to model a
linear imaging modality's encodig operation (Fourier transform for
MRI, Radon transform for CT, convolution for Microscopy etc.) but also
to common regularization operators such the identity matrix, the
partial derivatives etc.
Here follows a list that briefly describes the matrix operators in Gadgetron that have been used for the reconstruction examples provided later in this document (Chapter 3, Gadgetron Applications, Chapter 4, Standalone Applications).
The section provides a non-exhaustive list of availble matrix operators in Gadgetron toolboxes.
A general two-level implementation strategy is used for many
of the operators the Gadgetron provide. We first derive a class, say
IdentityOperator, from the
MatrixOperator base class. In this derived
class we implement the pure virtual functions of the base class;
mult_M, mult_MH, and
mult_MH_M. The overall algorithm of the
operator is implemented at this level. Like its superclass, the
IdentityOperator is however templated on the underlying
ARRAY_TYPE and thus cannot contain dedicated
implementation code to a specific array implementation. The
implementation of mult_M,
mult_MH, and mult_MH_M is
consequently based on a new set of pure virtual functions of the
templated ARRAY_TYPE. We provide another
level of inheritance, e.g.
cuIdentityOperator, which in this case
provides the cuNDArray-specific
implementation of the pure virtual function in
identityOperator. This hierachy has the
desired design goal that the core algorithm implementation is shared
in the base class of the operator while host/device specific
sub-components only are defined individually.
As an example we provide a simplified declaration of the
identityOperator/cuIdentityOperator
below. Without specific mentioning for the subsequent operators,
many follow a similar inheritance hierachy.
identityOperator
Implements multiplication of a vector with the identity matrix.
// Notice: simplified code without error-checking
template <class REAL, class ARRAY_TYPE>
class identityOperator : public matrixOperator<REAL, ARRAY_TYPE>
{
public:
identityOperator() : matrixOperator<REAL,ARRAY_TYPE>() {}
virtual ~identityOperator() {}
// to implement y += x
virtual bool operator_xpy( ARRAY_TYPE *x, ARRAY_TYPE *y ) = 0;
virtual int mult_M( ARRAY_TYPE *in, ARRAY_TYPE *out,
bool accumulate = false )
{
if( accumulate )
return operator_xpy( in, out );
else{
*out = *in;
return 0;
}
... // Similar code for mul_MH and mult_MH_M
}
// Notice:
// Simplified code without error checking and multi-device support
template <class REAL, class T>
class cuIdentityOperator
: public identityOperator< REAL, cuNDArray<T> >
{
public:
cuIdentityOperator() : identityOperator< REAL, cuNDArray<T> >() {}
virtual ~cuIdentityOperator() {}
virtual bool operator_xpy( cuNDArray<T> *x, cuNDArray<T> *y )
{
return cuNDA_axpy( get_one<T>(), x, y );
}
DECLARE_MATRIX_OPERATOR_DEVICE_SUPPORT(cuIdentityOperator);
}
Notice that the template arguments to the
cuIdentitytOperator differ from its base
class. REAL specifies the desired
precision (float or
double) and T
specifies the desired type - which could be identical to
REAL or e.g. a
complext<REAL>. Also notice how the
cuIdentitytOperator class definition
directly specifies the ARRAY_TYPE of its superclass (in this
case to be of type
cuNDArray<T>).
partialDerivativeOperator
Provides the partial derivative of an image in a given spatial dimension.
laplaceOperator
Computes the Laplacian of an image.
imageOperator
Performs multiplication with a diagonal matrix of the element-wise reciprocal of a given image.
convolutionOperator
Performs convolution of a given image with a given kernel.
senseOperator
Implements the encoding operator for the parallel MRI imaging technique Sense. Comes in two flavours for 1) Cartesian and 2) non-Cartesian reconstruction.
Other toolbox provided functionality such as Cartesian and non-Cartesian Fourier transforms can easily be added as new operators as needed.
The Gadgetron 'solvers' toolbox contain both a generic conjugate gradient solver to solve linear least squares reconstruction problems (see Section 2.2.5, “Linear Solvers” ) and a two flavors of a Split Bregman solver for non-linear problems using l1-norms for regularization (see Section 2.2.6, “Non-linear Solvers”).
The conjugate gradient solver is used to reconstruct an image posed as a minimizer to an l2-based optimization problem:
![]() |
The unknown image to be reconstructed is denoted here by u and the measured data by m. E is a linear operator modelling the encoding of the imaging modality (e.g. a Fourier transform for MRI, a Radon transform for CT etc.). R is a regularization operator often required to ensure uniqueness of the solution. Lambda is a scalar weight (with a default value of one) associated to each matrix operator and used to balance the various terms in the cost function. Finally p denotes some (possibly blank) prior image in the regularization term.
The closed form solution to the optimization problem is given by the linear system of equations:
![]() |
Put extremely short you set up and run a solver by 1) adding
the corresponding matrix operators to the solver, 2) computing the
right hand side of the equation above (probably using the
mult_MH function in the matrix operators), and
3) running the solve function in the solver
providing the right hand side as input argument.
An abbreviated version of the interface to the conjugate gradient solver is shown here
// Defined in solver.h
template <class ARRAY_TYPE> class solver
{
public:
enum solverOutputModes { OUTPUT_SILENT = 0,
OUTPUT_WARNINGS = 1,
OUTPUT_VERBOSE = 2,
OUTPUT_MAX = 3 };
solver( int output_mode = OUTPUT_SILENT ) {
output_mode_ = output_mode; }
virtual ~solver() {}
virtual void solver_error( std::string err ) {
std::cerr << err << std::endl; }
virtual void set_output_mode( int output_mode ) {
if( !(output_mode >= OUTPUT_MAX || output_mode < 0 )) {
output_mode_ = output_mode;
}
}
virtual boost::shared_ptr<ARRAY_TYPE> solve( ARRAY_TYPE* ) = 0;
protected:
int output_mode_;
};
// Defined in cgSolver.h
template <class REAL, class ELEMENT_TYPE, class ARRAY_TYPE>
class cgSolver : public solver<ARRAY_TYPE>
{
public:
cgSolver( int output_mode = solver<ARRAY_TYPE>::OUTPUT_SILENT )
: solver<ARRAY_TYPE>( output_mode ) {
// Initialize member variables
}
virtual ~cgSolver() {}
virtual int add_matrix_operator(
boost::shared_ptr< matrixOperator<REAL, ARRAY_TYPE> > op )
{ ... }
virtual int set_preconditioner(
boost::shared_ptr< cgPreconditioner<ARRAY_TYPE> > precond )
{ ... }
virtual void set_limit( REAL limit ) { ... }
virtual void set_iterations( unsigned int iterations )
{ ... }
virtual bool pre_solve( ARRAY_TYPE **rhs ) { return true; }
virtual bool post_solve( ARRAY_TYPE **rho ) { return true; }
virtual bool solver_clear( ARRAY_TYPE* ) = 0;
virtual bool solver_scal( ELEMENT_TYPE, ARRAY_TYPE* ) = 0;
virtual bool solver_axpy( ELEMENT_TYPE, ARRAY_TYPE*,
ARRAY_TYPE* ) = 0;
virtual bool solver_dump( ARRAY_TYPE *rho ) { return true; }
virtual ELEMENT_TYPE solver_dot( ARRAY_TYPE*, ARRAY_TYPE* ) = 0;
virtual boost::shared_ptr<ARRAY_TYPE> solve( ARRAY_TYPE *_rhs )
{
// The conjugate gradient solver code is found here...
}
protected:
boost::shared_ptr< std::vector<
boost::shared_ptr< matrixOperator<REAL, ARRAY_TYPE> > > > operators_;
boost::shared_ptr< cgPreconditioner<ARRAY_TYPE> > precond_;
unsigned int iterations_;
REAL limit_;
};// Defined in cuCGSolver.h
template <class REAL, class T>
class cuCGSolver : public cgSolver< REAL, T, cuNDArray<T> >
{
public:
cuCGSolver() : cgSolver< REAL, T, cuNDArray<T> >() {}
virtual ~cuCGSolver() {}
virtual bool pre_solve(cuNDArray<T> **rhs)
{ ... }
virtual bool post_solve(cuNDArray<T>**)
{ ... }
virtual void solver_error( std::string err )
{ ... }
virtual T solver_dot( cuNDArray<T> *x,
cuNDArray<T> *y ){ ... }
virtual bool solver_clear( cuNDArray<T> *x )
{ ... }
virtual bool solver_scal( T a,
cuNDArray<T> *x ){ ... }
virtual bool solver_axpy( T a, cuNDArray<T> *x,
cuNDArray<T> *y ){ ... }
};The overall inheritance hierachy is modelled and
implemented similarly to the matrixOperator
class hierachy described above (see Section 2.2.4, “Matrix Operators”). To use the solver the user
creates an instance of the solver for either the host or device
(e.g. the cuCGSolver above for a GPU-based
solver). The solver is configured using the functions in the
cgSolver base class. Finally the solve
function itself is found in the root of the hierachy; the
solver.
Note that any number of terms (matrix operators) can be added to the solver (or cost function).
The following code listing provides a short example of how to
define a conjugate gradient solver for GPU-based image deblurring
given an image and an estimate of the point spread function that
degraded the image. It uses the
convolutionOperator to model the blurring and
a partialDerivativeOperator in each spatial
dimension for regularization. The full code can be found in
$(GADGETRON_SOURCE)/apps/standalone/gpu/deblurring/2d/deblur_2d_cg.cpp.
{
<< Code that parses the command line
and loads the image and kernel from disk >>
// Define the desired precision
typedef float _real;
typedef complext<_real>::Type _complext;
// Upload host data to device
cuNDArray<_complext> data(host_data.get());
cuNDArray<_complext> kernel(host_kernel.get());
// Setup regularization operators
boost::shared_ptr< cuPartialDerivativeOperator<_real,_complext,2> >
Rx( new cuPartialDerivativeOperator<_real,_complext,2>(0) );
boost::shared_ptr< cuPartialDerivativeOperator<_real,_complext,2> >
Ry( new cuPartialDerivativeOperator<_real,_complext,2>(1) );
Rx->set_weight( lambda );
Ry->set_weight( lambda );
//
// Setup conjugate gradients solver
//
// Define encoding matrix
boost::shared_ptr< cuConvolutionOperator<_real,2> >
E( new cuConvolutionOperator<_real,2>() );
E->set_kernel( &kernel );
// Setup conjugate gradient solver
cuCGSolver<_real, _complext> cg;
// encoding matrix
cg.add_matrix_operator( E );
// regularization matrix
if( kappa>0.0 ) cg.add_matrix_operator( Rx );
// regularization matrix
if( kappa>0.0 ) cg.add_matrix_operator( Ry );
cg.set_iterations( num_iterations );
cg.set_limit( 1e-8 );
cg.set_output_mode( cuCGSolver<_real, _complext>::OUTPUT_VERBOSE );
// Form right hand side
cuNDArray<_complext> rhs; rhs.create(data.get_dimensions().get());
E->mult_MH( &data, &rhs );
//
// Conjugate gradient solver
//
boost::shared_ptr< cuNDArray<_complext> > cgresult = cg.solve(&rhs);
// All done, write out the result
boost::shared_ptr< hoNDArray<_complext> >
host_result = cgresult->to_host();
write_nd_array<_complext>(host_result.get(),
(char*)parms.get_parameter('r')->get_string_value());
}For an overview of the various standalone applications the Gadgetron provides - and instruction on how to run them - we refer to Chapter 4, Standalone Applications.
The Gadgetron includes two Split Bregman solvers to solve respectively
![]() |
where |.|TV denotes the Total Variation
norm. The solver to the upper (unconstraint) optimization problem is
defined in sbSolver.h while the solver to the
latter constraint problem declared in
sbcSolver.h. The Split Bregman formulation to
solve the above-mentioned minimization problems were chosen as they
integrate nicely with the linear conjugate solver desribed above
(Section 2.2.5, “Linear Solvers”). In fact, most of the work
in the two Split Bregman solvers is performed by a linear inner
solver (e.g. a conjugate gradient solver), but the input (right hand
side) to the inner solver varies from iteration to iteration.
The interface to the unconstraint Split Bregman solver is given here. We have seen the overall inheritance hierachy several times already, so it should suffice to provide only very abbreviated headers here:
// Defined in cgSolver.h
template <class REAL, class ELEMENT_TYPE,
class ARRAY_TYPE_REAL, class ARRAY_TYPE_ELEMENT>
class sbSolver : public solver<ARRAY_TYPE_ELEMENT>
{
public:
sbSolver( int output_mode )
: solver<ARRAY_TYPE_ELEMENT>( output_mode )
{ ... }
virtual ~sbSolver() {}
virtual int set_inner_solver(
boost::shared_ptr< solver<ARRAY_TYPE_ELEMENT> > solver ) { ... }
virtual int set_encoding_operator(
boost::shared_ptr< matrixOperator<REAL, ARRAY_TYPE_ELEMENT> > op )
{ ... }
virtual int add_regularization_operator(
boost::shared_ptr< matrixOperator<REAL, ARRAY_TYPE_ELEMENT> > op,
ARRAY_TYPE_ELEMENT *prior = 0x0 ) { ... }
virtual int add_regularization_group_operator(
boost::shared_ptr< matrixOperator<REAL, ARRAY_TYPE_ELEMENT> > op )
{ ... }
virtual int add_group( ARRAY_TYPE_ELEMENT *prior = 0x0 ) { ... }
virtual void set_tolerance( REAL tolerance ) { ... }
virtual void set_outer_iterations( unsigned int iterations ) { ... }
virtual void set_inner_iterations( unsigned int iterations ) { ... }
virtual void set_image_dimensions(
boost::shared_ptr< std::vector<unsigned int> > dims ){ ... }
virtual boost::shared_ptr<ARRAY_TYPE_ELEMENT>
solve( ARRAY_TYPE_ELEMENT *_f ) { ... }
...
};
// Defined in cuSBSolver.h
template <class REAL, class T>
class cuSBSolver
: public sbSolver< REAL, T, cuNDArray<REAL>, cuNDArray<T> >
{
public:
cuSBSolver()
: sbSolver< REAL, T, cuNDArray<REAL>, cuNDArray<T> >()
{ ... }
virtual ~cuSBSolver() {}
// Implementation of pure virtual functions
...
};To run the algorithm on the GPU the user would create an
instance of a cuSBSolver providing the two template arguments; the
desired precision and data type. Prior to running the
solve function with the measured data m, the user should provide 1) an configured
inner solver (e.g. an instance of a conjugate gradient solver), 2)
the encoding operator used in the inner solver, 3) the
regularization operators used in the inner solver, and 4) the
desired image dimensions as these cannot neccesarily be deduced from
the measured data.
Let us outline the code required to set up the solver for
TV-based image denoising. The full code can be found in
$(GADGETRON_SOURCE)/apps/standalone/gpu/denoising/2d/denoise_TV.cpp.
{
<< Command line parsing and data loading >>
//
// Setup regularization operators
//
boost::shared_ptr< cuPartialDerivativeOperator<_real,_real,2> >
Rx( new cuPartialDerivativeOperator<_real,_real,2>(0) );
boost::shared_ptr< cuPartialDerivativeOperator<_real,_real,2> >
Ry( new cuPartialDerivativeOperator<_real,_real,2>(1) );
Rx->set_weight( lambda );
Ry->set_weight( lambda );
//
// Setup inner conjugate gradient solver
//
// Define encoding matrix (identity)
boost::shared_ptr< cuIdentityOperator<_real,_real> >
E( new cuIdentityOperator<_real,_real>() );
E->set_weight( mu );
// Setup conjugate gradient solver
boost::shared_ptr< cuCGSolver<_real,_real> >
cg(new cuCGSolver<_real,_real>());
cg->add_matrix_operator( E ); // encoding matrix
cg->add_matrix_operator( Rx ); // regularization matrix
cg->add_matrix_operator( Ry ); // regularization matrix
//
// Setup unconstraint Split Bregman solver
//
cuSBSolver<_real,_real> sb;
sb.set_inner_solver( cg );
sb.set_encoding_operator( E );
// For anisotropic denoising
//sb.add_regularization_operator( Rx );
// For anisotropic denoising
//sb.add_regularization_operator( Ry );
// For isotropic denoising
sb.add_regularization_group_operator( Rx );
// For isotropic denoising
sb.add_regularization_group_operator( Ry );
sb.add_group();
sb.set_image_dimensions(data.get_dimensions());
// Run split-Bregman solver
boost::shared_ptr< cuNDArray<_real> > sbresult =
sb.solve(&data);
<< do something with the result >>
}The constrained Split Bregman solver inherits from the unconstaint Split Bregman solver is thus defined with an identical interface.
As you read on, keep in mind that the functionality of the various Gadgets presented below (Section 2.1.1, “Gadgets”) in is encompassed by toolboxes; i.e. a lot more functionality as what has been covered by the selected overview in this section (Section 2.2, “Gadgetron Toolboxes”) is contained in the available toolboxes.
Gadgets wrap the functionality of the toolboxes and provide generic building blocks for configuring the streaming reconstruction in the Gadgetron.
One of the original motivations for creating the Gadgetron was to make a high throughput MRI reconstruction engine that could be interfaced to different MRI vendor systems. Consequently, a lot of the functionality present in the initial release toolboxes and Gadgets is focused on MRI reconstruction. In this section we review the basic data structures used to describe MRI data and list some of the MRI Gadgets that are available. These Gadgets are used in several of the example applications in Chapter 3, Gadgetron Applications.
MRI data is processed in two different phases. In the first phase individual data (k-space) acquisitions are processed while in the second phase these acquisitions have been combined into images (which may still be in k-space). Correspondingly, there are two different types of Gadgets that dominate the MRI Gadgets; those who operate on individual acquisitions and those who operate on images. Naturally, there are also transitional Gadgets that operate on acquisitions but output images.
All MRI Gadgets inherit from Gadget2 as
described in Section 2.1.1, “Gadgets”, i.e. they operate on
two argument types, the main two base classes used are:
Gadget2< GadgetMessageAcquisition, hoNDArray< std::complex<float> > > Gadget2< GadgetMessageImage, hoNDArray< std::complex<float> > >
As seen, they take a data array (which is typically of complex
float type) and a header describing either the acquisition or the
image. These headers are defined in
GadgetMRIHeaders.h. The definition of
GadgetMessageAcquisition looks like
(abbreviated):
struct LoopCounters {
ACE_UINT16 acquisition;
ACE_UINT16 slice;
ACE_UINT16 partition;
ACE_UINT16 echo;
ACE_UINT16 phase;
ACE_UINT16 repetition;
ACE_UINT16 set;
ACE_UINT16 segment;
ACE_UINT16 channel;
};
struct GadgetMessageAcquisition
{
ACE_UINT32 flags;
ACE_UINT32 meas_uid;
ACE_UINT32 scan_counter;
ACE_UINT32 time_stamp;
ACE_UINT16 samples;
ACE_UINT16 channels;
float position[3];
float quarternion[4];
float table_position;
LoopCounters idx;
LoopCounters min_idx;
LoopCounters max_idx;
};It is a simple struct, which mainly serves the purpose of keeping track of a) the encoding properties of a given acquisition (phase ending number, etc.) and b) the spatial position and orientation that the data was acquired from. Different MRI systems have different conventions for how to label data, but in most cases one would be able to convert to this format.
The GadgetMessageImage data structure
is also just a struct for keeping track of image labels, position,
and orientation:
struct GadgetMessageImage
{
ACE_UINT32 flags;
ACE_UINT16 matrix_size[3];
ACE_UINT16 channels;
float position[3];
float quarternion[4];
float table_position;
LoopCounters data_idx_min;
LoopCounters data_idx_max;
LoopCounters data_idx_current;
ACE_UINT32 time_stamp;
ACE_UINT16 image_format;
ACE_UINT16 image_type;
ACE_UINT16 image_index;
ACE_UINT16 image_series_index;
}; This section contains a non-exhaustive list of available MRI Gadgets with a few brief comments on their function. The purpose is to make it easier to read the XML configuration files provided with the Gadgetron and to give some ideas of what modules can be reused in new reconstruction programs.
AccumulatorGadget
(gadgetroncore):
Simple Gadget for accumulating k-space profiles in an array and passing it on to next Gadget. Used for simple Cartesian FT MRI reconstructions.
AutoScaleGadget
(gadgetroncore):
Does simple histogram analysis of floating point images passing through and scales them. This is typically used upstream of conversion from floating point to unsigned short images.
CoilReductionGadget
(gadgetroncore):
Used to reduce the number of coils in a dataset. Typically
used to tune the performance of a given reconstruction by
eliminating data. This Gadget is commonly used in conjunction
with the PCACoilGadget which generates
virtual coils based on principal component analysis. The coil
reduction can be specified with either a mask or the number of
target coils as illustrated below
<gadget> <name>CoilReduction</name> <dll>gadgetroncore</dll> <class>CoilReductionGadget</class> <!-- Keep a max of 16 coils --> <property><name>coils_out</name><value>16</value></property> </gadget> <gadget> <name>CoilReduction</name> <dll>gadgetroncore</dll> <class>CoilReductionGadget</class> <!-- Keep only coil 2,3,4,5 and discard the rest--> <property> <name>coil_mask</name> <value>0 1 1 1 0 0 0 0</value> </property> </gadget>
CropAndCombineGadget
(gadgetroncore):
This Gadget is used to do a simple RMS coil combination in the image domain and remove 2x oversampling in the first dimension of the image as is commonly used in MRI. This Gadget is intended to be used after FFT of the data.
ExtractGadget
(gadgetroncore):
This Gadget is used to extract a given component (magnitude, real, imaginary, phase) from complex images, i.e. it converts complex images to real images containing specific components. The Gadget can be used to extract multiple components using a mask. The bit fields used to define the components are defined as:
#define GADGET_EXTRACT_MAGNITUDE (1 << 0) //1 #define GADGET_EXTRACT_REAL (1 << 1) //2 #define GADGET_EXTRACT_IMAG (1 << 2) //4 #define GADGET_EXTRACT_PHASE (1 << 3) //8
To specify the components, you just specify the mask, for example, the following specification would extract magnitude (1) and phase (8):
<gadget> <name>Extract</name> <dll>gadgetroncore</dll> <class>ExtractGadget</class> <property><name>extract_mask</name><value>9</value></property> </gadget>
Default behavior is to extract magnitude.
FFTGadget
(gadgetroncore):
This Gadget Fourier transforms along the first 3 dimensions of the dataset (frequency, phase, partition encoding directions) and passes on the data to the next Gadget.
FloatToUShortGadget
(gadgetroncore):
Converts floating point images to unsigned short images.
This Gadget would often be used in conjunction with a scaling
step (e.g. AutoScaleGadget) upstream to
ensure that the values will not get clipped or overflow during
the conversion to unsigned short. This Gadget does not make any
attempt to scale the data, it is assumed to be scaled upon
entry.
GPUCGGoldenRadial,
GPUCGFixedRadial
(gadgetroncgsense):
These Gadgets perform conjugate gradient based non-Cartesian SENSE reconstruction (Section 3.3, “Non-Cartesian 2D Parallel MRI (SENSE)”). The reconstruction behavior can be controlled with number of properties:
<gadget> <name>GPUCGRadial0</name> <dll>gadgetroncgsense</dll> <class>GPUCGGoldenRadialGadget</class> <property> <name>deviceno</name> <value>0</value> </property> <property> <name>sliceno</name> <value>0</value> </property> <property> <name>profiles_per_frame</name> <value>32</value> </property> <property> <name>shared_profiles</name> <value>0</value> </property> <property> <name>number_of_iterations</name> <value>10</value> </property> <property> <name>cg_limit</name> <value>1e-6</value> </property> <property> <name>oversampling</name> <value>1.5</value> </property> <property> <name>kernel_width</name> <value>5.5</value> </property> <property> <name>kappa</name> <value>0.1</value> </property> <property> <name>pass_on_undesired_data</name> <value>true</value> </property> </gadget>
GrappaGadget,
GrappaUnmixingGadget
(gadgetrongrappa):
These Gadgets are used together to perform 2D Cartesian
parallel imaging on the GPU. The
GrappaGadget is responsible for
calculating GRAPPA coefficients and the
GrappeUnmixingGadget Fourier transforms
the raw data and applies the coefficients. The
GrappaGadget has the ability to use
target channel compression, i.e. it can reconstruct using fewer
target channels than input channels to improve performance. See
Section 3.2, “Cartesian 2D Parallel MRI (GRAPPA)” for details. The target channel
compression is specificied like this:
<gadget> <name>Grappa</name> <dll>gadgetrongrappa</dll> <class>GrappaGadget</class> <property><name>target_coils</name><value>8</value></property> </gadget>
ImageFinishGadgetSHORT,
ImageFinishFLOAT,
ImageFinishCPLX
(gadgetroncore):
These 3 Gadgets are all template instances of the same
ImageFinishGadget. The only different
between them is that they operate on different types of image
data types as indicated by their names. Their purpose is to
return the reconstructed images to the output queue of the
Gadgetron so that they can be returned to the client.
MRINoiseAdjustGadget
(gadgetronmricore):
The Gadgetron has two noise pre-whitening Gadgets with
similar names MRINoiseAdjustGadget and
NoiseAdjustGadget. They both perform the
same operation, which is a) to collect noise adjust data when
present, calculate the noise decorrelation matrix, and perform
noise decorrelation (when the noise adjustment data is
available). The difference between the two Gadgets is that
MRINoiseAdjustGadget uses BLAS and LAPACK
routines to perform the operation, which makes it much faster
than the NoiseAdjustGadget. The latter
Gadget is provided to enable reconstruction on systems where
those libraries are not available.
NoiseAdjustGadget
(gadgetroncore):
See description of
MRINoiseAdjustGadget. This Gadget
performs noise pre-whitening without using the BLAS and LAPACK
libraries.
PCACoilGadget
(gadgetronmricore):
This Gadget is used to create virtual channels based on
principal component analysis of a portion of the data.
Specifically, data is accumulated for the first frame (for each
location, i.e. slice) and a principal component analysis is done
of this data. Once the PCA coefficients are available, all
subsequent data will be transformed into the virtual channel
domain and passed on down the Gadget chain. This Gadget is often
combined with the
CoilReductionGadget.
RemoveROOversamplingGadget
(gadgetroncore):
Simple Gadget to remove the 2x oversampling often used in the readout direction for Cartesian MRI.
The Gadgetron provides a mechanism to do prototype development in Python. We will again be using MRI as the example application.
The Python layer is accessed through a set of Python Gadgets that can encapsulate a Python module. This is seen in Figure 2.3, “Overview of Python Prototyping”, which illustrates a part of a Gadget chain with two Python Gadgets and one C/C++ Gadget. A Gadget chain can have any number of Python Gadgets and Python Gadgets can be mixed with C++ Gadgets.
The Python modules that are encapsulated in the Python Gadgets are expected to have certain characteristics. Specifically, the Gadgets must have at least 3 functions and these functions will be called by the Gadgetron framework at certain specific times:
Gadget reference function. A specific
function will be called when the Python Gadget is created. This
function is expected to receive a
GadgetReference which is a class (wrapped
in a Python module), which holds a reference to the Gadget, which
owns the Python module. The purpose of passing this reference is
to allow the Python module to return data to the Gadget when
reconstruction outputs are ready. See below for details.
Configuration function. This function
is used to receive the configuration (usually in XML format), when
it is passed to the Gadget, i.e. it is the Python equivalent of
process_config in the Gadget (see Section 2.1.1, “Gadgets”).
Reconstruction function. This function
is called when the Gadget receives data, i.e. it is the Python
equivalent of the process function in the
Gadget (see Section 2.1.1, “Gadgets”).
The user can chose the names of these functions freely in the Python module, but the function names must be specified when the Gadget is inserted in the XML configuration:
<gadget> <name>AccReconPython</name> <dll>gadgetronpython</dll> <class>AcquisitionPythonGadget</class> <property> <name>python_path</name> <value>/home/myuser/scripts/python</value> </property> <property> <name>python_module</name> <value>accumulate_and_recon</value> </property> <property> <name>gadget_reference_function</name> <value>set_gadget_reference</value> </property> <property> <name>input_function</name> <value>recon_function</value> </property> <property> <name>config_function</name> <value>config_function</value> </property> </gadget>
Notice how the 3 function names are specified through the
gadget_reference_function,
input_function, and
config_function parameter names. Also notice that
it is possible to specify a python_path to let the
Python interpreter know where to search for script. By default, the
gadgetron/lib is added to the search path.
Multiple pathnames can be added by separating the paths with
";".
The Python script referenced in the XML configuration above could look like this:
import numpy as np
import GadgetronPythonMRI as g
import GadgetronXML
import kspaceandimage as ki
myRef = g.GadgetReference()
myBuffer = 0
myParameters = 0
myCounter = 1;
mySeries = 1;
def set_gadget_reference(gadref):
global myLocalGadgetReference
myLocalGadgetReference = gadref
def config_function(conf):
global myBuffer
global myParameters
myParameters = GadgetronXML.getEncodingParameters(conf)
myBuffer = (np.zeros((myParameters["channels"], \
myParameters["slices"], \
myParameters["matrix_z"], \
myParameters["matrix_y"], \
myParameters["matrix_x"]))).astype('complex64')
def recon_function(acq, data):
global myLocalGadgetReference
global myBuffer
global myParameters
global myCounter
global mySeries
line_offset = (myParameters["matrix_y"] - \
myParameters["phase_encoding_lines"])>>1
myBuffer[:,acq.idx.slice, \
acq.idx.partition,acq.idx.line+line_offset,:] = data
if (acq.flags & (1<<1)): #Is this the last scan in slice
#FFT
image = ki.ktoi(myBuffer,(2,3,4))
#Create a new image header and transfer value
img_head = g.GadgetMessageImage()
img_head.channels = acq.channels
img_head.data_idx_curent = acq.idx
img_head.data_idx_max = acq.max_idx
img_head.data_idx_min = acq.min_idx
img_head.set_matrix_size(0,myBuffer.shape[4])
img_head.set_matrix_size(1,myBuffer.shape[3])
img_head.set_matrix_size(2,myBuffer.shape[2])
img_head.set_matrix_size(3,myBuffer.shape[1])
img_head.set_position(0,acq.get_position(0))
img_head.set_position(1,acq.get_position(1))
img_head.set_position(2,acq.get_position(2))
img_head.set_quarternion(0,acq.get_quarternion(0))
img_head.set_quarternion(1,acq.get_quarternion(1))
img_head.set_quarternion(2,acq.get_quarternion(2))
img_head.set_quarternion(3,acq.get_quarternion(3))
img_head.table_position = acq.table_position
img_head.time_stamp = acq.time_stamp
img_head.image_index = myCounter;
img_head.image_series_index = mySeries;
#Return image to Gadgetron
return myRef.return_image(img_head,image.astype('complex64'))
#print "Returning to Gadgetron"
return 0 #Everything OK
There is a lot going on in this script. Let us walk through the different parts and add some explanation. First look at the imports:
import numpy as np import GadgetronPythonMRI as g import GadgetronXML import kspaceandimage as ki
All the Python Gadget modules must include
numpy. The arrays
(NDArray) are passed to the Python module as
numpy arrays. The second module
GadgetronPythonMRI is a Python wrapped version of
some of the data structures used in the MRI part of the Gadgetron (see
Section 2.3.1, “MRI Gadgets”). Specifically, the
GadgetMessageAcquisition and
GadgetMessageImage headers are wrapped as
Python types (using Boost Python). The
GadgetronPythonMRI also contains a wrapped
version of the GadgetReference class:
class GadgetReference
{
public:
GadgetReference();
~GadgetReference();
int set_gadget(Gadget* g)
{
gadget_ = g;
return 0;
}
template<class T> int return_data(T header,
boost::python::numeric::array arr);
int return_acquisition(GadgetMessageAcquisition acq,
boost::python::numeric::array arr);
int return_image(GadgetMessageImage img,
boost::python::numeric::array arr);
protected:
Gadget* gadget_;
};
Using the return functions in this class interface, it is
possible for the Python module to return data to the Gadget.
GadgetronXML is a Python module provided with the
Gadgetron, which contains some XML helper functions that can (it is
not a requirement) be used to parse the XML parameters that the module
will receive from process_config.
kspaceandimage is also a python module provided
with the Gadgetron, it contains some simple wrapper functions for
performing Fourier transforms (to and from k-space) of MRI data. The
following section contains some initialization of global variables in
the Python module;
myRef = g.GadgetReference() myBuffer = 0 myParameters = 0 myCounter = 1; mySeries = 1;
As described above, each Python module must contain at least 3
functions corresponding to the 3 entry points from the Gadgetron
framework. The first one of these functions captures the
GadgetReference:
def set_gadget_reference(gadref):
global myLocalGadgetReference
myLocalGadgetReference = gadref
Using this reference, the Python module will be able to return images (or acquisitions) to the Gadget. The next function processes the configuration data:
def config_function(conf):
global myBuffer
global myParameters
myParameters = GadgetronXML.getEncodingParameters(conf)
myBuffer = (np.zeros((myParameters["channels"], \
myParameters["slices"], \
myParameters["matrix_z"], \
myParameters["matrix_y"], \
myParameters["matrix_x"]))).astype('complex64')
The GadgetronXML module is used to parse
some simple encoding parameters out of the XML configuration and they
are stored in a convenient data structure for later reference. The
config_function function also initializes a
buffer for the data.
Finally the recon_function (not repeated
here) simply takes the data as it comes it and stores it in a buffer.
Based on the flags field in the header, it is
determined when the last acquisition in each slice has arrived. As
this happens the buffer is Fourier transformed, an image header is
populated, and the result is returned (via the
GadgetReference) to the Gadgetron where it will
be processed by the next Gadget in the chain.
The Gadgetron distribution comes with a simple Python-based 2D
FT MRI reconstruction. The Gadget chain configuration for this
reconstruction can be found in
gadgets/python/python.xml.
The easiest way to get started making a new Gadget library is to
follow an example. In this example we create a new Gadget library
containing a single Gadget; ThresholdGadget.
Its purpose is to set all values below a certain fraction of the max
value to zero.
New Gadget libraries can either be created in the Gadgetron source tree, which allows easy access to all the other files in the Gadgetron, or they can be made as external libraries that link against an installed Gadgetron system. In this example we do the latter since this creates a new library that does not "taint" the Gadgetron source tree. It is trivial to move the library inside the Gadgetron source tree at some later point in time if desired. We assume that the Gadgetron is installed on the machine that you are working on. The command line entries, etc. correspond to a Linux console. If you are using Windows you have to adjust a bit.
Start by creating a new folder for the library:
user@mycomputer:~/temp$mkdir gadgetron_examplelibuser@mycomputer:~/temp$cd gadgetron_examplelib
We start by creating the class
ThresholdGadget. Create the following 3 files:
ThresholdGadget.h,
ThresholdGadget.cpp,
examplelib_export.h (the last file is just to
help us make sure that things work on Windows) with the following
content:
//ThresholdGadget.h
#ifndef THRESHOLDGADGET_H
#define THRESHOLDGADGET_H
#include "examplelib_export.h"
#include "Gadget.h"
#include "GadgetMRIHeaders.h"
#include "hoNDArray.h"
#include <complex>
class EXPORTGADGETSEXAMPLE ThresholdGadget :
public Gadget2<GadgetMessageImage, hoNDArray< std::complex<float> > >
{
public:
GADGET_DECLARE(ThresholdGadget)
protected:
virtual int process( GadgetContainerMessage< GadgetMessageImage>* m1,
GadgetContainerMessage< hoNDArray< std::complex<float> > >* m2);
virtual int process_config(ACE_Message_Block* mb);
float threshold_level_;
};
#endif //THRESHOLDGADGET_H//ThresholdGadget.cpp
#include "ThresholdGadget.h"
int ThresholdGadget::process_config(ACE_Message_Block* mb)
{
threshold_level_ = get_double_value("level");
if (threshold_level_ == 0.0) {
threshold_level_ = 1.0;
}
return GADGET_OK;
}
int ThresholdGadget::process(
GadgetContainerMessage< GadgetMessageImage>* m1,
GadgetContainerMessage< hoNDArray< std::complex<float> > >* m2)
{
std::complex<float>* d =
m2->getObjectPtr()->get_data_ptr();
unsigned long int elements =
m2->getObjectPtr()->get_number_of_elements();
//First find max
float max = 0.0;
for (unsigned long int i = 0; i < elements; i++) {
if (abs(d[i]) > max) {
max = abs(d[i]);
}
}
//Now threshold
for (unsigned long int i = 0; i < elements; i++) {
if (abs(d[i]) < threshold_level_*max) {
d[i] = std::complex<float>(0.0,0.0);
}
}
//Now pass on image
if (this->next()->putq(m1) < 0) {
return GADGET_FAIL;
}
return GADGET_OK;
}
GADGET_FACTORY_DECLARE(ThresholdGadget)//examplelib_export.h #ifndef EXAMPLE_EXPORT_H_ #define EXAMPLE_EXPORT_H_ #if defined (WIN32) #if defined (gadgetronexamplelib_EXPORTS) #define EXPORTGADGETSEXAMPLE __declspec(dllexport) #else #define EXPORTGADGETSEXAMPLE __declspec(dllimport) #endif #else #define EXPORTGADGETSEXAMPLE #endif #endif /* EXAMPLE_EXPORT_H_ */
Now that we have the files for the Gadget we need to set up the
build environment. In the folder
gadgetron_examplelib create a file called
CMakeLists.txt with the following content:
cmake_minimum_required(VERSION 2.6)
project(examplelib)
if (WIN32)
ADD_DEFINITIONS(-DWIN32 -D_WIN32 -D_WINDOWS)
ADD_DEFINITIONS(-DUNICODE -D_UNICODE)
SET (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /EHsc")
SET (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W3")
endif (WIN32)
###############################################################
#Bootstrap search for libraries
# (We need to find cmake modules in Gadgetron)
###############################################################
find_path(GADGETRON_CMAKE_MODULES FindGadgetron.cmake HINTS
$ENV{GADGETRON_HOME}/cmake
/usr/local/gadgetron)
if (NOT GADGETRON_CMAKE_MODULES)
MESSAGE(FATAL_ERROR "GADGETRON_CMAKE_MODULES cannot be found.
Try to set GADGETRON_HOME environment variable.")
endif(NOT GADGETRON_CMAKE_MODULES)
set(CMAKE_MODULE_PATH ${GADGETRON_CMAKE_MODULES})
###############################################################
find_package(Gadgetron REQUIRED)
find_package(Boost REQUIRED)
find_package(ACE REQUIRED)
set(CMAKE_INSTALL_PREFIX ${GADGETRON_HOME})
INCLUDE_DIRECTORIES(${ACE_INCLUDE_DIR}
${Boost_INCLUDE_DIR}
${GADGETRON_INCLUDE_DIR})
LINK_DIRECTORIES(${GADGETRON_LIB_DIR})
ADD_LIBRARY(gadgetronexamplelib SHARED ThresholdGadget.cpp)
TARGET_LINK_LIBRARIES(gadgetronexamplelib
hondarray
optimized ${ACE_LIBRARIES}
debug ${ACE_DEBUG_LIBRARY})
INSTALL (FILES ThresholdGadget.h
examplelib_export.h
DESTINATION include)
INSTALL(TARGETS gadgetronexamplelib DESTINATION lib)
INSTALL(FILES threshold.xml DESTINATION config)
The last thing we need is the XML configuration file to use when
running our new ThresholdGadget. In the same
folder create the threshold.xml file:
<?xml version="1.0" ?>
<gadgetron>
<readers>
<reader>
<slot>1001</slot>
<dll>gadgetroncore</dll>
<class>MRIAcquisitionReader</class>
</reader>
</readers>
<writers>
<writer>
<slot>1005</slot>
<dll>gadgetroncore</dll>
<class>MRIImageWriterFLOAT</class>
</writer>
</writers>
<stream>
<gadget>
<name>Acc</name>
<dll>gadgetroncore</dll>
<class>AccumulatorGadget</class>
</gadget>
<gadget>
<name>FFT</name>
<dll>gadgetroncore</dll>
<class>FFTGadget</class>
</gadget>
<gadget>
<name>CropCombine</name>
<dll>gadgetroncore</dll>
<class>CropAndCombineGadget</class>
</gadget>
<!-- This is where we insert our new Gadget -->
<gadget>
<name>Threshold</name>
<dll>gadgetronexamplelib</dll>
<class>ThresholdGadget</class>
<property><name>level</name><value>0.25</value></property>
</gadget>
<gadget>
<name>Extract</name>
<dll>gadgetroncore</dll>
<class>ExtractGadget</class>
</gadget>
<gadget>
<name>ImageFinishFLOAT</name>
<dll>gadgetroncore</dll>
<class>ImageFinishGadgetFLOAT</class>
</gadget>
</stream>
</gadgetron>
Check that you have 5 files in your folder:
user@mycomputer:gadgetron_examplelib$lsCMakeLists.txt ThresholdGadget.cpp ThresholdGadget.h examplelib_export.h threshold.xml
Next, let us create a build directory and
compile:
user@mycomputer:gadgetron_examplelib$mkdir build; cd build
In the build folder
user@mycomputer:build$cmake ../
Assuming the cmake process was successful:
user@mycomputer:build$makeScanning dependencies of target gadgetronexamplelib [100%] Building CXX object \ CMakeFiles/gadgetronexamplelib.dir/ThresholdGadget.cpp.o Linking CXX shared library libgadgetronexamplelib.dylib [100%] Built target gadgetronexamplelibuser@mycomputer:build$make install[100%] Built target gadgetronexamplelib Install the project... -- Install configuration: "" -- Up-to-date: /usr/local/gadgetron/include/ThresholdGadget.h -- Up-to-date: /usr/local/gadgetron/include/examplelib_export.h -- Installing: /usr/local/gadgetron/lib/libgadgetronexamplelib.so -- Up-to-date: /usr/local/gadgetron/config/threshold.xml
You may have to use sudo for the make install command depending on your setup.
You should now be able to run a reconstruction using your new reconstruction chain. Follow the instructions in Section 1.4, “Hello Gadgetron: Your First Image Reconstruction” if you have not yet tried to run a simple reconstruction. After having started up the Gadgetron, run the mriclient:
user@mycomputer:~/temp/test_data$ mriclient \
-d simple_gre.gmr \
-x simple_gre.xml \
-c threshold.xml
Gadgetron MRI Data Sender
-- host: localhost
-- port: 9002
-- data: simple_gre.gmr
-- parm: simple_gre.xml
-- conf: threshold.xml
-- loop: 1
(2815|140119753992000) Connection from 127.0.0.1:9002
Image Writer writing image
Image Writer writing image
Image Writer writing image
Image Writer writing image
Image Writer writing image
Image Writer writing image
Image Writer writing image
Image Writer writing image
Image Writer writing image
Image Writer writing image
2815, 81, GadgetronConnector, Close Message received
(2815|140119714780928) Handling close...
(2815|140119714780928) svc done...
(2815|140119714780928) Handling close...If you run it again with the level parameter
set to 0.00000001 (remember to re-install the
threshold.xml file in
gadgetron/config by running make
install):
<gadget>
<name>Threshold</name>
<dll>gadgetronexamplelib</dll>
<class>ThresholdGadget</class>
<property><name>level</name><value>0.00000001</value></property>
</gadget>
You should get two different results that look something like
Figure 2.4, “Result from ThresholdGadget
experiment”.
If you create interesting Gadget libraries please consider publishing them online to the benefit of the reconstruction community. An easy way to do this is by sending them to the Gadgetron team for us to publish right away on the web and possibly include in a future release of the Gadgetron.
The purpose of this section is to maintain a list over the available clients that are included in the Gadgetron distribution. The current available clients are:
mriclient:
This is the standard client for sending MRI data to the
Gadgetron. Two files are needed to send data to the Gadgetron; a
data file and an XML file with parameters. The data file is just a
flat file format of
GadgetMessageAcquisition structs and raw
data. In order to get usage information for the client, simply run
the client with no arguments.
The Gadgetron distribution comes with a
GadgetronConnector class, which can be used to
create clients. An example main.cpp file for a
client could look like:
#include "GadgetMessageInterface.h"
#include "GadgetronConnector.h"
int main(int argc, char** argv)
{
std::string host_name("localhost");
std::string port("9002");
std::string config_file("threshold.xml");
std::string xml_config;
//Generate some XML configuration in xml_fconfig
GadgetronConnector con;
//Register Readers and Writers
con.register_writer(....);
con.register_reader(....);
con.register_reader(....);
//Open a connection with the gadgetron
if (con.open(hostname, port_no) != 0) {
//Deal with errors
}
//Tell Gadgetron which XML configuration to run.
if (con.send_gadgetron_configuration_file(config_file) != 0) {
//Deal with errors
}
if (con.send_gadgetron_parameters(xml_config) != 0) {
//Deal with errors
}
//Send data
while ( .... ) { //some condition
GadgetContainerMessage<GadgetMessageIdentifier>* m1 =
new GadgetContainerMessage<GadgetMessageIdentifier>();
//Create data and add to m1
if (con.putq(m1) == -1) {
//Deal with errors
}
}
//Put a close package on the queue
GadgetContainerMessage<GadgetMessageIdentifier>* m1 =
new GadgetContainerMessage<GadgetMessageIdentifier>();
m1->getObjectPtr()->id = GADGET_MESSAGE_CLOSE;
if (con.putq(m1) == -1) {
//Deal with errors
}
con.wait(); //Wait for recon to finish
return 0;
}To compile this client, create a cmake file:
cmake_minimum_required(VERSION 2.6)
project(exampleclient)
if (WIN32)
ADD_DEFINITIONS(-DWIN32 -D_WIN32 -D_WINDOWS)
ADD_DEFINITIONS(-DUNICODE -D_UNICODE)
SET (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /EHsc")
SET (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W3")
endif (WIN32)
###############################################################
#Bootstrap search for libraries
# (We need to find cmake modules in Gadgetron)
###############################################################
find_path(GADGETRON_CMAKE_MODULES FindGadgetron.cmake HINTS
$ENV{GADGETRON_HOME}/cmake
/usr/local/gadgetron)
if (NOT GADGETRON_CMAKE_MODULES)
MESSAGE(FATAL_ERROR "GADGETRON_CMAKE_MODULES cannot be found.
Try to set GADGETRON_HOME environment variable.")
endif(NOT GADGETRON_CMAKE_MODULES)
set(CMAKE_MODULE_PATH ${GADGETRON_CMAKE_MODULES})
###############################################################
find_package(Gadgetron REQUIRED)
find_package(Boost REQUIRED)
find_package(ACE REQUIRED)
set(CMAKE_INSTALL_PREFIX ${GADGETRON_HOME})
INCLUDE_DIRECTORIES(${ACE_INCLUDE_DIR}
${Boost_INCLUDE_DIR}
${GADGETRON_INCLUDE_DIR})
LINK_DIRECTORIES(${GADGETRON_LIB_DIR})
add_executable(mygadgetronclient main.cpp)
target_link_libraries(mygadgetronclient
optimized gadgettools debug gadgettools${CMAKE_DEBUG_SUFFIX}
tinyxml
optimized ${ACE_LIBRARIES} debug ${ACE_DEBUG_LIBRARY})
install(TARGETS mygadgetronclient DESTINATION bin)
Run cmake and follow the normal make and make install instructions (see Section 2.3.3, “Making a new Gadget Library”).
Table of Contents
The simplest example application in the Gadgetron is a simple 2D FT MRI reconstruction. It receives 2D MRI data, collects it into k-space arrays, performs FFT of the data, combines channels (if there are multiple), and returns the images to the client. This example is included in the Gadgetron for testing and demonstration purposes only. It was not intended to be fast or otherwise optimal in any sense.
The Gadgets for this reconstruction are in the
core folder and the configuration file to use to
run this reconstruction is default.xml. The section
Section 1.4, “Hello Gadgetron: Your First Image Reconstruction” describes how to run a simple
reconstruction using this Gadget chain and how to download data to test
it.
In this section we will take a closer look at the Gadgets in this
chain and how they are implemented. The Gadgetron XML configuration file
(default.xml) looks like this:
<?xml version="1.0" ?>
<gadgetron>
<readers>
<reader>
<slot>1001</slot>
<dll>gadgetroncore</dll>
<class>MRIAcquisitionReader</class>
</reader>
</readers>
<writers>
<writer>
<slot>1004</slot>
<dll>gadgetroncore</dll>
<class>MRIImageWriterCPLX</class>
</writer>
<writer>
<slot>1005</slot>
<dll>gadgetroncore</dll>
<class>MRIImageWriterFLOAT</class>
</writer>
<writer>
<slot>1006</slot>
<dll>gadgetroncore</dll>
<class>MRIImageWriterUSHORT</class>
</writer>
</writers>
<stream>
<gadget>
<name>Acc</name>
<dll>gadgetroncore</dll>
<class>AccumulatorGadget</class>
</gadget>
<gadget>
<name>FFT</name>
<dll>gadgetroncore</dll>
<class>FFTGadget</class>
</gadget>
<gadget>
<name>CropCombine</name>
<dll>gadgetroncore</dll>
<class>CropAndCombineGadget</class>
</gadget>
<gadget>
<name>Extract</name>
<dll>gadgetroncore</dll>
<class>ExtractGadget</class>
</gadget>
<gadget>
<name>ImageFinishFLOAT</name>
<dll>gadgetroncore</dll>
<class>ImageFinishGadgetFLOAT</class>
</gadget>
</stream>
</gadgetron>
The resulting Gadget chain is illustrated in Figure 3.1, “Simple 2D FT Reconstruction Chain”. As described in Section 2.1.3, “Stream Configuration” the Gadgetron configuration
contains 3 sections: Readers, Writers, and the Stream. In this
particular case, there is only one Reader, which received MRI
Acquisitions. This data format is described in Section 2.3.1, “MRI Gadgets”. There are 3 Writers registered with this
configuration. They are all used to write MRI images, but responsible
for the different data types (complex float, float, or unsigned short).
In principle this means that this reconstruction is capable of returning
3 different types of images, but as is seen from the stream
configuration, the only output from this reconstruction will be float
format images. However, many reconstructions will have all 3 Writers
registered to make it easy to switch formats, i.e. it would be trivial
to turn this reconstruction into one that outputs unsigned short images
(have a look at the file default_short.xml) for an
example of how this is done.
As is seen in the stream section of the
configuration, this reconstruction uses 5 Gadgets. The first Gadget is
responsible for accumulating MRI acquisitions. To accomplish this, it
uses an accumulation buffer. When a k-space line arrives at the Gadget,
it will be inserted into the k-space buffer and when the last
acquisition in a slice/repetition has arrived, it will copy the entire
buffer and pass it on to the next Gadget.
Let's have a look at the definition of the
AccumulatorGadget class:
class EXPORTGADGETSCORE AccumulatorGadget :
public Gadget2< GadgetMessageAcquisition,
hoNDArray< std::complex<float> > >
{
public:
GADGET_DECLARE(AccumulatorGadget);
AccumulatorGadget();
~AccumulatorGadget();
protected:
virtual int process_config(ACE_Message_Block* mb);
virtual int process(
GadgetContainerMessage< GadgetMessageAcquisition >* m1,
GadgetContainerMessage< hoNDArray< std::complex<float> > > * m2);
hoNDArray< std::complex<float> >* buffer_;
std::vector<unsigned int> dimensions_;
int image_counter_;
int image_series_;
};
There are a few member variables to help us keep track of the
buffer and the data dimensions and the core functionality is implemented
in two functions: process_config, is used to set up
the buffer, and process, which is responsible for
the accumulation of data. Let us examine the
process_config function:
int AccumulatorGadget
::process_config(ACE_Message_Block* mb)
{
TiXmlDocument doc;
doc.Parse(mb->rd_ptr());
GadgetXMLNode n =
GadgetXMLNode(&doc).get<GadgetXMLNode>(std::string("gadgetron"))[0];
std::vector<long> dims =
n.get<long>(std::string("encoding.kspace.matrix_size.value"));
if (dims.size() < 3) {
dims.push_back(0);
}
if (dims[2] == 0) {
dims[2] = 1;
}
dimensions_.push_back(
n.get<long>(std::string("encoding.kspace.readout_length.value"))[0]);
dimensions_.push_back(dims[1]);
dimensions_.push_back(dims[2]);
dimensions_.push_back(
n.get<long>(std::string("encoding.channels.value"))[0]);
dimensions_.push_back(
n.get<long>(std::string("encoding.slices.value"))[0]);
if (!(buffer_ = new hoNDArray< std::complex<float> >())) {
GADGET_DEBUG1("Failed create buffer\n");
return GADGET_FAIL;
}
if (!buffer_->create(&dimensions_)) {
GADGET_DEBUG1("Failed allocate buffer array\n");
return GADGET_FAIL;
}
image_series_ = this->get_int_value("image_series");
return GADGET_OK;
}The main purpose of this function is to pull parameters out of the
XML configuration information in order to set up the buffer. As
mentioned in Section 2.1.1.1, “Gadget XML Configuration”, the convention is to
pass parameters into the Gadgets in XML format. To enable convenient
parsing of these parameters, the Gadgetron includes the
TinyXML library which can be used to parse the
parameters directly or the TiXmlDocument can be
used to create a GadgetXMLNode, which allows
access to the parameters using the path description of the parameters.
For example if part of the XML configuration information looks
like:
<gadgetron>
<encoding>
<kspace>
<matrix_size>
<value>128</value>
<value>128</value>
</matrix_size>
</encoding>
</gadgetron>The line
std::vector<long> dims =
n.get<long>(std::string("encoding.kspace.matrix_size.value"));
would cause dims to become a vector with two
long values of 128, 128. In simple example, we
have made some assumptions, i.e. our buffer always has the same number
of dimensions, which is 5. Specifically the dimensions are [kx, ky, kz,
channel, slice], to make sure that works out, we check if the number of
dimensions is less than 3 (i.e. it is 2D) in which case we add a
dimension.
Next we want to create a hoNDArray (see
Section 2.2.1, “NDArray”) to store the data. In order to be able
to allocate the array we need a std::vector<unsigned
int> to hold the dimensions of the array. We build the
dimensions up based on the parameters in the XML file.
After creating the dimensions vector, creation of the
hoNDArray is a two-step process, first the class
itself is allocated and then the create function is
used to allocate the data inside the class.
Now we are ready to receive and buffer data, which is done by the
process function:
int AccumulatorGadget::
process(GadgetContainerMessage<GadgetMessageAcquisition>* m1,
GadgetContainerMessage< hoNDArray< std::complex<float> > >* m2)
{
//Create buffer if it doesn't exist
if (!buffer_) {
GADGET_DEBUG1("Buffer not allocated\n");
return GADGET_FAIL;
}
//Grab pointers to buffer and data
std::complex<float>* b =
buffer_->get_data_ptr();
std::complex<float>* d =
m2->getObjectPtr()->get_data_ptr();
int samples = m1->getObjectPtr()->samples;
int line = m1->getObjectPtr()->idx.line;
int partition = m1->getObjectPtr()->idx.partition;
int slice = m1->getObjectPtr()->idx.slice;
//Does the data look OK?
if (samples != static_cast<int>(dimensions_[0])) {
GADGET_DEBUG1("Wrong number of samples received\n");
return GADGET_FAIL;
}
//Copy the data into the buffer.
size_t offset= 0;
//Copy the data for all the channels
for (int c = 0; c < m1->getObjectPtr()->channels; c++) {
offset =
slice*dimensions_[0]*dimensions_[1]*
dimensions_[2]*dimensions_[3] +
c*dimensions_[0]*dimensions_[1]*dimensions_[2] +
partition*dimensions_[0]*dimensions_[1] +
line*dimensions_[0];
memcpy(b+offset,
d+c*samples,
sizeof(std::complex<float>)*samples);
}
//Use the flags to determine if this is the last acquisition
bool is_last_scan_in_slice =
(m1->getObjectPtr()->flags & GADGET_FLAG_LAST_ACQ_IN_SLICE);
if (is_last_scan_in_slice) {
//OK, let's send out the k-space
//Create header
GadgetContainerMessage<GadgetMessageImage>* cm1 =
new GadgetContainerMessage<GadgetMessageImage>();
cm1->getObjectPtr()->flags = 0;
//Create the data array
GadgetContainerMessage<
hoNDArray< std::complex<float> > >* cm2 =
new GadgetContainerMessage<hoNDArray< std::complex<float> > >();
cm1->cont(cm2);
std::vector<unsigned int> img_dims(4);
img_dims[0] = dimensions_[0];
img_dims[1] = dimensions_[1];
img_dims[2] = dimensions_[2];
img_dims[3] = dimensions_[3];
//Allocate array
if (!cm2->getObjectPtr()->create(&img_dims)) {
GADGET_DEBUG1("Unable to allocate new image array\n");
cm1->release();
return -1;
}
size_t data_length = dimensions_[0]*dimensions_[1]*
dimensions_[2]*dimensions_[3];
offset = slice*data_length;
//Copy data
memcpy(cm2->getObjectPtr()->get_data_ptr(),b+offset,
sizeof(std::complex<float>)*data_length);
//Populate image header
cm1->getObjectPtr()->matrix_size[0] = img_dims[0];
cm1->getObjectPtr()->matrix_size[1] = img_dims[1];
cm1->getObjectPtr()->matrix_size[2] = img_dims[2];
cm1->getObjectPtr()->channels = img_dims[3];
cm1->getObjectPtr()->data_idx_min = m1->getObjectPtr()->min_idx;
cm1->getObjectPtr()->data_idx_max = m1->getObjectPtr()->max_idx;
cm1->getObjectPtr()->data_idx_current = m1->getObjectPtr()->idx;
memcpy(cm1->getObjectPtr()->position,
m1->getObjectPtr()->position,
sizeof(float)*3);
memcpy(cm1->getObjectPtr()->quarternion,
m1->getObjectPtr()->quarternion,
sizeof(float)*4);
cm1->getObjectPtr()->table_position =
m1->getObjectPtr()->table_position;
cm1->getObjectPtr()->image_format = GADGET_IMAGE_COMPLEX_FLOAT;
cm1->getObjectPtr()->image_index = ++image_counter_;
cm1->getObjectPtr()->image_series_index = image_series_;
//OK, pass it on
if (this->next()->putq(cm1) < 0) {
return GADGET_FAIL;
}
}
//We are done with this acquisition
m1->release();
return GADGET_OK;
}
This function has two basic tasks: insert data into the buffer and when enough data is present, copy the buffer and pass it on to next gadget.
In this case the copying of data is done with a very simple
memcpy command. There is a basic check for the
image dimensions, but a more robust application may have more checks of
the incoming data.
Once the data is in the buffer, we check to see if we should put
out an image. This is done with the flags field on
the acquisition. Specifically we check if a specific bit
(GADGET_FLAG_LAST_ACQ_IN_SLICE) is set. It is up to
the user how the flags field should be interpreted,
but the GadgetMRIHeaders.h file contains some
definitions of flags that are used to indicate various events in the
acquisition. If you define other flags for other Gadgets it may be a
good idea to check that they don't collide with existing flags:
#define GADGET_FLAG_ACQ_END (1 << 0) #define GADGET_FLAG_LAST_ACQ_IN_SLICE (1 << 1) #define GADGET_FLAG_LAST_ACQ_IN_MEAS (1 << 2) #define GADGET_FLAG_LAST_ACQ_IN_CONCAT (1 << 3) #define GADGET_FLAG_FIRST_ACQ_IN_SLICE (1 << 4) #define GADGET_FLAG_FIRST_ACQ_IN_MEAS (1 << 5) #define GADGET_FLAG_FIRST_ACQ_IN_CONCAT (1 << 6) #define GADGET_FLAG_IS_NOISE_SCAN (1 << 7)
If it is determined that this is the last acquisition for this
slice, we create a copy of the buffer and pass it on to the next Gadget.
Instead of a GadgetMessageAcquisition header we
now need a GadgetMessageImage header to pass
along with the data. This header structure is created and populated with
fields (orientation, etc.) from the acquisition header before it is
passed on to the Gadget in the stream.
Next Gadget is the FFTGadget. Since the
k-space buffering has been taken care of, the Fourier transform is a
relatively simple task. The process function uses
the FFTW wrapper class (Section 2.2.3, “Fourier Transforms”) to perform
the FFT along the first 3 dimensions of the array:
int FFTGadget::process(
GadgetContainerMessage< GadgetMessageImage>* m1,
GadgetContainerMessage< hoNDArray< std::complex<float> > >* m2)
{
FFT<float>::instance()->ifft(m2->getObjectPtr(),0);
FFT<float>::instance()->ifft(m2->getObjectPtr(),1);
FFT<float>::instance()->ifft(m2->getObjectPtr(),2);
if (this->next()->putq(m1) < 0) {
return GADGET_FAIL;
}
return GADGET_OK;
}Now that the images have been Fourier transformed, we need to
remove the oversampling that is done in the readout dimensions and we
need to combine the receiver channels. In this case, we are making some
assumptions, i.e. we assume two-fold oversampling in the readout and we
are doing a simple RMS coil combination to obtain combined magnitude
images. We will not repeat the source code here, it can be found in
gadgets/core/CropAndCombineGadget.cpp.
Last two remaining steps after the coil combination is to extract
the magnitude of the data and return the floating point images to the
Gadgetron so that they can be returned to the client. This is
accomplished in the ExtractGadget and the
ImageFinishGadgetFLOAT. Both of these Gadgets are
described in Section 2.3.1, “MRI Gadgets”.
The Gadgetron contains a high-throughput 2D Cartesian parallel imaging reconstruction (GRAPPA) implemented on the GPU. It is beyond the scope of this manual to review all the algorithmic details of this application, but we will give an overview here as an example of a more complicated reconstruction chain.
The Gadget chain is defined in the grappa.xml
and the resulting chain is illustrated in Figure 3.2, “GRAPPA Reconstruction Chain”.
To test this configuration, please download the GRAPPA test
datasets from https://sourceforge.net/projects/gadgetron/files/testdata/.
In the MRI section you will find two test datasets
gre_tgrappa_rate2.tar.gz and
gre_tgrappa_rate4.tar.gz. They are Cartesian
parallel imaging datasets with rate 2 and 4 acceleration respectively.
They were acquired with a 32 channel coil.
In order to run the GRAPPA reconstruction you have to have a CUDA enable GPU on your system and your Gadgetron distribution should be compiled with CUDA and CULA enabled. Please see Section 1.3, “Compiling and Installing Gadgetron” for details for your specific platform.
To run the reconstruction, start up your Gadgetron (in its own terminal window) and use the mriclient to send the data from another terminal:
user@host:~/temp/gadgetron_temp$ mriclient \
-d gre_tgrappa_rate4.gmr \
-x gre_tgrappa_rate4.xml \
-c grappa.xml
Gadgetron MRI Data Sender
-- host: localhost
-- port: 9002
-- data: gre_tgrappa_rate4.gmr
-- parm: gre_tgrappa_rate4.xml
-- conf: grappa.xml
-- loop: 1
(22460|139802100373312) Connection from 127.0.0.1:9002
Image Writer writing image
Image Writer writing image
Image Writer writing image
Image Writer writing image
Image Writer writing image
Image Writer writing image
Image Writer writing image
Image Writer writing image
Image Writer writing image
Image Writer writing image
Image Writer writing image
Image Writer writing image
22460, 81, GadgetronConnector, Close Message received
(22460|139802061166336) Handling close...
(22460|139802061166336) svc done...
(22460|139802061166336) Handling close...
user@host:~/temp/gadgetron_temp$
You should get example images that look similar to the ones in Figure 3.3, “GRAPPA Reconstruction Results”.
Let's take a closer look at some of the components of this reconstruction application.
The first Gadget is the NoiseAdjustGadget.
As described in Section 2.3.1, “MRI Gadgets”, the purpose of this
Gadget is to decorrelate the noise in the receiver channels. This
improves the parallel imaging performance, especially in cases where
there is a large amount of noise in just a few receiver elements. There
are two versions of this Gadget, one that uses the BLAS/LAPACK routines
for performance improvements and one that implements the same
functionality without these optimizations. When you call the included
grappa.xml configuration, you will use the
optimized version. If you do not have BLAS and LAPACK on your system,
you can modify the XML configuration to use the one from the
gadgets/core library.
Second step is removing the oversampling. This step could also be performed after the reconstruction (as it is done in Section 3.1, “Basic 2D FFT MRI”), but here we opt to remove this excess data to improve downstream performance.
The purpose of the next two Gadgets
(PCAGadget and
CoilReductionGadget) is to a) transform the
receiver coils into PCA virtual coils ordered by their information
content and b) remove some of the coils to improve downstream
performance. The first step is achieved by buffering the first frame of
data and then performing a principal component analysis (PCA) on the
first frame of data. Based on the determined PCA transformation all data
is then subsequently transformed into virtual coils. In the coil
reduction gadget we can now simple eliminate the channels that are above
a certain number. See Section 2.3.1, “MRI Gadgets” for details on
how to control the channel compression.
The next two Gadgets are responsible for the actual GRAPPA
reconstruction. The GrappaGadget calculates the
GRAPPA coefficients and GrappaUnmixingGadget
performs the Fourier transform of the raw data and applies the GRAPPA
coefficients to the aliased imaged to obtain unaliased images.
In general it is assumed that the data is acquired in such a way
that a set of neighboring frames can be averaged to yield a fully
sampled k-space; the data is acquired with a time-interleaved sampling
pattern. When enough calibration data is available to calculate GRAPPA
coefficients, i.e. when a fully sampled region of k-space is available,
the calibration data is sent to a grappa coefficient calculation object
(GrappaWeightsCalculator).
The GrappaWeightsCalculator is an active
object, which picks up weight calculation jobs from an input queue and
passes them on to the GPU where it uses toolbox functions to calculate
GRAPPA unmixing coefficients. These coefficients are Fourier transformed
to image space where they are combined for all coils and stored in a
GrappaWeights object.
When the GrappaGadget passes on the raw
data to the GrappaUnmixingGadget it passes a
reference to the GrappaWeights object which is to
be used when performing the unmixing operation. Let's have closer look
at the GrappaUnmixingGadget.h file:
struct GrappaUnmixingJob
{
boost::shared_ptr< GrappaWeights<float> > weights_;
};
class GrappaUnmixingGadget:
public Gadget3<GrappaUnmixingJob,
GadgetMessageImage,
hoNDArray<std::complex<float> > >
{
public:
GADGET_DECLARE(GrappaUnmixingGadget);
GrappaUnmixingGadget();
virtual ~GrappaUnmixingGadget();
protected:
virtual int process(GadgetContainerMessage<GrappaUnmixingJob>* m1,
GadgetContainerMessage<GadgetMessageImage>* m2,
GadgetContainerMessage<hoNDArray<std::complex<float> > >* m3);
};
We can see that the GrappaUnmixingGadget is
an example of a Gadget, which takes 3 arguments and the additional
argument in this case holds a reference to the unmixing
coefficients.
The GrappaWeightsCalculator will update the
coefficients as often as it is instructed to do so and the
GrappaGadget is in charge of determining when an
update should be done. Specifically, it monitors the incoming data and
when the slice orientation changes, a job will be submitted to update
the coefficients. If the slice is not changing, it is in principle OK to
continue with the current coefficients, but if data is available and the
GrappaWeightsCalculator is idle (the queue is
empty) a job will be submitted.
With this design, the data passes through the
GrappaGadget very quickly and the
GrappaUnmixingGadget can reconstruction the
images very quickly, i.e. it is simply a Fourier transform and an
element wise multiplication and sum over the coils. It is in other words
designed for very high throughput.
If the slice orientation changes, new coefficients will be
calculated, but this calculation will not be done by the time the data
reaches the GrappaUnmixingGadget and
consequently, the images will be reconstructed with the "old"
coefficients until the coefficients are ready. This design ensures low
latency, but when the slice changes, aliasing may occur for a few frames
until coefficients are updated.
After the unmixing, the images are scaled and magnitude is
extracted before returning images to the client. The
AutoScaleGadget has been added in this case to
ensure that images are in a reasonable range before converting to
unsigned short as the output in this case. Automatic image scaling can
be problematic, especially when doing quantitative imaging, but it was
added in this case to make the reconstruction more robust to data from
different sources. A better solution is to only use data where noise
calibration data is available and reconstruct SNR scaled images. Based
on typical SNR values for MRI images, it is fairly trivial to keep the
images in the appropriate range and perform a proper conversion to
unsigned short.
A final comment about the GRAPPA reconstruction is that it allows a second step of channel compression. More specifically, it is possible to reconstruct to a limited number of target channels to further improve performance. Between the upstream and downstream channel compression steps, it is possible to tune the performance of the reconstruction to enable real-time reconstruction on the available. hardware.
The Gadgetron includes an implementation of a GPU-based real-time non-Cartesian Sense reconstruction published in [SANGILD09]. One of the keys to obtaining real-time performance is an efficient GPU implementation of the non-Cartesian Fast Fourier Transform [SANGILD08]. The application reuses several of the gadgets we have seen in use already for the Cartesian Grappa implementation above (Section 3.2, “Cartesian 2D Parallel MRI (GRAPPA)”). An overview of the non-Cartesian Sense gadget chain is given in figure Figure 3.4, “Gadgetron Chain for Non-Cartesian Sense”.
The CGSenseGadget implements the
non-Cartesian Sense reconstruction. It contains a conjugate gradient
solver (Section 2.2.5, “Linear Solvers”) set up with a
nonCartesianSense image encoding matrix and an
imageOperator for regularization. Internally it
maintains a cyclic buffer of a few seconds of imaging data. It uses this
buffer to maintain a fully sampled (i.e. unaliased but blurred) k-space
image from which coil sensititivities and regularization images are
dynamically estimated. The combination of parallel imaging and image
regularization operators allows for alias-suppressed image
reconstruction using significant undersampling hereby achieving
real-time data acquisition rates per frame. The conjugate gradient
solver is able to reconstruct faster than the acquisition time e.g. a
192x192 image from 32 coils using 10 solver iterations on newer graphics
hardware.
To test this configuration please download the 32 channel radial
MRI test dataset (gre_golden_angle.tar.gz) from
https://sourceforge.net/projects/gadgetron/files/testdata/mri/.
Go to the folder in which you unpacked the data. We assume that you have
added $(GADGETRON_HOME)/bin to your PATH
environment variable. You need a CUDA enable GPU on your system and your
Gadgetron distribution should be compiled with CUDA and CULA enabled.
Please see Section 1.3, “Compiling and Installing Gadgetron” for details for your
specific platform.
To run the reconstruction; start up gadgetron (in its own terminal window) and use the mriclient to send the data from another terminal. First start gadgetron:
user@host$ gadgetron
Configuring servicesIf asked, allow the gadgetron application to allow incoming network connection. Next start the mriclient:
user@host$ mriclient \
-d gre_golden_angle.gmr \
-x gre_golden_angle.xml \
-c radial_single.xml
Gadgetron MRI Data Sender
-- host: localhost
-- port: 9002
-- data: gre_golden_angle.gmr
-- parm: gre_golden_angle.xml
-- conf: radial_single.xml
-- loop: 1
(23577|140735084973248) Connection from 127.0.0.1:9002
Image Writer writing image
Image Writer writing image
Image Writer writing image
...
Image Writer writing image
23577, 81, GadgetronConnector, Close Message received
(23577|4300226560) Handling close...
(23577|4300226560) svc done...
(23577|4300226560) Handling close...
user@host$
Your current folder now holds the reconstructed images. They will look something like the one depicted in Figure 3.5, “Non-Cartesian Sense Reconstruction Results”.
Table of Contents
This chapter demonstrates how to use the Gadgetron toolboxes (Section 2.2, “Gadgetron Toolboxes”) to build standalone applications. The
following non-exhaustive examples are build with the Gadgetron and
binaries should consequently be available in
$GADGETRON_HOME/bin. You need a CUDA enabled GPU on your
system and your Gadgetron distribution should be compiled with CUDA and
CULA enabled.
This example uses the unconstraint Split Bregman solver for total
variation based image denoising. The encoding matrix is defined as an
identityOperator and a
partialDerivativeOperator is used for each of the
two spatial directions to implement the regularization terms. The full
source code for the example can be found at
$(GADGETRON_SOURCE)/apps/standalone/gpu/denoising/2d/denoise_TV.cpp.
You can download some noisy Shepp-Logan phantom test datasets from
https://sourceforge.net/projects/gadgetron/files/testdata/phantom/shepp_logan/shepp.tar.gz.
In a terminal, go to the folder in which you unpacked the data. We
assume that you have added $(GADGETRON_HOME)/bin to your
PATH environment variable.
Try
user@host$ denoise_TV -d shepp_logan_256_256_med_noise.real -O 250 -m 1
Running denoising with the following parameters:
----------------------------------------------------
Noisy image file name (.real) : shepp_logan_256_256_med_noise.real
Result file name : denoised_image_TV.real
Number of cg iterations : 20
Number of sb inner iterations : 1
Number of sb outer iterations : 250
Regularization weight (mu) : 1
----------------------------------------------------
...
user@host$which runs 250 iterations of the solver with a regularization
weight of 1. The output is saved in the current folder in the file
denoised_image_TV.real. Running
denoise_TV with no arguments prints out a
brief usage description. We leave it as an exercise to run the algorihm
with various settings. The data file you downloaded contains two further
dataset (with lower and higher noise levels respectively) to try out as
well.
This example uses 1) the linear least squares solver, and 2) the
constraint Split Bregman solver for image deblurring. The encoding
matrix is defined as an convolutionOperator and a
partialDerivativeOperator is used for each of the
two spatial directions to implement the regularization terms.
We reuse the Shepp-Logan data from the image denoising experiement above (Section 4.1, “Image Denoising”).
First we generate a blurry Shepp-Logan phantom by convolution with
a Gaussian kernel. This is easily achieved using the function
mult_M in the
convolutionOperator. Soure code is provided at
$(GADGETRON_SOURCE)/apps/standalone/gpu/deblurring/2d/blur_2d.cpp
In a terminal, go to the folder in which you unpacked the Shepp Logan phantom.
Try
user@host$ blur_2d -d shepp_logan_256_256_no_noise.realwhich generates two complex images;
blurred_image.cplx and
kernel_image.cplx. For convienience a corresponding
magnitudes image is also saved as
blurred_image.real.
Now run the conjugate gradient solver. The source code for the
example can be found in
$(GADGETRON_SOURCE)/apps/standalone/gpu/deblurring/2d/deblur_2d_cg.cpp.
user@host$ deblur_2d_cg -K 1e-4
Running deblurring with the following parameters:
----------------------------------------------------
Blurred image file name (.cplx) : blurred_image.cplx
Kernel image file name (.cplx) : kernel_image.cplx
Result file name : cg_deblurred_image.cplx
Number of iterations : 25
Regularization weight : 1e-4
----------------------------------------------------
Iterating...
...
user@host$The result is saved in the current folder in the file
cg_deblurred_image_TV.cplx. A magnitudes image is
also saved as cg_deblurred_image.real. We leave it
as an exercise to run the algorihm with various settings. Try also to
add noise to the blurred image before the deconvolution.
Next run the constraint Split Bregman solver. The source code for
the example can be found in
$(GADGETRON_SOURCE)/apps/standalone/gpu/deblurring/2d/deblur_2d_sb.cpp.
user@host$ deblur_2d_sb -O 100 -L 0.5 -M 0.5
Running deblurring with the following parameters:
----------------------------------------------------
Blurred image file name (.cplx) : blurred_image.cplx
Kernel image file name (.cplx) : kernel_image.cplx
Result file name : sb_deblurred_image.cplx
Number of cg iterations : 20
Number of sb inner iterations : 1
Number of sb outer iterations : 100
Mu : 0.5
Lambda : 0.5
----------------------------------------------------
...
user@host$The result is saved in the current folder in the file
sb_deblurred_image_TV.cplx. A magnitudes image is
also saved as sb_deblurred_image.real. Again, try
to experiment with the various settings.
Notice. If the dimensions of the provided convolution kernel is exactly double that of the provided image, the convolution operator zero-pads the image before the convolution and removes the padding again after. As the convolution operator utilizes FFTs in its implementation, this oversampling is a way of avoiding cyclic boundary conditions during the convolution.
This example shows how to use the forwards and inverse NFFT on a
2D image. The source code can be found at
$(GADGETRON_SOURCE)/apps/standalone/gpu/MRI/nfft/2d/nfft_main.cpp
and
$(GADGETRON_SOURCE)/apps/standalone/gpu/MRI/nfft/2d/nffth_main.cpp.
Again we use the Shepp-Logan data downloaded for the image denoising experiement above (Section 4.1, “Image Denoising”).
Run the NFFT followed by the NFFTH. We use an oversampled matrix size of 384, 128 profiles in k-space (undersampling) with 384 samples each. The NFFT convolution kernel width is set to 5.5.
user@host$ nfft -d shepp_logan_256_256_no_noise.real \
-o 384 -p 128 -s 384 -k 5.5
Running reconstruction with the following parameters:
----------------------------------------------------
Input image file name (.real) : shepp_logan_256_256_no_noise.real
Result file name : samples.cplx
Oversampled matrix size : 384
Number of profiles : 128
Samples per profiles : 384
Kernel width : 5.5
----------------------------------------------------
Loading image from disk
Uploading, normalizing and converting to complex
Initializing plan
Computing golden ratio radial trajectories
NFFT preprocessing
Computing density compensation weights
Computing nfft (inverse gridding)
Output result to disk
user@host$user@host$ nffth -d samples.cplx -m 256 -o 384 -k 5.5
Running reconstruction with the following parameters:
----------------------------------------------------
Input samples file name (.cplx) : samples.cplx
Output image file name (.cplx) : result.cplx
Matrix size : 256
Oversampled matrix size : 384
Kernel width : 5.5
----------------------------------------------------
Loading samples from disk
Uploading samples to device
Initializing plan
Computing golden ratio radial trajectories
NFFT preprocessing
Computing density compensation weights
Computing nffth (gridding)
Output result to disk
user@host$The result is saved in file result.cplx. A
magnitudes image is saved as result.real.
Exercise: experiment with the settings to eliminate the aliasing.
Can I make a branching Gadget chain?
The short answer is no. We plan on supporting this in a future release, but it is not quite ready yet.
How can I help?
We are always looking for people who are interested in helping with the continuing development of the Gadgetron. There are many things you can do:
Use it.
When you develop new Gadgets or Toolboxes, please consider submitting them to us so that we can include them in the archive.
Help us implement some of the future features in Appendix B, Future Features. It is probably a good idea to get in touch with us before you start coding, just in case somebody is already working on it.
When working with the Gadgetron it is often necessary to write files with reconstructed images to disk, either as part of debugging or as the final reconstruction result. We have adopted a very simple multidimensional array file format for this purpose. The main advantage of this file format is its simplicity but there are a number of disadvantages and caveats as well as described in this section.
The simple array files are made up of a) a header followed by b) the data itself. This layout of data and header is illustrated in Figure A.1, “Simple Array File Format”. The header has a single 32-bit integer to indicate the number of dimensions of the dataset followed by one integer for each dimension to indicate the length of that dimension. The data follows immediately after the header. The data is stored such that the first dimension is the fastest moving dimension, second dimension is second fastest, etc. The header contains no information about the size of each individual data element and consequently the user needs to know what type of data is contained in the array. In general, the Gadgetron uses 3 different types of data and the convention is to use the file extension to indicate the data type in the file:
16-bit unsigned short. File extension:
*.short
32-bit float. File extension: *.real
32-bit complex float. Two 32-bit floating point values per data
element. File extension: *.cplx
Figure A.1. Simple Array File Format
![]() |
The simple array file format has a header followed by the data. The header consists of one 32-bit integer defining the number of dimensions (N-dimensions) followed by N-dimensions 32-bit unsigned integers each defining the length of each dimensions. In the example, the dataset has 4 dimensions and the size of those dimensions is 128x128x1x1, i.e. 16384 elements.
The Gadgetron framework provides function for reading these files in
C++. The functions are located in
toolboxes/ndarray/hoNDArray_fileio.h in the Gadgetron
source code distribution.
It is also trivial to read the files into Matlab. Below is a function which detects the data type based on the file extension and reads the file into Matlab.
function data = read_gadgetron_array(filename)
% data = read_gadgetron_array(filename)
%
% Reads simplified array format output from the Gadgetron
%
% The datatype is determined by the file extension.
% - *.short : 16-bit unsigned integer
% - *.real : 32-bit float
% - *.cplx : 32-bit complex (two 32-bit values per data element)
%
%
if (~exist(filename,'file')),
error('File not found.');
end
[path,name,ext] = fileparts(filename);
ext = lower(ext);
if (~strcmp(ext,'.short') && ~strcmp(ext,'.real') && ~strcmp(ext,'.cplx')),
error('Unknown file extension');
end
f = fopen(filename);
ndims = fread(f,1,'int32');
dims = fread(f,ndims,'int32');
switch ext
case '.short'
data = fread(f,prod(dims),'uint16');
case '.real'
data = fread(f,prod(dims),'float32');
case '.cplx'
data = fread(f,2*prod(dims),'float32');
data = complex(data(1:2:end),data(2:2:end));
otherwise
end
fclose(f);
data = reshape(data,dims');
end
The Gagdetron is evolving continuously and there are many things still that we would like to include but have not yet had the time to do. This appendix serves as a to-do list of features to be implement as we go along.
HDF5 Fileformat support. We have been using our simplified
multidimensional array file format for a while, but there are many
shortcomings with this format and we plan to replace it with a better
approach in the future. More specifically we aim to use the HDF5
Fileformat (http://www.hdfgroup.org/HDF5/)
throughout the Gadgetron.
Branching Gadget chains. There is currently no ability to branch and collect in the Gadgetron.
Persistent memory storage across Gadget chains.
Matlab Gadgets. It would be great to have a way to encapsulate Matlab code in a Gadget similar to the way that the Python Gadgets work.
[HANSEN12] Gadgetron: An Open Source Framework for Medical Image Reconstruction. To be published. 2012.
[HANSEN08] Cartesian SENSE and k-t SENSE reconstruction using commodity graphics hardware. Magn Reson Imaging. 2008.