C++ Introduction Tutorial

This is an introduction tutorial for FAST in C++.

Set up a CMake project

Before we can start coding in C++ with FAST we need to set up a CMake project. This will enable you to build a standalone executable using FAST.

  1. First install FAST on your machine.
  2. Then you need to install some tools (Git, CMake and a C++ compiler) to build your FAST application:

    Windows
    Download and install these tools: Git, CMake and Visual Studio Community.

    Ubuntu Linux

    sudo apt install git cmake g++
  3. Clone the FAST example project repository:

    git clone https://github.com/smistad/FAST-example-project.git
    cd FAST-example-project
  4. Setup your build environment using CMake:

    Windows (Visual Studio)
    Modify generator -G string to match your Visual studio version. This command will create a visual studio solution in your build folder.

    mkdir build
    cd build
    cmake .. -DFAST_DIR="C:/Program Files/FAST/fast/cmake/" -G "Visual Studio 16 2019" -A x64

    Linux

    mkdir build
    cd build
    cmake .. -DFAST_DIR=/opt/fast/cmake/
  5. Build the application:

    cmake --build . --config Release --target fastExample
  6. Run the application:

    Windows

    cd Release
    fastExample.exe

    Linux

    ./fastExample

    Now you should hopefully see this:

    Image

Download the test data

To download the 2~GB FAST test dataset, simply run the downloadTestData executable found in your FAST installation directory. With default install locations these are:

Linux: /opt/fast/bin/downloadTestData

Windows: C:/Program Files/FAST/fast/bin/downloadTestData.exe

Importing data

Now we are ready to start coding. Open the main.cpp file in an editor of your choice. Start by removing all the code in the main() function to start from scratch. Then add the following code to load an ultrasound image:

auto importer = ImageFileImporter::create(Config::getTestDataPath() + "US/Heart/ApicalFourChamber/US-2D_0.mhd");

This code will create an ImageFileImporter process object and set the filename of the ultrasound image to load. Config::getTestDataPath simply returns the path to where the test data you downloaded earlier is located. To execute the ImageFileImporter, and thereby loading the image, you can do the following:

auto image = importer->runAndGetOutputData<Image>();
std::cout << "An image was imported with size: " << image->getSize() << std::endl;

Use the command cmake --build . --config Release --target fastExample from before to rebuild the application.

Display the data

To display the image data, we need two things: a Window and a Renderer. For this example, we will use the ImageRenderer which is used to display images, and the SimpleWindow2D. The renderer is connected to the importer using the connect method:

auto renderer = ImageRenderer::create()->connect(importer);

This will form an execution pipeline, so that executing the renderer will trigger execute on the importer as well.

Our entire example should now look like this:

auto importer = ImageFileImporter::create(Config::getTestDataPath() + "US/Heart/ApicalFourChamber/US-2D_0.mhd");

// Set up a renderer and connect it to the importer
auto renderer = ImageRenderer::create()->connect(importer);

// Create a 2D window, add the renderer and start the computation/rendering loop.
SimpleWindow2D::create()
    ->connect(renderer)
    ->run();

You should now see this image:

Image

Add an image processing step

We will now add an image processing step to our pipeline. For this, we will use the NonLocalMeans filter which is excellent at de-noising ultrasound images.

auto importer = ImageFileImporter::create(Config::getTestDataPath() + "US/Heart/ApicalFourChamber/US-2D_0.mhd");

// Set up the NonLocalMeans processing step and connect it to the importer
auto filter = NonLocalMeans::create()->connect(importer);

// Set up the renderer and connect it to the NonLocalMeans filter
auto renderer = ImageRenderer::create()->connect(filter);

// Create a window, add the renderer and start the computation/rendering loop.
SimpleWindow2D::create()
    ->connect(renderer)
    ->run();

The results should look like this:

Image

Dual view window

Now, let's say we want to display the input image and the filtered image side-by-side. The SimpleWindow used above only has a single View, to show two images side-by-side we will need two View objects and therefore we will use the DualViewWindow2D instead. We also need an additional ImageRenderer which is connected to the ImageFileImporter. Putting this all together, it should look like this:

