#' Calculate blood perfusion using deconvolution.
#'
#' Implementation of the deconvolution technique of Ostergaard et al.
#' (http://www.ncbi.nlm.nih.gov/pubmed/8916023) for calculating cerebral
#' (or pulmonary) blood flow. Other relevant references include
#' http://www.ncbi.nlm.nih.gov/pubmed/16261573,
#' http://www.ncbi.nlm.nih.gov/pubmed/8916023, and
#' http://www.ncbi.nlm.nih.gov/pubmed/15332240.
#'
#' @param perfusionImage time series (n-D + time) perfusion acquisition.
#' @param voiMaskImage n-D mask image indicating where the cerebral blood
#' flow parameter images are calculated.
#' @param aifMaskImage n-D mask image indicating where the arterial input
#' function is calculated.
#' @param thresholdSVD is used to threshold the smaller elements of the
#' diagonal matrix during the SVD regularization. 0.2 is a common choice
#' (cf. page 571, end of column 2 in
#' http://www.ncbi.nlm.nih.gov/pubmed/16971140).
#' @param deltaTime time between volumetric acquisitions. We assume a uniform
#' time sampling.
#'
#' @return list with the cerebral blood flow image (cbfImage), cerebral blood
#' volume image (cbvImage), mean transit time (mttImage), and arterial input
#' function signal from the image (aifSignal) and the calculated arterial input
#' function concentration (aifConcentration).
#'
#' @author Tustison NJ
#'
#' @examples
#' \dontrun{
#'
#' perfusionFileName <- ""
#' if (file.exists(perfusionFileName)) {
#' perfusionImage <- antsImageRead(
#' filename = perfusionFileName,
#' dimension = 4, pixeltype = "float"
#' )
#' voiMaskImage <- antsImageRead(
#' filename = voiMaskFileName,
#' dimension = 3, pixeltype = "unsigned int"
#' )
#' aifMaskImage <- antsImageRead(
#' filename = aifMaskFileName,
#' dimension = 3, pixeltype = "unsigned int"
#' )
#'
#'
#' deltaTime <- 3.4
#'
#' results <- bloodPerfusionSVD(perfusionImage,
#' voiMaskImage, aifMaskImage,
#' thresholdSVD = 0.2, deltaTime = deltaTime
#' )
#'
#' antsImageWrite(results$cbfImage, paste0("cbf.nii.gz"))
#' antsImageWrite(results$cbvImage, paste0("cpbv.nii.gz"))
#' antsImageWrite(results$mttImage, paste0("mtt.nii.gz"))
#' }
#' }
#' @export
bloodPerfusionSVD <- function(
perfusionImage, voiMaskImage, aifMaskImage,
thresholdSVD = 0.2, deltaTime = 1.0) {
if (missing(perfusionImage)) {
stop("Error: The perfusion image is not specified.\n")
}
if (missing(voiMaskImage)) {
stop("Error: The VOI mask image is not specified.\n")
}
if (missing(aifMaskImage)) {
aifOutput <- generateAifMaskImage(perfusionImage, voiMaskImage)
aifMaskImage <- aifOutput$aifMaskImage
}
# The AIF signal is defined as the mean intensity value over the AIF
# region of interest at each time point
aifMatrix <- timeseries2matrix(perfusionImage, aifMaskImage)
Saif <- NULL
if (ncol(aifMatrix) == 1) {
Saif <- as.vector(aifMatrix)
} else {
Saif <- rowMeans(aifMatrix, na.rm = TRUE)
}
# Automatically find start of the AIF signal by finding the point at
# which the signal intensity curve exceeds 10% of the maximum
SaifMaxIndex <- which(Saif == max(Saif))
SaifStartIndex <- tail(which(Saif[1:SaifMaxIndex] <
0.1 * (max(Saif) - min(Saif))), n = 1) + 1
if (length(SaifStartIndex) == 0) {
SaifStartIndex <- which(Saif == min(Saif))
}
S0aif <- mean(Saif[1:(SaifStartIndex - 1)], na.rm = TRUE)
# See http://www.ncbi.nlm.nih.gov/pubmed/16261573, page 711,
# equation (3). Note that we exclude the constant of proportionality, k,
# and the TE parameters as they cancel out in estimating the residue
# function.
Caif <- -log(Saif / S0aif)
# If we can estimate the product CBF * R(t) we obtain an estimate for
# CBF because R(0) = 1. CBF * dT * R = ( V * L^-1 * U^T ) * Cvoi (cf
# http://www.ncbi.nlm.nih.gov/pubmed/8916023, page 713, equation (9)).
dSVD <- deconvolutionSVD(Caif, thresholdSVD)
# Measured signal over the entire region of interest
Svoi <- timeseries2matrix(perfusionImage, voiMaskImage)
numberOfTimePoints <- nrow(Svoi)
# Baseline signal
S0voi <- colMeans(Svoi[1:SaifStartIndex, ], na.rm = TRUE)
S0voi <- matrix(rep(S0voi, numberOfTimePoints),
nrow = numberOfTimePoints, byrow = TRUE
)
# See http://www.ncbi.nlm.nih.gov/pubmed/16261573, page 711, equation (3).
# Note that we exclude the constant of proportionality, k, and the TE
# parameters as they cancel out in estimating the residue function.
Cvoi <- -log(Svoi / S0voi)
residueFunction <- dSVD %*% Cvoi / deltaTime
residueFunction[residueFunction < 0.0] <- 0.0
# cbf is the maximum of the residue function at each voxel
.colMax <- function(data) apply(data, 2, max, na.rm = TRUE)
cbf <- .colMax(residueFunction)
cbfOutputImage <- matrixToImages(
as.matrix(t(cbf)),
antsImageClone(voiMaskImage, "float")
)[[1]]
# cbv is area under the curve at each voxel using the trapezoidal rule
cbv <- trapz(seq(
from = 0.0, by = deltaTime,
length.out = nrow(residueFunction)
), residueFunction)
cbvOutputImage <- matrixToImages(
as.matrix(cbv),
antsImageClone(voiMaskImage, "float")
)[[1]]
mtt <- cbv / cbf
mtt[which(is.na(mtt))] <- 0.0
mttOutputImage <- matrixToImages(
as.matrix(mtt),
antsImageClone(voiMaskImage, "float")
)[[1]]
return(list(
cbfImage = cbfOutputImage,
cbvImage = cbvOutputImage,
mttImage = mttOutputImage,
aifSignal = Saif,
aifConcentration = Caif
))
}
#' Calculate the area under a sampled curve (or set of curves).
#'
#' Given a vector (or a matrix) representing a curve (or set of
#' curves, columnwise),
#' the area (or set of areas) is calculated using the trapezoidal rule.
#'
#' @param x vector of samples for the dependent variable.
#' @param y vector or matrix of samples for the independent variable.
#' In the case of the latter, curves are organized column-wise.
#'
#' @return area (areas) under the sampled curve (curves).
#'
#' @author Tustison NJ
#'
#' @examples
#'
#' x <- seq(0, 1, by = 0.0001)
#' y <- exp(x)
#'
#' # Compare with true area of exp( 1 ) - 1 = 1.718282...
#' areaEstimate <- trapz(x, y)
#' @export
trapz <- function(x, y) {
idx <- 2:length(x)
if (is.vector(y)) {
return(0.5 * ((x[idx] - x[idx - 1]) %*% (y[idx] + y[idx - 1])))
} else {
return(0.5 * ((x[idx] - x[idx - 1]) %*% (y[idx, ] + y[idx - 1, ])))
}
}
#' Regularize SVD (deconvolution) solution of cerebral blood flow.
#'
#' Ostergaard's regularization approach for a model independent solution of
#' cerebral blood flow using a blood pool contrast agent
#' (http://www.ncbi.nlm.nih.gov/pubmed/8916023).
#'
#' @param arterialInputFunction vector specifying the arterial input function
#' over time.
#' @param thresholdSVD is used to threshold the smaller elements of the
#' diagonal matrix during the SVD regularization. 0.2 is a common choice
#' (cf. page 571, end of column 2 in
#' http://www.ncbi.nlm.nih.gov/pubmed/16971140).
#'
#' @return regularized residue function, i.e. the right-hand side of
#' CBF * dT * R = ( V * L^-1 * U^T ) * Cvoi.
#'
#' @author Tustison NJ
#'
#' @examples
#'
#' S0aif <- 17.62
#' Saif <- c(
#' 16.25, 16.37, 20.22, 78.96, 230.5, 249.79, 198.58, 147.76,
#' 110.39, 111.64, 129.43
#' )
#' Caif <- -log(Saif / S0aif)
#'
#' dSVD <- deconvolutionSVD(Caif, 0.2)
#'
#' @export
deconvolutionSVD <- function(arterialInputFunction, thresholdSVD = 0.2) {
if (!is.vector(arterialInputFunction)) {
stop("Expecting a vector.")
}
N <- length(arterialInputFunction)
Caif <- mat.or.vec(N, N)
for (j in 1:N)
{
Caif[j:N, j] <- arterialInputFunction[1:(N - j + 1)]
}
S <- svd(Caif)
Dinv <- 1.0 / S$d
Dinv[which(S$d < thresholdSVD * max(S$d))] <- 0.0
dSVD <- S$v %*% diag(Dinv) %*% t(S$u)
}
#' Automatically generate an arterial input function mask.
#'
#' Automated approach for generating a mask to be used for modeling the
#' arterial input function using the method described in
#' https://www.ncbi.nlm.nih.gov/pubmed/15956519.
#'
#' @param perfusionImage time series (n-D + time) perfusion acquisition.
#' @param voiMaskImage n-D mask image indicating where the cerebral blood
#' flow parameter images are calculated.
#' @param index If the user prefers a specific voxel if looking at the
#' plots, they can simply specify the index (gleaned from the file names)
#' and the corresponding AIF mask image is created.
#'
#' @return list( mask image, fitting results )
#'
#'
#' @author Tustison NJ
#'
#' @export
generateAifMaskImage <- function(
perfusionImage, voiMaskImage,
index = NA) {
fitGaussianFunction <- function(x, y, mean, logStd, scale, offset) {
f <- function(p) {
d <- p[3] * dnorm(x, mean = p[1], sd = exp(p[2])) + p[4]
error <- sum((d - y)^2, na.rm = TRUE) / length(d)
return(error)
}
optimization <- optim(c(mean, logStd, scale, offset), f)
return(optimization)
}
voiPerfusionMatrix <- timeseries2matrix(perfusionImage, voiMaskImage)
if (!is.na(index)) {
aifMatrix <- matrix(voiPerfusionMatrix[1, ], nrow = 1) * 0
aifMatrix[1, index] <- 1
aifMaskImage <- matrixToImages(aifMatrix, voiMaskImage)[[1]]
return(list(aifMaskImage = aifMaskImage))
}
# As we iterate through each voxel and look at the time series, we:
# 1. fit the image time-series intensities to a gaussian (mean, std,
# scale, and offset). Note that the time series spacing is 1
# although we might want to add this as a parameter.
# 2. we prune candidate voxels based on the fit. We exclude voxels
# where
# a. the fitted mean is less than the minTime or greater than
# the maxTime,
# b. the fitted std is < 1.5 or > 5. This is purely based on
# looking at some data which we might want to revisit,
# c. the fitted scale is less than 10.0,
# d. we test the goodness of fit with the estimated model
# using the Chi-squared test and exclude any voxels where
# p < 0.1.
headers <- c(
"Index", "GaussianFitMean", "GaussianFitStd", "GaussianFitScale",
"GaussianFitOffset", "GoodnessOfFit"
)
fittingResults <- data.frame(matrix(
data = NA,
nrow = ncol(voiPerfusionMatrix), ncol = length(headers)
))
colnames(fittingResults) <- headers
timePoints <- seq_len(nrow(voiPerfusionMatrix))
minTime <- 0.0
maxTime <- 1.0 * length(timePoints)
initialMean <- 0.5 * length(timePoints)
initialStandardDeviation <- 1.0
initialOffset <- 0.0
count <- 1
for (i in seq_len(ncol(voiPerfusionMatrix)))
{
# convert signal to concentration (ignore 1/TE factor).
concentrationData <- -log((voiPerfusionMatrix[, i]) /
(voiPerfusionMatrix[1, i]))
if (any(concentrationData < 0)) {
next
}
gauss <-
tryCatch(
{
fitGaussianFunction(
x = timePoints, y = concentrationData,
mean = initialMean, logStd = log(initialStandardDeviation),
scale = 0.5 * max(concentrationData), offset = initialOffset
)
},
error = function(cond) {
return(NA)
}
)
if (length(gauss) == 1 && is.na(gauss)) {
next
}
gauss$par[2] <- exp(gauss$par[2])
if (gauss$par[1] < minTime || gauss$par[1] > maxTime ||
gauss$par[2] < 1.5 || gauss$par[2] > 5.0 ||
gauss$par[3] < 10 || gauss$par[4] < 0) {
next
}
modelData <- gauss$par[3] * dnorm(timePoints,
mean = gauss$par[1], sd = gauss$par[2]
) +
gauss$par[4]
goodnessOfFit <- suppressWarnings(chisq.test(
x = concentrationData,
p = modelData, rescale.p = TRUE, simulate.p.value = TRUE
))
if (goodnessOfFit$p.value < 0.1) {
next
}
fittingResults$Index[count] <- i
fittingResults$GaussianFitMean[count] <- gauss$par[1]
fittingResults$GaussianFitStd[count] <- gauss$par[2]
fittingResults$GaussianFitScale[count] <- gauss$par[3]
fittingResults$GaussianFitOffset[count] <- gauss$par[4]
fittingResults$GoodnessOfFit[count] <- goodnessOfFit$p.value
count <- count + 1
}
deletedColumns <- seq.int(from = count, to = nrow(fittingResults), by = 1)
fittingResults <- fittingResults[-deletedColumns, ]
if (nrow(fittingResults) == 0) {
warning("Empty results. No voxels survived criteria.")
return()
}
# We sort the remaining voxels by the scale of the fitted model and
# choose the voxel with the highest scale.
fittingResults <- fittingResults[order(fittingResults$GaussianFitScale), ]
aifMatrix <- matrix(voiPerfusionMatrix[1, ], nrow = 1) * 0
aifMatrix[1, fittingResults$Index[1]] <- 1
aifMaskImage <- matrixToImages(aifMatrix, voiMaskImage)[[1]]
return(list(aifMaskImage = aifMaskImage, fittingResults = fittingResults))
}
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.