IOLink  IOL_v1.1.0_release
Fundamentals

IOLink is quite rich of functionalities to convert/transform/create views, and it should increase in further versions.

This chapter contains specific cases that we describe. It cannot be complete, of course, but should be upgraded case by case.

For more readability, cases are sorted by topic.

View type

View is the IOLink main interface and can represent any data. But in order to access to specific API according to the type of data, you must transform your View into the appropriate derived object:

  • ImageView
  • MultiImageView
  • LodImageView

In the case you receive a View object, this view can point toward any kind of data. You must downcast this object before being able to access to its content.

IOLink provides helpers to do this operation. These helpers are called Providers. Here is an example of use of ImageViewProvider:

if(ImageViewProvider::isImage(view))
{
std::shared_ptr<ImageView> image = ImageViewProvider::toImage(view);
// your code here
}
else
{
// your code here
}

View capabilities

Each kind of View has potentially several capabilities.

For example, an ImageView can be Readable, Writable, Reshapable, Memory, etc. Each of these are listed in the ImageCapability enumeration.

If you use a method linked to a capability that the current ImageView instance does not support, an exception will be thrown.

To check if an ImageView support a capability, you have to use the ImageCapabilitySet instance returned as this:

std::shared_ptr<ImageView> image = ImageViewProvider::toImage(view);
if(image->support(ImageCapability::READ))
{
// your reading code here
}
if(image->support(ImageCapability::WRITE))
{
// your writing code here
}
if(image->support(ImageCapability::RESHAPE))
{
// your reshape code here
}
if(image->support(ImageCapability::MEMORY_ACCESS))
{
// your low level buffer operations here
}

You also have the possibility to restrict view capabilities by using following helpers:

std::shared_ptr<ImageView> image = ImageViewProvider::toImage(view);
std::shared_ptr<ImageView> r_img = ImageViewProvider::toReadOnly(image);
// r_img has only Read capability now
std::shared_ptr<ImageView> w_img = ImageViewProvider::toWriteOnly(image);
// w_img has only Write capability now

Region

Region type is a basic type which represents an area (defined by its origin and size). For images, origin and size unit is pixel.

Read a region from a view

An helper is available to easily create a full region from a view and another one helps to retrieve the size (in pixels for images) of a region:

std::shared_ptr<ImageView> image = ImageViewFactory::allocate(VectorXu64{ 100, 200 }, DataTypeId::UINT16);
RegionXu64 fullRegion = RegionXu64::createFullRegion(image->shape());
std::cout << "region dimension = " << fullRegion.dimensionCount() << std::endl;
std::cout << "region min = " << fullRegion.min().toString() << std::endl;
std::cout << "region max = " << fullRegion.max().toString() << std::endl;
std::cout << "pixel count = " << fullRegion.elementCount()<< std::endl;

You will get following result:

region dimension = 2
region min = (0, 0)
region max = (99, 199)
Pixel counts = 20000

But if you want to access a smaller region, you can define your own region:

RegionXu64 region(VectorXu64{5, 6}, VectorXu64{10, 15});
std::cout << "region dimension = " << region.dimensionCount() << std::endl;
std::cout << "region min = " << region.min().toString() << std::endl;
std::cout << "region max = " << region.max().toString() << std::endl;
std::cout << "pixel count = " << region.elementCount()<< std::endl;

This time, you will get following result:

region dimension = 2
region min = (5, 6)
region max = (14, 20)
Pixel counts = 150

When your Region is defined, you can access to data through a method from ImageView:

// allocate memory to store pixels of given region
size_t bufferSize = region.elementCount() * image->dataType().byteCount();
std::vector<uint16_t> buffer(bufferSize);
image->readRegion(region, buffer.data());

Create a view onto a specific region

You have an ImageView and you want to create another view onto a small part of the same image. This can be helpful to isolate a part of a big image and transmit it to a third party, for example.

As explained above to read a specific region, you must first define a Region. Then you can use the appropriate method from the ImageViewFactory to create a view onto this region.

RegionXu64 smallRegion(VectorXu64{10, 20}, VectorXu64{100, 150});
std::shared_ptr<ImageView> viewSmallRegion = ImageViewFactory::extractRegion(view, smallRegion);
std::cout << "Shape of new view: " << viewSmallRegion->shape() << std::endl;

You will get:

Shape of new view: (100, 150)

Change ImageView dimension with region view

This time, let's say you have a volume (ImageView with 3 dimensions (W,H,D)), and you want to isolate a extractSliceFromAxis of this volume. Solution is to use Region view with the previous section explainations.

Finally, you will get another volume, this time with a flat dimension.

Example: From your volume, you want to isolate the Nth slice from the depth dimension. You will create a region view as follow:

RegionXu64 regionSlice(VectorXu64{0, 0, N}, VectorXu64{W, H, 1});
std::shared_ptr<ImageView> viewSlice = ImageViewFactory::extractRegion(image, regionSlice);

But this new sliceView has now a shape {W, H, 1}. Which means you still have a 3-dimensional ImageView, with a flat last dimension. The issue is even more complicated if you try to isolate a extractSliceFromAxis in other dimensions. Your view would have the shape {1, H, D} or {W, 1, D}.

So, if you wish to handle your extractSliceFromAxis as a 2-dimensional image, you have the possibility to pack the region as follow:

RegionXu64 regionSlice(VectorXu64{0, 0, N}, VectorXu64{W, H, 1});
std::shared_ptr<ImageView> viewSlice = ImageViewFactory::extractAdjustedRegion(image, regionSlice);

The flat dimension (size = 1) will be removed, and dimensions will be shifted if necessary to obtain an ImageView with 2 consecutives dimensions.

  • SliceView on SLICE dimension will have shape {W, H}.
  • SliceView on COLUMN dimension will have shape {H, D}.
  • SliceView on ROW dimension will have shape {W, D}.

Data type

Introspection

IOLink uses its own definition of basic types, called DataType. Theses types are created to make introspection possible. From any type, you can access at runtime to informations like bit depth, dimension, type of scalar, and interpretation.

Here are the basic types managed by IOLink:

Name C type Primitive type Size Bits Interpretation
UINT8 uint8_t UNSIGNED_INTEGER 1 8 RAW
UINT16 uint16_t UNSIGNED_INTEGER 1 16 RAW
UINT32 uint32_t UNSIGNED_INTEGER 1 32 RAW
UINT64 uint64_t UNSIGNED_INTEGER 1 64 RAW
INT8 int8_t SIGNED_INTEGER 1 8 RAW
INT16 int16_t SIGNED_INTEGER 1 16 RAW
INT32 int32_t SIGNED_INTEGER 1 32 RAW
INT64 int64_t SIGNED_INTEGER 1 64 RAW
FLOAT float FLOATING_POINT 1 32 RAW
DOUBLE double FLOATING_POINT 1 64 RAW
UTF8_STRING const char* UNICODE_STRING 1 8 RAW
UTF16_STRING const char16_t* UNICODE_STRING 1 16 RAW
UTF32_STRING const char32_t* UNICODE_STRING 1 32 RAW
VEC2_UINT8 uint8_t[2] UNSIGNED_INTEGER 2 8 RAW
VEC2_UINT16 uint16_t[2] UNSIGNED_INTEGER 2 16 RAW
VEC2_UINT32 uint32_t[2] UNSIGNED_INTEGER 2 32 RAW
VEC2_UINT64 uint64_t[2] UNSIGNED_INTEGER 2 64 RAW
VEC2_INT8 int8_t[2] SIGNED_INTEGER 2 8 RAW
VEC2_INT16 int16_t[2] SIGNED_INTEGER 2 16 RAW
VEC2_INT32 int32_t[2] SIGNED_INTEGER 2 32 RAW
VEC2_INT64 int64_t[2] SIGNED_INTEGER 2 64 RAW
VEC2_FLOAT float[2] FLOATING_POINT 2 32 RAW
VEC2_DOUBLE double[2] FLOATING_POINT 2 64 RAW
VEC3_UINT8 uint8_t[3] UNSIGNED_INTEGER 3 8 RAW
VEC3_UINT16 uint16_t[3] UNSIGNED_INTEGER 3 16 RAW
VEC3_UINT32 uint32_t[3] UNSIGNED_INTEGER 3 32 RAW
VEC3_UINT64 uint64_t[3] UNSIGNED_INTEGER 3 64 RAW
VEC3_INT8 int8_t[3] SIGNED_INTEGER 3 8 RAW
VEC3_INT16 int16_t[3] SIGNED_INTEGER 3 16 RAW
VEC3_INT32 int32_t[3] SIGNED_INTEGER 3 32 RAW
VEC3_INT64 int64_t[3] SIGNED_INTEGER 3 64 RAW
VEC3_FLOAT float[3] FLOATING_POINT 3 32 RAW
VEC3_DOUBLE double[3] FLOATING_POINT 3 64 RAW
VEC4_UINT8 uint8_t[4] UNSIGNED_INTEGER 4 8 RAW
VEC4_UINT16 uint16_t[4] UNSIGNED_INTEGER 4 16 RAW
VEC4_UINT32 uint32_t[4] UNSIGNED_INTEGER 4 32 RAW
VEC4_UINT64 uint64_t[4] UNSIGNED_INTEGER 4 64 RAW
VEC4_INT8 int8_t[4] SIGNED_INTEGER 4 8 RAW
VEC4_INT16 int16_t[4] SIGNED_INTEGER 4 16 RAW
VEC4_INT32 int32_t[4] SIGNED_INTEGER 4 32 RAW
VEC4_INT64 int64_t[4] SIGNED_INTEGER 4 64 RAW
VEC4_FLOAT float[4] FLOATING_POINT 4 32 RAW
VEC4_DOUBLE double[4] FLOATING_POINT 4 64 RAW
COMPLEX_FLOAT float[2] FLOATING_POINT 2 32 COMPLEX
COMPLEX_DOUBLE double[2] FLOATING_POINT 2 64 COMPLEX
MATRIX2_FLOAT float[4] FLOATING_POINT 4 32 SQUARE_MATRIX
MATRIX2_DOUBLE double[4] FLOATING_POINT 4 64 SQUARE_MATRIX
MATRIX3_FLOAT float[9] FLOATING_POINT 9 32 SQUARE_MATRIX
MATRIX3_DOUBLE double[9] FLOATING_POINT 9 64 SQUARE_MATRIX
MATRIX4_FLOAT float[16] FLOATING_POINT 16 32 SQUARE_MATRIX
MATRIX4_DOUBLE double[16] FLOATING_POINT 16 64 SQUARE_MATRIX

UINT8 is the typical type used for grayscale images which pixels are stored on 8 bits. VEC4_UINT8 allows to store RGBA8888 pixel values. etc...

Customer is free to create new types using the following mechanism:

// creation of a 5-dimensional unsigned data type coded on 128 bits
DataType CUSTOM_TYPE = DataType(PrimitiveTypeId::UNSIGNED_INTEGER, 5, 128);

At runtime, this mechanism allows users to know in detail the pixel definition of images which they handle.

void displayPixelInfo( std::shared_ptr<ImageView> view)
{
DataType type = view->dataType();
std::cout << "Scalar bit depth: " << type.bitDepth() << std::endl;
std::cout << "Number of components: " << type.dimension() << std::endl;
std::cout << "Size of one pixel in bytes:" << type.byteCount() << std::endl;
std::cout << "Size of one pixel in bits: " << type.bitCount() << std::endl;
std::cout << "Scalar type of pixel: " << DataType::extractScalarType(type) << std::endl;
//Each type can provide its data range (i.e. unsigned 8 bits => min= 0 , max= 256 )
const Vector2d range = DataType::standardRange(type);
std::cout << "Type: Range (" << range[0] << ", " << range[1] << ")" << std::endl;
}

This can be very usefull to size a memory buffer before reading a region of an imported ImageView. Here is a simple example:

DataType type = view->dataType();
RegionXu64 smallPart( VectorXu64({10, 10}), VectorXu64({5, 3}) );
std::vector<uint8_t> buffer(type->byteCount() * smallPart.elementCount());
// whatever given ImageView, now my buffer is correctly sized to read the region
...

Discrimination

You can need to handle views of any dataType, and you want to discriminate each one for specific treatments. Using switch statement is not possible to process each DataType values separately. You must use if statement as follow:

DataType type = view->dataType();
if(type == DataTypeId::INT64)
{
// do stuff
}
else if(type == DataTypeId::FLOAT)
{
// do stuff
}
else if(type == DataTypeId::VEC3_UINT32)
{
// do stuff
}
else
{
// do stuff
}

The following variant is also mainly used, and should be preffered if you only care for the DataType's dimension and bit depth:

switch (type.primitiveType()):
{
case PrimitiveTypeId::UNSIGNED_INTEGER:
switch(type.bitDepth())
{
case 8:
// do stuff
break;
case 16:
// do stuff
break;
...
}
break;
case PrimitiveTypeId::SIGNED_INTEGER:
switch(type.bitDepth())
{
case 8:
// do stuff
break;
case 16:
// do stuff
break;
...
}
break;
case PrimitiveTypeId::FLOATING_POINT:
switch(type.bitDepth())
{
case 32:
// do stuff
break;
case 64:
// do stuff
break;
}
break;
default:
// do stuff
}

MultiImage

MultiImageView is a type of View (as ImageView) which allows to store many ImageViews.

Create an empty MultiImage in memory

Following code shows how to create a MultiImageView with Read/Write capabilities.

These capabilities allow to read MultiImageView content and also modify it (by removing or adding new sub-images)

std::shared_ptr<MultiImageView> multiImgView = MultiImageViewFactory::create();

Create a MultiImageView from a list of ImageViews

Following code allows to directly create a MultiImageView filled with a list of ImageViews:

std::shared_ptr<ImageView> image1 = ...;
std::shared_ptr<ImageView> image2 = ...;
std::shared_ptr<ImageView> image3 = ...;
std::shared_ptr<MultiImageView> multiImageView = MultiImageViewFactory::create({image1, image2, image3});

Capabilities

MultiImageView has two capabilities which you can check before accessing to its content:

  • READ to access to any ImageViews which it contains
  • WRITE to add or remove ImageViews in it
// check if multiImageView has READ capability
if(multiImgView->support(MultiImageCapability::READ)
{
// do stuff
}
// check if multiImageView has WRITE capability
if(multiImgView->support(MultiImageCapability::WRITE)
{
// do stuff
}

Access MultiImage content

Internal frames are indexed from 0 to (frameCount - 1) like a C array.

// to retrieve count of internal frames (ImageViews)
uint64_t frameCount = multiImgView->frameCount();
// parse all frames
for(uint64_t idx = 0; idx < framecount ; idx++)
{
std::shared_ptr<ImageView> frame = multiImgView->frame(idx);
// do stuff
}

Modify MultiImage content

multiImgView->addFrame(ImageViewFactory::allocate(VectorXu64{100, 200}, DataTypeId::UINT8));
multiImgView->addFrame(ImageViewFactory::allocate(VectorXu64{50, 500, 300}, DataTypeId::UINT32));
//remove second frame
multiImgView->removeFrame(1);

A MultiImageView can contain many ImageView of any shape, type or dimension. And each frame (internal ImageView) can have heterogenous capabilities.

Stack MultiImageView content

A MultiImageView content can be stacked.

std::shared_ptr<MultiImageView> multiImg = ...; // set of ImageViews with same shape and datatype
// stack the images to add a new dimension (added in last position)
std::shared_ptr<ImageView> stackedImg = ImageViewFactory::stack(multiImg);
// stack the images to add SLICE dimension
std::shared_ptr<ImageIew> volume = ImageViewFactory::stack(multiImg, ImageDimension::SLICE);

In first stack method call, the dimension to add is not given. A new dimension will be added, but no information will be available in dimension info properties to indicate the type of the result image.

In second stack method call, the dimension to add is given. This dimension shall not already exist in dimension info properties of the stacked frames. If the stacked images had a known ImageType in dimension info properties, this information will be updated with the added dimension.

Region

Region type is a basic type which represents an area (defined by its origin and size). For images, origin and size unit is pixel.

Read a region from a view

An helper is available to easily create a full region from a view. Another one help to retrieve the size (in pixels for images) of a region.

std::shared_ptr<ImageView> image = ImageViewFactory::allocate(VectorXu64{100, 200}, DataTypeId::UINT16);
const RegionXu64 fullRegion = RegionXu64::createFullRegion(view->shape());
std::cout << " region dimension = " << fullRegion.dimensionCount() << std::endl;
std::cout << " region min = " << fullRegion.min()[0] << ":" << fullRegion.min()[1] << std::endl;
std::cout << " region max = " << fullRegion.max()[0] << ":" << fullRegion.max()[1] << std::endl;
std::cout << " Pixel Counts = " << fullRegion.elementCount()<< std::endl;

You will get following result:

region dimension = 2
region min = 0:0
region max = 99:199
Pixel counts = 20000

But if you want to access a smaller region, you can define your own region:

Region2u64 region(Vector2u64{5, 6}, Vector2u64{10, 15});
std::cout << " region dimension = " << region.dimensionCount() << std::endl;
std::cout << " region min = " << region.min()[0] << ":" << region.min()[1] << std::endl;
std::cout << " region max = " << region.max()[0] << ":" << region.max()[1] << std::endl;
std::cout << " Pixel Counts = " << region.elementCount()<< std::endl;

This time, you will get following result:

region dimension = 2
region min = 5:6
region max = 14:20
Pixel counts = 150

When your Region is defined, you can access to data through the read region method:

// allocate memory to store pixels of given region
std::vector<uint16_t> buffer(region.elementCount());
image->readRegion(region, buffer.data());

Channels

A channel is one component of your pixel. Your pixel is defined according to the Data Type of your view.

Extract a channel

A factory allows you to extract an ImageView from another one, which will only contain a specific channel (RED, GREEN, ALPHA, ....).

// we have a view with a datatype whose channel count is 3
std::shared_ptr<ImageView> imgView3U16 = ImageViewFactory::allocate(VectorXu64{10, 15}, DataTypeId::VEC3_UINT16);
// extract first channel
std::shared_ptr<ImageView> chan0_ImgView = ImageViewFactory::extractChannel(imgView3U16, 0);
// extract second channel
std::shared_ptr<ImageView> chan1_ImgView = ImageViewFactory::extractChannel(imgView3U16, 1);
// extract third channel
std::shared_ptr<ImageView> chan2_ImgView = ImageViewFactory::extractChannel(imgView3U16, 2);

Deinterlace an ImageView

Another factory allows you to create a MultiImageView from your deinterlaced ImageView:

// we have a view with a datatype whose channel count is 3
std::shared_ptr<ImageView> imgView3U16 = ImageViewFactory::allocate(VectorXu64{10, 15}, DataTypeId::VEC3_UINT16);
std::shared_ptr<MultiImageView> deinterlaced_img = MultiImageViewFactory::deinterlace(imgView3U16);
// this will print '3' as frame count
std::cout << "Nb frames : "<< deinterlaced_img->frameCount() << std::endl;

Interlace a MultiImageView

Another factory allows you to create an interlaced ImageView from your MultiImageView if contained frames have same shape, datatype, and are mono-channel:

// we have a MultiImageView with 3 frames whose datatype is scalar
DataType type = DataTypeId::INT16;
VectorXu64 shape{10, 15};
// create views with same shape and datatype
std::shared_ptr<ImageView> imgView = ImageViewFactory::allocate(shape, type);
std::shared_ptr<ImageView> imgView2 = ImageViewFactory::allocate(shape, type);
std::shared_ptr<ImageView> imgView3 = ImageViewFactory::allocate(shape, type);
std::shared_ptr<MultiImageView> multiImgView = MultiImageViewFactory::create();
multiImgView->addFrame(imgView1);
multiImgView->addFrame(imgView2);
multiImgView->addFrame(imgView3);
// interlace
std::shared_ptr<ImageView> interlaced_view = ImageViewFactory::interlace(multiImgView);
// this will print 'new datatype: VEC3_INT16'
std::cout << "new datatype: " << interlaced_view->datatype().toString() << std::endl;

Raw data

You can have to handle raw image data, which means any DataAccess containing un-encoded image data (no header, no metadata, no compression). This case is particular because it is the only one that IOLink can directly manage.

Create a view onto raw data

Following code shows how to create an readable Imageview from a pointer of data.

std::vector<uint8_t> buff(64);
// create an ImageView from a pointer toward raw image data (whose size is 64 bytes)
std::shared_ptr<ImageView> image = ImageViewFactory::fromBuffer(VectorXu64{8, 8}, DataTypeId.UINT8, buff, buff.size());

An ImageView can be created from a RandomAccess using the appropriate method from ImageViewFactory. As a RandomAccess describes only an array of bytes, you must pass shape and datatype information to this method.

An example of an ImageView creation from a file using a RandomAccess:

// shape and type to know in advance
VectorXu64 shape = VectorXu64{ 640, 480 };
DataType type = DataTypeId::UINT8;
// create stream from a file path, and create a bridge to convert it into RandomAccess
std::shared_ptr<RandomAccess> rawAccess = RandomAccessFactory::fromStream(StreamAccessFactory::openFileRead(filePath));
// create a view onto this RandomAcess (which must be readable)
std::shared_ptr<Imageview> viewRaw = ImageViewFactory::fromRandomAccess(rawAccess, shape, type);

Re-order dimensions

In ImageView, IOLink uses a canonical order of dimensions for reading and writing.

Dimensions are always ordered as following:

  • COLUMN
  • ROW
  • SLICE
  • CHANNEL
  • SEQUENCE

Of course, some dimensions can be missing in your ImageView, depending on your data. But the order is always the same.

(i.e. a sequence of image will contain dimensions COLUMN, ROW and SEQUENCE. In this order)

Data which are not initially created with IOLink, as show in the previous chapter, may need to be re-ordered to follow this convention.

You can use a method from ImageViewFactory to indicate the dimension order of your data. This will indicate to IOLink how to access them properly.

std::shared_ptr<Imageview> viewRaw; // a view onto raw data. Dimensions are SLICE, COLUMN, ROW
// stuff here to fill viewRaw
auto iolink_imageView = ImageViewFactory::reinterpretAxes(viewRaw, {ImageDimension::SLICE, ImageDimension::COLUMN, ImageDimension::ROW});

In previous example, original view contains 3 dimensions (SLICE, COLUMN, ROW). This view does not follow IOLink order. After a call to appropriate method from ImageViewFactory, you get a View which follows IOLink order: COLUMN, ROW, SLICE.

And now, as dimensions are identified and ordered with IOLink convention, your ImageView's ImageType will be recognized as an IMAGE_SEQUENCE.

Properties and metadata

In IOLink, for images, we distinguish 2 kinds of additional information:

  • Properties
  • Metadata

Metadata are all the informations stored with the image, either from a file, or added by the user. It can be anything, these informations are provided in the view through a tree of key/data peer.

Patient Name : Robert DUPONT
Age : 64
Dummy : [24, 6, 7]
...

Properties are specific to the "image" view type , and are considered as mandatory to correctly interpret the image content. These properties are either provided in the original image file (as Metadata), or set by the user or to default values at initialization.

Image Properties

Spatial Calibration properties (see SpatialCalibrationProperty):

This property describes the position of the image in space, by giving all attributes of the local axes of the image's referential.

fields type
origin() Vector3d
spacing() Vector3d
directions() SpacialDirections
unit() string

Image Info properties (see ImageInfoProperty):

This property is used to know what the image represents (a volume, a sequence...) and how its pixels (voxels, elements) should be interpreted.

fields type
interpretation() ImageInterpretation
hasAlpha() bool
axesRepresentation() ImageType
bitDepth() uint8_t
valueRange() Vector2d

Properties and Metadata are readable by default.

Read Properties

auto imgView = ImageViewFactory::allocate(VectorXu64{100, 200}, DataTypeId::UINT16);
SpatialCalibrationProperty calib = imgView->properties()->calibration();
ImageInfoProperty imageInfo = imgView->properties()->imageInfo();

Modify Properties

You must have WRITE access to properties.

auto imgView = ImageViewFactory::allocate(VectorXu64{100, 200}, DataTypeId::UINT16);
auto properties = imgView->properties();
// Properties can be set if the image view supports write access
if (imgView->support(ImageCapability::WRITE))
{
// replace whole calibration object at once
SpatialCalibrationProperty newCalib(Vector3d{1.0, 3.0, 5.0}, Vector3d{2.0, 4.0, 6.0}, SpatialDirections(), "cm");
auto newProperties = ImageProperties::fromProperties(properties, newCalib);
// or change one field of calibration directly
// original properties are cloned and modified one by one
newProperties = properties->clone();
newProperties->calibration().setOrigin(Vector3d{ 1.0, 3.0, 5.0 });
newProperties->calibration().setSpacing(Vector3d{ 2.0, 4.0, 6.0 });
newProperties->calibration().setDirections(SpatialDirections());
newProperties->calibration().setUnit("cm");
imgView->setProperties(newProperties);
}

Metadata

No default metadata is set at ImageView creation. The user can add them on the fly. Or he can directly access to metadata extracted from an image file.

Classes to create and/or handle Metadata are:

  • MetadataNode which represents a node.
  • MetadataNodeFactory which allows you to create MetadataNode objects.
  • VariantDataValue which represent any data value that you want to link to a node.
  • VariantDataValueFactory which allows you to create a VariantDataValue objects.

Create Metadata

std::shared_ptr<ImageView> image = ImageViewFactory::allocate(VectorXu64{12, 12}, DataTypeId::UINT8);
std::shared_ptr<MetadataNode> root = MetadataNodeFactory::create("root", nullptr);
root->addChild(MetadataNodeFactory::create("name", VariantDataValueFactory::create("John Doe")));
root->addChild(MetadataNodeFactory::create("age", VariantDataValueFactory::create(42)));
image->setMetadata(root);

Access to Metadata (from key)

auto root = image->metadata();
std::cout << "root children:" << root->childCount() << std::endl;
auto child0 = root->child("name");
auto child1 = root->child("age");
auto valraw_child0 = child0->value()->raw();
auto valraw_child1 = child1->value()->raw();
const std::string str_val_child0 = reinterpret_cast<const char* const*>(valraw_child0[0]);
const std::string str_val_child1 = reinterpret_cast<const char* const*>(valraw_child1[0]);
std::cout << "Name:" << str_val_child0 << std::endl;
std::cout << "Age:" << str_val_child1 << std::endl;

And you get:

root children: 2
Name: John Doe
Age: 42

As you see, conversion of raw metadata value can be tricky and involve some explicit reinterpretations. See next chapter to see how to simplify this task.

Interpret Metadata

A converter is available for basic conversion of metadata. See VariantDataValueConverter. If metadata value is a numerical, you can directly cast it into any numerical type (if conversion is possible) or transform it into String.

auto childAge = root->child("age");
// use converter to automatically cast the (numerical) value to desired type
uint32_t age = VariantDataValueConverter::toUint32(childAge->value());
float age_float = VariantDataValueConverter::toFloat(childAge->value());
double age_double = VariantDataValueConverter::toDouble(childAge->value());
std::string age_string = VariantDataValueConverter::toString(childAge->value());

Parse Metadata

You can also parse all Metadata without knowing its content.

The recursive way is the easiest one, but you can also parse Metadata by the iterative way.

Here is an example of recursive method:

void parseNode(std::shared_ptr<MetadataNode> node)
{
// process current node here
for (auto child : *node)
{
parseNode(*child);
}
}

Storages

If you haven't read the Storage concept part, I would recommend you to have a look on it for basic informations.

Conversions

As already explained, RandomAccess and StreamAccess are two different types of accessor, because they don't provide the same access method to data. First one needs to have the full access to whole accessor to jump to desired part at any moment. Second one does not allow to always access to any part of data, and not directly either, but is more convenient for data loading (since you don't necessary need to entirely load the data)

There is a method from RandomAccessFactory to transform StreamAccess into RandomAccess, and another one for the opposite.

Create a RandomAccess from a StreamAccess

Here is an example of conversion from StreamAccess into RandomAccess:

// you have a StreamAccess created (for example) from a std::iostream
std::shared_ptr<std::iostream> stdStream = ...;
std::shared_ptr<StreamAccess> stream = StreamAccessFactory::fromStdStream(stdStream);
// you create a RandomAccess from it
std::shared_ptr<RandomAccess> RandomAccess = RandomAccessFactory::fromStream(stream);

Remark: given stream must have seek capability, otherwise RandomAccess cannot be created

Create a StreamAccess from a RandomAccess

Here is an example of conversion from RandomAccess into StreamAccess:

// you have a RandomAccess
std::shared_ptr<RandomAccess> reader = RandomAccessFactory::fromBuffer(buffIn.data(), buffIn.size());
// you create a StreamAccess from it
std::shared_ptr<StreamAccess> stream = StreamAccessFactory::fromRandomAccess(reader);

Raw data

You can have to handle raw image data, which means any accessor containing un-encoded image data (no header, no metadata, no compression). This case is particular because it is the only one that IOLink can directly manage.

Create an ImageView from an accessor

To consider raw data as an image, you have the possibility to create an ImageView from a RandomAccess using ImageViewFactory. You just have to define the shape and data type to use at creation.

Following code shows how to create an readable Imageview from a pointer of data.

// create a RandomAccess from a pointer toward raw image data (whose size is 64 bytes)
std::shared_ptr<RandomAccess> reader = RandomAccessFactory::fromBuffer(ptrData, 64);
// size of the imageView shall be less than or equal to 64 bytes
VectorXu64 shape{8, 8};
auto ImgView = ImageViewFactory::fromRandomAccess(reader, shape, DataTypeId::UINT8);

If you want to create an ImageView, this time, onto raw data coming from a file, you must create a RandomAccess onto these file data. Of course, you must know in advance the shape and data type of your image.

// shape and type to know in advance
VectorXu64 shape = VectorXu64{ 640, 480 };
DataType type = DataTypeId::UINT8;
// create stream from a file path, and create a bridge to convert it into RandomAccess
std::shared_ptr<RandomAccess> rawAccess = RandomAccessFactory::fromStream(StreamAccessFactory::openFileRead(filePath));
// create a view onto this RandomAcess (which must be readable)
std::shared_ptr<Imageview> viewRaw = ImageViewFactory::fromRandomAccess(rawAccess, shape, type);