SimpleITK in R attempts to make all the common R array operators and arithmetic work on images.
First we load SimpleITK and set up a viewer for notebooks, using the jet colourmap.
library(SimpleITK)
Dwidth <- grid::unit(10, "cm")
## version of show using a jet colourmap.
setMethod('show', '_p_itk__simple__Image',
function(object)
{
jet.colors <- colorRampPalette(c("#00007F", "blue", "#007FFF", "cyan", "#7FFF7F", "yellow", "#FF7F00", "red", "#7F0000"))
a <- t(as.array(object))
rg <- range(a)
A <- (a-rg[1])/(rg[2]-rg[1])
dd <- dim(a)
sp <- object$GetSpacing()
sz <- object$GetSize()
worlddim <- sp*sz
worlddim <- worlddim/worlddim[1]
W <- Dwidth
H <- Dwidth * worlddim[2]
jc <- jet.colors(101)
A <- jc[A*100 + 1]
dim(A) <- dim(a)
grid::grid.raster(A,default.units="mm", width=W, height=H)
}
)
## Make a smaller plot size
options(repr.plot.height = 3, repr.plot.width=3)
img = GaussianSource("sitkUInt8", size=c(64,64))
img
Here we note a problem with the swig bindings to the procedural interface - dispatching and defaults are not as nice as the python version. This means that we can't say
GaborSource("sitkFloat32", size=c(64,64), frequency=.03)
because there are arguments missing before frequency and swig can't generate default arguments from c++ code.
Lets start by accessing a single pixel - note that indexing starts from 1:
img[25,25]
img[17:49,]
## Don't have the "from the end" slicing - negatives mean remove
img[,16:(img$GetHeight() - 16)]
img = GaborSource("sitkFloat32", size=c(64,64), sigma=c(16, 16), mean=c(32,32), frequency=.03)
img
img[, -(1:16)]
img[1:32, 1:32]
corner=img[1:32, 1:32]
corner[corner$GetWidth():1,]
We cannot perform tiling via slicing directly on an image. The reason for this restriction is that an image is not equivalent to an array. Unlike an array, an image enforces the concept of physical spacing between pixels/voxels, this spacing has to be uniform for each of the axes. Thus a non-uniform slicing, e.g. (1,2,3,3,2,1), will generate an error.
corner[c(1:corner$GetWidth(), corner$GetWidth():1), c(1:corner$GetHeight(), corner$GetHeight():1)]
We can still easily perform the tiling operation we want by slightly abusing the system, converting the image to an array, performing the tiling and then converting the array back to an image. Note that the new image will have unit spacings for each axis, the concept of physical spacing is lost once we convert to an array.
The correct, maintaining the data as an image, way of performing this operation is to use a SimpleITK filter as shown below.
arr = as.array(corner)
as.image(arr[c(1:corner$GetWidth(), corner$GetWidth():1), c(1:corner$GetHeight(), corner$GetHeight():1)])
Tile(corner, corner[corner$GetWidth():1,],corner[,corner$GetHeight():1],corner[corner$GetWidth():1, corner$GetHeight():1], c(2,2))
Extracting a single slice of a 3D image is a natural extension:
img3d = GaborSource("sitkFloat32", size=rep(64,3), sigma=rep(16, 3), mean=rep(32,3), frequency=.05)
img3d[,,32]
img3d[16,,]
img3d[,seq(from=1, to=img3d$GetHeight(), by=3), 32]
Most array operations are overloaded for images - meaning that an ITK filter is used to perform the pixel-wise operations:
## source the functions for loading data
source("downloaddata.R")
cthead <- ReadImage(fetch_data("cthead1.png"))
img = Cast(cthead,"sitkFloat32")
img
img[150,150]
timg <- img**2
timg
timg[150,150]
aimg <- img + 100
aimg[150,150]
Flip the image, x axis. This also changes the x coordinate of the origin. When we try to add the two images we will fail, because they do not occupy the same physical space. Again, SimpleITK images are not arrays.
flipped_img <- img[img$GetWidth():1, ]
img + flipped_img
As we really want to add the intensity values of the two images, we can modify the meta-data of the flipped image to match that of the original. We know they have the same number of pixels on each axis, so we can do this safely.
flipped_img$CopyInformation(img)
img + flipped_img
img > 150
(img > 90) + (img > 150)
m <- (img > 90) != (img > 150)
img * Cast(m, "sitkFloat32")
SimpleITK's implementation of the mathematical and logical operators is computationally efficient. The following cells show that this is not an idle claim, we will time the same operations using SimpleITK images and R arrays.
Create three images and equivalent arrays. In this case all images have default values for origin (0,0,0), spacing (1,1,1) and cosine matrix (I).
sz <- 512
# Create a sz^3 image whose pixels are all set to one, then get the equivalent R array.
im1 <- Image(sz, sz, sz, "sitkFloat64") + 1
arr1 <- as.array(im1)
# Create an sz^3 array whose pixels are set to the sequence 1..sz^3, then get the equivalent SimpleITK image.
arr2 <- array(seq(1, sz^3, 1.0), c(sz, sz, sz))
im2 <- as.image(arr2)
# Create a sz^3 image whose pixels are all set to two, then get the equivalent R array.
im3 <- Image(sz, sz, sz, "sitkFloat64") + 2
arr3 <- as.array(im3)
Time the same mathematical and logical operations using SimpleITK and R. Results are in seconds. Interpret the results according to the R documentation: "The ‘user time’ is the CPU time charged for the execution of user instructions of the calling process. The ‘system time’ is the CPU time charged for execution by the system on behalf of the calling process."
What we really care about is 'elapsed time' the wall clock. Note that the CPU usage for the SimpleITK code is higher than for the R code, due to the underlying multi-threaded implementation, but the elapsed time is lower.
As an exercise, check that the two expressions below indeed return the same results. Note that the SimpleITK logical operator results are 0,1 , while the R results are TRUE, FALSE.
system.time(((im1 * im2) / im3) > 24)
system.time(((arr1 * arr2) / arr3) > 24)