IOLink Python 1.11.0
Loading...
Searching...
No Matches
Inter-operability

Numpy Arrays

IOLink Python has capacity to inter-operate with NumPy arrays.

Python user can import its data from a NumPy Array into IOLink, and can interpret an ImageView that support the MEMORY_ACCESS capability as a NumPy array, without memory duplication.

Import a Numpy array into IOLink

IOLink python contains a class NumpyInterop which provides services for interoperability with NumPy.

import numpy
from array import array
# a numpy array is created
na = numpy.array([[11, 12, 13], [21, 22, 23]], numpy.uint16)
# a view is created on numpy array data
view = NumpyInterop.from_numpy_array(na)

Good to know, when the numpy array content is modified, the view content is impacted, and vice-versa.

Numpy allows to create arrays with different memory layouts:

  • C-Layout
  • Fortran-Layout
# a numpy array is created with Fortran layout
na_layout_fortran = numpy.array([[11, 12, 13], [21, 22, 23]], numpy.uint16, order ="F")
# another numpy array is created with C layout
na_layout_c = numpy.array([[11, 12, 13], [21, 22, 23]], numpy.uint16, order ="C")
# IOLink handles both layouts
view1 = NumpyInterop.from_numpy_array(na_layout_fortran)
view2 = NumpyInterop.from_numpy_array(na_layout_c)
print(view1.shape())
print(view2.shape())

Console outputs:

VectorXu64(2, 3)
VectorXu64(2, 3)

Dimension order issues

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

Dimensions are always ordered as following:

  • COLUMN
  • ROW
  • SLICE
  • CHANNEL
  • SEQUENCE

If the Numpy array to import into IOLink does not follow the same logic, IOLink allows to configure the import mechanism as follow:

# let's suppose a Numpy array which contains 2 dimensions: ROW and COLUMN
a = numpy.array([[11, 12, 13], [21, 22, 23]], numpy.uint16)
# First option: tell the array describe an image
view = NumpyInterop.from_numpy_array(a, AxesInterpretationId_IMAGE)
# Second option: give the dimensions of the numpy array
view = NumpyInterop.order_from_numpy_array(a, [ImageAxes.ROW, ImageAxes.COLUMN])
# data are reorganized for IOLink, without impact on Numpy Array original content

By doing this, data are reorganized for IOLink, without impact on Numpy array original content, and without any copy.

Export an IOLink in-memory ImageView into an Numpy array

The export only works with ImageView with the MEMORY_ACCESS capability since data must be available in memory for Numpy. Numpy array and ImageView share the same memory. Consequently, a modification in one will impact the other.

As IOLink views and Numpy arrays use different conventions for the order of dimensions, we need to change the indexing.

In Numpy conventions, order is almost inversed (T, Z, Y, X, C) in comparison to IOLink canonical order.

In the case of a view on vectorial data, Vector3u16 for example, the Numpy array will have an additionnal dimension at the end, its size being the number of components of the vectorial data type.

Example: a VOLUME ImageView of shape (12, 4, 6) will result in a Numpy array with a shape of (6, 4, 12) if the view store a scalar data type. If the stored data type is vectorial, for example Vector3u16, the resulting Numpy array will have the following shape (6, 4, 12, 3).

# an in-memory ImageView identified as a VOLUME
type = DataType(DataTypeId.UINT16);
properties = iolink.ImageProperties.from_data_type(type);
properties.image_info.set_axes_interpretation(iolink.AxesInterpretationId.VOLUME)
image = ImageViewFactory.allocate((6, 4), type, properties, None)
# export to Numpy array
np = NumpyInterop.to_numpy_array(image)
print(np.shape)

Console outputs:

(4, 6)

If your ImageView has no axes interpretation, which means dimensions are not clearly identified, the reorder is not possible at conversion into Numpy. Dimensions are kept in original order. Only vectorial DataType will be converted into an additionnal dimension at the end.

# an in-memory ImageView
dtype = DataType(DataTypeId.VEC3_UINT16)
image = ImageViewFactory.allocate((6, 4), dtype)
# export to Numpy array
np = NumpyInterop.to_numpy_array(image)
print(np.shape)

Console outputs:

(6, 4, 3)

As developers can choose to not follow Numpy convention, IOLink provides another method to export an ImageView as a Numpy Array. This time, the developer can choose the order of axes during export. But it needs the ImageView has all its axes well-defined (axes interpretation should not be UNKNOWN).

# an in-memory ImageView identified as a VOLUME
dtype = DataType(DataTypeId.UINT16)
properties = iolink.ImageProperties.from_data_type(dtype)
properties.image_info.set_axes_interpretation(iolink.AxesInterpretationId.VOLUME)
image = ImageViewFactory.allocate((6, 5, 4), dtype, properties, None)
# export to Numpy array by indicating axes order
np = NumpyInterop.order_to_numpy_array(image, [ROW, SLICE, COLUMN])
print(np.shape)

Console outputs:

(5, 4, 6)

In the case of vectorial data, a CHANNEL dimension will be created at export and shall be also ordered:

# an in-memory ImageView identified as a VOLUME
dtype = DataType(DataTypeId.VEC3_UINT16)
properties = iolink.ImageProperties.from_data_type(dtype)
properties.image_info.set_axes_interpretation(iolink.AxesInterpretationId.VOLUME)
image = ImageViewFactory.allocate((6, 5, 4), dtype, properties, None)
# export to Numpy array by indicating axes order
np = NumpyInterop.order_to_numpy_array(image, [ROW, SLICE, CHANNEL, COLUMN])
print(np.shape)

Console outputs:

(5, 4, 3, 6)

RAW I/O

In io module, I/O classes provides APIs to manipulate data streams.

StreamAccess in IOLink allows to manipulate raw data and is quite similar with the abstract class io.RawIOBase (which inherits from io.IOBase.

IOLink provides an inter-operability mechanism to adapt StreamAccess as a io.RawIOBase object.

stream = iolink.StreamAccessFactory.allocate()
ioBaseStream = stream.to_raw_io()
isinstance(ioBaseStream, io.RawIOBase)

Console outputs:

True

Embedding Python and IOLink

When using embedded Python in your application, you may want to be able to send a C++ ImageView instance to Python, and also retrieve a C++ instance from a Python one.

Warning: As the PIP package is only available with IOLink in its release version, if you try to do this interoperability with a debug version of IOLink on the C++ side, you certainly will have errors when smart pointers are freed.

Here is an exemple, using the C Python API:

++
// setup
PyObject* pModule = PyImport_ImportModule("iolink");
PyObject* pDict = PyModule_GetDict(pModule);
PyObject* pClass = PyDict_GetItemString(pDict, "ImageView");
// The method used to pass instance
PyObject* pFunc = PyObject_GetAttrString(pClass, "create_from_pointer");
// `image` being the ImageView instance you want to send to Python
// As we use shared pointer internally, you have to use pointers to shared pointers here.
std::shared_ptr<iolink::ImageView>* ptr = new std::shared_ptr<iolink::ImageView>(image);
// call the method
PyObject* pImage = PyObject_CallObject(pFunc, PyTuple_Pack(1, PyLong_FromVoidPtr(ptr)));
// the method used to retrieve our pointer
PyObject* toPointer = PyObject_GetAttrString(pClass, "to_pointer");
// getting the pointer from the Python instance.
// beware to dereference the tuple here, because it holds a reference to the
// Python instance.
PyObject* args = PyTuple_Pack(1, pImage);
PyObject* instancePointer = PyObject_CallObject(toPointer, args);
Py_DECREF(args);
// get the pointer as an integer and the cast it to a C++ instance.
unsigned long long ptrValue = PyLong_AsUnsignedLongLong(instancePointer);
std::shared_ptr<iolink::ImageView> instance(*reinterpret_cast<std::shared_ptr<iolink::ImageView>*>(ptrValue));

You can do it with MultiImageView and DataFrameView instances too, in a similar manner.

Here is an example for MultiImageView:

++
// setup
PyObject* pModule = PyImport_ImportModule("iolink");
PyObject* pDict = PyModule_GetDict(pModule);
PyObject* pClass = PyDict_GetItemString(pDict, "MutiImageView");
// The method used to pass instance
PyObject* pFunc = PyObject_GetAttrString(pClass, "create_from_pointer");
// `stack` being the MutiImageView instance you want to send to Python
// As we use shared pointer internally, you have to use pointers to shared pointers here.
std::shared_ptr<iolink::MutiImageView>* ptr = new std::shared_ptr<iolink::MutiImageView>(stack);
// call the method
PyObject* pStack = PyObject_CallObject(pFunc, PyTuple_Pack(1, PyLong_FromVoidPtr(ptr)));
// the method used to retrieve our pointer
PyObject* toPointer = PyObject_GetAttrString(pClass, "to_pointer");
// getting the pointer from the Python instance.
// beware to dereference the tuple here, because it holds a reference to the
// Python instance.
PyObject* args = PyTuple_Pack(1, pStack);
PyObject* instancePointer = PyObject_CallObject(toPointer, args);
Py_DECREF(args);
// get the pointer as an integer and the cast it to a C++ instance.
unsigned long long ptrValue = PyLong_AsUnsignedLongLong(instancePointer);
std::shared_ptr<iolink::MutiImageView> instance(*reinterpret_cast<std::shared_ptr<iolink::MutiImageView>*>(ptrValue));

And here is an example for DataFrameView:

++
// setup
PyObject* pModule = PyImport_ImportModule("iolink");
PyObject* pDict = PyModule_GetDict(pModule);
PyObject* pClass = PyDict_GetItemString(pDict, "DataFrameView");
// The method used to pass instance
PyObject* pFunc = PyObject_GetAttrString(pClass, "create_from_pointer");
// `frame` being the DataFrameView instance you want to send to Python
// As we use shared pointer internally, you have to use pointers to shared pointers here.
std::shared_ptr<iolink::DataFrameView>* ptr = new std::shared_ptr<iolink::DataFrameView>(frame);
// call the method
PyObject* pFrame = PyObject_CallObject(pFunc, PyTuple_Pack(1, PyLong_FromVoidPtr(ptr)));
// the method used to retrieve our pointer
PyObject* toPointer = PyObject_GetAttrString(pClass, "to_pointer");
// getting the pointer from the Python instance.
// beware to dereference the tuple here, because it holds a reference to the
// Python instance.
PyObject* args = PyTuple_Pack(1, pFrame);
PyObject* instancePointer = PyObject_CallObject(toPointer, args);
Py_DECREF(args);
// get the pointer as an integer and the cast it to a C++ instance.
unsigned long long ptrValue = PyLong_AsUnsignedLongLong(instancePointer);
std::shared_ptr<iolink::DataFrameView> instance(*reinterpret_cast<std::shared_ptr<iolink::DataFrameView>*>(ptrValue));

Pandas Interoperability

‍There is an iolink submodule pandas_interop that can be used to interoperate iolink.DataFrameView instances and pandas.DataFrame instances. Currently, these methods copies data between the two interfaces.

Limitations: currently, pandas data frames with bool columns are not supported, because we lack good data type to convert it to.

Example:

import iolink
from iolink.pandas_interop import PandasInterop
iolink_frame = iolink.DataFrameViewFactory.allocate(
[2, 5],
["name", "age"],
[iolink.DataTypeId.UTF8_STRING, iolink.DataTypeId.INT64]
)
pandas_frame = PandasInterop.to_pandas(iolink_frame)
# do stuff on pandas frame...
iolink_frame2 = PandasInterop.from_pandas(pandas_frame)
# do iolink stuff...