| IOLink 1.11.0

Loading...
Searching...
No Matches
Fundamentals

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

This chapters describes a variety of specific cases. 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 a `View` instance into the appropriate derived object:

- `ImageView`
- `MultiImageView`
- `LodImageView`

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` defines a variety of capabilities. They give access to various sets of methods. If you call a method of a non supported capability, a `NotImplementedError` will be raised.

For example, `ImageView` instances can be readable, writable, reshapable, or have their CPU memory buffer accessible. Each of these are listed in the `ImageCapability` enumeration.

There is a method to check if an `ImageView` supports a specific capability:

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

# Access to Image elements

To read and write the value of a single element of the image at a specific position, read/write methods from an `ImageView` instance that supports the `READ` and `WRITE` capabilities are available.

Position in an ImageView is always given as a `VectorXu64`, following IOLink canonical order.

For example, concerning a 2D ImageView, a VectorXu64 containing (25, 32) value will correspond to (offset in COLUMN dimension, offset in ROW dimension).

## Write/Read an element into/from a view

// write into the ImageView at position {25, 32}
VectorXu64 position{25, 32};
// if datatype of ImageView is VEC3_UINT32 (3 components of 32-unsigned-integers)
Vector3u32 typedBuffer{254, 35, 18};
image->write(position, typedBuffer.data());
// the same position can be read back
// using the corresponding read method
Vector3u32 typedReadBuf;
image->read(position, typedReadBuf.data());
// or if you ignore the DataType at compile-time, you can use a well-dimensioned byte array
std::vector<uint8_t> buff(image->dataType().byteCount());
image->read(position, buff.data());
iolink::Vector3u32
Vector< uint32_t, 3 > Vector3u32
Alias for uint32_t 3D vectors.
Definition Vector.h:294

# Region

Region types describes an axis aligned area, defined by its origin and size. For images, origin and size units are the pixel.

## Read a region from a view

A helper is available to create a region with (0, 0) as its origin and a vector as its size. It is useful when you want to read all the image content in one call.

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 with an image of shape (100, 200):

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 an `ImageView` instance that supports the `READ` capability.

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

## Create a view onto a specific region

You may want to create an `ImageView` that references a sub region of another `ImageView`, without copy. It can be useful, for example to split a big image into tiles to speed-up its processing.

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

You may want to extract only a part of a 3D image. To do this you can use a region with a size of 1 on the third dimension:

RegionXu64 regionSlice(VectorXu64{0, 0, 11}, VectorXu64{100, 200, 1});
std::shared_ptr<ImageView> viewSlice = ImageViewFactory::extractRegion(image, regionSlice);

The view on this slice is still three-dimensional, but the user may want a two-dimensional image as a result. There is a variant of the extract region method to do that:

RegionXu64 regionSlice(VectorXu64{0, 0, 11}, VectorXu64{100, 200, 1});
std::shared_ptr<ImageView> viewSlice = ImageViewFactory::extractAdjustedRegion(image, regionSlice);

When using this variant, all the shape dimensions of size one will be removed.

For example:

- view on region of size {100, 200, 1} will result in a shape of {100, 200)}.
- view on region of size {100, 1, 20} will result in a shape of {100, 20)}.
- view on region of size {1, 200, 20} will result in a shape of {200, 20)}.

# 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, element count, primitive type, 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. For example, VEC4_UINT8 allows to store RGBA8888 pixel values.

A user is able 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 useful to allocate a memory buffer before reading a region of an imported `ImageView`. Here is a simple example:

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

## Data Type Switch

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:

An user may need to branch the execution flow for different data types. As types are `DataType` instances you cannot use the `switch` statement and must use the `if` statement instead:

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

You can also build a decision tree, based on the different fields of the type. And you can use the `switch` statement. For example the following double switch is commonly used in IOLink's code:

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

# MultiImage

`MultiImageView` is another type of view, that stores an array of `ImageView` instances.

## 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
}

## Create an empty multi image in memory

`MultiImageViewFactory` provides a method to create a memory managed `MultiImageView`. This instance will support all the `MultiImageView` capabilities.

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

## Create a MultiImageView from a list of ImageView instances

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});

### 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 MultiImageView 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` instances of any shape, type, or dimensions. And each frame can have heterogeneous capabilities.

## Stack MultiImageView content

There is a specific method in `ImageViewFactory` to stack all frames of a `MultiImageView` instance and transform it into an `ImageView`. The frames must fulfill some conditions, all their data type and shape must be the same for the stacking process to work.

The method is used as follows:

// 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<ImageView> volume = ImageViewFactory::stack(multiImg, ImageAxis::SLICE);

The difference between `stackedImg` and `volume` in the example above is that the `stackedImg` dimension will lose its interpretation, as the interpretation of the new dimension is not given. In the `volume` case, the dimension `SLICE` is added, so as it is assumed that the `MultiImageView` stores a list of 2D images, the stacked result being a volume.

# Channels

A channel is a part of your data. `DataType` element count field defines the number of channels of a RGB image for example. In this first example we say that channels are interlaced. An alternative representation can be an image whose data type has an element count of one, and a `CHANNEL` additionnal dimension. This second kind of image is said to have planar channels.

## Extract a channel

You can extract a channel from an image using a method from `ImageViewFactory`:

// 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

You can create a `MultiImageView` from an `ImageView`, with each frame being a channel from the original image. Due to this change of interface, some properties are lost though.

