library( knitr ) knitr::opts_chunk$set(collapse = T, comment = "#>") library(ANTsR) library(ggplot2) library(grid) if (requireNamespace("parallel", quietly = TRUE)) { library(parallel) }
This document provides some examples illustrating how ANTsR may be used to work with multi channel images, such as rgb (i.e. color) data. This is still an extremely new feature and much of ANTsR does not yet support this image type. Examples will be added to this document as new functionality is implemented.
# How to plot a 2D color image plotColor <- function(imgList, scale=TRUE, vectors=NULL, points=NULL, paths=NULL) { if (class(imgList) == "antsImage") { imgList = list(imgList, imgList, imgList) } direction = antsGetDirection( imgList[[1]] ) # max in all images maxi = 1.0 if ( scale ) { maxi = max( unlist( lapply( imgList, function(x) { max(x) } ) ) ) } rgbList = lapply( imgList, function(x) { apply(t(as.matrix(x)),2,rev) / maxi }) rgbList = lapply( imgList, function(x) { t(as.matrix(x)) / maxi }) col <- rgb(rgbList[[1]], rgbList[[2]], rgbList[[3]]) d = dim(rgbList[[1]]) x = rep(1:d[2],each=d[1]) y = rep(1:d[1], d[2]) pts = antsTransformIndexToPhysicalPoint( imgList[[1]], cbind(x,y) ) dat = data.frame(x=pts[,1], y=pts[,2], col=col) x1 = min(pts[,1]) x2 = max(pts[,1]) y1 = min(pts[,2]) y2 = max(pts[,2]) g = ggplot(dat) + geom_raster(aes(x=x, y=y, fill=col), hjust=0, vjust=0, alpha=1) + theme(legend.position="none", aspect.ratio=1,text=element_blank(),axis.ticks=element_blank(), panel.grid=element_blank() ) + scale_fill_manual(values=as.character(levels(factor(col))) ) g = g + coord_cartesian( xlim=c(x1,x2), ylim=c(y1,y2) ) if ( direction[1,1] > 0 ) { g = g + scale_x_continuous( lim=c(x1,x2) ) } else { g = g + scale_x_reverse( lim=c(x2,x1) ) } if ( direction[2,2] > 0 ) { g = g + scale_y_continuous( lim=c(y1,y2) ) } else { g = g + scale_y_reverse( lim=c(y2,y1) ) } if ( !is.null(points) ) { pdat = data.frame( x=points[,1], y=points[,2], id=factor(1:dim(points)[1]) ) g = g + geom_point( data=pdat, aes(x=x, y=y, colour=id )) } if ( !is.null(paths) ) { g = g + geom_path(data=paths, aes(x=x,y=y,group=id,colour=id)) } if ( !is.null(vectors) ) { xvec = as.vector( t(as.matrix(vectors[[1]])) ) yvec = as.vector( -t(as.matrix(vectors[[2]])) ) vpts = antsTransformIndexToPhysicalPoint( imgList[[1]], cbind(x+0.5,y+0.5) ) mag = sqrt(xvec*xvec + yvec*yvec) elim = which(mag < 0.01) if (length(elim) > 0 ) { xvec = xvec[-elim] yvec = yvec[-elim] vpts = vpts[-elim,] } vdat = data.frame(x=vpts[,1]-xvec, y=vpts[,2]-yvec, xend=vpts[,1]+xvec, yend=vpts[,2]+yvec) g = g + geom_segment(data=vdat, aes(x=x,y=y,xend=xend,yend=yend), colour="white", alpha=0.5) } suppressWarnings(print(g)) }
Because ANTsR relies upon ITK for image IO, multichannel support is inherently built in. Basic conversions and math operations work as expected.
# Read in and display header info img = antsImageRead( getANTsRData("decslice")) img img[64,64] # Convert to an array arr = as.array(img) dim(arr) # Convert array to multichannel antsImage img2 = as.antsImage(arr, components=TRUE) img2 # Convert array to antsImage and set header info via reference image img2 = as.antsImage(arr, components=TRUE, reference=img ) img2 # Basic math ops work the same as for scalar images 2 * sum(img) sum(img * 2)
Many functions only work on scalar images, so here we split a multichannel image into a list
of scalar images, then the lapply
function provides a convenient way to process all channels.
# Convert to list of scalar images iList = splitChannels(img) plotColor(iList) sList = lapply( iList, function(x) { smoothImage(x, 1.5) } ) plotColor(sList) # Merge back into multichannel image simg = mergeChannels(sList) # Write to file # antsImageWrite(simg, "smoothslice.nii.gz")
It is worth mentioning that ANTsR is not yet compatible with mclapply
as provided
in the "parallel" package. It's something we will be looking into in the future. The
main problem has to do with allocating new images, some functions that only access
existing data will work, however it does not guarantee a faster execution time.
iMeans = 0 time1 = 0 timeParallel = NA # if (requireNamespace("pbapply", quietly = TRUE)) { # ptm = proc.time() # iMeans = pbapply::pblapply( iList, function(x) mean(x)) # iMeans = mclapply( iList, function(x) mean(x), mc.cores=min(detectCores(),2L) ) # timeParallel = proc.time() - ptm # unlist(iMeans) # } ptm = proc.time() iMeans = lapply(iList, function(x) mean(x) ) timeSerial = proc.time() - ptm unlist(iMeans) timeParallel timeSerial
dt = antsImageRead(getANTsRData("dtislice")) dtList = splitChannels(dt) # Component of tensor are stored as: XX, XY, XZ, YY, YZ, ZZ trace = dtList[[1]] + dtList[[4]] + dtList[[6]] trace[trace<0] = 0 plotColor( trace )
We only want to deal with voxels in the brain, so the getMask
function is used
to obtain rough estimate of the brain, and the image is plotted with background voxels
tinted red.
mask = getMask(trace) plotColor( list(trace, trace*mask, trace*mask))
Since we are only interested in voxels in the brain, a matrix is created where each row is voxel in the brain and each column in a tensor component, listed in upper.tri order. This allows us to quickly calculate the eigen decomposition for each tensor.
# list-based call to convert values to an array mat = do.call(rbind, lapply( dtList, function(x) { x[mask>0] } ) ) # simplest convesion to array # mat = as.array(dt, mask == 1) # convert tensor from vector to matrix initTensor <- function(x) { tens = diag(3) tens[lower.tri(tens, diag=TRUE)] = x tens = tens + t(tens) - diag(diag(tens)) return(tens) } getFractionalAnisotropy <- function(evs) { numer = sqrt( (evs[1]-evs[2])^2 + (evs[2]-evs[3])^2 + (evs[3]-evs[1])^2 ) denom = sqrt( sum(evs*evs) ) fa = 0 if ( denom > 0 ) { fa = sqrt(0.5) * numer/denom } fa } # Eigen decomposition for each tensor # eigs = unlist( apply(mat, 2, function(x) { # eigen( initTensor(x) ) # } ) # ) all_eigs = apply(mat, 2, function(x) { eigen( initTensor(x) ) } ) eigs = lapply(all_eigs, function(x) { x$values }) eigs = lapply(eigs, unlist) evalMat = do.call(rbind, eigs) evalMat[evalMat < 0 ] = 0 # Create images from the eigenvalues eval1 = makeImage(mask, evalMat[,1]) eval2 = makeImage(mask, evalMat[,2]) eval3 = makeImage(mask, evalMat[,3]) plotColor( list(eval1, eval2, eval3))
The Fractional Anisotropy (FA) is typically used to measure how directionally specific the diffusion of water is within a voxel. This values is plotted below
faValues = apply(evalMat, 1, getFractionalAnisotropy ) faValues[faValues < 0] = 0 fa = makeImage(mask, faValues) plotColor( fa )
Also of great interest is the eigenvector associated with the largest eigenvalue, this estimates the primary direction of diffusion (PDD) and in white matter this corresponds to the direction that is parallel to the myelinated axons in a fiber bundle
eig_vecs = lapply(all_eigs, function(x) { x$vectors }) eig_vecs = lapply(eig_vecs, c) vecMat = do.call(rbind, eig_vecs) evecx = makeImage(mask, vecMat[,1]) evecy = makeImage(mask, vecMat[,2]) evecz = makeImage(mask, vecMat[,3]) decList = list(evecx, evecy, evecz) decList = lapply(decList, abs) plotColor( decList )
Weighting the magnitude of the PDD by the FA is standard practice as it highlights regions with high directional specificity (i.e. white matter). This is known as a directional encoded colormap (DEC) [@Pajevic1999]
decList = lapply( decList, function(x){ fa*x } ) plotColor(decList)
To verify our processing, it can be useful to plot line segments showing the direction of some of the vectors. This is done in a subset of the image to avoid too much visual clutter.
lower = c(40,80) upper = c(80,120) subfa = cropIndices(fa, lowerind = lower, upperind = upper) subVecList = list(evecx, evecy, evecz) subVecList = lapply( subVecList, function(x){ cropIndices(x, lowerind = lower, upperind = upper)}) subDecList = lapply( subVecList, function(x){ abs(subfa*x) }) plotColor( subDecList, vectors=list(subVecList[[1]], subVecList[[2]]) )
A common use for DTI is fiber tractography [@Basser2000]. Here we give a simplified example in which we restrict the tracts to lie within the slice. The first step is define a set of seeds which serve as the starting points for our tractography. Often, all points in the white matter are used as seed for "whole-brain tracking." Here we manually define a small set of seed points for illustration.
# indices of seed points seedIndices = rbind( c(61,92), c(62,92), c(63,92) ) seedIndices = rbind( seedIndices, c(64,92), c(65,92), c(65,92) ) seedIndices = rbind( seedIndices, c(61,93), c(62,93), c(63,93) ) seedIndices = rbind( seedIndices, c(64,93), c(65,93), c(65,93) ) # convert to physical space points seedPts = antsTransformIndexToPhysicalPoint(fa, seedIndices) plotColor( subfa, points=seedPts )
trackFromSeed <- function(vecs, fa, seed) { stepSize = 0.2 faThresh = 0.2 iPt = seed pts = iPt idx = antsTransformPhysicalPointToIndex(fa, seed) faValue = fa[idx[1], idx[2]] ppdx = vecs[[1]][ idx[1], idx[2] ] ppdy = vecs[[2]][ idx[1], idx[2] ] lastVec = as.vector(antsGetDirection(fa) %*% as.matrix(c(ppdx,ppdy))) iVec = lastVec while ( faValue > faThresh ) { iVec = iVec / sqrt(sum(iVec*iVec)) if ( sum(iVec*lastVec) < 0 ) { iVec = -iVec } iPt = iPt + stepSize*iVec pts = rbind(pts, iPt) idx = antsTransformPhysicalPointToIndex(fa, iPt) faValue = fa[idx[1], idx[2]] lastVec = iVec ppdx = vecs[[1]][ idx[1], idx[2] ] ppdy = vecs[[2]][ idx[1], idx[2] ] iVec = as.vector(antsGetDirection(fa) %*% as.matrix(c(ppdx,ppdy))) } iPt = seed idx = antsTransformPhysicalPointToIndex(fa, seed) faValue = fa[idx[1], idx[2]] vecs = lapply( vecs, function(x){ -x }) ppdx = vecs[[1]][ idx[1], idx[2] ] ppdy = vecs[[2]][ idx[1], idx[2] ] lastVec = as.vector(antsGetDirection(fa) %*% as.matrix(c(ppdx,ppdy))) iVec = lastVec while ( faValue > faThresh ) { iVec = iVec / sqrt(sum(iVec*iVec)) if ( sum(iVec*lastVec) < 0 ) { iVec = -iVec } iPt = iPt + stepSize*iVec pts = rbind(iPt, pts) idx = antsTransformPhysicalPointToIndex(fa, iPt) faValue = fa[idx[1], idx[2]] lastVec = iVec ppdx = vecs[[1]][ idx[1], idx[2] ] ppdy = vecs[[2]][ idx[1], idx[2] ] iVec = as.vector(antsGetDirection(fa) %*% as.matrix(c(ppdx,ppdy))) } return(data.frame(x=pts[,1],y=pts[,2])) } trackx = c() tracky = c() trackid = c() for ( i in 1:dim(seedPts)[1] ) { track = trackFromSeed(list(evecx,evecy), fa, seedPts[i,]) trackx = c(trackx,track$x) tracky = c(tracky,track$y) trackid = c(trackid, rep(i, length(track$x))) } ldat = data.frame(x=trackx, y=tracky, id=factor(trackid)) plotColor(subDecList, paths=ldat, points=seedPts, vectors=list(subVecList[[1]], subVecList[[2]])) plotColor(subfa, paths=ldat, points=seedPts)
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.