vigra - Multi-Dimensional Arrays

[ VIGRA Homepage | Function Index | Class Index | Namespaces | File List | Main Page ]

Multi-Dimensional Arrays |

## Section Contents

- Basic MultiArray Usage
- Array Indexing via Coordinates
- One-dimensional Indexing and Scan-Order Iterator
- Important MultiArray Methods
  - The MultiArrayView Interface
  - subarray(p,q)
  - bind<M>(i) and bindAt(M, i)
  - expandElements(k) and bindElementChannel(i)
  - transpose()
  - isUnstrided(k)

# Basic MultiArray Usage

vigra::MultiArray is the most fundamental data structure in VIGRA. It holds a rectangular block of values in arbitrary many dimensions. Most VIGRA functions operate on top of MultiArray or the associated class MultiArrayView (see The MultiArrayView Interface).

A 2D image can be interpreted as a matrix, i.e. a 2-dimensional array, where each element holds the information of a specific pixel. Internally, the data are stored in a single 1-dimensional piece of memory, and MultiArray encapsulates the entire mapping between our familiar 2-dimensional notation and the raw memory. Pixels in an image are identified by a coordinate pair (x,y), where indexing starts at 0. That is, the pixels in a 800x600 image are indexed by `x = 0,...,799 and y = 0,...,599`. The principle analoguously extends to higher dimensions.

The structure of a multidimensional array is given by its `shape` vector, and the length of the shape vector is the array's dimension. The dimension must be fixed as a template parameter at compule time, while the shape is passed to the array's constructor. The second important template parameter is the pixel's `value_type`, as you know it form `std::vector`.

To represent the data of a gray scale image, we just need to store one value per pixel, so we choose a 2-dimensional array, where each element has the `unsigned char` type (in VIGRA, this type is also available as vigra::UInt8). We instantiate a gray scale image object like this:

#include <vigra/multi_array.hxx>
using namespace vigra; // for brevity in the examples - don't do this in header files!
int width = ..., height = ...;
MultiArray<2, UInt8> image(Shape2(width, height));

By default, VIGRA arrays are always zero-initialized. Another initial value can be provided in the constructor, or later via the `init()` function or the assignment operator:

MultiArray<2, UInt8> image(Shape2(width, height), 255); // init with value 255
image.init(128); // same effect, different initial value
image = 100; // yet another way

The `Shape2` typedef also exists for higher dimensions up to five as `Shape3` etc. If you need even more dimensions, use `MultiArrayShape<N>::type` instead, were N is the number of dimensions:

// dimension 0 runs from 0, 1, ..., 299
// dimension 1 runs from 0, 1, ..., 199
// dimension 2 runs from 0, 1, ..., 99
MultiArray<3, double> volume(Shape3(300, 200, 100));
MultiArray<7, float> array7D(MultiArrayShape<7>::type(20, 10, ...));

When storing RGB images we obviously can't simply use the unsigned char type because every pixel contains 3 numbers: values for red, green and blue. Mathematically, you want to store a data vector for each pixel. To this end, VIGRA provides the `vigra::RGBValue<ValueType>` class. So for RGB-images just use:

MultiArray<2, RGBValue<UInt8> > rgb_image(Shape2(256, 128));

vigra::RGBValue<ValueType> is a specialised 3-dimensional vector containing ValueType elements. Arbitrary short vectors can be stored in the `TinyVector<ValueType, SIZE>` class, which is the base class of RGBValue. It's length must be specified at compile time in the template parameter `SIZE`. Vectors whose length is known at compile time are very useful for the compiler to produce highly optimized code. Therefore, `Shape2` and it's higher-dimensional coursins are instances of `TinyVector` as well.

Alternatively you can use a 3-dimensional array `vigra::MultiArray<3, unsigned char>` to represent a color image. The third dimension has size 3 and contains the information for the red, green and blue channel.

MultiArray<3, UInt8> rgb_array(Shape3(256, 128, 3));

