![]() |
IOLink
IOL_v1.6.1_release
|
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 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:
ImageViewMultiImageViewLodImageViewIOLink provides helpers to do this operation. These helpers are called providers. Here is an example of use of ImageViewProvider:
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:
You also have the possibility to restrict view capabilities by using following helpers:
Region types describes an axis aligned area, defined by its origin and size. For images, origin and size units are the pixel.
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.
You will get following result with an image of shape (100, 200):
But if you want to access a smaller region, you can define your own region:
This time, you will get following result:
When your region is defined, you can access to data through a method from an ImageView instance that supports the READ capability.
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.
You will get:
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:
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:
When using this variant, all the shape dimensions of size one will be removed.
For example:
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:
At runtime, this mechanism allows users to know in detail the pixel definition of images which they handle.
This can be very useful to allocate a memory buffer before reading a region of an imported ImageView. Here is a simple example:
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:
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:
MultiImageView is another type of view, that stores an array of ImageView instances.
MultiImageView has two capabilities which you can check before accessing to its content:
READ to access to any ImageViews which it containsWRITE to add or remove ImageViews in itMultiImageViewFactory provides a method to create a memory managed MultiImageView. This instance will support all the MultiImageView capabilities.
Following code allows to directly create a MultiImageView filled with a list of ImageViews:
Internal frames are indexed from 0 to (frameCount - 1) like a C array.
A MultiImageView can contain many ImageView instances of any shape, type, or dimensions. And each frame can have heterogeneous capabilities.
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:
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.
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.
You can extract a channel from an image using a method from ImageViewFactory:
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.
Another factory allows you to create an interlaced ImageView from your MultiImageView if contained frames have the same shape, datatype, and are mono-channel:
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.
In ImageView instances, IOLink uses a canonical order of dimensions for reading and writing.
Dimensions are always ordered as following:
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.
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.
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 |
| axesRepresentation() | ImageType |
| bitDepth() | uint8_t |
| valueRange() | Vector2d |
Properties are readable by default.
Properties can be accessed by requesting the ImageProperties instance from an ImageView instance, and navigating until needed information:
Or you can use shortcuts for a direct access to the needed information:
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.
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.
And you get:
As you can see, the conversion of raw metadata value can be tricky and involve some explicit reinterpretations. See next chapter to see how to simplify this task.
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.
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:
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.
Here is an example of conversion from StreamAccess into RandomAccess:
Remark: given stream must have seek capability, otherwise the bridge would not be able to work.
Here is an example of conversion from RandomAccess into StreamAccess: