Search code examples
rscaleraster

Scale raster cell in stack from -1 to 1 R


I have a raster stack with 364 layers with a daily rate of change in NDVI values.

I want to scale these values in every cell if positive from 0 to 1 and if negative from -1 to 0. So far I have only found a solution that scale values in single layers (see here: Replace specific value in each band of raster brick in R) and not along cells of multilayer objects. Additionally I have a decent amount of cells with NA for the entire time series and I'm not quite sure how to deal with this fact either.

I took the code from the previously mentioned post and tried to get it working for my problem:

norm <- function(x){-1+(x-min)*((1-(-1))/(max-min))}

for(j in 1:ncell(tif)){

 if(is.na(sum(tif[j]))){
  NULL
 } else {

 cat(paste("Currently processing layer:", j,"/",ncell(tif), "\n"))

 min <- cellStats(tif[j],'min')
 max <- cellStats(tif[j],'max')

#initialize cluster
#number of cores to use for clusterR function (max recommended: ncores - 1)
beginCluster(31)

#normalize
tif[j] <- clusterR(tif[j], calc, args=list(fun=norm), export=c('min',"max"))

#end cluster
endCluster()
}
}

I'm not quite certain if this produces the desired output. Any help is very much appreciated!


Solution

  • Some example data

    library(raster)
    r <- raster(ncol=10, nrow=10)
    s <- stack(lapply(1:5, function(i) setValues(r, runif(100, -1, 1))))
    # adding NAs
    s[[2]][sample(100, 25, TRUE)] <- NA
    

    For scaling (or any other operation) by cell (as requested) you can use calc together with a function that works on a vector. For example:

    ff <- function(i) {
        p <- which(i >= 0)
        n <- which(i <= 0)
        # positive values
        if (length(p) > 0) {
            i[p] <- i[p] - min(i[p], na.rm=TRUE)
            i[p] <- i[p] / max(i[p])
        }
        # negative values
        if (length(n) > 0) {
            i[n] <- i[n] - max(i[n], na.rm=TRUE)
            i[n] <- i[n] / abs(min(i[n]))
        }
        i
    }
    

    Test it

    ff(c(-.3, -.1, .1, .4, .8))
    #[1] -1.0000000  0.0000000  0.0000000  0.4285714  1.0000000
    ff(c(-.3, -.1, .1, .4, .8, NA))
    #[1] -1.0000000  0.0000000  0.0000000  0.4285714  1.0000000         NA
    ff(c(-2,-1))
    #[1] -1  0
    ff(c(NA, NA))
    #[1] NA NA
    

    And use it

    z <- calc(s, ff)
    

    See the below to scale by layer, based on the min and max of all cell values (I first thought that this is what was asked for). Note that the functions I used below scale values from -1 to 1, but not the lowest positive value and highest negative value to zero.

    minv <- abs(cellStats(s,'min'))
    maxv <- cellStats(s,'max')
    
    f1 <- function(i, mn, mx) {
        j <- i < 0
        j[is.na(j)] <- TRUE
        i[j] <- i[j] / abs(mn)
        i[!j] <- i[!j] / mx
        i
    }
    
    ss <- list()
    for (i in 1:nlayers(s)) {
        ss[[i]] <- calc(s[[i]], fun=function(x) f1(x, minv[i], maxv[i]))
    }
    
    ss1 <- stack(ss)
    

    Or without a loop

    f2 <- function(x, mn, mx) {
        x <- t(x)
        i <- which(x > 0)
        i[is.na(i)] <- FALSE
        mxx <- x / mx
        x <- x / mn
        x[i] <- mxx[i]
        t(x)
    }
    
    ss2 <- calc(s, fun=function(x) f2(x, minv, maxv))
    

    For reference, to simply scale between 0 and 1

    mnv <- cellStats(s,'min')
    mxv <- cellStats(s,'max')
    x <- (s - mnv) / (mxv - mnv)
    

    To get values between -1 and 1 you can then do

    y <- 2 * (x - 1)
    

    But that way previously negative values can become positive and vice versa.

    See ?raster::scale for other types of scaling.