However, we are not going to use this form in the tutorial because `vigra::RGBValue<ValueType>` provides many helpful methods that are not available when color is just another array dimension.

# Array Indexing via Coordinates

The easiest way to access the values of an array is via the coordinates. A tuple of coordinates can again be specified by the appropriate shape object, which must be passed to the array's indexing operator:

int width = 300, height = 200;
MultiArray<2, int> image(Shape2(width, height));
// set all elements to 3
image.init(3);
// print pixel at x=1 and y=2
int x=1, y=2;
std::cout << image[Shape2(x,y)] << std::endl; // output: 3

Important Remark: Notice that VIGRA follows the mathematical convention of the index order: dimension 0 corresponds to the x (horizontal) coordinate, dimension 1 to the y (vertical) coordinate, and so on. Accordingly, dimension 0 is changing fastest in memory: when we increase x by one, we get to the next memory location. In matrix jargon, this is also known as Fortran order. Many image processing libraries (e.g. Image Magick, OpenCV, and Qt) use the same convention. However, others like Matlab and numpy, use the reverse order (so called C order). Don't be confused!

Internally, shape objects are implemented in terms of the powerful vigra::TinyVector class. This means that shape objects support the usual mathematical operations like addition, multiplication and scalar products. Coordinate computations can thus be performed on entire coordinate objects at once - there is no need to manipulate the individual coordinates separately.

Nonetheless, in some circumstances it is more convenient to provide coordinates individually rather than in a shape object. This is possible with round brackets (x,y):

// access via individual coordinates
image(1,2) = 22;

This kind of indexing is supported for dimensions up to five, and only if the array's dimension is known (this is not always the case: in a generic function where the dimension is a template parameter, you must use shape objects). In combination with the method `shape(n)`, that returns the length of the n-th dimension, we can use the coordinates to set the element of an entire row or column:

// set all elements of first row to 13
for (int i = 0; i < image.shape(1); i++)
image(1,i) = 13;

On first sight, individual coordinates may be necessary to iterator of the image or parts of it. But the following example shows that the same effect can be achieved with a shape object that is allocated outside the loop: 3rd column of a 8x4-matrix (initialized with 5) to 7.

// instantiate shape object (zero intialized by default)
Shape2 p;
// bind x=2
p[0] = 2;
// iterator over row 2
for(p[1]=0; p[1]<image.shape(1); ++p[1])
image[p] = 7;

We will discuss more powerful methods to access certain parts of an array in section Important MultiArray Methods.

# One-dimensional Indexing and Scan-Order Iterator

Regardless of the array's dimension, it is always possible to access elements with 1-dimensional index, its scan-order index, via the normal indexing operator. For example, `array[1]` refers to the index of the second array element. Defining a scan order is often called flattening of an array, because a high-dimensional data structure is accessed like a 1-dimensional vector. Notive that scan-order access in VIGRA does not require the data to be copied.

VIGRA defines scan-order by considering the dimensions from front to back. Thus, items are accessed such that only the x coordinate is incremented, while y (and possibly further coordinates) are held fixed at 0. When x is exhausted, y is incremented by one and the iteration starts again at x=0. To control iteration, the function `array.size()` returns the total number of elements:

