IOLink IOL_v1.8.0_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:
ImageView
MultiImageView
LodImageView
IOLink 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.
The class MetadataNodeHelper
offers some methods to simplify some operations on nodes, especially on more complex metadata trees:
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
: