--- title: "Array orientation: raster order and the R matrix" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Array orientation: raster order and the R matrix} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>", fig.width = 7, fig.height = 5 ) ``` ```{r setup} library(ximage) ``` ## The short version ximage draws data in *raster order*: the first cell of your matrix is the top-left of the picture, and each row of the matrix is a scan line reading left to right, down the page. This is the order of western reading, of `par(mfrow)`, and of every image format and spatial data reader (GDAL, PNG, JPEG, satellite scan lines). If you have a stream of pixel values in that order, the R matrix that ximage expects is ```r matrix(values, nrow, ncol, byrow = TRUE) ``` (note that `nrow` here is the number of scan lines, and that R asks for dimensions as nrow,ncol while image formats declare them as ncol,nrow). The round trip back to the pixel stream is `as.vector(t(m))`, because getting values *out* of a matrix is always column-major - there is no `byrow` for extraction, and none for `array()` at all. Everything else in this vignette is a demonstration of why those two lines are the whole story. ## Seeing the layout `xtext()` draws the values of a matrix at the centre of each cell, in exactly the layout `ximage()` uses. A small labelled matrix makes the convention self-evident. ```{r xtext} (m <- matrix(1:12, nrow = 3, byrow = TRUE)) ximage(m) xtext(m, add = TRUE) ``` Cell `[1, 1]` (value 1) is at the top left. Reading along the top row of the picture reads along the first row of the matrix: 1, 2, 3, 4. The default plot space is the *index space* of the matrix, `0,ncol` on x and `0,nrow` on y, so the picture is `ncol` wide and `nrow` tall and each cell is 1 x 1. Note the y axis: row 1 of the matrix is at the *top*, between y = 2 and y = 3. Axis coordinates increase upward as in any R plot, but row indices increase downward, as in any image. ximage does not pretend otherwise, it simply places the first row at the top where an image expects it. ## What image() does instead Base `image()` uses a different convention: the *rows* of the matrix map to the x axis and the *columns* to the y axis, with the first column at the bottom. The same matrix: ```{r image-vs} op <- par(mfrow = c(1, 2)) image(m, main = "image()") ximage(m, main = "ximage()") par(op) ``` `image()` has effectively rotated the data 90 degrees anticlockwise: what you see is the transpose, flipped. The classic incantation to make `image()` show a matrix the way you would print it is ```r image(t(m[nrow(m):1, ])) ``` and forgetting one half of that (or applying it twice) is the source of a whole genre of plotting bugs. With ximage the incantation is `ximage(m)`. `rasterImage()`, which ximage uses underneath, already follows raster order. What it lacks is everything else: it cannot start a plot, cannot map numeric values through a palette, and cannot handle missing values. ximage is raster-order `rasterImage()` with those limitations removed. ## From pixel stream to matrix and back Spatial readers hand you scan lines: a vector of values in raster order, with the dimensions declared alongside as (ncol, nrow) - x before y, the image-format convention. The two conversions are: ```{r roundtrip} vals <- 1:12 # the pixel stream, raster order dimension <- c(4L, 3L) # (ncol, nrow), as a reader declares it m <- matrix(vals, dimension[2L], byrow = TRUE) m ## and back again all(as.vector(t(m)) == vals) ``` `ximage()` applies this conversion for you when given reader output directly. A list with `dimension` and `extent` attributes (the vapour convention), or a vector or list with a `gis` attribute (the gdalraster convention), plots correctly with no handling at all: ```{r reader} l <- structure(list(vals), dimension = dimension, extent = c(0, 4, 0, 3)) ximage(l) xtext(l, add = TRUE) ``` The rule inside ximage is simple: if the dimensions arrived as an *attribute*, the data is a raster-order stream and gets the `byrow` treatment; a bare R matrix is taken to be already oriented. That is why `ximage(list(volcano))` and `ximage(volcano)` are identical, while reader output - even when a reader has pre-shaped it into a matrix - is reassembled from the stream. ## Multi-band arrays There is no `byrow` for `array()`, so a raster-order stream of RGB bands needs one more step. Band-sequential data (all of band 1, then band 2, then band 3, which is what GDAL readers return) becomes a display-ready array with: ```{r bands} nbands <- 3L stream <- runif(prod(dimension) * nbands) a <- aperm(array(stream, c(dimension[1:2], nbands)), c(2, 1, 3)) dim(a) ## (nrow, ncol, nbands) ximage(a) ``` The `array()` call shapes each band as (ncol, nrow) - filled column-major, so each *column* holds a scan line - and the `aperm()` swaps the first two dimensions to give R display orientation. Again, ximage does this for you when the bands come as a reader-output list; the recipe is shown for when you meet a raw stream. ## Extent: cells have edges The `extent` argument (xmin, xmax, ymin, ymax) declares the *outer edges* of the data, not the centres of the corner cells. The default index-space extent `c(0, ncol, 0, nrow)` follows the same rule: cell `[1, 1]` spans x from 0 to 1 and y from nrow - 1 to nrow, and its centre - where `xtext()` puts the label - is at (0.5, nrow - 0.5). ```{r extent} ximage(m, extent = c(140, 148, -44, -38)) xtext(m, extent = c(140, 148, -44, -38), add = TRUE) axis(1); axis(2) abline(v = seq(140, 148, by = 2), h = seq(-44, -38, by = 2), lty = 3) ``` This edge convention is shared by GDAL geotransforms and by `rasterImage()`, and it is what makes images from different sources overlay exactly: an extent is a statement about where the data sits, not about where its first sample was taken. `xcontour()` uses the matching cell-centre positions, so contour lines and image cells register correctly: ```{r contour} ximage(volcano) xcontour(volcano, add = TRUE, col = "white") ``` ## Summary - ximage displays matrices in raster order: first cell top-left, rows are scan lines. - Stream to matrix: `matrix(vals, nrow, byrow = TRUE)`. Matrix to stream: `as.vector(t(m))`. - Reader dimensions are (ncol, nrow); R dimensions are (nrow, ncol). - Multi-band streams: `aperm(array(vals, c(ncol, nrow, nbands)), c(2, 1, 3))`. - `extent` declares cell edges, cell centres sit half a cell in. - ximage applies all of this automatically for vapour and gdalraster output; the recipes are here for everything else.