MultiArray<2, int> intArray(Shape2(3,2));
for(int k=0; k<intArray.size(); ++k=
intArray[k] = k+1;
// the array now contains the values
//
// 1 2 3
// 4 5 6

Alternatively, scan-order access can be achieved with an STL-compatible iterator pair obtained by calling `array.begin()` and `array.end()`. Continuing with the example above, we can write:

// declare an alias for the iterator type
typedef MultiArray<2, int>::iterator Iter;
// iterate over intArray and print the elements in scan order
for (Iter i = intArray.begin(); i != intArray.end(); ++i)
std::cout << *i << " ";
std::cout << std::endl;
// output: 1 2 3 4 5 6

The iterator is implemented by class `StridedScanOrderIterator` which encapsulates all the bookkeeping necessary to get the elements in the correct order, even when the array was transposed (see below).

Scan-order access is useful to implement pointwise operations, e.g. the addition of two matrices. The following code adds two matrices and stores the result in the first one:

MultiArray<2, int> matrix1(Shape2(3,3)),
matrix2(Shape2(3,3));
... // fill in data
// use indexing
for (int i=0; i < matrix1.size(); ++i)
matrix1[i] += matrix2[i];
// use iterators
for (Iter i = matrix1.begin(), j = matrix2.begin(); i != matrix1.end(); ++i, ++j)
i += *j;

This is convenient because the actual high-dimensional shapes of the arrays are of no significance for point-wise operations as long as the shapes match. Be careful: the arrays themselves have no way of checking this condition. So thefollowing code using two transposed shapes is perfectly valid C++, but has probably not the intended effect:

MultiArray<2, int> matrix3(Shape2(3,2)),
matrix4(Shape2(2,3)); // transposed shape of matrix3
... // fill in data
for (int i=0; i < matrix3.size(); ++i)
matrix3[i] += matrix4[i]; // works, but may not have the intended effect

By the way: VIGRA provides the += operator (and is cousins) to write this more concisely, and this operator throws an exception if the shapes don't match:

matrix1 += matrix2; // works fine!
matrix3 += matrix4; // error: shape mismatch!

For more information on mathematical operations on arrays see the multi_math module.

As mentioned, VIGRA's scan order is similar to the NumPy-method `array.flatten()`. You use it, to copy a multi-dimensional array into an one-dimensional array, or to access elements in flattened order. The only difference is that NumPy uses "C-order" , i.e. the rightmost dimension takes priority, whereas VIGRA uses Fortran-order, i.e. the leftmost dimension takes priority. A method like flatten can be implemented in VIGRA as:

MultiArray<2, int> intArray(Shape2(3,2));
for(int k=0; k<intArray.size(); ++k=
intArray[k] = k+1;
// create 1D-array of appropriate size
std::vector<int> flatArray(intArray.size());
// copy 2D-array into 1D-array using the STL
std::copy(intArray.begin(), intArray.end(), flatArray.begin());
// print 1D-array on console
// (same output as printing from the StridedScanOrderIterator directly)
for (std::vector<int>::iterator i = flatArray.begin(); i != flatArray.end(); ++i)
std::cout << *iter << " ";
std::cout << std::endl;

To show the difference between VIGRA and NumPy we'll add the NumPy output, i.e. the result when we had used C-order in the code above:

    flatArray - index     0       1       2       3       4       5
    -----------------------------------------------------------------
    VIGRA-output:         1       2       3       4       5       6
    intArray - index    [0,0]   [1,0]   [2,0]   [0,1]   [1,1]   [2,1]
    -----------------------------------------------------------------
    NumPy-output:         1       4       2       5       3       6
    intArray - index    [0,0]   [0,1]   [1,0]   [1,1]   [2,0]   [2,1]

To change the axis priorities of the StridedScanOrderIterator, look at the transpose-function in the next section.

# Important MultiArray Methods

This part of the tutorial explains important methods of MultiArray. However, before we proceed, we need to introduce the class vigra::MultiArrayView.

## The MultiArrayView Interface

A vigra::MultiArrayView has the same interface as a MultiArray (with the exception of `reshape()` and `swap()`), but it doesn't own its data. Instead, it provides a view onto the data of some other array. In contrast, a MultiArray owns its data and is responsible for freeing it in the destructor. MultiArrays are automatically converted into MultiArrayViews when needed.

The point of this distinction is that MultiArrayViews can be used to access and manipulate the same data in many different ways without any need for creating copies. For example, we can work with a 2-dimensional slice of a volume dataset (i.e. a lower dimensional part of a 3D array) without first copying the slice into a 2D image. This is possible whenever the desired view can be realized by just manipulating the internal mapping from indices and shapes to memory locations, and not the memory layout itself.

This possibility – which is similarly implemented in other packages like Matlab and numpy – is a key ingredient for efficient computations with multi-dimensional arrays. Thus, most VIGRA functions actually receive MultiArrayViews to maximize flexibility. This section describes the most important ways to create new MultiArrayViews from an existing array or view. The complete documentation is available in the vigra::MultiArrayView reference.

## subarray(p,q)

This method creates a rectangular subarray of your array between the points p and q, where p (the starting point of the subregion) is included, q (the ending point) is not. `subarray` does not change the dimension of the array (this is the task of the various `bind`-methods).

To give an example, we create a 4x4 array that consitst of a checkerboard with 2x2 squares:

MultiArray<2, float> array_4x4(Shape2(4,4)); // zero (black) initialized
// paint the upper left 2x2 square white
array_4x4.subarray(Shape2(0,0), Shape2(2,2)) = 1.0;
// likewise for the lower right 2x2 square, but this time we
// store the array view explicitly for illustration
MultiArrayView<2, int> lower_right_square = array_4x4.subarray(Shape2(2,2), Shape2(4,4));
lower_right_square = 1.0;
// contents of array_4x4 now:
// 1 1 0 0
// 1 1 0 0
// 0 0 1 1
// 0 0 1 1

The positions p and q are specified with the familiar `Shape` objects. In this example we simply overwrite parts of the array. The following larger example uses `subarray` to output a half-sized subimage around the center of the original image: subimage_tutorial.cxx
The relevant part of this code is shown here (the functions `importImage` and `exportImage` are described in section Image Input and Output):

// read image given as first argument
// file type is determined automatically
ImageImportInfo info(argv[1]);
if(info.isGrayscale())
{
// read image from file
MultiArray<2, UInt8> imageArray(info.shape());
importImage(info, imageArray);
// we want to cut out the center of the original image, such that the
// size of the remaining image is half the original
Shape2 upperLeft = info.shape() / 4,
lowerRight = info.shape() - upperLeft;
// create subimage around center for output
MultiArrayView<2, UInt8> subimage = imageArray.subarray(upperLeft, lowerRight);
// write the subimage to the file provided as second command line argument
// the file type will be determined from the file name's extension
exportImage(subimage, ImageExportInfo(argv[2]));

After reading the (here: gray scale) image data to an array we need to calculate the coordinates of our subimage. In this case we want to cut out the middle part of the image. Afterwards we write the subimage into a new array. Look at the result:

input file | subimage output file

## bind<M>(i) and bindAt(M, i)

These methods bind axis M to the index i and thus reduce the dimension of the array by one. The only difference between the two forms is that the axis to be bound must be known at compile time in the first form, whereas it can be specified at runtime in the second.

Binding is useful when we want to access and/or manipulate a particular row or column of an image, or a single slice of a volume. In principle, the same can also be achieved by explicit loops, but use of `bind` often leads to more elegant and more generic code. Consider the following code to initialize the third column of an image with the constant 5:

// initialize 200x100 image
MultiArray<2, int> array2d(Shape2(200,100)); // zero initialized
// initialize column 2 with value 5 using a loop
for(int y=0; y<array2d.shape(1); ++y)
array2d(2, y) = 5;
// the same using bind
array2d.bind<0>(2) = 5;

NumPy-Users are familiar with the bind mechanism as "slicing". The example above written in numpy syntax becomes:

    array2d[2, :] = 5      // NumPy-equivalent of array2d.bind<0>(2) = 5

You can also initialize a lower-dimensional array with the bind-method:

// initialize new 1D array with 3rd column of a 2D array
MultiArray<1, int> array1d = array2d.bind<0>(2);

The array `array1d` contains the elements the 3rd column of `array2d`. This bahavior nicely illustrates the difference between a copy and a view: `array1d` contains a copy of the 3rd column, whereas the `bind` function only creates a new view to the existing data in `array2d`.

At this point we have to distinguish between the classes `MultiArray` and `MultiArrayView` . MultiArray inherits from MultiArrayView and contains the memory management of the array. With MultiArrayView we can view the data stored in a MultiArray. The code above produces a copy of the 3rd column of intArray. If we change the elements of `lowArray` nothing happens to `intArray` .

// initialize 200x100 image
MultiArray<2, int> array2d(Shape2(200,100)); // zero initialized
// initialize new 1D array with 3rd column of a 2D array
MultiArray<1, int> array1d = array2d.bind<0>(2);
// overwrite element [0] of array1d
array1d[0] = 1;
// this has no effect on the original array2d
// output: 0 1
std::cout << array2d(2, 0) << " " << array1d[0] << std::endl;
// initialize a view and overwrite element [0]
MultiArrayView<1, int> array_view = array2d.bind<0>(2);
array_view[0] = 2;
// now, the original array2d has changed as well
// output: 2 2
std::cout << array2d(2, 0) << " " << array_view[0] << std::endl;

Moving on to image processing we'll give an example how you can flip an image by using bind. We read a gray scale image into a 2-dimensional array called `imageArray` . Then we initalize a new array `newImageArray` of the same dimension and size and set the first row of `newImageArray` to the values of the last row of `imageArray` , the second row to the values of the second last row and so on. Hence, we flip the image top to bottom.

// mirror the image horizontally
// (for didactic reasons, we implement this variant explicitly,
// note that info.height()-1 is equal to the last y-index)
for (int y=0; y<info.height(); y++)
{
newImageArray.bind<1>(y) = imageArray.bind<1>(info.height()-1-y);
}

However, you don't need to implement a method like this yourself because VIGRA already provides the function reflectImage(). We use this function to flip the image left-to-right:

// mirror the image vertically
reflectImage(imageArray, newImageArray, vertical);

The complete example can be found in mirror_tutorial.cxx. (This program needs an infile and an outfile as command-line arguments and contains additional I/O code which will be explained in section Image Input and Output.) Here you can see what happens to an input file:

input file | mirrored top to bottom |  | mirrored left to right |

For completeness, there are five additional versions of the bind()-method:

bindInner(i) with scalar or multi-dimensional index i:
  if i is an `integer` , the innermost dimension (axis 0) is fixed to i,
  if i is `MultiArrayShape<M>::type` (a shape of size M), then the M innermost dimensions (axes 0...M-1) are fixed to the values in the shape vector
bindOuter(i) with scalar or multi-dimensional index i:
  if i is an `integer` , the outmost dimension (axis N-1) is fixed to i,
  if i is `MultiArrayShape<M>::type` (a shape of size M), then the M outmost dimensions (axes N-M ... N-1) are fixed to the values in the shape vector
diagonal() :
  Create a 1-dimensional view to the diagonal elements of the original array (i.e. `view[i] == array(i,i,i)` for a 3D original array).

The opposite of binding - inserting a new axis - is also possible. However, since we cannot alter the internal memory layout and thus cannot insert additional data elements, a new axis must be singleton axis, i.e. an axis with length 1. The argument of `insertSingletonDimension(k)` determines the position of the new axis, with `0 <= k <= N` when the original array has `N` dimensions:

MultiArray<2, int> array(20,10);
std::cout << array.insertSingletonDimension(1).shape() << "\n"; // prints "(20, 1, 10)"

## expandElements(k) and bindElementChannel(i)

When the array elements are vectors (i.e. vigra::TinyVector or vigra::RGBValue), we can expand these elements into an addtional array dimension:

MultiArray<2, TinyVector<int, 3> > vector_array(20, 10);
std::cout << vector_array.shape() << "\n"; // prints "(20, 10)"
MultiArrayView<3, int> expanded(vector_array.expandElements(2));
std::cout << expanded.shape() << "\n"; // prints "(20, 10, 3)"

The argument `k` of `expandElements(k)` determines the desired position of the channel axis, i.e. the index that refers to the vector elements. When the original vector array has `N` dimensions (not counting the channel axis), it is required that `0 <= k <= N`.

Often, we are only interested in a single channel of a vector-valued array. This can be achieved with the function `bindElementChannel(i)`. For example, we can extract the green channel (i.e. channel 1) from an RGB image like this:

MultiArray<2, RGBValue<UInt8> > rgb_array(20, 10);
MultiArrayView<2, UInt8> green_channel(rgb_array.bindElementChannel(1));

This is simply an abbreviation for `rgb_array.expandElements(0).bindInner(1)`.

## transpose()

Everyone is familiar with the `transpose()` function of a matrix (i.e. a 2-dimensional array). Once again, this operation is possible without copying the data by just manipulating the internal access functions. The following example demonstrates the difference between a transposed copy and view:

// create array
MultiArray<2, int> base_array(Shape2(4,4));
// init array such that pixel values are equal to their x coordinate
for (int i = 0; i < base_array.size(); i++)
{
base_array[i] = i % base_array.shape(0);
}
std::cout << "base_array:\n";
print(base_array);
// create a transposed array and a transposed view
MultiArray<2, int> transarray = base_array.transpose();
MultiArrayView<2, int> transarrayView = base_array.transpose();
std::cout << "transarray:\n";
print(transarray);
std::cout << "transArrayView:\n";
print(transarrayView);
// set transarray to 5
transarray = 5;
std::cout << "base_array after setting transarray to 5\n(no change, since transarray is a copy):\n";
print(base_array);
// set transarrayView to 5
transarrayView = 5;
std::cout << "base_array after setting transarrayView to 5\n(base_array changes because transarrayView is a view):\n";
print(base_array);

The output is:

    base_array:
    0  1  2  3
    0  1  2  3
    0  1  2  3
    0  1  2  3
    transarray:
    0  0  0  0
    1  1  1  1
    2  2  2  2
    3  3  3  3
    transArrayView:
    0  0  0  0
    1  1  1  1
    2  2  2  2
    3  3  3  3
    base_array after setting transarray to 5
    (no change, since transarray is a copy):
    0  1  2  3
    0  1  2  3
    0  1  2  3
    0  1  2  3
    base_array after setting transarrayView to 5
    (base_array changes because transarrayView is a view):
    5  5  5  5
    5  5  5  5
    5  5  5  5
    5  5  5  5

The function `MultiArrayView::transpose()` generalizes transposition to arrays of arbitrary dimensions. Here, it just reverses the order of the axes: axis 0 becomes axis N-1, axis 1 becomes axis N-2 and so on. In the following example we transpose a 5D array and print out its shape.

// transposing a 5D array
// instantiate 5D array
MultiArray<5, int> array5D(Shape5(1,2,3,4,5));
// print the shape of the original array
std::cout << "Shape of array5D: " << array5D.shape() << "\n";
// transpose array
MultiArrayView<5, int> arrayview5D = array5D.transpose();
// print the shape of transposed array
std::cout << "Shape of array5D view after default transpose(): " << arrayview5D.shape() << "\n";

The output is:

    Shape of array5D: (1, 2, 3, 4, 5)
    Shape of array5D view after default transpose(): (5, 4, 3, 2, 1)

Finally, `MultiArrayView::transpose()` can also be called with a shape object that specifies the desired permutation of the axes: When `permutation[k] = j`, axis `j` of the original array becomes axis `k` of the transposed array (remember, that VIGRA counts the axes from 0):

// transpose to an explicitly specified axis permutation
MultiArrayView<5, int> arrayview5D_permuted = array5D.transpose(Shape5(2,1,3,4,0));
// print the shape of transposed array
std::cout << "Shape of array5D view after user-defined transpose(): " << arrayview5D_permuted.shape() << "\n";
std::cout << " (applied permutation 2 => 0, 1 => 1, 3 => 2, 4 => 3, 0 => 4 to the axes)\n";

The permutation in the example is 2,1,3,4,0. Thus, original dimension 0 appears in the last position of the new view, original dimension 2 appears in the first position, and so on as demonstrated by the output of the example:

    Shape of array5D view after user-defined transpose(): (3, 2, 4, 5, 1)
        (applied permutation 2 => 0, 1 => 1, 3 => 2, 4 => 3, 0 => 4 to the axes)

When we transpose an image about the major diagonal, we can simply use the view created by `MultiArrayView::transpose()`. However, transposition about the minor diagonal requires a new image, which can be filled by transposeImage() like this:

#include <iostream>
#include <vigra/multi_array.hxx>
#include <vigra/impex.hxx>
#include <vigra/basicgeometry.hxx>
using namespace vigra;
int main(int argc, char ** argv)
{
if(argc != 3)
{
std::cout << "Usage: " << argv[0] << " infile outfile" << std::endl;
std::cout << "(grayscale images only, supported formats: " << impexListFormats() << ")" << std::endl;
return 1;
}
// choose diagonal for transpose
std::cout << "Transpose about which diagonal?\n";
std::cout << "1 - major\n";
std::cout << "2 - minor\n";
int mode;
std::cin >> mode;
try
{
// read image given as first command line argument
MultiArray<2, UInt8> imageArray;
importImage(argv[1], imageArray);
if(mode == 1)
{
// when transposing about the major diagonal, we can simply
// write a transposed view to the file given as second argument
exportImage(imageArray.transpose(), argv[2]);
}
else
{
// when transposing about the minor diagonal, we need a new
// image with reversed shape to hold the transposed data
MultiArray<2, UInt8> transposed(reverse(imageArray.shape()));
transposeImage(imageArray, transposed, minor);
exportImage(transposed, argv[2]);
}
}
catch (std::exception & e)
{
// catch any errors that might have occurred and print their reason
std::cout << e.what() << std::endl;
return 1;
}
return 0;
}

The result is:

input file | transpose about major diagonal | transpose about minor diagonal

In VIGRA, image transposition is also implemented in function `vigra::transposeImage(...)`. The difference is that transposeImage() copies the image data, whereas MultiArrayView::transpose() just changes the internal mapping from indices to memory locations.

Important note: Transposing an array also changes the direction of the StridedScanOrderIterator. Imagine a 3x4- matrix. Scan-order means that we iterate from left to right, row by row. Now, let's transpose the matrix to a 4x3 view. Than, scan-order in the new view is again left to right, row by row. However, in the original matrix this now corresponds to a transposed scan: from top to bottom, column by column. The same applies to the array's index operator with integer argument.

## isUnstrided(k)

A MultiArray always accesses its elements in consecutive memory order, i.e. `&array[i] == &array.data()[i]` for all `i` in the range `[0, array.size())`. However, this does in general not hold for MultiArrayViews, because changing array access is the whole point of view creation. Sometimes, it is necessary to find out if a view still has consecutive, unstrided memory access, for example when you want to pass on the view's data to an external library that only accepts plain C arrays: When the view happens to be unstrided, you can avoid to create a copy of the data. You can determine this with the function `isUnstrided(k)` which returns `true` when the array is unstrided up to dimension `k` (`k` defaults to `N-1`, i.e. the entire array must be unstrided):

MultiArray<2, int> array(20,10);
std::cout << array.isUnstrided() << " " << array.transpose().isUnstrided() << "\n"; // prints "true false"

© Ullrich Köthe (ullrich.koethe@iwr.uni-heidelberg.de) Heidelberg Collaboratory for Image Processing, University of Heidelberg, Germany | html generated using doxygen and Python vigra 1.10.0 (Thu Jan 8 2015)