BTK  0.3dev.0
Open-source library to visualize/process biomechanical data
Getting started

Table of contents


Integrate BTK in your project

The simplest way to integrate BTK in your programs is to use the file FindBTK.cmake included with the source code in the CMake folder or in the subfolder share/CMake if you use a binary release. This file will be used by CMake to find the include path, the libraries and all resources required to used BTK as an external library in you program.

For example, the file CMakeLists.txt used to compile the example AcquisitionConverted (see the folder /Examples/AcquisitionConverter in the sources) can be easily modify to be built as an external project and integrating BTK.

Old CMakeLists.txt file used to build the example AcquisitionConverter when compiling BTK:

SET(AcquisitionConverter_SRCS main.cpp)
ADD_EXECUTABLE(AcquisitionConverter ${AcquisitionConverter_SRCS})
TARGET_LINK_LIBRARIES(AcquisitionConverter BTKIO)

New CMakeLists.txt file to build AcquisitionConverter as an independent project:

PROJECT(AcquisitionConverter)
CMAKE_MINIMUM_REQUIRED(VERSION 2.6.2)
# Used to find the file FindBTK.cmake
SET(CMAKE_MODULE_PATH ${AcquisitionConverter_SOURCE_DIR})
# The required lines to use FindBTK.cmake in you project
FIND_PACKAGE(BTK REQUIRED)
INCLUDE(${BTK_USE_FILE})
SET(AcquisitionConverter_SRCS main.cpp)
ADD_EXECUTABLE(AcquisitionConverter ${AcquisitionConverter_SRCS})
TARGET_LINK_LIBRARIES(AcquisitionConverter BTKIO)

Object creation and shared pointer

To simplify the way to code and manage the memory used by BTK, it is proposed to use shared pointer. A shared pointer is like a pointer but without owner. Its content is shared by all the variables assigned to the shared pointer and will be deleted when no variable use it anymore (meaning, the memory is freed automatically).

To do that the most transparent as possible, each class in BTK has the type definition Pointer and the static method(s) New. So you cannot instantiate an object by using a constructor but by using one of the New methods proposed in each class with return a Pointer (i.e. a shared pointer). So, to create a object btk::Acquisition (or strictly speaking a shared pointer associated with a btk::Acquisition object), you can use the following code:

By using this code, you create a new shared pointer assigned to the variable acq. If you use a second variable and assign it to the first one, then the modification done by one is reflected in the other (as a regular pointer).

btk::Acquisition::Pointer foo = acq; // Shallow copy
std::cout << acq->GetFirstFrame() << std::end; // 1
foo->SetFirstFrame(10);
std::cout << acq->GetFirstFrame() << std::end; // 10