auto importer = ImageFileImporter::create(Config::getTestDataPath() + "US/Heart/ApicalFourChamber/US-2D_0.mhd");

// Set up the NonLocalMeans processing step and connect it to the importer
auto filter = NonLocalMeans::create()->connect(importer);

// Set a renderer and connect it to importer
auto renderer = ImageRenderer::create()->connect(importer);

// Set a renderer and connect it to the NonLocalMeans filter
auto filterRenderer = ImageRenderer::create()->connect(filter);

// Create a dual view window, add the renderers and start the computation/rendering loop.
DualViewWindow2D::create()
    ->connectLeft(renderer)
    ->connectRight(filterRenderer)
    ->run();

The result should look like this:

Image

Data streaming

So far we have only processed a single image, but now we are going to process a stream of images. FAST can stream images from disk, videos, cameras and even from ultrasound scanners. First, we are going to try streaming from disk. To do this, simply replace the ImageFileImporter with an ImageFileStreamer in the previous code example:

auto importer = ImageFileStreamer::create(Config::getTestDataPath() + "/US/Heart/ApicalFourChamber/US-2D_#.mhd", true);

Compile and run the application, and you should now see an entire ultrasound recording being played back to you on screen.

If you want to iterate through a stream of data manually, you this with a simply loop as follows:

auto streamer = ImageFileStreamer::create(Config::getTestDataPath() + "/US/Heart/ApicalFourChamber/US-2D_#.mhd");

auto filter = NonLocalMeans::create()->connect(streamer);
auto dataStream = DataStream(filter);

// Iterate through all frames in data stream
int frameNr = 0;
while(!dataStream.isDone()) {
    auto image = dataStream.getNextFrame<Image>(); // Get next frame in data stream of type Image
    std::cout << "Received image frame " << frameNr << " with size " << image->getSize() << std::endl;
    ++frameNr;
}

Access pixel data

To access pixel data on the host CPU we have to request access to it through an ImageAccess object. This is done by calling Image::getImageAccess. If the image is located on the GPU, it will be copied to the CPU for you. The ImageAccess object has several methods to read and write pixel data. Here are some examples:

auto importer = ImageFileImporter::create(Config::getTestDataPath() + "US/Heart/ApicalFourChamber/US-2D_0.mhd");

// Run importer and get access to pixel data on host with READ and WRITE access.
auto image = importer->runAndGetOutputData<Image>();
auto access = image->getImageAccess(ACCESS_READ_WRITE);

// Retrieve pixel value at x = 10, y = 23
float intensity = access->getScalar(Vector2i(10, 23));

// Retrieve pixel value at 1D position 1337
float intensity2 = access->getScalar(1337);

// Write pixel value 42 at x = 10, y = 23
access->setScalar(Vector2i(10, 23), 42);

If this image was a 3D image, you could simply use the Vector3i to index the volume instead.

Faster pixel access

The code above performs out-of-bounds check and data type conversion between the image true data type (in this case uchar) and float. This is slow, and not a good idea in a loop. Therefore there are faster versions of these function that doesn't before these kind of operations, but it requires you to know that you are within the bounds of the image (otherwise you will get a seg fault crash), and to know the data type of your image.

// Access pixel data without type conversion and out of bounds check using the Fast methods:

// Retrieve pixel value at x = 10, y = 23
uchar intensity = access->getScalarFast<uchar>(Vector2i(10, 23));

// Write pixel value 42 at x = 10, y = 23
access->setScalarFast(Vector2i(10, 23), 42);

Multi-channel images

If you have a multi-channel image, every pixel will have a vector, and you can use the same methods as above, by just replacing the word scalar with vector, see Image::getVector.

Exporting data

To export an image, we can use the ImageFileExporter process object. The following example simply imports a metaimage (.mhd) and exports it to disk as a jpeg image. There are many other exporters available for different kinds of data and formats, see the exporter API module page for an overview.

auto importer = ImageFileImporter::create(Config::getTestDataPath() + "US/Heart/ApicalFourChamber/US-2D_0.mhd");

auto exporter = ImageFileExporter::create("exported_image.jpg")
    ->connect(importer);
exporter->run();

Next steps

You have now finished the C++ introduction tutorial to FAST.