R syntactic sugar

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.

In [1]:
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)
In [2]:
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.

Image slicing and indexing

Lets start by accessing a single pixel - note that indexing starts from 1:

In [3]:
img[25,25]
198

Cropping

In [4]:
img[17:49,]
In [5]:
## Don't have the "from the end" slicing - negatives mean remove 

img[,16:(img$GetHeight() - 16)]
In [6]:
img = GaborSource("sitkFloat32", size=c(64,64), sigma=c(16, 16), mean=c(32,32), frequency=.03)
img
In [7]:
img[, -(1:16)]
In [8]:
img[1:32, 1:32]

Flipping

In [9]:
corner=img[1:32, 1:32]
corner[corner$GetWidth():1,]

Tiling (using slicing)

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.

In [10]:
corner[c(1:corner$GetWidth(), corner$GetWidth():1), c(1:corner$GetHeight(), corner$GetHeight():1)]
Error in corner[c(1:corner$GetWidth(), corner$GetWidth():1), c(1:corner$GetHeight(), : X spacing is not uniform

Traceback:

1. corner[c(1:corner$GetWidth(), corner$GetWidth():1), c(1:corner$GetHeight(), 
 .     corner$GetHeight():1)]
2. corner[c(1:corner$GetWidth(), corner$GetWidth():1), c(1:corner$GetHeight(), 
 .     corner$GetHeight():1)]
3. stop("X spacing is not uniform\n")

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.

In [11]:
arr = as.array(corner)
as.image(arr[c(1:corner$GetWidth(), corner$GetWidth():1), c(1:corner$GetHeight(), corner$GetHeight():1)])

Tiling (with SimpleITK filters)

In [12]:
Tile(corner, corner[corner$GetWidth():1,],corner[,corner$GetHeight():1],corner[corner$GetWidth():1, corner$GetHeight():1], c(2,2))

Slice extraction

Extracting a single slice of a 3D image is a natural extension:

In [13]:
img3d = GaborSource("sitkFloat32", size=rep(64,3), sigma=rep(16, 3), mean=rep(32,3), frequency=.05)
In [14]:
img3d[,,32]
In [15]:
img3d[16,,]

Subsampling

In [16]:
img3d[,seq(from=1, to=img3d$GetHeight(), by=3), 32]

Mathematical Operators

Most array operations are overloaded for images - meaning that an ITK filter is used to perform the pixel-wise operations:

In [17]:
## source the functions for loading data
source("downloaddata.R")
cthead <- ReadImage(fetch_data("cthead1.png"))
img = Cast(cthead,"sitkFloat32")
img
Loading required package: rPython

Loading required package: RJSONIO

In [18]:
img[150,150]
140
In [19]:
timg <- img**2
timg
timg[150,150]
19600
In [20]:
aimg <- img + 100
aimg[150,150]
240

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.

In [21]:
flipped_img <- img[img$GetWidth():1, ]
img + flipped_img
Warning message in f(...):
“Exception thrown in SimpleITK Add: /private/var/folders/74/ly5m__ys0055hy655pf18nq4wkgrpj/T/Rtmp327XQK/R.INSTALL61399c73d2c/SimpleITK/SITK/Build/ITK-prefix/include/ITK-4.13/itkImageToImageFilter.hxx:241:
itk::ERROR: AddImageFilter(0x7ffabde57030): Inputs do not occupy the same physical space! 
InputImage Origin: [0.0000000e+00, 0.0000000e+00], InputImage_1 Origin: [2.5500000e+02, 0.0000000e+00]
	Tolerance: 1.0000000e-06
InputImage Direction: 1.0000000e+00 0.0000000e+00
0.0000000e+00 1.0000000e+00
, InputImage_1 Direction: -1.0000000e+00 0.0000000e+00
0.0000000e+00 1.0000000e+00

	Tolerance: 1.0000000e-06”
Error in f(...): Exception in SITK - check warning messages

Traceback:

1. img + flipped_img
2. img + flipped_img
3. Add(e1, e2)
4. f(...)
5. stop("Exception in SITK - check warning messages\n")

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.

In [22]:
flipped_img$CopyInformation(img)
img + flipped_img

Logic and comparison operators

In [23]:
img > 150
In [24]:
(img > 90) + (img > 150)
In [25]:
m <- (img > 90) != (img > 150)
In [26]:
img * Cast(m, "sitkFloat32")

It's about time

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).

In [27]:
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.

In [28]:
system.time(((im1 * im2) / im3) > 24)
system.time(((arr1 * arr2) / arr3) > 24)
   user  system elapsed 
  2.338   3.483   1.267 
   user  system elapsed 
  1.370   2.028   3.632