The main difference compared to a regular pointer is when you use the delete function (or used implicitly in the class' destructor). If you delete the content of acq, then you can use again the variable foo to access/modify the acquisition. In the case of a regular pointer, this behavior is not possible and will crash your application.

delete acq;
std::cout << foo->GetFirstFrame() << std::end; // 10
// Memory will be freed automaticaly at the end of the function. No need to use the keyword delete

The only way to copy (or more exactly to create a deep copy) of the object is to use the method Clone.

btk::Acquisition::Pointer foo = acq->Clone(); // Deep copy
std::cout << acq->GetFirstFrame() << std::end; // 1
foo->SetFirstFrame(10);
std::cout << acq->GetFirstFrame() << std::end; // 1
std::cout << foo->GetFirstFrame() << std::end; // 10
// Both, acq and foo will be freed at the end of the function.

This is the same thing when you use a shared pointer as an input/output variable in a function. You do not need to manage the memory of the shared pointer or think about the use of a reference.

#include <btkAcquisition.h>
{
acq->Init(10,200,20,10); // 10 points, 200 frames, 20 analog channels with a number of frames 10 times greater
acq->SetFrequency(100.0); // 100 Hz. Analog's frequency is automatically set to 1000 Hz.
return acq;
};
int main()
{
btk::Acquisition::Pointer acq = defaultInit();
// ...
};

Reading an acquisition

The library BTK can read a dozen of acquisition files with the most popular, the C3D file format. All the known file format read and written in BTK are presented in this page.

To read an acquisition file you need to use the class btk::AcquisitionFileReader. This class, embedded in the module BTKIO, is a process with one output: the read acquisition.

#include <btkAcquisitionFileReader.h>
#include <btkAcquisition.h>
btk::Acquisition::Pointer readAcquisition(const std::string& filename)
{
reader->SetFilename(filename);
reader->Update();
return reader->GetOutput();
};
int main()
{
btk::Acquisition::Pointer acq = readAcquisition("myFile.c3d");
// ...
};

In the previous example, the selection of the file format is done automatically by checking the file's content and/or the file's extension. But, you can also force the file format to use. For that, you have to use the method AcquisitionFileReader::SetAcquisitionIO and give it the chosen file I/O.

#include <btkAcquisitionFileReader.h>
#include <btkC3DFileIO.h>
#include <btkAcquisition.h>
btk::Acquisition::Pointer readC3D(const std::string& filename)
{
reader->SetAcquisitionIO(io); // Will open the given file as a C3D file.
reader->SetFilename(filename);
reader->Update();
return reader->GetOutput();
};
int main()
{
btk::Acquisition::Pointer acq = readC3D("myFile.foo"); // Will read the file as a C3D
// ...
};

All the IO classes are listed in the page Read and written files in BTK

Accessing acquisition's data

An acquisition (btk::Acquisition) is mostly a generic container to store points (btk::Point), analog channels (btk::Analog), events (btk::Event) and metadata (btk::MetaData). You can easily construct a new acquisition or load it from an acquisition file as shown in the above examples.

The others informations stored in an acquisition are the following:

Accessing points

Note: In BTK, the term "point" means all data with 3 components along the time. So, a point can be also a force, a moment, a power, etc. Check btk::Point::GetType() to know the point's type.

In an acquisition, points are stored in a btk::PointCollection::Pointer object. To access to this collection, you can use the method btk::Acquisition::GetPoints.

To access to a point and its content, you can use the methods btk::Acquisition::GetPoint() which return a btk::Point::Pointer object based on the given index or label. If you give an invalid index or an invalid point's label, an exception (btk::OutOfRangeException) will be thrown.
Another way to access to the content of a specific point when you don't know it exist or not in the acquisition is to use the method btk::Acquisition::FindPoint(). Given a point's label, this method returns an iterator associated to the found point. In the case the point is not found, the iterator will point to the end of the list of points.

// btk::Acquisition::Pointer acq = ...
btk::Point::Pointer p = acq->GetPoint(0); // First point in the acquisition
std::cout << p->GetLabel() << std::endl;
p = acq->GetPoint("RHEE"); // Point with the label 'RHEE'. Could throw an exception
std::cout << p->GetFrameNumber() << std::endl;
btk::Acquisition::PointIterator itP = acq->FindPoint("IS_THERE_A_POINT_WITH_THIS_LABEL"); // Let's try to find this point ...
if (itP != acq->EndPoint()) // Point found as the iterator doesn't point to the end of the list
std::cout << (*itP)->GetValues() << std::endl;
else
std::cerr << No point found << std::endl;

When you know the iterator is valid, the best way to access to the content of the point is to use the following code (*itP)->. By using the code *itP. or itP->, you will access only to the methods of the shared pointer object (and not the pointed object by the shared pointer). This (little) drawback is due to the storage of pointers in the list instead of objects.

The use of an iterator has also an other main advantage. It gives you the possibility to access to all the point by iterating it. This gives you a simple and fast way to access to the points' data. To do this, you can use the methods btk::Acquisition::BeginPoint() and btk::Acquisition::EndPoint().

#include <btkAcquisitionFileReader.h>
#include <btkPoint.h>
std::string GetPointTypeAsString(btk::Point::Type t)
{
return "Marker";
else if (t == btk::Point::Angle)
return "Angle";
else if (t == btk::Point::Force)
return "Force";
else if (t == btk::Point::Moment)
return "Moment";
else if (t == btk::Point::Power)
return "Power";
else if (t == btk::Point::Scalar)
return "Scalar";
};
int main()
{
// ...
// btk::Acquisition::Pointer acq = ...
for (btk::Acquisition::PointConstIterator it = acq->BeginPoint() ; it != acq->EndPoint() ; ++it)
{
std::cout << (*it)->GetLabel() << " (" << (*it)->GetDescription() << "): " << GetPointTypeAsString((*it)->GetType());
}
return 0;
}

The code above, iterate in a for loop, the const iterator it from the first point (it = acq->BeginPoint()) to the last one (it != acq->EndPoint()), one by one (++it). The use of an Iterator or a ConstIterator depends of you need. An Iterator gives you the possibility to modify the associated object (see Modifying acquisition's data), while a ConstIterator is a read-only iterator which is enough to access to the points' data.

Accessing analog channels

In an acquisition, analog channels are stored in a btk::AnalogCollection::Pointer object. To access to this collection, you can use the method btk::Acquisition::GetAnalogs.

The way to access to the analog channels is similar to the points. you can use the method btk::Acquisition::GetAnalog() or an iterator (btk::Acquisition::AnalogIterator, btk::Acquisition::AnalogConstIterator with the methods btk::Acquisition::BeginAnalog() and btk::Acquisition::EndAnalog()). You can also use the method btk::Acquisition::FindAnalog() if you don't know if the analog exists in the acquisition.

Accessing events

In an acquisition, events are stored in a btk::EventCollection::Pointer object. To access to this collection, you can use the method btk::Acquisition::GetEvents.

As the points and the analog channels, you can access to the events stored in an acquisition by using the method btk::Acquisition::GetEvent() or an iterator (btk::Acquisition::EventIterator, btk::Acquisition::EventConstIterator with the methods btk::Acquisition::BeginEvent() and btk::Acquisition::EndEvent()). You can also use the method btk::Acquisition::FindEvent() if you don't know if the event exists in the acquisition.

Accessing metadata

A metadata is a generic container to store the acquisition configuration. Or said differently, a metadata contains every informations which cannot be set into markers' trajectories nor analog channels' measures, nor events. For example, if you want to include subject's informations (sex, weight, height, ...) or force platform's configuration (corner's coordinates, analog channel used, ...), then the metadata are the right place to do this. In the C3D file format, the metadata are known as groups and parameters.

To access to the acquisition's metadata, you have to use the method btk::Acquisition::GetMetaData which will return the root of the metadata. From this point, you have to use the methods of the class btk::MetaData. In the case where you want to access to the informations of a metadata, you will use the method btk::MetaData::GetInfo. This method returns a btk::MetaDataInfo::Pointer object which contains the values associated with the metadata.
For example, if you want to access to the value of the metadata SUBJECTS:WEIGHT, you can use the following code:

// btk::Acquisition::Pointer acq = ...
double weight = acq->GetMetaData()->GetChild("SUBJECTS")->GetChild("WEIGHT")->GetInfo()->ToDouble(0);

In the code above, the metadata WEIGHT is the child of the metadata SUBJECTS which is at the root of the metadata in the acquisition. It contains informations and at least one real value inside it.

Even if this code is the most direct way to access to a metadata and its information, it can thrown an exception if the metadata doesn't exist, or worst, can crash your app if there is no info (in this case, the method btk::MetaData::GetInfo returns an empty pointer). You can also extract the wrong (or corrupted) value, if this metadata is not used to store the weight as a real but as a string. The safe way to do this is the following:

// btk::Acquisition::Pointer acq = ...
double weight = 0.0;
btk::MetaData::ConstIterator itG = acq->GetMetaData()->FindChild("SUBJECTS");
if (itG != acq->EndMetaData()) // SUBJECTS exists
{
btk::MetaData::ConstIterator itP = (*itG)->FindChild("WEIGHT");
if (itP != (*itG)->End()) // WEIGHT exists
{
if ((*itP)->HasInfo())
{
btk::MetaDataInfo::Pointer swInfo = (*itP)->GetInfo();
if (swInfo->GetFormat() == btk::MetaDataInfo::Real)
{
if (swInfo->GetDimensions().size() == 0) // Only one value in the metadata
weight = swInfo->ToDouble(0);
}
}
}
}

This safe code can be summarized by the method btk::MetaData::ExtractChildInfo(). The corresponding code is the following.

// btk::Acquisition::Pointer acq = ...
double weight = 0.0;
btk::MetaData::ConstIterator itG = acq->GetMetaData()->FindChild("SUBJECTS");
if (itG != acq->EndMetaData()) // SUBJECTS exists
{
btk::MetaDataInfo::Pointer swInfo = (*itG)->ExtractChildInfo("WEIGHT", btk::MetaDataInfo::Real, 0);
if (swInfo) // Not an empty pointer
weight = swInfo->ToDouble(0);
}

Modifying acquisition's data

Starting from an acquisition you can easily modify it by, for example, removing points, adding events, resizing the number of frames, populates points or analogs values, etc. Lots of these actions are available by using the methods of the class btk::Acquisition.

All the classes contains methods to modify their members. It is easy to extract a point from an acquisition and then set its label or description. This is the same for every object (including analogs channels, events or metadata).

// acq = ...
btk::Point::Pointer foo = acq->GetPoint(0); // Throw an exception if the point at the index 0 doesn't exist.
foo->SetLabel("FOO");
foo->SetDescription("BAR");
foo->SetType(btk::Point::Angle);

In some case, these methods are not enough due to the complexity of the class. For example with the metadata, lots of methods were added to facilitate their creation with values (single or multiple values). It is the same if you want to extract them in a particular format (integer, float, double or string).

// acq = ...
btk::MetaData::Pointer foo = btk::MetaData::New("FOO", "little description");
// Check here that metadata FOO doesn't exist.
// If it is not the case, then it is not added and the method returns false. Otherwise it returns true.
acq->GetMetaData()->AppendChild(foo);
// Even if the metadata are added in the object 'foo', it is also included in the acquisition as we are working with pointer.
foo->AppendChild(btk::MetaData::New("BAR","metadata with a string value", "another description"));
foo->AppendChild(btk::MetaData::New("JOB", (int16_t)12, "This is a metadata with an integer value"));
foo->AppendChild(btk::MetaData::New("BAZ", 180.0, "And here a double"));
// To replace metadata's informations you can use the method btk::MetaData::SetInfo
foo->GetChild("BAR")->SetInfo(btk::MetaDataInfo::New((int8_t)64)); // It is now a byte instead of a string.

Finally, in some case, you want to add children in the metadata but the requirement to check if they exist pollute your code, then you can use the functions btk::MetaDataCreateChild. These functions declared in the file "btkMetaDataUtils.h" do for you the steps required to include a children in a metadata.

// From our previous example, the acquisition has the metadata FOO, FOO:BAR, FOO:JOB, FOO:BAZ
btk::MetaData::Pointer foo = acq->GetMetaData()->GetChild("FOO");
foo->AppendChild(btk::MetaData::New("BAR")); // Will return false and don't append the metadata.
// Use this instead
btk::MetaDataCreateChild(foo, "BAR", "It's working this case!");
// It is also working if the child doesn't exist
btk::MetaDataCreateChild(foo, "FOOBAR", std::vector<int8_t>(2,0)); // A new metadata with an array of bytes.

Note: Even, if you can create an infinite level of metadata, it is strongly advised to only use 2 levels of metadata. The first one corresponds to groups (without informations) and the second one to parameters. This use was introduced in the C3D file format.

Writing an acquisition

Saving an acquisition is as simple as reading one. If you want to save a modified acquisition or would like to convert a format into another one, you simply have to create an btk::AcquisitionFileWriter object, plug the acquisition in the input, set the filename and update the process.

void writeAcquisition(btk::Acquisition::Pointer acq, const std::string& filename)
{
writer->SetInput(acq)
writer->SetFilename(filename);
writer->Update();
};

The choice of the format for the saving is based on the extension of the filename. But you can also set an AcquisitionIO as with the reader.

Filtering an acquisition

The library BTK offers a mechanism of pipelines to easily process an acquisition or one of these parts (points, analogs channels, etc.). A pipeline is defined as a collection of processes linked together by their input(s) and output(s). You can create loops, branches or any more complex workflow.

To link the output(s) and input(s) of processes, you have just to use the methods SetInput() and GetOutput(). Check the documentation of each class for more information.

When your pipeline is created, you need only to update the latest level of processes by using the method Update(). This process will check if all of these inputs are updated before to use them to generate its output. If these inputs are not updated, then the previous processes will be updated to be sure to use valid inputs.

Note: You can also update a process by using its output (if it has one). Use its method Update(), and it will update its parent's process (can be useful when you prefer to store inputs/outputs instead of process).

Simple example: Acquisition file converter

As a simple example, you can create a file converter.

dot_inline_dotgraph_2.png

The following code is based on the example AcquisitionConverter. Depending the given inputs, this code try to convert/save an acquisition file into another one. With this code, you can transform a C3D to a TRC file, or the reverse, etc.

#include <btkAcquisitionFileReader.h>
#include <btkAcquisitionFileWriter.h>
#include <btkMacro.h>
int main(int argc, char *argv[])
{
if (argc != 3)
{
std::cerr << "No enough input arguements.\n\n"
<< "Usage: " << btkStripPathMacro(argv[0]) << " input output\n\n"
<< "Convert an acquisition file into another one, whatever the formats used."
<< std::endl;
return -1;
}
// New instantiation of an acquisition file reader.
// Set the path of the acquisition to read.
reader->SetFilename(argv[1]);
// New instamtiation of an acquisition file writer
// Set the path of the file to write. The acquisition will be exported in this file.
writer->SetFilename(argv[2]);
// Link the writer with the reader.
writer->SetInput(reader->GetOutput());
// Manage possible exceptions during the update of the pipeline.
try
{
writer->Update();
}
catch(btk::Exception& e) // General exception for BTK
{
std::cerr << "Exception: " << e.what() << std::endl;
std::remove(argv[2]);
return -2;
}
catch(std::exception& e) // General exception for the STL
{
std::cerr << "Unknown exception: " << e.what() << std::endl;
std::remove(argv[2]);
return -3;
}
catch(...) // Should never happen ...
{
std::cerr << "Unknown exception";
return -3;
}
return 0;
};

Less simple example: Acquisition merger

A more complex example, is the possibility to create a merger of acquisitions files, convert their units and write the new acquisition in a C3D file.

dot_inline_dotgraph_3.png

To create a such pipeline, we need to declare each process (readers, merger, converter and writer), link them and update the pipeline to generate the result.

#include <btkAcquisitionFileReader.h>
#include <btkMergeAcquisitionFilter.h>
#include <btkAcquisitionUnitConverter.h>
#include <btkAcquisitionFileWriter.h>
int main()
{
// Declare each process
// Readers
// Merger
// Converter
// Writer
// Link the processes together to create the pipeline
Merger->SetInput(0, TRBReader->GetOutput()); // From the TRB reader to the merger
Merger->SetInput(1, ANBReader->GetOutput()); // From the ANB reader to the merger
Merger->SetInput(2, CALForcePlateReader->GetOutput()); // From the CAL reader to the merger
Merger->SetInput(3, XLSOrthoTrakReader->GetOutput()); // From the XLS reader to the merger
Converter->SetInput(Merger->GetOutput()); // From the merger to the converter
C3DWriter->SetInput(Converter->GetOutput()); // From the converter to the C3D Writer;
// Set the option(s) for each process
TRBReader->SetFilename("OneAcquisition.trb");
ANBReader->SetFilename("OneAcquisition.anb");
CALForcePlateReader->SetFilename("OneAcquisition.cal");
XLSOrthoTrakReader->SetFilename("OneAcquisition.xls");
// No option for the merger
// Use the default units for the converter: millimeter (position), degree (angle), newton (force), newton-millimeter (moment) and watt (power).
C3DWriter->SetFilename("OneAcquisition.c3d");
// To execute the pipeline, it is only necessary to update the last process.
// The pipeline will check if the previous are already updated an do it if necessary.
C3DWriter->Update();
// Now check in your folder and you will find a new C3D file!
return 0;
};