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
na = numpy.array([[11, 12, 13], [21, 22, 23]], numpy.uint16)
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:
na_layout_fortran = numpy.array([[11, 12, 13], [21, 22, 23]], numpy.uint16, order ="F")
na_layout_c = numpy.array([[11, 12, 13], [21, 22, 23]], numpy.uint16, order ="C")
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:
a = numpy.array([[11, 12, 13], [21, 22, 23]], numpy.uint16)
view = NumpyInterop.from_numpy_array(a, AxesInterpretationId_IMAGE)
view = NumpyInterop.order_from_numpy_array(a, [ImageAxes.ROW, ImageAxes.COLUMN])
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).
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)
np = NumpyInterop.to_numpy_array(image)
print(np.shape)
Console outputs:
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.
dtype = DataType(DataTypeId.VEC3_UINT16)
image = ImageViewFactory.allocate((6, 4), dtype)
np = NumpyInterop.to_numpy_array(image)
print(np.shape)
Console outputs:
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).
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)
np = NumpyInterop.order_to_numpy_array(image, [ROW, SLICE, COLUMN])
print(np.shape)
Console outputs:
In the case of vectorial data, a CHANNEL dimension will be created at export and shall be also ordered:
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)
np = NumpyInterop.order_to_numpy_array(image, [ROW, SLICE, CHANNEL, COLUMN])
print(np.shape)
Console outputs:
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:
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:
++
PyObject* pModule = PyImport_ImportModule("iolink");
PyObject* pDict = PyModule_GetDict(pModule);
PyObject* pClass = PyDict_GetItemString(pDict, "ImageView");
PyObject* pFunc = PyObject_GetAttrString(pClass, "create_from_pointer");
std::shared_ptr<iolink::ImageView>* ptr = new std::shared_ptr<iolink::ImageView>(image);
PyObject* pImage = PyObject_CallObject(pFunc, PyTuple_Pack(1, PyLong_FromVoidPtr(ptr)));
PyObject* toPointer = PyObject_GetAttrString(pClass, "to_pointer");
PyObject* args = PyTuple_Pack(1, pImage);
PyObject* instancePointer = PyObject_CallObject(toPointer, args);
Py_DECREF(args);
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:
++
PyObject* pModule = PyImport_ImportModule("iolink");
PyObject* pDict = PyModule_GetDict(pModule);
PyObject* pClass = PyDict_GetItemString(pDict, "MutiImageView");
PyObject* pFunc = PyObject_GetAttrString(pClass, "create_from_pointer");
std::shared_ptr<iolink::MutiImageView>* ptr = new std::shared_ptr<iolink::MutiImageView>(stack);
PyObject* pStack = PyObject_CallObject(pFunc, PyTuple_Pack(1, PyLong_FromVoidPtr(ptr)));
PyObject* toPointer = PyObject_GetAttrString(pClass, "to_pointer");
PyObject* args = PyTuple_Pack(1, pStack);
PyObject* instancePointer = PyObject_CallObject(toPointer, args);
Py_DECREF(args);
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:
++
PyObject* pModule = PyImport_ImportModule("iolink");
PyObject* pDict = PyModule_GetDict(pModule);
PyObject* pClass = PyDict_GetItemString(pDict, "DataFrameView");
PyObject* pFunc = PyObject_GetAttrString(pClass, "create_from_pointer");
std::shared_ptr<iolink::DataFrameView>* ptr = new std::shared_ptr<iolink::DataFrameView>(frame);
PyObject* pFrame = PyObject_CallObject(pFunc, PyTuple_Pack(1, PyLong_FromVoidPtr(ptr)));
PyObject* toPointer = PyObject_GetAttrString(pClass, "to_pointer");
PyObject* args = PyTuple_Pack(1, pFrame);
PyObject* instancePointer = PyObject_CallObject(toPointer, args);
Py_DECREF(args);
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)
iolink_frame2 = PandasInterop.from_pandas(pandas_frame)