|
cartodata 6.0.0
|
Volumes rely on the allocator system of somaio: see Allocators system.
First, a Volume is built using any allocator method. This means a DataSource and an AllocatorContext are provided to it. It can remain unallocated (no data buffer) if it is not used directly, but its data values should not be accessed until it is allocated.
A subvolume can be opened into an existing volume. This used to be a separate class, VolumeView, up to aims-4.3, but has been merged with the regular Volume class in aims-4.4. A view is used either to operate on smaller data blocks than the whole volume itself, or to simulate the "volume with borders" concept, or to "paste" a volume (buffer or file) into a larger one. Access to views is granted from one (view) volume into another one, using the Volume::refVolume(), and Volume::posInRefVolume() methods. Depending on the AllocatorContext and DataSource used for both the bigger volume and the view, different behaviours can be achieved. In the following we name "Volume" the "larger" volume, and "View" the view in the former.
Here I am speaking of Volume and View implemented as two separate classes, but they might be implemented in the same class: a Volume would be a view into another if it holds a reference to another (bigger) Volume.
In any case, a View is a complete Volume and has all functionalities of a Volume. This includes the capability to allow other views into it.
A View holds a reference-counting pointer to the Volume it is open on. So the original bigger Volume cannot be deleted while a View looking into it is still living.
Using cleverly the different combinations of allocators and DataSource on Volumes and Views allows to implement every memory-saving operations on very large files: memory mapping, block-by-block sequential reading, etc.
There are several ways to iterate over the voxels of a volume:
In terms of performance, the impact depends on the "weight" of the operation performed with each voxel, the lighter this operation, the more impacted it is by the iteration overhead. For a very simple operation (like incrementing each voxel value), if we take the Volume::iterator performance as reference 1., we can expect approximately:
| Iteration type | Perf. | N-D support | view support |
|---|---|---|---|
| Volume::iterator with blitz++ support | 1. | yes | yes |
| pointers | 10. | yes | no |
| accessors with blitz++ support | 5-10 | no | yes |
| NDIterator | 1. | yes | yes |
| line_NDIterator | 10. | yes | yes |
Thus in most cases the best compromise between performance and flexibility (support for the general N-dimension case, support for views inside larger volumes) is using line_NDIterator.
For comparison, here are some tests results on a Core i7-4600U 2.1GHz CPU (laptop) using different compilers on linux and trying different types of implementations, all running on the same machine. The reference (factor 1.) was actually 4.2e+08 voxels/s using iterators with gcc 4.9. The implementation choice was taken as the best between implementations (marked with a star), and depends on the compiler. It can be seen that there are striking behavior changes between compilers. Moreover it seems that there are also different behaviors using different machines / processors, so the choices we have made might not be the best ones for every machine. However for now they are hard-coded using preprocessor #if directives.
| Iteration type | gcc 4.9 | gcc 6 | clang 3.8 | clang 6 |
|---|---|---|---|---|
| iterator | 1. | 1. | 0.9 | 0.8 |
| pointers | 10.9 | 10.9 | 10.6 | 11.2 |
| accessors (raw 4D) | 4.6 * | 4.6 * | 5.4 | 5.4 |
| accessors (blitz) | 4.5 | 4.6 | 10.7 * | 11. * |
| vector accessor (loop) | 0.2 | 2.1 * | 3.8 * | 5.4 * |
| vector accessor (switch on dim) | 3.5 * | 1.2 | 0.5 | 0.5 |
| vector accessor (blitz) | 0.3 | 0.7 | 0.17 | 0.2 |
| NDIterator | 0.9 | 1.2 | 1.2 | 1.2 |
| line_NDIterator | 10.7 | 10.4 | 10.6 | 11. |
A volume is not just an array. Each index in the array corresponds to a direction in space. By default, the first axis (X) corresponds to right->left head orientation, the second (Y) is anterior -> posterior, the third (Z) is superior -> inferior. Remaining axes are free of interpretation.
Axes orientation has a code using 3 letters for the initials of the axes directions: see this AIMS document and http://www.grahamwideman.com/gw/brain/orientation/orientterms.htm.
Using this convention, the default orientation in Cartodata / AIMS is "LPI".
Before CartoData 5.2 this LPI orientation was fixed.
In CartoData 5.2 the orientation can be changed.
Note that there are several axes orientations at different places and levels:
Array indices and memory orientation can differ, using strides. Starting in Cartodata 5.2, strides can also be negative, which means we can switch between increasing and decreasing axes directions without copying voxels data.
Thus "loading in LPI orientation" may mean 2 different things: load in LPI memory layout, meaning that voxels are contiguous in the R->L direction, or it may mean that the first index increases from right to left. We choose to take the second convention: "orientation" means indices axes orientation, whatever the actual order of voxels in memory. The voxels order is thus called "memory layout".
This doc is part of the C++ documentation of CartoData, but is mainly valid also for python exports. A python-oriented documentation and notebook are available on the PyAIMS documentation.
Querying orientation
Axes (indices) orientation can be queries using:
To know the orientation of a Volume, and to help transfroming to other orientations, the Referential class can help:
Note that Referential gives only information about the indices axes, not on the memory layout (which is handled by strides).
Storage (disk) layout orientation:
Note that the storage layout may be undefined if the volume header property "storage_to_memory" is not defined. This is especially the case if the volume has not be loaded from a file, but created in memory.
Flipping
To flip a volume to a different orientation, we may use:
After this operation we will use different indices: vol(z, y, x).
However voxels are left untouched in this operation: only strides have been modified. Thus the operation is relatively lightweight (just header properties have been modified).
If we want to actually copy voxels in a given orientation, we should use:
and it's possible to specify a memory layout different from the indices orientation:
Here indices will use the ASR orientation, but voxels will actually be in memory in LIA orientation.
Writing
Soma-IO and AIMS support writing non-LPI Volumes. This means that writing a Volume after any Volume::flipToOrientation() will have in the same result on disk.
Some formats like NIFTI support to write voxels in any orientation. This can be controlled via the Volume header "storage_to_memory" transformation. It has to be appropriately set before writing the Volume. Note that this transformation is in voxels (int coefficients) and gives the transformation between disk voxels layout and the indices axes.
Reading
Most, if not all, formats will assume a Volume allocated in LPI orientation for indices. However the strides management will usually allow to read volumes in arbitrary memory layout orientation. When the IO system allocates the volume for reading, it is possible to specify via an option the memory layout to be used. In the options dictionary, the property "orientation" can specify it:
The "orientation" property value is a string, which uses the letters for axes orientation ("LPI", "RAST" etc). Optionally the value "storage" will specify that we want to use the same memory layout as the disk storage layout.
Note that this specifies the memory layout, but the axes indices will still be LPI oriented. If you need another orientation, just use Volume::flipToOrientation() afterwards, as it is a lightweight, non-copy operation.
As usual the orientation option may be passed as an URI option in the filename (see Volumes IO in CartoData)