// 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 the 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;

# Create a view from a user buffer

It is possible to create an `ImageView` that will use a user allocated memory buffer for its pixel data. This method is highly unsafe, and the memory allocation and deallocation is the user's responsability.

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());

# Re-order dimensions

In `ImageView` instances, 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. For example a sequence of images will contain `COLUMN`, `ROW`, and `SEQUENCE` dimensions, in this order.

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

You can use a method from `ImageViewFactory` to change 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, {ImageAxis::SLICE, ImageAxis::COLUMN, ImageAxis::ROW});

In previous example, original view contains three dimensions: `SLICE`, `COLUMN`, `ROW`. This view does not follow IOLink order convention. After a call to appropriate method from `ImageViewFactory`, you get an `ImageView` which follows IOLink order: `COLUMN`, `ROW`, `SLICE`.

And now, as dimensions are identified and ordered with the IOLink convention, your `ImageView` axes interpretation field will return `VOLUME`.

# Image Properties

In IOLink, for images, we distinguish two kinds of additional informations: properties and metadata.

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

`SpatialCalibrationProperty` describes the position of the image in 3D space, giving all informations linked to the image's framework. It has the following fields:

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

`ImageInfoProperty` is used to know what the image represents (a volume, a sequence, etc.) and how its elements should be interpreted.

fields | type
--- | ---
interpretation() | ImageInterpretation
hasAlpha() | bool
axesInterpretation() | AxesInterpretation (ImageType in C#)
bitDepth() | uint8_t
valueRange() | Vector2d

## Read Properties

Properties are readable by default.

Properties can be accessed by requesting the `ImageProperties` instance from an `ImageView` instance, and navigating until needed information:

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

Or you can use shortcuts for a direct access to the needed information:

auto imgView = ImageViewFactory::allocate(VectorXu64{100, 200}, DataTypeId::UINT16);
// access to ImageInterpretation using shortcut
imgView.imageInterpretation();

## Modify Properties

You must have `WRITE` access to modify properties of an `ImageView`. In an `ImageView`, the modification of properties is designed to be transactional, which means whole properties shall normally be cloned so as to be modified and then re-assigned to the `ImageView`. The following code explains how to use this mechanism to modify an `ImageView` 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);
}
iolink::Vector3d
Vector< double, 3 > Vector3d
Alias for double 3D vectors.
Definition Vector.h:301

This syntax can seem complicated and restrictive, thus some shortcuts are present to help. But keep in mind that shortcuts for `set` operations trigger a transaction (see above code) with each call. Concerning in-memory `ImageViews`, this has no impact, but it can be an issue for some other implementations.

auto imgView = ImageViewFactory::allocate(VectorXu64{100, 200}, DataTypeId::UINT16);
// Properties can be set if the image view supports write access
if (imgView->support(ImageCapability::WRITE))
{
imgView.setSpatialOrigin(Vector3d{1.0, 3.0, 5.0});
imgView.setSpatialSpacing(Vector3d{2.0, 4.0, 6.0});
imgView.setSpatialDirections(SpatialDirections()); // default value for spatial directions
imgView.setSpatialUnit("cm");
}

# 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 and value pairs.

For example:

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

No default metadata is set at the `ImageView` creation. The user can add them on the fly. Or they 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.
- `MetadataNodeHelper` adds some new useful methods on `MetadataNode`.
- `VariantDataValue` which represents any data value that you want to link to a node.
- `VariantDataValueFactory` which allows you to create `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 can see, the conversion of raw metadata value can be tricky and involve some explicit reinterpretations.

The class `MetadataNodeHelper` offers some methods to simplify some operations on nodes, especially on more complex metadata trees:

// retrieve some node following a path, each separated by a slash
std::shared_ptr<const MetadataNode> node = MetadataNodeHelper::getNode(rootNode, "path/to/some/node");
// check that some path is valid in the metadata tree
bool result = MetadataNodeHelper::exists(rootNode, "path/to/some/node");
// create a new node following the given path
std::shared_ptr<MetadataNode> node = MetadataNodeHelper::createPath(rootNode, "path/to/some/node", value);

`VariantDataValueConverter` is a helper available useful for the conversion of metadata values. If the metadata value is a number, you can directly cast it into any number type (if conversion is possible) or transform it into a string.

auto childAge = root->child("age");
// use converter to automatically cast the (number) 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 using 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);
}
}

# DataAccess conversion

As already explained, `RandomAccess` and `StreamAccess` are two different types of accessors, because they don't provide the same access method to data. The first one needs to have complete access to the whole accessor to jump to desired part at any moment. The second one allow sequential access to data, and is close in functionnalities to the classic streams, for example files of pipes.

There is a method from `RandomAccessFactory` to transform a `StreamAccess` into a `RandomAccess`, and also the opposite way.

## Create a RandomAccess from a StreamAccess

Here is an example of conversion from `StreamAccess` into `RandomAccess`:

// you create a RandomAccess from your stream
std::shared_ptr<RandomAccess> RandomAccess = RandomAccessFactory::fromStream(stream);

Remark: given stream must have seek capability, otherwise the bridge would not be able to work.

## Create a StreamAccess from a RandomAccess

Here is an example of conversion from `RandomAccess` into `StreamAccess`:

// you create a StreamAccess from you accessor
std::shared_ptr<StreamAccess> stream = StreamAccessFactory::fromRandomAccess(random);